From f1355358c0254b502642c3a6da53c22b1e8656d5 Mon Sep 17 00:00:00 2001 From: Bogdan Drutu Date: Tue, 7 May 2019 14:13:31 -0700 Subject: [PATCH 0001/1517] Initial commit --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 202 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..15851d987e --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# opentelemetry-python \ No newline at end of file From 19a0715a3ef970524d3f320bdc6627d91cc6945b Mon Sep 17 00:00:00 2001 From: Sergey Kanzhelev Date: Fri, 10 May 2019 12:13:39 -0700 Subject: [PATCH 0002/1517] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 15851d987e..8e5aa15454 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ -# opentelemetry-python \ No newline at end of file +# opentelemetry-python + + +[![Join the chat at Python SIG gitter](https://badges.gitter.im/open-telemetry/opentelemetry-python.svg)](https://gitter.im/open-telemetry/opentelemetry-python?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) From ed980f4ac51405c7363588e10b953ea1d14679eb Mon Sep 17 00:00:00 2001 From: Sergey Kanzhelev Date: Fri, 10 May 2019 12:14:06 -0700 Subject: [PATCH 0003/1517] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e5aa15454..cf051fca93 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ # opentelemetry-python -[![Join the chat at Python SIG gitter](https://badges.gitter.im/open-telemetry/opentelemetry-python.svg)](https://gitter.im/open-telemetry/opentelemetry-python?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +Join the chat at Python SIG gitter: [![Join the chat at Python SIG gitter](https://badges.gitter.im/open-telemetry/opentelemetry-python.svg)](https://gitter.im/open-telemetry/opentelemetry-python?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) From 65ded17b4f220312ad1dd8185dd5727670764c3d Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Fri, 31 May 2019 14:33:49 -0700 Subject: [PATCH 0004/1517] Add issue templates (#7) --- .github/ISSUE_TEMPLATE/bug_report.md | 19 +++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..832b294a94 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,19 @@ +--- +name: Bug Report +about: Create a report to help us improve +labels: bug +--- + +**Describe your environment** Describe any aspect of your environment relevant to the problem, including your Python version, [platform](https://docs.python.org/3/library/platform.html), version numbers of installed dependencies, information about your cloud hosting provider, etc. If you're reporting a problem with a specific version of a library in this repo, please check whether the problem has been fixed on master. + +**Steps to reproduce** +Describe exactly how to reproduce the error. Include a code sample if applicable. + +**What is the expected behavior?** +What did you expect to see? + +**What is the actual behavior?** +What did you see instead? + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..fb456ec4df --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature Request +about: Suggest an idea for this project +labels: feature-request +--- + +Before opening a feature request against this repo, consider whether the feature should/could be implemented in the [other OpenCensus client libraries](https://github.com/census-instrumentation). If so, please [open an issue on opencensus-specs](https://github.com/census-instrumentation/opencensus-specs/issues/new) first. + +**Is your feature request related to a problem?** +If so, provide a concise description of the problem. + +**Describe the solution you'd like** +What do you want to happen instead? What is the expected behavior? + +**Describe alternatives you've considered** +Which alternative solutions or features have you considered? + +**Additional context** +Add any other context about the feature request here. From 10df89410051c87d5071bd4cc8be6d6f63446889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 4 Jun 2019 23:18:59 +0200 Subject: [PATCH 0005/1517] Link to OpenTelemetry from issue template (#10) --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index fb456ec4df..973549ab2d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,7 @@ about: Suggest an idea for this project labels: feature-request --- -Before opening a feature request against this repo, consider whether the feature should/could be implemented in the [other OpenCensus client libraries](https://github.com/census-instrumentation). If so, please [open an issue on opencensus-specs](https://github.com/census-instrumentation/opencensus-specs/issues/new) first. +Before opening a feature request against this repo, consider whether the feature should/could be implemented in the [other OpenTelemetry client libraries](https://github.com/open-telemetry/). If so, please [open an issue on opentelemetry-specification](https://github.com/open-telemetry/opentelemetry-specification/issues/new) first. **Is your feature request related to a problem?** If so, provide a concise description of the problem. From a1c5ab4b310df9a9e4066c3cc3055ff477303b5c Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 20 Jun 2019 11:17:54 -0700 Subject: [PATCH 0006/1517] Add notes and contribution info to readme (#16) --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cf051fca93..867773593f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,18 @@ -# opentelemetry-python +# OpenTelemetry Python +[![Gitter chat][gitter-image]][gitter-url] +[gitter-image]: https://badges.gitter.im/open-telemetry/opentelemetry-python.svg +[gitter-url]: https://gitter.im/open-telemetry/opentelemetry-python?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge -Join the chat at Python SIG gitter: [![Join the chat at Python SIG gitter](https://badges.gitter.im/open-telemetry/opentelemetry-python.svg)](https://gitter.im/open-telemetry/opentelemetry-python?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +The Python [OpenTelemetry](https://opentelemetry.io/) client. + +## Contributing + +The Python special interest group (SIG) meets regularly. See the OpenTelemetry +[community](https://github.com/open-telemetry/community#python-sdk) repo for +information on this and other language SIGs. + +See the [public meeting +notes](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit) +for a summary description of past meetings. To request edit access join the +meeting or get in touch on Gitter. From d1cea92de23dc4cb2230016b52f01fc7b5eafd8b Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 20 Jun 2019 11:19:24 -0700 Subject: [PATCH 0007/1517] Add initial tracer API (#11) This PR adds the tracing API package and proposes some conventions for the package structure, documentation, and style of the project. It borrows heavily from opentelemetry-java and opencensus-python. --- opentelemetry/__init__.py | 0 opentelemetry/api/__init__.py | 0 opentelemetry/api/trace/__init__.py | 242 ++++++++++++++++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 opentelemetry/__init__.py create mode 100644 opentelemetry/api/__init__.py create mode 100644 opentelemetry/api/trace/__init__.py diff --git a/opentelemetry/__init__.py b/opentelemetry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry/api/__init__.py b/opentelemetry/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry/api/trace/__init__.py b/opentelemetry/api/trace/__init__.py new file mode 100644 index 0000000000..6e4fc80f42 --- /dev/null +++ b/opentelemetry/api/trace/__init__.py @@ -0,0 +1,242 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The OpenTelemetry tracing API describes the classes used to generate +distributed traces. + +The :class:`.Tracer` class controls access to the execution context, and +manages span creation. Each operation in a trace is represented by a +:class:`.Span`, which records the start, end time, and metadata associated with +the operation. + +This module provides abstract (i.e. unimplemented) classes required for +tracing, and a concrete no-op ``BlankSpan`` that allows applications to use the +API package alone without a supporting implementation. + +The tracer supports creating spans that are "attached" or "detached" from the +context. By default, new spans are "attached" to the context in that they are +created as children of the currently active span, and the newly-created span +becomes the new active span:: + + # TODO (#15): which module holds the global tracer? + from opentelemetry.api.trace import tracer + + # Create a new root span, set it as the current span in context + with tracer.start_span("parent"): + # Attach a new child and update the current span + with tracer.start_span("child"): + do_work(): + # Close child span, set parent as current + # Close parent span, set default span as current + +When creating a span that's "detached" from the context the active span doesn't +change, and the caller is responsible for managing the span's lifetime:: + + from opentelemetry.api.trace import tracer + + # Explicit parent span assignment + span = tracer.create_span("child", parent=parent) as child: + + # The caller is responsible for starting and ending the span + span.start() + try: + do_work(span=child) + finally: + span.end() + +Applications should generally use a single global tracer, and use either +implicit or explicit context propagation consistently throughout. + +.. versionadded:: 0.1.0 +""" + +from __future__ import annotations + +from contextlib import contextmanager +from typing import Iterator + + +class Tracer(object): + """Handles span creation and in-process context propagation. + + This class provides methods for manipulating the context, creating spans, + and controlling spans' lifecycles. + """ + + def get_current_span(self) -> Span: + """Gets the currently active span from the context. + + If there is no current span, return a placeholder span with an invalid + context. + + Returns: + The currently active :class:`.Span`, or a placeholder span with an + invalid :class:`.SpanContext`. + """ + pass + + + @contextmanager + def start_span(self, name: str, parent: Span) -> Iterator[Span]: + """Context manager for span creation. + + Create a new child of the current span, or create a root span if no + current span exists. Start the span and set it as the current span in + this tracer's context. + + On exiting the context manager stop the span and set its parent as the + current span. + + Example:: + + with tracer.start_span("one") as parent: + parent.add_event("parent's event") + with tracer.start_span("two") as child: + child.add_event("child's event") + tracer.get_current_span() # returns child + tracer.get_current_span() # returns parent + tracer.get_current_span() # returns the previously active span + + This is a convenience method for creating spans attached to the + tracer's context. Applications that need more control over the span + lifetime should use :meth:`create_span` instead. For example:: + + with tracer.start_span(name) as span: + do_work() + + is equivalent to:: + + span = tracer.create_span(name, parent=tracer.get_current_span()) + with tracer.use_span(span): + do_work() + + Args: + name: The name of the span to be created. + parent: The span's parent. + + Yields: + The newly-created span. + """ + pass + + def create_span(self, name: str, parent: Span) -> Span: + """Creates a new child span of the given parent. + + Creating the span does not start it, and should not affect the tracer's + context. To start the span and update the tracer's context to make it + the currently active span, see :meth:`use_span`. + + Applications that need to explicitly set spans' parents or create spans + detached from the tracer's context should use this method. + + with tracer.start_span(name) as span: + do_work() + + This is equivalent to:: + + span = tracer.create_span(name, parent=tracer.get_current_span()) + with tracer.use_span(span): + do_work() + + Args: + name: The name of the span to be created. + parent: The span's parent. + + Returns: + The newly-created span. + """ + pass + + @contextmanager + def use_span(self, span: Span) -> Iterator[None]: + """Context manager for controlling a span's lifetime. + + Start the given span and set it as the current span in this tracer's + context. + + On exiting the context manager stop the span and set its parent as the + current span. + + Args: + span: The span to start and make current. + """ + pass + + +class Span(object): + """A span represents a single operation within a trace.""" + + def start(self) -> None: + """Sets the current time as the span's start time. + + Each span represents a single operation. The span's start time is the + wall time at which the operation started. + + Only the first call to ``start`` should modify the span, and + implementations are free to ignore or raise on further calls. + """ + pass + + def end(self) -> None: + """Sets the current time as the span's end time. + + The span's end time is the wall time at which the operation finished. + + Only the first call to ``end`` should modify the span, and + implementations are free to ignore or raise on further calls. + """ + pass + + def get_context(self) -> SpanContext: + """Gets the span's SpanContext. + + Get an immutable, serializable identifier for this span that can be + used to create new child spans. + + Returns: + A :class:`.SpanContext` with a copy of this span's immutable state. + """ + pass + + +class SpanContext(object): + """The state of a Span to propagate between processes. + + This class includes the immutable attributes of a :class:`.Span` that must + be propagated to a span's children and across process boundaries. + + Args: + trace_id: The ID of the trace that this span belongs to. + span_id: This span's ID. + options: Trace options to propagate. + state: Tracing-system-specific info to propagate. + """ + + def __init__(self, + trace_id: str, + span_id: str, + options: TraceOptions, + state: TraceState) -> None: + pass + + +# TODO +class TraceOptions(int): + pass + + +# TODO +class TraceState(dict): + pass From 4c8cc64f4895e9917e846591588a661288b1e597 Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Thu, 20 Jun 2019 11:50:27 -0700 Subject: [PATCH 0008/1517] Initial API package layout (#19) --- opentelemetry-api/CHANGELOG.md | 4 ++ opentelemetry-api/README.rst | 19 ++++++++ opentelemetry-api/opentelemetry/__init__.py | 1 + .../opentelemetry/context/__init__.py | 1 + .../distributedcontext/__init__.py | 1 + .../opentelemetry/internal/__init__.py | 19 ++++++++ .../opentelemetry/internal/version.py | 15 ++++++ .../opentelemetry/logs/__init__.py | 1 + .../opentelemetry/metrics/__init__.py | 1 + .../opentelemetry/resources/__init__.py | 1 + .../opentelemetry/trace/__init__.py | 1 + opentelemetry-api/setup.py | 46 +++++++++++++++++++ 12 files changed, 110 insertions(+) create mode 100644 opentelemetry-api/CHANGELOG.md create mode 100644 opentelemetry-api/README.rst create mode 100644 opentelemetry-api/opentelemetry/__init__.py create mode 100644 opentelemetry-api/opentelemetry/context/__init__.py create mode 100644 opentelemetry-api/opentelemetry/distributedcontext/__init__.py create mode 100644 opentelemetry-api/opentelemetry/internal/__init__.py create mode 100644 opentelemetry-api/opentelemetry/internal/version.py create mode 100644 opentelemetry-api/opentelemetry/logs/__init__.py create mode 100644 opentelemetry-api/opentelemetry/metrics/__init__.py create mode 100644 opentelemetry-api/opentelemetry/resources/__init__.py create mode 100644 opentelemetry-api/opentelemetry/trace/__init__.py create mode 100644 opentelemetry-api/setup.py diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md new file mode 100644 index 0000000000..09e4c38c76 --- /dev/null +++ b/opentelemetry-api/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## Unreleased +- Initial release diff --git a/opentelemetry-api/README.rst b/opentelemetry-api/README.rst new file mode 100644 index 0000000000..130fbbf39d --- /dev/null +++ b/opentelemetry-api/README.rst @@ -0,0 +1,19 @@ +OpenTelemetry Python API +============================================================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-api.svg + :target: https://pypi.org/project/opentelemetry-api/ + +Installation +------------ + +:: + + pip install opentelemetry-api + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/opentelemetry-api/opentelemetry/__init__.py b/opentelemetry-api/opentelemetry/__init__.py new file mode 100644 index 0000000000..8db66d3d0f --- /dev/null +++ b/opentelemetry-api/opentelemetry/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/opentelemetry-api/opentelemetry/context/__init__.py b/opentelemetry-api/opentelemetry/context/__init__.py new file mode 100644 index 0000000000..8db66d3d0f --- /dev/null +++ b/opentelemetry-api/opentelemetry/context/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/opentelemetry-api/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/opentelemetry/distributedcontext/__init__.py new file mode 100644 index 0000000000..8db66d3d0f --- /dev/null +++ b/opentelemetry-api/opentelemetry/distributedcontext/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/opentelemetry-api/opentelemetry/internal/__init__.py b/opentelemetry-api/opentelemetry/internal/__init__.py new file mode 100644 index 0000000000..83ac009440 --- /dev/null +++ b/opentelemetry-api/opentelemetry/internal/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .version import __version__ + +__all__ = [ + "__version__", +] diff --git a/opentelemetry-api/opentelemetry/internal/version.py b/opentelemetry-api/opentelemetry/internal/version.py new file mode 100644 index 0000000000..34f10b31ef --- /dev/null +++ b/opentelemetry-api/opentelemetry/internal/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.1.0" diff --git a/opentelemetry-api/opentelemetry/logs/__init__.py b/opentelemetry-api/opentelemetry/logs/__init__.py new file mode 100644 index 0000000000..8db66d3d0f --- /dev/null +++ b/opentelemetry-api/opentelemetry/logs/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/opentelemetry-api/opentelemetry/metrics/__init__.py b/opentelemetry-api/opentelemetry/metrics/__init__.py new file mode 100644 index 0000000000..8db66d3d0f --- /dev/null +++ b/opentelemetry-api/opentelemetry/metrics/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/opentelemetry-api/opentelemetry/resources/__init__.py b/opentelemetry-api/opentelemetry/resources/__init__.py new file mode 100644 index 0000000000..8db66d3d0f --- /dev/null +++ b/opentelemetry-api/opentelemetry/resources/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/opentelemetry-api/opentelemetry/trace/__init__.py b/opentelemetry-api/opentelemetry/trace/__init__.py new file mode 100644 index 0000000000..8db66d3d0f --- /dev/null +++ b/opentelemetry-api/opentelemetry/trace/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py new file mode 100644 index 0000000000..b03e118f0d --- /dev/null +++ b/opentelemetry-api/setup.py @@ -0,0 +1,46 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from setuptools import find_packages +from setuptools import setup +from opentelemetry.internal import __version__ + +setup( + name="opentelemetry-api", + version=__version__, # noqa + author="OpenTelemetry Authors", + author_email="cncf-opentelemetry-contributors@lists.cncf.io", + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + ], + description="OpenTelemetry Python API", + include_package_data=True, + long_description=open("README.rst").read(), + install_requires=[ + ], + extras_require={}, + license="Apache-2.0", + packages=find_packages(exclude=("examples", "tests",)), + namespace_packages=[], + url="https://github.com/open-telemetry/opentelemetry-python/opentelemetry-api", + zip_safe=False, +) From 835a7558f2218f2c46df37b83df0c22b06e4acac Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Thu, 20 Jun 2019 15:09:03 -0700 Subject: [PATCH 0009/1517] Add .gitignore and CODEOWNERS file (#20) --- .github/CODEOWNERS | 5 +++++ .gitignore | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .gitignore diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..e9a8df9505 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# Code owners file. +# This file controls who is tagged for review for any given pull request. + +# For anything not explicitly taken by someone else: +* @c24t @carlosalberto @Oberon00 @reyang diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..1eef73b484 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +*.py[cod] +*.sw[op] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +__pycache__ +venv*/ + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.nox +.tox +.cache +htmlcov + +# Translations +*.mo + +# Mac +.DS_Store + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# JetBrains +.idea + +# VSCode +.vscode From 41f0caa71401ca9eb70f9b10cc010c08cac5cb79 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 20 Jun 2019 16:46:41 -0700 Subject: [PATCH 0010/1517] Move extant tracing API into new pkg (#21) --- .../opentelemetry/trace/__init__.py | 243 +++++++++++++++++- opentelemetry/__init__.py | 0 opentelemetry/api/__init__.py | 0 opentelemetry/api/trace/__init__.py | 242 ----------------- 4 files changed, 242 insertions(+), 243 deletions(-) delete mode 100644 opentelemetry/__init__.py delete mode 100644 opentelemetry/api/__init__.py delete mode 100644 opentelemetry/api/trace/__init__.py diff --git a/opentelemetry-api/opentelemetry/trace/__init__.py b/opentelemetry-api/opentelemetry/trace/__init__.py index 8db66d3d0f..6e4fc80f42 100644 --- a/opentelemetry-api/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/opentelemetry/trace/__init__.py @@ -1 +1,242 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The OpenTelemetry tracing API describes the classes used to generate +distributed traces. + +The :class:`.Tracer` class controls access to the execution context, and +manages span creation. Each operation in a trace is represented by a +:class:`.Span`, which records the start, end time, and metadata associated with +the operation. + +This module provides abstract (i.e. unimplemented) classes required for +tracing, and a concrete no-op ``BlankSpan`` that allows applications to use the +API package alone without a supporting implementation. + +The tracer supports creating spans that are "attached" or "detached" from the +context. By default, new spans are "attached" to the context in that they are +created as children of the currently active span, and the newly-created span +becomes the new active span:: + + # TODO (#15): which module holds the global tracer? + from opentelemetry.api.trace import tracer + + # Create a new root span, set it as the current span in context + with tracer.start_span("parent"): + # Attach a new child and update the current span + with tracer.start_span("child"): + do_work(): + # Close child span, set parent as current + # Close parent span, set default span as current + +When creating a span that's "detached" from the context the active span doesn't +change, and the caller is responsible for managing the span's lifetime:: + + from opentelemetry.api.trace import tracer + + # Explicit parent span assignment + span = tracer.create_span("child", parent=parent) as child: + + # The caller is responsible for starting and ending the span + span.start() + try: + do_work(span=child) + finally: + span.end() + +Applications should generally use a single global tracer, and use either +implicit or explicit context propagation consistently throughout. + +.. versionadded:: 0.1.0 +""" + +from __future__ import annotations + +from contextlib import contextmanager +from typing import Iterator + + +class Tracer(object): + """Handles span creation and in-process context propagation. + + This class provides methods for manipulating the context, creating spans, + and controlling spans' lifecycles. + """ + + def get_current_span(self) -> Span: + """Gets the currently active span from the context. + + If there is no current span, return a placeholder span with an invalid + context. + + Returns: + The currently active :class:`.Span`, or a placeholder span with an + invalid :class:`.SpanContext`. + """ + pass + + + @contextmanager + def start_span(self, name: str, parent: Span) -> Iterator[Span]: + """Context manager for span creation. + + Create a new child of the current span, or create a root span if no + current span exists. Start the span and set it as the current span in + this tracer's context. + + On exiting the context manager stop the span and set its parent as the + current span. + + Example:: + + with tracer.start_span("one") as parent: + parent.add_event("parent's event") + with tracer.start_span("two") as child: + child.add_event("child's event") + tracer.get_current_span() # returns child + tracer.get_current_span() # returns parent + tracer.get_current_span() # returns the previously active span + + This is a convenience method for creating spans attached to the + tracer's context. Applications that need more control over the span + lifetime should use :meth:`create_span` instead. For example:: + + with tracer.start_span(name) as span: + do_work() + + is equivalent to:: + + span = tracer.create_span(name, parent=tracer.get_current_span()) + with tracer.use_span(span): + do_work() + + Args: + name: The name of the span to be created. + parent: The span's parent. + + Yields: + The newly-created span. + """ + pass + + def create_span(self, name: str, parent: Span) -> Span: + """Creates a new child span of the given parent. + + Creating the span does not start it, and should not affect the tracer's + context. To start the span and update the tracer's context to make it + the currently active span, see :meth:`use_span`. + + Applications that need to explicitly set spans' parents or create spans + detached from the tracer's context should use this method. + + with tracer.start_span(name) as span: + do_work() + + This is equivalent to:: + + span = tracer.create_span(name, parent=tracer.get_current_span()) + with tracer.use_span(span): + do_work() + + Args: + name: The name of the span to be created. + parent: The span's parent. + + Returns: + The newly-created span. + """ + pass + + @contextmanager + def use_span(self, span: Span) -> Iterator[None]: + """Context manager for controlling a span's lifetime. + + Start the given span and set it as the current span in this tracer's + context. + + On exiting the context manager stop the span and set its parent as the + current span. + + Args: + span: The span to start and make current. + """ + pass + + +class Span(object): + """A span represents a single operation within a trace.""" + + def start(self) -> None: + """Sets the current time as the span's start time. + + Each span represents a single operation. The span's start time is the + wall time at which the operation started. + + Only the first call to ``start`` should modify the span, and + implementations are free to ignore or raise on further calls. + """ + pass + + def end(self) -> None: + """Sets the current time as the span's end time. + + The span's end time is the wall time at which the operation finished. + + Only the first call to ``end`` should modify the span, and + implementations are free to ignore or raise on further calls. + """ + pass + + def get_context(self) -> SpanContext: + """Gets the span's SpanContext. + + Get an immutable, serializable identifier for this span that can be + used to create new child spans. + + Returns: + A :class:`.SpanContext` with a copy of this span's immutable state. + """ + pass + + +class SpanContext(object): + """The state of a Span to propagate between processes. + + This class includes the immutable attributes of a :class:`.Span` that must + be propagated to a span's children and across process boundaries. + + Args: + trace_id: The ID of the trace that this span belongs to. + span_id: This span's ID. + options: Trace options to propagate. + state: Tracing-system-specific info to propagate. + """ + + def __init__(self, + trace_id: str, + span_id: str, + options: TraceOptions, + state: TraceState) -> None: + pass + + +# TODO +class TraceOptions(int): + pass + + +# TODO +class TraceState(dict): + pass diff --git a/opentelemetry/__init__.py b/opentelemetry/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry/api/__init__.py b/opentelemetry/api/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry/api/trace/__init__.py b/opentelemetry/api/trace/__init__.py deleted file mode 100644 index 6e4fc80f42..0000000000 --- a/opentelemetry/api/trace/__init__.py +++ /dev/null @@ -1,242 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The OpenTelemetry tracing API describes the classes used to generate -distributed traces. - -The :class:`.Tracer` class controls access to the execution context, and -manages span creation. Each operation in a trace is represented by a -:class:`.Span`, which records the start, end time, and metadata associated with -the operation. - -This module provides abstract (i.e. unimplemented) classes required for -tracing, and a concrete no-op ``BlankSpan`` that allows applications to use the -API package alone without a supporting implementation. - -The tracer supports creating spans that are "attached" or "detached" from the -context. By default, new spans are "attached" to the context in that they are -created as children of the currently active span, and the newly-created span -becomes the new active span:: - - # TODO (#15): which module holds the global tracer? - from opentelemetry.api.trace import tracer - - # Create a new root span, set it as the current span in context - with tracer.start_span("parent"): - # Attach a new child and update the current span - with tracer.start_span("child"): - do_work(): - # Close child span, set parent as current - # Close parent span, set default span as current - -When creating a span that's "detached" from the context the active span doesn't -change, and the caller is responsible for managing the span's lifetime:: - - from opentelemetry.api.trace import tracer - - # Explicit parent span assignment - span = tracer.create_span("child", parent=parent) as child: - - # The caller is responsible for starting and ending the span - span.start() - try: - do_work(span=child) - finally: - span.end() - -Applications should generally use a single global tracer, and use either -implicit or explicit context propagation consistently throughout. - -.. versionadded:: 0.1.0 -""" - -from __future__ import annotations - -from contextlib import contextmanager -from typing import Iterator - - -class Tracer(object): - """Handles span creation and in-process context propagation. - - This class provides methods for manipulating the context, creating spans, - and controlling spans' lifecycles. - """ - - def get_current_span(self) -> Span: - """Gets the currently active span from the context. - - If there is no current span, return a placeholder span with an invalid - context. - - Returns: - The currently active :class:`.Span`, or a placeholder span with an - invalid :class:`.SpanContext`. - """ - pass - - - @contextmanager - def start_span(self, name: str, parent: Span) -> Iterator[Span]: - """Context manager for span creation. - - Create a new child of the current span, or create a root span if no - current span exists. Start the span and set it as the current span in - this tracer's context. - - On exiting the context manager stop the span and set its parent as the - current span. - - Example:: - - with tracer.start_span("one") as parent: - parent.add_event("parent's event") - with tracer.start_span("two") as child: - child.add_event("child's event") - tracer.get_current_span() # returns child - tracer.get_current_span() # returns parent - tracer.get_current_span() # returns the previously active span - - This is a convenience method for creating spans attached to the - tracer's context. Applications that need more control over the span - lifetime should use :meth:`create_span` instead. For example:: - - with tracer.start_span(name) as span: - do_work() - - is equivalent to:: - - span = tracer.create_span(name, parent=tracer.get_current_span()) - with tracer.use_span(span): - do_work() - - Args: - name: The name of the span to be created. - parent: The span's parent. - - Yields: - The newly-created span. - """ - pass - - def create_span(self, name: str, parent: Span) -> Span: - """Creates a new child span of the given parent. - - Creating the span does not start it, and should not affect the tracer's - context. To start the span and update the tracer's context to make it - the currently active span, see :meth:`use_span`. - - Applications that need to explicitly set spans' parents or create spans - detached from the tracer's context should use this method. - - with tracer.start_span(name) as span: - do_work() - - This is equivalent to:: - - span = tracer.create_span(name, parent=tracer.get_current_span()) - with tracer.use_span(span): - do_work() - - Args: - name: The name of the span to be created. - parent: The span's parent. - - Returns: - The newly-created span. - """ - pass - - @contextmanager - def use_span(self, span: Span) -> Iterator[None]: - """Context manager for controlling a span's lifetime. - - Start the given span and set it as the current span in this tracer's - context. - - On exiting the context manager stop the span and set its parent as the - current span. - - Args: - span: The span to start and make current. - """ - pass - - -class Span(object): - """A span represents a single operation within a trace.""" - - def start(self) -> None: - """Sets the current time as the span's start time. - - Each span represents a single operation. The span's start time is the - wall time at which the operation started. - - Only the first call to ``start`` should modify the span, and - implementations are free to ignore or raise on further calls. - """ - pass - - def end(self) -> None: - """Sets the current time as the span's end time. - - The span's end time is the wall time at which the operation finished. - - Only the first call to ``end`` should modify the span, and - implementations are free to ignore or raise on further calls. - """ - pass - - def get_context(self) -> SpanContext: - """Gets the span's SpanContext. - - Get an immutable, serializable identifier for this span that can be - used to create new child spans. - - Returns: - A :class:`.SpanContext` with a copy of this span's immutable state. - """ - pass - - -class SpanContext(object): - """The state of a Span to propagate between processes. - - This class includes the immutable attributes of a :class:`.Span` that must - be propagated to a span's children and across process boundaries. - - Args: - trace_id: The ID of the trace that this span belongs to. - span_id: This span's ID. - options: Trace options to propagate. - state: Tracing-system-specific info to propagate. - """ - - def __init__(self, - trace_id: str, - span_id: str, - options: TraceOptions, - state: TraceState) -> None: - pass - - -# TODO -class TraceOptions(int): - pass - - -# TODO -class TraceState(dict): - pass From e7b5279c5a827b679f3ded2e1f5d8874ad1c5c7b Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Fri, 21 Jun 2019 14:32:28 -0700 Subject: [PATCH 0011/1517] Add sphinx docs (#12) --- docs/Makefile | 19 ++++++++++ docs/conf.py | 68 ++++++++++++++++++++++++++++++++++++ docs/index.rst | 26 ++++++++++++++ docs/make.bat | 35 +++++++++++++++++++ docs/modules.rst | 7 ++++ docs/opentelemetry.trace.rst | 4 +++ 6 files changed, 159 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/modules.rst create mode 100644 docs/opentelemetry.trace.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000000..51285967a7 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000000..414c33d3df --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,68 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. + +import os +import sys +sys.path.insert(0, os.path.abspath('../opentelemetry-api/')) + + +# -- Project information ----------------------------------------------------- + +project = 'OpenTelemetry' +copyright = '2019, OpenTelemetry Authors' +author = 'OpenTelemetry Authors' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + # API doc generation + 'sphinx.ext.autodoc', + # Support for google-style docstrings + 'sphinx.ext.napoleon', + # Infer types from hints instead of docstrings + 'sphinx_autodoc_typehints', + # Add links to source from generated docs + 'sphinx.ext.viewcode', + # Link to other sphinx docs + 'sphinx.ext.intersphinx', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +autodoc_default_options = { + 'members': True, + 'undoc-members': True, + 'show-inheritance': True, + 'member-order': 'bysource' +} + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000000..aed678db96 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,26 @@ +.. OpenTelemetry documentation master file, created by + sphinx-quickstart on Mon Jun 3 22:48:38 2019. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +OpenTelemetry API +================= + +Welcome to OpenTelemetry's API documentation! + +This documentation describes the ``opentelemetry-api`` package, which provides +abstract types for OpenTelemetry implementations. + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + opentelemetry.trace + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000000..27f573b87a --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 0000000000..b795cd4515 --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,7 @@ +opentelemetry +============= + +.. toctree:: + :maxdepth: 4 + + opentelemetry.trace diff --git a/docs/opentelemetry.trace.rst b/docs/opentelemetry.trace.rst new file mode 100644 index 0000000000..cec44bd817 --- /dev/null +++ b/docs/opentelemetry.trace.rst @@ -0,0 +1,4 @@ +opentelemetry.trace package +=========================== + +.. automodule:: opentelemetry.trace From 5d1632a4aeb347110e3ce9772d8549f32427c573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 25 Jun 2019 08:52:03 +0200 Subject: [PATCH 0012/1517] Add a .pylintrc & fix warnings (#24) * Add .pylintrc based on OpenCensus (related #6). * Fix/disable pylint warnings. * pylint: Fix W0107: Unnecessary pass statement (unnecessary-pass) I'm not sure I like this warning. --- .pylintrc | 482 ++++++++++++++++++ dev-requirements.txt | 1 + .../opentelemetry/trace/__init__.py | 13 +- 3 files changed, 486 insertions(+), 10 deletions(-) create mode 100644 .pylintrc create mode 100644 dev-requirements.txt diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000000..7d7a8f50cf --- /dev/null +++ b/.pylintrc @@ -0,0 +1,482 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=0 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=missing-docstring, + fixme, # Warns about FIXME, TODO, etc. comments. + too-few-public-methods, # Might be good to re-enable this later. + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +# enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +#evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format=text + +# Tells whether to display a full report or only the messages. +#reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, while `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package.. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +#ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +#ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +#ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format=LF + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=_, + log, + logger + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=yes + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +variable-rgx=(([a-z_][a-z0-9_]{1,})|(_[a-z0-9_]*)|(__[a-z][a-z0-9_]+__))$ + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=yes + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library=six + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement. +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception". +overgeneral-exceptions=Exception diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000000..c326b529fd --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1 @@ +pylint~=2.3 diff --git a/opentelemetry-api/opentelemetry/trace/__init__.py b/opentelemetry-api/opentelemetry/trace/__init__.py index 6e4fc80f42..1281c8a84c 100644 --- a/opentelemetry-api/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/opentelemetry/trace/__init__.py @@ -68,7 +68,7 @@ from typing import Iterator -class Tracer(object): +class Tracer: """Handles span creation and in-process context propagation. This class provides methods for manipulating the context, creating spans, @@ -85,7 +85,6 @@ def get_current_span(self) -> Span: The currently active :class:`.Span`, or a placeholder span with an invalid :class:`.SpanContext`. """ - pass @contextmanager @@ -129,7 +128,6 @@ def start_span(self, name: str, parent: Span) -> Iterator[Span]: Yields: The newly-created span. """ - pass def create_span(self, name: str, parent: Span) -> Span: """Creates a new child span of the given parent. @@ -157,7 +155,6 @@ def create_span(self, name: str, parent: Span) -> Span: Returns: The newly-created span. """ - pass @contextmanager def use_span(self, span: Span) -> Iterator[None]: @@ -172,10 +169,9 @@ def use_span(self, span: Span) -> Iterator[None]: Args: span: The span to start and make current. """ - pass -class Span(object): +class Span: """A span represents a single operation within a trace.""" def start(self) -> None: @@ -187,7 +183,6 @@ def start(self) -> None: Only the first call to ``start`` should modify the span, and implementations are free to ignore or raise on further calls. """ - pass def end(self) -> None: """Sets the current time as the span's end time. @@ -197,7 +192,6 @@ def end(self) -> None: Only the first call to ``end`` should modify the span, and implementations are free to ignore or raise on further calls. """ - pass def get_context(self) -> SpanContext: """Gets the span's SpanContext. @@ -208,10 +202,9 @@ def get_context(self) -> SpanContext: Returns: A :class:`.SpanContext` with a copy of this span's immutable state. """ - pass -class SpanContext(object): +class SpanContext: """The state of a Span to propagate between processes. This class includes the immutable attributes of a :class:`.Span` that must From 132ef432a8275af5149bc4a77eaf5e5860ee8c2f Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 25 Jun 2019 13:55:52 -0700 Subject: [PATCH 0013/1517] Switch type hints to strings for 3.4 (#27) --- .../opentelemetry/trace/__init__.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/opentelemetry-api/opentelemetry/trace/__init__.py b/opentelemetry-api/opentelemetry/trace/__init__.py index 1281c8a84c..282bc72b3f 100644 --- a/opentelemetry-api/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/opentelemetry/trace/__init__.py @@ -62,8 +62,6 @@ .. versionadded:: 0.1.0 """ -from __future__ import annotations - from contextlib import contextmanager from typing import Iterator @@ -75,7 +73,7 @@ class Tracer: and controlling spans' lifecycles. """ - def get_current_span(self) -> Span: + def get_current_span(self) -> 'Span': """Gets the currently active span from the context. If there is no current span, return a placeholder span with an invalid @@ -88,7 +86,7 @@ def get_current_span(self) -> Span: @contextmanager - def start_span(self, name: str, parent: Span) -> Iterator[Span]: + def start_span(self, name: str, parent: 'Span') -> Iterator['Span']: """Context manager for span creation. Create a new child of the current span, or create a root span if no @@ -129,7 +127,7 @@ def start_span(self, name: str, parent: Span) -> Iterator[Span]: The newly-created span. """ - def create_span(self, name: str, parent: Span) -> Span: + def create_span(self, name: str, parent: 'Span') -> 'Span': """Creates a new child span of the given parent. Creating the span does not start it, and should not affect the tracer's @@ -157,7 +155,7 @@ def create_span(self, name: str, parent: Span) -> Span: """ @contextmanager - def use_span(self, span: Span) -> Iterator[None]: + def use_span(self, span: 'Span') -> Iterator[None]: """Context manager for controlling a span's lifetime. Start the given span and set it as the current span in this tracer's @@ -193,7 +191,7 @@ def end(self) -> None: implementations are free to ignore or raise on further calls. """ - def get_context(self) -> SpanContext: + def get_context(self) -> 'SpanContext': """Gets the span's SpanContext. Get an immutable, serializable identifier for this span that can be @@ -220,8 +218,8 @@ class SpanContext: def __init__(self, trace_id: str, span_id: str, - options: TraceOptions, - state: TraceState) -> None: + options: 'TraceOptions', + state: 'TraceState') -> None: pass From 746ce90774fdeb9eeb054d1000d84999bff82fe2 Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Wed, 26 Jun 2019 08:55:28 -0700 Subject: [PATCH 0014/1517] Improve package layout (#26) Auto discover namespace packages. --- opentelemetry-api/opentelemetry/__init__.py | 1 - .../opentelemetry/context/__init__.py | 14 ++++++++++++- .../distributedcontext/__init__.py | 14 ++++++++++++- .../opentelemetry/internal/version.py | 2 +- .../opentelemetry/logs/__init__.py | 14 ++++++++++++- .../opentelemetry/metrics/__init__.py | 14 ++++++++++++- .../opentelemetry/resources/__init__.py | 14 ++++++++++++- opentelemetry-api/setup.py | 20 +++++++++++-------- 8 files changed, 78 insertions(+), 15 deletions(-) delete mode 100644 opentelemetry-api/opentelemetry/__init__.py diff --git a/opentelemetry-api/opentelemetry/__init__.py b/opentelemetry-api/opentelemetry/__init__.py deleted file mode 100644 index 8db66d3d0f..0000000000 --- a/opentelemetry-api/opentelemetry/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/opentelemetry-api/opentelemetry/context/__init__.py b/opentelemetry-api/opentelemetry/context/__init__.py index 8db66d3d0f..d853a7bcf6 100644 --- a/opentelemetry-api/opentelemetry/context/__init__.py +++ b/opentelemetry-api/opentelemetry/context/__init__.py @@ -1 +1,13 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-api/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/opentelemetry/distributedcontext/__init__.py index 8db66d3d0f..d853a7bcf6 100644 --- a/opentelemetry-api/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/opentelemetry/distributedcontext/__init__.py @@ -1 +1,13 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-api/opentelemetry/internal/version.py b/opentelemetry-api/opentelemetry/internal/version.py index 34f10b31ef..a457c2b665 100644 --- a/opentelemetry-api/opentelemetry/internal/version.py +++ b/opentelemetry-api/opentelemetry/internal/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.0" +__version__ = "0.1.dev0" diff --git a/opentelemetry-api/opentelemetry/logs/__init__.py b/opentelemetry-api/opentelemetry/logs/__init__.py index 8db66d3d0f..d853a7bcf6 100644 --- a/opentelemetry-api/opentelemetry/logs/__init__.py +++ b/opentelemetry-api/opentelemetry/logs/__init__.py @@ -1 +1,13 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-api/opentelemetry/metrics/__init__.py b/opentelemetry-api/opentelemetry/metrics/__init__.py index 8db66d3d0f..d853a7bcf6 100644 --- a/opentelemetry-api/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/opentelemetry/metrics/__init__.py @@ -1 +1,13 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-api/opentelemetry/resources/__init__.py b/opentelemetry-api/opentelemetry/resources/__init__.py index 8db66d3d0f..d853a7bcf6 100644 --- a/opentelemetry-api/opentelemetry/resources/__init__.py +++ b/opentelemetry-api/opentelemetry/resources/__init__.py @@ -1 +1,13 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index b03e118f0d..2e405905aa 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -12,13 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from setuptools import find_packages -from setuptools import setup -from opentelemetry.internal import __version__ +import os +import setuptools -setup( +base_dir = os.path.dirname(__file__) + +package_info = {} +with open(os.path.join(base_dir, "opentelemetry", "internal", "version.py")) as f: + exec(f.read(), package_info) + +setuptools.setup( name="opentelemetry-api", - version=__version__, # noqa + version=package_info["__version__"], # noqa author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ @@ -39,8 +44,7 @@ ], extras_require={}, license="Apache-2.0", - packages=find_packages(exclude=("examples", "tests",)), - namespace_packages=[], - url="https://github.com/open-telemetry/opentelemetry-python/opentelemetry-api", + packages=setuptools.find_namespace_packages(include=["opentelemetry.*"]), + url="https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-api", zip_safe=False, ) From ad7bda6c3f10dba3307a013253e2f6e24eb675a7 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Wed, 26 Jun 2019 13:55:13 -0700 Subject: [PATCH 0015/1517] Add tox and travis, run pylint on builds (#30) --- .travis.yml | 16 ++++++++++++++++ tox.ini | 10 ++++++++++ 2 files changed, 26 insertions(+) create mode 100644 .travis.yml create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..4f9cb871ea --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +dist: xenial + +language: python + +python: + - '3.7' + +install: + - pip install tox-travis + +script: + - tox + +branches: + only: + - master diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000000..edfece5971 --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +skipsdist = True +envlist = py37-lint + +[testenv] +deps = + py37-lint: pylint + +commands = + py37-lint: pylint opentelemetry-api/opentelemetry/trace/ From d7d9907c1372e2b5042e548a4f6608cf4a5210d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Thu, 27 Jun 2019 09:31:50 +0200 Subject: [PATCH 0016/1517] Make Sphinx check for undefined references & .gitignore _build. (#32) --- .gitignore | 3 +++ docs/conf.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index 1eef73b484..b63de1b7f0 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ htmlcov # VSCode .vscode + +# Sphinx +_build/ diff --git a/docs/conf.py b/docs/conf.py index 414c33d3df..a8e09c67d8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,6 +40,13 @@ 'sphinx.ext.intersphinx', ] +intersphinx_mapping = {'python': ('https://docs.python.org/3/', None)} + +# http://www.sphinx-doc.org/en/master/config.html#confval-nitpicky +# Sphinx will warn about all references where the target cannot be found. +nitpicky = True +nitpick_ignore = [] + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] From 38d08e513126e9e59a23ae9281b4aec3dca72330 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 27 Jun 2019 09:03:05 -0700 Subject: [PATCH 0017/1517] Set pylint's line length to 79 chars (#35) --- .pylintrc | 2 +- opentelemetry-api/opentelemetry/trace/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pylintrc b/.pylintrc index 7d7a8f50cf..0345f53ad6 100644 --- a/.pylintrc +++ b/.pylintrc @@ -245,7 +245,7 @@ indent-after-paren=4 indent-string=' ' # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=79 # Maximum number of lines in a module. max-module-lines=1000 diff --git a/opentelemetry-api/opentelemetry/trace/__init__.py b/opentelemetry-api/opentelemetry/trace/__init__.py index 282bc72b3f..3ed57c0dac 100644 --- a/opentelemetry-api/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/opentelemetry/trace/__init__.py @@ -104,7 +104,7 @@ def start_span(self, name: str, parent: 'Span') -> Iterator['Span']: child.add_event("child's event") tracer.get_current_span() # returns child tracer.get_current_span() # returns parent - tracer.get_current_span() # returns the previously active span + tracer.get_current_span() # returns previously active span This is a convenience method for creating spans attached to the tracer's context. Applications that need more control over the span From 0bf0fad7e89f68ef070aeb28dcb8af332f589eaf Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 27 Jun 2019 09:38:05 -0700 Subject: [PATCH 0018/1517] Add type check to travis (#31) --- mypy.ini | 14 ++++++++++++++ opentelemetry-api/opentelemetry/__init__.py | 13 +++++++++++++ opentelemetry-api/opentelemetry/trace/__init__.py | 12 ++++++------ opentelemetry-api/setup.py | 1 + tox.ini | 6 ++++-- 5 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 mypy.ini create mode 100644 opentelemetry-api/opentelemetry/__init__.py diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000000..5899d1954e --- /dev/null +++ b/mypy.ini @@ -0,0 +1,14 @@ +[mypy] + disallow_any_unimported = True + disallow_any_expr = True + disallow_any_decorated = True + disallow_any_explicit = True + disallow_any_generics = True + disallow_subclassing_any = True + disallow_untyped_calls = True + disallow_untyped_defs = True + disallow_incomplete_defs = True + check_untyped_defs = True + disallow_untyped_decorators = True + warn_unused_ignores = True + warn_return_any = True diff --git a/opentelemetry-api/opentelemetry/__init__.py b/opentelemetry-api/opentelemetry/__init__.py new file mode 100644 index 0000000000..d853a7bcf6 --- /dev/null +++ b/opentelemetry-api/opentelemetry/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-api/opentelemetry/trace/__init__.py b/opentelemetry-api/opentelemetry/trace/__init__.py index 3ed57c0dac..6bae25c276 100644 --- a/opentelemetry-api/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/opentelemetry/trace/__init__.py @@ -63,7 +63,7 @@ """ from contextlib import contextmanager -from typing import Iterator +import typing class Tracer: @@ -85,8 +85,8 @@ def get_current_span(self) -> 'Span': """ - @contextmanager - def start_span(self, name: str, parent: 'Span') -> Iterator['Span']: + @contextmanager # type: ignore + def start_span(self, name: str, parent: 'Span') -> typing.Iterator['Span']: """Context manager for span creation. Create a new child of the current span, or create a root span if no @@ -154,8 +154,8 @@ def create_span(self, name: str, parent: 'Span') -> 'Span': The newly-created span. """ - @contextmanager - def use_span(self, span: 'Span') -> Iterator[None]: + @contextmanager # type: ignore + def use_span(self, span: 'Span') -> typing.Iterator[None]: """Context manager for controlling a span's lifetime. Start the given span and set it as the current span in this tracer's @@ -229,5 +229,5 @@ class TraceOptions(int): # TODO -class TraceState(dict): +class TraceState(typing.Dict[str, str]): pass diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index 2e405905aa..460417304f 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -41,6 +41,7 @@ include_package_data=True, long_description=open("README.rst").read(), install_requires=[ + "typing; python_version<'3.5'", ], extras_require={}, license="Apache-2.0", diff --git a/tox.ini b/tox.ini index edfece5971..aa70cf3b3e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,12 @@ [tox] skipsdist = True -envlist = py37-lint +envlist = py37-lint, py37-mypy [testenv] deps = py37-lint: pylint + py37-mypy: mypy commands = - py37-lint: pylint opentelemetry-api/opentelemetry/trace/ + py37-lint: pylint opentelemetry-api/opentelemetry/ + py37-mypy: mypy opentelemetry-api/opentelemetry/ From 7e3f9a1a25fd316a908b36a55fbdf900dff684f8 Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Mon, 1 Jul 2019 13:40:06 -0700 Subject: [PATCH 0019/1517] Improve package layout (#37) --- opentelemetry-api/setup.py | 5 +- .../{ => src}/opentelemetry/__init__.py | 0 .../opentelemetry/context/__init__.py | 0 .../distributedcontext/__init__.py | 0 .../opentelemetry/internal/__init__.py | 0 .../opentelemetry/internal/version.py | 0 .../{ => src}/opentelemetry/logs/__init__.py | 0 .../opentelemetry/metrics/__init__.py | 0 .../opentelemetry/resources/__init__.py | 0 .../{ => src}/opentelemetry/trace/__init__.py | 0 opentelemetry-sdk/CHANGELOG.md | 4 ++ opentelemetry-sdk/README.rst | 19 +++++++ opentelemetry-sdk/setup.py | 51 +++++++++++++++++++ .../src/opentelemetry/sdk/__init__.py | 15 ++++++ .../src/opentelemetry/sdk/version.py | 15 ++++++ tox.ini | 4 +- 16 files changed, 109 insertions(+), 4 deletions(-) rename opentelemetry-api/{ => src}/opentelemetry/__init__.py (100%) rename opentelemetry-api/{ => src}/opentelemetry/context/__init__.py (100%) rename opentelemetry-api/{ => src}/opentelemetry/distributedcontext/__init__.py (100%) rename opentelemetry-api/{ => src}/opentelemetry/internal/__init__.py (100%) rename opentelemetry-api/{ => src}/opentelemetry/internal/version.py (100%) rename opentelemetry-api/{ => src}/opentelemetry/logs/__init__.py (100%) rename opentelemetry-api/{ => src}/opentelemetry/metrics/__init__.py (100%) rename opentelemetry-api/{ => src}/opentelemetry/resources/__init__.py (100%) rename opentelemetry-api/{ => src}/opentelemetry/trace/__init__.py (100%) create mode 100644 opentelemetry-sdk/CHANGELOG.md create mode 100644 opentelemetry-sdk/README.rst create mode 100644 opentelemetry-sdk/setup.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/version.py diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index 460417304f..de97ec88c1 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -18,7 +18,7 @@ base_dir = os.path.dirname(__file__) package_info = {} -with open(os.path.join(base_dir, "opentelemetry", "internal", "version.py")) as f: +with open(os.path.join(base_dir, "src", "opentelemetry", "internal", "version.py")) as f: exec(f.read(), package_info) setuptools.setup( @@ -45,7 +45,8 @@ ], extras_require={}, license="Apache-2.0", - packages=setuptools.find_namespace_packages(include=["opentelemetry.*"]), + package_dir={"": "src"}, + packages=setuptools.find_namespace_packages(where="src"), url="https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-api", zip_safe=False, ) diff --git a/opentelemetry-api/opentelemetry/__init__.py b/opentelemetry-api/src/opentelemetry/__init__.py similarity index 100% rename from opentelemetry-api/opentelemetry/__init__.py rename to opentelemetry-api/src/opentelemetry/__init__.py diff --git a/opentelemetry-api/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py similarity index 100% rename from opentelemetry-api/opentelemetry/context/__init__.py rename to opentelemetry-api/src/opentelemetry/context/__init__.py diff --git a/opentelemetry-api/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py similarity index 100% rename from opentelemetry-api/opentelemetry/distributedcontext/__init__.py rename to opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py diff --git a/opentelemetry-api/opentelemetry/internal/__init__.py b/opentelemetry-api/src/opentelemetry/internal/__init__.py similarity index 100% rename from opentelemetry-api/opentelemetry/internal/__init__.py rename to opentelemetry-api/src/opentelemetry/internal/__init__.py diff --git a/opentelemetry-api/opentelemetry/internal/version.py b/opentelemetry-api/src/opentelemetry/internal/version.py similarity index 100% rename from opentelemetry-api/opentelemetry/internal/version.py rename to opentelemetry-api/src/opentelemetry/internal/version.py diff --git a/opentelemetry-api/opentelemetry/logs/__init__.py b/opentelemetry-api/src/opentelemetry/logs/__init__.py similarity index 100% rename from opentelemetry-api/opentelemetry/logs/__init__.py rename to opentelemetry-api/src/opentelemetry/logs/__init__.py diff --git a/opentelemetry-api/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py similarity index 100% rename from opentelemetry-api/opentelemetry/metrics/__init__.py rename to opentelemetry-api/src/opentelemetry/metrics/__init__.py diff --git a/opentelemetry-api/opentelemetry/resources/__init__.py b/opentelemetry-api/src/opentelemetry/resources/__init__.py similarity index 100% rename from opentelemetry-api/opentelemetry/resources/__init__.py rename to opentelemetry-api/src/opentelemetry/resources/__init__.py diff --git a/opentelemetry-api/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py similarity index 100% rename from opentelemetry-api/opentelemetry/trace/__init__.py rename to opentelemetry-api/src/opentelemetry/trace/__init__.py diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md new file mode 100644 index 0000000000..09e4c38c76 --- /dev/null +++ b/opentelemetry-sdk/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## Unreleased +- Initial release diff --git a/opentelemetry-sdk/README.rst b/opentelemetry-sdk/README.rst new file mode 100644 index 0000000000..e2bc0f6a72 --- /dev/null +++ b/opentelemetry-sdk/README.rst @@ -0,0 +1,19 @@ +OpenTelemetry Python SDK +============================================================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-sdk.svg + :target: https://pypi.org/project/opentelemetry-sdk/ + +Installation +------------ + +:: + + pip install opentelemetry-sdk + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py new file mode 100644 index 0000000000..b95fecb27a --- /dev/null +++ b/opentelemetry-sdk/setup.py @@ -0,0 +1,51 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import setuptools + +base_dir = os.path.dirname(__file__) + +package_info = {} +with open(os.path.join(base_dir, "src", "opentelemetry", "sdk", "version.py")) as f: + exec(f.read(), package_info) + +setuptools.setup( + name="opentelemetry-sdk", + version=package_info["__version__"], # noqa + author="OpenTelemetry Authors", + author_email="cncf-opentelemetry-contributors@lists.cncf.io", + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + ], + description="OpenTelemetry Python SDK", + include_package_data=True, + long_description=open("README.rst").read(), + install_requires=[ + ], + extras_require={}, + license="Apache-2.0", + package_dir={"": "src"}, + packages=setuptools.find_namespace_packages(where="src"), + url="https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-sdk", + zip_safe=False, +) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py new file mode 100644 index 0000000000..2d52388da3 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .version import __version__ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py new file mode 100644 index 0000000000..a457c2b665 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.1.dev0" diff --git a/tox.ini b/tox.ini index aa70cf3b3e..7fdf9032a1 100644 --- a/tox.ini +++ b/tox.ini @@ -8,5 +8,5 @@ deps = py37-mypy: mypy commands = - py37-lint: pylint opentelemetry-api/opentelemetry/ - py37-mypy: mypy opentelemetry-api/opentelemetry/ + py37-lint: pylint opentelemetry-api/src/opentelemetry/ + py37-mypy: mypy opentelemetry-api/src/opentelemetry/ From 7c3150fb6112b21b0d891a2d3f394a1be3094d85 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 2 Jul 2019 14:04:50 -0700 Subject: [PATCH 0020/1517] Update sphinx conf for package move (#39) --- docs/conf.py | 2 +- docs/modules.rst | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 docs/modules.rst diff --git a/docs/conf.py b/docs/conf.py index a8e09c67d8..670494bd93 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,7 @@ import os import sys -sys.path.insert(0, os.path.abspath('../opentelemetry-api/')) +sys.path.insert(0, os.path.abspath('../opentelemetry-api/src/')) # -- Project information ----------------------------------------------------- diff --git a/docs/modules.rst b/docs/modules.rst deleted file mode 100644 index b795cd4515..0000000000 --- a/docs/modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry -============= - -.. toctree:: - :maxdepth: 4 - - opentelemetry.trace From bd1341172d229aee609b1563b78130a742d9dd18 Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Wed, 3 Jul 2019 19:36:13 +0200 Subject: [PATCH 0021/1517] Initial support for explicit Span parent support. (#33) --- .../src/opentelemetry/trace/__init__.py | 150 ++++++++++-------- 1 file changed, 84 insertions(+), 66 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 6bae25c276..10f4691f6e 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -66,6 +66,60 @@ import typing +class Span: + """A span represents a single operation within a trace.""" + + def start(self) -> None: + """Sets the current time as the span's start time. + + Each span represents a single operation. The span's start time is the + wall time at which the operation started. + + Only the first call to ``start`` should modify the span, and + implementations are free to ignore or raise on further calls. + """ + + def end(self) -> None: + """Sets the current time as the span's end time. + + The span's end time is the wall time at which the operation finished. + + Only the first call to ``end`` should modify the span, and + implementations are free to ignore or raise on further calls. + """ + + def get_context(self) -> 'SpanContext': + """Gets the span's SpanContext. + + Get an immutable, serializable identifier for this span that can be + used to create new child spans. + + Returns: + A :class:`.SpanContext` with a copy of this span's immutable state. + """ + + +class SpanContext: + """The state of a Span to propagate between processes. + + This class includes the immutable attributes of a :class:`.Span` that must + be propagated to a span's children and across process boundaries. + + Args: + trace_id: The ID of the trace that this span belongs to. + span_id: This span's ID. + options: Trace options to propagate. + state: Tracing-system-specific info to propagate. + """ + + def __init__(self, + trace_id: str, + span_id: str, + options: 'TraceOptions', + state: 'TraceState') -> None: + pass + + class Tracer: """Handles span creation and in-process context propagation. @@ -73,6 +127,10 @@ class Tracer: and controlling spans' lifecycles. """ + # Constant used to represent the current span being used as a parent. + # This is the default behavior when creating spans. + CURRENT_SPAN = Span() + def get_current_span(self) -> 'Span': """Gets the currently active span from the context. @@ -84,15 +142,21 @@ def get_current_span(self) -> 'Span': invalid :class:`.SpanContext`. """ - @contextmanager # type: ignore - def start_span(self, name: str, parent: 'Span') -> typing.Iterator['Span']: + def start_span(self, + name: str, + parent: typing.Union['Span', 'SpanContext'] = CURRENT_SPAN + ) -> typing.Iterator['Span']: """Context manager for span creation. - Create a new child of the current span, or create a root span if no - current span exists. Start the span and set it as the current span in + Create a new span. Start the span and set it as the current span in this tracer's context. + By default the current span will be used as parent, but an explicit + parent can also be specified, either a ``Span`` or a ``SpanContext``. + If the specified value is ``None``, the created span will be a root + span. + On exiting the context manager stop the span and set its parent as the current span. @@ -115,40 +179,48 @@ def start_span(self, name: str, parent: 'Span') -> typing.Iterator['Span']: is equivalent to:: - span = tracer.create_span(name, parent=tracer.get_current_span()) + span = tracer.create_span(name) with tracer.use_span(span): do_work() Args: name: The name of the span to be created. - parent: The span's parent. + parent: The span's parent. Defaults to the current span. Yields: The newly-created span. """ - def create_span(self, name: str, parent: 'Span') -> 'Span': - """Creates a new child span of the given parent. + def create_span(self, + name: str, + parent: typing.Union['Span', 'SpanContext'] = CURRENT_SPAN + ) -> 'Span': + """Creates a span. Creating the span does not start it, and should not affect the tracer's context. To start the span and update the tracer's context to make it the currently active span, see :meth:`use_span`. - Applications that need to explicitly set spans' parents or create spans - detached from the tracer's context should use this method. + By default the current span will be used as parent, but an explicit + parent can also be specified, either a Span or a SpanContext. + If the specified value is `None`, the created span will be a root + span. + + Applications that need to create spans detached from the tracer's + context should use this method. with tracer.start_span(name) as span: do_work() This is equivalent to:: - span = tracer.create_span(name, parent=tracer.get_current_span()) + span = tracer.create_span(name) with tracer.use_span(span): do_work() Args: name: The name of the span to be created. - parent: The span's parent. + parent: The span's parent. Defaults to the current span. Returns: The newly-created span. @@ -169,60 +241,6 @@ def use_span(self, span: 'Span') -> typing.Iterator[None]: """ -class Span: - """A span represents a single operation within a trace.""" - - def start(self) -> None: - """Sets the current time as the span's start time. - - Each span represents a single operation. The span's start time is the - wall time at which the operation started. - - Only the first call to ``start`` should modify the span, and - implementations are free to ignore or raise on further calls. - """ - - def end(self) -> None: - """Sets the current time as the span's end time. - - The span's end time is the wall time at which the operation finished. - - Only the first call to ``end`` should modify the span, and - implementations are free to ignore or raise on further calls. - """ - - def get_context(self) -> 'SpanContext': - """Gets the span's SpanContext. - - Get an immutable, serializable identifier for this span that can be - used to create new child spans. - - Returns: - A :class:`.SpanContext` with a copy of this span's immutable state. - """ - - -class SpanContext: - """The state of a Span to propagate between processes. - - This class includes the immutable attributes of a :class:`.Span` that must - be propagated to a span's children and across process boundaries. - - Args: - trace_id: The ID of the trace that this span belongs to. - span_id: This span's ID. - options: Trace options to propagate. - state: Tracing-system-specific info to propagate. - """ - - def __init__(self, - trace_id: str, - span_id: str, - options: 'TraceOptions', - state: 'TraceState') -> None: - pass - - # TODO class TraceOptions(int): pass From 826346f7ccca14e8a25e2d122b4effd7db563f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 8 Jul 2019 10:28:34 +0200 Subject: [PATCH 0022/1517] Misc build improvements (build docs & more) (#38) * Build docs. * Minor tox.ini improvements. * .gitignore mypy * Fix doc build. * Tell travis to build docs (with 3.7). --- .gitignore | 3 +++ tox.ini | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index b63de1b7f0..679b6fd0cc 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ htmlcov # Sphinx _build/ + +# mypy +.mypy_cache/ diff --git a/tox.ini b/tox.ini index 7fdf9032a1..4997e45f35 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,27 @@ [tox] skipsdist = True -envlist = py37-lint, py37-mypy +envlist = py37-{lint,mypy}, docs + +[travis] +python = + 3.7: py37, docs [testenv] deps = - py37-lint: pylint - py37-mypy: mypy + lint: pylint~=2.3.1 + mypy: mypy~=0.711 + commands = py37-lint: pylint opentelemetry-api/src/opentelemetry/ py37-mypy: mypy opentelemetry-api/src/opentelemetry/ + +[testenv:docs] +deps = + sphinx~=2.1.2 + sphinx-rtd-theme~=0.4.3 + sphinx-autodoc-typehints~=1.6.0 + +commands = + sphinx-build -W --keep-going -b html -T . _build/html +changedir = docs From d78ea98f1a07365183a18dc6f805bbf103a19ffe Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 8 Jul 2019 13:45:55 -0700 Subject: [PATCH 0023/1517] Change sphinx default role to "any", update docs (#40) --- docs/conf.py | 3 +++ opentelemetry-api/src/opentelemetry/trace/__init__.py | 9 ++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 670494bd93..c4fee51f31 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,6 +24,9 @@ # -- General configuration --------------------------------------------------- +# Easy automatic cross-references for `code in backticks` +default_role = 'any' + # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 10f4691f6e..09602fdfb4 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -75,7 +75,7 @@ def start(self) -> None: Each span represents a single operation. The span's start time is the wall time at which the operation started. - Only the first call to ``start`` should modify the span, and + Only the first call to `start` should modify the span, and implementations are free to ignore or raise on further calls. """ @@ -84,7 +84,7 @@ def end(self) -> None: The span's end time is the wall time at which the operation finished. - Only the first call to ``end`` should modify the span, and + Only the first call to `end` should modify the span, and implementations are free to ignore or raise on further calls. """ @@ -153,9 +153,8 @@ def start_span(self, this tracer's context. By default the current span will be used as parent, but an explicit - parent can also be specified, either a ``Span`` or a ``SpanContext``. - If the specified value is ``None``, the created span will be a root - span. + parent can also be specified, either a `Span` or a `SpanContext`. If + the specified value is `None`, the created span will be a root span. On exiting the context manager stop the span and set its parent as the current span. From 9f0527de56f453b3dec1af7ed856af71b5678a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 10 Jul 2019 13:30:27 +0200 Subject: [PATCH 0024/1517] Add "global tracer"/backend objects (#15) (#29) * Global tracer registry: First somewhat satisfying iteration. * Add API docs for backend module * Fix documentation (and overlong lines within). * Sort imports. * Remove _UNIT_TEST_IGNORE_ENV. We should be able to ensure clean envvars for the test run. * Misc cleanup. * Fix backend.py not being included in wheel. * Rewrite backend/loader. * Rewrite backend/loader more, fix pylint, mypy. * Ditch 'default' in `_load_default_impl`, it's just wrong now. * Fix docs. * Apply new package structure. * Remove unit tests (for now). * Document the factory type aliases. * Store factory functions in respective modules. Gets rid of the dictionary in loader.py. * Fix factory return type (Optional) & improve docs. * Revert accidental changes to setup.py. * Remove unused global. --- docs/index.rst | 1 + docs/opentelemetry.loader.rst | 4 + opentelemetry-api/src/opentelemetry/loader.py | 170 ++++++++++++++++++ .../src/opentelemetry/trace/__init__.py | 43 ++++- 4 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 docs/opentelemetry.loader.rst create mode 100644 opentelemetry-api/src/opentelemetry/loader.py diff --git a/docs/index.rst b/docs/index.rst index aed678db96..90385dba4a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,7 @@ abstract types for OpenTelemetry implementations. :caption: Contents: opentelemetry.trace + opentelemetry.loader Indices and tables diff --git a/docs/opentelemetry.loader.rst b/docs/opentelemetry.loader.rst new file mode 100644 index 0000000000..bd6dd698f8 --- /dev/null +++ b/docs/opentelemetry.loader.rst @@ -0,0 +1,4 @@ +opentelemetry.loader module +=========================== + +.. automodule:: opentelemetry.loader diff --git a/opentelemetry-api/src/opentelemetry/loader.py b/opentelemetry-api/src/opentelemetry/loader.py new file mode 100644 index 0000000000..dc3a4e078e --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/loader.py @@ -0,0 +1,170 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +""" +The OpenTelemetry loader module is mainly used internally to load the +implementation for global objects like :func:`opentelemetry.trace.tracer`. + +.. _loader-factory: + +An instance of a global object of type ``T`` is always created with a factory +function with the following signature:: + + def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: + # ... + +That function is called with e.g., the type of the global object it should +create as an argument (e.g. the type object +:class:`opentelemetry.trace.Tracer`) and should return an instance of that type +(such that ``instanceof(my_factory_for_t(T), T)`` is true). Alternatively, it +may return ``None`` to indicate that the no-op default should be used. + +When loading an implementation, the following algorithm is used to find a +factory function or other means to create the global object: + + 1. If the environment variable + :samp:`OPENTELEMETRY_PYTHON_IMPLEMENTATION_{getter-name}` (e.g., + ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_TRACER``) is set to an nonempty + value, an attempt is made to import a module with that name and use a + factory function named ``get_opentelemetry_implementation`` in it. + 2. Otherwise, the same is tried with the environment + variable ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT``. + 3. Otherwise, if a :samp:`set_preferred_{}_implementation` was + called (e.g. + :func:`opentelemetry.trace.set_preferred_tracer_implementation`), the + callback set there is used (that is, the environment variables override + the callback set in code). + 4. Otherwise, if :func:`set_preferred_default_implementation` was called, + the callback set there is used. + 5. Otherwise, an attempt is made to import and use the OpenTelemetry SDK. + 6. Otherwise the default implementation that ships with the API + distribution (a fast no-op implementation) is used. + +If any of the above steps fails (e.g., a module is loaded but does not define +the required function or a module name is set but the module fails to load), +the search immediatelly skips to the last step. + +Note that the first two steps (those that query environment variables) are +skipped if :data:`sys.flags` has ``ignore_environment`` set (which usually +means that the Python interpreter was invoked with the ``-E`` or ``-I`` flag). +""" + +from typing import Type, TypeVar, Optional, Callable +import importlib +import os +import sys + +_T = TypeVar('_T') + +# "Untrusted" because this is usually user-provided and we don't trust the user +# to really return a _T: by using object, mypy forces us to check/cast +# explicitly. +_UntrustedImplFactory = Callable[[Type[_T]], Optional[object]] + + +# This would be the normal ImplementationFactory which would be used to +# annotate setters, were it not for https://github.com/python/mypy/issues/7092 +# Once that bug is resolved, setters should use this instead of duplicating the +# code. +#ImplementationFactory = Callable[[Type[_T]], Optional[_T]] + +_DEFAULT_FACTORY: Optional[_UntrustedImplFactory] = None + +def _try_load_impl_from_modname( + implementation_modname: str, api_type: Type[_T]) -> Optional[_T]: + try: + implementation_mod = importlib.import_module(implementation_modname) + except (ImportError, SyntaxError): + # TODO Log/warn + return None + + return _try_load_impl_from_mod(implementation_mod, api_type) + +def _try_load_impl_from_mod( + implementation_mod: object, api_type: Type[_T]) -> Optional[_T]: + + try: + # Note: We use such a long name to avoid calling a function that is not + # intended for this API. + + implementation_fn = getattr( + implementation_mod, + 'get_opentelemetry_implementation') # type: _UntrustedImplFactory + except AttributeError: + # TODO Log/warn + return None + + return _try_load_impl_from_callback(implementation_fn, api_type) + +def _try_load_impl_from_callback( + implementation_fn: _UntrustedImplFactory, + api_type: Type[_T] + ) -> Optional[_T]: + result = implementation_fn(api_type) + if result is None: + return None + if not isinstance(result, api_type): + # TODO Warn if wrong type is returned + return None + + # TODO: Warn if implementation_fn returns api_type(): It should return None + # to indicate using the default. + + return result + + +def _try_load_configured_impl( + api_type: Type[_T], factory: Optional[_UntrustedImplFactory[_T]] + ) -> Optional[_T]: + """Attempts to find any specially configured implementation. If none is + configured, or a load error occurs, returns `None` + """ + implementation_modname = None + if sys.flags.ignore_environment: + return None + implementation_modname = os.getenv( + 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_' + api_type.__name__.upper()) + if implementation_modname: + return _try_load_impl_from_modname(implementation_modname, api_type) + implementation_modname = os.getenv( + 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT') + if implementation_modname: + return _try_load_impl_from_modname(implementation_modname, api_type) + if factory is not None: + return _try_load_impl_from_callback(factory, api_type) + if _DEFAULT_FACTORY is not None: + return _try_load_impl_from_callback(_DEFAULT_FACTORY, api_type) + return None + +# Public to other opentelemetry-api modules +def _load_impl( + api_type: Type[_T], + factory: Optional[Callable[[Type[_T]], Optional[_T]]] + ) -> _T: + """Tries to load a configured implementation, if unsuccessful, returns a + fast no-op implemenation that is always available. + """ + + result = _try_load_configured_impl(api_type, factory) + if result is None: + return api_type() + return result + +def set_preferred_default_implementation( + implementation_factory: _UntrustedImplFactory) -> None: + """Sets a factory function that may be called for any implementation + object. See the :ref:`module docs ` for more details.""" + global _DEFAULT_FACTORY #pylint:disable=global-statement + _DEFAULT_FACTORY = implementation_factory diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 09602fdfb4..6a789276d9 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -64,7 +64,7 @@ from contextlib import contextmanager import typing - +from opentelemetry import loader class Span: """A span represents a single operation within a trace.""" @@ -119,7 +119,6 @@ def __init__(self, state: 'TraceState') -> None: pass - class Tracer: """Handles span creation and in-process context propagation. @@ -248,3 +247,43 @@ class TraceOptions(int): # TODO class TraceState(typing.Dict[str, str]): pass + + +_TRACER: typing.Optional[Tracer] = None +_TRACER_FACTORY: typing.Optional[ + typing.Callable[[typing.Type[Tracer]], typing.Optional[Tracer]]] = None + +def tracer() -> Tracer: + """Gets the current global :class:`~.Tracer` object. + If there isn't one set yet, a default will be loaded.""" + + global _TRACER, _TRACER_FACTORY #pylint:disable=global-statement + + if _TRACER is None: + #pylint:disable=protected-access + _TRACER = loader._load_impl(Tracer, _TRACER_FACTORY) + del _TRACER_FACTORY + + return _TRACER + +def set_preferred_tracer_implementation( + factory: typing.Callable[ + [typing.Type[Tracer]], typing.Optional[Tracer]] + ) -> None: + """Sets a factory function which may be used to create the tracer + implementation. + + See :mod:`opentelemetry.loader` for details. + + This function may not be called after a tracer is already loaded. + + Args: + factory: Callback that should create a new :class:`Tracer` instance. + """ + + global _TRACER_FACTORY #pylint:disable=global-statement + + if _TRACER: + raise RuntimeError("Tracer already loaded.") + + _TRACER_FACTORY = factory From eb10c5318481bd0aec2ac0fd1eb05d49e6ccc1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Fri, 12 Jul 2019 14:28:47 +0200 Subject: [PATCH 0025/1517] Add unittest based unit-tests. (#42) --- opentelemetry-api/tests/__init__.py | 13 ++++ opentelemetry-api/tests/test_loader.py | 95 ++++++++++++++++++++++++++ tox.ini | 17 ++++- 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 opentelemetry-api/tests/__init__.py create mode 100644 opentelemetry-api/tests/test_loader.py diff --git a/opentelemetry-api/tests/__init__.py b/opentelemetry-api/tests/__init__.py new file mode 100644 index 0000000000..d853a7bcf6 --- /dev/null +++ b/opentelemetry-api/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-api/tests/test_loader.py b/opentelemetry-api/tests/test_loader.py new file mode 100644 index 0000000000..72d008499f --- /dev/null +++ b/opentelemetry-api/tests/test_loader.py @@ -0,0 +1,95 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from importlib import reload +import sys +import unittest +import os + +from opentelemetry import loader +from opentelemetry import trace + +DUMMY_TRACER = None + +class DummyTracer(trace.Tracer): + pass + +def get_opentelemetry_implementation(type_): + global DUMMY_TRACER #pylint:disable=global-statement + assert type_ is trace.Tracer + DUMMY_TRACER = DummyTracer() + return DUMMY_TRACER + +#pylint:disable=redefined-outer-name,protected-access,unidiomatic-typecheck + +class TestLoader(unittest.TestCase): + + def setUp(self): + reload(loader) + reload(trace) + + # Need to reload self, otherwise DummyTracer will have the wrong base + # class after reloading `trace`. + reload(sys.modules[__name__]) + + + def test_get_default(self): + tracer = trace.tracer() + self.assertIs(type(tracer), trace.Tracer) + + def test_preferred_impl(self): + trace.set_preferred_tracer_implementation( + get_opentelemetry_implementation) + tracer = trace.tracer() + self.assertIs(tracer, DUMMY_TRACER) + + # NOTE: We use do_* + *_ methods because subtest wouldn't run setUp, + # which we require here. + def do_test_preferred_impl(self, setter): + setter(get_opentelemetry_implementation) + tracer = trace.tracer() + self.assertIs(tracer, DUMMY_TRACER) + def test_preferred_impl_with_tracer(self): + self.do_test_preferred_impl(trace.set_preferred_tracer_implementation) + def test_preferred_impl_with_default(self): + self.do_test_preferred_impl( + loader.set_preferred_default_implementation) + + def test_try_set_again(self): + self.assertTrue(trace.tracer()) + # Try setting after the tracer has already been created: + with self.assertRaises(RuntimeError) as einfo: + trace.set_preferred_tracer_implementation( + get_opentelemetry_implementation) + self.assertIn('already loaded', str(einfo.exception)) + + def do_test_get_envvar(self, envvar_suffix): + global DUMMY_TRACER #pylint:disable=global-statement + + # Test is not runnable with this! + self.assertFalse(sys.flags.ignore_environment) + + envname = 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_' + envvar_suffix + os.environ[envname] = __name__ + try: + tracer = trace.tracer() + self.assertIs(tracer, DUMMY_TRACER) + finally: + DUMMY_TRACER = None + del os.environ[envname] + self.assertIs(type(tracer), DummyTracer) + def test_get_envvar_tracer(self): + return self.do_test_get_envvar('TRACER') + def test_get_envvar_default(self): + return self.do_test_get_envvar('DEFAULT') diff --git a/tox.ini b/tox.ini index 4997e45f35..20eb439414 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] skipsdist = True -envlist = py37-{lint,mypy}, docs +envlist = py37-{lint,mypy,test}, docs [travis] python = @@ -11,10 +11,23 @@ deps = lint: pylint~=2.3.1 mypy: mypy~=0.711 +setenv = + PYTHONPATH={toxinidir}/opentelemetry-api/src/ + mypy: MYPYPATH={env:PYTHONPATH} + +changedir = + test: opentelemetry-api/tests + commands = - py37-lint: pylint opentelemetry-api/src/opentelemetry/ +; Prefer putting everything in one pylint command to profit from duplication +; warnings. + py37-lint: pylint opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ py37-mypy: mypy opentelemetry-api/src/opentelemetry/ +; For test code, we don't want to use the full mypy strictness, so we use its +; default flags instead. + py37-mypy: mypy opentelemetry-api/tests/ --config-file= + test: python -m unittest discover [testenv:docs] deps = From 69234bdcbc198d996602fd6a66e594e3d675af97 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 15 Jul 2019 13:57:12 -0700 Subject: [PATCH 0026/1517] Add default invalid SpanContext (#47) and TraceState, TraceOptions classes. --- .../src/opentelemetry/trace/__init__.py | 93 +++++++++++++++---- 1 file changed, 73 insertions(+), 20 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 6a789276d9..0660f9e394 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -66,6 +66,7 @@ import typing from opentelemetry import loader + class Span: """A span represents a single operation within a trace.""" @@ -99,6 +100,47 @@ def get_context(self) -> 'SpanContext': """ +class TraceOptions(int): + """A bitmask that represents options specific to the trace. + + The only supported option is the "recorded" flag (``0x01``). If set, this + flag indicates that the trace may have been recorded upstream. + + See the `W3C Trace Context - Traceparent`_ spec for details. + + .. _W3C Trace Context - Traceparent: + https://www.w3.org/TR/trace-context/#trace-flags + """ + DEFAULT = 0x00 + RECORDED = 0x01 + + @classmethod + def get_default(cls) -> 'TraceOptions': + return cls(cls.DEFAULT) + + +DEFAULT_TRACEOPTIONS = TraceOptions.get_default() + + +class TraceState(typing.Dict[str, str]): + """A list of key-value pairs representing vendor-specific trace info. + + Keys and values are strings of up to 256 printable US-ASCII characters. + Implementations should conform to the the `W3C Trace Context - Tracestate`_ + spec, which describes additional restrictions on valid field values. + + .. _W3C Trace Context - Tracestate: + https://www.w3.org/TR/trace-context/#tracestate-field + """ + + @classmethod + def get_default(cls) -> 'TraceState': + return cls() + + +DEFAULT_TRACESTATE = TraceState.get_default() + + class SpanContext: """The state of a Span to propagate between processes. @@ -113,11 +155,31 @@ class SpanContext: """ def __init__(self, - trace_id: str, - span_id: str, + trace_id: int, + span_id: int, options: 'TraceOptions', state: 'TraceState') -> None: - pass + self.trace_id = trace_id + self.span_id = span_id + self.options = options + self.state = state + + def is_valid(self) -> bool: + """Get whether this `SpanContext` is valid. + + A `SpanContext` is said to be invalid if its trace ID or span ID is + invalid (i.e. ``0``). + + Returns: + True if the `SpanContext` is valid, false otherwise. + """ + + +INVALID_SPAN_ID = 0 +INVALID_TRACE_ID = 0 +INVALID_SPAN_CONTEXT = SpanContext(INVALID_TRACE_ID, INVALID_SPAN_ID, + DEFAULT_TRACEOPTIONS, DEFAULT_TRACESTATE) + class Tracer: """Handles span creation and in-process context propagation. @@ -239,39 +301,31 @@ def use_span(self, span: 'Span') -> typing.Iterator[None]: """ -# TODO -class TraceOptions(int): - pass - - -# TODO -class TraceState(typing.Dict[str, str]): - pass - - _TRACER: typing.Optional[Tracer] = None _TRACER_FACTORY: typing.Optional[ typing.Callable[[typing.Type[Tracer]], typing.Optional[Tracer]]] = None + def tracer() -> Tracer: """Gets the current global :class:`~.Tracer` object. - If there isn't one set yet, a default will be loaded.""" - global _TRACER, _TRACER_FACTORY #pylint:disable=global-statement + If there isn't one set yet, a default will be loaded. + """ + global _TRACER, _TRACER_FACTORY # pylint:disable=global-statement if _TRACER is None: - #pylint:disable=protected-access + # pylint:disable=protected-access _TRACER = loader._load_impl(Tracer, _TRACER_FACTORY) del _TRACER_FACTORY return _TRACER + def set_preferred_tracer_implementation( factory: typing.Callable[ [typing.Type[Tracer]], typing.Optional[Tracer]] ) -> None: - """Sets a factory function which may be used to create the tracer - implementation. + """Set the factory to be used to create the tracer. See :mod:`opentelemetry.loader` for details. @@ -280,8 +334,7 @@ def set_preferred_tracer_implementation( Args: factory: Callback that should create a new :class:`Tracer` instance. """ - - global _TRACER_FACTORY #pylint:disable=global-statement + global _TRACER_FACTORY # pylint:disable=global-statement if _TRACER: raise RuntimeError("Tracer already loaded.") From 677e848547f23206a5ed4510123b1f7de0ecd7a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 16 Jul 2019 01:03:02 +0200 Subject: [PATCH 0027/1517] Lint more files, relax some type checks (#43) --- mypy-relaxed.ini | 22 +++++++++++++++++++ mypy.ini | 1 + opentelemetry-api/setup.py | 16 ++++++++------ opentelemetry-api/src/opentelemetry/loader.py | 9 ++++---- tox.ini | 8 +++---- 5 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 mypy-relaxed.ini diff --git a/mypy-relaxed.ini b/mypy-relaxed.ini new file mode 100644 index 0000000000..8f2be85aff --- /dev/null +++ b/mypy-relaxed.ini @@ -0,0 +1,22 @@ +; This is mainly intended for unit tests and such. So proably going forward, we +; will disable even more warnings here. +[mypy] + disallow_any_unimported = True +; disallow_any_expr = True + disallow_any_decorated = True + disallow_any_explicit = True + disallow_any_generics = True + disallow_subclassing_any = True + disallow_untyped_calls = True +; disallow_untyped_defs = True + disallow_incomplete_defs = True + check_untyped_defs = True + disallow_untyped_decorators = True + allow_untyped_globals = True +; Due to disabling some other warnings, unused ignores may occur. +; warn_unused_ignores = True + warn_return_any = True + strict_equality = True + +[mypy-setuptools] + ignore_missing_imports = True diff --git a/mypy.ini b/mypy.ini index 5899d1954e..5b46777838 100644 --- a/mypy.ini +++ b/mypy.ini @@ -12,3 +12,4 @@ disallow_untyped_decorators = True warn_unused_ignores = True warn_return_any = True + strict_equality = True diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index de97ec88c1..d2a27a0d44 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -15,15 +15,16 @@ import os import setuptools -base_dir = os.path.dirname(__file__) - -package_info = {} -with open(os.path.join(base_dir, "src", "opentelemetry", "internal", "version.py")) as f: - exec(f.read(), package_info) +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "internal", "version.py") +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) #pylint:disable=exec-used setuptools.setup( name="opentelemetry-api", - version=package_info["__version__"], # noqa + version=PACKAGE_INFO["__version__"], # noqa author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ @@ -47,6 +48,7 @@ license="Apache-2.0", package_dir={"": "src"}, packages=setuptools.find_namespace_packages(where="src"), - url="https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-api", + url=("https://github.com/open-telemetry/opentelemetry-python" + "/tree/master/opentelemetry-api"), zip_safe=False, ) diff --git a/opentelemetry-api/src/opentelemetry/loader.py b/opentelemetry-api/src/opentelemetry/loader.py index dc3a4e078e..3c2fdcab6a 100644 --- a/opentelemetry-api/src/opentelemetry/loader.py +++ b/opentelemetry-api/src/opentelemetry/loader.py @@ -80,7 +80,7 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: # code. #ImplementationFactory = Callable[[Type[_T]], Optional[_T]] -_DEFAULT_FACTORY: Optional[_UntrustedImplFactory] = None +_DEFAULT_FACTORY: Optional[_UntrustedImplFactory[object]] = None def _try_load_impl_from_modname( implementation_modname: str, api_type: Type[_T]) -> Optional[_T]: @@ -101,7 +101,8 @@ def _try_load_impl_from_mod( implementation_fn = getattr( implementation_mod, - 'get_opentelemetry_implementation') # type: _UntrustedImplFactory + 'get_opentelemetry_implementation' + ) # type: _UntrustedImplFactory[_T] except AttributeError: # TODO Log/warn return None @@ -109,7 +110,7 @@ def _try_load_impl_from_mod( return _try_load_impl_from_callback(implementation_fn, api_type) def _try_load_impl_from_callback( - implementation_fn: _UntrustedImplFactory, + implementation_fn: _UntrustedImplFactory[_T], api_type: Type[_T] ) -> Optional[_T]: result = implementation_fn(api_type) @@ -163,7 +164,7 @@ def _load_impl( return result def set_preferred_default_implementation( - implementation_factory: _UntrustedImplFactory) -> None: + implementation_factory: _UntrustedImplFactory[_T]) -> None: """Sets a factory function that may be called for any implementation object. See the :ref:`module docs ` for more details.""" global _DEFAULT_FACTORY #pylint:disable=global-statement diff --git a/tox.ini b/tox.ini index 20eb439414..bd2dae1c56 100644 --- a/tox.ini +++ b/tox.ini @@ -22,11 +22,11 @@ changedir = commands = ; Prefer putting everything in one pylint command to profit from duplication ; warnings. - py37-lint: pylint opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ + py37-lint: pylint opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ opentelemetry-api/setup.py py37-mypy: mypy opentelemetry-api/src/opentelemetry/ -; For test code, we don't want to use the full mypy strictness, so we use its -; default flags instead. - py37-mypy: mypy opentelemetry-api/tests/ --config-file= +; For test code, we don't want to enforce the full mypy strictness + py37-mypy: mypy opentelemetry-api/src/opentelemetry/ + py37-mypy: mypy --config-file=mypy-relaxed.ini opentelemetry-api/tests/ opentelemetry-api/setup.py test: python -m unittest discover [testenv:docs] From 3d0046a6c37529ccdcc17395ee4dc132bd31628a Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 16 Jul 2019 10:34:40 -0700 Subject: [PATCH 0028/1517] Add flake8 and isort lint checks (#46) --- .isort.cfg | 2 ++ opentelemetry-api/src/opentelemetry/loader.py | 17 ++++++---- .../src/opentelemetry/trace/__init__.py | 1 + opentelemetry-api/tests/test_loader.py | 16 +++++++--- tox.ini | 32 ++++++++++++------- 5 files changed, 44 insertions(+), 24 deletions(-) create mode 100644 .isort.cfg diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000000..3d19a1c6e0 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +from_first=True diff --git a/opentelemetry-api/src/opentelemetry/loader.py b/opentelemetry-api/src/opentelemetry/loader.py index 3c2fdcab6a..cf2069edb4 100644 --- a/opentelemetry-api/src/opentelemetry/loader.py +++ b/opentelemetry-api/src/opentelemetry/loader.py @@ -61,7 +61,7 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: means that the Python interpreter was invoked with the ``-E`` or ``-I`` flag). """ -from typing import Type, TypeVar, Optional, Callable +from typing import Callable, Optional, Type, TypeVar import importlib import os import sys @@ -78,10 +78,11 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: # annotate setters, were it not for https://github.com/python/mypy/issues/7092 # Once that bug is resolved, setters should use this instead of duplicating the # code. -#ImplementationFactory = Callable[[Type[_T]], Optional[_T]] +# ImplementationFactory = Callable[[Type[_T]], Optional[_T]] _DEFAULT_FACTORY: Optional[_UntrustedImplFactory[object]] = None + def _try_load_impl_from_modname( implementation_modname: str, api_type: Type[_T]) -> Optional[_T]: try: @@ -92,6 +93,7 @@ def _try_load_impl_from_modname( return _try_load_impl_from_mod(implementation_mod, api_type) + def _try_load_impl_from_mod( implementation_mod: object, api_type: Type[_T]) -> Optional[_T]: @@ -109,6 +111,7 @@ def _try_load_impl_from_mod( return _try_load_impl_from_callback(implementation_fn, api_type) + def _try_load_impl_from_callback( implementation_fn: _UntrustedImplFactory[_T], api_type: Type[_T] @@ -149,11 +152,10 @@ def _try_load_configured_impl( return _try_load_impl_from_callback(_DEFAULT_FACTORY, api_type) return None + # Public to other opentelemetry-api modules -def _load_impl( - api_type: Type[_T], - factory: Optional[Callable[[Type[_T]], Optional[_T]]] - ) -> _T: +def _load_impl(api_type: Type[_T], + factory: Optional[Callable[[Type[_T]], Optional[_T]]]) -> _T: """Tries to load a configured implementation, if unsuccessful, returns a fast no-op implemenation that is always available. """ @@ -163,9 +165,10 @@ def _load_impl( return api_type() return result + def set_preferred_default_implementation( implementation_factory: _UntrustedImplFactory[_T]) -> None: """Sets a factory function that may be called for any implementation object. See the :ref:`module docs ` for more details.""" - global _DEFAULT_FACTORY #pylint:disable=global-statement + global _DEFAULT_FACTORY # pylint:disable=global-statement _DEFAULT_FACTORY = implementation_factory diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 0660f9e394..d5fd5446a4 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -64,6 +64,7 @@ from contextlib import contextmanager import typing + from opentelemetry import loader diff --git a/opentelemetry-api/tests/test_loader.py b/opentelemetry-api/tests/test_loader.py index 72d008499f..68809468f5 100644 --- a/opentelemetry-api/tests/test_loader.py +++ b/opentelemetry-api/tests/test_loader.py @@ -13,25 +13,28 @@ # limitations under the License. from importlib import reload +import os import sys import unittest -import os from opentelemetry import loader from opentelemetry import trace DUMMY_TRACER = None + class DummyTracer(trace.Tracer): pass + def get_opentelemetry_implementation(type_): - global DUMMY_TRACER #pylint:disable=global-statement + global DUMMY_TRACER # pylint:disable=global-statement assert type_ is trace.Tracer DUMMY_TRACER = DummyTracer() return DUMMY_TRACER -#pylint:disable=redefined-outer-name,protected-access,unidiomatic-typecheck + +# pylint:disable=redefined-outer-name,protected-access,unidiomatic-typecheck class TestLoader(unittest.TestCase): @@ -43,7 +46,6 @@ def setUp(self): # class after reloading `trace`. reload(sys.modules[__name__]) - def test_get_default(self): tracer = trace.tracer() self.assertIs(type(tracer), trace.Tracer) @@ -60,8 +62,10 @@ def do_test_preferred_impl(self, setter): setter(get_opentelemetry_implementation) tracer = trace.tracer() self.assertIs(tracer, DUMMY_TRACER) + def test_preferred_impl_with_tracer(self): self.do_test_preferred_impl(trace.set_preferred_tracer_implementation) + def test_preferred_impl_with_default(self): self.do_test_preferred_impl( loader.set_preferred_default_implementation) @@ -75,7 +79,7 @@ def test_try_set_again(self): self.assertIn('already loaded', str(einfo.exception)) def do_test_get_envvar(self, envvar_suffix): - global DUMMY_TRACER #pylint:disable=global-statement + global DUMMY_TRACER # pylint:disable=global-statement # Test is not runnable with this! self.assertFalse(sys.flags.ignore_environment) @@ -89,7 +93,9 @@ def do_test_get_envvar(self, envvar_suffix): DUMMY_TRACER = None del os.environ[envname] self.assertIs(type(tracer), DummyTracer) + def test_get_envvar_tracer(self): return self.do_test_get_envvar('TRACER') + def test_get_envvar_default(self): return self.do_test_get_envvar('DEFAULT') diff --git a/tox.ini b/tox.ini index bd2dae1c56..085d260c26 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,13 @@ [tox] skipsdist = True -envlist = py37-{lint,mypy,test}, docs +envlist = py{34,35,36,37}-test, lint, py37-mypy, docs [travis] python = - 3.7: py37, docs + 3.7: py37, lint, docs [testenv] deps = - lint: pylint~=2.3.1 mypy: mypy~=0.711 setenv = @@ -18,23 +17,32 @@ setenv = changedir = test: opentelemetry-api/tests - commands = -; Prefer putting everything in one pylint command to profit from duplication -; warnings. - py37-lint: pylint opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ opentelemetry-api/setup.py py37-mypy: mypy opentelemetry-api/src/opentelemetry/ ; For test code, we don't want to enforce the full mypy strictness - py37-mypy: mypy opentelemetry-api/src/opentelemetry/ py37-mypy: mypy --config-file=mypy-relaxed.ini opentelemetry-api/tests/ opentelemetry-api/setup.py test: python -m unittest discover +[testenv:lint] +deps = + pylint~=2.3 + flake8~=3.7 + isort~=4.3 + +commands = +; Prefer putting everything in one pylint command to profit from duplication +; warnings. + pylint opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ + flake8 opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ + isort --check-only --recursive opentelemetry-api/src + [testenv:docs] deps = - sphinx~=2.1.2 - sphinx-rtd-theme~=0.4.3 - sphinx-autodoc-typehints~=1.6.0 + sphinx~=2.1 + sphinx-rtd-theme~=0.4 + sphinx-autodoc-typehints~=1.6 + +changedir = docs commands = sphinx-build -W --keep-going -b html -T . _build/html -changedir = docs From 7a7a20798e403a5339fb9efa7a77a07cd8edcd86 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Wed, 17 Jul 2019 16:05:59 -0700 Subject: [PATCH 0029/1517] Add stub tracer SDK (#55) --- opentelemetry-api/setup.py | 10 ++++---- .../src/opentelemetry/trace/__init__.py | 3 +-- .../opentelemetry/{internal => }/version.py | 0 opentelemetry-sdk/setup.py | 20 +++++++++------ .../src/opentelemetry/sdk/__init__.py | 6 ++++- .../src/opentelemetry/sdk/trace/__init__.py | 19 ++++++++++++++ .../tests}/__init__.py | 0 .../tests/trace}/__init__.py | 6 ----- opentelemetry-sdk/tests/trace/test_trace.py | 25 +++++++++++++++++++ tox.ini | 22 ++++++++++------ 10 files changed, 82 insertions(+), 29 deletions(-) rename opentelemetry-api/src/opentelemetry/{internal => }/version.py (100%) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py rename {opentelemetry-api/src/opentelemetry => opentelemetry-sdk/tests}/__init__.py (100%) rename {opentelemetry-api/src/opentelemetry/internal => opentelemetry-sdk/tests/trace}/__init__.py (89%) create mode 100644 opentelemetry-sdk/tests/trace/test_trace.py diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index d2a27a0d44..abf71aa63d 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -16,15 +16,14 @@ import setuptools BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "internal", "version.py") +VERSION_FILENAME = os.path.join(BASE_DIR, "src", "opentelemetry", "version.py") PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) #pylint:disable=exec-used + exec(f.read(), PACKAGE_INFO) setuptools.setup( name="opentelemetry-api", - version=PACKAGE_INFO["__version__"], # noqa + version=PACKAGE_INFO["__version__"], author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ @@ -47,7 +46,8 @@ extras_require={}, license="Apache-2.0", package_dir={"": "src"}, - packages=setuptools.find_namespace_packages(where="src"), + packages=setuptools.find_namespace_packages(where="src", + include="opentelemetry.*"), url=("https://github.com/open-telemetry/opentelemetry-python" "/tree/master/opentelemetry-api"), zip_safe=False, diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index d5fd5446a4..b6d856bb85 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -30,8 +30,7 @@ created as children of the currently active span, and the newly-created span becomes the new active span:: - # TODO (#15): which module holds the global tracer? - from opentelemetry.api.trace import tracer + from opentelemetry.trace import tracer # Create a new root span, set it as the current span in context with tracer.start_span("parent"): diff --git a/opentelemetry-api/src/opentelemetry/internal/version.py b/opentelemetry-api/src/opentelemetry/version.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/internal/version.py rename to opentelemetry-api/src/opentelemetry/version.py diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index b95fecb27a..a6e62114e0 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -15,15 +15,16 @@ import os import setuptools -base_dir = os.path.dirname(__file__) - -package_info = {} -with open(os.path.join(base_dir, "src", "opentelemetry", "sdk", "version.py")) as f: - exec(f.read(), package_info) +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join(BASE_DIR, "src", "opentelemetry", "sdk", + "version.py") +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) setuptools.setup( name="opentelemetry-sdk", - version=package_info["__version__"], # noqa + version=PACKAGE_INFO["__version__"], author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ @@ -41,11 +42,14 @@ include_package_data=True, long_description=open("README.rst").read(), install_requires=[ + "opentelemetry-api==0.1.dev0" ], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, - packages=setuptools.find_namespace_packages(where="src"), - url="https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-sdk", + packages=setuptools.find_namespace_packages(where="src", + include="opentelemetry.sdk.*"), + url=("https://github.com/open-telemetry/opentelemetry-python" + "/tree/master/opentelemetry-sdk"), zip_safe=False, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py index 2d52388da3..4978ec7226 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .version import __version__ +from . import trace + +__all__ = [ + "trace", +] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py new file mode 100644 index 0000000000..ca6f173dcb --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry import trace as trace_api + + +class Tracer(trace_api.Tracer): + pass diff --git a/opentelemetry-api/src/opentelemetry/__init__.py b/opentelemetry-sdk/tests/__init__.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/__init__.py rename to opentelemetry-sdk/tests/__init__.py diff --git a/opentelemetry-api/src/opentelemetry/internal/__init__.py b/opentelemetry-sdk/tests/trace/__init__.py similarity index 89% rename from opentelemetry-api/src/opentelemetry/internal/__init__.py rename to opentelemetry-sdk/tests/trace/__init__.py index 83ac009440..d853a7bcf6 100644 --- a/opentelemetry-api/src/opentelemetry/internal/__init__.py +++ b/opentelemetry-sdk/tests/trace/__init__.py @@ -11,9 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from .version import __version__ - -__all__ = [ - "__version__", -] diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py new file mode 100644 index 0000000000..7f6380e0ed --- /dev/null +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -0,0 +1,25 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import trace as trace_api +from opentelemetry.sdk import trace + + +class TestTracer(unittest.TestCase): + + def test_extends_api(self): + tracer = trace.Tracer() + self.assertIsInstance(tracer, trace_api.Tracer) diff --git a/tox.ini b/tox.ini index 085d260c26..f9761bcc01 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] skipsdist = True -envlist = py{34,35,36,37}-test, lint, py37-mypy, docs +envlist = py{34,35,36,37}-test-{api,sdk}, lint, py37-mypy, docs [travis] python = @@ -11,17 +11,22 @@ deps = mypy: mypy~=0.711 setenv = - PYTHONPATH={toxinidir}/opentelemetry-api/src/ - mypy: MYPYPATH={env:PYTHONPATH} + mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ changedir = - test: opentelemetry-api/tests + test-api: opentelemetry-api/tests + test-sdk: opentelemetry-sdk/tests + +commands_pre = + test-api: pip install -e {toxinidir}/opentelemetry-api + test-sdk: pip install -e {toxinidir}/opentelemetry-api + test-sdk: pip install -e {toxinidir}/opentelemetry-sdk commands = - py37-mypy: mypy opentelemetry-api/src/opentelemetry/ + mypy: mypy --namespace-packages opentelemetry-api/src/opentelemetry/ ; For test code, we don't want to enforce the full mypy strictness - py37-mypy: mypy --config-file=mypy-relaxed.ini opentelemetry-api/tests/ opentelemetry-api/setup.py - test: python -m unittest discover + mypy: mypy --namespace-packages --config-file=mypy-relaxed.ini opentelemetry-api/tests/ + test-{api,sdk}: python -m unittest discover [testenv:lint] deps = @@ -29,6 +34,9 @@ deps = flake8~=3.7 isort~=4.3 +commands_pre = + pip install -e {toxinidir}/opentelemetry-api + commands = ; Prefer putting everything in one pylint command to profit from duplication ; warnings. From 8a453eed345d56b83f009954dbd22fbc649f3108 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Wed, 24 Jul 2019 17:19:19 -0700 Subject: [PATCH 0030/1517] Span and SpanContext implementation (#58) --- .../src/opentelemetry/sdk/__init__.py | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 228 ++++++++++++++++++ .../src/opentelemetry/sdk/util.py | 22 ++ opentelemetry-sdk/tests/trace/test_trace.py | 8 + 4 files changed, 260 insertions(+) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/util.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py index 4978ec7226..7d6a6e83f4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py @@ -13,7 +13,9 @@ # limitations under the License. from . import trace +from . import util __all__ = [ "trace", + "util", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index ca6f173dcb..47d5b5bdad 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -12,7 +12,235 @@ # See the License for the specific language governing permissions and # limitations under the License. + +from collections import OrderedDict +from collections import deque +from collections import namedtuple +import threading +import typing + from opentelemetry import trace as trace_api +from opentelemetry.sdk import util + +try: + from collections.abc import MutableMapping + from collections.abc import Sequence +except ImportError: + from collections import MutableMapping + from collections import Sequence + + +MAX_NUM_ATTRIBUTES = 32 +MAX_NUM_EVENTS = 128 +MAX_NUM_LINKS = 32 + +AttributeValue = typing.Union[str, bool, float] + + +class BoundedList(Sequence): + """An append only list with a fixed max size.""" + def __init__(self, maxlen): + self.dropped = 0 + self._dq = deque(maxlen=maxlen) + self._lock = threading.Lock() + + def __repr__(self): + return ("{}({}, maxlen={})" + .format( + type(self).__name__, + list(self._dq), + self._dq.maxlen + )) + + def __getitem__(self, index): + return self._dq[index] + + def __len__(self): + return len(self._dq) + + def __iter__(self): + with self._lock: + return iter(self._dq.copy()) + + def append(self, item): + with self._lock: + if len(self._dq) == self._dq.maxlen: + self.dropped += 1 + self._dq.append(item) + + def extend(self, seq): + with self._lock: + to_drop = len(seq) + len(self._dq) - self._dq.maxlen + if to_drop > 0: + self.dropped += to_drop + self._dq.extend(seq) + + @classmethod + def from_seq(cls, maxlen, seq): + seq = tuple(seq) + if len(seq) > maxlen: + raise ValueError + bounded_list = cls(maxlen) + bounded_list._dq = deque(seq, maxlen=maxlen) + return bounded_list + + +class BoundedDict(MutableMapping): + """A dict with a fixed max capacity.""" + def __init__(self, maxlen): + if not isinstance(maxlen, int): + raise ValueError + if maxlen < 0: + raise ValueError + self.maxlen = maxlen + self.dropped = 0 + self._dict = OrderedDict() + self._lock = threading.Lock() + + def __repr__(self): + return ("{}({}, maxlen={})" + .format( + type(self).__name__, + dict(self._dict), + self.maxlen + )) + + def __getitem__(self, key): + return self._dict[key] + + def __setitem__(self, key, value): + with self._lock: + if self.maxlen == 0: + self.dropped += 1 + return + + if key in self._dict: + del self._dict[key] + elif len(self._dict) == self.maxlen: + del self._dict[next(iter(self._dict.keys()))] + self.dropped += 1 + self._dict[key] = value + + def __delitem__(self, key): + del self._dict[key] + + def __iter__(self): + with self._lock: + return iter(self._dict.copy()) + + def __len__(self): + return len(self._dict) + + @classmethod + def from_map(cls, maxlen, mapping): + mapping = OrderedDict(mapping) + if len(mapping) > maxlen: + raise ValueError + bounded_dict = cls(maxlen) + bounded_dict._dict = mapping + return bounded_dict + + +class SpanContext(trace_api.SpanContext): + """See `opentelemetry.trace.SpanContext`.""" + + def is_valid(self) -> bool: + return (self.trace_id == trace_api.INVALID_TRACE_ID or + self.span_id == trace_api.INVALID_SPAN_ID) + + +Event = namedtuple('Event', ('name', 'attributes')) + +Link = namedtuple('Link', ('context', 'attributes')) + + +class Span(trace_api.Span): + + # Initialize these lazily assuming most spans won't have them. + empty_attributes = BoundedDict(MAX_NUM_ATTRIBUTES) + empty_events = BoundedList(MAX_NUM_EVENTS) + empty_links = BoundedList(MAX_NUM_LINKS) + + def __init__(self: 'Span', + name: str, + context: 'SpanContext', + # TODO: span processor + parent: typing.Union['Span', 'SpanContext'] = None, + root: bool = False, + sampler=None, # TODO + trace_config=None, # TraceConfig TODO + resource=None, # Resource TODO + # TODO: is_recording + attributes=None, # type TODO + events=None, # type TODO + links=None, # type TODO + ) -> None: + """See `opentelemetry.trace.Span`.""" + if root: + if parent is not None: + raise ValueError("Root span can't have a parent") + + self.name = name + self.context = context + self.parent = parent + self.root = root + self.sampler = sampler + self.trace_config = trace_config + self.resource = resource + self.attributes = attributes + self.events = events + self.links = links + + if attributes is None: + self.attributes = Span.empty_attributes + else: + self.attributes = BoundedDict.from_map( + MAX_NUM_ATTRIBUTES, attributes) + + if events is None: + self.events = Span.empty_events + else: + self.events = BoundedList.from_seq(MAX_NUM_EVENTS, events) + + if links is None: + self.links = Span.empty_links + else: + self.links = BoundedList.from_seq(MAX_NUM_LINKS, links) + + self.end_time = None + self.start_time = None + + def set_attribute(self: 'Span', + key: str, + value: 'AttributeValue' + ) -> None: + if self.attributes is Span.empty_attributes: + self.attributes = BoundedDict(MAX_NUM_ATTRIBUTES) + self.attributes[key] = value + + def add_event(self: 'Span', + name: str, + attributes: typing.Dict[str, 'AttributeValue'] + ) -> None: + if self.events is Span.empty_events: + self.events = BoundedList(MAX_NUM_EVENTS) + self.events.append(Event(name, attributes)) + + def add_link(self: 'Span', + context: 'SpanContext', + attributes: typing.Dict[str, 'AttributeValue'], + ) -> None: + if self.links is Span.empty_links: + self.links = BoundedList(MAX_NUM_LINKS) + self.links.append(Link(context, attributes)) + + def start(self): + if self.end_time is None: + self.start_time = util.time_ns() + + def end(self): + if self.end_time is None: + self.end_time = util.time_ns() class Tracer(trace_api.Tracer): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util.py b/opentelemetry-sdk/src/opentelemetry/sdk/util.py new file mode 100644 index 0000000000..7e386ce44a --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util.py @@ -0,0 +1,22 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +try: + time_ns = time.time_ns # noqa +# Python versions < 3.7 +except AttributeError: + def time_ns(): + return int(time.time() * 1e9) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 7f6380e0ed..0c7f0a2b71 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from unittest import mock import unittest from opentelemetry import trace as trace_api @@ -23,3 +24,10 @@ class TestTracer(unittest.TestCase): def test_extends_api(self): tracer = trace.Tracer() self.assertIsInstance(tracer, trace_api.Tracer) + + +class TestSpan(unittest.TestCase): + + def test_basic_span(self): + span = trace.Span('name', mock.Mock(spec=trace.SpanContext)) + self.assertEqual(span.name, 'name') From 73cadac53210718997292b0230ccc38900945acb Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Fri, 26 Jul 2019 15:51:08 -0700 Subject: [PATCH 0031/1517] Add DefaultSpan in the API (#63) --- .../src/opentelemetry/trace/__init__.py | 13 ++++++++ opentelemetry-api/tests/trace/__init__.py | 13 ++++++++ .../tests/trace/test_defaultspan.py | 31 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 opentelemetry-api/tests/trace/__init__.py create mode 100644 opentelemetry-api/tests/trace/test_defaultspan.py diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index b6d856bb85..47f089192a 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -175,10 +175,23 @@ def is_valid(self) -> bool: """ +class DefaultSpan(Span): + """The default Span that is used when no Span implementation is available. + + All operations are no-op except context propagation. + """ + def __init__(self, context: 'SpanContext') -> None: + self._context = context + + def get_context(self) -> 'SpanContext': + return self._context + + INVALID_SPAN_ID = 0 INVALID_TRACE_ID = 0 INVALID_SPAN_CONTEXT = SpanContext(INVALID_TRACE_ID, INVALID_SPAN_ID, DEFAULT_TRACEOPTIONS, DEFAULT_TRACESTATE) +INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) class Tracer: diff --git a/opentelemetry-api/tests/trace/__init__.py b/opentelemetry-api/tests/trace/__init__.py new file mode 100644 index 0000000000..d853a7bcf6 --- /dev/null +++ b/opentelemetry-api/tests/trace/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-api/tests/trace/test_defaultspan.py b/opentelemetry-api/tests/trace/test_defaultspan.py new file mode 100644 index 0000000000..9e6b57dc3d --- /dev/null +++ b/opentelemetry-api/tests/trace/test_defaultspan.py @@ -0,0 +1,31 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import trace + + +class TestDefaultSpan(unittest.TestCase): + def test_ctor(self): + context = trace.SpanContext(1, 1, + trace.DEFAULT_TRACEOPTIONS, + trace.DEFAULT_TRACESTATE) + span = trace.DefaultSpan(context) + self.assertEqual(context, span.get_context()) + + def test_invalid_span(self): + self.assertIsNotNone(trace.INVALID_SPAN) + self.assertIsNotNone(trace.INVALID_SPAN.get_context()) + self.assertFalse(trace.INVALID_SPAN.get_context().is_valid()) From f609450b63814b366b77ce5793cc0c813402cfb4 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Fri, 26 Jul 2019 16:53:07 -0700 Subject: [PATCH 0032/1517] Lint SDK package (#65) --- .isort.cfg | 2 ++ .pylintrc | 2 ++ opentelemetry-api/src/opentelemetry/loader.py | 5 ++++- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 5 ++++- opentelemetry-sdk/src/opentelemetry/sdk/util.py | 2 +- tox.ini | 7 ++++--- 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index 3d19a1c6e0..20d62f8ecb 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,2 +1,4 @@ [settings] +force_single_line=True +from_first=True from_first=True diff --git a/.pylintrc b/.pylintrc index 0345f53ad6..23f9b090cd 100644 --- a/.pylintrc +++ b/.pylintrc @@ -63,6 +63,8 @@ confidence= disable=missing-docstring, fixme, # Warns about FIXME, TODO, etc. comments. too-few-public-methods, # Might be good to re-enable this later. + too-many-instance-attributes, + too-many-arguments # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/opentelemetry-api/src/opentelemetry/loader.py b/opentelemetry-api/src/opentelemetry/loader.py index cf2069edb4..9e28846b8c 100644 --- a/opentelemetry-api/src/opentelemetry/loader.py +++ b/opentelemetry-api/src/opentelemetry/loader.py @@ -61,7 +61,10 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: means that the Python interpreter was invoked with the ``-E`` or ``-I`` flag). """ -from typing import Callable, Optional, Type, TypeVar +from typing import Callable +from typing import Optional +from typing import Type +from typing import TypeVar import importlib import os import sys diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 47d5b5bdad..2508f7c59e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -23,13 +23,14 @@ from opentelemetry.sdk import util try: + # pylint: disable=ungrouped-imports from collections.abc import MutableMapping from collections.abc import Sequence except ImportError: + # pylint: disable=no-name-in-module,ungrouped-imports from collections import MutableMapping from collections import Sequence - MAX_NUM_ATTRIBUTES = 32 MAX_NUM_EVENTS = 128 MAX_NUM_LINKS = 32 @@ -81,6 +82,7 @@ def from_seq(cls, maxlen, seq): if len(seq) > maxlen: raise ValueError bounded_list = cls(maxlen) + # pylint: disable=protected-access bounded_list._dq = deque(seq, maxlen=maxlen) return bounded_list @@ -137,6 +139,7 @@ def from_map(cls, maxlen, mapping): if len(mapping) > maxlen: raise ValueError bounded_dict = cls(maxlen) + # pylint: disable=protected-access bounded_dict._dict = mapping return bounded_dict diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util.py b/opentelemetry-sdk/src/opentelemetry/sdk/util.py index 7e386ce44a..9886206fb7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util.py @@ -15,7 +15,7 @@ import time try: - time_ns = time.time_ns # noqa + time_ns = time.time_ns # pylint: disable=invalid-name # Python versions < 3.7 except AttributeError: def time_ns(): diff --git a/tox.ini b/tox.ini index f9761bcc01..cd71124361 100644 --- a/tox.ini +++ b/tox.ini @@ -36,13 +36,14 @@ deps = commands_pre = pip install -e {toxinidir}/opentelemetry-api + pip install -e {toxinidir}/opentelemetry-sdk commands = ; Prefer putting everything in one pylint command to profit from duplication ; warnings. - pylint opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ - flake8 opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ - isort --check-only --recursive opentelemetry-api/src + pylint opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry/ opentelemetry-sdk/tests/ + flake8 opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry/ opentelemetry-sdk/tests/ + isort --check-only --recursive opentelemetry-api/src opentelemetry-sdk/src [testenv:docs] deps = From 7c5e018c0af30a79af8c4bcbf04460645fb169ad Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Fri, 26 Jul 2019 20:03:23 -0700 Subject: [PATCH 0033/1517] Initial implementation of context (#57) --- mypy-relaxed.ini | 2 +- mypy.ini | 2 +- .../src/opentelemetry/context/__init__.py | 141 ++++++++++++++++++ .../opentelemetry/context/async_context.py | 42 ++++++ .../src/opentelemetry/context/base_context.py | 116 ++++++++++++++ .../context/thread_local_context.py | 44 ++++++ 6 files changed, 345 insertions(+), 2 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/context/async_context.py create mode 100644 opentelemetry-api/src/opentelemetry/context/base_context.py create mode 100644 opentelemetry-api/src/opentelemetry/context/thread_local_context.py diff --git a/mypy-relaxed.ini b/mypy-relaxed.ini index 8f2be85aff..205688353e 100644 --- a/mypy-relaxed.ini +++ b/mypy-relaxed.ini @@ -4,7 +4,7 @@ disallow_any_unimported = True ; disallow_any_expr = True disallow_any_decorated = True - disallow_any_explicit = True +; disallow_any_explicit = True disallow_any_generics = True disallow_subclassing_any = True disallow_untyped_calls = True diff --git a/mypy.ini b/mypy.ini index 5b46777838..ba375b62b1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,7 +2,7 @@ disallow_any_unimported = True disallow_any_expr = True disallow_any_decorated = True - disallow_any_explicit = True +; disallow_any_explicit = True disallow_any_generics = True disallow_subclassing_any = True disallow_untyped_calls = True diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index d853a7bcf6..a02bc8cb4e 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -11,3 +11,144 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + + +""" +The OpenTelemetry context module provides abstraction layer on top of +thread-local storage and contextvars. The long term direction is to switch to +contextvars provided by the Python runtime library. + +A global object ``Context`` is provided to access all the context related +functionalities: + + >>> from opentelemetry.context import Context + >>> Context.foo = 1 + >>> Context.foo = 2 + >>> Context.foo + 2 + +When explicit thread is used, a helper function `Context.with_current_context` +can be used to carry the context across threads: + + from threading import Thread + from opentelemetry.context import Context + + def work(name): + print('Entering worker:', Context) + Context.operation_id = name + print('Exiting worker:', Context) + + if __name__ == '__main__': + print('Main thread:', Context) + Context.operation_id = 'main' + + print('Main thread:', Context) + + # by default context is not propagated to worker thread + thread = Thread(target=work, args=('foo',)) + thread.start() + thread.join() + + print('Main thread:', Context) + + # user can propagate context explicitly + thread = Thread( + target=Context.with_current_context(work), + args=('bar',), + ) + thread.start() + thread.join() + + print('Main thread:', Context) + +Here goes another example using thread pool: + + import time + import threading + + from multiprocessing.dummy import Pool as ThreadPool + from opentelemetry.context import Context + + _console_lock = threading.Lock() + + def println(msg): + with _console_lock: + print(msg) + + def work(name): + println('Entering worker[{}]: {}'.format(name, Context)) + Context.operation_id = name + time.sleep(0.01) + println('Exiting worker[{}]: {}'.format(name, Context)) + + if __name__ == "__main__": + println('Main thread: {}'.format(Context)) + Context.operation_id = 'main' + pool = ThreadPool(2) # create a thread pool with 2 threads + pool.map(Context.with_current_context(work), [ + 'bear', + 'cat', + 'dog', + 'horse', + 'rabbit', + ]) + pool.close() + pool.join() + println('Main thread: {}'.format(Context)) + +Here goes a simple demo of how async could work in Python 3.7+: + + import asyncio + + from opentelemetry.context import Context + + class Span(object): + def __init__(self, name): + self.name = name + self.parent = Context.current_span + + def __repr__(self): + return ('{}(name={}, parent={})' + .format( + type(self).__name__, + self.name, + self.parent, + )) + + async def __aenter__(self): + Context.current_span = self + + async def __aexit__(self, exc_type, exc, tb): + Context.current_span = self.parent + + async def main(): + print(Context) + async with Span('foo'): + print(Context) + await asyncio.sleep(0.1) + async with Span('bar'): + print(Context) + await asyncio.sleep(0.1) + print(Context) + await asyncio.sleep(0.1) + print(Context) + + if __name__ == '__main__': + asyncio.run(main()) +""" + +import typing + +from .base_context import BaseRuntimeContext + +__all__ = ['Context'] + + +Context: typing.Optional[BaseRuntimeContext] + +try: + from .async_context import AsyncRuntimeContext + Context = AsyncRuntimeContext() # pylint:disable=invalid-name +except ImportError: + from .thread_local_context import ThreadLocalRuntimeContext + Context = ThreadLocalRuntimeContext() # pylint:disable=invalid-name diff --git a/opentelemetry-api/src/opentelemetry/context/async_context.py b/opentelemetry-api/src/opentelemetry/context/async_context.py new file mode 100644 index 0000000000..413e7b2543 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/async_context.py @@ -0,0 +1,42 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from contextvars import ContextVar +import typing + +from . import base_context + + +class AsyncRuntimeContext(base_context.BaseRuntimeContext): + class Slot(base_context.BaseRuntimeContext.Slot): + def __init__(self, name: str, default: 'object'): + # pylint: disable=super-init-not-called + self.name = name + self.contextvar: 'ContextVar[object]' = ContextVar(name) + self.default: typing.Callable[..., object] + self.default = base_context.wrap_callable(default) + + def clear(self) -> None: + self.contextvar.set(self.default()) + + def get(self) -> 'object': + try: + return self.contextvar.get() + except LookupError: + value = self.default() + self.set(value) + return value + + def set(self, value: 'object') -> None: + self.contextvar.set(value) diff --git a/opentelemetry-api/src/opentelemetry/context/base_context.py b/opentelemetry-api/src/opentelemetry/context/base_context.py new file mode 100644 index 0000000000..35ee179a4b --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/base_context.py @@ -0,0 +1,116 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import typing + + +def wrap_callable(target: 'object') -> typing.Callable[[], object]: + if callable(target): + return target + return lambda: target + + +class BaseRuntimeContext: + class Slot: + def __init__(self, name: str, default: 'object'): + raise NotImplementedError + + def clear(self) -> None: + raise NotImplementedError + + def get(self) -> 'object': + raise NotImplementedError + + def set(self, value: 'object') -> None: + raise NotImplementedError + + _lock = threading.Lock() + _slots: typing.Dict[str, Slot] = {} + + @classmethod + def clear(cls) -> None: + """Clear all slots to their default value.""" + keys = cls._slots.keys() + for name in keys: + slot = cls._slots[name] + slot.clear() + + @classmethod + def register_slot(cls, name: str, default: 'object' = None) -> 'Slot': + """Register a context slot with an optional default value. + + :type name: str + :param name: The name of the context slot. + + :type default: object + :param name: The default value of the slot, can be a value or lambda. + + :returns: The registered slot. + """ + with cls._lock: + if name in cls._slots: + raise ValueError('slot {} already registered'.format(name)) + slot = cls.Slot(name, default) + cls._slots[name] = slot + return slot + + def apply(self, snapshot: typing.Dict[str, 'object']) -> None: + """Set the current context from a given snapshot dictionary""" + + for name in snapshot: + setattr(self, name, snapshot[name]) + + def snapshot(self) -> typing.Dict[str, 'object']: + """Return a dictionary of current slots by reference.""" + + keys = self._slots.keys() + return dict((n, self._slots[n].get()) for n in keys) + + def __repr__(self) -> str: + return '{}({})'.format(type(self).__name__, self.snapshot()) + + def __getattr__(self, name: str) -> 'object': + if name not in self._slots: + self.register_slot(name, None) + slot = self._slots[name] + return slot.get() + + def __setattr__(self, name: str, value: 'object') -> None: + if name not in self._slots: + self.register_slot(name, None) + slot = self._slots[name] + slot.set(value) + + def with_current_context( + self, + func: typing.Callable[..., 'object'], + ) -> typing.Callable[..., 'object']: + """Capture the current context and apply it to the provided func. + """ + + caller_context = self.snapshot() + + def call_with_current_context( + *args: 'object', + **kwargs: 'object', + ) -> 'object': + try: + backup_context = self.snapshot() + self.apply(caller_context) + return func(*args, **kwargs) + finally: + self.apply(backup_context) + + return call_with_current_context diff --git a/opentelemetry-api/src/opentelemetry/context/thread_local_context.py b/opentelemetry-api/src/opentelemetry/context/thread_local_context.py new file mode 100644 index 0000000000..dd11128b7a --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/thread_local_context.py @@ -0,0 +1,44 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import typing + +from . import base_context + + +class ThreadLocalRuntimeContext(base_context.BaseRuntimeContext): + class Slot(base_context.BaseRuntimeContext.Slot): + _thread_local = threading.local() + + def __init__(self, name: str, default: 'object'): + # pylint: disable=super-init-not-called + self.name = name + self.default: typing.Callable[..., object] + self.default = base_context.wrap_callable(default) + + def clear(self) -> None: + setattr(self._thread_local, self.name, self.default()) + + def get(self) -> 'object': + try: + got: object = getattr(self._thread_local, self.name) + return got + except AttributeError: + value = self.default() + self.set(value) + return value + + def set(self, value: 'object') -> None: + setattr(self._thread_local, self.name, value) From e3f8ef0592456de6f92569c4e0be26199ecad8d5 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Sun, 28 Jul 2019 21:52:36 -0700 Subject: [PATCH 0034/1517] Adding the Resource API (#61) Design Decisions include: The API package should be minimal, and example implementations should live in SDK when possible. The precedent that exists is to name both the interface and the implementation in the sdk the same. This avoids the generally unhelpful "Default" prefix. opentelemetry-python is standardizing on Google style docstrings. As such resolving formatting that is inconsistent with the style guilde. --- .../src/opentelemetry/resources/__init__.py | 39 ++++++++++++++++ .../opentelemetry/sdk/resources/__init__.py | 44 +++++++++++++++++++ opentelemetry-sdk/tests/resources/__init__.py | 0 .../tests/resources/test_init.py | 33 ++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py create mode 100644 opentelemetry-sdk/tests/resources/__init__.py create mode 100644 opentelemetry-sdk/tests/resources/test_init.py diff --git a/opentelemetry-api/src/opentelemetry/resources/__init__.py b/opentelemetry-api/src/opentelemetry/resources/__init__.py index d853a7bcf6..0d1e34dddb 100644 --- a/opentelemetry-api/src/opentelemetry/resources/__init__.py +++ b/opentelemetry-api/src/opentelemetry/resources/__init__.py @@ -11,3 +11,42 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +import abc +import typing + + +class Resource(abc.ABC): + """The interface that resources must implement.""" + @staticmethod + @abc.abstractmethod + def create(labels: typing.Dict[str, str]) -> "Resource": + """Create a new resource. + + Args: + labels: the labels that define the resource + + Returns: + The resource with the labels in question + + """ + @property + @abc.abstractmethod + def labels(self) -> typing.Dict[str, str]: + """Return the label dictionary associated with this resource. + + Returns: + A dictionary with the labels of the resource + + """ + @abc.abstractmethod + def merge(self, other: typing.Optional["Resource"]) -> "Resource": + """Return a resource with the union of labels for both resources. + + Labels that exist in the main Resource take precedence unless the + label value is the empty string. + + Args: + other: The resource to merge in + + """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py new file mode 100644 index 0000000000..b488c0a0c7 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -0,0 +1,44 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import opentelemetry.resources as resources + + +class Resource(resources.Resource): + def __init__(self, labels): + self._labels = labels + + @staticmethod + def create(labels): + return Resource(labels) + + @property + def labels(self): + return self._labels + + def merge(self, other): + if other is None: + return self + if not self._labels: + return other + merged_labels = self.labels.copy() + for key, value in other.labels.items(): + if key not in merged_labels or merged_labels[key] == "": + merged_labels[key] = value + return Resource(merged_labels) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Resource): + return False + return self.labels == other.labels diff --git a/opentelemetry-sdk/tests/resources/__init__.py b/opentelemetry-sdk/tests/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-sdk/tests/resources/test_init.py b/opentelemetry-sdk/tests/resources/test_init.py new file mode 100644 index 0000000000..953b4c2517 --- /dev/null +++ b/opentelemetry-sdk/tests/resources/test_init.py @@ -0,0 +1,33 @@ +import unittest +from opentelemetry.sdk import resources + + +class TestResources(unittest.TestCase): + def test_resource_merge(self): + left = resources.Resource({"service": "ui"}) + right = resources.Resource({"host": "service-host"}) + self.assertEqual( + left.merge(right), + resources.Resource({ + "service": "ui", + "host": "service-host" + })) + + def test_resource_merge_empty_string(self): + """Verify Resource.merge behavior with the empty string. + + Labels from the source Resource take precedence, with + the exception of the empty string. + + """ + left = resources.Resource({"service": "ui", "host": ""}) + right = resources.Resource({ + "host": "service-host", + "service": "not-ui" + }) + self.assertEqual( + left.merge(right), + resources.Resource({ + "service": "ui", + "host": "service-host" + })) From 047eb9c18b36d0c6c294feaea0b70beb678d6153 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 30 Jul 2019 08:27:32 -0700 Subject: [PATCH 0035/1517] Contributing.md (#67) --- CONTRIBUTING.md | 34 ++++++++++++++++++++++++++++++++++ README.md | 9 +-------- 2 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..74d6b4f9da --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,34 @@ +# Contributing to opentelemetry-python + +The Python special interest group (SIG) meets regularly. See the OpenTelemetry +[community](https://github.com/open-telemetry/community#python-sdk) repo for +information on this and other language SIGs. + +See the [public meeting +notes](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit) +for a summary description of past meetings. To request edit access join the +meeting or get in touch on Gitter. + +## Running Tests + +Execute tests as well as the linting process with tox: + + pip install --user tox + tox # execute in the root of the repository + +## Testing + +The standard Python unittest module is used to author unit tests. + +## Design Choices + +As with other OpenTelemetry clients, opentelemetry-python follows the +(opentelemetry-specification)[https://github.com/open-telemetry/opentelemetry-specification]. + +It's especially valuable to read through the (library guidelines)[https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/library-guidelines.md]. + + +## Styleguide + +* docstrings should adhere to the Google styleguide as specified + with the (napolean extension)[http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#google-vs-numpy] extension in (Sphinx)[http://www.sphinx-doc.org/en/master/index.html]. \ No newline at end of file diff --git a/README.md b/README.md index 867773593f..e04d9e3cc6 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,4 @@ The Python [OpenTelemetry](https://opentelemetry.io/) client. ## Contributing -The Python special interest group (SIG) meets regularly. See the OpenTelemetry -[community](https://github.com/open-telemetry/community#python-sdk) repo for -information on this and other language SIGs. - -See the [public meeting -notes](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit) -for a summary description of past meetings. To request edit access join the -meeting or get in touch on Gitter. +See (CONTRIBUTING.md)[CONTRIBUTING.md] \ No newline at end of file From 3460432fdd1918cb6347eb700087d000c2f65bd1 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 5 Aug 2019 11:49:53 -0700 Subject: [PATCH 0036/1517] Document focusing on capabilities (#72) --- CONTRIBUTING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74d6b4f9da..9ad9ddf09e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,19 @@ As with other OpenTelemetry clients, opentelemetry-python follows the It's especially valuable to read through the (library guidelines)[https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/library-guidelines.md]. +### Focus on Capabilities, Not Structure Compliance + +OpenTelemetry is an evolving specification, one where the desires and +use cases are clear, but the method to satisfy those uses cases are not. + +As such, Contributions should provide functionality and behavior that +conforms to the specification, but the interface and structure is flexible. + +It is preferable to have contributions follow the idioms of the language +rather than conform to specific API names or argument patterns in the spec. + +For a deeper discussion, see: https://github.com/open-telemetry/opentelemetry-specification/issues/165 + ## Styleguide From 42415bacab119c21ba5cf7cdeac89587fb3c6eef Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 6 Aug 2019 13:36:20 -0700 Subject: [PATCH 0037/1517] Span creation in tracer SDK (#69) --- .isort.cfg | 1 - .pylintrc | 3 +- opentelemetry-api/setup.py | 1 + .../src/opentelemetry/trace/__init__.py | 48 ++++-- opentelemetry-api/src/opentelemetry/types.py | 19 +++ .../tests/trace/test_defaultspan.py | 4 +- opentelemetry-sdk/setup.py | 1 + .../src/opentelemetry/sdk/trace/__init__.py | 158 ++++++++++++++---- .../tests/resources/test_init.py | 1 + opentelemetry-sdk/tests/trace/test_trace.py | 91 +++++++++- 10 files changed, 281 insertions(+), 46 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/types.py diff --git a/.isort.cfg b/.isort.cfg index 20d62f8ecb..d3e7dbf03d 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,4 +1,3 @@ [settings] force_single_line=True from_first=True -from_first=True diff --git a/.pylintrc b/.pylintrc index 23f9b090cd..f0ae6e1ad8 100644 --- a/.pylintrc +++ b/.pylintrc @@ -64,7 +64,8 @@ disable=missing-docstring, fixme, # Warns about FIXME, TODO, etc. comments. too-few-public-methods, # Might be good to re-enable this later. too-many-instance-attributes, - too-many-arguments + too-many-arguments, + ungrouped-imports # Leave this up to isort # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index abf71aa63d..3e2aa72055 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -13,6 +13,7 @@ # limitations under the License. import os + import setuptools BASE_DIR = os.path.dirname(__file__) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 47f089192a..6c873002e4 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -66,6 +66,9 @@ from opentelemetry import loader +# TODO: quarantine +ParentSpan = typing.Optional[typing.Union['Span', 'SpanContext']] + class Span: """A span represents a single operation within a trace.""" @@ -119,7 +122,7 @@ def get_default(cls) -> 'TraceOptions': return cls(cls.DEFAULT) -DEFAULT_TRACEOPTIONS = TraceOptions.get_default() +DEFAULT_TRACE_OPTIONS = TraceOptions.get_default() class TraceState(typing.Dict[str, str]): @@ -138,7 +141,15 @@ def get_default(cls) -> 'TraceState': return cls() -DEFAULT_TRACESTATE = TraceState.get_default() +DEFAULT_TRACE_STATE = TraceState.get_default() + + +def format_trace_id(trace_id: int) -> str: + return '0x{:032x}'.format(trace_id) + + +def format_span_id(span_id: int) -> str: + return '0x{:016x}'.format(span_id) class SpanContext: @@ -157,12 +168,25 @@ class SpanContext: def __init__(self, trace_id: int, span_id: int, - options: 'TraceOptions', - state: 'TraceState') -> None: + trace_options: 'TraceOptions' = None, + trace_state: 'TraceState' = None + ) -> None: + if trace_options is None: + trace_options = DEFAULT_TRACE_OPTIONS + if trace_state is None: + trace_state = DEFAULT_TRACE_STATE self.trace_id = trace_id self.span_id = span_id - self.options = options - self.state = state + self.trace_options = trace_options + self.trace_state = trace_state + + def __repr__(self) -> str: + return ("{}(trace_id={}, span_id={})" + .format( + type(self).__name__, + format_trace_id(self.trace_id), + format_span_id(self.span_id) + )) def is_valid(self) -> bool: """Get whether this `SpanContext` is valid. @@ -173,6 +197,8 @@ def is_valid(self) -> bool: Returns: True if the `SpanContext` is valid, false otherwise. """ + return (self.trace_id != INVALID_TRACE_ID and + self.span_id != INVALID_SPAN_ID) class DefaultSpan(Span): @@ -187,10 +213,10 @@ def get_context(self) -> 'SpanContext': return self._context -INVALID_SPAN_ID = 0 -INVALID_TRACE_ID = 0 +INVALID_SPAN_ID = 0x0000000000000000 +INVALID_TRACE_ID = 0x00000000000000000000000000000000 INVALID_SPAN_CONTEXT = SpanContext(INVALID_TRACE_ID, INVALID_SPAN_ID, - DEFAULT_TRACEOPTIONS, DEFAULT_TRACESTATE) + DEFAULT_TRACE_OPTIONS, DEFAULT_TRACE_STATE) INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) @@ -219,7 +245,7 @@ def get_current_span(self) -> 'Span': @contextmanager # type: ignore def start_span(self, name: str, - parent: typing.Union['Span', 'SpanContext'] = CURRENT_SPAN + parent: ParentSpan = CURRENT_SPAN ) -> typing.Iterator['Span']: """Context manager for span creation. @@ -266,7 +292,7 @@ def start_span(self, def create_span(self, name: str, - parent: typing.Union['Span', 'SpanContext'] = CURRENT_SPAN + parent: ParentSpan = CURRENT_SPAN ) -> 'Span': """Creates a span. diff --git a/opentelemetry-api/src/opentelemetry/types.py b/opentelemetry-api/src/opentelemetry/types.py new file mode 100644 index 0000000000..ce5682ee0a --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/types.py @@ -0,0 +1,19 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import typing + +AttributeValue = typing.Union[str, bool, float] +Attributes = typing.Dict[str, AttributeValue] diff --git a/opentelemetry-api/tests/trace/test_defaultspan.py b/opentelemetry-api/tests/trace/test_defaultspan.py index 9e6b57dc3d..b3155d3261 100644 --- a/opentelemetry-api/tests/trace/test_defaultspan.py +++ b/opentelemetry-api/tests/trace/test_defaultspan.py @@ -20,8 +20,8 @@ class TestDefaultSpan(unittest.TestCase): def test_ctor(self): context = trace.SpanContext(1, 1, - trace.DEFAULT_TRACEOPTIONS, - trace.DEFAULT_TRACESTATE) + trace.DEFAULT_TRACE_OPTIONS, + trace.DEFAULT_TRACE_STATE) span = trace.DefaultSpan(context) self.assertEqual(context, span.get_context()) diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index a6e62114e0..790dc92893 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -13,6 +13,7 @@ # limitations under the License. import os + import setuptools BASE_DIR = os.path.dirname(__file__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 2508f7c59e..44f321e6c9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -16,10 +16,14 @@ from collections import OrderedDict from collections import deque from collections import namedtuple +from contextlib import contextmanager +import contextvars +import random import threading import typing from opentelemetry import trace as trace_api +from opentelemetry import types from opentelemetry.sdk import util try: @@ -31,12 +35,13 @@ from collections import MutableMapping from collections import Sequence + +_CURRENT_SPAN_CV = contextvars.ContextVar('current_span', default=None) + MAX_NUM_ATTRIBUTES = 32 MAX_NUM_EVENTS = 128 MAX_NUM_LINKS = 32 -AttributeValue = typing.Union[str, bool, float] - class BoundedList(Sequence): """An append only list with a fixed max size.""" @@ -144,20 +149,28 @@ def from_map(cls, maxlen, mapping): return bounded_dict -class SpanContext(trace_api.SpanContext): - """See `opentelemetry.trace.SpanContext`.""" - - def is_valid(self) -> bool: - return (self.trace_id == trace_api.INVALID_TRACE_ID or - self.span_id == trace_api.INVALID_SPAN_ID) - - Event = namedtuple('Event', ('name', 'attributes')) Link = namedtuple('Link', ('context', 'attributes')) class Span(trace_api.Span): + """See `opentelemetry.trace.Span`. + + Users should create `Span`s via the `Tracer` instead of this constructor. + + Args: + name: The name of the operation this span represents + context: The immutable span context + parent: This span's parent, may be a `SpanContext` if the parent is + remote, null if this is a root span + sampler: TODO + trace_config: TODO + resource: TODO + attributes: The span's attributes to be exported + events: Timestamped events to be exported + links: Links to other spans to be exported + """ # Initialize these lazily assuming most spans won't have them. empty_attributes = BoundedDict(MAX_NUM_ATTRIBUTES) @@ -166,27 +179,19 @@ class Span(trace_api.Span): def __init__(self: 'Span', name: str, - context: 'SpanContext', - # TODO: span processor - parent: typing.Union['Span', 'SpanContext'] = None, - root: bool = False, + context: 'trace_api.SpanContext', + parent: trace_api.ParentSpan = None, sampler=None, # TODO - trace_config=None, # TraceConfig TODO - resource=None, # Resource TODO - # TODO: is_recording - attributes=None, # type TODO - events=None, # type TODO - links=None, # type TODO + trace_config=None, # TODO + resource=None, # TODO + attributes: types.Attributes = None, # TODO + events: typing.Sequence[Event] = None, # TODO + links: typing.Sequence[Link] = None, # TODO ) -> None: - """See `opentelemetry.trace.Span`.""" - if root: - if parent is not None: - raise ValueError("Root span can't have a parent") self.name = name self.context = context self.parent = parent - self.root = root self.sampler = sampler self.trace_config = trace_config self.resource = resource @@ -213,9 +218,19 @@ def __init__(self: 'Span', self.end_time = None self.start_time = None + def __repr__(self): + return ('{}(name="{}")' + .format( + type(self).__name__, + self.name + )) + + def get_context(self): + return self.context + def set_attribute(self: 'Span', key: str, - value: 'AttributeValue' + value: 'types.AttributeValue' ) -> None: if self.attributes is Span.empty_attributes: self.attributes = BoundedDict(MAX_NUM_ATTRIBUTES) @@ -223,15 +238,15 @@ def set_attribute(self: 'Span', def add_event(self: 'Span', name: str, - attributes: typing.Dict[str, 'AttributeValue'] + attributes: 'types.Attributes', ) -> None: if self.events is Span.empty_events: self.events = BoundedList(MAX_NUM_EVENTS) self.events.append(Event(name, attributes)) def add_link(self: 'Span', - context: 'SpanContext', - attributes: typing.Dict[str, 'AttributeValue'], + context: 'trace_api.SpanContext', + attributes: 'types.Attributes', ) -> None: if self.links is Span.empty_links: self.links = BoundedList(MAX_NUM_LINKS) @@ -246,5 +261,88 @@ def end(self): self.end_time = util.time_ns() +def generate_span_id(): + """Get a new random span ID. + + Returns: + A random 64-bit int for use as a span ID + """ + return random.getrandbits(64) + + +def generate_trace_id(): + """Get a new random trace ID. + + Returns: + A random 128-bit int for use as a trace ID + """ + return random.getrandbits(128) + + class Tracer(trace_api.Tracer): - pass + """See `opentelemetry.trace.Tracer`. + + Args: + cv: The context variable that holds the current span. + """ + + def __init__(self, + cv: 'contextvars.ContextVar' = _CURRENT_SPAN_CV + ) -> None: + self._cv = cv + try: + self._cv.get() + except LookupError: + self._cv.set(None) + + def get_current_span(self): + """See `opentelemetry.trace.Tracer.get_current_span`.""" + return self._cv.get() + + @contextmanager + def start_span(self, + name: str, + parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN + ) -> typing.Iterator['Span']: + """See `opentelemetry.trace.Tracer.start_span`.""" + with self.use_span(self.create_span(name, parent)) as span: + yield span + + def create_span(self, + name: str, + parent: trace_api.ParentSpan = + trace_api.Tracer.CURRENT_SPAN + ) -> 'Span': + """See `opentelemetry.trace.Tracer.create_span`.""" + span_id = generate_span_id() + if parent is Tracer.CURRENT_SPAN: + parent = self.get_current_span() + if parent is None: + context = trace_api.SpanContext(generate_trace_id(), span_id) + else: + if isinstance(parent, trace_api.Span): + parent_context = parent.get_context() + elif isinstance(parent, trace_api.SpanContext): + parent_context = parent + else: + raise TypeError + context = trace_api.SpanContext( + parent_context.trace_id, + span_id, + parent_context.trace_options, + parent_context.trace_state) + return Span(name=name, context=context, parent=parent) + + @contextmanager + def use_span(self, span: 'Span') -> typing.Iterator['Span']: + """See `opentelemetry.trace.Tracer.use_span`.""" + span.start() + token = self._cv.set(span) + try: + yield span + finally: + self._cv.reset(token) + span.end() + + +tracer = Tracer() # pylint: disable=invalid-name diff --git a/opentelemetry-sdk/tests/resources/test_init.py b/opentelemetry-sdk/tests/resources/test_init.py index 953b4c2517..6ce69fff89 100644 --- a/opentelemetry-sdk/tests/resources/test_init.py +++ b/opentelemetry-sdk/tests/resources/test_init.py @@ -1,4 +1,5 @@ import unittest + from opentelemetry.sdk import resources diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 0c7f0a2b71..7a91d23765 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -13,6 +13,7 @@ # limitations under the License. from unittest import mock +import contextvars import unittest from opentelemetry import trace as trace_api @@ -26,8 +27,96 @@ def test_extends_api(self): self.assertIsInstance(tracer, trace_api.Tracer) +class TestSpanCreation(unittest.TestCase): + + def test_start_span_implicit(self): + context = contextvars.ContextVar('test_start_span_implicit') + tracer = trace.Tracer(context) + + self.assertIsNone(tracer.get_current_span()) + + with tracer.start_span('root') as root: + self.assertIs(tracer.get_current_span(), root) + + self.assertIsNotNone(root.start_time) + self.assertIsNone(root.end_time) + + with tracer.start_span('child') as child: + self.assertIs(tracer.get_current_span(), child) + self.assertIs(child.parent, root) + + self.assertIsNotNone(child.start_time) + self.assertIsNone(child.end_time) + + # The new child span should inherit the parent's context but + # get a new span ID. + root_context = root.get_context() + child_context = child.get_context() + self.assertEqual(root_context.trace_id, child_context.trace_id) + self.assertNotEqual(root_context.span_id, + child_context.span_id) + self.assertEqual(root_context.trace_state, + child_context.trace_state) + self.assertEqual(root_context.trace_options, + child_context.trace_options) + + # After exiting the child's scope the parent should become the + # current span again. + self.assertIs(tracer.get_current_span(), root) + self.assertIsNotNone(child.end_time) + + self.assertIsNone(tracer.get_current_span()) + self.assertIsNotNone(root.end_time) + + def test_start_span_explicit(self): + context = contextvars.ContextVar('test_start_span_explicit') + tracer = trace.Tracer(context) + + other_parent = trace_api.SpanContext( + trace_id=0x000000000000000000000000deadbeef, + span_id=0x00000000deadbef0 + ) + + self.assertIsNone(tracer.get_current_span()) + + # Test with the implicit root span + with tracer.start_span('root') as root: + self.assertIs(tracer.get_current_span(), root) + + self.assertIsNotNone(root.start_time) + self.assertIsNone(root.end_time) + + with tracer.start_span('stepchild', other_parent) as child: + # The child should become the current span as usual, but its + # parent should be the one passed in, not the + # previously-current span. + self.assertIs(tracer.get_current_span(), child) + self.assertNotEqual(child.parent, root) + self.assertIs(child.parent, other_parent) + + self.assertIsNotNone(child.start_time) + self.assertIsNone(child.end_time) + + # The child should inherit its context fromr the explicit + # parent, not the previously-current span. + child_context = child.get_context() + self.assertEqual(other_parent.trace_id, child_context.trace_id) + self.assertNotEqual(other_parent.span_id, + child_context.span_id) + self.assertEqual(other_parent.trace_state, + child_context.trace_state) + self.assertEqual(other_parent.trace_options, + child_context.trace_options) + + # After exiting the child's scope the last span on the stack should + # become current, not the child's parent. + self.assertNotEqual(tracer.get_current_span(), other_parent) + self.assertIs(tracer.get_current_span(), root) + self.assertIsNotNone(child.end_time) + + class TestSpan(unittest.TestCase): def test_basic_span(self): - span = trace.Span('name', mock.Mock(spec=trace.SpanContext)) + span = trace.Span('name', mock.Mock(spec=trace_api.SpanContext)) self.assertEqual(span.name, 'name') From df33d99ebac15b8aecbf4240da19724928781969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Thu, 8 Aug 2019 16:51:38 +0200 Subject: [PATCH 0038/1517] Fix typo in SDK's Span.start (#74) --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 44f321e6c9..f6f129827e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -253,7 +253,7 @@ def add_link(self: 'Span', self.links.append(Link(context, attributes)) def start(self): - if self.end_time is None: + if self.start_time is None: self.start_time = util.time_ns() def end(self): From 3cbdd30e01c8a7b694fce95aa3813ed3f1141d6b Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 8 Aug 2019 09:08:43 -0700 Subject: [PATCH 0039/1517] Fixing contributing.md links (#80) --- CONTRIBUTING.md | 9 ++++----- README.md | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ad9ddf09e..8a96c835af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,8 +4,7 @@ The Python special interest group (SIG) meets regularly. See the OpenTelemetry [community](https://github.com/open-telemetry/community#python-sdk) repo for information on this and other language SIGs. -See the [public meeting -notes](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit) +See the [public meeting notes](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit) for a summary description of past meetings. To request edit access join the meeting or get in touch on Gitter. @@ -23,9 +22,9 @@ The standard Python unittest module is used to author unit tests. ## Design Choices As with other OpenTelemetry clients, opentelemetry-python follows the -(opentelemetry-specification)[https://github.com/open-telemetry/opentelemetry-specification]. +[opentelemetry-specification](https://github.com/open-telemetry/opentelemetry-specification). -It's especially valuable to read through the (library guidelines)[https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/library-guidelines.md]. +It's especially valuable to read through the [library guidelines](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/library-guidelines.md). ### Focus on Capabilities, Not Structure Compliance @@ -44,4 +43,4 @@ For a deeper discussion, see: https://github.com/open-telemetry/opentelemetry-sp ## Styleguide * docstrings should adhere to the Google styleguide as specified - with the (napolean extension)[http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#google-vs-numpy] extension in (Sphinx)[http://www.sphinx-doc.org/en/master/index.html]. \ No newline at end of file + with the [napolean extension](http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#google-vs-numpy) extension in [Sphinx](http://www.sphinx-doc.org/en/master/index.html). \ No newline at end of file diff --git a/README.md b/README.md index e04d9e3cc6..edea6801e3 100644 --- a/README.md +++ b/README.md @@ -8,4 +8,4 @@ The Python [OpenTelemetry](https://opentelemetry.io/) client. ## Contributing -See (CONTRIBUTING.md)[CONTRIBUTING.md] \ No newline at end of file +See [CONTRIBUTING.md](CONTRIBUTING.md) \ No newline at end of file From e8ef980daf3c72abc699a654dfe6c4e5b8bfbcda Mon Sep 17 00:00:00 2001 From: Allan Feldman <6374032+a-feld@users.noreply.github.com> Date: Thu, 8 Aug 2019 17:47:57 -0700 Subject: [PATCH 0040/1517] Update and test default tracer APIs (#82) --- .../src/opentelemetry/trace/__init__.py | 8 ++++ opentelemetry-api/tests/trace/test_tracer.py | 39 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 opentelemetry-api/tests/trace/test_tracer.py diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 6c873002e4..45b6e768a5 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -241,6 +241,8 @@ def get_current_span(self) -> 'Span': The currently active :class:`.Span`, or a placeholder span with an invalid :class:`.SpanContext`. """ + # pylint: disable=no-self-use + return INVALID_SPAN @contextmanager # type: ignore def start_span(self, @@ -289,6 +291,8 @@ def start_span(self, Yields: The newly-created span. """ + # pylint: disable=unused-argument,no-self-use + yield INVALID_SPAN def create_span(self, name: str, @@ -324,6 +328,8 @@ def create_span(self, Returns: The newly-created span. """ + # pylint: disable=unused-argument,no-self-use + return INVALID_SPAN @contextmanager # type: ignore def use_span(self, span: 'Span') -> typing.Iterator[None]: @@ -338,6 +344,8 @@ def use_span(self, span: 'Span') -> typing.Iterator[None]: Args: span: The span to start and make current. """ + # pylint: disable=unused-argument,no-self-use + yield _TRACER: typing.Optional[Tracer] = None diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py new file mode 100644 index 0000000000..f1aa402ffa --- /dev/null +++ b/opentelemetry-api/tests/trace/test_tracer.py @@ -0,0 +1,39 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import trace + + +class TestTracer(unittest.TestCase): + def setUp(self): + self.tracer = trace.Tracer() + + def test_get_current_span(self): + span = self.tracer.get_current_span() + self.assertIsInstance(span, trace.Span) + + def test_start_span(self): + with self.tracer.start_span("") as span: + self.assertIsInstance(span, trace.Span) + + def test_create_span(self): + span = self.tracer.create_span("") + self.assertIsInstance(span, trace.Span) + + def test_use_span(self): + span = trace.Span() + with self.tracer.use_span(span): + pass From 2edc07fb55210997e3e457e749b93568a096c03e Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Tue, 13 Aug 2019 17:26:36 -0700 Subject: [PATCH 0041/1517] Update docs for context lib (#73) --- docs/index.rst | 3 ++- docs/opentelemetry.context.base_context.rst | 7 +++++++ docs/opentelemetry.context.rst | 14 ++++++++++++++ .../src/opentelemetry/context/__init__.py | 11 ++++++----- .../src/opentelemetry/context/base_context.py | 8 ++++++-- 5 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 docs/opentelemetry.context.base_context.rst create mode 100644 docs/opentelemetry.context.rst diff --git a/docs/index.rst b/docs/index.rst index 90385dba4a..8991f2b5df 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,8 +15,9 @@ abstract types for OpenTelemetry implementations. :maxdepth: 1 :caption: Contents: - opentelemetry.trace + opentelemetry.context opentelemetry.loader + opentelemetry.trace Indices and tables diff --git a/docs/opentelemetry.context.base_context.rst b/docs/opentelemetry.context.base_context.rst new file mode 100644 index 0000000000..ac28d40008 --- /dev/null +++ b/docs/opentelemetry.context.base_context.rst @@ -0,0 +1,7 @@ +opentelemetry.context.base\_context module +========================================== + +.. automodule:: opentelemetry.context.base_context + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.context.rst b/docs/opentelemetry.context.rst new file mode 100644 index 0000000000..7bc738a050 --- /dev/null +++ b/docs/opentelemetry.context.rst @@ -0,0 +1,14 @@ +opentelemetry.context package +============================= + +Submodules +---------- + +.. toctree:: + + opentelemetry.context.base_context + +Module contents +--------------- + +.. automodule:: opentelemetry.context diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index a02bc8cb4e..4edbf2bd04 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -19,7 +19,7 @@ contextvars provided by the Python runtime library. A global object ``Context`` is provided to access all the context related -functionalities: +functionalities:: >>> from opentelemetry.context import Context >>> Context.foo = 1 @@ -27,8 +27,9 @@ >>> Context.foo 2 -When explicit thread is used, a helper function `Context.with_current_context` -can be used to carry the context across threads: +When explicit thread is used, a helper function +``Context.with_current_context`` can be used to carry the context across +threads:: from threading import Thread from opentelemetry.context import Context @@ -61,7 +62,7 @@ def work(name): print('Main thread:', Context) -Here goes another example using thread pool: +Here goes another example using thread pool:: import time import threading @@ -96,7 +97,7 @@ def work(name): pool.join() println('Main thread: {}'.format(Context)) -Here goes a simple demo of how async could work in Python 3.7+: +Here goes a simple demo of how async could work in Python 3.7+:: import asyncio diff --git a/opentelemetry-api/src/opentelemetry/context/base_context.py b/opentelemetry-api/src/opentelemetry/context/base_context.py index 35ee179a4b..bb5703e7c3 100644 --- a/opentelemetry-api/src/opentelemetry/context/base_context.py +++ b/opentelemetry-api/src/opentelemetry/context/base_context.py @@ -37,7 +37,7 @@ def set(self, value: 'object') -> None: raise NotImplementedError _lock = threading.Lock() - _slots: typing.Dict[str, Slot] = {} + _slots: typing.Dict[str, 'BaseRuntimeContext.Slot'] = {} @classmethod def clear(cls) -> None: @@ -48,7 +48,11 @@ def clear(cls) -> None: slot.clear() @classmethod - def register_slot(cls, name: str, default: 'object' = None) -> 'Slot': + def register_slot( + cls, + name: str, + default: 'object' = None, + ) -> 'BaseRuntimeContext.Slot': """Register a context slot with an optional default value. :type name: str From 74a95342874ad4e7f01fb89e8cf34ad4f7385d44 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 15 Aug 2019 10:11:07 -0700 Subject: [PATCH 0042/1517] Adding propagators API and b3 SDK implementation (#51, #52) (#78) Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md. --- .../context/propagation/__init__.py | 4 + .../context/propagation/binaryformat.py | 58 ++++++ .../context/propagation/httptextformat.py | 109 +++++++++++ .../src/opentelemetry/sdk/context/__init__.py | 0 .../sdk/context/propagation/__init__.py | 0 .../sdk/context/propagation/b3_format.py | 109 +++++++++++ opentelemetry-sdk/tests/context/__init__.py | 0 .../tests/context/propagation/__init__.py | 0 .../context/propagation/test_b3_format.py | 177 ++++++++++++++++++ 9 files changed, 457 insertions(+) create mode 100644 opentelemetry-api/src/opentelemetry/context/propagation/__init__.py create mode 100644 opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py create mode 100644 opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/context/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py create mode 100644 opentelemetry-sdk/tests/context/__init__.py create mode 100644 opentelemetry-sdk/tests/context/propagation/__init__.py create mode 100644 opentelemetry-sdk/tests/context/propagation/test_b3_format.py diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py new file mode 100644 index 0000000000..b964c2a968 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py @@ -0,0 +1,4 @@ +from .binaryformat import BinaryFormat +from .httptextformat import HTTPTextFormat + +__all__ = ["BinaryFormat", "HTTPTextFormat"] diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py new file mode 100644 index 0000000000..f05ef69972 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py @@ -0,0 +1,58 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import typing + +from opentelemetry.trace import SpanContext + + +class BinaryFormat(abc.ABC): + """API for serialization of span context into binary formats. + + This class provides an interface that enables converting span contexts + to and from a binary format. + """ + @staticmethod + @abc.abstractmethod + def to_bytes(context: SpanContext) -> bytes: + """Creates a byte representation of a SpanContext. + + to_bytes should read values from a SpanContext and return a data + format to represent it, in bytes. + + Args: + context: the SpanContext to serialize + + Returns: + A bytes representation of the SpanContext. + + """ + @staticmethod + @abc.abstractmethod + def from_bytes(byte_representation: bytes) -> typing.Optional[SpanContext]: + """Return a SpanContext that was represented by bytes. + + from_bytes should return back a SpanContext that was constructed from + the data serialized in the byte_representation passed. If it is not + possible to read in a proper SpanContext, return None. + + Args: + byte_representation: the bytes to deserialize + + Returns: + A bytes representation of the SpanContext if it is valid. + Otherwise return None. + + """ diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py new file mode 100644 index 0000000000..860498fe35 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py @@ -0,0 +1,109 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import typing + +from opentelemetry.trace import SpanContext + +Setter = typing.Callable[[object, str, str], None] +Getter = typing.Callable[[object, str], typing.List[str]] + + +class HTTPTextFormat(abc.ABC): + """API for propagation of span context via headers. + + This class provides an interface that enables extracting and injecting + span context into headers of HTTP requests. HTTP frameworks and clients + can integrate with HTTPTextFormat by providing the object containing the + headers, and a getter and setter function for the extraction and + injection of values, respectively. + + Example:: + + import flask + import requests + from opentelemetry.context.propagation import HTTPTextFormat + + PROPAGATOR = HTTPTextFormat() + + + + def get_header_from_flask_request(request, key): + return request.headers.get_all(key) + + def set_header_into_requests_request(request: requests.Request, + key: str, value: str): + request.headers[key] = value + + def example_route(): + span_context = PROPAGATOR.extract( + get_header_from_flask_request, + flask.request + ) + request_to_downstream = requests.Request( + "GET", "http://httpbin.org/get" + ) + PROPAGATOR.inject( + span_context, + set_header_into_requests_request, + request_to_downstream + ) + session = requests.Session() + session.send(request_to_downstream.prepare()) + + + .. _Propagation API Specification: + https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md + """ + @abc.abstractmethod + def extract(self, get_from_carrier: Getter, + carrier: object) -> SpanContext: + """Create a SpanContext from values in the carrier. + + The extract function should retrieve values from the carrier + object using get_from_carrier, and use values to populate a + SpanContext value and return it. + + Args: + get_from_carrier: a function that can retrieve zero + or more values from the carrier. In the case that + the value does not exist, return an empty list. + carrier: and object which contains values that are + used to construct a SpanContext. This object + must be paired with an appropriate get_from_carrier + which understands how to extract a value from it. + Returns: + A SpanContext with configuration found in the carrier. + + """ + @abc.abstractmethod + def inject(self, context: SpanContext, set_in_carrier: Setter, + carrier: object) -> None: + """Inject values from a SpanContext into a carrier. + + inject enables the propagation of values into HTTP clients or + other objects which perform an HTTP request. Implementations + should use the set_in_carrier method to set values on the + carrier. + + Args: + context: The SpanContext to read values from. + set_in_carrier: A setter function that can set values + on the carrier. + carrier: An object that a place to define HTTP headers. + Should be paired with set_in_carrier, which should + know how to set header values on the carrier. + + """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py new file mode 100644 index 0000000000..eaeeb577d2 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -0,0 +1,109 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing + +from opentelemetry.context.propagation.httptextformat import HTTPTextFormat +import opentelemetry.trace as trace + + +class B3Format(HTTPTextFormat): + """Propagator for the B3 HTTP header format. + + See: https://github.com/openzipkin/b3-propagation + """ + + SINGLE_HEADER_KEY = "b3" + TRACE_ID_KEY = "x-b3-traceid" + SPAN_ID_KEY = "x-b3-spanid" + SAMPLED_KEY = "x-b3-sampled" + FLAGS_KEY = "x-b3-flags" + _SAMPLE_PROPAGATE_VALUES = set(["1", "True", "true", "d"]) + + @classmethod + def extract(cls, get_from_carrier, carrier): + trace_id = format_trace_id(trace.INVALID_TRACE_ID) + span_id = format_span_id(trace.INVALID_SPAN_ID) + sampled = 0 + flags = None + + single_header = _extract_first_element( + get_from_carrier(carrier, cls.SINGLE_HEADER_KEY)) + if single_header: + # The b3 spec calls for the sampling state to be + # "deferred", which is unspecified. This concept does not + # translate to SpanContext, so we set it as recorded. + sampled = "1" + fields = single_header.split("-", 4) + + if len(fields) == 1: + sampled = fields[0] + elif len(fields) == 2: + trace_id, span_id = fields + elif len(fields) == 3: + trace_id, span_id, sampled = fields + elif len(fields) == 4: + trace_id, span_id, sampled, _parent_span_id = fields + else: + return trace.INVALID_SPAN_CONTEXT + else: + trace_id = _extract_first_element( + get_from_carrier(carrier, cls.TRACE_ID_KEY)) or trace_id + span_id = _extract_first_element( + get_from_carrier(carrier, cls.SPAN_ID_KEY)) or span_id + sampled = _extract_first_element( + get_from_carrier(carrier, cls.SAMPLED_KEY)) or sampled + flags = _extract_first_element( + get_from_carrier(carrier, cls.FLAGS_KEY)) or flags + + options = 0 + # The b3 spec provides no defined behavior for both sample and + # flag values set. Since the setting of at least one implies + # the desire for some form of sampling, propagate if either + # header is set to allow. + if sampled in cls._SAMPLE_PROPAGATE_VALUES or flags == "1": + options |= trace.TraceOptions.RECORDED + + return trace.SpanContext( + # trace an span ids are encoded in hex, so must be converted + trace_id=int(trace_id, 16), + span_id=int(span_id, 16), + trace_options=options, + trace_state={}, + ) + + @classmethod + def inject(cls, context, set_in_carrier, carrier): + sampled = (trace.TraceOptions.RECORDED & context.trace_options) != 0 + set_in_carrier(carrier, cls.TRACE_ID_KEY, + format_trace_id(context.trace_id)) + set_in_carrier(carrier, cls.SPAN_ID_KEY, + format_span_id(context.span_id)) + set_in_carrier(carrier, cls.SAMPLED_KEY, "1" if sampled else "0") + + +def format_trace_id(trace_id: int): + """Format the trace id according to b3 specification.""" + return format(trace_id, "032x") + + +def format_span_id(span_id: int): + """Format the span id according to b3 specification.""" + return format(span_id, "016x") + + +def _extract_first_element(list_object: list) -> typing.Optional[object]: + if list_object: + return list_object[0] + return None diff --git a/opentelemetry-sdk/tests/context/__init__.py b/opentelemetry-sdk/tests/context/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-sdk/tests/context/propagation/__init__.py b/opentelemetry-sdk/tests/context/propagation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py new file mode 100644 index 0000000000..a24dd01c66 --- /dev/null +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -0,0 +1,177 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +import opentelemetry.trace as api_trace +import opentelemetry.sdk.context.propagation.b3_format as b3_format +import opentelemetry.sdk.trace as trace + +FORMAT = b3_format.B3Format() + + +def get_as_list(dict_object, key): + value = dict_object.get(key) + return [value] if value is not None else [] + + +class TestB3Format(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.serialized_trace_id = b3_format.format_trace_id( + trace.generate_trace_id()) + cls.serialized_span_id = b3_format.format_span_id( + trace.generate_span_id()) + + def test_extract_multi_header(self): + """Test the extraction of B3 headers.""" + carrier = { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.SAMPLED_KEY: "1", + } + span_context = FORMAT.extract(get_as_list, carrier) + new_carrier = {} + FORMAT.inject(span_context, dict.__setitem__, new_carrier) + self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], + self.serialized_trace_id) + self.assertEqual(new_carrier[FORMAT.SPAN_ID_KEY], + self.serialized_span_id) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + + def test_extract_single_header(self): + """Test the extraction from a single b3 header.""" + carrier = { + FORMAT.SINGLE_HEADER_KEY: + "{}-{}".format(self.serialized_trace_id, self.serialized_span_id) + } + span_context = FORMAT.extract(get_as_list, carrier) + new_carrier = {} + FORMAT.inject(span_context, dict.__setitem__, new_carrier) + self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], + self.serialized_trace_id) + self.assertEqual(new_carrier[FORMAT.SPAN_ID_KEY], + self.serialized_span_id) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + + def test_extract_header_precedence(self): + """A single b3 header should take precedence over multiple + headers. + """ + single_header_trace_id = self.serialized_trace_id[:-3] + "123" + carrier = { + FORMAT.SINGLE_HEADER_KEY: + "{}-{}".format(single_header_trace_id, self.serialized_span_id), + FORMAT.TRACE_ID_KEY: + self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: + self.serialized_span_id, + FORMAT.SAMPLED_KEY: + "1", + } + span_context = FORMAT.extract(get_as_list, carrier) + new_carrier = {} + FORMAT.inject(span_context, dict.__setitem__, new_carrier) + self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], + single_header_trace_id) + + def test_enabled_sampling(self): + """Test b3 sample key variants that turn on sampling.""" + for variant in ["1", "True", "true", "d"]: + carrier = { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.SAMPLED_KEY: variant, + } + span_context = FORMAT.extract(get_as_list, carrier) + new_carrier = {} + FORMAT.inject(span_context, dict.__setitem__, new_carrier) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + + def test_disabled_sampling(self): + """Test b3 sample key variants that turn off sampling.""" + for variant in ["0", "False", "false", None]: + carrier = { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.SAMPLED_KEY: variant, + } + span_context = FORMAT.extract(get_as_list, carrier) + new_carrier = {} + FORMAT.inject(span_context, dict.__setitem__, new_carrier) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "0") + + def test_flags(self): + """x-b3-flags set to "1" should result in propagation.""" + carrier = { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1", + } + span_context = FORMAT.extract(get_as_list, carrier) + new_carrier = {} + FORMAT.inject(span_context, dict.__setitem__, new_carrier) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + + def test_flags_and_sampling(self): + """Propagate if b3 flags and sampling are set.""" + carrier = { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1", + } + span_context = FORMAT.extract(get_as_list, carrier) + new_carrier = {} + FORMAT.inject(span_context, dict.__setitem__, new_carrier) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + + def test_64bit_trace_id(self): + """64 bit trace ids should be padded to 128 bit trace ids.""" + trace_id_64_bit = self.serialized_trace_id[:16] + carrier = { + FORMAT.TRACE_ID_KEY: trace_id_64_bit, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1", + } + span_context = FORMAT.extract(get_as_list, carrier) + new_carrier = {} + FORMAT.inject(span_context, dict.__setitem__, new_carrier) + self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], + "0" * 16 + trace_id_64_bit) + + def test_invalid_single_header(self): + """If an invalid single header is passed, return an + invalid SpanContext. + """ + carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} + span_context = FORMAT.extract(get_as_list, carrier) + self.assertEqual(span_context.trace_id, api_trace.INVALID_TRACE_ID) + self.assertEqual(span_context.span_id, api_trace.INVALID_SPAN_ID) + + def test_missing_trace_id(self): + """If a trace id is missing, populate an invalid trace id.""" + carrier = { + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1" + } + span_context = FORMAT.extract(get_as_list, carrier) + self.assertEqual(span_context.trace_id, api_trace.INVALID_TRACE_ID) + + def test_missing_span_id(self): + """If a trace id is missing, populate an invalid trace id.""" + carrier = { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.FLAGS_KEY: "1" + } + span_context = FORMAT.extract(get_as_list, carrier) + self.assertEqual(span_context.span_id, api_trace.INVALID_SPAN_ID) From b7b38a5d5477797553ff0b85a690f55c9436e009 Mon Sep 17 00:00:00 2001 From: Alban Crequy Date: Thu, 15 Aug 2019 19:15:56 +0200 Subject: [PATCH 0043/1517] Span: add attribute, event, link setter to the API (#83) --- .../src/opentelemetry/trace/__init__.py | 30 ++++++++ .../src/opentelemetry/sdk/trace/__init__.py | 14 ++-- opentelemetry-sdk/tests/trace/test_trace.py | 68 +++++++++++++++++++ 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 45b6e768a5..aed421307e 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -65,6 +65,7 @@ import typing from opentelemetry import loader +from opentelemetry import types # TODO: quarantine ParentSpan = typing.Optional[typing.Union['Span', 'SpanContext']] @@ -102,6 +103,35 @@ def get_context(self) -> 'SpanContext': A :class:`.SpanContext` with a copy of this span's immutable state. """ + def set_attribute(self: 'Span', + key: str, + value: types.AttributeValue, + ) -> None: + """Sets an Attribute. + + Sets a single Attribute with the key and value passed as arguments. + """ + + def add_event(self: 'Span', + name: str, + attributes: types.Attributes = None, + ) -> None: + """Adds an Event. + + Adds a single Event with the name and, optionally, attributes passed + as arguments. + """ + + def add_link(self: 'Span', + link_target_context: 'SpanContext', + attributes: types.Attributes = None, + ) -> None: + """Adds a Link to another span. + + Adds a single Link from this Span to another Span identified by the + `SpanContext` passed as argument. + """ + class TraceOptions(int): """A bitmask that represents options specific to the trace. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index f6f129827e..0a85c14c67 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -230,7 +230,7 @@ def get_context(self): def set_attribute(self: 'Span', key: str, - value: 'types.AttributeValue' + value: types.AttributeValue, ) -> None: if self.attributes is Span.empty_attributes: self.attributes = BoundedDict(MAX_NUM_ATTRIBUTES) @@ -238,19 +238,23 @@ def set_attribute(self: 'Span', def add_event(self: 'Span', name: str, - attributes: 'types.Attributes', + attributes: types.Attributes = None, ) -> None: if self.events is Span.empty_events: self.events = BoundedList(MAX_NUM_EVENTS) + if attributes is None: + attributes = Span.empty_attributes self.events.append(Event(name, attributes)) def add_link(self: 'Span', - context: 'trace_api.SpanContext', - attributes: 'types.Attributes', + link_target_context: 'trace_api.SpanContext', + attributes: types.Attributes = None, ) -> None: if self.links is Span.empty_links: self.links = BoundedList(MAX_NUM_LINKS) - self.links.append(Link(context, attributes)) + if attributes is None: + attributes = Span.empty_attributes + self.links.append(Link(link_target_context, attributes)) def start(self): if self.start_time is None: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 7a91d23765..7a4faf78d1 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -114,6 +114,74 @@ def test_start_span_explicit(self): self.assertIs(tracer.get_current_span(), root) self.assertIsNotNone(child.end_time) + def test_span_members(self): + context = contextvars.ContextVar('test_span_members') + tracer = trace.Tracer(context) + + other_context1 = trace_api.SpanContext( + trace_id=trace.generate_trace_id(), + span_id=trace.generate_span_id() + ) + other_context2 = trace_api.SpanContext( + trace_id=trace.generate_trace_id(), + span_id=trace.generate_span_id() + ) + + self.assertIsNone(tracer.get_current_span()) + + with tracer.start_span('root') as root: + root.set_attribute('component', 'http') + root.set_attribute('http.method', 'GET') + root.set_attribute('http.url', + 'https://example.com:779/path/12/?q=d#123') + root.set_attribute('http.status_code', 200) + root.set_attribute('http.status_text', 'OK') + root.set_attribute('misc.pi', 3.14) + + # Setting an attribute with the same key as an existing attribute + # SHOULD overwrite the existing attribute's value. + root.set_attribute('attr-key', 'attr-value1') + root.set_attribute('attr-key', 'attr-value2') + + root.add_event('event0') + root.add_event('event1', {'name': 'birthday'}) + + root.add_link(other_context1) + root.add_link(other_context2, {'name': 'neighbor'}) + + # The public API does not expose getters. + # Checks by accessing the span members directly + + self.assertEqual(len(root.attributes), 7) + self.assertEqual(root.attributes['component'], 'http') + self.assertEqual(root.attributes['http.method'], 'GET') + self.assertEqual(root.attributes['http.url'], + 'https://example.com:779/path/12/?q=d#123') + self.assertEqual(root.attributes['http.status_code'], 200) + self.assertEqual(root.attributes['http.status_text'], 'OK') + self.assertEqual(root.attributes['misc.pi'], 3.14) + self.assertEqual(root.attributes['attr-key'], 'attr-value2') + + self.assertEqual(len(root.events), 2) + self.assertEqual(root.events[0], + trace.Event(name='event0', + attributes={})) + self.assertEqual(root.events[1], + trace.Event(name='event1', + attributes={'name': 'birthday'})) + + self.assertEqual(len(root.links), 2) + self.assertEqual(root.links[0].context.trace_id, + other_context1.trace_id) + self.assertEqual(root.links[0].context.span_id, + other_context1.span_id) + self.assertEqual(root.links[0].attributes, {}) + self.assertEqual(root.links[1].context.trace_id, + other_context2.trace_id) + self.assertEqual(root.links[1].context.span_id, + other_context2.span_id) + self.assertEqual(root.links[1].attributes, {'name': 'neighbor'}) + class TestSpan(unittest.TestCase): From ae13b26d7b07216df99053d3e1ffdfea22ec298c Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Fri, 16 Aug 2019 16:25:08 -0700 Subject: [PATCH 0044/1517] Update contributing doc (#90) --- CONTRIBUTING.md | 87 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a96c835af..d11b7217cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,19 +5,65 @@ The Python special interest group (SIG) meets regularly. See the OpenTelemetry information on this and other language SIGs. See the [public meeting notes](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit) -for a summary description of past meetings. To request edit access join the -meeting or get in touch on Gitter. +for a summary description of past meetings. To request edit access, join the +meeting or get in touch on [Gitter](https://gitter.im/open-telemetry/opentelemetry-python). -## Running Tests +## Pull Request -Execute tests as well as the linting process with tox: +### How to Send Pull Requests - pip install --user tox - tox # execute in the root of the repository +Everyone is welcome to contribute code to `opentelemetry-python` via GitHub +pull requests (PRs). -## Testing +To create a new PR, fork the project in GitHub and clone the upstream repo: -The standard Python unittest module is used to author unit tests. +```sh +$ git clone https://https://github.com/open-telemetry/opentelemetry-python.git +``` + +Add your fork as an origin: + +```sh +$ git remote add fork https://github.com/YOUR_GITHUB_USERNAME/opentelemetry-python.git +``` + +Run tests: + +```sh +# make sure you have all supported versions of Python installed +$ pip install tox # only first time. +$ tox # execute in the root of the repository +``` + +Check out a new branch, make modifications and push the branch to your fork: + +```sh +$ git checkout -b feature +# edit files +$ git commit +$ git push fork feature +``` + +Open a pull request against the main `opentelemetry-python` repo. + +### How to Receive Comments + +* If the PR is not ready for review, please put `[WIP]` in the title, tag it + as `work-in-progress`, or mark it as [`draft`](https://github.blog/2019-02-14-introducing-draft-pull-requests/). +* Make sure CLA is signed and CI is clear. + +### How to Get PR Merged + +A PR is considered to be **ready to merge** when: +* It has received two approvals from Collaborators/Maintainers (at different + companies). +* Major feedbacks are resolved. +* It has been open for review for at least one working day. This gives people + reasonable time to review. +* Trivial change (typo, cosmetic, doc, etc.) doesn't have to wait for one day. +* Urgent fix can take exception as long as it has been actively communicated. + +Any Collaborator/Maintainer can merge the PR once it is **ready to merge**. ## Design Choices @@ -39,8 +85,29 @@ rather than conform to specific API names or argument patterns in the spec. For a deeper discussion, see: https://github.com/open-telemetry/opentelemetry-specification/issues/165 - ## Styleguide * docstrings should adhere to the Google styleguide as specified - with the [napolean extension](http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#google-vs-numpy) extension in [Sphinx](http://www.sphinx-doc.org/en/master/index.html). \ No newline at end of file + with the [napolean extension](http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#google-vs-numpy) extension in [Sphinx](http://www.sphinx-doc.org/en/master/index.html). + +## Become a Collaborator + +Collaborators have write access to the repo. + +To become a Collaborator: +* Become an active Contributor by working on PRs. +* Actively participate in the community meeting, design discussion, PR review + and issue discussion. +* Contact the Maintainers, express the willingness and commitment. +* Acknowledged and approved by two Maintainers (at different companies). + +## Become a Maintainer + +Maintainers have admin access to the repo. + +To become a Maintainer: +* Become a [member of OpenTelemetry organization](https://github.com/orgs/open-telemetry/people). +* Become a Collaborator. +* Demonstrate the ability and commitment. +* Contact the Maintainers, express the willingness and commitment. +* Acknowledged and approved by all the current Maintainers. \ No newline at end of file From 17afba58e9745f25dfa5445ee2e11816388b7221 Mon Sep 17 00:00:00 2001 From: Allan Feldman <6374032+a-feld@users.noreply.github.com> Date: Mon, 19 Aug 2019 11:10:15 -0700 Subject: [PATCH 0045/1517] Implement WSGI middleware integration. (#84) --- ext/opentelemetry-ext-wsgi/README.rst | 47 +++++ ext/opentelemetry-ext-wsgi/setup.cfg | 45 ++++ ext/opentelemetry-ext-wsgi/setup.py | 30 +++ .../src/opentelemetry/ext/wsgi/__init__.py | 105 ++++++++++ .../src/opentelemetry/ext/wsgi/version.py | 15 ++ ext/opentelemetry-ext-wsgi/tests/__init__.py | 0 .../tests/test_wsgi_middleware.py | 195 ++++++++++++++++++ tox.ini | 22 +- 8 files changed, 452 insertions(+), 7 deletions(-) create mode 100644 ext/opentelemetry-ext-wsgi/README.rst create mode 100644 ext/opentelemetry-ext-wsgi/setup.cfg create mode 100644 ext/opentelemetry-ext-wsgi/setup.py create mode 100644 ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py create mode 100644 ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py create mode 100644 ext/opentelemetry-ext-wsgi/tests/__init__.py create mode 100644 ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py diff --git a/ext/opentelemetry-ext-wsgi/README.rst b/ext/opentelemetry-ext-wsgi/README.rst new file mode 100644 index 0000000000..d47643c871 --- /dev/null +++ b/ext/opentelemetry-ext-wsgi/README.rst @@ -0,0 +1,47 @@ +OpenTelemetry WSGI Middleware +============================= + +This library provides a WSGI middleware that can be used on any WSGI framework +(such as Django / Flask) to track requests timing through OpenTelemetry. + + +Usage (Flask) +------------- + +.. code-block:: python + + from flask import Flask + from opentelemetry.ext.wsgi import OpenTelemetryMiddleware + + app = Flask(__name__) + app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) + + @app.route("/") + def hello(): + return "Hello!" + + if __name__ == "__main__": + app.run(debug=True) + + +Usage (Django) +-------------- + +Modify the application's ``wsgi.py`` file as shown below. + +.. code-block:: python + + import os + from opentelemetry.ext.wsgi import OpenTelemetryMiddleware + from django.core.wsgi import get_wsgi_application + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') + + application = get_wsgi_application() + application = OpenTelemetryMiddleware(application) + +References +---------- + +* `OpenTelemetry Project `_ +* `WSGI `_ diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg new file mode 100644 index 0000000000..a77e9fd1fb --- /dev/null +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -0,0 +1,45 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-wsgi +description = WSGI Middleware for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-wsgi +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find: +install_requires = + opentelemetry-api + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-wsgi/setup.py b/ext/opentelemetry-ext-wsgi/setup.py new file mode 100644 index 0000000000..f249602261 --- /dev/null +++ b/ext/opentelemetry-ext-wsgi/setup.py @@ -0,0 +1,30 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "ext", + "wsgi", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py new file mode 100644 index 0000000000..29d57567a0 --- /dev/null +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -0,0 +1,105 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The opentelemetry-ext-wsgi package provides a WSGI middleware that can be used +on any WSGI framework (such as Django / Flask) to track requests timing through +OpenTelemetry. +""" + +import functools +import wsgiref.util as wsgiref_util + +from opentelemetry import trace +from opentelemetry.ext.wsgi.version import __version__ # noqa + + +class OpenTelemetryMiddleware: + """The WSGI application middleware. + + This class is used to create and annotate spans for requests to a WSGI + application. + + Args: + wsgi: The WSGI application callable. + """ + + def __init__( + self, + wsgi, + propagators=None, + ): + self.wsgi = wsgi + + # TODO: implement context propagation + self.propagators = propagators + + @staticmethod + def _add_request_attributes(span, environ): + span.set_attribute("component", "http") + span.set_attribute("http.method", environ["REQUEST_METHOD"]) + + host = environ.get("HTTP_HOST") or environ["SERVER_NAME"] + span.set_attribute("http.host", host) + + url = ( + environ.get("REQUEST_URI") or + environ.get("RAW_URI") or + wsgiref_util.request_uri(environ, include_query=False) + ) + span.set_attribute("http.url", url) + + @staticmethod + def _add_response_attributes(span, status): + status_code, status_text = status.split(" ", 1) + span.set_attribute("http.status_text", status_text) + + try: + status_code = int(status_code) + except ValueError: + pass + else: + span.set_attribute("http.status_code", status_code) + + @classmethod + def _create_start_response(cls, span, start_response): + @functools.wraps(start_response) + def _start_response(status, response_headers, *args, **kwargs): + cls._add_response_attributes(span, status) + return start_response(status, response_headers, *args, **kwargs) + + return _start_response + + def __call__(self, environ, start_response): + """The WSGI application + + Args: + environ: A WSGI environment. + start_response: The WSGI start_response callable. + """ + + tracer = trace.tracer() + path_info = environ["PATH_INFO"] or "/" + + with tracer.start_span(path_info) as span: + self._add_request_attributes(span, environ) + start_response = self._create_start_response(span, start_response) + + iterable = self.wsgi(environ, start_response) + try: + for yielded in iterable: + yield yielded + finally: + if hasattr(iterable, 'close'): + iterable.close() diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py new file mode 100644 index 0000000000..a457c2b665 --- /dev/null +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.1.dev0" diff --git a/ext/opentelemetry-ext-wsgi/tests/__init__.py b/ext/opentelemetry-ext-wsgi/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py new file mode 100644 index 0000000000..70b7e8fcf6 --- /dev/null +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -0,0 +1,195 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import sys +import unittest +import unittest.mock as mock +import wsgiref.util as wsgiref_util + +from opentelemetry import trace as trace_api +from opentelemetry.ext.wsgi import OpenTelemetryMiddleware + + +class Response: + def __init__(self): + self.iter = iter([b"*"]) + self.close_calls = 0 + + def __iter__(self): + return self + + def __next__(self): + return next(self.iter) + + def close(self): + self.close_calls += 1 + + +def simple_wsgi(environ, start_response): + assert isinstance(environ, dict) + start_response("200 OK", [("Content-Type", "text/plain")]) + return [b"*"] + + +def create_iter_wsgi(response): + def iter_wsgi(environ, start_response): + assert isinstance(environ, dict) + start_response("200 OK", [("Content-Type", "text/plain")]) + return response + return iter_wsgi + + +def error_wsgi(environ, start_response): + assert isinstance(environ, dict) + try: + raise ValueError + except ValueError: + exc_info = sys.exc_info() + start_response("200 OK", [("Content-Type", "text/plain")], exc_info) + exc_info = None + return [b"*"] + + +class TestWsgiApplication(unittest.TestCase): + def setUp(self): + tracer = trace_api.tracer() + self.span_context_manager = mock.MagicMock() + self.span_context_manager.__enter__.return_value = \ + mock.create_autospec( + trace_api.Span, spec_set=True + ) + self.patcher = mock.patch.object( + tracer, + "start_span", + autospec=True, + spec_set=True, + return_value=self.span_context_manager, + ) + self.start_span = self.patcher.start() + + self.write_buffer = io.BytesIO() + self.write = self.write_buffer.write + + self.environ = {} + wsgiref_util.setup_testing_defaults(self.environ) + + self.status = None + self.response_headers = None + self.exc_info = None + + def tearDown(self): + self.patcher.stop() + + def start_response(self, status, response_headers, exc_info=None): + # The span should have started already + self.span_context_manager.__enter__.assert_called() + + self.status = status + self.response_headers = response_headers + self.exc_info = exc_info + return self.write + + def validate_response(self, response, error=None): + while True: + try: + value = next(response) + self.span_context_manager.__exit__.assert_not_called() + self.assertEqual(value, b"*") + except StopIteration: + self.span_context_manager.__exit__.assert_called() + break + + self.assertEqual(self.status, "200 OK") + self.assertEqual( + self.response_headers, + [("Content-Type", "text/plain")] + ) + if error: + self.assertIs(self.exc_info[0], error) + self.assertIsInstance(self.exc_info[1], error) + self.assertIsNotNone(self.exc_info[2]) + else: + self.assertIsNone(self.exc_info) + + # Verify that start_span has been called + self.start_span.assert_called_once_with("/") + + def test_basic_wsgi_call(self): + app = OpenTelemetryMiddleware(simple_wsgi) + response = app(self.environ, self.start_response) + self.validate_response(response) + + def test_wsgi_iterable(self): + original_response = Response() + iter_wsgi = create_iter_wsgi(original_response) + app = OpenTelemetryMiddleware(iter_wsgi) + response = app(self.environ, self.start_response) + # Verify that start_response has not been called yet + self.assertIsNone(self.status) + self.validate_response(response) + + # Verify that close has been called exactly once + assert original_response.close_calls == 1 + + def test_wsgi_exc_info(self): + app = OpenTelemetryMiddleware(error_wsgi) + response = app(self.environ, self.start_response) + self.validate_response(response, error=ValueError) + + +class TestWsgiAttributes(unittest.TestCase): + def setUp(self): + self.environ = {} + wsgiref_util.setup_testing_defaults(self.environ) + self.span = mock.create_autospec(trace_api.Span, spec_set=True) + + def test_request_attributes(self): + OpenTelemetryMiddleware._add_request_attributes( # noqa pylint: disable=protected-access + self.span, + self.environ, + ) + expected = ( + mock.call("component", "http"), + mock.call("http.method", "GET"), + mock.call("http.host", "127.0.0.1"), + mock.call("http.url", "http://127.0.0.1/"), + ) + self.assertEqual(self.span.set_attribute.call_count, len(expected)) + self.span.set_attribute.assert_has_calls(expected, any_order=True) + + def test_response_attributes(self): + OpenTelemetryMiddleware._add_response_attributes( # noqa pylint: disable=protected-access + self.span, "404 Not Found", + ) + expected = ( + mock.call("http.status_code", 404), + mock.call("http.status_text", "Not Found"), + ) + self.assertEqual(self.span.set_attribute.call_count, len(expected)) + self.span.set_attribute.assert_has_calls(expected, any_order=True) + + def test_response_attributes_invalid_status_code(self): + OpenTelemetryMiddleware._add_response_attributes( # noqa pylint: disable=protected-access + self.span, "Invalid Status Code", + ) + self.assertEqual(self.span.set_attribute.call_count, 1) + self.span.set_attribute.assert_called_with( + "http.status_text", + "Status Code", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tox.ini b/tox.ini index cd71124361..82178115c0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,11 @@ [tox] skipsdist = True -envlist = py{34,35,36,37}-test-{api,sdk}, lint, py37-mypy, docs +envlist = + py{34,35,36,37}-test-{api,sdk} + py{34,35,36,37}-test-ext-wsgi + lint + py37-mypy + docs [travis] python = @@ -16,17 +21,19 @@ setenv = changedir = test-api: opentelemetry-api/tests test-sdk: opentelemetry-sdk/tests + test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests commands_pre = - test-api: pip install -e {toxinidir}/opentelemetry-api - test-sdk: pip install -e {toxinidir}/opentelemetry-api + test: pip install -e {toxinidir}/opentelemetry-api test-sdk: pip install -e {toxinidir}/opentelemetry-sdk + ext: pip install -e {toxinidir}/opentelemetry-api + wsgi: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi commands = mypy: mypy --namespace-packages opentelemetry-api/src/opentelemetry/ ; For test code, we don't want to enforce the full mypy strictness mypy: mypy --namespace-packages --config-file=mypy-relaxed.ini opentelemetry-api/tests/ - test-{api,sdk}: python -m unittest discover + test: python -m unittest discover [testenv:lint] deps = @@ -37,13 +44,14 @@ deps = commands_pre = pip install -e {toxinidir}/opentelemetry-api pip install -e {toxinidir}/opentelemetry-sdk + pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi commands = ; Prefer putting everything in one pylint command to profit from duplication ; warnings. - pylint opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry/ opentelemetry-sdk/tests/ - flake8 opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry/ opentelemetry-sdk/tests/ - isort --check-only --recursive opentelemetry-api/src opentelemetry-sdk/src + pylint opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry/ opentelemetry-sdk/tests/ ext/opentelemetry-ext-wsgi/src/ ext/opentelemetry-ext-wsgi/tests/ + flake8 opentelemetry-api/src/opentelemetry/ opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry/ opentelemetry-sdk/tests/ ext/opentelemetry-ext-wsgi/src/ ext/opentelemetry-ext-wsgi/tests/ + isort --check-only --recursive opentelemetry-api/src opentelemetry-sdk/src ext/opentelemetry-ext-wsgi/src ext/opentelemetry-ext-wsgi/tests [testenv:docs] deps = From 20011c57b67e00208732487a4dd95ec0310f4cab Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Tue, 20 Aug 2019 09:42:02 -0700 Subject: [PATCH 0046/1517] Integrate tracer with context (#86) --- README.md | 20 +++++++++++++++ .../src/opentelemetry/context/base_context.py | 14 +++++++---- .../src/opentelemetry/sdk/trace/__init__.py | 25 ++++++++----------- opentelemetry-sdk/tests/trace/test_trace.py | 7 ++---- 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index edea6801e3..56f4d0f7d1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,26 @@ The Python [OpenTelemetry](https://opentelemetry.io/) client. +## Installation + +## Usage +```python +from opentelemetry import trace +from opentelemetry.context import Context +from opentelemetry.sdk.trace import Tracer + +trace.set_preferred_tracer_implementation(lambda T: Tracer()) +tracer = trace.tracer() +with tracer.start_span('foo'): + print(Context) + with tracer.start_span('bar'): + print(Context) + with tracer.start_span('baz'): + print(Context) + print(Context) + print(Context) +``` + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) \ No newline at end of file diff --git a/opentelemetry-api/src/opentelemetry/context/base_context.py b/opentelemetry-api/src/opentelemetry/context/base_context.py index bb5703e7c3..c750d993ad 100644 --- a/opentelemetry-api/src/opentelemetry/context/base_context.py +++ b/opentelemetry-api/src/opentelemetry/context/base_context.py @@ -64,11 +64,9 @@ def register_slot( :returns: The registered slot. """ with cls._lock: - if name in cls._slots: - raise ValueError('slot {} already registered'.format(name)) - slot = cls.Slot(name, default) - cls._slots[name] = slot - return slot + if name not in cls._slots: + cls._slots[name] = cls.Slot(name, default) + return cls._slots[name] def apply(self, snapshot: typing.Dict[str, 'object']) -> None: """Set the current context from a given snapshot dictionary""" @@ -97,6 +95,12 @@ def __setattr__(self, name: str, value: 'object') -> None: slot = self._slots[name] slot.set(value) + def __getitem__(self, name: str) -> 'object': + return self.__getattr__(name) + + def __setitem__(self, name: str, value: 'object') -> None: + self.__setattr__(name, value) + def with_current_context( self, func: typing.Callable[..., 'object'], diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0a85c14c67..03cf34c178 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -17,13 +17,13 @@ from collections import deque from collections import namedtuple from contextlib import contextmanager -import contextvars import random import threading import typing from opentelemetry import trace as trace_api from opentelemetry import types +from opentelemetry.context import Context from opentelemetry.sdk import util try: @@ -35,9 +35,6 @@ from collections import MutableMapping from collections import Sequence - -_CURRENT_SPAN_CV = contextvars.ContextVar('current_span', default=None) - MAX_NUM_ATTRIBUTES = 32 MAX_NUM_EVENTS = 128 MAX_NUM_LINKS = 32 @@ -287,21 +284,20 @@ class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`. Args: - cv: The context variable that holds the current span. + name: The name of the tracer. """ def __init__(self, - cv: 'contextvars.ContextVar' = _CURRENT_SPAN_CV + name: str = '' ) -> None: - self._cv = cv - try: - self._cv.get() - except LookupError: - self._cv.set(None) + slot_name = 'current_span' + if name: + slot_name = '{}.current_span'.format(name) + self._current_span_slot = Context.register_slot(slot_name) def get_current_span(self): """See `opentelemetry.trace.Tracer.get_current_span`.""" - return self._cv.get() + return self._current_span_slot.get() @contextmanager def start_span(self, @@ -341,11 +337,12 @@ def create_span(self, def use_span(self, span: 'Span') -> typing.Iterator['Span']: """See `opentelemetry.trace.Tracer.use_span`.""" span.start() - token = self._cv.set(span) + span_snapshot = self._current_span_slot.get() + self._current_span_slot.set(span) try: yield span finally: - self._cv.reset(token) + self._current_span_slot.set(span_snapshot) span.end() diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 7a4faf78d1..cf7ca21f13 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -13,7 +13,6 @@ # limitations under the License. from unittest import mock -import contextvars import unittest from opentelemetry import trace as trace_api @@ -30,8 +29,7 @@ def test_extends_api(self): class TestSpanCreation(unittest.TestCase): def test_start_span_implicit(self): - context = contextvars.ContextVar('test_start_span_implicit') - tracer = trace.Tracer(context) + tracer = trace.Tracer('test_start_span_implicit') self.assertIsNone(tracer.get_current_span()) @@ -69,8 +67,7 @@ def test_start_span_implicit(self): self.assertIsNotNone(root.end_time) def test_start_span_explicit(self): - context = contextvars.ContextVar('test_start_span_explicit') - tracer = trace.Tracer(context) + tracer = trace.Tracer('test_start_span_explicit') other_parent = trace_api.SpanContext( trace_id=0x000000000000000000000000deadbeef, From cb4fc12b7c1ff7cbc1e96fdb8eb1431a3168b5eb Mon Sep 17 00:00:00 2001 From: Allan Feldman <6374032+a-feld@users.noreply.github.com> Date: Tue, 20 Aug 2019 17:59:42 -0700 Subject: [PATCH 0047/1517] Fix test failures involving contextvars. (#96) --- opentelemetry-sdk/tests/trace/test_trace.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index cf7ca21f13..cd13d0fb70 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -112,8 +112,7 @@ def test_start_span_explicit(self): self.assertIsNotNone(child.end_time) def test_span_members(self): - context = contextvars.ContextVar('test_span_members') - tracer = trace.Tracer(context) + tracer = trace.Tracer('test_span_members') other_context1 = trace_api.SpanContext( trace_id=trace.generate_trace_id(), From 09d0fdfbc732b326a29595f25d4047a8ff3acbdd Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Thu, 22 Aug 2019 20:54:15 -0700 Subject: [PATCH 0048/1517] tag more reviewers (#102) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e9a8df9505..5f19bb72a0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,4 +2,4 @@ # This file controls who is tagged for review for any given pull request. # For anything not explicitly taken by someone else: -* @c24t @carlosalberto @Oberon00 @reyang +* @a-feld @c24t @carlosalberto @lzchen @Oberon00 @reyang @toumorokoshi From 2bcb228a7dd5ec7895bc27f81ea4d1b55737b58e Mon Sep 17 00:00:00 2001 From: Aliaksei Urbanski Date: Mon, 26 Aug 2019 13:48:04 +0300 Subject: [PATCH 0049/1517] Improve testing (#95) I believe it would be nice to have tests on CI not only for Python 3.7, but for all supported Python versions. These changes: - fix compatibility with Python 3.5 and 3.4 - add tests for various Python versions on CI - allow running tests for any branches --- .travis.yml | 13 +++++++++---- .../tests/test_wsgi_middleware.py | 6 ++++-- .../src/opentelemetry/context/__init__.py | 4 +++- .../src/opentelemetry/context/async_context.py | 9 +++++---- .../src/opentelemetry/context/base_context.py | 4 ++-- .../context/thread_local_context.py | 9 +++++---- opentelemetry-api/src/opentelemetry/loader.py | 2 +- .../src/opentelemetry/trace/__init__.py | 17 +++++++++++------ tox.ini | 7 +++++-- 9 files changed, 45 insertions(+), 26 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4f9cb871ea..64eebc3621 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,14 +3,19 @@ dist: xenial language: python python: + - '3.4' + - '3.5' + - '3.6' - '3.7' + - 'pypy3.5' + - '3.8-dev' + +matrix: + allow_failures: + - python: '3.8-dev' install: - pip install tox-travis script: - tox - -branches: - only: - - master diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index 70b7e8fcf6..0e86a871f4 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -94,7 +94,7 @@ def tearDown(self): def start_response(self, status, response_headers, exc_info=None): # The span should have started already - self.span_context_manager.__enter__.assert_called() + self.span_context_manager.__enter__.assert_called_with() self.status = status self.response_headers = response_headers @@ -108,7 +108,9 @@ def validate_response(self, response, error=None): self.span_context_manager.__exit__.assert_not_called() self.assertEqual(value, b"*") except StopIteration: - self.span_context_manager.__exit__.assert_called() + self.span_context_manager.__exit__.assert_called_with( + None, None, None + ) break self.assertEqual(self.status, "200 OK") diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 4edbf2bd04..1495f8828e 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -145,7 +145,9 @@ async def main(): __all__ = ['Context'] -Context: typing.Optional[BaseRuntimeContext] +Context = ( # pylint: disable=invalid-name + None +) # type: typing.Optional[BaseRuntimeContext] try: from .async_context import AsyncRuntimeContext diff --git a/opentelemetry-api/src/opentelemetry/context/async_context.py b/opentelemetry-api/src/opentelemetry/context/async_context.py index 413e7b2543..559d36234b 100644 --- a/opentelemetry-api/src/opentelemetry/context/async_context.py +++ b/opentelemetry-api/src/opentelemetry/context/async_context.py @@ -13,7 +13,7 @@ # limitations under the License. from contextvars import ContextVar -import typing +import typing # pylint: disable=unused-import from . import base_context @@ -23,9 +23,10 @@ class Slot(base_context.BaseRuntimeContext.Slot): def __init__(self, name: str, default: 'object'): # pylint: disable=super-init-not-called self.name = name - self.contextvar: 'ContextVar[object]' = ContextVar(name) - self.default: typing.Callable[..., object] - self.default = base_context.wrap_callable(default) + self.contextvar = ContextVar(name) # type: ContextVar[object] + self.default = base_context.wrap_callable( + default + ) # type: typing.Callable[..., object] def clear(self) -> None: self.contextvar.set(self.default()) diff --git a/opentelemetry-api/src/opentelemetry/context/base_context.py b/opentelemetry-api/src/opentelemetry/context/base_context.py index c750d993ad..5c1ffb4a8e 100644 --- a/opentelemetry-api/src/opentelemetry/context/base_context.py +++ b/opentelemetry-api/src/opentelemetry/context/base_context.py @@ -37,7 +37,7 @@ def set(self, value: 'object') -> None: raise NotImplementedError _lock = threading.Lock() - _slots: typing.Dict[str, 'BaseRuntimeContext.Slot'] = {} + _slots = {} # type: typing.Dict[str, 'BaseRuntimeContext.Slot'] @classmethod def clear(cls) -> None: @@ -112,7 +112,7 @@ def with_current_context( def call_with_current_context( *args: 'object', - **kwargs: 'object', + **kwargs: 'object' ) -> 'object': try: backup_context = self.snapshot() diff --git a/opentelemetry-api/src/opentelemetry/context/thread_local_context.py b/opentelemetry-api/src/opentelemetry/context/thread_local_context.py index dd11128b7a..8313fb57e8 100644 --- a/opentelemetry-api/src/opentelemetry/context/thread_local_context.py +++ b/opentelemetry-api/src/opentelemetry/context/thread_local_context.py @@ -13,7 +13,7 @@ # limitations under the License. import threading -import typing +import typing # pylint: disable=unused-import from . import base_context @@ -25,15 +25,16 @@ class Slot(base_context.BaseRuntimeContext.Slot): def __init__(self, name: str, default: 'object'): # pylint: disable=super-init-not-called self.name = name - self.default: typing.Callable[..., object] - self.default = base_context.wrap_callable(default) + self.default = base_context.wrap_callable( + default + ) # type: typing.Callable[..., object] def clear(self) -> None: setattr(self._thread_local, self.name, self.default()) def get(self) -> 'object': try: - got: object = getattr(self._thread_local, self.name) + got = getattr(self._thread_local, self.name) # type: object return got except AttributeError: value = self.default() diff --git a/opentelemetry-api/src/opentelemetry/loader.py b/opentelemetry-api/src/opentelemetry/loader.py index 9e28846b8c..78b7cc4f32 100644 --- a/opentelemetry-api/src/opentelemetry/loader.py +++ b/opentelemetry-api/src/opentelemetry/loader.py @@ -83,7 +83,7 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: # code. # ImplementationFactory = Callable[[Type[_T]], Optional[_T]] -_DEFAULT_FACTORY: Optional[_UntrustedImplFactory[object]] = None +_DEFAULT_FACTORY = None # type: Optional[_UntrustedImplFactory[object]] def _try_load_impl_from_modname( diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index aed421307e..0ff3039bcd 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -378,9 +378,15 @@ def use_span(self, span: 'Span') -> typing.Iterator[None]: yield -_TRACER: typing.Optional[Tracer] = None -_TRACER_FACTORY: typing.Optional[ - typing.Callable[[typing.Type[Tracer]], typing.Optional[Tracer]]] = None +# Once https://github.com/python/mypy/issues/7092 is resolved, +# the following type definition should be replaced with +# from opentelemetry.loader import ImplementationFactory +ImplementationFactory = typing.Callable[ + [typing.Type[Tracer]], typing.Optional[Tracer] +] + +_TRACER = None # type: typing.Optional[Tracer] +_TRACER_FACTORY = None # type: typing.Optional[ImplementationFactory] def tracer() -> Tracer: @@ -399,9 +405,8 @@ def tracer() -> Tracer: def set_preferred_tracer_implementation( - factory: typing.Callable[ - [typing.Type[Tracer]], typing.Optional[Tracer]] - ) -> None: + factory: ImplementationFactory +) -> None: """Set the factory to be used to create the tracer. See :mod:`opentelemetry.loader` for details. diff --git a/tox.ini b/tox.ini index 82178115c0..71bc408a39 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,9 @@ [tox] skipsdist = True +skip_missing_interpreters = True envlist = - py{34,35,36,37}-test-{api,sdk} - py{34,35,36,37}-test-ext-wsgi + py3{4,5,6,7,8}-test-{api,sdk,ext-wsgi} + pypy35-test-{api,sdk,ext-wsgi} lint py37-mypy docs @@ -24,6 +25,7 @@ changedir = test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests commands_pre = + pip install -U pip setuptools wheel test: pip install -e {toxinidir}/opentelemetry-api test-sdk: pip install -e {toxinidir}/opentelemetry-sdk ext: pip install -e {toxinidir}/opentelemetry-api @@ -36,6 +38,7 @@ commands = test: python -m unittest discover [testenv:lint] +basepython: python3.7 deps = pylint~=2.3 flake8~=3.7 From 0069fa81c1a61a278708da8c9bd196bd57a698a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 26 Aug 2019 16:52:46 -0500 Subject: [PATCH 0050/1517] fix DefaultSpan class name in documentation (#106) --- opentelemetry-api/src/opentelemetry/trace/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 0ff3039bcd..6336d676ab 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -22,8 +22,8 @@ the operation. This module provides abstract (i.e. unimplemented) classes required for -tracing, and a concrete no-op ``BlankSpan`` that allows applications to use the -API package alone without a supporting implementation. +tracing, and a concrete no-op :class:`.DefaultSpan` that allows applications +to use the API package alone without a supporting implementation. The tracer supports creating spans that are "attached" or "detached" from the context. By default, new spans are "attached" to the context in that they are From b868b5acb164f124aa4f75c64287e50408a10e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 27 Aug 2019 16:01:23 +0200 Subject: [PATCH 0051/1517] Requests integration (#94) Adds requests integration. Two commits that might be of special interest (see #94): * c66af2faa100b0f41afafdb6e4de9f4de4cc62bc requests test: Use actual Response object. Co-Authored-By: Allan Feldman <6374032+a-feld@users.noreply.github.com> * 1b90a0ddc286c936f7256b14e14ef36fd3c6a24f More tests, rename to http-requests to work around pylint bug (?) See previous CI failure for pylint issue: ************* Module ext/opentelemetry-ext-requests/src/__init__.py ext/opentelemetry-ext-requests/src/__init__.py:1:0: F0001: No module named ext/opentelemetry-ext-requests/src/__init__.py (fatal) It seems that pylint gets confused when there is more than one "requests" module?? --- .../README.rst | 30 ++++++ ext/opentelemetry-ext-http-requests/setup.cfg | 46 ++++++++ ext/opentelemetry-ext-http-requests/setup.py | 30 ++++++ .../ext/http_requests/__init__.py | 102 ++++++++++++++++++ .../ext/http_requests/version.py | 15 +++ .../tests/__init__.py | 0 .../tests/test_requests_integration.py | 89 +++++++++++++++ tox.ini | 13 ++- 8 files changed, 320 insertions(+), 5 deletions(-) create mode 100644 ext/opentelemetry-ext-http-requests/README.rst create mode 100644 ext/opentelemetry-ext-http-requests/setup.cfg create mode 100644 ext/opentelemetry-ext-http-requests/setup.py create mode 100644 ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py create mode 100644 ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py create mode 100644 ext/opentelemetry-ext-http-requests/tests/__init__.py create mode 100644 ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py diff --git a/ext/opentelemetry-ext-http-requests/README.rst b/ext/opentelemetry-ext-http-requests/README.rst new file mode 100644 index 0000000000..e0223572f3 --- /dev/null +++ b/ext/opentelemetry-ext-http-requests/README.rst @@ -0,0 +1,30 @@ +OpenTelemetry requests integration +================================== + +This library allows tracing HTTP requests made by the popular `requests <(https://2.python-requests.org//en/latest/>` library. + +Usage +----- + +.. code-block:: python + + import requests + import opentelemetry.ext.http_requests + from opentelemetry.trace import tracer + + opentelemetry.ext.http_requests.enable(tracer()) + response = requests.get(url='https://www.example.org/') + +Limitations +----------- + +Note that calls that do not use the higher-level APIs but use +:code:`requests.sessions.Session.send` (or an alias thereof) directly, are +currently not traced. If you find any other way to trigger an untraced HTTP +request, please report it via a GitHub issue with :code:`[requests: untraced +API]` in the title. + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg new file mode 100644 index 0000000000..01ee81fda1 --- /dev/null +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -0,0 +1,46 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-http-requests +description = OpenTelemetry requests integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-http-requests +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find: +install_requires = + opentelemetry-api >= 0.1.dev0 + requests ~= 2.0 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-http-requests/setup.py b/ext/opentelemetry-ext-http-requests/setup.py new file mode 100644 index 0000000000..be540c7f2b --- /dev/null +++ b/ext/opentelemetry-ext-http-requests/setup.py @@ -0,0 +1,30 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "ext", + "http_requests", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py new file mode 100644 index 0000000000..4d0d61d0a3 --- /dev/null +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -0,0 +1,102 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The opentelemetry-ext-requests package allows tracing HTTP requests made by the +popular requests library. +""" + +from urllib.parse import urlparse +import functools + +from requests.sessions import Session + + +# NOTE: Currently we force passing a tracer. But in turn, this forces the user +# to configure a SDK before enabling this integration. In turn, this means that +# if the SDK/tracer is already using `requests` they may, in theory, bypass our +# instrumentation when using `import from`, etc. (currently we only instrument +# a instance method so the probability for that is very low). +def enable(tracer): + """Enables tracing of all requests calls that go through + :code:`requests.session.Session.request` (this includes + :code:`requests.get`, etc.).""" + + # Since + # https://github.com/psf/requests/commit/d72d1162142d1bf8b1b5711c664fbbd674f349d1 + # (v0.7.0, Oct 23, 2011), get, post, etc are implemented via request which + # again, is implemented via Session.request (`Session` was named `session` + # before v1.0.0, Dec 17, 2012, see + # https://github.com/psf/requests/commit/4e5c4a6ab7bb0195dececdd19bb8505b872fe120) + + # Guard against double instrumentation + disable() + + wrapped = Session.request + + @functools.wraps(wrapped) + def instrumented_request(self, method, url, *args, **kwargs): + # TODO: Check if we are in an exporter, cf. OpenCensus + # execution_context.is_exporter() + + # See + # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#http-client + try: + parsed_url = urlparse(url) + except ValueError as exc: # Invalid URL + path = "".format(exc) + else: + if parsed_url is None: + path = "" + path = parsed_url.path + + with tracer.start_span(path) as span: + span.set_attribute("component", "http") + # TODO: The OTel spec says "SpanKind" MUST be "Client" but that + # seems to be a leftover, as Spans have no explicit field for + # kind. + span.set_attribute("http.method", method.upper()) + span.set_attribute("http.url", url) + + # TODO: Propagate the trace context via headers once we have a way + # to access propagators. + + result = wrapped(self, method, url, *args, **kwargs) # *** PROCEED + + span.set_attribute("http.status_code", result.status_code) + span.set_attribute("http.status_text", result.reason) + + return result + + # TODO: How to handle exceptions? Should we create events for them? Set + # certain attributes? + + instrumented_request.opentelemetry_ext_requests_applied = True + + Session.request = instrumented_request + + # TODO: We should also instrument requests.sessions.Session.send + # but to avoid doubled spans, we would need some context-local + # state (i.e., only create a Span if the current context's URL is + # different, then push the current URL, pop it afterwards) + + +def disable(): + """Disables instrumentation of :code:`requests` through this module. + + Note that this only works if no other module also patches requests.""" + + if getattr(Session.request, "opentelemetry_ext_requests_applied", False): + original = Session.request.__wrapped__ # pylint:disable=no-member + Session.request = original diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py new file mode 100644 index 0000000000..a457c2b665 --- /dev/null +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.1.dev0" diff --git a/ext/opentelemetry-ext-http-requests/tests/__init__.py b/ext/opentelemetry-ext-http-requests/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py new file mode 100644 index 0000000000..708505314d --- /dev/null +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -0,0 +1,89 @@ +import unittest +from unittest import mock +import sys + +import requests +import urllib3 + +import opentelemetry.ext.http_requests +from opentelemetry import trace + + +class TestRequestsIntegration(unittest.TestCase): + + # TODO: Copy & paste from test_wsgi_middleware + def setUp(self): + self.span_attrs = {} + self.tracer = trace.tracer() + self.span_context_manager = mock.MagicMock() + self.span = mock.create_autospec( + trace.Span, spec_set=True + ) + self.span_context_manager.__enter__.return_value = self.span + + def setspanattr(key, value): + self.assertIsInstance(key, str) + self.span_attrs[key] = value + + self.span.set_attribute = setspanattr + self.start_span_patcher = mock.patch.object( + self.tracer, + "start_span", + autospec=True, + spec_set=True, + return_value=self.span_context_manager + ) + self.start_span = self.start_span_patcher.start() + + mocked_response = requests.models.Response() + mocked_response.status_code = 200 + mocked_response.reason = "Roger that!" + self.send_patcher = mock.patch.object( + requests.Session, "send", autospec=True, spec_set=True, + return_value=mocked_response) + self.send = self.send_patcher.start() + + opentelemetry.ext.http_requests.enable(self.tracer) + + def tearDown(self): + opentelemetry.ext.http_requests.disable() + self.send_patcher.stop() + self.start_span_patcher.stop() + + def test_basic(self): + url = "https://www.example.org/foo/bar?x=y#top" + _response = requests.get(url=url) + self.assertEqual(1, len(self.send.call_args_list)) + self.tracer.start_span.assert_called_with("/foo/bar") + self.span_context_manager.__enter__.assert_called_with() + self.span_context_manager.__exit__.assert_called_with(None, None, None) + self.assertEqual(self.span_attrs, { + "component": "http", + "http.method": "GET", + "http.url": url, + "http.status_code": 200, + "http.status_text": "Roger that!" + }) + + def test_invalid_url(self): + url = "http://[::1/nope" + exception_type = requests.exceptions.InvalidURL + if (sys.version_info[:2] < (3, 5) + and tuple(map( + int, urllib3.__version__.split('.')[:2])) < (1, 25)): + exception_type = ValueError + + with self.assertRaises(exception_type): + _response = requests.post(url=url) + self.assertTrue(self.tracer.start_span.call_args[0][0].startswith( + " Date: Tue, 27 Aug 2019 19:49:39 +0300 Subject: [PATCH 0052/1517] Relax the pylint rule for constants (#108) These changes follow up the "Fix and improve tests for Python != 3.7" PR. --- .pylintrc | 2 +- opentelemetry-api/src/opentelemetry/context/__init__.py | 8 +++----- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/util.py | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.pylintrc b/.pylintrc index f0ae6e1ad8..4b33f1142e 100644 --- a/.pylintrc +++ b/.pylintrc @@ -323,7 +323,7 @@ class-naming-style=PascalCase #class-rgx= # Naming style matching correct constant names. -const-naming-style=UPPER_CASE +const-naming-style=any # Regular expression matching correct constant names. Overrides const-naming- # style. diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 1495f8828e..43cfdf30f5 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -145,13 +145,11 @@ async def main(): __all__ = ['Context'] -Context = ( # pylint: disable=invalid-name - None -) # type: typing.Optional[BaseRuntimeContext] +Context = None # type: typing.Optional[BaseRuntimeContext] try: from .async_context import AsyncRuntimeContext - Context = AsyncRuntimeContext() # pylint:disable=invalid-name + Context = AsyncRuntimeContext() except ImportError: from .thread_local_context import ThreadLocalRuntimeContext - Context = ThreadLocalRuntimeContext() # pylint:disable=invalid-name + Context = ThreadLocalRuntimeContext() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 03cf34c178..b99ce6208f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -346,4 +346,4 @@ def use_span(self, span: 'Span') -> typing.Iterator['Span']: span.end() -tracer = Tracer() # pylint: disable=invalid-name +tracer = Tracer() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util.py b/opentelemetry-sdk/src/opentelemetry/sdk/util.py index 9886206fb7..125a899f16 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util.py @@ -15,7 +15,7 @@ import time try: - time_ns = time.time_ns # pylint: disable=invalid-name + time_ns = time.time_ns # Python versions < 3.7 except AttributeError: def time_ns(): From abb5966a9031861b6c3edd70da6cb06b31c361ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 27 Aug 2019 18:51:21 +0200 Subject: [PATCH 0053/1517] Fix running tox on Windows. (#111) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f7feff5174..0b2e30c312 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ changedir = test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests commands_pre = - pip install -U pip setuptools wheel + python -m pip install -U pip setuptools wheel test: pip install -e {toxinidir}/opentelemetry-api test-sdk: pip install -e {toxinidir}/opentelemetry-sdk ext: pip install -e {toxinidir}/opentelemetry-api From 96e5bc1327ff9b3f0d72d484302d72810e607d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 27 Aug 2019 11:56:46 -0500 Subject: [PATCH 0054/1517] api: fix loader ignore_environment logic (#107) --- opentelemetry-api/src/opentelemetry/loader.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/loader.py b/opentelemetry-api/src/opentelemetry/loader.py index 78b7cc4f32..27f31584c2 100644 --- a/opentelemetry-api/src/opentelemetry/loader.py +++ b/opentelemetry-api/src/opentelemetry/loader.py @@ -139,16 +139,17 @@ def _try_load_configured_impl( configured, or a load error occurs, returns `None` """ implementation_modname = None - if sys.flags.ignore_environment: - return None - implementation_modname = os.getenv( - 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_' + api_type.__name__.upper()) - if implementation_modname: - return _try_load_impl_from_modname(implementation_modname, api_type) - implementation_modname = os.getenv( - 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT') - if implementation_modname: - return _try_load_impl_from_modname(implementation_modname, api_type) + if not sys.flags.ignore_environment: + implementation_modname = os.getenv( + 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_' + api_type.__name__.upper()) + if implementation_modname: + return _try_load_impl_from_modname(implementation_modname, + api_type) + implementation_modname = os.getenv( + 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT') + if implementation_modname: + return _try_load_impl_from_modname(implementation_modname, + api_type) if factory is not None: return _try_load_impl_from_callback(factory, api_type) if _DEFAULT_FACTORY is not None: From abe00f7ae9127fa3fa91c126bb837e3cb43d7b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Thu, 29 Aug 2019 10:54:42 -0500 Subject: [PATCH 0055/1517] span: implement update_name method (#112) --- opentelemetry-api/src/opentelemetry/trace/__init__.py | 10 ++++++++++ .../src/opentelemetry/sdk/trace/__init__.py | 3 +++ opentelemetry-sdk/tests/trace/test_trace.py | 3 +++ 3 files changed, 16 insertions(+) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 6336d676ab..810c8b78e2 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -132,6 +132,16 @@ def add_link(self: 'Span', `SpanContext` passed as argument. """ + def update_name(self, name: str) -> None: + """Updates the `Span` name. + + This will override the name provided via :func:`Tracer.create_span` + or :func:`Tracer.start_span`. + + Upon this update, any sampling behavior based on Span name will depend + on the implementation. + """ + class TraceOptions(int): """A bitmask that represents options specific to the trace. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index b99ce6208f..2d2b8b3c76 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -261,6 +261,9 @@ def end(self): if self.end_time is None: self.end_time = util.time_ns() + def update_name(self, name: str) -> None: + self.name = name + def generate_span_id(): """Get a new random span ID. diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index cd13d0fb70..f736a8152d 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -145,6 +145,9 @@ def test_span_members(self): root.add_link(other_context1) root.add_link(other_context2, {'name': 'neighbor'}) + root.update_name('toor') + self.assertEqual(root.name, 'toor') + # The public API does not expose getters. # Checks by accessing the span members directly From 12415f9a398f4cd4b7de8e33cc25832933d88375 Mon Sep 17 00:00:00 2001 From: Allan Feldman <6374032+a-feld@users.noreply.github.com> Date: Thu, 29 Aug 2019 08:55:37 -0700 Subject: [PATCH 0056/1517] Add initial DistributedContext implementation. (#92) --- .../distributedcontext/__init__.py | 120 ++++++++++++++++++ .../propagation/__init__.py | 18 +++ .../propagation/binaryformat.py | 61 +++++++++ .../propagation/httptextformat.py | 107 ++++++++++++++++ .../tests/distributedcontext/__init__.py | 13 ++ .../test_distributed_context.py | 108 ++++++++++++++++ .../sdk/distributedcontext/__init__.py | 67 ++++++++++ .../propagation/__init__.py | 0 .../tests/distributedcontext/__init__.py | 13 ++ .../test_distributed_context.py | 51 ++++++++ 10 files changed, 558 insertions(+) create mode 100644 opentelemetry-api/src/opentelemetry/distributedcontext/propagation/__init__.py create mode 100644 opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py create mode 100644 opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py create mode 100644 opentelemetry-api/tests/distributedcontext/__init__.py create mode 100644 opentelemetry-api/tests/distributedcontext/test_distributed_context.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/propagation/__init__.py create mode 100644 opentelemetry-sdk/tests/distributedcontext/__init__.py create mode 100644 opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index d853a7bcf6..859bb0cb4f 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -11,3 +11,123 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +from contextlib import contextmanager +import itertools +import string +import typing + +PRINTABLE = frozenset(itertools.chain( + string.ascii_letters, + string.digits, + string.punctuation, + " ", +)) + + +class EntryMetadata: + """A class representing metadata of a DistributedContext entry + + Args: + entry_ttl: The time to live (in service hops) of an entry. Must be + initially set to either :attr:`EntryMetadata.NO_PROPAGATION` + or :attr:`EntryMetadata.UNLIMITED_PROPAGATION`. + """ + + NO_PROPAGATION = 0 + UNLIMITED_PROPAGATION = -1 + + def __init__(self, entry_ttl: int) -> None: + self.entry_ttl = entry_ttl + + +class EntryKey(str): + """A class representing a key for a DistributedContext entry""" + + def __new__(cls, value: str) -> "EntryKey": + return cls.create(value) + + @staticmethod + def create(value: str) -> "EntryKey": + # pylint: disable=len-as-condition + if not 0 < len(value) <= 255 or any(c not in PRINTABLE for c in value): + raise ValueError("Invalid EntryKey", value) + + return typing.cast(EntryKey, value) + + +class EntryValue(str): + """A class representing the value of a DistributedContext entry""" + + def __new__(cls, value: str) -> "EntryValue": + return cls.create(value) + + @staticmethod + def create(value: str) -> "EntryValue": + if any(c not in PRINTABLE for c in value): + raise ValueError("Invalid EntryValue", value) + + return typing.cast(EntryValue, value) + + +class Entry: + def __init__( + self, + metadata: EntryMetadata, + key: EntryKey, + value: EntryValue, + ) -> None: + self.metadata = metadata + self.key = key + self.value = value + + +class DistributedContext: + """A container for distributed context entries""" + + def __init__(self, entries: typing.Iterable[Entry]) -> None: + self._container = {entry.key: entry for entry in entries} + + def get_entries(self) -> typing.Iterable[Entry]: + """Returns an immutable iterator to entries.""" + return self._container.values() + + def get_entry_value( + self, + key: EntryKey + ) -> typing.Optional[EntryValue]: + """Returns the entry associated with a key or None + + Args: + key: the key with which to perform a lookup + """ + if key in self._container: + return self._container[key].value + return None + + +class DistributedContextManager: + def get_current_context(self) -> typing.Optional[DistributedContext]: + """Gets the current DistributedContext. + + Returns: + A DistributedContext instance representing the current context. + """ + + @contextmanager # type: ignore + def use_context( + self, + context: DistributedContext, + ) -> typing.Iterator[DistributedContext]: + """Context manager for controlling a DistributedContext lifetime. + + Set the context as the active DistributedContext. + + On exiting, the context manager will restore the parent + DistributedContext. + + Args: + context: A DistributedContext instance to make current. + """ + # pylint: disable=no-self-use + yield context diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/__init__.py new file mode 100644 index 0000000000..c8706281ad --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .binaryformat import BinaryFormat +from .httptextformat import HTTPTextFormat + +__all__ = ["BinaryFormat", "HTTPTextFormat"] diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py new file mode 100644 index 0000000000..0441eac5e3 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py @@ -0,0 +1,61 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import typing + +from opentelemetry.distributedcontext import DistributedContext + + +class BinaryFormat(abc.ABC): + """API for serialization of span context into binary formats. + + This class provides an interface that enables converting span contexts + to and from a binary format. + """ + + @staticmethod + @abc.abstractmethod + def to_bytes(context: DistributedContext) -> bytes: + """Creates a byte representation of a DistributedContext. + + to_bytes should read values from a DistributedContext and return a data + format to represent it, in bytes. + + Args: + context: the DistributedContext to serialize + + Returns: + A bytes representation of the DistributedContext. + + """ + + @staticmethod + @abc.abstractmethod + def from_bytes( + byte_representation: bytes) -> typing.Optional[DistributedContext]: + """Return a DistributedContext that was represented by bytes. + + from_bytes should return back a DistributedContext that was constructed + from the data serialized in the byte_representation passed. If it is + not possible to read in a proper DistributedContext, return None. + + Args: + byte_representation: the bytes to deserialize + + Returns: + A bytes representation of the DistributedContext if it is valid. + Otherwise return None. + + """ diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py new file mode 100644 index 0000000000..3d11b7a352 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py @@ -0,0 +1,107 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import typing + +from opentelemetry.distributedcontext import DistributedContext + +Setter = typing.Callable[[object, str, str], None] +Getter = typing.Callable[[object, str], typing.List[str]] + + +class HTTPTextFormat(abc.ABC): + """API for propagation of span context via headers. + + This class provides an interface that enables extracting and injecting + span context into headers of HTTP requests. HTTP frameworks and clients + can integrate with HTTPTextFormat by providing the object containing the + headers, and a getter and setter function for the extraction and + injection of values, respectively. + + Example:: + + import flask + import requests + from opentelemetry.context.propagation import HTTPTextFormat + + PROPAGATOR = HTTPTextFormat() + + def get_header_from_flask_request(request, key): + return request.headers.get_all(key) + + def set_header_into_requests_request(request: requests.Request, + key: str, value: str): + request.headers[key] = value + + def example_route(): + distributed_context = PROPAGATOR.extract( + get_header_from_flask_request, + flask.request + ) + request_to_downstream = requests.Request( + "GET", "http://httpbin.org/get" + ) + PROPAGATOR.inject( + distributed_context, + set_header_into_requests_request, + request_to_downstream + ) + session = requests.Session() + session.send(request_to_downstream.prepare()) + + + .. _Propagation API Specification: + https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md + """ + @abc.abstractmethod + def extract(self, get_from_carrier: Getter, + carrier: object) -> DistributedContext: + """Create a DistributedContext from values in the carrier. + + The extract function should retrieve values from the carrier + object using get_from_carrier, and use values to populate a + DistributedContext value and return it. + + Args: + get_from_carrier: a function that can retrieve zero + or more values from the carrier. In the case that + the value does not exist, return an empty list. + carrier: and object which contains values that are + used to construct a DistributedContext. This object + must be paired with an appropriate get_from_carrier + which understands how to extract a value from it. + Returns: + A DistributedContext with configuration found in the carrier. + + """ + @abc.abstractmethod + def inject(self, context: DistributedContext, set_in_carrier: Setter, + carrier: object) -> None: + """Inject values from a DistributedContext into a carrier. + + inject enables the propagation of values into HTTP clients or + other objects which perform an HTTP request. Implementations + should use the set_in_carrier method to set values on the + carrier. + + Args: + context: The DistributedContext to read values from. + set_in_carrier: A setter function that can set values + on the carrier. + carrier: An object that a place to define HTTP headers. + Should be paired with set_in_carrier, which should + know how to set header values on the carrier. + + """ diff --git a/opentelemetry-api/tests/distributedcontext/__init__.py b/opentelemetry-api/tests/distributedcontext/__init__.py new file mode 100644 index 0000000000..d853a7bcf6 --- /dev/null +++ b/opentelemetry-api/tests/distributedcontext/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-api/tests/distributedcontext/test_distributed_context.py b/opentelemetry-api/tests/distributedcontext/test_distributed_context.py new file mode 100644 index 0000000000..be1aadac9a --- /dev/null +++ b/opentelemetry-api/tests/distributedcontext/test_distributed_context.py @@ -0,0 +1,108 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import distributedcontext + + +class TestEntryMetadata(unittest.TestCase): + def test_entry_ttl_no_propagation(self): + metadata = distributedcontext.EntryMetadata( + distributedcontext.EntryMetadata.NO_PROPAGATION, + ) + self.assertEqual(metadata.entry_ttl, 0) + + def test_entry_ttl_unlimited_propagation(self): + metadata = distributedcontext.EntryMetadata( + distributedcontext.EntryMetadata.UNLIMITED_PROPAGATION, + ) + self.assertEqual(metadata.entry_ttl, -1) + + +class TestEntryKey(unittest.TestCase): + def test_create_empty(self): + with self.assertRaises(ValueError): + distributedcontext.EntryKey.create("") + + def test_create_too_long(self): + with self.assertRaises(ValueError): + distributedcontext.EntryKey.create("a" * 256) + + def test_create_invalid_character(self): + with self.assertRaises(ValueError): + distributedcontext.EntryKey.create("\x00") + + def test_create_valid(self): + key = distributedcontext.EntryKey.create("ok") + self.assertEqual(key, "ok") + + def test_key_new(self): + key = distributedcontext.EntryKey("ok") + self.assertEqual(key, "ok") + + +class TestEntryValue(unittest.TestCase): + def test_create_invalid_character(self): + with self.assertRaises(ValueError): + distributedcontext.EntryValue.create("\x00") + + def test_create_valid(self): + key = distributedcontext.EntryValue.create("ok") + self.assertEqual(key, "ok") + + def test_key_new(self): + key = distributedcontext.EntryValue("ok") + self.assertEqual(key, "ok") + + +class TestDistributedContext(unittest.TestCase): + def setUp(self): + entry = self.entry = distributedcontext.Entry( + distributedcontext.EntryMetadata( + distributedcontext.EntryMetadata.NO_PROPAGATION, + ), + distributedcontext.EntryKey("key"), + distributedcontext.EntryValue("value"), + ) + self.context = distributedcontext.DistributedContext(( + entry, + )) + + def test_get_entries(self): + self.assertIn(self.entry, self.context.get_entries()) + + def test_get_entry_value_present(self): + value = self.context.get_entry_value( + self.entry.key, + ) + self.assertIs(value, self.entry.value) + + def test_get_entry_value_missing(self): + key = distributedcontext.EntryKey("missing") + value = self.context.get_entry_value(key) + self.assertIsNone(value) + + +class TestDistributedContextManager(unittest.TestCase): + def setUp(self): + self.manager = distributedcontext.DistributedContextManager() + + def test_get_current_context(self): + self.assertIsNone(self.manager.get_current_context()) + + def test_use_context(self): + expected = object() + with self.manager.use_context(expected) as output: + self.assertIs(output, expected) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py new file mode 100644 index 0000000000..fc449cfe2d --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py @@ -0,0 +1,67 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from contextlib import contextmanager +import typing + +from opentelemetry import distributedcontext as dctx_api +from opentelemetry.context import Context + + +class DistributedContextManager(dctx_api.DistributedContextManager): + """See `opentelemetry.distributedcontext.DistributedContextManager` + + Args: + name: The name of the context manager + """ + + def __init__(self, name: str = "") -> None: + if name: + slot_name = "DistributedContext.{}".format(name) + else: + slot_name = "DistributedContext" + + self._current_context = Context.register_slot(slot_name) + + def get_current_context( + self, + ) -> typing.Optional[dctx_api.DistributedContext]: + """Gets the current DistributedContext. + + Returns: + A DistributedContext instance representing the current context. + """ + return self._current_context.get() + + @contextmanager + def use_context( + self, + context: dctx_api.DistributedContext, + ) -> typing.Iterator[dctx_api.DistributedContext]: + """Context manager for controlling a DistributedContext lifetime. + + Set the context as the active DistributedContext. + + On exiting, the context manager will restore the parent + DistributedContext. + + Args: + context: A DistributedContext instance to make current. + """ + snapshot = self._current_context.get() + self._current_context.set(context) + try: + yield context + finally: + self._current_context.set(snapshot) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/propagation/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/propagation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-sdk/tests/distributedcontext/__init__.py b/opentelemetry-sdk/tests/distributedcontext/__init__.py new file mode 100644 index 0000000000..d853a7bcf6 --- /dev/null +++ b/opentelemetry-sdk/tests/distributedcontext/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py b/opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py new file mode 100644 index 0000000000..fedcf7c9dd --- /dev/null +++ b/opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py @@ -0,0 +1,51 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import distributedcontext as dctx_api +from opentelemetry.sdk import distributedcontext + + +class TestDistributedContextManager(unittest.TestCase): + def setUp(self): + self.manager = distributedcontext.DistributedContextManager() + + def test_use_context(self): + # Context is None initially + self.assertIsNone(self.manager.get_current_context()) + + # Start initial context + dctx = dctx_api.DistributedContext(()) + with self.manager.use_context(dctx) as current: + self.assertIs(current, dctx) + self.assertIs( + self.manager.get_current_context(), + dctx, + ) + + # Context is overridden + nested_dctx = dctx_api.DistributedContext(()) + with self.manager.use_context(nested_dctx) as current: + self.assertIs(current, nested_dctx) + self.assertIs( + self.manager.get_current_context(), + nested_dctx, + ) + + # Context is restored + self.assertIs( + self.manager.get_current_context(), + dctx, + ) From 9aba63085803b279c2774234c7617e1a62e131be Mon Sep 17 00:00:00 2001 From: Allan Feldman <6374032+a-feld@users.noreply.github.com> Date: Thu, 29 Aug 2019 10:20:42 -0700 Subject: [PATCH 0057/1517] Add initial black formatting (#104) Closes: #88 --- .flake8 | 2 + .isort.cfg | 7 +- .pylintrc | 5 +- docs/conf.py | 37 ++--- ext/opentelemetry-ext-http-requests/setup.py | 8 +- .../ext/http_requests/__init__.py | 2 +- .../tests/test_requests_integration.py | 59 ++++---- ext/opentelemetry-ext-wsgi/setup.py | 8 +- .../src/opentelemetry/ext/wsgi/__init__.py | 14 +- .../tests/test_wsgi_middleware.py | 21 ++- opentelemetry-api/setup.py | 15 +- .../src/opentelemetry/context/__init__.py | 4 +- .../opentelemetry/context/async_context.py | 8 +- .../src/opentelemetry/context/base_context.py | 38 +++-- .../context/propagation/binaryformat.py | 2 + .../context/propagation/httptextformat.py | 12 +- .../context/thread_local_context.py | 6 +- .../distributedcontext/__init__.py | 26 ++-- .../propagation/binaryformat.py | 3 +- .../propagation/httptextformat.py | 15 +- opentelemetry-api/src/opentelemetry/loader.py | 49 +++--- .../src/opentelemetry/resources/__init__.py | 3 + .../src/opentelemetry/trace/__init__.py | 104 +++++++------ .../test_distributed_context.py | 14 +- opentelemetry-api/tests/test_loader.py | 19 ++- .../tests/trace/test_defaultspan.py | 6 +- opentelemetry-sdk/setup.py | 20 +-- .../src/opentelemetry/sdk/__init__.py | 8 +- .../sdk/context/propagation/b3_format.py | 47 ++++-- .../sdk/distributedcontext/__init__.py | 7 +- .../src/opentelemetry/sdk/trace/__init__.py | 117 +++++++------- .../src/opentelemetry/sdk/util.py | 1 + .../context/propagation/test_b3_format.py | 59 ++++---- .../test_distributed_context.py | 15 +- .../tests/resources/test_init.py | 19 +-- opentelemetry-sdk/tests/trace/test_trace.py | 143 ++++++++++-------- pyproject.toml | 2 + tox.ini | 2 + 38 files changed, 479 insertions(+), 448 deletions(-) create mode 100644 .flake8 create mode 100644 pyproject.toml diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..5384053b3b --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore = E501,W503,E203 diff --git a/.isort.cfg b/.isort.cfg index d3e7dbf03d..6bde062b5c 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,3 +1,6 @@ [settings] -force_single_line=True -from_first=True +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True +line_length=79 diff --git a/.pylintrc b/.pylintrc index 4b33f1142e..8130305d70 100644 --- a/.pylintrc +++ b/.pylintrc @@ -65,7 +65,10 @@ disable=missing-docstring, too-few-public-methods, # Might be good to re-enable this later. too-many-instance-attributes, too-many-arguments, - ungrouped-imports # Leave this up to isort + ungrouped-imports, # Leave this up to isort + wrong-import-order, # Leave this up to isort + bad-continuation, # Leave this up to black + line-too-long # Leave this up to black # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/docs/conf.py b/docs/conf.py index c4fee51f31..d719ebe931 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,38 +12,39 @@ import os import sys -sys.path.insert(0, os.path.abspath('../opentelemetry-api/src/')) + +sys.path.insert(0, os.path.abspath("../opentelemetry-api/src/")) # -- Project information ----------------------------------------------------- -project = 'OpenTelemetry' -copyright = '2019, OpenTelemetry Authors' -author = 'OpenTelemetry Authors' +project = "OpenTelemetry" +copyright = "2019, OpenTelemetry Authors" +author = "OpenTelemetry Authors" # -- General configuration --------------------------------------------------- # Easy automatic cross-references for `code in backticks` -default_role = 'any' +default_role = "any" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ # API doc generation - 'sphinx.ext.autodoc', + "sphinx.ext.autodoc", # Support for google-style docstrings - 'sphinx.ext.napoleon', + "sphinx.ext.napoleon", # Infer types from hints instead of docstrings - 'sphinx_autodoc_typehints', + "sphinx_autodoc_typehints", # Add links to source from generated docs - 'sphinx.ext.viewcode', + "sphinx.ext.viewcode", # Link to other sphinx docs - 'sphinx.ext.intersphinx', + "sphinx.ext.intersphinx", ] -intersphinx_mapping = {'python': ('https://docs.python.org/3/', None)} +intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} # http://www.sphinx-doc.org/en/master/config.html#confval-nitpicky # Sphinx will warn about all references where the target cannot be found. @@ -51,18 +52,18 @@ nitpick_ignore = [] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] autodoc_default_options = { - 'members': True, - 'undoc-members': True, - 'show-inheritance': True, - 'member-order': 'bysource' + "members": True, + "undoc-members": True, + "show-inheritance": True, + "member-order": "bysource", } # -- Options for HTML output ------------------------------------------------- @@ -70,7 +71,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/ext/opentelemetry-ext-http-requests/setup.py b/ext/opentelemetry-ext-http-requests/setup.py index be540c7f2b..2f757e6cde 100644 --- a/ext/opentelemetry-ext-http-requests/setup.py +++ b/ext/opentelemetry-ext-http-requests/setup.py @@ -12,16 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. import os + import setuptools BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "ext", - "http_requests", - "version.py", + BASE_DIR, "src", "opentelemetry", "ext", "http_requests", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index 4d0d61d0a3..2689b8ea2d 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -17,8 +17,8 @@ popular requests library. """ -from urllib.parse import urlparse import functools +from urllib.parse import urlparse from requests.sessions import Session diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index 708505314d..fa7741695c 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -1,11 +1,10 @@ +import sys import unittest from unittest import mock -import sys +import opentelemetry.ext.http_requests import requests import urllib3 - -import opentelemetry.ext.http_requests from opentelemetry import trace @@ -16,9 +15,7 @@ def setUp(self): self.span_attrs = {} self.tracer = trace.tracer() self.span_context_manager = mock.MagicMock() - self.span = mock.create_autospec( - trace.Span, spec_set=True - ) + self.span = mock.create_autospec(trace.Span, spec_set=True) self.span_context_manager.__enter__.return_value = self.span def setspanattr(key, value): @@ -31,7 +28,7 @@ def setspanattr(key, value): "start_span", autospec=True, spec_set=True, - return_value=self.span_context_manager + return_value=self.span_context_manager, ) self.start_span = self.start_span_patcher.start() @@ -39,8 +36,12 @@ def setspanattr(key, value): mocked_response.status_code = 200 mocked_response.reason = "Roger that!" self.send_patcher = mock.patch.object( - requests.Session, "send", autospec=True, spec_set=True, - return_value=mocked_response) + requests.Session, + "send", + autospec=True, + spec_set=True, + return_value=mocked_response, + ) self.send = self.send_patcher.start() opentelemetry.ext.http_requests.enable(self.tracer) @@ -57,33 +58,39 @@ def test_basic(self): self.tracer.start_span.assert_called_with("/foo/bar") self.span_context_manager.__enter__.assert_called_with() self.span_context_manager.__exit__.assert_called_with(None, None, None) - self.assertEqual(self.span_attrs, { - "component": "http", - "http.method": "GET", - "http.url": url, - "http.status_code": 200, - "http.status_text": "Roger that!" - }) + self.assertEqual( + self.span_attrs, + { + "component": "http", + "http.method": "GET", + "http.url": url, + "http.status_code": 200, + "http.status_text": "Roger that!", + }, + ) def test_invalid_url(self): url = "http://[::1/nope" exception_type = requests.exceptions.InvalidURL - if (sys.version_info[:2] < (3, 5) - and tuple(map( - int, urllib3.__version__.split('.')[:2])) < (1, 25)): + if sys.version_info[:2] < (3, 5) and tuple( + map(int, urllib3.__version__.split(".")[:2]) + ) < (1, 25): exception_type = ValueError with self.assertRaises(exception_type): _response = requests.post(url=url) - self.assertTrue(self.tracer.start_span.call_args[0][0].startswith( - " None: self.contextvar.set(self.default()) - def get(self) -> 'object': + def get(self) -> "object": try: return self.contextvar.get() except LookupError: @@ -39,5 +39,5 @@ def get(self) -> 'object': self.set(value) return value - def set(self, value: 'object') -> None: + def set(self, value: "object") -> None: self.contextvar.set(value) diff --git a/opentelemetry-api/src/opentelemetry/context/base_context.py b/opentelemetry-api/src/opentelemetry/context/base_context.py index 5c1ffb4a8e..f1e37aa91f 100644 --- a/opentelemetry-api/src/opentelemetry/context/base_context.py +++ b/opentelemetry-api/src/opentelemetry/context/base_context.py @@ -16,7 +16,7 @@ import typing -def wrap_callable(target: 'object') -> typing.Callable[[], object]: +def wrap_callable(target: "object") -> typing.Callable[[], object]: if callable(target): return target return lambda: target @@ -24,16 +24,16 @@ def wrap_callable(target: 'object') -> typing.Callable[[], object]: class BaseRuntimeContext: class Slot: - def __init__(self, name: str, default: 'object'): + def __init__(self, name: str, default: "object"): raise NotImplementedError def clear(self) -> None: raise NotImplementedError - def get(self) -> 'object': + def get(self) -> "object": raise NotImplementedError - def set(self, value: 'object') -> None: + def set(self, value: "object") -> None: raise NotImplementedError _lock = threading.Lock() @@ -49,10 +49,8 @@ def clear(cls) -> None: @classmethod def register_slot( - cls, - name: str, - default: 'object' = None, - ) -> 'BaseRuntimeContext.Slot': + cls, name: str, default: "object" = None + ) -> "BaseRuntimeContext.Slot": """Register a context slot with an optional default value. :type name: str @@ -68,52 +66,50 @@ def register_slot( cls._slots[name] = cls.Slot(name, default) return cls._slots[name] - def apply(self, snapshot: typing.Dict[str, 'object']) -> None: + def apply(self, snapshot: typing.Dict[str, "object"]) -> None: """Set the current context from a given snapshot dictionary""" for name in snapshot: setattr(self, name, snapshot[name]) - def snapshot(self) -> typing.Dict[str, 'object']: + def snapshot(self) -> typing.Dict[str, "object"]: """Return a dictionary of current slots by reference.""" keys = self._slots.keys() return dict((n, self._slots[n].get()) for n in keys) def __repr__(self) -> str: - return '{}({})'.format(type(self).__name__, self.snapshot()) + return "{}({})".format(type(self).__name__, self.snapshot()) - def __getattr__(self, name: str) -> 'object': + def __getattr__(self, name: str) -> "object": if name not in self._slots: self.register_slot(name, None) slot = self._slots[name] return slot.get() - def __setattr__(self, name: str, value: 'object') -> None: + def __setattr__(self, name: str, value: "object") -> None: if name not in self._slots: self.register_slot(name, None) slot = self._slots[name] slot.set(value) - def __getitem__(self, name: str) -> 'object': + def __getitem__(self, name: str) -> "object": return self.__getattr__(name) - def __setitem__(self, name: str, value: 'object') -> None: + def __setitem__(self, name: str, value: "object") -> None: self.__setattr__(name, value) def with_current_context( - self, - func: typing.Callable[..., 'object'], - ) -> typing.Callable[..., 'object']: + self, func: typing.Callable[..., "object"] + ) -> typing.Callable[..., "object"]: """Capture the current context and apply it to the provided func. """ caller_context = self.snapshot() def call_with_current_context( - *args: 'object', - **kwargs: 'object' - ) -> 'object': + *args: "object", **kwargs: "object" + ) -> "object": try: backup_context = self.snapshot() self.apply(caller_context) diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py index f05ef69972..7f1a65882f 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py @@ -24,6 +24,7 @@ class BinaryFormat(abc.ABC): This class provides an interface that enables converting span contexts to and from a binary format. """ + @staticmethod @abc.abstractmethod def to_bytes(context: SpanContext) -> bytes: @@ -39,6 +40,7 @@ def to_bytes(context: SpanContext) -> bytes: A bytes representation of the SpanContext. """ + @staticmethod @abc.abstractmethod def from_bytes(byte_representation: bytes) -> typing.Optional[SpanContext]: diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py index 860498fe35..f3823a86d1 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py @@ -67,9 +67,11 @@ def example_route(): .. _Propagation API Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md """ + @abc.abstractmethod - def extract(self, get_from_carrier: Getter, - carrier: object) -> SpanContext: + def extract( + self, get_from_carrier: Getter, carrier: object + ) -> SpanContext: """Create a SpanContext from values in the carrier. The extract function should retrieve values from the carrier @@ -88,9 +90,11 @@ def extract(self, get_from_carrier: Getter, A SpanContext with configuration found in the carrier. """ + @abc.abstractmethod - def inject(self, context: SpanContext, set_in_carrier: Setter, - carrier: object) -> None: + def inject( + self, context: SpanContext, set_in_carrier: Setter, carrier: object + ) -> None: """Inject values from a SpanContext into a carrier. inject enables the propagation of values into HTTP clients or diff --git a/opentelemetry-api/src/opentelemetry/context/thread_local_context.py b/opentelemetry-api/src/opentelemetry/context/thread_local_context.py index 8313fb57e8..b60914f846 100644 --- a/opentelemetry-api/src/opentelemetry/context/thread_local_context.py +++ b/opentelemetry-api/src/opentelemetry/context/thread_local_context.py @@ -22,7 +22,7 @@ class ThreadLocalRuntimeContext(base_context.BaseRuntimeContext): class Slot(base_context.BaseRuntimeContext.Slot): _thread_local = threading.local() - def __init__(self, name: str, default: 'object'): + def __init__(self, name: str, default: "object"): # pylint: disable=super-init-not-called self.name = name self.default = base_context.wrap_callable( @@ -32,7 +32,7 @@ def __init__(self, name: str, default: 'object'): def clear(self) -> None: setattr(self._thread_local, self.name, self.default()) - def get(self) -> 'object': + def get(self) -> "object": try: got = getattr(self._thread_local, self.name) # type: object return got @@ -41,5 +41,5 @@ def get(self) -> 'object': self.set(value) return value - def set(self, value: 'object') -> None: + def set(self, value: "object") -> None: setattr(self._thread_local, self.name, value) diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index 859bb0cb4f..38ef3739b9 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -12,17 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from contextlib import contextmanager import itertools import string import typing +from contextlib import contextmanager -PRINTABLE = frozenset(itertools.chain( - string.ascii_letters, - string.digits, - string.punctuation, - " ", -)) +PRINTABLE = frozenset( + itertools.chain( + string.ascii_letters, string.digits, string.punctuation, " " + ) +) class EntryMetadata: @@ -72,10 +71,7 @@ def create(value: str) -> "EntryValue": class Entry: def __init__( - self, - metadata: EntryMetadata, - key: EntryKey, - value: EntryValue, + self, metadata: EntryMetadata, key: EntryKey, value: EntryValue ) -> None: self.metadata = metadata self.key = key @@ -92,10 +88,7 @@ def get_entries(self) -> typing.Iterable[Entry]: """Returns an immutable iterator to entries.""" return self._container.values() - def get_entry_value( - self, - key: EntryKey - ) -> typing.Optional[EntryValue]: + def get_entry_value(self, key: EntryKey) -> typing.Optional[EntryValue]: """Returns the entry associated with a key or None Args: @@ -116,8 +109,7 @@ def get_current_context(self) -> typing.Optional[DistributedContext]: @contextmanager # type: ignore def use_context( - self, - context: DistributedContext, + self, context: DistributedContext ) -> typing.Iterator[DistributedContext]: """Context manager for controlling a DistributedContext lifetime. diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py index 0441eac5e3..15f8cfdf63 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py @@ -44,7 +44,8 @@ def to_bytes(context: DistributedContext) -> bytes: @staticmethod @abc.abstractmethod def from_bytes( - byte_representation: bytes) -> typing.Optional[DistributedContext]: + byte_representation: bytes + ) -> typing.Optional[DistributedContext]: """Return a DistributedContext that was represented by bytes. from_bytes should return back a DistributedContext that was constructed diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py index 3d11b7a352..3e2c186283 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py @@ -65,9 +65,11 @@ def example_route(): .. _Propagation API Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md """ + @abc.abstractmethod - def extract(self, get_from_carrier: Getter, - carrier: object) -> DistributedContext: + def extract( + self, get_from_carrier: Getter, carrier: object + ) -> DistributedContext: """Create a DistributedContext from values in the carrier. The extract function should retrieve values from the carrier @@ -86,9 +88,14 @@ def extract(self, get_from_carrier: Getter, A DistributedContext with configuration found in the carrier. """ + @abc.abstractmethod - def inject(self, context: DistributedContext, set_in_carrier: Setter, - carrier: object) -> None: + def inject( + self, + context: DistributedContext, + set_in_carrier: Setter, + carrier: object, + ) -> None: """Inject values from a DistributedContext into a carrier. inject enables the propagation of values into HTTP clients or diff --git a/opentelemetry-api/src/opentelemetry/loader.py b/opentelemetry-api/src/opentelemetry/loader.py index 27f31584c2..0781ce15b7 100644 --- a/opentelemetry-api/src/opentelemetry/loader.py +++ b/opentelemetry-api/src/opentelemetry/loader.py @@ -61,15 +61,12 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: means that the Python interpreter was invoked with the ``-E`` or ``-I`` flag). """ -from typing import Callable -from typing import Optional -from typing import Type -from typing import TypeVar import importlib import os import sys +from typing import Callable, Optional, Type, TypeVar -_T = TypeVar('_T') +_T = TypeVar("_T") # "Untrusted" because this is usually user-provided and we don't trust the user # to really return a _T: by using object, mypy forces us to check/cast @@ -87,7 +84,8 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: def _try_load_impl_from_modname( - implementation_modname: str, api_type: Type[_T]) -> Optional[_T]: + implementation_modname: str, api_type: Type[_T] +) -> Optional[_T]: try: implementation_mod = importlib.import_module(implementation_modname) except (ImportError, SyntaxError): @@ -98,15 +96,15 @@ def _try_load_impl_from_modname( def _try_load_impl_from_mod( - implementation_mod: object, api_type: Type[_T]) -> Optional[_T]: + implementation_mod: object, api_type: Type[_T] +) -> Optional[_T]: try: # Note: We use such a long name to avoid calling a function that is not # intended for this API. implementation_fn = getattr( - implementation_mod, - 'get_opentelemetry_implementation' + implementation_mod, "get_opentelemetry_implementation" ) # type: _UntrustedImplFactory[_T] except AttributeError: # TODO Log/warn @@ -116,9 +114,8 @@ def _try_load_impl_from_mod( def _try_load_impl_from_callback( - implementation_fn: _UntrustedImplFactory[_T], - api_type: Type[_T] - ) -> Optional[_T]: + implementation_fn: _UntrustedImplFactory[_T], api_type: Type[_T] +) -> Optional[_T]: result = implementation_fn(api_type) if result is None: return None @@ -133,23 +130,27 @@ def _try_load_impl_from_callback( def _try_load_configured_impl( - api_type: Type[_T], factory: Optional[_UntrustedImplFactory[_T]] - ) -> Optional[_T]: + api_type: Type[_T], factory: Optional[_UntrustedImplFactory[_T]] +) -> Optional[_T]: """Attempts to find any specially configured implementation. If none is configured, or a load error occurs, returns `None` """ implementation_modname = None if not sys.flags.ignore_environment: implementation_modname = os.getenv( - 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_' + api_type.__name__.upper()) + "OPENTELEMETRY_PYTHON_IMPLEMENTATION_" + api_type.__name__.upper() + ) if implementation_modname: - return _try_load_impl_from_modname(implementation_modname, - api_type) + return _try_load_impl_from_modname( + implementation_modname, api_type + ) implementation_modname = os.getenv( - 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT') + "OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT" + ) if implementation_modname: - return _try_load_impl_from_modname(implementation_modname, - api_type) + return _try_load_impl_from_modname( + implementation_modname, api_type + ) if factory is not None: return _try_load_impl_from_callback(factory, api_type) if _DEFAULT_FACTORY is not None: @@ -158,8 +159,9 @@ def _try_load_configured_impl( # Public to other opentelemetry-api modules -def _load_impl(api_type: Type[_T], - factory: Optional[Callable[[Type[_T]], Optional[_T]]]) -> _T: +def _load_impl( + api_type: Type[_T], factory: Optional[Callable[[Type[_T]], Optional[_T]]] +) -> _T: """Tries to load a configured implementation, if unsuccessful, returns a fast no-op implemenation that is always available. """ @@ -171,7 +173,8 @@ def _load_impl(api_type: Type[_T], def set_preferred_default_implementation( - implementation_factory: _UntrustedImplFactory[_T]) -> None: + implementation_factory: _UntrustedImplFactory[_T] +) -> None: """Sets a factory function that may be called for any implementation object. See the :ref:`module docs ` for more details.""" global _DEFAULT_FACTORY # pylint:disable=global-statement diff --git a/opentelemetry-api/src/opentelemetry/resources/__init__.py b/opentelemetry-api/src/opentelemetry/resources/__init__.py index 0d1e34dddb..d6a6eb64a2 100644 --- a/opentelemetry-api/src/opentelemetry/resources/__init__.py +++ b/opentelemetry-api/src/opentelemetry/resources/__init__.py @@ -18,6 +18,7 @@ class Resource(abc.ABC): """The interface that resources must implement.""" + @staticmethod @abc.abstractmethod def create(labels: typing.Dict[str, str]) -> "Resource": @@ -30,6 +31,7 @@ def create(labels: typing.Dict[str, str]) -> "Resource": The resource with the labels in question """ + @property @abc.abstractmethod def labels(self) -> typing.Dict[str, str]: @@ -39,6 +41,7 @@ def labels(self) -> typing.Dict[str, str]: A dictionary with the labels of the resource """ + @abc.abstractmethod def merge(self, other: typing.Optional["Resource"]) -> "Resource": """Return a resource with the union of labels for both resources. diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 810c8b78e2..dc57358183 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -61,14 +61,13 @@ .. versionadded:: 0.1.0 """ -from contextlib import contextmanager import typing +from contextlib import contextmanager -from opentelemetry import loader -from opentelemetry import types +from opentelemetry import loader, types # TODO: quarantine -ParentSpan = typing.Optional[typing.Union['Span', 'SpanContext']] +ParentSpan = typing.Optional[typing.Union["Span", "SpanContext"]] class Span: @@ -93,7 +92,7 @@ def end(self) -> None: implementations are free to ignore or raise on further calls. """ - def get_context(self) -> 'SpanContext': + def get_context(self) -> "SpanContext": """Gets the span's SpanContext. Get an immutable, serializable identifier for this span that can be @@ -103,29 +102,28 @@ def get_context(self) -> 'SpanContext': A :class:`.SpanContext` with a copy of this span's immutable state. """ - def set_attribute(self: 'Span', - key: str, - value: types.AttributeValue, - ) -> None: + def set_attribute( + self: "Span", key: str, value: types.AttributeValue + ) -> None: """Sets an Attribute. Sets a single Attribute with the key and value passed as arguments. """ - def add_event(self: 'Span', - name: str, - attributes: types.Attributes = None, - ) -> None: + def add_event( + self: "Span", name: str, attributes: types.Attributes = None + ) -> None: """Adds an Event. Adds a single Event with the name and, optionally, attributes passed as arguments. """ - def add_link(self: 'Span', - link_target_context: 'SpanContext', - attributes: types.Attributes = None, - ) -> None: + def add_link( + self: "Span", + link_target_context: "SpanContext", + attributes: types.Attributes = None, + ) -> None: """Adds a Link to another span. Adds a single Link from this Span to another Span identified by the @@ -154,11 +152,12 @@ class TraceOptions(int): .. _W3C Trace Context - Traceparent: https://www.w3.org/TR/trace-context/#trace-flags """ + DEFAULT = 0x00 RECORDED = 0x01 @classmethod - def get_default(cls) -> 'TraceOptions': + def get_default(cls) -> "TraceOptions": return cls(cls.DEFAULT) @@ -177,7 +176,7 @@ class TraceState(typing.Dict[str, str]): """ @classmethod - def get_default(cls) -> 'TraceState': + def get_default(cls) -> "TraceState": return cls() @@ -185,11 +184,11 @@ def get_default(cls) -> 'TraceState': def format_trace_id(trace_id: int) -> str: - return '0x{:032x}'.format(trace_id) + return "0x{:032x}".format(trace_id) def format_span_id(span_id: int) -> str: - return '0x{:016x}'.format(span_id) + return "0x{:016x}".format(span_id) class SpanContext: @@ -205,12 +204,13 @@ class SpanContext: state: Tracing-system-specific info to propagate. """ - def __init__(self, - trace_id: int, - span_id: int, - trace_options: 'TraceOptions' = None, - trace_state: 'TraceState' = None - ) -> None: + def __init__( + self, + trace_id: int, + span_id: int, + trace_options: "TraceOptions" = None, + trace_state: "TraceState" = None, + ) -> None: if trace_options is None: trace_options = DEFAULT_TRACE_OPTIONS if trace_state is None: @@ -221,12 +221,11 @@ def __init__(self, self.trace_state = trace_state def __repr__(self) -> str: - return ("{}(trace_id={}, span_id={})" - .format( - type(self).__name__, - format_trace_id(self.trace_id), - format_span_id(self.span_id) - )) + return "{}(trace_id={}, span_id={})".format( + type(self).__name__, + format_trace_id(self.trace_id), + format_span_id(self.span_id), + ) def is_valid(self) -> bool: """Get whether this `SpanContext` is valid. @@ -237,8 +236,10 @@ def is_valid(self) -> bool: Returns: True if the `SpanContext` is valid, false otherwise. """ - return (self.trace_id != INVALID_TRACE_ID and - self.span_id != INVALID_SPAN_ID) + return ( + self.trace_id != INVALID_TRACE_ID + and self.span_id != INVALID_SPAN_ID + ) class DefaultSpan(Span): @@ -246,17 +247,22 @@ class DefaultSpan(Span): All operations are no-op except context propagation. """ - def __init__(self, context: 'SpanContext') -> None: + + def __init__(self, context: "SpanContext") -> None: self._context = context - def get_context(self) -> 'SpanContext': + def get_context(self) -> "SpanContext": return self._context INVALID_SPAN_ID = 0x0000000000000000 INVALID_TRACE_ID = 0x00000000000000000000000000000000 -INVALID_SPAN_CONTEXT = SpanContext(INVALID_TRACE_ID, INVALID_SPAN_ID, - DEFAULT_TRACE_OPTIONS, DEFAULT_TRACE_STATE) +INVALID_SPAN_CONTEXT = SpanContext( + INVALID_TRACE_ID, + INVALID_SPAN_ID, + DEFAULT_TRACE_OPTIONS, + DEFAULT_TRACE_STATE, +) INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) @@ -271,7 +277,7 @@ class Tracer: # This is the default behavior when creating spans. CURRENT_SPAN = Span() - def get_current_span(self) -> 'Span': + def get_current_span(self) -> "Span": """Gets the currently active span from the context. If there is no current span, return a placeholder span with an invalid @@ -285,10 +291,9 @@ def get_current_span(self) -> 'Span': return INVALID_SPAN @contextmanager # type: ignore - def start_span(self, - name: str, - parent: ParentSpan = CURRENT_SPAN - ) -> typing.Iterator['Span']: + def start_span( + self, name: str, parent: ParentSpan = CURRENT_SPAN + ) -> typing.Iterator["Span"]: """Context manager for span creation. Create a new span. Start the span and set it as the current span in @@ -334,10 +339,9 @@ def start_span(self, # pylint: disable=unused-argument,no-self-use yield INVALID_SPAN - def create_span(self, - name: str, - parent: ParentSpan = CURRENT_SPAN - ) -> 'Span': + def create_span( + self, name: str, parent: ParentSpan = CURRENT_SPAN + ) -> "Span": """Creates a span. Creating the span does not start it, and should not affect the tracer's @@ -372,7 +376,7 @@ def create_span(self, return INVALID_SPAN @contextmanager # type: ignore - def use_span(self, span: 'Span') -> typing.Iterator[None]: + def use_span(self, span: "Span") -> typing.Iterator[None]: """Context manager for controlling a span's lifetime. Start the given span and set it as the current span in this tracer's @@ -415,7 +419,7 @@ def tracer() -> Tracer: def set_preferred_tracer_implementation( - factory: ImplementationFactory + factory: ImplementationFactory ) -> None: """Set the factory to be used to create the tracer. diff --git a/opentelemetry-api/tests/distributedcontext/test_distributed_context.py b/opentelemetry-api/tests/distributedcontext/test_distributed_context.py index be1aadac9a..67a6004839 100644 --- a/opentelemetry-api/tests/distributedcontext/test_distributed_context.py +++ b/opentelemetry-api/tests/distributedcontext/test_distributed_context.py @@ -20,13 +20,13 @@ class TestEntryMetadata(unittest.TestCase): def test_entry_ttl_no_propagation(self): metadata = distributedcontext.EntryMetadata( - distributedcontext.EntryMetadata.NO_PROPAGATION, + distributedcontext.EntryMetadata.NO_PROPAGATION ) self.assertEqual(metadata.entry_ttl, 0) def test_entry_ttl_unlimited_propagation(self): metadata = distributedcontext.EntryMetadata( - distributedcontext.EntryMetadata.UNLIMITED_PROPAGATION, + distributedcontext.EntryMetadata.UNLIMITED_PROPAGATION ) self.assertEqual(metadata.entry_ttl, -1) @@ -71,22 +71,18 @@ class TestDistributedContext(unittest.TestCase): def setUp(self): entry = self.entry = distributedcontext.Entry( distributedcontext.EntryMetadata( - distributedcontext.EntryMetadata.NO_PROPAGATION, + distributedcontext.EntryMetadata.NO_PROPAGATION ), distributedcontext.EntryKey("key"), distributedcontext.EntryValue("value"), ) - self.context = distributedcontext.DistributedContext(( - entry, - )) + self.context = distributedcontext.DistributedContext((entry,)) def test_get_entries(self): self.assertIn(self.entry, self.context.get_entries()) def test_get_entry_value_present(self): - value = self.context.get_entry_value( - self.entry.key, - ) + value = self.context.get_entry_value(self.entry.key) self.assertIs(value, self.entry.value) def test_get_entry_value_missing(self): diff --git a/opentelemetry-api/tests/test_loader.py b/opentelemetry-api/tests/test_loader.py index 68809468f5..9123154b41 100644 --- a/opentelemetry-api/tests/test_loader.py +++ b/opentelemetry-api/tests/test_loader.py @@ -36,8 +36,8 @@ def get_opentelemetry_implementation(type_): # pylint:disable=redefined-outer-name,protected-access,unidiomatic-typecheck -class TestLoader(unittest.TestCase): +class TestLoader(unittest.TestCase): def setUp(self): reload(loader) reload(trace) @@ -52,7 +52,8 @@ def test_get_default(self): def test_preferred_impl(self): trace.set_preferred_tracer_implementation( - get_opentelemetry_implementation) + get_opentelemetry_implementation + ) tracer = trace.tracer() self.assertIs(tracer, DUMMY_TRACER) @@ -68,15 +69,17 @@ def test_preferred_impl_with_tracer(self): def test_preferred_impl_with_default(self): self.do_test_preferred_impl( - loader.set_preferred_default_implementation) + loader.set_preferred_default_implementation + ) def test_try_set_again(self): self.assertTrue(trace.tracer()) # Try setting after the tracer has already been created: with self.assertRaises(RuntimeError) as einfo: trace.set_preferred_tracer_implementation( - get_opentelemetry_implementation) - self.assertIn('already loaded', str(einfo.exception)) + get_opentelemetry_implementation + ) + self.assertIn("already loaded", str(einfo.exception)) def do_test_get_envvar(self, envvar_suffix): global DUMMY_TRACER # pylint:disable=global-statement @@ -84,7 +87,7 @@ def do_test_get_envvar(self, envvar_suffix): # Test is not runnable with this! self.assertFalse(sys.flags.ignore_environment) - envname = 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_' + envvar_suffix + envname = "OPENTELEMETRY_PYTHON_IMPLEMENTATION_" + envvar_suffix os.environ[envname] = __name__ try: tracer = trace.tracer() @@ -95,7 +98,7 @@ def do_test_get_envvar(self, envvar_suffix): self.assertIs(type(tracer), DummyTracer) def test_get_envvar_tracer(self): - return self.do_test_get_envvar('TRACER') + return self.do_test_get_envvar("TRACER") def test_get_envvar_default(self): - return self.do_test_get_envvar('DEFAULT') + return self.do_test_get_envvar("DEFAULT") diff --git a/opentelemetry-api/tests/trace/test_defaultspan.py b/opentelemetry-api/tests/trace/test_defaultspan.py index b3155d3261..a20b55cc4e 100644 --- a/opentelemetry-api/tests/trace/test_defaultspan.py +++ b/opentelemetry-api/tests/trace/test_defaultspan.py @@ -19,9 +19,9 @@ class TestDefaultSpan(unittest.TestCase): def test_ctor(self): - context = trace.SpanContext(1, 1, - trace.DEFAULT_TRACE_OPTIONS, - trace.DEFAULT_TRACE_STATE) + context = trace.SpanContext( + 1, 1, trace.DEFAULT_TRACE_OPTIONS, trace.DEFAULT_TRACE_STATE + ) span = trace.DefaultSpan(context) self.assertEqual(context, span.get_context()) diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index 790dc92893..fabf97cb39 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -17,8 +17,9 @@ import setuptools BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join(BASE_DIR, "src", "opentelemetry", "sdk", - "version.py") +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "sdk", "version.py" +) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: exec(f.read(), PACKAGE_INFO) @@ -42,15 +43,16 @@ description="OpenTelemetry Python SDK", include_package_data=True, long_description=open("README.rst").read(), - install_requires=[ - "opentelemetry-api==0.1.dev0" - ], + install_requires=["opentelemetry-api==0.1.dev0"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, - packages=setuptools.find_namespace_packages(where="src", - include="opentelemetry.sdk.*"), - url=("https://github.com/open-telemetry/opentelemetry-python" - "/tree/master/opentelemetry-sdk"), + packages=setuptools.find_namespace_packages( + where="src", include="opentelemetry.sdk.*" + ), + url=( + "https://github.com/open-telemetry/opentelemetry-python" + "/tree/master/opentelemetry-sdk" + ), zip_safe=False, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py index 7d6a6e83f4..81366d9d47 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py @@ -12,10 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from . import trace -from . import util +from . import trace, util -__all__ = [ - "trace", - "util", -] +__all__ = ["trace", "util"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py index eaeeb577d2..72d02d6070 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -14,8 +14,8 @@ import typing -from opentelemetry.context.propagation.httptextformat import HTTPTextFormat import opentelemetry.trace as trace +from opentelemetry.context.propagation.httptextformat import HTTPTextFormat class B3Format(HTTPTextFormat): @@ -39,7 +39,8 @@ def extract(cls, get_from_carrier, carrier): flags = None single_header = _extract_first_element( - get_from_carrier(carrier, cls.SINGLE_HEADER_KEY)) + get_from_carrier(carrier, cls.SINGLE_HEADER_KEY) + ) if single_header: # The b3 spec calls for the sampling state to be # "deferred", which is unspecified. This concept does not @@ -58,14 +59,30 @@ def extract(cls, get_from_carrier, carrier): else: return trace.INVALID_SPAN_CONTEXT else: - trace_id = _extract_first_element( - get_from_carrier(carrier, cls.TRACE_ID_KEY)) or trace_id - span_id = _extract_first_element( - get_from_carrier(carrier, cls.SPAN_ID_KEY)) or span_id - sampled = _extract_first_element( - get_from_carrier(carrier, cls.SAMPLED_KEY)) or sampled - flags = _extract_first_element( - get_from_carrier(carrier, cls.FLAGS_KEY)) or flags + trace_id = ( + _extract_first_element( + get_from_carrier(carrier, cls.TRACE_ID_KEY) + ) + or trace_id + ) + span_id = ( + _extract_first_element( + get_from_carrier(carrier, cls.SPAN_ID_KEY) + ) + or span_id + ) + sampled = ( + _extract_first_element( + get_from_carrier(carrier, cls.SAMPLED_KEY) + ) + or sampled + ) + flags = ( + _extract_first_element( + get_from_carrier(carrier, cls.FLAGS_KEY) + ) + or flags + ) options = 0 # The b3 spec provides no defined behavior for both sample and @@ -86,10 +103,12 @@ def extract(cls, get_from_carrier, carrier): @classmethod def inject(cls, context, set_in_carrier, carrier): sampled = (trace.TraceOptions.RECORDED & context.trace_options) != 0 - set_in_carrier(carrier, cls.TRACE_ID_KEY, - format_trace_id(context.trace_id)) - set_in_carrier(carrier, cls.SPAN_ID_KEY, - format_span_id(context.span_id)) + set_in_carrier( + carrier, cls.TRACE_ID_KEY, format_trace_id(context.trace_id) + ) + set_in_carrier( + carrier, cls.SPAN_ID_KEY, format_span_id(context.span_id) + ) set_in_carrier(carrier, cls.SAMPLED_KEY, "1" if sampled else "0") diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py index fc449cfe2d..a20cbf8963 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from contextlib import contextmanager import typing +from contextlib import contextmanager from opentelemetry import distributedcontext as dctx_api from opentelemetry.context import Context @@ -35,7 +35,7 @@ def __init__(self, name: str = "") -> None: self._current_context = Context.register_slot(slot_name) def get_current_context( - self, + self, ) -> typing.Optional[dctx_api.DistributedContext]: """Gets the current DistributedContext. @@ -46,8 +46,7 @@ def get_current_context( @contextmanager def use_context( - self, - context: dctx_api.DistributedContext, + self, context: dctx_api.DistributedContext ) -> typing.Iterator[dctx_api.DistributedContext]: """Context manager for controlling a DistributedContext lifetime. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 2d2b8b3c76..72c5c30346 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -13,13 +13,11 @@ # limitations under the License. -from collections import OrderedDict -from collections import deque -from collections import namedtuple -from contextlib import contextmanager import random import threading import typing +from collections import OrderedDict, deque, namedtuple +from contextlib import contextmanager from opentelemetry import trace as trace_api from opentelemetry import types @@ -42,18 +40,16 @@ class BoundedList(Sequence): """An append only list with a fixed max size.""" + def __init__(self, maxlen): self.dropped = 0 self._dq = deque(maxlen=maxlen) self._lock = threading.Lock() def __repr__(self): - return ("{}({}, maxlen={})" - .format( - type(self).__name__, - list(self._dq), - self._dq.maxlen - )) + return "{}({}, maxlen={})".format( + type(self).__name__, list(self._dq), self._dq.maxlen + ) def __getitem__(self, index): return self._dq[index] @@ -91,6 +87,7 @@ def from_seq(cls, maxlen, seq): class BoundedDict(MutableMapping): """A dict with a fixed max capacity.""" + def __init__(self, maxlen): if not isinstance(maxlen, int): raise ValueError @@ -102,12 +99,9 @@ def __init__(self, maxlen): self._lock = threading.Lock() def __repr__(self): - return ("{}({}, maxlen={})" - .format( - type(self).__name__, - dict(self._dict), - self.maxlen - )) + return "{}({}, maxlen={})".format( + type(self).__name__, dict(self._dict), self.maxlen + ) def __getitem__(self, key): return self._dict[key] @@ -146,9 +140,9 @@ def from_map(cls, maxlen, mapping): return bounded_dict -Event = namedtuple('Event', ('name', 'attributes')) +Event = namedtuple("Event", ("name", "attributes")) -Link = namedtuple('Link', ('context', 'attributes')) +Link = namedtuple("Link", ("context", "attributes")) class Span(trace_api.Span): @@ -174,17 +168,18 @@ class Span(trace_api.Span): empty_events = BoundedList(MAX_NUM_EVENTS) empty_links = BoundedList(MAX_NUM_LINKS) - def __init__(self: 'Span', - name: str, - context: 'trace_api.SpanContext', - parent: trace_api.ParentSpan = None, - sampler=None, # TODO - trace_config=None, # TODO - resource=None, # TODO - attributes: types.Attributes = None, # TODO - events: typing.Sequence[Event] = None, # TODO - links: typing.Sequence[Link] = None, # TODO - ) -> None: + def __init__( + self: "Span", + name: str, + context: "trace_api.SpanContext", + parent: trace_api.ParentSpan = None, + sampler=None, # TODO + trace_config=None, # TODO + resource=None, # TODO + attributes: types.Attributes = None, # TODO + events: typing.Sequence[Event] = None, # TODO + links: typing.Sequence[Link] = None, # TODO + ) -> None: self.name = name self.context = context @@ -200,7 +195,8 @@ def __init__(self: 'Span', self.attributes = Span.empty_attributes else: self.attributes = BoundedDict.from_map( - MAX_NUM_ATTRIBUTES, attributes) + MAX_NUM_ATTRIBUTES, attributes + ) if events is None: self.events = Span.empty_events @@ -216,37 +212,32 @@ def __init__(self: 'Span', self.start_time = None def __repr__(self): - return ('{}(name="{}")' - .format( - type(self).__name__, - self.name - )) + return '{}(name="{}")'.format(type(self).__name__, self.name) def get_context(self): return self.context - def set_attribute(self: 'Span', - key: str, - value: types.AttributeValue, - ) -> None: + def set_attribute( + self: "Span", key: str, value: types.AttributeValue + ) -> None: if self.attributes is Span.empty_attributes: self.attributes = BoundedDict(MAX_NUM_ATTRIBUTES) self.attributes[key] = value - def add_event(self: 'Span', - name: str, - attributes: types.Attributes = None, - ) -> None: + def add_event( + self: "Span", name: str, attributes: types.Attributes = None + ) -> None: if self.events is Span.empty_events: self.events = BoundedList(MAX_NUM_EVENTS) if attributes is None: attributes = Span.empty_attributes self.events.append(Event(name, attributes)) - def add_link(self: 'Span', - link_target_context: 'trace_api.SpanContext', - attributes: types.Attributes = None, - ) -> None: + def add_link( + self: "Span", + link_target_context: "trace_api.SpanContext", + attributes: types.Attributes = None, + ) -> None: if self.links is Span.empty_links: self.links = BoundedList(MAX_NUM_LINKS) if attributes is None: @@ -290,12 +281,10 @@ class Tracer(trace_api.Tracer): name: The name of the tracer. """ - def __init__(self, - name: str = '' - ) -> None: - slot_name = 'current_span' + def __init__(self, name: str = "") -> None: + slot_name = "current_span" if name: - slot_name = '{}.current_span'.format(name) + slot_name = "{}.current_span".format(name) self._current_span_slot = Context.register_slot(slot_name) def get_current_span(self): @@ -303,19 +292,20 @@ def get_current_span(self): return self._current_span_slot.get() @contextmanager - def start_span(self, - name: str, - parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN - ) -> typing.Iterator['Span']: + def start_span( + self, + name: str, + parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, + ) -> typing.Iterator["Span"]: """See `opentelemetry.trace.Tracer.start_span`.""" with self.use_span(self.create_span(name, parent)) as span: yield span - def create_span(self, - name: str, - parent: trace_api.ParentSpan = - trace_api.Tracer.CURRENT_SPAN - ) -> 'Span': + def create_span( + self, + name: str, + parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, + ) -> "Span": """See `opentelemetry.trace.Tracer.create_span`.""" span_id = generate_span_id() if parent is Tracer.CURRENT_SPAN: @@ -333,11 +323,12 @@ def create_span(self, parent_context.trace_id, span_id, parent_context.trace_options, - parent_context.trace_state) + parent_context.trace_state, + ) return Span(name=name, context=context, parent=parent) @contextmanager - def use_span(self, span: 'Span') -> typing.Iterator['Span']: + def use_span(self, span: "Span") -> typing.Iterator["Span"]: """See `opentelemetry.trace.Tracer.use_span`.""" span.start() span_snapshot = self._current_span_slot.get() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util.py b/opentelemetry-sdk/src/opentelemetry/sdk/util.py index 125a899f16..d814433eee 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util.py @@ -18,5 +18,6 @@ time_ns = time.time_ns # Python versions < 3.7 except AttributeError: + def time_ns(): return int(time.time() * 1e9) diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py index a24dd01c66..42ff3410f0 100644 --- a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -29,9 +29,11 @@ class TestB3Format(unittest.TestCase): @classmethod def setUpClass(cls): cls.serialized_trace_id = b3_format.format_trace_id( - trace.generate_trace_id()) + trace.generate_trace_id() + ) cls.serialized_span_id = b3_format.format_span_id( - trace.generate_span_id()) + trace.generate_span_id() + ) def test_extract_multi_header(self): """Test the extraction of B3 headers.""" @@ -43,25 +45,30 @@ def test_extract_multi_header(self): span_context = FORMAT.extract(get_as_list, carrier) new_carrier = {} FORMAT.inject(span_context, dict.__setitem__, new_carrier) - self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], - self.serialized_trace_id) - self.assertEqual(new_carrier[FORMAT.SPAN_ID_KEY], - self.serialized_span_id) + self.assertEqual( + new_carrier[FORMAT.TRACE_ID_KEY], self.serialized_trace_id + ) + self.assertEqual( + new_carrier[FORMAT.SPAN_ID_KEY], self.serialized_span_id + ) self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") def test_extract_single_header(self): """Test the extraction from a single b3 header.""" carrier = { - FORMAT.SINGLE_HEADER_KEY: - "{}-{}".format(self.serialized_trace_id, self.serialized_span_id) + FORMAT.SINGLE_HEADER_KEY: "{}-{}".format( + self.serialized_trace_id, self.serialized_span_id + ) } span_context = FORMAT.extract(get_as_list, carrier) new_carrier = {} FORMAT.inject(span_context, dict.__setitem__, new_carrier) - self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], - self.serialized_trace_id) - self.assertEqual(new_carrier[FORMAT.SPAN_ID_KEY], - self.serialized_span_id) + self.assertEqual( + new_carrier[FORMAT.TRACE_ID_KEY], self.serialized_trace_id + ) + self.assertEqual( + new_carrier[FORMAT.SPAN_ID_KEY], self.serialized_span_id + ) self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") def test_extract_header_precedence(self): @@ -70,20 +77,19 @@ def test_extract_header_precedence(self): """ single_header_trace_id = self.serialized_trace_id[:-3] + "123" carrier = { - FORMAT.SINGLE_HEADER_KEY: - "{}-{}".format(single_header_trace_id, self.serialized_span_id), - FORMAT.TRACE_ID_KEY: - self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: - self.serialized_span_id, - FORMAT.SAMPLED_KEY: - "1", + FORMAT.SINGLE_HEADER_KEY: "{}-{}".format( + single_header_trace_id, self.serialized_span_id + ), + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.SAMPLED_KEY: "1", } span_context = FORMAT.extract(get_as_list, carrier) new_carrier = {} FORMAT.inject(span_context, dict.__setitem__, new_carrier) - self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], - single_header_trace_id) + self.assertEqual( + new_carrier[FORMAT.TRACE_ID_KEY], single_header_trace_id + ) def test_enabled_sampling(self): """Test b3 sample key variants that turn on sampling.""" @@ -146,8 +152,9 @@ def test_64bit_trace_id(self): span_context = FORMAT.extract(get_as_list, carrier) new_carrier = {} FORMAT.inject(span_context, dict.__setitem__, new_carrier) - self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], - "0" * 16 + trace_id_64_bit) + self.assertEqual( + new_carrier[FORMAT.TRACE_ID_KEY], "0" * 16 + trace_id_64_bit + ) def test_invalid_single_header(self): """If an invalid single header is passed, return an @@ -162,7 +169,7 @@ def test_missing_trace_id(self): """If a trace id is missing, populate an invalid trace id.""" carrier = { FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1" + FORMAT.FLAGS_KEY: "1", } span_context = FORMAT.extract(get_as_list, carrier) self.assertEqual(span_context.trace_id, api_trace.INVALID_TRACE_ID) @@ -171,7 +178,7 @@ def test_missing_span_id(self): """If a trace id is missing, populate an invalid trace id.""" carrier = { FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.FLAGS_KEY: "1" + FORMAT.FLAGS_KEY: "1", } span_context = FORMAT.extract(get_as_list, carrier) self.assertEqual(span_context.span_id, api_trace.INVALID_SPAN_ID) diff --git a/opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py b/opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py index fedcf7c9dd..eddb61330d 100644 --- a/opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py +++ b/opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py @@ -30,22 +30,13 @@ def test_use_context(self): dctx = dctx_api.DistributedContext(()) with self.manager.use_context(dctx) as current: self.assertIs(current, dctx) - self.assertIs( - self.manager.get_current_context(), - dctx, - ) + self.assertIs(self.manager.get_current_context(), dctx) # Context is overridden nested_dctx = dctx_api.DistributedContext(()) with self.manager.use_context(nested_dctx) as current: self.assertIs(current, nested_dctx) - self.assertIs( - self.manager.get_current_context(), - nested_dctx, - ) + self.assertIs(self.manager.get_current_context(), nested_dctx) # Context is restored - self.assertIs( - self.manager.get_current_context(), - dctx, - ) + self.assertIs(self.manager.get_current_context(), dctx) diff --git a/opentelemetry-sdk/tests/resources/test_init.py b/opentelemetry-sdk/tests/resources/test_init.py index 6ce69fff89..47e986bd35 100644 --- a/opentelemetry-sdk/tests/resources/test_init.py +++ b/opentelemetry-sdk/tests/resources/test_init.py @@ -9,10 +9,8 @@ def test_resource_merge(self): right = resources.Resource({"host": "service-host"}) self.assertEqual( left.merge(right), - resources.Resource({ - "service": "ui", - "host": "service-host" - })) + resources.Resource({"service": "ui", "host": "service-host"}), + ) def test_resource_merge_empty_string(self): """Verify Resource.merge behavior with the empty string. @@ -22,13 +20,10 @@ def test_resource_merge_empty_string(self): """ left = resources.Resource({"service": "ui", "host": ""}) - right = resources.Resource({ - "host": "service-host", - "service": "not-ui" - }) + right = resources.Resource( + {"host": "service-host", "service": "not-ui"} + ) self.assertEqual( left.merge(right), - resources.Resource({ - "service": "ui", - "host": "service-host" - })) + resources.Resource({"service": "ui", "host": "service-host"}), + ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index f736a8152d..240972344c 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -20,26 +20,24 @@ class TestTracer(unittest.TestCase): - def test_extends_api(self): tracer = trace.Tracer() self.assertIsInstance(tracer, trace_api.Tracer) class TestSpanCreation(unittest.TestCase): - def test_start_span_implicit(self): - tracer = trace.Tracer('test_start_span_implicit') + tracer = trace.Tracer("test_start_span_implicit") self.assertIsNone(tracer.get_current_span()) - with tracer.start_span('root') as root: + with tracer.start_span("root") as root: self.assertIs(tracer.get_current_span(), root) self.assertIsNotNone(root.start_time) self.assertIsNone(root.end_time) - with tracer.start_span('child') as child: + with tracer.start_span("child") as child: self.assertIs(tracer.get_current_span(), child) self.assertIs(child.parent, root) @@ -51,12 +49,15 @@ def test_start_span_implicit(self): root_context = root.get_context() child_context = child.get_context() self.assertEqual(root_context.trace_id, child_context.trace_id) - self.assertNotEqual(root_context.span_id, - child_context.span_id) - self.assertEqual(root_context.trace_state, - child_context.trace_state) - self.assertEqual(root_context.trace_options, - child_context.trace_options) + self.assertNotEqual( + root_context.span_id, child_context.span_id + ) + self.assertEqual( + root_context.trace_state, child_context.trace_state + ) + self.assertEqual( + root_context.trace_options, child_context.trace_options + ) # After exiting the child's scope the parent should become the # current span again. @@ -67,23 +68,23 @@ def test_start_span_implicit(self): self.assertIsNotNone(root.end_time) def test_start_span_explicit(self): - tracer = trace.Tracer('test_start_span_explicit') + tracer = trace.Tracer("test_start_span_explicit") other_parent = trace_api.SpanContext( - trace_id=0x000000000000000000000000deadbeef, - span_id=0x00000000deadbef0 + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, ) self.assertIsNone(tracer.get_current_span()) # Test with the implicit root span - with tracer.start_span('root') as root: + with tracer.start_span("root") as root: self.assertIs(tracer.get_current_span(), root) self.assertIsNotNone(root.start_time) self.assertIsNone(root.end_time) - with tracer.start_span('stepchild', other_parent) as child: + with tracer.start_span("stepchild", other_parent) as child: # The child should become the current span as usual, but its # parent should be the one passed in, not the # previously-current span. @@ -98,12 +99,15 @@ def test_start_span_explicit(self): # parent, not the previously-current span. child_context = child.get_context() self.assertEqual(other_parent.trace_id, child_context.trace_id) - self.assertNotEqual(other_parent.span_id, - child_context.span_id) - self.assertEqual(other_parent.trace_state, - child_context.trace_state) - self.assertEqual(other_parent.trace_options, - child_context.trace_options) + self.assertNotEqual( + other_parent.span_id, child_context.span_id + ) + self.assertEqual( + other_parent.trace_state, child_context.trace_state + ) + self.assertEqual( + other_parent.trace_options, child_context.trace_options + ) # After exiting the child's scope the last span on the stack should # become current, not the child's parent. @@ -112,78 +116,85 @@ def test_start_span_explicit(self): self.assertIsNotNone(child.end_time) def test_span_members(self): - tracer = trace.Tracer('test_span_members') + tracer = trace.Tracer("test_span_members") other_context1 = trace_api.SpanContext( trace_id=trace.generate_trace_id(), - span_id=trace.generate_span_id() + span_id=trace.generate_span_id(), ) other_context2 = trace_api.SpanContext( trace_id=trace.generate_trace_id(), - span_id=trace.generate_span_id() + span_id=trace.generate_span_id(), ) self.assertIsNone(tracer.get_current_span()) - with tracer.start_span('root') as root: - root.set_attribute('component', 'http') - root.set_attribute('http.method', 'GET') - root.set_attribute('http.url', - 'https://example.com:779/path/12/?q=d#123') - root.set_attribute('http.status_code', 200) - root.set_attribute('http.status_text', 'OK') - root.set_attribute('misc.pi', 3.14) + with tracer.start_span("root") as root: + root.set_attribute("component", "http") + root.set_attribute("http.method", "GET") + root.set_attribute( + "http.url", "https://example.com:779/path/12/?q=d#123" + ) + root.set_attribute("http.status_code", 200) + root.set_attribute("http.status_text", "OK") + root.set_attribute("misc.pi", 3.14) # Setting an attribute with the same key as an existing attribute # SHOULD overwrite the existing attribute's value. - root.set_attribute('attr-key', 'attr-value1') - root.set_attribute('attr-key', 'attr-value2') + root.set_attribute("attr-key", "attr-value1") + root.set_attribute("attr-key", "attr-value2") - root.add_event('event0') - root.add_event('event1', {'name': 'birthday'}) + root.add_event("event0") + root.add_event("event1", {"name": "birthday"}) root.add_link(other_context1) - root.add_link(other_context2, {'name': 'neighbor'}) + root.add_link(other_context2, {"name": "neighbor"}) - root.update_name('toor') - self.assertEqual(root.name, 'toor') + root.update_name("toor") + self.assertEqual(root.name, "toor") # The public API does not expose getters. # Checks by accessing the span members directly self.assertEqual(len(root.attributes), 7) - self.assertEqual(root.attributes['component'], 'http') - self.assertEqual(root.attributes['http.method'], 'GET') - self.assertEqual(root.attributes['http.url'], - 'https://example.com:779/path/12/?q=d#123') - self.assertEqual(root.attributes['http.status_code'], 200) - self.assertEqual(root.attributes['http.status_text'], 'OK') - self.assertEqual(root.attributes['misc.pi'], 3.14) - self.assertEqual(root.attributes['attr-key'], 'attr-value2') + self.assertEqual(root.attributes["component"], "http") + self.assertEqual(root.attributes["http.method"], "GET") + self.assertEqual( + root.attributes["http.url"], + "https://example.com:779/path/12/?q=d#123", + ) + self.assertEqual(root.attributes["http.status_code"], 200) + self.assertEqual(root.attributes["http.status_text"], "OK") + self.assertEqual(root.attributes["misc.pi"], 3.14) + self.assertEqual(root.attributes["attr-key"], "attr-value2") self.assertEqual(len(root.events), 2) - self.assertEqual(root.events[0], - trace.Event(name='event0', - attributes={})) - self.assertEqual(root.events[1], - trace.Event(name='event1', - attributes={'name': 'birthday'})) + self.assertEqual( + root.events[0], trace.Event(name="event0", attributes={}) + ) + self.assertEqual( + root.events[1], + trace.Event(name="event1", attributes={"name": "birthday"}), + ) self.assertEqual(len(root.links), 2) - self.assertEqual(root.links[0].context.trace_id, - other_context1.trace_id) - self.assertEqual(root.links[0].context.span_id, - other_context1.span_id) + self.assertEqual( + root.links[0].context.trace_id, other_context1.trace_id + ) + self.assertEqual( + root.links[0].context.span_id, other_context1.span_id + ) self.assertEqual(root.links[0].attributes, {}) - self.assertEqual(root.links[1].context.trace_id, - other_context2.trace_id) - self.assertEqual(root.links[1].context.span_id, - other_context2.span_id) - self.assertEqual(root.links[1].attributes, {'name': 'neighbor'}) + self.assertEqual( + root.links[1].context.trace_id, other_context2.trace_id + ) + self.assertEqual( + root.links[1].context.span_id, other_context2.span_id + ) + self.assertEqual(root.links[1].attributes, {"name": "neighbor"}) class TestSpan(unittest.TestCase): - def test_basic_span(self): - span = trace.Span('name', mock.Mock(spec=trace_api.SpanContext)) - self.assertEqual(span.name, 'name') + span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + self.assertEqual(span.name, "name") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..a8f43fefdf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 79 diff --git a/tox.ini b/tox.ini index 0b2e30c312..443a5761ac 100644 --- a/tox.ini +++ b/tox.ini @@ -45,6 +45,7 @@ deps = pylint~=2.3 flake8~=3.7 isort~=4.3 + black>=19.3b0,==19.* commands_pre = pip install -e {toxinidir}/opentelemetry-api @@ -55,6 +56,7 @@ commands_pre = commands = ; Prefer putting everything in one pylint command to profit from duplication ; warnings. + black --check --diff opentelemetry-api/src/opentelemetry opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry opentelemetry-sdk/tests/ ext/opentelemetry-ext-http-requests/src/ ext/opentelemetry-ext-http-requests/tests/ ext/opentelemetry-ext-wsgi/tests/ pylint opentelemetry-api/src/opentelemetry opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry opentelemetry-sdk/tests/ ext/opentelemetry-ext-http-requests/src/ ext/opentelemetry-ext-http-requests/tests/ ext/opentelemetry-ext-wsgi/tests/ flake8 opentelemetry-api/src/ opentelemetry-api/tests/ opentelemetry-sdk/src/ opentelemetry-sdk/tests/ ext/opentelemetry-ext-wsgi/src/ ext/opentelemetry-ext-wsgi/tests/ ext/opentelemetry-ext-http-requests/src/ isort --check-only --recursive opentelemetry-api/src opentelemetry-sdk/src ext/opentelemetry-ext-wsgi/src ext/opentelemetry-ext-wsgi/tests ext/opentelemetry-ext-http-requests/src/ From 4aea780ab04fe93b93b8efca5b956febea050818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 2 Sep 2019 06:23:40 -0500 Subject: [PATCH 0058/1517] tox: add --diff to isort (#118) Sometimes it complains and it is difficult to understand what exactly it wants. The --diff option allows to know what is the format it expects. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 443a5761ac..5cbdfa2d5e 100644 --- a/tox.ini +++ b/tox.ini @@ -59,7 +59,7 @@ commands = black --check --diff opentelemetry-api/src/opentelemetry opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry opentelemetry-sdk/tests/ ext/opentelemetry-ext-http-requests/src/ ext/opentelemetry-ext-http-requests/tests/ ext/opentelemetry-ext-wsgi/tests/ pylint opentelemetry-api/src/opentelemetry opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry opentelemetry-sdk/tests/ ext/opentelemetry-ext-http-requests/src/ ext/opentelemetry-ext-http-requests/tests/ ext/opentelemetry-ext-wsgi/tests/ flake8 opentelemetry-api/src/ opentelemetry-api/tests/ opentelemetry-sdk/src/ opentelemetry-sdk/tests/ ext/opentelemetry-ext-wsgi/src/ ext/opentelemetry-ext-wsgi/tests/ ext/opentelemetry-ext-http-requests/src/ - isort --check-only --recursive opentelemetry-api/src opentelemetry-sdk/src ext/opentelemetry-ext-wsgi/src ext/opentelemetry-ext-wsgi/tests ext/opentelemetry-ext-http-requests/src/ + isort --check-only --diff --recursive opentelemetry-api/src opentelemetry-sdk/src ext/opentelemetry-ext-wsgi/src ext/opentelemetry-ext-wsgi/tests ext/opentelemetry-ext-http-requests/src/ [testenv:docs] deps = From ec0dfb42605ba34a0d2fd791378c7a93b6364398 Mon Sep 17 00:00:00 2001 From: Aliaksei Urbanski Date: Thu, 5 Sep 2019 09:57:30 +0300 Subject: [PATCH 0059/1517] Describe isort's multi_line_output setting (#109) These changes follow up the "Fix and improve tests for Python != 3.7" PR. The multi_line_output was already set to 3 in the "Add initial black formatting" PR, so after rebasing to master this commit contains only comment that describes a magic number from the isort configuration file. Corresponding PR: - https://github.com/open-telemetry/opentelemetry-python/pull/109 Related discussions: - https://github.com/open-telemetry/opentelemetry-python/pull/95#discussion_r315942697 - https://github.com/open-telemetry/opentelemetry-python/pull/95#issuecomment-523566519 --- .isort.cfg | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.isort.cfg b/.isort.cfg index 6bde062b5c..43cafae197 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,6 +1,14 @@ [settings] -multi_line_output=3 include_trailing_comma=True force_grid_wrap=0 use_parentheses=True line_length=79 + +; 3 stands for Vertical Hanging Indent, e.g. +; from third_party import ( +; lib1, +; lib2, +; lib3, +; ) +; docs: https://github.com/timothycrosley/isort#multi-line-output-modes +multi_line_output=3 From 15039dec7758437928fc85f6c37fcce8415b6e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Fri, 6 Sep 2019 11:30:18 +0200 Subject: [PATCH 0060/1517] Fix setup for ext packages. (#122) * Fix setup for ext packages. Previously the wheels would contain the metadata but not the actual code. * Check that all packages install properly in test. * Document why we don't use -e. --- ext/opentelemetry-ext-http-requests/setup.cfg | 2 +- ext/opentelemetry-ext-wsgi/setup.cfg | 2 +- tox.ini | 11 ++++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg index 01ee81fda1..7d41a525cd 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -37,7 +37,7 @@ classifiers = python_requires = >=3.4 package_dir= =src -packages=find: +packages=find_namespace: install_requires = opentelemetry-api >= 0.1.dev0 requests ~= 2.0 diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index a77e9fd1fb..4405e37a30 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -37,7 +37,7 @@ classifiers = python_requires = >=3.4 package_dir= =src -packages=find: +packages=find_namespace: install_requires = opentelemetry-api diff --git a/tox.ini b/tox.ini index 5cbdfa2d5e..acf1bcb367 100644 --- a/tox.ini +++ b/tox.ini @@ -26,12 +26,13 @@ changedir = test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests commands_pre = +; Install without -e to test the actual installation python -m pip install -U pip setuptools wheel - test: pip install -e {toxinidir}/opentelemetry-api - test-sdk: pip install -e {toxinidir}/opentelemetry-sdk - ext: pip install -e {toxinidir}/opentelemetry-api - wsgi: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi - http-requests: pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests + test: pip install {toxinidir}/opentelemetry-api + test-sdk: pip install {toxinidir}/opentelemetry-sdk + ext: pip install {toxinidir}/opentelemetry-api + wsgi: pip install {toxinidir}/ext/opentelemetry-ext-wsgi + http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests commands = mypy: mypy --namespace-packages opentelemetry-api/src/opentelemetry/ From 42acdb9a38dc515da74cd7022d1aa5933d6f6844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 6 Sep 2019 07:47:15 -0500 Subject: [PATCH 0061/1517] sdk/trace: add SpanProcessor (#115) SpanProcessor is an interface that allows to register hooks for Span start and end invocations. This commit adds the SpanProcessor interface to the SDK as well as the MultiSpanProcessor that allows to register multiple processors. --- .../src/opentelemetry/sdk/trace/__init__.py | 86 ++++++++++++- opentelemetry-sdk/tests/trace/test_trace.py | 113 ++++++++++++++++++ 2 files changed, 198 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 72c5c30346..21acb39494 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -145,6 +145,68 @@ def from_map(cls, maxlen, mapping): Link = namedtuple("Link", ("context", "attributes")) +class SpanProcessor: + """Interface which allows hooks for SDK's `Span`s start and end method + invocations. + + Span processors can be registered directly using + :func:`~Tracer:add_span_processor` and they are invoked in the same order + as they were registered. + """ + + def on_start(self, span: "Span") -> None: + """Called when a :class:`Span` is started. + + This method is called synchronously on the thread that starts the + span, therefore it should not block or throw an exception. + + Args: + span: The :class:`Span` that just started. + """ + + def on_end(self, span: "Span") -> None: + """Called when a :class:`Span` is ended. + + This method is called synchronously on the thread that ends the + span, therefore it should not block or throw an exception. + + Args: + span: The :class:`Span` that just ended. + """ + + def shutdown(self) -> None: + """Called when a :class:`Tracer` is shutdown.""" + + +class MultiSpanProcessor(SpanProcessor): + """Implementation of :class:`SpanProcessor` that forwards all received + events to a list of `SpanProcessor`. + """ + + def __init__(self): + # use a tuple to avoid race conditions when adding a new span and + # iterating through it on "on_start" and "on_end". + self._span_processors = () + self._lock = threading.Lock() + + def add_span_processor(self, span_processor: SpanProcessor): + """Adds a SpanProcessor to the list handled by this instance.""" + with self._lock: + self._span_processors = self._span_processors + (span_processor,) + + def on_start(self, span: "Span") -> None: + for sp in self._span_processors: + sp.on_start(span) + + def on_end(self, span: "Span") -> None: + for sp in self._span_processors: + sp.on_end(span) + + def shutdown(self) -> None: + for sp in self._span_processors: + sp.shutdown() + + class Span(trace_api.Span): """See `opentelemetry.trace.Span`. @@ -161,6 +223,8 @@ class Span(trace_api.Span): attributes: The span's attributes to be exported events: Timestamped events to be exported links: Links to other spans to be exported + span_processor: `SpanProcessor` to invoke when starting and ending + this `Span`. """ # Initialize these lazily assuming most spans won't have them. @@ -179,6 +243,7 @@ def __init__( attributes: types.Attributes = None, # TODO events: typing.Sequence[Event] = None, # TODO links: typing.Sequence[Link] = None, # TODO + span_processor: SpanProcessor = SpanProcessor(), ) -> None: self.name = name @@ -190,6 +255,7 @@ def __init__( self.attributes = attributes self.events = events self.links = links + self.span_processor = span_processor if attributes is None: self.attributes = Span.empty_attributes @@ -247,10 +313,12 @@ def add_link( def start(self): if self.start_time is None: self.start_time = util.time_ns() + self.span_processor.on_start(self) def end(self): if self.end_time is None: self.end_time = util.time_ns() + self.span_processor.on_end(self) def update_name(self, name: str) -> None: self.name = name @@ -286,6 +354,7 @@ def __init__(self, name: str = "") -> None: if name: slot_name = "{}.current_span".format(name) self._current_span_slot = Context.register_slot(slot_name) + self._active_span_processor = MultiSpanProcessor() def get_current_span(self): """See `opentelemetry.trace.Tracer.get_current_span`.""" @@ -325,7 +394,12 @@ def create_span( parent_context.trace_options, parent_context.trace_state, ) - return Span(name=name, context=context, parent=parent) + return Span( + name=name, + context=context, + parent=parent, + span_processor=self._active_span_processor, + ) @contextmanager def use_span(self, span: "Span") -> typing.Iterator["Span"]: @@ -339,5 +413,15 @@ def use_span(self, span: "Span") -> typing.Iterator["Span"]: self._current_span_slot.set(span_snapshot) span.end() + def add_span_processor(self, span_processor: SpanProcessor) -> None: + """Registers a new :class:`SpanProcessor` for this `Tracer`. + + The span processors are invoked in the same order they are registered. + """ + + # no lock here because MultiSpanProcessor.add_span_processor is + # thread safe + self._active_span_processor.add_span_processor(span_processor) + tracer = Tracer() diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 240972344c..c0a0e65008 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -198,3 +198,116 @@ class TestSpan(unittest.TestCase): def test_basic_span(self): span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) self.assertEqual(span.name, "name") + + +def span_event_start_fmt(span_processor_name, span_name): + return span_processor_name + ":" + span_name + ":start" + + +def span_event_end_fmt(span_processor_name, span_name): + return span_processor_name + ":" + span_name + ":end" + + +class MySpanProcessor(trace.SpanProcessor): + def __init__(self, name, span_list): + self.name = name + self.span_list = span_list + + def on_start(self, span: "trace.Span") -> None: + self.span_list.append(span_event_start_fmt(self.name, span.name)) + + def on_end(self, span: "trace.Span") -> None: + self.span_list.append(span_event_end_fmt(self.name, span.name)) + + +class TestSpanProcessor(unittest.TestCase): + def test_span_processor(self): + tracer = trace.Tracer() + + spans_calls_list = [] # filled by MySpanProcessor + expected_list = [] # filled by hand + + # Span processors are created but not added to the tracer yet + sp1 = MySpanProcessor("SP1", spans_calls_list) + sp2 = MySpanProcessor("SP2", spans_calls_list) + + with tracer.start_span("foo"): + with tracer.start_span("bar"): + with tracer.start_span("baz"): + pass + + # at this point lists must be empty + self.assertEqual(len(spans_calls_list), 0) + + # add single span processor + tracer.add_span_processor(sp1) + + with tracer.start_span("foo"): + expected_list.append(span_event_start_fmt("SP1", "foo")) + + with tracer.start_span("bar"): + expected_list.append(span_event_start_fmt("SP1", "bar")) + + with tracer.start_span("baz"): + expected_list.append(span_event_start_fmt("SP1", "baz")) + + expected_list.append(span_event_end_fmt("SP1", "baz")) + + expected_list.append(span_event_end_fmt("SP1", "bar")) + + expected_list.append(span_event_end_fmt("SP1", "foo")) + + self.assertListEqual(spans_calls_list, expected_list) + + spans_calls_list.clear() + expected_list.clear() + + # go for multiple span processors + tracer.add_span_processor(sp2) + + with tracer.start_span("foo"): + expected_list.append(span_event_start_fmt("SP1", "foo")) + expected_list.append(span_event_start_fmt("SP2", "foo")) + + with tracer.start_span("bar"): + expected_list.append(span_event_start_fmt("SP1", "bar")) + expected_list.append(span_event_start_fmt("SP2", "bar")) + + with tracer.start_span("baz"): + expected_list.append(span_event_start_fmt("SP1", "baz")) + expected_list.append(span_event_start_fmt("SP2", "baz")) + + expected_list.append(span_event_end_fmt("SP1", "baz")) + expected_list.append(span_event_end_fmt("SP2", "baz")) + + expected_list.append(span_event_end_fmt("SP1", "bar")) + expected_list.append(span_event_end_fmt("SP2", "bar")) + + expected_list.append(span_event_end_fmt("SP1", "foo")) + expected_list.append(span_event_end_fmt("SP2", "foo")) + + # compare if two lists are the same + self.assertListEqual(spans_calls_list, expected_list) + + def test_add_span_processor_after_span_creation(self): + tracer = trace.Tracer() + + spans_calls_list = [] # filled by MySpanProcessor + expected_list = [] # filled by hand + + # Span processors are created but not added to the tracer yet + sp = MySpanProcessor("SP1", spans_calls_list) + + with tracer.start_span("foo"): + with tracer.start_span("bar"): + with tracer.start_span("baz"): + # add span processor after spans have been created + tracer.add_span_processor(sp) + + expected_list.append(span_event_end_fmt("SP1", "baz")) + + expected_list.append(span_event_end_fmt("SP1", "bar")) + + expected_list.append(span_event_end_fmt("SP1", "foo")) + + self.assertListEqual(spans_calls_list, expected_list) From 17bef26bd9f6d972b1c376c668cc2c4a988584b1 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 10 Sep 2019 01:06:18 -0700 Subject: [PATCH 0062/1517] Flesh out README, add release schedule (#127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Flesh out README, add release schedule * Update README.md Co-Authored-By: Christian Neumüller * Update README.md Co-Authored-By: Christian Neumüller * Add note about alpha/beta compatability, support * Move development notes into contributing doc * Add note about library, app dev API/SDK use --- CONTRIBUTING.md | 34 +++++++++++++++++++++++++------ README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d11b7217cb..b327ce96e1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,26 @@ See the [public meeting notes](https://docs.google.com/document/d/1CIMGoIOZ-c3-i for a summary description of past meetings. To request edit access, join the meeting or get in touch on [Gitter](https://gitter.im/open-telemetry/opentelemetry-python). -## Pull Request +## Development + +This project uses [`tox`](https://tox.readthedocs.io) to automate some aspects +of development, including testing against multiple Python versions. + +You can run: + +- `tox` to run all existing tox commands, including unit tests for all packages + under multiple Python versions +- `tox -e docs` to regenerate the API docs +- `tox -e test-api` and `tox -e test-sdk` to run the API and SDK unit tests +- `tox -e py37-test-api` to e.g. run the the API unit tests under a specific + Python version +- `tox -e lint` to run lint checks on all code + +See +[`tox.ini`](https://github.com/open-telemetry/opentelemetry-python/blob/master/tox.ini) +for more detail on available tox commands. + +## Pull Requests ### How to Send Pull Requests @@ -52,7 +71,7 @@ Open a pull request against the main `opentelemetry-python` repo. as `work-in-progress`, or mark it as [`draft`](https://github.blog/2019-02-14-introducing-draft-pull-requests/). * Make sure CLA is signed and CI is clear. -### How to Get PR Merged +### How to Get PRs Merged A PR is considered to be **ready to merge** when: * It has received two approvals from Collaborators/Maintainers (at different @@ -85,10 +104,13 @@ rather than conform to specific API names or argument patterns in the spec. For a deeper discussion, see: https://github.com/open-telemetry/opentelemetry-specification/issues/165 -## Styleguide +## Style Guide -* docstrings should adhere to the Google styleguide as specified - with the [napolean extension](http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#google-vs-numpy) extension in [Sphinx](http://www.sphinx-doc.org/en/master/index.html). +* docstrings should adhere to the [Google Python Style + Guide](http://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) + as specified with the [napolean + extension](http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#google-vs-numpy) + extension in [Sphinx](http://www.sphinx-doc.org/en/master/index.html). ## Become a Collaborator @@ -110,4 +132,4 @@ To become a Maintainer: * Become a Collaborator. * Demonstrate the ability and commitment. * Contact the Maintainers, express the willingness and commitment. -* Acknowledged and approved by all the current Maintainers. \ No newline at end of file +* Acknowledged and approved by all the current Maintainers. diff --git a/README.md b/README.md index 56f4d0f7d1..991314bdf1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,37 @@ The Python [OpenTelemetry](https://opentelemetry.io/) client. ## Installation -## Usage +This repository includes multiple installable packages. The `opentelemetry-api` +package includes abstract classes and no-op implementations that comprise the OpenTelemetry API following +[the +specification](https://github.com/open-telemetry/opentelemetry-specification). +The `opentelemetry-sdk` package is the reference implementation of the API. + +Libraries that produce telemetry data should only depend on `opentelemetry-api`, +and defer the choice of the SDK to the application developer. Applications may +depend on `opentelemetry-sdk` or another package that implements the API. + +To install the API and SDK packages, fork or clone this repo and do an +[editable +install](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs) +via `pip`: + +```sh +pip install -e ./opentelemetry-api +pip install -e ./opentelemetry-sdk +``` + +The +[`ext/`](https://github.com/open-telemetry/opentelemetry-python/tree/master/ext) +directory includes OpenTelemetry integration packages, which can be installed +separately as: + +```sh +pip install -e ./ext/opentelemetry-ext-{integration} +``` + +## Quick Start + ```python from opentelemetry import trace from opentelemetry.context import Context @@ -28,4 +58,24 @@ with tracer.start_span('foo'): ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) \ No newline at end of file +See [CONTRIBUTING.md](CONTRIBUTING.md) + +## Release Schedule + +OpenTelemetry Python is under active development. Our goal is to release an +_alpha_ version of the library at the end of September 2019. This release isn't +guaranteed to conform to a specific version of the specification, and future +releases will not attempt to maintain backwards compatibility with the alpha +release. + +| Component | Version | Target Date | +| --------------------------- | ------- | ----------------- | +| Tracing API | Alpha | September 30 2019 | +| Tracing SDK | Alpha | September 30 2019 | +| Metrics API | Alpha | September 30 2019 | +| Metrics SDK | Alpha | September 30 2019 | +| Jaeger Trace Exporter | Alpha | Unknown | +| Prometheus Metrics Exporter | Alpha | Unknown | +| Context Propagation | Alpha | September 30 2019 | +| OpenTracing Bridge | Alpha | Unknown | +| OpenCensus Bridge | Alpha | Unknown | From 21c1c4abd51652477068e3a02c60a4fe644eea7d Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 10 Sep 2019 22:14:26 -0700 Subject: [PATCH 0063/1517] Introducing an example app instrumented with opentelemetry (#129) Creating an example app that showcases how an application integrates with opentelemetry. --- README.md | 2 + .../tests/test_requests_integration.py | 7 +- opentelemetry-api/tests/test_loader.py | 5 +- opentelemetry-example-app/README.rst | 12 ++++ opentelemetry-example-app/setup.py | 53 ++++++++++++++ .../src/opentelemetry_example_app/__init__.py | 0 .../flask_example.py | 69 +++++++++++++++++++ opentelemetry-example-app/tests/__init__.py | 13 ++++ .../tests/test_flask_example.py | 14 ++++ .../context/propagation/test_b3_format.py | 3 +- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- tox.ini | 26 +++++-- 12 files changed, 192 insertions(+), 14 deletions(-) create mode 100644 opentelemetry-example-app/README.rst create mode 100644 opentelemetry-example-app/setup.py create mode 100644 opentelemetry-example-app/src/opentelemetry_example_app/__init__.py create mode 100644 opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py create mode 100644 opentelemetry-example-app/tests/__init__.py create mode 100644 opentelemetry-example-app/tests/test_flask_example.py diff --git a/README.md b/README.md index 991314bdf1..b597d6f611 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ with tracer.start_span('foo'): print(Context) ``` +See [opentelemetry-example-app](./opentelemetry-example-app/README.rst) for a complete example. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index fa7741695c..fac5435ccc 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -2,9 +2,10 @@ import unittest from unittest import mock -import opentelemetry.ext.http_requests import requests import urllib3 + +import opentelemetry.ext.http_requests from opentelemetry import trace @@ -53,7 +54,7 @@ def tearDown(self): def test_basic(self): url = "https://www.example.org/foo/bar?x=y#top" - _response = requests.get(url=url) + requests.get(url=url) self.assertEqual(1, len(self.send.call_args_list)) self.tracer.start_span.assert_called_with("/foo/bar") self.span_context_manager.__enter__.assert_called_with() @@ -78,7 +79,7 @@ def test_invalid_url(self): exception_type = ValueError with self.assertRaises(exception_type): - _response = requests.post(url=url) + requests.post(url=url) self.assertTrue( self.tracer.start_span.call_args[0][0].startswith( "`_ \ No newline at end of file diff --git a/opentelemetry-example-app/setup.py b/opentelemetry-example-app/setup.py new file mode 100644 index 0000000000..4494d4ad0f --- /dev/null +++ b/opentelemetry-example-app/setup.py @@ -0,0 +1,53 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import setuptools + +setuptools.setup( + name="opentelemetry-example-app", + version="0.1.dev0", + author="OpenTelemetry Authors", + author_email="cncf-opentelemetry-contributors@lists.cncf.io", + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + ], + description="OpenTelemetry Python API", + include_package_data=True, + long_description=open("README.rst").read(), + install_requires=[ + "typing; python_version<'3.5'", + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-ext-http-requests", + "opentelemetry-ext-wsgi", + "flask", + "requests", + ], + license="Apache-2.0", + package_dir={"": "src"}, + packages=setuptools.find_namespace_packages(where="src"), + url=( + "https://github.com/open-telemetry/opentelemetry-python" + "/tree/master/opentelemetry-example-app" + ), + zip_safe=False, +) diff --git a/opentelemetry-example-app/src/opentelemetry_example_app/__init__.py b/opentelemetry-example-app/src/opentelemetry_example_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py new file mode 100644 index 0000000000..9568f270c9 --- /dev/null +++ b/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -0,0 +1,69 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This module serves as an example to integrate with flask, using +the requests library to perform downstream requests +""" +import time + +import flask + +import opentelemetry.ext.http_requests +from opentelemetry import trace +from opentelemetry.ext.wsgi import OpenTelemetryMiddleware +from opentelemetry.sdk.trace import Tracer + + +def configure_opentelemetry(flask_app: flask.Flask): + """Configure a flask application to use OpenTelemetry. + + This activates the specific components: + + * sets tracer to the SDK's Tracer + * enables requests integration on the Tracer + * uses a WSGI middleware to enable configuration + + TODO: + + * processors? + * exporters? + * propagators? + """ + # Start by configuring all objects required to ensure + # a complete end to end workflow. + # the preferred implementation of these objects must be set, + # as the opentelemetry-api defines the interface with a no-op + # implementation. + trace.set_preferred_tracer_implementation(lambda _: Tracer()) + # Integrations are the glue that binds the OpenTelemetry API + # and the frameworks and libraries that are used together, automatically + # creating Spans and propagating context as appropriate. + opentelemetry.ext.http_requests.enable(trace.tracer()) + flask_app.wsgi_app = OpenTelemetryMiddleware(flask_app.wsgi_app) + + +app = flask.Flask(__name__) + + +@app.route("/") +def hello(): + # emit a trace that measures how long the + # sleep takes + with trace.tracer().start_span("sleep"): + time.sleep(0.001) + return "hello" + + +configure_opentelemetry(app) diff --git a/opentelemetry-example-app/tests/__init__.py b/opentelemetry-example-app/tests/__init__.py new file mode 100644 index 0000000000..d853a7bcf6 --- /dev/null +++ b/opentelemetry-example-app/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-example-app/tests/test_flask_example.py b/opentelemetry-example-app/tests/test_flask_example.py new file mode 100644 index 0000000000..ca2a237a60 --- /dev/null +++ b/opentelemetry-example-app/tests/test_flask_example.py @@ -0,0 +1,14 @@ +import unittest + +import opentelemetry_example_app.flask_example as flask_example + + +class TestFlaskExample(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.app = flask_example.app + + def test_full_path(self): + with self.app.test_client() as client: + response = client.get("/") + assert response.data.decode() == "hello" diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py index 42ff3410f0..09d3f88f41 100644 --- a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -13,9 +13,10 @@ # limitations under the License. import unittest -import opentelemetry.trace as api_trace + import opentelemetry.sdk.context.propagation.b3_format as b3_format import opentelemetry.sdk.trace as trace +import opentelemetry.trace as api_trace FORMAT = b3_format.B3Format() diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index c0a0e65008..f763293ca2 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest import mock import unittest +from unittest import mock from opentelemetry import trace as trace_api from opentelemetry.sdk import trace diff --git a/tox.ini b/tox.ini index acf1bcb367..8967adaff7 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,ext-wsgi,ext-http-requests} - pypy35-test-{api,sdk,ext-wsgi,ext-http-requests} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests} + pypy35-test-{api,sdk,example-app,ext-wsgi,ext-http-requests} lint py37-mypy docs @@ -22,6 +22,7 @@ setenv = changedir = test-api: opentelemetry-api/tests test-sdk: opentelemetry-sdk/tests + test-example-app: opentelemetry-example-app/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests @@ -30,6 +31,10 @@ commands_pre = python -m pip install -U pip setuptools wheel test: pip install {toxinidir}/opentelemetry-api test-sdk: pip install {toxinidir}/opentelemetry-sdk + example-app: pip install {toxinidir}/opentelemetry-sdk + example-app: pip install {toxinidir}/ext/opentelemetry-ext-wsgi + example-app: pip install {toxinidir}/ext/opentelemetry-ext-http-requests + example-app: pip install {toxinidir}/opentelemetry-example-app ext: pip install {toxinidir}/opentelemetry-api wsgi: pip install {toxinidir}/ext/opentelemetry-ext-wsgi http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests @@ -53,14 +58,23 @@ commands_pre = pip install -e {toxinidir}/opentelemetry-sdk pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests + pip install -e {toxinidir}/opentelemetry-example-app commands = ; Prefer putting everything in one pylint command to profit from duplication ; warnings. - black --check --diff opentelemetry-api/src/opentelemetry opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry opentelemetry-sdk/tests/ ext/opentelemetry-ext-http-requests/src/ ext/opentelemetry-ext-http-requests/tests/ ext/opentelemetry-ext-wsgi/tests/ - pylint opentelemetry-api/src/opentelemetry opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry opentelemetry-sdk/tests/ ext/opentelemetry-ext-http-requests/src/ ext/opentelemetry-ext-http-requests/tests/ ext/opentelemetry-ext-wsgi/tests/ - flake8 opentelemetry-api/src/ opentelemetry-api/tests/ opentelemetry-sdk/src/ opentelemetry-sdk/tests/ ext/opentelemetry-ext-wsgi/src/ ext/opentelemetry-ext-wsgi/tests/ ext/opentelemetry-ext-http-requests/src/ - isort --check-only --diff --recursive opentelemetry-api/src opentelemetry-sdk/src ext/opentelemetry-ext-wsgi/src ext/opentelemetry-ext-wsgi/tests ext/opentelemetry-ext-http-requests/src/ + black --check --diff . + pylint opentelemetry-api/src/opentelemetry \ + opentelemetry-api/tests/ \ + opentelemetry-sdk/src/opentelemetry \ + opentelemetry-sdk/tests/ \ + ext/opentelemetry-ext-http-requests/src/ \ + ext/opentelemetry-ext-http-requests/tests/ \ + ext/opentelemetry-ext-wsgi/tests/ \ + opentelemetry-example-app/src/opentelemetry_example_app/ \ + opentelemetry-example-app/tests/ + flake8 . + isort --check-only --diff --recursive . [testenv:docs] deps = From 5ca9e064ec03592fe0510f92619bc9268d4fb92f Mon Sep 17 00:00:00 2001 From: Aliaksei Urbanski Date: Wed, 11 Sep 2019 11:25:00 +0300 Subject: [PATCH 0064/1517] Fix skipping tests for PyPy (#133) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8967adaff7..73507c3be3 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ skipsdist = True skip_missing_interpreters = True envlist = py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests} - pypy35-test-{api,sdk,example-app,ext-wsgi,ext-http-requests} + pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests} lint py37-mypy docs From fb115680682a68df141cb42e3fa6af7bdf4c115d Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 12 Sep 2019 14:25:29 -0700 Subject: [PATCH 0065/1517] Metrics API with RFC 0003 (#87) * Create functions Comments for Meter More comments Add more comments Fix typos * fix lint * Fix lint * fix typing * Remove options, constructors, seperate labels * Consistent naming for float and int * Abstract time series * Use ABC * Fix typo * Fix docs * seperate measure classes * Add examples * fix lint * Update to RFC 0003 * Add spancontext, measurebatch * Fix docs * Fix comments * fix lint * fix lint * fix lint * skip examples * white space * fix spacing * fix imports * fix imports * LabelValues to str * Black formatting * fix isort * Remove aggregation * Fix names * Remove aggregation from docs * Fix lint --- docs/index.rst | 1 + docs/opentelemetry.metrics.rst | 14 + docs/opentelemetry.metrics.time_series.rst | 5 + .../src/opentelemetry/metrics/__init__.py | 304 ++++++++++++++++++ .../metrics/examples/pre_aggregated.py | 36 +++ .../src/opentelemetry/metrics/examples/raw.py | 45 +++ .../src/opentelemetry/metrics/time_series.py | 39 +++ 7 files changed, 444 insertions(+) create mode 100644 docs/opentelemetry.metrics.rst create mode 100644 docs/opentelemetry.metrics.time_series.rst create mode 100644 opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py create mode 100644 opentelemetry-api/src/opentelemetry/metrics/examples/raw.py create mode 100644 opentelemetry-api/src/opentelemetry/metrics/time_series.py diff --git a/docs/index.rst b/docs/index.rst index 8991f2b5df..4d968ccd64 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,6 +17,7 @@ abstract types for OpenTelemetry implementations. opentelemetry.context opentelemetry.loader + opentelemetry.metrics opentelemetry.trace diff --git a/docs/opentelemetry.metrics.rst b/docs/opentelemetry.metrics.rst new file mode 100644 index 0000000000..2d025d3197 --- /dev/null +++ b/docs/opentelemetry.metrics.rst @@ -0,0 +1,14 @@ +opentelemetry.metrics package +============================= + +Submodules +---------- + +.. toctree:: + + opentelemetry.metrics.time_series + +Module contents +--------------- + +.. automodule:: opentelemetry.metrics diff --git a/docs/opentelemetry.metrics.time_series.rst b/docs/opentelemetry.metrics.time_series.rst new file mode 100644 index 0000000000..16297d7eac --- /dev/null +++ b/docs/opentelemetry.metrics.time_series.rst @@ -0,0 +1,5 @@ +opentelemetry.metrics.time\_series module +========================================== + +.. automodule:: opentelemetry.metrics.time_series + diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index d853a7bcf6..68563bd492 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -11,3 +11,307 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +""" +The OpenTelemetry metrics API describes the classes used to report raw +measurements, as well as metrics with known aggregation and labels. + +The `Meter` class is used to construct `Metric` s to record raw statistics +as well as metrics with predefined aggregation. + +See the `metrics api`_ spec for terminology and context clarification. + +.. _metrics api: + https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-metrics.md + + +""" + +from abc import ABC, abstractmethod +from typing import List + +from opentelemetry.metrics.time_series import ( + CounterTimeSeries, + GaugeTimeSeries, + MeasureTimeSeries, +) +from opentelemetry.trace import SpanContext + +LabelKeys = List["LabelKey"] +LabelValues = List[str] + + +class Meter: + """An interface to allow the recording of metrics. + + `Metric` s are used for recording pre-defined aggregation (gauge and + counter), or raw values (measure) in which the aggregation and labels + for the exported metric are deferred. + """ + + def create_float_counter( + self, + name: str, + description: str, + unit: str, + label_keys: LabelKeys, + span_context: SpanContext = None, + ) -> "FloatCounter": + """Creates a counter type metric that contains float values. + + Args: + name: The name of the counter. + description: Human readable description of the metric. + unit: Unit of the metric values. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order MUST be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: A new `FloatCounter` + """ + + def create_int_counter( + self, + name: str, + description: str, + unit: str, + label_keys: LabelKeys, + span_context: SpanContext = None, + ) -> "IntCounter": + """Creates a counter type metric that contains int values. + + Args: + name: The name of the counter. + description: Human readable description of the metric. + unit: Unit of the metric values. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order MUST be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: + A new `IntCounter` + """ + + def create_float_gauge( + self, + name: str, + description: str, + unit: str, + label_keys: LabelKeys, + span_context: SpanContext = None, + ) -> "FloatGauge": + """Creates a gauge type metric that contains float values. + + Args: + name: The name of the counter. + description: Human readable description of the metric. + unit: Unit of the metric values. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order MUST be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: + A new `FloatGauge` + """ + + def create_int_gauge( + self, + name: str, + description: str, + unit: str, + label_keys: LabelKeys, + span_context: SpanContext = None, + ) -> "IntGauge": + """Creates a gauge type metric that contains int values. + + Args: + name: The name of the counter. + description: Human readable description of the metric. + unit: Unit of the metric values. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order MUST be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: + A new `IntGauge` + """ + + def create_int_measure( + self, + name: str, + description: str, + unit: str, + label_keys: LabelKeys, + span_context: SpanContext = None, + ) -> "IntMeasure": + """Creates a measure used to record raw int values. + + Args: + name: The name of the measure. + description: Human readable description of this measure. + unit: Unit of the measure values. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order MUST be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: + A new `IntMeasure` + """ + + def create_float_measure( + self, + name: str, + description: str, + unit: str, + label_keys: LabelKeys, + span_context: SpanContext = None, + ) -> "FloatMeasure": + """Creates a Measure used to record raw float values. + + Args: + name: the name of the measure + description: Human readable description of this measure. + unit: Unit of the measure values. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order MUST be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: + A new `FloatMeasure` + """ + + +class Metric(ABC): + """Base class for various types of metrics. + + Metric class that inherit from this class are specialized with the type of + time series that the metric holds. Metric is constructed from the meter. + """ + + @abstractmethod + def get_or_create_time_series(self, label_values: LabelValues) -> "object": + """Gets and returns a timeseries, a container for a cumulative value. + + If the provided label values are not already associated with this + metric, a new timeseries is returned, otherwise it returns the existing + timeseries with the exact label values. The timeseries returned + contains logic and behaviour specific to the type of metric that + overrides this function. + + Args: + label_values: A list of label values that will be associated + with the return timeseries. + """ + + def remove_time_series(self, label_values: LabelValues) -> None: + """Removes the timeseries from the Metric, if present. + + The timeseries with matching label values will be removed. + + args: + label_values: The list of label values to match against. + """ + + def clear(self) -> None: + """Removes all timeseries from the `Metric`.""" + + +class FloatCounter(Metric): + """A counter type metric that holds float values. + + Cumulative values can go up or stay the same, but can never go down. + Cumulative values cannot be negative. + """ + + def get_or_create_time_series( + self, label_values: LabelValues + ) -> "CounterTimeSeries": + """Gets a `CounterTimeSeries` with a cumulative float value.""" + + +class IntCounter(Metric): + """A counter type metric that holds int values. + + Cumulative values can go up or stay the same, but can never go down. + Cumulative values cannot be negative. + """ + + def get_or_create_time_series( + self, label_values: LabelValues + ) -> "CounterTimeSeries": + """Gets a `CounterTimeSeries` with a cumulative int value.""" + + +class FloatGauge(Metric): + """A gauge type metric that holds float values. + + Cumulative value can go both up and down. Values can be negative. + """ + + def get_or_create_time_series( + self, label_values: LabelValues + ) -> "GaugeTimeSeries": + """Gets a `GaugeTimeSeries` with a cumulative float value.""" + + +class IntGauge(Metric): + """A gauge type metric that holds int values. + + Cumulative value can go both up and down. Values can be negative. + """ + + def get_or_create_time_series( + self, label_values: LabelValues + ) -> "GaugeTimeSeries": + """Gets a `GaugeTimeSeries` with a cumulative int value.""" + + +class FloatMeasure(Metric): + """A measure type metric that holds float values. + + Measure metrics represent raw statistics that are recorded. + """ + + def get_or_create_time_series( + self, label_values: LabelValues + ) -> "MeasureTimeSeries": + """Gets a `MeasureTimeSeries` with a cumulated float value.""" + + +class IntMeasure(Metric): + """A measure type metric that holds int values. + + Measure metrics represent raw statistics that are recorded. + """ + + def get_or_create_time_series( + self, label_values: LabelValues + ) -> "MeasureTimeSeries": + """Gets a `MeasureTimeSeries` with a cumulated int value.""" + + +class LabelKey: + """The label keys associated with the metric. + + :type key: str + :param key: the key for the label + + :type description: str + :param description: description of the label + """ + + def __init__(self, key: str, description: str) -> None: + self.key = key + self.description = description diff --git a/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py b/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py new file mode 100644 index 0000000000..c9c55f01b8 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py @@ -0,0 +1,36 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: skip-file +from opentelemetry.metrics import LabelKey, LabelValue, Meter + +METER = Meter() +LABEL_KEYS = [ + LabelKey("environment", "the environment the application is running in") +] +COUNTER = METER.create_int_counter( + "sum numbers", # pragma: no cover + "sum numbers over time", + "number", + LABEL_KEYS, +) +LABEL_VALUE_TESTING = [LabelValue("Testing")] +LABEL_VALUE_STAGING = [LabelValue("Staging")] + +# Metrics sent to some exporter +METRIC_TESTING = COUNTER.get_or_create_time_series(LABEL_VALUE_TESTING) +METRIC_STAGING = COUNTER.get_or_create_time_series(LABEL_VALUE_STAGING) + +for i in range(100): + METRIC_STAGING.add(i) diff --git a/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py b/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py new file mode 100644 index 0000000000..3c82e14d53 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py @@ -0,0 +1,45 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: skip-file +from opentelemetry.metrics import LabelKey, LabelValue, MeasureBatch, Meter +from opentelemetry.metrics.aggregation import LastValueAggregation + +METER = Meter() +LABEL_KEYS = [ + LabelKey("environment", "the environment the application is running in") +] +MEASURE = METER.create_float_measure( + "idle_cpu_percentage", + "cpu idle over time", + "percentage", + LABEL_KEYS, + LastValueAggregation, +) +LABEL_VALUE_TESTING = [LabelValue("Testing")] +LABEL_VALUE_STAGING = [LabelValue("Staging")] + +# Metrics sent to some exporter +MEASURE_METRIC_TESTING = MEASURE.get_or_create_time_series(LABEL_VALUE_TESTING) +MEASURE_METRIC_STAGING = MEASURE.get_or_create_time_series(LABEL_VALUE_STAGING) + +# record individual measures +STATISTIC = 100 +MEASURE_METRIC_STAGING.record(STATISTIC) + +# record multiple observed values +BATCH = MeasureBatch() +BATCH.record( + [(MEASURE_METRIC_TESTING, STATISTIC), (MEASURE_METRIC_STAGING, STATISTIC)] +) diff --git a/opentelemetry-api/src/opentelemetry/metrics/time_series.py b/opentelemetry-api/src/opentelemetry/metrics/time_series.py new file mode 100644 index 0000000000..b14ef973ad --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/metrics/time_series.py @@ -0,0 +1,39 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing + + +class CounterTimeSeries: + def add(self, value: typing.Union[float, int]) -> None: + """Adds the given value to the current value. Cannot be negative.""" + + def set(self, value: typing.Union[float, int]) -> None: + """Sets the current value to the given value. + + The given value must be larger than the current recorded value. + """ + + +class GaugeTimeSeries: + def set(self, value: typing.Union[float, int]) -> None: + """Sets the current value to the given value. Can be negative.""" + + +class MeasureTimeSeries: + def record(self, value: typing.Union[float, int]) -> None: + """Records the given value to this measure. + + Logic depends on type of aggregation used for this measure. + """ From 731ed026fe077fe8a45be879aa1afcb27cfeebbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 13 Sep 2019 06:35:25 -0500 Subject: [PATCH 0066/1517] Add Link and Event classes (#130) * remove annotation for self * sdk/tests/trace/span: reorganize by member Put all actions of the same member together so it is easier to update the tests. * add Link class Make Link a class, also implement addLazyLink * add Event class Make Event a class and implement add_lazy_event --- .../src/opentelemetry/trace/__init__.py | 69 ++++++++++++++++--- opentelemetry-api/src/opentelemetry/types.py | 2 +- .../src/opentelemetry/sdk/trace/__init__.py | 39 +++++------ opentelemetry-sdk/tests/trace/test_trace.py | 58 +++++++++++----- 4 files changed, 119 insertions(+), 49 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index dc57358183..e6a7a55649 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -70,6 +70,47 @@ ParentSpan = typing.Optional[typing.Union["Span", "SpanContext"]] +class Link: + """A link to a `Span`.""" + + def __init__( + self, context: "SpanContext", attributes: types.Attributes = None + ) -> None: + self._context = context + self._attributes = attributes + + @property + def context(self) -> "SpanContext": + return self._context + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + +class Event: + """A text annotation with a set of attributes.""" + + def __init__( + self, name: str, timestamp: int, attributes: types.Attributes = None + ) -> None: + self._name = name + self._attributes = attributes + self._timestamp = timestamp + + @property + def name(self) -> str: + return self._name + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + @property + def timestamp(self) -> int: + return self._timestamp + + class Span: """A span represents a single operation within a trace.""" @@ -102,34 +143,44 @@ def get_context(self) -> "SpanContext": A :class:`.SpanContext` with a copy of this span's immutable state. """ - def set_attribute( - self: "Span", key: str, value: types.AttributeValue - ) -> None: + def set_attribute(self, key: str, value: types.AttributeValue) -> None: """Sets an Attribute. Sets a single Attribute with the key and value passed as arguments. """ def add_event( - self: "Span", name: str, attributes: types.Attributes = None + self, name: str, attributes: types.Attributes = None ) -> None: - """Adds an Event. + """Adds an `Event`. - Adds a single Event with the name and, optionally, attributes passed + Adds a single `Event` with the name and, optionally, attributes passed as arguments. """ + def add_lazy_event(self, event: Event) -> None: + """Adds an `Event`. + + Adds an `Event` that has previously been created. + """ + def add_link( - self: "Span", + self, link_target_context: "SpanContext", attributes: types.Attributes = None, ) -> None: - """Adds a Link to another span. + """Adds a `Link` to another span. - Adds a single Link from this Span to another Span identified by the + Adds a single `Link` from this Span to another Span identified by the `SpanContext` passed as argument. """ + def add_lazy_link(self, link: "Link") -> None: + """Adds a `Link` to another span. + + Adds a `Link` that has previously been created. + """ + def update_name(self, name: str) -> None: """Updates the `Span` name. diff --git a/opentelemetry-api/src/opentelemetry/types.py b/opentelemetry-api/src/opentelemetry/types.py index ce5682ee0a..28fab89389 100644 --- a/opentelemetry-api/src/opentelemetry/types.py +++ b/opentelemetry-api/src/opentelemetry/types.py @@ -16,4 +16,4 @@ import typing AttributeValue = typing.Union[str, bool, float] -Attributes = typing.Dict[str, AttributeValue] +Attributes = typing.Optional[typing.Dict[str, AttributeValue]] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 21acb39494..a6f70589b3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -16,7 +16,7 @@ import random import threading import typing -from collections import OrderedDict, deque, namedtuple +from collections import OrderedDict, deque from contextlib import contextmanager from opentelemetry import trace as trace_api @@ -140,11 +140,6 @@ def from_map(cls, maxlen, mapping): return bounded_dict -Event = namedtuple("Event", ("name", "attributes")) - -Link = namedtuple("Link", ("context", "attributes")) - - class SpanProcessor: """Interface which allows hooks for SDK's `Span`s start and end method invocations. @@ -233,7 +228,7 @@ class Span(trace_api.Span): empty_links = BoundedList(MAX_NUM_LINKS) def __init__( - self: "Span", + self, name: str, context: "trace_api.SpanContext", parent: trace_api.ParentSpan = None, @@ -241,8 +236,8 @@ def __init__( trace_config=None, # TODO resource=None, # TODO attributes: types.Attributes = None, # TODO - events: typing.Sequence[Event] = None, # TODO - links: typing.Sequence[Link] = None, # TODO + events: typing.Sequence[trace_api.Event] = None, # TODO + links: typing.Sequence[trace_api.Link] = None, # TODO span_processor: SpanProcessor = SpanProcessor(), ) -> None: @@ -283,32 +278,36 @@ def __repr__(self): def get_context(self): return self.context - def set_attribute( - self: "Span", key: str, value: types.AttributeValue - ) -> None: + def set_attribute(self, key: str, value: types.AttributeValue) -> None: if self.attributes is Span.empty_attributes: self.attributes = BoundedDict(MAX_NUM_ATTRIBUTES) self.attributes[key] = value def add_event( - self: "Span", name: str, attributes: types.Attributes = None + self, name: str, attributes: types.Attributes = None ) -> None: - if self.events is Span.empty_events: - self.events = BoundedList(MAX_NUM_EVENTS) if attributes is None: attributes = Span.empty_attributes - self.events.append(Event(name, attributes)) + self.add_lazy_event(trace_api.Event(name, util.time_ns(), attributes)) + + def add_lazy_event(self, event: trace_api.Event) -> None: + if self.events is Span.empty_events: + self.events = BoundedList(MAX_NUM_EVENTS) + self.events.append(event) def add_link( - self: "Span", + self, link_target_context: "trace_api.SpanContext", attributes: types.Attributes = None, ) -> None: - if self.links is Span.empty_links: - self.links = BoundedList(MAX_NUM_LINKS) if attributes is None: attributes = Span.empty_attributes - self.links.append(Link(link_target_context, attributes)) + self.add_lazy_link(trace_api.Link(link_target_context, attributes)) + + def add_lazy_link(self, link: "trace_api.Link") -> None: + if self.links is Span.empty_links: + self.links = BoundedList(MAX_NUM_LINKS) + self.links.append(link) def start(self): if self.start_time is None: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index f763293ca2..63253eb2bd 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -17,6 +17,7 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import trace +from opentelemetry.sdk import util class TestTracer(unittest.TestCase): @@ -126,10 +127,15 @@ def test_span_members(self): trace_id=trace.generate_trace_id(), span_id=trace.generate_span_id(), ) + other_context3 = trace_api.SpanContext( + trace_id=trace.generate_trace_id(), + span_id=trace.generate_span_id(), + ) self.assertIsNone(tracer.get_current_span()) with tracer.start_span("root") as root: + # attributes root.set_attribute("component", "http") root.set_attribute("http.method", "GET") root.set_attribute( @@ -144,18 +150,6 @@ def test_span_members(self): root.set_attribute("attr-key", "attr-value1") root.set_attribute("attr-key", "attr-value2") - root.add_event("event0") - root.add_event("event1", {"name": "birthday"}) - - root.add_link(other_context1) - root.add_link(other_context2, {"name": "neighbor"}) - - root.update_name("toor") - self.assertEqual(root.name, "toor") - - # The public API does not expose getters. - # Checks by accessing the span members directly - self.assertEqual(len(root.attributes), 7) self.assertEqual(root.attributes["component"], "http") self.assertEqual(root.attributes["http.method"], "GET") @@ -168,16 +162,34 @@ def test_span_members(self): self.assertEqual(root.attributes["misc.pi"], 3.14) self.assertEqual(root.attributes["attr-key"], "attr-value2") - self.assertEqual(len(root.events), 2) - self.assertEqual( - root.events[0], trace.Event(name="event0", attributes={}) + # events + root.add_event("event0") + root.add_event("event1", {"name": "birthday"}) + now = util.time_ns() + root.add_lazy_event( + trace_api.Event("event2", now, {"name": "hello"}) ) - self.assertEqual( - root.events[1], - trace.Event(name="event1", attributes={"name": "birthday"}), + + self.assertEqual(len(root.events), 3) + + self.assertEqual(root.events[0].name, "event0") + self.assertEqual(root.events[0].attributes, {}) + + self.assertEqual(root.events[1].name, "event1") + self.assertEqual(root.events[1].attributes, {"name": "birthday"}) + + self.assertEqual(root.events[2].name, "event2") + self.assertEqual(root.events[2].attributes, {"name": "hello"}) + self.assertEqual(root.events[2].timestamp, now) + + # links + root.add_link(other_context1) + root.add_link(other_context2, {"name": "neighbor"}) + root.add_lazy_link( + trace_api.Link(other_context3, {"component": "http"}) ) - self.assertEqual(len(root.links), 2) + self.assertEqual(len(root.links), 3) self.assertEqual( root.links[0].context.trace_id, other_context1.trace_id ) @@ -192,6 +204,14 @@ def test_span_members(self): root.links[1].context.span_id, other_context2.span_id ) self.assertEqual(root.links[1].attributes, {"name": "neighbor"}) + self.assertEqual( + root.links[2].context.span_id, other_context3.span_id + ) + self.assertEqual(root.links[2].attributes, {"component": "http"}) + + # name + root.update_name("toor") + self.assertEqual(root.name, "toor") class TestSpan(unittest.TestCase): From d5e5471f3da59ad9a614197ee51c8b054a14bbd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 13 Sep 2019 06:44:22 -0500 Subject: [PATCH 0067/1517] sdk: add Exporter interface, SimpleSpanProcessor and InMemorySpanExporter (#119) * sdk/trace: add Exporter interface and SimpleExportSpanProcessor Exporter is an interface that allows different services to export recorded spans in its own format. SimpleExportSpanProcessor is an implementation of SpanProcessor that passes ended spans directly to a configured Exporter. The current interface for exporters directly receives an SDK Span, it could be improved in the future to receive a different object containing a representation of the Span. * sdk/trace/exporter: add InMemorySpanExporter InMemorySpanExporter is a simple implementation of the Exporter interface that saves the exported spans in a list in memory. This class is useful for testing purposes. --- .../sdk/trace/export/__init__.py | 78 +++++++++++++++ .../trace/export/in_memory_span_exporter.py | 58 +++++++++++ .../tests/trace/export/__init__.py | 13 +++ .../tests/trace/export/test_export.py | 44 +++++++++ .../export/test_in_memory_span_exporter.py | 97 +++++++++++++++++++ 5 files changed, 290 insertions(+) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py create mode 100644 opentelemetry-sdk/tests/trace/export/__init__.py create mode 100644 opentelemetry-sdk/tests/trace/export/test_export.py create mode 100644 opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py new file mode 100644 index 0000000000..0c011f9976 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -0,0 +1,78 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import typing +from enum import Enum + +from .. import Span, SpanProcessor + +logger = logging.getLogger(__name__) + + +class SpanExportResult(Enum): + SUCCESS = 0 + FAILED_RETRYABLE = 1 + FAILED_NOT_RETRYABLE = 2 + + +class SpanExporter: + """Interface for exporting spans. + + Interface to be implemented by services that want to export recorded in + its own format. + + To export data this MUST be registered to the :class`..Tracer` using a + `SimpleExportSpanProcessor` or a `BatchSpanProcessor`. + """ + + def export(self, spans: typing.Sequence[Span]) -> "SpanExportResult": + """Exports a batch of telemetry data. + + Args: + spans: The list of `Span`s to be exported + + Returns: + The result of the export + """ + + def shutdown(self) -> None: + """Shuts down the exporter. + + Called when the SDK is shut down. + """ + + +class SimpleExportSpanProcessor(SpanProcessor): + """Simple SpanProcessor implementation. + + SimpleExportSpanProcessor is an implementation of `SpanProcessor` that + passes ended spans directly to the configured `SpanExporter`. + """ + + def __init__(self, span_exporter: SpanExporter): + self.span_exporter = span_exporter + + def on_start(self, span: Span) -> None: + pass + + def on_end(self, span: Span) -> None: + try: + self.span_exporter.export((span,)) + # pylint: disable=broad-except + except Exception as exc: + logger.warning("Exception while exporting data: %s", exc) + + def shutdown(self) -> None: + self.span_exporter.shutdown() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py new file mode 100644 index 0000000000..b536120560 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py @@ -0,0 +1,58 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import typing + +from .. import Span +from . import SpanExporter, SpanExportResult + + +class InMemorySpanExporter(SpanExporter): + """Implementation of :class:`.Exporter` that stores spans in memory. + + This class can be used for testing purposes. It stores the exported spans + in a list in memory that can be retrieved using the + :func:`.get_finished_spans` method. + """ + + def __init__(self): + self._finished_spans = [] + self._stopped = False + self._lock = threading.Lock() + + def clear(self): + """Clear list of collected spans.""" + with self._lock: + self._finished_spans.clear() + + def get_finished_spans(self): + """Get list of collected spans.""" + with self._lock: + return tuple(self._finished_spans) + + def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: + """Stores a list of spans in memory.""" + if self._stopped: + return SpanExportResult.FAILED_NOT_RETRYABLE + with self._lock: + self._finished_spans.extend(spans) + return SpanExportResult.SUCCESS + + def shutdown(self): + """Shut downs the exporter. + + Calls to export after the exporter has been shut down will fail. + """ + self._stopped = True diff --git a/opentelemetry-sdk/tests/trace/export/__init__.py b/opentelemetry-sdk/tests/trace/export/__init__.py new file mode 100644 index 0000000000..d853a7bcf6 --- /dev/null +++ b/opentelemetry-sdk/tests/trace/export/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py new file mode 100644 index 0000000000..ef9786ca63 --- /dev/null +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -0,0 +1,44 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace import export + + +class TestSimpleExportSpanProcessor(unittest.TestCase): + def test_simple_span_processor(self): + class MySpanExporter(export.SpanExporter): + def __init__(self, destination): + self.destination = destination + + def export(self, spans: trace.Span) -> export.SpanExportResult: + self.destination.extend(span.name for span in spans) + return export.SpanExportResult.SUCCESS + + tracer = trace.Tracer() + + spans_names_list = [] + + my_exporter = MySpanExporter(destination=spans_names_list) + span_processor = export.SimpleExportSpanProcessor(my_exporter) + tracer.add_span_processor(span_processor) + + with tracer.start_span("foo"): + with tracer.start_span("bar"): + with tracer.start_span("xxx"): + pass + + self.assertListEqual(["xxx", "bar", "foo"], spans_names_list) diff --git a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py new file mode 100644 index 0000000000..01d4487e2e --- /dev/null +++ b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py @@ -0,0 +1,97 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from opentelemetry import trace as trace_api +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace import export +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + + +class TestInMemorySpanExporter(unittest.TestCase): + def test_get_finished_spans(self): + tracer = trace.Tracer() + + memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(memory_exporter) + tracer.add_span_processor(span_processor) + + with tracer.start_span("foo"): + with tracer.start_span("bar"): + with tracer.start_span("xxx"): + pass + + span_list = memory_exporter.get_finished_spans() + spans_names_list = [span.name for span in span_list] + self.assertListEqual(["xxx", "bar", "foo"], spans_names_list) + + def test_clear(self): + tracer = trace.Tracer() + + memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(memory_exporter) + tracer.add_span_processor(span_processor) + + with tracer.start_span("foo"): + with tracer.start_span("bar"): + with tracer.start_span("xxx"): + pass + + memory_exporter.clear() + span_list = memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + + def test_shutdown(self): + tracer = trace.Tracer() + + memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(memory_exporter) + tracer.add_span_processor(span_processor) + + with tracer.start_span("foo"): + with tracer.start_span("bar"): + with tracer.start_span("xxx"): + pass + + span_list = memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 3) + + memory_exporter.shutdown() + + # after shutdown no new spans are accepted + with tracer.start_span("foo"): + with tracer.start_span("bar"): + with tracer.start_span("xxx"): + pass + + span_list = memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 3) + + def test_return_code(self): + span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + span_list = (span,) + memory_exporter = InMemorySpanExporter() + + ret = memory_exporter.export(span_list) + self.assertEqual(ret, export.SpanExportResult.SUCCESS) + + memory_exporter.shutdown() + + # after shutdown export should fail + ret = memory_exporter.export(span_list) + self.assertEqual(ret, export.SpanExportResult.FAILED_NOT_RETRYABLE) From 876f34c5b46e0466045aee058fb7826cba806d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Fri, 13 Sep 2019 22:11:48 +0200 Subject: [PATCH 0068/1517] Fix lint build after merging #130 (#135) --- opentelemetry-sdk/tests/trace/test_trace.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 63253eb2bd..1cbaa15237 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -16,8 +16,7 @@ from unittest import mock from opentelemetry import trace as trace_api -from opentelemetry.sdk import trace -from opentelemetry.sdk import util +from opentelemetry.sdk import trace, util class TestTracer(unittest.TestCase): From 46ae4158dbab6e80e0f4a937b8686e3566102fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 16 Sep 2019 02:51:11 -0500 Subject: [PATCH 0069/1517] sdk/trace/span: comply with concurrency specification (#131) * sdk/trace/span: comply with concurrency specification According to the spec [1], all methods in span must be thread safe. This commit guarantees that by adding a lock in the places it was missing. [1] https://github.com/open-telemetry/opentelemetry-specification/blob/d34318277b1999edccf5ab1d63024ec172ae5e49/specification/concurrency.md * sdk/trace/span: ignore call to methods after span is ended Calls to methods like set_attribute, add_event, add_link and so on should not happen after the span has been ended. This commit introduces some checks to ignore and print a warning when those calls happen. * avoid holding a lock while logging --- .../src/opentelemetry/sdk/trace/__init__.py | 64 +++++++++++++++---- opentelemetry-sdk/tests/trace/test_trace.py | 56 ++++++++++++++-- 2 files changed, 104 insertions(+), 16 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index a6f70589b3..8d469eae0a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. +import logging import random import threading import typing @@ -24,6 +25,8 @@ from opentelemetry.context import Context from opentelemetry.sdk import util +logger = logging.getLogger(__name__) + try: # pylint: disable=ungrouped-imports from collections.abc import MutableMapping @@ -251,6 +254,7 @@ def __init__( self.events = events self.links = links self.span_processor = span_processor + self._lock = threading.Lock() if attributes is None: self.attributes = Span.empty_attributes @@ -279,8 +283,14 @@ def get_context(self): return self.context def set_attribute(self, key: str, value: types.AttributeValue) -> None: - if self.attributes is Span.empty_attributes: - self.attributes = BoundedDict(MAX_NUM_ATTRIBUTES) + with self._lock: + has_ended = self.end_time is not None + if not has_ended: + if self.attributes is Span.empty_attributes: + self.attributes = BoundedDict(MAX_NUM_ATTRIBUTES) + if has_ended: + logger.warning("Setting attribute on ended span.") + return self.attributes[key] = value def add_event( @@ -291,8 +301,14 @@ def add_event( self.add_lazy_event(trace_api.Event(name, util.time_ns(), attributes)) def add_lazy_event(self, event: trace_api.Event) -> None: - if self.events is Span.empty_events: - self.events = BoundedList(MAX_NUM_EVENTS) + with self._lock: + has_ended = self.end_time is not None + if not has_ended: + if self.events is Span.empty_events: + self.events = BoundedList(MAX_NUM_EVENTS) + if has_ended: + logger.warning("Calling add_event() on an ended span.") + return self.events.append(event) def add_link( @@ -305,21 +321,45 @@ def add_link( self.add_lazy_link(trace_api.Link(link_target_context, attributes)) def add_lazy_link(self, link: "trace_api.Link") -> None: - if self.links is Span.empty_links: - self.links = BoundedList(MAX_NUM_LINKS) + with self._lock: + has_ended = self.end_time is not None + if not has_ended: + if self.links is Span.empty_links: + self.links = BoundedList(MAX_NUM_LINKS) + if has_ended: + logger.warning("Calling add_link() on an ended span.") + return self.links.append(link) def start(self): - if self.start_time is None: - self.start_time = util.time_ns() - self.span_processor.on_start(self) + with self._lock: + has_started = self.start_time is not None + if not has_started: + self.start_time = util.time_ns() + if has_started: + logger.warning("Calling start() on a started span.") + return + self.span_processor.on_start(self) def end(self): - if self.end_time is None: - self.end_time = util.time_ns() - self.span_processor.on_end(self) + with self._lock: + if self.start_time is None: + raise RuntimeError("Calling end() on a not started span.") + has_ended = self.end_time is not None + if not has_ended: + self.end_time = util.time_ns() + if has_ended: + logger.warning("Calling end() on an ended span.") + return + + self.span_processor.on_end(self) def update_name(self, name: str) -> None: + with self._lock: + has_ended = self.end_time is not None + if has_ended: + logger.warning("Calling update_name() on an ended span.") + return self.name = name diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 1cbaa15237..c8c7142235 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -115,6 +115,12 @@ def test_start_span_explicit(self): self.assertIs(tracer.get_current_span(), root) self.assertIsNotNone(child.end_time) + +class TestSpan(unittest.TestCase): + def test_basic_span(self): + span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + self.assertEqual(span.name, "name") + def test_span_members(self): tracer = trace.Tracer("test_span_members") @@ -212,11 +218,53 @@ def test_span_members(self): root.update_name("toor") self.assertEqual(root.name, "toor") - -class TestSpan(unittest.TestCase): - def test_basic_span(self): + def test_start_span(self): + """Start twice, end a not started""" span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) - self.assertEqual(span.name, "name") + + # end not started span + self.assertRaises(RuntimeError, span.end) + + span.start() + start_time = span.start_time + span.start() + self.assertEqual(start_time, span.start_time) + + def test_ended_span(self): + """"Events, attributes are not allowed after span is ended""" + tracer = trace.Tracer("test_ended_span") + + other_context1 = trace_api.SpanContext( + trace_id=trace.generate_trace_id(), + span_id=trace.generate_span_id(), + ) + + with tracer.start_span("root") as root: + # everything should be empty at the beginning + self.assertEqual(len(root.attributes), 0) + self.assertEqual(len(root.events), 0) + self.assertEqual(len(root.links), 0) + + # call end first time + root.end() + end_time0 = root.end_time + + # call it a second time + root.end() + # end time shouldn't be changed + self.assertEqual(end_time0, root.end_time) + + root.set_attribute("component", "http") + self.assertEqual(len(root.attributes), 0) + + root.add_event("event1") + self.assertEqual(len(root.events), 0) + + root.add_link(other_context1) + self.assertEqual(len(root.links), 0) + + root.update_name("xxx") + self.assertEqual(root.name, "root") def span_event_start_fmt(span_processor_name, span_name): From 531d0b166eb3102e0f4c38b30ceb69ecf66be1dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 16 Sep 2019 11:19:34 +0200 Subject: [PATCH 0070/1517] Ensure that type info is picked up from installed package. (#124) * Ensure that type info is picked up from installed package. This required moving all top-level modules to a sub-package, to not create collisions with py.typed marker files. See https://www.python.org/dev/peps/pep-0561/#packaging-type-information * Add MANIFEST.in for SDK package. --- docs/index.rst | 3 ++- docs/opentelemetry.loader.rst | 4 ---- docs/opentelemetry.util.loader.rst | 4 ++++ opentelemetry-api/MANIFEST.in | 7 +++++++ opentelemetry-api/setup.py | 4 +++- .../src/opentelemetry/context/py.typed | 0 .../opentelemetry/distributedcontext/py.typed | 0 .../src/opentelemetry/logs/py.typed | 0 .../src/opentelemetry/metrics/py.typed | 0 .../src/opentelemetry/resources/py.typed | 0 .../src/opentelemetry/trace/__init__.py | 6 +++--- .../src/opentelemetry/trace/py.typed | 0 .../src/opentelemetry/{ => util}/loader.py | 0 .../src/opentelemetry/util/py.typed | 0 .../src/opentelemetry/{ => util}/types.py | 0 .../src/opentelemetry/{ => util}/version.py | 0 opentelemetry-api/tests/mypysmoke.py | 5 +++++ opentelemetry-api/tests/test_loader.py | 3 ++- opentelemetry-sdk/MANIFEST.in | 7 +++++++ .../src/opentelemetry/sdk/trace/__init__.py | 2 +- tox.ini | 16 +++++++++++++--- 21 files changed, 47 insertions(+), 14 deletions(-) delete mode 100644 docs/opentelemetry.loader.rst create mode 100644 docs/opentelemetry.util.loader.rst create mode 100644 opentelemetry-api/MANIFEST.in create mode 100644 opentelemetry-api/src/opentelemetry/context/py.typed create mode 100644 opentelemetry-api/src/opentelemetry/distributedcontext/py.typed create mode 100644 opentelemetry-api/src/opentelemetry/logs/py.typed create mode 100644 opentelemetry-api/src/opentelemetry/metrics/py.typed create mode 100644 opentelemetry-api/src/opentelemetry/resources/py.typed create mode 100644 opentelemetry-api/src/opentelemetry/trace/py.typed rename opentelemetry-api/src/opentelemetry/{ => util}/loader.py (100%) create mode 100644 opentelemetry-api/src/opentelemetry/util/py.typed rename opentelemetry-api/src/opentelemetry/{ => util}/types.py (100%) rename opentelemetry-api/src/opentelemetry/{ => util}/version.py (100%) create mode 100644 opentelemetry-api/tests/mypysmoke.py create mode 100644 opentelemetry-sdk/MANIFEST.in diff --git a/docs/index.rst b/docs/index.rst index 4d968ccd64..b4a3398478 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,9 +16,10 @@ abstract types for OpenTelemetry implementations. :caption: Contents: opentelemetry.context - opentelemetry.loader + opentelemetry.util.loader opentelemetry.metrics opentelemetry.trace + opentelemetry.util.loader Indices and tables diff --git a/docs/opentelemetry.loader.rst b/docs/opentelemetry.loader.rst deleted file mode 100644 index bd6dd698f8..0000000000 --- a/docs/opentelemetry.loader.rst +++ /dev/null @@ -1,4 +0,0 @@ -opentelemetry.loader module -=========================== - -.. automodule:: opentelemetry.loader diff --git a/docs/opentelemetry.util.loader.rst b/docs/opentelemetry.util.loader.rst new file mode 100644 index 0000000000..079d2e4a38 --- /dev/null +++ b/docs/opentelemetry.util.loader.rst @@ -0,0 +1,4 @@ +opentelemetry.util.loader module +================================ + +.. automodule:: opentelemetry.util.loader diff --git a/opentelemetry-api/MANIFEST.in b/opentelemetry-api/MANIFEST.in new file mode 100644 index 0000000000..191b7d1959 --- /dev/null +++ b/opentelemetry-api/MANIFEST.in @@ -0,0 +1,7 @@ +prune tests +graft src +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include MANIFEST.in +include README.rst diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index d9e900465a..eff8230fc2 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -17,7 +17,9 @@ import setuptools BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join(BASE_DIR, "src", "opentelemetry", "version.py") +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "util", "version.py" +) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: exec(f.read(), PACKAGE_INFO) diff --git a/opentelemetry-api/src/opentelemetry/context/py.typed b/opentelemetry-api/src/opentelemetry/context/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/py.typed b/opentelemetry-api/src/opentelemetry/distributedcontext/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-api/src/opentelemetry/logs/py.typed b/opentelemetry-api/src/opentelemetry/logs/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-api/src/opentelemetry/metrics/py.typed b/opentelemetry-api/src/opentelemetry/metrics/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-api/src/opentelemetry/resources/py.typed b/opentelemetry-api/src/opentelemetry/resources/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index e6a7a55649..2be60151af 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -64,7 +64,7 @@ import typing from contextlib import contextmanager -from opentelemetry import loader, types +from opentelemetry.util import loader, types # TODO: quarantine ParentSpan = typing.Optional[typing.Union["Span", "SpanContext"]] @@ -445,7 +445,7 @@ def use_span(self, span: "Span") -> typing.Iterator[None]: # Once https://github.com/python/mypy/issues/7092 is resolved, # the following type definition should be replaced with -# from opentelemetry.loader import ImplementationFactory +# from opentelemetry.util.loader import ImplementationFactory ImplementationFactory = typing.Callable[ [typing.Type[Tracer]], typing.Optional[Tracer] ] @@ -474,7 +474,7 @@ def set_preferred_tracer_implementation( ) -> None: """Set the factory to be used to create the tracer. - See :mod:`opentelemetry.loader` for details. + See :mod:`opentelemetry.util.loader` for details. This function may not be called after a tracer is already loaded. diff --git a/opentelemetry-api/src/opentelemetry/trace/py.typed b/opentelemetry-api/src/opentelemetry/trace/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-api/src/opentelemetry/loader.py b/opentelemetry-api/src/opentelemetry/util/loader.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/loader.py rename to opentelemetry-api/src/opentelemetry/util/loader.py diff --git a/opentelemetry-api/src/opentelemetry/util/py.typed b/opentelemetry-api/src/opentelemetry/util/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-api/src/opentelemetry/types.py b/opentelemetry-api/src/opentelemetry/util/types.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/types.py rename to opentelemetry-api/src/opentelemetry/util/types.py diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/util/version.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/version.py rename to opentelemetry-api/src/opentelemetry/util/version.py diff --git a/opentelemetry-api/tests/mypysmoke.py b/opentelemetry-api/tests/mypysmoke.py new file mode 100644 index 0000000000..25601ebf84 --- /dev/null +++ b/opentelemetry-api/tests/mypysmoke.py @@ -0,0 +1,5 @@ +import opentelemetry.trace + + +def dummy_check_mypy_returntype() -> opentelemetry.trace.Tracer: + return opentelemetry.trace.tracer() diff --git a/opentelemetry-api/tests/test_loader.py b/opentelemetry-api/tests/test_loader.py index 9c75329b83..942479ab7d 100644 --- a/opentelemetry-api/tests/test_loader.py +++ b/opentelemetry-api/tests/test_loader.py @@ -17,7 +17,8 @@ import unittest from importlib import reload -from opentelemetry import loader, trace +from opentelemetry import trace +from opentelemetry.util import loader DUMMY_TRACER = None diff --git a/opentelemetry-sdk/MANIFEST.in b/opentelemetry-sdk/MANIFEST.in new file mode 100644 index 0000000000..191b7d1959 --- /dev/null +++ b/opentelemetry-sdk/MANIFEST.in @@ -0,0 +1,7 @@ +prune tests +graft src +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include MANIFEST.in +include README.rst diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 8d469eae0a..23f368955b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -21,9 +21,9 @@ from contextlib import contextmanager from opentelemetry import trace as trace_api -from opentelemetry import types from opentelemetry.context import Context from opentelemetry.sdk import util +from opentelemetry.util import types logger = logging.getLogger(__name__) diff --git a/tox.ini b/tox.ini index 73507c3be3..d0fb6cc2dc 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests} pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests} lint - py37-mypy + py37-{mypy,mypyinstalled} docs [travis] @@ -14,7 +14,7 @@ python = [testenv] deps = - mypy: mypy~=0.711 + mypy,mypyinstalled: mypy~=0.711 setenv = mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ @@ -39,11 +39,21 @@ commands_pre = wsgi: pip install {toxinidir}/ext/opentelemetry-ext-wsgi http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests +; Using file:// here because otherwise tox invokes just "pip install +; opentelemetry-api", leading to an error + mypyinstalled: pip install file://{toxinidir}/opentelemetry-api/ + commands = + test: python -m unittest discover + mypy: mypy --namespace-packages opentelemetry-api/src/opentelemetry/ ; For test code, we don't want to enforce the full mypy strictness mypy: mypy --namespace-packages --config-file=mypy-relaxed.ini opentelemetry-api/tests/ - test: python -m unittest discover + +; Test that mypy can pick up typeinfo from an installed package (otherwise, +; implicit Any due to unfollowed import would result). + mypyinstalled: mypy --namespace-packages opentelemetry-api/tests/mypysmoke.py --strict + [testenv:lint] basepython: python3.7 From be7577d6f2f2530433278e7393050ac6fe71b581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 16 Sep 2019 19:06:09 +0200 Subject: [PATCH 0071/1517] Fix duplicate doc entry (mismerge of #124). (#138) --- docs/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index b4a3398478..f4c8e6e336 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,7 +16,6 @@ abstract types for OpenTelemetry implementations. :caption: Contents: opentelemetry.context - opentelemetry.util.loader opentelemetry.metrics opentelemetry.trace opentelemetry.util.loader From b6e97fc63bfc341fc9a06d66bb049ec8cbfad4b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 18 Sep 2019 08:09:10 +0200 Subject: [PATCH 0072/1517] span: add is_recording_events (#141) --- .../src/opentelemetry/trace/__init__.py | 7 +++++++ .../src/opentelemetry/sdk/trace/__init__.py | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 2be60151af..22489333b9 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -191,6 +191,13 @@ def update_name(self, name: str) -> None: on the implementation. """ + def is_recording_events(self) -> bool: + """Returns whether this span will be recorded. + + Returns true if this Span is active and recording information like + events with the add_event operation and attributes using set_attribute. + """ + class TraceOptions(int): """A bitmask that represents options specific to the trace. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 23f368955b..0d4ae83185 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -284,6 +284,8 @@ def get_context(self): def set_attribute(self, key: str, value: types.AttributeValue) -> None: with self._lock: + if not self.is_recording_events(): + return has_ended = self.end_time is not None if not has_ended: if self.attributes is Span.empty_attributes: @@ -302,6 +304,8 @@ def add_event( def add_lazy_event(self, event: trace_api.Event) -> None: with self._lock: + if not self.is_recording_events(): + return has_ended = self.end_time is not None if not has_ended: if self.events is Span.empty_events: @@ -322,6 +326,8 @@ def add_link( def add_lazy_link(self, link: "trace_api.Link") -> None: with self._lock: + if not self.is_recording_events(): + return has_ended = self.end_time is not None if not has_ended: if self.links is Span.empty_links: @@ -333,6 +339,8 @@ def add_lazy_link(self, link: "trace_api.Link") -> None: def start(self): with self._lock: + if not self.is_recording_events(): + return has_started = self.start_time is not None if not has_started: self.start_time = util.time_ns() @@ -343,6 +351,8 @@ def start(self): def end(self): with self._lock: + if not self.is_recording_events(): + return if self.start_time is None: raise RuntimeError("Calling end() on a not started span.") has_ended = self.end_time is not None @@ -362,6 +372,9 @@ def update_name(self, name: str) -> None: return self.name = name + def is_recording_events(self) -> bool: + return True + def generate_span_id(): """Get a new random span ID. From dbb3be802bae8e4e5c36748869dbc789e50de217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 18 Sep 2019 08:15:22 +0200 Subject: [PATCH 0073/1517] Add SpanKind option at span creation (closes #103). (#139) --- .../ext/http_requests/__init__.py | 7 ++-- .../tests/test_requests_integration.py | 4 +- .../src/opentelemetry/ext/wsgi/__init__.py | 2 +- .../tests/test_wsgi_middleware.py | 4 +- .../src/opentelemetry/trace/__init__.py | 42 ++++++++++++++++++- .../src/opentelemetry/sdk/trace/__init__.py | 8 +++- opentelemetry-sdk/tests/trace/test_trace.py | 6 ++- 7 files changed, 62 insertions(+), 11 deletions(-) diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index 2689b8ea2d..d06e78b052 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -22,6 +22,8 @@ from requests.sessions import Session +from opentelemetry.trace import SpanKind + # NOTE: Currently we force passing a tracer. But in turn, this forces the user # to configure a SDK before enabling this integration. In turn, this means that @@ -61,11 +63,8 @@ def instrumented_request(self, method, url, *args, **kwargs): path = "" path = parsed_url.path - with tracer.start_span(path) as span: + with tracer.start_span(path, kind=SpanKind.CLIENT) as span: span.set_attribute("component", "http") - # TODO: The OTel spec says "SpanKind" MUST be "Client" but that - # seems to be a leftover, as Spans have no explicit field for - # kind. span.set_attribute("http.method", method.upper()) span.set_attribute("http.url", url) diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index fac5435ccc..d2624f63d1 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -56,7 +56,9 @@ def test_basic(self): url = "https://www.example.org/foo/bar?x=y#top" requests.get(url=url) self.assertEqual(1, len(self.send.call_args_list)) - self.tracer.start_span.assert_called_with("/foo/bar") + self.tracer.start_span.assert_called_with( + "/foo/bar", kind=trace.SpanKind.CLIENT + ) self.span_context_manager.__enter__.assert_called_with() self.span_context_manager.__exit__.assert_called_with(None, None, None) self.assertEqual( diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 974a6998a8..a3120c2c0a 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -88,7 +88,7 @@ def __call__(self, environ, start_response): tracer = trace.tracer() path_info = environ["PATH_INFO"] or "/" - with tracer.start_span(path_info) as span: + with tracer.start_span(path_info, kind=trace.SpanKind.SERVER) as span: self._add_request_attributes(span, environ) start_response = self._create_start_response(span, start_response) diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index b423224ade..73bba50452 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -125,7 +125,9 @@ def validate_response(self, response, error=None): self.assertIsNone(self.exc_info) # Verify that start_span has been called - self.start_span.assert_called_once_with("/") + self.start_span.assert_called_once_with( + "/", kind=trace_api.SpanKind.SERVER + ) def test_basic_wsgi_call(self): app = OpenTelemetryMiddleware(simple_wsgi) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 22489333b9..b79cdeb4df 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -61,6 +61,7 @@ .. versionadded:: 0.1.0 """ +import enum import typing from contextlib import contextmanager @@ -111,6 +112,33 @@ def timestamp(self) -> int: return self._timestamp +class SpanKind(enum.Enum): + """Specifies additional details on how this span relates to its parent span. + + Note that this enumeration is experimental and likely to change. See + https://github.com/open-telemetry/opentelemetry-specification/pull/226. + """ + + #: Default value. Indicates that the span is used internally in the application. + INTERNAL = 0 + + #: Indicates that the span describes an operation that handles a remote request. + SERVER = 1 + + #: Indicates that the span describes a request to some remote service. + CLIENT = 2 + + #: Indicates that the span describes a producer sending a message to a + #: broker. Unlike client and server, there is usually no direct critical + #: path latency relationship between producer and consumer spans. + PRODUCER = 3 + + #: Indicates that the span describes consumer receiving a message from a + #: broker. Unlike client and server, there is usually no direct critical + #: path latency relationship between producer and consumer spans. + CONSUMER = 4 + + class Span: """A span represents a single operation within a trace.""" @@ -350,7 +378,10 @@ def get_current_span(self) -> "Span": @contextmanager # type: ignore def start_span( - self, name: str, parent: ParentSpan = CURRENT_SPAN + self, + name: str, + parent: ParentSpan = CURRENT_SPAN, + kind: SpanKind = SpanKind.INTERNAL, ) -> typing.Iterator["Span"]: """Context manager for span creation. @@ -390,6 +421,8 @@ def start_span( Args: name: The name of the span to be created. parent: The span's parent. Defaults to the current span. + kind: The span's kind (relationship to parent). Note that is + meaningful even if there is no parent. Yields: The newly-created span. @@ -398,7 +431,10 @@ def start_span( yield INVALID_SPAN def create_span( - self, name: str, parent: ParentSpan = CURRENT_SPAN + self, + name: str, + parent: ParentSpan = CURRENT_SPAN, + kind: SpanKind = SpanKind.INTERNAL, ) -> "Span": """Creates a span. @@ -426,6 +462,8 @@ def create_span( Args: name: The name of the span to be created. parent: The span's parent. Defaults to the current span. + kind: The span's kind (relationship to parent). Note that is + meaningful even if there is no parent. Returns: The newly-created span. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0d4ae83185..a694476e1f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -241,6 +241,7 @@ def __init__( attributes: types.Attributes = None, # TODO events: typing.Sequence[trace_api.Event] = None, # TODO links: typing.Sequence[trace_api.Link] = None, # TODO + kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, span_processor: SpanProcessor = SpanProcessor(), ) -> None: @@ -253,6 +254,8 @@ def __init__( self.attributes = attributes self.events = events self.links = links + self.kind = kind + self.span_processor = span_processor self._lock = threading.Lock() @@ -417,15 +420,17 @@ def start_span( self, name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, + kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, ) -> typing.Iterator["Span"]: """See `opentelemetry.trace.Tracer.start_span`.""" - with self.use_span(self.create_span(name, parent)) as span: + with self.use_span(self.create_span(name, parent, kind)) as span: yield span def create_span( self, name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, + kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, ) -> "Span": """See `opentelemetry.trace.Tracer.create_span`.""" span_id = generate_span_id() @@ -451,6 +456,7 @@ def create_span( context=context, parent=parent, span_processor=self._active_span_processor, + kind=kind, ) @contextmanager diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index c8c7142235..0570affc41 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -36,10 +36,14 @@ def test_start_span_implicit(self): self.assertIsNotNone(root.start_time) self.assertIsNone(root.end_time) + self.assertEqual(root.kind, trace_api.SpanKind.INTERNAL) - with tracer.start_span("child") as child: + with tracer.start_span( + "child", kind=trace_api.SpanKind.CLIENT + ) as child: self.assertIs(tracer.get_current_span(), child) self.assertIs(child.parent, root) + self.assertEqual(child.kind, trace_api.SpanKind.CLIENT) self.assertIsNotNone(child.start_time) self.assertIsNone(child.end_time) From 2290df735e11f55bb222c6e0add00237596958da Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 18 Sep 2019 16:43:44 -0700 Subject: [PATCH 0074/1517] Metrics API RFC 0003 cont'd (#136) * Create functions Comments for Meter More comments Add more comments Fix typos * fix lint * Fix lint * fix typing * Remove options, constructors, seperate labels * Consistent naming for float and int * Abstract time series * Use ABC * Fix typo * Fix docs * seperate measure classes * Add examples * fix lint * Update to RFC 0003 * Add spancontext, measurebatch * Fix docs * Fix comments * fix lint * fix lint * fix lint * skip examples * white space * fix spacing * fix imports * fix imports * LabelValues to str * Black formatting * fix isort * Remove aggregation * Fix names * Remove aggregation from docs * Fix lint * metric changes * Typing * Fix lint * Fix lint * Add space * Fix lint * fix comments * address comments * fix comments --- .../src/opentelemetry/metrics/__init__.py | 321 +++++++----------- .../metrics/examples/pre_aggregated.py | 18 +- .../src/opentelemetry/metrics/examples/raw.py | 26 +- .../src/opentelemetry/metrics/time_series.py | 12 +- 4 files changed, 138 insertions(+), 239 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 68563bd492..94a7dc0e31 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -26,9 +26,9 @@ """ - from abc import ABC, abstractmethod -from typing import List +from enum import Enum +from typing import List, Union from opentelemetry.metrics.time_series import ( CounterTimeSeries, @@ -37,10 +37,8 @@ ) from opentelemetry.trace import SpanContext -LabelKeys = List["LabelKey"] -LabelValues = List[str] - +# pylint: disable=unused-argument class Meter: """An interface to allow the recording of metrics. @@ -49,160 +47,118 @@ class Meter: for the exported metric are deferred. """ - def create_float_counter( - self, - name: str, - description: str, - unit: str, - label_keys: LabelKeys, - span_context: SpanContext = None, - ) -> "FloatCounter": - """Creates a counter type metric that contains float values. - - Args: - name: The name of the counter. - description: Human readable description of the metric. - unit: Unit of the metric values. - label_keys: list of keys for the labels with dynamic values. - Order of the list is important as the same order MUST be used - on recording when suppling values for these labels. - span_context: The `SpanContext` that identifies the `Span` - that the metric is associated with. - - Returns: A new `FloatCounter` - """ - - def create_int_counter( - self, - name: str, - description: str, - unit: str, - label_keys: LabelKeys, - span_context: SpanContext = None, - ) -> "IntCounter": - """Creates a counter type metric that contains int values. - - Args: - name: The name of the counter. - description: Human readable description of the metric. - unit: Unit of the metric values. - label_keys: list of keys for the labels with dynamic values. - Order of the list is important as the same order MUST be used - on recording when suppling values for these labels. - span_context: The `SpanContext` that identifies the `Span` - that the metric is associated with. - - Returns: - A new `IntCounter` - """ - - def create_float_gauge( - self, - name: str, - description: str, - unit: str, - label_keys: LabelKeys, - span_context: SpanContext = None, - ) -> "FloatGauge": - """Creates a gauge type metric that contains float values. - - Args: - name: The name of the counter. - description: Human readable description of the metric. - unit: Unit of the metric values. - label_keys: list of keys for the labels with dynamic values. - Order of the list is important as the same order MUST be used - on recording when suppling values for these labels. - span_context: The `SpanContext` that identifies the `Span` - that the metric is associated with. - - Returns: - A new `FloatGauge` - """ - - def create_int_gauge( - self, - name: str, - description: str, - unit: str, - label_keys: LabelKeys, - span_context: SpanContext = None, - ) -> "IntGauge": - """Creates a gauge type metric that contains int values. - - Args: - name: The name of the counter. - description: Human readable description of the metric. - unit: Unit of the metric values. - label_keys: list of keys for the labels with dynamic values. - Order of the list is important as the same order MUST be used - on recording when suppling values for these labels. - span_context: The `SpanContext` that identifies the `Span` - that the metric is associated with. - - Returns: - A new `IntGauge` - """ + # TODO: RecordBatch + + +class ValueType(Enum): + FLOAT = 0 + INT = 1 + + +def create_counter( + name: str, + description: str, + unit: str, + value_type: "ValueType", + is_bidirectional: bool = False, + label_keys: List[str] = None, + span_context: SpanContext = None, +) -> Union["FloatCounter", "IntCounter"]: + """Creates a counter metric with type value_type. + + By default, counter values can only go up (unidirectional). The API + should reject negative inputs to unidirectional counter metrics. + Counter metrics have a bidirectional option to allow for negative + inputs. + + Args: + name: The name of the counter. + description: Human readable description of the metric. + unit: Unit of the metric values. + value_type: The type of values being recorded by the metric. + is_bidirectional: Set to true to allow negative inputs. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order must be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: A new counter metric for values of the given value_type. + """ - def create_int_measure( - self, - name: str, - description: str, - unit: str, - label_keys: LabelKeys, - span_context: SpanContext = None, - ) -> "IntMeasure": - """Creates a measure used to record raw int values. - Args: - name: The name of the measure. - description: Human readable description of this measure. - unit: Unit of the measure values. - label_keys: list of keys for the labels with dynamic values. - Order of the list is important as the same order MUST be used - on recording when suppling values for these labels. - span_context: The `SpanContext` that identifies the `Span` - that the metric is associated with. - - Returns: - A new `IntMeasure` - """ +def create_gauge( + name: str, + description: str, + unit: str, + value_type: "ValueType", + is_unidirectional: bool = False, + label_keys: List[str] = None, + span_context: SpanContext = None, +) -> Union["FloatGauge", "IntGauge"]: + """Creates a gauge metric with type value_type. + + By default, gauge values can go both up and down (bidirectional). The API + allows for an optional unidirectional flag, in which when set will reject + descending update values. + + Args: + name: The name of the gauge. + description: Human readable description of the metric. + unit: Unit of the metric values. + value_type: The type of values being recorded by the metric. + is_unidirectional: Set to true to reject negative inputs. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order must be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: A new gauge metric for values of the given value_type. + """ - def create_float_measure( - self, - name: str, - description: str, - unit: str, - label_keys: LabelKeys, - span_context: SpanContext = None, - ) -> "FloatMeasure": - """Creates a Measure used to record raw float values. - Args: - name: the name of the measure - description: Human readable description of this measure. - unit: Unit of the measure values. - label_keys: list of keys for the labels with dynamic values. - Order of the list is important as the same order MUST be used - on recording when suppling values for these labels. - span_context: The `SpanContext` that identifies the `Span` - that the metric is associated with. - - Returns: - A new `FloatMeasure` - """ +def create_measure( + name: str, + description: str, + unit: str, + value_type: "ValueType", + is_non_negative: bool = False, + label_keys: List[str] = None, + span_context: SpanContext = None, +) -> Union["FloatMeasure", "IntMeasure"]: + """Creates a measure metric with type value_type. + + Measure metrics represent raw statistics that are recorded. As an option, + measure metrics can be declared as non-negative. The API will reject + negative metric events for non-negative measures. + + Args: + name: The name of the measure. + description: Human readable description of the metric. + unit: Unit of the metric values. + value_type: The type of values being recorded by the metric. + is_non_negative: Set to true to reject negative inputs. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order must be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: A new measure metric for values of the given value_type. + """ class Metric(ABC): """Base class for various types of metrics. Metric class that inherit from this class are specialized with the type of - time series that the metric holds. Metric is constructed from the meter. + time series that the metric holds. """ @abstractmethod - def get_or_create_time_series(self, label_values: LabelValues) -> "object": - """Gets and returns a timeseries, a container for a cumulative value. + def get_or_create_time_series(self, label_values: List[str]) -> "object": + """Gets a timeseries, used for repeated-use of metrics instruments. If the provided label values are not already associated with this metric, a new timeseries is returned, otherwise it returns the existing @@ -215,7 +171,7 @@ def get_or_create_time_series(self, label_values: LabelValues) -> "object": with the return timeseries. """ - def remove_time_series(self, label_values: LabelValues) -> None: + def remove_time_series(self, label_values: List[str]) -> None: """Removes the timeseries from the Metric, if present. The timeseries with matching label values will be removed. @@ -229,89 +185,54 @@ def clear(self) -> None: class FloatCounter(Metric): - """A counter type metric that holds float values. - - Cumulative values can go up or stay the same, but can never go down. - Cumulative values cannot be negative. - """ + """A counter type metric that holds float values.""" def get_or_create_time_series( - self, label_values: LabelValues + self, label_values: List[str] ) -> "CounterTimeSeries": - """Gets a `CounterTimeSeries` with a cumulative float value.""" + """Gets a `CounterTimeSeries` with a float value.""" class IntCounter(Metric): - """A counter type metric that holds int values. - - Cumulative values can go up or stay the same, but can never go down. - Cumulative values cannot be negative. - """ + """A counter type metric that holds int values.""" def get_or_create_time_series( - self, label_values: LabelValues + self, label_values: List[str] ) -> "CounterTimeSeries": - """Gets a `CounterTimeSeries` with a cumulative int value.""" + """Gets a `CounterTimeSeries` with an int value.""" class FloatGauge(Metric): - """A gauge type metric that holds float values. - - Cumulative value can go both up and down. Values can be negative. - """ + """A gauge type metric that holds float values.""" def get_or_create_time_series( - self, label_values: LabelValues + self, label_values: List[str] ) -> "GaugeTimeSeries": - """Gets a `GaugeTimeSeries` with a cumulative float value.""" + """Gets a `GaugeTimeSeries` with a float value.""" class IntGauge(Metric): - """A gauge type metric that holds int values. - - Cumulative value can go both up and down. Values can be negative. - """ + """A gauge type metric that holds int values.""" def get_or_create_time_series( - self, label_values: LabelValues + self, label_values: List[str] ) -> "GaugeTimeSeries": - """Gets a `GaugeTimeSeries` with a cumulative int value.""" + """Gets a `GaugeTimeSeries` with an int value.""" class FloatMeasure(Metric): - """A measure type metric that holds float values. - - Measure metrics represent raw statistics that are recorded. - """ + """A measure type metric that holds float values.""" def get_or_create_time_series( - self, label_values: LabelValues + self, label_values: List[str] ) -> "MeasureTimeSeries": - """Gets a `MeasureTimeSeries` with a cumulated float value.""" + """Gets a `MeasureTimeSeries` with a float value.""" class IntMeasure(Metric): - """A measure type metric that holds int values. - - Measure metrics represent raw statistics that are recorded. - """ + """A measure type metric that holds int values.""" def get_or_create_time_series( - self, label_values: LabelValues + self, label_values: List[str] ) -> "MeasureTimeSeries": - """Gets a `MeasureTimeSeries` with a cumulated int value.""" - - -class LabelKey: - """The label keys associated with the metric. - - :type key: str - :param key: the key for the label - - :type description: str - :param description: description of the label - """ - - def __init__(self, key: str, description: str) -> None: - self.key = key - self.description = description + """Gets a `MeasureTimeSeries` with an int value.""" diff --git a/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py b/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py index c9c55f01b8..a07610b2ea 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py +++ b/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py @@ -13,24 +13,20 @@ # limitations under the License. # pylint: skip-file -from opentelemetry.metrics import LabelKey, LabelValue, Meter +from opentelemetry import metrics -METER = Meter() -LABEL_KEYS = [ - LabelKey("environment", "the environment the application is running in") -] +METER = metrics.Meter() COUNTER = METER.create_int_counter( - "sum numbers", # pragma: no cover + "sum numbers", "sum numbers over time", "number", - LABEL_KEYS, + metrics.ValueType.FLOAT, + ["environment"], ) -LABEL_VALUE_TESTING = [LabelValue("Testing")] -LABEL_VALUE_STAGING = [LabelValue("Staging")] # Metrics sent to some exporter -METRIC_TESTING = COUNTER.get_or_create_time_series(LABEL_VALUE_TESTING) -METRIC_STAGING = COUNTER.get_or_create_time_series(LABEL_VALUE_STAGING) +METRIC_TESTING = COUNTER.get_or_create_time_series("Testing") +METRIC_STAGING = COUNTER.get_or_create_time_series("Staging") for i in range(100): METRIC_STAGING.add(i) diff --git a/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py b/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py index 3c82e14d53..ada44da704 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py +++ b/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py @@ -13,33 +13,21 @@ # limitations under the License. # pylint: skip-file -from opentelemetry.metrics import LabelKey, LabelValue, MeasureBatch, Meter -from opentelemetry.metrics.aggregation import LastValueAggregation +from opentelemetry import metrics -METER = Meter() -LABEL_KEYS = [ - LabelKey("environment", "the environment the application is running in") -] -MEASURE = METER.create_float_measure( +METER = metrics.Meter() +MEASURE = metrics.create_measure( "idle_cpu_percentage", "cpu idle over time", "percentage", - LABEL_KEYS, - LastValueAggregation, + metrics.ValueType.FLOAT, + ["environment"], ) -LABEL_VALUE_TESTING = [LabelValue("Testing")] -LABEL_VALUE_STAGING = [LabelValue("Staging")] # Metrics sent to some exporter -MEASURE_METRIC_TESTING = MEASURE.get_or_create_time_series(LABEL_VALUE_TESTING) -MEASURE_METRIC_STAGING = MEASURE.get_or_create_time_series(LABEL_VALUE_STAGING) +MEASURE_METRIC_TESTING = MEASURE.get_or_create_time_series("Testing") +MEASURE_METRIC_STAGING = MEASURE.get_or_create_time_series("Staging") # record individual measures STATISTIC = 100 MEASURE_METRIC_STAGING.record(STATISTIC) - -# record multiple observed values -BATCH = MeasureBatch() -BATCH.record( - [(MEASURE_METRIC_TESTING, STATISTIC), (MEASURE_METRIC_STAGING, STATISTIC)] -) diff --git a/opentelemetry-api/src/opentelemetry/metrics/time_series.py b/opentelemetry-api/src/opentelemetry/metrics/time_series.py index b14ef973ad..25ad9efe77 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/time_series.py +++ b/opentelemetry-api/src/opentelemetry/metrics/time_series.py @@ -17,12 +17,9 @@ class CounterTimeSeries: def add(self, value: typing.Union[float, int]) -> None: - """Adds the given value to the current value. Cannot be negative.""" + """Adds the given value to the current value. - def set(self, value: typing.Union[float, int]) -> None: - """Sets the current value to the given value. - - The given value must be larger than the current recorded value. + The input value cannot be negative if not bidirectional. """ @@ -33,7 +30,4 @@ def set(self, value: typing.Union[float, int]) -> None: class MeasureTimeSeries: def record(self, value: typing.Union[float, int]) -> None: - """Records the given value to this measure. - - Logic depends on type of aggregation used for this measure. - """ + """Records the given value to this measure.""" From 8258927441478b6e793c8c2031b459203b481ba3 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Wed, 18 Sep 2019 20:59:56 -0700 Subject: [PATCH 0075/1517] Adding a working propagator, adding to integrations and example (#137) Adding a full, end-to-end example of propagation at work in the example application, including a test. Adding the use of propagators into the integrations. --- .../ext/http_requests/__init__.py | 3 + .../src/opentelemetry/ext/wsgi/__init__.py | 28 ++++++-- .../tests/test_wsgi_middleware.py | 2 +- .../context/propagation/httptextformat.py | 10 +-- .../propagation/tracecontexthttptextformat.py | 39 +++++++++++ .../src/opentelemetry/propagators/__init__.py | 70 +++++++++++++++++++ .../flask_example.py | 18 +++-- .../tests/test_flask_example.py | 51 +++++++++++++- .../sdk/context/propagation/b3_format.py | 1 - 9 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py create mode 100644 opentelemetry-api/src/opentelemetry/propagators/__init__.py diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index d06e78b052..994968e196 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -22,6 +22,7 @@ from requests.sessions import Session +from opentelemetry import propagators from opentelemetry.trace import SpanKind @@ -71,6 +72,8 @@ def instrumented_request(self, method, url, *args, **kwargs): # TODO: Propagate the trace context via headers once we have a way # to access propagators. + headers = kwargs.setdefault("headers", {}) + propagators.inject(tracer, type(headers).__setitem__, headers) result = wrapped(self, method, url, *args, **kwargs) # *** PROCEED span.set_attribute("http.status_code", result.status_code) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index a3120c2c0a..28caa77e52 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -19,9 +19,10 @@ """ import functools +import typing import wsgiref.util as wsgiref_util -from opentelemetry import trace +from opentelemetry import propagators, trace from opentelemetry.ext.wsgi.version import __version__ # noqa @@ -35,12 +36,9 @@ class OpenTelemetryMiddleware: wsgi: The WSGI application callable. """ - def __init__(self, wsgi, propagators=None): + def __init__(self, wsgi): self.wsgi = wsgi - # TODO: implement context propagation - self.propagators = propagators - @staticmethod def _add_request_attributes(span, environ): span.set_attribute("component", "http") @@ -87,8 +85,11 @@ def __call__(self, environ, start_response): tracer = trace.tracer() path_info = environ["PATH_INFO"] or "/" + parent_span = propagators.extract(get_header_from_environ, environ) - with tracer.start_span(path_info, kind=trace.SpanKind.SERVER) as span: + with tracer.start_span( + path_info, parent_span, kind=trace.SpanKind.SERVER + ) as span: self._add_request_attributes(span, environ) start_response = self._create_start_response(span, start_response) @@ -99,3 +100,18 @@ def __call__(self, environ, start_response): finally: if hasattr(iterable, "close"): iterable.close() + + +def get_header_from_environ( + environ: dict, header_name: str +) -> typing.List[str]: + """Retrieve the header value from the wsgi environ dictionary. + + Returns: + A string with the header value if it exists, else None. + """ + environ_key = "HTTP_" + header_name.upper().replace("-", "_") + value = environ.get(environ_key) + if value: + return [value] + return [] diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index 73bba50452..52cffc051a 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -126,7 +126,7 @@ def validate_response(self, response, error=None): # Verify that start_span has been called self.start_span.assert_called_once_with( - "/", kind=trace_api.SpanKind.SERVER + "/", trace_api.INVALID_SPAN_CONTEXT, kind=trace_api.SpanKind.SERVER ) def test_basic_wsgi_call(self): diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py index f3823a86d1..35bdfbb3fe 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py @@ -17,8 +17,10 @@ from opentelemetry.trace import SpanContext -Setter = typing.Callable[[object, str, str], None] -Getter = typing.Callable[[object, str], typing.List[str]] +_T = typing.TypeVar("_T") + +Setter = typing.Callable[[typing.Type[_T], str, str], None] +Getter = typing.Callable[[typing.Type[_T], str], typing.List[str]] class HTTPTextFormat(abc.ABC): @@ -70,7 +72,7 @@ def example_route(): @abc.abstractmethod def extract( - self, get_from_carrier: Getter, carrier: object + self, get_from_carrier: Getter[_T], carrier: _T ) -> SpanContext: """Create a SpanContext from values in the carrier. @@ -93,7 +95,7 @@ def extract( @abc.abstractmethod def inject( - self, context: SpanContext, set_in_carrier: Setter, carrier: object + self, context: SpanContext, set_in_carrier: Setter[_T], carrier: _T ) -> None: """Inject values from a SpanContext into a carrier. diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py new file mode 100644 index 0000000000..575644a91f --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py @@ -0,0 +1,39 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import typing + +import opentelemetry.trace as trace +from opentelemetry.context.propagation import httptextformat + +_T = typing.TypeVar("_T") + + +class TraceContextHTTPTextFormat(httptextformat.HTTPTextFormat): + """TODO: extracts and injects using w3c TraceContext's headers. + """ + + def extract( + self, _get_from_carrier: httptextformat.Getter[_T], _carrier: _T + ) -> trace.SpanContext: + return trace.INVALID_SPAN_CONTEXT + + def inject( + self, + context: trace.SpanContext, + set_in_carrier: httptextformat.Setter[_T], + carrier: _T, + ) -> None: + pass diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py new file mode 100644 index 0000000000..aa59669407 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -0,0 +1,70 @@ +import typing + +import opentelemetry.context.propagation.httptextformat as httptextformat +import opentelemetry.trace as trace +from opentelemetry.context.propagation.tracecontexthttptextformat import ( + TraceContextHTTPTextFormat, +) + +_T = typing.TypeVar("_T") + + +def extract( + get_from_carrier: httptextformat.Getter[_T], carrier: _T +) -> trace.SpanContext: + """Load the parent SpanContext from values in the carrier. + + Using the specified HTTPTextFormatter, the propagator will + extract a SpanContext from the carrier. If one is found, + it will be set as the parent context of the current span. + + Args: + get_from_carrier: a function that can retrieve zero + or more values from the carrier. In the case that + the value does not exist, return an empty list. + carrier: and object which contains values that are + used to construct a SpanContext. This object + must be paired with an appropriate get_from_carrier + which understands how to extract a value from it. + """ + return get_global_httptextformat().extract(get_from_carrier, carrier) + + +def inject( + tracer: trace.Tracer, + set_in_carrier: httptextformat.Setter[_T], + carrier: _T, +) -> None: + """Inject values from the current context into the carrier. + + inject enables the propagation of values into HTTP clients or + other objects which perform an HTTP request. Implementations + should use the set_in_carrier method to set values on the + carrier. + + Args: + set_in_carrier: A setter function that can set values + on the carrier. + carrier: An object that contains a representation of HTTP + headers. Should be paired with set_in_carrier, which + should know how to set header values on the carrier. + """ + get_global_httptextformat().inject( + tracer.get_current_span().get_context(), set_in_carrier, carrier + ) + + +_HTTP_TEXT_FORMAT = ( + TraceContextHTTPTextFormat() +) # type: httptextformat.HTTPTextFormat + + +def get_global_httptextformat() -> httptextformat.HTTPTextFormat: + return _HTTP_TEXT_FORMAT + + +def set_global_httptextformat( + http_text_format: httptextformat.HTTPTextFormat +) -> None: + global _HTTP_TEXT_FORMAT # pylint:disable=global-statement + _HTTP_TEXT_FORMAT = http_text_format diff --git a/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 9568f270c9..229acdfb43 100644 --- a/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -16,13 +16,13 @@ This module serves as an example to integrate with flask, using the requests library to perform downstream requests """ -import time - import flask +import requests import opentelemetry.ext.http_requests -from opentelemetry import trace +from opentelemetry import propagators, trace from opentelemetry.ext.wsgi import OpenTelemetryMiddleware +from opentelemetry.sdk.context.propagation.b3_format import B3Format from opentelemetry.sdk.trace import Tracer @@ -39,7 +39,6 @@ def configure_opentelemetry(flask_app: flask.Flask): * processors? * exporters? - * propagators? """ # Start by configuring all objects required to ensure # a complete end to end workflow. @@ -47,6 +46,13 @@ def configure_opentelemetry(flask_app: flask.Flask): # as the opentelemetry-api defines the interface with a no-op # implementation. trace.set_preferred_tracer_implementation(lambda _: Tracer()) + # Next, we need to configure how the values that are used by + # traces and metrics are propagated (such as what specific headers + # carry this value). + + # TBD: can remove once default TraceContext propagators are installed. + propagators.set_global_httptextformat(B3Format()) + # Integrations are the glue that binds the OpenTelemetry API # and the frameworks and libraries that are used together, automatically # creating Spans and propagating context as appropriate. @@ -61,8 +67,8 @@ def configure_opentelemetry(flask_app: flask.Flask): def hello(): # emit a trace that measures how long the # sleep takes - with trace.tracer().start_span("sleep"): - time.sleep(0.001) + with trace.tracer().start_span("example-request"): + requests.get("http://www.example.com") return "hello" diff --git a/opentelemetry-example-app/tests/test_flask_example.py b/opentelemetry-example-app/tests/test_flask_example.py index ca2a237a60..d33e2b9ac0 100644 --- a/opentelemetry-example-app/tests/test_flask_example.py +++ b/opentelemetry-example-app/tests/test_flask_example.py @@ -1,6 +1,13 @@ import unittest +from unittest import mock + +import requests +from werkzeug.test import Client +from werkzeug.wrappers import BaseResponse import opentelemetry_example_app.flask_example as flask_example +from opentelemetry.sdk import trace +from opentelemetry.sdk.context.propagation import b3_format class TestFlaskExample(unittest.TestCase): @@ -8,7 +15,45 @@ class TestFlaskExample(unittest.TestCase): def setUpClass(cls): cls.app = flask_example.app + def setUp(self): + mocked_response = requests.models.Response() + mocked_response.status_code = 200 + mocked_response.reason = "Roger that!" + self.send_patcher = mock.patch.object( + requests.Session, + "send", + autospec=True, + spec_set=True, + return_value=mocked_response, + ) + self.send = self.send_patcher.start() + + def tearDown(self): + self.send_patcher.stop() + def test_full_path(self): - with self.app.test_client() as client: - response = client.get("/") - assert response.data.decode() == "hello" + trace_id = trace.generate_trace_id() + # We need to use the Werkzeug test app because + # The headers are injected at the wsgi layer. + # The flask test app will not include these, and + # result in the values not propagated. + client = Client(self.app.wsgi_app, BaseResponse) + # emulate b3 headers + client.get( + "/", + headers={ + "x-b3-traceid": b3_format.format_trace_id(trace_id), + "x-b3-spanid": b3_format.format_span_id( + trace.generate_span_id() + ), + "x-b3-sampled": "1", + }, + ) + # assert the http request header was propagated through. + prepared_request = self.send.call_args[0][1] + headers = prepared_request.headers + for required_header in {"x-b3-traceid", "x-b3-spanid", "x-b3-sampled"}: + self.assertIn(required_header, headers) + self.assertEqual( + headers["x-b3-traceid"], b3_format.format_trace_id(trace_id) + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py index 72d02d6070..9e8f7d3f19 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -91,7 +91,6 @@ def extract(cls, get_from_carrier, carrier): # header is set to allow. if sampled in cls._SAMPLE_PROPAGATE_VALUES or flags == "1": options |= trace.TraceOptions.RECORDED - return trace.SpanContext( # trace an span ids are encoded in hex, so must be converted trace_id=int(trace_id, 16), From ca10173422f3b80672dd312e53460c62d428378e Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 19 Sep 2019 11:43:30 -0700 Subject: [PATCH 0076/1517] Metrics API RFC 0009 (#140) * Create functions Comments for Meter More comments Add more comments Fix typos * fix lint * Fix lint * fix typing * Remove options, constructors, seperate labels * Consistent naming for float and int * Abstract time series * Use ABC * Fix typo * Fix docs * seperate measure classes * Add examples * fix lint * Update to RFC 0003 * Add spancontext, measurebatch * Fix docs * Fix comments * fix lint * fix lint * fix lint * skip examples * white space * fix spacing * fix imports * fix imports * LabelValues to str * Black formatting * fix isort * Remove aggregation * Fix names * Remove aggregation from docs * Fix lint * metric changes * Typing * Fix lint * Fix lint * Add space * Fix lint * fix comments * handle, recordbatch * docs * Update recordbatch * black * Fix typo * remove ValueType * fix lint --- docs/opentelemetry.metrics.handle.rst | 5 + docs/opentelemetry.metrics.rst | 2 +- docs/opentelemetry.metrics.time_series.rst | 5 - .../src/opentelemetry/metrics/__init__.py | 97 +++++++++---------- .../metrics/examples/pre_aggregated.py | 6 +- .../src/opentelemetry/metrics/examples/raw.py | 7 +- .../metrics/{time_series.py => handle.py} | 6 +- 7 files changed, 65 insertions(+), 63 deletions(-) create mode 100644 docs/opentelemetry.metrics.handle.rst delete mode 100644 docs/opentelemetry.metrics.time_series.rst rename opentelemetry-api/src/opentelemetry/metrics/{time_series.py => handle.py} (93%) diff --git a/docs/opentelemetry.metrics.handle.rst b/docs/opentelemetry.metrics.handle.rst new file mode 100644 index 0000000000..826a0e4e5a --- /dev/null +++ b/docs/opentelemetry.metrics.handle.rst @@ -0,0 +1,5 @@ +opentelemetry.metrics.handle module +========================================== + +.. automodule:: opentelemetry.metrics.handle + diff --git a/docs/opentelemetry.metrics.rst b/docs/opentelemetry.metrics.rst index 2d025d3197..289f1842ba 100644 --- a/docs/opentelemetry.metrics.rst +++ b/docs/opentelemetry.metrics.rst @@ -6,7 +6,7 @@ Submodules .. toctree:: - opentelemetry.metrics.time_series + opentelemetry.metrics.handle Module contents --------------- diff --git a/docs/opentelemetry.metrics.time_series.rst b/docs/opentelemetry.metrics.time_series.rst deleted file mode 100644 index 16297d7eac..0000000000 --- a/docs/opentelemetry.metrics.time_series.rst +++ /dev/null @@ -1,5 +0,0 @@ -opentelemetry.metrics.time\_series module -========================================== - -.. automodule:: opentelemetry.metrics.time_series - diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 94a7dc0e31..668c61cbc3 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -27,13 +27,12 @@ """ from abc import ABC, abstractmethod -from enum import Enum -from typing import List, Union +from typing import Dict, List, Tuple, Type, Union -from opentelemetry.metrics.time_series import ( - CounterTimeSeries, - GaugeTimeSeries, - MeasureTimeSeries, +from opentelemetry.metrics.handle import ( + CounterHandle, + GaugeHandle, + MeasureHandle, ) from opentelemetry.trace import SpanContext @@ -47,19 +46,30 @@ class Meter: for the exported metric are deferred. """ - # TODO: RecordBatch + def record_batch( + self, + label_tuples: Dict[str, str], + record_tuples: List[Tuple["Metric", Union[float, int]]], + ) -> None: + """Atomically records a batch of `Metric` and value pairs. + Allows the functionality of acting upon multiple metrics with + a single API call. Implementations should find handles that match + the key-value pairs in the label tuples. -class ValueType(Enum): - FLOAT = 0 - INT = 1 + Args: + label_tuples: A collection of key value pairs that will be matched + against to record for the metric-handle that has those labels. + record_tuples: A list of pairs of `Metric` s and the + corresponding value to record for that metric. + """ def create_counter( name: str, description: str, unit: str, - value_type: "ValueType", + value_type: Union[Type[float], Type[int]], is_bidirectional: bool = False, label_keys: List[str] = None, span_context: SpanContext = None, @@ -91,7 +101,7 @@ def create_gauge( name: str, description: str, unit: str, - value_type: "ValueType", + value_type: Union[Type[float], Type[int]], is_unidirectional: bool = False, label_keys: List[str] = None, span_context: SpanContext = None, @@ -122,7 +132,7 @@ def create_measure( name: str, description: str, unit: str, - value_type: "ValueType", + value_type: Union[Type[float], Type[int]], is_non_negative: bool = False, label_keys: List[str] = None, span_context: SpanContext = None, @@ -157,82 +167,71 @@ class Metric(ABC): """ @abstractmethod - def get_or_create_time_series(self, label_values: List[str]) -> "object": - """Gets a timeseries, used for repeated-use of metrics instruments. + def get_handle(self, label_values: List[str]) -> "object": + """Gets a handle, used for repeated-use of metrics instruments. - If the provided label values are not already associated with this - metric, a new timeseries is returned, otherwise it returns the existing - timeseries with the exact label values. The timeseries returned - contains logic and behaviour specific to the type of metric that - overrides this function. + Handles are useful to reduce the cost of repeatedly recording a metric + with a pre-defined set of label values. All metric kinds (counter, + gauge, measure) support declaring a set of required label keys. The + values corresponding to these keys should be specified in every handle. + "Unspecified" label values, in cases where a handle is requested but + a value was not provided are permitted. Args: label_values: A list of label values that will be associated - with the return timeseries. + with the return handle. """ - def remove_time_series(self, label_values: List[str]) -> None: - """Removes the timeseries from the Metric, if present. + def remove_handle(self, label_values: List[str]) -> None: + """Removes the handle from the Metric, if present. - The timeseries with matching label values will be removed. + The handle with matching label values will be removed. args: label_values: The list of label values to match against. """ def clear(self) -> None: - """Removes all timeseries from the `Metric`.""" + """Removes all handles from the `Metric`.""" class FloatCounter(Metric): """A counter type metric that holds float values.""" - def get_or_create_time_series( - self, label_values: List[str] - ) -> "CounterTimeSeries": - """Gets a `CounterTimeSeries` with a float value.""" + def get_handle(self, label_values: List[str]) -> "CounterHandle": + """Gets a `CounterHandle` with a float value.""" class IntCounter(Metric): """A counter type metric that holds int values.""" - def get_or_create_time_series( - self, label_values: List[str] - ) -> "CounterTimeSeries": - """Gets a `CounterTimeSeries` with an int value.""" + def get_handle(self, label_values: List[str]) -> "CounterHandle": + """Gets a `CounterHandle` with an int value.""" class FloatGauge(Metric): """A gauge type metric that holds float values.""" - def get_or_create_time_series( - self, label_values: List[str] - ) -> "GaugeTimeSeries": - """Gets a `GaugeTimeSeries` with a float value.""" + def get_handle(self, label_values: List[str]) -> "GaugeHandle": + """Gets a `GaugeHandle` with a float value.""" class IntGauge(Metric): """A gauge type metric that holds int values.""" - def get_or_create_time_series( - self, label_values: List[str] - ) -> "GaugeTimeSeries": - """Gets a `GaugeTimeSeries` with an int value.""" + def get_handle(self, label_values: List[str]) -> "GaugeHandle": + """Gets a `GaugeHandle` with an int value.""" class FloatMeasure(Metric): """A measure type metric that holds float values.""" - def get_or_create_time_series( - self, label_values: List[str] - ) -> "MeasureTimeSeries": - """Gets a `MeasureTimeSeries` with a float value.""" + def get_handle(self, label_values: List[str]) -> "MeasureHandle": + """Gets a `MeasureHandle` with a float value.""" class IntMeasure(Metric): """A measure type metric that holds int values.""" - def get_or_create_time_series( - self, label_values: List[str] - ) -> "MeasureTimeSeries": - """Gets a `MeasureTimeSeries` with an int value.""" + def get_handle(self, label_values: List[str]) -> "MeasureHandle": + """Gets a `MeasureHandle` with an int value.""" diff --git a/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py b/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py index a07610b2ea..3cbf4d3d20 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py +++ b/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py @@ -16,7 +16,7 @@ from opentelemetry import metrics METER = metrics.Meter() -COUNTER = METER.create_int_counter( +COUNTER = METER.create_counter( "sum numbers", "sum numbers over time", "number", @@ -25,8 +25,8 @@ ) # Metrics sent to some exporter -METRIC_TESTING = COUNTER.get_or_create_time_series("Testing") -METRIC_STAGING = COUNTER.get_or_create_time_series("Staging") +METRIC_TESTING = COUNTER.get_handle("Testing") +METRIC_STAGING = COUNTER.get_handle("Staging") for i in range(100): METRIC_STAGING.add(i) diff --git a/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py b/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py index ada44da704..501b2e5b61 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py +++ b/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py @@ -25,9 +25,12 @@ ) # Metrics sent to some exporter -MEASURE_METRIC_TESTING = MEASURE.get_or_create_time_series("Testing") -MEASURE_METRIC_STAGING = MEASURE.get_or_create_time_series("Staging") +MEASURE_METRIC_TESTING = MEASURE.get_handle("Testing") +MEASURE_METRIC_STAGING = MEASURE.get_handle("Staging") # record individual measures STATISTIC = 100 MEASURE_METRIC_STAGING.record(STATISTIC) + +# Batch recording +METER.record_batch([tuple(MEASURE_METRIC_STAGING, STATISTIC)]) diff --git a/opentelemetry-api/src/opentelemetry/metrics/time_series.py b/opentelemetry-api/src/opentelemetry/metrics/handle.py similarity index 93% rename from opentelemetry-api/src/opentelemetry/metrics/time_series.py rename to opentelemetry-api/src/opentelemetry/metrics/handle.py index 25ad9efe77..5da3cf43e7 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/time_series.py +++ b/opentelemetry-api/src/opentelemetry/metrics/handle.py @@ -15,7 +15,7 @@ import typing -class CounterTimeSeries: +class CounterHandle: def add(self, value: typing.Union[float, int]) -> None: """Adds the given value to the current value. @@ -23,11 +23,11 @@ def add(self, value: typing.Union[float, int]) -> None: """ -class GaugeTimeSeries: +class GaugeHandle: def set(self, value: typing.Union[float, int]) -> None: """Sets the current value to the given value. Can be negative.""" -class MeasureTimeSeries: +class MeasureHandle: def record(self, value: typing.Union[float, int]) -> None: """Records the given value to this measure.""" From 60fe16012823c9768800f8dfcfb3ba3f339755fe Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Fri, 20 Sep 2019 13:42:23 -0700 Subject: [PATCH 0077/1517] Console exporter (#156) --- README.md | 9 +++++---- .../src/opentelemetry/sdk/trace/__init__.py | 15 ++++++++++++++- .../opentelemetry/sdk/trace/export/__init__.py | 14 ++++++++++++++ .../sdk/trace/export/in_memory_span_exporter.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/util.py | 7 +++++++ 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b597d6f611..a7fbd3efd7 100644 --- a/README.md +++ b/README.md @@ -43,17 +43,18 @@ pip install -e ./ext/opentelemetry-ext-{integration} from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace.export import ConsoleSpanExporter +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor trace.set_preferred_tracer_implementation(lambda T: Tracer()) tracer = trace.tracer() +tracer.add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) with tracer.start_span('foo'): - print(Context) with tracer.start_span('bar'): - print(Context) with tracer.start_span('baz'): print(Context) - print(Context) - print(Context) ``` See [opentelemetry-example-app](./opentelemetry-example-app/README.rst) for a complete example. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index a694476e1f..c839fe4b61 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -280,7 +280,20 @@ def __init__( self.start_time = None def __repr__(self): - return '{}(name="{}")'.format(type(self).__name__, self.name) + return '{}(name="{}", context={})'.format( + type(self).__name__, self.name, self.context + ) + + def __str__(self): + return '{}(name="{}", context={}, kind={}, parent={}, start_time={}, end_time={})'.format( + type(self).__name__, + self.name, + self.context, + self.kind, + repr(self.parent), + util.ns_to_iso_str(self.start_time) if self.start_time else "None", + util.ns_to_iso_str(self.end_time) if self.end_time else "None", + ) def get_context(self): return self.context diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 0c011f9976..52d6d3bd22 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -76,3 +76,17 @@ def on_end(self, span: Span) -> None: def shutdown(self) -> None: self.span_exporter.shutdown() + + +class ConsoleSpanExporter(SpanExporter): + """Implementation of :class:`SpanExporter` that prints spans to the + console. + + This class can be used for diagnostic purposes. It prints the exported + spans to the console STDOUT. + """ + + def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: + for span in spans: + print(span) + return SpanExportResult.SUCCESS diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py index b536120560..0818805f39 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py @@ -20,7 +20,7 @@ class InMemorySpanExporter(SpanExporter): - """Implementation of :class:`.Exporter` that stores spans in memory. + """Implementation of :class:`.SpanExporter` that stores spans in memory. This class can be used for testing purposes. It stores the exported spans in a list in memory that can be retrieved using the diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util.py b/opentelemetry-sdk/src/opentelemetry/sdk/util.py index d814433eee..d855e85179 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime import time try: @@ -21,3 +22,9 @@ def time_ns(): return int(time.time() * 1e9) + + +def ns_to_iso_str(nanoseconds): + """Get an ISO 8601 string from time_ns value.""" + ts = datetime.datetime.fromtimestamp(nanoseconds / 1e9) + return ts.strftime("%Y-%m-%dT%H:%M:%S.%fZ") From 83aad2ff5ddba90fc6a882e9f2140417d286b5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 24 Sep 2019 11:30:14 +0200 Subject: [PATCH 0078/1517] Make use_span more flexible (closes #147). (#154) Co-Authored-By: Reiley Yang Co-Authored-By: Chris Kleinknecht --- .../src/opentelemetry/trace/__init__.py | 18 ++++++++++----- .../src/opentelemetry/sdk/trace/__init__.py | 23 ++++++++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index b79cdeb4df..33c62e9d3b 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -415,7 +415,8 @@ def start_span( is equivalent to:: span = tracer.create_span(name) - with tracer.use_span(span): + span.start() + with tracer.use_span(span, end_on_exit=True): do_work() Args: @@ -472,17 +473,22 @@ def create_span( return INVALID_SPAN @contextmanager # type: ignore - def use_span(self, span: "Span") -> typing.Iterator[None]: + def use_span( + self, span: "Span", end_on_exit: bool = False + ) -> typing.Iterator[None]: """Context manager for controlling a span's lifetime. - Start the given span and set it as the current span in this tracer's - context. + Set the given span as the current span in this tracer's context. - On exiting the context manager stop the span and set its parent as the - current span. + On exiting the context manager set the span that was previously active + as the current span (this is usually but not necessarily the parent of + the given span). If ``end_on_exit`` is ``True``, then the span is also + ended when exiting the context manager. Args: span: The span to start and make current. + end_on_exit: Whether to end the span automatically when leaving the + context manager. """ # pylint: disable=unused-argument,no-self-use yield diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index c839fe4b61..544906e6fb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -436,7 +436,10 @@ def start_span( kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, ) -> typing.Iterator["Span"]: """See `opentelemetry.trace.Tracer.start_span`.""" - with self.use_span(self.create_span(name, parent, kind)) as span: + + span = self.create_span(name, parent, kind) + span.start() + with self.use_span(span, end_on_exit=True): yield span def create_span( @@ -473,16 +476,20 @@ def create_span( ) @contextmanager - def use_span(self, span: "Span") -> typing.Iterator["Span"]: + def use_span( + self, span: Span, end_on_exit: bool = False + ) -> typing.Iterator[Span]: """See `opentelemetry.trace.Tracer.use_span`.""" - span.start() - span_snapshot = self._current_span_slot.get() - self._current_span_slot.set(span) try: - yield span + span_snapshot = self._current_span_slot.get() + self._current_span_slot.set(span) + try: + yield span + finally: + self._current_span_slot.set(span_snapshot) finally: - self._current_span_slot.set(span_snapshot) - span.end() + if end_on_exit: + span.end() def add_span_processor(self, span_processor: SpanProcessor) -> None: """Registers a new :class:`SpanProcessor` for this `Tracer`. From 7813924da2aacdf9b7d257bf5c8825162c4eac73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 24 Sep 2019 21:53:36 +0200 Subject: [PATCH 0079/1517] WSGI fixes (#148) Fix http.url. Don't delay calling wrapped app. --- .../src/opentelemetry/ext/wsgi/__init__.py | 76 ++++++++--- .../tests/test_wsgi_middleware.py | 123 +++++++++++++++--- 2 files changed, 162 insertions(+), 37 deletions(-) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 28caa77e52..61f061948d 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -44,14 +44,34 @@ def _add_request_attributes(span, environ): span.set_attribute("component", "http") span.set_attribute("http.method", environ["REQUEST_METHOD"]) - host = environ.get("HTTP_HOST") or environ["SERVER_NAME"] + host = environ.get("HTTP_HOST") + if not host: + host = environ["SERVER_NAME"] + port = environ["SERVER_PORT"] + if ( + port != "80" + and environ["wsgi.url_scheme"] == "http" + or port != "443" + ): + host += ":" + port + + # NOTE: Nonstandard span.set_attribute("http.host", host) - url = ( - environ.get("REQUEST_URI") - or environ.get("RAW_URI") - or wsgiref_util.request_uri(environ, include_query=False) - ) + url = environ.get("REQUEST_URI") or environ.get("RAW_URI") + + if url: + if url[0] == "/": + # We assume that no scheme-relative URLs will be in url here. + # After all, if a request is made to http://myserver//foo, we may get + # //foo which looks like scheme-relative but isn't. + url = environ["wsgi.url_scheme"] + "://" + host + url + elif not url.startswith(environ["wsgi.url_scheme"] + ":"): + # Something fishy is in RAW_URL. Let's fall back to request_uri() + url = wsgiref_util.request_uri(environ) + else: + url = wsgiref_util.request_uri(environ) + span.set_attribute("http.url", url) @staticmethod @@ -85,24 +105,27 @@ def __call__(self, environ, start_response): tracer = trace.tracer() path_info = environ["PATH_INFO"] or "/" - parent_span = propagators.extract(get_header_from_environ, environ) + parent_span = propagators.extract(_get_header_from_environ, environ) - with tracer.start_span( + span = tracer.create_span( path_info, parent_span, kind=trace.SpanKind.SERVER - ) as span: - self._add_request_attributes(span, environ) - start_response = self._create_start_response(span, start_response) + ) + span.start() + try: + with tracer.use_span(span): + self._add_request_attributes(span, environ) + start_response = self._create_start_response( + span, start_response + ) - iterable = self.wsgi(environ, start_response) - try: - for yielded in iterable: - yield yielded - finally: - if hasattr(iterable, "close"): - iterable.close() + iterable = self.wsgi(environ, start_response) + return _end_span_after_iterating(iterable, span, tracer) + except: # noqa + span.end() + raise -def get_header_from_environ( +def _get_header_from_environ( environ: dict, header_name: str ) -> typing.List[str]: """Retrieve the header value from the wsgi environ dictionary. @@ -115,3 +138,18 @@ def get_header_from_environ( if value: return [value] return [] + + +# Put this in a subfunction to not delay the call to the wrapped +# WSGI application (instrumentation should change the application +# behavior as little as possible). +def _end_span_after_iterating(iterable, span, tracer): + try: + with tracer.use_span(span): + for yielded in iterable: + yield yielded + finally: + close = getattr(iterable, "close", None) + if close: + close() + span.end() diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index 52cffc051a..cd1778c492 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -17,6 +17,7 @@ import unittest import unittest.mock as mock import wsgiref.util as wsgiref_util +from urllib.parse import urlparse from opentelemetry import trace as trace_api from opentelemetry.ext.wsgi import OpenTelemetryMiddleware @@ -52,6 +53,15 @@ def iter_wsgi(environ, start_response): return iter_wsgi +def create_gen_wsgi(response): + def gen_wsgi(environ, start_response): + result = create_iter_wsgi(response)(environ, start_response) + yield from result + getattr(result, "close", lambda: None)() + + return gen_wsgi + + def error_wsgi(environ, start_response): assert isinstance(environ, dict) try: @@ -66,18 +76,15 @@ def error_wsgi(environ, start_response): class TestWsgiApplication(unittest.TestCase): def setUp(self): tracer = trace_api.tracer() - self.span_context_manager = mock.MagicMock() - self.span_context_manager.__enter__.return_value = mock.create_autospec( - trace_api.Span, spec_set=True - ) - self.patcher = mock.patch.object( + self.span = mock.create_autospec(trace_api.Span, spec_set=True) + self.create_span_patcher = mock.patch.object( tracer, - "start_span", + "create_span", autospec=True, spec_set=True, - return_value=self.span_context_manager, + return_value=self.span, ) - self.start_span = self.patcher.start() + self.create_span = self.create_span_patcher.start() self.write_buffer = io.BytesIO() self.write = self.write_buffer.write @@ -90,11 +97,11 @@ def setUp(self): self.exc_info = None def tearDown(self): - self.patcher.stop() + self.create_span_patcher.stop() def start_response(self, status, response_headers, exc_info=None): # The span should have started already - self.span_context_manager.__enter__.assert_called_with() + self.span.start.assert_called_once_with() self.status = status self.response_headers = response_headers @@ -105,12 +112,10 @@ def validate_response(self, response, error=None): while True: try: value = next(response) - self.span_context_manager.__exit__.assert_not_called() + self.assertEqual(0, self.span.end.call_count) self.assertEqual(value, b"*") except StopIteration: - self.span_context_manager.__exit__.assert_called_with( - None, None, None - ) + self.span.end.assert_called_once_with() break self.assertEqual(self.status, "200 OK") @@ -125,9 +130,10 @@ def validate_response(self, response, error=None): self.assertIsNone(self.exc_info) # Verify that start_span has been called - self.start_span.assert_called_once_with( + self.create_span.assert_called_with( "/", trace_api.INVALID_SPAN_CONTEXT, kind=trace_api.SpanKind.SERVER ) + self.span.start.assert_called_with() def test_basic_wsgi_call(self): app = OpenTelemetryMiddleware(simple_wsgi) @@ -139,12 +145,24 @@ def test_wsgi_iterable(self): iter_wsgi = create_iter_wsgi(original_response) app = OpenTelemetryMiddleware(iter_wsgi) response = app(self.environ, self.start_response) - # Verify that start_response has not been called yet + # Verify that start_response has been called + self.assertTrue(self.status) + self.validate_response(response) + + # Verify that close has been called exactly once + self.assertEqual(original_response.close_calls, 1) + + def test_wsgi_generator(self): + original_response = Response() + gen_wsgi = create_gen_wsgi(original_response) + app = OpenTelemetryMiddleware(gen_wsgi) + response = app(self.environ, self.start_response) + # Verify that start_response has not been called self.assertIsNone(self.status) self.validate_response(response) # Verify that close has been called exactly once - assert original_response.close_calls == 1 + self.assertEqual(original_response.close_calls, 1) def test_wsgi_exc_info(self): app = OpenTelemetryMiddleware(error_wsgi) @@ -159,18 +177,87 @@ def setUp(self): self.span = mock.create_autospec(trace_api.Span, spec_set=True) def test_request_attributes(self): + self.environ["QUERY_STRING"] = "foo=bar" + OpenTelemetryMiddleware._add_request_attributes( # noqa pylint: disable=protected-access self.span, self.environ ) + expected = ( mock.call("component", "http"), mock.call("http.method", "GET"), mock.call("http.host", "127.0.0.1"), - mock.call("http.url", "http://127.0.0.1/"), + mock.call("http.url", "http://127.0.0.1/?foo=bar"), ) self.assertEqual(self.span.set_attribute.call_count, len(expected)) self.span.set_attribute.assert_has_calls(expected, any_order=True) + def validate_url(self, expected_url): + OpenTelemetryMiddleware._add_request_attributes( # noqa pylint: disable=protected-access + self.span, self.environ + ) + attrs = { + args[0][0]: args[0][1] + for args in self.span.set_attribute.call_args_list + } + self.assertIn("http.url", attrs) + self.assertEqual(attrs["http.url"], expected_url) + self.assertIn("http.host", attrs) + self.assertEqual( + attrs["http.host"], urlparse(attrs["http.url"]).netloc + ) + + def test_request_attributes_with_partial_raw_uri(self): + self.environ["RAW_URI"] = "/#top" + self.validate_url("http://127.0.0.1/#top") + + def test_request_attributes_with_partial_raw_uri_and_nonstandard_port( + self + ): + self.environ["RAW_URI"] = "/?" + del self.environ["HTTP_HOST"] + self.environ["SERVER_PORT"] = "8080" + self.validate_url("http://127.0.0.1:8080/?") + + def test_https_uri_port(self): + del self.environ["HTTP_HOST"] + self.environ["SERVER_PORT"] = "443" + self.environ["wsgi.url_scheme"] = "https" + self.validate_url("https://127.0.0.1/") + + self.environ["SERVER_PORT"] = "8080" + self.validate_url("https://127.0.0.1:8080/") + + self.environ["SERVER_PORT"] = "80" + self.validate_url("https://127.0.0.1:80/") + + def test_request_attributes_with_nonstandard_port_and_no_host(self): + del self.environ["HTTP_HOST"] + self.environ["SERVER_PORT"] = "8080" + self.validate_url("http://127.0.0.1:8080/") + + self.environ["SERVER_PORT"] = "443" + self.validate_url("http://127.0.0.1:443/") + + def test_request_attributes_with_nonstandard_port(self): + self.environ["HTTP_HOST"] += ":8080" + self.validate_url("http://127.0.0.1:8080/") + + def test_request_attributes_with_faux_scheme_relative_raw_uri(self): + self.environ["RAW_URI"] = "//127.0.0.1/?" + self.validate_url("http://127.0.0.1//127.0.0.1/?") + + def test_request_attributes_with_pathless_raw_uri(self): + self.environ["PATH_INFO"] = "" + self.environ["RAW_URI"] = "http://hello" + self.environ["HTTP_HOST"] = "hello" + self.validate_url("http://hello") + + def test_request_attributes_with_full_request_uri(self): + self.environ["HTTP_HOST"] = "127.0.0.1:8080" + self.environ["REQUEST_URI"] = "http://127.0.0.1:8080/?foo=bar#top" + self.validate_url("http://127.0.0.1:8080/?foo=bar#top") + def test_response_attributes(self): OpenTelemetryMiddleware._add_response_attributes( # noqa pylint: disable=protected-access self.span, "404 Not Found" From b657c2740c2ffc640a47c7ca274f0bed52b1a068 Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Tue, 24 Sep 2019 20:12:56 -0700 Subject: [PATCH 0080/1517] Skeleton for azure monitor exporters (#151) --- .../README.rst | 10 ++++ .../examples/trace.py | 13 ++++++ ext/opentelemetry-ext-azure-monitor/setup.cfg | 46 +++++++++++++++++++ ext/opentelemetry-ext-azure-monitor/setup.py | 26 +++++++++++ .../ext/azure_monitor/__init__.py | 23 ++++++++++ .../opentelemetry/ext/azure_monitor/trace.py | 25 ++++++++++ .../ext/azure_monitor/version.py | 15 ++++++ .../tests/__init__.py | 0 tox.ini | 4 ++ 9 files changed, 162 insertions(+) create mode 100644 ext/opentelemetry-ext-azure-monitor/README.rst create mode 100644 ext/opentelemetry-ext-azure-monitor/examples/trace.py create mode 100644 ext/opentelemetry-ext-azure-monitor/setup.cfg create mode 100644 ext/opentelemetry-ext-azure-monitor/setup.py create mode 100644 ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/__init__.py create mode 100644 ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py create mode 100644 ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/version.py create mode 100644 ext/opentelemetry-ext-azure-monitor/tests/__init__.py diff --git a/ext/opentelemetry-ext-azure-monitor/README.rst b/ext/opentelemetry-ext-azure-monitor/README.rst new file mode 100644 index 0000000000..d8c3e16074 --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/README.rst @@ -0,0 +1,10 @@ +OpenTelemetry Azure Monitor Exporters +===================================== + +This library provides integration with Microsoft Azure Monitor. + +References +---------- + +* `Azure Monitor `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-azure-monitor/examples/trace.py b/ext/opentelemetry-ext-azure-monitor/examples/trace.py new file mode 100644 index 0000000000..49b38c051d --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/examples/trace.py @@ -0,0 +1,13 @@ +from opentelemetry import trace +from opentelemetry.ext.azure_monitor import AzureMonitorSpanExporter +from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor + +trace.set_preferred_tracer_implementation(lambda T: Tracer()) +tracer = trace.tracer() +tracer.add_span_processor( + SimpleExportSpanProcessor(AzureMonitorSpanExporter()) +) + +with tracer.start_span("hello") as span: + print("Hello, World!") diff --git a/ext/opentelemetry-ext-azure-monitor/setup.cfg b/ext/opentelemetry-ext-azure-monitor/setup.cfg new file mode 100644 index 0000000000..3110ed3531 --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/setup.cfg @@ -0,0 +1,46 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-azure-monitor +description = Azure Monitor integration for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-azure-monitor +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api + opentelemetry-sdk + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-azure-monitor/setup.py b/ext/opentelemetry-ext-azure-monitor/setup.py new file mode 100644 index 0000000000..5f8afcb23a --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/setup.py @@ -0,0 +1,26 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "azure_monitor", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/__init__.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/__init__.py new file mode 100644 index 0000000000..81222c546e --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/__init__.py @@ -0,0 +1,23 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The opentelemetry-ext-azure-monitor package provides integration with +Microsoft Azure Monitor. +""" + +from opentelemetry.ext.azure_monitor.trace import AzureMonitorSpanExporter +from opentelemetry.ext.azure_monitor.version import __version__ # noqa + +__all__ = ["AzureMonitorSpanExporter"] diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py new file mode 100644 index 0000000000..a65cdd92a1 --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py @@ -0,0 +1,25 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult + + +class AzureMonitorSpanExporter(SpanExporter): + def __init__(self): + pass + + def export(self, spans): + for span in spans: + print(span) # TODO: add actual implementation here + return SpanExportResult.SUCCESS diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/version.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/version.py new file mode 100644 index 0000000000..a457c2b665 --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.1.dev0" diff --git a/ext/opentelemetry-ext-azure-monitor/tests/__init__.py b/ext/opentelemetry-ext-azure-monitor/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tox.ini b/tox.ini index d0fb6cc2dc..871931dc55 100644 --- a/tox.ini +++ b/tox.ini @@ -66,6 +66,7 @@ deps = commands_pre = pip install -e {toxinidir}/opentelemetry-api pip install -e {toxinidir}/opentelemetry-sdk + pip install -e {toxinidir}/ext/opentelemetry-ext-azure-monitor pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests pip install -e {toxinidir}/opentelemetry-example-app @@ -78,6 +79,9 @@ commands = opentelemetry-api/tests/ \ opentelemetry-sdk/src/opentelemetry \ opentelemetry-sdk/tests/ \ + ext/opentelemetry-ext-azure-monitor/examples/ \ + ext/opentelemetry-ext-azure-monitor/src/ \ + ext/opentelemetry-ext-azure-monitor/tests/ \ ext/opentelemetry-ext-http-requests/src/ \ ext/opentelemetry-ext-http-requests/tests/ \ ext/opentelemetry-ext-wsgi/tests/ \ From 17961e615639e8f40f84464c4b52141bb2d18b23 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Wed, 25 Sep 2019 08:19:00 -0700 Subject: [PATCH 0081/1517] Add link to docs to README (#170) --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a7fbd3efd7..097f3e146c 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,11 @@ with tracer.start_span('foo'): print(Context) ``` -See [opentelemetry-example-app](./opentelemetry-example-app/README.rst) for a complete example. +See the [API +documentation](https://open-telemetry.github.io/opentelemetry-python/) for more +detail, and the +[opentelemetry-example-app](./opentelemetry-example-app/README.rst) for a +complete example. ## Contributing From 77adc46ae250f7d89a2af5d80e8ab163ca2fdda8 Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Wed, 25 Sep 2019 12:46:41 -0700 Subject: [PATCH 0082/1517] Move example app to the examples folder (#172) --- README.md | 4 ++-- .../opentelemetry-example-app}/README.rst | 0 .../opentelemetry-example-app}/setup.py | 0 .../src/opentelemetry_example_app/__init__.py | 0 .../opentelemetry_example_app/flask_example.py | 0 .../opentelemetry-example-app}/tests/__init__.py | 0 .../tests/test_flask_example.py | 0 tox.ini | 16 ++++++++-------- 8 files changed, 10 insertions(+), 10 deletions(-) rename {opentelemetry-example-app => examples/opentelemetry-example-app}/README.rst (100%) rename {opentelemetry-example-app => examples/opentelemetry-example-app}/setup.py (100%) rename {opentelemetry-example-app => examples/opentelemetry-example-app}/src/opentelemetry_example_app/__init__.py (100%) rename {opentelemetry-example-app => examples/opentelemetry-example-app}/src/opentelemetry_example_app/flask_example.py (100%) rename {opentelemetry-example-app => examples/opentelemetry-example-app}/tests/__init__.py (100%) rename {opentelemetry-example-app => examples/opentelemetry-example-app}/tests/test_flask_example.py (100%) diff --git a/README.md b/README.md index 097f3e146c..4b4e0f25ab 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,8 @@ with tracer.start_span('foo'): See the [API documentation](https://open-telemetry.github.io/opentelemetry-python/) for more detail, and the -[opentelemetry-example-app](./opentelemetry-example-app/README.rst) for a -complete example. +[opentelemetry-example-app](./examples/opentelemetry-example-app/README.rst) +for a complete example. ## Contributing diff --git a/opentelemetry-example-app/README.rst b/examples/opentelemetry-example-app/README.rst similarity index 100% rename from opentelemetry-example-app/README.rst rename to examples/opentelemetry-example-app/README.rst diff --git a/opentelemetry-example-app/setup.py b/examples/opentelemetry-example-app/setup.py similarity index 100% rename from opentelemetry-example-app/setup.py rename to examples/opentelemetry-example-app/setup.py diff --git a/opentelemetry-example-app/src/opentelemetry_example_app/__init__.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/__init__.py similarity index 100% rename from opentelemetry-example-app/src/opentelemetry_example_app/__init__.py rename to examples/opentelemetry-example-app/src/opentelemetry_example_app/__init__.py diff --git a/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py similarity index 100% rename from opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py rename to examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py diff --git a/opentelemetry-example-app/tests/__init__.py b/examples/opentelemetry-example-app/tests/__init__.py similarity index 100% rename from opentelemetry-example-app/tests/__init__.py rename to examples/opentelemetry-example-app/tests/__init__.py diff --git a/opentelemetry-example-app/tests/test_flask_example.py b/examples/opentelemetry-example-app/tests/test_flask_example.py similarity index 100% rename from opentelemetry-example-app/tests/test_flask_example.py rename to examples/opentelemetry-example-app/tests/test_flask_example.py diff --git a/tox.ini b/tox.ini index 871931dc55..f8d2f55d28 100644 --- a/tox.ini +++ b/tox.ini @@ -22,9 +22,9 @@ setenv = changedir = test-api: opentelemetry-api/tests test-sdk: opentelemetry-sdk/tests - test-example-app: opentelemetry-example-app/tests - test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests + test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests + test-example-app: examples/opentelemetry-example-app/tests commands_pre = ; Install without -e to test the actual installation @@ -32,9 +32,9 @@ commands_pre = test: pip install {toxinidir}/opentelemetry-api test-sdk: pip install {toxinidir}/opentelemetry-sdk example-app: pip install {toxinidir}/opentelemetry-sdk - example-app: pip install {toxinidir}/ext/opentelemetry-ext-wsgi example-app: pip install {toxinidir}/ext/opentelemetry-ext-http-requests - example-app: pip install {toxinidir}/opentelemetry-example-app + example-app: pip install {toxinidir}/ext/opentelemetry-ext-wsgi + example-app: pip install {toxinidir}/examples/opentelemetry-example-app ext: pip install {toxinidir}/opentelemetry-api wsgi: pip install {toxinidir}/ext/opentelemetry-ext-wsgi http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests @@ -67,9 +67,9 @@ commands_pre = pip install -e {toxinidir}/opentelemetry-api pip install -e {toxinidir}/opentelemetry-sdk pip install -e {toxinidir}/ext/opentelemetry-ext-azure-monitor - pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests - pip install -e {toxinidir}/opentelemetry-example-app + pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi + pip install -e {toxinidir}/examples/opentelemetry-example-app commands = ; Prefer putting everything in one pylint command to profit from duplication @@ -85,8 +85,8 @@ commands = ext/opentelemetry-ext-http-requests/src/ \ ext/opentelemetry-ext-http-requests/tests/ \ ext/opentelemetry-ext-wsgi/tests/ \ - opentelemetry-example-app/src/opentelemetry_example_app/ \ - opentelemetry-example-app/tests/ + examples/opentelemetry-example-app/src/opentelemetry_example_app/ \ + examples/opentelemetry-example-app/tests/ flake8 . isort --check-only --diff --recursive . From e14076d4a1327c57e43a673a058717ed93dde92b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 25 Sep 2019 22:48:35 +0200 Subject: [PATCH 0083/1517] WSGI: Fix port 80 always appended in http.host (#173) --- .../src/opentelemetry/ext/wsgi/__init__.py | 8 +++++--- .../tests/test_wsgi_middleware.py | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 61f061948d..5e619eb7c6 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -48,10 +48,12 @@ def _add_request_attributes(span, environ): if not host: host = environ["SERVER_NAME"] port = environ["SERVER_PORT"] + scheme = environ["wsgi.url_scheme"] if ( - port != "80" - and environ["wsgi.url_scheme"] == "http" - or port != "443" + scheme == "http" + and port != "80" + or scheme == "https" + and port != "443" ): host += ":" + port diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index cd1778c492..a88782d642 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -203,9 +203,7 @@ def validate_url(self, expected_url): self.assertIn("http.url", attrs) self.assertEqual(attrs["http.url"], expected_url) self.assertIn("http.host", attrs) - self.assertEqual( - attrs["http.host"], urlparse(attrs["http.url"]).netloc - ) + self.assertEqual(attrs["http.host"], urlparse(expected_url).netloc) def test_request_attributes_with_partial_raw_uri(self): self.environ["RAW_URI"] = "/#top" @@ -231,6 +229,18 @@ def test_https_uri_port(self): self.environ["SERVER_PORT"] = "80" self.validate_url("https://127.0.0.1:80/") + def test_http_uri_port(self): + del self.environ["HTTP_HOST"] + self.environ["SERVER_PORT"] = "80" + self.environ["wsgi.url_scheme"] = "http" + self.validate_url("http://127.0.0.1/") + + self.environ["SERVER_PORT"] = "8080" + self.validate_url("http://127.0.0.1:8080/") + + self.environ["SERVER_PORT"] = "443" + self.validate_url("http://127.0.0.1:443/") + def test_request_attributes_with_nonstandard_port_and_no_host(self): del self.environ["HTTP_HOST"] self.environ["SERVER_PORT"] = "8080" From 81cd8d616e9b7715354c3da6ac56d74f98d38850 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Wed, 25 Sep 2019 14:41:03 -0700 Subject: [PATCH 0084/1517] Build and host docs via github action (#167) --- .github/workflows/docs.yml | 23 +++++++++++++++++++++++ docs/conf.py | 3 +++ 2 files changed, 26 insertions(+) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000..423396ea8d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,23 @@ +name: Docs + +on: + push: + branches: + - master + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-python@v1 + - name: Build docs + run: | + pip install --upgrade tox + tox -e docs + - name: Publish to gh-pages + uses: JamesIves/github-pages-deploy-action@2.0.2 + env: + ACCESS_TOKEN: ${{ secrets.DocsPushToken }} + BRANCH: gh-pages + FOLDER: docs/_build/html/ diff --git a/docs/conf.py b/docs/conf.py index d719ebe931..694ba7f005 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,6 +42,9 @@ "sphinx.ext.viewcode", # Link to other sphinx docs "sphinx.ext.intersphinx", + # Add a .nojekyll file to the generated HTML docs + # https://help.github.com/en/articles/files-that-start-with-an-underscore-are-missing + "sphinx.ext.githubpages", ] intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} From 84f589b6f8a4770c3c6ce29c7b352b1972241c4f Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 26 Sep 2019 07:55:50 -0700 Subject: [PATCH 0085/1517] Add missing license boilerplate to a few files (#176) --- .../tests/test_flask_example.py | 14 ++++++++++++++ .../tests/test_requests_integration.py | 14 ++++++++++++++ .../opentelemetry/context/propagation/__init__.py | 14 ++++++++++++++ .../src/opentelemetry/propagators/__init__.py | 14 ++++++++++++++ opentelemetry-api/tests/mypysmoke.py | 14 ++++++++++++++ opentelemetry-sdk/tests/resources/test_init.py | 14 ++++++++++++++ 6 files changed, 84 insertions(+) diff --git a/examples/opentelemetry-example-app/tests/test_flask_example.py b/examples/opentelemetry-example-app/tests/test_flask_example.py index d33e2b9ac0..fd0b89e98c 100644 --- a/examples/opentelemetry-example-app/tests/test_flask_example.py +++ b/examples/opentelemetry-example-app/tests/test_flask_example.py @@ -1,3 +1,17 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import unittest from unittest import mock diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index d2624f63d1..fe0cd88607 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -1,3 +1,17 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import sys import unittest from unittest import mock diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py index b964c2a968..c8706281ad 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py @@ -1,3 +1,17 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from .binaryformat import BinaryFormat from .httptextformat import HTTPTextFormat diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index aa59669407..5b71e8785a 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -1,3 +1,17 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import typing import opentelemetry.context.propagation.httptextformat as httptextformat diff --git a/opentelemetry-api/tests/mypysmoke.py b/opentelemetry-api/tests/mypysmoke.py index 25601ebf84..7badc13b69 100644 --- a/opentelemetry-api/tests/mypysmoke.py +++ b/opentelemetry-api/tests/mypysmoke.py @@ -1,3 +1,17 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import opentelemetry.trace diff --git a/opentelemetry-sdk/tests/resources/test_init.py b/opentelemetry-sdk/tests/resources/test_init.py index 47e986bd35..2afe17e563 100644 --- a/opentelemetry-sdk/tests/resources/test_init.py +++ b/opentelemetry-sdk/tests/resources/test_init.py @@ -1,3 +1,17 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import unittest from opentelemetry.sdk import resources From d0946cdcc4edaaa39122989bf05f03a38ebae8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 27 Sep 2019 14:14:08 +0200 Subject: [PATCH 0086/1517] sdk/trace/exporters: add batch span processor exporter (#153) The exporters specification states that two built-in span processors should be implemented, the simple processor span and the batch processor span. This commit implements the latter, it is mainly based on the opentelemetry/java one. The algorithm implements the following logic: - a condition variable is used to notify the worker thread in case the queue is half full, so that exporting can start before the queue gets full and spans are dropped. - export is called each schedule_delay_millis if there is a least one new span to export. - when the processor is shutdown all remaining spans are exported. --- .../sdk/trace/export/__init__.py | 122 +++++++++++++ .../tests/trace/export/test_export.py | 167 +++++++++++++++++- 2 files changed, 281 insertions(+), 8 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 52d6d3bd22..66122985c0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -12,10 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import collections import logging +import threading import typing from enum import Enum +from opentelemetry.sdk import util + from .. import Span, SpanProcessor logger = logging.getLogger(__name__) @@ -78,6 +82,124 @@ def shutdown(self) -> None: self.span_exporter.shutdown() +class BatchExportSpanProcessor(SpanProcessor): + """Batch span processor implementation. + + BatchExportSpanProcessor is an implementation of `SpanProcessor` that + batches ended spans and pushes them to the configured `SpanExporter`. + """ + + def __init__( + self, + span_exporter: SpanExporter, + max_queue_size: int = 2048, + schedule_delay_millis: float = 5000, + max_export_batch_size: int = 512, + ): + if max_queue_size <= 0: + raise ValueError("max_queue_size must be a positive integer.") + + if schedule_delay_millis <= 0: + raise ValueError("schedule_delay_millis must be positive.") + + if max_export_batch_size <= 0: + raise ValueError( + "max_export_batch_size must be a positive integer." + ) + + if max_export_batch_size > max_queue_size: + raise ValueError( + "max_export_batch_size must be less than and equal to max_export_batch_size." + ) + + self.span_exporter = span_exporter + self.queue = collections.deque([], max_queue_size) + self.worker_thread = threading.Thread(target=self.worker, daemon=True) + self.condition = threading.Condition(threading.Lock()) + self.schedule_delay_millis = schedule_delay_millis + self.max_export_batch_size = max_export_batch_size + self.max_queue_size = max_queue_size + self.done = False + # flag that indicates that spans are being dropped + self._spans_dropped = False + # precallocated list to send spans to exporter + self.spans_list = [None] * self.max_export_batch_size + self.worker_thread.start() + + def on_start(self, span: Span) -> None: + pass + + def on_end(self, span: Span) -> None: + if self.done: + logging.warning("Already shutdown, dropping span.") + return + if len(self.queue) == self.max_queue_size: + if not self._spans_dropped: + logging.warning("Queue is full, likely spans will be dropped.") + self._spans_dropped = True + + self.queue.appendleft(span) + + if len(self.queue) >= self.max_queue_size // 2: + with self.condition: + self.condition.notify() + + def worker(self): + timeout = self.schedule_delay_millis / 1e3 + while not self.done: + if len(self.queue) < self.max_export_batch_size: + with self.condition: + self.condition.wait(timeout) + if not self.queue: + # spurious notification, let's wait again + continue + if self.done: + # missing spans will be sent when calling flush + break + + # substract the duration of this export call to the next timeout + start = util.time_ns() + self.export() + end = util.time_ns() + duration = (end - start) / 1e9 + timeout = self.schedule_delay_millis / 1e3 - duration + + # be sure that all spans are sent + self._flush() + + def export(self) -> bool: + """Exports at most max_export_batch_size spans.""" + idx = 0 + + # currently only a single thread acts as consumer, so queue.pop() will + # not raise an exception + while idx < self.max_export_batch_size and self.queue: + self.spans_list[idx] = self.queue.pop() + idx += 1 + try: + self.span_exporter.export(self.spans_list[:idx]) + # pylint: disable=broad-except + except Exception: + logger.exception("Exception while exporting data.") + + # clean up list + for index in range(idx): + self.spans_list[index] = None + + def _flush(self): + # export all elements until queue is empty + while self.queue: + self.export() + + def shutdown(self) -> None: + # signal the worker thread to finish and then wait for it + self.done = True + with self.condition: + self.condition.notify_all() + self.worker_thread.join() + self.span_exporter.shutdown() + + class ConsoleSpanExporter(SpanExporter): """Implementation of :class:`SpanExporter` that prints spans to the console. diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index ef9786ca63..de7a5cd9d7 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -12,22 +12,34 @@ # See the License for the specific language governing permissions and # limitations under the License. +import time import unittest +from unittest import mock +from opentelemetry import trace as trace_api from opentelemetry.sdk import trace from opentelemetry.sdk.trace import export -class TestSimpleExportSpanProcessor(unittest.TestCase): - def test_simple_span_processor(self): - class MySpanExporter(export.SpanExporter): - def __init__(self, destination): - self.destination = destination +class MySpanExporter(export.SpanExporter): + """Very simple span exporter used for testing.""" + + def __init__(self, destination, max_export_batch_size=None): + self.destination = destination + self.max_export_batch_size = max_export_batch_size - def export(self, spans: trace.Span) -> export.SpanExportResult: - self.destination.extend(span.name for span in spans) - return export.SpanExportResult.SUCCESS + def export(self, spans: trace.Span) -> export.SpanExportResult: + if ( + self.max_export_batch_size is not None + and len(spans) > self.max_export_batch_size + ): + raise ValueError("Batch is too big") + self.destination.extend(span.name for span in spans) + return export.SpanExportResult.SUCCESS + +class TestSimpleExportSpanProcessor(unittest.TestCase): + def test_simple_span_processor(self): tracer = trace.Tracer() spans_names_list = [] @@ -42,3 +54,142 @@ def export(self, spans: trace.Span) -> export.SpanExportResult: pass self.assertListEqual(["xxx", "bar", "foo"], spans_names_list) + + +def _create_start_and_end_span(name, span_processor): + span = trace.Span( + name, + mock.Mock(spec=trace_api.SpanContext), + span_processor=span_processor, + ) + span.start() + span.end() + + +class TestBatchExportSpanProcessor(unittest.TestCase): + def test_batch_span_processor(self): + spans_names_list = [] + + my_exporter = MySpanExporter(destination=spans_names_list) + span_processor = export.BatchExportSpanProcessor(my_exporter) + + span_names = ["xxx", "bar", "foo"] + + for name in span_names: + _create_start_and_end_span(name, span_processor) + + span_processor.shutdown() + self.assertListEqual(span_names, spans_names_list) + + def test_batch_span_processor_lossless(self): + """Test that no spans are lost when sending max_queue_size spans""" + spans_names_list = [] + + my_exporter = MySpanExporter( + destination=spans_names_list, max_export_batch_size=128 + ) + span_processor = export.BatchExportSpanProcessor( + my_exporter, max_queue_size=512, max_export_batch_size=128 + ) + + for _ in range(512): + _create_start_and_end_span("foo", span_processor) + + span_processor.shutdown() + self.assertEqual(len(spans_names_list), 512) + + def test_batch_span_processor_many_spans(self): + """Test that no spans are lost when sending many spans""" + spans_names_list = [] + + my_exporter = MySpanExporter( + destination=spans_names_list, max_export_batch_size=128 + ) + span_processor = export.BatchExportSpanProcessor( + my_exporter, + max_queue_size=256, + max_export_batch_size=64, + schedule_delay_millis=100, + ) + + for _ in range(4): + for _ in range(256): + _create_start_and_end_span("foo", span_processor) + + time.sleep(0.05) # give some time for the exporter to upload spans + + span_processor.shutdown() + self.assertEqual(len(spans_names_list), 1024) + + def test_batch_span_processor_scheduled_delay(self): + """Test that spans are exported each schedule_delay_millis""" + spans_names_list = [] + + my_exporter = MySpanExporter(destination=spans_names_list) + span_processor = export.BatchExportSpanProcessor( + my_exporter, schedule_delay_millis=50 + ) + + # create single span + _create_start_and_end_span("foo", span_processor) + + time.sleep(0.05 + 0.02) + # span should be already exported + self.assertEqual(len(spans_names_list), 1) + + span_processor.shutdown() + + def test_batch_span_processor_parameters(self): + # zero max_queue_size + self.assertRaises( + ValueError, export.BatchExportSpanProcessor, None, max_queue_size=0 + ) + + # negative max_queue_size + self.assertRaises( + ValueError, + export.BatchExportSpanProcessor, + None, + max_queue_size=-500, + ) + + # zero schedule_delay_millis + self.assertRaises( + ValueError, + export.BatchExportSpanProcessor, + None, + schedule_delay_millis=0, + ) + + # negative schedule_delay_millis + self.assertRaises( + ValueError, + export.BatchExportSpanProcessor, + None, + schedule_delay_millis=-500, + ) + + # zero max_export_batch_size + self.assertRaises( + ValueError, + export.BatchExportSpanProcessor, + None, + max_export_batch_size=0, + ) + + # negative max_export_batch_size + self.assertRaises( + ValueError, + export.BatchExportSpanProcessor, + None, + max_export_batch_size=-500, + ) + + # max_export_batch_size > max_queue_size: + self.assertRaises( + ValueError, + export.BatchExportSpanProcessor, + None, + max_queue_size=256, + max_export_batch_size=512, + ) From 92353c0fe6a411836b024332e2cabdae06a2ea83 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Fri, 27 Sep 2019 13:03:10 -0700 Subject: [PATCH 0087/1517] Implementing W3C TraceContext (fixes #116) (#180) * Implementing TraceContext (fixes #116) This introduces a w3c TraceContext propagator, primarily inspired by opencensus. --- .../context/propagation/httptextformat.py | 4 +- .../propagation/tracecontexthttptextformat.py | 135 ++++++++++- opentelemetry-api/tests/context/__init__.py | 0 .../tests/context/propagation/__init__.py | 0 .../test_tracecontexthttptextformat.py | 215 ++++++++++++++++++ 5 files changed, 346 insertions(+), 8 deletions(-) create mode 100644 opentelemetry-api/tests/context/__init__.py create mode 100644 opentelemetry-api/tests/context/propagation/__init__.py create mode 100644 opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py index 35bdfbb3fe..9b6098a9a4 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py @@ -19,8 +19,8 @@ _T = typing.TypeVar("_T") -Setter = typing.Callable[[typing.Type[_T], str, str], None] -Getter = typing.Callable[[typing.Type[_T], str], typing.List[str]] +Setter = typing.Callable[[_T, str, str], None] +Getter = typing.Callable[[_T, str], typing.List[str]] class HTTPTextFormat(abc.ABC): diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py index 575644a91f..abe778db95 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # - +import re import typing import opentelemetry.trace as trace @@ -20,20 +20,143 @@ _T = typing.TypeVar("_T") +# Keys and values are strings of up to 256 printable US-ASCII characters. +# Implementations should conform to the the `W3C Trace Context - Tracestate`_ +# spec, which describes additional restrictions on valid field values. +# +# .. _W3C Trace Context - Tracestate: +# https://www.w3.org/TR/trace-context/#tracestate-field + + +_KEY_WITHOUT_VENDOR_FORMAT = r"[a-z][_0-9a-z\-\*\/]{0,255}" +_KEY_WITH_VENDOR_FORMAT = ( + r"[a-z][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}" +) + +_KEY_FORMAT = _KEY_WITHOUT_VENDOR_FORMAT + "|" + _KEY_WITH_VENDOR_FORMAT +_VALUE_FORMAT = ( + r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]" +) + +_DELIMITER_FORMAT = "[ \t]*,[ \t]*" +_MEMBER_FORMAT = "({})(=)({})".format(_KEY_FORMAT, _VALUE_FORMAT) + +_DELIMITER_FORMAT_RE = re.compile(_DELIMITER_FORMAT) +_MEMBER_FORMAT_RE = re.compile(_MEMBER_FORMAT) + class TraceContextHTTPTextFormat(httptextformat.HTTPTextFormat): - """TODO: extracts and injects using w3c TraceContext's headers. + """Extracts and injects using w3c TraceContext's headers. """ + _TRACEPARENT_HEADER_NAME = "traceparent" + _TRACESTATE_HEADER_NAME = "tracestate" + _TRACEPARENT_HEADER_FORMAT = ( + "^[ \t]*([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})" + + "(-.*)?[ \t]*$" + ) + _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + + @classmethod def extract( - self, _get_from_carrier: httptextformat.Getter[_T], _carrier: _T + cls, get_from_carrier: httptextformat.Getter[_T], carrier: _T ) -> trace.SpanContext: - return trace.INVALID_SPAN_CONTEXT + """Extracts a valid SpanContext from the carrier. + """ + header = get_from_carrier(carrier, cls._TRACEPARENT_HEADER_NAME) + + if not header: + return trace.INVALID_SPAN_CONTEXT + + match = re.search(cls._TRACEPARENT_HEADER_FORMAT_RE, header[0]) + if not match: + return trace.INVALID_SPAN_CONTEXT + + version = match.group(1) + trace_id = match.group(2) + span_id = match.group(3) + trace_options = match.group(4) + + if trace_id == "0" * 32 or span_id == "0" * 16: + return trace.INVALID_SPAN_CONTEXT + + if version == "00": + if match.group(5): + return trace.INVALID_SPAN_CONTEXT + if version == "ff": + return trace.INVALID_SPAN_CONTEXT + tracestate = trace.TraceState() + for tracestate_header in get_from_carrier( + carrier, cls._TRACESTATE_HEADER_NAME + ): + # typing.Dict's update is not recognized by pylint: + # https://github.com/PyCQA/pylint/issues/2420 + tracestate.update( # pylint:disable=E1101 + _parse_tracestate(tracestate_header) + ) + + span_context = trace.SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id, 16), + trace_options=trace.TraceOptions(trace_options), + trace_state=tracestate, + ) + + return span_context + + @classmethod def inject( - self, + cls, context: trace.SpanContext, set_in_carrier: httptextformat.Setter[_T], carrier: _T, ) -> None: - pass + if context == trace.INVALID_SPAN_CONTEXT: + return + traceparent_string = "00-{:032x}-{:016x}-{:02x}".format( + context.trace_id, context.span_id, context.trace_options + ) + set_in_carrier( + carrier, cls._TRACEPARENT_HEADER_NAME, traceparent_string + ) + if context.trace_state: + tracestate_string = _format_tracestate(context.trace_state) + set_in_carrier( + carrier, cls._TRACESTATE_HEADER_NAME, tracestate_string + ) + + +def _parse_tracestate(string: str) -> trace.TraceState: + """Parse a w3c tracestate header into a TraceState. + + Args: + string: the value of the tracestate header. + + Returns: + A valid TraceState that contains values extracted from + the tracestate header. + """ + tracestate = trace.TraceState() + for member in re.split(_DELIMITER_FORMAT_RE, string): + match = _MEMBER_FORMAT_RE.match(member) + if not match: + raise ValueError("illegal key-value format %r" % (member)) + key, _eq, value = match.groups() + # typing.Dict's update is not recognized by pylint: + # https://github.com/PyCQA/pylint/issues/2420 + tracestate[key] = value # pylint:disable=E1137 + return tracestate + + +def _format_tracestate(tracestate: trace.TraceState) -> str: + """Parse a w3c tracestate header into a TraceState. + + Args: + tracestate: the tracestate header to write + + Returns: + A string that adheres to the w3c tracestate + header format. + """ + return ",".join(key + "=" + value for key, value in tracestate.items()) diff --git a/opentelemetry-api/tests/context/__init__.py b/opentelemetry-api/tests/context/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-api/tests/context/propagation/__init__.py b/opentelemetry-api/tests/context/propagation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py new file mode 100644 index 0000000000..aaf392be24 --- /dev/null +++ b/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py @@ -0,0 +1,215 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing +import unittest + +from opentelemetry import trace +from opentelemetry.context.propagation import tracecontexthttptextformat + +FORMAT = tracecontexthttptextformat.TraceContextHTTPTextFormat() + + +def get_as_list( + dict_object: typing.Dict[str, str], key: str +) -> typing.List[str]: + value = dict_object.get(key) + return [value] if value is not None else [] + + +class TestTraceContextFormat(unittest.TestCase): + TRACE_ID = int("12345678901234567890123456789012", 16) # type:int + SPAN_ID = int("1234567890123456", 16) # type:int + + def test_no_traceparent_header(self): + """When tracecontext headers are not present, a new SpanContext + should be created. + + RFC 4.2.2: + + If no traceparent header is received, the vendor creates a new trace-id and parent-id that represents the current request. + """ + output = {} # type:typing.Dict[str, str] + span_context = FORMAT.extract(get_as_list, output) + self.assertTrue(isinstance(span_context, trace.SpanContext)) + + def test_from_headers_tracestate_entry_limit(self): + """If more than 33 entries are passed, allow them. + + We are explicitly choosing not to limit the list members + as outlined in RFC 3.3.1.1 + + RFC 3.3.1.1 + + There can be a maximum of 32 list-members in a list. + """ + + span_context = FORMAT.extract( + get_as_list, + { + "traceparent": "00-12345678901234567890123456789012-1234567890123456-00", + "tracestate": ",".join( + [ + "a00=0,a01=1,a02=2,a03=3,a04=4,a05=5,a06=6,a07=7,a08=8,a09=9", + "b00=0,b01=1,b02=2,b03=3,b04=4,b05=5,b06=6,b07=7,b08=8,b09=9", + "c00=0,c01=1,c02=2,c03=3,c04=4,c05=5,c06=6,c07=7,c08=8,c09=9", + "d00=0,d01=1,d02=2", + ] + ), + }, + ) + self.assertEqual(len(span_context.trace_state), 33) + + def test_from_headers_tracestate_duplicated_keys(self): + """If a duplicate tracestate header is present, the most recent entry + is used. + + RFC 3.3.1.4 + + Only one entry per key is allowed because the entry represents that last position in the trace. + Hence vendors must overwrite their entry upon reentry to their tracing system. + + For example, if a vendor name is Congo and a trace started in their system and then went through + a system named Rojo and later returned to Congo, the tracestate value would not be: + + congo=congosFirstPosition,rojo=rojosFirstPosition,congo=congosSecondPosition + + Instead, the entry would be rewritten to only include the most recent position: + + congo=congosSecondPosition,rojo=rojosFirstPosition + """ + span_context = FORMAT.extract( + get_as_list, + { + "traceparent": "00-12345678901234567890123456789012-1234567890123456-00", + "tracestate": "foo=1,bar=2,foo=3", + }, + ) + self.assertEqual(span_context.trace_state, {"foo": "3", "bar": "2"}) + + def test_headers_with_tracestate(self): + """When there is a traceparent and tracestate header, data from + both should be addded to the SpanContext. + """ + traceparent_value = "00-{trace_id}-{span_id}-00".format( + trace_id=format(self.TRACE_ID, "032x"), + span_id=format(self.SPAN_ID, "016x"), + ) + tracestate_value = "foo=1,bar=2,baz=3" + span_context = FORMAT.extract( + get_as_list, + {"traceparent": traceparent_value, "tracestate": tracestate_value}, + ) + self.assertEqual(span_context.trace_id, self.TRACE_ID) + self.assertEqual(span_context.span_id, self.SPAN_ID) + self.assertEqual( + span_context.trace_state, {"foo": "1", "bar": "2", "baz": "3"} + ) + + output = {} # type:typing.Dict[str, str] + FORMAT.inject(span_context, dict.__setitem__, output) + self.assertEqual(output["traceparent"], traceparent_value) + for pair in ["foo=1", "bar=2", "baz=3"]: + self.assertIn(pair, output["tracestate"]) + self.assertEqual(output["tracestate"].count(","), 2) + + def test_invalid_trace_id(self): + """If the trace id is invalid, we must ignore the full traceparent header. + + Also ignore any tracestate. + + RFC 3.2.2.3 + + If the trace-id value is invalid (for example if it contains non-allowed characters or all + zeros), vendors MUST ignore the traceparent. + + RFC 3.3 + + If the vendor failed to parse traceparent, it MUST NOT attempt to parse tracestate. + Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. + """ + span_context = FORMAT.extract( + get_as_list, + { + "traceparent": "00-00000000000000000000000000000000-1234567890123456-00", + "tracestate": "foo=1,bar=2,foo=3", + }, + ) + self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) + + def test_invalid_parent_id(self): + """If the parent id is invalid, we must ignore the full traceparent header. + + Also ignore any tracestate. + + RFC 3.2.2.3 + + Vendors MUST ignore the traceparent when the parent-id is invalid (for example, + if it contains non-lowercase hex characters). + + RFC 3.3 + + If the vendor failed to parse traceparent, it MUST NOT attempt to parse tracestate. + Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. + """ + span_context = FORMAT.extract( + get_as_list, + { + "traceparent": "00-00000000000000000000000000000000-0000000000000000-00", + "tracestate": "foo=1,bar=2,foo=3", + }, + ) + self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) + + def test_no_send_empty_tracestate(self): + """If the tracestate is empty, do not set the header. + + RFC 3.3.1.1 + + Empty and whitespace-only list members are allowed. Vendors MUST accept empty + tracestate headers but SHOULD avoid sending them. + """ + output = {} # type:typing.Dict[str, str] + FORMAT.inject( + trace.SpanContext(self.TRACE_ID, self.SPAN_ID), + dict.__setitem__, + output, + ) + self.assertTrue("traceparent" in output) + self.assertFalse("tracestate" in output) + + def test_format_not_supported(self): + """If the traceparent does not adhere to the supported format, discard it and + create a new tracecontext. + + RFC 4.3 + + If the version cannot be parsed, the vendor creates a new traceparent header and + deletes tracestate. + """ + span_context = FORMAT.extract( + get_as_list, + { + "traceparent": "00-12345678901234567890123456789012-1234567890123456-00-residue", + "tracestate": "foo=1,bar=2,foo=3", + }, + ) + self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) + + def test_propagate_invalid_context(self): + """Do not propagate invalid trace context. + """ + output = {} # type:typing.Dict[str, str] + FORMAT.inject(trace.INVALID_SPAN_CONTEXT, dict.__setitem__, output) + self.assertFalse("traceparent" in output) From 2ab7b9dc7ed6afff8607bacaf93f32662396f250 Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Fri, 27 Sep 2019 14:00:01 -0700 Subject: [PATCH 0088/1517] fix time conversion bug (#182) --- opentelemetry-sdk/src/opentelemetry/sdk/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util.py b/opentelemetry-sdk/src/opentelemetry/sdk/util.py index d855e85179..4f4ec82b9d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util.py @@ -26,5 +26,5 @@ def time_ns(): def ns_to_iso_str(nanoseconds): """Get an ISO 8601 string from time_ns value.""" - ts = datetime.datetime.fromtimestamp(nanoseconds / 1e9) + ts = datetime.datetime.utcfromtimestamp(nanoseconds / 1e9) return ts.strftime("%Y-%m-%dT%H:%M:%S.%fZ") From 74f2cec9dda968cf414c1da28b3ca3e2696ebe27 Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Fri, 27 Sep 2019 14:24:09 -0700 Subject: [PATCH 0089/1517] Introduce Context.suppress_instrumentation (#181) --- .../src/opentelemetry/ext/http_requests/__init__.py | 5 +++-- .../src/opentelemetry/sdk/trace/export/__init__.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index 994968e196..a117da5daa 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -23,6 +23,7 @@ from requests.sessions import Session from opentelemetry import propagators +from opentelemetry.context import Context from opentelemetry.trace import SpanKind @@ -50,8 +51,8 @@ def enable(tracer): @functools.wraps(wrapped) def instrumented_request(self, method, url, *args, **kwargs): - # TODO: Check if we are in an exporter, cf. OpenCensus - # execution_context.is_exporter() + if Context.suppress_instrumentation: + return wrapped(self, method, url, *args, **kwargs) # See # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#http-client diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 66122985c0..2f42c19c39 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -18,6 +18,7 @@ import typing from enum import Enum +from opentelemetry.context import Context from opentelemetry.sdk import util from .. import Span, SpanProcessor @@ -72,11 +73,15 @@ def on_start(self, span: Span) -> None: pass def on_end(self, span: Span) -> None: + suppress_instrumentation = Context.suppress_instrumentation try: + Context.suppress_instrumentation = True self.span_exporter.export((span,)) # pylint: disable=broad-except except Exception as exc: logger.warning("Exception while exporting data: %s", exc) + finally: + Context.suppress_instrumentation = suppress_instrumentation def shutdown(self) -> None: self.span_exporter.shutdown() @@ -176,11 +181,15 @@ def export(self) -> bool: while idx < self.max_export_batch_size and self.queue: self.spans_list[idx] = self.queue.pop() idx += 1 + suppress_instrumentation = Context.suppress_instrumentation try: + Context.suppress_instrumentation = True self.span_exporter.export(self.spans_list[:idx]) # pylint: disable=broad-except except Exception: logger.exception("Exception while exporting data.") + finally: + Context.suppress_instrumentation = suppress_instrumentation # clean up list for index in range(idx): From 764d3171d7435915bc536da184d6ba44b241581f Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 27 Sep 2019 15:16:05 -0700 Subject: [PATCH 0090/1517] Metrics Implementation (#160) * Create functions Comments for Meter More comments Add more comments Fix typos * fix lint * Fix lint * fix typing * Remove options, constructors, seperate labels * Consistent naming for float and int * Abstract time series * Use ABC * Fix typo * Fix docs * seperate measure classes * Add examples * fix lint * Update to RFC 0003 * Add spancontext, measurebatch * Fix docs * Fix comments * fix lint * fix lint * fix lint * skip examples * white space * fix spacing * fix imports * fix imports * LabelValues to str * Black formatting * fix isort * Remove aggregation * Fix names * Remove aggregation from docs * Fix lint * metric changes * Typing * Fix lint * Fix lint * Add space * Fix lint * fix comments * handle, recordbatch * docs * Update recordbatch * black * Fix typo * remove ValueType * fix lint * sdk * metrics * example * counter * Tests * Address comments * ADd tests * Fix typing and examples * black * fix lint * remove override * Fix tests * mypy * fix lint * fix type * fix typing * fix tests * isort * isort * isort * isort * noop * lint * lint * fix tuple typing * fix type * black * address comments * fix type * fix lint * remove imports * default tests * fix lint * usse sequence * remove ellipses * remove ellipses * black * Fix typo * fix example * fix type * fix type * address comments --- docs/opentelemetry.metrics.handle.rst | 5 - docs/opentelemetry.metrics.rst | 7 - .../metrics_example.py | 37 ++- .../src/opentelemetry/metrics/__init__.py | 295 +++++++++--------- .../metrics/__init__.py} | 19 -- .../tests/metrics/test_metrics.py | 68 ++++ .../src/opentelemetry/sdk/__init__.py | 4 +- .../src/opentelemetry/sdk/metrics/__init__.py | 246 +++++++++++++++ .../src/opentelemetry/sdk/trace/__init__.py | 113 +------ .../src/opentelemetry/sdk/util.py | 113 +++++++ .../tests/metrics/__init__.py | 20 -- .../tests/metrics/test_metrics.py | 189 +++++++++++ 12 files changed, 780 insertions(+), 336 deletions(-) delete mode 100644 docs/opentelemetry.metrics.handle.rst rename opentelemetry-api/src/opentelemetry/metrics/examples/raw.py => examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py (53%) rename opentelemetry-api/{src/opentelemetry/metrics/examples/pre_aggregated.py => tests/metrics/__init__.py} (59%) create mode 100644 opentelemetry-api/tests/metrics/test_metrics.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py rename opentelemetry-api/src/opentelemetry/metrics/handle.py => opentelemetry-sdk/tests/metrics/__init__.py (52%) create mode 100644 opentelemetry-sdk/tests/metrics/test_metrics.py diff --git a/docs/opentelemetry.metrics.handle.rst b/docs/opentelemetry.metrics.handle.rst deleted file mode 100644 index 826a0e4e5a..0000000000 --- a/docs/opentelemetry.metrics.handle.rst +++ /dev/null @@ -1,5 +0,0 @@ -opentelemetry.metrics.handle module -========================================== - -.. automodule:: opentelemetry.metrics.handle - diff --git a/docs/opentelemetry.metrics.rst b/docs/opentelemetry.metrics.rst index 289f1842ba..358d5491a6 100644 --- a/docs/opentelemetry.metrics.rst +++ b/docs/opentelemetry.metrics.rst @@ -1,13 +1,6 @@ opentelemetry.metrics package ============================= -Submodules ----------- - -.. toctree:: - - opentelemetry.metrics.handle - Module contents --------------- diff --git a/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py similarity index 53% rename from opentelemetry-api/src/opentelemetry/metrics/examples/raw.py rename to examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py index 501b2e5b61..dd9509feb2 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py @@ -11,26 +11,29 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +# +""" +This module serves as an example for a simple application using metrics +""" -# pylint: skip-file from opentelemetry import metrics +from opentelemetry.sdk.metrics import Counter, Meter -METER = metrics.Meter() -MEASURE = metrics.create_measure( - "idle_cpu_percentage", - "cpu idle over time", - "percentage", - metrics.ValueType.FLOAT, - ["environment"], +metrics.set_preferred_meter_implementation(lambda _: Meter()) +meter = metrics.meter() +counter = meter.create_metric( + "available memory", + "available memory", + "bytes", + int, + Counter, + ("environment",), ) -# Metrics sent to some exporter -MEASURE_METRIC_TESTING = MEASURE.get_handle("Testing") -MEASURE_METRIC_STAGING = MEASURE.get_handle("Staging") - -# record individual measures -STATISTIC = 100 -MEASURE_METRIC_STAGING.record(STATISTIC) +label_values = ("staging",) +counter_handle = counter.get_handle(label_values) +counter_handle.add(100) +meter.record_batch(label_values, [(counter, 50)]) +print(counter_handle.data) -# Batch recording -METER.record_batch([tuple(MEASURE_METRIC_STAGING, STATISTIC)]) +# TODO: exporters diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 668c61cbc3..61cc8bdfac 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -27,147 +27,44 @@ """ from abc import ABC, abstractmethod -from typing import Dict, List, Tuple, Type, Union +from typing import Callable, Optional, Sequence, Tuple, Type, TypeVar -from opentelemetry.metrics.handle import ( - CounterHandle, - GaugeHandle, - MeasureHandle, -) -from opentelemetry.trace import SpanContext +from opentelemetry.util import loader +ValueT = TypeVar("ValueT", int, float) -# pylint: disable=unused-argument -class Meter: - """An interface to allow the recording of metrics. - - `Metric` s are used for recording pre-defined aggregation (gauge and - counter), or raw values (measure) in which the aggregation and labels - for the exported metric are deferred. - """ - - def record_batch( - self, - label_tuples: Dict[str, str], - record_tuples: List[Tuple["Metric", Union[float, int]]], - ) -> None: - """Atomically records a batch of `Metric` and value pairs. - - Allows the functionality of acting upon multiple metrics with - a single API call. Implementations should find handles that match - the key-value pairs in the label tuples. - - Args: - label_tuples: A collection of key value pairs that will be matched - against to record for the metric-handle that has those labels. - record_tuples: A list of pairs of `Metric` s and the - corresponding value to record for that metric. - """ - - -def create_counter( - name: str, - description: str, - unit: str, - value_type: Union[Type[float], Type[int]], - is_bidirectional: bool = False, - label_keys: List[str] = None, - span_context: SpanContext = None, -) -> Union["FloatCounter", "IntCounter"]: - """Creates a counter metric with type value_type. - By default, counter values can only go up (unidirectional). The API - should reject negative inputs to unidirectional counter metrics. - Counter metrics have a bidirectional option to allow for negative - inputs. +class DefaultMetricHandle: + """The default MetricHandle. - Args: - name: The name of the counter. - description: Human readable description of the metric. - unit: Unit of the metric values. - value_type: The type of values being recorded by the metric. - is_bidirectional: Set to true to allow negative inputs. - label_keys: list of keys for the labels with dynamic values. - Order of the list is important as the same order must be used - on recording when suppling values for these labels. - span_context: The `SpanContext` that identifies the `Span` - that the metric is associated with. - - Returns: A new counter metric for values of the given value_type. + Used when no MetricHandle implementation is available. """ -def create_gauge( - name: str, - description: str, - unit: str, - value_type: Union[Type[float], Type[int]], - is_unidirectional: bool = False, - label_keys: List[str] = None, - span_context: SpanContext = None, -) -> Union["FloatGauge", "IntGauge"]: - """Creates a gauge metric with type value_type. +class CounterHandle: + def add(self, value: ValueT) -> None: + """Increases the value of the handle by ``value``""" - By default, gauge values can go both up and down (bidirectional). The API - allows for an optional unidirectional flag, in which when set will reject - descending update values. - - Args: - name: The name of the gauge. - description: Human readable description of the metric. - unit: Unit of the metric values. - value_type: The type of values being recorded by the metric. - is_unidirectional: Set to true to reject negative inputs. - label_keys: list of keys for the labels with dynamic values. - Order of the list is important as the same order must be used - on recording when suppling values for these labels. - span_context: The `SpanContext` that identifies the `Span` - that the metric is associated with. - - Returns: A new gauge metric for values of the given value_type. - """ +class GaugeHandle: + def set(self, value: ValueT) -> None: + """Sets the current value of the handle to ``value``.""" -def create_measure( - name: str, - description: str, - unit: str, - value_type: Union[Type[float], Type[int]], - is_non_negative: bool = False, - label_keys: List[str] = None, - span_context: SpanContext = None, -) -> Union["FloatMeasure", "IntMeasure"]: - """Creates a measure metric with type value_type. - Measure metrics represent raw statistics that are recorded. As an option, - measure metrics can be declared as non-negative. The API will reject - negative metric events for non-negative measures. - - Args: - name: The name of the measure. - description: Human readable description of the metric. - unit: Unit of the metric values. - value_type: The type of values being recorded by the metric. - is_non_negative: Set to true to reject negative inputs. - label_keys: list of keys for the labels with dynamic values. - Order of the list is important as the same order must be used - on recording when suppling values for these labels. - span_context: The `SpanContext` that identifies the `Span` - that the metric is associated with. - - Returns: A new measure metric for values of the given value_type. - """ +class MeasureHandle: + def record(self, value: ValueT) -> None: + """Records the given ``value`` to this handle.""" class Metric(ABC): """Base class for various types of metrics. Metric class that inherit from this class are specialized with the type of - time series that the metric holds. + handle that the metric holds. """ @abstractmethod - def get_handle(self, label_values: List[str]) -> "object": + def get_handle(self, label_values: Sequence[str]) -> "object": """Gets a handle, used for repeated-use of metrics instruments. Handles are useful to reduce the cost of repeatedly recording a metric @@ -178,60 +75,150 @@ def get_handle(self, label_values: List[str]) -> "object": a value was not provided are permitted. Args: - label_values: A list of label values that will be associated - with the return handle. + label_values: Values to associate with the returned handle. """ - def remove_handle(self, label_values: List[str]) -> None: - """Removes the handle from the Metric, if present. - The handle with matching label values will be removed. +class DefaultMetric(Metric): + """The default Metric used when no Metric implementation is available.""" - args: - label_values: The list of label values to match against. - """ + def get_handle(self, label_values: Sequence[str]) -> "DefaultMetricHandle": + return DefaultMetricHandle() - def clear(self) -> None: - """Removes all handles from the `Metric`.""" +class Counter(Metric): + """A counter type metric that expresses the computation of a sum.""" -class FloatCounter(Metric): - """A counter type metric that holds float values.""" + def get_handle(self, label_values: Sequence[str]) -> "CounterHandle": + """Gets a `CounterHandle`.""" + return CounterHandle() - def get_handle(self, label_values: List[str]) -> "CounterHandle": - """Gets a `CounterHandle` with a float value.""" +class Gauge(Metric): + """A gauge type metric that expresses a pre-calculated value. -class IntCounter(Metric): - """A counter type metric that holds int values.""" + Gauge metrics have a value that is either ``Set`` by explicit + instrumentation or observed through a callback. This kind of metric + should be used when the metric cannot be expressed as a sum or because + the measurement interval is arbitrary. + """ - def get_handle(self, label_values: List[str]) -> "CounterHandle": - """Gets a `CounterHandle` with an int value.""" + def get_handle(self, label_values: Sequence[str]) -> "GaugeHandle": + """Gets a `GaugeHandle`.""" + return GaugeHandle() -class FloatGauge(Metric): - """A gauge type metric that holds float values.""" +class Measure(Metric): + """A measure type metric that represent raw stats that are recorded. - def get_handle(self, label_values: List[str]) -> "GaugeHandle": - """Gets a `GaugeHandle` with a float value.""" + Measure metrics represent raw statistics that are recorded. By + default, measure metrics can accept both positive and negatives. + Negative inputs will be discarded when monotonic is True. + """ + def get_handle(self, label_values: Sequence[str]) -> "MeasureHandle": + """Gets a `MeasureHandle` with a float value.""" + return MeasureHandle() -class IntGauge(Metric): - """A gauge type metric that holds int values.""" - def get_handle(self, label_values: List[str]) -> "GaugeHandle": - """Gets a `GaugeHandle` with an int value.""" +MetricT = TypeVar("MetricT", Counter, Gauge, Measure) -class FloatMeasure(Metric): - """A measure type metric that holds float values.""" +# pylint: disable=unused-argument +class Meter: + """An interface to allow the recording of metrics. - def get_handle(self, label_values: List[str]) -> "MeasureHandle": - """Gets a `MeasureHandle` with a float value.""" + `Metric` s are used for recording pre-defined aggregation (gauge and + counter), or raw values (measure) in which the aggregation and labels + for the exported metric are deferred. + """ + + def record_batch( + self, + label_values: Sequence[str], + record_tuples: Sequence[Tuple["Metric", ValueT]], + ) -> None: + """Atomically records a batch of `Metric` and value pairs. + Allows the functionality of acting upon multiple metrics with + a single API call. Implementations should find metric and handles that + match the key-value pairs in the label tuples. + + Args: + label_values: The values that will be matched against to record for + the handles under each metric that has those labels. + record_tuples: A sequence of pairs of `Metric` s and the + corresponding value to record for that metric. + """ + + def create_metric( + self, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + metric_type: Type[MetricT], + label_keys: Sequence[str] = None, + enabled: bool = True, + monotonic: bool = False, + ) -> "Metric": + """Creates a ``metric_kind`` metric with type ``value_type``. + + Args: + name: The name of the metric. + description: Human-readable description of the metric. + unit: Unit of the metric values. + value_type: The type of values being recorded by the metric. + metric_type: The type of metric being created. + label_keys: The keys for the labels with dynamic values. + Order of the sequence is important as the same order must be + used on recording when suppling values for these labels. + enabled: Whether to report the metric by default. + monotonic: Whether to only allow non-negative values. + + Returns: A new ``metric_type`` metric with values of ``value_type``. + """ + # pylint: disable=no-self-use + return DefaultMetric() + + +# Once https://github.com/python/mypy/issues/7092 is resolved, +# the following type definition should be replaced with +# from opentelemetry.util.loader import ImplementationFactory +ImplementationFactory = Callable[[Type[Meter]], Optional[Meter]] + +_METER = None +_METER_FACTORY = None + + +def meter() -> Meter: + """Gets the current global :class:`~.Meter` object. + + If there isn't one set yet, a default will be loaded. + """ + global _METER, _METER_FACTORY # pylint:disable=global-statement + + if _METER is None: + # pylint:disable=protected-access + _METER = loader._load_impl(Meter, _METER_FACTORY) + del _METER_FACTORY + + return _METER + + +def set_preferred_meter_implementation(factory: ImplementationFactory) -> None: + """Set the factory to be used to create the meter. + + See :mod:`opentelemetry.util.loader` for details. + + This function may not be called after a meter is already loaded. + + Args: + factory: Callback that should create a new :class:`Meter` instance. + """ + global _METER, _METER_FACTORY # pylint:disable=global-statement -class IntMeasure(Metric): - """A measure type metric that holds int values.""" + if _METER: + raise RuntimeError("Meter already loaded.") - def get_handle(self, label_values: List[str]) -> "MeasureHandle": - """Gets a `MeasureHandle` with an int value.""" + _METER_FACTORY = factory diff --git a/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py b/opentelemetry-api/tests/metrics/__init__.py similarity index 59% rename from opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py rename to opentelemetry-api/tests/metrics/__init__.py index 3cbf4d3d20..d853a7bcf6 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py +++ b/opentelemetry-api/tests/metrics/__init__.py @@ -11,22 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -# pylint: skip-file -from opentelemetry import metrics - -METER = metrics.Meter() -COUNTER = METER.create_counter( - "sum numbers", - "sum numbers over time", - "number", - metrics.ValueType.FLOAT, - ["environment"], -) - -# Metrics sent to some exporter -METRIC_TESTING = COUNTER.get_handle("Testing") -METRIC_STAGING = COUNTER.get_handle("Staging") - -for i in range(100): - METRIC_STAGING.add(i) diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py new file mode 100644 index 0000000000..14667f62ea --- /dev/null +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -0,0 +1,68 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import metrics + + +# pylint: disable=no-self-use +class TestMeter(unittest.TestCase): + def setUp(self): + self.meter = metrics.Meter() + + def test_record_batch(self): + counter = metrics.Counter() + self.meter.record_batch(("values"), ((counter, 1))) + + def test_create_metric(self): + metric = self.meter.create_metric("", "", "", float, metrics.Counter) + self.assertIsInstance(metric, metrics.DefaultMetric) + + +class TestMetrics(unittest.TestCase): + def test_default(self): + default = metrics.DefaultMetric() + handle = default.get_handle(("test", "test1")) + self.assertIsInstance(handle, metrics.DefaultMetricHandle) + + def test_counter(self): + counter = metrics.Counter() + handle = counter.get_handle(("test", "test1")) + self.assertIsInstance(handle, metrics.CounterHandle) + + def test_gauge(self): + gauge = metrics.Gauge() + handle = gauge.get_handle(("test", "test1")) + self.assertIsInstance(handle, metrics.GaugeHandle) + + def test_measure(self): + measure = metrics.Measure() + handle = measure.get_handle(("test", "test1")) + self.assertIsInstance(handle, metrics.MeasureHandle) + + def test_default_handle(self): + metrics.DefaultMetricHandle() + + def test_counter_handle(self): + handle = metrics.CounterHandle() + handle.add(1) + + def test_gauge_handle(self): + handle = metrics.GaugeHandle() + handle.set(1) + + def test_measure_handle(self): + handle = metrics.MeasureHandle() + handle.record(1) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py index 81366d9d47..0f3bff4571 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from . import trace, util +from . import metrics, trace, util -__all__ = ["trace", "util"] +__all__ = ["metrics", "trace", "util"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py new file mode 100644 index 0000000000..f80a72c770 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -0,0 +1,246 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from typing import Sequence, Tuple, Type + +from opentelemetry import metrics as metrics_api + +logger = logging.getLogger(__name__) + + +class BaseHandle: + def __init__( + self, + value_type: Type[metrics_api.ValueT], + enabled: bool, + monotonic: bool, + ): + self.data = 0 + self.value_type = value_type + self.enabled = enabled + self.monotonic = monotonic + + def _validate_update(self, value: metrics_api.ValueT): + if not self.enabled: + return False + if not isinstance(value, self.value_type): + logger.warning( + "Invalid value passed for %s.", self.value_type.__name__ + ) + return False + return True + + +class CounterHandle(metrics_api.CounterHandle, BaseHandle): + def update(self, value: metrics_api.ValueT) -> None: + if self._validate_update(value): + if self.monotonic and value < 0: + logger.warning("Monotonic counter cannot descend.") + return + self.data += value + + def add(self, value: metrics_api.ValueT) -> None: + """See `opentelemetry.metrics.CounterHandle._add`.""" + self.update(value) + + +class GaugeHandle(metrics_api.GaugeHandle, BaseHandle): + def update(self, value: metrics_api.ValueT) -> None: + if self._validate_update(value): + if self.monotonic and value < self.data: + logger.warning("Monotonic gauge cannot descend.") + return + self.data = value + + def set(self, value: metrics_api.ValueT) -> None: + """See `opentelemetry.metrics.GaugeHandle._set`.""" + self.update(value) + + +class MeasureHandle(metrics_api.MeasureHandle, BaseHandle): + def update(self, value: metrics_api.ValueT) -> None: + if self._validate_update(value): + if self.monotonic and value < 0: + logger.warning("Monotonic measure cannot accept negatives.") + return + # TODO: record + + def record(self, value: metrics_api.ValueT) -> None: + """See `opentelemetry.metrics.MeasureHandle._record`.""" + self.update(value) + + +class Metric(metrics_api.Metric): + """See `opentelemetry.metrics.Metric`.""" + + HANDLE_TYPE = BaseHandle + + def __init__( + self, + name: str, + description: str, + unit: str, + value_type: Type[metrics_api.ValueT], + label_keys: Sequence[str] = None, + enabled: bool = True, + monotonic: bool = False, + ): + self.name = name + self.description = description + self.unit = unit + self.value_type = value_type + self.label_keys = label_keys + self.enabled = enabled + self.monotonic = monotonic + self.handles = {} + + def get_handle(self, label_values: Sequence[str]) -> BaseHandle: + """See `opentelemetry.metrics.Metric.get_handle`.""" + handle = self.handles.get(label_values) + if not handle: + handle = self.HANDLE_TYPE( + self.value_type, self.enabled, self.monotonic + ) + self.handles[label_values] = handle + return handle + + +class Counter(Metric): + """See `opentelemetry.metrics.Counter`. + + By default, counter values can only go up (monotonic). Negative inputs + will be discarded for monotonic counter metrics. Counter metrics that + have a monotonic option set to False allows negative inputs. + """ + + HANDLE_TYPE = CounterHandle + + def __init__( + self, + name: str, + description: str, + unit: str, + value_type: Type[metrics_api.ValueT], + label_keys: Sequence[str] = None, + enabled: bool = True, + monotonic: bool = True, + ): + super().__init__( + name, + description, + unit, + value_type, + label_keys=label_keys, + enabled=enabled, + monotonic=monotonic, + ) + + +class Gauge(Metric): + """See `opentelemetry.metrics.Gauge`. + + By default, gauge values can go both up and down (non-monotonic). + Negative inputs will be discarded for monotonic gauge metrics. + """ + + HANDLE_TYPE = GaugeHandle + + def __init__( + self, + name: str, + description: str, + unit: str, + value_type: Type[metrics_api.ValueT], + label_keys: Sequence[str] = None, + enabled: bool = True, + monotonic: bool = False, + ): + super().__init__( + name, + description, + unit, + value_type, + label_keys=label_keys, + enabled=enabled, + monotonic=monotonic, + ) + + +class Measure(Metric): + """See `opentelemetry.metrics.Measure`. + + By default, measure metrics can accept both positive and negatives. + Negative inputs will be discarded when monotonic is True. + """ + + HANDLE_TYPE = MeasureHandle + + def __init__( + self, + name: str, + description: str, + unit: str, + value_type: Type[metrics_api.ValueT], + label_keys: Sequence[str] = None, + enabled: bool = False, + monotonic: bool = False, + ): + super().__init__( + name, + description, + unit, + value_type, + label_keys=label_keys, + enabled=enabled, + monotonic=monotonic, + ) + + +class Meter(metrics_api.Meter): + """See `opentelemetry.metrics.Meter`.""" + + def record_batch( + self, + label_values: Sequence[str], + record_tuples: Sequence[Tuple[metrics_api.Metric, metrics_api.ValueT]], + ) -> None: + """See `opentelemetry.metrics.Meter.record_batch`.""" + for metric, value in record_tuples: + metric.get_handle(label_values).update(value) + + def create_metric( + self, + name: str, + description: str, + unit: str, + value_type: Type[metrics_api.ValueT], + metric_type: Type[metrics_api.MetricT], + label_keys: Sequence[str] = None, + enabled: bool = True, + monotonic: bool = False, + ) -> metrics_api.MetricT: + """See `opentelemetry.metrics.Meter.create_metric`.""" + return metric_type( + name, + description, + unit, + value_type, + label_keys=label_keys, + enabled=enabled, + monotonic=monotonic, + ) + + +meter = Meter() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 544906e6fb..dc3efb0368 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -17,132 +17,21 @@ import random import threading import typing -from collections import OrderedDict, deque from contextlib import contextmanager from opentelemetry import trace as trace_api from opentelemetry.context import Context from opentelemetry.sdk import util +from opentelemetry.sdk.util import BoundedDict, BoundedList from opentelemetry.util import types logger = logging.getLogger(__name__) -try: - # pylint: disable=ungrouped-imports - from collections.abc import MutableMapping - from collections.abc import Sequence -except ImportError: - # pylint: disable=no-name-in-module,ungrouped-imports - from collections import MutableMapping - from collections import Sequence - MAX_NUM_ATTRIBUTES = 32 MAX_NUM_EVENTS = 128 MAX_NUM_LINKS = 32 -class BoundedList(Sequence): - """An append only list with a fixed max size.""" - - def __init__(self, maxlen): - self.dropped = 0 - self._dq = deque(maxlen=maxlen) - self._lock = threading.Lock() - - def __repr__(self): - return "{}({}, maxlen={})".format( - type(self).__name__, list(self._dq), self._dq.maxlen - ) - - def __getitem__(self, index): - return self._dq[index] - - def __len__(self): - return len(self._dq) - - def __iter__(self): - with self._lock: - return iter(self._dq.copy()) - - def append(self, item): - with self._lock: - if len(self._dq) == self._dq.maxlen: - self.dropped += 1 - self._dq.append(item) - - def extend(self, seq): - with self._lock: - to_drop = len(seq) + len(self._dq) - self._dq.maxlen - if to_drop > 0: - self.dropped += to_drop - self._dq.extend(seq) - - @classmethod - def from_seq(cls, maxlen, seq): - seq = tuple(seq) - if len(seq) > maxlen: - raise ValueError - bounded_list = cls(maxlen) - # pylint: disable=protected-access - bounded_list._dq = deque(seq, maxlen=maxlen) - return bounded_list - - -class BoundedDict(MutableMapping): - """A dict with a fixed max capacity.""" - - def __init__(self, maxlen): - if not isinstance(maxlen, int): - raise ValueError - if maxlen < 0: - raise ValueError - self.maxlen = maxlen - self.dropped = 0 - self._dict = OrderedDict() - self._lock = threading.Lock() - - def __repr__(self): - return "{}({}, maxlen={})".format( - type(self).__name__, dict(self._dict), self.maxlen - ) - - def __getitem__(self, key): - return self._dict[key] - - def __setitem__(self, key, value): - with self._lock: - if self.maxlen == 0: - self.dropped += 1 - return - - if key in self._dict: - del self._dict[key] - elif len(self._dict) == self.maxlen: - del self._dict[next(iter(self._dict.keys()))] - self.dropped += 1 - self._dict[key] = value - - def __delitem__(self, key): - del self._dict[key] - - def __iter__(self): - with self._lock: - return iter(self._dict.copy()) - - def __len__(self): - return len(self._dict) - - @classmethod - def from_map(cls, maxlen, mapping): - mapping = OrderedDict(mapping) - if len(mapping) > maxlen: - raise ValueError - bounded_dict = cls(maxlen) - # pylint: disable=protected-access - bounded_dict._dict = mapping - return bounded_dict - - class SpanProcessor: """Interface which allows hooks for SDK's `Span`s start and end method invocations. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util.py b/opentelemetry-sdk/src/opentelemetry/sdk/util.py index 4f4ec82b9d..ede52e8307 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util.py @@ -13,7 +13,18 @@ # limitations under the License. import datetime +import threading import time +from collections import OrderedDict, deque + +try: + # pylint: disable=ungrouped-imports + from collections.abc import MutableMapping + from collections.abc import Sequence +except ImportError: + # pylint: disable=no-name-in-module,ungrouped-imports + from collections import MutableMapping + from collections import Sequence try: time_ns = time.time_ns @@ -28,3 +39,105 @@ def ns_to_iso_str(nanoseconds): """Get an ISO 8601 string from time_ns value.""" ts = datetime.datetime.utcfromtimestamp(nanoseconds / 1e9) return ts.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + + +class BoundedList(Sequence): + """An append only list with a fixed max size.""" + + def __init__(self, maxlen): + self.dropped = 0 + self._dq = deque(maxlen=maxlen) + self._lock = threading.Lock() + + def __repr__(self): + return "{}({}, maxlen={})".format( + type(self).__name__, list(self._dq), self._dq.maxlen + ) + + def __getitem__(self, index): + return self._dq[index] + + def __len__(self): + return len(self._dq) + + def __iter__(self): + with self._lock: + return iter(self._dq.copy()) + + def append(self, item): + with self._lock: + if len(self._dq) == self._dq.maxlen: + self.dropped += 1 + self._dq.append(item) + + def extend(self, seq): + with self._lock: + to_drop = len(seq) + len(self._dq) - self._dq.maxlen + if to_drop > 0: + self.dropped += to_drop + self._dq.extend(seq) + + @classmethod + def from_seq(cls, maxlen, seq): + seq = tuple(seq) + if len(seq) > maxlen: + raise ValueError + bounded_list = cls(maxlen) + # pylint: disable=protected-access + bounded_list._dq = deque(seq, maxlen=maxlen) + return bounded_list + + +class BoundedDict(MutableMapping): + """A dict with a fixed max capacity.""" + + def __init__(self, maxlen): + if not isinstance(maxlen, int): + raise ValueError + if maxlen < 0: + raise ValueError + self.maxlen = maxlen + self.dropped = 0 + self._dict = OrderedDict() + self._lock = threading.Lock() + + def __repr__(self): + return "{}({}, maxlen={})".format( + type(self).__name__, dict(self._dict), self.maxlen + ) + + def __getitem__(self, key): + return self._dict[key] + + def __setitem__(self, key, value): + with self._lock: + if self.maxlen == 0: + self.dropped += 1 + return + + if key in self._dict: + del self._dict[key] + elif len(self._dict) == self.maxlen: + del self._dict[next(iter(self._dict.keys()))] + self.dropped += 1 + self._dict[key] = value + + def __delitem__(self, key): + del self._dict[key] + + def __iter__(self): + with self._lock: + return iter(self._dict.copy()) + + def __len__(self): + return len(self._dict) + + @classmethod + def from_map(cls, maxlen, mapping): + mapping = OrderedDict(mapping) + if len(mapping) > maxlen: + raise ValueError + bounded_dict = cls(maxlen) + # pylint: disable=protected-access + bounded_dict._dict = mapping + return bounded_dict diff --git a/opentelemetry-api/src/opentelemetry/metrics/handle.py b/opentelemetry-sdk/tests/metrics/__init__.py similarity index 52% rename from opentelemetry-api/src/opentelemetry/metrics/handle.py rename to opentelemetry-sdk/tests/metrics/__init__.py index 5da3cf43e7..d853a7bcf6 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/handle.py +++ b/opentelemetry-sdk/tests/metrics/__init__.py @@ -11,23 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -import typing - - -class CounterHandle: - def add(self, value: typing.Union[float, int]) -> None: - """Adds the given value to the current value. - - The input value cannot be negative if not bidirectional. - """ - - -class GaugeHandle: - def set(self, value: typing.Union[float, int]) -> None: - """Sets the current value to the given value. Can be negative.""" - - -class MeasureHandle: - def record(self, value: typing.Union[float, int]) -> None: - """Records the given value to this measure.""" diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py new file mode 100644 index 0000000000..dc4151c4ee --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -0,0 +1,189 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from opentelemetry import metrics as metrics_api +from opentelemetry.sdk import metrics + + +class TestMeter(unittest.TestCase): + def test_extends_api(self): + meter = metrics.Meter() + self.assertIsInstance(meter, metrics_api.Meter) + + def test_record_batch(self): + meter = metrics.Meter() + label_keys = ("key1",) + label_values = ("value1",) + counter = metrics.Counter("name", "desc", "unit", float, label_keys) + record_tuples = [(counter, 1.0)] + meter.record_batch(label_values, record_tuples) + self.assertEqual(counter.get_handle(label_values).data, 1.0) + + def test_record_batch_multiple(self): + meter = metrics.Meter() + label_keys = ("key1", "key2", "key3") + label_values = ("value1", "value2", "value3") + counter = metrics.Counter("name", "desc", "unit", float, label_keys) + gauge = metrics.Gauge("name", "desc", "unit", int, label_keys) + measure = metrics.Measure("name", "desc", "unit", float, label_keys) + record_tuples = [(counter, 1.0), (gauge, 5), (measure, 3.0)] + meter.record_batch(label_values, record_tuples) + self.assertEqual(counter.get_handle(label_values).data, 1.0) + self.assertEqual(gauge.get_handle(label_values).data, 5) + self.assertEqual(measure.get_handle(label_values).data, 0) + + def test_record_batch_exists(self): + meter = metrics.Meter() + label_keys = ("key1",) + label_values = ("value1",) + counter = metrics.Counter("name", "desc", "unit", float, label_keys) + handle = counter.get_handle(label_values) + handle.update(1.0) + record_tuples = [(counter, 1.0)] + meter.record_batch(label_values, record_tuples) + self.assertEqual(counter.get_handle(label_values), handle) + self.assertEqual(handle.data, 2.0) + + def test_create_metric(self): + meter = metrics.Meter() + counter = meter.create_metric( + "name", "desc", "unit", int, metrics.Counter, () + ) + self.assertTrue(isinstance(counter, metrics.Counter)) + self.assertEqual(counter.value_type, int) + self.assertEqual(counter.name, "name") + + def test_create_gauge(self): + meter = metrics.Meter() + gauge = meter.create_metric( + "name", "desc", "unit", float, metrics.Gauge, () + ) + self.assertTrue(isinstance(gauge, metrics.Gauge)) + self.assertEqual(gauge.value_type, float) + self.assertEqual(gauge.name, "name") + + def test_create_measure(self): + meter = metrics.Meter() + measure = meter.create_metric( + "name", "desc", "unit", float, metrics.Measure, () + ) + self.assertTrue(isinstance(measure, metrics.Measure)) + self.assertEqual(measure.value_type, float) + self.assertEqual(measure.name, "name") + + +class TestMetric(unittest.TestCase): + def test_get_handle(self): + metric_types = [metrics.Counter, metrics.Gauge, metrics.Measure] + for _type in metric_types: + metric = _type("name", "desc", "unit", int, ("key",)) + label_values = ("value",) + handle = metric.get_handle(label_values) + self.assertEqual(metric.handles.get(label_values), handle) + + +class TestCounterHandle(unittest.TestCase): + def test_update(self): + handle = metrics.CounterHandle(float, True, False) + handle.update(2.0) + self.assertEqual(handle.data, 2.0) + + def test_add(self): + handle = metrics.CounterHandle(int, True, False) + handle.add(3) + self.assertEqual(handle.data, 3) + + def test_add_disabled(self): + handle = metrics.CounterHandle(int, False, False) + handle.add(3) + self.assertEqual(handle.data, 0) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_add_monotonic(self, logger_mock): + handle = metrics.CounterHandle(int, True, True) + handle.add(-3) + self.assertEqual(handle.data, 0) + self.assertTrue(logger_mock.warning.called) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_add_incorrect_type(self, logger_mock): + handle = metrics.CounterHandle(int, True, False) + handle.add(3.0) + self.assertEqual(handle.data, 0) + self.assertTrue(logger_mock.warning.called) + + +class TestGaugeHandle(unittest.TestCase): + def test_update(self): + handle = metrics.GaugeHandle(float, True, False) + handle.update(2.0) + self.assertEqual(handle.data, 2.0) + + def test_set(self): + handle = metrics.GaugeHandle(int, True, False) + handle.set(3) + self.assertEqual(handle.data, 3) + + def test_set_disabled(self): + handle = metrics.GaugeHandle(int, False, False) + handle.set(3) + self.assertEqual(handle.data, 0) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_set_monotonic(self, logger_mock): + handle = metrics.GaugeHandle(int, True, True) + handle.set(-3) + self.assertEqual(handle.data, 0) + self.assertTrue(logger_mock.warning.called) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_set_incorrect_type(self, logger_mock): + handle = metrics.GaugeHandle(int, True, False) + handle.set(3.0) + self.assertEqual(handle.data, 0) + self.assertTrue(logger_mock.warning.called) + + +class TestMeasureHandle(unittest.TestCase): + def test_update(self): + handle = metrics.MeasureHandle(float, False, False) + handle.update(2.0) + self.assertEqual(handle.data, 0) + + def test_record(self): + handle = metrics.MeasureHandle(int, False, False) + handle.record(3) + self.assertEqual(handle.data, 0) + + def test_record_disabled(self): + handle = metrics.MeasureHandle(int, False, False) + handle.record(3) + self.assertEqual(handle.data, 0) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_record_monotonic(self, logger_mock): + handle = metrics.MeasureHandle(int, True, True) + handle.record(-3) + self.assertEqual(handle.data, 0) + self.assertTrue(logger_mock.warning.called) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_record_incorrect_type(self, logger_mock): + handle = metrics.MeasureHandle(int, True, False) + handle.record(3.0) + self.assertEqual(handle.data, 0) + self.assertTrue(logger_mock.warning.called) From e52314037fecfd567a7ac1e26ce1ff90ccfef81f Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Fri, 27 Sep 2019 16:55:58 -0700 Subject: [PATCH 0091/1517] Implement Azure Monitor Exporter (#175) --- .../README.rst | 7 + .../examples/client.py | 30 +++ .../examples/server.py | 44 ++++ .../examples/trace.py | 14 ++ .../ext/azure_monitor/protocol.py | 201 ++++++++++++++++++ .../opentelemetry/ext/azure_monitor/trace.py | 158 +++++++++++++- .../opentelemetry/ext/azure_monitor/util.py | 42 ++++ 7 files changed, 491 insertions(+), 5 deletions(-) create mode 100644 ext/opentelemetry-ext-azure-monitor/examples/client.py create mode 100644 ext/opentelemetry-ext-azure-monitor/examples/server.py create mode 100644 ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/protocol.py create mode 100644 ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/util.py diff --git a/ext/opentelemetry-ext-azure-monitor/README.rst b/ext/opentelemetry-ext-azure-monitor/README.rst index d8c3e16074..976d9a531e 100644 --- a/ext/opentelemetry-ext-azure-monitor/README.rst +++ b/ext/opentelemetry-ext-azure-monitor/README.rst @@ -3,6 +3,13 @@ OpenTelemetry Azure Monitor Exporters This library provides integration with Microsoft Azure Monitor. +Installation +------------ + +:: + + pip install opentelemetry-ext-azure-monitor + References ---------- diff --git a/ext/opentelemetry-ext-azure-monitor/examples/client.py b/ext/opentelemetry-ext-azure-monitor/examples/client.py new file mode 100644 index 0000000000..ff954788e6 --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/examples/client.py @@ -0,0 +1,30 @@ +# Copyright 2019, OpenCensus 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 requests + +from opentelemetry import trace +from opentelemetry.ext import http_requests +from opentelemetry.ext.azure_monitor import AzureMonitorSpanExporter +from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + +trace.set_preferred_tracer_implementation(lambda T: Tracer()) +tracer = trace.tracer() +http_requests.enable(tracer) +span_processor = BatchExportSpanProcessor(AzureMonitorSpanExporter()) +tracer.add_span_processor(span_processor) + +response = requests.get(url="http://127.0.0.1:5000/") +span_processor.shutdown() diff --git a/ext/opentelemetry-ext-azure-monitor/examples/server.py b/ext/opentelemetry-ext-azure-monitor/examples/server.py new file mode 100644 index 0000000000..54727ef737 --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/examples/server.py @@ -0,0 +1,44 @@ +# Copyright 2019, OpenCensus 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 flask +import requests + +from opentelemetry import trace +from opentelemetry.ext import http_requests +from opentelemetry.ext.azure_monitor import AzureMonitorSpanExporter +from opentelemetry.ext.wsgi import OpenTelemetryMiddleware +from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + +trace.set_preferred_tracer_implementation(lambda T: Tracer()) + +http_requests.enable(trace.tracer()) +span_processor = BatchExportSpanProcessor(AzureMonitorSpanExporter()) +trace.tracer().add_span_processor(span_processor) + +app = flask.Flask(__name__) +app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) + + +@app.route("/") +def hello(): + with trace.tracer().start_span("parent"): + requests.get("https://www.wikipedia.org/wiki/Rabbit") + return "hello" + + +if __name__ == "__main__": + app.run(debug=True) + span_processor.shutdown() diff --git a/ext/opentelemetry-ext-azure-monitor/examples/trace.py b/ext/opentelemetry-ext-azure-monitor/examples/trace.py index 49b38c051d..8e8f887aa1 100644 --- a/ext/opentelemetry-ext-azure-monitor/examples/trace.py +++ b/ext/opentelemetry-ext-azure-monitor/examples/trace.py @@ -1,3 +1,17 @@ +# Copyright 2019, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from opentelemetry import trace from opentelemetry.ext.azure_monitor import AzureMonitorSpanExporter from opentelemetry.sdk.trace import Tracer diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/protocol.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/protocol.py new file mode 100644 index 0000000000..ccdf5eef8d --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/protocol.py @@ -0,0 +1,201 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class BaseObject(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for key in kwargs: + self[key] = kwargs[key] + + def __repr__(self): + tmp = {} + current = self + while True: + for item in current.items(): + if item[0] not in tmp: + tmp[item[0]] = item[1] + if ( + current._default # noqa pylint: disable=protected-access + == current + ): + break + current = current._default # noqa pylint: disable=protected-access + return repr(tmp) + + def __setattr__(self, name, value): + self[name] = value + + def __getattr__(self, name): + try: + return self[name] + except KeyError: + raise AttributeError( + "'{}' object has no attribute {}".format( + type(self).__name__, name + ) + ) + + def __getitem__(self, key): + if self._default is self: + return super().__getitem__(key) + if key in self: + return super().__getitem__(key) + return self._default[key] + + +BaseObject._default = BaseObject() # noqa pylint: disable=protected-access + + +class Data(BaseObject): + _default = BaseObject(baseData=None, baseType=None) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.baseData = self.baseData # noqa pylint: disable=invalid-name + self.baseType = self.baseType # noqa pylint: disable=invalid-name + + +class DataPoint(BaseObject): + _default = BaseObject( + ns="", + name="", + kind=None, + value=0.0, + count=None, + min=None, + max=None, + stdDev=None, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.name = self.name + self.value = self.value + + +class Envelope(BaseObject): + _default = BaseObject( + ver=1, + name="", + time="", + sampleRate=None, + seq=None, + iKey=None, + flags=None, + tags=None, + data=None, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.name = self.name + self.time = self.time + + +class Event(BaseObject): + _default = BaseObject(ver=2, name="", properties=None, measurements=None) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ver = self.ver + self.name = self.name + + +class ExceptionData(BaseObject): + _default = BaseObject( + ver=2, + exceptions=[], + severityLevel=None, + problemId=None, + properties=None, + measurements=None, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ver = self.ver + self.exceptions = self.exceptions + + +class Message(BaseObject): + _default = BaseObject( + ver=2, + message="", + severityLevel=None, + properties=None, + measurements=None, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ver = self.ver + self.message = self.message + + +class MetricData(BaseObject): + _default = BaseObject(ver=2, metrics=[], properties=None) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ver = self.ver + self.metrics = self.metrics + + +class RemoteDependency(BaseObject): + _default = BaseObject( + ver=2, + name="", + id="", + resultCode="", + duration="", + success=True, + data=None, + type=None, + target=None, + properties=None, + measurements=None, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ver = self.ver + self.name = self.name + self.resultCode = self.resultCode # noqa pylint: disable=invalid-name + self.duration = self.duration + + +class Request(BaseObject): + _default = BaseObject( + ver=2, + id="", + duration="", + responseCode="", + success=True, + source=None, + name=None, + url=None, + properties=None, + measurements=None, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ver = self.ver + self.id = self.id # noqa pylint: disable=invalid-name + self.duration = self.duration + self.responseCode = ( # noqa pylint: disable=invalid-name + self.responseCode + ) + self.success = self.success diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py index a65cdd92a1..16f9252fd0 100644 --- a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py +++ b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py @@ -12,14 +12,162 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json +import logging +from urllib.parse import urlparse + +import requests + +from opentelemetry.ext.azure_monitor import protocol, util from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.sdk.util import ns_to_iso_str +from opentelemetry.trace import Span, SpanKind + +logger = logging.getLogger(__name__) class AzureMonitorSpanExporter(SpanExporter): - def __init__(self): - pass + def __init__(self, **options): + self.options = util.Options(**options) + if not self.options.instrumentation_key: + raise ValueError("The instrumentation_key is not provided.") def export(self, spans): - for span in spans: - print(span) # TODO: add actual implementation here - return SpanExportResult.SUCCESS + envelopes = tuple(map(self.span_to_envelope, spans)) + + try: + response = requests.post( + url=self.options.endpoint, + data=json.dumps(envelopes), + headers={ + "Accept": "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + timeout=self.options.timeout, + ) + except requests.RequestException as ex: + logger.warning("Transient client side error %s.", ex) + return SpanExportResult.FAILED_RETRYABLE + + text = "N/A" + data = None # noqa pylint: disable=unused-variable + try: + text = response.text + except Exception as ex: # noqa pylint: disable=broad-except + logger.warning("Error while reading response body %s.", ex) + else: + try: + data = json.loads(text) # noqa pylint: disable=unused-variable + except Exception: # noqa pylint: disable=broad-except + pass + + if response.status_code == 200: + logger.info("Transmission succeeded: %s.", text) + return SpanExportResult.SUCCESS + + if response.status_code in ( + 206, # Partial Content + 429, # Too Many Requests + 500, # Internal Server Error + 503, # Service Unavailable + ): + return SpanExportResult.FAILED_RETRYABLE + + return SpanExportResult.FAILED_NOT_RETRYABLE + + @staticmethod + def ns_to_duration(nanoseconds): + value = (nanoseconds + 500000) // 1000000 # duration in milliseconds + value, microseconds = divmod(value, 1000) + value, seconds = divmod(value, 60) + value, minutes = divmod(value, 60) + days, hours = divmod(value, 24) + return "{:d}.{:02d}:{:02d}:{:02d}.{:03d}".format( + days, hours, minutes, seconds, microseconds + ) + + def span_to_envelope(self, span): # noqa pylint: disable=too-many-branches + envelope = protocol.Envelope( + iKey=self.options.instrumentation_key, + tags=dict(util.azure_monitor_context), + time=ns_to_iso_str(span.start_time), + ) + envelope.tags["ai.operation.id"] = "{:032x}".format( + span.context.trace_id + ) + parent = span.parent + if isinstance(parent, Span): + parent = parent.context + if parent: + envelope.tags[ + "ai.operation.parentId" + ] = "|{:032x}.{:016x}.".format(parent.trace_id, parent.span_id) + if span.kind in (SpanKind.CONSUMER, SpanKind.SERVER): + envelope.name = "Microsoft.ApplicationInsights.Request" + data = protocol.Request( + id="|{:032x}.{:016x}.".format( + span.context.trace_id, span.context.span_id + ), + duration=self.ns_to_duration(span.end_time - span.start_time), + responseCode="0", + success=False, + properties={}, + ) + envelope.data = protocol.Data( + baseData=data, baseType="RequestData" + ) + if "http.method" in span.attributes: + data.name = span.attributes["http.method"] + if "http.route" in span.attributes: + data.name = data.name + " " + span.attributes["http.route"] + envelope.tags["ai.operation.name"] = data.name + if "http.url" in span.attributes: + data.url = span.attributes["http.url"] + if "http.status_code" in span.attributes: + status_code = span.attributes["http.status_code"] + data.responseCode = str(status_code) + data.success = 200 <= status_code < 400 + else: + envelope.name = "Microsoft.ApplicationInsights.RemoteDependency" + data = protocol.RemoteDependency( + name=span.name, + id="|{:032x}.{:016x}.".format( + span.context.trace_id, span.context.span_id + ), + resultCode="0", # TODO + duration=self.ns_to_duration(span.end_time - span.start_time), + success=True, # TODO + properties={}, + ) + envelope.data = protocol.Data( + baseData=data, baseType="RemoteDependencyData" + ) + if span.kind in (SpanKind.CLIENT, SpanKind.PRODUCER): + data.type = "HTTP" # TODO + if "http.url" in span.attributes: + url = span.attributes["http.url"] + # TODO: error handling, probably put scheme as well + data.name = urlparse(url).netloc + if "http.status_code" in span.attributes: + data.resultCode = str(span.attributes["http.status_code"]) + else: # SpanKind.INTERNAL + data.type = "InProc" + for key in span.attributes: + data.properties[key] = span.attributes[key] + if span.links: + links = [] + for link in span.links: + links.append( + { + "operation_Id": "{:032x}".format( + link.context.trace_id + ), + "id": "|{:032x}.{:016x}.".format( + link.context.trace_id, link.context.span_id + ), + } + ) + data.properties["_MS.links"] = json.dumps(links) + print(data.properties["_MS.links"]) + # TODO: tracestate, tags + return envelope diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/util.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/util.py new file mode 100644 index 0000000000..f97dbd3e33 --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/util.py @@ -0,0 +1,42 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import locale +import os +import platform +import sys + +from opentelemetry.ext.azure_monitor.protocol import BaseObject +from opentelemetry.ext.azure_monitor.version import __version__ as ext_version +from opentelemetry.sdk.version import __version__ as opentelemetry_version + +azure_monitor_context = { + "ai.cloud.role": os.path.basename(sys.argv[0]) or "Python Application", + "ai.cloud.roleInstance": platform.node(), + "ai.device.id": platform.node(), + "ai.device.locale": locale.getdefaultlocale()[0], + "ai.device.osVersion": platform.version(), + "ai.device.type": "Other", + "ai.internal.sdkVersion": "py{}:ot{}:ext{}".format( + platform.python_version(), opentelemetry_version, ext_version + ), +} + + +class Options(BaseObject): + _default = BaseObject( + endpoint="https://dc.services.visualstudio.com/v2/track", + instrumentation_key=os.getenv("APPINSIGHTS_INSTRUMENTATIONKEY", None), + timeout=10.0, # networking timeout in seconds + ) From 250b9faed8d5ca4741dd9b8ede3aff7947c70167 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Fri, 27 Sep 2019 18:15:20 -0700 Subject: [PATCH 0092/1517] Span add override parameters for start_time and end_time (#179) --- .../src/opentelemetry/trace/__init__.py | 4 ++-- .../src/opentelemetry/sdk/trace/__init__.py | 12 ++++++++---- opentelemetry-sdk/tests/trace/test_trace.py | 10 ++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 33c62e9d3b..094255aa1e 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -142,7 +142,7 @@ class SpanKind(enum.Enum): class Span: """A span represents a single operation within a trace.""" - def start(self) -> None: + def start(self, start_time: int = None) -> None: """Sets the current time as the span's start time. Each span represents a single operation. The span's start time is the @@ -152,7 +152,7 @@ def start(self) -> None: implementations are free to ignore or raise on further calls. """ - def end(self) -> None: + def end(self, end_time: int = None) -> None: """Sets the current time as the span's end time. The span's end time is the wall time at which the operation finished. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index dc3efb0368..f4124e3694 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -242,19 +242,21 @@ def add_lazy_link(self, link: "trace_api.Link") -> None: return self.links.append(link) - def start(self): + def start(self, start_time: int = None): with self._lock: if not self.is_recording_events(): return has_started = self.start_time is not None if not has_started: - self.start_time = util.time_ns() + self.start_time = ( + start_time if start_time is not None else util.time_ns() + ) if has_started: logger.warning("Calling start() on a started span.") return self.span_processor.on_start(self) - def end(self): + def end(self, end_time: int = None): with self._lock: if not self.is_recording_events(): return @@ -262,7 +264,9 @@ def end(self): raise RuntimeError("Calling end() on a not started span.") has_ended = self.end_time is not None if not has_ended: - self.end_time = util.time_ns() + self.end_time = ( + end_time if end_time is not None else util.time_ns() + ) if has_ended: logger.warning("Calling end() on an ended span.") return diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 0570affc41..378534453c 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -234,6 +234,16 @@ def test_start_span(self): span.start() self.assertEqual(start_time, span.start_time) + def test_span_override_start_and_end_time(self): + """Span sending custom start_time and end_time values""" + span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + start_time = 123 + span.start(start_time) + self.assertEqual(start_time, span.start_time) + end_time = 456 + span.end(end_time) + self.assertEqual(end_time, span.end_time) + def test_ended_span(self): """"Events, attributes are not allowed after span is ended""" tracer = trace.Tracer("test_ended_span") From 9dda706624ec807ccddd0323cdb0b63385ed7904 Mon Sep 17 00:00:00 2001 From: Ram Thiru Date: Mon, 30 Sep 2019 07:06:43 -0700 Subject: [PATCH 0093/1517] CONTRIBUTING.md: Fix clone URL (#177) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b327ce96e1..1512dfe702 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ pull requests (PRs). To create a new PR, fork the project in GitHub and clone the upstream repo: ```sh -$ git clone https://https://github.com/open-telemetry/opentelemetry-python.git +$ git clone https://github.com/open-telemetry/opentelemetry-python.git ``` Add your fork as an origin: From 126d1e791c0dbcc07cf5e38fa8e9267ff58e3308 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 30 Sep 2019 12:07:05 -0700 Subject: [PATCH 0094/1517] Add B3 exporter to alpha release table (#164) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4b4e0f25ab..f8a6f125cc 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ release. | Tracing SDK | Alpha | September 30 2019 | | Metrics API | Alpha | September 30 2019 | | Metrics SDK | Alpha | September 30 2019 | +| Zipkin Trace Exporter | Alpha | September 30 2019 | | Jaeger Trace Exporter | Alpha | Unknown | | Prometheus Metrics Exporter | Alpha | Unknown | | Context Propagation | Alpha | September 30 2019 | From bad5938b36824531c58795168caea49ce46af29d Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 30 Sep 2019 17:46:45 -0700 Subject: [PATCH 0095/1517] Update README for alpha release (#189) --- README.md | 66 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f8a6f125cc..eac0a23727 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,16 @@ The `opentelemetry-sdk` package is the reference implementation of the API. Libraries that produce telemetry data should only depend on `opentelemetry-api`, and defer the choice of the SDK to the application developer. Applications may -depend on `opentelemetry-sdk` or another package that implements the API. +depend on `opentelemetry-sdk` or another package that implements the API. -To install the API and SDK packages, fork or clone this repo and do an -[editable -install](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs) -via `pip`: +**Please note** that this library is currently in _alpha_, and shouldn't be +used in production environments. + +The API and SDK packages are available on PyPI, and can installed via `pip`: ```sh -pip install -e ./opentelemetry-api -pip install -e ./opentelemetry-sdk +pip install opentelemetry-api +pip install opentelemetry-sdk ``` The @@ -34,6 +34,16 @@ directory includes OpenTelemetry integration packages, which can be installed separately as: ```sh +pip install opentelemetry-ext-{integration} +``` + +To install the development versions of these packages instead, clone or fork +this repo and do an [editable +install](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs): + +```sh +pip install -e ./opentelemetry-api +pip install -e ./opentelemetry-sdk pip install -e ./ext/opentelemetry-ext-{integration} ``` @@ -69,21 +79,27 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) ## Release Schedule -OpenTelemetry Python is under active development. Our goal is to release an -_alpha_ version of the library at the end of September 2019. This release isn't -guaranteed to conform to a specific version of the specification, and future -releases will not attempt to maintain backwards compatibility with the alpha -release. - -| Component | Version | Target Date | -| --------------------------- | ------- | ----------------- | -| Tracing API | Alpha | September 30 2019 | -| Tracing SDK | Alpha | September 30 2019 | -| Metrics API | Alpha | September 30 2019 | -| Metrics SDK | Alpha | September 30 2019 | -| Zipkin Trace Exporter | Alpha | September 30 2019 | -| Jaeger Trace Exporter | Alpha | Unknown | -| Prometheus Metrics Exporter | Alpha | Unknown | -| Context Propagation | Alpha | September 30 2019 | -| OpenTracing Bridge | Alpha | Unknown | -| OpenCensus Bridge | Alpha | Unknown | +OpenTelemetry Python is under active development. + +The library is not yet _generally available_, and releases aren't guaranteed to +conform to a specific version of the specification. Future releases will not +attempt to maintain backwards compatibility with current releases. + +The _alpha_ release includes: + +- Tracing API +- Tracing SDK +- Metrics API +- Metrics SDK +- W3C Context Propagation +- B3 Context Propagation + +Future release targets include: + +| Component | Version | Target Date | +| --------------------------- | ------- | --------------- | +| Zipkin Trace Exporter | Beta | October 14 2019 | +| Jaeger Trace Exporter | Beta | October 14 2019 | +| Prometheus Metrics Exporter | Beta | October 14 2019 | +| OpenTracing Bridge | Beta | October 14 2019 | +| OpenCensus Bridge | Beta | October 14 2019 | From 1968eedacdceec9856b75f60979391e38a8cb0e6 Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Wed, 2 Oct 2019 10:33:20 -0700 Subject: [PATCH 0096/1517] Update Contributing.md doc (#194) --- CONTRIBUTING.md | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1512dfe702..9bb81d7165 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,6 +8,11 @@ See the [public meeting notes](https://docs.google.com/document/d/1CIMGoIOZ-c3-i for a summary description of past meetings. To request edit access, join the meeting or get in touch on [Gitter](https://gitter.im/open-telemetry/opentelemetry-python). +See to the [community membership document](https://github.com/open-telemetry/community/blob/master/community-membership.md) +on how to become a [**Member**](https://github.com/open-telemetry/community/blob/master/community-membership.md#member), +[**Approver**](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver) +and [**Maintainer**](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer). + ## Development This project uses [`tox`](https://tox.readthedocs.io) to automate some aspects @@ -74,15 +79,16 @@ Open a pull request against the main `opentelemetry-python` repo. ### How to Get PRs Merged A PR is considered to be **ready to merge** when: -* It has received two approvals from Collaborators/Maintainers (at different - companies). +* It has received two approvals from [Approvers](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver) + / [Maintainers](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer) + (at different companies). * Major feedbacks are resolved. * It has been open for review for at least one working day. This gives people reasonable time to review. * Trivial change (typo, cosmetic, doc, etc.) doesn't have to wait for one day. * Urgent fix can take exception as long as it has been actively communicated. -Any Collaborator/Maintainer can merge the PR once it is **ready to merge**. +Any Approver / Maintainer can merge the PR once it is **ready to merge**. ## Design Choices @@ -111,25 +117,3 @@ For a deeper discussion, see: https://github.com/open-telemetry/opentelemetry-sp as specified with the [napolean extension](http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#google-vs-numpy) extension in [Sphinx](http://www.sphinx-doc.org/en/master/index.html). - -## Become a Collaborator - -Collaborators have write access to the repo. - -To become a Collaborator: -* Become an active Contributor by working on PRs. -* Actively participate in the community meeting, design discussion, PR review - and issue discussion. -* Contact the Maintainers, express the willingness and commitment. -* Acknowledged and approved by two Maintainers (at different companies). - -## Become a Maintainer - -Maintainers have admin access to the repo. - -To become a Maintainer: -* Become a [member of OpenTelemetry organization](https://github.com/orgs/open-telemetry/people). -* Become a Collaborator. -* Demonstrate the ability and commitment. -* Contact the Maintainers, express the willingness and commitment. -* Acknowledged and approved by all the current Maintainers. From 3b37675aa65b024eed15840f5b416d444213e5c2 Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Thu, 3 Oct 2019 20:28:39 -0700 Subject: [PATCH 0097/1517] Add **simple** client/server examples (#191) --- examples/trace/client.py | 42 ++++++++++++++++++++++++++++++ examples/trace/server.py | 55 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100755 examples/trace/client.py create mode 100755 examples/trace/server.py diff --git a/examples/trace/client.py b/examples/trace/client.py new file mode 100755 index 0000000000..662cea8d96 --- /dev/null +++ b/examples/trace/client.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests + +from opentelemetry import trace +from opentelemetry.ext import http_requests +from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +# The preferred tracer implementation must be set, as the opentelemetry-api +# defines the interface with a no-op implementation. +trace.set_preferred_tracer_implementation(lambda T: Tracer()) +tracer = trace.tracer() + +# Integrations are the glue that binds the OpenTelemetry API and the +# frameworks and libraries that are used together, automatically creating +# Spans and propagating context as appropriate. +http_requests.enable(tracer) + +# SpanExporter receives the spans and send them to the target location. +span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter()) +tracer.add_span_processor(span_processor) + +response = requests.get(url="http://127.0.0.1:5000/") +span_processor.shutdown() diff --git a/examples/trace/server.py b/examples/trace/server.py new file mode 100755 index 0000000000..878898593c --- /dev/null +++ b/examples/trace/server.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import flask +import requests + +from opentelemetry import trace +from opentelemetry.ext import http_requests +from opentelemetry.ext.wsgi import OpenTelemetryMiddleware +from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +# The preferred tracer implementation must be set, as the opentelemetry-api +# defines the interface with a no-op implementation. +trace.set_preferred_tracer_implementation(lambda T: Tracer()) + +# Integrations are the glue that binds the OpenTelemetry API and the +# frameworks and libraries that are used together, automatically creating +# Spans and propagating context as appropriate. +http_requests.enable(trace.tracer()) + +# SpanExporter receives the spans and send them to the target location. +span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter()) +trace.tracer().add_span_processor(span_processor) + +app = flask.Flask(__name__) +app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) + + +@app.route("/") +def hello(): + with trace.tracer().start_span("parent"): + requests.get("https://www.wikipedia.org/wiki/Rabbit") + return "hello" + + +if __name__ == "__main__": + app.run(debug=True) + span_processor.shutdown() From d069c940555ed64b43f1cf7c06e7f1c7b7352b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 7 Oct 2019 11:06:27 +0200 Subject: [PATCH 0098/1517] Remove unused dev-requirements.txt (#200) The requirements are contained in tox.ini now. --- dev-requirements.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 dev-requirements.txt diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index c326b529fd..0000000000 --- a/dev-requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pylint~=2.3 From 0d9b7bdc3ae003678a023eb0f649b503e47d6e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 7 Oct 2019 04:10:46 -0500 Subject: [PATCH 0099/1517] Fx bug in BoundedList for Python 3.4 and add tests (#199) * fix bug in BoundedList for python 3.4 and add tests collections.deque.copy() was introduced in python 3.5, this commit changes that by the deque constructor and adds some tests to BoundedList and BoundedDict to avoid similar problems in the future. Also, improve docstrings of BoundedList and BoundedDict classes --- .../src/opentelemetry/sdk/util.py | 14 +- opentelemetry-sdk/tests/test_util.py | 213 ++++++++++++++++++ 2 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 opentelemetry-sdk/tests/test_util.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util.py b/opentelemetry-sdk/src/opentelemetry/sdk/util.py index ede52e8307..c2df2b3f4e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util.py @@ -42,7 +42,11 @@ def ns_to_iso_str(nanoseconds): class BoundedList(Sequence): - """An append only list with a fixed max size.""" + """An append only list with a fixed max size. + + Calls to `append` and `extend` will drop the oldest elements if there is + not enough room. + """ def __init__(self, maxlen): self.dropped = 0 @@ -62,7 +66,7 @@ def __len__(self): def __iter__(self): with self._lock: - return iter(self._dq.copy()) + return iter(deque(self._dq)) def append(self, item): with self._lock: @@ -89,7 +93,11 @@ def from_seq(cls, maxlen, seq): class BoundedDict(MutableMapping): - """A dict with a fixed max capacity.""" + """An ordered dict with a fixed max capacity. + + Oldest elements are dropped when the dict is full and a new element is + added. + """ def __init__(self, maxlen): if not isinstance(maxlen, int): diff --git a/opentelemetry-sdk/tests/test_util.py b/opentelemetry-sdk/tests/test_util.py new file mode 100644 index 0000000000..ead310bd8d --- /dev/null +++ b/opentelemetry-sdk/tests/test_util.py @@ -0,0 +1,213 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import unittest + +from opentelemetry.sdk.util import BoundedDict, BoundedList + + +class TestBoundedList(unittest.TestCase): + base = [52, 36, 53, 29, 54, 99, 56, 48, 22, 35, 21, 65, 10, 95, 42, 60] + + def test_raises(self): + """Test corner cases + + - negative list size + - access out of range indexes + """ + with self.assertRaises(ValueError): + BoundedList(-1) + + blist = BoundedList(4) + blist.append(37) + blist.append(13) + + with self.assertRaises(IndexError): + _ = blist[2] + + with self.assertRaises(IndexError): + _ = blist[4] + + with self.assertRaises(IndexError): + _ = blist[-3] + + def test_from_seq(self): + list_len = len(self.base) + base_copy = list(self.base) + blist = BoundedList.from_seq(list_len, base_copy) + + self.assertEqual(len(blist), list_len) + + # modify base_copy and test that blist is not changed + for idx in range(list_len): + base_copy[idx] = idx * base_copy[idx] + + for idx in range(list_len): + self.assertEqual(blist[idx], self.base[idx]) + + # test that iter yields the correct number of elements + self.assertEqual(len(tuple(blist)), list_len) + + # sequence too big + with self.assertRaises(ValueError): + BoundedList.from_seq(list_len / 2, self.base) + + def test_append_no_drop(self): + """Append max capacity elements to the list without dropping elements.""" + # create empty list + list_len = len(self.base) + blist = BoundedList(list_len) + self.assertEqual(len(blist), 0) + + # fill list + for item in self.base: + blist.append(item) + + self.assertEqual(len(blist), list_len) + self.assertEqual(blist.dropped, 0) + + for idx in range(list_len): + self.assertEqual(blist[idx], self.base[idx]) + + # test __iter__ in BoundedList + for idx, val in enumerate(blist): + self.assertEqual(val, self.base[idx]) + + def test_append_drop(self): + """Append more than max capacity elements and test that oldest ones are dropped.""" + list_len = len(self.base) + # create full BoundedList + blist = BoundedList.from_seq(list_len, self.base) + + # try to append more items + for val in self.base: + # should drop the element without raising exceptions + blist.append(2 * val) + + self.assertEqual(len(blist), list_len) + self.assertEqual(blist.dropped, list_len) + + # test that new elements are in the list + for idx in range(list_len): + self.assertEqual(blist[idx], 2 * self.base[idx]) + + def test_extend_no_drop(self): + # create empty list + list_len = len(self.base) + blist = BoundedList(list_len) + self.assertEqual(len(blist), 0) + + # fill list + blist.extend(self.base) + + self.assertEqual(len(blist), list_len) + self.assertEqual(blist.dropped, 0) + + for idx in range(list_len): + self.assertEqual(blist[idx], self.base[idx]) + + # test __iter__ in BoundedList + for idx, val in enumerate(blist): + self.assertEqual(val, self.base[idx]) + + def test_extend_drop(self): + list_len = len(self.base) + # create full BoundedList + blist = BoundedList.from_seq(list_len, self.base) + other_list = [13, 37, 51, 91] + + # try to extend with more elements + blist.extend(other_list) + + self.assertEqual(len(blist), list_len) + self.assertEqual(blist.dropped, len(other_list)) + + +class TestBoundedDict(unittest.TestCase): + base = collections.OrderedDict( + [ + ("name", "Firulais"), + ("age", 7), + ("weight", 13), + ("vaccinated", True), + ] + ) + + def test_negative_maxlen(self): + with self.assertRaises(ValueError): + BoundedDict(-1) + + def test_from_map(self): + dic_len = len(self.base) + base_copy = collections.OrderedDict(self.base) + bdict = BoundedDict.from_map(dic_len, base_copy) + + self.assertEqual(len(bdict), dic_len) + + # modify base_copy and test that bdict is not changed + base_copy["name"] = "Bruno" + base_copy["age"] = 3 + + for key in self.base: + self.assertEqual(bdict[key], self.base[key]) + + # test that iter yields the correct number of elements + self.assertEqual(len(tuple(bdict)), dic_len) + + # map too big + with self.assertRaises(ValueError): + BoundedDict.from_map(dic_len / 2, self.base) + + def test_bounded_dict(self): + # create empty dict + dic_len = len(self.base) + bdict = BoundedDict(dic_len) + self.assertEqual(len(bdict), 0) + + # fill dict + for key in self.base: + bdict[key] = self.base[key] + + self.assertEqual(len(bdict), dic_len) + self.assertEqual(bdict.dropped, 0) + + for key in self.base: + self.assertEqual(bdict[key], self.base[key]) + + # test __iter__ in BoundedDict + for key in bdict: + self.assertEqual(bdict[key], self.base[key]) + + # updating an existing element should not drop + bdict["name"] = "Bruno" + self.assertEqual(bdict.dropped, 0) + + # try to append more elements + for key in self.base: + bdict["new-" + key] = self.base[key] + + self.assertEqual(len(bdict), dic_len) + self.assertEqual(bdict.dropped, dic_len) + + # test that elements in the dict are the new ones + for key in self.base: + self.assertEqual(bdict["new-" + key], self.base[key]) + + # delete an element + del bdict["new-name"] + self.assertEqual(len(bdict), dic_len - 1) + + with self.assertRaises(KeyError): + _ = bdict["new-name"] From c94a576c4bc8c1bd7b8959bf84b456ab95b8cbab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 7 Oct 2019 20:07:49 +0200 Subject: [PATCH 0100/1517] Move util.time_ns to API. (#205) --- opentelemetry-api/src/opentelemetry/util/__init__.py | 12 ++++++++++++ .../src/opentelemetry/sdk/trace/__init__.py | 10 ++++------ .../src/opentelemetry/sdk/trace/export/__init__.py | 6 +++--- opentelemetry-sdk/src/opentelemetry/sdk/util.py | 9 --------- opentelemetry-sdk/tests/trace/test_trace.py | 5 +++-- 5 files changed, 22 insertions(+), 20 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/util/__init__.py diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py new file mode 100644 index 0000000000..cbf36d4c05 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -0,0 +1,12 @@ +import time + +# Since we want API users to be able to provide timestamps, +# this needs to be in the API. + +try: + time_ns = time.time_ns +# Python versions < 3.7 +except AttributeError: + + def time_ns() -> int: + return int(time.time() * 1e9) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index f4124e3694..1cee1933e2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -23,7 +23,7 @@ from opentelemetry.context import Context from opentelemetry.sdk import util from opentelemetry.sdk.util import BoundedDict, BoundedList -from opentelemetry.util import types +from opentelemetry.util import time_ns, types logger = logging.getLogger(__name__) @@ -205,7 +205,7 @@ def add_event( ) -> None: if attributes is None: attributes = Span.empty_attributes - self.add_lazy_event(trace_api.Event(name, util.time_ns(), attributes)) + self.add_lazy_event(trace_api.Event(name, time_ns(), attributes)) def add_lazy_event(self, event: trace_api.Event) -> None: with self._lock: @@ -249,7 +249,7 @@ def start(self, start_time: int = None): has_started = self.start_time is not None if not has_started: self.start_time = ( - start_time if start_time is not None else util.time_ns() + start_time if start_time is not None else time_ns() ) if has_started: logger.warning("Calling start() on a started span.") @@ -264,9 +264,7 @@ def end(self, end_time: int = None): raise RuntimeError("Calling end() on a not started span.") has_ended = self.end_time is not None if not has_ended: - self.end_time = ( - end_time if end_time is not None else util.time_ns() - ) + self.end_time = end_time if end_time is not None else time_ns() if has_ended: logger.warning("Calling end() on an ended span.") return diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 2f42c19c39..ce362813ec 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -19,7 +19,7 @@ from enum import Enum from opentelemetry.context import Context -from opentelemetry.sdk import util +from opentelemetry.util import time_ns from .. import Span, SpanProcessor @@ -163,9 +163,9 @@ def worker(self): break # substract the duration of this export call to the next timeout - start = util.time_ns() + start = time_ns() self.export() - end = util.time_ns() + end = time_ns() duration = (end - start) / 1e9 timeout = self.schedule_delay_millis / 1e3 - duration diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util.py b/opentelemetry-sdk/src/opentelemetry/sdk/util.py index c2df2b3f4e..da6ada90c3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util.py @@ -14,7 +14,6 @@ import datetime import threading -import time from collections import OrderedDict, deque try: @@ -26,14 +25,6 @@ from collections import MutableMapping from collections import Sequence -try: - time_ns = time.time_ns -# Python versions < 3.7 -except AttributeError: - - def time_ns(): - return int(time.time() * 1e9) - def ns_to_iso_str(nanoseconds): """Get an ISO 8601 string from time_ns value.""" diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 378534453c..dc593a9d62 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -16,7 +16,8 @@ from unittest import mock from opentelemetry import trace as trace_api -from opentelemetry.sdk import trace, util +from opentelemetry.sdk import trace +from opentelemetry.util import time_ns class TestTracer(unittest.TestCase): @@ -174,7 +175,7 @@ def test_span_members(self): # events root.add_event("event0") root.add_event("event1", {"name": "birthday"}) - now = util.time_ns() + now = time_ns() root.add_lazy_event( trace_api.Event("event2", now, {"name": "hello"}) ) From 9fa92f3b30848f0c00df1cd7afbc714fb97acc78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 8 Oct 2019 05:24:23 -0500 Subject: [PATCH 0101/1517] Add Jaeger exporter (#174) This adds a Jeager exporter for OpenTelemetry. This exporter is based on https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-jaeger. The exporter uses thrift and can be configured to send data to the agent and also to a remote collector. There is a long discussion going on about how to include generated files in the repo, so for now just put them here. --- .flake8 | 1 + .isort.cfg | 1 + .pylintrc | 2 +- ext/opentelemetry-ext-jaeger/README.rst | 67 ++ .../examples/jaeger_exporter_example.py | 51 ++ ext/opentelemetry-ext-jaeger/setup.cfg | 47 + ext/opentelemetry-ext-jaeger/setup.py | 26 + .../src/opentelemetry/ext/jaeger/__init__.py | 363 ++++++++ .../opentelemetry/ext/jaeger/gen/__init__.py | 4 + .../ext/jaeger/gen/agent/Agent-remote | 124 +++ .../ext/jaeger/gen/agent/Agent.py | 246 ++++++ .../ext/jaeger/gen/agent/__init__.py | 1 + .../ext/jaeger/gen/agent/constants.py | 12 + .../ext/jaeger/gen/agent/ttypes.py | 15 + .../ext/jaeger/gen/jaeger/Collector-remote | 117 +++ .../ext/jaeger/gen/jaeger/Collector.py | 243 +++++ .../ext/jaeger/gen/jaeger/__init__.py | 1 + .../ext/jaeger/gen/jaeger/constants.py | 12 + .../ext/jaeger/gen/jaeger/ttypes.py | 831 ++++++++++++++++++ .../gen/zipkincore/ZipkinCollector-remote | 117 +++ .../jaeger/gen/zipkincore/ZipkinCollector.py | 243 +++++ .../ext/jaeger/gen/zipkincore/__init__.py | 1 + .../ext/jaeger/gen/zipkincore/constants.py | 28 + .../ext/jaeger/gen/zipkincore/ttypes.py | 647 ++++++++++++++ .../src/opentelemetry/ext/jaeger/version.py | 16 + .../tests/__init__.py | 0 .../tests/test_jaeger_exporter.py | 254 ++++++ .../thrift/agent.thrift | 27 + .../thrift/jaeger.thrift | 85 ++ .../thrift/zipkincore.thrift | 346 ++++++++ pyproject.toml | 19 + tox.ini | 10 +- 32 files changed, 3954 insertions(+), 3 deletions(-) create mode 100644 ext/opentelemetry-ext-jaeger/README.rst create mode 100644 ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py create mode 100644 ext/opentelemetry-ext-jaeger/setup.cfg create mode 100644 ext/opentelemetry-ext-jaeger/setup.py create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/__init__.py create mode 100755 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/Agent-remote create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/Agent.py create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/__init__.py create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/constants.py create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/ttypes.py create mode 100755 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/Collector-remote create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/Collector.py create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/__init__.py create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/constants.py create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/ttypes.py create mode 100755 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ZipkinCollector-remote create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ZipkinCollector.py create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/__init__.py create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/constants.py create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ttypes.py create mode 100644 ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py create mode 100644 ext/opentelemetry-ext-jaeger/tests/__init__.py create mode 100644 ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py create mode 100644 ext/opentelemetry-ext-jaeger/thrift/agent.thrift create mode 100644 ext/opentelemetry-ext-jaeger/thrift/jaeger.thrift create mode 100644 ext/opentelemetry-ext-jaeger/thrift/zipkincore.thrift diff --git a/.flake8 b/.flake8 index 5384053b3b..a3411a1614 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,3 @@ [flake8] ignore = E501,W503,E203 +exclude = .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/,ext/opentelemetry-ext-jaeger/build/* diff --git a/.isort.cfg b/.isort.cfg index 43cafae197..4bf64a34f1 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -12,3 +12,4 @@ line_length=79 ; ) ; docs: https://github.com/timothycrosley/isort#multi-line-output-modes multi_line_output=3 +skip_glob=ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/* diff --git a/.pylintrc b/.pylintrc index 8130305d70..782fc58700 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,7 +7,7 @@ extension-pkg-whitelist= # Add files or directories to the blacklist. They should be base names, not # paths. -ignore=CVS +ignore=CVS,gen # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. diff --git a/ext/opentelemetry-ext-jaeger/README.rst b/ext/opentelemetry-ext-jaeger/README.rst new file mode 100644 index 0000000000..2c2e94cd9f --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/README.rst @@ -0,0 +1,67 @@ +OpenTelemetry Jaeger Exporter +============================= + +Installation +------------ + +:: + + pip install opentelemetry-ext-jaeger + + +Usage +----- + +The **OpenTelemetry Jaeger Exporter** allows to export `OpenTelemetry`_ traces to `Jaeger`_. +This exporter always send traces to the configured agent using Thrift compact protocol over UDP. +An optional collector can be configured, in this case Thrift binary protocol over HTTP is used. +gRPC is still not supported by this implementation. + + +.. _Jaeger: https://www.jaegertracing.io/ +.. _OpenTelemetry: https://github.com/opentelemetry/opentelemetry-python/ + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext import jaeger + from opentelemetry.sdk.trace import Tracer + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + + trace.set_preferred_tracer_implementation(lambda T: Tracer()) + tracer = trace.tracer() + + # create a JaegerSpanExporter + jaeger_exporter = jaeger.JaegerSpanExporter( + service_name='my-helloworld-service', + # configure agent + agent_host_name='localhost', + agent_port=6831, + # optional: configure also collector + # collector_host_name='localhost', + # collector_port=14268, + # collector_endpoint='/api/traces?format=jaeger.thrift', + # username=xxxx, # optional + # password=xxxx, # optional + ) + + # Create a BatchExportSpanProcessor and add the exporter to it + span_processor = BatchExportSpanProcessor(jaeger_exporter) + + # add to the tracer + tracer.add_span_processor(span_processor) + + with tracer.start_span('foo'): + print('Hello world!') + + # shutdown the span processor + # TODO: this has to be improved so user doesn't need to call it manually + span_processor.shutdown() + +The `examples <./examples>`_ folder contains more elaborated examples. + +References +---------- + +* `Jaeger `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py new file mode 100644 index 0000000000..b43b158e85 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py @@ -0,0 +1,51 @@ +import time + +from opentelemetry import trace +from opentelemetry.ext import jaeger +from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + +trace.set_preferred_tracer_implementation(lambda T: Tracer()) +tracer = trace.tracer() + +# create a JaegerSpanExporter +jaeger_exporter = jaeger.JaegerSpanExporter( + service_name="my-helloworld-service", + # configure agent + agent_host_name="localhost", + agent_port=6831, + # optional: configure also collector + # collector_host_name="localhost", + # collector_port=14268, + # collector_endpoint="/api/traces?format=jaeger.thrift", + # username=xxxx, # optional + # password=xxxx, # optional +) + +# create a BatchExportSpanProcessor and add the exporter to it +span_processor = BatchExportSpanProcessor(jaeger_exporter) + +# add to the tracer +tracer.add_span_processor(span_processor) + +# create some spans for testing +with tracer.start_span("foo") as foo: + time.sleep(0.1) + foo.set_attribute("my_atribbute", True) + foo.add_event("event in foo", {"name": "foo1"}) + with tracer.start_span("bar") as bar: + time.sleep(0.2) + bar.set_attribute("speed", 100.0) + bar.add_link(foo.get_context()) + + with tracer.start_span("baz") as baz: + time.sleep(0.3) + baz.set_attribute("name", "mauricio") + + time.sleep(0.2) + + time.sleep(0.1) + +# shutdown the span processor +# TODO: this has to be improved so user doesn't need to call it manually +span_processor.shutdown() diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/ext/opentelemetry-ext-jaeger/setup.cfg new file mode 100644 index 0000000000..a5f04f1e9b --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/setup.cfg @@ -0,0 +1,47 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-jaeger +description = Jaeger Exporter for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-jaeger +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + thrift >= 0.10.0 + opentelemetry-api + opentelemetry-sdk + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-jaeger/setup.py b/ext/opentelemetry-ext-jaeger/setup.py new file mode 100644 index 0000000000..44f6eb32b1 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/setup.py @@ -0,0 +1,26 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "jaeger", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py new file mode 100644 index 0000000000..b824c1a51b --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -0,0 +1,363 @@ +# Copyright 2018, OpenCensus Authors +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Jaeger Span Exporter for OpenTelemetry.""" + +import base64 +import logging +import socket + +from thrift.protocol import TBinaryProtocol, TCompactProtocol +from thrift.transport import THttpClient, TTransport + +import opentelemetry.trace as trace_api +from opentelemetry.ext.jaeger.gen.agent import Agent as agent +from opentelemetry.ext.jaeger.gen.jaeger import Collector as jaeger +from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult + +DEFAULT_AGENT_HOST_NAME = "localhost" +DEFAULT_AGENT_PORT = 6831 +DEFAULT_COLLECTOR_ENDPOINT = "/api/traces?format=jaeger.thrift" + +UDP_PACKET_MAX_LENGTH = 65000 + +logger = logging.getLogger(__name__) + + +class JaegerSpanExporter(SpanExporter): + """Jaeger span exporter for OpenTelemetry. + + Args: + service_name: Service that logged an annotation in a trace.Classifier + when query for spans. + agent_host_name: The host name of the Jaeger-Agent. + agent_port: The port of the Jaeger-Agent. + collector_host_name: The host name of the Jaeger-Collector HTTP + Thrift. + collector_port: The port of the Jaeger-Collector HTTP Thrift. + collector_endpoint: The endpoint of the Jaeger-Collector HTTP Thrift. + username: The user name of the Basic Auth if authentication is + required. + password: The password of the Basic Auth if authentication is + required. + """ + + def __init__( + self, + service_name, + agent_host_name=DEFAULT_AGENT_HOST_NAME, + agent_port=DEFAULT_AGENT_PORT, + collector_host_name=None, + collector_port=None, + collector_endpoint=DEFAULT_COLLECTOR_ENDPOINT, + username=None, + password=None, + ): + self.service_name = service_name + self.agent_host_name = agent_host_name + self.agent_port = agent_port + self._agent_client = None + self.collector_host_name = collector_host_name + self.collector_port = collector_port + self.collector_endpoint = collector_endpoint + self.username = username + self.password = password + self._collector = None + + @property + def agent_client(self): + if self._agent_client is None: + self._agent_client = AgentClientUDP( + host_name=self.agent_host_name, port=self.agent_port + ) + return self._agent_client + + @property + def collector(self): + if self._collector is not None: + return self._collector + + if self.collector_host_name is None or self.collector_port is None: + return None + + thrift_url = "http://{}:{}{}".format( + self.collector_host_name, + self.collector_port, + self.collector_endpoint, + ) + + auth = None + if self.username is not None and self.password is not None: + auth = (self.username, self.password) + + self._collector = Collector(thrift_url=thrift_url, auth=auth) + return self._collector + + def export(self, spans): + jaeger_spans = _translate_to_jaeger(spans) + + batch = jaeger.Batch( + spans=jaeger_spans, + process=jaeger.Process(serviceName=self.service_name), + ) + + if self.collector is not None: + self.collector.submit(batch) + self.agent_client.emit(batch) + + return SpanExportResult.SUCCESS + + def shutdown(self): + pass + + +def _translate_to_jaeger(spans: Span): + """Translate the spans to Jaeger format. + + Args: + spans: Tuple of spans to convert + """ + + jaeger_spans = [] + + for span in spans: + ctx = span.get_context() + trace_id = ctx.trace_id + span_id = ctx.span_id + + start_time_us = span.start_time // 1e3 + duration_us = (span.end_time - span.start_time) // 1e3 + + parent_id = 0 + if isinstance(span.parent, trace_api.Span): + parent_id = span.parent.get_context().span_id + elif isinstance(span.parent, trace_api.SpanContext): + parent_id = span.parent.span_id + + tags = _extract_tags(span.attributes) + + # TODO: status is missing: + # https://github.com/open-telemetry/opentelemetry-python/issues/98 + + refs = _extract_refs_from_span(span) + logs = _extract_logs_from_span(span) + + flags = int(ctx.trace_options) + + jaeger_span = jaeger.Span( + traceIdHigh=_get_trace_id_high(trace_id), + traceIdLow=_get_trace_id_low(trace_id), + # generated code expects i64 + spanId=_convert_int_to_i64(span_id), + operationName=span.name, + startTime=start_time_us, + duration=duration_us, + tags=tags, + logs=logs, + references=refs, + flags=flags, + parentSpanId=_convert_int_to_i64(parent_id), + ) + + jaeger_spans.append(jaeger_span) + + return jaeger_spans + + +def _extract_refs_from_span(span): + if not span.links: + return None + + refs = [] + for link in span.links: + trace_id = link.context.trace_id + span_id = link.context.span_id + refs.append( + jaeger.SpanRef( + refType=jaeger.SpanRefType.FOLLOWS_FROM, + traceIdHigh=_get_trace_id_high(trace_id), + traceIdLow=_get_trace_id_low(trace_id), + spanId=_convert_int_to_i64(span_id), + ) + ) + return refs + + +def _convert_int_to_i64(val): + """Convert integer to signed int64 (i64)""" + if val > 0x7FFFFFFFFFFFFFFF: + val -= 0x10000000000000000 + return val + + +def _get_trace_id_low(trace_id): + return _convert_int_to_i64(trace_id & 0xFFFFFFFFFFFFFFFF) + + +def _get_trace_id_high(trace_id): + return _convert_int_to_i64((trace_id >> 64) & 0xFFFFFFFFFFFFFFFF) + + +def _extract_logs_from_span(span): + if not span.events: + return None + + logs = [] + + for event in span.events: + fields = [] + if event.attributes is not None: + fields = _extract_tags(event.attributes) + + fields.append( + jaeger.Tag( + key="message", vType=jaeger.TagType.STRING, vStr=event.name + ) + ) + + event_timestamp_us = event.timestamp // 1e3 + logs.append( + jaeger.Log(timestamp=int(event_timestamp_us), fields=fields) + ) + return logs + + +def _extract_tags(attr): + if not attr: + return None + tags = [] + for attribute_key, attribute_value in attr.items(): + tag = _convert_attribute_to_tag(attribute_key, attribute_value) + if tag is None: + continue + tags.append(tag) + return tags + + +def _convert_attribute_to_tag(key, attr): + """Convert the attributes to jaeger tags.""" + if isinstance(attr, bool): + return jaeger.Tag(key=key, vBool=attr, vType=jaeger.TagType.BOOL) + if isinstance(attr, str): + return jaeger.Tag(key=key, vStr=attr, vType=jaeger.TagType.STRING) + if isinstance(attr, int): + return jaeger.Tag(key=key, vLong=attr, vType=jaeger.TagType.LONG) + if isinstance(attr, float): + return jaeger.Tag(key=key, vDouble=attr, vType=jaeger.TagType.DOUBLE) + logger.warning("Could not serialize attribute %s:%r to tag", key, attr) + return None + + +class AgentClientUDP: + """Implement a UDP client to agent. + + Args: + host_name: The host name of the Jaeger server. + port: The port of the Jaeger server. + max_packet_size: Maximum size of UDP packet. + client: Class for creating new client objects for agencies. + """ + + def __init__( + self, + host_name, + port, + max_packet_size=UDP_PACKET_MAX_LENGTH, + client=agent.Client, + ): + self.address = (host_name, port) + self.max_packet_size = max_packet_size + self.buffer = TTransport.TMemoryBuffer() + self.client = client( + iprot=TCompactProtocol.TCompactProtocol(trans=self.buffer) + ) + + def emit(self, batch: jaeger.Batch): + """ + Args: + batch: Object to emit Jaeger spans. + """ + + # pylint: disable=protected-access + self.client._seqid = 0 + # truncate and reset the position of BytesIO object + self.buffer._buffer.truncate(0) + self.buffer._buffer.seek(0) + self.client.emitBatch(batch) + buff = self.buffer.getvalue() + if len(buff) > self.max_packet_size: + logger.warning( + "Data exceeds the max UDP packet size; size %r, max %r", + len(buff), + self.max_packet_size, + ) + return + + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_socket: + udp_socket.sendto(buff, self.address) + + +class Collector: + """Submits collected spans to Thrift HTTP server. + + Args: + thrift_url: URL of the Jaeger HTTP Thrift. + auth: Auth tuple that contains username and password for Basic Auth. + client: Class for creating a Jaeger collector client. + http_transport: Class for creating new client for Thrift HTTP server. + """ + + def __init__( + self, + thrift_url="", + auth=None, + client=jaeger.Client, + http_transport=THttpClient.THttpClient, + ): + self.thrift_url = thrift_url + self.auth = auth + self.http_transport = http_transport(uri_or_host=thrift_url) + self.client = client( + iprot=TBinaryProtocol.TBinaryProtocol(trans=self.http_transport) + ) + + # set basic auth header + if auth is not None: + auth_header = "{}:{}".format(*auth) + decoded = base64.b64encode(auth_header.encode()).decode("ascii") + basic_auth = dict(Authorization="Basic {}".format(decoded)) + self.http_transport.setCustomHeaders(basic_auth) + + def submit(self, batch: jaeger.Batch): + """Submits batches to Thrift HTTP Server through Binary Protocol. + + Args: + batch: Object to emit Jaeger spans. + """ + try: + self.client.submitBatches([batch]) + # it will call http_transport.flush() and + # status code and message will be updated + code = self.http_transport.code + msg = self.http_transport.message + if code >= 300 or code < 200: + logger.error( + "Traces cannot be uploaded; HTTP status code: %s, message %s", + code, + msg, + ) + finally: + if self.http_transport.isOpen(): + self.http_transport.close() diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/__init__.py new file mode 100644 index 0000000000..52b3cfb3e9 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/__init__.py @@ -0,0 +1,4 @@ + +import sys +from os.path import dirname +sys.path.append(dirname(__file__)) diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/Agent-remote b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/Agent-remote new file mode 100755 index 0000000000..5db3d20804 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/Agent-remote @@ -0,0 +1,124 @@ +#!/usr/bin/env python +# +# Autogenerated by Thrift Compiler (0.10.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +import sys +import pprint +if sys.version_info[0] > 2: + from urllib.parse import urlparse +else: + from urlparse import urlparse +from thrift.transport import TTransport, TSocket, TSSLSocket, THttpClient +from thrift.protocol.TBinaryProtocol import TBinaryProtocol + +from agent import Agent +from agent.ttypes import * + +if len(sys.argv) <= 1 or sys.argv[1] == '--help': + print('') + print('Usage: ' + sys.argv[0] + ' [-h host[:port]] [-u url] [-f[ramed]] [-s[sl]] [-novalidate] [-ca_certs certs] [-keyfile keyfile] [-certfile certfile] function [arg1 [arg2...]]') + print('') + print('Functions:') + print(' void emitZipkinBatch( spans)') + print(' void emitBatch(Batch batch)') + print('') + sys.exit(0) + +pp = pprint.PrettyPrinter(indent=2) +host = 'localhost' +port = 9090 +uri = '' +framed = False +ssl = False +validate = True +ca_certs = None +keyfile = None +certfile = None +http = False +argi = 1 + +if sys.argv[argi] == '-h': + parts = sys.argv[argi + 1].split(':') + host = parts[0] + if len(parts) > 1: + port = int(parts[1]) + argi += 2 + +if sys.argv[argi] == '-u': + url = urlparse(sys.argv[argi + 1]) + parts = url[1].split(':') + host = parts[0] + if len(parts) > 1: + port = int(parts[1]) + else: + port = 80 + uri = url[2] + if url[4]: + uri += '?%s' % url[4] + http = True + argi += 2 + +if sys.argv[argi] == '-f' or sys.argv[argi] == '-framed': + framed = True + argi += 1 + +if sys.argv[argi] == '-s' or sys.argv[argi] == '-ssl': + ssl = True + argi += 1 + +if sys.argv[argi] == '-novalidate': + validate = False + argi += 1 + +if sys.argv[argi] == '-ca_certs': + ca_certs = sys.argv[argi+1] + argi += 2 + +if sys.argv[argi] == '-keyfile': + keyfile = sys.argv[argi+1] + argi += 2 + +if sys.argv[argi] == '-certfile': + certfile = sys.argv[argi+1] + argi += 2 + +cmd = sys.argv[argi] +args = sys.argv[argi + 1:] + +if http: + transport = THttpClient.THttpClient(host, port, uri) +else: + if ssl: + socket = TSSLSocket.TSSLSocket(host, port, validate=validate, ca_certs=ca_certs, keyfile=keyfile, certfile=certfile) + else: + socket = TSocket.TSocket(host, port) + if framed: + transport = TTransport.TFramedTransport(socket) + else: + transport = TTransport.TBufferedTransport(socket) +protocol = TBinaryProtocol(transport) +client = Agent.Client(protocol) +transport.open() + +if cmd == 'emitZipkinBatch': + if len(args) != 1: + print('emitZipkinBatch requires 1 args') + sys.exit(1) + pp.pprint(client.emitZipkinBatch(eval(args[0]),)) + +elif cmd == 'emitBatch': + if len(args) != 1: + print('emitBatch requires 1 args') + sys.exit(1) + pp.pprint(client.emitBatch(eval(args[0]),)) + +else: + print('Unrecognized method %s' % cmd) + sys.exit(1) + +transport.close() diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/Agent.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/Agent.py new file mode 100644 index 0000000000..e8e0fe662e --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/Agent.py @@ -0,0 +1,246 @@ +# +# Autogenerated by Thrift Compiler (0.10.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException +from thrift.protocol.TProtocol import TProtocolException +import sys +import logging +from .ttypes import * +from thrift.Thrift import TProcessor +from thrift.transport import TTransport + + +class Iface(object): + def emitZipkinBatch(self, spans): + """ + Parameters: + - spans + """ + pass + + def emitBatch(self, batch): + """ + Parameters: + - batch + """ + pass + + +class Client(Iface): + def __init__(self, iprot, oprot=None): + self._iprot = self._oprot = iprot + if oprot is not None: + self._oprot = oprot + self._seqid = 0 + + def emitZipkinBatch(self, spans): + """ + Parameters: + - spans + """ + self.send_emitZipkinBatch(spans) + + def send_emitZipkinBatch(self, spans): + self._oprot.writeMessageBegin('emitZipkinBatch', TMessageType.ONEWAY, self._seqid) + args = emitZipkinBatch_args() + args.spans = spans + args.write(self._oprot) + self._oprot.writeMessageEnd() + self._oprot.trans.flush() + + def emitBatch(self, batch): + """ + Parameters: + - batch + """ + self.send_emitBatch(batch) + + def send_emitBatch(self, batch): + self._oprot.writeMessageBegin('emitBatch', TMessageType.ONEWAY, self._seqid) + args = emitBatch_args() + args.batch = batch + args.write(self._oprot) + self._oprot.writeMessageEnd() + self._oprot.trans.flush() + + +class Processor(Iface, TProcessor): + def __init__(self, handler): + self._handler = handler + self._processMap = {} + self._processMap["emitZipkinBatch"] = Processor.process_emitZipkinBatch + self._processMap["emitBatch"] = Processor.process_emitBatch + + def process(self, iprot, oprot): + (name, type, seqid) = iprot.readMessageBegin() + if name not in self._processMap: + iprot.skip(TType.STRUCT) + iprot.readMessageEnd() + x = TApplicationException(TApplicationException.UNKNOWN_METHOD, 'Unknown function %s' % (name)) + oprot.writeMessageBegin(name, TMessageType.EXCEPTION, seqid) + x.write(oprot) + oprot.writeMessageEnd() + oprot.trans.flush() + return + else: + self._processMap[name](self, seqid, iprot, oprot) + return True + + def process_emitZipkinBatch(self, seqid, iprot, oprot): + args = emitZipkinBatch_args() + args.read(iprot) + iprot.readMessageEnd() + try: + self._handler.emitZipkinBatch(args.spans) + except (TTransport.TTransportException, KeyboardInterrupt, SystemExit): + raise + except: + pass + + def process_emitBatch(self, seqid, iprot, oprot): + args = emitBatch_args() + args.read(iprot) + iprot.readMessageEnd() + try: + self._handler.emitBatch(args.batch) + except (TTransport.TTransportException, KeyboardInterrupt, SystemExit): + raise + except: + pass + +# HELPER FUNCTIONS AND STRUCTURES + + +class emitZipkinBatch_args(object): + """ + Attributes: + - spans + """ + + thrift_spec = ( + None, # 0 + (1, TType.LIST, 'spans', (TType.STRUCT, (zipkincore.ttypes.Span, zipkincore.ttypes.Span.thrift_spec), False), None, ), # 1 + ) + + def __init__(self, spans=None,): + self.spans = spans + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.LIST: + self.spans = [] + (_etype3, _size0) = iprot.readListBegin() + for _i4 in range(_size0): + _elem5 = zipkincore.ttypes.Span() + _elem5.read(iprot) + self.spans.append(_elem5) + iprot.readListEnd() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('emitZipkinBatch_args') + if self.spans is not None: + oprot.writeFieldBegin('spans', TType.LIST, 1) + oprot.writeListBegin(TType.STRUCT, len(self.spans)) + for iter6 in self.spans: + iter6.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class emitBatch_args(object): + """ + Attributes: + - batch + """ + + thrift_spec = ( + None, # 0 + (1, TType.STRUCT, 'batch', (jaeger.ttypes.Batch, jaeger.ttypes.Batch.thrift_spec), None, ), # 1 + ) + + def __init__(self, batch=None,): + self.batch = batch + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.STRUCT: + self.batch = jaeger.ttypes.Batch() + self.batch.read(iprot) + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('emitBatch_args') + if self.batch is not None: + oprot.writeFieldBegin('batch', TType.STRUCT, 1) + self.batch.write(oprot) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/__init__.py new file mode 100644 index 0000000000..1059cfbc01 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/__init__.py @@ -0,0 +1 @@ +__all__ = ['ttypes', 'constants', 'Agent'] diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/constants.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/constants.py new file mode 100644 index 0000000000..eb0d35aa12 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/constants.py @@ -0,0 +1,12 @@ +# +# Autogenerated by Thrift Compiler (0.10.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException +from thrift.protocol.TProtocol import TProtocolException +import sys +from .ttypes import * diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/ttypes.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/ttypes.py new file mode 100644 index 0000000000..fc8743cba9 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/ttypes.py @@ -0,0 +1,15 @@ +# +# Autogenerated by Thrift Compiler (0.10.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException +from thrift.protocol.TProtocol import TProtocolException +import sys +import jaeger.ttypes +import zipkincore.ttypes + +from thrift.transport import TTransport diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/Collector-remote b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/Collector-remote new file mode 100755 index 0000000000..5903f02360 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/Collector-remote @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# +# Autogenerated by Thrift Compiler (0.10.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +import sys +import pprint +if sys.version_info[0] > 2: + from urllib.parse import urlparse +else: + from urlparse import urlparse +from thrift.transport import TTransport, TSocket, TSSLSocket, THttpClient +from thrift.protocol.TBinaryProtocol import TBinaryProtocol + +from jaeger import Collector +from jaeger.ttypes import * + +if len(sys.argv) <= 1 or sys.argv[1] == '--help': + print('') + print('Usage: ' + sys.argv[0] + ' [-h host[:port]] [-u url] [-f[ramed]] [-s[sl]] [-novalidate] [-ca_certs certs] [-keyfile keyfile] [-certfile certfile] function [arg1 [arg2...]]') + print('') + print('Functions:') + print(' submitBatches( batches)') + print('') + sys.exit(0) + +pp = pprint.PrettyPrinter(indent=2) +host = 'localhost' +port = 9090 +uri = '' +framed = False +ssl = False +validate = True +ca_certs = None +keyfile = None +certfile = None +http = False +argi = 1 + +if sys.argv[argi] == '-h': + parts = sys.argv[argi + 1].split(':') + host = parts[0] + if len(parts) > 1: + port = int(parts[1]) + argi += 2 + +if sys.argv[argi] == '-u': + url = urlparse(sys.argv[argi + 1]) + parts = url[1].split(':') + host = parts[0] + if len(parts) > 1: + port = int(parts[1]) + else: + port = 80 + uri = url[2] + if url[4]: + uri += '?%s' % url[4] + http = True + argi += 2 + +if sys.argv[argi] == '-f' or sys.argv[argi] == '-framed': + framed = True + argi += 1 + +if sys.argv[argi] == '-s' or sys.argv[argi] == '-ssl': + ssl = True + argi += 1 + +if sys.argv[argi] == '-novalidate': + validate = False + argi += 1 + +if sys.argv[argi] == '-ca_certs': + ca_certs = sys.argv[argi+1] + argi += 2 + +if sys.argv[argi] == '-keyfile': + keyfile = sys.argv[argi+1] + argi += 2 + +if sys.argv[argi] == '-certfile': + certfile = sys.argv[argi+1] + argi += 2 + +cmd = sys.argv[argi] +args = sys.argv[argi + 1:] + +if http: + transport = THttpClient.THttpClient(host, port, uri) +else: + if ssl: + socket = TSSLSocket.TSSLSocket(host, port, validate=validate, ca_certs=ca_certs, keyfile=keyfile, certfile=certfile) + else: + socket = TSocket.TSocket(host, port) + if framed: + transport = TTransport.TFramedTransport(socket) + else: + transport = TTransport.TBufferedTransport(socket) +protocol = TBinaryProtocol(transport) +client = Collector.Client(protocol) +transport.open() + +if cmd == 'submitBatches': + if len(args) != 1: + print('submitBatches requires 1 args') + sys.exit(1) + pp.pprint(client.submitBatches(eval(args[0]),)) + +else: + print('Unrecognized method %s' % cmd) + sys.exit(1) + +transport.close() diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/Collector.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/Collector.py new file mode 100644 index 0000000000..f6f809b089 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/Collector.py @@ -0,0 +1,243 @@ +# +# Autogenerated by Thrift Compiler (0.10.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException +from thrift.protocol.TProtocol import TProtocolException +import sys +import logging +from .ttypes import * +from thrift.Thrift import TProcessor +from thrift.transport import TTransport + + +class Iface(object): + def submitBatches(self, batches): + """ + Parameters: + - batches + """ + pass + + +class Client(Iface): + def __init__(self, iprot, oprot=None): + self._iprot = self._oprot = iprot + if oprot is not None: + self._oprot = oprot + self._seqid = 0 + + def submitBatches(self, batches): + """ + Parameters: + - batches + """ + self.send_submitBatches(batches) + return self.recv_submitBatches() + + def send_submitBatches(self, batches): + self._oprot.writeMessageBegin('submitBatches', TMessageType.CALL, self._seqid) + args = submitBatches_args() + args.batches = batches + args.write(self._oprot) + self._oprot.writeMessageEnd() + self._oprot.trans.flush() + + def recv_submitBatches(self): + iprot = self._iprot + (fname, mtype, rseqid) = iprot.readMessageBegin() + if mtype == TMessageType.EXCEPTION: + x = TApplicationException() + x.read(iprot) + iprot.readMessageEnd() + raise x + result = submitBatches_result() + result.read(iprot) + iprot.readMessageEnd() + if result.success is not None: + return result.success + raise TApplicationException(TApplicationException.MISSING_RESULT, "submitBatches failed: unknown result") + + +class Processor(Iface, TProcessor): + def __init__(self, handler): + self._handler = handler + self._processMap = {} + self._processMap["submitBatches"] = Processor.process_submitBatches + + def process(self, iprot, oprot): + (name, type, seqid) = iprot.readMessageBegin() + if name not in self._processMap: + iprot.skip(TType.STRUCT) + iprot.readMessageEnd() + x = TApplicationException(TApplicationException.UNKNOWN_METHOD, 'Unknown function %s' % (name)) + oprot.writeMessageBegin(name, TMessageType.EXCEPTION, seqid) + x.write(oprot) + oprot.writeMessageEnd() + oprot.trans.flush() + return + else: + self._processMap[name](self, seqid, iprot, oprot) + return True + + def process_submitBatches(self, seqid, iprot, oprot): + args = submitBatches_args() + args.read(iprot) + iprot.readMessageEnd() + result = submitBatches_result() + try: + result.success = self._handler.submitBatches(args.batches) + msg_type = TMessageType.REPLY + except (TTransport.TTransportException, KeyboardInterrupt, SystemExit): + raise + except Exception as ex: + msg_type = TMessageType.EXCEPTION + logging.exception(ex) + result = TApplicationException(TApplicationException.INTERNAL_ERROR, 'Internal error') + oprot.writeMessageBegin("submitBatches", msg_type, seqid) + result.write(oprot) + oprot.writeMessageEnd() + oprot.trans.flush() + +# HELPER FUNCTIONS AND STRUCTURES + + +class submitBatches_args(object): + """ + Attributes: + - batches + """ + + thrift_spec = ( + None, # 0 + (1, TType.LIST, 'batches', (TType.STRUCT, (Batch, Batch.thrift_spec), False), None, ), # 1 + ) + + def __init__(self, batches=None,): + self.batches = batches + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.LIST: + self.batches = [] + (_etype45, _size42) = iprot.readListBegin() + for _i46 in range(_size42): + _elem47 = Batch() + _elem47.read(iprot) + self.batches.append(_elem47) + iprot.readListEnd() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('submitBatches_args') + if self.batches is not None: + oprot.writeFieldBegin('batches', TType.LIST, 1) + oprot.writeListBegin(TType.STRUCT, len(self.batches)) + for iter48 in self.batches: + iter48.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class submitBatches_result(object): + """ + Attributes: + - success + """ + + thrift_spec = ( + (0, TType.LIST, 'success', (TType.STRUCT, (BatchSubmitResponse, BatchSubmitResponse.thrift_spec), False), None, ), # 0 + ) + + def __init__(self, success=None,): + self.success = success + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 0: + if ftype == TType.LIST: + self.success = [] + (_etype52, _size49) = iprot.readListBegin() + for _i53 in range(_size49): + _elem54 = BatchSubmitResponse() + _elem54.read(iprot) + self.success.append(_elem54) + iprot.readListEnd() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('submitBatches_result') + if self.success is not None: + oprot.writeFieldBegin('success', TType.LIST, 0) + oprot.writeListBegin(TType.STRUCT, len(self.success)) + for iter55 in self.success: + iter55.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/__init__.py new file mode 100644 index 0000000000..515d97d672 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/__init__.py @@ -0,0 +1 @@ +__all__ = ['ttypes', 'constants', 'Collector'] diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/constants.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/constants.py new file mode 100644 index 0000000000..eb0d35aa12 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/constants.py @@ -0,0 +1,12 @@ +# +# Autogenerated by Thrift Compiler (0.10.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException +from thrift.protocol.TProtocol import TProtocolException +import sys +from .ttypes import * diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/ttypes.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/ttypes.py new file mode 100644 index 0000000000..a43252b79d --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/ttypes.py @@ -0,0 +1,831 @@ +# +# Autogenerated by Thrift Compiler (0.10.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException +from thrift.protocol.TProtocol import TProtocolException +import sys + +from thrift.transport import TTransport + + +class TagType(object): + STRING = 0 + DOUBLE = 1 + BOOL = 2 + LONG = 3 + BINARY = 4 + + _VALUES_TO_NAMES = { + 0: "STRING", + 1: "DOUBLE", + 2: "BOOL", + 3: "LONG", + 4: "BINARY", + } + + _NAMES_TO_VALUES = { + "STRING": 0, + "DOUBLE": 1, + "BOOL": 2, + "LONG": 3, + "BINARY": 4, + } + + +class SpanRefType(object): + CHILD_OF = 0 + FOLLOWS_FROM = 1 + + _VALUES_TO_NAMES = { + 0: "CHILD_OF", + 1: "FOLLOWS_FROM", + } + + _NAMES_TO_VALUES = { + "CHILD_OF": 0, + "FOLLOWS_FROM": 1, + } + + +class Tag(object): + """ + Attributes: + - key + - vType + - vStr + - vDouble + - vBool + - vLong + - vBinary + """ + + thrift_spec = ( + None, # 0 + (1, TType.STRING, 'key', 'UTF8', None, ), # 1 + (2, TType.I32, 'vType', None, None, ), # 2 + (3, TType.STRING, 'vStr', 'UTF8', None, ), # 3 + (4, TType.DOUBLE, 'vDouble', None, None, ), # 4 + (5, TType.BOOL, 'vBool', None, None, ), # 5 + (6, TType.I64, 'vLong', None, None, ), # 6 + (7, TType.STRING, 'vBinary', 'BINARY', None, ), # 7 + ) + + def __init__(self, key=None, vType=None, vStr=None, vDouble=None, vBool=None, vLong=None, vBinary=None,): + self.key = key + self.vType = vType + self.vStr = vStr + self.vDouble = vDouble + self.vBool = vBool + self.vLong = vLong + self.vBinary = vBinary + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.STRING: + self.key = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.I32: + self.vType = iprot.readI32() + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.STRING: + self.vStr = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 4: + if ftype == TType.DOUBLE: + self.vDouble = iprot.readDouble() + else: + iprot.skip(ftype) + elif fid == 5: + if ftype == TType.BOOL: + self.vBool = iprot.readBool() + else: + iprot.skip(ftype) + elif fid == 6: + if ftype == TType.I64: + self.vLong = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 7: + if ftype == TType.STRING: + self.vBinary = iprot.readBinary() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('Tag') + if self.key is not None: + oprot.writeFieldBegin('key', TType.STRING, 1) + oprot.writeString(self.key.encode('utf-8') if sys.version_info[0] == 2 else self.key) + oprot.writeFieldEnd() + if self.vType is not None: + oprot.writeFieldBegin('vType', TType.I32, 2) + oprot.writeI32(self.vType) + oprot.writeFieldEnd() + if self.vStr is not None: + oprot.writeFieldBegin('vStr', TType.STRING, 3) + oprot.writeString(self.vStr.encode('utf-8') if sys.version_info[0] == 2 else self.vStr) + oprot.writeFieldEnd() + if self.vDouble is not None: + oprot.writeFieldBegin('vDouble', TType.DOUBLE, 4) + oprot.writeDouble(self.vDouble) + oprot.writeFieldEnd() + if self.vBool is not None: + oprot.writeFieldBegin('vBool', TType.BOOL, 5) + oprot.writeBool(self.vBool) + oprot.writeFieldEnd() + if self.vLong is not None: + oprot.writeFieldBegin('vLong', TType.I64, 6) + oprot.writeI64(self.vLong) + oprot.writeFieldEnd() + if self.vBinary is not None: + oprot.writeFieldBegin('vBinary', TType.STRING, 7) + oprot.writeBinary(self.vBinary) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + if self.key is None: + raise TProtocolException(message='Required field key is unset!') + if self.vType is None: + raise TProtocolException(message='Required field vType is unset!') + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class Log(object): + """ + Attributes: + - timestamp + - fields + """ + + thrift_spec = ( + None, # 0 + (1, TType.I64, 'timestamp', None, None, ), # 1 + (2, TType.LIST, 'fields', (TType.STRUCT, (Tag, Tag.thrift_spec), False), None, ), # 2 + ) + + def __init__(self, timestamp=None, fields=None,): + self.timestamp = timestamp + self.fields = fields + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.I64: + self.timestamp = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.LIST: + self.fields = [] + (_etype3, _size0) = iprot.readListBegin() + for _i4 in range(_size0): + _elem5 = Tag() + _elem5.read(iprot) + self.fields.append(_elem5) + iprot.readListEnd() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('Log') + if self.timestamp is not None: + oprot.writeFieldBegin('timestamp', TType.I64, 1) + oprot.writeI64(self.timestamp) + oprot.writeFieldEnd() + if self.fields is not None: + oprot.writeFieldBegin('fields', TType.LIST, 2) + oprot.writeListBegin(TType.STRUCT, len(self.fields)) + for iter6 in self.fields: + iter6.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + if self.timestamp is None: + raise TProtocolException(message='Required field timestamp is unset!') + if self.fields is None: + raise TProtocolException(message='Required field fields is unset!') + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class SpanRef(object): + """ + Attributes: + - refType + - traceIdLow + - traceIdHigh + - spanId + """ + + thrift_spec = ( + None, # 0 + (1, TType.I32, 'refType', None, None, ), # 1 + (2, TType.I64, 'traceIdLow', None, None, ), # 2 + (3, TType.I64, 'traceIdHigh', None, None, ), # 3 + (4, TType.I64, 'spanId', None, None, ), # 4 + ) + + def __init__(self, refType=None, traceIdLow=None, traceIdHigh=None, spanId=None,): + self.refType = refType + self.traceIdLow = traceIdLow + self.traceIdHigh = traceIdHigh + self.spanId = spanId + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.I32: + self.refType = iprot.readI32() + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.I64: + self.traceIdLow = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.I64: + self.traceIdHigh = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 4: + if ftype == TType.I64: + self.spanId = iprot.readI64() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('SpanRef') + if self.refType is not None: + oprot.writeFieldBegin('refType', TType.I32, 1) + oprot.writeI32(self.refType) + oprot.writeFieldEnd() + if self.traceIdLow is not None: + oprot.writeFieldBegin('traceIdLow', TType.I64, 2) + oprot.writeI64(self.traceIdLow) + oprot.writeFieldEnd() + if self.traceIdHigh is not None: + oprot.writeFieldBegin('traceIdHigh', TType.I64, 3) + oprot.writeI64(self.traceIdHigh) + oprot.writeFieldEnd() + if self.spanId is not None: + oprot.writeFieldBegin('spanId', TType.I64, 4) + oprot.writeI64(self.spanId) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + if self.refType is None: + raise TProtocolException(message='Required field refType is unset!') + if self.traceIdLow is None: + raise TProtocolException(message='Required field traceIdLow is unset!') + if self.traceIdHigh is None: + raise TProtocolException(message='Required field traceIdHigh is unset!') + if self.spanId is None: + raise TProtocolException(message='Required field spanId is unset!') + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class Span(object): + """ + Attributes: + - traceIdLow + - traceIdHigh + - spanId + - parentSpanId + - operationName + - references + - flags + - startTime + - duration + - tags + - logs + """ + + thrift_spec = ( + None, # 0 + (1, TType.I64, 'traceIdLow', None, None, ), # 1 + (2, TType.I64, 'traceIdHigh', None, None, ), # 2 + (3, TType.I64, 'spanId', None, None, ), # 3 + (4, TType.I64, 'parentSpanId', None, None, ), # 4 + (5, TType.STRING, 'operationName', 'UTF8', None, ), # 5 + (6, TType.LIST, 'references', (TType.STRUCT, (SpanRef, SpanRef.thrift_spec), False), None, ), # 6 + (7, TType.I32, 'flags', None, None, ), # 7 + (8, TType.I64, 'startTime', None, None, ), # 8 + (9, TType.I64, 'duration', None, None, ), # 9 + (10, TType.LIST, 'tags', (TType.STRUCT, (Tag, Tag.thrift_spec), False), None, ), # 10 + (11, TType.LIST, 'logs', (TType.STRUCT, (Log, Log.thrift_spec), False), None, ), # 11 + ) + + def __init__(self, traceIdLow=None, traceIdHigh=None, spanId=None, parentSpanId=None, operationName=None, references=None, flags=None, startTime=None, duration=None, tags=None, logs=None,): + self.traceIdLow = traceIdLow + self.traceIdHigh = traceIdHigh + self.spanId = spanId + self.parentSpanId = parentSpanId + self.operationName = operationName + self.references = references + self.flags = flags + self.startTime = startTime + self.duration = duration + self.tags = tags + self.logs = logs + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.I64: + self.traceIdLow = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.I64: + self.traceIdHigh = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.I64: + self.spanId = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 4: + if ftype == TType.I64: + self.parentSpanId = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 5: + if ftype == TType.STRING: + self.operationName = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 6: + if ftype == TType.LIST: + self.references = [] + (_etype10, _size7) = iprot.readListBegin() + for _i11 in range(_size7): + _elem12 = SpanRef() + _elem12.read(iprot) + self.references.append(_elem12) + iprot.readListEnd() + else: + iprot.skip(ftype) + elif fid == 7: + if ftype == TType.I32: + self.flags = iprot.readI32() + else: + iprot.skip(ftype) + elif fid == 8: + if ftype == TType.I64: + self.startTime = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 9: + if ftype == TType.I64: + self.duration = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 10: + if ftype == TType.LIST: + self.tags = [] + (_etype16, _size13) = iprot.readListBegin() + for _i17 in range(_size13): + _elem18 = Tag() + _elem18.read(iprot) + self.tags.append(_elem18) + iprot.readListEnd() + else: + iprot.skip(ftype) + elif fid == 11: + if ftype == TType.LIST: + self.logs = [] + (_etype22, _size19) = iprot.readListBegin() + for _i23 in range(_size19): + _elem24 = Log() + _elem24.read(iprot) + self.logs.append(_elem24) + iprot.readListEnd() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('Span') + if self.traceIdLow is not None: + oprot.writeFieldBegin('traceIdLow', TType.I64, 1) + oprot.writeI64(self.traceIdLow) + oprot.writeFieldEnd() + if self.traceIdHigh is not None: + oprot.writeFieldBegin('traceIdHigh', TType.I64, 2) + oprot.writeI64(self.traceIdHigh) + oprot.writeFieldEnd() + if self.spanId is not None: + oprot.writeFieldBegin('spanId', TType.I64, 3) + oprot.writeI64(self.spanId) + oprot.writeFieldEnd() + if self.parentSpanId is not None: + oprot.writeFieldBegin('parentSpanId', TType.I64, 4) + oprot.writeI64(self.parentSpanId) + oprot.writeFieldEnd() + if self.operationName is not None: + oprot.writeFieldBegin('operationName', TType.STRING, 5) + oprot.writeString(self.operationName.encode('utf-8') if sys.version_info[0] == 2 else self.operationName) + oprot.writeFieldEnd() + if self.references is not None: + oprot.writeFieldBegin('references', TType.LIST, 6) + oprot.writeListBegin(TType.STRUCT, len(self.references)) + for iter25 in self.references: + iter25.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + if self.flags is not None: + oprot.writeFieldBegin('flags', TType.I32, 7) + oprot.writeI32(self.flags) + oprot.writeFieldEnd() + if self.startTime is not None: + oprot.writeFieldBegin('startTime', TType.I64, 8) + oprot.writeI64(self.startTime) + oprot.writeFieldEnd() + if self.duration is not None: + oprot.writeFieldBegin('duration', TType.I64, 9) + oprot.writeI64(self.duration) + oprot.writeFieldEnd() + if self.tags is not None: + oprot.writeFieldBegin('tags', TType.LIST, 10) + oprot.writeListBegin(TType.STRUCT, len(self.tags)) + for iter26 in self.tags: + iter26.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + if self.logs is not None: + oprot.writeFieldBegin('logs', TType.LIST, 11) + oprot.writeListBegin(TType.STRUCT, len(self.logs)) + for iter27 in self.logs: + iter27.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + if self.traceIdLow is None: + raise TProtocolException(message='Required field traceIdLow is unset!') + if self.traceIdHigh is None: + raise TProtocolException(message='Required field traceIdHigh is unset!') + if self.spanId is None: + raise TProtocolException(message='Required field spanId is unset!') + if self.parentSpanId is None: + raise TProtocolException(message='Required field parentSpanId is unset!') + if self.operationName is None: + raise TProtocolException(message='Required field operationName is unset!') + if self.flags is None: + raise TProtocolException(message='Required field flags is unset!') + if self.startTime is None: + raise TProtocolException(message='Required field startTime is unset!') + if self.duration is None: + raise TProtocolException(message='Required field duration is unset!') + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class Process(object): + """ + Attributes: + - serviceName + - tags + """ + + thrift_spec = ( + None, # 0 + (1, TType.STRING, 'serviceName', 'UTF8', None, ), # 1 + (2, TType.LIST, 'tags', (TType.STRUCT, (Tag, Tag.thrift_spec), False), None, ), # 2 + ) + + def __init__(self, serviceName=None, tags=None,): + self.serviceName = serviceName + self.tags = tags + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.STRING: + self.serviceName = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.LIST: + self.tags = [] + (_etype31, _size28) = iprot.readListBegin() + for _i32 in range(_size28): + _elem33 = Tag() + _elem33.read(iprot) + self.tags.append(_elem33) + iprot.readListEnd() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('Process') + if self.serviceName is not None: + oprot.writeFieldBegin('serviceName', TType.STRING, 1) + oprot.writeString(self.serviceName.encode('utf-8') if sys.version_info[0] == 2 else self.serviceName) + oprot.writeFieldEnd() + if self.tags is not None: + oprot.writeFieldBegin('tags', TType.LIST, 2) + oprot.writeListBegin(TType.STRUCT, len(self.tags)) + for iter34 in self.tags: + iter34.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + if self.serviceName is None: + raise TProtocolException(message='Required field serviceName is unset!') + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class Batch(object): + """ + Attributes: + - process + - spans + """ + + thrift_spec = ( + None, # 0 + (1, TType.STRUCT, 'process', (Process, Process.thrift_spec), None, ), # 1 + (2, TType.LIST, 'spans', (TType.STRUCT, (Span, Span.thrift_spec), False), None, ), # 2 + ) + + def __init__(self, process=None, spans=None,): + self.process = process + self.spans = spans + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.STRUCT: + self.process = Process() + self.process.read(iprot) + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.LIST: + self.spans = [] + (_etype38, _size35) = iprot.readListBegin() + for _i39 in range(_size35): + _elem40 = Span() + _elem40.read(iprot) + self.spans.append(_elem40) + iprot.readListEnd() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('Batch') + if self.process is not None: + oprot.writeFieldBegin('process', TType.STRUCT, 1) + self.process.write(oprot) + oprot.writeFieldEnd() + if self.spans is not None: + oprot.writeFieldBegin('spans', TType.LIST, 2) + oprot.writeListBegin(TType.STRUCT, len(self.spans)) + for iter41 in self.spans: + iter41.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + if self.process is None: + raise TProtocolException(message='Required field process is unset!') + if self.spans is None: + raise TProtocolException(message='Required field spans is unset!') + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class BatchSubmitResponse(object): + """ + Attributes: + - ok + """ + + thrift_spec = ( + None, # 0 + (1, TType.BOOL, 'ok', None, None, ), # 1 + ) + + def __init__(self, ok=None,): + self.ok = ok + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.BOOL: + self.ok = iprot.readBool() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('BatchSubmitResponse') + if self.ok is not None: + oprot.writeFieldBegin('ok', TType.BOOL, 1) + oprot.writeBool(self.ok) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + if self.ok is None: + raise TProtocolException(message='Required field ok is unset!') + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ZipkinCollector-remote b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ZipkinCollector-remote new file mode 100755 index 0000000000..2b59c3275d --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ZipkinCollector-remote @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# +# Autogenerated by Thrift Compiler (0.10.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +import sys +import pprint +if sys.version_info[0] > 2: + from urllib.parse import urlparse +else: + from urlparse import urlparse +from thrift.transport import TTransport, TSocket, TSSLSocket, THttpClient +from thrift.protocol.TBinaryProtocol import TBinaryProtocol + +from zipkincore import ZipkinCollector +from zipkincore.ttypes import * + +if len(sys.argv) <= 1 or sys.argv[1] == '--help': + print('') + print('Usage: ' + sys.argv[0] + ' [-h host[:port]] [-u url] [-f[ramed]] [-s[sl]] [-novalidate] [-ca_certs certs] [-keyfile keyfile] [-certfile certfile] function [arg1 [arg2...]]') + print('') + print('Functions:') + print(' submitZipkinBatch( spans)') + print('') + sys.exit(0) + +pp = pprint.PrettyPrinter(indent=2) +host = 'localhost' +port = 9090 +uri = '' +framed = False +ssl = False +validate = True +ca_certs = None +keyfile = None +certfile = None +http = False +argi = 1 + +if sys.argv[argi] == '-h': + parts = sys.argv[argi + 1].split(':') + host = parts[0] + if len(parts) > 1: + port = int(parts[1]) + argi += 2 + +if sys.argv[argi] == '-u': + url = urlparse(sys.argv[argi + 1]) + parts = url[1].split(':') + host = parts[0] + if len(parts) > 1: + port = int(parts[1]) + else: + port = 80 + uri = url[2] + if url[4]: + uri += '?%s' % url[4] + http = True + argi += 2 + +if sys.argv[argi] == '-f' or sys.argv[argi] == '-framed': + framed = True + argi += 1 + +if sys.argv[argi] == '-s' or sys.argv[argi] == '-ssl': + ssl = True + argi += 1 + +if sys.argv[argi] == '-novalidate': + validate = False + argi += 1 + +if sys.argv[argi] == '-ca_certs': + ca_certs = sys.argv[argi+1] + argi += 2 + +if sys.argv[argi] == '-keyfile': + keyfile = sys.argv[argi+1] + argi += 2 + +if sys.argv[argi] == '-certfile': + certfile = sys.argv[argi+1] + argi += 2 + +cmd = sys.argv[argi] +args = sys.argv[argi + 1:] + +if http: + transport = THttpClient.THttpClient(host, port, uri) +else: + if ssl: + socket = TSSLSocket.TSSLSocket(host, port, validate=validate, ca_certs=ca_certs, keyfile=keyfile, certfile=certfile) + else: + socket = TSocket.TSocket(host, port) + if framed: + transport = TTransport.TFramedTransport(socket) + else: + transport = TTransport.TBufferedTransport(socket) +protocol = TBinaryProtocol(transport) +client = ZipkinCollector.Client(protocol) +transport.open() + +if cmd == 'submitZipkinBatch': + if len(args) != 1: + print('submitZipkinBatch requires 1 args') + sys.exit(1) + pp.pprint(client.submitZipkinBatch(eval(args[0]),)) + +else: + print('Unrecognized method %s' % cmd) + sys.exit(1) + +transport.close() diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ZipkinCollector.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ZipkinCollector.py new file mode 100644 index 0000000000..6167a8e9f1 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ZipkinCollector.py @@ -0,0 +1,243 @@ +# +# Autogenerated by Thrift Compiler (0.10.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException +from thrift.protocol.TProtocol import TProtocolException +import sys +import logging +from .ttypes import * +from thrift.Thrift import TProcessor +from thrift.transport import TTransport + + +class Iface(object): + def submitZipkinBatch(self, spans): + """ + Parameters: + - spans + """ + pass + + +class Client(Iface): + def __init__(self, iprot, oprot=None): + self._iprot = self._oprot = iprot + if oprot is not None: + self._oprot = oprot + self._seqid = 0 + + def submitZipkinBatch(self, spans): + """ + Parameters: + - spans + """ + self.send_submitZipkinBatch(spans) + return self.recv_submitZipkinBatch() + + def send_submitZipkinBatch(self, spans): + self._oprot.writeMessageBegin('submitZipkinBatch', TMessageType.CALL, self._seqid) + args = submitZipkinBatch_args() + args.spans = spans + args.write(self._oprot) + self._oprot.writeMessageEnd() + self._oprot.trans.flush() + + def recv_submitZipkinBatch(self): + iprot = self._iprot + (fname, mtype, rseqid) = iprot.readMessageBegin() + if mtype == TMessageType.EXCEPTION: + x = TApplicationException() + x.read(iprot) + iprot.readMessageEnd() + raise x + result = submitZipkinBatch_result() + result.read(iprot) + iprot.readMessageEnd() + if result.success is not None: + return result.success + raise TApplicationException(TApplicationException.MISSING_RESULT, "submitZipkinBatch failed: unknown result") + + +class Processor(Iface, TProcessor): + def __init__(self, handler): + self._handler = handler + self._processMap = {} + self._processMap["submitZipkinBatch"] = Processor.process_submitZipkinBatch + + def process(self, iprot, oprot): + (name, type, seqid) = iprot.readMessageBegin() + if name not in self._processMap: + iprot.skip(TType.STRUCT) + iprot.readMessageEnd() + x = TApplicationException(TApplicationException.UNKNOWN_METHOD, 'Unknown function %s' % (name)) + oprot.writeMessageBegin(name, TMessageType.EXCEPTION, seqid) + x.write(oprot) + oprot.writeMessageEnd() + oprot.trans.flush() + return + else: + self._processMap[name](self, seqid, iprot, oprot) + return True + + def process_submitZipkinBatch(self, seqid, iprot, oprot): + args = submitZipkinBatch_args() + args.read(iprot) + iprot.readMessageEnd() + result = submitZipkinBatch_result() + try: + result.success = self._handler.submitZipkinBatch(args.spans) + msg_type = TMessageType.REPLY + except (TTransport.TTransportException, KeyboardInterrupt, SystemExit): + raise + except Exception as ex: + msg_type = TMessageType.EXCEPTION + logging.exception(ex) + result = TApplicationException(TApplicationException.INTERNAL_ERROR, 'Internal error') + oprot.writeMessageBegin("submitZipkinBatch", msg_type, seqid) + result.write(oprot) + oprot.writeMessageEnd() + oprot.trans.flush() + +# HELPER FUNCTIONS AND STRUCTURES + + +class submitZipkinBatch_args(object): + """ + Attributes: + - spans + """ + + thrift_spec = ( + None, # 0 + (1, TType.LIST, 'spans', (TType.STRUCT, (Span, Span.thrift_spec), False), None, ), # 1 + ) + + def __init__(self, spans=None,): + self.spans = spans + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.LIST: + self.spans = [] + (_etype17, _size14) = iprot.readListBegin() + for _i18 in range(_size14): + _elem19 = Span() + _elem19.read(iprot) + self.spans.append(_elem19) + iprot.readListEnd() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('submitZipkinBatch_args') + if self.spans is not None: + oprot.writeFieldBegin('spans', TType.LIST, 1) + oprot.writeListBegin(TType.STRUCT, len(self.spans)) + for iter20 in self.spans: + iter20.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class submitZipkinBatch_result(object): + """ + Attributes: + - success + """ + + thrift_spec = ( + (0, TType.LIST, 'success', (TType.STRUCT, (Response, Response.thrift_spec), False), None, ), # 0 + ) + + def __init__(self, success=None,): + self.success = success + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 0: + if ftype == TType.LIST: + self.success = [] + (_etype24, _size21) = iprot.readListBegin() + for _i25 in range(_size21): + _elem26 = Response() + _elem26.read(iprot) + self.success.append(_elem26) + iprot.readListEnd() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('submitZipkinBatch_result') + if self.success is not None: + oprot.writeFieldBegin('success', TType.LIST, 0) + oprot.writeListBegin(TType.STRUCT, len(self.success)) + for iter27 in self.success: + iter27.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/__init__.py new file mode 100644 index 0000000000..90e4f9d9c7 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/__init__.py @@ -0,0 +1 @@ +__all__ = ['ttypes', 'constants', 'ZipkinCollector'] diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/constants.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/constants.py new file mode 100644 index 0000000000..d66961b02b --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/constants.py @@ -0,0 +1,28 @@ +# +# Autogenerated by Thrift Compiler (0.10.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException +from thrift.protocol.TProtocol import TProtocolException +import sys +from .ttypes import * +CLIENT_SEND = "cs" +CLIENT_RECV = "cr" +SERVER_SEND = "ss" +SERVER_RECV = "sr" +MESSAGE_SEND = "ms" +MESSAGE_RECV = "mr" +WIRE_SEND = "ws" +WIRE_RECV = "wr" +CLIENT_SEND_FRAGMENT = "csf" +CLIENT_RECV_FRAGMENT = "crf" +SERVER_SEND_FRAGMENT = "ssf" +SERVER_RECV_FRAGMENT = "srf" +LOCAL_COMPONENT = "lc" +CLIENT_ADDR = "ca" +SERVER_ADDR = "sa" +MESSAGE_ADDR = "ma" diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ttypes.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ttypes.py new file mode 100644 index 0000000000..251c5a3694 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ttypes.py @@ -0,0 +1,647 @@ +# +# Autogenerated by Thrift Compiler (0.10.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException +from thrift.protocol.TProtocol import TProtocolException +import sys + +from thrift.transport import TTransport + + +class AnnotationType(object): + BOOL = 0 + BYTES = 1 + I16 = 2 + I32 = 3 + I64 = 4 + DOUBLE = 5 + STRING = 6 + + _VALUES_TO_NAMES = { + 0: "BOOL", + 1: "BYTES", + 2: "I16", + 3: "I32", + 4: "I64", + 5: "DOUBLE", + 6: "STRING", + } + + _NAMES_TO_VALUES = { + "BOOL": 0, + "BYTES": 1, + "I16": 2, + "I32": 3, + "I64": 4, + "DOUBLE": 5, + "STRING": 6, + } + + +class Endpoint(object): + """ + Indicates the network context of a service recording an annotation with two + exceptions. + + When a BinaryAnnotation, and key is CLIENT_ADDR or SERVER_ADDR, + the endpoint indicates the source or destination of an RPC. This exception + allows zipkin to display network context of uninstrumented services, or + clients such as web browsers. + + Attributes: + - ipv4: IPv4 host address packed into 4 bytes. + + Ex for the ip 1.2.3.4, it would be (1 << 24) | (2 << 16) | (3 << 8) | 4 + - port: IPv4 port + + Note: this is to be treated as an unsigned integer, so watch for negatives. + + Conventionally, when the port isn't known, port = 0. + - service_name: Service name in lowercase, such as "memcache" or "zipkin-web" + + Conventionally, when the service name isn't known, service_name = "unknown". + - ipv6: IPv6 host address packed into 16 bytes. Ex Inet6Address.getBytes() + """ + + thrift_spec = ( + None, # 0 + (1, TType.I32, 'ipv4', None, None, ), # 1 + (2, TType.I16, 'port', None, None, ), # 2 + (3, TType.STRING, 'service_name', 'UTF8', None, ), # 3 + (4, TType.STRING, 'ipv6', 'BINARY', None, ), # 4 + ) + + def __init__(self, ipv4=None, port=None, service_name=None, ipv6=None,): + self.ipv4 = ipv4 + self.port = port + self.service_name = service_name + self.ipv6 = ipv6 + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.I32: + self.ipv4 = iprot.readI32() + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.I16: + self.port = iprot.readI16() + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.STRING: + self.service_name = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 4: + if ftype == TType.STRING: + self.ipv6 = iprot.readBinary() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('Endpoint') + if self.ipv4 is not None: + oprot.writeFieldBegin('ipv4', TType.I32, 1) + oprot.writeI32(self.ipv4) + oprot.writeFieldEnd() + if self.port is not None: + oprot.writeFieldBegin('port', TType.I16, 2) + oprot.writeI16(self.port) + oprot.writeFieldEnd() + if self.service_name is not None: + oprot.writeFieldBegin('service_name', TType.STRING, 3) + oprot.writeString(self.service_name.encode('utf-8') if sys.version_info[0] == 2 else self.service_name) + oprot.writeFieldEnd() + if self.ipv6 is not None: + oprot.writeFieldBegin('ipv6', TType.STRING, 4) + oprot.writeBinary(self.ipv6) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class Annotation(object): + """ + An annotation is similar to a log statement. It includes a host field which + allows these events to be attributed properly, and also aggregatable. + + Attributes: + - timestamp: Microseconds from epoch. + + This value should use the most precise value possible. For example, + gettimeofday or syncing nanoTime against a tick of currentTimeMillis. + - value + - host: Always the host that recorded the event. By specifying the host you allow + rollup of all events (such as client requests to a service) by IP address. + """ + + thrift_spec = ( + None, # 0 + (1, TType.I64, 'timestamp', None, None, ), # 1 + (2, TType.STRING, 'value', 'UTF8', None, ), # 2 + (3, TType.STRUCT, 'host', (Endpoint, Endpoint.thrift_spec), None, ), # 3 + ) + + def __init__(self, timestamp=None, value=None, host=None,): + self.timestamp = timestamp + self.value = value + self.host = host + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.I64: + self.timestamp = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.STRING: + self.value = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.STRUCT: + self.host = Endpoint() + self.host.read(iprot) + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('Annotation') + if self.timestamp is not None: + oprot.writeFieldBegin('timestamp', TType.I64, 1) + oprot.writeI64(self.timestamp) + oprot.writeFieldEnd() + if self.value is not None: + oprot.writeFieldBegin('value', TType.STRING, 2) + oprot.writeString(self.value.encode('utf-8') if sys.version_info[0] == 2 else self.value) + oprot.writeFieldEnd() + if self.host is not None: + oprot.writeFieldBegin('host', TType.STRUCT, 3) + self.host.write(oprot) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class BinaryAnnotation(object): + """ + Binary annotations are tags applied to a Span to give it context. For + example, a binary annotation of "http.uri" could the path to a resource in a + RPC call. + + Binary annotations of type STRING are always queryable, though more a + historical implementation detail than a structural concern. + + Binary annotations can repeat, and vary on the host. Similar to Annotation, + the host indicates who logged the event. This allows you to tell the + difference between the client and server side of the same key. For example, + the key "http.uri" might be different on the client and server side due to + rewriting, like "/api/v1/myresource" vs "/myresource. Via the host field, + you can see the different points of view, which often help in debugging. + + Attributes: + - key + - value + - annotation_type + - host: The host that recorded tag, which allows you to differentiate between + multiple tags with the same key. There are two exceptions to this. + + When the key is CLIENT_ADDR or SERVER_ADDR, host indicates the source or + destination of an RPC. This exception allows zipkin to display network + context of uninstrumented services, or clients such as web browsers. + """ + + thrift_spec = ( + None, # 0 + (1, TType.STRING, 'key', 'UTF8', None, ), # 1 + (2, TType.STRING, 'value', 'BINARY', None, ), # 2 + (3, TType.I32, 'annotation_type', None, None, ), # 3 + (4, TType.STRUCT, 'host', (Endpoint, Endpoint.thrift_spec), None, ), # 4 + ) + + def __init__(self, key=None, value=None, annotation_type=None, host=None,): + self.key = key + self.value = value + self.annotation_type = annotation_type + self.host = host + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.STRING: + self.key = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.STRING: + self.value = iprot.readBinary() + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.I32: + self.annotation_type = iprot.readI32() + else: + iprot.skip(ftype) + elif fid == 4: + if ftype == TType.STRUCT: + self.host = Endpoint() + self.host.read(iprot) + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('BinaryAnnotation') + if self.key is not None: + oprot.writeFieldBegin('key', TType.STRING, 1) + oprot.writeString(self.key.encode('utf-8') if sys.version_info[0] == 2 else self.key) + oprot.writeFieldEnd() + if self.value is not None: + oprot.writeFieldBegin('value', TType.STRING, 2) + oprot.writeBinary(self.value) + oprot.writeFieldEnd() + if self.annotation_type is not None: + oprot.writeFieldBegin('annotation_type', TType.I32, 3) + oprot.writeI32(self.annotation_type) + oprot.writeFieldEnd() + if self.host is not None: + oprot.writeFieldBegin('host', TType.STRUCT, 4) + self.host.write(oprot) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class Span(object): + """ + A trace is a series of spans (often RPC calls) which form a latency tree. + + The root span is where trace_id = id and parent_id = Nil. The root span is + usually the longest interval in the trace, starting with a SERVER_RECV + annotation and ending with a SERVER_SEND. + + Attributes: + - trace_id + - name: Span name in lowercase, rpc method for example + + Conventionally, when the span name isn't known, name = "unknown". + - id + - parent_id + - annotations + - binary_annotations + - debug + - timestamp: Microseconds from epoch of the creation of this span. + + This value should be set directly by instrumentation, using the most + precise value possible. For example, gettimeofday or syncing nanoTime + against a tick of currentTimeMillis. + + For compatibilty with instrumentation that precede this field, collectors + or span stores can derive this via Annotation.timestamp. + For example, SERVER_RECV.timestamp or CLIENT_SEND.timestamp. + + This field is optional for compatibility with old data: first-party span + stores are expected to support this at time of introduction. + - duration: Measurement of duration in microseconds, used to support queries. + + This value should be set directly, where possible. Doing so encourages + precise measurement decoupled from problems of clocks, such as skew or NTP + updates causing time to move backwards. + + For compatibilty with instrumentation that precede this field, collectors + or span stores can derive this by subtracting Annotation.timestamp. + For example, SERVER_SEND.timestamp - SERVER_RECV.timestamp. + + If this field is persisted as unset, zipkin will continue to work, except + duration query support will be implementation-specific. Similarly, setting + this field non-atomically is implementation-specific. + + This field is i64 vs i32 to support spans longer than 35 minutes. + - trace_id_high: Optional unique 8-byte additional identifier for a trace. If non zero, this + means the trace uses 128 bit traceIds instead of 64 bit. + """ + + thrift_spec = ( + None, # 0 + (1, TType.I64, 'trace_id', None, None, ), # 1 + None, # 2 + (3, TType.STRING, 'name', 'UTF8', None, ), # 3 + (4, TType.I64, 'id', None, None, ), # 4 + (5, TType.I64, 'parent_id', None, None, ), # 5 + (6, TType.LIST, 'annotations', (TType.STRUCT, (Annotation, Annotation.thrift_spec), False), None, ), # 6 + None, # 7 + (8, TType.LIST, 'binary_annotations', (TType.STRUCT, (BinaryAnnotation, BinaryAnnotation.thrift_spec), False), None, ), # 8 + (9, TType.BOOL, 'debug', None, False, ), # 9 + (10, TType.I64, 'timestamp', None, None, ), # 10 + (11, TType.I64, 'duration', None, None, ), # 11 + (12, TType.I64, 'trace_id_high', None, None, ), # 12 + ) + + def __init__(self, trace_id=None, name=None, id=None, parent_id=None, annotations=None, binary_annotations=None, debug=thrift_spec[9][4], timestamp=None, duration=None, trace_id_high=None,): + self.trace_id = trace_id + self.name = name + self.id = id + self.parent_id = parent_id + self.annotations = annotations + self.binary_annotations = binary_annotations + self.debug = debug + self.timestamp = timestamp + self.duration = duration + self.trace_id_high = trace_id_high + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.I64: + self.trace_id = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.STRING: + self.name = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 4: + if ftype == TType.I64: + self.id = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 5: + if ftype == TType.I64: + self.parent_id = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 6: + if ftype == TType.LIST: + self.annotations = [] + (_etype3, _size0) = iprot.readListBegin() + for _i4 in range(_size0): + _elem5 = Annotation() + _elem5.read(iprot) + self.annotations.append(_elem5) + iprot.readListEnd() + else: + iprot.skip(ftype) + elif fid == 8: + if ftype == TType.LIST: + self.binary_annotations = [] + (_etype9, _size6) = iprot.readListBegin() + for _i10 in range(_size6): + _elem11 = BinaryAnnotation() + _elem11.read(iprot) + self.binary_annotations.append(_elem11) + iprot.readListEnd() + else: + iprot.skip(ftype) + elif fid == 9: + if ftype == TType.BOOL: + self.debug = iprot.readBool() + else: + iprot.skip(ftype) + elif fid == 10: + if ftype == TType.I64: + self.timestamp = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 11: + if ftype == TType.I64: + self.duration = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 12: + if ftype == TType.I64: + self.trace_id_high = iprot.readI64() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('Span') + if self.trace_id is not None: + oprot.writeFieldBegin('trace_id', TType.I64, 1) + oprot.writeI64(self.trace_id) + oprot.writeFieldEnd() + if self.name is not None: + oprot.writeFieldBegin('name', TType.STRING, 3) + oprot.writeString(self.name.encode('utf-8') if sys.version_info[0] == 2 else self.name) + oprot.writeFieldEnd() + if self.id is not None: + oprot.writeFieldBegin('id', TType.I64, 4) + oprot.writeI64(self.id) + oprot.writeFieldEnd() + if self.parent_id is not None: + oprot.writeFieldBegin('parent_id', TType.I64, 5) + oprot.writeI64(self.parent_id) + oprot.writeFieldEnd() + if self.annotations is not None: + oprot.writeFieldBegin('annotations', TType.LIST, 6) + oprot.writeListBegin(TType.STRUCT, len(self.annotations)) + for iter12 in self.annotations: + iter12.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + if self.binary_annotations is not None: + oprot.writeFieldBegin('binary_annotations', TType.LIST, 8) + oprot.writeListBegin(TType.STRUCT, len(self.binary_annotations)) + for iter13 in self.binary_annotations: + iter13.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + if self.debug is not None: + oprot.writeFieldBegin('debug', TType.BOOL, 9) + oprot.writeBool(self.debug) + oprot.writeFieldEnd() + if self.timestamp is not None: + oprot.writeFieldBegin('timestamp', TType.I64, 10) + oprot.writeI64(self.timestamp) + oprot.writeFieldEnd() + if self.duration is not None: + oprot.writeFieldBegin('duration', TType.I64, 11) + oprot.writeI64(self.duration) + oprot.writeFieldEnd() + if self.trace_id_high is not None: + oprot.writeFieldBegin('trace_id_high', TType.I64, 12) + oprot.writeI64(self.trace_id_high) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class Response(object): + """ + Attributes: + - ok + """ + + thrift_spec = ( + None, # 0 + (1, TType.BOOL, 'ok', None, None, ), # 1 + ) + + def __init__(self, ok=None,): + self.ok = ok + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.BOOL: + self.ok = iprot.readBool() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('Response') + if self.ok is not None: + oprot.writeFieldBegin('ok', TType.BOOL, 1) + oprot.writeBool(self.ok) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + if self.ok is None: + raise TProtocolException(message='Required field ok is unset!') + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py new file mode 100644 index 0000000000..262f246714 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -0,0 +1,16 @@ +# Copyright 2019, OpenCensus Authors +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.1.dev0" diff --git a/ext/opentelemetry-ext-jaeger/tests/__init__.py b/ext/opentelemetry-ext-jaeger/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py new file mode 100644 index 0000000000..3fb14cd354 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py @@ -0,0 +1,254 @@ +# Copyright 2018, OpenCensus Authors +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +# pylint:disable=no-name-in-module +# pylint:disable=import-error +import opentelemetry.ext.jaeger as jaeger_exporter +from opentelemetry import trace as trace_api +from opentelemetry.ext.jaeger.gen.jaeger import ttypes as jaeger +from opentelemetry.sdk import trace + + +class TestJaegerSpanExporter(unittest.TestCase): + def test_constructor_default(self): + service_name = "my-service-name" + host_name = "localhost" + thrift_port = None + agent_port = 6831 + collector_endpoint = "/api/traces?format=jaeger.thrift" + exporter = jaeger_exporter.JaegerSpanExporter(service_name) + + self.assertEqual(exporter.service_name, service_name) + self.assertEqual(exporter.collector_host_name, None) + self.assertEqual(exporter.agent_host_name, host_name) + self.assertEqual(exporter.agent_port, agent_port) + self.assertEqual(exporter.collector_port, thrift_port) + self.assertEqual(exporter.collector_endpoint, collector_endpoint) + self.assertEqual(exporter.username, None) + self.assertEqual(exporter.password, None) + self.assertTrue(exporter.collector is None) + self.assertTrue(exporter.agent_client is not None) + + def test_constructor_explicit(self): + service = "my-opentelemetry-jaeger" + collector_host_name = "opentelemetry.io" + collector_port = 15875 + collector_endpoint = "/myapi/traces?format=jaeger.thrift" + + agent_port = 14268 + agent_host_name = "opentelemetry.com" + + username = "username" + password = "password" + auth = (username, password) + + exporter = jaeger_exporter.JaegerSpanExporter( + service_name=service, + collector_host_name=collector_host_name, + collector_port=collector_port, + collector_endpoint=collector_endpoint, + agent_host_name=agent_host_name, + agent_port=agent_port, + username=username, + password=password, + ) + self.assertEqual(exporter.service_name, service) + self.assertEqual(exporter.agent_host_name, agent_host_name) + self.assertEqual(exporter.agent_port, agent_port) + self.assertEqual(exporter.collector_host_name, collector_host_name) + self.assertEqual(exporter.collector_port, collector_port) + self.assertTrue(exporter.collector is not None) + self.assertEqual(exporter.collector.auth, auth) + # property should not construct new object + collector = exporter.collector + self.assertEqual(exporter.collector, collector) + # property should construct new object + # pylint: disable=protected-access + exporter._collector = None + exporter.username = None + exporter.password = None + self.assertNotEqual(exporter.collector, collector) + self.assertTrue(exporter.collector.auth is None) + + # pylint: disable=too-many-locals + def test_translate_to_jaeger(self): + # pylint: disable=invalid-name + self.maxDiff = None + + span_names = ("test1", "test2", "test3") + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + trace_id_high = 0x6E0C63257DE34C92 + trace_id_low = 0x6F9EFCD03927272E + span_id = 0x34BF92DEEFC58C92 + parent_id = 0x1111111111111111 + other_id = 0x2222222222222222 + + base_time = 683647322 * 1e9 # in ns + start_times = (base_time, base_time + 150 * 1e6, base_time + 300 * 1e6) + durations = (50 * 1e6, 100 * 1e6, 200 * 1e6) + end_times = ( + start_times[0] + durations[0], + start_times[1] + durations[1], + start_times[2] + durations[2], + ) + + span_context = trace_api.SpanContext(trace_id, span_id) + parent_context = trace_api.SpanContext(trace_id, parent_id) + other_context = trace_api.SpanContext(trace_id, other_id) + + event_attributes = { + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + } + + event_timestamp = base_time + 50e6 + event = trace_api.Event( + name="event0", + timestamp=event_timestamp, + attributes=event_attributes, + ) + + link_attributes = {"key_bool": True} + + link = trace_api.Link( + context=other_context, attributes=link_attributes + ) + + otel_spans = [ + trace.Span( + name=span_names[0], + context=span_context, + parent=parent_context, + events=(event,), + links=(link,), + ), + trace.Span( + name=span_names[1], context=parent_context, parent=None + ), + trace.Span(name=span_names[2], context=other_context, parent=None), + ] + + otel_spans[0].start_time = start_times[0] + # added here to preserve order + otel_spans[0].set_attribute("key_bool", False) + otel_spans[0].set_attribute("key_string", "hello_world") + otel_spans[0].set_attribute("key_float", 111.22) + otel_spans[0].end_time = end_times[0] + + otel_spans[1].start_time = start_times[1] + otel_spans[1].end_time = end_times[1] + + otel_spans[2].start_time = start_times[2] + otel_spans[2].end_time = end_times[2] + + # pylint: disable=protected-access + spans = jaeger_exporter._translate_to_jaeger(otel_spans) + + expected_spans = [ + jaeger.Span( + operationName=span_names[0], + traceIdHigh=trace_id_high, + traceIdLow=trace_id_low, + spanId=span_id, + parentSpanId=parent_id, + startTime=start_times[0] / 1e3, + duration=durations[0] / 1e3, + flags=0, + tags=[ + jaeger.Tag( + key="key_bool", vType=jaeger.TagType.BOOL, vBool=False + ), + jaeger.Tag( + key="key_string", + vType=jaeger.TagType.STRING, + vStr="hello_world", + ), + jaeger.Tag( + key="key_float", + vType=jaeger.TagType.DOUBLE, + vDouble=111.22, + ), + ], + references=[ + jaeger.SpanRef( + refType=jaeger.SpanRefType.FOLLOWS_FROM, + traceIdHigh=trace_id_high, + traceIdLow=trace_id_low, + spanId=other_id, + ) + ], + logs=[ + jaeger.Log( + timestamp=event_timestamp / 1e3, + fields=[ + jaeger.Tag( + key="annotation_bool", + vType=jaeger.TagType.BOOL, + vBool=True, + ), + jaeger.Tag( + key="annotation_string", + vType=jaeger.TagType.STRING, + vStr="annotation_test", + ), + jaeger.Tag( + key="key_float", + vType=jaeger.TagType.DOUBLE, + vDouble=0.3, + ), + jaeger.Tag( + key="message", + vType=jaeger.TagType.STRING, + vStr="event0", + ), + ], + ) + ], + ), + jaeger.Span( + operationName=span_names[1], + traceIdHigh=trace_id_high, + traceIdLow=trace_id_low, + spanId=parent_id, + parentSpanId=0, + startTime=int(start_times[1] // 1e3), + duration=int(durations[1] // 1e3), + flags=0, + ), + jaeger.Span( + operationName=span_names[2], + traceIdHigh=trace_id_high, + traceIdLow=trace_id_low, + spanId=other_id, + parentSpanId=0, + startTime=int(start_times[2] // 1e3), + duration=int(durations[2] // 1e3), + flags=0, + ), + ] + + # events are complicated to compare because order of fields + # (attributes) is otel is not important but in jeager it is + self.assertCountEqual( + spans[0].logs[0].fields, expected_spans[0].logs[0].fields + ) + # get rid of fields to be able to compare the whole spans + spans[0].logs[0].fields = None + expected_spans[0].logs[0].fields = None + + self.assertEqual(spans, expected_spans) diff --git a/ext/opentelemetry-ext-jaeger/thrift/agent.thrift b/ext/opentelemetry-ext-jaeger/thrift/agent.thrift new file mode 100644 index 0000000000..5d3c9201b6 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/thrift/agent.thrift @@ -0,0 +1,27 @@ +# Copyright (c) 2016 Uber Technologies, 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. + +include "jaeger.thrift" +include "zipkincore.thrift" + +namespace cpp jaegertracing.agent.thrift +namespace java io.jaegertracing.agent.thrift +namespace php Jaeger.Thrift.Agent +namespace netcore Jaeger.Thrift.Agent +namespace lua jaeger.thrift.agent + +service Agent { + oneway void emitZipkinBatch(1: list spans) + oneway void emitBatch(1: jaeger.Batch batch) +} diff --git a/ext/opentelemetry-ext-jaeger/thrift/jaeger.thrift b/ext/opentelemetry-ext-jaeger/thrift/jaeger.thrift new file mode 100644 index 0000000000..ae9fcaa014 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/thrift/jaeger.thrift @@ -0,0 +1,85 @@ +# Copyright (c) 2016 Uber Technologies, 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. + +namespace cpp jaegertracing.thrift +namespace java io.jaegertracing.thriftjava +namespace php Jaeger.Thrift +namespace netcore Jaeger.Thrift +namespace lua jaeger.thrift + +# TagType denotes the type of a Tag's value. +enum TagType { STRING, DOUBLE, BOOL, LONG, BINARY } + +# Tag is a basic strongly typed key/value pair. It has been flattened to reduce the use of pointers in golang +struct Tag { + 1: required string key + 2: required TagType vType + 3: optional string vStr + 4: optional double vDouble + 5: optional bool vBool + 6: optional i64 vLong + 7: optional binary vBinary +} + +# Log is a timed even with an arbitrary set of tags. +struct Log { + 1: required i64 timestamp + 2: required list fields +} + +enum SpanRefType { CHILD_OF, FOLLOWS_FROM } + +# SpanRef describes causal relationship of the current span to another span (e.g. 'child-of') +struct SpanRef { + 1: required SpanRefType refType + 2: required i64 traceIdLow + 3: required i64 traceIdHigh + 4: required i64 spanId +} + +# Span represents a named unit of work performed by a service. +struct Span { + 1: required i64 traceIdLow # the least significant 64 bits of a traceID + 2: required i64 traceIdHigh # the most significant 64 bits of a traceID; 0 when only 64bit IDs are used + 3: required i64 spanId # unique span id (only unique within a given trace) + 4: required i64 parentSpanId # since nearly all spans will have parents spans, CHILD_OF refs do not have to be explicit + 5: required string operationName + 6: optional list references # causal references to other spans + 7: required i32 flags # a bit field used to propagate sampling decisions. 1 signifies a SAMPLED span, 2 signifies a DEBUG span. + 8: required i64 startTime + 9: required i64 duration + 10: optional list tags + 11: optional list logs +} + +# Process describes the traced process/service that emits spans. +struct Process { + 1: required string serviceName + 2: optional list tags +} + +# Batch is a collection of spans reported out of process. +struct Batch { + 1: required Process process + 2: required list spans +} + +# BatchSubmitResponse is the response on submitting a batch. +struct BatchSubmitResponse { + 1: required bool ok # The Collector's client is expected to only log (or emit a counter) when not ok equals false +} + +service Collector { + list submitBatches(1: list batches) +} diff --git a/ext/opentelemetry-ext-jaeger/thrift/zipkincore.thrift b/ext/opentelemetry-ext-jaeger/thrift/zipkincore.thrift new file mode 100644 index 0000000000..d5259e78b9 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/thrift/zipkincore.thrift @@ -0,0 +1,346 @@ +# Copyright 2012 Twitter 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. +namespace cpp twitter.zipkin.thrift +namespace java com.twitter.zipkin.thriftjava +#@namespace scala com.twitter.zipkin.thriftscala +namespace rb Zipkin +namespace php Jaeger.Thrift.Agent.Zipkin +namespace netcore Jaeger.Thrift.Agent.Zipkin +namespace lua jaeger.thrift.agent + + +#************** Annotation.value ************** +/** + * The client sent ("cs") a request to a server. There is only one send per + * span. For example, if there's a transport error, each attempt can be logged + * as a WIRE_SEND annotation. + * + * If chunking is involved, each chunk could be logged as a separate + * CLIENT_SEND_FRAGMENT in the same span. + * + * Annotation.host is not the server. It is the host which logged the send + * event, almost always the client. When logging CLIENT_SEND, instrumentation + * should also log the SERVER_ADDR. + */ +const string CLIENT_SEND = "cs" +/** + * The client received ("cr") a response from a server. There is only one + * receive per span. For example, if duplicate responses were received, each + * can be logged as a WIRE_RECV annotation. + * + * If chunking is involved, each chunk could be logged as a separate + * CLIENT_RECV_FRAGMENT in the same span. + * + * Annotation.host is not the server. It is the host which logged the receive + * event, almost always the client. The actual endpoint of the server is + * recorded separately as SERVER_ADDR when CLIENT_SEND is logged. + */ +const string CLIENT_RECV = "cr" +/** + * The server sent ("ss") a response to a client. There is only one response + * per span. If there's a transport error, each attempt can be logged as a + * WIRE_SEND annotation. + * + * Typically, a trace ends with a server send, so the last timestamp of a trace + * is often the timestamp of the root span's server send. + * + * If chunking is involved, each chunk could be logged as a separate + * SERVER_SEND_FRAGMENT in the same span. + * + * Annotation.host is not the client. It is the host which logged the send + * event, almost always the server. The actual endpoint of the client is + * recorded separately as CLIENT_ADDR when SERVER_RECV is logged. + */ +const string SERVER_SEND = "ss" +/** + * The server received ("sr") a request from a client. There is only one + * request per span. For example, if duplicate responses were received, each + * can be logged as a WIRE_RECV annotation. + * + * Typically, a trace starts with a server receive, so the first timestamp of a + * trace is often the timestamp of the root span's server receive. + * + * If chunking is involved, each chunk could be logged as a separate + * SERVER_RECV_FRAGMENT in the same span. + * + * Annotation.host is not the client. It is the host which logged the receive + * event, almost always the server. When logging SERVER_RECV, instrumentation + * should also log the CLIENT_ADDR. + */ +const string SERVER_RECV = "sr" +/** + * Message send ("ms") is a request to send a message to a destination, usually + * a broker. This may be the only annotation in a messaging span. If WIRE_SEND + * exists in the same span, it follows this moment and clarifies delays sending + * the message, such as batching. + * + * Unlike RPC annotations like CLIENT_SEND, messaging spans never share a span + * ID. For example, "ms" should always be the parent of "mr". + * + * Annotation.host is not the destination, it is the host which logged the send + * event: the producer. When annotating MESSAGE_SEND, instrumentation should + * also tag the MESSAGE_ADDR. + */ +const string MESSAGE_SEND = "ms" +/** + * A consumer received ("mr") a message from a broker. This may be the only + * annotation in a messaging span. If WIRE_RECV exists in the same span, it + * precedes this moment and clarifies any local queuing delay. + * + * Unlike RPC annotations like SERVER_RECV, messaging spans never share a span + * ID. For example, "mr" should always be a child of "ms" unless it is a root + * span. + * + * Annotation.host is not the broker, it is the host which logged the receive + * event: the consumer. When annotating MESSAGE_RECV, instrumentation should + * also tag the MESSAGE_ADDR. + */ +const string MESSAGE_RECV = "mr" +/** + * Optionally logs an attempt to send a message on the wire. Multiple wire send + * events could indicate network retries. A lag between client or server send + * and wire send might indicate queuing or processing delay. + */ +const string WIRE_SEND = "ws" +/** + * Optionally logs an attempt to receive a message from the wire. Multiple wire + * receive events could indicate network retries. A lag between wire receive + * and client or server receive might indicate queuing or processing delay. + */ +const string WIRE_RECV = "wr" +/** + * Optionally logs progress of a (CLIENT_SEND, WIRE_SEND). For example, this + * could be one chunk in a chunked request. + */ +const string CLIENT_SEND_FRAGMENT = "csf" +/** + * Optionally logs progress of a (CLIENT_RECV, WIRE_RECV). For example, this + * could be one chunk in a chunked response. + */ +const string CLIENT_RECV_FRAGMENT = "crf" +/** + * Optionally logs progress of a (SERVER_SEND, WIRE_SEND). For example, this + * could be one chunk in a chunked response. + */ +const string SERVER_SEND_FRAGMENT = "ssf" +/** + * Optionally logs progress of a (SERVER_RECV, WIRE_RECV). For example, this + * could be one chunk in a chunked request. + */ +const string SERVER_RECV_FRAGMENT = "srf" + +#***** BinaryAnnotation.key ****** +/** + * The value of "lc" is the component or namespace of a local span. + * + * BinaryAnnotation.host adds service context needed to support queries. + * + * Local Component("lc") supports three key features: flagging, query by + * service and filtering Span.name by namespace. + * + * While structurally the same, local spans are fundamentally different than + * RPC spans in how they should be interpreted. For example, zipkin v1 tools + * center on RPC latency and service graphs. Root local-spans are neither + * indicative of critical path RPC latency, nor have impact on the shape of a + * service graph. By flagging with "lc", tools can special-case local spans. + * + * Zipkin v1 Spans are unqueryable unless they can be indexed by service name. + * The only path to a service name is by (Binary)?Annotation.host.serviceName. + * By logging "lc", a local span can be queried even if no other annotations + * are logged. + * + * The value of "lc" is the namespace of Span.name. For example, it might be + * "finatra2", for a span named "bootstrap". "lc" allows you to resolves + * conflicts for the same Span.name, for example "finatra/bootstrap" vs + * "finch/bootstrap". Using local component, you'd search for spans named + * "bootstrap" where "lc=finch" + */ +const string LOCAL_COMPONENT = "lc" + +#***** BinaryAnnotation.key where value = [1] and annotation_type = BOOL ****** +/** + * Indicates a client address ("ca") in a span. Most likely, there's only one. + * Multiple addresses are possible when a client changes its ip or port within + * a span. + */ +const string CLIENT_ADDR = "ca" +/** + * Indicates a server address ("sa") in a span. Most likely, there's only one. + * Multiple addresses are possible when a client is redirected, or fails to a + * different server ip or port. + */ +const string SERVER_ADDR = "sa" +/** + * Indicates the remote address of a messaging span, usually the broker. + */ +const string MESSAGE_ADDR = "ma" + +/** + * Indicates the network context of a service recording an annotation with two + * exceptions. + * + * When a BinaryAnnotation, and key is CLIENT_ADDR or SERVER_ADDR, + * the endpoint indicates the source or destination of an RPC. This exception + * allows zipkin to display network context of uninstrumented services, or + * clients such as web browsers. + */ +struct Endpoint { + /** + * IPv4 host address packed into 4 bytes. + * + * Ex for the ip 1.2.3.4, it would be (1 << 24) | (2 << 16) | (3 << 8) | 4 + */ + 1: i32 ipv4 + /** + * IPv4 port + * + * Note: this is to be treated as an unsigned integer, so watch for negatives. + * + * Conventionally, when the port isn't known, port = 0. + */ + 2: i16 port + /** + * Service name in lowercase, such as "memcache" or "zipkin-web" + * + * Conventionally, when the service name isn't known, service_name = "unknown". + */ + 3: string service_name + /** + * IPv6 host address packed into 16 bytes. Ex Inet6Address.getBytes() + */ + 4: optional binary ipv6 +} + +/** + * An annotation is similar to a log statement. It includes a host field which + * allows these events to be attributed properly, and also aggregatable. + */ +struct Annotation { + /** + * Microseconds from epoch. + * + * This value should use the most precise value possible. For example, + * gettimeofday or syncing nanoTime against a tick of currentTimeMillis. + */ + 1: i64 timestamp + 2: string value // what happened at the timestamp? + /** + * Always the host that recorded the event. By specifying the host you allow + * rollup of all events (such as client requests to a service) by IP address. + */ + 3: optional Endpoint host + // don't reuse 4: optional i32 OBSOLETE_duration // how long did the operation take? microseconds +} + +enum AnnotationType { BOOL, BYTES, I16, I32, I64, DOUBLE, STRING } + +/** + * Binary annotations are tags applied to a Span to give it context. For + * example, a binary annotation of "http.uri" could the path to a resource in a + * RPC call. + * + * Binary annotations of type STRING are always queryable, though more a + * historical implementation detail than a structural concern. + * + * Binary annotations can repeat, and vary on the host. Similar to Annotation, + * the host indicates who logged the event. This allows you to tell the + * difference between the client and server side of the same key. For example, + * the key "http.uri" might be different on the client and server side due to + * rewriting, like "/api/v1/myresource" vs "/myresource. Via the host field, + * you can see the different points of view, which often help in debugging. + */ +struct BinaryAnnotation { + 1: string key, + 2: binary value, + 3: AnnotationType annotation_type, + /** + * The host that recorded tag, which allows you to differentiate between + * multiple tags with the same key. There are two exceptions to this. + * + * When the key is CLIENT_ADDR or SERVER_ADDR, host indicates the source or + * destination of an RPC. This exception allows zipkin to display network + * context of uninstrumented services, or clients such as web browsers. + */ + 4: optional Endpoint host +} + +/** + * A trace is a series of spans (often RPC calls) which form a latency tree. + * + * The root span is where trace_id = id and parent_id = Nil. The root span is + * usually the longest interval in the trace, starting with a SERVER_RECV + * annotation and ending with a SERVER_SEND. + */ +struct Span { + 1: i64 trace_id # unique trace id, use for all spans in trace + /** + * Span name in lowercase, rpc method for example + * + * Conventionally, when the span name isn't known, name = "unknown". + */ + 3: string name, + 4: i64 id, # unique span id, only used for this span + 5: optional i64 parent_id, # parent span id + 6: list annotations, # all annotations/events that occured, sorted by timestamp + 8: list binary_annotations # any binary annotations + 9: optional bool debug = 0 # if true, we DEMAND that this span passes all samplers + /** + * Microseconds from epoch of the creation of this span. + * + * This value should be set directly by instrumentation, using the most + * precise value possible. For example, gettimeofday or syncing nanoTime + * against a tick of currentTimeMillis. + * + * For compatibilty with instrumentation that precede this field, collectors + * or span stores can derive this via Annotation.timestamp. + * For example, SERVER_RECV.timestamp or CLIENT_SEND.timestamp. + * + * This field is optional for compatibility with old data: first-party span + * stores are expected to support this at time of introduction. + */ + 10: optional i64 timestamp, + /** + * Measurement of duration in microseconds, used to support queries. + * + * This value should be set directly, where possible. Doing so encourages + * precise measurement decoupled from problems of clocks, such as skew or NTP + * updates causing time to move backwards. + * + * For compatibilty with instrumentation that precede this field, collectors + * or span stores can derive this by subtracting Annotation.timestamp. + * For example, SERVER_SEND.timestamp - SERVER_RECV.timestamp. + * + * If this field is persisted as unset, zipkin will continue to work, except + * duration query support will be implementation-specific. Similarly, setting + * this field non-atomically is implementation-specific. + * + * This field is i64 vs i32 to support spans longer than 35 minutes. + */ + 11: optional i64 duration + /** + * Optional unique 8-byte additional identifier for a trace. If non zero, this + * means the trace uses 128 bit traceIds instead of 64 bit. + */ + 12: optional i64 trace_id_high +} + +# define TChannel service + +struct Response { + 1: required bool ok +} + +service ZipkinCollector { + list submitZipkinBatch(1: list spans) +} diff --git a/pyproject.toml b/pyproject.toml index a8f43fefdf..eff7e2e3ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,21 @@ [tool.black] line-length = 79 +exclude = ''' +( + /( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen # generated files + )/ + | foo.py # also separately exclude a file named foo.py in + # the root of the project +) +''' diff --git a/tox.ini b/tox.ini index f8d2f55d28..0db2364f19 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests} - pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger} + pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger} lint py37-{mypy,mypyinstalled} docs @@ -23,6 +23,7 @@ changedir = test-api: opentelemetry-api/tests test-sdk: opentelemetry-sdk/tests test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests + test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-example-app: examples/opentelemetry-example-app/tests @@ -38,6 +39,8 @@ commands_pre = ext: pip install {toxinidir}/opentelemetry-api wsgi: pip install {toxinidir}/ext/opentelemetry-ext-wsgi http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests + jaeger: pip install {toxinidir}/opentelemetry-sdk + jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger ; Using file:// here because otherwise tox invokes just "pip install ; opentelemetry-api", leading to an error @@ -68,6 +71,7 @@ commands_pre = pip install -e {toxinidir}/opentelemetry-sdk pip install -e {toxinidir}/ext/opentelemetry-ext-azure-monitor pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests + pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi pip install -e {toxinidir}/examples/opentelemetry-example-app @@ -84,6 +88,8 @@ commands = ext/opentelemetry-ext-azure-monitor/tests/ \ ext/opentelemetry-ext-http-requests/src/ \ ext/opentelemetry-ext-http-requests/tests/ \ + ext/opentelemetry-ext-jaeger/src/opentelemetry \ + ext/opentelemetry-ext-jaeger/tests/ \ ext/opentelemetry-ext-wsgi/tests/ \ examples/opentelemetry-example-app/src/opentelemetry_example_app/ \ examples/opentelemetry-example-app/tests/ From b0da53d28b041338e04982f3a9715926887b5cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 9 Oct 2019 10:43:41 +0200 Subject: [PATCH 0102/1517] Fix some "errors" found by mypy. (#204) Fix some errors found by mypy (split from #201). --- .../src/opentelemetry/context/__init__.py | 7 +-- .../src/opentelemetry/trace/__init__.py | 2 +- .../sdk/context/propagation/b3_format.py | 21 +++++---- .../src/opentelemetry/sdk/metrics/__init__.py | 7 +-- .../src/opentelemetry/sdk/trace/__init__.py | 47 ++++++++++--------- .../sdk/trace/export/__init__.py | 13 +++-- .../src/opentelemetry/sdk/util.py | 6 +-- 7 files changed, 55 insertions(+), 48 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index cf6c72dd8d..43a7722f88 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -138,19 +138,14 @@ async def main(): asyncio.run(main()) """ -import typing - from .base_context import BaseRuntimeContext __all__ = ["Context"] - -Context = None # type: typing.Optional[BaseRuntimeContext] - try: from .async_context import AsyncRuntimeContext - Context = AsyncRuntimeContext() + Context = AsyncRuntimeContext() # type: BaseRuntimeContext except ImportError: from .thread_local_context import ThreadLocalRuntimeContext diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 094255aa1e..18eced4504 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -142,7 +142,7 @@ class SpanKind(enum.Enum): class Span: """A span represents a single operation within a trace.""" - def start(self, start_time: int = None) -> None: + def start(self, start_time: typing.Optional[int] = None) -> None: """Sets the current time as the span's start time. Each span represents a single operation. The span's start time is the diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py index 9e8f7d3f19..2eca8afaa1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -35,7 +35,7 @@ class B3Format(HTTPTextFormat): def extract(cls, get_from_carrier, carrier): trace_id = format_trace_id(trace.INVALID_TRACE_ID) span_id = format_span_id(trace.INVALID_SPAN_ID) - sampled = 0 + sampled = "0" flags = None single_header = _extract_first_element( @@ -95,8 +95,8 @@ def extract(cls, get_from_carrier, carrier): # trace an span ids are encoded in hex, so must be converted trace_id=int(trace_id, 16), span_id=int(span_id, 16), - trace_options=options, - trace_state={}, + trace_options=trace.TraceOptions(options), + trace_state=trace.TraceState(), ) @classmethod @@ -111,17 +111,20 @@ def inject(cls, context, set_in_carrier, carrier): set_in_carrier(carrier, cls.SAMPLED_KEY, "1" if sampled else "0") -def format_trace_id(trace_id: int): +def format_trace_id(trace_id: int) -> str: """Format the trace id according to b3 specification.""" return format(trace_id, "032x") -def format_span_id(span_id: int): +def format_span_id(span_id: int) -> str: """Format the span id according to b3 specification.""" return format(span_id, "016x") -def _extract_first_element(list_object: list) -> typing.Optional[object]: - if list_object: - return list_object[0] - return None +_T = typing.TypeVar("_T") + + +def _extract_first_element(items: typing.Iterable[_T]) -> typing.Optional[_T]: + if items is None: + return None + return next(iter(items), None) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index f80a72c770..041d0e5dcd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -27,12 +27,12 @@ def __init__( enabled: bool, monotonic: bool, ): - self.data = 0 + self.data = value_type() self.value_type = value_type self.enabled = enabled self.monotonic = monotonic - def _validate_update(self, value: metrics_api.ValueT): + def _validate_update(self, value: metrics_api.ValueT) -> bool: if not self.enabled: return False if not isinstance(value, self.value_type): @@ -232,7 +232,8 @@ def create_metric( monotonic: bool = False, ) -> metrics_api.MetricT: """See `opentelemetry.metrics.Meter.create_metric`.""" - return metric_type( + # Ignore type b/c of mypy bug in addition to missing annotations + return metric_type( # type: ignore name, description, unit, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 1cee1933e2..eb754fadb8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -73,10 +73,10 @@ class MultiSpanProcessor(SpanProcessor): def __init__(self): # use a tuple to avoid race conditions when adding a new span and # iterating through it on "on_start" and "on_end". - self._span_processors = () + self._span_processors = () # type: typing.Tuple[SpanProcessor, ...] self._lock = threading.Lock() - def add_span_processor(self, span_processor: SpanProcessor): + def add_span_processor(self, span_processor: SpanProcessor) -> None: """Adds a SpanProcessor to the list handled by this instance.""" with self._lock: self._span_processors = self._span_processors + (span_processor,) @@ -122,11 +122,11 @@ class Span(trace_api.Span): def __init__( self, name: str, - context: "trace_api.SpanContext", + context: trace_api.SpanContext, parent: trace_api.ParentSpan = None, - sampler=None, # TODO - trace_config=None, # TODO - resource=None, # TODO + sampler: None = None, # TODO + trace_config: None = None, # TODO + resource: None = None, # TODO attributes: types.Attributes = None, # TODO events: typing.Sequence[trace_api.Event] = None, # TODO links: typing.Sequence[trace_api.Link] = None, # TODO @@ -140,9 +140,6 @@ def __init__( self.sampler = sampler self.trace_config = trace_config self.resource = resource - self.attributes = attributes - self.events = events - self.links = links self.kind = kind self.span_processor = span_processor @@ -165,8 +162,8 @@ def __init__( else: self.links = BoundedList.from_seq(MAX_NUM_LINKS, links) - self.end_time = None - self.start_time = None + self.end_time = None # type: typing.Optional[int] + self.start_time = None # type: typing.Optional[int] def __repr__(self): return '{}(name="{}", context={})'.format( @@ -203,9 +200,13 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: def add_event( self, name: str, attributes: types.Attributes = None ) -> None: - if attributes is None: - attributes = Span.empty_attributes - self.add_lazy_event(trace_api.Event(name, time_ns(), attributes)) + self.add_lazy_event( + trace_api.Event( + name, + time_ns(), + Span.empty_attributes if attributes is None else attributes, + ) + ) def add_lazy_event(self, event: trace_api.Event) -> None: with self._lock: @@ -226,7 +227,9 @@ def add_link( attributes: types.Attributes = None, ) -> None: if attributes is None: - attributes = Span.empty_attributes + attributes = ( + Span.empty_attributes + ) # TODO: empty_attributes is not a Dict. Use Mapping? self.add_lazy_link(trace_api.Link(link_target_context, attributes)) def add_lazy_link(self, link: "trace_api.Link") -> None: @@ -242,7 +245,7 @@ def add_lazy_link(self, link: "trace_api.Link") -> None: return self.links.append(link) - def start(self, start_time: int = None): + def start(self, start_time: typing.Optional[int] = None) -> None: with self._lock: if not self.is_recording_events(): return @@ -256,7 +259,7 @@ def start(self, start_time: int = None): return self.span_processor.on_start(self) - def end(self, end_time: int = None): + def end(self, end_time: int = None) -> None: with self._lock: if not self.is_recording_events(): return @@ -283,7 +286,7 @@ def is_recording_events(self) -> bool: return True -def generate_span_id(): +def generate_span_id() -> int: """Get a new random span ID. Returns: @@ -292,7 +295,7 @@ def generate_span_id(): return random.getrandbits(64) -def generate_trace_id(): +def generate_trace_id() -> int: """Get a new random trace ID. Returns: @@ -325,7 +328,7 @@ def start_span( name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, - ) -> typing.Iterator["Span"]: + ) -> typing.Iterator[trace_api.Span]: """See `opentelemetry.trace.Tracer.start_span`.""" span = self.create_span(name, parent, kind) @@ -368,8 +371,8 @@ def create_span( @contextmanager def use_span( - self, span: Span, end_on_exit: bool = False - ) -> typing.Iterator[Span]: + self, span: trace_api.Span, end_on_exit: bool = False + ) -> typing.Iterator[trace_api.Span]: """See `opentelemetry.trace.Tracer.use_span`.""" try: span_snapshot = self._current_span_slot.get() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index ce362813ec..a76a658b3a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -118,7 +118,9 @@ def __init__( ) self.span_exporter = span_exporter - self.queue = collections.deque([], max_queue_size) + self.queue = collections.deque( + [], max_queue_size + ) # type: typing.Deque[Span] self.worker_thread = threading.Thread(target=self.worker, daemon=True) self.condition = threading.Condition(threading.Lock()) self.schedule_delay_millis = schedule_delay_millis @@ -128,7 +130,9 @@ def __init__( # flag that indicates that spans are being dropped self._spans_dropped = False # precallocated list to send spans to exporter - self.spans_list = [None] * self.max_export_batch_size + self.spans_list = [ + None + ] * self.max_export_batch_size # type: typing.List[typing.Optional[Span]] self.worker_thread.start() def on_start(self, span: Span) -> None: @@ -172,7 +176,7 @@ def worker(self): # be sure that all spans are sent self._flush() - def export(self) -> bool: + def export(self) -> None: """Exports at most max_export_batch_size spans.""" idx = 0 @@ -184,7 +188,8 @@ def export(self) -> bool: suppress_instrumentation = Context.suppress_instrumentation try: Context.suppress_instrumentation = True - self.span_exporter.export(self.spans_list[:idx]) + # Ignore type b/c the Optional[None]+slicing is too "clever" for mypy + self.span_exporter.export(self.spans_list[:idx]) # type: ignore # pylint: disable=broad-except except Exception: logger.exception("Exception while exporting data.") diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util.py b/opentelemetry-sdk/src/opentelemetry/sdk/util.py index da6ada90c3..2265c29460 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util.py @@ -41,7 +41,7 @@ class BoundedList(Sequence): def __init__(self, maxlen): self.dropped = 0 - self._dq = deque(maxlen=maxlen) + self._dq = deque(maxlen=maxlen) # type: deque self._lock = threading.Lock() def __repr__(self): @@ -97,8 +97,8 @@ def __init__(self, maxlen): raise ValueError self.maxlen = maxlen self.dropped = 0 - self._dict = OrderedDict() - self._lock = threading.Lock() + self._dict = OrderedDict() # type: OrderedDict + self._lock = threading.Lock() # type: threading.Lock def __repr__(self): return "{}({}, maxlen={})".format( From 3c513e30e1ea8fd3f45fbfb77fec692e004d164c Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 14 Oct 2019 14:05:35 -0700 Subject: [PATCH 0103/1517] Update README for new milestones (#218) --- README.md | 49 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index eac0a23727..6edad08a2b 100644 --- a/README.md +++ b/README.md @@ -82,24 +82,43 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) OpenTelemetry Python is under active development. The library is not yet _generally available_, and releases aren't guaranteed to -conform to a specific version of the specification. Future releases will not -attempt to maintain backwards compatibility with current releases. +conform to a specific version of the specification. Future releases will not +attempt to maintain backwards compatibility with previous releases. -The _alpha_ release includes: +The [v0.1 alpha +release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.1.0) +includes: - Tracing API - Tracing SDK - Metrics API -- Metrics SDK -- W3C Context Propagation +- Metrics SDK (Partial) +- W3C Trace Context Propagation - B3 Context Propagation - -Future release targets include: - -| Component | Version | Target Date | -| --------------------------- | ------- | --------------- | -| Zipkin Trace Exporter | Beta | October 14 2019 | -| Jaeger Trace Exporter | Beta | October 14 2019 | -| Prometheus Metrics Exporter | Beta | October 14 2019 | -| OpenTracing Bridge | Beta | October 14 2019 | -| OpenCensus Bridge | Beta | October 14 2019 | +- HTTP Integrations + +See the [project +milestones](https://github.com/open-telemetry/opentelemetry-python/milestones) +for details on upcoming releases. The dates and features described here are +estimates, and subject to change. + +Future releases targets include: + +| Component | Version | Target Date | +| --------------------------- | ---------- | --------------- | +| Jaeger Trace Exporter | Alpha v0.2 | October 28 2019 | +| Metrics SDK (Complete) | Alpha v0.2 | October 28 2019 | +| Prometheus Metrics Exporter | Alpha v0.2 | October 28 2019 | +| OpenTracing Bridge | Alpha v0.2 | October 28 2019 | + +| Component | Version | Target Date | +| ----------------------------------- | ---------- | ---------------- | +| Zipkin Trace Exporter | Alpha v0.3 | November 15 2019 | +| W3C Correlation Context Propagation | Alpha v0.3 | November 15 2019 | +| Support for Tags/Baggage | Alpha v0.3 | November 15 2019 | +| Metrics Aggregation | Alpha v0.3 | November 15 2019 | +| gRPC Integrations | Alpha v0.3 | November 15 2019 | + +| Component | Version | Target Date | +| ----------------- | ---------- | ---------------- | +| OpenCensus Bridge | Alpha v0.4 | December 31 2019 | From 63f559ec257da7868a58699cbac598f3c57bd889 Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Tue, 15 Oct 2019 09:49:40 +0200 Subject: [PATCH 0104/1517] Refactor current span handling for newly created spans. (#198) 1. Make Tracer.start_span() simply create and start the Span, without setting it as the current instance. 2. Add an extra Tracer.start_as_current_span() to create the Span and set it as the current instance automatically. Co-Authored-By: Chris Kleinknecht --- .../src/opentelemetry/trace/__init__.py | 89 ++++++++++--- .../src/opentelemetry/sdk/trace/__init__.py | 17 ++- opentelemetry-sdk/tests/trace/test_trace.py | 119 +++++++++++++----- 3 files changed, 171 insertions(+), 54 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 18eced4504..1b0a3e758f 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -26,16 +26,16 @@ to use the API package alone without a supporting implementation. The tracer supports creating spans that are "attached" or "detached" from the -context. By default, new spans are "attached" to the context in that they are +context. New spans are "attached" to the context in that they are created as children of the currently active span, and the newly-created span -becomes the new active span:: +can optionally become the new active span:: from opentelemetry.trace import tracer # Create a new root span, set it as the current span in context - with tracer.start_span("parent"): + with tracer.start_as_current_span("parent"): # Attach a new child and update the current span - with tracer.start_span("child"): + with tracer.start_as_current_span("child"): do_work(): # Close child span, set parent as current # Close parent span, set default span as current @@ -62,6 +62,7 @@ """ import enum +import types as python_types import typing from contextlib import contextmanager @@ -226,6 +227,26 @@ def is_recording_events(self) -> bool: events with the add_event operation and attributes using set_attribute. """ + def __enter__(self) -> "Span": + """Invoked when `Span` is used as a context manager. + + Returns the `Span` itself. + """ + return self + + def __exit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc_val: typing.Optional[BaseException], + exc_tb: typing.Optional[python_types.TracebackType], + ) -> typing.Optional[bool]: + """Ends context manager and calls `end` on the `Span`. + + Returns False. + """ + self.end() + return False + class TraceOptions(int): """A bitmask that represents options specific to the trace. @@ -376,30 +397,64 @@ def get_current_span(self) -> "Span": # pylint: disable=no-self-use return INVALID_SPAN - @contextmanager # type: ignore def start_span( self, name: str, parent: ParentSpan = CURRENT_SPAN, kind: SpanKind = SpanKind.INTERNAL, - ) -> typing.Iterator["Span"]: - """Context manager for span creation. + ) -> "Span": + """Starts a span. - Create a new span. Start the span and set it as the current span in - this tracer's context. + Create a new span. Start the span without setting it as the current + span in this tracer's context. By default the current span will be used as parent, but an explicit parent can also be specified, either a `Span` or a `SpanContext`. If the specified value is `None`, the created span will be a root span. - On exiting the context manager stop the span and set its parent as the + The span can be used as context manager. On exiting, the span will be + ended. + + Example:: + + # tracer.get_current_span() will be used as the implicit parent. + # If none is found, the created span will be a root instance. + with tracer.start_span("one") as child: + child.add_event("child's event") + + Applications that need to set the newly created span as the current + instance should use :meth:`start_as_current_span` instead. + + Args: + name: The name of the span to be created. + parent: The span's parent. Defaults to the current span. + kind: The span's kind (relationship to parent). Note that is + meaningful even if there is no parent. + + Returns: + The newly-created span. + """ + # pylint: disable=unused-argument,no-self-use + return INVALID_SPAN + + @contextmanager # type: ignore + def start_as_current_span( + self, + name: str, + parent: ParentSpan = CURRENT_SPAN, + kind: SpanKind = SpanKind.INTERNAL, + ) -> typing.Iterator["Span"]: + """Context manager for creating a new span and set it + as the current span in this tracer's context. + + On exiting the context manager stops the span and set its parent as the current span. Example:: - with tracer.start_span("one") as parent: + with tracer.start_as_current_span("one") as parent: parent.add_event("parent's event") - with tracer.start_span("two") as child: + with tracer.start_as_current_span("two") as child: child.add_event("child's event") tracer.get_current_span() # returns child tracer.get_current_span() # returns parent @@ -407,15 +462,14 @@ def start_span( This is a convenience method for creating spans attached to the tracer's context. Applications that need more control over the span - lifetime should use :meth:`create_span` instead. For example:: + lifetime should use :meth:`start_span` instead. For example:: - with tracer.start_span(name) as span: + with tracer.start_as_current_span(name) as span: do_work() is equivalent to:: - span = tracer.create_span(name) - span.start() + span = tracer.start_span(name) with tracer.use_span(span, end_on_exit=True): do_work() @@ -428,6 +482,7 @@ def start_span( Yields: The newly-created span. """ + # pylint: disable=unused-argument,no-self-use yield INVALID_SPAN @@ -451,7 +506,7 @@ def create_span( Applications that need to create spans detached from the tracer's context should use this method. - with tracer.start_span(name) as span: + with tracer.start_as_current_span(name) as span: do_work() This is equivalent to:: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index eb754fadb8..c7b33a353d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -322,19 +322,28 @@ def get_current_span(self): """See `opentelemetry.trace.Tracer.get_current_span`.""" return self._current_span_slot.get() - @contextmanager def start_span( self, name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, - ) -> typing.Iterator[trace_api.Span]: + ) -> "Span": """See `opentelemetry.trace.Tracer.start_span`.""" span = self.create_span(name, parent, kind) span.start() - with self.use_span(span, end_on_exit=True): - yield span + return span + + def start_as_current_span( + self, + name: str, + parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, + kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, + ) -> typing.Iterator[trace_api.Span]: + """See `opentelemetry.trace.Tracer.start_as_current_span`.""" + + span = self.start_span(name, parent, kind) + return self.use_span(span, end_on_exit=True) def create_span( self, diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index dc593a9d62..b55ea897fa 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -32,17 +32,17 @@ def test_start_span_implicit(self): self.assertIsNone(tracer.get_current_span()) - with tracer.start_span("root") as root: - self.assertIs(tracer.get_current_span(), root) + root = tracer.start_span("root") + self.assertIsNotNone(root.start_time) + self.assertIsNone(root.end_time) + self.assertEqual(root.kind, trace_api.SpanKind.INTERNAL) - self.assertIsNotNone(root.start_time) - self.assertIsNone(root.end_time) - self.assertEqual(root.kind, trace_api.SpanKind.INTERNAL) + with tracer.use_span(root, True): + self.assertIs(tracer.get_current_span(), root) with tracer.start_span( "child", kind=trace_api.SpanKind.CLIENT ) as child: - self.assertIs(tracer.get_current_span(), child) self.assertIs(child.parent, root) self.assertEqual(child.kind, trace_api.SpanKind.CLIENT) @@ -64,9 +64,9 @@ def test_start_span_implicit(self): root_context.trace_options, child_context.trace_options ) - # After exiting the child's scope the parent should become the - # current span again. - self.assertIs(tracer.get_current_span(), root) + # Verify start_span() did not set the current span. + self.assertIs(tracer.get_current_span(), root) + self.assertIsNotNone(child.end_time) self.assertIsNone(tracer.get_current_span()) @@ -82,26 +82,25 @@ def test_start_span_explicit(self): self.assertIsNone(tracer.get_current_span()) + root = tracer.start_span("root") + self.assertIsNotNone(root.start_time) + self.assertIsNone(root.end_time) + # Test with the implicit root span - with tracer.start_span("root") as root: + with tracer.use_span(root, True): self.assertIs(tracer.get_current_span(), root) - self.assertIsNotNone(root.start_time) - self.assertIsNone(root.end_time) - with tracer.start_span("stepchild", other_parent) as child: - # The child should become the current span as usual, but its - # parent should be the one passed in, not the - # previously-current span. - self.assertIs(tracer.get_current_span(), child) + # The child's parent should be the one passed in, + # not the current span. self.assertNotEqual(child.parent, root) self.assertIs(child.parent, other_parent) self.assertIsNotNone(child.start_time) self.assertIsNone(child.end_time) - # The child should inherit its context fromr the explicit - # parent, not the previously-current span. + # The child should inherit its context from the explicit + # parent, not the current span. child_context = child.get_context() self.assertEqual(other_parent.trace_id, child_context.trace_id) self.assertNotEqual( @@ -114,6 +113,60 @@ def test_start_span_explicit(self): other_parent.trace_options, child_context.trace_options ) + # Verify start_span() did not set the current span. + self.assertIs(tracer.get_current_span(), root) + + # Verify ending the child did not set the current span. + self.assertIs(tracer.get_current_span(), root) + self.assertIsNotNone(child.end_time) + + def test_start_as_current_span_implicit(self): + tracer = trace.Tracer("test_start_as_current_span_implicit") + + self.assertIsNone(tracer.get_current_span()) + + with tracer.start_as_current_span("root") as root: + self.assertIs(tracer.get_current_span(), root) + + with tracer.start_as_current_span("child") as child: + self.assertIs(tracer.get_current_span(), child) + self.assertIs(child.parent, root) + + # After exiting the child's scope the parent should become the + # current span again. + self.assertIs(tracer.get_current_span(), root) + self.assertIsNotNone(child.end_time) + + self.assertIsNone(tracer.get_current_span()) + self.assertIsNotNone(root.end_time) + + def test_start_as_current_span_explicit(self): + tracer = trace.Tracer("test_start_as_current_span_explicit") + + other_parent = trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + ) + + self.assertIsNone(tracer.get_current_span()) + + # Test with the implicit root span + with tracer.start_as_current_span("root") as root: + self.assertIs(tracer.get_current_span(), root) + + self.assertIsNotNone(root.start_time) + self.assertIsNone(root.end_time) + + with tracer.start_as_current_span( + "stepchild", other_parent + ) as child: + # The child should become the current span as usual, but its + # parent should be the one passed in, not the + # previously-current span. + self.assertIs(tracer.get_current_span(), child) + self.assertNotEqual(child.parent, root) + self.assertIs(child.parent, other_parent) + # After exiting the child's scope the last span on the stack should # become current, not the child's parent. self.assertNotEqual(tracer.get_current_span(), other_parent) @@ -144,7 +197,7 @@ def test_span_members(self): self.assertIsNone(tracer.get_current_span()) - with tracer.start_span("root") as root: + with tracer.start_as_current_span("root") as root: # attributes root.set_attribute("component", "http") root.set_attribute("http.method", "GET") @@ -254,7 +307,7 @@ def test_ended_span(self): span_id=trace.generate_span_id(), ) - with tracer.start_span("root") as root: + with tracer.start_as_current_span("root") as root: # everything should be empty at the beginning self.assertEqual(len(root.attributes), 0) self.assertEqual(len(root.events), 0) @@ -313,9 +366,9 @@ def test_span_processor(self): sp1 = MySpanProcessor("SP1", spans_calls_list) sp2 = MySpanProcessor("SP2", spans_calls_list) - with tracer.start_span("foo"): - with tracer.start_span("bar"): - with tracer.start_span("baz"): + with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): pass # at this point lists must be empty @@ -324,13 +377,13 @@ def test_span_processor(self): # add single span processor tracer.add_span_processor(sp1) - with tracer.start_span("foo"): + with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) - with tracer.start_span("bar"): + with tracer.start_as_current_span("bar"): expected_list.append(span_event_start_fmt("SP1", "bar")) - with tracer.start_span("baz"): + with tracer.start_as_current_span("baz"): expected_list.append(span_event_start_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP1", "baz")) @@ -347,15 +400,15 @@ def test_span_processor(self): # go for multiple span processors tracer.add_span_processor(sp2) - with tracer.start_span("foo"): + with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) expected_list.append(span_event_start_fmt("SP2", "foo")) - with tracer.start_span("bar"): + with tracer.start_as_current_span("bar"): expected_list.append(span_event_start_fmt("SP1", "bar")) expected_list.append(span_event_start_fmt("SP2", "bar")) - with tracer.start_span("baz"): + with tracer.start_as_current_span("baz"): expected_list.append(span_event_start_fmt("SP1", "baz")) expected_list.append(span_event_start_fmt("SP2", "baz")) @@ -380,9 +433,9 @@ def test_add_span_processor_after_span_creation(self): # Span processors are created but not added to the tracer yet sp = MySpanProcessor("SP1", spans_calls_list) - with tracer.start_span("foo"): - with tracer.start_span("bar"): - with tracer.start_span("baz"): + with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): # add span processor after spans have been created tracer.add_span_processor(sp) From 7f574557d95fcd7bf27b8f5ee5fb116c3e9ce460 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Wed, 16 Oct 2019 16:19:46 -0700 Subject: [PATCH 0105/1517] Add set_status to Span (#213) --- docs/opentelemetry.trace.rst | 12 +- docs/opentelemetry.trace.status.rst | 7 + .../src/opentelemetry/trace/__init__.py | 6 + .../src/opentelemetry/trace/status.py | 185 ++++++++++++++++++ .../src/opentelemetry/sdk/trace/__init__.py | 9 + opentelemetry-sdk/tests/trace/test_trace.py | 31 +++ 6 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 docs/opentelemetry.trace.status.rst create mode 100644 opentelemetry-api/src/opentelemetry/trace/status.py diff --git a/docs/opentelemetry.trace.rst b/docs/opentelemetry.trace.rst index cec44bd817..a57b5dcbff 100644 --- a/docs/opentelemetry.trace.rst +++ b/docs/opentelemetry.trace.rst @@ -1,4 +1,14 @@ opentelemetry.trace package =========================== -.. automodule:: opentelemetry.trace +Submodules +---------- + +.. toctree:: + + opentelemetry.trace.status + +Module contents +--------------- + +.. automodule:: opentelemetry.trace \ No newline at end of file diff --git a/docs/opentelemetry.trace.status.rst b/docs/opentelemetry.trace.status.rst new file mode 100644 index 0000000000..0205446c80 --- /dev/null +++ b/docs/opentelemetry.trace.status.rst @@ -0,0 +1,7 @@ +opentelemetry.trace.status +========================== + +.. automodule:: opentelemetry.trace.status + :members: + :undoc-members: + :show-inheritance: diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 1b0a3e758f..fac9f55da7 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -66,6 +66,7 @@ import typing from contextlib import contextmanager +from opentelemetry.trace.status import Status from opentelemetry.util import loader, types # TODO: quarantine @@ -227,6 +228,11 @@ def is_recording_events(self) -> bool: events with the add_event operation and attributes using set_attribute. """ + def set_status(self, status: Status) -> None: + """Sets the Status of the Span. If used, this will override the default + Span status, which is OK. + """ + def __enter__(self) -> "Span": """Invoked when `Span` is used as a context manager. diff --git a/opentelemetry-api/src/opentelemetry/trace/status.py b/opentelemetry-api/src/opentelemetry/trace/status.py new file mode 100644 index 0000000000..4fc50b33e5 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/trace/status.py @@ -0,0 +1,185 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import enum +import typing + + +class StatusCanonicalCode(enum.Enum): + """Represents the canonical set of status codes of a finished Span.""" + + OK = 0 + """Not an error, returned on success.""" + + CANCELLED = 1 + """The operation was cancelled, typically by the caller.""" + + UNKNOWN = 2 + """Unknown error. + + For example, this error may be returned when a Status value received from + another address space belongs to an error space that is not known in this + address space. Also errors raised by APIs that do not return enough error + information may be converted to this error. + """ + + INVALID_ARGUMENT = 3 + """The client specified an invalid argument. + + Note that this differs from FAILED_PRECONDITION. INVALID_ARGUMENT indicates + arguments that are problematic regardless of the state of the system (e.g., + a malformed file name). + """ + + DEADLINE_EXCEEDED = 4 + """The deadline expired before the operation could complete. + + For operations that change the state of the system, this error may be + returned even if the operation has completed successfully. For example, a + successful response from a server could have been delayed long + """ + + NOT_FOUND = 5 + """Some requested entity (e.g., file or directory) was not found. + + Note to server developers: if a request is denied for an entire class of + users, such as gradual feature rollout or undocumented whitelist, NOT_FOUND + may be used. If a request is denied for some users within a class of users, + such as user-based access control, PERMISSION_DENIED must be used. + """ + + ALREADY_EXISTS = 6 + """The entity that a client attempted to create (e.g., file or directory) + already exists. + """ + + PERMISSION_DENIED = 7 + """The caller does not have permission to execute the specified operation. + + PERMISSION_DENIED must not be used for rejections caused by exhausting some + resource (use RESOURCE_EXHAUSTED instead for those errors). + PERMISSION_DENIED must not be used if the caller can not be identified (use + UNAUTHENTICATED instead for those errors). This error code does not imply + the request is valid or the requested entity exists or satisfies other + pre-conditions. + """ + + RESOURCE_EXHAUSTED = 8 + """Some resource has been exhausted, perhaps a per-user quota, or perhaps + the entire file system is out of space. + """ + + FAILED_PRECONDITION = 9 + """The operation was rejected because the system is not in a state required + for the operation's execution. + + For example, the directory to be deleted is non-empty, an rmdir operation + is applied to a non-directory, etc. Service implementors can use the + following guidelines to decide between FAILED_PRECONDITION, ABORTED, and + UNAVAILABLE: + + (a) Use UNAVAILABLE if the client can retry just the failing call. + (b) Use ABORTED if the client should retry at a higher level (e.g., + when a client-specified test-and-set fails, indicating the client + should restart a read-modify-write sequence). + (c) Use FAILED_PRECONDITION if the client should not retry until the + system state has been explicitly fixed. + + E.g., if an "rmdir" fails because the directory is non-empty, + FAILED_PRECONDITION should be returned since the client should not retry + unless the files are deleted from the directory. + """ + + ABORTED = 10 + """The operation was aborted, typically due to a concurrency issue such as a + sequencer check failure or transaction abort. + + See the guidelines above for deciding between FAILED_PRECONDITION, ABORTED, + and UNAVAILABLE. + """ + + OUT_OF_RANGE = 11 + """The operation was attempted past the valid range. + + E.g., seeking or reading past end-of-file. Unlike INVALID_ARGUMENT, this + error indicates a problem that may be fixed if the system state changes. + For example, a 32-bit file system will generate INVALID_ARGUMENT if asked + to read at an offset that is not in the range [0,2^32-1],but it will + generate OUT_OF_RANGE if asked to read from an offset past the current file + size. There is a fair bit of overlap between FAILED_PRECONDITION and + OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific error) + when it applies so that callers who are iterating through a space can + easily look for an OUT_OF_RANGE error to detect when they are done. + """ + + UNIMPLEMENTED = 12 + """The operation is not implemented or is not supported/enabled in this + service. + """ + + INTERNAL = 13 + """Internal errors. + + This means that some invariants expected by the underlying system have been + broken. This error code is reserved for serious errors. + """ + + UNAVAILABLE = 14 + """The service is currently unavailable. + + This is most likely a transient condition, which can be corrected by + retrying with a backoff. Note that it is not always safe to retry + non-idempotent operations. + """ + + DATA_LOSS = 15 + """Unrecoverable data loss or corruption.""" + + UNAUTHENTICATED = 16 + """The request does not have valid authentication credentials for the + operation. + """ + + +class Status: + """Represents the status of a finished Span. + + Args: + canonical_code: The canonical status code that describes the result + status of the operation. + description: An optional description of the status. + """ + + def __init__( + self, + canonical_code: "StatusCanonicalCode" = StatusCanonicalCode.OK, + description: typing.Optional[str] = None, + ): + self._canonical_code = canonical_code + self._description = description + + @property + def canonical_code(self) -> "StatusCanonicalCode": + """Represents the canonical status code of a finished Span.""" + return self._canonical_code + + @property + def description(self) -> typing.Optional[str]: + """Status description""" + return self._description + + @property + def is_ok(self) -> bool: + """Returns false if this represents an error, true otherwise.""" + return self._canonical_code is StatusCanonicalCode.OK diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index c7b33a353d..7b274f852f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -143,6 +143,7 @@ def __init__( self.kind = kind self.span_processor = span_processor + self.status = trace_api.Status() self._lock = threading.Lock() if attributes is None: @@ -285,6 +286,14 @@ def update_name(self, name: str) -> None: def is_recording_events(self) -> bool: return True + def set_status(self, status: trace_api.Status) -> None: + with self._lock: + has_ended = self.end_time is not None + if has_ended: + logger.warning("Calling set_status() on an ended span.") + return + self.status = status + def generate_span_id() -> int: """Get a new random span ID. diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index b55ea897fa..fa8547a0a5 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -288,6 +288,24 @@ def test_start_span(self): span.start() self.assertEqual(start_time, span.start_time) + # default status + self.assertTrue(span.status.is_ok) + self.assertIs( + span.status.canonical_code, trace_api.status.StatusCanonicalCode.OK + ) + self.assertIs(span.status.description, None) + + # status + new_status = trace_api.status.Status( + trace_api.status.StatusCanonicalCode.CANCELLED, "Test description" + ) + span.set_status(new_status) + self.assertIs( + span.status.canonical_code, + trace_api.status.StatusCanonicalCode.CANCELLED, + ) + self.assertIs(span.status.description, "Test description") + def test_span_override_start_and_end_time(self): """Span sending custom start_time and end_time values""" span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) @@ -334,6 +352,19 @@ def test_ended_span(self): root.update_name("xxx") self.assertEqual(root.name, "root") + new_status = trace_api.status.Status( + trace_api.status.StatusCanonicalCode.CANCELLED, + "Test description", + ) + root.set_status(new_status) + # default status + self.assertTrue(root.status.is_ok) + self.assertEqual( + root.status.canonical_code, + trace_api.status.StatusCanonicalCode.OK, + ) + self.assertIs(root.status.description, None) + def span_event_start_fmt(span_processor_name, span_name): return span_processor_name + ":" + span_name + ":start" From 5c63be7c4bc81848c3da937632879322c63a0913 Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Thu, 17 Oct 2019 08:09:31 -0700 Subject: [PATCH 0106/1517] Add contextmanager for Context (#215) --- .../src/opentelemetry/context/base_context.py | 10 ++++++ .../sdk/trace/export/__init__.py | 35 +++++++++---------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/base_context.py b/opentelemetry-api/src/opentelemetry/context/base_context.py index f1e37aa91f..99d6869dd5 100644 --- a/opentelemetry-api/src/opentelemetry/context/base_context.py +++ b/opentelemetry-api/src/opentelemetry/context/base_context.py @@ -14,6 +14,7 @@ import threading import typing +from contextlib import contextmanager def wrap_callable(target: "object") -> typing.Callable[[], object]: @@ -99,6 +100,15 @@ def __getitem__(self, name: str) -> "object": def __setitem__(self, name: str, value: "object") -> None: self.__setattr__(name, value) + @contextmanager # type: ignore + def use(self, **kwargs: typing.Dict[str, object]) -> typing.Iterator[None]: + snapshot = {key: self[key] for key in kwargs} + for key in kwargs: + self[key] = kwargs[key] + yield + for key in kwargs: + self[key] = snapshot[key] + def with_current_context( self, func: typing.Callable[..., "object"] ) -> typing.Callable[..., "object"]: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index a76a658b3a..ecdc93b0ad 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -73,15 +73,12 @@ def on_start(self, span: Span) -> None: pass def on_end(self, span: Span) -> None: - suppress_instrumentation = Context.suppress_instrumentation - try: - Context.suppress_instrumentation = True - self.span_exporter.export((span,)) - # pylint: disable=broad-except - except Exception as exc: - logger.warning("Exception while exporting data: %s", exc) - finally: - Context.suppress_instrumentation = suppress_instrumentation + with Context.use(suppress_instrumentation=True): + try: + self.span_exporter.export((span,)) + # pylint: disable=broad-except + except Exception as exc: + logger.warning("Exception while exporting data: %s", exc) def shutdown(self) -> None: self.span_exporter.shutdown() @@ -185,16 +182,16 @@ def export(self) -> None: while idx < self.max_export_batch_size and self.queue: self.spans_list[idx] = self.queue.pop() idx += 1 - suppress_instrumentation = Context.suppress_instrumentation - try: - Context.suppress_instrumentation = True - # Ignore type b/c the Optional[None]+slicing is too "clever" for mypy - self.span_exporter.export(self.spans_list[:idx]) # type: ignore - # pylint: disable=broad-except - except Exception: - logger.exception("Exception while exporting data.") - finally: - Context.suppress_instrumentation = suppress_instrumentation + with Context.use(suppress_instrumentation=True): + try: + # Ignore type b/c the Optional[None]+slicing is too "clever" + # for mypy + self.span_exporter.export( + self.spans_list[:idx] + ) # type: ignore + # pylint: disable=broad-except + except Exception: + logger.exception("Exception while exporting data.") # clean up list for index in range(idx): From 3fa3a83c9b12c4b9a93496e00454756dc8c938bf Mon Sep 17 00:00:00 2001 From: Johannes Liebermann Date: Mon, 21 Oct 2019 17:27:32 +0200 Subject: [PATCH 0107/1517] Fix mypy errors (#229) In particular, the following errors are fixed in this commit: * Don't return False in __exit__ Returning a literal causes a mypy error when combined with the `typing.Optional[bool]` type hint. Furthermore, exception handling is the same when returning `False` and when returning `None` (the exception is re-raised). Therefore, it's simpler to remove the return statement and change the type hint to `None`. * Correctly initialize nested tuple Tuples of length 1 should be initialized with a trailing comma to be properly interpreted. * Pass correct type to use_context() in test * Add type annotations for test helper functions Since we have `disallow_untyped_calls = True` in our mypy config for tests, we must add type annotations to any function that is called from a test. Addditionally, bump minimal mypy version to 0.740 to consistently reproduce these errors. --- opentelemetry-api/src/opentelemetry/trace/__init__.py | 8 ++------ .../distributedcontext/test_distributed_context.py | 10 +++++++++- opentelemetry-api/tests/metrics/test_metrics.py | 2 +- opentelemetry-api/tests/test_loader.py | 5 +++-- tox.ini | 2 +- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index fac9f55da7..fff5d556b9 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -245,13 +245,9 @@ def __exit__( exc_type: typing.Optional[typing.Type[BaseException]], exc_val: typing.Optional[BaseException], exc_tb: typing.Optional[python_types.TracebackType], - ) -> typing.Optional[bool]: - """Ends context manager and calls `end` on the `Span`. - - Returns False. - """ + ) -> None: + """Ends context manager and calls `end` on the `Span`.""" self.end() - return False class TraceOptions(int): diff --git a/opentelemetry-api/tests/distributedcontext/test_distributed_context.py b/opentelemetry-api/tests/distributedcontext/test_distributed_context.py index 67a6004839..c730603b16 100644 --- a/opentelemetry-api/tests/distributedcontext/test_distributed_context.py +++ b/opentelemetry-api/tests/distributedcontext/test_distributed_context.py @@ -99,6 +99,14 @@ def test_get_current_context(self): self.assertIsNone(self.manager.get_current_context()) def test_use_context(self): - expected = object() + expected = distributedcontext.DistributedContext( + ( + distributedcontext.Entry( + distributedcontext.EntryMetadata(0), + distributedcontext.EntryKey("0"), + distributedcontext.EntryValue(""), + ), + ) + ) with self.manager.use_context(expected) as output: self.assertIs(output, expected) diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 14667f62ea..97ac92fcde 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -24,7 +24,7 @@ def setUp(self): def test_record_batch(self): counter = metrics.Counter() - self.meter.record_batch(("values"), ((counter, 1))) + self.meter.record_batch(("values"), ((counter, 1),)) def test_create_metric(self): metric = self.meter.create_metric("", "", "", float, metrics.Counter) diff --git a/opentelemetry-api/tests/test_loader.py b/opentelemetry-api/tests/test_loader.py index 942479ab7d..970b615963 100644 --- a/opentelemetry-api/tests/test_loader.py +++ b/opentelemetry-api/tests/test_loader.py @@ -16,6 +16,7 @@ import sys import unittest from importlib import reload +from typing import Any, Callable from opentelemetry import trace from opentelemetry.util import loader @@ -59,7 +60,7 @@ def test_preferred_impl(self): # NOTE: We use do_* + *_ methods because subtest wouldn't run setUp, # which we require here. - def do_test_preferred_impl(self, setter): + def do_test_preferred_impl(self, setter: Callable[[Any], Any]) -> None: setter(get_opentelemetry_implementation) tracer = trace.tracer() self.assertIs(tracer, DUMMY_TRACER) @@ -81,7 +82,7 @@ def test_try_set_again(self): ) self.assertIn("already loaded", str(einfo.exception)) - def do_test_get_envvar(self, envvar_suffix): + def do_test_get_envvar(self, envvar_suffix: str) -> None: global DUMMY_TRACER # pylint:disable=global-statement # Test is not runnable with this! diff --git a/tox.ini b/tox.ini index 0db2364f19..19816d3d9c 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ python = [testenv] deps = - mypy,mypyinstalled: mypy~=0.711 + mypy,mypyinstalled: mypy~=0.740 setenv = mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ From 1fd8659f3c3261070b24729743676f7de5066510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 22 Oct 2019 19:39:53 -0500 Subject: [PATCH 0108/1517] Fix bad variable type in jaeger-ext (#230) --- .../src/opentelemetry/ext/jaeger/__init__.py | 11 ++- .../tests/test_jaeger_exporter.py | 88 ++++++++++++++++--- 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py index b824c1a51b..e9d5c5884d 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -123,6 +123,11 @@ def shutdown(self): pass +def _nsec_to_usec_round(nsec): + """Round nanoseconds to microseconds""" + return (nsec + 500) // 10 ** 3 + + def _translate_to_jaeger(spans: Span): """Translate the spans to Jaeger format. @@ -137,8 +142,8 @@ def _translate_to_jaeger(spans: Span): trace_id = ctx.trace_id span_id = ctx.span_id - start_time_us = span.start_time // 1e3 - duration_us = (span.end_time - span.start_time) // 1e3 + start_time_us = _nsec_to_usec_round(span.start_time) + duration_us = _nsec_to_usec_round(span.end_time - span.start_time) parent_id = 0 if isinstance(span.parent, trace_api.Span): @@ -227,7 +232,7 @@ def _extract_logs_from_span(span): ) ) - event_timestamp_us = event.timestamp // 1e3 + event_timestamp_us = _nsec_to_usec_round(event.timestamp) logs.append( jaeger.Log(timestamp=int(event_timestamp_us), fields=fields) ) diff --git a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py index 3fb14cd354..9c7e4b044c 100644 --- a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py +++ b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py @@ -14,6 +14,7 @@ # limitations under the License. import unittest +from unittest import mock # pylint:disable=no-name-in-module # pylint:disable=import-error @@ -24,7 +25,19 @@ class TestJaegerSpanExporter(unittest.TestCase): + def setUp(self): + # create and save span to be used in tests + context = trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + ) + + self._test_span = trace.Span("test_span", context=context) + self._test_span.start() + self._test_span.end() + def test_constructor_default(self): + """Test the default values assigned by constructor.""" service_name = "my-service-name" host_name = "localhost" thrift_port = None @@ -44,13 +57,14 @@ def test_constructor_default(self): self.assertTrue(exporter.agent_client is not None) def test_constructor_explicit(self): + """Test the constructor passing all the options.""" service = "my-opentelemetry-jaeger" collector_host_name = "opentelemetry.io" collector_port = 15875 collector_endpoint = "/myapi/traces?format=jaeger.thrift" agent_port = 14268 - agent_host_name = "opentelemetry.com" + agent_host_name = "opentelemetry.io" username = "username" password = "password" @@ -84,6 +98,14 @@ def test_constructor_explicit(self): self.assertNotEqual(exporter.collector, collector) self.assertTrue(exporter.collector.auth is None) + def test_nsec_to_usec_round(self): + # pylint: disable=protected-access + nsec_to_usec_round = jaeger_exporter._nsec_to_usec_round + + self.assertEqual(nsec_to_usec_round(5000), 5) + self.assertEqual(nsec_to_usec_round(5499), 5) + self.assertEqual(nsec_to_usec_round(5500), 6) + # pylint: disable=too-many-locals def test_translate_to_jaeger(self): # pylint: disable=invalid-name @@ -97,9 +119,13 @@ def test_translate_to_jaeger(self): parent_id = 0x1111111111111111 other_id = 0x2222222222222222 - base_time = 683647322 * 1e9 # in ns - start_times = (base_time, base_time + 150 * 1e6, base_time + 300 * 1e6) - durations = (50 * 1e6, 100 * 1e6, 200 * 1e6) + base_time = 683647322 * 10 ** 9 # in ns + start_times = ( + base_time, + base_time + 150 * 10 ** 6, + base_time + 300 * 10 ** 6, + ) + durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6) end_times = ( start_times[0] + durations[0], start_times[1] + durations[1], @@ -116,7 +142,7 @@ def test_translate_to_jaeger(self): "key_float": 0.3, } - event_timestamp = base_time + 50e6 + event_timestamp = base_time + 50 * 10 ** 6 event = trace_api.Event( name="event0", timestamp=event_timestamp, @@ -166,8 +192,8 @@ def test_translate_to_jaeger(self): traceIdLow=trace_id_low, spanId=span_id, parentSpanId=parent_id, - startTime=start_times[0] / 1e3, - duration=durations[0] / 1e3, + startTime=start_times[0] // 10 ** 3, + duration=durations[0] // 10 ** 3, flags=0, tags=[ jaeger.Tag( @@ -194,7 +220,7 @@ def test_translate_to_jaeger(self): ], logs=[ jaeger.Log( - timestamp=event_timestamp / 1e3, + timestamp=event_timestamp // 10 ** 3, fields=[ jaeger.Tag( key="annotation_bool", @@ -226,8 +252,8 @@ def test_translate_to_jaeger(self): traceIdLow=trace_id_low, spanId=parent_id, parentSpanId=0, - startTime=int(start_times[1] // 1e3), - duration=int(durations[1] // 1e3), + startTime=start_times[1] // 10 ** 3, + duration=durations[1] // 10 ** 3, flags=0, ), jaeger.Span( @@ -236,14 +262,14 @@ def test_translate_to_jaeger(self): traceIdLow=trace_id_low, spanId=other_id, parentSpanId=0, - startTime=int(start_times[2] // 1e3), - duration=int(durations[2] // 1e3), + startTime=start_times[2] // 10 ** 3, + duration=durations[2] // 10 ** 3, flags=0, ), ] # events are complicated to compare because order of fields - # (attributes) is otel is not important but in jeager it is + # (attributes) in otel is not important but in jeager it is self.assertCountEqual( spans[0].logs[0].fields, expected_spans[0].logs[0].fields ) @@ -252,3 +278,39 @@ def test_translate_to_jaeger(self): expected_spans[0].logs[0].fields = None self.assertEqual(spans, expected_spans) + + def test_export(self): + """Test that agent and/or collector are invoked""" + exporter = jaeger_exporter.JaegerSpanExporter( + "test_export", agent_host_name="localhost", agent_port=6318 + ) + + # just agent is configured now + agent_client_mock = mock.Mock(spec=jaeger_exporter.AgentClientUDP) + # pylint: disable=protected-access + exporter._agent_client = agent_client_mock + + exporter.export((self._test_span,)) + self.assertEqual(agent_client_mock.emit.call_count, 1) + + # add also a collector and test that both are called + collector_mock = mock.Mock(spec=jaeger_exporter.Collector) + # pylint: disable=protected-access + exporter._collector = collector_mock + + exporter.export((self._test_span,)) + self.assertEqual(agent_client_mock.emit.call_count, 2) + self.assertEqual(collector_mock.submit.call_count, 1) + + def test_agent_client(self): + agent_client = jaeger_exporter.AgentClientUDP( + host_name="localhost", port=6354 + ) + + batch = jaeger.Batch( + # pylint: disable=protected-access + spans=jaeger_exporter._translate_to_jaeger((self._test_span,)), + process=jaeger.Process(serviceName="xxx"), + ) + + agent_client.emit(batch) From e4d89490e8705c7a0d5ea331db35c63d4cb2de44 Mon Sep 17 00:00:00 2001 From: Johannes Liebermann Date: Thu, 24 Oct 2019 16:46:23 +0200 Subject: [PATCH 0109/1517] OpenTracing Bridge - Initial implementation (#211) Initial implementation, without baggage support. --- .../README.rst | 19 + .../setup.cfg | 46 ++ .../setup.py | 26 ++ .../ext/opentracing_shim/__init__.py | 259 +++++++++++ .../ext/opentracing_shim/util.py | 54 +++ .../ext/opentracing_shim/version.py | 15 + .../tests/__init__.py | 0 .../tests/test_shim.py | 433 ++++++++++++++++++ .../tests/test_util.py | 66 +++ .../src/opentelemetry/trace/__init__.py | 10 +- .../src/opentelemetry/sdk/trace/__init__.py | 7 +- opentelemetry-sdk/tests/trace/test_trace.py | 5 +- tox.ini | 9 +- 13 files changed, 941 insertions(+), 8 deletions(-) create mode 100644 ext/opentelemetry-ext-opentracing-shim/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/setup.cfg create mode 100644 ext/opentelemetry-ext-opentracing-shim/setup.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/test_util.py diff --git a/ext/opentelemetry-ext-opentracing-shim/README.rst b/ext/opentelemetry-ext-opentracing-shim/README.rst new file mode 100644 index 0000000000..2e81391219 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/README.rst @@ -0,0 +1,19 @@ +OpenTracing Shim for OpenTelemetry +============================================================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-opentracing-shim.svg + :target: https://pypi.org/project/opentelemetry-opentracing-shim/ + +Installation +------------ + +:: + + pip install opentelemetry-opentracing-shim + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg new file mode 100644 index 0000000000..c3b750f80f --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -0,0 +1,46 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-opentracing-shim +description = OpenTracing Shim for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-opentracing-shim +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentracing + opentelemetry-api + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.py b/ext/opentelemetry-ext-opentracing-shim/setup.py new file mode 100644 index 0000000000..bbec88b500 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/setup.py @@ -0,0 +1,26 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "opentracing_shim", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py new file mode 100644 index 0000000000..1a6479fc58 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -0,0 +1,259 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +import opentracing + +from opentelemetry.ext.opentracing_shim import util + +logger = logging.getLogger(__name__) + + +def create_tracer(otel_tracer): + return TracerShim(otel_tracer) + + +class SpanContextShim(opentracing.SpanContext): + def __init__(self, otel_context): + self._otel_context = otel_context + + def unwrap(self): + """Returns the wrapped OpenTelemetry `SpanContext` object.""" + + return self._otel_context + + @property + def baggage(self): + logger.warning( + "Using unimplemented property baggage on class %s.", + self.__class__.__name__, + ) + # TODO: Implement. + + +class SpanShim(opentracing.Span): + def __init__(self, tracer, context, span): + super().__init__(tracer, context) + self._otel_span = span + + def unwrap(self): + """Returns the wrapped OpenTelemetry `Span` object.""" + + return self._otel_span + + def set_operation_name(self, operation_name): + self._otel_span.update_name(operation_name) + return self + + def finish(self, finish_time=None): + end_time = finish_time + if end_time is not None: + end_time = util.time_seconds_to_ns(finish_time) + self._otel_span.end(end_time=end_time) + + def set_tag(self, key, value): + self._otel_span.set_attribute(key, value) + return self + + def log_kv(self, key_values, timestamp=None): + if timestamp is not None: + event_timestamp = util.time_seconds_to_ns(timestamp) + else: + event_timestamp = None + + event_name = util.event_name_from_kv(key_values) + self._otel_span.add_event(event_name, event_timestamp, key_values) + return self + + def set_baggage_item(self, key, value): + logger.warning( + "Calling unimplemented method set_baggage_item() on class %s", + self.__class__.__name__, + ) + # TODO: Implement. + + def get_baggage_item(self, key): + logger.warning( + "Calling unimplemented method get_baggage_item() on class %s", + self.__class__.__name__, + ) + # TODO: Implement. + + # TODO: Verify calls to deprecated methods `log_event()` and `log()` on + # base class work properly (it's probably fine because both methods call + # `log_kv()`). + + +class ScopeShim(opentracing.Scope): + """A `ScopeShim` wraps the OpenTelemetry functionality related to span + activation/deactivation while using OpenTracing `Scope` objects for + presentation. + + There are two ways to construct a `ScopeShim` object: using the default + initializer and using the `from_context_manager()` class method. + + It is necessary to have both ways for constructing `ScopeShim` objects + because in some cases we need to create the object from a context manager, + in which case our only way of retrieving a `Span` object is by calling the + `__enter__()` method on the context manager, which makes the span active in + the OpenTelemetry tracer; whereas in other cases we need to accept a + `SpanShim` object and wrap it in a `ScopeShim`. + """ + + def __init__(self, manager, span, span_cm=None): + super().__init__(manager, span) + self._span_cm = span_cm + + # TODO: Change type of `manager` argument to `opentracing.ScopeManager`? We + # need to get rid of `manager.tracer` for this. + @classmethod + def from_context_manager(cls, manager, span_cm): + """Constructs a `ScopeShim` from an OpenTelemetry `Span` context + manager (as returned by `Tracer.use_span()`). + """ + + otel_span = span_cm.__enter__() + span_context = SpanContextShim(otel_span.get_context()) + span = SpanShim(manager.tracer, span_context, otel_span) + return cls(manager, span, span_cm) + + def close(self): + if self._span_cm is not None: + # We don't have error information to pass to `__exit__()` so we + # pass `None` in all arguments. If the OpenTelemetry tracer + # implementation requires this information, the `__exit__()` method + # on `opentracing.Scope` should be overridden and modified to pass + # the relevant values to this `close()` method. + self._span_cm.__exit__(None, None, None) + else: + self._span.unwrap().end() + + +class ScopeManagerShim(opentracing.ScopeManager): + def __init__(self, tracer): + # The only thing the `__init__()` method on the base class does is + # initialize `self._noop_span` and `self._noop_scope` with no-op + # objects. Therefore, it doesn't seem useful to call it. + # pylint: disable=super-init-not-called + self._tracer = tracer + + def activate(self, span, finish_on_close): + span_cm = self._tracer.unwrap().use_span( + span.unwrap(), end_on_exit=finish_on_close + ) + return ScopeShim.from_context_manager(self, span_cm=span_cm) + + @property + def active(self): + span = self._tracer.unwrap().get_current_span() + if span is None: + return None + + span_context = SpanContextShim(span.get_context()) + wrapped_span = SpanShim(self._tracer, span_context, span) + return ScopeShim(self, span=wrapped_span) + # TODO: The returned `ScopeShim` instance here always ends the + # corresponding span, regardless of the `finish_on_close` value used + # when activating the span. This is because here we return a *new* + # `ScopeShim` rather than returning a saved instance of `ScopeShim`. + # https://github.com/open-telemetry/opentelemetry-python/pull/211/files#r335398792 + + @property + def tracer(self): + return self._tracer + + +class TracerShim(opentracing.Tracer): + def __init__(self, tracer): + super().__init__(scope_manager=ScopeManagerShim(self)) + self._otel_tracer = tracer + + def unwrap(self): + """Returns the wrapped OpenTelemetry `Tracer` object.""" + + return self._otel_tracer + + def start_active_span( + self, + operation_name, + child_of=None, + references=None, + tags=None, + start_time=None, + ignore_active_span=False, + finish_on_close=True, + ): + span = self.start_span( + operation_name=operation_name, + child_of=child_of, + references=references, + tags=tags, + start_time=start_time, + ignore_active_span=ignore_active_span, + ) + return self._scope_manager.activate(span, finish_on_close) + + def start_span( + self, + operation_name=None, + child_of=None, + references=None, + tags=None, + start_time=None, + ignore_active_span=False, + ): + # Use active span as parent when no explicit parent is specified. + if not ignore_active_span and not child_of: + child_of = self.active_span + + # Use the specified parent or the active span if possible. Otherwise, + # use a `None` parent, which triggers the creation of a new trace. + parent = child_of.unwrap() if child_of else None + span = self._otel_tracer.create_span(operation_name, parent) + + if references: + for ref in references: + span.add_link(ref.referenced_context.unwrap()) + + if tags: + for key, value in tags.items(): + span.set_attribute(key, value) + + # The OpenTracing API expects time values to be `float` values which + # represent the number of seconds since the epoch. OpenTelemetry + # represents time values as nanoseconds since the epoch. + start_time_ns = start_time + if start_time_ns is not None: + start_time_ns = util.time_seconds_to_ns(start_time) + + span.start(start_time=start_time_ns) + context = SpanContextShim(span.get_context()) + return SpanShim(self, context, span) + + def inject(self, span_context, format, carrier): + # pylint: disable=redefined-builtin + logger.warning( + "Calling unimplemented method inject() on class %s", + self.__class__.__name__, + ) + # TODO: Implement. + + def extract(self, format, carrier): + # pylint: disable=redefined-builtin + logger.warning( + "Calling unimplemented method extract() on class %s", + self.__class__.__name__, + ) + # TODO: Implement. diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py new file mode 100644 index 0000000000..97e2415e44 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py @@ -0,0 +1,54 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# A default event name to be used for logging events when a better event name +# can't be derived from the event's key-value pairs. +DEFAULT_EVENT_NAME = "log" + + +def time_seconds_to_ns(time_seconds): + """Converts a time value in seconds to a time value in nanoseconds. + + `time_seconds` is a `float` as returned by `time.time()` which represents + the number of seconds since the epoch. + + The returned value is an `int` representing the number of nanoseconds since + the epoch. + """ + + return int(time_seconds * 1e9) + + +def time_seconds_from_ns(time_nanoseconds): + """Converts a time value in nanoseconds to a time value in seconds. + + `time_nanoseconds` is an `int` representing the number of nanoseconds since + the epoch. + + The returned value is a `float` representing the number of seconds since + the epoch. + """ + + return time_nanoseconds / 1e9 + + +def event_name_from_kv(key_values): + """A helper function which returns an event name from the given dict, or a + default event name. + """ + + if key_values is None or "event" not in key_values: + return DEFAULT_EVENT_NAME + + return key_values["event"] diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py new file mode 100644 index 0000000000..a457c2b665 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.1.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py new file mode 100644 index 0000000000..b6691911dd --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -0,0 +1,433 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import unittest + +import opentracing + +import opentelemetry.ext.opentracing_shim as opentracingshim +from opentelemetry import trace +from opentelemetry.ext.opentracing_shim import util +from opentelemetry.sdk.trace import Tracer + + +class TestShim(unittest.TestCase): + # pylint: disable=too-many-public-methods + + def setUp(self): + """Create an OpenTelemetry tracer and a shim before every test case.""" + + self.tracer = trace.tracer() + self.shim = opentracingshim.create_tracer(self.tracer) + + @classmethod + def setUpClass(cls): + """Set preferred tracer implementation only once rather than before + every test method. + """ + + trace.set_preferred_tracer_implementation(lambda T: Tracer()) + + def test_shim_type(self): + # Verify shim is an OpenTracing tracer. + self.assertIsInstance(self.shim, opentracing.Tracer) + + def test_start_active_span(self): + """Test span creation and activation using `start_active_span()`.""" + + with self.shim.start_active_span("TestSpan") as scope: + # Verify correct type of Scope and Span objects. + self.assertIsInstance(scope, opentracing.Scope) + self.assertIsInstance(scope.span, opentracing.Span) + + # Verify span is started. + self.assertIsNotNone(scope.span.unwrap().start_time) + + # Verify span is active. + self.assertEqual( + self.shim.active_span.context.unwrap(), + scope.span.context.unwrap(), + ) + # TODO: We can't check for equality of self.shim.active_span and + # scope.span because the same OpenTelemetry span is returned inside + # different SpanShim objects. A possible solution is described + # here: + # https://github.com/open-telemetry/opentelemetry-python/issues/161#issuecomment-534136274 + + # Verify span has ended. + self.assertIsNotNone(scope.span.unwrap().end_time) + + # Verify no span is active. + self.assertIsNone(self.shim.active_span) + + def test_start_span(self): + """Test span creation using `start_span()`.""" + + with self.shim.start_span("TestSpan") as span: + # Verify correct type of Span object. + self.assertIsInstance(span, opentracing.Span) + + # Verify span is started. + self.assertIsNotNone(span.unwrap().start_time) + + # Verify `start_span()` does NOT make the span active. + self.assertIsNone(self.shim.active_span) + + # Verify span has ended. + self.assertIsNotNone(span.unwrap().end_time) + + def test_start_span_no_contextmanager(self): + """Test `start_span()` without a `with` statement.""" + + span = self.shim.start_span("TestSpan") + + # Verify span is started. + self.assertIsNotNone(span.unwrap().start_time) + + # Verify `start_span()` does NOT make the span active. + self.assertIsNone(self.shim.active_span) + + span.finish() + + def test_explicit_span_finish(self): + """Test `finish()` method on `Span` objects.""" + + span = self.shim.start_span("TestSpan") + + # Verify span hasn't ended. + self.assertIsNone(span.unwrap().end_time) + + span.finish() + + # Verify span has ended. + self.assertIsNotNone(span.unwrap().end_time) + + def test_explicit_start_time(self): + """Test `start_time` argument.""" + + now = time.time() + with self.shim.start_active_span("TestSpan", start_time=now) as scope: + result = util.time_seconds_from_ns(scope.span.unwrap().start_time) + # Tolerate inaccuracies of less than a microsecond. + # TODO: Put a link to an explanation in the docs. + # TODO: This seems to work consistently, but we should find out the + # biggest possible loss of precision. + self.assertAlmostEqual(result, now, places=6) + + def test_explicit_end_time(self): + """Test `end_time` argument of `finish()` method.""" + + span = self.shim.start_span("TestSpan") + now = time.time() + span.finish(now) + + end_time = util.time_seconds_from_ns(span.unwrap().end_time) + # Tolerate inaccuracies of less than a microsecond. + # TODO: Put a link to an explanation in the docs. + # TODO: This seems to work consistently, but we should find out the + # biggest possible loss of precision. + self.assertAlmostEqual(end_time, now, places=6) + + def test_explicit_span_activation(self): + """Test manual activation and deactivation of a span.""" + + span = self.shim.start_span("TestSpan") + + # Verify no span is currently active. + self.assertIsNone(self.shim.active_span) + + with self.shim.scope_manager.activate( + span, finish_on_close=True + ) as scope: + # Verify span is active. + self.assertEqual( + self.shim.active_span.context.unwrap(), + scope.span.context.unwrap(), + ) + + # Verify no span is active. + self.assertIsNone(self.shim.active_span) + + def test_start_active_span_finish_on_close(self): + """Test `finish_on_close` argument of `start_active_span()`.""" + + with self.shim.start_active_span( + "TestSpan", finish_on_close=True + ) as scope: + # Verify span hasn't ended. + self.assertIsNone(scope.span.unwrap().end_time) + + # Verify span has ended. + self.assertIsNotNone(scope.span.unwrap().end_time) + + with self.shim.start_active_span( + "TestSpan", finish_on_close=False + ) as scope: + # Verify span hasn't ended. + self.assertIsNone(scope.span.unwrap().end_time) + + # Verify span hasn't ended after scope had been closed. + self.assertIsNone(scope.span.unwrap().end_time) + + scope.span.finish() + + def test_activate_finish_on_close(self): + """Test `finish_on_close` argument of `activate()`.""" + + span = self.shim.start_span("TestSpan") + + with self.shim.scope_manager.activate( + span, finish_on_close=True + ) as scope: + # Verify span is active. + self.assertEqual( + self.shim.active_span.context.unwrap(), + scope.span.context.unwrap(), + ) + + # Verify span has ended. + self.assertIsNotNone(span.unwrap().end_time) + + span = self.shim.start_span("TestSpan") + + with self.shim.scope_manager.activate( + span, finish_on_close=False + ) as scope: + # Verify span is active. + self.assertEqual( + self.shim.active_span.context.unwrap(), + scope.span.context.unwrap(), + ) + + # Verify span hasn't ended. + self.assertIsNone(span.unwrap().end_time) + + span.finish() + + def test_explicit_scope_close(self): + """Test `close()` method on `ScopeShim`.""" + + with self.shim.start_active_span("ParentSpan") as parent: + # Verify parent span is active. + self.assertEqual( + self.shim.active_span.context.unwrap(), + parent.span.context.unwrap(), + ) + + child = self.shim.start_active_span("ChildSpan") + + # Verify child span is active. + self.assertEqual( + self.shim.active_span.context.unwrap(), + child.span.context.unwrap(), + ) + + # Verify child span hasn't ended. + self.assertIsNone(child.span.unwrap().end_time) + + child.close() + + # Verify child span has ended. + self.assertIsNotNone(child.span.unwrap().end_time) + + # Verify parent span becomes active again. + self.assertEqual( + self.shim.active_span.context.unwrap(), + parent.span.context.unwrap(), + ) + + def test_parent_child_implicit(self): + """Test parent-child relationship and activation/deactivation of spans + without specifying the parent span upon creation. + """ + + with self.shim.start_active_span("ParentSpan") as parent: + # Verify parent span is the active span. + self.assertEqual( + self.shim.active_span.context.unwrap(), + parent.span.context.unwrap(), + ) + + with self.shim.start_active_span("ChildSpan") as child: + # Verify child span is the active span. + self.assertEqual( + self.shim.active_span.context.unwrap(), + child.span.context.unwrap(), + ) + + # Verify parent-child relationship. + parent_trace_id = parent.span.unwrap().get_context().trace_id + child_trace_id = child.span.unwrap().get_context().trace_id + + self.assertEqual(parent_trace_id, child_trace_id) + self.assertEqual( + child.span.unwrap().parent, parent.span.unwrap() + ) + + # Verify parent span becomes the active span again. + self.assertEqual( + self.shim.active_span.context.unwrap(), + parent.span.context.unwrap() + # TODO: Check equality of the spans themselves rather than + # their context once the SpanShim reconstruction problem has + # been addressed (see previous TODO). + ) + + # Verify there is no active span. + self.assertIsNone(self.shim.active_span) + + def test_parent_child_explicit_span(self): + """Test parent-child relationship of spans when specifying a `Span` + object as a parent upon creation. + """ + + with self.shim.start_span("ParentSpan") as parent: + with self.shim.start_active_span( + "ChildSpan", child_of=parent + ) as child: + parent_trace_id = parent.unwrap().get_context().trace_id + child_trace_id = child.span.unwrap().get_context().trace_id + + self.assertEqual(child_trace_id, parent_trace_id) + self.assertEqual(child.span.unwrap().parent, parent.unwrap()) + + with self.shim.start_span("ParentSpan") as parent: + child = self.shim.start_span("ChildSpan", child_of=parent) + + parent_trace_id = parent.unwrap().get_context().trace_id + child_trace_id = child.unwrap().get_context().trace_id + + self.assertEqual(child_trace_id, parent_trace_id) + self.assertEqual(child.unwrap().parent, parent.unwrap()) + + child.finish() + + def test_parent_child_explicit_span_context(self): + """Test parent-child relationship of spans when specifying a + `SpanContext` object as a parent upon creation. + """ + + with self.shim.start_span("ParentSpan") as parent: + with self.shim.start_active_span( + "ChildSpan", child_of=parent.context + ) as child: + parent_trace_id = parent.unwrap().get_context().trace_id + child_trace_id = child.span.unwrap().get_context().trace_id + + self.assertEqual(child_trace_id, parent_trace_id) + self.assertEqual( + child.span.unwrap().parent, parent.context.unwrap() + ) + + with self.shim.start_span("ParentSpan") as parent: + with self.shim.start_span( + "SpanWithContextParent", child_of=parent.context + ) as child: + parent_trace_id = parent.unwrap().get_context().trace_id + child_trace_id = child.unwrap().get_context().trace_id + + self.assertEqual(child_trace_id, parent_trace_id) + self.assertEqual( + child.unwrap().parent, parent.context.unwrap() + ) + + def test_references(self): + """Test span creation using the `references` argument.""" + + with self.shim.start_span("ParentSpan") as parent: + ref = opentracing.child_of(parent.context) + + with self.shim.start_active_span( + "ChildSpan", references=[ref] + ) as child: + self.assertEqual( + child.span.unwrap().links[0].context, + parent.context.unwrap(), + ) + + def test_set_operation_name(self): + """Test `set_operation_name()` method.""" + + with self.shim.start_active_span("TestName") as scope: + self.assertEqual(scope.span.unwrap().name, "TestName") + + scope.span.set_operation_name("NewName") + self.assertEqual(scope.span.unwrap().name, "NewName") + + def test_tags(self): + """Test tags behavior using the `tags` argument and the `set_tags()` + method. + """ + + tags = {"foo": "bar"} + with self.shim.start_active_span("TestSetTag", tags=tags) as scope: + scope.span.set_tag("baz", "qux") + + self.assertEqual(scope.span.unwrap().attributes["foo"], "bar") + self.assertEqual(scope.span.unwrap().attributes["baz"], "qux") + + def test_span_tracer(self): + """Test the `tracer` property on `Span` objects.""" + + with self.shim.start_active_span("TestSpan") as scope: + self.assertEqual(scope.span.tracer, self.shim) + + def test_log_kv(self): + """Test the `log_kv()` method on `Span` objects.""" + + with self.shim.start_span("TestSpan") as span: + span.log_kv({"foo": "bar"}) + self.assertEqual(span.unwrap().events[0].attributes["foo"], "bar") + # Verify timestamp was generated automatically. + self.assertIsNotNone(span.unwrap().events[0].timestamp) + + # Test explicit timestamp. + now = time.time() + span.log_kv({"foo": "bar"}, now) + result = util.time_seconds_from_ns( + span.unwrap().events[1].timestamp + ) + self.assertEqual(span.unwrap().events[1].attributes["foo"], "bar") + # Tolerate inaccuracies of less than a microsecond. + # TODO: Put a link to an explanation in the docs. + # TODO: This seems to work consistently, but we should find out the + # biggest possible loss of precision. + self.assertAlmostEqual(result, now, places=6) + + def test_span_context(self): + """Test construction of `SpanContextShim` objects.""" + + otel_context = trace.SpanContext(1234, 5678) + context = opentracingshim.SpanContextShim(otel_context) + + self.assertIsInstance(context, opentracing.SpanContext) + self.assertEqual(context.unwrap().trace_id, 1234) + self.assertEqual(context.unwrap().span_id, 5678) + + def test_span_on_error(self): + """Verify error tag and logs are created on span when an exception is + raised. + """ + + # Raise an exception while a span is active. + with self.assertRaises(Exception): + with self.shim.start_active_span("TestName") as scope: + raise Exception + + # Verify exception details have been added to span. + self.assertEqual(scope.span.unwrap().attributes["error"], True) + self.assertEqual( + scope.span.unwrap().events[0].attributes["error.kind"], Exception + ) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_util.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_util.py new file mode 100644 index 0000000000..84bdc73a6a --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_util.py @@ -0,0 +1,66 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import unittest + +from opentelemetry.ext.opentracing_shim import util +from opentelemetry.util import time_ns + + +class TestUtil(unittest.TestCase): + def test_event_name_from_kv(self): + # Test basic behavior. + event_name = "send HTTP request" + res = util.event_name_from_kv({"event": event_name, "foo": "bar"}) + self.assertEqual(res, event_name) + + # Test None. + res = util.event_name_from_kv(None) + self.assertEqual(res, util.DEFAULT_EVENT_NAME) + + # Test empty dict. + res = util.event_name_from_kv({}) + self.assertEqual(res, util.DEFAULT_EVENT_NAME) + + # Test missing `event` field. + res = util.event_name_from_kv({"foo": "bar"}) + self.assertEqual(res, util.DEFAULT_EVENT_NAME) + + def test_time_seconds_to_ns(self): + time_seconds = time.time() + result = util.time_seconds_to_ns(time_seconds) + + self.assertEqual(result, int(time_seconds * 1e9)) + + def test_time_seconds_from_ns(self): + time_nanoseconds = time_ns() + result = util.time_seconds_from_ns(time_nanoseconds) + + self.assertEqual(result, time_nanoseconds / 1e9) + + def test_time_conversion_precision(self): + """Verify time conversion from seconds to nanoseconds and vice versa is + accurate enough. + """ + + time_seconds = 1570484241.9501917 + time_nanoseconds = util.time_seconds_to_ns(time_seconds) + result = util.time_seconds_from_ns(time_nanoseconds) + + # Tolerate inaccuracies of less than a microsecond. + # TODO: Put a link to an explanation in the docs. + # TODO: This seems to work consistently, but we should find out the + # biggest possible loss of precision. + self.assertAlmostEqual(result, time_seconds, places=6) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index fff5d556b9..2a21212fae 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -180,12 +180,16 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: """ def add_event( - self, name: str, attributes: types.Attributes = None + self, + name: str, + timestamp: int = None, + attributes: types.Attributes = None, ) -> None: """Adds an `Event`. - Adds a single `Event` with the name and, optionally, attributes passed - as arguments. + Adds a single `Event` with the name and, optionally, a timestamp and + attributes passed as arguments. Implementations should generate a + timestamp if the `timestamp` argument is omitted. """ def add_lazy_event(self, event: Event) -> None: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 7b274f852f..f8a058d87f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -199,12 +199,15 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: self.attributes[key] = value def add_event( - self, name: str, attributes: types.Attributes = None + self, + name: str, + timestamp: int = None, + attributes: types.Attributes = None, ) -> None: self.add_lazy_event( trace_api.Event( name, - time_ns(), + time_ns() if timestamp is None else timestamp, Span.empty_attributes if attributes is None else attributes, ) ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index fa8547a0a5..d1e3033d52 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -227,8 +227,10 @@ def test_span_members(self): # events root.add_event("event0") - root.add_event("event1", {"name": "birthday"}) now = time_ns() + root.add_event( + "event1", timestamp=now, attributes={"name": "birthday"} + ) root.add_lazy_event( trace_api.Event("event2", now, {"name": "hello"}) ) @@ -240,6 +242,7 @@ def test_span_members(self): self.assertEqual(root.events[1].name, "event1") self.assertEqual(root.events[1].attributes, {"name": "birthday"}) + self.assertEqual(root.events[1].timestamp, now) self.assertEqual(root.events[2].name, "event2") self.assertEqual(root.events[2].attributes, {"name": "hello"}) diff --git a/tox.ini b/tox.ini index 19816d3d9c..a6abe8e158 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger} - pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} + pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} lint py37-{mypy,mypyinstalled} docs @@ -26,6 +26,7 @@ changedir = test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-example-app: examples/opentelemetry-example-app/tests + test-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests commands_pre = ; Install without -e to test the actual installation @@ -41,6 +42,7 @@ commands_pre = http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests jaeger: pip install {toxinidir}/opentelemetry-sdk jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger + opentracing-shim: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-opentracing-shim ; Using file:// here because otherwise tox invokes just "pip install ; opentelemetry-api", leading to an error @@ -74,6 +76,7 @@ commands_pre = pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi pip install -e {toxinidir}/examples/opentelemetry-example-app + pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim commands = ; Prefer putting everything in one pylint command to profit from duplication @@ -90,6 +93,8 @@ commands = ext/opentelemetry-ext-http-requests/tests/ \ ext/opentelemetry-ext-jaeger/src/opentelemetry \ ext/opentelemetry-ext-jaeger/tests/ \ + ext/opentelemetry-ext-opentracing-shim/src/ \ + ext/opentelemetry-ext-opentracing-shim/tests/ \ ext/opentelemetry-ext-wsgi/tests/ \ examples/opentelemetry-example-app/src/opentelemetry_example_app/ \ examples/opentelemetry-example-app/tests/ From 5c898504bcf895e6add8a2dd2d73cb5fca61f395 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 24 Oct 2019 07:57:52 -0700 Subject: [PATCH 0110/1517] Add sampler API, use in SDK tracer (#225) --- .flake8 | 16 +- .pylintrc | 3 +- docs/conf.py | 2 +- .../src/opentelemetry/trace/__init__.py | 17 +- .../src/opentelemetry/trace/sampling.py | 125 ++++++++++ .../tests/trace/test_sampling.py | 223 ++++++++++++++++++ .../sdk/context/propagation/b3_format.py | 4 +- .../src/opentelemetry/sdk/trace/__init__.py | 84 +++++-- opentelemetry-sdk/tests/trace/test_trace.py | 24 ++ 9 files changed, 463 insertions(+), 35 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/trace/sampling.py create mode 100644 opentelemetry-api/tests/trace/test_sampling.py diff --git a/.flake8 b/.flake8 index a3411a1614..5abd0630ea 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,15 @@ [flake8] -ignore = E501,W503,E203 -exclude = .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/,ext/opentelemetry-ext-jaeger/build/* +ignore = + E501 # line too long, defer to black + F401 # unused import, defer to pylint + W503 # allow line breaks after binary ops, not after +exclude = + .bzr + .git + .hg + .svn + .tox + CVS + __pycache__ + ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/ + ext/opentelemetry-ext-jaeger/build/* diff --git a/.pylintrc b/.pylintrc index 782fc58700..1aa1e10d0b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -68,7 +68,8 @@ disable=missing-docstring, ungrouped-imports, # Leave this up to isort wrong-import-order, # Leave this up to isort bad-continuation, # Leave this up to black - line-too-long # Leave this up to black + line-too-long, # Leave this up to black + exec-used # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/docs/conf.py b/docs/conf.py index 694ba7f005..94d17cb3eb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,7 @@ # -- Project information ----------------------------------------------------- project = "OpenTelemetry" -copyright = "2019, OpenTelemetry Authors" +copyright = "2019, OpenTelemetry Authors" # pylint: disable=redefined-builtin author = "OpenTelemetry Authors" diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 2a21212fae..b64ce852b0 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -257,8 +257,8 @@ def __exit__( class TraceOptions(int): """A bitmask that represents options specific to the trace. - The only supported option is the "recorded" flag (``0x01``). If set, this - flag indicates that the trace may have been recorded upstream. + The only supported option is the "sampled" flag (``0x01``). If set, this + flag indicates that the trace may have been sampled upstream. See the `W3C Trace Context - Traceparent`_ spec for details. @@ -267,12 +267,16 @@ class TraceOptions(int): """ DEFAULT = 0x00 - RECORDED = 0x01 + SAMPLED = 0x01 @classmethod def get_default(cls) -> "TraceOptions": return cls(cls.DEFAULT) + @property + def sampled(self) -> bool: + return bool(self & TraceOptions.SAMPLED) + DEFAULT_TRACE_OPTIONS = TraceOptions.get_default() @@ -313,8 +317,8 @@ class SpanContext: Args: trace_id: The ID of the trace that this span belongs to. span_id: This span's ID. - options: Trace options to propagate. - state: Tracing-system-specific info to propagate. + trace_options: Trace options to propagate. + trace_state: Tracing-system-specific info to propagate. """ def __init__( @@ -367,6 +371,9 @@ def __init__(self, context: "SpanContext") -> None: def get_context(self) -> "SpanContext": return self._context + def is_recording_events(self) -> bool: + return False + INVALID_SPAN_ID = 0x0000000000000000 INVALID_TRACE_ID = 0x00000000000000000000000000000000 diff --git a/opentelemetry-api/src/opentelemetry/trace/sampling.py b/opentelemetry-api/src/opentelemetry/trace/sampling.py new file mode 100644 index 0000000000..f16e80495b --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/trace/sampling.py @@ -0,0 +1,125 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +from typing import Dict, Mapping, Optional, Sequence + +# pylint: disable=unused-import +from opentelemetry.trace import Link, SpanContext +from opentelemetry.util.types import AttributeValue + + +class Decision: + """A sampling decision as applied to a newly-created Span. + + Args: + sampled: Whether the `Span` should be sampled. + attributes: Attributes to add to the `Span`. + """ + + def __repr__(self) -> str: + return "{}({}, attributes={})".format( + type(self).__name__, str(self.sampled), str(self.attributes) + ) + + def __init__( + self, + sampled: bool = False, + attributes: Mapping[str, "AttributeValue"] = None, + ) -> None: + self.sampled = sampled # type: bool + if attributes is None: + self.attributes = {} # type: Dict[str, "AttributeValue"] + else: + self.attributes = dict(attributes) + + +class Sampler(abc.ABC): + @abc.abstractmethod + def should_sample( + self, + parent_context: Optional["SpanContext"], + trace_id: int, + span_id: int, + name: str, + links: Sequence["Link"] = (), + ) -> "Decision": + pass + + +class StaticSampler(Sampler): + """Sampler that always returns the same decision.""" + + def __init__(self, decision: "Decision"): + self._decision = decision + + def should_sample( + self, + parent_context: Optional["SpanContext"], + trace_id: int, + span_id: int, + name: str, + links: Sequence["Link"] = (), + ) -> "Decision": + return self._decision + + +class ProbabilitySampler(Sampler): + def __init__(self, rate: float): + self._rate = rate + self._bound = self.get_bound_for_rate(self._rate) + + # The sampler checks the last 8 bytes of the trace ID to decide whether to + # sample a given trace. + CHECK_BYTES = 0xFFFFFFFFFFFFFFFF + + @classmethod + def get_bound_for_rate(cls, rate: float) -> int: + return round(rate * (cls.CHECK_BYTES + 1)) + + @property + def rate(self) -> float: + return self._rate + + @rate.setter + def rate(self, new_rate: float) -> None: + self._rate = new_rate + self._bound = self.get_bound_for_rate(self._rate) + + @property + def bound(self) -> int: + return self._bound + + def should_sample( + self, + parent_context: Optional["SpanContext"], + trace_id: int, + span_id: int, + name: str, + links: Sequence["Link"] = (), + ) -> "Decision": + if parent_context is not None: + return Decision(parent_context.trace_options.sampled) + + return Decision(trace_id & self.CHECK_BYTES < self.bound) + + +# Samplers that ignore the parent sampling decision and never/always sample. +ALWAYS_OFF = StaticSampler(Decision(False)) +ALWAYS_ON = StaticSampler(Decision(True)) + +# Samplers that respect the parent sampling decision, but otherwise +# never/always sample. +DEFAULT_OFF = ProbabilitySampler(0.0) +DEFAULT_ON = ProbabilitySampler(1.0) diff --git a/opentelemetry-api/tests/trace/test_sampling.py b/opentelemetry-api/tests/trace/test_sampling.py new file mode 100644 index 0000000000..b456aa91f1 --- /dev/null +++ b/opentelemetry-api/tests/trace/test_sampling.py @@ -0,0 +1,223 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import trace +from opentelemetry.trace import sampling + +TO_DEFAULT = trace.TraceOptions(trace.TraceOptions.DEFAULT) +TO_SAMPLED = trace.TraceOptions(trace.TraceOptions.SAMPLED) + + +class TestSampler(unittest.TestCase): + def test_always_on(self): + no_record_always_on = sampling.ALWAYS_ON.should_sample( + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_DEFAULT + ), + 0xDEADBEF1, + 0xDEADBEF2, + "unsampled parent, sampling on", + ) + self.assertTrue(no_record_always_on.sampled) + self.assertEqual(no_record_always_on.attributes, {}) + + sampled_always_on = sampling.ALWAYS_ON.should_sample( + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_SAMPLED + ), + 0xDEADBEF1, + 0xDEADBEF2, + "sampled parent, sampling on", + ) + self.assertTrue(sampled_always_on.sampled) + self.assertEqual(sampled_always_on.attributes, {}) + + def test_always_off(self): + no_record_always_off = sampling.ALWAYS_OFF.should_sample( + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_DEFAULT + ), + 0xDEADBEF1, + 0xDEADBEF2, + "unsampled parent, sampling off", + ) + self.assertFalse(no_record_always_off.sampled) + self.assertEqual(no_record_always_off.attributes, {}) + + sampled_always_on = sampling.ALWAYS_OFF.should_sample( + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_SAMPLED + ), + 0xDEADBEF1, + 0xDEADBEF2, + "sampled parent, sampling off", + ) + self.assertFalse(sampled_always_on.sampled) + self.assertEqual(sampled_always_on.attributes, {}) + + def test_default_on(self): + no_record_default_on = sampling.DEFAULT_ON.should_sample( + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_DEFAULT + ), + 0xDEADBEF1, + 0xDEADBEF2, + "unsampled parent, sampling on", + ) + self.assertFalse(no_record_default_on.sampled) + self.assertEqual(no_record_default_on.attributes, {}) + + sampled_default_on = sampling.DEFAULT_ON.should_sample( + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_SAMPLED + ), + 0xDEADBEF1, + 0xDEADBEF2, + "sampled parent, sampling on", + ) + self.assertTrue(sampled_default_on.sampled) + self.assertEqual(sampled_default_on.attributes, {}) + + def test_default_off(self): + no_record_default_off = sampling.DEFAULT_OFF.should_sample( + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_DEFAULT + ), + 0xDEADBEF1, + 0xDEADBEF2, + "unsampled parent, sampling off", + ) + self.assertFalse(no_record_default_off.sampled) + self.assertEqual(no_record_default_off.attributes, {}) + + sampled_default_off = sampling.DEFAULT_OFF.should_sample( + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_SAMPLED + ), + 0xDEADBEF1, + 0xDEADBEF2, + "sampled parent, sampling off", + ) + self.assertTrue(sampled_default_off.sampled) + self.assertEqual(sampled_default_off.attributes, {}) + + def test_probability_sampler(self): + sampler = sampling.ProbabilitySampler(0.5) + + # Check that we sample based on the trace ID if the parent context is + # null + self.assertTrue( + sampler.should_sample( + None, 0x7FFFFFFFFFFFFFFF, 0xDEADBEEF, "span name" + ).sampled + ) + self.assertFalse( + sampler.should_sample( + None, 0x8000000000000000, 0xDEADBEEF, "span name" + ).sampled + ) + + # Check that the sampling decision matches the parent context if given, + # and that the sampler ignores the trace ID + self.assertFalse( + sampler.should_sample( + trace.SpanContext( + 0xDEADBEF0, 0xDEADBEF1, trace_options=TO_DEFAULT + ), + 0x8000000000000000, + 0xDEADBEEF, + "span name", + ).sampled + ) + self.assertTrue( + sampler.should_sample( + trace.SpanContext( + 0xDEADBEF0, 0xDEADBEF1, trace_options=TO_SAMPLED + ), + 0x8000000000000001, + 0xDEADBEEF, + "span name", + ).sampled + ) + + def test_probability_sampler_zero(self): + default_off = sampling.ProbabilitySampler(0.0) + self.assertFalse( + default_off.should_sample( + None, 0x0, 0xDEADBEEF, "span name" + ).sampled + ) + + def test_probability_sampler_one(self): + default_off = sampling.ProbabilitySampler(1.0) + self.assertTrue( + default_off.should_sample( + None, 0xFFFFFFFFFFFFFFFF, 0xDEADBEEF, "span name" + ).sampled + ) + + def test_probability_sampler_limits(self): + + # Sample one of every 2^64 (= 5e-20) traces. This is the lowest + # possible meaningful sampling rate, only traces with trace ID 0x0 + # should get sampled. + almost_always_off = sampling.ProbabilitySampler(2 ** -64) + self.assertTrue( + almost_always_off.should_sample( + None, 0x0, 0xDEADBEEF, "span name" + ).sampled + ) + self.assertFalse( + almost_always_off.should_sample( + None, 0x1, 0xDEADBEEF, "span name" + ).sampled + ) + self.assertEqual( + sampling.ProbabilitySampler.get_bound_for_rate(2 ** -64), 0x1 + ) + + # Sample every trace with (last 8 bytes of) trace ID less than + # 0xffffffffffffffff. In principle this is the highest possible + # sampling rate less than 1, but we can't actually express this rate as + # a float! + # + # In practice, the highest possible sampling rate is: + # + # round(sys.float_info.epsilon * 2 ** 64) + + almost_always_on = sampling.ProbabilitySampler(1 - 2 ** -64) + self.assertTrue( + almost_always_on.should_sample( + None, 0xFFFFFFFFFFFFFFFE, 0xDEADBEEF, "span name" + ).sampled + ) + + # These tests are logically consistent, but fail because of the float + # precision issue above. Changing the sampler to check fewer bytes of + # the trace ID will cause these to pass. + + # self.assertFalse( + # almost_always_on.should_sample( + # None, + # 0xffffffffffffffff, + # 0xdeadbeef, + # "span name", + # ).sampled + # ) + # self.assertEqual( + # sampling.ProbabilitySampler.get_bound_for_rate(1 - 2 ** -64)), + # 0xffffffffffffffff, + # ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py index 2eca8afaa1..7d59fddb9e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -90,7 +90,7 @@ def extract(cls, get_from_carrier, carrier): # the desire for some form of sampling, propagate if either # header is set to allow. if sampled in cls._SAMPLE_PROPAGATE_VALUES or flags == "1": - options |= trace.TraceOptions.RECORDED + options |= trace.TraceOptions.SAMPLED return trace.SpanContext( # trace an span ids are encoded in hex, so must be converted trace_id=int(trace_id, 16), @@ -101,7 +101,7 @@ def extract(cls, get_from_carrier, carrier): @classmethod def inject(cls, context, set_in_carrier, carrier): - sampled = (trace.TraceOptions.RECORDED & context.trace_options) != 0 + sampled = (trace.TraceOptions.SAMPLED & context.trace_options) != 0 set_in_carrier( carrier, cls.TRACE_ID_KEY, format_trace_id(context.trace_id) ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index f8a058d87f..46e9b83e35 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -16,13 +16,14 @@ import logging import random import threading -import typing from contextlib import contextmanager +from typing import Iterator, Optional, Sequence, Tuple from opentelemetry import trace as trace_api from opentelemetry.context import Context from opentelemetry.sdk import util from opentelemetry.sdk.util import BoundedDict, BoundedList +from opentelemetry.trace import sampling from opentelemetry.util import time_ns, types logger = logging.getLogger(__name__) @@ -73,7 +74,7 @@ class MultiSpanProcessor(SpanProcessor): def __init__(self): # use a tuple to avoid race conditions when adding a new span and # iterating through it on "on_start" and "on_end". - self._span_processors = () # type: typing.Tuple[SpanProcessor, ...] + self._span_processors = () # type: Tuple[SpanProcessor, ...] self._lock = threading.Lock() def add_span_processor(self, span_processor: SpanProcessor) -> None: @@ -104,7 +105,7 @@ class Span(trace_api.Span): context: The immutable span context parent: This span's parent, may be a `SpanContext` if the parent is remote, null if this is a root span - sampler: TODO + sampler: The sampler used to create this span trace_config: TODO resource: TODO attributes: The span's attributes to be exported @@ -124,12 +125,12 @@ def __init__( name: str, context: trace_api.SpanContext, parent: trace_api.ParentSpan = None, - sampler: None = None, # TODO + sampler: Optional[sampling.Sampler] = None, trace_config: None = None, # TODO resource: None = None, # TODO attributes: types.Attributes = None, # TODO - events: typing.Sequence[trace_api.Event] = None, # TODO - links: typing.Sequence[trace_api.Link] = None, # TODO + events: Sequence[trace_api.Event] = None, # TODO + links: Sequence[trace_api.Link] = None, # TODO kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, span_processor: SpanProcessor = SpanProcessor(), ) -> None: @@ -163,8 +164,8 @@ def __init__( else: self.links = BoundedList.from_seq(MAX_NUM_LINKS, links) - self.end_time = None # type: typing.Optional[int] - self.start_time = None # type: typing.Optional[int] + self.end_time = None # type: Optional[int] + self.start_time = None # type: Optional[int] def __repr__(self): return '{}(name="{}", context={})'.format( @@ -249,7 +250,7 @@ def add_lazy_link(self, link: "trace_api.Link") -> None: return self.links.append(link) - def start(self, start_time: typing.Optional[int] = None) -> None: + def start(self, start_time: Optional[int] = None) -> None: with self._lock: if not self.is_recording_events(): return @@ -323,12 +324,17 @@ class Tracer(trace_api.Tracer): name: The name of the tracer. """ - def __init__(self, name: str = "") -> None: + def __init__( + self, + name: str = "", + sampler: sampling.Sampler = trace_api.sampling.ALWAYS_ON, + ) -> None: slot_name = "current_span" if name: slot_name = "{}.current_span".format(name) self._current_span_slot = Context.register_slot(slot_name) self._active_span_processor = MultiSpanProcessor() + self.sampler = sampler def get_current_span(self): """See `opentelemetry.trace.Tracer.get_current_span`.""" @@ -351,7 +357,7 @@ def start_as_current_span( name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, - ) -> typing.Iterator[trace_api.Span]: + ) -> Iterator[trace_api.Span]: """See `opentelemetry.trace.Tracer.start_as_current_span`.""" span = self.start_span(name, parent, kind) @@ -362,38 +368,68 @@ def create_span( name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, - ) -> "Span": - """See `opentelemetry.trace.Tracer.create_span`.""" - span_id = generate_span_id() + ) -> "trace_api.Span": + """See `opentelemetry.trace.Tracer.create_span`. + + If `parent` is null the new span will be created as a root span, i.e. a + span with no parent context. By default, the new span will be created + as a child of the current span in this tracer's context, or as a root + span if no current span exists. + """ + if parent is Tracer.CURRENT_SPAN: parent = self.get_current_span() + if parent is None: - context = trace_api.SpanContext(generate_trace_id(), span_id) + parent_context = None + new_span_context = trace_api.SpanContext( + generate_trace_id(), generate_span_id() + ) else: if isinstance(parent, trace_api.Span): parent_context = parent.get_context() elif isinstance(parent, trace_api.SpanContext): parent_context = parent else: + # TODO: error handling raise TypeError - context = trace_api.SpanContext( + + new_span_context = trace_api.SpanContext( parent_context.trace_id, - span_id, + generate_span_id(), parent_context.trace_options, parent_context.trace_state, ) - return Span( - name=name, - context=context, - parent=parent, - span_processor=self._active_span_processor, - kind=kind, + + # The sampler decides whether to create a real or no-op span at the + # time of span creation. No-op spans do not record events, and are not + # exported. + # The sampler may also add attributes to the newly-created span, e.g. + # to include information about the sampling decision. + sampling_decision = self.sampler.should_sample( + parent_context, + new_span_context.trace_id, + new_span_context.span_id, + name, + {}, # TODO: links ) + if sampling_decision.sampled: + return Span( + name=name, + context=new_span_context, + parent=parent, + sampler=self.sampler, + attributes=sampling_decision.attributes, + span_processor=self._active_span_processor, + kind=kind, + ) + return trace_api.DefaultSpan(context=new_span_context) + @contextmanager def use_span( self, span: trace_api.Span, end_on_exit: bool = False - ) -> typing.Iterator[trace_api.Span]: + ) -> Iterator[trace_api.Span]: """See `opentelemetry.trace.Tracer.use_span`.""" try: span_snapshot = self._current_span_slot.get() diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index d1e3033d52..b7bfa1a915 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -17,6 +17,7 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import trace +from opentelemetry.trace import sampling from opentelemetry.util import time_ns @@ -26,6 +27,29 @@ def test_extends_api(self): self.assertIsInstance(tracer, trace_api.Tracer) +class TestTracerSampling(unittest.TestCase): + def test_default_sampler(self): + tracer = trace.Tracer() + + # Check that the default tracer creates real spans via the default + # sampler + root_span = tracer.create_span(name="root span", parent=None) + self.assertIsInstance(root_span, trace.Span) + child_span = tracer.create_span(name="child span", parent=root_span) + self.assertIsInstance(child_span, trace.Span) + + def test_sampler_no_sampling(self): + tracer = trace.Tracer() + tracer.sampler = sampling.ALWAYS_OFF + + # Check that the default tracer creates no-op spans if the sampler + # decides not to sampler + root_span = tracer.create_span(name="root span", parent=None) + self.assertIsInstance(root_span, trace_api.DefaultSpan) + child_span = tracer.create_span(name="child span", parent=root_span) + self.assertIsInstance(child_span, trace_api.DefaultSpan) + + class TestSpanCreation(unittest.TestCase): def test_start_span_implicit(self): tracer = trace.Tracer("test_start_span_implicit") From 26d56c0e27b82b559b492f5ef011f291094f0c3c Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 24 Oct 2019 14:06:38 -0700 Subject: [PATCH 0111/1517] SDK Tracer treats invalid span parent like null (#233, #235) The SDK tracer will now create spans with invalid parents as brand new spans, similar to not having a parent at all. Adding this behavior to the Tracer ensures that integrations do not have to handle invalid span contexts in their own code, and ensures that behavior is consistent with w3c tracecontext (which specifies invalid results should be handled by creating new spans). Setting the parent to none on spans if the parent context is invalid, reducing logic to handle that situation in downstream processing like exporters. --- .../src/opentelemetry/sdk/trace/__init__.py | 50 ++++++++++--------- opentelemetry-sdk/tests/trace/test_trace.py | 14 ++++++ 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 46e9b83e35..879d4e6385 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -376,30 +376,33 @@ def create_span( as a child of the current span in this tracer's context, or as a root span if no current span exists. """ + span_id = generate_span_id() if parent is Tracer.CURRENT_SPAN: parent = self.get_current_span() - if parent is None: - parent_context = None - new_span_context = trace_api.SpanContext( - generate_trace_id(), generate_span_id() - ) + parent_context = parent + if isinstance(parent_context, trace_api.Span): + parent_context = parent.get_context() + + if parent_context is not None and not isinstance( + parent_context, trace_api.SpanContext + ): + raise TypeError + + if parent_context is None or not parent_context.is_valid(): + parent = parent_context = None + trace_id = generate_trace_id() + trace_options = None + trace_state = None else: - if isinstance(parent, trace_api.Span): - parent_context = parent.get_context() - elif isinstance(parent, trace_api.SpanContext): - parent_context = parent - else: - # TODO: error handling - raise TypeError - - new_span_context = trace_api.SpanContext( - parent_context.trace_id, - generate_span_id(), - parent_context.trace_options, - parent_context.trace_state, - ) + trace_id = parent_context.trace_id + trace_options = parent_context.trace_options + trace_state = parent_context.trace_state + + context = trace_api.SpanContext( + trace_id, span_id, trace_options, trace_state + ) # The sampler decides whether to create a real or no-op span at the # time of span creation. No-op spans do not record events, and are not @@ -408,8 +411,8 @@ def create_span( # to include information about the sampling decision. sampling_decision = self.sampler.should_sample( parent_context, - new_span_context.trace_id, - new_span_context.span_id, + context.trace_id, + context.span_id, name, {}, # TODO: links ) @@ -417,14 +420,15 @@ def create_span( if sampling_decision.sampled: return Span( name=name, - context=new_span_context, + context=context, parent=parent, sampler=self.sampler, attributes=sampling_decision.attributes, span_processor=self._active_span_processor, kind=kind, ) - return trace_api.DefaultSpan(context=new_span_context) + + return trace_api.DefaultSpan(context=context) @contextmanager def use_span( diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index b7bfa1a915..626a5499ec 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -51,6 +51,20 @@ def test_sampler_no_sampling(self): class TestSpanCreation(unittest.TestCase): + def test_create_span_invalid_spancontext(self): + """If an invalid span context is passed as the parent, the created + span should use a new span id. + + Invalid span contexts should also not be added as a parent. This + eliminates redundant error handling logic in exporters. + """ + tracer = trace.Tracer("test_create_span_invalid_spancontext") + new_span = tracer.create_span( + "root", parent=trace_api.INVALID_SPAN_CONTEXT + ) + self.assertTrue(new_span.context.is_valid()) + self.assertIsNone(new_span.parent) + def test_start_span_implicit(self): tracer = trace.Tracer("test_start_span_implicit") From dba806f0f8a83e572d88a98e2c4ec0b2e88939d5 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 28 Oct 2019 05:53:27 -0700 Subject: [PATCH 0112/1517] Introducing and documenting the concept of OpenTelemetry buddies. (#237) To foster building of the community, introducing the concept of OpenTelemetry buddies to provide experienced partners for potential contributors. --- CONTRIBUTING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9bb81d7165..74cc345874 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,19 @@ on how to become a [**Member**](https://github.com/open-telemetry/community/blob [**Approver**](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver) and [**Maintainer**](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer). +## Find a Buddy and get Started Quickly! + +If you are looking for someone to help you find a starting point and be a resource for your first contribution, join our +Gitter and find a buddy! + +1. Join [Gitter.im](https://gitter.im) and join our [chat room](https://gitter.im/open-telemetry/opentelemetry-python). +2. Post in the room with an introduction to yourself, what area you are interested in (checks issues marked "Help Wanted"), +and say you are looking for a buddy. We will match you with someone who has experience in that area. + +Your OpenTelemetry buddy is your resource to talk to directly on all aspects of contributing to OpenTelemetry: providing +context, reviewing PRs, and helping those get merged. Buddies will not be available 24/7, but is committed to responding +during their normal contribution hours. + ## Development This project uses [`tox`](https://tox.readthedocs.io) to automate some aspects From f803b30bd14fcd8a5241b403d00e7b859f98c5f8 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 28 Oct 2019 11:49:43 -0700 Subject: [PATCH 0113/1517] Make start_as_current_span default (#246) --- README.md | 6 ++--- .../flask_example.py | 2 +- examples/trace/server.py | 2 +- .../examples/server.py | 2 +- .../examples/trace.py | 2 +- .../ext/http_requests/__init__.py | 2 +- .../tests/test_requests_integration.py | 10 ++++---- ext/opentelemetry-ext-jaeger/README.rst | 2 +- .../examples/jaeger_exporter_example.py | 6 ++--- opentelemetry-api/tests/trace/test_tracer.py | 4 ++++ .../tests/trace/export/test_export.py | 21 ++++++++++++++++ .../export/test_in_memory_span_exporter.py | 24 +++++++++---------- 12 files changed, 54 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 6edad08a2b..0d4548c923 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,9 @@ tracer = trace.tracer() tracer.add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) -with tracer.start_span('foo'): - with tracer.start_span('bar'): - with tracer.start_span('baz'): +with tracer.start_as_current_span('foo'): + with tracer.start_as_current_span('bar'): + with tracer.start_as_current_span('baz'): print(Context) ``` diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 229acdfb43..18dffa3000 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -67,7 +67,7 @@ def configure_opentelemetry(flask_app: flask.Flask): def hello(): # emit a trace that measures how long the # sleep takes - with trace.tracer().start_span("example-request"): + with trace.tracer().start_as_current_span("example-request"): requests.get("http://www.example.com") return "hello" diff --git a/examples/trace/server.py b/examples/trace/server.py index 878898593c..3632540e21 100755 --- a/examples/trace/server.py +++ b/examples/trace/server.py @@ -45,7 +45,7 @@ @app.route("/") def hello(): - with trace.tracer().start_span("parent"): + with trace.tracer().start_as_current_span("parent"): requests.get("https://www.wikipedia.org/wiki/Rabbit") return "hello" diff --git a/ext/opentelemetry-ext-azure-monitor/examples/server.py b/ext/opentelemetry-ext-azure-monitor/examples/server.py index 54727ef737..9374c986a4 100644 --- a/ext/opentelemetry-ext-azure-monitor/examples/server.py +++ b/ext/opentelemetry-ext-azure-monitor/examples/server.py @@ -34,7 +34,7 @@ @app.route("/") def hello(): - with trace.tracer().start_span("parent"): + with trace.tracer().start_as_current_span("parent"): requests.get("https://www.wikipedia.org/wiki/Rabbit") return "hello" diff --git a/ext/opentelemetry-ext-azure-monitor/examples/trace.py b/ext/opentelemetry-ext-azure-monitor/examples/trace.py index 8e8f887aa1..75b7dfa151 100644 --- a/ext/opentelemetry-ext-azure-monitor/examples/trace.py +++ b/ext/opentelemetry-ext-azure-monitor/examples/trace.py @@ -23,5 +23,5 @@ SimpleExportSpanProcessor(AzureMonitorSpanExporter()) ) -with tracer.start_span("hello") as span: +with tracer.start_as_current_span("hello") as span: print("Hello, World!") diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index a117da5daa..f05202c055 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -65,7 +65,7 @@ def instrumented_request(self, method, url, *args, **kwargs): path = "" path = parsed_url.path - with tracer.start_span(path, kind=SpanKind.CLIENT) as span: + with tracer.start_as_current_span(path, kind=SpanKind.CLIENT) as span: span.set_attribute("component", "http") span.set_attribute("http.method", method.upper()) span.set_attribute("http.url", url) diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index fe0cd88607..2a02e1916a 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -40,12 +40,12 @@ def setspanattr(key, value): self.span.set_attribute = setspanattr self.start_span_patcher = mock.patch.object( self.tracer, - "start_span", + "start_as_current_span", autospec=True, spec_set=True, return_value=self.span_context_manager, ) - self.start_span = self.start_span_patcher.start() + self.start_as_current_span = self.start_span_patcher.start() mocked_response = requests.models.Response() mocked_response.status_code = 200 @@ -70,7 +70,7 @@ def test_basic(self): url = "https://www.example.org/foo/bar?x=y#top" requests.get(url=url) self.assertEqual(1, len(self.send.call_args_list)) - self.tracer.start_span.assert_called_with( + self.tracer.start_as_current_span.assert_called_with( "/foo/bar", kind=trace.SpanKind.CLIENT ) self.span_context_manager.__enter__.assert_called_with() @@ -97,10 +97,10 @@ def test_invalid_url(self): with self.assertRaises(exception_type): requests.post(url=url) self.assertTrue( - self.tracer.start_span.call_args[0][0].startswith( + self.tracer.start_as_current_span.call_args[0][0].startswith( " Date: Mon, 28 Oct 2019 18:30:47 -0700 Subject: [PATCH 0114/1517] Implement direct calling convention of metric instruments (#224) --- docs/conf.py | 5 +- .../metrics_example.py | 7 +++ .../src/opentelemetry/metrics/__init__.py | 56 ++++++++++++++++--- .../tests/metrics/test_metrics.py | 12 ++++ .../src/opentelemetry/sdk/metrics/__init__.py | 55 +++++++++++------- .../tests/metrics/test_metrics.py | 46 +++++++++------ 6 files changed, 137 insertions(+), 44 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 94d17cb3eb..8b8c11d47b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,7 +52,10 @@ # http://www.sphinx-doc.org/en/master/config.html#confval-nitpicky # Sphinx will warn about all references where the target cannot be found. nitpicky = True -nitpick_ignore = [] +# Sphinx does not recognize generic type TypeVars +# Container supposedly were fixed, but does not work +# https://github.com/sphinx-doc/sphinx/pull/3744 +nitpick_ignore = [("py:class", "ValueT"), ("py:class", "typing.Tuple")] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py index dd9509feb2..41bdba8597 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py @@ -31,8 +31,15 @@ ) label_values = ("staging",) + +# Direct metric usage +counter.add(label_values, 25) + +# Handle usage counter_handle = counter.get_handle(label_values) counter_handle.add(100) + +# Record batch usage meter.record_batch(label_values, [(counter, 50)]) print(counter_handle.data) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 61cc8bdfac..e866aa97cf 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -26,7 +26,6 @@ """ -from abc import ABC, abstractmethod from typing import Callable, Optional, Sequence, Tuple, Type, TypeVar from opentelemetry.util import loader @@ -43,27 +42,38 @@ class DefaultMetricHandle: class CounterHandle: def add(self, value: ValueT) -> None: - """Increases the value of the handle by ``value``""" + """Increases the value of the handle by ``value``. + + Args: + value: The value to record to the handle. + """ class GaugeHandle: def set(self, value: ValueT) -> None: - """Sets the current value of the handle to ``value``.""" + """Sets the current value of the handle to ``value``. + + Args: + value: The value to record to the handle. + """ class MeasureHandle: def record(self, value: ValueT) -> None: - """Records the given ``value`` to this handle.""" + """Records the given ``value`` to this handle. + + Args: + value: The value to record to the handle. + """ -class Metric(ABC): +class Metric: """Base class for various types of metrics. Metric class that inherit from this class are specialized with the type of handle that the metric holds. """ - @abstractmethod def get_handle(self, label_values: Sequence[str]) -> "object": """Gets a handle, used for repeated-use of metrics instruments. @@ -83,6 +93,11 @@ class DefaultMetric(Metric): """The default Metric used when no Metric implementation is available.""" def get_handle(self, label_values: Sequence[str]) -> "DefaultMetricHandle": + """Gets a `DefaultMetricHandle`. + + Args: + label_values: The label values associated with the handle. + """ return DefaultMetricHandle() @@ -93,6 +108,14 @@ def get_handle(self, label_values: Sequence[str]) -> "CounterHandle": """Gets a `CounterHandle`.""" return CounterHandle() + def add(self, label_values: Sequence[str], value: ValueT) -> None: + """Increases the value of the counter by ``value``. + + Args: + label_values: The label values associated with the metric. + value: The value to add to the counter metric. + """ + class Gauge(Metric): """A gauge type metric that expresses a pre-calculated value. @@ -107,6 +130,14 @@ def get_handle(self, label_values: Sequence[str]) -> "GaugeHandle": """Gets a `GaugeHandle`.""" return GaugeHandle() + def set(self, label_values: Sequence[str], value: ValueT) -> None: + """Sets the value of the gauge to ``value``. + + Args: + label_values: The label values associated with the metric. + value: The value to set the gauge metric to. + """ + class Measure(Metric): """A measure type metric that represent raw stats that are recorded. @@ -120,6 +151,14 @@ def get_handle(self, label_values: Sequence[str]) -> "MeasureHandle": """Gets a `MeasureHandle` with a float value.""" return MeasureHandle() + def record(self, label_values: Sequence[str], value: ValueT) -> None: + """Records the ``value`` to the measure. + + Args: + label_values: The label values associated with the metric. + value: The value to record to this measure metric. + """ + MetricT = TypeVar("MetricT", Counter, Gauge, Measure) @@ -145,8 +184,9 @@ def record_batch( match the key-value pairs in the label tuples. Args: - label_values: The values that will be matched against to record for - the handles under each metric that has those labels. + label_values: The label values associated with all measurements in + the batch. A measurement is a tuple, representing the `Metric` + being recorded and the corresponding value to record. record_tuples: A sequence of pairs of `Metric` s and the corresponding value to record for that metric. """ diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 97ac92fcde..758534f235 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -42,16 +42,28 @@ def test_counter(self): handle = counter.get_handle(("test", "test1")) self.assertIsInstance(handle, metrics.CounterHandle) + def test_counter_add(self): + counter = metrics.Counter() + counter.add(("value",), 1) + def test_gauge(self): gauge = metrics.Gauge() handle = gauge.get_handle(("test", "test1")) self.assertIsInstance(handle, metrics.GaugeHandle) + def test_gauge_set(self): + gauge = metrics.Gauge() + gauge.set(("value",), 1) + def test_measure(self): measure = metrics.Measure() handle = measure.get_handle(("test", "test1")) self.assertIsInstance(handle, metrics.MeasureHandle) + def test_measure_record(self): + measure = metrics.Measure() + measure.record(("value",), 1) + def test_default_handle(self): metrics.DefaultMetricHandle() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 041d0e5dcd..0a941cd0d1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -44,43 +44,34 @@ def _validate_update(self, value: metrics_api.ValueT) -> bool: class CounterHandle(metrics_api.CounterHandle, BaseHandle): - def update(self, value: metrics_api.ValueT) -> None: + def add(self, value: metrics_api.ValueT) -> None: + """See `opentelemetry.metrics.CounterHandle.add`.""" if self._validate_update(value): if self.monotonic and value < 0: logger.warning("Monotonic counter cannot descend.") return self.data += value - def add(self, value: metrics_api.ValueT) -> None: - """See `opentelemetry.metrics.CounterHandle._add`.""" - self.update(value) - class GaugeHandle(metrics_api.GaugeHandle, BaseHandle): - def update(self, value: metrics_api.ValueT) -> None: + def set(self, value: metrics_api.ValueT) -> None: + """See `opentelemetry.metrics.GaugeHandle.set`.""" if self._validate_update(value): if self.monotonic and value < self.data: logger.warning("Monotonic gauge cannot descend.") return self.data = value - def set(self, value: metrics_api.ValueT) -> None: - """See `opentelemetry.metrics.GaugeHandle._set`.""" - self.update(value) - class MeasureHandle(metrics_api.MeasureHandle, BaseHandle): - def update(self, value: metrics_api.ValueT) -> None: + def record(self, value: metrics_api.ValueT) -> None: + """See `opentelemetry.metrics.MeasureHandle.record`.""" if self._validate_update(value): if self.monotonic and value < 0: logger.warning("Monotonic measure cannot accept negatives.") return # TODO: record - def record(self, value: metrics_api.ValueT) -> None: - """See `opentelemetry.metrics.MeasureHandle._record`.""" - self.update(value) - class Metric(metrics_api.Metric): """See `opentelemetry.metrics.Metric`.""" @@ -116,8 +107,10 @@ def get_handle(self, label_values: Sequence[str]) -> BaseHandle: self.handles[label_values] = handle return handle + UPDATE_FUNCTION = lambda x, y: None # noqa: E731 + -class Counter(Metric): +class Counter(Metric, metrics_api.Counter): """See `opentelemetry.metrics.Counter`. By default, counter values can only go up (monotonic). Negative inputs @@ -147,8 +140,16 @@ def __init__( monotonic=monotonic, ) + def add( + self, label_values: Sequence[str], value: metrics_api.ValueT + ) -> None: + """See `opentelemetry.metrics.Counter.add`.""" + self.get_handle(label_values).add(value) + + UPDATE_FUNCTION = add + -class Gauge(Metric): +class Gauge(Metric, metrics_api.Gauge): """See `opentelemetry.metrics.Gauge`. By default, gauge values can go both up and down (non-monotonic). @@ -177,8 +178,16 @@ def __init__( monotonic=monotonic, ) + def set( + self, label_values: Sequence[str], value: metrics_api.ValueT + ) -> None: + """See `opentelemetry.metrics.Gauge.set`.""" + self.get_handle(label_values).set(value) + + UPDATE_FUNCTION = set -class Measure(Metric): + +class Measure(Metric, metrics_api.Measure): """See `opentelemetry.metrics.Measure`. By default, measure metrics can accept both positive and negatives. @@ -207,6 +216,14 @@ def __init__( monotonic=monotonic, ) + def record( + self, label_values: Sequence[str], value: metrics_api.ValueT + ) -> None: + """See `opentelemetry.metrics.Measure.record`.""" + self.get_handle(label_values).record(value) + + UPDATE_FUNCTION = record + class Meter(metrics_api.Meter): """See `opentelemetry.metrics.Meter`.""" @@ -218,7 +235,7 @@ def record_batch( ) -> None: """See `opentelemetry.metrics.Meter.record_batch`.""" for metric, value in record_tuples: - metric.get_handle(label_values).update(value) + metric.UPDATE_FUNCTION(label_values, value) def create_metric( self, diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index dc4151c4ee..cc37bc1a8a 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -51,8 +51,8 @@ def test_record_batch_exists(self): label_keys = ("key1",) label_values = ("value1",) counter = metrics.Counter("name", "desc", "unit", float, label_keys) + counter.add(label_values, 1.0) handle = counter.get_handle(label_values) - handle.update(1.0) record_tuples = [(counter, 1.0)] meter.record_batch(label_values, record_tuples) self.assertEqual(counter.get_handle(label_values), handle) @@ -96,12 +96,35 @@ def test_get_handle(self): self.assertEqual(metric.handles.get(label_values), handle) -class TestCounterHandle(unittest.TestCase): - def test_update(self): - handle = metrics.CounterHandle(float, True, False) - handle.update(2.0) - self.assertEqual(handle.data, 2.0) +class TestCounter(unittest.TestCase): + def test_add(self): + metric = metrics.Counter("name", "desc", "unit", int, ("key",)) + handle = metric.get_handle(("value",)) + metric.add(("value",), 3) + metric.add(("value",), 2) + self.assertEqual(handle.data, 5) + +class TestGauge(unittest.TestCase): + def test_set(self): + metric = metrics.Gauge("name", "desc", "unit", int, ("key",)) + handle = metric.get_handle(("value",)) + metric.set(("value",), 3) + self.assertEqual(handle.data, 3) + metric.set(("value",), 2) + self.assertEqual(handle.data, 2) + + +class TestMeasure(unittest.TestCase): + def test_record(self): + metric = metrics.Measure("name", "desc", "unit", int, ("key",)) + handle = metric.get_handle(("value",)) + metric.record(("value",), 3) + # Record not implemented yet + self.assertEqual(handle.data, 0) + + +class TestCounterHandle(unittest.TestCase): def test_add(self): handle = metrics.CounterHandle(int, True, False) handle.add(3) @@ -128,11 +151,6 @@ def test_add_incorrect_type(self, logger_mock): class TestGaugeHandle(unittest.TestCase): - def test_update(self): - handle = metrics.GaugeHandle(float, True, False) - handle.update(2.0) - self.assertEqual(handle.data, 2.0) - def test_set(self): handle = metrics.GaugeHandle(int, True, False) handle.set(3) @@ -159,14 +177,10 @@ def test_set_incorrect_type(self, logger_mock): class TestMeasureHandle(unittest.TestCase): - def test_update(self): - handle = metrics.MeasureHandle(float, False, False) - handle.update(2.0) - self.assertEqual(handle.data, 0) - def test_record(self): handle = metrics.MeasureHandle(int, False, False) handle.record(3) + # Record not implemented yet self.assertEqual(handle.data, 0) def test_record_disabled(self): From 602d42a45f6a4684342b298c5a7c4dab680e301a Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 29 Oct 2019 11:01:59 -0700 Subject: [PATCH 0115/1517] Adding w3c tracecontext integration test (#228) Verifying that our tracecontext is compliant with the w3c tracecontext reference is valuable. Adding a tox command to verify that the TraceContext propagator adheres to the w3c spec. The tracecontexthttptextformat is now completely compliant with the w3c tracecontext test suite. --- .flake8 | 1 + .gitignore | 1 + .isort.cfg | 1 + .../tests/test_wsgi_middleware.py | 2 +- .../propagation/tracecontexthttptextformat.py | 54 +++++--- .../propagation/binaryformat.py | 2 +- .../src/opentelemetry/propagators/__init__.py | 2 +- .../src/opentelemetry/trace/__init__.py | 5 +- .../src/opentelemetry/util/loader.py | 2 +- .../test_tracecontexthttptextformat.py | 116 ++++++++---------- .../src/opentelemetry/sdk/trace/__init__.py | 2 - .../context/propagation/test_b3_format.py | 10 +- pyproject.toml | 2 + scripts/tracecontext-integration-test.sh | 27 ++++ tests/w3c_tracecontext_validation_server.py | 77 ++++++++++++ tox.ini | 19 +++ 16 files changed, 224 insertions(+), 99 deletions(-) create mode 100755 scripts/tracecontext-integration-test.sh create mode 100644 tests/w3c_tracecontext_validation_server.py diff --git a/.flake8 b/.flake8 index 5abd0630ea..a0924f947d 100644 --- a/.flake8 +++ b/.flake8 @@ -10,6 +10,7 @@ exclude = .svn .tox CVS + target __pycache__ ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/ ext/opentelemetry-ext-jaeger/build/* diff --git a/.gitignore b/.gitignore index 679b6fd0cc..c9acc31940 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ _build/ # mypy .mypy_cache/ +target diff --git a/.isort.cfg b/.isort.cfg index 4bf64a34f1..96011ae93d 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -12,4 +12,5 @@ line_length=79 ; ) ; docs: https://github.com/timothycrosley/isort#multi-line-output-modes multi_line_output=3 +skip=target skip_glob=ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/* diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index a88782d642..e5dc9654fd 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -210,7 +210,7 @@ def test_request_attributes_with_partial_raw_uri(self): self.validate_url("http://127.0.0.1/#top") def test_request_attributes_with_partial_raw_uri_and_nonstandard_port( - self + self, ): self.environ["RAW_URI"] = "/?" del self.environ["HTTP_HOST"] diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py index abe778db95..20f2601fa2 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py @@ -39,11 +39,13 @@ ) _DELIMITER_FORMAT = "[ \t]*,[ \t]*" -_MEMBER_FORMAT = "({})(=)({})".format(_KEY_FORMAT, _VALUE_FORMAT) +_MEMBER_FORMAT = "({})(=)({})[ \t]*".format(_KEY_FORMAT, _VALUE_FORMAT) _DELIMITER_FORMAT_RE = re.compile(_DELIMITER_FORMAT) _MEMBER_FORMAT_RE = re.compile(_MEMBER_FORMAT) +_TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS = 32 + class TraceContextHTTPTextFormat(httptextformat.HTTPTextFormat): """Extracts and injects using w3c TraceContext's headers. @@ -86,15 +88,10 @@ def extract( if version == "ff": return trace.INVALID_SPAN_CONTEXT - tracestate = trace.TraceState() - for tracestate_header in get_from_carrier( + tracestate_headers = get_from_carrier( carrier, cls._TRACESTATE_HEADER_NAME - ): - # typing.Dict's update is not recognized by pylint: - # https://github.com/PyCQA/pylint/issues/2420 - tracestate.update( # pylint:disable=E1101 - _parse_tracestate(tracestate_header) - ) + ) + tracestate = _parse_tracestate(tracestate_headers) span_context = trace.SpanContext( trace_id=int(trace_id, 16), @@ -127,8 +124,8 @@ def inject( ) -def _parse_tracestate(string: str) -> trace.TraceState: - """Parse a w3c tracestate header into a TraceState. +def _parse_tracestate(header_list: typing.List[str]) -> trace.TraceState: + """Parse one or more w3c tracestate header into a TraceState. Args: string: the value of the tracestate header. @@ -136,16 +133,35 @@ def _parse_tracestate(string: str) -> trace.TraceState: Returns: A valid TraceState that contains values extracted from the tracestate header. + + If the format of one headers is illegal, all values will + be discarded and an empty tracestate will be returned. + + If the number of keys is beyond the maximum, all values + will be discarded and an empty tracestate will be returned. """ tracestate = trace.TraceState() - for member in re.split(_DELIMITER_FORMAT_RE, string): - match = _MEMBER_FORMAT_RE.match(member) - if not match: - raise ValueError("illegal key-value format %r" % (member)) - key, _eq, value = match.groups() - # typing.Dict's update is not recognized by pylint: - # https://github.com/PyCQA/pylint/issues/2420 - tracestate[key] = value # pylint:disable=E1137 + value_count = 0 + for header in header_list: + for member in re.split(_DELIMITER_FORMAT_RE, header): + # empty members are valid, but no need to process further. + if not member: + continue + match = _MEMBER_FORMAT_RE.fullmatch(member) + if not match: + # TODO: log this? + return trace.TraceState() + key, _eq, value = match.groups() + if key in tracestate: # pylint:disable=E1135 + # duplicate keys are not legal in + # the header, so we will remove + return trace.TraceState() + # typing.Dict's update is not recognized by pylint: + # https://github.com/PyCQA/pylint/issues/2420 + tracestate[key] = value # pylint:disable=E1137 + value_count += 1 + if value_count > _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS: + return trace.TraceState() return tracestate diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py index 15f8cfdf63..d6d083c0da 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py @@ -44,7 +44,7 @@ def to_bytes(context: DistributedContext) -> bytes: @staticmethod @abc.abstractmethod def from_bytes( - byte_representation: bytes + byte_representation: bytes, ) -> typing.Optional[DistributedContext]: """Return a DistributedContext that was represented by bytes. diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index 5b71e8785a..bb75d84c3a 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -78,7 +78,7 @@ def get_global_httptextformat() -> httptextformat.HTTPTextFormat: def set_global_httptextformat( - http_text_format: httptextformat.HTTPTextFormat + http_text_format: httptextformat.HTTPTextFormat, ) -> None: global _HTTP_TEXT_FORMAT # pylint:disable=global-statement _HTTP_TEXT_FORMAT = http_text_format diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index b64ce852b0..1ac7d73d49 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -338,10 +338,11 @@ def __init__( self.trace_state = trace_state def __repr__(self) -> str: - return "{}(trace_id={}, span_id={})".format( + return "{}(trace_id={}, span_id={}, trace_state={!r})".format( type(self).__name__, format_trace_id(self.trace_id), format_span_id(self.span_id), + self.trace_state, ) def is_valid(self) -> bool: @@ -589,7 +590,7 @@ def tracer() -> Tracer: def set_preferred_tracer_implementation( - factory: ImplementationFactory + factory: ImplementationFactory, ) -> None: """Set the factory to be used to create the tracer. diff --git a/opentelemetry-api/src/opentelemetry/util/loader.py b/opentelemetry-api/src/opentelemetry/util/loader.py index 0781ce15b7..3ae5a52fc5 100644 --- a/opentelemetry-api/src/opentelemetry/util/loader.py +++ b/opentelemetry-api/src/opentelemetry/util/loader.py @@ -173,7 +173,7 @@ def _load_impl( def set_preferred_default_implementation( - implementation_factory: _UntrustedImplFactory[_T] + implementation_factory: _UntrustedImplFactory[_T], ) -> None: """Sets a factory function that may be called for any implementation object. See the :ref:`module docs ` for more details.""" diff --git a/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py index aaf392be24..ed952e0dba 100644 --- a/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py @@ -22,10 +22,10 @@ def get_as_list( - dict_object: typing.Dict[str, str], key: str + dict_object: typing.Dict[str, typing.List[str]], key: str ) -> typing.List[str]: value = dict_object.get(key) - return [value] if value is not None else [] + return value if value is not None else [] class TestTraceContextFormat(unittest.TestCase): @@ -40,64 +40,10 @@ def test_no_traceparent_header(self): If no traceparent header is received, the vendor creates a new trace-id and parent-id that represents the current request. """ - output = {} # type:typing.Dict[str, str] + output = {} # type:typing.Dict[str, typing.List[str]] span_context = FORMAT.extract(get_as_list, output) self.assertTrue(isinstance(span_context, trace.SpanContext)) - def test_from_headers_tracestate_entry_limit(self): - """If more than 33 entries are passed, allow them. - - We are explicitly choosing not to limit the list members - as outlined in RFC 3.3.1.1 - - RFC 3.3.1.1 - - There can be a maximum of 32 list-members in a list. - """ - - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": "00-12345678901234567890123456789012-1234567890123456-00", - "tracestate": ",".join( - [ - "a00=0,a01=1,a02=2,a03=3,a04=4,a05=5,a06=6,a07=7,a08=8,a09=9", - "b00=0,b01=1,b02=2,b03=3,b04=4,b05=5,b06=6,b07=7,b08=8,b09=9", - "c00=0,c01=1,c02=2,c03=3,c04=4,c05=5,c06=6,c07=7,c08=8,c09=9", - "d00=0,d01=1,d02=2", - ] - ), - }, - ) - self.assertEqual(len(span_context.trace_state), 33) - - def test_from_headers_tracestate_duplicated_keys(self): - """If a duplicate tracestate header is present, the most recent entry - is used. - - RFC 3.3.1.4 - - Only one entry per key is allowed because the entry represents that last position in the trace. - Hence vendors must overwrite their entry upon reentry to their tracing system. - - For example, if a vendor name is Congo and a trace started in their system and then went through - a system named Rojo and later returned to Congo, the tracestate value would not be: - - congo=congosFirstPosition,rojo=rojosFirstPosition,congo=congosSecondPosition - - Instead, the entry would be rewritten to only include the most recent position: - - congo=congosSecondPosition,rojo=rojosFirstPosition - """ - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": "00-12345678901234567890123456789012-1234567890123456-00", - "tracestate": "foo=1,bar=2,foo=3", - }, - ) - self.assertEqual(span_context.trace_state, {"foo": "3", "bar": "2"}) - def test_headers_with_tracestate(self): """When there is a traceparent and tracestate header, data from both should be addded to the SpanContext. @@ -109,7 +55,10 @@ def test_headers_with_tracestate(self): tracestate_value = "foo=1,bar=2,baz=3" span_context = FORMAT.extract( get_as_list, - {"traceparent": traceparent_value, "tracestate": tracestate_value}, + { + "traceparent": [traceparent_value], + "tracestate": [tracestate_value], + }, ) self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) @@ -125,7 +74,8 @@ def test_headers_with_tracestate(self): self.assertEqual(output["tracestate"].count(","), 2) def test_invalid_trace_id(self): - """If the trace id is invalid, we must ignore the full traceparent header. + """If the trace id is invalid, we must ignore the full traceparent header, + and return a random, valid trace. Also ignore any tracestate. @@ -142,8 +92,10 @@ def test_invalid_trace_id(self): span_context = FORMAT.extract( get_as_list, { - "traceparent": "00-00000000000000000000000000000000-1234567890123456-00", - "tracestate": "foo=1,bar=2,foo=3", + "traceparent": [ + "00-00000000000000000000000000000000-1234567890123456-00" + ], + "tracestate": ["foo=1,bar=2,foo=3"], }, ) self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) @@ -166,8 +118,10 @@ def test_invalid_parent_id(self): span_context = FORMAT.extract( get_as_list, { - "traceparent": "00-00000000000000000000000000000000-0000000000000000-00", - "tracestate": "foo=1,bar=2,foo=3", + "traceparent": [ + "00-00000000000000000000000000000000-0000000000000000-00" + ], + "tracestate": ["foo=1,bar=2,foo=3"], }, ) self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) @@ -195,14 +149,15 @@ def test_format_not_supported(self): RFC 4.3 - If the version cannot be parsed, the vendor creates a new traceparent header and - deletes tracestate. + If the version cannot be parsed, return an invalid trace header. """ span_context = FORMAT.extract( get_as_list, { - "traceparent": "00-12345678901234567890123456789012-1234567890123456-00-residue", - "tracestate": "foo=1,bar=2,foo=3", + "traceparent": [ + "00-12345678901234567890123456789012-1234567890123456-00-residue" + ], + "tracestate": ["foo=1,bar=2,foo=3"], }, ) self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) @@ -213,3 +168,30 @@ def test_propagate_invalid_context(self): output = {} # type:typing.Dict[str, str] FORMAT.inject(trace.INVALID_SPAN_CONTEXT, dict.__setitem__, output) self.assertFalse("traceparent" in output) + + def test_tracestate_empty_header(self): + """Test tracestate with an additional empty header (should be ignored)""" + span_context = FORMAT.extract( + get_as_list, + { + "traceparent": [ + "00-12345678901234567890123456789012-1234567890123456-00" + ], + "tracestate": ["foo=1", ""], + }, + ) + self.assertEqual(span_context.trace_state["foo"], "1") + + def test_tracestate_header_with_trailing_comma(self): + """Do not propagate invalid trace context. + """ + span_context = FORMAT.extract( + get_as_list, + { + "traceparent": [ + "00-12345678901234567890123456789012-1234567890123456-00" + ], + "tracestate": ["foo=1,"], + }, + ) + self.assertEqual(span_context.trace_state["foo"], "1") diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 879d4e6385..0909b9b434 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -301,7 +301,6 @@ def set_status(self, status: trace_api.Status) -> None: def generate_span_id() -> int: """Get a new random span ID. - Returns: A random 64-bit int for use as a span ID """ @@ -310,7 +309,6 @@ def generate_span_id() -> int: def generate_trace_id() -> int: """Get a new random trace ID. - Returns: A random 128-bit int for use as a trace ID """ diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py index 09d3f88f41..1215508269 100644 --- a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -16,7 +16,7 @@ import opentelemetry.sdk.context.propagation.b3_format as b3_format import opentelemetry.sdk.trace as trace -import opentelemetry.trace as api_trace +import opentelemetry.trace as trace_api FORMAT = b3_format.B3Format() @@ -163,8 +163,8 @@ def test_invalid_single_header(self): """ carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} span_context = FORMAT.extract(get_as_list, carrier) - self.assertEqual(span_context.trace_id, api_trace.INVALID_TRACE_ID) - self.assertEqual(span_context.span_id, api_trace.INVALID_SPAN_ID) + self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) + self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) def test_missing_trace_id(self): """If a trace id is missing, populate an invalid trace id.""" @@ -173,7 +173,7 @@ def test_missing_trace_id(self): FORMAT.FLAGS_KEY: "1", } span_context = FORMAT.extract(get_as_list, carrier) - self.assertEqual(span_context.trace_id, api_trace.INVALID_TRACE_ID) + self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) def test_missing_span_id(self): """If a trace id is missing, populate an invalid trace id.""" @@ -182,4 +182,4 @@ def test_missing_span_id(self): FORMAT.FLAGS_KEY: "1", } span_context = FORMAT.extract(get_as_list, carrier) - self.assertEqual(span_context.span_id, api_trace.INVALID_SPAN_ID) + self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) diff --git a/pyproject.toml b/pyproject.toml index eff7e2e3ec..cb3c2eb546 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,10 @@ exclude = ''' | \.mypy_cache | \.tox | \.venv + | \.vscode | _build | buck-out + | target | build | dist | ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen # generated files diff --git a/scripts/tracecontext-integration-test.sh b/scripts/tracecontext-integration-test.sh new file mode 100755 index 0000000000..4d482ddafe --- /dev/null +++ b/scripts/tracecontext-integration-test.sh @@ -0,0 +1,27 @@ +#!/bin/sh +set -e +# hard-coding the git tag to ensure stable builds. +TRACECONTEXT_GIT_TAG="98f210efd89c63593dce90e2bae0a1bdcb986f51" +# clone w3c tracecontext tests +mkdir -p target +rm -rf ./target/trace-context +git clone https://github.com/w3c/trace-context ./target/trace-context +cd ./target/trace-context && git checkout $TRACECONTEXT_GIT_TAG && cd - +# start example opentelemetry service, which propagates trace-context by +# default. +python ./tests/w3c_tracecontext_validation_server.py 1>&2 & +EXAMPLE_SERVER_PID=$! +# give the app server a little time to start up. Not adding some sort +# of delay would cause many of the tracecontext tests to fail being +# unable to connect. +sleep 1 +onshutdown() +{ + # send a sigint, to ensure + # it is caught as a KeyboardInterrupt in the + # example service. + kill $EXAMPLE_SERVER_PID +} +trap onshutdown EXIT +cd ./target/trace-context/test +python test.py http://127.0.0.1:5000/verify-tracecontext diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py new file mode 100644 index 0000000000..a26141f14c --- /dev/null +++ b/tests/w3c_tracecontext_validation_server.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This server is intended to be used with the W3C tracecontext validation +Service. It implements the APIs needed to be exercised by the test bed. +""" + +import json + +import flask +import requests + +from opentelemetry import trace +from opentelemetry.ext import http_requests +from opentelemetry.ext.wsgi import OpenTelemetryMiddleware +from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +# The preferred tracer implementation must be set, as the opentelemetry-api +# defines the interface with a no-op implementation. +trace.set_preferred_tracer_implementation(lambda T: Tracer()) + +# Integrations are the glue that binds the OpenTelemetry API and the +# frameworks and libraries that are used together, automatically creating +# Spans and propagating context as appropriate. +http_requests.enable(trace.tracer()) + +# SpanExporter receives the spans and send them to the target location. +span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter()) +trace.tracer().add_span_processor(span_processor) + +app = flask.Flask(__name__) +app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) + + +@app.route("/verify-tracecontext", methods=["POST"]) +def verify_tracecontext(): + """Upon reception of some payload, sends a request back to the designated + url. + + This route is designed to be testable with the w3c tracecontext server / + client test. + """ + for action in flask.request.json: + requests.post( + url=action["url"], + data=json.dumps(action["arguments"]), + headers={ + "Accept": "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + timeout=5.0, + ) + return "hello" + + +if __name__ == "__main__": + try: + app.run(debug=True) + finally: + span_processor.shutdown() diff --git a/tox.ini b/tox.ini index a6abe8e158..e30cb1a14b 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} lint + py37-tracecontext py37-{mypy,mypyinstalled} docs @@ -111,3 +112,21 @@ changedir = docs commands = sphinx-build -W --keep-going -b html -T . _build/html + +[testenv:py37-tracecontext] +basepython: python3.7 +deps = + # needed for tracecontext + aiohttp~=3.6 + # needed for example trace integration + flask~=1.1 + requests~=2.7 + +commands_pre = + pip install -e {toxinidir}/opentelemetry-api + pip install -e {toxinidir}/opentelemetry-sdk + pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests + pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi + +commands = + {toxinidir}/scripts/tracecontext-integration-test.sh From 7f11ba3e99e4694b0e871d1d5f69b4fa4ae1e76c Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 29 Oct 2019 11:11:47 -0700 Subject: [PATCH 0116/1517] Reblacken (#249) From d3e2a15aa16629c427189b2c368164ff7073b06a Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 29 Oct 2019 11:59:09 -0700 Subject: [PATCH 0117/1517] Add changelogs for 0.1a.0 release (#251) --- ext/opentelemetry-ext-azure-monitor/CHANGELOG.md | 8 ++++++++ ext/opentelemetry-ext-http-requests/CHANGELOG.md | 8 ++++++++ ext/opentelemetry-ext-wsgi/CHANGELOG.md | 8 ++++++++ opentelemetry-api/CHANGELOG.md | 5 +++++ opentelemetry-sdk/CHANGELOG.md | 4 ++++ 5 files changed, 33 insertions(+) create mode 100644 ext/opentelemetry-ext-azure-monitor/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-http-requests/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-wsgi/CHANGELOG.md diff --git a/ext/opentelemetry-ext-azure-monitor/CHANGELOG.md b/ext/opentelemetry-ext-azure-monitor/CHANGELOG.md new file mode 100644 index 0000000000..5838fb403b --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## Unreleased + +## 0.1a.0 +Released 2019-09-30 + +- Initial release diff --git a/ext/opentelemetry-ext-http-requests/CHANGELOG.md b/ext/opentelemetry-ext-http-requests/CHANGELOG.md new file mode 100644 index 0000000000..5838fb403b --- /dev/null +++ b/ext/opentelemetry-ext-http-requests/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## Unreleased + +## 0.1a.0 +Released 2019-09-30 + +- Initial release diff --git a/ext/opentelemetry-ext-wsgi/CHANGELOG.md b/ext/opentelemetry-ext-wsgi/CHANGELOG.md new file mode 100644 index 0000000000..5838fb403b --- /dev/null +++ b/ext/opentelemetry-ext-wsgi/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## Unreleased + +## 0.1a.0 +Released 2019-09-30 + +- Initial release diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 09e4c38c76..b63bb4eb14 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,3 +2,8 @@ ## Unreleased - Initial release + +## 0.1a.0 +Released 2019-09-30 + +- Initial release diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 09e4c38c76..5838fb403b 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -1,4 +1,8 @@ # Changelog ## Unreleased + +## 0.1a.0 +Released 2019-09-30 + - Initial release From be562774933be2e08296e80466f41adff3ce3223 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 29 Oct 2019 18:53:08 -0700 Subject: [PATCH 0118/1517] Update changelogs for 0.2a0 release (#253) --- .../CHANGELOG.md | 9 ++++++++- .../CHANGELOG.md | 9 ++++++++- ext/opentelemetry-ext-jaeger/CHANGELOG.md | 9 +++++++++ .../CHANGELOG.md | 9 +++++++++ ext/opentelemetry-ext-wsgi/CHANGELOG.md | 9 ++++++++- opentelemetry-api/CHANGELOG.md | 17 +++++++++++++++-- opentelemetry-sdk/CHANGELOG.md | 15 ++++++++++++++- 7 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 ext/opentelemetry-ext-jaeger/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md diff --git a/ext/opentelemetry-ext-azure-monitor/CHANGELOG.md b/ext/opentelemetry-ext-azure-monitor/CHANGELOG.md index 5838fb403b..ac7fc896a3 100644 --- a/ext/opentelemetry-ext-azure-monitor/CHANGELOG.md +++ b/ext/opentelemetry-ext-azure-monitor/CHANGELOG.md @@ -2,7 +2,14 @@ ## Unreleased -## 0.1a.0 +## 0.2a0 + +Released 2019-10-29 + +- Updates for core library changes + +## 0.1a0 + Released 2019-09-30 - Initial release diff --git a/ext/opentelemetry-ext-http-requests/CHANGELOG.md b/ext/opentelemetry-ext-http-requests/CHANGELOG.md index 5838fb403b..ac7fc896a3 100644 --- a/ext/opentelemetry-ext-http-requests/CHANGELOG.md +++ b/ext/opentelemetry-ext-http-requests/CHANGELOG.md @@ -2,7 +2,14 @@ ## Unreleased -## 0.1a.0 +## 0.2a0 + +Released 2019-10-29 + +- Updates for core library changes + +## 0.1a0 + Released 2019-09-30 - Initial release diff --git a/ext/opentelemetry-ext-jaeger/CHANGELOG.md b/ext/opentelemetry-ext-jaeger/CHANGELOG.md new file mode 100644 index 0000000000..73350ef7ad --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## Unreleased + +## 0.2a.0 + +Released 2019-10-29 + +- Initial release diff --git a/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md b/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md new file mode 100644 index 0000000000..73350ef7ad --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## Unreleased + +## 0.2a.0 + +Released 2019-10-29 + +- Initial release diff --git a/ext/opentelemetry-ext-wsgi/CHANGELOG.md b/ext/opentelemetry-ext-wsgi/CHANGELOG.md index 5838fb403b..ac7fc896a3 100644 --- a/ext/opentelemetry-ext-wsgi/CHANGELOG.md +++ b/ext/opentelemetry-ext-wsgi/CHANGELOG.md @@ -2,7 +2,14 @@ ## Unreleased -## 0.1a.0 +## 0.2a0 + +Released 2019-10-29 + +- Updates for core library changes + +## 0.1a0 + Released 2019-09-30 - Initial release diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index b63bb4eb14..ff78c0bf1e 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -1,9 +1,22 @@ # Changelog ## Unreleased -- Initial release -## 0.1a.0 +## 0.2a0 + +Released 2019-10-29 + +- W3C TraceContext fixes and compliance tests + ([#228](https://github.com/open-telemetry/opentelemetry-python/pull/228)) +- Multiple metrics API changes +- Multiple tracing API changes +- Multiple context API changes +- Sampler API + ([#225](https://github.com/open-telemetry/opentelemetry-python/pull/225)) +- Multiple bugfixes and improvements + +## 0.1a0 + Released 2019-09-30 - Initial release diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 5838fb403b..8ddb3e60ef 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,7 +2,20 @@ ## Unreleased -## 0.1a.0 +## 0.2a0 + +Released 2019-10-29 + +- W3C TraceContext fixes and compliance tests + ([#228](https://github.com/open-telemetry/opentelemetry-python/pull/228)) +- Multiple metrics SDK changes +- Multiple tracing SDK changes +- Sampler SDK + ([#225](https://github.com/open-telemetry/opentelemetry-python/pull/225)) +- Multiple bugfixes and improvements + +## 0.1a0 + Released 2019-09-30 - Initial release From 6d1cd1f8f826bd7f36baaee949ff66f3c8007243 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 29 Oct 2019 18:55:38 -0700 Subject: [PATCH 0119/1517] Publish to PyPI via github action (#247) --- .github/workflows/publish.yml | 23 +++++++++++++++++++++++ opentelemetry-api/setup.py | 1 + opentelemetry-sdk/setup.py | 1 + scripts/build.sh | 24 ++++++++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 .github/workflows/publish.yml create mode 100755 scripts/build.sh diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000..fca8fcca04 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,23 @@ +name: Publish + +on: + release: + types: [created] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-python@v1 + with: + python-version: '3.7' + - name: Build wheels + run: ./scripts/build.sh + - name: Publish to PyPI + env: + TWINE_USERNAME: '__token__' + TWINE_PASSWORD: ${{ secrets.pypi_password }} + run: | + pip install twine + twine upload --skip-existing --verbose dist/* diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index eff8230fc2..ee8adf26ae 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -43,6 +43,7 @@ description="OpenTelemetry Python API", include_package_data=True, long_description=open("README.rst").read(), + long_description_content_type="text/x-rst", install_requires=["typing; python_version<'3.5'"], extras_require={}, license="Apache-2.0", diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index fabf97cb39..82c839aea7 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -43,6 +43,7 @@ description="OpenTelemetry Python SDK", include_package_data=True, long_description=open("README.rst").read(), + long_description_content_type="text/x-rst", install_requires=["opentelemetry-api==0.1.dev0"], extras_require={}, license="Apache-2.0", diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000000..97af69babc --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# This script builds wheels for the API, SDK, and extension packages in the +# dist/ dir, to be uploaded to PyPI. + +set -ev + +# Get the latest versions of packaging tools +python3 -m pip install --upgrade pip setuptools wheel + +BASEDIR=$(dirname $(readlink -f $(dirname $0))) + +( + cd $BASEDIR + mkdir -p dist + rm -rf dist/* + + for d in opentelemetry-api/ opentelemetry-sdk/ ext/*/ ; do + ( + cd "$d" + python3 setup.py --verbose bdist_wheel --dist-dir "$BASEDIR/dist/" + ) + done +) From e53edd52c4634c9e24654d697f15b5d05f35fd0c Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 31 Oct 2019 12:04:01 -0700 Subject: [PATCH 0120/1517] Add warning to OT shim log/log_event calls (#255) --- .../setup.cfg | 1 + .../ext/opentracing_shim/__init__.py | 13 +++++++---- .../tests/test_shim.py | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg index c3b750f80f..a9daabee0b 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -39,6 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = + Deprecated >= 1.2.6 opentracing opentelemetry-api diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index 1a6479fc58..7ee8ad71e0 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -15,6 +15,7 @@ import logging import opentracing +from deprecated import deprecated from opentelemetry.ext.opentracing_shim import util @@ -77,6 +78,14 @@ def log_kv(self, key_values, timestamp=None): self._otel_span.add_event(event_name, event_timestamp, key_values) return self + @deprecated(reason="This method is deprecated in favor of log_kv") + def log(self, **kwargs): + super().log(**kwargs) + + @deprecated(reason="This method is deprecated in favor of log_kv") + def log_event(self, event, payload=None): + super().log_event(event, payload=payload) + def set_baggage_item(self, key, value): logger.warning( "Calling unimplemented method set_baggage_item() on class %s", @@ -91,10 +100,6 @@ def get_baggage_item(self, key): ) # TODO: Implement. - # TODO: Verify calls to deprecated methods `log_event()` and `log()` on - # base class work properly (it's probably fine because both methods call - # `log_kv()`). - class ScopeShim(opentracing.Scope): """A `ScopeShim` wraps the OpenTelemetry functionality related to span diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index b6691911dd..047687c911 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -406,6 +406,28 @@ def test_log_kv(self): # biggest possible loss of precision. self.assertAlmostEqual(result, now, places=6) + def test_log(self): + """Test the deprecated `log` method on `Span` objects.""" + + with self.shim.start_span("TestSpan") as span: + with self.assertWarns(DeprecationWarning): + span.log(event="foo", payload="bar") + + self.assertEqual(span.unwrap().events[0].attributes["event"], "foo") + self.assertEqual(span.unwrap().events[0].attributes["payload"], "bar") + self.assertIsNotNone(span.unwrap().events[0].timestamp) + + def test_log_event(self): + """Test the deprecated `log_event` method on `Span` objects.""" + + with self.shim.start_span("TestSpan") as span: + with self.assertWarns(DeprecationWarning): + span.log_event("foo", "bar") + + self.assertEqual(span.unwrap().events[0].attributes["event"], "foo") + self.assertEqual(span.unwrap().events[0].attributes["payload"], "bar") + self.assertIsNotNone(span.unwrap().events[0].timestamp) + def test_span_context(self): """Test construction of `SpanContextShim` objects.""" From 1ad88ef39bec4e6e2f768b833ea231b7e0055f55 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Sat, 2 Nov 2019 10:34:09 -0700 Subject: [PATCH 0121/1517] Bump version to 0.3.dev0 (#260) --- examples/opentelemetry-example-app/setup.py | 2 +- .../src/opentelemetry/ext/azure_monitor/version.py | 2 +- ext/opentelemetry-ext-http-requests/setup.cfg | 2 +- .../src/opentelemetry/ext/http_requests/version.py | 2 +- .../src/opentelemetry/ext/jaeger/version.py | 2 +- .../src/opentelemetry/ext/opentracing_shim/version.py | 2 +- .../src/opentelemetry/ext/wsgi/version.py | 2 +- opentelemetry-api/src/opentelemetry/util/version.py | 2 +- opentelemetry-sdk/setup.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/opentelemetry-example-app/setup.py b/examples/opentelemetry-example-app/setup.py index 4494d4ad0f..73a3e20784 100644 --- a/examples/opentelemetry-example-app/setup.py +++ b/examples/opentelemetry-example-app/setup.py @@ -16,7 +16,7 @@ setuptools.setup( name="opentelemetry-example-app", - version="0.1.dev0", + version="0.3.dev0", author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/version.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/version.py index a457c2b665..93ef792d05 100644 --- a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/version.py +++ b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.dev0" +__version__ = "0.3.dev0" diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg index 7d41a525cd..ceb5c13775 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.1.dev0 + opentelemetry-api >= 0.3.dev0 requests ~= 2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py index a457c2b665..93ef792d05 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.dev0" +__version__ = "0.3.dev0" diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index 262f246714..1f3a27dc85 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.dev0" +__version__ = "0.3.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index a457c2b665..93ef792d05 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.dev0" +__version__ = "0.3.dev0" diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index a457c2b665..93ef792d05 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.dev0" +__version__ = "0.3.dev0" diff --git a/opentelemetry-api/src/opentelemetry/util/version.py b/opentelemetry-api/src/opentelemetry/util/version.py index a457c2b665..93ef792d05 100644 --- a/opentelemetry-api/src/opentelemetry/util/version.py +++ b/opentelemetry-api/src/opentelemetry/util/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.dev0" +__version__ = "0.3.dev0" diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index 82c839aea7..f93b4b0f10 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -44,7 +44,7 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["opentelemetry-api==0.1.dev0"], + install_requires=["opentelemetry-api==0.3.dev0"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index a457c2b665..93ef792d05 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.dev0" +__version__ = "0.3.dev0" From 9290eaf27d9ba1b426a3358c8297c1c03ca913ab Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Mon, 4 Nov 2019 14:57:02 -0800 Subject: [PATCH 0122/1517] Do not use latest sphinx-autodoc-typehints (#267) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index e30cb1a14b..9f114201a5 100644 --- a/tox.ini +++ b/tox.ini @@ -106,7 +106,7 @@ commands = deps = sphinx~=2.1 sphinx-rtd-theme~=0.4 - sphinx-autodoc-typehints~=1.6 + sphinx-autodoc-typehints<=1.9 changedir = docs From e5ba7e7749106fbf986e9f4e4d2b359565e94a7a Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 5 Nov 2019 07:59:18 -0800 Subject: [PATCH 0123/1517] Metrics Console Exporter + Add last_updated_timestamp to metrics (#192) --- README.md | 30 ++++++++ .../src/opentelemetry/sdk/metrics/__init__.py | 15 ++++ .../sdk/metrics/export/__init__.py | 73 +++++++++++++++++++ .../tests/metrics/export/__init__.py | 13 ++++ .../tests/metrics/export/test_export.py | 40 ++++++++++ 5 files changed, 171 insertions(+) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py create mode 100644 opentelemetry-sdk/tests/metrics/export/__init__.py create mode 100644 opentelemetry-sdk/tests/metrics/export/test_export.py diff --git a/README.md b/README.md index 0d4548c923..8d45feb8ad 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ pip install -e ./ext/opentelemetry-ext-{integration} ## Quick Start +### Tracing + ```python from opentelemetry import trace from opentelemetry.context import Context @@ -67,6 +69,34 @@ with tracer.start_as_current_span('foo'): print(Context) ``` +### Metrics + +```python +from opentelemetry import metrics +from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter + +metrics.set_preferred_meter_implementation(lambda T: Meter()) +meter = metrics.meter() +exporter = ConsoleMetricsExporter() + +counter = meter.create_metric( + "available memory", + "available memory", + "bytes", + int, + Counter, + ("environment",), +) + +label_values = ("staging",) +counter_handle = counter.get_handle(label_values) +counter_handle.add(100) + +exporter.export([(counter, label_values)]) +exporter.shutdown() +``` + See the [API documentation](https://open-telemetry.github.io/opentelemetry-python/) for more detail, and the diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 0a941cd0d1..e6f5d53166 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -16,6 +16,7 @@ from typing import Sequence, Tuple, Type from opentelemetry import metrics as metrics_api +from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -31,6 +32,7 @@ def __init__( self.value_type = value_type self.enabled = enabled self.monotonic = monotonic + self.last_update_timestamp = time_ns() def _validate_update(self, value: metrics_api.ValueT) -> bool: if not self.enabled: @@ -42,6 +44,11 @@ def _validate_update(self, value: metrics_api.ValueT) -> bool: return False return True + def __repr__(self): + return '{}(data="{}", last_update_timestamp={})'.format( + type(self).__name__, self.data, self.last_update_timestamp + ) + class CounterHandle(metrics_api.CounterHandle, BaseHandle): def add(self, value: metrics_api.ValueT) -> None: @@ -50,6 +57,7 @@ def add(self, value: metrics_api.ValueT) -> None: if self.monotonic and value < 0: logger.warning("Monotonic counter cannot descend.") return + self.last_update_timestamp = time_ns() self.data += value @@ -60,6 +68,7 @@ def set(self, value: metrics_api.ValueT) -> None: if self.monotonic and value < self.data: logger.warning("Monotonic gauge cannot descend.") return + self.last_update_timestamp = time_ns() self.data = value @@ -70,6 +79,7 @@ def record(self, value: metrics_api.ValueT) -> None: if self.monotonic and value < 0: logger.warning("Monotonic measure cannot accept negatives.") return + self.last_update_timestamp = time_ns() # TODO: record @@ -107,6 +117,11 @@ def get_handle(self, label_values: Sequence[str]) -> BaseHandle: self.handles[label_values] = handle return handle + def __repr__(self): + return '{}(name="{}", description={})'.format( + type(self).__name__, self.name, self.description + ) + UPDATE_FUNCTION = lambda x, y: None # noqa: E731 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py new file mode 100644 index 0000000000..b6cb396331 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -0,0 +1,73 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum +from typing import Sequence, Tuple + +from .. import Metric + + +class MetricsExportResult(Enum): + SUCCESS = 0 + FAILED_RETRYABLE = 1 + FAILED_NOT_RETRYABLE = 2 + + +class MetricsExporter: + """Interface for exporting metrics. + + Interface to be implemented by services that want to export recorded + metrics in its own format. + """ + + def export( + self, metric_tuples: Sequence[Tuple[Metric, Sequence[str]]] + ) -> "MetricsExportResult": + """Exports a batch of telemetry data. + + Args: + metric_tuples: A sequence of metric pairs. A metric pair consists + of a `Metric` and a sequence of strings. The sequence of + strings will be used to get the corresponding `MetricHandle` + from the `Metric` to export. + + Returns: + The result of the export + """ + + def shutdown(self) -> None: + """Shuts down the exporter. + + Called when the SDK is shut down. + """ + + +class ConsoleMetricsExporter(MetricsExporter): + """Implementation of `MetricsExporter` that prints metrics to the console. + + This class can be used for diagnostic purposes. It prints the exported + metric handles to the console STDOUT. + """ + + def export( + self, metric_tuples: Sequence[Tuple[Metric, Sequence[str]]] + ) -> "MetricsExportResult": + for metric, label_values in metric_tuples: + handle = metric.get_handle(label_values) + print( + '{}(data="{}", label_values="{}", metric_data={})'.format( + type(self).__name__, metric, label_values, handle + ) + ) + return MetricsExportResult.SUCCESS diff --git a/opentelemetry-sdk/tests/metrics/export/__init__.py b/opentelemetry-sdk/tests/metrics/export/__init__.py new file mode 100644 index 0000000000..d853a7bcf6 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/export/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py new file mode 100644 index 0000000000..ca8e8a3631 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -0,0 +1,40 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from opentelemetry.sdk import metrics +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter + + +class TestConsoleMetricsExporter(unittest.TestCase): + # pylint: disable=no-self-use + def test_export(self): + exporter = ConsoleMetricsExporter() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + ("environment",), + ) + label_values = ("staging",) + handle = metric.get_handle(label_values) + result = '{}(data="{}", label_values="{}", metric_data={})'.format( + ConsoleMetricsExporter.__name__, metric, label_values, handle + ) + with mock.patch("sys.stdout") as mock_stdout: + exporter.export([(metric, label_values)]) + mock_stdout.write.assert_any_call(result) From 31213fca50f4fd18a0865a39582ed9c63eed513f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lipt=C3=A1k?= Date: Tue, 5 Nov 2019 11:54:20 -0500 Subject: [PATCH 0124/1517] Add Travis badge to README (#257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gábor Lipták --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8d45feb8ad..33ff5a8fc6 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [gitter-image]: https://badges.gitter.im/open-telemetry/opentelemetry-python.svg [gitter-url]: https://gitter.im/open-telemetry/opentelemetry-python?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge +[![Build Status](https://travis-ci.org/open-telemetry/opentelemetry-python.svg?branch=master)](https://travis-ci.org/open-telemetry/opentelemetry-python) The Python [OpenTelemetry](https://opentelemetry.io/) client. From f4cd5cca3c6fa4f2f66249fd171f548f3f8baabc Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 5 Nov 2019 14:34:16 -0800 Subject: [PATCH 0125/1517] README updates for alpha v2 release (#261) --- README.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 33ff5a8fc6..875a0cd4a8 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,14 @@ includes: - B3 Context Propagation - HTTP Integrations +The [v0.2 alpha +release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.2.0) +includes: + +- OpenTracing Bridge +- Jaeger Trace Exporter +- Trace Sampling + See the [project milestones](https://github.com/open-telemetry/opentelemetry-python/milestones) for details on upcoming releases. The dates and features described here are @@ -135,13 +143,6 @@ estimates, and subject to change. Future releases targets include: -| Component | Version | Target Date | -| --------------------------- | ---------- | --------------- | -| Jaeger Trace Exporter | Alpha v0.2 | October 28 2019 | -| Metrics SDK (Complete) | Alpha v0.2 | October 28 2019 | -| Prometheus Metrics Exporter | Alpha v0.2 | October 28 2019 | -| OpenTracing Bridge | Alpha v0.2 | October 28 2019 | - | Component | Version | Target Date | | ----------------------------------- | ---------- | ---------------- | | Zipkin Trace Exporter | Alpha v0.3 | November 15 2019 | @@ -149,7 +150,9 @@ Future releases targets include: | Support for Tags/Baggage | Alpha v0.3 | November 15 2019 | | Metrics Aggregation | Alpha v0.3 | November 15 2019 | | gRPC Integrations | Alpha v0.3 | November 15 2019 | +| Prometheus Metrics Exporter | Alpha v0.3 | November 15 2019 | -| Component | Version | Target Date | -| ----------------- | ---------- | ---------------- | -| OpenCensus Bridge | Alpha v0.4 | December 31 2019 | +| Component | Version | Target Date | +| ---------------------- | ---------- | ---------------- | +| OpenCensus Bridge | Alpha v0.4 | December 31 2019 | +| Metrics SDK (Complete) | Alpha v0.4 | December 31 2019 | From d231c531e02f68c20ab990877226eb3afe14c574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 5 Nov 2019 18:53:16 -0500 Subject: [PATCH 0126/1517] API: change order of arguments in add_event (#270) e4d89490e870 ("OpenTracing Bridge - Initial implementation (#211)") introduced a new timestamp argument to the add_event method. This commit moves that argument to be the last one because it is more common to have event attributes than an explicitly timestamp. --- .../ext/opentracing_shim/__init__.py | 2 +- .../src/opentelemetry/trace/__init__.py | 4 +- .../src/opentelemetry/sdk/trace/__init__.py | 4 +- opentelemetry-sdk/tests/trace/test_trace.py | 77 +++++++++++-------- 4 files changed, 50 insertions(+), 37 deletions(-) diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index 7ee8ad71e0..7271d79ea9 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -75,7 +75,7 @@ def log_kv(self, key_values, timestamp=None): event_timestamp = None event_name = util.event_name_from_kv(key_values) - self._otel_span.add_event(event_name, event_timestamp, key_values) + self._otel_span.add_event(event_name, key_values, event_timestamp) return self @deprecated(reason="This method is deprecated in favor of log_kv") diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 1ac7d73d49..412a106229 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -95,7 +95,7 @@ class Event: """A text annotation with a set of attributes.""" def __init__( - self, name: str, timestamp: int, attributes: types.Attributes = None + self, name: str, attributes: types.Attributes, timestamp: int ) -> None: self._name = name self._attributes = attributes @@ -182,8 +182,8 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: def add_event( self, name: str, - timestamp: int = None, attributes: types.Attributes = None, + timestamp: int = None, ) -> None: """Adds an `Event`. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0909b9b434..d06d34d93b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -202,14 +202,14 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: def add_event( self, name: str, - timestamp: int = None, attributes: types.Attributes = None, + timestamp: int = None, ) -> None: self.add_lazy_event( trace_api.Event( name, - time_ns() if timestamp is None else timestamp, Span.empty_attributes if attributes is None else attributes, + time_ns() if timestamp is None else timestamp, ) ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 626a5499ec..50249479a7 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -213,30 +213,15 @@ def test_start_as_current_span_explicit(self): class TestSpan(unittest.TestCase): + def setUp(self): + self.tracer = trace.Tracer("test_span") + def test_basic_span(self): span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) self.assertEqual(span.name, "name") - def test_span_members(self): - tracer = trace.Tracer("test_span_members") - - other_context1 = trace_api.SpanContext( - trace_id=trace.generate_trace_id(), - span_id=trace.generate_span_id(), - ) - other_context2 = trace_api.SpanContext( - trace_id=trace.generate_trace_id(), - span_id=trace.generate_span_id(), - ) - other_context3 = trace_api.SpanContext( - trace_id=trace.generate_trace_id(), - span_id=trace.generate_span_id(), - ) - - self.assertIsNone(tracer.get_current_span()) - - with tracer.start_as_current_span("root") as root: - # attributes + def test_attributes(self): + with self.tracer.start_as_current_span("root") as root: root.set_attribute("component", "http") root.set_attribute("http.method", "GET") root.set_attribute( @@ -263,30 +248,57 @@ def test_span_members(self): self.assertEqual(root.attributes["misc.pi"], 3.14) self.assertEqual(root.attributes["attr-key"], "attr-value2") - # events + def test_events(self): + self.assertIsNone(self.tracer.get_current_span()) + + with self.tracer.start_as_current_span("root") as root: + # only event name root.add_event("event0") + + # event name and attributes now = time_ns() - root.add_event( - "event1", timestamp=now, attributes={"name": "birthday"} - ) + root.add_event("event1", {"name": "pluto"}) + + # event name, attributes and timestamp + now = time_ns() + root.add_event("event2", {"name": "birthday"}, now) + + # lazy event root.add_lazy_event( - trace_api.Event("event2", now, {"name": "hello"}) + trace_api.Event("event3", {"name": "hello"}, now) ) - self.assertEqual(len(root.events), 3) + self.assertEqual(len(root.events), 4) self.assertEqual(root.events[0].name, "event0") self.assertEqual(root.events[0].attributes, {}) self.assertEqual(root.events[1].name, "event1") - self.assertEqual(root.events[1].attributes, {"name": "birthday"}) - self.assertEqual(root.events[1].timestamp, now) + self.assertEqual(root.events[1].attributes, {"name": "pluto"}) self.assertEqual(root.events[2].name, "event2") - self.assertEqual(root.events[2].attributes, {"name": "hello"}) + self.assertEqual(root.events[2].attributes, {"name": "birthday"}) self.assertEqual(root.events[2].timestamp, now) - # links + self.assertEqual(root.events[3].name, "event3") + self.assertEqual(root.events[3].attributes, {"name": "hello"}) + self.assertEqual(root.events[3].timestamp, now) + + def test_links(self): + other_context1 = trace_api.SpanContext( + trace_id=trace.generate_trace_id(), + span_id=trace.generate_span_id(), + ) + other_context2 = trace_api.SpanContext( + trace_id=trace.generate_trace_id(), + span_id=trace.generate_span_id(), + ) + other_context3 = trace_api.SpanContext( + trace_id=trace.generate_trace_id(), + span_id=trace.generate_span_id(), + ) + + with self.tracer.start_as_current_span("root") as root: root.add_link(other_context1) root.add_link(other_context2, {"name": "neighbor"}) root.add_lazy_link( @@ -313,6 +325,8 @@ def test_span_members(self): ) self.assertEqual(root.links[2].attributes, {"component": "http"}) + def test_update_name(self): + with self.tracer.start_as_current_span("root") as root: # name root.update_name("toor") self.assertEqual(root.name, "toor") @@ -359,14 +373,13 @@ def test_span_override_start_and_end_time(self): def test_ended_span(self): """"Events, attributes are not allowed after span is ended""" - tracer = trace.Tracer("test_ended_span") other_context1 = trace_api.SpanContext( trace_id=trace.generate_trace_id(), span_id=trace.generate_span_id(), ) - with tracer.start_as_current_span("root") as root: + with self.tracer.start_as_current_span("root") as root: # everything should be empty at the beginning self.assertEqual(len(root.attributes), 0) self.assertEqual(len(root.events), 0) From 72a331c3aae02d5aed1d8109e3388d412bfb0238 Mon Sep 17 00:00:00 2001 From: Aliaksei Urbanski Date: Thu, 7 Nov 2019 03:11:45 +0300 Subject: [PATCH 0127/1517] Add test coverage collecting (#128) --- .coveragerc | 5 ++ .gitignore | 1 + .travis.yml | 4 ++ .../opentelemetry/context/async_context.py | 52 ++++++++++--------- pytest.ini | 2 + scripts/coverage.sh | 27 ++++++++++ tox.ini | 21 +++++++- 7 files changed, 86 insertions(+), 26 deletions(-) create mode 100644 .coveragerc create mode 100644 pytest.ini create mode 100755 scripts/coverage.sh diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..6f2257aba5 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[run] +omit = + */tests/* + */setup.py + */gen/* diff --git a/.gitignore b/.gitignore index c9acc31940..473ef20a4a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ venv*/ pip-log.txt # Unit test / coverage reports +coverage.xml .coverage .nox .tox diff --git a/.travis.yml b/.travis.yml index 64eebc3621..f8c01c80be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,3 +19,7 @@ install: script: - tox + +after_success: + - pip install codecov + - codecov -v diff --git a/opentelemetry-api/src/opentelemetry/context/async_context.py b/opentelemetry-api/src/opentelemetry/context/async_context.py index e7337d103f..267059fb31 100644 --- a/opentelemetry-api/src/opentelemetry/context/async_context.py +++ b/opentelemetry-api/src/opentelemetry/context/async_context.py @@ -12,32 +12,34 @@ # See the License for the specific language governing permissions and # limitations under the License. -import typing # pylint: disable=unused-import -from contextvars import ContextVar +try: + from contextvars import ContextVar +except ImportError: + pass +else: + import typing # pylint: disable=unused-import + from . import base_context -from . import base_context + class AsyncRuntimeContext(base_context.BaseRuntimeContext): + class Slot(base_context.BaseRuntimeContext.Slot): + def __init__(self, name: str, default: object): + # pylint: disable=super-init-not-called + self.name = name + self.contextvar = ContextVar(name) # type: ContextVar[object] + self.default = base_context.wrap_callable( + default + ) # type: typing.Callable[..., object] + def clear(self) -> None: + self.contextvar.set(self.default()) -class AsyncRuntimeContext(base_context.BaseRuntimeContext): - class Slot(base_context.BaseRuntimeContext.Slot): - def __init__(self, name: str, default: "object"): - # pylint: disable=super-init-not-called - self.name = name - self.contextvar = ContextVar(name) # type: ContextVar[object] - self.default = base_context.wrap_callable( - default - ) # type: typing.Callable[..., object] + def get(self) -> object: + try: + return self.contextvar.get() + except LookupError: + value = self.default() + self.set(value) + return value - def clear(self) -> None: - self.contextvar.set(self.default()) - - def get(self) -> "object": - try: - return self.contextvar.get() - except LookupError: - value = self.default() - self.set(value) - return value - - def set(self, value: "object") -> None: - self.contextvar.set(value) + def set(self, value: object) -> None: + self.contextvar.set(value) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000..a71d000b56 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = -rs -v diff --git a/scripts/coverage.sh b/scripts/coverage.sh new file mode 100755 index 0000000000..bddf39a90c --- /dev/null +++ b/scripts/coverage.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e + +function cov { + pytest \ + --ignore-glob=*/setup.py \ + --cov ${1} \ + --cov-append \ + --cov-branch \ + --cov-report='' \ + ${1} +} + + +coverage erase + +cov opentelemetry-api +cov opentelemetry-sdk +cov ext/opentelemetry-ext-http-requests +cov ext/opentelemetry-ext-jaeger +cov ext/opentelemetry-ext-opentracing-shim +cov ext/opentelemetry-ext-wsgi +cov examples/opentelemetry-example-app + +coverage report +coverage xml diff --git a/tox.ini b/tox.ini index 9f114201a5..70afcb120c 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,11 @@ skip_missing_interpreters = True envlist = py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} + py3{4,5,6,7,8}-coverage + + ; Coverage is temporarily disabled for pypy3 due to the pytest bug. + ; pypy3-coverage + lint py37-tracecontext py37-{mypy,mypyinstalled} @@ -15,6 +20,8 @@ python = [testenv] deps = + test: pytest + coverage: pytest-cov mypy,mypyinstalled: mypy~=0.740 setenv = @@ -45,12 +52,24 @@ commands_pre = jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-opentracing-shim +; In order to get a healthy coverage report, +; we have to install packages in editable mode. + coverage: pip install -e {toxinidir}/opentelemetry-api + coverage: pip install -e {toxinidir}/opentelemetry-sdk + coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-azure-monitor + coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests + coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger + coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim + coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi + coverage: pip install -e {toxinidir}/examples/opentelemetry-example-app + ; Using file:// here because otherwise tox invokes just "pip install ; opentelemetry-api", leading to an error mypyinstalled: pip install file://{toxinidir}/opentelemetry-api/ commands = - test: python -m unittest discover + test: pytest + coverage: {toxinidir}/scripts/coverage.sh mypy: mypy --namespace-packages opentelemetry-api/src/opentelemetry/ ; For test code, we don't want to enforce the full mypy strictness From f51021572be4e24f0f37a2313dfcdd44364080bc Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 6 Nov 2019 16:42:39 -0800 Subject: [PATCH 0128/1517] Decentralizing the Python members (#268) --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 875a0cd4a8..9086623ae7 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,28 @@ for a complete example. See [CONTRIBUTING.md](CONTRIBUTING.md) +We meet weekly on Thursday at 8AM PT. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates. + +Meetings take place via [Zoom video conference](https://zoom.us/j/6729396170). + +Meeting notes are available as a public [Google doc](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit). For edit access, get in touch on [Gitter](https://gitter.im/open-telemetry/opentelemetry-python). + +Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): + +- [Carlos Alberto Cortez](https://github.com/carlosalberto), LightStep +- [Christian Neumüller](https://github.com/Oberon00), Dynatrace +- [Leighton Chen](https://github.com/lzchen), Microsoft +- [Yusuke Tsutsumi](https://github.com/toumorokoshi), Zillow Group + +*Find more about the approver role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver).* + +Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-telemetry/teams/python-maintainers)): + +- [Chris Kleinknecht](https://github.com/c24t), Google +- [Reiley Yang](https://github.com/reyang), Microsoft + +*Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).* + ## Release Schedule OpenTelemetry Python is under active development. From e95a115094159e2191169126518cd83b5174d1cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Thu, 7 Nov 2019 14:33:21 +0100 Subject: [PATCH 0129/1517] Use logger.exception to get traceback for export exception. (#269) --- .../src/opentelemetry/sdk/trace/export/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index ecdc93b0ad..442b2b2bac 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -77,8 +77,8 @@ def on_end(self, span: Span) -> None: try: self.span_exporter.export((span,)) # pylint: disable=broad-except - except Exception as exc: - logger.warning("Exception while exporting data: %s", exc) + except Exception: + logger.exception("Exception while exporting Span.") def shutdown(self) -> None: self.span_exporter.shutdown() @@ -191,7 +191,7 @@ def export(self) -> None: ) # type: ignore # pylint: disable=broad-except except Exception: - logger.exception("Exception while exporting data.") + logger.exception("Exception while exporting Span batch.") # clean up list for index in range(idx): From 9fd4ccd374f568e53850a564c1207b50e8534114 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 7 Nov 2019 15:13:18 -0800 Subject: [PATCH 0130/1517] Adding pymongo integration (#232) --- ext/opentelemetry-ext-pymongo/README.rst | 27 +++ ext/opentelemetry-ext-pymongo/setup.cfg | 46 +++++ ext/opentelemetry-ext-pymongo/setup.py | 26 +++ .../src/opentelemetry/ext/pymongo/__init__.py | 109 ++++++++++ .../src/opentelemetry/ext/pymongo/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/test_pymongo_integration.py | 187 ++++++++++++++++++ tox.ini | 9 +- 8 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 ext/opentelemetry-ext-pymongo/README.rst create mode 100644 ext/opentelemetry-ext-pymongo/setup.cfg create mode 100644 ext/opentelemetry-ext-pymongo/setup.py create mode 100644 ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py create mode 100644 ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py create mode 100644 ext/opentelemetry-ext-pymongo/tests/__init__.py create mode 100644 ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py diff --git a/ext/opentelemetry-ext-pymongo/README.rst b/ext/opentelemetry-ext-pymongo/README.rst new file mode 100644 index 0000000000..1e8011f4c2 --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/README.rst @@ -0,0 +1,27 @@ +OpenTelemetry pymongo integration +================================= + +The integration with MongoDB supports the `pymongo`_ library and is specified +to ``trace_integration`` using ``'pymongo'``. + +.. _pymongo: https://pypi.org/project/pymongo + +Usage +----- + +.. code:: python + + from pymongo import MongoClient + from opentelemetry.trace import tracer + from opentelemetry.trace.ext.pymongo import trace_integration + + trace_integration(tracer()) + client = MongoClient() + db = client["MongoDB_Database"] + collection = db["MongoDB_Collection"] + collection.find_one() + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg new file mode 100644 index 0000000000..f9362c75b3 --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -0,0 +1,46 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-pymongo +description = OpenTelemetry pymongo integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-pymongo +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api >= 0.3.dev0 + pymongo ~= 3.1 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-pymongo/setup.py b/ext/opentelemetry-ext-pymongo/setup.py new file mode 100644 index 0000000000..ed63ddf42d --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/setup.py @@ -0,0 +1,26 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "pymongo", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py new file mode 100644 index 0000000000..fa1cc1583e --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -0,0 +1,109 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The opentelemetry-ext-pymongo package allows tracing commands made by the +pymongo library. +""" + +from pymongo import monitoring + +from opentelemetry.trace import SpanKind +from opentelemetry.trace.status import Status, StatusCanonicalCode + +DATABASE_TYPE = "mongodb" +COMMAND_ATTRIBUTES = ["filter", "sort", "skip", "limit", "pipeline"] + + +def trace_integration(tracer=None): + """Integrate with pymongo to trace it using event listener. + https://api.mongodb.com/python/current/api/pymongo/monitoring.html + """ + + monitoring.register(CommandTracer(tracer)) + + +class CommandTracer(monitoring.CommandListener): + def __init__(self, tracer): + if tracer is None: + raise ValueError("The tracer is not provided.") + self._tracer = tracer + self._span_dict = {} + + def started(self, event: monitoring.CommandStartedEvent): + command = event.command.get(event.command_name, "") + name = DATABASE_TYPE + "." + event.command_name + statement = event.command_name + if command: + name += "." + command + statement += " " + command + + try: + span = self._tracer.start_span(name, kind=SpanKind.CLIENT) + span.set_attribute("component", DATABASE_TYPE) + span.set_attribute("db.type", DATABASE_TYPE) + span.set_attribute("db.instance", event.database_name) + span.set_attribute("db.statement", statement) + if event.connection_id is not None: + span.set_attribute("peer.hostname", event.connection_id[0]) + span.set_attribute("peer.port", event.connection_id[1]) + + # pymongo specific, not specified by spec + span.set_attribute("db.mongo.operation_id", event.operation_id) + span.set_attribute("db.mongo.request_id", event.request_id) + + for attr in COMMAND_ATTRIBUTES: + _attr = event.command.get(attr) + if _attr is not None: + span.set_attribute("db.mongo." + attr, str(_attr)) + + # Add Span to dictionary + self._span_dict[_get_span_dict_key(event)] = span + except Exception as ex: # noqa pylint: disable=broad-except + if span is not None: + span.set_status(Status(StatusCanonicalCode.INTERNAL, str(ex))) + span.end() + self._remove_span(event) + + def succeeded(self, event: monitoring.CommandSucceededEvent): + span = self._get_span(event) + if span is not None: + span.set_attribute( + "db.mongo.duration_micros", event.duration_micros + ) + span.set_status(Status(StatusCanonicalCode.OK, event.reply)) + span.end() + self._remove_span(event) + + def failed(self, event: monitoring.CommandFailedEvent): + span = self._get_span(event) + if span is not None: + span.set_attribute( + "db.mongo.duration_micros", event.duration_micros + ) + span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure)) + span.end() + self._remove_span(event) + + def _get_span(self, event): + return self._span_dict.get(_get_span_dict_key(event)) + + def _remove_span(self, event): + self._span_dict.pop(_get_span_dict_key(event)) + + +def _get_span_dict_key(event): + if event.connection_id is not None: + return (event.request_id, event.connection_id) + return event.request_id diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py new file mode 100644 index 0000000000..93ef792d05 --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.3.dev0" diff --git a/ext/opentelemetry-ext-pymongo/tests/__init__.py b/ext/opentelemetry-ext-pymongo/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py new file mode 100644 index 0000000000..95f0ae3413 --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py @@ -0,0 +1,187 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from opentelemetry import trace as trace_api +from opentelemetry.ext.pymongo import CommandTracer, trace_integration +from opentelemetry.util import time_ns + + +class TestPymongoIntegration(unittest.TestCase): + def test_trace_integration(self): + mock_register = mock.Mock() + patch = mock.patch( + "pymongo.monitoring.register", side_effect=mock_register + ) + mock_tracer = MockTracer() + with patch: + trace_integration(mock_tracer) + + self.assertTrue(mock_register.called) + + def test_started(self): + command_attrs = { + "filter": "filter", + "sort": "sort", + "limit": "limit", + "pipeline": "pipeline", + "command_name": "find", + } + mock_tracer = MockTracer() + command_tracer = CommandTracer(mock_tracer) + mock_event = MockEvent( + command_attrs, ("test.com", "1234"), "test_request_id" + ) + command_tracer.started(event=mock_event) + # pylint: disable=protected-access + span = command_tracer._get_span(mock_event) + self.assertIs(span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual(span.name, "mongodb.command_name.find") + self.assertEqual(span.attributes["component"], "mongodb") + self.assertEqual(span.attributes["db.type"], "mongodb") + self.assertEqual(span.attributes["db.instance"], "database_name") + self.assertEqual(span.attributes["db.statement"], "command_name find") + self.assertEqual(span.attributes["peer.hostname"], "test.com") + self.assertEqual(span.attributes["peer.port"], "1234") + self.assertEqual( + span.attributes["db.mongo.operation_id"], "operation_id" + ) + self.assertEqual( + span.attributes["db.mongo.request_id"], "test_request_id" + ) + + self.assertEqual(span.attributes["db.mongo.filter"], "filter") + self.assertEqual(span.attributes["db.mongo.sort"], "sort") + self.assertEqual(span.attributes["db.mongo.limit"], "limit") + self.assertEqual(span.attributes["db.mongo.pipeline"], "pipeline") + + def test_succeeded(self): + mock_tracer = MockTracer() + mock_event = MockEvent({}) + command_tracer = CommandTracer(mock_tracer) + command_tracer.started(event=mock_event) + # pylint: disable=protected-access + span = command_tracer._get_span(mock_event) + command_tracer.succeeded(event=mock_event) + self.assertEqual( + span.attributes["db.mongo.duration_micros"], "duration_micros" + ) + self.assertIs( + span.status.canonical_code, trace_api.status.StatusCanonicalCode.OK + ) + self.assertEqual(span.status.description, "reply") + self.assertIsNotNone(span.end_time) + + def test_failed(self): + mock_tracer = MockTracer() + mock_event = MockEvent({}) + command_tracer = CommandTracer(mock_tracer) + command_tracer.started(event=mock_event) + # pylint: disable=protected-access + span = command_tracer._get_span(mock_event) + command_tracer.failed(event=mock_event) + self.assertEqual( + span.attributes["db.mongo.duration_micros"], "duration_micros" + ) + self.assertIs( + span.status.canonical_code, + trace_api.status.StatusCanonicalCode.UNKNOWN, + ) + self.assertEqual(span.status.description, "failure") + self.assertIsNotNone(span.end_time) + + def test_multiple_commands(self): + mock_tracer = MockTracer() + first_mock_event = MockEvent({}, ("firstUrl", "123"), "first") + second_mock_event = MockEvent({}, ("secondUrl", "456"), "second") + command_tracer = CommandTracer(mock_tracer) + command_tracer.started(event=first_mock_event) + # pylint: disable=protected-access + first_span = command_tracer._get_span(first_mock_event) + command_tracer.started(event=second_mock_event) + # pylint: disable=protected-access + second_span = command_tracer._get_span(second_mock_event) + command_tracer.succeeded(event=first_mock_event) + command_tracer.failed(event=second_mock_event) + + self.assertEqual(first_span.attributes["db.mongo.request_id"], "first") + self.assertIs( + first_span.status.canonical_code, + trace_api.status.StatusCanonicalCode.OK, + ) + self.assertEqual( + second_span.attributes["db.mongo.request_id"], "second" + ) + self.assertIs( + second_span.status.canonical_code, + trace_api.status.StatusCanonicalCode.UNKNOWN, + ) + + +class MockCommand: + def __init__(self, command_attrs): + self.command_attrs = command_attrs + + def get(self, key, default=""): + return self.command_attrs.get(key, default) + + +class MockEvent: + def __init__(self, command_attrs, connection_id=None, request_id=""): + self.command = MockCommand(command_attrs) + self.connection_id = connection_id + self.request_id = request_id + + def __getattr__(self, item): + return item + + +class MockSpan: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + return False + + def __init__(self): + self.status = None + self.name = "" + self.kind = trace_api.SpanKind.INTERNAL + self.attributes = None + self.end_time = None + + def set_attribute(self, key, value): + self.attributes[key] = value + + def set_status(self, status): + self.status = status + + def end(self, end_time=None): + self.end_time = end_time if end_time is not None else time_ns() + + +class MockTracer: + def __init__(self): + self.end_span = mock.Mock() + + # pylint: disable=no-self-use + def start_span(self, name, kind): + span = MockSpan() + span.attributes = {} + span.status = None + span.name = name + span.kind = kind + return span diff --git a/tox.ini b/tox.ini index 70afcb120c..600d6b86c5 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} - pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} + pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} py3{4,5,6,7,8}-coverage ; Coverage is temporarily disabled for pypy3 due to the pytest bug. @@ -32,6 +32,7 @@ changedir = test-sdk: opentelemetry-sdk/tests test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests + test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-example-app: examples/opentelemetry-example-app/tests test-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests @@ -47,6 +48,7 @@ commands_pre = example-app: pip install {toxinidir}/examples/opentelemetry-example-app ext: pip install {toxinidir}/opentelemetry-api wsgi: pip install {toxinidir}/ext/opentelemetry-ext-wsgi + pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests jaeger: pip install {toxinidir}/opentelemetry-sdk jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger @@ -94,6 +96,7 @@ commands_pre = pip install -e {toxinidir}/ext/opentelemetry-ext-azure-monitor pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger + pip install -e {toxinidir}/ext/opentelemetry-ext-pymongo pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi pip install -e {toxinidir}/examples/opentelemetry-example-app pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim @@ -115,6 +118,8 @@ commands = ext/opentelemetry-ext-jaeger/tests/ \ ext/opentelemetry-ext-opentracing-shim/src/ \ ext/opentelemetry-ext-opentracing-shim/tests/ \ + ext/opentelemetry-ext-pymongo/src/opentelemetry \ + ext/opentelemetry-ext-pymongo/tests/ \ ext/opentelemetry-ext-wsgi/tests/ \ examples/opentelemetry-example-app/src/opentelemetry_example_app/ \ examples/opentelemetry-example-app/tests/ From ff63d8c335c31d9e130e8c1ef1992b6ee6da492f Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sat, 9 Nov 2019 09:52:44 +0800 Subject: [PATCH 0131/1517] Fix version string 0.2a.0 -> 0.2a0 (#279) --- ext/opentelemetry-ext-jaeger/CHANGELOG.md | 2 +- ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/opentelemetry-ext-jaeger/CHANGELOG.md b/ext/opentelemetry-ext-jaeger/CHANGELOG.md index 73350ef7ad..f059e90f5a 100644 --- a/ext/opentelemetry-ext-jaeger/CHANGELOG.md +++ b/ext/opentelemetry-ext-jaeger/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -## 0.2a.0 +## 0.2a0 Released 2019-10-29 diff --git a/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md b/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md index 73350ef7ad..f059e90f5a 100644 --- a/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md +++ b/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -## 0.2a.0 +## 0.2a0 Released 2019-10-29 From 3bbcfe5748aeb8309ae9b8181230a91f493e8d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 12 Nov 2019 13:55:10 -0500 Subject: [PATCH 0132/1517] Add pypi badges and installation instructions to all ext packages (#281) --- README.md | 7 ++----- ext/opentelemetry-ext-http-requests/README.rst | 14 +++++++++++++- ext/opentelemetry-ext-jaeger/README.rst | 7 +++++++ ext/opentelemetry-ext-wsgi/README.rst | 13 +++++++++++++ 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9086623ae7..c7b9a837c8 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ # OpenTelemetry Python -[![Gitter chat][gitter-image]][gitter-url] - -[gitter-image]: https://badges.gitter.im/open-telemetry/opentelemetry-python.svg -[gitter-url]: https://gitter.im/open-telemetry/opentelemetry-python?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge -[![Build Status](https://travis-ci.org/open-telemetry/opentelemetry-python.svg?branch=master)](https://travis-ci.org/open-telemetry/opentelemetry-python) +[![Gitter chat](https://img.shields.io/gitter/room/opentelemetry/opentelemetry-python)](https://gitter.im/open-telemetry/opentelemetry-python) +[![Build status](https://travis-ci.org/open-telemetry/opentelemetry-python.svg?branch=master)](https://travis-ci.org/open-telemetry/opentelemetry-python) The Python [OpenTelemetry](https://opentelemetry.io/) client. diff --git a/ext/opentelemetry-ext-http-requests/README.rst b/ext/opentelemetry-ext-http-requests/README.rst index e0223572f3..33759f5cec 100644 --- a/ext/opentelemetry-ext-http-requests/README.rst +++ b/ext/opentelemetry-ext-http-requests/README.rst @@ -1,7 +1,19 @@ OpenTelemetry requests integration ================================== -This library allows tracing HTTP requests made by the popular `requests <(https://2.python-requests.org//en/latest/>` library. +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-http-requests.svg + :target: https://pypi.org/project/opentelemetry-ext-http-requests/ + +This library allows tracing HTTP requests made by the popular `requests `_ library. + +Installation +------------ + +:: + + pip install opentelemetry-ext-http-requests Usage ----- diff --git a/ext/opentelemetry-ext-jaeger/README.rst b/ext/opentelemetry-ext-jaeger/README.rst index 04ccdfac4a..321e993be1 100644 --- a/ext/opentelemetry-ext-jaeger/README.rst +++ b/ext/opentelemetry-ext-jaeger/README.rst @@ -1,6 +1,13 @@ OpenTelemetry Jaeger Exporter ============================= +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-jaeger.svg + :target: https://pypi.org/project/opentelemetry-ext-jaeger/ + +This library allows to export tracing data to `Jaeger `_. + Installation ------------ diff --git a/ext/opentelemetry-ext-wsgi/README.rst b/ext/opentelemetry-ext-wsgi/README.rst index d47643c871..e14d2dd05a 100644 --- a/ext/opentelemetry-ext-wsgi/README.rst +++ b/ext/opentelemetry-ext-wsgi/README.rst @@ -1,9 +1,22 @@ OpenTelemetry WSGI Middleware ============================= +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-wsgi.svg + :target: https://pypi.org/project/opentelemetry-opentracing-wsgi/ + + This library provides a WSGI middleware that can be used on any WSGI framework (such as Django / Flask) to track requests timing through OpenTelemetry. +Installation +------------ + +:: + + pip install opentelemetry-opentracing-wsgi + Usage (Flask) ------------- From 850a7c2ea571f79804635e498ba847e1c5d9db05 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 12 Nov 2019 10:58:18 -0800 Subject: [PATCH 0133/1517] Bump sphinx-autodoc-typehints to 1.10.2 (#271) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 600d6b86c5..e0e076fe77 100644 --- a/tox.ini +++ b/tox.ini @@ -130,7 +130,7 @@ commands = deps = sphinx~=2.1 sphinx-rtd-theme~=0.4 - sphinx-autodoc-typehints<=1.9 + sphinx-autodoc-typehints~=1.10.2 changedir = docs From fcdd9fe1524971f4d60fda6c6f0a74ff59d214cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 12 Nov 2019 14:00:49 -0500 Subject: [PATCH 0134/1517] ext/opentracing-shim: implement inject and extract (#256) This commit implements inject() and extract() support for TEXT_MAP and HTTP_HEADERS formats by using the configured OpenTelemetry propagators. The support for binary propagators is not completed on opentelemetry-python so this commit does not include for such format. --- .../CHANGELOG.md | 2 + .../ext/opentracing_shim/__init__.py | 39 +++++-- .../tests/test_shim.py | 102 +++++++++++++++++- 3 files changed, 133 insertions(+), 10 deletions(-) diff --git a/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md b/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md index f059e90f5a..8fe7c21469 100644 --- a/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md +++ b/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Implement extract and inject support for HTTP_HEADERS and TEXT_MAP formats. + ## 0.2a0 Released 2019-10-29 diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index 7271d79ea9..dae4d84865 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -17,6 +17,7 @@ import opentracing from deprecated import deprecated +from opentelemetry import propagators from opentelemetry.ext.opentracing_shim import util logger = logging.getLogger(__name__) @@ -184,6 +185,10 @@ class TracerShim(opentracing.Tracer): def __init__(self, tracer): super().__init__(scope_manager=ScopeManagerShim(self)) self._otel_tracer = tracer + self._supported_formats = ( + opentracing.Format.TEXT_MAP, + opentracing.Format.HTTP_HEADERS, + ) def unwrap(self): """Returns the wrapped OpenTelemetry `Tracer` object.""" @@ -249,16 +254,32 @@ def start_span( def inject(self, span_context, format, carrier): # pylint: disable=redefined-builtin - logger.warning( - "Calling unimplemented method inject() on class %s", - self.__class__.__name__, + # This implementation does not perform the injecting by itself but + # uses the configured propagators in opentelemetry.propagators. + # TODO: Support Format.BINARY once it is supported in + # opentelemetry-python. + if format not in self._supported_formats: + raise opentracing.UnsupportedFormatException + + propagator = propagators.get_global_httptextformat() + propagator.inject( + span_context.unwrap(), type(carrier).__setitem__, carrier ) - # TODO: Implement. def extract(self, format, carrier): # pylint: disable=redefined-builtin - logger.warning( - "Calling unimplemented method extract() on class %s", - self.__class__.__name__, - ) - # TODO: Implement. + # This implementation does not perform the extracing by itself but + # uses the configured propagators in opentelemetry.propagators. + # TODO: Support Format.BINARY once it is supported in + # opentelemetry-python. + if format not in self._supported_formats: + raise opentracing.UnsupportedFormatException + + def get_as_list(dict_object, key): + value = dict_object.get(key) + return [value] if value is not None else [] + + propagator = propagators.get_global_httptextformat() + otel_context = propagator.extract(get_as_list, carrier) + + return SpanContextShim(otel_context) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index 047687c911..b1cefbcbb4 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -18,7 +18,8 @@ import opentracing import opentelemetry.ext.opentracing_shim as opentracingshim -from opentelemetry import trace +from opentelemetry import propagators, trace +from opentelemetry.context.propagation.httptextformat import HTTPTextFormat from opentelemetry.ext.opentracing_shim import util from opentelemetry.sdk.trace import Tracer @@ -40,6 +41,17 @@ def setUpClass(cls): trace.set_preferred_tracer_implementation(lambda T: Tracer()) + # Save current propagator to be restored on teardown. + cls._previous_propagator = propagators.get_global_httptextformat() + + # Set mock propagator for testing. + propagators.set_global_httptextformat(MockHTTPTextFormat) + + @classmethod + def tearDownClass(cls): + # Restore previous propagator. + propagators.set_global_httptextformat(cls._previous_propagator) + def test_shim_type(self): # Verify shim is an OpenTracing tracer. self.assertIsInstance(self.shim, opentracing.Tracer) @@ -453,3 +465,91 @@ def test_span_on_error(self): self.assertEqual( scope.span.unwrap().events[0].attributes["error.kind"], Exception ) + + def test_inject_http_headers(self): + """Test `inject()` method for Format.HTTP_HEADERS.""" + + otel_context = trace.SpanContext(trace_id=1220, span_id=7478) + context = opentracingshim.SpanContextShim(otel_context) + + headers = {} + self.shim.inject(context, opentracing.Format.HTTP_HEADERS, headers) + self.assertEqual(headers[MockHTTPTextFormat.TRACE_ID_KEY], str(1220)) + self.assertEqual(headers[MockHTTPTextFormat.SPAN_ID_KEY], str(7478)) + + def test_inject_text_map(self): + """Test `inject()` method for Format.TEXT_MAP.""" + + otel_context = trace.SpanContext(trace_id=1220, span_id=7478) + context = opentracingshim.SpanContextShim(otel_context) + + # Verify Format.TEXT_MAP + text_map = {} + self.shim.inject(context, opentracing.Format.TEXT_MAP, text_map) + self.assertEqual(text_map[MockHTTPTextFormat.TRACE_ID_KEY], str(1220)) + self.assertEqual(text_map[MockHTTPTextFormat.SPAN_ID_KEY], str(7478)) + + def test_inject_binary(self): + """Test `inject()` method for Format.BINARY.""" + + otel_context = trace.SpanContext(trace_id=1220, span_id=7478) + context = opentracingshim.SpanContextShim(otel_context) + + # Verify exception for non supported binary format. + with self.assertRaises(opentracing.UnsupportedFormatException): + self.shim.inject(context, opentracing.Format.BINARY, bytearray()) + + def test_extract_http_headers(self): + """Test `extract()` method for Format.HTTP_HEADERS.""" + + carrier = { + MockHTTPTextFormat.TRACE_ID_KEY: 1220, + MockHTTPTextFormat.SPAN_ID_KEY: 7478, + } + + ctx = self.shim.extract(opentracing.Format.HTTP_HEADERS, carrier) + self.assertEqual(ctx.unwrap().trace_id, 1220) + self.assertEqual(ctx.unwrap().span_id, 7478) + + def test_extract_text_map(self): + """Test `extract()` method for Format.TEXT_MAP.""" + + carrier = { + MockHTTPTextFormat.TRACE_ID_KEY: 1220, + MockHTTPTextFormat.SPAN_ID_KEY: 7478, + } + + ctx = self.shim.extract(opentracing.Format.TEXT_MAP, carrier) + self.assertEqual(ctx.unwrap().trace_id, 1220) + self.assertEqual(ctx.unwrap().span_id, 7478) + + def test_extract_binary(self): + """Test `extract()` method for Format.BINARY.""" + + # Verify exception for non supported binary format. + with self.assertRaises(opentracing.UnsupportedFormatException): + self.shim.extract(opentracing.Format.BINARY, bytearray()) + + +class MockHTTPTextFormat(HTTPTextFormat): + """Mock propagator for testing purposes.""" + + TRACE_ID_KEY = "mock-traceid" + SPAN_ID_KEY = "mock-spanid" + + @classmethod + def extract(cls, get_from_carrier, carrier): + trace_id_list = get_from_carrier(carrier, cls.TRACE_ID_KEY) + span_id_list = get_from_carrier(carrier, cls.SPAN_ID_KEY) + + if not trace_id_list or not span_id_list: + return trace.INVALID_SPAN_CONTEXT + + return trace.SpanContext( + trace_id=int(trace_id_list[0]), span_id=int(span_id_list[0]) + ) + + @classmethod + def inject(cls, context, set_in_carrier, carrier): + set_in_carrier(carrier, cls.TRACE_ID_KEY, str(context.trace_id)) + set_in_carrier(carrier, cls.SPAN_ID_KEY, str(context.span_id)) From b1c28bb77afd37fde75899e49ffd60d63e567eb1 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 15 Nov 2019 13:45:51 -0800 Subject: [PATCH 0135/1517] Skip pytest 5.2.3 (#293) --- tox.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index e0e076fe77..30e1d3745b 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,10 @@ python = [testenv] deps = - test: pytest + ; skip 5.2.3 pytest due to bug + ; https://github.com/pytest-dev/pytest/issues/6194 + test: pytest!=5.2.3 + coverage: pytest!=5.2.3 coverage: pytest-cov mypy,mypyinstalled: mypy~=0.740 From d5b580f0d05e301b9eee0a9ddd44fdcc7ae01047 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sat, 16 Nov 2019 06:12:33 +0800 Subject: [PATCH 0136/1517] Fix typo, wording (#289) --- CONTRIBUTING.md | 6 +++--- ext/opentelemetry-ext-jaeger/README.rst | 2 +- ext/opentelemetry-ext-wsgi/README.rst | 6 +++--- .../context/propagation/tracecontexthttptextformat.py | 2 +- opentelemetry-api/src/opentelemetry/trace/__init__.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74cc345874..e84971fa4a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ If you are looking for someone to help you find a starting point and be a resour Gitter and find a buddy! 1. Join [Gitter.im](https://gitter.im) and join our [chat room](https://gitter.im/open-telemetry/opentelemetry-python). -2. Post in the room with an introduction to yourself, what area you are interested in (checks issues marked "Help Wanted"), +2. Post in the room with an introduction to yourself, what area you are interested in (check issues marked "Help Wanted"), and say you are looking for a buddy. We will match you with someone who has experience in that area. Your OpenTelemetry buddy is your resource to talk to directly on all aspects of contributing to OpenTelemetry: providing @@ -37,7 +37,7 @@ You can run: under multiple Python versions - `tox -e docs` to regenerate the API docs - `tox -e test-api` and `tox -e test-sdk` to run the API and SDK unit tests -- `tox -e py37-test-api` to e.g. run the the API unit tests under a specific +- `tox -e py37-test-api` to e.g. run the API unit tests under a specific Python version - `tox -e lint` to run lint checks on all code @@ -115,7 +115,7 @@ It's especially valuable to read through the [library guidelines](https://github OpenTelemetry is an evolving specification, one where the desires and use cases are clear, but the method to satisfy those uses cases are not. -As such, Contributions should provide functionality and behavior that +As such, contributions should provide functionality and behavior that conforms to the specification, but the interface and structure is flexible. It is preferable to have contributions follow the idioms of the language diff --git a/ext/opentelemetry-ext-jaeger/README.rst b/ext/opentelemetry-ext-jaeger/README.rst index 321e993be1..a03d1b0c1e 100644 --- a/ext/opentelemetry-ext-jaeger/README.rst +++ b/ext/opentelemetry-ext-jaeger/README.rst @@ -26,7 +26,7 @@ gRPC is still not supported by this implementation. .. _Jaeger: https://www.jaegertracing.io/ -.. _OpenTelemetry: https://github.com/opentelemetry/opentelemetry-python/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. code:: python diff --git a/ext/opentelemetry-ext-wsgi/README.rst b/ext/opentelemetry-ext-wsgi/README.rst index e14d2dd05a..82641bcaa4 100644 --- a/ext/opentelemetry-ext-wsgi/README.rst +++ b/ext/opentelemetry-ext-wsgi/README.rst @@ -3,8 +3,8 @@ OpenTelemetry WSGI Middleware |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-wsgi.svg - :target: https://pypi.org/project/opentelemetry-opentracing-wsgi/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-wsgi.svg + :target: https://pypi.org/project/opentelemetry-ext-wsgi/ This library provides a WSGI middleware that can be used on any WSGI framework @@ -15,7 +15,7 @@ Installation :: - pip install opentelemetry-opentracing-wsgi + pip install opentelemetry-ext-wsgi Usage (Flask) diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py index 20f2601fa2..5d00632ed1 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py @@ -21,7 +21,7 @@ _T = typing.TypeVar("_T") # Keys and values are strings of up to 256 printable US-ASCII characters. -# Implementations should conform to the the `W3C Trace Context - Tracestate`_ +# Implementations should conform to the `W3C Trace Context - Tracestate`_ # spec, which describes additional restrictions on valid field values. # # .. _W3C Trace Context - Tracestate: diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 412a106229..74ee0f5dd1 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -135,7 +135,7 @@ class SpanKind(enum.Enum): #: path latency relationship between producer and consumer spans. PRODUCER = 3 - #: Indicates that the span describes consumer receiving a message from a + #: Indicates that the span describes a consumer receiving a message from a #: broker. Unlike client and server, there is usually no direct critical #: path latency relationship between producer and consumer spans. CONSUMER = 4 @@ -285,7 +285,7 @@ class TraceState(typing.Dict[str, str]): """A list of key-value pairs representing vendor-specific trace info. Keys and values are strings of up to 256 printable US-ASCII characters. - Implementations should conform to the the `W3C Trace Context - Tracestate`_ + Implementations should conform to the `W3C Trace Context - Tracestate`_ spec, which describes additional restrictions on valid field values. .. _W3C Trace Context - Tracestate: From 30a1bb8e9af2abb960eec9388a99ce8cbb7420b1 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 15 Nov 2019 18:09:34 -0800 Subject: [PATCH 0137/1517] Removing add_link and add_lazy_link from api/sdk (#259) Prevents adding links after spans are created for OTEP 0006. --- .../examples/jaeger_exporter_example.py | 5 +- .../ext/opentracing_shim/__init__.py | 9 ++- .../src/opentelemetry/trace/__init__.py | 34 +++++------ .../src/opentelemetry/trace/sampling.py | 5 +- .../src/opentelemetry/sdk/trace/__init__.py | 48 ++++++--------- opentelemetry-sdk/tests/trace/test_trace.py | 61 ++++++++++++++----- 6 files changed, 95 insertions(+), 67 deletions(-) diff --git a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py index 5cf57bfcca..89d93809e4 100644 --- a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py +++ b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py @@ -33,10 +33,11 @@ time.sleep(0.1) foo.set_attribute("my_atribbute", True) foo.add_event("event in foo", {"name": "foo1"}) - with tracer.start_as_current_span("bar") as bar: + with tracer.start_as_current_span( + "bar", links=[trace.Link(foo.get_context())] + ) as bar: time.sleep(0.2) bar.set_attribute("speed", 100.0) - bar.add_link(foo.get_context()) with tracer.start_as_current_span("baz") as baz: time.sleep(0.3) diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index dae4d84865..eefb85466a 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -17,6 +17,7 @@ import opentracing from deprecated import deprecated +import opentelemetry.trace as trace_api from opentelemetry import propagators from opentelemetry.ext.opentracing_shim import util @@ -231,11 +232,15 @@ def start_span( # Use the specified parent or the active span if possible. Otherwise, # use a `None` parent, which triggers the creation of a new trace. parent = child_of.unwrap() if child_of else None - span = self._otel_tracer.create_span(operation_name, parent) + links = [] if references: for ref in references: - span.add_link(ref.referenced_context.unwrap()) + links.append(trace_api.Link(ref.referenced_context.unwrap())) + + span = self._otel_tracer.create_span( + operation_name, parent, links=links + ) if tags: for key, value in tags.items(): diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 74ee0f5dd1..4f1dc539a3 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -80,7 +80,10 @@ def __init__( self, context: "SpanContext", attributes: types.Attributes = None ) -> None: self._context = context - self._attributes = attributes + if attributes is None: + self._attributes = {} # type: types.Attributes + else: + self._attributes = attributes @property def context(self) -> "SpanContext": @@ -198,23 +201,6 @@ def add_lazy_event(self, event: Event) -> None: Adds an `Event` that has previously been created. """ - def add_link( - self, - link_target_context: "SpanContext", - attributes: types.Attributes = None, - ) -> None: - """Adds a `Link` to another span. - - Adds a single `Link` from this Span to another Span identified by the - `SpanContext` passed as argument. - """ - - def add_lazy_link(self, link: "Link") -> None: - """Adds a `Link` to another span. - - Adds a `Link` that has previously been created. - """ - def update_name(self, name: str) -> None: """Updates the `Span` name. @@ -416,6 +402,8 @@ def start_span( name: str, parent: ParentSpan = CURRENT_SPAN, kind: SpanKind = SpanKind.INTERNAL, + attributes: typing.Optional[types.Attributes] = None, + links: typing.Sequence[Link] = (), ) -> "Span": """Starts a span. @@ -444,6 +432,8 @@ def start_span( parent: The span's parent. Defaults to the current span. kind: The span's kind (relationship to parent). Note that is meaningful even if there is no parent. + attributes: The span's attributes. + links: Links span to other spans Returns: The newly-created span. @@ -457,6 +447,8 @@ def start_as_current_span( name: str, parent: ParentSpan = CURRENT_SPAN, kind: SpanKind = SpanKind.INTERNAL, + attributes: typing.Optional[types.Attributes] = None, + links: typing.Sequence[Link] = (), ) -> typing.Iterator["Span"]: """Context manager for creating a new span and set it as the current span in this tracer's context. @@ -492,6 +484,8 @@ def start_as_current_span( parent: The span's parent. Defaults to the current span. kind: The span's kind (relationship to parent). Note that is meaningful even if there is no parent. + attributes: The span's attributes. + links: Links span to other spans Yields: The newly-created span. @@ -505,6 +499,8 @@ def create_span( name: str, parent: ParentSpan = CURRENT_SPAN, kind: SpanKind = SpanKind.INTERNAL, + attributes: typing.Optional[types.Attributes] = None, + links: typing.Sequence[Link] = (), ) -> "Span": """Creates a span. @@ -534,6 +530,8 @@ def create_span( parent: The span's parent. Defaults to the current span. kind: The span's kind (relationship to parent). Note that is meaningful even if there is no parent. + attributes: The span's attributes. + links: Links span to other spans Returns: The newly-created span. diff --git a/opentelemetry-api/src/opentelemetry/trace/sampling.py b/opentelemetry-api/src/opentelemetry/trace/sampling.py index f16e80495b..967f4fd46b 100644 --- a/opentelemetry-api/src/opentelemetry/trace/sampling.py +++ b/opentelemetry-api/src/opentelemetry/trace/sampling.py @@ -17,7 +17,7 @@ # pylint: disable=unused-import from opentelemetry.trace import Link, SpanContext -from opentelemetry.util.types import AttributeValue +from opentelemetry.util.types import Attributes, AttributeValue class Decision: @@ -53,6 +53,7 @@ def should_sample( trace_id: int, span_id: int, name: str, + attributes: Optional[Attributes] = None, links: Sequence["Link"] = (), ) -> "Decision": pass @@ -70,6 +71,7 @@ def should_sample( trace_id: int, span_id: int, name: str, + attributes: Optional[Attributes] = None, links: Sequence["Link"] = (), ) -> "Decision": return self._decision @@ -107,6 +109,7 @@ def should_sample( trace_id: int, span_id: int, name: str, + attributes: Optional[Attributes] = None, # TODO links: Sequence["Link"] = (), ) -> "Decision": if parent_context is not None: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index d06d34d93b..9ac9d81bc2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -130,7 +130,7 @@ def __init__( resource: None = None, # TODO attributes: types.Attributes = None, # TODO events: Sequence[trace_api.Event] = None, # TODO - links: Sequence[trace_api.Link] = None, # TODO + links: Sequence[trace_api.Link] = (), kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, span_processor: SpanProcessor = SpanProcessor(), ) -> None: @@ -226,30 +226,6 @@ def add_lazy_event(self, event: trace_api.Event) -> None: return self.events.append(event) - def add_link( - self, - link_target_context: "trace_api.SpanContext", - attributes: types.Attributes = None, - ) -> None: - if attributes is None: - attributes = ( - Span.empty_attributes - ) # TODO: empty_attributes is not a Dict. Use Mapping? - self.add_lazy_link(trace_api.Link(link_target_context, attributes)) - - def add_lazy_link(self, link: "trace_api.Link") -> None: - with self._lock: - if not self.is_recording_events(): - return - has_ended = self.end_time is not None - if not has_ended: - if self.links is Span.empty_links: - self.links = BoundedList(MAX_NUM_LINKS) - if has_ended: - logger.warning("Calling add_link() on an ended span.") - return - self.links.append(link) - def start(self, start_time: Optional[int] = None) -> None: with self._lock: if not self.is_recording_events(): @@ -343,10 +319,12 @@ def start_span( name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, + attributes: Optional[types.Attributes] = None, + links: Sequence[trace_api.Link] = (), ) -> "Span": """See `opentelemetry.trace.Tracer.start_span`.""" - span = self.create_span(name, parent, kind) + span = self.create_span(name, parent, kind, attributes, links) span.start() return span @@ -355,10 +333,12 @@ def start_as_current_span( name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, + attributes: Optional[types.Attributes] = None, + links: Sequence[trace_api.Link] = (), ) -> Iterator[trace_api.Span]: """See `opentelemetry.trace.Tracer.start_as_current_span`.""" - span = self.start_span(name, parent, kind) + span = self.start_span(name, parent, kind, attributes, links) return self.use_span(span, end_on_exit=True) def create_span( @@ -366,6 +346,8 @@ def create_span( name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, + attributes: Optional[types.Attributes] = None, + links: Sequence[trace_api.Link] = (), ) -> "trace_api.Span": """See `opentelemetry.trace.Tracer.create_span`. @@ -412,18 +394,26 @@ def create_span( context.trace_id, context.span_id, name, - {}, # TODO: links + attributes, + links, ) if sampling_decision.sampled: + if attributes is None: + span_attributes = sampling_decision.attributes + else: + # apply sampling decision attributes after initial attributes + span_attributes = attributes.copy() + span_attributes.update(sampling_decision.attributes) return Span( name=name, context=context, parent=parent, sampler=self.sampler, - attributes=sampling_decision.attributes, + attributes=span_attributes, span_processor=self._active_span_processor, kind=kind, + links=links, ) return trace_api.DefaultSpan(context=context) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 50249479a7..0d36c003e0 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -248,6 +248,46 @@ def test_attributes(self): self.assertEqual(root.attributes["misc.pi"], 3.14) self.assertEqual(root.attributes["attr-key"], "attr-value2") + attributes = { + "attr-key": "val", + "attr-key2": "val2", + "attr-in-both": "span-attr", + } + with self.tracer.start_as_current_span( + "root2", attributes=attributes + ) as root: + self.assertEqual(len(root.attributes), 3) + self.assertEqual(root.attributes["attr-key"], "val") + self.assertEqual(root.attributes["attr-key2"], "val2") + self.assertEqual(root.attributes["attr-in-both"], "span-attr") + + decision_attributes = { + "sampler-attr": "sample-val", + "attr-in-both": "decision-attr", + } + self.tracer.sampler = sampling.StaticSampler( + sampling.Decision(sampled=True, attributes=decision_attributes) + ) + + with self.tracer.start_as_current_span("root2") as root: + self.assertEqual(len(root.attributes), 2) + self.assertEqual(root.attributes["sampler-attr"], "sample-val") + self.assertEqual(root.attributes["attr-in-both"], "decision-attr") + + attributes = { + "attr-key": "val", + "attr-key2": "val2", + "attr-in-both": "span-attr", + } + with self.tracer.start_as_current_span( + "root2", attributes=attributes + ) as root: + self.assertEqual(len(root.attributes), 4) + self.assertEqual(root.attributes["attr-key"], "val") + self.assertEqual(root.attributes["attr-key2"], "val2") + self.assertEqual(root.attributes["sampler-attr"], "sample-val") + self.assertEqual(root.attributes["attr-in-both"], "decision-attr") + def test_events(self): self.assertIsNone(self.tracer.get_current_span()) @@ -297,13 +337,12 @@ def test_links(self): trace_id=trace.generate_trace_id(), span_id=trace.generate_span_id(), ) - - with self.tracer.start_as_current_span("root") as root: - root.add_link(other_context1) - root.add_link(other_context2, {"name": "neighbor"}) - root.add_lazy_link( - trace_api.Link(other_context3, {"component": "http"}) - ) + links = [ + trace_api.Link(other_context1), + trace_api.Link(other_context2, {"name": "neighbor"}), + trace_api.Link(other_context3, {"component": "http"}), + ] + with self.tracer.start_as_current_span("root", links=links) as root: self.assertEqual(len(root.links), 3) self.assertEqual( @@ -374,11 +413,6 @@ def test_span_override_start_and_end_time(self): def test_ended_span(self): """"Events, attributes are not allowed after span is ended""" - other_context1 = trace_api.SpanContext( - trace_id=trace.generate_trace_id(), - span_id=trace.generate_span_id(), - ) - with self.tracer.start_as_current_span("root") as root: # everything should be empty at the beginning self.assertEqual(len(root.attributes), 0) @@ -400,9 +434,6 @@ def test_ended_span(self): root.add_event("event1") self.assertEqual(len(root.events), 0) - root.add_link(other_context1) - self.assertEqual(len(root.links), 0) - root.update_name("xxx") self.assertEqual(root.name, "root") From 348889d90a02cfaddc61a2f7a49f3d6101904055 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 15 Nov 2019 18:10:21 -0800 Subject: [PATCH 0138/1517] Adding more documentation around examples (#277) --- README.md | 6 +- examples/README.md | 11 +++ examples/basic_tracer/README.md | 62 ++++++++++++++ examples/basic_tracer/__init__.py | 14 +++ .../basic_tracer/images/jaeger-ui-detail.png | Bin 0 -> 123383 bytes examples/basic_tracer/images/jaeger-ui.png | Bin 0 -> 256630 bytes examples/basic_tracer/tests/__init__.py | 13 +++ examples/basic_tracer/tests/test_tracer.py | 27 ++++++ examples/basic_tracer/tracer.py | 52 ++++++++++++ examples/http/README.md | 80 ++++++++++++++++++ examples/http/__init__.py | 13 +++ examples/http/images/jaeger-ui-detail.png | Bin 0 -> 144383 bytes examples/http/images/jaeger-ui.png | Bin 0 -> 234643 bytes examples/http/requirements.txt | 1 + examples/{trace => http}/server.py | 29 +++++-- examples/http/tests/__init__.py | 13 +++ examples/http/tests/test_http.py | 36 ++++++++ .../client.py => http/tracer_client.py} | 24 ++++-- tox.ini | 18 +++- 19 files changed, 377 insertions(+), 22 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/basic_tracer/README.md create mode 100644 examples/basic_tracer/__init__.py create mode 100644 examples/basic_tracer/images/jaeger-ui-detail.png create mode 100644 examples/basic_tracer/images/jaeger-ui.png create mode 100644 examples/basic_tracer/tests/__init__.py create mode 100644 examples/basic_tracer/tests/test_tracer.py create mode 100755 examples/basic_tracer/tracer.py create mode 100644 examples/http/README.md create mode 100644 examples/http/__init__.py create mode 100644 examples/http/images/jaeger-ui-detail.png create mode 100644 examples/http/images/jaeger-ui.png create mode 100644 examples/http/requirements.txt rename examples/{trace => http}/server.py (75%) create mode 100644 examples/http/tests/__init__.py create mode 100644 examples/http/tests/test_http.py rename examples/{trace/client.py => http/tracer_client.py} (79%) diff --git a/README.md b/README.md index c7b9a837c8..bb199eebd5 100644 --- a/README.md +++ b/README.md @@ -95,11 +95,7 @@ exporter.export([(counter, label_values)]) exporter.shutdown() ``` -See the [API -documentation](https://open-telemetry.github.io/opentelemetry-python/) for more -detail, and the -[opentelemetry-example-app](./examples/opentelemetry-example-app/README.rst) -for a complete example. +See the [API documentation](https://open-telemetry.github.io/opentelemetry-python/) for more detail, and the [examples folder](./examples) for a more sample code. ## Contributing diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000000..7fcc3f7dc4 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +# Examples +This folder contains various examples to demonstrate using OpenTelemetry. + +##### basic_tracer +This example shows how to use OpenTelemetry to instrument an application - e.g. a batch job. + +##### http +This example shows how to use [OpenTelemetryMiddleware](https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-wsgi) and [requests](https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-http-requests) integrations to instrument a client and a server. + +##### opentelemetry-example-app +This package is a complete example of an application instrumented with OpenTelemetry. \ No newline at end of file diff --git a/examples/basic_tracer/README.md b/examples/basic_tracer/README.md new file mode 100644 index 0000000000..4dc0e96bea --- /dev/null +++ b/examples/basic_tracer/README.md @@ -0,0 +1,62 @@ +# Overview + +This example shows how to use OpenTelemetry to instrument a Python application - e.g. a batch job. +It supports exporting spans either to the console or to [Jaeger](https://www.jaegertracing.io). + +## Installation + +```sh +$ pip install opentelemetry-api opentelemetry-sdk +``` + +Setup [Jaeger Tracing](https://www.jaegertracing.io/docs/latest/getting-started/#all-in-one) + +## Run the Application + +### Console + +* Run the sample + +```bash +$ # from this directory +$ python tracer.py +``` + +The output will be displayed at the console + +```bash +AsyncRuntimeContext({'current_span': Span(name="baz", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x5611c1407e06e4d7, trace_state={}))}) +Span(name="baz", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x5611c1407e06e4d7, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="bar", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1b9db0e0cc1a3f60, trace_state={})), start_time=2019-11-07T21:26:45.934412Z, end_time=2019-11-07T21:26:45.934567Z) +Span(name="bar", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1b9db0e0cc1a3f60, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="foo", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1d5d87441ec2f410, trace_state={})), start_time=2019-11-07T21:26:45.934396Z, end_time=2019-11-07T21:26:45.934576Z) +Span(name="foo", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1d5d87441ec2f410, trace_state={}), kind=SpanKind.INTERNAL, parent=None, start_time=2019-11-07T21:26:45.934369Z, end_time=2019-11-07T21:26:45.934580Z) +``` + + +### Jaeger + +* Run the sample + +```sh +$ pip install opentelemetry-ext-jaeger +$ # from this directory +$ EXPORTER=jaeger python tracer.py +``` + +#### Jaeger UI + +Open the Jaeger UI in your browser [http://localhost:16686](http://localhost:16686) + +

+Select `basic-service` under *Service Name* and click on *Find Traces*. + +Click on the trace to view its details. + +

+ +## Useful links +- For more information on OpenTelemetry, visit: +- For more information on tracing in Python, visit: + +## LICENSE + +Apache License 2.0 diff --git a/examples/basic_tracer/__init__.py b/examples/basic_tracer/__init__.py new file mode 100644 index 0000000000..88051cd8bb --- /dev/null +++ b/examples/basic_tracer/__init__.py @@ -0,0 +1,14 @@ +# pylint: disable=C0103 +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/examples/basic_tracer/images/jaeger-ui-detail.png b/examples/basic_tracer/images/jaeger-ui-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..63491e132b5db66441142e4fea3c60d287f00e89 GIT binary patch literal 123383 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 RiUT{7_ce*lKEH^u+} literal 0 HcmV?d00001 diff --git a/examples/basic_tracer/images/jaeger-ui.png b/examples/basic_tracer/images/jaeger-ui.png new file mode 100644 index 0000000000000000000000000000000000000000..08e6718eef130bc6afb927930a85b3f4ca26fdb6 GIT binary patch literal 256630 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�PJzltjD}U+3J1Fhj9N+@ul9#Q>e@b!2-*j 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%0D^~Nd4 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

@1Mo?R_t*le-o)^%={swm`v%L;#W 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:JSBU;A9<9|;+0!G3J@6XGw7@&0=6 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%0A%*Nk z_K^cAKe4-@7cGHu1Xw)h&2DW5T&8<-J+$6lpT3Lz{WGNb@{zJg4(B50_6m5stGhd} zf-UA2Am-NV^X<_zWUfEZu8ag<6y2q48l+%QS8V z>lk|Y6WJTj<}RrpndqqE>^XT2if;vLqa1PYwKH2}t%3JGYXX1XILE@EIWGdsc__MO z9*2P%KuPlQMGD*)Xb8`njV0$@mvR{R-w(bH^@xGr7m=B$b7iOwf;r3C9oGMm)$C7h zBD0;`mafbGYcHCG6uF%XyYUgwcZqu01GYnL7?D+(IPen}cCSX~3opIA-#8M8o=#Up zYn@^L^&=Z;InY{kajG+00)v1-i4A3gYTWwX3%w~HxWi%DVpyuiXuPWcm)5Ri=vzQQ zH;yePcx_mnX~(tP76(M?^n8v$385pvHHYI|j{rr`u}nB|EoWuWqxGi15pJ);!y-WD z$hU!?QR@-|JTIQgo2SW^fge}LAre;Z+4)>qwk#_j2b$wVEVa4JFwqviFJ7amfF?!K zK+E`>&Ksw>^*}=T29Paz&4%L_O$LD?3m$9~;L^I$0Q!Cr8DYtGe~>udV)gw$lFa#J zN)ciR+&O~p3;_}pp?UgdTm*`81q8}Kw4;~9Zq5PGM~TZMUGD?gr=Al=QERdj(q9pi zPHtLr3^8G^fdz+ZJ!H~K?lQO$GK2)K^X+jBG&b!T45T6SK#^NefFzJDB-p3I=2#;? z@W=4{nXNwxHTx5iGdd?Z+mu{ni~cy!)fiW;!-d=UcmB|w9UuE^Whh9GId3|Y%x%BC zi7av5JMn!S^+hJ@Vmgt_B=HZC`f64NvRf-4^(_h;Si*(dt22QKn~&yks5UqZD|zGB zX7)ZhhsZh;;QOIxMpQJm!x9*XcBIjd+tot|S)Kixu5#991K3vAT`NjBTh)(T++v33 z#jEySx8Rt|Y@jP$o_(O5)0QA-+-ETk>qTMjQQrOgD|0T~R(1krn+ed>Iqbd&T^}6p z;ASk_6hKq{d{}o)g2oB+%>CK2`y(Zk)xd+9vu^u%=rE~h>82r=IHQIA$ Im&2L=0Y^SHMF0Q* literal 0 HcmV?d00001 diff --git a/examples/basic_tracer/tests/__init__.py b/examples/basic_tracer/tests/__init__.py new file mode 100644 index 0000000000..d853a7bcf6 --- /dev/null +++ b/examples/basic_tracer/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/examples/basic_tracer/tests/test_tracer.py b/examples/basic_tracer/tests/test_tracer.py new file mode 100644 index 0000000000..d5922d6086 --- /dev/null +++ b/examples/basic_tracer/tests/test_tracer.py @@ -0,0 +1,27 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import subprocess +import unittest + + +class TestBasicTracerExample(unittest.TestCase): + def test_basic_tracer(self): + dirpath = os.path.dirname(os.path.realpath(__file__)) + test_script = "{}/../tracer.py".format(dirpath) + output = subprocess.check_output(test_script).decode() + + self.assertIn('name="foo"', output) + self.assertIn('name="bar"', output) + self.assertIn('name="baz"', output) diff --git a/examples/basic_tracer/tracer.py b/examples/basic_tracer/tracer.py new file mode 100755 index 0000000000..c99141f5aa --- /dev/null +++ b/examples/basic_tracer/tracer.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from opentelemetry import trace +from opentelemetry.context import Context +from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace.export import ( + BatchExportSpanProcessor, + ConsoleSpanExporter, +) + +if os.getenv("EXPORTER") == "jaeger": + from opentelemetry.ext.jaeger import JaegerSpanExporter + + exporter = JaegerSpanExporter( + service_name="basic-service", + agent_host_name="localhost", + agent_port=6831, + ) +else: + exporter = ConsoleSpanExporter() + +# The preferred tracer implementation must be set, as the opentelemetry-api +# defines the interface with a no-op implementation. +trace.set_preferred_tracer_implementation(lambda T: Tracer()) +tracer = trace.tracer() + +# SpanExporter receives the spans and send them to the target location. +span_processor = BatchExportSpanProcessor(exporter) + +tracer.add_span_processor(span_processor) +with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): + print(Context) + +span_processor.shutdown() diff --git a/examples/http/README.md b/examples/http/README.md new file mode 100644 index 0000000000..5e6f2d7efa --- /dev/null +++ b/examples/http/README.md @@ -0,0 +1,80 @@ +# Overview + +This example shows how to use [OpenTelemetryMiddleware](https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-wsgi) and [requests](https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-http-requests) integrations to instrument a client and a server in Python. +It supports exporting spans either to the console or to [Jaeger](https://www.jaegertracing.io). + +## Installation + +```sh +$ pip install opentelemetry-api opentelemetry-sdk opentelemetry-ext-wsgi opentelemetry-ext-http-requests +``` + +Setup [Jaeger Tracing](https://www.jaegertracing.io/docs/latest/getting-started/#all-in-one) + +## Run the Application + +### Console + +* Run the server + +```bash +$ # from this directory +$ python server.py +``` + +* Run the client from a different terminal + +```bash +$ # from this directory +$ python tracer_client.py +``` + +The output will be displayed at the console on the client side + +```bash +Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x3703fd889dcdeb2b, trace_state={}), kind=SpanKind.CLIENT, parent=None, start_time=2019-11-07T21:52:59.591634Z, end_time=2019-11-07T21:53:00.386014Z) +``` + +And on the server + +```bash +127.0.0.1 - - [07/Nov/2019 13:53:00] "GET / HTTP/1.1" 200 - +Span(name="/wiki/Rabbit", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x4bf0be462b91d6ef, trace_state={}), kind=SpanKind.CLIENT, parent=Span(name="parent", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x68338643ccb2d53b, trace_state={})), start_time=2019-11-07T21:52:59.601597Z, end_time=2019-11-07T21:53:00.380491Z) +Span(name="parent", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x68338643ccb2d53b, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x36050ac596949bc1, trace_state={})), start_time=2019-11-07T21:52:59.601233Z, end_time=2019-11-07T21:53:00.384485Z) +Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x36050ac596949bc1, trace_state={}), kind=SpanKind.SERVER, parent=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x3703fd889dcdeb2b, trace_state={}), start_time=2019-11-07T21:52:59.600816Z, end_time=2019-11-07T21:53:00.385322Z) +``` + +### Jaeger + +* Run the server + +```sh +$ pip install opentelemetry-ext-jaeger +$ # from this directory +$ EXPORTER=jaeger python server.py +``` + +* Run the client from a different terminal + +```bash +$ EXPORTER=jaeger python tracer_client.py +``` + +#### Jaeger UI + +Open the Jaeger UI in your browser [http://localhost:16686](http://localhost:16686) + +

+Select `http-server` under *Service Name* and click on *Find Traces*. + +Click on the trace to view its details. + +

+ +## Useful links +- For more information on OpenTelemetry, visit: +- For more information on tracing in Python, visit: + +## LICENSE + +Apache License 2.0 diff --git a/examples/http/__init__.py b/examples/http/__init__.py new file mode 100644 index 0000000000..d853a7bcf6 --- /dev/null +++ b/examples/http/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/examples/http/images/jaeger-ui-detail.png b/examples/http/images/jaeger-ui-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..50d1901de6cd0ea353c329a46d09c905c3a461a1 GIT binary patch literal 144383 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

2r<gW0;4e?1fR&{<1m2+dVX_NwhGsxN z=joQ@%z3)G+c3~k!a@tIbSqCUY`llsfImb4JD+rA#|1tZV_dVu&}*q@w_YI+4{T|m z{-(D}MLFYkvx7V6nP8*MaIY1Z_NTp015IgNz%scwPa8mELLuvC9-+ttA#sW8Kp)*bl5^5VOH zzi<@pCHINH)AlJ2C^w-f8tr>}~<8+yWuhkh&MZ6O)|~+$koI0t^B}I)y}< zfYT(V%>N9m3gmPM0n$^$301vxfWl4PA^h(zhx~_hF7&ZMBPK?t29cZreK@cz@!8q* z^@mA!qk-(%bW`Lx@Yjv8GE2MZbnzS%Pw%dZdKb}~G79C>HpeUqjY3-Y~aBarr_z&!eLFR2j(%zJBJX z+|16-4uHJQ0Th)-O+dgt4rUYjf2Mf6&@q1^-#(?zE`b4w;LQ=uK#Xus#w&XbyoAU2 zXzmVduPZlLt6_!b%1OrLJ#nRGPkSdV&eOi_E0QLR2WI{KNXSd1k4AU^cy@7j-jvM! z1|BeVy;_ggkfx%o*Fqo?}T+ z<`hM?{E2fv{{=p3Brd1j3BAiw)a7&5xx|Lc8>wW1U8RZkMUC(vO&DPV>dbN*QMk)m z1!|>s3`Z?&!o1pcqO<|(ZhWl&XjSQI0R6v zN)=zS0{Me-*+`;|Tb`h^G? z5et-)2N)yBMxA9~c}U1=Z|u73Ca{wU(l#=MNaauDV&rfObX0_%kUReD-2~o%V!n0) zSpBy^ptGr3j?)VZ1gZ*xTkEO*L_J!Lzv|!rPVN4*)*c1zwXeoggLzb}fjkZHQPken ztg^sn@EJV_oKV|WXm2>5!@J%G2ja44i_6yWb8rjBr?3Qo`K=F9ppzuhVdo zyL`&SlZK|yI{~4NLiogrJNWWH`3R60KnB=gDCmBM4=-IvPesfZ+4~VM6is9)GRLwW z*K*8HW?tpMpn`;bq1%Vigsp9P%Pcw!?_Si7M8^ zhOy%~O-&@~*VjwFX(*`MgW_=6jEo4%zyW51q{3Rb?$_oT(+{@s=6bN00d3|TP>h;d z-#|+1GVmTh_L=$X)W(6uaHS4U9eQbwNmUAC4-m$ejN*s+jgLGak*Yd4XBds z;6s~kmset1Z|1FTm-j$0;K{L-I||hLZBfBw2JaG->)ZznO{xy3n88O_6=3IroZAsm zCu+;(|9B+rTZoFW4*xP&%zWTpTO>nU|MOG8iW|OX=3~e0W#N!+y@iU(oExU^{EpM&R*xp`lK*5^6$4cw4xcGgDiEokt&!zP1xMx7_R6Ho zbvW}vZ%}^LIZizm$P%n_GG&rAx%;W?cq~i0CxM<9UBH2d%d_uq==JJ^ARDLqcsk8Ue;` zV4)rWuKbSSj&ZF?*M2uhko)02M%})fZ!nN1@dOk+ zJP$vQt;gs!jTv(-jN`G}pQaYtt@bpR)_;4F&FzTU*t|@(-?1(=$Jx(ySF9{M!edcy zFG1k6xG`znD_H@RlT@;eh9kHxIfv-^tWSMuYb+0x*4V%}-^lfF-Ph$aUIYeQ_$)h@C}TqE+9hZ4Y$F za9dviie2kn@WW8)?ko7zkn5USEt z!(Q{r{q@*85V!%943t21<1v42@5hIXC{K1M{Jj#`X`yH4bH`cRMFBxQ@f*V#ujap8 zYyNO^Rop2(4l7A_(Ud6)8&)3bbA2387Ir<+$ud!h1!tm26P{Oi;UVF^I>lxc`v!Q^ ztP!iI{>Dw+s`F50$im*NqL*B+J3!!@`!z~|vcgOg#OpI2dhvD$NP(GRXjHJ9u135| zR_3M{H}4iY58S1K``|;(uAeLXp-`Vmj{^QfklX#&)+QUsx`cDf4~KuVKcIa~Nz;|G zuy)C@B;Uo@Zf}@fapG>EI8`n7-7_*)48m`(u1@EvP^R}MH%46f_DUDvo*nexRDoQ< zcVBS1jPfRRnqOLpc|iz< z;#)$RwO%N&zILyA?`tt{cs!|dO~MGCV?}B@z|Jk!RZ*zF_~Em$;3-Yu)?H$K(9%^o zm|!=f>{YJX(;Jx_2)>z+>KGCx!kOc&2;a7-VMgIz379PO?8-LM!kZzqd4s&cn_i-U z?OqpM@iKhjGT3QzYyx!W-`UZH!uG1aVR4~Mx@QeWxcwkJQS(Ia^@BfJ;MEElpbl}C z{=F0hG7lX&$#rj)1s~I+A6jhmg}HOJ>(M!prd4~J=ykg+-|}H7O-t9Hsd{5*LmSRG z*d#e`Z#G7{sHjXiDmvFMkqo*P_K7hjz?U{bwA02`2mN`)Q2pzDk{Cx5g zUHP1d*TjModtuG8|KUx$XFEH&=c^9W)|BC_0qhFuY<2B~X)A9^X z61p1lBo$A|dU5q$p}$^pj5p|@!A~CxdFps2d%D~cXb@g&JmlvBUrbgn=jN4`5PSZ3 z#AsBOyD?f}7{aj=Jd~=JwNv}^E#F_J7B3E{ZOx0*UuQTCnI3yQ*zoy3V91Auw4x}A z$5!llvN=JDUC)^p!W_?Yf+_ul!`Y^u`}lOaz#UdTo^Gm|d?D%9rT%ES{o*v7KFs}$ zo52rdgV3|)a-3~vkW_ERBU*)ghWH9>q6FTQK5B_V#qJBt+6U|1EKP4i_`Hxw>nt{> zfu*4uHTu-3PWQy}|57BQkAO~M408R7*@%L`M@k#;-%Itx)KW4HoNxBxwL0=fw8G{b z2+@ugJj=@AXT{RCt8?XkIO(fOxSHEwOJDXV0k0e<8&npllyJ>|y;KdUyQM#Do@4El z5hi*{t(YHj-8qM4m}Db%vQf=7(`c|rM6Pd1H0UY9q$JHYN2^uImQ=l7WqtaXwX<%Y zHfX-VrAvjJ2rthhl*eJ&Q<*ZuDpQQ?H(vX9nE?ekz*pJCo9o@Hld(Vp9k!agTj-u4 zWMjeKgp0&x&dQr#k3P;0!7XuhTW#GRd#LcRyg8w#sMj}mK2UJKKbRQox!%EPI}aEV z2V+vgE7HvRBYwA~C@CSqI)@?0gobZQyUBJ@CJg#xxzUF!+)C~Co(+r!1J0ZfhCgrJ zgGW)Zi8xIN+_W)n+Iw_ugZm#R&X)%aMwjDP;QRvMYmo7F4>|6?)b=xA#U7JkBbuk@ zjhFJAM~Q;$U3f-EzbxNsO;>Ud9MXJXS~xosKpNfv&2O&34MhYT9(rqej72N~)|;%n z36Iae%7urScJVxE^9v1takxun==?Mn9-OP`U3#5?54@H+^mKV^`wo*yGdU*o_{Nrp zcsF(+oRIf!bJ8sjptT&Hl0wEDe>nuN7)X7SQswsN2YM+&ih6MGi~i;l(!iSKCI-rv zQtmACMIJL$lm$10*~*Go^gK)?z!VEU&;gORqhxH4T&NBudZg~Ka1Ez zX7;>pfv!XSw2g%(PYhuY;~()MNvq%@@cMJRk6}{KrMN+R$P_)cn2NrNFi`Km=pvoU zr35D3&UM?nxqF6FbG(z9v>oct?^>&`j`q8d2Ds7S3E4QNtBL~PiECMTT(2mHYHp7v z(@l#eT^5+il*&z&(9}+z&W=uCt=JyVdobTQT{?m`AB7v9?$-8Nlz_8!N^~!WCoRBb z-%7=cc3kjXll#opuS!H8@3ApctabQ2EQ z3uhARsQKh~7exgnFhTWCH-VP{hH|mC2iNgzB;$3%_magq_P+6=uy{IYwB59|x#+`L zA9B4FN!QMGo^#6za8p0jyZz#jB#D^|8jVu6#=}*NQki6%n?T0R4yENpM}sa%k)>|j z@F07G+pKA)K36YmGT(9Y#ygm7pFSYZ<9PRCW}!N9K0PmC-1x?c4Q- zrK!`|@Oi*D0}q!lo)206y@|e;5T6zwA0NkIZ}QM!EXv?|e&c7QU|RVZFsewU_iIz# zR3_G=%yP=6*uR0o2;1um(?S4_mix=hz9h`Ciabg-MDb@h(#|&#@SsKS@K9S57%Bjk z(&s-%%8s99;Th4qG`u-8TMzAI(gy2#mV+%?!p zYe=WB7t+Q5n^v4Og6Qju)Y)80;3?B^U{<(4hUe*R#SmmF4xv1Q4>99+cZtfAyo?t} zz`M9NLQbrI%`_Cp>8vL3aE+6`bF?Z67#lqq{02=pZ66|NNgFhw>A@Jrv-Sfh%P_&p z<3AZN2ifk&@pcKE&k?{>&T^d1>B-N!%@kNKTaybl6;hXF`FSn}m(*%p@6@YJBmovS z;L_lJa94LYSgp2P9m%V;?~>|SeFii)FtUQ#smn@BsTxme2HKB0_TT#Vywk0N;l<4p6}V2gLjkf`1bI?Qruz0 zb>HU`--p4vVL>#^Cs0^)RFl%Muy8xiHTgO|M&|`crBjGtL=;j$r=FucBJuh(Kl3u7 z-d1u}Am+s;b9*Rgx#t5a1nV?}}%1fm&-kG_$ z)XFcduIk@>aD@CK1s@dR&p-1)6bTb^!|G^9hM=w(Y@uAJnTnlx^&Eb%+J*s&~#c!W+f-Ht(y#M-+9NEswl z(P9!Fd(IoLbTh6q6Nx`Y2YY^5nwep5>lv<;;HLiXdx1}v#()m$M3+D-Db#8T8yy{u zx6&j2C6kv6mX(do$E+;FhOpF=E<6d!f|gf*WqknmLx2xCV~4#OhMex!+fAd;8Mzik zI7d_&_o(BwC8!6$OIgNC7k*75M5(=8Hw&P4jSOp}N<73JyC zle@r4av%D903Oh0aO|&fFD))83OPEU-@Qu92zB$kXkM7Cai`5}yb4q|-12p=t6IKQ z8Tj7jb~v>z9i@TJag4YJ9cwVRV(9mgKlMXGFe)ZuMrsf*<)3STh?xc*N)`9K2MZHZ z#B+p;oLqSRO@@%*6y7g^Qet4+31#yO3P|>D^mTM(#IcT_)HFwtLAtKMWOZ4bpluQFtri@uPyd`6T513o@;) z2zSnsojTNv0WKC@wwNN-&_?$?0VHDaoI;iJu9QmqX#b_H9k_zZ_p#=Tqpwz{-Z;7L zogbhgN6f_8kZ^1f7}T?2a%g<-UmD=z!N$HqHtw#JOhS0#;4hQid*}SIAe7WBfA^iU z3Bl99xAWl(((797pf5!@{!+h`lR*^fp`3(`3LC{QUHzrDQPj`&!b%-x9INq(RrYKZ z@sZx^W44UXyE%N8UNg2nT7+_Pz63=GzkK$hJoF^?w6wG|J*}^!X=89OoUMt-6K;zA z%f;ojl$G|rgVQzaH;n|A$%|{35F$qOFjYc%lgIp5Tu3?zPn=ua>n#Xksfm^%V-rG^ z45JAGNm*YFeZ)=w!lLu!Gdd_8jpwx;Y`q!M%8=g~CO;KR|4~C~ifry8)lN)n@K1aE zt+o}E&Dvt-Tcwu2&#(5%t)fW}A-lTnIz=kke{ps0?VdPtrjxUCTjyG53G-3gL+OAZ9vJI6 z&Eyk?qhlLu$ALSMccLv*w&aJS0dJ%!gO`>^2=-{ud){finPv1<8iS1zdyIIjz|Yok zv(x^e$4kSL$uwcC~wo zc#NSpZXS}vKKkSr}R-0{EI7fbI{#xie6c#x-`KA+bbnc64nJYqDDsZU_+U#t7vFL^K z-U%8kt10CUA}DYUywi*k9_AA&2G40oeBnS~@tT&n=$Sf~BLUOPa6T&|wt21Mo{bvH z`J3_;xqS7jFRxzWGVJS*_vM>R zC?-o3q?-TS8!hDC#A9y%+*<_X-i%7v74F?z8M-z_##*d+jx;G}8Ey=VFF%{0%rS9x z8WFRgVzt{D`m15t)ek|qVrb!K*4EYqX)>cbhUn;p%U`nKmlIXAbjPW|5ghvqU=IRL zYvfDknu`G#?hKBm7|>J?bal4(sf!Or zCNfgl*=n_ismmg)jd2jc+_}Bf)kx~`_S8&v?j9_|dQwZ%m)r*?(~oqxiD*asu>O(I z7$Tu+)a`E{kDm&W&}xWX|DJ@AIDmv_F(Mh|d~cEz5wkWBf9xA255L;d6;Ia`?;P9c zwc!&?8)}}IKxJWJ$w-KIyPP=X1=gdWGCG_bUmVUj4?A6-HK2Oll;F+Po(f%OhRJFi z&x2A2XX6qg)1Erq9zJcnydf-`q!r|<73OLcuFuamW7rLql8$o1-xQ6_;Bc+tyKp3+ ziWUdQB)Gv13A2hWMSqeMSep3uB3rKZ@xRvg7$TEEwELf1`vQ^4tc6|U|Hu?8QZW3D zVF6w29ZFCTq6yf$;sy@DCObZ6>P;?K+^#4wVHYJ;Rk2~`Xx>z7R(**~c-LpQfp^^d z9btW)!iQW(2ME>5*v-@Pgisp95m{-{6grmMKI_RF;XC=r^-TNlO2u zY?;PIp2*~Ry6W5wa`?H^t>~$`2SRMQStrqUB+wJ|LVRv?d!Lur7HUuQV*Ux;dCqS-(QoYZO32{4NgLn0t4*M$#-THdh#lFFB|RHC2;if4!u76^~AZnZAkUdijdO zam#;=mZNom{z+kB;gj$y#xZJfgy;}FcN`9ft-#6St|!Deq^JsMCGFK{FsZVbZFSC4 zY!_A0$tr47%dr_+ddHT%XydgQ2A z3Fag1vSVYj^y|vhwx_x3hMB!p++v<&cTKb3w=OPiIGh~}`xlxdK;+A$yUzSAA38+7 z@BFs$_vCv?Z*|n_u`^pmbXlt&No*>BudP{#PbW#&-5+epFoF(46&Q;Sft2o7D1z(# zw?pN|0pE%CG0;k2j@KG}&``tM=*4-MRsAb>Yg2^9>2v6+yKvJMg{aGzQiPv4j!|)<)Ak}ri^^=uL-4l+$$gSHp_~vKPX2Bm?YjkBqHo$mpvxX2H0($!XnU1`ayk*6M+AUv|tjeJAjYNmj2E{ZWuS5|+=gzFWSjPO0l zZ%*Jby!#lRPXmb|PIhAXhUhnRghFJ98-9QjE%)u${XtHFff~wBRzj~PC^$+Qa|$X~ z0s(FWSN?IxbKW=px31tA7vREfuXuetZv9A}J-kD$V(n>LHdx{V4=0{dnEB@|^F%JRL2EWl6C}*{S&LhbtZ|}EKSV#J z>&(>tF=E-;Ij*_q4R_B;AdXf5uEU*X7f1ZXg|O0!v?9z|OxJaJ(~lzoH`HZv4XU zu_r--lDmI>JweS#am?;eR$`MK@$|MRMFqc=NHr!RTqX|_EL^RdU4!WZO=pm8P9uva z1`H*V0k3ai15y5{3{P)ka`+f^<7fZTZwllgo*oZVCP>!0A7Ua3JLJ&P43%F~C@XW4 z&|V?vN`c{`RfBCyHYd-zu-z_Ji;RZLGiKqD?%5e;HY#9!TLOs->A$U!0b-4lA8k19 z{gwe~vvklVU39cB)2mmu+Qu5N?h*ZV@TwgVfl$%gu|-RxfdWSfKvkxNG@J+C(UG{O zkPc~q)`j?~56B|P^-SSrKT;`_=e9W|^}rDtlGsu2Q0Z2O5l#AbX!UuCWpu_aSb8Va zRV~RB+;Y`=`rCyJ40dIvn6GF#6$bKWpJ9K zw=O-~e73}H%wz=zQ+ph~7gww%;}!AmXaF(I(P+rElJ)KXftSUQYjv__XTEo>kyw+q z?~%?Bo1DHBe&E;f`YHiOKv?ieYtqU=3aw)d@v*F)ob}0~rZW_glNGX1s0&J^y_?w& zaXF#qR9iGjxJ+ogZcM^N;mS@1CsxW7CkO~lIO9pFM`e^C_|)ud1OXzV@DizavD>C) z2b*=Eh?vhG47D^L&@TjqzX_^w!!CN~jya`52L9oM#5pYCcFAo-Tkwg{@!??ve1?t- zoTHbR9_9Y$&tLOegA}N>X?G0Tq{Yk&U;Oc*5Ye)80l5^g^j!al)q?;SuAk4p5J17J zug;@22kF}M^yyCh8f;VtO858gzMpW6r0cQ*`29ZgT#k_Yup7MLS{6pmMkeB_m4v2x z^hoLO#|X?&jkx8cs?ovvxYmlVDLQ7*bzRkyuw9ww<@;sQ*)-{fo)((H6}+CLAp=la z^p`N>cPSMQfkjp`43a9?z4MQ>^N zCaS+|;1a$t>_WNRw61osshgc2|E@O4zKTy{He2kmO&kyw_EuXONy2m6wA%GbbhjPk zfoSJkT#8j0x+M5F9pVF?TL+S)q{T~>Ux&db0Qs4u;N?31-`~>S0$^&~6=F0mPv|sC zNeSh_sl8VrL)wjyfqI?~BAl@c0rwLfOljwph({G1dB7Gf(DA^0`^lH%lA9UlYJ3bz z&;5BQZ?v64zdTFa+14xZ(+Lqdl6{GvN+GON34tz-O>#c#_2jT zwPIUFI3FZ?);$u7?%7B*TS{2gmuu@n>q}%uymV%lr65}_(mL$Em_WmwRlRdiG$G)* z)#gAUo~9iO>^hYAa!I1DJ}0dn7am9Y^qJo~b*Vo&(az~h z0Nw9ra%==?^uAbu{zeTh76*9foqZkkjP zA^mOAB$V(uA7#|@{zRO=Y{>&Si8miwFy)lt!n}UL740H|wE9IJea@?#|wV1X11IQY5dvlO-^s|Xu#IaDg$ zSS6F#p25oTw38Slk zWt*yA^SroR|6Bs3u5lMV1Q(a^q5~t(&q>MlzRFh?m$$Wy#FmgR6Z=$I`13g03tBm8 zEmacu9?a)8o%?n@U@GbKK{p+;(`F%$t*=Dfd6%7%^?7oV+3`82JZoN3DsvL zmsIT_S6ng1P5s)m7D`NP5z!uR$t+hrBGZ@?pm|If5zUc)|HskD0T$IUs^|ks{RJ4}AW5V_wGqKRuJprqq=3|@=!C|P z)q03Ue)K6H0AtT;n;(bLoR<{otG2ci7l?28nXP1e2;xs|hCvOBaFDKX4)YuQ?z-;| z^wY=aE;RD?!pdoS?ux265qUcrc!tJyqDHk6>hgw37UFVeuf!28DBkL<6Z#1zHw1%( z&vYlV4H?$H8w(|?)mx#k6skSEK!{+{D4djhzYfK$#!Ly8U>#XJM}92xe- zi|@7vXD33De3mb=y2f|lCDN;gp|I4{TqWXP&ulmmx6x7RG6-^Yl2dJ&ebMe2Bw2ED zYIpMSt!UZ7lj>%w+um#3m~ADdYb;otOecLc{Q2-Is%vp&CyXA)607yOqRk9%Re^7{ zn%KIDI^NO;ximX>T=Bh~d`_^?mUoDVi31aZO~>ZG#znA0I8>fPJX}`DEb$ zew$gk5xnfJWPSHTM&b)2vE#WjUtxHy-9yWF#MHaWN2jQqi+f9?ym@9M^sGog7_zdm zVRcy@aqJ?cC3};LWI9GP=Q5Af^l1;sG5j6rT7tC3pI;q0FI^B}y{v!z^bMG3K@TAV zGdr=w==-^qon333Fp+?a=65(f3aM$@f$QlYtD-37|il4f~f) zeE0$+QM#E(T{?1q>Grs<&He7%)*GW*FInkOvhQF=#W>s51N{qA9+32Xl+N$Q?r}b4 zO;NonB}pk=FWMbG4KJs5 zq&z-Gii=9W;AY0NlqB?C{50+aF4J0JnNq=%rC7CDQ9;J%zy>9`zVlFwZp~Ck)>#Y? z6)Fl4zQ|X*%aNbRv2|3YEE_UEc&$9eu{3yQS>kc*@RiFWWmdZ9w5-nR;)Z!A@FJ^K zLpHvA%ll=)6`p$}>p$Y_L&SHsXJh#zJ^)R1Um|tr+{5W{`FxGV!L0^5u~ET8WWt_t zJACZLE*QlXHiBkm=(>d+DesioXv~8mB4ZUTK{*Ae)W_u)roBQnuGU#m?d{dCTB0Vj zT+ul+crqniQIq=;sdfXNH?Dwie3Za-A|BvlXC^BEYq^zL!Q8+ItXy>)7#7)6L}}zLQ2}zJf&(mO7AGjxV-rIZ_NFG_Xv<#D7Yi_ zx6Bd>kPxha7j}>B|HX0-G4RZrE+XjD> zj*8V^?(NYqXXg7yb%_vS=Vjvs3%{BVXw~qcI0;tC+-+OZ6OT}=V3BuX%`-(@VO_~z zyx@uP$;Sel5;+>RrfCIfcrSPIaJhD@OO%(${G`Qz=(IIRJg7RYeY`cWw1*cEF;Gw5 zq~=l;RafWe_l<&h_bR0Av{(3WqA+o?_LMbe7inazJ2mQSlqs958@*z_Dhapi-IsMJ zjojHRZ?ysiF*zb({VcNjr;ZdI7oGf&_&-3Os*}`xs z`qtbr-j-H=@Aa%k@}2o%d6yU`Zu|t_M76MFwui#PibMc zWaJ4O=hX8m1}rh(*0;j$+d6Xu5@>4l8fU0xzNC(gY9~A_X3BnW$px+7(_Bk7f*sTQ zU*prRc^*HF62mI&Q17g-dbv0@g@iVqOPXJ>lg`3Re+y0RtF=rDda|NbVWW)+O#H5?)rd+I{W~QM>BxH#ug2ji?-3v2jlF<#k7FMGhaA*zBqT zkn^XjY*%meNse&~^SMwy+4tmCg{>9Pilc8x!~Q7jkviv-D0?M(j&+M|zlj|G+FWKx zZ~UG^lx}_j2_b>{i_d3VuWr7PN2=cz@_U)kV5Q)64cr75#Ij>p8g$PV0L!dh0PMj zO^q|NB0X3(Udc$=wVutkQCd;E-&Cq)x)VxHGZ|IMklv_5k^jL8)k}oBjDU%5Mhr%= z3;R0L#9}qEiEoS#;aB_A@kO0hSLsep$kqqR@jDZS;z0a=Na^-tQxu zKa8v6B40Z(0)k0cL^wC^o|+VM3%n=Rv*1BcKxkMy{C@q1DdJlMcta&8*N0-l z*Y(WqG?Ii1;d~^6heJx+7Mz}kxXFUVT*1X>?hID#FMyNUnL8}I(X8@F8*BiR4&V0o z^_Hv;*l$c0hFWPMGar4*8qpiGoRL;m23lB>kXTWr-lu11j~#)Rc5(zZ<_pLVyck~sz<-i)W+39uze#x0r78` zf(+3yZ^7j!_h8DF#^R5fk!nm^==H*#yz>&P2cdF&1*HHngdZ44DKa49otDV0u1=UL zkAol+#2?8|NgpTc@*%PAq!T11*|G#wz%P0oGlT~kUc(nm+qdN_5%~uv;JlPfMv5-> z4$P|KJ=>oOJ6}jdV4Qum?M)VqXispp4m?Tp|iHIOF=t zwx_vxT%5Uo<*y}OjUj9!Obx_@P8b>=jTL_a#90iPx zO)-HA-HuAd4fW2fW@%Y3!cuqAn^s04u1aA|XzbtK1_EJzhyYQVNtFgA`gfw73rUo> zdaTs%0i07>pc>;|^q@(L5UQXPv8HB9I@c#kj8F^PMI1BxNP0>;h^-z#s-$-=G_~iA z#ATWu1hlQ3IxCZb!`+Dla|a`+$$9`5*_bULHmJYV|AJ?(eC_F^J&7T7$`~uDz51;x zUC#|b>pFlde#-4>as^&oTnVe56uGgyd;i*$2!&qQSLSIlHaA*wlo!O^E~Y%QLqrvP z;}J6|G)vY&TFsQNksxyJmnhr*=?C}`kPbmpHe5fBa1gaVKVaNS_!q8`LwtvE1)Jh; zl?a3c>TgJGI`;zgXx;~ssLwGtp}iB56Whuc)`)Szd>$WBSi&5tZ5R<_>2SfgauB}cVw?!~Th%`DpN}R3` zdLtQhB7vM<4Oi(EswPJvk1d;sEB3wrN0fueRFfmTO(_#3(g`7Uos}c+)0}^Y{tM55 zn}*;qTMhQGgx@#K54q_EsuqwP9Uu1q?a`j~$n;W# zw0$ObUA;Q`=I*_CL0uPauh#Xyax@a}dlaY7e(F*i=|T{`=6Z4a%Ff|%lqX+{L#oys zeHuz;9R-g_l((zth#9C>ykCFMS496-(}YG>a_`ejz!FB&!SyF>;=s{KkgU727v^Eh zDw1Kb-Z4I4*nB2RGhR{Rk3=V}*9guugZSNqGEe%3o2HDI5o~M)<(6VE$|{O*xOR5M zS#`er66{x-kODTqg!R04`%M7xc_jcGM~#Zl`8{bvRe+ACo5ocoOw(6S@LWq7<*fEF zvr9kVN(9!qz-jW_3AIG^Yq;o@7khWFh>|{wA@;QCF~1HAtPemnlxj^$)nnYNndm9U z2?smheQ%6Ay$d(OniupSDmlj4nJ+~?=RQIYyE~>X?9%+4JjZE*NHAN!>gukET}Ut& z(E5?9rvEz=E_MuyNVOZXswq6%^=sB?l8(WH^NQYL_YtbiN){N(Dx@^rPPw6h@y7XT z@_hCye_zE>pVNCnU{+@U9=~d3N#IW;q#^}6YV@Nf{r;Xu3L^pl|3cL~1zju%J~Ms8 zZ;N6VMjVaf+x|p%0jgTCI{U$>tTywUoLqeGdI=3=eLiJtH-KN!E+flZ%=Q?{% zd;wb(vmbm47r%WN+DbgoS5 z&jkY#KLl7AF&GVH$=@hRIu=rO&R}V}S0>O7f(D47%Prz!CS9hz#ypJSSLhPfaReA} z60+LO8flAW`*fiNKwf!COb_sleYESPI%D9a)*4XU<(ZO|ZIks$*iweOYvq#n6g@&+ zG;*755qG|L?us(Ibx*1bMn{P;9cBY3-7!zj1pSp49)NX`^&e&&qu z!$%0j2%!Ss)!RD{Cv;29L1HWK4Ih!F8*5bTa*J z*zPwdLO=}*^Y-=dE*WG;sUKxs^JQ{v2Z5>K?okgae?J<2#-RFKEmnf1ywnFPvVVVlq z`|>^p{K1>5y>rOMm2UI0vR4dCfsMtOSI&-ivY41FZT;hDRMGgxo1)Sbm~0h}Op-*i zP*Mw-!`3{0dTuAqD%Cc2q|nnSABTq(x0txk*wDX)r5I-|*(BbScp@?5iRFkr>lo;O zYxIEXo=_!KOKo&kj;?j=krZkV#L%Di;I(+owPQ+WB)0~We=pNCVgMBXbzH{E+ux>s zN)A}Y?bPvo6nIMhjA-F_0nT!HGN-K?Jb%~yBe1Bc<)9anfz<0HgywBWl5mwn14H@( zh7jpOrT*jbg3Fd{$$B(zN+_=u{VnR;L7c6_g{gs_@Qy^ecGH#ab?Z~H7mXKAFNs`M zR8DrlDf0Upi$TlFy6J{5#Lu%xE1zw(q9ME4#oHChs{wvTox6gtk@os2oOF;~4PV53 zwxrAE!h*>wAdQ?Vqgj5i%=Aj!vo+f+W9S(T!;=oly)l$DTziUHwsn2&{h>CYvD-F& zMg@eG!+Tu`Db2e8wy#NilKdyBv{D1C!$Q3M;l23qSmGHkuM|Q?y)VcxY|-m_Ur=?e z;vtKGq(5{8d5WS6Xfe=pkRu&Nh?i|>Lx34eV(1Z=D3P_R+7br`^~aYwVASTC>ui56 zZ0b0+rGH4~sb-(=q9k%hsCj_wMWQBlZL=D65AFTC8>`ZIR*FK%0fasVn(~+ z`$bTJ=ki;#4zLO-(b!17G1Gfv^He#C-lzS9W6?O0D(pH0W);(%Te0+iL;#}qYd1g& z{5yPGzh7c_sp0^!d_3^-Z*7wg3IiRWFw1z|VPf^@QY`oPMEVvbDPNXz5d`5OD4Xn} zS-&U$N;t=O$-B>&SMga`Lxw>gfG$3yZesk;Z9r<6V#KSYEM?{8on;sXB+W&5onadY zIIL3j?B|>{@RkF7FS0sGd9@~Q-&SL`7U|~5-!7OukJ4qUG*oGCL4?;uNKaQDzboOW zq@1wYxN&x-kviWA)7hQrKEI!gNvCiDqV&k=5dJYn1xTS*Ox;Z4uPoY~&;Y4t;C@3T zJN$XDL;y)AE9xy%(qn%~uPd}cWn~?3$=q-kvFm}ekH6h#c$jHnhP_m zQGBGfU31SQjuPTL&r2Z_fJ@<@adv| zryu`Nz7$G;o3T+*{!@rUK(I4TvC-Uxg`KAMCl6Ojdl=26QFd4 zbCy-|72TXXA5i>(@sTcOd|WnBEW@wL5TjkaLHj~I^oE9xO2|f+K(Xio-%yGu=Bm7; zqzyDs@7D<9r*!eyIyP$RDT|VmS$MRNcco5&G!AEa6st&9z1>>^?&rK$91ZIipHqI@WI5>{=*9)KLq$~!~o9_a4C+m&jxEgMj&<5pE zG6MDuG#kS5V(D=SL8L@vD=UkB+mDni7H(sgWWU(t}$#24XVoR6{vJu@w9r2hHbab82OGXiK)grJgXm{{So{pN?-z|&j z5nr=FQH#6oaM#PlpEYZy4d#uw%d*qb;uQ_x8nz#%Y_s;{RACn@2IVpb`BA> zYCOA@L-3g%u~cI1)0FOYTW@Y89dR7mT6-Bv+rF1SR!%v(s*mKD0XSQ`yZTYVi4?y2 z)5T`a&9Fb61CTiJ+6v-gE}(5)e!H03CxBhjMO{9=C+!uwa`g(++m5f^MNKdOom7Ev zW8sYV{8hsv<-o_;=S>j-tn?T^qUt%cN0cSjmu7{r>c zg;G?4t=Q_ETEBNZHbHuvPiwxoysV%TyFA3ba8vPgMk<-QDzsW$EWSalW?(CCxP$@q z8bwGX`MkKQ?HtrOIw5^=zd@7~uW^(2N>y z8-=|PVbMQ-w%T0)*(MR+KD=p&7o%P9xK(*UrV+bPtHD4<%|u8%ko>ZuX}j=x=sanO z&H3z!WFqhLa}sQ`e6te!j`ItOyPR<+MS!mev zcos=KLhkrUdGaPsF0gzmEkm4#czAIQI!MnkB9V)_dEc4GVmfPsvHJygqVg0aapBbp z6dMiyC?7P9%OYEZY7I^U)kMB1uf)8}M93aF*c%)y|IXJXJ4HGnKf(H%zclK&NW%B<(=rB@k)qIj%dI0(d{k*0vJl5RF9m^ zp~?l!&-enN6y|Qa0#`(w7ksAUzEFmE*?Um8JCRT4eu6`eKVU>6(4-^8OWHG% zbwlQAjRD&HR?DYIziTt;)gr)1Ann#@>hjEDF@K@E_5^*Ze{bhW0~UO?(>>d{n9l+5#1JK#l{OA=GnJzCF^m1w~WKj!k< zVrspyjKAruTf9WrOOF$v{9$;a@me&keK}3b?zE4hC?j0wdPTp3=wdZxF;kv)wbdd= zJ`Iq!!YbABhDN`{mI?W~Zh!GraZ9uM$A-B7GvF_11tcy-t*V`b zzfqA{1`QliJN%-yzp~LQg3WTFt1Z{44U7S}P!AWv)Zl9E@LbhdGHrL3N2a9sO`3+r zY-wxH9q##j#^~#`PYFgucrBjZ4v3w!W5zRhi2hf-K)`pg44D@TXURiErzkr6`fBGG z=w@g-fjUPfIUq%aS~OK3ppF)LHqG}3k@o8YpnnVq2yqafr@xE#`iCykUScG{KkhSL z-BuEz*B*ridvGuei;{t_GL*BtPBZKZ&5Efyj7Td1+S-^lR`{>!XnoZO zZEeEduHqjL;(+(8Td}a5YJ4gJ`&`Uk>_kS+O$wObtQ8biL@lGxj^alnoWY6UV|uJd z?+R-wOVnTm9N1TT2hL~>IyQ~yYd<-(K)8?mA+KQ}9~p({Vd3!%_pbgxn-@~R!c*R( zqf)#?zY}dp=9!aC$4~YIEumD{xplBGs9q^pO`WIuSzaJo!w5{g~9gZT+CV_UAthA zwkvO}`=D!1)Lush6Q4jEI0~t3kf3pSnRwY>9w;r%Vs4Z-lBtp+`^LpHVuaiuuf2@s zS^gY-`=1$5KnWQIOdb18|F`((YyRNnFWFmP|7F~kaW^8oTv21@0#G&D8B35Khf9D5+ z{#Ex%2AJnUffs3cViv$4YZV^`3)(wtQhKMQH;9ook@RrQJ$Q!n! zTq!dO@wHxvkhx}d?l6A|> zzPb*bfrhPpSKWo9=X3Ny`F=AyOl;_*18^K=nH+!7$mqLm|0E*?`Euvl$G#dfi7a$G zdsX-mPF$Q$eGotzVg9k=!+{`>MaA!8OYqY}je?1%_fHXBNagEuw%>76ejl>na{693 zN>MIn$&}{7uQ-U-#~RJV(YFv+DHrzcHjo$4WX~An5ezU6fh!@zoUQYbC4$<({RL~a zA0Y42UkD{6`9&3=Dx z7O6Inet`?LmdBv-B91r58>y;T#@6IEPTh^)m>_0FTCT4C=s*4Nz9o&Vq?8_<*%L-) zPq=Z8O+OKz$YlootE6OQEk7Cpt}nW&-|7don2wYXo$1nW#rGTt&t_8gU^u@o3h8ej zg<=O^);qIdif1$6c~#xdx7dEloGL79Yp@tLPYsKI%?|WjZ!+u7{Pu#-NXh9XaH&aPlUj48B zM)tr*QC_kafPR8f;s2b)dwAgS+_o|=pOqZ2!*Gt23pHC3+ceuX@LJ6U+dvipE#h}T zcNR*N8xegYxOo1fVo}G^;}_X_=N=#D0)*irjo;*bpX-c?L&^;#QZOq;F`IoF1Q}%b z&=0VjkL+oYSyfER6Blv1Gq=&`vwnb7jdc zzch_aE8r+(P|u&EkO0#E&(YjM86dvQl@h%E5pTx*1$Dg{D8ITw!GPVIusC6U-vv;A zU53=tC;2hRdKwrzmG^@>JfsN>4Hk`RO_jxXr>XM{d>~bX?p@TO2O*>uz7W=<=Dj&D z(1rVijs<^yuiO4KTyWpo>v^4tH!V5>Iik2{kec#|oALS^kZ&x@b2^2r7dv1wyCSid zO`ohH(EIVa5pnpM+wr90Q2DR4Z!jx|>Zz6k2^0iVwJINx-_fIb`oAA9vlwcXn8d*S ztQ6K1s}vBKsDXOqVJ1j4hxV-*x8>lU(Q@A|VgRI#)J+DXezGU|egFl>S(uvsf*yLk zkuTfl`%sFHmgt>5pa9s>{S05uJX;#(i;CZ<}?|L*RO51z@!rVSx2WHYSF zwohP?3xZlsSYA!PzIp~}hxPrKYAo-hD9{zJM$d?=4)%i-DlWXS7fRa{# z-dX{WdS~IagBrpU>EOEt{15!WCMfhZkU=8h2JVFVZIE@oCQ^ZP#sRKp^G)9_DAd;2 z)O5LSSAgu*tCl(Ty5jMfhUG#C(ex#Z{VM@1EY7EO{<3gE<0oqcDu&y7JYh|@E(z%0 z`=WV7m|zU`MGMVGKAE}=6f3J;VG|v8#-Vk6QSHC?EJY>7`{RASV&uR}BGa^#{ngEu z*LrwK@$}buKZkiAcw`~GZ#xyz(mX&SGkv`xqnyU#pRyf3et->Sjm$Y6MRVdvMR(#x zUKj*>@Ugdl|Lhpnt8|~16Qdk^gDk?u`rRov&ApEmuU#fh3U z6*@Kvt{N$A=6Lw$2HDC-cv{N25#RvZ-h`PtzkGYTP|6)USpsqb`XVTLH(q_v+TlU3 z-QVEy&NVO5+^qc5x)FTr2LSm~_=nQrKQ^2>13ZaxN;u;`z&`HVCV=8YMzY3|T}D*u zSAK|w{vZjDj#k;8?U-zwirOpX6~o2X3b$o{;K1&cRD&;=jkRo2TqvV4eEWUo6#J&u zDvvk57`=o~_|PT!P?khT^OIO|sgz1%k+K$t4&@!cXGdYF@Enb3q6qLzp}035WNAA& z%Gn}ssqIsF`z#bUxw|2}J3QDVbvC=3&l0}!32$`bfWZj4rmMsx%jKi-i3dp;{n4Dd9u>hV} zEVZ|v2yq|`L=z3lnij8TYK06{Yi}A|{Ue;Jza4qWU^F8*9CUy2<7h;KqUi%cD?VeC zXcXQmusLr+R#Uz`i7--SIZNX;*igNio?XP0U|_5D$-u4Lk(cn`@~Vv0@N#6`ME+9I;Y{EG&E)%gdwqAu2ZY!)ola-26AfYVwZ|Kj9jXl#b--yr{95KoPq8GN;lh zk??nxBGUkv>KkSH7FGSLAv69F@Y>V9p@=^r`50kNn0Tel8lU%^2jIwH6e5L8254)f zyzy@%d}{*~JTh?kaJ|puIMnL7x#A{s`Z)TZAKptB2C(8zu>Y{Xw#rBF+%t;c z6OaK1l8?HUxy7p1Roen5Y69u`nz<=#LOj9hbux9m*#u{W%?tbEBT(Z|mH3 zZ=J@Ke{t*crsXLZ?t`sGyPb%nUzZ7Par8SbOn3yTM zP;qGHLD66hErEoEB{?SG^01wyqPXR$?bL?6B}P^5v56`(Qv7J*___*=QM*2W#6S|> z5QC+YLWFCNVenD_4!0wMb;=MT6JsfF8XUPt#~dP4GG2fW%9q-ZH+zTNvVf}HA^Qfo$N;YybleU##GVvr|o2?d^fR4jh#rBjPNid1F zFm)%jv0^V{pb*lbEB>KYF#RZkvIe?ytSo=Z!p9k^J9lnt`Fx5Q2JX z>am?3b2V^{et(o9sII9A>70g%*F;?1ssd+%ct=O8BU7U(Z$*{EkU^Wnjj!Atw}Pw* z_I_B?<{4=GI-L%n5G5sW#f24SZY8u5a2-sA=@23LO-dWq6ROtM*3m)g+S=NqLF$!3 z8bd8RDh%cwBe;tCrluT^^F8F*qyLQI+x5_L{jTLOnD|FoO7sKR>}Z>&Mxt0^gcuN>Fw>Ei}cYm z_3cR6t165w+H;i=#4L79^*W<~6Lp0F`Mv=!m;GTBnq;2q>e>HzTM~_nj#BqU4*>!U zbD^TPrEl{b)avRgDc`Fn>tsW5*6-g4kw7$Q!Ryl!{XNJtxgr#n4Wo^sJSQ7dN^r#| z?^8mWMtJ@m<}TFp6+@BM`0U+MF|XI_^O$y^1=#0}&uJ@IjLYxI2?f=cKO-U{G6L*< z7%gY3xA*kqb>5q!A($q6st9Rs2hQt{fd-C&ophYuH}&Z9C=nMpO#1Kefkq;%@h94EmPT-Ba)OL=01*$H&O^_7u9Dw7jjx z6_8pYRcA0HoaXeEvj~&ujeE#azC6u6A4fiJdxKkDEQ7(zs5d_AO%>Q5Rt$>1EI;vx zB-wfU@@Baus}Yicw5Ll=OE35a6s?dl%2N{9GB@e+DBHnF%&OS_yLw`P7c5z*?&dbO zU-tXH-Day3LpJMjF(sr@oT6EheWl0ZfILJ>uCgd?z#kMR3ssmB=1 zg{Vujxwv|(bbm_JcTj9J!_@C-Fnqm$4Hy?k{fv3tzy=+odoji)^pSY za&shkz#1gvCzL7;2nYxj9_QAV^|?ZRPnlB{F$8NL2;A_ahurxnq=)8L?tGYoq0AiH zJQ#5)hYSury=*Lb%FY8cw)0;`0#ca0>eS0lTQjQ@c?)ZzDyEw9u#D*<3Z7)>tBba8 zU5%O<)HZdoOU^o3XwKV(MN%=yent?zr?KG&4Wb1Am!9BwgsCZozPi3VYVdtnV$;Zz z{*xwj?Y?u!g7tmj!4ZyjuV9dUPOZTP5XX+X+|%@19kq%k;)Ie6$7~HR8(|Sfn$Z4-4L6Mi3!=- zI!>|EmoTfDg&Qm;kUU9kq8JwmFY6k7Q8(tKqRnyHq@yino&XT>saoNNl zMOy)>Nt)P=#W4U2;oH0NzE=0Cs$*fT^tAefCciis5lL|VQdrh%Lm%rr8cwS)GE#3} zN{+p@wfYnm7M2>ED;IoVx_!t07<=3WyV0o`4|A*d7G~foo#^x5;h3))0JQ=^E1kIW zb$oxGRZ^TU#wENPk$&?SHIclaRx)=e+%`RB>2r zCkI$B)b#?)W5*E(+*T)HfYvTz_jQF?;H72_wM*+tlwfvLQ&W>;NyIYWPjc`30T}uR zwd?8)hW|y+oHo5{cB7GEw~d)vs-t?NU@iudZp(Zq@$ghE$YTgQCL~W8<4fP&pfHVG zSaSIAJ5_4TJBqu@f(!9XkMNzgdZ?}qS4l0 z?220JmQ`p4bIQR4OA)N)C>Jd+kYk5ir>{B3Bt-{)T$hy`0)5t^KXK68Zw(wAOSslG zp)Eztwqs=hS#Qzqyq+lDi1f4xhRG9#OWP=oOpfRTD zzv~Npx{OFafKd)j)gCUmDv|+I@QA}@9*nCPk}hIHbPh%&I}7&qi;H419@n!_y|7_h z(zic;82P+U@o($y7lkV^h75M8z!g3d3t6ho=YJj^@z;Awfsn3)&$pbef&sQsM`EeZ z;pC}&R7G|Hp|lg|A?$~APX;DT?E(8u;%0+km`kc0Hx-LypZSJjGYcbR05>R-k&;3I zm-;f913XmpSq<;~&9Ne$ooS4ko)^YI3Wqodgn&fE9}2Af>e~-#D)#sH?>VodEzxPh zoSvTE_*R-y^Q3RU>UsB3;Awbx_}j7ku<<)!J(uFf>yq+~i;U5%1cUYuK{7w@juCwF zFy7BW3^YpX;zS!SJsyHU>Hea6_d8_r#;Xv>Ibj(&IMj2dyTYeM4MiI7G%9t<*%qo4Xum1H!Ey?O{vXbMxBMuG_XPrERzX$ zaSJiginrRu4a>zD@Xol$$;f#nZpv;(J|OXVz5OE9)F*wP>(y%q@8j!gCW`5oof3Fz zCAs>dR_yamv$2;~8-gB?ATS;${hP+l5uwV1MC)rqnpiMxvu^bgQP2yeqL1&{%jYHg z6G{pcT|_G_VYZq5&2xa zD9p2iiF5B@yp9rl1pOwp?TbhOjiX4tf?;^f1jRV9lC+QO`n6g2x4JhwyQj}b$s+g? zD|N^%GnWb_&DE4NgxKQZaIOpSrjl*4)EaL=3a#^sU5pt8Qj=x0-}|ZJWTXlk?SfJV zTzOp%FoHuWPVk{5f}xCAN9XGnh15u^Rw=$COFo}^xwbNLD;$ZY*vG-48tw76JUXMC zDr7rhWCmllWBbQtqv#GIH1Gj`e!Yo%ngCTjQJh@H|L-&C-=Vw%2Htd( zcHsVgtl*@!O^)zk4#N9q{xqGQD3xc~q{Cx85;m#(pc0L@#QA6t)BZ-DjRhCp$cel5 zC}gk0S)zd{c6?U(o{T1@&NmM7YWr+Q+zBz03XB6W0C}QSO4W6vI1~{~7j)(=(y9&a zE8b?=lbIJJ=%ZrLRtpsd_OiBLyf)f;qmf}TuUP+G$H^NZ!to8CSS35XlL~@2#e2h0 z+^u*7syG7S;c@kn*)NSkzpW^!KL`(ahKvD-6F+~*YtJFrbro3s*C*<=8YRB5E^yxv z>vJ}3YIeBN2}-+qy<3uvk?(olw3tVg?ka3PSVlo~vjiKYBLQ-#Nl_lE0|=#IK|&gj zOY}Ei(RtZ@ZOl)$1Ob>cpDYq|Uqz>>n46oUi{s3Hp#B*^iCECY(n(keC@H_935Y3Q zH<6{JdAVW64vYEV0b|-Ecj_p&C(wA0eQ_vlS2|#hE>W{|zMf%gSSq0jmtgEqzi2%l z(iSzs?≶suyKf1?zgA!UX%HT9bV-aZ#MpmE|bc715P* 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 literal 0 HcmV?d00001 diff --git a/examples/http/requirements.txt b/examples/http/requirements.txt new file mode 100644 index 0000000000..7e1060246f --- /dev/null +++ b/examples/http/requirements.txt @@ -0,0 +1 @@ +flask diff --git a/examples/trace/server.py b/examples/http/server.py similarity index 75% rename from examples/trace/server.py rename to examples/http/server.py index 3632540e21..82cb070c27 100755 --- a/examples/trace/server.py +++ b/examples/http/server.py @@ -14,6 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + import flask import requests @@ -22,30 +24,41 @@ from opentelemetry.ext.wsgi import OpenTelemetryMiddleware from opentelemetry.sdk.trace import Tracer from opentelemetry.sdk.trace.export import ( + BatchExportSpanProcessor, ConsoleSpanExporter, - SimpleExportSpanProcessor, ) +if os.getenv("EXPORTER") == "jaeger": + from opentelemetry.ext.jaeger import JaegerSpanExporter + + exporter = JaegerSpanExporter( + service_name="http-server", + agent_host_name="localhost", + agent_port=6831, + ) +else: + exporter = ConsoleSpanExporter() + # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. trace.set_preferred_tracer_implementation(lambda T: Tracer()) +tracer = trace.tracer() + +# SpanExporter receives the spans and send them to the target location. +span_processor = BatchExportSpanProcessor(exporter) +tracer.add_span_processor(span_processor) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(trace.tracer()) - -# SpanExporter receives the spans and send them to the target location. -span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter()) -trace.tracer().add_span_processor(span_processor) - +http_requests.enable(tracer) app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) @app.route("/") def hello(): - with trace.tracer().start_as_current_span("parent"): + with tracer.start_as_current_span("parent"): requests.get("https://www.wikipedia.org/wiki/Rabbit") return "hello" diff --git a/examples/http/tests/__init__.py b/examples/http/tests/__init__.py new file mode 100644 index 0000000000..d853a7bcf6 --- /dev/null +++ b/examples/http/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/examples/http/tests/test_http.py b/examples/http/tests/test_http.py new file mode 100644 index 0000000000..7aa3f93c15 --- /dev/null +++ b/examples/http/tests/test_http.py @@ -0,0 +1,36 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import subprocess +import unittest +from time import sleep + + +class TestHttpExample(unittest.TestCase): + @classmethod + def setup_class(cls): + dirpath = os.path.dirname(os.path.realpath(__file__)) + server_script = "{}/../server.py".format(dirpath) + cls.server = subprocess.Popen([server_script]) + sleep(1) + + def test_http(self): + dirpath = os.path.dirname(os.path.realpath(__file__)) + test_script = "{}/../tracer_client.py".format(dirpath) + output = subprocess.check_output(test_script).decode() + self.assertIn('name="/"', output) + + @classmethod + def teardown_class(cls): + cls.server.terminate() diff --git a/examples/trace/client.py b/examples/http/tracer_client.py similarity index 79% rename from examples/trace/client.py rename to examples/http/tracer_client.py index 662cea8d96..671d1d71f9 100755 --- a/examples/trace/client.py +++ b/examples/http/tracer_client.py @@ -14,29 +14,41 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + import requests from opentelemetry import trace from opentelemetry.ext import http_requests from opentelemetry.sdk.trace import Tracer from opentelemetry.sdk.trace.export import ( + BatchExportSpanProcessor, ConsoleSpanExporter, - SimpleExportSpanProcessor, ) +if os.getenv("EXPORTER") == "jaeger": + from opentelemetry.ext.jaeger import JaegerSpanExporter + + exporter = JaegerSpanExporter( + service_name="http-client", + agent_host_name="localhost", + agent_port=6831, + ) +else: + exporter = ConsoleSpanExporter() + # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. trace.set_preferred_tracer_implementation(lambda T: Tracer()) tracer = trace.tracer() +# SpanExporter receives the spans and send them to the target location. +span_processor = BatchExportSpanProcessor(exporter) +tracer.add_span_processor(span_processor) + # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. http_requests.enable(tracer) - -# SpanExporter receives the spans and send them to the target location. -span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter()) -tracer.add_span_processor(span_processor) - response = requests.get(url="http://127.0.0.1:5000/") span_processor.shutdown() diff --git a/tox.ini b/tox.ini index 30e1d3745b..8f0d02d1cc 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} - pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} + pypy3-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} py3{4,5,6,7,8}-coverage ; Coverage is temporarily disabled for pypy3 due to the pytest bug. @@ -38,6 +38,8 @@ changedir = test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-example-app: examples/opentelemetry-example-app/tests + test-example-basic-tracer: examples/basic_tracer/tests + test-example-http: examples/http/tests test-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests commands_pre = @@ -49,6 +51,14 @@ commands_pre = example-app: pip install {toxinidir}/ext/opentelemetry-ext-http-requests example-app: pip install {toxinidir}/ext/opentelemetry-ext-wsgi example-app: pip install {toxinidir}/examples/opentelemetry-example-app + example-basic-tracer: pip install -e {toxinidir}/opentelemetry-api + example-basic-tracer: pip install -e {toxinidir}/opentelemetry-sdk + example-http: pip install -e {toxinidir}/opentelemetry-api + example-http: pip install -e {toxinidir}/opentelemetry-sdk + example-http: pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests + example-http: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi + example-http: pip install -r {toxinidir}/examples/http/requirements.txt + ext: pip install {toxinidir}/opentelemetry-api wsgi: pip install {toxinidir}/ext/opentelemetry-ext-wsgi pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo @@ -125,7 +135,9 @@ commands = ext/opentelemetry-ext-pymongo/tests/ \ ext/opentelemetry-ext-wsgi/tests/ \ examples/opentelemetry-example-app/src/opentelemetry_example_app/ \ - examples/opentelemetry-example-app/tests/ + examples/opentelemetry-example-app/tests/ \ + examples/basic_tracer/ \ + examples/http/ flake8 . isort --check-only --diff --recursive . From fb3e71522d9937cb8b559ebed780114ab3352221 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Sun, 17 Nov 2019 10:10:25 -0800 Subject: [PATCH 0139/1517] Change v3 alpha release date to 12/6 (#294) --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bb199eebd5..19f65b5d1a 100644 --- a/README.md +++ b/README.md @@ -158,16 +158,16 @@ estimates, and subject to change. Future releases targets include: -| Component | Version | Target Date | -| ----------------------------------- | ---------- | ---------------- | -| Zipkin Trace Exporter | Alpha v0.3 | November 15 2019 | -| W3C Correlation Context Propagation | Alpha v0.3 | November 15 2019 | -| Support for Tags/Baggage | Alpha v0.3 | November 15 2019 | -| Metrics Aggregation | Alpha v0.3 | November 15 2019 | -| gRPC Integrations | Alpha v0.3 | November 15 2019 | -| Prometheus Metrics Exporter | Alpha v0.3 | November 15 2019 | +| Component | Version | Target Date | +| ----------------------------------- | ---------- | --------------- | +| Zipkin Trace Exporter | Alpha v0.3 | December 6 2019 | +| W3C Correlation Context Propagation | Alpha v0.3 | December 6 2019 | +| Support for Tags/Baggage | Alpha v0.3 | December 6 2019 | +| Metrics Aggregation | Alpha v0.3 | December 6 2019 | +| gRPC Integrations | Alpha v0.3 | December 6 2019 | +| Prometheus Metrics Exporter | Alpha v0.3 | December 6 2019 | | Component | Version | Target Date | | ---------------------- | ---------- | ---------------- | | OpenCensus Bridge | Alpha v0.4 | December 31 2019 | -| Metrics SDK (Complete) | Alpha v0.4 | December 31 2019 | +| Metrics SDK (Complete) | Alpha v0.4 | December 31 2019 | From 077a08e36f48f197b85df7c628f393fb8888bab5 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 21 Nov 2019 08:13:14 -0800 Subject: [PATCH 0140/1517] Remove Azure Exporters out of main repo (master) (#272) --- README.md | 8 + .../CHANGELOG.md | 15 -- .../README.rst | 17 -- .../examples/client.py | 30 --- .../examples/server.py | 44 ---- .../examples/trace.py | 27 --- ext/opentelemetry-ext-azure-monitor/setup.cfg | 46 ---- ext/opentelemetry-ext-azure-monitor/setup.py | 26 --- .../ext/azure_monitor/__init__.py | 23 -- .../ext/azure_monitor/protocol.py | 201 ------------------ .../opentelemetry/ext/azure_monitor/trace.py | 173 --------------- .../opentelemetry/ext/azure_monitor/util.py | 42 ---- .../ext/azure_monitor/version.py | 15 -- .../tests/__init__.py | 0 tox.ini | 5 - 15 files changed, 8 insertions(+), 664 deletions(-) delete mode 100644 ext/opentelemetry-ext-azure-monitor/CHANGELOG.md delete mode 100644 ext/opentelemetry-ext-azure-monitor/README.rst delete mode 100644 ext/opentelemetry-ext-azure-monitor/examples/client.py delete mode 100644 ext/opentelemetry-ext-azure-monitor/examples/server.py delete mode 100644 ext/opentelemetry-ext-azure-monitor/examples/trace.py delete mode 100644 ext/opentelemetry-ext-azure-monitor/setup.cfg delete mode 100644 ext/opentelemetry-ext-azure-monitor/setup.py delete mode 100644 ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/__init__.py delete mode 100644 ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/protocol.py delete mode 100644 ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py delete mode 100644 ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/util.py delete mode 100644 ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/version.py delete mode 100644 ext/opentelemetry-ext-azure-monitor/tests/__init__.py diff --git a/README.md b/README.md index 19f65b5d1a..793d46d588 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,14 @@ exporter.shutdown() See the [API documentation](https://open-telemetry.github.io/opentelemetry-python/) for more detail, and the [examples folder](./examples) for a more sample code. +## Extensions + +### Third-party exporters + +OpenTelemetry supports integration with the following third-party exporters. + +- [Azure Monitor](https://github.com/microsoft/opentelemetry-exporters-python/tree/master/azure_monitor) + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/ext/opentelemetry-ext-azure-monitor/CHANGELOG.md b/ext/opentelemetry-ext-azure-monitor/CHANGELOG.md deleted file mode 100644 index ac7fc896a3..0000000000 --- a/ext/opentelemetry-ext-azure-monitor/CHANGELOG.md +++ /dev/null @@ -1,15 +0,0 @@ -# Changelog - -## Unreleased - -## 0.2a0 - -Released 2019-10-29 - -- Updates for core library changes - -## 0.1a0 - -Released 2019-09-30 - -- Initial release diff --git a/ext/opentelemetry-ext-azure-monitor/README.rst b/ext/opentelemetry-ext-azure-monitor/README.rst deleted file mode 100644 index 976d9a531e..0000000000 --- a/ext/opentelemetry-ext-azure-monitor/README.rst +++ /dev/null @@ -1,17 +0,0 @@ -OpenTelemetry Azure Monitor Exporters -===================================== - -This library provides integration with Microsoft Azure Monitor. - -Installation ------------- - -:: - - pip install opentelemetry-ext-azure-monitor - -References ----------- - -* `Azure Monitor `_ -* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-azure-monitor/examples/client.py b/ext/opentelemetry-ext-azure-monitor/examples/client.py deleted file mode 100644 index ff954788e6..0000000000 --- a/ext/opentelemetry-ext-azure-monitor/examples/client.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2019, OpenCensus 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 requests - -from opentelemetry import trace -from opentelemetry.ext import http_requests -from opentelemetry.ext.azure_monitor import AzureMonitorSpanExporter -from opentelemetry.sdk.trace import Tracer -from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - -trace.set_preferred_tracer_implementation(lambda T: Tracer()) -tracer = trace.tracer() -http_requests.enable(tracer) -span_processor = BatchExportSpanProcessor(AzureMonitorSpanExporter()) -tracer.add_span_processor(span_processor) - -response = requests.get(url="http://127.0.0.1:5000/") -span_processor.shutdown() diff --git a/ext/opentelemetry-ext-azure-monitor/examples/server.py b/ext/opentelemetry-ext-azure-monitor/examples/server.py deleted file mode 100644 index 9374c986a4..0000000000 --- a/ext/opentelemetry-ext-azure-monitor/examples/server.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2019, OpenCensus 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 flask -import requests - -from opentelemetry import trace -from opentelemetry.ext import http_requests -from opentelemetry.ext.azure_monitor import AzureMonitorSpanExporter -from opentelemetry.ext.wsgi import OpenTelemetryMiddleware -from opentelemetry.sdk.trace import Tracer -from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - -trace.set_preferred_tracer_implementation(lambda T: Tracer()) - -http_requests.enable(trace.tracer()) -span_processor = BatchExportSpanProcessor(AzureMonitorSpanExporter()) -trace.tracer().add_span_processor(span_processor) - -app = flask.Flask(__name__) -app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) - - -@app.route("/") -def hello(): - with trace.tracer().start_as_current_span("parent"): - requests.get("https://www.wikipedia.org/wiki/Rabbit") - return "hello" - - -if __name__ == "__main__": - app.run(debug=True) - span_processor.shutdown() diff --git a/ext/opentelemetry-ext-azure-monitor/examples/trace.py b/ext/opentelemetry-ext-azure-monitor/examples/trace.py deleted file mode 100644 index 75b7dfa151..0000000000 --- a/ext/opentelemetry-ext-azure-monitor/examples/trace.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2019, OpenCensus Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from opentelemetry import trace -from opentelemetry.ext.azure_monitor import AzureMonitorSpanExporter -from opentelemetry.sdk.trace import Tracer -from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor - -trace.set_preferred_tracer_implementation(lambda T: Tracer()) -tracer = trace.tracer() -tracer.add_span_processor( - SimpleExportSpanProcessor(AzureMonitorSpanExporter()) -) - -with tracer.start_as_current_span("hello") as span: - print("Hello, World!") diff --git a/ext/opentelemetry-ext-azure-monitor/setup.cfg b/ext/opentelemetry-ext-azure-monitor/setup.cfg deleted file mode 100644 index 3110ed3531..0000000000 --- a/ext/opentelemetry-ext-azure-monitor/setup.cfg +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-ext-azure-monitor -description = Azure Monitor integration for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-azure-monitor -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 3 - Alpha - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - -[options] -python_requires = >=3.4 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api - opentelemetry-sdk - -[options.packages.find] -where = src diff --git a/ext/opentelemetry-ext-azure-monitor/setup.py b/ext/opentelemetry-ext-azure-monitor/setup.py deleted file mode 100644 index 5f8afcb23a..0000000000 --- a/ext/opentelemetry-ext-azure-monitor/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "azure_monitor", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/__init__.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/__init__.py deleted file mode 100644 index 81222c546e..0000000000 --- a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The opentelemetry-ext-azure-monitor package provides integration with -Microsoft Azure Monitor. -""" - -from opentelemetry.ext.azure_monitor.trace import AzureMonitorSpanExporter -from opentelemetry.ext.azure_monitor.version import __version__ # noqa - -__all__ = ["AzureMonitorSpanExporter"] diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/protocol.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/protocol.py deleted file mode 100644 index ccdf5eef8d..0000000000 --- a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/protocol.py +++ /dev/null @@ -1,201 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -class BaseObject(dict): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for key in kwargs: - self[key] = kwargs[key] - - def __repr__(self): - tmp = {} - current = self - while True: - for item in current.items(): - if item[0] not in tmp: - tmp[item[0]] = item[1] - if ( - current._default # noqa pylint: disable=protected-access - == current - ): - break - current = current._default # noqa pylint: disable=protected-access - return repr(tmp) - - def __setattr__(self, name, value): - self[name] = value - - def __getattr__(self, name): - try: - return self[name] - except KeyError: - raise AttributeError( - "'{}' object has no attribute {}".format( - type(self).__name__, name - ) - ) - - def __getitem__(self, key): - if self._default is self: - return super().__getitem__(key) - if key in self: - return super().__getitem__(key) - return self._default[key] - - -BaseObject._default = BaseObject() # noqa pylint: disable=protected-access - - -class Data(BaseObject): - _default = BaseObject(baseData=None, baseType=None) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.baseData = self.baseData # noqa pylint: disable=invalid-name - self.baseType = self.baseType # noqa pylint: disable=invalid-name - - -class DataPoint(BaseObject): - _default = BaseObject( - ns="", - name="", - kind=None, - value=0.0, - count=None, - min=None, - max=None, - stdDev=None, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.name = self.name - self.value = self.value - - -class Envelope(BaseObject): - _default = BaseObject( - ver=1, - name="", - time="", - sampleRate=None, - seq=None, - iKey=None, - flags=None, - tags=None, - data=None, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.name = self.name - self.time = self.time - - -class Event(BaseObject): - _default = BaseObject(ver=2, name="", properties=None, measurements=None) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.ver = self.ver - self.name = self.name - - -class ExceptionData(BaseObject): - _default = BaseObject( - ver=2, - exceptions=[], - severityLevel=None, - problemId=None, - properties=None, - measurements=None, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.ver = self.ver - self.exceptions = self.exceptions - - -class Message(BaseObject): - _default = BaseObject( - ver=2, - message="", - severityLevel=None, - properties=None, - measurements=None, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.ver = self.ver - self.message = self.message - - -class MetricData(BaseObject): - _default = BaseObject(ver=2, metrics=[], properties=None) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.ver = self.ver - self.metrics = self.metrics - - -class RemoteDependency(BaseObject): - _default = BaseObject( - ver=2, - name="", - id="", - resultCode="", - duration="", - success=True, - data=None, - type=None, - target=None, - properties=None, - measurements=None, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.ver = self.ver - self.name = self.name - self.resultCode = self.resultCode # noqa pylint: disable=invalid-name - self.duration = self.duration - - -class Request(BaseObject): - _default = BaseObject( - ver=2, - id="", - duration="", - responseCode="", - success=True, - source=None, - name=None, - url=None, - properties=None, - measurements=None, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.ver = self.ver - self.id = self.id # noqa pylint: disable=invalid-name - self.duration = self.duration - self.responseCode = ( # noqa pylint: disable=invalid-name - self.responseCode - ) - self.success = self.success diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py deleted file mode 100644 index 16f9252fd0..0000000000 --- a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import logging -from urllib.parse import urlparse - -import requests - -from opentelemetry.ext.azure_monitor import protocol, util -from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult -from opentelemetry.sdk.util import ns_to_iso_str -from opentelemetry.trace import Span, SpanKind - -logger = logging.getLogger(__name__) - - -class AzureMonitorSpanExporter(SpanExporter): - def __init__(self, **options): - self.options = util.Options(**options) - if not self.options.instrumentation_key: - raise ValueError("The instrumentation_key is not provided.") - - def export(self, spans): - envelopes = tuple(map(self.span_to_envelope, spans)) - - try: - response = requests.post( - url=self.options.endpoint, - data=json.dumps(envelopes), - headers={ - "Accept": "application/json", - "Content-Type": "application/json; charset=utf-8", - }, - timeout=self.options.timeout, - ) - except requests.RequestException as ex: - logger.warning("Transient client side error %s.", ex) - return SpanExportResult.FAILED_RETRYABLE - - text = "N/A" - data = None # noqa pylint: disable=unused-variable - try: - text = response.text - except Exception as ex: # noqa pylint: disable=broad-except - logger.warning("Error while reading response body %s.", ex) - else: - try: - data = json.loads(text) # noqa pylint: disable=unused-variable - except Exception: # noqa pylint: disable=broad-except - pass - - if response.status_code == 200: - logger.info("Transmission succeeded: %s.", text) - return SpanExportResult.SUCCESS - - if response.status_code in ( - 206, # Partial Content - 429, # Too Many Requests - 500, # Internal Server Error - 503, # Service Unavailable - ): - return SpanExportResult.FAILED_RETRYABLE - - return SpanExportResult.FAILED_NOT_RETRYABLE - - @staticmethod - def ns_to_duration(nanoseconds): - value = (nanoseconds + 500000) // 1000000 # duration in milliseconds - value, microseconds = divmod(value, 1000) - value, seconds = divmod(value, 60) - value, minutes = divmod(value, 60) - days, hours = divmod(value, 24) - return "{:d}.{:02d}:{:02d}:{:02d}.{:03d}".format( - days, hours, minutes, seconds, microseconds - ) - - def span_to_envelope(self, span): # noqa pylint: disable=too-many-branches - envelope = protocol.Envelope( - iKey=self.options.instrumentation_key, - tags=dict(util.azure_monitor_context), - time=ns_to_iso_str(span.start_time), - ) - envelope.tags["ai.operation.id"] = "{:032x}".format( - span.context.trace_id - ) - parent = span.parent - if isinstance(parent, Span): - parent = parent.context - if parent: - envelope.tags[ - "ai.operation.parentId" - ] = "|{:032x}.{:016x}.".format(parent.trace_id, parent.span_id) - if span.kind in (SpanKind.CONSUMER, SpanKind.SERVER): - envelope.name = "Microsoft.ApplicationInsights.Request" - data = protocol.Request( - id="|{:032x}.{:016x}.".format( - span.context.trace_id, span.context.span_id - ), - duration=self.ns_to_duration(span.end_time - span.start_time), - responseCode="0", - success=False, - properties={}, - ) - envelope.data = protocol.Data( - baseData=data, baseType="RequestData" - ) - if "http.method" in span.attributes: - data.name = span.attributes["http.method"] - if "http.route" in span.attributes: - data.name = data.name + " " + span.attributes["http.route"] - envelope.tags["ai.operation.name"] = data.name - if "http.url" in span.attributes: - data.url = span.attributes["http.url"] - if "http.status_code" in span.attributes: - status_code = span.attributes["http.status_code"] - data.responseCode = str(status_code) - data.success = 200 <= status_code < 400 - else: - envelope.name = "Microsoft.ApplicationInsights.RemoteDependency" - data = protocol.RemoteDependency( - name=span.name, - id="|{:032x}.{:016x}.".format( - span.context.trace_id, span.context.span_id - ), - resultCode="0", # TODO - duration=self.ns_to_duration(span.end_time - span.start_time), - success=True, # TODO - properties={}, - ) - envelope.data = protocol.Data( - baseData=data, baseType="RemoteDependencyData" - ) - if span.kind in (SpanKind.CLIENT, SpanKind.PRODUCER): - data.type = "HTTP" # TODO - if "http.url" in span.attributes: - url = span.attributes["http.url"] - # TODO: error handling, probably put scheme as well - data.name = urlparse(url).netloc - if "http.status_code" in span.attributes: - data.resultCode = str(span.attributes["http.status_code"]) - else: # SpanKind.INTERNAL - data.type = "InProc" - for key in span.attributes: - data.properties[key] = span.attributes[key] - if span.links: - links = [] - for link in span.links: - links.append( - { - "operation_Id": "{:032x}".format( - link.context.trace_id - ), - "id": "|{:032x}.{:016x}.".format( - link.context.trace_id, link.context.span_id - ), - } - ) - data.properties["_MS.links"] = json.dumps(links) - print(data.properties["_MS.links"]) - # TODO: tracestate, tags - return envelope diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/util.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/util.py deleted file mode 100644 index f97dbd3e33..0000000000 --- a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/util.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import locale -import os -import platform -import sys - -from opentelemetry.ext.azure_monitor.protocol import BaseObject -from opentelemetry.ext.azure_monitor.version import __version__ as ext_version -from opentelemetry.sdk.version import __version__ as opentelemetry_version - -azure_monitor_context = { - "ai.cloud.role": os.path.basename(sys.argv[0]) or "Python Application", - "ai.cloud.roleInstance": platform.node(), - "ai.device.id": platform.node(), - "ai.device.locale": locale.getdefaultlocale()[0], - "ai.device.osVersion": platform.version(), - "ai.device.type": "Other", - "ai.internal.sdkVersion": "py{}:ot{}:ext{}".format( - platform.python_version(), opentelemetry_version, ext_version - ), -} - - -class Options(BaseObject): - _default = BaseObject( - endpoint="https://dc.services.visualstudio.com/v2/track", - instrumentation_key=os.getenv("APPINSIGHTS_INSTRUMENTATIONKEY", None), - timeout=10.0, # networking timeout in seconds - ) diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/version.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/version.py deleted file mode 100644 index 93ef792d05..0000000000 --- a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.3.dev0" diff --git a/ext/opentelemetry-ext-azure-monitor/tests/__init__.py b/ext/opentelemetry-ext-azure-monitor/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tox.ini b/tox.ini index 8f0d02d1cc..88f1a85899 100644 --- a/tox.ini +++ b/tox.ini @@ -71,7 +71,6 @@ commands_pre = ; we have to install packages in editable mode. coverage: pip install -e {toxinidir}/opentelemetry-api coverage: pip install -e {toxinidir}/opentelemetry-sdk - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-azure-monitor coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim @@ -106,7 +105,6 @@ deps = commands_pre = pip install -e {toxinidir}/opentelemetry-api pip install -e {toxinidir}/opentelemetry-sdk - pip install -e {toxinidir}/ext/opentelemetry-ext-azure-monitor pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger pip install -e {toxinidir}/ext/opentelemetry-ext-pymongo @@ -122,9 +120,6 @@ commands = opentelemetry-api/tests/ \ opentelemetry-sdk/src/opentelemetry \ opentelemetry-sdk/tests/ \ - ext/opentelemetry-ext-azure-monitor/examples/ \ - ext/opentelemetry-ext-azure-monitor/src/ \ - ext/opentelemetry-ext-azure-monitor/tests/ \ ext/opentelemetry-ext-http-requests/src/ \ ext/opentelemetry-ext-http-requests/tests/ \ ext/opentelemetry-ext-jaeger/src/opentelemetry \ From da8b8d907c29fc358ca635114dac2967b6501563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Sat, 23 Nov 2019 01:27:55 +0100 Subject: [PATCH 0141/1517] Add Flask integration based on WSGI ext (#206) The flask integration has (only) two advantages over the plain WSGI middleware approach: - It can use the endpoint as span name (which is lower cardinality than the route; cf #270) - It can set the http.route attribute. In addition, it also has an easier syntax to enable (you don't have to know about Flask.wsgi_app). --- examples/opentelemetry-example-app/setup.py | 2 +- .../flask_example.py | 4 +- ext/opentelemetry-ext-flask/README.rst | 35 ++++ ext/opentelemetry-ext-flask/setup.cfg | 50 ++++++ ext/opentelemetry-ext-flask/setup.py | 26 +++ .../src/opentelemetry/ext/flask/__init__.py | 93 ++++++++++ .../src/opentelemetry/ext/flask/version.py | 15 ++ ext/opentelemetry-ext-flask/tests/__init__.py | 0 .../tests/test_flask_integration.py | 136 ++++++++++++++ ext/opentelemetry-ext-testutil/setup.cfg | 47 +++++ ext/opentelemetry-ext-testutil/setup.py | 26 +++ .../opentelemetry/ext/testutil/__init__.py | 0 .../src/opentelemetry/ext/testutil/version.py | 1 + .../ext/testutil/wsgitestutil.py | 41 +++++ ext/opentelemetry-ext-wsgi/setup.cfg | 4 + .../src/opentelemetry/ext/wsgi/__init__.py | 169 ++++++++++-------- .../tests/test_wsgi_middleware.py | 68 ++----- tox.ini | 21 ++- 18 files changed, 603 insertions(+), 135 deletions(-) create mode 100644 ext/opentelemetry-ext-flask/README.rst create mode 100644 ext/opentelemetry-ext-flask/setup.cfg create mode 100644 ext/opentelemetry-ext-flask/setup.py create mode 100644 ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py create mode 100644 ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py create mode 100644 ext/opentelemetry-ext-flask/tests/__init__.py create mode 100644 ext/opentelemetry-ext-flask/tests/test_flask_integration.py create mode 100644 ext/opentelemetry-ext-testutil/setup.cfg create mode 100644 ext/opentelemetry-ext-testutil/setup.py create mode 100644 ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/__init__.py create mode 100644 ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py create mode 100644 ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py diff --git a/examples/opentelemetry-example-app/setup.py b/examples/opentelemetry-example-app/setup.py index 73a3e20784..0e577cd264 100644 --- a/examples/opentelemetry-example-app/setup.py +++ b/examples/opentelemetry-example-app/setup.py @@ -38,7 +38,7 @@ "opentelemetry-api", "opentelemetry-sdk", "opentelemetry-ext-http-requests", - "opentelemetry-ext-wsgi", + "opentelemetry-ext-flask", "flask", "requests", ], diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 18dffa3000..85df625efe 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -21,7 +21,7 @@ import opentelemetry.ext.http_requests from opentelemetry import propagators, trace -from opentelemetry.ext.wsgi import OpenTelemetryMiddleware +from opentelemetry.ext.flask import instrument_app from opentelemetry.sdk.context.propagation.b3_format import B3Format from opentelemetry.sdk.trace import Tracer @@ -57,7 +57,7 @@ def configure_opentelemetry(flask_app: flask.Flask): # and the frameworks and libraries that are used together, automatically # creating Spans and propagating context as appropriate. opentelemetry.ext.http_requests.enable(trace.tracer()) - flask_app.wsgi_app = OpenTelemetryMiddleware(flask_app.wsgi_app) + instrument_app(flask_app) app = flask.Flask(__name__) diff --git a/ext/opentelemetry-ext-flask/README.rst b/ext/opentelemetry-ext-flask/README.rst new file mode 100644 index 0000000000..182f0960b2 --- /dev/null +++ b/ext/opentelemetry-ext-flask/README.rst @@ -0,0 +1,35 @@ +OpenTelemetry Flask tracing +=========================== + +This library builds on the OpenTelemetry WSGI middleware to track web requests +in Flask applications. In addition to opentelemetry-ext-wsgi, it supports +flask-specific features such as: + +* The Flask endpoint name is used as the Span name. +* The ``http.route`` Span attribute is set so that one can see which URL rule + matched a request. + +Usage +----- + +.. code-block:: python + + from flask import Flask + from opentelemetry.ext.flask import instrument_app + + app = Flask(__name__) + instrument_app(app) # This is where the magic happens. ✨ + + @app.route("/") + def hello(): + return "Hello!" + + if __name__ == "__main__": + app.run(debug=True) + + +References +---------- + +* `OpenTelemetry Project `_ +* `OpenTelemetry WSGI extension `_ diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg new file mode 100644 index 0000000000..1d4956a82f --- /dev/null +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -0,0 +1,50 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-flask +description = Flask tracing for OpenTelemetry (based on opentelemetry-ext-wsgi) +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-flask +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-ext-wsgi + +[options.extras_require] +test = + flask~=1.0 + opentelemetry-ext-testutil + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-flask/setup.py b/ext/opentelemetry-ext-flask/setup.py new file mode 100644 index 0000000000..34b27c6034 --- /dev/null +++ b/ext/opentelemetry-ext-flask/setup.py @@ -0,0 +1,26 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "flask", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py new file mode 100644 index 0000000000..eedc8d5998 --- /dev/null +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -0,0 +1,93 @@ +# Note: This package is not named "flask" because of +# https://github.com/PyCQA/pylint/issues/2648 + +import logging + +from flask import request as flask_request + +import opentelemetry.ext.wsgi as otel_wsgi +from opentelemetry import propagators, trace +from opentelemetry.util import time_ns + +logger = logging.getLogger(__name__) + +_ENVIRON_STARTTIME_KEY = object() +_ENVIRON_SPAN_KEY = object() +_ENVIRON_ACTIVATION_KEY = object() + + +def instrument_app(flask): + """Makes the passed-in Flask object traced by OpenTelemetry. + + You must not call this function multiple times on the same Flask object. + """ + + wsgi = flask.wsgi_app + + def wrapped_app(environ, start_response): + # We want to measure the time for route matching, etc. + # In theory, we could start the span here and use update_name later + # but that API is "highly discouraged" so we better avoid it. + environ[_ENVIRON_STARTTIME_KEY] = time_ns() + + def _start_response(status, response_headers, *args, **kwargs): + span = flask_request.environ.get(_ENVIRON_SPAN_KEY) + if span: + otel_wsgi.add_response_attributes( + span, status, response_headers + ) + else: + logger.warning( + "Flask environ's OpenTelemetry span missing at _start_response(%s)", + status, + ) + return start_response(status, response_headers, *args, **kwargs) + + return wsgi(environ, _start_response) + + flask.wsgi_app = wrapped_app + + flask.before_request(_before_flask_request) + flask.teardown_request(_teardown_flask_request) + + +def _before_flask_request(): + environ = flask_request.environ + span_name = flask_request.endpoint or otel_wsgi.get_default_span_name( + environ + ) + parent_span = propagators.extract( + otel_wsgi.get_header_from_environ, environ + ) + + tracer = trace.tracer() + + span = tracer.create_span( + span_name, parent_span, kind=trace.SpanKind.SERVER + ) + span.start(environ.get(_ENVIRON_STARTTIME_KEY)) + activation = tracer.use_span(span, end_on_exit=True) + activation.__enter__() + environ[_ENVIRON_ACTIVATION_KEY] = activation + environ[_ENVIRON_SPAN_KEY] = span + otel_wsgi.add_request_attributes(span, environ) + if flask_request.url_rule: + # For 404 that result from no route found, etc, we don't have a url_rule. + span.set_attribute("http.route", flask_request.url_rule.rule) + + +def _teardown_flask_request(exc): + activation = flask_request.environ.get(_ENVIRON_ACTIVATION_KEY) + if not activation: + logger.warning( + "Flask environ's OpenTelemetry activation missing at _teardown_flask_request(%s)", + exc, + ) + return + + if exc is None: + activation.__exit__(None, None, None) + else: + activation.__exit__( + type(exc), exc, getattr(exc, "__traceback__", None) + ) diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py new file mode 100644 index 0000000000..ed56d30324 --- /dev/null +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.3dev0" diff --git a/ext/opentelemetry-ext-flask/tests/__init__.py b/ext/opentelemetry-ext-flask/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py new file mode 100644 index 0000000000..dfb9dee885 --- /dev/null +++ b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py @@ -0,0 +1,136 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from flask import Flask +from werkzeug.test import Client +from werkzeug.wrappers import BaseResponse + +import opentelemetry.ext.flask as otel_flask +from opentelemetry import trace as trace_api +from opentelemetry.ext.testutil.wsgitestutil import WsgiTestBase + + +class TestFlaskIntegration(WsgiTestBase): + def setUp(self): + super().setUp() + + self.span_attrs = {} + + def setspanattr(key, value): + self.assertIsInstance(key, str) + self.span_attrs[key] = value + + self.span.set_attribute = setspanattr + + self.app = Flask(__name__) + + def hello_endpoint(helloid): + if helloid == 500: + raise ValueError(":-(") + return "Hello: " + str(helloid) + + self.app.route("/hello/")(hello_endpoint) + + otel_flask.instrument_app(self.app) + self.client = Client(self.app, BaseResponse) + + def test_simple(self): + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + + self.create_span.assert_called_with( + "hello_endpoint", + trace_api.INVALID_SPAN_CONTEXT, + kind=trace_api.SpanKind.SERVER, + ) + self.assertEqual(1, self.span.start.call_count) + + # TODO: Change this test to use the SDK, as mocking becomes painful + + self.assertEqual( + self.span_attrs, + { + "component": "http", + "http.method": "GET", + "http.host": "localhost", + "http.url": "http://localhost/hello/123", + "http.route": "/hello/", + "http.status_code": 200, + "http.status_text": "OK", + }, + ) + + def test_404(self): + resp = self.client.post("/bye") + self.assertEqual(404, resp.status_code) + resp.close() + + self.create_span.assert_called_with( + "/bye", + trace_api.INVALID_SPAN_CONTEXT, + kind=trace_api.SpanKind.SERVER, + ) + self.assertEqual(1, self.span.start.call_count) + + # Nope, this uses Tracer.use_span(end_on_exit) + # self.assertEqual(1, self.span.end.call_count) + # TODO: Change this test to use the SDK, as mocking becomes painful + + self.assertEqual( + self.span_attrs, + { + "component": "http", + "http.method": "POST", + "http.host": "localhost", + "http.url": "http://localhost/bye", + "http.status_code": 404, + "http.status_text": "NOT FOUND", + }, + ) + + def test_internal_error(self): + resp = self.client.get("/hello/500") + self.assertEqual(500, resp.status_code) + resp.close() + + self.create_span.assert_called_with( + "hello_endpoint", + trace_api.INVALID_SPAN_CONTEXT, + kind=trace_api.SpanKind.SERVER, + ) + self.assertEqual(1, self.span.start.call_count) + + # Nope, this uses Tracer.use_span(end_on_exit) + # self.assertEqual(1, self.span.end.call_count) + # TODO: Change this test to use the SDK, as mocking becomes painful + + self.assertEqual( + self.span_attrs, + { + "component": "http", + "http.method": "GET", + "http.host": "localhost", + "http.url": "http://localhost/hello/500", + "http.route": "/hello/", + "http.status_code": 500, + "http.status_text": "INTERNAL SERVER ERROR", + }, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/ext/opentelemetry-ext-testutil/setup.cfg b/ext/opentelemetry-ext-testutil/setup.cfg new file mode 100644 index 0000000000..170e949cf6 --- /dev/null +++ b/ext/opentelemetry-ext-testutil/setup.cfg @@ -0,0 +1,47 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +[metadata] +name = opentelemetry-ext-testutil +description = Test utilities for OpenTelemetry unit tests +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-testutil +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-ext-wsgi + +[options.extras_require] +test = flask~=1.0 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-testutil/setup.py b/ext/opentelemetry-ext-testutil/setup.py new file mode 100644 index 0000000000..9de576d356 --- /dev/null +++ b/ext/opentelemetry-ext-testutil/setup.py @@ -0,0 +1,26 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "testutil", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/__init__.py b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py new file mode 100644 index 0000000000..70ddecedf8 --- /dev/null +++ b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py @@ -0,0 +1 @@ +__version__ = "0.3dev0" diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py new file mode 100644 index 0000000000..d9cc9ff6a9 --- /dev/null +++ b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py @@ -0,0 +1,41 @@ +import io +import unittest +import unittest.mock as mock +import wsgiref.util as wsgiref_util + +from opentelemetry import trace as trace_api + + +class WsgiTestBase(unittest.TestCase): + def setUp(self): + tracer = trace_api.tracer() + self.span = mock.create_autospec(trace_api.Span, spec_set=True) + self.create_span_patcher = mock.patch.object( + tracer, + "create_span", + autospec=True, + spec_set=True, + return_value=self.span, + ) + self.create_span = self.create_span_patcher.start() + self.write_buffer = io.BytesIO() + self.write = self.write_buffer.write + + self.environ = {} + wsgiref_util.setup_testing_defaults(self.environ) + + self.status = None + self.response_headers = None + self.exc_info = None + + def tearDown(self): + self.create_span_patcher.stop() + + def start_response(self, status, response_headers, exc_info=None): + # The span should have started already + self.assertEqual(1, self.span.start.call_count) + + self.status = status + self.response_headers = response_headers + self.exc_info = exc_info + return self.write diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index 4405e37a30..1db49209be 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -41,5 +41,9 @@ packages=find_namespace: install_requires = opentelemetry-api +[options.extras_require] +test = + opentelemetry-ext-testutil + [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 5e619eb7c6..a2cf163342 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -24,75 +24,108 @@ from opentelemetry import propagators, trace from opentelemetry.ext.wsgi.version import __version__ # noqa +from opentelemetry.util import time_ns + + +def get_header_from_environ( + environ: dict, header_name: str +) -> typing.List[str]: + """Retrieve a HTTP header value from the PEP3333-conforming WSGI environ. + + Returns: + A list with a single string with the header value if it exists, else an empty list. + """ + environ_key = "HTTP_" + header_name.upper().replace("-", "_") + value = environ.get(environ_key) + if value is not None: + return [value] + return [] + + +def add_request_attributes(span, environ): + """Adds HTTP request attributes from the PEP3333-conforming WSGI environ to span.""" + + span.set_attribute("component", "http") + span.set_attribute("http.method", environ["REQUEST_METHOD"]) + + host = environ.get("HTTP_HOST") + if not host: + host = environ["SERVER_NAME"] + port = environ["SERVER_PORT"] + scheme = environ["wsgi.url_scheme"] + if ( + scheme == "http" + and port != "80" + or scheme == "https" + and port != "443" + ): + host += ":" + port + + # NOTE: Nonstandard (but see + # https://github.com/open-telemetry/opentelemetry-specification/pull/263) + span.set_attribute("http.host", host) + + url = environ.get("REQUEST_URI") or environ.get("RAW_URI") + + if url: + if url[0] == "/": + # We assume that no scheme-relative URLs will be in url here. + # After all, if a request is made to http://myserver//foo, we may get + # //foo which looks like scheme-relative but isn't. + url = environ["wsgi.url_scheme"] + "://" + host + url + elif not url.startswith(environ["wsgi.url_scheme"] + ":"): + # Something fishy is in RAW_URL. Let's fall back to request_uri() + url = wsgiref_util.request_uri(environ) + else: + url = wsgiref_util.request_uri(environ) + + span.set_attribute("http.url", url) + + +def add_response_attributes( + span, start_response_status, response_headers +): # pylint: disable=unused-argument + """Adds HTTP response attributes to span using the arguments + passed to a PEP3333-conforming start_response callable.""" + + status_code, status_text = start_response_status.split(" ", 1) + span.set_attribute("http.status_text", status_text) + + try: + status_code = int(status_code) + except ValueError: + pass + else: + span.set_attribute("http.status_code", status_code) + + +def get_default_span_name(environ): + """Calculates a (generic) span name for an incoming HTTP request based on the PEP3333 conforming WSGI environ.""" + + # TODO: Update once + # https://github.com/open-telemetry/opentelemetry-specification/issues/270 + # is resolved + return environ.get("PATH_INFO", "/") class OpenTelemetryMiddleware: """The WSGI application middleware. - This class is used to create and annotate spans for requests to a WSGI - application. + This class is a PEP 3333 conforming WSGI middleware that starts and + annotates spans for any requests it is invoked with. Args: - wsgi: The WSGI application callable. + wsgi: The WSGI application callable to forward requests to. """ def __init__(self, wsgi): self.wsgi = wsgi @staticmethod - def _add_request_attributes(span, environ): - span.set_attribute("component", "http") - span.set_attribute("http.method", environ["REQUEST_METHOD"]) - - host = environ.get("HTTP_HOST") - if not host: - host = environ["SERVER_NAME"] - port = environ["SERVER_PORT"] - scheme = environ["wsgi.url_scheme"] - if ( - scheme == "http" - and port != "80" - or scheme == "https" - and port != "443" - ): - host += ":" + port - - # NOTE: Nonstandard - span.set_attribute("http.host", host) - - url = environ.get("REQUEST_URI") or environ.get("RAW_URI") - - if url: - if url[0] == "/": - # We assume that no scheme-relative URLs will be in url here. - # After all, if a request is made to http://myserver//foo, we may get - # //foo which looks like scheme-relative but isn't. - url = environ["wsgi.url_scheme"] + "://" + host + url - elif not url.startswith(environ["wsgi.url_scheme"] + ":"): - # Something fishy is in RAW_URL. Let's fall back to request_uri() - url = wsgiref_util.request_uri(environ) - else: - url = wsgiref_util.request_uri(environ) - - span.set_attribute("http.url", url) - - @staticmethod - def _add_response_attributes(span, status): - status_code, status_text = status.split(" ", 1) - span.set_attribute("http.status_text", status_text) - - try: - status_code = int(status_code) - except ValueError: - pass - else: - span.set_attribute("http.status_code", status_code) - - @classmethod - def _create_start_response(cls, span, start_response): + def _create_start_response(span, start_response): @functools.wraps(start_response) def _start_response(status, response_headers, *args, **kwargs): - cls._add_response_attributes(span, status) + add_response_attributes(span, status, response_headers) return start_response(status, response_headers, *args, **kwargs) return _start_response @@ -105,17 +138,20 @@ def __call__(self, environ, start_response): start_response: The WSGI start_response callable. """ + start_timestamp = time_ns() + tracer = trace.tracer() - path_info = environ["PATH_INFO"] or "/" - parent_span = propagators.extract(_get_header_from_environ, environ) + parent_span = propagators.extract(get_header_from_environ, environ) + span_name = get_default_span_name(environ) span = tracer.create_span( - path_info, parent_span, kind=trace.SpanKind.SERVER + span_name, parent_span, kind=trace.SpanKind.SERVER ) - span.start() + span.start(start_timestamp) + try: with tracer.use_span(span): - self._add_request_attributes(span, environ) + add_request_attributes(span, environ) start_response = self._create_start_response( span, start_response ) @@ -127,21 +163,6 @@ def __call__(self, environ, start_response): raise -def _get_header_from_environ( - environ: dict, header_name: str -) -> typing.List[str]: - """Retrieve the header value from the wsgi environ dictionary. - - Returns: - A string with the header value if it exists, else None. - """ - environ_key = "HTTP_" + header_name.upper().replace("-", "_") - value = environ.get(environ_key) - if value: - return [value] - return [] - - # Put this in a subfunction to not delay the call to the wrapped # WSGI application (instrumentation should change the application # behavior as little as possible). diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index e5dc9654fd..9c77acbb75 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -12,15 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import io import sys import unittest import unittest.mock as mock import wsgiref.util as wsgiref_util from urllib.parse import urlparse +import opentelemetry.ext.wsgi as otel_wsgi from opentelemetry import trace as trace_api -from opentelemetry.ext.wsgi import OpenTelemetryMiddleware +from opentelemetry.ext.testutil.wsgitestutil import WsgiTestBase class Response: @@ -73,41 +73,7 @@ def error_wsgi(environ, start_response): return [b"*"] -class TestWsgiApplication(unittest.TestCase): - def setUp(self): - tracer = trace_api.tracer() - self.span = mock.create_autospec(trace_api.Span, spec_set=True) - self.create_span_patcher = mock.patch.object( - tracer, - "create_span", - autospec=True, - spec_set=True, - return_value=self.span, - ) - self.create_span = self.create_span_patcher.start() - - self.write_buffer = io.BytesIO() - self.write = self.write_buffer.write - - self.environ = {} - wsgiref_util.setup_testing_defaults(self.environ) - - self.status = None - self.response_headers = None - self.exc_info = None - - def tearDown(self): - self.create_span_patcher.stop() - - def start_response(self, status, response_headers, exc_info=None): - # The span should have started already - self.span.start.assert_called_once_with() - - self.status = status - self.response_headers = response_headers - self.exc_info = exc_info - return self.write - +class TestWsgiApplication(WsgiTestBase): def validate_response(self, response, error=None): while True: try: @@ -133,29 +99,29 @@ def validate_response(self, response, error=None): self.create_span.assert_called_with( "/", trace_api.INVALID_SPAN_CONTEXT, kind=trace_api.SpanKind.SERVER ) - self.span.start.assert_called_with() + self.assertEqual(1, self.span.start.call_count) def test_basic_wsgi_call(self): - app = OpenTelemetryMiddleware(simple_wsgi) + app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) response = app(self.environ, self.start_response) self.validate_response(response) def test_wsgi_iterable(self): original_response = Response() iter_wsgi = create_iter_wsgi(original_response) - app = OpenTelemetryMiddleware(iter_wsgi) + app = otel_wsgi.OpenTelemetryMiddleware(iter_wsgi) response = app(self.environ, self.start_response) # Verify that start_response has been called self.assertTrue(self.status) self.validate_response(response) # Verify that close has been called exactly once - self.assertEqual(original_response.close_calls, 1) + self.assertEqual(1, original_response.close_calls) def test_wsgi_generator(self): original_response = Response() gen_wsgi = create_gen_wsgi(original_response) - app = OpenTelemetryMiddleware(gen_wsgi) + app = otel_wsgi.OpenTelemetryMiddleware(gen_wsgi) response = app(self.environ, self.start_response) # Verify that start_response has not been called self.assertIsNone(self.status) @@ -165,7 +131,7 @@ def test_wsgi_generator(self): self.assertEqual(original_response.close_calls, 1) def test_wsgi_exc_info(self): - app = OpenTelemetryMiddleware(error_wsgi) + app = otel_wsgi.OpenTelemetryMiddleware(error_wsgi) response = app(self.environ, self.start_response) self.validate_response(response, error=ValueError) @@ -179,9 +145,7 @@ def setUp(self): def test_request_attributes(self): self.environ["QUERY_STRING"] = "foo=bar" - OpenTelemetryMiddleware._add_request_attributes( # noqa pylint: disable=protected-access - self.span, self.environ - ) + otel_wsgi.add_request_attributes(self.span, self.environ) expected = ( mock.call("component", "http"), @@ -193,9 +157,7 @@ def test_request_attributes(self): self.span.set_attribute.assert_has_calls(expected, any_order=True) def validate_url(self, expected_url): - OpenTelemetryMiddleware._add_request_attributes( # noqa pylint: disable=protected-access - self.span, self.environ - ) + otel_wsgi.add_request_attributes(self.span, self.environ) attrs = { args[0][0]: args[0][1] for args in self.span.set_attribute.call_args_list @@ -269,9 +231,7 @@ def test_request_attributes_with_full_request_uri(self): self.validate_url("http://127.0.0.1:8080/?foo=bar#top") def test_response_attributes(self): - OpenTelemetryMiddleware._add_response_attributes( # noqa pylint: disable=protected-access - self.span, "404 Not Found" - ) + otel_wsgi.add_response_attributes(self.span, "404 Not Found", {}) expected = ( mock.call("http.status_code", 404), mock.call("http.status_text", "Not Found"), @@ -280,9 +240,7 @@ def test_response_attributes(self): self.span.set_attribute.assert_has_calls(expected, any_order=True) def test_response_attributes_invalid_status_code(self): - OpenTelemetryMiddleware._add_response_attributes( # noqa pylint: disable=protected-access - self.span, "Invalid Status Code" - ) + otel_wsgi.add_response_attributes(self.span, "Invalid Status Code", {}) self.assertEqual(self.span.set_attribute.call_count, 1) self.span.set_attribute.assert_called_with( "http.status_text", "Status Code" diff --git a/tox.ini b/tox.ini index 88f1a85899..f5624503b6 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,10 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} - pypy3-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} + pypy3-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} + pypy3-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} py3{4,5,6,7,8}-coverage ; Coverage is temporarily disabled for pypy3 due to the pytest bug. @@ -37,6 +39,7 @@ changedir = test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests + test-ext-flask: ext/opentelemetry-ext-flask/tests test-example-app: examples/opentelemetry-example-app/tests test-example-basic-tracer: examples/basic_tracer/tests test-example-http: examples/http/tests @@ -50,6 +53,7 @@ commands_pre = example-app: pip install {toxinidir}/opentelemetry-sdk example-app: pip install {toxinidir}/ext/opentelemetry-ext-http-requests example-app: pip install {toxinidir}/ext/opentelemetry-ext-wsgi + example-app: pip install {toxinidir}/ext/opentelemetry-ext-flask example-app: pip install {toxinidir}/examples/opentelemetry-example-app example-basic-tracer: pip install -e {toxinidir}/opentelemetry-api example-basic-tracer: pip install -e {toxinidir}/opentelemetry-sdk @@ -60,7 +64,9 @@ commands_pre = example-http: pip install -r {toxinidir}/examples/http/requirements.txt ext: pip install {toxinidir}/opentelemetry-api - wsgi: pip install {toxinidir}/ext/opentelemetry-ext-wsgi + wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-testutil + wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-wsgi + flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests jaeger: pip install {toxinidir}/opentelemetry-sdk @@ -74,7 +80,9 @@ commands_pre = coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim + coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-testutil coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi + coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-flask[test] coverage: pip install -e {toxinidir}/examples/opentelemetry-example-app ; Using file:// here because otherwise tox invokes just "pip install @@ -108,7 +116,9 @@ commands_pre = pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger pip install -e {toxinidir}/ext/opentelemetry-ext-pymongo + pip install -e {toxinidir}/ext/opentelemetry-ext-testutil pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi + pip install -e {toxinidir}/ext/opentelemetry-ext-flask[test] pip install -e {toxinidir}/examples/opentelemetry-example-app pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim @@ -128,7 +138,11 @@ commands = ext/opentelemetry-ext-opentracing-shim/tests/ \ ext/opentelemetry-ext-pymongo/src/opentelemetry \ ext/opentelemetry-ext-pymongo/tests/ \ + ext/opentelemetry-ext-testutil/src/opentelemetry \ + ext/opentelemetry-ext-wsgi/src/ \ ext/opentelemetry-ext-wsgi/tests/ \ + ext/opentelemetry-ext-flask/src/ \ + ext/opentelemetry-ext-flask/tests/ \ examples/opentelemetry-example-app/src/opentelemetry_example_app/ \ examples/opentelemetry-example-app/tests/ \ examples/basic_tracer/ \ @@ -161,6 +175,7 @@ commands_pre = pip install -e {toxinidir}/opentelemetry-sdk pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi + pip install -e {toxinidir}/ext/opentelemetry-ext-flask commands = {toxinidir}/scripts/tracecontext-integration-test.sh From 693391cf6a6d466fe44ff55f2ae678a081853b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Sat, 23 Nov 2019 03:00:01 +0100 Subject: [PATCH 0142/1517] Update travis.yml for release of Python 3.8. (#295) --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index f8c01c80be..223cfbb587 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,12 @@ python: - '3.5' - '3.6' - '3.7' + - '3.8' - 'pypy3.5' - - '3.8-dev' -matrix: - allow_failures: - - python: '3.8-dev' +#matrix: +# allow_failures: +# - python: '3.8-dev' install: - pip install tox-travis From 033bcdc6f97b380944cf1a234a6f4b3415fed3e2 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 25 Nov 2019 11:23:36 -0800 Subject: [PATCH 0143/1517] Remove create_span from the API (#290) Simplify the API by removing create_span. Resolves #152 --- .../src/opentelemetry/ext/flask/__init__.py | 8 ++- .../tests/test_flask_integration.py | 13 ++-- .../ext/opentracing_shim/__init__.py | 17 +++-- .../ext/testutil/wsgitestutil.py | 11 ++- .../src/opentelemetry/ext/wsgi/__init__.py | 6 +- .../tests/test_wsgi_middleware.py | 3 +- .../src/opentelemetry/trace/__init__.py | 72 +++---------------- opentelemetry-api/tests/trace/test_tracer.py | 4 -- .../src/opentelemetry/sdk/trace/__init__.py | 38 +++------- opentelemetry-sdk/tests/trace/test_trace.py | 14 ++-- 10 files changed, 51 insertions(+), 135 deletions(-) diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index eedc8d5998..662cea752a 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -62,10 +62,12 @@ def _before_flask_request(): tracer = trace.tracer() - span = tracer.create_span( - span_name, parent_span, kind=trace.SpanKind.SERVER + span = tracer.start_span( + span_name, + parent_span, + kind=trace.SpanKind.SERVER, + start_time=environ.get(_ENVIRON_STARTTIME_KEY), ) - span.start(environ.get(_ENVIRON_STARTTIME_KEY)) activation = tracer.use_span(span, end_on_exit=True) activation.__enter__() environ[_ENVIRON_ACTIVATION_KEY] = activation diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py index dfb9dee885..d03e7604a8 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest +from unittest import mock from flask import Flask from werkzeug.test import Client @@ -52,12 +53,12 @@ def test_simple(self): self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) - self.create_span.assert_called_with( + self.start_span.assert_called_with( "hello_endpoint", trace_api.INVALID_SPAN_CONTEXT, kind=trace_api.SpanKind.SERVER, + start_time=mock.ANY, ) - self.assertEqual(1, self.span.start.call_count) # TODO: Change this test to use the SDK, as mocking becomes painful @@ -79,12 +80,12 @@ def test_404(self): self.assertEqual(404, resp.status_code) resp.close() - self.create_span.assert_called_with( + self.start_span.assert_called_with( "/bye", trace_api.INVALID_SPAN_CONTEXT, kind=trace_api.SpanKind.SERVER, + start_time=mock.ANY, ) - self.assertEqual(1, self.span.start.call_count) # Nope, this uses Tracer.use_span(end_on_exit) # self.assertEqual(1, self.span.end.call_count) @@ -107,12 +108,12 @@ def test_internal_error(self): self.assertEqual(500, resp.status_code) resp.close() - self.create_span.assert_called_with( + self.start_span.assert_called_with( "hello_endpoint", trace_api.INVALID_SPAN_CONTEXT, kind=trace_api.SpanKind.SERVER, + start_time=mock.ANY, ) - self.assertEqual(1, self.span.start.call_count) # Nope, this uses Tracer.use_span(end_on_exit) # self.assertEqual(1, self.span.end.call_count) diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index eefb85466a..338674eec4 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -238,14 +238,6 @@ def start_span( for ref in references: links.append(trace_api.Link(ref.referenced_context.unwrap())) - span = self._otel_tracer.create_span( - operation_name, parent, links=links - ) - - if tags: - for key, value in tags.items(): - span.set_attribute(key, value) - # The OpenTracing API expects time values to be `float` values which # represent the number of seconds since the epoch. OpenTelemetry # represents time values as nanoseconds since the epoch. @@ -253,7 +245,14 @@ def start_span( if start_time_ns is not None: start_time_ns = util.time_seconds_to_ns(start_time) - span.start(start_time=start_time_ns) + span = self._otel_tracer.start_span( + operation_name, + parent, + links=links, + attributes=tags, + start_time=start_time_ns, + ) + context = SpanContextShim(span.get_context()) return SpanShim(self, context, span) diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py index d9cc9ff6a9..fba6a8cda2 100644 --- a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py +++ b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py @@ -10,14 +10,14 @@ class WsgiTestBase(unittest.TestCase): def setUp(self): tracer = trace_api.tracer() self.span = mock.create_autospec(trace_api.Span, spec_set=True) - self.create_span_patcher = mock.patch.object( + self.start_span_patcher = mock.patch.object( tracer, - "create_span", + "start_span", autospec=True, spec_set=True, return_value=self.span, ) - self.create_span = self.create_span_patcher.start() + self.start_span = self.start_span_patcher.start() self.write_buffer = io.BytesIO() self.write = self.write_buffer.write @@ -29,12 +29,9 @@ def setUp(self): self.exc_info = None def tearDown(self): - self.create_span_patcher.stop() + self.start_span_patcher.stop() def start_response(self, status, response_headers, exc_info=None): - # The span should have started already - self.assertEqual(1, self.span.start.call_count) - self.status = status self.response_headers = response_headers self.exc_info = exc_info diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index a2cf163342..0d947ff867 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -24,7 +24,6 @@ from opentelemetry import propagators, trace from opentelemetry.ext.wsgi.version import __version__ # noqa -from opentelemetry.util import time_ns def get_header_from_environ( @@ -138,16 +137,13 @@ def __call__(self, environ, start_response): start_response: The WSGI start_response callable. """ - start_timestamp = time_ns() - tracer = trace.tracer() parent_span = propagators.extract(get_header_from_environ, environ) span_name = get_default_span_name(environ) - span = tracer.create_span( + span = tracer.start_span( span_name, parent_span, kind=trace.SpanKind.SERVER ) - span.start(start_timestamp) try: with tracer.use_span(span): diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index 9c77acbb75..97c93880e4 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -96,10 +96,9 @@ def validate_response(self, response, error=None): self.assertIsNone(self.exc_info) # Verify that start_span has been called - self.create_span.assert_called_with( + self.start_span.assert_called_with( "/", trace_api.INVALID_SPAN_CONTEXT, kind=trace_api.SpanKind.SERVER ) - self.assertEqual(1, self.span.start.call_count) def test_basic_wsgi_call(self): app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 4f1dc539a3..f2abf8ff9b 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -30,10 +30,10 @@ created as children of the currently active span, and the newly-created span can optionally become the new active span:: - from opentelemetry.trace import tracer + from opentelemetry import trace # Create a new root span, set it as the current span in context - with tracer.start_as_current_span("parent"): + with trace.tracer().start_as_current_span("parent"): # Attach a new child and update the current span with tracer.start_as_current_span("child"): do_work(): @@ -43,17 +43,15 @@ When creating a span that's "detached" from the context the active span doesn't change, and the caller is responsible for managing the span's lifetime:: - from opentelemetry.api.trace import tracer + from opentelemetry import trace # Explicit parent span assignment - span = tracer.create_span("child", parent=parent) as child: + child = trace.tracer().start_span("child", parent=parent) - # The caller is responsible for starting and ending the span - span.start() try: do_work(span=child) finally: - span.end() + child.end() Applications should generally use a single global tracer, and use either implicit or explicit context propagation consistently throughout. @@ -147,16 +145,6 @@ class SpanKind(enum.Enum): class Span: """A span represents a single operation within a trace.""" - def start(self, start_time: typing.Optional[int] = None) -> None: - """Sets the current time as the span's start time. - - Each span represents a single operation. The span's start time is the - wall time at which the operation started. - - Only the first call to `start` should modify the span, and - implementations are free to ignore or raise on further calls. - """ - def end(self, end_time: int = None) -> None: """Sets the current time as the span's end time. @@ -204,8 +192,7 @@ def add_lazy_event(self, event: Event) -> None: def update_name(self, name: str) -> None: """Updates the `Span` name. - This will override the name provided via :func:`Tracer.create_span` - or :func:`Tracer.start_span`. + This will override the name provided via :func:`Tracer.start_span`. Upon this update, any sampling behavior based on Span name will depend on the implementation. @@ -404,6 +391,7 @@ def start_span( kind: SpanKind = SpanKind.INTERNAL, attributes: typing.Optional[types.Attributes] = None, links: typing.Sequence[Link] = (), + start_time: typing.Optional[int] = None, ) -> "Span": """Starts a span. @@ -434,6 +422,7 @@ def start_span( meaningful even if there is no parent. attributes: The span's attributes. links: Links span to other spans + start_time: Sets the start time of a span Returns: The newly-created span. @@ -494,51 +483,6 @@ def start_as_current_span( # pylint: disable=unused-argument,no-self-use yield INVALID_SPAN - def create_span( - self, - name: str, - parent: ParentSpan = CURRENT_SPAN, - kind: SpanKind = SpanKind.INTERNAL, - attributes: typing.Optional[types.Attributes] = None, - links: typing.Sequence[Link] = (), - ) -> "Span": - """Creates a span. - - Creating the span does not start it, and should not affect the tracer's - context. To start the span and update the tracer's context to make it - the currently active span, see :meth:`use_span`. - - By default the current span will be used as parent, but an explicit - parent can also be specified, either a Span or a SpanContext. - If the specified value is `None`, the created span will be a root - span. - - Applications that need to create spans detached from the tracer's - context should use this method. - - with tracer.start_as_current_span(name) as span: - do_work() - - This is equivalent to:: - - span = tracer.create_span(name) - with tracer.use_span(span): - do_work() - - Args: - name: The name of the span to be created. - parent: The span's parent. Defaults to the current span. - kind: The span's kind (relationship to parent). Note that is - meaningful even if there is no parent. - attributes: The span's attributes. - links: Links span to other spans - - Returns: - The newly-created span. - """ - # pylint: disable=unused-argument,no-self-use - return INVALID_SPAN - @contextmanager # type: ignore def use_span( self, span: "Span", end_on_exit: bool = False diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py index 79ff76afc1..b57f2ff6d2 100644 --- a/opentelemetry-api/tests/trace/test_tracer.py +++ b/opentelemetry-api/tests/trace/test_tracer.py @@ -33,10 +33,6 @@ def test_start_as_current_span(self): with self.tracer.start_as_current_span("") as span: self.assertIsInstance(span, trace.Span) - def test_create_span(self): - span = self.tracer.create_span("") - self.assertIsInstance(span, trace.Span) - def test_use_span(self): span = trace.Span() with self.tracer.use_span(span): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 9ac9d81bc2..9b55c39567 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -314,20 +314,6 @@ def get_current_span(self): """See `opentelemetry.trace.Tracer.get_current_span`.""" return self._current_span_slot.get() - def start_span( - self, - name: str, - parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, - kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, - attributes: Optional[types.Attributes] = None, - links: Sequence[trace_api.Link] = (), - ) -> "Span": - """See `opentelemetry.trace.Tracer.start_span`.""" - - span = self.create_span(name, parent, kind, attributes, links) - span.start() - return span - def start_as_current_span( self, name: str, @@ -341,22 +327,16 @@ def start_as_current_span( span = self.start_span(name, parent, kind, attributes, links) return self.use_span(span, end_on_exit=True) - def create_span( + def start_span( self, name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, attributes: Optional[types.Attributes] = None, links: Sequence[trace_api.Link] = (), - ) -> "trace_api.Span": - """See `opentelemetry.trace.Tracer.create_span`. - - If `parent` is null the new span will be created as a root span, i.e. a - span with no parent context. By default, the new span will be created - as a child of the current span in this tracer's context, or as a root - span if no current span exists. - """ - span_id = generate_span_id() + start_time: Optional[int] = None, + ) -> "Span": + """See `opentelemetry.trace.Tracer.start_span`.""" if parent is Tracer.CURRENT_SPAN: parent = self.get_current_span() @@ -381,7 +361,7 @@ def create_span( trace_state = parent_context.trace_state context = trace_api.SpanContext( - trace_id, span_id, trace_options, trace_state + trace_id, generate_span_id(), trace_options, trace_state ) # The sampler decides whether to create a real or no-op span at the @@ -405,7 +385,7 @@ def create_span( # apply sampling decision attributes after initial attributes span_attributes = attributes.copy() span_attributes.update(sampling_decision.attributes) - return Span( + span = Span( name=name, context=context, parent=parent, @@ -415,8 +395,10 @@ def create_span( kind=kind, links=links, ) - - return trace_api.DefaultSpan(context=context) + span.start(start_time=start_time) + else: + span = trace_api.DefaultSpan(context=context) + return span @contextmanager def use_span( diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 0d36c003e0..e797b1c890 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -33,9 +33,9 @@ def test_default_sampler(self): # Check that the default tracer creates real spans via the default # sampler - root_span = tracer.create_span(name="root span", parent=None) + root_span = tracer.start_span(name="root span", parent=None) self.assertIsInstance(root_span, trace.Span) - child_span = tracer.create_span(name="child span", parent=root_span) + child_span = tracer.start_span(name="child span", parent=root_span) self.assertIsInstance(child_span, trace.Span) def test_sampler_no_sampling(self): @@ -44,22 +44,22 @@ def test_sampler_no_sampling(self): # Check that the default tracer creates no-op spans if the sampler # decides not to sampler - root_span = tracer.create_span(name="root span", parent=None) + root_span = tracer.start_span(name="root span", parent=None) self.assertIsInstance(root_span, trace_api.DefaultSpan) - child_span = tracer.create_span(name="child span", parent=root_span) + child_span = tracer.start_span(name="child span", parent=root_span) self.assertIsInstance(child_span, trace_api.DefaultSpan) class TestSpanCreation(unittest.TestCase): - def test_create_span_invalid_spancontext(self): + def test_start_span_invalid_spancontext(self): """If an invalid span context is passed as the parent, the created span should use a new span id. Invalid span contexts should also not be added as a parent. This eliminates redundant error handling logic in exporters. """ - tracer = trace.Tracer("test_create_span_invalid_spancontext") - new_span = tracer.create_span( + tracer = trace.Tracer("test_start_span_invalid_spancontext") + new_span = tracer.start_span( "root", parent=trace_api.INVALID_SPAN_CONTEXT ) self.assertTrue(new_span.context.is_valid()) From eee7d84804b23fef1eed2b585af3d40dccb9ab1f Mon Sep 17 00:00:00 2001 From: Johannes Liebermann Date: Mon, 25 Nov 2019 22:09:44 +0100 Subject: [PATCH 0144/1517] Add documentation for the OpenTracing shim (#244) --- docs/conf.py | 13 +- docs/index.rst | 1 + docs/opentelemetry.ext.opentracing_shim.rst | 8 + .../ext/opentracing_shim/__init__.py | 429 +++++++++++++++++- tox.ini | 2 + 5 files changed, 437 insertions(+), 16 deletions(-) create mode 100644 docs/opentelemetry.ext.opentracing_shim.rst diff --git a/docs/conf.py b/docs/conf.py index 8b8c11d47b..538472cf05 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,10 @@ import os import sys -sys.path.insert(0, os.path.abspath("../opentelemetry-api/src/")) +sys.path[:0] = [ + os.path.abspath("../opentelemetry-api/src/"), + os.path.abspath("../ext/opentelemetry-ext-opentracing-shim/src/"), +] # -- Project information ----------------------------------------------------- @@ -47,7 +50,13 @@ "sphinx.ext.githubpages", ] -intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "opentracing": ( + "https://opentracing-python.readthedocs.io/en/latest/", + None, + ), +} # http://www.sphinx-doc.org/en/master/config.html#confval-nitpicky # Sphinx will warn about all references where the target cannot be found. diff --git a/docs/index.rst b/docs/index.rst index f4c8e6e336..b98ef55316 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,6 +19,7 @@ abstract types for OpenTelemetry implementations. opentelemetry.metrics opentelemetry.trace opentelemetry.util.loader + opentelemetry.ext.opentracing_shim Indices and tables diff --git a/docs/opentelemetry.ext.opentracing_shim.rst b/docs/opentelemetry.ext.opentracing_shim.rst new file mode 100644 index 0000000000..921d6c290b --- /dev/null +++ b/docs/opentelemetry.ext.opentracing_shim.rst @@ -0,0 +1,8 @@ +opentelemetry.ext.opentracing_shim package +========================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.opentracing_shim + :no-show-inheritance: diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index 338674eec4..a9e74dbc58 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -12,6 +12,72 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +The OpenTelemetry OpenTracing shim is a library which allows an easy migration +from OpenTracing to OpenTelemetry. + +The shim consists of a set of classes which implement the OpenTracing Python +API while using OpenTelemetry constructs behind the scenes. Its purpose is to +allow applications which are already instrumented using OpenTracing to start +using OpenTelemetry with a minimal effort, without having to rewrite large +portions of the codebase. + +To use the shim, a :class:`TracerShim` instance is created and then used as if +it were an "ordinary" OpenTracing :class:`opentracing.Tracer`, as in the +following example:: + + import time + + from opentelemetry import trace + from opentelemetry.sdk.trace import Tracer + from opentelemetry.ext.opentracing_shim import create_tracer + + # Tell OpenTelemetry which Tracer implementation to use. + trace.set_preferred_tracer_implementation(lambda T: Tracer()) + # Create an OpenTelemetry Tracer. + otel_tracer = trace.tracer() + # Create an OpenTracing shim. + shim = create_tracer(otel_tracer) + + with shim.start_active_span("ProcessHTTPRequest"): + print("Processing HTTP request") + # Sleeping to mock real work. + time.sleep(0.1) + with shim.start_active_span("GetDataFromDB"): + print("Getting data from DB") + # Sleeping to mock real work. + time.sleep(0.2) + +Note: + While the OpenTracing Python API represents time values as the number of + **seconds** since the epoch expressed as :obj:`float` values, the + OpenTelemetry Python API represents time values as the number of + **nanoseconds** since the epoch expressed as :obj:`int` values. This fact + requires the OpenTracing shim to convert time values back and forth between + the two representations, which involves floating point arithmetic. + + Due to the way computers represent floating point values in hardware, + representation of decimal floating point values in binary-based hardware is + imprecise by definition. + + The above results in **slight imprecisions** in time values passed to the + shim via the OpenTracing API when comparing the value passed to the shim + and the value stored in the OpenTelemetry :class:`opentelemetry.trace.Span` + object behind the scenes. **This is not a bug in this library or in + Python**. Rather, this is a generic problem which stems from the fact that + not every decimal floating point number can be correctly represented in + binary, and therefore affects other libraries and programming languages as + well. More information about this problem can be found in the + `Floating Point Arithmetic\\: Issues and Limitations`_ section of the + Python documentation. + + While testing this library, the aforementioned imprecisions were observed + to be of *less than a microsecond*. + +.. _Floating Point Arithmetic\\: Issues and Limitations: + https://docs.python.org/3/tutorial/floatingpoint.html +""" + import logging import opentracing @@ -25,20 +91,56 @@ def create_tracer(otel_tracer): + """Creates a :class:`TracerShim` object from the provided OpenTelemetry + :class:`opentelemetry.trace.Tracer`. + + The returned :class:`TracerShim` is an implementation of + :class:`opentracing.Tracer` using OpenTelemetry under the hood. + + Args: + otel_tracer: A :class:`opentelemetry.trace.Tracer` to be used for + constructing the :class:`TracerShim`. This tracer will be used + to perform the actual tracing when user code is instrumented using + the OpenTracing API. + + Returns: + The created :class:`TracerShim`. + """ + return TracerShim(otel_tracer) class SpanContextShim(opentracing.SpanContext): + """Implements :class:`opentracing.SpanContext` by wrapping a + :class:`opentelemetry.trace.SpanContext` object. + + Args: + otel_context: A :class:`opentelemetry.trace.SpanContext` to be used for + constructing the :class:`SpanContextShim`. + """ + def __init__(self, otel_context): self._otel_context = otel_context def unwrap(self): - """Returns the wrapped OpenTelemetry `SpanContext` object.""" + """Returns the wrapped :class:`opentelemetry.trace.SpanContext` + object. + + Returns: + The :class:`opentelemetry.trace.SpanContext` object wrapped by this + :class:`SpanContextShim`. + """ return self._otel_context @property def baggage(self): + """Implements the ``baggage`` property from the base class. + + Warning: + Not implemented yet. + """ + logger.warning( "Using unimplemented property baggage on class %s.", self.__class__.__name__, @@ -47,30 +149,102 @@ def baggage(self): class SpanShim(opentracing.Span): + """Implements :class:`opentracing.Span` by wrapping a + :class:`opentelemetry.trace.Span` object. + + Args: + tracer: The :class:`opentracing.Tracer` that created this `SpanShim`. + context: A :class:`SpanContextShim` which contains the context for this + :class:`SpanShim`. + span: A :class:`opentelemetry.trace.Span` to wrap. + """ + def __init__(self, tracer, context, span): super().__init__(tracer, context) self._otel_span = span def unwrap(self): - """Returns the wrapped OpenTelemetry `Span` object.""" + """Returns the wrapped :class:`opentelemetry.trace.Span` object. + + Returns: + The :class:`opentelemetry.trace.Span` object wrapped by this + :class:`SpanShim`. + """ return self._otel_span def set_operation_name(self, operation_name): + """Implements the ``set_operation_name()`` method from the base class. + + Updates the name of the wrapped OpenTelemetry span. + + Returns: + Returns this :class:`SpanShim` instance to allow call chaining. + """ + self._otel_span.update_name(operation_name) return self def finish(self, finish_time=None): + """Implements the ``finish()`` method from the base class. + + Ends the OpenTelemetry span wrapped by this :class:`SpanShim`. + + If *finish_time* is provided, the time value is converted to the + OpenTelemetry time format (number of nanoseconds since the epoch, + expressed as an integer) and passed on to the OpenTelemetry tracer when + ending the OpenTelemetry span. If *finish_time* isn't provided, it is + up to the OpenTelemetry tracer implementation to generate a timestamp + when ending the span. + + Args: + finish_time(:obj:`float`, optional): An explicit finish time + expressed as the number of seconds since the epoch as returned + by :func:`time.time()`. Defaults to `None`. + """ + end_time = finish_time if end_time is not None: end_time = util.time_seconds_to_ns(finish_time) self._otel_span.end(end_time=end_time) def set_tag(self, key, value): + """Implements the ``set_tag()`` method from the base class. + + Sets an OpenTelemetry attribute on the wrapped OpenTelemetry span. + + Args: + key(:obj:`str`): A tag key. + value: A tag value. Can be one of :obj:`str`, :obj:`bool`, + :obj:`int`, :obj:`float` + + Returns: + Returns this :class:`SpanShim` instance to allow call chaining. + """ + self._otel_span.set_attribute(key, value) return self def log_kv(self, key_values, timestamp=None): + """Implements the ``log_kv()`` method from the base class. + + Logs an :class:`opentelemetry.trace.Event` for the wrapped + OpenTelemetry span. + + Note: + The OpenTracing API defines the values of *key_values* to be of any + type. However, the OpenTelemetry API requires that the values be + one of :obj:`str`, :obj:`bool`, :obj:`float`. Therefore, only these + types are supported as values. + + Args: + key_values(:obj:`dict`): A dict with :obj:`str` keys and values of + type :obj:`str`, :obj:`bool` or :obj:`float`. + + Returns: + Returns this :class:`SpanShim` instance to allow call chaining. + """ + if timestamp is not None: event_timestamp = util.time_seconds_to_ns(timestamp) else: @@ -89,6 +263,12 @@ def log_event(self, event, payload=None): super().log_event(event, payload=payload) def set_baggage_item(self, key, value): + """Implements the ``set_baggage_item()`` method from the base class. + + Warning: + Not implemented yet. + """ + logger.warning( "Calling unimplemented method set_baggage_item() on class %s", self.__class__.__name__, @@ -96,6 +276,12 @@ def set_baggage_item(self, key, value): # TODO: Implement. def get_baggage_item(self, key): + """Implements the ``get_baggage_item()`` method from the base class. + + Warning: + Not implemented yet. + """ + logger.warning( "Calling unimplemented method get_baggage_item() on class %s", self.__class__.__name__, @@ -105,18 +291,39 @@ def get_baggage_item(self, key): class ScopeShim(opentracing.Scope): """A `ScopeShim` wraps the OpenTelemetry functionality related to span - activation/deactivation while using OpenTracing `Scope` objects for - presentation. + activation/deactivation while using OpenTracing :class:`opentracing.Scope` + objects for presentation. + + Unlike other classes in this package, the `ScopeShim` class doesn't wrap an + OpenTelemetry class because OpenTelemetry doesn't have the notion of + "scope" (though it *does* have similar functionality). There are two ways to construct a `ScopeShim` object: using the default - initializer and using the `from_context_manager()` class method. + initializer and using the :meth:`from_context_manager()` class method. It is necessary to have both ways for constructing `ScopeShim` objects - because in some cases we need to create the object from a context manager, - in which case our only way of retrieving a `Span` object is by calling the - `__enter__()` method on the context manager, which makes the span active in - the OpenTelemetry tracer; whereas in other cases we need to accept a - `SpanShim` object and wrap it in a `ScopeShim`. + because in some cases we need to create the object from an OpenTelemetry + `Span` context manager (as returned by + :meth:`opentelemetry.trace.Tracer.use_span`), in which case our only way of + retrieving a `Span` object is by calling the ``__enter__()`` method on the + context manager, which makes the span active in the OpenTelemetry tracer; + whereas in other cases we need to accept a `SpanShim` object and wrap it in + a `ScopeShim`. The former is used mainly when the instrumentation code + retrieves the currently-active span using `ScopeManagerShim.active`. The + latter is mainly used when the instrumentation code activates a span using + :meth:`ScopeManagerShim.activate`. + + Args: + manager: The :class:`ScopeManagerShim` that created this + :class:`ScopeShim`. + span: The :class:`SpanShim` this :class:`ScopeShim` controls. + span_cm(:class:`contextlib.AbstractContextManager`, optional): A + Python context manager which yields an OpenTelemetry `Span` from + its ``__enter__()`` method. Used by :meth:`from_context_manager` to + store the context manager as an attribute so that it can later be + closed by calling its ``__exit__()`` method. Defaults to `None`. + + TODO: Is :class:`contextlib.AbstractContextManager` the correct type for *span_cm*? """ def __init__(self, manager, span, span_cm=None): @@ -127,8 +334,27 @@ def __init__(self, manager, span, span_cm=None): # need to get rid of `manager.tracer` for this. @classmethod def from_context_manager(cls, manager, span_cm): - """Constructs a `ScopeShim` from an OpenTelemetry `Span` context - manager (as returned by `Tracer.use_span()`). + """Constructs a :class:`ScopeShim` from an OpenTelemetry `Span` context + manager. + + The method extracts a `Span` object from the context manager by calling + the context manager's ``__enter__()`` method. This causes the span to + start in the OpenTelemetry tracer. + + Example usage:: + + span = otel_tracer.start_span("TestSpan") + span_cm = otel_tracer.use_span(span) + scope_shim = ScopeShim.from_context_manager( + scope_manager_shim, + span_cm=span_cm, + ) + + Args: + manager: The :class:`ScopeManagerShim` that created this + :class:`ScopeShim`. + span_cm: An OpenTelemetry `Span` context manager as returned by + :meth:`opentelemetry.trace.Tracer.use_span`. """ otel_span = span_cm.__enter__() @@ -137,6 +363,26 @@ def from_context_manager(cls, manager, span_cm): return cls(manager, span, span_cm) def close(self): + """Implements the `close()` method from :class:`opentracing.Scope`. + + Closes the `ScopeShim`. If the `ScopeShim` was created from a context + manager, calling this method sets the active span in the + OpenTelemetry tracer back to the span which was active before this + `ScopeShim` was created. In addition, if the span represented by this + `ScopeShim` was activated with the *finish_on_close* argument set to + `True`, calling this method will end the span. + + Warning: + In the current state of the implementation it is possible to create + a `ScopeShim` directly from a `SpanShim`, that is - without using + :meth:`from_context_manager()`. For that reason we need to be able + to end the span represented by the `ScopeShim` in this case, too. + Please note that closing a `ScopeShim` created this way (for + example as returned by :meth:`ScopeManagerShim.active`) **always + ends the associated span**, regardless of the value passed in + *finish_on_close* when activating the span. + """ + if self._span_cm is not None: # We don't have error information to pass to `__exit__()` so we # pass `None` in all arguments. If the OpenTelemetry tracer @@ -149,14 +395,44 @@ def close(self): class ScopeManagerShim(opentracing.ScopeManager): + """Implements :class:`opentracing.ScopeManager` by setting and getting the + active `Span` in the OpenTelemetry tracer. + + This class keeps a reference to a :class:`TracerShim` as an attribute. This + reference is used to communicate with the OpenTelemetry tracer. It is + necessary to have a reference to the :class:`TracerShim` rather than the + :class:`opentelemetry.trace.Tracer` wrapped by it because when constructing + a :class:`SpanShim` we need to pass a reference to a + :class:`opentracing.Tracer`. + + Args: + tracer: A :class:`TracerShim` to use for setting and getting active + span state. + """ + def __init__(self, tracer): - # The only thing the `__init__()` method on the base class does is + # The only thing the ``__init__()``` method on the base class does is # initialize `self._noop_span` and `self._noop_scope` with no-op # objects. Therefore, it doesn't seem useful to call it. # pylint: disable=super-init-not-called self._tracer = tracer def activate(self, span, finish_on_close): + """Implements the ``activate()`` method from the base class. + + Activates a :class:`SpanShim` and returns a :class:`ScopeShim` which + represents the active span. + + Args: + span: A :class:`SpanShim` to be activated. + finish_on_close(:obj:`bool`): Determines whether the OpenTelemetry + span should be ended when the returned :class:`ScopeShim` is + closed. + + Returns: + A :class:`ScopeShim` representing the activated span. + """ + span_cm = self._tracer.unwrap().use_span( span.unwrap(), end_on_exit=finish_on_close ) @@ -164,6 +440,23 @@ def activate(self, span, finish_on_close): @property def active(self): + """Implements the ``active`` property from the base class. + + Returns a :class:`ScopeShim` object representing the currently-active + span in the OpenTelemetry tracer. + + Returns: + A :class:`ScopeShim` representing the active span in the + OpenTelemetry tracer, or `None` if no span is currently active. + + Warning: + Calling :meth:`ScopeShim.close` on the :class:`ScopeShim` returned + by this property **always ends the corresponding span**, regardless + of the *finish_on_close* value used when activating the span. This + is a limitation of the current implementation of the OpenTracing + shim and is likely to be handled in future versions. + """ + span = self._tracer.unwrap().get_current_span() if span is None: return None @@ -179,10 +472,41 @@ def active(self): @property def tracer(self): + """Returns the :class:`TracerShim` reference used by this + :class:`ScopeManagerShim` for setting and getting the active span from + the OpenTelemetry tracer. + + Returns: + The :class:`TracerShim` used for setting and getting the active + span. + + Warning: + This property is *not* a part of the OpenTracing API. It used + internally by the current implementation of the OpenTracing shim + and will likely be removed in future versions. + """ + return self._tracer class TracerShim(opentracing.Tracer): + """Implements :class:`opentracing.Tracer` by wrapping a + :class:`opentelemetry.trace.Tracer` object. + + This wrapper class allows using an OpenTelemetry tracer as if it were an + OpenTracing tracer. It exposes the same methods as an "ordinary" + OpenTracing tracer, and uses OpenTelemetry transparently for performing the + actual tracing. + + This class depends on the *OpenTelemetry API*. Therefore, any + implementation of a :class:`opentelemetry.trace.Tracer` should work with + this class. + + Args: + tracer: A :class:`opentelemetry.trace.Tracer` to use for tracing. This + tracer will be invoked by the shim to create actual spans. + """ + def __init__(self, tracer): super().__init__(scope_manager=ScopeManagerShim(self)) self._otel_tracer = tracer @@ -192,7 +516,12 @@ def __init__(self, tracer): ) def unwrap(self): - """Returns the wrapped OpenTelemetry `Tracer` object.""" + """Returns the :class:`opentelemetry.trace.Tracer` object that is + wrapped by this :class:`TracerShim` and used for actual tracing. + + Returns: + The :class:`opentelemetry.trace.Tracer` used for actual tracing. + """ return self._otel_tracer @@ -206,6 +535,41 @@ def start_active_span( ignore_active_span=False, finish_on_close=True, ): + """Implements the ``start_active_span()`` method from the base class. + + Starts and activates a span. In terms of functionality, this method + behaves exactly like the same method on a "regular" OpenTracing tracer. + See :meth:`opentracing.Tracer.start_active_span` for more details. + + Args: + operation_name(:obj:`str`): Name of the operation represented by + the new span from the perspective of the current service. + child_of(:class:`SpanShim` or :class:`SpanContextShim`, optional): + A :class:`SpanShim` or :class:`SpanContextShim` representing + the parent in a "child of" reference. If specified, the + *references* parameter must be omitted. Defaults to `None`. + references(:obj:`list`, optional): A list of + :class:`opentracing.Reference` objects that identify one or + more parents of type :class:`SpanContextShim`. Defaults to + `None`. + tags(:obj:`dict`, optional): A dictionary of tags. The keys must be + of type :obj:`str`. The values may be one of :obj:`str`, + :obj:`bool`, :obj:`int`, :obj:`float`. Defaults to `None`. + start_time(:obj:`float`, optional): An explicit start time + expressed as the number of seconds since the epoch as returned + by :func:`time.time()`. Defaults to `None`. + ignore_active_span(:obj:`bool`, optional): Ignore the + currently-active span in the OpenTelemetry tracer and make the + created span the root span of a new trace. Defaults to `False`. + finish_on_close(:obj:`bool`, optional): Determines whether the + created span should end automatically when closing the returned + :class:`ScopeShim`. Defaults to `True`. + + Returns: + A :class:`ScopeShim` that is already activated by the + :class:`ScopeManagerShim`. + """ + span = self.start_span( operation_name=operation_name, child_of=child_of, @@ -225,6 +589,37 @@ def start_span( start_time=None, ignore_active_span=False, ): + """Implements the ``start_span()`` method from the base class. + + Starts a span. In terms of functionality, this method behaves exactly + like the same method on a "regular" OpenTracing tracer. See + :meth:`opentracing.Tracer.start_span` for more details. + + Args: + operation_name(:obj:`str`): Name of the operation represented by + the new span from the perspective of the current service. + child_of(:class:`SpanShim` or :class:`SpanContextShim`, optional): + A :class:`SpanShim` or :class:`SpanContextShim` representing + the parent in a "child of" reference. If specified, the + *references* parameter must be omitted. Defaults to `None`. + references(:obj:`list`, optional): A list of + :class:`opentracing.Reference` objects that identify one or + more parents of type :class:`SpanContextShim`. Defaults to + `None`. + tags(:obj:`dict`, optional): A dictionary of tags. The keys must be + of type :obj:`str`. The values may be one of :obj:`str`, + :obj:`bool`, :obj:`int`, :obj:`float`. Defaults to `None`. + start_time(:obj:`float`, optional): An explicit start time + expressed as the number of seconds since the epoch as returned + by :func:`time.time()`. Defaults to `None`. + ignore_active_span(:obj:`bool`, optional): Ignore the + currently-active span in the OpenTelemetry tracer and make the + created span the root span of a new trace. Defaults to `False`. + + Returns: + An already-started :class:`SpanShim` instance. + """ + # Use active span as parent when no explicit parent is specified. if not ignore_active_span and not child_of: child_of = self.active_span @@ -257,6 +652,9 @@ def start_span( return SpanShim(self, context, span) def inject(self, span_context, format, carrier): + """Implements the ``inject`` method from the base class.""" + + # TODO: Finish documentation. # pylint: disable=redefined-builtin # This implementation does not perform the injecting by itself but # uses the configured propagators in opentelemetry.propagators. @@ -271,6 +669,9 @@ def inject(self, span_context, format, carrier): ) def extract(self, format, carrier): + """Implements the ``extract`` method from the base class.""" + + # TODO: Finish documentation. # pylint: disable=redefined-builtin # This implementation does not perform the extracing by itself but # uses the configured propagators in opentelemetry.propagators. diff --git a/tox.ini b/tox.ini index f5624503b6..9d8e679dfa 100644 --- a/tox.ini +++ b/tox.ini @@ -155,6 +155,8 @@ deps = sphinx~=2.1 sphinx-rtd-theme~=0.4 sphinx-autodoc-typehints~=1.10.2 + opentracing~=2.2.0 + Deprecated>=1.2.6 changedir = docs From 5f311e0394fa1ebdc332dc62bc6cff3524ebf855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 27 Nov 2019 02:02:10 +0100 Subject: [PATCH 0145/1517] Fix example tests on Win32 (#302) --- examples/basic_tracer/tests/test_tracer.py | 5 ++++- examples/http/tests/test_http.py | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/basic_tracer/tests/test_tracer.py b/examples/basic_tracer/tests/test_tracer.py index d5922d6086..4bd1a25c0e 100644 --- a/examples/basic_tracer/tests/test_tracer.py +++ b/examples/basic_tracer/tests/test_tracer.py @@ -13,6 +13,7 @@ # limitations under the License. import os import subprocess +import sys import unittest @@ -20,7 +21,9 @@ class TestBasicTracerExample(unittest.TestCase): def test_basic_tracer(self): dirpath = os.path.dirname(os.path.realpath(__file__)) test_script = "{}/../tracer.py".format(dirpath) - output = subprocess.check_output(test_script).decode() + output = subprocess.check_output( + (sys.executable, test_script) + ).decode() self.assertIn('name="foo"', output) self.assertIn('name="bar"', output) diff --git a/examples/http/tests/test_http.py b/examples/http/tests/test_http.py index 7aa3f93c15..0ae81fe7de 100644 --- a/examples/http/tests/test_http.py +++ b/examples/http/tests/test_http.py @@ -13,6 +13,7 @@ # limitations under the License. import os import subprocess +import sys import unittest from time import sleep @@ -22,13 +23,15 @@ class TestHttpExample(unittest.TestCase): def setup_class(cls): dirpath = os.path.dirname(os.path.realpath(__file__)) server_script = "{}/../server.py".format(dirpath) - cls.server = subprocess.Popen([server_script]) + cls.server = subprocess.Popen([sys.executable, server_script]) sleep(1) def test_http(self): dirpath = os.path.dirname(os.path.realpath(__file__)) test_script = "{}/../tracer_client.py".format(dirpath) - output = subprocess.check_output(test_script).decode() + output = subprocess.check_output( + (sys.executable, test_script) + ).decode() self.assertIn('name="/"', output) @classmethod From a42b0637c87ce07108d8cfa2bdb9e5de5e515819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 26 Nov 2019 20:59:13 -0500 Subject: [PATCH 0146/1517] SDK: shut down span processors automatically (#280) The BatchExportSpanProcessor is an asynchronous span processor that uses a worker thread to call the different exporters. Before this commit applications had to shut down the span processor explicitely to guarantee that all the spans were summited to the exporters, this was not very intuitive for the users. This commit removes that limitation by implementing the tracer's __del__ method and an atexit hook. According to __del__'s documentation [1] it is possible that sometimes it's not called, for that reason the atexit hook is also used to guarantee that the processor is shut down in all the cases. [1] https://docs.python.org/3/reference/datamodel.html#object.__del__ --- examples/basic_tracer/tracer.py | 2 - examples/http/server.py | 1 - examples/http/tracer_client.py | 1 - ext/opentelemetry-ext-jaeger/README.rst | 4 - .../examples/jaeger_exporter_example.py | 4 - .../src/opentelemetry/sdk/trace/__init__.py | 14 ++++ .../tests/trace/export/test_export.py | 9 +++ opentelemetry-sdk/tests/trace/test_trace.py | 73 +++++++++++++++++++ 8 files changed, 96 insertions(+), 12 deletions(-) diff --git a/examples/basic_tracer/tracer.py b/examples/basic_tracer/tracer.py index c99141f5aa..69ee0a1602 100755 --- a/examples/basic_tracer/tracer.py +++ b/examples/basic_tracer/tracer.py @@ -48,5 +48,3 @@ with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): print(Context) - -span_processor.shutdown() diff --git a/examples/http/server.py b/examples/http/server.py index 82cb070c27..63973da74d 100755 --- a/examples/http/server.py +++ b/examples/http/server.py @@ -65,4 +65,3 @@ def hello(): if __name__ == "__main__": app.run(debug=True) - span_processor.shutdown() diff --git a/examples/http/tracer_client.py b/examples/http/tracer_client.py index 671d1d71f9..dde25b9bb3 100755 --- a/examples/http/tracer_client.py +++ b/examples/http/tracer_client.py @@ -51,4 +51,3 @@ # Spans and propagating context as appropriate. http_requests.enable(tracer) response = requests.get(url="http://127.0.0.1:5000/") -span_processor.shutdown() diff --git a/ext/opentelemetry-ext-jaeger/README.rst b/ext/opentelemetry-ext-jaeger/README.rst index a03d1b0c1e..742ec6220f 100644 --- a/ext/opentelemetry-ext-jaeger/README.rst +++ b/ext/opentelemetry-ext-jaeger/README.rst @@ -61,10 +61,6 @@ gRPC is still not supported by this implementation. with tracer.start_as_current_span('foo'): print('Hello world!') - # shutdown the span processor - # TODO: this has to be improved so user doesn't need to call it manually - span_processor.shutdown() - The `examples <./examples>`_ folder contains more elaborated examples. References diff --git a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py index 89d93809e4..d459855dcf 100644 --- a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py +++ b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py @@ -46,7 +46,3 @@ time.sleep(0.2) time.sleep(0.1) - -# shutdown the span processor -# TODO: this has to be improved so user doesn't need to call it manually -span_processor.shutdown() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 9b55c39567..c6fe5829cc 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. +import atexit import logging import random import threading @@ -296,12 +297,15 @@ class Tracer(trace_api.Tracer): Args: name: The name of the tracer. + shutdown_on_exit: Register an atexit hook to shut down the tracer when + the application exits. """ def __init__( self, name: str = "", sampler: sampling.Sampler = trace_api.sampling.ALWAYS_ON, + shutdown_on_exit: bool = True, ) -> None: slot_name = "current_span" if name: @@ -309,6 +313,9 @@ def __init__( self._current_span_slot = Context.register_slot(slot_name) self._active_span_processor = MultiSpanProcessor() self.sampler = sampler + self._atexit_handler = None + if shutdown_on_exit: + self._atexit_handler = atexit.register(self.shutdown) def get_current_span(self): """See `opentelemetry.trace.Tracer.get_current_span`.""" @@ -426,5 +433,12 @@ def add_span_processor(self, span_processor: SpanProcessor) -> None: # thread safe self._active_span_processor.add_span_processor(span_processor) + def shutdown(self): + """Shut down the span processors added to the tracer.""" + self._active_span_processor.shutdown() + if self._atexit_handler is not None: + atexit.unregister(self._atexit_handler) + self._atexit_handler = None + tracer = Tracer() diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index d45afc299a..9ad65aea88 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -27,6 +27,7 @@ class MySpanExporter(export.SpanExporter): def __init__(self, destination, max_export_batch_size=None): self.destination = destination self.max_export_batch_size = max_export_batch_size + self.is_shutdown = False def export(self, spans: trace.Span) -> export.SpanExportResult: if ( @@ -37,6 +38,9 @@ def export(self, spans: trace.Span) -> export.SpanExportResult: self.destination.extend(span.name for span in spans) return export.SpanExportResult.SUCCESS + def shutdown(self): + self.is_shutdown = True + class TestSimpleExportSpanProcessor(unittest.TestCase): def test_simple_span_processor(self): @@ -55,6 +59,9 @@ def test_simple_span_processor(self): self.assertListEqual(["xxx", "bar", "foo"], spans_names_list) + span_processor.shutdown() + self.assertTrue(my_exporter.is_shutdown) + def test_simple_span_processor_no_context(self): """Check that we process spans that are never made active. @@ -102,6 +109,8 @@ def test_batch_span_processor(self): span_processor.shutdown() self.assertListEqual(span_names, spans_names_list) + self.assertTrue(my_exporter.is_shutdown) + def test_batch_span_processor_lossless(self): """Test that no spans are lost when sending max_queue_size spans""" spans_names_list = [] diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index e797b1c890..e8144c9373 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import shutil +import subprocess import unittest from unittest import mock @@ -26,6 +28,77 @@ def test_extends_api(self): tracer = trace.Tracer() self.assertIsInstance(tracer, trace_api.Tracer) + def test_shutdown(self): + tracer = trace.Tracer() + + mock_processor1 = mock.Mock(spec=trace.SpanProcessor) + tracer.add_span_processor(mock_processor1) + + mock_processor2 = mock.Mock(spec=trace.SpanProcessor) + tracer.add_span_processor(mock_processor2) + + tracer.shutdown() + + self.assertEqual(mock_processor1.shutdown.call_count, 1) + self.assertEqual(mock_processor2.shutdown.call_count, 1) + + shutdown_python_code = """ +import atexit +from unittest import mock + +from opentelemetry.sdk import trace + +mock_processor = mock.Mock(spec=trace.SpanProcessor) + +def print_shutdown_count(): + print(mock_processor.shutdown.call_count) + +# atexit hooks are called in inverse order they are added, so do this before +# creating the tracer +atexit.register(print_shutdown_count) + +tracer = trace.Tracer({tracer_parameters}) +tracer.add_span_processor(mock_processor) + +{tracer_shutdown} +""" + + def run_general_code(shutdown_on_exit, explicit_shutdown): + tracer_parameters = "" + tracer_shutdown = "" + + if not shutdown_on_exit: + tracer_parameters = "shutdown_on_exit=False" + + if explicit_shutdown: + tracer_shutdown = "tracer.shutdown()" + + return subprocess.check_output( + [ + # use shutil to avoid calling python outside the + # virtualenv on windows. + shutil.which("python"), + "-c", + shutdown_python_code.format( + tracer_parameters=tracer_parameters, + tracer_shutdown=tracer_shutdown, + ), + ] + ) + + # test default shutdown_on_exit (True) + out = run_general_code(True, False) + self.assertTrue(out.startswith(b"1")) + + # test that shutdown is called only once even if Tracer.shutdown is + # called explicitely + out = run_general_code(True, True) + self.assertTrue(out.startswith(b"1")) + + # test shutdown_on_exit=False + out = run_general_code(False, False) + self.assertTrue(out.startswith(b"0")) + class TestTracerSampling(unittest.TestCase): def test_default_sampler(self): From 2bb6a177d90e7862a949039a3aaaf1798b9eea76 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 27 Nov 2019 13:40:10 -0600 Subject: [PATCH 0147/1517] Fix isort issues (#309) --- .isort.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/.isort.cfg b/.isort.cfg index 96011ae93d..31620ab99b 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -14,3 +14,4 @@ line_length=79 multi_line_output=3 skip=target skip_glob=ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/* +known_first_party=opentelemetry From d3bb2280daed284e7adf530a978903a1bbf503c9 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 27 Nov 2019 14:19:17 -0800 Subject: [PATCH 0148/1517] Always recreate lint env (#310) --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 9d8e679dfa..7dc2eb6dff 100644 --- a/tox.ini +++ b/tox.ini @@ -104,6 +104,7 @@ commands = [testenv:lint] basepython: python3.7 +recreate = True deps = pylint~=2.3 flake8~=3.7 From 4ead3f4846bd82ef2b0991c206f1882534f8b83c Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 2 Dec 2019 21:25:53 -0800 Subject: [PATCH 0149/1517] Implement LabelSet for metrics (#258) The primary purpose of LabelSets are to have an optimal way of re-using handles with the same label values. We achieve this by having the keys and values of the labels encoded and stored in each LabelSet instance, so we can have an easy lookup to the corresponding handle for each metric instrument. --- .../metrics_example.py | 8 +- .../src/opentelemetry/metrics/__init__.py | 65 ++++++++---- .../tests/metrics/test_metrics.py | 28 +++-- .../src/opentelemetry/sdk/metrics/__init__.py | 63 +++++++---- .../tests/metrics/export/test_export.py | 11 +- .../tests/metrics/test_metrics.py | 100 +++++++++++++----- tox.ini | 2 +- 7 files changed, 195 insertions(+), 82 deletions(-) diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py index 41bdba8597..246d6c3507 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py @@ -30,17 +30,17 @@ ("environment",), ) -label_values = ("staging",) +label_set = meter.get_label_set({"environment": "staging"}) # Direct metric usage -counter.add(label_values, 25) +counter.add(label_set, 25) # Handle usage -counter_handle = counter.get_handle(label_values) +counter_handle = counter.get_handle(label_set) counter_handle.add(100) # Record batch usage -meter.record_batch(label_values, [(counter, 50)]) +meter.record_batch(label_set, [(counter, 50)]) print(counter_handle.data) # TODO: exporters diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index e866aa97cf..465020606d 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -26,7 +26,8 @@ """ -from typing import Callable, Optional, Sequence, Tuple, Type, TypeVar +import abc +from typing import Callable, Dict, Optional, Sequence, Tuple, Type, TypeVar from opentelemetry.util import loader @@ -67,6 +68,25 @@ def record(self, value: ValueT) -> None: """ +class LabelSet(abc.ABC): + """A canonicalized set of labels useful for preaggregation + + Re-usable LabelSet objects provide a potential optimization for scenarios + where handles might not be effective. For example, if the LabelSet will be + re-used but only used once per metrics, handles do not offer any + optimization. It may best to pre-compute a canonicalized LabelSet once and + re-use it with the direct calling convention. LabelSets are immutable and + should be opaque in implementation. + """ + + +class DefaultLabelSet(LabelSet): + """The default LabelSet. + + Used when no LabelSet implementation is available. + """ + + class Metric: """Base class for various types of metrics. @@ -74,7 +94,7 @@ class Metric: handle that the metric holds. """ - def get_handle(self, label_values: Sequence[str]) -> "object": + def get_handle(self, label_set: LabelSet) -> "object": """Gets a handle, used for repeated-use of metrics instruments. Handles are useful to reduce the cost of repeatedly recording a metric @@ -85,18 +105,18 @@ def get_handle(self, label_values: Sequence[str]) -> "object": a value was not provided are permitted. Args: - label_values: Values to associate with the returned handle. + label_set: `LabelSet` to associate with the returned handle. """ class DefaultMetric(Metric): """The default Metric used when no Metric implementation is available.""" - def get_handle(self, label_values: Sequence[str]) -> "DefaultMetricHandle": + def get_handle(self, label_set: LabelSet) -> "DefaultMetricHandle": """Gets a `DefaultMetricHandle`. Args: - label_values: The label values associated with the handle. + label_set: `LabelSet` to associate with the returned handle. """ return DefaultMetricHandle() @@ -104,15 +124,15 @@ def get_handle(self, label_values: Sequence[str]) -> "DefaultMetricHandle": class Counter(Metric): """A counter type metric that expresses the computation of a sum.""" - def get_handle(self, label_values: Sequence[str]) -> "CounterHandle": + def get_handle(self, label_set: LabelSet) -> "CounterHandle": """Gets a `CounterHandle`.""" return CounterHandle() - def add(self, label_values: Sequence[str], value: ValueT) -> None: + def add(self, label_set: LabelSet, value: ValueT) -> None: """Increases the value of the counter by ``value``. Args: - label_values: The label values associated with the metric. + label_set: `LabelSet` to associate with the returned handle. value: The value to add to the counter metric. """ @@ -126,15 +146,15 @@ class Gauge(Metric): the measurement interval is arbitrary. """ - def get_handle(self, label_values: Sequence[str]) -> "GaugeHandle": + def get_handle(self, label_set: LabelSet) -> "GaugeHandle": """Gets a `GaugeHandle`.""" return GaugeHandle() - def set(self, label_values: Sequence[str], value: ValueT) -> None: + def set(self, label_set: LabelSet, value: ValueT) -> None: """Sets the value of the gauge to ``value``. Args: - label_values: The label values associated with the metric. + label_set: `LabelSet` to associate with the returned handle. value: The value to set the gauge metric to. """ @@ -147,15 +167,15 @@ class Measure(Metric): Negative inputs will be discarded when monotonic is True. """ - def get_handle(self, label_values: Sequence[str]) -> "MeasureHandle": + def get_handle(self, label_set: LabelSet) -> "MeasureHandle": """Gets a `MeasureHandle` with a float value.""" return MeasureHandle() - def record(self, label_values: Sequence[str], value: ValueT) -> None: + def record(self, label_set: LabelSet, value: ValueT) -> None: """Records the ``value`` to the measure. Args: - label_values: The label values associated with the metric. + label_set: `LabelSet` to associate with the returned handle. value: The value to record to this measure metric. """ @@ -174,7 +194,7 @@ class Meter: def record_batch( self, - label_values: Sequence[str], + label_set: LabelSet, record_tuples: Sequence[Tuple["Metric", ValueT]], ) -> None: """Atomically records a batch of `Metric` and value pairs. @@ -184,7 +204,7 @@ def record_batch( match the key-value pairs in the label tuples. Args: - label_values: The label values associated with all measurements in + label_set: The `LabelSet` associated with all measurements in the batch. A measurement is a tuple, representing the `Metric` being recorded and the corresponding value to record. record_tuples: A sequence of pairs of `Metric` s and the @@ -211,8 +231,6 @@ def create_metric( value_type: The type of values being recorded by the metric. metric_type: The type of metric being created. label_keys: The keys for the labels with dynamic values. - Order of the sequence is important as the same order must be - used on recording when suppling values for these labels. enabled: Whether to report the metric by default. monotonic: Whether to only allow non-negative values. @@ -221,6 +239,17 @@ def create_metric( # pylint: disable=no-self-use return DefaultMetric() + def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": + """Gets a `LabelSet` with the given labels. + + Args: + labels: A dictionary representing label key to label value pairs. + + Returns: A `LabelSet` object canonicalized using the given input. + """ + # pylint: disable=no-self-use + return DefaultLabelSet() + # Once https://github.com/python/mypy/issues/7092 is resolved, # the following type definition should be replaced with diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 758534f235..f8610a6fa4 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -24,45 +24,57 @@ def setUp(self): def test_record_batch(self): counter = metrics.Counter() - self.meter.record_batch(("values"), ((counter, 1),)) + label_set = metrics.LabelSet() + self.meter.record_batch(label_set, ((counter, 1),)) def test_create_metric(self): metric = self.meter.create_metric("", "", "", float, metrics.Counter) self.assertIsInstance(metric, metrics.DefaultMetric) + def test_get_label_set(self): + metric = self.meter.get_label_set({}) + self.assertIsInstance(metric, metrics.DefaultLabelSet) + class TestMetrics(unittest.TestCase): def test_default(self): default = metrics.DefaultMetric() - handle = default.get_handle(("test", "test1")) + default_ls = metrics.DefaultLabelSet() + handle = default.get_handle(default_ls) self.assertIsInstance(handle, metrics.DefaultMetricHandle) def test_counter(self): counter = metrics.Counter() - handle = counter.get_handle(("test", "test1")) + label_set = metrics.LabelSet() + handle = counter.get_handle(label_set) self.assertIsInstance(handle, metrics.CounterHandle) def test_counter_add(self): counter = metrics.Counter() - counter.add(("value",), 1) + label_set = metrics.LabelSet() + counter.add(label_set, 1) def test_gauge(self): gauge = metrics.Gauge() - handle = gauge.get_handle(("test", "test1")) + label_set = metrics.LabelSet() + handle = gauge.get_handle(label_set) self.assertIsInstance(handle, metrics.GaugeHandle) def test_gauge_set(self): gauge = metrics.Gauge() - gauge.set(("value",), 1) + label_set = metrics.LabelSet() + gauge.set(label_set, 1) def test_measure(self): measure = metrics.Measure() - handle = measure.get_handle(("test", "test1")) + label_set = metrics.LabelSet() + handle = measure.get_handle(label_set) self.assertIsInstance(handle, metrics.MeasureHandle) def test_measure_record(self): measure = metrics.Measure() - measure.record(("value",), 1) + label_set = metrics.LabelSet() + measure.record(label_set, 1) def test_default_handle(self): metrics.DefaultMetricHandle() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index e6f5d53166..bb495bc1be 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -13,7 +13,8 @@ # limitations under the License. import logging -from typing import Sequence, Tuple, Type +from collections import OrderedDict +from typing import Dict, Sequence, Tuple, Type from opentelemetry import metrics as metrics_api from opentelemetry.util import time_ns @@ -21,6 +22,14 @@ logger = logging.getLogger(__name__) +# pylint: disable=redefined-outer-name +class LabelSet(metrics_api.LabelSet): + """See `opentelemetry.metrics.LabelSet.""" + + def __init__(self, labels: Dict[str, str] = None): + self.labels = labels + + class BaseHandle: def __init__( self, @@ -107,14 +116,14 @@ def __init__( self.monotonic = monotonic self.handles = {} - def get_handle(self, label_values: Sequence[str]) -> BaseHandle: + def get_handle(self, label_set: LabelSet) -> BaseHandle: """See `opentelemetry.metrics.Metric.get_handle`.""" - handle = self.handles.get(label_values) + handle = self.handles.get(label_set) if not handle: handle = self.HANDLE_TYPE( self.value_type, self.enabled, self.monotonic ) - self.handles[label_values] = handle + self.handles[label_set] = handle return handle def __repr__(self): @@ -155,11 +164,9 @@ def __init__( monotonic=monotonic, ) - def add( - self, label_values: Sequence[str], value: metrics_api.ValueT - ) -> None: + def add(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.Counter.add`.""" - self.get_handle(label_values).add(value) + self.get_handle(label_set).add(value) UPDATE_FUNCTION = add @@ -193,11 +200,9 @@ def __init__( monotonic=monotonic, ) - def set( - self, label_values: Sequence[str], value: metrics_api.ValueT - ) -> None: + def set(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.Gauge.set`.""" - self.get_handle(label_values).set(value) + self.get_handle(label_set).set(value) UPDATE_FUNCTION = set @@ -231,26 +236,31 @@ def __init__( monotonic=monotonic, ) - def record( - self, label_values: Sequence[str], value: metrics_api.ValueT - ) -> None: + def record(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.Measure.record`.""" - self.get_handle(label_values).record(value) + self.get_handle(label_set).record(value) UPDATE_FUNCTION = record +# Used when getting a LabelSet with no key/values +EMPTY_LABEL_SET = LabelSet() + + class Meter(metrics_api.Meter): """See `opentelemetry.metrics.Meter`.""" + def __init__(self): + self.labels = {} + def record_batch( self, - label_values: Sequence[str], + label_set: LabelSet, record_tuples: Sequence[Tuple[metrics_api.Metric, metrics_api.ValueT]], ) -> None: """See `opentelemetry.metrics.Meter.record_batch`.""" for metric, value in record_tuples: - metric.UPDATE_FUNCTION(label_values, value) + metric.UPDATE_FUNCTION(label_set, value) def create_metric( self, @@ -275,5 +285,22 @@ def create_metric( monotonic=monotonic, ) + def get_label_set(self, labels: Dict[str, str]): + """See `opentelemetry.metrics.Meter.create_metric`. + + This implementation encodes the labels to use as a map key. + + Args: + labels: The dictionary of label keys to label values. + """ + if len(labels) == 0: + return EMPTY_LABEL_SET + # Use simple encoding for now until encoding API is implemented + encoded = tuple(sorted(labels.items())) + # If LabelSet exists for this meter in memory, use existing one + if encoded not in self.labels: + self.labels[encoded] = LabelSet(labels=labels) + return self.labels[encoded] + meter = Meter() diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index ca8e8a3631..4d8e6df857 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -22,19 +22,22 @@ class TestConsoleMetricsExporter(unittest.TestCase): # pylint: disable=no-self-use def test_export(self): + meter = metrics.Meter() exporter = ConsoleMetricsExporter() metric = metrics.Counter( "available memory", "available memory", "bytes", int, + meter, ("environment",), ) - label_values = ("staging",) - handle = metric.get_handle(label_values) + kvp = {"environment": "staging"} + label_set = meter.get_label_set(kvp) + handle = metric.get_handle(label_set) result = '{}(data="{}", label_values="{}", metric_data={})'.format( - ConsoleMetricsExporter.__name__, metric, label_values, handle + ConsoleMetricsExporter.__name__, metric, label_set, handle ) with mock.patch("sys.stdout") as mock_stdout: - exporter.export([(metric, label_values)]) + exporter.export([(metric, label_set)]) mock_stdout.write.assert_any_call(result) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index cc37bc1a8a..81e6dd2c9d 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -27,35 +27,46 @@ def test_extends_api(self): def test_record_batch(self): meter = metrics.Meter() label_keys = ("key1",) - label_values = ("value1",) - counter = metrics.Counter("name", "desc", "unit", float, label_keys) + counter = metrics.Counter( + "name", "desc", "unit", float, meter, label_keys + ) + kvp = {"key1": "value1"} + label_set = meter.get_label_set(kvp) record_tuples = [(counter, 1.0)] - meter.record_batch(label_values, record_tuples) - self.assertEqual(counter.get_handle(label_values).data, 1.0) + meter.record_batch(label_set, record_tuples) + self.assertEqual(counter.get_handle(label_set).data, 1.0) def test_record_batch_multiple(self): meter = metrics.Meter() label_keys = ("key1", "key2", "key3") - label_values = ("value1", "value2", "value3") - counter = metrics.Counter("name", "desc", "unit", float, label_keys) + kvp = {"key1": "value1", "key2": "value2", "key3": "value3"} + label_set = meter.get_label_set(kvp) + counter = metrics.Counter( + "name", "desc", "unit", float, meter, label_keys + ) gauge = metrics.Gauge("name", "desc", "unit", int, label_keys) - measure = metrics.Measure("name", "desc", "unit", float, label_keys) + measure = metrics.Measure( + "name", "desc", "unit", float, meter, label_keys + ) record_tuples = [(counter, 1.0), (gauge, 5), (measure, 3.0)] - meter.record_batch(label_values, record_tuples) - self.assertEqual(counter.get_handle(label_values).data, 1.0) - self.assertEqual(gauge.get_handle(label_values).data, 5) - self.assertEqual(measure.get_handle(label_values).data, 0) + meter.record_batch(label_set, record_tuples) + self.assertEqual(counter.get_handle(label_set).data, 1.0) + self.assertEqual(gauge.get_handle(label_set).data, 5) + self.assertEqual(measure.get_handle(label_set).data, 0) def test_record_batch_exists(self): meter = metrics.Meter() label_keys = ("key1",) - label_values = ("value1",) - counter = metrics.Counter("name", "desc", "unit", float, label_keys) - counter.add(label_values, 1.0) - handle = counter.get_handle(label_values) + kvp = {"key1": "value1"} + label_set = meter.get_label_set(kvp) + counter = metrics.Counter( + "name", "desc", "unit", float, meter, label_keys + ) + counter.add(label_set, 1.0) + handle = counter.get_handle(label_set) record_tuples = [(counter, 1.0)] - meter.record_batch(label_values, record_tuples) - self.assertEqual(counter.get_handle(label_values), handle) + meter.record_batch(label_set, record_tuples) + self.assertEqual(counter.get_handle(label_set), handle) self.assertEqual(handle.data, 2.0) def test_create_metric(self): @@ -85,41 +96,72 @@ def test_create_measure(self): self.assertEqual(measure.value_type, float) self.assertEqual(measure.name, "name") + def test_get_label_set(self): + meter = metrics.Meter() + kvp = {"environment": "staging", "a": "z"} + label_set = meter.get_label_set(kvp) + encoded = tuple(sorted(kvp.items())) + self.assertIs(meter.labels[encoded], label_set) + + def test_get_label_set_empty(self): + meter = metrics.Meter() + kvp = {} + label_set = meter.get_label_set(kvp) + self.assertEqual(label_set, metrics.EMPTY_LABEL_SET) + + def test_get_label_set_exists(self): + meter = metrics.Meter() + kvp = {"environment": "staging", "a": "z"} + label_set = meter.get_label_set(kvp) + label_set2 = meter.get_label_set(kvp) + self.assertIs(label_set, label_set2) + class TestMetric(unittest.TestCase): def test_get_handle(self): + meter = metrics.Meter() metric_types = [metrics.Counter, metrics.Gauge, metrics.Measure] for _type in metric_types: - metric = _type("name", "desc", "unit", int, ("key",)) - label_values = ("value",) - handle = metric.get_handle(label_values) - self.assertEqual(metric.handles.get(label_values), handle) + metric = _type("name", "desc", "unit", int, meter, ("key",)) + kvp = {"key": "value"} + label_set = meter.get_label_set(kvp) + handle = metric.get_handle(label_set) + self.assertEqual(metric.handles.get(label_set), handle) class TestCounter(unittest.TestCase): def test_add(self): + meter = metrics.Meter() metric = metrics.Counter("name", "desc", "unit", int, ("key",)) - handle = metric.get_handle(("value",)) - metric.add(("value",), 3) - metric.add(("value",), 2) + kvp = {"key": "value"} + label_set = meter.get_label_set(kvp) + handle = metric.get_handle(label_set) + metric.add(label_set, 3) + metric.add(label_set, 2) self.assertEqual(handle.data, 5) class TestGauge(unittest.TestCase): def test_set(self): + meter = metrics.Meter() metric = metrics.Gauge("name", "desc", "unit", int, ("key",)) - handle = metric.get_handle(("value",)) - metric.set(("value",), 3) + kvp = {"key": "value"} + label_set = meter.get_label_set(kvp) + handle = metric.get_handle(label_set) + metric.set(label_set, 3) self.assertEqual(handle.data, 3) - metric.set(("value",), 2) + metric.set(label_set, 2) self.assertEqual(handle.data, 2) class TestMeasure(unittest.TestCase): def test_record(self): + meter = metrics.Meter() metric = metrics.Measure("name", "desc", "unit", int, ("key",)) - handle = metric.get_handle(("value",)) - metric.record(("value",), 3) + kvp = {"key": "value"} + label_set = meter.get_label_set(kvp) + handle = metric.get_handle(label_set) + metric.record(label_set, 3) # Record not implemented yet self.assertEqual(handle.data, 0) diff --git a/tox.ini b/tox.ini index 7dc2eb6dff..9898094ca3 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,7 @@ deps = test: pytest!=5.2.3 coverage: pytest!=5.2.3 coverage: pytest-cov - mypy,mypyinstalled: mypy~=0.740 + mypy,mypyinstalled: mypy==0.740 setenv = mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ From 15991afceaec3ca3ff5ea19419201f960850d0e3 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 3 Dec 2019 15:59:49 -0800 Subject: [PATCH 0150/1517] Adding link to docs (#312) Fixes #245 --- .../tests/test_shim.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index b1cefbcbb4..0daabf199a 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -132,8 +132,8 @@ def test_explicit_start_time(self): now = time.time() with self.shim.start_active_span("TestSpan", start_time=now) as scope: result = util.time_seconds_from_ns(scope.span.unwrap().start_time) - # Tolerate inaccuracies of less than a microsecond. - # TODO: Put a link to an explanation in the docs. + # Tolerate inaccuracies of less than a microsecond. See Note: + # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.ext.opentracing_shim.html # TODO: This seems to work consistently, but we should find out the # biggest possible loss of precision. self.assertAlmostEqual(result, now, places=6) @@ -146,8 +146,8 @@ def test_explicit_end_time(self): span.finish(now) end_time = util.time_seconds_from_ns(span.unwrap().end_time) - # Tolerate inaccuracies of less than a microsecond. - # TODO: Put a link to an explanation in the docs. + # Tolerate inaccuracies of less than a microsecond. See Note: + # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.ext.opentracing_shim.html # TODO: This seems to work consistently, but we should find out the # biggest possible loss of precision. self.assertAlmostEqual(end_time, now, places=6) @@ -412,8 +412,8 @@ def test_log_kv(self): span.unwrap().events[1].timestamp ) self.assertEqual(span.unwrap().events[1].attributes["foo"], "bar") - # Tolerate inaccuracies of less than a microsecond. - # TODO: Put a link to an explanation in the docs. + # Tolerate inaccuracies of less than a microsecond. See Note: + # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.ext.opentracing_shim.html # TODO: This seems to work consistently, but we should find out the # biggest possible loss of precision. self.assertAlmostEqual(result, now, places=6) From cfecca122c202f1dcc9361f6ab2b286c0ec17226 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 6 Dec 2019 10:48:03 -0800 Subject: [PATCH 0151/1517] Ensure the API returns right value types (#307) Fixes #142 Enabling --strict mode for mypy. Added a test in the sdk and the same test in the api to test the different behaviours between the Tracer, Span and Metric classes. --- mypy.ini | 5 ++ .../src/opentelemetry/metrics/__init__.py | 2 +- .../src/opentelemetry/trace/__init__.py | 12 +++-- .../src/opentelemetry/trace/sampling.py | 2 +- .../tests/test_implementation.py | 54 +++++++++++++++++++ .../src/opentelemetry/sdk/metrics/__init__.py | 10 ++-- .../src/opentelemetry/sdk/trace/__init__.py | 4 +- .../tests/test_implementation.py | 53 ++++++++++++++++++ 8 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 opentelemetry-api/tests/test_implementation.py create mode 100644 opentelemetry-sdk/tests/test_implementation.py diff --git a/mypy.ini b/mypy.ini index ba375b62b1..dca41f8c6b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -10,6 +10,11 @@ disallow_incomplete_defs = True check_untyped_defs = True disallow_untyped_decorators = True + warn_unused_configs = True warn_unused_ignores = True warn_return_any = True + warn_redundant_casts = True strict_equality = True + strict_optional = True + no_implicit_optional = True + no_implicit_reexport = True \ No newline at end of file diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 465020606d..4946300e15 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -218,7 +218,7 @@ def create_metric( unit: str, value_type: Type[ValueT], metric_type: Type[MetricT], - label_keys: Sequence[str] = None, + label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, ) -> "Metric": diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index f2abf8ff9b..27361b9a43 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -145,7 +145,7 @@ class SpanKind(enum.Enum): class Span: """A span represents a single operation within a trace.""" - def end(self, end_time: int = None) -> None: + def end(self, end_time: typing.Optional[int] = None) -> None: """Sets the current time as the span's end time. The span's end time is the wall time at which the operation finished. @@ -163,6 +163,8 @@ def get_context(self) -> "SpanContext": Returns: A :class:`.SpanContext` with a copy of this span's immutable state. """ + # pylint: disable=no-self-use + return INVALID_SPAN_CONTEXT def set_attribute(self, key: str, value: types.AttributeValue) -> None: """Sets an Attribute. @@ -174,7 +176,7 @@ def add_event( self, name: str, attributes: types.Attributes = None, - timestamp: int = None, + timestamp: typing.Optional[int] = None, ) -> None: """Adds an `Event`. @@ -204,6 +206,8 @@ def is_recording_events(self) -> bool: Returns true if this Span is active and recording information like events with the add_event operation and attributes using set_attribute. """ + # pylint: disable=no-self-use + return False def set_status(self, status: Status) -> None: """Sets the Status of the Span. If used, this will override the default @@ -298,8 +302,8 @@ def __init__( self, trace_id: int, span_id: int, - trace_options: "TraceOptions" = None, - trace_state: "TraceState" = None, + trace_options: "TraceOptions" = DEFAULT_TRACE_OPTIONS, + trace_state: "TraceState" = DEFAULT_TRACE_STATE, ) -> None: if trace_options is None: trace_options = DEFAULT_TRACE_OPTIONS diff --git a/opentelemetry-api/src/opentelemetry/trace/sampling.py b/opentelemetry-api/src/opentelemetry/trace/sampling.py index 967f4fd46b..e0f5f17c75 100644 --- a/opentelemetry-api/src/opentelemetry/trace/sampling.py +++ b/opentelemetry-api/src/opentelemetry/trace/sampling.py @@ -36,7 +36,7 @@ def __repr__(self) -> str: def __init__( self, sampled: bool = False, - attributes: Mapping[str, "AttributeValue"] = None, + attributes: Optional[Mapping[str, "AttributeValue"]] = None, ) -> None: self.sampled = sampled # type: bool if attributes is None: diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py new file mode 100644 index 0000000000..60bf9dd9fa --- /dev/null +++ b/opentelemetry-api/tests/test_implementation.py @@ -0,0 +1,54 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import metrics, trace + + +class TestAPIOnlyImplementation(unittest.TestCase): + """ + This test is in place to ensure the API is returning values that + are valid. The same tests have been added to the SDK with + different expected results. See issue for more details: + https://github.com/open-telemetry/opentelemetry-python/issues/142 + """ + + def test_tracer(self): + tracer = trace.Tracer() + with tracer.start_span("test") as span: + self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) + self.assertEqual(span, trace.INVALID_SPAN) + self.assertIs(span.is_recording_events(), False) + with tracer.start_span("test2") as span2: + self.assertEqual( + span2.get_context(), trace.INVALID_SPAN_CONTEXT + ) + self.assertEqual(span2, trace.INVALID_SPAN) + self.assertIs(span2.is_recording_events(), False) + + def test_span(self): + span = trace.Span() + self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) + self.assertIs(span.is_recording_events(), False) + + def test_default_span(self): + span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) + self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) + self.assertIs(span.is_recording_events(), False) + + def test_meter(self): + meter = metrics.Meter() + metric = meter.create_metric("", "", "", float, metrics.Counter) + self.assertIsInstance(metric, metrics.DefaultMetric) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index bb495bc1be..753c35a32e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -103,7 +103,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], - label_keys: Sequence[str] = None, + label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, ): @@ -150,7 +150,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], - label_keys: Sequence[str] = None, + label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = True, ): @@ -186,7 +186,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], - label_keys: Sequence[str] = None, + label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, ): @@ -222,7 +222,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], - label_keys: Sequence[str] = None, + label_keys: Sequence[str] = (), enabled: bool = False, monotonic: bool = False, ): @@ -269,7 +269,7 @@ def create_metric( unit: str, value_type: Type[metrics_api.ValueT], metric_type: Type[metrics_api.MetricT], - label_keys: Sequence[str] = None, + label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, ) -> metrics_api.MetricT: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index c6fe5829cc..5967960ba3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -204,7 +204,7 @@ def add_event( self, name: str, attributes: types.Attributes = None, - timestamp: int = None, + timestamp: Optional[int] = None, ) -> None: self.add_lazy_event( trace_api.Event( @@ -241,7 +241,7 @@ def start(self, start_time: Optional[int] = None) -> None: return self.span_processor.on_start(self) - def end(self, end_time: int = None) -> None: + def end(self, end_time: Optional[int] = None) -> None: with self._lock: if not self.is_recording_events(): return diff --git a/opentelemetry-sdk/tests/test_implementation.py b/opentelemetry-sdk/tests/test_implementation.py new file mode 100644 index 0000000000..9aaa5fc35a --- /dev/null +++ b/opentelemetry-sdk/tests/test_implementation.py @@ -0,0 +1,53 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry.metrics import DefaultMetric +from opentelemetry.sdk import metrics, trace +from opentelemetry.trace import INVALID_SPAN, INVALID_SPAN_CONTEXT + + +class TestSDKImplementation(unittest.TestCase): + """ + This test is in place to ensure the SDK implementation of the API + is returning values that are valid. The same tests have been added + to the API with different expected results. See issue for more details: + https://github.com/open-telemetry/opentelemetry-python/issues/142 + """ + + def test_tracer(self): + tracer = trace.Tracer() + with tracer.start_span("test") as span: + self.assertNotEqual(span.get_context(), INVALID_SPAN_CONTEXT) + self.assertNotEqual(span, INVALID_SPAN) + self.assertIs(span.is_recording_events(), True) + with tracer.start_span("test2") as span2: + self.assertNotEqual(span2.get_context(), INVALID_SPAN_CONTEXT) + self.assertNotEqual(span2, INVALID_SPAN) + self.assertIs(span2.is_recording_events(), True) + + def test_span(self): + with self.assertRaises(Exception): + # pylint: disable=no-value-for-parameter + span = trace.Span() + + span = trace.Span("name", INVALID_SPAN_CONTEXT) + self.assertEqual(span.get_context(), INVALID_SPAN_CONTEXT) + self.assertIs(span.is_recording_events(), True) + + def test_meter(self): + meter = metrics.Meter() + metric = meter.create_metric("", "", "", float, metrics.Counter) + self.assertNotIsInstance(metric, DefaultMetric) From 3d441b1f8243147b552e281f3ff23cd017ef942a Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Fri, 6 Dec 2019 10:49:16 -0800 Subject: [PATCH 0152/1517] Remove obsolete version requirement for pytest (#317) --- tox.ini | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 9898094ca3..50e89d2f60 100644 --- a/tox.ini +++ b/tox.ini @@ -22,10 +22,8 @@ python = [testenv] deps = - ; skip 5.2.3 pytest due to bug - ; https://github.com/pytest-dev/pytest/issues/6194 - test: pytest!=5.2.3 - coverage: pytest!=5.2.3 + test: pytest + coverage: pytest coverage: pytest-cov mypy,mypyinstalled: mypy==0.740 From 1c8b9a292780d480eb45169fe60c7adcdf65b372 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 9 Dec 2019 15:24:12 -0800 Subject: [PATCH 0153/1517] Update CODEOWNERS to include only python-approvers (#321) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5f19bb72a0..d7f0199e5d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,4 +2,4 @@ # This file controls who is tagged for review for any given pull request. # For anything not explicitly taken by someone else: -* @a-feld @c24t @carlosalberto @lzchen @Oberon00 @reyang @toumorokoshi +* @open-telemetry/python-approvers From 889baba8bba29271a4c5d4791e42728a7c4bdaf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 10 Dec 2019 00:26:00 +0100 Subject: [PATCH 0154/1517] Update WSGI & Flask integrations to follow new semantic conventions (#299) Updates flask & WSGI integrations to follow new semantic conventions for HTTP as of #263. --- .flake8 | 1 + .../src/opentelemetry/ext/flask/__init__.py | 9 +- .../tests/test_flask_integration.py | 56 +++++--- .../src/opentelemetry/ext/wsgi/__init__.py | 126 +++++++++++------ .../tests/test_wsgi_middleware.py | 132 ++++++++++++------ 5 files changed, 216 insertions(+), 108 deletions(-) diff --git a/.flake8 b/.flake8 index a0924f947d..8fc8507d92 100644 --- a/.flake8 +++ b/.flake8 @@ -3,6 +3,7 @@ ignore = E501 # line too long, defer to black F401 # unused import, defer to pylint W503 # allow line breaks after binary ops, not after + E203 # allow whitespace before ':' (https://github.com/psf/black#slices) exclude = .bzr .git diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index 662cea752a..cce038ccb5 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -62,20 +62,21 @@ def _before_flask_request(): tracer = trace.tracer() + attributes = otel_wsgi.collect_request_attributes(environ) + if flask_request.url_rule: + # For 404 that result from no route found, etc, we don't have a url_rule. + attributes["http.route"] = flask_request.url_rule.rule span = tracer.start_span( span_name, parent_span, kind=trace.SpanKind.SERVER, + attributes=attributes, start_time=environ.get(_ENVIRON_STARTTIME_KEY), ) activation = tracer.use_span(span, end_on_exit=True) activation.__enter__() environ[_ENVIRON_ACTIVATION_KEY] = activation environ[_ENVIRON_SPAN_KEY] = span - otel_wsgi.add_request_attributes(span, environ) - if flask_request.url_rule: - # For 404 that result from no route found, etc, we don't have a url_rule. - span.set_attribute("http.route", flask_request.url_rule.rule) def _teardown_flask_request(exc): diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py index d03e7604a8..b943a25f22 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py @@ -57,6 +57,17 @@ def test_simple(self): "hello_endpoint", trace_api.INVALID_SPAN_CONTEXT, kind=trace_api.SpanKind.SERVER, + attributes={ + "component": "http", + "http.method": "GET", + "http.server_name": "localhost", + "http.scheme": "http", + "host.port": 80, + "http.host": "localhost", + "http.target": "/hello/123", + "http.flavor": "1.1", + "http.route": "/hello/", + }, start_time=mock.ANY, ) @@ -64,15 +75,7 @@ def test_simple(self): self.assertEqual( self.span_attrs, - { - "component": "http", - "http.method": "GET", - "http.host": "localhost", - "http.url": "http://localhost/hello/123", - "http.route": "/hello/", - "http.status_code": 200, - "http.status_text": "OK", - }, + {"http.status_code": 200, "http.status_text": "OK"}, ) def test_404(self): @@ -84,6 +87,16 @@ def test_404(self): "/bye", trace_api.INVALID_SPAN_CONTEXT, kind=trace_api.SpanKind.SERVER, + attributes={ + "component": "http", + "http.method": "POST", + "http.server_name": "localhost", + "http.scheme": "http", + "host.port": 80, + "http.host": "localhost", + "http.target": "/bye", + "http.flavor": "1.1", + }, start_time=mock.ANY, ) @@ -93,14 +106,7 @@ def test_404(self): self.assertEqual( self.span_attrs, - { - "component": "http", - "http.method": "POST", - "http.host": "localhost", - "http.url": "http://localhost/bye", - "http.status_code": 404, - "http.status_text": "NOT FOUND", - }, + {"http.status_code": 404, "http.status_text": "NOT FOUND"}, ) def test_internal_error(self): @@ -112,6 +118,17 @@ def test_internal_error(self): "hello_endpoint", trace_api.INVALID_SPAN_CONTEXT, kind=trace_api.SpanKind.SERVER, + attributes={ + "component": "http", + "http.method": "GET", + "http.server_name": "localhost", + "http.scheme": "http", + "host.port": 80, + "http.host": "localhost", + "http.target": "/hello/500", + "http.flavor": "1.1", + "http.route": "/hello/", + }, start_time=mock.ANY, ) @@ -122,11 +139,6 @@ def test_internal_error(self): self.assertEqual( self.span_attrs, { - "component": "http", - "http.method": "GET", - "http.host": "localhost", - "http.url": "http://localhost/hello/500", - "http.route": "/hello/", "http.status_code": 500, "http.status_text": "INTERNAL SERVER ERROR", }, diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 0d947ff867..16a0f9d944 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -24,6 +24,9 @@ from opentelemetry import propagators, trace from opentelemetry.ext.wsgi.version import __version__ # noqa +from opentelemetry.trace.status import Status, StatusCanonicalCode + +_HTTP_VERSION_PREFIX = "HTTP/" def get_header_from_environ( @@ -41,44 +44,80 @@ def get_header_from_environ( return [] -def add_request_attributes(span, environ): - """Adds HTTP request attributes from the PEP3333-conforming WSGI environ to span.""" - - span.set_attribute("component", "http") - span.set_attribute("http.method", environ["REQUEST_METHOD"]) - - host = environ.get("HTTP_HOST") - if not host: - host = environ["SERVER_NAME"] - port = environ["SERVER_PORT"] - scheme = environ["wsgi.url_scheme"] - if ( - scheme == "http" - and port != "80" - or scheme == "https" - and port != "443" - ): - host += ":" + port - - # NOTE: Nonstandard (but see - # https://github.com/open-telemetry/opentelemetry-specification/pull/263) - span.set_attribute("http.host", host) - - url = environ.get("REQUEST_URI") or environ.get("RAW_URI") - - if url: - if url[0] == "/": - # We assume that no scheme-relative URLs will be in url here. - # After all, if a request is made to http://myserver//foo, we may get - # //foo which looks like scheme-relative but isn't. - url = environ["wsgi.url_scheme"] + "://" + host + url - elif not url.startswith(environ["wsgi.url_scheme"] + ":"): - # Something fishy is in RAW_URL. Let's fall back to request_uri() - url = wsgiref_util.request_uri(environ) +def setifnotnone(dic, key, value): + if value is not None: + dic[key] = value + + +def http_status_to_canonical_code(code: int, allow_redirect: bool = True): + # pylint:disable=too-many-branches,too-many-return-statements + if code < 100: + return StatusCanonicalCode.UNKNOWN + if code <= 299: + return StatusCanonicalCode.OK + if code <= 399: + if allow_redirect: + return StatusCanonicalCode.OK + return StatusCanonicalCode.DEADLINE_EXCEEDED + if code <= 499: + if code == 401: # HTTPStatus.UNAUTHORIZED: + return StatusCanonicalCode.UNAUTHENTICATED + if code == 403: # HTTPStatus.FORBIDDEN: + return StatusCanonicalCode.PERMISSION_DENIED + if code == 404: # HTTPStatus.NOT_FOUND: + return StatusCanonicalCode.NOT_FOUND + if code == 429: # HTTPStatus.TOO_MANY_REQUESTS: + return StatusCanonicalCode.RESOURCE_EXHAUSTED + return StatusCanonicalCode.INVALID_ARGUMENT + if code <= 599: + if code == 501: # HTTPStatus.NOT_IMPLEMENTED: + return StatusCanonicalCode.UNIMPLEMENTED + if code == 503: # HTTPStatus.SERVICE_UNAVAILABLE: + return StatusCanonicalCode.UNAVAILABLE + if code == 504: # HTTPStatus.GATEWAY_TIMEOUT: + return StatusCanonicalCode.DEADLINE_EXCEEDED + return StatusCanonicalCode.INTERNAL + return StatusCanonicalCode.UNKNOWN + + +def collect_request_attributes(environ): + """Collects HTTP request attributes from the PEP3333-conforming + WSGI environ and returns a dictionary to be used as span creation attributes.""" + + result = { + "component": "http", + "http.method": environ["REQUEST_METHOD"], + "http.server_name": environ["SERVER_NAME"], + "http.scheme": environ["wsgi.url_scheme"], + "host.port": int(environ["SERVER_PORT"]), + } + + setifnotnone(result, "http.host", environ.get("HTTP_HOST")) + target = environ.get("RAW_URI") + if target is None: # Note: `"" or None is None` + target = environ.get("REQUEST_URI") + if target is not None: + result["http.target"] = target else: - url = wsgiref_util.request_uri(environ) + result["http.url"] = wsgiref_util.request_uri(environ) + + remote_addr = environ.get("REMOTE_ADDR") + if remote_addr: + result[ + "peer.ipv6" if ":" in remote_addr else "peer.ipv4" + ] = remote_addr + remote_host = environ.get("REMOTE_HOST") + if remote_host and remote_host != remote_addr: + result["peer.hostname"] = remote_host - span.set_attribute("http.url", url) + setifnotnone(result, "peer.port", environ.get("REMOTE_PORT")) + flavor = environ.get("SERVER_PROTOCOL", "") + if flavor.upper().startswith(_HTTP_VERSION_PREFIX): + flavor = flavor[len(_HTTP_VERSION_PREFIX) :] + if flavor: + result["http.flavor"] = flavor + + return result def add_response_attributes( @@ -93,9 +132,15 @@ def add_response_attributes( try: status_code = int(status_code) except ValueError: - pass + span.set_status( + Status( + StatusCanonicalCode.UNKNOWN, + "Non-integer HTTP status: " + repr(status_code), + ) + ) else: span.set_attribute("http.status_code", status_code) + span.set_status(Status(http_status_to_canonical_code(status_code))) def get_default_span_name(environ): @@ -142,12 +187,14 @@ def __call__(self, environ, start_response): span_name = get_default_span_name(environ) span = tracer.start_span( - span_name, parent_span, kind=trace.SpanKind.SERVER + span_name, + parent_span, + kind=trace.SpanKind.SERVER, + attributes=collect_request_attributes(environ), ) try: with tracer.use_span(span): - add_request_attributes(span, environ) start_response = self._create_start_response( span, start_response ) @@ -155,6 +202,7 @@ def __call__(self, environ, start_response): iterable = self.wsgi(environ, start_response) return _end_span_after_iterating(iterable, span, tracer) except: # noqa + # TODO Set span status (cf. https://github.com/open-telemetry/opentelemetry-python/issues/292) span.end() raise diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index 97c93880e4..a78b4d19f0 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -16,7 +16,7 @@ import unittest import unittest.mock as mock import wsgiref.util as wsgiref_util -from urllib.parse import urlparse +from urllib.parse import urlsplit import opentelemetry.ext.wsgi as otel_wsgi from opentelemetry import trace as trace_api @@ -97,7 +97,19 @@ def validate_response(self, response, error=None): # Verify that start_span has been called self.start_span.assert_called_with( - "/", trace_api.INVALID_SPAN_CONTEXT, kind=trace_api.SpanKind.SERVER + "/", + trace_api.INVALID_SPAN_CONTEXT, + kind=trace_api.SpanKind.SERVER, + attributes={ + "component": "http", + "http.method": "GET", + "http.server_name": "127.0.0.1", + "http.scheme": "http", + "host.port": 80, + "http.host": "127.0.0.1", + "http.flavor": "1.0", + "http.url": "http://127.0.0.1/", + }, ) def test_basic_wsgi_call(self): @@ -144,31 +156,43 @@ def setUp(self): def test_request_attributes(self): self.environ["QUERY_STRING"] = "foo=bar" - otel_wsgi.add_request_attributes(self.span, self.environ) - - expected = ( - mock.call("component", "http"), - mock.call("http.method", "GET"), - mock.call("http.host", "127.0.0.1"), - mock.call("http.url", "http://127.0.0.1/?foo=bar"), + attrs = otel_wsgi.collect_request_attributes(self.environ) + self.assertDictEqual( + attrs, + { + "component": "http", + "http.method": "GET", + "http.host": "127.0.0.1", + "http.url": "http://127.0.0.1/?foo=bar", + "host.port": 80, + "http.scheme": "http", + "http.server_name": "127.0.0.1", + "http.flavor": "1.0", + }, ) - self.assertEqual(self.span.set_attribute.call_count, len(expected)) - self.span.set_attribute.assert_has_calls(expected, any_order=True) - def validate_url(self, expected_url): - otel_wsgi.add_request_attributes(self.span, self.environ) - attrs = { - args[0][0]: args[0][1] - for args in self.span.set_attribute.call_args_list + def validate_url(self, expected_url, raw=False, has_host=True): + parts = urlsplit(expected_url) + expected = { + "http.scheme": parts.scheme, + "host.port": parts.port or (80 if parts.scheme == "http" else 443), + "http.server_name": parts.hostname, # Not true in the general case, but for all tests. } - self.assertIn("http.url", attrs) - self.assertEqual(attrs["http.url"], expected_url) - self.assertIn("http.host", attrs) - self.assertEqual(attrs["http.host"], urlparse(expected_url).netloc) + if raw: + expected["http.target"] = expected_url.split(parts.netloc, 1)[1] + else: + expected["http.url"] = expected_url + if has_host: + expected["http.host"] = parts.hostname + + attrs = otel_wsgi.collect_request_attributes(self.environ) + self.assertGreaterEqual( + attrs.items(), expected.items(), expected_url + " expected." + ) def test_request_attributes_with_partial_raw_uri(self): self.environ["RAW_URI"] = "/#top" - self.validate_url("http://127.0.0.1/#top") + self.validate_url("http://127.0.0.1/#top", raw=True) def test_request_attributes_with_partial_raw_uri_and_nonstandard_port( self, @@ -176,58 +200,80 @@ def test_request_attributes_with_partial_raw_uri_and_nonstandard_port( self.environ["RAW_URI"] = "/?" del self.environ["HTTP_HOST"] self.environ["SERVER_PORT"] = "8080" - self.validate_url("http://127.0.0.1:8080/?") + self.validate_url("http://127.0.0.1:8080/?", raw=True, has_host=False) def test_https_uri_port(self): del self.environ["HTTP_HOST"] self.environ["SERVER_PORT"] = "443" self.environ["wsgi.url_scheme"] = "https" - self.validate_url("https://127.0.0.1/") + self.validate_url("https://127.0.0.1/", has_host=False) self.environ["SERVER_PORT"] = "8080" - self.validate_url("https://127.0.0.1:8080/") + self.validate_url("https://127.0.0.1:8080/", has_host=False) self.environ["SERVER_PORT"] = "80" - self.validate_url("https://127.0.0.1:80/") + self.validate_url("https://127.0.0.1:80/", has_host=False) def test_http_uri_port(self): del self.environ["HTTP_HOST"] self.environ["SERVER_PORT"] = "80" self.environ["wsgi.url_scheme"] = "http" - self.validate_url("http://127.0.0.1/") + self.validate_url("http://127.0.0.1/", has_host=False) self.environ["SERVER_PORT"] = "8080" - self.validate_url("http://127.0.0.1:8080/") + self.validate_url("http://127.0.0.1:8080/", has_host=False) self.environ["SERVER_PORT"] = "443" - self.validate_url("http://127.0.0.1:443/") + self.validate_url("http://127.0.0.1:443/", has_host=False) def test_request_attributes_with_nonstandard_port_and_no_host(self): del self.environ["HTTP_HOST"] self.environ["SERVER_PORT"] = "8080" - self.validate_url("http://127.0.0.1:8080/") + self.validate_url("http://127.0.0.1:8080/", has_host=False) self.environ["SERVER_PORT"] = "443" - self.validate_url("http://127.0.0.1:443/") - - def test_request_attributes_with_nonstandard_port(self): - self.environ["HTTP_HOST"] += ":8080" - self.validate_url("http://127.0.0.1:8080/") + self.validate_url("http://127.0.0.1:443/", has_host=False) + + def test_request_attributes_with_conflicting_nonstandard_port(self): + self.environ[ + "HTTP_HOST" + ] += ":8080" # Note that we do not correct SERVER_PORT + expected = { + "http.host": "127.0.0.1:8080", + "http.url": "http://127.0.0.1:8080/", + "host.port": 80, + } + self.assertGreaterEqual( + otel_wsgi.collect_request_attributes(self.environ).items(), + expected.items(), + ) def test_request_attributes_with_faux_scheme_relative_raw_uri(self): self.environ["RAW_URI"] = "//127.0.0.1/?" - self.validate_url("http://127.0.0.1//127.0.0.1/?") - - def test_request_attributes_with_pathless_raw_uri(self): - self.environ["PATH_INFO"] = "" - self.environ["RAW_URI"] = "http://hello" - self.environ["HTTP_HOST"] = "hello" - self.validate_url("http://hello") + self.validate_url("http://127.0.0.1//127.0.0.1/?", raw=True) + + def test_request_attributes_pathless(self): + self.environ["RAW_URI"] = "" + expected = {"http.target": ""} + self.assertGreaterEqual( + otel_wsgi.collect_request_attributes(self.environ).items(), + expected.items(), + ) def test_request_attributes_with_full_request_uri(self): self.environ["HTTP_HOST"] = "127.0.0.1:8080" - self.environ["REQUEST_URI"] = "http://127.0.0.1:8080/?foo=bar#top" - self.validate_url("http://127.0.0.1:8080/?foo=bar#top") + self.environ["REQUEST_METHOD"] = "CONNECT" + self.environ[ + "REQUEST_URI" + ] = "127.0.0.1:8080" # Might happen in a CONNECT request + expected = { + "http.host": "127.0.0.1:8080", + "http.target": "127.0.0.1:8080", + } + self.assertGreaterEqual( + otel_wsgi.collect_request_attributes(self.environ).items(), + expected.items(), + ) def test_response_attributes(self): otel_wsgi.add_response_attributes(self.span, "404 Not Found", {}) From 28c692e759abc4ea5467e96e3a6528cdd552d20f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 9 Dec 2019 17:26:42 -0600 Subject: [PATCH 0155/1517] Add posargs (#324) Fixes #323 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 50e89d2f60..782ef88ba0 100644 --- a/tox.ini +++ b/tox.ini @@ -88,7 +88,7 @@ commands_pre = mypyinstalled: pip install file://{toxinidir}/opentelemetry-api/ commands = - test: pytest + test: pytest {posargs} coverage: {toxinidir}/scripts/coverage.sh mypy: mypy --namespace-packages opentelemetry-api/src/opentelemetry/ From 105fe2f1c335a4105787cd42ef37453958e54c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 10 Dec 2019 00:39:26 +0100 Subject: [PATCH 0156/1517] Do full Sphinx build every time to not hide errors (#322) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 782ef88ba0..0ccef88f27 100644 --- a/tox.ini +++ b/tox.ini @@ -160,7 +160,7 @@ deps = changedir = docs commands = - sphinx-build -W --keep-going -b html -T . _build/html + sphinx-build -E -a -W --keep-going -b html -T . _build/html [testenv:py37-tracecontext] basepython: python3.7 From b25af7fe39b009bffbb3af9a7b6250348e77d6e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 10 Dec 2019 02:54:56 +0100 Subject: [PATCH 0157/1517] Add eachdist.py to simplify build (#291) --- .flake8 | 2 + .gitignore | 1 + .isort.cfg | 2 +- CONTRIBUTING.md | 9 +- dev-requirements.txt | 10 + eachdist.ini | 16 + ext/opentelemetry-ext-testutil/setup.cfg | 2 +- pyproject.toml | 16 +- scripts/eachdist.py | 508 +++++++++++++++++++++++ tox.ini | 77 +--- 10 files changed, 567 insertions(+), 76 deletions(-) create mode 100644 dev-requirements.txt create mode 100644 eachdist.ini create mode 100755 scripts/eachdist.py diff --git a/.flake8 b/.flake8 index 8fc8507d92..6d1c2bc175 100644 --- a/.flake8 +++ b/.flake8 @@ -11,6 +11,8 @@ exclude = .svn .tox CVS + .venv*/ + venv*/ target __pycache__ ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/ diff --git a/.gitignore b/.gitignore index 473ef20a4a..42e6d0bf04 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ lib lib64 __pycache__ venv*/ +.venv*/ # Installer logs pip-log.txt diff --git a/.isort.cfg b/.isort.cfg index 31620ab99b..4e7bff8bb4 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -13,5 +13,5 @@ line_length=79 ; docs: https://github.com/timothycrosley/isort#multi-line-output-modes multi_line_output=3 skip=target -skip_glob=ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/* +skip_glob=ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/*,.venv*/*,venv*/* known_first_party=opentelemetry diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e84971fa4a..e61bb85c2b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,14 @@ during their normal contribution hours. ## Development -This project uses [`tox`](https://tox.readthedocs.io) to automate some aspects +To quickly get up and running, you can use the `scripts/eachdist.py` tool that +ships with this project. First create a virtualenv and activate it. +Then run `python scripts/eachdist.py develop` to install all required packages +as well as the project's packages themselves (in `--editable` mode). +You can then run `scripts/eachdist.py test` to test everything or +`scripts/eachdist.py lint` to lint everything (fixing anything that is auto-fixable). + +Additionally, this project uses [`tox`](https://tox.readthedocs.io) to automate some aspects of development, including testing against multiple Python versions. You can run: diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000000..6ce5479306 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,10 @@ +pylint~=2.3 +flake8~=3.7 +isort~=4.3 +black>=19.3b0,==19.* +mypy==0.740 +sphinx~=2.1 +sphinx-rtd-theme~=0.4 +sphinx-autodoc-typehints~=1.10.2 +pytest!=5.2.3 +pytest-cov>=2.8 diff --git a/eachdist.ini b/eachdist.ini new file mode 100644 index 0000000000..8baab0c2e1 --- /dev/null +++ b/eachdist.ini @@ -0,0 +1,16 @@ +# These will be sorted first in that order. +# All packages that are depended upon by others should be listed here. +[DEFAULT] +sortfirst= + opentelemetry-api + opentelemetry-sdk + ext/opentelemetry-ext-wsgi + ext/* + +[lintroots] +extraroots=examples/*,scripts/ +subglob=*.py,tests/,test/,src/*,examples/* + +[testroots] +extraroots=examples/*,tests/ +subglob=tests/,test/ diff --git a/ext/opentelemetry-ext-testutil/setup.cfg b/ext/opentelemetry-ext-testutil/setup.cfg index 170e949cf6..520df8bf97 100644 --- a/ext/opentelemetry-ext-testutil/setup.cfg +++ b/ext/opentelemetry-ext-testutil/setup.cfg @@ -38,7 +38,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-ext-wsgi + opentelemetry-api [options.extras_require] test = flask~=1.0 diff --git a/pyproject.toml b/pyproject.toml index cb3c2eb546..5d19c29882 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,21 +3,7 @@ line-length = 79 exclude = ''' ( /( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | \.vscode - | _build - | buck-out - | target - | build - | dist - | ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen # generated files + ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen # generated files )/ - | foo.py # also separately exclude a file named foo.py in - # the root of the project ) ''' diff --git a/scripts/eachdist.py b/scripts/eachdist.py new file mode 100755 index 0000000000..8d41315fc7 --- /dev/null +++ b/scripts/eachdist.py @@ -0,0 +1,508 @@ +#!/usr/bin/env python3 + +import argparse +import shlex +import shutil +import subprocess +import sys +from collections import namedtuple +from configparser import ConfigParser +from inspect import cleandoc +from itertools import chain +from pathlib import Path, PurePath + +DEFAULT_ALLSEP = " " +DEFAULT_ALLFMT = "{rel}" + + +def unique(elems): + seen = set() + for elem in elems: + if elem not in seen: + yield elem + seen.add(elem) + + +try: + subprocess_run = subprocess.run +except AttributeError: # Py < 3.5 compat + CompletedProcess = namedtuple("CompletedProcess", "returncode") + + def subprocess_run(*args, **kwargs): + check = kwargs.pop("check", False) + if check: + subprocess.check_call(*args, **kwargs) + return CompletedProcess(returncode=0) + return CompletedProcess(returncode=subprocess.call(*args, **kwargs)) + + +def extraargs_help(calledcmd): + return cleandoc( + """ + Additional arguments to pass on to {}. + + This is collected from any trailing arguments passed to `%(prog)s`. + Use an initial `--` to separate them from regular arguments. + """.format( + calledcmd + ) + ) + + +def parse_args(args=None): + parser = argparse.ArgumentParser(description="Development helper script.") + parser.set_defaults(parser=parser) + parser.add_argument( + "--dry-run", + action="store_true", + help="Only display what would be done, don't actually do anything.", + ) + subparsers = parser.add_subparsers(metavar="COMMAND") + subparsers.required = True + + excparser = subparsers.add_parser( + "exec", + help="Run a command for each or all targets.", + formatter_class=argparse.RawTextHelpFormatter, + description=cleandoc( + """Run a command according to the `format` argument for each or all targets. + + This is an advanced command that is used internally by other commands. + + For example, to install all distributions in this repository + editable, you could use: + + scripts/eachdist.py exec "python -m pip install -e {}" + + This will run pip for all distributions which is quite slow. It gets + a bit faster if we only invoke pip once but with all the paths + gathered together, which can be achieved by using `--all`: + + scripts/eachdist.py exec "python -m pip install {}" --all "-e {}" + + The sortfirst option in the DEFAULT section of eachdist.ini makes + sure that dependencies are installed before their dependents. + + Search for usages of `parse_subargs` in the source code of this script + to see more examples. + + This command first collects target paths and then executes + commands according to `format` and `--all`. + + Target paths are initially all Python distribution root paths + (as determined by the existence of setup.py, etc. files). + They are then augmented according to the section of the + `PROJECT_ROOT/eachdist.ini` config file specified by the `--mode` option. + + The following config options are available (and processed in that order): + + - `extraroots`: List of project root-relative glob expressions. + The resulting paths will be added. + - `sortfirst`: List of glob expressions. + Any matching paths will be put to the front of the path list, + in the same order they appear in this option. If more than one + glob matches, ordering is according to the first. + - `subglob`: List of glob expressions. Each path added so far is removed + and replaced with the result of all glob expressions relative to it (in + order of the glob expressions). + + After all this, any duplicate paths are removed (the first occurrence remains). + """ + ), + ) + excparser.set_defaults(func=execute_args) + excparser.add_argument( + "format", + help=cleandoc( + """Format string for the command to execute. + + The available replacements depend on whether `--all` is specified. + If `--all` was specified, there is only a single replacement, + `{}`, that is replaced with the string that is generated from + joining all targets formatted with `--all` to a single string + with the value of `--allsep` as separator. + + If `--all` was not specified, the following replacements are available: + + - `{}`: the absolute path to the current target in POSIX format + (with forward slashes) + - `{rel}`: like `{}` but relative to the project root. + - `{raw}`: the absolute path to the current target in native format + (thus exactly the same as `{}` on Unix but with backslashes on Windows). + - `{rawrel}`: like `{raw}` but relative to the project root. + + The resulting string is then split according to POSIX shell rules + (so you can use quotation marks or backslashes to handle arguments + containing spaces). + + The first token is the name of the executable to run, the remaining + tokens are the arguments. + + Note that a shell is *not* involved by default. + You can add bash/sh/cmd/powershell yourself to the format if you want. + + If `--all` was specified, the resulting command is simply executed once. + Otherwise, the command is executed for each found target. In both cases, + the project root is the working directory. + """ + ), + ) + excparser.add_argument( + "--all", + nargs="?", + const=DEFAULT_ALLFMT, + metavar="ALLFORMAT", + help=cleandoc( + """Instead of running the command for each target, join all target + paths together to run a single command. + + This option optionally takes a format string to apply to each path. The + available replacements are the ones that would be available for `format` + if `--all` was not specified. + + Default ALLFORMAT if this flag is specified: `%(const)s`. + """ + ), + ) + excparser.add_argument( + "--allsep", + help=cleandoc( + """Separator string for the strings resulting from `--all`. + Only valid if `--all` is specified. + """ + ), + ) + excparser.add_argument( + "--allowexitcode", + type=int, + action="append", + default=[0], + help=cleandoc( + """The given command exit code is treated as success and does not abort execution. + Can be specified multiple times. + """ + ), + ) + excparser.add_argument( + "--mode", + "-m", + default="DEFAULT", + help=cleandoc( + """Section of config file to use for target selection configuration. + See description of exec for available options.""" + ), + ) + + instparser = subparsers.add_parser( + "install", help="Install all distributions." + ) + + def setup_instparser(instparser): + instparser.set_defaults(func=install_args) + instparser.add_argument( + "pipargs", nargs=argparse.REMAINDER, help=extraargs_help("pip") + ) + + setup_instparser(instparser) + instparser.add_argument("--editable", "-e", action="store_true") + instparser.add_argument("--with-dev-deps", action="store_true") + instparser.add_argument("--eager-upgrades", action="store_true") + + devparser = subparsers.add_parser( + "develop", + help="Install all distributions editable + dev dependencies.", + ) + setup_instparser(devparser) + devparser.set_defaults( + editable=True, with_dev_deps=True, eager_upgrades=True + ) + + lintparser = subparsers.add_parser( + "lint", help="Lint everything, autofixing if possible." + ) + lintparser.add_argument("--check-only", action="store_true") + lintparser.set_defaults(func=lint_args) + + testparser = subparsers.add_parser( + "test", + help="Test everything (run pytest yourself for more complex operations).", + ) + testparser.set_defaults(func=test_args) + testparser.add_argument( + "pytestargs", nargs=argparse.REMAINDER, help=extraargs_help("pytest") + ) + + return parser.parse_args(args) + + +def find_projectroot(search_start=Path(".")): + root = search_start.resolve() + for root in chain((root,), root.parents): + if any((root / marker).exists() for marker in (".git", "tox.ini")): + return root + return None + + +def find_targets_unordered(rootpath): + for subdir in rootpath.iterdir(): + if not subdir.is_dir(): + continue + if subdir.name.startswith(".") or subdir.name.startswith("venv"): + continue + if any( + (subdir / marker).exists() + for marker in ("setup.py", "pyproject.toml") + ): + yield subdir + else: + yield from find_targets_unordered(subdir) + + +def getlistcfg(strval): + return [ + val.strip() + for line in strval.split("\n") + for val in line.split(",") + if val.strip() + ] + + +def find_targets(mode, rootpath): + if not rootpath: + sys.exit("Could not find a root directory.") + + cfg = ConfigParser() + cfg.read(str(rootpath / "eachdist.ini")) + mcfg = cfg[mode] + + targets = list(find_targets_unordered(rootpath)) + if "extraroots" in mcfg: + targets += [ + path + for extraglob in getlistcfg(mcfg["extraroots"]) + for path in rootpath.glob(extraglob) + ] + if "sortfirst" in mcfg: + sortfirst = getlistcfg(mcfg["sortfirst"]) + + def keyfunc(path): + path = path.relative_to(rootpath) + for idx, pattern in enumerate(sortfirst): + if path.match(pattern): + return idx + return float("inf") + + targets.sort(key=keyfunc) + + subglobs = getlistcfg(mcfg.get("subglob", "")) + if subglobs: + targets = [ + newentry + for newentry in ( + target / subdir + for target in targets + for subglob in subglobs + # We need to special-case the dot, because glob fails to parse that with an IndexError. + for subdir in ( + (target,) if subglob == "." else target.glob(subglob) + ) + ) + if ".egg-info" not in str(newentry) and newentry.exists() + ] + + return list(unique(targets)) + + +def runsubprocess(dry_run, params, *args, **kwargs): + cmdstr = join_args(params) + if dry_run: + print(cmdstr) + return None + + # Py < 3.6 compat. + cwd = kwargs.get("cwd") + if cwd and isinstance(cwd, PurePath): + kwargs["cwd"] = str(cwd) + + check = kwargs.pop("check") # Enforce specifying check + + print(">>>", cmdstr, file=sys.stderr) + + # This is a workaround for subprocess.run(['python']) leaving the virtualenv on Win32. + # The cause for this is that when running the python.exe in a virtualenv, + # the wrapper executable launches the global python as a subprocess and the search sequence + # for CreateProcessW which subprocess.run and Popen use is a follows + # (https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw): + # > 1. The directory from which the application loaded. + # This will be the directory of the global python.exe, not the venv directory, due to the suprocess mechanism. + # > 6. The directories that are listed in the PATH environment variable. + # Only this would find the "correct" python.exe. + + params = list(params) + executable = shutil.which(params[0]) # On Win32, pytho + if executable: + params[0] = executable + try: + return subprocess_run(params, *args, check=check, **kwargs) + except OSError as exc: + raise ValueError( + "Failed executing " + repr(params) + ": " + str(exc) + ) from exc + + +def execute_args(args): + if args.allsep and not args.all: + args.parser.error("--allsep specified but not --all.") + + if args.all and not args.allsep: + args.allsep = DEFAULT_ALLSEP + + rootpath = find_projectroot() + targets = find_targets(args.mode, rootpath) + if not targets: + sys.exit("Error: No targets selected (root: {})".format(rootpath)) + + def fmt_for_path(fmt, path): + return fmt.format( + path.as_posix(), + rel=path.relative_to(rootpath).as_posix(), + raw=path, + rawrel=path.relative_to(rootpath), + ) + + def _runcmd(cmd): + result = runsubprocess( + args.dry_run, shlex.split(cmd), cwd=rootpath, check=False + ) + if result is not None and result.returncode not in args.allowexitcode: + print( + "'{}' failed with code {}".format(cmd, result.returncode), + file=sys.stderr, + ) + sys.exit(result.returncode) + + if args.all: + allstr = args.allsep.join( + fmt_for_path(args.all, path) for path in targets + ) + cmd = args.format.format(allstr) + _runcmd(cmd) + else: + for target in targets: + cmd = fmt_for_path(args.format, target) + _runcmd(cmd) + + +def clean_remainder_args(remainder_args): + if remainder_args and remainder_args[0] == "--": + del remainder_args[0] + + +def join_args(arglist): + return " ".join(map(shlex.quote, arglist)) + + +def install_args(args): + clean_remainder_args(args.pipargs) + if args.eager_upgrades: + args.pipargs += ["--upgrade-strategy=eager"] + + if args.with_dev_deps: + runsubprocess( + args.dry_run, + [ + "python", + "-m", + "pip", + "install", + "--upgrade", + "pip", + "setuptools", + "wheel", + ] + + args.pipargs, + check=True, + ) + + allfmt = "-e 'file://{}'" if args.editable else "'file://{}'" + execute_args( + parse_subargs( + args, + ( + "exec", + "python -m pip install {} " + join_args(args.pipargs), + "--all", + allfmt, + ), + ) + ) + if args.with_dev_deps: + rootpath = find_projectroot() + runsubprocess( + args.dry_run, + [ + "python", + "-m", + "pip", + "install", + "--upgrade", + "-r", + str(rootpath / "dev-requirements.txt"), + ] + + args.pipargs, + check=True, + ) + + +def parse_subargs(parentargs, args): + subargs = parse_args(args) + subargs.dry_run = parentargs.dry_run or subargs.dry_run + return subargs + + +def lint_args(args): + rootdir = str(find_projectroot()) + + runsubprocess( + args.dry_run, + ("black", ".") + (("--diff", "--check") if args.check_only else ()), + cwd=rootdir, + check=True, + ) + runsubprocess( + args.dry_run, + ("isort", "--recursive", ".") + + (("--diff", "--check-only") if args.check_only else ()), + cwd=rootdir, + check=True, + ) + runsubprocess(args.dry_run, ("flake8", rootdir), check=True) + execute_args( + parse_subargs( + args, ("exec", "pylint {}", "--all", "--mode", "lintroots",), + ) + ) + + +def test_args(args): + clean_remainder_args(args.pytestargs) + execute_args( + parse_subargs( + args, + ( + "exec", + "pytest {} " + join_args(args.pytestargs), + "--mode", + "testroots", + ), + ) + ) + + +def main(): + args = parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/tox.ini b/tox.ini index 0ccef88f27..80123a5ead 100644 --- a/tox.ini +++ b/tox.ini @@ -22,10 +22,11 @@ python = [testenv] deps = + -c dev-requirements.txt test: pytest coverage: pytest coverage: pytest-cov - mypy,mypyinstalled: mypy==0.740 + mypy,mypyinstalled: mypy setenv = mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ @@ -73,15 +74,7 @@ commands_pre = ; In order to get a healthy coverage report, ; we have to install packages in editable mode. - coverage: pip install -e {toxinidir}/opentelemetry-api - coverage: pip install -e {toxinidir}/opentelemetry-sdk - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-testutil - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-flask[test] - coverage: pip install -e {toxinidir}/examples/opentelemetry-example-app + coverage: python {toxinidir}/scripts/eachdist.py install --editable ; Using file:// here because otherwise tox invokes just "pip install ; opentelemetry-api", leading to an error @@ -104,56 +97,24 @@ commands = basepython: python3.7 recreate = True deps = - pylint~=2.3 - flake8~=3.7 - isort~=4.3 - black>=19.3b0,==19.* + -c dev-requirements.txt + pylint + flake8 + isort + black commands_pre = - pip install -e {toxinidir}/opentelemetry-api - pip install -e {toxinidir}/opentelemetry-sdk - pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests - pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger - pip install -e {toxinidir}/ext/opentelemetry-ext-pymongo - pip install -e {toxinidir}/ext/opentelemetry-ext-testutil - pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi - pip install -e {toxinidir}/ext/opentelemetry-ext-flask[test] - pip install -e {toxinidir}/examples/opentelemetry-example-app - pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim + python scripts/eachdist.py install --editable commands = -; Prefer putting everything in one pylint command to profit from duplication -; warnings. - black --check --diff . - pylint opentelemetry-api/src/opentelemetry \ - opentelemetry-api/tests/ \ - opentelemetry-sdk/src/opentelemetry \ - opentelemetry-sdk/tests/ \ - ext/opentelemetry-ext-http-requests/src/ \ - ext/opentelemetry-ext-http-requests/tests/ \ - ext/opentelemetry-ext-jaeger/src/opentelemetry \ - ext/opentelemetry-ext-jaeger/tests/ \ - ext/opentelemetry-ext-opentracing-shim/src/ \ - ext/opentelemetry-ext-opentracing-shim/tests/ \ - ext/opentelemetry-ext-pymongo/src/opentelemetry \ - ext/opentelemetry-ext-pymongo/tests/ \ - ext/opentelemetry-ext-testutil/src/opentelemetry \ - ext/opentelemetry-ext-wsgi/src/ \ - ext/opentelemetry-ext-wsgi/tests/ \ - ext/opentelemetry-ext-flask/src/ \ - ext/opentelemetry-ext-flask/tests/ \ - examples/opentelemetry-example-app/src/opentelemetry_example_app/ \ - examples/opentelemetry-example-app/tests/ \ - examples/basic_tracer/ \ - examples/http/ - flake8 . - isort --check-only --diff --recursive . + python scripts/eachdist.py lint --check-only [testenv:docs] deps = - sphinx~=2.1 - sphinx-rtd-theme~=0.4 - sphinx-autodoc-typehints~=1.10.2 + -c dev-requirements.txt + sphinx + sphinx-rtd-theme + sphinx-autodoc-typehints opentracing~=2.2.0 Deprecated>=1.2.6 @@ -172,11 +133,11 @@ deps = requests~=2.7 commands_pre = - pip install -e {toxinidir}/opentelemetry-api - pip install -e {toxinidir}/opentelemetry-sdk - pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests - pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi - pip install -e {toxinidir}/ext/opentelemetry-ext-flask + pip install -e {toxinidir}/opentelemetry-api \ + -e {toxinidir}/opentelemetry-sdk \ + -e {toxinidir}/ext/opentelemetry-ext-http-requests \ + -e {toxinidir}/ext/opentelemetry-ext-wsgi \ + -e {toxinidir}/ext/opentelemetry-ext-flask commands = {toxinidir}/scripts/tracecontext-integration-test.sh From 43c7ccd5ce04fb54df40b8eb6e82196cd814e7b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 11 Dec 2019 18:55:54 +0100 Subject: [PATCH 0158/1517] Work around build failures (#330) --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 80123a5ead..520d6ee394 100644 --- a/tox.ini +++ b/tox.ini @@ -131,6 +131,8 @@ deps = # needed for example trace integration flask~=1.1 requests~=2.7 + # Pinned due to https://github.com/open-telemetry/opentelemetry-python/issues/329 + multidict==4.6.1 commands_pre = pip install -e {toxinidir}/opentelemetry-api \ From cb0169c1680976f65d7f4c975cf5d3c729e3abb9 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 11 Dec 2019 15:13:27 -0600 Subject: [PATCH 0159/1517] Set status for ended spans (#297) Fixes #292 --- .../src/opentelemetry/trace/__init__.py | 13 +++++- .../src/opentelemetry/trace/status.py | 4 +- .../src/opentelemetry/sdk/trace/__init__.py | 43 +++++++++++++++++-- opentelemetry-sdk/tests/trace/test_trace.py | 26 +++++------ 4 files changed, 65 insertions(+), 21 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 27361b9a43..8452d93b76 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -122,10 +122,12 @@ class SpanKind(enum.Enum): https://github.com/open-telemetry/opentelemetry-specification/pull/226. """ - #: Default value. Indicates that the span is used internally in the application. + #: Default value. Indicates that the span is used internally in the + # application. INTERNAL = 0 - #: Indicates that the span describes an operation that handles a remote request. + #: Indicates that the span describes an operation that handles a remote + # request. SERVER = 1 #: Indicates that the span describes a request to some remote service. @@ -228,6 +230,7 @@ def __exit__( exc_tb: typing.Optional[python_types.TracebackType], ) -> None: """Ends context manager and calls `end` on the `Span`.""" + self.end() @@ -396,6 +399,7 @@ def start_span( attributes: typing.Optional[types.Attributes] = None, links: typing.Sequence[Link] = (), start_time: typing.Optional[int] = None, + set_status_on_exception: bool = True, ) -> "Span": """Starts a span. @@ -427,6 +431,11 @@ def start_span( attributes: The span's attributes. links: Links span to other spans start_time: Sets the start time of a span + set_status_on_exception: Only relevant if the returned span is used + in a with/context manager. Defines wether the span status will + be automatically set to UNKNOWN when an uncaught exception is + raised in the span with block. The span status won't be set by + this mechanism if it was previousy set manually. Returns: The newly-created span. diff --git a/opentelemetry-api/src/opentelemetry/trace/status.py b/opentelemetry-api/src/opentelemetry/trace/status.py index 4fc50b33e5..0abe9747ad 100644 --- a/opentelemetry-api/src/opentelemetry/trace/status.py +++ b/opentelemetry-api/src/opentelemetry/trace/status.py @@ -163,14 +163,14 @@ class Status: def __init__( self, - canonical_code: "StatusCanonicalCode" = StatusCanonicalCode.OK, + canonical_code: StatusCanonicalCode = StatusCanonicalCode.OK, description: typing.Optional[str] = None, ): self._canonical_code = canonical_code self._description = description @property - def canonical_code(self) -> "StatusCanonicalCode": + def canonical_code(self) -> StatusCanonicalCode: """Represents the canonical status code of a finished Span.""" return self._canonical_code diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 5967960ba3..70f7d3f2b0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -18,13 +18,15 @@ import random import threading from contextlib import contextmanager -from typing import Iterator, Optional, Sequence, Tuple +from types import TracebackType +from typing import Iterator, Optional, Sequence, Tuple, Type from opentelemetry import trace as trace_api from opentelemetry.context import Context from opentelemetry.sdk import util from opentelemetry.sdk.util import BoundedDict, BoundedList from opentelemetry.trace import sampling +from opentelemetry.trace.status import Status, StatusCanonicalCode from opentelemetry.util import time_ns, types logger = logging.getLogger(__name__) @@ -134,6 +136,7 @@ def __init__( links: Sequence[trace_api.Link] = (), kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, span_processor: SpanProcessor = SpanProcessor(), + set_status_on_exception: bool = True, ) -> None: self.name = name @@ -143,9 +146,10 @@ def __init__( self.trace_config = trace_config self.resource = resource self.kind = kind + self._set_status_on_exception = set_status_on_exception self.span_processor = span_processor - self.status = trace_api.Status() + self.status = None self._lock = threading.Lock() if attributes is None: @@ -174,7 +178,10 @@ def __repr__(self): ) def __str__(self): - return '{}(name="{}", context={}, kind={}, parent={}, start_time={}, end_time={})'.format( + return ( + '{}(name="{}", context={}, kind={}, ' + "parent={}, start_time={}, end_time={})" + ).format( type(self).__name__, self.name, self.context, @@ -254,6 +261,9 @@ def end(self, end_time: Optional[int] = None) -> None: logger.warning("Calling end() on an ended span.") return + if self.status is None: + self.set_status(Status(canonical_code=StatusCanonicalCode.OK)) + self.span_processor.on_end(self) def update_name(self, name: str) -> None: @@ -275,6 +285,29 @@ def set_status(self, status: trace_api.Status) -> None: return self.status = status + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + """Ends context manager and calls `end` on the `Span`.""" + + if ( + self.status is None + and self._set_status_on_exception + and exc_val is not None + ): + + self.set_status( + Status( + canonical_code=StatusCanonicalCode.UNKNOWN, + description="{}: {}".format(exc_type.__name__, exc_val), + ) + ) + + super().__exit__(exc_type, exc_val, exc_tb) + def generate_span_id() -> int: """Get a new random span ID. @@ -334,7 +367,7 @@ def start_as_current_span( span = self.start_span(name, parent, kind, attributes, links) return self.use_span(span, end_on_exit=True) - def start_span( + def start_span( # pylint: disable=too-many-locals self, name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, @@ -342,6 +375,7 @@ def start_span( attributes: Optional[types.Attributes] = None, links: Sequence[trace_api.Link] = (), start_time: Optional[int] = None, + set_status_on_exception: bool = True, ) -> "Span": """See `opentelemetry.trace.Tracer.start_span`.""" @@ -401,6 +435,7 @@ def start_span( span_processor=self._active_span_processor, kind=kind, links=links, + set_status_on_exception=set_status_on_exception, ) span.start(start_time=start_time) else: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index e8144c9373..9ec68feeb0 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -20,6 +20,7 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import trace from opentelemetry.trace import sampling +from opentelemetry.trace.status import StatusCanonicalCode from opentelemetry.util import time_ns @@ -455,12 +456,7 @@ def test_start_span(self): span.start() self.assertEqual(start_time, span.start_time) - # default status - self.assertTrue(span.status.is_ok) - self.assertIs( - span.status.canonical_code, trace_api.status.StatusCanonicalCode.OK - ) - self.assertIs(span.status.description, None) + self.assertIs(span.status, None) # status new_status = trace_api.status.Status( @@ -515,13 +511,17 @@ def test_ended_span(self): "Test description", ) root.set_status(new_status) - # default status - self.assertTrue(root.status.is_ok) - self.assertEqual( - root.status.canonical_code, - trace_api.status.StatusCanonicalCode.OK, - ) - self.assertIs(root.status.description, None) + self.assertIs(root.status, None) + + def test_error_status(self): + try: + with trace.Tracer("test_error_status").start_span("root") as root: + raise Exception("unknown") + except Exception: # pylint: disable=broad-except + pass + + self.assertIs(root.status.canonical_code, StatusCanonicalCode.UNKNOWN) + self.assertEqual(root.status.description, "Exception: unknown") def span_event_start_fmt(span_processor_name, span_name): From 27b08973ad3629a7c688e4d453d157da8c9675c9 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Wed, 11 Dec 2019 14:36:59 -0800 Subject: [PATCH 0160/1517] Update README post-v3 release (#332) --- README.md | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 793d46d588..a6f822eca1 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,9 @@ OpenTelemetry Python is under active development. The library is not yet _generally available_, and releases aren't guaranteed to conform to a specific version of the specification. Future releases will not -attempt to maintain backwards compatibility with previous releases. +attempt to maintain backwards compatibility with previous releases. Each alpha +release includes significant changes to the API and SDK packages, making them +incompatible with each other. The [v0.1 alpha release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.1.0) @@ -159,6 +161,14 @@ includes: - Jaeger Trace Exporter - Trace Sampling +The [v0.3 alpha +release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.3.0) +includes: + +- Metrics Instruments and Labels +- Flask Integration +- PyMongo Integration + See the [project milestones](https://github.com/open-telemetry/opentelemetry-python/milestones) for details on upcoming releases. The dates and features described here are @@ -166,16 +176,13 @@ estimates, and subject to change. Future releases targets include: -| Component | Version | Target Date | -| ----------------------------------- | ---------- | --------------- | -| Zipkin Trace Exporter | Alpha v0.3 | December 6 2019 | -| W3C Correlation Context Propagation | Alpha v0.3 | December 6 2019 | -| Support for Tags/Baggage | Alpha v0.3 | December 6 2019 | -| Metrics Aggregation | Alpha v0.3 | December 6 2019 | -| gRPC Integrations | Alpha v0.3 | December 6 2019 | -| Prometheus Metrics Exporter | Alpha v0.3 | December 6 2019 | - -| Component | Version | Target Date | -| ---------------------- | ---------- | ---------------- | -| OpenCensus Bridge | Alpha v0.4 | December 31 2019 | -| Metrics SDK (Complete) | Alpha v0.4 | December 31 2019 | +| Component | Version | Target Date | +| ----------------------------------- | ---------- | ---------------- | +| Zipkin Trace Exporter | Alpha v0.4 | December 31 2019 | +| W3C Correlation Context Propagation | Alpha v0.4 | December 31 2019 | +| Support for Tags/Baggage | Alpha v0.4 | December 31 2019 | +| Metrics Aggregation | Alpha v0.4 | December 31 2019 | +| gRPC Integrations | Alpha v0.4 | December 31 2019 | +| Prometheus Metrics Exporter | Alpha v0.4 | December 31 2019 | +| OpenCensus Bridge | Alpha v0.4 | December 31 2019 | +| Metrics SDK (Complete) | Alpha v0.4 | December 31 2019 | From d081d6d5944a6f6f65aae976b288a97455c608a8 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Wed, 11 Dec 2019 14:47:28 -0800 Subject: [PATCH 0161/1517] Pick up changelogs from #328, update dev version (#331) --- examples/opentelemetry-example-app/setup.py | 2 +- ext/opentelemetry-ext-flask/CHANGELOG.md | 9 +++++++++ .../src/opentelemetry/ext/flask/version.py | 2 +- ext/opentelemetry-ext-http-requests/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-http-requests/setup.cfg | 2 +- .../src/opentelemetry/ext/http_requests/version.py | 2 +- ext/opentelemetry-ext-jaeger/CHANGELOG.md | 4 ++++ .../src/opentelemetry/ext/jaeger/version.py | 2 +- ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md | 7 ++++++- .../src/opentelemetry/ext/opentracing_shim/version.py | 2 +- ext/opentelemetry-ext-pymongo/CHANGELOG.md | 9 +++++++++ ext/opentelemetry-ext-pymongo/setup.cfg | 2 +- .../src/opentelemetry/ext/pymongo/version.py | 2 +- .../src/opentelemetry/ext/testutil/version.py | 2 +- ext/opentelemetry-ext-wsgi/CHANGELOG.md | 8 ++++++++ .../src/opentelemetry/ext/wsgi/version.py | 2 +- opentelemetry-api/CHANGELOG.md | 9 +++++++++ opentelemetry-api/src/opentelemetry/util/version.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 10 ++++++++++ opentelemetry-sdk/setup.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- 21 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 ext/opentelemetry-ext-flask/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-pymongo/CHANGELOG.md diff --git a/examples/opentelemetry-example-app/setup.py b/examples/opentelemetry-example-app/setup.py index 0e577cd264..ae614aee33 100644 --- a/examples/opentelemetry-example-app/setup.py +++ b/examples/opentelemetry-example-app/setup.py @@ -16,7 +16,7 @@ setuptools.setup( name="opentelemetry-example-app", - version="0.3.dev0", + version="0.4.dev0", author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ diff --git a/ext/opentelemetry-ext-flask/CHANGELOG.md b/ext/opentelemetry-ext-flask/CHANGELOG.md new file mode 100644 index 0000000000..d4df57eefc --- /dev/null +++ b/ext/opentelemetry-ext-flask/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## Unreleased + +## 0.3a0 + +Released 2019-12-11 + +- Initial release diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py index ed56d30324..2f792fff80 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3dev0" +__version__ = "0.4.dev0" diff --git a/ext/opentelemetry-ext-http-requests/CHANGELOG.md b/ext/opentelemetry-ext-http-requests/CHANGELOG.md index ac7fc896a3..0e9be6e475 100644 --- a/ext/opentelemetry-ext-http-requests/CHANGELOG.md +++ b/ext/opentelemetry-ext-http-requests/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.3a0 + +Released 2019-10-29 + ## 0.2a0 Released 2019-10-29 diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg index ceb5c13775..bb3f50c480 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.3.dev0 + opentelemetry-api >= 0.4.dev0 requests ~= 2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py index 93ef792d05..2f792fff80 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.dev0" +__version__ = "0.4.dev0" diff --git a/ext/opentelemetry-ext-jaeger/CHANGELOG.md b/ext/opentelemetry-ext-jaeger/CHANGELOG.md index f059e90f5a..05ffbffe0f 100644 --- a/ext/opentelemetry-ext-jaeger/CHANGELOG.md +++ b/ext/opentelemetry-ext-jaeger/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.3a0 + +Released 2019-12-11 + ## 0.2a0 Released 2019-10-29 diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index 1f3a27dc85..39a7c8a016 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.dev0" +__version__ = "0.4.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md b/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md index 8fe7c21469..af8ddc4ae8 100644 --- a/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md +++ b/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md @@ -2,7 +2,12 @@ ## Unreleased -- Implement extract and inject support for HTTP_HEADERS and TEXT_MAP formats. +## 0.3a0 + +Released 2019-12-11 + +- Implement extract and inject support for HTTP_HEADERS and TEXT_MAP formats + ([#256](https://github.com/open-telemetry/opentelemetry-python/pull/256)) ## 0.2a0 diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index 93ef792d05..2f792fff80 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.dev0" +__version__ = "0.4.dev0" diff --git a/ext/opentelemetry-ext-pymongo/CHANGELOG.md b/ext/opentelemetry-ext-pymongo/CHANGELOG.md new file mode 100644 index 0000000000..d4df57eefc --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## Unreleased + +## 0.3a0 + +Released 2019-12-11 + +- Initial release diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index f9362c75b3..ecc0ce3b77 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.3.dev0 + opentelemetry-api >= 0.4.dev0 pymongo ~= 3.1 [options.packages.find] diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index 93ef792d05..2f792fff80 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.dev0" +__version__ = "0.4.dev0" diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py index 70ddecedf8..116884f355 100644 --- a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py +++ b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py @@ -1 +1 @@ -__version__ = "0.3dev0" +__version__ = "0.4.dev0" diff --git a/ext/opentelemetry-ext-wsgi/CHANGELOG.md b/ext/opentelemetry-ext-wsgi/CHANGELOG.md index ac7fc896a3..62d8a5baf0 100644 --- a/ext/opentelemetry-ext-wsgi/CHANGELOG.md +++ b/ext/opentelemetry-ext-wsgi/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +## 0.3a0 + +Released 2019-12-11 + +- Support new semantic conventions + ([#299](https://github.com/open-telemetry/opentelemetry-python/pull/299)) +- Updates for core library changes + ## 0.2a0 Released 2019-10-29 diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index 93ef792d05..2f792fff80 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.dev0" +__version__ = "0.4.dev0" diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index ff78c0bf1e..dc5ccc3ace 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +## 0.3a0 + +Released 2019-12-11 + +- Multiple tracing API changes +- Multiple metrics API changes +- Remove option to create unstarted spans from API + ([#290](https://github.com/open-telemetry/opentelemetry-python/pull/290)) + ## 0.2a0 Released 2019-10-29 diff --git a/opentelemetry-api/src/opentelemetry/util/version.py b/opentelemetry-api/src/opentelemetry/util/version.py index 93ef792d05..2f792fff80 100644 --- a/opentelemetry-api/src/opentelemetry/util/version.py +++ b/opentelemetry-api/src/opentelemetry/util/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.dev0" +__version__ = "0.4.dev0" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 8ddb3e60ef..d981045858 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,16 @@ ## Unreleased +## 0.3a0 + +Released 2019-12-11 + +- Multiple tracing SDK changes +- Multiple metrics SDK changes +- Add metrics exporters + ([#192](https://github.com/open-telemetry/opentelemetry-python/pull/192)) +- Multiple bugfixes and improvements + ## 0.2a0 Released 2019-10-29 diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index f93b4b0f10..cbfb0f075d 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -44,7 +44,7 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["opentelemetry-api==0.3.dev0"], + install_requires=["opentelemetry-api==0.4.dev0"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 93ef792d05..2f792fff80 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.dev0" +__version__ = "0.4.dev0" From e86760fbc5fb5ac3f8b4747ce6a804d0f565f6ce Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 12 Dec 2019 10:18:15 -0800 Subject: [PATCH 0162/1517] Adding docs for sdk and integrations (#319) Adding documentation that was previously missing for multiple sdks and integrations. Signed-off-by: Alex Boten --- docs/conf.py | 20 +++++++-- docs/index.rst | 32 ++++++++++---- docs/opentelemetry.ext.flask.rst | 10 +++++ docs/opentelemetry.ext.http_requests.rst | 10 +++++ ...telemetry.ext.jaeger.gen.jaeger.ttypes.rst | 11 +++++ docs/opentelemetry.ext.jaeger.rst | 19 +++++++++ docs/opentelemetry.ext.pymongo.rst | 10 +++++ docs/opentelemetry.ext.wsgi.rst | 10 +++++ docs/opentelemetry.sdk.context.rst | 7 ++++ docs/opentelemetry.sdk.metrics.rst | 7 ++++ docs/opentelemetry.sdk.trace.export.rst | 7 ++++ docs/opentelemetry.sdk.trace.rst | 14 +++++++ docs/opentelemetry.trace.rst | 3 +- docs/opentelemetry.trace.sampling.rst | 7 ++++ .../ext/opentracing_shim/__init__.py | 42 ++++++++++--------- .../src/opentelemetry/ext/pymongo/__init__.py | 3 ++ .../src/opentelemetry/trace/sampling.py | 4 +- .../src/opentelemetry/sdk/__init__.py | 4 ++ .../src/opentelemetry/sdk/metrics/__init__.py | 2 +- .../src/opentelemetry/sdk/trace/__init__.py | 21 +++++----- .../sdk/trace/export/__init__.py | 6 +-- tox.ini | 3 ++ 22 files changed, 206 insertions(+), 46 deletions(-) create mode 100644 docs/opentelemetry.ext.flask.rst create mode 100644 docs/opentelemetry.ext.http_requests.rst create mode 100644 docs/opentelemetry.ext.jaeger.gen.jaeger.ttypes.rst create mode 100644 docs/opentelemetry.ext.jaeger.rst create mode 100644 docs/opentelemetry.ext.pymongo.rst create mode 100644 docs/opentelemetry.ext.wsgi.rst create mode 100644 docs/opentelemetry.sdk.context.rst create mode 100644 docs/opentelemetry.sdk.metrics.rst create mode 100644 docs/opentelemetry.sdk.trace.export.rst create mode 100644 docs/opentelemetry.sdk.trace.rst create mode 100644 docs/opentelemetry.trace.sampling.rst diff --git a/docs/conf.py b/docs/conf.py index 538472cf05..aa0b4096bc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,12 +12,21 @@ import os import sys +from os import listdir +from os.path import isdir, join -sys.path[:0] = [ +source_dirs = [ os.path.abspath("../opentelemetry-api/src/"), - os.path.abspath("../ext/opentelemetry-ext-opentracing-shim/src/"), + os.path.abspath("../opentelemetry-sdk/src/"), ] +ext = "../ext" +ext_dirs = [ + os.path.abspath("/".join(["../ext", f, "src"])) + for f in listdir(ext) + if isdir(join(ext, f)) +] +sys.path[:0] = source_dirs + ext_dirs # -- Project information ----------------------------------------------------- @@ -64,7 +73,12 @@ # Sphinx does not recognize generic type TypeVars # Container supposedly were fixed, but does not work # https://github.com/sphinx-doc/sphinx/pull/3744 -nitpick_ignore = [("py:class", "ValueT"), ("py:class", "typing.Tuple")] +nitpick_ignore = [ + ("py:class", "ValueT"), + ("py:class", "MetricT"), + ("py:class", "typing.Tuple"), + ("py:class", "pymongo.monitoring.CommandListener"), +] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/index.rst b/docs/index.rst index b98ef55316..c597d4a681 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,24 +3,42 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -OpenTelemetry API -================= +OpenTelemetry +============= -Welcome to OpenTelemetry's API documentation! +Welcome to OpenTelemetry's documentation! -This documentation describes the ``opentelemetry-api`` package, which provides -abstract types for OpenTelemetry implementations. +This documentation describes the ``opentelemetry-api``, ``opentelemetry-sdk`` +and integration packages. .. toctree:: :maxdepth: 1 - :caption: Contents: + :caption: OpenTelemetry API: opentelemetry.context opentelemetry.metrics opentelemetry.trace opentelemetry.util.loader - opentelemetry.ext.opentracing_shim +.. toctree:: + :maxdepth: 1 + :caption: OpenTelemetry SDK: + + opentelemetry.sdk.context + opentelemetry.sdk.metrics + opentelemetry.sdk.trace + + +.. toctree:: + :maxdepth: 1 + :caption: OpenTelemetry Integrations: + + opentelemetry.ext.flask + opentelemetry.ext.http_requests + opentelemetry.ext.jaeger + opentelemetry.ext.opentracing_shim + opentelemetry.ext.pymongo + opentelemetry.ext.wsgi Indices and tables ================== diff --git a/docs/opentelemetry.ext.flask.rst b/docs/opentelemetry.ext.flask.rst new file mode 100644 index 0000000000..87ad93b867 --- /dev/null +++ b/docs/opentelemetry.ext.flask.rst @@ -0,0 +1,10 @@ +opentelemetry.ext.flask package +========================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.flask + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.ext.http_requests.rst b/docs/opentelemetry.ext.http_requests.rst new file mode 100644 index 0000000000..fbc9bd2a63 --- /dev/null +++ b/docs/opentelemetry.ext.http_requests.rst @@ -0,0 +1,10 @@ +opentelemetry.ext.http_requests package +========================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.http_requests + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.ext.jaeger.gen.jaeger.ttypes.rst b/docs/opentelemetry.ext.jaeger.gen.jaeger.ttypes.rst new file mode 100644 index 0000000000..8b69e013b4 --- /dev/null +++ b/docs/opentelemetry.ext.jaeger.gen.jaeger.ttypes.rst @@ -0,0 +1,11 @@ +opentelemetry.ext.jaeger.gen.jaeger.ttypes +========================================== + + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.jaeger.gen.jaeger.ttypes + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.ext.jaeger.rst b/docs/opentelemetry.ext.jaeger.rst new file mode 100644 index 0000000000..74a7fb399e --- /dev/null +++ b/docs/opentelemetry.ext.jaeger.rst @@ -0,0 +1,19 @@ +opentelemetry.ext.jaeger package +========================================== + + +Submodules +---------- + +.. toctree:: + + opentelemetry.ext.jaeger.gen.jaeger.ttypes + + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.jaeger + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.ext.pymongo.rst b/docs/opentelemetry.ext.pymongo.rst new file mode 100644 index 0000000000..e5c262ac16 --- /dev/null +++ b/docs/opentelemetry.ext.pymongo.rst @@ -0,0 +1,10 @@ +opentelemetry.ext.pymongo package +========================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.pymongo + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.ext.wsgi.rst b/docs/opentelemetry.ext.wsgi.rst new file mode 100644 index 0000000000..506bc1efe4 --- /dev/null +++ b/docs/opentelemetry.ext.wsgi.rst @@ -0,0 +1,10 @@ +opentelemetry.ext.wsgi package +========================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.wsgi + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.sdk.context.rst b/docs/opentelemetry.sdk.context.rst new file mode 100644 index 0000000000..12b15b1ed8 --- /dev/null +++ b/docs/opentelemetry.sdk.context.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.context +========================================== + +.. automodule:: opentelemetry.sdk.context + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.sdk.metrics.rst b/docs/opentelemetry.sdk.metrics.rst new file mode 100644 index 0000000000..6d646c3b15 --- /dev/null +++ b/docs/opentelemetry.sdk.metrics.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.metrics package +========================================== + +.. automodule:: opentelemetry.sdk.metrics + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.sdk.trace.export.rst b/docs/opentelemetry.sdk.trace.export.rst new file mode 100644 index 0000000000..b876f366fd --- /dev/null +++ b/docs/opentelemetry.sdk.trace.export.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.trace.export +========================================== + +.. automodule:: opentelemetry.sdk.trace.export + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/opentelemetry.sdk.trace.rst b/docs/opentelemetry.sdk.trace.rst new file mode 100644 index 0000000000..7bb3569fe6 --- /dev/null +++ b/docs/opentelemetry.sdk.trace.rst @@ -0,0 +1,14 @@ +opentelemetry.sdk.trace package +========================================== + +Submodules +---------- + +.. toctree:: + + opentelemetry.sdk.trace.export + +.. automodule:: opentelemetry.sdk.trace + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.trace.rst b/docs/opentelemetry.trace.rst index a57b5dcbff..bfef5839bf 100644 --- a/docs/opentelemetry.trace.rst +++ b/docs/opentelemetry.trace.rst @@ -6,9 +6,10 @@ Submodules .. toctree:: + opentelemetry.trace.sampling opentelemetry.trace.status Module contents --------------- -.. automodule:: opentelemetry.trace \ No newline at end of file +.. automodule:: opentelemetry.trace diff --git a/docs/opentelemetry.trace.sampling.rst b/docs/opentelemetry.trace.sampling.rst new file mode 100644 index 0000000000..09a77b166e --- /dev/null +++ b/docs/opentelemetry.trace.sampling.rst @@ -0,0 +1,7 @@ +opentelemetry.trace.sampling +============================ + +.. automodule:: opentelemetry.trace.sampling + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index a9e74dbc58..0f2328a651 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -303,14 +303,15 @@ class ScopeShim(opentracing.Scope): It is necessary to have both ways for constructing `ScopeShim` objects because in some cases we need to create the object from an OpenTelemetry - `Span` context manager (as returned by + `opentelemetry.trace.Span` context manager (as returned by :meth:`opentelemetry.trace.Tracer.use_span`), in which case our only way of - retrieving a `Span` object is by calling the ``__enter__()`` method on the - context manager, which makes the span active in the OpenTelemetry tracer; - whereas in other cases we need to accept a `SpanShim` object and wrap it in - a `ScopeShim`. The former is used mainly when the instrumentation code - retrieves the currently-active span using `ScopeManagerShim.active`. The - latter is mainly used when the instrumentation code activates a span using + retrieving a `opentelemetry.trace.Span` object is by calling the + ``__enter__()`` method on the context manager, which makes the span active + in the OpenTelemetry tracer; whereas in other cases we need to accept a + `SpanShim` object and wrap it in a `ScopeShim`. The former is used mainly + when the instrumentation code retrieves the currently-active span using + `ScopeManagerShim.active`. The latter is mainly used when the + instrumentation code activates a span using :meth:`ScopeManagerShim.activate`. Args: @@ -318,12 +319,14 @@ class ScopeShim(opentracing.Scope): :class:`ScopeShim`. span: The :class:`SpanShim` this :class:`ScopeShim` controls. span_cm(:class:`contextlib.AbstractContextManager`, optional): A - Python context manager which yields an OpenTelemetry `Span` from - its ``__enter__()`` method. Used by :meth:`from_context_manager` to - store the context manager as an attribute so that it can later be - closed by calling its ``__exit__()`` method. Defaults to `None`. - - TODO: Is :class:`contextlib.AbstractContextManager` the correct type for *span_cm*? + Python context manager which yields an OpenTelemetry + `opentelemetry.trace.Span` from its ``__enter__()`` method. Used + by :meth:`from_context_manager` to store the context manager as + an attribute so that it can later be closed by calling its + ``__exit__()`` method. Defaults to `None`. + + TODO: Is :class:`contextlib.AbstractContextManager` the correct + type for *span_cm*? """ def __init__(self, manager, span, span_cm=None): @@ -334,12 +337,13 @@ def __init__(self, manager, span, span_cm=None): # need to get rid of `manager.tracer` for this. @classmethod def from_context_manager(cls, manager, span_cm): - """Constructs a :class:`ScopeShim` from an OpenTelemetry `Span` context + """Constructs a :class:`ScopeShim` from an OpenTelemetry + `opentelemetry.trace.Span` context manager. - The method extracts a `Span` object from the context manager by calling - the context manager's ``__enter__()`` method. This causes the span to - start in the OpenTelemetry tracer. + The method extracts a `opentelemetry.trace.Span` object from the + context manager by calling the context manager's ``__enter__()`` + method. This causes the span to start in the OpenTelemetry tracer. Example usage:: @@ -353,7 +357,7 @@ def from_context_manager(cls, manager, span_cm): Args: manager: The :class:`ScopeManagerShim` that created this :class:`ScopeShim`. - span_cm: An OpenTelemetry `Span` context manager as returned by + span_cm: A context manager as returned by :meth:`opentelemetry.trace.Tracer.use_span`. """ @@ -396,7 +400,7 @@ def close(self): class ScopeManagerShim(opentracing.ScopeManager): """Implements :class:`opentracing.ScopeManager` by setting and getting the - active `Span` in the OpenTelemetry tracer. + active `opentelemetry.trace.Span` in the OpenTelemetry tracer. This class keeps a reference to a :class:`TracerShim` as an attribute. This reference is used to communicate with the OpenTelemetry tracer. It is diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index fa1cc1583e..8c49892e0f 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -42,6 +42,7 @@ def __init__(self, tracer): self._span_dict = {} def started(self, event: monitoring.CommandStartedEvent): + """ Method to handle a pymongo CommandStartedEvent """ command = event.command.get(event.command_name, "") name = DATABASE_TYPE + "." + event.command_name statement = event.command_name @@ -77,6 +78,7 @@ def started(self, event: monitoring.CommandStartedEvent): self._remove_span(event) def succeeded(self, event: monitoring.CommandSucceededEvent): + """ Method to handle a pymongo CommandSucceededEvent """ span = self._get_span(event) if span is not None: span.set_attribute( @@ -87,6 +89,7 @@ def succeeded(self, event: monitoring.CommandSucceededEvent): self._remove_span(event) def failed(self, event: monitoring.CommandFailedEvent): + """ Method to handle a pymongo CommandFailedEvent """ span = self._get_span(event) if span is not None: span.set_attribute( diff --git a/opentelemetry-api/src/opentelemetry/trace/sampling.py b/opentelemetry-api/src/opentelemetry/trace/sampling.py index e0f5f17c75..2425c27c07 100644 --- a/opentelemetry-api/src/opentelemetry/trace/sampling.py +++ b/opentelemetry-api/src/opentelemetry/trace/sampling.py @@ -24,8 +24,8 @@ class Decision: """A sampling decision as applied to a newly-created Span. Args: - sampled: Whether the `Span` should be sampled. - attributes: Attributes to add to the `Span`. + sampled: Whether the `opentelemetry.trace.Span` should be sampled. + attributes: Attributes to add to the `opentelemetry.trace.Span`. """ def __repr__(self) -> str: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py index 0f3bff4571..7b87f0d8f6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py @@ -12,6 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +The OpenTelemetry SDK package is an implementation of the OpenTelemetry +API +""" from . import metrics, trace, util __all__ = ["metrics", "trace", "util"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 753c35a32e..f0c3e0e6d3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -24,7 +24,7 @@ # pylint: disable=redefined-outer-name class LabelSet(metrics_api.LabelSet): - """See `opentelemetry.metrics.LabelSet.""" + """See `opentelemetry.metrics.LabelSet`.""" def __init__(self, labels: Dict[str, str] = None): self.labels = labels diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 70f7d3f2b0..140626aa94 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -25,7 +25,7 @@ from opentelemetry.context import Context from opentelemetry.sdk import util from opentelemetry.sdk.util import BoundedDict, BoundedList -from opentelemetry.trace import sampling +from opentelemetry.trace import SpanContext, sampling from opentelemetry.trace.status import Status, StatusCanonicalCode from opentelemetry.util import time_ns, types @@ -37,36 +37,36 @@ class SpanProcessor: - """Interface which allows hooks for SDK's `Span`s start and end method + """Interface which allows hooks for SDK's `Span` start and end method invocations. Span processors can be registered directly using - :func:`~Tracer:add_span_processor` and they are invoked in the same order + :func:`Tracer.add_span_processor` and they are invoked in the same order as they were registered. """ def on_start(self, span: "Span") -> None: - """Called when a :class:`Span` is started. + """Called when a :class:`opentelemetry.trace.Span` is started. This method is called synchronously on the thread that starts the span, therefore it should not block or throw an exception. Args: - span: The :class:`Span` that just started. + span: The :class:`opentelemetry.trace.Span` that just started. """ def on_end(self, span: "Span") -> None: - """Called when a :class:`Span` is ended. + """Called when a :class:`opentelemetry.trace.Span` is ended. This method is called synchronously on the thread that ends the span, therefore it should not block or throw an exception. Args: - span: The :class:`Span` that just ended. + span: The :class:`opentelemetry.trace.Span` that just ended. """ def shutdown(self) -> None: - """Called when a :class:`Tracer` is shutdown.""" + """Called when a :class:`opentelemetry.sdk.trace.Tracer` is shutdown.""" class MultiSpanProcessor(SpanProcessor): @@ -101,7 +101,8 @@ def shutdown(self) -> None: class Span(trace_api.Span): """See `opentelemetry.trace.Span`. - Users should create `Span`s via the `Tracer` instead of this constructor. + Users should create `Span` objects via the `Tracer` instead of this + constructor. Args: name: The name of the operation this span represents @@ -376,7 +377,7 @@ def start_span( # pylint: disable=too-many-locals links: Sequence[trace_api.Link] = (), start_time: Optional[int] = None, set_status_on_exception: bool = True, - ) -> "Span": + ) -> trace_api.Span: """See `opentelemetry.trace.Tracer.start_span`.""" if parent is Tracer.CURRENT_SPAN: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 442b2b2bac..36459c5b73 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -38,15 +38,15 @@ class SpanExporter: Interface to be implemented by services that want to export recorded in its own format. - To export data this MUST be registered to the :class`..Tracer` using a - `SimpleExportSpanProcessor` or a `BatchSpanProcessor`. + To export data this MUST be registered to the :class`opentelemetry.sdk.trace.Tracer` using a + `SimpleExportSpanProcessor` or a `BatchExportSpanProcessor`. """ def export(self, spans: typing.Sequence[Span]) -> "SpanExportResult": """Exports a batch of telemetry data. Args: - spans: The list of `Span`s to be exported + spans: The list of `opentelemetry.trace.Span` objects to be exported Returns: The result of the export diff --git a/tox.ini b/tox.ini index 520d6ee394..2ded7d1b75 100644 --- a/tox.ini +++ b/tox.ini @@ -117,6 +117,9 @@ deps = sphinx-autodoc-typehints opentracing~=2.2.0 Deprecated>=1.2.6 + thrift>=0.10.0 + pymongo ~= 3.1 + flask~=1.0 changedir = docs From d74c39c71ae7bd3a0afd94879b15e00f493f3adb Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 12 Dec 2019 12:05:06 -0800 Subject: [PATCH 0163/1517] Make @toumorokoshi maintainer in README (#334) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6f822eca1..684680c0fb 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,6 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Carlos Alberto Cortez](https://github.com/carlosalberto), LightStep - [Christian Neumüller](https://github.com/Oberon00), Dynatrace - [Leighton Chen](https://github.com/lzchen), Microsoft -- [Yusuke Tsutsumi](https://github.com/toumorokoshi), Zillow Group *Find more about the approver role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver).* @@ -128,6 +127,7 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t - [Chris Kleinknecht](https://github.com/c24t), Google - [Reiley Yang](https://github.com/reyang), Microsoft +- [Yusuke Tsutsumi](https://github.com/toumorokoshi), Zillow Group *Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).* From baab508ff92b333cf7310ddd46c966808b825102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Sat, 14 Dec 2019 01:36:23 +0100 Subject: [PATCH 0164/1517] Named tracers (#301) Implements the "Tracers" part of #203. --- README.md | 8 +- examples/basic_tracer/tracer.py | 12 +- examples/http/server.py | 10 +- examples/http/tracer_client.py | 10 +- .../flask_example.py | 13 +- .../src/opentelemetry/ext/flask/__init__.py | 3 +- .../ext/http_requests/__init__.py | 5 +- .../tests/test_requests_integration.py | 35 +++-- ext/opentelemetry-ext-jaeger/README.rst | 6 +- .../examples/jaeger_exporter_example.py | 10 +- .../ext/opentracing_shim/__init__.py | 19 +-- .../tests/test_shim.py | 9 +- .../ext/testutil/wsgitestutil.py | 12 +- .../src/opentelemetry/ext/wsgi/__init__.py | 11 +- .../src/opentelemetry/trace/__init__.py | 85 +++++++++---- .../src/opentelemetry/util/loader.py | 21 +-- opentelemetry-api/tests/mypysmoke.py | 4 +- .../tests/test_implementation.py | 3 +- opentelemetry-api/tests/test_loader.py | 52 ++++---- .../src/opentelemetry/sdk/trace/__init__.py | 120 ++++++++++++++---- .../tests/test_implementation.py | 2 +- .../tests/trace/export/test_export.py | 10 +- .../export/test_in_memory_span_exporter.py | 62 +++------ opentelemetry-sdk/tests/trace/test_trace.py | 117 +++++++++++++---- tests/w3c_tracecontext_validation_server.py | 8 +- 25 files changed, 427 insertions(+), 220 deletions(-) diff --git a/README.md b/README.md index 684680c0fb..fcedf05e02 100644 --- a/README.md +++ b/README.md @@ -52,15 +52,15 @@ pip install -e ./ext/opentelemetry-ext-{integration} ```python from opentelemetry import trace from opentelemetry.context import Context -from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace import TracerSource from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor -trace.set_preferred_tracer_implementation(lambda T: Tracer()) -tracer = trace.tracer() -tracer.add_span_processor( +trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +trace.tracer_source().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) +tracer = trace.tracer_source().get_tracer(__name__) with tracer.start_as_current_span('foo'): with tracer.start_as_current_span('bar'): with tracer.start_as_current_span('baz'): diff --git a/examples/basic_tracer/tracer.py b/examples/basic_tracer/tracer.py index 69ee0a1602..4b392fd1ea 100755 --- a/examples/basic_tracer/tracer.py +++ b/examples/basic_tracer/tracer.py @@ -18,7 +18,7 @@ from opentelemetry import trace from opentelemetry.context import Context -from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace import TracerSource from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, ConsoleSpanExporter, @@ -37,13 +37,17 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_implementation(lambda T: Tracer()) -tracer = trace.tracer() +trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + +# We tell OpenTelemetry who it is that is creating spans. In this case, we have +# no real name (no setup.py), so we make one up. If we had a version, we would +# also specify it here. +tracer = trace.tracer_source().get_tracer(__name__) # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) -tracer.add_span_processor(span_processor) +trace.tracer_source().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): diff --git a/examples/http/server.py b/examples/http/server.py index 63973da74d..68e3d952b0 100755 --- a/examples/http/server.py +++ b/examples/http/server.py @@ -22,7 +22,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests from opentelemetry.ext.wsgi import OpenTelemetryMiddleware -from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace import TracerSource from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, ConsoleSpanExporter, @@ -41,17 +41,17 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_implementation(lambda T: Tracer()) -tracer = trace.tracer() +trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +tracer = trace.tracer_source().get_tracer(__name__) # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) -tracer.add_span_processor(span_processor) +trace.tracer_source().add_span_processor(span_processor) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(tracer) +http_requests.enable(trace.tracer_source()) app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) diff --git a/examples/http/tracer_client.py b/examples/http/tracer_client.py index dde25b9bb3..746608db3b 100755 --- a/examples/http/tracer_client.py +++ b/examples/http/tracer_client.py @@ -20,7 +20,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests -from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace import TracerSource from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, ConsoleSpanExporter, @@ -39,15 +39,15 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_implementation(lambda T: Tracer()) -tracer = trace.tracer() +trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +tracer_source = trace.tracer_source() # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) -tracer.add_span_processor(span_processor) +tracer_source.add_span_processor(span_processor) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(tracer) +http_requests.enable(tracer_source) response = requests.get(url="http://127.0.0.1:5000/") diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 85df625efe..ae484dd30e 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -17,13 +17,14 @@ the requests library to perform downstream requests """ import flask +import pkg_resources import requests import opentelemetry.ext.http_requests from opentelemetry import propagators, trace from opentelemetry.ext.flask import instrument_app from opentelemetry.sdk.context.propagation.b3_format import B3Format -from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace import TracerSource def configure_opentelemetry(flask_app: flask.Flask): @@ -45,7 +46,7 @@ def configure_opentelemetry(flask_app: flask.Flask): # the preferred implementation of these objects must be set, # as the opentelemetry-api defines the interface with a no-op # implementation. - trace.set_preferred_tracer_implementation(lambda _: Tracer()) + trace.set_preferred_tracer_source_implementation(lambda _: TracerSource()) # Next, we need to configure how the values that are used by # traces and metrics are propagated (such as what specific headers # carry this value). @@ -56,7 +57,7 @@ def configure_opentelemetry(flask_app: flask.Flask): # Integrations are the glue that binds the OpenTelemetry API # and the frameworks and libraries that are used together, automatically # creating Spans and propagating context as appropriate. - opentelemetry.ext.http_requests.enable(trace.tracer()) + opentelemetry.ext.http_requests.enable(trace.tracer_source()) instrument_app(flask_app) @@ -67,7 +68,11 @@ def configure_opentelemetry(flask_app: flask.Flask): def hello(): # emit a trace that measures how long the # sleep takes - with trace.tracer().start_as_current_span("example-request"): + version = pkg_resources.get_distribution( + "opentelemetry-example-app" + ).version + tracer = trace.tracer_source().get_tracer(__name__, version) + with tracer.start_as_current_span("example-request"): requests.get("http://www.example.com") return "hello" diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index cce038ccb5..ce11b18d63 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -7,6 +7,7 @@ import opentelemetry.ext.wsgi as otel_wsgi from opentelemetry import propagators, trace +from opentelemetry.ext.flask.version import __version__ from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -60,7 +61,7 @@ def _before_flask_request(): otel_wsgi.get_header_from_environ, environ ) - tracer = trace.tracer() + tracer = trace.tracer_source().get_tracer(__name__, __version__) attributes = otel_wsgi.collect_request_attributes(environ) if flask_request.url_rule: diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index f05202c055..4f5a18cf9e 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -24,6 +24,7 @@ from opentelemetry import propagators from opentelemetry.context import Context +from opentelemetry.ext.http_requests.version import __version__ from opentelemetry.trace import SpanKind @@ -32,7 +33,7 @@ # if the SDK/tracer is already using `requests` they may, in theory, bypass our # instrumentation when using `import from`, etc. (currently we only instrument # a instance method so the probability for that is very low). -def enable(tracer): +def enable(tracer_source): """Enables tracing of all requests calls that go through :code:`requests.session.Session.request` (this includes :code:`requests.get`, etc.).""" @@ -47,6 +48,8 @@ def enable(tracer): # Guard against double instrumentation disable() + tracer = tracer_source.get_tracer(__name__, __version__) + wrapped = Session.request @functools.wraps(wrapped) diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index 2a02e1916a..35cf3110f3 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -16,6 +16,7 @@ import unittest from unittest import mock +import pkg_resources import requests import urllib3 @@ -28,7 +29,16 @@ class TestRequestsIntegration(unittest.TestCase): # TODO: Copy & paste from test_wsgi_middleware def setUp(self): self.span_attrs = {} - self.tracer = trace.tracer() + self.tracer_source = trace.TracerSource() + self.tracer = trace.Tracer() + self.get_tracer_patcher = mock.patch.object( + self.tracer_source, + "get_tracer", + autospec=True, + spec_set=True, + return_value=self.tracer, + ) + self.get_tracer = self.get_tracer_patcher.start() self.span_context_manager = mock.MagicMock() self.span = mock.create_autospec(trace.Span, spec_set=True) self.span_context_manager.__enter__.return_value = self.span @@ -45,7 +55,6 @@ def setspanattr(key, value): spec_set=True, return_value=self.span_context_manager, ) - self.start_as_current_span = self.start_span_patcher.start() mocked_response = requests.models.Response() mocked_response.status_code = 200 @@ -57,12 +66,21 @@ def setspanattr(key, value): spec_set=True, return_value=mocked_response, ) + + self.start_as_current_span = self.start_span_patcher.start() self.send = self.send_patcher.start() - opentelemetry.ext.http_requests.enable(self.tracer) + opentelemetry.ext.http_requests.enable(self.tracer_source) + distver = pkg_resources.get_distribution( + "opentelemetry-ext-http-requests" + ).version + self.get_tracer.assert_called_with( + opentelemetry.ext.http_requests.__name__, distver + ) def tearDown(self): opentelemetry.ext.http_requests.disable() + self.get_tracer_patcher.stop() self.send_patcher.stop() self.start_span_patcher.stop() @@ -70,7 +88,7 @@ def test_basic(self): url = "https://www.example.org/foo/bar?x=y#top" requests.get(url=url) self.assertEqual(1, len(self.send.call_args_list)) - self.tracer.start_as_current_span.assert_called_with( + self.tracer.start_as_current_span.assert_called_with( # pylint:disable=no-member "/foo/bar", kind=trace.SpanKind.CLIENT ) self.span_context_manager.__enter__.assert_called_with() @@ -96,11 +114,12 @@ def test_invalid_url(self): with self.assertRaises(exception_type): requests.post(url=url) + call_args = ( + self.tracer.start_as_current_span.call_args # pylint:disable=no-member + ) self.assertTrue( - self.tracer.start_as_current_span.call_args[0][0].startswith( - " bool: INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) +class TracerSource: + # pylint:disable=no-self-use,unused-argument + def get_tracer( + self, + instrumenting_module_name: str, + instrumenting_library_version: str = "", + ) -> "Tracer": + """Returns a `Tracer` for use by the given instrumentation library. + + For any two calls it is undefined whether the same or different + `Tracer` instances are returned, even for different library names. + + This function may return different `Tracer` types (e.g. a no-op tracer + vs. a functional tracer). + + Args: + instrumenting_module_name: The name of the instrumenting module + (usually just ``__name__``). + + This should *not* be the name of the module that is + instrumented but the name of the module doing the instrumentation. + E.g., instead of ``"requests"``, use + ``"opentelemetry.ext.http_requests"``. + + instrumenting_library_version: Optional. The version string of the + instrumenting library. Usually this should be the same as + ``pkg_resources.get_distribution(instrumenting_library_name).version``. + """ + return Tracer() + + class Tracer: """Handles span creation and in-process context propagation. @@ -522,43 +560,46 @@ def use_span( # the following type definition should be replaced with # from opentelemetry.util.loader import ImplementationFactory ImplementationFactory = typing.Callable[ - [typing.Type[Tracer]], typing.Optional[Tracer] + [typing.Type[TracerSource]], typing.Optional[TracerSource] ] -_TRACER = None # type: typing.Optional[Tracer] -_TRACER_FACTORY = None # type: typing.Optional[ImplementationFactory] +_TRACER_SOURCE = None # type: typing.Optional[TracerSource] +_TRACER_SOURCE_FACTORY = None # type: typing.Optional[ImplementationFactory] -def tracer() -> Tracer: - """Gets the current global :class:`~.Tracer` object. +def tracer_source() -> TracerSource: + """Gets the current global :class:`~.TracerSource` object. If there isn't one set yet, a default will be loaded. """ - global _TRACER, _TRACER_FACTORY # pylint:disable=global-statement + global _TRACER_SOURCE, _TRACER_SOURCE_FACTORY # pylint:disable=global-statement - if _TRACER is None: + if _TRACER_SOURCE is None: # pylint:disable=protected-access - _TRACER = loader._load_impl(Tracer, _TRACER_FACTORY) - del _TRACER_FACTORY + _TRACER_SOURCE = loader._load_impl( + TracerSource, _TRACER_SOURCE_FACTORY + ) + del _TRACER_SOURCE_FACTORY - return _TRACER + return _TRACER_SOURCE -def set_preferred_tracer_implementation( +def set_preferred_tracer_source_implementation( factory: ImplementationFactory, ) -> None: - """Set the factory to be used to create the tracer. + """Set the factory to be used to create the tracer source. See :mod:`opentelemetry.util.loader` for details. This function may not be called after a tracer is already loaded. Args: - factory: Callback that should create a new :class:`Tracer` instance. + factory: Callback that should create a new :class:`TracerSource` + instance. """ - global _TRACER_FACTORY # pylint:disable=global-statement + global _TRACER_SOURCE_FACTORY # pylint:disable=global-statement - if _TRACER: - raise RuntimeError("Tracer already loaded.") + if _TRACER_SOURCE: + raise RuntimeError("TracerSource already loaded.") - _TRACER_FACTORY = factory + _TRACER_SOURCE_FACTORY = factory diff --git a/opentelemetry-api/src/opentelemetry/util/loader.py b/opentelemetry-api/src/opentelemetry/util/loader.py index 3ae5a52fc5..b65c822ab9 100644 --- a/opentelemetry-api/src/opentelemetry/util/loader.py +++ b/opentelemetry-api/src/opentelemetry/util/loader.py @@ -15,7 +15,8 @@ """ The OpenTelemetry loader module is mainly used internally to load the -implementation for global objects like :func:`opentelemetry.trace.tracer`. +implementation for global objects like +:func:`opentelemetry.trace.tracer_source`. .. _loader-factory: @@ -27,7 +28,7 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: That function is called with e.g., the type of the global object it should create as an argument (e.g. the type object -:class:`opentelemetry.trace.Tracer`) and should return an instance of that type +:class:`opentelemetry.trace.TracerSource`) and should return an instance of that type (such that ``instanceof(my_factory_for_t(T), T)`` is true). Alternatively, it may return ``None`` to indicate that the no-op default should be used. @@ -36,16 +37,16 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: 1. If the environment variable :samp:`OPENTELEMETRY_PYTHON_IMPLEMENTATION_{getter-name}` (e.g., - ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_TRACER``) is set to an nonempty - value, an attempt is made to import a module with that name and use a - factory function named ``get_opentelemetry_implementation`` in it. - 2. Otherwise, the same is tried with the environment - variable ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT``. + ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_TRACERSOURCE``) is set to an + nonempty value, an attempt is made to import a module with that name and + use a factory function named ``get_opentelemetry_implementation`` in it. + 2. Otherwise, the same is tried with the environment variable + ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT``. 3. Otherwise, if a :samp:`set_preferred_{}_implementation` was called (e.g. - :func:`opentelemetry.trace.set_preferred_tracer_implementation`), the - callback set there is used (that is, the environment variables override - the callback set in code). + :func:`opentelemetry.trace.set_preferred_tracer_source_implementation`), + the callback set there is used (that is, the environment variables + override the callback set in code). 4. Otherwise, if :func:`set_preferred_default_implementation` was called, the callback set there is used. 5. Otherwise, an attempt is made to import and use the OpenTelemetry SDK. diff --git a/opentelemetry-api/tests/mypysmoke.py b/opentelemetry-api/tests/mypysmoke.py index 7badc13b69..3f652adca1 100644 --- a/opentelemetry-api/tests/mypysmoke.py +++ b/opentelemetry-api/tests/mypysmoke.py @@ -15,5 +15,5 @@ import opentelemetry.trace -def dummy_check_mypy_returntype() -> opentelemetry.trace.Tracer: - return opentelemetry.trace.tracer() +def dummy_check_mypy_returntype() -> opentelemetry.trace.TracerSource: + return opentelemetry.trace.tracer_source() diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index 60bf9dd9fa..cd126229f9 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -26,7 +26,8 @@ class TestAPIOnlyImplementation(unittest.TestCase): """ def test_tracer(self): - tracer = trace.Tracer() + tracer_source = trace.TracerSource() + tracer = tracer_source.get_tracer(__name__) with tracer.start_span("test") as span: self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) self.assertEqual(span, trace.INVALID_SPAN) diff --git a/opentelemetry-api/tests/test_loader.py b/opentelemetry-api/tests/test_loader.py index 970b615963..8ac397afcb 100644 --- a/opentelemetry-api/tests/test_loader.py +++ b/opentelemetry-api/tests/test_loader.py @@ -21,18 +21,18 @@ from opentelemetry import trace from opentelemetry.util import loader -DUMMY_TRACER = None +DUMMY_TRACER_SOURCE = None -class DummyTracer(trace.Tracer): +class DummyTracerSource(trace.TracerSource): pass def get_opentelemetry_implementation(type_): - global DUMMY_TRACER # pylint:disable=global-statement - assert type_ is trace.Tracer - DUMMY_TRACER = DummyTracer() - return DUMMY_TRACER + global DUMMY_TRACER_SOURCE # pylint:disable=global-statement + assert type_ is trace.TracerSource + DUMMY_TRACER_SOURCE = DummyTracerSource() + return DUMMY_TRACER_SOURCE # pylint:disable=redefined-outer-name,protected-access,unidiomatic-typecheck @@ -43,30 +43,32 @@ def setUp(self): reload(loader) reload(trace) - # Need to reload self, otherwise DummyTracer will have the wrong base - # class after reloading `trace`. + # Need to reload self, otherwise DummyTracerSource will have the wrong + # base class after reloading `trace`. reload(sys.modules[__name__]) def test_get_default(self): - tracer = trace.tracer() - self.assertIs(type(tracer), trace.Tracer) + tracer_source = trace.tracer_source() + self.assertIs(type(tracer_source), trace.TracerSource) def test_preferred_impl(self): - trace.set_preferred_tracer_implementation( + trace.set_preferred_tracer_source_implementation( get_opentelemetry_implementation ) - tracer = trace.tracer() - self.assertIs(tracer, DUMMY_TRACER) + tracer_source = trace.tracer_source() + self.assertIs(tracer_source, DUMMY_TRACER_SOURCE) # NOTE: We use do_* + *_ methods because subtest wouldn't run setUp, # which we require here. def do_test_preferred_impl(self, setter: Callable[[Any], Any]) -> None: setter(get_opentelemetry_implementation) - tracer = trace.tracer() - self.assertIs(tracer, DUMMY_TRACER) + tracer_source = trace.tracer_source() + self.assertIs(tracer_source, DUMMY_TRACER_SOURCE) def test_preferred_impl_with_tracer(self): - self.do_test_preferred_impl(trace.set_preferred_tracer_implementation) + self.do_test_preferred_impl( + trace.set_preferred_tracer_source_implementation + ) def test_preferred_impl_with_default(self): self.do_test_preferred_impl( @@ -74,16 +76,16 @@ def test_preferred_impl_with_default(self): ) def test_try_set_again(self): - self.assertTrue(trace.tracer()) - # Try setting after the tracer has already been created: + self.assertTrue(trace.tracer_source()) + # Try setting after the tracer_source has already been created: with self.assertRaises(RuntimeError) as einfo: - trace.set_preferred_tracer_implementation( + trace.set_preferred_tracer_source_implementation( get_opentelemetry_implementation ) self.assertIn("already loaded", str(einfo.exception)) def do_test_get_envvar(self, envvar_suffix: str) -> None: - global DUMMY_TRACER # pylint:disable=global-statement + global DUMMY_TRACER_SOURCE # pylint:disable=global-statement # Test is not runnable with this! self.assertFalse(sys.flags.ignore_environment) @@ -91,15 +93,15 @@ def do_test_get_envvar(self, envvar_suffix: str) -> None: envname = "OPENTELEMETRY_PYTHON_IMPLEMENTATION_" + envvar_suffix os.environ[envname] = __name__ try: - tracer = trace.tracer() - self.assertIs(tracer, DUMMY_TRACER) + tracer_source = trace.tracer_source() + self.assertIs(tracer_source, DUMMY_TRACER_SOURCE) finally: - DUMMY_TRACER = None + DUMMY_TRACER_SOURCE = None del os.environ[envname] - self.assertIs(type(tracer), DummyTracer) + self.assertIs(type(tracer_source), DummyTracerSource) def test_get_envvar_tracer(self): - return self.do_test_get_envvar("TRACER") + return self.do_test_get_envvar("TRACERSOURCE") def test_get_envvar_default(self): return self.do_test_get_envvar("DEFAULT") diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 140626aa94..3035ae7ef9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -41,8 +41,8 @@ class SpanProcessor: invocations. Span processors can be registered directly using - :func:`Tracer.add_span_processor` and they are invoked in the same order - as they were registered. + :func:`TracerSource.add_span_processor` and they are invoked + in the same order as they were registered. """ def on_start(self, span: "Span") -> None: @@ -137,6 +137,7 @@ def __init__( links: Sequence[trace_api.Link] = (), kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, span_processor: SpanProcessor = SpanProcessor(), + instrumentation_info: "InstrumentationInfo" = None, set_status_on_exception: bool = True, ) -> None: @@ -172,6 +173,7 @@ def __init__( self.end_time = None # type: Optional[int] self.start_time = None # type: Optional[int] + self.instrumentation_info = instrumentation_info def __repr__(self): return '{}(name="{}", context={})'.format( @@ -326,6 +328,46 @@ def generate_trace_id() -> int: return random.getrandbits(128) +class InstrumentationInfo: + """Immutable information about an instrumentation library module. + + See `TracerSource.get_tracer` for the meaning of the properties. + """ + + __slots__ = ("_name", "_version") + + def __init__(self, name: str, version: str): + self._name = name + self._version = version + + def __repr__(self): + return "{}({}, {})".format( + type(self).__name__, self._name, self._version + ) + + def __hash__(self): + return hash((self._name, self._version)) + + def __eq__(self, value): + return type(value) is type(self) and (self._name, self._version) == ( + value._name, + value._version, + ) + + def __lt__(self, value): + if type(value) is not type(self): + return NotImplemented + return (self._name, self._version) < (value._name, value._version) + + @property + def version(self) -> str: + return self._version + + @property + def name(self) -> str: + return self._name + + class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`. @@ -337,23 +379,15 @@ class Tracer(trace_api.Tracer): def __init__( self, - name: str = "", - sampler: sampling.Sampler = trace_api.sampling.ALWAYS_ON, - shutdown_on_exit: bool = True, + source: "TracerSource", + instrumentation_info: InstrumentationInfo, ) -> None: - slot_name = "current_span" - if name: - slot_name = "{}.current_span".format(name) - self._current_span_slot = Context.register_slot(slot_name) - self._active_span_processor = MultiSpanProcessor() - self.sampler = sampler - self._atexit_handler = None - if shutdown_on_exit: - self._atexit_handler = atexit.register(self.shutdown) + self.source = source + self.instrumentation_info = instrumentation_info def get_current_span(self): """See `opentelemetry.trace.Tracer.get_current_span`.""" - return self._current_span_slot.get() + return self.source.get_current_span() def start_as_current_span( self, @@ -411,7 +445,7 @@ def start_span( # pylint: disable=too-many-locals # exported. # The sampler may also add attributes to the newly-created span, e.g. # to include information about the sampling decision. - sampling_decision = self.sampler.should_sample( + sampling_decision = self.source.sampler.should_sample( parent_context, context.trace_id, context.span_id, @@ -431,11 +465,12 @@ def start_span( # pylint: disable=too-many-locals name=name, context=context, parent=parent, - sampler=self.sampler, + sampler=self.source.sampler, attributes=span_attributes, - span_processor=self._active_span_processor, + span_processor=self.source._active_span_processor, # pylint:disable=protected-access kind=kind, links=links, + instrumentation_info=self.instrumentation_info, set_status_on_exception=set_status_on_exception, ) span.start(start_time=start_time) @@ -449,18 +484,56 @@ def use_span( ) -> Iterator[trace_api.Span]: """See `opentelemetry.trace.Tracer.use_span`.""" try: - span_snapshot = self._current_span_slot.get() - self._current_span_slot.set(span) + span_snapshot = self.source.get_current_span() + self.source._current_span_slot.set( # pylint:disable=protected-access + span + ) try: yield span finally: - self._current_span_slot.set(span_snapshot) + self.source._current_span_slot.set( # pylint:disable=protected-access + span_snapshot + ) finally: if end_on_exit: span.end() + +class TracerSource(trace_api.TracerSource): + def __init__( + self, + sampler: sampling.Sampler = trace_api.sampling.ALWAYS_ON, + shutdown_on_exit: bool = True, + ): + # TODO: How should multiple TracerSources behave? Should they get their own contexts? + # This could be done by adding `str(id(self))` to the slot name. + self._current_span_slot = Context.register_slot("current_span") + self._active_span_processor = MultiSpanProcessor() + self.sampler = sampler + self._atexit_handler = None + if shutdown_on_exit: + self._atexit_handler = atexit.register(self.shutdown) + + def get_tracer( + self, + instrumenting_module_name: str, + instrumenting_library_version: str = "", + ) -> "trace_api.Tracer": + if not instrumenting_module_name: # Reject empty strings too. + instrumenting_module_name = "ERROR:MISSING MODULE NAME" + logger.error("get_tracer called with missing module name.") + return Tracer( + self, + InstrumentationInfo( + instrumenting_module_name, instrumenting_library_version + ), + ) + + def get_current_span(self) -> Span: + return self._current_span_slot.get() + def add_span_processor(self, span_processor: SpanProcessor) -> None: - """Registers a new :class:`SpanProcessor` for this `Tracer`. + """Registers a new :class:`SpanProcessor` for this `TracerSource`. The span processors are invoked in the same order they are registered. """ @@ -475,6 +548,3 @@ def shutdown(self): if self._atexit_handler is not None: atexit.unregister(self._atexit_handler) self._atexit_handler = None - - -tracer = Tracer() diff --git a/opentelemetry-sdk/tests/test_implementation.py b/opentelemetry-sdk/tests/test_implementation.py index 9aaa5fc35a..d8d6bae139 100644 --- a/opentelemetry-sdk/tests/test_implementation.py +++ b/opentelemetry-sdk/tests/test_implementation.py @@ -28,7 +28,7 @@ class TestSDKImplementation(unittest.TestCase): """ def test_tracer(self): - tracer = trace.Tracer() + tracer = trace.TracerSource().get_tracer(__name__) with tracer.start_span("test") as span: self.assertNotEqual(span.get_context(), INVALID_SPAN_CONTEXT) self.assertNotEqual(span, INVALID_SPAN) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 9ad65aea88..54fdee2629 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -44,13 +44,14 @@ def shutdown(self): class TestSimpleExportSpanProcessor(unittest.TestCase): def test_simple_span_processor(self): - tracer = trace.Tracer() + tracer_source = trace.TracerSource() + tracer = tracer_source.get_tracer(__name__) spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) span_processor = export.SimpleExportSpanProcessor(my_exporter) - tracer.add_span_processor(span_processor) + tracer_source.add_span_processor(span_processor) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): @@ -68,13 +69,14 @@ def test_simple_span_processor_no_context(self): SpanProcessors should act on a span's start and end events whether or not it is ever the active span. """ - tracer = trace.Tracer() + tracer_source = trace.TracerSource() + tracer = tracer_source.get_tracer(__name__) spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) span_processor = export.SimpleExportSpanProcessor(my_exporter) - tracer.add_span_processor(span_processor) + tracer_source.add_span_processor(span_processor) with tracer.start_span("foo"): with tracer.start_span("bar"): diff --git a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py index b52d148c1b..5c5194053b 100644 --- a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py +++ b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py @@ -24,62 +24,40 @@ class TestInMemorySpanExporter(unittest.TestCase): - def test_get_finished_spans(self): - tracer = trace.Tracer() - - memory_exporter = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(memory_exporter) - tracer.add_span_processor(span_processor) - - with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("xxx"): + def setUp(self): + self.tracer_source = trace.TracerSource() + self.tracer = self.tracer_source.get_tracer(__name__) + self.memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) + self.tracer_source.add_span_processor(span_processor) + self.exec_scenario() + + def exec_scenario(self): + with self.tracer.start_as_current_span("foo"): + with self.tracer.start_as_current_span("bar"): + with self.tracer.start_as_current_span("xxx"): pass - span_list = memory_exporter.get_finished_spans() + def test_get_finished_spans(self): + span_list = self.memory_exporter.get_finished_spans() spans_names_list = [span.name for span in span_list] self.assertListEqual(["xxx", "bar", "foo"], spans_names_list) def test_clear(self): - tracer = trace.Tracer() - - memory_exporter = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(memory_exporter) - tracer.add_span_processor(span_processor) - - with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("xxx"): - pass - - memory_exporter.clear() - span_list = memory_exporter.get_finished_spans() + self.memory_exporter.clear() + span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) def test_shutdown(self): - tracer = trace.Tracer() - - memory_exporter = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(memory_exporter) - tracer.add_span_processor(span_processor) - - with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("xxx"): - pass - - span_list = memory_exporter.get_finished_spans() + span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) - memory_exporter.shutdown() + self.memory_exporter.shutdown() # after shutdown no new spans are accepted - with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("xxx"): - pass + self.exec_scenario() - span_list = memory_exporter.get_finished_spans() + span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) def test_return_code(self): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 9ec68feeb0..98a7bb100e 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -24,21 +24,26 @@ from opentelemetry.util import time_ns +def new_tracer() -> trace_api.Tracer: + return trace.TracerSource().get_tracer(__name__) + + class TestTracer(unittest.TestCase): def test_extends_api(self): - tracer = trace.Tracer() + tracer = new_tracer() + self.assertIsInstance(tracer, trace.Tracer) self.assertIsInstance(tracer, trace_api.Tracer) def test_shutdown(self): - tracer = trace.Tracer() + tracer_source = trace.TracerSource() mock_processor1 = mock.Mock(spec=trace.SpanProcessor) - tracer.add_span_processor(mock_processor1) + tracer_source.add_span_processor(mock_processor1) mock_processor2 = mock.Mock(spec=trace.SpanProcessor) - tracer.add_span_processor(mock_processor2) + tracer_source.add_span_processor(mock_processor2) - tracer.shutdown() + tracer_source.shutdown() self.assertEqual(mock_processor1.shutdown.call_count, 1) self.assertEqual(mock_processor2.shutdown.call_count, 1) @@ -58,8 +63,8 @@ def print_shutdown_count(): # creating the tracer atexit.register(print_shutdown_count) -tracer = trace.Tracer({tracer_parameters}) -tracer.add_span_processor(mock_processor) +tracer_source = trace.TracerSource({tracer_parameters}) +tracer_source.add_span_processor(mock_processor) {tracer_shutdown} """ @@ -72,7 +77,7 @@ def run_general_code(shutdown_on_exit, explicit_shutdown): tracer_parameters = "shutdown_on_exit=False" if explicit_shutdown: - tracer_shutdown = "tracer.shutdown()" + tracer_shutdown = "tracer_source.shutdown()" return subprocess.check_output( [ @@ -103,7 +108,7 @@ def run_general_code(shutdown_on_exit, explicit_shutdown): class TestTracerSampling(unittest.TestCase): def test_default_sampler(self): - tracer = trace.Tracer() + tracer = new_tracer() # Check that the default tracer creates real spans via the default # sampler @@ -113,8 +118,8 @@ def test_default_sampler(self): self.assertIsInstance(child_span, trace.Span) def test_sampler_no_sampling(self): - tracer = trace.Tracer() - tracer.sampler = sampling.ALWAYS_OFF + tracer_source = trace.TracerSource(sampling.ALWAYS_OFF) + tracer = tracer_source.get_tracer(__name__) # Check that the default tracer creates no-op spans if the sampler # decides not to sampler @@ -132,15 +137,68 @@ def test_start_span_invalid_spancontext(self): Invalid span contexts should also not be added as a parent. This eliminates redundant error handling logic in exporters. """ - tracer = trace.Tracer("test_start_span_invalid_spancontext") + tracer = new_tracer() new_span = tracer.start_span( "root", parent=trace_api.INVALID_SPAN_CONTEXT ) self.assertTrue(new_span.context.is_valid()) self.assertIsNone(new_span.parent) + def test_instrumentation_info(self): + tracer_source = trace.TracerSource() + tracer1 = tracer_source.get_tracer("instr1") + tracer2 = tracer_source.get_tracer("instr2", "1.3b3") + span1 = tracer1.start_span("s1") + span2 = tracer2.start_span("s2") + self.assertEqual( + span1.instrumentation_info, trace.InstrumentationInfo("instr1", "") + ) + self.assertEqual( + span2.instrumentation_info, + trace.InstrumentationInfo("instr2", "1.3b3"), + ) + + self.assertEqual(span2.instrumentation_info.version, "1.3b3") + self.assertEqual(span2.instrumentation_info.name, "instr2") + + self.assertLess( + span1.instrumentation_info, span2.instrumentation_info + ) # Check sortability. + + def test_invalid_instrumentation_info(self): + tracer_source = trace.TracerSource() + tracer1 = tracer_source.get_tracer("") + tracer2 = tracer_source.get_tracer(None) + self.assertEqual( + tracer1.instrumentation_info, tracer2.instrumentation_info + ) + self.assertIsInstance( + tracer1.instrumentation_info, trace.InstrumentationInfo + ) + span1 = tracer1.start_span("foo") + self.assertTrue(span1.is_recording_events()) + self.assertEqual(tracer1.instrumentation_info.version, "") + self.assertEqual( + tracer1.instrumentation_info.name, "ERROR:MISSING MODULE NAME" + ) + + def test_span_processor_for_source(self): + tracer_source = trace.TracerSource() + tracer1 = tracer_source.get_tracer("instr1") + tracer2 = tracer_source.get_tracer("instr2", "1.3b3") + span1 = tracer1.start_span("s1") + span2 = tracer2.start_span("s2") + + # pylint:disable=protected-access + self.assertIs( + span1.span_processor, tracer_source._active_span_processor + ) + self.assertIs( + span2.span_processor, tracer_source._active_span_processor + ) + def test_start_span_implicit(self): - tracer = trace.Tracer("test_start_span_implicit") + tracer = new_tracer() self.assertIsNone(tracer.get_current_span()) @@ -185,7 +243,7 @@ def test_start_span_implicit(self): self.assertIsNotNone(root.end_time) def test_start_span_explicit(self): - tracer = trace.Tracer("test_start_span_explicit") + tracer = new_tracer() other_parent = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, @@ -233,7 +291,7 @@ def test_start_span_explicit(self): self.assertIsNotNone(child.end_time) def test_start_as_current_span_implicit(self): - tracer = trace.Tracer("test_start_as_current_span_implicit") + tracer = new_tracer() self.assertIsNone(tracer.get_current_span()) @@ -253,7 +311,7 @@ def test_start_as_current_span_implicit(self): self.assertIsNotNone(root.end_time) def test_start_as_current_span_explicit(self): - tracer = trace.Tracer("test_start_as_current_span_explicit") + tracer = new_tracer() other_parent = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, @@ -288,7 +346,7 @@ def test_start_as_current_span_explicit(self): class TestSpan(unittest.TestCase): def setUp(self): - self.tracer = trace.Tracer("test_span") + self.tracer = new_tracer() def test_basic_span(self): span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) @@ -335,14 +393,19 @@ def test_attributes(self): self.assertEqual(root.attributes["attr-key2"], "val2") self.assertEqual(root.attributes["attr-in-both"], "span-attr") + def test_sampling_attributes(self): decision_attributes = { "sampler-attr": "sample-val", "attr-in-both": "decision-attr", } - self.tracer.sampler = sampling.StaticSampler( - sampling.Decision(sampled=True, attributes=decision_attributes) + tracer_source = trace.TracerSource( + sampling.StaticSampler( + sampling.Decision(sampled=True, attributes=decision_attributes) + ) ) + self.tracer = tracer_source.get_tracer(__name__) + with self.tracer.start_as_current_span("root2") as root: self.assertEqual(len(root.attributes), 2) self.assertEqual(root.attributes["sampler-attr"], "sample-val") @@ -515,7 +578,9 @@ def test_ended_span(self): def test_error_status(self): try: - with trace.Tracer("test_error_status").start_span("root") as root: + with trace.TracerSource().get_tracer(__name__).start_span( + "root" + ) as root: raise Exception("unknown") except Exception: # pylint: disable=broad-except pass @@ -546,7 +611,8 @@ def on_end(self, span: "trace.Span") -> None: class TestSpanProcessor(unittest.TestCase): def test_span_processor(self): - tracer = trace.Tracer() + tracer_source = trace.TracerSource() + tracer = tracer_source.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand @@ -564,7 +630,7 @@ def test_span_processor(self): self.assertEqual(len(spans_calls_list), 0) # add single span processor - tracer.add_span_processor(sp1) + tracer_source.add_span_processor(sp1) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) @@ -587,7 +653,7 @@ def test_span_processor(self): expected_list.clear() # go for multiple span processors - tracer.add_span_processor(sp2) + tracer_source.add_span_processor(sp2) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) @@ -614,7 +680,8 @@ def test_span_processor(self): self.assertListEqual(spans_calls_list, expected_list) def test_add_span_processor_after_span_creation(self): - tracer = trace.Tracer() + tracer_source = trace.TracerSource() + tracer = tracer_source.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand @@ -626,7 +693,7 @@ def test_add_span_processor_after_span_creation(self): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): # add span processor after spans have been created - tracer.add_span_processor(sp) + tracer_source.add_span_processor(sp) expected_list.append(span_event_end_fmt("SP1", "baz")) diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py index a26141f14c..bea4d4fde5 100644 --- a/tests/w3c_tracecontext_validation_server.py +++ b/tests/w3c_tracecontext_validation_server.py @@ -26,7 +26,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests from opentelemetry.ext.wsgi import OpenTelemetryMiddleware -from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace import TracerSource from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleExportSpanProcessor, @@ -34,16 +34,16 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_implementation(lambda T: Tracer()) +trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(trace.tracer()) +http_requests.enable(trace.tracer_source()) # SpanExporter receives the spans and send them to the target location. span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter()) -trace.tracer().add_span_processor(span_processor) +trace.tracer_source().add_span_processor(span_processor) app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) From 8fa21e6bcbb0b28917039661ca2ba369d0dee6b2 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 16 Dec 2019 13:14:24 -0800 Subject: [PATCH 0165/1517] Updating date for v0.4 (#337) As per the discussion in the SIG, the release date of v0.4 has been pushed to January 31st. --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fcedf05e02..0129772c61 100644 --- a/README.md +++ b/README.md @@ -178,11 +178,11 @@ Future releases targets include: | Component | Version | Target Date | | ----------------------------------- | ---------- | ---------------- | -| Zipkin Trace Exporter | Alpha v0.4 | December 31 2019 | -| W3C Correlation Context Propagation | Alpha v0.4 | December 31 2019 | -| Support for Tags/Baggage | Alpha v0.4 | December 31 2019 | -| Metrics Aggregation | Alpha v0.4 | December 31 2019 | -| gRPC Integrations | Alpha v0.4 | December 31 2019 | -| Prometheus Metrics Exporter | Alpha v0.4 | December 31 2019 | -| OpenCensus Bridge | Alpha v0.4 | December 31 2019 | -| Metrics SDK (Complete) | Alpha v0.4 | December 31 2019 | +| Zipkin Trace Exporter | Alpha v0.4 | January 28 2020 | +| W3C Correlation Context Propagation | Alpha v0.4 | January 28 2020 | +| Support for Tags/Baggage | Alpha v0.4 | January 28 2020 | +| Metrics Aggregation | Alpha v0.4 | January 28 2020 | +| gRPC Integrations | Alpha v0.4 | January 28 2020 | +| Prometheus Metrics Exporter | Alpha v0.4 | January 28 2020 | +| OpenCensus Bridge | Alpha v0.4 | January 28 2020 | +| Metrics SDK (Complete) | Alpha v0.4 | January 28 2020 | From 4458698ea178498f7af2d476644b34afcc366335 Mon Sep 17 00:00:00 2001 From: alrex Date: Sun, 29 Dec 2019 22:02:55 -0800 Subject: [PATCH 0166/1517] Adding Zipkin exporter (#320) Signed-off-by: Alex Boten --- ext/opentelemetry-ext-zipkin/CHANGELOG.md | 4 + ext/opentelemetry-ext-zipkin/README.rst | 67 +++++ ext/opentelemetry-ext-zipkin/setup.cfg | 47 ++++ ext/opentelemetry-ext-zipkin/setup.py | 26 ++ .../src/opentelemetry/ext/zipkin/__init__.py | 184 +++++++++++++ .../src/opentelemetry/ext/zipkin/version.py | 15 ++ .../tests/__init__.py | 13 + .../tests/test_zipkin_exporter.py | 242 ++++++++++++++++++ scripts/coverage.sh | 2 + tox.ini | 10 +- 10 files changed, 606 insertions(+), 4 deletions(-) create mode 100644 ext/opentelemetry-ext-zipkin/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-zipkin/README.rst create mode 100644 ext/opentelemetry-ext-zipkin/setup.cfg create mode 100644 ext/opentelemetry-ext-zipkin/setup.py create mode 100644 ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py create mode 100644 ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py create mode 100644 ext/opentelemetry-ext-zipkin/tests/__init__.py create mode 100644 ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py diff --git a/ext/opentelemetry-ext-zipkin/CHANGELOG.md b/ext/opentelemetry-ext-zipkin/CHANGELOG.md new file mode 100644 index 0000000000..617d979ab2 --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## Unreleased + diff --git a/ext/opentelemetry-ext-zipkin/README.rst b/ext/opentelemetry-ext-zipkin/README.rst new file mode 100644 index 0000000000..f91d0c2c6a --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/README.rst @@ -0,0 +1,67 @@ +OpenTelemetry Zipkin Exporter +============================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-zipkin.svg + :target: https://pypi.org/project/opentelemetry-ext-zipkin/ + +This library allows to export tracing data to `Zipkin `_. + +Installation +------------ + +:: + + pip install opentelemetry-ext-zipkin + + +Usage +----- + +The **OpenTelemetry Zipkin Exporter** allows to export `OpenTelemetry`_ traces to `Zipkin`_. +This exporter always send traces to the configured Zipkin collector using HTTP. + + +.. _Zipkin: https://zipkin.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext import zipkin + from opentelemetry.sdk.trace import TracerSource + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + + trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + tracer = trace.tracer_source().get_tracer(__name__) + + # create a ZipkinSpanExporter + zipkin_exporter = zipkin.ZipkinSpanExporter( + service_name="my-helloworld-service", + # optional: + # host_name="localhost", + # port=9411, + # endpoint="/api/v2/spans", + # protocol="http", + # ipv4="", + # ipv6="", + # retry=False, + ) + + # Create a BatchExportSpanProcessor and add the exporter to it + span_processor = BatchExportSpanProcessor(zipkin_exporter) + + # add to the tracer + trace.tracer_source().add_span_processor(span_processor) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + +The `examples <./examples>`_ folder contains more elaborated examples. + +References +---------- + +* `Zipkin `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg new file mode 100644 index 0000000000..89d60d149a --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -0,0 +1,47 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-zipkin +description = Zipkin Span Exporter for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-zipkin +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + requests~=2.7 + opentelemetry-api + opentelemetry-sdk + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-zipkin/setup.py b/ext/opentelemetry-ext-zipkin/setup.py new file mode 100644 index 0000000000..f93bbad449 --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/setup.py @@ -0,0 +1,26 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "zipkin", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py new file mode 100644 index 0000000000..e0b5791d1e --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -0,0 +1,184 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Zipkin Span Exporter for OpenTelemetry.""" + +import json +import logging +from typing import Optional, Sequence + +import requests + +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.trace import Span, SpanContext, SpanKind + +DEFAULT_ENDPOINT = "/api/v2/spans" +DEFAULT_HOST_NAME = "localhost" +DEFAULT_PORT = 9411 +DEFAULT_PROTOCOL = "http" +DEFAULT_RETRY = False +ZIPKIN_HEADERS = {"Content-Type": "application/json"} + +SPAN_KIND_MAP = { + SpanKind.INTERNAL: None, + SpanKind.SERVER: "SERVER", + SpanKind.CLIENT: "CLIENT", + SpanKind.PRODUCER: "PRODUCER", + SpanKind.CONSUMER: "CONSUMER", +} + +SUCCESS_STATUS_CODES = (200, 202) + +logger = logging.getLogger(__name__) + + +class ZipkinSpanExporter(SpanExporter): + """Zipkin span exporter for OpenTelemetry. + + Args: + service_name: Service that logged an annotation in a trace.Classifier + when query for spans. + host_name: The host name of the Zipkin server + port: The port of the Zipkin server + endpoint: The endpoint of the Zipkin server + protocol: The protocol used for the request. + ipv4: Primary IPv4 address associated with this connection. + ipv6: Primary IPv6 address associated with this connection. + retry: Set to True to configure the exporter to retry on failure. + """ + + def __init__( + self, + service_name: str, + host_name: str = DEFAULT_HOST_NAME, + port: int = DEFAULT_PORT, + endpoint: str = DEFAULT_ENDPOINT, + protocol: str = DEFAULT_PROTOCOL, + ipv4: Optional[str] = None, + ipv6: Optional[str] = None, + retry: Optional[str] = DEFAULT_RETRY, + ): + self.service_name = service_name + self.host_name = host_name + self.port = port + self.endpoint = endpoint + self.protocol = protocol + self.url = "{}://{}:{}{}".format( + self.protocol, self.host_name, self.port, self.endpoint + ) + self.ipv4 = ipv4 + self.ipv6 = ipv6 + self.retry = retry + + def export(self, spans: Sequence[Span]) -> SpanExportResult: + zipkin_spans = self._translate_to_zipkin(spans) + result = requests.post( + url=self.url, data=json.dumps(zipkin_spans), headers=ZIPKIN_HEADERS + ) + + if result.status_code not in SUCCESS_STATUS_CODES: + logger.error( + "Traces cannot be uploaded; status code: %s, message %s", + result.status_code, + result.text, + ) + + if self.retry: + return SpanExportResult.FAILED_RETRYABLE + return SpanExportResult.FAILED_NOT_RETRYABLE + return SpanExportResult.SUCCESS + + def _translate_to_zipkin(self, spans: Sequence[Span]): + + local_endpoint = { + "serviceName": self.service_name, + "port": self.port, + } + + if self.ipv4 is not None: + local_endpoint["ipv4"] = self.ipv4 + + if self.ipv6 is not None: + local_endpoint["ipv6"] = self.ipv6 + + zipkin_spans = [] + for span in spans: + context = span.get_context() + trace_id = context.trace_id + span_id = context.span_id + + # Timestamp in zipkin spans is int of microseconds. + # see: https://zipkin.io/pages/instrumenting.html + start_timestamp_mus = _nsec_to_usec_round(span.start_time) + duration_mus = _nsec_to_usec_round(span.end_time - span.start_time) + + zipkin_span = { + "traceId": format(trace_id, "x"), + "id": format(span_id, "x"), + "name": span.name, + "timestamp": start_timestamp_mus, + "duration": duration_mus, + "localEndpoint": local_endpoint, + "kind": SPAN_KIND_MAP[span.kind], + "tags": _extract_tags_from_span(span.attributes), + "annotations": _extract_annotations_from_events(span.events), + } + + if context.trace_options.sampled: + zipkin_span["debug"] = 1 + + if isinstance(span.parent, Span): + zipkin_span["parentId"] = format( + span.parent.get_context().span_id, "x" + ) + elif isinstance(span.parent, SpanContext): + zipkin_span["parentId"] = format(span.parent.span_id, "x") + + zipkin_spans.append(zipkin_span) + return zipkin_spans + + def shutdown(self) -> None: + pass + + +def _extract_tags_from_span(attr): + if not attr: + return None + tags = {} + for attribute_key, attribute_value in attr.items(): + if isinstance(attribute_value, (int, bool, float)): + value = str(attribute_value) + elif isinstance(attribute_value, str): + value = attribute_value[:128] + else: + logger.warning("Could not serialize tag %s", attribute_key) + continue + tags[attribute_key] = value + return tags + + +def _extract_annotations_from_events(events): + return ( + [ + {"timestamp": _nsec_to_usec_round(e.timestamp), "value": e.name} + for e in events + ] + if events + else None + ) + + +def _nsec_to_usec_round(nsec): + """Round nanoseconds to microseconds""" + return (nsec + 500) // 10 ** 3 diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py new file mode 100644 index 0000000000..93ef792d05 --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.3.dev0" diff --git a/ext/opentelemetry-ext-zipkin/tests/__init__.py b/ext/opentelemetry-ext-zipkin/tests/__init__.py new file mode 100644 index 0000000000..d853a7bcf6 --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py new file mode 100644 index 0000000000..745c662f53 --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -0,0 +1,242 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import unittest +from unittest.mock import MagicMock, patch + +from opentelemetry import trace as trace_api +from opentelemetry.ext.zipkin import ZipkinSpanExporter +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace.export import SpanExportResult +from opentelemetry.trace import TraceOptions + + +class MockResponse: + def __init__(self, status_code): + self.status_code = status_code + self.text = status_code + + +class TestZipkinSpanExporter(unittest.TestCase): + def setUp(self): + # create and save span to be used in tests + context = trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + ) + + self._test_span = trace.Span("test_span", context=context) + self._test_span.start() + self._test_span.end() + + def test_constructor_default(self): + """Test the default values assigned by constructor.""" + service_name = "my-service-name" + host_name = "localhost" + port = 9411 + endpoint = "/api/v2/spans" + exporter = ZipkinSpanExporter(service_name) + ipv4 = None + ipv6 = None + protocol = "http" + url = "http://localhost:9411/api/v2/spans" + + self.assertEqual(exporter.service_name, service_name) + self.assertEqual(exporter.host_name, host_name) + self.assertEqual(exporter.port, port) + self.assertEqual(exporter.endpoint, endpoint) + self.assertEqual(exporter.ipv4, ipv4) + self.assertEqual(exporter.ipv6, ipv6) + self.assertEqual(exporter.protocol, protocol) + self.assertEqual(exporter.url, url) + + def test_constructor_explicit(self): + """Test the constructor passing all the options.""" + service_name = "my-opentelemetry-zipkin" + host_name = "opentelemetry.io" + port = 15875 + endpoint = "/myapi/traces?format=zipkin" + ipv4 = "1.2.3.4" + ipv6 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + protocol = "https" + url = "https://opentelemetry.io:15875/myapi/traces?format=zipkin" + exporter = ZipkinSpanExporter( + service_name=service_name, + host_name=host_name, + port=port, + endpoint=endpoint, + ipv4=ipv4, + ipv6=ipv6, + protocol=protocol, + ) + + self.assertEqual(exporter.service_name, service_name) + self.assertEqual(exporter.host_name, host_name) + self.assertEqual(exporter.port, port) + self.assertEqual(exporter.endpoint, endpoint) + self.assertEqual(exporter.ipv4, ipv4) + self.assertEqual(exporter.ipv6, ipv6) + self.assertEqual(exporter.protocol, protocol) + self.assertEqual(exporter.url, url) + + # pylint: disable=too-many-locals + def test_export(self): + + span_names = ("test1", "test2", "test3") + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + span_id = 0x34BF92DEEFC58C92 + parent_id = 0x1111111111111111 + other_id = 0x2222222222222222 + + base_time = 683647322 * 10 ** 9 # in ns + start_times = ( + base_time, + base_time + 150 * 10 ** 6, + base_time + 300 * 10 ** 6, + ) + durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6) + end_times = ( + start_times[0] + durations[0], + start_times[1] + durations[1], + start_times[2] + durations[2], + ) + + span_context = trace_api.SpanContext( + trace_id, + span_id, + trace_options=TraceOptions(TraceOptions.SAMPLED), + ) + parent_context = trace_api.SpanContext(trace_id, parent_id) + other_context = trace_api.SpanContext(trace_id, other_id) + + event_attributes = { + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + } + + event_timestamp = base_time + 50 * 10 ** 6 + event = trace_api.Event( + name="event0", + timestamp=event_timestamp, + attributes=event_attributes, + ) + + link_attributes = {"key_bool": True} + + link = trace_api.Link( + context=other_context, attributes=link_attributes + ) + + otel_spans = [ + trace.Span( + name=span_names[0], + context=span_context, + parent=parent_context, + events=(event,), + links=(link,), + ), + trace.Span( + name=span_names[1], context=parent_context, parent=None + ), + trace.Span(name=span_names[2], context=other_context, parent=None), + ] + + otel_spans[0].start_time = start_times[0] + # added here to preserve order + otel_spans[0].set_attribute("key_bool", False) + otel_spans[0].set_attribute("key_string", "hello_world") + otel_spans[0].set_attribute("key_float", 111.22) + otel_spans[0].end_time = end_times[0] + + otel_spans[1].start_time = start_times[1] + otel_spans[1].end_time = end_times[1] + + otel_spans[2].start_time = start_times[2] + otel_spans[2].end_time = end_times[2] + + service_name = "test-service" + local_endpoint = { + "serviceName": service_name, + "port": 9411, + } + + exporter = ZipkinSpanExporter(service_name) + expected = [ + { + "traceId": format(trace_id, "x"), + "id": format(span_id, "x"), + "name": span_names[0], + "timestamp": start_times[0] // 10 ** 3, + "duration": durations[0] // 10 ** 3, + "localEndpoint": local_endpoint, + "kind": None, + "tags": { + "key_bool": "False", + "key_string": "hello_world", + "key_float": "111.22", + }, + "annotations": [ + { + "timestamp": event_timestamp // 10 ** 3, + "value": "event0", + } + ], + "debug": 1, + "parentId": format(parent_id, "x"), + }, + { + "traceId": format(trace_id, "x"), + "id": format(parent_id, "x"), + "name": span_names[1], + "timestamp": start_times[1] // 10 ** 3, + "duration": durations[1] // 10 ** 3, + "localEndpoint": local_endpoint, + "kind": None, + "tags": None, + "annotations": None, + }, + { + "traceId": format(trace_id, "x"), + "id": format(other_id, "x"), + "name": span_names[2], + "timestamp": start_times[2] // 10 ** 3, + "duration": durations[2] // 10 ** 3, + "localEndpoint": local_endpoint, + "kind": None, + "tags": None, + "annotations": None, + }, + ] + + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export(otel_spans) + self.assertEqual(SpanExportResult.SUCCESS, status) + + mock_post.assert_called_with( + url="http://localhost:9411/api/v2/spans", + data=json.dumps(expected), + headers={"Content-Type": "application/json"}, + ) + + @patch("requests.post") + def test_invalid_response(self, mock_post): + mock_post.return_value = MockResponse(404) + spans = [] + exporter = ZipkinSpanExporter("test-service") + status = exporter.export(spans) + self.assertEqual(SpanExportResult.FAILED_NOT_RETRYABLE, status) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index bddf39a90c..9b981b0817 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -17,10 +17,12 @@ coverage erase cov opentelemetry-api cov opentelemetry-sdk +cov ext/opentelemetry-ext-flask cov ext/opentelemetry-ext-http-requests cov ext/opentelemetry-ext-jaeger cov ext/opentelemetry-ext-opentracing-shim cov ext/opentelemetry-ext-wsgi +cov ext/opentelemetry-ext-zipkin cov examples/opentelemetry-example-app coverage report diff --git a/tox.ini b/tox.ini index 2ded7d1b75..8a4321a95c 100644 --- a/tox.ini +++ b/tox.ini @@ -2,10 +2,10 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} - pypy3-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} - py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} - pypy3-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim} + pypy3-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim} + pypy3-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim} py3{4,5,6,7,8}-coverage ; Coverage is temporarily disabled for pypy3 due to the pytest bug. @@ -38,6 +38,7 @@ changedir = test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests + test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests test-ext-flask: ext/opentelemetry-ext-flask/tests test-example-app: examples/opentelemetry-example-app/tests test-example-basic-tracer: examples/basic_tracer/tests @@ -71,6 +72,7 @@ commands_pre = jaeger: pip install {toxinidir}/opentelemetry-sdk jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-opentracing-shim + zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin ; In order to get a healthy coverage report, ; we have to install packages in editable mode. From b72cab5934e2db2db7a75e574875a477921df469 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 2 Jan 2020 20:57:26 -0800 Subject: [PATCH 0167/1517] Use current logger instead of global (#351) All loggers should be local to the module, rather than use the global. --- .../src/opentelemetry/sdk/trace/export/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 36459c5b73..b70fb01019 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -137,11 +137,11 @@ def on_start(self, span: Span) -> None: def on_end(self, span: Span) -> None: if self.done: - logging.warning("Already shutdown, dropping span.") + logger.warning("Already shutdown, dropping span.") return if len(self.queue) == self.max_queue_size: if not self._spans_dropped: - logging.warning("Queue is full, likely spans will be dropped.") + logger.warning("Queue is full, likely spans will be dropped.") self._spans_dropped = True self.queue.appendleft(span) From b01f7e855c62d78d7e187bb2186811f9d34f9eee Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Sun, 5 Jan 2020 09:41:15 -0800 Subject: [PATCH 0168/1517] Adding DB API integration + MySQL connector integration (#264) Adding the ext.dbapi and ext.mysql package. --- ext/opentelemetry-ext-dbapi/README.rst | 25 ++ ext/opentelemetry-ext-dbapi/setup.cfg | 46 ++++ ext/opentelemetry-ext-dbapi/setup.py | 26 ++ .../src/opentelemetry/ext/dbapi/__init__.py | 225 ++++++++++++++++++ .../src/opentelemetry/ext/dbapi/version.py | 15 ++ ext/opentelemetry-ext-dbapi/tests/__init__.py | 0 .../tests/test_dbapi_integration.py | 181 ++++++++++++++ ext/opentelemetry-ext-mysql/README.rst | 29 +++ ext/opentelemetry-ext-mysql/setup.cfg | 47 ++++ ext/opentelemetry-ext-mysql/setup.py | 26 ++ .../src/opentelemetry/ext/mysql/__init__.py | 43 ++++ .../src/opentelemetry/ext/mysql/version.py | 15 ++ ext/opentelemetry-ext-mysql/tests/__init__.py | 0 .../tests/test_mysql_integration.py | 44 ++++ tox.ini | 13 +- 15 files changed, 731 insertions(+), 4 deletions(-) create mode 100644 ext/opentelemetry-ext-dbapi/README.rst create mode 100644 ext/opentelemetry-ext-dbapi/setup.cfg create mode 100644 ext/opentelemetry-ext-dbapi/setup.py create mode 100644 ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py create mode 100644 ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py create mode 100644 ext/opentelemetry-ext-dbapi/tests/__init__.py create mode 100644 ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py create mode 100644 ext/opentelemetry-ext-mysql/README.rst create mode 100644 ext/opentelemetry-ext-mysql/setup.cfg create mode 100644 ext/opentelemetry-ext-mysql/setup.py create mode 100644 ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py create mode 100644 ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py create mode 100644 ext/opentelemetry-ext-mysql/tests/__init__.py create mode 100644 ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst new file mode 100644 index 0000000000..6dc3121603 --- /dev/null +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -0,0 +1,25 @@ +OpenTelemetry Database API integration +================================= + +The trace integration with Database API supports libraries following the specification. + +.. PEP 249 -- Python Database API Specification v2.0: https://www.python.org/dev/peps/pep-0249/ + +Usage +----- + +.. code:: python + + import mysql.connector + from opentelemetry.trace import tracer + from opentelemetry.ext.dbapi import trace_integration + + + # Ex: mysql.connector + trace_integration(tracer(), mysql.connector, "connect", "mysql") + + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg new file mode 100644 index 0000000000..f0de68dc26 --- /dev/null +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -0,0 +1,46 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-dbapi +description = OpenTelemetry Database API integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-dbapi +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api >= 0.4.dev0 + wrapt >= 1.0.0, < 2.0.0 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-dbapi/setup.py b/ext/opentelemetry-ext-dbapi/setup.py new file mode 100644 index 0000000000..5e1a68ac51 --- /dev/null +++ b/ext/opentelemetry-ext-dbapi/setup.py @@ -0,0 +1,26 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "dbapi", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py new file mode 100644 index 0000000000..7ba1de1795 --- /dev/null +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -0,0 +1,225 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The opentelemetry-ext-dbapi package allows tracing queries made by the +ibraries following Ptyhon Database API specification: +https://www.python.org/dev/peps/pep-0249/ +""" + +import logging +import typing + +import wrapt + +from opentelemetry.trace import SpanKind, Tracer +from opentelemetry.trace.status import Status, StatusCanonicalCode + +logger = logging.getLogger(__name__) + + +def trace_integration( + tracer: Tracer, + connect_module: typing.Callable[..., any], + connect_method_name: str, + database_component: str, + database_type: str = "", + connection_attributes: typing.Dict = None, +): + """Integrate with DB API library. + https://www.python.org/dev/peps/pep-0249/ + Args: + tracer: The :class:`Tracer` to use. + connect_module: Module name where connect method is available. + connect_method_name: The connect method name. + database_component: Database driver name or database name "JDBI", "jdbc", "odbc", "postgreSQL". + database_type: The Database type. For any SQL database, "sql". + connection_attributes: Attribute names for database, port, host and user in Connection object. + """ + + # pylint: disable=unused-argument + def wrap_connect( + wrapped: typing.Callable[..., any], + instance: typing.Any, + args: typing.Tuple[any, any], + kwargs: typing.Dict[any, any], + ): + db_integration = DatabaseApiIntegration( + tracer, + database_component, + database_type=database_type, + connection_attributes=connection_attributes, + ) + return db_integration.wrapped_connection(wrapped, args, kwargs) + + try: + wrapt.wrap_function_wrapper( + connect_module, connect_method_name, wrap_connect + ) + except Exception as ex: # pylint: disable=broad-except + logger.warning("Failed to integrate with DB API. %s", str(ex)) + + +class DatabaseApiIntegration: + # pylint: disable=unused-argument + def __init__( + self, + tracer: Tracer, + database_component: str, + database_type: str = "sql", + connection_attributes=None, + ): + if tracer is None: + raise ValueError("The tracer is not provided.") + self.connection_attributes = connection_attributes + if self.connection_attributes is None: + self.connection_attributes = { + "database": "database", + "port": "port", + "host": "host", + "user": "user", + } + self.tracer = tracer + self.database_component = database_component + self.database_type = database_type + self.connection_props = {} + self.span_attributes = {} + self.name = "" + self.database = "" + + def wrapped_connection( + self, + connect_method: typing.Callable[..., any], + args: typing.Tuple[any, any], + kwargs: typing.Dict[any, any], + ): + """Add object proxy to connection object. + """ + connection = connect_method(*args, **kwargs) + + for key, value in self.connection_attributes.items(): + attribute = getattr(connection, value, None) + if attribute: + self.connection_props[key] = attribute + traced_connection = TracedConnection(connection, self) + return traced_connection + + +# pylint: disable=abstract-method +class TracedConnection(wrapt.ObjectProxy): + + # pylint: disable=unused-argument + def __init__( + self, + connection, + db_api_integration: DatabaseApiIntegration, + *args, + **kwargs + ): + wrapt.ObjectProxy.__init__(self, connection) + self._db_api_integration = db_api_integration + + self._db_api_integration.name = ( + self._db_api_integration.database_component + ) + self._db_api_integration.database = self._db_api_integration.connection_props.get( + "database", "" + ) + if self._db_api_integration.database: + self._db_api_integration.name += ( + "." + self._db_api_integration.database + ) + user = self._db_api_integration.connection_props.get("user") + if user is not None: + self._db_api_integration.span_attributes["db.user"] = user + host = self._db_api_integration.connection_props.get("host") + if host is not None: + self._db_api_integration.span_attributes["net.peer.name"] = host + port = self._db_api_integration.connection_props.get("port") + if port is not None: + self._db_api_integration.span_attributes["net.peer.port"] = port + + def cursor(self, *args, **kwargs): + return TracedCursor( + self.__wrapped__.cursor(*args, **kwargs), self._db_api_integration + ) + + +# pylint: disable=abstract-method +class TracedCursor(wrapt.ObjectProxy): + + # pylint: disable=unused-argument + def __init__( + self, + cursor, + db_api_integration: DatabaseApiIntegration, + *args, + **kwargs + ): + wrapt.ObjectProxy.__init__(self, cursor) + self._db_api_integration = db_api_integration + + def execute(self, *args, **kwargs): + return self._traced_execution( + self.__wrapped__.execute, *args, **kwargs + ) + + def executemany(self, *args, **kwargs): + return self._traced_execution( + self.__wrapped__.executemany, *args, **kwargs + ) + + def callproc(self, *args, **kwargs): + return self._traced_execution( + self.__wrapped__.callproc, *args, **kwargs + ) + + def _traced_execution( + self, + query_method: typing.Callable[..., any], + *args: typing.Tuple[any, any], + **kwargs: typing.Dict[any, any] + ): + + statement = args[0] if args else "" + with self._db_api_integration.tracer.start_as_current_span( + self._db_api_integration.name, kind=SpanKind.CLIENT + ) as span: + span.set_attribute( + "component", self._db_api_integration.database_component + ) + span.set_attribute( + "db.type", self._db_api_integration.database_type + ) + span.set_attribute( + "db.instance", self._db_api_integration.database + ) + span.set_attribute("db.statement", statement) + + for ( + attribute_key, + attribute_value, + ) in self._db_api_integration.span_attributes.items(): + span.set_attribute(attribute_key, attribute_value) + + if len(args) > 1: + span.set_attribute("db.statement.parameters", str(args[1])) + + try: + result = query_method(*args, **kwargs) + span.set_status(Status(StatusCanonicalCode.OK)) + return result + except Exception as ex: # pylint: disable=broad-except + span.set_status(Status(StatusCanonicalCode.UNKNOWN, str(ex))) + raise ex diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py new file mode 100644 index 0000000000..2f792fff80 --- /dev/null +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.4.dev0" diff --git a/ext/opentelemetry-ext-dbapi/tests/__init__.py b/ext/opentelemetry-ext-dbapi/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py new file mode 100644 index 0000000000..f5d1299e83 --- /dev/null +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -0,0 +1,181 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from opentelemetry import trace as trace_api +from opentelemetry.ext.dbapi import DatabaseApiIntegration + + +class TestDBApiIntegration(unittest.TestCase): + def setUp(self): + self.tracer = trace_api.Tracer() + self.span = MockSpan() + self.start_current_span_patcher = mock.patch.object( + self.tracer, + "start_as_current_span", + autospec=True, + spec_set=True, + return_value=self.span, + ) + + self.start_as_current_span = self.start_current_span_patcher.start() + + def tearDown(self): + self.start_current_span_patcher.stop() + + def test_span_succeeded(self): + connection_props = { + "database": "testdatabase", + "server_host": "testhost", + "server_port": 123, + "user": "testuser", + } + connection_attributes = { + "database": "database", + "port": "server_port", + "host": "server_host", + "user": "user", + } + db_integration = DatabaseApiIntegration( + self.tracer, "testcomponent", "testtype", connection_attributes + ) + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, connection_props + ) + cursor = mock_connection.cursor() + cursor.execute("Test query", ("param1Value", False)) + self.assertTrue(self.start_as_current_span.called) + self.assertEqual( + self.start_as_current_span.call_args[0][0], + "testcomponent.testdatabase", + ) + self.assertIs( + self.start_as_current_span.call_args[1]["kind"], + trace_api.SpanKind.CLIENT, + ) + self.assertEqual(self.span.attributes["component"], "testcomponent") + self.assertEqual(self.span.attributes["db.type"], "testtype") + self.assertEqual(self.span.attributes["db.instance"], "testdatabase") + self.assertEqual(self.span.attributes["db.statement"], "Test query") + self.assertEqual( + self.span.attributes["db.statement.parameters"], + "('param1Value', False)", + ) + self.assertEqual(self.span.attributes["db.user"], "testuser") + self.assertEqual(self.span.attributes["net.peer.name"], "testhost") + self.assertEqual(self.span.attributes["net.peer.port"], 123) + self.assertIs( + self.span.status.canonical_code, + trace_api.status.StatusCanonicalCode.OK, + ) + + def test_span_failed(self): + db_integration = DatabaseApiIntegration(self.tracer, "testcomponent") + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, {} + ) + cursor = mock_connection.cursor() + try: + cursor.execute("Test query", throw_exception=True) + except Exception: # pylint: disable=broad-except + self.assertEqual( + self.span.attributes["db.statement"], "Test query" + ) + self.assertIs( + self.span.status.canonical_code, + trace_api.status.StatusCanonicalCode.UNKNOWN, + ) + self.assertEqual(self.span.status.description, "Test Exception") + + def test_executemany(self): + db_integration = DatabaseApiIntegration(self.tracer, "testcomponent") + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, {} + ) + cursor = mock_connection.cursor() + cursor.executemany("Test query") + self.assertTrue(self.start_as_current_span.called) + self.assertEqual(self.span.attributes["db.statement"], "Test query") + + def test_callproc(self): + db_integration = DatabaseApiIntegration(self.tracer, "testcomponent") + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, {} + ) + cursor = mock_connection.cursor() + cursor.callproc("Test stored procedure") + self.assertTrue(self.start_as_current_span.called) + self.assertEqual( + self.span.attributes["db.statement"], "Test stored procedure" + ) + + +# pylint: disable=unused-argument +def mock_connect(*args, **kwargs): + database = kwargs.get("database") + server_host = kwargs.get("server_host") + server_port = kwargs.get("server_port") + user = kwargs.get("user") + return MockConnection(database, server_port, server_host, user) + + +class MockConnection: + def __init__(self, database, server_port, server_host, user): + self.database = database + self.server_port = server_port + self.server_host = server_host + self.user = user + + # pylint: disable=no-self-use + def cursor(self): + return MockCursor() + + +class MockCursor: + # pylint: disable=unused-argument, no-self-use + def execute(self, query, params=None, throw_exception=False): + if throw_exception: + raise Exception("Test Exception") + + # pylint: disable=unused-argument, no-self-use + def executemany(self, query, params=None, throw_exception=False): + if throw_exception: + raise Exception("Test Exception") + + # pylint: disable=unused-argument, no-self-use + def callproc(self, query, params=None, throw_exception=False): + if throw_exception: + raise Exception("Test Exception") + + +class MockSpan: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + return False + + def __init__(self): + self.status = None + self.name = "" + self.kind = trace_api.SpanKind.INTERNAL + self.attributes = {} + + def set_attribute(self, key, value): + self.attributes[key] = value + + def set_status(self, status): + self.status = status diff --git a/ext/opentelemetry-ext-mysql/README.rst b/ext/opentelemetry-ext-mysql/README.rst new file mode 100644 index 0000000000..e819a63769 --- /dev/null +++ b/ext/opentelemetry-ext-mysql/README.rst @@ -0,0 +1,29 @@ +OpenTelemetry MySQL integration +================================= + +The integration with MySQL supports the `mysql-connector`_ library and is specified +to ``trace_integration`` using ``'MySQL'``. + +.. mysql-connector: https://pypi.org/project/mysql-connector/ + +Usage +----- + +.. code:: python + + import mysql.connector + from opentelemetry.trace import tracer + from opentelemetry.ext.mysql import trace_integration + + trace_integration(tracer()) + cnx = mysql.connector.connect(database='MySQL_Database') + cursor = cnx.cursor() + cursor.execute("INSERT INTO test (testField) VALUES (123)" + cursor.close() + cnx.close() + + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg new file mode 100644 index 0000000000..fdc608bb3d --- /dev/null +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -0,0 +1,47 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-mysql +description = OpenTelemetry MySQL integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-mysql +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api >= 0.4.dev0 + mysql-connector-python ~= 8.0 + wrapt >= 1.0.0, < 2.0.0 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-mysql/setup.py b/ext/opentelemetry-ext-mysql/setup.py new file mode 100644 index 0000000000..b2c62679e1 --- /dev/null +++ b/ext/opentelemetry-ext-mysql/setup.py @@ -0,0 +1,26 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "mysql", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py new file mode 100644 index 0000000000..9c8c3e9da7 --- /dev/null +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -0,0 +1,43 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The opentelemetry-ext-mysql package allows tracing MySQL queries made by the +MySQL Connector/Python library. +""" + +import mysql.connector + +from opentelemetry.ext.dbapi import trace_integration as db_integration +from opentelemetry.trace import Tracer + + +def trace_integration(tracer: Tracer): + """Integrate with MySQL Connector/Python library. + https://dev.mysql.com/doc/connector-python/en/ + """ + connection_attributes = { + "database": "database", + "port": "server_port", + "host": "server_host", + "user": "user", + } + db_integration( + tracer, + mysql.connector, + "connect", + "mysql", + "sql", + connection_attributes, + ) diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py new file mode 100644 index 0000000000..2f792fff80 --- /dev/null +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.4.dev0" diff --git a/ext/opentelemetry-ext-mysql/tests/__init__.py b/ext/opentelemetry-ext-mysql/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py new file mode 100644 index 0000000000..1bcd851750 --- /dev/null +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -0,0 +1,44 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +import mysql.connector + +from opentelemetry import trace as trace_api +from opentelemetry.ext.mysql import trace_integration + + +class TestMysqlIntegration(unittest.TestCase): + def test_trace_integration(self): + tracer = trace_api.Tracer() + span = mock.create_autospec(trace_api.Span, spec_set=True) + start_current_span_patcher = mock.patch.object( + tracer, + "start_as_current_span", + autospec=True, + spec_set=True, + return_value=span, + ) + start_as_current_span = start_current_span_patcher.start() + + with mock.patch("mysql.connector.connect") as mock_connect: + mock_connect.get.side_effect = mysql.connector.MySQLConnection() + trace_integration(tracer) + cnx = mysql.connector.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + self.assertTrue(start_as_current_span.called) diff --git a/tox.ini b/tox.ini index 8a4321a95c..9f2741db82 100644 --- a/tox.ini +++ b/tox.ini @@ -2,10 +2,10 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim} - pypy3-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim} - py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim} - pypy3-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim} + pypy3-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim} + pypy3-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim} py3{4,5,6,7,8}-coverage ; Coverage is temporarily disabled for pypy3 due to the pytest bug. @@ -36,6 +36,8 @@ changedir = test-sdk: opentelemetry-sdk/tests test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests + test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests + test-ext-mysql: ext/opentelemetry-ext-mysql/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests @@ -67,6 +69,9 @@ commands_pre = wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-testutil wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-wsgi flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] + dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi + mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi + mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests jaeger: pip install {toxinidir}/opentelemetry-sdk From 8a6b408ee208ae49ee590dd085ec8ad7027946fc Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 7 Jan 2020 09:58:27 -0800 Subject: [PATCH 0169/1517] Using InMemorySpanExporter for wsgi/flask tests (#306) The InMemorySpanExporter provides a friendly interface to retrieving span information, reducing the need for mocking in unit tests. Signed-off-by: Alex Boten --- .../tests/test_flask_integration.py | 142 +++++++----------- .../ext/testutil/wsgitestutil.py | 50 +++--- .../tests/test_wsgi_middleware.py | 17 ++- tox.ini | 1 + 4 files changed, 90 insertions(+), 120 deletions(-) diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py index b943a25f22..09e62b7ba1 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py @@ -13,7 +13,6 @@ # limitations under the License. import unittest -from unittest import mock from flask import Flask from werkzeug.test import Client @@ -24,18 +23,28 @@ from opentelemetry.ext.testutil.wsgitestutil import WsgiTestBase +def expected_attributes(override_attributes): + default_attributes = { + "component": "http", + "http.method": "GET", + "http.server_name": "localhost", + "http.scheme": "http", + "host.port": 80, + "http.host": "localhost", + "http.target": "/", + "http.flavor": "1.1", + "http.status_text": "OK", + "http.status_code": 200, + } + for key, val in override_attributes.items(): + default_attributes[key] = val + return default_attributes + + class TestFlaskIntegration(WsgiTestBase): def setUp(self): super().setUp() - self.span_attrs = {} - - def setspanattr(key, value): - self.assertIsInstance(key, str) - self.span_attrs[key] = value - - self.span.set_attribute = setspanattr - self.app = Flask(__name__) def hello_endpoint(helloid): @@ -49,100 +58,57 @@ def hello_endpoint(helloid): self.client = Client(self.app, BaseResponse) def test_simple(self): - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) - - self.start_span.assert_called_with( - "hello_endpoint", - trace_api.INVALID_SPAN_CONTEXT, - kind=trace_api.SpanKind.SERVER, - attributes={ - "component": "http", - "http.method": "GET", - "http.server_name": "localhost", - "http.scheme": "http", - "host.port": 80, - "http.host": "localhost", + expected_attrs = expected_attributes( + { "http.target": "/hello/123", - "http.flavor": "1.1", "http.route": "/hello/", - }, - start_time=mock.ANY, - ) - - # TODO: Change this test to use the SDK, as mocking becomes painful - - self.assertEqual( - self.span_attrs, - {"http.status_code": 200, "http.status_text": "OK"}, + } ) + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "hello_endpoint") + self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) def test_404(self): - resp = self.client.post("/bye") - self.assertEqual(404, resp.status_code) - resp.close() - - self.start_span.assert_called_with( - "/bye", - trace_api.INVALID_SPAN_CONTEXT, - kind=trace_api.SpanKind.SERVER, - attributes={ - "component": "http", + expected_attrs = expected_attributes( + { "http.method": "POST", - "http.server_name": "localhost", - "http.scheme": "http", - "host.port": 80, - "http.host": "localhost", "http.target": "/bye", - "http.flavor": "1.1", - }, - start_time=mock.ANY, + "http.status_text": "NOT FOUND", + "http.status_code": 404, + } ) - # Nope, this uses Tracer.use_span(end_on_exit) - # self.assertEqual(1, self.span.end.call_count) - # TODO: Change this test to use the SDK, as mocking becomes painful - - self.assertEqual( - self.span_attrs, - {"http.status_code": 404, "http.status_text": "NOT FOUND"}, - ) - - def test_internal_error(self): - resp = self.client.get("/hello/500") - self.assertEqual(500, resp.status_code) + resp = self.client.post("/bye") + self.assertEqual(404, resp.status_code) resp.close() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "/bye") + self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) - self.start_span.assert_called_with( - "hello_endpoint", - trace_api.INVALID_SPAN_CONTEXT, - kind=trace_api.SpanKind.SERVER, - attributes={ - "component": "http", - "http.method": "GET", - "http.server_name": "localhost", - "http.scheme": "http", - "host.port": 80, - "http.host": "localhost", + def test_internal_error(self): + expected_attrs = expected_attributes( + { "http.target": "/hello/500", - "http.flavor": "1.1", "http.route": "/hello/", - }, - start_time=mock.ANY, - ) - - # Nope, this uses Tracer.use_span(end_on_exit) - # self.assertEqual(1, self.span.end.call_count) - # TODO: Change this test to use the SDK, as mocking becomes painful - - self.assertEqual( - self.span_attrs, - { - "http.status_code": 500, "http.status_text": "INTERNAL SERVER ERROR", - }, + "http.status_code": 500, + } ) + resp = self.client.get("/hello/500") + self.assertEqual(500, resp.status_code) + resp.close() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "hello_endpoint") + self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) if __name__ == "__main__": diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py index d30f007066..5f99d08df0 100644 --- a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py +++ b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py @@ -1,32 +1,38 @@ import io import unittest -import unittest.mock as mock import wsgiref.util as wsgiref_util +from importlib import reload from opentelemetry import trace as trace_api +from opentelemetry.sdk.trace import TracerSource, export +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +_MEMORY_EXPORTER = None class WsgiTestBase(unittest.TestCase): - def setUp(self): - self.span = mock.create_autospec(trace_api.Span, spec_set=True) - tracer = trace_api.Tracer() - self.get_tracer_patcher = mock.patch.object( - trace_api.TracerSource, - "get_tracer", - autospec=True, - spec_set=True, - return_value=tracer, - ) - self.get_tracer_patcher.start() - - self.start_span_patcher = mock.patch.object( - tracer, - "start_span", - autospec=True, - spec_set=True, - return_value=self.span, + @classmethod + def setUpClass(cls): + global _MEMORY_EXPORTER # pylint:disable=global-statement + trace_api.set_preferred_tracer_source_implementation( + lambda T: TracerSource() ) - self.start_span = self.start_span_patcher.start() + tracer_source = trace_api.tracer_source() + _MEMORY_EXPORTER = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(_MEMORY_EXPORTER) + tracer_source.add_span_processor(span_processor) + + @classmethod + def tearDownClass(cls): + reload(trace_api) + + def setUp(self): + + self.memory_exporter = _MEMORY_EXPORTER + self.memory_exporter.clear() + self.write_buffer = io.BytesIO() self.write = self.write_buffer.write @@ -37,10 +43,6 @@ def setUp(self): self.response_headers = None self.exc_info = None - def tearDown(self): - self.get_tracer_patcher.stop() - self.start_span_patcher.stop() - def start_response(self, status, response_headers, exc_info=None): self.status = status self.response_headers = response_headers diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index a78b4d19f0..1912dd0079 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -78,10 +78,8 @@ def validate_response(self, response, error=None): while True: try: value = next(response) - self.assertEqual(0, self.span.end.call_count) self.assertEqual(value, b"*") except StopIteration: - self.span.end.assert_called_once_with() break self.assertEqual(self.status, "200 OK") @@ -95,12 +93,13 @@ def validate_response(self, response, error=None): else: self.assertIsNone(self.exc_info) - # Verify that start_span has been called - self.start_span.assert_called_with( - "/", - trace_api.INVALID_SPAN_CONTEXT, - kind=trace_api.SpanKind.SERVER, - attributes={ + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "/") + self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) + self.assertEqual( + span_list[0].attributes, + { "component": "http", "http.method": "GET", "http.server_name": "127.0.0.1", @@ -109,6 +108,8 @@ def validate_response(self, response, error=None): "http.host": "127.0.0.1", "http.flavor": "1.0", "http.url": "http://127.0.0.1/", + "http.status_text": "OK", + "http.status_code": 200, }, ) diff --git a/tox.ini b/tox.ini index 9f2741db82..1aca91e75e 100644 --- a/tox.ini +++ b/tox.ini @@ -68,6 +68,7 @@ commands_pre = ext: pip install {toxinidir}/opentelemetry-api wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-testutil wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-wsgi + wsgi,flask: pip install {toxinidir}/opentelemetry-sdk flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi From a89bbc882beb44884d174d8a5c08ff36d6c5226e Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Tue, 7 Jan 2020 10:01:46 -0800 Subject: [PATCH 0170/1517] Updating network connection attributes names to latest in spec (#350) Updating attributes names to match https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-span-general.md#general-network-connection-attributes --- .../src/opentelemetry/ext/pymongo/__init__.py | 4 ++-- .../tests/test_pymongo_integration.py | 4 ++-- .../src/opentelemetry/ext/wsgi/__init__.py | 8 +++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index 8c49892e0f..3c95a30615 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -57,8 +57,8 @@ def started(self, event: monitoring.CommandStartedEvent): span.set_attribute("db.instance", event.database_name) span.set_attribute("db.statement", statement) if event.connection_id is not None: - span.set_attribute("peer.hostname", event.connection_id[0]) - span.set_attribute("peer.port", event.connection_id[1]) + span.set_attribute("net.peer.name", event.connection_id[0]) + span.set_attribute("net.peer.port", event.connection_id[1]) # pymongo specific, not specified by spec span.set_attribute("db.mongo.operation_id", event.operation_id) diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py index 95f0ae3413..6c99e09e71 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py @@ -54,8 +54,8 @@ def test_started(self): self.assertEqual(span.attributes["db.type"], "mongodb") self.assertEqual(span.attributes["db.instance"], "database_name") self.assertEqual(span.attributes["db.statement"], "command_name find") - self.assertEqual(span.attributes["peer.hostname"], "test.com") - self.assertEqual(span.attributes["peer.port"], "1234") + self.assertEqual(span.attributes["net.peer.name"], "test.com") + self.assertEqual(span.attributes["net.peer.port"], "1234") self.assertEqual( span.attributes["db.mongo.operation_id"], "operation_id" ) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 6581662d59..e6751f34ce 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -103,14 +103,12 @@ def collect_request_attributes(environ): remote_addr = environ.get("REMOTE_ADDR") if remote_addr: - result[ - "peer.ipv6" if ":" in remote_addr else "peer.ipv4" - ] = remote_addr + result["net.peer.ip"] = remote_addr remote_host = environ.get("REMOTE_HOST") if remote_host and remote_host != remote_addr: - result["peer.hostname"] = remote_host + result["net.peer.name"] = remote_host - setifnotnone(result, "peer.port", environ.get("REMOTE_PORT")) + setifnotnone(result, "net.peer.port", environ.get("REMOTE_PORT")) flavor = environ.get("SERVER_PROTOCOL", "") if flavor.upper().startswith(_HTTP_VERSION_PREFIX): flavor = flavor[len(_HTTP_VERSION_PREFIX) :] From c24b3b5032cd27dd4912348e633abcd2bb48d53a Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 9 Jan 2020 11:38:32 -0800 Subject: [PATCH 0171/1517] Remove @reyang from maintainers in README (#356) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0129772c61..a8422735ef 100644 --- a/README.md +++ b/README.md @@ -120,13 +120,13 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Carlos Alberto Cortez](https://github.com/carlosalberto), LightStep - [Christian Neumüller](https://github.com/Oberon00), Dynatrace - [Leighton Chen](https://github.com/lzchen), Microsoft +- [Reiley Yang](https://github.com/reyang), Microsoft *Find more about the approver role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver).* Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-telemetry/teams/python-maintainers)): - [Chris Kleinknecht](https://github.com/c24t), Google -- [Reiley Yang](https://github.com/reyang), Microsoft - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Zillow Group *Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).* From 908150caada72a1c3ff63e944b39bd0070578b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez?= Date: Mon, 13 Jan 2020 20:52:23 +0100 Subject: [PATCH 0172/1517] Remove pinned multidict (#364) --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index 1aca91e75e..642a8a556b 100644 --- a/tox.ini +++ b/tox.ini @@ -142,8 +142,6 @@ deps = # needed for example trace integration flask~=1.1 requests~=2.7 - # Pinned due to https://github.com/open-telemetry/opentelemetry-python/issues/329 - multidict==4.6.1 commands_pre = pip install -e {toxinidir}/opentelemetry-api \ From f0ba817a55556698f2da76d9d46066d1069e1622 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 13 Jan 2020 14:25:10 -0600 Subject: [PATCH 0173/1517] Protect start_time and end_time from being set manually by the user (#363) Fixes #361 --- .../tests/test_jaeger_exporter.py | 12 ++++++------ .../tests/test_zipkin_exporter.py | 12 ++++++------ .../src/opentelemetry/sdk/trace/__init__.py | 18 ++++++++++++++---- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py index 9c7e4b044c..f8ead96ef0 100644 --- a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py +++ b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py @@ -169,18 +169,18 @@ def test_translate_to_jaeger(self): trace.Span(name=span_names[2], context=other_context, parent=None), ] - otel_spans[0].start_time = start_times[0] + otel_spans[0].start(start_time=start_times[0]) # added here to preserve order otel_spans[0].set_attribute("key_bool", False) otel_spans[0].set_attribute("key_string", "hello_world") otel_spans[0].set_attribute("key_float", 111.22) - otel_spans[0].end_time = end_times[0] + otel_spans[0].end(end_time=end_times[0]) - otel_spans[1].start_time = start_times[1] - otel_spans[1].end_time = end_times[1] + otel_spans[1].start(start_time=start_times[1]) + otel_spans[1].end(end_time=end_times[1]) - otel_spans[2].start_time = start_times[2] - otel_spans[2].end_time = end_times[2] + otel_spans[2].start(start_time=start_times[2]) + otel_spans[2].end(end_time=end_times[2]) # pylint: disable=protected-access spans = jaeger_exporter._translate_to_jaeger(otel_spans) diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py index 745c662f53..e2bdb41305 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -154,18 +154,18 @@ def test_export(self): trace.Span(name=span_names[2], context=other_context, parent=None), ] - otel_spans[0].start_time = start_times[0] + otel_spans[0].start(start_time=start_times[0]) # added here to preserve order otel_spans[0].set_attribute("key_bool", False) otel_spans[0].set_attribute("key_string", "hello_world") otel_spans[0].set_attribute("key_float", 111.22) - otel_spans[0].end_time = end_times[0] + otel_spans[0].end(end_time=end_times[0]) - otel_spans[1].start_time = start_times[1] - otel_spans[1].end_time = end_times[1] + otel_spans[1].start(start_time=start_times[1]) + otel_spans[1].end(end_time=end_times[1]) - otel_spans[2].start_time = start_times[2] - otel_spans[2].end_time = end_times[2] + otel_spans[2].start(start_time=start_times[2]) + otel_spans[2].end(end_time=end_times[2]) service_name = "test-service" local_endpoint = { diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 3035ae7ef9..b969587eeb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -171,10 +171,18 @@ def __init__( else: self.links = BoundedList.from_seq(MAX_NUM_LINKS, links) - self.end_time = None # type: Optional[int] - self.start_time = None # type: Optional[int] + self._end_time = None # type: Optional[int] + self._start_time = None # type: Optional[int] self.instrumentation_info = instrumentation_info + @property + def start_time(self): + return self._start_time + + @property + def end_time(self): + return self._end_time + def __repr__(self): return '{}(name="{}", context={})'.format( type(self).__name__, self.name, self.context @@ -243,7 +251,7 @@ def start(self, start_time: Optional[int] = None) -> None: return has_started = self.start_time is not None if not has_started: - self.start_time = ( + self._start_time = ( start_time if start_time is not None else time_ns() ) if has_started: @@ -259,7 +267,9 @@ def end(self, end_time: Optional[int] = None) -> None: raise RuntimeError("Calling end() on a not started span.") has_ended = self.end_time is not None if not has_ended: - self.end_time = end_time if end_time is not None else time_ns() + self._end_time = ( + end_time if end_time is not None else time_ns() + ) if has_ended: logger.warning("Calling end() on an ended span.") return From ada53ff5120e3554fe055b1c953b0c3087628ef7 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 15 Jan 2020 13:40:21 -0800 Subject: [PATCH 0174/1517] Separate no-op from interfaces (#311) Fixes #66 --- .../tests/test_dbapi_integration.py | 2 +- .../tests/test_requests_integration.py | 4 +- .../tests/test_mysql_integration.py | 2 +- .../src/opentelemetry/metrics/__init__.py | 36 ++++- .../src/opentelemetry/trace/__init__.py | 125 +++++++++++++++--- .../tests/metrics/test_metrics.py | 2 +- .../tests/test_implementation.py | 20 ++- opentelemetry-api/tests/test_loader.py | 9 +- opentelemetry-api/tests/trace/test_tracer.py | 4 +- 9 files changed, 166 insertions(+), 38 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py index f5d1299e83..afe5a49a02 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -21,7 +21,7 @@ class TestDBApiIntegration(unittest.TestCase): def setUp(self): - self.tracer = trace_api.Tracer() + self.tracer = trace_api.DefaultTracer() self.span = MockSpan() self.start_current_span_patcher = mock.patch.object( self.tracer, diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index 35cf3110f3..de659f20e1 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -29,8 +29,8 @@ class TestRequestsIntegration(unittest.TestCase): # TODO: Copy & paste from test_wsgi_middleware def setUp(self): self.span_attrs = {} - self.tracer_source = trace.TracerSource() - self.tracer = trace.Tracer() + self.tracer_source = trace.DefaultTracerSource() + self.tracer = trace.DefaultTracer() self.get_tracer_patcher = mock.patch.object( self.tracer_source, "get_tracer", diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py index 1bcd851750..3b6eaa0c64 100644 --- a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -23,7 +23,7 @@ class TestMysqlIntegration(unittest.TestCase): def test_trace_integration(self): - tracer = trace_api.Tracer() + tracer = trace_api.DefaultTracer() span = mock.create_autospec(trace_api.Span, spec_set=True) start_current_span_patcher = mock.patch.object( tracer, diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 4946300e15..947d57b976 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -184,7 +184,7 @@ def record(self, label_set: LabelSet, value: ValueT) -> None: # pylint: disable=unused-argument -class Meter: +class Meter(abc.ABC): """An interface to allow the recording of metrics. `Metric` s are used for recording pre-defined aggregation (gauge and @@ -192,6 +192,7 @@ class Meter: for the exported metric are deferred. """ + @abc.abstractmethod def record_batch( self, label_set: LabelSet, @@ -211,6 +212,7 @@ def record_batch( corresponding value to record for that metric. """ + @abc.abstractmethod def create_metric( self, name: str, @@ -236,9 +238,8 @@ def create_metric( Returns: A new ``metric_type`` metric with values of ``value_type``. """ - # pylint: disable=no-self-use - return DefaultMetric() + @abc.abstractmethod def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": """Gets a `LabelSet` with the given labels. @@ -247,6 +248,33 @@ def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": Returns: A `LabelSet` object canonicalized using the given input. """ + + +class DefaultMeter(Meter): + """The default Meter used when no Meter implementation is available.""" + + def record_batch( + self, + label_set: LabelSet, + record_tuples: Sequence[Tuple["Metric", ValueT]], + ) -> None: + pass + + def create_metric( + self, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + metric_type: Type[MetricT], + label_keys: Sequence[str] = (), + enabled: bool = True, + monotonic: bool = False, + ) -> "Metric": + # pylint: disable=no-self-use + return DefaultMetric() + + def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": # pylint: disable=no-self-use return DefaultLabelSet() @@ -269,7 +297,7 @@ def meter() -> Meter: if _METER is None: # pylint:disable=protected-access - _METER = loader._load_impl(Meter, _METER_FACTORY) + _METER = loader._load_impl(DefaultMeter, _METER_FACTORY) del _METER_FACTORY return _METER diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index e426d11a1a..014e82b3bc 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -66,6 +66,7 @@ by `tracer_source`. """ +import abc import enum import types as python_types import typing @@ -151,9 +152,10 @@ class SpanKind(enum.Enum): CONSUMER = 4 -class Span: +class Span(abc.ABC): """A span represents a single operation within a trace.""" + @abc.abstractmethod def end(self, end_time: typing.Optional[int] = None) -> None: """Sets the current time as the span's end time. @@ -163,6 +165,7 @@ def end(self, end_time: typing.Optional[int] = None) -> None: implementations are free to ignore or raise on further calls. """ + @abc.abstractmethod def get_context(self) -> "SpanContext": """Gets the span's SpanContext. @@ -172,15 +175,15 @@ def get_context(self) -> "SpanContext": Returns: A :class:`.SpanContext` with a copy of this span's immutable state. """ - # pylint: disable=no-self-use - return INVALID_SPAN_CONTEXT + @abc.abstractmethod def set_attribute(self, key: str, value: types.AttributeValue) -> None: """Sets an Attribute. Sets a single Attribute with the key and value passed as arguments. """ + @abc.abstractmethod def add_event( self, name: str, @@ -194,12 +197,14 @@ def add_event( timestamp if the `timestamp` argument is omitted. """ + @abc.abstractmethod def add_lazy_event(self, event: Event) -> None: """Adds an `Event`. Adds an `Event` that has previously been created. """ + @abc.abstractmethod def update_name(self, name: str) -> None: """Updates the `Span` name. @@ -209,15 +214,15 @@ def update_name(self, name: str) -> None: on the implementation. """ + @abc.abstractmethod def is_recording_events(self) -> bool: """Returns whether this span will be recorded. Returns true if this Span is active and recording information like events with the add_event operation and attributes using set_attribute. """ - # pylint: disable=no-self-use - return False + @abc.abstractmethod def set_status(self, status: Status) -> None: """Sets the Status of the Span. If used, this will override the default Span status, which is OK. @@ -362,6 +367,29 @@ def get_context(self) -> "SpanContext": def is_recording_events(self) -> bool: return False + def end(self, end_time: typing.Optional[int] = None) -> None: + pass + + def set_attribute(self, key: str, value: types.AttributeValue) -> None: + pass + + def add_event( + self, + name: str, + attributes: types.Attributes = None, + timestamp: typing.Optional[int] = None, + ) -> None: + pass + + def add_lazy_event(self, event: Event) -> None: + pass + + def update_name(self, name: str) -> None: + pass + + def set_status(self, status: Status) -> None: + pass + INVALID_SPAN_ID = 0x0000000000000000 INVALID_TRACE_ID = 0x00000000000000000000000000000000 @@ -374,8 +402,8 @@ def is_recording_events(self) -> bool: INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) -class TracerSource: - # pylint:disable=no-self-use,unused-argument +class TracerSource(abc.ABC): + @abc.abstractmethod def get_tracer( self, instrumenting_module_name: str, @@ -402,10 +430,24 @@ def get_tracer( instrumenting library. Usually this should be the same as ``pkg_resources.get_distribution(instrumenting_library_name).version``. """ - return Tracer() -class Tracer: +class DefaultTracerSource(TracerSource): + """The default TracerSource, used when no implementation is available. + + All operations are no-op. + """ + + def get_tracer( + self, + instrumenting_module_name: str, + instrumenting_library_version: str = "", + ) -> "Tracer": + # pylint:disable=no-self-use,unused-argument + return DefaultTracer() + + +class Tracer(abc.ABC): """Handles span creation and in-process context propagation. This class provides methods for manipulating the context, creating spans, @@ -414,8 +456,9 @@ class Tracer: # Constant used to represent the current span being used as a parent. # This is the default behavior when creating spans. - CURRENT_SPAN = Span() + CURRENT_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) + @abc.abstractmethod def get_current_span(self) -> "Span": """Gets the currently active span from the context. @@ -426,9 +469,8 @@ def get_current_span(self) -> "Span": The currently active :class:`.Span`, or a placeholder span with an invalid :class:`.SpanContext`. """ - # pylint: disable=no-self-use - return INVALID_SPAN + @abc.abstractmethod def start_span( self, name: str, @@ -478,10 +520,9 @@ def start_span( Returns: The newly-created span. """ - # pylint: disable=unused-argument,no-self-use - return INVALID_SPAN @contextmanager # type: ignore + @abc.abstractmethod def start_as_current_span( self, name: str, @@ -531,10 +572,8 @@ def start_as_current_span( The newly-created span. """ - # pylint: disable=unused-argument,no-self-use - yield INVALID_SPAN - @contextmanager # type: ignore + @abc.abstractmethod def use_span( self, span: "Span", end_on_exit: bool = False ) -> typing.Iterator[None]: @@ -552,6 +591,47 @@ def use_span( end_on_exit: Whether to end the span automatically when leaving the context manager. """ + + +class DefaultTracer(Tracer): + """The default Tracer, used when no Tracer implementation is available. + + All operations are no-op. + """ + + def get_current_span(self) -> "Span": + # pylint: disable=no-self-use + return INVALID_SPAN + + def start_span( + self, + name: str, + parent: ParentSpan = Tracer.CURRENT_SPAN, + kind: SpanKind = SpanKind.INTERNAL, + attributes: typing.Optional[types.Attributes] = None, + links: typing.Sequence[Link] = (), + start_time: typing.Optional[int] = None, + set_status_on_exception: bool = True, + ) -> "Span": + # pylint: disable=unused-argument,no-self-use + return INVALID_SPAN + + @contextmanager # type: ignore + def start_as_current_span( + self, + name: str, + parent: ParentSpan = Tracer.CURRENT_SPAN, + kind: SpanKind = SpanKind.INTERNAL, + attributes: typing.Optional[types.Attributes] = None, + links: typing.Sequence[Link] = (), + ) -> typing.Iterator["Span"]: + # pylint: disable=unused-argument,no-self-use + yield INVALID_SPAN + + @contextmanager # type: ignore + def use_span( + self, span: "Span", end_on_exit: bool = False + ) -> typing.Iterator[None]: # pylint: disable=unused-argument,no-self-use yield @@ -576,9 +656,14 @@ def tracer_source() -> TracerSource: if _TRACER_SOURCE is None: # pylint:disable=protected-access - _TRACER_SOURCE = loader._load_impl( - TracerSource, _TRACER_SOURCE_FACTORY - ) + try: + _TRACER_SOURCE = loader._load_impl( + TracerSource, _TRACER_SOURCE_FACTORY # type: ignore + ) + except TypeError: + # if we raised an exception trying to instantiate an + # abstract class, default to no-op tracer impl + _TRACER_SOURCE = DefaultTracerSource() del _TRACER_SOURCE_FACTORY return _TRACER_SOURCE diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index f8610a6fa4..a8959266b2 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -20,7 +20,7 @@ # pylint: disable=no-self-use class TestMeter(unittest.TestCase): def setUp(self): - self.meter = metrics.Meter() + self.meter = metrics.DefaultMeter() def test_record_batch(self): counter = metrics.Counter() diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index cd126229f9..c7d1d453a1 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -26,7 +26,12 @@ class TestAPIOnlyImplementation(unittest.TestCase): """ def test_tracer(self): - tracer_source = trace.TracerSource() + with self.assertRaises(TypeError): + # pylint: disable=abstract-class-instantiated + trace.TracerSource() # type:ignore + + def test_default_tracer(self): + tracer_source = trace.DefaultTracerSource() tracer = tracer_source.get_tracer(__name__) with tracer.start_span("test") as span: self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) @@ -40,9 +45,9 @@ def test_tracer(self): self.assertIs(span2.is_recording_events(), False) def test_span(self): - span = trace.Span() - self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) - self.assertIs(span.is_recording_events(), False) + with self.assertRaises(TypeError): + # pylint: disable=abstract-class-instantiated + trace.Span() # type:ignore def test_default_span(self): span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) @@ -50,6 +55,11 @@ def test_default_span(self): self.assertIs(span.is_recording_events(), False) def test_meter(self): - meter = metrics.Meter() + with self.assertRaises(TypeError): + # pylint: disable=abstract-class-instantiated + metrics.Meter() # type:ignore + + def test_default_meter(self): + meter = metrics.DefaultMeter() metric = meter.create_metric("", "", "", float, metrics.Counter) self.assertIsInstance(metric, metrics.DefaultMetric) diff --git a/opentelemetry-api/tests/test_loader.py b/opentelemetry-api/tests/test_loader.py index 8ac397afcb..eda241615f 100644 --- a/opentelemetry-api/tests/test_loader.py +++ b/opentelemetry-api/tests/test_loader.py @@ -25,7 +25,12 @@ class DummyTracerSource(trace.TracerSource): - pass + def get_tracer( + self, + instrumenting_module_name: str, + instrumenting_library_version: str = "", + ) -> "trace.Tracer": + return trace.DefaultTracer() def get_opentelemetry_implementation(type_): @@ -49,7 +54,7 @@ def setUp(self): def test_get_default(self): tracer_source = trace.tracer_source() - self.assertIs(type(tracer_source), trace.TracerSource) + self.assertIs(type(tracer_source), trace.DefaultTracerSource) def test_preferred_impl(self): trace.set_preferred_tracer_source_implementation( diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py index b57f2ff6d2..20c218ad8f 100644 --- a/opentelemetry-api/tests/trace/test_tracer.py +++ b/opentelemetry-api/tests/trace/test_tracer.py @@ -19,7 +19,7 @@ class TestTracer(unittest.TestCase): def setUp(self): - self.tracer = trace.Tracer() + self.tracer = trace.DefaultTracer() def test_get_current_span(self): span = self.tracer.get_current_span() @@ -34,6 +34,6 @@ def test_start_as_current_span(self): self.assertIsInstance(span, trace.Span) def test_use_span(self): - span = trace.Span() + span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) with self.tracer.use_span(span): pass From 3883e0a284c14421ba36012e45bd9e9184d88475 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 20 Jan 2020 09:09:21 -0800 Subject: [PATCH 0175/1517] Updating examples to use tracer_source (#360) Many examples were not updated to use the new named tracers code. Signed-off-by: Alex Boten --- ext/opentelemetry-ext-dbapi/README.rst | 4 ++-- ext/opentelemetry-ext-http-requests/README.rst | 4 ++-- ext/opentelemetry-ext-mysql/README.rst | 4 ++-- ext/opentelemetry-ext-pymongo/README.rst | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst index 6dc3121603..f3eb9b241c 100644 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -11,12 +11,12 @@ Usage .. code:: python import mysql.connector - from opentelemetry.trace import tracer + from opentelemetry.trace import tracer_source from opentelemetry.ext.dbapi import trace_integration # Ex: mysql.connector - trace_integration(tracer(), mysql.connector, "connect", "mysql") + trace_integration(tracer_source(), mysql.connector, "connect", "mysql") References diff --git a/ext/opentelemetry-ext-http-requests/README.rst b/ext/opentelemetry-ext-http-requests/README.rst index 33759f5cec..7b2d434023 100644 --- a/ext/opentelemetry-ext-http-requests/README.rst +++ b/ext/opentelemetry-ext-http-requests/README.rst @@ -22,9 +22,9 @@ Usage import requests import opentelemetry.ext.http_requests - from opentelemetry.trace import tracer + from opentelemetry.trace import tracer_source - opentelemetry.ext.http_requests.enable(tracer()) + opentelemetry.ext.http_requests.enable(tracer_source()) response = requests.get(url='https://www.example.org/') Limitations diff --git a/ext/opentelemetry-ext-mysql/README.rst b/ext/opentelemetry-ext-mysql/README.rst index e819a63769..e899a980fc 100644 --- a/ext/opentelemetry-ext-mysql/README.rst +++ b/ext/opentelemetry-ext-mysql/README.rst @@ -12,10 +12,10 @@ Usage .. code:: python import mysql.connector - from opentelemetry.trace import tracer + from opentelemetry.trace import tracer_source from opentelemetry.ext.mysql import trace_integration - trace_integration(tracer()) + trace_integration(tracer_source()) cnx = mysql.connector.connect(database='MySQL_Database') cursor = cnx.cursor() cursor.execute("INSERT INTO test (testField) VALUES (123)" diff --git a/ext/opentelemetry-ext-pymongo/README.rst b/ext/opentelemetry-ext-pymongo/README.rst index 1e8011f4c2..e8a42084be 100644 --- a/ext/opentelemetry-ext-pymongo/README.rst +++ b/ext/opentelemetry-ext-pymongo/README.rst @@ -12,10 +12,10 @@ Usage .. code:: python from pymongo import MongoClient - from opentelemetry.trace import tracer + from opentelemetry.trace import tracer_source from opentelemetry.trace.ext.pymongo import trace_integration - trace_integration(tracer()) + trace_integration(tracer_source()) client = MongoClient() db = client["MongoDB_Database"] collection = db["MongoDB_Collection"] From 4fca8c985769d891822823eb91120ea91aa57acd Mon Sep 17 00:00:00 2001 From: Jake Malachowski <5766239+jakemalachowski@users.noreply.github.com> Date: Mon, 20 Jan 2020 11:13:53 -0600 Subject: [PATCH 0176/1517] Add runtime validation in setAttribute (#348) Validate attribute value data types before adding to span Add lists as an accepted data type. By adding validation during the setAttribute phase, this allows allows exporters to avoid redundant code to validate attributes. --- .../src/opentelemetry/sdk/trace/__init__.py | 32 +++++++++++ opentelemetry-sdk/tests/trace/test_trace.py | 53 ++++++++++++++++++- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index b969587eeb..0bae38c8b3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -18,6 +18,7 @@ import random import threading from contextlib import contextmanager +from numbers import Number from types import TracebackType from typing import Iterator, Optional, Sequence, Tuple, Type @@ -216,8 +217,39 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: if has_ended: logger.warning("Setting attribute on ended span.") return + + if isinstance(value, Sequence): + error_message = self._check_attribute_value_sequence(value) + if error_message is not None: + logger.warning("%s in attribute value sequence", error_message) + return + elif not isinstance(value, (bool, str, Number, Sequence)): + logger.warning("invalid type for attribute value") + return + self.attributes[key] = value + @staticmethod + def _check_attribute_value_sequence(sequence: Sequence) -> Optional[str]: + """ + Checks if sequence items are valid and are of the same type + """ + if len(sequence) == 0: + return None + + first_element_type = type(sequence[0]) + + if issubclass(first_element_type, Number): + first_element_type = Number + + if first_element_type not in (bool, str, Number): + return "invalid type" + + for element in sequence: + if not isinstance(element, first_element_type): + return "different type" + return None + def add_event( self, name: str, diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 98a7bb100e..5f32f775fe 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -368,7 +368,11 @@ def test_attributes(self): root.set_attribute("attr-key", "attr-value1") root.set_attribute("attr-key", "attr-value2") - self.assertEqual(len(root.attributes), 7) + root.set_attribute("empty-list", []) + root.set_attribute("list-of-bools", [True, True, False]) + root.set_attribute("list-of-numerics", [123, 3.14, 0]) + + self.assertEqual(len(root.attributes), 10) self.assertEqual(root.attributes["component"], "http") self.assertEqual(root.attributes["http.method"], "GET") self.assertEqual( @@ -379,6 +383,13 @@ def test_attributes(self): self.assertEqual(root.attributes["http.status_text"], "OK") self.assertEqual(root.attributes["misc.pi"], 3.14) self.assertEqual(root.attributes["attr-key"], "attr-value2") + self.assertEqual(root.attributes["empty-list"], []) + self.assertEqual( + root.attributes["list-of-bools"], [True, True, False] + ) + self.assertEqual( + root.attributes["list-of-numerics"], [123, 3.14, 0] + ) attributes = { "attr-key": "val", @@ -393,6 +404,46 @@ def test_attributes(self): self.assertEqual(root.attributes["attr-key2"], "val2") self.assertEqual(root.attributes["attr-in-both"], "span-attr") + def test_invalid_attribute_values(self): + with self.tracer.start_as_current_span("root") as root: + root.set_attribute("non-primitive-data-type", dict()) + root.set_attribute( + "list-of-mixed-data-types-numeric-first", + [123, False, "string"], + ) + root.set_attribute( + "list-of-mixed-data-types-non-numeric-first", + [False, 123, "string"], + ) + root.set_attribute( + "list-with-non-primitive-data-type", [dict(), 123] + ) + + self.assertEqual(len(root.attributes), 0) + + def test_check_sequence_helper(self): + # pylint: disable=protected-access + self.assertEqual( + trace.Span._check_attribute_value_sequence([1, 2, 3.4, "ss", 4]), + "different type", + ) + self.assertEqual( + trace.Span._check_attribute_value_sequence([dict(), 1, 2, 3.4, 4]), + "invalid type", + ) + self.assertEqual( + trace.Span._check_attribute_value_sequence( + ["sw", "lf", 3.4, "ss"] + ), + "different type", + ) + self.assertIsNone( + trace.Span._check_attribute_value_sequence([1, 2, 3.4, 5]) + ) + self.assertIsNone( + trace.Span._check_attribute_value_sequence(["ss", "dw", "fw"]) + ) + def test_sampling_attributes(self): decision_attributes = { "sampler-attr": "sample-val", From 4f71fba91f115fdebafc2210b15d6dc53afc4cf7 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 20 Jan 2020 11:18:26 -0600 Subject: [PATCH 0177/1517] Set status properly on end (#358) In accordance with the specification, a trace status should always be set on an ended span. --- .../src/opentelemetry/sdk/trace/__init__.py | 10 ++-- opentelemetry-sdk/tests/trace/test_trace.py | 57 ++++++++++++------- pytest.ini | 2 + 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0bae38c8b3..9829c8b33b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -67,7 +67,8 @@ def on_end(self, span: "Span") -> None: """ def shutdown(self) -> None: - """Called when a :class:`opentelemetry.sdk.trace.Tracer` is shutdown.""" + """Called when a :class:`opentelemetry.sdk.trace.Tracer` is shutdown. + """ class MultiSpanProcessor(SpanProcessor): @@ -299,16 +300,17 @@ def end(self, end_time: Optional[int] = None) -> None: raise RuntimeError("Calling end() on a not started span.") has_ended = self.end_time is not None if not has_ended: + if self.status is None: + self.status = Status(canonical_code=StatusCanonicalCode.OK) + self._end_time = ( end_time if end_time is not None else time_ns() ) + if has_ended: logger.warning("Calling end() on an ended span.") return - if self.status is None: - self.set_status(Status(canonical_code=StatusCanonicalCode.OK)) - self.span_processor.on_end(self) def update_name(self, name: str) -> None: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 5f32f775fe..53a10518aa 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -15,6 +15,7 @@ import shutil import subprocess import unittest +from logging import ERROR, WARNING from unittest import mock from opentelemetry import trace as trace_api @@ -167,8 +168,10 @@ def test_instrumentation_info(self): def test_invalid_instrumentation_info(self): tracer_source = trace.TracerSource() - tracer1 = tracer_source.get_tracer("") - tracer2 = tracer_source.get_tracer(None) + with self.assertLogs(level=ERROR): + tracer1 = tracer_source.get_tracer("") + with self.assertLogs(level=ERROR): + tracer2 = tracer_source.get_tracer(None) self.assertEqual( tracer1.instrumentation_info, tracer2.instrumentation_info ) @@ -567,7 +570,8 @@ def test_start_span(self): span.start() start_time = span.start_time - span.start() + with self.assertLogs(level=WARNING): + span.start() self.assertEqual(start_time, span.start_time) self.assertIs(span.status, None) @@ -596,36 +600,45 @@ def test_span_override_start_and_end_time(self): def test_ended_span(self): """"Events, attributes are not allowed after span is ended""" - with self.tracer.start_as_current_span("root") as root: - # everything should be empty at the beginning - self.assertEqual(len(root.attributes), 0) - self.assertEqual(len(root.events), 0) - self.assertEqual(len(root.links), 0) + root = self.tracer.start_span("root") - # call end first time - root.end() - end_time0 = root.end_time + # everything should be empty at the beginning + self.assertEqual(len(root.attributes), 0) + self.assertEqual(len(root.events), 0) + self.assertEqual(len(root.links), 0) + + # call end first time + root.end() + end_time0 = root.end_time - # call it a second time + # call it a second time + with self.assertLogs(level=WARNING): root.end() - # end time shouldn't be changed - self.assertEqual(end_time0, root.end_time) + # end time shouldn't be changed + self.assertEqual(end_time0, root.end_time) + with self.assertLogs(level=WARNING): root.set_attribute("component", "http") - self.assertEqual(len(root.attributes), 0) + self.assertEqual(len(root.attributes), 0) + with self.assertLogs(level=WARNING): root.add_event("event1") - self.assertEqual(len(root.events), 0) + self.assertEqual(len(root.events), 0) + with self.assertLogs(level=WARNING): root.update_name("xxx") - self.assertEqual(root.name, "root") + self.assertEqual(root.name, "root") - new_status = trace_api.status.Status( - trace_api.status.StatusCanonicalCode.CANCELLED, - "Test description", - ) + new_status = trace_api.status.Status( + trace_api.status.StatusCanonicalCode.CANCELLED, "Test description", + ) + + with self.assertLogs(level=WARNING): root.set_status(new_status) - self.assertIs(root.status, None) + self.assertEqual( + root.status.canonical_code, + trace_api.status.StatusCanonicalCode.OK, + ) def test_error_status(self): try: diff --git a/pytest.ini b/pytest.ini index a71d000b56..013d2c555c 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,4 @@ [pytest] addopts = -rs -v +log_cli = true +log_cli_level = warning From 983edc6238113ca835dbdd7d6077979951c69c62 Mon Sep 17 00:00:00 2001 From: Jake Malachowski <5766239+jakemalachowski@users.noreply.github.com> Date: Mon, 20 Jan 2020 11:32:27 -0600 Subject: [PATCH 0178/1517] Add int and valid sequences to AttributeValue type (#368) The OT Spec states one of the valid AttributeValue types are: An array of primitive type values. The array MUST be homogeneous, i.e. it MUST NOT contain values of different types. This change updates the AttributeValue type to include homogeneous sequences of str, bool, int, and float --- opentelemetry-api/src/opentelemetry/util/types.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index 28fab89389..5ce93d84b2 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -13,7 +13,16 @@ # limitations under the License. -import typing +from typing import Dict, Optional, Sequence, Union -AttributeValue = typing.Union[str, bool, float] -Attributes = typing.Optional[typing.Dict[str, AttributeValue]] +AttributeValue = Union[ + str, + bool, + int, + float, + Sequence[str], + Sequence[bool], + Sequence[int], + Sequence[float], +] +Attributes = Optional[Dict[str, AttributeValue]] From 47d42aac878bc1ad62325d2cc1c1924c6f855b85 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 20 Jan 2020 14:02:54 -0800 Subject: [PATCH 0179/1517] flask: using string keys for wsgi environ values (#366) Some WSGI servers (such as Gunicorn) expect keys in the environ object to be strings. --- .../src/opentelemetry/ext/flask/__init__.py | 6 +++--- .../tests/test_flask_integration.py | 21 ++++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index ce11b18d63..4bf0cc8c68 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -12,9 +12,9 @@ logger = logging.getLogger(__name__) -_ENVIRON_STARTTIME_KEY = object() -_ENVIRON_SPAN_KEY = object() -_ENVIRON_ACTIVATION_KEY = object() +_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key" +_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key" +_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key" def instrument_app(flask): diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py index 09e62b7ba1..9d2f256011 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py @@ -14,7 +14,7 @@ import unittest -from flask import Flask +from flask import Flask, request from werkzeug.test import Client from werkzeug.wrappers import BaseResponse @@ -57,6 +57,25 @@ def hello_endpoint(helloid): otel_flask.instrument_app(self.app) self.client = Client(self.app, BaseResponse) + def test_only_strings_in_environ(self): + """ + Some WSGI servers (such as Gunicorn) expect keys in the environ object + to be strings + + OpenTelemetry should adhere to this convention. + """ + nonstring_keys = set() + + def assert_environ(): + for key in request.environ: + if not isinstance(key, str): + nonstring_keys.add(key) + return "hi" + + self.app.route("/assert_environ")(assert_environ) + self.client.get("/assert_environ") + self.assertEqual(nonstring_keys, set()) + def test_simple(self): expected_attrs = expected_attributes( { From a40edf5d2762d7ea87d879cfc9e82c1c3b70220d Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Tue, 21 Jan 2020 16:32:38 -0800 Subject: [PATCH 0180/1517] Adding psycopg2 integration (#298) --- ext/opentelemetry-ext-dbapi/README.rst | 3 +- .../src/opentelemetry/ext/dbapi/__init__.py | 119 +++++++++--------- ext/opentelemetry-ext-psycopg2/README.rst | 29 +++++ ext/opentelemetry-ext-psycopg2/setup.cfg | 47 +++++++ ext/opentelemetry-ext-psycopg2/setup.py | 26 ++++ .../opentelemetry/ext/psycopg2/__init__.py | 96 ++++++++++++++ .../src/opentelemetry/ext/psycopg2/version.py | 15 +++ .../tests/__init__.py | 0 .../tests/test_psycopg2_integration.py | 30 +++++ tox.ini | 7 +- 10 files changed, 312 insertions(+), 60 deletions(-) create mode 100644 ext/opentelemetry-ext-psycopg2/README.rst create mode 100644 ext/opentelemetry-ext-psycopg2/setup.cfg create mode 100644 ext/opentelemetry-ext-psycopg2/setup.py create mode 100644 ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py create mode 100644 ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py create mode 100644 ext/opentelemetry-ext-psycopg2/tests/__init__.py create mode 100644 ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst index f3eb9b241c..3618453823 100644 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -14,7 +14,8 @@ Usage from opentelemetry.trace import tracer_source from opentelemetry.ext.dbapi import trace_integration - + trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + tracer = trace.tracer_source().get_tracer(__name__) # Ex: mysql.connector trace_integration(tracer_source(), mysql.connector, "connect", "mysql") diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index 7ba1de1795..88e9d3a0b1 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -18,6 +18,7 @@ https://www.python.org/dev/peps/pep-0249/ """ +import functools import logging import typing @@ -72,7 +73,6 @@ def wrap_connect( class DatabaseApiIntegration: - # pylint: disable=unused-argument def __init__( self, tracer: Tracer, @@ -80,8 +80,6 @@ def __init__( database_type: str = "sql", connection_attributes=None, ): - if tracer is None: - raise ValueError("The tracer is not provided.") self.connection_attributes = connection_attributes if self.connection_attributes is None: self.connection_attributes = { @@ -107,18 +105,40 @@ def wrapped_connection( """Add object proxy to connection object. """ connection = connect_method(*args, **kwargs) + self.get_connection_attributes(connection) + traced_connection = TracedConnectionProxy(connection, self) + return traced_connection + def get_connection_attributes(self, connection): + # Populate span fields using connection for key, value in self.connection_attributes.items(): - attribute = getattr(connection, value, None) + # Allow attributes nested in connection object + attribute = functools.reduce( + lambda attribute, attribute_value: getattr( + attribute, attribute_value, None + ), + value.split("."), + connection, + ) if attribute: self.connection_props[key] = attribute - traced_connection = TracedConnection(connection, self) - return traced_connection + self.name = self.database_component + self.database = self.connection_props.get("database", "") + if self.database: + self.name += "." + self.database + user = self.connection_props.get("user") + if user is not None: + self.span_attributes["db.user"] = user + host = self.connection_props.get("host") + if host is not None: + self.span_attributes["net.peer.name"] = host + port = self.connection_props.get("port") + if port is not None: + self.span_attributes["net.peer.port"] = port # pylint: disable=abstract-method -class TracedConnection(wrapt.ObjectProxy): - +class TracedConnectionProxy(wrapt.ObjectProxy): # pylint: disable=unused-argument def __init__( self, @@ -130,62 +150,17 @@ def __init__( wrapt.ObjectProxy.__init__(self, connection) self._db_api_integration = db_api_integration - self._db_api_integration.name = ( - self._db_api_integration.database_component - ) - self._db_api_integration.database = self._db_api_integration.connection_props.get( - "database", "" - ) - if self._db_api_integration.database: - self._db_api_integration.name += ( - "." + self._db_api_integration.database - ) - user = self._db_api_integration.connection_props.get("user") - if user is not None: - self._db_api_integration.span_attributes["db.user"] = user - host = self._db_api_integration.connection_props.get("host") - if host is not None: - self._db_api_integration.span_attributes["net.peer.name"] = host - port = self._db_api_integration.connection_props.get("port") - if port is not None: - self._db_api_integration.span_attributes["net.peer.port"] = port - def cursor(self, *args, **kwargs): - return TracedCursor( + return TracedCursorProxy( self.__wrapped__.cursor(*args, **kwargs), self._db_api_integration ) -# pylint: disable=abstract-method -class TracedCursor(wrapt.ObjectProxy): - - # pylint: disable=unused-argument - def __init__( - self, - cursor, - db_api_integration: DatabaseApiIntegration, - *args, - **kwargs - ): - wrapt.ObjectProxy.__init__(self, cursor) +class TracedCursor: + def __init__(self, db_api_integration: DatabaseApiIntegration): self._db_api_integration = db_api_integration - def execute(self, *args, **kwargs): - return self._traced_execution( - self.__wrapped__.execute, *args, **kwargs - ) - - def executemany(self, *args, **kwargs): - return self._traced_execution( - self.__wrapped__.executemany, *args, **kwargs - ) - - def callproc(self, *args, **kwargs): - return self._traced_execution( - self.__wrapped__.callproc, *args, **kwargs - ) - - def _traced_execution( + def traced_execution( self, query_method: typing.Callable[..., any], *args: typing.Tuple[any, any], @@ -223,3 +198,33 @@ def _traced_execution( except Exception as ex: # pylint: disable=broad-except span.set_status(Status(StatusCanonicalCode.UNKNOWN, str(ex))) raise ex + + +# pylint: disable=abstract-method +class TracedCursorProxy(wrapt.ObjectProxy): + + # pylint: disable=unused-argument + def __init__( + self, + cursor, + db_api_integration: DatabaseApiIntegration, + *args, + **kwargs + ): + wrapt.ObjectProxy.__init__(self, cursor) + self._traced_cursor = TracedCursor(db_api_integration) + + def execute(self, *args, **kwargs): + return self._traced_cursor.traced_execution( + self.__wrapped__.execute, *args, **kwargs + ) + + def executemany(self, *args, **kwargs): + return self._traced_cursor.traced_execution( + self.__wrapped__.executemany, *args, **kwargs + ) + + def callproc(self, *args, **kwargs): + return self._traced_cursor.traced_execution( + self.__wrapped__.callproc, *args, **kwargs + ) diff --git a/ext/opentelemetry-ext-psycopg2/README.rst b/ext/opentelemetry-ext-psycopg2/README.rst new file mode 100644 index 0000000000..d7599492ac --- /dev/null +++ b/ext/opentelemetry-ext-psycopg2/README.rst @@ -0,0 +1,29 @@ +OpenTelemetry Psycopg integration +================================= + +The integration with PostgreSQL supports the `Psycopg`_ library and is specified +to ``trace_integration`` using ``'PostgreSQL'``. + +.. Psycopg: http://initd.org/psycopg/ + +Usage +----- + +.. code:: python + import psycopg2 + from opentelemetry import trace + from opentelemetry.sdk.trace import TracerSource + from opentelemetry.trace.ext.psycopg2 import trace_integration + + trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + tracer = trace.tracer_source().get_tracer(__name__) + trace_integration(tracer) + cnx = psycopg2.connect(database='Database') + cursor = cnx.cursor() + cursor.execute("INSERT INTO test (testField) VALUES (123)") + cursor.close() + cnx.close() + +References +---------- +* `OpenTelemetry Project `_ \ No newline at end of file diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg new file mode 100644 index 0000000000..f26c5918eb --- /dev/null +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -0,0 +1,47 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-psycopg2 +description = OpenTelemetry psycopg2 integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-psycopg2 +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api >= 0.4.dev0 + psycopg2-binary >= 2.7.3.1 + wrapt >= 1.0.0, < 2.0.0 + +[options.packages.find] +where = src \ No newline at end of file diff --git a/ext/opentelemetry-ext-psycopg2/setup.py b/ext/opentelemetry-ext-psycopg2/setup.py new file mode 100644 index 0000000000..a84391e6dd --- /dev/null +++ b/ext/opentelemetry-ext-psycopg2/setup.py @@ -0,0 +1,26 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "psycopg2", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py new file mode 100644 index 0000000000..4181688489 --- /dev/null +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py @@ -0,0 +1,96 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The opentelemetry-ext-psycopg2 package allows tracing PostgreSQL queries made by the +Psycopg2 library. +""" + +import logging +import typing + +import psycopg2 +import wrapt +from psycopg2.sql import Composable + +from opentelemetry.ext.dbapi import DatabaseApiIntegration, TracedCursor +from opentelemetry.trace import Tracer + +logger = logging.getLogger(__name__) + +DATABASE_COMPONENT = "postgresql" +DATABASE_TYPE = "sql" + + +def trace_integration(tracer): + """Integrate with PostgreSQL Psycopg library. + Psycopg: http://initd.org/psycopg/ + """ + + connection_attributes = { + "database": "info.dbname", + "port": "info.port", + "host": "info.host", + "user": "info.user", + } + db_integration = DatabaseApiIntegration( + tracer, + DATABASE_COMPONENT, + database_type=DATABASE_TYPE, + connection_attributes=connection_attributes, + ) + + # pylint: disable=unused-argument + def wrap_connect( + connect_func: typing.Callable[..., any], + instance: typing.Any, + args: typing.Tuple[any, any], + kwargs: typing.Dict[any, any], + ): + connection = connect_func(*args, **kwargs) + db_integration.get_connection_attributes(connection) + connection.cursor_factory = PsycopgTraceCursor + return connection + + try: + wrapt.wrap_function_wrapper(psycopg2, "connect", wrap_connect) + except Exception as ex: # pylint: disable=broad-except + logger.warning("Failed to integrate with pyscopg2. %s", str(ex)) + + class PsycopgTraceCursor(psycopg2.extensions.cursor): + def __init__(self, *args, **kwargs): + self._traced_cursor = TracedCursor(db_integration) + super(PsycopgTraceCursor, self).__init__(*args, **kwargs) + + # pylint: disable=redefined-builtin + def execute(self, query, vars=None): + if isinstance(query, Composable): + query = query.as_string(self) + return self._traced_cursor.traced_execution( + super(PsycopgTraceCursor, self).execute, query, vars + ) + + # pylint: disable=redefined-builtin + def executemany(self, query, vars): + if isinstance(query, Composable): + query = query.as_string(self) + return self._traced_cursor.traced_execution( + super(PsycopgTraceCursor, self).executemany, query, vars + ) + + # pylint: disable=redefined-builtin + def callproc(self, procname, vars=None): + return self._traced_cursor.traced_execution( + super(PsycopgTraceCursor, self).callproc, procname, vars + ) diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py new file mode 100644 index 0000000000..6b39cd19b5 --- /dev/null +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py @@ -0,0 +1,15 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.4.dev0" diff --git a/ext/opentelemetry-ext-psycopg2/tests/__init__.py b/ext/opentelemetry-ext-psycopg2/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py b/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py new file mode 100644 index 0000000000..56ab3a8aae --- /dev/null +++ b/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py @@ -0,0 +1,30 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +import psycopg2 + +from opentelemetry import trace as trace_api +from opentelemetry.ext.psycopg2 import trace_integration + + +class TestPostgresqlIntegration(unittest.TestCase): + def test_trace_integration(self): + tracer = trace_api.DefaultTracer() + with mock.patch("psycopg2.connect"): + trace_integration(tracer) + cnx = psycopg2.connect(database="test") + self.assertIsNotNone(cnx.cursor_factory) diff --git a/tox.ini b/tox.ini index 642a8a556b..d077d07893 100644 --- a/tox.ini +++ b/tox.ini @@ -2,9 +2,9 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-psycopg2,ext-pymongo,ext-zipkin,opentracing-shim} pypy3-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim} - py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-psycopg2,ext-pymongo,ext-zipkin,opentracing-shim} pypy3-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim} py3{4,5,6,7,8}-coverage @@ -39,6 +39,7 @@ changedir = test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests test-ext-mysql: ext/opentelemetry-ext-mysql/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests + test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests test-ext-flask: ext/opentelemetry-ext-flask/tests @@ -74,6 +75,8 @@ commands_pre = mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo + psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi + psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-psycopg2 http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests jaeger: pip install {toxinidir}/opentelemetry-sdk jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger From 7415679b6882f4d6e130bcf881ef9aa543fbd2bd Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 23 Jan 2020 08:48:56 -0600 Subject: [PATCH 0181/1517] Add support for B3 parentspanid (#286) Supporting B3's technical definition of a parentspanid, by sourcing the span id of the parent span during injection from the propagator. --- .../ext/opentracing_shim/__init__.py | 15 +- .../tests/test_shim.py | 15 +- .../context/propagation/httptextformat.py | 6 +- .../propagation/tracecontexthttptextformat.py | 5 +- .../src/opentelemetry/propagators/__init__.py | 2 +- .../test_tracecontexthttptextformat.py | 59 +++-- .../sdk/context/propagation/b3_format.py | 19 +- .../context/propagation/test_b3_format.py | 202 ++++++++++++------ 8 files changed, 210 insertions(+), 113 deletions(-) diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index 7c7640017b..ce35681d3f 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -89,6 +89,7 @@ from opentelemetry import propagators from opentelemetry.ext.opentracing_shim import util from opentelemetry.ext.opentracing_shim.version import __version__ +from opentelemetry.trace import DefaultSpan logger = logging.getLogger(__name__) @@ -101,10 +102,10 @@ def create_tracer(otel_tracer_source): :class:`opentracing.Tracer` using OpenTelemetry under the hood. Args: - otel_tracer_source: A :class:`opentelemetry.trace.TracerSource` to be used for - constructing the :class:`TracerShim`. A tracer from this source will be used - to perform the actual tracing when user code is instrumented using - the OpenTracing API. + otel_tracer_source: A :class:`opentelemetry.trace.TracerSource` to be + used for constructing the :class:`TracerShim`. A tracer from this + source will be used to perform the actual tracing when user code is + instrumented using the OpenTracing API. Returns: The created :class:`TracerShim`. @@ -667,12 +668,16 @@ def inject(self, span_context, format, carrier): # uses the configured propagators in opentelemetry.propagators. # TODO: Support Format.BINARY once it is supported in # opentelemetry-python. + if format not in self._supported_formats: raise opentracing.UnsupportedFormatException propagator = propagators.get_global_httptextformat() + propagator.inject( - span_context.unwrap(), type(carrier).__setitem__, carrier + DefaultSpan(span_context.unwrap()), + type(carrier).__setitem__, + carrier, ) def extract(self, format, carrier): diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index d42098dce7..997f97195e 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -13,7 +13,7 @@ # limitations under the License. import time -import unittest +from unittest import TestCase import opentracing @@ -24,7 +24,7 @@ from opentelemetry.sdk.trace import TracerSource -class TestShim(unittest.TestCase): +class TestShim(TestCase): # pylint: disable=too-many-public-methods def setUp(self): @@ -486,6 +486,7 @@ def test_inject_text_map(self): # Verify Format.TEXT_MAP text_map = {} + self.shim.inject(context, opentracing.Format.TEXT_MAP, text_map) self.assertEqual(text_map[MockHTTPTextFormat.TRACE_ID_KEY], str(1220)) self.assertEqual(text_map[MockHTTPTextFormat.SPAN_ID_KEY], str(7478)) @@ -551,6 +552,10 @@ def extract(cls, get_from_carrier, carrier): ) @classmethod - def inject(cls, context, set_in_carrier, carrier): - set_in_carrier(carrier, cls.TRACE_ID_KEY, str(context.trace_id)) - set_in_carrier(carrier, cls.SPAN_ID_KEY, str(context.span_id)) + def inject(cls, span, set_in_carrier, carrier): + set_in_carrier( + carrier, cls.TRACE_ID_KEY, str(span.get_context().trace_id) + ) + set_in_carrier( + carrier, cls.SPAN_ID_KEY, str(span.get_context().span_id) + ) diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py index 9b6098a9a4..b64a298c41 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py @@ -15,7 +15,7 @@ import abc import typing -from opentelemetry.trace import SpanContext +from opentelemetry.trace import Span, SpanContext _T = typing.TypeVar("_T") @@ -95,9 +95,9 @@ def extract( @abc.abstractmethod def inject( - self, context: SpanContext, set_in_carrier: Setter[_T], carrier: _T + self, span: Span, set_in_carrier: Setter[_T], carrier: _T ) -> None: - """Inject values from a SpanContext into a carrier. + """Inject values from a Span into a carrier. inject enables the propagation of values into HTTP clients or other objects which perform an HTTP request. Implementations diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py index 5d00632ed1..6f50f00839 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py @@ -105,10 +105,13 @@ def extract( @classmethod def inject( cls, - context: trace.SpanContext, + span: trace.Span, set_in_carrier: httptextformat.Setter[_T], carrier: _T, ) -> None: + + context = span.get_context() + if context == trace.INVALID_SPAN_CONTEXT: return traceparent_string = "00-{:032x}-{:016x}-{:02x}".format( diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index bb75d84c3a..3974a4cb03 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -64,7 +64,7 @@ def inject( should know how to set header values on the carrier. """ get_global_httptextformat().inject( - tracer.get_current_span().get_context(), set_in_carrier, carrier + tracer.get_current_span(), set_in_carrier, carrier ) diff --git a/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py index ed952e0dba..c39fd3182c 100644 --- a/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py @@ -14,6 +14,7 @@ import typing import unittest +from unittest.mock import Mock from opentelemetry import trace from opentelemetry.context.propagation import tracecontexthttptextformat @@ -38,7 +39,8 @@ def test_no_traceparent_header(self): RFC 4.2.2: - If no traceparent header is received, the vendor creates a new trace-id and parent-id that represents the current request. + If no traceparent header is received, the vendor creates a new + trace-id and parent-id that represents the current request. """ output = {} # type:typing.Dict[str, typing.List[str]] span_context = FORMAT.extract(get_as_list, output) @@ -66,8 +68,10 @@ def test_headers_with_tracestate(self): span_context.trace_state, {"foo": "1", "bar": "2", "baz": "3"} ) + mock_span = Mock() + mock_span.configure_mock(**{"get_context.return_value": span_context}) output = {} # type:typing.Dict[str, str] - FORMAT.inject(span_context, dict.__setitem__, output) + FORMAT.inject(mock_span, dict.__setitem__, output) self.assertEqual(output["traceparent"], traceparent_value) for pair in ["foo=1", "bar=2", "baz=3"]: self.assertIn(pair, output["tracestate"]) @@ -81,13 +85,16 @@ def test_invalid_trace_id(self): RFC 3.2.2.3 - If the trace-id value is invalid (for example if it contains non-allowed characters or all - zeros), vendors MUST ignore the traceparent. + If the trace-id value is invalid (for example if it contains + non-allowed characters or all zeros), vendors MUST ignore the + traceparent. RFC 3.3 - If the vendor failed to parse traceparent, it MUST NOT attempt to parse tracestate. - Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. + If the vendor failed to parse traceparent, it MUST NOT attempt to + parse tracestate. + Note that the opposite is not true: failure to parse tracestate MUST + NOT affect the parsing of traceparent. """ span_context = FORMAT.extract( get_as_list, @@ -101,19 +108,22 @@ def test_invalid_trace_id(self): self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) def test_invalid_parent_id(self): - """If the parent id is invalid, we must ignore the full traceparent header. + """If the parent id is invalid, we must ignore the full traceparent + header. Also ignore any tracestate. RFC 3.2.2.3 - Vendors MUST ignore the traceparent when the parent-id is invalid (for example, - if it contains non-lowercase hex characters). + Vendors MUST ignore the traceparent when the parent-id is invalid (for + example, if it contains non-lowercase hex characters). RFC 3.3 - If the vendor failed to parse traceparent, it MUST NOT attempt to parse tracestate. - Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. + If the vendor failed to parse traceparent, it MUST NOT attempt to parse + tracestate. + Note that the opposite is not true: failure to parse tracestate MUST + NOT affect the parsing of traceparent. """ span_context = FORMAT.extract( get_as_list, @@ -131,15 +141,19 @@ def test_no_send_empty_tracestate(self): RFC 3.3.1.1 - Empty and whitespace-only list members are allowed. Vendors MUST accept empty - tracestate headers but SHOULD avoid sending them. + Empty and whitespace-only list members are allowed. Vendors MUST accept + empty tracestate headers but SHOULD avoid sending them. """ output = {} # type:typing.Dict[str, str] - FORMAT.inject( - trace.SpanContext(self.TRACE_ID, self.SPAN_ID), - dict.__setitem__, - output, + mock_span = Mock() + mock_span.configure_mock( + **{ + "get_context.return_value": trace.SpanContext( + self.TRACE_ID, self.SPAN_ID + ) + } ) + FORMAT.inject(mock_span, dict.__setitem__, output) self.assertTrue("traceparent" in output) self.assertFalse("tracestate" in output) @@ -155,7 +169,8 @@ def test_format_not_supported(self): get_as_list, { "traceparent": [ - "00-12345678901234567890123456789012-1234567890123456-00-residue" + "00-12345678901234567890123456789012-" + "1234567890123456-00-residue" ], "tracestate": ["foo=1,bar=2,foo=3"], }, @@ -163,14 +178,14 @@ def test_format_not_supported(self): self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) def test_propagate_invalid_context(self): - """Do not propagate invalid trace context. - """ + """Do not propagate invalid trace context.""" output = {} # type:typing.Dict[str, str] - FORMAT.inject(trace.INVALID_SPAN_CONTEXT, dict.__setitem__, output) + FORMAT.inject(trace.Span(), dict.__setitem__, output) self.assertFalse("traceparent" in output) def test_tracestate_empty_header(self): - """Test tracestate with an additional empty header (should be ignored)""" + """Test tracestate with an additional empty header (should be ignored) + """ span_context = FORMAT.extract( get_as_list, { diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py index 7d59fddb9e..4c9214dbcc 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -27,6 +27,7 @@ class B3Format(HTTPTextFormat): SINGLE_HEADER_KEY = "b3" TRACE_ID_KEY = "x-b3-traceid" SPAN_ID_KEY = "x-b3-spanid" + PARENT_SPAN_ID_KEY = "x-b3-parentspanid" SAMPLED_KEY = "x-b3-sampled" FLAGS_KEY = "x-b3-flags" _SAMPLE_PROPAGATE_VALUES = set(["1", "True", "true", "d"]) @@ -55,7 +56,7 @@ def extract(cls, get_from_carrier, carrier): elif len(fields) == 3: trace_id, span_id, sampled = fields elif len(fields) == 4: - trace_id, span_id, sampled, _parent_span_id = fields + trace_id, span_id, sampled, _ = fields else: return trace.INVALID_SPAN_CONTEXT else: @@ -100,14 +101,22 @@ def extract(cls, get_from_carrier, carrier): ) @classmethod - def inject(cls, context, set_in_carrier, carrier): - sampled = (trace.TraceOptions.SAMPLED & context.trace_options) != 0 + def inject(cls, span, set_in_carrier, carrier): + sampled = ( + trace.TraceOptions.SAMPLED & span.context.trace_options + ) != 0 set_in_carrier( - carrier, cls.TRACE_ID_KEY, format_trace_id(context.trace_id) + carrier, cls.TRACE_ID_KEY, format_trace_id(span.context.trace_id) ) set_in_carrier( - carrier, cls.SPAN_ID_KEY, format_span_id(context.span_id) + carrier, cls.SPAN_ID_KEY, format_span_id(span.context.span_id) ) + if span.parent is not None: + set_in_carrier( + carrier, + cls.PARENT_SPAN_ID_KEY, + format_span_id(span.parent.context.span_id), + ) set_in_carrier(carrier, cls.SAMPLED_KEY, "1" if sampled else "0") diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py index 1215508269..17f7fdf7ca 100644 --- a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -26,6 +26,28 @@ def get_as_list(dict_object, key): return [value] if value is not None else [] +def get_child_parent_new_carrier(old_carrier): + + parent_context = FORMAT.extract(get_as_list, old_carrier) + + parent = trace.Span("parent", parent_context) + child = trace.Span( + "child", + trace_api.SpanContext( + parent_context.trace_id, + trace.generate_span_id(), + trace_options=parent_context.trace_options, + trace_state=parent_context.trace_state, + ), + parent=parent, + ) + + new_carrier = {} + FORMAT.inject(child, dict.__setitem__, new_carrier) + + return child, parent, new_carrier + + class TestB3Format(unittest.TestCase): @classmethod def setUpClass(cls): @@ -35,40 +57,76 @@ def setUpClass(cls): cls.serialized_span_id = b3_format.format_span_id( trace.generate_span_id() ) + cls.serialized_parent_id = b3_format.format_span_id( + trace.generate_span_id() + ) def test_extract_multi_header(self): """Test the extraction of B3 headers.""" - carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.SAMPLED_KEY: "1", - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + child, parent, new_carrier = get_child_parent_new_carrier( + { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.PARENT_SPAN_ID_KEY: self.serialized_parent_id, + FORMAT.SAMPLED_KEY: "1", + } + ) + self.assertEqual( - new_carrier[FORMAT.TRACE_ID_KEY], self.serialized_trace_id + new_carrier[FORMAT.TRACE_ID_KEY], + b3_format.format_trace_id(child.context.trace_id), ) self.assertEqual( - new_carrier[FORMAT.SPAN_ID_KEY], self.serialized_span_id + new_carrier[FORMAT.SPAN_ID_KEY], + b3_format.format_span_id(child.context.span_id), + ) + self.assertEqual( + new_carrier[FORMAT.PARENT_SPAN_ID_KEY], + b3_format.format_span_id(parent.context.span_id), ) self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") def test_extract_single_header(self): """Test the extraction from a single b3 header.""" - carrier = { - FORMAT.SINGLE_HEADER_KEY: "{}-{}".format( - self.serialized_trace_id, self.serialized_span_id - ) - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + child, parent, new_carrier = get_child_parent_new_carrier( + { + FORMAT.SINGLE_HEADER_KEY: "{}-{}".format( + self.serialized_trace_id, self.serialized_span_id + ) + } + ) + self.assertEqual( - new_carrier[FORMAT.TRACE_ID_KEY], self.serialized_trace_id + new_carrier[FORMAT.TRACE_ID_KEY], + b3_format.format_trace_id(child.context.trace_id), ) self.assertEqual( - new_carrier[FORMAT.SPAN_ID_KEY], self.serialized_span_id + new_carrier[FORMAT.SPAN_ID_KEY], + b3_format.format_span_id(child.context.span_id), + ) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + + child, parent, new_carrier = get_child_parent_new_carrier( + { + FORMAT.SINGLE_HEADER_KEY: "{}-{}-1-{}".format( + self.serialized_trace_id, + self.serialized_span_id, + self.serialized_parent_id, + ) + } + ) + + self.assertEqual( + new_carrier[FORMAT.TRACE_ID_KEY], + b3_format.format_trace_id(child.context.trace_id), + ) + self.assertEqual( + new_carrier[FORMAT.SPAN_ID_KEY], + b3_format.format_span_id(child.context.span_id), + ) + self.assertEqual( + new_carrier[FORMAT.PARENT_SPAN_ID_KEY], + b3_format.format_span_id(parent.context.span_id), ) self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") @@ -77,17 +135,18 @@ def test_extract_header_precedence(self): headers. """ single_header_trace_id = self.serialized_trace_id[:-3] + "123" - carrier = { - FORMAT.SINGLE_HEADER_KEY: "{}-{}".format( - single_header_trace_id, self.serialized_span_id - ), - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.SAMPLED_KEY: "1", - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + + _, _, new_carrier = get_child_parent_new_carrier( + { + FORMAT.SINGLE_HEADER_KEY: "{}-{}".format( + single_header_trace_id, self.serialized_span_id + ), + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.SAMPLED_KEY: "1", + } + ) + self.assertEqual( new_carrier[FORMAT.TRACE_ID_KEY], single_header_trace_id ) @@ -95,64 +154,65 @@ def test_extract_header_precedence(self): def test_enabled_sampling(self): """Test b3 sample key variants that turn on sampling.""" for variant in ["1", "True", "true", "d"]: - carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.SAMPLED_KEY: variant, - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + _, _, new_carrier = get_child_parent_new_carrier( + { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.SAMPLED_KEY: variant, + } + ) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") def test_disabled_sampling(self): """Test b3 sample key variants that turn off sampling.""" for variant in ["0", "False", "false", None]: - carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.SAMPLED_KEY: variant, - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + _, _, new_carrier = get_child_parent_new_carrier( + { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.SAMPLED_KEY: variant, + } + ) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "0") def test_flags(self): """x-b3-flags set to "1" should result in propagation.""" - carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + _, _, new_carrier = get_child_parent_new_carrier( + { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1", + } + ) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") def test_flags_and_sampling(self): """Propagate if b3 flags and sampling are set.""" - carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + _, _, new_carrier = get_child_parent_new_carrier( + { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1", + } + ) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") def test_64bit_trace_id(self): """64 bit trace ids should be padded to 128 bit trace ids.""" trace_id_64_bit = self.serialized_trace_id[:16] - carrier = { - FORMAT.TRACE_ID_KEY: trace_id_64_bit, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + + _, _, new_carrier = get_child_parent_new_carrier( + { + FORMAT.TRACE_ID_KEY: trace_id_64_bit, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1", + } + ) + self.assertEqual( new_carrier[FORMAT.TRACE_ID_KEY], "0" * 16 + trace_id_64_bit ) From ccb97e582555391600ac45c44be2eb61b522b61f Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 23 Jan 2020 10:18:25 -0800 Subject: [PATCH 0182/1517] Test fix: removing use of ABC Span in API test (#375) --- .../context/propagation/test_tracecontexthttptextformat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py index c39fd3182c..8f283ef881 100644 --- a/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py @@ -180,7 +180,7 @@ def test_format_not_supported(self): def test_propagate_invalid_context(self): """Do not propagate invalid trace context.""" output = {} # type:typing.Dict[str, str] - FORMAT.inject(trace.Span(), dict.__setitem__, output) + FORMAT.inject(trace.INVALID_SPAN, dict.__setitem__, output) self.assertFalse("traceparent" in output) def test_tracestate_empty_header(self): From 0173263ebbea9148aaaec766271b902711c611fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez?= Date: Tue, 28 Jan 2020 04:12:26 +0100 Subject: [PATCH 0183/1517] Rebuild docs if source files change (#380) --- .github/workflows/docs.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 423396ea8d..59c825e7e2 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,7 +4,12 @@ on: push: branches: - master - + paths: + - 'docs/**' + - 'ext/**' + - 'opentelemetry-python/opentelemetry-api/src/opentelemetry/**' + - 'opentelemetry-python/opentelemetry-sdk/src/opentelemetry/sdk/**' + jobs: docs: runs-on: ubuntu-latest From 3995075f2d6379fcc3d1f46d416c757cea0adb7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez?= Date: Wed, 29 Jan 2020 06:46:52 +0100 Subject: [PATCH 0184/1517] Export span status to Jaeger (#367) --- ext/opentelemetry-ext-jaeger/CHANGELOG.md | 2 ++ .../src/opentelemetry/ext/jaeger/__init__.py | 33 +++++++++++++++---- .../tests/test_jaeger_exporter.py | 30 +++++++++++++++++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/ext/opentelemetry-ext-jaeger/CHANGELOG.md b/ext/opentelemetry-ext-jaeger/CHANGELOG.md index 05ffbffe0f..90745ada0d 100644 --- a/ext/opentelemetry-ext-jaeger/CHANGELOG.md +++ b/ext/opentelemetry-ext-jaeger/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Export span status ([#367](https://github.com/open-telemetry/opentelemetry-python/pull/367)) + ## 0.3a0 Released 2019-12-11 diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py index e9d5c5884d..e990b7d97e 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -26,6 +26,7 @@ from opentelemetry.ext.jaeger.gen.agent import Agent as agent from opentelemetry.ext.jaeger.gen.jaeger import Collector as jaeger from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult +from opentelemetry.trace.status import StatusCanonicalCode DEFAULT_AGENT_HOST_NAME = "localhost" DEFAULT_AGENT_PORT = 6831 @@ -145,6 +146,8 @@ def _translate_to_jaeger(spans: Span): start_time_us = _nsec_to_usec_round(span.start_time) duration_us = _nsec_to_usec_round(span.end_time - span.start_time) + status = span.status + parent_id = 0 if isinstance(span.parent, trace_api.Span): parent_id = span.parent.get_context().span_id @@ -153,8 +156,16 @@ def _translate_to_jaeger(spans: Span): tags = _extract_tags(span.attributes) - # TODO: status is missing: - # https://github.com/open-telemetry/opentelemetry-python/issues/98 + tags.extend( + [ + _get_long_tag("status.code", status.canonical_code.value), + _get_string_tag("status.message", status.description), + ] + ) + + # Ensure that if Status.Code is not OK, that we set the "error" tag on the Jaeger span. + if status.canonical_code is not StatusCanonicalCode.OK: + tags.append(_get_bool_tag("error", True)) refs = _extract_refs_from_span(span) logs = _extract_logs_from_span(span) @@ -222,9 +233,7 @@ def _extract_logs_from_span(span): logs = [] for event in span.events: - fields = [] - if event.attributes is not None: - fields = _extract_tags(event.attributes) + fields = _extract_tags(event.attributes) fields.append( jaeger.Tag( @@ -241,7 +250,7 @@ def _extract_logs_from_span(span): def _extract_tags(attr): if not attr: - return None + return [] tags = [] for attribute_key, attribute_value in attr.items(): tag = _convert_attribute_to_tag(attribute_key, attribute_value) @@ -265,6 +274,18 @@ def _convert_attribute_to_tag(key, attr): return None +def _get_long_tag(key, val): + return jaeger.Tag(key=key, vLong=val, vType=jaeger.TagType.LONG) + + +def _get_string_tag(key, val): + return jaeger.Tag(key=key, vStr=val, vType=jaeger.TagType.STRING) + + +def _get_bool_tag(key, val): + return jaeger.Tag(key=key, vBool=val, vType=jaeger.TagType.BOOL) + + class AgentClientUDP: """Implement a UDP client to agent. diff --git a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py index f8ead96ef0..e891393b42 100644 --- a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py +++ b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py @@ -22,6 +22,7 @@ from opentelemetry import trace as trace_api from opentelemetry.ext.jaeger.gen.jaeger import ttypes as jaeger from opentelemetry.sdk import trace +from opentelemetry.trace.status import Status, StatusCanonicalCode class TestJaegerSpanExporter(unittest.TestCase): @@ -155,6 +156,17 @@ def test_translate_to_jaeger(self): context=other_context, attributes=link_attributes ) + default_status_tags = [ + jaeger.Tag( + key="status.code", + vType=jaeger.TagType.LONG, + vLong=StatusCanonicalCode.OK.value, + ), + jaeger.Tag( + key="status.message", vType=jaeger.TagType.STRING, vStr=None, + ), + ] + otel_spans = [ trace.Span( name=span_names[0], @@ -174,6 +186,9 @@ def test_translate_to_jaeger(self): otel_spans[0].set_attribute("key_bool", False) otel_spans[0].set_attribute("key_string", "hello_world") otel_spans[0].set_attribute("key_float", 111.22) + otel_spans[0].set_status( + Status(StatusCanonicalCode.UNKNOWN, "Example description") + ) otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) @@ -209,6 +224,19 @@ def test_translate_to_jaeger(self): vType=jaeger.TagType.DOUBLE, vDouble=111.22, ), + jaeger.Tag( + key="status.code", + vType=jaeger.TagType.LONG, + vLong=StatusCanonicalCode.UNKNOWN.value, + ), + jaeger.Tag( + key="status.message", + vType=jaeger.TagType.STRING, + vStr="Example description", + ), + jaeger.Tag( + key="error", vType=jaeger.TagType.BOOL, vBool=True, + ), ], references=[ jaeger.SpanRef( @@ -255,6 +283,7 @@ def test_translate_to_jaeger(self): startTime=start_times[1] // 10 ** 3, duration=durations[1] // 10 ** 3, flags=0, + tags=default_status_tags, ), jaeger.Span( operationName=span_names[2], @@ -265,6 +294,7 @@ def test_translate_to_jaeger(self): startTime=start_times[2] // 10 ** 3, duration=durations[2] // 10 ** 3, flags=0, + tags=default_status_tags, ), ] From 46749739fcbd4af98477e225951df98b76efbfbc Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Wed, 29 Jan 2020 13:31:50 -0800 Subject: [PATCH 0185/1517] Change imports to avoid pylint namespace errors (#376) --- .../src/opentelemetry/ext/opentracing_shim/__init__.py | 7 +++++-- ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index ce35681d3f..b7753754db 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -80,6 +80,9 @@ https://docs.python.org/3/tutorial/floatingpoint.html """ +# TODO: make pylint use 3p opentracing module for type inference +# pylint:disable=no-member + import logging import opentracing @@ -266,7 +269,7 @@ def log(self, **kwargs): def log_event(self, event, payload=None): super().log_event(event, payload=payload) - def set_baggage_item(self, key, value): + def set_baggage_item(self, key, value): # pylint:disable=unused-argument """Implements the ``set_baggage_item()`` method from the base class. Warning: @@ -279,7 +282,7 @@ def set_baggage_item(self, key, value): ) # TODO: Implement. - def get_baggage_item(self, key): + def get_baggage_item(self, key): # pylint:disable=unused-argument """Implements the ``get_baggage_item()`` method from the base class. Warning: diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index 997f97195e..fb82cb4402 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +# TODO: make pylint use 3p opentracing module for type inference +# pylint:disable=no-member + import time from unittest import TestCase From 6aa69ae4bc07d07bf33652435c5fa63b65eb0190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 29 Jan 2020 16:32:24 -0500 Subject: [PATCH 0186/1517] README: update list of approvers (#388) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index a8422735ef..b9b866833a 100644 --- a/README.md +++ b/README.md @@ -117,9 +117,12 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): +- [Alex Boten](https://github.com/codeboten), LightStep - [Carlos Alberto Cortez](https://github.com/carlosalberto), LightStep - [Christian Neumüller](https://github.com/Oberon00), Dynatrace +- [Hector Hernandez](https://github.com/hectorhdzg), Microsoft - [Leighton Chen](https://github.com/lzchen), Microsoft +- [Mauricio Vásquez](https://github.com/mauriciovasquezbernal), Kinvolk - [Reiley Yang](https://github.com/reyang), Microsoft *Find more about the approver role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver).* From 31f5726f25741ad4c5c5df4694efa49b0d0903cb Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 30 Jan 2020 13:39:04 -0600 Subject: [PATCH 0187/1517] Set status in start_as_current_span too (#381) Fixes #377 --- .../src/opentelemetry/sdk/trace/__init__.py | 17 +++++++++++ opentelemetry-sdk/tests/trace/test_trace.py | 30 ++++++++++++------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 9829c8b33b..fd279e603a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -538,6 +538,23 @@ def use_span( self.source._current_span_slot.set( # pylint:disable=protected-access span_snapshot ) + + except Exception as error: # pylint: disable=broad-except + if ( + span.status is None + and span._set_status_on_exception # pylint:disable=protected-access # noqa + ): + span.set_status( + Status( + canonical_code=StatusCanonicalCode.UNKNOWN, + description="{}: {}".format( + type(error).__name__, error + ), + ) + ) + + raise + finally: if end_on_exit: span.end() diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 53a10518aa..449fb51e8e 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -641,16 +641,26 @@ def test_ended_span(self): ) def test_error_status(self): - try: - with trace.TracerSource().get_tracer(__name__).start_span( - "root" - ) as root: - raise Exception("unknown") - except Exception: # pylint: disable=broad-except - pass - - self.assertIs(root.status.canonical_code, StatusCanonicalCode.UNKNOWN) - self.assertEqual(root.status.description, "Exception: unknown") + def error_status_test(context): + with self.assertRaises(AssertionError): + with context as root: + raise AssertionError("unknown") + + self.assertIs( + root.status.canonical_code, StatusCanonicalCode.UNKNOWN + ) + self.assertEqual( + root.status.description, "AssertionError: unknown" + ) + + error_status_test( + trace.TracerSource().get_tracer(__name__).start_span("root") + ) + error_status_test( + trace.TracerSource() + .get_tracer(__name__) + .start_as_current_span("root") + ) def span_event_start_fmt(span_processor_name, span_name): From 682ecc2ffe1fc90bd9b83953792dbde92a8fbf09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Thu, 30 Jan 2020 14:43:14 -0500 Subject: [PATCH 0188/1517] ext/jaeger: export span kind (#387) Encode the span kind as a "span.kind" tag. --- ext/opentelemetry-ext-jaeger/CHANGELOG.md | 1 + .../src/opentelemetry/ext/jaeger/__init__.py | 1 + .../tests/test_jaeger_exporter.py | 17 ++++++++++++++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/ext/opentelemetry-ext-jaeger/CHANGELOG.md b/ext/opentelemetry-ext-jaeger/CHANGELOG.md index 90745ada0d..6cdbefa823 100644 --- a/ext/opentelemetry-ext-jaeger/CHANGELOG.md +++ b/ext/opentelemetry-ext-jaeger/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Export span status ([#367](https://github.com/open-telemetry/opentelemetry-python/pull/367)) +- Export span kind ([#387](https://github.com/open-telemetry/opentelemetry-python/pull/387)) ## 0.3a0 diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py index e990b7d97e..a90313d913 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -160,6 +160,7 @@ def _translate_to_jaeger(spans: Span): [ _get_long_tag("status.code", status.canonical_code.value), _get_string_tag("status.message", status.description), + _get_string_tag("span.kind", span.kind.name), ] ) diff --git a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py index e891393b42..23fce98b79 100644 --- a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py +++ b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py @@ -156,7 +156,7 @@ def test_translate_to_jaeger(self): context=other_context, attributes=link_attributes ) - default_status_tags = [ + default_tags = [ jaeger.Tag( key="status.code", vType=jaeger.TagType.LONG, @@ -165,6 +165,11 @@ def test_translate_to_jaeger(self): jaeger.Tag( key="status.message", vType=jaeger.TagType.STRING, vStr=None, ), + jaeger.Tag( + key="span.kind", + vType=jaeger.TagType.STRING, + vStr=trace_api.SpanKind.INTERNAL.name, + ), ] otel_spans = [ @@ -174,6 +179,7 @@ def test_translate_to_jaeger(self): parent=parent_context, events=(event,), links=(link,), + kind=trace_api.SpanKind.CLIENT, ), trace.Span( name=span_names[1], context=parent_context, parent=None @@ -234,6 +240,11 @@ def test_translate_to_jaeger(self): vType=jaeger.TagType.STRING, vStr="Example description", ), + jaeger.Tag( + key="span.kind", + vType=jaeger.TagType.STRING, + vStr=trace_api.SpanKind.CLIENT.name, + ), jaeger.Tag( key="error", vType=jaeger.TagType.BOOL, vBool=True, ), @@ -283,7 +294,7 @@ def test_translate_to_jaeger(self): startTime=start_times[1] // 10 ** 3, duration=durations[1] // 10 ** 3, flags=0, - tags=default_status_tags, + tags=default_tags, ), jaeger.Span( operationName=span_names[2], @@ -294,7 +305,7 @@ def test_translate_to_jaeger(self): startTime=start_times[2] // 10 ** 3, duration=durations[2] // 10 ** 3, flags=0, - tags=default_status_tags, + tags=default_tags, ), ] From 27f04b6d3a62710acf3b9e9e9c21a97a35ec018e Mon Sep 17 00:00:00 2001 From: Golovin Pavel Date: Fri, 31 Jan 2020 23:49:53 +0300 Subject: [PATCH 0189/1517] Add missing ABC for Metric (#391) Fix #390 --- opentelemetry-api/src/opentelemetry/metrics/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 947d57b976..c6b339be13 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -87,13 +87,14 @@ class DefaultLabelSet(LabelSet): """ -class Metric: +class Metric(abc.ABC): """Base class for various types of metrics. Metric class that inherit from this class are specialized with the type of handle that the metric holds. """ + @abc.abstractmethod def get_handle(self, label_set: LabelSet) -> "object": """Gets a handle, used for repeated-use of metrics instruments. From aecc3477b0431821015a0455492764baf90a0c36 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Fri, 31 Jan 2020 15:57:56 -0800 Subject: [PATCH 0190/1517] Use default TraceContext propagator in example (#188) --- .../flask_example.py | 10 +++----- .../tests/test_flask_example.py | 23 +++++++++---------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index ae484dd30e..f7a9872b6b 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -21,9 +21,8 @@ import requests import opentelemetry.ext.http_requests -from opentelemetry import propagators, trace +from opentelemetry import trace from opentelemetry.ext.flask import instrument_app -from opentelemetry.sdk.context.propagation.b3_format import B3Format from opentelemetry.sdk.trace import TracerSource @@ -43,17 +42,14 @@ def configure_opentelemetry(flask_app: flask.Flask): """ # Start by configuring all objects required to ensure # a complete end to end workflow. - # the preferred implementation of these objects must be set, + # The preferred implementation of these objects must be set, # as the opentelemetry-api defines the interface with a no-op # implementation. trace.set_preferred_tracer_source_implementation(lambda _: TracerSource()) + # Next, we need to configure how the values that are used by # traces and metrics are propagated (such as what specific headers # carry this value). - - # TBD: can remove once default TraceContext propagators are installed. - propagators.set_global_httptextformat(B3Format()) - # Integrations are the glue that binds the OpenTelemetry API # and the frameworks and libraries that are used together, automatically # creating Spans and propagating context as appropriate. diff --git a/examples/opentelemetry-example-app/tests/test_flask_example.py b/examples/opentelemetry-example-app/tests/test_flask_example.py index fd0b89e98c..69be9e4bfc 100644 --- a/examples/opentelemetry-example-app/tests/test_flask_example.py +++ b/examples/opentelemetry-example-app/tests/test_flask_example.py @@ -20,8 +20,8 @@ from werkzeug.wrappers import BaseResponse import opentelemetry_example_app.flask_example as flask_example -from opentelemetry.sdk import trace -from opentelemetry.sdk.context.propagation import b3_format +from opentelemetry import trace +from opentelemetry.sdk import trace as trace_sdk class TestFlaskExample(unittest.TestCase): @@ -46,7 +46,7 @@ def tearDown(self): self.send_patcher.stop() def test_full_path(self): - trace_id = trace.generate_trace_id() + trace_id = trace_sdk.generate_trace_id() # We need to use the Werkzeug test app because # The headers are injected at the wsgi layer. # The flask test app will not include these, and @@ -56,18 +56,17 @@ def test_full_path(self): client.get( "/", headers={ - "x-b3-traceid": b3_format.format_trace_id(trace_id), - "x-b3-spanid": b3_format.format_span_id( - trace.generate_span_id() - ), - "x-b3-sampled": "1", + "traceparent": "00-{:032x}-{:016x}-{:02x}".format( + trace_id, + trace_sdk.generate_span_id(), + trace.TraceOptions.SAMPLED, + ) }, ) # assert the http request header was propagated through. prepared_request = self.send.call_args[0][1] headers = prepared_request.headers - for required_header in {"x-b3-traceid", "x-b3-spanid", "x-b3-sampled"}: - self.assertIn(required_header, headers) - self.assertEqual( - headers["x-b3-traceid"], b3_format.format_trace_id(trace_id) + self.assertRegex( + headers["traceparent"], + r"00-{:032x}-[0-9a-f]{{16}}-01".format(trace_id), ) From fa1f50b1b31b46311a5324b66a4d9aa0b62e0c94 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 4 Feb 2020 10:12:30 -0800 Subject: [PATCH 0191/1517] Move alpha v4 release date target to 2/21 (#393) As discussed in the SIG call, move the alpha v4 release date three weeks back to account for ongoing work. --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b9b866833a..05f4df29bc 100644 --- a/README.md +++ b/README.md @@ -179,13 +179,13 @@ estimates, and subject to change. Future releases targets include: -| Component | Version | Target Date | -| ----------------------------------- | ---------- | ---------------- | -| Zipkin Trace Exporter | Alpha v0.4 | January 28 2020 | -| W3C Correlation Context Propagation | Alpha v0.4 | January 28 2020 | -| Support for Tags/Baggage | Alpha v0.4 | January 28 2020 | -| Metrics Aggregation | Alpha v0.4 | January 28 2020 | -| gRPC Integrations | Alpha v0.4 | January 28 2020 | -| Prometheus Metrics Exporter | Alpha v0.4 | January 28 2020 | -| OpenCensus Bridge | Alpha v0.4 | January 28 2020 | -| Metrics SDK (Complete) | Alpha v0.4 | January 28 2020 | +| Component | Version | Target Date | +| ----------------------------------- | ---------- | ----------------- | +| Zipkin Trace Exporter | Alpha v0.4 | February 21 2020 | +| W3C Correlation Context Propagation | Alpha v0.4 | February 21 2020 | +| Support for Tags/Baggage | Alpha v0.4 | February 21 2020 | +| Metrics Aggregation | Alpha v0.4 | February 21 2020 | +| gRPC Integrations | Alpha v0.4 | February 21 2020 | +| Prometheus Metrics Exporter | Alpha v0.4 | February 21 2020 | +| OpenCensus Bridge | Alpha v0.4 | February 21 2020 | +| Metrics SDK (Complete) | Alpha v0.4 | February 21 2020 | From 9ec1f1fba52973df3b8c7b22426927d3ac4244c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 4 Feb 2020 15:06:57 -0500 Subject: [PATCH 0192/1517] sdk: Implement force_flush for span processors (#389) https://github.com/open-telemetry/opentelemetry-specification/pull/370 added the requirement to have a "force_flush" method in the span processors. This commit exposes an already existing internal method on the batch span processor that does exactly the same, it also adds it to the span processor interface and as a no-op to the simple span processor. --- .../src/opentelemetry/sdk/trace/__init__.py | 5 +++ .../sdk/trace/export/__init__.py | 7 ++-- .../tests/trace/export/test_export.py | 36 ++++++++++++++++--- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index fd279e603a..23f1aaf79b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -70,6 +70,11 @@ def shutdown(self) -> None: """Called when a :class:`opentelemetry.sdk.trace.Tracer` is shutdown. """ + def force_flush(self) -> None: + """Export all ended spans to the configured Exporter that have not + yet been exported. + """ + class MultiSpanProcessor(SpanProcessor): """Implementation of :class:`SpanProcessor` that forwards all received diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index b70fb01019..5db2c1e957 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -83,6 +83,9 @@ def on_end(self, span: Span) -> None: def shutdown(self) -> None: self.span_exporter.shutdown() + def force_flush(self) -> None: + pass + class BatchExportSpanProcessor(SpanProcessor): """Batch span processor implementation. @@ -171,7 +174,7 @@ def worker(self): timeout = self.schedule_delay_millis / 1e3 - duration # be sure that all spans are sent - self._flush() + self.force_flush() def export(self) -> None: """Exports at most max_export_batch_size spans.""" @@ -197,7 +200,7 @@ def export(self) -> None: for index in range(idx): self.spans_list[index] = None - def _flush(self): + def force_flush(self): # export all elements until queue is empty while self.queue: self.export() diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 54fdee2629..43299ebe6a 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -97,7 +97,7 @@ def _create_start_and_end_span(name, span_processor): class TestBatchExportSpanProcessor(unittest.TestCase): - def test_batch_span_processor(self): + def test_shutdown(self): spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) @@ -109,9 +109,35 @@ def test_batch_span_processor(self): _create_start_and_end_span(name, span_processor) span_processor.shutdown() + self.assertTrue(my_exporter.is_shutdown) + + # check that spans are exported without an explicitly call to + # force_flush() self.assertListEqual(span_names, spans_names_list) - self.assertTrue(my_exporter.is_shutdown) + def test_flush(self): + spans_names_list = [] + + my_exporter = MySpanExporter(destination=spans_names_list) + span_processor = export.BatchExportSpanProcessor(my_exporter) + + span_names0 = ["xxx", "bar", "foo"] + span_names1 = ["yyy", "baz", "fox"] + + for name in span_names0: + _create_start_and_end_span(name, span_processor) + + span_processor.force_flush() + self.assertListEqual(span_names0, spans_names_list) + + # create some more spans to check that span processor still works + for name in span_names1: + _create_start_and_end_span(name, span_processor) + + span_processor.force_flush() + self.assertListEqual(span_names0 + span_names1, spans_names_list) + + span_processor.shutdown() def test_batch_span_processor_lossless(self): """Test that no spans are lost when sending max_queue_size spans""" @@ -127,8 +153,9 @@ def test_batch_span_processor_lossless(self): for _ in range(512): _create_start_and_end_span("foo", span_processor) - span_processor.shutdown() + span_processor.force_flush() self.assertEqual(len(spans_names_list), 512) + span_processor.shutdown() def test_batch_span_processor_many_spans(self): """Test that no spans are lost when sending many spans""" @@ -150,8 +177,9 @@ def test_batch_span_processor_many_spans(self): time.sleep(0.05) # give some time for the exporter to upload spans - span_processor.shutdown() + span_processor.force_flush() self.assertEqual(len(spans_names_list), 1024) + span_processor.shutdown() def test_batch_span_processor_scheduled_delay(self): """Test that spans are exported each schedule_delay_millis""" From ae484cba65f41a73ca3590d3e2f6eccb6eae4901 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 4 Feb 2020 12:37:16 -0800 Subject: [PATCH 0193/1517] Add OpenTracing shim example (#282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Diego Hurtado Co-authored-by: Christian Neumüller Co-authored-by: Mauricio Vásquez --- examples/opentracing/README.md | 90 ++++++++++++++++++ examples/opentracing/__init__.py | 0 .../images/jaeger-span-expanded.png | Bin 0 -> 171564 bytes .../opentracing/images/jaeger-trace-full.png | Bin 0 -> 164051 bytes examples/opentracing/main.py | 46 +++++++++ examples/opentracing/rediscache.py | 61 ++++++++++++ examples/opentracing/requirements.txt | 6 ++ .../tests/test_shim.py | 1 - 8 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 examples/opentracing/README.md create mode 100644 examples/opentracing/__init__.py create mode 100644 examples/opentracing/images/jaeger-span-expanded.png create mode 100644 examples/opentracing/images/jaeger-trace-full.png create mode 100755 examples/opentracing/main.py create mode 100644 examples/opentracing/rediscache.py create mode 100644 examples/opentracing/requirements.txt diff --git a/examples/opentracing/README.md b/examples/opentracing/README.md new file mode 100644 index 0000000000..2f7a926417 --- /dev/null +++ b/examples/opentracing/README.md @@ -0,0 +1,90 @@ +# Overview + +This example shows how to use the [`opentelemetry-ext-opentracing-shim` +package](https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-opentracing-shim) +to interact with libraries instrumented with +[`opentracing-python`](https://github.com/opentracing/opentracing-python). + +The included `rediscache` library creates spans via the OpenTracing Redis +integration, +[`redis_opentracing`](https://github.com/opentracing-contrib/python-redis). +Spans are exported via the Jaeger exporter, which is attached to the +OpenTelemetry tracer. + +## Installation + +### Jaeger + +Install and run +[Jaeger](https://www.jaegertracing.io/docs/latest/getting-started/#all-in-one). +See the [basic tracer +example](https://github.com/open-telemetry/opentelemetry-python/tree/master/examples/basic-tracer) +for more detail. + +### Redis + +Install Redis following the [instructions](https://redis.io/topics/quickstart). + +Make sure that the Redis server is running by executing this: + +```sh +$ redis-server +``` + +### Python Dependencies + +Install the Python dependencies in [`requirements.txt`](requirements.txt): + +```sh +$ pip install -r requirements.txt +``` + +Alternatively, you can install the Python dependencies separately: + +```sh +$ pip install \ + opentelemetry-api \ + opentelemetry-sdk \ + opentelemetry-ext-jaeger \ + opentelemetry-opentracing-shim \ + redis \ + redis_opentracing +``` + +## Run the Application + +The example script calculates a few Fibonacci numbers and stores the results in +Redis. The script, the `rediscache` library, and the OpenTracing Redis +integration all contribute spans to the trace. + +To run the script: + +```sh +$ python main.py +``` + +After running, you can view the generated trace in the Jaeger UI. + +#### Jaeger UI + +Open the Jaeger UI in your browser at + and view traces for the +"OpenTracing Shim Example" service. + +Each `main.py` run should generate a trace, and each trace should include +multiple spans that represent calls to Redis. + +

+ +Note that tags and logs (OpenTracing) and attributes and events (OpenTelemetry) +from both tracing systems appear in the exported trace. + +

+ +## Useful links +- For more information on OpenTelemetry, visit: +- For more information on tracing in Python, visit: + +## LICENSE + +Apache License 2.0 diff --git a/examples/opentracing/__init__.py b/examples/opentracing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/opentracing/images/jaeger-span-expanded.png b/examples/opentracing/images/jaeger-span-expanded.png new file mode 100644 index 0000000000000000000000000000000000000000..6da4b4b014d5d95cc1989e8436513302a4f657fe GIT binary patch literal 171564 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 NQdidbJzdGd`=2Fgo`e7Z literal 0 HcmV?d00001 diff --git a/examples/opentracing/images/jaeger-trace-full.png b/examples/opentracing/images/jaeger-trace-full.png new file mode 100644 index 0000000000000000000000000000000000000000..c28255bd1309182c0de47c7a8c0ed8fc9650d738 GIT binary patch literal 164051 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%0A1Mx;;G!u|Za9rL~ zmuR~>EJ^Ko^`_frY9y{Fj9|4pCUFwmq|)rul&04)_pdplmS% zLaj(%fB%t5b0NtN^X<2asV17FF2boNGkoAZ+*ZA(rhxe{(o^$UB@1#m3P=@1G>g^5 zO)BMEWEdfGK{OS|ZDJc^YLw{$CghB0XVC@88YX%r@_n}bxL<3s#&mEXGD&lI|XKdoXRV zz_2#%eyRv@zU<0R?)p0#ZUL_Q(7V0YD{ZmYA0IYzmX`a2QMQuq?C4XHxQ3axg873_ zRmi($o^ci_|N5b}_TrViw`BIzG~%MG4r|ZsHV}&1gp7+~oMfNMl8>AU_kNyxlq+uf z6!U4eqpUykc%(u5ps?NLNQP}YB96^_iRCoW5ASY>_>|kOHwazg_Xw0Z00}9w?CX%^E#aXj-ok^!d&` zogdeN#p>8aGb$|_vXy=YB-TPb@WiM)B6iGg9UQr~#cZZL;D|Lx*azQTK3@L$$RA03 z{|nYjkwVZ(^13v)H#?BdR{CBL2(A}0CeD$JRkAk&Ya+QSu;^v9W5oO-;MXPVu~erA zCtw}?fs@;R&aa!HFG�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%0Ao2j)UFb_;B9sKygjCajOgOhYI_doK2$Z~h4|GVp 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 z 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 Date: Fri, 7 Feb 2020 13:55:09 -0800 Subject: [PATCH 0194/1517] Add pymongo functional tests (#340) --- .travis.yml | 3 + .../tests/docker-compose.yml | 7 ++ .../tests/pymongo/test_pymongo_functional.py | 108 ++++++++++++++++++ ...pymongo_integration.py => test_pymongo.py} | 2 +- tox.ini | 23 +++- 5 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml create mode 100644 ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py rename ext/opentelemetry-ext-pymongo/tests/{test_pymongo_integration.py => test_pymongo.py} (99%) diff --git a/.travis.yml b/.travis.yml index 223cfbb587..e8133bbb85 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,9 @@ python: # allow_failures: # - python: '3.8-dev' +services: + - docker + install: - pip install tox-travis diff --git a/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml b/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml new file mode 100644 index 0000000000..1dab842f89 --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3' + +services: + otmongo: + ports: + - "27017:27017" + image: mongo:latest \ No newline at end of file diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py new file mode 100644 index 0000000000..4ef14fd789 --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py @@ -0,0 +1,108 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import typing +import unittest + +from pymongo import MongoClient + +from opentelemetry import trace as trace_api +from opentelemetry.ext.pymongo import trace_integration +from opentelemetry.sdk.trace import Span, Tracer, TracerSource +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +MONGODB_HOST = os.getenv("MONGODB_HOST ", "localhost") +MONGODB_PORT = int(os.getenv("MONGODB_PORT ", "27017")) +MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME ", "opentelemetry-tests") +MONGODB_COLLECTION_NAME = "test" + + +class TestFunctionalPymongo(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls._tracer_source = TracerSource() + cls._tracer = Tracer(cls._tracer_source, None) + cls._span_exporter = InMemorySpanExporter() + cls._span_processor = SimpleExportSpanProcessor(cls._span_exporter) + cls._tracer_source.add_span_processor(cls._span_processor) + trace_integration(cls._tracer) + client = MongoClient( + MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000 + ) + db = client[MONGODB_DB_NAME] + cls._collection = db[MONGODB_COLLECTION_NAME] + + def setUp(self): + self._span_exporter.clear() + + def validate_spans(self): + spans = self._span_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + for span in spans: + if span.name == "rootSpan": + root_span = span + else: + pymongo_span = span + self.assertIsInstance(span.start_time, int) + self.assertIsInstance(span.end_time, int) + self.assertIsNot(root_span, None) + self.assertIsNot(pymongo_span, None) + self.assertIsNotNone(pymongo_span.parent) + self.assertEqual(pymongo_span.parent.name, root_span.name) + self.assertIs(pymongo_span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual( + pymongo_span.attributes["db.instance"], MONGODB_DB_NAME + ) + self.assertEqual( + pymongo_span.attributes["net.peer.name"], MONGODB_HOST + ) + self.assertEqual( + pymongo_span.attributes["net.peer.port"], MONGODB_PORT + ) + + def test_insert(self): + """Should create a child span for insert + """ + with self._tracer.start_as_current_span("rootSpan"): + self._collection.insert_one( + {"name": "testName", "value": "testValue"} + ) + self.validate_spans() + + def test_update(self): + """Should create a child span for update + """ + with self._tracer.start_as_current_span("rootSpan"): + self._collection.update_one( + {"name": "testName"}, {"$set": {"value": "someOtherValue"}} + ) + self.validate_spans() + + def test_find(self): + """Should create a child span for find + """ + with self._tracer.start_as_current_span("rootSpan"): + self._collection.find_one() + self.validate_spans() + + def test_delete(self): + """Should create a child span for delete + """ + with self._tracer.start_as_current_span("rootSpan"): + self._collection.delete_one({"name": "testName"}) + self.validate_spans() diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py similarity index 99% rename from ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py rename to ext/opentelemetry-ext-pymongo/tests/test_pymongo.py index 6c99e09e71..0889d9d994 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py @@ -20,7 +20,7 @@ from opentelemetry.util import time_ns -class TestPymongoIntegration(unittest.TestCase): +class TestPymongo(unittest.TestCase): def test_trace_integration(self): mock_register = mock.Mock() patch = mock.patch( diff --git a/tox.ini b/tox.ini index d077d07893..4195e8629d 100644 --- a/tox.ini +++ b/tox.ini @@ -15,10 +15,11 @@ envlist = py37-tracecontext py37-{mypy,mypyinstalled} docs + docker-tests [travis] python = - 3.7: py37, lint, docs + 3.7: py37, lint, docs, docker-tests [testenv] deps = @@ -155,3 +156,23 @@ commands_pre = commands = {toxinidir}/scripts/tracecontext-integration-test.sh + +[testenv:docker-tests] +deps = + pytest + docker-compose >= 1.25.2 + pymongo ~= 3.1 + +changedir = + ext/opentelemetry-ext-docker-tests/tests + +commands_pre = + pip install -e {toxinidir}/opentelemetry-api \ + -e {toxinidir}/opentelemetry-sdk \ + -e {toxinidir}/ext/opentelemetry-ext-pymongo + - docker-compose up -d +commands = + pytest {posargs} + +commands_post = + docker-compose down \ No newline at end of file From ad7a809a6dda7fbf73d9f47a3ff2ed85752cd0af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 7 Feb 2020 17:30:29 -0500 Subject: [PATCH 0195/1517] sdk: fix force_flush in batch span processor (#397) #389 implemented force_flush() for the span processor. For BatchSpanProcessor it was implemented by exposing an already existing _flush() method, it created a race condition because the _flush() method was intended to be called only from the context of the worker thread, this because it uses the export() method that is not thread safe. The result after that PR is that some tests were failing randomly because export() was being executed in two different threads, the worker thread and the user thread calling force_flush(). This commit fixes it by implementing a more sophisticated flush mechanism. When a flush is requested, a special span token is inserted in the spans queue, a flag indicating a flush operation is on progress is set and the worker thread is waken up, after it a condition variable is monitored waiting for the worker thread to indicate that the token has been processed. The worker thread has a new logic to avoid sleeping (waiting on the condition variable) when there is a flush operation going on, it also notifies the caller (using another condition variable) when the token has been processed. --- .../src/opentelemetry/sdk/trace/__init__.py | 13 +++- .../sdk/trace/export/__init__.py | 62 ++++++++++++++++--- .../tests/trace/export/test_export.py | 33 ++++++++-- 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 23f1aaf79b..e429467061 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -70,9 +70,16 @@ def shutdown(self) -> None: """Called when a :class:`opentelemetry.sdk.trace.Tracer` is shutdown. """ - def force_flush(self) -> None: - """Export all ended spans to the configured Exporter that have not - yet been exported. + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Export all ended spans to the configured Exporter that have not yet + been exported. + + Args: + timeout_millis: The maximum amount of time to wait for spans to be + exported. + + Returns: + False if the timeout is exceeded, True otherwise. """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 5db2c1e957..3e2cc02c33 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -19,6 +19,7 @@ from enum import Enum from opentelemetry.context import Context +from opentelemetry.trace import DefaultSpan from opentelemetry.util import time_ns from .. import Span, SpanProcessor @@ -83,8 +84,9 @@ def on_end(self, span: Span) -> None: def shutdown(self) -> None: self.span_exporter.shutdown() - def force_flush(self) -> None: - pass + def force_flush(self, timeout_millis: int = 30000) -> bool: + # pylint: disable=unused-argument + return True class BatchExportSpanProcessor(SpanProcessor): @@ -94,6 +96,8 @@ class BatchExportSpanProcessor(SpanProcessor): batches ended spans and pushes them to the configured `SpanExporter`. """ + _FLUSH_TOKEN_SPAN = DefaultSpan(context=None) + def __init__( self, span_exporter: SpanExporter, @@ -123,6 +127,9 @@ def __init__( ) # type: typing.Deque[Span] self.worker_thread = threading.Thread(target=self.worker, daemon=True) self.condition = threading.Condition(threading.Lock()) + self.flush_condition = threading.Condition(threading.Lock()) + # flag to indicate that there is a flush operation on progress + self._flushing = False self.schedule_delay_millis = schedule_delay_millis self.max_export_batch_size = max_export_batch_size self.max_queue_size = max_queue_size @@ -156,7 +163,10 @@ def on_end(self, span: Span) -> None: def worker(self): timeout = self.schedule_delay_millis / 1e3 while not self.done: - if len(self.queue) < self.max_export_batch_size: + if ( + len(self.queue) < self.max_export_batch_size + and not self._flushing + ): with self.condition: self.condition.wait(timeout) if not self.queue: @@ -174,17 +184,21 @@ def worker(self): timeout = self.schedule_delay_millis / 1e3 - duration # be sure that all spans are sent - self.force_flush() + self._drain_queue() def export(self) -> None: """Exports at most max_export_batch_size spans.""" idx = 0 - + notify_flush = False # currently only a single thread acts as consumer, so queue.pop() will # not raise an exception while idx < self.max_export_batch_size and self.queue: - self.spans_list[idx] = self.queue.pop() - idx += 1 + span = self.queue.pop() + if span is self._FLUSH_TOKEN_SPAN: + notify_flush = True + else: + self.spans_list[idx] = span + idx += 1 with Context.use(suppress_instrumentation=True): try: # Ignore type b/c the Optional[None]+slicing is too "clever" @@ -196,15 +210,45 @@ def export(self) -> None: except Exception: logger.exception("Exception while exporting Span batch.") + if notify_flush: + with self.flush_condition: + self.flush_condition.notify() + # clean up list for index in range(idx): self.spans_list[index] = None - def force_flush(self): - # export all elements until queue is empty + def _drain_queue(self): + """"Export all elements until queue is empty. + + Can only be called from the worker thread context because it invokes + `export` that is not thread safe. + """ while self.queue: self.export() + def force_flush(self, timeout_millis: int = 30000) -> bool: + if self.done: + logger.warning("Already shutdown, ignoring call to force_flush().") + return True + + self._flushing = True + self.queue.appendleft(self._FLUSH_TOKEN_SPAN) + + # wake up worker thread + with self.condition: + self.condition.notify_all() + + # wait for token to be processed + with self.flush_condition: + ret = self.flush_condition.wait(timeout_millis / 1e3) + + self._flushing = False + + if not ret: + logger.warning("Timeout was exceeded in force_flush().") + return ret + def shutdown(self) -> None: # signal the worker thread to finish and then wait for it self.done = True diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 43299ebe6a..ea513a4858 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -14,6 +14,7 @@ import time import unittest +from logging import WARNING from unittest import mock from opentelemetry import trace as trace_api @@ -24,10 +25,16 @@ class MySpanExporter(export.SpanExporter): """Very simple span exporter used for testing.""" - def __init__(self, destination, max_export_batch_size=None): + def __init__( + self, + destination, + max_export_batch_size=None, + export_timeout_millis=0.0, + ): self.destination = destination self.max_export_batch_size = max_export_batch_size self.is_shutdown = False + self.export_timeout = export_timeout_millis / 1e3 def export(self, spans: trace.Span) -> export.SpanExportResult: if ( @@ -35,6 +42,7 @@ def export(self, spans: trace.Span) -> export.SpanExportResult: and len(spans) > self.max_export_batch_size ): raise ValueError("Batch is too big") + time.sleep(self.export_timeout) self.destination.extend(span.name for span in spans) return export.SpanExportResult.SUCCESS @@ -127,18 +135,33 @@ def test_flush(self): for name in span_names0: _create_start_and_end_span(name, span_processor) - span_processor.force_flush() + self.assertTrue(span_processor.force_flush()) self.assertListEqual(span_names0, spans_names_list) # create some more spans to check that span processor still works for name in span_names1: _create_start_and_end_span(name, span_processor) - span_processor.force_flush() + self.assertTrue(span_processor.force_flush()) self.assertListEqual(span_names0 + span_names1, spans_names_list) span_processor.shutdown() + def test_flush_timeout(self): + spans_names_list = [] + + my_exporter = MySpanExporter( + destination=spans_names_list, export_timeout_millis=500 + ) + span_processor = export.BatchExportSpanProcessor(my_exporter) + + _create_start_and_end_span("foo", span_processor) + + # check that the timeout is not meet + with self.assertLogs(level=WARNING): + self.assertFalse(span_processor.force_flush(100)) + span_processor.shutdown() + def test_batch_span_processor_lossless(self): """Test that no spans are lost when sending max_queue_size spans""" spans_names_list = [] @@ -153,7 +176,7 @@ def test_batch_span_processor_lossless(self): for _ in range(512): _create_start_and_end_span("foo", span_processor) - span_processor.force_flush() + self.assertTrue(span_processor.force_flush()) self.assertEqual(len(spans_names_list), 512) span_processor.shutdown() @@ -177,7 +200,7 @@ def test_batch_span_processor_many_spans(self): time.sleep(0.05) # give some time for the exporter to upload spans - span_processor.force_flush() + self.assertTrue(span_processor.force_flush()) self.assertEqual(len(spans_names_list), 1024) span_processor.shutdown() From 7229435e1d86679c5e9df380ddcafe2171efdb44 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 10 Feb 2020 18:42:42 -0800 Subject: [PATCH 0196/1517] Use travis pypy3 (#408) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e8133bbb85..67e3a58da1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ python: - '3.6' - '3.7' - '3.8' - - 'pypy3.5' + - 'pypy3' #matrix: # allow_failures: From fe99dbc9b0f27d0ccfcc7773fea963b7966221ab Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 11 Feb 2020 11:07:11 -0800 Subject: [PATCH 0197/1517] Metrics export pipeline + metrics stdout exporter (#341) Initial implementation of the end-to-end metrics pipeline. --- ...telemetry.sdk.metrics.export.aggregate.rst | 7 + ...entelemetry.sdk.metrics.export.batcher.rst | 11 + docs/opentelemetry.sdk.metrics.export.rst | 7 + docs/opentelemetry.sdk.metrics.rst | 8 + examples/metrics/record.py | 69 ++++++ examples/metrics/stateful.py | 72 ++++++ examples/metrics/stateless.py | 57 +++++ .../metrics_example.py | 12 +- .../tests/test_jaeger_exporter.py | 4 +- .../src/opentelemetry/ext/zipkin/__init__.py | 5 +- .../tests/test_zipkin_exporter.py | 9 +- .../src/opentelemetry/metrics/__init__.py | 76 +++++- .../tests/metrics/test_metrics.py | 39 +++- .../src/opentelemetry/sdk/metrics/__init__.py | 185 ++++++++++----- .../sdk/metrics/export/__init__.py | 33 +-- .../sdk/metrics/export/aggregate.py | 58 +++++ .../sdk/metrics/export/batcher.py | 100 ++++++++ .../sdk/metrics/export/controller.py | 56 +++++ .../src/opentelemetry/sdk/trace/__init__.py | 4 +- .../tests/metrics/export/test_export.py | 221 +++++++++++++++++- .../tests/metrics/test_metrics.py | 182 ++++++++++----- opentelemetry-sdk/tests/trace/test_trace.py | 5 +- scripts/eachdist.py | 2 +- 23 files changed, 1052 insertions(+), 170 deletions(-) create mode 100644 docs/opentelemetry.sdk.metrics.export.aggregate.rst create mode 100644 docs/opentelemetry.sdk.metrics.export.batcher.rst create mode 100644 docs/opentelemetry.sdk.metrics.export.rst create mode 100644 examples/metrics/record.py create mode 100644 examples/metrics/stateful.py create mode 100644 examples/metrics/stateless.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py diff --git a/docs/opentelemetry.sdk.metrics.export.aggregate.rst b/docs/opentelemetry.sdk.metrics.export.aggregate.rst new file mode 100644 index 0000000000..7c9306c684 --- /dev/null +++ b/docs/opentelemetry.sdk.metrics.export.aggregate.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.metrics.export.aggregate +========================================== + +.. automodule:: opentelemetry.sdk.metrics.export.aggregate + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.sdk.metrics.export.batcher.rst b/docs/opentelemetry.sdk.metrics.export.batcher.rst new file mode 100644 index 0000000000..5dbd1d6e58 --- /dev/null +++ b/docs/opentelemetry.sdk.metrics.export.batcher.rst @@ -0,0 +1,11 @@ +opentelemetry.sdk.metrics.export.batcher +========================================== + +.. toctree:: + + opentelemetry.sdk.metrics.export + +.. automodule:: opentelemetry.sdk.metrics.export.batcher + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.sdk.metrics.export.rst b/docs/opentelemetry.sdk.metrics.export.rst new file mode 100644 index 0000000000..1ae51170e4 --- /dev/null +++ b/docs/opentelemetry.sdk.metrics.export.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.metrics.export +========================================== + +.. automodule:: opentelemetry.sdk.metrics.export + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.sdk.metrics.rst b/docs/opentelemetry.sdk.metrics.rst index 6d646c3b15..ec8687dd2d 100644 --- a/docs/opentelemetry.sdk.metrics.rst +++ b/docs/opentelemetry.sdk.metrics.rst @@ -1,6 +1,14 @@ opentelemetry.sdk.metrics package ========================================== +Submodules +---------- + +.. toctree:: + + opentelemetry.sdk.metrics.export.aggregate + opentelemetry.sdk.metrics.export.batcher + .. automodule:: opentelemetry.sdk.metrics :members: :undoc-members: diff --git a/examples/metrics/record.py b/examples/metrics/record.py new file mode 100644 index 0000000000..be68c8083f --- /dev/null +++ b/examples/metrics/record.py @@ -0,0 +1,69 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This module serves as an example for a simple application using metrics. +It demonstrates the different ways you can record metrics via the meter. +""" +import time + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.controller import PushController + +# Meter is responsible for creating and recording metrics +metrics.set_preferred_meter_implementation(lambda _: Meter()) +meter = metrics.meter() +# exporter to export metrics to the console +exporter = ConsoleMetricsExporter() +# controller collects metrics created from meter and exports it via the +# exporter every interval +controller = PushController(meter, exporter, 5) + +# Example to show how to record using the meter +counter = meter.create_metric( + "requests", "number of requests", 1, int, Counter, ("environment",) +) + +counter2 = meter.create_metric( + "clicks", "number of clicks", 1, int, Counter, ("environment",) +) + +# Labelsets are used to identify key-values that are associated with a specific +# metric that you want to record. These are useful for pre-aggregation and can +# be used to store custom dimensions pertaining to a metric + +# The meter takes a dictionary of key value pairs +label_set = meter.get_label_set({"environment": "staging"}) + +# Handle usage +# You can record metrics with metric handles. Handles are created by passing in +# a labelset. A handle is essentially metric data that corresponds to a specific +# set of labels. Therefore, getting a handle using the same set of labels will +# yield the same metric handle. +counter_handle = counter.get_handle(label_set) +counter_handle.add(100) + +# Direct metric usage +# You can record metrics directly using the metric instrument. You pass in a +# labelset that you would like to record for. +counter.add(25, label_set) + +# Record batch usage +# You can record metrics in a batch by passing in a labelset and a sequence of +# (metric, value) pairs. The value would be recorded for each metric using the +# specified labelset for each. +meter.record_batch(label_set, [(counter, 50), (counter2, 70)]) +time.sleep(100) diff --git a/examples/metrics/stateful.py b/examples/metrics/stateful.py new file mode 100644 index 0000000000..c43f795e22 --- /dev/null +++ b/examples/metrics/stateful.py @@ -0,0 +1,72 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This module serves as an example for a simple application using metrics +Examples show how to recording affects the collection of metrics to be exported +""" +import time + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.controller import PushController + +# Batcher used to collect all created metrics from meter ready for exporting +# Pass in true/false to indicate whether the batcher is stateful. True +# indicates the batcher computes checkpoints from over the process lifetime. +# False indicates the batcher computes checkpoints which describe the updates +# of a single collection period (deltas) +batcher = UngroupedBatcher(True) +# If a batcher is not provded, a default batcher is used +# Meter is responsible for creating and recording metrics +metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) +meter = metrics.meter() +# exporter to export metrics to the console +exporter = ConsoleMetricsExporter() +# controller collects metrics created from meter and exports it via the +# exporter every interval +controller = PushController(meter, exporter, 5) + +counter = meter.create_metric( + "requests", "number of requests", 1, int, Counter, ("environment",) +) + +counter2 = meter.create_metric( + "clicks", "number of clicks", 1, int, Counter, ("environment",) +) + +# Labelsets are used to identify key-values that are associated with a specific +# metric that you want to record. These are useful for pre-aggregation and can +# be used to store custom dimensions pertaining to a metric +label_set = meter.get_label_set({"environment": "staging"}) +label_set2 = meter.get_label_set({"environment": "testing"}) + +counter.add(25, label_set) +# We sleep for 5 seconds, exported value should be 25 +time.sleep(5) + +counter.add(50, label_set) +# exported value should be 75 +time.sleep(5) + +counter.add(35, label_set2) +# should be two exported values 75 and 35, one for each labelset +time.sleep(5) + +counter2.add(5, label_set) +# should be three exported values, labelsets can be reused for different +# metrics but will be recorded seperately, 75, 35 and 5 +time.sleep(5) diff --git a/examples/metrics/stateless.py b/examples/metrics/stateless.py new file mode 100644 index 0000000000..69213cbddd --- /dev/null +++ b/examples/metrics/stateless.py @@ -0,0 +1,57 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This module serves as an example for a simple application using metrics +Examples show how to recording affects the collection of metrics to be exported +""" +import time + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.controller import PushController + +# Batcher used to collect all created metrics from meter ready for exporting +# Pass in false for non-stateful batcher. Indicates the batcher computes +# checkpoints which describe the updates of a single collection period (deltas) +batcher = UngroupedBatcher(False) +# Meter is responsible for creating and recording metrics +metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) +meter = metrics.meter() +# exporter to export metrics to the console +exporter = ConsoleMetricsExporter() +# controller collects metrics created from meter and exports it via the +# exporter every interval +controller = PushController(meter, exporter, 5) + +counter = meter.create_metric( + "requests", "number of requests", 1, int, Counter, ("environment",) +) + +# Labelsets are used to identify key-values that are associated with a specific +# metric that you want to record. These are useful for pre-aggregation and can +# be used to store custom dimensions pertaining to a metric +label_set = meter.get_label_set({"environment": "staging"}) + +counter.add(25, label_set) +# We sleep for 5 seconds, exported value should be 25 +time.sleep(5) + +counter.add(50, label_set) +# exported value should be 50 due to non-stateful batcher +time.sleep(20) + +# Following exported values would be 0 diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py index 246d6c3507..2f42361902 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py @@ -18,8 +18,12 @@ from opentelemetry import metrics from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.controller import PushController -metrics.set_preferred_meter_implementation(lambda _: Meter()) +batcher = UngroupedBatcher(True) +metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) meter = metrics.meter() counter = meter.create_metric( "available memory", @@ -33,7 +37,7 @@ label_set = meter.get_label_set({"environment": "staging"}) # Direct metric usage -counter.add(label_set, 25) +counter.add(25, label_set) # Handle usage counter_handle = counter.get_handle(label_set) @@ -41,6 +45,6 @@ # Record batch usage meter.record_batch(label_set, [(counter, 50)]) -print(counter_handle.data) -# TODO: exporters +exporter = ConsoleMetricsExporter() +controller = PushController(meter, exporter, 5) diff --git a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py index 23fce98b79..08c5a4aded 100644 --- a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py +++ b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py @@ -163,7 +163,7 @@ def test_translate_to_jaeger(self): vLong=StatusCanonicalCode.OK.value, ), jaeger.Tag( - key="status.message", vType=jaeger.TagType.STRING, vStr=None, + key="status.message", vType=jaeger.TagType.STRING, vStr=None ), jaeger.Tag( key="span.kind", @@ -246,7 +246,7 @@ def test_translate_to_jaeger(self): vStr=trace_api.SpanKind.CLIENT.name, ), jaeger.Tag( - key="error", vType=jaeger.TagType.BOOL, vBool=True, + key="error", vType=jaeger.TagType.BOOL, vBool=True ), ], references=[ diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index e0b5791d1e..fec4da8c3e 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -101,10 +101,7 @@ def export(self, spans: Sequence[Span]) -> SpanExportResult: def _translate_to_zipkin(self, spans: Sequence[Span]): - local_endpoint = { - "serviceName": self.service_name, - "port": self.port, - } + local_endpoint = {"serviceName": self.service_name, "port": self.port} if self.ipv4 is not None: local_endpoint["ipv4"] = self.ipv4 diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py index e2bdb41305..467bc610bd 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -114,9 +114,7 @@ def test_export(self): ) span_context = trace_api.SpanContext( - trace_id, - span_id, - trace_options=TraceOptions(TraceOptions.SAMPLED), + trace_id, span_id, trace_options=TraceOptions(TraceOptions.SAMPLED) ) parent_context = trace_api.SpanContext(trace_id, parent_id) other_context = trace_api.SpanContext(trace_id, other_id) @@ -168,10 +166,7 @@ def test_export(self): otel_spans[2].end(end_time=end_times[2]) service_name = "test-service" - local_endpoint = { - "serviceName": service_name, - "port": 9411, - } + local_endpoint = {"serviceName": service_name, "port": 9411} exporter = ZipkinSpanExporter(service_name) expected = [ diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index c6b339be13..3e04354d3c 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -40,13 +40,34 @@ class DefaultMetricHandle: Used when no MetricHandle implementation is available. """ + def add(self, value: ValueT) -> None: + """No-op implementation of `CounterHandle` add. + + Args: + value: The value to add to the handle. + """ + + def set(self, value: ValueT) -> None: + """No-op implementation of `GaugeHandle` set. + + Args: + value: The value to set to the handle. + """ + + def record(self, value: ValueT) -> None: + """No-op implementation of `MeasureHandle` record. + + Args: + value: The value to record to the handle. + """ + class CounterHandle: def add(self, value: ValueT) -> None: """Increases the value of the handle by ``value``. Args: - value: The value to record to the handle. + value: The value to add to the handle. """ @@ -55,7 +76,7 @@ def set(self, value: ValueT) -> None: """Sets the current value of the handle to ``value``. Args: - value: The value to record to the handle. + value: The value to set to the handle. """ @@ -121,6 +142,30 @@ def get_handle(self, label_set: LabelSet) -> "DefaultMetricHandle": """ return DefaultMetricHandle() + def add(self, value: ValueT, label_set: LabelSet) -> None: + """No-op implementation of `Counter` add. + + Args: + value: The value to add to the counter metric. + label_set: `LabelSet` to associate with the returned handle. + """ + + def set(self, value: ValueT, label_set: LabelSet) -> None: + """No-op implementation of `Gauge` set. + + Args: + value: The value to set the gauge metric to. + label_set: `LabelSet` to associate with the returned handle. + """ + + def record(self, value: ValueT, label_set: LabelSet) -> None: + """No-op implementation of `Measure` record. + + Args: + value: The value to record to this measure metric. + label_set: `LabelSet` to associate with the returned handle. + """ + class Counter(Metric): """A counter type metric that expresses the computation of a sum.""" @@ -129,12 +174,12 @@ def get_handle(self, label_set: LabelSet) -> "CounterHandle": """Gets a `CounterHandle`.""" return CounterHandle() - def add(self, label_set: LabelSet, value: ValueT) -> None: + def add(self, value: ValueT, label_set: LabelSet) -> None: """Increases the value of the counter by ``value``. Args: - label_set: `LabelSet` to associate with the returned handle. value: The value to add to the counter metric. + label_set: `LabelSet` to associate with the returned handle. """ @@ -151,12 +196,12 @@ def get_handle(self, label_set: LabelSet) -> "GaugeHandle": """Gets a `GaugeHandle`.""" return GaugeHandle() - def set(self, label_set: LabelSet, value: ValueT) -> None: + def set(self, value: ValueT, label_set: LabelSet) -> None: """Sets the value of the gauge to ``value``. Args: - label_set: `LabelSet` to associate with the returned handle. value: The value to set the gauge metric to. + label_set: `LabelSet` to associate with the returned handle. """ @@ -172,12 +217,12 @@ def get_handle(self, label_set: LabelSet) -> "MeasureHandle": """Gets a `MeasureHandle` with a float value.""" return MeasureHandle() - def record(self, label_set: LabelSet, value: ValueT) -> None: + def record(self, value: ValueT, label_set: LabelSet) -> None: """Records the ``value`` to the measure. Args: - label_set: `LabelSet` to associate with the returned handle. value: The value to record to this measure metric. + label_set: `LabelSet` to associate with the returned handle. """ @@ -224,6 +269,7 @@ def create_metric( label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, + absolute: bool = True, ) -> "Metric": """Creates a ``metric_kind`` metric with type ``value_type``. @@ -235,8 +281,10 @@ def create_metric( metric_type: The type of metric being created. label_keys: The keys for the labels with dynamic values. enabled: Whether to report the metric by default. - monotonic: Whether to only allow non-negative values. - + monotonic: Configure a counter or gauge that accepts only + monotonic/non-monotonic updates. + absolute: Configure a measure that does or does not accept negative + updates. Returns: A new ``metric_type`` metric with values of ``value_type``. """ @@ -271,6 +319,7 @@ def create_metric( label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, + absolute: bool = True, ) -> "Metric": # pylint: disable=no-self-use return DefaultMetric() @@ -298,7 +347,12 @@ def meter() -> Meter: if _METER is None: # pylint:disable=protected-access - _METER = loader._load_impl(DefaultMeter, _METER_FACTORY) + try: + _METER = loader._load_impl(Meter, _METER_FACTORY) # type: ignore + except TypeError: + # if we raised an exception trying to instantiate an + # abstract class, default to no-op tracer impl + _METER = DefaultMeter() del _METER_FACTORY return _METER diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index a8959266b2..3ec0f81c71 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -13,6 +13,8 @@ # limitations under the License. import unittest +from contextlib import contextmanager +from unittest import mock from opentelemetry import metrics @@ -52,7 +54,7 @@ def test_counter(self): def test_counter_add(self): counter = metrics.Counter() label_set = metrics.LabelSet() - counter.add(label_set, 1) + counter.add(1, label_set) def test_gauge(self): gauge = metrics.Gauge() @@ -63,7 +65,7 @@ def test_gauge(self): def test_gauge_set(self): gauge = metrics.Gauge() label_set = metrics.LabelSet() - gauge.set(label_set, 1) + gauge.set(1, label_set) def test_measure(self): measure = metrics.Measure() @@ -74,7 +76,7 @@ def test_measure(self): def test_measure_record(self): measure = metrics.Measure() label_set = metrics.LabelSet() - measure.record(label_set, 1) + measure.record(1, label_set) def test_default_handle(self): metrics.DefaultMetricHandle() @@ -90,3 +92,34 @@ def test_gauge_handle(self): def test_measure_handle(self): handle = metrics.MeasureHandle() handle.record(1) + + +@contextmanager +# type: ignore +def patch_metrics_globals(meter=None, meter_factory=None): + """Mock metrics._METER and metrics._METER_FACTORY. + + This prevents previous changes to these values from affecting the code in + this scope, and prevents changes in this scope from leaking out and + affecting other tests. + """ + with mock.patch("opentelemetry.metrics._METER", meter): + with mock.patch("opentelemetry.metrics._METER_FACTORY", meter_factory): + yield + + +class TestGlobals(unittest.TestCase): + def test_meter_default_factory(self): + """Check that the default meter is a DefaultMeter.""" + with patch_metrics_globals(): + meter = metrics.meter() + self.assertIsInstance(meter, metrics.DefaultMeter) + # Check that we don't create a new instance on each call + self.assertIs(meter, metrics.meter()) + + def test_meter_custom_factory(self): + """Check that we use the provided factory for custom global meters.""" + mock_meter = mock.Mock(metrics.Meter) + with patch_metrics_globals(meter_factory=lambda _: mock_meter): + meter = metrics.meter() + self.assertIs(meter, mock_meter) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index f0c3e0e6d3..ea16878a7b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -17,6 +17,8 @@ from typing import Dict, Sequence, Tuple, Type from opentelemetry import metrics as metrics_api +from opentelemetry.sdk.metrics.export.aggregate import Aggregator +from opentelemetry.sdk.metrics.export.batcher import Batcher, UngroupedBatcher from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -27,20 +29,52 @@ class LabelSet(metrics_api.LabelSet): """See `opentelemetry.metrics.LabelSet`.""" def __init__(self, labels: Dict[str, str] = None): - self.labels = labels + if labels is None: + labels = {} + # LabelSet properties used only in dictionaries for fast lookup + self._labels = tuple(labels.items()) + self._encoded = tuple(sorted(labels.items())) + + @property + def labels(self): + return self._labels + + def __hash__(self): + return hash(self._encoded) + + def __eq__(self, other): + return self._encoded == other._encoded class BaseHandle: + """The base handle class containing common behavior for all handles. + + Handles are responsible for operating on data for metric instruments for a + specific set of labels. + + Args: + value_type: The type of values this handle holds (int, float). + enabled: True if the originating instrument is enabled. + monotonic: Indicates acceptance of only monotonic/non-monotonic values + for updating counter and gauge handles. + absolute: Indicates acceptance of negative updates to measure handles. + aggregator: The aggregator for this handle. Will handle aggregation + upon updates and checkpointing of values for exporting. + """ + def __init__( self, value_type: Type[metrics_api.ValueT], enabled: bool, monotonic: bool, + absolute: bool, + aggregator: Aggregator, ): - self.data = value_type() self.value_type = value_type self.enabled = enabled self.monotonic = monotonic + self.absolute = absolute + self.aggregator = aggregator self.last_update_timestamp = time_ns() def _validate_update(self, value: metrics_api.ValueT) -> bool: @@ -53,9 +87,15 @@ def _validate_update(self, value: metrics_api.ValueT) -> bool: return False return True + def update(self, value: metrics_api.ValueT): + self.last_update_timestamp = time_ns() + self.aggregator.update(value) + def __repr__(self): return '{}(data="{}", last_update_timestamp={})'.format( - type(self).__name__, self.data, self.last_update_timestamp + type(self).__name__, + self.aggregator.current, + self.last_update_timestamp, ) @@ -66,34 +106,37 @@ def add(self, value: metrics_api.ValueT) -> None: if self.monotonic and value < 0: logger.warning("Monotonic counter cannot descend.") return - self.last_update_timestamp = time_ns() - self.data += value + self.update(value) class GaugeHandle(metrics_api.GaugeHandle, BaseHandle): def set(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.GaugeHandle.set`.""" if self._validate_update(value): - if self.monotonic and value < self.data: + if self.monotonic and value < self.aggregator.current: logger.warning("Monotonic gauge cannot descend.") return - self.last_update_timestamp = time_ns() - self.data = value + self.update(value) class MeasureHandle(metrics_api.MeasureHandle, BaseHandle): def record(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.MeasureHandle.record`.""" if self._validate_update(value): - if self.monotonic and value < 0: - logger.warning("Monotonic measure cannot accept negatives.") + if self.absolute and value < 0: + logger.warning("Absolute measure cannot accept negatives.") return - self.last_update_timestamp = time_ns() - # TODO: record + self.update(value) class Metric(metrics_api.Metric): - """See `opentelemetry.metrics.Metric`.""" + """Base class for all metric types. + + Also known as metric instrument. This is the class that is used to + represent a metric that is to be continuously recorded and tracked. Each + metric has a set of handles that are created from the metric. See + `BaseHandle` for information on handles. + """ HANDLE_TYPE = BaseHandle @@ -103,17 +146,21 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], + meter: "Meter", label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, + absolute: bool = True, ): self.name = name self.description = description self.unit = unit self.value_type = value_type + self.meter = meter self.label_keys = label_keys self.enabled = enabled self.monotonic = monotonic + self.absolute = absolute self.handles = {} def get_handle(self, label_set: LabelSet) -> BaseHandle: @@ -121,9 +168,14 @@ def get_handle(self, label_set: LabelSet) -> BaseHandle: handle = self.handles.get(label_set) if not handle: handle = self.HANDLE_TYPE( - self.value_type, self.enabled, self.monotonic + self.value_type, + self.enabled, + self.monotonic, + self.absolute, + # Aggregator will be created based off type of metric + self.meter.batcher.aggregator_for(self.__class__), ) - self.handles[label_set] = handle + self.handles[label_set] = handle return handle def __repr__(self): @@ -138,8 +190,8 @@ class Counter(Metric, metrics_api.Counter): """See `opentelemetry.metrics.Counter`. By default, counter values can only go up (monotonic). Negative inputs - will be discarded for monotonic counter metrics. Counter metrics that - have a monotonic option set to False allows negative inputs. + will be rejected for monotonic counter metrics. Counter metrics that have a + monotonic option set to False allows negative inputs. """ HANDLE_TYPE = CounterHandle @@ -150,21 +202,25 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], + meter: "Meter", label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = True, + absolute: bool = False, ): super().__init__( name, description, unit, value_type, + meter, label_keys=label_keys, enabled=enabled, monotonic=monotonic, + absolute=absolute, ) - def add(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: + def add(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: """See `opentelemetry.metrics.Counter.add`.""" self.get_handle(label_set).add(value) @@ -175,7 +231,7 @@ class Gauge(Metric, metrics_api.Gauge): """See `opentelemetry.metrics.Gauge`. By default, gauge values can go both up and down (non-monotonic). - Negative inputs will be discarded for monotonic gauge metrics. + Negative inputs will be rejected for monotonic gauge metrics. """ HANDLE_TYPE = GaugeHandle @@ -186,21 +242,25 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], + meter: "Meter", label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, + absolute: bool = False, ): super().__init__( name, description, unit, value_type, + meter, label_keys=label_keys, enabled=enabled, monotonic=monotonic, + absolute=absolute, ) - def set(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: + def set(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: """See `opentelemetry.metrics.Gauge.set`.""" self.get_handle(label_set).set(value) @@ -208,50 +268,58 @@ def set(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: class Measure(Metric, metrics_api.Measure): - """See `opentelemetry.metrics.Measure`. - - By default, measure metrics can accept both positive and negatives. - Negative inputs will be discarded when monotonic is True. - """ + """See `opentelemetry.metrics.Measure`.""" HANDLE_TYPE = MeasureHandle - def __init__( - self, - name: str, - description: str, - unit: str, - value_type: Type[metrics_api.ValueT], - label_keys: Sequence[str] = (), - enabled: bool = False, - monotonic: bool = False, - ): - super().__init__( - name, - description, - unit, - value_type, - label_keys=label_keys, - enabled=enabled, - monotonic=monotonic, - ) - - def record(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: + def record(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: """See `opentelemetry.metrics.Measure.record`.""" self.get_handle(label_set).record(value) UPDATE_FUNCTION = record +class Record: + """Container class used for processing in the `Batcher`""" + + def __init__( + self, metric: Metric, label_set: LabelSet, aggregator: Aggregator + ): + self.metric = metric + self.label_set = label_set + self.aggregator = aggregator + + # Used when getting a LabelSet with no key/values EMPTY_LABEL_SET = LabelSet() class Meter(metrics_api.Meter): - """See `opentelemetry.metrics.Meter`.""" + """See `opentelemetry.metrics.Meter`. + + Args: + batcher: The `Batcher` used for this meter. + """ - def __init__(self): - self.labels = {} + def __init__(self, batcher: Batcher = UngroupedBatcher(True)): + self.batcher = batcher + self.metrics = set() + + def collect(self) -> None: + """Collects all the metrics created with this `Meter` for export. + + Utilizes the batcher to create checkpoints of the current values in + each aggregator belonging to the metrics that were created with this + meter instance. + """ + for metric in self.metrics: + if metric.enabled: + for label_set, handle in metric.handles.items(): + # TODO: Consider storing records in memory? + record = Record(metric, label_set, handle.aggregator) + # Checkpoints the current aggregators + # Applies different batching logic based on type of batcher + self.batcher.process(record) def record_batch( self, @@ -260,7 +328,7 @@ def record_batch( ) -> None: """See `opentelemetry.metrics.Meter.record_batch`.""" for metric, value in record_tuples: - metric.UPDATE_FUNCTION(label_set, value) + metric.UPDATE_FUNCTION(value, label_set) def create_metric( self, @@ -272,18 +340,23 @@ def create_metric( label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, + absolute: bool = True, ) -> metrics_api.MetricT: """See `opentelemetry.metrics.Meter.create_metric`.""" # Ignore type b/c of mypy bug in addition to missing annotations - return metric_type( # type: ignore + metric = metric_type( # type: ignore name, description, unit, value_type, + self, label_keys=label_keys, enabled=enabled, monotonic=monotonic, + absolute=absolute, ) + self.metrics.add(metric) + return metric def get_label_set(self, labels: Dict[str, str]): """See `opentelemetry.metrics.Meter.create_metric`. @@ -295,12 +368,4 @@ def get_label_set(self, labels: Dict[str, str]): """ if len(labels) == 0: return EMPTY_LABEL_SET - # Use simple encoding for now until encoding API is implemented - encoded = tuple(sorted(labels.items())) - # If LabelSet exists for this meter in memory, use existing one - if encoded not in self.labels: - self.labels[encoded] = LabelSet(labels=labels) - return self.labels[encoded] - - -meter = Meter() + return LabelSet(labels=labels) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index b6cb396331..6901a4efe4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -15,8 +15,6 @@ from enum import Enum from typing import Sequence, Tuple -from .. import Metric - class MetricsExportResult(Enum): SUCCESS = 0 @@ -24,6 +22,13 @@ class MetricsExportResult(Enum): FAILED_NOT_RETRYABLE = 2 +class MetricRecord: + def __init__(self, aggregator, label_set, metric): + self.aggregator = aggregator + self.label_set = label_set + self.metric = metric + + class MetricsExporter: """Interface for exporting metrics. @@ -32,15 +37,15 @@ class MetricsExporter: """ def export( - self, metric_tuples: Sequence[Tuple[Metric, Sequence[str]]] + self, metric_records: Sequence[MetricRecord] ) -> "MetricsExportResult": """Exports a batch of telemetry data. Args: - metric_tuples: A sequence of metric pairs. A metric pair consists - of a `Metric` and a sequence of strings. The sequence of - strings will be used to get the corresponding `MetricHandle` - from the `Metric` to export. + metric_records: A sequence of `MetricRecord` s. A `MetricRecord` + contains the metric to be exported, the label set associated + with that metric, as well as the aggregator used to export the + current checkpointed value. Returns: The result of the export @@ -57,17 +62,19 @@ class ConsoleMetricsExporter(MetricsExporter): """Implementation of `MetricsExporter` that prints metrics to the console. This class can be used for diagnostic purposes. It prints the exported - metric handles to the console STDOUT. + metrics to the console STDOUT. """ def export( - self, metric_tuples: Sequence[Tuple[Metric, Sequence[str]]] + self, metric_records: Sequence[MetricRecord] ) -> "MetricsExportResult": - for metric, label_values in metric_tuples: - handle = metric.get_handle(label_values) + for record in metric_records: print( - '{}(data="{}", label_values="{}", metric_data={})'.format( - type(self).__name__, metric, label_values, handle + '{}(data="{}", label_set="{}", value={})'.format( + type(self).__name__, + record.metric, + record.label_set.labels, + record.aggregator.checkpoint, ) ) return MetricsExportResult.SUCCESS diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py new file mode 100644 index 0000000000..642fe1cdfe --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -0,0 +1,58 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc + + +class Aggregator(abc.ABC): + """Base class for aggregators. + + Aggregators are responsible for holding aggregated values and taking a + snapshot of these values upon export (checkpoint). + """ + + def __init__(self): + self.current = None + self.checkpoint = None + + @abc.abstractmethod + def update(self, value): + """Updates the current with the new value.""" + + @abc.abstractmethod + def take_checkpoint(self): + """Stores a snapshot of the current value.""" + + @abc.abstractmethod + def merge(self, other): + """Combines two aggregator values.""" + + +class CounterAggregator(Aggregator): + """Aggregator for Counter metrics.""" + + def __init__(self): + super().__init__() + self.current = 0 + self.checkpoint = 0 + + def update(self, value): + self.current += value + + def take_checkpoint(self): + self.checkpoint = self.current + self.current = 0 + + def merge(self, other): + self.checkpoint += other.checkpoint diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py new file mode 100644 index 0000000000..c81db0fe74 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -0,0 +1,100 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +from typing import Sequence, Type + +from opentelemetry.metrics import Counter, MetricT +from opentelemetry.sdk.metrics.export import MetricRecord +from opentelemetry.sdk.metrics.export.aggregate import ( + Aggregator, + CounterAggregator, +) + + +class Batcher(abc.ABC): + """Base class for all batcher types. + + The batcher is responsible for storing the aggregators and aggregated + values received from updates from metrics in the meter. The stored values + will be sent to an exporter for exporting. + """ + + def __init__(self, stateful: bool): + self._batch_map = {} + # stateful=True indicates the batcher computes checkpoints from over + # the process lifetime. False indicates the batcher computes + # checkpoints which describe the updates of a single collection period + # (deltas) + self.stateful = stateful + + def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: + """Returns an aggregator based on metric type. + + Aggregators keep track of and updates values when metrics get updated. + """ + # pylint:disable=R0201 + if metric_type == Counter: + return CounterAggregator() + # TODO: Add other aggregators + return CounterAggregator() + + def checkpoint_set(self) -> Sequence[MetricRecord]: + """Returns a list of MetricRecords used for exporting. + + The list of MetricRecords is a snapshot created from the current + data in all of the aggregators in this batcher. + """ + metric_records = [] + for (metric, label_set), aggregator in self._batch_map.items(): + metric_records.append(MetricRecord(aggregator, label_set, metric)) + return metric_records + + def finished_collection(self): + """Performs certain post-export logic. + + For batchers that are stateless, resets the batch map. + """ + if not self.stateful: + self._batch_map = {} + + @abc.abstractmethod + def process(self, record) -> None: + """Stores record information to be ready for exporting. + + Depending on type of batcher, performs pre-export logic, such as + filtering records based off of keys. + """ + + +class UngroupedBatcher(Batcher): + """Accepts all records and passes them for exporting""" + + def process(self, record): + # Checkpoints the current aggregator value to be collected for export + record.aggregator.take_checkpoint() + batch_key = (record.metric, record.label_set) + batch_value = self._batch_map.get(batch_key) + aggregator = record.aggregator + if batch_value: + # Update the stored checkpointed value if exists. The call to merge + # here combines only identical records (same key). + batch_value.merge(aggregator) + return + if self.stateful: + # if stateful batcher, create a copy of the aggregator and update + # it with the current checkpointed value for long-term storage + aggregator = self.aggregator_for(record.metric.__class__) + aggregator.merge(record.aggregator) + self._batch_map[batch_key] = aggregator diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py new file mode 100644 index 0000000000..03c857f04d --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py @@ -0,0 +1,56 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import atexit +import threading + + +class PushController(threading.Thread): + """A push based controller, used for exporting. + + Uses a worker thread that periodically collects metrics for exporting, + exports them and performs some post-processing. + """ + + daemon = True + + def __init__(self, meter, exporter, interval, shutdown_on_exit=True): + super().__init__() + self.meter = meter + self.exporter = exporter + self.interval = interval + self.finished = threading.Event() + self._atexit_handler = None + if shutdown_on_exit: + self._atexit_handler = atexit.register(self.shutdown) + self.start() + + def run(self): + while not self.finished.wait(self.interval): + self.tick() + + def shutdown(self): + self.finished.set() + self.exporter.shutdown() + if self._atexit_handler is not None: + atexit.unregister(self._atexit_handler) + self._atexit_handler = None + + def tick(self): + # Collect all of the meter's metrics to be exported + self.meter.collect() + # Export the given metrics in the batcher + self.exporter.export(self.meter.batcher.checkpoint_set()) + # Perform post-exporting logic based on batcher configuration + self.meter.batcher.finished_collection() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index e429467061..ff0f78f3ce 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -434,9 +434,7 @@ class Tracer(trace_api.Tracer): """ def __init__( - self, - source: "TracerSource", - instrumentation_info: InstrumentationInfo, + self, source: "TracerSource", instrumentation_info: InstrumentationInfo ) -> None: self.source = source self.instrumentation_info = instrumentation_info diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 4d8e6df857..816bfcfca9 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -16,9 +16,16 @@ from unittest import mock from opentelemetry.sdk import metrics -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricsExporter, + MetricRecord, +) +from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator +from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.controller import PushController +# pylint: disable=protected-access class TestConsoleMetricsExporter(unittest.TestCase): # pylint: disable=no-self-use def test_export(self): @@ -34,10 +41,214 @@ def test_export(self): ) kvp = {"environment": "staging"} label_set = meter.get_label_set(kvp) - handle = metric.get_handle(label_set) - result = '{}(data="{}", label_values="{}", metric_data={})'.format( - ConsoleMetricsExporter.__name__, metric, label_set, handle + aggregator = CounterAggregator() + record = MetricRecord(aggregator, label_set, metric) + result = '{}(data="{}", label_set="{}", value={})'.format( + ConsoleMetricsExporter.__name__, + metric, + label_set.labels, + aggregator.checkpoint, ) with mock.patch("sys.stdout") as mock_stdout: - exporter.export([(metric, label_set)]) + exporter.export([record]) mock_stdout.write.assert_any_call(result) + + +class TestBatcher(unittest.TestCase): + def test_aggregator_for_counter(self): + batcher = UngroupedBatcher(True) + self.assertTrue( + isinstance( + batcher.aggregator_for(metrics.Counter), CounterAggregator + ) + ) + + # TODO: Add other aggregator tests + + def test_checkpoint_set(self): + meter = metrics.Meter() + batcher = UngroupedBatcher(True) + aggregator = CounterAggregator() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + meter, + ("environment",), + ) + aggregator.update(1.0) + label_set = metrics.LabelSet() + _batch_map = {} + _batch_map[(metric, label_set)] = aggregator + batcher._batch_map = _batch_map + records = batcher.checkpoint_set() + self.assertEqual(len(records), 1) + self.assertEqual(records[0].metric, metric) + self.assertEqual(records[0].label_set, label_set) + self.assertEqual(records[0].aggregator, aggregator) + + def test_checkpoint_set_empty(self): + batcher = UngroupedBatcher(True) + records = batcher.checkpoint_set() + self.assertEqual(len(records), 0) + + def test_finished_collection_stateless(self): + meter = metrics.Meter() + batcher = UngroupedBatcher(False) + aggregator = CounterAggregator() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + meter, + ("environment",), + ) + aggregator.update(1.0) + label_set = metrics.LabelSet() + _batch_map = {} + _batch_map[(metric, label_set)] = aggregator + batcher._batch_map = _batch_map + batcher.finished_collection() + self.assertEqual(len(batcher._batch_map), 0) + + def test_finished_collection_stateful(self): + meter = metrics.Meter() + batcher = UngroupedBatcher(True) + aggregator = CounterAggregator() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + meter, + ("environment",), + ) + aggregator.update(1.0) + label_set = metrics.LabelSet() + _batch_map = {} + _batch_map[(metric, label_set)] = aggregator + batcher._batch_map = _batch_map + batcher.finished_collection() + self.assertEqual(len(batcher._batch_map), 1) + + # TODO: Abstract the logic once other batchers implemented + def test_ungrouped_batcher_process_exists(self): + meter = metrics.Meter() + batcher = UngroupedBatcher(True) + aggregator = CounterAggregator() + aggregator2 = CounterAggregator() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + meter, + ("environment",), + ) + label_set = metrics.LabelSet() + _batch_map = {} + _batch_map[(metric, label_set)] = aggregator + aggregator2.update(1.0) + batcher._batch_map = _batch_map + record = metrics.Record(metric, label_set, aggregator2) + batcher.process(record) + self.assertEqual(len(batcher._batch_map), 1) + self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) + self.assertEqual( + batcher._batch_map.get((metric, label_set)).current, 0 + ) + self.assertEqual( + batcher._batch_map.get((metric, label_set)).checkpoint, 1.0 + ) + + def test_ungrouped_batcher_process_not_exists(self): + meter = metrics.Meter() + batcher = UngroupedBatcher(True) + aggregator = CounterAggregator() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + meter, + ("environment",), + ) + label_set = metrics.LabelSet() + _batch_map = {} + aggregator.update(1.0) + batcher._batch_map = _batch_map + record = metrics.Record(metric, label_set, aggregator) + batcher.process(record) + self.assertEqual(len(batcher._batch_map), 1) + self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) + self.assertEqual( + batcher._batch_map.get((metric, label_set)).current, 0 + ) + self.assertEqual( + batcher._batch_map.get((metric, label_set)).checkpoint, 1.0 + ) + + def test_ungrouped_batcher_process_not_stateful(self): + meter = metrics.Meter() + batcher = UngroupedBatcher(True) + aggregator = CounterAggregator() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + meter, + ("environment",), + ) + label_set = metrics.LabelSet() + _batch_map = {} + aggregator.update(1.0) + batcher._batch_map = _batch_map + record = metrics.Record(metric, label_set, aggregator) + batcher.process(record) + self.assertEqual(len(batcher._batch_map), 1) + self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) + self.assertEqual( + batcher._batch_map.get((metric, label_set)).current, 0 + ) + self.assertEqual( + batcher._batch_map.get((metric, label_set)).checkpoint, 1.0 + ) + + +class TestAggregator(unittest.TestCase): + # TODO: test other aggregators once implemented + def test_counter_update(self): + counter = CounterAggregator() + counter.update(1.0) + counter.update(2.0) + self.assertEqual(counter.current, 3.0) + + def test_counter_checkpoint(self): + counter = CounterAggregator() + counter.update(2.0) + counter.take_checkpoint() + self.assertEqual(counter.current, 0) + self.assertEqual(counter.checkpoint, 2.0) + + def test_counter_merge(self): + counter = CounterAggregator() + counter2 = CounterAggregator() + counter.checkpoint = 1.0 + counter2.checkpoint = 3.0 + counter.merge(counter2) + self.assertEqual(counter.checkpoint, 4.0) + + +class TestController(unittest.TestCase): + def test_push_controller(self): + meter = mock.Mock() + exporter = mock.Mock() + controller = PushController(meter, exporter, 5.0) + meter.collect.assert_not_called() + exporter.export.assert_not_called() + controller.shutdown() + self.assertTrue(controller.finished.isSet()) + exporter.shutdown.assert_any_call() diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 81e6dd2c9d..3a08433e8d 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -17,6 +17,7 @@ from opentelemetry import metrics as metrics_api from opentelemetry.sdk import metrics +from opentelemetry.sdk.metrics import export class TestMeter(unittest.TestCase): @@ -24,6 +25,43 @@ def test_extends_api(self): meter = metrics.Meter() self.assertIsInstance(meter, metrics_api.Meter) + def test_collect(self): + meter = metrics.Meter() + batcher_mock = mock.Mock() + meter.batcher = batcher_mock + label_keys = ("key1",) + counter = metrics.Counter( + "name", "desc", "unit", float, meter, label_keys + ) + kvp = {"key1": "value1"} + label_set = meter.get_label_set(kvp) + counter.add(label_set, 1.0) + meter.metrics.add(counter) + meter.collect() + self.assertTrue(batcher_mock.process.called) + + def test_collect_no_metrics(self): + meter = metrics.Meter() + batcher_mock = mock.Mock() + meter.batcher = batcher_mock + meter.collect() + self.assertFalse(batcher_mock.process.called) + + def test_collect_disabled_metric(self): + meter = metrics.Meter() + batcher_mock = mock.Mock() + meter.batcher = batcher_mock + label_keys = ("key1",) + counter = metrics.Counter( + "name", "desc", "unit", float, meter, label_keys, False + ) + kvp = {"key1": "value1"} + label_set = meter.get_label_set(kvp) + counter.add(label_set, 1.0) + meter.metrics.add(counter) + meter.collect() + self.assertFalse(batcher_mock.process.called) + def test_record_batch(self): meter = metrics.Meter() label_keys = ("key1",) @@ -34,7 +72,7 @@ def test_record_batch(self): label_set = meter.get_label_set(kvp) record_tuples = [(counter, 1.0)] meter.record_batch(label_set, record_tuples) - self.assertEqual(counter.get_handle(label_set).data, 1.0) + self.assertEqual(counter.get_handle(label_set).aggregator.current, 1.0) def test_record_batch_multiple(self): meter = metrics.Meter() @@ -44,15 +82,16 @@ def test_record_batch_multiple(self): counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) - gauge = metrics.Gauge("name", "desc", "unit", int, label_keys) + gauge = metrics.Gauge("name", "desc", "unit", int, meter, label_keys) measure = metrics.Measure( "name", "desc", "unit", float, meter, label_keys ) record_tuples = [(counter, 1.0), (gauge, 5), (measure, 3.0)] meter.record_batch(label_set, record_tuples) - self.assertEqual(counter.get_handle(label_set).data, 1.0) - self.assertEqual(gauge.get_handle(label_set).data, 5) - self.assertEqual(measure.get_handle(label_set).data, 0) + self.assertEqual(counter.get_handle(label_set).aggregator.current, 1.0) + self.assertEqual(gauge.get_handle(label_set).aggregator.current, 5.0) + # TODO: Fix when aggregator implemented for measure + self.assertEqual(measure.get_handle(label_set).aggregator.current, 3.0) def test_record_batch_exists(self): meter = metrics.Meter() @@ -62,12 +101,12 @@ def test_record_batch_exists(self): counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) - counter.add(label_set, 1.0) + counter.add(1.0, label_set) handle = counter.get_handle(label_set) record_tuples = [(counter, 1.0)] meter.record_batch(label_set, record_tuples) self.assertEqual(counter.get_handle(label_set), handle) - self.assertEqual(handle.data, 2.0) + self.assertEqual(handle.aggregator.current, 2.0) def test_create_metric(self): meter = metrics.Meter() @@ -100,8 +139,9 @@ def test_get_label_set(self): meter = metrics.Meter() kvp = {"environment": "staging", "a": "z"} label_set = meter.get_label_set(kvp) - encoded = tuple(sorted(kvp.items())) - self.assertIs(meter.labels[encoded], label_set) + label_set2 = meter.get_label_set(kvp) + labels = set([label_set, label_set2]) + self.assertEqual(len(labels), 1) def test_get_label_set_empty(self): meter = metrics.Meter() @@ -109,13 +149,6 @@ def test_get_label_set_empty(self): label_set = meter.get_label_set(kvp) self.assertEqual(label_set, metrics.EMPTY_LABEL_SET) - def test_get_label_set_exists(self): - meter = metrics.Meter() - kvp = {"environment": "staging", "a": "z"} - label_set = meter.get_label_set(kvp) - label_set2 = meter.get_label_set(kvp) - self.assertIs(label_set, label_set2) - class TestMetric(unittest.TestCase): def test_get_handle(self): @@ -132,114 +165,155 @@ def test_get_handle(self): class TestCounter(unittest.TestCase): def test_add(self): meter = metrics.Meter() - metric = metrics.Counter("name", "desc", "unit", int, ("key",)) + metric = metrics.Counter("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) handle = metric.get_handle(label_set) - metric.add(label_set, 3) - metric.add(label_set, 2) - self.assertEqual(handle.data, 5) + metric.add(3, label_set) + metric.add(2, label_set) + self.assertEqual(handle.aggregator.current, 5) class TestGauge(unittest.TestCase): def test_set(self): meter = metrics.Meter() - metric = metrics.Gauge("name", "desc", "unit", int, ("key",)) + metric = metrics.Gauge("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) handle = metric.get_handle(label_set) - metric.set(label_set, 3) - self.assertEqual(handle.data, 3) - metric.set(label_set, 2) - self.assertEqual(handle.data, 2) + metric.set(3, label_set) + self.assertEqual(handle.aggregator.current, 3) + metric.set(2, label_set) + # TODO: Fix once other aggregators implemented + self.assertEqual(handle.aggregator.current, 5) class TestMeasure(unittest.TestCase): def test_record(self): meter = metrics.Meter() - metric = metrics.Measure("name", "desc", "unit", int, ("key",)) + metric = metrics.Measure("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) handle = metric.get_handle(label_set) - metric.record(label_set, 3) - # Record not implemented yet - self.assertEqual(handle.data, 0) + metric.record(3, label_set) + # TODO: Fix once other aggregators implemented + self.assertEqual(handle.aggregator.current, 3) class TestCounterHandle(unittest.TestCase): def test_add(self): - handle = metrics.CounterHandle(int, True, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.CounterHandle(int, True, False, False, aggregator) handle.add(3) - self.assertEqual(handle.data, 3) + self.assertEqual(handle.aggregator.current, 3) def test_add_disabled(self): - handle = metrics.CounterHandle(int, False, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.CounterHandle(int, False, False, False, aggregator) handle.add(3) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) @mock.patch("opentelemetry.sdk.metrics.logger") def test_add_monotonic(self, logger_mock): - handle = metrics.CounterHandle(int, True, True) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.CounterHandle(int, True, True, False, aggregator) handle.add(-3) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @mock.patch("opentelemetry.sdk.metrics.logger") def test_add_incorrect_type(self, logger_mock): - handle = metrics.CounterHandle(int, True, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.CounterHandle(int, True, False, False, aggregator) handle.add(3.0) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) + @mock.patch("opentelemetry.sdk.metrics.time_ns") + def test_update(self, time_mock): + aggregator = export.aggregate.CounterAggregator() + handle = metrics.CounterHandle(int, True, False, False, aggregator) + time_mock.return_value = 123 + handle.update(4.0) + self.assertEqual(handle.last_update_timestamp, 123) + self.assertEqual(handle.aggregator.current, 4.0) + +# TODO: fix tests once aggregator implemented class TestGaugeHandle(unittest.TestCase): def test_set(self): - handle = metrics.GaugeHandle(int, True, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.GaugeHandle(int, True, False, False, aggregator) handle.set(3) - self.assertEqual(handle.data, 3) + self.assertEqual(handle.aggregator.current, 3) def test_set_disabled(self): - handle = metrics.GaugeHandle(int, False, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.GaugeHandle(int, False, False, False, aggregator) handle.set(3) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) @mock.patch("opentelemetry.sdk.metrics.logger") def test_set_monotonic(self, logger_mock): - handle = metrics.GaugeHandle(int, True, True) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.GaugeHandle(int, True, True, False, aggregator) handle.set(-3) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @mock.patch("opentelemetry.sdk.metrics.logger") def test_set_incorrect_type(self, logger_mock): - handle = metrics.GaugeHandle(int, True, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.GaugeHandle(int, True, False, False, aggregator) handle.set(3.0) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) + @mock.patch("opentelemetry.sdk.metrics.time_ns") + def test_update(self, time_mock): + aggregator = export.aggregate.CounterAggregator() + handle = metrics.GaugeHandle(int, True, False, False, aggregator) + time_mock.return_value = 123 + handle.update(4.0) + self.assertEqual(handle.last_update_timestamp, 123) + self.assertEqual(handle.aggregator.current, 4.0) + +# TODO: fix tests once aggregator implemented class TestMeasureHandle(unittest.TestCase): def test_record(self): - handle = metrics.MeasureHandle(int, False, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.MeasureHandle(int, False, False, False, aggregator) handle.record(3) - # Record not implemented yet - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) def test_record_disabled(self): - handle = metrics.MeasureHandle(int, False, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.MeasureHandle(int, False, False, False, aggregator) handle.record(3) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) @mock.patch("opentelemetry.sdk.metrics.logger") def test_record_monotonic(self, logger_mock): - handle = metrics.MeasureHandle(int, True, True) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.MeasureHandle(int, True, False, True, aggregator) handle.record(-3) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @mock.patch("opentelemetry.sdk.metrics.logger") def test_record_incorrect_type(self, logger_mock): - handle = metrics.MeasureHandle(int, True, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.MeasureHandle(int, True, False, False, aggregator) handle.record(3.0) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) + + @mock.patch("opentelemetry.sdk.metrics.time_ns") + def test_update(self, time_mock): + aggregator = export.aggregate.CounterAggregator() + handle = metrics.MeasureHandle(int, True, False, False, aggregator) + time_mock.return_value = 123 + handle.update(4.0) + self.assertEqual(handle.last_update_timestamp, 123) + self.assertEqual(handle.aggregator.current, 4.0) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 449fb51e8e..fff520bcb2 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -630,14 +630,13 @@ def test_ended_span(self): self.assertEqual(root.name, "root") new_status = trace_api.status.Status( - trace_api.status.StatusCanonicalCode.CANCELLED, "Test description", + trace_api.status.StatusCanonicalCode.CANCELLED, "Test description" ) with self.assertLogs(level=WARNING): root.set_status(new_status) self.assertEqual( - root.status.canonical_code, - trace_api.status.StatusCanonicalCode.OK, + root.status.canonical_code, trace_api.status.StatusCanonicalCode.OK ) def test_error_status(self): diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 8d41315fc7..406afb6ebf 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -479,7 +479,7 @@ def lint_args(args): runsubprocess(args.dry_run, ("flake8", rootdir), check=True) execute_args( parse_subargs( - args, ("exec", "pylint {}", "--all", "--mode", "lintroots",), + args, ("exec", "pylint {}", "--all", "--mode", "lintroots") ) ) From 2a54a9321d780b5f163ae4b14b7e28faeea07c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 11 Feb 2020 18:38:37 -0500 Subject: [PATCH 0198/1517] SDK: set sampled flag on sampling trace (#407) --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 2 ++ opentelemetry-sdk/tests/trace/test_trace.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index ff0f78f3ce..7a1594db63 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -509,6 +509,8 @@ def start_span( # pylint: disable=too-many-locals ) if sampling_decision.sampled: + options = context.trace_options | trace_api.TraceOptions.SAMPLED + context.trace_options = trace_api.TraceOptions(options) if attributes is None: span_attributes = sampling_decision.attributes else: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index fff520bcb2..a62f91a7e3 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -117,6 +117,7 @@ def test_default_sampler(self): self.assertIsInstance(root_span, trace.Span) child_span = tracer.start_span(name="child span", parent=root_span) self.assertIsInstance(child_span, trace.Span) + self.assertTrue(root_span.context.trace_options.sampled) def test_sampler_no_sampling(self): tracer_source = trace.TracerSource(sampling.ALWAYS_OFF) @@ -251,6 +252,9 @@ def test_start_span_explicit(self): other_parent = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, + trace_options=trace_api.TraceOptions( + trace_api.TraceOptions.SAMPLED + ), ) self.assertIsNone(tracer.get_current_span()) From 19d573af02753d3270cf965319d5edd4f7a66b68 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Wed, 12 Feb 2020 16:03:44 -0800 Subject: [PATCH 0199/1517] Add io and formatter options to console exporter (#412) Co-authored-by: Diego Hurtado --- .../sdk/trace/export/__init__.py | 11 +++++++- .../tests/trace/export/test_export.py | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 3e2cc02c33..87f4a097d5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -14,6 +14,7 @@ import collections import logging +import sys import threading import typing from enum import Enum @@ -266,7 +267,15 @@ class ConsoleSpanExporter(SpanExporter): spans to the console STDOUT. """ + def __init__( + self, + out: typing.IO = sys.stdout, + formatter: typing.Callable[[Span], str] = str, + ): + self.out = out + self.formatter = formatter + def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: for span in spans: - print(span) + self.out.write(self.formatter(span)) return SpanExportResult.SUCCESS diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index ea513a4858..e598b9680a 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -276,3 +276,31 @@ def test_batch_span_processor_parameters(self): max_queue_size=256, max_export_batch_size=512, ) + + +class TestConsoleSpanExporter(unittest.TestCase): + def test_export(self): # pylint: disable=no-self-use + """Check that the console exporter prints spans.""" + exporter = export.ConsoleSpanExporter() + + # Mocking stdout interferes with debugging and test reporting, mock on + # the exporter instance instead. + span = trace.Span("span name", mock.Mock()) + with mock.patch.object(exporter, "out") as mock_stdout: + exporter.export([span]) + mock_stdout.write.assert_called_once_with(str(span)) + self.assertEqual(mock_stdout.write.call_count, 1) + + def test_export_custom(self): # pylint: disable=no-self-use + """Check that console exporter uses custom io, formatter.""" + mock_span_str = mock.Mock(str) + + def formatter(span): # pylint: disable=unused-argument + return mock_span_str + + mock_stdout = mock.Mock() + exporter = export.ConsoleSpanExporter( + out=mock_stdout, formatter=formatter + ) + exporter.export([trace.Span("span name", mock.Mock())]) + mock_stdout.write.assert_called_once_with(mock_span_str) From c50aab8c70e6c4bfbe647d661f93483f49246efc Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 13 Feb 2020 09:32:17 -0800 Subject: [PATCH 0200/1517] Clean up ProbabilitySampler for 64 bit trace IDs (#238) Co-authored-by: alrex --- .../src/opentelemetry/trace/sampling.py | 10 ++--- .../tests/trace/test_sampling.py | 37 ++++++++++++++----- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/sampling.py b/opentelemetry-api/src/opentelemetry/trace/sampling.py index 2425c27c07..503c2e03eb 100644 --- a/opentelemetry-api/src/opentelemetry/trace/sampling.py +++ b/opentelemetry-api/src/opentelemetry/trace/sampling.py @@ -82,13 +82,13 @@ def __init__(self, rate: float): self._rate = rate self._bound = self.get_bound_for_rate(self._rate) - # The sampler checks the last 8 bytes of the trace ID to decide whether to - # sample a given trace. - CHECK_BYTES = 0xFFFFFFFFFFFFFFFF + # For compatibility with 64 bit trace IDs, the sampler checks the 64 + # low-order bits of the trace ID to decide whether to sample a given trace. + TRACE_ID_LIMIT = (1 << 64) - 1 @classmethod def get_bound_for_rate(cls, rate: float) -> int: - return round(rate * (cls.CHECK_BYTES + 1)) + return round(rate * (cls.TRACE_ID_LIMIT + 1)) @property def rate(self) -> float: @@ -115,7 +115,7 @@ def should_sample( if parent_context is not None: return Decision(parent_context.trace_options.sampled) - return Decision(trace_id & self.CHECK_BYTES < self.bound) + return Decision(trace_id & self.TRACE_ID_LIMIT < self.bound) # Samplers that ignore the parent sampling decision and never/always sample. diff --git a/opentelemetry-api/tests/trace/test_sampling.py b/opentelemetry-api/tests/trace/test_sampling.py index b456aa91f1..f04aecef45 100644 --- a/opentelemetry-api/tests/trace/test_sampling.py +++ b/opentelemetry-api/tests/trace/test_sampling.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import sys import unittest from opentelemetry import trace @@ -137,7 +138,7 @@ def test_probability_sampler(self): trace.SpanContext( 0xDEADBEF0, 0xDEADBEF1, trace_options=TO_DEFAULT ), - 0x8000000000000000, + 0x7FFFFFFFFFFFFFFF, 0xDEADBEEF, "span name", ).sampled @@ -147,7 +148,7 @@ def test_probability_sampler(self): trace.SpanContext( 0xDEADBEF0, 0xDEADBEF1, trace_options=TO_SAMPLED ), - 0x8000000000000001, + 0x8000000000000000, 0xDEADBEEF, "span name", ).sampled @@ -189,14 +190,13 @@ def test_probability_sampler_limits(self): sampling.ProbabilitySampler.get_bound_for_rate(2 ** -64), 0x1 ) - # Sample every trace with (last 8 bytes of) trace ID less than - # 0xffffffffffffffff. In principle this is the highest possible - # sampling rate less than 1, but we can't actually express this rate as - # a float! + # Sample every trace with trace ID less than 0xffffffffffffffff. In + # principle this is the highest possible sampling rate less than 1, but + # we can't actually express this rate as a float! # # In practice, the highest possible sampling rate is: # - # round(sys.float_info.epsilon * 2 ** 64) + # 1 - sys.float_info.epsilon almost_always_on = sampling.ProbabilitySampler(1 - 2 ** -64) self.assertTrue( @@ -212,12 +212,29 @@ def test_probability_sampler_limits(self): # self.assertFalse( # almost_always_on.should_sample( # None, - # 0xffffffffffffffff, - # 0xdeadbeef, + # 0xFFFFFFFFFFFFFFFF, + # 0xDEADBEEF, # "span name", # ).sampled # ) # self.assertEqual( # sampling.ProbabilitySampler.get_bound_for_rate(1 - 2 ** -64)), - # 0xffffffffffffffff, + # 0xFFFFFFFFFFFFFFFF, # ) + + # Check that a sampler with the highest effective sampling rate < 1 + # refuses to sample traces with trace ID 0xffffffffffffffff. + almost_almost_always_on = sampling.ProbabilitySampler( + 1 - sys.float_info.epsilon + ) + self.assertFalse( + almost_almost_always_on.should_sample( + None, 0xFFFFFFFFFFFFFFFF, 0xDEADBEEF, "span name" + ).sampled + ) + # Check that the higest effective sampling rate is actually lower than + # the highest theoretical sampling rate. If this test fails the test + # above is wrong. + self.assertLess( + almost_almost_always_on.bound, 0xFFFFFFFFFFFFFFFF, + ) From 72862c91a0a12a7420c6bb12a296d9b052ef8686 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 13 Feb 2020 15:18:39 -0800 Subject: [PATCH 0201/1517] Adding Context API (#395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change implements the Context API portion of OTEP #66. The CorrelationContext API and Propagation API changes will come in future PRs. We're leveraging entrypoints to support other implementations of the Context API if/when necessary. For backwards compatibility, this change uses aiocontextvars for Python versions older than 3.7. Co-authored-by: Diego Hurtado Co-authored-by: Mauricio Vásquez --- ....rst => opentelemetry.context.context.rst} | 2 +- docs/opentelemetry.context.rst | 2 +- .../ext/http_requests/__init__.py | 5 +- opentelemetry-api/setup.py | 7 + .../src/opentelemetry/context/__init__.py | 254 ++++++++---------- .../opentelemetry/context/async_context.py | 45 ---- .../src/opentelemetry/context/base_context.py | 130 --------- .../src/opentelemetry/context/context.py | 44 +++ .../opentelemetry/context/default_context.py | 34 +++ .../context/thread_local_context.py | 45 ---- .../distributedcontext/__init__.py | 22 +- .../trace/propagation/__init__.py | 26 ++ .../tests/context/test_context.py | 65 +++++ opentelemetry-sdk/setup.py | 12 +- .../sdk/context/aiocontextvarsfix.py | 86 ++++++ .../sdk/context/contextvars_context.py | 48 ++++ .../sdk/context/threadlocal_context.py | 44 +++ .../sdk/distributedcontext/__init__.py | 27 +- .../src/opentelemetry/sdk/trace/__init__.py | 17 +- .../sdk/trace/export/__init__.py | 36 +-- opentelemetry-sdk/tests/conftest.py | 30 +++ .../tests/context/test_asyncio.py | 154 +++++++++++ .../tests/context/test_context.py | 63 +++++ .../tests/context/test_threads.py | 87 ++++++ tox.ini | 1 + 25 files changed, 879 insertions(+), 407 deletions(-) rename docs/{opentelemetry.context.base_context.rst => opentelemetry.context.context.rst} (73%) delete mode 100644 opentelemetry-api/src/opentelemetry/context/async_context.py delete mode 100644 opentelemetry-api/src/opentelemetry/context/base_context.py create mode 100644 opentelemetry-api/src/opentelemetry/context/context.py create mode 100644 opentelemetry-api/src/opentelemetry/context/default_context.py delete mode 100644 opentelemetry-api/src/opentelemetry/context/thread_local_context.py create mode 100644 opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py create mode 100644 opentelemetry-api/tests/context/test_context.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/context/aiocontextvarsfix.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py create mode 100644 opentelemetry-sdk/tests/conftest.py create mode 100644 opentelemetry-sdk/tests/context/test_asyncio.py create mode 100644 opentelemetry-sdk/tests/context/test_context.py create mode 100644 opentelemetry-sdk/tests/context/test_threads.py diff --git a/docs/opentelemetry.context.base_context.rst b/docs/opentelemetry.context.context.rst similarity index 73% rename from docs/opentelemetry.context.base_context.rst rename to docs/opentelemetry.context.context.rst index ac28d40008..331557d2dd 100644 --- a/docs/opentelemetry.context.base_context.rst +++ b/docs/opentelemetry.context.context.rst @@ -1,7 +1,7 @@ opentelemetry.context.base\_context module ========================================== -.. automodule:: opentelemetry.context.base_context +.. automodule:: opentelemetry.context.context :members: :undoc-members: :show-inheritance: diff --git a/docs/opentelemetry.context.rst b/docs/opentelemetry.context.rst index 7bc738a050..2b25793458 100644 --- a/docs/opentelemetry.context.rst +++ b/docs/opentelemetry.context.rst @@ -6,7 +6,7 @@ Submodules .. toctree:: - opentelemetry.context.base_context + opentelemetry.context.context Module contents --------------- diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index 4f5a18cf9e..a557e6fc45 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -22,8 +22,7 @@ from requests.sessions import Session -from opentelemetry import propagators -from opentelemetry.context import Context +from opentelemetry import context, propagators from opentelemetry.ext.http_requests.version import __version__ from opentelemetry.trace import SpanKind @@ -54,7 +53,7 @@ def enable(tracer_source): @functools.wraps(wrapped) def instrumented_request(self, method, url, *args, **kwargs): - if Context.suppress_instrumentation: + if context.get_value("suppress_instrumentation"): return wrapped(self, method, url, *args, **kwargs) # See diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index ee8adf26ae..fad86f171b 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -56,4 +56,11 @@ "/tree/master/opentelemetry-api" ), zip_safe=False, + entry_points={ + "opentelemetry_context": [ + "default_context = " + "opentelemetry.context.default_context:" + "DefaultRuntimeContext", + ] + }, ) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 43a7722f88..63de570abc 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -12,141 +12,119 @@ # See the License for the specific language governing permissions and # limitations under the License. - -""" -The OpenTelemetry context module provides abstraction layer on top of -thread-local storage and contextvars. The long term direction is to switch to -contextvars provided by the Python runtime library. - -A global object ``Context`` is provided to access all the context related -functionalities:: - - >>> from opentelemetry.context import Context - >>> Context.foo = 1 - >>> Context.foo = 2 - >>> Context.foo - 2 - -When explicit thread is used, a helper function -``Context.with_current_context`` can be used to carry the context across -threads:: - - from threading import Thread - from opentelemetry.context import Context - - def work(name): - print('Entering worker:', Context) - Context.operation_id = name - print('Exiting worker:', Context) - - if __name__ == '__main__': - print('Main thread:', Context) - Context.operation_id = 'main' - - print('Main thread:', Context) - - # by default context is not propagated to worker thread - thread = Thread(target=work, args=('foo',)) - thread.start() - thread.join() - - print('Main thread:', Context) - - # user can propagate context explicitly - thread = Thread( - target=Context.with_current_context(work), - args=('bar',), - ) - thread.start() - thread.join() - - print('Main thread:', Context) - -Here goes another example using thread pool:: - - import time - import threading - - from multiprocessing.dummy import Pool as ThreadPool - from opentelemetry.context import Context - - _console_lock = threading.Lock() - - def println(msg): - with _console_lock: - print(msg) - - def work(name): - println('Entering worker[{}]: {}'.format(name, Context)) - Context.operation_id = name - time.sleep(0.01) - println('Exiting worker[{}]: {}'.format(name, Context)) - - if __name__ == "__main__": - println('Main thread: {}'.format(Context)) - Context.operation_id = 'main' - pool = ThreadPool(2) # create a thread pool with 2 threads - pool.map(Context.with_current_context(work), [ - 'bear', - 'cat', - 'dog', - 'horse', - 'rabbit', - ]) - pool.close() - pool.join() - println('Main thread: {}'.format(Context)) - -Here goes a simple demo of how async could work in Python 3.7+:: - - import asyncio - - from opentelemetry.context import Context - - class Span(object): - def __init__(self, name): - self.name = name - self.parent = Context.current_span - - def __repr__(self): - return ('{}(name={}, parent={})' - .format( - type(self).__name__, - self.name, - self.parent, - )) - - async def __aenter__(self): - Context.current_span = self - - async def __aexit__(self, exc_type, exc, tb): - Context.current_span = self.parent - - async def main(): - print(Context) - async with Span('foo'): - print(Context) - await asyncio.sleep(0.1) - async with Span('bar'): - print(Context) - await asyncio.sleep(0.1) - print(Context) - await asyncio.sleep(0.1) - print(Context) - - if __name__ == '__main__': - asyncio.run(main()) -""" - -from .base_context import BaseRuntimeContext - -__all__ = ["Context"] - -try: - from .async_context import AsyncRuntimeContext - - Context = AsyncRuntimeContext() # type: BaseRuntimeContext -except ImportError: - from .thread_local_context import ThreadLocalRuntimeContext - - Context = ThreadLocalRuntimeContext() +import logging +import typing +from os import environ + +from pkg_resources import iter_entry_points + +from opentelemetry.context.context import Context, RuntimeContext + +logger = logging.getLogger(__name__) +_RUNTIME_CONTEXT = None # type: typing.Optional[RuntimeContext] + + +def get_value(key: str, context: typing.Optional[Context] = None) -> "object": + """To access the local state of a concern, the RuntimeContext API + provides a function which takes a context and a key as input, + and returns a value. + + Args: + key: The key of the value to retrieve. + context: The context from which to retrieve the value, if None, the current context is used. + """ + return context.get(key) if context is not None else get_current().get(key) + + +def set_value( + key: str, value: "object", context: typing.Optional[Context] = None +) -> Context: + """To record the local state of a cross-cutting concern, the + RuntimeContext API provides a function which takes a context, a + key, and a value as input, and returns an updated context + which contains the new value. + + Args: + key: The key of the entry to set + value: The value of the entry to set + context: The context to copy, if None, the current context is used + """ + if context is None: + context = get_current() + new_values = context.copy() + new_values[key] = value + return Context(new_values) + + +def remove_value( + key: str, context: typing.Optional[Context] = None +) -> Context: + """To remove a value, this method returns a new context with the key + cleared. Note that the removed value still remains present in the old + context. + + Args: + key: The key of the entry to remove + context: The context to copy, if None, the current context is used + """ + if context is None: + context = get_current() + new_values = context.copy() + new_values.pop(key, None) + return Context(new_values) + + +def get_current() -> Context: + """To access the context associated with program execution, + the RuntimeContext API provides a function which takes no arguments + and returns a RuntimeContext. + """ + + global _RUNTIME_CONTEXT # pylint: disable=global-statement + if _RUNTIME_CONTEXT is None: + # FIXME use a better implementation of a configuration manager to avoid having + # to get configuration values straight from environment variables + + configured_context = environ.get( + "OPENTELEMETRY_CONTEXT", "default_context" + ) # type: str + try: + _RUNTIME_CONTEXT = next( + iter_entry_points("opentelemetry_context", configured_context) + ).load()() + except Exception: # pylint: disable=broad-except + logger.error("Failed to load context: %s", configured_context) + + return _RUNTIME_CONTEXT.get_current() # type:ignore + + +def set_current(context: Context) -> Context: + """To associate a context with program execution, the Context + API provides a function which takes a Context. + + Args: + context: The context to use as current. + """ + old_context = get_current() + _RUNTIME_CONTEXT.set_current(context) # type:ignore + return old_context + + +def with_current_context( + func: typing.Callable[..., "object"] +) -> typing.Callable[..., "object"]: + """Capture the current context and apply it to the provided func.""" + + caller_context = get_current() + + def call_with_current_context( + *args: "object", **kwargs: "object" + ) -> "object": + try: + backup = get_current() + set_current(caller_context) + return func(*args, **kwargs) + finally: + set_current(backup) + + return call_with_current_context diff --git a/opentelemetry-api/src/opentelemetry/context/async_context.py b/opentelemetry-api/src/opentelemetry/context/async_context.py deleted file mode 100644 index 267059fb31..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/async_context.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -try: - from contextvars import ContextVar -except ImportError: - pass -else: - import typing # pylint: disable=unused-import - from . import base_context - - class AsyncRuntimeContext(base_context.BaseRuntimeContext): - class Slot(base_context.BaseRuntimeContext.Slot): - def __init__(self, name: str, default: object): - # pylint: disable=super-init-not-called - self.name = name - self.contextvar = ContextVar(name) # type: ContextVar[object] - self.default = base_context.wrap_callable( - default - ) # type: typing.Callable[..., object] - - def clear(self) -> None: - self.contextvar.set(self.default()) - - def get(self) -> object: - try: - return self.contextvar.get() - except LookupError: - value = self.default() - self.set(value) - return value - - def set(self, value: object) -> None: - self.contextvar.set(value) diff --git a/opentelemetry-api/src/opentelemetry/context/base_context.py b/opentelemetry-api/src/opentelemetry/context/base_context.py deleted file mode 100644 index 99d6869dd5..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/base_context.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading -import typing -from contextlib import contextmanager - - -def wrap_callable(target: "object") -> typing.Callable[[], object]: - if callable(target): - return target - return lambda: target - - -class BaseRuntimeContext: - class Slot: - def __init__(self, name: str, default: "object"): - raise NotImplementedError - - def clear(self) -> None: - raise NotImplementedError - - def get(self) -> "object": - raise NotImplementedError - - def set(self, value: "object") -> None: - raise NotImplementedError - - _lock = threading.Lock() - _slots = {} # type: typing.Dict[str, 'BaseRuntimeContext.Slot'] - - @classmethod - def clear(cls) -> None: - """Clear all slots to their default value.""" - keys = cls._slots.keys() - for name in keys: - slot = cls._slots[name] - slot.clear() - - @classmethod - def register_slot( - cls, name: str, default: "object" = None - ) -> "BaseRuntimeContext.Slot": - """Register a context slot with an optional default value. - - :type name: str - :param name: The name of the context slot. - - :type default: object - :param name: The default value of the slot, can be a value or lambda. - - :returns: The registered slot. - """ - with cls._lock: - if name not in cls._slots: - cls._slots[name] = cls.Slot(name, default) - return cls._slots[name] - - def apply(self, snapshot: typing.Dict[str, "object"]) -> None: - """Set the current context from a given snapshot dictionary""" - - for name in snapshot: - setattr(self, name, snapshot[name]) - - def snapshot(self) -> typing.Dict[str, "object"]: - """Return a dictionary of current slots by reference.""" - - keys = self._slots.keys() - return dict((n, self._slots[n].get()) for n in keys) - - def __repr__(self) -> str: - return "{}({})".format(type(self).__name__, self.snapshot()) - - def __getattr__(self, name: str) -> "object": - if name not in self._slots: - self.register_slot(name, None) - slot = self._slots[name] - return slot.get() - - def __setattr__(self, name: str, value: "object") -> None: - if name not in self._slots: - self.register_slot(name, None) - slot = self._slots[name] - slot.set(value) - - def __getitem__(self, name: str) -> "object": - return self.__getattr__(name) - - def __setitem__(self, name: str, value: "object") -> None: - self.__setattr__(name, value) - - @contextmanager # type: ignore - def use(self, **kwargs: typing.Dict[str, object]) -> typing.Iterator[None]: - snapshot = {key: self[key] for key in kwargs} - for key in kwargs: - self[key] = kwargs[key] - yield - for key in kwargs: - self[key] = snapshot[key] - - def with_current_context( - self, func: typing.Callable[..., "object"] - ) -> typing.Callable[..., "object"]: - """Capture the current context and apply it to the provided func. - """ - - caller_context = self.snapshot() - - def call_with_current_context( - *args: "object", **kwargs: "object" - ) -> "object": - try: - backup_context = self.snapshot() - self.apply(caller_context) - return func(*args, **kwargs) - finally: - self.apply(backup_context) - - return call_with_current_context diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py new file mode 100644 index 0000000000..148312a884 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -0,0 +1,44 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing +from abc import ABC, abstractmethod + + +class Context(typing.Dict[str, object]): + def __setitem__(self, key: str, value: object) -> None: + raise ValueError + + +class RuntimeContext(ABC): + """The RuntimeContext interface provides a wrapper for the different + mechanisms that are used to propagate context in Python. + Implementations can be made available via entry_points and + selected through environment variables. + """ + + @abstractmethod + def set_current(self, context: Context) -> None: + """ Sets the current `Context` object. + + Args: + context: The Context to set. + """ + + @abstractmethod + def get_current(self) -> Context: + """ Returns the current `Context` object. """ + + +__all__ = ["Context", "RuntimeContext"] diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py new file mode 100644 index 0000000000..6c83f839d3 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -0,0 +1,34 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from opentelemetry.context.context import Context, RuntimeContext + + +class DefaultRuntimeContext(RuntimeContext): + """A default implementation of the RuntimeContext interface using + a dictionary to store values. + """ + + def __init__(self) -> None: + self._current_context = Context() + + def set_current(self, context: Context) -> None: + """See `opentelemetry.context.RuntimeContext.set_current`.""" + self._current_context = context + + def get_current(self) -> Context: + """See `opentelemetry.context.RuntimeContext.get_current`.""" + return self._current_context + + +__all__ = ["DefaultRuntimeContext"] diff --git a/opentelemetry-api/src/opentelemetry/context/thread_local_context.py b/opentelemetry-api/src/opentelemetry/context/thread_local_context.py deleted file mode 100644 index b60914f846..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/thread_local_context.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading -import typing # pylint: disable=unused-import - -from . import base_context - - -class ThreadLocalRuntimeContext(base_context.BaseRuntimeContext): - class Slot(base_context.BaseRuntimeContext.Slot): - _thread_local = threading.local() - - def __init__(self, name: str, default: "object"): - # pylint: disable=super-init-not-called - self.name = name - self.default = base_context.wrap_callable( - default - ) # type: typing.Callable[..., object] - - def clear(self) -> None: - setattr(self._thread_local, self.name, self.default()) - - def get(self) -> "object": - try: - got = getattr(self._thread_local, self.name) # type: object - return got - except AttributeError: - value = self.default() - self.set(value) - return value - - def set(self, value: "object") -> None: - setattr(self._thread_local, self.name, value) diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index 38ef3739b9..a89d982550 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -17,6 +17,9 @@ import typing from contextlib import contextmanager +from opentelemetry.context import get_value, set_current, set_value +from opentelemetry.context.context import Context + PRINTABLE = frozenset( itertools.chain( string.ascii_letters, string.digits, string.punctuation, " " @@ -100,7 +103,9 @@ def get_entry_value(self, key: EntryKey) -> typing.Optional[EntryValue]: class DistributedContextManager: - def get_current_context(self) -> typing.Optional[DistributedContext]: + def get_current_context( + self, context: typing.Optional[Context] = None + ) -> typing.Optional[DistributedContext]: """Gets the current DistributedContext. Returns: @@ -123,3 +128,18 @@ def use_context( """ # pylint: disable=no-self-use yield context + + +_DISTRIBUTED_CONTEXT_KEY = "DistributedContext" + + +def distributed_context_from_context( + context: typing.Optional[Context] = None, +) -> DistributedContext: + return get_value(_DISTRIBUTED_CONTEXT_KEY, context) # type: ignore + + +def with_distributed_context( + dctx: DistributedContext, context: typing.Optional[Context] = None +) -> None: + set_current(set_value(_DISTRIBUTED_CONTEXT_KEY, dctx, context=context)) diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py new file mode 100644 index 0000000000..67d8a76a53 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -0,0 +1,26 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Optional + +from opentelemetry.trace import INVALID_SPAN_CONTEXT, Span, SpanContext + +_SPAN_CONTEXT_KEY = "extracted-span-context" +_SPAN_KEY = "current-span" + + +def get_span_key(tracer_source_id: Optional[str] = None) -> str: + key = _SPAN_KEY + if tracer_source_id is not None: + key = "{}-{}".format(key, tracer_source_id) + return key diff --git a/opentelemetry-api/tests/context/test_context.py b/opentelemetry-api/tests/context/test_context.py new file mode 100644 index 0000000000..2536e5149b --- /dev/null +++ b/opentelemetry-api/tests/context/test_context.py @@ -0,0 +1,65 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import context +from opentelemetry.context.context import Context + + +def do_work() -> None: + context.set_current(context.set_value("say", "bar")) + + +class TestContext(unittest.TestCase): + def setUp(self): + context.set_current(Context()) + + def test_context(self): + self.assertIsNone(context.get_value("say")) + empty = context.get_current() + second = context.set_value("say", "foo") + self.assertEqual(context.get_value("say", context=second), "foo") + + do_work() + self.assertEqual(context.get_value("say"), "bar") + third = context.get_current() + + self.assertIsNone(context.get_value("say", context=empty)) + self.assertEqual(context.get_value("say", context=second), "foo") + self.assertEqual(context.get_value("say", context=third), "bar") + + def test_set_value(self): + first = context.set_value("a", "yyy") + second = context.set_value("a", "zzz") + third = context.set_value("a", "---", first) + self.assertEqual("yyy", context.get_value("a", context=first)) + self.assertEqual("zzz", context.get_value("a", context=second)) + self.assertEqual("---", context.get_value("a", context=third)) + self.assertEqual(None, context.get_value("a")) + + def test_context_is_immutable(self): + with self.assertRaises(ValueError): + # ensure a context + context.get_current()["test"] = "cant-change-immutable" + + def test_set_current(self): + context.set_current(context.set_value("a", "yyy")) + + old_context = context.set_current(context.set_value("a", "zzz")) + self.assertEqual("yyy", context.get_value("a", context=old_context)) + self.assertEqual("zzz", context.get_value("a")) + + context.set_current(old_context) + self.assertEqual("yyy", context.get_value("a")) diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index cbfb0f075d..7e88bb3bfe 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -44,7 +44,7 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["opentelemetry-api==0.4.dev0"], + install_requires=["opentelemetry-api==0.4.dev0", "aiocontextvars"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, @@ -56,4 +56,14 @@ "/tree/master/opentelemetry-sdk" ), zip_safe=False, + entry_points={ + "opentelemetry_context": [ + "contextvars_context = " + "opentelemetry.sdk.context.contextvars_context:" + "ContextVarsRuntimeContext", + "threadlocal_context = " + "opentelemetry.sdk.context.threadlocal_context:" + "ThreadLocalRuntimeContext", + ] + }, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/aiocontextvarsfix.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/aiocontextvarsfix.py new file mode 100644 index 0000000000..6aa1779378 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/aiocontextvarsfix.py @@ -0,0 +1,86 @@ +# type: ignore +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This module is a patch to allow aiocontextvars to work for older versions +# of Python 3.5. It is copied and pasted from: +# https://github.com/fantix/aiocontextvars/issues/88#issuecomment-522276290 + +import asyncio +import asyncio.coroutines +import asyncio.futures +import concurrent.futures + +if not hasattr(asyncio, "_get_running_loop"): + # noinspection PyCompatibility + # pylint:disable=protected-access + import asyncio.events + from threading import local as threading_local + + if not hasattr(asyncio.events, "_get_running_loop"): + + class _RunningLoop(threading_local): + _loop = None + + _running_loop = _RunningLoop() + + def _get_running_loop(): + return _running_loop._loop + + def set_running_loop(loop): # noqa: F811 + _running_loop._loop = loop + + def _get_event_loop(): + current_loop = _get_running_loop() + if current_loop is not None: + return current_loop + return asyncio.events.get_event_loop_policy().get_event_loop() + + asyncio.events.get_event_loop = _get_event_loop + asyncio.events._get_running_loop = _get_running_loop + asyncio.events._set_running_loop = set_running_loop + + asyncio._get_running_loop = asyncio.events._get_running_loop + asyncio._set_running_loop = asyncio.events._set_running_loop + +# noinspection PyUnresolvedReferences +import aiocontextvars # pylint: disable=unused-import,wrong-import-position # noqa # isort:skip + + +def _run_coroutine_threadsafe(coro, loop): + """ + Patch to create task in the same thread instead of in the callback. + This ensures that contextvars get copied. Python 3.7 copies contextvars + without this. + """ + if not asyncio.coroutines.iscoroutine(coro): + raise TypeError("A coroutine object is required") + future = concurrent.futures.Future() + task = asyncio.ensure_future(coro, loop=loop) + + def callback() -> None: + try: + # noinspection PyProtectedMember,PyUnresolvedReferences + # pylint:disable=protected-access + asyncio.futures._chain_future(task, future) + except Exception as exc: + if future.set_running_or_notify_cancel(): + future.set_exception(exc) + raise + + loop.call_soon_threadsafe(callback) + return future + + +asyncio.run_coroutine_threadsafe = _run_coroutine_threadsafe diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py new file mode 100644 index 0000000000..0a350e2699 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -0,0 +1,48 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from contextvars import ContextVar +from sys import version_info + +from opentelemetry.context import Context +from opentelemetry.context.context import RuntimeContext + +if (3, 5, 3) <= version_info < (3, 7): + import aiocontextvars # type: ignore # pylint:disable=unused-import + +elif (3, 4) < version_info <= (3, 5, 2): + import opentelemetry.sdk.context.aiocontextvarsfix # pylint:disable=unused-import + + +class ContextVarsRuntimeContext(RuntimeContext): + """An implementation of the RuntimeContext interface which wraps ContextVar under + the hood. This is the prefered implementation for usage with Python 3.5+ + """ + + _CONTEXT_KEY = "current_context" + + def __init__(self) -> None: + self._current_context = ContextVar( + self._CONTEXT_KEY, default=Context() + ) + + def set_current(self, context: Context) -> None: + """See `opentelemetry.context.RuntimeContext.set_current`.""" + self._current_context.set(context) + + def get_current(self) -> Context: + """See `opentelemetry.context.RuntimeContext.get_current`.""" + return self._current_context.get() + + +__all__ = ["ContextVarsRuntimeContext"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py new file mode 100644 index 0000000000..26d4329c52 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -0,0 +1,44 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading + +from opentelemetry.context import Context, RuntimeContext + + +class ThreadLocalRuntimeContext(RuntimeContext): + """An implementation of the RuntimeContext interface + which uses thread-local storage under the hood. This + implementation is available for usage with Python 3.4. + """ + + _CONTEXT_KEY = "current_context" + + def __init__(self) -> None: + self._current_context = threading.local() + + def set_current(self, context: Context) -> None: + """See `opentelemetry.context.RuntimeContext.set_current`.""" + setattr(self._current_context, self._CONTEXT_KEY, context) + + def get_current(self) -> Context: + """See `opentelemetry.context.RuntimeContext.get_current`.""" + if not hasattr(self._current_context, self._CONTEXT_KEY): + setattr( + self._current_context, self._CONTEXT_KEY, Context(), + ) + return getattr(self._current_context, self._CONTEXT_KEY) + + +__all__ = ["ThreadLocalRuntimeContext"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py index a20cbf8963..7a0a66a8a9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py @@ -16,33 +16,27 @@ from contextlib import contextmanager from opentelemetry import distributedcontext as dctx_api -from opentelemetry.context import Context +from opentelemetry.context import Context, get_value, set_value +from opentelemetry.distributedcontext import ( + distributed_context_from_context, + with_distributed_context, +) class DistributedContextManager(dctx_api.DistributedContextManager): """See `opentelemetry.distributedcontext.DistributedContextManager` - Args: - name: The name of the context manager """ - def __init__(self, name: str = "") -> None: - if name: - slot_name = "DistributedContext.{}".format(name) - else: - slot_name = "DistributedContext" - - self._current_context = Context.register_slot(slot_name) - def get_current_context( - self, + self, context: typing.Optional[Context] = None ) -> typing.Optional[dctx_api.DistributedContext]: """Gets the current DistributedContext. Returns: A DistributedContext instance representing the current context. """ - return self._current_context.get() + return distributed_context_from_context(context=context) @contextmanager def use_context( @@ -58,9 +52,10 @@ def use_context( Args: context: A DistributedContext instance to make current. """ - snapshot = self._current_context.get() - self._current_context.set(context) + snapshot = distributed_context_from_context() + with_distributed_context(context) + try: yield context finally: - self._current_context.set(snapshot) + with_distributed_context(snapshot) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 7a1594db63..6d249e6508 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -22,11 +22,12 @@ from types import TracebackType from typing import Iterator, Optional, Sequence, Tuple, Type +from opentelemetry import context as context_api from opentelemetry import trace as trace_api -from opentelemetry.context import Context from opentelemetry.sdk import util from opentelemetry.sdk.util import BoundedDict, BoundedList from opentelemetry.trace import SpanContext, sampling +from opentelemetry.trace.propagation import get_span_key from opentelemetry.trace.status import Status, StatusCanonicalCode from opentelemetry.util import time_ns, types @@ -540,16 +541,14 @@ def use_span( ) -> Iterator[trace_api.Span]: """See `opentelemetry.trace.Tracer.use_span`.""" try: - span_snapshot = self.source.get_current_span() - self.source._current_span_slot.set( # pylint:disable=protected-access - span + context_snapshot = context_api.get_current() + context_api.set_current( + context_api.set_value(self.source.key, span) ) try: yield span finally: - self.source._current_span_slot.set( # pylint:disable=protected-access - span_snapshot - ) + context_api.set_current(context_snapshot) except Exception as error: # pylint: disable=broad-except if ( @@ -580,7 +579,7 @@ def __init__( ): # TODO: How should multiple TracerSources behave? Should they get their own contexts? # This could be done by adding `str(id(self))` to the slot name. - self._current_span_slot = Context.register_slot("current_span") + self.key = get_span_key(tracer_source_id=str(id(self))) self._active_span_processor = MultiSpanProcessor() self.sampler = sampler self._atexit_handler = None @@ -603,7 +602,7 @@ def get_tracer( ) def get_current_span(self) -> Span: - return self._current_span_slot.get() + return context_api.get_value(self.key) # type: ignore def add_span_processor(self, span_processor: SpanProcessor) -> None: """Registers a new :class:`SpanProcessor` for this `TracerSource`. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 87f4a097d5..0a1b1c8041 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -19,7 +19,7 @@ import typing from enum import Enum -from opentelemetry.context import Context +from opentelemetry.context import get_current, set_current, set_value from opentelemetry.trace import DefaultSpan from opentelemetry.util import time_ns @@ -75,12 +75,14 @@ def on_start(self, span: Span) -> None: pass def on_end(self, span: Span) -> None: - with Context.use(suppress_instrumentation=True): - try: - self.span_exporter.export((span,)) - # pylint: disable=broad-except - except Exception: - logger.exception("Exception while exporting Span.") + backup_context = get_current() + set_current(set_value("suppress_instrumentation", True)) + try: + self.span_exporter.export((span,)) + # pylint: disable=broad-except + except Exception: + logger.exception("Exception while exporting Span.") + set_current(backup_context) def shutdown(self) -> None: self.span_exporter.shutdown() @@ -200,16 +202,16 @@ def export(self) -> None: else: self.spans_list[idx] = span idx += 1 - with Context.use(suppress_instrumentation=True): - try: - # Ignore type b/c the Optional[None]+slicing is too "clever" - # for mypy - self.span_exporter.export( - self.spans_list[:idx] - ) # type: ignore - # pylint: disable=broad-except - except Exception: - logger.exception("Exception while exporting Span batch.") + backup_context = get_current() + set_current(set_value("suppress_instrumentation", True)) + try: + # Ignore type b/c the Optional[None]+slicing is too "clever" + # for mypy + self.span_exporter.export(self.spans_list[:idx]) # type: ignore + # pylint: disable=broad-except + except Exception: + logger.exception("Exception while exporting Span batch.") + set_current(backup_context) if notify_flush: with self.flush_condition: diff --git a/opentelemetry-sdk/tests/conftest.py b/opentelemetry-sdk/tests/conftest.py new file mode 100644 index 0000000000..59e306f130 --- /dev/null +++ b/opentelemetry-sdk/tests/conftest.py @@ -0,0 +1,30 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import environ +from sys import version_info + + +def pytest_sessionstart(session): + # pylint: disable=unused-argument + if version_info < (3, 5): + # contextvars are not supported in 3.4, use thread-local storage + environ["OPENTELEMETRY_CONTEXT"] = "threadlocal_context" + else: + environ["OPENTELEMETRY_CONTEXT"] = "contextvars_context" + + +def pytest_sessionfinish(session): + # pylint: disable=unused-argument + environ.pop("OPENTELEMETRY_CONTEXT") diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py new file mode 100644 index 0000000000..5dc3637598 --- /dev/null +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -0,0 +1,154 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import unittest +from unittest.mock import patch + +from opentelemetry import context +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace import export +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +try: + import contextvars # pylint: disable=unused-import + from opentelemetry.sdk.context.contextvars_context import ( + ContextVarsRuntimeContext, + ) +except ImportError: + raise unittest.SkipTest("contextvars not available") + + +_SPAN_NAMES = [ + "test_span1", + "test_span2", + "test_span3", + "test_span4", + "test_span5", +] + + +def stop_loop_when(loop, cond_func, timeout=5.0): + """Registers a periodic callback that stops the loop when cond_func() == True. + Compatible with both Tornado and asyncio. + """ + if cond_func() or timeout <= 0.0: + loop.stop() + return + + timeout -= 0.1 + loop.call_later(0.1, stop_loop_when, loop, cond_func, timeout) + + +def do_work() -> None: + context.set_current(context.set_value("say", "bar")) + + +class TestAsyncio(unittest.TestCase): + @asyncio.coroutine + def task(self, name): + with self.tracer.start_as_current_span(name): + context.set_value("say", "bar") + + def submit_another_task(self, name): + self.loop.create_task(self.task(name)) + + def setUp(self): + self.previous_context = context.get_current() + context.set_current(context.Context()) + self.tracer_source = trace.TracerSource() + self.tracer = self.tracer_source.get_tracer(__name__) + self.memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) + self.tracer_source.add_span_processor(span_processor) + self.loop = asyncio.get_event_loop() + + def tearDown(self): + context.set_current(self.previous_context) + + @patch( + "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() + ) + def test_with_asyncio(self): + with self.tracer.start_as_current_span("asyncio_test"): + for name in _SPAN_NAMES: + self.submit_another_task(name) + + stop_loop_when( + self.loop, + lambda: len(self.memory_exporter.get_finished_spans()) >= 5, + timeout=5.0, + ) + self.loop.run_forever() + span_list = self.memory_exporter.get_finished_spans() + span_names_list = [span.name for span in span_list] + expected = [ + "test_span1", + "test_span2", + "test_span3", + "test_span4", + "test_span5", + "asyncio_test", + ] + self.assertCountEqual(span_names_list, expected) + span_names_list.sort() + expected.sort() + self.assertListEqual(span_names_list, expected) + expected_parent = next( + span for span in span_list if span.name == "asyncio_test" + ) + for span in span_list: + if span is expected_parent: + continue + self.assertEqual(span.parent, expected_parent) + + +class TestContextVarsContext(unittest.TestCase): + def setUp(self): + self.previous_context = context.get_current() + + def tearDown(self): + context.set_current(self.previous_context) + + @patch( + "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() + ) + def test_context(self): + self.assertIsNone(context.get_value("say")) + empty = context.get_current() + second = context.set_value("say", "foo") + + self.assertEqual(context.get_value("say", context=second), "foo") + + do_work() + self.assertEqual(context.get_value("say"), "bar") + third = context.get_current() + + self.assertIsNone(context.get_value("say", context=empty)) + self.assertEqual(context.get_value("say", context=second), "foo") + self.assertEqual(context.get_value("say", context=third), "bar") + + @patch( + "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() + ) + def test_set_value(self): + first = context.set_value("a", "yyy") + second = context.set_value("a", "zzz") + third = context.set_value("a", "---", first) + self.assertEqual("yyy", context.get_value("a", context=first)) + self.assertEqual("zzz", context.get_value("a", context=second)) + self.assertEqual("---", context.get_value("a", context=third)) + self.assertEqual(None, context.get_value("a")) diff --git a/opentelemetry-sdk/tests/context/test_context.py b/opentelemetry-sdk/tests/context/test_context.py new file mode 100644 index 0000000000..88a63109d1 --- /dev/null +++ b/opentelemetry-sdk/tests/context/test_context.py @@ -0,0 +1,63 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import patch + +from opentelemetry import context +from opentelemetry.sdk.context.threadlocal_context import ( + ThreadLocalRuntimeContext, +) + + +def do_work() -> None: + context.set_current(context.set_value("say", "bar")) + + +class TestThreadLocalContext(unittest.TestCase): + def setUp(self): + self.previous_context = context.get_current() + + def tearDown(self): + context.set_current(self.previous_context) + + @patch( + "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() + ) + def test_context(self): + self.assertIsNone(context.get_value("say")) + empty = context.get_current() + second = context.set_value("say", "foo") + + self.assertEqual(context.get_value("say", context=second), "foo") + + do_work() + self.assertEqual(context.get_value("say"), "bar") + third = context.get_current() + + self.assertIsNone(context.get_value("say", context=empty)) + self.assertEqual(context.get_value("say", context=second), "foo") + self.assertEqual(context.get_value("say", context=third), "bar") + + @patch( + "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() + ) + def test_set_value(self): + first = context.set_value("a", "yyy") + second = context.set_value("a", "zzz") + third = context.set_value("a", "---", first) + self.assertEqual("yyy", context.get_value("a", context=first)) + self.assertEqual("zzz", context.get_value("a", context=second)) + self.assertEqual("---", context.get_value("a", context=third)) + self.assertEqual(None, context.get_value("a")) diff --git a/opentelemetry-sdk/tests/context/test_threads.py b/opentelemetry-sdk/tests/context/test_threads.py new file mode 100644 index 0000000000..e8552b9135 --- /dev/null +++ b/opentelemetry-sdk/tests/context/test_threads.py @@ -0,0 +1,87 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from multiprocessing.dummy import Pool +from unittest.mock import patch + +from opentelemetry import context +from opentelemetry.sdk import trace +from opentelemetry.sdk.context.threadlocal_context import ( + ThreadLocalRuntimeContext, +) +from opentelemetry.sdk.trace import export +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + + +class TestThreads(unittest.TestCase): + span_names = [ + "test_span1", + "test_span2", + "test_span3", + "test_span4", + "test_span5", + ] + + def do_work(self, name="default"): + with self.tracer.start_as_current_span(name): + context.set_value("say-something", "bar") + + def setUp(self): + self.previous_context = context.get_current() + context.set_current(context.Context()) + self.tracer_source = trace.TracerSource() + self.tracer = self.tracer_source.get_tracer(__name__) + self.memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) + self.tracer_source.add_span_processor(span_processor) + + def tearDown(self): + context.set_current(self.previous_context) + + @patch( + "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() + ) + def test_with_threads(self): + with self.tracer.start_as_current_span("threads_test"): + pool = Pool(5) # create a thread pool + pool.map( + context.with_current_context(self.do_work), self.span_names + ) + pool.close() + pool.join() + span_list = self.memory_exporter.get_finished_spans() + span_names_list = [span.name for span in span_list] + expected = [ + "test_span1", + "test_span2", + "test_span3", + "test_span4", + "test_span5", + "threads_test", + ] + self.assertCountEqual(span_names_list, expected) + span_names_list.sort() + expected.sort() + self.assertListEqual(span_names_list, expected) + expected_parent = next( + span for span in span_list if span.name == "threads_test" + ) + # FIXME + for span in span_list: + if span is expected_parent: + continue + self.assertEqual(span.parent, expected_parent) diff --git a/tox.ini b/tox.ini index 4195e8629d..51eda59d70 100644 --- a/tox.ini +++ b/tox.ini @@ -82,6 +82,7 @@ commands_pre = jaeger: pip install {toxinidir}/opentelemetry-sdk jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-opentracing-shim + zipkin: pip install {toxinidir}/opentelemetry-sdk zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin ; In order to get a healthy coverage report, From 96943b242fc56b56e65695fc092bcbee7b32b27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 14 Feb 2020 14:07:32 -0500 Subject: [PATCH 0202/1517] Remove monotonic and absolute metrics intrument options (#410) Following opentelemetry-specification/430. --- .../src/opentelemetry/metrics/__init__.py | 12 +---- .../src/opentelemetry/sdk/metrics/__init__.py | 41 ---------------- .../tests/metrics/test_metrics.py | 48 +++++-------------- 3 files changed, 13 insertions(+), 88 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 3e04354d3c..5045c38eed 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -208,9 +208,7 @@ def set(self, value: ValueT, label_set: LabelSet) -> None: class Measure(Metric): """A measure type metric that represent raw stats that are recorded. - Measure metrics represent raw statistics that are recorded. By - default, measure metrics can accept both positive and negatives. - Negative inputs will be discarded when monotonic is True. + Measure metrics represent raw statistics that are recorded. """ def get_handle(self, label_set: LabelSet) -> "MeasureHandle": @@ -268,8 +266,6 @@ def create_metric( metric_type: Type[MetricT], label_keys: Sequence[str] = (), enabled: bool = True, - monotonic: bool = False, - absolute: bool = True, ) -> "Metric": """Creates a ``metric_kind`` metric with type ``value_type``. @@ -281,10 +277,6 @@ def create_metric( metric_type: The type of metric being created. label_keys: The keys for the labels with dynamic values. enabled: Whether to report the metric by default. - monotonic: Configure a counter or gauge that accepts only - monotonic/non-monotonic updates. - absolute: Configure a measure that does or does not accept negative - updates. Returns: A new ``metric_type`` metric with values of ``value_type``. """ @@ -318,8 +310,6 @@ def create_metric( metric_type: Type[MetricT], label_keys: Sequence[str] = (), enabled: bool = True, - monotonic: bool = False, - absolute: bool = True, ) -> "Metric": # pylint: disable=no-self-use return DefaultMetric() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index ea16878a7b..4c9231582c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -55,9 +55,6 @@ class BaseHandle: Args: value_type: The type of values this handle holds (int, float). enabled: True if the originating instrument is enabled. - monotonic: Indicates acceptance of only monotonic/non-monotonic values - for updating counter and gauge handles. - absolute: Indicates acceptance of negative updates to measure handles. aggregator: The aggregator for this handle. Will handle aggregation upon updates and checkpointing of values for exporting. """ @@ -66,14 +63,10 @@ def __init__( self, value_type: Type[metrics_api.ValueT], enabled: bool, - monotonic: bool, - absolute: bool, aggregator: Aggregator, ): self.value_type = value_type self.enabled = enabled - self.monotonic = monotonic - self.absolute = absolute self.aggregator = aggregator self.last_update_timestamp = time_ns() @@ -103,9 +96,6 @@ class CounterHandle(metrics_api.CounterHandle, BaseHandle): def add(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.CounterHandle.add`.""" if self._validate_update(value): - if self.monotonic and value < 0: - logger.warning("Monotonic counter cannot descend.") - return self.update(value) @@ -113,9 +103,6 @@ class GaugeHandle(metrics_api.GaugeHandle, BaseHandle): def set(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.GaugeHandle.set`.""" if self._validate_update(value): - if self.monotonic and value < self.aggregator.current: - logger.warning("Monotonic gauge cannot descend.") - return self.update(value) @@ -123,9 +110,6 @@ class MeasureHandle(metrics_api.MeasureHandle, BaseHandle): def record(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.MeasureHandle.record`.""" if self._validate_update(value): - if self.absolute and value < 0: - logger.warning("Absolute measure cannot accept negatives.") - return self.update(value) @@ -149,8 +133,6 @@ def __init__( meter: "Meter", label_keys: Sequence[str] = (), enabled: bool = True, - monotonic: bool = False, - absolute: bool = True, ): self.name = name self.description = description @@ -159,8 +141,6 @@ def __init__( self.meter = meter self.label_keys = label_keys self.enabled = enabled - self.monotonic = monotonic - self.absolute = absolute self.handles = {} def get_handle(self, label_set: LabelSet) -> BaseHandle: @@ -170,8 +150,6 @@ def get_handle(self, label_set: LabelSet) -> BaseHandle: handle = self.HANDLE_TYPE( self.value_type, self.enabled, - self.monotonic, - self.absolute, # Aggregator will be created based off type of metric self.meter.batcher.aggregator_for(self.__class__), ) @@ -188,10 +166,6 @@ def __repr__(self): class Counter(Metric, metrics_api.Counter): """See `opentelemetry.metrics.Counter`. - - By default, counter values can only go up (monotonic). Negative inputs - will be rejected for monotonic counter metrics. Counter metrics that have a - monotonic option set to False allows negative inputs. """ HANDLE_TYPE = CounterHandle @@ -205,8 +179,6 @@ def __init__( meter: "Meter", label_keys: Sequence[str] = (), enabled: bool = True, - monotonic: bool = True, - absolute: bool = False, ): super().__init__( name, @@ -216,8 +188,6 @@ def __init__( meter, label_keys=label_keys, enabled=enabled, - monotonic=monotonic, - absolute=absolute, ) def add(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: @@ -229,9 +199,6 @@ def add(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: class Gauge(Metric, metrics_api.Gauge): """See `opentelemetry.metrics.Gauge`. - - By default, gauge values can go both up and down (non-monotonic). - Negative inputs will be rejected for monotonic gauge metrics. """ HANDLE_TYPE = GaugeHandle @@ -245,8 +212,6 @@ def __init__( meter: "Meter", label_keys: Sequence[str] = (), enabled: bool = True, - monotonic: bool = False, - absolute: bool = False, ): super().__init__( name, @@ -256,8 +221,6 @@ def __init__( meter, label_keys=label_keys, enabled=enabled, - monotonic=monotonic, - absolute=absolute, ) def set(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: @@ -339,8 +302,6 @@ def create_metric( metric_type: Type[metrics_api.MetricT], label_keys: Sequence[str] = (), enabled: bool = True, - monotonic: bool = False, - absolute: bool = True, ) -> metrics_api.MetricT: """See `opentelemetry.metrics.Meter.create_metric`.""" # Ignore type b/c of mypy bug in addition to missing annotations @@ -352,8 +313,6 @@ def create_metric( self, label_keys=label_keys, enabled=enabled, - monotonic=monotonic, - absolute=absolute, ) self.metrics.add(metric) return metric diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 3a08433e8d..a887621b0c 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -203,28 +203,20 @@ def test_record(self): class TestCounterHandle(unittest.TestCase): def test_add(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, True, False, False, aggregator) + handle = metrics.CounterHandle(int, True, aggregator) handle.add(3) self.assertEqual(handle.aggregator.current, 3) def test_add_disabled(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, False, False, False, aggregator) + handle = metrics.CounterHandle(int, False, aggregator) handle.add(3) self.assertEqual(handle.aggregator.current, 0) - @mock.patch("opentelemetry.sdk.metrics.logger") - def test_add_monotonic(self, logger_mock): - aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, True, True, False, aggregator) - handle.add(-3) - self.assertEqual(handle.aggregator.current, 0) - self.assertTrue(logger_mock.warning.called) - @mock.patch("opentelemetry.sdk.metrics.logger") def test_add_incorrect_type(self, logger_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, True, False, False, aggregator) + handle = metrics.CounterHandle(int, True, aggregator) handle.add(3.0) self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @@ -232,7 +224,7 @@ def test_add_incorrect_type(self, logger_mock): @mock.patch("opentelemetry.sdk.metrics.time_ns") def test_update(self, time_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, True, False, False, aggregator) + handle = metrics.CounterHandle(int, True, aggregator) time_mock.return_value = 123 handle.update(4.0) self.assertEqual(handle.last_update_timestamp, 123) @@ -243,28 +235,20 @@ def test_update(self, time_mock): class TestGaugeHandle(unittest.TestCase): def test_set(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, False, False, aggregator) + handle = metrics.GaugeHandle(int, True, aggregator) handle.set(3) self.assertEqual(handle.aggregator.current, 3) def test_set_disabled(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, False, False, False, aggregator) + handle = metrics.GaugeHandle(int, False, aggregator) handle.set(3) self.assertEqual(handle.aggregator.current, 0) - @mock.patch("opentelemetry.sdk.metrics.logger") - def test_set_monotonic(self, logger_mock): - aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, True, False, aggregator) - handle.set(-3) - self.assertEqual(handle.aggregator.current, 0) - self.assertTrue(logger_mock.warning.called) - @mock.patch("opentelemetry.sdk.metrics.logger") def test_set_incorrect_type(self, logger_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, False, False, aggregator) + handle = metrics.GaugeHandle(int, True, aggregator) handle.set(3.0) self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @@ -272,7 +256,7 @@ def test_set_incorrect_type(self, logger_mock): @mock.patch("opentelemetry.sdk.metrics.time_ns") def test_update(self, time_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, False, False, aggregator) + handle = metrics.GaugeHandle(int, True, aggregator) time_mock.return_value = 123 handle.update(4.0) self.assertEqual(handle.last_update_timestamp, 123) @@ -283,28 +267,20 @@ def test_update(self, time_mock): class TestMeasureHandle(unittest.TestCase): def test_record(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.MeasureHandle(int, False, False, False, aggregator) + handle = metrics.MeasureHandle(int, False, aggregator) handle.record(3) self.assertEqual(handle.aggregator.current, 0) def test_record_disabled(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.MeasureHandle(int, False, False, False, aggregator) + handle = metrics.MeasureHandle(int, False, aggregator) handle.record(3) self.assertEqual(handle.aggregator.current, 0) - @mock.patch("opentelemetry.sdk.metrics.logger") - def test_record_monotonic(self, logger_mock): - aggregator = export.aggregate.CounterAggregator() - handle = metrics.MeasureHandle(int, True, False, True, aggregator) - handle.record(-3) - self.assertEqual(handle.aggregator.current, 0) - self.assertTrue(logger_mock.warning.called) - @mock.patch("opentelemetry.sdk.metrics.logger") def test_record_incorrect_type(self, logger_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.MeasureHandle(int, True, False, False, aggregator) + handle = metrics.MeasureHandle(int, True, aggregator) handle.record(3.0) self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @@ -312,7 +288,7 @@ def test_record_incorrect_type(self, logger_mock): @mock.patch("opentelemetry.sdk.metrics.time_ns") def test_update(self, time_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.MeasureHandle(int, True, False, False, aggregator) + handle = metrics.MeasureHandle(int, True, aggregator) time_mock.return_value = 123 handle.update(4.0) self.assertEqual(handle.last_update_timestamp, 123) From da541c870f44f0e3427eb1702f3f273f940709eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 18 Feb 2020 13:10:19 -0500 Subject: [PATCH 0203/1517] Remove "print(Context)" from basic tracer example (#428) Two reasons to remove it: - Now Context is a class and it doesn't print any useful information. - We don't expect users to interact directly with the context, so avoid it. --- README.md | 3 +-- examples/basic_tracer/tracer.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 05f4df29bc..97a76e1f0c 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,6 @@ pip install -e ./ext/opentelemetry-ext-{integration} ```python from opentelemetry import trace -from opentelemetry.context import Context from opentelemetry.sdk.trace import TracerSource from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor @@ -64,7 +63,7 @@ tracer = trace.tracer_source().get_tracer(__name__) with tracer.start_as_current_span('foo'): with tracer.start_as_current_span('bar'): with tracer.start_as_current_span('baz'): - print(Context) + print("Hello world from OpenTelemetry Python!") ``` ### Metrics diff --git a/examples/basic_tracer/tracer.py b/examples/basic_tracer/tracer.py index 4b392fd1ea..5093280a5a 100755 --- a/examples/basic_tracer/tracer.py +++ b/examples/basic_tracer/tracer.py @@ -17,7 +17,6 @@ import os from opentelemetry import trace -from opentelemetry.context import Context from opentelemetry.sdk.trace import TracerSource from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, @@ -51,4 +50,4 @@ with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): - print(Context) + print("Hello world from OpenTelemetry Python!") From d824f19192eb050fb9a3778b8b6fd0772ffac4a3 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 18 Feb 2020 10:31:47 -0800 Subject: [PATCH 0204/1517] Moving contextvars and threadlocal context implementations to the API (#419) While keeping the flexibility of the interface to support additional implementations of the RuntimeContext, moving the ContextVarsRuntimeContext and ThreadLocalRuntimeContext into the API ensures a useful context implementation for users without requiring additional configuration. This changes checks the version number to determine which implementation to use, unless it is overridden by the OPENTELEMETRY_CONTEXT environment variable. Removing the DefaultRuntimeContext as part of this change. Signed-off-by: Alex Boten Co-authored-by: Chris Kleinknecht --- opentelemetry-api/setup.py | 14 ++- .../src/opentelemetry/context/__init__.py | 8 +- .../context/aiocontextvarsfix.py | 2 +- .../context/contextvars_context.py | 7 +- .../opentelemetry/context/default_context.py | 34 -------- .../context/threadlocal_context.py | 7 +- .../tests/context/test_contextvars_context.py | 68 +++++++++++++++ .../tests/context/test_threadlocal_context.py | 8 +- opentelemetry-sdk/setup.py | 12 +-- .../tests/context/test_asyncio.py | 44 +--------- .../tests/context/test_threads.py | 87 ------------------- 11 files changed, 99 insertions(+), 192 deletions(-) rename {opentelemetry-sdk/src/opentelemetry/sdk => opentelemetry-api/src/opentelemetry}/context/aiocontextvarsfix.py (96%) rename {opentelemetry-sdk/src/opentelemetry/sdk => opentelemetry-api/src/opentelemetry}/context/contextvars_context.py (88%) delete mode 100644 opentelemetry-api/src/opentelemetry/context/default_context.py rename {opentelemetry-sdk/src/opentelemetry/sdk => opentelemetry-api/src/opentelemetry}/context/threadlocal_context.py (88%) create mode 100644 opentelemetry-api/tests/context/test_contextvars_context.py rename opentelemetry-sdk/tests/context/test_context.py => opentelemetry-api/tests/context/test_threadlocal_context.py (93%) delete mode 100644 opentelemetry-sdk/tests/context/test_threads.py diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index fad86f171b..20e7f58143 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -44,7 +44,10 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["typing; python_version<'3.5'"], + install_requires=[ + "typing; python_version<'3.5'", + "aiocontextvars; python_version<'3.7' and python_version>='3.5'", + ], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, @@ -58,9 +61,12 @@ zip_safe=False, entry_points={ "opentelemetry_context": [ - "default_context = " - "opentelemetry.context.default_context:" - "DefaultRuntimeContext", + "contextvars_context = " + "opentelemetry.context.contextvars_context:" + "ContextVarsRuntimeContext", + "threadlocal_context = " + "opentelemetry.context.threadlocal_context:" + "ThreadLocalRuntimeContext", ] }, ) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 63de570abc..1d1b53e7cb 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -15,6 +15,7 @@ import logging import typing from os import environ +from sys import version_info from pkg_resources import iter_entry_points @@ -84,9 +85,14 @@ def get_current() -> Context: if _RUNTIME_CONTEXT is None: # FIXME use a better implementation of a configuration manager to avoid having # to get configuration values straight from environment variables + if version_info < (3, 5): + # contextvars are not supported in 3.4, use thread-local storage + default_context = "threadlocal_context" + else: + default_context = "contextvars_context" configured_context = environ.get( - "OPENTELEMETRY_CONTEXT", "default_context" + "OPENTELEMETRY_CONTEXT", default_context ) # type: str try: _RUNTIME_CONTEXT = next( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/aiocontextvarsfix.py b/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py similarity index 96% rename from opentelemetry-sdk/src/opentelemetry/sdk/context/aiocontextvarsfix.py rename to opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py index 6aa1779378..f0d4ce56ff 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/aiocontextvarsfix.py +++ b/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py @@ -55,7 +55,7 @@ def _get_event_loop(): asyncio._set_running_loop = asyncio.events._set_running_loop # noinspection PyUnresolvedReferences -import aiocontextvars # pylint: disable=unused-import,wrong-import-position # noqa # isort:skip +import aiocontextvars # pylint: disable=import-error,unused-import,wrong-import-position # noqa # isort:skip def _run_coroutine_threadsafe(coro, loop): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py similarity index 88% rename from opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py rename to opentelemetry-api/src/opentelemetry/context/contextvars_context.py index 0a350e2699..1fd202275a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -14,14 +14,13 @@ from contextvars import ContextVar from sys import version_info -from opentelemetry.context import Context -from opentelemetry.context.context import RuntimeContext +from opentelemetry.context.context import Context, RuntimeContext if (3, 5, 3) <= version_info < (3, 7): - import aiocontextvars # type: ignore # pylint:disable=unused-import + import aiocontextvars # type: ignore # pylint:disable=unused-import,import-error elif (3, 4) < version_info <= (3, 5, 2): - import opentelemetry.sdk.context.aiocontextvarsfix # pylint:disable=unused-import + import opentelemetry.context.aiocontextvarsfix # pylint:disable=unused-import class ContextVarsRuntimeContext(RuntimeContext): diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py deleted file mode 100644 index 6c83f839d3..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2020, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from opentelemetry.context.context import Context, RuntimeContext - - -class DefaultRuntimeContext(RuntimeContext): - """A default implementation of the RuntimeContext interface using - a dictionary to store values. - """ - - def __init__(self) -> None: - self._current_context = Context() - - def set_current(self, context: Context) -> None: - """See `opentelemetry.context.RuntimeContext.set_current`.""" - self._current_context = context - - def get_current(self) -> Context: - """See `opentelemetry.context.RuntimeContext.get_current`.""" - return self._current_context - - -__all__ = ["DefaultRuntimeContext"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py similarity index 88% rename from opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py rename to opentelemetry-api/src/opentelemetry/context/threadlocal_context.py index 26d4329c52..899ab86326 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py @@ -14,7 +14,7 @@ import threading -from opentelemetry.context import Context, RuntimeContext +from opentelemetry.context.context import Context, RuntimeContext class ThreadLocalRuntimeContext(RuntimeContext): @@ -38,7 +38,10 @@ def get_current(self) -> Context: setattr( self._current_context, self._CONTEXT_KEY, Context(), ) - return getattr(self._current_context, self._CONTEXT_KEY) + context = getattr( + self._current_context, self._CONTEXT_KEY + ) # type: Context + return context __all__ = ["ThreadLocalRuntimeContext"] diff --git a/opentelemetry-api/tests/context/test_contextvars_context.py b/opentelemetry-api/tests/context/test_contextvars_context.py new file mode 100644 index 0000000000..ebc15d6d9a --- /dev/null +++ b/opentelemetry-api/tests/context/test_contextvars_context.py @@ -0,0 +1,68 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import patch + +from opentelemetry import context + +try: + import contextvars # pylint: disable=unused-import + from opentelemetry.context.contextvars_context import ( + ContextVarsRuntimeContext, + ) +except ImportError: + raise unittest.SkipTest("contextvars not available") + + +def do_work() -> None: + context.set_current(context.set_value("say", "bar")) + + +class TestContextVarsContext(unittest.TestCase): + def setUp(self): + self.previous_context = context.get_current() + + def tearDown(self): + context.set_current(self.previous_context) + + @patch( + "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() # type: ignore + ) + def test_context(self): + self.assertIsNone(context.get_value("say")) + empty = context.get_current() + second = context.set_value("say", "foo") + + self.assertEqual(context.get_value("say", context=second), "foo") + + do_work() + self.assertEqual(context.get_value("say"), "bar") + third = context.get_current() + + self.assertIsNone(context.get_value("say", context=empty)) + self.assertEqual(context.get_value("say", context=second), "foo") + self.assertEqual(context.get_value("say", context=third), "bar") + + @patch( + "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() # type: ignore + ) + def test_set_value(self): + first = context.set_value("a", "yyy") + second = context.set_value("a", "zzz") + third = context.set_value("a", "---", first) + self.assertEqual("yyy", context.get_value("a", context=first)) + self.assertEqual("zzz", context.get_value("a", context=second)) + self.assertEqual("---", context.get_value("a", context=third)) + self.assertEqual(None, context.get_value("a")) diff --git a/opentelemetry-sdk/tests/context/test_context.py b/opentelemetry-api/tests/context/test_threadlocal_context.py similarity index 93% rename from opentelemetry-sdk/tests/context/test_context.py rename to opentelemetry-api/tests/context/test_threadlocal_context.py index 88a63109d1..aca6b69de7 100644 --- a/opentelemetry-sdk/tests/context/test_context.py +++ b/opentelemetry-api/tests/context/test_threadlocal_context.py @@ -16,9 +16,7 @@ from unittest.mock import patch from opentelemetry import context -from opentelemetry.sdk.context.threadlocal_context import ( - ThreadLocalRuntimeContext, -) +from opentelemetry.context.threadlocal_context import ThreadLocalRuntimeContext def do_work() -> None: @@ -33,7 +31,7 @@ def tearDown(self): context.set_current(self.previous_context) @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() + "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() # type: ignore ) def test_context(self): self.assertIsNone(context.get_value("say")) @@ -51,7 +49,7 @@ def test_context(self): self.assertEqual(context.get_value("say", context=third), "bar") @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() + "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() # type: ignore ) def test_set_value(self): first = context.set_value("a", "yyy") diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index 7e88bb3bfe..cbfb0f075d 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -44,7 +44,7 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["opentelemetry-api==0.4.dev0", "aiocontextvars"], + install_requires=["opentelemetry-api==0.4.dev0"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, @@ -56,14 +56,4 @@ "/tree/master/opentelemetry-sdk" ), zip_safe=False, - entry_points={ - "opentelemetry_context": [ - "contextvars_context = " - "opentelemetry.sdk.context.contextvars_context:" - "ContextVarsRuntimeContext", - "threadlocal_context = " - "opentelemetry.sdk.context.threadlocal_context:" - "ThreadLocalRuntimeContext", - ] - }, ) diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index 5dc3637598..e1cb90f452 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -25,7 +25,7 @@ try: import contextvars # pylint: disable=unused-import - from opentelemetry.sdk.context.contextvars_context import ( + from opentelemetry.context.contextvars_context import ( ContextVarsRuntimeContext, ) except ImportError: @@ -53,10 +53,6 @@ def stop_loop_when(loop, cond_func, timeout=5.0): loop.call_later(0.1, stop_loop_when, loop, cond_func, timeout) -def do_work() -> None: - context.set_current(context.set_value("say", "bar")) - - class TestAsyncio(unittest.TestCase): @asyncio.coroutine def task(self, name): @@ -114,41 +110,3 @@ def test_with_asyncio(self): if span is expected_parent: continue self.assertEqual(span.parent, expected_parent) - - -class TestContextVarsContext(unittest.TestCase): - def setUp(self): - self.previous_context = context.get_current() - - def tearDown(self): - context.set_current(self.previous_context) - - @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() - ) - def test_context(self): - self.assertIsNone(context.get_value("say")) - empty = context.get_current() - second = context.set_value("say", "foo") - - self.assertEqual(context.get_value("say", context=second), "foo") - - do_work() - self.assertEqual(context.get_value("say"), "bar") - third = context.get_current() - - self.assertIsNone(context.get_value("say", context=empty)) - self.assertEqual(context.get_value("say", context=second), "foo") - self.assertEqual(context.get_value("say", context=third), "bar") - - @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() - ) - def test_set_value(self): - first = context.set_value("a", "yyy") - second = context.set_value("a", "zzz") - third = context.set_value("a", "---", first) - self.assertEqual("yyy", context.get_value("a", context=first)) - self.assertEqual("zzz", context.get_value("a", context=second)) - self.assertEqual("---", context.get_value("a", context=third)) - self.assertEqual(None, context.get_value("a")) diff --git a/opentelemetry-sdk/tests/context/test_threads.py b/opentelemetry-sdk/tests/context/test_threads.py deleted file mode 100644 index e8552b9135..0000000000 --- a/opentelemetry-sdk/tests/context/test_threads.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2020, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -from multiprocessing.dummy import Pool -from unittest.mock import patch - -from opentelemetry import context -from opentelemetry.sdk import trace -from opentelemetry.sdk.context.threadlocal_context import ( - ThreadLocalRuntimeContext, -) -from opentelemetry.sdk.trace import export -from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, -) - - -class TestThreads(unittest.TestCase): - span_names = [ - "test_span1", - "test_span2", - "test_span3", - "test_span4", - "test_span5", - ] - - def do_work(self, name="default"): - with self.tracer.start_as_current_span(name): - context.set_value("say-something", "bar") - - def setUp(self): - self.previous_context = context.get_current() - context.set_current(context.Context()) - self.tracer_source = trace.TracerSource() - self.tracer = self.tracer_source.get_tracer(__name__) - self.memory_exporter = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) - self.tracer_source.add_span_processor(span_processor) - - def tearDown(self): - context.set_current(self.previous_context) - - @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() - ) - def test_with_threads(self): - with self.tracer.start_as_current_span("threads_test"): - pool = Pool(5) # create a thread pool - pool.map( - context.with_current_context(self.do_work), self.span_names - ) - pool.close() - pool.join() - span_list = self.memory_exporter.get_finished_spans() - span_names_list = [span.name for span in span_list] - expected = [ - "test_span1", - "test_span2", - "test_span3", - "test_span4", - "test_span5", - "threads_test", - ] - self.assertCountEqual(span_names_list, expected) - span_names_list.sort() - expected.sort() - self.assertListEqual(span_names_list, expected) - expected_parent = next( - span for span in span_list if span.name == "threads_test" - ) - # FIXME - for span in span_list: - if span is expected_parent: - continue - self.assertEqual(span.parent, expected_parent) From 120ae29068de2e0f1cc5f3173cfdc9806f25ed18 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 18 Feb 2020 10:33:30 -0800 Subject: [PATCH 0205/1517] Upgrading tox envs to python3.8 (#426) Python3.8 is now stable, and we should run the common tasks under 3.8 instead of 3.7 --- tox.ini | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index 51eda59d70..5a69ca62b6 100644 --- a/tox.ini +++ b/tox.ini @@ -12,14 +12,14 @@ envlist = ; pypy3-coverage lint - py37-tracecontext - py37-{mypy,mypyinstalled} + py38-tracecontext + py38-{mypy,mypyinstalled} docs docker-tests [travis] python = - 3.7: py37, lint, docs, docker-tests + 3.8: py38, lint, docs, docker-tests [testenv] deps = @@ -107,7 +107,7 @@ commands = [testenv:lint] -basepython: python3.7 +basepython: python3.8 recreate = True deps = -c dev-requirements.txt @@ -139,8 +139,8 @@ changedir = docs commands = sphinx-build -E -a -W --keep-going -b html -T . _build/html -[testenv:py37-tracecontext] -basepython: python3.7 +[testenv:py38-tracecontext] +basepython: python3.8 deps = # needed for tracecontext aiohttp~=3.6 From 5a0dae96da92f26e523775678425b857e0ac3d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 18 Feb 2020 22:59:43 -0500 Subject: [PATCH 0206/1517] Implement MinMaxSumCount aggregator (#422) Adding one of the core aggregators in the metrics API. This aggregator is the default aggregator for measure metrics and keeps the minimum, maximum, sum and count of those measures. --- examples/metrics/simple_example.py | 106 ++++++++++++++++++ examples/metrics/stateful.py | 72 ------------ examples/metrics/stateless.py | 57 ---------- .../sdk/metrics/export/aggregate.py | 50 +++++++++ .../sdk/metrics/export/batcher.py | 7 +- .../tests/metrics/export/test_export.py | 89 ++++++++++++++- .../tests/metrics/test_metrics.py | 35 +++--- 7 files changed, 264 insertions(+), 152 deletions(-) create mode 100644 examples/metrics/simple_example.py delete mode 100644 examples/metrics/stateful.py delete mode 100644 examples/metrics/stateless.py diff --git a/examples/metrics/simple_example.py b/examples/metrics/simple_example.py new file mode 100644 index 0000000000..75da80b73a --- /dev/null +++ b/examples/metrics/simple_example.py @@ -0,0 +1,106 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This module serves as an example for a simple application using metrics +It shows: +- How to configure a meter passing a sateful or stateless. +- How to configure an exporter and how to create a controller. +- How to create some metrics intruments and how to capture data with them. +""" +import sys +import time + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import Counter, Measure, Meter +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.controller import PushController + +batcher_mode = "stateful" + + +def usage(argv): + print("usage:") + print("{} [mode]".format(argv[0])) + print("mode: stateful (default) or stateless") + + +if len(sys.argv) >= 2: + batcher_mode = sys.argv[1] + if batcher_mode not in ("stateful", "stateless"): + print("bad mode specified.") + usage(sys.argv) + sys.exit(1) + +# Batcher used to collect all created metrics from meter ready for exporting +# Pass in True/False to indicate whether the batcher is stateful. +# True indicates the batcher computes checkpoints from over the process +# lifetime. +# False indicates the batcher computes checkpoints which describe the updates +# of a single collection period (deltas) +batcher = UngroupedBatcher(batcher_mode == "stateful") + +# If a batcher is not provided, a default batcher is used +# Meter is responsible for creating and recording metrics +metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) +meter = metrics.meter() + +# Exporter to export metrics to the console +exporter = ConsoleMetricsExporter() + +# A PushController collects metrics created from meter and exports it via the +# exporter every interval +controller = PushController(meter, exporter, 5) + +# Metric instruments allow to capture measurements +requests_counter = meter.create_metric( + "requests", "number of requests", 1, int, Counter, ("environment",) +) + +clicks_counter = meter.create_metric( + "clicks", "number of clicks", 1, int, Counter, ("environment",) +) + +requests_size = meter.create_metric( + "requests_size", "size of requests", 1, int, Measure, ("environment",) +) + +# Labelsets are used to identify key-values that are associated with a specific +# metric that you want to record. These are useful for pre-aggregation and can +# be used to store custom dimensions pertaining to a metric +staging_label_set = meter.get_label_set({"environment": "staging"}) +testing_label_set = meter.get_label_set({"environment": "testing"}) + +# Update the metric instruments using the direct calling convention +requests_size.record(100, staging_label_set) +requests_counter.add(25, staging_label_set) +# Sleep for 5 seconds, exported value should be 25 +time.sleep(5) + +requests_size.record(5000, staging_label_set) +requests_counter.add(50, staging_label_set) +# Exported value should be 75 +time.sleep(5) + +requests_size.record(2, testing_label_set) +requests_counter.add(35, testing_label_set) +# There should be two exported values 75 and 35, one for each labelset +time.sleep(5) + +clicks_counter.add(5, staging_label_set) +# There should be three exported values, labelsets can be reused for different +# metrics but will be recorded seperately, 75, 35 and 5 + +time.sleep(5) diff --git a/examples/metrics/stateful.py b/examples/metrics/stateful.py deleted file mode 100644 index c43f795e22..0000000000 --- a/examples/metrics/stateful.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -This module serves as an example for a simple application using metrics -Examples show how to recording affects the collection of metrics to be exported -""" -import time - -from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Meter -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher -from opentelemetry.sdk.metrics.export.controller import PushController - -# Batcher used to collect all created metrics from meter ready for exporting -# Pass in true/false to indicate whether the batcher is stateful. True -# indicates the batcher computes checkpoints from over the process lifetime. -# False indicates the batcher computes checkpoints which describe the updates -# of a single collection period (deltas) -batcher = UngroupedBatcher(True) -# If a batcher is not provded, a default batcher is used -# Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) -meter = metrics.meter() -# exporter to export metrics to the console -exporter = ConsoleMetricsExporter() -# controller collects metrics created from meter and exports it via the -# exporter every interval -controller = PushController(meter, exporter, 5) - -counter = meter.create_metric( - "requests", "number of requests", 1, int, Counter, ("environment",) -) - -counter2 = meter.create_metric( - "clicks", "number of clicks", 1, int, Counter, ("environment",) -) - -# Labelsets are used to identify key-values that are associated with a specific -# metric that you want to record. These are useful for pre-aggregation and can -# be used to store custom dimensions pertaining to a metric -label_set = meter.get_label_set({"environment": "staging"}) -label_set2 = meter.get_label_set({"environment": "testing"}) - -counter.add(25, label_set) -# We sleep for 5 seconds, exported value should be 25 -time.sleep(5) - -counter.add(50, label_set) -# exported value should be 75 -time.sleep(5) - -counter.add(35, label_set2) -# should be two exported values 75 and 35, one for each labelset -time.sleep(5) - -counter2.add(5, label_set) -# should be three exported values, labelsets can be reused for different -# metrics but will be recorded seperately, 75, 35 and 5 -time.sleep(5) diff --git a/examples/metrics/stateless.py b/examples/metrics/stateless.py deleted file mode 100644 index 69213cbddd..0000000000 --- a/examples/metrics/stateless.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -This module serves as an example for a simple application using metrics -Examples show how to recording affects the collection of metrics to be exported -""" -import time - -from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Meter -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher -from opentelemetry.sdk.metrics.export.controller import PushController - -# Batcher used to collect all created metrics from meter ready for exporting -# Pass in false for non-stateful batcher. Indicates the batcher computes -# checkpoints which describe the updates of a single collection period (deltas) -batcher = UngroupedBatcher(False) -# Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) -meter = metrics.meter() -# exporter to export metrics to the console -exporter = ConsoleMetricsExporter() -# controller collects metrics created from meter and exports it via the -# exporter every interval -controller = PushController(meter, exporter, 5) - -counter = meter.create_metric( - "requests", "number of requests", 1, int, Counter, ("environment",) -) - -# Labelsets are used to identify key-values that are associated with a specific -# metric that you want to record. These are useful for pre-aggregation and can -# be used to store custom dimensions pertaining to a metric -label_set = meter.get_label_set({"environment": "staging"}) - -counter.add(25, label_set) -# We sleep for 5 seconds, exported value should be 25 -time.sleep(5) - -counter.add(50, label_set) -# exported value should be 50 due to non-stateful batcher -time.sleep(20) - -# Following exported values would be 0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 642fe1cdfe..5c55ba038a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -13,6 +13,7 @@ # limitations under the License. import abc +from collections import namedtuple class Aggregator(abc.ABC): @@ -56,3 +57,52 @@ def take_checkpoint(self): def merge(self, other): self.checkpoint += other.checkpoint + + +class MinMaxSumCountAggregator(Aggregator): + """Agregator for Measure metrics that keeps min, max, sum and count.""" + + _TYPE = namedtuple("minmaxsumcount", "min max sum count") + + @classmethod + def _min(cls, val1, val2): + if val1 is None and val2 is None: + return None + return min(val1 or val2, val2 or val1) + + @classmethod + def _max(cls, val1, val2): + if val1 is None and val2 is None: + return None + return max(val1 or val2, val2 or val1) + + @classmethod + def _sum(cls, val1, val2): + if val1 is None and val2 is None: + return None + return (val1 or 0) + (val2 or 0) + + def __init__(self): + super().__init__() + self.current = self._TYPE(None, None, None, 0) + self.checkpoint = self._TYPE(None, None, None, 0) + + def update(self, value): + self.current = self._TYPE( + self._min(self.current.min, value), + self._max(self.current.max, value), + self._sum(self.current.sum, value), + self.current.count + 1, + ) + + def take_checkpoint(self): + self.checkpoint = self.current + self.current = self._TYPE(None, None, None, 0) + + def merge(self, other): + self.checkpoint = self._TYPE( + self._min(self.checkpoint.min, other.checkpoint.min), + self._max(self.checkpoint.max, other.checkpoint.max), + self._sum(self.checkpoint.sum, other.checkpoint.sum), + self.checkpoint.count + other.checkpoint.count, + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index c81db0fe74..86ddc3fcc1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -15,11 +15,12 @@ import abc from typing import Sequence, Type -from opentelemetry.metrics import Counter, MetricT +from opentelemetry.metrics import Counter, Measure, MetricT from opentelemetry.sdk.metrics.export import MetricRecord from opentelemetry.sdk.metrics.export.aggregate import ( Aggregator, CounterAggregator, + MinMaxSumCountAggregator, ) @@ -45,8 +46,10 @@ def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: Aggregators keep track of and updates values when metrics get updated. """ # pylint:disable=R0201 - if metric_type == Counter: + if issubclass(metric_type, Counter): return CounterAggregator() + if issubclass(metric_type, Measure): + return MinMaxSumCountAggregator() # TODO: Add other aggregators return CounterAggregator() diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 816bfcfca9..5df6c6d08a 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -20,7 +20,10 @@ ConsoleMetricsExporter, MetricRecord, ) -from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator +from opentelemetry.sdk.metrics.export.aggregate import ( + CounterAggregator, + MinMaxSumCountAggregator, +) from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.metrics.export.controller import PushController @@ -218,22 +221,21 @@ def test_ungrouped_batcher_process_not_stateful(self): ) -class TestAggregator(unittest.TestCase): - # TODO: test other aggregators once implemented - def test_counter_update(self): +class TestCounterAggregator(unittest.TestCase): + def test_update(self): counter = CounterAggregator() counter.update(1.0) counter.update(2.0) self.assertEqual(counter.current, 3.0) - def test_counter_checkpoint(self): + def test_checkpoint(self): counter = CounterAggregator() counter.update(2.0) counter.take_checkpoint() self.assertEqual(counter.current, 0) self.assertEqual(counter.checkpoint, 2.0) - def test_counter_merge(self): + def test_merge(self): counter = CounterAggregator() counter2 = CounterAggregator() counter.checkpoint = 1.0 @@ -242,6 +244,81 @@ def test_counter_merge(self): self.assertEqual(counter.checkpoint, 4.0) +class TestMinMaxSumCountAggregator(unittest.TestCase): + def test_update(self): + mmsc = MinMaxSumCountAggregator() + # test current values without any update + self.assertEqual( + mmsc.current, (None, None, None, 0), + ) + + # call update with some values + values = (3, 50, 3, 97) + for val in values: + mmsc.update(val) + + self.assertEqual( + mmsc.current, (min(values), max(values), sum(values), len(values)), + ) + + def test_checkpoint(self): + mmsc = MinMaxSumCountAggregator() + + # take checkpoint wihtout any update + mmsc.take_checkpoint() + self.assertEqual( + mmsc.checkpoint, (None, None, None, 0), + ) + + # call update with some values + values = (3, 50, 3, 97) + for val in values: + mmsc.update(val) + + mmsc.take_checkpoint() + self.assertEqual( + mmsc.checkpoint, + (min(values), max(values), sum(values), len(values)), + ) + + self.assertEqual( + mmsc.current, (None, None, None, 0), + ) + + def test_merge(self): + mmsc1 = MinMaxSumCountAggregator() + mmsc2 = MinMaxSumCountAggregator() + + checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) + checkpoint2 = MinMaxSumCountAggregator._TYPE(1, 33, 44, 2) + + mmsc1.checkpoint = checkpoint1 + mmsc2.checkpoint = checkpoint2 + + mmsc1.merge(mmsc2) + + self.assertEqual( + mmsc1.checkpoint, + ( + min(checkpoint1.min, checkpoint2.min), + max(checkpoint1.max, checkpoint2.max), + checkpoint1.sum + checkpoint2.sum, + checkpoint1.count + checkpoint2.count, + ), + ) + + def test_merge_with_empty(self): + mmsc1 = MinMaxSumCountAggregator() + mmsc2 = MinMaxSumCountAggregator() + + checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) + mmsc1.checkpoint = checkpoint1 + + mmsc1.merge(mmsc2) + + self.assertEqual(mmsc1.checkpoint, checkpoint1) + + class TestController(unittest.TestCase): def test_push_controller(self): meter = mock.Mock() diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index a887621b0c..db7e2d8c85 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -90,8 +90,10 @@ def test_record_batch_multiple(self): meter.record_batch(label_set, record_tuples) self.assertEqual(counter.get_handle(label_set).aggregator.current, 1.0) self.assertEqual(gauge.get_handle(label_set).aggregator.current, 5.0) - # TODO: Fix when aggregator implemented for measure - self.assertEqual(measure.get_handle(label_set).aggregator.current, 3.0) + self.assertEqual( + measure.get_handle(label_set).aggregator.current, + (3.0, 3.0, 3.0, 1), + ) def test_record_batch_exists(self): meter = metrics.Meter() @@ -195,9 +197,13 @@ def test_record(self): kvp = {"key": "value"} label_set = meter.get_label_set(kvp) handle = metric.get_handle(label_set) - metric.record(3, label_set) - # TODO: Fix once other aggregators implemented - self.assertEqual(handle.aggregator.current, 3) + values = (37, 42, 7) + for val in values: + metric.record(val, label_set) + self.assertEqual( + handle.aggregator.current, + (min(values), max(values), sum(values), len(values)), + ) class TestCounterHandle(unittest.TestCase): @@ -263,33 +269,32 @@ def test_update(self, time_mock): self.assertEqual(handle.aggregator.current, 4.0) -# TODO: fix tests once aggregator implemented class TestMeasureHandle(unittest.TestCase): def test_record(self): - aggregator = export.aggregate.CounterAggregator() - handle = metrics.MeasureHandle(int, False, aggregator) + aggregator = export.aggregate.MinMaxSumCountAggregator() + handle = metrics.MeasureHandle(int, True, aggregator) handle.record(3) - self.assertEqual(handle.aggregator.current, 0) + self.assertEqual(handle.aggregator.current, (3, 3, 3, 1)) def test_record_disabled(self): - aggregator = export.aggregate.CounterAggregator() + aggregator = export.aggregate.MinMaxSumCountAggregator() handle = metrics.MeasureHandle(int, False, aggregator) handle.record(3) - self.assertEqual(handle.aggregator.current, 0) + self.assertEqual(handle.aggregator.current, (None, None, None, 0)) @mock.patch("opentelemetry.sdk.metrics.logger") def test_record_incorrect_type(self, logger_mock): - aggregator = export.aggregate.CounterAggregator() + aggregator = export.aggregate.MinMaxSumCountAggregator() handle = metrics.MeasureHandle(int, True, aggregator) handle.record(3.0) - self.assertEqual(handle.aggregator.current, 0) + self.assertEqual(handle.aggregator.current, (None, None, None, 0)) self.assertTrue(logger_mock.warning.called) @mock.patch("opentelemetry.sdk.metrics.time_ns") def test_update(self, time_mock): - aggregator = export.aggregate.CounterAggregator() + aggregator = export.aggregate.MinMaxSumCountAggregator() handle = metrics.MeasureHandle(int, True, aggregator) time_mock.return_value = 123 handle.update(4.0) self.assertEqual(handle.last_update_timestamp, 123) - self.assertEqual(handle.aggregator.current, 4.0) + self.assertEqual(handle.aggregator.current, (4.0, 4.0, 4.0, 1)) From 972e2abf0638f9e3c3792529468eaf5a4cdf5bd9 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Wed, 19 Feb 2020 14:15:27 -0800 Subject: [PATCH 0207/1517] Adding trace.get_tracer (#430) It's fairly common to need to retrieve tracers, and as such adding the additional tracer_source() call to every retrieval of a tracer can add to a lot of extra boilerplate at minimal semantic value. It would be uncommon for one to use multiple tracer_source objects, as typically one would want all tracers to be created and behave in a similar way (e.g. passed to the same span processor). Co-Authored-By: Chris Kleinknecht --- README.md | 2 +- examples/basic_tracer/tracer.py | 2 +- examples/http/server.py | 2 +- .../flask_example.py | 2 +- examples/opentracing/main.py | 2 +- ext/opentelemetry-ext-dbapi/README.rst | 2 +- .../src/opentelemetry/ext/flask/__init__.py | 2 +- ext/opentelemetry-ext-jaeger/README.rst | 2 +- .../examples/jaeger_exporter_example.py | 2 +- .../ext/opentracing_shim/__init__.py | 2 +- ext/opentelemetry-ext-psycopg2/README.rst | 2 +- .../src/opentelemetry/ext/wsgi/__init__.py | 2 +- ext/opentelemetry-ext-zipkin/README.rst | 2 +- .../src/opentelemetry/trace/__init__.py | 22 +++++++++- opentelemetry-api/tests/trace/test_globals.py | 41 +++++++++++++++++++ 15 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 opentelemetry-api/tests/trace/test_globals.py diff --git a/README.md b/README.md index 97a76e1f0c..b7cda2ddb0 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) trace.tracer_source().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) -tracer = trace.tracer_source().get_tracer(__name__) +tracer = trace.get_tracer(__name__) with tracer.start_as_current_span('foo'): with tracer.start_as_current_span('bar'): with tracer.start_as_current_span('baz'): diff --git a/examples/basic_tracer/tracer.py b/examples/basic_tracer/tracer.py index 5093280a5a..a6b33a01b3 100755 --- a/examples/basic_tracer/tracer.py +++ b/examples/basic_tracer/tracer.py @@ -41,7 +41,7 @@ # We tell OpenTelemetry who it is that is creating spans. In this case, we have # no real name (no setup.py), so we make one up. If we had a version, we would # also specify it here. -tracer = trace.tracer_source().get_tracer(__name__) +tracer = trace.get_tracer(__name__) # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) diff --git a/examples/http/server.py b/examples/http/server.py index 68e3d952b0..8d1aea1e06 100755 --- a/examples/http/server.py +++ b/examples/http/server.py @@ -42,7 +42,7 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) -tracer = trace.tracer_source().get_tracer(__name__) +tracer = trace.get_tracer(__name__) # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index f7a9872b6b..62795751d3 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -67,7 +67,7 @@ def hello(): version = pkg_resources.get_distribution( "opentelemetry-example-app" ).version - tracer = trace.tracer_source().get_tracer(__name__, version) + tracer = trace.get_tracer(__name__, version) with tracer.start_as_current_span("example-request"): requests.get("http://www.example.com") return "hello" diff --git a/examples/opentracing/main.py b/examples/opentracing/main.py index 81d23f10e4..922e1263b5 100755 --- a/examples/opentracing/main.py +++ b/examples/opentracing/main.py @@ -28,7 +28,7 @@ redis_cache = RedisCache(opentracing_tracer) # Appication code uses an OpenTelemetry Tracer as usual. -tracer = trace.tracer_source().get_tracer(__name__) +tracer = trace.get_tracer(__name__) @redis_cache diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst index 3618453823..b0bdbdd312 100644 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -15,7 +15,7 @@ Usage from opentelemetry.ext.dbapi import trace_integration trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) - tracer = trace.tracer_source().get_tracer(__name__) + tracer = trace.get_tracer(__name__) # Ex: mysql.connector trace_integration(tracer_source(), mysql.connector, "connect", "mysql") diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index 4bf0cc8c68..aa9217c00e 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -61,7 +61,7 @@ def _before_flask_request(): otel_wsgi.get_header_from_environ, environ ) - tracer = trace.tracer_source().get_tracer(__name__, __version__) + tracer = trace.get_tracer(__name__, __version__) attributes = otel_wsgi.collect_request_attributes(environ) if flask_request.url_rule: diff --git a/ext/opentelemetry-ext-jaeger/README.rst b/ext/opentelemetry-ext-jaeger/README.rst index 80306aa59b..00339cb37f 100644 --- a/ext/opentelemetry-ext-jaeger/README.rst +++ b/ext/opentelemetry-ext-jaeger/README.rst @@ -36,7 +36,7 @@ gRPC is still not supported by this implementation. from opentelemetry.sdk.trace.export import BatchExportSpanProcessor trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) - tracer = trace.tracer_source().get_tracer(__name__) + tracer = trace.get_tracer(__name__) # create a JaegerSpanExporter jaeger_exporter = jaeger.JaegerSpanExporter( diff --git a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py index 9eec28dc75..6b0646bb99 100644 --- a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py +++ b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py @@ -6,7 +6,7 @@ from opentelemetry.sdk.trace.export import BatchExportSpanProcessor trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) -tracer = trace.tracer_source().get_tracer(__name__) +tracer = trace.get_tracer(__name__) # create a JaegerSpanExporter jaeger_exporter = jaeger.JaegerSpanExporter( diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index b7753754db..11ef52ec79 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -36,7 +36,7 @@ trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) # Create an OpenTelemetry Tracer. - otel_tracer = trace.tracer_source().get_tracer(__name__) + otel_tracer = trace.get_tracer(__name__) # Create an OpenTracing shim. shim = create_tracer(otel_tracer) diff --git a/ext/opentelemetry-ext-psycopg2/README.rst b/ext/opentelemetry-ext-psycopg2/README.rst index d7599492ac..9399c80fac 100644 --- a/ext/opentelemetry-ext-psycopg2/README.rst +++ b/ext/opentelemetry-ext-psycopg2/README.rst @@ -16,7 +16,7 @@ Usage from opentelemetry.trace.ext.psycopg2 import trace_integration trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) - tracer = trace.tracer_source().get_tracer(__name__) + tracer = trace.get_tracer(__name__) trace_integration(tracer) cnx = psycopg2.connect(database='Database') cursor = cnx.cursor() diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index e6751f34ce..37a3a0e9e0 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -162,7 +162,7 @@ class OpenTelemetryMiddleware: def __init__(self, wsgi): self.wsgi = wsgi - self.tracer = trace.tracer_source().get_tracer(__name__, __version__) + self.tracer = trace.get_tracer(__name__, __version__) @staticmethod def _create_start_response(span, start_response): diff --git a/ext/opentelemetry-ext-zipkin/README.rst b/ext/opentelemetry-ext-zipkin/README.rst index f91d0c2c6a..57dd4b7faa 100644 --- a/ext/opentelemetry-ext-zipkin/README.rst +++ b/ext/opentelemetry-ext-zipkin/README.rst @@ -34,7 +34,7 @@ This exporter always send traces to the configured Zipkin collector using HTTP. from opentelemetry.sdk.trace.export import BatchExportSpanProcessor trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) - tracer = trace.tracer_source().get_tracer(__name__) + tracer = trace.get_tracer(__name__) # create a ZipkinSpanExporter zipkin_exporter = zipkin.ZipkinSpanExporter( diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 014e82b3bc..f0b1fda1b3 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -36,7 +36,7 @@ from opentelemetry import trace - tracer = trace.tracer_source().get_tracer(__name__) + tracer = trace.get_tracer(__name__) # Create a new root span, set it as the current span in context with tracer.start_as_current_span("parent"): @@ -68,6 +68,7 @@ import abc import enum +import logging import types as python_types import typing from contextlib import contextmanager @@ -75,6 +76,8 @@ from opentelemetry.trace.status import Status from opentelemetry.util import loader, types +logger = logging.getLogger(__name__) + # TODO: quarantine ParentSpan = typing.Optional[typing.Union["Span", "SpanContext"]] @@ -647,6 +650,19 @@ def use_span( _TRACER_SOURCE_FACTORY = None # type: typing.Optional[ImplementationFactory] +def get_tracer( + instrumenting_module_name: str, instrumenting_library_version: str = "" +) -> "Tracer": + """Returns a `Tracer` for use by the given instrumentation library. + + This function is a convenience wrapper for + opentelemetry.trace.tracer_source().get_tracer + """ + return tracer_source().get_tracer( + instrumenting_module_name, instrumenting_library_version + ) + + def tracer_source() -> TracerSource: """Gets the current global :class:`~.TracerSource` object. @@ -663,6 +679,10 @@ def tracer_source() -> TracerSource: except TypeError: # if we raised an exception trying to instantiate an # abstract class, default to no-op tracer impl + logger.warning( + "Unable to instantiate TracerSource from tracer source factory.", + exc_info=True, + ) _TRACER_SOURCE = DefaultTracerSource() del _TRACER_SOURCE_FACTORY diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py new file mode 100644 index 0000000000..2ad74fb2ab --- /dev/null +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -0,0 +1,41 @@ +import importlib +import unittest + +from opentelemetry import trace + + +class TestGlobals(unittest.TestCase): + def setUp(self): + importlib.reload(trace) + + # this class has to be declared after the importlib + # reload, or else it will inherit from an old + # TracerSource, rather than the new TraceSource ABC. + # created from reload. + + static_tracer = trace.DefaultTracer() + + class DummyTracerSource(trace.TracerSource): + """TraceSource used for testing""" + + def get_tracer( + self, + instrumenting_module_name: str, + instrumenting_library_version: str = "", + ) -> trace.Tracer: + # pylint:disable=no-self-use,unused-argument + return static_tracer + + trace.set_preferred_tracer_source_implementation( + lambda _: DummyTracerSource() + ) + + @staticmethod + def tearDown() -> None: + importlib.reload(trace) + + def test_get_tracer(self): + """trace.get_tracer should proxy to the global tracer source.""" + from_global_api = trace.get_tracer("foo") + from_tracer_api = trace.tracer_source().get_tracer("foo") + self.assertIs(from_global_api, from_tracer_api) From 18edbbe125f5fbdfe2b1fc38e1fb33b3add2a894 Mon Sep 17 00:00:00 2001 From: joshuahlang Date: Thu, 20 Feb 2020 09:25:04 -0800 Subject: [PATCH 0208/1517] Clean up tox.ini (#417) Remove redundant environments and separate environments into logical groupings for maintainability. --- tox.ini | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 5a69ca62b6..8d5fe1d9fe 100644 --- a/tox.ini +++ b/tox.ini @@ -2,10 +2,70 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-psycopg2,ext-pymongo,ext-zipkin,opentracing-shim} - pypy3-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim} - py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-psycopg2,ext-pymongo,ext-zipkin,opentracing-shim} - pypy3-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim} + + ; Environments are organized by individual package, allowing + ; for specifying supported Python versions per package. + ; opentelemetry-api + py3{4,5,6,7,8}-test-api + pypy3-test-api + + ; opentelemetry-sdk + py3{4,5,6,7,8}-test-sdk + pypy3-test-sdk + + ; opentelemetry-example-app + py3{4,5,6,7,8}-test-example-app + pypy3-test-example-app + + ; examples/basic_tracer + py3{4,5,6,7,8}-test-example-basic-tracer + pypy3-test-example-basic-tracer + + ; examples/http + py3{4,5,6,7,8}-test-example-http + pypy3-test-example-http + + ; opentelemetry-ext-dbapi + py3{4,5,6,7,8}-test-ext-dbapi + pypy3-test-ext-dbapi + + ; opentelemetry-ext-flask + py3{4,5,6,7,8}-test-ext-flask + pypy3-test-ext-flask + + ; opentelemetry-ext-http-requests + py3{4,5,6,7,8}-test-ext-http-requests + pypy3-test-ext-http-requests + + ; opentelemetry-ext-jaeger + py3{4,5,6,7,8}-test-ext-jaeger + pypy3-test-ext-jaeger + + ; opentelemetry-ext-mysql + py3{4,5,6,7,8}-test-ext-mysql + pypy3-test-ext-mysql + + ; opentelemetry-ext-psycopg2 + py3{4,5,6,7,8}-test-ext-psycopg2 + ; ext-psycopg2 intentionally excluded from pypy3 + + ; opentelemetry-ext-pymongo + py3{4,5,6,7,8}-test-ext-pymongo + pypy3-test-ext-pymongo + + ; opentelemetry-ext-wsgi + py3{4,5,6,7,8}-test-ext-wsgi + pypy3-test-ext-wsgi + + ; opentelemetry-ext-zipkin + py3{4,5,6,7,8}-test-ext-zipkin + pypy3-test-ext-zipkin + + ; opentelemetry-opentracing-shim + py3{4,5,6,7,8}-test-opentracing-shim + pypy3-test-opentracing-shim + + py3{4,5,6,7,8}-coverage ; Coverage is temporarily disabled for pypy3 due to the pytest bug. From f41f83de9888d5f84b25b7f73e9978557684a87d Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 20 Feb 2020 10:45:40 -0800 Subject: [PATCH 0209/1517] All named tracers now share the same context (#424) The conversation in open-telemetry/opentelemetry-specification#455 is specifying that the tracer is no longer responsible for handling the setting and getting of active spans. As such, named tracers would only be responsible for creating new spans, but not for setting the active one. This implies that there tracers do not have their own active spans. In addition, there is a benefit of having a single active span, as it vastly simplifies the process to modify the current span (no need to explicitly retrieve the tracer responsible for getting the span). --- .../opentelemetry/trace/propagation/__init__.py | 9 +-------- .../src/opentelemetry/sdk/trace/__init__.py | 14 +++++--------- opentelemetry-sdk/tests/trace/test_trace.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 67d8a76a53..881a74287a 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -16,11 +16,4 @@ from opentelemetry.trace import INVALID_SPAN_CONTEXT, Span, SpanContext _SPAN_CONTEXT_KEY = "extracted-span-context" -_SPAN_KEY = "current-span" - - -def get_span_key(tracer_source_id: Optional[str] = None) -> str: - key = _SPAN_KEY - if tracer_source_id is not None: - key = "{}-{}".format(key, tracer_source_id) - return key +SPAN_KEY = "current-span" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 6d249e6508..9a285a458d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -27,7 +27,7 @@ from opentelemetry.sdk import util from opentelemetry.sdk.util import BoundedDict, BoundedList from opentelemetry.trace import SpanContext, sampling -from opentelemetry.trace.propagation import get_span_key +from opentelemetry.trace.propagation import SPAN_KEY from opentelemetry.trace.status import Status, StatusCanonicalCode from opentelemetry.util import time_ns, types @@ -542,9 +542,7 @@ def use_span( """See `opentelemetry.trace.Tracer.use_span`.""" try: context_snapshot = context_api.get_current() - context_api.set_current( - context_api.set_value(self.source.key, span) - ) + context_api.set_current(context_api.set_value(SPAN_KEY, span)) try: yield span finally: @@ -577,9 +575,6 @@ def __init__( sampler: sampling.Sampler = trace_api.sampling.ALWAYS_ON, shutdown_on_exit: bool = True, ): - # TODO: How should multiple TracerSources behave? Should they get their own contexts? - # This could be done by adding `str(id(self))` to the slot name. - self.key = get_span_key(tracer_source_id=str(id(self))) self._active_span_processor = MultiSpanProcessor() self.sampler = sampler self._atexit_handler = None @@ -601,8 +596,9 @@ def get_tracer( ), ) - def get_current_span(self) -> Span: - return context_api.get_value(self.key) # type: ignore + @staticmethod + def get_current_span() -> Span: + return context_api.get_value(SPAN_KEY) # type: ignore def add_span_processor(self, span_processor: SpanProcessor) -> None: """Registers a new :class:`SpanProcessor` for this `TracerSource`. diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index a62f91a7e3..fa6ee3cf27 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -201,6 +201,22 @@ def test_span_processor_for_source(self): span2.span_processor, tracer_source._active_span_processor ) + def test_get_current_span_multiple_tracers(self): + """In the case where there are multiple tracers, + get_current_span will return the same active span + for both tracers. + """ + tracer_1 = new_tracer() + tracer_2 = new_tracer() + root = tracer_1.start_span("root") + with tracer_1.use_span(root, True): + self.assertIs(tracer_1.get_current_span(), root) + self.assertIs(tracer_2.get_current_span(), root) + + # outside of the loop, both should not reference a span. + self.assertIs(tracer_1.get_current_span(), None) + self.assertIs(tracer_2.get_current_span(), None) + def test_start_span_implicit(self): tracer = new_tracer() From 0a8ecd118b323259ff223a6d4e09d148530bb282 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 20 Feb 2020 15:12:37 -0800 Subject: [PATCH 0210/1517] Build wheels from source dists (#437) Co-authored-by: Yusuke Tsutsumi --- scripts/build.sh | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 97af69babc..c66e234af0 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -9,16 +9,29 @@ set -ev python3 -m pip install --upgrade pip setuptools wheel BASEDIR=$(dirname $(readlink -f $(dirname $0))) +DISTDIR=dist ( cd $BASEDIR - mkdir -p dist - rm -rf dist/* + mkdir -p $DISTDIR + rm -r $DISTDIR/* for d in opentelemetry-api/ opentelemetry-sdk/ ext/*/ ; do ( + echo "building $d" cd "$d" - python3 setup.py --verbose bdist_wheel --dist-dir "$BASEDIR/dist/" + # Some ext directories (such as docker tests) are not intended to be + # packaged. Verify the intent by looking for a setup.py. + if [ -f setup.py ]; then + python3 setup.py sdist --dist-dir "$BASEDIR/dist/" clean --all + fi ) done + # Build a wheel for each source distribution + ( + cd $DISTDIR + for x in *.tar.gz ; do + pip wheel --no-deps $x + done + ) ) From 26e0576dc29d518f36838bd4da435de0c52c396f Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Fri, 21 Feb 2020 11:31:04 -0800 Subject: [PATCH 0211/1517] Prometheus metric exporter (#378) prometheus-exporter: initial commit --- examples/metrics/prometheus.py | 55 ++++++ ext/opentelemetry-ext-prometheus/CHANGELOG.md | 4 + ext/opentelemetry-ext-prometheus/README.rst | 72 ++++++++ ext/opentelemetry-ext-prometheus/setup.cfg | 47 ++++++ ext/opentelemetry-ext-prometheus/setup.py | 26 +++ .../opentelemetry/ext/prometheus/__init__.py | 147 +++++++++++++++++ .../opentelemetry/ext/prometheus/version.py | 15 ++ .../tests/__init__.py | 13 ++ .../tests/test_prometheus_exporter.py | 156 ++++++++++++++++++ tox.ini | 7 + 10 files changed, 542 insertions(+) create mode 100644 examples/metrics/prometheus.py create mode 100644 ext/opentelemetry-ext-prometheus/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-prometheus/README.rst create mode 100644 ext/opentelemetry-ext-prometheus/setup.cfg create mode 100644 ext/opentelemetry-ext-prometheus/setup.py create mode 100644 ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py create mode 100644 ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py create mode 100644 ext/opentelemetry-ext-prometheus/tests/__init__.py create mode 100644 ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py diff --git a/examples/metrics/prometheus.py b/examples/metrics/prometheus.py new file mode 100644 index 0000000000..14f612c6a9 --- /dev/null +++ b/examples/metrics/prometheus.py @@ -0,0 +1,55 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This module serves as an example for a simple application using metrics +Examples show how to recording affects the collection of metrics to be exported +""" + +from prometheus_client import start_http_server + +from opentelemetry import metrics +from opentelemetry.ext.prometheus import PrometheusMetricsExporter +from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics.export.controller import PushController + +# Start Prometheus client +start_http_server(port=8000, addr="localhost") + +# Meter is responsible for creating and recording metrics +metrics.set_preferred_meter_implementation(lambda _: Meter()) +meter = metrics.meter() +# exporter to export metrics to Prometheus +prefix = "MyAppPrefix" +exporter = PrometheusMetricsExporter(prefix) +# controller collects metrics created from meter and exports it via the +# exporter every interval +controller = PushController(meter, exporter, 5) + +counter = meter.create_metric( + "requests", + "number of requests", + "requests", + int, + Counter, + ("environment",), +) + +# Labelsets are used to identify key-values that are associated with a specific +# metric that you want to record. These are useful for pre-aggregation and can +# be used to store custom dimensions pertaining to a metric +label_set = meter.get_label_set({"environment": "staging"}) + +counter.add(25, label_set) +input("Press any key to exit...") diff --git a/ext/opentelemetry-ext-prometheus/CHANGELOG.md b/ext/opentelemetry-ext-prometheus/CHANGELOG.md new file mode 100644 index 0000000000..617d979ab2 --- /dev/null +++ b/ext/opentelemetry-ext-prometheus/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## Unreleased + diff --git a/ext/opentelemetry-ext-prometheus/README.rst b/ext/opentelemetry-ext-prometheus/README.rst new file mode 100644 index 0000000000..2d968d6a7c --- /dev/null +++ b/ext/opentelemetry-ext-prometheus/README.rst @@ -0,0 +1,72 @@ +OpenTelemetry Prometheus Exporter +============================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-prometheus.svg + :target: https://pypi.org/project/opentelemetry-ext-prometheus/ + +This library allows to export metrics data to `Prometheus `_. + +Installation +------------ + +:: + + pip install opentelemetry-ext-prometheus + + +Usage +----- + +The **OpenTelemetry Prometheus Exporter** allows to export `OpenTelemetry`_ metrics to `Prometheus`_. + + +.. _Prometheus: https://prometheus.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + +.. code:: python + + from opentelemetry import metrics + from opentelemetry.ext.prometheus import PrometheusMetricsExporter + from opentelemetry.sdk.metrics import Counter, Meter + from opentelemetry.sdk.metrics.export.controller import PushController + from prometheus_client import start_http_server + + # Start Prometheus client + start_http_server(port=8000, addr="localhost") + + # Meter is responsible for creating and recording metrics + metrics.set_preferred_meter_implementation(lambda _: Meter()) + meter = metrics.meter() + # exporter to export metrics to Prometheus + prefix = "MyAppPrefix" + exporter = PrometheusMetricsExporter(prefix) + # controller collects metrics created from meter and exports it via the + # exporter every interval + controller = PushController(meter, exporter, 5) + + counter = meter.create_metric( + "requests", + "number of requests", + "requests", + int, + Counter, + ("environment",), + ) + + # Labelsets are used to identify key-values that are associated with a specific + # metric that you want to record. These are useful for pre-aggregation and can + # be used to store custom dimensions pertaining to a metric + label_set = meter.get_label_set({"environment": "staging"}) + + counter.add(25, label_set) + input("Press any key to exit...") + + + +References +---------- + +* `Prometheus `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/ext/opentelemetry-ext-prometheus/setup.cfg new file mode 100644 index 0000000000..f6bfd7c38b --- /dev/null +++ b/ext/opentelemetry-ext-prometheus/setup.cfg @@ -0,0 +1,47 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-prometheus +description = Prometheus Metric Exporter for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-prometheus +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + prometheus_client >= 0.5.0, < 1.0.0 + opentelemetry-api + opentelemetry-sdk + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-prometheus/setup.py b/ext/opentelemetry-ext-prometheus/setup.py new file mode 100644 index 0000000000..aa968af60d --- /dev/null +++ b/ext/opentelemetry-ext-prometheus/setup.py @@ -0,0 +1,26 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "prometheus", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py new file mode 100644 index 0000000000..5b4a17a556 --- /dev/null +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py @@ -0,0 +1,147 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Prometheus Metrics Exporter for OpenTelemetry.""" + +import collections +import logging +import re +from typing import Sequence + +from prometheus_client import start_http_server +from prometheus_client.core import ( + REGISTRY, + CollectorRegistry, + CounterMetricFamily, + GaugeMetricFamily, + UnknownMetricFamily, +) + +from opentelemetry.metrics import Counter, Gauge, Measure, Metric +from opentelemetry.sdk.metrics.export import ( + MetricRecord, + MetricsExporter, + MetricsExportResult, +) + +logger = logging.getLogger(__name__) + + +class PrometheusMetricsExporter(MetricsExporter): + """Prometheus metric exporter for OpenTelemetry. + + Args: + prefix: single-word application prefix relevant to the domain + the metric belongs to. + """ + + def __init__(self, prefix: str = ""): + self._collector = CustomCollector(prefix) + REGISTRY.register(self._collector) + + def export( + self, metric_records: Sequence[MetricRecord] + ) -> MetricsExportResult: + self._collector.add_metrics_data(metric_records) + return MetricsExportResult.SUCCESS + + def shutdown(self) -> None: + REGISTRY.unregister(self._collector) + + +class CustomCollector: + """ CustomCollector represents the Prometheus Collector object + https://github.com/prometheus/client_python#custom-collectors + """ + + def __init__(self, prefix: str = ""): + self._prefix = prefix + self._metrics_to_export = collections.deque() + self._non_letters_nor_digits_re = re.compile( + r"[^\w]", re.UNICODE | re.IGNORECASE + ) + + def add_metrics_data(self, metric_records: Sequence[MetricRecord]): + self._metrics_to_export.append(metric_records) + + def collect(self): + """Collect fetches the metrics from OpenTelemetry + and delivers them as Prometheus Metrics. + Collect is invoked every time a prometheus.Gatherer is run + for example when the HTTP endpoint is invoked by Prometheus. + """ + + while self._metrics_to_export: + for metric_record in self._metrics_to_export.popleft(): + prometheus_metric = self._translate_to_prometheus( + metric_record + ) + if prometheus_metric is not None: + yield prometheus_metric + + def _translate_to_prometheus(self, metric_record: MetricRecord): + prometheus_metric = None + label_values = [] + label_keys = [] + for label_tuple in metric_record.label_set.labels: + label_keys.append(self._sanitize(label_tuple[0])) + label_values.append(label_tuple[1]) + + metric_name = "" + if self._prefix != "": + metric_name = self._prefix + "_" + metric_name += self._sanitize(metric_record.metric.name) + + if isinstance(metric_record.metric, Counter): + prometheus_metric = CounterMetricFamily( + name=metric_name, + documentation=metric_record.metric.description, + labels=label_keys, + ) + prometheus_metric.add_metric( + labels=label_values, value=metric_record.aggregator.checkpoint + ) + + elif isinstance(metric_record.metric, Gauge): + prometheus_metric = GaugeMetricFamily( + name=metric_name, + documentation=metric_record.metric.description, + labels=label_keys, + ) + prometheus_metric.add_metric( + labels=label_values, value=metric_record.aggregator.checkpoint + ) + + # TODO: Add support for histograms when supported in OT + elif isinstance(metric_record.metric, Measure): + prometheus_metric = UnknownMetricFamily( + name=metric_name, + documentation=metric_record.metric.description, + labels=label_keys, + ) + prometheus_metric.add_metric( + labels=label_values, value=metric_record.aggregator.checkpoint + ) + + else: + logger.warning( + "Unsupported metric type. %s", type(metric_record.metric) + ) + return prometheus_metric + + def _sanitize(self, key): + """ sanitize the given metric name or label according to Prometheus rule. + Replace all characters other than [A-Za-z0-9_] with '_'. + """ + return self._non_letters_nor_digits_re.sub("_", key) diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py new file mode 100644 index 0000000000..6b39cd19b5 --- /dev/null +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py @@ -0,0 +1,15 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.4.dev0" diff --git a/ext/opentelemetry-ext-prometheus/tests/__init__.py b/ext/opentelemetry-ext-prometheus/tests/__init__.py new file mode 100644 index 0000000000..6ab2e961ec --- /dev/null +++ b/ext/opentelemetry-ext-prometheus/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py new file mode 100644 index 0000000000..94fea96c5b --- /dev/null +++ b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py @@ -0,0 +1,156 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from prometheus_client.core import CounterMetricFamily + +from opentelemetry.ext.prometheus import ( + CustomCollector, + PrometheusMetricsExporter, +) +from opentelemetry.sdk import metrics +from opentelemetry.sdk.metrics.export import MetricRecord, MetricsExportResult +from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator + + +class TestPrometheusMetricExporter(unittest.TestCase): + def setUp(self): + self._meter = metrics.Meter() + self._test_metric = self._meter.create_metric( + "testname", + "testdesc", + "unit", + int, + metrics.Counter, + ["environment"], + ) + kvp = {"environment": "staging"} + self._test_label_set = self._meter.get_label_set(kvp) + + self._mock_registry_register = mock.Mock() + self._registry_register_patch = mock.patch( + "prometheus_client.core.REGISTRY.register", + side_effect=self._mock_registry_register, + ) + + # pylint: disable=protected-access + def test_constructor(self): + """Test the constructor.""" + with self._registry_register_patch: + exporter = PrometheusMetricsExporter("testprefix") + self.assertEqual(exporter._collector._prefix, "testprefix") + self.assertTrue(self._mock_registry_register.called) + + def test_shutdown(self): + with mock.patch( + "prometheus_client.core.REGISTRY.unregister" + ) as registry_unregister_patch: + exporter = PrometheusMetricsExporter() + exporter.shutdown() + self.assertTrue(registry_unregister_patch.called) + + def test_export(self): + with self._registry_register_patch: + record = MetricRecord( + CounterAggregator(), self._test_label_set, self._test_metric + ) + exporter = PrometheusMetricsExporter() + result = exporter.export([record]) + # pylint: disable=protected-access + self.assertEqual(len(exporter._collector._metrics_to_export), 1) + self.assertIs(result, MetricsExportResult.SUCCESS) + + def test_counter_to_prometheus(self): + meter = metrics.Meter() + metric = meter.create_metric( + "test@name", + "testdesc", + "unit", + int, + metrics.Counter, + ["environment@", "os"], + ) + kvp = {"environment@": "staging", "os": "Windows"} + label_set = meter.get_label_set(kvp) + aggregator = CounterAggregator() + aggregator.update(123) + aggregator.take_checkpoint() + record = MetricRecord(aggregator, label_set, metric) + collector = CustomCollector("testprefix") + collector.add_metrics_data([record]) + + for prometheus_metric in collector.collect(): + self.assertEqual(type(prometheus_metric), CounterMetricFamily) + self.assertEqual(prometheus_metric.name, "testprefix_test_name") + self.assertEqual(prometheus_metric.documentation, "testdesc") + self.assertTrue(len(prometheus_metric.samples) == 1) + self.assertEqual(prometheus_metric.samples[0].value, 123) + self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) + self.assertEqual( + prometheus_metric.samples[0].labels["environment_"], "staging" + ) + self.assertEqual( + prometheus_metric.samples[0].labels["os"], "Windows" + ) + + # TODO: Add unit test once GaugeAggregator is available + # TODO: Add unit test once Measure Aggregators are available + + def test_invalid_metric(self): + + meter = metrics.Meter() + metric = meter.create_metric( + "tesname", "testdesc", "unit", int, TestMetric + ) + kvp = {"environment": "staging"} + label_set = meter.get_label_set(kvp) + record = MetricRecord(None, label_set, metric) + collector = CustomCollector("testprefix") + collector.add_metrics_data([record]) + collector.collect() + self.assertLogs("opentelemetry.ext.prometheus", level="WARNING") + + def test_sanitize(self): + collector = CustomCollector("testprefix") + self.assertEqual( + collector._sanitize("1!2@3#4$5%6^7&8*9(0)_-"), + "1_2_3_4_5_6_7_8_9_0___", + ) + self.assertEqual(collector._sanitize(",./?;:[]{}"), "__________") + self.assertEqual(collector._sanitize("TestString"), "TestString") + self.assertEqual(collector._sanitize("aAbBcC_12_oi"), "aAbBcC_12_oi") + + +class TestMetric(metrics.Metric): + def __init__( + self, + name: str, + description: str, + unit: str, + value_type, + meter, + label_keys, + enabled: bool = True, + ): + super().__init__( + name, + description, + unit, + value_type, + meter, + label_keys=label_keys, + enabled=enabled, + ) diff --git a/tox.ini b/tox.ini index 8d5fe1d9fe..be7f1db9f7 100644 --- a/tox.ini +++ b/tox.ini @@ -44,6 +44,10 @@ envlist = ; opentelemetry-ext-mysql py3{4,5,6,7,8}-test-ext-mysql pypy3-test-ext-mysql + + ; opentelemetry-ext-prometheus + py3{4,5,6,7,8}-test-ext-prometheus + pypy3-test-ext-prometheus ; opentelemetry-ext-psycopg2 py3{4,5,6,7,8}-test-ext-psycopg2 @@ -99,6 +103,7 @@ changedir = test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests test-ext-mysql: ext/opentelemetry-ext-mysql/tests + test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests @@ -135,6 +140,8 @@ commands_pre = dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql + prometheus: pip install {toxinidir}/opentelemetry-sdk + prometheus: pip install {toxinidir}/ext/opentelemetry-ext-prometheus pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-psycopg2 From 8e79c3f56a25944632ea213f1c841a5504c06a7e Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Fri, 21 Feb 2020 13:24:02 -0800 Subject: [PATCH 0212/1517] Adding changelogs and readme updates for 0.4 (#438) Adding changelogs and readme updates for 0.4 Co-Authored-By: Chris Kleinknecht --- README.md | 46 +++++++++++++++---- ext/opentelemetry-ext-dbapi/CHANGELOG.md | 9 ++++ ext/opentelemetry-ext-flask/CHANGELOG.md | 6 +++ ext/opentelemetry-ext-jaeger/CHANGELOG.md | 4 ++ ext/opentelemetry-ext-mysql/CHANGELOG.md | 9 ++++ ext/opentelemetry-ext-prometheus/CHANGELOG.md | 6 +++ ext/opentelemetry-ext-psycopg2/CHANGELOG.md | 9 ++++ ext/opentelemetry-ext-pymongo/CHANGELOG.md | 8 ++++ ext/opentelemetry-ext-wsgi/CHANGELOG.md | 7 +++ ext/opentelemetry-ext-zipkin/CHANGELOG.md | 5 ++ opentelemetry-api/CHANGELOG.md | 24 ++++++++++ opentelemetry-sdk/CHANGELOG.md | 37 +++++++++++++++ 12 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 ext/opentelemetry-ext-dbapi/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-mysql/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-psycopg2/CHANGELOG.md diff --git a/README.md b/README.md index b7cda2ddb0..17641dd570 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,34 @@ includes: - Flask Integration - PyMongo Integration +The v0.4 alpha release includes: + +- Metrics MinMaxSumCount Aggregator +- Context API +- Full Metrics SDK Pipeline +- Metrics STDOUT Exporter +- Dbapi2 Integration +- MySQL Integration +- Psycopg2 Integration +- Zipkin Exporter +- Prometheus Metrics Exporter +- New Examples and Improvements to Existing Examples + +Thank you to the following individuals for contributing to this release: + +* Alex Boten +* Chris Kleinknecht +* Christian Neumüller +* Daniel González +* Diego Hurtado +* Golovin Pavel +* Hector Hernandez +* Jake Malachowski +* Joshua H Lang +* Leighton Chen +* Mauricio Vásquez +* Yusuke Tsutsumi + See the [project milestones](https://github.com/open-telemetry/opentelemetry-python/milestones) for details on upcoming releases. The dates and features described here are @@ -178,13 +206,11 @@ estimates, and subject to change. Future releases targets include: -| Component | Version | Target Date | -| ----------------------------------- | ---------- | ----------------- | -| Zipkin Trace Exporter | Alpha v0.4 | February 21 2020 | -| W3C Correlation Context Propagation | Alpha v0.4 | February 21 2020 | -| Support for Tags/Baggage | Alpha v0.4 | February 21 2020 | -| Metrics Aggregation | Alpha v0.4 | February 21 2020 | -| gRPC Integrations | Alpha v0.4 | February 21 2020 | -| Prometheus Metrics Exporter | Alpha v0.4 | February 21 2020 | -| OpenCensus Bridge | Alpha v0.4 | February 21 2020 | -| Metrics SDK (Complete) | Alpha v0.4 | February 21 2020 | +| Component | Version | Target Date | +| ----------------------------------- | ---------- | ------------ | +| W3C Correlation Context Propagation | Beta v1 | March 16 2020| +| Support for Tags/Baggage | Beta v1 | March 16 2020| +| gRPC Integrations | Beta v1 | March 16 2020| +| OpenTelemetry Collector Exporter | Beta v1 | March 16 2020| +| OpenCensus Bridge | Beta v1 | March 16 2020| +| Metrics SDK (Complete) | Beta v1 | March 16 2020| diff --git a/ext/opentelemetry-ext-dbapi/CHANGELOG.md b/ext/opentelemetry-ext-dbapi/CHANGELOG.md new file mode 100644 index 0000000000..f32ad5bd4c --- /dev/null +++ b/ext/opentelemetry-ext-dbapi/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## Unreleased + +## 0.4a0 + +Released 2020-02-21 + +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-flask/CHANGELOG.md b/ext/opentelemetry-ext-flask/CHANGELOG.md index d4df57eefc..d3bf663073 100644 --- a/ext/opentelemetry-ext-flask/CHANGELOG.md +++ b/ext/opentelemetry-ext-flask/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +## 0.4a0 + +- Use string keys for WSGI environ values + ([#366](https://github.com/open-telemetry/opentelemetry-python/pull/366)) + + ## 0.3a0 Released 2019-12-11 diff --git a/ext/opentelemetry-ext-jaeger/CHANGELOG.md b/ext/opentelemetry-ext-jaeger/CHANGELOG.md index 6cdbefa823..2b360199c0 100644 --- a/ext/opentelemetry-ext-jaeger/CHANGELOG.md +++ b/ext/opentelemetry-ext-jaeger/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.4a0 + +Released 2020-02-21 + - Export span status ([#367](https://github.com/open-telemetry/opentelemetry-python/pull/367)) - Export span kind ([#387](https://github.com/open-telemetry/opentelemetry-python/pull/387)) diff --git a/ext/opentelemetry-ext-mysql/CHANGELOG.md b/ext/opentelemetry-ext-mysql/CHANGELOG.md new file mode 100644 index 0000000000..f32ad5bd4c --- /dev/null +++ b/ext/opentelemetry-ext-mysql/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## Unreleased + +## 0.4a0 + +Released 2020-02-21 + +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-prometheus/CHANGELOG.md b/ext/opentelemetry-ext-prometheus/CHANGELOG.md index 617d979ab2..91730a080a 100644 --- a/ext/opentelemetry-ext-prometheus/CHANGELOG.md +++ b/ext/opentelemetry-ext-prometheus/CHANGELOG.md @@ -2,3 +2,9 @@ ## Unreleased +## 0.4a0 + +Released 2020-02-21 + +- Initial release + diff --git a/ext/opentelemetry-ext-psycopg2/CHANGELOG.md b/ext/opentelemetry-ext-psycopg2/CHANGELOG.md new file mode 100644 index 0000000000..f32ad5bd4c --- /dev/null +++ b/ext/opentelemetry-ext-psycopg2/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## Unreleased + +## 0.4a0 + +Released 2020-02-21 + +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-pymongo/CHANGELOG.md b/ext/opentelemetry-ext-pymongo/CHANGELOG.md index d4df57eefc..c13f1be4a2 100644 --- a/ext/opentelemetry-ext-pymongo/CHANGELOG.md +++ b/ext/opentelemetry-ext-pymongo/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +## 0.4a0 + +Released 2020-02-21 + +- Updating network connection attribute names + ([#350](https://github.com/open-telemetry/opentelemetry-python/pull/350)) + + ## 0.3a0 Released 2019-12-11 diff --git a/ext/opentelemetry-ext-wsgi/CHANGELOG.md b/ext/opentelemetry-ext-wsgi/CHANGELOG.md index 62d8a5baf0..a82a5de7c8 100644 --- a/ext/opentelemetry-ext-wsgi/CHANGELOG.md +++ b/ext/opentelemetry-ext-wsgi/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +## 0.4a0 + +Released 2020-02-21 + +- Updating network connection attribute names + ([#350](https://github.com/open-telemetry/opentelemetry-python/pull/350)) + ## 0.3a0 Released 2019-12-11 diff --git a/ext/opentelemetry-ext-zipkin/CHANGELOG.md b/ext/opentelemetry-ext-zipkin/CHANGELOG.md index 617d979ab2..f32ad5bd4c 100644 --- a/ext/opentelemetry-ext-zipkin/CHANGELOG.md +++ b/ext/opentelemetry-ext-zipkin/CHANGELOG.md @@ -2,3 +2,8 @@ ## Unreleased +## 0.4a0 + +Released 2020-02-21 + +- Initial release \ No newline at end of file diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index dc5ccc3ace..9864fc67d3 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,30 @@ ## Unreleased +## 0.4a0 + +Released 2020-02-21 + +- Separate Default classes from interface descriptions + ([#311](https://github.com/open-telemetry/opentelemetry-python/pull/311)) +- Added named Tracers + ([#301](https://github.com/open-telemetry/opentelemetry-python/pull/301)) +- Add int and valid sequenced to AttributeValue type + ([#368](https://github.com/open-telemetry/opentelemetry-python/pull/368)) +- Add ABC for Metric + ([#391](https://github.com/open-telemetry/opentelemetry-python/pull/391)) +- Metric classes required for export pipeline + ([#341](https://github.com/open-telemetry/opentelemetry-python/pull/341)) +- Adding Context API Implementation + ([#395](https://github.com/open-telemetry/opentelemetry-python/pull/395)) +- Remove monotonic and absolute metric instruments + ([#410](https://github.com/open-telemetry/opentelemetry-python/pull/410)) +- Adding trace.get_tracer function + ([#430](https://github.com/open-telemetry/opentelemetry-python/pull/430)) + + + + ## 0.3a0 Released 2019-12-11 diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index d981045858..54502d7eae 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,43 @@ ## Unreleased +## 0.4a0 + +Released 2020-02-21 + +- Added named Tracers + ([#301](https://github.com/open-telemetry/opentelemetry-python/pull/301)) +- Set status for ended spans + ([#297](https://github.com/open-telemetry/opentelemetry-python/pull/297) and + [#358](https://github.com/open-telemetry/opentelemetry-python/pull/358)) +- Use module loggers + ([#351](https://github.com/open-telemetry/opentelemetry-python/pull/351)) +- Protect start_time and end_time from being set manually by the user + ([#363](https://github.com/open-telemetry/opentelemetry-python/pull/363)) +- Add runtime validation for set_attribute + ([#348](https://github.com/open-telemetry/opentelemetry-python/pull/348)) +- Add support for B3 ParentSpanID + ([#286](https://github.com/open-telemetry/opentelemetry-python/pull/286)) +- Set status in start_as_current_span + ([#377](https://github.com/open-telemetry/opentelemetry-python/pull/377)) +- Implement force_flush for span processors + ([#389](https://github.com/open-telemetry/opentelemetry-python/pull/389)) +- Metrics export pipeline, and stdout exporter + ([#341](https://github.com/open-telemetry/opentelemetry-python/pull/389)) +- Set sampled flag on sampling trace + ([#407](https://github.com/open-telemetry/opentelemetry-python/pull/407)) +- Add io and formatter options to console exporter + ([#412](https://github.com/open-telemetry/opentelemetry-python/pull/412)) +- Clean up ProbabilitySample for 64 bit trace IDs + ([#238](https://github.com/open-telemetry/opentelemetry-python/pull/238)) +- Adding Context API Implementation + ([#395](https://github.com/open-telemetry/opentelemetry-python/pull/395)) +- Remove monotonic and absolute metric instruments + ([#410](https://github.com/open-telemetry/opentelemetry-python/pull/410)) +- Implement MinMaxSumCount aggregator + ([#422](https://github.com/open-telemetry/opentelemetry-python/pull/422)) + + ## 0.3a0 Released 2019-12-11 From 52fcd2f1ecba104ef97710a80997ce34af901e41 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Fri, 21 Feb 2020 14:54:53 -0800 Subject: [PATCH 0213/1517] Version bump for 0.5.dev0 on master (#443) --- examples/opentelemetry-example-app/setup.py | 2 +- ext/opentelemetry-ext-dbapi/setup.cfg | 2 +- .../src/opentelemetry/ext/dbapi/version.py | 2 +- .../src/opentelemetry/ext/flask/version.py | 2 +- ext/opentelemetry-ext-http-requests/setup.cfg | 2 +- .../src/opentelemetry/ext/http_requests/version.py | 2 +- .../src/opentelemetry/ext/jaeger/version.py | 2 +- ext/opentelemetry-ext-mysql/setup.cfg | 2 +- .../src/opentelemetry/ext/mysql/version.py | 2 +- .../src/opentelemetry/ext/opentracing_shim/version.py | 2 +- .../src/opentelemetry/ext/prometheus/version.py | 2 +- ext/opentelemetry-ext-psycopg2/setup.cfg | 2 +- .../src/opentelemetry/ext/psycopg2/version.py | 2 +- ext/opentelemetry-ext-pymongo/setup.cfg | 2 +- .../src/opentelemetry/ext/pymongo/version.py | 2 +- .../src/opentelemetry/ext/testutil/version.py | 2 +- .../src/opentelemetry/ext/wsgi/version.py | 2 +- .../src/opentelemetry/ext/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/util/version.py | 2 +- opentelemetry-sdk/setup.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/examples/opentelemetry-example-app/setup.py b/examples/opentelemetry-example-app/setup.py index ae614aee33..637ad084d8 100644 --- a/examples/opentelemetry-example-app/setup.py +++ b/examples/opentelemetry-example-app/setup.py @@ -16,7 +16,7 @@ setuptools.setup( name="opentelemetry-example-app", - version="0.4.dev0", + version="0.5.dev0", author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index f0de68dc26..3826d80893 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.4.dev0 + opentelemetry-api >= 0.5.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg index bb3f50c480..a064eb3c62 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.4.dev0 + opentelemetry-api >= 0.5.dev0 requests ~= 2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index 39a7c8a016..fdf63e3792 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index fdc608bb3d..b29636fa13 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.4.dev0 + opentelemetry-api >= 0.5.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py index 6b39cd19b5..f48cb5bee5 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index f26c5918eb..a4a67e2019 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.4.dev0 + opentelemetry-api >= 0.5.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py index 6b39cd19b5..f48cb5bee5 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index ecc0ce3b77..0f6a06ea7f 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.4.dev0 + opentelemetry-api >= 0.5.dev0 pymongo ~= 3.1 [options.packages.find] diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py index 116884f355..9aea0d23ea 100644 --- a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py +++ b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py @@ -1 +1 @@ -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py index 93ef792d05..d13bf96748 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.dev0" +__version__ = "0.5.dev0" diff --git a/opentelemetry-api/src/opentelemetry/util/version.py b/opentelemetry-api/src/opentelemetry/util/version.py index 2f792fff80..d13bf96748 100644 --- a/opentelemetry-api/src/opentelemetry/util/version.py +++ b/opentelemetry-api/src/opentelemetry/util/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index cbfb0f075d..50fe925605 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -44,7 +44,7 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["opentelemetry-api==0.4.dev0"], + install_requires=["opentelemetry-api==0.5.dev0"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 2f792fff80..d13bf96748 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" From ed25287fff574396800be8bb977b90068ad6a175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 21 Feb 2020 18:22:03 -0500 Subject: [PATCH 0214/1517] Rename TracerSource to TracerProvider (#441) Following discussion in #434, align the name with the specification. Co-authored-by: Chris Kleinknecht --- README.md | 6 +- examples/basic_tracer/tracer.py | 6 +- examples/http/server.py | 8 +-- examples/http/tracer_client.py | 10 +-- .../flask_example.py | 8 ++- examples/opentracing/main.py | 10 +-- ext/opentelemetry-ext-dbapi/README.rst | 6 +- .../tests/pymongo/test_pymongo_functional.py | 8 +-- .../README.rst | 4 +- .../ext/http_requests/__init__.py | 4 +- .../tests/test_requests_integration.py | 6 +- ext/opentelemetry-ext-jaeger/README.rst | 4 +- .../examples/jaeger_exporter_example.py | 6 +- ext/opentelemetry-ext-mysql/README.rst | 4 +- .../ext/opentracing_shim/__init__.py | 12 ++-- .../tests/test_shim.py | 8 +-- ext/opentelemetry-ext-psycopg2/README.rst | 4 +- ext/opentelemetry-ext-pymongo/README.rst | 4 +- .../ext/testutil/wsgitestutil.py | 10 +-- ext/opentelemetry-ext-zipkin/README.rst | 6 +- .../src/opentelemetry/trace/__init__.py | 60 ++++++++--------- .../src/opentelemetry/util/loader.py | 8 +-- opentelemetry-api/tests/mypysmoke.py | 4 +- .../tests/test_implementation.py | 6 +- opentelemetry-api/tests/test_loader.py | 48 +++++++------- opentelemetry-api/tests/trace/test_globals.py | 19 +++--- .../src/opentelemetry/sdk/trace/__init__.py | 12 ++-- .../tests/context/test_asyncio.py | 6 +- .../tests/test_implementation.py | 2 +- .../tests/trace/export/test_export.py | 12 ++-- .../export/test_in_memory_span_exporter.py | 6 +- opentelemetry-sdk/tests/trace/test_trace.py | 64 +++++++++---------- tests/w3c_tracecontext_validation_server.py | 8 +-- 33 files changed, 196 insertions(+), 193 deletions(-) diff --git a/README.md b/README.md index 17641dd570..6f5b9f213e 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,12 @@ pip install -e ./ext/opentelemetry-ext-{integration} ```python from opentelemetry import trace -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) -trace.tracer_source().add_span_processor( +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) +trace.tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) tracer = trace.get_tracer(__name__) diff --git a/examples/basic_tracer/tracer.py b/examples/basic_tracer/tracer.py index a6b33a01b3..bfb50a9890 100755 --- a/examples/basic_tracer/tracer.py +++ b/examples/basic_tracer/tracer.py @@ -17,7 +17,7 @@ import os from opentelemetry import trace -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, ConsoleSpanExporter, @@ -36,7 +36,7 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) # We tell OpenTelemetry who it is that is creating spans. In this case, we have # no real name (no setup.py), so we make one up. If we had a version, we would @@ -46,7 +46,7 @@ # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) -trace.tracer_source().add_span_processor(span_processor) +trace.tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): diff --git a/examples/http/server.py b/examples/http/server.py index 8d1aea1e06..50bc566b77 100755 --- a/examples/http/server.py +++ b/examples/http/server.py @@ -22,7 +22,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests from opentelemetry.ext.wsgi import OpenTelemetryMiddleware -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, ConsoleSpanExporter, @@ -41,17 +41,17 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) -trace.tracer_source().add_span_processor(span_processor) +trace.tracer_provider().add_span_processor(span_processor) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(trace.tracer_source()) +http_requests.enable(trace.tracer_provider()) app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) diff --git a/examples/http/tracer_client.py b/examples/http/tracer_client.py index 746608db3b..6fd0a726a4 100755 --- a/examples/http/tracer_client.py +++ b/examples/http/tracer_client.py @@ -20,7 +20,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, ConsoleSpanExporter, @@ -39,15 +39,15 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) -tracer_source = trace.tracer_source() +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) +tracer_provider = trace.tracer_provider() # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) -tracer_source.add_span_processor(span_processor) +tracer_provider.add_span_processor(span_processor) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(tracer_source) +http_requests.enable(tracer_provider) response = requests.get(url="http://127.0.0.1:5000/") diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 62795751d3..a33b3b58f4 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -23,7 +23,7 @@ import opentelemetry.ext.http_requests from opentelemetry import trace from opentelemetry.ext.flask import instrument_app -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider def configure_opentelemetry(flask_app: flask.Flask): @@ -45,7 +45,9 @@ def configure_opentelemetry(flask_app: flask.Flask): # The preferred implementation of these objects must be set, # as the opentelemetry-api defines the interface with a no-op # implementation. - trace.set_preferred_tracer_source_implementation(lambda _: TracerSource()) + trace.set_preferred_tracer_provider_implementation( + lambda _: TracerProvider() + ) # Next, we need to configure how the values that are used by # traces and metrics are propagated (such as what specific headers @@ -53,7 +55,7 @@ def configure_opentelemetry(flask_app: flask.Flask): # Integrations are the glue that binds the OpenTelemetry API # and the frameworks and libraries that are used together, automatically # creating Spans and propagating context as appropriate. - opentelemetry.ext.http_requests.enable(trace.tracer_source()) + opentelemetry.ext.http_requests.enable(trace.tracer_provider()) instrument_app(flask_app) diff --git a/examples/opentracing/main.py b/examples/opentracing/main.py index 922e1263b5..665099aeee 100755 --- a/examples/opentracing/main.py +++ b/examples/opentracing/main.py @@ -3,13 +3,13 @@ from opentelemetry import trace from opentelemetry.ext import opentracing_shim from opentelemetry.ext.jaeger import JaegerSpanExporter -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor from rediscache import RedisCache # Configure the tracer using the default implementation -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) -tracer_source = trace.tracer_source() +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) +tracer_provider = trace.tracer_provider() # Configure the tracer to export traces to Jaeger jaeger_exporter = JaegerSpanExporter( @@ -18,11 +18,11 @@ agent_port=6831, ) span_processor = SimpleExportSpanProcessor(jaeger_exporter) -tracer_source.add_span_processor(span_processor) +tracer_provider.add_span_processor(span_processor) # Create an OpenTracing shim. This implements the OpenTracing tracer API, but # forwards calls to the underlying OpenTelemetry tracer. -opentracing_tracer = opentracing_shim.create_tracer(tracer_source) +opentracing_tracer = opentracing_shim.create_tracer(tracer_provider) # Our example caching library expects an OpenTracing-compliant tracer. redis_cache = RedisCache(opentracing_tracer) diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst index b0bdbdd312..a5ad301399 100644 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -11,13 +11,13 @@ Usage .. code:: python import mysql.connector - from opentelemetry.trace import tracer_source + from opentelemetry.trace import tracer_provider from opentelemetry.ext.dbapi import trace_integration - trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # Ex: mysql.connector - trace_integration(tracer_source(), mysql.connector, "connect", "mysql") + trace_integration(tracer_provider(), mysql.connector, "connect", "mysql") References diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py index 4ef14fd789..c728aebf38 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py @@ -20,7 +20,7 @@ from opentelemetry import trace as trace_api from opentelemetry.ext.pymongo import trace_integration -from opentelemetry.sdk.trace import Span, Tracer, TracerSource +from opentelemetry.sdk.trace import Span, Tracer, TracerProvider from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, @@ -35,11 +35,11 @@ class TestFunctionalPymongo(unittest.TestCase): @classmethod def setUpClass(cls): - cls._tracer_source = TracerSource() - cls._tracer = Tracer(cls._tracer_source, None) + cls._tracer_provider = TracerProvider() + cls._tracer = Tracer(cls._tracer_provider, None) cls._span_exporter = InMemorySpanExporter() cls._span_processor = SimpleExportSpanProcessor(cls._span_exporter) - cls._tracer_source.add_span_processor(cls._span_processor) + cls._tracer_provider.add_span_processor(cls._span_processor) trace_integration(cls._tracer) client = MongoClient( MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000 diff --git a/ext/opentelemetry-ext-http-requests/README.rst b/ext/opentelemetry-ext-http-requests/README.rst index 7b2d434023..a4b79005b5 100644 --- a/ext/opentelemetry-ext-http-requests/README.rst +++ b/ext/opentelemetry-ext-http-requests/README.rst @@ -22,9 +22,9 @@ Usage import requests import opentelemetry.ext.http_requests - from opentelemetry.trace import tracer_source + from opentelemetry.trace import tracer_provider - opentelemetry.ext.http_requests.enable(tracer_source()) + opentelemetry.ext.http_requests.enable(tracer_provider()) response = requests.get(url='https://www.example.org/') Limitations diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index a557e6fc45..d21ca8258c 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -32,7 +32,7 @@ # if the SDK/tracer is already using `requests` they may, in theory, bypass our # instrumentation when using `import from`, etc. (currently we only instrument # a instance method so the probability for that is very low). -def enable(tracer_source): +def enable(tracer_provider): """Enables tracing of all requests calls that go through :code:`requests.session.Session.request` (this includes :code:`requests.get`, etc.).""" @@ -47,7 +47,7 @@ def enable(tracer_source): # Guard against double instrumentation disable() - tracer = tracer_source.get_tracer(__name__, __version__) + tracer = tracer_provider.get_tracer(__name__, __version__) wrapped = Session.request diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index de659f20e1..0a61016c77 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -29,10 +29,10 @@ class TestRequestsIntegration(unittest.TestCase): # TODO: Copy & paste from test_wsgi_middleware def setUp(self): self.span_attrs = {} - self.tracer_source = trace.DefaultTracerSource() + self.tracer_provider = trace.DefaultTracerProvider() self.tracer = trace.DefaultTracer() self.get_tracer_patcher = mock.patch.object( - self.tracer_source, + self.tracer_provider, "get_tracer", autospec=True, spec_set=True, @@ -70,7 +70,7 @@ def setspanattr(key, value): self.start_as_current_span = self.start_span_patcher.start() self.send = self.send_patcher.start() - opentelemetry.ext.http_requests.enable(self.tracer_source) + opentelemetry.ext.http_requests.enable(self.tracer_provider) distver = pkg_resources.get_distribution( "opentelemetry-ext-http-requests" ).version diff --git a/ext/opentelemetry-ext-jaeger/README.rst b/ext/opentelemetry-ext-jaeger/README.rst index 00339cb37f..04f7c4082b 100644 --- a/ext/opentelemetry-ext-jaeger/README.rst +++ b/ext/opentelemetry-ext-jaeger/README.rst @@ -32,10 +32,10 @@ gRPC is still not supported by this implementation. from opentelemetry import trace from opentelemetry.ext import jaeger - from opentelemetry.sdk.trace import TracerSource + from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # create a JaegerSpanExporter diff --git a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py index 6b0646bb99..81815da935 100644 --- a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py +++ b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py @@ -2,10 +2,10 @@ from opentelemetry import trace from opentelemetry.ext import jaeger -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # create a JaegerSpanExporter @@ -26,7 +26,7 @@ span_processor = BatchExportSpanProcessor(jaeger_exporter) # add to the tracer factory -trace.tracer_source().add_span_processor(span_processor) +trace.tracer_provider().add_span_processor(span_processor) # create some spans for testing with tracer.start_as_current_span("foo") as foo: diff --git a/ext/opentelemetry-ext-mysql/README.rst b/ext/opentelemetry-ext-mysql/README.rst index e899a980fc..a0f268c0d3 100644 --- a/ext/opentelemetry-ext-mysql/README.rst +++ b/ext/opentelemetry-ext-mysql/README.rst @@ -12,10 +12,10 @@ Usage .. code:: python import mysql.connector - from opentelemetry.trace import tracer_source + from opentelemetry.trace import tracer_provider from opentelemetry.ext.mysql import trace_integration - trace_integration(tracer_source()) + trace_integration(tracer_provider()) cnx = mysql.connector.connect(database='MySQL_Database') cursor = cnx.cursor() cursor.execute("INSERT INTO test (testField) VALUES (123)" diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index 11ef52ec79..1ba196d9e0 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -29,11 +29,11 @@ import time from opentelemetry import trace - from opentelemetry.sdk.trace import TracerSource + from opentelemetry.sdk.trace import TracerProvider from opentelemetry.ext.opentracing_shim import create_tracer # Tell OpenTelemetry which Tracer implementation to use. - trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) # Create an OpenTelemetry Tracer. otel_tracer = trace.get_tracer(__name__) @@ -97,15 +97,15 @@ logger = logging.getLogger(__name__) -def create_tracer(otel_tracer_source): +def create_tracer(otel_tracer_provider): """Creates a :class:`TracerShim` object from the provided OpenTelemetry - :class:`opentelemetry.trace.TracerSource`. + :class:`opentelemetry.trace.TracerProvider`. The returned :class:`TracerShim` is an implementation of :class:`opentracing.Tracer` using OpenTelemetry under the hood. Args: - otel_tracer_source: A :class:`opentelemetry.trace.TracerSource` to be + otel_tracer_provider: A :class:`opentelemetry.trace.TracerProvider` to be used for constructing the :class:`TracerShim`. A tracer from this source will be used to perform the actual tracing when user code is instrumented using the OpenTracing API. @@ -114,7 +114,7 @@ def create_tracer(otel_tracer_source): The created :class:`TracerShim`. """ - return TracerShim(otel_tracer_source.get_tracer(__name__, __version__)) + return TracerShim(otel_tracer_provider.get_tracer(__name__, __version__)) class SpanContextShim(opentracing.SpanContext): diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index eacfc639b3..0d099340ec 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -24,7 +24,7 @@ from opentelemetry import propagators, trace from opentelemetry.context.propagation.httptextformat import HTTPTextFormat from opentelemetry.ext.opentracing_shim import util -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider class TestShim(TestCase): @@ -33,7 +33,7 @@ class TestShim(TestCase): def setUp(self): """Create an OpenTelemetry tracer and a shim before every test case.""" - self.shim = opentracingshim.create_tracer(trace.tracer_source()) + self.shim = opentracingshim.create_tracer(trace.tracer_provider()) @classmethod def setUpClass(cls): @@ -41,8 +41,8 @@ def setUpClass(cls): every test method. """ - trace.set_preferred_tracer_source_implementation( - lambda T: TracerSource() + trace.set_preferred_tracer_provider_implementation( + lambda T: TracerProvider() ) # Save current propagator to be restored on teardown. diff --git a/ext/opentelemetry-ext-psycopg2/README.rst b/ext/opentelemetry-ext-psycopg2/README.rst index 9399c80fac..baa874954c 100644 --- a/ext/opentelemetry-ext-psycopg2/README.rst +++ b/ext/opentelemetry-ext-psycopg2/README.rst @@ -12,10 +12,10 @@ Usage .. code:: python import psycopg2 from opentelemetry import trace - from opentelemetry.sdk.trace import TracerSource + from opentelemetry.sdk.trace import TracerProvider from opentelemetry.trace.ext.psycopg2 import trace_integration - trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) trace_integration(tracer) cnx = psycopg2.connect(database='Database') diff --git a/ext/opentelemetry-ext-pymongo/README.rst b/ext/opentelemetry-ext-pymongo/README.rst index e8a42084be..a4ecd9c904 100644 --- a/ext/opentelemetry-ext-pymongo/README.rst +++ b/ext/opentelemetry-ext-pymongo/README.rst @@ -12,10 +12,10 @@ Usage .. code:: python from pymongo import MongoClient - from opentelemetry.trace import tracer_source + from opentelemetry.trace import tracer_provider from opentelemetry.trace.ext.pymongo import trace_integration - trace_integration(tracer_source()) + trace_integration(tracer_provider()) client = MongoClient() db = client["MongoDB_Database"] collection = db["MongoDB_Collection"] diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py index 5f99d08df0..18b64364db 100644 --- a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py +++ b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py @@ -4,7 +4,7 @@ from importlib import reload from opentelemetry import trace as trace_api -from opentelemetry.sdk.trace import TracerSource, export +from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) @@ -16,13 +16,13 @@ class WsgiTestBase(unittest.TestCase): @classmethod def setUpClass(cls): global _MEMORY_EXPORTER # pylint:disable=global-statement - trace_api.set_preferred_tracer_source_implementation( - lambda T: TracerSource() + trace_api.set_preferred_tracer_provider_implementation( + lambda T: TracerProvider() ) - tracer_source = trace_api.tracer_source() + tracer_provider = trace_api.tracer_provider() _MEMORY_EXPORTER = InMemorySpanExporter() span_processor = export.SimpleExportSpanProcessor(_MEMORY_EXPORTER) - tracer_source.add_span_processor(span_processor) + tracer_provider.add_span_processor(span_processor) @classmethod def tearDownClass(cls): diff --git a/ext/opentelemetry-ext-zipkin/README.rst b/ext/opentelemetry-ext-zipkin/README.rst index 57dd4b7faa..f933ba4a68 100644 --- a/ext/opentelemetry-ext-zipkin/README.rst +++ b/ext/opentelemetry-ext-zipkin/README.rst @@ -30,10 +30,10 @@ This exporter always send traces to the configured Zipkin collector using HTTP. from opentelemetry import trace from opentelemetry.ext import zipkin - from opentelemetry.sdk.trace import TracerSource + from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # create a ZipkinSpanExporter @@ -53,7 +53,7 @@ This exporter always send traces to the configured Zipkin collector using HTTP. span_processor = BatchExportSpanProcessor(zipkin_exporter) # add to the tracer - trace.tracer_source().add_span_processor(span_processor) + trace.tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): print("Hello world!") diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index f0b1fda1b3..b141e466aa 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -26,7 +26,7 @@ to use the API package alone without a supporting implementation. To get a tracer, you need to provide the package name from which you are -calling the tracer APIs to OpenTelemetry by calling `TracerSource.get_tracer` +calling the tracer APIs to OpenTelemetry by calling `TracerProvider.get_tracer` with the calling module name and the version of your package. The tracer supports creating spans that are "attached" or "detached" from the @@ -57,13 +57,13 @@ finally: child.end() -Applications should generally use a single global tracer source, and use either -implicit or explicit context propagation consistently throughout. +Applications should generally use a single global TracerProvider, and use +either implicit or explicit context propagation consistently throughout. .. versionadded:: 0.1.0 .. versionchanged:: 0.3.0 - `TracerSource` was introduced and the global ``tracer`` getter was replaced - by `tracer_source`. + `TracerProvider` was introduced and the global ``tracer`` getter was replaced + by `tracer_provider`. """ import abc @@ -405,7 +405,7 @@ def set_status(self, status: Status) -> None: INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) -class TracerSource(abc.ABC): +class TracerProvider(abc.ABC): @abc.abstractmethod def get_tracer( self, @@ -435,8 +435,8 @@ def get_tracer( """ -class DefaultTracerSource(TracerSource): - """The default TracerSource, used when no implementation is available. +class DefaultTracerProvider(TracerProvider): + """The default TracerProvider, used when no implementation is available. All operations are no-op. """ @@ -643,11 +643,11 @@ def use_span( # the following type definition should be replaced with # from opentelemetry.util.loader import ImplementationFactory ImplementationFactory = typing.Callable[ - [typing.Type[TracerSource]], typing.Optional[TracerSource] + [typing.Type[TracerProvider]], typing.Optional[TracerProvider] ] -_TRACER_SOURCE = None # type: typing.Optional[TracerSource] -_TRACER_SOURCE_FACTORY = None # type: typing.Optional[ImplementationFactory] +_TRACER_PROVIDER = None # type: typing.Optional[TracerProvider] +_TRACER_PROVIDER_FACTORY = None # type: typing.Optional[ImplementationFactory] def get_tracer( @@ -656,55 +656,55 @@ def get_tracer( """Returns a `Tracer` for use by the given instrumentation library. This function is a convenience wrapper for - opentelemetry.trace.tracer_source().get_tracer + opentelemetry.trace.tracer_provider().get_tracer """ - return tracer_source().get_tracer( + return tracer_provider().get_tracer( instrumenting_module_name, instrumenting_library_version ) -def tracer_source() -> TracerSource: - """Gets the current global :class:`~.TracerSource` object. +def tracer_provider() -> TracerProvider: + """Gets the current global :class:`~.TracerProvider` object. If there isn't one set yet, a default will be loaded. """ - global _TRACER_SOURCE, _TRACER_SOURCE_FACTORY # pylint:disable=global-statement + global _TRACER_PROVIDER, _TRACER_PROVIDER_FACTORY # pylint:disable=global-statement - if _TRACER_SOURCE is None: + if _TRACER_PROVIDER is None: # pylint:disable=protected-access try: - _TRACER_SOURCE = loader._load_impl( - TracerSource, _TRACER_SOURCE_FACTORY # type: ignore + _TRACER_PROVIDER = loader._load_impl( + TracerProvider, _TRACER_PROVIDER_FACTORY # type: ignore ) except TypeError: # if we raised an exception trying to instantiate an # abstract class, default to no-op tracer impl logger.warning( - "Unable to instantiate TracerSource from tracer source factory.", + "Unable to instantiate TracerProvider from factory.", exc_info=True, ) - _TRACER_SOURCE = DefaultTracerSource() - del _TRACER_SOURCE_FACTORY + _TRACER_PROVIDER = DefaultTracerProvider() + del _TRACER_PROVIDER_FACTORY - return _TRACER_SOURCE + return _TRACER_PROVIDER -def set_preferred_tracer_source_implementation( +def set_preferred_tracer_provider_implementation( factory: ImplementationFactory, ) -> None: - """Set the factory to be used to create the tracer source. + """Set the factory to be used to create the global TracerProvider. See :mod:`opentelemetry.util.loader` for details. This function may not be called after a tracer is already loaded. Args: - factory: Callback that should create a new :class:`TracerSource` + factory: Callback that should create a new :class:`TracerProvider` instance. """ - global _TRACER_SOURCE_FACTORY # pylint:disable=global-statement + global _TRACER_PROVIDER_FACTORY # pylint:disable=global-statement - if _TRACER_SOURCE: - raise RuntimeError("TracerSource already loaded.") + if _TRACER_PROVIDER: + raise RuntimeError("TracerProvider already loaded.") - _TRACER_SOURCE_FACTORY = factory + _TRACER_PROVIDER_FACTORY = factory diff --git a/opentelemetry-api/src/opentelemetry/util/loader.py b/opentelemetry-api/src/opentelemetry/util/loader.py index b65c822ab9..eeda2b3e7f 100644 --- a/opentelemetry-api/src/opentelemetry/util/loader.py +++ b/opentelemetry-api/src/opentelemetry/util/loader.py @@ -16,7 +16,7 @@ """ The OpenTelemetry loader module is mainly used internally to load the implementation for global objects like -:func:`opentelemetry.trace.tracer_source`. +:func:`opentelemetry.trace.tracer_provider`. .. _loader-factory: @@ -28,7 +28,7 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: That function is called with e.g., the type of the global object it should create as an argument (e.g. the type object -:class:`opentelemetry.trace.TracerSource`) and should return an instance of that type +:class:`opentelemetry.trace.TracerProvider`) and should return an instance of that type (such that ``instanceof(my_factory_for_t(T), T)`` is true). Alternatively, it may return ``None`` to indicate that the no-op default should be used. @@ -37,14 +37,14 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: 1. If the environment variable :samp:`OPENTELEMETRY_PYTHON_IMPLEMENTATION_{getter-name}` (e.g., - ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_TRACERSOURCE``) is set to an + ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_TRACERPROVIDER``) is set to an nonempty value, an attempt is made to import a module with that name and use a factory function named ``get_opentelemetry_implementation`` in it. 2. Otherwise, the same is tried with the environment variable ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT``. 3. Otherwise, if a :samp:`set_preferred_{}_implementation` was called (e.g. - :func:`opentelemetry.trace.set_preferred_tracer_source_implementation`), + :func:`opentelemetry.trace.set_preferred_tracer_provider_implementation`), the callback set there is used (that is, the environment variables override the callback set in code). 4. Otherwise, if :func:`set_preferred_default_implementation` was called, diff --git a/opentelemetry-api/tests/mypysmoke.py b/opentelemetry-api/tests/mypysmoke.py index 3f652adca1..bbbda93ef2 100644 --- a/opentelemetry-api/tests/mypysmoke.py +++ b/opentelemetry-api/tests/mypysmoke.py @@ -15,5 +15,5 @@ import opentelemetry.trace -def dummy_check_mypy_returntype() -> opentelemetry.trace.TracerSource: - return opentelemetry.trace.tracer_source() +def dummy_check_mypy_returntype() -> opentelemetry.trace.TracerProvider: + return opentelemetry.trace.tracer_provider() diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index c7d1d453a1..d1e1a0913a 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -28,11 +28,11 @@ class TestAPIOnlyImplementation(unittest.TestCase): def test_tracer(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated - trace.TracerSource() # type:ignore + trace.TracerProvider() # type:ignore def test_default_tracer(self): - tracer_source = trace.DefaultTracerSource() - tracer = tracer_source.get_tracer(__name__) + tracer_provider = trace.DefaultTracerProvider() + tracer = tracer_provider.get_tracer(__name__) with tracer.start_span("test") as span: self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) self.assertEqual(span, trace.INVALID_SPAN) diff --git a/opentelemetry-api/tests/test_loader.py b/opentelemetry-api/tests/test_loader.py index eda241615f..76575df705 100644 --- a/opentelemetry-api/tests/test_loader.py +++ b/opentelemetry-api/tests/test_loader.py @@ -21,10 +21,10 @@ from opentelemetry import trace from opentelemetry.util import loader -DUMMY_TRACER_SOURCE = None +DUMMY_TRACER_PROVIDER = None -class DummyTracerSource(trace.TracerSource): +class DummyTracerProvider(trace.TracerProvider): def get_tracer( self, instrumenting_module_name: str, @@ -34,10 +34,10 @@ def get_tracer( def get_opentelemetry_implementation(type_): - global DUMMY_TRACER_SOURCE # pylint:disable=global-statement - assert type_ is trace.TracerSource - DUMMY_TRACER_SOURCE = DummyTracerSource() - return DUMMY_TRACER_SOURCE + global DUMMY_TRACER_PROVIDER # pylint:disable=global-statement + assert type_ is trace.TracerProvider + DUMMY_TRACER_PROVIDER = DummyTracerProvider() + return DUMMY_TRACER_PROVIDER # pylint:disable=redefined-outer-name,protected-access,unidiomatic-typecheck @@ -48,31 +48,31 @@ def setUp(self): reload(loader) reload(trace) - # Need to reload self, otherwise DummyTracerSource will have the wrong + # Need to reload self, otherwise DummyTracerProvider will have the wrong # base class after reloading `trace`. reload(sys.modules[__name__]) def test_get_default(self): - tracer_source = trace.tracer_source() - self.assertIs(type(tracer_source), trace.DefaultTracerSource) + tracer_provider = trace.tracer_provider() + self.assertIs(type(tracer_provider), trace.DefaultTracerProvider) def test_preferred_impl(self): - trace.set_preferred_tracer_source_implementation( + trace.set_preferred_tracer_provider_implementation( get_opentelemetry_implementation ) - tracer_source = trace.tracer_source() - self.assertIs(tracer_source, DUMMY_TRACER_SOURCE) + tracer_provider = trace.tracer_provider() + self.assertIs(tracer_provider, DUMMY_TRACER_PROVIDER) # NOTE: We use do_* + *_ methods because subtest wouldn't run setUp, # which we require here. def do_test_preferred_impl(self, setter: Callable[[Any], Any]) -> None: setter(get_opentelemetry_implementation) - tracer_source = trace.tracer_source() - self.assertIs(tracer_source, DUMMY_TRACER_SOURCE) + tracer_provider = trace.tracer_provider() + self.assertIs(tracer_provider, DUMMY_TRACER_PROVIDER) def test_preferred_impl_with_tracer(self): self.do_test_preferred_impl( - trace.set_preferred_tracer_source_implementation + trace.set_preferred_tracer_provider_implementation ) def test_preferred_impl_with_default(self): @@ -81,16 +81,16 @@ def test_preferred_impl_with_default(self): ) def test_try_set_again(self): - self.assertTrue(trace.tracer_source()) - # Try setting after the tracer_source has already been created: + self.assertTrue(trace.tracer_provider()) + # Try setting after the tracer_provider has already been created: with self.assertRaises(RuntimeError) as einfo: - trace.set_preferred_tracer_source_implementation( + trace.set_preferred_tracer_provider_implementation( get_opentelemetry_implementation ) self.assertIn("already loaded", str(einfo.exception)) def do_test_get_envvar(self, envvar_suffix: str) -> None: - global DUMMY_TRACER_SOURCE # pylint:disable=global-statement + global DUMMY_TRACER_PROVIDER # pylint:disable=global-statement # Test is not runnable with this! self.assertFalse(sys.flags.ignore_environment) @@ -98,15 +98,15 @@ def do_test_get_envvar(self, envvar_suffix: str) -> None: envname = "OPENTELEMETRY_PYTHON_IMPLEMENTATION_" + envvar_suffix os.environ[envname] = __name__ try: - tracer_source = trace.tracer_source() - self.assertIs(tracer_source, DUMMY_TRACER_SOURCE) + tracer_provider = trace.tracer_provider() + self.assertIs(tracer_provider, DUMMY_TRACER_PROVIDER) finally: - DUMMY_TRACER_SOURCE = None + DUMMY_TRACER_PROVIDER = None del os.environ[envname] - self.assertIs(type(tracer_source), DummyTracerSource) + self.assertIs(type(tracer_provider), DummyTracerProvider) def test_get_envvar_tracer(self): - return self.do_test_get_envvar("TRACERSOURCE") + return self.do_test_get_envvar("TRACERPROVIDER") def test_get_envvar_default(self): return self.do_test_get_envvar("DEFAULT") diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 2ad74fb2ab..7c4d8e3549 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -8,15 +8,14 @@ class TestGlobals(unittest.TestCase): def setUp(self): importlib.reload(trace) - # this class has to be declared after the importlib - # reload, or else it will inherit from an old - # TracerSource, rather than the new TraceSource ABC. - # created from reload. + # This class has to be declared after the importlib reload, or else it + # will inherit from an old TracerProvider, rather than the new + # TracerProvider ABC created from reload. static_tracer = trace.DefaultTracer() - class DummyTracerSource(trace.TracerSource): - """TraceSource used for testing""" + class DummyTracerProvider(trace.TracerProvider): + """TracerProvider used for testing""" def get_tracer( self, @@ -26,8 +25,8 @@ def get_tracer( # pylint:disable=no-self-use,unused-argument return static_tracer - trace.set_preferred_tracer_source_implementation( - lambda _: DummyTracerSource() + trace.set_preferred_tracer_provider_implementation( + lambda _: DummyTracerProvider() ) @staticmethod @@ -35,7 +34,7 @@ def tearDown() -> None: importlib.reload(trace) def test_get_tracer(self): - """trace.get_tracer should proxy to the global tracer source.""" + """trace.get_tracer should proxy to the global tracer provider.""" from_global_api = trace.get_tracer("foo") - from_tracer_api = trace.tracer_source().get_tracer("foo") + from_tracer_api = trace.tracer_provider().get_tracer("foo") self.assertIs(from_global_api, from_tracer_api) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 9a285a458d..7ce0ea3a83 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -43,7 +43,7 @@ class SpanProcessor: invocations. Span processors can be registered directly using - :func:`TracerSource.add_span_processor` and they are invoked + :func:`TracerProvider.add_span_processor` and they are invoked in the same order as they were registered. """ @@ -388,7 +388,7 @@ def generate_trace_id() -> int: class InstrumentationInfo: """Immutable information about an instrumentation library module. - See `TracerSource.get_tracer` for the meaning of the properties. + See `TracerProvider.get_tracer` for the meaning of the properties. """ __slots__ = ("_name", "_version") @@ -435,7 +435,9 @@ class Tracer(trace_api.Tracer): """ def __init__( - self, source: "TracerSource", instrumentation_info: InstrumentationInfo + self, + source: "TracerProvider", + instrumentation_info: InstrumentationInfo, ) -> None: self.source = source self.instrumentation_info = instrumentation_info @@ -569,7 +571,7 @@ def use_span( span.end() -class TracerSource(trace_api.TracerSource): +class TracerProvider(trace_api.TracerProvider): def __init__( self, sampler: sampling.Sampler = trace_api.sampling.ALWAYS_ON, @@ -601,7 +603,7 @@ def get_current_span() -> Span: return context_api.get_value(SPAN_KEY) # type: ignore def add_span_processor(self, span_processor: SpanProcessor) -> None: - """Registers a new :class:`SpanProcessor` for this `TracerSource`. + """Registers a new :class:`SpanProcessor` for this `TracerProvider`. The span processors are invoked in the same order they are registered. """ diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index e1cb90f452..22773a80cd 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -65,11 +65,11 @@ def submit_another_task(self, name): def setUp(self): self.previous_context = context.get_current() context.set_current(context.Context()) - self.tracer_source = trace.TracerSource() - self.tracer = self.tracer_source.get_tracer(__name__) + self.tracer_provider = trace.TracerProvider() + self.tracer = self.tracer_provider.get_tracer(__name__) self.memory_exporter = InMemorySpanExporter() span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) - self.tracer_source.add_span_processor(span_processor) + self.tracer_provider.add_span_processor(span_processor) self.loop = asyncio.get_event_loop() def tearDown(self): diff --git a/opentelemetry-sdk/tests/test_implementation.py b/opentelemetry-sdk/tests/test_implementation.py index d8d6bae139..3c0cd2ba20 100644 --- a/opentelemetry-sdk/tests/test_implementation.py +++ b/opentelemetry-sdk/tests/test_implementation.py @@ -28,7 +28,7 @@ class TestSDKImplementation(unittest.TestCase): """ def test_tracer(self): - tracer = trace.TracerSource().get_tracer(__name__) + tracer = trace.TracerProvider().get_tracer(__name__) with tracer.start_span("test") as span: self.assertNotEqual(span.get_context(), INVALID_SPAN_CONTEXT) self.assertNotEqual(span, INVALID_SPAN) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index e598b9680a..e1c709719a 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -52,14 +52,14 @@ def shutdown(self): class TestSimpleExportSpanProcessor(unittest.TestCase): def test_simple_span_processor(self): - tracer_source = trace.TracerSource() - tracer = tracer_source.get_tracer(__name__) + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) span_processor = export.SimpleExportSpanProcessor(my_exporter) - tracer_source.add_span_processor(span_processor) + tracer_provider.add_span_processor(span_processor) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): @@ -77,14 +77,14 @@ def test_simple_span_processor_no_context(self): SpanProcessors should act on a span's start and end events whether or not it is ever the active span. """ - tracer_source = trace.TracerSource() - tracer = tracer_source.get_tracer(__name__) + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) span_processor = export.SimpleExportSpanProcessor(my_exporter) - tracer_source.add_span_processor(span_processor) + tracer_provider.add_span_processor(span_processor) with tracer.start_span("foo"): with tracer.start_span("bar"): diff --git a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py index 5c5194053b..45b65fb372 100644 --- a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py +++ b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py @@ -25,11 +25,11 @@ class TestInMemorySpanExporter(unittest.TestCase): def setUp(self): - self.tracer_source = trace.TracerSource() - self.tracer = self.tracer_source.get_tracer(__name__) + self.tracer_provider = trace.TracerProvider() + self.tracer = self.tracer_provider.get_tracer(__name__) self.memory_exporter = InMemorySpanExporter() span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) - self.tracer_source.add_span_processor(span_processor) + self.tracer_provider.add_span_processor(span_processor) self.exec_scenario() def exec_scenario(self): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index fa6ee3cf27..982df89667 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -26,7 +26,7 @@ def new_tracer() -> trace_api.Tracer: - return trace.TracerSource().get_tracer(__name__) + return trace.TracerProvider().get_tracer(__name__) class TestTracer(unittest.TestCase): @@ -36,15 +36,15 @@ def test_extends_api(self): self.assertIsInstance(tracer, trace_api.Tracer) def test_shutdown(self): - tracer_source = trace.TracerSource() + tracer_provider = trace.TracerProvider() mock_processor1 = mock.Mock(spec=trace.SpanProcessor) - tracer_source.add_span_processor(mock_processor1) + tracer_provider.add_span_processor(mock_processor1) mock_processor2 = mock.Mock(spec=trace.SpanProcessor) - tracer_source.add_span_processor(mock_processor2) + tracer_provider.add_span_processor(mock_processor2) - tracer_source.shutdown() + tracer_provider.shutdown() self.assertEqual(mock_processor1.shutdown.call_count, 1) self.assertEqual(mock_processor2.shutdown.call_count, 1) @@ -64,8 +64,8 @@ def print_shutdown_count(): # creating the tracer atexit.register(print_shutdown_count) -tracer_source = trace.TracerSource({tracer_parameters}) -tracer_source.add_span_processor(mock_processor) +tracer_provider = trace.TracerProvider({tracer_parameters}) +tracer_provider.add_span_processor(mock_processor) {tracer_shutdown} """ @@ -78,7 +78,7 @@ def run_general_code(shutdown_on_exit, explicit_shutdown): tracer_parameters = "shutdown_on_exit=False" if explicit_shutdown: - tracer_shutdown = "tracer_source.shutdown()" + tracer_shutdown = "tracer_provider.shutdown()" return subprocess.check_output( [ @@ -120,8 +120,8 @@ def test_default_sampler(self): self.assertTrue(root_span.context.trace_options.sampled) def test_sampler_no_sampling(self): - tracer_source = trace.TracerSource(sampling.ALWAYS_OFF) - tracer = tracer_source.get_tracer(__name__) + tracer_provider = trace.TracerProvider(sampling.ALWAYS_OFF) + tracer = tracer_provider.get_tracer(__name__) # Check that the default tracer creates no-op spans if the sampler # decides not to sampler @@ -147,9 +147,9 @@ def test_start_span_invalid_spancontext(self): self.assertIsNone(new_span.parent) def test_instrumentation_info(self): - tracer_source = trace.TracerSource() - tracer1 = tracer_source.get_tracer("instr1") - tracer2 = tracer_source.get_tracer("instr2", "1.3b3") + tracer_provider = trace.TracerProvider() + tracer1 = tracer_provider.get_tracer("instr1") + tracer2 = tracer_provider.get_tracer("instr2", "1.3b3") span1 = tracer1.start_span("s1") span2 = tracer2.start_span("s2") self.assertEqual( @@ -168,11 +168,11 @@ def test_instrumentation_info(self): ) # Check sortability. def test_invalid_instrumentation_info(self): - tracer_source = trace.TracerSource() + tracer_provider = trace.TracerProvider() with self.assertLogs(level=ERROR): - tracer1 = tracer_source.get_tracer("") + tracer1 = tracer_provider.get_tracer("") with self.assertLogs(level=ERROR): - tracer2 = tracer_source.get_tracer(None) + tracer2 = tracer_provider.get_tracer(None) self.assertEqual( tracer1.instrumentation_info, tracer2.instrumentation_info ) @@ -187,18 +187,18 @@ def test_invalid_instrumentation_info(self): ) def test_span_processor_for_source(self): - tracer_source = trace.TracerSource() - tracer1 = tracer_source.get_tracer("instr1") - tracer2 = tracer_source.get_tracer("instr2", "1.3b3") + tracer_provider = trace.TracerProvider() + tracer1 = tracer_provider.get_tracer("instr1") + tracer2 = tracer_provider.get_tracer("instr2", "1.3b3") span1 = tracer1.start_span("s1") span2 = tracer2.start_span("s2") # pylint:disable=protected-access self.assertIs( - span1.span_processor, tracer_source._active_span_processor + span1.span_processor, tracer_provider._active_span_processor ) self.assertIs( - span2.span_processor, tracer_source._active_span_processor + span2.span_processor, tracer_provider._active_span_processor ) def test_get_current_span_multiple_tracers(self): @@ -472,13 +472,13 @@ def test_sampling_attributes(self): "sampler-attr": "sample-val", "attr-in-both": "decision-attr", } - tracer_source = trace.TracerSource( + tracer_provider = trace.TracerProvider( sampling.StaticSampler( sampling.Decision(sampled=True, attributes=decision_attributes) ) ) - self.tracer = tracer_source.get_tracer(__name__) + self.tracer = tracer_provider.get_tracer(__name__) with self.tracer.start_as_current_span("root2") as root: self.assertEqual(len(root.attributes), 2) @@ -673,10 +673,10 @@ def error_status_test(context): ) error_status_test( - trace.TracerSource().get_tracer(__name__).start_span("root") + trace.TracerProvider().get_tracer(__name__).start_span("root") ) error_status_test( - trace.TracerSource() + trace.TracerProvider() .get_tracer(__name__) .start_as_current_span("root") ) @@ -704,8 +704,8 @@ def on_end(self, span: "trace.Span") -> None: class TestSpanProcessor(unittest.TestCase): def test_span_processor(self): - tracer_source = trace.TracerSource() - tracer = tracer_source.get_tracer(__name__) + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand @@ -723,7 +723,7 @@ def test_span_processor(self): self.assertEqual(len(spans_calls_list), 0) # add single span processor - tracer_source.add_span_processor(sp1) + tracer_provider.add_span_processor(sp1) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) @@ -746,7 +746,7 @@ def test_span_processor(self): expected_list.clear() # go for multiple span processors - tracer_source.add_span_processor(sp2) + tracer_provider.add_span_processor(sp2) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) @@ -773,8 +773,8 @@ def test_span_processor(self): self.assertListEqual(spans_calls_list, expected_list) def test_add_span_processor_after_span_creation(self): - tracer_source = trace.TracerSource() - tracer = tracer_source.get_tracer(__name__) + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand @@ -786,7 +786,7 @@ def test_add_span_processor_after_span_creation(self): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): # add span processor after spans have been created - tracer_source.add_span_processor(sp) + tracer_provider.add_span_processor(sp) expected_list.append(span_event_end_fmt("SP1", "baz")) diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py index bea4d4fde5..4ec179c354 100644 --- a/tests/w3c_tracecontext_validation_server.py +++ b/tests/w3c_tracecontext_validation_server.py @@ -26,7 +26,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests from opentelemetry.ext.wsgi import OpenTelemetryMiddleware -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleExportSpanProcessor, @@ -34,16 +34,16 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(trace.tracer_source()) +http_requests.enable(trace.tracer_provider()) # SpanExporter receives the spans and send them to the target location. span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter()) -trace.tracer_source().add_span_processor(span_processor) +trace.tracer_provider().add_span_processor(span_processor) app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) From a4e7a9a800cec8047d367ee175dd0bfa288b03ea Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 24 Feb 2020 09:25:46 -0800 Subject: [PATCH 0215/1517] Fix new ext READMEs (#444) Some of the new ext packages had ReStructuredText errors. PyPI rejected the uploads for these packages with: HTTPError: 400 Client Error: The description failed to render for 'text/x-rst'. See https://pypi.org/help/#description-content-type for more information. for url: https://upload.pypi.org/legacy/ --- ext/opentelemetry-ext-dbapi/README.rst | 4 ++-- ext/opentelemetry-ext-mysql/README.rst | 4 ++-- ext/opentelemetry-ext-prometheus/README.rst | 2 +- ext/opentelemetry-ext-psycopg2/README.rst | 7 ++++--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst index a5ad301399..895b47b9ba 100644 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -1,5 +1,5 @@ OpenTelemetry Database API integration -================================= +====================================== The trace integration with Database API supports libraries following the specification. @@ -8,7 +8,7 @@ The trace integration with Database API supports libraries following the specifi Usage ----- -.. code:: python +.. code-block:: python import mysql.connector from opentelemetry.trace import tracer_provider diff --git a/ext/opentelemetry-ext-mysql/README.rst b/ext/opentelemetry-ext-mysql/README.rst index a0f268c0d3..087d4cb361 100644 --- a/ext/opentelemetry-ext-mysql/README.rst +++ b/ext/opentelemetry-ext-mysql/README.rst @@ -1,10 +1,10 @@ OpenTelemetry MySQL integration -================================= +=============================== The integration with MySQL supports the `mysql-connector`_ library and is specified to ``trace_integration`` using ``'MySQL'``. -.. mysql-connector: https://pypi.org/project/mysql-connector/ +.. _mysql-connector: https://pypi.org/project/mysql-connector/ Usage ----- diff --git a/ext/opentelemetry-ext-prometheus/README.rst b/ext/opentelemetry-ext-prometheus/README.rst index 2d968d6a7c..e70332556e 100644 --- a/ext/opentelemetry-ext-prometheus/README.rst +++ b/ext/opentelemetry-ext-prometheus/README.rst @@ -1,5 +1,5 @@ OpenTelemetry Prometheus Exporter -============================= +================================= |pypi| diff --git a/ext/opentelemetry-ext-psycopg2/README.rst b/ext/opentelemetry-ext-psycopg2/README.rst index baa874954c..127b74f0c7 100644 --- a/ext/opentelemetry-ext-psycopg2/README.rst +++ b/ext/opentelemetry-ext-psycopg2/README.rst @@ -4,12 +4,13 @@ OpenTelemetry Psycopg integration The integration with PostgreSQL supports the `Psycopg`_ library and is specified to ``trace_integration`` using ``'PostgreSQL'``. -.. Psycopg: http://initd.org/psycopg/ +.. _Psycopg: http://initd.org/psycopg/ Usage ----- -.. code:: python +.. code-block:: python + import psycopg2 from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider @@ -26,4 +27,4 @@ Usage References ---------- -* `OpenTelemetry Project `_ \ No newline at end of file +* `OpenTelemetry Project `_ From 5b2e6932f20cd2d8656e4cbc6b6b385fb8bcfb6d Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 26 Feb 2020 15:56:41 -0800 Subject: [PATCH 0216/1517] Adding attach/detach methods as per spec (#429) This change updates the Context API with the following: - removes the remove_value method - removes the set_current method - adds attach and detach methods Fixes #420 Co-authored-by: Chris Kleinknecht --- .../src/opentelemetry/context/__init__.py | 145 ++++++++++-------- .../src/opentelemetry/context/context.py | 13 +- .../context/contextvars_context.py | 10 +- .../context/threadlocal_context.py | 17 +- .../distributedcontext/__init__.py | 4 +- .../tests/context/base_context.py | 77 ++++++++++ .../tests/context/test_context.py | 11 +- .../tests/context/test_contextvars_context.py | 51 ++---- .../tests/context/test_threadlocal_context.py | 50 ++---- .../src/opentelemetry/sdk/trace/__init__.py | 5 +- .../sdk/trace/export/__init__.py | 12 +- .../tests/context/test_asyncio.py | 5 +- 12 files changed, 226 insertions(+), 174 deletions(-) create mode 100644 opentelemetry-api/tests/context/base_context.py diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 1d1b53e7cb..1ac837f51c 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -14,6 +14,7 @@ import logging import typing +from functools import wraps from os import environ from sys import version_info @@ -25,6 +26,47 @@ _RUNTIME_CONTEXT = None # type: typing.Optional[RuntimeContext] +_F = typing.TypeVar("_F", bound=typing.Callable[..., typing.Any]) + + +def _load_runtime_context(func: _F) -> _F: + """A decorator used to initialize the global RuntimeContext + + Returns: + A wrapper of the decorated method. + """ + + @wraps(func) # type: ignore + def wrapper( + *args: typing.Tuple[typing.Any, typing.Any], + **kwargs: typing.Dict[typing.Any, typing.Any] + ) -> typing.Optional[typing.Any]: + global _RUNTIME_CONTEXT # pylint: disable=global-statement + if _RUNTIME_CONTEXT is None: + # FIXME use a better implementation of a configuration manager to avoid having + # to get configuration values straight from environment variables + if version_info < (3, 5): + # contextvars are not supported in 3.4, use thread-local storage + default_context = "threadlocal_context" + else: + default_context = "contextvars_context" + + configured_context = environ.get( + "OPENTELEMETRY_CONTEXT", default_context + ) # type: str + try: + _RUNTIME_CONTEXT = next( + iter_entry_points( + "opentelemetry_context", configured_context + ) + ).load()() + except Exception: # pylint: disable=broad-except + logger.error("Failed to load context: %s", configured_context) + return func(*args, **kwargs) # type: ignore + + return wrapper # type:ignore + + def get_value(key: str, context: typing.Optional[Context] = None) -> "object": """To access the local state of a concern, the RuntimeContext API provides a function which takes a context and a key as input, @@ -33,6 +75,9 @@ def get_value(key: str, context: typing.Optional[Context] = None) -> "object": Args: key: The key of the value to retrieve. context: The context from which to retrieve the value, if None, the current context is used. + + Returns: + The value associated with the key. """ return context.get(key) if context is not None else get_current().get(key) @@ -46,91 +91,55 @@ def set_value( which contains the new value. Args: - key: The key of the entry to set - value: The value of the entry to set - context: The context to copy, if None, the current context is used - """ - if context is None: - context = get_current() - new_values = context.copy() - new_values[key] = value - return Context(new_values) + key: The key of the entry to set. + value: The value of the entry to set. + context: The context to copy, if None, the current context is used. - -def remove_value( - key: str, context: typing.Optional[Context] = None -) -> Context: - """To remove a value, this method returns a new context with the key - cleared. Note that the removed value still remains present in the old - context. - - Args: - key: The key of the entry to remove - context: The context to copy, if None, the current context is used + Returns: + A new `Context` containing the value set. """ if context is None: context = get_current() new_values = context.copy() - new_values.pop(key, None) + new_values[key] = value return Context(new_values) +@_load_runtime_context # type: ignore def get_current() -> Context: """To access the context associated with program execution, - the RuntimeContext API provides a function which takes no arguments - and returns a RuntimeContext. - """ - - global _RUNTIME_CONTEXT # pylint: disable=global-statement - if _RUNTIME_CONTEXT is None: - # FIXME use a better implementation of a configuration manager to avoid having - # to get configuration values straight from environment variables - if version_info < (3, 5): - # contextvars are not supported in 3.4, use thread-local storage - default_context = "threadlocal_context" - else: - default_context = "contextvars_context" - - configured_context = environ.get( - "OPENTELEMETRY_CONTEXT", default_context - ) # type: str - try: - _RUNTIME_CONTEXT = next( - iter_entry_points("opentelemetry_context", configured_context) - ).load()() - except Exception: # pylint: disable=broad-except - logger.error("Failed to load context: %s", configured_context) + the Context API provides a function which takes no arguments + and returns a Context. + Returns: + The current `Context` object. + """ return _RUNTIME_CONTEXT.get_current() # type:ignore -def set_current(context: Context) -> Context: - """To associate a context with program execution, the Context - API provides a function which takes a Context. +@_load_runtime_context # type: ignore +def attach(context: Context) -> object: + """Associates a Context with the caller's current execution unit. Returns + a token that can be used to restore the previous Context. Args: - context: The context to use as current. - """ - old_context = get_current() - _RUNTIME_CONTEXT.set_current(context) # type:ignore - return old_context - + context: The Context to set as current. -def with_current_context( - func: typing.Callable[..., "object"] -) -> typing.Callable[..., "object"]: - """Capture the current context and apply it to the provided func.""" + Returns: + A token that can be used with `detach` to reset the context. + """ + return _RUNTIME_CONTEXT.attach(context) # type:ignore - caller_context = get_current() - def call_with_current_context( - *args: "object", **kwargs: "object" - ) -> "object": - try: - backup = get_current() - set_current(caller_context) - return func(*args, **kwargs) - finally: - set_current(backup) +@_load_runtime_context # type: ignore +def detach(token: object) -> None: + """Resets the Context associated with the caller's current execution unit + to the value it had before attaching a specified Context. - return call_with_current_context + Args: + token: The Token that was returned by a previous call to attach a Context. + """ + try: + _RUNTIME_CONTEXT.detach(token) # type: ignore + except Exception: # pylint: disable=broad-except + logger.error("Failed to detach context") diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 148312a884..1c7cfba963 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -29,8 +29,9 @@ class RuntimeContext(ABC): """ @abstractmethod - def set_current(self, context: Context) -> None: - """ Sets the current `Context` object. + def attach(self, context: Context) -> object: + """ Sets the current `Context` object. Returns a + token that can be used to reset to the previous `Context`. Args: context: The Context to set. @@ -40,5 +41,13 @@ def set_current(self, context: Context) -> None: def get_current(self) -> Context: """ Returns the current `Context` object. """ + @abstractmethod + def detach(self, token: object) -> None: + """ Resets Context to a previous value + + Args: + token: A reference to a previous Context. + """ + __all__ = ["Context", "RuntimeContext"] diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index 1fd202275a..0d075e0776 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -35,13 +35,17 @@ def __init__(self) -> None: self._CONTEXT_KEY, default=Context() ) - def set_current(self, context: Context) -> None: - """See `opentelemetry.context.RuntimeContext.set_current`.""" - self._current_context.set(context) + def attach(self, context: Context) -> object: + """See `opentelemetry.context.RuntimeContext.attach`.""" + return self._current_context.set(context) def get_current(self) -> Context: """See `opentelemetry.context.RuntimeContext.get_current`.""" return self._current_context.get() + def detach(self, token: object) -> None: + """See `opentelemetry.context.RuntimeContext.detach`.""" + self._current_context.reset(token) # type: ignore + __all__ = ["ContextVarsRuntimeContext"] diff --git a/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py b/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py index 899ab86326..6a0e76bb69 100644 --- a/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py +++ b/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py @@ -23,14 +23,20 @@ class ThreadLocalRuntimeContext(RuntimeContext): implementation is available for usage with Python 3.4. """ + class Token: + def __init__(self, context: Context) -> None: + self._context = context + _CONTEXT_KEY = "current_context" def __init__(self) -> None: self._current_context = threading.local() - def set_current(self, context: Context) -> None: - """See `opentelemetry.context.RuntimeContext.set_current`.""" + def attach(self, context: Context) -> object: + """See `opentelemetry.context.RuntimeContext.attach`.""" + current = self.get_current() setattr(self._current_context, self._CONTEXT_KEY, context) + return self.Token(current) def get_current(self) -> Context: """See `opentelemetry.context.RuntimeContext.get_current`.""" @@ -43,5 +49,12 @@ def get_current(self) -> Context: ) # type: Context return context + def detach(self, token: object) -> None: + """See `opentelemetry.context.RuntimeContext.detach`.""" + if not isinstance(token, self.Token): + raise ValueError("invalid token") + # pylint: disable=protected-access + setattr(self._current_context, self._CONTEXT_KEY, token._context) + __all__ = ["ThreadLocalRuntimeContext"] diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index a89d982550..dbc7b7e79b 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -17,7 +17,7 @@ import typing from contextlib import contextmanager -from opentelemetry.context import get_value, set_current, set_value +from opentelemetry.context import attach, get_value, set_value from opentelemetry.context.context import Context PRINTABLE = frozenset( @@ -142,4 +142,4 @@ def distributed_context_from_context( def with_distributed_context( dctx: DistributedContext, context: typing.Optional[Context] = None ) -> None: - set_current(set_value(_DISTRIBUTED_CONTEXT_KEY, dctx, context=context)) + attach(set_value(_DISTRIBUTED_CONTEXT_KEY, dctx, context=context)) diff --git a/opentelemetry-api/tests/context/base_context.py b/opentelemetry-api/tests/context/base_context.py new file mode 100644 index 0000000000..66e6df97a2 --- /dev/null +++ b/opentelemetry-api/tests/context/base_context.py @@ -0,0 +1,77 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from logging import ERROR + +from opentelemetry import context + + +def do_work() -> None: + context.attach(context.set_value("say", "bar")) + + +class ContextTestCases: + class BaseTest(unittest.TestCase): + def setUp(self) -> None: + self.previous_context = context.get_current() + + def tearDown(self) -> None: + context.attach(self.previous_context) + + def test_context(self): + self.assertIsNone(context.get_value("say")) + empty = context.get_current() + second = context.set_value("say", "foo") + + self.assertEqual(context.get_value("say", context=second), "foo") + + do_work() + self.assertEqual(context.get_value("say"), "bar") + third = context.get_current() + + self.assertIsNone(context.get_value("say", context=empty)) + self.assertEqual(context.get_value("say", context=second), "foo") + self.assertEqual(context.get_value("say", context=third), "bar") + + def test_set_value(self): + first = context.set_value("a", "yyy") + second = context.set_value("a", "zzz") + third = context.set_value("a", "---", first) + self.assertEqual("yyy", context.get_value("a", context=first)) + self.assertEqual("zzz", context.get_value("a", context=second)) + self.assertEqual("---", context.get_value("a", context=third)) + self.assertEqual(None, context.get_value("a")) + + def test_attach(self): + context.attach(context.set_value("a", "yyy")) + + token = context.attach(context.set_value("a", "zzz")) + self.assertEqual("zzz", context.get_value("a")) + + context.detach(token) + self.assertEqual("yyy", context.get_value("a")) + + with self.assertLogs(level=ERROR): + context.detach("some garbage") + + def test_detach_out_of_order(self): + t1 = context.attach(context.set_value("c", 1)) + self.assertEqual(context.get_current(), {"c": 1}) + t2 = context.attach(context.set_value("c", 2)) + self.assertEqual(context.get_current(), {"c": 2}) + context.detach(t1) + self.assertEqual(context.get_current(), {}) + context.detach(t2) + self.assertEqual(context.get_current(), {"c": 1}) diff --git a/opentelemetry-api/tests/context/test_context.py b/opentelemetry-api/tests/context/test_context.py index 2536e5149b..8942a333ed 100644 --- a/opentelemetry-api/tests/context/test_context.py +++ b/opentelemetry-api/tests/context/test_context.py @@ -19,12 +19,12 @@ def do_work() -> None: - context.set_current(context.set_value("say", "bar")) + context.attach(context.set_value("say", "bar")) class TestContext(unittest.TestCase): def setUp(self): - context.set_current(Context()) + context.attach(Context()) def test_context(self): self.assertIsNone(context.get_value("say")) @@ -55,11 +55,10 @@ def test_context_is_immutable(self): context.get_current()["test"] = "cant-change-immutable" def test_set_current(self): - context.set_current(context.set_value("a", "yyy")) + context.attach(context.set_value("a", "yyy")) - old_context = context.set_current(context.set_value("a", "zzz")) - self.assertEqual("yyy", context.get_value("a", context=old_context)) + token = context.attach(context.set_value("a", "zzz")) self.assertEqual("zzz", context.get_value("a")) - context.set_current(old_context) + context.detach(token) self.assertEqual("yyy", context.get_value("a")) diff --git a/opentelemetry-api/tests/context/test_contextvars_context.py b/opentelemetry-api/tests/context/test_contextvars_context.py index ebc15d6d9a..d19ac5ca12 100644 --- a/opentelemetry-api/tests/context/test_contextvars_context.py +++ b/opentelemetry-api/tests/context/test_contextvars_context.py @@ -17,6 +17,8 @@ from opentelemetry import context +from .base_context import ContextTestCases + try: import contextvars # pylint: disable=unused-import from opentelemetry.context.contextvars_context import ( @@ -26,43 +28,14 @@ raise unittest.SkipTest("contextvars not available") -def do_work() -> None: - context.set_current(context.set_value("say", "bar")) - - -class TestContextVarsContext(unittest.TestCase): - def setUp(self): - self.previous_context = context.get_current() - - def tearDown(self): - context.set_current(self.previous_context) - - @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() # type: ignore - ) - def test_context(self): - self.assertIsNone(context.get_value("say")) - empty = context.get_current() - second = context.set_value("say", "foo") - - self.assertEqual(context.get_value("say", context=second), "foo") - - do_work() - self.assertEqual(context.get_value("say"), "bar") - third = context.get_current() +class TestContextVarsContext(ContextTestCases.BaseTest): + def setUp(self) -> None: + super(TestContextVarsContext, self).setUp() + self.mock_runtime = patch.object( + context, "_RUNTIME_CONTEXT", ContextVarsRuntimeContext(), + ) + self.mock_runtime.start() - self.assertIsNone(context.get_value("say", context=empty)) - self.assertEqual(context.get_value("say", context=second), "foo") - self.assertEqual(context.get_value("say", context=third), "bar") - - @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() # type: ignore - ) - def test_set_value(self): - first = context.set_value("a", "yyy") - second = context.set_value("a", "zzz") - third = context.set_value("a", "---", first) - self.assertEqual("yyy", context.get_value("a", context=first)) - self.assertEqual("zzz", context.get_value("a", context=second)) - self.assertEqual("---", context.get_value("a", context=third)) - self.assertEqual(None, context.get_value("a")) + def tearDown(self) -> None: + super(TestContextVarsContext, self).tearDown() + self.mock_runtime.stop() diff --git a/opentelemetry-api/tests/context/test_threadlocal_context.py b/opentelemetry-api/tests/context/test_threadlocal_context.py index aca6b69de7..342163020e 100644 --- a/opentelemetry-api/tests/context/test_threadlocal_context.py +++ b/opentelemetry-api/tests/context/test_threadlocal_context.py @@ -12,50 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from unittest.mock import patch from opentelemetry import context from opentelemetry.context.threadlocal_context import ThreadLocalRuntimeContext +from .base_context import ContextTestCases -def do_work() -> None: - context.set_current(context.set_value("say", "bar")) +class TestThreadLocalContext(ContextTestCases.BaseTest): + def setUp(self) -> None: + super(TestThreadLocalContext, self).setUp() + self.mock_runtime = patch.object( + context, "_RUNTIME_CONTEXT", ThreadLocalRuntimeContext(), + ) + self.mock_runtime.start() -class TestThreadLocalContext(unittest.TestCase): - def setUp(self): - self.previous_context = context.get_current() - - def tearDown(self): - context.set_current(self.previous_context) - - @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() # type: ignore - ) - def test_context(self): - self.assertIsNone(context.get_value("say")) - empty = context.get_current() - second = context.set_value("say", "foo") - - self.assertEqual(context.get_value("say", context=second), "foo") - - do_work() - self.assertEqual(context.get_value("say"), "bar") - third = context.get_current() - - self.assertIsNone(context.get_value("say", context=empty)) - self.assertEqual(context.get_value("say", context=second), "foo") - self.assertEqual(context.get_value("say", context=third), "bar") - - @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() # type: ignore - ) - def test_set_value(self): - first = context.set_value("a", "yyy") - second = context.set_value("a", "zzz") - third = context.set_value("a", "---", first) - self.assertEqual("yyy", context.get_value("a", context=first)) - self.assertEqual("zzz", context.get_value("a", context=second)) - self.assertEqual("---", context.get_value("a", context=third)) - self.assertEqual(None, context.get_value("a")) + def tearDown(self) -> None: + super(TestThreadLocalContext, self).tearDown() + self.mock_runtime.stop() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 7ce0ea3a83..dd0169ea9f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -543,12 +543,11 @@ def use_span( ) -> Iterator[trace_api.Span]: """See `opentelemetry.trace.Tracer.use_span`.""" try: - context_snapshot = context_api.get_current() - context_api.set_current(context_api.set_value(SPAN_KEY, span)) + token = context_api.attach(context_api.set_value(SPAN_KEY, span)) try: yield span finally: - context_api.set_current(context_snapshot) + context_api.detach(token) except Exception as error: # pylint: disable=broad-except if ( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 0a1b1c8041..0f96808ea8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -19,7 +19,7 @@ import typing from enum import Enum -from opentelemetry.context import get_current, set_current, set_value +from opentelemetry.context import attach, detach, get_current, set_value from opentelemetry.trace import DefaultSpan from opentelemetry.util import time_ns @@ -75,14 +75,13 @@ def on_start(self, span: Span) -> None: pass def on_end(self, span: Span) -> None: - backup_context = get_current() - set_current(set_value("suppress_instrumentation", True)) + token = attach(set_value("suppress_instrumentation", True)) try: self.span_exporter.export((span,)) # pylint: disable=broad-except except Exception: logger.exception("Exception while exporting Span.") - set_current(backup_context) + detach(token) def shutdown(self) -> None: self.span_exporter.shutdown() @@ -202,8 +201,7 @@ def export(self) -> None: else: self.spans_list[idx] = span idx += 1 - backup_context = get_current() - set_current(set_value("suppress_instrumentation", True)) + token = attach(set_value("suppress_instrumentation", True)) try: # Ignore type b/c the Optional[None]+slicing is too "clever" # for mypy @@ -211,7 +209,7 @@ def export(self) -> None: # pylint: disable=broad-except except Exception: logger.exception("Exception while exporting Span batch.") - set_current(backup_context) + detach(token) if notify_flush: with self.flush_condition: diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index 22773a80cd..ea7ebbddbf 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -63,8 +63,7 @@ def submit_another_task(self, name): self.loop.create_task(self.task(name)) def setUp(self): - self.previous_context = context.get_current() - context.set_current(context.Context()) + self.token = context.attach(context.Context()) self.tracer_provider = trace.TracerProvider() self.tracer = self.tracer_provider.get_tracer(__name__) self.memory_exporter = InMemorySpanExporter() @@ -73,7 +72,7 @@ def setUp(self): self.loop = asyncio.get_event_loop() def tearDown(self): - context.set_current(self.previous_context) + context.detach(self.token) @patch( "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() From 2578b37177afe1c9f19123acd400e9c06612da6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Thu, 27 Feb 2020 18:39:38 -0500 Subject: [PATCH 0217/1517] Make Counter and MinMaxSumCount aggregators thread safe (#439) --- .../sdk/metrics/export/aggregate.py | 77 ++++++----- .../tests/metrics/export/test_export.py | 128 ++++++++++++++++-- 2 files changed, 161 insertions(+), 44 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 5c55ba038a..f082cce891 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -13,6 +13,7 @@ # limitations under the License. import abc +import threading from collections import namedtuple @@ -47,62 +48,66 @@ def __init__(self): super().__init__() self.current = 0 self.checkpoint = 0 + self._lock = threading.Lock() def update(self, value): - self.current += value + with self._lock: + self.current += value def take_checkpoint(self): - self.checkpoint = self.current - self.current = 0 + with self._lock: + self.checkpoint = self.current + self.current = 0 def merge(self, other): - self.checkpoint += other.checkpoint + with self._lock: + self.checkpoint += other.checkpoint class MinMaxSumCountAggregator(Aggregator): """Agregator for Measure metrics that keeps min, max, sum and count.""" _TYPE = namedtuple("minmaxsumcount", "min max sum count") + _EMPTY = _TYPE(None, None, None, 0) @classmethod - def _min(cls, val1, val2): - if val1 is None and val2 is None: - return None - return min(val1 or val2, val2 or val1) - - @classmethod - def _max(cls, val1, val2): - if val1 is None and val2 is None: - return None - return max(val1 or val2, val2 or val1) - - @classmethod - def _sum(cls, val1, val2): - if val1 is None and val2 is None: - return None - return (val1 or 0) + (val2 or 0) + def _merge_checkpoint(cls, val1, val2): + if val1 is cls._EMPTY: + return val2 + if val2 is cls._EMPTY: + return val1 + return cls._TYPE( + min(val1.min, val2.min), + max(val1.max, val2.max), + val1.sum + val2.sum, + val1.count + val2.count, + ) def __init__(self): super().__init__() - self.current = self._TYPE(None, None, None, 0) - self.checkpoint = self._TYPE(None, None, None, 0) + self.current = self._EMPTY + self.checkpoint = self._EMPTY + self._lock = threading.Lock() def update(self, value): - self.current = self._TYPE( - self._min(self.current.min, value), - self._max(self.current.max, value), - self._sum(self.current.sum, value), - self.current.count + 1, - ) + with self._lock: + if self.current is self._EMPTY: + self.current = self._TYPE(value, value, value, 1) + else: + self.current = self._TYPE( + min(self.current.min, value), + max(self.current.max, value), + self.current.sum + value, + self.current.count + 1, + ) def take_checkpoint(self): - self.checkpoint = self.current - self.current = self._TYPE(None, None, None, 0) + with self._lock: + self.checkpoint = self.current + self.current = self._EMPTY def merge(self, other): - self.checkpoint = self._TYPE( - self._min(self.checkpoint.min, other.checkpoint.min), - self._max(self.checkpoint.max, other.checkpoint.max), - self._sum(self.checkpoint.sum, other.checkpoint.sum), - self.checkpoint.count + other.checkpoint.count, - ) + with self._lock: + self.checkpoint = self._merge_checkpoint( + self.checkpoint, other.checkpoint + ) diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 5df6c6d08a..51d7aaaf4f 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import concurrent.futures +import random import unittest from unittest import mock @@ -222,6 +224,15 @@ def test_ungrouped_batcher_process_not_stateful(self): class TestCounterAggregator(unittest.TestCase): + @staticmethod + def call_update(counter): + update_total = 0 + for _ in range(0, 100000): + val = random.getrandbits(32) + counter.update(val) + update_total += val + return update_total + def test_update(self): counter = CounterAggregator() counter.update(1.0) @@ -243,13 +254,58 @@ def test_merge(self): counter.merge(counter2) self.assertEqual(counter.checkpoint, 4.0) + def test_concurrent_update(self): + counter = CounterAggregator() + + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + fut1 = executor.submit(self.call_update, counter) + fut2 = executor.submit(self.call_update, counter) + + updapte_total = fut1.result() + fut2.result() + + counter.take_checkpoint() + self.assertEqual(updapte_total, counter.checkpoint) + + def test_concurrent_update_and_checkpoint(self): + counter = CounterAggregator() + checkpoint_total = 0 + + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + fut = executor.submit(self.call_update, counter) + + while not fut.done(): + counter.take_checkpoint() + checkpoint_total += counter.checkpoint + + counter.take_checkpoint() + checkpoint_total += counter.checkpoint + + self.assertEqual(fut.result(), checkpoint_total) + class TestMinMaxSumCountAggregator(unittest.TestCase): + @staticmethod + def call_update(mmsc): + min_ = float("inf") + max_ = float("-inf") + sum_ = 0 + count_ = 0 + for _ in range(0, 100000): + val = random.getrandbits(32) + mmsc.update(val) + if val < min_: + min_ = val + if val > max_: + max_ = val + sum_ += val + count_ += 1 + return MinMaxSumCountAggregator._TYPE(min_, max_, sum_, count_) + def test_update(self): mmsc = MinMaxSumCountAggregator() # test current values without any update self.assertEqual( - mmsc.current, (None, None, None, 0), + mmsc.current, MinMaxSumCountAggregator._EMPTY, ) # call update with some values @@ -267,7 +323,7 @@ def test_checkpoint(self): # take checkpoint wihtout any update mmsc.take_checkpoint() self.assertEqual( - mmsc.checkpoint, (None, None, None, 0), + mmsc.checkpoint, MinMaxSumCountAggregator._EMPTY, ) # call update with some values @@ -282,7 +338,7 @@ def test_checkpoint(self): ) self.assertEqual( - mmsc.current, (None, None, None, 0), + mmsc.current, MinMaxSumCountAggregator._EMPTY, ) def test_merge(self): @@ -299,14 +355,34 @@ def test_merge(self): self.assertEqual( mmsc1.checkpoint, - ( - min(checkpoint1.min, checkpoint2.min), - max(checkpoint1.max, checkpoint2.max), - checkpoint1.sum + checkpoint2.sum, - checkpoint1.count + checkpoint2.count, + MinMaxSumCountAggregator._merge_checkpoint( + checkpoint1, checkpoint2 ), ) + def test_merge_checkpoint(self): + func = MinMaxSumCountAggregator._merge_checkpoint + _type = MinMaxSumCountAggregator._TYPE + empty = MinMaxSumCountAggregator._EMPTY + + ret = func(empty, empty) + self.assertEqual(ret, empty) + + ret = func(empty, _type(0, 0, 0, 0)) + self.assertEqual(ret, _type(0, 0, 0, 0)) + + ret = func(_type(0, 0, 0, 0), empty) + self.assertEqual(ret, _type(0, 0, 0, 0)) + + ret = func(_type(0, 0, 0, 0), _type(0, 0, 0, 0)) + self.assertEqual(ret, _type(0, 0, 0, 0)) + + ret = func(_type(44, 23, 55, 86), empty) + self.assertEqual(ret, _type(44, 23, 55, 86)) + + ret = func(_type(3, 150, 101, 3), _type(1, 33, 44, 2)) + self.assertEqual(ret, _type(1, 150, 101 + 44, 2 + 3)) + def test_merge_with_empty(self): mmsc1 = MinMaxSumCountAggregator() mmsc2 = MinMaxSumCountAggregator() @@ -318,6 +394,42 @@ def test_merge_with_empty(self): self.assertEqual(mmsc1.checkpoint, checkpoint1) + def test_concurrent_update(self): + mmsc = MinMaxSumCountAggregator() + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as ex: + fut1 = ex.submit(self.call_update, mmsc) + fut2 = ex.submit(self.call_update, mmsc) + + ret1 = fut1.result() + ret2 = fut2.result() + + update_total = MinMaxSumCountAggregator._merge_checkpoint( + ret1, ret2 + ) + mmsc.take_checkpoint() + + self.assertEqual(update_total, mmsc.checkpoint) + + def test_concurrent_update_and_checkpoint(self): + mmsc = MinMaxSumCountAggregator() + checkpoint_total = MinMaxSumCountAggregator._TYPE(2 ** 32, 0, 0, 0) + + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex: + fut = ex.submit(self.call_update, mmsc) + + while not fut.done(): + mmsc.take_checkpoint() + checkpoint_total = MinMaxSumCountAggregator._merge_checkpoint( + checkpoint_total, mmsc.checkpoint + ) + + mmsc.take_checkpoint() + checkpoint_total = MinMaxSumCountAggregator._merge_checkpoint( + checkpoint_total, mmsc.checkpoint + ) + + self.assertEqual(checkpoint_total, fut.result()) + class TestController(unittest.TestCase): def test_push_controller(self): From a756492e83cc717c9bc60df010fe3e17b633c8c4 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 27 Feb 2020 16:37:24 -0800 Subject: [PATCH 0218/1517] OT Collector trace exporter (#405) Based on the OpenCensus agent exporter. Fixes #343 Co-authored-by: Chris Kleinknecht --- examples/basic_tracer/README.md | 20 ++ .../basic_tracer/docker/collector-config.yaml | 19 ++ .../basic_tracer/docker/docker-compose.yaml | 20 ++ examples/basic_tracer/tracer.py | 11 + .../CHANGELOG.md | 4 + ext/opentelemetry-ext-otcollector/README.rst | 55 ++++ ext/opentelemetry-ext-otcollector/setup.cfg | 49 +++ ext/opentelemetry-ext-otcollector/setup.py | 26 ++ .../opentelemetry/ext/otcollector/__init__.py | 13 + .../otcollector/trace_exporter/__init__.py | 187 +++++++++++ .../src/opentelemetry/ext/otcollector/util.py | 99 ++++++ .../opentelemetry/ext/otcollector/version.py | 15 + .../tests/__init__.py | 0 .../tests/test_otcollector_exporter.py | 305 ++++++++++++++++++ tox.ini | 11 +- 15 files changed, 831 insertions(+), 3 deletions(-) create mode 100644 examples/basic_tracer/docker/collector-config.yaml create mode 100644 examples/basic_tracer/docker/docker-compose.yaml create mode 100644 ext/opentelemetry-ext-otcollector/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-otcollector/README.rst create mode 100644 ext/opentelemetry-ext-otcollector/setup.cfg create mode 100644 ext/opentelemetry-ext-otcollector/setup.py create mode 100644 ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py create mode 100644 ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py create mode 100644 ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py create mode 100644 ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py create mode 100644 ext/opentelemetry-ext-otcollector/tests/__init__.py create mode 100644 ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py diff --git a/examples/basic_tracer/README.md b/examples/basic_tracer/README.md index 4dc0e96bea..ae9e4ca895 100644 --- a/examples/basic_tracer/README.md +++ b/examples/basic_tracer/README.md @@ -53,6 +53,26 @@ Click on the trace to view its details.

+### Collector + +* Start Collector + +```sh +$ pip install docker-compose +$ cd docker +$ docker-compose up + +* Run the sample + +$ pip install opentelemetry-ext-otcollector +$ # from this directory +$ EXPORTER=collector python tracer.py +``` + +Collector is configured to export to Jaeger, follow Jaeger UI isntructions to find the traces. + + + ## Useful links - For more information on OpenTelemetry, visit: - For more information on tracing in Python, visit: diff --git a/examples/basic_tracer/docker/collector-config.yaml b/examples/basic_tracer/docker/collector-config.yaml new file mode 100644 index 0000000000..bcf59c5802 --- /dev/null +++ b/examples/basic_tracer/docker/collector-config.yaml @@ -0,0 +1,19 @@ +receivers: + opencensus: + endpoint: "0.0.0.0:55678" + +exporters: + jaeger_grpc: + endpoint: jaeger-all-in-one:14250 + logging: {} + +processors: + batch: + queued_retry: + +service: + pipelines: + traces: + receivers: [opencensus] + exporters: [jaeger_grpc, logging] + processors: [batch, queued_retry] diff --git a/examples/basic_tracer/docker/docker-compose.yaml b/examples/basic_tracer/docker/docker-compose.yaml new file mode 100644 index 0000000000..71d7ccd5a1 --- /dev/null +++ b/examples/basic_tracer/docker/docker-compose.yaml @@ -0,0 +1,20 @@ +version: "2" +services: + + # Collector + collector: + image: omnition/opentelemetry-collector-contrib:latest + command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"] + volumes: + - ./collector-config.yaml:/conf/collector-config.yaml + ports: + - "55678:55678" + + jaeger-all-in-one: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "6831:6831/udp" + - "6832:6832/udp" + - "14268" + - "14250" diff --git a/examples/basic_tracer/tracer.py b/examples/basic_tracer/tracer.py index bfb50a9890..a454eab7a9 100755 --- a/examples/basic_tracer/tracer.py +++ b/examples/basic_tracer/tracer.py @@ -26,12 +26,23 @@ if os.getenv("EXPORTER") == "jaeger": from opentelemetry.ext.jaeger import JaegerSpanExporter + print("Using JaegerSpanExporter") exporter = JaegerSpanExporter( service_name="basic-service", agent_host_name="localhost", agent_port=6831, ) +elif os.getenv("EXPORTER") == "collector": + from opentelemetry.ext.otcollector.trace_exporter import ( + CollectorSpanExporter, + ) + + print("Using CollectorSpanExporter") + exporter = CollectorSpanExporter( + service_name="basic-service", endpoint="localhost:55678" + ) else: + print("Using ConsoleSpanExporter") exporter = ConsoleSpanExporter() # The preferred tracer implementation must be set, as the opentelemetry-api diff --git a/ext/opentelemetry-ext-otcollector/CHANGELOG.md b/ext/opentelemetry-ext-otcollector/CHANGELOG.md new file mode 100644 index 0000000000..617d979ab2 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## Unreleased + diff --git a/ext/opentelemetry-ext-otcollector/README.rst b/ext/opentelemetry-ext-otcollector/README.rst new file mode 100644 index 0000000000..33d8d58747 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/README.rst @@ -0,0 +1,55 @@ +OpenTelemetry Collector Exporter +================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-otcollector.svg + :target: https://pypi.org/project/opentelemetry-ext-otcollector/ + +This library allows to export data to `OpenTelemetry Collector `_. + +Installation +------------ + +:: + + pip install opentelemetry-ext-otcollector + + +Usage +----- + +The **OpenTelemetry Collector Exporter** allows to export `OpenTelemetry`_ traces to `OpenTelemetry Collector`_. + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext.otcollector.trace_exporter import CollectorSpanExporter + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + + + # create a CollectorSpanExporter + collector_exporter = CollectorSpanExporter( + # optional: + # endpoint="myCollectorUrl:55678", + # service_name="test_service", + # host_name="machine/container name", + ) + + # Create a BatchExportSpanProcessor and add the exporter to it + span_processor = BatchExportSpanProcessor(collector_exporter) + + # Configure the tracer to use the collector exporter + tracer_provider = TracerProvider() + tracer_provider.add_span_processor(span_processor) + tracer = TracerProvider().get_tracer(__name__) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + +References +---------- + +* `OpenTelemetry Collector `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-otcollector/setup.cfg b/ext/opentelemetry-ext-otcollector/setup.cfg new file mode 100644 index 0000000000..acc5b37723 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/setup.cfg @@ -0,0 +1,49 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-otcollector +description = OpenTelemetry Collector Exporter +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-otcollector +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + grpcio >= 1.0.0, < 2.0.0 + opencensus-proto >= 0.1.0, < 1.0.0 + opentelemetry-api >= 0.5.dev0 + opentelemetry-sdk >= 0.5.dev0 + protobuf >= 3.8.0 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-otcollector/setup.py b/ext/opentelemetry-ext-otcollector/setup.py new file mode 100644 index 0000000000..ecd8419511 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/setup.py @@ -0,0 +1,26 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "otcollector", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py new file mode 100644 index 0000000000..6ab2e961ec --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py new file mode 100644 index 0000000000..8712682ecf --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py @@ -0,0 +1,187 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""OpenTelemetry Collector Exporter.""" + +import logging +from typing import Optional, Sequence + +import grpc +from opencensus.proto.agent.trace.v1 import ( + trace_service_pb2, + trace_service_pb2_grpc, +) +from opencensus.proto.trace.v1 import trace_pb2 + +import opentelemetry.ext.otcollector.util as utils +import opentelemetry.trace as trace_api +from opentelemetry.sdk.trace import Span, SpanContext +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.trace import SpanKind, TraceState + +DEFAULT_ENDPOINT = "localhost:55678" + +logger = logging.getLogger(__name__) + + +# pylint: disable=no-member +class CollectorSpanExporter(SpanExporter): + """OpenTelemetry Collector span exporter. + + Args: + endpoint: OpenTelemetry Collector OpenCensus receiver endpoint. + service_name: Name of Collector service. + host_name: Host name. + client: TraceService client stub. + """ + + def __init__( + self, + endpoint=DEFAULT_ENDPOINT, + service_name=None, + host_name=None, + client=None, + ): + self.endpoint = endpoint + if client is None: + self.channel = grpc.insecure_channel(self.endpoint) + self.client = trace_service_pb2_grpc.TraceServiceStub( + channel=self.channel + ) + else: + self.client = client + + self.node = utils.get_node(service_name, host_name) + + def export(self, spans: Sequence[Span]) -> SpanExportResult: + try: + responses = self.client.Export(self.generate_span_requests(spans)) + + # Read response + for _ in responses: + pass + + except grpc.RpcError: + return SpanExportResult.FAILED_NOT_RETRYABLE + + return SpanExportResult.SUCCESS + + def shutdown(self) -> None: + pass + + def generate_span_requests(self, spans): + collector_spans = translate_to_collector(spans) + service_request = trace_service_pb2.ExportTraceServiceRequest( + node=self.node, spans=collector_spans + ) + yield service_request + + +# pylint: disable=too-many-branches +def translate_to_collector(spans: Sequence[Span]): + collector_spans = [] + for span in spans: + status = None + if span.status is not None: + status = trace_pb2.Status( + code=span.status.canonical_code.value, + message=span.status.description, + ) + collector_span = trace_pb2.Span( + name=trace_pb2.TruncatableString(value=span.name), + kind=utils.get_collector_span_kind(span.kind), + trace_id=span.context.trace_id.to_bytes(16, "big"), + span_id=span.context.span_id.to_bytes(8, "big"), + start_time=utils.proto_timestamp_from_time_ns(span.start_time), + end_time=utils.proto_timestamp_from_time_ns(span.end_time), + status=status, + ) + + parent_id = 0 + if isinstance(span.parent, trace_api.Span): + parent_id = span.parent.get_context().span_id + elif isinstance(span.parent, trace_api.SpanContext): + parent_id = span.parent.span_id + + collector_span.parent_span_id = parent_id.to_bytes(8, "big") + + if span.context.trace_state is not None: + for (key, value) in span.context.trace_state.items(): + collector_span.tracestate.entries.add(key=key, value=value) + + if span.attributes: + for (key, value) in span.attributes.items(): + utils.add_proto_attribute_value( + collector_span.attributes, key, value + ) + + if span.events: + for event in span.events: + + collector_annotation = trace_pb2.Span.TimeEvent.Annotation( + description=trace_pb2.TruncatableString(value=event.name) + ) + + if event.attributes: + for (key, value) in event.attributes.items(): + utils.add_proto_attribute_value( + collector_annotation.attributes, key, value + ) + + collector_span.time_events.time_event.add( + time=utils.proto_timestamp_from_time_ns(event.timestamp), + annotation=collector_annotation, + ) + + if span.links: + for link in span.links: + collector_span_link = collector_span.links.link.add() + collector_span_link.trace_id = link.context.trace_id.to_bytes( + 16, "big" + ) + collector_span_link.span_id = link.context.span_id.to_bytes( + 8, "big" + ) + + collector_span_link.type = ( + trace_pb2.Span.Link.Type.TYPE_UNSPECIFIED + ) + + if isinstance(span.parent, trace_api.Span): + if ( + link.context.span_id + == span.parent.get_context().span_id + and link.context.trace_id + == span.parent.get_context().trace_id + ): + collector_span_link.type = ( + trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN + ) + elif isinstance(span.parent, trace_api.SpanContext): + if ( + link.context.span_id == span.parent.span_id + and link.context.trace_id == span.parent.trace_id + ): + collector_span_link.type = ( + trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN + ) + + if link.attributes: + for (key, value) in link.attributes.items(): + utils.add_proto_attribute_value( + collector_span_link.attributes, key, value + ) + + collector_spans.append(collector_span) + return collector_spans diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py new file mode 100644 index 0000000000..7d605ab8f9 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py @@ -0,0 +1,99 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import socket +import time + +from google.protobuf.timestamp_pb2 import Timestamp +from opencensus.proto.agent.common.v1 import common_pb2 +from opencensus.proto.trace.v1 import trace_pb2 + +from opentelemetry.ext.otcollector.version import ( + __version__ as otcollector_exporter_version, +) +from opentelemetry.trace import SpanKind +from opentelemetry.util.version import __version__ as opentelemetry_version + + +def proto_timestamp_from_time_ns(time_ns): + """Converts datetime to protobuf timestamp. + + Args: + time_ns: Time in nanoseconds + + Returns: + Returns protobuf timestamp. + """ + ts = Timestamp() + if time_ns is not None: + # pylint: disable=no-member + ts.FromNanoseconds(time_ns) + return ts + + +# pylint: disable=no-member +def get_collector_span_kind(kind: SpanKind): + if kind is SpanKind.SERVER: + return trace_pb2.Span.SpanKind.SERVER + if kind is SpanKind.CLIENT: + return trace_pb2.Span.SpanKind.CLIENT + return trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED + + +def add_proto_attribute_value(pb_attributes, key, value): + """Sets string, int, boolean or float value on protobuf + span, link or annotation attributes. + + Args: + pb_attributes: protobuf Span's attributes property. + key: attribute key to set. + value: attribute value + """ + + if isinstance(value, bool): + pb_attributes.attribute_map[key].bool_value = value + elif isinstance(value, int): + pb_attributes.attribute_map[key].int_value = value + elif isinstance(value, str): + pb_attributes.attribute_map[key].string_value.value = value + elif isinstance(value, float): + pb_attributes.attribute_map[key].double_value = value + else: + pb_attributes.attribute_map[key].string_value.value = str(value) + + +# pylint: disable=no-member +def get_node(service_name, host_name): + """Generates Node message from params and system information. + + Args: + service_name: Name of Collector service. + host_name: Host name. + """ + return common_pb2.Node( + identifier=common_pb2.ProcessIdentifier( + host_name=socket.gethostname() if host_name is None else host_name, + pid=os.getpid(), + start_timestamp=proto_timestamp_from_time_ns( + int(time.time() * 1e9) + ), + ), + library_info=common_pb2.LibraryInfo( + language=common_pb2.LibraryInfo.Language.Value("PYTHON"), + exporter_version=otcollector_exporter_version, + core_library_version=opentelemetry_version, + ), + service_info=common_pb2.ServiceInfo(name=service_name), + ) diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py new file mode 100644 index 0000000000..f48cb5bee5 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py @@ -0,0 +1,15 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-otcollector/tests/__init__.py b/ext/opentelemetry-ext-otcollector/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py new file mode 100644 index 0000000000..0e83b038be --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py @@ -0,0 +1,305 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +import grpc +from google.protobuf.timestamp_pb2 import Timestamp +from opencensus.proto.trace.v1 import trace_pb2 + +import opentelemetry.ext.otcollector.util as utils +from opentelemetry import trace as trace_api +from opentelemetry.ext.otcollector.trace_exporter import ( + CollectorSpanExporter, + translate_to_collector, +) +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace.export import SpanExportResult +from opentelemetry.trace import TraceOptions + + +# pylint: disable=no-member +class TestCollectorSpanExporter(unittest.TestCase): + def test_constructor(self): + mock_get_node = mock.Mock() + patch = mock.patch( + "opentelemetry.ext.otcollector.util.get_node", + side_effect=mock_get_node, + ) + service_name = "testServiceName" + host_name = "testHostName" + client = grpc.insecure_channel("") + endpoint = "testEndpoint" + with patch: + exporter = CollectorSpanExporter( + service_name=service_name, + host_name=host_name, + endpoint=endpoint, + client=client, + ) + + self.assertIs(exporter.client, client) + self.assertEqual(exporter.endpoint, endpoint) + mock_get_node.assert_called_with(service_name, host_name) + + def test_get_collector_span_kind(self): + result = utils.get_collector_span_kind(trace_api.SpanKind.SERVER) + self.assertIs(result, trace_pb2.Span.SpanKind.SERVER) + result = utils.get_collector_span_kind(trace_api.SpanKind.CLIENT) + self.assertIs(result, trace_pb2.Span.SpanKind.CLIENT) + result = utils.get_collector_span_kind(trace_api.SpanKind.CONSUMER) + self.assertIs(result, trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED) + result = utils.get_collector_span_kind(trace_api.SpanKind.PRODUCER) + self.assertIs(result, trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED) + result = utils.get_collector_span_kind(trace_api.SpanKind.INTERNAL) + self.assertIs(result, trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED) + + def test_proto_timestamp_from_time_ns(self): + result = utils.proto_timestamp_from_time_ns(12345) + self.assertIsInstance(result, Timestamp) + self.assertEqual(result.nanos, 12345) + + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + def test_translate_to_collector(self): + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + span_id = 0x34BF92DEEFC58C92 + parent_id = 0x1111111111111111 + base_time = 683647322 * 10 ** 9 # in ns + start_times = ( + base_time, + base_time + 150 * 10 ** 6, + base_time + 300 * 10 ** 6, + ) + durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6) + end_times = ( + start_times[0] + durations[0], + start_times[1] + durations[1], + start_times[2] + durations[2], + ) + span_context = trace_api.SpanContext( + trace_id, + span_id, + trace_options=TraceOptions(TraceOptions.SAMPLED), + trace_state=trace_api.TraceState({"testKey": "testValue"}), + ) + parent_context = trace_api.SpanContext(trace_id, parent_id) + other_context = trace_api.SpanContext(trace_id, span_id) + event_attributes = { + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + } + event_timestamp = base_time + 50 * 10 ** 6 + event = trace_api.Event( + name="event0", + timestamp=event_timestamp, + attributes=event_attributes, + ) + link_attributes = {"key_bool": True} + link_1 = trace_api.Link( + context=other_context, attributes=link_attributes + ) + link_2 = trace_api.Link( + context=parent_context, attributes=link_attributes + ) + span_1 = trace.Span( + name="test1", + context=span_context, + parent=parent_context, + events=(event,), + links=(link_1,), + kind=trace_api.SpanKind.CLIENT, + ) + span_2 = trace.Span( + name="test2", + context=parent_context, + parent=None, + kind=trace_api.SpanKind.SERVER, + ) + span_3 = trace.Span( + name="test3", context=other_context, links=(link_2,), parent=span_2 + ) + otel_spans = [span_1, span_2, span_3] + otel_spans[0].start(start_time=start_times[0]) + otel_spans[0].set_attribute("key_bool", False) + otel_spans[0].set_attribute("key_string", "hello_world") + otel_spans[0].set_attribute("key_float", 111.22) + otel_spans[0].set_attribute("key_int", 333) + otel_spans[0].set_status( + trace_api.Status( + trace_api.status.StatusCanonicalCode.INTERNAL, + "test description", + ) + ) + otel_spans[0].end(end_time=end_times[0]) + otel_spans[1].start(start_time=start_times[1]) + otel_spans[1].end(end_time=end_times[1]) + otel_spans[2].start(start_time=start_times[2]) + otel_spans[2].end(end_time=end_times[2]) + output_spans = translate_to_collector(otel_spans) + + self.assertEqual(len(output_spans), 3) + self.assertEqual( + output_spans[0].trace_id, b"n\x0cc%}\xe3L\x92o\x9e\xfc\xd09''." + ) + self.assertEqual( + output_spans[0].span_id, b"4\xbf\x92\xde\xef\xc5\x8c\x92" + ) + self.assertEqual( + output_spans[0].name, trace_pb2.TruncatableString(value="test1") + ) + self.assertEqual( + output_spans[1].name, trace_pb2.TruncatableString(value="test2") + ) + self.assertEqual( + output_spans[2].name, trace_pb2.TruncatableString(value="test3") + ) + self.assertEqual( + output_spans[0].start_time.seconds, + int(start_times[0] / 1000000000), + ) + self.assertEqual( + output_spans[0].end_time.seconds, int(end_times[0] / 1000000000) + ) + self.assertEqual(output_spans[0].kind, trace_api.SpanKind.CLIENT.value) + self.assertEqual(output_spans[1].kind, trace_api.SpanKind.SERVER.value) + + self.assertEqual( + output_spans[0].parent_span_id, b"\x11\x11\x11\x11\x11\x11\x11\x11" + ) + self.assertEqual( + output_spans[2].parent_span_id, b"\x11\x11\x11\x11\x11\x11\x11\x11" + ) + self.assertEqual( + output_spans[0].status.code, + trace_api.status.StatusCanonicalCode.INTERNAL.value, + ) + self.assertEqual(output_spans[0].status.message, "test description") + self.assertEqual(len(output_spans[0].tracestate.entries), 1) + self.assertEqual(output_spans[0].tracestate.entries[0].key, "testKey") + self.assertEqual( + output_spans[0].tracestate.entries[0].value, "testValue" + ) + + self.assertEqual( + output_spans[0].attributes.attribute_map["key_bool"].bool_value, + False, + ) + self.assertEqual( + output_spans[0] + .attributes.attribute_map["key_string"] + .string_value.value, + "hello_world", + ) + self.assertEqual( + output_spans[0].attributes.attribute_map["key_float"].double_value, + 111.22, + ) + self.assertEqual( + output_spans[0].attributes.attribute_map["key_int"].int_value, 333 + ) + + self.assertEqual( + output_spans[0].time_events.time_event[0].time.seconds, 683647322 + ) + self.assertEqual( + output_spans[0] + .time_events.time_event[0] + .annotation.description.value, + "event0", + ) + self.assertEqual( + output_spans[0] + .time_events.time_event[0] + .annotation.attributes.attribute_map["annotation_bool"] + .bool_value, + True, + ) + self.assertEqual( + output_spans[0] + .time_events.time_event[0] + .annotation.attributes.attribute_map["annotation_string"] + .string_value.value, + "annotation_test", + ) + self.assertEqual( + output_spans[0] + .time_events.time_event[0] + .annotation.attributes.attribute_map["key_float"] + .double_value, + 0.3, + ) + + self.assertEqual( + output_spans[0].links.link[0].trace_id, + b"n\x0cc%}\xe3L\x92o\x9e\xfc\xd09''.", + ) + self.assertEqual( + output_spans[0].links.link[0].span_id, + b"4\xbf\x92\xde\xef\xc5\x8c\x92", + ) + self.assertEqual( + output_spans[0].links.link[0].type, + trace_pb2.Span.Link.Type.TYPE_UNSPECIFIED, + ) + self.assertEqual( + output_spans[2].links.link[0].type, + trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN, + ) + self.assertEqual( + output_spans[0] + .links.link[0] + .attributes.attribute_map["key_bool"] + .bool_value, + True, + ) + + def test_export(self): + mock_client = mock.MagicMock() + mock_export = mock.MagicMock() + mock_client.Export = mock_export + host_name = "testHostName" + collector_exporter = CollectorSpanExporter( + client=mock_client, host_name=host_name + ) + + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + span_id = 0x34BF92DEEFC58C92 + span_context = trace_api.SpanContext( + trace_id, span_id, trace_options=TraceOptions(TraceOptions.SAMPLED) + ) + otel_spans = [ + trace.Span( + name="test1", + context=span_context, + kind=trace_api.SpanKind.CLIENT, + ) + ] + result_status = collector_exporter.export(otel_spans) + self.assertEqual(SpanExportResult.SUCCESS, result_status) + + # pylint: disable=unsubscriptable-object + export_arg = mock_export.call_args[0] + service_request = next(export_arg[0]) + output_spans = getattr(service_request, "spans") + output_node = getattr(service_request, "node") + self.assertEqual(len(output_spans), 1) + self.assertIsNotNone(getattr(output_node, "library_info")) + self.assertIsNotNone(getattr(output_node, "service_info")) + output_identifier = getattr(output_node, "identifier") + self.assertEqual( + getattr(output_identifier, "host_name"), "testHostName" + ) diff --git a/tox.ini b/tox.ini index be7f1db9f7..23f8dbce05 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,6 @@ skipsdist = True skip_missing_interpreters = True envlist = - ; Environments are organized by individual package, allowing ; for specifying supported Python versions per package. ; opentelemetry-api @@ -44,7 +43,10 @@ envlist = ; opentelemetry-ext-mysql py3{4,5,6,7,8}-test-ext-mysql pypy3-test-ext-mysql - + ; opentelemetry-ext-otcollector + py3{4,5,6,7,8}-test-ext-otcollector + ; ext-otcollector intentionally excluded from pypy3 + ; opentelemetry-ext-prometheus py3{4,5,6,7,8}-test-ext-prometheus pypy3-test-ext-prometheus @@ -103,6 +105,7 @@ changedir = test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests test-ext-mysql: ext/opentelemetry-ext-mysql/tests + test-ext-otcollector: ext/opentelemetry-ext-otcollector/tests test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests @@ -140,6 +143,8 @@ commands_pre = dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql + otcollector: pip install {toxinidir}/opentelemetry-sdk + otcollector: pip install {toxinidir}/ext/opentelemetry-ext-otcollector prometheus: pip install {toxinidir}/opentelemetry-sdk prometheus: pip install {toxinidir}/ext/opentelemetry-ext-prometheus pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo @@ -243,4 +248,4 @@ commands = pytest {posargs} commands_post = - docker-compose down \ No newline at end of file + docker-compose down From c2ba0653518a3587c0cabf8b94fad374c9c53e56 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Fri, 28 Feb 2020 11:36:36 -0800 Subject: [PATCH 0219/1517] API: Renaming TraceOptions to TraceFlags (#450) Renaming TraceOptions to TraceFlags, which is the term used to describe the flags associated with the trace in the OpenTelemetry specification. Closes #434 --- .../tests/test_flask_example.py | 2 +- .../src/opentelemetry/ext/jaeger/__init__.py | 2 +- .../tests/test_otcollector_exporter.py | 6 +-- .../src/opentelemetry/ext/zipkin/__init__.py | 2 +- .../tests/test_zipkin_exporter.py | 4 +- .../propagation/tracecontexthttptextformat.py | 6 +-- .../src/opentelemetry/trace/__init__.py | 18 ++++----- .../src/opentelemetry/trace/sampling.py | 2 +- .../tests/trace/test_sampling.py | 40 ++++++------------- .../sdk/context/propagation/b3_format.py | 8 ++-- .../src/opentelemetry/sdk/trace/__init__.py | 10 ++--- .../context/propagation/test_b3_format.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 10 ++--- 13 files changed, 46 insertions(+), 66 deletions(-) diff --git a/examples/opentelemetry-example-app/tests/test_flask_example.py b/examples/opentelemetry-example-app/tests/test_flask_example.py index 69be9e4bfc..cbefadc532 100644 --- a/examples/opentelemetry-example-app/tests/test_flask_example.py +++ b/examples/opentelemetry-example-app/tests/test_flask_example.py @@ -59,7 +59,7 @@ def test_full_path(self): "traceparent": "00-{:032x}-{:016x}-{:02x}".format( trace_id, trace_sdk.generate_span_id(), - trace.TraceOptions.SAMPLED, + trace.TraceFlags.SAMPLED, ) }, ) diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py index a90313d913..6679ce6b7e 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -171,7 +171,7 @@ def _translate_to_jaeger(spans: Span): refs = _extract_refs_from_span(span) logs = _extract_logs_from_span(span) - flags = int(ctx.trace_options) + flags = int(ctx.trace_flags) jaeger_span = jaeger.Span( traceIdHigh=_get_trace_id_high(trace_id), diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py index 0e83b038be..9a17ea6c94 100644 --- a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py @@ -27,7 +27,7 @@ ) from opentelemetry.sdk import trace from opentelemetry.sdk.trace.export import SpanExportResult -from opentelemetry.trace import TraceOptions +from opentelemetry.trace import TraceFlags # pylint: disable=no-member @@ -92,7 +92,7 @@ def test_translate_to_collector(self): span_context = trace_api.SpanContext( trace_id, span_id, - trace_options=TraceOptions(TraceOptions.SAMPLED), + trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state=trace_api.TraceState({"testKey": "testValue"}), ) parent_context = trace_api.SpanContext(trace_id, parent_id) @@ -279,7 +279,7 @@ def test_export(self): trace_id = 0x6E0C63257DE34C926F9EFCD03927272E span_id = 0x34BF92DEEFC58C92 span_context = trace_api.SpanContext( - trace_id, span_id, trace_options=TraceOptions(TraceOptions.SAMPLED) + trace_id, span_id, trace_flags=TraceFlags(TraceFlags.SAMPLED) ) otel_spans = [ trace.Span( diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index fec4da8c3e..077fa9a6b4 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -132,7 +132,7 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): "annotations": _extract_annotations_from_events(span.events), } - if context.trace_options.sampled: + if context.trace_flags.sampled: zipkin_span["debug"] = 1 if isinstance(span.parent, Span): diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py index 467bc610bd..c779c7388f 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -20,7 +20,7 @@ from opentelemetry.ext.zipkin import ZipkinSpanExporter from opentelemetry.sdk import trace from opentelemetry.sdk.trace.export import SpanExportResult -from opentelemetry.trace import TraceOptions +from opentelemetry.trace import TraceFlags class MockResponse: @@ -114,7 +114,7 @@ def test_export(self): ) span_context = trace_api.SpanContext( - trace_id, span_id, trace_options=TraceOptions(TraceOptions.SAMPLED) + trace_id, span_id, trace_flags=TraceFlags(TraceFlags.SAMPLED) ) parent_context = trace_api.SpanContext(trace_id, parent_id) other_context = trace_api.SpanContext(trace_id, other_id) diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py index 6f50f00839..0f07841eb7 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py @@ -77,7 +77,7 @@ def extract( version = match.group(1) trace_id = match.group(2) span_id = match.group(3) - trace_options = match.group(4) + trace_flags = match.group(4) if trace_id == "0" * 32 or span_id == "0" * 16: return trace.INVALID_SPAN_CONTEXT @@ -96,7 +96,7 @@ def extract( span_context = trace.SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), - trace_options=trace.TraceOptions(trace_options), + trace_flags=trace.TraceFlags(trace_flags), trace_state=tracestate, ) @@ -115,7 +115,7 @@ def inject( if context == trace.INVALID_SPAN_CONTEXT: return traceparent_string = "00-{:032x}-{:016x}-{:02x}".format( - context.trace_id, context.span_id, context.trace_options + context.trace_id, context.span_id, context.trace_flags ) set_in_carrier( carrier, cls._TRACEPARENT_HEADER_NAME, traceparent_string diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index b141e466aa..a6633e434a 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -249,7 +249,7 @@ def __exit__( self.end() -class TraceOptions(int): +class TraceFlags(int): """A bitmask that represents options specific to the trace. The only supported option is the "sampled" flag (``0x01``). If set, this @@ -265,15 +265,15 @@ class TraceOptions(int): SAMPLED = 0x01 @classmethod - def get_default(cls) -> "TraceOptions": + def get_default(cls) -> "TraceFlags": return cls(cls.DEFAULT) @property def sampled(self) -> bool: - return bool(self & TraceOptions.SAMPLED) + return bool(self & TraceFlags.SAMPLED) -DEFAULT_TRACE_OPTIONS = TraceOptions.get_default() +DEFAULT_TRACE_OPTIONS = TraceFlags.get_default() class TraceState(typing.Dict[str, str]): @@ -312,7 +312,7 @@ class SpanContext: Args: trace_id: The ID of the trace that this span belongs to. span_id: This span's ID. - trace_options: Trace options to propagate. + trace_flags: Trace options to propagate. trace_state: Tracing-system-specific info to propagate. """ @@ -320,16 +320,16 @@ def __init__( self, trace_id: int, span_id: int, - trace_options: "TraceOptions" = DEFAULT_TRACE_OPTIONS, + trace_flags: "TraceFlags" = DEFAULT_TRACE_OPTIONS, trace_state: "TraceState" = DEFAULT_TRACE_STATE, ) -> None: - if trace_options is None: - trace_options = DEFAULT_TRACE_OPTIONS + if trace_flags is None: + trace_flags = DEFAULT_TRACE_OPTIONS if trace_state is None: trace_state = DEFAULT_TRACE_STATE self.trace_id = trace_id self.span_id = span_id - self.trace_options = trace_options + self.trace_flags = trace_flags self.trace_state = trace_state def __repr__(self) -> str: diff --git a/opentelemetry-api/src/opentelemetry/trace/sampling.py b/opentelemetry-api/src/opentelemetry/trace/sampling.py index 503c2e03eb..c398efbf86 100644 --- a/opentelemetry-api/src/opentelemetry/trace/sampling.py +++ b/opentelemetry-api/src/opentelemetry/trace/sampling.py @@ -113,7 +113,7 @@ def should_sample( links: Sequence["Link"] = (), ) -> "Decision": if parent_context is not None: - return Decision(parent_context.trace_options.sampled) + return Decision(parent_context.trace_flags.sampled) return Decision(trace_id & self.TRACE_ID_LIMIT < self.bound) diff --git a/opentelemetry-api/tests/trace/test_sampling.py b/opentelemetry-api/tests/trace/test_sampling.py index f04aecef45..0a3d819528 100644 --- a/opentelemetry-api/tests/trace/test_sampling.py +++ b/opentelemetry-api/tests/trace/test_sampling.py @@ -18,16 +18,14 @@ from opentelemetry import trace from opentelemetry.trace import sampling -TO_DEFAULT = trace.TraceOptions(trace.TraceOptions.DEFAULT) -TO_SAMPLED = trace.TraceOptions(trace.TraceOptions.SAMPLED) +TO_DEFAULT = trace.TraceFlags(trace.TraceFlags.DEFAULT) +TO_SAMPLED = trace.TraceFlags(trace.TraceFlags.SAMPLED) class TestSampler(unittest.TestCase): def test_always_on(self): no_record_always_on = sampling.ALWAYS_ON.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_DEFAULT - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_DEFAULT), 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling on", @@ -36,9 +34,7 @@ def test_always_on(self): self.assertEqual(no_record_always_on.attributes, {}) sampled_always_on = sampling.ALWAYS_ON.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_SAMPLED - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_SAMPLED), 0xDEADBEF1, 0xDEADBEF2, "sampled parent, sampling on", @@ -48,9 +44,7 @@ def test_always_on(self): def test_always_off(self): no_record_always_off = sampling.ALWAYS_OFF.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_DEFAULT - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_DEFAULT), 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling off", @@ -59,9 +53,7 @@ def test_always_off(self): self.assertEqual(no_record_always_off.attributes, {}) sampled_always_on = sampling.ALWAYS_OFF.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_SAMPLED - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_SAMPLED), 0xDEADBEF1, 0xDEADBEF2, "sampled parent, sampling off", @@ -71,9 +63,7 @@ def test_always_off(self): def test_default_on(self): no_record_default_on = sampling.DEFAULT_ON.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_DEFAULT - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_DEFAULT), 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling on", @@ -82,9 +72,7 @@ def test_default_on(self): self.assertEqual(no_record_default_on.attributes, {}) sampled_default_on = sampling.DEFAULT_ON.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_SAMPLED - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_SAMPLED), 0xDEADBEF1, 0xDEADBEF2, "sampled parent, sampling on", @@ -94,9 +82,7 @@ def test_default_on(self): def test_default_off(self): no_record_default_off = sampling.DEFAULT_OFF.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_DEFAULT - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_DEFAULT), 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling off", @@ -105,9 +91,7 @@ def test_default_off(self): self.assertEqual(no_record_default_off.attributes, {}) sampled_default_off = sampling.DEFAULT_OFF.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_SAMPLED - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_SAMPLED), 0xDEADBEF1, 0xDEADBEF2, "sampled parent, sampling off", @@ -136,7 +120,7 @@ def test_probability_sampler(self): self.assertFalse( sampler.should_sample( trace.SpanContext( - 0xDEADBEF0, 0xDEADBEF1, trace_options=TO_DEFAULT + 0xDEADBEF0, 0xDEADBEF1, trace_flags=TO_DEFAULT ), 0x7FFFFFFFFFFFFFFF, 0xDEADBEEF, @@ -146,7 +130,7 @@ def test_probability_sampler(self): self.assertTrue( sampler.should_sample( trace.SpanContext( - 0xDEADBEF0, 0xDEADBEF1, trace_options=TO_SAMPLED + 0xDEADBEF0, 0xDEADBEF1, trace_flags=TO_SAMPLED ), 0x8000000000000000, 0xDEADBEEF, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py index 4c9214dbcc..4da487618b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -91,20 +91,18 @@ def extract(cls, get_from_carrier, carrier): # the desire for some form of sampling, propagate if either # header is set to allow. if sampled in cls._SAMPLE_PROPAGATE_VALUES or flags == "1": - options |= trace.TraceOptions.SAMPLED + options |= trace.TraceFlags.SAMPLED return trace.SpanContext( # trace an span ids are encoded in hex, so must be converted trace_id=int(trace_id, 16), span_id=int(span_id, 16), - trace_options=trace.TraceOptions(options), + trace_flags=trace.TraceFlags(options), trace_state=trace.TraceState(), ) @classmethod def inject(cls, span, set_in_carrier, carrier): - sampled = ( - trace.TraceOptions.SAMPLED & span.context.trace_options - ) != 0 + sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 set_in_carrier( carrier, cls.TRACE_ID_KEY, format_trace_id(span.context.trace_id) ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index dd0169ea9f..58f4e16fc2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -486,15 +486,15 @@ def start_span( # pylint: disable=too-many-locals if parent_context is None or not parent_context.is_valid(): parent = parent_context = None trace_id = generate_trace_id() - trace_options = None + trace_flags = None trace_state = None else: trace_id = parent_context.trace_id - trace_options = parent_context.trace_options + trace_flags = parent_context.trace_flags trace_state = parent_context.trace_state context = trace_api.SpanContext( - trace_id, generate_span_id(), trace_options, trace_state + trace_id, generate_span_id(), trace_flags, trace_state ) # The sampler decides whether to create a real or no-op span at the @@ -512,8 +512,8 @@ def start_span( # pylint: disable=too-many-locals ) if sampling_decision.sampled: - options = context.trace_options | trace_api.TraceOptions.SAMPLED - context.trace_options = trace_api.TraceOptions(options) + options = context.trace_flags | trace_api.TraceFlags.SAMPLED + context.trace_flags = trace_api.TraceFlags(options) if attributes is None: span_attributes = sampling_decision.attributes else: diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py index 17f7fdf7ca..ae55b02bfd 100644 --- a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -36,7 +36,7 @@ def get_child_parent_new_carrier(old_carrier): trace_api.SpanContext( parent_context.trace_id, trace.generate_span_id(), - trace_options=parent_context.trace_options, + trace_flags=parent_context.trace_flags, trace_state=parent_context.trace_state, ), parent=parent, diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 982df89667..9678ba283b 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -117,7 +117,7 @@ def test_default_sampler(self): self.assertIsInstance(root_span, trace.Span) child_span = tracer.start_span(name="child span", parent=root_span) self.assertIsInstance(child_span, trace.Span) - self.assertTrue(root_span.context.trace_options.sampled) + self.assertTrue(root_span.context.trace_flags.sampled) def test_sampler_no_sampling(self): tracer_provider = trace.TracerProvider(sampling.ALWAYS_OFF) @@ -251,7 +251,7 @@ def test_start_span_implicit(self): root_context.trace_state, child_context.trace_state ) self.assertEqual( - root_context.trace_options, child_context.trace_options + root_context.trace_flags, child_context.trace_flags ) # Verify start_span() did not set the current span. @@ -268,9 +268,7 @@ def test_start_span_explicit(self): other_parent = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, - trace_options=trace_api.TraceOptions( - trace_api.TraceOptions.SAMPLED - ), + trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) self.assertIsNone(tracer.get_current_span()) @@ -303,7 +301,7 @@ def test_start_span_explicit(self): other_parent.trace_state, child_context.trace_state ) self.assertEqual( - other_parent.trace_options, child_context.trace_options + other_parent.trace_flags, child_context.trace_flags ) # Verify start_span() did not set the current span. From 11467c47928a79e2edb830f410177ebba9a905c9 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Sat, 29 Feb 2020 22:01:20 -0800 Subject: [PATCH 0220/1517] api: Implement "named" meters + Remove "Batcher" from Meter constructor (#431) Implements #221. Also fixes #394. Stateful.py and stateless.py in metrics example folder are not changed to use the new loader in anticipation of #422 being merged first and removing them. Lastly, moves InstrumentationInfo from trace.py in the sdk to utils. --- README.md | 11 +- docs/opentelemetry.sdk.metrics.rst | 1 + docs/opentelemetry.sdk.trace.rst | 1 + ...opentelemetry.sdk.util.instrumentation.rst | 4 + examples/metrics/prometheus.py | 6 +- examples/metrics/record.py | 10 +- examples/metrics/simple_example.py | 24 ++-- .../metrics_example.py | 50 -------- .../tests/test_prometheus_exporter.py | 6 +- .../src/opentelemetry/metrics/__init__.py | 119 ++++++++++++++---- .../src/opentelemetry/util/__init__.py | 13 ++ .../tests/metrics/test_metrics.py | 53 +------- .../tests/test_implementation.py | 23 +++- .../src/opentelemetry/sdk/metrics/__init__.py | 30 ++++- .../src/opentelemetry/sdk/trace/__init__.py | 43 +------ .../sdk/{util.py => util/__init__.py} | 1 - .../opentelemetry/sdk/util/instrumentation.py | 55 ++++++++ .../tests/metrics/export/test_export.py | 32 ++--- .../tests/metrics/test_implementation.py | 35 ++++++ .../tests/metrics/test_metrics.py | 34 ++--- .../tests/{ => trace}/test_implementation.py | 10 +- opentelemetry-sdk/tests/trace/test_trace.py | 8 +- 22 files changed, 321 insertions(+), 248 deletions(-) create mode 100644 docs/opentelemetry.sdk.util.instrumentation.rst delete mode 100644 examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py rename opentelemetry-sdk/src/opentelemetry/sdk/{util.py => util/__init__.py} (99%) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py create mode 100644 opentelemetry-sdk/tests/metrics/test_implementation.py rename opentelemetry-sdk/tests/{ => trace}/test_implementation.py (85%) diff --git a/README.md b/README.md index 6f5b9f213e..734a824661 100644 --- a/README.md +++ b/README.md @@ -70,12 +70,14 @@ with tracer.start_as_current_span('foo'): ```python from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.controller import PushController -metrics.set_preferred_meter_implementation(lambda T: Meter()) -meter = metrics.meter() +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) +meter = metrics.get_meter(__name__) exporter = ConsoleMetricsExporter() +controller = PushController(meter, exporter, 5) counter = meter.create_metric( "available memory", @@ -89,9 +91,6 @@ counter = meter.create_metric( label_values = ("staging",) counter_handle = counter.get_handle(label_values) counter_handle.add(100) - -exporter.export([(counter, label_values)]) -exporter.shutdown() ``` See the [API documentation](https://open-telemetry.github.io/opentelemetry-python/) for more detail, and the [examples folder](./examples) for a more sample code. diff --git a/docs/opentelemetry.sdk.metrics.rst b/docs/opentelemetry.sdk.metrics.rst index ec8687dd2d..88612046c8 100644 --- a/docs/opentelemetry.sdk.metrics.rst +++ b/docs/opentelemetry.sdk.metrics.rst @@ -8,6 +8,7 @@ Submodules opentelemetry.sdk.metrics.export.aggregate opentelemetry.sdk.metrics.export.batcher + opentelemetry.sdk.util.instrumentation .. automodule:: opentelemetry.sdk.metrics :members: diff --git a/docs/opentelemetry.sdk.trace.rst b/docs/opentelemetry.sdk.trace.rst index 7bb3569fe6..1c0e9b6f61 100644 --- a/docs/opentelemetry.sdk.trace.rst +++ b/docs/opentelemetry.sdk.trace.rst @@ -7,6 +7,7 @@ Submodules .. toctree:: opentelemetry.sdk.trace.export + opentelemetry.sdk.util.instrumentation .. automodule:: opentelemetry.sdk.trace :members: diff --git a/docs/opentelemetry.sdk.util.instrumentation.rst b/docs/opentelemetry.sdk.util.instrumentation.rst new file mode 100644 index 0000000000..a7d391bcee --- /dev/null +++ b/docs/opentelemetry.sdk.util.instrumentation.rst @@ -0,0 +1,4 @@ +opentelemetry.sdk.util.instrumentation +========================================== + +.. automodule:: opentelemetry.sdk.util.instrumentation diff --git a/examples/metrics/prometheus.py b/examples/metrics/prometheus.py index 14f612c6a9..4d30f8abcc 100644 --- a/examples/metrics/prometheus.py +++ b/examples/metrics/prometheus.py @@ -21,15 +21,15 @@ from opentelemetry import metrics from opentelemetry.ext.prometheus import PrometheusMetricsExporter -from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export.controller import PushController # Start Prometheus client start_http_server(port=8000, addr="localhost") # Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_implementation(lambda _: Meter()) -meter = metrics.meter() +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) +meter = metrics.get_meter(__name__) # exporter to export metrics to Prometheus prefix = "MyAppPrefix" exporter = PrometheusMetricsExporter(prefix) diff --git a/examples/metrics/record.py b/examples/metrics/record.py index be68c8083f..f008ff6746 100644 --- a/examples/metrics/record.py +++ b/examples/metrics/record.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,13 +19,15 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController +# The preferred tracer implementation must be set, as the opentelemetry-api +# defines the interface with a no-op implementation. +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) # Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_implementation(lambda _: Meter()) -meter = metrics.meter() +meter = metrics.get_meter(__name__) # exporter to export metrics to the console exporter = ConsoleMetricsExporter() # controller collects metrics created from meter and exports it via the diff --git a/examples/metrics/simple_example.py b/examples/metrics/simple_example.py index 75da80b73a..7587915517 100644 --- a/examples/metrics/simple_example.py +++ b/examples/metrics/simple_example.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,9 +23,8 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Measure, Meter +from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.metrics.export.controller import PushController batcher_mode = "stateful" @@ -44,18 +43,15 @@ def usage(argv): usage(sys.argv) sys.exit(1) -# Batcher used to collect all created metrics from meter ready for exporting -# Pass in True/False to indicate whether the batcher is stateful. -# True indicates the batcher computes checkpoints from over the process -# lifetime. -# False indicates the batcher computes checkpoints which describe the updates -# of a single collection period (deltas) -batcher = UngroupedBatcher(batcher_mode == "stateful") - -# If a batcher is not provided, a default batcher is used # Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) -meter = metrics.meter() +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) + +# Meter's namespace corresponds to the string passed as the first argument Pass +# in True/False to indicate whether the batcher is stateful. True indicates the +# batcher computes checkpoints from over the process lifetime. False indicates +# the batcher computes checkpoints which describe the updates of a single +# collection period (deltas) +meter = metrics.get_meter(__name__, batcher_mode == "stateful") # Exporter to export metrics to the console exporter = ConsoleMetricsExporter() diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py deleted file mode 100644 index 2f42361902..0000000000 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -This module serves as an example for a simple application using metrics -""" - -from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Meter -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher -from opentelemetry.sdk.metrics.export.controller import PushController - -batcher = UngroupedBatcher(True) -metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) -meter = metrics.meter() -counter = meter.create_metric( - "available memory", - "available memory", - "bytes", - int, - Counter, - ("environment",), -) - -label_set = meter.get_label_set({"environment": "staging"}) - -# Direct metric usage -counter.add(25, label_set) - -# Handle usage -counter_handle = counter.get_handle(label_set) -counter_handle.add(100) - -# Record batch usage -meter.record_batch(label_set, [(counter, 50)]) - -exporter = ConsoleMetricsExporter() -controller = PushController(meter, exporter, 5) diff --git a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py index 94fea96c5b..f688347538 100644 --- a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py +++ b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py @@ -28,7 +28,7 @@ class TestPrometheusMetricExporter(unittest.TestCase): def setUp(self): - self._meter = metrics.Meter() + self._meter = metrics.MeterProvider().get_meter(__name__) self._test_metric = self._meter.create_metric( "testname", "testdesc", @@ -74,7 +74,7 @@ def test_export(self): self.assertIs(result, MetricsExportResult.SUCCESS) def test_counter_to_prometheus(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = meter.create_metric( "test@name", "testdesc", @@ -111,7 +111,7 @@ def test_counter_to_prometheus(self): def test_invalid_metric(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = meter.create_metric( "tesname", "testdesc", "unit", int, TestMetric ) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 5045c38eed..c1b330551a 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,10 +27,13 @@ """ import abc +import logging from typing import Callable, Dict, Optional, Sequence, Tuple, Type, TypeVar from opentelemetry.util import loader +logger = logging.getLogger(__name__) + ValueT = TypeVar("ValueT", int, float) @@ -224,6 +227,56 @@ def record(self, value: ValueT, label_set: LabelSet) -> None: """ +class MeterProvider(abc.ABC): + @abc.abstractmethod + def get_meter( + self, + instrumenting_module_name: str, + stateful: bool = True, + instrumenting_library_version: str = "", + ) -> "Meter": + """Returns a `Meter` for use by the given instrumentation library. + + This function may return different `Meter` types (e.g. a no-op meter + vs. a functional meter). + + Args: + instrumenting_module_name: The name of the instrumenting module + (usually just ``__name__``). + + This should *not* be the name of the module that is + instrumented but the name of the module doing the instrumentation. + E.g., instead of ``"requests"``, use + ``"opentelemetry.ext.http_requests"``. + + stateful: True/False to indicate whether the meter will be + stateful. True indicates the meter computes checkpoints + from over the process lifetime. False indicates the meter + computes checkpoints which describe the updates of a single + collection period (deltas). + + instrumenting_library_version: Optional. The version string of the + instrumenting library. Usually this should be the same as + ``pkg_resources.get_distribution(instrumenting_library_name).version``. + """ + + +class DefaultMeterProvider(MeterProvider): + """The default MeterProvider, used when no implementation is available. + + All operations are no-op. + """ + + def get_meter( + self, + instrumenting_module_name: str, + stateful: bool = True, + instrumenting_library_version: str = "", + ) -> "Meter": + # pylint:disable=no-self-use,unused-argument + return DefaultMeter() + + MetricT = TypeVar("MetricT", Counter, Gauge, Measure) @@ -322,45 +375,69 @@ def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": # Once https://github.com/python/mypy/issues/7092 is resolved, # the following type definition should be replaced with # from opentelemetry.util.loader import ImplementationFactory -ImplementationFactory = Callable[[Type[Meter]], Optional[Meter]] - -_METER = None -_METER_FACTORY = None +ImplementationFactory = Callable[ + [Type[MeterProvider]], Optional[MeterProvider] +] + +_METER_PROVIDER = None +_METER_PROVIDER_FACTORY = None + + +def get_meter( + instrumenting_module_name: str, + stateful: bool = True, + instrumenting_library_version: str = "", +) -> "Meter": + """Returns a `Meter` for use by the given instrumentation library. + This function is a convenience wrapper for + opentelemetry.metrics.meter_provider().get_meter + """ + return meter_provider().get_meter( + instrumenting_module_name, stateful, instrumenting_library_version + ) -def meter() -> Meter: - """Gets the current global :class:`~.Meter` object. +def meter_provider() -> MeterProvider: + """Gets the current global :class:`~.MeterProvider` object. If there isn't one set yet, a default will be loaded. """ - global _METER, _METER_FACTORY # pylint:disable=global-statement + global _METER_PROVIDER, _METER_PROVIDER_FACTORY # pylint:disable=global-statement - if _METER is None: + if _METER_PROVIDER is None: # pylint:disable=protected-access try: - _METER = loader._load_impl(Meter, _METER_FACTORY) # type: ignore + _METER_PROVIDER = loader._load_impl( + MeterProvider, _METER_PROVIDER_FACTORY # type: ignore + ) except TypeError: # if we raised an exception trying to instantiate an - # abstract class, default to no-op tracer impl - _METER = DefaultMeter() - del _METER_FACTORY + # abstract class, default to no-op meter impl + logger.warning( + "Unable to instantiate MeterProvider from meter provider factory.", + exc_info=True, + ) + _METER_PROVIDER = DefaultMeterProvider() + _METER_PROVIDER_FACTORY = None - return _METER + return _METER_PROVIDER -def set_preferred_meter_implementation(factory: ImplementationFactory) -> None: - """Set the factory to be used to create the meter. +def set_preferred_meter_provider_implementation( + factory: ImplementationFactory, +) -> None: + """Set the factory to be used to create the meter provider. See :mod:`opentelemetry.util.loader` for details. This function may not be called after a meter is already loaded. Args: - factory: Callback that should create a new :class:`Meter` instance. + factory: Callback that should create a new :class:`MeterProvider` instance. """ - global _METER, _METER_FACTORY # pylint:disable=global-statement + global _METER_PROVIDER_FACTORY # pylint:disable=global-statement - if _METER: - raise RuntimeError("Meter already loaded.") + if _METER_PROVIDER: + raise RuntimeError("MeterProvider already loaded.") - _METER_FACTORY = factory + _METER_PROVIDER_FACTORY = factory diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index cbf36d4c05..9bfc79df21 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -1,3 +1,16 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import time # Since we want API users to be able to provide timestamps, diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 3ec0f81c71..788ce57680 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,31 +13,11 @@ # limitations under the License. import unittest -from contextlib import contextmanager -from unittest import mock from opentelemetry import metrics # pylint: disable=no-self-use -class TestMeter(unittest.TestCase): - def setUp(self): - self.meter = metrics.DefaultMeter() - - def test_record_batch(self): - counter = metrics.Counter() - label_set = metrics.LabelSet() - self.meter.record_batch(label_set, ((counter, 1),)) - - def test_create_metric(self): - metric = self.meter.create_metric("", "", "", float, metrics.Counter) - self.assertIsInstance(metric, metrics.DefaultMetric) - - def test_get_label_set(self): - metric = self.meter.get_label_set({}) - self.assertIsInstance(metric, metrics.DefaultLabelSet) - - class TestMetrics(unittest.TestCase): def test_default(self): default = metrics.DefaultMetric() @@ -92,34 +72,3 @@ def test_gauge_handle(self): def test_measure_handle(self): handle = metrics.MeasureHandle() handle.record(1) - - -@contextmanager -# type: ignore -def patch_metrics_globals(meter=None, meter_factory=None): - """Mock metrics._METER and metrics._METER_FACTORY. - - This prevents previous changes to these values from affecting the code in - this scope, and prevents changes in this scope from leaking out and - affecting other tests. - """ - with mock.patch("opentelemetry.metrics._METER", meter): - with mock.patch("opentelemetry.metrics._METER_FACTORY", meter_factory): - yield - - -class TestGlobals(unittest.TestCase): - def test_meter_default_factory(self): - """Check that the default meter is a DefaultMeter.""" - with patch_metrics_globals(): - meter = metrics.meter() - self.assertIsInstance(meter, metrics.DefaultMeter) - # Check that we don't create a new instance on each call - self.assertIs(meter, metrics.meter()) - - def test_meter_custom_factory(self): - """Check that we use the provided factory for custom global meters.""" - mock_meter = mock.Mock(metrics.Meter) - with patch_metrics_globals(meter_factory=lambda _: mock_meter): - meter = metrics.meter() - self.assertIs(meter, mock_meter) diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index d1e1a0913a..0035803bd2 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,6 +25,8 @@ class TestAPIOnlyImplementation(unittest.TestCase): https://github.com/open-telemetry/opentelemetry-python/issues/142 """ + # TRACER + def test_tracer(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated @@ -54,12 +56,31 @@ def test_default_span(self): self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) self.assertIs(span.is_recording_events(), False) + # METER + def test_meter(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated metrics.Meter() # type:ignore def test_default_meter(self): + meter_provider = metrics.DefaultMeterProvider() + meter = meter_provider.get_meter(__name__) + self.assertIsInstance(meter, metrics.DefaultMeter) + + # pylint: disable=no-self-use + def test_record_batch(self): + meter = metrics.DefaultMeter() + counter = metrics.Counter() + label_set = metrics.LabelSet() + meter.record_batch(label_set, ((counter, 1),)) + + def test_create_metric(self): meter = metrics.DefaultMeter() metric = meter.create_metric("", "", "", float, metrics.Counter) self.assertIsInstance(metric, metrics.DefaultMetric) + + def test_get_label_set(self): + meter = metrics.DefaultMeter() + label_set = meter.get_label_set({}) + self.assertIsInstance(label_set, metrics.DefaultLabelSet) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 4c9231582c..fdf145d87c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ from opentelemetry import metrics as metrics_api from opentelemetry.sdk.metrics.export.aggregate import Aggregator from opentelemetry.sdk.metrics.export.batcher import Batcher, UngroupedBatcher +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -261,12 +262,16 @@ class Meter(metrics_api.Meter): """See `opentelemetry.metrics.Meter`. Args: - batcher: The `Batcher` used for this meter. + instrumentation_info: The `InstrumentationInfo` for this meter. + stateful: Indicates whether the meter is stateful. """ - def __init__(self, batcher: Batcher = UngroupedBatcher(True)): - self.batcher = batcher + def __init__( + self, instrumentation_info: "InstrumentationInfo", stateful: bool, + ): + self.instrumentation_info = instrumentation_info self.metrics = set() + self.batcher = UngroupedBatcher(stateful) def collect(self) -> None: """Collects all the metrics created with this `Meter` for export. @@ -328,3 +333,20 @@ def get_label_set(self, labels: Dict[str, str]): if len(labels) == 0: return EMPTY_LABEL_SET return LabelSet(labels=labels) + + +class MeterProvider(metrics_api.MeterProvider): + def get_meter( + self, + instrumenting_module_name: str, + stateful=True, + instrumenting_library_version: str = "", + ) -> "metrics_api.Meter": + if not instrumenting_module_name: # Reject empty strings too. + raise ValueError("get_meter called with missing module name.") + return Meter( + InstrumentationInfo( + instrumenting_module_name, instrumenting_library_version + ), + stateful=stateful, + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 58f4e16fc2..7f305dcc0d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -26,6 +26,7 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import util from opentelemetry.sdk.util import BoundedDict, BoundedList +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanContext, sampling from opentelemetry.trace.propagation import SPAN_KEY from opentelemetry.trace.status import Status, StatusCanonicalCode @@ -152,7 +153,7 @@ def __init__( links: Sequence[trace_api.Link] = (), kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, span_processor: SpanProcessor = SpanProcessor(), - instrumentation_info: "InstrumentationInfo" = None, + instrumentation_info: InstrumentationInfo = None, set_status_on_exception: bool = True, ) -> None: @@ -385,46 +386,6 @@ def generate_trace_id() -> int: return random.getrandbits(128) -class InstrumentationInfo: - """Immutable information about an instrumentation library module. - - See `TracerProvider.get_tracer` for the meaning of the properties. - """ - - __slots__ = ("_name", "_version") - - def __init__(self, name: str, version: str): - self._name = name - self._version = version - - def __repr__(self): - return "{}({}, {})".format( - type(self).__name__, self._name, self._version - ) - - def __hash__(self): - return hash((self._name, self._version)) - - def __eq__(self, value): - return type(value) is type(self) and (self._name, self._version) == ( - value._name, - value._version, - ) - - def __lt__(self, value): - if type(value) is not type(self): - return NotImplemented - return (self._name, self._version) < (value._name, value._version) - - @property - def version(self) -> str: - return self._version - - @property - def name(self) -> str: - return self._name - - class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py similarity index 99% rename from opentelemetry-sdk/src/opentelemetry/sdk/util.py rename to opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index 2265c29460..009a0bcdd7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import datetime import threading from collections import OrderedDict, deque diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py new file mode 100644 index 0000000000..893a6066d9 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -0,0 +1,55 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class InstrumentationInfo: + """Immutable information about an instrumentation library module. + + See `opentelemetry.trace.TracerProvider.get_tracer` or + `opentelemetry.metrics.MeterProvider.get_meter` for the meaning of these + properties. + """ + + __slots__ = ("_name", "_version") + + def __init__(self, name: str, version: str): + self._name = name + self._version = version + + def __repr__(self): + return "{}({}, {})".format( + type(self).__name__, self._name, self._version + ) + + def __hash__(self): + return hash((self._name, self._version)) + + def __eq__(self, value): + return type(value) is type(self) and (self._name, self._version) == ( + value._name, + value._version, + ) + + def __lt__(self, value): + if type(value) is not type(self): + return NotImplemented + return (self._name, self._version) < (value._name, value._version) + + @property + def version(self) -> str: + return self._version + + @property + def name(self) -> str: + return self._name diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 51d7aaaf4f..cd12bcbb6b 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ class TestConsoleMetricsExporter(unittest.TestCase): # pylint: disable=no-self-use def test_export(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) exporter = ConsoleMetricsExporter() metric = metrics.Counter( "available memory", @@ -45,7 +45,7 @@ def test_export(self): ("environment",), ) kvp = {"environment": "staging"} - label_set = meter.get_label_set(kvp) + label_set = metrics.LabelSet(kvp) aggregator = CounterAggregator() record = MetricRecord(aggregator, label_set, metric) result = '{}(data="{}", label_set="{}", value={})'.format( @@ -71,7 +71,7 @@ def test_aggregator_for_counter(self): # TODO: Add other aggregator tests def test_checkpoint_set(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() metric = metrics.Counter( @@ -99,7 +99,7 @@ def test_checkpoint_set_empty(self): self.assertEqual(len(records), 0) def test_finished_collection_stateless(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(False) aggregator = CounterAggregator() metric = metrics.Counter( @@ -119,7 +119,7 @@ def test_finished_collection_stateless(self): self.assertEqual(len(batcher._batch_map), 0) def test_finished_collection_stateful(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() metric = metrics.Counter( @@ -140,7 +140,7 @@ def test_finished_collection_stateful(self): # TODO: Abstract the logic once other batchers implemented def test_ungrouped_batcher_process_exists(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() aggregator2 = CounterAggregator() @@ -169,7 +169,7 @@ def test_ungrouped_batcher_process_exists(self): ) def test_ungrouped_batcher_process_not_exists(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() metric = metrics.Counter( @@ -196,7 +196,7 @@ def test_ungrouped_batcher_process_not_exists(self): ) def test_ungrouped_batcher_process_not_stateful(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() metric = metrics.Counter( @@ -304,9 +304,7 @@ def call_update(mmsc): def test_update(self): mmsc = MinMaxSumCountAggregator() # test current values without any update - self.assertEqual( - mmsc.current, MinMaxSumCountAggregator._EMPTY, - ) + self.assertEqual(mmsc.current, MinMaxSumCountAggregator._EMPTY) # call update with some values values = (3, 50, 3, 97) @@ -314,7 +312,7 @@ def test_update(self): mmsc.update(val) self.assertEqual( - mmsc.current, (min(values), max(values), sum(values), len(values)), + mmsc.current, (min(values), max(values), sum(values), len(values)) ) def test_checkpoint(self): @@ -322,9 +320,7 @@ def test_checkpoint(self): # take checkpoint wihtout any update mmsc.take_checkpoint() - self.assertEqual( - mmsc.checkpoint, MinMaxSumCountAggregator._EMPTY, - ) + self.assertEqual(mmsc.checkpoint, MinMaxSumCountAggregator._EMPTY) # call update with some values values = (3, 50, 3, 97) @@ -337,9 +333,7 @@ def test_checkpoint(self): (min(values), max(values), sum(values), len(values)), ) - self.assertEqual( - mmsc.current, MinMaxSumCountAggregator._EMPTY, - ) + self.assertEqual(mmsc.current, MinMaxSumCountAggregator._EMPTY) def test_merge(self): mmsc1 = MinMaxSumCountAggregator() diff --git a/opentelemetry-sdk/tests/metrics/test_implementation.py b/opentelemetry-sdk/tests/metrics/test_implementation.py new file mode 100644 index 0000000000..1fedc9ae57 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_implementation.py @@ -0,0 +1,35 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry.metrics import DefaultLabelSet, DefaultMeter, DefaultMetric +from opentelemetry.sdk import metrics + + +class TestMeterImplementation(unittest.TestCase): + """ + This test is in place to ensure the SDK implementation of the API + is returning values that are valid. The same tests have been added + to the API with different expected results. See issue for more details: + https://github.com/open-telemetry/opentelemetry-python/issues/142 + """ + + def test_meter(self): + meter = metrics.MeterProvider().get_meter(__name__) + metric = meter.create_metric("", "", "", float, metrics.Counter) + label_set = meter.get_label_set({"key1": "val1"}) + self.assertNotIsInstance(meter, DefaultMeter) + self.assertNotIsInstance(metric, DefaultMetric) + self.assertNotIsInstance(label_set, DefaultLabelSet) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index db7e2d8c85..daba171c51 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,11 +22,11 @@ class TestMeter(unittest.TestCase): def test_extends_api(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) self.assertIsInstance(meter, metrics_api.Meter) def test_collect(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher_mock = mock.Mock() meter.batcher = batcher_mock label_keys = ("key1",) @@ -41,14 +41,14 @@ def test_collect(self): self.assertTrue(batcher_mock.process.called) def test_collect_no_metrics(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher_mock = mock.Mock() meter.batcher = batcher_mock meter.collect() self.assertFalse(batcher_mock.process.called) def test_collect_disabled_metric(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher_mock = mock.Mock() meter.batcher = batcher_mock label_keys = ("key1",) @@ -63,7 +63,7 @@ def test_collect_disabled_metric(self): self.assertFalse(batcher_mock.process.called) def test_record_batch(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1",) counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys @@ -75,7 +75,7 @@ def test_record_batch(self): self.assertEqual(counter.get_handle(label_set).aggregator.current, 1.0) def test_record_batch_multiple(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1", "key2", "key3") kvp = {"key1": "value1", "key2": "value2", "key3": "value3"} label_set = meter.get_label_set(kvp) @@ -96,7 +96,7 @@ def test_record_batch_multiple(self): ) def test_record_batch_exists(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1",) kvp = {"key1": "value1"} label_set = meter.get_label_set(kvp) @@ -111,7 +111,7 @@ def test_record_batch_exists(self): self.assertEqual(handle.aggregator.current, 2.0) def test_create_metric(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) counter = meter.create_metric( "name", "desc", "unit", int, metrics.Counter, () ) @@ -120,7 +120,7 @@ def test_create_metric(self): self.assertEqual(counter.name, "name") def test_create_gauge(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) gauge = meter.create_metric( "name", "desc", "unit", float, metrics.Gauge, () ) @@ -129,7 +129,7 @@ def test_create_gauge(self): self.assertEqual(gauge.name, "name") def test_create_measure(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) measure = meter.create_metric( "name", "desc", "unit", float, metrics.Measure, () ) @@ -138,7 +138,7 @@ def test_create_measure(self): self.assertEqual(measure.name, "name") def test_get_label_set(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) kvp = {"environment": "staging", "a": "z"} label_set = meter.get_label_set(kvp) label_set2 = meter.get_label_set(kvp) @@ -146,7 +146,7 @@ def test_get_label_set(self): self.assertEqual(len(labels), 1) def test_get_label_set_empty(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) kvp = {} label_set = meter.get_label_set(kvp) self.assertEqual(label_set, metrics.EMPTY_LABEL_SET) @@ -154,7 +154,7 @@ def test_get_label_set_empty(self): class TestMetric(unittest.TestCase): def test_get_handle(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric_types = [metrics.Counter, metrics.Gauge, metrics.Measure] for _type in metric_types: metric = _type("name", "desc", "unit", int, meter, ("key",)) @@ -166,7 +166,7 @@ def test_get_handle(self): class TestCounter(unittest.TestCase): def test_add(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = metrics.Counter("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) @@ -178,7 +178,7 @@ def test_add(self): class TestGauge(unittest.TestCase): def test_set(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = metrics.Gauge("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) @@ -192,7 +192,7 @@ def test_set(self): class TestMeasure(unittest.TestCase): def test_record(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = metrics.Measure("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) diff --git a/opentelemetry-sdk/tests/test_implementation.py b/opentelemetry-sdk/tests/trace/test_implementation.py similarity index 85% rename from opentelemetry-sdk/tests/test_implementation.py rename to opentelemetry-sdk/tests/trace/test_implementation.py index 3c0cd2ba20..74d3d5a923 100644 --- a/opentelemetry-sdk/tests/test_implementation.py +++ b/opentelemetry-sdk/tests/trace/test_implementation.py @@ -14,12 +14,11 @@ import unittest -from opentelemetry.metrics import DefaultMetric -from opentelemetry.sdk import metrics, trace +from opentelemetry.sdk import trace from opentelemetry.trace import INVALID_SPAN, INVALID_SPAN_CONTEXT -class TestSDKImplementation(unittest.TestCase): +class TestTracerImplementation(unittest.TestCase): """ This test is in place to ensure the SDK implementation of the API is returning values that are valid. The same tests have been added @@ -46,8 +45,3 @@ def test_span(self): span = trace.Span("name", INVALID_SPAN_CONTEXT) self.assertEqual(span.get_context(), INVALID_SPAN_CONTEXT) self.assertIs(span.is_recording_events(), True) - - def test_meter(self): - meter = metrics.Meter() - metric = meter.create_metric("", "", "", float, metrics.Counter) - self.assertNotIsInstance(metric, DefaultMetric) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 9678ba283b..188c019acc 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -20,6 +20,7 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import trace +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import sampling from opentelemetry.trace.status import StatusCanonicalCode from opentelemetry.util import time_ns @@ -153,11 +154,10 @@ def test_instrumentation_info(self): span1 = tracer1.start_span("s1") span2 = tracer2.start_span("s2") self.assertEqual( - span1.instrumentation_info, trace.InstrumentationInfo("instr1", "") + span1.instrumentation_info, InstrumentationInfo("instr1", "") ) self.assertEqual( - span2.instrumentation_info, - trace.InstrumentationInfo("instr2", "1.3b3"), + span2.instrumentation_info, InstrumentationInfo("instr2", "1.3b3") ) self.assertEqual(span2.instrumentation_info.version, "1.3b3") @@ -177,7 +177,7 @@ def test_invalid_instrumentation_info(self): tracer1.instrumentation_info, tracer2.instrumentation_info ) self.assertIsInstance( - tracer1.instrumentation_info, trace.InstrumentationInfo + tracer1.instrumentation_info, InstrumentationInfo ) span1 = tracer1.start_span("foo") self.assertTrue(span1.is_recording_events()) From 344d72b288ee0fe618a3e93266cd07ed16cdcd81 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 2 Mar 2020 11:42:24 -0800 Subject: [PATCH 0221/1517] Prepare to host on readthedocs.org (#452) --- .readthedocs.yml | 14 ++++++++++++++ docs-requirements.txt | 10 ++++++++++ tox.ini | 12 +++++++----- 3 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 .readthedocs.yml create mode 100644 docs-requirements.txt diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000000..3dcf0e5cf6 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,14 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +version: 2 + +sphinx: + configuration: docs/conf.py + +build: + image: latest + +python: + version: 3.8 + install: + - requirements: docs-requirements.txt diff --git a/docs-requirements.txt b/docs-requirements.txt new file mode 100644 index 0000000000..ab952473a9 --- /dev/null +++ b/docs-requirements.txt @@ -0,0 +1,10 @@ +sphinx~=2.4 +sphinx-rtd-theme~=0.4 +sphinx-autodoc-typehints~=1.10.2 + +# Required by ext packages +opentracing~=2.2.0 +Deprecated>=1.2.6 +thrift>=0.10.0 +pymongo~=3.1 +flask~=1.0 diff --git a/tox.ini b/tox.ini index 23f8dbce05..3588ca6949 100644 --- a/tox.ini +++ b/tox.ini @@ -197,14 +197,16 @@ commands = [testenv:docs] deps = -c dev-requirements.txt + -c docs-requirements.txt sphinx sphinx-rtd-theme sphinx-autodoc-typehints - opentracing~=2.2.0 - Deprecated>=1.2.6 - thrift>=0.10.0 - pymongo ~= 3.1 - flask~=1.0 + # Required by ext packages + opentracing + Deprecated + thrift + pymongo + flask changedir = docs From 888bed91695da7466a27683898c4b674538289dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 2 Mar 2020 17:02:03 -0500 Subject: [PATCH 0222/1517] sdk: Implement observer instrument (#425) Observer instruments are used to capture a current set of values at a point in time [1]. This commit extends the Meter interface to allow to register an observer instrument by pasing a callback that will be executed at collection time. The logic inside collection is updated to consider these instruments and a new ObserverAggregator is implemented. [1] https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-metrics.md#observer-instruments --- examples/metrics/observer_example.py | 72 ++++++++ examples/metrics/record.py | 16 +- examples/metrics/simple_example.py | 27 ++- .../opentelemetry/ext/prometheus/__init__.py | 14 +- .../src/opentelemetry/metrics/__init__.py | 127 +++++++------ .../tests/metrics/test_metrics.py | 15 -- .../tests/test_implementation.py | 7 + .../src/opentelemetry/sdk/metrics/__init__.py | 136 ++++++++++---- .../sdk/metrics/export/aggregate.py | 29 +++ .../sdk/metrics/export/batcher.py | 5 +- .../tests/metrics/export/test_export.py | 87 +++++++++ .../tests/metrics/test_metrics.py | 168 +++++++++++------- tox.ini | 5 +- 13 files changed, 517 insertions(+), 191 deletions(-) create mode 100644 examples/metrics/observer_example.py diff --git a/examples/metrics/observer_example.py b/examples/metrics/observer_example.py new file mode 100644 index 0000000000..aff25ee476 --- /dev/null +++ b/examples/metrics/observer_example.py @@ -0,0 +1,72 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This example shows how the Observer metric instrument can be used to capture +asynchronous metrics data. +""" +import psutil + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import LabelSet, MeterProvider +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.controller import PushController + +# Configure a stateful batcher +batcher = UngroupedBatcher(stateful=True) + +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) +meter = metrics.get_meter(__name__) + +# Exporter to export metrics to the console +exporter = ConsoleMetricsExporter() + +# Configure a push controller +controller = PushController(meter=meter, exporter=exporter, interval=2) + + +# Callback to gather cpu usage +def get_cpu_usage_callback(observer): + for (number, percent) in enumerate(psutil.cpu_percent(percpu=True)): + label_set = meter.get_label_set({"cpu_number": str(number)}) + observer.observe(percent, label_set) + + +meter.register_observer( + callback=get_cpu_usage_callback, + name="cpu_percent", + description="per-cpu usage", + unit="1", + value_type=float, + label_keys=("cpu_number",), +) + + +# Callback to gather RAM memory usage +def get_ram_usage_callback(observer): + ram_percent = psutil.virtual_memory().percent + observer.observe(ram_percent, LabelSet()) + + +meter.register_observer( + callback=get_ram_usage_callback, + name="ram_percent", + description="RAM memory usage", + unit="1", + value_type=float, + label_keys=(), +) + +input("Press a key to finish...\n") diff --git a/examples/metrics/record.py b/examples/metrics/record.py index f008ff6746..a376b2aafc 100644 --- a/examples/metrics/record.py +++ b/examples/metrics/record.py @@ -32,15 +32,25 @@ exporter = ConsoleMetricsExporter() # controller collects metrics created from meter and exports it via the # exporter every interval -controller = PushController(meter, exporter, 5) +controller = PushController(meter=meter, exporter=exporter, interval=5) # Example to show how to record using the meter counter = meter.create_metric( - "requests", "number of requests", 1, int, Counter, ("environment",) + name="requests", + description="number of requests", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), ) counter2 = meter.create_metric( - "clicks", "number of clicks", 1, int, Counter, ("environment",) + name="clicks", + description="number of clicks", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), ) # Labelsets are used to identify key-values that are associated with a specific diff --git a/examples/metrics/simple_example.py b/examples/metrics/simple_example.py index 7587915517..2b8f5cfac8 100644 --- a/examples/metrics/simple_example.py +++ b/examples/metrics/simple_example.py @@ -62,15 +62,30 @@ def usage(argv): # Metric instruments allow to capture measurements requests_counter = meter.create_metric( - "requests", "number of requests", 1, int, Counter, ("environment",) + name="requests", + description="number of requests", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), ) clicks_counter = meter.create_metric( - "clicks", "number of clicks", 1, int, Counter, ("environment",) + name="clicks", + description="number of clicks", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), ) requests_size = meter.create_metric( - "requests_size", "size of requests", 1, int, Measure, ("environment",) + name="requests_size", + description="size of requests", + unit="1", + value_type=int, + metric_type=Measure, + label_keys=("environment",), ) # Labelsets are used to identify key-values that are associated with a specific @@ -82,21 +97,15 @@ def usage(argv): # Update the metric instruments using the direct calling convention requests_size.record(100, staging_label_set) requests_counter.add(25, staging_label_set) -# Sleep for 5 seconds, exported value should be 25 time.sleep(5) requests_size.record(5000, staging_label_set) requests_counter.add(50, staging_label_set) -# Exported value should be 75 time.sleep(5) requests_size.record(2, testing_label_set) requests_counter.add(35, testing_label_set) -# There should be two exported values 75 and 35, one for each labelset time.sleep(5) clicks_counter.add(5, staging_label_set) -# There should be three exported values, labelsets can be reused for different -# metrics but will be recorded seperately, 75, 35 and 5 - time.sleep(5) diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py index 5b4a17a556..ebe68e3f4d 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py @@ -24,11 +24,10 @@ REGISTRY, CollectorRegistry, CounterMetricFamily, - GaugeMetricFamily, UnknownMetricFamily, ) -from opentelemetry.metrics import Counter, Gauge, Measure, Metric +from opentelemetry.metrics import Counter, Measure, Metric from opentelemetry.sdk.metrics.export import ( MetricRecord, MetricsExporter, @@ -112,17 +111,6 @@ def _translate_to_prometheus(self, metric_record: MetricRecord): prometheus_metric.add_metric( labels=label_values, value=metric_record.aggregator.checkpoint ) - - elif isinstance(metric_record.metric, Gauge): - prometheus_metric = GaugeMetricFamily( - name=metric_name, - documentation=metric_record.metric.description, - labels=label_keys, - ) - prometheus_metric.add_metric( - labels=label_values, value=metric_record.aggregator.checkpoint - ) - # TODO: Add support for histograms when supported in OT elif isinstance(metric_record.metric, Measure): prometheus_metric = UnknownMetricFamily( diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index c1b330551a..3ba9bcad00 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -50,13 +50,6 @@ def add(self, value: ValueT) -> None: value: The value to add to the handle. """ - def set(self, value: ValueT) -> None: - """No-op implementation of `GaugeHandle` set. - - Args: - value: The value to set to the handle. - """ - def record(self, value: ValueT) -> None: """No-op implementation of `MeasureHandle` record. @@ -74,15 +67,6 @@ def add(self, value: ValueT) -> None: """ -class GaugeHandle: - def set(self, value: ValueT) -> None: - """Sets the current value of the handle to ``value``. - - Args: - value: The value to set to the handle. - """ - - class MeasureHandle: def record(self, value: ValueT) -> None: """Records the given ``value`` to this handle. @@ -124,7 +108,7 @@ def get_handle(self, label_set: LabelSet) -> "object": Handles are useful to reduce the cost of repeatedly recording a metric with a pre-defined set of label values. All metric kinds (counter, - gauge, measure) support declaring a set of required label keys. The + measure) support declaring a set of required label keys. The values corresponding to these keys should be specified in every handle. "Unspecified" label values, in cases where a handle is requested but a value was not provided are permitted. @@ -153,14 +137,6 @@ def add(self, value: ValueT, label_set: LabelSet) -> None: label_set: `LabelSet` to associate with the returned handle. """ - def set(self, value: ValueT, label_set: LabelSet) -> None: - """No-op implementation of `Gauge` set. - - Args: - value: The value to set the gauge metric to. - label_set: `LabelSet` to associate with the returned handle. - """ - def record(self, value: ValueT, label_set: LabelSet) -> None: """No-op implementation of `Measure` record. @@ -186,28 +162,6 @@ def add(self, value: ValueT, label_set: LabelSet) -> None: """ -class Gauge(Metric): - """A gauge type metric that expresses a pre-calculated value. - - Gauge metrics have a value that is either ``Set`` by explicit - instrumentation or observed through a callback. This kind of metric - should be used when the metric cannot be expressed as a sum or because - the measurement interval is arbitrary. - """ - - def get_handle(self, label_set: LabelSet) -> "GaugeHandle": - """Gets a `GaugeHandle`.""" - return GaugeHandle() - - def set(self, value: ValueT, label_set: LabelSet) -> None: - """Sets the value of the gauge to ``value``. - - Args: - value: The value to set the gauge metric to. - label_set: `LabelSet` to associate with the returned handle. - """ - - class Measure(Metric): """A measure type metric that represent raw stats that are recorded. @@ -227,6 +181,37 @@ def record(self, value: ValueT, label_set: LabelSet) -> None: """ +class Observer(abc.ABC): + """An observer type metric instrument used to capture a current set of values. + + + Observer instruments are asynchronous, a callback is invoked with the + observer instrument as argument allowing the user to capture multiple + values per collection interval. + """ + + @abc.abstractmethod + def observe(self, value: ValueT, label_set: LabelSet) -> None: + """Captures ``value`` to the observer. + + Args: + value: The value to capture to this observer metric. + label_set: `LabelSet` associated to ``value``. + """ + + +class DefaultObserver(Observer): + """No-op implementation of ``Observer``.""" + + def observe(self, value: ValueT, label_set: LabelSet) -> None: + """Captures ``value`` to the observer. + + Args: + value: The value to capture to this observer metric. + label_set: `LabelSet` associated to ``value``. + """ + + class MeterProvider(abc.ABC): @abc.abstractmethod def get_meter( @@ -277,15 +262,16 @@ def get_meter( return DefaultMeter() -MetricT = TypeVar("MetricT", Counter, Gauge, Measure) +MetricT = TypeVar("MetricT", Counter, Measure, Observer) +ObserverCallbackT = Callable[[Observer], None] # pylint: disable=unused-argument class Meter(abc.ABC): """An interface to allow the recording of metrics. - `Metric` s are used for recording pre-defined aggregation (gauge and - counter), or raw values (measure) in which the aggregation and labels + `Metric` s are used for recording pre-defined aggregation (counter), + or raw values (measure) in which the aggregation and labels for the exported metric are deferred. """ @@ -325,7 +311,8 @@ def create_metric( Args: name: The name of the metric. description: Human-readable description of the metric. - unit: Unit of the metric values. + unit: Unit of the metric values following the UCUM convention + (https://unitsofmeasure.org/ucum.html). value_type: The type of values being recorded by the metric. metric_type: The type of metric being created. label_keys: The keys for the labels with dynamic values. @@ -333,6 +320,32 @@ def create_metric( Returns: A new ``metric_type`` metric with values of ``value_type``. """ + @abc.abstractmethod + def register_observer( + self, + callback: ObserverCallbackT, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + label_keys: Sequence[str] = (), + enabled: bool = True, + ) -> "Observer": + """Registers an ``Observer`` metric instrument. + + Args: + callback: Callback invoked each collection interval with the + observer as argument. + name: The name of the metric. + description: Human-readable description of the metric. + unit: Unit of the metric values following the UCUM convention + (https://unitsofmeasure.org/ucum.html). + value_type: The type of values being recorded by the metric. + label_keys: The keys for the labels with dynamic values. + enabled: Whether to report the metric by default. + Returns: A new ``Observer`` metric instrument. + """ + @abc.abstractmethod def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": """Gets a `LabelSet` with the given labels. @@ -367,6 +380,18 @@ def create_metric( # pylint: disable=no-self-use return DefaultMetric() + def register_observer( + self, + callback: ObserverCallbackT, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + label_keys: Sequence[str] = (), + enabled: bool = True, + ) -> "Observer": + return DefaultObserver() + def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": # pylint: disable=no-self-use return DefaultLabelSet() diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 788ce57680..45913ca672 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -36,17 +36,6 @@ def test_counter_add(self): label_set = metrics.LabelSet() counter.add(1, label_set) - def test_gauge(self): - gauge = metrics.Gauge() - label_set = metrics.LabelSet() - handle = gauge.get_handle(label_set) - self.assertIsInstance(handle, metrics.GaugeHandle) - - def test_gauge_set(self): - gauge = metrics.Gauge() - label_set = metrics.LabelSet() - gauge.set(1, label_set) - def test_measure(self): measure = metrics.Measure() label_set = metrics.LabelSet() @@ -65,10 +54,6 @@ def test_counter_handle(self): handle = metrics.CounterHandle() handle.add(1) - def test_gauge_handle(self): - handle = metrics.GaugeHandle() - handle.set(1) - def test_measure_handle(self): handle = metrics.MeasureHandle() handle.record(1) diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index 0035803bd2..7271eb5139 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest +from unittest import mock from opentelemetry import metrics, trace @@ -80,6 +81,12 @@ def test_create_metric(self): metric = meter.create_metric("", "", "", float, metrics.Counter) self.assertIsInstance(metric, metrics.DefaultMetric) + def test_register_observer(self): + meter = metrics.DefaultMeter() + callback = mock.Mock() + observer = meter.register_observer(callback, "", "", "", int, (), True) + self.assertIsInstance(observer, metrics.DefaultObserver) + def test_get_label_set(self): meter = metrics.DefaultMeter() label_set = meter.get_label_set({}) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index fdf145d87c..fc0fe6ae52 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -100,13 +100,6 @@ def add(self, value: metrics_api.ValueT) -> None: self.update(value) -class GaugeHandle(metrics_api.GaugeHandle, BaseHandle): - def set(self, value: metrics_api.ValueT) -> None: - """See `opentelemetry.metrics.GaugeHandle.set`.""" - if self._validate_update(value): - self.update(value) - - class MeasureHandle(metrics_api.MeasureHandle, BaseHandle): def record(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.MeasureHandle.record`.""" @@ -158,7 +151,7 @@ def get_handle(self, label_set: LabelSet) -> BaseHandle: return handle def __repr__(self): - return '{}(name="{}", description={})'.format( + return '{}(name="{}", description="{}")'.format( type(self).__name__, self.name, self.description ) @@ -198,14 +191,24 @@ def add(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: UPDATE_FUNCTION = add -class Gauge(Metric, metrics_api.Gauge): - """See `opentelemetry.metrics.Gauge`. - """ +class Measure(Metric, metrics_api.Measure): + """See `opentelemetry.metrics.Measure`.""" + + HANDLE_TYPE = MeasureHandle + + def record(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: + """See `opentelemetry.metrics.Measure.record`.""" + self.get_handle(label_set).record(value) + + UPDATE_FUNCTION = record - HANDLE_TYPE = GaugeHandle + +class Observer(metrics_api.Observer): + """See `opentelemetry.metrics.Observer`.""" def __init__( self, + callback: metrics_api.ObserverCallbackT, name: str, description: str, unit: str, @@ -214,40 +217,59 @@ def __init__( label_keys: Sequence[str] = (), enabled: bool = True, ): - super().__init__( - name, - description, - unit, - value_type, - meter, - label_keys=label_keys, - enabled=enabled, - ) - - def set(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: - """See `opentelemetry.metrics.Gauge.set`.""" - self.get_handle(label_set).set(value) - - UPDATE_FUNCTION = set - + self.callback = callback + self.name = name + self.description = description + self.unit = unit + self.value_type = value_type + self.meter = meter + self.label_keys = label_keys + self.enabled = enabled -class Measure(Metric, metrics_api.Measure): - """See `opentelemetry.metrics.Measure`.""" + self.aggregators = {} - HANDLE_TYPE = MeasureHandle + def observe(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: + if not self.enabled: + return + if not isinstance(value, self.value_type): + logger.warning( + "Invalid value passed for %s.", self.value_type.__name__ + ) + return - def record(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: - """See `opentelemetry.metrics.Measure.record`.""" - self.get_handle(label_set).record(value) + if label_set not in self.aggregators: + # TODO: how to cleanup aggregators? + self.aggregators[label_set] = self.meter.batcher.aggregator_for( + self.__class__ + ) + aggregator = self.aggregators[label_set] + aggregator.update(value) + + def run(self) -> bool: + try: + self.callback(self) + # pylint: disable=broad-except + except Exception as exc: + logger.warning( + "Exception while executing observer callback: %s.", exc + ) + return False + return True - UPDATE_FUNCTION = record + def __repr__(self): + return '{}(name="{}", description="{}")'.format( + type(self).__name__, self.name, self.description + ) class Record: """Container class used for processing in the `Batcher`""" def __init__( - self, metric: Metric, label_set: LabelSet, aggregator: Aggregator + self, + metric: metrics_api.MetricT, + label_set: LabelSet, + aggregator: Aggregator, ): self.metric = metric self.label_set = label_set @@ -271,6 +293,7 @@ def __init__( ): self.instrumentation_info = instrumentation_info self.metrics = set() + self.observers = set() self.batcher = UngroupedBatcher(stateful) def collect(self) -> None: @@ -280,6 +303,11 @@ def collect(self) -> None: each aggregator belonging to the metrics that were created with this meter instance. """ + + self._collect_metrics() + self._collect_observers() + + def _collect_metrics(self) -> None: for metric in self.metrics: if metric.enabled: for label_set, handle in metric.handles.items(): @@ -289,6 +317,19 @@ def collect(self) -> None: # Applies different batching logic based on type of batcher self.batcher.process(record) + def _collect_observers(self) -> None: + for observer in self.observers: + if not observer.enabled: + continue + + # TODO: capture timestamp? + if not observer.run(): + continue + + for label_set, aggregator in observer.aggregators.items(): + record = Record(observer, label_set, aggregator) + self.batcher.process(record) + def record_batch( self, label_set: LabelSet, @@ -322,6 +363,29 @@ def create_metric( self.metrics.add(metric) return metric + def register_observer( + self, + callback: metrics_api.ObserverCallbackT, + name: str, + description: str, + unit: str, + value_type: Type[metrics_api.ValueT], + label_keys: Sequence[str] = (), + enabled: bool = True, + ) -> metrics_api.Observer: + ob = Observer( + callback, + name, + description, + unit, + value_type, + self, + label_keys, + enabled, + ) + self.observers.add(ob) + return ob + def get_label_set(self, labels: Dict[str, str]): """See `opentelemetry.metrics.Meter.create_metric`. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index f082cce891..5b730cc804 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -111,3 +111,32 @@ def merge(self, other): self.checkpoint = self._merge_checkpoint( self.checkpoint, other.checkpoint ) + + +class ObserverAggregator(Aggregator): + """Same as MinMaxSumCount but also with last value.""" + + _TYPE = namedtuple("minmaxsumcountlast", "min max sum count last") + + def __init__(self): + super().__init__() + self.mmsc = MinMaxSumCountAggregator() + self.current = None + self.checkpoint = self._TYPE(None, None, None, 0, None) + + def update(self, value): + self.mmsc.update(value) + self.current = value + + def take_checkpoint(self): + self.mmsc.take_checkpoint() + self.checkpoint = self._TYPE(*(self.mmsc.checkpoint + (self.current,))) + + def merge(self, other): + self.mmsc.merge(other.mmsc) + self.checkpoint = self._TYPE( + *( + self.mmsc.checkpoint + + (other.checkpoint.last or self.checkpoint.last,) + ) + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 86ddc3fcc1..f4418c6139 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -15,12 +15,13 @@ import abc from typing import Sequence, Type -from opentelemetry.metrics import Counter, Measure, MetricT +from opentelemetry.metrics import Counter, Measure, MetricT, Observer from opentelemetry.sdk.metrics.export import MetricRecord from opentelemetry.sdk.metrics.export.aggregate import ( Aggregator, CounterAggregator, MinMaxSumCountAggregator, + ObserverAggregator, ) @@ -50,6 +51,8 @@ def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: return CounterAggregator() if issubclass(metric_type, Measure): return MinMaxSumCountAggregator() + if issubclass(metric_type, Observer): + return ObserverAggregator() # TODO: Add other aggregators return CounterAggregator() diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index cd12bcbb6b..3aab1632ec 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -25,6 +25,7 @@ from opentelemetry.sdk.metrics.export.aggregate import ( CounterAggregator, MinMaxSumCountAggregator, + ObserverAggregator, ) from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.metrics.export.controller import PushController @@ -425,6 +426,92 @@ def test_concurrent_update_and_checkpoint(self): self.assertEqual(checkpoint_total, fut.result()) +class TestObserverAggregator(unittest.TestCase): + def test_update(self): + observer = ObserverAggregator() + # test current values without any update + self.assertEqual( + observer.mmsc.current, (None, None, None, 0), + ) + self.assertIsNone(observer.current) + + # call update with some values + values = (3, 50, 3, 97, 27) + for val in values: + observer.update(val) + + self.assertEqual( + observer.mmsc.current, + (min(values), max(values), sum(values), len(values)), + ) + + self.assertEqual(observer.current, values[-1]) + + def test_checkpoint(self): + observer = ObserverAggregator() + + # take checkpoint wihtout any update + observer.take_checkpoint() + self.assertEqual( + observer.checkpoint, (None, None, None, 0, None), + ) + + # call update with some values + values = (3, 50, 3, 97) + for val in values: + observer.update(val) + + observer.take_checkpoint() + self.assertEqual( + observer.checkpoint, + (min(values), max(values), sum(values), len(values), values[-1]), + ) + + def test_merge(self): + observer1 = ObserverAggregator() + observer2 = ObserverAggregator() + + mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) + mmsc_checkpoint2 = MinMaxSumCountAggregator._TYPE(1, 33, 44, 2) + + checkpoint1 = ObserverAggregator._TYPE(*(mmsc_checkpoint1 + (23,))) + + checkpoint2 = ObserverAggregator._TYPE(*(mmsc_checkpoint2 + (27,))) + + observer1.mmsc.checkpoint = mmsc_checkpoint1 + observer2.mmsc.checkpoint = mmsc_checkpoint2 + + observer1.checkpoint = checkpoint1 + observer2.checkpoint = checkpoint2 + + observer1.merge(observer2) + + self.assertEqual( + observer1.checkpoint, + ( + min(checkpoint1.min, checkpoint2.min), + max(checkpoint1.max, checkpoint2.max), + checkpoint1.sum + checkpoint2.sum, + checkpoint1.count + checkpoint2.count, + checkpoint2.last, + ), + ) + + def test_merge_with_empty(self): + observer1 = ObserverAggregator() + observer2 = ObserverAggregator() + + mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) + checkpoint1 = ObserverAggregator._TYPE(*(mmsc_checkpoint1 + (23,))) + + observer1.mmsc.checkpoint = mmsc_checkpoint1 + observer1.checkpoint = checkpoint1 + + observer1.merge(observer2) + + self.assertEqual(observer1.checkpoint, checkpoint1) + + class TestController(unittest.TestCase): def test_push_controller(self): meter = mock.Mock() diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index daba171c51..ea20cdd593 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -62,6 +62,23 @@ def test_collect_disabled_metric(self): meter.collect() self.assertFalse(batcher_mock.process.called) + def test_collect_observers(self): + meter = metrics.MeterProvider().get_meter(__name__) + batcher_mock = mock.Mock() + meter.batcher = batcher_mock + + def callback(observer): + self.assertIsInstance(observer, metrics_api.Observer) + observer.observe(45, meter.get_label_set(())) + + observer = metrics.Observer( + callback, "name", "desc", "unit", int, meter, (), True + ) + + meter.observers.add(observer) + meter.collect() + self.assertTrue(batcher_mock.process.called) + def test_record_batch(self): meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1",) @@ -82,14 +99,12 @@ def test_record_batch_multiple(self): counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) - gauge = metrics.Gauge("name", "desc", "unit", int, meter, label_keys) measure = metrics.Measure( "name", "desc", "unit", float, meter, label_keys ) - record_tuples = [(counter, 1.0), (gauge, 5), (measure, 3.0)] + record_tuples = [(counter, 1.0), (measure, 3.0)] meter.record_batch(label_set, record_tuples) self.assertEqual(counter.get_handle(label_set).aggregator.current, 1.0) - self.assertEqual(gauge.get_handle(label_set).aggregator.current, 5.0) self.assertEqual( measure.get_handle(label_set).aggregator.current, (3.0, 3.0, 3.0, 1), @@ -115,28 +130,39 @@ def test_create_metric(self): counter = meter.create_metric( "name", "desc", "unit", int, metrics.Counter, () ) - self.assertTrue(isinstance(counter, metrics.Counter)) + self.assertIsInstance(counter, metrics.Counter) self.assertEqual(counter.value_type, int) self.assertEqual(counter.name, "name") - def test_create_gauge(self): - meter = metrics.MeterProvider().get_meter(__name__) - gauge = meter.create_metric( - "name", "desc", "unit", float, metrics.Gauge, () - ) - self.assertTrue(isinstance(gauge, metrics.Gauge)) - self.assertEqual(gauge.value_type, float) - self.assertEqual(gauge.name, "name") - def test_create_measure(self): meter = metrics.MeterProvider().get_meter(__name__) measure = meter.create_metric( "name", "desc", "unit", float, metrics.Measure, () ) - self.assertTrue(isinstance(measure, metrics.Measure)) + self.assertIsInstance(measure, metrics.Measure) self.assertEqual(measure.value_type, float) self.assertEqual(measure.name, "name") + def test_register_observer(self): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = mock.Mock() + + observer = meter.register_observer( + callback, "name", "desc", "unit", int, (), True + ) + + self.assertIsInstance(observer, metrics_api.Observer) + self.assertEqual(len(meter.observers), 1) + + self.assertEqual(observer.callback, callback) + self.assertEqual(observer.name, "name") + self.assertEqual(observer.description, "desc") + self.assertEqual(observer.unit, "unit") + self.assertEqual(observer.value_type, int) + self.assertEqual(observer.label_keys, ()) + self.assertTrue(observer.enabled) + def test_get_label_set(self): meter = metrics.MeterProvider().get_meter(__name__) kvp = {"environment": "staging", "a": "z"} @@ -155,7 +181,7 @@ def test_get_label_set_empty(self): class TestMetric(unittest.TestCase): def test_get_handle(self): meter = metrics.MeterProvider().get_meter(__name__) - metric_types = [metrics.Counter, metrics.Gauge, metrics.Measure] + metric_types = [metrics.Counter, metrics.Measure] for _type in metric_types: metric = _type("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} @@ -176,20 +202,6 @@ def test_add(self): self.assertEqual(handle.aggregator.current, 5) -class TestGauge(unittest.TestCase): - def test_set(self): - meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.Gauge("name", "desc", "unit", int, meter, ("key",)) - kvp = {"key": "value"} - label_set = meter.get_label_set(kvp) - handle = metric.get_handle(label_set) - metric.set(3, label_set) - self.assertEqual(handle.aggregator.current, 3) - metric.set(2, label_set) - # TODO: Fix once other aggregators implemented - self.assertEqual(handle.aggregator.current, 5) - - class TestMeasure(unittest.TestCase): def test_record(self): meter = metrics.MeterProvider().get_meter(__name__) @@ -206,6 +218,72 @@ def test_record(self): ) +class TestObserver(unittest.TestCase): + def test_observe(self): + meter = metrics.MeterProvider().get_meter(__name__) + observer = metrics.Observer( + None, "name", "desc", "unit", int, meter, ("key",), True + ) + kvp = {"key": "value"} + label_set = meter.get_label_set(kvp) + values = (37, 42, 7, 21) + for val in values: + observer.observe(val, label_set) + self.assertEqual( + observer.aggregators[label_set].mmsc.current, + (min(values), max(values), sum(values), len(values)), + ) + + self.assertEqual(observer.aggregators[label_set].current, values[-1]) + + def test_observe_disabled(self): + meter = metrics.MeterProvider().get_meter(__name__) + observer = metrics.Observer( + None, "name", "desc", "unit", int, meter, ("key",), False + ) + kvp = {"key": "value"} + label_set = meter.get_label_set(kvp) + observer.observe(37, label_set) + self.assertEqual(len(observer.aggregators), 0) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_observe_incorrect_type(self, logger_mock): + meter = metrics.MeterProvider().get_meter(__name__) + observer = metrics.Observer( + None, "name", "desc", "unit", int, meter, ("key",), True + ) + kvp = {"key": "value"} + label_set = meter.get_label_set(kvp) + observer.observe(37.0, label_set) + self.assertEqual(len(observer.aggregators), 0) + self.assertTrue(logger_mock.warning.called) + + def test_run(self): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = mock.Mock() + observer = metrics.Observer( + callback, "name", "desc", "unit", int, meter, (), True + ) + + self.assertTrue(observer.run()) + callback.assert_called_once_with(observer) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_run_exception(self, logger_mock): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = mock.Mock() + callback.side_effect = Exception("We have a problem!") + + observer = metrics.Observer( + callback, "name", "desc", "unit", int, meter, (), True + ) + + self.assertFalse(observer.run()) + self.assertTrue(logger_mock.warning.called) + + class TestCounterHandle(unittest.TestCase): def test_add(self): aggregator = export.aggregate.CounterAggregator() @@ -237,38 +315,6 @@ def test_update(self, time_mock): self.assertEqual(handle.aggregator.current, 4.0) -# TODO: fix tests once aggregator implemented -class TestGaugeHandle(unittest.TestCase): - def test_set(self): - aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, aggregator) - handle.set(3) - self.assertEqual(handle.aggregator.current, 3) - - def test_set_disabled(self): - aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, False, aggregator) - handle.set(3) - self.assertEqual(handle.aggregator.current, 0) - - @mock.patch("opentelemetry.sdk.metrics.logger") - def test_set_incorrect_type(self, logger_mock): - aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, aggregator) - handle.set(3.0) - self.assertEqual(handle.aggregator.current, 0) - self.assertTrue(logger_mock.warning.called) - - @mock.patch("opentelemetry.sdk.metrics.time_ns") - def test_update(self, time_mock): - aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, aggregator) - time_mock.return_value = 123 - handle.update(4.0) - self.assertEqual(handle.last_update_timestamp, 123) - self.assertEqual(handle.aggregator.current, 4.0) - - class TestMeasureHandle(unittest.TestCase): def test_record(self): aggregator = export.aggregate.MinMaxSumCountAggregator() diff --git a/tox.ini b/tox.ini index 3588ca6949..0423a6cc7e 100644 --- a/tox.ini +++ b/tox.ini @@ -187,6 +187,7 @@ deps = flake8 isort black + psutil commands_pre = python scripts/eachdist.py install --editable @@ -238,7 +239,7 @@ deps = docker-compose >= 1.25.2 pymongo ~= 3.1 -changedir = +changedir = ext/opentelemetry-ext-docker-tests/tests commands_pre = @@ -246,7 +247,7 @@ commands_pre = -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/ext/opentelemetry-ext-pymongo - docker-compose up -d -commands = +commands = pytest {posargs} commands_post = From 005575e537ff97f345610c92276d6b55c32e65e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 4 Mar 2020 00:01:56 -0500 Subject: [PATCH 0223/1517] sdk: fix ConsoleSpanExporter (#455) 19d573af0275 ("Add io and formatter options to console exporter (#412)") changed the way spans are printed by using write() instead of print(). In Python 3.x sys.stdout is line-buffered, so the spans were not being printed to the console at the right timing. This commit fixes that by adding an explicit flush() call at the end of the export function , it also changes the default formatter to include a line break. To be precise, only one of the changes was needed to solve the problem, but as a matter of completness both are included, i.e, to handle the case where the formatter chosen by the user doesn't append a line break. --- .../src/opentelemetry/sdk/trace/export/__init__.py | 5 ++++- opentelemetry-sdk/tests/trace/export/test_export.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 0f96808ea8..e5d96eff9e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -14,6 +14,7 @@ import collections import logging +import os import sys import threading import typing @@ -270,7 +271,8 @@ class ConsoleSpanExporter(SpanExporter): def __init__( self, out: typing.IO = sys.stdout, - formatter: typing.Callable[[Span], str] = str, + formatter: typing.Callable[[Span], str] = lambda span: str(span) + + os.linesep, ): self.out = out self.formatter = formatter @@ -278,4 +280,5 @@ def __init__( def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: for span in spans: self.out.write(self.formatter(span)) + self.out.flush() return SpanExportResult.SUCCESS diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index e1c709719a..cedb596766 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import time import unittest from logging import WARNING @@ -288,8 +289,9 @@ def test_export(self): # pylint: disable=no-self-use span = trace.Span("span name", mock.Mock()) with mock.patch.object(exporter, "out") as mock_stdout: exporter.export([span]) - mock_stdout.write.assert_called_once_with(str(span)) + mock_stdout.write.assert_called_once_with(str(span) + os.linesep) self.assertEqual(mock_stdout.write.call_count, 1) + self.assertEqual(mock_stdout.flush.call_count, 1) def test_export_custom(self): # pylint: disable=no-self-use """Check that console exporter uses custom io, formatter.""" From a7535a1bad4fa8b77a987bee111ca8dd90c64ba1 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 5 Mar 2020 10:30:42 -0800 Subject: [PATCH 0224/1517] jaeger: Usage README Update for opentelemetry-ext-jaeger (#459) Usage docs for opentelemetry-ext-jaeger need to be updated after the change to `TracerSource` with v0.4. Looks like it was partially updated already. Users following the usage docs will currently run into this error: `AttributeError: 'Tracer' object has no attribute 'add_span_processor'` --- ext/opentelemetry-ext-jaeger/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-jaeger/README.rst b/ext/opentelemetry-ext-jaeger/README.rst index 04f7c4082b..6813fbdeee 100644 --- a/ext/opentelemetry-ext-jaeger/README.rst +++ b/ext/opentelemetry-ext-jaeger/README.rst @@ -56,7 +56,7 @@ gRPC is still not supported by this implementation. span_processor = BatchExportSpanProcessor(jaeger_exporter) # add to the tracer - tracer.add_span_processor(span_processor) + trace.tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span('foo'): print('Hello world!') From 9ed98eb9320b9064e43c3b43ee7c4990eec3657a Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 6 Mar 2020 12:44:16 -0800 Subject: [PATCH 0225/1517] api: Implementing Propagators API to use Context (#446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementing Propagators API to use Context. Moving tracecontexthttptextformat to trace/propagation, as TraceContext is specific to trace rather that broader context propagation. Using attach/detach for wsgi and flask extensions, enabling activation of the full context rather that activating of a sub component such as traces. Adding a composite propagator. Co-authored-by: Mauricio Vásquez --- .../src/opentelemetry/ext/flask/__init__.py | 11 +- .../ext/http_requests/__init__.py | 2 +- .../tests/test_requests_integration.py | 1 + .../ext/opentracing_shim/__init__.py | 14 +- .../tests/test_shim.py | 51 ++++-- .../src/opentelemetry/ext/wsgi/__init__.py | 16 +- .../context/propagation/__init__.py | 18 --- .../context/propagation/binaryformat.py | 60 ------- .../propagation/__init__.py | 18 --- .../propagation/binaryformat.py | 62 ------- .../propagation/httptextformat.py | 114 ------------- .../src/opentelemetry/propagators/__init__.py | 88 +++++++--- .../opentelemetry/propagators/composite.py | 69 ++++++++ .../trace/propagation/__init__.py | 19 ++- .../propagation/httptextformat.py | 77 +++------ .../propagation/tracecontexthttptextformat.py | 70 ++++---- .../tests/propagators/test_composite.py | 107 ++++++++++++ .../test_tracecontexthttptextformat.py | 152 ++++++++++-------- .../sdk/context/propagation/b3_format.py | 74 ++++++--- .../context/propagation/test_b3_format.py | 21 ++- 20 files changed, 533 insertions(+), 511 deletions(-) delete mode 100644 opentelemetry-api/src/opentelemetry/context/propagation/__init__.py delete mode 100644 opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py delete mode 100644 opentelemetry-api/src/opentelemetry/distributedcontext/propagation/__init__.py delete mode 100644 opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py delete mode 100644 opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py create mode 100644 opentelemetry-api/src/opentelemetry/propagators/composite.py rename opentelemetry-api/src/opentelemetry/{context => trace}/propagation/httptextformat.py (50%) rename opentelemetry-api/src/opentelemetry/{context => trace}/propagation/tracecontexthttptextformat.py (71%) create mode 100644 opentelemetry-api/tests/propagators/test_composite.py rename opentelemetry-api/tests/{context => trace}/propagation/test_tracecontexthttptextformat.py (60%) diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index aa9217c00e..b30b42d3fd 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -6,8 +6,9 @@ from flask import request as flask_request import opentelemetry.ext.wsgi as otel_wsgi -from opentelemetry import propagators, trace +from opentelemetry import context, propagators, trace from opentelemetry.ext.flask.version import __version__ +from opentelemetry.trace.propagation import get_span_from_context from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -15,6 +16,7 @@ _ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key" _ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key" _ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key" +_ENVIRON_TOKEN = "opentelemetry-flask.token" def instrument_app(flask): @@ -57,8 +59,8 @@ def _before_flask_request(): span_name = flask_request.endpoint or otel_wsgi.get_default_span_name( environ ) - parent_span = propagators.extract( - otel_wsgi.get_header_from_environ, environ + token = context.attach( + propagators.extract(otel_wsgi.get_header_from_environ, environ) ) tracer = trace.get_tracer(__name__, __version__) @@ -69,7 +71,6 @@ def _before_flask_request(): attributes["http.route"] = flask_request.url_rule.rule span = tracer.start_span( span_name, - parent_span, kind=trace.SpanKind.SERVER, attributes=attributes, start_time=environ.get(_ENVIRON_STARTTIME_KEY), @@ -78,6 +79,7 @@ def _before_flask_request(): activation.__enter__() environ[_ENVIRON_ACTIVATION_KEY] = activation environ[_ENVIRON_SPAN_KEY] = span + environ[_ENVIRON_TOKEN] = token def _teardown_flask_request(exc): @@ -95,3 +97,4 @@ def _teardown_flask_request(exc): activation.__exit__( type(exc), exc, getattr(exc, "__traceback__", None) ) + context.detach(flask_request.environ.get(_ENVIRON_TOKEN)) diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index d21ca8258c..8e4b3e2cc0 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -76,7 +76,7 @@ def instrumented_request(self, method, url, *args, **kwargs): # to access propagators. headers = kwargs.setdefault("headers", {}) - propagators.inject(tracer, type(headers).__setitem__, headers) + propagators.inject(type(headers).__setitem__, headers) result = wrapped(self, method, url, *args, **kwargs) # *** PROCEED span.set_attribute("http.status_code", result.status_code) diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index 0a61016c77..ea37cbbf1b 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -41,6 +41,7 @@ def setUp(self): self.get_tracer = self.get_tracer_patcher.start() self.span_context_manager = mock.MagicMock() self.span = mock.create_autospec(trace.Span, spec_set=True) + self.span.get_context.return_value = trace.INVALID_SPAN_CONTEXT self.span_context_manager.__enter__.return_value = self.span def setspanattr(key, value): diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index 1ba196d9e0..bd9d22678e 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -93,6 +93,10 @@ from opentelemetry.ext.opentracing_shim import util from opentelemetry.ext.opentracing_shim.version import __version__ from opentelemetry.trace import DefaultSpan +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, +) logger = logging.getLogger(__name__) @@ -677,11 +681,8 @@ def inject(self, span_context, format, carrier): propagator = propagators.get_global_httptextformat() - propagator.inject( - DefaultSpan(span_context.unwrap()), - type(carrier).__setitem__, - carrier, - ) + ctx = set_span_in_context(DefaultSpan(span_context.unwrap())) + propagator.inject(type(carrier).__setitem__, carrier, context=ctx) def extract(self, format, carrier): """Implements the ``extract`` method from the base class.""" @@ -700,6 +701,7 @@ def get_as_list(dict_object, key): return [value] if value is not None else [] propagator = propagators.get_global_httptextformat() - otel_context = propagator.extract(get_as_list, carrier) + ctx = propagator.extract(get_as_list, carrier) + otel_context = get_span_from_context(ctx).get_context() return SpanContextShim(otel_context) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index 0d099340ec..2a3fe819c9 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -16,15 +16,26 @@ # pylint:disable=no-member import time +import typing from unittest import TestCase import opentracing import opentelemetry.ext.opentracing_shim as opentracingshim from opentelemetry import propagators, trace -from opentelemetry.context.propagation.httptextformat import HTTPTextFormat +from opentelemetry.context import Context from opentelemetry.ext.opentracing_shim import util from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, +) +from opentelemetry.trace.propagation.httptextformat import ( + Getter, + HTTPTextFormat, + HTTPTextFormatT, + Setter, +) class TestShim(TestCase): @@ -49,7 +60,7 @@ def setUpClass(cls): cls._previous_propagator = propagators.get_global_httptextformat() # Set mock propagator for testing. - propagators.set_global_httptextformat(MockHTTPTextFormat) + propagators.set_global_httptextformat(MockHTTPTextFormat()) @classmethod def tearDownClass(cls): @@ -541,23 +552,37 @@ class MockHTTPTextFormat(HTTPTextFormat): TRACE_ID_KEY = "mock-traceid" SPAN_ID_KEY = "mock-spanid" - @classmethod - def extract(cls, get_from_carrier, carrier): - trace_id_list = get_from_carrier(carrier, cls.TRACE_ID_KEY) - span_id_list = get_from_carrier(carrier, cls.SPAN_ID_KEY) + def extract( + self, + get_from_carrier: Getter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + trace_id_list = get_from_carrier(carrier, self.TRACE_ID_KEY) + span_id_list = get_from_carrier(carrier, self.SPAN_ID_KEY) if not trace_id_list or not span_id_list: - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN) - return trace.SpanContext( - trace_id=int(trace_id_list[0]), span_id=int(span_id_list[0]) + return set_span_in_context( + trace.DefaultSpan( + trace.SpanContext( + trace_id=int(trace_id_list[0]), + span_id=int(span_id_list[0]), + ) + ) ) - @classmethod - def inject(cls, span, set_in_carrier, carrier): + def inject( + self, + set_in_carrier: Setter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> None: + span = get_span_from_context(context) set_in_carrier( - carrier, cls.TRACE_ID_KEY, str(span.get_context().trace_id) + carrier, self.TRACE_ID_KEY, str(span.get_context().trace_id) ) set_in_carrier( - carrier, cls.SPAN_ID_KEY, str(span.get_context().span_id) + carrier, self.SPAN_ID_KEY, str(span.get_context().span_id) ) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 37a3a0e9e0..b96fc057d1 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -22,8 +22,9 @@ import typing import wsgiref.util as wsgiref_util -from opentelemetry import propagators, trace +from opentelemetry import context, propagators, trace from opentelemetry.ext.wsgi.version import __version__ +from opentelemetry.trace.propagation import get_span_from_context from opentelemetry.trace.status import Status, StatusCanonicalCode _HTTP_VERSION_PREFIX = "HTTP/" @@ -181,12 +182,13 @@ def __call__(self, environ, start_response): start_response: The WSGI start_response callable. """ - parent_span = propagators.extract(get_header_from_environ, environ) + token = context.attach( + propagators.extract(get_header_from_environ, environ) + ) span_name = get_default_span_name(environ) span = self.tracer.start_span( span_name, - parent_span, kind=trace.SpanKind.SERVER, attributes=collect_request_attributes(environ), ) @@ -197,17 +199,20 @@ def __call__(self, environ, start_response): span, start_response ) iterable = self.wsgi(environ, start_response) - return _end_span_after_iterating(iterable, span, self.tracer) + return _end_span_after_iterating( + iterable, span, self.tracer, token + ) except: # noqa # TODO Set span status (cf. https://github.com/open-telemetry/opentelemetry-python/issues/292) span.end() + context.detach(token) raise # Put this in a subfunction to not delay the call to the wrapped # WSGI application (instrumentation should change the application # behavior as little as possible). -def _end_span_after_iterating(iterable, span, tracer): +def _end_span_after_iterating(iterable, span, tracer, token): try: with tracer.use_span(span): for yielded in iterable: @@ -217,3 +222,4 @@ def _end_span_after_iterating(iterable, span, tracer): if close: close() span.end() + context.detach(token) diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py deleted file mode 100644 index c8706281ad..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .binaryformat import BinaryFormat -from .httptextformat import HTTPTextFormat - -__all__ = ["BinaryFormat", "HTTPTextFormat"] diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py deleted file mode 100644 index 7f1a65882f..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import abc -import typing - -from opentelemetry.trace import SpanContext - - -class BinaryFormat(abc.ABC): - """API for serialization of span context into binary formats. - - This class provides an interface that enables converting span contexts - to and from a binary format. - """ - - @staticmethod - @abc.abstractmethod - def to_bytes(context: SpanContext) -> bytes: - """Creates a byte representation of a SpanContext. - - to_bytes should read values from a SpanContext and return a data - format to represent it, in bytes. - - Args: - context: the SpanContext to serialize - - Returns: - A bytes representation of the SpanContext. - - """ - - @staticmethod - @abc.abstractmethod - def from_bytes(byte_representation: bytes) -> typing.Optional[SpanContext]: - """Return a SpanContext that was represented by bytes. - - from_bytes should return back a SpanContext that was constructed from - the data serialized in the byte_representation passed. If it is not - possible to read in a proper SpanContext, return None. - - Args: - byte_representation: the bytes to deserialize - - Returns: - A bytes representation of the SpanContext if it is valid. - Otherwise return None. - - """ diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/__init__.py deleted file mode 100644 index c8706281ad..0000000000 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .binaryformat import BinaryFormat -from .httptextformat import HTTPTextFormat - -__all__ = ["BinaryFormat", "HTTPTextFormat"] diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py deleted file mode 100644 index d6d083c0da..0000000000 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import abc -import typing - -from opentelemetry.distributedcontext import DistributedContext - - -class BinaryFormat(abc.ABC): - """API for serialization of span context into binary formats. - - This class provides an interface that enables converting span contexts - to and from a binary format. - """ - - @staticmethod - @abc.abstractmethod - def to_bytes(context: DistributedContext) -> bytes: - """Creates a byte representation of a DistributedContext. - - to_bytes should read values from a DistributedContext and return a data - format to represent it, in bytes. - - Args: - context: the DistributedContext to serialize - - Returns: - A bytes representation of the DistributedContext. - - """ - - @staticmethod - @abc.abstractmethod - def from_bytes( - byte_representation: bytes, - ) -> typing.Optional[DistributedContext]: - """Return a DistributedContext that was represented by bytes. - - from_bytes should return back a DistributedContext that was constructed - from the data serialized in the byte_representation passed. If it is - not possible to read in a proper DistributedContext, return None. - - Args: - byte_representation: the bytes to deserialize - - Returns: - A bytes representation of the DistributedContext if it is valid. - Otherwise return None. - - """ diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py deleted file mode 100644 index 3e2c186283..0000000000 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import abc -import typing - -from opentelemetry.distributedcontext import DistributedContext - -Setter = typing.Callable[[object, str, str], None] -Getter = typing.Callable[[object, str], typing.List[str]] - - -class HTTPTextFormat(abc.ABC): - """API for propagation of span context via headers. - - This class provides an interface that enables extracting and injecting - span context into headers of HTTP requests. HTTP frameworks and clients - can integrate with HTTPTextFormat by providing the object containing the - headers, and a getter and setter function for the extraction and - injection of values, respectively. - - Example:: - - import flask - import requests - from opentelemetry.context.propagation import HTTPTextFormat - - PROPAGATOR = HTTPTextFormat() - - def get_header_from_flask_request(request, key): - return request.headers.get_all(key) - - def set_header_into_requests_request(request: requests.Request, - key: str, value: str): - request.headers[key] = value - - def example_route(): - distributed_context = PROPAGATOR.extract( - get_header_from_flask_request, - flask.request - ) - request_to_downstream = requests.Request( - "GET", "http://httpbin.org/get" - ) - PROPAGATOR.inject( - distributed_context, - set_header_into_requests_request, - request_to_downstream - ) - session = requests.Session() - session.send(request_to_downstream.prepare()) - - - .. _Propagation API Specification: - https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md - """ - - @abc.abstractmethod - def extract( - self, get_from_carrier: Getter, carrier: object - ) -> DistributedContext: - """Create a DistributedContext from values in the carrier. - - The extract function should retrieve values from the carrier - object using get_from_carrier, and use values to populate a - DistributedContext value and return it. - - Args: - get_from_carrier: a function that can retrieve zero - or more values from the carrier. In the case that - the value does not exist, return an empty list. - carrier: and object which contains values that are - used to construct a DistributedContext. This object - must be paired with an appropriate get_from_carrier - which understands how to extract a value from it. - Returns: - A DistributedContext with configuration found in the carrier. - - """ - - @abc.abstractmethod - def inject( - self, - context: DistributedContext, - set_in_carrier: Setter, - carrier: object, - ) -> None: - """Inject values from a DistributedContext into a carrier. - - inject enables the propagation of values into HTTP clients or - other objects which perform an HTTP request. Implementations - should use the set_in_carrier method to set values on the - carrier. - - Args: - context: The DistributedContext to read values from. - set_in_carrier: A setter function that can set values - on the carrier. - carrier: An object that a place to define HTTP headers. - Should be paired with set_in_carrier, which should - know how to set header values on the carrier. - - """ diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index 3974a4cb03..f9b537cd86 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -12,49 +12,87 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +API for propagation of context. + +Example:: + + import flask + import requests + from opentelemetry import propagators + + + PROPAGATOR = propagators.get_global_httptextformat() + + + def get_header_from_flask_request(request, key): + return request.headers.get_all(key) + + def set_header_into_requests_request(request: requests.Request, + key: str, value: str): + request.headers[key] = value + + def example_route(): + context = PROPAGATOR.extract( + get_header_from_flask_request, + flask.request + ) + request_to_downstream = requests.Request( + "GET", "http://httpbin.org/get" + ) + PROPAGATOR.inject( + set_header_into_requests_request, + request_to_downstream, + context=context + ) + session = requests.Session() + session.send(request_to_downstream.prepare()) + + +.. _Propagation API Specification: + https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md +""" + import typing -import opentelemetry.context.propagation.httptextformat as httptextformat import opentelemetry.trace as trace -from opentelemetry.context.propagation.tracecontexthttptextformat import ( +from opentelemetry.context import get_current +from opentelemetry.context.context import Context +from opentelemetry.trace.propagation import httptextformat +from opentelemetry.trace.propagation.tracecontexthttptextformat import ( TraceContextHTTPTextFormat, ) -_T = typing.TypeVar("_T") - def extract( - get_from_carrier: httptextformat.Getter[_T], carrier: _T -) -> trace.SpanContext: - """Load the parent SpanContext from values in the carrier. - - Using the specified HTTPTextFormatter, the propagator will - extract a SpanContext from the carrier. If one is found, - it will be set as the parent context of the current span. + get_from_carrier: httptextformat.Getter[httptextformat.HTTPTextFormatT], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, +) -> Context: + """ Uses the configured propagator to extract a Context from the carrier. Args: get_from_carrier: a function that can retrieve zero or more values from the carrier. In the case that the value does not exist, return an empty list. carrier: and object which contains values that are - used to construct a SpanContext. This object + used to construct a Context. This object must be paired with an appropriate get_from_carrier which understands how to extract a value from it. + context: an optional Context to use. Defaults to current + context if not set. """ - return get_global_httptextformat().extract(get_from_carrier, carrier) + return get_global_httptextformat().extract( + get_from_carrier, carrier, context + ) def inject( - tracer: trace.Tracer, - set_in_carrier: httptextformat.Setter[_T], - carrier: _T, + set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, ) -> None: - """Inject values from the current context into the carrier. - - inject enables the propagation of values into HTTP clients or - other objects which perform an HTTP request. Implementations - should use the set_in_carrier method to set values on the - carrier. + """ Uses the configured propagator to inject a Context into the carrier. Args: set_in_carrier: A setter function that can set values @@ -62,10 +100,10 @@ def inject( carrier: An object that contains a representation of HTTP headers. Should be paired with set_in_carrier, which should know how to set header values on the carrier. + context: an optional Context to use. Defaults to current + context if not set. """ - get_global_httptextformat().inject( - tracer.get_current_span(), set_in_carrier, carrier - ) + get_global_httptextformat().inject(set_in_carrier, carrier, context) _HTTP_TEXT_FORMAT = ( diff --git a/opentelemetry-api/src/opentelemetry/propagators/composite.py b/opentelemetry-api/src/opentelemetry/propagators/composite.py new file mode 100644 index 0000000000..4ec953c839 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/propagators/composite.py @@ -0,0 +1,69 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import typing + +from opentelemetry.context.context import Context +from opentelemetry.trace.propagation import httptextformat + +logger = logging.getLogger(__name__) + + +class CompositeHTTPPropagator(httptextformat.HTTPTextFormat): + """ CompositeHTTPPropagator provides a mechanism for combining multiple + propagators into a single one. + + Args: + propagators: the list of propagators to use + """ + + def __init__( + self, propagators: typing.Sequence[httptextformat.HTTPTextFormat] + ) -> None: + self._propagators = propagators + + def extract( + self, + get_from_carrier: httptextformat.Getter[ + httptextformat.HTTPTextFormatT + ], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + """ Run each of the configured propagators with the given context and carrier. + Propagators are run in the order they are configured, if multiple + propagators write the same context key, the propagator later in the list + will override previous propagators. + + See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract` + """ + for propagator in self._propagators: + context = propagator.extract(get_from_carrier, carrier, context) + return context # type: ignore + + def inject( + self, + set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> None: + """ Run each of the configured propagators with the given context and carrier. + Propagators are run in the order they are configured, if multiple + propagators write the same carrier key, the propagator later in the list + will override previous propagators. + + See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` + """ + for propagator in self._propagators: + propagator.inject(set_in_carrier, carrier, context) diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 881a74287a..90e7f9dcb3 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -13,7 +13,22 @@ # limitations under the License. from typing import Optional -from opentelemetry.trace import INVALID_SPAN_CONTEXT, Span, SpanContext +from opentelemetry import trace as trace_api +from opentelemetry.context import get_value, set_value +from opentelemetry.context.context import Context -_SPAN_CONTEXT_KEY = "extracted-span-context" SPAN_KEY = "current-span" + + +def set_span_in_context( + span: trace_api.Span, context: Optional[Context] = None +) -> Context: + ctx = set_value(SPAN_KEY, span, context=context) + return ctx + + +def get_span_from_context(context: Optional[Context] = None) -> trace_api.Span: + span = get_value(SPAN_KEY, context=context) + if not isinstance(span, trace_api.Span): + return trace_api.INVALID_SPAN + return span diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/httptextformat.py similarity index 50% rename from opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py rename to opentelemetry-api/src/opentelemetry/trace/propagation/httptextformat.py index b64a298c41..500014d738 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/httptextformat.py @@ -15,89 +15,59 @@ import abc import typing -from opentelemetry.trace import Span, SpanContext +from opentelemetry.context.context import Context -_T = typing.TypeVar("_T") +HTTPTextFormatT = typing.TypeVar("HTTPTextFormatT") -Setter = typing.Callable[[_T, str, str], None] -Getter = typing.Callable[[_T, str], typing.List[str]] +Setter = typing.Callable[[HTTPTextFormatT, str, str], None] +Getter = typing.Callable[[HTTPTextFormatT, str], typing.List[str]] class HTTPTextFormat(abc.ABC): - """API for propagation of span context via headers. - - This class provides an interface that enables extracting and injecting - span context into headers of HTTP requests. HTTP frameworks and clients + """This class provides an interface that enables extracting and injecting + context into headers of HTTP requests. HTTP frameworks and clients can integrate with HTTPTextFormat by providing the object containing the headers, and a getter and setter function for the extraction and injection of values, respectively. - Example:: - - import flask - import requests - from opentelemetry.context.propagation import HTTPTextFormat - - PROPAGATOR = HTTPTextFormat() - - - - def get_header_from_flask_request(request, key): - return request.headers.get_all(key) - - def set_header_into_requests_request(request: requests.Request, - key: str, value: str): - request.headers[key] = value - - def example_route(): - span_context = PROPAGATOR.extract( - get_header_from_flask_request, - flask.request - ) - request_to_downstream = requests.Request( - "GET", "http://httpbin.org/get" - ) - PROPAGATOR.inject( - span_context, - set_header_into_requests_request, - request_to_downstream - ) - session = requests.Session() - session.send(request_to_downstream.prepare()) - - - .. _Propagation API Specification: - https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md """ @abc.abstractmethod def extract( - self, get_from_carrier: Getter[_T], carrier: _T - ) -> SpanContext: - """Create a SpanContext from values in the carrier. + self, + get_from_carrier: Getter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + """Create a Context from values in the carrier. The extract function should retrieve values from the carrier object using get_from_carrier, and use values to populate a - SpanContext value and return it. + Context value and return it. Args: get_from_carrier: a function that can retrieve zero or more values from the carrier. In the case that the value does not exist, return an empty list. carrier: and object which contains values that are - used to construct a SpanContext. This object + used to construct a Context. This object must be paired with an appropriate get_from_carrier which understands how to extract a value from it. + context: an optional Context to use. Defaults to current + context if not set. Returns: - A SpanContext with configuration found in the carrier. + A Context with configuration found in the carrier. """ @abc.abstractmethod def inject( - self, span: Span, set_in_carrier: Setter[_T], carrier: _T + self, + set_in_carrier: Setter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, ) -> None: - """Inject values from a Span into a carrier. + """Inject values from a Context into a carrier. inject enables the propagation of values into HTTP clients or other objects which perform an HTTP request. Implementations @@ -105,11 +75,12 @@ def inject( carrier. Args: - context: The SpanContext to read values from. set_in_carrier: A setter function that can set values on the carrier. carrier: An object that a place to define HTTP headers. Should be paired with set_in_carrier, which should know how to set header values on the carrier. + context: an optional Context to use. Defaults to current + context if not set. """ diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py similarity index 71% rename from opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py rename to opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py index 0f07841eb7..28db4e4557 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py @@ -16,9 +16,12 @@ import typing import opentelemetry.trace as trace -from opentelemetry.context.propagation import httptextformat - -_T = typing.TypeVar("_T") +from opentelemetry.context.context import Context +from opentelemetry.trace.propagation import ( + get_span_from_context, + httptextformat, + set_span_in_context, +) # Keys and values are strings of up to 256 printable US-ASCII characters. # Implementations should conform to the `W3C Trace Context - Tracestate`_ @@ -59,20 +62,26 @@ class TraceContextHTTPTextFormat(httptextformat.HTTPTextFormat): ) _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) - @classmethod def extract( - cls, get_from_carrier: httptextformat.Getter[_T], carrier: _T - ) -> trace.SpanContext: - """Extracts a valid SpanContext from the carrier. + self, + get_from_carrier: httptextformat.Getter[ + httptextformat.HTTPTextFormatT + ], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + """Extracts SpanContext from the carrier. + + See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract` """ - header = get_from_carrier(carrier, cls._TRACEPARENT_HEADER_NAME) + header = get_from_carrier(carrier, self._TRACEPARENT_HEADER_NAME) if not header: - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN, context) - match = re.search(cls._TRACEPARENT_HEADER_FORMAT_RE, header[0]) + match = re.search(self._TRACEPARENT_HEADER_FORMAT_RE, header[0]) if not match: - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN, context) version = match.group(1) trace_id = match.group(2) @@ -80,16 +89,16 @@ def extract( trace_flags = match.group(4) if trace_id == "0" * 32 or span_id == "0" * 16: - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN, context) if version == "00": if match.group(5): - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN, context) if version == "ff": - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN, context) tracestate_headers = get_from_carrier( - carrier, cls._TRACESTATE_HEADER_NAME + carrier, self._TRACESTATE_HEADER_NAME ) tracestate = _parse_tracestate(tracestate_headers) @@ -99,31 +108,34 @@ def extract( trace_flags=trace.TraceFlags(trace_flags), trace_state=tracestate, ) + return set_span_in_context(trace.DefaultSpan(span_context), context) - return span_context - - @classmethod def inject( - cls, - span: trace.Span, - set_in_carrier: httptextformat.Setter[_T], - carrier: _T, + self, + set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, ) -> None: + """Injects SpanContext into the carrier. - context = span.get_context() + See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` + """ + span_context = get_span_from_context(context).get_context() - if context == trace.INVALID_SPAN_CONTEXT: + if span_context == trace.INVALID_SPAN_CONTEXT: return traceparent_string = "00-{:032x}-{:016x}-{:02x}".format( - context.trace_id, context.span_id, context.trace_flags + span_context.trace_id, + span_context.span_id, + span_context.trace_flags, ) set_in_carrier( - carrier, cls._TRACEPARENT_HEADER_NAME, traceparent_string + carrier, self._TRACEPARENT_HEADER_NAME, traceparent_string ) - if context.trace_state: - tracestate_string = _format_tracestate(context.trace_state) + if span_context.trace_state: + tracestate_string = _format_tracestate(span_context.trace_state) set_in_carrier( - carrier, cls._TRACESTATE_HEADER_NAME, tracestate_string + carrier, self._TRACESTATE_HEADER_NAME, tracestate_string ) diff --git a/opentelemetry-api/tests/propagators/test_composite.py b/opentelemetry-api/tests/propagators/test_composite.py new file mode 100644 index 0000000000..09ac0ecf68 --- /dev/null +++ b/opentelemetry-api/tests/propagators/test_composite.py @@ -0,0 +1,107 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import Mock + +from opentelemetry.propagators.composite import CompositeHTTPPropagator + + +def get_as_list(dict_object, key): + value = dict_object.get(key) + return [value] if value is not None else [] + + +def mock_inject(name, value="data"): + def wrapped(setter, carrier=None, context=None): + carrier[name] = value + + return wrapped + + +def mock_extract(name, value="context"): + def wrapped(getter, carrier=None, context=None): + new_context = context.copy() + new_context[name] = value + return new_context + + return wrapped + + +class TestCompositePropagator(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.mock_propagator_0 = Mock( + inject=mock_inject("mock-0"), extract=mock_extract("mock-0") + ) + cls.mock_propagator_1 = Mock( + inject=mock_inject("mock-1"), extract=mock_extract("mock-1") + ) + cls.mock_propagator_2 = Mock( + inject=mock_inject("mock-0", value="data2"), + extract=mock_extract("mock-0", value="context2"), + ) + + def test_no_propagators(self): + propagator = CompositeHTTPPropagator([]) + new_carrier = {} + propagator.inject(dict.__setitem__, carrier=new_carrier) + self.assertEqual(new_carrier, {}) + + context = propagator.extract( + get_as_list, carrier=new_carrier, context={} + ) + self.assertEqual(context, {}) + + def test_single_propagator(self): + propagator = CompositeHTTPPropagator([self.mock_propagator_0]) + + new_carrier = {} + propagator.inject(dict.__setitem__, carrier=new_carrier) + self.assertEqual(new_carrier, {"mock-0": "data"}) + + context = propagator.extract( + get_as_list, carrier=new_carrier, context={} + ) + self.assertEqual(context, {"mock-0": "context"}) + + def test_multiple_propagators(self): + propagator = CompositeHTTPPropagator( + [self.mock_propagator_0, self.mock_propagator_1] + ) + + new_carrier = {} + propagator.inject(dict.__setitem__, carrier=new_carrier) + self.assertEqual(new_carrier, {"mock-0": "data", "mock-1": "data"}) + + context = propagator.extract( + get_as_list, carrier=new_carrier, context={} + ) + self.assertEqual(context, {"mock-0": "context", "mock-1": "context"}) + + def test_multiple_propagators_same_key(self): + # test that when multiple propagators extract/inject the same + # key, the later propagator values are extracted/injected + propagator = CompositeHTTPPropagator( + [self.mock_propagator_0, self.mock_propagator_2] + ) + + new_carrier = {} + propagator.inject(dict.__setitem__, carrier=new_carrier) + self.assertEqual(new_carrier, {"mock-0": "data2"}) + + context = propagator.extract( + get_as_list, carrier=new_carrier, context={} + ) + self.assertEqual(context, {"mock-0": "context2"}) diff --git a/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py similarity index 60% rename from opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py rename to opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 8f283ef881..6ee4a957d2 100644 --- a/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -14,10 +14,13 @@ import typing import unittest -from unittest.mock import Mock from opentelemetry import trace -from opentelemetry.context.propagation import tracecontexthttptextformat +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, + tracecontexthttptextformat, +) FORMAT = tracecontexthttptextformat.TraceContextHTTPTextFormat() @@ -43,8 +46,8 @@ def test_no_traceparent_header(self): trace-id and parent-id that represents the current request. """ output = {} # type:typing.Dict[str, typing.List[str]] - span_context = FORMAT.extract(get_as_list, output) - self.assertTrue(isinstance(span_context, trace.SpanContext)) + span = get_span_from_context(FORMAT.extract(get_as_list, output)) + self.assertIsInstance(span.get_context(), trace.SpanContext) def test_headers_with_tracestate(self): """When there is a traceparent and tracestate header, data from @@ -55,23 +58,25 @@ def test_headers_with_tracestate(self): span_id=format(self.SPAN_ID, "016x"), ) tracestate_value = "foo=1,bar=2,baz=3" - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": [traceparent_value], - "tracestate": [tracestate_value], - }, - ) + span_context = get_span_from_context( + FORMAT.extract( + get_as_list, + { + "traceparent": [traceparent_value], + "tracestate": [tracestate_value], + }, + ) + ).get_context() self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) self.assertEqual( span_context.trace_state, {"foo": "1", "bar": "2", "baz": "3"} ) - - mock_span = Mock() - mock_span.configure_mock(**{"get_context.return_value": span_context}) output = {} # type:typing.Dict[str, str] - FORMAT.inject(mock_span, dict.__setitem__, output) + span = trace.DefaultSpan(span_context) + + ctx = set_span_in_context(span) + FORMAT.inject(dict.__setitem__, output, ctx) self.assertEqual(output["traceparent"], traceparent_value) for pair in ["foo=1", "bar=2", "baz=3"]: self.assertIn(pair, output["tracestate"]) @@ -96,16 +101,18 @@ def test_invalid_trace_id(self): Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. """ - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": [ - "00-00000000000000000000000000000000-1234567890123456-00" - ], - "tracestate": ["foo=1,bar=2,foo=3"], - }, + span = get_span_from_context( + FORMAT.extract( + get_as_list, + { + "traceparent": [ + "00-00000000000000000000000000000000-1234567890123456-00" + ], + "tracestate": ["foo=1,bar=2,foo=3"], + }, + ) ) - self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) + self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) def test_invalid_parent_id(self): """If the parent id is invalid, we must ignore the full traceparent @@ -125,16 +132,18 @@ def test_invalid_parent_id(self): Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. """ - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": [ - "00-00000000000000000000000000000000-0000000000000000-00" - ], - "tracestate": ["foo=1,bar=2,foo=3"], - }, + span = get_span_from_context( + FORMAT.extract( + get_as_list, + { + "traceparent": [ + "00-00000000000000000000000000000000-0000000000000000-00" + ], + "tracestate": ["foo=1,bar=2,foo=3"], + }, + ) ) - self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) + self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) def test_no_send_empty_tracestate(self): """If the tracestate is empty, do not set the header. @@ -145,15 +154,11 @@ def test_no_send_empty_tracestate(self): empty tracestate headers but SHOULD avoid sending them. """ output = {} # type:typing.Dict[str, str] - mock_span = Mock() - mock_span.configure_mock( - **{ - "get_context.return_value": trace.SpanContext( - self.TRACE_ID, self.SPAN_ID - ) - } + span = trace.DefaultSpan( + trace.SpanContext(self.TRACE_ID, self.SPAN_ID) ) - FORMAT.inject(mock_span, dict.__setitem__, output) + ctx = set_span_in_context(span) + FORMAT.inject(dict.__setitem__, output, ctx) self.assertTrue("traceparent" in output) self.assertFalse("tracestate" in output) @@ -165,48 +170,55 @@ def test_format_not_supported(self): If the version cannot be parsed, return an invalid trace header. """ - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": [ - "00-12345678901234567890123456789012-" - "1234567890123456-00-residue" - ], - "tracestate": ["foo=1,bar=2,foo=3"], - }, + span = get_span_from_context( + FORMAT.extract( + get_as_list, + { + "traceparent": [ + "00-12345678901234567890123456789012-" + "1234567890123456-00-residue" + ], + "tracestate": ["foo=1,bar=2,foo=3"], + }, + ) ) - self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) + self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) def test_propagate_invalid_context(self): """Do not propagate invalid trace context.""" output = {} # type:typing.Dict[str, str] - FORMAT.inject(trace.INVALID_SPAN, dict.__setitem__, output) + ctx = set_span_in_context(trace.INVALID_SPAN) + FORMAT.inject(dict.__setitem__, output, context=ctx) self.assertFalse("traceparent" in output) def test_tracestate_empty_header(self): """Test tracestate with an additional empty header (should be ignored) """ - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": [ - "00-12345678901234567890123456789012-1234567890123456-00" - ], - "tracestate": ["foo=1", ""], - }, + span = get_span_from_context( + FORMAT.extract( + get_as_list, + { + "traceparent": [ + "00-12345678901234567890123456789012-1234567890123456-00" + ], + "tracestate": ["foo=1", ""], + }, + ) ) - self.assertEqual(span_context.trace_state["foo"], "1") + self.assertEqual(span.get_context().trace_state["foo"], "1") def test_tracestate_header_with_trailing_comma(self): """Do not propagate invalid trace context. """ - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": [ - "00-12345678901234567890123456789012-1234567890123456-00" - ], - "tracestate": ["foo=1,"], - }, + span = get_span_from_context( + FORMAT.extract( + get_as_list, + { + "traceparent": [ + "00-12345678901234567890123456789012-1234567890123456-00" + ], + "tracestate": ["foo=1,"], + }, + ) ) - self.assertEqual(span_context.trace_state["foo"], "1") + self.assertEqual(span.get_context().trace_state["foo"], "1") diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py index 4da487618b..3e03c9aa02 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -15,7 +15,17 @@ import typing import opentelemetry.trace as trace -from opentelemetry.context.propagation.httptextformat import HTTPTextFormat +from opentelemetry.context import Context +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, +) +from opentelemetry.trace.propagation.httptextformat import ( + Getter, + HTTPTextFormat, + HTTPTextFormatT, + Setter, +) class B3Format(HTTPTextFormat): @@ -32,15 +42,19 @@ class B3Format(HTTPTextFormat): FLAGS_KEY = "x-b3-flags" _SAMPLE_PROPAGATE_VALUES = set(["1", "True", "true", "d"]) - @classmethod - def extract(cls, get_from_carrier, carrier): + def extract( + self, + get_from_carrier: Getter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: trace_id = format_trace_id(trace.INVALID_TRACE_ID) span_id = format_span_id(trace.INVALID_SPAN_ID) sampled = "0" flags = None single_header = _extract_first_element( - get_from_carrier(carrier, cls.SINGLE_HEADER_KEY) + get_from_carrier(carrier, self.SINGLE_HEADER_KEY) ) if single_header: # The b3 spec calls for the sampling state to be @@ -58,29 +72,29 @@ def extract(cls, get_from_carrier, carrier): elif len(fields) == 4: trace_id, span_id, sampled, _ = fields else: - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN) else: trace_id = ( _extract_first_element( - get_from_carrier(carrier, cls.TRACE_ID_KEY) + get_from_carrier(carrier, self.TRACE_ID_KEY) ) or trace_id ) span_id = ( _extract_first_element( - get_from_carrier(carrier, cls.SPAN_ID_KEY) + get_from_carrier(carrier, self.SPAN_ID_KEY) ) or span_id ) sampled = ( _extract_first_element( - get_from_carrier(carrier, cls.SAMPLED_KEY) + get_from_carrier(carrier, self.SAMPLED_KEY) ) or sampled ) flags = ( _extract_first_element( - get_from_carrier(carrier, cls.FLAGS_KEY) + get_from_carrier(carrier, self.FLAGS_KEY) ) or flags ) @@ -90,32 +104,41 @@ def extract(cls, get_from_carrier, carrier): # flag values set. Since the setting of at least one implies # the desire for some form of sampling, propagate if either # header is set to allow. - if sampled in cls._SAMPLE_PROPAGATE_VALUES or flags == "1": + if sampled in self._SAMPLE_PROPAGATE_VALUES or flags == "1": options |= trace.TraceFlags.SAMPLED - return trace.SpanContext( - # trace an span ids are encoded in hex, so must be converted - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), - trace_flags=trace.TraceFlags(options), - trace_state=trace.TraceState(), + return set_span_in_context( + trace.DefaultSpan( + trace.SpanContext( + # trace an span ids are encoded in hex, so must be converted + trace_id=int(trace_id, 16), + span_id=int(span_id, 16), + trace_flags=trace.TraceFlags(options), + trace_state=trace.TraceState(), + ) + ) ) - @classmethod - def inject(cls, span, set_in_carrier, carrier): + def inject( + self, + set_in_carrier: Setter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> None: + span = get_span_from_context(context=context) sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 set_in_carrier( - carrier, cls.TRACE_ID_KEY, format_trace_id(span.context.trace_id) + carrier, self.TRACE_ID_KEY, format_trace_id(span.context.trace_id), ) set_in_carrier( - carrier, cls.SPAN_ID_KEY, format_span_id(span.context.span_id) + carrier, self.SPAN_ID_KEY, format_span_id(span.context.span_id) ) if span.parent is not None: set_in_carrier( carrier, - cls.PARENT_SPAN_ID_KEY, + self.PARENT_SPAN_ID_KEY, format_span_id(span.parent.context.span_id), ) - set_in_carrier(carrier, cls.SAMPLED_KEY, "1" if sampled else "0") + set_in_carrier(carrier, self.SAMPLED_KEY, "1" if sampled else "0") def format_trace_id(trace_id: int) -> str: @@ -128,10 +151,9 @@ def format_span_id(span_id: int) -> str: return format(span_id, "016x") -_T = typing.TypeVar("_T") - - -def _extract_first_element(items: typing.Iterable[_T]) -> typing.Optional[_T]: +def _extract_first_element( + items: typing.Iterable[HTTPTextFormatT], +) -> typing.Optional[HTTPTextFormatT]: if items is None: return None return next(iter(items), None) diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py index ae55b02bfd..0cdda1bcd0 100644 --- a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -17,6 +17,10 @@ import opentelemetry.sdk.context.propagation.b3_format as b3_format import opentelemetry.sdk.trace as trace import opentelemetry.trace as trace_api +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, +) FORMAT = b3_format.B3Format() @@ -28,7 +32,8 @@ def get_as_list(dict_object, key): def get_child_parent_new_carrier(old_carrier): - parent_context = FORMAT.extract(get_as_list, old_carrier) + ctx = FORMAT.extract(get_as_list, old_carrier) + parent_context = get_span_from_context(ctx).get_context() parent = trace.Span("parent", parent_context) child = trace.Span( @@ -43,7 +48,8 @@ def get_child_parent_new_carrier(old_carrier): ) new_carrier = {} - FORMAT.inject(child, dict.__setitem__, new_carrier) + ctx = set_span_in_context(child) + FORMAT.inject(dict.__setitem__, new_carrier, context=ctx) return child, parent, new_carrier @@ -222,7 +228,8 @@ def test_invalid_single_header(self): invalid SpanContext. """ carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} - span_context = FORMAT.extract(get_as_list, carrier) + ctx = FORMAT.extract(get_as_list, carrier) + span_context = get_span_from_context(ctx).get_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) @@ -232,7 +239,9 @@ def test_missing_trace_id(self): FORMAT.SPAN_ID_KEY: self.serialized_span_id, FORMAT.FLAGS_KEY: "1", } - span_context = FORMAT.extract(get_as_list, carrier) + + ctx = FORMAT.extract(get_as_list, carrier) + span_context = get_span_from_context(ctx).get_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) def test_missing_span_id(self): @@ -241,5 +250,7 @@ def test_missing_span_id(self): FORMAT.TRACE_ID_KEY: self.serialized_trace_id, FORMAT.FLAGS_KEY: "1", } - span_context = FORMAT.extract(get_as_list, carrier) + + ctx = FORMAT.extract(get_as_list, carrier) + span_context = get_span_from_context(ctx).get_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) From 1f1e69905358986cef14f5dba03f9a8d1fd1de0a Mon Sep 17 00:00:00 2001 From: Dave Grochowski Date: Mon, 9 Mar 2020 13:33:28 -0400 Subject: [PATCH 0226/1517] Re-raise errors caught in opentelemetry.sdk.trace.Tracer.use_span() (#469) User raised exceptions were consumed by the context manager. --- .../src/opentelemetry/sdk/trace/__init__.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 7f305dcc0d..f7d471a78f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -524,7 +524,7 @@ def use_span( ) ) - raise + raise finally: if end_on_exit: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 188c019acc..9592d9a15e 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -679,6 +679,32 @@ def error_status_test(context): .start_as_current_span("root") ) + def test_override_error_status(self): + def error_status_test(context): + with self.assertRaises(AssertionError): + with context as root: + root.set_status( + trace_api.status.Status( + StatusCanonicalCode.UNAVAILABLE, + "Error: Unavailable", + ) + ) + raise AssertionError("unknown") + + self.assertIs( + root.status.canonical_code, StatusCanonicalCode.UNAVAILABLE + ) + self.assertEqual(root.status.description, "Error: Unavailable") + + error_status_test( + trace.TracerProvider().get_tracer(__name__).start_span("root") + ) + error_status_test( + trace.TracerProvider() + .get_tracer(__name__) + .start_as_current_span("root") + ) + def span_event_start_fmt(span_processor_name, span_name): return span_processor_name + ":" + span_name + ":start" From 9af79516cddb05b89ac5341324f56b90b985b776 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 9 Mar 2020 12:23:12 -0700 Subject: [PATCH 0227/1517] rm -f dist dir on build (#462) --- scripts/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index c66e234af0..cd857d048a 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -14,7 +14,7 @@ DISTDIR=dist ( cd $BASEDIR mkdir -p $DISTDIR - rm -r $DISTDIR/* + rm -rf $DISTDIR/* for d in opentelemetry-api/ opentelemetry-sdk/ ext/*/ ; do ( From d7d9b158eafc84dacb84344a8f68ea1fb9181526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 9 Mar 2020 14:56:23 -0500 Subject: [PATCH 0228/1517] Resources sdk (#464) Moving resources to the SDK, as per the specification. Modifying the values it accepts to int, float, str, and bool, to match the spec. Introducing an empty resource. To short-circuit the common empty situation. --- .../src/opentelemetry/resources/__init__.py | 55 ------------------- .../src/opentelemetry/resources/py.typed | 0 .../src/opentelemetry/sdk/metrics/__init__.py | 13 ++++- .../opentelemetry/sdk/resources/__init__.py | 41 ++++++++------ .../src/opentelemetry/sdk/trace/__init__.py | 8 ++- .../tests/metrics/test_metrics.py | 21 ++++++- .../{test_init.py => test_resources.py} | 42 ++++++++++++++ opentelemetry-sdk/tests/trace/test_trace.py | 16 +++++- 8 files changed, 119 insertions(+), 77 deletions(-) delete mode 100644 opentelemetry-api/src/opentelemetry/resources/__init__.py delete mode 100644 opentelemetry-api/src/opentelemetry/resources/py.typed rename opentelemetry-sdk/tests/resources/{test_init.py => test_resources.py} (55%) diff --git a/opentelemetry-api/src/opentelemetry/resources/__init__.py b/opentelemetry-api/src/opentelemetry/resources/__init__.py deleted file mode 100644 index d6a6eb64a2..0000000000 --- a/opentelemetry-api/src/opentelemetry/resources/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import abc -import typing - - -class Resource(abc.ABC): - """The interface that resources must implement.""" - - @staticmethod - @abc.abstractmethod - def create(labels: typing.Dict[str, str]) -> "Resource": - """Create a new resource. - - Args: - labels: the labels that define the resource - - Returns: - The resource with the labels in question - - """ - - @property - @abc.abstractmethod - def labels(self) -> typing.Dict[str, str]: - """Return the label dictionary associated with this resource. - - Returns: - A dictionary with the labels of the resource - - """ - - @abc.abstractmethod - def merge(self, other: typing.Optional["Resource"]) -> "Resource": - """Return a resource with the union of labels for both resources. - - Labels that exist in the main Resource take precedence unless the - label value is the empty string. - - Args: - other: The resource to merge in - - """ diff --git a/opentelemetry-api/src/opentelemetry/resources/py.typed b/opentelemetry-api/src/opentelemetry/resources/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index fc0fe6ae52..5c616b39cf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -19,6 +19,7 @@ from opentelemetry import metrics as metrics_api from opentelemetry.sdk.metrics.export.aggregate import Aggregator from opentelemetry.sdk.metrics.export.batcher import Batcher, UngroupedBatcher +from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.util import time_ns @@ -289,12 +290,16 @@ class Meter(metrics_api.Meter): """ def __init__( - self, instrumentation_info: "InstrumentationInfo", stateful: bool, + self, + instrumentation_info: "InstrumentationInfo", + stateful: bool, + resource: Resource = Resource.create_empty(), ): self.instrumentation_info = instrumentation_info self.metrics = set() self.observers = set() self.batcher = UngroupedBatcher(stateful) + self.resource = resource def collect(self) -> None: """Collects all the metrics created with this `Meter` for export. @@ -400,6 +405,11 @@ def get_label_set(self, labels: Dict[str, str]): class MeterProvider(metrics_api.MeterProvider): + def __init__( + self, resource: Resource = Resource.create_empty(), + ): + self.resource = resource + def get_meter( self, instrumenting_module_name: str, @@ -413,4 +423,5 @@ def get_meter( instrumenting_module_name, instrumenting_library_version ), stateful=stateful, + resource=self.resource, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index b488c0a0c7..05c015de68 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -12,28 +12,34 @@ # See the License for the specific language governing permissions and # limitations under the License. -import opentelemetry.resources as resources +import typing +LabelValue = typing.Union[str, bool, int, float] +Labels = typing.Dict[str, LabelValue] -class Resource(resources.Resource): - def __init__(self, labels): - self._labels = labels + +class Resource: + def __init__(self, labels: Labels): + self._labels = labels.copy() @staticmethod - def create(labels): + def create(labels: Labels) -> "Resource": + if not labels: + return _EMPTY_RESOURCE return Resource(labels) + @staticmethod + def create_empty() -> "Resource": + return _EMPTY_RESOURCE + @property - def labels(self): - return self._labels - - def merge(self, other): - if other is None: - return self - if not self._labels: - return other - merged_labels = self.labels.copy() - for key, value in other.labels.items(): + def labels(self) -> Labels: + return self._labels.copy() + + def merge(self, other: "Resource") -> "Resource": + merged_labels = self.labels + # pylint: disable=protected-access + for key, value in other._labels.items(): if key not in merged_labels or merged_labels[key] == "": merged_labels[key] = value return Resource(merged_labels) @@ -41,4 +47,7 @@ def merge(self, other): def __eq__(self, other: object) -> bool: if not isinstance(other, Resource): return False - return self.labels == other.labels + return self._labels == other._labels + + +_EMPTY_RESOURCE = Resource({}) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index f7d471a78f..2891101c97 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -25,6 +25,7 @@ from opentelemetry import context as context_api from opentelemetry import trace as trace_api from opentelemetry.sdk import util +from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util import BoundedDict, BoundedList from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanContext, sampling @@ -127,7 +128,7 @@ class Span(trace_api.Span): remote, null if this is a root span sampler: The sampler used to create this span trace_config: TODO - resource: TODO + resource: Entity producing telemetry attributes: The span's attributes to be exported events: Timestamped events to be exported links: Links to other spans to be exported @@ -147,7 +148,7 @@ def __init__( parent: trace_api.ParentSpan = None, sampler: Optional[sampling.Sampler] = None, trace_config: None = None, # TODO - resource: None = None, # TODO + resource: None = None, attributes: types.Attributes = None, # TODO events: Sequence[trace_api.Event] = None, # TODO links: Sequence[trace_api.Link] = (), @@ -486,6 +487,7 @@ def start_span( # pylint: disable=too-many-locals context=context, parent=parent, sampler=self.source.sampler, + resource=self.source.resource, attributes=span_attributes, span_processor=self.source._active_span_processor, # pylint:disable=protected-access kind=kind, @@ -535,9 +537,11 @@ class TracerProvider(trace_api.TracerProvider): def __init__( self, sampler: sampling.Sampler = trace_api.sampling.ALWAYS_ON, + resource: Resource = Resource.create_empty(), shutdown_on_exit: bool = True, ): self._active_span_processor = MultiSpanProcessor() + self.resource = resource self.sampler = sampler self._atexit_handler = None if shutdown_on_exit: diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index ea20cdd593..6fcba4de63 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -16,10 +16,24 @@ from unittest import mock from opentelemetry import metrics as metrics_api -from opentelemetry.sdk import metrics +from opentelemetry.sdk import metrics, resources from opentelemetry.sdk.metrics import export +class TestMeterProvider(unittest.TestCase): + def test_resource(self): + resource = resources.Resource.create({}) + meter_provider = metrics.MeterProvider(resource=resource) + meter = meter_provider.get_meter(__name__) + self.assertIs(meter.resource, resource) + + def test_resource_empty(self): + meter_provider = metrics.MeterProvider() + meter = meter_provider.get_meter(__name__) + # pylint: disable=protected-access + self.assertIs(meter.resource, resources._EMPTY_RESOURCE) + + class TestMeter(unittest.TestCase): def test_extends_api(self): meter = metrics.MeterProvider().get_meter(__name__) @@ -126,13 +140,16 @@ def test_record_batch_exists(self): self.assertEqual(handle.aggregator.current, 2.0) def test_create_metric(self): - meter = metrics.MeterProvider().get_meter(__name__) + resource = mock.Mock(spec=resources.Resource) + meter_provider = metrics.MeterProvider(resource=resource) + meter = meter_provider.get_meter(__name__) counter = meter.create_metric( "name", "desc", "unit", int, metrics.Counter, () ) self.assertIsInstance(counter, metrics.Counter) self.assertEqual(counter.value_type, int) self.assertEqual(counter.name, "name") + self.assertIs(counter.meter.resource, resource) def test_create_measure(self): meter = metrics.MeterProvider().get_meter(__name__) diff --git a/opentelemetry-sdk/tests/resources/test_init.py b/opentelemetry-sdk/tests/resources/test_resources.py similarity index 55% rename from opentelemetry-sdk/tests/resources/test_init.py rename to opentelemetry-sdk/tests/resources/test_resources.py index 2afe17e563..16cf29057c 100644 --- a/opentelemetry-sdk/tests/resources/test_init.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -12,12 +12,35 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=protected-access + import unittest from opentelemetry.sdk import resources class TestResources(unittest.TestCase): + def test_create(self): + labels = { + "service": "ui", + "version": 1, + "has_bugs": True, + "cost": 112.12, + } + + resource = resources.Resource.create(labels) + self.assertIsInstance(resource, resources.Resource) + self.assertEqual(resource.labels, labels) + + resource = resources.Resource.create_empty() + self.assertIs(resource, resources._EMPTY_RESOURCE) + + resource = resources.Resource.create(None) + self.assertIs(resource, resources._EMPTY_RESOURCE) + + resource = resources.Resource.create({}) + self.assertIs(resource, resources._EMPTY_RESOURCE) + def test_resource_merge(self): left = resources.Resource({"service": "ui"}) right = resources.Resource({"host": "service-host"}) @@ -41,3 +64,22 @@ def test_resource_merge_empty_string(self): left.merge(right), resources.Resource({"service": "ui", "host": "service-host"}), ) + + def test_immutability(self): + labels = { + "service": "ui", + "version": 1, + "has_bugs": True, + "cost": 112.12, + } + + labels_copy = labels.copy() + + resource = resources.Resource.create(labels) + self.assertEqual(resource.labels, labels_copy) + + resource.labels["has_bugs"] = False + self.assertEqual(resource.labels, labels_copy) + + labels["cost"] = 999.91 + self.assertEqual(resource.labels, labels_copy) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 9592d9a15e..5d59f6998d 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -19,7 +19,7 @@ from unittest import mock from opentelemetry import trace as trace_api -from opentelemetry.sdk import trace +from opentelemetry.sdk import resources, trace from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import sampling from opentelemetry.trace.status import StatusCanonicalCode @@ -364,6 +364,20 @@ def test_start_as_current_span_explicit(self): self.assertIs(tracer.get_current_span(), root) self.assertIsNotNone(child.end_time) + def test_explicit_span_resource(self): + resource = resources.Resource.create({}) + tracer_provider = trace.TracerProvider(resource=resource) + tracer = tracer_provider.get_tracer(__name__) + span = tracer.start_span("root") + self.assertIs(span.resource, resource) + + def test_default_span_resource(self): + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) + span = tracer.start_span("root") + # pylint: disable=protected-access + self.assertIs(span.resource, resources._EMPTY_RESOURCE) + class TestSpan(unittest.TestCase): def setUp(self): From 9850fb3a9bedbef7a3ce164c95bd743eab23992b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 10 Mar 2020 00:14:36 -0500 Subject: [PATCH 0229/1517] sdk: Improve attributes validation (#460) 4fca8c985769 ("Add runtime validation in setAttribute (#348)") added a robust attribute validation using numbers.Number to validate numeric types. Although the approach is correct, it presents some complications because Complex, Fraction and Decimal are accepted because they are Numbers. This presents a problem to the exporters because they will have to consider all these different cases when converting attributes to the underlying exporter representation. This commit simplifies the logic by accepting only int and float as numeric values. --- .../src/opentelemetry/sdk/trace/__init__.py | 12 ++++++------ opentelemetry-sdk/tests/trace/test_trace.py | 19 ++++++++++++++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 2891101c97..56b1039012 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -18,7 +18,6 @@ import random import threading from contextlib import contextmanager -from numbers import Number from types import TracebackType from typing import Iterator, Optional, Sequence, Tuple, Type @@ -234,12 +233,16 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: logger.warning("Setting attribute on ended span.") return + if not key: + logger.warning("invalid key (empty or null)") + return + if isinstance(value, Sequence): error_message = self._check_attribute_value_sequence(value) if error_message is not None: logger.warning("%s in attribute value sequence", error_message) return - elif not isinstance(value, (bool, str, Number, Sequence)): + elif not isinstance(value, (bool, str, int, float)): logger.warning("invalid type for attribute value") return @@ -255,10 +258,7 @@ def _check_attribute_value_sequence(sequence: Sequence) -> Optional[str]: first_element_type = type(sequence[0]) - if issubclass(first_element_type, Number): - first_element_type = Number - - if first_element_type not in (bool, str, Number): + if first_element_type not in (bool, str, int, float): return "invalid type" for element in sequence: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 5d59f6998d..a0e22f9311 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -405,7 +405,7 @@ def test_attributes(self): root.set_attribute("empty-list", []) root.set_attribute("list-of-bools", [True, True, False]) - root.set_attribute("list-of-numerics", [123, 3.14, 0]) + root.set_attribute("list-of-numerics", [123, 314, 0]) self.assertEqual(len(root.attributes), 10) self.assertEqual(root.attributes["component"], "http") @@ -423,7 +423,7 @@ def test_attributes(self): root.attributes["list-of-bools"], [True, True, False] ) self.assertEqual( - root.attributes["list-of-numerics"], [123, 3.14, 0] + root.attributes["list-of-numerics"], [123, 314, 0] ) attributes = { @@ -454,6 +454,9 @@ def test_invalid_attribute_values(self): "list-with-non-primitive-data-type", [dict(), 123] ) + root.set_attribute("", 123) + root.set_attribute(None, 123) + self.assertEqual(len(root.attributes), 0) def test_check_sequence_helper(self): @@ -472,8 +475,18 @@ def test_check_sequence_helper(self): ), "different type", ) + self.assertEqual( + trace.Span._check_attribute_value_sequence([1, 2, 3.4, 5]), + "different type", + ) + self.assertIsNone( + trace.Span._check_attribute_value_sequence([1, 2, 3, 5]) + ) + self.assertIsNone( + trace.Span._check_attribute_value_sequence([1.2, 2.3, 3.4, 4.5]) + ) self.assertIsNone( - trace.Span._check_attribute_value_sequence([1, 2, 3.4, 5]) + trace.Span._check_attribute_value_sequence([True, False]) ) self.assertIsNone( trace.Span._check_attribute_value_sequence(["ss", "dw", "fw"]) From d5f3a7ff4ebc23c040787941ade35bd98b9487f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 10 Mar 2020 11:50:16 -0500 Subject: [PATCH 0230/1517] Improve docs structure (#467) - Creates a tree structure for documentation, it allows to organize it better from a developer point of view and also the rendered documentation should be easier to navigate. - Moves partially the main readme to be included in the online docs, the main readme will be updated to have a link to avoid duplicated content) - Moves the examples folder to the docs, so they can be accessed through the online documentation. Creates a new pair of "macros" to create links to specific versions, scm_web & scm_raw_web. Co-authored-by: Chris Kleinknecht --- docs-requirements.txt | 3 + docs/api/api.rst | 12 ++ .../context.context.rst} | 0 .../context.rst} | 2 +- .../metrics.rst} | 0 .../trace.rst} | 4 +- .../trace.sampling.rst} | 0 .../trace.status.rst} | 0 .../util.loader.rst} | 0 docs/conf.py | 30 ++++- docs/examples/basic_tracer/README.rst | 76 ++++++++++++ .../examples}/basic_tracer/__init__.py | 0 .../basic_tracer/docker/collector-config.yaml | 0 .../basic_tracer/docker/docker-compose.yaml | 0 .../examples}/basic_tracer/tests/__init__.py | 0 .../basic_tracer/tests/test_tracer.py | 0 .../examples}/basic_tracer/tracer.py | 0 docs/examples/http/README.rst | 87 ++++++++++++++ {examples => docs/examples}/http/__init__.py | 0 .../examples}/http/requirements.txt | 0 {examples => docs/examples}/http/server.py | 0 .../examples}/http/tests/__init__.py | 0 .../examples}/http/tests/test_http.py | 0 .../examples}/http/tracer_client.py | 0 .../examples}/metrics/observer_example.py | 0 .../examples}/metrics/prometheus.py | 0 {examples => docs/examples}/metrics/record.py | 0 .../examples}/metrics/simple_example.py | 0 .../opentelemetry-example-app/README.rst | 0 .../opentelemetry-example-app/setup.py | 0 .../src/opentelemetry_example_app/__init__.py | 0 .../flask_example.py | 0 .../tests/__init__.py | 0 .../tests/test_flask_example.py | 0 docs/examples/opentracing/README.rst | 97 +++++++++++++++ .../examples}/opentracing/__init__.py | 0 .../images/jaeger-span-expanded.png | Bin .../opentracing/images/jaeger-trace-full.png | Bin .../examples}/opentracing/main.py | 0 .../examples}/opentracing/rediscache.py | 0 .../examples}/opentracing/requirements.txt | 0 docs/ext/dbapi/dbapi.rst | 10 ++ .../flask/flask.rst} | 4 +- .../http_requests/http_requests.rst} | 3 +- docs/ext/jaeger/jaeger.rst | 17 +++ docs/ext/mysql/mysql.rst | 10 ++ .../ext/opentracing_shim/opentracing_shim.rst | 5 + docs/ext/otcollector/otcollector.rst | 10 ++ docs/ext/prometheus/prometheus.rst | 10 ++ docs/ext/psycopg2/psycopg2.rst | 10 ++ .../pymongo/pymongo.rst} | 3 +- .../wsgi/wsgi.rst} | 4 +- docs/ext/zipkin/zipkin.rst | 10 ++ docs/index.rst | 113 +++++++++++++----- docs/metrics_example.py | 23 ++++ ...telemetry.ext.jaeger.gen.jaeger.ttypes.rst | 11 -- docs/opentelemetry.ext.jaeger.rst | 19 --- docs/opentelemetry.ext.opentracing_shim.rst | 8 -- .../context.rst} | 0 .../metrics.export.aggregate.rst} | 0 .../metrics.export.batcher.rst} | 2 +- .../metrics.export.rst} | 0 .../metrics.rst} | 6 +- docs/sdk/sdk.rst | 11 ++ .../trace.export.rst} | 0 .../trace.rst} | 4 +- .../util.instrumentation.rst} | 0 docs/trace_example.py | 18 +++ examples/README.md | 11 -- examples/basic_tracer/README.md | 82 ------------- .../basic_tracer/images/jaeger-ui-detail.png | Bin 123383 -> 0 bytes examples/basic_tracer/images/jaeger-ui.png | Bin 256630 -> 0 bytes examples/http/README.md | 80 ------------- examples/http/images/jaeger-ui-detail.png | Bin 144383 -> 0 bytes examples/http/images/jaeger-ui.png | Bin 234643 -> 0 bytes examples/opentracing/README.md | 90 -------------- .../src/opentelemetry/sdk/trace/__init__.py | 26 ++-- scripts/coverage.sh | 2 +- tox.ini | 19 +-- 79 files changed, 560 insertions(+), 372 deletions(-) create mode 100644 docs/api/api.rst rename docs/{opentelemetry.context.context.rst => api/context.context.rst} (100%) rename docs/{opentelemetry.context.rst => api/context.rst} (83%) rename docs/{opentelemetry.metrics.rst => api/metrics.rst} (100%) rename docs/{opentelemetry.trace.rst => api/trace.rst} (72%) rename docs/{opentelemetry.trace.sampling.rst => api/trace.sampling.rst} (100%) rename docs/{opentelemetry.trace.status.rst => api/trace.status.rst} (100%) rename docs/{opentelemetry.util.loader.rst => api/util.loader.rst} (100%) create mode 100644 docs/examples/basic_tracer/README.rst rename {examples => docs/examples}/basic_tracer/__init__.py (100%) rename {examples => docs/examples}/basic_tracer/docker/collector-config.yaml (100%) rename {examples => docs/examples}/basic_tracer/docker/docker-compose.yaml (100%) rename {examples => docs/examples}/basic_tracer/tests/__init__.py (100%) rename {examples => docs/examples}/basic_tracer/tests/test_tracer.py (100%) rename {examples => docs/examples}/basic_tracer/tracer.py (100%) create mode 100644 docs/examples/http/README.rst rename {examples => docs/examples}/http/__init__.py (100%) rename {examples => docs/examples}/http/requirements.txt (100%) rename {examples => docs/examples}/http/server.py (100%) rename {examples => docs/examples}/http/tests/__init__.py (100%) rename {examples => docs/examples}/http/tests/test_http.py (100%) rename {examples => docs/examples}/http/tracer_client.py (100%) rename {examples => docs/examples}/metrics/observer_example.py (100%) rename {examples => docs/examples}/metrics/prometheus.py (100%) rename {examples => docs/examples}/metrics/record.py (100%) rename {examples => docs/examples}/metrics/simple_example.py (100%) rename {examples => docs/examples}/opentelemetry-example-app/README.rst (100%) rename {examples => docs/examples}/opentelemetry-example-app/setup.py (100%) rename {examples => docs/examples}/opentelemetry-example-app/src/opentelemetry_example_app/__init__.py (100%) rename {examples => docs/examples}/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py (100%) rename {examples => docs/examples}/opentelemetry-example-app/tests/__init__.py (100%) rename {examples => docs/examples}/opentelemetry-example-app/tests/test_flask_example.py (100%) create mode 100644 docs/examples/opentracing/README.rst rename {examples => docs/examples}/opentracing/__init__.py (100%) rename {examples => docs/examples}/opentracing/images/jaeger-span-expanded.png (100%) rename {examples => docs/examples}/opentracing/images/jaeger-trace-full.png (100%) rename {examples => docs/examples}/opentracing/main.py (100%) rename {examples => docs/examples}/opentracing/rediscache.py (100%) rename {examples => docs/examples}/opentracing/requirements.txt (100%) create mode 100644 docs/ext/dbapi/dbapi.rst rename docs/{opentelemetry.ext.flask.rst => ext/flask/flask.rst} (63%) rename docs/{opentelemetry.ext.http_requests.rst => ext/http_requests/http_requests.rst} (62%) create mode 100644 docs/ext/jaeger/jaeger.rst create mode 100644 docs/ext/mysql/mysql.rst create mode 100644 docs/ext/opentracing_shim/opentracing_shim.rst create mode 100644 docs/ext/otcollector/otcollector.rst create mode 100644 docs/ext/prometheus/prometheus.rst create mode 100644 docs/ext/psycopg2/psycopg2.rst rename docs/{opentelemetry.ext.pymongo.rst => ext/pymongo/pymongo.rst} (63%) rename docs/{opentelemetry.ext.wsgi.rst => ext/wsgi/wsgi.rst} (63%) create mode 100644 docs/ext/zipkin/zipkin.rst create mode 100644 docs/metrics_example.py delete mode 100644 docs/opentelemetry.ext.jaeger.gen.jaeger.ttypes.rst delete mode 100644 docs/opentelemetry.ext.jaeger.rst delete mode 100644 docs/opentelemetry.ext.opentracing_shim.rst rename docs/{opentelemetry.sdk.context.rst => sdk/context.rst} (100%) rename docs/{opentelemetry.sdk.metrics.export.aggregate.rst => sdk/metrics.export.aggregate.rst} (100%) rename docs/{opentelemetry.sdk.metrics.export.batcher.rst => sdk/metrics.export.batcher.rst} (85%) rename docs/{opentelemetry.sdk.metrics.export.rst => sdk/metrics.export.rst} (100%) rename docs/{opentelemetry.sdk.metrics.rst => sdk/metrics.rst} (61%) create mode 100644 docs/sdk/sdk.rst rename docs/{opentelemetry.sdk.trace.export.rst => sdk/trace.export.rst} (100%) rename docs/{opentelemetry.sdk.trace.rst => sdk/trace.rst} (73%) rename docs/{opentelemetry.sdk.util.instrumentation.rst => sdk/util.instrumentation.rst} (100%) create mode 100644 docs/trace_example.py delete mode 100644 examples/README.md delete mode 100644 examples/basic_tracer/README.md delete mode 100644 examples/basic_tracer/images/jaeger-ui-detail.png delete mode 100644 examples/basic_tracer/images/jaeger-ui.png delete mode 100644 examples/http/README.md delete mode 100644 examples/http/images/jaeger-ui-detail.png delete mode 100644 examples/http/images/jaeger-ui.png delete mode 100644 examples/opentracing/README.md diff --git a/docs-requirements.txt b/docs-requirements.txt index ab952473a9..41600cfe8c 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -8,3 +8,6 @@ Deprecated>=1.2.6 thrift>=0.10.0 pymongo~=3.1 flask~=1.0 +mysql-connector-python ~= 8.0 +wrapt >= 1.0.0, < 2.0.0 +psycopg2-binary >= 2.7.3.1 diff --git a/docs/api/api.rst b/docs/api/api.rst new file mode 100644 index 0000000000..3b15cd0d36 --- /dev/null +++ b/docs/api/api.rst @@ -0,0 +1,12 @@ +OpenTelemetry Python API +======================== + +.. TODO: what is the API + +.. toctree:: + :maxdepth: 1 + + context + metrics + trace + util.loader \ No newline at end of file diff --git a/docs/opentelemetry.context.context.rst b/docs/api/context.context.rst similarity index 100% rename from docs/opentelemetry.context.context.rst rename to docs/api/context.context.rst diff --git a/docs/opentelemetry.context.rst b/docs/api/context.rst similarity index 83% rename from docs/opentelemetry.context.rst rename to docs/api/context.rst index 2b25793458..7aef5ffe7d 100644 --- a/docs/opentelemetry.context.rst +++ b/docs/api/context.rst @@ -6,7 +6,7 @@ Submodules .. toctree:: - opentelemetry.context.context + context.context Module contents --------------- diff --git a/docs/opentelemetry.metrics.rst b/docs/api/metrics.rst similarity index 100% rename from docs/opentelemetry.metrics.rst rename to docs/api/metrics.rst diff --git a/docs/opentelemetry.trace.rst b/docs/api/trace.rst similarity index 72% rename from docs/opentelemetry.trace.rst rename to docs/api/trace.rst index bfef5839bf..00823aa036 100644 --- a/docs/opentelemetry.trace.rst +++ b/docs/api/trace.rst @@ -6,8 +6,8 @@ Submodules .. toctree:: - opentelemetry.trace.sampling - opentelemetry.trace.status + trace.sampling + trace.status Module contents --------------- diff --git a/docs/opentelemetry.trace.sampling.rst b/docs/api/trace.sampling.rst similarity index 100% rename from docs/opentelemetry.trace.sampling.rst rename to docs/api/trace.sampling.rst diff --git a/docs/opentelemetry.trace.status.rst b/docs/api/trace.status.rst similarity index 100% rename from docs/opentelemetry.trace.status.rst rename to docs/api/trace.status.rst diff --git a/docs/opentelemetry.util.loader.rst b/docs/api/util.loader.rst similarity index 100% rename from docs/opentelemetry.util.loader.rst rename to docs/api/util.loader.rst diff --git a/docs/conf.py b/docs/conf.py index aa0b4096bc..a509f14f5d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,8 +30,8 @@ # -- Project information ----------------------------------------------------- -project = "OpenTelemetry" -copyright = "2019, OpenTelemetry Authors" # pylint: disable=redefined-builtin +project = "OpenTelemetry Python" +copyright = "OpenTelemetry Authors" # pylint: disable=redefined-builtin author = "OpenTelemetry Authors" @@ -57,6 +57,8 @@ # Add a .nojekyll file to the generated HTML docs # https://help.github.com/en/articles/files-that-start-with-an-underscore-are-missing "sphinx.ext.githubpages", + # Support external links to different versions in the Github repo + "sphinx.ext.extlinks", ] intersphinx_mapping = { @@ -106,3 +108,27 @@ # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] + +# Support external links to specific versions of the files in the Github repo +branch = os.environ.get("READTHEDOCS_VERSION") +if branch is None or branch == "latest": + branch = "master" + +REPO = "open-telemetry/opentelemetry-python/" +scm_raw_web = "https://raw.githubusercontent.com/" + REPO + branch +scm_web = "https://github.com/" + REPO + "blob/" + branch + +# Store variables in the epilogue so they are globally available. +rst_epilog = """ +.. |SCM_WEB| replace:: {s} +.. |SCM_RAW_WEB| replace:: {sr} +.. |SCM_BRANCH| replace:: {b} +""".format( + s=scm_web, sr=scm_raw_web, b=branch +) + +# used to have links to repo files +extlinks = { + "scm_raw_web": (scm_raw_web + "/%s", "scm_raw_web"), + "scm_web": (scm_web + "/%s", "scm_web"), +} diff --git a/docs/examples/basic_tracer/README.rst b/docs/examples/basic_tracer/README.rst new file mode 100644 index 0000000000..2cc2d0c6a6 --- /dev/null +++ b/docs/examples/basic_tracer/README.rst @@ -0,0 +1,76 @@ +Basic Tracer +============ + +This example shows how to use OpenTelemetry to instrument a Python application - e.g. a batch job. +It supports exporting spans either to the console or to Jaeger_. + +The source files required to run this example are available :scm_web:`here `. + + +Run the application +------------------- + +Console +******* + +* Run the sample + +.. code-block:: sh + + $ python tracer.py + +The output will be displayed at the console + +:: + + Hello world from OpenTelemetry Python! + Span(name="baz", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x5611c1407e06e4d7, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="bar", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1b9db0e0cc1a3f60, trace_state={})), start_time=2019-11-07T21:26:45.934412Z, end_time=2019-11-07T21:26:45.934567Z) + Span(name="bar", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1b9db0e0cc1a3f60, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="foo", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1d5d87441ec2f410, trace_state={})), start_time=2019-11-07T21:26:45.934396Z, end_time=2019-11-07T21:26:45.934576Z) + Span(name="foo", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1d5d87441ec2f410, trace_state={}), kind=SpanKind.INTERNAL, parent=None, start_time=2019-11-07T21:26:45.934369Z, end_time=2019-11-07T21:26:45.934580Z) + + +Jaeger +****** + +Setup `Jaeger Tracing `_. + +* Run the sample + +.. code-block:: sh + + $ pip install opentelemetry-ext-jaeger + $ EXPORTER=jaeger python tracer.py + + +The traces should be available in the Jaeger UI at ``_ + + +Collector +********* + +* Start Collector + +.. code-block:: sh + + $ pip install docker-compose + $ cd docker + $ docker-compose up + +* Run the sample + +.. code-block:: sh + + $ pip install opentelemetry-ext-otcollector + $ EXPORTER=collector python tracer.py + + +Collector is configured to export to Jaeger, follow Jaeger UI instructions to find the traces. + +Useful links +------------ + +- For more information on OpenTelemetry, visit OpenTelemetry_. +- For more information on tracing in Python, visit Jaeger_. + +.. _Jaeger: https://www.jaegertracing.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ \ No newline at end of file diff --git a/examples/basic_tracer/__init__.py b/docs/examples/basic_tracer/__init__.py similarity index 100% rename from examples/basic_tracer/__init__.py rename to docs/examples/basic_tracer/__init__.py diff --git a/examples/basic_tracer/docker/collector-config.yaml b/docs/examples/basic_tracer/docker/collector-config.yaml similarity index 100% rename from examples/basic_tracer/docker/collector-config.yaml rename to docs/examples/basic_tracer/docker/collector-config.yaml diff --git a/examples/basic_tracer/docker/docker-compose.yaml b/docs/examples/basic_tracer/docker/docker-compose.yaml similarity index 100% rename from examples/basic_tracer/docker/docker-compose.yaml rename to docs/examples/basic_tracer/docker/docker-compose.yaml diff --git a/examples/basic_tracer/tests/__init__.py b/docs/examples/basic_tracer/tests/__init__.py similarity index 100% rename from examples/basic_tracer/tests/__init__.py rename to docs/examples/basic_tracer/tests/__init__.py diff --git a/examples/basic_tracer/tests/test_tracer.py b/docs/examples/basic_tracer/tests/test_tracer.py similarity index 100% rename from examples/basic_tracer/tests/test_tracer.py rename to docs/examples/basic_tracer/tests/test_tracer.py diff --git a/examples/basic_tracer/tracer.py b/docs/examples/basic_tracer/tracer.py similarity index 100% rename from examples/basic_tracer/tracer.py rename to docs/examples/basic_tracer/tracer.py diff --git a/docs/examples/http/README.rst b/docs/examples/http/README.rst new file mode 100644 index 0000000000..2d7ef887d7 --- /dev/null +++ b/docs/examples/http/README.rst @@ -0,0 +1,87 @@ +HTTP Example +============ + +This example shows how to use +`OpenTelemetryMiddleware `_ +and `requests `_ integrations to instrument a client and a server in Python. +It supports exporting spans either to the console or to Jaeger_. + +The source files required to run this example are available :scm_web:`here `. + + +Installation +------------ + +.. code-block:: sh + + $ pip install opentelemetry-api + $ pip install opentelemetry-sdk + $ pip install opentelemetry-ext-wsgi + $ pip install opentelemetry-ext-http-requests + + +Run the application +------------------- + +Console +******* + +* Run the server + +.. code-block:: sh + + $ python server.py + + +* Run the client from a different terminal + +.. code-block:: sh + + $ python tracer_client.py + + +The output will be displayed at the console on the client side + +:: + + Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x3703fd889dcdeb2b, trace_state={}), kind=SpanKind.CLIENT, parent=None, start_time=2019-11-07T21:52:59.591634Z, end_time=2019-11-07T21:53:00.386014Z) + + +And on the server + +:: + + 127.0.0.1 - - [07/Nov/2019 13:53:00] "GET / HTTP/1.1" 200 - + Span(name="/wiki/Rabbit", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x4bf0be462b91d6ef, trace_state={}), kind=SpanKind.CLIENT, parent=Span(name="parent", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x68338643ccb2d53b, trace_state={})), start_time=2019-11-07T21:52:59.601597Z, end_time=2019-11-07T21:53:00.380491Z) + Span(name="parent", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x68338643ccb2d53b, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x36050ac596949bc1, trace_state={})), start_time=2019-11-07T21:52:59.601233Z, end_time=2019-11-07T21:53:00.384485Z) + Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x36050ac596949bc1, trace_state={}), kind=SpanKind.SERVER, parent=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x3703fd889dcdeb2b, trace_state={}), start_time=2019-11-07T21:52:59.600816Z, end_time=2019-11-07T21:53:00.385322Z) + + +Jaeger +****** + +Setup `Jaeger Tracing `_. + +* Run the server + +.. code-block:: sh + + $ pip install opentelemetry-ext-jaeger + $ EXPORTER=jaeger python server.py + +* Run the client from a different terminal + +.. code-block:: sh + + $ EXPORTER=jaeger python tracer_client.py + +The traces should be available in the Jaeger UI at ``_ + +Useful links +------------ + +- For more information on OpenTelemetry, visit OpenTelemetry_. +- For more information on tracing in Python, visit Jaeger_. + +.. _Jaeger: https://www.jaegertracing.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ \ No newline at end of file diff --git a/examples/http/__init__.py b/docs/examples/http/__init__.py similarity index 100% rename from examples/http/__init__.py rename to docs/examples/http/__init__.py diff --git a/examples/http/requirements.txt b/docs/examples/http/requirements.txt similarity index 100% rename from examples/http/requirements.txt rename to docs/examples/http/requirements.txt diff --git a/examples/http/server.py b/docs/examples/http/server.py similarity index 100% rename from examples/http/server.py rename to docs/examples/http/server.py diff --git a/examples/http/tests/__init__.py b/docs/examples/http/tests/__init__.py similarity index 100% rename from examples/http/tests/__init__.py rename to docs/examples/http/tests/__init__.py diff --git a/examples/http/tests/test_http.py b/docs/examples/http/tests/test_http.py similarity index 100% rename from examples/http/tests/test_http.py rename to docs/examples/http/tests/test_http.py diff --git a/examples/http/tracer_client.py b/docs/examples/http/tracer_client.py similarity index 100% rename from examples/http/tracer_client.py rename to docs/examples/http/tracer_client.py diff --git a/examples/metrics/observer_example.py b/docs/examples/metrics/observer_example.py similarity index 100% rename from examples/metrics/observer_example.py rename to docs/examples/metrics/observer_example.py diff --git a/examples/metrics/prometheus.py b/docs/examples/metrics/prometheus.py similarity index 100% rename from examples/metrics/prometheus.py rename to docs/examples/metrics/prometheus.py diff --git a/examples/metrics/record.py b/docs/examples/metrics/record.py similarity index 100% rename from examples/metrics/record.py rename to docs/examples/metrics/record.py diff --git a/examples/metrics/simple_example.py b/docs/examples/metrics/simple_example.py similarity index 100% rename from examples/metrics/simple_example.py rename to docs/examples/metrics/simple_example.py diff --git a/examples/opentelemetry-example-app/README.rst b/docs/examples/opentelemetry-example-app/README.rst similarity index 100% rename from examples/opentelemetry-example-app/README.rst rename to docs/examples/opentelemetry-example-app/README.rst diff --git a/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py similarity index 100% rename from examples/opentelemetry-example-app/setup.py rename to docs/examples/opentelemetry-example-app/setup.py diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/__init__.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/__init__.py similarity index 100% rename from examples/opentelemetry-example-app/src/opentelemetry_example_app/__init__.py rename to docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/__init__.py diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py similarity index 100% rename from examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py rename to docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py diff --git a/examples/opentelemetry-example-app/tests/__init__.py b/docs/examples/opentelemetry-example-app/tests/__init__.py similarity index 100% rename from examples/opentelemetry-example-app/tests/__init__.py rename to docs/examples/opentelemetry-example-app/tests/__init__.py diff --git a/examples/opentelemetry-example-app/tests/test_flask_example.py b/docs/examples/opentelemetry-example-app/tests/test_flask_example.py similarity index 100% rename from examples/opentelemetry-example-app/tests/test_flask_example.py rename to docs/examples/opentelemetry-example-app/tests/test_flask_example.py diff --git a/docs/examples/opentracing/README.rst b/docs/examples/opentracing/README.rst new file mode 100644 index 0000000000..0305da9955 --- /dev/null +++ b/docs/examples/opentracing/README.rst @@ -0,0 +1,97 @@ +OpenTracing Shim Example +========================== + +This example shows how to use the `opentelemetry-ext-opentracing-shim +package `_ +to interact with libraries instrumented with +`opentracing-python `_. + +The included ``rediscache`` library creates spans via the OpenTracing Redis +integration, +`redis_opentracing `_. +Spans are exported via the Jaeger exporter, which is attached to the +OpenTelemetry tracer. + + +The source files required to run this example are available :scm_web:`here `. + +Installation +------------ + +Jaeger +****** + +Setup `Jaeger Tracing `_. + +Redis +***** + +Install Redis following the `instructions `_. + +Make sure that the Redis server is running by executing this: + +.. code-block:: sh + + $ redis-server + + +Python Dependencies +******************* + +Install the Python dependencies in :scm_raw_web:`requirements.txt ` + +.. code-block:: sh + + $ pip install -r requirements.txt + + +Alternatively, you can install the Python dependencies separately: + +.. code-block:: sh + + $ pip install \ + opentelemetry-api \ + opentelemetry-sdk \ + opentelemetry-ext-jaeger \ + opentelemetry-opentracing-shim \ + redis \ + redis_opentracing + + +Run the Application +------------------- + +The example script calculates a few Fibonacci numbers and stores the results in +Redis. The script, the ``rediscache`` library, and the OpenTracing Redis +integration all contribute spans to the trace. + +To run the script: + +.. code-block:: sh + + $ python main.py + + +After running, you can view the generated trace in the Jaeger UI. + +Jaeger UI +********* + +Open the Jaeger UI in your browser at +``_ and view traces for the +"OpenTracing Shim Example" service. + +Each ``main.py`` run should generate a trace, and each trace should include +multiple spans that represent calls to Redis. + +Note that tags and logs (OpenTracing) and attributes and events (OpenTelemetry) +from both tracing systems appear in the exported trace. + +Useful links +------------ + +- For more information on OpenTelemetry, visit OpenTelemetry_. +- For more information on tracing in Python, visit Jaeger_. + +.. _Jaeger: https://www.jaegertracing.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/examples/opentracing/__init__.py b/docs/examples/opentracing/__init__.py similarity index 100% rename from examples/opentracing/__init__.py rename to docs/examples/opentracing/__init__.py diff --git a/examples/opentracing/images/jaeger-span-expanded.png b/docs/examples/opentracing/images/jaeger-span-expanded.png similarity index 100% rename from examples/opentracing/images/jaeger-span-expanded.png rename to docs/examples/opentracing/images/jaeger-span-expanded.png diff --git a/examples/opentracing/images/jaeger-trace-full.png b/docs/examples/opentracing/images/jaeger-trace-full.png similarity index 100% rename from examples/opentracing/images/jaeger-trace-full.png rename to docs/examples/opentracing/images/jaeger-trace-full.png diff --git a/examples/opentracing/main.py b/docs/examples/opentracing/main.py similarity index 100% rename from examples/opentracing/main.py rename to docs/examples/opentracing/main.py diff --git a/examples/opentracing/rediscache.py b/docs/examples/opentracing/rediscache.py similarity index 100% rename from examples/opentracing/rediscache.py rename to docs/examples/opentracing/rediscache.py diff --git a/examples/opentracing/requirements.txt b/docs/examples/opentracing/requirements.txt similarity index 100% rename from examples/opentracing/requirements.txt rename to docs/examples/opentracing/requirements.txt diff --git a/docs/ext/dbapi/dbapi.rst b/docs/ext/dbapi/dbapi.rst new file mode 100644 index 0000000000..dbe6dbbeab --- /dev/null +++ b/docs/ext/dbapi/dbapi.rst @@ -0,0 +1,10 @@ +.. include:: ../../../ext/opentelemetry-ext-dbapi/README.rst + + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.dbapi + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.ext.flask.rst b/docs/ext/flask/flask.rst similarity index 63% rename from docs/opentelemetry.ext.flask.rst rename to docs/ext/flask/flask.rst index 87ad93b867..e65323cc81 100644 --- a/docs/opentelemetry.ext.flask.rst +++ b/docs/ext/flask/flask.rst @@ -1,5 +1,5 @@ -opentelemetry.ext.flask package -========================================== +.. include:: ../../../ext/opentelemetry-ext-flask/README.rst + Module contents --------------- diff --git a/docs/opentelemetry.ext.http_requests.rst b/docs/ext/http_requests/http_requests.rst similarity index 62% rename from docs/opentelemetry.ext.http_requests.rst rename to docs/ext/http_requests/http_requests.rst index fbc9bd2a63..779be3e033 100644 --- a/docs/opentelemetry.ext.http_requests.rst +++ b/docs/ext/http_requests/http_requests.rst @@ -1,5 +1,4 @@ -opentelemetry.ext.http_requests package -========================================== +.. include:: ../../../ext/opentelemetry-ext-http-requests/README.rst Module contents --------------- diff --git a/docs/ext/jaeger/jaeger.rst b/docs/ext/jaeger/jaeger.rst new file mode 100644 index 0000000000..70b9c04205 --- /dev/null +++ b/docs/ext/jaeger/jaeger.rst @@ -0,0 +1,17 @@ +.. include:: ../../../ext/opentelemetry-ext-jaeger/README.rst + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.jaeger + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. automodule:: opentelemetry.ext.jaeger.gen.jaeger.ttypes + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ext/mysql/mysql.rst b/docs/ext/mysql/mysql.rst new file mode 100644 index 0000000000..e2c01371cd --- /dev/null +++ b/docs/ext/mysql/mysql.rst @@ -0,0 +1,10 @@ +.. include:: ../../../ext/opentelemetry-ext-mysql/README.rst + + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.mysql + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ext/opentracing_shim/opentracing_shim.rst b/docs/ext/opentracing_shim/opentracing_shim.rst new file mode 100644 index 0000000000..f27974b9c0 --- /dev/null +++ b/docs/ext/opentracing_shim/opentracing_shim.rst @@ -0,0 +1,5 @@ +OpenTracing Shim for OpenTelemetry +================================== + +.. automodule:: opentelemetry.ext.opentracing_shim + :no-show-inheritance: diff --git a/docs/ext/otcollector/otcollector.rst b/docs/ext/otcollector/otcollector.rst new file mode 100644 index 0000000000..c940bfacaa --- /dev/null +++ b/docs/ext/otcollector/otcollector.rst @@ -0,0 +1,10 @@ +.. include:: ../../../ext/opentelemetry-ext-otcollector/README.rst + + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.otcollector + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ext/prometheus/prometheus.rst b/docs/ext/prometheus/prometheus.rst new file mode 100644 index 0000000000..7f331cb6da --- /dev/null +++ b/docs/ext/prometheus/prometheus.rst @@ -0,0 +1,10 @@ +.. include:: ../../../ext/opentelemetry-ext-prometheus/README.rst + + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.prometheus + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ext/psycopg2/psycopg2.rst b/docs/ext/psycopg2/psycopg2.rst new file mode 100644 index 0000000000..89cebce345 --- /dev/null +++ b/docs/ext/psycopg2/psycopg2.rst @@ -0,0 +1,10 @@ +.. include:: ../../../ext/opentelemetry-ext-psycopg2/README.rst + + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.psycopg2 + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.ext.pymongo.rst b/docs/ext/pymongo/pymongo.rst similarity index 63% rename from docs/opentelemetry.ext.pymongo.rst rename to docs/ext/pymongo/pymongo.rst index e5c262ac16..848c0dba25 100644 --- a/docs/opentelemetry.ext.pymongo.rst +++ b/docs/ext/pymongo/pymongo.rst @@ -1,5 +1,4 @@ -opentelemetry.ext.pymongo package -========================================== +.. include:: ../../../ext/opentelemetry-ext-pymongo/README.rst Module contents --------------- diff --git a/docs/opentelemetry.ext.wsgi.rst b/docs/ext/wsgi/wsgi.rst similarity index 63% rename from docs/opentelemetry.ext.wsgi.rst rename to docs/ext/wsgi/wsgi.rst index 506bc1efe4..be8194bdd6 100644 --- a/docs/opentelemetry.ext.wsgi.rst +++ b/docs/ext/wsgi/wsgi.rst @@ -1,5 +1,5 @@ -opentelemetry.ext.wsgi package -========================================== +.. include:: ../../../ext/opentelemetry-ext-wsgi/README.rst + Module contents --------------- diff --git a/docs/ext/zipkin/zipkin.rst b/docs/ext/zipkin/zipkin.rst new file mode 100644 index 0000000000..8826178908 --- /dev/null +++ b/docs/ext/zipkin/zipkin.rst @@ -0,0 +1,10 @@ +.. include:: ../../../ext/opentelemetry-ext-zipkin/README.rst + + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.zipkin + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst index c597d4a681..f52129829a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,47 +1,104 @@ -.. OpenTelemetry documentation master file, created by - sphinx-quickstart on Mon Jun 3 22:48:38 2019. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +OpenTelemetry-Python +==================== -OpenTelemetry -============= +The Python `OpenTelemetry `_ client. -Welcome to OpenTelemetry's documentation! +.. image:: https://img.shields.io/gitter/room/opentelemetry/opentelemetry-python + :target: https://gitter.im/open-telemetry/opentelemetry-python + :alt: Gitter Chat -This documentation describes the ``opentelemetry-api``, ``opentelemetry-sdk`` -and integration packages. -.. toctree:: - :maxdepth: 1 - :caption: OpenTelemetry API: +This documentation describes the :doc:`opentelemetry-api `, +:doc:`opentelemetry-sdk `, and several `integration packages <#integrations>`_. + +**Please note** that this library is currently in alpha, and shouldn't be +used in production environments. + +Installation +------------ + +The API and SDK packages are available on PyPI, and can installed via pip: + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + +In addition, there are several extension packages which can be installed separately as:: + + pip install opentelemetry-ext-{integration} + +The extension packages can be found in :scm_web:`ext/ directory of the repository `. + +In addition, third party exporters are available: + +* `Azure Monitor `_ + +Installing Cutting Edge Packages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While the project is pre-1.0, there may be significant functionality that +has not yet been released to PyPI. In that situation, you may want to +install the packages directly from the repo. This can be done by cloning the +repositry and doing an `editable +install `_: - opentelemetry.context - opentelemetry.metrics - opentelemetry.trace - opentelemetry.util.loader +.. code-block:: sh + + git clone https://github.com/open-telemetry/opentelemetry-python.git + cd opentelemetry-python + pip install -e ./opentelemetry-api + pip install -e ./opentelemetry-sdk + pip install -e ./ext/opentelemetry-ext-{integration} + + +Quick Start +----------- + +OpenTelemetry can be used to emit distributed traces and metrics from your application. +The following are two simple examples using the API and SDK, you can find more +elaborated examples in `Examples`_. + +.. TODO: Link to complete and better examples + +Tracing +~~~~~~~ + +.. literalinclude:: trace_example.py + :language: python + +Metrics +~~~~~~~ + +.. literalinclude:: metrics_example.py + :language: python .. toctree:: :maxdepth: 1 - :caption: OpenTelemetry SDK: + :caption: OpenTelemetry Python Packages + :name: packages - opentelemetry.sdk.context - opentelemetry.sdk.metrics - opentelemetry.sdk.trace + api/api + sdk/sdk + +.. toctree:: + :maxdepth: 1 + :caption: OpenTelemetry Integrations + :name: integrations + :glob: + ext/** .. toctree:: :maxdepth: 1 - :caption: OpenTelemetry Integrations: + :caption: Examples + :name: examples + :glob: - opentelemetry.ext.flask - opentelemetry.ext.http_requests - opentelemetry.ext.jaeger - opentelemetry.ext.opentracing_shim - opentelemetry.ext.pymongo - opentelemetry.ext.wsgi + examples/** Indices and tables -================== +------------------ * :ref:`genindex` * :ref:`modindex` diff --git a/docs/metrics_example.py b/docs/metrics_example.py new file mode 100644 index 0000000000..8ae01f5e98 --- /dev/null +++ b/docs/metrics_example.py @@ -0,0 +1,23 @@ +from opentelemetry import metrics +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.controller import PushController + +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) +meter = metrics.get_meter(__name__) +exporter = ConsoleMetricsExporter() +controller = PushController(meter, exporter, 5) + +requests_counter = meter.create_metric( + name="requests", + description="number of requests", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), +) + +staging_label_set = meter.get_label_set({"environment": "staging"}) +requests_counter.add(25, staging_label_set) + +input("Press a key to finish...\n") diff --git a/docs/opentelemetry.ext.jaeger.gen.jaeger.ttypes.rst b/docs/opentelemetry.ext.jaeger.gen.jaeger.ttypes.rst deleted file mode 100644 index 8b69e013b4..0000000000 --- a/docs/opentelemetry.ext.jaeger.gen.jaeger.ttypes.rst +++ /dev/null @@ -1,11 +0,0 @@ -opentelemetry.ext.jaeger.gen.jaeger.ttypes -========================================== - - -Module contents ---------------- - -.. automodule:: opentelemetry.ext.jaeger.gen.jaeger.ttypes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/opentelemetry.ext.jaeger.rst b/docs/opentelemetry.ext.jaeger.rst deleted file mode 100644 index 74a7fb399e..0000000000 --- a/docs/opentelemetry.ext.jaeger.rst +++ /dev/null @@ -1,19 +0,0 @@ -opentelemetry.ext.jaeger package -========================================== - - -Submodules ----------- - -.. toctree:: - - opentelemetry.ext.jaeger.gen.jaeger.ttypes - - -Module contents ---------------- - -.. automodule:: opentelemetry.ext.jaeger - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/opentelemetry.ext.opentracing_shim.rst b/docs/opentelemetry.ext.opentracing_shim.rst deleted file mode 100644 index 921d6c290b..0000000000 --- a/docs/opentelemetry.ext.opentracing_shim.rst +++ /dev/null @@ -1,8 +0,0 @@ -opentelemetry.ext.opentracing_shim package -========================================== - -Module contents ---------------- - -.. automodule:: opentelemetry.ext.opentracing_shim - :no-show-inheritance: diff --git a/docs/opentelemetry.sdk.context.rst b/docs/sdk/context.rst similarity index 100% rename from docs/opentelemetry.sdk.context.rst rename to docs/sdk/context.rst diff --git a/docs/opentelemetry.sdk.metrics.export.aggregate.rst b/docs/sdk/metrics.export.aggregate.rst similarity index 100% rename from docs/opentelemetry.sdk.metrics.export.aggregate.rst rename to docs/sdk/metrics.export.aggregate.rst diff --git a/docs/opentelemetry.sdk.metrics.export.batcher.rst b/docs/sdk/metrics.export.batcher.rst similarity index 85% rename from docs/opentelemetry.sdk.metrics.export.batcher.rst rename to docs/sdk/metrics.export.batcher.rst index 5dbd1d6e58..dab2dd3415 100644 --- a/docs/opentelemetry.sdk.metrics.export.batcher.rst +++ b/docs/sdk/metrics.export.batcher.rst @@ -3,7 +3,7 @@ opentelemetry.sdk.metrics.export.batcher .. toctree:: - opentelemetry.sdk.metrics.export + metrics.export .. automodule:: opentelemetry.sdk.metrics.export.batcher :members: diff --git a/docs/opentelemetry.sdk.metrics.export.rst b/docs/sdk/metrics.export.rst similarity index 100% rename from docs/opentelemetry.sdk.metrics.export.rst rename to docs/sdk/metrics.export.rst diff --git a/docs/opentelemetry.sdk.metrics.rst b/docs/sdk/metrics.rst similarity index 61% rename from docs/opentelemetry.sdk.metrics.rst rename to docs/sdk/metrics.rst index 88612046c8..7030285982 100644 --- a/docs/opentelemetry.sdk.metrics.rst +++ b/docs/sdk/metrics.rst @@ -6,9 +6,9 @@ Submodules .. toctree:: - opentelemetry.sdk.metrics.export.aggregate - opentelemetry.sdk.metrics.export.batcher - opentelemetry.sdk.util.instrumentation + metrics.export.aggregate + metrics.export.batcher + util.instrumentation .. automodule:: opentelemetry.sdk.metrics :members: diff --git a/docs/sdk/sdk.rst b/docs/sdk/sdk.rst new file mode 100644 index 0000000000..b16a6f2650 --- /dev/null +++ b/docs/sdk/sdk.rst @@ -0,0 +1,11 @@ +OpenTelemetry Python SDK +======================== + +.. TODO: what is the SDK + +.. toctree:: + :maxdepth: 1 + + context + metrics + trace \ No newline at end of file diff --git a/docs/opentelemetry.sdk.trace.export.rst b/docs/sdk/trace.export.rst similarity index 100% rename from docs/opentelemetry.sdk.trace.export.rst rename to docs/sdk/trace.export.rst diff --git a/docs/opentelemetry.sdk.trace.rst b/docs/sdk/trace.rst similarity index 73% rename from docs/opentelemetry.sdk.trace.rst rename to docs/sdk/trace.rst index 1c0e9b6f61..ce06fb4abb 100644 --- a/docs/opentelemetry.sdk.trace.rst +++ b/docs/sdk/trace.rst @@ -6,8 +6,8 @@ Submodules .. toctree:: - opentelemetry.sdk.trace.export - opentelemetry.sdk.util.instrumentation + trace.export + util.instrumentation .. automodule:: opentelemetry.sdk.trace :members: diff --git a/docs/opentelemetry.sdk.util.instrumentation.rst b/docs/sdk/util.instrumentation.rst similarity index 100% rename from docs/opentelemetry.sdk.util.instrumentation.rst rename to docs/sdk/util.instrumentation.rst diff --git a/docs/trace_example.py b/docs/trace_example.py new file mode 100644 index 0000000000..8197dee156 --- /dev/null +++ b/docs/trace_example.py @@ -0,0 +1,18 @@ +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) +trace.tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): + print("Hello world from OpenTelemetry Python!") diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 7fcc3f7dc4..0000000000 --- a/examples/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Examples -This folder contains various examples to demonstrate using OpenTelemetry. - -##### basic_tracer -This example shows how to use OpenTelemetry to instrument an application - e.g. a batch job. - -##### http -This example shows how to use [OpenTelemetryMiddleware](https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-wsgi) and [requests](https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-http-requests) integrations to instrument a client and a server. - -##### opentelemetry-example-app -This package is a complete example of an application instrumented with OpenTelemetry. \ No newline at end of file diff --git a/examples/basic_tracer/README.md b/examples/basic_tracer/README.md deleted file mode 100644 index ae9e4ca895..0000000000 --- a/examples/basic_tracer/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# Overview - -This example shows how to use OpenTelemetry to instrument a Python application - e.g. a batch job. -It supports exporting spans either to the console or to [Jaeger](https://www.jaegertracing.io). - -## Installation - -```sh -$ pip install opentelemetry-api opentelemetry-sdk -``` - -Setup [Jaeger Tracing](https://www.jaegertracing.io/docs/latest/getting-started/#all-in-one) - -## Run the Application - -### Console - -* Run the sample - -```bash -$ # from this directory -$ python tracer.py -``` - -The output will be displayed at the console - -```bash -AsyncRuntimeContext({'current_span': Span(name="baz", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x5611c1407e06e4d7, trace_state={}))}) -Span(name="baz", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x5611c1407e06e4d7, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="bar", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1b9db0e0cc1a3f60, trace_state={})), start_time=2019-11-07T21:26:45.934412Z, end_time=2019-11-07T21:26:45.934567Z) -Span(name="bar", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1b9db0e0cc1a3f60, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="foo", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1d5d87441ec2f410, trace_state={})), start_time=2019-11-07T21:26:45.934396Z, end_time=2019-11-07T21:26:45.934576Z) -Span(name="foo", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1d5d87441ec2f410, trace_state={}), kind=SpanKind.INTERNAL, parent=None, start_time=2019-11-07T21:26:45.934369Z, end_time=2019-11-07T21:26:45.934580Z) -``` - - -### Jaeger - -* Run the sample - -```sh -$ pip install opentelemetry-ext-jaeger -$ # from this directory -$ EXPORTER=jaeger python tracer.py -``` - -#### Jaeger UI - -Open the Jaeger UI in your browser [http://localhost:16686](http://localhost:16686) - -

-Select `basic-service` under *Service Name* and click on *Find Traces*. - -Click on the trace to view its details. - -

- -### Collector - -* Start Collector - -```sh -$ pip install docker-compose -$ cd docker -$ docker-compose up - -* Run the sample - -$ pip install opentelemetry-ext-otcollector -$ # from this directory -$ EXPORTER=collector python tracer.py -``` - -Collector is configured to export to Jaeger, follow Jaeger UI isntructions to find the traces. - - - -## Useful links -- For more information on OpenTelemetry, visit: -- For more information on tracing in Python, visit: - -## LICENSE - -Apache License 2.0 diff --git a/examples/basic_tracer/images/jaeger-ui-detail.png b/examples/basic_tracer/images/jaeger-ui-detail.png deleted file mode 100644 index 63491e132b5db66441142e4fea3c60d287f00e89..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123383 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 RiUT{7_ce*lKEH^u+} diff --git a/examples/basic_tracer/images/jaeger-ui.png b/examples/basic_tracer/images/jaeger-ui.png deleted file mode 100644 index 08e6718eef130bc6afb927930a85b3f4ca26fdb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 256630 zcmb5Vby!qg_dk5-5CjoP2?0fr?wBDYhHeFp}}$&ri_t}l6gn@9UQ@kuoX;c^b= z;L^H|c6t?kFB!)E-X+w%F-w|T@wGkqu}byQ;vJD!Cm-cYYQks=Ba?RBv+|=4pQJzbY5uZ9!`~=0-NESJU0fClF9pnhS}H%g{jGtVo?}13Qb;7#AtIGt z^eL26E=;Qv7ZFf+zTI;L|@k_ zvmpye_QEM|T7|jXi2TYbRByG#Vjn4Dwi#S@pG#Y^M)Pr8?w9T#c_H_c=BO-P6`bFC za7on=YABIPGd_5rqBQm`!WOyLeb;WW{f#WwLWolvw#vk(cA+=|wyoy==G~Mcc8cBL z+soKDoW`GpDi)Ob7VM_ssPfUqcuJw@Q{^b;(3|-Y@$%F&x5>J2 z_6$x)UiV+KUBbu>VXQU1)K-}xEHlh*6x(V^i9N01){xa#=kZff)zSzW&sUjJUNnrj zlXnCMzV!!7O5vaWTDUV*0=&Xht%n=~W-&kAM-ORj zv>*LGIJL9vXDsLRtV5|@&LGygUhSmva#wXuU~O`FP)Noyw5MZTlZ^Rdo>H0534w$K zJVLy?1CLnu+P@RK%!E04lJj1L6G8{^fk@e%$;8_gJqyI0vHxV~qFi1V-We>}8s3>e ztQh^&+uQ3DC-*eQuBJ3a?eG}O!t_K{|CBh0`Pah2LVT-Rm6JNtIUUbIci>uPO#4>1ME3y}gikK3+`bwo7<@e<;`W5$lc*<+?Hc*#*YU@4Y^G{;ZH zp$c))B6=M3#T;jXq$b#8L9&ub=q2j{+sO@MT>UmuC+dM5$6b=&!Q~> zWwaQxV>tC!#gKSHHY#QL;dm}K>Ub8Fr`Pvy=|q%06k#@s_gVH`=5wJCk9A}Aj0;mD z+h^Zr?9`%r_UcJq%&J{tw96ok5N%e}O5Vs2#b*+e(8u}itBjwCpb@6>@Z79b!Ou)M zsn@%{z2NhVigvE@;=<`|U!H)JVSo6#Ua44i3o|lasZmDl8+Owff8guvuiG(Qx=vV9 z3^`%EouUhkyZ6q80!RbHkALM5x`p7iN0ZAe+_*>KLL^9te8u_o`qw*OY3tG(h$TJeNS1Tz-8EHm~q&7_>qq2 z8nF}Bx%;!GzrK!A`VnuOIo^)>OsP$^hFmjF99knsGDzaue>R%2UavfY+ zRUdjV^kG;s*VC{=RAP;oo7f5Mpt9WiH<~sgGiXJ0vEaST=W^)*ZsRtf;~swY|5=vN@bIn2?{o z?@%-Kd75NGVzOu=d20JhGaUBCY8pB5vFdsCMs;ls6I{Bxr5$5S5Q#G_yIDE#tE___ z#@;Q|tq{{6qnV|a<)v{~gH+=Wb`gWSYM9l>Qsp6x~nSKH>r5If@xt%bQ=xO0RiSKBML&nZ0rPYU%5brOjUuvID|| zFa~1Z4$_4IWHw><*Zi*!zD~1}u*`2N zXq>`#mENhW!(b*v#nQ3u!KdfdXSl}g677N(B~HAt6%4Fo{%8)rQ_uHdxALb}IxiI3|7@zyouq?S|8 ztq)@Yy`S)`P0V2avs&dwJiM)6?mG>wbe>rLSUkErr#i-q*1J&b^l8~SCJm9kC06x9v-=bpToy88t#LM&=JOW9EBT#a1m>EEeUs+pOi z`|_o9psM^Cy|d9?P4~2JZO%pVg~P@51BN6?sqMt=&6%XS4Yy`9{|m=Av%U0#_||fW z>^`V|%eBk$!l7p84bzpm9)I&3mwEWSt$(0vU>~LiXA>uyfZ_TWqvh@Qb`R5Sgb#(^ zWJnqf8%bP`m7E!)oA6l;)0Wdk#V6+7-d&N~<0IOHsdT-h>8tCfIXgG)E-I-xodozi48 zCQh+V6^kvHi}wUnWa|OWXlr;e95od7i;Nel;^S7UI(WFL-&w)b(91Z&qz|nM(7F7% zu@;~GJ^Mkc`sv5Bw9QEwIC=-abzI6bAYeCkP@3mZeRCt=P9XmYYBl~=9>D$YI~w5i>MsubT+#gJit{!UAOL?+fFG}1?7!b81afiyZsT3O3CQZo zDJz3lT`L!BYe!c*xSQs+d*8tuL{3VEt^h!F_v(YCti!eo@}IERGjKCdSCg=UJMdUM zggZcrAMD?$JK{imPS zUiN<^IlBILSl|G8ub%Mo^YHQh!y6=pT-}w>viGunVIXhs0O|~sAtk^kBnJ75;Qx8_ zN92DzN;j!LTK_Nc|33V`gn!92{3DZJNI>X+W&Y2j|B^y@ug3mAy7-Tx z|GEomT8bFL`_HUN5i|YN%LFI#w!OTj9(V=K?CKBu${6nd=L)v5oEIcZVnEZ80hHxs z^}Mh)X9=2w9(kYa1>Y~x#=XYUlOsw@lay=DRnT-Hm)r!7B0|k9;>UV99^`Y{uL6*s-MuPiR|
AngD9RTkf7gAbI&H)TBfn-7(88xW)*F?G;bfe-ZUR0*Q~FVPjFFF{y9z>WV3 z21$!}&4ISx(X(GquM}JyldLV0Ir03G#9NR(&X@E**0-piT5g^TC zpV9o?A4;t@0BFVen zifXh6+A^7Bsw@6qb=)@xg69A9Ab9N>4Z!DbRm;}`4tI&Z0sn7qe>x`k2-p` z?r-f@GJ`tc`W~EpFe?O@50K?f>i^ICf3xQU+IIf*pzSvv36R=SWkh2J>S&MDR^8ibOS`o|7OA!4(i~VOD@V!loUukz_B~J^*`_b&7K6< zmHg8KK7X)#Met3kuB$oolN7KBeNwwn8Lz3Z|)#Yy8YzK1+c!^-TBU`+nM{WgoRfCJ}DDrgEg z>8GM?7yk_7kKwPFf&;3Tezo1kLU`MA#<>@YshxWE;aP@5O=-=HORvI&`?2=bEacvC zIvmy4PcikeckjHV|1-yLiGIH1Xd2$yWz69B9FQR(i!_|NQjfY)<{uUQY4MqX%Iq88 z6R5p*pB#EAI`4PzER4aU@>d{ME6(;O(5MFQ$uPGmP@CwP1YR7D8b{%p12WSVZcoNR z|5%;6!7u$s!+%t)aHUHSJ=qs7&H{2%)b{sBf6K3dp# zUYj0njO2FRl~I&$UP#|E)qf9~bJ?|E8oUMi`4h!2^8sh>o;l#ax2yPV=|Pj9u>1Ak z-&odDf`WO-GSW;bg99&*11mh;Q%pPz6HPp%|8fIG^4z_gZUC}?MMh(>M$2spv_<94 z_8EY)5!H$M`e&$rTF_t@t| zBRC$sOOrN$_ivqUxDqEC^Riwm_Yo-KF%>)KmEj73;XsZ4F#$D^1Qqk#gc{^-@>Q%)5$+m8GECGt$Krg5$5IDWd=N zze;8WC40ua7S}ev3Ca+;;~etW6u$w9x$kS}m{VR!)^?@+H_6wDK*{f3T0Ew-16^m3 za+6<7^fm1`*9QTS=#`VU((ehl;7Uo-+QrcbhAK3f&7wrd-_9K^up1BuD8!GBb z^l!8pSG1)a7B-XyS8l07HvgBpK7(uF;P#>(UMLl4D0TwW|Bv5+2C|wAN|=>kTu}_Z z1zJkk!fy0mg2_O6K5b%#!4_BZ^tccGZ}uSp`m{{<_3%k;W>8j~@RZr*C%vC$p0nhEre(++f$@XJg;bezg0Wm zauwO$GhM905)2AA{tR*Tj?;1Dd^2YGZ2mIfuzAP7qT5*Gu}rWmo&-Ix`-&a%&T)4! zj4?X*<#yu1M;@mz$(eTz-Oz1qgxAreEt#Iu<)PW-;d*?m-_zzkZ&a3kmhX1`1Cie) zFzEY+y=b`c#qVP2k5Z>YvE?UyqWMJMJOjb_LhFEs!*f`jb5Z8_v?w^uN=$QY!|d{O zpR|4Fd|*e<$gv&?u?`$<+GnUcntHy0X8fH?@KNHj%;p4*2yD6S#lI;ct|;2{i!{vS z#ccoOn_&YFaCsR&b05i=bL+(1_}#wqiCck>FckmwXCm$uA#kzwoC)-ucK878X`OZS z?p@cIexYmfC}aThK?pnZtb^&XEoSHPv2?@w`!$EAj^|Qd$Fm<&W)HI6ugBg$epSJF zoUp;t?+vtpecoKJ4cMrCKRu5*8XYr&o0tTkk%#`nX|~yhS$_LJHWtb2Zxa8e_>LT{ z@h)%L?^$o0zi>RJ)t%t7&By9X>*wwcad@7og_QzSx1G$cRH#UcVR2c!Am z#13rf>I=9{PtVmfTwd&$*`iu88?Bt{xa!28iQEK$1=HgO{X=k`3I=x2s2sDsIU&Q~ z@#by!QNirJD4#+&xFUbhZ#^CT;JENZGr+}tIUY84aM9a(uKMM8&bPuK^lLzYgQ`Nk zyyHqv0FTOY+kQ7)g7HmfBd)iEVocSb_HzZcI8bL4Qqa45OnscTn+AtKI ztPE-w1JBKu*v-N2y|MGViDPL=j+=@3s#aMul-d5`+X0#t3aF87q z?0blFT`c#s&sJ*0l~G#DJR1+QK!(ZnK}pVw5AtNX4v3HX&Y#V#gVS*)b0Db;4?d&= zcHfctm$blQq3nP;G}@8;3~Bav05KoN_H0^W_<%@mc&h~BBDcf$`pukkHJFR7dCV44 zqtNW+EA@Nm@l3UQi+8FL%1Dv3W%;e@V^TjF>p()C!s|h9SBMJXJ9nUH7~RVi1jdnT zPI1#7LtwB^+Ey0?%nz-=MRi?qB((zs$dA}>0CBFea732J6o=;iAvPGa_$FWa*BZ^f zm-zLItin;>NBH_%jr;jSxAj6;sX1tY!RY+{629)+u4BBM@?P>b1Uvf%Tw~joV-9SrALY#2<~&Q; z_l^7=@rLKU22ZT?=`-}K!PCk@cc#&Q z5eAiBkPBzNgG1}ZQ7?8Y7wguR?z$n(qXzvau~-1)-p1TmJbDrN*BsekmzBkKI zGxZ*`+Iv@QA-FhmIMZRa1zbd(I2dG2%Omr}gH9Utzt_xRq_eai&U97-OB5tPOjkyAC z`x>VY|AAfR%vg-Pefc%%Ys(3mru>{T2d+3kI!p6Bh^VK#G#?Z9xdUw=zBQm;_deop zX5?+!{Tdk}iJ(TWX$Cg9Xd2CF?*{I^WHRP|ApphpTG^*=J?UIe$T@%7JM(~j$akSC z*8hb9qd~GAV|Fn*=MtTB80EXu?AckJ*7_uyNOeigD9`pqnbij!@AD2OOl98QCUnPJ zb9(boXl4G>ZxJhB+Z=z7*~VmIXf_O1kW(@U`Qt7xae+bA)65w4(OLt{tLd zcK|9j$6TdlT3d`W<{b<=(4Tmm;q7xbk8dgS2O1fQtRK%WlEncg2+oz`Yt7CVsomiU zMb*-^b)DRgpT4yEE_EQjy*Im1NAh<6_|15x=cI>JQS;c#8}sUFKiD;HPm3IzZrEw$ z$OAHE3E!XWQ?CV+`I%odM;&8KE3UK=1A{C%jU(upZlsD02jB|+^ zR{bETqBT)FjJEg2Gx66RI2QGq>}^PIMEVJbVNjTt+3!jCCmuk-45tKi$Ybun^GN-s zywkA2lQ85l8qb<7)UIG0e>yMIZMzty^U;a?S{REDt3; z<*BmIm)Z6Gc8GS7nQc2;Ak474ILi+R`3)i$^Mo5zmfo1_XBl>a1=P2W8-6a4d-c_` z`W@7ySL`j&U|KJaFV|056?yV8Mf#ebH$~TcvcA)IzqD%iUKBx2MKGaHQQ(pk+;X#u z@$#>X=?dwXkmqimtcbSO`+y+`L@_^gH9sEBxFSx4k-|OU%{mnTuzgW-v-m|1^uBv; zaZZ3w#b{aTfRfitt4of8zW}mI5CN&S^^S6(KTa#SKVEL|YdFJqcEfZOe%=NDxkWnD zn{eMsU`E>dbqe<=My)i~4ZiqP=KbHGnzSgni8l0GceJmk_;&pQ`Xs)wR{o13$H0f5-@xB0uE`7x)C z*a%xAo!p(I6APUaM^#Ew+PAMl^o@Mby=JBtKXT5MH)^E!A~f|bPhu|(vYfsW9p+KW zAA>a!^~Z9rr>~IaSZGZzPh*+HRhfnn>MctgMhQ_iI~`70XCiI(6RT3pqxh(gC9?9X z0t+zAZ>&Jw1%g5id0K@0YVQVA>pfe3XPa?=3_V}56;c}M{rIs;n75m}R;X`9zL1i~1C*a+dA+NNli_odZkYRVf&Tn?d>e#L2rT9dn-jn{UP%j{Bb#}!&>67lbZ|&7xJ6Q zuSrcmOZV=K`KHAvG<#vy-l~qloaI4(imk6)eR7k8L&6rM7`MyO5PLCwt-)muS|&8} zNlil>2=YHUq%c0-beoIu)}QW9tq=@E-3Nl6-)w(TU=<^7gEky(=BhUhxLT&R57p$v zk@1;!GmyP+H#d5sAW&0XDW5LE>?-4X5o?KA7!Vz|1gL6ZVNLmNebM)8I#20>bKbBM zyKHPdYitlOM|Tub^J``g_{&0Id)JVSs)j3HD*PS3SagI!j4slBN$5Dmc|~pp2SQGB z^~K+Aa3DY1hzsm6A(gh1N}|H$(FteYlxBEasxu`^D4(g~W+uQ!p5ld)0_GdIMT5+0 zi>8seZ*B6h58#(a@D0VK6sUMaQX80N0+x5atE*?=3)Rx{`q;9P{Z6lD< z3jFy12#T!rY~C<|2Hw;=zZhb=&~`n1KvH@F21ztC;et5Ad*B|b$l%u%ByG6Po|3dE z7|@!jxvCbgi;>d)asIPdN;uHCm9fy2{qg=eO1f2Q+rj;b9+ed)RJ7@i&2hy)36nQg zPv*~K9RdvooUc%6hJe{5Sv%_JI@CzfSnA z^Lr zXmSDK4DW3B&gUzI0WJC&OOa{%-qlY4;Ba|U+I`GnZFtyl2}CbG2$`VK9_i2hI#SP6 zPx!Fqjf}1ZA9OQ#8qZ>p^QzTpN*_&F>6g8qmq?A@nYgLitUuy1;c^xkT|Oe}N(;!~ zh(Z+}tLkxT$34HNAt2v;tSThJ|J3||ZMV!8`r(UH&UmN5(BL?@4(_%S@WL?5$o=PI zvW13>P-X&P+56x*4)Ce#yfr%v05pE;eG|!1nEP2A2Bj9)Sm^&*;ViOZEzW{TkN)gE zX#>bOPS`;8x>kB7?U-$69jNan(+UAW8Syot-ea;XC&A}m9_^k#i#fUk@!hlOT>f+= zIYl<+AwHRW@rXz-&ckU6?Z6@WoQ#wj`VTe8siJ5KV6Z0pHZZt3J0~JN;v?)c0?53w zYyIXK1fvpylhD$Eff3`K?;>;*f!&@-Z)I2)p!t{72G_Dk-`(!%NBJ5J=iU(tfy(R=_EMWqA2y z03K_yZb;Xw2mm`3+_LzhO3{~Uy;wltav5T1?87nYIPSwHb!mC(s5qCeJ=cco?jGVZvV|?9Fw#StH$LM{2vOr$24&U&W zl{e$slu+Jr<2{|phv2ptkT}jzL8LbkczLnpz(7NT1>maQG^Ry5g|BwEHd+OGD2Vl!7nGpB;F zwzZUfZx*Sog7$Rk}&Cso()o-+!+^eifz&*BHT#2@474LNRMUp z^YqPapshKe^#RMkQ8=>=ZU*vS`ci z5ZEKW3gy31e<}f1NMm9Ig}GGUg8*wq#r>6l zk7+dMnB1e9HUtE2w7cU2?q@e4nmuQ7Q2qlI9881GX)-gJ+xilfd=I^)rPrD5+Th87 z_gzQC-+dI6nZswruEf*;z*zg9(Z`Eq4>If|*GY#|7vBK{bH!f4Mfj2FK2#|W8g5Q3 z0g$^@QQ2=}1L>r@l;3DnUPZ!*6En5)#_yNZ*YdRC;*rl%L@4=ihJ^bRJI6C}Yg>#$ zi1?o3y}qPjJUc5)p(emLf#xF=p7IGIk6dAvJ+xe=$rkZ9My0$V}0fOZh4~mdC#bjyp?k^|$N_9&u zBmALFZ4g{QOIbmID`H2uqd{v=d9hKI6*8|*aSKM`op$q<@cEl>agg);cRBNXy|5*r z)_xng=M_MZ&5|_q%j#_Q2)Kq?HtVUx2XX~~aB7_NQ`O}~2(u-6WgfHvTVq`Q7~7Li zq({O$5Qrml-kH;zE?`qEO!#KJXXb{Iable^9eeampl#2hRqW_|$+)-j2p5pI{LZqn zR|#JJ z?P+B&2%R5E%vJGXOalK(D^iE2F*7=L)->lz^BmFiJKU%YT@{>x+>Xf|y@BrHlW{ln^h2BiBNhaK~ywi_4F@qY-A8 zKG4le^&WJlHWtlrj(pV;mNiX%MY6XNS3C}o>hY-ldSKJ<8O+1_+H*q=`5eP?`ejr^ z;kykcm0(EC!Pci7CQi~~GoJBe-$ZSBM$;PSutLU_7pNbF+JcM>V75Dfzh-4kpTl|P z6kv$hMrL|DMToGs7BHn?pAUS9D$+vKja~LD18ssi=su*3tzzK2xSVS(9RfO4`@hHn zz3Ojw3MUIt9iPuwIR&;2Mh^u|aJ{I#^*KLN0f(jeJuL-4`@X8CZ}}INx_Q}$Uc}mH;eV2wf z`%Fmj6>D3te9)$3PrhuOnGIVJEIBCGSXm)L}ZU=2;MU8 zg~Pa{$6~vDL&RBR5*hC$)}k@42u&QvC|p@Q=Ucbz>h~l(LU#!Ajdw4i=O0B4 zD>#wnTgjtyth-8EzhePa{+uT#vre&nCAfj7#eoeF6`JY0xgr#@BFn-7DL>Q5N!kLm z$KJWbsnk|l)q|eO*@Si?*!7UTHFHNLUQ6#hBZk?USiQQkxFjZ3|*l(Z2j@ z7mG{hB=yc%1!Hw-K+t?HE*iIg9AWQ`8|1w1?u2aNUQaR*MPc`}B)XG{Ed(K#1|Jtc zvK_emdj}&phDeJlPwg9&C4zA=%0}KW=FDNcyUuo{g>QbR%W)Y9O6_VU-+Jygu*ewc zE4rP+a)T0a@q%p%kLYtu7o4%afB$D}LM%bjX{@o$3w-O?n}7_aB%wI7O-X}oz0AtZ zX}d5#TWVi&ecqH6a3XQRfwEZn_JUyr{yAEt_d)YmdkbGA z9s~VpwBOKkw6c0mBLJ{$Se5itBcyy2?rfgC)21n4)?9=!bgby67QVfwGfjv^xlS|p z5=VaV6?yaSa* zo2)s4=Qh9JMxg7phGhDi*g{lYI`zQ#M=iv;!tzBJb1+!|g8K!>Bb9e9yrL|qi`etS zvuP4w;a-Hwer9@ia8KE4pf*D2WuG>b@z!B0l5w|;0`i+E`yvxN%H?|9oAY7;k{Kp0VJ}mI zWbdaOuP|oy_vBdy7p$xe8XCE>M(hnP^M?oJeVoM2n)9RN6;3Ve39StoVdN2qm#(yh zK|{oLse#@ceHU`x5Xw?oz)3+I2CWw2$*dP*QG<>Q`_12f{lzF5pOOL#3zuRrLz-ga z^dW6IGz8G#B~{eqd@ZUY2rQ)PRGxuFjsCS?$Og=IVYuJEcLjx#q#dAOicm8{C<8J) z2Cm$;xH-NED?GA^gosah2e)e7|+6jcf+KC~;#vA>V=zJuAO6{OjqyKl8EH z%)teS8ZU)+fxvz2QiqvqX+O^UNo!u>1C`d;LW_*q z#1cKbY@uZ+U!}bEb8Tc(M(^BD=wK?N^-a3t1M;)}u8rE}Er*QX@~HX(H}derK>ae$ z%+ohE;Hfong#WgymF}S#r?x9Qw&J%){{m;|WlTo&Mj5mI+x-?=psn(pBtetKDOrOM zE!ytJs7u~T#HoC65g_uk0E;m=wKs=L6UQgYPoYpS_IXs|h8fM_D^tltgv+8I`K@@g zHZJV&=E{Z~!HCPkg@RpK5pmn@n1C_hVgN zRffA;xHBDE_$+{6hzKiRZrU-}HPNAbIrA+|&Qc}MfX)n3o7<99PTI*%jwHCtdd-c$B0Ud)O?C8D2yGxh3vsuM_3 zV9+-JT&Lwv*bb@Zy%9lpJ!&svk@oq~RuH>-Xa#(5+%0s0F$*$Wg79oGTO0~=Ybr7c z-=WFk8e{1<0os_ANQ_|1QW2}f0VixNYEk4e>%#JcIY`t7(qh-86Nncec|D}2hpiQ_ zAa|340MMFvI?Kk{>_ZydUH!1cjTLFJnvMm?XQ5Q8eS)zr!A%aC65(v_7OGOV7NFI{k?QJFMHqBU}_ z^`k0-n4tDu%K0D;wxVRMjrz3J)PMUtuGLKFo)F9~!a*C&D2E#1lMN7qfzh1hVUq3R zFLzrm_rd|rjlBIzhq;x{AZV$?oEzOk3{98TJIq%|gL}UB0@axGeF7Lw6*cM~&h~6c`T{#g}_mbbQi-$S>VL z7EQEvv1F|2zaYf(_HmJ)5jp?W%DbfwSs{9J84Y^ zSQzhxZZ92ZVWM=Xnh)0YWHpa$7UvR7x!FSEN>`&2=_@#Tukz%)W6yID7pEU}hty#8 zg}AuH?)l0c(I8z~x@NC^zQat`qgl`SmAu0%6Ah9&f=AXleE85JWDkH1P@Es*@A(8cpDaF!$Ea0NYWkzt=96?ys*A3-79!#vR z3oCHUSaXowS`I_sm+Sui3*2s;PGK5{?qC7GW_NhcG?yR@drm!Al%MD2Wu%Y^HT#-U z8ufx-LvaRXcjKpR%Ydg-!U8s>yi)$ba$sl@cQE>c4WrGC>!l$R3U-ed7Vqs6D!9V52 zEb^{@_YZO<3ka=cjx6TB!A8!0KhIRmd#I9$G?hz^Ra4LfGN#Zj!uiVukl8luIM`Ud zF`RrK=!!cRTJMaT5eIVhJ9Vc|r;5Nmk)q*ddlNDsmtFsNKv7=kys}Y`+z_fukLP6X zg@+I3ru^UoWT<+;JjpMO9@6>g7uEfds8TI-uhV(+FqodWAof;{!VL=cBY*M^zN^2z z10F}Fo+zPtK9CXCgUDxFHr?&F_$BUfTaH5eMMAhzU-aH@=Ec;{g|{jBH%kbOXG*`Z zrYBzHyCnB0Qb3ulaFfn27BjQLC>(orvy)+u7P^Y{5h{vxxglwZOs#R_qZ@~x!R@LW zi$&i*(_Ez>J7UtMQIP#Q0g|ca-ovLK3ZHAbKA-0#&5k{3BS`jLoBueIl3n$)PPs!A zdQ!oGDzj;jMMr~0go7=EY^E&V1r(Y{KDQPla{l0TbXamocYvy#aE`I!>(iy-AcaMJ zrdYr{%Qv}X8Ics!XyX$g4rBTx^g;mLYO8Ag+%@f7iU9Cy!WG^13bJA0jRK<^@>IhV z^I52`rP26IGOI_}h7lXEJ1?^3j_8tPIe@7fKOoFrKK&t_%=lGk^~+u21?{>4tp57k zeklD;1_$+uy+IUyZECC^pPOjxHD;z}Y~X||_Bwg*L5llRdh8mane8N>B-TlMYZ;%Q zgni7RkK^6x7301Opf@3w@Lu1vtyHM>a!U$&)%qgX1;ZE38%O^lj0nh8Baih9zm7XI z4U-cS&p!?{svz|dQvmgV^a+{qQb%XwZ`R0HBoT`IXnqA|0_KvSpN8&0X6y%Gj%~#} zcYUfWFU8`-c;#g@Oq0j;8W^w2Y0KcMiAtuqvkh)C+h3?xGD8>hxxSO1T=^;qUJom6 zdc^6?brU7r6d2#b^1vVE{Yc+t#C-8>AZ}bzrCDQuce=p^U@Wn2|+f7h4qFn2-UmgFF0UmxjJoT{q^C zBsXtA=9Ku-g{sC#reFbn_GtcL-L&i>{ux(#)9c_r1%$TRWz;%6k&n|gu<;6{kc%9w z9DSf|*r^hg0eVj+w?%+mDaSOX;dF%69&CsYnETk3MP^>Ge)w zy!g%ety;flgKp())R<{{sZm&MQSol9M%C1(1aRNreu!d((w^iSa7S?-^t-;56AD}z zt0PubtsBi#u07EUfm$o#&i1Qk5Y5){%*kv6pZI`Jlq@V4%JyD&PRz&mQ-kPJ9zL%4 z$qOvh_OO=i=MxwZkRDWUjB}4^=ILX=-NAi65Q{Wb;XY(|Lia8sVymg1^7M#_+^6aPk@H0Ea__FP@?TD?eQC{c^|=xYON&TzJ(K11_sm<1ICe zHN1fuQYC!mPl2&nux{;}O|y2Nm#M-PZoZ9YXB;hLC;oQpm>5|75;)0UBl>-QWr+gc zu*-W2&*kM79~SUC=v|q5WHzS%-QIL<-P+HYmr|mpR~tUY{F&CK0x1VXPKBa3?K4lW zfmR0ym5fx*C9fV*%F2U&!zhVMlu)v|?r!=$pBX&9#G+K|(AZqGmrAm(p)|=COA?oY zgnaV@5rE8~!ls}s9VEa!HL+Dw95A2pVc3^}-5i@&)BEHy$3plLD7b$qf;#x?l-*w^ ziSAFrR8YPb>|Oz8Dk-(dz=dBlVSc8Ki^2s51p5M(XJ7U zuwLN-g0k}{O_vNQKHOOb0J`B&29>ObJg-ecHyxMn06}(VS0@2J*gV*wj;`O!Y%$=Y-i~nNkLKmm1kX_ji>#Ojucr0oi`fm{MJis1 z)Xx~C(|h~k0hKW|cXkdMKqnZiaL#sExnQ!fI|4O zvb6)|u694~ zGDG%KTHH@hOwau=cqc7ROMUSA>e88Ivy5z_xkijd#nhJ^g`(1=s3_*jX`PoppKZ|G1r`qP?jX2Wk#QRLu`~%tCpW+_ zaNBSIS^=I6`O+aYgg77{K0oq3jG=j&|GO(3cB`t~?kC(xMAStPu17_pGnlOh*6p## zNUC56dmYBN46K|i_#CiL@D6<7X}3tD>4@dGdea<9f2ZIXb1>Zmjt#X}ELCTEZ;;C{ z_ZZFXC%22X2(e>b>Yc{9EV-9?+SjOq>5Q9@TdZziJH875c(0GrUR9IL(_~>XPZPO0 z=alL{)zruMgkoXx7n6N%>xi(KXEM*)tlt6;*2&Te2g%#Uc-?;LiZ%6KJr-Eg@;#Rz^sBf~n7=Z`FC|5Uu+~@nJm-oG(|AJ#l1v`5;yl>VkDZ@#;+F zjBK9LS$KKib;v1psB9d~R7u6a%MX`cL#|qpCso=b|EB`)j zLOQ_d!`+s?q1BvgZB@qQpTQ&5dR=GqN!lIRC!w>?95WehfuUW&)%7Ou=gUz4%epSbT#@dxQLLxiYy}JG&i{jU14Gag6_~Rp@lOE7SS?g z8MZ@wxR6paKyEH@UrY^$VIFHhyda^(d)zk0uCW33OckrXOV+6Z}38 zrb&and}M_ktPJ0IyL0@1F^RhKv*}$2zRy>TG7=2Ui26M2a`h`2v_+3n1SBg$l2UW(uu?^H75&PQfGqJy^RhOF>##3t33K{TUbz-Fru zq7n-zJ1#^@?4eW`tzi*27z4^B83#dH&}hJ z%&?}|?B&BSf6S71^3ZchQB#z~R>m6?GcIEmRqRK)VoZob6j+7dpSdRJb~oZQ#1!@V zobV-B!FLgwh&=$266msnKZ0Qraf;r0Di|r`X{;X)eW7XbfYvD}Y0d2+ZHj-N^|PKY zxWwIP3GNluzkw2X?mh8k9lS|_w%%z_-uwCG#j#nyZ^~XIc*>%9jdJ<{!xP3@kA+}@ zF($#=lE(+1?RV2ZPwoHKEAE!yh^0%x0b;}!!^e&!k5XzV*CQumSC&ZZ=5saDg?g+? z*75Ywu}27+BPr1u6pXu)xQ8lHljiQ52FD)B&+jN+X`Er9V{igejZltl+|E8qs6YurQ7V4#DT1{iK3IViXxf!~A5BZT1JS8E!M9L-E1UBA ziq@WvpAiSbFyYl(eeHtklYu&>)XPQubNTZ@-s zfVe=br}qM`v0C@NUeUQaWpP`?XO79thYSvrEoqWkuFm-=Wk#&rOmSdloSov=K>>U_ zN1QIRe;`Zr9nb0!Y(wyLKoe41U~Ar9?K*7+&?-b=*+FX$`LkNy*960BNXSuv3;h3M z@2dl%UcPvj5KtPCZVZ%`ZV&{e5tWh25?t>F$#5hNbt-7rg3K z_P+Q2|NY}-_s-6oIW=d_%zRGN=`||N49jLM=d+zm;0zR>z(l;{9Xj#+&Dbbjuzc&0 zefZW~zmE&Q`@mWe<+bm=yPshSfnRNBOVRbunC-=3U$|hV+>8vBtP|fiiZ=(ch2K8n zz9u2W_d19YR)vK{==*%_Tg5WY*JR;|iyPyG6&oKq>vcAB%xa$EGE=Z$#N=#vsK_EW z!qct1C?W#_*JmN24{6?*I-nL;JAk*2sMoh>uELD>&VPNdc&ll5I<$FCLe;9weT;Hg zo%5`lmh;EcvC80ngY|o{x--wmx^x36Vd62@*Q3D&BAt%4GVx|-_u+S7KZEkVRlrlW z?ykq_-upc;m^0ng$DTRi$Pd# z?}_K< z9ULe*n>-Sa!mtp77t}I5(wT8n)QIW`<4ejF-dGi`i*RCq#m~slJUrVIf6LA!qZ?ydysqWPI~E({o#Wx|PHP36UGbXU9Z5E^?@tJo{1&LfQ^D z;u2Gqx1?5DXL*92a5KCxaZOGV=qc^It;LG4dhHTN zDQL^+Y;|wq?6)OmXWw!A@$WrRl2IV*XoWL6deyMCGf~3((`D|gFHRS_VqBK_DD@0t z2a2NCnx>qb3hD)me0f!eJTA>9s43Z26u?285O6)vsx3Mk=FLyLk`gkx{Q{d^lHmnb z!N`CNG2QmW3v|nED=UNN@NFSZE0{Pg`=Gq-5OV+j9{N=jFj;<{okF51!Nsxn@9bb)LBEsFTRHKkn45>Ej!^Bn1 zHkWLRz2{V{vaISA78C3DIL%;i7)V=eKh~=^(ie?gC^X-0dTDd@8{LV8>mVp!&?$G~ zv*=NVi_mP&Q-yP$e1bn3`|QYk!>TN4 z427DYj|lhUfWc)=4ZPTILERl3l? zT5YG(bDQ25fcJLJvfz~LJNp=4>zrf#qjL8{_n;O)`Y(joH?_iX1!Iu6#4siO&>M+AjQjsHeAZf(dP9>pX?Xb@ZFt zQ^#Ok&m$#Dm#YizGw66g0gt*UX7g+Tjt%>o;KI}0KIg_eD*D1@B)UgwQ*73~*!V)c zmz0(Xj)+neh6GV#h|kUi1R<=-nK3@baOVU^VW@}t!QKtWGxlS4ZAne^7bdrr$=zP= z-X-b}hQWrWTK7y!;~VuOHv1*&7A%Zk`i(7Dz{@iiSCpJC3lb~ZP>!XWE1<$`?j~(Z zCV2(LRxxaz8j0SpjdSO* zUq|H56rsSzAI(WnNuq{N2hsY?3>x!IJ%x*he%9A)-uBFFZtM7pSmgDP-q+Sat8c%jW{=7zh0T)CS@v$(;qs$40CsVxQZ| zpFFjdW^?fD)6~+XCv!4ZJp`}btDwTvn``B&W<&}H3v`;=7x8>8l5Lt5?9W57J+@nWxp?I@eB4hQaR*Vg z6gsT*ZlK>~U?<=KIE@(Q2Uq(T%|DzeaSDNf>jNY;uazgGZO@tUQt`q( zEtOAFqjccZTxaKc6waWM@fMZ8DdksaJImLtZZG=hM#0TTi?y)n(Uy}TaMK0K&gHl;Wy-vYY`s_|Lkz>Y_R zxlSzPRu&i%Rb$yJ^iDRHz7{8qvTZF987ZstTV@pYSL=%eKH+zf41 zR}xQ(NfCvr3o|pQ`HrJf=@L|+;}lB-%P119OyGuqzPTCd$#vMBQQNm_a_5;++Q<}x z!=~x`Nba^bxL7224;xL82Zcx9(^t2sAmi3kp}#5-OnYY@ZCA zt`=>1{)Eqqggd8AuaAl%I+lA$V|G6j-VgIk=Zx$&CxnSBxhwYU6OncmwX8I#SFpd> z68#L1(j%-Gd+|C5E^ll?=bX9baz5KKmgk$U;-V{!-v)2Okji++0n9XQJI zFWIEG^k~?q?wD*XWV7HV2=GuoT*HEK(oThKH7?(YDMMHW!L#pVRu-cOO}-&~?fSB) zWzx9re!>-eL_r6RiC*l$$Y4Q3R{MEsSQW0)v-L3{@e|a1^zzLWC(fdl?GbB zCDPj0V{yTpe5Bl~GWz6~Ca})aKd{gU8^)TFtzgV{tH@jrYd}HGj@9wN1lNXR9%$dw zrm|j*fnO}B#YobufU`xBMctoeLSYg9_#@XrN#dpi>L~3nHVb|6N!<)MAOdKQ=S2bOP zNB4)UH4fIVW6_M{hKI%H^RWjBeqKJnOk`RLs1ul)Gwh8E9Y%=MI~WxAbb*}(OMOMG z&T2$Ae4(u#Q9rR)BzN)oaB8&e*fURI(f9#A_x$MHYZ$N&9PSDj*9xKO8= zmTkq9w$ER>`jrDNFwiVBCemK?j8K@M+%58p8(H^Vq9;2{3Ck4%RHIOUf(d($pJ6@V zlBE=qkn+$!#An+WfraP$w5qf2g{!D6$N>oKE!U`p*k4q=(KS(`z`iuSEhqY|9o%oH z)Yx2#0Gp1mTPljapB%mQ$OX@EpO&cE3j-lYrEB3eS9WkGNO9oRWf3vdz*+^V6X(~a zzL*caD#{F8JsH$IpeFtHG8c_5VLLcxA>)I23KPJdQ>F;8-?vtX3?5d!S$5SOoF&U! zcfDEPRp53BGOPivw}@SIA+P;5)dkctfx_?2V(x`6Qj`OC!uQb0u)f(0{y)ETSuND4z*)L9dV$~q&V$?*AqLzE{QeDlGtuA%>d_VB&m~hFC zYE{yveZ4$Gl?uV-Dzv#bqXt@yHni=Py0bQK38M=#Om+>Nq>Im+2ocq*R+4x{$R}IH z?y4onL`1X0J8yof5YcEpNg+_fDo$ z=rEj@9&D4sV`p#BToFm)L2R1uH3g%Zu#qlpH`+2*Jz+|AM*-LuR9>F8lH9Rt0#n#l z;DRzL^;cSup%ahIwA9|GdC|h_As+yPJ&I1z3#1I&5=IzSjt+z;*P36VBKJB+#3#IR zr#XUm@i0y2*{pB_&&FhuvmYFjM-&S9OW8UNseb#CvwhxoCORPX;rL!R^8?98Le(fW z??Ao^L*qj2%c$G}a|15rC%*MmDIAMa-s80u@Qc4RR@T$r(x#qG)rRY6pWj!xMD9%A`s$!Hj9OB6f=(6f=hwm z<>zKkx2oJy&4(K`rsIqUL=TrA-I#WByoe=p8Af-u>wx{ly~S3{;<)5MNew2rXOPGU zO~4SWIGbIx-eau=!Rhw=(dIi;SV|dP(HpfYMyI#w+tE|P7jo1P6NFgvy8Zbt$!!E) z`ujq02>!Emh9(iph#`4y@wOhyP}as~H?cD4Hsn3iMfPpQmb4_bw>V9V+pAdjEZ2Bt zc<;r6GtSy4?(iA(oA45L+WZ53A{5wsZL-+@%!G``az!K-KSN5y?hLrlIC;UuB!3uP zxnLzpH(YP}}Y; zgDz)@R)3CsJ)_5zQ<1SqMIbM!7wR^bPJ2qzn7@YDwly})1fx5bYVC5Lcx8#(B%=x& z9lij;ceOZ+y||71AU5$#w?*?POQ!6z3smSEZ3Q)gAogBssSwAU1dkSQCgOw$KJ93T z2DcHkkFm$87{#+RYywBqz;|uir;1=P)OL3Pq{m+bU)?SRW;JqS#ZY@=g-7uO4CTO9w|+okJL^yT2idZz9aV#{U~Z99nQZp`UWeVibr z5t!Ix9lwusHSjgbyP}ugP774wzk0(-Zs^$o;_h`!v90P20*aL~?4%d!q^;t@n{*7# z9vk24Wa!q@=zO;^_X8Mpr~(R3Q-_{kp?*pSQ5lguJ*AJ5JYi<@1K}I29t=q-ZGXZq51fmutdh=U)eW85>ZIoZ-%B zvyHQt8YgXMoWL+ zpmWkAY(nVe>1x!0K$}KWo!V8`a-CeIY(?pN-@3UT-M~_u3EhfqAM-cASk1G4iF?~} zg>odxe@TC>gl}=Ge06%g7h1TTtUm&2MX>N65>ogb4%-T8853311DpHMeFvDbb)``X2#i&{;c;8aYPylu;yb+_-1mBMhi z?F4RE9ijQ_V0u2qW*cYr7Rq??hA;1nD&LX{)&!^y&4goxO!oS$n0Q5me_?W#$>Rx| ze1l=y^OmJkemwZ2NOyuHzDS<)8h90^PqO$*2eH$wChQR8G)=B#tw$7!b%*cz%>oV_ zx;Z*^dR_10q4q6BrN;$QXe`x4Y2adoCp~*@tjKj}OJgs^8Z8PQ1 zo!_9{-uozWmXT&zdlLQ)><1MVYx{{p(?MF|CYkDRi#(yL__kT$po6)H4!SAV!1*cE zgBxn?@*#3M0MDR{TEHgqWe!R;KY(YDlmnh=H>;?%?*d@j-br463p*8#GAW_A`Ck#m;VETqA`Z{0&3 z1Vod|q;|4N)?CHKCN7|&kVR8wKs9_eAm1#sF5%&Wz5j&U_&`>E*1Rx3OulcI4{S0+iB6E8OknNM!^@d z-B7(wnj}-bT5^kwdsZKkIAfyV^pviDQe-M+BCbc&<6@d2prA~J_*yY`Bi%RN(f!MWD9oQBNN+e=9jFm5x;z` z+aau**-dO@+LAyg*5KgsA$X?PQLge4@7gL_JRgrFmd#tTv#7okNiV))2Srd}pX%Wr zXzRH)=22PaYdZef{yBrh_ zx`JVAH;`joBjaOwr;hK`%y)9Xi)Y@L|Ej@Oyflj>ha^ikvz~rWS%b~N-r%XFy9Knv z>^S_jxKUeHFMT8`8EZ0zx6hD4AVn$@7+O}lHo>1-b_ucWTbYXQw_OTKu{5txCBHT4 za4vpsv<>Twck~yV%G(*KVyxXM*s|XBd2I^y8%ucSie_)0i+@#1EpMyU-sh9u zt(6hFMg}9;Rp@<+DqLN3XU-&k%-t~LGDhh6n|b#K8JF*e*h^J>*$210<2l)hCL~s` z->0AYDA8Ut@l~Qwa^l-lT*_I`2hX0ER-C{hNGg*jkl@m*EH(x=-{3k^LK4dt88rxc z#7qV;=i;RIuW}9V51qF7B;X|+?5wC>Ka6fq(=*2ZZ72M}oc;ZCSrOv?Dzo`J)Lde! z{{UG`YJ2V$PCxAH>KU>|=xaQ>ui?<+!b-7~ia#l88L_+h^)%cs5Jx`JIb@>P`W(L9 z;{=^3JOl4A=3a-qQar6i-#vWQ13$x^{Y25Y5mQ0wTrq*=3d`brHbmZpX>5``158h+ z%GV;;c$JVv99@NL<%w{vZ~~)q1HnvN7iD~L^wJrV(-E~W&w_pR%y#D!FgtsXwFHV3 zy|<)6;FVOD4?qT)8{;n_r6!12gL&d%DW1dLM};NjNT|K9LPrNbkk{+X_=TbX&1*Uk zhP%7I82lpLse^(Mm%H5?Amz+{I<1s0q(G!DF*U7%%J_P2!>ttby}flpMrlLhp>~{LZ;-F=YudkU@icv+t z|C_$v?uS1~5A%Ez#0H=s&gcZ*%$dt5&4`!;=Oq!=;J|otcPn!D2d-2$Co>=^Z@F_1 zy5TQYTNnNFZ8?PK^$-<2uy(WW(lG7}7SXj-2)|=<{qyup>p_0sp1bI4z$nI|^6$9T zDYPeA=C<1*jE((ye5ccPvf80*+f>b!Q+j?s!0BKr^U2|J7*G-E( zSWi|Xy>nlp*=bMjG8iRr`Wjjvjmx}>Si+-I7cV7L0Bn|qV9#EHvu=Kf~ zXq^AN$pOPeya0o9J#yeW6$l}hF`2O?fNM%DvEbVP$A-bNix33PId!!(=M9fB4T%*eR)*K`2*o)L5nEwB&wB1ah* zt0t=WF&H4r7ktYys^S=z3{i7aT|4#fVdJajp$Wz0Yu(T@0b@NqZwn2eM-Q}B{cFvl zqLL(_#x$km5LWSzvAxKl087$>FR>~MjD^EIxd$nxKKM2(qIR%6_7R{)Rop)Gui1jh z=R$yBVSQF|UeH)%L=C;b;Ji*iQj@?hD;049)SS1TrTEj)$I^pb2da(F?H%bv8SlA`#VVM zQh?J#7@Fn{0KgdLM<{|(!2Qg>;WW4orvLc+&wCU9uoS4VmlGrsn&8(?I!pk6QE#o0 zFB4!}VbY$+QkEHxfi&J3yJgFx^(yHt2;>d!g2<)BfUS#0xVKRQX_eTrH258-V%LB; z0zXY&In{7`1iD`jUKuAlCumvb-^vFtab#E={gNYH5CKhCEz2G=iQ!!}2d`4snuId@ z9EaV6URB1DbAoHxL9}+)0P%R#IJt)ie}7t#KVV5hN^u~nIUNBbN_utD;5&#KahVf} z;63r|bVBf=$s$EB$i}0L_A3JBwtTD6Px{S%e|iWE2n_W;^VChf#uGomOF_~*c!Ch6 zOIX$DkZF2JsKa}cd}-|ND*;DPT%!T(0K25NF9HQ-uu{An2w4_kUX;pjJpRXmA|M#P z_LuhMzf9u?xMkWRL>Yv~c`Rj+Q=&U>{PP|^F_|3TN%q#Ahthz{4Oo4Re|i>(P4ZA9 z({Cj@{~XJUdj)~!U~#$S2(3#HxaKcPe*J|8nHRw;u0wpm!;S;b0V7j)WfNuu5;sC$ zFglLPB&bo|J?)Nars~KHGt+z5{KOk zDH^#%>9MenrcfhRjqYD=JWB;M4cs=ittX#|C-d#zW`ViNI>b^{Jh&O?v)MA zH=y~6@matxfJPd49F>2sI6VLbxk?&a*Io?_yGTQ6oAieRfj7~j~urE1xa;tb^kA2M+5k$J2mSw^ z{@-1zLJQ#gvW%Yq(m$QjU%U|a>V*PBO5?)g+~J>HSKS1L9&ss?S?!mwk^c2RHvh(& z|FQXZmHwYL|7M8)a~i+7r~gl<;pGS3i!HefN0~ju0b1lSE>uPsmH7?M?SEMHl)^c8Zi zj{^lW%6FR54+p4%PJ^=w4y?3)H;R7|?8OG95GE1~hK|1e9pD)Wku2aBS@PepkCu}X zfIBV@4}KwV^EWT_@8R@5fY3pm*{Sd-2&uvX%VMDCBm?qk{8JrZPyrg`R|4b-@>QuF z+GkU|&p1SQ7x((F?duOt3qrr3rjl|ztXQD;03tHlU?KKbF!ATQ&5;h4-5P&^$|Ew& zq0ok6wA2*&!YarHxKoS&y!R*!IYOd#{eXnC(0CePu}=SPe$?e3KR=GcdbrT;SJIX5 zsMFs!w@5}|c?M-*@p4O#JxYs6s)QY*xnYL&18PLQ6UBt?vI& zgA~-j7+$Qwsw|bnAwl|C|>Ksa-0m99< zbslLbKt2yH$jmXmS{>zZK@ktY?2FgCj|=dBIEJ0m;1~PUs_<(Nv-GGTfAmtJro|3wy*8e^G%<5+v7ywFzBN!V=iFwJzF z_xUrduaW{f5P_Az{2wCJhR9&UPZLUnG$>04ntG;8*FJJR0U7PzYx6Y71F|AjO6mWD zy-*IcSm>US`k@G+Iuh#R2}>2o?Y0G~lye~J4wdu(d6;BKmpi_5a3No$>}0_&6shsJ zj#Nn;dPB9dyLDXR@`tGeumF={pml>EuE%!`d{r9ghtE(E9)Q$IB0L=Y-N~M?14B3y7?sT&@2QAr0VX zL~Vjm>>=eOP?qwE$ok^XnnizmiGSabac)S8vJ%S8%L>K0G z=xg=B4MbQ(=9r6#X?&l^|5Ef|~Y@WK!Rpj@GSdsJIff0D+O@uO@%|<%I`j?y9}@!+!h(EVtt9mMJDAY~7%9*jQnS7VTpJ&UbJ9sp!jKvp_j+g*MbEU6m< zE~Mw8%`W~2rz8L!;P%w}A7m8(8rw-6_W$AiPnP~v$c9YNWX)y1;V_D?LWO*hC+48` zPpqM%o_<@Y^q^x6jK?;-N8Y9cgiGS2&mKAb*5nV*7#{^Fw_7WU`0=Db6U@uR@WI<7 zMjT5URJmo3;%K5AGLZONt1qV=+w`zegF3JVa_>@sBci@Q3n!IG_;Ck4KNe0m$T%v3 zGhLD)rerk%g;6)g>=^tHD}My&kSA!rb(B!$umm(FvCi-OhdJqjCX?`e)bUe;k#d$*&4}*wE$!mB!BTKh|ax zFa{YxUGI@jPeTE=Hvg%~<0`cMSUs#DZZ?kTXB_>+1Vq2_p`ZPbD%J3e6K%4-zPEd3aRr-oh_iuykq4ou(C@1=T5<_E&<#$wk4_wk{On z`qQp&03@l-cAeU7F`bfXK~AxpRlJxfc8q(1{qF& zrQ`qdxg=1``9yd=chHmqQRzIBC~3W3-EXa1K1YvF?p(Z40faxdkL75N0Or%RLdOSO z)KWwQBXYYb0h0rTu09Di)+Sf%4$hJrM`!CVS-E;s-t+;G_MJ6W;AI;xD5tRD9fv5} z*#akRnOn_>^o)wdk~3?u%Pl^)@mC+fnB{?|ss0{y5jY?Y!x6&U*5KKw2^;b>@Xs=i z9~{1}v$Il5QF$g8EFc_J=*9c0SbV`|(&v)w+&0`WYY5jgN__nEOzO2ex zx@@N~36Ia&Ac1RFaOoNLHXRZ3Ytb&YO^q?OPvOqx1G&vZ=1b+9E`yXF>CKwEe8FO| z=MCf7n77rql2UW|#%<@_eRzAs zRZ@jHeDar~ocdClcSoxHr#?3!+{Rz5FUq8mSa8^xRA@?|lew=f*Ub3M=VMwMc57b3 z7TFWf`WA2R#9P`;-nu!zKg({MMu*enF4m=GmYR3Vq1isSd3!1RP@cyYhp)>g){la4P$gK-w^OKkELn{4v+SRix?3O&$a zeawk8D5&UI1a#t_uz&wyB8r0INwc^{s_V+B5WzuTSwB+k-9DR^IaK4A{-Hk8_4|{V z4;s0$L4t1W3hj#ZtZF+treo_C%6t13Fpj!q6tuS31nEWoX@A~BF#fjQd=}ZpT%I%7V;TF(F z;}&=2bnP}iaFXTygHhkZDeW!tqGN&A7K<^3#`u2|t}-Hgv%M!&wt9IPrl*Ej%H zH6U9iORtj}PcZE(4iHU^nxZ2yVLHtRUlP_EuLzgkL+G&NHe9AQwW;Yo12frT+w?qE z%yi=M?Wm}*n4~m!eH?v}o#})bP(Y3CmZo9ot+toFI4efXag`Hc>)jUDcUE&u3epjT z<{6QAUr`D71y^JrcV(XOHOFrVJD`nZb)scP`((2-aSmf-X~4dF?_oA#-lAYH8$PJ! z+9;O5y0YV$nB%qgO~d4S=Iyj z09{Oll^syjvt}pSHzp^5F$`UB~2dego;gkv6$0vJN(ydp8Tp;9n$t zlC;&-=`A_#UCS<=ol>H8W{P?Jgt@NX8Wg$vyt?_IxiPcsa=B{3m8w3Ctx-^=jyF`K8J8iFdW*fXGg&3yJqf+ zY-)RGAD%ocvVAXL4NfB>xX+X(L`PW^yeiGRMV>|}?>ZKAqx?+Hta_5dvn*ZrI9bWU&lGsE~e+9Ua&~pbC!{K0+UQ}Yrs{9!B_hWgKqG_ z&)wW?S~H`kHDDKRpD@{E07&+gD6w&Wc!_`f#!XTJSc+BqcSd$Y?+;4G-e0bgN8_T` z#^1`tSC0WVXl82hg_W~z?$1Xxdg?N3y*-%D?^c{sJBgNV`|TX7(PUg%tF&pRO2TB3 zQQ3LDw6sjNh2Y+;SNQyN8aVbHV+SG!EK?wQ{SaU>Z9eF@uKk{F7|`6a!4nMuCR6-ZeJl=_G*>0hKasIfxfuP zBA8>_h&wPRhTcGgOK#-;#KNT}t?iH4?OM4;XLQ@GbF)6-)w5~sSb0|spika{(e?3| zw!O3KyrmZUnk+44zHF_@RFA~pbP}+pp?s+qng8eq3T3&QH)qnY8SI4!*L5$x>CtQP z+#dKNb~o$Ipwi1x?1RqcpC^`>d(-o#-_CrrpeuJRaK;IgfyFcJ@2+1z4e!s~609|z ztKQA!9Tv6}vl*w_=Cdxbezt`yewzG7UM_<_R05BDn#y_e$}F~$ObAM>v= z=ru!)x%dGMmTZ}Gy)RL=XZ*3n#1ma(W)9+sHRjA{_pHT|&uVe#lb1L$(O>}azIS*W zlrIbiZ?qzBxD+<)%y2r)_q4%{`UqeqNkg0I$o#7TPyp(+*9i_dpY}lE$4d*kMv;bX zDW~Mx!Y}nOo%|GdAO~KA*fr)hC&}b!R4f@YU(92pr4{7X&THh|UW6=t2bg?eM%&^N zW=7r(LEr^tHw;=3DK1M2ocy{K^6sfuo&*)3!Wc<(_y=D#%4ghcavk+s_x3o*wM*$+ zMI@fGv;guRc**J~>f93VZ@jnKD$}k+)z3g0^eDlY}0iO?xyqj)|nmo38>84SL?PR5}pen8=Aq-}Z? z7LV!Ps{Im)cjET-1T|~z$C-QEHkFLHIsz^;MZG9DD;RLCw9t5%xR3fFye^o_ddM_6 zukCF>(ps#$pxacei!7;@YxRlt{%1~zZY|g3wVfb+7uh>USy%tE zcSE1V!J^po9|w&R)b>-B6o*Gg==wdIC|a_C_# zgsk?ZhiavCFU@%1ehqG7t*>Wivgk%?Udt8i&`qBHr!eu{jiRY@f5l}-TwehG)}*rY zj(NpKm)196wOJjTelgnM+E7%QU9wng6oO~J{&7Bg5 z?Ckc9?qtklMb?U>CJfl(H&h0f5M2GurS=O71J;#o_P?fefBKD2n9-02C_;IiIkU`0 zjlDNxP3)`ANgGf#Dbc4+=WceFrg>sxjtBm0H~TXvo*-|ODuqP3Uf&Rh#YdI8-QLvM zuBG)5K7+>nU_0BOHt9`o!azSf<6rjt?nj)@Wi0NQPgnf3)ES`M5ex-M~ z3S1tH`VvH`W+umFO^om$A9^N!i%ZDK0f}2}2?Xv$q-BoQNUlMt&Zr_BUnl6RQDt!5 zki@V&mm00dLVlhludkX8UcFAh{f<8K@FlbC{SAxz2W>gk+x%O5UYKFC77sdanSS&* zP~8z_i^@k7QaiG^vGX!q%laIumP9(_UfL6C)eweD8-;5_V>|3NgFjXqzPS{;QJ+d_ zd2>g{Vz0RdKlxQIqE5~j4fY_&GNVFJWMyYAvHt4)?{TF|u_X06e9AgjPBV#iRSJBe z4vpcKA~7eZN%okg>0rulvsNsu3J}7sDQ8c-m5ma{#Z38t$cE4ey%~#pT0ullO*mU)<7g%lu zg4iido}r|x&Z@<06>0~1~zu6yDN)C{QT&P=`J7u228(jqVw zty*HY7gY@I$|j?rVT5e8La;mZ&_I+!H#O01GAeU6+kLG$&Z-duAq_lTfU0Bh!OnWK#r+(6 zzsObEf}!nJ_*y~VB}<)xI}US+`)Sdkg_^YL+zy3gy(`yu! zxSi{P84`wi8)h=|o$}vX*?)pm0=i`nU_1!pyJ_37F)Sp z$wBdyzqYm}3LP5}GqDsM!y`glX6$Ppp!g!xxKo4=>mMz7feamD0^eDNI`m0!e%=tH`Hn8ltYQy!G4hAVvb~I~6mg zMM$~xNJtQ`Y^C#ht6h_-QfP+90-n%29unQMb+^cz>UQCA}dmdv3J^; zD}>k^3sIvyCp{YaM7dqgNzAo5pLi^k>%!J=7W#NJ8f?S%_+5fsRPZzmL+#`@OVFQ) z)87YI;wR`m`Et~B^VqPBGE=rdf$SJj5O5}M=Ts^r@}i@|%o1qx9wE`hj<;apX5ExU zD>S{$J9dyWUp|I^Pi{*ol9MscODtZu(ly@Yl|_qN=?g}oz4h#mRW+K8(tStYk2n|Ml2G>i2|mrYK2gq6c$P($M;akxoBsYT z{L6>BfO6gY*k|_T8Iu=&Y=eF zQixK2@9=kazP=4eF)bbPh6WcGIfVu5Fvv9J`n}0-sf1~2(A&@h2hzj7TUY^C20uETmmIyJ_9rt@cc*17-6ax}kv_Z@_UGj$caAICMfdA7xW_-w6+g||UcGOO) zK9o-bprmnNH#P=|eDWZVUl5#dNNIc$V5r%9A?~M0T6cs1QWfldz8sNK0H)Em`QS+1 z|7JUPE@;gKuP}y$?^OcmPAO$;*E?j<-&1Wd2c(uuFGwGe0@<22pWO*$ty94N59-T} z4S-0cfTBhE(oRW2B${FcWIs(JO-g?_?*F6rRfsxNLsLi%3+6=lk;pzG%@bVQ@w<66 zbUyIx%0g~$&LNY17sTTaGI>6SJkFyB{Kt4EiLCX%Y(U%%*k*^Chbb!(Ql=FkHSBO; zKjd1=Rq%-oRS=o%xwC+L&>S;l7$N!m%dYGH)kmlTvzJoyD9oJ+`Fpx|$MFLEWVxCyx$ObwAVeh~7 zC?RnwptCLNBjMPe1;UYuNlZP41ZZ0E;Xu~TtaBp$?vebTF%3pJA}=SMD1|#8f=i^ zaV2#TM>cU&z<1rO#WQ1DAnMRg{_c=F2Ta*} zGXwnNVBsws2L5Yz@0BRnKfiJ)t37|*_@F0yx|S>yPr4rA(ZmMO#K)8>xSM47!uFOYniZ^D+Xw zGYN~`j7CPv7|=FqKHDRsh%g49)&hrukg>WE$gsne)RgW}W46SA#1Oez$QE9#JfNIE z1Lzb;zONT)f{xVbZ-Ultio^6`S4#yBJD-jK^QbNBK*sY%V0A6>1j|8+2<~e9R>;E?qhu_4j00B`XUJ$`N6wnhuiK<<$&sikmk0AmI zN*wj-(7vn60Lvrz78j9axMn4Q0BM}BQywz8N(PYPe&+C}#}NPH0Ac>Z;m|xxzy!Tx z-d;yWw=|&J>>VSw_hDCgGGHVT+1bC<{O}vENk}|R?(hz%JLP0Rh<)FK*N5TVF#|Y5 zbdx?GJc_RE!U0npua_W=7KH)Bzs#vB9R{DcEI?RQF>|d*<_YW*I7O^!yCd;L0aD6S z7j%xH0_q^X0!(u@tuikRn&#^}fUa*ID8izLdh@pxnMi^LxKuueV~c-)Ry)mL6MQ7C zQ=p)$Zw8z7XjQibD^3LSi&Mx*`PVw$kpP$?viX`9S!_83xiV^F$D_Un830#U1KF+| zc;3KzIga3F_g*rg^|8odgF)B(0SlA#rybQpiJt zH$w9HzaKmBAL+E2EMRC-iKGECX(~WGzUgR!b4ZGb7HDosD}&6ba6t=$?UKu3jw(e6 z&|NZj85fz|mqQ6+^A4+{O-%_K_+&4s_P5lJz6k=`l$I2cx#B~;`B#K|j8#mNfWfWq z+7TY9p%A2oWAkjtWKRP*o$T(s$U`;!J+z0u_=7oK#Xx=yq1TRLPi3g@{B}}JWU@C? z08Hz0mU21LT1wE|a)keA(tq>}#`?DnbU?Q0wh`V1%FZ08T3$m?(4b%mKMESOp-&9) z?a1VV2Fd6ClruLpCzF5K^`Dajq?ACrt|HNxH7zapGaX%RSwBDhrlo-inoCM{IISszNgOcJk zR?|BlGQqQby9UnMaZ*8m64>^I(t2PcpvjvVs#rf0huAZApTgxE;P_B9>VHd%&|B~H zdHHLq$%>7;)=$pdzEVB7D#rC9J+J^#7P1{Bu zE!w(D-TT!iX7=I3O8Yw-a=Y`nN}mNCfRe?B0ZT9|cFaSzVQIOm1CN=@Q6)PYUTgRN)0KXNrFXq0tWe1W}p-ozLtyfjk z2RLlTzpgKNAcRD=$6w8WDtoNdnIsHMw^4jMQ5`_T!}FP@Yzd&Onvgbb(6osn)5axq z3Q^c{Xr}AAL8qyh;$;XuZ#~G`m2qRHJ^9I&53PHI`~IA6xKT^3Ijl(xu}5O>I-}tQ zAX`KfVub}Nzon=o+(75i43}doK3fj*x7&+}VVajL0g@H#dnv5ZHaT|IC$OjZw6O(! zW@#k$qxFEj!Rvc#%~QLzf#k2cbUpS3W_u24g|=axv8~9KB95gKfYvO)n&iS^ccJiS zYABzo9wZW>)oWRo+ovu9!M`BUdH7xS)|w!D+47awg7#(<{P*=V!Hu_eA~_b7Y^(Wb z0b8TjJm$A)gqe(@YF(xcq<7~sD`Oe%6TtliRrH07lGT`3gb6i$=_I!|{Ck(sFmS%A z@ufH%gaFjgJyLi;C;YR!0VC_8z?g0R^`@E7i?^z4WVr86&rY@ zkG52LAi8edD5Dtxrv?phHLdG(7|j_av+v6k2QDloLm`I_6mldc=e;_{wL^xwULh`t z3DbY>x{%+^mindYz82ct=?jYT*~D%g8g3h%Q|cG#Fy;&rJuCMn*b~i3D5I=OOn15p zD?)Fp-+iL5HnRq0qf5R&rECM59}d82#1c;{^3!`vh!e_h#UZxiTupb?UyIg(3w$ql z!69GU_5>w+<(`e{q>FGrfFp{bt=Q}{ZwHPf#ySohDw~#XbTNyu4ns$J?bhyVQLfL+ z_C{LXNUkl{bXRQV=Z!qV4x_zf>A2Y|M`~ig_>q*eR7|$geZNg~cV0EyA-#^kjRFk; z{D#bDLFkj4dA=^0v2%gfQR8 zO+r*AqZHCm1{lFoF5)9;#q%Zq-i6`)MaO^|R#BipAMHl{E==w|6P!(H)7$3hzN0Fb zd;>s{^&n=|D*Hw?KUAgzVa|-dQi@I3*LK?Mf~TzS12oxsHA!CsZ%lkep~WJDM6-oH z_~Aq2DEx+mjK}5A}*z0VhCd6Pf3?#xICP>Ob|M3ZDC% z-8Eqww|nQ}<7IdyqCC&lLkq=Iodn^DhR39B=0ojpEy}V$LlcyCZ;x>tip*48hU~ev zTibEa4&XS2&N^%W@tYZpWqRzJJ8yKVtxHNzi zrMRIhB1UsSsV2AeY+1r-xt7yH!P{72H^lC6Gk#`nli*BVyP_OtX&KW}CBnU8&z)tB zc4D~(UkQ`uq22@jP)yip1tEdDL$9G+Z9tHEECR@_x$?^<2Bn2EI4aWxV#@>Gd$K=n z>(hlc2rs2=1-jU%6|E|ap!cDAIa;|&blP#Ed zmST(`&*xPH!26NAH9_`p$hvaB?J1=dzf257+{Li}!`^$xQ~maT;BPXbMMx>3K1PIO zZ|xG%R8sayvsq^QcjIfE^61{|>#@A9H2G)%ek^-35&QG@ zBH(}7!qRU(k&_`_nuE-;>uamiO~jn)c%?6hpcO;LXwg5;6fDGTk`PWzacuv1m_KW; zb?#uS>fsLDKL2As4*H24SZA2c0ym~d(WkE`A*8eCXN+yRrUt%7(E2;l$Bl*uE>Lt@^r>P zD@qJR#erdSk!`jo-gKwnaTip_W}%b%%zqo-<~a4$P7wWUj}bZY zDQ3p<#`|4Szoy3*wE{(jxF zKrS2ZUp81=F9Pn@qQe;vI|0mQc{DpN#bY*Mk-J)Ay3(!I6LwrxktcK|sJua$G>+rO z;D)djmhfxkhn#2M?~sgI)FbaZo+Ze!O{Nq}g zD@Q%wgwg9cT@r&x$##L!;R{Q+%{vN9hj-{MO5{lQ*&~-W?txG%M;A{Q)A>pNJ^Zmf z_OTbjLu$_Y6AiMQxE(MAbEVoe&etvNzBH3DUbq}v_u@<()A~Xg_=OrY0X%DUr!z4b z5@w%c>zXCq{+=s_KCI&xpY7f%4H3+A2bg=4R;#?xsnKL9*WqQ=rTr-!cTFC=fi8Wq zO&YiY<;?oRsbU#WruE=4hNV1JjIZ^301GTEIoqh>T(x=M$q5;VGh`$Yz;J@s?BE|~ z3x0zzTFRg!myY|Cj6?%{C^NWI1Ltlp29AJF{G!}?&?FOUC;vJ$Ddg?aL##evTRibG zT&x~BvzWLl*sOUlMP=WxDaKswtF_GM>iO*!ef1`C2;=nkpS`PpJVqH-6qu2*VB7^I zA)s0`LlK=Au&^)c^vt&4YMvGIl6~XN`TkhKjw&#BX@uKKwXXsK0Cn>&a10xS+ALIJ zjAbXoQVQm@JtWB1Ur3X0V^GcX$lTP|f{!baeNBpA1r_+}5FE3Rq3sU{Mp&%1wM1SI zrQlFBsA=e`9q+Phke~iK3wGX3ibm?`b;Jf~fWTNW-WK=^an^3Vdt=7=?s~LeEelLe z2XCYA#?Y0dO!W{UOYOA~@7;;K<;aX>RErJM;#5qCF+Oogvf+vf;6Mc+t2U(@GYSXw z71fRPV=HbAc>eQ znQ7Eq>^hJqaDD*1{?t>ZFIF;42OyY`%{#EKE(KRqKQFsC&F92$>gh(`CKltWkif{D zDK5JqY>8`T~pK7a@9!Y&)KOnunD6qaV zo&9RA({(&XL_PmNRB6c@-aGqUOqamI>twOpb7EBVU#_GS)Hf(d>*hAY0tdzy3=}3| zGCJL`;upvn?Gy*K1BDFwu0z6MfLjkoHJdv%5;&$})`hARdYZ_Uwu^Lo2>@H9N@)~P zM-02#9Zr6^R7$5|p=8-#89W&6%TqpII!vWa&aJIFcMHz!|H0%zn5U?TR(255Bi!7A zxDj5dvIl#g?U=BB^O>zQSyZaasrLbG`m*7~+aP(Is?9$G9}BP2VUNzVUL9-i$alo(k?SUXj3)? zwJA@PE>7-Apv(u(Eq&W@n) zca8J*+kpeVhaVRiJg;BPGwdp*$AxtQYkoy}Qufx56KE}FAy=%(D;X0`$o-f{$*S7r z4`W~Syc3dhIq5cCM5a_55Nky4VnuA`aGtMGJ*(bV_bN2N4Qfz@feE;Bj) z9nQy<=c28DNV;uk@5YCah}vdAg%~9E2$;9pzraYy*VoNX1wp33HAKSsT4y^zc@huyiN0wI zk-GHmrX5(zftJ1e#&X(lI`c+bc_0hw-H9+4ZnJqp>SvZ6mLVLpz`FLetb@9)Z$p$-Rx8;TJ9~^dk+VnPpg+5u$Dx$~L{z8esKj2qu zZl?~burnLcvqxucJ^g)U@b7qQBR);${<{5msnylv73g>8sijmac1lqg}?)GjJ*1smJ&7szj8_x$Jf`jrI6pa3G8Txz5t zG?X!<4|vYCZ0LgmW`KRRe6k|sdJd^ZnGs{@##OQnvGV8njVsUcn^Y$@jMd|bB|o-# zudOa4dh1Kpr76V|st0ws@0iT`WRMe$Sc)I%gAqG2Dd@^yuLF z_&v*wW44`A4l>uCj56Gb#IT?x)hYy8M~`ISB4?0X04iOqT(geuGWG|w(tH0-$hQCJ zD)M9yG*{ix5##<0>K7xQXcLmu9#RG&Yy#TAe_vfp^LJ0&v)sgw*Hyw>R;DA@D^o-O zRs2)bt=4>g!BbJ=nDZAYgcu&U4CT1If@$$BP7+$Gmqf|yvOpkp)J!0{7xD7 z`N(EMD6%oKEVuRM?xRW68i6|Cp~?MKEC0t1t$hy~KyvHI6#mn@;l+bG{|G`HSv&#e z;C!+P9wM_sv_K(QdP1~75|^`ZF7N2;;*od;1pTEgH8lLkum9)4_CAd)%Llz#Is8yY zVJICJhOVO*|M{_hn_n4Wq-FVERiMovV~F5y<02PMB+x&6zV zHA)nWZ^(;CN!EYQ>mNgUWdc9$GA3yW*$wRfm550qhS;v>MB?y5u|>N~!_!rpfKU|u zK`66duC(G&{sIJ+4}Z5JxDD+TVVXDeXLTTw9(--xRcKZlb!>KJw_uk;wP4pNhO<$~ z#D*K#?j$f5=2~C)0OJQKxN)}rCIwABxWDx=&-7YL(g$>BQ3h@m$~0>BqC{2qqMO~32-49Lgs93NzRv$`bdX`wM#7<7K9m>sp7SsHoTQVJQ?_p?en*^UWvj? zw>zB6#EPL%>hXThl}Q1}$OxD>z7nwME_Tpe1ixAis_=9|z(_G1y?mAd`P7hGYqcMP zh&ht&LRb)ha<^96A00ds>m+zAW~aRgBOt)qE)AH5dd;6}2I8F=b*_V{ZZxGMhp~Ship#!VjKaw91Kx78GN7ha&J> z3ld?@1vYTi_L=$&vxa)@D@lQHyF=RJ21qOgqy#CBUjMdd;LN?OGlO?++xeF~U`sBJ z%VoGjuD2qEKbr|6N%ggnNmqLJ!jU%>v5>ztw~7~jH&FR6ATX=Sn7gudwe8Kxa1t3(OK833HBui_E?E`a*#q3*S- z=L&QP)wTh?Zyr%&D?bWj8@&S^)u2-N_w8KFDi#CuRiy8Kw!clvYr=! z(667RPBitOUYP7{Kv@<@HRYv@75Y%;lB&p5N5Mt$&6+y2TKWL!O4xDr#3?lD;ZcWaUZ0F5~0zBV1Hz~@?!GzD@HK`6Pc>b>sbKn*?7vb8D3z~kF-aNScqTz3_a zl1i1peez*+{!K>`xbx^*g`4BnA7#ZJF;f84#DcIzq-nXxZ=Mgx5iPsF?he3&U-xm3 zsWZ|CHcwRG){d5rD>&by+d9|fxjr+JUh{O^Dvgoo!Dbe_On+^iMtb4`W~%2=yWg%u zAUoZyN}}DGci?ng{$}!0!5(YY>IcjvaA$<(zOkt|Oy(JdE&&q;UdSb+Ac*OSym~Tr zCqDSv&-X%#EDSQASu+5GBnikc-T1)w1#-#G@IAB6o)bzO-ID%+ie@5iH2%ehz}-{ z_Ton#)cmm*sk`wM%+wD;nY4?U(e3maDe)xW>XaUesX+y9i{(4g4ytdxOUD$Q(YG&s ze!lfeigH)kn^m}V=Y+Q51HpPgx+7DH75B^D9XO=NO|>Z$83mt-vjI+O??97<7Dcn@ z-Jpt~mc6_4#(Em(slzeJRIk{*K}wmlHd+SvSd~{?8y@XUIeD*T^T~U*y$l^%Kyy@S z$o!)RgUo*fsx*u;U&P$PmOy$3ECDs*PTraR(NHmugHK;?>*Z2?%SsGD;~7Iz6s6Fs zP1i83bdwLpCnT3=SCB^;+K8w#-raeAwTz3TyaL!C;X96$RW9vr=V6ja5jw)?ebdx4 zpI^KjGZtWIoDh?uulG>&g{_HaY@VmbXo^W2C!pRrW{{4vzWN#^W(VimtZU{= zE0;F&pL7PjH&uMXqO9AKWIv=&7;)$OVE9VaBjk8OXz^LZ8FDtz`9q32D zm%8V!2Gz-XM9bsQ2&wdW{Q%_T0(X4BW83IIck*@rzA|*v&P$Y$6PjLLTdeW4qPDub zN^R-{>;OZ4bXm{&$i+&NsZq^37Imw;J4?KO+<@5Zdspw7nO+3uk{|1FO-x0V!XIOg zMbq1hE zk*%-yNEXkoOTN1W{IAW_ozbv7QFrF*Du5GlcAo(QzUY3`b#7#Z4m8xO83wQ2QU2ab z9Mt#kYV4$q39(I#qAIz=QI~urIfsoK^81VPQ%rn!_qa3huNfM7A(bq>ujIayv`YY2 z^4kLscJm$R;#sHWk2LJN&2hT5rK3~y@9FF8X_`riu0n2+#6pfC0cx0R>Nj4w~ZI9P)-?WJqwCqHG&UCuGn z1{1^HIut+7l^Xf3u|jB=^m?Z0gQ^25ly zW>#L(&htN>@s={(=ueY97NA+amL9oku>@QQt&mf=QSH7P-RO>(Qlc3VVX}GO77j$- zSbVdVRwRKzO}y|+l=pPdILsbyr#z@ePMvYL_>>YtS8>mo{WNU-G(CNS-K6 zf560-=Lz4bys~w_gotP9$;_ci&g-vL;?%eDsEU_)5NEO-xze2I#(ICDW4tZfdM)19 zAn-x6C0HHDh9?SA!9@3%h3~`)0QH~A)5?c|0%5RTskvsk)9R%W>kp7OBUi5~Pe*vv^A1ET^Fqg~Ywn3GzT7w!l^zIu;9 zB*~C0^P2qq7LSu0oO)lW*Zs0Qbk8O>Q{PypCcaCh+IYxQ@U)|JDlp4^a|uhU14)aP z8xSC7y7aC#PQD<}(kAvm%X#Z0!9e2a%C zdYm%72dC!iaX`>B1O6q)1+x|t0F$nM+wJAAHAn67N>yFwYU#=Xm^+-9j?+_z48z_l zxMl4ngeJF)0M>os3jo~Hjy})kaf?&yflIL=wj?shm&<1a5}p&fL1=n(ZiQpV!HhT$ zr2%l@&Ks8baThNA1*B{002i?T(+)CcEQB#k20M51m};31W74|2kI}lXCwZ^t#RM0- zviDmsyF1ZW2u7HacsI2J7*TN@9?PRWFu4mjJd;h_JdxM6nmkhgM~{Tm zKy;NOEmvkTVyXU~?N{)F>1D@7?>gcwVEd|cwpP2zHs@iU)#LarpA zyFs?CyoTGjv+UVKaNCs~(R%sOmmt&bSG6kz3u>dpl%$tNz*AGR=x?j@@8df1;R+dm z%TJkjJydP|awpBNb0EGM(70YqAqnD$m=71tCRt|=WVi|5i3r=)k$n2q$tYjq8i{GS zHErLwa`{Iu|EPVwNQbVS1qIF9*OtcXH)}_Y{V==Kf&%J42CBJ;6@IJ))aq8MXGMDp zSCr>FU9Ek-&MG0&h}{S3GvC|z4z4EWjg@DEXHi3Yej=L(0A*?A%ANjzveqtzfzNo* zQ?S_DbGfblxs#eqiqcQdcg>rMLtcISqfscYp^s zEH%b=y*EO-$-iuBojOQyvw*M6&G3)@OgKu0Q>H`UZk`3=VI>6=JOyzU9DT{|23gnR zO@ahMP0V`?g4!QZ#>n6m`R|ztEQhPfo+Sf;WTkepj44uT@v}mITi-z0$ee-Dl|?~= zSSQfR%2FKCz!Y22ZAp{vl*XBntq2!RK5#R-{)*=ONZBwJ4MxzT+nc=pS>a4f{)I}O zA_&ry5u;|jPkL=_z9^2rTI66?CIGiF9S&_tD>ixAC}QjcfHHp#fnl5lHHG;}c_VMm zivfl_y~G~rcFn9-lB~f{rbzi7Yso<>YNLgTZzz@o#sf5tQ2ugLDY263(ml8UjiX3? z#&yPXSh;h~bUZfHDnug~_@u2C=~Yu2$3OTX;!|KzXVlvZmF9kX*7%BZ0J3`)0qq** zF92DoJRy@#0<4FIaywUqXoN0H?PlW1@sBhL5|4V)6+j}C9FxL3*F8=~vCYk%hRwFk zX#Vdw{GWg10&-V-tYC_0MPwGmTCH$iFY3g&<;;uN0Q&lp`rzsB_T}V1471k6h7%il z+JPgRS-Nu^;vGZ))dfzh^XgRaWFP%qq9G>*_kbzZp@eO6Wh1SdDQL&*{Pv9TZ7Rx_ z3l-u9_W6aFzQtbmIn*h|IncrS%)@R(E7g-W&ZIv-@(kAvJUQNiLgwqV5V+cUEFe`* zEyW?*=-a{`@XRTucx`zcXAb`IQcK2%o#(%+iP2yXnC`7dw9-hX1WDQy?YkcF8TlT9 z18-##=WGxuti>m_6!HL%dj!c(-7CDCRc3p75#rI1?NSTfZp&b!Y?n%TJsw$~u{l6Y z0_5Y%YXu9`Ej;Q0cU$%&8(qo*c0sQj0_G?4aGE+S=+G>gNX9c0BMF`+@YuAytwF96#vyDnsmJ`PtnShgaDPe!JPTl#Ii+* z7_D-F4ONer3GsVTY^gaV_B5JT)}pMO7-6wiDi&u6+0JQ=lotE;PX9v`pGU7Ce7{r9*Jn1_TR$zMIX z^T4;%l{%uQGY-Q$w}m&~W>WCfLN{EX_qnJnu?v(npC!QMi}Qf_bKpv%-a2fJEwfmP zwqwJn`2Bj9ZprIh0krijFa>ISE3y_WDn!+7?zZgyaCn^~&F4K_@%Idx;3Ule|0o6^ zSW!u!6T}SPT8=t)V``l5$Kq^?XTi+UUCl?#*Qt?>_t9EMtP$U?a1{kfLCcJ~(=k`R zGQ%c^vIKd&ZbhWL^7Y#;(>ebE+5AD7!UcMes8USY47>*C!{Z*HKQ#)D$9$dP^A#k^ z_jUZLAtnkfvgRwEkz>(TmDNQ*qJ25T)m!Orya(d1wy)NbC2Zw;;5OmJW@=BIZ+VqL zonDSON_Ng>i$Fsy97>?^S^n7Vad?%fGpXVpd&HLmf7LEIx%q!xx}>;&{|`BgIe8XQ zW=e+{e0C=6`Y=QqSl%9Hh}l9)<1i^Fr|!Y)EY#ODAt!WkpI^-CK{@4oN<6`WdaG#9 z-Gk+GU;e5UA*Md<(-Il5ZaLrFK!MFX%~&RAdcy&6`w^v6nd`Tw9qf7)`JkcM-0m6f z$Q;HSEqtH$rmu4q^hy~AGC=}v@!ABa6usA^d^VoIG92I{{_ti+sfTf966c%)?XemZ zJeL~k<)(Tx4{u{iZ}JDrCrVakkh8d3#DA)2eYptbcz7;wxqq}+4-%Dv@`C!pFL#57 zCfAlUbAhzJ#_eZ6cfPXcf%mLaisRzQ(gcIBm5$T`Q^IalPQJZsh&NHlAd`0Vm=f>P z()Awtq98<4L)43hC$^tMbWu2rhZ!vldl8K!a=CqAis8~e{#irpjk1~lG|2E7DuAF( zq2}OC3s?kFW`y)Ia0M!M1ZF`!z-<0HG8DV4lvs>Up6LSKP;1PR5TqJVyU($7J5~aD zQkf_dNb2N%79@)X!kQ;)Fx6l2bsm5=T@Ak@B`6;Vgd~-G`oe>(WYv+7GNHZk?!_5@ z9&Y-sM^{wim%*d`{_VOGcS#VRA!p%mVZC#BhcK{IC1m)3Vc7n>pk=Z9#*V4JaU7`6 z{0Dqkr{4Tr=L`1XTh=q{GlLB(HyID)*LLn?c`PyyB(JKf9y-d&3wF040!NUC=tT`S zg3iMk$lbc`)5YTq45;|9MEpxH9Qd7hPB5^oMmgo`DlPm-F2hLV zo4@UU_J>1_rqTEuvhj=#A#8Z?YaAgSg*oB`3d)Fx8Ijb2p0(ni{SrO9b#gzUBJ0LS z=-Kv9W6|TFOdBdJ9p}dI5XGMalYx@u?ibAQh{qEciNGSSR~aGa&K{s&Me^{M_t2xD z5CwP??o6gczc0+b$k<&425R^@cHU`=X60F_+mm0wx>&%IeVMH)Rg?FggBR5n`P1Am@Le6YdA!#Q>Vl!~TYaGJs zA>CBfa0l{7Yfrj7!84rBV6?hGKq8wPBM@MWU?iyg$`rW#=^>&fw?1hB9|hQrd%ZtD zVUL3>QVhOj&->d|4GvqdN)-=bLP8egMq|}VH;$p8Vf((IPV{Q9>lmtamyzEBO@dxL zg3OD-P)iDQ+)TlWCHfp~35ZCsfq2_qZ1b!EotKpR(n}~ebfJ@xU*|O62Ybg{fthT* z%%3R|fB;>Z^z{>Gv8Xu@g$#mx8y{4bMC@Gu@|+ov{Bq`@y6VyG?Tnn86}gJsTY(?p z%|Le--$+yxHzytP1&j(pj6zI-TPSaL<1_&?=p90f>Kb0ra}hOO-eHPwIn$U?jG8(F zvEHCgHr|w}TwnsGmQSD|PzW?= zEIAoiP%ke+_4g#&TLk2>sM#CK8_WH#K35x^R{pR7WX8d1nej`{!%xop*ETM{pSW!( z)_&MA^c?NeC?z;O44Ed9gjixfazVMZY}4H!+{}c)s}!X?Y69o}mvU!sV?E+c;}adoi7M2`$+C){F8p|g7mwJ`xWDs=Yqw`&>b%! z5bplUtoui?|LuahR&MjP*~tt11Iia-R2sCV`?JE5U;u)FF+!~2Dy06M)d>rE{WLX;7&+q7s2CZ+Z?s=3P>LKn6)><#~#5R zoLK*ow`1Z}{D<-0B0FW~zq}t(9j7SZVe9o{2Et} zc^wie8I0(+e>k3FL|!|OD{bBQFY(P(Sn7L@TC(3h`1hAb4uSk5dXFb;Ja2*nULM>+ zPW|@>e*2wb78%Sdl8AUSvlm9J_ZNKLm&`Zz0KCAz^V>g)pNUB@&OKpmEclNo?jj<_ zjOu!|36II`q8H|6?ay0q0!?v(556EJ6lF*L`+Wa3O7Cm5aBBU6RBz!2A#-XAI3=c+ zhR*y!L>pfzb|u}6IgNj*yM`ZzGmhuN?}Pp?Dmpr0*KgdgY~o?K0Y1h;2u6hA_9`u*7f9{>{*TNe z#exyW%*AznxH+kzv%uBQi6Nvm1;ClDpH$`D4et9Y$MY7{whpBqckBTp?etnxM|@-&a-A zl%O#j7wPe)+Htt~L6>%#a+nO}4LL$gBz*e-L3>3tVNFy-i3NRL186`75Jc6}r*D@% zvhK*uJngsdHH_F*qNd~>vGLUMyDhwSjBWt#0ijE=#nHO;b4^VPK;N(O^CfQGzU?w{ zCwO7K$Co#;`)*tzDGA9d%eE}@mgMlb26dSpC>^Jyps0f^fyC2q%pfTnF>vVO8ourO zgVg>d$u;h7VInX2P@zHKgFV!WFZ`>`i)hnjw)Co#snXU8(yJMOV~gLqbLXPMqU)O@ z-uEIs*S?AwsS}&FX1tCw$db;|FCiXn&uK6>nIG#&qGM^_YpL$H09?C(;NU@zFTEa6 z3epJl4wdTQu5Uf1zDd0g_6n=FB0%HKAxQKI}3oEnmoSt))v44NU0aT$sN-vEPQn% zmta&m{0 zHF}+14CpjC5fe?RMSJFk{D_x6a`MZkqrQYShx6e~hP*kTh#|5tl7crS^E>@9@ z?Os_#yP9V>q(_}H^}-Z#<|mjTc-TaFhwv7aDc(7;2$xmm*xEyJxK{Xs`}Z3G7LHz6 zSZLT@e8i4N6*)Wt~s~Lt@cZKLVbFs&9~zTcZ2_OwCWd|CdvCMSI{51sHfu|8|FOJ zv0D`PUNO(Xl9iQ}u1&$V3hG8LAKw>SaAAi^4P4X^7!hH`-C&&`X35tQRy7BJd&S{N zYoTdXkCP`)J~vlS(vAt_($Freq**_u5n!`RFT?avCWF_a)nUN*e@OH#2|RH$79>g! zUJhDLLm?F_9mGD$hsrm?t3Taz8z_!%>anHsi9NL1UMl1-i+<@B@WT7O9Q*|B=pMb-NX6c6yv|?jxeE`#C4GGh< zj$O2{NQ46G(@?xwznYq9UA}$K(GRVj0174@DTZ>lX+hndP zKj&K|P|%%tUan1Y^>ii8@)#EhZT9sKK6G3e##Ian$wN&E!*CwwzR;$Jcu_nEs+y@% z>5|m@bPP79D`+W?bsFs7{{8Q})Z2j6O$<7i?svqMcaRWO&&(953<2l95zu%8cflvU zpje}(1H$0y+JH6KTC<=>-&WIg41P&_Ay|U5Q*IAfh6-9#?@C|rvewad4PNosRZ3#| zYz6eZdOAPyT`^{EZh7D$hh|xPRemK`$J@Ujh_cUS|5}BPJWYU*MW3Q{_k<_tCTtDT zU<-;FubP9_4jcjk8H!rHKqVii6;q$_4JKI!UV!>cDjLj()#`BZS=8o6`J;0;RBI@M zk_^Y4Uoktr1rWLH^3c@NsFjSHW!?4u&mIHcb>83JW)Fm*UEBul@E^H;jN-;By7SL<63NJ85V-{NDKf$r}Fqy|Yq`m`x;ya7CK^eULr6t zIj+4*Weq6B!3l}3MPcOfRK;>nLqD$<7t6GKm(w2eQ2r1wZByA$!t{De?&S0C1IdeLEG&B~x(~;+Rv9FSg0{U!R9J#_=IZPYI zMHzH1|2%KO%>7vvv;&WTJ%awze>&b9@5x*x2H74tUkbgAT8Hw2&#nP3q7wH0P+3*p zP)hC)=kQxCgHE}f;@QzeUcm3`35fexMUt!4+L;r~-i@k6h7+7ii~FA8#zGc^6h6`G zUh9%G&*NP$viN$m<>=%oxc}A6ZMvqxxRbZYx=U(Uki|^Bwlv_9J5(#_Tj3z3I@9v<+Ki!uoLz#Hq+8g3}VHJTIo6!<)N%& zH5u)N;(o!EmR*Gf!w;21fPjp6wjE#2|34~H? z3p(eP9;~sg@s(2V{b#(|8D~R3Ol!`4Kh_US8A_p_okFy*~bJMOtJ-866Losiyz!Ho8%&pZ?ckH8rr2A(ns$2eY;#5vImcNqk zmVfWe2&8lno~{f9i$?-x<9MD<(ywqiO!S#<%PZMZ!5jMyQe7aP-_hUAmf?jduRXf*X z8I5g|4@?wq+VKMjT=S~=Ukv*hC##L7I+pEYvEq2Ogv;e4ZP_Vba>Lon#j)LVLlt^* z-?04dD2OSl8*YKtWg+4}H)__FZsK40WZ9U1JVtmtPPh%&hke@BYZjeaxO%J@l(ISZ zJ?pWr&rgsSKv^2))~y0K-B1d0kv3c>kr$Mg==#$0W{u4PK`@7SYjWs%R=Q!?_Sx|+ zsCY}(6-V9Wk`DwzVZ4xKNPR;?lIz??JdnIB?x>!q`5Z=bh?!ZY-=5?YV|ZWX0JKzz zg_gJ%?>v6|II;HDvU7ToQN=EIa8aAT@fUDL{m4WocYPNm=(~Cq#tDZpBH>;&L=SUtoBgAnpphZ>a`#6iW2UV|095u6i0PUSR{xak*nU%(hwy9AVvg$Fa95J8MBD2Dcoi%|?jjI1(n2UD18D@o=Ob>dt%v#6 zyVlnNv z+H#Ezu0<1JDr-H?B=xyia(P?@5R88z-1$ zbNnXbYwZ^Ovzz(_Nve}Tslb&{7e}w@nH;Co&2z|9->C^sD3obQwmcb+y$>>?IG+z) z{nY7DKwj*1#com^_8KUppEym{f;>V#TcroA)yJF{ywZGF zG+cu85U3p+FbxTevF=UrYUz6G`g0w^4IdSx7C+d3$|yQ^gRFU+T}(_{+;J>pEx3h5 zBpV#lc;Hu8r`(c~UYR%%tRWv%b@Q)yjTF5f5;)L4kWzsDj_mJ|n?*<;P55G7dyJlXK24x@ zhKC1-?NkXMXD?=yBSdQ(IfBqV7N8M2$ik?=G0>gbKk~;$^YIhUsIKFF&IolYKRO z+>MvAA?7?i)LR`#ZSLw+lE-1bbX@ADpaWc>{&9<>yfr22kd+I)q3G!7`f{^=_&E!D zJLh&SW)w~5JGXYqRimC=M-wRDS{vV6gf-H-BT*n#T#)!R5>zT=5C*jSJNNM`j6-X@ zkJ`8Nzkhn9zPb1&9aM6Y+UC|8>zU~5T{>b?y|3l!JXQ=8_HQAzLJG5YZr*%bF6vm> zFg>0>ec`!KkZo*ae-nwZJ@K@vdtKEYj3e1Hb_I;KPKvM-)M!%{I7FGcm%0nP^K-EU zRV7;0#>(&YKUHN;J(jSGmYa6>?pKhQd<6vJQwlkSs8kiNk~(I(W**=*(rcuVWRBUo&=gnWoLtdeb@6GLt6|>U>R7 zQc^b-XOe!ofbVFDZ$^f|a8_DcnvbvVw8$hb#3AsER__!P7hhBhF23!oRx&8m@C=T; zCYNlGqL;J%4Xq@W0|$ESo@ot8tt}WD85wCUBQ0Y;x`#I!dD!4Q6JZ{dzbAnI9r7|O zhI!%0FrW7vgI8hOKGr)gx=a}qxG#+63b=BK52)}!p{(7yuOK#itGNQrwOR)9ZQXfk0hO;HxoD={SerIAiO6jh2?3-^~Oe3NT0_;6M{Z?EQ4bsCm}x>NonA@wK|1-H;(s?-wMr|=)?wt$ym4;Whhi(AeF(f^8J z*dQwkX}$b`)-mzO`{mwW#_`McfMEk&R3=7tV^o~b8As;SXJuZ(A#o=J2gS=}5BKAT zvG53XwKRi8uFo%z{m&uF90CeOz|Q+U8>3K-fnSVo9OxuGs4s}uQBZcL`Cqbywj?qv z+YA5mq5t?bFLbzsw>oTieW`iyD$k#46weF!qh^j8Lb45)bUclhZB!!&PFDUSDd9ma zz!e5q(H?U#{7k#Iz!%&u(GG4REGfE!GFX&>&TBfnX7i?`hWsl_qAFoYc`2X;yLag& z4SpDF2SC!e(;*)J<#f29(;?hTxv_llK$VsWtg|&FY;`Tr^39z%xoJaG3(ysyczoB$ ze~J5LF2Qfyl3cirm$_FGtmYRNC)59e%BXR|#AO$AL^os;Ljw~RG9U&I(}%(NT_6JHEfK1g^q3XwE5PCxe8*tbB^$BK3v zi{1OjPWt(>Sc_so9%B|3)-W?O6T8}T^G$ks4W+Ly-l17J!F>3z95ppH*awxfXYVG7 z$z#Rp?~fT0c6U+(y>KMh+*n5fgfVTD5g+jbak?WTCwCuAm_>soF_yK5lHvN#A^hVn zuTc`fUs56LX`xx~KUXbgKg1y+(eAM6*bV&8jvRa#9Q+#WqZU_Lka{p3k+QO~MV$;h zb~hP=_dg!?^Q9Lz3Jbg4rldJI&Vq_H>FIhZeGG3*zGxJa5P^%D0s!7uBUX$Un_DfY zJxX|c#cLPCRm}+5$}oG<*A}nF#kQWX^z7p*+HSmbM_9bT>C{ZJu77#&`0?ZM8SHSE zUO;3h;jN*6g(z|xCK9>k@9(bx#y*MM`y+m{ez~X*5m56mHj+vSYw5bKZpuh__0wYW0zsI8wH+f-myUDfja@DtmS3*q8A$XbpZKf9<$ym)%xKDoLXPEC4 zH*b6!7~#5VD?KEsyJ2_wo>l2^l^&4!EmXNN!4w|I)TtCk+eT{~s8MQ~(EDR(ZG ze579BKUP(-6y_4|guRu-t2sxc?ftGs2mYewyi&Hj6o0xFDZEHg!0#OU?%liD7U?yu zQQm^DEqU_mKi5wdD40<<6=^rfkJd1|HOaM^{eO!HjT}ZbbnD5?=sRn{fr^^On2ORN z$G$-Grcv!Y0e^>|230i;X2`i8v+_sM>t=*;<{O5Y;@yb9<=H(7PPS|@l zb;&nYYNcmf{w$pP#?dpTal7iBijN#wR<^itvDKfRz%TsF;v%qS3-b#Nu0OXQ{1p{X zN;x$xFtEBjJ9nlb@JV=#(tC2Qzq()Y>|5EP=6nD3Q2$lWpWg^Y^x4lcwOjFTxxyhx zonNRG`}qO*tGI=9Zd@})E7Grly&*2%K6HET_x2^q?b}*dTO|92b>sd+fH71cK+3~D z2ON6f0!wV87XT97LJATlqpQo$O^)9Fa;&>7rL+0Ff{E#92$Nt`mkjGEdJduIuf=8x z-*2yavF+#D2q7or-lj>Yh&O}Mk7h>b*_5oEX`VACb?y6ExbOTNK=N?wPW-TlIR0C3 zzcHroe>IBk=QH3hFKNo}1ARljj_a|)>=%^u^`lPjC_3#;%HEV(6=Qx)PulaZ=QTgS zQ3?X$Ad5Y)U3zVyHw24#z_zm{Q~d7DcT1 zdM7;S)n9N(0!gYYe;y+IUl*t~#g^6F#atsiv@&%xbn9>r%t<+!nH42(OTQ?N>Q*VY z{iy_tJz>kJ_I#niUY)eCLJ?O$Ivk#BWHG-kIOe^R;=6HV?a2Ii%UgeA4ryteu&($? z>XMxJz((bPIR@QmF^-!*50^B|nDc5P1*UpHC>tr=axL&_L~khn!>Y;LBgslxJiVpg zJz62wGn%0;E%!OjaX43q{(<80@6of$z z!{EwanBw7W>_`TTqSsD7dh`egIES8`zHq^>(lgS~(2%i$g)HDT&%7@M_V!BZuP2yU zQH$8uL@MrU*IHDh`eF6B3mr{qT8Z|ko^eOg9xoKX^vQV!>{(-teyf4n1VKQl0zuaj$x0Nrj1( z=Gx6nlu&LLdHpc;to`1Fs9yf)3^nDSn!C6RMN9@>3K0^BUNsC?nl-B#UpI%Ii_H!3 z##83tT!o0*#@h36aD38cDRvVRCdMvMNWqVKtB`KrClUqhFHQEIo}NYkbyT*5_kZfI z3QcO+9CV;xTjh+i6$w_Z@+9}C6!3q^9!RxmZP1>pP_K%VzW8}|Z-VE1R+lN$Z}!U_sB4w1Qm2gV>|5oCW$OWOt57nEDJaH+mW#0uyVq*(?zsJJXk&sEQW z&`l&5rP3rR972&(#Vd|N+}u@S5n*A8K+sVscM^LT5)zM+DRGs8{jF1-F#Bwd*?+_& zVHcQ}KPXj&E_g{@T=1P6X^q`4>5`zP{MP~@y4C-|!;(gPm+DPLz`=RI zDBc|3l+W1>)xmZh-k6u~bw;$RFT9*di=mP%5U4%j)&Su^0&^@aaXLyX&8Iq}WYDSJ9v%EvJI&x0bjh-8a)Ar&Oe=%MT*peUDdU)EJZMo`N)8OD> zd1Yk=TU2|(L99EuL<-KP;x(e_t|mYOK9M8 zqUBVHz`;7kvDyE&j@_3pJnheSj8wM}OYE!I6>pR` z#wGaM{+t$GlLZ(`UhO$42_OP1BphV!1>qH_6YY^p324U;7#JEh!DV*G`zqh88!vjM z2iwNVor2xYsr)SWA1AV%)WLe`7}tP*OJmAYuF+z>*_<~;3?E538z&n*dws1`l?@E% zdq#fR6Xx!n04+s}OjiTmDF|4O#>;&2+zL#F*FfcHUW<@)4G0MV>XKo<&M9jcoOaOCKL!^@%++7;TKA&HmG(Og zi6?}0h2^M^eU&w>KSjaKqn`Bkq=}N0Ck-5a$3!Ykr&$@BzxS6X^1XbNiiVT(G63xu z_JJzZVbqJ}kVZ|UG!C-hk@jIgdXU(MbdxNrz8+Dk$Eu6b<;$8a!QE;e9)&{IofhsX zdIh;)CDh}^YO;<%$IHOpe;T*?&EE$i8^b2&2W3u2#d?UT2}T>*pC}qrr<3R1{X9tQ zf!YffIkWhg#Wy*k!D9K=2|Z6PF1q0cnPCs%y+9i_Fvcc3uZeeMTjNGM@>;~Epv6v3 z#7TRuXrPR0Lf`F-?bMjfvr7ov6cP7sZ`VNzEU=jl*` z{LR2AGdR^hO*Q77P9u^!P0t;^8s8k({{uQ;s1ESZ53jKCQNG?4!`5M~ZXw5bOT$+p zuiSkiK5S&`YQFw;F&&q5;v3i9Q}5VOz%S#U$AKaMWv326AeNwYYa&;tQ|~q)>OQY+ zf+YNdF=*w`!bgnpP&&Zpy^m4^(P5RcnDJDXg}1l2d0q4^I4TvY)@D3xm?8sr`L6-d z=CGvVp>a0zvfNuC*%s+p7J6~VHjy1-;Z(>R%2sl#(2EbPov=ul%Nf3wu3%bMN#C@x zQW)P5l~gn9zy0sS1$~*ZC|Yw+TMfY2veLa2Au-yL5}E5T8lRYWm|N@3N$6(Ip}H4} z&G)$-L;l5I2=f6ZOGo@cR@PH|e1UoGEJR*Mw=vQG$F4_f6`-g;w(O~Lq510S`1_hf zE~}zTcdt`kYf5#;m9k}Ls$(3N)TDF|OOL15!D;tX zsPexq0+niz!T?|kn_Q57I6L?tG{^_>PTzS}K|w=C#_PaAF3;B|V0Q>!*7@ln;dhN9 zrT6>gEYrICp2H_S-~nrz=Vlqqidp|3#=bHv$}Z}9Xawm}QZVQ+00jv}KvBB8q@|@} zKm=3-L`u4)8IbM<0qM@6yK9D;`@Khf)c5`KeSf^Jx#h=+z1LoQ?S0N!(cmvxwa+!O z5>$BNdZaY8nkz|I;<>FYR03WYMpC73_yxHxKFIrfJMV*6=tDA|+}%m`1}|s5=d!J{ zw76JN5E2qH0$$W+mXF_;tIoTD1fk?5>OncWy;1q=i=sMjP<{Up8tRgWzOJa$td}vy z9gM3hLOsN2qxJ!hfCJKeGjVXlGq(yIcT96T44H>1nW7i1yEAJexeC44GAC*r zLYx_hhG>dSvpf)8LuKCSO)OR%OK-wP_5lnG-^eMBvuT@!548 z*SP=W<2edoB)PiV{niVtJ%hbO%I0_KFRMDR{~U`fTBUn!U{3wdSX8eW+&T(n5KB*6 z3w%A>$7r(hl7mxtu&BPeB|n>tDtC3g=tHQilDRqmO;!O#hsO)#`tpz9l*r`;=}7OF zNm14h)0xz{2KuZ5vYek3%q#aOJ50`e$UZs4lB{F5Th?QkD%KP8+>0ew7RvGp4Ku6r zA^#7*eUi``d@)95_0KP6AUB`goT?!B&%Zz?v!&q+KaVSejqtRm1E`W|p=G3ASllJd zFACzhw^oXc#3dh37H&^%UiLbf(a($W+C6cR5%X3_3)D}W_3_Mz7ZZObE5ymgH5I@O ztGQ(&ghC*uS$jF|-4Kl`S}{QRQI2|ADEMWygR%N5CP zkhlxq{Bj7d@KW`cMT5TaSs4J2Zl`lX)*B0V4FBWq=V&n=8Y))%Qx*GP+4FCt<)z?ze$IPp zA7UH>sJ6V*Cvt3k@LU@JLj3O6hM3Y~@fV;IXrPKgYdroGjY*$k#)L#X46g$xb4inZJO=o$lw6 zQ{SpLuKdbQ!OiojN2|pMeaOYH0fGeyAM)FZ`2Uy|jO@Mxmu!%_cecI&%1$P(n*udw z*x%XnFuSkMz$D?}*Fy?vId=77JPoSk9O>lqZ85J-ZJEhR8K+c-qoo1ai87Pgw#!Vb z_=%KR_AAVmL-TL;t7D>vUvpVUQ+uDr&(oC*x<${dTGqy#!;74Km3k_hoIau%=I<41 z*D}Eg$$!YKq^z93o|@LJWUg8c#uP_I<@i__6OJ?TJ_3^0#buo)L%?(?R zAAfii;bvpqu>xB~_G85s+xaKDnFL^BV!pfuKvC}TLWt_n4&VblWc^o-2Z8|uGlJPt z3F+pabc+M5$BCO*I{p_F(3{n%Kih?)zuG<=M5)(Ylm~tKW#2K^V!BG!vcKj%rbMxI zWeZf33cb$jp(@2Pb2LYGnJ#E;-Ga6l&VgHF(wd6`iu~8?#K#nr45X)iP%&6%C zWsNUDFg#!>$>J<_hu>?)WnJ*lS=W2msh|UJ7O_z}8goCUSJ3)XzWx7@<0Nr@d9Dhi z)A>5lLHjF6P`P=VFC?ExWi{+UyJPR|$GwMUY8F0@ARQGK;i=Ku1K<1Nam)0d7DX^k z<+r0w@%Z!L;@sgm7U!&vk@4}VoK*tlu@i@>bVJvc9#->LKb{Pa@qP-IJ&RV$n@qVW z<_VKdbc4?+!(-3oA7qw))^>*Vohkn=M}hk07HGpYUf335XtNg207F3*rK!z7g%|P4 zOD~f6C6jti$8uoKp4O$W!czUDog=g_X4Di7)V`?kYUi}HoUXgA4X=+MSlVv9=`9_7 zcv0`%edl69el%A<2_uJ$G5r#gydvy5OzEIS0SDJV>m!I~G8y~78{z=a2!SFeo`I7}(1a?TW@ebowiQ$W7q7(~O(-x%+(m>ZkjD5KYaP^{L9tg4%88*7jl!amCC| zq&FY8s(x=w&U;(2Trvr~joE&#?pP7!=D^FU-G%hSdEu!oj;E)p5QCE`!BUeAH7~vs ztl@(2^gt%4E*-TjWjGMI7qrOudl0jn;F5Zp+*rf=xupIC;F2onar{$BN!G!x)7ka^ z5e{JZ4pgp`99L(cp8Ks{#!S!HQjZf@*_g0u&yt-C$;f{5&4hBS$}4}y>w-OB2bFKb z=#UYWC!EY*pVyaVctJ&}kzB>itM+>Tfj0$Y47tFS51724@ph?s+?=!4Vb*4`(z*8G zLw4?gJ6uu9n&;0L&cW*-e1MPqB0rUVHn6_ob7$86Ctm_dE*f*GxMop9zzIW-s)2dg zfINNnZ1t(s(e@)BB(f@m3Zre;-ACrigmF6wC%;o1BdxHCQ#kGFH@WV6hRdEehpnV_wTT}__+G#iFSHB^LO&=5cI_zGnO8W)i~Ze0 zGq7{f&h)y>FYLdD2|36A6F)#9Khx(R8(3U?(#rY-45^GB0z=FBcOLo3T8)?^dh$cm zF-ftrR_7)Xs#G`Bct<&Vr!otsc@)Fb@7D^tec;&{mX@V>oHtp%dISv_=6d5L3b8xi z)w|$Z;jTSFJuxve%aPr1ULVhnWXWm2bE4lb_T+ zzgFz$_)RFjcX#(g;a-|_WqHGHTa&A^MGPjnpK`f<`_ApGwaYc*1PhAZKwRJ#S75ko zD{!gp4*IRJO2uL^FL`}$foEQ4AVXn2xMy|zBKV4sN`K0Ok#C3&20U#SIhhyiE7W6| zRq=VRxlhmeg8_rjA@m}76=U~<>4&6Gh|>shFA`XRt-C5E*uO)54cIT>LqkL@`*WYO z(5DDh&3Y650U|aIyUj7h_Q3&L_UmVbsm2l-+x2V;4?JB+FiR{P>$VxszFa5$q(aQE z+?vvS=7ju?A#wmnSphS;w4$z`ytV3OH%9XZtR-M72I4;J_qOvi+Qo`bEFss##N>m- zpt=;IvFSs#)l^{blZVuFPR*wAdfyqKB2vaEM*SyJ`WA!Ix*fH-&QII{&t8EgaN8ei zxBo1xj|h(Q_EZ6^9#F!0wD#~w|B}SZHxMze`X?3^XTvZ6pNP)6IyKc&r76J+K|SeW z(}&Ui7C}KW_0nm7+3?~JRl+-*r%9*l*#iroVP7Fp(0JvwQ3Jz;sCggww#mC{PnOw~ zow}WMmvgYKBx~uv_Olc;&%M^ZK{EE&@?_Ax;D@Fe@%= z`5~hG$k+ch>T*b;3+w398{l~^hMikMU@_bw!@z;!r#-b+=QCHKS7N(P!mT`)2I` zla5lqktgDsK@`yY`43gOJ@PwLHDdi!)`k&NEVk3}B!f-F$2%#&u7pbl#H{ zl4#E*yMQ|QRCnrLHe4^X_nuaC*1;s{e$MF@k9u&r+KBy1(*5=ikjBQ89@x(&{a@%8 zA^uO#fMf#)9UKok}EH;13nm9|x$1Itcg8>K7T@vr>f28?I9nAyk_EI^wV^xU2nn|c{hg%ptG zYZuVvZGOBroHea*#Bjk=HT)qrH@RUpHSpwb-eyOO?pydGDy)uyPe7mq*0$WJ8Z5l= zYnd>RM(|edG}uC(nJxoRA5VMEso(ZMGpt> z?fPy(^F)Pn>FE(uN}X3A1@bdL*qSusKC`8z{^@g++9hESAMOBOsXs1@uaYcoV-unVJA1|{ z*kjfK6#8S1)(DI^kP)!6nSi$rAO+a+@LUa#X7pRf9lq@gs>J`v9x_ z^7=URV}IQc!@Bb$d4Fs?tHw7EX!$)0A7jepxWD?mo*?#pSPN{YTuDNOOHI9oS&r@{ zcI+W$+@+E{kyXPm@bF)7?FA6w_$vCS);`mUk`b}-0k0rL*cn_bRo7BBgv380FleeP zSdg}cN^2-YOZQi2pJUu;yrvarXqc_~xcndLkTk zj__fir`*;^;ZnyDTAR!BBKBVnw3@8y7VqKPw>f?gpSIe-19nL84v=c|M{#u0Cj*uxL;U?kA zYEtObw&k*JnytEbo8E?pQFzY>tBGN!o=OT*(&UMdOaKcLGa(T9hrE-rrJ%c$K1$ly z6dc`H0Z%gOaAHREy)*jhX5P095|N?r-hI-1aO+0;F8Nzk*+&pCRrWdiqhsK!MEJy< z1MYjT^CysAlG9DB>qLa0S>b@!M<*Gdp(^k&Z0eSJ*9VrACd(^RXCgw+*LgBDHeP~rz8+GDez8Q_ z%u}Mc%FZ!2v$K(-(+yp$i133Ag8VcTpAxfMd)_VS)*D&A#-)C#&w;pb#V~%VtBTD% zx@p$GF~i5l8VB8xJxP{q_#1*AgRF+VknL}8-Yh@n_=A|!#jf%a4>&cp+y!T*wLGV%w^zNfgAFGj-Y$u= z%!@Sv&3bPft=b2ZZwNS)j$=c>>2loJM=WjYvU(YqZ5)^0Sva1Z_KQr2%nqtT49rOc zme^ca{;o7pMh`vS%jb1D65zTwb(<{F+)MB|Ahw5?Jly0|jvQDV9mT02n0UQ{^1!va zh^127DA@KWT^~Ld8B>4_x8{r|*S1Hs4}DZO{|@71aph`w^amP!2f^gxJ~s*LFG5!c zUQ`S52*%4tYudkf3tJr86jy`PP1GO;oC)Fv23c~Zcn=;!X3WgqK%Up@CgT#tCX!>n z3x%bDJ#-Po&C3~gi|>@MI>jluyy~xKs_(MgO2)wAH<@6y9IocH=C>47 zxj4Y9sH@(2A}IxS_Jlf%wV*3$zq7gjXLsxpd&44hoF9uaJwA`rU!{w~!z5BiA~|iV zQhL9{`>6NBhnzK678D1+t#{qwCjZ)oMo?lJ6)F8P?R%}F_h8v}^m#8z66`6kltq<`lm~6p=(^~6ZQI5QZ(PrH zvo9ro-Bs!rVZS&^CHjUz{#dZqJpD}*i=2hGo!z^6BX8ZkYj-a`HKoaUi8Qd&*#FrG z@mcVeJV@pzyWd~Vz@01H8T+S!(DyST--*6M?%k*#Bn~w+q6&s&m3l`)5Cr&;DG}+a zE5c)kGobPf1U#%bVukVMjd#K|m-@E2W3j~jua={7Lo^}1M^@e{;a~F^a_MiF&$%4D zUI1Cw)rhq9qGvfSsN~1fGBPjvzaEKgz>EX4o*g)!t^|b>CX_EkKijEG4xNE^SZZ~D z(W*M@@rW!t!c$kKbE%S=v#&33ofTRgyB!Y9g+M3OyNBN|SO{gdcM~|*VEUquE&P)9 zk`F;bEMwdDyE{fD2Fxir(pK93+g%9e(hYF;9z3d2rMN8HI61L` zy+!<~Mu$+rVOez?Tjx6OZ${}ne-McI+U&W>wu zr;cH&Lc>FlJL-XL;wenGKL5<(`0s#w!0JJRW|RHZU+AI=Uci{lF;w~Ye7m?N&X;rE zT!Xa&619&_^f39}?+pOW*ld1zvkz^R}za>X5L^DsFX{(NREP{ClAX*8K!z5*0ei5OU+t% zy~qsil8~E~a6Rxi+w@g}lKPJ5i7ijvRu7PADjRD)+FLI=Y#Z`B=`kIh03}Uf==qDa zKtz`NE8WuCMN@z23-1}tkl%+UK(7N!x?tAqY@_o_EESJLi+AMCHh&|x0BCHffsXktvbqSMx%u^g>lq#W#SYYjpm)~aUM;Q!9 zqsBEc(R)3V_72k2>wKHg->%mhu1QlOR}#WsY49ZF(*Bz%&tQh(b|J)JQ220(TlM2n zwU+3U;#=#VISY7IhaE)$aajFs@dCj$dVnOZKa*_RBO%<*F>+lVor%$u=JoiBoX6io?Kd2dcexN&A`C;O=JRl#3d}C8{kWCIZ z8u@t1ePOA-x)V`xPJ6^~U*~GJRjF;)oekGl)6_X0Jybs@Cb)H9+$rw0LxS;t1tbta z&e4ix(oOzfxr4$Sh$kHJU+DfO0TJOKfGlBawpkMFV^Mw5*Ci0*X_vcdRouHYe&%@C zoNAkg8*{B8qqd+wBbr#n50|~nVsUqaSHWod^uzmiecvPxbhf}F$U|8hxc9-NRejo& zhOKJOt-x_o5H<=1^!8KK&;e79W*1zaXqH2zM;L6}I(Sl5P<@m_KK#&leUcV&x=`?q zp;|A;Q1GLoo>Z)J-g%P{BUN}Z?h1*LxZOL4yTldg4>f(n(~52D}U0$pKv;bT43>kf@&dkGYgnoc%ufJ&*eKO|D{M zFvkh+$Xp+OI&(fBM|ou#-~4`=j}K&8c-?Yo`DJdGfAmH#N$o2J*mp70d7!o3Mm0L< zEP9m+LeX~dTv^n0csM=ET4+5AILnQQPD>E$>3i0mEBZzvC)>W~Rc51`Nhdv(y=VJy%`$ZSD!=Ea$C&#=$-1_ZH}1J4RIfydc7k`Q4g1;lf7b@) zh4jLNc^|q5h0ssH`VqBRE$*%LqJ}GFPVTca8xdis4co5;#{T}oJ03cQ!%dN4RcdEt zT*a|5W9h|j9_Lmjp2~Nf1Dn%$u9U+Qc)Ng&K->%``;E7s#C`g52P7pG#Bo%RTs{mK zk{BM3FsO<+3FfIJvD4qPDN#m}TbieYLoVH{EteHI>3K25{G5D1Abk_MO#!~)8h>}? zyv+kfvLdphVKCllQb;v^wF0RhZTs0v*$0y{8G^$m?wgBw8B$@{c$Yu{{Yl)9#sDJj z2bHA12L30;&yRfCzQF+MZPuX24`)=c_58XKD|o-l{pNh4?b+ncp>K}KW-Ba zWMZU?`xrL2&45FH`!>jRv!8i&CQWRUyA)|P*xM<_$o=NAz4VR%%XB?$3KmmfXOmJL zVziB4qxjtFwkWG-<)Zx9#001H`ud`S&b_)=K}oLp$@$jGE@m&{*{PW8<`gqst6}fe zdln*T+L&*nQgya@?ohn&_AW!3wPT#DMcePKQfJiF28Djdw!0MN%&1YmCLb@x6x@(3 z@C7vyy?b~Zz40vR5?kL=W1C+3quEElW{6%10|yLT$d~vC2Blw1vo7a@0dX?xN@V}N z4B#JG@-(ypv6I7Q&-2?b?Csfi4;=jp_|ilJl~{Y#nlW)K-07$23@P#L;9l}09S4^a zY^JM-lf3sizV^lkGST$3wvL%Mb5A*M@L_vc&TiA-h8Ais?$4(hy@;_3^hB)=!sd>J z+c}Ol_Fry=GCexQ_K7&H2euFkh+#r|phf*7V@#|aqobK@<63y&k^`%m`a{m;rP;-| zspqfaC(9?Y>RyKtNpqd-c~p8HR&-nuvH9T1!+owV3YMTB&0&(%3piW_es25kovnpn zL0;H)GX9>U)2#-Gk5HOP`!C!0`A7Vev-41*Ia-Cgx&>4!eGUDZ{!{09xcBZ1UX~=l zX6wWbVfgUG!2$DOEAjE!Hk2GZ9Sb|g`u6Qk1Q%w1KPxHaP}K`2M^pL6?P;jtb~Svi z)l7= z<4;M^*LhLW=NE+Aal3ag{U$kkdt_=qhR(!=V4b;qH&LPmvhTtJIG~Ub(J+${lX40A#~m;ThJ3*_ zS%*aG{$~d=Xgj#s<^7Kx7qOdfZ;(Wq2oSVj z+A2RBa#OosWosLZg%QPlhKYa!l%RXq*13@=f349Cu-L>BY?S6&h6}i2izW%`R_<)~ zUpBVczz80}ZM?YkJqe?aW_@ont$9%hR(C)o`t#*>>zSMEvNCxgXRa=KZ3 z*}LfjzL2d6C;vlDALp%V)rrcIiu%KLhWqX>Vc_N7OibP-O-aA118ko(ujB!|dsdv|0Gj&i4f=nz%XSZH&-G=biRf=bGq8A4y3sulNx22nz zUe&Yay}Slwc<1F=-##j}H88f~b>ZaRwAHwvisI$HS}kS#{u%F&0O-NB6K}0U#&jJ~ z_9SgBVFP%PpW0T6{_96s9w2rM z)!t6}4>4oVekIir@*jkvv4ic?Cr>h^2`CS0l(dlSo^m!<*yjkm1i8@JsJibzEUJ7V zr}t)OXYdynUv?y6UlE(bx!DSo!eq1#p-YjMG^)4x(qBNjpjy|Gq6-jRRXatlduLw+ zq3m_2RjPREN05Mpl(Y7R85SmdI_!&GsCuY#lphA*&x8$H-b6&aBm!smxUR{ zGUL8uN&0B*tn`6kIuL#8>N@c1=23kKvi@k)A34{~lsroFIA3ic+YBxhG^=o8A7dXF z=34hsj#gOyywHJ4X;8@yzG}Z~H`LIY>z|eRc%XHmu*^{k*=-!NuOGF*^v7^!+b;1z zX4^Cmp8fjdt3}8MNgjE-`Cs@!+YL3^Za%>4eIhV?qz7pf8fo{`2j(T477zKQ$s{sxQ$nO60(AmyNjhfpV z&mRgKzN>l1bF{EIA5tXoeBkV?v>Jz4)KKHFL;F5`CE@kTb7v-9pTZN^qeTQxh~Ac2@Fvkw1Kju2 zDn8{k^U{s5TuZ(wb-9)S=K;F|LvRK^0pb4a_GBzOU_yra8)a;2K*O-9d64w<7XW7! zfxj(Zv4p+BzEIa$7mX#pE_mXaO8Qc1Cqv8`6@Y>O9KU^;yGiu6MCk0UN9L++$ zPsa;8Qg-`I%%a}II0f={m7?J*-lEN*II$|!>$U~*Mud`JQ~$&Oz4QK zZBvP5jVY4C`$LeHRvATSzx(D7R7}}{?lNWPAo}UoCk02e`%Hy!{{yj>Ja&hNjOWY) zr~^KI-+7~ki2Z9Y#HgX5{z`yTUr)X=>s2irVki8#z#B4VR=#)B*~gsEld>Or4b zLxPOkkMRXp>r=md;&a~mNdkFZ{UtqB`F)3CF!o@v(#WUXVyflGuwcLBps7*avpS3r zKn5NLlpvu7ljK}yZbUsv!Z!y{+Or*IX3VSgB|tzcfEmn5&3!`M)5CMc1&+nm$vJ*x z>-w&?xV&ZqKGTX+#tk?-m;Gf5=&)>OjC}=SXB>Y*`HPMDJOh6lkV`o~{`)ryM84M5 zqlVBS_^^3nj;yYDnzQ((zw0Ui0qFn-erwJv`^3@3uh#h3Vl5rorak<27CKw?`kQ7hz;io z%Q&Uv^t8g`N`-m}pg_^1nCOxNvD*4zB{&%uIN`e54xluZgMkoy!`t|Buzm( zTp%b5x!}qrJ^V;QlP7)9K$_vX_hx*o(D~BlCjGT7R5?Wwb5>7SVysWg^V?&{1J*Bx zr%GvG1Ev86VT4bb07*|Gcc0rVHa)zS)(WZ{SX&MF`}^a{zwqiZUx9l5F#HEJ?MjE~ zj=FRKDf*hfM2zS$$T5iK`Rz!5Pd^7eeM`AK$$zFVjA5|{J+allb~|HHd$x2VI6tn+ zCn19Zv5N%Ps<&LI6G-)@Yc_*;=wvlG`KYY9<3kWV+AcS7kr?%onP5PCfMcHyKw zpra9@R4Xhc%H_=%Ar}@K;WMi5iccaUfaNkRqQ?`CdM8vPJ#)dEAhwSTx+1X8yjIdA zp|#1IFfV2aBo%%uYi?6bY8n(KO=I%Vw$OG*K$B*sQ#d-MGcgwIm!=+_U+f6HnmEkW zL9AI6N45g-5zp`2yV6AE>)e8StQTcMbaMtWUxSA5zgIvCy#mXscGrHdKqt->h(aZq z)z7R1Z6Y8ulbbap`L#jjyAt6wa7Pqn>aw-z((SIlL;??D*$CFLd-55e%?oL$} zg-*N-)o+V4L)jNN86c!&MO2O}4}(VGNK6jOh3+*VbI1c#+Dm$c9yd}Zc$`=4Lrwgv zA18-qt`%o&PKmDOq~r^{sn)J8HAge#88tN&;L=Yc9Cv3INtPzvDm9U)K?{81jSwth zZ^@Oo0KT!InP6RnDzFjKp{p@a- zgq^che}eh@rT&_B1TRRv_H{L6{@Q{8tuKXH zh{5v~d7X1XqD)PMk@b?#R31xwNQhLK#xAkNRZMjnk45UQkMouIW#X)`AnDJ}3ttOw zO6H(!scVCdHlpVjl#`u zTpQIyHJfQLd#6jtT$b%X$zE5U z@SIE)p4F>v*i{IPH_Aj_k7mtSF>a0J2YF@0F}x7Ti-i%aWQm#iNmCwjdzNI!6i!GE$_fCp~ZV?N;&Jf>8A6n=FKd1qn)GE8<}vz{EUo_ zMCDP2NbqVd?{mTmDN z-!A|>q(k16-B-T)p&F`XlHMa{Zpl&o+4Kh6$niIjNBwS(gEGioad%gzH)FSRT z36V5_Aq8A3c&St6K*4D8?NzTXul=L4U@@Z=eXIUbWjuS`%S8L1G6#*9U~!5X8t=>! zldwp5ZC|Em7%YYA&pjFPKHMIrnHDNjRYFy};x+Rp-<_((G&HlS$>I$@E7$jKU@SuN zn%r|m7R5}Rz-lff{y`VtQwnqd8b9dxjX#!XqB`;N)!*cw&5pr$fi-mQ)h1bdP2rhP zscR6X6Kir&;Uhy|-1qO1LNrWqxE0o~z|ciT&lgK#^XIdNTeqf1Sov~cBt!uvaWj=T3LlE8Yi_C zl95Dxoqg~JhLXiaz!Wc;aqqv_M-_d*BiRXPMbU{LlVMSYa~v^t*vcKkh~eE`X>uCV`UjPlpEQ?)}dL* zbBw_YPM8ntAD>BngwOXm)2$T(xo!9C$s-j=@}nAD;@|XEfgf}^mG1YY|D=8#)S!gY zDo`_Mhm%(#0Cs0m!}DqSA%e)lA?>sV7FZ_+WMH=^@%~37r~T?QKwNWjE&)niL}^U% zH$tac$Fan`5bOeZx^K2?aShuxlA>e!Px*o7gzHQK9HAl}*qqe0w=w}+#vx)lC~9aG z)v~7F*wIQ6y^y*$T*GcYEJWWOuPWu_Ie94Qsf3Hh-8X)3`nVrb;+$wYFj$P%tJ}GZ zRh55&5eWks25*7)3_yTCfbt9Isz!;_dIp~^zd6;L)e9VzPe59#jP1&Mgy z?#w0W--I+v0Sr1+1G~8YEX++zopTuWyK;5~Ur@J$cXraAoVb}_LYBMrKddCtahAsm z0@7n+6KZ|WpA|)*fadYu>+8FWAjSCXKr^TxCo1PjK5KK*-ZvhTHLZbY);akmX-?A; zKV@5S6G+@`idL}T3QGvbsI4P)%eH?fQl*-GoqPW*5FS#3G*541i0bOnMn>Dj3wzOP z3l>NXh1ELLR_P(d;y%62Zmo2AlKK(jwmG|ZI(+LQwy~?f=rvX1 zKN?sBIk?5_mlJMkjRONk-z=d@IDuv!G3 zyVS4oF|F$c-xjSxtcDgDNuNh_q+Lr8JN!N*V@68ng}c)m`!VG@V+)d1k3(7i^i_j$ z=;fBU9q&-*IgyAnvPti@5V5HFHi-l`u(XZa zFojCh@Gr>mX@k!CWh-~3ajUx9j*rV*cc+Logpu7cm^qPFQyW zWTwvdtMg5ifaliKysmh3BVK_wIT7rLg9AtV3RPQA_{9H041_zZt(b-flaK zEc$+@I8M}&(bEfa5dv9zc#W^(MZy3M8Sttp6z=`Df8mzlM>wF4EVX0%oIj?uv z;K}5;5DWXsogVt>o9bd7uQU6%f!n<`q4V=b8M#A_XJM39@k?nn{!oj4;Cwv%KKWH& zEDs>ekM5L&@qJ1&K$56eCjjEQaKt-1=f-bRpVGgb5Vtpzm)W{~7UpWS zul-amII!Hzu|@A;FcG|0vRzN2$anz}ux?H^fisO8s#tKKv_R_dn)4ouYs%|Wz-O1a zPbl(-WDY%)oj6V~Ph8G93SXd5x|&$zwgMMwa$K0hD`x&>S5deIr}LRK>#dm7>WyP& z91KrkZ~D#ryaMe|!9O~R&jWl=r=_2{v+_%Dl0~OkvdgLtEPtb!9M0kCW6weZV(0BM zJmYP~_s`#(tslB=#$yKSHndHU3F#6aO8VpPTlsCzm9r`5a6x8^<>4(u7Pg#6oI;hJ zYnasDN7UyRetVs{`qA0PYj}4Ht-EM{crH9gmYd_0Y7w#G3EsG++X934vQt3pR{ZW7 zF067Ou5+XGMJusTTX@Y)6BsMU1yO#G^l6~G`Xk!`mxOO_5KTH4)>ol#`v*(YsDUP^ zb6vrJ%`^i&X9~#r1vbaWFPT3+*Pu_v(4h*WOBwh!%iQH zs9u}+TJ!af0hqEjKp@2)OYwX^Vf&sb=>GSX%iD|o8UP(D-$rwE5wOd6narZ7*nz@! z%J1X`!967ms*>;By}qT9xLEEV-$utJ-3iwp@eCCcK;`Ms$z4u@u(5HGRs+^VtyCxh zY{*k8_oF%@60wI8*>MSxzN}1)_*VVB+6Ug?UCyHPn+b-7DUY<=$yGS0Qk|_^`n|O0 zwnmjumwlKo>P2}8X^3sa#oJR9v?sf~zP2++J$`XR%eh*bU77J1{+^u%$SvO8*;&RF z10ajq<1E@4mChQfRn8r9QY~TEP1e1Iy}4@WWoh~pmNB^grUSv^X!D=FY5(r`=KF!( zf0_JW0ZJj!C*l}L2-R;*$-=5wDAWEFL!qz79|eZ?QT%kXm&~`eDczmc1Lyo01*)n~ zLWeb~PleO&tUV@x;NxHBQPxxRty2hfF%>D8in@dGw0*T-es131+*1I0YrAg8{;(E& zS7~azgQ2yx6{O2iuu#c!-!Fq?KFT7JhWDyf>Ggf?Zjo_X$MzBBk zF~fMsF|#h;altdG!P}}3AGvdv*W0fa>ZYe^_&j><)SQhdj}==Hns#dw;F+17oFs00 zf2Scl$-shWIaOSZ5^&zXFbbm<<31*huhCYo=(D{S2JpNnMKUFY`JTj~n!+kR0Oe9L zd+;0+&X=0w5}8WTg7I3Bku=za^BsGI<==8t>tDV2BLUDBqeJq+>Pc-esKws9a~z;%?RC>#YUDl`K47gbvM(&dQPO)8ry|QJF@q_ z_tI3Kw5E`YweD+n1AMLX^~}@2z&kH0;+DeQBqLqK8gfgS#}LR??$lu1nxwlf?4nTc z53%|o3!3EeaoYp^pSxu~gw{TWV4;40lME-8q2b&R? zvYMa(a^8&xY@=-ThmSvt1}Djbi~O7H2YoeHmiXUsc2v8qfg zK{8Dg;c1oO`;{r&%mkpj{a06~(qIFj^|qWhZnUyvKstl9;>X6unk7B88k$nq7kDlz zbj^%+kKn=_FA&h{G$^8l0cx7L{i3HgOrkvBT>IY4kQB^dQqc|n1F8RKH+CXX&eaW^>5Q7Twia|1`NI4J{PRL<8hD1_aNFREH>(0U$Wgp2SjmaiE&S2 zOAX>H=V(B9FHIgrrYbzV-uX!IzNdL~&4E5hVL88tS983OY2V(8<5S0`)R>&;G@+)Z zMY@)YZ?+Bww8t#E*el-tOu=hB{EgRseifrqURW6K;o;cxE3O+NRH70SM`BhNZ3KfC zljfzXS4M~hjoP7Zey=;8OSd#WT^cE0Xm9E&+AbzxBl%aT5=U>AIIo{X$}fObL06AE z-?}FKArD_d#T6CFW-c~mZ`{y-;_8a8Gywb3yZAsEtg`y(ZMKLxSk_M+1j+vh46hCH z)YF6b#Kp1BxM#i;gf2-a8p^j&zdHB2S`)_~s4o88WKZ8R&TWr?CsEis0;PY7?Hd%1 zS8ziotLAKDM1h$d#S+3a9#~&qKoJDA0m6oy)IgHA4UhXFqmCi38O3Kl$6=wDD;u5p zgV*!89q6Z$m9x8Qp=ku+X~&$Jki=Ri2uKm@6hoIC|FT>^au6!~+!^RE|KjPU8eGh^^C9m&{uc-q=oZn|wQnPZoc650Y=Hw2flr$fp|+K9 zMJ0c3p%wcQFbdsih%#pF%D=vKD(`s6hS2k(5~5i)MAYG*?CFINBc0DHPH72}p8tW) zbyaYtN1eTazh{;Mk^-M~w<3W*shs%-bVc%{LV0)3WpN&=?AaKvTuX4{#?83_s0AX4 z_KYA_(26~DXB{k1L3Yh$?MoR8)0d7cRc7sV&;)g#4n6VaJqf-LynwC&I49B2aCtE%9(G}6G zmoz74O!dAC?FBqD>eP^HQM+NE=}GcO2#$&69H4Y%1*Fxdcb6Q^=*-fWzBVdah29N3 zURa|kFr}+N;XmItP5(L{Xgd?`a)uXJCJhsnARS4e%d>vPX}t|9rUERx$u$eF9+R6E zXa#P1>c&hm_-Tpn%#rBg_+pvR4oN(kt6aG6aZ}HGf6629`c!Wwqg12xAD{C*r2^gZ z;`Ctc7v%a{qRS;(I(dKNBUsibIRM7io|X_;v?Tzt>)j4_h@LgHbz)Sr08xQ2t*k>w zZ+USf0pFHA&WrZUOtQig-59JVoRtbVg%z_^kQUP{m9ar-!iR2d3YwZmXQ!%HZd_5;BQOc_OE{;NirEBn2($hZ#~(W zN%$12_pQ2=Og>N|Q{0dGzU!vXow_}QoWxTD;a4bQ{*eNd96QVgA}(hmFk*|k6cweV z^HC@S`Fv$=iNa@aUbA?a+uyIac)&$BVQgHeX}C3aYMWE*yu9DCKSjbUT}vZ+vLJ;@ z$n1s@qKM5+!`Q>Qc`Q+vZi4gy`5b2yJXVpGE$NKfZ0(6jW5+ zhJx~4uN0mD49r7?_cVPk2j!Su z$qFh`&+Hw*fQa~E?oH@QEZ z77zUze?Mu2F|u&d?&cc3CfFB8cg-N9w0+wTV3T)0h~Wiqfb)ws`xxCdHT7_BM3}JE ztwks?FDA#vq9;uxg#j>e=}UET zP18D$zNH}qNd^^(nOYau$^%p8zqr$hj;Nis(T-_oT=u<%My6W`nFq{&*FU8p?m++X zVpwg3ON;J1`H<{FMq7}TunlR{1VUWornL46ulBk;VH*05MWhERm>yZ#QD0HY# z4gWYq>G{xwmYjk&I{S|6ck|0t#+2TAr#IVV1Z~N1BdDYt&otOI>CNSp0YpY`#d(ffE}b{#c2e=cdjlRtm-m29l|h8=_}OB6=}u zO$&&4N&$pW)SW6*j$Vb&^Flk-?WvWC3e%%=Y>+;N8(LFlE$Lh3u)S zQClr{IfUl#-r=)YlK~_B$DF6aK%BpmWke7#@Pz&dXRNyXhN~Y2L<$x~^WWgVpi3e@ zR!tmpetzjgf+}|(63%S)*G)MnkS+*)q(S4W*E>TDYR8q4sw-Y#-wH9dN*~L<#@ngl z{Qn{Bt)rs+zNqn`q@=sMQ$$)o>28olluk*>0YOmd25IS*5@{qPrMtVkYi8aD6+hqK z`^WEHYnICyg*ErybNAV2-}BsEO_3{hEef4G7Y24ACP1U#hm{x#s%X8OS%31P(bnIo z%rKbJ(SAQ*_KQf4VvJU3WhdrRoM1>Sk4Bn+aBbU<`J7hH%HxwqCmY7tsfn*OJEe=H z>M`d?4X43bav#to2tf}(25fCM_kqwSbw`|!a|Km?e`1My7dg0PQ8D*n?FUbQ)@~pW zLb41T7}!&84n9KBow6brU&8>t`95NC_4x;Z%ZOt?&X$pBD8xB!(YI$kiH96Wcvo%mzcHHzSbiyx3t?yBK=INJB;c?@ z-nb61m3seskI6=^JM!S!@$ZDYCG@U5k>*O)_)R$cf8HHw{)Ax)BHWLMSv5-B$d@v= z;hI=oJ;w3kFq0N?IrXWqA_XYU;NUm7I%uD8sQIdId`-?E+B&XrxJA%N^CF|6*()XS zM@w%KHsmBbS$uqGyN(QW2x&FLumEG6}&0-t2+%Y*m!~r zGz*9c_ljiy#_7__7&tRh-bMJ0P|00U!VZw%gWGTc-uo}NeaDSlcY5dl1~em(e*EMF zr%3+WxvKC~T;5pVz1dK;Hkn0vG$I(;Xz!9qpL)ir1;|?jVe$3OL$((P)&%wxZ?Ozg zXIWjM2w_-~d#y_=^I5Di|Aunv0FXQtk%~*?*CFf=aw0fsXu6!vSPV5cHk9=+X2&|dq^*S_2~b#WyH@Fvf}?!dzlh3uU(r@_X$17_Ft~4C#-#c{hni z;cICte}oA%6mz`PNsLe+TEQl9?%#~6Io%2lLF$k;d(!xoowIdl4%YQ+=H_Z~&MSDL zitXF|nEc@XpR9Xu6lwl;QA(0>F1S2PW~Fs)Maicwg$ju@PJ9bkGMh0H!KqqVWjP6J zJw_+zBEn)iGvvdQGymie1nJpp@bUgFMp0ASpcf?};h6DdydJqg6*_YGZeeszm<AlCMvoq1_wOH@A`htPvlPvL9xE~@^p1CNJe>zmn?Gs-%9BQtp3Z1?}*x{#ijx7%w$w znM1H!NKq&91g$-SW(Br~5M;%gwsLE%Up+P>VZ_nJGB5Q-5@gDoDY_<nO&cmdOs z;(TweKr_Ybz%lUVig`lhzd^0j-YuEO2oP6ykid%yun^vl6J2t50^gsssJnQyh1J?I z1xGorjSu`JX=Ka$NwyD-ktQOv<+65GhKLGm$({@)wvb6tx+o!8O$ zZ34GT7wGkC!%C@|0+lo%1Q7_B-6xt+384xqIth8tpOs!J`w4hAK)6>55)>Brgt*v8 zS7y>h`B;m#rAonKk_-lOA04vBfEvcW>+9EIYa*>uq7UA;MnY@NFNTOvp>Zy-HP_cD zAZ9{1kn*|Pq18lAgY_6ITPoDp`f_HJ=?y0z=~j}};j!fdhoiQD`zc0Cyi=9?mN=Zl ze^2`0>$^1c+K7Sa{y5by3Eej05*d|c1KW8mBT2#W8sCx_-pWc>;LS4m={2pHxOE8a zi3der=pZi%d59fNC zZ}@AFqZPvTn(L*cq-2$UiA#Hye(itmrp)>I=8`7cOuq_!&u`HW8}Sn9c?oCtu*oP3 z*L_OO5U0EA%cS#ABWL`BgW|X*AP`LOKb9G0yf}MBOfFiDkoO9U7uxOk0N-*I-i#2| z@za|u`1$DCwL=tT*RR1YC%5r&%c0qY*Po^{L?=BS&E+@S&)M^MY=aL>DWKCnuAPmt zR;p_qUlbABAoQ0P>A!)4y$|Ic?5uillHTPd#bx9F; zW0dr+;qrjmr{0LAa3p5-;ge6N*5&mLg0>|kipT6eCSXY;WwmQ3cSk?{8BPG5l|_nq zC+oeKoKBAA>x$Nw!CQ~;(RZIc^<0iFbUSJ017&ow3#m&|`<=$5 zriPf}Ff6X@1uM5!SE4-FoczW{zDH3sOtEvnAV7J|IGBw9I&N?N02K9w-7l@IP`5f~ z!vVXv_MbnOd*axbv02KpZMGb7qPn7kYijs!A&@m2=gKATyOHxE-hq2yP0OSIe&c6! zdPauDfe-08368nzsb3k8m{r-}tY|K}qACY>6rl5m(piczivlfKL1%IzTwM9Of*;GxFE5&B{pG`wZ!n)n zm#*JERZ@xp%D29TMavPx3e*HYEnjq5S)N+5oaf%PbNa`k8pfZ3E=+yn8)6Y*6Clwe${aGf9SAk z8+w$Fj~XVSX7q1STDFrRDvu%7im#ECJI(hUsn1o@PhDv%}!m;eVm8ORJN>0S~_%6`%Hg zHI~?PpsEqlr3q@y

j1nSDfRmdRSH$p;Di{kbq}e5ZwBzfW(S@&7u$*A67a7lefAW+*5(G63dYS;R>zK))!i80jk0Z#dCF9O(lg(%dl#ni~V5&mg#9os;T^_ zFTJ@rt6?%Lq?g1%o$ib1x8`J(1&_psI=E3;&SLiP9bu&U69YD^S_ zwC}}l_);A;CYk_Tpc0uC37h_(Cn!ea#A7f)F97cW*-p)4uG1VizJq-ce&wBkqz>}Ny zjc3{nFEZpQzT*Cd`cc`ILzvC4&rgcfy!igMBHsm8zOjEg7=lR@qmrd+T#;4fE7O*z z(b9m(Y4Cw4y(yF{scQ14{fa6x572n(PXiM7^h{oi|Eda0aA7NvJs$$__)VAti6PL%w(kq~wW>n;nJ;BO zCAU!uS1NR2x(Cfa)r+LMY+vfPe7@G~hdJ~;B%O?0-CeVWEM~lBbTVW>xMlMMZdlBW zGn~`u!Pq-^3f$=E#f4(Qpoa~3HK(-{%9#C(6bS$7Za<(j$bSyFX{tq~&>G8wxi~zL zA3SJn%i30}BB9;Wu(d5t32W)>L~dF(-o^;o1Fld51l2(4tf*!hts${!z^yORZLUa) z_iI8z!Yy46N>MjW&T#@EET&Gwc-Ae{<8)Lm6o77eh9QTynURUUGrl z=C`FZMR#9<;eS_yPgvnHy}o%hRHPxcx^Aqb>ywo>u%X&wfK*ZG{ARUFck@h&$3I62 zr)r#GjYpX8;*@s)#9a&>=Cdd%T$Od`Q;I={Gyi+v2!DabGJ zWaG_}x*X_JeF$<&N=gPH4gt&Zfp_xF1Cz~`sLiozEcPd3GZ|V7Co3m^Gd8|uA~2=j zl|g<+Wx=N;a4ZUbr*k*M!9_)E_Vs&wB7mJnkE!q0&2Vj%?8#9l)hob8TDm6N9U^nSFN4LOs9fP*3gVR5{miBHW_%@QJQDCpL+PJbUXWyQyVWef%Fk z%iTg?c%6zemg2(rAuw4m7ZYS`w!f?71*Fq`EV|SVBv zZ=9U2eJhgBRBH7g4-HoUxNgY!ta~i{H1{k(hm(Dlx+wD{4N5kUidaj=zUM;cQ8sa zJQyy8cE>PDOj8&Sy_qm{@2Wny9;b=q zi;i~2Ks9h^Poif&0joY!-kmnrqzAh^Yw%NUyrC(RZQ`jjjzd?73R~RV2Zt7(JJ@5< zA3SEY*(93q<)r>Dh2ouD`?1AOQDCw*1bF=45^uIVQzc;KUggSwyXz`^2l4~##t;D4 z3ss|}s*yW;mPj|KLda%17+PB)VN~c2WH2_@96T20MZj<*vNyZq9B>@(4!H%4zdwBV z&{AR68@g>w2BJp76geB!0Q#2mpdnmMAI3}c0mBR=WK8mw6hY_MbM<(?$Vlw9YfmPd z@#1H0ZhU~%3&X)?^S4r()Mm~7-Q7@33ZbquED9ldu6>0y0w2B%7snr6QFJ+lBEJ(u zOZ%M~@`$w?QS39 zqOOwi%4t4UZZhCQ9wG`)aPQ4moZd@KR>PHC+*v~2T^J z;$9O*vQAoa42|%SN+C_HjXSWv5Ap+9HN>|ze-)rmJ}=!IE~U~#3To^AQC+V1e&xOi zG9&FS!k&RzW^0|xeIcG#XR;xCfJf;QPc6V|PQLKQ9|74EdVAFadDR)dCgWG~I zj`v}o2sabBj7!;1u_#4|jf@yzJd|W)A$>F8U0A-`@)!WVDweeUegy- zAzBxtl?Bfm<9`EuA^AfL9MvkA&a z6}ZDTQpLiY6F6BRWXiL*2un*umfwp{FA5rG)YwHUS$%}Wp`LzvVfqgH0h7-qET82V z5^F`e!8*x9vHh`xavJ_5Smp_(vuuw_lPdHrPG3*nF%~DI`){8p3#e@bh#<`lcL1Fy zYaG%WGMZj>j)A*l3HNi5ytg7n>3|y}F5+h9bc}u8b2Z)YUHr_1E zeIcHyNw7tXrZk!SaX>Lk3gC8Je~HWcVW2sZP#1BO7L*YHbnX~y*6GC!l+8i7o&b%I z0q^?Iwf3{hwOuOQ@q9@$UmzMEP)Ctul%bFx*%Xf~eZS9}`4avxWI%j?t>VX9fd2Y0 z?lvN)&|A2kd)0Xf<#oRV6guaAIlUZ1L|p1AMB^#y7OEfg$l@j^2yGn(U(H_GM#~;o zbGXK&O$NAY}`d(|2+OGfE<79Jprj4Yu{)?3DlCmHeTh4Z)2qPpt2 z4Z)2k9fCvjxn%tI%#BwNsNAlrQ~mV@7;141y=X3Txt@F#F$CDpoKRAeV~$ve-ROEQ zy(fzOCKExabbbU_*o~-wuQk@LlPuCQfwWEETg3!;J?36CY&?}8FR7WAj}X1Kthk(6 zfbu;BTp4>@m6R|VVUUBon}gw7b& zm^Kd)fP@R^bZ0~HAE)-Vyr5R)MO03)`~rp2m^EozgxZEreqV2jU#L6)Z{#`I zlZ6LvqAy%Jj$i+H`B>_md5%$AgV3>;2+7Q(ts84uem2qhQJ(Hs8t)f|^_HFMGh0gTO>yzOoXLB= zo!8PpaJi$B6C08qmP8hQ9D9u5v%uI}y;KZ*!IBA1H+F6(_HIsHIc z-DO%HTEIo(FYb58?_;-+K&h#zr{^`zsX;-w#+sv;PxB1$gUc=K=D`sn7j_HnPkWl&R}WNUvXZFMa@PrOvcWj))czn8 zKM;x{CmafhF@ebw0a&0vFaIst!xAc=D9m)v_uoqE189eqwnzzA{901zxq0z3vShgE z$NRrIC5S!G4g4j-MQH|Qy*@>5l2;BE-f+`#FvymH?)B>KAYcE&4`KA3(0^g9LRrIR zms~gfK*;SS6~;ph)r@9(T`u*;PsJEyR$ICHEbTwfx@GrYBw|v&O;WrSlVg0a@v03x z7frCT4c%=bigo+6`*GuPJ+*#Q-qG7MbQp>$>w+)`t1J9O< zN?9aF3~`Vhd@-^7ikT={V!Kw4cqd^T)HKM8$P?Dt{;RRJc!;~|9k;e z--Kzi8%l2&si4qEq5vK`mza_uPm}Kt{>F!(BnaM*7v3+zdzJ2V+NO*p;@!-icEb@7 zhhFyEZ(fZ?$Br*b9lTe_TrLk^=7ej54hvL#`@3@4%AzVUgUBrgftJ)SM()DsSv7xE7El{2q3j+{AA|f&{}7u^|NJJ zcvEo}?DPjy)Zno}p%pD8cftJUg^%Yib{k^YtG^#u`@DK|#O!}c=y~q=n`uW2$H@?wcyJggY z+r%-s%(#o8k{SCP$`FYXp3JNf{46>zW5x5AkMu2%#wp0>Tr`U#+548-pA5+ z@yG!hNT=|2gBq2c(+?7?AkFe)~tf(=NIQdL%SlYuHjS)wm9s zGsGBnX8b8pe0!q;sb#?TymFNg@!)NzF#b?mqOvQ=HX*2W$K7|s-6iKc;_tOjWWGZS zdF;JEv3&<`CfBG24)xE|Z-QYt4zW}aqG?lNtc;1qy3giwY>xLO z>@qH(Wv6vkcDs>!p({%8m_-R`K5w|*x8iu-G-3^Q!nluS9)tei9}qHq96$&WqkxP& z)(Eag#2^}La&Ir{7nIUrw;P=z1j}*FHWQ_E7*CHR&CSX+V;!Lt zB`y7_!UdU`&#aQS22^~H@uKZOulEl;C+c~$l;RB=J+(f1su~(9UCetq@K7=R0k0Sd zaiHtP&~R?m<%7|`+yeY1H>CwCF)(rgz9leADcP~ll;ss!du0m3PP9X;l0H|o83}^9 zU(>cL?|S4NdBPNmZw7oJ5Zz9){Yq7l_`}tU+NV`!CyYX8*Uv~v<)|nmX&-z*5m%qJ zH9LUS44=D_6-?;Fxi%QE%?h{%dfr^)2SZrh_6t@atDVV(w&tEk9YoQ;7LJ4Xhu2yz zMQ;`%>FtR}^gq?OIF0M1%U^`*3EvRlw5U))7`}jBX9qq1n`aVJ5vf7k*dT*?x3G2v zqiEvZ=9AQe>{&=u6wY-~^EF#XMk%2s638^=DnRTKAsQPNy=s7nw5<&?4TMckG)WIU zy-1l(se<=|LVfE1W6RiztCNip4V9--m@?MPWg6t7JBK zX&^eqFP;JEQ$fe>`PDY@&O{lL2hIOaGNe8{-S6D8xQ(uQb7sEwK@3=pD?IFz8WcF5 zoC(adOjYha{r=AGHLCOrSpEJDik+c*o;@*%;7h*=0(!1snV`+yg{zg->LIxlcShoU zGK>Oa(}<|KjwF)kw<}l*m$2j&8F@06;jeMvh5FQMj`4>a&Rm7npSP|OjAZ#HU1#=2 z3CV&zm(8(G-hLctO8Ef#`f5*a`z)5}rhlWor-hSpPDSE}Y6VFHEQUEYMqPyha+;Jr zk~&i}Qcac+*ASithZY*RrGWKRvBqvlthZqA4bnf_b%P=fGNA@V&af}-= z0ZA~}+QtSz;v`?_NqP+NVznF_DDw7XV}LCPY?rE4q^(AeE9VIbd1Fi=0sIReE%)2? zB#YSf;r#*V?ttadatA&NBG54Y>NEm&8PR=s3%f3Z$?Jc6jdhU|wBcSqciQjXq#^vI z&G~3Ofe)91I)MWr8_#vd&!C2co18f5PrzHDii08vG0^^t^$ZhKk*mI}(;_06v6(M6 z2!GeEwf&QyA91S^iS40;1l)Y?V;no@h9y|48xdr!v-ugoNgQZ5I8(F1SKSkjzo8>5 z;geE6Defgi;+asCTVNdh9?-L+qiB5zR@lH0y7GLN+Go~j=&s?w9BrjX(yRIGS=@q( z0>3SZyKxSL!e!BA`{1iPm!oR&j^)pz4fU4=7L@29Xw0?^TbI4k2L)S9sz%=<+{Uf` zn!$ANDY`Q%@;KW4WVZnEE=va*3_9ZXRBtEd3c50e9w*3`8-b{kmX}~j1KvOL1wS6a z>K7oKkRd7v4!{ocBtel6fVx*b6j~_W@)fWDcCu46x8Zg3SA%HljD_-7$bf?g_)0y`8JeAbQvB-|`TzYdrwGCQoMA`1wOuQpn3WOTpiSL|x-s6kauKe@HBEzbSk) zx2|ZBI`|kXqL6sjY$|MbnhzH$_I=&1%IwOCls!Ff$HzN?ukvH~-dv1rgYT3Rn$9R= zS*P=IjoxNgQ-bAm9xoCU`95b<(FTEG+PX8irq56KUY!;Q`2JL0&1-h z6)`2J*bk_1Fuf$); zAeoDTF;Ydm9>DwItAD@e{IwD$%4neD#>O8ekz;K-at*sXJ3HO>H5)VUU-~>cTyUGQ zlb_O`KPHs2HF**B_~zy&PVmxO)C{xM^Oo$a#_Ph~+QC65^whMNU*5C}_RGOnqOq~@ zljYL9*TIv*_&&UHKWkzm`C3@i?J*9hMBy>sUxjx0jGd>c0TqE@AwexF2P@uFT zK4z#r3H1_eFAm-6$9lWNClcho^1-p0XTg&%*t_j3N0xuptWG*@7m|3uxQbYFu9qMp zGE9P(1R)((Cf_Z#8P({YCcp9(a^6RYiwzZJDoKUlqsdT%n4`TuSiSp0Fk4;&jNYUI zxCT(C)FAV~=fF#T064YD7Q2cOdqghv4eN55s5w8@XN}D`v!)rc7j(ub?Cj{MI#nkg zdRr0a&-$GBKmai8$yr}tPpLrz-dH044sCHnI8Yie0e@IF95%Bke7f)D-00MZe6sd} z{2Mfa1$gdP*v#1QpHw_WmXhpw!om;^d)PYTlGc}X9jdbbQd$MWsHj!(gihz{-HC-V z8{jT+vL0jk)FzZ98M|+<$Q=6})88+bx9g1^!ivb&HL83IUP8PdQxHB*oxGrxGDW3n z<=B}}Yc676uxD&+s)X=R*fk{%A2gn+p`97Yu8&Qy136E=(I)_(m?RrphRo4xclC`I z3n3PZY0!St_-n}2TXi*|2;ZX}V>KlyD<==-;Fx~HhFk;i9#^vxnYM#+{1W0lyEU=L z24)j&-(6Dl!u27@Q`Y_(u+9H+Rin{a2Rkpsx8_9WP(` zfjyYi&re_UiT63=#=E7mu~!%nuVrT14dU?iDq`$)>%%>t5_C5)I`Is=%&{wS?5eg(9de?t7#-6wm-`f0Q{*KWo>&o|^rl2}xXkPPb6i&kfOB6Mr(?ncWi z$-3m&Z&3;s`32P+Ib~kf!7L(<3=CWo=u)i{3)m)1o8L6aG%}Hljl!jxch(yAu=ZWv4Xz4j&PEqo~yWBY+@m< zBf=eJOh`lcKRw`n0uSn#2FH}2i$tAmMl?$+=_p$?B1<%q?}(*gVJlU{=(s+LwUQ&s?xPs?fdmasEHP zXCj1>5AR2j?50jFz)c}x`Vc$O74Z}vq?=)cn1Ke8FK$5cW2*Nz#Ok(kAJU=n)J*4~ zJ7OE-=6q!gl%xw#aMNp5u_tV}JZg>~kU;zzoH%I9kLGzY9C&Y=c?saeSAzIeyIZxa zb?`)m1jwN|c)_v0))RfVMn`jbm};FZn;Y4^<~jO?zwpr`q%~3xzhDSCPCvChbXO$` z?icGE(iaxO>sKGgMdz>v8qLXwBIjPRpga?|Tcf=mFot@E9xd2@8_cnxsIvTld;OzL zjOPVI#lO+5qXD(_dQWQqo7CkkE>pkpac#8s&qrrwimKQ-I2e+D{iG?kCdg_gC86%L zSHt+jD5CE+xg7Nn#{V!1I^g%@;_wB1u0{++1Xa8!L0N1+V%(-8rmbXerL~zdmUTR*$o#5yRJ9g|={Dpa!=(PR-pfD0)KEs_ICrIWN8&g@&dvDXK$-hbH0EYPP4R%BK=!iIwa0yT ze%4#k^9nEtnha5MtHEyp#w5-B6sKBnes6c{dygYP)IN~?xr3bgZ;gT{E*?C4b0w!( zu;bdkv+um*ylR$jM8ZB8M^cuUZ~aB6(8~MNTM~oi<6)<5UEQK zdYAR!%uFjsQ!J&9qD7IjP0y4GxiUgpX>?*jC3WuUs^esSZs8}2JVkN}>LuAH1kP2u zd-$k=n~PD9)r-2H4>Ni}erV zH&B3rm`rED`|X~fJHppRT`B5R6!9>4ZdMRAAxQFPwc8OCc8c6Auk2+`cXO|=ym^>( z;7COVTj8(4S`6q-pSv;Ma|s1yyg@Fh-I&AYNg89VT;JSkQV#AqFbg4i&s2-dx*=p z1fo>c`{Lo_KZ;>u+Z_nD@lz=v|5XrmB^TfJx(dBH8Jiaqx4RwA?a+!_{Wc@}v@}^v zg~bSbl3${~xd9Y-*A%U{<`{5%-}sH|l=(~DSrsb0zG#}p4T7%ns(Wh&Zx*gt&QHT5 zJ9x$yJx}|5J(rWHUhfGA*yhSQj=pekmg=@1@KN@J^Q(S|@XM<=xH4SdmcZZ$g+lPq zsi*3|nw`Pf!u$Z1T!9vT!7Us!vz7J3pO`|()6d(8U$<}jlg7l%_*j{(&S7IPRn~N2 z-RN_`@$&DA^~v3_)$W;ases=f;j;l7@zYuV%lUuc7|`H;T$wj8G!T(O{OWKZxIbCD zjM}5%i=X#&zoa1w$o!qq&y-(?!!mHxb)MBq9^Ka`PrmIW0TKdJ<6hVSP`02T25(Bs zpzrx^S?E56<&+U0h+}V35nv^9JaW2rLWh*~OL2i5wfgc37kZoa{HB{;gy#epx-71K zTy6(fZh2 zoUnKatHjEM4JbG)RE5uXnSvtbM2GfsiyK)%FL=R%S3YShWXKf2&tV(Z^00!K%R9UF zdj=LuEtqHEYz@7T$)IJbBWsWGfmb#?2@N`AzO@}?E(wHEVLULIR8AKh?FC%w zyu4Oqnu^M_PbbFwIfdp%B@F}Gc$-HRPWI?lNjy@oukn^nWuDWyH16?C74xlW>CB1q z@qJ3}L(O1}`55RD`r~9PG5eD=nijK3jbMJCj)K29aQ+}DZ#c(aK?*gCdgR+s=6*VS z9#%7yAO@*ZxA~Y>M2u>Vlo>%c+wOtp0dvpuK@`CQfjF@L# zf8t@7IXH{T$quaZs*Bq(;)8d^%(h3*a40kC)rJEm$_NJm{|olw)?CEMz$Fr#pJ%5d zqJIJ1;18pb4KDigQ2wNq)#FMs=tFv+O>og;X+9aKX`py>5*=!G;e)~h7`@;IN3y?G z7+6*~xRk3#vKu!f(&KwTD}o*GtaI44(C31aYu%hzAswwlgekTy{3k=1s~)pPYnB@} zA%cxW4KH9AyU-v!PDC6()5D8#Rc`#e_??N(!=o&AqYsf2Yt#`lR>a%QfiN%MwU;tUr3MjXHcYM_dVc-_0{SV3ck*Ivj`n;I%dNbH9k)hSdF~51{w4 zK-6W|``6+N!*!6PaeQAsvWtCy{M=>tZEFVmTIukIOquU$Gu=JtVTsJ-#80#%m`Rx)kb8JcnL~GTyHMT>7K7PwzwD2n@E;`1KE?Ae zNXw2|*m7YB@<|v;-DS#)5qSu`hMLQRRMqrkmZ*n1igYFy!IDYwaaqLQQ`|Z>9cseu zi89kGhpGdiSnriiFO0nd<9P`asM{@{cKthAOEdrwLy=SE^ugbar6CVY7E4D+|35vQ zF{;HFwmebQyro{zc}1|cYFrWhJa!u}@r+z*S5o`r{%!aw-Uc7#JP+;COQy67s3814B9A3Z9BSI>H( za{}Jm{q$jPu>2LTK9R7U1@8uHIObM;L^2d+P32Uj7AhhE8Fg868s~tQL_Ca2 ztr=2uK>v5rHty=KO8e*aGkK3yjm@oOpB6ZIZKW z;6U#KD3^|eWXGS2 z>25~Ul97EM!p_g}3(NIGsCG!)F6Krc24whRmFz~t!ge{2wKrOwZ1t^K<#Yi6H*<3c zF4woZbuMS>6*<tk#aoMp^Amtfdph{E z-crsr2Viuo&p?1V)T1tT+6-cg$Hb7aoI)d5lqImKkT=iPi}2AxpX}A$u3WU}boRLM zqK!GYN_Vdl3#Z%qZ1o065Pk|&FZsD|R-?LBda+ex-VnZUx=7^UJJa$6b7-O4^dq5t zn?CETZIVfP6Io+<^OpuzpEY&{4ZBU9bgZNxq4C#bFoxph+2q{bgw4w>Jp45aH6_0D zeXbMvRRk_654ON=dE9GoZ&3Z_fY|l>?rmHMP>%k2^*z7KzOBih82%*WKBK$$^Ijow ze-m_zEzRMtxq*?&JVArgUsZNvtlO#dDq7VlXUsTovjIFT7%l-q&?MFPDZ9IGzq;WZ_gt-C(B6Z&miG9pPAU4kRH3UTSQl#sH{fR zUld~i^YpRpy5?JTOyC*S;s@SodTAKaiFJ3YzG&bTN$M^D$$R}yK$Q`p;x+-)lz;0C zlXx6JUx}YnWk>!~LAc$iS8fZ&#h8Ep%coSo`J07qFR_7z?>;>V4A1>8!`NJ|3*T0f z<^}KjAk4MRz$;p8Z#F$kznVdgqoDyMDjyRUC8E4WJ1R4j5oo_jtEs6ts!n)LhhrDz z#3}6_!XZt_cZsDi>8f0+ZCHq{S{*5s8$HCgqeQm5Zn9sPndj zcC?F)NkbH4qQ61`X%pk&gRRl2$1X=b9goI#tcwZ?`&2RqQJNVSHp?gm4VH7jm&;gP zE9cggZ~he!Aoy2z#nb~-p1)I-;Rt|?>i3HWkN*KSwWlfUnP;@I^>l?d$yvg9eu9QC zd&Gl+j~7PDH5@I;FF=P@gz$ZlRO@&9fw@k}7FG6wfi z))DSmVg|!uW@j4pNBE3f4t-VXPuurJ*|5Xb2Sj}kJAw0{NBNV z*SX;yZ7TG&~9|nAXd*emBJ41 zNWY%>Yru_D|GNwwAR=iL5Mp=0^9L&YbAkdHkre2M$N;*FgeDBjq=e|!P|8=qQc zY3_=-+KCq|_jpa_4@m3tXKhprd4^kO>QjXn-8);N1t?e}FJ4 zbC%TcHnB_mR6Eh!P&l}j*e}u>NV4LO)m_ zywF{2n}^miW({7M&(pSSFZx!z-3}QNBV3b(C*KB#*1&$D$|AxiKW-LW6LR-ybR3k- zTnb$Np|AGigepVTQ5H2fgLLOi8a%dmaB*EQs-Jt{box0Vku{E(qsJ}x$vM;9P34o% zOHL7$v9!1OPmJs2^ZHZelEP-zGJalP0czXT)a##_Ch}K`1`Rh9U`2=Td%qq(;{Exj zaR3C!+3Em5<8OtvRsA75>x_5$$GT5v;a~ETL5#9%Qzr{P30a`etE+n{`hJIyvCLw* zod{iHrUQL+cJ|r1AXqwBIpZ8z^ z8i!bQ_4(jpi=!UtvX;Yo{)IN?QF`}?m>n`d`f1up_ci0ZsQ5R`#jMZEZ}7+xZ|BNF-pc|=_MS&Gw%Sh9C7N1l-Nk+Q50#tU!F)nR z@eW^tMQ_(^$5j6Zj97s5F8jm(o=7v>-p$rIP~$?BfwH)c3ZbiieHN@Q-)_y(Mw2~@xhn1%?vhBsA^)4j*BUD}{$M``I99eRieg!<>3a3s0W?rO{?%gl&bUKHPN zZw0@MF&);FNvt?6HB>OMt-QGuB%q+d-g41Qv;39*|_%3bBEyNJVH)(&!WaH%E_45IeG|-6s(7x0)e(#+T!ES)T5W7+EYMzIGtJ z9r=|Ld1BWv_v~YbL;|OA&dA1qngF@lLkLhO)}{7Y#TwI;wOQKNIDj8R%8;Rx`6wR$ z2q|@BTuTZLNRk$Zl>GJC8}2R`v1C_cvC0G1I}X=E&SIZboh)`k$XRInQp))@N zsfzO>X#6=L1QuVGM)e^)D9Q0km5ajUK-I@{WSii0%WENHp#MtmAxzA7J-r~W3?LXu zQYo3Epmo1=I;Y z_};A68#6}2kWBY7KUt^IuaJ$cGc@=iHnR)-50ejI4YJ(K9{^FsJoGTff<5QIBoaDM zZBbmpc?6AMd;~4i9qCHj3vURU!5ue#xNLe!)og3x!uifMxL0;d<}o|3IHr1ExB}mr1hvl-PF!DKe|TkK<3vqYH)(k+ zDGCkkFS_%>eIy(i8EKpUKU{rvSXJHj^#MUC5s(h)Zjest?r!OnMmnUsODXB@5Cjg= z-Hm{Bch@=J_Ipo@<9BBzhYFU9lqtp%&9y!z{kZUb1V+;VM~j?i|6`+ywn9&(mvS1io}RU zDGYf`<$Y5XJzK6bm~)lQa{s1&&fDaJ=bgnjFJMO0qtf%9%dl0BdNf0Md$)S2|G+U} z>}Z{mO3)D5>3kzCDU73kGv~!BMDj9Z=>$*mmF3^`1%l0kGj9M1M{xnl0vVd(kdWuN z5Xlq1|3D`v5}<-nrdf3!YryLCeK{hGA=F$|2mb%^eqmu|(EPuNooakznk!nrSy;Zp z&g8jgSQG5G&?Yz*nba!%9Praxk(Ilt5^ai4ccAUJDe5wE2~KJ_kSEi(ENxg+R7M3V zc4-vI_WnUonhK&hkK0BZ7{ zNw?9Tj!sN0ITBCk{pA>+m~cw-h*-_kc2T5eCH=UlHRgsn+uv|mQ0J!M0aaM}I$x;C>LK$R~hue_J^DfgCG*cwGasgs2 zQFyt;ObP|xj=WN}P+Y%xZk4D2A~CbZ1c`yr@TI22JB*n~%aU(IcA&v`H#NsAsMaTk zztA885%EI!Yd*j3l35)c5ee?(Bfbh%;yzk*t?c8?@s9+bF927s#-bZ=XK3iud9to{ z)#Umo(bf)euadG|L=T9Q#Z5MLFzH>Zv`kLQP>*G8OI4zqxf!Ge$~+lhgYrLwD;cSd zIr5q#g@_C;b~3c1QwPQ?1yOW_Jccmt5!w6tLpGnXM{9)C{;~^Dz%ZP71u!)aN%i?s zR#wKYD*Oj6nv#%wVp~AXLCXck3*y!!z9Rf3bP|Y|c_3l`JLC7fK930S^W1szA5b}- zmTw?}Gq)H^xM63|HDW07hoE0AyrPb|^3CCFEi|Sw)}GUZ@W?>JsB6=j|%WS(Tlk?T*`*$fnN-lQyBT zlMW;~a)zx>0=BozfUf&P{3~*41SQ9{-*Tpnw4q78`hwot<5IDau8vtvgh)H}I3-Z?I&< z^jawoq*b9&TCzrLV$K3AtH4v%w||$F1gs}aN6OYvG1$c%dX(hY`MLA_7l?@4O)fE! zTVGFI+V7_0Bq3cKw)D)9<1vv|4!yCq@uS;h!|c#oHX{Xk*tivZY_+LRA&aagB+uY# z(W|wITH6ZL5toQjqQ}QiK^%qxeOVi}a#88rNbm$WCgu?l74gxKg#_b6uDY|c!jE9w z$N0ifVfUam$g|u^sWwH920?4fV)DqtvJUZ*g8iRS&NGw7Jf0a?F6Wm#Eezw}<2Zdh@nlY@JV3(zE~caILJSx+P18AfZw9bj6Q?iYXk z0K=$X|B0MH%WCNUsNQp7QSVFMn?Hd7A#BPS%_4zxbaXHghEkcw@SfBC2Y?Cl!tRV^ zYesC3WoK6o)^CdbQzb(9^DjHN5gu2)48CR)^TgXW2eoO9Ag54PAwPA`ctpo=7!tCN z=%zB{$0I8$OWwHlYyeR#!-~z~&CgPiYW2k0FkqwK%5UvdjKrBa;|y+0^49bBm+5$a zQ+qboz1A5RAqh6#wRrI6rvt997AW_;l8rok{(h!z)ikE=qfqEoXpNz`Kt-Qa*a>au z!yNKkLMC=bowuq?CED8FEj}#zUMCPM&(cY_RxSMq@e>l7 zD>tW&>;6Kg6sOJ+utpMH?YtlrCwe8D7R|3Bu1(2kttAqDfitn*TTt%+G1lpo@T2&1 zS^+WvCIFer;aEleOL~>2e4Kc4sRWU{Xr5eg3fsvx{xO{nDxZk^Ia(Q&i$u>&g8c!Y-TMP$T$6vR{|moI1r}VmYOw#rFrb9wR2Oyc@o0Ih z$feEpfrxhQ&S0MGFu)@(m>-odoky>>Z!5QYNK${Igf-)Vdm6g8AxVRV6rH}WyYo`j zVOK|{y=&VHrURMQV-cD4S8xa;#R~Yoo0CPS(FM_-b*sp-bFi%H(}A{#u*j|MFej2E z+4RJWMCNkl^xSG9%c=9OZ}StzpwL{t`;{)Yg1>mwqwVF65=7jVeAIP8T%9>_ZFM`! ziQ+kOg+YtP!mE|t6=_TaY1Ip9t_B(bCLy6E4Nng!%>MqK44&;>uDI>xtI@PIKW?;j z7Rcf9n86c9yJ6>TpL&7#pqwf*A;YBBe6`fsCeAak`*mcfUpkn-fvoaxRYf>Q_;<6E zgbQ}+KV0Yk5Ri|Gr(5@_cv7v14JQs$_|UB~1f|Za21udNrxJ z;1#dWTZ|Frx6alUCRKxh&s=`Z*RaBWKwa|lvLXTdaX288iCDRnIpjfJTl4nK!HMm-usIlh?K z{sLR=hHCmL^CPb1gp1U^MDR+Xex0pD#9F&(tY)Ag9!Zc=gRUbuH<@-rxg>=ZcUaA$ zNGEep+}0L*edraQJE1)0TLz4h+bt+8`*2xWIyfv+5gR}ABf(uqAXr}@HrbZpj;`O} zJ4Cfko0D>#xhSqI%-Xlvo@&+cJ*%m0rL*J1{O^(j`WyhKLOVg){#SCNe$)Mg z__tGwi5t)efw>w!bWMGq^zFSC}5sp zsn$D6>0MvMHD*sd_sbnnrGIJvj4rqKW{EzeyAt!`LTnhu zd45`~_eBJvD38edICNDZE%4JytHro?Ya`h)xaWVHPZ9BemZ!&kj>Y2N-D^b+6tU{b z`~3fs6mh9v5PPA|YzId+vr4hw82}wtB$8^70gr=l$p?-v3B7&|0xv#nt71Q|Zgbt) z9+}oin3vRL%EXNwPp^JYrm7im)G)B)OVW5I0K2$TX-csjsA|DZA}X)b^)h5DM`e z$}OIH>zypdwLYb-Q0Or;o9@ZC6u~b91J_r^m#H^3oZ-cJ-EWFG=;xfmZrSirFDWC% z-z3}~%9vXzoWG8>#8-GI8L_4=YRfQizr!+Il<%3n69uae(GM?8vY-k2JBl(pu%OoW!IDY z?{=s9t$0n8$9(#`Foz{zi8m2IM}&yN^13O^d5}r`E4HdU+_hM6i=veY#+-*dTg`)Q z#!F!`7^9(5m0ITm4QuvT=0*R)gUu3V?82$OSS1onne7tln^ELAo_Xo2H(uf+_&3TN zWD-1d!n~~hE+4jiR)&UT26oQsBx}C#f8gTDaO3obr=k1OKQ@^kGBd}lcrEoxGHOnM zo8iqJ%=Pm(%PQ|3SmF+iO)2E!zt{y5lJVn9`?M!uIrA&G-Oe~X8LxBHarTr49gWqz zR-;FPC0hv((rSIprpl1ZI)7mm&Y^&eGh76Jfm8e5GfiC9M2PS5tI#HohG1WW@l|)o zlS+hS#^q0XW+g*tarKYh_YkfM-M*7%Uwqu%?8}*;JV$JmQ?Jj_Pvk!kBAq2x>zIln ze;+%M3*W#=hLQ89%yY9C2l->qi2j3CfLa1D+yx{7tz*n>#D5qFOCFE}(v%-@3IBYK z94Q2E=bMYm!#(GL1*Vb;(u+@LUV1Csj$!xnyIn#C6c2JdYlqyDFO)h#r>hOJsVe#2 z-k$=KeC-XA>gJvTO!|5;Ig;)y1D`OC1ipF6Er_wHvHH(nm`tcut=B&}&AYR5rdHPs z^>u>Sk;yVpv0uy|F<>~*uJoR{i?K@RxYNTod7YC{=4U1r0$7Ktio&~{)Agn>1pb6* z_iJUATZAM>i24VTtXo$7te+RfvmpAbe6ORGgT)`cUZbk$CqB{`g@y_4r?Mi&zVKZc zx8zH1VN%qc@qE)`1lr%ig!ByPE3{e@R_zs#YDuVV*2maAJ`IzZ4(c?s(7EgR#<(eo zfR`@_L+6OGW|QupY+Ve6mmk|R%PB%0So}0DPqFFuI`67?ol>IkvKKcE0#-92`B6U5=t%ic4t1 z%E@0#?l}6%A5pGj%c(lfe+EuFAq-{SyJQ;Uu;$B+xX6|)Qs_dNK}|1xDy?5(w^TNp z$OoL#x%O$!Rg{scjJmmYe%aHNvFHukO3DDq?j@nOontKuS}Mkos@l!5vlV*TKbF9^ zd*3nz@!v#EIWwQUN1w3lj`oq?dl4At&^V)ugCc%P)Jm6dUoQSXxF=*+p3f4 zjdK>(a9yCECf$ix=#J<8pg5o5>S=w*bwfYPqy+3p`p(o@*>s@ z_45$?u!qaYbrB^+!a`;aGh(qL)8&4KvM|xs)O-~h&CZ_F7G|YmP~t1AEH8+_hY;Rq zD1V%meqScZi0`bYZzxIZw~?u~MG(!&R*3PZ&Gwt2#r#HzG0P4NIR8Y%$QxK!xWrub zf6HfSUU0|I>pa@Xm48YieLG*Z(Ft=kV~Dm8K&Ng5%&pkrL0;2%>CJ5H9LXpHIA{8YS$Vy;Ym7{7xRz?>>_4l zICUMT#r9~!b1zqHbL7>nFKbsb{IBgDjV7%K8T2Hxi)Q=K;m#>@@SpO~*(I00;mL>{ z1u@IovVJx+l#Rqssotf^&`tAe@QDX5&yB45;|0*O;g=(niwjZzk(`|OX?kv^dORX8 z-Vzvp)6oe}?y`2kh@b423c9}!?cZ>$0S3Eu7N&$SWw6kaQZ>eN3kdoTE55$DK0ZOD zYFzGr8`3yzRyP4$r1$kjqy4NoK-&Mia2S5~P!22lI{!Vx&w;L!uwcE?zhV6yX{Oh= zkE&zi0DhE)yBHc8Xm)Wu=godieWLHT+3?J^e%t4m{WJ7+8{OJ2kBWZ_aYwH>GA)ZV z8HktwqChMp5}-sj+!n>Xj1Dp#E|DX96UX?%NuF{RR{8ZN@B8Uz0bk(M^86SER}SB0 zb_N~v*Xe7kE0m>FWB{F{ck7OKv@BxvUSrO!1`v{8`M409vH{z{5nSdsklf+DBLee0 zzjf!ywd(kxz1-Q+QOrDW`T?VEYXsd6bCG{V0@EwXUbW|09mL_!PtzY(#6EntgKf+C zQz8KnAf^!T`Vr7K#_<0Flqq2ltiP3i6zSgpnZpOHW(i8Jt~_}U>TudFz3VDG^cpI9 z+v4GPbo&k;7mqYC=yvP}XGIvOdqm$6ooiyh_j$w&1Is?|No&L@%aM>1_rixpL{I{9 z7a{_nIl#aY?R=2Td!7YpHE3FT=jz%3IHaGr^AEp2mh2I%Lkq2?_>$aY5zc3I<2>dp z1qsq@4~*V%RmUO}pycWTC$~LGilFZwzmYR8Wnj+x5f5^8yP~*g{Xkry(;P1&Ltr_s zAu*>#!-6H93*)_)c{3qm@C@S*?*?X7)q&hGJi{03`fq;O6bIh8)!{4KpPv9lbrxKd z$?v|md6=+EbM`rJ`fjf+8b9jgncy&o+wcwb3L*y~dnW5j_xgaCTGC%i=os+q?I>-NbX0diEweLnY z6|IN@#ZC-`0A>&Ps1zt?#DJMw|%CB^J?Ov zs;w9M2{$x%yA8P2`6H?{7L1hL>qro%?YtPdnOxg`{O>5ld=O1LRAu`Y`7BXC9M)=Hl}5C~rYx?TXoqBD&nr>ua}yDq)fiA^iD) zjWZ=zS673lCHR za)EE<2emeqpLYyA$YcwA%Z7bCF}q%q_q;B0`QSh*;-i5~bG6a>(Lb?GSM$TXNPy!9 z@f>0@w=LM@mxejpQ_u?LPT-4oynP+!P!F-}mP2Q~Cpg41ZqP0c0b8V|ejn)h3Mrz$ z(^IcMX->R6fO;C>ozt)0Qe3@QEV8*xa%|;KO}zn29A$=|*>Td(Xrw5Io0UtE6_6TA zr9r>0v0Pofq7^5DD=w+bwc%Qvzs>G1t0>JgD{D+F@n+4mKAHN+N%RGPLU!|3 z{9{_YxUtfIfes6>ru<=&ylbgMK_n~4)|dfuy;FJzTkO!i$}-OgMYH;#JzSHPW~_7f z9tzMmr!6E7F0P`Uh4FD=*%St8-AZgdUBW zwOJ>;tzu|suNKvc_j~!G2=~SPUB2S-zi#|aabMO=82m3OsH+Q(d22{jXFNW}2BS z(&6di_v&i8stTMBm%Q|0MLsqc2v57&z6n96McE!gJcEIND1n#uY8KFXzu40`a5a;% z2@j?llGVmc7Kq5>mT|d3o#>ZZulu=Ittu~1l*!|lm5X5=0JP-@`sb+SWGX(n`J$H&Jkp?4()?4paq(@HW)fn-=J+s*09rRLJNmMs)xa z-0g3RD!So;##+7OhC~L^+EMOb4`@LfMJ8KxQPIlT23B05L zPgA@}i#}*lN$Wb>`C^?*UN70Mcn&UD$j{bvUMb z)rq7aZue88NwhR+P@GL0B9qnNXPf8?U{I05L*DIk3@eq_XwQlvD*|Y7adCGzon3kw znD3@OyKZu%`DVl-MTQ#3@@T!->QPrq>F-(bhH?|SH&rwv+tbgS1;KzAHo{t2Tj$Qs z&YF6(guzaRUKUWDo#T*$#d_n8;7lve8Op4t3gtI%9%f_-Ey8|>AMQ=pU%rOP81N3# zbo~N)6`n)`TGFyzI;iOPlCOQ)kskP9NrAa-2hw^G*MuOvNRbc*YOK;1f65wc-T7(< zY|E7AqwkEY5xU8!B7*LEo7F0~_<*M7* z=LuRoyE<>9ErdfCW?g0+{u*RZ4_mF_*!G1!a zWwOPGQy$aAfGJYT_wUJHs9*yMWK#=&3ZE_dZ6#}Bih?oK)YQgXTn|+C`+IsKaikj& zl%|M2{(M)PhM{X+p(S9jVac`w_wKB&z5175+@in#Ht8uY_Mgw4 zQ;hhR)pE#DAo=G#(Gj(|1k0^I=Z~Q%q*I6M_Fo31ewq>=ne?Bcc@)lsZ{BEZ z)A9!e{N&1eu$val=1;HVM>ZD5Ht5vpeSN)b4rAjT0zS<|# zU=-?&_f@{f#V6?LvHYUv30JZ%&!o>+l2SKI#@hI)1q1{rPDmG2-ucCD3D7+Cdr2-L z*?Xh}OSbH2_on(%mYhiL8TQ6|d%W3lHluqD%=by-`1~cb!y2%5PtPs^sh^bgihCYt zS`4;6G`RNgxtuxV4F$Bae)09{tBOY=+Tm5N&L6{T4Gm=eNU^DObGY_P$!u$HanL~d zFoCvc{ek`%v)L~pH8fs#FSdxR9|=OmhUw|A09u|Z^Q2Jg!2KX1{lNz7>6AdRFz8Wp z=QV4ujFvCdQKkQr;g{B+(-^W>qATFQ`mGQa1A)8$8jy|c!& z=0|~BD67d#|5D%$F-K)<^arg{o|evAc-1`%)+b6d7&#&5sQ=E;KK-+|oq?V#N@P|0 zz4c^&>5;(pH`ZlQJUCrBxIIoK)FJG5XC{Dmv5Zi1axz6$54A#u z)CcAqBH^fMVHYa(K$~O2+o{)#~4!xxE-dE<+4~?Y*tjxCVEjy*s-n<6xfah@T;zef0#B9L#XZj zo&2+dU3y(BCUk{fvHl#a%TfsYS|yZavqml9!1|WJ<487}EaA5=;AC4Xb9YCT{Gx?i z=T+o;A^n6Mi7aC2`yJh=d*k7hdb6RF{5X+;@h6^-$Art@97PA@n=<_?2hxVls&O{J zwmf83Yn{On4x%VWi~qR7oui#}l?oCAzANO-DLV@CW!&9mVW4S>gLn%GT}j5oo9hHh zS*a{5pO$~Y6#W8Va7Uq?N@p2ATv|%E^swU7Pb4P3`lI%cq+gvJUPoF48JA{d^-0GEuz)=a0UsT(7q%dOVD(E%G zbHIbe*0cNwpjt8!cr;v5;T7Kr7CxWqP`|=z$mIBjWBlq4X?Aw3?@d~}IWM_>zn`mt zeY+n{#jupv6HINOZ6e%M=B0sywxNynV|r!pK!g zTcL-F!>H%{q9J9vdQE7}D5>y!?(j;pHJ;C7^hd0%pR88@1)u%Fz;W5EG^iv=4?;63 z-PiswE!L_5*esS}$^$YJluToz*LfB#`I(9>y!YO$dNQ|8XRRYiLBY7sD6L)=N!VRJ zS7FtPhlfYK$e>96|Kr<=R9x9JQnkCj~YVs$YK0v!z%2`1X86yWK!t^6l+lNc`|AD z5A)TeX1vVpx5iEt3n(||?TKU0IZE^{PaXhBhwr4hWEBiYMm6w}!2hgv4hb~fybB0v zK}WGA);_=<%k|ITO`Z=_V&vkj)N4+#wo&z?m_PzWEeeB-#Nhu&My7-d7nKXL)oKk3 zp+C6DGP*OuITG**i8eqaZ#MwtxZy@iXu*vu2vp4ViovriW4@BYp@JyXxejsSS8qbw z0EOB2|i#*f`cNRG_WKKn#Ye zE|~ZG^MtREo>S&GQ0rnp)<>?&m3I}h9;R8rA%2WzVW9fA4u2(|EK9!+yB~a|SRg4) zV>zr?ZB|Q}gr|<4U*%M1g4nM-q>WLMC?s@da+qLYW{k?;I9C)!gK}Jk{WgaZ-KS*4 zK&Gok65d|^03*GZ6HZ8tSyd?(JY~Ti)Il8rP1@^2O5kHjW_jS}sKjA{VFEkTy z4=6+eC|Hxq;t}fK&li+uiN+2P_T17E)~{@{wc{5GWV2Gyf#MuCFxK@GaQOkJq_5?Y zw~^HKVd&EjJh5OYM#FZ!k}oI3toK9Q`%kG?5jfw1cv5-l8yo4~@6Gs{5#lMz>uj{) z?r-s+9Vbv^_{hr2s%ViI+!{~MQiXStxK2~VV{97qwcbD$Vhsu#L7p4i8-q-TNLD;2|casIOs;%Q!ClyQf zV4qXpt7HS z=%|Q>=yy_+5&$mt5Zv^kCqv@VJOG?4ukwXTsJGa%yc;d^BEnL^VhYNM@gU)C^6aAX zI}&EsrPmE3ro=O2f_Vt=i2@ph5VZinM@GNUn&y8LJ)g*OVqr-Vm#Xa~SCm1mgwhYE z0c{MU(Y1Kl#dvHX8}C2 z!iWbzz2*8mmQKiLfNzwWhmaF;yt8%^*Z%49oa?h|VH{KiTw9s}*>ry8P@fRwxKbQ@ zwte?gz5b@-eal2v7&gu77=2py+>zXp8E+TVfkaBPirV*vcFhhhrMxAYm+;DzPTE%f zb7Pmb8VJ3Ep(R}l=}!qp`1fSGio>QKV2&6DBg0>E)hlU75l-j_qJDM-q&+UPB2-Yg*RHS*X? z#NBBigCXB&4zfyhI7*<9`%d>J?m({(jl7M*4ikn_^g_~WwO$t#rmTlDmw?S|kWni! zkQnz=8n1bqzqFyhZ!TkOimVI|PFrL#@)T+?o{ zHo=<_8J*bjCY#Ii!`7Ry2|bQqvcx(t{_U^qpemXPhtHTNFsJjk7(5nt7PC>Je}JYykxC}%qF0`3haR&ZE9M8@-}V!V$Od!wXUIQF6;hLC#D zfEmQn0^N59{^Kxwit|wZ$a@Oi^kQRd@*m;Tb1U|3!XnHdC3uN@$HH@b!0jhLvi)zX z)tnC|j|NnRzfY^uL~~NW$E&-$dw_GddMGBVQJ$MHIhb*ei)XyH7tC%#1{;Q8M2pDw|4RVUZaPeUPubQk7Z8?6iAhv@&?mU@&)ly2}5)3L7v{M8ck3UI4^l z#YEkXVF?!4JrsH4>kB9k)t z-bH=Z-o4Uca}?_Y0?60_r|Ub#nRzM=f@*xdSC2)DB$+wdvj*JqnWrPgEdKN;R1z{2 zbUIwhBc1x^R*aIsN4poiBwcQR1%-1u{)7Kgo@UJj~6e_9Kyn#ZiovAPCfV5;asJyaCJulTA&L(AAptm! zf73+3?=H?KE-`wLltspr=c`D`Z|=`*R5`oab+~i|+Cai5@)g(Qk`)uqfE5VE%6&6M zMU4h9l%f@NXIwucDL8&p#6c%Z;IWlps=q!_9&!hE*;8@Sg>U=8o%;J68YgI*u_Ny8 zXEYc}jISSyx=bzGY2t0fRh~J0T#v5^vC$lw$>Z#9{VBxyOLp?Zl@aEauwB)S5`Z-7 z(NQR-zw=v2NFNuq%ya2=zW)CaDob`x(`Z47ZopP&_o8RIf}RyAf!_<=8V`aR zW?!a}1XtcF{jZfNL#FG5jTYzh3T4=DmH5ZP;0g=7raca8Ou^hR0SgksXUDN=n1Oyl zUH2B}$C`7;S1tWJ9}~w8m_AoS_J2mlT)Xo;9i{pjsU;OEVs1^3`iL-DOaVEf2^(E>y?l7D_Qzx$pKG4t1_R0Kcl)d zrpFKM;`{LRfm0zmaZGPBoa4U~@OR&ds^5?vG~Zy8`rclr>(K=f)D5DO*l>}yq2Y>< ziL1=SEu>6p!u7q9=6CJUqRAS$SGrQst7e%nAL~-d!D=w7HOLu2P$%A`bkb*M(AiM|HGw`|J!^59{Jg(!lOv zLV;MWKGS)2b+*x0^8Nc#j`)3fNmOwTu{xP^Sh0jpI~!ihl}M-_9Nr=VYAN;iLq}%i zmlS3Z=?5IheteP6ekwofYK~E0` zi}I3ydYzZXJfVhv?)h~g`6oI75#*MeHtq(7X0%5&6SVzar?-o_9qa`VekdtZfyWX!Ur*T{?dXg?)K`f}PO)J}-A#)ZpVj zcJ|`H?n|gbXr|mYhV{w5oJLuRzVLO3-CI-nWimEx80yR4eY7R)ooxzuy}t!#5~cp% zy!Y4g1isJ|-cG&5>>dWl6j`yBPu0Jp~0p!}5-? zvMT*J4DAty%}^?E+^7_d?GQfZSt1!1V~)9|*C{9LB48*8>YBpkH7?qbPadUym{u5> z0>UehD&eaBta8>%Kot&VPn14n90`O+=37hr5BH9P(_WkLR6N3ko1stYsIrBg?3sd=$8T!N>C?}jl831U631EIF(r(8O9L66X@ zsTY>3L!nU-s^@~I6GAx=@OM7G1uhO3xx>1mUatrXQ)X=j*4eVZ3_cpPr)W`{B(9vE z7(uyDjqSX{w4T`yNLyUFKqrKs>Z?dYlA4zk7zAr4heHwSPvX0d>qjnfhb7 z5uiHnPXre2bGiyvc6~U|Q`QL^ zSGW?%`(?`3_Esi9u<>)%Hy765#Vciqw_d^l;Ds$3toso~UKSSAB_TWwa@PsK07ttt z92$nzI~BnYL)Q!euWJf{rKw=E)D|)WG(}gAlCj}~DsUFc5ICgGk)rfnOFoe-J&44KK8O>1RlcJD85@MN_`4~B6@P6uZ(Kh0wGT*?aO(K(JacJH z)sNz^>RK0ntEXKxFyPBOMwqvAO$}LY%guUubJdJ@d?m0nkF@CWi1rfxHEEswa&59F zW3xESE~KZ)@>^MOc???eeAG*q`!k1^u_Y9`DP2<40XyOKs7OuLcI#mlrm)beB?uP2r zPTgV!s`#C9>AaoKZJqIZT|y8g8iFXP9c_JmQll`ROZo?w!c6FrSt0vL{RtmSrqzSO zkX&d=`ti663=$@dt_TJRB1I5|sWazE%*UZJ3fcQ?a_1u(?M6!NTGxWry}sQQ=dtsQ zllbZRu@jG&0UC(Gx#FfkkVl^Aw z;VDJvfPg!bl9H3()uJ}RA&H3W2_+hlvUCM`#{xx;_ZmCk49(2X2JOsS56&%4XY1nq z#M-d2^GX(S2$%!6e8mv*Sem#|ckRx*jL~GDdU()&-mCmHi;L}%QgwxPHDcNv2&$5w z1-$Pubu8LOWhoy3BSF zC(bHSQf3#TxJC^xR}C6J2;9q074>V}!+DJmI2xTym_Q`A$j+x)Otq`cH(a&fP=|aT z>u_Dedz)c@Evc}xX-WnrdHu4&qHpQ!6WsO}|F9YVvAov`zF~lnw;LU2T{8dbRl{xG zgZOl_{$oCp1z4OrH?hQp>;81=#mA8P&1PPxJQaI}s6^|xk2km8RCzFW>)*SLIb-HZ zmma&$za>FsgP7u<9$~Q`F~8HFK$r3Q95C=#BQZ@S&kG?FKWj$>{Q)!$vMT;^H<} zx^cd!V@$dj-NG+0F}N$B8b{||BAhg6h@KMPEsK?k^O=SP(YnON*yAlQ=Jf?)xwhP< zlbr@$sqat67~}2HXZ8e$FOssu_X{M8qZ1Spb+7}s2Wpd;aw2F?JNHX{#5-e(lslnT zogbR(a01-yfU-u6W*P7;8D6V|kxgyCh^$S0V7igVwkqTG6#5Jcy32}}OQ!MlxD4iB zAxV?pKvvN&p1K*@-;05XBMa0t@G$dMgpEyCD(XtjCLiETdwE_*rFus%&Lbh*oW18- zc{r1e?urjFaxNvh9(-V~*3SWE=$*^zUzwIeeky}-=PfVw^oe6|*QPIE$`v9>Fjv{-zkpqiH|fRnV+n%IJK7es77t^(ggh}h zy)jm8&a$hll@^-y!~E>zF=ts@8djN{1$%^<@X0q6Y|t9<)#oetEI)A7TJ}ux2AFn^ z_h90%ZnUx^(UsvfaDQ}=qJ?fPZm&ObePLk1n>_njxr~dS)$d6&^|WJZ76zu46R>6* zKVycvkwGgb=wf!%AQDtWP>*DSrx?FX?&tnyAK)sr4Whw|zE?k=zqkJr1sDu15?D;o zFuM0!IFL29g}OSHjI3;+4u>w^-7DYp7=8Uu(M1Y&%tWzlEqXo*Z7y$yFn_XbeT(W) zCj%p?9GB!-^oPc zt^#7Zm9`OFB%WtE65~=b?I`n+6*z06E5b$o(pVx{kZzC1biMz$AT;j?zu%_DcRz#b zy`b4%wCVLue;WO$)_@T%zIVP-J%6qD(cB zv0-=O8>RZ%THu@zhtWuuo;x8vGL-ZoyAg3qeml$q&OOf39;$O^Qn1i;pFqD)N;-Yg zQ_4DidgeP?Ob`tP4baW}@!#d;PC34^5b{E-1=^$-D?V-b@k-s{fzY9unh;`MhegdV zJ8|fspwnwMl$ysSV4<>X%XQb~Gw*E6;ChXJ<`v8S-KddiN|4#@YNLQ$vZk>VgM1-4 zak0QF$h~2)~r`o}Mej0%iS2N#F1aS9X^N(_+w&4d-|-;57cxZ-f@Lp*1NoIXo%s1 zOxf=BYm>3% zS&4qxqKb9z)T_FNLX~=Rr%JbtroaSM-Ol`DnVIO%x1zGnM7we!0P)x zNumTo8zii)S`24CDIl7pW$1xt({EnCO&K#)Mg~ojy||NG%*xI_*`a?J+>t@tcGt`b zKBtRSi_=RcBj4Ndo_zOnd0Aj!RCq&%ez}Gs|!3)H$xh^y$upYq#I>QjEYir&DRg zprMMhNl~!A(@qp;i_b=1P^jFrCo{BhD{<9 zYElu)!N#W!_J5i>wMq=apfUrnpf?9iZz-iQv zdB?M5^ishoZP~v5TG#YpR?9?IJh1Kz$kW>#&*1k*R6_BPDti zVBiBR#@eBB1PS5rU&L3`w%&ac#O7*XLQUR>idjr_wf7i814IkNQFSc}JM|l@?N27I zGf{KUTjm?%4uNYT7LzXjP&_LhZpVFCg$y1Jh*;qRGZvnxOG^!yHA)B#Cm_b}P7Wep zByIX$4MG*4VlKH*%V^1tJ$_adg0pq-@Bs`T*)zey@ZWWqL-{-LIX2~zf)Ju+HRO=^ zzxy=0S;P3dV9Q8WxWFoC!e&0yW-gvlpFAU)pH!wMFVU||Pad7hB=Ec3RR(Y9#8hBj zrT=2PYUkeVT`_E$_w-f!G=2q4rwuJF_>`2C0W;hmkAG5FoF_fc&HeUm-ml%e z+G5}3>PMDrQJZ%*Z6Ct#zd%fj#N&3TReikS>V(pqdd`7u2HYw2{#Ds%Sk7^|pt*=X zu{#k1Ur>v@k55||eoIc}XD7zh&`Hz*o&*e9ud5s3>J!8U#;=4hIrXoi&NcUEswVfC zGob|#1lX3b(sE2BJ$PqM0YG9h5=a;9R*+MaWb+#yUq#wrlKDS#{N+hP2u$r zhPu~rR=2A^m;5cV7dgB(KI(|AKc-$p@iN|C9OLxv#DfoZ!quQ9jU{*}fjkg)w%T#e z60DVvpU@|#*n&2oPWAUh(AOdbXNd0T>ageT=?{Y5^eR0&k->jZq(5A}h(BPycRX!V z)wUgLkgQO)r_v@HTI2M=r&{gjB-~yxm?k%rkC%#!92Oo_*8rSB`CUqd+^Hrqed*qX zn6#*I=&O@w1m#i|6)w9nFEZqQR47a_E)CUevcgPiG@uDXV9CinQyH|^g#=0F=WFd{ zWs&nQRQJlTLH&Yk5zwQi)hvu~=2(h2t5Zt)p09*y?S|K{%up8;V#w!6d*G}E*~kMj zBqh#e`OFZv=Ti>iP@QOpm2mL1naG_3c{XQ`MA}js2LzIY5~5W$y%gBuTg_&}_9EnF zm{(t@T;?!ME->YdT11kiJQc(VXR->1d#8@RzmpsAWLd^LO{g(tBpTW$vjgsZy{vkt zPkdx^clSl%kabya5|MHB%Z~`m&I~~x&xkbf#^eWJ$*>=VvlY5*X7RdO4|+)nrlw<9 zTU!@;Lz3FRwo7yxFmjfbmtR`kr+pBlYyf*J_K$^oXO{He#CwnR->4hKr@!6w{t-m7 zABFux@Fu{dXB`6Qs37kPSgc8r;X%mK+0Kc7xDFgSkg#5*91Cd?yJqs45Eyq8Fw{7;{kY|K^4Jabv671qA(b{ACX@Ly-CJuiGbA68g zJ0n3X|0TadW?KJe?!<;>kA-hH2gKs5W+UiVlb?FSIMUwBk4Vo5>a=2Si4k zTX;T=&>w96bV9j%eQZE)GT46ZLu2@iyfv9q$1|C~09Sa8Y-&NWQ{uP;(~{=InMdK< z13N*JyD9T3^0_m$U1ZRbtxe3eGZH9QK564GDYeY>kGh90jAk3n(T|xlx!m5IPJcXTl@*Pxmd+xHDrrjC(k57XrJA~(aIXP!w4wfDIS z4Val)k==pffE`~D2ELXwP*}Rne8H36<9<=G!R>Q==&qW6>^G2$amIx{gHF0`1$p4fCx5d!NV&^Ox%eBDIAJjW`-CCrdb=ek z)C1En<-tJY)~-XB>*V*R$HGyiWhn-0EhcB?-eY8M}V>^x|nOeL(bPlNRQJQ9FVqe$A(5E z105c{9%V4XZX~ejG!3JXvrx|35uskTKM@SXqB|@a?Ry%xIFt!upZ!l1%Mkz|M_-G~ z!UUuTkU$VEIXbAn{&pyZ5&69fClIwX=HF*Kj@78>AHs}w0+;0pVST*h66*clXx|lA zM!5wkz5b5sjW@hfoM||J!uDjMjl23dHq9mn_#N!HOL;SgVd15khY43n!6;x+4cXWQKdT#O~b7xfZdo-SJl_tx) zV%1B6R?eZyiI%c4Q~dVs#5w2Z67nzB+DAqSq9f0yo>RA5UofJGm-H~Ez(=O_DE3cu z&2$J-@2|lw&Y4<}&V;p#aPLy7vrTzIb;|0yYiqP+?K34n8%g@)dm$6A?2I zLN9J!lGtbRyn!)N=@VchyXkI|pVeHFQv)8V`t zL+OFvOKGD7oWw^B@Jvk$t$**5f)EAJ*UNZSxXK8^z)^<`5BXc}w6K-AbV)KmbP!!F)2){)G9tA4`iJx`xi`rac2z?OOrsKth&6GR2$RV#NFx7oVWAiXo^&Xn+d%rJV zjKV!@9S(8NA@wvwF2#=z*l~92VpSwHHbR{BTJtzBl-ldYY^1;OPwh-TFY6?Nl9-x( zZ8jxy!U{?vlu+{;m00Gqt2h7-T~)U+oVgGvi9 z4m}S7K?tmrbW@A`onc`r8m`bl0-M|QBeefOXn9J;LXl-YIS4U%H3`^O;!L5H z2z~O|=SK90e!8%Uv4Rc@-n+1mk0)x>RQn>9v|rud3ui$?&0D?y7C{?06bmg#-cUg2 z@`OjFGN{v|<+H|l=lhASaLRAAT zV47Ycz=WLkjhRBl>33IGl~QdT^@#0MUphP)mf_TrFktUS;)kY6$ak`s{SxVW`Q@M;_)#9x~!YoN#$$ zHCusf&%c@;D5*d5^DG4jz?@CH$f5j?P+3?^GSHV|8dv?Lx?81@un^Z?LY(Z8Q+=%B z)rbeJnS0^2n}n3&z7N`;r;yQ<&$&FLHqImge#|1=v#1&uy_09{&`g+zs zF;h&N|NFPWdj(EryE*Rr-++tNHmg)i%tclpQg;4|%{!svVwAxsinPZlo8>lQ7psW1 zI$bm|6rHC2j-H7zV}Y|~Q^a|KBvVrc5k9zP@Us#L?-yp|Kq4ykZ$=d6z8(@f zKiwRkon6F#afXcuco&jTv6V;v;7I?~N4~Pxd~vt%Zit7X@8KiZD^MG#G_|tA_0+jq z$7y7K%yZ?`&@)D(##e8gik?fMN_&fI53>*W-*g}zu!ux+$EdiwW$^0fWuDIZg(i~x+T#G8xE(R8>M!ji zIPy;%!puo*jeGy|m9zh2AsdY_ASqp_-`&ZElXWu3~& z`!nq8h=VrdtJ{wAOC!gp`PmDSqP$A7r}y$dD_09(`K!H>C z_h|O*OLU4$s~=Q%*9IrU7Q!@MD?V&!U9kW1i`Cia{kIxM4i6$f#_|yP57&hZXpkTs zfp)Dt8JqE+O$&YpWco2LOlWQm7LlF6-D;%JVSaN1Z0Ts-sgq35E0k?Y^pUpSJH%16 zC*K+Ryb`*kdPIj$>I{1x5H`$^&W*&w51BY4xIAo3G18X!d?3l0PYLnd?pxh(197SW zkYDe_Q27FsSV_xiAXMemFS)v_$zGTz_w?+I1=&jm#iw1TUPn2nq)mZecafnoI^lAZ zG12^s4UNq~(pH77@~lrocYAc*EC!s%pWWtF(dk!M-XF2_^ST9-p#-4P>i(j|ooDUI zq!-pxM2W+r9rXFzqf`;&5_&ZLY{aV&B7W3h59Al+OkX;z7{raA2RFIRN)C*>+SNWB zf<UUWg`u=Lt}FPbc=( z#;YDkq!3WO-hy#=k-o|>ZPiHLz9*s3A3~bz_}Z5JnRb%KC!N7c}Vy2DpJE4CzuxuDSxLK8K(`$#4=Utnlh* zVrP?Sv-k6)zt8Iz-h+u}w0%moQ_xkYsedkI_fW&87E$2>$0Sew5AMRMh$Is_@vGrg z9__iqZZ^B%z_AeE6bow9-GCZS8t*v^!(w~p?I(nJTqoL1%NjhAR+0$%c6w9nl>{N?y`(=!*&DO)c|}7+Pex zsZrSj>oXq@?1Q8OT*bLG$nEQ;B&_QfnM0J^TE1p{+N;p1LTXthUEXfS``z-f=iq+p zu-3QRbrSUpGpVT|uQn<&5rxTI+nIxWU$8die~g5GBhELDTb2~F_LJ>FbGRv#9Kc25 z|ALxq3aAMS0T}#4shEh^-*@bQB$V8^Iw%VzS%qnRW5b9Lg8!JAIoLTN_i*Dr#NtDC zZZ7lV1u*{Iel60|W)g+*=h~VYaF1jkm{;t&=Bu#pi02`h zD2(EZxB!e!5=8gHuc}?6u}`1lBRG8*@PyEq%&|M3a<+td8At(lsoQAe?_Tt6p2o*| z{*nlses`dvX?()nt~MhJ zW2V+cLF}{Q#GO}t@VnYd`M?L7A~vQ*Bz_F;Oim$J=zvd;^z^WvvaYBIU!K6IJUS4` zFEFdG>elxrP_l&dx9V=s0yu!jF_}-L0AzGD&kKdw%hQKZi8^%`(q}>?SJkHDK^I=CtP{a6d;}u(u)(91WF!uv0 zh!ZAN4~$n*qZYl`;pqiRBoNYrN3<#Mh=>Tf@n#Cr0XQK&D#8OF-T|8%o76HyYFD9Y z;Ps-`H)PF_{H233dtXJ!gA(*aA5HB5Yp%?!*<)4;hmVFLtx~8S2ek^7?5MJB!(9qk zuKL^hx|xS7;XtxoKh#{`8?l+bxd`JSh`Dv+z_?{iTHaS!&AOm9+FaU!Z(33%8Ls1U z8RbMo`p8UtTBMRjrg@w+j~m<1)7;}a69V?32p@$I=u`OXS36_cRZR5#wX3*L7u03j z*W(1(h=1Y}h6+domL2@;D>S~t$V3V1iFJS@yCMIv(+ac`D}>vFqG3>7CCbfbDn56Qu|df|4K3*6Rnu8Qv}! z^stYZ$_`B^v~cssM%lio80h3LL9lp}W5IOeB@B%I??@3kY}I)X8J0;LUtNc#Vut9X zq;=A$R>w$(wyy}ZlsZ=h?saQDdbJnwCa7dzb9UfBzy82LWZ`g^F8}PL(?m<%M(daJ zuVB~~P2mD6;7#6y31J8D@z2)4ao(q-adtfb`g1nf@CB`$3Vv!QJeBy@D1pVWuaz=ojeqwBzR2VGp441owB9&+N& zDd1Y>%9g6vPv!l%w2QC3AM^nqh|&g4Nd#5}map+$dT8}sNQnCJRGjlF@Wg`nlHn|0pNUm?397XaAnEVe4D120G)YFh#pW?w z6ld?JFAKj=N*0FMYOww-nDJvQd?+Evi3W`DWB6$!81J^tM&GOo22tw71H>e%oWkY zC1Gp#9_BoY`W`M#=?+ysyob}Y<=9*KGIN08oiy_9d2Rp1&uCRAT<5>vizN^vA;Fsn zB9N4BhS|^HNi8uT3XYHSUq^LDi{L<1Y*911V1(@&C<(yMXlq3$r`$$$+O?}(*IBn0 z^*V}MBm%jj&b0Wr%FQzuGre%zW2W|k)tgAFCifZ&Ct(mN& zR01p}1LE2x{I4d2OaN!?vM`Um@$8_-{o)U1Ld)15b(Hxy*yis`K;?Wv1s4e*IAbjc z5d9&W{D7y28*!l*ZW$E57T{7==3u_qz@6kB3E|iJ5r$BpuMyFw4=M9}QhS}rUg3p* z!hK+wmxG0Gs}$KN;E@MRe#hJd1=<0OQaAp&D?lIOPqt9P_7Eo_cyw_*mI*t~k1uH7 zRfKf-j70+p84R<#^{hU(Eixz7hg>*Od%i>hdZKS;CZIE}^KJpc_7?JF%VqUBYY^#> zDC|*4aR>a&TuogY(FyD^C<+^1%*L4AO(h+El~h%p*mK%ARY5;&{5QOZ@8j+2f-Zpk zwI4?9hLXI_;mA&CR*AQnB+yXTfbmGm3(1~jTK*l97E9?{bcxsflyE? zH983=i<~N0;-}M=V&Bhn`yz4fIRIzy<&b()iNtoN(gMZj#SJboo_6wrFIz12tkPg5 zaj_{u@0Zaa^S-~P{nkHeruXTi#&b$g!Ow(+wQ8+!$MuHHxiKg-kX)GgkqWncv}0M% zyN`d2meQc^m< zU~ykjxp<*{211#V5*tYBF<-kvC1?<|zb+Pvn1P!D+{Eg?El=ZnKHFO~;@FiUIIre* zA4uxt6%+!FD{{sLriVjR3l&OvGhKUzEy~!McAX<3+?{+M{L&W`Y6i{9GRj?Xbq?b! z?xi>^+@uA36$Sf~Hk0r@QAObS1t0ZCFe$@+grKFV z8;0nGmrT7==~0`I=vVLs@2H2JS1`0W=VZzkn*##`c&wK?a zPicj5Zo>@*V&XZxp8AZbW5kptPopMyz3hFNUZLAd%BX%~jWmY`8_BMh^*fh+;jgXY zUd@+BP5Vh{wuf(>MxzjZ^3$g50dHFf`d(}>0rw$Ri&O1DSIaEyZ1Gj$>O%{sg53&mieXl%vbRNMr$ugdwu7&XwvNGpNe_rXuKAj=4vCoPSLuZ;RFul6h}hv zHeVcE>katHjl;U2u$qzHnGqpI8elkcye9W4Pu_mMWu+v{V3R7p*EKnRY`u8c#4%n&<86O^X z6xQO9>Pb^F45FSQQNs82Mb6(W!lmOblPO>R&8aBH6yw6L(4{o>2 z-;@!k^muk8My{hkx-hU|7Is}0TsMZ;67Hvw0na|*98O|UbKAI#+&lleY)OC4+W9yp zE-CUQ*w^ii$1sCySr-%CF_SpHV7$oq?i9@ojTTkOg*(J3bcs7`O#ERFXCOkwR55ZQsyt4_VbGtT2t^R<4Hf1OM91s{`1y%FVICBn zk^PnYMNQxV*zn~u>H+?BY7hyYR)`Rxkx-@NC$E55-Yr1-XW1oFK)n4Q?^XNb;j z>lyvoQ5(0Yw|ux~Tknd7+9{2QCqr%oY7@D}L(0p+-v)ppF7)SPqGeK4TZ*SkPMnev zG|=^+4!sk^aCas(@0?Yi4uMKWTq8fy^O)&95e4&1cy(a~cFNS&!cmM~ZWQtA7G;Zn zd{p`mTdTu;XOkJ9K6F*R82N_{&VE{jEJ%ZAz$Ij&0|V20na>AFav;3mPtNJu|6ok?l3nJr^`;j% z^{etX%TOPc&6#=wb!*>Mb+>&o1m!KVh})Fv{}&BV@ZHzS6!zNo;*qUPoIYEXH5;o0 zO>FHc?7AHt&YDgo7BMQ7WoNjaiRcy2J%(@SB}4u1-vjit+x2x+k0@q>@J?Rl7#d!@ z*gklc$<>G~$2NPPpdI?l7DJOEnr_f1XYFV4MQi_0MCS4-havt^$DXH$xV_5J>EZ2~ zgbDUYF(PoMgP-8BqprDCH_+)W_oj1GlCj*@xplu0hkTS%HM3_*@E(Mzb0%%yfHSU^ zOBF7VPi#G64~iY(ni-9Clz1&ewfga=(b+HF|>_ZDLbpEM8D!$vY{5u4iXHTU5*BU&(-29>!vOZ;PxAwXh`e!+|kDtb# zt4B^>AJ4AF1f2&vT#VZAf&BN#dq{a-#k@E2CBr)ssCIr4us5%~%gx2$FPm_fPI+&1 zVj*kN2 zWYx>vnt}fBP`AgWaqRu&0jbJ!(E%17RKtEhXkjgNIX>tCT@?DLdn*=BwaBs!DLSpc z#Z{dLj0qD`6zE2DKe7h9_A@k{k#dAeQZn9UY$aLD80Ig^aX8h4^XN>8+{skt;rZEh~s@KV02hF;)h^5CiN zBpZo1Y;fpwA&(^7y=;#8B`4F+Y@mlJfyqz;6ZVxzRdO0a@7p5j zFsgVdK7c-6WzcDHsgX6nbp{TDbCEw^6;t$@l^JifDO(K6zkNIEADkKEgzg1CUDXAf z4jg_3o_%d>7xM;Sqe0GB)H_uq<*hzy`AU{@@HR2_X4UtrS!~S zZq7Zi?ojM|fBCzQgWPL4z1iSbI-Y_gY^2cDYVfR3Kd81B{{oPQK2QHq#=ooDaYBUr zE<6<11O&R){w)N{7ktZ;T0-A>r)wXa3-@;W@Nd1uH3j!RNVBZdizNs;sTER>t@La& zL72A^wYW>dD1j`E&`zm}_MdR+FMAa$>2^3q?mCe8kNxc>kNH&i6t=nUxul-inAbQE zUZnb9Aw!`~NpZ%0!gD4oT$s1Y{W0tBRezJ=8&=lbhJ9ZaBr~Q`(^OctB7~f_FE5`7 zv}?QLG*BY`6un)thH$`86A13n+|vEtS7~D1alqf3B}2gMmPA}TjYsm6Wn(N9Qn)I8lp7~Qu4kudELSbe%J2(8dlp^RM7wHOoSa1?!O0B`A(YtC`ht^@I z&n+Vp0>b_(Z)38aiyhED-H^JH7U3S=5b5C|JPJN=Vb)H@uINLIpCBK7cfx?r;|~lm zM0_s9^C$?k(o)yu%*^znQpF4nOI4DIOg>+;hqnswTcI4kEvmCN; z;6!w9jr#9NdBhN*^ir+0F4RXoOa8eK8D*Sh^+wOBv+uXm(l;Hx8nQi_5eohYaJI-j zeIJ(@zUK@1=U@3*!6Sd!_udfwxRci?VgQkYp+&%v;CgL-O9M+~I%fHA-a#APkUoS; z4wYPVM3unuzlzAn=ab$TVJ==%$s~V+@y%Db5gxuZ=Do{zi>^S3ghSvqmIq#q zzclTyY~EQjwoH2SK$I6`Z$g{N;HIieC6{&&2v@TZIBi92ZfYEv8V)&Mj-&lCpV_&1 z)bJ95cJ1@U%2S+Cn7P`o^PIS}fS-)~Dl>eYH23W8s}-Y7+Z2{TQhVg($E7Ak+2k%wnBL2WEH;ys1g)*DFd7f` zF5s*UqY4F*9!U9X@8=kZ1KBXCWFat;GA{0_jh*zuT!1ek$hU{*qwVNhxedXyK*X4W zKqwvk3)8{5hSaGTm(&$~F|_}|wh~^M>~DFe#OnnJxniWwJ%8 z1k}gl2{+caxGz0RQFFNVyOuibHO#cHOnJ$~j?s?AXvVZRhuFNv*K2mpXggUC1_Zq= z#|6C8agh=&h5I+u`iG~&?&mzufEY?Es=a&>?%xR8tH!a$TaV;xEjyUu=X)1fVm@1^ z6?9$^btX8+Gq)Iz=exFPP;N6;Yx^wi5wQzhe#4kL)84s%fcwS~GF>0SiyG)_X5bv) zDos66`*Ma0*%SplE4pUvC`3RP^7uv<0zxXjQo`9(I=;NIgW#)CH{-XT0&aj60kehs z8b=KuX;s4ww=z`C1=o#&NSl4cgyVph`}zHBD1k;n6Rfzc%?<#im>B5 zBarpx0W(?xt{qWY&iH0g(jb=@PiSN>2c^%&YO+FnFFXgfyAs?M69XS`g|QC6n9_U* z&&ISXkK%M44!mZte+WTykk^hp2S!JOgQ%hOHt_~wKXE_jSa+UdU|KAZ`P~p$^m>nK z{>?=jUIPC2>`l{cMINjRd~VtL+y5_xPljK4vH}!;l3Wt|>q}kSAJj2&%#AW&?YN0^ zO~(E7&sXFQJiyc*PF*<#-4`g$!tWXv0X|7Asy9QMudsB+e_K*4E> z1w7gCnK!bl8-aH|z!me-`;w2q+XWqs#h9|#y-FI7m zEd+2DYP}tpZv}5%(g+@>VArTd@r`ixru~rd($t(?NSHClIhvcVd);INaaVE5p4h5f zZ#w}rshMao>mVZGa$TV`^ZO{w#(tM-qKBST$=eTc@PwC5pYu4U-ZO{qe~NF8PC`v{ zLRjgee49TMEM9Xg_f9aW7qWIL$rxEeR_jiE;=6|pvw#PO4;tpYZ)2piBg%+W9x?3C z8JfkK?^-0>b2%EHj$i1?wfuf|O|Y6J<$S;6DQW$8uilTEv;5YGQLl{n?flB(oe>`> zf~cSdI(orPo!J&qC8hAWS!OP@~;k^8_Oji`0380`itnfGKP zaY;x4jqx9m{JTpj$#6+3#U&1ph48~Ve2>hue&lJD=AuCpl?Jx{Hm;J(U8KQ->4Wg{ zJ_mY3Nh@k;QimyI>?B_|A6E}m7P*mbdeTUMM_yODPL!`DF?#!Cc5Lq+&Sf$~MtW8U zc^``bY7DyX3-jXHfj_}dgzNUVH_8kj+upQt5DEJc02t;gdcqfT6Kse8t6*k0y(`6| zJ5c@%z`HuT`t7+IU%W`dSJKa=NBY;Hx%y`$nd0^>{2~OW!|NvJ$=hSJk+}E}-0GXt zSkf2NCMq$c2#`<1RZ3lggUw$(d9Kv{VgC{HI6S zx}7*@ZeNZia59RrlA^W_13T{PBQlnLZk%p$`Tx!iu)8GSnsV?!|kTmg&F=XmJjL z4Q2uh^^!j3l~bdsw{g}FLV3*pP$&WHDCfuRER zWUo3)C|aqN&^bgz1RE?10i#I9?wmMdk7@fMTjog$m-Q^U@^x)4c9O3{+KpIKz7;Wi1fC?!Lp z%VeL&>A9^OmF3CMJ;9fYAs)hXAdxu(cysdt!I{$+vOIiHqEpNpB9?7x9A|-fW2j}e z$l^%GTjRy0<@1TI?}+QhJgc+B@XzXd4`%lwBEAS6ecD@|s4DEvul<&Jp>UXI;T$T{ zC&PSvY*u^P!Ab83%gN4JIDhU-5?R`fZ*9@}*@xOhMh%c0vJQRMd1!Jutr zfNxODGsvaVaUvz36zR`chM)Nx1!RUdL3&_^(M;4z@kD_yeZ{ zTT&JIB`pNY)vI!F;#&k6kG!v0)i454QZg5gOGXZqPDh90Ezd6c)B5`i;smTzrIz8W z*<5eBk%%;=n0`t<(CGgS7TGT#6=Y`eZy|Vc~~@G*nc9`!yOJDa%z~h)`CKJTE<0 zR4D8F{Wf67JK*1<;N)hp&zXs9i^Z%i7p|5~)3t|um$Taw{6?%1cS{m%e-$oIVEwo1 zfdpC5S>{WEArqDPCW^bPzlFBwVv$_zbY35B%%i4W>bW%4CQ$SoonCIjZQ zvr4l&qshq;Q1%_`*F2>QNdrgkuL`dzA+L3Y@tSJmx3bjO4*Px2=WV7Eg`?8!Dom#0 zSeKItX#4Ty@MpKHgA0y54?~|}_AATIp_dyZc+KseteN#7qzj$(&?QYLry{5QMq^@o zn6r1!-1fLppK|idx|B~aXvntKmZ2B^@?oja{vi!`{*(CW>1($+dbdf$8h3+t>RBa{ ztgKx-yMmv4e1jlDP?G_TP~Vta+ zSblzLc=+MCuAe_RAZWV392EXf0nBx^zA$1 zyKs#u4TS5o#JJE${9XNTo+w2R%ej#Q)as;o3b!a7?XM*yeb~YdcmE6xakJp-Dq_?P z!7qJD1fTYh(0>b@MH@Lt2~I}9ddohutBn7^3G7=@MP^>Pd2&SPU);BF5m+GBmq6Ew>@+N-RF-_(~W&Zj?efGw9bIF>^ z3WJliO-~J`6p(La_2R5ibnb810l74@d>VQ|W>E3UnB(}>j z?RkH;CbG#tUsKXK2{ODC@-nxbM7%2!E9kRy>`nS>C;~X=Gllm@clM^k=#WuGd{v3V z3E9lpiEnV&~(-kWkSf5s+a=(UW+D3mgiN?1%6kb^5R9lsK+L$4de z!XfW@c%t<6{c$Ru7_8yMT+i`nFq>Il%|E=!c(sHnjeOhAx)HeBbvPVq?p5irG2Bsm z&M6&w*+=G*0qzSUTit3s8nJ7>m_Df^4-!mlS<%Cg9vm8i;%$;sSf76B#_8usK1fMP z;Wh~DT52MNEq7J!4W+g3hK>c2atku|GWPTT`BGQ)sP{XTUGL2}RwK>NJ9W7qiZm_Y za~RR1?{4sC{)!*o`+2`K;1-Z*lTfk_3_=l1^K@Q@UWQunRkdZ5X9_>(34c_AG>S$g z+(4rdf3?&NRH3gsBp{$bz@zrE0>p-qZ8!E6?+e= zIHCLnRR^4N+Fl-6%?zgX(j&BZ)Sfc;ttTq8C=EPLj*^N#-HNmUa)Aj*V&3948B~L@j+hV z6@mlub4f`Wv;d=WqxhHC%f`J@(p$|6AReCpwsfdJBKUzc6S9??r$qvIhP%W=Sil|QP z6L^lqruy2r;5UYWJ-W)su-qe75L+ZFhMvRsRs`}CcK3xbrvC9kBoBthW!niOB7&Vg zM~24PZFArlom3DT_picld5u3$X8a!xZKg>k?!ymxpVJPTHUCP3Q-)$=&mE}{Z<(5! zre|d_tWm-JDVPEg0MXh)ig3OPekb)9sDpZ3q{VHA1?#r5yJdd#vPuNhG2 zduj9H)Ol>+eP?DF4FC17l83hLs#G($kg~q@MR}x7n&8mT(;z=`slA7{#JHas#^k|* zfw5xpYxXdH-uti0HU?IXE!uq9$*s6oV^*9NHuGXFcGifETpRz&rG`e7c*3BK17X%gkpr}THF`23ZoYOew~QuxNpzFt+Pr530E%8LU~T+_q? z&nLuds6#92#26gJ3wS9+lUmCLMQoDO90e z^}AOK*H{eu1M}~N#QuH<95+rDF^_{4r;XXM3hnJxDJ?7O85fvDqgP?tu3m|)T=9p? zn_HPxPTeo?Deen_UYuAPzh0^utGw!MHfcO~a!Y&TLx3nATrd7b?gm!5*a18KDhJ4a zt~Hy0?v`P9O7fSVAjh8z_KFo#6PGO!zQ}MZ;AI7>TD~j5vjr$g6A|gy55R07u8Az# z^o*YmJiSe+!SXj}56t;~du(NZCo1JUE}2XkGaXdkC`sc{n7zVMw?#8|=& zU~5s}=Ho5*lgQ66>MRJ7>8kZ!n=VSGz^&-!3~E}y%xWn3Tl9#oJ5yl>@Hy_<6R&$2 zGYi-=MJVe7yW?F++CA}ZdQn}EJNZpGaqm*rV4ACJzfZrP@CJ4gJ^{vCLT7w8T7ejW z?|$9^zI2>HtS=j03VZQF!ygR|-rlo47U4Pi%TAUKEn&w5P^qy`qaZl?9Q*52j-IiW zmdi4BN1G_NLt6PK<`l6xiEEuqCtyc&>2 z8I|R@9-5`~0I()n%&X_qZOxx?|Ay?FH87L+rQXliY+6&^?)c+AK}7 zl@v|x@H}wm0RH1DCqIqJJUN4!o7aAH?eDB*>ATomVFFF!SEqyX^6=8Vn)API{V|C&KOXW6s@!;2)ArJ%z|$bznID+CmJGLm2>QRflR*3Oqa*?`F>%jm z?KevH_Vp5*Nb4>?i^A;pm;**@wQ-`k{{)3y zxXz8nG=rnI#c3{3C|i~0r&TP|d$!&U2;*q#9uRr;1U_1DnUlZliUd07%q%R8a|V|v zGU8K}3oHU15IXJJZ8(bZXK(9CJm z)-P)=O6CvFFC5m?Sl2(Bt2{3>MG>lne~Qi=S-Ic4UbYeA(#(D!{ALokN>E2twT{en z?$m8n*&(>kB-IN;w0>Xsa+qnSv6GmUb|P2F(vs!p&!2x@)kg9lCicP)*M`*Jkvb(V z7c-?c1Wx-BP44AY;bgAW0nHC%3wN#i)~CxqM2j-sAtEG=vfaTS_)3y0G>(&Wd-_`m z&`mZJkoLY605hbcA{kutnB?wkz7LmtUFZ1d{brR8*JpuV=?2J4gj%xgjmEX=YD+2` z#zJ%!TU;TZS>og$qP$(X2M3kks?nC_Z4TX)p)}Z7{KQJpDVhlOp}4jjeOWAQo>X|y z4R~8k!Q|WZ&ZlgIy}pHR(pfSqa0Wf@Um@ifpq!z(g|u_N?ei6gjFP<6Gux<<#vARL zJHP#h*EFN0)D^Zmf_agGcfQ2HMFDJw8GXyj%~$veSMZ+Mlg@JY8v=q=hr@+wgUi1& z71gB}$Dwal9i9u3&_DQ?GXDW|-3W;+;3y5FM;>prHjL3mJlyKRevGC_vI2jG2XF8x zP8P;`Jq7-*UxvjR$KJfUy6PSa+#l3rBM>!nKzEFi!HyJ-ltLqMC|P-2M`PR)>pfF{ z-6l5J6y(6-y;YwvhqpYuqqaYU0I>DDbV85yAK&p?2Fxt|jC~R5<`=YUw;gQ1wC0R? zy46nW52;VSBQ93Hr_J2%nXh%9>WGGjq)i0QO{Z$-l!st|wY}+{PQEG5V1EQm5)bje zFvnQkUQc#3^(*CeNkRp!2m4B_x{fx zvi+LX)9KeqMZ%ooxL2CV|Cw9_T_qPKq@}kxz1dZA@jZC-F1euX#q{1NmYg$nqDa55 z@<#V@r?p^PRqHr+A06dtRkIHlo)Fg`FlGQ0y6SYOtPT3=;B7w6M0v(yhVA&1=WD4` z)#U0b`F+e%7gwW}wZn?vU0ng}qUZr4$a->1WCG76D119XLoc;(JWa`+`>|EuI=N%` zXCSa(6h+`POiV<;lzv!Lxl26GKv+ASskIgD9?s{ocowec`gu5XFr&#*{rq_es#N|2 zjR(dKp=CQZcB^}0B20iW7W37sS6mMwq%MjaeK8IJzg1UPf62JvB(%F!4@aLogr+sD zeM0}qph+(5Bxa%U!98oYKWAI^+avC}{P`G8+nXjGFbfEj0(9GtZ$=)XivSx|9R0D> zbZZMN7&s!YPZyv2t{H3)&V6V5@l6Vv`vR#*Ru%+4-4N9o7#V!- zsDr4f3!#@}rNcV-czeb=wS)20jaXXov* zk7b#PtegBGJVtsi<-p1Dg_;^tzE)pw<6D&z*-%*3X#CU^+D2l<3%!@`h`ZuV{%jrJ zt9VL@N}JiHO_BT>$JbPhDvDT&nhTkOMe?)M$7~ zlg@nW%`QiQzmlNffWLA0vkBXw4MQTFS`H?(^YdB9i1fTNT$$$PrzP*aKdoKz@|GCk z3b8xTmocd zPFr#$1EQews)t@5Sg}DdPlAxWT>DUGYLB3P9Ub6_uPz4yG)#Bx-F^uG|4QqF^seV4 zK!jK7pb&x2N;e6UKMKQcCh4(ZU!DlOlSdZCNxtVBiryQWxR*Smxa4aP*dYrXr7Mn2 zO1oHbfTK4^g5~ej_IYYON*v*PpcjLdsJxiDthypOD1Di%#ZZqVi0R zftKlGJ2np9;l&DGHzU4!03Dg0{IR*&C^YZ#Xx`#IK?=9k0JJ1=!RrY1drzOVr_O`8 zkMYeLN5&!UY1tuOai?@#Y$%-OeW?+lst|ZR9CJhRkQ+y{FtXEKhg_CgO0B{^p0RuC z|5;97spscSXe@x)seeRD@2(afT`H1HSh><(-xA#w_-yV48#8BfkvOLcdjw5M z=lS@6$9%5jFZlci<3DU13cm7f$?9i)BUo?V?d?F|USh|mG^*OKBgUyJ8r;vy4)}c( z4{`&MpQ111Vowj`Sso1=BW3?MfAw4b1;=rxF=lGWLtV-z=|pRbG{Y2J484X?axd$7 z`fLid=Q~h0SRuzWtVLF>K=Z9Ju&GIUacPfNgrX+UbNX>4baitx`QUJiit4lm?nP4$ z-GKCXu>HV|9-I#5UFmKAHRd6`dIZepMtO-^(%!w=<&_Kg9>0ydG5Ch>0AeH%6Hwea zKnsh17}e|3^WJvo-&b*Qw3)_v!8XS&d1cu48Xj)E52ETYJ=V|H*B`%(BWZt~1rhpa zI=(+u(<|0V`g;uW6RefG5A)L1m5HP}=(Gh=*4m*4X9P1o`S zPA@4M;#s1I@1;AL_*eV$ygnc53h2SfKw^@xPbP}D+NP5+a9~N2*4TA%Qo?AH9dj3$ zv7ZB57C3{fb6a>lypkZ9zO=d1DB+v`TXufS%Ef=*sya<;f!BjcRCzu+AdYCywAxmBH%@%xZ z#v+mAa!3G4Tu=bBcR%<(+)_}6 zb3m*v+Vuiq-{P|=P?KD#4(}4&`S6A){m}sNob}oJ+*_~s`5jh>+7bh?tyQYqLq5hT z^iPfXgc=j@6|Z*sf&le{uS*~od^}Y1yz>zey`07FYD=C_`ZINa9P3z77o?r3%N(a9< zh)IEHJG*B zH+c4&uqH>~7~(BpvJg1;1g5#R8!tIW!h#kpxxabh-zWvopV-24ODLKUrNq13(yV{n zo7C6tVsM5w{{8o-w_-Mis@pG4W>1RptT2JOoev z*4I)@;P@pt&IP=0+QU!bm9x>Qk~GGncX1#^i-#XUtAk^1*@t>#-jXPc`1bLz%6s6 z2r@@0r;-dT6xezB>+ymnmK>{w+)L>Xz5j=^w+zeb3*Ln{At^1L64D^up`;+)BB3bV z9nuO&i3n2C(v3)$f>P4mB_Z7n&smR(zxRK>yyqM*^t#k9d+)Vo=AL`*nW_I_;C8d% zYMuGl73=Bw^RXJ_%P?l<@h~RXl2)XR)5W*aNTl z&1V^F{A1+Ok;h)uwZ_vs{_aN|+PP{)WB~=*T6Gv080Fe=)}x`w($Y=`tNqr;eV=vg zawbulS>LcmZcQcHxQ^JS`9=SFex=w@G>tX+Z$y$kF0fj`c4vsv-96vJ@J6% z(e0m)<8!xlmFT|!KN--T2I$>tiHm3YbO@$}N$NQf=Lt!7{5y$V$ZB5rzQxUy4 z(MJwRGyeJ!XHKVBR0a~_dXP79xA1Cvx`s~S{G2Y=@%b%Q?VDK&u?o*SG`wh|2=pM@ z%#6G1=g-k7c&Ac=Qf!R87fnjHn~lb8+a|EWfAygZUz~bb+Voh1`7pmT0t$a{qayEy zQ6T+Ej0QohYqrQvM7pMcf^SE~$A`=q`4mL9jnjG23)jE;betl0uVlF$R`dz$p|LQe+hV|G|0 z@V$d+x9)`&yGLUo0%hwMj~R>M>ckBW4vySMSs3((O%$KT1pGHl5u&yw<5C*>tom7| zlpIrxYSp(m#_CLxwiuh-J_VU;)YWcXehmEg$txKp5&Sk+{HXrMW8T2$E*p+xMV z>Wz)|PEI@x)^2Ulcyrm&3LE@#v>f*MKsZvrj8ugSUwn>V%j@|A)C4! zVZ8!*{nId&l7nX->(=a7`}^<^6Es)#FOh4W0ifFfgGXRfbGlgL(;ZtP=M>3IIY)eq zWI;bLy=!q;$M9ZFw75mK%D`nDy9UY)j9cAU%nQknOYrEQ_7UpW_nD6Q7D;jbIBqW(p34WRxH+?X7)_U1&`66%|Ls^NSM;ccVn5g`lI{ z^hBR-^A3@RNxmELFlRDVEJe$PeYH&)QDkGwpVC&;d@|=Z&UeHTC!%iUt9C&O0-=8QvkfCsAw0Qmfdm0aT!By+~tJv4KP%;SlYm9ep4y+Q3 z80>)l#_hvIq)Rxuh|g1K(ksAl(quI-84`-7B5<;OlNrX7DDgnaXY1nB`ZcuNZRFQp z*S|x)r!8x+YbcByD*O0bS=MZZeq0aJ;&#cSpesXXW*ql(5_rgyJW?-SEVh)(1g7$Ne z>>H}05fTAg3M%gdweHWv9FIf5uIz#%R_#Jbu(*S7^z-L=ZyAg8jZeM#w$2-4cv8XG zpFb;Z{Pyh5QXn=>KfFQtuT!E=a_-<&n7i+$tbFIWoIHN{1{NO)ef_X2Tt^E%P!V^TFxxp`jR>IhTnX1u9#A;Q{>+5$L{6FF ziw!p_u;*5-(pry^v|%3$5Zn0a3L_DPF-AU@gj`pIQM#X%weu%!D18~q+P9J?T{eAl zP+a#6?CWs(v%$;je+;Mrqaa zBH94Yuax|3;;!gKCMQ0%#qtj(cCzz3GHKk^Mn=3AQ)sU{pOP9G8J(P*=nkPoYz9lZ zRw4*`AF)-Y3I<-NxRj^dOUV;UKyb<@93?jj(0$|S;s#yEo!klSES7sA ztlHdi6T|v+`m#5h16!qk#EU0?@r`�PJzltjD}U+3J1Fhj9N+@ul9#Q>e@b!2-*j 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%0D^~Nd4 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

@1Mo?R_t*le-o)^%={swm`v%L;#W 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:JSBU;A9<9|;+0!G3J@6XGw7@&0=6 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%0A%*Nk z_K^cAKe4-@7cGHu1Xw)h&2DW5T&8<-J+$6lpT3Lz{WGNb@{zJg4(B50_6m5stGhd} zf-UA2Am-NV^X<_zWUfEZu8ag<6y2q48l+%QS8V z>lk|Y6WJTj<}RrpndqqE>^XT2if;vLqa1PYwKH2}t%3JGYXX1XILE@EIWGdsc__MO z9*2P%KuPlQMGD*)Xb8`njV0$@mvR{R-w(bH^@xGr7m=B$b7iOwf;r3C9oGMm)$C7h zBD0;`mafbGYcHCG6uF%XyYUgwcZqu01GYnL7?D+(IPen}cCSX~3opIA-#8M8o=#Up zYn@^L^&=Z;InY{kajG+00)v1-i4A3gYTWwX3%w~HxWi%DVpyuiXuPWcm)5Ri=vzQQ zH;yePcx_mnX~(tP76(M?^n8v$385pvHHYI|j{rr`u}nB|EoWuWqxGi15pJ);!y-WD z$hU!?QR@-|JTIQgo2SW^fge}LAre;Z+4)>qwk#_j2b$wVEVa4JFwqviFJ7amfF?!K zK+E`>&Ksw>^*}=T29Paz&4%L_O$LD?3m$9~;L^I$0Q!Cr8DYtGe~>udV)gw$lFa#J zN)ciR+&O~p3;_}pp?UgdTm*`81q8}Kw4;~9Zq5PGM~TZMUGD?gr=Al=QERdj(q9pi zPHtLr3^8G^fdz+ZJ!H~K?lQO$GK2)K^X+jBG&b!T45T6SK#^NefFzJDB-p3I=2#;? z@W=4{nXNwxHTx5iGdd?Z+mu{ni~cy!)fiW;!-d=UcmB|w9UuE^Whh9GId3|Y%x%BC zi7av5JMn!S^+hJ@Vmgt_B=HZC`f64NvRf-4^(_h;Si*(dt22QKn~&yks5UqZD|zGB zX7)ZhhsZh;;QOIxMpQJm!x9*XcBIjd+tot|S)Kixu5#991K3vAT`NjBTh)(T++v33 z#jEySx8Rt|Y@jP$o_(O5)0QA-+-ETk>qTMjQQrOgD|0T~R(1krn+ed>Iqbd&T^}6p z;ASk_6hKq{d{}o)g2oB+%>CK2`y(Zk)xd+9vu^u%=rE~h>82r=IHQIA$ Im&2L=0Y^SHMF0Q* diff --git a/examples/http/README.md b/examples/http/README.md deleted file mode 100644 index 5e6f2d7efa..0000000000 --- a/examples/http/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Overview - -This example shows how to use [OpenTelemetryMiddleware](https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-wsgi) and [requests](https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-http-requests) integrations to instrument a client and a server in Python. -It supports exporting spans either to the console or to [Jaeger](https://www.jaegertracing.io). - -## Installation - -```sh -$ pip install opentelemetry-api opentelemetry-sdk opentelemetry-ext-wsgi opentelemetry-ext-http-requests -``` - -Setup [Jaeger Tracing](https://www.jaegertracing.io/docs/latest/getting-started/#all-in-one) - -## Run the Application - -### Console - -* Run the server - -```bash -$ # from this directory -$ python server.py -``` - -* Run the client from a different terminal - -```bash -$ # from this directory -$ python tracer_client.py -``` - -The output will be displayed at the console on the client side - -```bash -Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x3703fd889dcdeb2b, trace_state={}), kind=SpanKind.CLIENT, parent=None, start_time=2019-11-07T21:52:59.591634Z, end_time=2019-11-07T21:53:00.386014Z) -``` - -And on the server - -```bash -127.0.0.1 - - [07/Nov/2019 13:53:00] "GET / HTTP/1.1" 200 - -Span(name="/wiki/Rabbit", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x4bf0be462b91d6ef, trace_state={}), kind=SpanKind.CLIENT, parent=Span(name="parent", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x68338643ccb2d53b, trace_state={})), start_time=2019-11-07T21:52:59.601597Z, end_time=2019-11-07T21:53:00.380491Z) -Span(name="parent", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x68338643ccb2d53b, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x36050ac596949bc1, trace_state={})), start_time=2019-11-07T21:52:59.601233Z, end_time=2019-11-07T21:53:00.384485Z) -Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x36050ac596949bc1, trace_state={}), kind=SpanKind.SERVER, parent=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x3703fd889dcdeb2b, trace_state={}), start_time=2019-11-07T21:52:59.600816Z, end_time=2019-11-07T21:53:00.385322Z) -``` - -### Jaeger - -* Run the server - -```sh -$ pip install opentelemetry-ext-jaeger -$ # from this directory -$ EXPORTER=jaeger python server.py -``` - -* Run the client from a different terminal - -```bash -$ EXPORTER=jaeger python tracer_client.py -``` - -#### Jaeger UI - -Open the Jaeger UI in your browser [http://localhost:16686](http://localhost:16686) - -

-Select `http-server` under *Service Name* and click on *Find Traces*. - -Click on the trace to view its details. - -

- -## Useful links -- For more information on OpenTelemetry, visit: -- For more information on tracing in Python, visit: - -## LICENSE - -Apache License 2.0 diff --git a/examples/http/images/jaeger-ui-detail.png b/examples/http/images/jaeger-ui-detail.png deleted file mode 100644 index 50d1901de6cd0ea353c329a46d09c905c3a461a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 144383 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

2r<gW0;4e?1fR&{<1m2+dVX_NwhGsxN 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≶suyKf1?zgA!UX%HT9bV-aZ#MpmE|bc715P* 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 diff --git a/examples/opentracing/README.md b/examples/opentracing/README.md deleted file mode 100644 index 2f7a926417..0000000000 --- a/examples/opentracing/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# Overview - -This example shows how to use the [`opentelemetry-ext-opentracing-shim` -package](https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-opentracing-shim) -to interact with libraries instrumented with -[`opentracing-python`](https://github.com/opentracing/opentracing-python). - -The included `rediscache` library creates spans via the OpenTracing Redis -integration, -[`redis_opentracing`](https://github.com/opentracing-contrib/python-redis). -Spans are exported via the Jaeger exporter, which is attached to the -OpenTelemetry tracer. - -## Installation - -### Jaeger - -Install and run -[Jaeger](https://www.jaegertracing.io/docs/latest/getting-started/#all-in-one). -See the [basic tracer -example](https://github.com/open-telemetry/opentelemetry-python/tree/master/examples/basic-tracer) -for more detail. - -### Redis - -Install Redis following the [instructions](https://redis.io/topics/quickstart). - -Make sure that the Redis server is running by executing this: - -```sh -$ redis-server -``` - -### Python Dependencies - -Install the Python dependencies in [`requirements.txt`](requirements.txt): - -```sh -$ pip install -r requirements.txt -``` - -Alternatively, you can install the Python dependencies separately: - -```sh -$ pip install \ - opentelemetry-api \ - opentelemetry-sdk \ - opentelemetry-ext-jaeger \ - opentelemetry-opentracing-shim \ - redis \ - redis_opentracing -``` - -## Run the Application - -The example script calculates a few Fibonacci numbers and stores the results in -Redis. The script, the `rediscache` library, and the OpenTracing Redis -integration all contribute spans to the trace. - -To run the script: - -```sh -$ python main.py -``` - -After running, you can view the generated trace in the Jaeger UI. - -#### Jaeger UI - -Open the Jaeger UI in your browser at - and view traces for the -"OpenTracing Shim Example" service. - -Each `main.py` run should generate a trace, and each trace should include -multiple spans that represent calls to Redis. - -

- -Note that tags and logs (OpenTracing) and attributes and events (OpenTelemetry) -from both tracing systems appear in the exported trace. - -

- -## Useful links -- For more information on OpenTelemetry, visit: -- For more information on tracing in Python, visit: - -## LICENSE - -Apache License 2.0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 56b1039012..0c98cc33af 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -136,9 +136,9 @@ class Span(trace_api.Span): """ # Initialize these lazily assuming most spans won't have them. - empty_attributes = BoundedDict(MAX_NUM_ATTRIBUTES) - empty_events = BoundedList(MAX_NUM_EVENTS) - empty_links = BoundedList(MAX_NUM_LINKS) + _empty_attributes = BoundedDict(MAX_NUM_ATTRIBUTES) + _empty_events = BoundedList(MAX_NUM_EVENTS) + _empty_links = BoundedList(MAX_NUM_LINKS) def __init__( self, @@ -171,19 +171,19 @@ def __init__( self._lock = threading.Lock() if attributes is None: - self.attributes = Span.empty_attributes + self.attributes = Span._empty_attributes else: self.attributes = BoundedDict.from_map( MAX_NUM_ATTRIBUTES, attributes ) if events is None: - self.events = Span.empty_events + self.events = Span._empty_events else: self.events = BoundedList.from_seq(MAX_NUM_EVENTS, events) if links is None: - self.links = Span.empty_links + self.links = Span._empty_links else: self.links = BoundedList.from_seq(MAX_NUM_LINKS, links) @@ -227,7 +227,7 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: return has_ended = self.end_time is not None if not has_ended: - if self.attributes is Span.empty_attributes: + if self.attributes is Span._empty_attributes: self.attributes = BoundedDict(MAX_NUM_ATTRIBUTES) if has_ended: logger.warning("Setting attribute on ended span.") @@ -275,7 +275,7 @@ def add_event( self.add_lazy_event( trace_api.Event( name, - Span.empty_attributes if attributes is None else attributes, + Span._empty_attributes if attributes is None else attributes, time_ns() if timestamp is None else timestamp, ) ) @@ -286,7 +286,7 @@ def add_lazy_event(self, event: trace_api.Event) -> None: return has_ended = self.end_time is not None if not has_ended: - if self.events is Span.empty_events: + if self.events is Span._empty_events: self.events = BoundedList(MAX_NUM_EVENTS) if has_ended: logger.warning("Calling add_event() on an ended span.") @@ -373,6 +373,7 @@ def __exit__( def generate_span_id() -> int: """Get a new random span ID. + Returns: A random 64-bit int for use as a span ID """ @@ -381,6 +382,7 @@ def generate_span_id() -> int: def generate_trace_id() -> int: """Get a new random trace ID. + Returns: A random 128-bit int for use as a trace ID """ @@ -405,7 +407,6 @@ def __init__( self.instrumentation_info = instrumentation_info def get_current_span(self): - """See `opentelemetry.trace.Tracer.get_current_span`.""" return self.source.get_current_span() def start_as_current_span( @@ -416,8 +417,6 @@ def start_as_current_span( attributes: Optional[types.Attributes] = None, links: Sequence[trace_api.Link] = (), ) -> Iterator[trace_api.Span]: - """See `opentelemetry.trace.Tracer.start_as_current_span`.""" - span = self.start_span(name, parent, kind, attributes, links) return self.use_span(span, end_on_exit=True) @@ -431,8 +430,6 @@ def start_span( # pylint: disable=too-many-locals start_time: Optional[int] = None, set_status_on_exception: bool = True, ) -> trace_api.Span: - """See `opentelemetry.trace.Tracer.start_span`.""" - if parent is Tracer.CURRENT_SPAN: parent = self.get_current_span() @@ -504,7 +501,6 @@ def start_span( # pylint: disable=too-many-locals def use_span( self, span: trace_api.Span, end_on_exit: bool = False ) -> Iterator[trace_api.Span]: - """See `opentelemetry.trace.Tracer.use_span`.""" try: token = context_api.attach(context_api.set_value(SPAN_KEY, span)) try: diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 9b981b0817..b9f9b6493e 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -23,7 +23,7 @@ cov ext/opentelemetry-ext-jaeger cov ext/opentelemetry-ext-opentracing-shim cov ext/opentelemetry-ext-wsgi cov ext/opentelemetry-ext-zipkin -cov examples/opentelemetry-example-app +cov docs/examples/opentelemetry-example-app coverage report coverage xml diff --git a/tox.ini b/tox.ini index 0423a6cc7e..52c82eba07 100644 --- a/tox.ini +++ b/tox.ini @@ -16,11 +16,11 @@ envlist = py3{4,5,6,7,8}-test-example-app pypy3-test-example-app - ; examples/basic_tracer + ; docs/examples/basic_tracer py3{4,5,6,7,8}-test-example-basic-tracer pypy3-test-example-basic-tracer - ; examples/http + ; docs/examples/http py3{4,5,6,7,8}-test-example-http pypy3-test-example-http @@ -112,9 +112,9 @@ changedir = test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests test-ext-flask: ext/opentelemetry-ext-flask/tests - test-example-app: examples/opentelemetry-example-app/tests - test-example-basic-tracer: examples/basic_tracer/tests - test-example-http: examples/http/tests + test-example-app: docs/examples/opentelemetry-example-app/tests + test-example-basic-tracer: docs/examples/basic_tracer/tests + test-example-http: docs/examples/http/tests test-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests commands_pre = @@ -126,14 +126,14 @@ commands_pre = example-app: pip install {toxinidir}/ext/opentelemetry-ext-http-requests example-app: pip install {toxinidir}/ext/opentelemetry-ext-wsgi example-app: pip install {toxinidir}/ext/opentelemetry-ext-flask - example-app: pip install {toxinidir}/examples/opentelemetry-example-app + example-app: pip install {toxinidir}/docs/examples/opentelemetry-example-app example-basic-tracer: pip install -e {toxinidir}/opentelemetry-api example-basic-tracer: pip install -e {toxinidir}/opentelemetry-sdk example-http: pip install -e {toxinidir}/opentelemetry-api example-http: pip install -e {toxinidir}/opentelemetry-sdk example-http: pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests example-http: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi - example-http: pip install -r {toxinidir}/examples/http/requirements.txt + example-http: pip install -r {toxinidir}/docs/examples/http/requirements.txt ext: pip install {toxinidir}/opentelemetry-api wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-testutil @@ -208,11 +208,14 @@ deps = thrift pymongo flask + mysql-connector-python + wrapt + psycopg2-binary changedir = docs commands = - sphinx-build -E -a -W --keep-going -b html -T . _build/html + sphinx-build -E -a --keep-going -b html -T . _build/html [testenv:py38-tracecontext] basepython: python3.8 From 4b6a52d620ac803c31ec5464b1df5af510e85425 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 10 Mar 2020 10:59:20 -0700 Subject: [PATCH 0231/1517] Rename "handle" to "bound metric" (#470) bound metric is the new name, as dictated in the spec. --- README.md | 20 ++-- docs/examples/metrics/record.py | 16 +-- .../src/opentelemetry/metrics/__init__.py | 105 +++++++++--------- .../tests/metrics/test_metrics.py | 30 ++--- .../src/opentelemetry/sdk/metrics/__init__.py | 80 +++++-------- .../tests/metrics/export/test_export.py | 8 +- .../tests/metrics/test_metrics.py | 89 ++++++++------- 7 files changed, 167 insertions(+), 181 deletions(-) diff --git a/README.md b/README.md index 734a824661..84ce5244af 100644 --- a/README.md +++ b/README.md @@ -77,20 +77,20 @@ from opentelemetry.sdk.metrics.export.controller import PushController metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) meter = metrics.get_meter(__name__) exporter = ConsoleMetricsExporter() -controller = PushController(meter, exporter, 5) +controller = PushController(meter=meter, exporter=exporter, interval=5) counter = meter.create_metric( - "available memory", - "available memory", - "bytes", - int, - Counter, - ("environment",), + name="available memory", + description="available memory", + unit="bytes", + value_type=int, + metric_type=Counter, + label_keys=("environment",), ) -label_values = ("staging",) -counter_handle = counter.get_handle(label_values) -counter_handle.add(100) +label_set = meter.get_label_set({"environment": "staging"}) +bound_counter = counter.bind(label_set) +bound_counter.add(100) ``` See the [API documentation](https://open-telemetry.github.io/opentelemetry-python/) for more detail, and the [examples folder](./examples) for a more sample code. diff --git a/docs/examples/metrics/record.py b/docs/examples/metrics/record.py index a376b2aafc..d4c3d03d82 100644 --- a/docs/examples/metrics/record.py +++ b/docs/examples/metrics/record.py @@ -60,13 +60,15 @@ # The meter takes a dictionary of key value pairs label_set = meter.get_label_set({"environment": "staging"}) -# Handle usage -# You can record metrics with metric handles. Handles are created by passing in -# a labelset. A handle is essentially metric data that corresponds to a specific -# set of labels. Therefore, getting a handle using the same set of labels will -# yield the same metric handle. -counter_handle = counter.get_handle(label_set) -counter_handle.add(100) +# Bound instrument usage + +# You can record metrics with bound metric instruments. Bound metric +# instruments are created by passing in a labelset. A bound metric instrument +# is essentially metric data that corresponds to a specific set of labels. +# Therefore, getting a bound metric instrument using the same set of labels +# will yield the same bound metric instrument. +bound_counter = counter.bind(label_set) +bound_counter.add(100) # Direct metric usage # You can record metrics directly using the metric instrument. You pass in a diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 3ba9bcad00..e521b2654c 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -37,42 +37,42 @@ ValueT = TypeVar("ValueT", int, float) -class DefaultMetricHandle: - """The default MetricHandle. +class DefaultBoundInstrument: + """The default bound metric instrument. - Used when no MetricHandle implementation is available. + Used when no bound instrument implementation is available. """ def add(self, value: ValueT) -> None: - """No-op implementation of `CounterHandle` add. + """No-op implementation of `BoundCounter` add. Args: - value: The value to add to the handle. + value: The value to add to the bound metric instrument. """ def record(self, value: ValueT) -> None: - """No-op implementation of `MeasureHandle` record. + """No-op implementation of `BoundMeasure` record. Args: - value: The value to record to the handle. + value: The value to record to the bound metric instrument. """ -class CounterHandle: +class BoundCounter: def add(self, value: ValueT) -> None: - """Increases the value of the handle by ``value``. + """Increases the value of the bound counter by ``value``. Args: - value: The value to add to the handle. + value: The value to add to the bound counter. """ -class MeasureHandle: +class BoundMeasure: def record(self, value: ValueT) -> None: - """Records the given ``value`` to this handle. + """Records the given ``value`` to this bound measure. Args: - value: The value to record to the handle. + value: The value to record to the bound measure. """ @@ -80,11 +80,11 @@ class LabelSet(abc.ABC): """A canonicalized set of labels useful for preaggregation Re-usable LabelSet objects provide a potential optimization for scenarios - where handles might not be effective. For example, if the LabelSet will be - re-used but only used once per metrics, handles do not offer any - optimization. It may best to pre-compute a canonicalized LabelSet once and - re-use it with the direct calling convention. LabelSets are immutable and - should be opaque in implementation. + where bound metric instruments might not be effective. For example, if the + LabelSet will be re-used but only used once per metrics, bound metric + instruments do not offer any optimization. It may best to pre-compute a + canonicalized LabelSet once and re-use it with the direct calling + convention. LabelSets are immutable and should be opaque in implementation. """ @@ -99,42 +99,43 @@ class Metric(abc.ABC): """Base class for various types of metrics. Metric class that inherit from this class are specialized with the type of - handle that the metric holds. + bound metric instrument that the metric holds. """ @abc.abstractmethod - def get_handle(self, label_set: LabelSet) -> "object": - """Gets a handle, used for repeated-use of metrics instruments. + def bind(self, label_set: LabelSet) -> "object": + """Gets a bound metric instrument. - Handles are useful to reduce the cost of repeatedly recording a metric - with a pre-defined set of label values. All metric kinds (counter, - measure) support declaring a set of required label keys. The - values corresponding to these keys should be specified in every handle. - "Unspecified" label values, in cases where a handle is requested but - a value was not provided are permitted. + Bound metric instruments are useful to reduce the cost of repeatedly + recording a metric with a pre-defined set of label values. All metric + kinds (counter, measure) support declaring a set of required label + keys. The values corresponding to these keys should be specified in + every bound metric instrument. "Unspecified" label values, in cases + where a bound metric instrument is requested but a value was not + provided are permitted. Args: - label_set: `LabelSet` to associate with the returned handle. + label_set: `LabelSet` to associate with the bound instrument. """ class DefaultMetric(Metric): """The default Metric used when no Metric implementation is available.""" - def get_handle(self, label_set: LabelSet) -> "DefaultMetricHandle": - """Gets a `DefaultMetricHandle`. + def bind(self, label_set: LabelSet) -> "DefaultBoundInstrument": + """Gets a `DefaultBoundInstrument`. Args: - label_set: `LabelSet` to associate with the returned handle. + label_set: `LabelSet` to associate with the bound instrument. """ - return DefaultMetricHandle() + return DefaultBoundInstrument() def add(self, value: ValueT, label_set: LabelSet) -> None: """No-op implementation of `Counter` add. Args: value: The value to add to the counter metric. - label_set: `LabelSet` to associate with the returned handle. + label_set: `LabelSet` to associate with the bound instrument. """ def record(self, value: ValueT, label_set: LabelSet) -> None: @@ -142,23 +143,23 @@ def record(self, value: ValueT, label_set: LabelSet) -> None: Args: value: The value to record to this measure metric. - label_set: `LabelSet` to associate with the returned handle. + label_set: `LabelSet` to associate with the bound instrument. """ class Counter(Metric): """A counter type metric that expresses the computation of a sum.""" - def get_handle(self, label_set: LabelSet) -> "CounterHandle": - """Gets a `CounterHandle`.""" - return CounterHandle() + def bind(self, label_set: LabelSet) -> "BoundCounter": + """Gets a `BoundCounter`.""" + return BoundCounter() def add(self, value: ValueT, label_set: LabelSet) -> None: """Increases the value of the counter by ``value``. Args: value: The value to add to the counter metric. - label_set: `LabelSet` to associate with the returned handle. + label_set: `LabelSet` to associate with the returned bound counter. """ @@ -168,21 +169,22 @@ class Measure(Metric): Measure metrics represent raw statistics that are recorded. """ - def get_handle(self, label_set: LabelSet) -> "MeasureHandle": - """Gets a `MeasureHandle` with a float value.""" - return MeasureHandle() + def bind(self, label_set: LabelSet) -> "BoundMeasure": + """Gets a `BoundMeasure`.""" + return BoundMeasure() def record(self, value: ValueT, label_set: LabelSet) -> None: """Records the ``value`` to the measure. Args: value: The value to record to this measure metric. - label_set: `LabelSet` to associate with the returned handle. + label_set: `LabelSet` to associate with the returned bound measure. """ class Observer(abc.ABC): - """An observer type metric instrument used to capture a current set of values. + """An observer type metric instrument used to capture a current set of + values. Observer instruments are asynchronous, a callback is invoked with the @@ -283,16 +285,15 @@ def record_batch( ) -> None: """Atomically records a batch of `Metric` and value pairs. - Allows the functionality of acting upon multiple metrics with - a single API call. Implementations should find metric and handles that - match the key-value pairs in the label tuples. + Allows the functionality of acting upon multiple metrics with a single + API call. Implementations should find bound metric instruments that + match the key-value pairs in the labelset. - Args: - label_set: The `LabelSet` associated with all measurements in - the batch. A measurement is a tuple, representing the `Metric` - being recorded and the corresponding value to record. - record_tuples: A sequence of pairs of `Metric` s and the - corresponding value to record for that metric. + Args: label_set: The `LabelSet` associated with all measurements in the + batch. A measurement is a tuple, representing the `Metric` being + recorded and the corresponding value to record. record_tuples: A + sequence of pairs of `Metric` s and the corresponding value to + record for that metric. """ @abc.abstractmethod diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 45913ca672..9ecbede9f2 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -22,14 +22,16 @@ class TestMetrics(unittest.TestCase): def test_default(self): default = metrics.DefaultMetric() default_ls = metrics.DefaultLabelSet() - handle = default.get_handle(default_ls) - self.assertIsInstance(handle, metrics.DefaultMetricHandle) + bound_metric_instr = default.bind(default_ls) + self.assertIsInstance( + bound_metric_instr, metrics.DefaultBoundInstrument + ) def test_counter(self): counter = metrics.Counter() label_set = metrics.LabelSet() - handle = counter.get_handle(label_set) - self.assertIsInstance(handle, metrics.CounterHandle) + bound_counter = counter.bind(label_set) + self.assertIsInstance(bound_counter, metrics.BoundCounter) def test_counter_add(self): counter = metrics.Counter() @@ -39,21 +41,21 @@ def test_counter_add(self): def test_measure(self): measure = metrics.Measure() label_set = metrics.LabelSet() - handle = measure.get_handle(label_set) - self.assertIsInstance(handle, metrics.MeasureHandle) + bound_measure = measure.bind(label_set) + self.assertIsInstance(bound_measure, metrics.BoundMeasure) def test_measure_record(self): measure = metrics.Measure() label_set = metrics.LabelSet() measure.record(1, label_set) - def test_default_handle(self): - metrics.DefaultMetricHandle() + def test_default_bound_metric(self): + metrics.DefaultBoundInstrument() - def test_counter_handle(self): - handle = metrics.CounterHandle() - handle.add(1) + def test_bound_counter(self): + bound_counter = metrics.BoundCounter() + bound_counter.add(1) - def test_measure_handle(self): - handle = metrics.MeasureHandle() - handle.record(1) + def test_bound_measure(self): + bound_measure = metrics.BoundMeasure() + bound_measure.record(1) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 5c616b39cf..9c840da298 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -13,12 +13,11 @@ # limitations under the License. import logging -from collections import OrderedDict from typing import Dict, Sequence, Tuple, Type from opentelemetry import metrics as metrics_api from opentelemetry.sdk.metrics.export.aggregate import Aggregator -from opentelemetry.sdk.metrics.export.batcher import Batcher, UngroupedBatcher +from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.util import time_ns @@ -48,17 +47,18 @@ def __eq__(self, other): return self._encoded == other._encoded -class BaseHandle: - """The base handle class containing common behavior for all handles. +class BaseBoundInstrument: + """Class containing common behavior for all bound metric instruments. - Handles are responsible for operating on data for metric instruments for a - specific set of labels. + Bound metric instruments are responsible for operating on data for metric + instruments for a specific set of labels. Args: - value_type: The type of values this handle holds (int, float). + value_type: The type of values for this bound instrument (int, float). enabled: True if the originating instrument is enabled. - aggregator: The aggregator for this handle. Will handle aggregation - upon updates and checkpointing of values for exporting. + aggregator: The aggregator for this bound metric instrument. Will + handle aggregation upon updates and checkpointing of values for + exporting. """ def __init__( @@ -94,16 +94,16 @@ def __repr__(self): ) -class CounterHandle(metrics_api.CounterHandle, BaseHandle): +class BoundCounter(metrics_api.BoundCounter, BaseBoundInstrument): def add(self, value: metrics_api.ValueT) -> None: - """See `opentelemetry.metrics.CounterHandle.add`.""" + """See `opentelemetry.metrics.BoundCounter.add`.""" if self._validate_update(value): self.update(value) -class MeasureHandle(metrics_api.MeasureHandle, BaseHandle): +class BoundMeasure(metrics_api.BoundMeasure, BaseBoundInstrument): def record(self, value: metrics_api.ValueT) -> None: - """See `opentelemetry.metrics.MeasureHandle.record`.""" + """See `opentelemetry.metrics.BoundMeasure.record`.""" if self._validate_update(value): self.update(value) @@ -113,11 +113,11 @@ class Metric(metrics_api.Metric): Also known as metric instrument. This is the class that is used to represent a metric that is to be continuously recorded and tracked. Each - metric has a set of handles that are created from the metric. See - `BaseHandle` for information on handles. + metric has a set of bound metrics that are created from the metric. See + `BaseBoundInstrument` for information on bound metric instruments. """ - HANDLE_TYPE = BaseHandle + BOUND_INSTR_TYPE = BaseBoundInstrument def __init__( self, @@ -136,20 +136,20 @@ def __init__( self.meter = meter self.label_keys = label_keys self.enabled = enabled - self.handles = {} + self.bound_instruments = {} - def get_handle(self, label_set: LabelSet) -> BaseHandle: - """See `opentelemetry.metrics.Metric.get_handle`.""" - handle = self.handles.get(label_set) - if not handle: - handle = self.HANDLE_TYPE( + def bind(self, label_set: LabelSet) -> BaseBoundInstrument: + """See `opentelemetry.metrics.Metric.bind`.""" + bound_instrument = self.bound_instruments.get(label_set) + if not bound_instrument: + bound_instrument = self.BOUND_INSTR_TYPE( self.value_type, self.enabled, # Aggregator will be created based off type of metric self.meter.batcher.aggregator_for(self.__class__), ) - self.handles[label_set] = handle - return handle + self.bound_instruments[label_set] = bound_instrument + return bound_instrument def __repr__(self): return '{}(name="{}", description="{}")'.format( @@ -163,31 +163,11 @@ class Counter(Metric, metrics_api.Counter): """See `opentelemetry.metrics.Counter`. """ - HANDLE_TYPE = CounterHandle - - def __init__( - self, - name: str, - description: str, - unit: str, - value_type: Type[metrics_api.ValueT], - meter: "Meter", - label_keys: Sequence[str] = (), - enabled: bool = True, - ): - super().__init__( - name, - description, - unit, - value_type, - meter, - label_keys=label_keys, - enabled=enabled, - ) + BOUND_INSTR_TYPE = BoundCounter def add(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: """See `opentelemetry.metrics.Counter.add`.""" - self.get_handle(label_set).add(value) + self.bind(label_set).add(value) UPDATE_FUNCTION = add @@ -195,11 +175,11 @@ def add(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: class Measure(Metric, metrics_api.Measure): """See `opentelemetry.metrics.Measure`.""" - HANDLE_TYPE = MeasureHandle + BOUND_INSTR_TYPE = BoundMeasure def record(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: """See `opentelemetry.metrics.Measure.record`.""" - self.get_handle(label_set).record(value) + self.bind(label_set).record(value) UPDATE_FUNCTION = record @@ -315,9 +295,9 @@ def collect(self) -> None: def _collect_metrics(self) -> None: for metric in self.metrics: if metric.enabled: - for label_set, handle in metric.handles.items(): + for label_set, bound_instr in metric.bound_instruments.items(): # TODO: Consider storing records in memory? - record = Record(metric, label_set, handle.aggregator) + record = Record(metric, label_set, bound_instr.aggregator) # Checkpoints the current aggregators # Applies different batching logic based on type of batcher self.batcher.process(record) diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 3aab1632ec..33d8d07c29 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -430,9 +430,7 @@ class TestObserverAggregator(unittest.TestCase): def test_update(self): observer = ObserverAggregator() # test current values without any update - self.assertEqual( - observer.mmsc.current, (None, None, None, 0), - ) + self.assertEqual(observer.mmsc.current, (None, None, None, 0)) self.assertIsNone(observer.current) # call update with some values @@ -452,9 +450,7 @@ def test_checkpoint(self): # take checkpoint wihtout any update observer.take_checkpoint() - self.assertEqual( - observer.checkpoint, (None, None, None, 0, None), - ) + self.assertEqual(observer.checkpoint, (None, None, None, 0, None)) # call update with some values values = (3, 50, 3, 97) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 6fcba4de63..4e7e532d86 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -103,7 +103,7 @@ def test_record_batch(self): label_set = meter.get_label_set(kvp) record_tuples = [(counter, 1.0)] meter.record_batch(label_set, record_tuples) - self.assertEqual(counter.get_handle(label_set).aggregator.current, 1.0) + self.assertEqual(counter.bind(label_set).aggregator.current, 1.0) def test_record_batch_multiple(self): meter = metrics.MeterProvider().get_meter(__name__) @@ -118,10 +118,9 @@ def test_record_batch_multiple(self): ) record_tuples = [(counter, 1.0), (measure, 3.0)] meter.record_batch(label_set, record_tuples) - self.assertEqual(counter.get_handle(label_set).aggregator.current, 1.0) + self.assertEqual(counter.bind(label_set).aggregator.current, 1.0) self.assertEqual( - measure.get_handle(label_set).aggregator.current, - (3.0, 3.0, 3.0, 1), + measure.bind(label_set).aggregator.current, (3.0, 3.0, 3.0, 1) ) def test_record_batch_exists(self): @@ -133,11 +132,11 @@ def test_record_batch_exists(self): "name", "desc", "unit", float, meter, label_keys ) counter.add(1.0, label_set) - handle = counter.get_handle(label_set) + bound_counter = counter.bind(label_set) record_tuples = [(counter, 1.0)] meter.record_batch(label_set, record_tuples) - self.assertEqual(counter.get_handle(label_set), handle) - self.assertEqual(handle.aggregator.current, 2.0) + self.assertEqual(counter.bind(label_set), bound_counter) + self.assertEqual(bound_counter.aggregator.current, 2.0) def test_create_metric(self): resource = mock.Mock(spec=resources.Resource) @@ -196,15 +195,17 @@ def test_get_label_set_empty(self): class TestMetric(unittest.TestCase): - def test_get_handle(self): + def test_bind(self): meter = metrics.MeterProvider().get_meter(__name__) metric_types = [metrics.Counter, metrics.Measure] for _type in metric_types: metric = _type("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) - handle = metric.get_handle(label_set) - self.assertEqual(metric.handles.get(label_set), handle) + bound_instrument = metric.bind(label_set) + self.assertEqual( + metric.bound_instruments.get(label_set), bound_instrument + ) class TestCounter(unittest.TestCase): @@ -213,10 +214,10 @@ def test_add(self): metric = metrics.Counter("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) - handle = metric.get_handle(label_set) + bound_counter = metric.bind(label_set) metric.add(3, label_set) metric.add(2, label_set) - self.assertEqual(handle.aggregator.current, 5) + self.assertEqual(bound_counter.aggregator.current, 5) class TestMeasure(unittest.TestCase): @@ -225,12 +226,12 @@ def test_record(self): metric = metrics.Measure("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) - handle = metric.get_handle(label_set) + bound_measure = metric.bind(label_set) values = (37, 42, 7) for val in values: metric.record(val, label_set) self.assertEqual( - handle.aggregator.current, + bound_measure.aggregator.current, (min(values), max(values), sum(values), len(values)), ) @@ -301,63 +302,67 @@ def test_run_exception(self, logger_mock): self.assertTrue(logger_mock.warning.called) -class TestCounterHandle(unittest.TestCase): +class TestBoundCounter(unittest.TestCase): def test_add(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, True, aggregator) - handle.add(3) - self.assertEqual(handle.aggregator.current, 3) + bound_metric = metrics.BoundCounter(int, True, aggregator) + bound_metric.add(3) + self.assertEqual(bound_metric.aggregator.current, 3) def test_add_disabled(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, False, aggregator) - handle.add(3) - self.assertEqual(handle.aggregator.current, 0) + bound_counter = metrics.BoundCounter(int, False, aggregator) + bound_counter.add(3) + self.assertEqual(bound_counter.aggregator.current, 0) @mock.patch("opentelemetry.sdk.metrics.logger") def test_add_incorrect_type(self, logger_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, True, aggregator) - handle.add(3.0) - self.assertEqual(handle.aggregator.current, 0) + bound_counter = metrics.BoundCounter(int, True, aggregator) + bound_counter.add(3.0) + self.assertEqual(bound_counter.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @mock.patch("opentelemetry.sdk.metrics.time_ns") def test_update(self, time_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, True, aggregator) + bound_counter = metrics.BoundCounter(int, True, aggregator) time_mock.return_value = 123 - handle.update(4.0) - self.assertEqual(handle.last_update_timestamp, 123) - self.assertEqual(handle.aggregator.current, 4.0) + bound_counter.update(4.0) + self.assertEqual(bound_counter.last_update_timestamp, 123) + self.assertEqual(bound_counter.aggregator.current, 4.0) -class TestMeasureHandle(unittest.TestCase): +class TestBoundMeasure(unittest.TestCase): def test_record(self): aggregator = export.aggregate.MinMaxSumCountAggregator() - handle = metrics.MeasureHandle(int, True, aggregator) - handle.record(3) - self.assertEqual(handle.aggregator.current, (3, 3, 3, 1)) + bound_measure = metrics.BoundMeasure(int, True, aggregator) + bound_measure.record(3) + self.assertEqual(bound_measure.aggregator.current, (3, 3, 3, 1)) def test_record_disabled(self): aggregator = export.aggregate.MinMaxSumCountAggregator() - handle = metrics.MeasureHandle(int, False, aggregator) - handle.record(3) - self.assertEqual(handle.aggregator.current, (None, None, None, 0)) + bound_measure = metrics.BoundMeasure(int, False, aggregator) + bound_measure.record(3) + self.assertEqual( + bound_measure.aggregator.current, (None, None, None, 0) + ) @mock.patch("opentelemetry.sdk.metrics.logger") def test_record_incorrect_type(self, logger_mock): aggregator = export.aggregate.MinMaxSumCountAggregator() - handle = metrics.MeasureHandle(int, True, aggregator) - handle.record(3.0) - self.assertEqual(handle.aggregator.current, (None, None, None, 0)) + bound_measure = metrics.BoundMeasure(int, True, aggregator) + bound_measure.record(3.0) + self.assertEqual( + bound_measure.aggregator.current, (None, None, None, 0) + ) self.assertTrue(logger_mock.warning.called) @mock.patch("opentelemetry.sdk.metrics.time_ns") def test_update(self, time_mock): aggregator = export.aggregate.MinMaxSumCountAggregator() - handle = metrics.MeasureHandle(int, True, aggregator) + bound_measure = metrics.BoundMeasure(int, True, aggregator) time_mock.return_value = 123 - handle.update(4.0) - self.assertEqual(handle.last_update_timestamp, 123) - self.assertEqual(handle.aggregator.current, (4.0, 4.0, 4.0, 1)) + bound_measure.update(4.0) + self.assertEqual(bound_measure.last_update_timestamp, 123) + self.assertEqual(bound_measure.aggregator.current, (4.0, 4.0, 4.0, 1)) From dd8521ec6d3e089586463a77f4df154899aedb22 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Wed, 11 Mar 2020 13:07:33 -0700 Subject: [PATCH 0232/1517] Adding OT Collector metrics exporter (#454) Current implementation use OpenCensus receiver available in Collector, this will need to be updated eventually to use OT receiver when is ready. Fixes #344 Co-authored-by: Chris Kleinknecht --- examples/metrics/collector.py | 53 +++++ examples/metrics/docker/collector-config.yaml | 18 ++ examples/metrics/docker/docker-compose.yaml | 19 ++ examples/metrics/docker/prometheus.yaml | 5 + ext/opentelemetry-ext-otcollector/README.rst | 49 ++++- .../otcollector/metrics_exporter/__init__.py | 165 ++++++++++++++ .../test_otcollector_metrics_exporter.py | 202 ++++++++++++++++++ ....py => test_otcollector_trace_exporter.py} | 0 8 files changed, 508 insertions(+), 3 deletions(-) create mode 100644 examples/metrics/collector.py create mode 100644 examples/metrics/docker/collector-config.yaml create mode 100644 examples/metrics/docker/docker-compose.yaml create mode 100644 examples/metrics/docker/prometheus.yaml create mode 100644 ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py create mode 100644 ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py rename ext/opentelemetry-ext-otcollector/tests/{test_otcollector_exporter.py => test_otcollector_trace_exporter.py} (100%) diff --git a/examples/metrics/collector.py b/examples/metrics/collector.py new file mode 100644 index 0000000000..230b296393 --- /dev/null +++ b/examples/metrics/collector.py @@ -0,0 +1,53 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This module serves as an example for a simple application using metrics +exporting to Collector +""" + +from opentelemetry import metrics +from opentelemetry.ext.otcollector.metrics_exporter import ( + CollectorMetricsExporter, +) +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export.controller import PushController + +# Meter is responsible for creating and recording metrics +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) +meter = metrics.get_meter(__name__) +# exporter to export metrics to OT Collector +exporter = CollectorMetricsExporter( + service_name="basic-service", endpoint="localhost:55678" +) +# controller collects metrics created from meter and exports it via the +# exporter every interval +controller = PushController(meter, exporter, 5) + +counter = meter.create_metric( + "requests", + "number of requests", + "requests", + int, + Counter, + ("environment",), +) + +# Labelsets are used to identify key-values that are associated with a specific +# metric that you want to record. These are useful for pre-aggregation and can +# be used to store custom dimensions pertaining to a metric +label_set = meter.get_label_set({"environment": "staging"}) + +counter.add(25, label_set) +input("Press any key to exit...") diff --git a/examples/metrics/docker/collector-config.yaml b/examples/metrics/docker/collector-config.yaml new file mode 100644 index 0000000000..78f685d208 --- /dev/null +++ b/examples/metrics/docker/collector-config.yaml @@ -0,0 +1,18 @@ +receivers: + opencensus: + endpoint: "0.0.0.0:55678" + +exporters: + prometheus: + endpoint: "0.0.0.0:8889" + logging: {} + +processors: + batch: + queued_retry: + +service: + pipelines: + metrics: + receivers: [opencensus] + exporters: [logging, prometheus] diff --git a/examples/metrics/docker/docker-compose.yaml b/examples/metrics/docker/docker-compose.yaml new file mode 100644 index 0000000000..d12a0a4f18 --- /dev/null +++ b/examples/metrics/docker/docker-compose.yaml @@ -0,0 +1,19 @@ +version: "2" +services: + + otel-collector: + image: omnition/opentelemetry-collector-contrib:latest + command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"] + volumes: + - ./collector-config.yaml:/conf/collector-config.yaml + ports: + - "8889:8889" # Prometheus exporter metrics + - "55678:55678" # OpenCensus receiver + + prometheus: + container_name: prometheus + image: prom/prometheus:latest + volumes: + - ./prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" diff --git a/examples/metrics/docker/prometheus.yaml b/examples/metrics/docker/prometheus.yaml new file mode 100644 index 0000000000..4eb2357231 --- /dev/null +++ b/examples/metrics/docker/prometheus.yaml @@ -0,0 +1,5 @@ +scrape_configs: + - job_name: 'otel-collector' + scrape_interval: 5s + static_configs: + - targets: ['otel-collector:8889'] diff --git a/ext/opentelemetry-ext-otcollector/README.rst b/ext/opentelemetry-ext-otcollector/README.rst index 33d8d58747..200ec9a91d 100644 --- a/ext/opentelemetry-ext-otcollector/README.rst +++ b/ext/opentelemetry-ext-otcollector/README.rst @@ -6,7 +6,7 @@ OpenTelemetry Collector Exporter .. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-otcollector.svg :target: https://pypi.org/project/opentelemetry-ext-otcollector/ -This library allows to export data to `OpenTelemetry Collector `_. +This library allows to export data to `OpenTelemetry Collector `_ , currently using OpenCensus receiver in Collector side. Installation ------------ @@ -16,8 +16,8 @@ Installation pip install opentelemetry-ext-otcollector -Usage ------ +Traces Usage +------------ The **OpenTelemetry Collector Exporter** allows to export `OpenTelemetry`_ traces to `OpenTelemetry Collector`_. @@ -48,6 +48,49 @@ The **OpenTelemetry Collector Exporter** allows to export `OpenTelemetry`_ trace with tracer.start_as_current_span("foo"): print("Hello world!") +Metrics Usage +------------- + +The **OpenTelemetry Collector Exporter** allows to export `OpenTelemetry`_ metrics to `OpenTelemetry Collector`_. + +.. code:: python + + from opentelemetry import metrics + from opentelemetry.ext.otcollector.metrics_exporter import CollectorMetricsExporter + from opentelemetry.sdk.metrics import Counter, MeterProvider + from opentelemetry.sdk.metrics.export.controller import PushController + + + # create a CollectorMetricsExporter + collector_exporter = CollectorMetricsExporter( + # optional: + # endpoint="myCollectorUrl:55678", + # service_name="test_service", + # host_name="machine/container name", + ) + + # Meter is responsible for creating and recording metrics + metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) + meter = metrics.get_meter(__name__) + # controller collects metrics created from meter and exports it via the + # exporter every interval + controller = PushController(meter, collector_exporter, 5) + counter = meter.create_metric( + "requests", + "number of requests", + "requests", + int, + Counter, + ("environment",), + ) + # Labelsets are used to identify key-values that are associated with a specific + # metric that you want to record. These are useful for pre-aggregation and can + # be used to store custom dimensions pertaining to a metric + label_set = meter.get_label_set({"environment": "staging"}) + + counter.add(25, label_set) + + References ---------- diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py new file mode 100644 index 0000000000..12715035c2 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py @@ -0,0 +1,165 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""OpenTelemetry Collector Metrics Exporter.""" + +import logging +from typing import Sequence + +import grpc +from opencensus.proto.agent.metrics.v1 import ( + metrics_service_pb2, + metrics_service_pb2_grpc, +) +from opencensus.proto.metrics.v1 import metrics_pb2 + +import opentelemetry.ext.otcollector.util as utils +from opentelemetry.sdk.metrics import Counter, Metric +from opentelemetry.sdk.metrics.export import ( + MetricRecord, + MetricsExporter, + MetricsExportResult, + aggregate, +) + +DEFAULT_ENDPOINT = "localhost:55678" + +logger = logging.getLogger(__name__) + + +# pylint: disable=no-member +class CollectorMetricsExporter(MetricsExporter): + """OpenTelemetry Collector metrics exporter. + + Args: + endpoint: OpenTelemetry Collector OpenCensus receiver endpoint. + service_name: Name of Collector service. + host_name: Host name. + client: MetricsService client stub. + """ + + def __init__( + self, + endpoint: str = DEFAULT_ENDPOINT, + service_name: str = None, + host_name: str = None, + client: metrics_service_pb2_grpc.MetricsServiceStub = None, + ): + self.endpoint = endpoint + if client is None: + channel = grpc.insecure_channel(self.endpoint) + self.client = metrics_service_pb2_grpc.MetricsServiceStub( + channel=channel + ) + else: + self.client = client + + self.node = utils.get_node(service_name, host_name) + + def export( + self, metric_records: Sequence[MetricRecord] + ) -> MetricsExportResult: + try: + responses = self.client.Export( + self.generate_metrics_requests(metric_records) + ) + + # Read response + for _ in responses: + pass + + except grpc.RpcError: + return MetricsExportResult.FAILED_RETRYABLE + + return MetricsExportResult.SUCCESS + + def shutdown(self) -> None: + pass + + def generate_metrics_requests( + self, metrics: Sequence[MetricRecord] + ) -> metrics_service_pb2.ExportMetricsServiceRequest: + collector_metrics = translate_to_collector(metrics) + service_request = metrics_service_pb2.ExportMetricsServiceRequest( + node=self.node, metrics=collector_metrics + ) + yield service_request + + +# pylint: disable=too-many-branches +def translate_to_collector( + metric_records: Sequence[MetricRecord], +) -> Sequence[metrics_pb2.Metric]: + collector_metrics = [] + for metric_record in metric_records: + + label_values = [] + label_keys = [] + for label_tuple in metric_record.label_set.labels: + label_keys.append(metrics_pb2.LabelKey(key=label_tuple[0])) + label_values.append( + metrics_pb2.LabelValue( + has_value=label_tuple[1] is not None, value=label_tuple[1] + ) + ) + + metric_descriptor = metrics_pb2.MetricDescriptor( + name=metric_record.metric.name, + description=metric_record.metric.description, + unit=metric_record.metric.unit, + type=get_collector_metric_type(metric_record.metric), + label_keys=label_keys, + ) + + timeseries = metrics_pb2.TimeSeries( + label_values=label_values, + points=[get_collector_point(metric_record)], + ) + collector_metrics.append( + metrics_pb2.Metric( + metric_descriptor=metric_descriptor, timeseries=[timeseries] + ) + ) + return collector_metrics + + +# pylint: disable=no-else-return +def get_collector_metric_type(metric: Metric) -> metrics_pb2.MetricDescriptor: + if isinstance(metric, Counter): + if metric.value_type == int: + return metrics_pb2.MetricDescriptor.CUMULATIVE_INT64 + elif metric.value_type == float: + return metrics_pb2.MetricDescriptor.CUMULATIVE_DOUBLE + return metrics_pb2.MetricDescriptor.UNSPECIFIED + + +def get_collector_point(metric_record: MetricRecord) -> metrics_pb2.Point: + point = metrics_pb2.Point( + timestamp=utils.proto_timestamp_from_time_ns( + metric_record.metric.bind( + metric_record.label_set + ).last_update_timestamp + ) + ) + if metric_record.metric.value_type == int: + point.int64_value = metric_record.aggregator.checkpoint + elif metric_record.metric.value_type == float: + point.double_value = metric_record.aggregator.checkpoint + else: + raise TypeError( + "Unsupported metric type: {}".format( + metric_record.metric.value_type + ) + ) + return point diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py new file mode 100644 index 0000000000..f58f9768d6 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py @@ -0,0 +1,202 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +import grpc +from google.protobuf.timestamp_pb2 import Timestamp +from opencensus.proto.metrics.v1 import metrics_pb2 + +from opentelemetry import metrics +from opentelemetry.ext.otcollector import metrics_exporter +from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider +from opentelemetry.sdk.metrics.export import ( + MetricRecord, + MetricsExportResult, + aggregate, +) + + +# pylint: disable=no-member +class TestCollectorMetricsExporter(unittest.TestCase): + @classmethod + def setUpClass(cls): + # pylint: disable=protected-access + cls._meter_defaults = ( + metrics._METER_PROVIDER, + metrics._METER_PROVIDER_FACTORY, + ) + metrics.set_preferred_meter_provider_implementation( + lambda _: MeterProvider() + ) + cls._meter = metrics.get_meter(__name__) + kvp = {"environment": "staging"} + cls._test_label_set = cls._meter.get_label_set(kvp) + + @classmethod + def tearDownClass(cls): + # pylint: disable=protected-access + ( + metrics._METER_PROVIDER, + metrics._METER_PROVIDER_FACTORY, + ) = cls._meter_defaults + + def test_constructor(self): + mock_get_node = mock.Mock() + patch = mock.patch( + "opentelemetry.ext.otcollector.util.get_node", + side_effect=mock_get_node, + ) + service_name = "testServiceName" + host_name = "testHostName" + client = grpc.insecure_channel("") + endpoint = "testEndpoint" + with patch: + exporter = metrics_exporter.CollectorMetricsExporter( + service_name=service_name, + host_name=host_name, + endpoint=endpoint, + client=client, + ) + + self.assertIs(exporter.client, client) + self.assertEqual(exporter.endpoint, endpoint) + mock_get_node.assert_called_with(service_name, host_name) + + def test_get_collector_metric_type(self): + result = metrics_exporter.get_collector_metric_type( + Counter("testName", "testDescription", "unit", int, None) + ) + self.assertIs(result, metrics_pb2.MetricDescriptor.CUMULATIVE_INT64) + result = metrics_exporter.get_collector_metric_type( + Counter("testName", "testDescription", "unit", float, None) + ) + self.assertIs(result, metrics_pb2.MetricDescriptor.CUMULATIVE_DOUBLE) + result = metrics_exporter.get_collector_metric_type( + Measure("testName", "testDescription", "unit", None, None) + ) + self.assertIs(result, metrics_pb2.MetricDescriptor.UNSPECIFIED) + + def test_get_collector_point(self): + aggregator = aggregate.CounterAggregator() + label_set = self._meter.get_label_set({"environment": "staging"}) + int_counter = self._meter.create_metric( + "testName", "testDescription", "unit", int, Counter + ) + float_counter = self._meter.create_metric( + "testName", "testDescription", "unit", float, Counter + ) + measure = self._meter.create_metric( + "testName", "testDescription", "unit", float, Measure + ) + result = metrics_exporter.get_collector_point( + MetricRecord(aggregator, label_set, int_counter) + ) + self.assertIsInstance(result, metrics_pb2.Point) + self.assertIsInstance(result.timestamp, Timestamp) + self.assertEqual(result.int64_value, 0) + aggregator.update(123.5) + aggregator.take_checkpoint() + result = metrics_exporter.get_collector_point( + MetricRecord(aggregator, label_set, float_counter) + ) + self.assertEqual(result.double_value, 123.5) + self.assertRaises( + TypeError, + metrics_exporter.get_collector_point( + MetricRecord(aggregator, label_set, measure) + ), + ) + + def test_export(self): + mock_client = mock.MagicMock() + mock_export = mock.MagicMock() + mock_client.Export = mock_export + host_name = "testHostName" + collector_exporter = metrics_exporter.CollectorMetricsExporter( + client=mock_client, host_name=host_name + ) + test_metric = self._meter.create_metric( + "testname", "testdesc", "unit", int, Counter, ["environment"] + ) + record = MetricRecord( + aggregate.CounterAggregator(), self._test_label_set, test_metric + ) + + result = collector_exporter.export([record]) + self.assertIs(result, MetricsExportResult.SUCCESS) + # pylint: disable=unsubscriptable-object + export_arg = mock_export.call_args[0] + service_request = next(export_arg[0]) + output_metrics = getattr(service_request, "metrics") + output_node = getattr(service_request, "node") + self.assertEqual(len(output_metrics), 1) + self.assertIsNotNone(getattr(output_node, "library_info")) + self.assertIsNotNone(getattr(output_node, "service_info")) + output_identifier = getattr(output_node, "identifier") + self.assertEqual( + getattr(output_identifier, "host_name"), "testHostName" + ) + + def test_translate_to_collector(self): + + test_metric = self._meter.create_metric( + "testname", "testdesc", "unit", int, Counter, ["environment"] + ) + aggregator = aggregate.CounterAggregator() + aggregator.update(123) + aggregator.take_checkpoint() + record = MetricRecord(aggregator, self._test_label_set, test_metric) + output_metrics = metrics_exporter.translate_to_collector([record]) + self.assertEqual(len(output_metrics), 1) + self.assertIsInstance(output_metrics[0], metrics_pb2.Metric) + self.assertEqual(output_metrics[0].metric_descriptor.name, "testname") + self.assertEqual( + output_metrics[0].metric_descriptor.description, "testdesc" + ) + self.assertEqual(output_metrics[0].metric_descriptor.unit, "unit") + self.assertEqual( + output_metrics[0].metric_descriptor.type, + metrics_pb2.MetricDescriptor.CUMULATIVE_INT64, + ) + self.assertEqual( + len(output_metrics[0].metric_descriptor.label_keys), 1 + ) + self.assertEqual( + output_metrics[0].metric_descriptor.label_keys[0].key, + "environment", + ) + self.assertEqual(len(output_metrics[0].timeseries), 1) + self.assertEqual(len(output_metrics[0].timeseries[0].label_values), 1) + self.assertEqual( + output_metrics[0].timeseries[0].label_values[0].has_value, True + ) + self.assertEqual( + output_metrics[0].timeseries[0].label_values[0].value, "staging" + ) + self.assertEqual(len(output_metrics[0].timeseries[0].points), 1) + self.assertEqual( + output_metrics[0].timeseries[0].points[0].timestamp.seconds, + record.metric.bind(record.label_set).last_update_timestamp + // 1000000000, + ) + self.assertEqual( + output_metrics[0].timeseries[0].points[0].timestamp.nanos, + record.metric.bind(record.label_set).last_update_timestamp + % 1000000000, + ) + self.assertEqual( + output_metrics[0].timeseries[0].points[0].int64_value, 123 + ) diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py similarity index 100% rename from ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py rename to ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py From 52332cefab8d44a564392f4cc37b6d52b4fdffab Mon Sep 17 00:00:00 2001 From: Liz Fong-Jones Date: Thu, 12 Mar 2020 16:30:13 -0400 Subject: [PATCH 0233/1517] Add example for pyodbc (#481) --- ext/opentelemetry-ext-dbapi/README.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst index 895b47b9ba..33ec8de6f6 100644 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -11,13 +11,16 @@ Usage .. code-block:: python import mysql.connector + import pyodbc from opentelemetry.trace import tracer_provider from opentelemetry.ext.dbapi import trace_integration trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # Ex: mysql.connector - trace_integration(tracer_provider(), mysql.connector, "connect", "mysql") + trace_integration(tracer_provider(), mysql.connector, "connect", "mysql", "sql") + # Ex: pyodbc + trace_integration(tracer_provider(), pyodbc, "Connection", "odbc", "sql") References From 3654a86f75425d260b1380e856b0d6ae4af29904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 13 Mar 2020 15:48:22 -0500 Subject: [PATCH 0234/1517] Update main readme (#484) Remove duplicated content from the main readme and add a link to the online documentation. --- README.md | 67 ++++++-------------------------------------------- docs/index.rst | 8 +++--- 2 files changed, 13 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 84ce5244af..906481fec9 100644 --- a/README.md +++ b/README.md @@ -45,63 +45,11 @@ pip install -e ./opentelemetry-sdk pip install -e ./ext/opentelemetry-ext-{integration} ``` -## Quick Start - -### Tracing - -```python -from opentelemetry import trace -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ConsoleSpanExporter -from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor - -trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) -trace.tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) -) -tracer = trace.get_tracer(__name__) -with tracer.start_as_current_span('foo'): - with tracer.start_as_current_span('bar'): - with tracer.start_as_current_span('baz'): - print("Hello world from OpenTelemetry Python!") -``` - -### Metrics - -```python -from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, MeterProvider -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.controller import PushController - -metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) -meter = metrics.get_meter(__name__) -exporter = ConsoleMetricsExporter() -controller = PushController(meter=meter, exporter=exporter, interval=5) - -counter = meter.create_metric( - name="available memory", - description="available memory", - unit="bytes", - value_type=int, - metric_type=Counter, - label_keys=("environment",), -) - -label_set = meter.get_label_set({"environment": "staging"}) -bound_counter = counter.bind(label_set) -bound_counter.add(100) -``` - -See the [API documentation](https://open-telemetry.github.io/opentelemetry-python/) for more detail, and the [examples folder](./examples) for a more sample code. - -## Extensions - -### Third-party exporters - -OpenTelemetry supports integration with the following third-party exporters. +## Documentation -- [Azure Monitor](https://github.com/microsoft/opentelemetry-exporters-python/tree/master/azure_monitor) +The online documentation is available at https://opentelemetry-python.readthedocs.io/, +if you want to access the documentation for the latest version use +https://opentelemetry-python.readthedocs.io/en/latest/. ## Contributing @@ -170,11 +118,12 @@ includes: - Flask Integration - PyMongo Integration -The v0.4 alpha release includes: +The [v0.4 alpha +release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.4.0) release includes: - Metrics MinMaxSumCount Aggregator -- Context API -- Full Metrics SDK Pipeline +- Context API +- Full Metrics SDK Pipeline - Metrics STDOUT Exporter - Dbapi2 Integration - MySQL Integration diff --git a/docs/index.rst b/docs/index.rst index f52129829a..3007b7f74d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,9 +30,11 @@ In addition, there are several extension packages which can be installed separat The extension packages can be found in :scm_web:`ext/ directory of the repository `. -In addition, third party exporters are available: +Extensions +---------- -* `Azure Monitor `_ +Visit `OpenTelemetry Registry `_ to find +related projects like exporters, instrumentation libraries, tracer implementations, etc. Installing Cutting Edge Packages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -40,7 +42,7 @@ Installing Cutting Edge Packages While the project is pre-1.0, there may be significant functionality that has not yet been released to PyPI. In that situation, you may want to install the packages directly from the repo. This can be done by cloning the -repositry and doing an `editable +repository and doing an `editable install `_: .. code-block:: sh From 68e909b52ccad0f0d20cf95334733d055d56b9f9 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Sat, 14 Mar 2020 14:16:00 -0600 Subject: [PATCH 0235/1517] Add a configuration manager (#466) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #123 This basically removes loader.py (uses entry points instead) and all the calls for set_preferred_implementation* in order to cleanly separate user code from configuration. It introduces a configuration manager that can have configuration set by a configuration file or environment variables that override the former configuration method. Co-authored-by: Mauricio Vásquez Co-authored-by: Chris Kleinknecht --- docs/api/api.rst | 1 - docs/api/util.loader.rst | 4 - docs/examples/basic_tracer/tracer.py | 7 +- docs/examples/http/server.py | 8 +- docs/examples/http/tracer_client.py | 6 +- docs/examples/metrics/observer_example.py | 3 +- docs/examples/metrics/prometheus.py | 3 +- docs/examples/metrics/record.py | 5 +- docs/examples/metrics/simple_example.py | 3 +- .../flask_example.py | 9 +- docs/examples/opentracing/main.py | 5 +- examples/metrics/collector.py | 3 +- ext/opentelemetry-ext-dbapi/README.rst | 1 - ext/opentelemetry-ext-jaeger/README.rst | 2 - .../examples/jaeger_exporter_example.py | 4 +- .../ext/opentracing_shim/__init__.py | 4 - .../tests/test_shim.py | 12 +- .../test_otcollector_metrics_exporter.py | 16 +- ext/opentelemetry-ext-prometheus/README.rst | 3 +- .../tests/test_prometheus_exporter.py | 9 +- ext/opentelemetry-ext-psycopg2/README.rst | 2 - .../ext/testutil/wsgitestutil.py | 6 +- ext/opentelemetry-ext-zipkin/README.rst | 2 - opentelemetry-api/setup.py | 18 +- .../opentelemetry/configuration/__init__.py | 129 +++++++++++++ .../src/opentelemetry/configuration/py.typed | 0 .../src/opentelemetry/metrics/__init__.py | 71 ++----- .../src/opentelemetry/trace/__init__.py | 74 ++----- .../src/opentelemetry/util/loader.py | 182 ------------------ .../tests/configuration/__init__.py | 0 .../tests/configuration/test_configuration.py | 113 +++++++++++ opentelemetry-api/tests/mypysmoke.py | 2 +- opentelemetry-api/tests/test_loader.py | 112 ----------- opentelemetry-api/tests/trace/test_globals.py | 36 +--- opentelemetry-sdk/setup.py | 8 + tests/w3c_tracecontext_validation_server.py | 21 +- 36 files changed, 343 insertions(+), 541 deletions(-) delete mode 100644 docs/api/util.loader.rst create mode 100644 opentelemetry-api/src/opentelemetry/configuration/__init__.py create mode 100644 opentelemetry-api/src/opentelemetry/configuration/py.typed delete mode 100644 opentelemetry-api/src/opentelemetry/util/loader.py create mode 100644 opentelemetry-api/tests/configuration/__init__.py create mode 100644 opentelemetry-api/tests/configuration/test_configuration.py delete mode 100644 opentelemetry-api/tests/test_loader.py diff --git a/docs/api/api.rst b/docs/api/api.rst index 3b15cd0d36..6ae631147a 100644 --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -9,4 +9,3 @@ OpenTelemetry Python API context metrics trace - util.loader \ No newline at end of file diff --git a/docs/api/util.loader.rst b/docs/api/util.loader.rst deleted file mode 100644 index 079d2e4a38..0000000000 --- a/docs/api/util.loader.rst +++ /dev/null @@ -1,4 +0,0 @@ -opentelemetry.util.loader module -================================ - -.. automodule:: opentelemetry.util.loader diff --git a/docs/examples/basic_tracer/tracer.py b/docs/examples/basic_tracer/tracer.py index a454eab7a9..8982e5cd7b 100755 --- a/docs/examples/basic_tracer/tracer.py +++ b/docs/examples/basic_tracer/tracer.py @@ -45,10 +45,7 @@ print("Using ConsoleSpanExporter") exporter = ConsoleSpanExporter() -# The preferred tracer implementation must be set, as the opentelemetry-api -# defines the interface with a no-op implementation. -trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) - +trace.set_tracer_provider(TracerProvider()) # We tell OpenTelemetry who it is that is creating spans. In this case, we have # no real name (no setup.py), so we make one up. If we had a version, we would # also specify it here. @@ -56,8 +53,8 @@ # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) +trace.get_tracer_provider().add_span_processor(span_processor) -trace.tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): diff --git a/docs/examples/http/server.py b/docs/examples/http/server.py index 50bc566b77..a665abdd49 100755 --- a/docs/examples/http/server.py +++ b/docs/examples/http/server.py @@ -39,19 +39,17 @@ else: exporter = ConsoleSpanExporter() -# The preferred tracer implementation must be set, as the opentelemetry-api -# defines the interface with a no-op implementation. -trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) -trace.tracer_provider().add_span_processor(span_processor) +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor(span_processor) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(trace.tracer_provider()) +http_requests.enable(trace.get_tracer_provider()) app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) diff --git a/docs/examples/http/tracer_client.py b/docs/examples/http/tracer_client.py index 6fd0a726a4..0e83c5c53f 100755 --- a/docs/examples/http/tracer_client.py +++ b/docs/examples/http/tracer_client.py @@ -37,10 +37,8 @@ else: exporter = ConsoleSpanExporter() -# The preferred tracer implementation must be set, as the opentelemetry-api -# defines the interface with a no-op implementation. -trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) -tracer_provider = trace.tracer_provider() +trace.set_tracer_provider(TracerProvider()) +tracer_provider = trace.get_tracer_provider() # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) diff --git a/docs/examples/metrics/observer_example.py b/docs/examples/metrics/observer_example.py index aff25ee476..7f5614d058 100644 --- a/docs/examples/metrics/observer_example.py +++ b/docs/examples/metrics/observer_example.py @@ -19,7 +19,7 @@ import psutil from opentelemetry import metrics -from opentelemetry.sdk.metrics import LabelSet, MeterProvider +from opentelemetry.sdk.metrics import LabelSet from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.metrics.export.controller import PushController @@ -27,7 +27,6 @@ # Configure a stateful batcher batcher = UngroupedBatcher(stateful=True) -metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) meter = metrics.get_meter(__name__) # Exporter to export metrics to the console diff --git a/docs/examples/metrics/prometheus.py b/docs/examples/metrics/prometheus.py index 4d30f8abcc..3c9a33f32a 100644 --- a/docs/examples/metrics/prometheus.py +++ b/docs/examples/metrics/prometheus.py @@ -21,14 +21,13 @@ from opentelemetry import metrics from opentelemetry.ext.prometheus import PrometheusMetricsExporter -from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics import Counter from opentelemetry.sdk.metrics.export.controller import PushController # Start Prometheus client start_http_server(port=8000, addr="localhost") # Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) meter = metrics.get_meter(__name__) # exporter to export metrics to Prometheus prefix = "MyAppPrefix" diff --git a/docs/examples/metrics/record.py b/docs/examples/metrics/record.py index d4c3d03d82..948fdf1091 100644 --- a/docs/examples/metrics/record.py +++ b/docs/examples/metrics/record.py @@ -19,13 +19,10 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics import Counter from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController -# The preferred tracer implementation must be set, as the opentelemetry-api -# defines the interface with a no-op implementation. -metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) # Meter is responsible for creating and recording metrics meter = metrics.get_meter(__name__) # exporter to export metrics to the console diff --git a/docs/examples/metrics/simple_example.py b/docs/examples/metrics/simple_example.py index 2b8f5cfac8..0108d1620b 100644 --- a/docs/examples/metrics/simple_example.py +++ b/docs/examples/metrics/simple_example.py @@ -23,7 +23,7 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider +from opentelemetry.sdk.metrics import Counter, Measure from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController @@ -44,7 +44,6 @@ def usage(argv): sys.exit(1) # Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) # Meter's namespace corresponds to the string passed as the first argument Pass # in True/False to indicate whether the batcher is stateful. True indicates the diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index a33b3b58f4..12d3eed204 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -23,7 +23,6 @@ import opentelemetry.ext.http_requests from opentelemetry import trace from opentelemetry.ext.flask import instrument_app -from opentelemetry.sdk.trace import TracerProvider def configure_opentelemetry(flask_app: flask.Flask): @@ -42,12 +41,6 @@ def configure_opentelemetry(flask_app: flask.Flask): """ # Start by configuring all objects required to ensure # a complete end to end workflow. - # The preferred implementation of these objects must be set, - # as the opentelemetry-api defines the interface with a no-op - # implementation. - trace.set_preferred_tracer_provider_implementation( - lambda _: TracerProvider() - ) # Next, we need to configure how the values that are used by # traces and metrics are propagated (such as what specific headers @@ -55,7 +48,7 @@ def configure_opentelemetry(flask_app: flask.Flask): # Integrations are the glue that binds the OpenTelemetry API # and the frameworks and libraries that are used together, automatically # creating Spans and propagating context as appropriate. - opentelemetry.ext.http_requests.enable(trace.tracer_provider()) + opentelemetry.ext.http_requests.enable(trace.get_tracer_provider()) instrument_app(flask_app) diff --git a/docs/examples/opentracing/main.py b/docs/examples/opentracing/main.py index 665099aeee..0c331ddee3 100755 --- a/docs/examples/opentracing/main.py +++ b/docs/examples/opentracing/main.py @@ -3,13 +3,10 @@ from opentelemetry import trace from opentelemetry.ext import opentracing_shim from opentelemetry.ext.jaeger import JaegerSpanExporter -from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor from rediscache import RedisCache -# Configure the tracer using the default implementation -trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) -tracer_provider = trace.tracer_provider() +tracer_provider = trace.get_tracer_provider() # Configure the tracer to export traces to Jaeger jaeger_exporter = JaegerSpanExporter( diff --git a/examples/metrics/collector.py b/examples/metrics/collector.py index 230b296393..25bc4ef915 100644 --- a/examples/metrics/collector.py +++ b/examples/metrics/collector.py @@ -21,11 +21,10 @@ from opentelemetry.ext.otcollector.metrics_exporter import ( CollectorMetricsExporter, ) -from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics import Counter from opentelemetry.sdk.metrics.export.controller import PushController # Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) meter = metrics.get_meter(__name__) # exporter to export metrics to OT Collector exporter = CollectorMetricsExporter( diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst index 33ec8de6f6..24d4577306 100644 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -15,7 +15,6 @@ Usage from opentelemetry.trace import tracer_provider from opentelemetry.ext.dbapi import trace_integration - trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # Ex: mysql.connector trace_integration(tracer_provider(), mysql.connector, "connect", "mysql", "sql") diff --git a/ext/opentelemetry-ext-jaeger/README.rst b/ext/opentelemetry-ext-jaeger/README.rst index 6813fbdeee..47549992db 100644 --- a/ext/opentelemetry-ext-jaeger/README.rst +++ b/ext/opentelemetry-ext-jaeger/README.rst @@ -32,10 +32,8 @@ gRPC is still not supported by this implementation. from opentelemetry import trace from opentelemetry.ext import jaeger - from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # create a JaegerSpanExporter diff --git a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py index 81815da935..1f0c303e06 100644 --- a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py +++ b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py @@ -2,10 +2,8 @@ from opentelemetry import trace from opentelemetry.ext import jaeger -from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor -trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # create a JaegerSpanExporter @@ -26,7 +24,7 @@ span_processor = BatchExportSpanProcessor(jaeger_exporter) # add to the tracer factory -trace.tracer_provider().add_span_processor(span_processor) +trace.get_tracer_provider().add_span_processor(span_processor) # create some spans for testing with tracer.start_as_current_span("foo") as foo: diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index bd9d22678e..331ed10baf 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -29,12 +29,8 @@ import time from opentelemetry import trace - from opentelemetry.sdk.trace import TracerProvider from opentelemetry.ext.opentracing_shim import create_tracer - # Tell OpenTelemetry which Tracer implementation to use. - trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) - # Create an OpenTelemetry Tracer. otel_tracer = trace.get_tracer(__name__) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index 2a3fe819c9..1d619ab277 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -43,19 +43,11 @@ class TestShim(TestCase): def setUp(self): """Create an OpenTelemetry tracer and a shim before every test case.""" - - self.shim = opentracingshim.create_tracer(trace.tracer_provider()) + trace.set_tracer_provider(TracerProvider()) + self.shim = opentracingshim.create_tracer(trace.get_tracer_provider()) @classmethod def setUpClass(cls): - """Set preferred tracer implementation only once rather than before - every test method. - """ - - trace.set_preferred_tracer_provider_implementation( - lambda T: TracerProvider() - ) - # Save current propagator to be restored on teardown. cls._previous_propagator = propagators.get_global_httptextformat() diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py index f58f9768d6..ab6f4c8ccd 100644 --- a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py @@ -34,25 +34,11 @@ class TestCollectorMetricsExporter(unittest.TestCase): @classmethod def setUpClass(cls): # pylint: disable=protected-access - cls._meter_defaults = ( - metrics._METER_PROVIDER, - metrics._METER_PROVIDER_FACTORY, - ) - metrics.set_preferred_meter_provider_implementation( - lambda _: MeterProvider() - ) + metrics.set_meter_provider(MeterProvider()) cls._meter = metrics.get_meter(__name__) kvp = {"environment": "staging"} cls._test_label_set = cls._meter.get_label_set(kvp) - @classmethod - def tearDownClass(cls): - # pylint: disable=protected-access - ( - metrics._METER_PROVIDER, - metrics._METER_PROVIDER_FACTORY, - ) = cls._meter_defaults - def test_constructor(self): mock_get_node = mock.Mock() patch = mock.patch( diff --git a/ext/opentelemetry-ext-prometheus/README.rst b/ext/opentelemetry-ext-prometheus/README.rst index e70332556e..f7049194a2 100644 --- a/ext/opentelemetry-ext-prometheus/README.rst +++ b/ext/opentelemetry-ext-prometheus/README.rst @@ -29,7 +29,7 @@ The **OpenTelemetry Prometheus Exporter** allows to export `OpenTelemetry`_ metr from opentelemetry import metrics from opentelemetry.ext.prometheus import PrometheusMetricsExporter - from opentelemetry.sdk.metrics import Counter, Meter + from opentelemetry.sdk.metrics import Counter from opentelemetry.sdk.metrics.export.controller import PushController from prometheus_client import start_http_server @@ -37,7 +37,6 @@ The **OpenTelemetry Prometheus Exporter** allows to export `OpenTelemetry`_ metr start_http_server(port=8000, addr="localhost") # Meter is responsible for creating and recording metrics - metrics.set_preferred_meter_implementation(lambda _: Meter()) meter = metrics.meter() # exporter to export metrics to Prometheus prefix = "MyAppPrefix" diff --git a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py index f688347538..512a3170ad 100644 --- a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py +++ b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py @@ -21,6 +21,7 @@ CustomCollector, PrometheusMetricsExporter, ) +from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk import metrics from opentelemetry.sdk.metrics.export import MetricRecord, MetricsExportResult from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator @@ -28,7 +29,8 @@ class TestPrometheusMetricExporter(unittest.TestCase): def setUp(self): - self._meter = metrics.MeterProvider().get_meter(__name__) + set_meter_provider(metrics.MeterProvider()) + self._meter = get_meter_provider().get_meter(__name__) self._test_metric = self._meter.create_metric( "testname", "testdesc", @@ -74,7 +76,7 @@ def test_export(self): self.assertIs(result, MetricsExportResult.SUCCESS) def test_counter_to_prometheus(self): - meter = metrics.MeterProvider().get_meter(__name__) + meter = get_meter_provider().get_meter(__name__) metric = meter.create_metric( "test@name", "testdesc", @@ -110,8 +112,7 @@ def test_counter_to_prometheus(self): # TODO: Add unit test once Measure Aggregators are available def test_invalid_metric(self): - - meter = metrics.MeterProvider().get_meter(__name__) + meter = get_meter_provider().get_meter(__name__) metric = meter.create_metric( "tesname", "testdesc", "unit", int, TestMetric ) diff --git a/ext/opentelemetry-ext-psycopg2/README.rst b/ext/opentelemetry-ext-psycopg2/README.rst index 127b74f0c7..3cf4cf110b 100644 --- a/ext/opentelemetry-ext-psycopg2/README.rst +++ b/ext/opentelemetry-ext-psycopg2/README.rst @@ -13,10 +13,8 @@ Usage import psycopg2 from opentelemetry import trace - from opentelemetry.sdk.trace import TracerProvider from opentelemetry.trace.ext.psycopg2 import trace_integration - trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) trace_integration(tracer) cnx = psycopg2.connect(database='Database') diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py index 18b64364db..cdce28b907 100644 --- a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py +++ b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py @@ -16,10 +16,8 @@ class WsgiTestBase(unittest.TestCase): @classmethod def setUpClass(cls): global _MEMORY_EXPORTER # pylint:disable=global-statement - trace_api.set_preferred_tracer_provider_implementation( - lambda T: TracerProvider() - ) - tracer_provider = trace_api.tracer_provider() + trace_api.set_tracer_provider(TracerProvider()) + tracer_provider = trace_api.get_tracer_provider() _MEMORY_EXPORTER = InMemorySpanExporter() span_processor = export.SimpleExportSpanProcessor(_MEMORY_EXPORTER) tracer_provider.add_span_processor(span_processor) diff --git a/ext/opentelemetry-ext-zipkin/README.rst b/ext/opentelemetry-ext-zipkin/README.rst index f933ba4a68..9c88e45060 100644 --- a/ext/opentelemetry-ext-zipkin/README.rst +++ b/ext/opentelemetry-ext-zipkin/README.rst @@ -30,10 +30,8 @@ This exporter always send traces to the configured Zipkin collector using HTTP. from opentelemetry import trace from opentelemetry.ext import zipkin - from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # create a ZipkinSpanExporter diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index 20e7f58143..5eb91d1f1b 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -12,14 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os +from os.path import dirname, join import setuptools -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "util", "version.py" -) +BASE_DIR = dirname(__file__) +VERSION_FILENAME = join(BASE_DIR, "src", "opentelemetry", "util", "version.py") PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: exec(f.read(), PACKAGE_INFO) @@ -67,6 +65,14 @@ "threadlocal_context = " "opentelemetry.context.threadlocal_context:" "ThreadLocalRuntimeContext", - ] + ], + "opentelemetry_meter_provider": [ + "default_meter_provider = " + "opentelemetry.metrics:DefaultMeterProvider" + ], + "opentelemetry_tracer_provider": [ + "default_tracer_provider = " + "opentelemetry.trace:DefaultTracerProvider" + ], }, ) diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py new file mode 100644 index 0000000000..2e6c29aa33 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -0,0 +1,129 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# FIXME find a better way to avoid all those "Expression has type "Any"" errors +# type: ignore + +""" +Simple configuration manager + +This is a configuration manager for the Tracer and Meter providers. It reads +configuration from environment variables prefixed with OPENTELEMETRY_PYTHON_: + +1. OPENTELEMETRY_PYTHON_TRACER_PROVIDER +2. OPENTELEMETRY_PYTHON_METER_PROVIDER + +The value of these environment variables should be the name of the entry point +that points to the class that implements either provider. This OpenTelemetry +API package provides one entry point for each, which can be found in the +setup.py file: + +entry_points={ + ... + "opentelemetry_meter_provider": [ + "default_meter_provider = " + "opentelemetry.metrics:DefaultMeterProvider" + ], + "opentelemetry_tracer_provider": [ + "default_tracer_provider = " + "opentelemetry.trace:DefaultTracerProvider" + ], +} + +To use the meter provider above, then the +OPENTELEMETRY_PYTHON_METER_PROVIDER should be set to +"default_meter_provider" (this is not actually necessary since the +OpenTelemetry API provided providers are the default ones used if no +configuration is found in the environment variables). + +Once this is done, the configuration manager can be used by simply importing +it from opentelemetry.configuration.Configuration. This is a class that can +be instantiated as many times as needed without concern because it will +always produce the same instance. Its attributes are lazy loaded and they +hold an instance of their corresponding provider. So, for example, to get +the configured meter provider: + +from opentelemetry.configuration import Configuration + +tracer_provider = Configuration().tracer_provider +""" + +from logging import getLogger +from os import environ + +from pkg_resources import iter_entry_points + +logger = getLogger(__name__) + + +class Configuration: + _instance = None + + __slots__ = ("tracer_provider", "meter_provider") + + def __new__(cls) -> "Configuration": + if Configuration._instance is None: + + configuration = { + key: "default_{}".format(key) for key in cls.__slots__ + } + + for key, value in configuration.items(): + configuration[key] = environ.get( + "OPENTELEMETRY_PYTHON_{}".format(key.upper()), value + ) + + for key, value in configuration.items(): + underscored_key = "_{}".format(key) + + setattr(Configuration, underscored_key, None) + setattr( + Configuration, + key, + property( + fget=lambda cls, local_key=key, local_value=value: cls._load( + key=local_key, value=local_value + ) + ), + ) + + Configuration._instance = object.__new__(cls) + + return cls._instance + + @classmethod + def _load(cls, key=None, value=None): + underscored_key = "_{}".format(key) + + if getattr(cls, underscored_key) is None: + try: + setattr( + cls, + underscored_key, + next( + iter_entry_points( + "opentelemetry_{}".format(key), name=value, + ) + ).load()(), + ) + except Exception: # pylint: disable=broad-except + # FIXME Decide on how to handle this. Should an exception be + # raised here, or only a message should be logged and should + # we fall back to the default meter provider? + logger.error( + "Failed to load configured provider %s", value, + ) + raise + + return getattr(cls, underscored_key) diff --git a/opentelemetry-api/src/opentelemetry/configuration/py.typed b/opentelemetry-api/src/opentelemetry/configuration/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index e521b2654c..c37119529b 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -27,13 +27,12 @@ """ import abc -import logging -from typing import Callable, Dict, Optional, Sequence, Tuple, Type, TypeVar +from logging import getLogger +from typing import Callable, Dict, Sequence, Tuple, Type, TypeVar -from opentelemetry.util import loader - -logger = logging.getLogger(__name__) +from opentelemetry.configuration import Configuration # type: ignore +logger = getLogger(__name__) ValueT = TypeVar("ValueT", int, float) @@ -398,15 +397,7 @@ def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": return DefaultLabelSet() -# Once https://github.com/python/mypy/issues/7092 is resolved, -# the following type definition should be replaced with -# from opentelemetry.util.loader import ImplementationFactory -ImplementationFactory = Callable[ - [Type[MeterProvider]], Optional[MeterProvider] -] - _METER_PROVIDER = None -_METER_PROVIDER_FACTORY = None def get_meter( @@ -418,52 +409,24 @@ def get_meter( This function is a convenience wrapper for opentelemetry.metrics.meter_provider().get_meter """ - return meter_provider().get_meter( + return get_meter_provider().get_meter( instrumenting_module_name, stateful, instrumenting_library_version ) -def meter_provider() -> MeterProvider: - """Gets the current global :class:`~.MeterProvider` object. +def set_meter_provider(meter_provider: MeterProvider) -> None: + """Sets the current global :class:`~.MeterProvider` object.""" + global _METER_PROVIDER # pylint: disable=global-statement + _METER_PROVIDER = meter_provider - If there isn't one set yet, a default will be loaded. - """ - global _METER_PROVIDER, _METER_PROVIDER_FACTORY # pylint:disable=global-statement - if _METER_PROVIDER is None: - # pylint:disable=protected-access - try: - _METER_PROVIDER = loader._load_impl( - MeterProvider, _METER_PROVIDER_FACTORY # type: ignore - ) - except TypeError: - # if we raised an exception trying to instantiate an - # abstract class, default to no-op meter impl - logger.warning( - "Unable to instantiate MeterProvider from meter provider factory.", - exc_info=True, - ) - _METER_PROVIDER = DefaultMeterProvider() - _METER_PROVIDER_FACTORY = None - - return _METER_PROVIDER - - -def set_preferred_meter_provider_implementation( - factory: ImplementationFactory, -) -> None: - """Set the factory to be used to create the meter provider. - - See :mod:`opentelemetry.util.loader` for details. - - This function may not be called after a meter is already loaded. - - Args: - factory: Callback that should create a new :class:`MeterProvider` instance. - """ - global _METER_PROVIDER_FACTORY # pylint:disable=global-statement +def get_meter_provider() -> MeterProvider: + """Gets the current global :class:`~.MeterProvider` object.""" + global _METER_PROVIDER # pylint: disable=global-statement - if _METER_PROVIDER: - raise RuntimeError("MeterProvider already loaded.") + if _METER_PROVIDER is None: + _METER_PROVIDER = ( + Configuration().meter_provider # type: ignore # pylint: disable=no-member + ) - _METER_PROVIDER_FACTORY = factory + return _METER_PROVIDER # type: ignore diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index a6633e434a..e23404f707 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -63,20 +63,21 @@ .. versionadded:: 0.1.0 .. versionchanged:: 0.3.0 `TracerProvider` was introduced and the global ``tracer`` getter was replaced - by `tracer_provider`. + by `get_tracer_provider`. """ import abc import enum -import logging import types as python_types import typing from contextlib import contextmanager +from logging import getLogger +from opentelemetry.configuration import Configuration # type: ignore from opentelemetry.trace.status import Status -from opentelemetry.util import loader, types +from opentelemetry.util import types -logger = logging.getLogger(__name__) +logger = getLogger(__name__) # TODO: quarantine ParentSpan = typing.Optional[typing.Union["Span", "SpanContext"]] @@ -639,15 +640,7 @@ def use_span( yield -# Once https://github.com/python/mypy/issues/7092 is resolved, -# the following type definition should be replaced with -# from opentelemetry.util.loader import ImplementationFactory -ImplementationFactory = typing.Callable[ - [typing.Type[TracerProvider]], typing.Optional[TracerProvider] -] - -_TRACER_PROVIDER = None # type: typing.Optional[TracerProvider] -_TRACER_PROVIDER_FACTORY = None # type: typing.Optional[ImplementationFactory] +_TRACER_PROVIDER = None def get_tracer( @@ -658,53 +651,24 @@ def get_tracer( This function is a convenience wrapper for opentelemetry.trace.tracer_provider().get_tracer """ - return tracer_provider().get_tracer( + return get_tracer_provider().get_tracer( instrumenting_module_name, instrumenting_library_version ) -def tracer_provider() -> TracerProvider: - """Gets the current global :class:`~.TracerProvider` object. - - If there isn't one set yet, a default will be loaded. - """ - global _TRACER_PROVIDER, _TRACER_PROVIDER_FACTORY # pylint:disable=global-statement - - if _TRACER_PROVIDER is None: - # pylint:disable=protected-access - try: - _TRACER_PROVIDER = loader._load_impl( - TracerProvider, _TRACER_PROVIDER_FACTORY # type: ignore - ) - except TypeError: - # if we raised an exception trying to instantiate an - # abstract class, default to no-op tracer impl - logger.warning( - "Unable to instantiate TracerProvider from factory.", - exc_info=True, - ) - _TRACER_PROVIDER = DefaultTracerProvider() - del _TRACER_PROVIDER_FACTORY - - return _TRACER_PROVIDER - +def set_tracer_provider(tracer_provider: TracerProvider) -> None: + """Sets the current global :class:`~.TracerProvider` object.""" + global _TRACER_PROVIDER # pylint: disable=global-statement + _TRACER_PROVIDER = tracer_provider -def set_preferred_tracer_provider_implementation( - factory: ImplementationFactory, -) -> None: - """Set the factory to be used to create the global TracerProvider. - See :mod:`opentelemetry.util.loader` for details. +def get_tracer_provider() -> TracerProvider: + """Gets the current global :class:`~.TracerProvider` object.""" + global _TRACER_PROVIDER # pylint: disable=global-statement - This function may not be called after a tracer is already loaded. - - Args: - factory: Callback that should create a new :class:`TracerProvider` - instance. - """ - global _TRACER_PROVIDER_FACTORY # pylint:disable=global-statement - - if _TRACER_PROVIDER: - raise RuntimeError("TracerProvider already loaded.") + if _TRACER_PROVIDER is None: + _TRACER_PROVIDER = ( + Configuration().tracer_provider # type: ignore # pylint: disable=no-member + ) - _TRACER_PROVIDER_FACTORY = factory + return _TRACER_PROVIDER # type: ignore diff --git a/opentelemetry-api/src/opentelemetry/util/loader.py b/opentelemetry-api/src/opentelemetry/util/loader.py deleted file mode 100644 index eeda2b3e7f..0000000000 --- a/opentelemetry-api/src/opentelemetry/util/loader.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -""" -The OpenTelemetry loader module is mainly used internally to load the -implementation for global objects like -:func:`opentelemetry.trace.tracer_provider`. - -.. _loader-factory: - -An instance of a global object of type ``T`` is always created with a factory -function with the following signature:: - - def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: - # ... - -That function is called with e.g., the type of the global object it should -create as an argument (e.g. the type object -:class:`opentelemetry.trace.TracerProvider`) and should return an instance of that type -(such that ``instanceof(my_factory_for_t(T), T)`` is true). Alternatively, it -may return ``None`` to indicate that the no-op default should be used. - -When loading an implementation, the following algorithm is used to find a -factory function or other means to create the global object: - - 1. If the environment variable - :samp:`OPENTELEMETRY_PYTHON_IMPLEMENTATION_{getter-name}` (e.g., - ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_TRACERPROVIDER``) is set to an - nonempty value, an attempt is made to import a module with that name and - use a factory function named ``get_opentelemetry_implementation`` in it. - 2. Otherwise, the same is tried with the environment variable - ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT``. - 3. Otherwise, if a :samp:`set_preferred_{}_implementation` was - called (e.g. - :func:`opentelemetry.trace.set_preferred_tracer_provider_implementation`), - the callback set there is used (that is, the environment variables - override the callback set in code). - 4. Otherwise, if :func:`set_preferred_default_implementation` was called, - the callback set there is used. - 5. Otherwise, an attempt is made to import and use the OpenTelemetry SDK. - 6. Otherwise the default implementation that ships with the API - distribution (a fast no-op implementation) is used. - -If any of the above steps fails (e.g., a module is loaded but does not define -the required function or a module name is set but the module fails to load), -the search immediatelly skips to the last step. - -Note that the first two steps (those that query environment variables) are -skipped if :data:`sys.flags` has ``ignore_environment`` set (which usually -means that the Python interpreter was invoked with the ``-E`` or ``-I`` flag). -""" - -import importlib -import os -import sys -from typing import Callable, Optional, Type, TypeVar - -_T = TypeVar("_T") - -# "Untrusted" because this is usually user-provided and we don't trust the user -# to really return a _T: by using object, mypy forces us to check/cast -# explicitly. -_UntrustedImplFactory = Callable[[Type[_T]], Optional[object]] - - -# This would be the normal ImplementationFactory which would be used to -# annotate setters, were it not for https://github.com/python/mypy/issues/7092 -# Once that bug is resolved, setters should use this instead of duplicating the -# code. -# ImplementationFactory = Callable[[Type[_T]], Optional[_T]] - -_DEFAULT_FACTORY = None # type: Optional[_UntrustedImplFactory[object]] - - -def _try_load_impl_from_modname( - implementation_modname: str, api_type: Type[_T] -) -> Optional[_T]: - try: - implementation_mod = importlib.import_module(implementation_modname) - except (ImportError, SyntaxError): - # TODO Log/warn - return None - - return _try_load_impl_from_mod(implementation_mod, api_type) - - -def _try_load_impl_from_mod( - implementation_mod: object, api_type: Type[_T] -) -> Optional[_T]: - - try: - # Note: We use such a long name to avoid calling a function that is not - # intended for this API. - - implementation_fn = getattr( - implementation_mod, "get_opentelemetry_implementation" - ) # type: _UntrustedImplFactory[_T] - except AttributeError: - # TODO Log/warn - return None - - return _try_load_impl_from_callback(implementation_fn, api_type) - - -def _try_load_impl_from_callback( - implementation_fn: _UntrustedImplFactory[_T], api_type: Type[_T] -) -> Optional[_T]: - result = implementation_fn(api_type) - if result is None: - return None - if not isinstance(result, api_type): - # TODO Warn if wrong type is returned - return None - - # TODO: Warn if implementation_fn returns api_type(): It should return None - # to indicate using the default. - - return result - - -def _try_load_configured_impl( - api_type: Type[_T], factory: Optional[_UntrustedImplFactory[_T]] -) -> Optional[_T]: - """Attempts to find any specially configured implementation. If none is - configured, or a load error occurs, returns `None` - """ - implementation_modname = None - if not sys.flags.ignore_environment: - implementation_modname = os.getenv( - "OPENTELEMETRY_PYTHON_IMPLEMENTATION_" + api_type.__name__.upper() - ) - if implementation_modname: - return _try_load_impl_from_modname( - implementation_modname, api_type - ) - implementation_modname = os.getenv( - "OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT" - ) - if implementation_modname: - return _try_load_impl_from_modname( - implementation_modname, api_type - ) - if factory is not None: - return _try_load_impl_from_callback(factory, api_type) - if _DEFAULT_FACTORY is not None: - return _try_load_impl_from_callback(_DEFAULT_FACTORY, api_type) - return None - - -# Public to other opentelemetry-api modules -def _load_impl( - api_type: Type[_T], factory: Optional[Callable[[Type[_T]], Optional[_T]]] -) -> _T: - """Tries to load a configured implementation, if unsuccessful, returns a - fast no-op implemenation that is always available. - """ - - result = _try_load_configured_impl(api_type, factory) - if result is None: - return api_type() - return result - - -def set_preferred_default_implementation( - implementation_factory: _UntrustedImplFactory[_T], -) -> None: - """Sets a factory function that may be called for any implementation - object. See the :ref:`module docs ` for more details.""" - global _DEFAULT_FACTORY # pylint:disable=global-statement - _DEFAULT_FACTORY = implementation_factory diff --git a/opentelemetry-api/tests/configuration/__init__.py b/opentelemetry-api/tests/configuration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py new file mode 100644 index 0000000000..754f9c4894 --- /dev/null +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -0,0 +1,113 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from json import dumps +from unittest import TestCase +from unittest.mock import patch + +from opentelemetry.configuration import Configuration # type: ignore +from pytest import fixture # type: ignore # pylint: disable=import-error + + +class TestConfiguration(TestCase): + class IterEntryPointsMock: + def __init__( + self, argument, name=None + ): # pylint: disable=unused-argument + self._name = name + + def __next__(self): + return self + + def __call__(self): + return self._name + + def load(self): + return self + + @fixture(autouse=True) + def configdir(self, tmpdir): # type: ignore # pylint: disable=no-self-use + tmpdir.chdir() + tmpdir.mkdir(".config").join("opentelemetry_python.json").write( + dumps({"tracer_provider": "overridden_tracer_provider"}) + ) + + def setUp(self): + Configuration._instance = None # pylint: disable=protected-access + + def tearDown(self): + Configuration._instance = None # pylint: disable=protected-access + + def test_singleton(self): + self.assertIs(Configuration(), Configuration()) + + @patch( + "opentelemetry.configuration.iter_entry_points", + **{"side_effect": IterEntryPointsMock} # type: ignore + ) + def test_lazy( # type: ignore + self, mock_iter_entry_points, # pylint: disable=unused-argument + ): + configuration = Configuration() + + self.assertIsNone( + configuration._tracer_provider # pylint: disable=no-member,protected-access + ) + + configuration.tracer_provider # pylint: disable=pointless-statement + + self.assertEqual( + configuration._tracer_provider, # pylint: disable=no-member,protected-access + "default_tracer_provider", + ) + + @patch( + "opentelemetry.configuration.iter_entry_points", + **{"side_effect": IterEntryPointsMock} # type: ignore + ) + def test_default_values( # type: ignore + self, mock_iter_entry_points # pylint: disable=unused-argument + ): + self.assertEqual( + Configuration().tracer_provider, "default_tracer_provider" + ) # pylint: disable=no-member + self.assertEqual( + Configuration().meter_provider, "default_meter_provider" + ) # pylint: disable=no-member + + @patch( + "opentelemetry.configuration.iter_entry_points", + **{"side_effect": IterEntryPointsMock} # type: ignore + ) + @patch.dict( + "os.environ", + {"OPENTELEMETRY_PYTHON_METER_PROVIDER": "overridden_meter_provider"}, + ) + def test_environment_variables( # type: ignore + self, mock_iter_entry_points # pylint: disable=unused-argument + ): # type: ignore + self.assertEqual( + Configuration().tracer_provider, "default_tracer_provider" + ) # pylint: disable=no-member + self.assertEqual( + Configuration().meter_provider, "overridden_meter_provider" + ) # pylint: disable=no-member + + def test_property(self): + with self.assertRaises(AttributeError): + Configuration().tracer_provider = "new_tracer_provider" + + def test_slots(self): + with self.assertRaises(AttributeError): + Configuration().xyz = "xyz" # pylint: disable=assigning-non-slot diff --git a/opentelemetry-api/tests/mypysmoke.py b/opentelemetry-api/tests/mypysmoke.py index bbbda93ef2..2891f3ee62 100644 --- a/opentelemetry-api/tests/mypysmoke.py +++ b/opentelemetry-api/tests/mypysmoke.py @@ -16,4 +16,4 @@ def dummy_check_mypy_returntype() -> opentelemetry.trace.TracerProvider: - return opentelemetry.trace.tracer_provider() + return opentelemetry.trace.get_tracer_provider() diff --git a/opentelemetry-api/tests/test_loader.py b/opentelemetry-api/tests/test_loader.py deleted file mode 100644 index 76575df705..0000000000 --- a/opentelemetry-api/tests/test_loader.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import sys -import unittest -from importlib import reload -from typing import Any, Callable - -from opentelemetry import trace -from opentelemetry.util import loader - -DUMMY_TRACER_PROVIDER = None - - -class DummyTracerProvider(trace.TracerProvider): - def get_tracer( - self, - instrumenting_module_name: str, - instrumenting_library_version: str = "", - ) -> "trace.Tracer": - return trace.DefaultTracer() - - -def get_opentelemetry_implementation(type_): - global DUMMY_TRACER_PROVIDER # pylint:disable=global-statement - assert type_ is trace.TracerProvider - DUMMY_TRACER_PROVIDER = DummyTracerProvider() - return DUMMY_TRACER_PROVIDER - - -# pylint:disable=redefined-outer-name,protected-access,unidiomatic-typecheck - - -class TestLoader(unittest.TestCase): - def setUp(self): - reload(loader) - reload(trace) - - # Need to reload self, otherwise DummyTracerProvider will have the wrong - # base class after reloading `trace`. - reload(sys.modules[__name__]) - - def test_get_default(self): - tracer_provider = trace.tracer_provider() - self.assertIs(type(tracer_provider), trace.DefaultTracerProvider) - - def test_preferred_impl(self): - trace.set_preferred_tracer_provider_implementation( - get_opentelemetry_implementation - ) - tracer_provider = trace.tracer_provider() - self.assertIs(tracer_provider, DUMMY_TRACER_PROVIDER) - - # NOTE: We use do_* + *_ methods because subtest wouldn't run setUp, - # which we require here. - def do_test_preferred_impl(self, setter: Callable[[Any], Any]) -> None: - setter(get_opentelemetry_implementation) - tracer_provider = trace.tracer_provider() - self.assertIs(tracer_provider, DUMMY_TRACER_PROVIDER) - - def test_preferred_impl_with_tracer(self): - self.do_test_preferred_impl( - trace.set_preferred_tracer_provider_implementation - ) - - def test_preferred_impl_with_default(self): - self.do_test_preferred_impl( - loader.set_preferred_default_implementation - ) - - def test_try_set_again(self): - self.assertTrue(trace.tracer_provider()) - # Try setting after the tracer_provider has already been created: - with self.assertRaises(RuntimeError) as einfo: - trace.set_preferred_tracer_provider_implementation( - get_opentelemetry_implementation - ) - self.assertIn("already loaded", str(einfo.exception)) - - def do_test_get_envvar(self, envvar_suffix: str) -> None: - global DUMMY_TRACER_PROVIDER # pylint:disable=global-statement - - # Test is not runnable with this! - self.assertFalse(sys.flags.ignore_environment) - - envname = "OPENTELEMETRY_PYTHON_IMPLEMENTATION_" + envvar_suffix - os.environ[envname] = __name__ - try: - tracer_provider = trace.tracer_provider() - self.assertIs(tracer_provider, DUMMY_TRACER_PROVIDER) - finally: - DUMMY_TRACER_PROVIDER = None - del os.environ[envname] - self.assertIs(type(tracer_provider), DummyTracerProvider) - - def test_get_envvar_tracer(self): - return self.do_test_get_envvar("TRACERPROVIDER") - - def test_get_envvar_default(self): - return self.do_test_get_envvar("DEFAULT") diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 7c4d8e3549..2e0339b99d 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -1,40 +1,18 @@ -import importlib import unittest +from unittest.mock import patch from opentelemetry import trace class TestGlobals(unittest.TestCase): def setUp(self): - importlib.reload(trace) + self._patcher = patch("opentelemetry.trace._TRACER_PROVIDER") + self._mock_tracer_provider = self._patcher.start() - # This class has to be declared after the importlib reload, or else it - # will inherit from an old TracerProvider, rather than the new - # TracerProvider ABC created from reload. - - static_tracer = trace.DefaultTracer() - - class DummyTracerProvider(trace.TracerProvider): - """TracerProvider used for testing""" - - def get_tracer( - self, - instrumenting_module_name: str, - instrumenting_library_version: str = "", - ) -> trace.Tracer: - # pylint:disable=no-self-use,unused-argument - return static_tracer - - trace.set_preferred_tracer_provider_implementation( - lambda _: DummyTracerProvider() - ) - - @staticmethod - def tearDown() -> None: - importlib.reload(trace) + def tearDown(self) -> None: + self._patcher.stop() def test_get_tracer(self): """trace.get_tracer should proxy to the global tracer provider.""" - from_global_api = trace.get_tracer("foo") - from_tracer_api = trace.tracer_provider().get_tracer("foo") - self.assertIs(from_global_api, from_tracer_api) + trace.get_tracer("foo", "var") + self._mock_tracer_provider.get_tracer.assert_called_with("foo", "var") diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index 50fe925605..c471ff0231 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -56,4 +56,12 @@ "/tree/master/opentelemetry-sdk" ), zip_safe=False, + entry_points={ + "opentelemetry_meter_provider": [ + "sdk_meter_provider = opentelemetry.sdk.metrics:MeterProvider" + ], + "opentelemetry_tracer_provider": [ + "sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider" + ], + }, ) diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py index 4ec179c354..c2119f9587 100644 --- a/tests/w3c_tracecontext_validation_server.py +++ b/tests/w3c_tracecontext_validation_server.py @@ -23,27 +23,28 @@ import flask import requests -from opentelemetry import trace -from opentelemetry.ext import http_requests -from opentelemetry.ext.wsgi import OpenTelemetryMiddleware from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( + +# FIXME This could likely be avoided by integrating this script into the +# standard test running mechanisms. + +from opentelemetry import trace # noqa # isort:skip +from opentelemetry.ext import http_requests # noqa # isort:skip" +from opentelemetry.ext.wsgi import OpenTelemetryMiddleware # noqa # isort:skip +from opentelemetry.sdk.trace.export import ( # noqa # isort:skip ConsoleSpanExporter, SimpleExportSpanProcessor, ) -# The preferred tracer implementation must be set, as the opentelemetry-api -# defines the interface with a no-op implementation. -trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) - # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(trace.tracer_provider()) +trace.set_tracer_provider(TracerProvider()) +http_requests.enable(trace.get_tracer_provider()) # SpanExporter receives the spans and send them to the target location. span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter()) -trace.tracer_provider().add_span_processor(span_processor) +trace.get_tracer_provider().add_span_processor(span_processor) app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) From 4f1aa28d0cfa626359d2470e7d920eb06dc2392f Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Sat, 14 Mar 2020 15:07:17 -0700 Subject: [PATCH 0236/1517] Use SDK providers in example code (#488) --- docs/examples/metrics/observer_example.py | 3 ++- docs/examples/metrics/prometheus.py | 3 ++- docs/examples/metrics/record.py | 4 ++- docs/examples/metrics/simple_example.py | 14 +++++------ .../flask_example.py | 25 ++++++++----------- docs/examples/opentracing/main.py | 3 +++ examples/metrics/collector.py | 3 ++- ext/opentelemetry-ext-dbapi/README.rst | 5 +++- ext/opentelemetry-ext-jaeger/README.rst | 2 ++ .../examples/jaeger_exporter_example.py | 2 ++ .../ext/opentracing_shim/__init__.py | 4 +++ ext/opentelemetry-ext-prometheus/README.rst | 3 ++- ext/opentelemetry-ext-psycopg2/README.rst | 2 ++ ext/opentelemetry-ext-zipkin/README.rst | 2 ++ 14 files changed, 47 insertions(+), 28 deletions(-) diff --git a/docs/examples/metrics/observer_example.py b/docs/examples/metrics/observer_example.py index 7f5614d058..563d503c3e 100644 --- a/docs/examples/metrics/observer_example.py +++ b/docs/examples/metrics/observer_example.py @@ -19,7 +19,7 @@ import psutil from opentelemetry import metrics -from opentelemetry.sdk.metrics import LabelSet +from opentelemetry.sdk.metrics import LabelSet, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.metrics.export.controller import PushController @@ -27,6 +27,7 @@ # Configure a stateful batcher batcher = UngroupedBatcher(stateful=True) +metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__) # Exporter to export metrics to the console diff --git a/docs/examples/metrics/prometheus.py b/docs/examples/metrics/prometheus.py index 3c9a33f32a..c3754c89a2 100644 --- a/docs/examples/metrics/prometheus.py +++ b/docs/examples/metrics/prometheus.py @@ -21,13 +21,14 @@ from opentelemetry import metrics from opentelemetry.ext.prometheus import PrometheusMetricsExporter -from opentelemetry.sdk.metrics import Counter +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export.controller import PushController # Start Prometheus client start_http_server(port=8000, addr="localhost") # Meter is responsible for creating and recording metrics +metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__) # exporter to export metrics to Prometheus prefix = "MyAppPrefix" diff --git a/docs/examples/metrics/record.py b/docs/examples/metrics/record.py index 948fdf1091..b558e18fca 100644 --- a/docs/examples/metrics/record.py +++ b/docs/examples/metrics/record.py @@ -19,10 +19,12 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController +# Use the meter type provided by the SDK package +metrics.set_meter_provider(MeterProvider()) # Meter is responsible for creating and recording metrics meter = metrics.get_meter(__name__) # exporter to export metrics to the console diff --git a/docs/examples/metrics/simple_example.py b/docs/examples/metrics/simple_example.py index 0108d1620b..a0152b29f3 100644 --- a/docs/examples/metrics/simple_example.py +++ b/docs/examples/metrics/simple_example.py @@ -23,7 +23,7 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Measure +from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController @@ -43,13 +43,13 @@ def usage(argv): usage(sys.argv) sys.exit(1) -# Meter is responsible for creating and recording metrics -# Meter's namespace corresponds to the string passed as the first argument Pass -# in True/False to indicate whether the batcher is stateful. True indicates the -# batcher computes checkpoints from over the process lifetime. False indicates -# the batcher computes checkpoints which describe the updates of a single -# collection period (deltas) +# The Meter is responsible for creating and recording metrics. Each meter has a +# unique name, which we set as the module's name here. The second argument +# determines whether how metrics are collected: if true, metrics accumulate +# over the process lifetime. If false, metrics are reset at the beginning of +# each collection interval. +metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__, batcher_mode == "stateful") # Exporter to export metrics to the console diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 12d3eed204..89933be190 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -23,6 +23,7 @@ import opentelemetry.ext.http_requests from opentelemetry import trace from opentelemetry.ext.flask import instrument_app +from opentelemetry.sdk.trace import TracerProvider def configure_opentelemetry(flask_app: flask.Flask): @@ -33,21 +34,16 @@ def configure_opentelemetry(flask_app: flask.Flask): * sets tracer to the SDK's Tracer * enables requests integration on the Tracer * uses a WSGI middleware to enable configuration - - TODO: - - * processors? - * exporters? """ - # Start by configuring all objects required to ensure - # a complete end to end workflow. + # Start by configuring all objects required to ensure a complete end to end + # workflow. + trace.set_tracer_provider(TracerProvider()) - # Next, we need to configure how the values that are used by - # traces and metrics are propagated (such as what specific headers - # carry this value). - # Integrations are the glue that binds the OpenTelemetry API - # and the frameworks and libraries that are used together, automatically - # creating Spans and propagating context as appropriate. + # Next, we need to configure how the values that are used by traces and + # metrics are propagated (such as what specific headers carry this value). + # Integrations are the glue that binds the OpenTelemetry API and the + # frameworks and libraries that are used together, automatically creating + # Spans and propagating context as appropriate. opentelemetry.ext.http_requests.enable(trace.get_tracer_provider()) instrument_app(flask_app) @@ -57,8 +53,7 @@ def configure_opentelemetry(flask_app: flask.Flask): @app.route("/") def hello(): - # emit a trace that measures how long the - # sleep takes + # Emit a trace that measures how long the sleep takes version = pkg_resources.get_distribution( "opentelemetry-example-app" ).version diff --git a/docs/examples/opentracing/main.py b/docs/examples/opentracing/main.py index 0c331ddee3..8586e0b789 100755 --- a/docs/examples/opentracing/main.py +++ b/docs/examples/opentracing/main.py @@ -3,9 +3,12 @@ from opentelemetry import trace from opentelemetry.ext import opentracing_shim from opentelemetry.ext.jaeger import JaegerSpanExporter +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor from rediscache import RedisCache +# Configure the tracer using the default implementation +trace.set_tracer_provider(TracerProvider()) tracer_provider = trace.get_tracer_provider() # Configure the tracer to export traces to Jaeger diff --git a/examples/metrics/collector.py b/examples/metrics/collector.py index 25bc4ef915..929cfd43f3 100644 --- a/examples/metrics/collector.py +++ b/examples/metrics/collector.py @@ -21,10 +21,11 @@ from opentelemetry.ext.otcollector.metrics_exporter import ( CollectorMetricsExporter, ) -from opentelemetry.sdk.metrics import Counter +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export.controller import PushController # Meter is responsible for creating and recording metrics +metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__) # exporter to export metrics to OT Collector exporter = CollectorMetricsExporter( diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst index 24d4577306..b3f91511c3 100644 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -12,9 +12,12 @@ Usage import mysql.connector import pyodbc - from opentelemetry.trace import tracer_provider + from opentelemetry.ext.dbapi import trace_integration + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.trace import tracer_provider + trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) # Ex: mysql.connector trace_integration(tracer_provider(), mysql.connector, "connect", "mysql", "sql") diff --git a/ext/opentelemetry-ext-jaeger/README.rst b/ext/opentelemetry-ext-jaeger/README.rst index 47549992db..23be60ac68 100644 --- a/ext/opentelemetry-ext-jaeger/README.rst +++ b/ext/opentelemetry-ext-jaeger/README.rst @@ -32,8 +32,10 @@ gRPC is still not supported by this implementation. from opentelemetry import trace from opentelemetry.ext import jaeger + from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) # create a JaegerSpanExporter diff --git a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py index 1f0c303e06..1aca62fc23 100644 --- a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py +++ b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py @@ -2,8 +2,10 @@ from opentelemetry import trace from opentelemetry.ext import jaeger +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor +trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) # create a JaegerSpanExporter diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index 331ed10baf..6e77ac5bc7 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -29,8 +29,12 @@ import time from opentelemetry import trace + from opentelemetry.sdk.trace import TracerProvider from opentelemetry.ext.opentracing_shim import create_tracer + # Tell OpenTelemetry which Tracer implementation to use. + trace.set_tracer_provider(TracerProvider()) + # Create an OpenTelemetry Tracer. otel_tracer = trace.get_tracer(__name__) diff --git a/ext/opentelemetry-ext-prometheus/README.rst b/ext/opentelemetry-ext-prometheus/README.rst index f7049194a2..f9e3d9416b 100644 --- a/ext/opentelemetry-ext-prometheus/README.rst +++ b/ext/opentelemetry-ext-prometheus/README.rst @@ -29,7 +29,7 @@ The **OpenTelemetry Prometheus Exporter** allows to export `OpenTelemetry`_ metr from opentelemetry import metrics from opentelemetry.ext.prometheus import PrometheusMetricsExporter - from opentelemetry.sdk.metrics import Counter + from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export.controller import PushController from prometheus_client import start_http_server @@ -37,6 +37,7 @@ The **OpenTelemetry Prometheus Exporter** allows to export `OpenTelemetry`_ metr start_http_server(port=8000, addr="localhost") # Meter is responsible for creating and recording metrics + metrics.set_meter_provider(MeterProvider()) meter = metrics.meter() # exporter to export metrics to Prometheus prefix = "MyAppPrefix" diff --git a/ext/opentelemetry-ext-psycopg2/README.rst b/ext/opentelemetry-ext-psycopg2/README.rst index 3cf4cf110b..00222a4013 100644 --- a/ext/opentelemetry-ext-psycopg2/README.rst +++ b/ext/opentelemetry-ext-psycopg2/README.rst @@ -13,8 +13,10 @@ Usage import psycopg2 from opentelemetry import trace + from opentelemetry.sdk.trace import TracerProvider from opentelemetry.trace.ext.psycopg2 import trace_integration + trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) trace_integration(tracer) cnx = psycopg2.connect(database='Database') diff --git a/ext/opentelemetry-ext-zipkin/README.rst b/ext/opentelemetry-ext-zipkin/README.rst index 9c88e45060..af119fb410 100644 --- a/ext/opentelemetry-ext-zipkin/README.rst +++ b/ext/opentelemetry-ext-zipkin/README.rst @@ -30,8 +30,10 @@ This exporter always send traces to the configured Zipkin collector using HTTP. from opentelemetry import trace from opentelemetry.ext import zipkin + from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) # create a ZipkinSpanExporter From 71c0c83511cfe7ad638a791b72b146bfaeac84e1 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Sat, 14 Mar 2020 15:29:34 -0700 Subject: [PATCH 0237/1517] Config module doc fixes (#487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the new configuration module from #466 to the generated docs. Co-authored-by: Diego Hurtado Co-authored-by: Mauricio Vásquez --- docs/api/api.rst | 1 + docs/api/configuration.rst | 10 +++++ .../opentelemetry/configuration/__init__.py | 42 ++++++++++--------- 3 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 docs/api/configuration.rst diff --git a/docs/api/api.rst b/docs/api/api.rst index 6ae631147a..8af950652f 100644 --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -6,6 +6,7 @@ OpenTelemetry Python API .. toctree:: :maxdepth: 1 + configuration context metrics trace diff --git a/docs/api/configuration.rst b/docs/api/configuration.rst new file mode 100644 index 0000000000..06ae433277 --- /dev/null +++ b/docs/api/configuration.rst @@ -0,0 +1,10 @@ +opentelemetry.configuration module +================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.configuration + :members: + :undoc-members: + :show-inheritance: diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index 2e6c29aa33..f038e57016 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -19,30 +19,31 @@ Simple configuration manager This is a configuration manager for the Tracer and Meter providers. It reads -configuration from environment variables prefixed with OPENTELEMETRY_PYTHON_: +configuration from environment variables prefixed with +``OPENTELEMETRY_PYTHON_``: -1. OPENTELEMETRY_PYTHON_TRACER_PROVIDER -2. OPENTELEMETRY_PYTHON_METER_PROVIDER +1. ``OPENTELEMETRY_PYTHON_TRACER_PROVIDER`` +2. ``OPENTELEMETRY_PYTHON_METER_PROVIDER`` The value of these environment variables should be the name of the entry point that points to the class that implements either provider. This OpenTelemetry API package provides one entry point for each, which can be found in the -setup.py file: - -entry_points={ - ... - "opentelemetry_meter_provider": [ - "default_meter_provider = " - "opentelemetry.metrics:DefaultMeterProvider" - ], - "opentelemetry_tracer_provider": [ - "default_tracer_provider = " - "opentelemetry.trace:DefaultTracerProvider" - ], -} +setup.py file:: + + entry_points={ + ... + "opentelemetry_meter_provider": [ + "default_meter_provider = " + "opentelemetry.metrics:DefaultMeterProvider" + ], + "opentelemetry_tracer_provider": [ + "default_tracer_provider = " + "opentelemetry.trace:DefaultTracerProvider" + ], + } To use the meter provider above, then the -OPENTELEMETRY_PYTHON_METER_PROVIDER should be set to +``OPENTELEMETRY_PYTHON_METER_PROVIDER`` should be set to "default_meter_provider" (this is not actually necessary since the OpenTelemetry API provided providers are the default ones used if no configuration is found in the environment variables). @@ -52,11 +53,12 @@ be instantiated as many times as needed without concern because it will always produce the same instance. Its attributes are lazy loaded and they hold an instance of their corresponding provider. So, for example, to get -the configured meter provider: +the configured meter provider:: -from opentelemetry.configuration import Configuration + from opentelemetry.configuration import Configuration + + tracer_provider = Configuration().tracer_provider -tracer_provider = Configuration().tracer_provider """ from logging import getLogger From f52468b1b2c3d9bd6a71a203e1f0e99bc1b8eac3 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Sat, 14 Mar 2020 16:11:28 -0700 Subject: [PATCH 0238/1517] Fix versionchanged doc from #466 (#489) --- opentelemetry-api/src/opentelemetry/metrics/__init__.py | 6 +++++- opentelemetry-api/src/opentelemetry/trace/__init__.py | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index c37119529b..8254594282 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -24,7 +24,11 @@ .. _metrics api: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-metrics.md - +.. versionadded:: 0.1.0 +.. versionchanged:: 0.5.0 + ``meter_provider`` was replaced by `get_meter_provider`, + ``set_preferred_meter_provider_implementation`` was replaced by + `set_meter_provider`. """ import abc from logging import getLogger diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index e23404f707..69cca344b6 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -62,8 +62,12 @@ .. versionadded:: 0.1.0 .. versionchanged:: 0.3.0 - `TracerProvider` was introduced and the global ``tracer`` getter was replaced - by `get_tracer_provider`. + `TracerProvider` was introduced and the global ``tracer`` getter was + replaced by ``tracer_provider``. +.. versionchanged:: 0.5.0 + ``tracer_provider`` was replaced by `get_tracer_provider`, + ``set_preferred_tracer_provider_implementation`` was replaced by + `set_tracer_provider`. """ import abc From 264e6c3f00d7581f8627d5099d1fdc2545611044 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 16 Mar 2020 10:32:29 -0700 Subject: [PATCH 0239/1517] Adding Correlation Context API and propagator (#471) This change removes Distributed Context and replaces it with the Correlations Context API. This change also adds the Correlation Context Propagator to the global httptextformat propagator. Fixes #416 Co-authored-by: Diego Hurtado Co-authored-by: Chris Kleinknecht --- .../correlationcontext/__init__.py | 100 ++++++++++++ .../propagation/__init__.py | 108 +++++++++++++ .../distributedcontext/__init__.py | 145 ----------------- .../src/opentelemetry/propagators/__init__.py | 8 +- .../test_correlation_context.py | 70 ++++++++ .../test_correlation_context_propagation.py | 151 ++++++++++++++++++ .../test_distributed_context.py | 112 ------------- .../propagators/test_global_httptextformat.py | 71 ++++++++ .../sdk/distributedcontext/__init__.py | 61 ------- .../propagation/__init__.py | 0 .../__init__.py | 0 .../test_distributed_context.py | 42 ----- 12 files changed, 506 insertions(+), 362 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py create mode 100644 opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py delete mode 100644 opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py create mode 100644 opentelemetry-api/tests/correlationcontext/test_correlation_context.py create mode 100644 opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py delete mode 100644 opentelemetry-api/tests/distributedcontext/test_distributed_context.py create mode 100644 opentelemetry-api/tests/propagators/test_global_httptextformat.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/propagation/__init__.py rename opentelemetry-sdk/tests/{distributedcontext => correlationcontext}/__init__.py (100%) delete mode 100644 opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py diff --git a/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py b/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py new file mode 100644 index 0000000000..b0b3624d72 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py @@ -0,0 +1,100 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import typing + +from opentelemetry.context import get_value, set_value +from opentelemetry.context.context import Context + +_CORRELATION_CONTEXT_KEY = "correlation-context" + + +def get_correlations( + context: typing.Optional[Context] = None, +) -> typing.Dict[str, object]: + """ Returns the name/value pairs in the CorrelationContext + + Args: + context: The Context to use. If not set, uses current Context + + Returns: + Name/value pairs in the CorrelationContext + """ + correlations = get_value(_CORRELATION_CONTEXT_KEY, context=context) + if isinstance(correlations, dict): + return correlations.copy() + return {} + + +def get_correlation( + name: str, context: typing.Optional[Context] = None +) -> typing.Optional[object]: + """ Provides access to the value for a name/value pair in the CorrelationContext + + Args: + name: The name of the value to retrieve + context: The Context to use. If not set, uses current Context + + Returns: + The value associated with the given name, or null if the given name is + not present. + """ + return get_correlations(context=context).get(name) + + +def set_correlation( + name: str, value: object, context: typing.Optional[Context] = None +) -> Context: + """Sets a value in the CorrelationContext + + Args: + name: The name of the value to set + value: The value to set + context: The Context to use. If not set, uses current Context + + Returns: + A Context with the value updated + """ + correlations = get_correlations(context=context) + correlations[name] = value + return set_value(_CORRELATION_CONTEXT_KEY, correlations, context=context) + + +def remove_correlation( + name: str, context: typing.Optional[Context] = None +) -> Context: + """Removes a value from the CorrelationContext + Args: + name: The name of the value to remove + context: The Context to use. If not set, uses current Context + + Returns: + A Context with the name/value removed + """ + correlations = get_correlations(context=context) + correlations.pop(name, None) + + return set_value(_CORRELATION_CONTEXT_KEY, correlations, context=context) + + +def clear_correlations(context: typing.Optional[Context] = None) -> Context: + """Removes all values from the CorrelationContext + Args: + context: The Context to use. If not set, uses current Context + + Returns: + A Context with all correlations removed + """ + return set_value(_CORRELATION_CONTEXT_KEY, {}, context=context) diff --git a/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py new file mode 100644 index 0000000000..72ad80de1a --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py @@ -0,0 +1,108 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import re +import typing +import urllib.parse + +from opentelemetry import correlationcontext +from opentelemetry.context import get_current +from opentelemetry.context.context import Context +from opentelemetry.trace.propagation import httptextformat + + +class CorrelationContextPropagator(httptextformat.HTTPTextFormat): + MAX_HEADER_LENGTH = 8192 + MAX_PAIR_LENGTH = 4096 + MAX_PAIRS = 180 + _CORRELATION_CONTEXT_HEADER_NAME = "otcorrelationcontext" + + def extract( + self, + get_from_carrier: httptextformat.Getter[ + httptextformat.HTTPTextFormatT + ], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + """ Extract CorrelationContext from the carrier. + + See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract` + """ + + if context is None: + context = get_current() + + header = _extract_first_element( + get_from_carrier(carrier, self._CORRELATION_CONTEXT_HEADER_NAME) + ) + + if not header or len(header) > self.MAX_HEADER_LENGTH: + return context + + correlations = header.split(",") + total_correlations = self.MAX_PAIRS + for correlation in correlations: + if total_correlations <= 0: + return context + total_correlations -= 1 + if len(correlation) > self.MAX_PAIR_LENGTH: + continue + try: + name, value = correlation.split("=", 1) + except Exception: # pylint: disable=broad-except + continue + context = correlationcontext.set_correlation( + urllib.parse.unquote(name).strip(), + urllib.parse.unquote(value).strip(), + context=context, + ) + + return context + + def inject( + self, + set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> None: + """Injects CorrelationContext into the carrier. + + See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` + """ + correlations = correlationcontext.get_correlations(context=context) + if not correlations: + return + + correlation_context_string = _format_correlations(correlations) + set_in_carrier( + carrier, + self._CORRELATION_CONTEXT_HEADER_NAME, + correlation_context_string, + ) + + +def _format_correlations(correlations: typing.Dict[str, object]) -> str: + return ",".join( + key + "=" + urllib.parse.quote_plus(str(value)) + for key, value in correlations.items() + ) + + +def _extract_first_element( + items: typing.Iterable[httptextformat.HTTPTextFormatT], +) -> typing.Optional[httptextformat.HTTPTextFormatT]: + if items is None: + return None + return next(iter(items), None) diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py deleted file mode 100644 index dbc7b7e79b..0000000000 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import itertools -import string -import typing -from contextlib import contextmanager - -from opentelemetry.context import attach, get_value, set_value -from opentelemetry.context.context import Context - -PRINTABLE = frozenset( - itertools.chain( - string.ascii_letters, string.digits, string.punctuation, " " - ) -) - - -class EntryMetadata: - """A class representing metadata of a DistributedContext entry - - Args: - entry_ttl: The time to live (in service hops) of an entry. Must be - initially set to either :attr:`EntryMetadata.NO_PROPAGATION` - or :attr:`EntryMetadata.UNLIMITED_PROPAGATION`. - """ - - NO_PROPAGATION = 0 - UNLIMITED_PROPAGATION = -1 - - def __init__(self, entry_ttl: int) -> None: - self.entry_ttl = entry_ttl - - -class EntryKey(str): - """A class representing a key for a DistributedContext entry""" - - def __new__(cls, value: str) -> "EntryKey": - return cls.create(value) - - @staticmethod - def create(value: str) -> "EntryKey": - # pylint: disable=len-as-condition - if not 0 < len(value) <= 255 or any(c not in PRINTABLE for c in value): - raise ValueError("Invalid EntryKey", value) - - return typing.cast(EntryKey, value) - - -class EntryValue(str): - """A class representing the value of a DistributedContext entry""" - - def __new__(cls, value: str) -> "EntryValue": - return cls.create(value) - - @staticmethod - def create(value: str) -> "EntryValue": - if any(c not in PRINTABLE for c in value): - raise ValueError("Invalid EntryValue", value) - - return typing.cast(EntryValue, value) - - -class Entry: - def __init__( - self, metadata: EntryMetadata, key: EntryKey, value: EntryValue - ) -> None: - self.metadata = metadata - self.key = key - self.value = value - - -class DistributedContext: - """A container for distributed context entries""" - - def __init__(self, entries: typing.Iterable[Entry]) -> None: - self._container = {entry.key: entry for entry in entries} - - def get_entries(self) -> typing.Iterable[Entry]: - """Returns an immutable iterator to entries.""" - return self._container.values() - - def get_entry_value(self, key: EntryKey) -> typing.Optional[EntryValue]: - """Returns the entry associated with a key or None - - Args: - key: the key with which to perform a lookup - """ - if key in self._container: - return self._container[key].value - return None - - -class DistributedContextManager: - def get_current_context( - self, context: typing.Optional[Context] = None - ) -> typing.Optional[DistributedContext]: - """Gets the current DistributedContext. - - Returns: - A DistributedContext instance representing the current context. - """ - - @contextmanager # type: ignore - def use_context( - self, context: DistributedContext - ) -> typing.Iterator[DistributedContext]: - """Context manager for controlling a DistributedContext lifetime. - - Set the context as the active DistributedContext. - - On exiting, the context manager will restore the parent - DistributedContext. - - Args: - context: A DistributedContext instance to make current. - """ - # pylint: disable=no-self-use - yield context - - -_DISTRIBUTED_CONTEXT_KEY = "DistributedContext" - - -def distributed_context_from_context( - context: typing.Optional[Context] = None, -) -> DistributedContext: - return get_value(_DISTRIBUTED_CONTEXT_KEY, context) # type: ignore - - -def with_distributed_context( - dctx: DistributedContext, context: typing.Optional[Context] = None -) -> None: - attach(set_value(_DISTRIBUTED_CONTEXT_KEY, dctx, context=context)) diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index f9b537cd86..264fe06b5c 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -58,6 +58,10 @@ def example_route(): import opentelemetry.trace as trace from opentelemetry.context import get_current from opentelemetry.context.context import Context +from opentelemetry.correlationcontext.propagation import ( + CorrelationContextPropagator, +) +from opentelemetry.propagators import composite from opentelemetry.trace.propagation import httptextformat from opentelemetry.trace.propagation.tracecontexthttptextformat import ( TraceContextHTTPTextFormat, @@ -106,8 +110,8 @@ def inject( get_global_httptextformat().inject(set_in_carrier, carrier, context) -_HTTP_TEXT_FORMAT = ( - TraceContextHTTPTextFormat() +_HTTP_TEXT_FORMAT = composite.CompositeHTTPPropagator( + [TraceContextHTTPTextFormat(), CorrelationContextPropagator()], ) # type: httptextformat.HTTPTextFormat diff --git a/opentelemetry-api/tests/correlationcontext/test_correlation_context.py b/opentelemetry-api/tests/correlationcontext/test_correlation_context.py new file mode 100644 index 0000000000..3bddb951e2 --- /dev/null +++ b/opentelemetry-api/tests/correlationcontext/test_correlation_context.py @@ -0,0 +1,70 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import context +from opentelemetry import correlationcontext as cctx + + +class TestCorrelationContextManager(unittest.TestCase): + def test_set_correlation(self): + self.assertEqual({}, cctx.get_correlations()) + + ctx = cctx.set_correlation("test", "value") + self.assertEqual(cctx.get_correlation("test", context=ctx), "value") + + ctx = cctx.set_correlation("test", "value2", context=ctx) + self.assertEqual(cctx.get_correlation("test", context=ctx), "value2") + + def test_correlations_current_context(self): + token = context.attach(cctx.set_correlation("test", "value")) + self.assertEqual(cctx.get_correlation("test"), "value") + context.detach(token) + self.assertEqual(cctx.get_correlation("test"), None) + + def test_set_multiple_correlations(self): + ctx = cctx.set_correlation("test", "value") + ctx = cctx.set_correlation("test2", "value2", context=ctx) + self.assertEqual(cctx.get_correlation("test", context=ctx), "value") + self.assertEqual(cctx.get_correlation("test2", context=ctx), "value2") + self.assertEqual( + cctx.get_correlations(context=ctx), + {"test": "value", "test2": "value2"}, + ) + + def test_modifying_correlations(self): + ctx = cctx.set_correlation("test", "value") + self.assertEqual(cctx.get_correlation("test", context=ctx), "value") + correlations = cctx.get_correlations(context=ctx) + correlations["test"] = "mess-this-up" + self.assertEqual(cctx.get_correlation("test", context=ctx), "value") + + def test_remove_correlations(self): + self.assertEqual({}, cctx.get_correlations()) + + ctx = cctx.set_correlation("test", "value") + ctx = cctx.set_correlation("test2", "value2", context=ctx) + ctx = cctx.remove_correlation("test", context=ctx) + self.assertEqual(cctx.get_correlation("test", context=ctx), None) + self.assertEqual(cctx.get_correlation("test2", context=ctx), "value2") + + def test_clear_correlations(self): + self.assertEqual({}, cctx.get_correlations()) + + ctx = cctx.set_correlation("test", "value") + self.assertEqual(cctx.get_correlation("test", context=ctx), "value") + + ctx = cctx.clear_correlations(context=ctx) + self.assertEqual(cctx.get_correlations(context=ctx), {}) diff --git a/opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py b/opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py new file mode 100644 index 0000000000..d0ecbdb17f --- /dev/null +++ b/opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py @@ -0,0 +1,151 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import typing +import unittest + +from opentelemetry import correlationcontext +from opentelemetry.context import get_current +from opentelemetry.correlationcontext.propagation import ( + CorrelationContextPropagator, +) + + +def get_as_list( + dict_object: typing.Dict[str, typing.List[str]], key: str +) -> typing.List[str]: + return dict_object.get(key, []) + + +class TestCorrelationContextPropagation(unittest.TestCase): + def setUp(self): + self.propagator = CorrelationContextPropagator() + + def _extract(self, header_value): + """Test helper""" + header = {"otcorrelationcontext": [header_value]} + return correlationcontext.get_correlations( + self.propagator.extract(get_as_list, header) + ) + + def _inject(self, values): + """Test helper""" + ctx = get_current() + for k, v in values.items(): + ctx = correlationcontext.set_correlation(k, v, context=ctx) + output = {} + self.propagator.inject(dict.__setitem__, output, context=ctx) + return output.get("otcorrelationcontext") + + def test_no_context_header(self): + correlations = correlationcontext.get_correlations( + self.propagator.extract(get_as_list, {}) + ) + self.assertEqual(correlations, {}) + + def test_empty_context_header(self): + header = "" + self.assertEqual(self._extract(header), {}) + + def test_valid_header(self): + header = "key1=val1,key2=val2" + expected = {"key1": "val1", "key2": "val2"} + self.assertEqual(self._extract(header), expected) + + def test_valid_header_with_space(self): + header = "key1 = val1, key2 =val2 " + expected = {"key1": "val1", "key2": "val2"} + self.assertEqual(self._extract(header), expected) + + def test_valid_header_with_properties(self): + header = "key1=val1,key2=val2;prop=1" + expected = {"key1": "val1", "key2": "val2;prop=1"} + self.assertEqual(self._extract(header), expected) + + def test_valid_header_with_url_escaped_comma(self): + header = "key%2C1=val1,key2=val2%2Cval3" + expected = {"key,1": "val1", "key2": "val2,val3"} + self.assertEqual(self._extract(header), expected) + + def test_valid_header_with_invalid_value(self): + header = "key1=val1,key2=val2,a,val3" + expected = {"key1": "val1", "key2": "val2"} + self.assertEqual(self._extract(header), expected) + + def test_valid_header_with_empty_value(self): + header = "key1=,key2=val2" + expected = {"key1": "", "key2": "val2"} + self.assertEqual(self._extract(header), expected) + + def test_invalid_header(self): + header = "header1" + expected = {} + self.assertEqual(self._extract(header), expected) + + def test_header_too_long(self): + long_value = "s" * (CorrelationContextPropagator.MAX_HEADER_LENGTH + 1) + header = "key1={}".format(long_value) + expected = {} + self.assertEqual(self._extract(header), expected) + + def test_header_contains_too_many_entries(self): + header = ",".join( + [ + "key{}=val".format(k) + for k in range(CorrelationContextPropagator.MAX_PAIRS + 1) + ] + ) + self.assertEqual( + len(self._extract(header)), CorrelationContextPropagator.MAX_PAIRS + ) + + def test_header_contains_pair_too_long(self): + long_value = "s" * (CorrelationContextPropagator.MAX_PAIR_LENGTH + 1) + header = "key1=value1,key2={},key3=value3".format(long_value) + expected = {"key1": "value1", "key3": "value3"} + self.assertEqual(self._extract(header), expected) + + def test_inject_no_correlations(self): + values = {} + output = self._inject(values) + self.assertEqual(None, output) + + def test_inject(self): + values = { + "key1": "val1", + "key2": "val2", + } + output = self._inject(values) + self.assertIn("key1=val1", output) + self.assertIn("key2=val2", output) + + def test_inject_escaped_values(self): + values = { + "key1": "val1,val2", + "key2": "val3=4", + } + output = self._inject(values) + self.assertIn("key1=val1%2Cval2", output) + self.assertIn("key2=val3%3D4", output) + + def test_inject_non_string_values(self): + values = { + "key1": True, + "key2": 123, + "key3": 123.567, + } + output = self._inject(values) + self.assertIn("key1=True", output) + self.assertIn("key2=123", output) + self.assertIn("key3=123.567", output) diff --git a/opentelemetry-api/tests/distributedcontext/test_distributed_context.py b/opentelemetry-api/tests/distributedcontext/test_distributed_context.py deleted file mode 100644 index c730603b16..0000000000 --- a/opentelemetry-api/tests/distributedcontext/test_distributed_context.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -from opentelemetry import distributedcontext - - -class TestEntryMetadata(unittest.TestCase): - def test_entry_ttl_no_propagation(self): - metadata = distributedcontext.EntryMetadata( - distributedcontext.EntryMetadata.NO_PROPAGATION - ) - self.assertEqual(metadata.entry_ttl, 0) - - def test_entry_ttl_unlimited_propagation(self): - metadata = distributedcontext.EntryMetadata( - distributedcontext.EntryMetadata.UNLIMITED_PROPAGATION - ) - self.assertEqual(metadata.entry_ttl, -1) - - -class TestEntryKey(unittest.TestCase): - def test_create_empty(self): - with self.assertRaises(ValueError): - distributedcontext.EntryKey.create("") - - def test_create_too_long(self): - with self.assertRaises(ValueError): - distributedcontext.EntryKey.create("a" * 256) - - def test_create_invalid_character(self): - with self.assertRaises(ValueError): - distributedcontext.EntryKey.create("\x00") - - def test_create_valid(self): - key = distributedcontext.EntryKey.create("ok") - self.assertEqual(key, "ok") - - def test_key_new(self): - key = distributedcontext.EntryKey("ok") - self.assertEqual(key, "ok") - - -class TestEntryValue(unittest.TestCase): - def test_create_invalid_character(self): - with self.assertRaises(ValueError): - distributedcontext.EntryValue.create("\x00") - - def test_create_valid(self): - key = distributedcontext.EntryValue.create("ok") - self.assertEqual(key, "ok") - - def test_key_new(self): - key = distributedcontext.EntryValue("ok") - self.assertEqual(key, "ok") - - -class TestDistributedContext(unittest.TestCase): - def setUp(self): - entry = self.entry = distributedcontext.Entry( - distributedcontext.EntryMetadata( - distributedcontext.EntryMetadata.NO_PROPAGATION - ), - distributedcontext.EntryKey("key"), - distributedcontext.EntryValue("value"), - ) - self.context = distributedcontext.DistributedContext((entry,)) - - def test_get_entries(self): - self.assertIn(self.entry, self.context.get_entries()) - - def test_get_entry_value_present(self): - value = self.context.get_entry_value(self.entry.key) - self.assertIs(value, self.entry.value) - - def test_get_entry_value_missing(self): - key = distributedcontext.EntryKey("missing") - value = self.context.get_entry_value(key) - self.assertIsNone(value) - - -class TestDistributedContextManager(unittest.TestCase): - def setUp(self): - self.manager = distributedcontext.DistributedContextManager() - - def test_get_current_context(self): - self.assertIsNone(self.manager.get_current_context()) - - def test_use_context(self): - expected = distributedcontext.DistributedContext( - ( - distributedcontext.Entry( - distributedcontext.EntryMetadata(0), - distributedcontext.EntryKey("0"), - distributedcontext.EntryValue(""), - ), - ) - ) - with self.manager.use_context(expected) as output: - self.assertIs(output, expected) diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py new file mode 100644 index 0000000000..6045feb675 --- /dev/null +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -0,0 +1,71 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing +import unittest + +from opentelemetry import correlationcontext, trace +from opentelemetry.propagators import extract, inject +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, +) + + +def get_as_list( + dict_object: typing.Dict[str, typing.List[str]], key: str +) -> typing.List[str]: + value = dict_object.get(key) + return value if value is not None else [] + + +class TestDefaultGlobalPropagator(unittest.TestCase): + """Test ensures the default global composite propagator works as intended + """ + + TRACE_ID = int("12345678901234567890123456789012", 16) # type:int + SPAN_ID = int("1234567890123456", 16) # type:int + + def test_propagation(self): + traceparent_value = "00-{trace_id}-{span_id}-00".format( + trace_id=format(self.TRACE_ID, "032x"), + span_id=format(self.SPAN_ID, "016x"), + ) + tracestate_value = "foo=1,bar=2,baz=3" + headers = { + "otcorrelationcontext": ["key1=val1,key2=val2"], + "traceparent": [traceparent_value], + "tracestate": [tracestate_value], + } + ctx = extract(get_as_list, headers) + correlations = correlationcontext.get_correlations(context=ctx) + expected = {"key1": "val1", "key2": "val2"} + self.assertEqual(correlations, expected) + span_context = get_span_from_context(context=ctx).get_context() + + self.assertEqual(span_context.trace_id, self.TRACE_ID) + self.assertEqual(span_context.span_id, self.SPAN_ID) + + span = trace.DefaultSpan(span_context) + ctx = correlationcontext.set_correlation("key3", "val3") + ctx = correlationcontext.set_correlation("key4", "val4", context=ctx) + ctx = set_span_in_context(span, context=ctx) + output = {} + inject(dict.__setitem__, output, context=ctx) + self.assertEqual(traceparent_value, output["traceparent"]) + self.assertIn("key3=val3", output["otcorrelationcontext"]) + self.assertIn("key4=val4", output["otcorrelationcontext"]) + self.assertIn("foo=1", output["tracestate"]) + self.assertIn("bar=2", output["tracestate"]) + self.assertIn("baz=3", output["tracestate"]) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py deleted file mode 100644 index 7a0a66a8a9..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import typing -from contextlib import contextmanager - -from opentelemetry import distributedcontext as dctx_api -from opentelemetry.context import Context, get_value, set_value -from opentelemetry.distributedcontext import ( - distributed_context_from_context, - with_distributed_context, -) - - -class DistributedContextManager(dctx_api.DistributedContextManager): - """See `opentelemetry.distributedcontext.DistributedContextManager` - - """ - - def get_current_context( - self, context: typing.Optional[Context] = None - ) -> typing.Optional[dctx_api.DistributedContext]: - """Gets the current DistributedContext. - - Returns: - A DistributedContext instance representing the current context. - """ - return distributed_context_from_context(context=context) - - @contextmanager - def use_context( - self, context: dctx_api.DistributedContext - ) -> typing.Iterator[dctx_api.DistributedContext]: - """Context manager for controlling a DistributedContext lifetime. - - Set the context as the active DistributedContext. - - On exiting, the context manager will restore the parent - DistributedContext. - - Args: - context: A DistributedContext instance to make current. - """ - snapshot = distributed_context_from_context() - with_distributed_context(context) - - try: - yield context - finally: - with_distributed_context(snapshot) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/propagation/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/propagation/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry-sdk/tests/distributedcontext/__init__.py b/opentelemetry-sdk/tests/correlationcontext/__init__.py similarity index 100% rename from opentelemetry-sdk/tests/distributedcontext/__init__.py rename to opentelemetry-sdk/tests/correlationcontext/__init__.py diff --git a/opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py b/opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py deleted file mode 100644 index eddb61330d..0000000000 --- a/opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -from opentelemetry import distributedcontext as dctx_api -from opentelemetry.sdk import distributedcontext - - -class TestDistributedContextManager(unittest.TestCase): - def setUp(self): - self.manager = distributedcontext.DistributedContextManager() - - def test_use_context(self): - # Context is None initially - self.assertIsNone(self.manager.get_current_context()) - - # Start initial context - dctx = dctx_api.DistributedContext(()) - with self.manager.use_context(dctx) as current: - self.assertIs(current, dctx) - self.assertIs(self.manager.get_current_context(), dctx) - - # Context is overridden - nested_dctx = dctx_api.DistributedContext(()) - with self.manager.use_context(nested_dctx) as current: - self.assertIs(current, nested_dctx) - self.assertIs(self.manager.get_current_context(), nested_dctx) - - # Context is restored - self.assertIs(self.manager.get_current_context(), dctx) From 8d951ee10313e113317628755e73a9067f8301f3 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 16 Mar 2020 12:25:23 -0700 Subject: [PATCH 0240/1517] Feature/getting started (#494) As a requirement for the opentelemetry beta, providing a documentation page that runs through the use of traces, metrics, and common exporters. In the future, the code snippets in the page will be reconciled to use existing snippets, after some significant refactors around documentation is merged in. Fixes #491 Co-authored-by: Chris Kleinknecht --- .../flask_example.py | 39 +- docs/getting-started.rst | 468 ++++++++++++++++++ docs/images/jaeger_trace.png | Bin 0 -> 156601 bytes docs/index.rst | 23 +- docs/trace_example.py | 4 +- examples/metrics/docker/prometheus.yaml | 2 +- 6 files changed, 486 insertions(+), 50 deletions(-) create mode 100644 docs/getting-started.rst create mode 100644 docs/images/jaeger_trace.png diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 89933be190..f8a4997392 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -17,50 +17,31 @@ the requests library to perform downstream requests """ import flask -import pkg_resources import requests import opentelemetry.ext.http_requests from opentelemetry import trace from opentelemetry.ext.flask import instrument_app from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ConsoleSpanExporter +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor - -def configure_opentelemetry(flask_app: flask.Flask): - """Configure a flask application to use OpenTelemetry. - - This activates the specific components: - - * sets tracer to the SDK's Tracer - * enables requests integration on the Tracer - * uses a WSGI middleware to enable configuration - """ - # Start by configuring all objects required to ensure a complete end to end - # workflow. - trace.set_tracer_provider(TracerProvider()) - - # Next, we need to configure how the values that are used by traces and - # metrics are propagated (such as what specific headers carry this value). - # Integrations are the glue that binds the OpenTelemetry API and the - # frameworks and libraries that are used together, automatically creating - # Spans and propagating context as appropriate. - opentelemetry.ext.http_requests.enable(trace.get_tracer_provider()) - instrument_app(flask_app) - +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) app = flask.Flask(__name__) +opentelemetry.ext.http_requests.enable(trace.get_tracer_provider()) +instrument_app(app) @app.route("/") def hello(): - # Emit a trace that measures how long the sleep takes - version = pkg_resources.get_distribution( - "opentelemetry-example-app" - ).version - tracer = trace.get_tracer(__name__, version) + tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("example-request"): requests.get("http://www.example.com") return "hello" -configure_opentelemetry(app) +app.run(debug=True) diff --git a/docs/getting-started.rst b/docs/getting-started.rst new file mode 100644 index 0000000000..08d6e300d6 --- /dev/null +++ b/docs/getting-started.rst @@ -0,0 +1,468 @@ +Getting Started with OpenTelemetry Python +========================================= + +This guide will walk you through instrumenting a Python application with ``opentelemetry-python``. + +For more elaborate examples, see `examples`. + +Hello world: emitting a trace to your console +--------------------------------------------- + +To get started, install both the opentelemetry API and SDK: + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + +The API package provides the interfaces required by the application owner, as well +as some helper logic to load implementations. + +The SDK provides an implementation of those interfaces, designed to be generic and extensible enough +that in many situations, the SDK will be sufficient. + +Once installed, we can now utilize the packages to emit spans from your application. A span +represents an action within your application that you want to instrument, such as an HTTP request +or a database call. Once instrumented, the application owner can extract helpful information such as +how long the action took, or add arbitrary attributes to the span that may provide more insight for debugging. + +Here's an example of a script that emits a trace containing three named spans: "foo", "bar", and "baz": + +.. code-block:: python + + # tracing.py + from opentelemetry import trace + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import ConsoleSpanExporter + from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor + + trace.set_tracer_provider(TracerProvider()) + trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) + ) + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span('foo'): + with tracer.start_as_current_span('bar'): + with tracer.start_as_current_span('baz'): + print("Hello world from OpenTelemetry Python!") + +We can run it, and see the traces print to your console: + +.. code-block:: sh + + $ python tracing.py + Hello world from OpenTelemetry Python! + Span(name="baz", + context=SpanContext(trace_id=0x37c1b154d9ab5a4b94b0046484b90400, + span_id=0xfacbb82a4d0cf5dd, + trace_state={} + ), + kind=SpanKind.INTERNAL, + parent=Span(name="bar", + context=SpanContext(trace_id=0x37c1b154d9ab5a4b94b0046484b90400, + span_id=0xb1894e8d588f5f62, + trace_state={}) + ), + start_time=2020-03-15T05:12:08.345394Z, + end_time=2020-03-15T05:12:08.345450Z + ) + Span(name="bar", + context=SpanContext(trace_id=0x37c1b154d9ab5a4b94b0046484b90400, + span_id=0xb1894e8d588f5f62, + trace_state={} + ), + kind=SpanKind.INTERNAL, + parent=Span(name="foo", + context=SpanContext(trace_id=0x37c1b154d9ab5a4b94b0046484b90400, + span_id=0xde5ea23d6a9e4180, + trace_state={}) + ), + start_time=2020-03-15T05:12:08.345360Z, + end_time=2020-03-15T05:12:08.345597Z + ) + Span(name="foo", + context=SpanContext(trace_id=0x37c1b154d9ab5a4b94b0046484b90400, + span_id=0xde5ea23d6a9e4180, + trace_state={} + ), + kind=SpanKind.INTERNAL, + parent=None, + start_time=2020-03-15T05:12:08.345287Z, + end_time=2020-03-15T05:12:08.345673Z + ) + + +Each span typically represents a single operation or unit of work. +Spans can be nested, and have a parent-child relationship with other spans. +While a given span is active, newly-created spans will inherit the active span's trace ID, options, and other attributes of its context. +A span without a parent is called the "root span", and a trace is comprised of one root span and its descendants. + +In the example above, the OpenTelemetry Python library creates one trace containing three spans and prints it to STDOUT. + +Configure exporters to emit spans elsewhere +------------------------------------------- + +The example above does emit information about all spans, but the output is a bit hard to read. +In common cases, you would instead *export* this data to an application performance monitoring backend, to be visualized and queried. +It is also common to aggregate span and trace information from multiple services into a single database, so that actions that require multiple services can still all be visualized together. + +This concept is known as distributed tracing. One such distributed tracing backend is known as Jaeger. + +The Jaeger project provides an all-in-one docker container that provides a UI, database, and consumer. Let's bring +it up now: + +.. code-block:: sh + + docker run -p 16686:16686 -p 6831:6831/udp jaegertracing/all-in-one + +This will start Jaeger on port 16686 locally, and expose Jaeger thrift agent on port 6831. You can visit it at http://localhost:16686. + +With this backend up, your application will now need to export traces to this system. ``opentelemetry-sdk`` does not provide an exporter +for Jaeger, but you can install that as a separate package: + +.. code-block:: sh + + pip install opentelemetry-ext-jaeger + +Once installed, update your code to import the Jaeger exporter, and use that instead: + +.. code-block:: python + + # jaeger-example.py + from opentelemetry import trace + from opentelemetry.ext import jaeger + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor + + trace.set_tracer_provider(TracerProvider()) + + jaeger_exporter = jaeger.JaegerSpanExporter( + service_name="my-helloworld-service", agent_host_name="localhost", agent_port=6831 + ) + + trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(jaeger_exporter) + ) + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span('foo'): + with tracer.start_as_current_span('bar'): + with tracer.start_as_current_span('baz'): + print("Hello world from OpenTelemetry Python!") + +Run the script: + +.. code-block:: python + + python jaeger-example.py + +You can then visit the jaeger UI, see you service under "services", and find your traces! + +.. image:: images/jaeger_trace.png + +Integrations example with Flask +------------------------------- + +The above is a great example, but it's very manual. Within the telemetry space, there are common actions that one wants to instrument: + +* HTTP responses from web services +* HTTP requests from clients +* Database calls + +To help instrument common scenarios, opentelemetry also has the concept of "instrumentations": packages that are designed to interface +with a specific framework or library, such as Flask and psycopg2. A list of the currently curated extension packages can be found :scm_web:`here `. + +We will now instrument a basic Flask application that uses the requests library to send HTTP requests. First, install the instrumentation packages themselves: + +.. code-block:: sh + + pip install opentelemetry-ext-flask + pip install opentelemetry-ext-http-requests + + +And let's write a small Flask application that sends an HTTP request, activating each instrumentation during the initialization: + +.. code-block:: python + + # flask_example.py + import flask + import requests + + import opentelemetry.ext.http_requests + from opentelemetry import trace + from opentelemetry.ext.flask import instrument_app + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import ConsoleSpanExporter + from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor + + trace.set_tracer_provider(TracerProvider()) + trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) + ) + + app = flask.Flask(__name__) + opentelemetry.ext.http_requests.enable(trace.get_tracer_provider()) + instrument_app(app) + + @app.route("/") + def hello(): + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span("example-request"): + requests.get("http://www.example.com") + return "hello" + + app.run(debug=True, port=5000) + + +Now run the above script, hit the root url (http://localhost:5000/) a few times, and watch your spans be emitted! + +.. code-block:: sh + + python flask_example.py + + +Adding Metrics +-------------- + +Spans are a great way to get detailed information about what your application is doing, but +what about a more aggregated perspective? OpenTelemetry provides supports for metrics, a time series +of numbers that might express things such as CPU utilization, request count for an HTTP server, or a +business metric such as transactions. + +All metrics can be annotated with labels: additional qualifiers that help describe what +subdivision of the measurements the metric represents. + +The following is an example of emitting metrics to console, in a similar fashion to the trace example: + +.. code-block:: python + + # metrics.py + import sys + import time + + from opentelemetry import metrics + from opentelemetry.sdk.metrics import Counter, MeterProvider + from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter + from opentelemetry.sdk.metrics.export.controller import PushController + + metrics.set_meter_provider(MeterProvider()) + meter = metrics.get_meter(__name__, True) + exporter = ConsoleMetricsExporter() + controller = PushController(meter, exporter, 5) + + staging_label_set = meter.get_label_set({"environment": "staging"}) + + requests_counter = meter.create_metric( + name="requests", + description="number of requests", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), + ) + + requests_counter.add(25, staging_label_set) + time.sleep(5) + + requests_counter.add(20, staging_label_set) + time.sleep(5) + + +The sleeps will cause the script to take a while, but running it should yield: + +.. code-block:: sh + + $ python metrics.py + ConsoleMetricsExporter(data="Counter(name="requests", description="number of requests")", label_set="(('environment', 'staging'),)", value=25) + ConsoleMetricsExporter(data="Counter(name="requests", description="number of requests")", label_set="(('environment', 'staging'),)", value=45) + +Using Prometheus +---------------- + +Similar to traces, it is really valuable for metrics to have its own data store to help visualize and query the data. A common solution for this is +`Prometheus `_. + +Let's start by bringing up a Prometheus instance ourselves, to scrape our application. Write the following configuration: + +.. code-block:: yaml + + # prometheus.yml + scrape_configs: + - job_name: 'my-app' + scrape_interval: 5s + static_configs: + - targets: ['localhost:8000'] + +And start a docker container for it: + +.. code-block:: sh + + # --net=host will not work properly outside of Linux. + docker run --net=host -v ./prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus\ + --log.level=debug --config.file=/etc/prometheus/prometheus.yml + +For our Python application, we will need to install an exporter specific to Prometheus: + +.. code-block:: sh + + pip install opentelemetry-ext-prometheus + + +And use that instead of the `ConsoleMetricsExporter`: + +.. code-block:: python + + # prometheus.py + import sys + import time + + from opentelemetry import metrics + from opentelemetry.ext.prometheus import PrometheusMetricsExporter + from opentelemetry.sdk.metrics import Counter, MeterProvider + from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter + from opentelemetry.sdk.metrics.export.controller import PushController + from prometheus_client import start_http_server + + # Start Prometheus client + start_http_server(port=8000, addr="localhost") + + batcher_mode = "stateful" + metrics.set_meter_provider(MeterProvider()) + meter = metrics.get_meter(__name__, batcher_mode == "stateful") + exporter = PrometheusMetricsExporter("MyAppPrefix") + controller = PushController(meter, exporter, 5) + + staging_label_set = meter.get_label_set({"environment": "staging"}) + + requests_counter = meter.create_metric( + name="requests", + description="number of requests", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), + ) + + requests_counter.add(25, staging_label_set) + time.sleep(5) + + requests_counter.add(20, staging_label_set) + time.sleep(5) + + # This line is added to keep the HTTP server up long enough to scrape. + input("Press any key to exit...") + + +The Prometheus server will run locally on port 8000, and the instrumented code will make metrics available to Prometheus via the `PrometheusMetricsExporter`. +Visit the Prometheus UI (http://localhost:9090) to view your metrics. + + +Using the OpenTelemetry Collector for traces and metrics +-------------------------------------------------------- + +Although it's possible to directly export your telemetry data to specific backends, you may more complex use cases, including: + +* having a single telemetry sink shared by multiple services, to reduce overhead of switching exporters +* aggregating metrics or traces across multiple services, running on multiple hosts + +To enable a broad range of aggregation strategies, OpenTelemetry provides the `opentelemetry-collector `_. +The Collector is a flexible application that can consume trace and metric data and export to multiple other backends, including to another instance of the Collector. + +To see how this works in practice, let's start the Collector locally. Write the following file: + +.. code-block:: yaml + + # otel-collector-config.yaml + receivers: + opencensus: + endpoint: 0.0.0.0:55678 + exporters: + logging: + loglevel: debug + sampling_initial: 10 + sampling_thereafter: 50 + processors: + batch: + queued_retry: + service: + pipelines: + traces: + receivers: [opencensus] + exporters: [logging] + processors: [batch, queued_retry] + metrics: + receivers: [opencensus] + exporters: [logging] + +Start the docker container: + +.. code-block:: sh + + docker run -p 55678:55678\ + -v ./otel-collector-config.yaml:/etc/otel-collector-config.yaml\ + omnition/opentelemetry-collector-contrib:latest \ + --config=/etc/otel-collector-config.yaml + +Install the OpenTelemetry Collector exporter: + +.. code-block:: sh + + pip install opentelemetry-ext-otcollector + +And execute the following script: + +.. code-block:: python + + # otcollector.py + import time + from opentelemetry import trace + from opentelemetry.ext.otcollector.trace_exporter import CollectorSpanExporter + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + from opentelemetry import metrics + from opentelemetry.ext.otcollector.metrics_exporter import CollectorMetricsExporter + from opentelemetry.sdk.metrics import Counter, MeterProvider + from opentelemetry.sdk.metrics.export.controller import PushController + + + # create a CollectorSpanExporter + span_exporter = CollectorSpanExporter( + # optional: + # endpoint="myCollectorUrl:55678", + # service_name="test_service", + # host_name="machine/container name", + ) + tracer_provider = TracerProvider() + trace.set_tracer_provider(tracer_provider) + tracer_provider.add_span_processor(span_processor) + + # create a CollectorMetricsExporter + metric_exporter = CollectorMetricsExporter( + # optional: + # endpoint="myCollectorUrl:55678", + # service_name="test_service", + # host_name="machine/container name", + ) + + # Meter is responsible for creating and recording metrics + metrics.set_meter_provider(MeterProvider()) + meter = metrics.get_meter(__name__) + # controller collects metrics created from meter and exports it via the + # exporter every interval + controller = PushController(meter, collector_exporter, 5) + + # Configure the tracer to use the collector exporter + tracer = trace.get_tracer_provider().get_tracer(__name__) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + + counter = meter.create_metric( + "requests", "number of requests", "requests", int, Counter, ("environment",), + ) + # Labelsets are used to identify key-values that are associated with a specific + # metric that you want to record. These are useful for pre-aggregation and can + # be used to store custom dimensions pertaining to a metric + label_set = meter.get_label_set({"environment": "staging"}) + + counter.add(25, label_set) + time.sleep(10) # give push_controller time to push metrics diff --git a/docs/images/jaeger_trace.png b/docs/images/jaeger_trace.png new file mode 100644 index 0000000000000000000000000000000000000000..83afbe1293b8c1d50611eb1d8f4686da601881c2 GIT binary patch literal 156601 zcmbrlbzD^6x<5`yO9&DoLrEwdk~4HENJvYAlyna*(k;?8Ac7zwEg{|ADkCxEFmy9C z{5GF+?z#7#?;oFgUO!(iWHWoOwby#${k+%nJQJm%rgWF^0U-tk#@%Pi@-Hzk@X;6; zSjG6bz$e!O%Pbfe#Lw*H0XAfEm7gW*dyJOwb&~ ztekdFHinT=r-J;0v}_bVQzSf5;m#BfvzeTp0;Mq#qkT6*W9!OX#>&c=3$Edlg!RK0 z;fK0H4K>eSp>lV5y;l*m7#<})FYQQ5h@{INsZ&vdh8|>n7+{8A;!9&vRA6*04;F60 z;T#x+H`nSu-q^3?8aDD*&Kqu8qhZnuUW&r+L*2+U_(JJpGjQ~pc&ZuIWf=sm@FEEB1E=txO#6= zXCErGl1jjBnGNK#1#I-|4F4ptq@?4zragj)<5~nnN=4pz;uon|qD-DxT&wir%#Ea! z)_)Yk^ZuSJGm~(bsOkp_`oi%do5;?3vQFA}RFQ}E1eujYp$B8oyEfk)+>BJEM&7%^ zGb@k583W}L*P+Z3iQg7mS?{@BoXt~3-}~^3-}0xw>r0`oX);0NpZ&x)bVHvtx}tXK z5+5%m@6SkYaVCf2eLbs;qOGbBjcex8L*(+-Nsgbvxs@CTVo2 zZK22qdKHgR%w*xACGNdEM66Mn{*_*KCNA05TlRKpr9Ge`LidgoU(U!E$g};-N~km_ z`~90h@T*Mfth{&%_0grcXr0~=>bD$im`pC+kIhu@l4PjJ>b#zMwX=Jw(=1@p_Tk7b zCdDwjy_xhWYJ$14}@=`K*Ne&%)!b2F80UZ8l+V zl3%wO-h*O2UCM1bG%ppaczhA~yYeI3^G}*bhgBZu+)*F9$SiR9W7i_H7BK4>KzGVz zDjF6M_3A-2^IHV!1dgFfp_`#z9ox^AnE#NTRkYcTkw8N2+|+I)ZDvWTd>7j5rB)(l zMJYruc~0@yqT=vm=Rq2SeafV{HoU>L)dd&}1v342%eZ*2q$` zzGiV^4BSMX3m%7mIhSG_a-a7&T*y2!ot>KU-pk@~6UXrL#mFAbIsG2*bw z`TFb)eOVJ3#{IRn`&Pt5!B1V@p4DTjFW|kC=AgM_A&sSsNnxI%;37HLrp<>vbcZ#M znCs40X;ytw;b2EIb~yHfb{QAqY%El}l?(lBSalJ06UN*E{h9(vlSQpb{~ zprmWpt6t(*^Wg=)(^#L_;n<{D672loX3QzZ!Mq`}BAKr;bst+6@jD-NVs$?0lndWh zjkW7t5pYjeN%u<^NzcX2xNAXrkK~nTao|{r%x)Ym-5pwjxaVjyUCa~?_ScP2rC?$s`W zU;aF(UbZ$~RXt=p5vWQ8cQsTOPOxH zN^O?&lwf5!FOvK9N!{?2DB+DMOnT2HhG zl|;Fx6P+y0lAydmfpy;t>P(^;*7?E_%Il73Jht`5zp71L3qKE*`W0zsq zxh-Gs`s+Q-&X{l!(xC0tl^*Y}ZjGyI-lXoi+lZCxH=&34i;!p2(vz*Ds{QAOCFmdPjtlgg&3zZKr0wi*o7z#C+=Lcbi%*N6{|(xGe-J&hbK+lc zrM%cT%))SCcy@Rxa$bj5fi;08j8lX63iklZ9*g0Q0Up7-S{#U_L{&9h0?r#^9Aede z5i;-YFD59aBvq18RnkmbBmzG$<72tGG=C$RE61@`!BX;b&_W$uvtkj zI6T90v?#qGQY-W2voFuS7{2uD35z#;T4veQd-`Etgbl9Fk?!J^-`|xT6~VTyy7Xc? zOHo-xZ3c$Upzi#-cwFE<`E|1K?BVsBYa*HVW`%iLYrIB0{;7+(mEdKc&q1rvLu#6N z#YR6_S0g#Q%d42He%myU#`hyK=hBk0t2xuMvr_$}^LhJIv(p_U8_e9Yej2=DcFbLp zu=tT|?&-|f%Am7hs-CdrMzFxG5}?Zo7}uc*U;{@`*%4FI92pojGx;5`g1^z+|<+3 zHKAoUtu^zI>rB!tN+}m389$-B7hQ+3~DOr|Nrv;cP9%ca!hmXI|9tev#Xx^vsdy z*{yF9d~KuEta8<3JY;Ou*zVN5BNe26an%FsgK+ybpZwmfp3`g2t!%Bqv`hazHplVC z!TGt}8wtJ*X_acz!)vMV>&oLiWP6ERctq;M&mbPjS?_{9WFA*5wXU2pNV z{`K!4>$HN}{r8)k-2b^PV1vB3cX;`E_;~*{H!u`@`>D8w9n{J}U*68q%E=vghopdj zm=O4{0sqgf|9<8F7;5m}Lxn~8{^!X5ap#{S!MwLS_#Zp^hq(Uw6p)uBA(;1HvX>-W z(Bo_Z)bY?xUR@jbjdiOVKy$a(9RK(YTw`h|l0NN&VPHsOJd>BvhGK5z5M*;J_|)0g zJ777z4Q~{%7~>Gp)Fdb-H_BhSmtSwJ$r2s?V24Tun{bB?H%uL;!J+5QQ>Ws{NFti$ zchUJ>tn#wbGL)L+cTYO3T6`WLEMKUf{23h*bBCRrWQn0X^z`(kjE#*4TM?Q14Q|Mv z-=|Engb0+SvF^Qpi}Q#P^Z#&Z7D#aUC-=(WRkihtSCtkDnHq4xR*}wXmH%WCMtsIC zgJxgP%nG_@B9jK@I~1!L9$t#s6cC@7{v^-i7A= z-}ej*qhZFv_xoLHkd(@8__-Ix^n;g8;Xjeu5CO2`I;Wft82>1qNy~q*-nRGTW0s@u z=g5*+{xACZml|$G-00=_s>Q!S#WjQV|EAr4j^^XSIf4svd3SjHN4CMp^0va}c;lB{ zcl3SVt^WE`o&P{Y(pZPj+Y0Mf@3*q2{U_!i{WO>i&1Fy}p8)w4Is4>4kx_61JFwun zAD(sq*(dZ{1-07zN0uo46f+d^Rj*uaFqK;it`*n!?Vdgjj&7Mz26{f|rUnJ`V>9yL zepO>@Zj`Ra1gtDi9Y@$1kzLkuzTvUoOY%mqh=g*%fdgACN<3teYWv$oe z%ts2;#>v0SP3M=Cz5gBo%=mC*;ng1NFVmJD5L;NLv|Wn zhl~ywNYu9&Cp=ynx@ms)Eyo+-vo>M#?pBepSizfRjb-|&D&>$*E_JG({>I_;p@3vU z-C#INkfMoG<^UnMQw z_VRzfWZMX7uI|E$BKlY1iMOKN>mZuUSBRO|Y21aev9T#~Apa6=+a6hZX6ExWV=sg0 zCLi}{@A-fTcJt5je``oww_sD}WV{HMOMqCq4mS6>GVT2;5By&L({FCmWZA9Xr0CXy zg2_(HZ_dk^5fP9rbL^;jW!ArqIT}hB=A)TU?J3beG2I$*6jR=Iz%-lPKN|w}E{sL! z7rq^z4*Hw8#V3hFiH;p@MnCV31%~rCE}E3#JmTX6hU13+j{Mt2ydsl^yTi>R3j6!} zS6&)%n>83>eUg_pALW?io&*qwSP(M|(&_K5n)P>ThU1W-ON|?axs88ln~!$1{4$xV zCkG;^llSaq%3B;<1z^&=z6a%hGtYZzPGIe?$lY3T6qA7+kvF@t-3mnfRv5P9T z5aHW(7}oJ{|Am{D?o-WUm5g~6He62C)Q>R!0678Hz#=-xAH4&9Cm(Ci&Nd&P`4wR z8JI}yR#qg8euRG`XU5>?|DX-aFO5<>W;ue%*?@te&ah`dhzpMfH23ibJ)7d+_*d-x ze}38)VF1MY7vVgcYi(m0e*RW7xZe6W=m(Ho4Y0m#}I4&S? z9oA+<;OEqUOl3|qbf{EVa@LOZM}IF5_@=xwBC(qd#H$3&XTFB(1xc4_t(R8hUbzA2 zXW)=dGw?++{T#d)S?N?(nPZs=aUVWC2+U4Tw?$EgKL-3p3u~e`S6ljI#<{h|4LMK) zVD4x8vIe%R1w1Kb)5|65$(!rl8}z>Fr~AKpCDD~fV-b=y%0~dcrraX`_>Z@DGNK5GIT)%~;81FwPtv6Rk-<$(DjXlN{)_)s18|Ue5TRmG4#`3Q3q@MtylwMSbu!y7c zw;LS9(PxoDl+?|cLRSRH*YCk8*g?zi|t` zpEw7#&_+vy)b)NsjH_FZp`EwgFPxtg+U~BKE)@DMAU_hR%e^j!p1eL4lY240CZYB?&ATAImpRZ2lTk%h(r&@$_pi5D4mWXR~T@gz1q@jBk zh<#%80Y~HiR0Y0Yv9do(V*c?xAK-s6$O0zmnq<4X|08TNrY#xbzS!}w*Tll%drrH} zCp=xyPL}}FvjM#ypi863lP{Bg6B`-!-Dv|);)3bD;7kCJ)iBj>3DGZ8nV75J?25jF zdADB7g-Pf0pq&E`v(^iF1MGlU^>yPN7koSSViq+4^V=~nnxDTMo-fF~Twy0dzfxN4 zFu6WREArg@&Dyo5#F-{MR}*!>RNyr@CimGGwRcQ?0NapHaWxCPeu@8?c(Y zqmma}9@BP3zrsD0E^ur`q_*wUIZ5&!zQRWi0f;oonr1Zdb}vfZpSk(8o7d?Q9rN!V zFst~Mz)Cf|tL3PWx6nrNFr#kU_8_#hlY zMy_Jc&`!Du0`}~&`Wo-1W%~8J5RxZ>&hQE*de5Jlf9VFmgVilcv=B|y-fXo= z=s}*wQnCW0fa71=I3lWdgTpws7GILTT_m8nhzL53@YrcUQO#em|2w^|Bj-?$&dW{i z|M0Czk(bh()RI2v8Wm(xdLSSs_UFXmBP?%MInIRyAPDA5)cKIxu;R~@4ZytZtU?%M zcsB{yvp??llY>FtawDhRsn%;x%XRrCDIM6Ce#05z#?3gSl7pH&>?T z%M3T7F;XB7(B%Ri>9Ct);;Q+%Hhglv(Y5=&o)FCSK|U(mUF{t3^<)l)9YFX6Hjqt& z_5Fk(f4Nh+me$dLokVxIdbL$Kbz5ylcb7 zQU`FX#d-`areMi?ePIx&%N0OtenkcV9O)UCj*>CADLP3Ykcpe&SN($6WtB!4#JhRZ zc#2o3d1xYmf+HRui&0Q+H$La*(H@WgHHAYUuO~nsN#t^$dJ?Vc=@VyS2x; zb?6ZgIoJ|R0wKu9>d&|uqxctML|S(PcKxdJ{l`K;$$(Fc3UxuYg%zqMqe0h)>wiWAy_UVNkp)R=&+$M|Gt0?((#-F z>K4wejIaW?kPg^$E~kfXw^6ms*but=0I&`yhuP!Eb!6`ZiNux$v711JN9SFUnt(z5 zmQbwZcFo_yUCZ)~Jq|rekPXdl;`!=PNr|gfkyg8h11~;m9i4BM^NP#TNjx{zXGugp zc2PhNI|mB#yH{lp)kdb-Uv@HFYjlNA@As<*y1ncDQUsem@^|$OgUDFaODVm8hbWQD z6Ro_Mt(xDD_s-^DuA`rZvM>cXHy{6ASM&a%HSIRYZBJ^i1eePslL_6=HaqhgNgP#; zukd$5G&St2_Ec6f)J5qBg7;s-T-2U87%A1Ef#BQ9AtxFL0I=GOQ==pKeN>?XlhcTd zfmSVe&gXn!@sxfFGsL>-Yy5ECch0Sku`B`lBgq$m0B}+v#ZForMCLrS7QcnJv+8T? zpnB6f(}LX9Me)wUFZ)gJHy*fNvJF2}dC)gixh1Uw*PS_M+KO_o5K_d~g@;$*1t$n+ zNxS&MJ3+#JMTX+c?|FxP+V4;o6iHpJfA2+EMCqD)1F(CvrX=+PT1zm~KVf{*a?%`g zzA#a!60=;aZtOn%G-W-17Vd+}i3yl8_BQ@R{3!l5#6~$X06xo^(4BQ>P)O6Oq5y=| z$T2#A_mjXc65Cm^6~O6xDs(Sl_90Btp!}hp%qZWbE&Orxw0-#!yI?eEKT@fy%pkWZ zgyG`_!!&IBLXt%5C z+$)Dqs%!G_Ndh?B#@=oYIGGax;!S~QZCOH81B7D@h;L>Fn!r&Wi+xuYZ?SL|lJ&ouD|HxPfF<8J@p* zi?STfe7amsNlmY_3-FFfOuUC0V#R}IBexOL1DT?yTlKkf7qaW4KHfikGP_pecQ$AW zle}CcMP=Fe7x7&KVT(`2>a218il=$o(U~*#Q)I@bX9&KB8HEz1tt%Fx2cedAr{Jlf za4Kor+SohH>i6#7*#`+Keg|-B!6&1~T(%hQo0H0d!~=cW)8eRuB6kNamPB&eSL|R+ zu;AIX+19D%onLFGqbJ|*p~f?(!jVj+MmNo6qYMF$X9y`)`jerdJhxFhEy9RZdX-&dvd$=^6FalE2L zlLZBq(_X@XR8WbU1bT)NeZ)&tu|l;(Rr*Vscga$ofCdT14z>C;;&n8xH0n!YH_KFE z&>9$U_P%GglSG1R(SWs_s9SAAb?fFJt@UA6^l0nNMXP#Iw%halt56Cxj#Ns&9rxTB zQK?Z0dt;PheUaqxm+G#BWJ7!beTq6Gwd63!ZCr?99e}F2e=<)HE%}va8yGupNWo~@ zym;4{1IsO!thnahUUlrlkOi7vK4sHD6u>WF>j3Zt$vxD?X}Mz9bvU>u`?ksvUh&bk zu_EWZD^UrQagBDq@lu2>K~pO~?F3_NCFNee9$!z2NmyrOY7MwtX2^AocDwxf!j^px zHW>E<2UYA#MNoHpD!7fxVQr74crP-MCzqG_gox7~5vtt-Tl;+;g?ehmAN7(O9LU2x zE-=6%Reo~TT;Bt{u`|Ob9Gc3vBV|$kWc;;=;|#mxe$@KWq&n}?=UB+$kYLP#hnt(5 zA}sIvv$Z7BE(TFyA06%7}IQJ&g~Dg9K9wt=Tjn0+sF%`MoMj7Y?$_~ zEGX>Yo)ZnyO2@bv@cNv&UWOolrh_MGV=)TGI-v0)uol-}aV0>lUpZL`LLl zetSIoL%vo%v^9Pw`iXf9=c9LEB`OTAM>!W}{na64esEY<# zXlFIFa<`n=N1tW|Mt%^mpL6*P!9n`k#YA3t_&^vGFY0{;k8Iqip6`GeWp|(LVd;V1 zmpt#ScLYGIRU&YRzgK8O4BVRBtP|JjH4@2zn4;+a95NH93Aq2jL-=!1|SXAMgd`5FlFZ-_8*#OQCF z|2{P;&)sB<`de>?e3lzq?C`@kr`-;oy*|smX>dt;RThLYhuT!cYDPv|FwpZsN80=7 zxXkseR-`6|W6QQ{XUd)^S30&{p|hC$`2y6K`|)A^P%R&x z7Q~#&Eme1RM!u;S@F;#T4AN9Yw*6Fs)DFB9$#U;RbCV%hEe!ILy%-fyBrDE54^WC> zWwIoTA9gdVR{I7vIVsmWyVqr|Fk1HdtKM5Ka`cY6$?SKT$pJ;a|M?w`#{Q|Fg`2UG z`11Bva>+kS{h6F_`1Xgi3H*LoDZk1OdMgf@x_45Ntrpn2{=)eQ8(zxEe!RtRYr#*d z1+yO~MYyQZdkC(n^~Qi+t)d9;hKEsyJ?zW_UK^O?2=#yC@9qk3(Lx|w+5`&=2}d?I z`7xee+rgCH#)BHbnwsPzq&Y|_<}!AZhQE4Sz;`Ep4{FZ4X-{!n#zNI@3+c;U7s~db# zR(gyGA-lt;LYiVxko%6#nW{md*Vy4xTVVyOvB%qXcB34H^per^oIXi$+VwsMtFs9@ zV%@PQT6Da36AQh^m`u02^%e*?*j5gXg7CYfU-MeT=oUG%3fOvqec~<|!h30+uzPSU zuj7<+`2?=8=90eTx9uE+Mc-9iRJ()6MCg-s38Ox+!Xz;-359g2fJn_5c=wEcb~c

+ + Getting Started +   •   + API Documentation +   •   + Getting In Touch (Gitter) + +

+ +

+ + GitHub release (latest by date including pre-releases) + + + Codecov Status + + + license + +
+ + Build Status + + Beta +

+ +

+ + Contributing +   •   + Examples + +

+ +--- + +## About this project The Python [OpenTelemetry](https://opentelemetry.io/) client. From b5b297c7606fdf851c32f413968d842aa7e23b60 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 5 May 2020 13:50:55 -0600 Subject: [PATCH 0323/1517] api: Add reset for configuration testing (#636) It is a common occurrence in tests that the global Configuration object needs to be "reset" between tests. This means that its attributes need to be set back to their original values. Since the Configuration object is immutable by design, some additional, non-production available mechanism is needed to perform this action. The need for this feature was mentioned in a conversation in #630. --- .../tests/base_test.py | 5 ++++ .../tests/test_automatic.py | 3 -- .../tests/test_programmatic.py | 3 -- .../opentelemetry/configuration/__init__.py | 17 +++++++++++ .../tests/configuration/test_configuration.py | 30 ++++++++++++++----- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/ext/opentelemetry-ext-flask/tests/base_test.py b/ext/opentelemetry-ext-flask/tests/base_test.py index 7147afd719..42341826df 100644 --- a/ext/opentelemetry-ext-flask/tests/base_test.py +++ b/ext/opentelemetry-ext-flask/tests/base_test.py @@ -19,6 +19,7 @@ from werkzeug.wrappers import BaseResponse from opentelemetry import trace +from opentelemetry.configuration import Configuration def expected_attributes(override_attributes): @@ -40,6 +41,10 @@ def expected_attributes(override_attributes): class InstrumentationTest: + def setUp(self): # pylint: disable=invalid-name + super().setUp() # pylint: disable=no-member + Configuration._reset() # pylint: disable=protected-access + @staticmethod def _hello_endpoint(helloid): if helloid == 500: diff --git a/ext/opentelemetry-ext-flask/tests/test_automatic.py b/ext/opentelemetry-ext-flask/tests/test_automatic.py index 04f43d6e64..b94c7b33d6 100644 --- a/ext/opentelemetry-ext-flask/tests/test_automatic.py +++ b/ext/opentelemetry-ext-flask/tests/test_automatic.py @@ -16,7 +16,6 @@ from werkzeug.test import Client from werkzeug.wrappers import BaseResponse -from opentelemetry.configuration import Configuration from opentelemetry.ext.flask import FlaskInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase @@ -29,8 +28,6 @@ class TestAutomatic(InstrumentationTest, TestBase, WsgiTestBase): def setUp(self): super().setUp() - Configuration._instance = None # pylint: disable=protected-access - Configuration.__slots__ = [] # pylint: disable=protected-access FlaskInstrumentor().instrument() self.app = flask.Flask(__name__) diff --git a/ext/opentelemetry-ext-flask/tests/test_programmatic.py b/ext/opentelemetry-ext-flask/tests/test_programmatic.py index 1075f808cb..4e17f25fdc 100644 --- a/ext/opentelemetry-ext-flask/tests/test_programmatic.py +++ b/ext/opentelemetry-ext-flask/tests/test_programmatic.py @@ -14,7 +14,6 @@ from flask import Flask -from opentelemetry.configuration import Configuration from opentelemetry.ext.flask import FlaskInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase @@ -27,8 +26,6 @@ class TestProgrammatic(InstrumentationTest, TestBase, WsgiTestBase): def setUp(self): super().setUp() - Configuration._instance = None # pylint: disable=protected-access - Configuration.__slots__ = [] # pylint: disable=protected-access self.app = Flask(__name__) FlaskInstrumentor().instrument_app(self.app) diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index 57b1c324c6..ad546b0b86 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -122,3 +122,20 @@ def __new__(cls) -> "Configuration": def __getattr__(self, name): return None + + @classmethod + def _reset(cls): + """ + This method "resets" the global configuration attributes + + It is not intended to be used by production code but by testing code + only. + """ + + for slot in cls.__slots__: + if slot in cls.__dict__.keys(): + delattr(cls, slot) + delattr(cls, "_{}".format(slot)) + + cls.__slots__ = [] + cls._instance = None diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index 9688ec28b6..c736c97262 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -20,14 +20,10 @@ class TestConfiguration(TestCase): - def setUp(self): - # This is added here to force a reload of the whole Configuration - # class, resetting its internal attributes so that each tests starts - # with a clean class. - from opentelemetry.configuration import Configuration # type: ignore - def tearDown(self): - from opentelemetry.configuration import Configuration # type: ignore + # This call resets the attributes of the Configuration class so that + # each test is executed in the same conditions. + Configuration._reset() def test_singleton(self): self.assertIsInstance(Configuration(), Configuration) @@ -72,3 +68,23 @@ def test_slots(self): def test_getattr(self): self.assertIsNone(Configuration().XYZ) + + def test_reset(self): + environ_patcher = patch.dict( + "os.environ", # type: ignore + {"OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider"}, + ) + + environ_patcher.start() + + self.assertEqual( + Configuration().TRACER_PROVIDER, "tracer_provider" + ) # pylint: disable=no-member + + environ_patcher.stop() + + Configuration._reset() + + self.assertIsNone( + Configuration().TRACER_PROVIDER + ) # pylint: disable=no-member From 08898951c8ceaccf97eba69cbf191c012e5bef2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 6 May 2020 11:06:11 -0500 Subject: [PATCH 0324/1517] docs: Fix autoinstrumentation docs (#544) - Convert file to rst to be included in the generated documentation - Include docs for main module. - Use internal reference instead of read the docs link --- .../auto_instrumentation.rst | 8 + docs/examples/auto-instrumentation/README.rst | 189 ++++++++++++++++++ .../auto_instrumentation/__init__.py | 2 +- 3 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 docs/examples/auto-instrumentation/README.rst diff --git a/docs/auto_instrumentation/auto_instrumentation.rst b/docs/auto_instrumentation/auto_instrumentation.rst index a45512f7f9..c1fd0eff2a 100644 --- a/docs/auto_instrumentation/auto_instrumentation.rst +++ b/docs/auto_instrumentation/auto_instrumentation.rst @@ -1,6 +1,14 @@ OpenTelemetry Python Autoinstrumentation ======================================== +.. automodule:: opentelemetry.auto_instrumentation + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + .. toctree:: :maxdepth: 1 diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst new file mode 100644 index 0000000000..6592ecb958 --- /dev/null +++ b/docs/examples/auto-instrumentation/README.rst @@ -0,0 +1,189 @@ +Autoinstrumentation +=================== + +Overview +-------- + +This example shows how to use auto-instrumentation in OpenTelemetry. +This example is also based on a previous example for OpenTracing that +can be found +`here `__. + +The source files of these examples are available :scm_web:`here `. + +This example uses 2 scripts whose main difference is they being +instrumented manually or not: + +1. ``server_instrumented.py`` which has been instrumented manually +2. ``server_uninstrumented.py`` which has not been instrumented manually + +The former will be run without the automatic instrumentation agent and +the latter with the automatic instrumentation agent. They should produce +the same result, showing that the automatic instrumentation agent does +the equivalent of what manual instrumentation does. + +In order to understand this better, here is the relevant part of both +scripts: + +Manually instrumented server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``server_instrumented.py`` + +.. code:: python + + @app.route("/server_request") + def server_request(): + with tracer.start_as_current_span( + "server_request", + parent=propagators.extract( + lambda dict_, key: dict_.get(key, []), request.headers + )["current-span"], + ): + print(request.args.get("param")) + return "served" + +Publisher not instrumented manually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``server_uninstrumented.py`` + +.. code:: python + + @app.route("/server_request") + def server_request(): + print(request.args.get("param")) + return "served" + +Preparation +----------- + +This example will be executed in a separate virtual environment: + +.. code:: sh + + $ mkdir auto_instrumentation + $ virtualenv auto_instrumentation + $ source auto_instrumentation/bin/activate + +Installation +------------ + +.. code:: sh + + $ pip install opentelemetry-sdk + $ pip install opentelemetry-auto-instrumentation + $ pip install opentelemetry-ext-flask + $ pip install requests + +Execution +--------- + +Execution of the manually instrumented server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is done in 2 separate consoles, one to run each of the scripts that +make up this example: + +.. code:: sh + + $ source auto_instrumentation/bin/activate + $ python server_instrumented.py + +.. code:: sh + + $ source auto_instrumentation/bin/activate + $ python client.py testing + +The execution of ``server_instrumented.py`` should return an output +similar to: + +.. code:: sh + + { + "name": "server_request", + "context": { + "trace_id": "0xfa002aad260b5f7110db674a9ddfcd23", + "span_id": "0x8b8bbaf3ca9c5131", + "trace_state": "{}" + }, + "kind": "SpanKind.SERVER", + "parent_id": null, + "start_time": "2020-04-30T17:28:57.886397Z", + "end_time": "2020-04-30T17:28:57.886490Z", + "status": { + "canonical_code": "OK" + }, + "attributes": { + "component": "http", + "http.method": "GET", + "http.server_name": "127.0.0.1", + "http.scheme": "http", + "host.port": 8082, + "http.host": "localhost:8082", + "http.target": "/server_request?param=testing", + "net.peer.ip": "127.0.0.1", + "net.peer.port": 52872, + "http.flavor": "1.1" + }, + "events": [], + "links": [] + } + +Execution of an automatically instrumented server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now, kill the execution of ``server_instrumented.py`` with ``ctrl + c`` +and run this instead: + +.. code:: sh + + $ opentelemetry-auto-instrumentation python server_uninstrumented.py + +In the console where you previously executed ``client.py``, run again +this again: + +.. code:: sh + + $ python client.py testing + +The execution of ``server_uninstrumented.py`` should return an output +similar to: + +.. code:: sh + + { + "name": "server_request", + "context": { + "trace_id": "0x9f528e0b76189f539d9c21b1a7a2fc24", + "span_id": "0xd79760685cd4c269", + "trace_state": "{}" + }, + "kind": "SpanKind.SERVER", + "parent_id": "0xb4fb7eee22ef78e4", + "start_time": "2020-04-30T17:10:02.400604Z", + "end_time": "2020-04-30T17:10:02.401858Z", + "status": { + "canonical_code": "OK" + }, + "attributes": { + "component": "http", + "http.method": "GET", + "http.server_name": "127.0.0.1", + "http.scheme": "http", + "host.port": 8082, + "http.host": "localhost:8082", + "http.target": "/server_request?param=testing", + "net.peer.ip": "127.0.0.1", + "net.peer.port": 48240, + "http.flavor": "1.1", + "http.route": "/server_request", + "http.status_text": "OK", + "http.status_code": 200 + }, + "events": [], + "links": [] + } + +Both outputs are equivalent since the automatic instrumentation does +what the manual instrumentation does too. diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py index 0d8d7dff27..6432b1209b 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py @@ -24,5 +24,5 @@ The code in ``program.py`` needs to use one of the packages for which there is an OpenTelemetry integration. For a list of the available integrations please -check `here `_. +check :doc:`here <../../index>`. """ From 38fd2164fb18b08fc5416558d3cef4aa91373dc9 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 6 May 2020 16:27:01 -0600 Subject: [PATCH 0325/1517] Add Django instrumentation (#593) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initial Instrumentation Co-authored-by: Mauricio Vásquez Co-authored-by: Mathieu Hinderyckx Co-authored-by: alrex Co-authored-by: Yusuke Tsutsumi --- .flake8 | 3 +- .gitignore | 4 + docs/examples/django/README.rst | 108 ++++++++++++++ docs/examples/django/client.py | 45 ++++++ .../instrumentation_example/__init__.py | 0 .../django/instrumentation_example/asgi.py | 31 ++++ .../instrumentation_example/settings.py | 133 ++++++++++++++++++ .../django/instrumentation_example/urls.py | 35 +++++ .../django/instrumentation_example/wsgi.py | 31 ++++ docs/examples/django/manage.py | 43 ++++++ docs/examples/django/pages/__init__.py | 15 ++ docs/examples/django/pages/apps.py | 18 +++ .../django/pages/migrations/__init__.py | 0 docs/examples/django/pages/urls.py | 18 +++ docs/examples/django/pages/views.py | 32 +++++ docs/ext/django/django.rst | 7 + ext/opentelemetry-ext-django/CHANGELOG.md | 5 + ext/opentelemetry-ext-django/README.rst | 24 ++++ ext/opentelemetry-ext-django/setup.cfg | 55 ++++++++ ext/opentelemetry-ext-django/setup.py | 32 +++++ .../src/opentelemetry/ext/django/__init__.py | 72 ++++++++++ .../opentelemetry/ext/django/middleware.py | 114 +++++++++++++++ .../src/opentelemetry/ext/django/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/conftest.py | 19 +++ .../tests/test_middleware.py | 108 ++++++++++++++ ext/opentelemetry-ext-django/tests/views.py | 9 ++ tox.ini | 12 +- 28 files changed, 984 insertions(+), 4 deletions(-) create mode 100644 docs/examples/django/README.rst create mode 100644 docs/examples/django/client.py create mode 100644 docs/examples/django/instrumentation_example/__init__.py create mode 100644 docs/examples/django/instrumentation_example/asgi.py create mode 100644 docs/examples/django/instrumentation_example/settings.py create mode 100644 docs/examples/django/instrumentation_example/urls.py create mode 100644 docs/examples/django/instrumentation_example/wsgi.py create mode 100755 docs/examples/django/manage.py create mode 100644 docs/examples/django/pages/__init__.py create mode 100644 docs/examples/django/pages/apps.py create mode 100644 docs/examples/django/pages/migrations/__init__.py create mode 100644 docs/examples/django/pages/urls.py create mode 100644 docs/examples/django/pages/views.py create mode 100644 docs/ext/django/django.rst create mode 100644 ext/opentelemetry-ext-django/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-django/README.rst create mode 100644 ext/opentelemetry-ext-django/setup.cfg create mode 100644 ext/opentelemetry-ext-django/setup.py create mode 100644 ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py create mode 100644 ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py create mode 100644 ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py create mode 100644 ext/opentelemetry-ext-django/tests/__init__.py create mode 100644 ext/opentelemetry-ext-django/tests/conftest.py create mode 100644 ext/opentelemetry-ext-django/tests/test_middleware.py create mode 100644 ext/opentelemetry-ext-django/tests/views.py diff --git a/.flake8 b/.flake8 index 5922f31d8f..f511c4c3e0 100644 --- a/.flake8 +++ b/.flake8 @@ -2,7 +2,8 @@ ignore = E501 # line too long, defer to black F401 # unused import, defer to pylint - W503 # allow line breaks after binary ops, not after + W503 # allow line breaks before binary ops + W504 # allow line breaks after binary ops E203 # allow whitespace before ':' (https://github.com/psf/black#slices) exclude = .bzr diff --git a/.gitignore b/.gitignore index 42e6d0bf04..75cdf09293 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,7 @@ _build/ # mypy .mypy_cache/ target + +# Django example + +docs/examples/django/db.sqlite3 diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst new file mode 100644 index 0000000000..2b2f158e7a --- /dev/null +++ b/docs/examples/django/README.rst @@ -0,0 +1,108 @@ +OpenTelemetry Django Instrumentation Example +============================================ + +This shows how to use `opentelemetry-ext-django` to automatically instrument a +Django app. + +For more user convenience, a Django app is already provided in this directory. + +Preparation +----------- + +This example will be executed in a separate virtual environment: + +.. code-block:: + + $ mkdir django_auto_instrumentation + $ virtualenv django_auto_instrumentation + $ source django_auto_instrumentation/bin/activate + + +Installation +------------ + +.. code-block:: + + $ pip install opentelemetry-sdk + $ pip install opentelemetry-ext-django + $ pip install requests + + +Execution +--------- + +Execution of the Django app +........................... + +Set these environment variables first: + +#. `export OPENTELEMETRY_PYTHON_DJANGO_INSTRUMENT=True` +#. `export DJANGO_SETTINGS_MODULE=instrumentation_example.settings` + +The way to achieve OpenTelemetry instrumentation for your Django app is to use +an `opentelemetry.ext.django.DjangoInstrumentor` to instrument the app. + +Clone the `opentelemetry-python` repository and go to `opentelemetry-python/docs/examples/django`. + +Once there, open the `manage.py` file. The call to `DjangoInstrumentor().instrument()` +in `main` is all that is needed to make the app be instrumented. + +Run the Django app with `python manage.py runserver`. + +Execution of the client +....................... + +Open up a new console and activate the previous virtual environment there too: + +`source django_auto_instrumentation/bin/activate` + +Go to `opentelemetry-python/ext/opentelemetry-ext-django/example`, once there +run the client with: + +`python client.py hello` + +Go to the previous console, where the Django app is running. You should see +output similar to this one: + +.. code-block:: + + { + "name": "home_page_view", + "context": { + "trace_id": "0xed88755c56d95d05a506f5f70e7849b9", + "span_id": "0x0a94c7a60e0650d5", + "trace_state": "{}" + }, + "kind": "SpanKind.SERVER", + "parent_id": "0x3096ef92e621c22d", + "start_time": "2020-04-26T01:49:57.205833Z", + "end_time": "2020-04-26T01:49:57.206214Z", + "status": { + "canonical_code": "OK" + }, + "attributes": { + "component": "http", + "http.method": "GET", + "http.server_name": "localhost", + "http.scheme": "http", + "host.port": 8000, + "http.host": "localhost:8000", + "http.url": "http://localhost:8000/?param=hello", + "net.peer.ip": "127.0.0.1", + "http.flavor": "1.1", + "http.status_text": "OK", + "http.status_code": 200 + }, + "events": [], + "links": [] + } + +The last output shows spans automatically generated by the OpenTelemetry Django +Instrumentation package. + +References +---------- + +* `Django `_ +* `OpenTelemetry Project `_ +* `OpenTelemetry Django extension `_ diff --git a/docs/examples/django/client.py b/docs/examples/django/client.py new file mode 100644 index 0000000000..e65285c35d --- /dev/null +++ b/docs/examples/django/client.py @@ -0,0 +1,45 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sys import argv + +from requests import get + +from opentelemetry import propagators, trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer_provider().get_tracer(__name__) + +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) + + +with tracer.start_as_current_span("client"): + + with tracer.start_as_current_span("client-server"): + headers = {} + propagators.inject(dict.__setitem__, headers) + requested = get( + "http://localhost:8000", + params={"param": argv[1]}, + headers=headers, + ) + + assert requested.status_code == 200 diff --git a/docs/examples/django/instrumentation_example/__init__.py b/docs/examples/django/instrumentation_example/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/django/instrumentation_example/asgi.py b/docs/examples/django/instrumentation_example/asgi.py new file mode 100644 index 0000000000..dd8fb568f4 --- /dev/null +++ b/docs/examples/django/instrumentation_example/asgi.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +ASGI config for instrumentation_example project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "instrumentation_example.settings" +) + +application = get_asgi_application() diff --git a/docs/examples/django/instrumentation_example/settings.py b/docs/examples/django/instrumentation_example/settings.py new file mode 100644 index 0000000000..b5b8897b91 --- /dev/null +++ b/docs/examples/django/instrumentation_example/settings.py @@ -0,0 +1,133 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Django settings for instrumentation_example project. + +Generated by "django-admin startproject" using Django 3.0.4. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.0/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "it%*!=l2(fcawu=!m-06n&#j(iq2j#%$fu6)myi*b9i5ojk+6+" + +# SECURITY WARNING: don"t run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "instrumentation_example.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "instrumentation_example.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/3.0/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.0/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.0/howto/static-files/ + +STATIC_URL = "/static/" diff --git a/docs/examples/django/instrumentation_example/urls.py b/docs/examples/django/instrumentation_example/urls.py new file mode 100644 index 0000000000..292467155f --- /dev/null +++ b/docs/examples/django/instrumentation_example/urls.py @@ -0,0 +1,35 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""instrumentation_example URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path("", views.home, name="home") +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path("", Home.as_view(), name="home") +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path("blog/", include("blog.urls")) +""" +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path("admin/", admin.site.urls), + path("", include("pages.urls")), +] diff --git a/docs/examples/django/instrumentation_example/wsgi.py b/docs/examples/django/instrumentation_example/wsgi.py new file mode 100644 index 0000000000..70ea9e0db5 --- /dev/null +++ b/docs/examples/django/instrumentation_example/wsgi.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +WSGI config for instrumentation_example project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "instrumentation_example.settings" +) + +application = get_wsgi_application() diff --git a/docs/examples/django/manage.py b/docs/examples/django/manage.py new file mode 100755 index 0000000000..fdf32287c5 --- /dev/null +++ b/docs/examples/django/manage.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Django"s command-line utility for administrative tasks.""" +import os +import sys + +from opentelemetry.ext.django import DjangoInstrumentor + + +def main(): + + # This call is what makes the Django application be instrumented + DjangoInstrumentor().instrument() + + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "instrumentation_example.settings" + ) + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/docs/examples/django/pages/__init__.py b/docs/examples/django/pages/__init__.py new file mode 100644 index 0000000000..5855e41f3a --- /dev/null +++ b/docs/examples/django/pages/__init__.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +default_app_config = "pages.apps.PagesConfig" diff --git a/docs/examples/django/pages/apps.py b/docs/examples/django/pages/apps.py new file mode 100644 index 0000000000..0f12b7b66c --- /dev/null +++ b/docs/examples/django/pages/apps.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from django.apps import AppConfig + + +class PagesConfig(AppConfig): + name = "pages" diff --git a/docs/examples/django/pages/migrations/__init__.py b/docs/examples/django/pages/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/django/pages/urls.py b/docs/examples/django/pages/urls.py new file mode 100644 index 0000000000..99c95765a4 --- /dev/null +++ b/docs/examples/django/pages/urls.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from django.urls import path + +from .views import home_page_view + +urlpatterns = [path("", home_page_view, name="home")] diff --git a/docs/examples/django/pages/views.py b/docs/examples/django/pages/views.py new file mode 100644 index 0000000000..d54633c329 --- /dev/null +++ b/docs/examples/django/pages/views.py @@ -0,0 +1,32 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from django.http import HttpResponse + +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer_provider().get_tracer(__name__) + +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) + + +def home_page_view(request): + return HttpResponse("Hello, world") diff --git a/docs/ext/django/django.rst b/docs/ext/django/django.rst new file mode 100644 index 0000000000..1a2c844e28 --- /dev/null +++ b/docs/ext/django/django.rst @@ -0,0 +1,7 @@ +OpenTelemetry Django Instrumentation +==================================== + +.. automodule:: opentelemetry.ext.django + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-django/CHANGELOG.md b/ext/opentelemetry-ext-django/CHANGELOG.md new file mode 100644 index 0000000000..3e04402cea --- /dev/null +++ b/ext/opentelemetry-ext-django/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release diff --git a/ext/opentelemetry-ext-django/README.rst b/ext/opentelemetry-ext-django/README.rst new file mode 100644 index 0000000000..b922046ca6 --- /dev/null +++ b/ext/opentelemetry-ext-django/README.rst @@ -0,0 +1,24 @@ +OpenTelemetry Django Tracing +============================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-django.svg + :target: https://pypi.org/project/opentelemetry-ext-django/ + +This library allows tracing requests for Django applications. + +Installation +------------ + +:: + + pip install opentelemetry-ext-django + + +References +---------- + +* `Django `_ +* `OpenTelemetry Django Tracing `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-django/setup.cfg b/ext/opentelemetry-ext-django/setup.cfg new file mode 100644 index 0000000000..c308a6f352 --- /dev/null +++ b/ext/opentelemetry-ext-django/setup.cfg @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-django +description = OpenTelemetry Instrumentation for Django +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-django +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.6 +package_dir= + =src +packages=find_namespace: +install_requires = + django >= 2.2 + opentelemetry-ext-wsgi == 0.7.dev0 + opentelemetry-auto-instrumentation == 0.7.dev0 + opentelemetry-api == 0.7.dev0 + +[options.extras_require] +test = + opentelemetry-test == 0.7.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + django = opentelemetry.ext.django:DjangoInstrumentor diff --git a/ext/opentelemetry-ext-django/setup.py b/ext/opentelemetry-ext-django/setup.py new file mode 100644 index 0000000000..45cc68c0f4 --- /dev/null +++ b/ext/opentelemetry-ext-django/setup.py @@ -0,0 +1,32 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os.path import dirname, join + +from setuptools import setup + +PACKAGE_INFO = {} +with open( + join( + dirname(__file__), + "src", + "opentelemetry", + "ext", + "django", + "version.py", + ) +) as f: + exec(f.read(), PACKAGE_INFO) + +setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py new file mode 100644 index 0000000000..f59d90b490 --- /dev/null +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py @@ -0,0 +1,72 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from logging import getLogger + +from django.conf import settings + +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.configuration import Configuration +from opentelemetry.ext.django.middleware import _DjangoMiddleware + +_logger = getLogger(__name__) + + +class DjangoInstrumentor(BaseInstrumentor): + """An instrumentor for Django + + See `BaseInstrumentor` + """ + + _opentelemetry_middleware = ".".join( + [_DjangoMiddleware.__module__, _DjangoMiddleware.__qualname__] + ) + + def _instrument(self, **kwargs): + + # FIXME this is probably a pattern that will show up in the rest of the + # ext. Find a better way of implementing this. + # FIXME Probably the evaluation of strings into boolean values can be + # built inside the Configuration class itself with the magic method + # __bool__ + + if Configuration().DJANGO_INSTRUMENT != "True": + return + + # This can not be solved, but is an inherent problem of this approach: + # the order of middleware entries matters, and here you have no control + # on that: + # https://docs.djangoproject.com/en/3.0/topics/http/middleware/#activating-middleware + # https://docs.djangoproject.com/en/3.0/ref/middleware/#middleware-ordering + + settings_middleware = getattr(settings, "MIDDLEWARE", []) + settings_middleware.append(self._opentelemetry_middleware) + + setattr(settings, "MIDDLEWARE", settings_middleware) + + def _uninstrument(self, **kwargs): + settings_middleware = getattr(settings, "MIDDLEWARE", None) + + # FIXME This is starting to smell like trouble. We have 2 mechanisms + # that may make this condition be True, one implemented in + # BaseInstrumentor and another one implemented in _instrument. Both + # stop _instrument from running and thus, settings_middleware not being + # set. + if settings_middleware is None or ( + self._opentelemetry_middleware not in settings_middleware + ): + return + + settings_middleware.remove(self._opentelemetry_middleware) + setattr(settings, "MIDDLEWARE", settings_middleware) diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py new file mode 100644 index 0000000000..5974c5e503 --- /dev/null +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py @@ -0,0 +1,114 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from logging import getLogger + +from django.utils.deprecation import MiddlewareMixin + +from opentelemetry.context import attach, detach +from opentelemetry.ext.django.version import __version__ +from opentelemetry.ext.wsgi import ( + add_response_attributes, + collect_request_attributes, + get_header_from_environ, +) +from opentelemetry.propagators import extract +from opentelemetry.trace import SpanKind, get_tracer + +_logger = getLogger(__name__) + + +class _DjangoMiddleware(MiddlewareMixin): + """Django Middleware for OpenTelemetry + """ + + _environ_activation_key = ( + "opentelemetry-instrumentor-django.activation_key" + ) + _environ_token = "opentelemetry-instrumentor-django.token" + _environ_span_key = "opentelemetry-instrumentor-django.span_key" + + def process_view( + self, request, view_func, view_args, view_kwargs + ): # pylint: disable=unused-argument + # request.META is a dictionary containing all available HTTP headers + # Read more about request.META here: + # https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpRequest.META + + # environ = { + # key.lower().replace('_', '-').replace("http-", "", 1): value + # for key, value in request.META.items() + # } + + environ = request.META + + token = attach(extract(get_header_from_environ, environ)) + + tracer = get_tracer(__name__, __version__) + + attributes = collect_request_attributes(environ) + + span = tracer.start_span( + view_func.__name__, + kind=SpanKind.SERVER, + attributes=attributes, + start_time=environ.get( + "opentelemetry-instrumentor-django.starttime_key" + ), + ) + + activation = tracer.use_span(span, end_on_exit=True) + activation.__enter__() + + request.META[self._environ_activation_key] = activation + request.META[self._environ_span_key] = span + request.META[self._environ_token] = token + + def process_exception(self, request, exception): + # Django can call this method and process_response later. In order + # to avoid __exit__ and detach from being called twice then, the + # respective keys are being removed here. + if self._environ_activation_key in request.META.keys(): + request.META[self._environ_activation_key].__exit__( + type(exception), + exception, + getattr(exception, "__traceback__", None), + ) + request.META.pop(self._environ_activation_key) + + detach(request.environ[self._environ_token]) + request.META.pop(self._environ_token, None) + + def process_response(self, request, response): + if ( + self._environ_activation_key in request.META.keys() + and self._environ_span_key in request.META.keys() + ): + add_response_attributes( + request.META[self._environ_span_key], + "{} {}".format(response.status_code, response.reason_phrase), + response, + ) + request.META.pop(self._environ_span_key) + + request.META[self._environ_activation_key].__exit__( + None, None, None + ) + request.META.pop(self._environ_activation_key) + + if self._environ_token in request.META.keys(): + detach(request.environ.get(self._environ_token)) + request.META.pop(self._environ_token) + + return response diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py new file mode 100644 index 0000000000..86c61362ab --- /dev/null +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-django/tests/__init__.py b/ext/opentelemetry-ext-django/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-django/tests/conftest.py b/ext/opentelemetry-ext-django/tests/conftest.py new file mode 100644 index 0000000000..b2b39bc049 --- /dev/null +++ b/ext/opentelemetry-ext-django/tests/conftest.py @@ -0,0 +1,19 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import environ + + +def pytest_sessionstart(session): # pylint: disable=unused-argument + environ.setdefault("OPENTELEMETRY_PYTHON_DJANGO_INSTRUMENT", "True") diff --git a/ext/opentelemetry-ext-django/tests/test_middleware.py b/ext/opentelemetry-ext-django/tests/test_middleware.py new file mode 100644 index 0000000000..afee6acf77 --- /dev/null +++ b/ext/opentelemetry-ext-django/tests/test_middleware.py @@ -0,0 +1,108 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sys import modules + +from django.conf import settings +from django.conf.urls import url +from django.test import Client +from django.test.utils import setup_test_environment, teardown_test_environment + +from opentelemetry.ext.django import DjangoInstrumentor +from opentelemetry.test.wsgitestutil import WsgiTestBase +from opentelemetry.trace import SpanKind +from opentelemetry.trace.status import StatusCanonicalCode + +from .views import error, traced # pylint: disable=import-error + +urlpatterns = [ + url(r"^traced/", traced), + url(r"^error/", error), +] +_django_instrumentor = DjangoInstrumentor() + + +class TestMiddleware(WsgiTestBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + settings.configure(ROOT_URLCONF=modules[__name__]) + + def setUp(self): + super().setUp() + setup_test_environment() + _django_instrumentor.instrument() + + def tearDown(self): + super().tearDown() + teardown_test_environment() + _django_instrumentor.uninstrument() + + def test_traced_get(self): + Client().get("/traced/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "traced") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertEqual(span.attributes["http.method"], "GET") + self.assertEqual( + span.attributes["http.url"], "http://testserver/traced/" + ) + self.assertEqual(span.attributes["http.scheme"], "http") + self.assertEqual(span.attributes["http.status_code"], 200) + self.assertEqual(span.attributes["http.status_text"], "OK") + + def test_traced_post(self): + Client().post("/traced/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "traced") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertEqual(span.attributes["http.method"], "POST") + self.assertEqual( + span.attributes["http.url"], "http://testserver/traced/" + ) + self.assertEqual(span.attributes["http.scheme"], "http") + self.assertEqual(span.attributes["http.status_code"], 200) + self.assertEqual(span.attributes["http.status_text"], "OK") + + def test_error(self): + with self.assertRaises(ValueError): + Client().get("/error/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "error") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual( + span.status.canonical_code, StatusCanonicalCode.UNKNOWN + ) + self.assertEqual(span.attributes["http.method"], "GET") + self.assertEqual( + span.attributes["http.url"], "http://testserver/error/" + ) + self.assertEqual(span.attributes["http.scheme"], "http") diff --git a/ext/opentelemetry-ext-django/tests/views.py b/ext/opentelemetry-ext-django/tests/views.py new file mode 100644 index 0000000000..498a4518ed --- /dev/null +++ b/ext/opentelemetry-ext-django/tests/views.py @@ -0,0 +1,9 @@ +from django.http import HttpResponse + + +def traced(request): # pylint: disable=unused-argument + return HttpResponse() + + +def error(request): # pylint: disable=unused-argument + raise ValueError("error") diff --git a/tox.ini b/tox.ini index 1570df787c..84dd157dba 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,9 @@ envlist = py3{4,5,6,7,8}-test-example-http pypy3-test-example-http + py3{6,7,8}-test-ext-django + pypy3-test-ext-django + ; opentelemetry-ext-dbapi py3{4,5,6,7,8}-test-ext-dbapi pypy3-test-ext-dbapi @@ -127,6 +130,7 @@ changedir = test-ext-requests: ext/opentelemetry-ext-requests/tests test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests + test-ext-django: ext/opentelemetry-ext-django/tests test-ext-mysql: ext/opentelemetry-ext-mysql/tests test-ext-otcollector: ext/opentelemetry-ext-otcollector/tests test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests @@ -168,13 +172,15 @@ commands_pre = grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc[test] - wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-wsgi - - flask: pip install {toxinidir}/opentelemetry-auto-instrumentation + wsgi,flask,django: pip install {toxinidir}/tests/util + wsgi,flask,django: pip install {toxinidir}/ext/opentelemetry-ext-wsgi + flask,django: pip install {toxinidir}/opentelemetry-auto-instrumentation flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi[test] + django: pip install {toxinidir}/ext/opentelemetry-ext-django[test] + mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql[test] From e800d26df9403501e445aaed8fc99c8a0f013933 Mon Sep 17 00:00:00 2001 From: joshuahlang Date: Wed, 6 May 2020 20:40:47 -0700 Subject: [PATCH 0326/1517] aiohttp: aiohttp client (#421) Adding initial aiohttp client. This module is only supported on Python3.5, which is the oldest supported by aiohttp. Co-authored-by: Yusuke Tsutsumi --- docs-requirements.txt | 1 + docs/conf.py | 1 + docs/ext/aiohttp_client/aiohttp_client.rst | 7 + .../CHANGELOG.md | 5 + ext/opentelemetry-ext-aiohttp-client/LICENSE | 201 +++++++++++ .../MANIFEST.in | 9 + .../README.rst | 24 ++ .../setup.cfg | 46 +++ ext/opentelemetry-ext-aiohttp-client/setup.py | 26 ++ .../ext/aiohttp_client/__init__.py | 234 ++++++++++++ .../ext/aiohttp_client/version.py | 15 + .../tests/__init__.py | 0 .../tests/test_aiohttp_client_integration.py | 338 ++++++++++++++++++ .../tests/docker-compose.yml | 35 +- scripts/coverage.sh | 9 +- tox.ini | 11 +- 16 files changed, 948 insertions(+), 14 deletions(-) create mode 100644 docs/ext/aiohttp_client/aiohttp_client.rst create mode 100644 ext/opentelemetry-ext-aiohttp-client/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-aiohttp-client/LICENSE create mode 100644 ext/opentelemetry-ext-aiohttp-client/MANIFEST.in create mode 100644 ext/opentelemetry-ext-aiohttp-client/README.rst create mode 100644 ext/opentelemetry-ext-aiohttp-client/setup.cfg create mode 100644 ext/opentelemetry-ext-aiohttp-client/setup.py create mode 100644 ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py create mode 100644 ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py create mode 100644 ext/opentelemetry-ext-aiohttp-client/tests/__init__.py create mode 100644 ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py diff --git a/docs-requirements.txt b/docs-requirements.txt index f2d6ec2a57..ca3db13e0a 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -3,6 +3,7 @@ sphinx-rtd-theme~=0.4 sphinx-autodoc-typehints~=1.10.2 # Required by ext packages +aiohttp ~= 3.0 Deprecated>=1.2.6 PyMySQL~=0.9.3 flask~=1.0 diff --git a/docs/conf.py b/docs/conf.py index acd44c0c7a..c71995dfb4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -68,6 +68,7 @@ "https://opentracing-python.readthedocs.io/en/latest/", None, ), + "aiohttp": ("https://aiohttp.readthedocs.io/en/stable/", None), } # http://www.sphinx-doc.org/en/master/config.html#confval-nitpicky diff --git a/docs/ext/aiohttp_client/aiohttp_client.rst b/docs/ext/aiohttp_client/aiohttp_client.rst new file mode 100644 index 0000000000..e5ab26b0ea --- /dev/null +++ b/docs/ext/aiohttp_client/aiohttp_client.rst @@ -0,0 +1,7 @@ +OpenTelemetry aiohttp client Integration +======================================== + +.. automodule:: opentelemetry.ext.aiohttp_client + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-aiohttp-client/CHANGELOG.md b/ext/opentelemetry-ext-aiohttp-client/CHANGELOG.md new file mode 100644 index 0000000000..3e04402cea --- /dev/null +++ b/ext/opentelemetry-ext-aiohttp-client/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release diff --git a/ext/opentelemetry-ext-aiohttp-client/LICENSE b/ext/opentelemetry-ext-aiohttp-client/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-aiohttp-client/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-aiohttp-client/MANIFEST.in b/ext/opentelemetry-ext-aiohttp-client/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-aiohttp-client/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-aiohttp-client/README.rst b/ext/opentelemetry-ext-aiohttp-client/README.rst new file mode 100644 index 0000000000..c257639cf3 --- /dev/null +++ b/ext/opentelemetry-ext-aiohttp-client/README.rst @@ -0,0 +1,24 @@ +OpenTelemetry aiohttp client Integration +======================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-aiohttp-client.svg + :target: https://pypi.org/project/opentelemetry-ext-aiohttp-client/ + +This library allows tracing HTTP requests made by the +`aiohttp client `_ library. + +Installation +------------ + +:: + + pip install opentelemetry-ext-aiohttp-client + + +References +---------- + +* `OpenTelemetry Project `_ +* `aiohttp client Tracing `_ diff --git a/ext/opentelemetry-ext-aiohttp-client/setup.cfg b/ext/opentelemetry-ext-aiohttp-client/setup.cfg new file mode 100644 index 0000000000..6c82039ecd --- /dev/null +++ b/ext/opentelemetry-ext-aiohttp-client/setup.cfg @@ -0,0 +1,46 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-aiohttp-client +description = OpenTelemetry aiohttp client integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-aiohttp-client +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5.3 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api >= 0.7.dev0 + aiohttp ~= 3.0 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-aiohttp-client/setup.py b/ext/opentelemetry-ext-aiohttp-client/setup.py new file mode 100644 index 0000000000..1b44425f06 --- /dev/null +++ b/ext/opentelemetry-ext-aiohttp-client/setup.py @@ -0,0 +1,26 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "aiohttp_client", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py new file mode 100644 index 0000000000..3c4e7f4eda --- /dev/null +++ b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py @@ -0,0 +1,234 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The opentelemetry-ext-aiohttp-client package allows tracing HTTP requests +made by the aiohttp client library. + +Usage +----- + + .. code:: python + + import aiohttp + from opentelemetry.ext.aiohttp_client import ( + create_trace_config, + url_path_span_name + ) + import yarl + + def strip_query_params(url: yarl.URL) -> str: + return str(url.with_query(None)) + + async with aiohttp.ClientSession(trace_configs=[create_trace_config( + # Remove all query params from the URL attribute on the span. + url_filter=strip_query_params, + # Use the URL's path as the span name. + span_name=url_path_span_name + )]) as session: + async with session.get(url) as response: + await response.text() + +""" + +import contextlib +import socket +import types +import typing + +import aiohttp + +from opentelemetry import context as context_api +from opentelemetry import propagators, trace +from opentelemetry.ext.aiohttp_client.version import __version__ +from opentelemetry.trace import SpanKind +from opentelemetry.trace.status import Status, StatusCanonicalCode + + +# TODO: refactor this code to some common utility +def http_status_to_canonical_code(status: int) -> StatusCanonicalCode: + # pylint:disable=too-many-branches,too-many-return-statements + if status < 100: + return StatusCanonicalCode.UNKNOWN + if status <= 399: + return StatusCanonicalCode.OK + if status <= 499: + if status == 401: # HTTPStatus.UNAUTHORIZED: + return StatusCanonicalCode.UNAUTHENTICATED + if status == 403: # HTTPStatus.FORBIDDEN: + return StatusCanonicalCode.PERMISSION_DENIED + if status == 404: # HTTPStatus.NOT_FOUND: + return StatusCanonicalCode.NOT_FOUND + if status == 429: # HTTPStatus.TOO_MANY_REQUESTS: + return StatusCanonicalCode.RESOURCE_EXHAUSTED + return StatusCanonicalCode.INVALID_ARGUMENT + if status <= 599: + if status == 501: # HTTPStatus.NOT_IMPLEMENTED: + return StatusCanonicalCode.UNIMPLEMENTED + if status == 503: # HTTPStatus.SERVICE_UNAVAILABLE: + return StatusCanonicalCode.UNAVAILABLE + if status == 504: # HTTPStatus.GATEWAY_TIMEOUT: + return StatusCanonicalCode.DEADLINE_EXCEEDED + return StatusCanonicalCode.INTERNAL + return StatusCanonicalCode.UNKNOWN + + +def url_path_span_name(params: aiohttp.TraceRequestStartParams) -> str: + """Extract a span name from the request URL path. + + A simple callable to extract the path portion of the requested URL + for use as the span name. + + :param aiohttp.TraceRequestStartParams params: Parameters describing + the traced request. + + :return: The URL path. + :rtype: str + """ + return params.url.path + + +def create_trace_config( + url_filter: typing.Optional[typing.Callable[[str], str]] = None, + span_name: typing.Optional[ + typing.Union[ + typing.Callable[[aiohttp.TraceRequestStartParams], str], str + ] + ] = None, +) -> aiohttp.TraceConfig: + """Create an aiohttp-compatible trace configuration. + + One span is created for the entire HTTP request, including initial + TCP/TLS setup if the connection doesn't exist. + + By default the span name is set to the HTTP request method. + + Example usage: + + .. code:: python + + import aiohttp + from opentelemetry.ext.aiohttp_client import create_trace_config + + async with aiohttp.ClientSession(trace_configs=[create_trace_config()]) as session: + async with session.get(url) as response: + await response.text() + + + :param url_filter: A callback to process the requested URL prior to adding + it as a span attribute. This can be useful to remove sensitive data + such as API keys or user personal information. + + :param str span_name: Override the default span name. + + :return: An object suitable for use with :py:class:`aiohttp.ClientSession`. + :rtype: :py:class:`aiohttp.TraceConfig` + """ + # `aiohttp.TraceRequestStartParams` resolves to `aiohttp.tracing.TraceRequestStartParams` + # which doesn't exist in the aiottp intersphinx inventory. + # Explicitly specify the type for the `span_name` param and rtype to work + # around this issue. + + tracer = trace.get_tracer_provider().get_tracer(__name__, __version__) + + def _end_trace(trace_config_ctx: types.SimpleNamespace): + context_api.detach(trace_config_ctx.token) + trace_config_ctx.span.end() + + async def on_request_start( + unused_session: aiohttp.ClientSession, + trace_config_ctx: types.SimpleNamespace, + params: aiohttp.TraceRequestStartParams, + ): + http_method = params.method.upper() + if trace_config_ctx.span_name is None: + request_span_name = http_method + elif callable(trace_config_ctx.span_name): + request_span_name = str(trace_config_ctx.span_name(params)) + else: + request_span_name = str(trace_config_ctx.span_name) + + trace_config_ctx.span = trace_config_ctx.tracer.start_span( + request_span_name, + kind=SpanKind.CLIENT, + attributes={ + "component": "http", + "http.method": http_method, + "http.url": trace_config_ctx.url_filter(params.url) + if callable(trace_config_ctx.url_filter) + else str(params.url), + }, + ) + + trace_config_ctx.token = context_api.attach( + trace.propagation.set_span_in_context(trace_config_ctx.span) + ) + + propagators.inject( + tracer, type(params.headers).__setitem__, params.headers + ) + + async def on_request_end( + unused_session: aiohttp.ClientSession, + trace_config_ctx: types.SimpleNamespace, + params: aiohttp.TraceRequestEndParams, + ): + trace_config_ctx.span.set_status( + Status(http_status_to_canonical_code(int(params.response.status))) + ) + trace_config_ctx.span.set_attribute( + "http.status_code", params.response.status + ) + trace_config_ctx.span.set_attribute( + "http.status_text", params.response.reason + ) + _end_trace(trace_config_ctx) + + async def on_request_exception( + unused_session: aiohttp.ClientSession, + trace_config_ctx: types.SimpleNamespace, + params: aiohttp.TraceRequestExceptionParams, + ): + if isinstance( + params.exception, + (aiohttp.ServerTimeoutError, aiohttp.TooManyRedirects), + ): + status = StatusCanonicalCode.DEADLINE_EXCEEDED + # Assume any getaddrinfo error is a DNS failure. + elif isinstance( + params.exception, aiohttp.ClientConnectorError + ) and isinstance(params.exception.os_error, socket.gaierror): + # DNS resolution failed + status = StatusCanonicalCode.UNKNOWN + else: + status = StatusCanonicalCode.UNAVAILABLE + + trace_config_ctx.span.set_status(Status(status)) + _end_trace(trace_config_ctx) + + def _trace_config_ctx_factory(**kwargs): + kwargs.setdefault("trace_request_ctx", {}) + return types.SimpleNamespace( + span_name=span_name, tracer=tracer, url_filter=url_filter, **kwargs + ) + + trace_config = aiohttp.TraceConfig( + trace_config_ctx_factory=_trace_config_ctx_factory + ) + + trace_config.on_request_start.append(on_request_start) + trace_config.on_request_end.append(on_request_end) + trace_config.on_request_exception.append(on_request_exception) + + return trace_config diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py new file mode 100644 index 0000000000..fefa5ee917 --- /dev/null +++ b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py @@ -0,0 +1,15 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-aiohttp-client/tests/__init__.py b/ext/opentelemetry-ext-aiohttp-client/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py b/ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py new file mode 100644 index 0000000000..ae29801797 --- /dev/null +++ b/ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -0,0 +1,338 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import contextlib +import typing +import urllib.parse +from http import HTTPStatus + +import aiohttp +import aiohttp.test_utils +import yarl + +import opentelemetry.ext.aiohttp_client +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace.status import StatusCanonicalCode + + +class TestAioHttpIntegration(TestBase): + maxDiff = None + + def assert_spans(self, spans): + self.assertEqual( + [ + ( + span.name, + (span.status.canonical_code, span.status.description), + dict(span.attributes), + ) + for span in self.memory_exporter.get_finished_spans() + ], + spans, + ) + + def test_url_path_span_name(self): + for url, expected in ( + ( + yarl.URL("http://hostname.local:1234/some/path?query=params"), + "/some/path", + ), + (yarl.URL("http://hostname.local:1234"), "/"), + ): + with self.subTest(url=url): + params = aiohttp.TraceRequestStartParams("METHOD", url, {}) + actual = opentelemetry.ext.aiohttp_client.url_path_span_name( + params + ) + self.assertEqual(actual, expected) + self.assertIsInstance(actual, str) + + @staticmethod + def _http_request( + trace_config, + url: str, + method: str = "GET", + status_code: int = HTTPStatus.OK, + request_handler: typing.Callable = None, + **kwargs + ) -> typing.Tuple[str, int]: + """Helper to start an aiohttp test server and send an actual HTTP request to it.""" + + async def do_request(): + async def default_handler(unused_request): + return aiohttp.web.Response(status=int(status_code)) + + handler = request_handler or default_handler + + app = aiohttp.web.Application() + parsed_url = urllib.parse.urlparse(url) + app.add_routes([aiohttp.web.get(parsed_url.path, handler)]) + app.add_routes([aiohttp.web.post(parsed_url.path, handler)]) + app.add_routes([aiohttp.web.patch(parsed_url.path, handler)]) + + with contextlib.suppress(aiohttp.ClientError): + async with aiohttp.test_utils.TestServer(app) as server: + netloc = (server.host, server.port) + async with aiohttp.test_utils.TestClient( + server, trace_configs=[trace_config] + ) as client: + await client.start_server() + await client.request( + method, url, trace_request_ctx={}, **kwargs + ) + return netloc + + loop = asyncio.get_event_loop() + return loop.run_until_complete(do_request()) + + def test_http_status_to_canonical_code(self): + for status_code, expected in ( + (HTTPStatus.OK, StatusCanonicalCode.OK), + (HTTPStatus.ACCEPTED, StatusCanonicalCode.OK), + (HTTPStatus.IM_USED, StatusCanonicalCode.OK), + (HTTPStatus.MULTIPLE_CHOICES, StatusCanonicalCode.OK), + (HTTPStatus.BAD_REQUEST, StatusCanonicalCode.INVALID_ARGUMENT), + (HTTPStatus.UNAUTHORIZED, StatusCanonicalCode.UNAUTHENTICATED), + (HTTPStatus.FORBIDDEN, StatusCanonicalCode.PERMISSION_DENIED), + (HTTPStatus.NOT_FOUND, StatusCanonicalCode.NOT_FOUND), + ( + HTTPStatus.UNPROCESSABLE_ENTITY, + StatusCanonicalCode.INVALID_ARGUMENT, + ), + ( + HTTPStatus.TOO_MANY_REQUESTS, + StatusCanonicalCode.RESOURCE_EXHAUSTED, + ), + (HTTPStatus.NOT_IMPLEMENTED, StatusCanonicalCode.UNIMPLEMENTED), + (HTTPStatus.SERVICE_UNAVAILABLE, StatusCanonicalCode.UNAVAILABLE), + ( + HTTPStatus.GATEWAY_TIMEOUT, + StatusCanonicalCode.DEADLINE_EXCEEDED, + ), + ( + HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, + StatusCanonicalCode.INTERNAL, + ), + (600, StatusCanonicalCode.UNKNOWN), + (99, StatusCanonicalCode.UNKNOWN), + ): + with self.subTest(status_code=status_code): + actual = opentelemetry.ext.aiohttp_client.http_status_to_canonical_code( + int(status_code) + ) + self.assertEqual(actual, expected, status_code) + + def test_status_codes(self): + for status_code, span_status in ( + (HTTPStatus.OK, StatusCanonicalCode.OK), + (HTTPStatus.TEMPORARY_REDIRECT, StatusCanonicalCode.OK), + (HTTPStatus.SERVICE_UNAVAILABLE, StatusCanonicalCode.UNAVAILABLE), + ( + HTTPStatus.GATEWAY_TIMEOUT, + StatusCanonicalCode.DEADLINE_EXCEEDED, + ), + ): + with self.subTest(status_code=status_code): + host, port = self._http_request( + trace_config=opentelemetry.ext.aiohttp_client.create_trace_config(), + url="/test-path?query=param#foobar", + status_code=status_code, + ) + + self.assert_spans( + [ + ( + "GET", + (span_status, None), + { + "component": "http", + "http.method": "GET", + "http.url": "http://{}:{}/test-path?query=param#foobar".format( + host, port + ), + "http.status_code": int(status_code), + "http.status_text": status_code.phrase, + }, + ) + ] + ) + + self.memory_exporter.clear() + + def test_span_name_option(self): + for span_name, method, path, expected in ( + ("static", "POST", "/static-span-name", "static"), + ( + lambda params: "{} - {}".format( + params.method, params.url.path + ), + "PATCH", + "/some/path", + "PATCH - /some/path", + ), + ): + with self.subTest(span_name=span_name, method=method, path=path): + host, port = self._http_request( + trace_config=opentelemetry.ext.aiohttp_client.create_trace_config( + span_name=span_name + ), + method=method, + url=path, + status_code=HTTPStatus.OK, + ) + + self.assert_spans( + [ + ( + expected, + (StatusCanonicalCode.OK, None), + { + "component": "http", + "http.method": method, + "http.url": "http://{}:{}{}".format( + host, port, path + ), + "http.status_code": int(HTTPStatus.OK), + "http.status_text": HTTPStatus.OK.phrase, + }, + ) + ] + ) + self.memory_exporter.clear() + + def test_url_filter_option(self): + # Strips all query params from URL before adding as a span attribute. + def strip_query_params(url: yarl.URL) -> str: + return str(url.with_query(None)) + + host, port = self._http_request( + trace_config=opentelemetry.ext.aiohttp_client.create_trace_config( + url_filter=strip_query_params + ), + url="/some/path?query=param&other=param2", + status_code=HTTPStatus.OK, + ) + + self.assert_spans( + [ + ( + "GET", + (StatusCanonicalCode.OK, None), + { + "component": "http", + "http.method": "GET", + "http.url": "http://{}:{}/some/path".format( + host, port + ), + "http.status_code": int(HTTPStatus.OK), + "http.status_text": HTTPStatus.OK.phrase, + }, + ) + ] + ) + + def test_connection_errors(self): + trace_configs = [ + opentelemetry.ext.aiohttp_client.create_trace_config() + ] + + for url, expected_status in ( + ("http://this-is-unknown.local/", StatusCanonicalCode.UNKNOWN), + ("http://127.0.0.1:1/", StatusCanonicalCode.UNAVAILABLE), + ): + with self.subTest(expected_status=expected_status): + + async def do_request(url): + async with aiohttp.ClientSession( + trace_configs=trace_configs + ) as session: + async with session.get(url): + pass + + loop = asyncio.get_event_loop() + with self.assertRaises(aiohttp.ClientConnectorError): + loop.run_until_complete(do_request(url)) + + self.assert_spans( + [ + ( + "GET", + (expected_status, None), + { + "component": "http", + "http.method": "GET", + "http.url": url, + }, + ) + ] + ) + self.memory_exporter.clear() + + def test_timeout(self): + async def request_handler(unused_request): + await asyncio.sleep(1) + return aiohttp.web.Response() + + host, port = self._http_request( + trace_config=opentelemetry.ext.aiohttp_client.create_trace_config(), + url="/test_timeout", + request_handler=request_handler, + timeout=aiohttp.ClientTimeout(sock_read=0.01), + ) + + self.assert_spans( + [ + ( + "GET", + (StatusCanonicalCode.DEADLINE_EXCEEDED, None), + { + "component": "http", + "http.method": "GET", + "http.url": "http://{}:{}/test_timeout".format( + host, port + ), + }, + ) + ] + ) + + def test_too_many_redirects(self): + async def request_handler(request): + # Create a redirect loop. + location = request.url + raise aiohttp.web.HTTPFound(location=location) + + host, port = self._http_request( + trace_config=opentelemetry.ext.aiohttp_client.create_trace_config(), + url="/test_too_many_redirects", + request_handler=request_handler, + max_redirects=2, + ) + + self.assert_spans( + [ + ( + "GET", + (StatusCanonicalCode.DEADLINE_EXCEEDED, None), + { + "component": "http", + "http.method": "GET", + "http.url": "http://{}:{}/test_too_many_redirects".format( + host, port + ), + }, + ) + ] + ) diff --git a/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml b/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml index 3e642c584e..f996db077e 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml +++ b/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml @@ -3,18 +3,18 @@ version: '3' services: otmongo: ports: - - "27017:27017" + - "27017:27017" image: mongo:latest otmysql: - ports: - - "3306:3306" - image: mysql:latest - restart: always - environment: - MYSQL_USER: testuser - MYSQL_PASSWORD: testpassword - MYSQL_ALLOW_EMPTY_PASSWORD: "yes" - MYSQL_DATABASE: opentelemetry-tests + ports: + - "3306:3306" + image: mysql:latest + restart: always + environment: + MYSQL_USER: testuser + MYSQL_PASSWORD: testpassword + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + MYSQL_DATABASE: opentelemetry-tests otpostgres: image: postgres ports: @@ -26,5 +26,16 @@ services: otredis: image: redis:4.0-alpine ports: - - "127.0.0.1:6379:6379" - + - "127.0.0.1:6379:6379" + otjaeger: + image: jaegertracing/all-in-one:1.8 + environment: + COLLECTOR_ZIPKIN_HTTP_PORT: "9411" + ports: + - "5775:5775/udp" + - "6831:6831/udp" + - "6832:6832/udp" + - "5778:5778" + - "16686:16686" + - "14268:14268" + - "9411:9411" diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 839380c27f..1ff42d9e53 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -12,6 +12,8 @@ function cov { ${1} } +PYTHON_VERSION=$(python -c 'import sys; print(".".join(map(str, sys.version_info[:3])))') +PYTHON_VERSION_INFO=(${PYTHON_VERSION//./ }) coverage erase @@ -25,5 +27,10 @@ cov ext/opentelemetry-ext-wsgi cov ext/opentelemetry-ext-zipkin cov docs/examples/opentelemetry-example-app -coverage report +# aiohttp is only supported on Python 3.5+. +if [ ${PYTHON_VERSION_INFO[1]} -gt 4 ]; then + cov ext/opentelemetry-ext-aiohttp-client +fi + +coverage report --show-missing coverage xml diff --git a/tox.ini b/tox.ini index 84dd157dba..bb8f785861 100644 --- a/tox.ini +++ b/tox.ini @@ -28,9 +28,14 @@ envlist = py3{4,5,6,7,8}-test-example-http pypy3-test-example-http + ; opentelemetry-ext-aiohttp-client + py3{5,6,7,8}-test-ext-aiohttp-client + pypy3-test-ext-aiohttp-client + + ; opentelemetry-ext-django py3{6,7,8}-test-ext-django pypy3-test-ext-django - + ; opentelemetry-ext-dbapi py3{4,5,6,7,8}-test-ext-dbapi pypy3-test-ext-dbapi @@ -127,6 +132,7 @@ changedir = test-sdk: opentelemetry-sdk/tests test-auto-instrumentation: opentelemetry-auto-instrumentation/tests test-ext-grpc: ext/opentelemetry-ext-grpc/tests + test-ext-aiohttp-client: ext/opentelemetry-ext-aiohttp-client/tests test-ext-requests: ext/opentelemetry-ext-requests/tests test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests @@ -204,6 +210,9 @@ commands_pre = requests: pip install {toxinidir}/opentelemetry-auto-instrumentation requests: pip install {toxinidir}/ext/opentelemetry-ext-requests[test] + aiohttp-client: pip install {toxinidir}/opentelemetry-sdk + aiohttp-client: pip install {toxinidir}/ext/opentelemetry-ext-aiohttp-client + jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger opentracing-shim: pip install {toxinidir}/ext/opentelemetry-ext-opentracing-shim From c061d3cbb263b71941ea1ab99c6137b6b7ca071d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 8 May 2020 11:38:37 -0500 Subject: [PATCH 0327/1517] docs: Add missing dependencies and fix django (#657) - Add missing sqlalchemy and django dependencies - Configure django in docs/conf.py to avoid non configured exeception --- docs-requirements.txt | 2 ++ docs/conf.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/docs-requirements.txt b/docs-requirements.txt index ca3db13e0a..26b74cccc2 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -5,6 +5,7 @@ sphinx-autodoc-typehints~=1.10.2 # Required by ext packages aiohttp ~= 3.0 Deprecated>=1.2.6 +django>=2.2 PyMySQL~=0.9.3 flask~=1.0 mysql-connector-python~=8.0 @@ -13,5 +14,6 @@ prometheus_client>=0.5.0,<1.0.0 psycopg2-binary>=2.7.3.1 pymongo~=3.1 redis>=2.6 +sqlalchemy>=1.0 thrift>=0.10.0 wrapt >=1.0.0,<2.0.0 diff --git a/docs/conf.py b/docs/conf.py index c71995dfb4..3ea491b4fc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,6 +15,15 @@ from os import listdir from os.path import isdir, join +# configure django to avoid the following exception: +# django.core.exceptions.ImproperlyConfigured: Requested settings, but settings +# are not configured. You must either define the environment variable +# DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings. +from django.conf import settings + +settings.configure() + + source_dirs = [ os.path.abspath("../opentelemetry-api/src/"), os.path.abspath("../opentelemetry-sdk/src/"), From 0ffd71ef37e26a5b2a7af65e64823067b2c897a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 8 May 2020 21:04:36 -0500 Subject: [PATCH 0328/1517] ext/mysql: Add instrumentor interface (#655) Implement to helper methods to allow users to enable / disable instrumentation in a single connection object. Co-authored-by: Diego Hurtado Co-authored-by: alrex --- ext/opentelemetry-ext-dbapi/CHANGELOG.md | 2 + .../src/opentelemetry/ext/dbapi/__init__.py | 61 ++++++++++++- .../tests/test_dbapi_integration.py | 26 ++++++ .../tests/mysql/test_mysql_functional.py | 5 +- .../tests/pymysql/test_pymysql_functional.py | 1 + ext/opentelemetry-ext-mysql/CHANGELOG.md | 2 + ext/opentelemetry-ext-mysql/setup.cfg | 5 ++ .../src/opentelemetry/ext/mysql/__init__.py | 87 ++++++++++++++----- .../tests/test_mysql_integration.py | 87 +++++++++++++++---- ext/opentelemetry-ext-pymysql/CHANGELOG.md | 1 + .../src/opentelemetry/ext/pymysql/__init__.py | 61 ++++++++++--- .../tests/test_pymysql_integration.py | 37 ++++++++ tox.ini | 3 +- 13 files changed, 323 insertions(+), 55 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/CHANGELOG.md b/ext/opentelemetry-ext-dbapi/CHANGELOG.md index f32ad5bd4c..c259c66786 100644 --- a/ext/opentelemetry-ext-dbapi/CHANGELOG.md +++ b/ext/opentelemetry-ext-dbapi/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Implement instrument_connection and uninstrument_connection ([#624](https://github.com/open-telemetry/opentelemetry-python/pull/624)) + ## 0.4a0 Released 2020-02-21 diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index abb936bf7b..626b672d6c 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -103,7 +103,7 @@ def wrap_connect( """ # pylint: disable=unused-argument - def wrap_connect_( + def _wrap_connect( wrapped: typing.Callable[..., any], instance: typing.Any, args: typing.Tuple[any, any], @@ -119,7 +119,7 @@ def wrap_connect_( try: wrapt.wrap_function_wrapper( - connect_module, connect_method_name, wrap_connect_ + connect_module, connect_method_name, _wrap_connect ) except Exception as ex: # pylint: disable=broad-except logger.warning("Failed to integrate with DB API. %s", str(ex)) @@ -128,11 +128,65 @@ def wrap_connect_( def unwrap_connect( connect_module: typing.Callable[..., any], connect_method_name: str, ): + """Disable integration with DB API library. + https://www.python.org/dev/peps/pep-0249/ + + Args: + connect_module: Module name where the connect method is available. + connect_method_name: The connect method name. + """ conn = getattr(connect_module, connect_method_name, None) if isinstance(conn, wrapt.ObjectProxy): setattr(connect_module, connect_method_name, conn.__wrapped__) +def instrument_connection( + tracer, + connection, + database_component: str, + database_type: str = "", + connection_attributes: typing.Dict = None, +): + """Enable instrumentation in a database connection. + + Args: + tracer: The :class:`Tracer` to use. + connection: The connection to instrument. + database_component: Database driver name or database name "JDBI", + "jdbc", "odbc", "postgreSQL". + database_type: The Database type. For any SQL database, "sql". + connection_attributes: Attribute names for database, port, host and + user in a connection object. + + Returns: + An instrumented connection. + """ + db_integration = DatabaseApiIntegration( + tracer, + database_component, + database_type, + connection_attributes=connection_attributes, + ) + db_integration.get_connection_attributes(connection) + return TracedConnectionProxy(connection, db_integration) + + +def uninstrument_connection(connection): + """Disable instrumentation in a database connection. + + Args: + connection: The connection to uninstrument. + + Returns: + An uninstrumented connection. + """ + if isinstance(connection, wrapt.ObjectProxy): + return connection.__wrapped__ + + logger.warning("Connection is not instrumented") + return connection + + class DatabaseApiIntegration: def __init__( self, @@ -167,8 +221,7 @@ def wrapped_connection( """ connection = connect_method(*args, **kwargs) self.get_connection_attributes(connection) - traced_connection = TracedConnectionProxy(connection, self) - return traced_connection + return TracedConnectionProxy(connection, self) def get_connection_attributes(self, connection): # Populate span fields using connection diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py index 6614a8809b..88c243c5bf 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. + +import logging from unittest import mock from opentelemetry import trace as trace_api @@ -138,6 +140,30 @@ def test_unwrap_connect(self, mock_dbapi): self.assertEqual(mock_dbapi.connect.call_count, 2) self.assertIsInstance(connection, mock.Mock) + def test_instrument_connection(self): + connection = mock.Mock() + # Avoid get_attributes failing because can't concatenate mock + connection.database = "-" + connection2 = dbapi.instrument_connection(self.tracer, connection, "-") + self.assertIsInstance(connection2, dbapi.TracedConnectionProxy) + self.assertIs(connection2.__wrapped__, connection) + + def test_uninstrument_connection(self): + connection = mock.Mock() + # Set connection.database to avoid a failure because mock can't + # be concatenated + connection.database = "-" + connection2 = dbapi.instrument_connection(self.tracer, connection, "-") + self.assertIsInstance(connection2, dbapi.TracedConnectionProxy) + self.assertIs(connection2.__wrapped__, connection) + + connection3 = dbapi.uninstrument_connection(connection2) + self.assertIs(connection3, connection) + + with self.assertLogs(level=logging.WARNING): + connection4 = dbapi.uninstrument_connection(connection) + self.assertIs(connection4, connection) + # pylint: disable=unused-argument def mock_connect(*args, **kwargs): diff --git a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py index 46f63d3c66..d1f4c689f9 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py @@ -18,7 +18,7 @@ import mysql.connector from opentelemetry import trace as trace_api -from opentelemetry.ext.mysql import trace_integration +from opentelemetry.ext.mysql import MySQLInstrumentor from opentelemetry.test.test_base import TestBase MYSQL_USER = os.getenv("MYSQL_USER ", "testuser") @@ -35,7 +35,7 @@ def setUpClass(cls): cls._connection = None cls._cursor = None cls._tracer = cls.tracer_provider.get_tracer(__name__) - trace_integration(cls.tracer_provider) + MySQLInstrumentor().instrument() cls._connection = mysql.connector.connect( user=MYSQL_USER, password=MYSQL_PASSWORD, @@ -49,6 +49,7 @@ def setUpClass(cls): def tearDownClass(cls): if cls._connection: cls._connection.close() + MySQLInstrumentor().uninstrument() def validate_spans(self): spans = self.memory_exporter.get_finished_spans() diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py index 15ec6dccca..5b004fe18f 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py @@ -48,6 +48,7 @@ def setUpClass(cls): def tearDownClass(cls): if cls._connection: cls._connection.close() + PyMySQLInstrumentor().uninstrument() def validate_spans(self): spans = self.memory_exporter.get_finished_spans() diff --git a/ext/opentelemetry-ext-mysql/CHANGELOG.md b/ext/opentelemetry-ext-mysql/CHANGELOG.md index f32ad5bd4c..66dcecacce 100644 --- a/ext/opentelemetry-ext-mysql/CHANGELOG.md +++ b/ext/opentelemetry-ext-mysql/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Implement instrumentor interface ([#654](https://github.com/open-telemetry/opentelemetry-python/pull/654)) + ## 0.4a0 Released 2020-02-21 diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 0b592641ce..d71512ee79 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -42,6 +42,7 @@ packages=find_namespace: install_requires = opentelemetry-api == 0.7.dev0 opentelemetry-ext-dbapi == 0.7.dev0 + opentelemetry-auto-instrumentation == 0.7.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 @@ -51,3 +52,7 @@ test = [options.packages.find] where = src + +[options.entry_points] +opentelemetry_instrumentor = + mysql = opentelemetry.ext.pymysql:MySQLInstrumentor diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index 2a8b2ab3a8..a70c1d46ea 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -13,8 +13,8 @@ # limitations under the License. """ -The integration with MySQL supports the `mysql-connector`_ library and is specified -to ``trace_integration`` using ``'MySQL'``. +MySQL instrumentation supporting `mysql-connector`_, it can be enabled by +using ``MySQLInstrumentor``. .. _mysql-connector: https://pypi.org/project/mysql-connector/ @@ -26,12 +26,13 @@ import mysql.connector from opentelemetry import trace from opentelemetry.trace import TracerProvider - from opentelemetry.ext.mysql import trace_integration + from opentelemetry.ext.mysql import MySQLInstrumentor trace.set_tracer_provider(TracerProvider()) - trace_integration() - cnx = mysql.connector.connect(database='MySQL_Database') + MySQLInstrumentor().instrument() + + cnx = mysql.connector.connect(database="MySQL_Database") cursor = cnx.cursor() cursor.execute("INSERT INTO test (testField) VALUES (123)" cursor.close() @@ -45,29 +46,71 @@ import mysql.connector -from opentelemetry.ext.dbapi import wrap_connect +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.ext import dbapi from opentelemetry.ext.mysql.version import __version__ from opentelemetry.trace import TracerProvider, get_tracer -def trace_integration(tracer_provider: typing.Optional[TracerProvider] = None): - """Integrate with MySQL Connector/Python library. - https://dev.mysql.com/doc/connector-python/en/ - """ - - tracer = get_tracer(__name__, __version__, tracer_provider) - - connection_attributes = { +class MySQLInstrumentor(BaseInstrumentor): + _CONNECTION_ATTRIBUTES = { "database": "database", "port": "server_port", "host": "server_host", "user": "user", } - wrap_connect( - tracer, - mysql.connector, - "connect", - "mysql", - "sql", - connection_attributes, - ) + + _DATABASE_COMPONENT = "mysql" + _DATABASE_TYPE = "sql" + + def _instrument(self, **kwargs): + """Integrate with MySQL Connector/Python library. + https://dev.mysql.com/doc/connector-python/en/ + """ + tracer_provider = kwargs.get("tracer_provider") + + tracer = get_tracer(__name__, __version__, tracer_provider) + + dbapi.wrap_connect( + tracer, + mysql.connector, + "connect", + self._DATABASE_COMPONENT, + self._DATABASE_TYPE, + self._CONNECTION_ATTRIBUTES, + ) + + def _uninstrument(self, **kwargs): + """"Disable MySQL instrumentation""" + dbapi.unwrap_connect(mysql.connector, "connect") + + # pylint:disable=no-self-use + def instrument_connection(self, connection): + """Enable instrumentation in a MySQL connection. + + Args: + connection: The connection to instrument. + + Returns: + An instrumented connection. + """ + tracer = get_tracer(__name__, __version__) + + return dbapi.instrument_connection( + tracer, + connection, + self._DATABASE_COMPONENT, + self._DATABASE_TYPE, + self._CONNECTION_ATTRIBUTES, + ) + + def uninstrument_connection(self, connection): + """Disable instrumentation in a MySQL connection. + + Args: + connection: The connection to uninstrument. + + Returns: + An uninstrumented connection. + """ + return dbapi.uninstrument_connection(connection) diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py index 39a70bc551..6a86ef8927 100644 --- a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -17,19 +17,26 @@ import mysql.connector import opentelemetry.ext.mysql +from opentelemetry.ext.mysql import MySQLInstrumentor from opentelemetry.sdk import resources from opentelemetry.test.test_base import TestBase class TestMysqlIntegration(TestBase): - def test_trace_integration(self): - with mock.patch("mysql.connector.connect") as mock_connect: - mock_connect.get.side_effect = mysql.connector.MySQLConnection() - opentelemetry.ext.mysql.trace_integration() - cnx = mysql.connector.connect(database="test") - cursor = cnx.cursor() - query = "SELECT * FROM test" - cursor.execute(query) + def tearDown(self): + super().tearDown() + with self.disable_logging(): + MySQLInstrumentor().uninstrument() + + @mock.patch("mysql.connector.connect") + # pylint: disable=unused-argument + def test_instrumentor(self, mock_connect): + MySQLInstrumentor().instrument() + + cnx = mysql.connector.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @@ -38,21 +45,69 @@ def test_trace_integration(self): # Check version and name in span's instrumentation info self.check_span_instrumentation_info(span, opentelemetry.ext.mysql) - def test_custom_tracer_provider(self): + # check that no spans are generated after uninstrumen + MySQLInstrumentor().uninstrument() + + cnx = mysql.connector.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + @mock.patch("mysql.connector.connect") + # pylint: disable=unused-argument + def test_custom_tracer_provider(self, mock_connect): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result - with mock.patch("mysql.connector.connect") as mock_connect: - mock_connect.get.side_effect = mysql.connector.MySQLConnection() - opentelemetry.ext.mysql.trace_integration(tracer_provider) - cnx = mysql.connector.connect(database="test") - cursor = cnx.cursor() - query = "SELECT * FROM test" - cursor.execute(query) + MySQLInstrumentor().instrument(tracer_provider=tracer_provider) + cnx = mysql.connector.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) span_list = exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertIs(span.resource, resource) + + @mock.patch("mysql.connector.connect") + # pylint: disable=unused-argument + def test_instrument_connection(self, mock_connect): + cnx = mysql.connector.connect(database="test") + query = "SELECT * FROM test" + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) + + cnx = MySQLInstrumentor().instrument_connection(cnx) + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + @mock.patch("mysql.connector.connect") + # pylint: disable=unused-argument + def test_uninstrument_connection(self, mock_connect): + MySQLInstrumentor().instrument() + cnx = mysql.connector.connect(database="test") + query = "SELECT * FROM test" + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + cnx = MySQLInstrumentor().uninstrument_connection(cnx) + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) diff --git a/ext/opentelemetry-ext-pymysql/CHANGELOG.md b/ext/opentelemetry-ext-pymysql/CHANGELOG.md index 224f1bac53..940018bc41 100644 --- a/ext/opentelemetry-ext-pymysql/CHANGELOG.md +++ b/ext/opentelemetry-ext-pymysql/CHANGELOG.md @@ -2,5 +2,6 @@ ## Unreleased +- Implement instrument_connection and uninstrument_connection ([#624](https://github.com/open-telemetry/opentelemetry-python/pull/624)) - Implement PyMySQL integration ([#504](https://github.com/open-telemetry/opentelemetry-python/pull/504)) - Implement instrumentor interface ([#611](https://github.com/open-telemetry/opentelemetry-python/pull/611)) diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py index 29baedc5cd..14a320fdd0 100644 --- a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py @@ -48,12 +48,22 @@ import pymysql from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.ext.dbapi import unwrap_connect, wrap_connect +from opentelemetry.ext import dbapi from opentelemetry.ext.pymysql.version import __version__ from opentelemetry.trace import TracerProvider, get_tracer class PyMySQLInstrumentor(BaseInstrumentor): + _CONNECTION_ATTRIBUTES = { + "database": "db", + "port": "port", + "host": "host", + "user": "user", + } + + _DATABASE_COMPONENT = "mysql" + _DATABASE_TYPE = "sql" + def _instrument(self, **kwargs): """Integrate with the PyMySQL library. https://github.com/PyMySQL/PyMySQL/ @@ -62,15 +72,46 @@ def _instrument(self, **kwargs): tracer = get_tracer(__name__, __version__, tracer_provider) - connection_attributes = { - "database": "db", - "port": "port", - "host": "host", - "user": "user", - } - wrap_connect( - tracer, pymysql, "connect", "mysql", "sql", connection_attributes + dbapi.wrap_connect( + tracer, + pymysql, + "connect", + self._DATABASE_COMPONENT, + self._DATABASE_TYPE, + self._CONNECTION_ATTRIBUTES, ) def _uninstrument(self, **kwargs): - unwrap_connect(pymysql, "connect") + """"Disable PyMySQL instrumentation""" + dbapi.unwrap_connect(pymysql, "connect") + + # pylint:disable=no-self-use + def instrument_connection(self, connection): + """Enable instrumentation in a PyMySQL connection. + + Args: + connection: The connection to instrument. + + Returns: + An instrumented connection. + """ + tracer = get_tracer(__name__, __version__) + + return dbapi.instrument_connection( + tracer, + connection, + self._DATABASE_COMPONENT, + self._DATABASE_TYPE, + self._CONNECTION_ATTRIBUTES, + ) + + def uninstrument_connection(self, connection): + """Disable instrumentation in a PyMySQL connection. + + Args: + connection: The connection to uninstrument. + + Returns: + An uninstrumented connection. + """ + return dbapi.uninstrument_connection(connection) diff --git a/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py b/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py index efcd8af2f7..60c060f117 100644 --- a/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py +++ b/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py @@ -75,3 +75,40 @@ def test_custom_tracer_provider(self, mock_connect): span = spans_list[0] self.assertIs(span.resource, resource) + + @mock.patch("pymysql.connect") + # pylint: disable=unused-argument + def test_instrument_connection(self, mock_connect): + cnx = pymysql.connect(database="test") + query = "SELECT * FROM test" + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) + + cnx = PyMySQLInstrumentor().instrument_connection(cnx) + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + @mock.patch("pymysql.connect") + # pylint: disable=unused-argument + def test_uninstrument_connection(self, mock_connect): + PyMySQLInstrumentor().instrument() + cnx = pymysql.connect(database="test") + query = "SELECT * FROM test" + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + cnx = PyMySQLInstrumentor().uninstrument_connection(cnx) + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) diff --git a/tox.ini b/tox.ini index bb8f785861..0ca6fb376f 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,7 @@ envlist = ; opentelemetry-ext-django py3{6,7,8}-test-ext-django pypy3-test-ext-django - + ; opentelemetry-ext-dbapi py3{4,5,6,7,8}-test-ext-dbapi pypy3-test-ext-dbapi @@ -187,6 +187,7 @@ commands_pre = django: pip install {toxinidir}/ext/opentelemetry-ext-django[test] + mysql: pip install {toxinidir}/opentelemetry-auto-instrumentation mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql[test] From f15230bb9e871f13b1b71a48ee9896e247ab157d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Sat, 9 May 2020 00:32:29 -0500 Subject: [PATCH 0329/1517] ext/{dbapi,pymysql}: Implement methods to (un)-instrument connections (#624) Implement to helper methods to allow users to enable / disable instrumentation in a single connection object. The current integrations based on dbapi allow to patch the connect function on those libraries to return an instrumented connection, however it's not possible to enable instrumentation on a connection previously created or to disable instrumentation in an instrumented connection. This only implements the logic in PyMySQL, it can be easily done in other instrumentations once we think it's worth to be done. Co-authored-by: Diego Hurtado Co-authored-by: Yusuke Tsutsumi From b2e671a70553926b1c48e38f161dc32c1734a529 Mon Sep 17 00:00:00 2001 From: Nir Hadassi Date: Mon, 11 May 2020 08:15:07 +0300 Subject: [PATCH 0330/1517] ext/requests - apply custom attributes callback (#656) Implement passing an optional span_callback callback when instrumenting requests. The callback will be called just before returning the result and will be invoked with the span and the result (type of requests.Response). This mimics the same functionality as the js http plugin. --- .../opentelemetry/ext/requests/__init__.py | 20 ++++++++++-- .../tests/test_requests_integration.py | 31 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py index c98a24cc88..2a82e9820c 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py @@ -55,7 +55,7 @@ # pylint: disable=unused-argument -def _instrument(tracer_provider=None): +def _instrument(tracer_provider=None, span_callback=None): """Enables tracing of all requests calls that go through :code:`requests.session.Session.request` (this includes :code:`requests.get`, etc.).""" @@ -101,6 +101,8 @@ def instrumented_request(self, method, url, *args, **kwargs): span.set_status( Status(_http_status_to_canonical_code(result.status_code)) ) + if span_callback is not None: + span_callback(span, result) return result @@ -156,8 +158,22 @@ def _http_status_to_canonical_code(code: int, allow_redirect: bool = True): class RequestsInstrumentor(BaseInstrumentor): + """An instrumentor for requests + See `BaseInstrumentor` + """ + def _instrument(self, **kwargs): - _instrument(tracer_provider=kwargs.get("tracer_provider")) + """Instruments requests module + + Args: + **kwargs: Optional arguments + ``tracer_provider``: a TracerProvider, defaults to global + ``span_callback``: An optional callback invoked before returning the http response. Invoked with Span and requests.Response + """ + _instrument( + tracer_provider=kwargs.get("tracer_provider"), + span_callback=kwargs.get("span_callback"), + ) def _uninstrument(self, **kwargs): _uninstrument() diff --git a/ext/opentelemetry-ext-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-requests/tests/test_requests_integration.py index 7764aad3ec..28359d8f38 100644 --- a/ext/opentelemetry-ext-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-requests/tests/test_requests_integration.py @@ -183,6 +183,37 @@ def test_distributed_context(self): finally: propagators.set_global_httptextformat(previous_propagator) + def test_span_callback(self): + RequestsInstrumentor().uninstrument() + + def span_callback(span, result: requests.Response): + span.set_attribute( + "http.response.body", result.content.decode("utf-8") + ) + + RequestsInstrumentor().instrument( + tracer_provider=self.tracer_provider, span_callback=span_callback, + ) + + result = requests.get(self.URL) + self.assertEqual(result.text, "Hello!") + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + span = span_list[0] + + self.assertEqual( + span.attributes, + { + "component": "http", + "http.method": "GET", + "http.url": self.URL, + "http.status_code": 200, + "http.status_text": "OK", + "http.response.body": "Hello!", + }, + ) + def test_custom_tracer_provider(self): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) From 1140332e9ce5b30d1ad37d9118d5f69f5d6afc16 Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Mon, 11 May 2020 12:33:06 -0400 Subject: [PATCH 0331/1517] jinja2: Add jinja2 instrumentation (#643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrating the jinja2 plugin forked from ddtracepy. Co-authored-by: Mauricio Vásquez --- docs/ext/jinja2/jinja2.rst | 7 + ext/opentelemetry-ext-jinja2/CHANGELOG.md | 5 + ext/opentelemetry-ext-jinja2/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-jinja2/MANIFEST.in | 9 + ext/opentelemetry-ext-jinja2/README.rst | 21 ++ ext/opentelemetry-ext-jinja2/setup.cfg | 56 +++++ ext/opentelemetry-ext-jinja2/setup.py | 26 +++ .../src/opentelemetry/ext/jinja2/__init__.py | 147 +++++++++++++ .../src/opentelemetry/ext/jinja2/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/templates/base.html | 1 + .../tests/templates/template.html | 2 + .../tests/test_jinja2.py | 194 +++++++++++++++++ tox.ini | 8 + 14 files changed, 692 insertions(+) create mode 100644 docs/ext/jinja2/jinja2.rst create mode 100644 ext/opentelemetry-ext-jinja2/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-jinja2/LICENSE create mode 100644 ext/opentelemetry-ext-jinja2/MANIFEST.in create mode 100644 ext/opentelemetry-ext-jinja2/README.rst create mode 100644 ext/opentelemetry-ext-jinja2/setup.cfg create mode 100644 ext/opentelemetry-ext-jinja2/setup.py create mode 100644 ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/__init__.py create mode 100644 ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py create mode 100644 ext/opentelemetry-ext-jinja2/tests/__init__.py create mode 100644 ext/opentelemetry-ext-jinja2/tests/templates/base.html create mode 100644 ext/opentelemetry-ext-jinja2/tests/templates/template.html create mode 100644 ext/opentelemetry-ext-jinja2/tests/test_jinja2.py diff --git a/docs/ext/jinja2/jinja2.rst b/docs/ext/jinja2/jinja2.rst new file mode 100644 index 0000000000..d9b461627d --- /dev/null +++ b/docs/ext/jinja2/jinja2.rst @@ -0,0 +1,7 @@ +OpenTelemetry Jinja2 Instrumentation +==================================== + +.. automodule:: opentelemetry.ext.jinja2 + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-jinja2/CHANGELOG.md b/ext/opentelemetry-ext-jinja2/CHANGELOG.md new file mode 100644 index 0000000000..bd0f6b54ae --- /dev/null +++ b/ext/opentelemetry-ext-jinja2/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Add jinja2 instrumentation ([#643](https://github.com/open-telemetry/opentelemetry-python/pull/643)) diff --git a/ext/opentelemetry-ext-jinja2/LICENSE b/ext/opentelemetry-ext-jinja2/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-jinja2/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-jinja2/MANIFEST.in b/ext/opentelemetry-ext-jinja2/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-jinja2/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-jinja2/README.rst b/ext/opentelemetry-ext-jinja2/README.rst new file mode 100644 index 0000000000..09e74e21d3 --- /dev/null +++ b/ext/opentelemetry-ext-jinja2/README.rst @@ -0,0 +1,21 @@ +OpenTelemetry jinja2 integration +================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-jinja2.svg + :target: https://pypi.org/project/opentelemetry-ext-jinja2/ + +Installation +------------ + +:: + + pip install opentelemetry-ext-jinja2 + + +References +---------- + +* `OpenTelemetry jinja2 integration `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-jinja2/setup.cfg b/ext/opentelemetry-ext-jinja2/setup.cfg new file mode 100644 index 0000000000..8aa18bab8e --- /dev/null +++ b/ext/opentelemetry-ext-jinja2/setup.cfg @@ -0,0 +1,56 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-jinja2 +description = OpenTelemetry jinja2 integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-jinja2 +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.7.dev0 + opentelemetry-auto-instrumentation == 0.7.dev0 + jinja2~=2.7 + wrapt >= 1.0.0, < 2.0.0 + +[options.extras_require] +test = + opentelemetry-test == 0.7.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + jinja2 = opentelemetry.ext.jinja2:Jinja2Instrumentor diff --git a/ext/opentelemetry-ext-jinja2/setup.py b/ext/opentelemetry-ext-jinja2/setup.py new file mode 100644 index 0000000000..323c1b3353 --- /dev/null +++ b/ext/opentelemetry-ext-jinja2/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "jinja2", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/__init__.py b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/__init__.py new file mode 100644 index 0000000000..4aabb832ba --- /dev/null +++ b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/__init__.py @@ -0,0 +1,147 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" + +Usage +----- + +The OpenTelemetry ``jinja2`` integration traces templates loading, compilation +and rendering. + +Usage +----- + +.. code-block:: python + + from jinja2 import Environment, FileSystemLoader + from opentelemetry.ext.jinja2 import Jinja2Instrumentor + from opentelemetry import trace + from opentelemetry.trace import TracerProvider + + trace.set_tracer_provider(TracerProvider()) + + Jinja2Instrumentor().instrument() + + env = Environment(loader=FileSystemLoader("templates")) + template = env.get_template("mytemplate.html") + +API +--- +""" +# pylint: disable=no-value-for-parameter + +import logging + +import jinja2 +from wrapt import ObjectProxy +from wrapt import wrap_function_wrapper as _wrap + +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.ext.jinja2.version import __version__ +from opentelemetry.trace import SpanKind, get_tracer +from opentelemetry.trace.status import Status, StatusCanonicalCode + +logger = logging.getLogger(__name__) + +ATTRIBUTE_JINJA2_TEMPLATE_NAME = "jinja2.template_name" +ATTRIBUTE_JINJA2_TEMPLATE_PATH = "jinja2.template_path" +DEFAULT_TEMPLATE_NAME = "" + + +def _with_tracer_wrapper(func): + """Helper for providing tracer for wrapper functions. + """ + + def _with_tracer(tracer): + def wrapper(wrapped, instance, args, kwargs): + return func(tracer, wrapped, instance, args, kwargs) + + return wrapper + + return _with_tracer + + +@_with_tracer_wrapper +def _wrap_render(tracer, wrapped, instance, args, kwargs): + """Wrap `Template.render()` or `Template.generate()` + """ + template_name = instance.name or DEFAULT_TEMPLATE_NAME + attributes = {ATTRIBUTE_JINJA2_TEMPLATE_NAME: template_name} + with tracer.start_as_current_span( + "jinja2.render", kind=SpanKind.INTERNAL, attributes=attributes + ): + return wrapped(*args, **kwargs) + + +@_with_tracer_wrapper +def _wrap_compile(tracer, wrapped, _, args, kwargs): + template_name = ( + args[1] if len(args) > 1 else kwargs.get("name", DEFAULT_TEMPLATE_NAME) + ) + attributes = {ATTRIBUTE_JINJA2_TEMPLATE_NAME: template_name} + with tracer.start_as_current_span( + "jinja2.compile", kind=SpanKind.INTERNAL, attributes=attributes + ): + return wrapped(*args, **kwargs) + + +@_with_tracer_wrapper +def _wrap_load_template(tracer, wrapped, _, args, kwargs): + template_name = kwargs.get("name", args[0]) + attributes = {ATTRIBUTE_JINJA2_TEMPLATE_NAME: template_name} + with tracer.start_as_current_span( + "jinja2.load", kind=SpanKind.INTERNAL, attributes=attributes + ) as span: + template = None + try: + template = wrapped(*args, **kwargs) + return template + finally: + if template: + span.set_attribute( + ATTRIBUTE_JINJA2_TEMPLATE_PATH, template.filename + ) + + +def _unwrap(obj, attr): + func = getattr(obj, attr, None) + if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"): + setattr(obj, attr, func.__wrapped__) + + +class Jinja2Instrumentor(BaseInstrumentor): + """An instrumentor for jinja2 + + See `BaseInstrumentor` + """ + + def _instrument(self, **kwargs): + tracer_provider = kwargs.get("tracer_provider") + tracer = get_tracer(__name__, __version__, tracer_provider) + + _wrap(jinja2, "environment.Template.render", _wrap_render(tracer)) + _wrap(jinja2, "environment.Template.generate", _wrap_render(tracer)) + _wrap(jinja2, "environment.Environment.compile", _wrap_compile(tracer)) + _wrap( + jinja2, + "environment.Environment._load_template", + _wrap_load_template(tracer), + ) + + def _uninstrument(self, **kwargs): + _unwrap(jinja2.Template, "render") + _unwrap(jinja2.Template, "generate") + _unwrap(jinja2.Environment, "compile") + _unwrap(jinja2.Environment, "_load_template") diff --git a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py new file mode 100644 index 0000000000..86c61362ab --- /dev/null +++ b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-jinja2/tests/__init__.py b/ext/opentelemetry-ext-jinja2/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-jinja2/tests/templates/base.html b/ext/opentelemetry-ext-jinja2/tests/templates/base.html new file mode 100644 index 0000000000..05490d0c02 --- /dev/null +++ b/ext/opentelemetry-ext-jinja2/tests/templates/base.html @@ -0,0 +1 @@ +Message: {% block content %}{% endblock %} diff --git a/ext/opentelemetry-ext-jinja2/tests/templates/template.html b/ext/opentelemetry-ext-jinja2/tests/templates/template.html new file mode 100644 index 0000000000..ab28182415 --- /dev/null +++ b/ext/opentelemetry-ext-jinja2/tests/templates/template.html @@ -0,0 +1,2 @@ +{% extends 'base.html' %} +{% block content %}Hello {{name}}!{% endblock %} diff --git a/ext/opentelemetry-ext-jinja2/tests/test_jinja2.py b/ext/opentelemetry-ext-jinja2/tests/test_jinja2.py new file mode 100644 index 0000000000..6abfa9a836 --- /dev/null +++ b/ext/opentelemetry-ext-jinja2/tests/test_jinja2.py @@ -0,0 +1,194 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import jinja2 + +from opentelemetry import trace as trace_api +from opentelemetry.ext.jinja2 import Jinja2Instrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import get_tracer + +TEST_DIR = os.path.dirname(os.path.realpath(__file__)) +TMPL_DIR = os.path.join(TEST_DIR, "templates") + + +class TestJinja2Instrumentor(TestBase): + def setUp(self): + super().setUp() + Jinja2Instrumentor().instrument() + # prevent cache effects when using Template('code...') + # pylint: disable=protected-access + jinja2.environment._spontaneous_environments.clear() + self.tracer = get_tracer(__name__) + + def tearDown(self): + super().tearDown() + Jinja2Instrumentor().uninstrument() + + def test_render_inline_template_with_root(self): + with self.tracer.start_as_current_span("root"): + template = jinja2.environment.Template("Hello {{name}}!") + self.assertEqual(template.render(name="Jinja"), "Hello Jinja!") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 3) + + # pylint:disable=unbalanced-tuple-unpacking + render, template, root = spans[:3] + + self.assertIs(render.parent, root.get_context()) + self.assertIs(template.parent, root.get_context()) + self.assertIsNone(root.parent) + + def test_render_inline_template(self): + template = jinja2.environment.Template("Hello {{name}}!") + self.assertEqual(template.render(name="Jinja"), "Hello Jinja!") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + + # pylint:disable=unbalanced-tuple-unpacking + template, render = spans + + self.assertEqual(template.name, "jinja2.compile") + self.assertIs(template.kind, trace_api.SpanKind.INTERNAL) + self.assertEqual( + template.attributes, {"jinja2.template_name": ""}, + ) + + self.assertEqual(render.name, "jinja2.render") + self.assertIs(render.kind, trace_api.SpanKind.INTERNAL) + self.assertEqual( + render.attributes, {"jinja2.template_name": ""}, + ) + + def test_generate_inline_template_with_root(self): + with self.tracer.start_as_current_span("root"): + template = jinja2.environment.Template("Hello {{name}}!") + self.assertEqual( + "".join(template.generate(name="Jinja")), "Hello Jinja!" + ) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 3) + + # pylint:disable=unbalanced-tuple-unpacking + template, generate, root = spans + + self.assertIs(generate.parent, root.get_context()) + self.assertIs(template.parent, root.get_context()) + self.assertIsNone(root.parent) + + def test_generate_inline_template(self): + template = jinja2.environment.Template("Hello {{name}}!") + self.assertEqual( + "".join(template.generate(name="Jinja")), "Hello Jinja!" + ) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + + # pylint:disable=unbalanced-tuple-unpacking + template, generate = spans[:2] + + self.assertEqual(template.name, "jinja2.compile") + self.assertIs(template.kind, trace_api.SpanKind.INTERNAL) + self.assertEqual( + template.attributes, {"jinja2.template_name": ""}, + ) + + self.assertEqual(generate.name, "jinja2.render") + self.assertIs(generate.kind, trace_api.SpanKind.INTERNAL) + self.assertEqual( + generate.attributes, {"jinja2.template_name": ""}, + ) + + def test_file_template_with_root(self): + with self.tracer.start_as_current_span("root"): + loader = jinja2.loaders.FileSystemLoader(TMPL_DIR) + env = jinja2.Environment(loader=loader) + template = env.get_template("template.html") + self.assertEqual( + template.render(name="Jinja"), "Message: Hello Jinja!" + ) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 6) + + # pylint:disable=unbalanced-tuple-unpacking + compile2, load2, compile1, load1, render, root = spans + + self.assertIs(compile2.parent, load2.get_context()) + self.assertIs(load2.parent, root.get_context()) + self.assertIs(compile1.parent, load1.get_context()) + self.assertIs(load1.parent, render.get_context()) + self.assertIs(render.parent, root.get_context()) + self.assertIsNone(root.parent) + + def test_file_template(self): + loader = jinja2.loaders.FileSystemLoader(TMPL_DIR) + env = jinja2.Environment(loader=loader) + template = env.get_template("template.html") + self.assertEqual( + template.render(name="Jinja"), "Message: Hello Jinja!" + ) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 5) + + # pylint:disable=unbalanced-tuple-unpacking + compile2, load2, compile1, load1, render = spans + + self.assertEqual(compile2.name, "jinja2.compile") + self.assertEqual(load2.name, "jinja2.load") + self.assertEqual(compile1.name, "jinja2.compile") + self.assertEqual(load1.name, "jinja2.load") + self.assertEqual(render.name, "jinja2.render") + + self.assertEqual( + compile2.attributes, {"jinja2.template_name": "template.html"}, + ) + self.assertEqual( + load2.attributes, + { + "jinja2.template_name": "template.html", + "jinja2.template_path": os.path.join( + TMPL_DIR, "template.html" + ), + }, + ) + self.assertEqual( + compile1.attributes, {"jinja2.template_name": "base.html"}, + ) + self.assertEqual( + load1.attributes, + { + "jinja2.template_name": "base.html", + "jinja2.template_path": os.path.join(TMPL_DIR, "base.html"), + }, + ) + self.assertEqual( + render.attributes, {"jinja2.template_name": "template.html"}, + ) + + def test_uninstrumented(self): + Jinja2Instrumentor().uninstrument() + + jinja2.environment.Template("Hello {{name}}!") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + + Jinja2Instrumentor().instrument() diff --git a/tox.ini b/tox.ini index 0ca6fb376f..8d4971d320 100644 --- a/tox.ini +++ b/tox.ini @@ -48,6 +48,10 @@ envlist = py3{4,5,6,7,8}-test-ext-requests pypy3-test-ext-requests + ; opentelemetry-ext-jinja2 + py3{4,5,6,7,8}-test-ext-jinja2 + pypy3-test-ext-jinja2 + ; opentelemetry-ext-jaeger py3{4,5,6,7,8}-test-ext-jaeger pypy3-test-ext-jaeger @@ -134,6 +138,7 @@ changedir = test-ext-grpc: ext/opentelemetry-ext-grpc/tests test-ext-aiohttp-client: ext/opentelemetry-ext-aiohttp-client/tests test-ext-requests: ext/opentelemetry-ext-requests/tests + test-ext-jinja2: ext/opentelemetry-ext-jinja2/tests test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests test-ext-django: ext/opentelemetry-ext-django/tests @@ -211,6 +216,9 @@ commands_pre = requests: pip install {toxinidir}/opentelemetry-auto-instrumentation requests: pip install {toxinidir}/ext/opentelemetry-ext-requests[test] + jinja2: pip install {toxinidir}/opentelemetry-auto-instrumentation + jinja2: pip install {toxinidir}/ext/opentelemetry-ext-jinja2[test] + aiohttp-client: pip install {toxinidir}/opentelemetry-sdk aiohttp-client: pip install {toxinidir}/ext/opentelemetry-ext-aiohttp-client From fc2d073d73f8d10ca1f16eed7ef7d516a26e1724 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 11 May 2020 13:52:33 -0700 Subject: [PATCH 0332/1517] Adding script and workflow to automate the preparation of releases (#615) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prepare-release workflow is triggered when a new release/ branch is created. This workflow parses the number from the branch name and updates version.py/changelog.md files accordingly. It then creates a pull request with those changes. This addresses part of #374. Co-authored-by: Mauricio Vásquez --- .github/workflows/prepare-release.yml | 27 +++++++++ scripts/prepare_release.sh | 80 +++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 .github/workflows/prepare-release.yml create mode 100755 scripts/prepare_release.sh diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000000..7d15b02bed --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,27 @@ +name: prepare-release +on: + push: + branch: [ 'release/*' ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Get the version + id: get_version + run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + + - name: Prepare the release + id: update + run: | + ./scripts/prepare_release.sh ${{ steps.get_version.outputs.VERSION }} + + - name: Create Pull Request + id: create-pr + uses: peter-evans/create-pull-request@v2.7.0 + with: + branch: ${{ steps.get_version.outputs.VERSION }}-auto + title: '[pre-release] Update changelogs, version [${{ steps.get_version.outputs.VERSION }}]' + if: ${{ steps.update.outputs.version_updated == 1 }} diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh new file mode 100755 index 0000000000..23dd038b40 --- /dev/null +++ b/scripts/prepare_release.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# +# This script: +# 1. parses the version number from the branch name +# 2. updates version.py files to match that version +# 3. iterates through CHANGELOG.md files and updates any files containing +# unreleased changes +# 4. sets the output variable 'version_updated' to determine whether +# the github action to create a pull request should run. this allows +# maintainers to merge changes back into the release branch without +# triggering unnecessary pull requests +# + +VERSION=`echo $1 | awk -F "/" '{print $NF}'` +echo "Using version ${VERSION}" + +# check the version matches expected versioning e.g +# 0.6, 0.6b, 0.6b0, 0.6.0 +if [[ ! "${VERSION}" =~ ^([0-9])(\.*[0-9]{1,5}[a-b]*){1,3}$ ]]; then + echo "Version number invalid: $VERSION" + exit 1 +fi + +function update_version_file() { + errors=0 + for f in `find . -name version.py`; do + # check if version is already in version.py + grep -q ${VERSION} $f; + rc=$? + if [ $rc == 0 ]; then + errors=1 + echo "${f} already contains ${VERSION}" + continue + fi + # update version.py + perl -i -pe "s/__version__.*/__version__ = \"${VERSION}\"/g" ${f}; + git add ${f}; + echo "Updating ${f}" + done + if [ ${errors} != 0 ]; then + echo "::set-output name=version_updated::0" + exit 0 + fi +} + +function update_changelog() { + errors=0 + for f in `find . -name CHANGELOG.md`; do + # check if version is already in CHANGELOG + grep -q ${VERSION} $f; + rc=$? + if [ $rc == 0 ]; then + errors=1 + echo "${f} already contains ${VERSION}" + continue + fi + # check if changelog contains any new details + changes=`sed -n '/## Unreleased/,/^##/p' ${f} | grep -v '^##' | wc -w | awk '{$1=$1;print}'` + if [ ${changes} != "0" ]; then + # update CHANGELOG.md + perl -i -pe 's/## Unreleased.*/## Unreleased\n\n## '${VERSION}'/' ${f}; + git add ${f}; + echo "Updating ${f}" + else + echo "Skipping ${f}, no changes detected" + fi + done + if [ ${errors} != 0 ]; then + echo "::set-output name=version_updated::0" + exit 0 + fi +} + +update_version_file +update_changelog + +git config --local user.email "action@github.com" +git config --local user.name "GitHub Action" +git commit -m "updating changelogs and version to ${VERSION}" +echo "::set-output name=version_updated::1" From bf850cdf89ab071e47be7f9c8c2732f7c086ae5b Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 11 May 2020 14:12:48 -0700 Subject: [PATCH 0333/1517] Adding released date (#672) Adding a release date to the CHANGELOG.md --- scripts/prepare_release.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh index 23dd038b40..73ee141092 100755 --- a/scripts/prepare_release.sh +++ b/scripts/prepare_release.sh @@ -45,6 +45,7 @@ function update_version_file() { function update_changelog() { errors=0 + RELEASE_DATE=`date +%F` for f in `find . -name CHANGELOG.md`; do # check if version is already in CHANGELOG grep -q ${VERSION} $f; @@ -58,7 +59,7 @@ function update_changelog() { changes=`sed -n '/## Unreleased/,/^##/p' ${f} | grep -v '^##' | wc -w | awk '{$1=$1;print}'` if [ ${changes} != "0" ]; then # update CHANGELOG.md - perl -i -pe 's/## Unreleased.*/## Unreleased\n\n## '${VERSION}'/' ${f}; + perl -i -pe 's/## Unreleased.*/## Unreleased\n\n## '${VERSION}'\n\nReleased '${RELEASE_DATE}'/' ${f}; git add ${f}; echo "Updating ${f}" else From 0ae01b99b952fbc3af37faae93dbc9ac5d6c5fad Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 11 May 2020 14:15:12 -0700 Subject: [PATCH 0334/1517] docs: backfill changelogs since 0.6 (#671) Halfway through the 0.6 to 0.7 release, a policy was added to ensure changelogs are added to each PR as they are merged in. Retroactively backfilling changelog entries that were missed before the policy was enacted. --- ext/opentelemetry-ext-pymongo/CHANGELOG.md | 1 - ext/opentelemetry-ext-pymysql/CHANGELOG.md | 4 +--- ext/opentelemetry-ext-requests/CHANGELOG.md | 10 ++++++-- ext/opentelemetry-ext-zipkin/CHANGELOG.md | 3 +++ opentelemetry-api/CHANGELOG.md | 17 +++++++++++++- .../CHANGELOG.md | 11 ++++++++- opentelemetry-sdk/CHANGELOG.md | 23 +++++++++++++++++-- 7 files changed, 59 insertions(+), 10 deletions(-) diff --git a/ext/opentelemetry-ext-pymongo/CHANGELOG.md b/ext/opentelemetry-ext-pymongo/CHANGELOG.md index c8661e548d..dd3cef2e3a 100644 --- a/ext/opentelemetry-ext-pymongo/CHANGELOG.md +++ b/ext/opentelemetry-ext-pymongo/CHANGELOG.md @@ -4,7 +4,6 @@ - Implement instrumentor interface ([#612](https://github.com/open-telemetry/opentelemetry-python/pull/612)) - ## 0.4a0 Released 2020-02-21 diff --git a/ext/opentelemetry-ext-pymysql/CHANGELOG.md b/ext/opentelemetry-ext-pymysql/CHANGELOG.md index 940018bc41..33144da913 100644 --- a/ext/opentelemetry-ext-pymysql/CHANGELOG.md +++ b/ext/opentelemetry-ext-pymysql/CHANGELOG.md @@ -2,6 +2,4 @@ ## Unreleased -- Implement instrument_connection and uninstrument_connection ([#624](https://github.com/open-telemetry/opentelemetry-python/pull/624)) -- Implement PyMySQL integration ([#504](https://github.com/open-telemetry/opentelemetry-python/pull/504)) -- Implement instrumentor interface ([#611](https://github.com/open-telemetry/opentelemetry-python/pull/611)) +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-requests/CHANGELOG.md b/ext/opentelemetry-ext-requests/CHANGELOG.md index 34ca8ff226..97167f86aa 100644 --- a/ext/opentelemetry-ext-requests/CHANGELOG.md +++ b/ext/opentelemetry-ext-requests/CHANGELOG.md @@ -2,8 +2,14 @@ ## Unreleased -- Rename package to opentelemetry-ext-requests ([#619](https://github.com/open-telemetry/opentelemetry-python/pull/619)) -- Implement instrumentor interface ([#597](https://github.com/open-telemetry/opentelemetry-python/pull/597)) +- Rename package to opentelemetry-ext-requests + ([#619](https://github.com/open-telemetry/opentelemetry-python/pull/619)) +- Implement instrumentor interface, enabling auto-instrumentation + ([#597](https://github.com/open-telemetry/opentelemetry-python/pull/597)) +- Adding disable_session for more granular instrumentation control + ([#573](https://github.com/open-telemetry/opentelemetry-python/pull/573)) +- Add a callback for custom attributes + ([#656](https://github.com/open-telemetry/opentelemetry-python/pull/656)) ## 0.3a0 diff --git a/ext/opentelemetry-ext-zipkin/CHANGELOG.md b/ext/opentelemetry-ext-zipkin/CHANGELOG.md index f32ad5bd4c..28dee2d90b 100644 --- a/ext/opentelemetry-ext-zipkin/CHANGELOG.md +++ b/ext/opentelemetry-ext-zipkin/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- bugfix: 'debug' field is now correct + ([#549](https://github.com/open-telemetry/opentelemetry-python/pull/549)) + ## 0.4a0 Released 2020-02-21 diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index b6b28c119d..45d6dc0ea0 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,21 @@ ## Unreleased +- Add reset for the global configuration object, for testing purposes + ([#636](https://github.com/open-telemetry/opentelemetry-python/pull/636)) +- tracer.get_tracer now optionally accepts a TracerProvider + ([#602](https://github.com/open-telemetry/opentelemetry-python/pull/602)) +- Configuration object can now be used by any component of opentelemetry, + including 3rd party instrumentations + ([#563](https://github.com/open-telemetry/opentelemetry-python/pull/563)) +- bugfix: configuration object now matches fields in a case-sensitive manner + ([#583](https://github.com/open-telemetry/opentelemetry-python/pull/583)) +- bugfix: configuration object now accepts all valid python variable names + ([#583](https://github.com/open-telemetry/opentelemetry-python/pull/583)) +- bugfix: configuration undefined attributes now return None instead of raising + an AttributeError. + ([#583](https://github.com/open-telemetry/opentelemetry-python/pull/583)) + ## 0.6b0 Released 2020-03-30 @@ -36,7 +51,7 @@ Released 2020-03-16 - Renaming TracerSource to TraceProvider ([#441](https://github.com/open-telemetry/opentelemetry-python/pull/441)) - Adding attach/detach methods as per spec - ([#429](https://github.com/open-telemetry/opentelemetry-python/pull/450) + ([#429](https://github.com/open-telemetry/opentelemetry-python/pull/450) ## 0.4a0 diff --git a/opentelemetry-auto-instrumentation/CHANGELOG.md b/opentelemetry-auto-instrumentation/CHANGELOG.md index fba2c751d6..ede42d2e20 100644 --- a/opentelemetry-auto-instrumentation/CHANGELOG.md +++ b/opentelemetry-auto-instrumentation/CHANGELOG.md @@ -1,7 +1,16 @@ # Changelog +## Unreleased + +- Add support for programmatic instrumentation + ([#579](https://github.com/open-telemetry/opentelemetry-python/pull/569)) +- bugfix: enable auto-instrumentation command to work for custom entry points + (e.g. flask_run) + ([#567](https://github.com/open-telemetry/opentelemetry-python/pull/567)) + + ## 0.6b0 Released 2020-03-30 -- Initial release. \ No newline at end of file +- Initial release. diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 23663a9960..d833c41e31 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,25 @@ ## Unreleased +- Exporter API: span parents are now always spancontext + ([#548](https://github.com/open-telemetry/opentelemetry-python/pull/548)) +- tracer.get_tracer now optionally accepts a TracerProvider + ([#602](https://github.com/open-telemetry/opentelemetry-python/pull/602)) +- Console span exporter now prints prettier, more legible messages + ([#505](https://github.com/open-telemetry/opentelemetry-python/pull/505)) +- bugfix: B3 propagation now retrieves parentSpanId correctly + ([#621](https://github.com/open-telemetry/opentelemetry-python/pull/621)) +- bugfix: a DefaultSpan now longer causes an exception when used with tracer + ([#577](https://github.com/open-telemetry/opentelemetry-python/pull/577)) +- move last_updated_timestamp into aggregators instead of bound metric + instrument + ([#522](https://github.com/open-telemetry/opentelemetry-python/pull/522)) +- bugfix: suppressing instrumentation in metrics to eliminate an infinite loop + of telemetry + ([#529](https://github.com/open-telemetry/opentelemetry-python/pull/529)) +- bugfix: freezing span attribute sequences, reducing potential user errors + ([#529](https://github.com/open-telemetry/opentelemetry-python/pull/529)) + ## 0.6b0 Released 2020-03-30 @@ -32,13 +51,13 @@ Released 2020-03-16 - Implement observer instrument ([#425](https://github.com/open-telemetry/opentelemetry-python/pull/425)) -## 0.4a0 +## 0.4a0 Released 2020-02-21 - Added named Tracers ([#301](https://github.com/open-telemetry/opentelemetry-python/pull/301)) -- Set status for ended spans +- Set status for ended spans ([#297](https://github.com/open-telemetry/opentelemetry-python/pull/297) and [#358](https://github.com/open-telemetry/opentelemetry-python/pull/358)) - Use module loggers From 021723a5c1cdde8cd15b542179e1fd83bd32819b Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 11 May 2020 21:46:43 -0700 Subject: [PATCH 0335/1517] pin flake8 at 3.7.9 (#679) --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 24da70defa..b8ae14c89c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,5 @@ pylint==2.4.4 -flake8~=3.7 +flake8==3.7.9 isort~=4.3 black>=19.3b0,==19.* mypy==0.740 From 82359d5bfc6fe34f3012a44b3d8580f083706c9c Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 11 May 2020 22:30:09 -0700 Subject: [PATCH 0336/1517] fix branch name filtering (#677) --- .github/workflows/prepare-release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 7d15b02bed..95ed9f466e 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -1,7 +1,8 @@ name: prepare-release on: push: - branch: [ 'release/*' ] + branches: + - 'release/**' jobs: build: From e84faa4c7569e0fe4744c54adb4d006d091d9a48 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 12 May 2020 15:09:50 -0600 Subject: [PATCH 0337/1517] docs: Add more documentation for the instrumentor base class (#638) Fixes #637 Co-authored-by: Yusuke Tsutsumi Co-authored-by: Chris Kleinknecht --- .../opentelemetry/configuration/__init__.py | 11 ++++- .../auto_instrumentation/instrumentor.py | 40 ++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index ad546b0b86..d0fc11dc59 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -74,9 +74,18 @@ To use the meter provider above, then the ``OPENTELEMETRY_PYTHON_METER_PROVIDER`` should be set to -"default_meter_provider" (this is not actually necessary since the +``"default_meter_provider"`` (this is not actually necessary since the OpenTelemetry API provided providers are the default ones used if no configuration is found in the environment variables). + +This object can be used by any OpenTelemetry component, native or external. +For that reason, the ``Configuration`` object is designed to be immutable. +If a component would change the value of one of the ``Configuration`` object +attributes then another component that relied on that value may break, leading +to bugs that are very hard to debug. To avoid this situation, the preferred +approach for components that need a different value than the one provided by +the ``Configuration`` object is to implement a mechanism that allows the user +to override this value instead of changing it. """ from os import environ diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py index f5d7cf7ddc..dd58775bc7 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py @@ -24,7 +24,17 @@ class BaseInstrumentor(ABC): - """An ABC for instrumentors""" + """An ABC for instrumentors + + Child classes of this ABC should instrument specific third + party libraries or frameworks either by using the + ``opentelemetry-auto-instrumentation`` command or by calling their methods + directly. + + Since every third party library or framework is different and has different + instrumentation needs, more methods can be added to the child classes as + needed to provide practical instrumentation to the end user. + """ _instance = None _is_instrumented = False @@ -38,14 +48,30 @@ def __new__(cls): @abstractmethod def _instrument(self, **kwargs): - """Instrument""" + """Instrument the library""" @abstractmethod def _uninstrument(self, **kwargs): - """Uninstrument""" + """Uninstrument the library""" def instrument(self, **kwargs): - """Instrument""" + """Instrument the library + + This method will be called without any optional arguments by the + ``opentelemetry-auto-instrumentation`` command. The configuration of + the instrumentation when done in this way should be done by previously + setting the configuration (using environment variables or any other + mechanism) that will be used later by the code in the ``instrument`` + implementation via the global ``Configuration`` object. + + The ``instrument`` methods ``kwargs`` should default to values from the + ``Configuration`` object. + + This means that calling this method directly without passing any + optional values should do the very same thing that the + ``opentelemetry-auto-instrumentation`` command does. This approach is + followed because the ``Configuration`` object is immutable. + """ if not self._is_instrumented: result = self._instrument(**kwargs) @@ -57,7 +83,11 @@ def instrument(self, **kwargs): return None def uninstrument(self, **kwargs): - """Uninstrument""" + """Uninstrument the library + + See ``BaseInstrumentor.instrument`` for more information regarding the + usage of ``kwargs``. + """ if self._is_instrumented: result = self._uninstrument(**kwargs) From 8d09319c43a24b05d14128361de2c9afe8c856b6 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 12 May 2020 16:37:01 -0700 Subject: [PATCH 0338/1517] fix: Set correct development status (#687) --- ext/opentelemetry-ext-django/setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-django/setup.cfg b/ext/opentelemetry-ext-django/setup.cfg index c308a6f352..de279e182b 100644 --- a/ext/opentelemetry-ext-django/setup.cfg +++ b/ext/opentelemetry-ext-django/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/ope platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Beta + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python From f1a7feb60b43128b6da651b34b9500a2f55f91bc Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Wed, 13 May 2020 17:51:28 -0400 Subject: [PATCH 0339/1517] Add exporter to Datadog (#572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an exporter to Datadog. This implementation makes use of ddtrace to handle the creation of Datadog traces and writing them to the Datadog agent. Co-Authored-By: Mauricio Vásquez --- docs-requirements.txt | 1 + docs/examples/datadog_exporter/README.rst | 81 ++++ docs/examples/datadog_exporter/client.py | 52 +++ .../datadog_exporter/datadog_exporter.py | 37 ++ docs/examples/datadog_exporter/server.py | 49 +++ docs/ext/datadog/datadog.rst | 7 + ext/opentelemetry-ext-datadog/CHANGELOG.md | 7 + ext/opentelemetry-ext-datadog/README.rst | 29 ++ ext/opentelemetry-ext-datadog/setup.cfg | 47 ++ ext/opentelemetry-ext-datadog/setup.py | 27 ++ .../src/opentelemetry/ext/datadog/__init__.py | 51 +++ .../src/opentelemetry/ext/datadog/exporter.py | 204 +++++++++ .../ext/datadog/spanprocessor.py | 222 ++++++++++ .../src/opentelemetry/ext/datadog/version.py | 15 + .../tests/__init__.py | 0 .../tests/test_datadog_exporter.py | 405 ++++++++++++++++++ scripts/coverage.sh | 1 + tox.ini | 7 + 18 files changed, 1242 insertions(+) create mode 100644 docs/examples/datadog_exporter/README.rst create mode 100644 docs/examples/datadog_exporter/client.py create mode 100644 docs/examples/datadog_exporter/datadog_exporter.py create mode 100644 docs/examples/datadog_exporter/server.py create mode 100644 docs/ext/datadog/datadog.rst create mode 100644 ext/opentelemetry-ext-datadog/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-datadog/README.rst create mode 100644 ext/opentelemetry-ext-datadog/setup.cfg create mode 100644 ext/opentelemetry-ext-datadog/setup.py create mode 100644 ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/__init__.py create mode 100644 ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py create mode 100644 ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/spanprocessor.py create mode 100644 ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py create mode 100644 ext/opentelemetry-ext-datadog/tests/__init__.py create mode 100644 ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py diff --git a/docs-requirements.txt b/docs-requirements.txt index 26b74cccc2..55b647402f 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -3,6 +3,7 @@ sphinx-rtd-theme~=0.4 sphinx-autodoc-typehints~=1.10.2 # Required by ext packages +ddtrace>=0.34.0 aiohttp ~= 3.0 Deprecated>=1.2.6 django>=2.2 diff --git a/docs/examples/datadog_exporter/README.rst b/docs/examples/datadog_exporter/README.rst new file mode 100644 index 0000000000..961ac9ca05 --- /dev/null +++ b/docs/examples/datadog_exporter/README.rst @@ -0,0 +1,81 @@ +Datadog Exporter Example +======================== + +These examples show how to use OpenTelemetry to send tracing data to Datadog. + + +Basic Example +------------- + +* Installation + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + pip install opentelemetry-ext-datadog + +* Start Datadog Agent + +.. code-block:: sh + + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock:ro \ + -v /proc/:/host/proc/:ro \ + -v /sys/fs/cgroup/:/host/sys/fs/cgroup:ro \ + -p 127.0.0.1:8126:8126/tcp \ + -e DD_API_KEY="" \ + -e DD_APM_ENABLED=true \ + datadog/agent:latest + +* Run example + +.. code-block:: sh + + python datadog_exporter.py + +Auto-Instrumention Example +-------------------------- + +* Installation + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + pip install opentelemetry-ext-datadog + pip install opentelemetry-auto-instrumentation + pip install opentelemetry-ext-flask + pip install flask + pip install requests + +* Start Datadog Agent + +.. code-block:: sh + + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock:ro \ + -v /proc/:/host/proc/:ro \ + -v /sys/fs/cgroup/:/host/sys/fs/cgroup:ro \ + -p 127.0.0.1:8126:8126/tcp \ + -e DD_API_KEY="" \ + -e DD_APM_ENABLED=true \ + datadog/agent:latest + +* Start server + +.. code-block:: sh + + opentelemetry-auto-instrumentation python server.py + +* Run client + +.. code-block:: sh + + opentelemetry-auto-instrumentation python client.py testing + +* Run client with parameter to raise error + +.. code-block:: sh + + opentelemetry-auto-instrumentation python client.py error diff --git a/docs/examples/datadog_exporter/client.py b/docs/examples/datadog_exporter/client.py new file mode 100644 index 0000000000..3969ef04d9 --- /dev/null +++ b/docs/examples/datadog_exporter/client.py @@ -0,0 +1,52 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sys import argv + +from requests import get + +from opentelemetry import propagators, trace +from opentelemetry.ext.datadog import ( + DatadogExportSpanProcessor, + DatadogSpanExporter, +) +from opentelemetry.sdk.trace import TracerProvider + +trace.set_tracer_provider(TracerProvider()) + +trace.get_tracer_provider().add_span_processor( + DatadogExportSpanProcessor( + DatadogSpanExporter( + agent_url="http://localhost:8126", service="example-client" + ) + ) +) + +tracer = trace.get_tracer(__name__) + +assert len(argv) == 2 + +with tracer.start_as_current_span("client"): + + with tracer.start_as_current_span("client-server"): + headers = {} + propagators.inject(dict.__setitem__, headers) + requested = get( + "http://localhost:8082/server_request", + params={"param": argv[1]}, + headers=headers, + ) + + assert requested.status_code == 200 + print(requested.text) diff --git a/docs/examples/datadog_exporter/datadog_exporter.py b/docs/examples/datadog_exporter/datadog_exporter.py new file mode 100644 index 0000000000..0b3af99223 --- /dev/null +++ b/docs/examples/datadog_exporter/datadog_exporter.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry import trace +from opentelemetry.ext.datadog import ( + DatadogExportSpanProcessor, + DatadogSpanExporter, +) +from opentelemetry.sdk.trace import TracerProvider + +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer(__name__) + +exporter = DatadogSpanExporter( + agent_url="http://localhost:8126", service="example" +) + +span_processor = DatadogExportSpanProcessor(exporter) +trace.get_tracer_provider().add_span_processor(span_processor) + +with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): + print("Hello world from OpenTelemetry Python!") diff --git a/docs/examples/datadog_exporter/server.py b/docs/examples/datadog_exporter/server.py new file mode 100644 index 0000000000..0d545e2b7b --- /dev/null +++ b/docs/examples/datadog_exporter/server.py @@ -0,0 +1,49 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from flask import Flask, request + +from opentelemetry import trace +from opentelemetry.ext.datadog import ( + DatadogExportSpanProcessor, + DatadogSpanExporter, +) +from opentelemetry.sdk.trace import TracerProvider + +app = Flask(__name__) + +trace.set_tracer_provider(TracerProvider()) + +trace.get_tracer_provider().add_span_processor( + DatadogExportSpanProcessor( + DatadogSpanExporter( + agent_url="http://localhost:8126", service="example-server" + ) + ) +) + +tracer = trace.get_tracer(__name__) + + +@app.route("/server_request") +def server_request(): + param = request.args.get("param") + with tracer.start_as_current_span("server-inner"): + if param == "error": + raise ValueError("forced server error") + return "served: {}".format(param) + + +if __name__ == "__main__": + app.run(port=8082) diff --git a/docs/ext/datadog/datadog.rst b/docs/ext/datadog/datadog.rst new file mode 100644 index 0000000000..5ae7e04289 --- /dev/null +++ b/docs/ext/datadog/datadog.rst @@ -0,0 +1,7 @@ +OpenTelemetry Datadog Exporter +============================== + +.. automodule:: opentelemetry.ext.datadog + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-datadog/CHANGELOG.md b/ext/opentelemetry-ext-datadog/CHANGELOG.md new file mode 100644 index 0000000000..333b2b3f8b --- /dev/null +++ b/ext/opentelemetry-ext-datadog/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## Unreleased + +- Add exporter to Datadog + ([#572](https://github.com/open-telemetry/opentelemetry-python/pull/572)) + diff --git a/ext/opentelemetry-ext-datadog/README.rst b/ext/opentelemetry-ext-datadog/README.rst new file mode 100644 index 0000000000..9f9a2aeb88 --- /dev/null +++ b/ext/opentelemetry-ext-datadog/README.rst @@ -0,0 +1,29 @@ +OpenTelemetry Datadog Exporter +============================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-datadog.svg + :target: https://pypi.org/project/opentelemetry-ext-datadog/ + +This library allows to export tracing data to `Datadog +`_. OpenTelemetry span event and links are not +supported. + +Installation +------------ + +:: + + pip install opentelemetry-ext-datadog + + +.. _Datadog: https://www.datadoghq.com/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + + +References +---------- + +* `Datadog `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-datadog/setup.cfg b/ext/opentelemetry-ext-datadog/setup.cfg new file mode 100644 index 0000000000..f17192709b --- /dev/null +++ b/ext/opentelemetry-ext-datadog/setup.cfg @@ -0,0 +1,47 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-datadog +description = Datadog Span Exporter for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-datadog +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + ddtrace>=0.34.0 + opentelemetry-api==0.7.dev0 + opentelemetry-sdk==0.7.dev0 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-datadog/setup.py b/ext/opentelemetry-ext-datadog/setup.py new file mode 100644 index 0000000000..f657391104 --- /dev/null +++ b/ext/opentelemetry-ext-datadog/setup.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "datadog", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/__init__.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/__init__.py new file mode 100644 index 0000000000..0c01cf7fba --- /dev/null +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/__init__.py @@ -0,0 +1,51 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The **OpenTelemetry Datadog Exporter** provides a span exporter from +`OpenTelemetry`_ traces to `Datadog`_ by using the Datadog Agent. + +Usage +----- + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext.datadog import DatadogExportSpanProcessor, DatadogSpanExporter + from opentelemetry.sdk.trace import TracerProvider + + trace.set_tracer_provider(TracerProvider()) + tracer = trace.get_tracer(__name__) + + exporter = DatadogSpanExporter( + agent_url="http://agent:8126", service="my-helloworld-service" + ) + + span_processor = DatadogExportSpanProcessor(exporter) + trace.get_tracer_provider().add_span_processor(span_processor) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + +API +--- +.. _Datadog: https://www.datadoghq.com/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +""" +# pylint: disable=import-error + +from .exporter import DatadogSpanExporter +from .spanprocessor import DatadogExportSpanProcessor + +__all__ = ["DatadogExportSpanProcessor", "DatadogSpanExporter"] diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py new file mode 100644 index 0000000000..4843200a2c --- /dev/null +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py @@ -0,0 +1,204 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os +from urllib.parse import urlparse + +from ddtrace.ext import SpanTypes as DatadogSpanTypes +from ddtrace.internal.writer import AgentWriter +from ddtrace.span import Span as DatadogSpan + +import opentelemetry.trace as trace_api +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.trace.status import StatusCanonicalCode + +logger = logging.getLogger(__name__) + + +DEFAULT_AGENT_URL = "http://localhost:8126" +_INSTRUMENTATION_SPAN_TYPES = { + "opentelemetry.ext.aiohttp-client": DatadogSpanTypes.HTTP, + "opentelemetry.ext.dbapi": DatadogSpanTypes.SQL, + "opentelemetry.ext.django": DatadogSpanTypes.WEB, + "opentelemetry.ext.flask": DatadogSpanTypes.WEB, + "opentelemetry.ext.grpc": DatadogSpanTypes.GRPC, + "opentelemetry.ext.jinja2": DatadogSpanTypes.TEMPLATE, + "opentelemetry.ext.mysql": DatadogSpanTypes.SQL, + "opentelemetry.ext.psycopg2": DatadogSpanTypes.SQL, + "opentelemetry.ext.pymongo": DatadogSpanTypes.MONGODB, + "opentelemetry.ext.pymysql": DatadogSpanTypes.SQL, + "opentelemetry.ext.redis": DatadogSpanTypes.REDIS, + "opentelemetry.ext.requests": DatadogSpanTypes.HTTP, + "opentelemetry.ext.sqlalchemy": DatadogSpanTypes.SQL, + "opentelemetry.ext.wsgi": DatadogSpanTypes.WEB, +} + + +class DatadogSpanExporter(SpanExporter): + """Datadog span exporter for OpenTelemetry. + + Args: + agent_url: The url of the Datadog Agent or use `DD_TRACE_AGENT_URL` environment variable + service: The service to be used for the application or use `DD_SERVICE` environment variable + """ + + def __init__(self, agent_url=None, service=None): + self.agent_url = ( + agent_url + if agent_url + else os.environ.get("DD_TRACE_AGENT_URL", DEFAULT_AGENT_URL) + ) + self.service = service if service else os.environ.get("DD_SERVICE") + self._agent_writer = None + + @property + def agent_writer(self): + if self._agent_writer is None: + url_parsed = urlparse(self.agent_url) + if url_parsed.scheme in ("http", "https"): + self._agent_writer = AgentWriter( + hostname=url_parsed.hostname, + port=url_parsed.port, + https=url_parsed.scheme == "https", + ) + elif url_parsed.scheme == "unix": + self._agent_writer = AgentWriter(uds_path=url_parsed.path) + else: + raise ValueError( + "Unknown scheme `%s` for agent URL" % url_parsed.scheme + ) + return self._agent_writer + + def export(self, spans): + datadog_spans = self._translate_to_datadog(spans) + + self.agent_writer.write(spans=datadog_spans) + + return SpanExportResult.SUCCESS + + def shutdown(self): + if self.agent_writer.started: + self.agent_writer.stop() + self.agent_writer.join(self.agent_writer.exit_timeout) + + def _translate_to_datadog(self, spans): + datadog_spans = [] + + for span in spans: + trace_id, parent_id, span_id = _get_trace_ids(span) + + # datadog Span is initialized with a reference to the tracer which is + # used to record the span when it is finished. We can skip ignore this + # because we are not calling the finish method and explictly set the + # duration. + tracer = None + + datadog_span = DatadogSpan( + tracer, + _get_span_name(span), + service=self.service, + resource=_get_resource(span), + span_type=_get_span_type(span), + trace_id=trace_id, + span_id=span_id, + parent_id=parent_id, + ) + datadog_span.start_ns = span.start_time + datadog_span.duration_ns = span.end_time - span.start_time + + if span.status.canonical_code is not StatusCanonicalCode.OK: + datadog_span.error = 1 + if span.status.description: + exc_type, exc_val = _get_exc_info(span) + # no mapping for error.stack since traceback not recorded + datadog_span.set_tag("error.msg", exc_val) + datadog_span.set_tag("error.type", exc_type) + + datadog_span.set_tags(span.attributes) + + # span events and span links are not supported + + datadog_spans.append(datadog_span) + + return datadog_spans + + +def _get_trace_ids(span): + """Extract tracer ids from span""" + ctx = span.get_context() + trace_id = ctx.trace_id + span_id = ctx.span_id + + if isinstance(span.parent, trace_api.Span): + parent_id = span.parent.get_context().span_id + elif isinstance(span.parent, trace_api.SpanContext): + parent_id = span.parent.span_id + else: + parent_id = 0 + + trace_id = _convert_trace_id_uint64(trace_id) + + return trace_id, parent_id, span_id + + +def _convert_trace_id_uint64(otel_id): + """Convert 128-bit int used for trace_id to 64-bit unsigned int""" + return otel_id & 0xFFFFFFFFFFFFFFFF + + +def _get_span_name(span): + """Get span name by using instrumentation and kind while backing off to + span.name + """ + instrumentation_name = ( + span.instrumentation_info.name if span.instrumentation_info else None + ) + span_kind_name = span.kind.name if span.kind else None + name = ( + "{}.{}".format(instrumentation_name, span_kind_name) + if instrumentation_name and span_kind_name + else span.name + ) + return name + + +def _get_resource(span): + """Get resource name for span""" + if "http.method" in span.attributes: + route = span.attributes.get( + "http.route", span.attributes.get("http.path") + ) + return ( + span.attributes["http.method"] + " " + route + if route + else span.attributes["http.method"] + ) + + return span.name + + +def _get_span_type(span): + """Get Datadog span type""" + instrumentation_name = ( + span.instrumentation_info.name if span.instrumentation_info else None + ) + span_type = _INSTRUMENTATION_SPAN_TYPES.get(instrumentation_name) + return span_type + + +def _get_exc_info(span): + """Parse span status description for exception type and value""" + exc_type, exc_val = span.status.description.split(":", 1) + return exc_type, exc_val.strip() diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/spanprocessor.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/spanprocessor.py new file mode 100644 index 0000000000..600778c88c --- /dev/null +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/spanprocessor.py @@ -0,0 +1,222 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import logging +import threading +import typing + +from opentelemetry.context import attach, detach, set_value +from opentelemetry.sdk.trace import Span, SpanProcessor +from opentelemetry.sdk.trace.export import SpanExporter +from opentelemetry.trace import INVALID_TRACE_ID +from opentelemetry.util import time_ns + +logger = logging.getLogger(__name__) + + +class DatadogExportSpanProcessor(SpanProcessor): + """Datadog exporter span processor + + DatadogExportSpanProcessor is an implementation of `SpanProcessor` that + batches all opened spans into a list per trace. When all spans for a trace + are ended, the trace is queues up for export. This is required for exporting + to the Datadog Agent which expects to received list of spans for each trace. + """ + + _FLUSH_TOKEN = INVALID_TRACE_ID + + def __init__( + self, + span_exporter: SpanExporter, + schedule_delay_millis: float = 5000, + max_trace_size: int = 4096, + ): + if max_trace_size <= 0: + raise ValueError("max_queue_size must be a positive integer.") + + if schedule_delay_millis <= 0: + raise ValueError("schedule_delay_millis must be positive.") + + self.span_exporter = span_exporter + + # queue trace_ids for traces with recently ended spans for worker thread to check + # for exporting + self.check_traces_queue = ( + collections.deque() + ) # type: typing.Deque[int] + + self.traces_lock = threading.Lock() + # dictionary of trace_ids to a list of spans where the first span is the + # first opened span for the trace + self.traces = collections.defaultdict(list) + # counter to keep track of the number of spans and ended spans for a + # trace_id + self.traces_spans_count = collections.Counter() + self.traces_spans_ended_count = collections.Counter() + + self.worker_thread = threading.Thread(target=self.worker, daemon=True) + + # threading conditions used for flushing and shutdown + self.condition = threading.Condition(threading.Lock()) + self.flush_condition = threading.Condition(threading.Lock()) + + # flag to indicate that there is a flush operation on progress + self._flushing = False + + self.max_trace_size = max_trace_size + self._spans_dropped = False + self.schedule_delay_millis = schedule_delay_millis + self.done = False + self.worker_thread.start() + + def on_start(self, span: Span) -> None: + ctx = span.get_context() + trace_id = ctx.trace_id + + with self.traces_lock: + # check upper bound on number of spans for trace before adding new + # span + if self.traces_spans_count[trace_id] == self.max_trace_size: + logger.warning("Max spans for trace, spans will be dropped.") + self._spans_dropped = True + return + + # add span to end of list for a trace and update the counter + self.traces[trace_id].append(span) + self.traces_spans_count[trace_id] += 1 + + def on_end(self, span: Span) -> None: + if self.done: + logger.warning("Already shutdown, dropping span.") + return + + ctx = span.get_context() + trace_id = ctx.trace_id + + with self.traces_lock: + self.traces_spans_ended_count[trace_id] += 1 + if self.is_trace_exportable(trace_id): + self.check_traces_queue.appendleft(trace_id) + + def worker(self): + timeout = self.schedule_delay_millis / 1e3 + while not self.done: + if not self._flushing: + with self.condition: + self.condition.wait(timeout) + if not self.check_traces_queue: + # spurious notification, let's wait again + continue + if self.done: + # missing spans will be sent when calling flush + break + + # substract the duration of this export call to the next timeout + start = time_ns() + self.export() + end = time_ns() + duration = (end - start) / 1e9 + timeout = self.schedule_delay_millis / 1e3 - duration + + # be sure that all spans are sent + self._drain_queue() + + def is_trace_exportable(self, trace_id): + return ( + self.traces_spans_count[trace_id] + - self.traces_spans_ended_count[trace_id] + <= 0 + ) + + def export(self) -> None: + """Exports traces with finished spans.""" + notify_flush = False + export_trace_ids = [] + + while self.check_traces_queue: + trace_id = self.check_traces_queue.pop() + if trace_id is self._FLUSH_TOKEN: + notify_flush = True + else: + with self.traces_lock: + # check whether trace is exportable again in case that new + # spans were started since we last concluded trace was + # exportable + if self.is_trace_exportable(trace_id): + export_trace_ids.append(trace_id) + del self.traces_spans_count[trace_id] + del self.traces_spans_ended_count[trace_id] + + if len(export_trace_ids) > 0: + token = attach(set_value("suppress_instrumentation", True)) + + for trace_id in export_trace_ids: + with self.traces_lock: + try: + # Ignore type b/c the Optional[None]+slicing is too "clever" + # for mypy + self.span_exporter.export(self.traces[trace_id]) # type: ignore + # pylint: disable=broad-except + except Exception: + logger.exception( + "Exception while exporting Span batch." + ) + finally: + del self.traces[trace_id] + + detach(token) + + if notify_flush: + with self.flush_condition: + self.flush_condition.notify() + + def _drain_queue(self): + """"Export all elements until queue is empty. + + Can only be called from the worker thread context because it invokes + `export` that is not thread safe. + """ + while self.check_traces_queue: + self.export() + + def force_flush(self, timeout_millis: int = 30000) -> bool: + if self.done: + logger.warning("Already shutdown, ignoring call to force_flush().") + return True + + self._flushing = True + self.check_traces_queue.appendleft(self._FLUSH_TOKEN) + + # wake up worker thread + with self.condition: + self.condition.notify_all() + + # wait for token to be processed + with self.flush_condition: + ret = self.flush_condition.wait(timeout_millis / 1e3) + + self._flushing = False + + if not ret: + logger.warning("Timeout was exceeded in force_flush().") + return ret + + def shutdown(self) -> None: + # signal the worker thread to finish and then wait for it + self.done = True + with self.condition: + self.condition.notify_all() + self.worker_thread.join() + self.span_exporter.shutdown() diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py new file mode 100644 index 0000000000..86c61362ab --- /dev/null +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-datadog/tests/__init__.py b/ext/opentelemetry-ext-datadog/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py b/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py new file mode 100644 index 0000000000..97ca3fa9b9 --- /dev/null +++ b/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py @@ -0,0 +1,405 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools +import logging +import time +import unittest +from unittest import mock + +from ddtrace.internal.writer import AgentWriter + +from opentelemetry import trace as trace_api +from opentelemetry.ext import datadog +from opentelemetry.sdk import trace +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo + + +class MockDatadogSpanExporter(datadog.DatadogSpanExporter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + agent_writer_mock = mock.Mock(spec=AgentWriter) + agent_writer_mock.started = True + agent_writer_mock.exit_timeout = 1 + self._agent_writer = agent_writer_mock + + +def get_spans(tracer, exporter, shutdown=True): + if shutdown: + tracer.source.shutdown() + + spans = [ + call_args[-1]["spans"] + for call_args in exporter.agent_writer.write.call_args_list + ] + + return [span.to_dict() for span in itertools.chain.from_iterable(spans)] + + +class TestDatadogSpanExporter(unittest.TestCase): + def setUp(self): + self.exporter = MockDatadogSpanExporter() + self.span_processor = datadog.DatadogExportSpanProcessor(self.exporter) + tracer_provider = trace.TracerProvider() + tracer_provider.add_span_processor(self.span_processor) + self.tracer_provider = tracer_provider + self.tracer = tracer_provider.get_tracer(__name__) + + def tearDown(self): + self.tracer_provider.shutdown() + + def test_constructor_default(self): + """Test the default values assigned by constructor.""" + exporter = datadog.DatadogSpanExporter() + + self.assertEqual(exporter.agent_url, "http://localhost:8126") + self.assertIsNone(exporter.service) + self.assertIsNotNone(exporter.agent_writer) + + def test_constructor_explicit(self): + """Test the constructor passing all the options.""" + agent_url = "http://localhost:8126" + exporter = datadog.DatadogSpanExporter( + agent_url=agent_url, service="explicit" + ) + + self.assertEqual(exporter.agent_url, agent_url) + self.assertEqual(exporter.service, "explicit") + self.assertIsNotNone(exporter.agent_writer) + + @mock.patch.dict( + "os.environ", + {"DD_TRACE_AGENT_URL": "http://agent:8126", "DD_SERVICE": "environ"}, + ) + def test_constructor_environ(self): + exporter = datadog.DatadogSpanExporter() + + self.assertEqual(exporter.agent_url, "http://agent:8126") + self.assertEqual(exporter.service, "environ") + self.assertIsNotNone(exporter.agent_writer) + + # pylint: disable=too-many-locals + @mock.patch.dict("os.environ", {"DD_SERVICE": "test-service"}) + def test_translate_to_datadog(self): + # pylint: disable=invalid-name + self.maxDiff = None + + span_names = ("test1", "test2", "test3") + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + trace_id_low = 0x6F9EFCD03927272E + span_id = 0x34BF92DEEFC58C92 + parent_id = 0x1111111111111111 + other_id = 0x2222222222222222 + + base_time = 683647322 * 10 ** 9 # in ns + start_times = ( + base_time, + base_time + 150 * 10 ** 6, + base_time + 300 * 10 ** 6, + ) + durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6) + end_times = ( + start_times[0] + durations[0], + start_times[1] + durations[1], + start_times[2] + durations[2], + ) + + span_context = trace_api.SpanContext( + trace_id, span_id, is_remote=False + ) + parent_context = trace_api.SpanContext( + trace_id, parent_id, is_remote=False + ) + other_context = trace_api.SpanContext( + trace_id, other_id, is_remote=False + ) + + instrumentation_info = InstrumentationInfo(__name__, "0") + + otel_spans = [ + trace.Span( + name=span_names[0], + context=span_context, + parent=parent_context, + kind=trace_api.SpanKind.CLIENT, + instrumentation_info=instrumentation_info, + ), + trace.Span( + name=span_names[1], + context=parent_context, + parent=None, + instrumentation_info=instrumentation_info, + ), + trace.Span( + name=span_names[2], context=other_context, parent=None, + ), + ] + + otel_spans[0].start(start_time=start_times[0]) + otel_spans[0].end(end_time=end_times[0]) + + otel_spans[1].start(start_time=start_times[1]) + otel_spans[1].end(end_time=end_times[1]) + + otel_spans[2].start(start_time=start_times[2]) + otel_spans[2].end(end_time=end_times[2]) + + # pylint: disable=protected-access + exporter = datadog.DatadogSpanExporter() + datadog_spans = [ + span.to_dict() + for span in exporter._translate_to_datadog(otel_spans) + ] + + expected_spans = [ + dict( + trace_id=trace_id_low, + parent_id=parent_id, + span_id=span_id, + name="tests.test_datadog_exporter.CLIENT", + resource=span_names[0], + start=start_times[0], + duration=durations[0], + error=0, + service="test-service", + ), + dict( + trace_id=trace_id_low, + parent_id=0, + span_id=parent_id, + name="tests.test_datadog_exporter.INTERNAL", + resource=span_names[1], + start=start_times[1], + duration=durations[1], + error=0, + service="test-service", + ), + dict( + trace_id=trace_id_low, + parent_id=0, + span_id=other_id, + name=span_names[2], + resource=span_names[2], + start=start_times[2], + duration=durations[2], + error=0, + service="test-service", + ), + ] + + self.assertEqual(datadog_spans, expected_spans) + + @mock.patch.dict("os.environ", {"DD_SERVICE": "test-service"}) + def test_export(self): + """Test that agent and/or collector are invoked""" + # create and save span to be used in tests + context = trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + ) + + test_span = trace.Span("test_span", context=context) + test_span.start() + test_span.end() + + self.exporter.export((test_span,)) + + self.assertEqual(self.exporter.agent_writer.write.call_count, 1) + + def test_resources(self): + test_attributes = [ + {}, + {"http.method": "GET", "http.route": "/foo"}, + {"http.method": "GET", "http.path": "/foo"}, + ] + + for index, test in enumerate(test_attributes): + with self.tracer.start_span(str(index), attributes=test): + pass + + datadog_spans = get_spans(self.tracer, self.exporter) + + self.assertEqual(len(datadog_spans), 3) + + actual = [span["resource"] for span in datadog_spans] + expected = ["0", "GET /foo", "GET /foo"] + + self.assertEqual(actual, expected) + + def test_span_types(self): + test_instrumentations = [ + "opentelemetry.ext.aiohttp-client", + "opentelemetry.ext.dbapi", + "opentelemetry.ext.django", + "opentelemetry.ext.flask", + "opentelemetry.ext.grpc", + "opentelemetry.ext.jinja2", + "opentelemetry.ext.mysql", + "opentelemetry.ext.psycopg2", + "opentelemetry.ext.pymongo", + "opentelemetry.ext.pymysql", + "opentelemetry.ext.redis", + "opentelemetry.ext.requests", + "opentelemetry.ext.sqlalchemy", + "opentelemetry.ext.wsgi", + ] + + for index, instrumentation in enumerate(test_instrumentations): + # change tracer's instrumentation info before starting span + self.tracer.instrumentation_info = InstrumentationInfo( + instrumentation, "0" + ) + with self.tracer.start_span(str(index)): + pass + + datadog_spans = get_spans(self.tracer, self.exporter) + + self.assertEqual(len(datadog_spans), 14) + + actual = [span.get("type") for span in datadog_spans] + expected = [ + "http", + "sql", + "web", + "web", + "grpc", + "template", + "sql", + "sql", + "mongodb", + "sql", + "redis", + "http", + "sql", + "web", + ] + self.assertEqual(actual, expected) + + def test_errors(self): + with self.assertRaises(ValueError): + with self.tracer.start_span("foo"): + raise ValueError("bar") + + datadog_spans = get_spans(self.tracer, self.exporter) + + self.assertEqual(len(datadog_spans), 1) + + span = datadog_spans[0] + self.assertEqual(span["error"], 1) + self.assertEqual(span["meta"]["error.msg"], "bar") + self.assertEqual(span["meta"]["error.type"], "ValueError") + + def test_shutdown(self): + span_names = ["xxx", "bar", "foo"] + + for name in span_names: + with self.tracer.start_span(name): + pass + + self.span_processor.shutdown() + + # check that spans are exported without an explicitly call to + # force_flush() + datadog_spans = get_spans(self.tracer, self.exporter) + actual = [span.get("resource") for span in datadog_spans] + self.assertListEqual(span_names, actual) + + def test_flush(self): + span_names0 = ["xxx", "bar", "foo"] + span_names1 = ["yyy", "baz", "fox"] + + for name in span_names0: + with self.tracer.start_span(name): + pass + + self.assertTrue(self.span_processor.force_flush()) + datadog_spans = get_spans(self.tracer, self.exporter, shutdown=False) + actual0 = [span.get("resource") for span in datadog_spans] + self.assertListEqual(span_names0, actual0) + + # create some more spans to check that span processor still works + for name in span_names1: + with self.tracer.start_span(name): + pass + + self.assertTrue(self.span_processor.force_flush()) + datadog_spans = get_spans(self.tracer, self.exporter) + actual1 = [span.get("resource") for span in datadog_spans] + self.assertListEqual(span_names0 + span_names1, actual1) + + def test_span_processor_lossless(self): + """Test that no spans are lost when sending max_trace_size spans""" + span_processor = datadog.DatadogExportSpanProcessor( + self.exporter, max_trace_size=128 + ) + tracer_provider = trace.TracerProvider() + tracer_provider.add_span_processor(span_processor) + tracer = tracer_provider.get_tracer(__name__) + + with tracer.start_as_current_span("root"): + for _ in range(127): + with tracer.start_span("foo"): + pass + + self.assertTrue(span_processor.force_flush()) + datadog_spans = get_spans(tracer, self.exporter) + self.assertEqual(len(datadog_spans), 128) + tracer_provider.shutdown() + + def test_span_processor_dropped_spans(self): + """Test that spans are lost when exceeding max_trace_size spans""" + span_processor = datadog.DatadogExportSpanProcessor( + self.exporter, max_trace_size=128 + ) + tracer_provider = trace.TracerProvider() + tracer_provider.add_span_processor(span_processor) + tracer = tracer_provider.get_tracer(__name__) + + with tracer.start_as_current_span("root"): + for _ in range(127): + with tracer.start_span("foo"): + pass + with self.assertLogs(level=logging.WARNING): + with tracer.start_span("one-too-many"): + pass + + self.assertTrue(span_processor.force_flush()) + datadog_spans = get_spans(tracer, self.exporter) + self.assertEqual(len(datadog_spans), 128) + tracer_provider.shutdown() + + def test_span_processor_scheduled_delay(self): + """Test that spans are exported each schedule_delay_millis""" + delay = 300 + span_processor = datadog.DatadogExportSpanProcessor( + self.exporter, schedule_delay_millis=delay + ) + tracer_provider = trace.TracerProvider() + tracer_provider.add_span_processor(span_processor) + tracer = tracer_provider.get_tracer(__name__) + + with tracer.start_span("foo"): + pass + + time.sleep(delay / (1e3 * 2)) + datadog_spans = get_spans(tracer, self.exporter, shutdown=False) + self.assertEqual(len(datadog_spans), 0) + + time.sleep(delay / (1e3 * 2) + 0.01) + datadog_spans = get_spans(tracer, self.exporter, shutdown=False) + self.assertEqual(len(datadog_spans), 1) + + tracer_provider.shutdown() diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 1ff42d9e53..248e5faea8 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -19,6 +19,7 @@ coverage erase cov opentelemetry-api cov opentelemetry-sdk +cov ext/opentelemetry-ext-datadog cov ext/opentelemetry-ext-flask cov ext/opentelemetry-ext-requests cov ext/opentelemetry-ext-jaeger diff --git a/tox.ini b/tox.ini index 8d4971d320..40a2c02d70 100644 --- a/tox.ini +++ b/tox.ini @@ -56,6 +56,9 @@ envlist = py3{4,5,6,7,8}-test-ext-jaeger pypy3-test-ext-jaeger + ; opentelemetry-ext-datadog + py3{5,6,7,8}-test-ext-datadog + ; opentelemetry-ext-mysql py3{4,5,6,7,8}-test-ext-mysql pypy3-test-ext-mysql @@ -140,6 +143,7 @@ changedir = test-ext-requests: ext/opentelemetry-ext-requests/tests test-ext-jinja2: ext/opentelemetry-ext-jinja2/tests test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests + test-ext-datadog: ext/opentelemetry-ext-datadog/tests test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests test-ext-django: ext/opentelemetry-ext-django/tests test-ext-mysql: ext/opentelemetry-ext-mysql/tests @@ -224,6 +228,9 @@ commands_pre = jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger + datadog: pip install {toxinidir}/opentelemetry-sdk + datadog: pip install {toxinidir}/ext/opentelemetry-ext-datadog + opentracing-shim: pip install {toxinidir}/ext/opentelemetry-ext-opentracing-shim zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin From a7d7921c3378c71a971adc279b47eb086dc0ca85 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 13 May 2020 16:16:58 -0600 Subject: [PATCH 0340/1517] chore: Add missing files (#674) Fixes #673 --- ext/opentelemetry-ext-django/LICENSE | 201 +++++++++++++++++++++++ ext/opentelemetry-ext-django/MANIFEST.in | 9 + 2 files changed, 210 insertions(+) create mode 100644 ext/opentelemetry-ext-django/LICENSE create mode 100644 ext/opentelemetry-ext-django/MANIFEST.in diff --git a/ext/opentelemetry-ext-django/LICENSE b/ext/opentelemetry-ext-django/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-django/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-django/MANIFEST.in b/ext/opentelemetry-ext-django/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-django/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE From 45a9e53632e29179a2237a32a4c075718f3d16d9 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 13 May 2020 17:04:09 -0700 Subject: [PATCH 0341/1517] release: updating master to 0.8.dev0 (#690) Bumping dev version number --- docs/examples/opentelemetry-example-app/setup.py | 2 +- ext/opentelemetry-ext-aiohttp-client/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-aiohttp-client/setup.cfg | 2 +- .../src/opentelemetry/ext/aiohttp_client/version.py | 2 +- ext/opentelemetry-ext-datadog/setup.cfg | 4 ++-- .../src/opentelemetry/ext/datadog/version.py | 2 +- ext/opentelemetry-ext-dbapi/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-dbapi/setup.cfg | 4 ++-- .../src/opentelemetry/ext/dbapi/version.py | 2 +- ext/opentelemetry-ext-django/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-django/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/django/version.py | 2 +- ext/opentelemetry-ext-flask/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-flask/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/flask/version.py | 2 +- ext/opentelemetry-ext-grpc/setup.cfg | 6 +++--- .../src/opentelemetry/ext/grpc/version.py | 2 +- ext/opentelemetry-ext-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/ext/jaeger/version.py | 2 +- ext/opentelemetry-ext-jinja2/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-jinja2/setup.cfg | 6 +++--- .../src/opentelemetry/ext/jinja2/version.py | 2 +- ext/opentelemetry-ext-mysql/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-mysql/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/mysql/version.py | 2 +- ext/opentelemetry-ext-opentracing-shim/setup.cfg | 4 ++-- .../src/opentelemetry/ext/opentracing_shim/version.py | 2 +- ext/opentelemetry-ext-otcollector/setup.cfg | 4 ++-- .../src/opentelemetry/ext/otcollector/version.py | 2 +- ext/opentelemetry-ext-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/ext/prometheus/version.py | 2 +- ext/opentelemetry-ext-psycopg2/setup.cfg | 2 +- .../src/opentelemetry/ext/psycopg2/version.py | 2 +- ext/opentelemetry-ext-pymongo/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-pymongo/setup.cfg | 6 +++--- .../src/opentelemetry/ext/pymongo/version.py | 2 +- ext/opentelemetry-ext-pymysql/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-pymysql/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/pymysql/version.py | 2 +- ext/opentelemetry-ext-redis/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-redis/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/redis/version.py | 2 +- ext/opentelemetry-ext-requests/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-requests/setup.cfg | 6 +++--- .../src/opentelemetry/ext/requests/version.py | 2 +- ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-sqlalchemy/setup.cfg | 6 +++--- .../src/opentelemetry/ext/sqlalchemy/version.py | 2 +- ext/opentelemetry-ext-wsgi/setup.cfg | 4 ++-- .../src/opentelemetry/ext/wsgi/version.py | 2 +- ext/opentelemetry-ext-zipkin/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/ext/zipkin/version.py | 2 +- opentelemetry-api/CHANGELOG.md | 4 ++++ opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-auto-instrumentation/CHANGELOG.md | 4 ++++ opentelemetry-auto-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/auto_instrumentation/version.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 4 ++++ opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 62 files changed, 140 insertions(+), 80 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py index fb78cd8446..ced6d54e4d 100644 --- a/docs/examples/opentelemetry-example-app/setup.py +++ b/docs/examples/opentelemetry-example-app/setup.py @@ -16,7 +16,7 @@ setuptools.setup( name="opentelemetry-example-app", - version="0.7.dev0", + version="0.8.dev0", author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ diff --git a/ext/opentelemetry-ext-aiohttp-client/CHANGELOG.md b/ext/opentelemetry-ext-aiohttp-client/CHANGELOG.md index 3e04402cea..43990fab16 100644 --- a/ext/opentelemetry-ext-aiohttp-client/CHANGELOG.md +++ b/ext/opentelemetry-ext-aiohttp-client/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Initial release diff --git a/ext/opentelemetry-ext-aiohttp-client/setup.cfg b/ext/opentelemetry-ext-aiohttp-client/setup.cfg index 6c82039ecd..80d53a1f89 100644 --- a/ext/opentelemetry-ext-aiohttp-client/setup.cfg +++ b/ext/opentelemetry-ext-aiohttp-client/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.7.dev0 + opentelemetry-api >= 0.8.dev0 aiohttp ~= 3.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py index fefa5ee917..6a0169eecf 100644 --- a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py +++ b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-datadog/setup.cfg b/ext/opentelemetry-ext-datadog/setup.cfg index f17192709b..667491eb8f 100644 --- a/ext/opentelemetry-ext-datadog/setup.cfg +++ b/ext/opentelemetry-ext-datadog/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = ddtrace>=0.34.0 - opentelemetry-api==0.7.dev0 - opentelemetry-sdk==0.7.dev0 + opentelemetry-api==0.8.dev0 + opentelemetry-sdk==0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-dbapi/CHANGELOG.md b/ext/opentelemetry-ext-dbapi/CHANGELOG.md index c259c66786..288334d6ba 100644 --- a/ext/opentelemetry-ext-dbapi/CHANGELOG.md +++ b/ext/opentelemetry-ext-dbapi/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Implement instrument_connection and uninstrument_connection ([#624](https://github.com/open-telemetry/opentelemetry-python/pull/624)) ## 0.4a0 diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 53fe066dfd..e7ae7aa502 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.7.dev0 + opentelemetry-api == 0.8.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.7.dev0 + opentelemetry-test == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-django/CHANGELOG.md b/ext/opentelemetry-ext-django/CHANGELOG.md index 3e04402cea..43990fab16 100644 --- a/ext/opentelemetry-ext-django/CHANGELOG.md +++ b/ext/opentelemetry-ext-django/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Initial release diff --git a/ext/opentelemetry-ext-django/setup.cfg b/ext/opentelemetry-ext-django/setup.cfg index de279e182b..85d4d88833 100644 --- a/ext/opentelemetry-ext-django/setup.cfg +++ b/ext/opentelemetry-ext-django/setup.cfg @@ -39,13 +39,13 @@ package_dir= packages=find_namespace: install_requires = django >= 2.2 - opentelemetry-ext-wsgi == 0.7.dev0 - opentelemetry-auto-instrumentation == 0.7.dev0 - opentelemetry-api == 0.7.dev0 + opentelemetry-ext-wsgi == 0.8.dev0 + opentelemetry-auto-instrumentation == 0.8.dev0 + opentelemetry-api == 0.8.dev0 [options.extras_require] test = - opentelemetry-test == 0.7.dev0 + opentelemetry-test == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-flask/CHANGELOG.md b/ext/opentelemetry-ext-flask/CHANGELOG.md index 7d4d85b719..d5b1635919 100644 --- a/ext/opentelemetry-ext-flask/CHANGELOG.md +++ b/ext/opentelemetry-ext-flask/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Add exclude list for paths and hosts ([#630](https://github.com/open-telemetry/opentelemetry-python/pull/630)) diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index 12abffc081..4ce79b6696 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-ext-wsgi == 0.7.dev0 - opentelemetry-auto-instrumentation == 0.7.dev0 - opentelemetry-api == 0.7.dev0 + opentelemetry-ext-wsgi == 0.8.dev0 + opentelemetry-auto-instrumentation == 0.8.dev0 + opentelemetry-api == 0.8.dev0 [options.extras_require] test = flask~=1.0 - opentelemetry-test == 0.7.dev0 + opentelemetry-test == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index f774ddc6c5..93fac771bf 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.7.dev0 + opentelemetry-api == 0.8.dev0 grpcio ~= 1.27 [options.extras_require] test = - opentelemetry-test == 0.7.dev0 - opentelemetry-sdk == 0.7.dev0 + opentelemetry-test == 0.8.dev0 + opentelemetry-sdk == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/ext/opentelemetry-ext-jaeger/setup.cfg index ead723c82b..9d61b6185f 100644 --- a/ext/opentelemetry-ext-jaeger/setup.cfg +++ b/ext/opentelemetry-ext-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.7.dev0 - opentelemetry-sdk == 0.7.dev0 + opentelemetry-api == 0.8.dev0 + opentelemetry-sdk == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index 75b9029c1f..f646da0a24 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-jinja2/CHANGELOG.md b/ext/opentelemetry-ext-jinja2/CHANGELOG.md index bd0f6b54ae..ededf47daf 100644 --- a/ext/opentelemetry-ext-jinja2/CHANGELOG.md +++ b/ext/opentelemetry-ext-jinja2/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Add jinja2 instrumentation ([#643](https://github.com/open-telemetry/opentelemetry-python/pull/643)) diff --git a/ext/opentelemetry-ext-jinja2/setup.cfg b/ext/opentelemetry-ext-jinja2/setup.cfg index 8aa18bab8e..f592870cfe 100644 --- a/ext/opentelemetry-ext-jinja2/setup.cfg +++ b/ext/opentelemetry-ext-jinja2/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.7.dev0 - opentelemetry-auto-instrumentation == 0.7.dev0 + opentelemetry-api == 0.8.dev0 + opentelemetry-auto-instrumentation == 0.8.dev0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.7.dev0 + opentelemetry-test == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py +++ b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-mysql/CHANGELOG.md b/ext/opentelemetry-ext-mysql/CHANGELOG.md index 66dcecacce..8c1b06faa6 100644 --- a/ext/opentelemetry-ext-mysql/CHANGELOG.md +++ b/ext/opentelemetry-ext-mysql/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Implement instrumentor interface ([#654](https://github.com/open-telemetry/opentelemetry-python/pull/654)) ## 0.4a0 diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index d71512ee79..25bf824339 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.7.dev0 - opentelemetry-ext-dbapi == 0.7.dev0 - opentelemetry-auto-instrumentation == 0.7.dev0 + opentelemetry-api == 0.8.dev0 + opentelemetry-ext-dbapi == 0.8.dev0 + opentelemetry-auto-instrumentation == 0.8.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.7.dev0 + opentelemetry-test == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg index 1d2b19e0d5..1d67b11983 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.7.dev0 + opentelemetry-api == 0.8.dev0 [options.extras_require] test = - opentelemetry-test == 0.7.dev0 + opentelemetry-test == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-otcollector/setup.cfg b/ext/opentelemetry-ext-otcollector/setup.cfg index 0e02201bc8..8ffd4b6e4d 100644 --- a/ext/opentelemetry-ext-otcollector/setup.cfg +++ b/ext/opentelemetry-ext-otcollector/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.7.dev0 - opentelemetry-sdk == 0.7.dev0 + opentelemetry-api == 0.8.dev0 + opentelemetry-sdk == 0.8.dev0 protobuf >= 3.8.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/ext/opentelemetry-ext-prometheus/setup.cfg index 84ec3938d1..c35ad29dcb 100644 --- a/ext/opentelemetry-ext-prometheus/setup.cfg +++ b/ext/opentelemetry-ext-prometheus/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.7.dev0 - opentelemetry-sdk == 0.7.dev0 + opentelemetry-api == 0.8.dev0 + opentelemetry-sdk == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index f8c13ff9b0..4b4627fa83 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.7.dev0 + opentelemetry-api == 0.8.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-pymongo/CHANGELOG.md b/ext/opentelemetry-ext-pymongo/CHANGELOG.md index dd3cef2e3a..83a310c143 100644 --- a/ext/opentelemetry-ext-pymongo/CHANGELOG.md +++ b/ext/opentelemetry-ext-pymongo/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Implement instrumentor interface ([#612](https://github.com/open-telemetry/opentelemetry-python/pull/612)) ## 0.4a0 diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index 5c65a39551..cf9350e4c3 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.7.dev0 - opentelemetry-auto-instrumentation == 0.7.dev0 + opentelemetry-api == 0.8.dev0 + opentelemetry-auto-instrumentation == 0.8.dev0 pymongo ~= 3.1 [options.extras_require] test = - opentelemetry-test == 0.7.dev0 + opentelemetry-test == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-pymysql/CHANGELOG.md b/ext/opentelemetry-ext-pymysql/CHANGELOG.md index 33144da913..654146fece 100644 --- a/ext/opentelemetry-ext-pymysql/CHANGELOG.md +++ b/ext/opentelemetry-ext-pymysql/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-pymysql/setup.cfg b/ext/opentelemetry-ext-pymysql/setup.cfg index 9564f3f637..5b0df1b937 100644 --- a/ext/opentelemetry-ext-pymysql/setup.cfg +++ b/ext/opentelemetry-ext-pymysql/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.7.dev0 - opentelemetry-ext-dbapi == 0.7.dev0 - opentelemetry-auto-instrumentation == 0.7.dev0 + opentelemetry-api == 0.8.dev0 + opentelemetry-ext-dbapi == 0.8.dev0 + opentelemetry-auto-instrumentation == 0.8.dev0 PyMySQL ~= 0.9.3 [options.extras_require] test = - opentelemetry-test == 0.7.dev0 + opentelemetry-test == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-redis/CHANGELOG.md b/ext/opentelemetry-ext-redis/CHANGELOG.md index 33144da913..654146fece 100644 --- a/ext/opentelemetry-ext-redis/CHANGELOG.md +++ b/ext/opentelemetry-ext-redis/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-redis/setup.cfg b/ext/opentelemetry-ext-redis/setup.cfg index e24605c8e3..5ebd72f6d0 100644 --- a/ext/opentelemetry-ext-redis/setup.cfg +++ b/ext/opentelemetry-ext-redis/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.7dev0 - opentelemetry-auto-instrumentation == 0.7dev0 + opentelemetry-api == 0.8.dev0 + opentelemetry-auto-instrumentation == 0.8.dev0 redis >= 2.6 wrapt >= 1.12.1 [options.extras_require] test = - opentelemetry-test == 0.7.dev0 - opentelemetry-sdk == 0.7dev0 + opentelemetry-test == 0.8.dev0 + opentelemetry-sdk == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py +++ b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-requests/CHANGELOG.md b/ext/opentelemetry-ext-requests/CHANGELOG.md index 97167f86aa..c7bb877431 100644 --- a/ext/opentelemetry-ext-requests/CHANGELOG.md +++ b/ext/opentelemetry-ext-requests/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Rename package to opentelemetry-ext-requests ([#619](https://github.com/open-telemetry/opentelemetry-python/pull/619)) - Implement instrumentor interface, enabling auto-instrumentation diff --git a/ext/opentelemetry-ext-requests/setup.cfg b/ext/opentelemetry-ext-requests/setup.cfg index 9ab8401dd0..3fe0ef66d2 100644 --- a/ext/opentelemetry-ext-requests/setup.cfg +++ b/ext/opentelemetry-ext-requests/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.7.dev0 - opentelemetry-auto-instrumentation == 0.7.dev0 + opentelemetry-api == 0.8.dev0 + opentelemetry-auto-instrumentation == 0.8.dev0 requests ~= 2.0 [options.extras_require] test = - opentelemetry-test == 0.7.dev0 + opentelemetry-test == 0.8.dev0 httpretty ~= 1.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md b/ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md index 33144da913..654146fece 100644 --- a/ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md +++ b/ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-sqlalchemy/setup.cfg b/ext/opentelemetry-ext-sqlalchemy/setup.cfg index a13cead988..68473b383f 100644 --- a/ext/opentelemetry-ext-sqlalchemy/setup.cfg +++ b/ext/opentelemetry-ext-sqlalchemy/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.7.dev0 - opentelemetry-auto-instrumentation == 0.7.dev0 + opentelemetry-api == 0.8.dev0 + opentelemetry-auto-instrumentation == 0.8.dev0 wrapt >= 1.11.2 sqlalchemy [options.extras_require] test = - opentelemetry-sdk == 0.7.dev0 + opentelemetry-sdk == 0.8.dev0 pytest [options.packages.find] diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py +++ b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index fd4a6a60d8..87e117589c 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -40,11 +40,11 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.7.dev0 + opentelemetry-api == 0.8.dev0 [options.extras_require] test = - opentelemetry-test == 0.7.dev0 + opentelemetry-test == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-zipkin/CHANGELOG.md b/ext/opentelemetry-ext-zipkin/CHANGELOG.md index 28dee2d90b..2d0e17cdb0 100644 --- a/ext/opentelemetry-ext-zipkin/CHANGELOG.md +++ b/ext/opentelemetry-ext-zipkin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - bugfix: 'debug' field is now correct ([#549](https://github.com/open-telemetry/opentelemetry-python/pull/549)) diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg index 68ae439e39..25f8d51c8b 100644 --- a/ext/opentelemetry-ext-zipkin/setup.cfg +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 0.7.dev0 - opentelemetry-sdk == 0.7.dev0 + opentelemetry-api == 0.8.dev0 + opentelemetry-sdk == 0.8.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py index 86c61362ab..bcf6a35777 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 45d6dc0ea0..8cdfad0fe0 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Add reset for the global configuration object, for testing purposes ([#636](https://github.com/open-telemetry/opentelemetry-python/pull/636)) - tracer.get_tracer now optionally accepts a TracerProvider diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 86c61362ab..bcf6a35777 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/opentelemetry-auto-instrumentation/CHANGELOG.md b/opentelemetry-auto-instrumentation/CHANGELOG.md index ede42d2e20..028b84877d 100644 --- a/opentelemetry-auto-instrumentation/CHANGELOG.md +++ b/opentelemetry-auto-instrumentation/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Add support for programmatic instrumentation ([#579](https://github.com/open-telemetry/opentelemetry-python/pull/569)) - bugfix: enable auto-instrumentation command to work for custom entry points diff --git a/opentelemetry-auto-instrumentation/setup.cfg b/opentelemetry-auto-instrumentation/setup.cfg index 1ab2010cd8..96da015f86 100644 --- a/opentelemetry-auto-instrumentation/setup.cfg +++ b/opentelemetry-auto-instrumentation/setup.cfg @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: zip_safe = False include_package_data = True -install_requires = opentelemetry-api == 0.7.dev0 +install_requires = opentelemetry-api == 0.8.dev0 [options.packages.find] where = src diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py index 86c61362ab..bcf6a35777 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index d833c41e31..f771a2df34 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.7b1 + +Released 2020-05-12 + - Exporter API: span parents are now always spancontext ([#548](https://github.com/open-telemetry/opentelemetry-python/pull/548)) - tracer.get_tracer now optionally accepts a TracerProvider diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index da0f282ea6..5bb12fc655 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: zip_safe = False include_package_data = True -install_requires = opentelemetry-api==0.7.dev0 +install_requires = opentelemetry-api==0.8.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 86c61362ab..bcf6a35777 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 55bf2e584c..82d720c51c 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.7.dev0" +__version__ = "0.8.dev0" From 11810d869a3691d42f1321ff3232671d413aab53 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 14 May 2020 16:40:05 -0700 Subject: [PATCH 0342/1517] fix: new version of grpc causes linting issues if these aren't implemented (#696) This addresses the linting problems caused by a newer version of grpc: ************ Module _interceptor ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/_interceptor.py:167:0: W0223: Method '__enter__' is abstract in class 'Channel' but is not overridden (abstract-method) ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/_interceptor.py:167:0: W0223: Method '__exit__' is abstract in class 'Channel' but is not overridden (abstract-method) --- .../src/opentelemetry/ext/grpc/grpcext/_interceptor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/_interceptor.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/_interceptor.py index 0cae2cf9fd..74861913b9 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/_interceptor.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/_interceptor.py @@ -230,6 +230,14 @@ def close(self): ) self._channel.close() + def __enter__(self): + """Enters the runtime context related to the channel object.""" + raise NotImplementedError() + + def __exit__(self, exc_type, exc_val, exc_tb): + """Exits the runtime context related to the channel object.""" + raise NotImplementedError() + def intercept_channel(channel, *interceptors): result = channel From 4ab3978cf71af6425a120e9ebfb9960b27987b7a Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Thu, 14 May 2020 20:05:21 -0400 Subject: [PATCH 0343/1517] add check for event attribute values (#678) This PR addresses issue #352 Validates attribute values and events upon span creation and setting attributes on spans. Validation logic involves checking if value is one of (int, float, str, bool, list), and if list, each element must be valid and must be all the same primitive, valid type list values will not contain nested lists If value is a list, grants immutable property by converting value to a tuple. --- .../tests/test_shim.py | 3 - opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 95 ++++++++++++------ opentelemetry-sdk/tests/trace/test_trace.py | 96 ++++++++++++------- 4 files changed, 125 insertions(+), 71 deletions(-) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index 7a0913f973..abca2e052b 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -463,9 +463,6 @@ def test_span_on_error(self): # Verify exception details have been added to span. self.assertEqual(scope.span.unwrap().attributes["error"], True) - self.assertEqual( - scope.span.unwrap().events[0].attributes["error.kind"], Exception - ) def test_inject_http_headers(self): """Test `inject()` method for Format.HTTP_HEADERS.""" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index f771a2df34..476908cf91 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Validate span attribute types in SDK (#678) + ## 0.7b1 Released 2020-05-12 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 5eff5e6130..5b74b4a618 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -17,7 +17,6 @@ import atexit import json import logging -import os import random import threading from collections import OrderedDict @@ -41,6 +40,7 @@ MAX_NUM_ATTRIBUTES = 32 MAX_NUM_EVENTS = 128 MAX_NUM_LINKS = 32 +VALID_ATTR_VALUE_TYPES = (bool, str, int, float) class SpanProcessor: @@ -189,6 +189,48 @@ def attributes(self) -> types.Attributes: return self._event_formatter() +def _is_valid_attribute_value(value: types.AttributeValue) -> bool: + """Checks if attribute value is valid. + + An attribute value is valid if it is one of the valid types. If the value + is a sequence, it is only valid if all items in the sequence are of valid + type, not a sequence, and are of the same type. + """ + + if isinstance(value, Sequence): + if len(value) == 0: + return True + + first_element_type = type(value[0]) + + if first_element_type not in VALID_ATTR_VALUE_TYPES: + logger.warning( + "Invalid type %s in attribute value sequence. Expected one of " + "%s or a sequence of those types", + first_element_type.__name__, + [valid_type.__name__ for valid_type in VALID_ATTR_VALUE_TYPES], + ) + return False + + for element in list(value)[1:]: + if not isinstance(element, first_element_type): + logger.warning( + "Mixed types %s and %s in attribute value sequence", + first_element_type.__name__, + type(element).__name__, + ) + return False + elif not isinstance(value, VALID_ATTR_VALUE_TYPES): + logger.warning( + "Invalid type %s for attribute value. Expected one of %s or a " + "sequence of those types", + type(value).__name__, + [valid_type.__name__ for valid_type in VALID_ATTR_VALUE_TYPES], + ) + return False + return True + + class Span(trace_api.Span): """See `opentelemetry.trace.Span`. @@ -245,7 +287,8 @@ def __init__( self.status = None self._lock = threading.Lock() - if attributes is None: + self._filter_attribute_values(attributes) + if not attributes: self.attributes = Span._empty_attributes else: self.attributes = BoundedDict.from_map( @@ -255,7 +298,10 @@ def __init__( if events is None: self.events = Span._empty_events else: - self.events = BoundedList.from_seq(MAX_NUM_EVENTS, events) + self.events = BoundedList(MAX_NUM_EVENTS) + for event in events: + self._filter_attribute_values(event.attributes) + self.events.append(event) if links is None: self.links = Span._empty_links @@ -372,37 +418,24 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: logger.warning("invalid key (empty or null)") return - if isinstance(value, Sequence): - error_message = self._check_attribute_value_sequence(value) - if error_message is not None: - logger.warning("%s in attribute value sequence", error_message) - return + if _is_valid_attribute_value(value): # Freeze mutable sequences defensively if isinstance(value, MutableSequence): value = tuple(value) - elif not isinstance(value, (bool, str, int, float)): - logger.warning("invalid type for attribute value") - return - - self.attributes[key] = value + with self._lock: + self.attributes[key] = value @staticmethod - def _check_attribute_value_sequence(sequence: Sequence) -> Optional[str]: - """ - Checks if sequence items are valid and are of the same type - """ - if len(sequence) == 0: - return None - - first_element_type = type(sequence[0]) - - if first_element_type not in (bool, str, int, float): - return "invalid type" - - for element in sequence: - if not isinstance(element, first_element_type): - return "different type" - return None + def _filter_attribute_values(attributes: types.Attributes): + if attributes: + for attr_key, attr_value in list(attributes.items()): + if _is_valid_attribute_value(attr_value): + if isinstance(attr_value, MutableSequence): + attributes[attr_key] = tuple(attr_value) + else: + attributes[attr_key] = attr_value + else: + attributes.pop(attr_key) def _add_event(self, event: EventBase) -> None: with self._lock: @@ -423,7 +456,8 @@ def add_event( attributes: types.Attributes = None, timestamp: Optional[int] = None, ) -> None: - if attributes is None: + self._filter_attribute_values(attributes) + if not attributes: attributes = Span._empty_attributes self._add_event( Event( @@ -514,7 +548,6 @@ def __exit__( and self._set_status_on_exception and exc_val is not None ): - self.set_status( Status( canonical_code=StatusCanonicalCode.UNKNOWN, diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 1094f1afb9..e468652ec0 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -487,38 +487,26 @@ def test_invalid_attribute_values(self): self.assertEqual(len(root.attributes), 0) - def test_check_sequence_helper(self): + def test_check_attribute_helper(self): # pylint: disable=protected-access - self.assertEqual( - trace.Span._check_attribute_value_sequence([1, 2, 3.4, "ss", 4]), - "different type", - ) - self.assertEqual( - trace.Span._check_attribute_value_sequence([dict(), 1, 2, 3.4, 4]), - "invalid type", - ) - self.assertEqual( - trace.Span._check_attribute_value_sequence( - ["sw", "lf", 3.4, "ss"] - ), - "different type", - ) - self.assertEqual( - trace.Span._check_attribute_value_sequence([1, 2, 3.4, 5]), - "different type", + self.assertFalse(trace._is_valid_attribute_value([1, 2, 3.4, "ss", 4])) + self.assertFalse( + trace._is_valid_attribute_value([dict(), 1, 2, 3.4, 4]) ) - self.assertIsNone( - trace.Span._check_attribute_value_sequence([1, 2, 3, 5]) - ) - self.assertIsNone( - trace.Span._check_attribute_value_sequence([1.2, 2.3, 3.4, 4.5]) - ) - self.assertIsNone( - trace.Span._check_attribute_value_sequence([True, False]) - ) - self.assertIsNone( - trace.Span._check_attribute_value_sequence(["ss", "dw", "fw"]) + self.assertFalse( + trace._is_valid_attribute_value(["sw", "lf", 3.4, "ss"]) ) + self.assertFalse(trace._is_valid_attribute_value([1, 2, 3.4, 5])) + self.assertTrue(trace._is_valid_attribute_value([1, 2, 3, 5])) + self.assertTrue(trace._is_valid_attribute_value([1.2, 2.3, 3.4, 4.5])) + self.assertTrue(trace._is_valid_attribute_value([True, False])) + self.assertTrue(trace._is_valid_attribute_value(["ss", "dw", "fw"])) + self.assertTrue(trace._is_valid_attribute_value([])) + self.assertFalse(trace._is_valid_attribute_value(dict())) + self.assertTrue(trace._is_valid_attribute_value(True)) + self.assertTrue(trace._is_valid_attribute_value("hi")) + self.assertTrue(trace._is_valid_attribute_value(3.4)) + self.assertTrue(trace._is_valid_attribute_value(15)) def test_sampling_attributes(self): decision_attributes = { @@ -561,33 +549,67 @@ def test_events(self): # event name and attributes now = time_ns() - root.add_event("event1", {"name": "pluto"}) + root.add_event( + "event1", {"name": "pluto", "some_bools": [True, False]} + ) # event name, attributes and timestamp now = time_ns() - root.add_event("event2", {"name": "birthday"}, now) + root.add_event("event2", {"name": ["birthday"]}, now) + + mutable_list = ["original_contents"] + root.add_event("event3", {"name": mutable_list}) def event_formatter(): return {"name": "hello"} # lazy event - root.add_lazy_event("event3", event_formatter, now) + root.add_lazy_event("event4", event_formatter, now) - self.assertEqual(len(root.events), 4) + self.assertEqual(len(root.events), 5) self.assertEqual(root.events[0].name, "event0") self.assertEqual(root.events[0].attributes, {}) self.assertEqual(root.events[1].name, "event1") - self.assertEqual(root.events[1].attributes, {"name": "pluto"}) + self.assertEqual( + root.events[1].attributes, + {"name": "pluto", "some_bools": (True, False)}, + ) self.assertEqual(root.events[2].name, "event2") - self.assertEqual(root.events[2].attributes, {"name": "birthday"}) + self.assertEqual( + root.events[2].attributes, {"name": ("birthday",)} + ) self.assertEqual(root.events[2].timestamp, now) self.assertEqual(root.events[3].name, "event3") - self.assertEqual(root.events[3].attributes, {"name": "hello"}) - self.assertEqual(root.events[3].timestamp, now) + self.assertEqual( + root.events[3].attributes, {"name": ("original_contents",)} + ) + mutable_list = ["new_contents"] + self.assertEqual( + root.events[3].attributes, {"name": ("original_contents",)} + ) + + self.assertEqual(root.events[4].name, "event4") + self.assertEqual(root.events[4].attributes, {"name": "hello"}) + self.assertEqual(root.events[4].timestamp, now) + + def test_invalid_event_attributes(self): + self.assertIsNone(self.tracer.get_current_span()) + + with self.tracer.start_as_current_span("root") as root: + root.add_event("event0", {"attr1": True, "attr2": ["hi", False]}) + root.add_event("event0", {"attr1": dict()}) + root.add_event("event0", {"attr1": [[True]]}) + root.add_event("event0", {"attr1": [dict()], "attr2": [1, 2]}) + + self.assertEqual(len(root.events), 4) + self.assertEqual(root.events[0].attributes, {"attr1": True}) + self.assertEqual(root.events[1].attributes, {}) + self.assertEqual(root.events[2].attributes, {}) + self.assertEqual(root.events[3].attributes, {"attr2": (1, 2)}) def test_links(self): other_context1 = trace_api.SpanContext( From 34a96165b3fe5ab759cda0f8fcb2ae8b5e1896af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Thu, 14 May 2020 22:27:16 -0500 Subject: [PATCH 0344/1517] docs: Fix warning and treat them as errors (#666) The CI is not able to catch many documentation problems because we are ignoring warnings. This commit fixes most of the warnings, ignores the rest and enables a flag to treat them as errors. Co-authored-by: Diego Hurtado --- docs/conf.py | 20 ++++++- docs/examples/django/README.rst | 22 ++++---- docs/sdk/context.rst | 7 --- docs/sdk/sdk.rst | 1 - .../src/opentelemetry/ext/datadog/exporter.py | 4 +- .../src/opentelemetry/ext/dbapi/__init__.py | 52 +++++++++++-------- .../ext/opentracing_shim/__init__.py | 3 +- .../opentelemetry/ext/prometheus/__init__.py | 2 +- .../opentelemetry/ext/sqlalchemy/__init__.py | 3 +- .../opentelemetry/configuration/__init__.py | 2 +- tox.ini | 6 +-- 11 files changed, 68 insertions(+), 54 deletions(-) delete mode 100644 docs/sdk/context.rst diff --git a/docs/conf.py b/docs/conf.py index 3ea491b4fc..42fcf29f4f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -78,6 +78,8 @@ None, ), "aiohttp": ("https://aiohttp.readthedocs.io/en/stable/", None), + "wrapt": ("https://wrapt.readthedocs.io/en/latest/", None), + "pymongo": ("https://pymongo.readthedocs.io/en/stable/", None), } # http://www.sphinx-doc.org/en/master/config.html#confval-nitpicky @@ -89,8 +91,22 @@ nitpick_ignore = [ ("py:class", "ValueT"), ("py:class", "MetricT"), - ("py:class", "typing.Tuple"), - ("py:class", "pymongo.monitoring.CommandListener"), + # Even if wrapt is added to intersphinx_mapping, sphinx keeps failing + # with "class reference target not found: ObjectProxy". + ("py:class", "ObjectProxy"), + # TODO: Understand why sphinx is not able to find this local class + ( + "py:class", + "opentelemetry.trace.propagation.httptextformat.HTTPTextFormat", + ), + ( + "any", + "opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract", + ), + ( + "any", + "opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject", + ), ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 2b2f158e7a..95c106d32f 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -1,7 +1,7 @@ OpenTelemetry Django Instrumentation Example ============================================ -This shows how to use `opentelemetry-ext-django` to automatically instrument a +This shows how to use ``opentelemetry-ext-django`` to automatically instrument a Django app. For more user convenience, a Django app is already provided in this directory. @@ -36,30 +36,30 @@ Execution of the Django app Set these environment variables first: -#. `export OPENTELEMETRY_PYTHON_DJANGO_INSTRUMENT=True` -#. `export DJANGO_SETTINGS_MODULE=instrumentation_example.settings` +#. ``export OPENTELEMETRY_PYTHON_DJANGO_INSTRUMENT=True`` +#. ``export DJANGO_SETTINGS_MODULE=instrumentation_example.settings`` The way to achieve OpenTelemetry instrumentation for your Django app is to use -an `opentelemetry.ext.django.DjangoInstrumentor` to instrument the app. +an ``opentelemetry.ext.django.DjangoInstrumentor`` to instrument the app. -Clone the `opentelemetry-python` repository and go to `opentelemetry-python/docs/examples/django`. +Clone the ``opentelemetry-python`` repository and go to ``opentelemetry-python/docs/examples/django``. -Once there, open the `manage.py` file. The call to `DjangoInstrumentor().instrument()` -in `main` is all that is needed to make the app be instrumented. +Once there, open the ``manage.py`` file. The call to ``DjangoInstrumentor().instrument()`` +in ``main`` is all that is needed to make the app be instrumented. -Run the Django app with `python manage.py runserver`. +Run the Django app with ``python manage.py runserver``. Execution of the client ....................... Open up a new console and activate the previous virtual environment there too: -`source django_auto_instrumentation/bin/activate` +``source django_auto_instrumentation/bin/activate`` -Go to `opentelemetry-python/ext/opentelemetry-ext-django/example`, once there +Go to ``opentelemetry-python/ext/opentelemetry-ext-django/example``, once there run the client with: -`python client.py hello` +``python client.py hello`` Go to the previous console, where the Django app is running. You should see output similar to this one: diff --git a/docs/sdk/context.rst b/docs/sdk/context.rst deleted file mode 100644 index 12b15b1ed8..0000000000 --- a/docs/sdk/context.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.sdk.context -========================================== - -.. automodule:: opentelemetry.sdk.context - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/sdk/sdk.rst b/docs/sdk/sdk.rst index b16a6f2650..27e9d44fa4 100644 --- a/docs/sdk/sdk.rst +++ b/docs/sdk/sdk.rst @@ -6,6 +6,5 @@ OpenTelemetry Python SDK .. toctree:: :maxdepth: 1 - context metrics trace \ No newline at end of file diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py index 4843200a2c..4420e69db5 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py @@ -50,8 +50,8 @@ class DatadogSpanExporter(SpanExporter): """Datadog span exporter for OpenTelemetry. Args: - agent_url: The url of the Datadog Agent or use `DD_TRACE_AGENT_URL` environment variable - service: The service to be used for the application or use `DD_SERVICE` environment variable + agent_url: The url of the Datadog Agent or use ``DD_TRACE_AGENT_URL`` environment variable + service: The service to be used for the application or use ``DD_SERVICE`` environment variable """ def __init__(self, agent_url=None, service=None): diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index 626b672d6c..57649be7b4 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -13,8 +13,9 @@ # limitations under the License. """ -The trace integration with Database API supports libraries following the -`Python Database API Specification v2.0. `_ +The trace integration with Database API supports libraries that follow the +Python Database API Specification v2.0. +``_ Usage ----- @@ -53,7 +54,7 @@ def trace_integration( - connect_module: typing.Callable[..., any], + connect_module: typing.Callable[..., typing.Any], connect_method_name: str, database_component: str, database_type: str = "", @@ -66,10 +67,13 @@ def trace_integration( Args: connect_module: Module name where connect method is available. connect_method_name: The connect method name. - database_component: Database driver name or database name "JDBI", "jdbc", "odbc", "postgreSQL". + database_component: Database driver name or database name "JDBI", + "jdbc", "odbc", "postgreSQL". database_type: The Database type. For any SQL database, "sql". - connection_attributes: Attribute names for database, port, host and user in Connection object. - tracer_provider: The :class:`TracerProvider` to use. If ommited the current configured one is used. + connection_attributes: Attribute names for database, port, host and + user in Connection object. + tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to + use. If ommited the current configured one is used. """ tracer = get_tracer(__name__, __version__, tracer_provider) wrap_connect( @@ -84,7 +88,7 @@ def trace_integration( def wrap_connect( tracer: Tracer, - connect_module: typing.Callable[..., any], + connect_module: typing.Callable[..., typing.Any], connect_method_name: str, database_component: str, database_type: str = "", @@ -94,20 +98,22 @@ def wrap_connect( https://www.python.org/dev/peps/pep-0249/ Args: - tracer: The :class:`Tracer` to use. + tracer: The :class:`opentelemetry.trace.Tracer` to use. connect_module: Module name where connect method is available. connect_method_name: The connect method name. - database_component: Database driver name or database name "JDBI", "jdbc", "odbc", "postgreSQL". + database_component: Database driver name or database name "JDBI", + "jdbc", "odbc", "postgreSQL". database_type: The Database type. For any SQL database, "sql". - connection_attributes: Attribute names for database, port, host and user in Connection object. + connection_attributes: Attribute names for database, port, host and + user in Connection object. """ # pylint: disable=unused-argument - def _wrap_connect( - wrapped: typing.Callable[..., any], + def wrap_connect_( + wrapped: typing.Callable[..., typing.Any], instance: typing.Any, - args: typing.Tuple[any, any], - kwargs: typing.Dict[any, any], + args: typing.Tuple[typing.Any, typing.Any], + kwargs: typing.Dict[typing.Any, typing.Any], ): db_integration = DatabaseApiIntegration( tracer, @@ -119,14 +125,14 @@ def _wrap_connect( try: wrapt.wrap_function_wrapper( - connect_module, connect_method_name, _wrap_connect + connect_module, connect_method_name, wrap_connect_ ) except Exception as ex: # pylint: disable=broad-except logger.warning("Failed to integrate with DB API. %s", str(ex)) def unwrap_connect( - connect_module: typing.Callable[..., any], connect_method_name: str, + connect_module: typing.Callable[..., typing.Any], connect_method_name: str, ): """Disable integration with DB API library. https://www.python.org/dev/peps/pep-0249/ @@ -150,7 +156,7 @@ def instrument_connection( """Enable instrumentation in a database connection. Args: - tracer: The :class:`Tracer` to use. + tracer: The :class:`opentelemetry.trace.Tracer` to use. connection: The connection to instrument. database_component: Database driver name or database name "JDBI", "jdbc", "odbc", "postgreSQL". @@ -213,9 +219,9 @@ def __init__( def wrapped_connection( self, - connect_method: typing.Callable[..., any], - args: typing.Tuple[any, any], - kwargs: typing.Dict[any, any], + connect_method: typing.Callable[..., typing.Any], + args: typing.Tuple[typing.Any, typing.Any], + kwargs: typing.Dict[typing.Any, typing.Any], ): """Add object proxy to connection object. """ @@ -279,9 +285,9 @@ def __init__(self, db_api_integration: DatabaseApiIntegration): def traced_execution( self, - query_method: typing.Callable[..., any], - *args: typing.Tuple[any, any], - **kwargs: typing.Dict[any, any] + query_method: typing.Callable[..., typing.Any], + *args: typing.Tuple[typing.Any, typing.Any], + **kwargs: typing.Dict[typing.Any, typing.Any] ): statement = args[0] if args else "" diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index 9bb6fc8d6b..c62f063de2 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -241,8 +241,7 @@ def set_tag(self, key, value): def log_kv(self, key_values, timestamp=None): """Implements the ``log_kv()`` method from the base class. - Logs an :class:`opentelemetry.trace.Event` for the wrapped - OpenTelemetry span. + Logs an event for the wrapped OpenTelemetry span. Note: The OpenTracing API defines the values of *key_values* to be of any diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py index 1dd307af93..f6f91fc586 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py @@ -94,7 +94,7 @@ class PrometheusMetricsExporter(MetricsExporter): Args: prefix: single-word application prefix relevant to the domain - the metric belongs to. + the metric belongs to. """ def __init__(self, prefix: str = ""): diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py index 19078fe0a5..05b384429b 100644 --- a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py +++ b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py @@ -16,11 +16,12 @@ Instrument `sqlalchemy`_ to report SQL queries. There are two options for instrumenting code. The first option is to use -the `opentelemetry-auto-instrumentation` executable which will automatically +the ``opentelemetry-auto-instrumentation`` executable which will automatically instrument your SQLAlchemy engine. The second is to programmatically enable instrumentation via the following code: .. _sqlalchemy: https://pypi.org/project/sqlalchemy/ + Usage ----- .. code:: python diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index d0fc11dc59..d2c78845d0 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -29,7 +29,7 @@ 2. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_`` 3. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND__ELSE`` 4. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND_else`` -4. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND_else2`` +5. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND_else2`` These won't: diff --git a/tox.ini b/tox.ini index 40a2c02d70..04ea2b1650 100644 --- a/tox.ini +++ b/tox.ini @@ -280,13 +280,13 @@ commands = [testenv:docs] deps = - -c dev-requirements.txt - -r docs-requirements.txt + -c {toxinidir}/dev-requirements.txt + -r {toxinidir}/docs-requirements.txt changedir = docs commands = - sphinx-build -E -a --keep-going -b html -T . _build/html + sphinx-build -E -a -W -b html -T . _build/html [testenv:py38-tracecontext] basepython: python3.8 From 194824ba685f9986199db4200a2bed04746dbb57 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 14 May 2020 22:19:57 -0700 Subject: [PATCH 0345/1517] api: ensure status is always string (#640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensuring that exporters can always expect status descriptions to be a string. This was implemented to be defensive against instrumentations such as PyMongo, which currently set status as a dict. Co-authored-by: Mauricio Vásquez Co-authored-by: Yusuke Tsutsumi --- .../otcollector/trace_exporter/__init__.py | 1 + .../tests/test_otcollector_trace_exporter.py | 9 +++++ .../src/opentelemetry/trace/status.py | 9 ++++- opentelemetry-api/tests/trace/test_status.py | 35 +++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 opentelemetry-api/tests/trace/test_status.py diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py index fb6237e86d..914d97ec5a 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py @@ -98,6 +98,7 @@ def translate_to_collector(spans: Sequence[Span]): code=span.status.canonical_code.value, message=span.status.description, ) + collector_span = trace_pb2.Span( name=trace_pb2.TruncatableString(value=span.name), kind=utils.get_collector_span_kind(span.kind), diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py index 4a0a556137..97d276af40 100644 --- a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py @@ -154,6 +154,11 @@ def test_translate_to_collector(self): ) otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) + otel_spans[1].set_status( + trace_api.Status( + trace_api.status.StatusCanonicalCode.INTERNAL, {"test", "val"}, + ) + ) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) otel_spans[2].end(end_time=end_times[2]) @@ -263,6 +268,10 @@ def test_translate_to_collector(self): output_spans[0].links.link[0].type, trace_pb2.Span.Link.Type.TYPE_UNSPECIFIED, ) + self.assertEqual( + output_spans[1].status.code, + trace_api.status.StatusCanonicalCode.INTERNAL.value, + ) self.assertEqual( output_spans[2].links.link[0].type, trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN, diff --git a/opentelemetry-api/src/opentelemetry/trace/status.py b/opentelemetry-api/src/opentelemetry/trace/status.py index 4ae2ad96d0..4191369dfe 100644 --- a/opentelemetry-api/src/opentelemetry/trace/status.py +++ b/opentelemetry-api/src/opentelemetry/trace/status.py @@ -13,8 +13,11 @@ # limitations under the License. import enum +import logging import typing +logger = logging.getLogger(__name__) + class StatusCanonicalCode(enum.Enum): """Represents the canonical set of status codes of a finished Span.""" @@ -167,7 +170,11 @@ def __init__( description: typing.Optional[str] = None, ): self._canonical_code = canonical_code - self._description = description + self._description = None + if description is not None and not isinstance(description, str): + logger.warning("Invalid status description type, expected str") + else: + self._description = description @property def canonical_code(self) -> StatusCanonicalCode: diff --git a/opentelemetry-api/tests/trace/test_status.py b/opentelemetry-api/tests/trace/test_status.py new file mode 100644 index 0000000000..6c086f3ae0 --- /dev/null +++ b/opentelemetry-api/tests/trace/test_status.py @@ -0,0 +1,35 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from logging import WARNING + +from opentelemetry.trace.status import Status, StatusCanonicalCode + + +class TestStatus(unittest.TestCase): + def test_constructor(self): + status = Status() + self.assertIs(status.canonical_code, StatusCanonicalCode.OK) + self.assertIsNone(status.description) + + status = Status(StatusCanonicalCode.UNAVAILABLE, "unavailable") + self.assertIs(status.canonical_code, StatusCanonicalCode.UNAVAILABLE) + self.assertEqual(status.description, "unavailable") + + def test_invalid_description(self): + with self.assertLogs(level=WARNING): + status = Status(description={"test": "val"}) # type: ignore + self.assertIs(status.canonical_code, StatusCanonicalCode.OK) + self.assertEqual(status.description, None) From 5913d4a4cb799ae2ab7afb65bbfaaedabfe2d0cd Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 15 May 2020 09:57:22 -0700 Subject: [PATCH 0346/1517] testing: add in-memory metrics exporter (#653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding InMemoryMetricsExporter to help with unit-tests, as part of the change i'm also adding meter_provider and memory_metrics_exporter to TestBase Co-authored-by: Mauricio Vásquez --- .../export/in_memory_metrics_exporter.py | 58 +++++++++++++++++++ .../util/src/opentelemetry/test/test_base.py | 30 +++++++++- 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py new file mode 100644 index 0000000000..a5df2bb1d8 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +from typing import Sequence + +from . import MetricRecord, MetricsExporter, MetricsExportResult + + +class InMemoryMetricsExporter(MetricsExporter): + """Implementation of `MetricsExporter` that stores metrics in memory. + + This class can be used for testing purposes. It stores exported metrics + in a list in memory that can be retrieved using the + :func:`.get_exported_metrics` method. + """ + + def __init__(self): + self._exported_metrics = [] + self._stopped = False + self._lock = threading.Lock() + + def clear(self): + """Clear list of collected metrics.""" + with self._lock: + self._exported_metrics.clear() + + def export( + self, metric_records: Sequence[MetricRecord] + ) -> MetricsExportResult: + if self._stopped: + return MetricsExportResult.FAILURE + with self._lock: + self._exported_metrics.extend(metric_records) + return MetricsExportResult.SUCCESS + + def get_exported_metrics(self): + """Get list of collected metrics.""" + with self._lock: + return tuple(self._exported_metrics) + + def shutdown(self) -> None: + """Shuts down the exporter. + + Called when the SDK is shut down. + """ + self._stopped = True diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index ca015ff011..4b40d52ccb 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -16,7 +16,12 @@ import unittest from contextlib import contextmanager +from opentelemetry import metrics as metrics_api from opentelemetry import trace as trace_api +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export.in_memory_metrics_exporter import ( + InMemoryMetricsExporter, +) from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, @@ -26,14 +31,19 @@ class TestBase(unittest.TestCase): @classmethod def setUpClass(cls): - cls.original_provider = trace_api.get_tracer_provider() + cls.original_tracer_provider = trace_api.get_tracer_provider() result = cls.create_tracer_provider() cls.tracer_provider, cls.memory_exporter = result trace_api.set_tracer_provider(cls.tracer_provider) + cls.original_meter_provider = metrics_api.get_meter_provider() + result = cls.create_meter_provider() + cls.meter_provider, cls.memory_metrics_exporter = result + metrics_api.set_meter_provider(cls.meter_provider) @classmethod def tearDownClass(cls): - trace_api.set_tracer_provider(cls.original_provider) + trace_api.set_tracer_provider(cls.original_tracer_provider) + metrics_api.set_meter_provider(cls.original_meter_provider) def setUp(self): self.memory_exporter.clear() @@ -53,7 +63,7 @@ def create_tracer_provider(**kwargs): Returns: A list with the tracer provider in the first element and the - memory exporter in the second. + in-memory span exporter in the second. """ tracer_provider = TracerProvider(**kwargs) memory_exporter = InMemorySpanExporter() @@ -62,6 +72,20 @@ def create_tracer_provider(**kwargs): return tracer_provider, memory_exporter + @staticmethod + def create_meter_provider(**kwargs): + """Helper to create a configured meter provider + + Creates a `MeterProvider` and an `InMemoryMetricsExporter`. + + Returns: + A list with the meter provider in the first element and the + in-memory metrics exporter in the second + """ + meter_provider = MeterProvider(**kwargs) + memory_exporter = InMemoryMetricsExporter() + return meter_provider, memory_exporter + @staticmethod @contextmanager def disable_logging(highest_level=logging.CRITICAL): From 01b91170dafd90bba9e4acabbacaca58cb0f8516 Mon Sep 17 00:00:00 2001 From: Nir Hadassi Date: Tue, 19 May 2020 07:28:48 +0300 Subject: [PATCH 0347/1517] ext/jaeger - Transform resource to tags when exporting (#645) Labels in the resource now populate span tags for the Jaeger exporter. Co-authored-by: Yusuke Tsutsumi --- .../src/opentelemetry/ext/jaeger/__init__.py | 2 ++ .../tests/test_jaeger_exporter.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py index 23ed6e725d..852f94f610 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -197,6 +197,8 @@ def _translate_to_jaeger(spans: Span): parent_id = span.parent.span_id if span.parent else 0 tags = _extract_tags(span.attributes) + if span.resource: + tags.extend(_extract_tags(span.resource.labels)) tags.extend( [ diff --git a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py index 2c792fd5d1..904ca9bff1 100644 --- a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py +++ b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py @@ -22,6 +22,7 @@ from opentelemetry import trace as trace_api from opentelemetry.ext.jaeger.gen.jaeger import ttypes as jaeger from opentelemetry.sdk import trace +from opentelemetry.sdk.trace import Resource from opentelemetry.trace.status import Status, StatusCanonicalCode @@ -199,6 +200,9 @@ def test_translate_to_jaeger(self): otel_spans[0].set_attribute("key_bool", False) otel_spans[0].set_attribute("key_string", "hello_world") otel_spans[0].set_attribute("key_float", 111.22) + otel_spans[0].resource = Resource( + labels={"key_resource": "some_resource"} + ) otel_spans[0].set_status( Status(StatusCanonicalCode.UNKNOWN, "Example description") ) @@ -237,6 +241,11 @@ def test_translate_to_jaeger(self): vType=jaeger.TagType.DOUBLE, vDouble=111.22, ), + jaeger.Tag( + key="key_resource", + vType=jaeger.TagType.STRING, + vStr="some_resource", + ), jaeger.Tag( key="status.code", vType=jaeger.TagType.LONG, From 7eccae2b97759b1fb25ef8483a6dff0d616f4a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20=C3=85mdal?= Date: Tue, 19 May 2020 00:54:24 -0400 Subject: [PATCH 0348/1517] docs: update docs for requests plugin (#697) updating incorrect documentation for requests plugin. --- .../src/opentelemetry/ext/requests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py index 2a82e9820c..1621c4a95e 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py @@ -25,7 +25,7 @@ import opentelemetry.ext.requests # You can optionally pass a custom TracerProvider to RequestInstrumentor.instrument() - opentelemetry.ext.requests.RequestInstrumentor.instrument() + opentelemetry.ext.requests.RequestsInstrumentor().instrument() response = requests.get(url="https://www.example.org/") Limitations From 9e58b8a8f87eefbc8c78d0b82f98f5dd2976480b Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 19 May 2020 23:19:54 -0600 Subject: [PATCH 0349/1517] api: Handle boolean, integer and float values in Configuration (#662) Many times, configuration values are just boolean values. The Configuration object now only returns the literal configuration value. It would be useful to have this object return True instead of "True" to keep code a bit shorter --- .../src/opentelemetry/ext/django/__init__.py | 2 +- .../opentelemetry/configuration/__init__.py | 19 +++++++ .../tests/configuration/test_configuration.py | 55 +++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py index f59d90b490..d4e78351aa 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py @@ -41,7 +41,7 @@ def _instrument(self, **kwargs): # built inside the Configuration class itself with the magic method # __bool__ - if Configuration().DJANGO_INSTRUMENT != "True": + if not Configuration().DJANGO_INSTRUMENT: return # This can not be solved, but is an inherent problem of this approach: diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index d2c78845d0..4809341502 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -78,6 +78,11 @@ OpenTelemetry API provided providers are the default ones used if no configuration is found in the environment variables). +Configuration values that are exactly ``"True"`` or ``"False"`` will be +converted to its boolean values of ``True`` and ``False`` respectively. + +Configuration values that can be casted to integers or floats will be casted. + This object can be used by any OpenTelemetry component, native or external. For that reason, the ``Configuration`` object is designed to be immutable. If a component would change the value of one of the ``Configuration`` object @@ -110,6 +115,20 @@ def __new__(cls) -> "Configuration": key = match.group(1) + if value == "True": + value = True + elif value == "False": + value = False + else: + try: + value = int(value) + except ValueError: + pass + try: + value = float(value) + except ValueError: + pass + setattr(Configuration, "_{}".format(key), value) setattr( Configuration, diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index c736c97262..a817a1bf43 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -88,3 +88,58 @@ def test_reset(self): self.assertIsNone( Configuration().TRACER_PROVIDER ) # pylint: disable=no-member + + @patch.dict( + "os.environ", # type: ignore + { + "OPENTELEMETRY_PYTHON_TRUE": "True", + "OPENTELEMETRY_PYTHON_FALSE": "False", + }, + ) + def test_boolean(self): + self.assertIsInstance( + Configuration().TRUE, bool + ) # pylint: disable=no-member + self.assertIsInstance( + Configuration().FALSE, bool + ) # pylint: disable=no-member + self.assertTrue(Configuration().TRUE) # pylint: disable=no-member + self.assertFalse(Configuration().FALSE) # pylint: disable=no-member + + @patch.dict( + "os.environ", # type: ignore + { + "OPENTELEMETRY_PYTHON_POSITIVE_INTEGER": "123", + "OPENTELEMETRY_PYTHON_NEGATIVE_INTEGER": "-123", + "OPENTELEMETRY_PYTHON_NON_INTEGER": "-12z3", + }, + ) + def test_integer(self): + self.assertEqual( + Configuration().POSITIVE_INTEGER, 123 + ) # pylint: disable=no-member + self.assertEqual( + Configuration().NEGATIVE_INTEGER, -123 + ) # pylint: disable=no-member + self.assertEqual( + Configuration().NON_INTEGER, "-12z3" + ) # pylint: disable=no-member + + @patch.dict( + "os.environ", # type: ignore + { + "OPENTELEMETRY_PYTHON_POSITIVE_FLOAT": "123.123", + "OPENTELEMETRY_PYTHON_NEGATIVE_FLOAT": "-123.123", + "OPENTELEMETRY_PYTHON_NON_FLOAT": "-12z3.123", + }, + ) + def test_float(self): + self.assertEqual( + Configuration().POSITIVE_FLOAT, 123.123 + ) # pylint: disable=no-member + self.assertEqual( + Configuration().NEGATIVE_FLOAT, -123.123 + ) # pylint: disable=no-member + self.assertEqual( + Configuration().NON_FLOAT, "-12z3.123" + ) # pylint: disable=no-member From 2d9c8df5de7c158cea21aa2bbcd35a6b7d65ee98 Mon Sep 17 00:00:00 2001 From: Connor Adams Date: Thu, 21 May 2020 00:50:17 -0400 Subject: [PATCH 0350/1517] ext/psycopg2: Implement BaseInstrumentor interface (#694) - Implemented BaseInstrumentor interface to enable auto-instrumentation - Added integration tests (same tests as other db integrations) --- .../src/opentelemetry/ext/dbapi/__init__.py | 79 +++++----- .../tests/test_dbapi_integration.py | 4 - .../tests/postgres/test_psycopg_functional.py | 5 +- ext/opentelemetry-ext-psycopg2/CHANGELOG.md | 2 + ext/opentelemetry-ext-psycopg2/setup.cfg | 10 ++ .../opentelemetry/ext/psycopg2/__init__.py | 137 +++++++++--------- .../tests/test_psycopg2_integration.py | 104 ++++++++++++- tox.ini | 3 +- 8 files changed, 216 insertions(+), 128 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index 57649be7b4..a5fda62dea 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -174,7 +174,7 @@ def instrument_connection( connection_attributes=connection_attributes, ) db_integration.get_connection_attributes(connection) - return TracedConnectionProxy(connection, db_integration) + return get_traced_connection_proxy(connection, db_integration) def uninstrument_connection(connection): @@ -227,7 +227,7 @@ def wrapped_connection( """ connection = connect_method(*args, **kwargs) self.get_connection_attributes(connection) - return TracedConnectionProxy(connection, self) + return get_traced_connection_proxy(connection, self) def get_connection_attributes(self, connection): # Populate span fields using connection @@ -260,23 +260,21 @@ def get_connection_attributes(self, connection): self.span_attributes["net.peer.port"] = port -# pylint: disable=abstract-method -class TracedConnectionProxy(wrapt.ObjectProxy): - # pylint: disable=unused-argument - def __init__( - self, - connection, - db_api_integration: DatabaseApiIntegration, - *args, - **kwargs - ): - wrapt.ObjectProxy.__init__(self, connection) - self._db_api_integration = db_api_integration +def get_traced_connection_proxy( + connection, db_api_integration, *args, **kwargs +): + # pylint: disable=abstract-method + class TracedConnectionProxy(wrapt.ObjectProxy): + # pylint: disable=unused-argument + def __init__(self, connection, *args, **kwargs): + wrapt.ObjectProxy.__init__(self, connection) + + def cursor(self, *args, **kwargs): + return get_traced_cursor_proxy( + self.__wrapped__.cursor(*args, **kwargs), db_api_integration + ) - def cursor(self, *args, **kwargs): - return TracedCursorProxy( - self.__wrapped__.cursor(*args, **kwargs), self._db_api_integration - ) + return TracedConnectionProxy(connection, *args, **kwargs) class TracedCursor: @@ -323,31 +321,28 @@ def traced_execution( raise ex -# pylint: disable=abstract-method -class TracedCursorProxy(wrapt.ObjectProxy): +def get_traced_cursor_proxy(cursor, db_api_integration, *args, **kwargs): + _traced_cursor = TracedCursor(db_api_integration) + # pylint: disable=abstract-method + class TracedCursorProxy(wrapt.ObjectProxy): - # pylint: disable=unused-argument - def __init__( - self, - cursor, - db_api_integration: DatabaseApiIntegration, - *args, - **kwargs - ): - wrapt.ObjectProxy.__init__(self, cursor) - self._traced_cursor = TracedCursor(db_api_integration) + # pylint: disable=unused-argument + def __init__(self, cursor, *args, **kwargs): + wrapt.ObjectProxy.__init__(self, cursor) - def execute(self, *args, **kwargs): - return self._traced_cursor.traced_execution( - self.__wrapped__.execute, *args, **kwargs - ) + def execute(self, *args, **kwargs): + return _traced_cursor.traced_execution( + self.__wrapped__.execute, *args, **kwargs + ) - def executemany(self, *args, **kwargs): - return self._traced_cursor.traced_execution( - self.__wrapped__.executemany, *args, **kwargs - ) + def executemany(self, *args, **kwargs): + return _traced_cursor.traced_execution( + self.__wrapped__.executemany, *args, **kwargs + ) - def callproc(self, *args, **kwargs): - return self._traced_cursor.traced_execution( - self.__wrapped__.callproc, *args, **kwargs - ) + def callproc(self, *args, **kwargs): + return _traced_cursor.traced_execution( + self.__wrapped__.callproc, *args, **kwargs + ) + + return TracedCursorProxy(cursor, *args, **kwargs) diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py index 88c243c5bf..a2ba9f7d89 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -125,7 +125,6 @@ def test_wrap_connect(self, mock_dbapi): dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-") connection = mock_dbapi.connect() self.assertEqual(mock_dbapi.connect.call_count, 1) - self.assertIsInstance(connection, dbapi.TracedConnectionProxy) self.assertIsInstance(connection.__wrapped__, mock.Mock) @mock.patch("opentelemetry.ext.dbapi") @@ -133,7 +132,6 @@ def test_unwrap_connect(self, mock_dbapi): dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-") connection = mock_dbapi.connect() self.assertEqual(mock_dbapi.connect.call_count, 1) - self.assertIsInstance(connection, dbapi.TracedConnectionProxy) dbapi.unwrap_connect(mock_dbapi, "connect") connection = mock_dbapi.connect() @@ -145,7 +143,6 @@ def test_instrument_connection(self): # Avoid get_attributes failing because can't concatenate mock connection.database = "-" connection2 = dbapi.instrument_connection(self.tracer, connection, "-") - self.assertIsInstance(connection2, dbapi.TracedConnectionProxy) self.assertIs(connection2.__wrapped__, connection) def test_uninstrument_connection(self): @@ -154,7 +151,6 @@ def test_uninstrument_connection(self): # be concatenated connection.database = "-" connection2 = dbapi.instrument_connection(self.tracer, connection, "-") - self.assertIsInstance(connection2, dbapi.TracedConnectionProxy) self.assertIs(connection2.__wrapped__, connection) connection3 = dbapi.uninstrument_connection(connection2) diff --git a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py index e0537ad293..d0bdd685f4 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py @@ -18,7 +18,7 @@ import psycopg2 from opentelemetry import trace as trace_api -from opentelemetry.ext.psycopg2 import trace_integration +from opentelemetry.ext.psycopg2 import Psycopg2Instrumentor from opentelemetry.test.test_base import TestBase POSTGRES_HOST = os.getenv("POSTGRESQL_HOST ", "localhost") @@ -35,7 +35,7 @@ def setUpClass(cls): cls._connection = None cls._cursor = None cls._tracer = cls.tracer_provider.get_tracer(__name__) - trace_integration(cls.tracer_provider) + Psycopg2Instrumentor().instrument(tracer_provider=cls.tracer_provider) cls._connection = psycopg2.connect( dbname=POSTGRES_DB_NAME, user=POSTGRES_USER, @@ -52,6 +52,7 @@ def tearDownClass(cls): cls._cursor.close() if cls._connection: cls._connection.close() + Psycopg2Instrumentor().uninstrument() def validate_spans(self): spans = self.memory_exporter.get_finished_spans() diff --git a/ext/opentelemetry-ext-psycopg2/CHANGELOG.md b/ext/opentelemetry-ext-psycopg2/CHANGELOG.md index f32ad5bd4c..1a6748f7ba 100644 --- a/ext/opentelemetry-ext-psycopg2/CHANGELOG.md +++ b/ext/opentelemetry-ext-psycopg2/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Implement instrumentor interface, enabling auto-instrumentation ([#694]https://github.com/open-telemetry/opentelemetry-python/pull/694) + ## 0.4a0 Released 2020-02-21 diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index 4b4627fa83..2a6e69605d 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -41,8 +41,18 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.8.dev0 + opentelemetry-ext-dbapi == 0.8.dev0 + opentelemetry-auto-instrumentation == 0.8.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 +[options.extras_require] +test = + opentelemetry-test == 0.8.dev0 + [options.packages.find] where = src + +[options.entry_points] +opentelemetry_instrumentor = + psycopg2 = opentelemetry.ext.psycopg2:Psycopg2Instrumentor diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py index a0db2993ba..582d147320 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py @@ -13,8 +13,8 @@ # limitations under the License. """ -The integration with PostgreSQL supports the `Psycopg`_ library and is specified -to ``trace_integration`` using ``'PostgreSQL'``. +The integration with PostgreSQL supports the `Psycopg`_ library, it can be enabled by +using ``Psycopg2Instrumentor``. .. _Psycopg: http://initd.org/psycopg/ @@ -26,11 +26,12 @@ import psycopg2 from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.trace.ext.psycopg2 import trace_integration + from opentelemetry.trace.ext.psycopg2 import Psycopg2Instrumentor trace.set_tracer_provider(TracerProvider()) - trace_integration() + Psycopg2Instrumentor().instrument() + cnx = psycopg2.connect(database='Database') cursor = cnx.cursor() cursor.execute("INSERT INTO test (testField) VALUES (123)") @@ -41,83 +42,77 @@ --- """ -import logging import typing import psycopg2 import wrapt -from psycopg2.sql import Composable -from opentelemetry.ext.dbapi import DatabaseApiIntegration, TracedCursor +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.ext import dbapi from opentelemetry.ext.psycopg2.version import __version__ -from opentelemetry.trace import Tracer, get_tracer - -logger = logging.getLogger(__name__) - -DATABASE_COMPONENT = "postgresql" -DATABASE_TYPE = "sql" +from opentelemetry.trace import TracerProvider, get_tracer -def trace_integration(tracer_provider=None): - """Integrate with PostgreSQL Psycopg library. - Psycopg: http://initd.org/psycopg/ - """ - - tracer = get_tracer(__name__, __version__, tracer_provider) - - connection_attributes = { +class Psycopg2Instrumentor(BaseInstrumentor): + _CONNECTION_ATTRIBUTES = { "database": "info.dbname", "port": "info.port", "host": "info.host", "user": "info.user", } - db_integration = DatabaseApiIntegration( - tracer, - DATABASE_COMPONENT, - database_type=DATABASE_TYPE, - connection_attributes=connection_attributes, - ) - - # pylint: disable=unused-argument - def wrap_connect( - connect_func: typing.Callable[..., any], - instance: typing.Any, - args: typing.Tuple[any, any], - kwargs: typing.Dict[any, any], - ): - connection = connect_func(*args, **kwargs) - db_integration.get_connection_attributes(connection) - connection.cursor_factory = PsycopgTraceCursor - return connection - - try: - wrapt.wrap_function_wrapper(psycopg2, "connect", wrap_connect) - except Exception as ex: # pylint: disable=broad-except - logger.warning("Failed to integrate with pyscopg2. %s", str(ex)) - - class PsycopgTraceCursor(psycopg2.extensions.cursor): - def __init__(self, *args, **kwargs): - self._traced_cursor = TracedCursor(db_integration) - super(PsycopgTraceCursor, self).__init__(*args, **kwargs) - - # pylint: disable=redefined-builtin - def execute(self, query, vars=None): - if isinstance(query, Composable): - query = query.as_string(self) - return self._traced_cursor.traced_execution( - super(PsycopgTraceCursor, self).execute, query, vars - ) - - # pylint: disable=redefined-builtin - def executemany(self, query, vars): - if isinstance(query, Composable): - query = query.as_string(self) - return self._traced_cursor.traced_execution( - super(PsycopgTraceCursor, self).executemany, query, vars - ) - - # pylint: disable=redefined-builtin - def callproc(self, procname, vars=None): - return self._traced_cursor.traced_execution( - super(PsycopgTraceCursor, self).callproc, procname, vars - ) + + _DATABASE_COMPONENT = "postgresql" + _DATABASE_TYPE = "sql" + + def _instrument(self, **kwargs): + """Integrate with PostgreSQL Psycopg library. + Psycopg: http://initd.org/psycopg/ + """ + + tracer_provider = kwargs.get("tracer_provider") + + tracer = get_tracer(__name__, __version__, tracer_provider) + + dbapi.wrap_connect( + tracer, + psycopg2, + "connect", + self._DATABASE_COMPONENT, + self._DATABASE_TYPE, + self._CONNECTION_ATTRIBUTES, + ) + + def _uninstrument(self, **kwargs): + """"Disable Psycopg2 instrumentation""" + dbapi.unwrap_connect(psycopg2, "connect") + + # pylint:disable=no-self-use + def instrument_connection(self, connection): + """Enable instrumentation in a Psycopg2 connection. + + Args: + connection: The connection to instrument. + + Returns: + An instrumented connection. + """ + tracer = get_tracer(__name__, __version__) + + return dbapi.instrument_connection( + tracer, + connection, + self._DATABASE_COMPONENT, + self._DATABASE_TYPE, + self._CONNECTION_ATTRIBUTES, + ) + + def uninstrument_connection(self, connection): + """Disable instrumentation in a Psycopg2 connection. + + Args: + connection: The connection to uninstrument. + + Returns: + An uninstrumented connection. + """ + return dbapi.uninstrument_connection(connection) diff --git a/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py b/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py index b4724e308b..f854787bd9 100644 --- a/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py +++ b/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py @@ -12,17 +12,105 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from unittest import mock import psycopg2 -from opentelemetry.ext.psycopg2 import trace_integration +import opentelemetry.ext.psycopg2 +from opentelemetry.ext.psycopg2 import Psycopg2Instrumentor +from opentelemetry.sdk import resources +from opentelemetry.test.test_base import TestBase -class TestPostgresqlIntegration(unittest.TestCase): - def test_trace_integration(self): - with mock.patch("psycopg2.connect"): - trace_integration() - cnx = psycopg2.connect(database="test") - self.assertIsNotNone(cnx.cursor_factory) +class TestPostgresqlIntegration(TestBase): + def tearDown(self): + super().tearDown() + with self.disable_logging(): + Psycopg2Instrumentor().uninstrument() + + @mock.patch("psycopg2.connect") + # pylint: disable=unused-argument + def test_instrumentor(self, mock_connect): + Psycopg2Instrumentor().instrument() + + cnx = psycopg2.connect(database="test") + + cursor = cnx.cursor() + + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check version and name in span's instrumentation info + self.check_span_instrumentation_info(span, opentelemetry.ext.psycopg2) + + # check that no spans are generated after uninstrument + Psycopg2Instrumentor().uninstrument() + + cnx = psycopg2.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + @mock.patch("psycopg2.connect") + # pylint: disable=unused-argument + def test_custom_tracer_provider(self, mock_connect): + resource = resources.Resource.create({}) + result = self.create_tracer_provider(resource=resource) + tracer_provider, exporter = result + + Psycopg2Instrumentor().instrument(tracer_provider=tracer_provider) + + cnx = psycopg2.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertIs(span.resource, resource) + + @mock.patch("psycopg2.connect") + # pylint: disable=unused-argument + def test_instrument_connection(self, mock_connect): + cnx = psycopg2.connect(database="test") + query = "SELECT * FROM test" + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) + + cnx = Psycopg2Instrumentor().instrument_connection(cnx) + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + @mock.patch("psycopg2.connect") + # pylint: disable=unused-argument + def test_uninstrument_connection(self, mock_connect): + Psycopg2Instrumentor().instrument() + cnx = psycopg2.connect(database="test") + query = "SELECT * FROM test" + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + cnx = Psycopg2Instrumentor().uninstrument_connection(cnx) + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) diff --git a/tox.ini b/tox.ini index 04ea2b1650..4494487407 100644 --- a/tox.ini +++ b/tox.ini @@ -207,8 +207,9 @@ commands_pre = pymongo: pip install {toxinidir}/opentelemetry-auto-instrumentation pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo[test] + psycopg2: pip install {toxinidir}/opentelemetry-auto-instrumentation psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi - psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-psycopg2 + psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-psycopg2[test] pymysql: pip install {toxinidir}/opentelemetry-auto-instrumentation pymysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi From 224974879c286062d9a5e494fa7d1469832eabbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Thu, 21 May 2020 14:11:59 -0500 Subject: [PATCH 0351/1517] docs: Consolidate getting started guide and remove duplicated examples (#658) There are some examples that are duplicated in the getting started guide and in the examples folder itself. This commit removes the duplicated examples and updates the getting started guide to include then from real source files that are passed through the linter and have tests. Co-authored-by: Diego Hurtado Co-authored-by: alrex --- docs/examples/auto-instrumentation/README.md | 165 --------- docs/examples/basic_tracer/README.rst | 39 --- docs/examples/basic_tracer/__init__.py | 14 - docs/examples/http/README.rst | 65 ---- docs/examples/http/__init__.py | 13 - docs/examples/http/requirements.txt | 1 - docs/examples/http/server.py | 59 ---- docs/examples/http/tests/__init__.py | 13 - docs/examples/jaeger_exporter/README.rst | 46 --- .../jaeger_exporter/jaeger_exporter.py | 46 --- docs/examples/otcollector-metrics/README.rst | 51 --- .../docker/collector-config.yaml | 18 - .../docker/docker-compose.yaml | 19 -- .../docker/prometheus.yaml | 5 - docs/examples/otcollector-tracer/README.rst | 50 --- .../docker/collector-config.yaml | 19 -- .../docker/docker-compose.yaml | 20 -- docs/examples/prometheus/README.rst | 34 -- docs/getting-started.rst | 323 ++++-------------- .../flask_example.py} | 35 +- .../jaeger_example.py} | 22 +- .../metrics_example.py} | 26 +- docs/getting_started/otcollector_example.py | 74 ++++ .../prometheus_example.py} | 26 +- .../tests/__init__.py | 0 .../tests/test_flask.py} | 34 +- .../tests/test_tracing.py} | 2 +- .../tracing_example.py} | 20 +- docs/metrics_example.py | 23 -- docs/trace_example.py | 18 - tox.ini | 21 +- 31 files changed, 239 insertions(+), 1062 deletions(-) delete mode 100644 docs/examples/auto-instrumentation/README.md delete mode 100644 docs/examples/basic_tracer/README.rst delete mode 100644 docs/examples/basic_tracer/__init__.py delete mode 100644 docs/examples/http/README.rst delete mode 100644 docs/examples/http/__init__.py delete mode 100644 docs/examples/http/requirements.txt delete mode 100755 docs/examples/http/server.py delete mode 100644 docs/examples/http/tests/__init__.py delete mode 100644 docs/examples/jaeger_exporter/README.rst delete mode 100644 docs/examples/jaeger_exporter/jaeger_exporter.py delete mode 100644 docs/examples/otcollector-metrics/README.rst delete mode 100644 docs/examples/otcollector-metrics/docker/collector-config.yaml delete mode 100644 docs/examples/otcollector-metrics/docker/docker-compose.yaml delete mode 100644 docs/examples/otcollector-metrics/docker/prometheus.yaml delete mode 100644 docs/examples/otcollector-tracer/README.rst delete mode 100644 docs/examples/otcollector-tracer/docker/collector-config.yaml delete mode 100644 docs/examples/otcollector-tracer/docker/docker-compose.yaml delete mode 100644 docs/examples/prometheus/README.rst rename docs/{examples/http/client.py => getting_started/flask_example.py} (55%) mode change 100755 => 100644 rename docs/{examples/otcollector-tracer/collector.py => getting_started/jaeger_example.py} (75%) rename docs/{examples/otcollector-metrics/collector.py => getting_started/metrics_example.py} (73%) create mode 100644 docs/getting_started/otcollector_example.py rename docs/{examples/prometheus/prometheus.py => getting_started/prometheus_example.py} (74%) rename docs/{examples/basic_tracer => getting_started}/tests/__init__.py (100%) rename docs/{examples/http/tests/test_http.py => getting_started/tests/test_flask.py} (53%) rename docs/{examples/basic_tracer/tests/test_tracer.py => getting_started/tests/test_tracing.py} (94%) rename docs/{examples/basic_tracer/tracer.py => getting_started/tracing_example.py} (63%) mode change 100755 => 100644 delete mode 100644 docs/metrics_example.py delete mode 100644 docs/trace_example.py diff --git a/docs/examples/auto-instrumentation/README.md b/docs/examples/auto-instrumentation/README.md deleted file mode 100644 index 7ed40f6b96..0000000000 --- a/docs/examples/auto-instrumentation/README.md +++ /dev/null @@ -1,165 +0,0 @@ -# Overview - -This example shows how to use auto-instrumentation in OpenTelemetry. This example is also based on a previous example -for OpenTracing that can be found [here](https://github.com/yurishkuro/opentracing-tutorial/tree/master/python). - -This example uses 2 scripts whose main difference is they being instrumented manually or not: - -1. `server_instrumented.py` which has been instrumented manually -2. `server_uninstrumented.py` which has not been instrumented manually - -The former will be run without the automatic instrumentation agent and the latter with the automatic instrumentation -agent. They should produce the same result, showing that the automatic instrumentation agent does the equivalent -of what manual instrumentation does. - -In order to understand this better, here is the relevant part of both scripts: - -## Manually instrumented server - -`server_instrumented.py` - -```python -@app.route("/server_request") -def server_request(): - with tracer.start_as_current_span( - "server_request", - parent=propagators.extract( - lambda dict_, key: dict_.get(key, []), request.headers - )["current-span"], - ): - print(request.args.get("param")) - return "served" -``` - -## Publisher not instrumented manually - -`server_uninstrumented.py` - -```python -@app.route("/server_request") -def server_request(): - print(request.args.get("param")) - return "served" -``` - -# Preparation - -This example will be executed in a separate virtual environment: - -```sh -$ mkdir auto_instrumentation -$ virtualenv auto_instrumentation -$ source auto_instrumentation/bin/activate -``` - -# Installation - -```sh -$ pip install opentelemetry-sdk -$ pip install opentelemetry-auto-instrumentation -$ pip install opentelemetry-ext-flask -$ pip install requests -``` - -# Execution - -## Execution of the manually instrumented server - -This is done in 2 separate consoles, one to run each of the scripts that make up this example: - -```sh -$ source auto_instrumentation/bin/activate -$ python opentelemetry-python/docs/examples/auto-instrumentation/server_instrumented.py -``` - -```sh -$ source auto_instrumentation/bin/activate -$ python opentelemetry-python/docs/examples/auto-instrumentation/client.py testing -``` - -The execution of `server_instrumented.py` should return an output similar to: - -```sh -{ - "name": "server_request", - "context": { - "trace_id": "0xfa002aad260b5f7110db674a9ddfcd23", - "span_id": "0x8b8bbaf3ca9c5131", - "trace_state": "{}" - }, - "kind": "SpanKind.SERVER", - "parent_id": null, - "start_time": "2020-04-30T17:28:57.886397Z", - "end_time": "2020-04-30T17:28:57.886490Z", - "status": { - "canonical_code": "OK" - }, - "attributes": { - "component": "http", - "http.method": "GET", - "http.server_name": "127.0.0.1", - "http.scheme": "http", - "host.port": 8082, - "http.host": "localhost:8082", - "http.target": "/server_request?param=testing", - "net.peer.ip": "127.0.0.1", - "net.peer.port": 52872, - "http.flavor": "1.1" - }, - "events": [], - "links": [] -} -``` - -## Execution of an automatically instrumented server - -Now, kill the execution of `server_instrumented.py` with `ctrl + c` and run this instead: - -```sh -$ opentelemetry-auto-instrumentation python docs/examples/auto-instrumentation/server_uninstrumented.py -``` - -In the console where you previously executed `client.py`, run again this again: - -```sh -$ python opentelemetry-python/docs/examples/auto-instrumentation/client.py testing -``` - -The execution of `server_uninstrumented.py` should return an output similar to: - -```sh -{ - "name": "server_request", - "context": { - "trace_id": "0x9f528e0b76189f539d9c21b1a7a2fc24", - "span_id": "0xd79760685cd4c269", - "trace_state": "{}" - }, - "kind": "SpanKind.SERVER", - "parent_id": "0xb4fb7eee22ef78e4", - "start_time": "2020-04-30T17:10:02.400604Z", - "end_time": "2020-04-30T17:10:02.401858Z", - "status": { - "canonical_code": "OK" - }, - "attributes": { - "component": "http", - "http.method": "GET", - "http.server_name": "127.0.0.1", - "http.scheme": "http", - "host.port": 8082, - "http.host": "localhost:8082", - "http.target": "/server_request?param=testing", - "net.peer.ip": "127.0.0.1", - "net.peer.port": 48240, - "http.flavor": "1.1", - "http.route": "/server_request", - "http.status_text": "OK", - "http.status_code": 200 - }, - "events": [], - "links": [] -} -``` - -Both outputs are equivalent since the automatic instrumentation does what the manual instrumentation does too. diff --git a/docs/examples/basic_tracer/README.rst b/docs/examples/basic_tracer/README.rst deleted file mode 100644 index 47b16074c5..0000000000 --- a/docs/examples/basic_tracer/README.rst +++ /dev/null @@ -1,39 +0,0 @@ -Basic Tracer -============ - -This example shows how to use OpenTelemetry to instrument a Python application - e.g. a batch job. - -The source files of this example are available :scm_web:`here `. - -Installation ------------- - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - -Run the Example ---------------- - -.. code-block:: sh - - python tracer.py - -The output will be displayed in the console: - -:: - - Hello world from OpenTelemetry Python! - Span(name="baz", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x5611c1407e06e4d7, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="bar", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1b9db0e0cc1a3f60, trace_state={})), start_time=2019-11-07T21:26:45.934412Z, end_time=2019-11-07T21:26:45.934567Z) - Span(name="bar", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1b9db0e0cc1a3f60, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="foo", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1d5d87441ec2f410, trace_state={})), start_time=2019-11-07T21:26:45.934396Z, end_time=2019-11-07T21:26:45.934576Z) - Span(name="foo", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1d5d87441ec2f410, trace_state={}), kind=SpanKind.INTERNAL, parent=None, start_time=2019-11-07T21:26:45.934369Z, end_time=2019-11-07T21:26:45.934580Z) - - -Useful links ------------- - -- OpenTelemetry_ -- :doc:`../../api/trace` - -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ \ No newline at end of file diff --git a/docs/examples/basic_tracer/__init__.py b/docs/examples/basic_tracer/__init__.py deleted file mode 100644 index c9670f8041..0000000000 --- a/docs/examples/basic_tracer/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# pylint: disable=C0103 -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/docs/examples/http/README.rst b/docs/examples/http/README.rst deleted file mode 100644 index edbb5044f6..0000000000 --- a/docs/examples/http/README.rst +++ /dev/null @@ -1,65 +0,0 @@ -HTTP Integration Example -======================== - -This example shows how to use -:doc:`WSGI Middleware <../../ext/wsgi/wsgi>` -and :doc:`requests <../../ext/requests/requests>` integrations to -instrument an HTTP client and server in Python. - -The source files required to run this example are available :scm_web:`here `. - -Installation ------------- - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - pip install opentelemetry-ext-wsgi - pip install opentelemetry-ext-requests - pip install flask - - -Run the Example ---------------- - -* Run the server - -.. code-block:: sh - - python server.py - - -* Run the client from a different terminal - -.. code-block:: sh - - python client.py - - -The output will be displayed in the console on the client side: - -:: - - Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x3703fd889dcdeb2b, trace_state={}), kind=SpanKind.CLIENT, parent=None, start_time=2019-11-07T21:52:59.591634Z, end_time=2019-11-07T21:53:00.386014Z) - - -And on the server: - -:: - - 127.0.0.1 - - [07/Nov/2019 13:53:00] "GET / HTTP/1.1" 200 - - Span(name="/wiki/Rabbit", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x4bf0be462b91d6ef, trace_state={}), kind=SpanKind.CLIENT, parent=Span(name="parent", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x68338643ccb2d53b, trace_state={})), start_time=2019-11-07T21:52:59.601597Z, end_time=2019-11-07T21:53:00.380491Z) - Span(name="parent", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x68338643ccb2d53b, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x36050ac596949bc1, trace_state={})), start_time=2019-11-07T21:52:59.601233Z, end_time=2019-11-07T21:53:00.384485Z) - Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x36050ac596949bc1, trace_state={}), kind=SpanKind.SERVER, parent=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x3703fd889dcdeb2b, trace_state={}), start_time=2019-11-07T21:52:59.600816Z, end_time=2019-11-07T21:53:00.385322Z) - - -Useful links ------------- - -- OpenTelemetry_ -- :doc:`../../api/trace` -- :doc:`../../ext/wsgi/wsgi` -- :doc:`../../ext/requests/requests` - -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ \ No newline at end of file diff --git a/docs/examples/http/__init__.py b/docs/examples/http/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/docs/examples/http/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/docs/examples/http/requirements.txt b/docs/examples/http/requirements.txt deleted file mode 100644 index 7e1060246f..0000000000 --- a/docs/examples/http/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -flask diff --git a/docs/examples/http/server.py b/docs/examples/http/server.py deleted file mode 100755 index 10af2eb951..0000000000 --- a/docs/examples/http/server.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import flask -import requests - -from opentelemetry import trace -from opentelemetry.ext.requests import RequestsInstrumentor -from opentelemetry.ext.wsgi import OpenTelemetryMiddleware -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - BatchExportSpanProcessor, - ConsoleSpanExporter, -) - -# The preferred tracer implementation must be set, as the opentelemetry-api -# defines the interface with a no-op implementation. -# It must be done before instrumenting any library. -trace.set_tracer_provider(TracerProvider()) - -# Integrations are the glue that binds the OpenTelemetry API and the -# frameworks and libraries that are used together, automatically creating -# Spans and propagating context as appropriate. -RequestsInstrumentor().instrument() -app = flask.Flask(__name__) -app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) - -# Configure a console span exporter. -exporter = ConsoleSpanExporter() -span_processor = BatchExportSpanProcessor(exporter) -trace.get_tracer_provider().add_span_processor(span_processor) - -tracer = trace.get_tracer(__name__) - - -@app.route("/") -def hello(): - with tracer.start_as_current_span("parent"): - requests.get("https://www.wikipedia.org/wiki/Rabbit") - return "hello" - - -if __name__ == "__main__": - app.run(debug=True) diff --git a/docs/examples/http/tests/__init__.py b/docs/examples/http/tests/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/docs/examples/http/tests/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/docs/examples/jaeger_exporter/README.rst b/docs/examples/jaeger_exporter/README.rst deleted file mode 100644 index d4e20bad3f..0000000000 --- a/docs/examples/jaeger_exporter/README.rst +++ /dev/null @@ -1,46 +0,0 @@ -Jaeger Exporter Example -======================= - -This example shows how to use OpenTelemetry to send tracing data to Jaeger. - -The source files of this example are available :scm_web:`here `. - -Installation ------------- - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - pip install opentelemetry-ext-jaeger - -Run the Example ---------------- - -* Start Jaeger - -.. code-block:: sh - - docker run --rm \ - -p 6831:6831/udp \ - -p 6832:6832/udp \ - -p 16686:16686 \ - jaegertracing/all-in-one:1.13 \ - --log-level=debug - -* Run the example - -.. code-block:: sh - - python jaeger_exporter.py - -The traces will be available at http://localhost:16686/. - -Useful links ------------- - -- OpenTelemetry_ -- :doc:`../../api/trace` -- :doc:`../../ext/jaeger/jaeger` - -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ \ No newline at end of file diff --git a/docs/examples/jaeger_exporter/jaeger_exporter.py b/docs/examples/jaeger_exporter/jaeger_exporter.py deleted file mode 100644 index 1b0bf5acb4..0000000000 --- a/docs/examples/jaeger_exporter/jaeger_exporter.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from opentelemetry import trace -from opentelemetry.ext.jaeger import JaegerSpanExporter -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - -trace.set_tracer_provider(TracerProvider()) -tracer = trace.get_tracer(__name__) - -exporter = JaegerSpanExporter( - service_name="my-helloworld-service", - # configure agent - agent_host_name="localhost", - agent_port=6831, - # optional: configure also collector - # collector_host_name="localhost", - # collector_port=14268, - # collector_endpoint="/api/traces?format=jaeger.thrift", - # username=xxxx, # optional - # password=xxxx, # optional -) - -span_processor = BatchExportSpanProcessor(exporter) -trace.get_tracer_provider().add_span_processor(span_processor) - -with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("baz"): - print("Hello world from OpenTelemetry Python!") diff --git a/docs/examples/otcollector-metrics/README.rst b/docs/examples/otcollector-metrics/README.rst deleted file mode 100644 index c52127cbd2..0000000000 --- a/docs/examples/otcollector-metrics/README.rst +++ /dev/null @@ -1,51 +0,0 @@ -OT Collector Metrics Exporter Example -===================================== - -This example shows how to export metrics to the OT collector. - -The source files of this example are available :scm_web:`here `. - -Installation ------------- - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - pip install opentelemetry-ext-otcollector - -Run the Example ---------------- - -Before running the example, it's necessary to run the OpenTelemetry collector -and Prometheus. The :scm_web:`docker ` -folder contains the a docker-compose template with the configuration of those -services. - -.. code-block:: sh - - pip install docker-compose - cd docker - docker-compose up - - -Now, the example can be executed: - -.. code-block:: sh - - python collector.py - - -The metrics are available in the Prometheus dashboard at http://localhost:9090/graph, -look for the "requests" metric on the list. - -Useful links ------------- - -- OpenTelemetry_ -- OTCollector_ -- :doc:`../../api/metrics` -- :doc:`../../ext/otcollector/otcollector` - -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ -.. _OTCollector: https://github.com/open-telemetry/opentelemetry-collector \ No newline at end of file diff --git a/docs/examples/otcollector-metrics/docker/collector-config.yaml b/docs/examples/otcollector-metrics/docker/collector-config.yaml deleted file mode 100644 index 78f685d208..0000000000 --- a/docs/examples/otcollector-metrics/docker/collector-config.yaml +++ /dev/null @@ -1,18 +0,0 @@ -receivers: - opencensus: - endpoint: "0.0.0.0:55678" - -exporters: - prometheus: - endpoint: "0.0.0.0:8889" - logging: {} - -processors: - batch: - queued_retry: - -service: - pipelines: - metrics: - receivers: [opencensus] - exporters: [logging, prometheus] diff --git a/docs/examples/otcollector-metrics/docker/docker-compose.yaml b/docs/examples/otcollector-metrics/docker/docker-compose.yaml deleted file mode 100644 index d12a0a4f18..0000000000 --- a/docs/examples/otcollector-metrics/docker/docker-compose.yaml +++ /dev/null @@ -1,19 +0,0 @@ -version: "2" -services: - - otel-collector: - image: omnition/opentelemetry-collector-contrib:latest - command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"] - volumes: - - ./collector-config.yaml:/conf/collector-config.yaml - ports: - - "8889:8889" # Prometheus exporter metrics - - "55678:55678" # OpenCensus receiver - - prometheus: - container_name: prometheus - image: prom/prometheus:latest - volumes: - - ./prometheus.yaml:/etc/prometheus/prometheus.yml - ports: - - "9090:9090" diff --git a/docs/examples/otcollector-metrics/docker/prometheus.yaml b/docs/examples/otcollector-metrics/docker/prometheus.yaml deleted file mode 100644 index 4eb2357231..0000000000 --- a/docs/examples/otcollector-metrics/docker/prometheus.yaml +++ /dev/null @@ -1,5 +0,0 @@ -scrape_configs: - - job_name: 'otel-collector' - scrape_interval: 5s - static_configs: - - targets: ['otel-collector:8889'] diff --git a/docs/examples/otcollector-tracer/README.rst b/docs/examples/otcollector-tracer/README.rst deleted file mode 100644 index d82803df40..0000000000 --- a/docs/examples/otcollector-tracer/README.rst +++ /dev/null @@ -1,50 +0,0 @@ -OT Collector Tracer Exporter Example -==================================== - -This example shows how to export traces to the OT collector. - -The source files of this example are available :scm_web:`here `. - -Installation ------------- - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - pip install opentelemetry-ext-otcollector - -Run the Example ---------------- - -Before running the example, it's necessary to run the OpenTelemetry collector -and Jaeger. The :scm_web:`docker ` -folder contains the a docker-compose template with the configuration of those -services. - -.. code-block:: sh - - pip install docker-compose - cd docker - docker-compose up - - -Now, the example can be executed: - -.. code-block:: sh - - python collector.py - - -The traces are available in the Jaeger UI at http://localhost:16686/. - -Useful links ------------- - -- OpenTelemetry_ -- OTCollector_ -- :doc:`../../api/trace` -- :doc:`../../ext/otcollector/otcollector` - -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ -.. _OTCollector: https://github.com/open-telemetry/opentelemetry-collector \ No newline at end of file diff --git a/docs/examples/otcollector-tracer/docker/collector-config.yaml b/docs/examples/otcollector-tracer/docker/collector-config.yaml deleted file mode 100644 index bcf59c5802..0000000000 --- a/docs/examples/otcollector-tracer/docker/collector-config.yaml +++ /dev/null @@ -1,19 +0,0 @@ -receivers: - opencensus: - endpoint: "0.0.0.0:55678" - -exporters: - jaeger_grpc: - endpoint: jaeger-all-in-one:14250 - logging: {} - -processors: - batch: - queued_retry: - -service: - pipelines: - traces: - receivers: [opencensus] - exporters: [jaeger_grpc, logging] - processors: [batch, queued_retry] diff --git a/docs/examples/otcollector-tracer/docker/docker-compose.yaml b/docs/examples/otcollector-tracer/docker/docker-compose.yaml deleted file mode 100644 index 71d7ccd5a1..0000000000 --- a/docs/examples/otcollector-tracer/docker/docker-compose.yaml +++ /dev/null @@ -1,20 +0,0 @@ -version: "2" -services: - - # Collector - collector: - image: omnition/opentelemetry-collector-contrib:latest - command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"] - volumes: - - ./collector-config.yaml:/conf/collector-config.yaml - ports: - - "55678:55678" - - jaeger-all-in-one: - image: jaegertracing/all-in-one:latest - ports: - - "16686:16686" - - "6831:6831/udp" - - "6832:6832/udp" - - "14268" - - "14250" diff --git a/docs/examples/prometheus/README.rst b/docs/examples/prometheus/README.rst deleted file mode 100644 index 963687fdd9..0000000000 --- a/docs/examples/prometheus/README.rst +++ /dev/null @@ -1,34 +0,0 @@ -Prometheus Metrics Exporter Example -=================================== - -This example shows how to export metrics to Prometheus. - -The source files of this example are available :scm_web:`here `. - -Installation ------------- - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - pip install opentelemetry-ext-prometheus - -Run the Example ---------------- - -.. code-block:: sh - - python prometheus.py - - -The metrics are available at http://localhost:8000/. - -Useful links ------------- - -- OpenTelemetry_ -- :doc:`../../api/metrics` -- :doc:`OpenTelemetry Prometheus Exporter <../../ext/prometheus/prometheus>` - -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 5d20fbe2c0..c4ed47ad33 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -28,69 +28,69 @@ how long the action took, or add arbitrary attributes to the span that may provi Here's an example of a script that emits a trace containing three named spans: "foo", "bar", and "baz": -.. code-block:: python - - # tracing.py - from opentelemetry import trace - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import ConsoleSpanExporter - from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor - - trace.set_tracer_provider(TracerProvider()) - trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) - ) - tracer = trace.get_tracer(__name__) - with tracer.start_as_current_span('foo'): - with tracer.start_as_current_span('bar'): - with tracer.start_as_current_span('baz'): - print("Hello world from OpenTelemetry Python!") +.. literalinclude:: getting_started/tracing_example.py + :language: python + :lines: 15- We can run it, and see the traces print to your console: .. code-block:: sh - $ python tracing.py - Hello world from OpenTelemetry Python! - Span(name="baz", - context=SpanContext(trace_id=0x37c1b154d9ab5a4b94b0046484b90400, - span_id=0xfacbb82a4d0cf5dd, - trace_state={} - ), - kind=SpanKind.INTERNAL, - parent=Span(name="bar", - context=SpanContext(trace_id=0x37c1b154d9ab5a4b94b0046484b90400, - span_id=0xb1894e8d588f5f62, - trace_state={}) - ), - start_time=2020-03-15T05:12:08.345394Z, - end_time=2020-03-15T05:12:08.345450Z - ) - Span(name="bar", - context=SpanContext(trace_id=0x37c1b154d9ab5a4b94b0046484b90400, - span_id=0xb1894e8d588f5f62, - trace_state={} - ), - kind=SpanKind.INTERNAL, - parent=Span(name="foo", - context=SpanContext(trace_id=0x37c1b154d9ab5a4b94b0046484b90400, - span_id=0xde5ea23d6a9e4180, - trace_state={}) - ), - start_time=2020-03-15T05:12:08.345360Z, - end_time=2020-03-15T05:12:08.345597Z - ) - Span(name="foo", - context=SpanContext(trace_id=0x37c1b154d9ab5a4b94b0046484b90400, - span_id=0xde5ea23d6a9e4180, - trace_state={} - ), - kind=SpanKind.INTERNAL, - parent=None, - start_time=2020-03-15T05:12:08.345287Z, - end_time=2020-03-15T05:12:08.345673Z - ) - + $ python tracing_example.py + { + "name": "baz", + "context": { + "trace_id": "0xb51058883c02f880111c959f3aa786a2", + "span_id": "0xb2fa4c39f5f35e13", + "trace_state": "{}" + }, + "kind": "SpanKind.INTERNAL", + "parent_id": "0x77e577e6a8813bf4", + "start_time": "2020-05-07T14:39:52.906272Z", + "end_time": "2020-05-07T14:39:52.906343Z", + "status": { + "canonical_code": "OK" + }, + "attributes": {}, + "events": [], + "links": [] + } + { + "name": "bar", + "context": { + "trace_id": "0xb51058883c02f880111c959f3aa786a2", + "span_id": "0x77e577e6a8813bf4", + "trace_state": "{}" + }, + "kind": "SpanKind.INTERNAL", + "parent_id": "0x3791d950cc5140c5", + "start_time": "2020-05-07T14:39:52.906230Z", + "end_time": "2020-05-07T14:39:52.906601Z", + "status": { + "canonical_code": "OK" + }, + "attributes": {}, + "events": [], + "links": [] + } + { + "name": "foo", + "context": { + "trace_id": "0xb51058883c02f880111c959f3aa786a2", + "span_id": "0x3791d950cc5140c5", + "trace_state": "{}" + }, + "kind": "SpanKind.INTERNAL", + "parent_id": null, + "start_time": "2020-05-07T14:39:52.906157Z", + "end_time": "2020-05-07T14:39:52.906743Z", + "status": { + "canonical_code": "OK" + }, + "attributes": {}, + "events": [], + "links": [] + } Each span typically represents a single operation or unit of work. Spans can be nested, and have a parent-child relationship with other spans. @@ -126,34 +126,15 @@ for Jaeger, but you can install that as a separate package: Once installed, update your code to import the Jaeger exporter, and use that instead: -.. code-block:: python - - # jaeger-example.py - from opentelemetry import trace - from opentelemetry.ext import jaeger - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor - - trace.set_tracer_provider(TracerProvider()) - - jaeger_exporter = jaeger.JaegerSpanExporter( - service_name="my-helloworld-service", agent_host_name="localhost", agent_port=6831 - ) - - trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(jaeger_exporter) - ) - tracer = trace.get_tracer(__name__) - with tracer.start_as_current_span('foo'): - with tracer.start_as_current_span('bar'): - with tracer.start_as_current_span('baz'): - print("Hello world from OpenTelemetry Python!") +.. literalinclude:: getting_started/jaeger_example.py + :language: python + :lines: 15- Run the script: .. code-block:: python - python jaeger-example.py + python jaeger_example.py You can then visit the jaeger UI, see you service under "services", and find your traces! @@ -181,36 +162,9 @@ We will now instrument a basic Flask application that uses the requests library And let's write a small Flask application that sends an HTTP request, activating each instrumentation during the initialization: -.. code-block:: python - - # flask_example.py - import flask - import requests - - import opentelemetry.ext.requests - from opentelemetry import trace - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import ConsoleSpanExporter - from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor - from opentelemetry.ext.flask import FlaskInstrumentor - - trace.set_tracer_provider(TracerProvider()) - trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) - ) - - app = flask.Flask(__name__) - FlaskInstrumentor().instrument_app(app) - opentelemetry.ext.http_requests.RequestsInstrumentor().instrument() - - @app.route("/") - def hello(): - tracer = trace.get_tracer(__name__) - with tracer.start_as_current_span("example-request"): - requests.get("http://www.example.com") - return "hello" - - app.run(debug=True, port=5000) +.. literalinclude:: getting_started/flask_example.py + :language: python + :lines: 15- Now run the above script, hit the root url (http://localhost:5000/) a few times, and watch your spans be emitted! @@ -233,45 +187,15 @@ subdivision of the measurements the metric represents. The following is an example of emitting metrics to console, in a similar fashion to the trace example: -.. code-block:: python - - # metrics.py - import sys - import time - - from opentelemetry import metrics - from opentelemetry.sdk.metrics import Counter, MeterProvider - from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter - from opentelemetry.sdk.metrics.export.controller import PushController - - metrics.set_meter_provider(MeterProvider()) - meter = metrics.get_meter(__name__, True) - exporter = ConsoleMetricsExporter() - controller = PushController(meter, exporter, 5) - - staging_labels = {"environment": "staging"} - - requests_counter = meter.create_metric( - name="requests", - description="number of requests", - unit="1", - value_type=int, - metric_type=Counter, - label_keys=("environment",), - ) - - requests_counter.add(25, staging_labels) - time.sleep(5) - - requests_counter.add(20, staging_labels) - time.sleep(5) - +.. literalinclude:: getting_started/metrics_example.py + :language: python + :lines: 15- The sleeps will cause the script to take a while, but running it should yield: .. code-block:: sh - $ python metrics.py + $ python metrics_example.py ConsoleMetricsExporter(data="Counter(name="requests", description="number of requests")", labels="(('environment', 'staging'),)", value=25) ConsoleMetricsExporter(data="Counter(name="requests", description="number of requests")", labels="(('environment', 'staging'),)", value=45) @@ -309,48 +233,9 @@ For our Python application, we will need to install an exporter specific to Prom And use that instead of the `ConsoleMetricsExporter`: -.. code-block:: python - - # prometheus.py - import sys - import time - - from opentelemetry import metrics - from opentelemetry.ext.prometheus import PrometheusMetricsExporter - from opentelemetry.sdk.metrics import Counter, MeterProvider - from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter - from opentelemetry.sdk.metrics.export.controller import PushController - from prometheus_client import start_http_server - - # Start Prometheus client - start_http_server(port=8000, addr="localhost") - - batcher_mode = "stateful" - metrics.set_meter_provider(MeterProvider()) - meter = metrics.get_meter(__name__, batcher_mode == "stateful") - exporter = PrometheusMetricsExporter("MyAppPrefix") - controller = PushController(meter, exporter, 5) - - staging_labels = {"environment": "staging"} - - requests_counter = meter.create_metric( - name="requests", - description="number of requests", - unit="1", - value_type=int, - metric_type=Counter, - label_keys=("environment",), - ) - - requests_counter.add(25, staging_labels) - time.sleep(5) - - requests_counter.add(20, staging_labels) - time.sleep(5) - - # This line is added to keep the HTTP server up long enough to scrape. - input("Press any key to exit...") - +.. literalinclude:: getting_started/prometheus_example.py + :language: python + :lines: 15- The Prometheus server will run locally on port 8000, and the instrumented code will make metrics available to Prometheus via the `PrometheusMetricsExporter`. Visit the Prometheus UI (http://localhost:9090) to view your metrics. @@ -408,64 +293,6 @@ Install the OpenTelemetry Collector exporter: And execute the following script: -.. code-block:: python - - # otcollector.py - import time - from opentelemetry import trace - from opentelemetry.ext.otcollector.trace_exporter import CollectorSpanExporter - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - from opentelemetry import metrics - from opentelemetry.ext.otcollector.metrics_exporter import CollectorMetricsExporter - from opentelemetry.sdk.metrics import Counter, MeterProvider - from opentelemetry.sdk.metrics.export.controller import PushController - - - # create a CollectorSpanExporter - span_exporter = CollectorSpanExporter( - # optional: - # endpoint="myCollectorUrl:55678", - # service_name="test_service", - # host_name="machine/container name", - ) - tracer_provider = TracerProvider() - trace.set_tracer_provider(tracer_provider) - span_processor = BatchExportSpanProcessor(span_exporter) - tracer_provider.add_span_processor(span_processor) - - # create a CollectorMetricsExporter - metric_exporter = CollectorMetricsExporter( - # optional: - # endpoint="myCollectorUrl:55678", - # service_name="test_service", - # host_name="machine/container name", - ) - - # Meter is responsible for creating and recording metrics - metrics.set_meter_provider(MeterProvider()) - meter = metrics.get_meter(__name__) - # controller collects metrics created from meter and exports it via the - # exporter every interval - controller = PushController(meter, metric_exporter, 5) - - # Configure the tracer to use the collector exporter - tracer = trace.get_tracer_provider().get_tracer(__name__) - - with tracer.start_as_current_span("foo"): - print("Hello world!") - - requests_counter = meter.create_metric( - name="requests", - description="number of requests", - unit="1", - value_type=int, - metric_type=Counter, - label_keys=("environment",), - ) - # Labels are used to identify key-values that are associated with a specific - # metric that you want to record. These are useful for pre-aggregation and can - # be used to store custom dimensions pertaining to a metric - labels = {"environment": "staging"} - requests_counter.add(25, labels) - time.sleep(10) # give push_controller time to push metrics +.. literalinclude:: getting_started/otcollector_example.py + :language: python + :lines: 15- diff --git a/docs/examples/http/client.py b/docs/getting_started/flask_example.py old mode 100755 new mode 100644 similarity index 55% rename from docs/examples/http/client.py rename to docs/getting_started/flask_example.py index 56b0ca6ea7..77a50a799b --- a/docs/examples/http/client.py +++ b/docs/getting_started/flask_example.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 -# # Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,32 +12,35 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - +# flask_example.py +import flask import requests from opentelemetry import trace +from opentelemetry.ext.flask import FlaskInstrumentor from opentelemetry.ext.requests import RequestsInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( - BatchExportSpanProcessor, ConsoleSpanExporter, + SimpleExportSpanProcessor, ) -# The preferred tracer implementation must be set, as the opentelemetry-api -# defines the interface with a no-op implementation. -# It must be done before instrumenting any library. trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) -# Enable instrumentation in the requests library. +app = flask.Flask(__name__) +FlaskInstrumentor().instrument_app(app) RequestsInstrumentor().instrument() -# Configure a console span exporter. -exporter = ConsoleSpanExporter() -span_processor = BatchExportSpanProcessor(exporter) -trace.get_tracer_provider().add_span_processor(span_processor) -# Integrations are the glue that binds the OpenTelemetry API and the -# frameworks and libraries that are used together, automatically creating -# Spans and propagating context as appropriate. -response = requests.get(url="http://127.0.0.1:5000/") +@app.route("/") +def hello(): + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span("example-request"): + requests.get("http://www.example.com") + return "hello" + + +app.run(debug=True, port=5000) diff --git a/docs/examples/otcollector-tracer/collector.py b/docs/getting_started/jaeger_example.py similarity index 75% rename from docs/examples/otcollector-tracer/collector.py rename to docs/getting_started/jaeger_example.py index d92fa52f3a..8ddb4a0586 100644 --- a/docs/examples/otcollector-tracer/collector.py +++ b/docs/getting_started/jaeger_example.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 -# # Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,22 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - +# jaeger_example.py from opentelemetry import trace -from opentelemetry.ext.otcollector.trace_exporter import CollectorSpanExporter +from opentelemetry.ext import jaeger from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor -exporter = CollectorSpanExporter( - service_name="basic-service", endpoint="localhost:55678" +trace.set_tracer_provider(TracerProvider()) + +jaeger_exporter = jaeger.JaegerSpanExporter( + service_name="my-helloworld-service", + agent_host_name="localhost", + agent_port=6831, +) + +trace.get_tracer_provider().add_span_processor( + BatchExportSpanProcessor(jaeger_exporter) ) -trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) -span_processor = BatchExportSpanProcessor(exporter) -trace.get_tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): diff --git a/docs/examples/otcollector-metrics/collector.py b/docs/getting_started/metrics_example.py similarity index 73% rename from docs/examples/otcollector-metrics/collector.py rename to docs/getting_started/metrics_example.py index b0420e1bb8..e9e9b1cf7d 100644 --- a/docs/examples/otcollector-metrics/collector.py +++ b/docs/getting_started/metrics_example.py @@ -11,26 +11,22 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# -""" -This example shows how to export metrics to the OT collector. -""" + +# metrics.py +import time from opentelemetry import metrics -from opentelemetry.ext.otcollector.metrics_exporter import ( - CollectorMetricsExporter, -) from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController -exporter = CollectorMetricsExporter( - service_name="basic-service", endpoint="localhost:55678" -) - metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__) +meter = metrics.get_meter(__name__, True) +exporter = ConsoleMetricsExporter() controller = PushController(meter, exporter, 5) +staging_labels = {"environment": "staging"} + requests_counter = meter.create_metric( name="requests", description="number of requests", @@ -40,8 +36,8 @@ label_keys=("environment",), ) -staging_labels = {"environment": "staging"} requests_counter.add(25, staging_labels) +time.sleep(5) -print("Metrics are available now at http://localhost:9090/graph") -input("Press any key to exit...") +requests_counter.add(20, staging_labels) +time.sleep(5) diff --git a/docs/getting_started/otcollector_example.py b/docs/getting_started/otcollector_example.py new file mode 100644 index 0000000000..b1887c3d0c --- /dev/null +++ b/docs/getting_started/otcollector_example.py @@ -0,0 +1,74 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# otcollector.py +import time + +from opentelemetry import metrics, trace +from opentelemetry.ext.otcollector.metrics_exporter import ( + CollectorMetricsExporter, +) +from opentelemetry.ext.otcollector.trace_exporter import CollectorSpanExporter +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export.controller import PushController +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + +# create a CollectorSpanExporter +span_exporter = CollectorSpanExporter( + # optional: + # endpoint="myCollectorUrl:55678", + # service_name="test_service", + # host_name="machine/container name", +) +tracer_provider = TracerProvider() +trace.set_tracer_provider(tracer_provider) +span_processor = BatchExportSpanProcessor(span_exporter) +tracer_provider.add_span_processor(span_processor) + +# create a CollectorMetricsExporter +metric_exporter = CollectorMetricsExporter( + # optional: + # endpoint="myCollectorUrl:55678", + # service_name="test_service", + # host_name="machine/container name", +) + +# Meter is responsible for creating and recording metrics +metrics.set_meter_provider(MeterProvider()) +meter = metrics.get_meter(__name__) +# controller collects metrics created from meter and exports it via the +# exporter every interval +controller = PushController(meter, metric_exporter, 5) + +# Configure the tracer to use the collector exporter +tracer = trace.get_tracer_provider().get_tracer(__name__) + +with tracer.start_as_current_span("foo"): + print("Hello world!") + +requests_counter = meter.create_metric( + name="requests", + description="number of requests", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), +) +# Labels are used to identify key-values that are associated with a specific +# metric that you want to record. These are useful for pre-aggregation and can +# be used to store custom dimensions pertaining to a metric +labels = {"environment": "staging"} +requests_counter.add(25, labels) +time.sleep(10) # give push_controller time to push metrics diff --git a/docs/examples/prometheus/prometheus.py b/docs/getting_started/prometheus_example.py similarity index 74% rename from docs/examples/prometheus/prometheus.py rename to docs/getting_started/prometheus_example.py index cdb60e9cec..ce658d3419 100644 --- a/docs/examples/prometheus/prometheus.py +++ b/docs/getting_started/prometheus_example.py @@ -11,27 +11,30 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# -""" -This example shows how to export metrics to Prometheus. -""" + +# prometheus.py +import sys +import time from prometheus_client import start_http_server from opentelemetry import metrics from opentelemetry.ext.prometheus import PrometheusMetricsExporter -from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController # Start Prometheus client start_http_server(port=8000, addr="localhost") +batcher_mode = "stateful" metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__) - -exporter = PrometheusMetricsExporter(prefix="MyAppPrefix") +meter = metrics.get_meter(__name__, batcher_mode == "stateful") +exporter = PrometheusMetricsExporter("MyAppPrefix") controller = PushController(meter, exporter, 5) +staging_labels = {"environment": "staging"} + requests_counter = meter.create_metric( name="requests", description="number of requests", @@ -41,8 +44,11 @@ label_keys=("environment",), ) -staging_labels = {"environment": "staging"} requests_counter.add(25, staging_labels) +time.sleep(5) + +requests_counter.add(20, staging_labels) +time.sleep(5) -print("Metrics are available now at http://localhost:8000/") +# This line is added to keep the HTTP server up long enough to scrape. input("Press any key to exit...") diff --git a/docs/examples/basic_tracer/tests/__init__.py b/docs/getting_started/tests/__init__.py similarity index 100% rename from docs/examples/basic_tracer/tests/__init__.py rename to docs/getting_started/tests/__init__.py diff --git a/docs/examples/http/tests/test_http.py b/docs/getting_started/tests/test_flask.py similarity index 53% rename from docs/examples/http/tests/test_http.py rename to docs/getting_started/tests/test_flask.py index 6749f9b799..4c0430d0a7 100644 --- a/docs/examples/http/tests/test_http.py +++ b/docs/getting_started/tests/test_flask.py @@ -17,23 +17,27 @@ import unittest from time import sleep +import requests -class TestHttpExample(unittest.TestCase): - @classmethod - def setup_class(cls): + +class TestFlask(unittest.TestCase): + def test_flask(self): dirpath = os.path.dirname(os.path.realpath(__file__)) - server_script = "{}/../server.py".format(dirpath) - cls.server = subprocess.Popen([sys.executable, server_script]) + server_script = "{}/../flask_example.py".format(dirpath) + server = subprocess.Popen( + [sys.executable, server_script], stdout=subprocess.PIPE, + ) sleep(1) - def test_http(self): - dirpath = os.path.dirname(os.path.realpath(__file__)) - test_script = "{}/../client.py".format(dirpath) - output = subprocess.check_output( - (sys.executable, test_script) - ).decode() - self.assertIn('"name": "/"', output) + try: + result = requests.get("http://localhost:5000") + self.assertEqual(result.status_code, 200) + + sleep(0.1) + finally: + server.terminate() - @classmethod - def teardown_class(cls): - cls.server.terminate() + output = str(server.stdout.read()) + self.assertIn('"name": ""', output) + self.assertIn('"name": "example-request"', output) + self.assertIn('"name": "hello"', output) diff --git a/docs/examples/basic_tracer/tests/test_tracer.py b/docs/getting_started/tests/test_tracing.py similarity index 94% rename from docs/examples/basic_tracer/tests/test_tracer.py rename to docs/getting_started/tests/test_tracing.py index 77b25be5f0..54738e7798 100644 --- a/docs/examples/basic_tracer/tests/test_tracer.py +++ b/docs/getting_started/tests/test_tracing.py @@ -20,7 +20,7 @@ class TestBasicTracerExample(unittest.TestCase): def test_basic_tracer(self): dirpath = os.path.dirname(os.path.realpath(__file__)) - test_script = "{}/../tracer.py".format(dirpath) + test_script = "{}/../tracing_example.py".format(dirpath) output = subprocess.check_output( (sys.executable, test_script) ).decode() diff --git a/docs/examples/basic_tracer/tracer.py b/docs/getting_started/tracing_example.py old mode 100755 new mode 100644 similarity index 63% rename from docs/examples/basic_tracer/tracer.py rename to docs/getting_started/tracing_example.py index 20c520b88a..5d449458b4 --- a/docs/examples/basic_tracer/tracer.py +++ b/docs/getting_started/tracing_example.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 -# # Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,29 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - +# tracing.py from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( - BatchExportSpanProcessor, ConsoleSpanExporter, + SimpleExportSpanProcessor, ) -# The preferred tracer implementation must be set, as the opentelemetry-api -# defines the interface with a no-op implementation. trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) -# We tell OpenTelemetry who it is that is creating spans. In this case, we have -# no real name (no setup.py), so we make one up. If we had a version, we would -# also specify it here. tracer = trace.get_tracer(__name__) -# SpanExporter receives the spans and send them to the target location. -exporter = ConsoleSpanExporter() -span_processor = BatchExportSpanProcessor(exporter) -trace.get_tracer_provider().add_span_processor(span_processor) - with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): diff --git a/docs/metrics_example.py b/docs/metrics_example.py deleted file mode 100644 index 6ab4fd55fb..0000000000 --- a/docs/metrics_example.py +++ /dev/null @@ -1,23 +0,0 @@ -from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, MeterProvider -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.controller import PushController - -metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__) -exporter = ConsoleMetricsExporter() -controller = PushController(meter, exporter, 5) - -requests_counter = meter.create_metric( - name="requests", - description="number of requests", - unit="1", - value_type=int, - metric_type=Counter, - label_keys=("environment",), -) - -staging_labels = {"environment": "staging"} -requests_counter.add(25, staging_labels) - -input("Press a key to finish...\n") diff --git a/docs/trace_example.py b/docs/trace_example.py deleted file mode 100644 index 5c87807ec5..0000000000 --- a/docs/trace_example.py +++ /dev/null @@ -1,18 +0,0 @@ -from opentelemetry import trace -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - ConsoleSpanExporter, - SimpleExportSpanProcessor, -) - -trace.set_tracer_provider(TracerProvider()) -trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) -) - -tracer = trace.get_tracer(__name__) - -with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("baz"): - print("Hello world from OpenTelemetry Python!") diff --git a/tox.ini b/tox.ini index 4494487407..3c9b1db957 100644 --- a/tox.ini +++ b/tox.ini @@ -20,13 +20,9 @@ envlist = py3{4,5,6,7,8}-test-example-app pypy3-test-example-app - ; docs/examples/basic_tracer - py3{4,5,6,7,8}-test-example-basic-tracer - pypy3-test-example-basic-tracer - - ; docs/examples/http - py3{4,5,6,7,8}-test-example-http - pypy3-test-example-http + ; docs/getting-started + py3{4,5,6,7,8}-test-getting-started + pypy3-test-getting-started ; opentelemetry-ext-aiohttp-client py3{5,6,7,8}-test-ext-aiohttp-client @@ -156,8 +152,7 @@ changedir = test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests test-ext-flask: ext/opentelemetry-ext-flask/tests test-example-app: docs/examples/opentelemetry-example-app/tests - test-example-basic-tracer: docs/examples/basic_tracer/tests - test-example-http: docs/examples/http/tests + test-getting-started: docs/getting_started/tests test-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests test-ext-sqlalchemy: ext/opentelemetry-ext-sqlalchemy/tests test-ext-redis: ext/opentelemetry-ext-redis/tests @@ -180,10 +175,10 @@ commands_pre = example-app: pip install {toxinidir}/ext/opentelemetry-ext-flask example-app: pip install {toxinidir}/docs/examples/opentelemetry-example-app - example-http: pip install -e {toxinidir}/opentelemetry-auto-instrumentation - example-http: pip install -e {toxinidir}/ext/opentelemetry-ext-requests - example-http: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi - example-http: pip install -r {toxinidir}/docs/examples/http/requirements.txt + getting-started: pip install -e {toxinidir}/opentelemetry-auto-instrumentation + getting-started: pip install -e {toxinidir}/ext/opentelemetry-ext-requests + getting-started: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi + getting-started: pip install -e {toxinidir}/ext/opentelemetry-ext-flask grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc[test] From ae533f7569c10d711edc161e7831e8a61c3ea551 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 21 May 2020 13:15:12 -0700 Subject: [PATCH 0352/1517] infra: accelerate builds (#693) Some build time improvements: - split lint/docker-tests/docs into their own steps. Since lint is usually the thing that fails anyways, it's good to have it run first. We could make the build depend on this step to prevent slowing other builds waiting in the pipeline (since we only have 5 workers) - move all pip install commands into a single line per test environment. this reduces the overhead of calling the pip command separately multiple times per environment. - removed pip upgrade command for pypy3 and py38 --- .travis.yml | 21 +++++++++++++------- tox.ini | 55 ++++++++++++++--------------------------------------- 2 files changed, 28 insertions(+), 48 deletions(-) diff --git a/.travis.yml b/.travis.yml index 999abc7f43..fda383970a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,13 +4,20 @@ language: python cache: pip -python: - - 'pypy3' - - '3.8' - - '3.4' - - '3.5' - - '3.6' - - '3.7' +matrix: + include: + - python: 3.8 + env: TOXENV=lint + - python: pypy3 + - python: 3.8 + - python: 3.4 + - python: 3.5 + - python: 3.6 + - python: 3.7 + - python: 3.8 + env: TOXENV=docker-tests + - python: 3.8 + env: TOXENV=docs #matrix: # allow_failures: diff --git a/tox.ini b/tox.ini index 3c9b1db957..2e92c1bdbd 100644 --- a/tox.ini +++ b/tox.ini @@ -115,10 +115,6 @@ envlist = docs docker-tests -[travis] -python = - 3.8: py38, lint, docs, docker-tests - [testenv] deps = -c dev-requirements.txt @@ -159,26 +155,16 @@ changedir = commands_pre = ; Install without -e to test the actual installation - python -m pip install -U pip setuptools wheel - + py3{4,5,6,7}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. - test: pip install {toxinidir}/opentelemetry-api - test: pip install {toxinidir}/opentelemetry-sdk - test: pip install {toxinidir}/tests/util + test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util test-auto-instrumentation: pip install {toxinidir}/opentelemetry-auto-instrumentation - example-app: pip install {toxinidir}/opentelemetry-auto-instrumentation - example-app: pip install {toxinidir}/ext/opentelemetry-ext-requests - example-app: pip install {toxinidir}/ext/opentelemetry-ext-wsgi - example-app: pip install {toxinidir}/ext/opentelemetry-ext-flask - example-app: pip install {toxinidir}/docs/examples/opentelemetry-example-app + example-app: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-requests {toxinidir}/ext/opentelemetry-ext-wsgi {toxinidir}/ext/opentelemetry-ext-flask {toxinidir}/docs/examples/opentelemetry-example-app - getting-started: pip install -e {toxinidir}/opentelemetry-auto-instrumentation - getting-started: pip install -e {toxinidir}/ext/opentelemetry-ext-requests - getting-started: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi - getting-started: pip install -e {toxinidir}/ext/opentelemetry-ext-flask + getting-started: pip install -e {toxinidir}/opentelemetry-auto-instrumentation -e {toxinidir}/ext/opentelemetry-ext-requests -e {toxinidir}/ext/opentelemetry-ext-wsgi -e {toxinidir}/ext/opentelemetry-ext-flask grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc[test] @@ -191,48 +177,35 @@ commands_pre = django: pip install {toxinidir}/ext/opentelemetry-ext-django[test] - mysql: pip install {toxinidir}/opentelemetry-auto-instrumentation - mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi - mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql[test] + mysql: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-mysql[test] otcollector: pip install {toxinidir}/ext/opentelemetry-ext-otcollector prometheus: pip install {toxinidir}/ext/opentelemetry-ext-prometheus - pymongo: pip install {toxinidir}/opentelemetry-auto-instrumentation - pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo[test] + pymongo: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-pymongo[test] - psycopg2: pip install {toxinidir}/opentelemetry-auto-instrumentation - psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi - psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-psycopg2[test] + psycopg2: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-psycopg2 {toxinidir}/ext/opentelemetry-ext-psycopg2[test] - pymysql: pip install {toxinidir}/opentelemetry-auto-instrumentation - pymysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi - pymysql: pip install {toxinidir}/ext/opentelemetry-ext-pymysql[test] + pymysql: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-pymysql[test] - redis: pip install {toxinidir}/opentelemetry-auto-instrumentation - redis: pip install {toxinidir}/ext/opentelemetry-ext-redis[test] + redis: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-redis[test] - requests: pip install {toxinidir}/opentelemetry-auto-instrumentation - requests: pip install {toxinidir}/ext/opentelemetry-ext-requests[test] + requests: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-requests[test] - jinja2: pip install {toxinidir}/opentelemetry-auto-instrumentation - jinja2: pip install {toxinidir}/ext/opentelemetry-ext-jinja2[test] + jinja2: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-jinja2[test] - aiohttp-client: pip install {toxinidir}/opentelemetry-sdk - aiohttp-client: pip install {toxinidir}/ext/opentelemetry-ext-aiohttp-client + aiohttp-client: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-aiohttp-client jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger - datadog: pip install {toxinidir}/opentelemetry-sdk - datadog: pip install {toxinidir}/ext/opentelemetry-ext-datadog + datadog: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-datadog opentracing-shim: pip install {toxinidir}/ext/opentelemetry-ext-opentracing-shim zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin - sqlalchemy: pip install {toxinidir}/opentelemetry-auto-instrumentation - sqlalchemy: pip install {toxinidir}/ext/opentelemetry-ext-sqlalchemy + sqlalchemy: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-sqlalchemy ; In order to get a healthy coverage report, ; we have to install packages in editable mode. From 58b7bcc207dff81f124c26be6ae3a14bc0607211 Mon Sep 17 00:00:00 2001 From: Connor Adams Date: Thu, 21 May 2020 19:14:25 -0400 Subject: [PATCH 0353/1517] SQLite3 Instrumentation (#719) Adds instrumentation for python sqlite3 library. --- ext/opentelemetry-ext-sqlite3/CHANGELOG.md | 5 + ext/opentelemetry-ext-sqlite3/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-sqlite3/MANIFEST.in | 9 + ext/opentelemetry-ext-sqlite3/README.rst | 21 ++ ext/opentelemetry-ext-sqlite3/setup.cfg | 57 +++++ ext/opentelemetry-ext-sqlite3/setup.py | 26 +++ .../src/opentelemetry/ext/sqlite3/__init__.py | 111 ++++++++++ .../src/opentelemetry/ext/sqlite3/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/test_sqlite3.py | 83 ++++++++ tox.ini | 7 + 11 files changed, 535 insertions(+) create mode 100644 ext/opentelemetry-ext-sqlite3/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-sqlite3/LICENSE create mode 100644 ext/opentelemetry-ext-sqlite3/MANIFEST.in create mode 100644 ext/opentelemetry-ext-sqlite3/README.rst create mode 100644 ext/opentelemetry-ext-sqlite3/setup.cfg create mode 100644 ext/opentelemetry-ext-sqlite3/setup.py create mode 100644 ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py create mode 100644 ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py create mode 100644 ext/opentelemetry-ext-sqlite3/tests/__init__.py create mode 100644 ext/opentelemetry-ext-sqlite3/tests/test_sqlite3.py diff --git a/ext/opentelemetry-ext-sqlite3/CHANGELOG.md b/ext/opentelemetry-ext-sqlite3/CHANGELOG.md new file mode 100644 index 0000000000..33144da913 --- /dev/null +++ b/ext/opentelemetry-ext-sqlite3/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-sqlite3/LICENSE b/ext/opentelemetry-ext-sqlite3/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-sqlite3/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-sqlite3/MANIFEST.in b/ext/opentelemetry-ext-sqlite3/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-sqlite3/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-sqlite3/README.rst b/ext/opentelemetry-ext-sqlite3/README.rst new file mode 100644 index 0000000000..3620434b20 --- /dev/null +++ b/ext/opentelemetry-ext-sqlite3/README.rst @@ -0,0 +1,21 @@ +OpenTelemetry SQLite3 Integration +================================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-sqlite3.svg + :target: https://pypi.org/project/opentelemetry-ext-sqlite3/ + +Installation +------------ + +:: + + pip install opentelemetry-ext-sqlite3 + + +References +---------- +* `OpenTelemetry SQLite3 Integration `_ +* `OpenTelemetry Project `_ + diff --git a/ext/opentelemetry-ext-sqlite3/setup.cfg b/ext/opentelemetry-ext-sqlite3/setup.cfg new file mode 100644 index 0000000000..836d9d17d3 --- /dev/null +++ b/ext/opentelemetry-ext-sqlite3/setup.cfg @@ -0,0 +1,57 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-sqlite3 +description = OpenTelemetry SQLite3 integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-sqlite3 +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.8.dev0 + opentelemetry-ext-dbapi == 0.8.dev0 + opentelemetry-auto-instrumentation == 0.8.dev0 + wrapt >= 1.0.0, < 2.0.0 + +[options.extras_require] +test = + opentelemetry-test == 0.8.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + sqlite3 = opentelemetry.ext.sqlite3:SQLite3Instrumentor diff --git a/ext/opentelemetry-ext-sqlite3/setup.py b/ext/opentelemetry-ext-sqlite3/setup.py new file mode 100644 index 0000000000..dd89f058db --- /dev/null +++ b/ext/opentelemetry-ext-sqlite3/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "sqlite3", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py new file mode 100644 index 0000000000..5c9a009360 --- /dev/null +++ b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py @@ -0,0 +1,111 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +SQLite instrumentation supporting `sqlite3`_, it can be enabled by +using ``SQLite3Instrumentor``. + +.. _sqlite3: https://docs.python.org/3/library/sqlite3.html + +Usage +----- + +.. code:: python + + import sqlite3 + from opentelemetry import trace + from opentelemetry.trace import TracerProvider + from opentelemetry.ext.sqlite3 import SQLite3Instrumentor + + trace.set_tracer_provider(TracerProvider()) + + SQLite3Instrumentor().instrument() + + cnx = sqlite3.connect('example.db') + cursor = cnx.cursor() + cursor.execute("INSERT INTO test (testField) VALUES (123)") + cursor.close() + cnx.close() + +API +--- +""" + +import sqlite3 +import typing + +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.ext import dbapi +from opentelemetry.ext.sqlite3.version import __version__ +from opentelemetry.trace import TracerProvider, get_tracer + + +class SQLite3Instrumentor(BaseInstrumentor): + # No useful attributes of sqlite3 connection object + _CONNECTION_ATTRIBUTES = {} + + _DATABASE_COMPONENT = "sqlite3" + _DATABASE_TYPE = "sql" + + def _instrument(self, **kwargs): + """Integrate with SQLite3 Python library. + https://docs.python.org/3/library/sqlite3.html + """ + tracer_provider = kwargs.get("tracer_provider") + + tracer = get_tracer(__name__, __version__, tracer_provider) + + dbapi.wrap_connect( + tracer, + sqlite3, + "connect", + self._DATABASE_COMPONENT, + self._DATABASE_TYPE, + self._CONNECTION_ATTRIBUTES, + ) + + def _uninstrument(self, **kwargs): + """"Disable SQLite3 instrumentation""" + dbapi.unwrap_connect(sqlite3, "connect") + + # pylint:disable=no-self-use + def instrument_connection(self, connection): + """Enable instrumentation in a SQLite connection. + + Args: + connection: The connection to instrument. + + Returns: + An instrumented connection. + """ + tracer = get_tracer(__name__, __version__) + + return dbapi.instrument_connection( + tracer, + connection, + self._DATABASE_COMPONENT, + self._DATABASE_TYPE, + self._CONNECTION_ATTRIBUTES, + ) + + def uninstrument_connection(self, connection): + """Disable instrumentation in a SQLite connection. + + Args: + connection: The connection to uninstrument. + + Returns: + An uninstrumented connection. + """ + return dbapi.uninstrument_connection(connection) diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py new file mode 100644 index 0000000000..bcf6a35777 --- /dev/null +++ b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-sqlite3/tests/__init__.py b/ext/opentelemetry-ext-sqlite3/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-sqlite3/tests/test_sqlite3.py b/ext/opentelemetry-ext-sqlite3/tests/test_sqlite3.py new file mode 100644 index 0000000000..7f4793bda3 --- /dev/null +++ b/ext/opentelemetry-ext-sqlite3/tests/test_sqlite3.py @@ -0,0 +1,83 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sqlite3 + +from opentelemetry import trace as trace_api +from opentelemetry.ext.sqlite3 import SQLite3Instrumentor +from opentelemetry.test.test_base import TestBase + + +class TestSQLite3(TestBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._connection = None + cls._cursor = None + cls._tracer = cls.tracer_provider.get_tracer(__name__) + SQLite3Instrumentor().instrument(tracer_provider=cls.tracer_provider) + cls._connection = sqlite3.connect(":memory:") + cls._cursor = cls._connection.cursor() + + @classmethod + def tearDownClass(cls): + if cls._cursor: + cls._cursor.close() + if cls._connection: + cls._connection.close() + + def validate_spans(self): + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + for span in spans: + if span.name == "rootSpan": + root_span = span + else: + child_span = span + self.assertIsInstance(span.start_time, int) + self.assertIsInstance(span.end_time, int) + self.assertIsNotNone(root_span) + self.assertIsNotNone(child_span) + self.assertEqual(root_span.name, "rootSpan") + self.assertEqual(child_span.name, "sqlite3") + self.assertIsNotNone(child_span.parent) + self.assertIs(child_span.parent, root_span.get_context()) + self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) + + def test_execute(self): + """Should create a child span for execute method + """ + with self._tracer.start_as_current_span("rootSpan"): + self._cursor.execute( + "CREATE TABLE IF NOT EXISTS test (id integer)" + ) + self.validate_spans() + + def test_executemany(self): + """Should create a child span for executemany + """ + with self._tracer.start_as_current_span("rootSpan"): + data = [("1",), ("2",), ("3",)] + stmt = "INSERT INTO test (id) VALUES (?)" + self._cursor.executemany(stmt, data) + self.validate_spans() + + def test_callproc(self): + """Should create a child span for callproc + """ + with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( + Exception + ): + self._cursor.callproc("test", ()) + self.validate_spans() diff --git a/tox.ini b/tox.ini index 2e92c1bdbd..5742e717d7 100644 --- a/tox.ini +++ b/tox.ini @@ -79,6 +79,10 @@ envlist = py3{4,5,6,7,8}-test-ext-pymysql pypy3-test-ext-pymysql + ; opentelemetry-ext-sqlite3 + py3{4,5,6,7,8}-test-ext-sqlite3 + pypy3-test-ext-sqlite3 + ; opentelemetry-ext-wsgi py3{4,5,6,7,8}-test-ext-wsgi pypy3-test-ext-wsgi @@ -144,6 +148,7 @@ changedir = test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests test-ext-pymysql: ext/opentelemetry-ext-pymysql/tests + test-ext-sqlite3: ext/opentelemetry-ext-sqlite3/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests test-ext-flask: ext/opentelemetry-ext-flask/tests @@ -189,6 +194,8 @@ commands_pre = pymysql: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-pymysql[test] + sqlite3: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-sqlite3[test] + redis: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-redis[test] requests: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-requests[test] From b4e135bbc11a475edb363e9c8dc10bce4d2faf86 Mon Sep 17 00:00:00 2001 From: Daisuke Taniwaki Date: Fri, 22 May 2020 08:53:43 +0900 Subject: [PATCH 0354/1517] sdk: Specify to_json indent from arguments (#718) We want to get a json of span without indent, but it's not possible with Span#to_json because the method uses hard-coded indent. We currently use a workaround of json.loads(span.to_json()) which is not efficient in the performance. Co-authored-by: alrex Co-authored-by: Yusuke Tsutsumi --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 5b74b4a618..4f05168b44 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -360,7 +360,7 @@ def _format_links(links): f_links.append(f_link) return f_links - def to_json(self): + def to_json(self, indent=4): parent_id = None if self.parent is not None: if isinstance(self.parent, Span): @@ -397,7 +397,7 @@ def to_json(self): f_span["events"] = self._format_events(self.events) f_span["links"] = self._format_links(self.links) - return json.dumps(f_span, indent=4) + return json.dumps(f_span, indent=indent) def get_context(self): return self.context From 97b7c22cdae7271d41dfcb4e129287a5ec1cb679 Mon Sep 17 00:00:00 2001 From: Nir Hadassi Date: Fri, 22 May 2020 06:28:44 +0300 Subject: [PATCH 0355/1517] ext/Zipkin - Transform resource to tags when exporting (#707) Implement the missing part of exporting the TraceProvider resource into Zipkin. Same as in js. Resources are now included into span tags. Co-authored-by: alrex Co-authored-by: Yusuke Tsutsumi --- .../src/opentelemetry/ext/zipkin/__init__.py | 17 ++++++--- .../tests/test_zipkin_exporter.py | 36 ++++++++++++++++--- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index 8b4aa93c72..8a487290ce 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -175,7 +175,7 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): "duration": duration_mus, "localEndpoint": local_endpoint, "kind": SPAN_KIND_MAP[span.kind], - "tags": _extract_tags_from_span(span.attributes), + "tags": _extract_tags_from_span(span), "annotations": _extract_annotations_from_events(span.events), } @@ -196,11 +196,11 @@ def shutdown(self) -> None: pass -def _extract_tags_from_span(attr): - if not attr: - return None +def _extract_tags_from_dict(tags_dict): tags = {} - for attribute_key, attribute_value in attr.items(): + if not tags_dict: + return tags + for attribute_key, attribute_value in tags_dict.items(): if isinstance(attribute_value, (int, bool, float)): value = str(attribute_value) elif isinstance(attribute_value, str): @@ -212,6 +212,13 @@ def _extract_tags_from_span(attr): return tags +def _extract_tags_from_span(span: Span): + tags = _extract_tags_from_dict(getattr(span, "attributes", None)) + if span.resource: + tags.update(_extract_tags_from_dict(span.resource.labels)) + return tags + + def _extract_annotations_from_events(events): return ( [ diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py index cd21839e3e..1f2d53d304 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -19,6 +19,7 @@ from opentelemetry import trace as trace_api from opentelemetry.ext.zipkin import ZipkinSpanExporter from opentelemetry.sdk import trace +from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.trace import TraceFlags @@ -95,7 +96,7 @@ def test_constructor_explicit(self): # pylint: disable=too-many-locals def test_export(self): - span_names = ("test1", "test2", "test3") + span_names = ("test1", "test2", "test3", "test4") trace_id = 0x6E0C63257DE34C926F9EFCD03927272E span_id = 0x34BF92DEEFC58C92 parent_id = 0x1111111111111111 @@ -106,12 +107,14 @@ def test_export(self): base_time, base_time + 150 * 10 ** 6, base_time + 300 * 10 ** 6, + base_time + 400 * 10 ** 6, ) - durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6) + durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6, 300 * 10 ** 6) end_times = ( start_times[0] + durations[0], start_times[1] + durations[1], start_times[2] + durations[2], + start_times[3] + durations[3], ) span_context = trace_api.SpanContext( @@ -158,6 +161,7 @@ def test_export(self): name=span_names[1], context=parent_context, parent=None ), trace.Span(name=span_names[2], context=other_context, parent=None), + trace.Span(name=span_names[3], context=other_context, parent=None), ] otel_spans[0].start(start_time=start_times[0]) @@ -168,11 +172,21 @@ def test_export(self): otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) + otel_spans[1].resource = Resource( + labels={"key_resource": "some_resource"} + ) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) + otel_spans[2].set_attribute("key_string", "hello_world") + otel_spans[2].resource = Resource( + labels={"key_resource": "some_resource"} + ) otel_spans[2].end(end_time=end_times[2]) + otel_spans[3].start(start_time=start_times[3]) + otel_spans[3].end(end_time=end_times[3]) + service_name = "test-service" local_endpoint = {"serviceName": service_name, "port": 9411} @@ -208,7 +222,7 @@ def test_export(self): "duration": durations[1] // 10 ** 3, "localEndpoint": local_endpoint, "kind": None, - "tags": None, + "tags": {"key_resource": "some_resource"}, "annotations": None, }, { @@ -219,7 +233,21 @@ def test_export(self): "duration": durations[2] // 10 ** 3, "localEndpoint": local_endpoint, "kind": None, - "tags": None, + "tags": { + "key_string": "hello_world", + "key_resource": "some_resource", + }, + "annotations": None, + }, + { + "traceId": format(trace_id, "x"), + "id": format(other_id, "x"), + "name": span_names[3], + "timestamp": start_times[3] // 10 ** 3, + "duration": durations[3] // 10 ** 3, + "localEndpoint": local_endpoint, + "kind": None, + "tags": {}, "annotations": None, }, ] From f15d76a80e5463b2a7c10177898f3548b257e7e6 Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Fri, 22 May 2020 00:24:39 -0400 Subject: [PATCH 0356/1517] datadog: Add Datadog propagator (#705) This adds DatadogFormatfor extracting and injecting trace contexts with Datadog-specific headers. --- .../opentelemetry/ext/datadog/constants.py | 4 + .../src/opentelemetry/ext/datadog/exporter.py | 14 ++ .../opentelemetry/ext/datadog/propagator.py | 127 +++++++++++++ .../tests/test_datadog_exporter.py | 37 ++++ .../tests/test_datadog_format.py | 173 ++++++++++++++++++ 5 files changed, 355 insertions(+) create mode 100644 ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py create mode 100644 ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py create mode 100644 ext/opentelemetry-ext-datadog/tests/test_datadog_format.py diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py new file mode 100644 index 0000000000..54d7946ab4 --- /dev/null +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py @@ -0,0 +1,4 @@ +DD_ORIGIN = "_dd_origin" +AUTO_REJECT = 0 +AUTO_KEEP = 1 +USER_KEEP = 2 diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py index 4420e69db5..01a0191996 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py @@ -24,6 +24,9 @@ from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace.status import StatusCanonicalCode +# pylint:disable=relative-beyond-top-level +from .constants import DD_ORIGIN + logger = logging.getLogger(__name__) @@ -128,6 +131,11 @@ def _translate_to_datadog(self, spans): datadog_span.set_tags(span.attributes) + # add origin to root span + origin = _get_origin(span) + if origin and parent_id == 0: + datadog_span.set_tag(DD_ORIGIN, origin) + # span events and span links are not supported datadog_spans.append(datadog_span) @@ -202,3 +210,9 @@ def _get_exc_info(span): """Parse span status description for exception type and value""" exc_type, exc_val = span.status.description.split(":", 1) return exc_type, exc_val.strip() + + +def _get_origin(span): + ctx = span.get_context() + origin = ctx.trace_state.get(DD_ORIGIN) + return origin diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py new file mode 100644 index 0000000000..d6595e8d93 --- /dev/null +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py @@ -0,0 +1,127 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing + +from opentelemetry import trace +from opentelemetry.context import Context +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, +) +from opentelemetry.trace.propagation.httptextformat import ( + Getter, + HTTPTextFormat, + HTTPTextFormatT, + Setter, +) + +# pylint:disable=relative-beyond-top-level +from . import constants + + +class DatadogFormat(HTTPTextFormat): + """Propagator for the Datadog HTTP header format. + """ + + TRACE_ID_KEY = "x-datadog-trace-id" + PARENT_ID_KEY = "x-datadog-parent-id" + SAMPLING_PRIORITY_KEY = "x-datadog-sampling-priority" + ORIGIN_KEY = "x-datadog-origin" + + def extract( + self, + get_from_carrier: Getter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + trace_id = extract_first_element( + get_from_carrier(carrier, self.TRACE_ID_KEY) + ) + + span_id = extract_first_element( + get_from_carrier(carrier, self.PARENT_ID_KEY) + ) + + sampled = extract_first_element( + get_from_carrier(carrier, self.SAMPLING_PRIORITY_KEY) + ) + + origin = extract_first_element( + get_from_carrier(carrier, self.ORIGIN_KEY) + ) + + trace_flags = trace.TraceFlags() + if sampled and int(sampled) in ( + constants.AUTO_KEEP, + constants.USER_KEEP, + ): + trace_flags |= trace.TraceFlags.SAMPLED + + if trace_id is None or span_id is None: + return set_span_in_context(trace.INVALID_SPAN, context) + + span_context = trace.SpanContext( + trace_id=int(trace_id), + span_id=int(span_id), + is_remote=True, + trace_flags=trace_flags, + trace_state=trace.TraceState({constants.DD_ORIGIN: origin}), + ) + + return set_span_in_context(trace.DefaultSpan(span_context), context) + + def inject( + self, + set_in_carrier: Setter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> None: + span = get_span_from_context(context=context) + sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 + set_in_carrier( + carrier, self.TRACE_ID_KEY, format_trace_id(span.context.trace_id), + ) + set_in_carrier( + carrier, self.PARENT_ID_KEY, format_span_id(span.context.span_id) + ) + set_in_carrier( + carrier, + self.SAMPLING_PRIORITY_KEY, + str(constants.AUTO_KEEP if sampled else constants.AUTO_REJECT), + ) + if constants.DD_ORIGIN in span.context.trace_state: + set_in_carrier( + carrier, + self.ORIGIN_KEY, + span.context.trace_state[constants.DD_ORIGIN], + ) + + +def format_trace_id(trace_id: int) -> str: + """Format the trace id for Datadog.""" + return str(trace_id & 0xFFFFFFFFFFFFFFFF) + + +def format_span_id(span_id: int) -> str: + """Format the span id for Datadog.""" + return str(span_id) + + +def extract_first_element( + items: typing.Iterable[HTTPTextFormatT], +) -> typing.Optional[HTTPTextFormatT]: + if items is None: + return None + return next(iter(items), None) diff --git a/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py b/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py index 97ca3fa9b9..14fd550789 100644 --- a/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py +++ b/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py @@ -403,3 +403,40 @@ def test_span_processor_scheduled_delay(self): self.assertEqual(len(datadog_spans), 1) tracer_provider.shutdown() + + def test_origin(self): + context = trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=trace_api.INVALID_SPAN, + is_remote=True, + trace_state=trace_api.TraceState( + {datadog.constants.DD_ORIGIN: "origin-service"} + ), + ) + + root_span = trace.Span(name="root", context=context, parent=None) + child_span = trace.Span( + name="child", context=context, parent=root_span + ) + root_span.start() + child_span.start() + child_span.end() + root_span.end() + + # pylint: disable=protected-access + exporter = datadog.DatadogSpanExporter() + datadog_spans = [ + span.to_dict() + for span in exporter._translate_to_datadog([root_span, child_span]) + ] + + self.assertEqual(len(datadog_spans), 2) + + actual = [ + span["meta"].get(datadog.constants.DD_ORIGIN) + if "meta" in span + else None + for span in datadog_spans + ] + expected = ["origin-service", None] + self.assertListEqual(actual, expected) diff --git a/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py b/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py new file mode 100644 index 0000000000..cf2fbf4220 --- /dev/null +++ b/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py @@ -0,0 +1,173 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import trace as trace_api +from opentelemetry.ext.datadog import constants, propagator +from opentelemetry.sdk import trace +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, +) + +FORMAT = propagator.DatadogFormat() + + +def get_as_list(dict_object, key): + value = dict_object.get(key) + return [value] if value is not None else [] + + +class TestDatadogFormat(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.serialized_trace_id = propagator.format_trace_id( + trace.generate_trace_id() + ) + cls.serialized_parent_id = propagator.format_span_id( + trace.generate_span_id() + ) + cls.serialized_origin = "origin-service" + + def test_malformed_headers(self): + """Test with no Datadog headers""" + malformed_trace_id_key = FORMAT.TRACE_ID_KEY + "-x" + malformed_parent_id_key = FORMAT.PARENT_ID_KEY + "-x" + context = get_span_from_context( + FORMAT.extract( + get_as_list, + { + malformed_trace_id_key: self.serialized_trace_id, + malformed_parent_id_key: self.serialized_parent_id, + }, + ) + ).get_context() + + self.assertNotEqual(context.trace_id, int(self.serialized_trace_id)) + self.assertNotEqual(context.span_id, int(self.serialized_parent_id)) + self.assertFalse(context.is_remote) + + def test_missing_trace_id(self): + """If a trace id is missing, populate an invalid trace id.""" + carrier = { + FORMAT.PARENT_ID_KEY: self.serialized_parent_id, + } + + ctx = FORMAT.extract(get_as_list, carrier) + span_context = get_span_from_context(ctx).get_context() + self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) + + def test_missing_parent_id(self): + """If a parent id is missing, populate an invalid trace id.""" + carrier = { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + } + + ctx = FORMAT.extract(get_as_list, carrier) + span_context = get_span_from_context(ctx).get_context() + self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) + + def test_context_propagation(self): + """Test the propagation of Datadog headers.""" + parent_context = get_span_from_context( + FORMAT.extract( + get_as_list, + { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.PARENT_ID_KEY: self.serialized_parent_id, + FORMAT.SAMPLING_PRIORITY_KEY: str(constants.AUTO_KEEP), + FORMAT.ORIGIN_KEY: self.serialized_origin, + }, + ) + ).get_context() + + self.assertEqual( + parent_context.trace_id, int(self.serialized_trace_id) + ) + self.assertEqual( + parent_context.span_id, int(self.serialized_parent_id) + ) + self.assertEqual(parent_context.trace_flags, constants.AUTO_KEEP) + self.assertEqual( + parent_context.trace_state.get(constants.DD_ORIGIN), + self.serialized_origin, + ) + self.assertTrue(parent_context.is_remote) + + child = trace.Span( + "child", + trace_api.SpanContext( + parent_context.trace_id, + trace.generate_span_id(), + is_remote=False, + trace_flags=parent_context.trace_flags, + trace_state=parent_context.trace_state, + ), + parent=parent_context, + ) + + child_carrier = {} + child_context = set_span_in_context(child) + FORMAT.inject(dict.__setitem__, child_carrier, context=child_context) + + self.assertEqual( + child_carrier[FORMAT.TRACE_ID_KEY], self.serialized_trace_id + ) + self.assertEqual( + child_carrier[FORMAT.PARENT_ID_KEY], str(child.context.span_id) + ) + self.assertEqual( + child_carrier[FORMAT.SAMPLING_PRIORITY_KEY], + str(constants.AUTO_KEEP), + ) + self.assertEqual( + child_carrier.get(FORMAT.ORIGIN_KEY), self.serialized_origin + ) + + def test_sampling_priority_auto_reject(self): + """Test sampling priority rejected.""" + parent_context = get_span_from_context( + FORMAT.extract( + get_as_list, + { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.PARENT_ID_KEY: self.serialized_parent_id, + FORMAT.SAMPLING_PRIORITY_KEY: str(constants.AUTO_REJECT), + }, + ) + ).get_context() + + self.assertEqual(parent_context.trace_flags, constants.AUTO_REJECT) + + child = trace.Span( + "child", + trace_api.SpanContext( + parent_context.trace_id, + trace.generate_span_id(), + is_remote=False, + trace_flags=parent_context.trace_flags, + trace_state=parent_context.trace_state, + ), + parent=parent_context, + ) + + child_carrier = {} + child_context = set_span_in_context(child) + FORMAT.inject(dict.__setitem__, child_carrier, context=child_context) + + self.assertEqual( + child_carrier[FORMAT.SAMPLING_PRIORITY_KEY], + str(constants.AUTO_REJECT), + ) From 46f8f6465da39d36532d69cf12f65e1f7de7111f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 22 May 2020 22:01:05 -0600 Subject: [PATCH 0357/1517] opencensus: Rename otcollector to opencensus (#695) renaming otcollector to opencensus, as it's using opencensus under the hood. This was originally intended to be replaced by otlp, by a new package can be created for that instead. Co-authored-by: alrex --- .../opencensus-exporter-metrics/README.rst | 52 +++++++ .../opencensus-exporter-metrics/collector.py | 47 +++++++ .../docker/collector-config.yaml | 18 +++ .../docker/docker-compose.yaml | 19 +++ .../docker/prometheus.yaml | 5 + .../opencensus-exporter-tracer/README.rst | 51 +++++++ .../opencensus-exporter-tracer/collector.py | 36 +++++ .../docker/collector-config.yaml | 19 +++ .../docker/docker-compose.yaml | 20 +++ .../opencensusexporter/opencensusexporter.rst | 7 + docs/ext/otcollector/otcollector.rst | 7 - .../tests/docker-compose.yml | 8 +- .../test_opencensusexporter_functional.py | 129 ++++++++++++++++++ .../CHANGELOG.md | 0 .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 24 ++++ .../setup.cfg | 6 +- .../setup.py | 2 +- .../ext/opencensusexporter/__init__.py | 18 +++ .../metrics_exporter/__init__.py | 11 +- .../trace_exporter/__init__.py | 16 +-- .../ext/opencensusexporter}/util.py | 6 +- .../ext/opencensusexporter}/version.py | 0 .../tests/__init__.py | 0 .../test_otcollector_metrics_exporter.py | 8 +- .../tests/test_otcollector_trace_exporter.py | 12 +- ext/opentelemetry-ext-otcollector/README.rst | 24 ---- .../opentelemetry/ext/otcollector/__init__.py | 86 ------------ tox.ini | 13 +- 30 files changed, 488 insertions(+), 156 deletions(-) create mode 100644 docs/examples/opencensus-exporter-metrics/README.rst create mode 100644 docs/examples/opencensus-exporter-metrics/collector.py create mode 100644 docs/examples/opencensus-exporter-metrics/docker/collector-config.yaml create mode 100644 docs/examples/opencensus-exporter-metrics/docker/docker-compose.yaml create mode 100644 docs/examples/opencensus-exporter-metrics/docker/prometheus.yaml create mode 100644 docs/examples/opencensus-exporter-tracer/README.rst create mode 100644 docs/examples/opencensus-exporter-tracer/collector.py create mode 100644 docs/examples/opencensus-exporter-tracer/docker/collector-config.yaml create mode 100644 docs/examples/opencensus-exporter-tracer/docker/docker-compose.yaml create mode 100644 docs/ext/opencensusexporter/opencensusexporter.rst delete mode 100644 docs/ext/otcollector/otcollector.rst create mode 100644 ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py rename ext/{opentelemetry-ext-otcollector => opentelemetry-ext-opencensusexporter}/CHANGELOG.md (100%) rename ext/{opentelemetry-ext-otcollector => opentelemetry-ext-opencensusexporter}/LICENSE (100%) rename ext/{opentelemetry-ext-otcollector => opentelemetry-ext-opencensusexporter}/MANIFEST.in (100%) create mode 100644 ext/opentelemetry-ext-opencensusexporter/README.rst rename ext/{opentelemetry-ext-otcollector => opentelemetry-ext-opencensusexporter}/setup.cfg (92%) rename ext/{opentelemetry-ext-otcollector => opentelemetry-ext-opencensusexporter}/setup.py (91%) create mode 100644 ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/__init__.py rename ext/{opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector => opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter}/metrics_exporter/__init__.py (94%) rename ext/{opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector => opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter}/trace_exporter/__init__.py (92%) rename ext/{opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector => opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter}/util.py (94%) rename ext/{opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector => opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter}/version.py (100%) rename ext/{opentelemetry-ext-otcollector => opentelemetry-ext-opencensusexporter}/tests/__init__.py (100%) rename ext/{opentelemetry-ext-otcollector => opentelemetry-ext-opencensusexporter}/tests/test_otcollector_metrics_exporter.py (96%) rename ext/{opentelemetry-ext-otcollector => opentelemetry-ext-opencensusexporter}/tests/test_otcollector_trace_exporter.py (97%) delete mode 100644 ext/opentelemetry-ext-otcollector/README.rst delete mode 100644 ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py diff --git a/docs/examples/opencensus-exporter-metrics/README.rst b/docs/examples/opencensus-exporter-metrics/README.rst new file mode 100644 index 0000000000..30961e061d --- /dev/null +++ b/docs/examples/opencensus-exporter-metrics/README.rst @@ -0,0 +1,52 @@ +OpenTelemetry Collector Metrics OpenCensus Exporter Example +=========================================================== + +This example shows how to use the OpenCensus Exporter to export metrics to +the OpenTelemetry collector. + +The source files of this example are available :scm_web:`here `. + +Installation +------------ + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + pip install opentelemetry-ext-opencensusexporter + +Run the Example +--------------- + +Before running the example, it's necessary to run the OpenTelemetry collector +and Prometheus. The :scm_web:`docker ` +folder contains the a docker-compose template with the configuration of those +services. + +.. code-block:: sh + + pip install docker-compose + cd docker + docker-compose up + + +Now, the example can be executed: + +.. code-block:: sh + + python collector.py + + +The metrics are available in the Prometheus dashboard at http://localhost:9090/graph, +look for the "requests" metric on the list. + +Useful links +------------ + +- OpenTelemetry_ +- `OpenTelemetry Collector`_ +- :doc:`../../api/trace` +- :doc:`../../ext/opencensusexporter/opencensusexporter` + +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +.. _OpenTelemetry Collector: https://github.com/open-telemetry/opentelemetry-collector diff --git a/docs/examples/opencensus-exporter-metrics/collector.py b/docs/examples/opencensus-exporter-metrics/collector.py new file mode 100644 index 0000000000..89dabd12ea --- /dev/null +++ b/docs/examples/opencensus-exporter-metrics/collector.py @@ -0,0 +1,47 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This example shows how to export metrics to the OT collector. +""" + +from opentelemetry import metrics +from opentelemetry.ext.opencensusexporter.metrics_exporter import ( + OpenCensusMetricsExporter, +) +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export.controller import PushController + +exporter = OpenCensusMetricsExporter( + service_name="basic-service", endpoint="localhost:55678" +) + +metrics.set_meter_provider(MeterProvider()) +meter = metrics.get_meter(__name__) +controller = PushController(meter, exporter, 5) + +requests_counter = meter.create_metric( + name="requests", + description="number of requests", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), +) + +staging_labels = {"environment": "staging"} +requests_counter.add(25, staging_labels) + +print("Metrics are available now at http://localhost:9090/graph") +input("Press any key to exit...") diff --git a/docs/examples/opencensus-exporter-metrics/docker/collector-config.yaml b/docs/examples/opencensus-exporter-metrics/docker/collector-config.yaml new file mode 100644 index 0000000000..78f685d208 --- /dev/null +++ b/docs/examples/opencensus-exporter-metrics/docker/collector-config.yaml @@ -0,0 +1,18 @@ +receivers: + opencensus: + endpoint: "0.0.0.0:55678" + +exporters: + prometheus: + endpoint: "0.0.0.0:8889" + logging: {} + +processors: + batch: + queued_retry: + +service: + pipelines: + metrics: + receivers: [opencensus] + exporters: [logging, prometheus] diff --git a/docs/examples/opencensus-exporter-metrics/docker/docker-compose.yaml b/docs/examples/opencensus-exporter-metrics/docker/docker-compose.yaml new file mode 100644 index 0000000000..d12a0a4f18 --- /dev/null +++ b/docs/examples/opencensus-exporter-metrics/docker/docker-compose.yaml @@ -0,0 +1,19 @@ +version: "2" +services: + + otel-collector: + image: omnition/opentelemetry-collector-contrib:latest + command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"] + volumes: + - ./collector-config.yaml:/conf/collector-config.yaml + ports: + - "8889:8889" # Prometheus exporter metrics + - "55678:55678" # OpenCensus receiver + + prometheus: + container_name: prometheus + image: prom/prometheus:latest + volumes: + - ./prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" diff --git a/docs/examples/opencensus-exporter-metrics/docker/prometheus.yaml b/docs/examples/opencensus-exporter-metrics/docker/prometheus.yaml new file mode 100644 index 0000000000..4eb2357231 --- /dev/null +++ b/docs/examples/opencensus-exporter-metrics/docker/prometheus.yaml @@ -0,0 +1,5 @@ +scrape_configs: + - job_name: 'otel-collector' + scrape_interval: 5s + static_configs: + - targets: ['otel-collector:8889'] diff --git a/docs/examples/opencensus-exporter-tracer/README.rst b/docs/examples/opencensus-exporter-tracer/README.rst new file mode 100644 index 0000000000..0019994308 --- /dev/null +++ b/docs/examples/opencensus-exporter-tracer/README.rst @@ -0,0 +1,51 @@ +OpenTelemetry Collector Tracer OpenCensus Exporter Example +========================================================== + +This example shows how to use the OpenCensus Exporter to export traces to the +OpenTelemetry collector. + +The source files of this example are available :scm_web:`here `. + +Installation +------------ + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + pip install opentelemetry-ext-opencensusexporter + +Run the Example +--------------- + +Before running the example, it's necessary to run the OpenTelemetry collector +and Jaeger. The :scm_web:`docker ` +folder contains a ``docker-compose`` template with the configuration of those +services. + +.. code-block:: sh + + pip install docker-compose + cd docker + docker-compose up + + +Now, the example can be executed: + +.. code-block:: sh + + python collector.py + + +The traces are available in the Jaeger UI at http://localhost:16686/. + +Useful links +------------ + +- OpenTelemetry_ +- `OpenTelemetry Collector`_ +- :doc:`../../api/trace` +- :doc:`../../ext/opencensusexporter/opencensusexporter` + +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +.. _OpenTelemetry Collector: https://github.com/open-telemetry/opentelemetry-collector diff --git a/docs/examples/opencensus-exporter-tracer/collector.py b/docs/examples/opencensus-exporter-tracer/collector.py new file mode 100644 index 0000000000..3f0c18aaf7 --- /dev/null +++ b/docs/examples/opencensus-exporter-tracer/collector.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry import trace +from opentelemetry.ext.opencensusexporter.trace_exporter import ( + OpenCensusSpanExporter, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + +exporter = OpenCensusSpanExporter( + service_name="basic-service", endpoint="localhost:55678" +) + +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer(__name__) +span_processor = BatchExportSpanProcessor(exporter) + +trace.get_tracer_provider().add_span_processor(span_processor) +with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): + print("Hello world from OpenTelemetry Python!") diff --git a/docs/examples/opencensus-exporter-tracer/docker/collector-config.yaml b/docs/examples/opencensus-exporter-tracer/docker/collector-config.yaml new file mode 100644 index 0000000000..bcf59c5802 --- /dev/null +++ b/docs/examples/opencensus-exporter-tracer/docker/collector-config.yaml @@ -0,0 +1,19 @@ +receivers: + opencensus: + endpoint: "0.0.0.0:55678" + +exporters: + jaeger_grpc: + endpoint: jaeger-all-in-one:14250 + logging: {} + +processors: + batch: + queued_retry: + +service: + pipelines: + traces: + receivers: [opencensus] + exporters: [jaeger_grpc, logging] + processors: [batch, queued_retry] diff --git a/docs/examples/opencensus-exporter-tracer/docker/docker-compose.yaml b/docs/examples/opencensus-exporter-tracer/docker/docker-compose.yaml new file mode 100644 index 0000000000..71d7ccd5a1 --- /dev/null +++ b/docs/examples/opencensus-exporter-tracer/docker/docker-compose.yaml @@ -0,0 +1,20 @@ +version: "2" +services: + + # Collector + collector: + image: omnition/opentelemetry-collector-contrib:latest + command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"] + volumes: + - ./collector-config.yaml:/conf/collector-config.yaml + ports: + - "55678:55678" + + jaeger-all-in-one: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "6831:6831/udp" + - "6832:6832/udp" + - "14268" + - "14250" diff --git a/docs/ext/opencensusexporter/opencensusexporter.rst b/docs/ext/opencensusexporter/opencensusexporter.rst new file mode 100644 index 0000000000..07b9a85558 --- /dev/null +++ b/docs/ext/opencensusexporter/opencensusexporter.rst @@ -0,0 +1,7 @@ +OpenCensus Exporter +=================== + +.. automodule:: opentelemetry.ext.opencensusexporter + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ext/otcollector/otcollector.rst b/docs/ext/otcollector/otcollector.rst deleted file mode 100644 index 286aed6c08..0000000000 --- a/docs/ext/otcollector/otcollector.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Collector Exporter -================================ - -.. automodule:: opentelemetry.ext.otcollector - :members: - :undoc-members: - :show-inheritance: diff --git a/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml b/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml index f996db077e..bbb005a02e 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml +++ b/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml @@ -38,4 +38,10 @@ services: - "5778:5778" - "16686:16686" - "14268:14268" - - "9411:9411" + - "9411:9411" + otopencensus: + image: omnition/opencensus-collector:0.1.11 + command: --logging-exporter DEBUG + ports: + - "8888:8888" + - "55678:55678" diff --git a/ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py b/ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py new file mode 100644 index 0000000000..929002a4c7 --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py @@ -0,0 +1,129 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry import trace +from opentelemetry.context import attach, detach, set_value +from opentelemetry.ext.opencensusexporter.trace_exporter import ( + OpenCensusSpanExporter, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.test.test_base import TestBase + + +class ExportStatusSpanProcessor(SimpleExportSpanProcessor): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.export_status = [] + + def on_end(self, span): + token = attach(set_value("suppress_instrumentation", True)) + self.export_status.append(self.span_exporter.export((span,))) + detach(token) + + +class TestOpenCensusSpanExporter(TestBase): + def setUp(self): + super().setUp() + + trace.set_tracer_provider(TracerProvider()) + self.tracer = trace.get_tracer(__name__) + self.span_processor = ExportStatusSpanProcessor( + OpenCensusSpanExporter( + service_name="basic-service", endpoint="localhost:55678" + ) + ) + + trace.get_tracer_provider().add_span_processor(self.span_processor) + + def test_export(self): + with self.tracer.start_as_current_span("foo"): + with self.tracer.start_as_current_span("bar"): + with self.tracer.start_as_current_span("baz"): + pass + + self.assertTrue(len(self.span_processor.export_status), 3) + + for export_status in self.span_processor.export_status: + self.assertEqual(export_status.name, "SUCCESS") + self.assertEqual(export_status.value, 0) + + +# FIXME This test fails because of an issue in the OpenCensus collector +# reported here: +# https://github.com/census-instrumentation/opencensus-service/issues/641 +# Uncomment this test when this issue gets fixed. + +# from time import sleep +# from opentelemetry.ext.opencensusexporter.metrics_exporter import ( +# OpenCensusMetricsExporter, +# ) +# from opentelemetry.sdk.metrics import Counter, MeterProvider +# from opentelemetry.sdk.metrics.export.controller import PushController + +# from opentelemetry import metrics +# +# +# class ExportStatusMetricController(PushController): +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.export_status = [] +# +# def run(self): +# while not self.finished.wait(self.interval): +# self.tick() +# +# def tick(self): +# # Collect all of the meter's metrics to be exported +# self.meter.collect() +# token = attach(set_value("suppress_instrumentation", True)) +# # Export the given metrics in the batcher +# self.export_status.append( +# self.exporter.export(self.meter.batcher.checkpoint_set()) +# ) +# detach(token) +# # Perform post-exporting logic based on batcher configuration +# self.meter.batcher.finished_collection() +# +# +# class TestOpenCensusMetricsExporter(TestBase): +# def setUp(self): +# super().setUp() +# +# metrics.set_meter_provider(MeterProvider()) +# self.meter = metrics.get_meter(__name__) +# self.controller = ExportStatusMetricController( +# self.meter, +# OpenCensusMetricsExporter( +# service_name="basic-service", endpoint="localhost:55678" +# ), +# 1, +# ) +# +# def test_export(self): +# +# self.meter.create_metric( +# name="requests", +# description="number of requests", +# unit="1", +# value_type=int, +# metric_type=Counter, +# label_keys=("environment",), +# ).add(25, {"environment": "staging"}) +# +# sleep(2) +# +# self.assertEqual(len(self.controller.export_status), 1) +# self.assertEqual(self.controller.export_status[0].name, "SUCCESS") +# self.assertEqual(self.controller.export_status[0].value, 0) diff --git a/ext/opentelemetry-ext-otcollector/CHANGELOG.md b/ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md similarity index 100% rename from ext/opentelemetry-ext-otcollector/CHANGELOG.md rename to ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md diff --git a/ext/opentelemetry-ext-otcollector/LICENSE b/ext/opentelemetry-ext-opencensusexporter/LICENSE similarity index 100% rename from ext/opentelemetry-ext-otcollector/LICENSE rename to ext/opentelemetry-ext-opencensusexporter/LICENSE diff --git a/ext/opentelemetry-ext-otcollector/MANIFEST.in b/ext/opentelemetry-ext-opencensusexporter/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-otcollector/MANIFEST.in rename to ext/opentelemetry-ext-opencensusexporter/MANIFEST.in diff --git a/ext/opentelemetry-ext-opencensusexporter/README.rst b/ext/opentelemetry-ext-opencensusexporter/README.rst new file mode 100644 index 0000000000..75563053d1 --- /dev/null +++ b/ext/opentelemetry-ext-opencensusexporter/README.rst @@ -0,0 +1,24 @@ +OpenCensus Exporter +=================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-opencensusexporter.svg + :target: https://pypi.org/project/opentelemetry-ext-opencensusexporter/ + +This library allows to export traces and metrics using OpenCensus. + +Installation +------------ + +:: + + pip install opentelemetry-ext-opencensusexporter + + +References +---------- + +* `OpenCensus Exporter `_ +* `OpenTelemetry Collector `_ +* `OpenTelemetry `_ diff --git a/ext/opentelemetry-ext-otcollector/setup.cfg b/ext/opentelemetry-ext-opencensusexporter/setup.cfg similarity index 92% rename from ext/opentelemetry-ext-otcollector/setup.cfg rename to ext/opentelemetry-ext-opencensusexporter/setup.cfg index 8ffd4b6e4d..850368fd0d 100644 --- a/ext/opentelemetry-ext-otcollector/setup.cfg +++ b/ext/opentelemetry-ext-opencensusexporter/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-otcollector -description = OpenTelemetry Collector Exporter +name = opentelemetry-ext-opencensusexporter +description = OpenCensus Exporter long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-otcollector +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-opencensusexporter platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-otcollector/setup.py b/ext/opentelemetry-ext-opencensusexporter/setup.py similarity index 91% rename from ext/opentelemetry-ext-otcollector/setup.py rename to ext/opentelemetry-ext-opencensusexporter/setup.py index d5c2886ac5..dfb8fd82bf 100644 --- a/ext/opentelemetry-ext-otcollector/setup.py +++ b/ext/opentelemetry-ext-opencensusexporter/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "otcollector", "version.py" + BASE_DIR, "src", "opentelemetry", "ext", "opencensusexporter", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/__init__.py b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/__init__.py new file mode 100644 index 0000000000..27aae238f4 --- /dev/null +++ b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/__init__.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The **OpenCensus Exporter** allows to export traces and metrics using +OpenCensus. +""" diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/metrics_exporter/__init__.py similarity index 94% rename from ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py rename to ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/metrics_exporter/__init__.py index 3a43d9cc84..faa0788c7f 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py +++ b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/metrics_exporter/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""OpenTelemetry Collector Metrics Exporter.""" +"""OpenCensus Collector Metrics Exporter.""" import logging from typing import Sequence @@ -24,13 +24,12 @@ ) from opencensus.proto.metrics.v1 import metrics_pb2 -import opentelemetry.ext.otcollector.util as utils +import opentelemetry.ext.opencensusexporter.util as utils from opentelemetry.sdk.metrics import Counter, Metric from opentelemetry.sdk.metrics.export import ( MetricRecord, MetricsExporter, MetricsExportResult, - aggregate, ) DEFAULT_ENDPOINT = "localhost:55678" @@ -39,11 +38,11 @@ # pylint: disable=no-member -class CollectorMetricsExporter(MetricsExporter): - """OpenTelemetry Collector metrics exporter. +class OpenCensusMetricsExporter(MetricsExporter): + """OpenCensus metrics exporter. Args: - endpoint: OpenTelemetry Collector OpenCensus receiver endpoint. + endpoint: OpenCensus Collector receiver endpoint. service_name: Name of Collector service. host_name: Host name. client: MetricsService client stub. diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/trace_exporter/__init__.py similarity index 92% rename from ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py rename to ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/trace_exporter/__init__.py index 914d97ec5a..adadef1666 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py +++ b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/trace_exporter/__init__.py @@ -12,10 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""OpenTelemetry Collector Exporter.""" +"""OpenCensus Span Exporter.""" import logging -from typing import Optional, Sequence +from typing import Sequence import grpc from opencensus.proto.agent.trace.v1 import ( @@ -24,11 +24,9 @@ ) from opencensus.proto.trace.v1 import trace_pb2 -import opentelemetry.ext.otcollector.util as utils -import opentelemetry.trace as trace_api -from opentelemetry.sdk.trace import Span, SpanContext +import opentelemetry.ext.opencensusexporter.util as utils +from opentelemetry.sdk.trace import Span from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult -from opentelemetry.trace import SpanKind, TraceState DEFAULT_ENDPOINT = "localhost:55678" @@ -36,11 +34,11 @@ # pylint: disable=no-member -class CollectorSpanExporter(SpanExporter): - """OpenTelemetry Collector span exporter. +class OpenCensusSpanExporter(SpanExporter): + """OpenCensus Collector span exporter. Args: - endpoint: OpenTelemetry Collector OpenCensus receiver endpoint. + endpoint: OpenCensus Collector receiver endpoint. service_name: Name of Collector service. host_name: Host name. client: TraceService client stub. diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/util.py similarity index 94% rename from ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py rename to ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/util.py index e8717b0c8b..88ff6258f7 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py +++ b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/util.py @@ -21,8 +21,8 @@ from opencensus.proto.agent.common.v1 import common_pb2 from opencensus.proto.trace.v1 import trace_pb2 -from opentelemetry.ext.otcollector.version import ( - __version__ as otcollector_exporter_version, +from opentelemetry.ext.opencensusexporter.version import ( + __version__ as opencensusexporter_exporter_version, ) from opentelemetry.trace import SpanKind @@ -96,7 +96,7 @@ def get_node(service_name, host_name): ), library_info=common_pb2.LibraryInfo( language=common_pb2.LibraryInfo.Language.Value("PYTHON"), - exporter_version=otcollector_exporter_version, + exporter_version=opencensusexporter_exporter_version, core_library_version=OPENTELEMETRY_VERSION, ), service_info=common_pb2.ServiceInfo(name=service_name), diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py similarity index 100% rename from ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py rename to ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py diff --git a/ext/opentelemetry-ext-otcollector/tests/__init__.py b/ext/opentelemetry-ext-opencensusexporter/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-otcollector/tests/__init__.py rename to ext/opentelemetry-ext-opencensusexporter/tests/__init__.py diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py similarity index 96% rename from ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py rename to ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py index 7dcbf452e1..63ea28cd93 100644 --- a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py +++ b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py @@ -20,7 +20,7 @@ from opencensus.proto.metrics.v1 import metrics_pb2 from opentelemetry import metrics -from opentelemetry.ext.otcollector import metrics_exporter +from opentelemetry.ext.opencensusexporter import metrics_exporter from opentelemetry.sdk.metrics import ( Counter, Measure, @@ -47,7 +47,7 @@ def setUpClass(cls): def test_constructor(self): mock_get_node = mock.Mock() patch = mock.patch( - "opentelemetry.ext.otcollector.util.get_node", + "opentelemetry.ext.opencensusexporter.util.get_node", side_effect=mock_get_node, ) service_name = "testServiceName" @@ -55,7 +55,7 @@ def test_constructor(self): client = grpc.insecure_channel("") endpoint = "testEndpoint" with patch: - exporter = metrics_exporter.CollectorMetricsExporter( + exporter = metrics_exporter.OpenCensusMetricsExporter( service_name=service_name, host_name=host_name, endpoint=endpoint, @@ -115,7 +115,7 @@ def test_export(self): mock_export = mock.MagicMock() mock_client.Export = mock_export host_name = "testHostName" - collector_exporter = metrics_exporter.CollectorMetricsExporter( + collector_exporter = metrics_exporter.OpenCensusMetricsExporter( client=mock_client, host_name=host_name ) test_metric = self._meter.create_metric( diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_trace_exporter.py similarity index 97% rename from ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py rename to ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_trace_exporter.py index 97d276af40..0801d6d0c1 100644 --- a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py +++ b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_trace_exporter.py @@ -19,10 +19,10 @@ from google.protobuf.timestamp_pb2 import Timestamp from opencensus.proto.trace.v1 import trace_pb2 -import opentelemetry.ext.otcollector.util as utils +import opentelemetry.ext.opencensusexporter.util as utils from opentelemetry import trace as trace_api -from opentelemetry.ext.otcollector.trace_exporter import ( - CollectorSpanExporter, +from opentelemetry.ext.opencensusexporter.trace_exporter import ( + OpenCensusSpanExporter, translate_to_collector, ) from opentelemetry.sdk import trace @@ -35,7 +35,7 @@ class TestCollectorSpanExporter(unittest.TestCase): def test_constructor(self): mock_get_node = mock.Mock() patch = mock.patch( - "opentelemetry.ext.otcollector.util.get_node", + "opentelemetry.ext.opencensusexporter.util.get_node", side_effect=mock_get_node, ) service_name = "testServiceName" @@ -43,7 +43,7 @@ def test_constructor(self): client = grpc.insecure_channel("") endpoint = "testEndpoint" with patch: - exporter = CollectorSpanExporter( + exporter = OpenCensusSpanExporter( service_name=service_name, host_name=host_name, endpoint=endpoint, @@ -289,7 +289,7 @@ def test_export(self): mock_export = mock.MagicMock() mock_client.Export = mock_export host_name = "testHostName" - collector_exporter = CollectorSpanExporter( + collector_exporter = OpenCensusSpanExporter( client=mock_client, host_name=host_name ) diff --git a/ext/opentelemetry-ext-otcollector/README.rst b/ext/opentelemetry-ext-otcollector/README.rst deleted file mode 100644 index 916c64ffe6..0000000000 --- a/ext/opentelemetry-ext-otcollector/README.rst +++ /dev/null @@ -1,24 +0,0 @@ -OpenTelemetry Collector Exporter -================================ - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-otcollector.svg - :target: https://pypi.org/project/opentelemetry-ext-otcollector/ - -This library allows to export data to `OpenTelemetry Collector`_ , currently using OpenCensus receiver in Collector side. - -Installation ------------- - -:: - - pip install opentelemetry-ext-otcollector - - -References ----------- - -* `OpenTelemetry Collector Exporter `_ -* `OpenTelemetry Collector `_ -* `OpenTelemetry `_ diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py deleted file mode 100644 index ae5e2ac33f..0000000000 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The **OpenTelemetry Collector Exporter** allows to export OpenTelemetry traces to OpenTelemetry Collector. - -.. code:: python - - from opentelemetry import trace - from opentelemetry.ext.otcollector.trace_exporter import CollectorSpanExporter - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - - - # create a CollectorSpanExporter - collector_exporter = CollectorSpanExporter( - # optional: - # endpoint="myCollectorUrl:55678", - # service_name="test_service", - # host_name="machine/container name", - ) - - # Create a BatchExportSpanProcessor and add the exporter to it - span_processor = BatchExportSpanProcessor(collector_exporter) - - # Configure the tracer to use the collector exporter - tracer_provider = TracerProvider() - tracer_provider.add_span_processor(span_processor) - tracer = TracerProvider().get_tracer(__name__) - - with tracer.start_as_current_span("foo"): - print("Hello world!") - -Metrics Usage -------------- - -The **OpenTelemetry Collector Exporter** allows to export OpenTelemetry metrics to OpenTelemetry Collector. - -.. code:: python - - from opentelemetry import metrics - from opentelemetry.ext.otcollector.metrics_exporter import CollectorMetricsExporter - from opentelemetry.sdk.metrics import Counter, MeterProvider - from opentelemetry.sdk.metrics.export.controller import PushController - - - # create a CollectorMetricsExporter - collector_exporter = CollectorMetricsExporter( - # optional: - # endpoint="myCollectorUrl:55678", - # service_name="test_service", - # host_name="machine/container name", - ) - - # Meter is responsible for creating and recording metrics - metrics.set_meter_provider(MeterProvider()) - meter = metrics.get_meter(__name__) - # controller collects metrics created from meter and exports it via the - # exporter every interval - controller = PushController(meter, collector_exporter, 5) - counter = meter.create_metric( - "requests", - "number of requests", - "requests", - int, - Counter, - ("environment",), - ) - # Labels are used to identify key-values that are associated with a specific - # metric that you want to record. These are useful for pre-aggregation and can - # be used to store custom dimensions pertaining to a metric - labels = {"environment": "staging"} - - counter.add(25, labels) -""" diff --git a/tox.ini b/tox.ini index 5742e717d7..8f44a47edf 100644 --- a/tox.ini +++ b/tox.ini @@ -59,9 +59,9 @@ envlist = py3{4,5,6,7,8}-test-ext-mysql pypy3-test-ext-mysql - ; opentelemetry-ext-otcollector - py3{4,5,6,7,8}-test-ext-otcollector - ; ext-otcollector intentionally excluded from pypy3 + ; opentelemetry-ext-opencensusexporter + py3{4,5,6,7,8}-test-ext-opencensusexporter + ; ext-opencensusexporter intentionally excluded from pypy3 ; opentelemetry-ext-prometheus py3{4,5,6,7,8}-test-ext-prometheus @@ -143,7 +143,7 @@ changedir = test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests test-ext-django: ext/opentelemetry-ext-django/tests test-ext-mysql: ext/opentelemetry-ext-mysql/tests - test-ext-otcollector: ext/opentelemetry-ext-otcollector/tests + test-ext-opencensusexporter: ext/opentelemetry-ext-opencensusexporter/tests test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests @@ -184,7 +184,7 @@ commands_pre = mysql: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-mysql[test] - otcollector: pip install {toxinidir}/ext/opentelemetry-ext-otcollector + opencensusexporter: pip install {toxinidir}/ext/opentelemetry-ext-opencensusexporter prometheus: pip install {toxinidir}/ext/opentelemetry-ext-prometheus @@ -310,7 +310,8 @@ commands_pre = -e {toxinidir}/ext/opentelemetry-ext-pymongo \ -e {toxinidir}/ext/opentelemetry-ext-pymysql \ -e {toxinidir}/ext/opentelemetry-ext-sqlalchemy \ - -e {toxinidir}/ext/opentelemetry-ext-redis + -e {toxinidir}/ext/opentelemetry-ext-redis \ + -e {toxinidir}/ext/opentelemetry-ext-opencensusexporter docker-compose up -d python check_availability.py commands = From 0e9a8e42dec18ee669814172c772c89797e6e071 Mon Sep 17 00:00:00 2001 From: Yash Mehrotra Date: Sun, 24 May 2020 04:34:57 +0530 Subject: [PATCH 0358/1517] sdk: Span.resource will now default to an empty resource (#724) Right now, A resource object attached to a Span can be None. That means that there's two similar objects that refer to an empty resource: None, and the _EMPTY_RESOURCE global. We should just always default to the _EMPTY_RESOURCE object: this avoids None checks in exporters. Co-authored-by: Yusuke Tsutsumi --- docs/sdk/resources.rst | 7 +++++++ docs/sdk/sdk.rst | 3 ++- .../src/opentelemetry/ext/jaeger/__init__.py | 3 +-- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 docs/sdk/resources.rst diff --git a/docs/sdk/resources.rst b/docs/sdk/resources.rst new file mode 100644 index 0000000000..08732ac025 --- /dev/null +++ b/docs/sdk/resources.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.resources package +========================================== + +.. automodule:: opentelemetry.sdk.resources + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sdk/sdk.rst b/docs/sdk/sdk.rst index 27e9d44fa4..b1dae535e2 100644 --- a/docs/sdk/sdk.rst +++ b/docs/sdk/sdk.rst @@ -7,4 +7,5 @@ OpenTelemetry Python SDK :maxdepth: 1 metrics - trace \ No newline at end of file + resources + trace diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py index 852f94f610..18ec0dc7bf 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -197,8 +197,7 @@ def _translate_to_jaeger(spans: Span): parent_id = span.parent.span_id if span.parent else 0 tags = _extract_tags(span.attributes) - if span.resource: - tags.extend(_extract_tags(span.resource.labels)) + tags.extend(_extract_tags(span.resource.labels)) tags.extend( [ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 4f05168b44..66e13d9cc8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -264,7 +264,7 @@ def __init__( parent: Optional[trace_api.SpanContext] = None, sampler: Optional[sampling.Sampler] = None, trace_config: None = None, # TODO - resource: None = None, + resource: Resource = Resource.create_empty(), attributes: types.Attributes = None, # TODO events: Sequence[Event] = None, # TODO links: Sequence[trace_api.Link] = (), From c88e585b8a4c610784a5214c2a0bd3fdf5e0783e Mon Sep 17 00:00:00 2001 From: Daisuke Taniwaki Date: Sun, 24 May 2020 12:06:32 +0900 Subject: [PATCH 0359/1517] sdk: Add a unit test for to_json (#725) Adding a unit test to the to_json method in the span. Co-authored-by: Yusuke Tsutsumi --- opentelemetry-sdk/tests/trace/test_trace.py | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index e468652ec0..7e6ab49c39 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -904,3 +904,35 @@ def test_add_span_processor_after_span_creation(self): expected_list.append(span_event_end_fmt("SP1", "foo")) self.assertListEqual(spans_calls_list, expected_list) + + def test_to_json(self): + context = trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), + ) + span = trace.Span("span-name", context) + + self.assertEqual( + span.to_json(), + """{ + "name": "span-name", + "context": { + "trace_id": "0x000000000000000000000000deadbeef", + "span_id": "0x00000000deadbef0", + "trace_state": "{}" + }, + "kind": "SpanKind.INTERNAL", + "parent_id": null, + "start_time": null, + "end_time": null, + "attributes": {}, + "events": [], + "links": [] +}""", + ) + self.assertEqual( + span.to_json(indent=None), + '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "{}"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "attributes": {}, "events": [], "links": []}', + ) From b799e54dae8c23d005da2e192611b25dc9e5c508 Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Sat, 23 May 2020 23:07:52 -0400 Subject: [PATCH 0360/1517] docs: fix imports for pymongo and psycopg2 (#723) Incorrect import documentation in both pymongo and psycopg2. --- .../src/opentelemetry/ext/psycopg2/__init__.py | 2 +- .../src/opentelemetry/ext/pymongo/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py index 582d147320..c82343f4de 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py @@ -26,7 +26,7 @@ import psycopg2 from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.trace.ext.psycopg2 import Psycopg2Instrumentor + from opentelemetry.ext.psycopg2 import Psycopg2Instrumentor trace.set_tracer_provider(TracerProvider()) diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index ee61f6d4f9..ebe52ed5f1 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -26,7 +26,7 @@ from pymongo import MongoClient from opentelemetry import trace from opentelemetry.trace import TracerProvider - from opentelemetry.trace.ext.pymongo import PymongoInstrumentor + from opentelemetry.ext.pymongo import PymongoInstrumentor trace.set_tracer_provider(TracerProvider()) From 93e7db8fb28537ee513d550d3efcafe258bc5f2f Mon Sep 17 00:00:00 2001 From: HiveTraum Date: Mon, 25 May 2020 20:29:51 +0500 Subject: [PATCH 0361/1517] ext/django: django downgrade to 1.10 (#717) Adding support for django 1.10+ --- ext/opentelemetry-ext-django/setup.cfg | 6 ++++-- .../src/opentelemetry/ext/django/middleware.py | 7 +++++-- tox.ini | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ext/opentelemetry-ext-django/setup.cfg b/ext/opentelemetry-ext-django/setup.cfg index 85d4d88833..b5847272c5 100644 --- a/ext/opentelemetry-ext-django/setup.cfg +++ b/ext/opentelemetry-ext-django/setup.cfg @@ -28,17 +28,19 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.6 +python_requires = >=3.4 package_dir= =src packages=find_namespace: install_requires = - django >= 2.2 + django >= 1.10 opentelemetry-ext-wsgi == 0.8.dev0 opentelemetry-auto-instrumentation == 0.8.dev0 opentelemetry-api == 0.8.dev0 diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py index 5974c5e503..912ce0f1a2 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py @@ -14,8 +14,6 @@ from logging import getLogger -from django.utils.deprecation import MiddlewareMixin - from opentelemetry.context import attach, detach from opentelemetry.ext.django.version import __version__ from opentelemetry.ext.wsgi import ( @@ -26,6 +24,11 @@ from opentelemetry.propagators import extract from opentelemetry.trace import SpanKind, get_tracer +try: + from django.utils.deprecation import MiddlewareMixin +except ImportError: + MiddlewareMixin = object + _logger = getLogger(__name__) diff --git a/tox.ini b/tox.ini index 8f44a47edf..918e99f7ca 100644 --- a/tox.ini +++ b/tox.ini @@ -29,7 +29,7 @@ envlist = pypy3-test-ext-aiohttp-client ; opentelemetry-ext-django - py3{6,7,8}-test-ext-django + py3{4,5,6,7,8}-test-ext-django pypy3-test-ext-django ; opentelemetry-ext-dbapi From e0fd43558f9c7acfaf740907cca55d8709d323f8 Mon Sep 17 00:00:00 2001 From: Vladimir Mihailenco Date: Tue, 26 May 2020 19:39:50 +0300 Subject: [PATCH 0362/1517] fix: Fix error message (#729) --- .../src/opentelemetry/sdk/trace/export/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index fbe30720ea..f06f5d2914 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -120,7 +120,7 @@ def __init__( if max_export_batch_size > max_queue_size: raise ValueError( - "max_export_batch_size must be less than and equal to max_export_batch_size." + "max_export_batch_size must be less than and equal to max_queue_size." ) self.span_exporter = span_exporter From b51a5188c9c4d15e20b85759227dc39464aa31d0 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 26 May 2020 11:53:08 -0600 Subject: [PATCH 0363/1517] chore: Remove commented out test case (#731) Fixes #730 --- .../test_opencensusexporter_functional.py | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py b/ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py index 929002a4c7..af0f049bb0 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py @@ -58,72 +58,3 @@ def test_export(self): for export_status in self.span_processor.export_status: self.assertEqual(export_status.name, "SUCCESS") self.assertEqual(export_status.value, 0) - - -# FIXME This test fails because of an issue in the OpenCensus collector -# reported here: -# https://github.com/census-instrumentation/opencensus-service/issues/641 -# Uncomment this test when this issue gets fixed. - -# from time import sleep -# from opentelemetry.ext.opencensusexporter.metrics_exporter import ( -# OpenCensusMetricsExporter, -# ) -# from opentelemetry.sdk.metrics import Counter, MeterProvider -# from opentelemetry.sdk.metrics.export.controller import PushController - -# from opentelemetry import metrics -# -# -# class ExportStatusMetricController(PushController): -# def __init__(self, *args, **kwargs): -# super().__init__(*args, **kwargs) -# self.export_status = [] -# -# def run(self): -# while not self.finished.wait(self.interval): -# self.tick() -# -# def tick(self): -# # Collect all of the meter's metrics to be exported -# self.meter.collect() -# token = attach(set_value("suppress_instrumentation", True)) -# # Export the given metrics in the batcher -# self.export_status.append( -# self.exporter.export(self.meter.batcher.checkpoint_set()) -# ) -# detach(token) -# # Perform post-exporting logic based on batcher configuration -# self.meter.batcher.finished_collection() -# -# -# class TestOpenCensusMetricsExporter(TestBase): -# def setUp(self): -# super().setUp() -# -# metrics.set_meter_provider(MeterProvider()) -# self.meter = metrics.get_meter(__name__) -# self.controller = ExportStatusMetricController( -# self.meter, -# OpenCensusMetricsExporter( -# service_name="basic-service", endpoint="localhost:55678" -# ), -# 1, -# ) -# -# def test_export(self): -# -# self.meter.create_metric( -# name="requests", -# description="number of requests", -# unit="1", -# value_type=int, -# metric_type=Counter, -# label_keys=("environment",), -# ).add(25, {"environment": "staging"}) -# -# sleep(2) -# -# self.assertEqual(len(self.controller.export_status), 1) -# self.assertEqual(self.controller.export_status[0].name, "SUCCESS") -# self.assertEqual(self.controller.export_status[0].value, 0) From 027f61fcd2bd2593fc0cbd30b2260f2c7ab806ef Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 26 May 2020 14:05:07 -0700 Subject: [PATCH 0364/1517] infra: adding step to publish action (#733) Adding a step to test that uploading packages to pypi should work. This should prevent issues that occurred in the release of 0.7.0 from happening where a classifier was set incorrectly (3 - Beta), causing pypi to return a 400 and fail publishing packages. --- .github/workflows/publish.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fca8fcca04..13ee1e24c4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,10 +14,24 @@ jobs: python-version: '3.7' - name: Build wheels run: ./scripts/build.sh + - name: Install twine + run: | + pip install twine + # The step below publishes to testpypi in order to catch any issues + # with the package configuration that would cause a failure to upload + # to pypi. One example of such a failure is if a classifier is + # rejected by pypi (e.g "3 - Beta"). This would cause a failure during the + # middle of the package upload causing the action to fail, and certain packages + # might have already been updated, this would be bad. + - name: Publish to TestPyPI + env: + TWINE_USERNAME: ${{ secrets.test_pypi_username }} + TWINE_PASSWORD: ${{ secrets.test_pypi_password }} + run: | + twine upload --repository testpypi --skip-existing --verbose dist/* - name: Publish to PyPI env: TWINE_USERNAME: '__token__' TWINE_PASSWORD: ${{ secrets.pypi_password }} run: | - pip install twine twine upload --skip-existing --verbose dist/* From 1ee85c8e583074830c61df711d63029fef11e239 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 26 May 2020 14:34:21 -0700 Subject: [PATCH 0365/1517] infra: removing workflow, updating script (#734) More work to improve release process: - adding RELEASING.md file to describe release process - updated prepare_release.sh script to allow it to be run manually by maintainers - removed workflow to automatically publish PR, current recommendation from otel technical committee is not to use bot accounts --- .github/workflows/prepare-release.yml | 28 --------- RELEASING.md | 82 +++++++++++++++++++++++++++ scripts/prepare_release.sh | 17 ++++-- 3 files changed, 94 insertions(+), 33 deletions(-) delete mode 100644 .github/workflows/prepare-release.yml create mode 100644 RELEASING.md diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml deleted file mode 100644 index 95ed9f466e..0000000000 --- a/.github/workflows/prepare-release.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: prepare-release -on: - push: - branches: - - 'release/**' - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Get the version - id: get_version - run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} - - - name: Prepare the release - id: update - run: | - ./scripts/prepare_release.sh ${{ steps.get_version.outputs.VERSION }} - - - name: Create Pull Request - id: create-pr - uses: peter-evans/create-pull-request@v2.7.0 - with: - branch: ${{ steps.get_version.outputs.VERSION }}-auto - title: '[pre-release] Update changelogs, version [${{ steps.get_version.outputs.VERSION }}]' - if: ${{ steps.update.outputs.version_updated == 1 }} diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000000..d4749dedbe --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,82 @@ +# Releasing OpenTelemetry Packages (for maintainers only) +This document explains how to publish all OT modules at version x.y.z. Ensure that you’re following semver when choosing a version number. + +Release Process: +* [Create a new branch](#create-a-new-branch) +* [Open a Pull Request](#open-a-pull-request) +* [Create a Release](#Create-a-Release) +* [Move stable tag](#Move-stable-tag) +* [Update master](#Update-master) +* [Check PyPI](#Check-PyPI) +* [Troubleshooting](#troubleshooting) + + +## Create a new branch +The following script does the following: +- update master locally +- creates a new release branch `release/` +- updates version and changelog files +- commits the change to a new branch `release/-auto` + +*NOTE: This script was run by a GitHub Action but required the Action bot to be excluded from the CLA check, which it currently is not.* + +```bash +./scripts/prepare_release.sh 0.7b0 +``` + +## Open a Pull Request + +The PR should be opened from the `release/-auto` branch created as part of running `prepare_release.sh` in the steps above. + +## Create a Release + +- Create the GH release from the release branch, using a new tag for this micro version, e.g. `v0.7.0` +- Copy the changelogs from all packages that changed into the release notes (and reformat to remove hard line wraps) + + +## Check PyPI + +This should be handled automatically on release by the [publish action](https://github.com/open-telemetry/opentelemetry-python/blob/master/.github/workflows/publish.yml). + + - Check the [action logs](https://github.com/open-telemetry/opentelemetry-python/actions?query=workflow%3APublish) to make sure packages have been uploaded to PyPI +- Check the release history (e.g. https://pypi.org/project/opentelemetry-api/#history) on PyPI + +If for some reason the action failed, see [Publish failed](#publish-failed) below + +## Move stable tag + +This will ensure the docs are pointing at the stable release. + +```bash +git tag -d stable +git tag stable +git push origin stable +``` + +## Update master + +Ensure the version and changelog updates have been applied to master. + +```bash +# checkout a new branch from master +git checkout -b v0.7b0-master-update +# cherry pick the change from the release branch +git cherry-pick $(git log -n 1 origin/release/0.7b0 --format="%H") +# update the version number, make it a "dev" greater than release number, e.g. 0.8.dev0 +perl -i -p -e 's/0.7b0/0.8.dev0/' $(git grep -l "0.7b0" | grep -vi CHANGELOG) +# open a PR targeting master see #331 +git commit -m +``` + +## Troubleshooting + +### Publish failed + +If for some reason the action failed, do it manually: + +- To avoid pushing untracked changes, check out the repo in a new dir +- Switch to the release branch (important so we don't publish packages with "dev" versions) +- Build distributions with `./scripts/build.sh` +- Delete distributions we don't want to push (e.g. `testutil`) +- Push to PyPI as `twine upload --skip-existing --verbose dist/*` +- Double check PyPI! diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh index 73ee141092..bf8eb3e961 100755 --- a/scripts/prepare_release.sh +++ b/scripts/prepare_release.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash -x # # This script: # 1. parses the version number from the branch name @@ -72,10 +72,17 @@ function update_changelog() { fi } +# create the release branch +git checkout master +git reset --hard origin/master +git checkout -b release/${VERSION} +git push origin release/${VERSION} + +# create a temporary branch to create a PR for updated version and changelogs +git checkout -b release/${VERSION}-auto update_version_file update_changelog - -git config --local user.email "action@github.com" -git config --local user.name "GitHub Action" git commit -m "updating changelogs and version to ${VERSION}" -echo "::set-output name=version_updated::1" + +echo "Time to create a release, here's a sample title:" +echo "[pre-release] Update changelogs, version [${VERSION}]" From 06bab74f0f6710316f6e05ead2409e6f27aa5e58 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 26 May 2020 15:17:45 -0700 Subject: [PATCH 0366/1517] chore: amend changelogs (#737) --- ext/opentelemetry-ext-django/CHANGELOG.md | 2 ++ ext/opentelemetry-ext-grpc/CHANGELOG.md | 5 +++++ ext/opentelemetry-ext-jaeger/CHANGELOG.md | 3 +++ ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md | 3 +++ ext/opentelemetry-ext-zipkin/CHANGELOG.md | 3 +++ opentelemetry-api/CHANGELOG.md | 5 +++++ opentelemetry-sdk/CHANGELOG.md | 9 ++++++++- 7 files changed, 29 insertions(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-django/CHANGELOG.md b/ext/opentelemetry-ext-django/CHANGELOG.md index 43990fab16..f46a42c223 100644 --- a/ext/opentelemetry-ext-django/CHANGELOG.md +++ b/ext/opentelemetry-ext-django/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add support for django >= 1.10 (#717) + ## 0.7b1 Released 2020-05-12 diff --git a/ext/opentelemetry-ext-grpc/CHANGELOG.md b/ext/opentelemetry-ext-grpc/CHANGELOG.md index 3d9a300699..2bf8ae44af 100644 --- a/ext/opentelemetry-ext-grpc/CHANGELOG.md +++ b/ext/opentelemetry-ext-grpc/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +- lint: version of grpc causes lint issues + ([#696](https://github.com/open-telemetry/opentelemetry-python/pull/696)) + ## 0.6b0 Released 2020-03-30 diff --git a/ext/opentelemetry-ext-jaeger/CHANGELOG.md b/ext/opentelemetry-ext-jaeger/CHANGELOG.md index 879e6b0f84..b1e9e84485 100644 --- a/ext/opentelemetry-ext-jaeger/CHANGELOG.md +++ b/ext/opentelemetry-ext-jaeger/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Transform resource to tags when exporting + ([#645](https://github.com/open-telemetry/opentelemetry-python/pull/645)) + ## 0.6b0 Released 2020-03-30 diff --git a/ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md b/ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md index 0f6ed327bb..6662cf1b54 100644 --- a/ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md +++ b/ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Rename otcollector to opencensus + ([#695](https://github.com/open-telemetry/opentelemetry-python/pull/695)) + ## 0.5b0 Released 2020-03-16 diff --git a/ext/opentelemetry-ext-zipkin/CHANGELOG.md b/ext/opentelemetry-ext-zipkin/CHANGELOG.md index 2d0e17cdb0..49111e22c2 100644 --- a/ext/opentelemetry-ext-zipkin/CHANGELOG.md +++ b/ext/opentelemetry-ext-zipkin/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Transform resource to tags when exporting + ([#707](https://github.com/open-telemetry/opentelemetry-python/pull/707)) + ## 0.7b1 Released 2020-05-12 diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 8cdfad0fe0..daa6c7286a 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Handle boolean, integer and float values in Configuration + ([#662](https://github.com/open-telemetry/opentelemetry-python/pull/662)) +- bugfix: ensure status is always string + ([#640](https://github.com/open-telemetry/opentelemetry-python/pull/640)) + ## 0.7b1 Released 2020-05-12 diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 476908cf91..bece33dfca 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,7 +2,14 @@ ## Unreleased -- Validate span attribute types in SDK (#678) +- Validate span attribute types in SDK + ([#678](https://github.com/open-telemetry/opentelemetry-python/pull/678)) +- Specify to_json indent from arguments + ([#718](https://github.com/open-telemetry/opentelemetry-python/pull/718)) +- Span.resource will now default to an empty resource + ([#724](https://github.com/open-telemetry/opentelemetry-python/pull/724)) +- bugfix: Fix error message + ([#729](https://github.com/open-telemetry/opentelemetry-python/pull/729)) ## 0.7b1 From bc3ee8081e334ecbbc9033d17833cb5dda9d3d49 Mon Sep 17 00:00:00 2001 From: Emmanuel Courreges Date: Wed, 27 May 2020 07:38:42 +0200 Subject: [PATCH 0367/1517] aiohttp_client: fix propagation of traceparent in aiohttp_client (#709) Fix propagation of aiohttp client traces, which were not properly injected previously. Co-authored-by: Yusuke Tsutsumi --- .../src/opentelemetry/ext/aiohttp_client/__init__.py | 4 +--- .../tests/test_aiohttp_client_integration.py | 7 +++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py index 3c4e7f4eda..77dadbd645 100644 --- a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py +++ b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py @@ -175,9 +175,7 @@ async def on_request_start( trace.propagation.set_span_in_context(trace_config_ctx.span) ) - propagators.inject( - tracer, type(params.headers).__setitem__, params.headers - ) + propagators.inject(type(params.headers).__setitem__, params.headers) async def on_request_end( unused_session: aiohttp.ClientSession, diff --git a/ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py b/ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py index ae29801797..8cf5048e58 100644 --- a/ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -71,7 +71,8 @@ def _http_request( """Helper to start an aiohttp test server and send an actual HTTP request to it.""" async def do_request(): - async def default_handler(unused_request): + async def default_handler(request): + assert "traceparent" in request.headers return aiohttp.web.Response(status=int(status_code)) handler = request_handler or default_handler @@ -281,8 +282,9 @@ async def do_request(url): self.memory_exporter.clear() def test_timeout(self): - async def request_handler(unused_request): + async def request_handler(request): await asyncio.sleep(1) + assert "traceparent" in request.headers return aiohttp.web.Response() host, port = self._http_request( @@ -312,6 +314,7 @@ def test_too_many_redirects(self): async def request_handler(request): # Create a redirect loop. location = request.url + assert "traceparent" in request.headers raise aiohttp.web.HTTPFound(location=location) host, port = self._http_request( From 334a534687275f84e5719fcec638b24ab9f63f4e Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 27 May 2020 20:54:58 +0530 Subject: [PATCH 0368/1517] Introduce a bootstrap command to auto-install packages (#650) This commit introduces a new boostrap command that is shipped as part of the opentelemetry-auto-instrumentation package. The command detects installed libraries and installs the relevant auto-instrumentation packages. --- .../CHANGELOG.md | 3 + opentelemetry-auto-instrumentation/setup.cfg | 1 + .../auto_instrumentation/__init__.py | 21 +- .../auto_instrumentation/bootstrap.py | 203 ++++++++++++++++++ .../tests/test_bootstrap.py | 125 +++++++++++ 5 files changed, 350 insertions(+), 3 deletions(-) create mode 100644 opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/bootstrap.py create mode 100644 opentelemetry-auto-instrumentation/tests/test_bootstrap.py diff --git a/opentelemetry-auto-instrumentation/CHANGELOG.md b/opentelemetry-auto-instrumentation/CHANGELOG.md index 028b84877d..f637c6f568 100644 --- a/opentelemetry-auto-instrumentation/CHANGELOG.md +++ b/opentelemetry-auto-instrumentation/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add a new bootstrap command that enables automatic instrument installations. + ([#650](https://github.com/open-telemetry/opentelemetry-python/pull/650)) + ## 0.7b1 Released 2020-05-12 diff --git a/opentelemetry-auto-instrumentation/setup.cfg b/opentelemetry-auto-instrumentation/setup.cfg index 96da015f86..9829c84134 100644 --- a/opentelemetry-auto-instrumentation/setup.cfg +++ b/opentelemetry-auto-instrumentation/setup.cfg @@ -49,3 +49,4 @@ where = src [options.entry_points] console_scripts = opentelemetry-auto-instrumentation = opentelemetry.auto_instrumentation.auto_instrumentation:run + opentelemetry-bootstrap = opentelemetry.auto_instrumentation.bootstrap:run diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py index 6432b1209b..590bac8b3c 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py @@ -13,10 +13,11 @@ # limitations under the License. """ -Usage ------ -This package provides a command that automatically instruments a program: +This package provides a couple of commands that help automatically instruments a program: + +opentelemetry-auto-instrumentation +----------------------------------- :: @@ -25,4 +26,18 @@ The code in ``program.py`` needs to use one of the packages for which there is an OpenTelemetry integration. For a list of the available integrations please check :doc:`here <../../index>`. + + +opentelemetry-bootstrap +------------------------ + +:: + + opentelemetry-bootstrap --action=install|requirements + +This commands inspects the active Python site-packages and figures out which +instrumentation packages the user might want to install. By default it prints out +a list of the suggested instrumentation packages which can be added to a requirements.txt +file. It also supports installing the suggested packages when run with :code:`--action=install` +flag. """ diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/bootstrap.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/bootstrap.py new file mode 100644 index 0000000000..e313ca9631 --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/bootstrap.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 + +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import pkgutil +import subprocess +import sys +from logging import getLogger + +logger = getLogger(__file__) + + +# target library to desired instrumentor path/versioned package name +instrumentations = { + "dbapi": "opentelemetry-ext-dbapi>=0.8b0", + "django": "opentelemetry-ext-django>=0.8b0", + "flask": "opentelemetry-ext-flask>=0.8b0", + "grpc": "opentelemetry-ext-grpc>=0.8b0", + "requests": "opentelemetry-ext-requests>=0.8b0", + "jinja2": "opentelemetry-ext-jinja2>=0.8b0", + "mysql": "opentelemetry-ext-mysql>=0.8b0", + "psycopg2": "opentelemetry-ext-psycopg2>=0.8b0", + "pymongo": "opentelemetry-ext-pymongo>=0.8b0", + "pymysql": "opentelemetry-ext-pymysql>=0.8b0", + "redis": "opentelemetry-ext-redis>=0.8b0", + "sqlalchemy": "opentelemetry-ext-sqlalchemy>=0.8b0", + "wsgi": "opentelemetry-ext-wsgi>=0.8b0", +} + +# relevant instrumentors and tracers to uninstall and check for conflicts for target libraries +libraries = { + "dbapi": ("opentelemetry-ext-dbapi",), + "django": ("opentelemetry-ext-django",), + "flask": ("opentelemetry-ext-flask",), + "grpc": ("opentelemetry-ext-grpc",), + "requests": ("opentelemetry-ext-requests",), + "jinja2": ("opentelemetry-ext-jinja2",), + "mysql": ("opentelemetry-ext-mysql",), + "psycopg2": ("opentelemetry-ext-psycopg2",), + "pymongo": ("opentelemetry-ext-pymongo",), + "pymysql": ("opentelemetry-ext-pymysql",), + "redis": ("opentelemetry-ext-redis",), + "sqlalchemy": ("opentelemetry-ext-sqlalchemy",), + "wsgi": ("opentelemetry-ext-wsgi",), +} + + +def _install_package(library, instrumentation): + """ + Ensures that desired version is installed w/o upgrading its dependencies + by uninstalling where necessary (if `target` is not provided). + + + OpenTelemetry auto-instrumentation packages often have traced libraries + as instrumentation dependency (e.g. flask for opentelemetry-ext-flask), + so using -I on library could cause likely undesired Flask upgrade. + Using --no-dependencies alone would leave potential for nonfunctional + installations. + """ + pip_list = _sys_pip_freeze() + for package in libraries[library]: + if "{}==".format(package).lower() in pip_list: + logger.info( + "Existing %s installation detected. Uninstalling.", package + ) + _sys_pip_uninstall(package) + _sys_pip_install(instrumentation) + + +def _syscall(func): + def wrapper(package=None): + try: + if package: + return func(package) + return func() + except subprocess.SubprocessError as exp: + cmd = getattr(exp, "cmd", None) + if cmd: + msg = 'Error calling system command "{0}"'.format( + " ".join(cmd) + ) + if package: + msg = '{0} for package "{1}"'.format(msg, package) + raise RuntimeError(msg) + + return wrapper + + +@_syscall +def _sys_pip_freeze(): + return ( + subprocess.check_output([sys.executable, "-m", "pip", "freeze"]) + .decode() + .lower() + ) + + +@_syscall +def _sys_pip_install(package): + # explicit upgrade strategy to override potential pip config + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "-U", + "--upgrade-strategy", + "only-if-needed", + package, + ] + ) + + +@_syscall +def _sys_pip_uninstall(package): + subprocess.check_call( + [sys.executable, "-m", "pip", "uninstall", "-y", package] + ) + + +def _pip_check(): + """Ensures none of the instrumentations have dependency conflicts. + Clean check reported as: + 'No broken requirements found.' + Dependency conflicts are reported as: + 'opentelemetry-ext-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' + To not be too restrictive, we'll only check for relevant packages. + """ + check_pipe = subprocess.Popen( + [sys.executable, "-m", "pip", "check"], stdout=subprocess.PIPE + ) + pip_check = check_pipe.communicate()[0].decode() + pip_check_lower = pip_check.lower() + for package_tup in libraries.values(): + for package in package_tup: + if package.lower() in pip_check_lower: + raise RuntimeError( + "Dependency conflict found: {}".format(pip_check) + ) + + +def _is_installed(library): + return library in sys.modules or pkgutil.find_loader(library) is not None + + +def _find_installed_libraries(): + return {k: v for k, v in instrumentations.items() if _is_installed(k)} + + +def _run_requirements(packages): + print("\n".join(packages.values()), end="") + + +def _run_install(packages): + for pkg, inst in packages.items(): + _install_package(pkg, inst) + + _pip_check() + + +def run() -> None: + action_install = "install" + action_requirements = "requirements" + + parser = argparse.ArgumentParser( + description=""" + opentelemetry-bootstrap detects installed libraries and automatically + installs the relevant instrumentation packages for them. + """ + ) + parser.add_argument( + "-a", + "--action", + choices=[action_install, action_requirements], + default=action_requirements, + help=""" + install - uses pip to install the new requirements using to the + currently active site-package. + requirements - prints out the new requirements to stdout. Action can + be piped and appended to a requirements.txt file. + """, + ) + args = parser.parse_args() + + cmd = { + action_install: _run_install, + action_requirements: _run_requirements, + }[args.action] + cmd(_find_installed_libraries()) diff --git a/opentelemetry-auto-instrumentation/tests/test_bootstrap.py b/opentelemetry-auto-instrumentation/tests/test_bootstrap.py new file mode 100644 index 0000000000..8af684084b --- /dev/null +++ b/opentelemetry-auto-instrumentation/tests/test_bootstrap.py @@ -0,0 +1,125 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from functools import reduce +from io import StringIO +from random import sample +from unittest import TestCase +from unittest.mock import call, patch + +from opentelemetry.auto_instrumentation import bootstrap + + +def sample_packages(packages, rate): + sampled = sample(list(packages), int(len(packages) * rate),) + return {k: v for k, v in packages.items() if k in sampled} + + +class TestBootstrap(TestCase): + + installed_libraries = {} + installed_instrumentations = {} + + @classmethod + def setUpClass(cls): + # select random 60% of instrumentations + cls.installed_libraries = sample_packages( + bootstrap.instrumentations, 0.6 + ) + + # treat 50% of sampled packages as pre-installed + cls.installed_instrumentations = sample_packages( + cls.installed_libraries, 0.5 + ) + + cls.pkg_patcher = patch( + "opentelemetry.auto_instrumentation.bootstrap._find_installed_libraries", + return_value=cls.installed_libraries, + ) + + pip_freeze_output = [] + for inst in cls.installed_instrumentations.values(): + inst = inst.replace(">=", "==") + if "==" not in inst: + inst = "{}==x.y".format(inst) + pip_freeze_output.append(inst) + + cls.pip_freeze_patcher = patch( + "opentelemetry.auto_instrumentation.bootstrap._sys_pip_freeze", + return_value="\n".join(pip_freeze_output), + ) + cls.pip_install_patcher = patch( + "opentelemetry.auto_instrumentation.bootstrap._sys_pip_install", + ) + cls.pip_uninstall_patcher = patch( + "opentelemetry.auto_instrumentation.bootstrap._sys_pip_uninstall", + ) + cls.pip_check_patcher = patch( + "opentelemetry.auto_instrumentation.bootstrap._pip_check", + ) + + cls.pkg_patcher.start() + cls.mock_pip_freeze = cls.pip_freeze_patcher.start() + cls.mock_pip_install = cls.pip_install_patcher.start() + cls.mock_pip_uninstall = cls.pip_uninstall_patcher.start() + cls.mock_pip_check = cls.pip_check_patcher.start() + + @classmethod + def tearDownClass(cls): + cls.pip_check_patcher.start() + cls.pip_uninstall_patcher.start() + cls.pip_install_patcher.start() + cls.pip_freeze_patcher.start() + cls.pkg_patcher.stop() + + @patch("sys.argv", ["bootstrap", "-a", "pipenv"]) + def test_run_unknown_cmd(self): + with self.assertRaises(SystemExit): + bootstrap.run() + + @patch("sys.argv", ["bootstrap", "-a", "requirements"]) + def test_run_cmd_print(self): + with patch("sys.stdout", new=StringIO()) as fake_out: + bootstrap.run() + self.assertEqual( + fake_out.getvalue(), + "\n".join(self.installed_libraries.values()), + ) + + @patch("sys.argv", ["bootstrap", "-a", "install"]) + def test_run_cmd_install(self): + bootstrap.run() + + self.assertEqual( + self.mock_pip_freeze.call_count, len(self.installed_libraries) + ) + + to_uninstall = reduce( + lambda x, y: x + y, + [ + pkgs + for lib, pkgs in bootstrap.libraries.items() + if lib in self.installed_instrumentations + ], + ) + self.mock_pip_uninstall.assert_has_calls( + [call(i) for i in to_uninstall], any_order=True + ) + + self.mock_pip_install.assert_has_calls( + [call(i) for i in self.installed_libraries.values()], + any_order=True, + ) + self.assertEqual(self.mock_pip_check.call_count, 1) From 64b3cf205570243757509a316c4cdff711de3e96 Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Wed, 27 May 2020 12:17:35 -0400 Subject: [PATCH 0369/1517] asgi: Add ASGI middleware (#716) Adding an ASGI extension. Co-authored-by: Emil Madsen Co-authored-by: alrex Co-authored-by: Florimond Manca --- docs-requirements.txt | 1 + docs/ext/asgi/asgi.rst | 10 + ext/opentelemetry-ext-asgi/CHANGELOG.md | 5 + ext/opentelemetry-ext-asgi/README.rst | 60 +++ ext/opentelemetry-ext-asgi/setup.cfg | 50 +++ ext/opentelemetry-ext-asgi/setup.py | 26 ++ .../src/opentelemetry/ext/asgi/__init__.py | 211 +++++++++++ .../src/opentelemetry/ext/asgi/version.py | 15 + ext/opentelemetry-ext-asgi/tests/__init__.py | 0 .../tests/test_asgi_middleware.py | 345 ++++++++++++++++++ scripts/coverage.sh | 3 + .../src/opentelemetry/test/asgitestutil.py | 76 ++++ .../src/opentelemetry/test/spantestutil.py | 43 +++ .../src/opentelemetry/test/wsgitestutil.py | 43 +-- tox.ini | 8 +- 15 files changed, 869 insertions(+), 27 deletions(-) create mode 100644 docs/ext/asgi/asgi.rst create mode 100644 ext/opentelemetry-ext-asgi/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-asgi/README.rst create mode 100644 ext/opentelemetry-ext-asgi/setup.cfg create mode 100644 ext/opentelemetry-ext-asgi/setup.py create mode 100644 ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/__init__.py create mode 100644 ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py create mode 100644 ext/opentelemetry-ext-asgi/tests/__init__.py create mode 100644 ext/opentelemetry-ext-asgi/tests/test_asgi_middleware.py create mode 100644 tests/util/src/opentelemetry/test/asgitestutil.py create mode 100644 tests/util/src/opentelemetry/test/spantestutil.py diff --git a/docs-requirements.txt b/docs-requirements.txt index 55b647402f..358316fa7e 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -3,6 +3,7 @@ sphinx-rtd-theme~=0.4 sphinx-autodoc-typehints~=1.10.2 # Required by ext packages +asgiref~=3.0 ddtrace>=0.34.0 aiohttp ~= 3.0 Deprecated>=1.2.6 diff --git a/docs/ext/asgi/asgi.rst b/docs/ext/asgi/asgi.rst new file mode 100644 index 0000000000..82c9833069 --- /dev/null +++ b/docs/ext/asgi/asgi.rst @@ -0,0 +1,10 @@ +opentelemetry.ext.asgi package +============================== + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.asgi + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-asgi/CHANGELOG.md b/ext/opentelemetry-ext-asgi/CHANGELOG.md new file mode 100644 index 0000000000..7936dd50fc --- /dev/null +++ b/ext/opentelemetry-ext-asgi/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Add ASGI middleware ([#716](https://github.com/open-telemetry/opentelemetry-python/pull/716)) diff --git a/ext/opentelemetry-ext-asgi/README.rst b/ext/opentelemetry-ext-asgi/README.rst new file mode 100644 index 0000000000..bc286e5c8b --- /dev/null +++ b/ext/opentelemetry-ext-asgi/README.rst @@ -0,0 +1,60 @@ +OpenTelemetry ASGI Middleware +============================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-asgi.svg + :target: https://pypi.org/project/opentelemetry-ext-asgi/ + + +This library provides a ASGI middleware that can be used on any ASGI framework +(such as Django, Starlette, FastAPI or Quart) to track requests timing through OpenTelemetry. + +Installation +------------ + +:: + + pip install opentelemetry-ext-asgi + + +Usage (Quart) +------------- + +.. code-block:: python + + from quart import Quart + from opentelemetry.ext.asgi import OpenTelemetryMiddleware + + app = Quart(__name__) + app.asgi_app = OpenTelemetryMiddleware(app.asgi_app) + + @app.route("/") + async def hello(): + return "Hello!" + + if __name__ == "__main__": + app.run(debug=True) + + +Usage (Django 3.0) +------------------ + +Modify the application's ``asgi.py`` file as shown below. + +.. code-block:: python + + import os + from django.core.asgi import get_asgi_application + from opentelemetry.ext.asgi import OpenTelemetryMiddleware + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'asgi_example.settings') + + application = get_asgi_application() + application = OpenTelemetryMiddleware(application) + + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-asgi/setup.cfg b/ext/opentelemetry-ext-asgi/setup.cfg new file mode 100644 index 0000000000..f834737735 --- /dev/null +++ b/ext/opentelemetry-ext-asgi/setup.cfg @@ -0,0 +1,50 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-asgi +description = ASGI Middleware for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-asgi +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.8.dev0 + asgiref ~= 3.0 + +[options.extras_require] +test = + opentelemetry-ext-testutil + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-asgi/setup.py b/ext/opentelemetry-ext-asgi/setup.py new file mode 100644 index 0000000000..8bf19bd422 --- /dev/null +++ b/ext/opentelemetry-ext-asgi/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "asgi", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/__init__.py b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/__init__.py new file mode 100644 index 0000000000..69c30848da --- /dev/null +++ b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/__init__.py @@ -0,0 +1,211 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The opentelemetry-ext-asgi package provides an ASGI middleware that can be used +on any ASGI framework (such as Django-channels / Quart) to track requests +timing through OpenTelemetry. +""" + +import operator +import typing +import urllib +from functools import wraps + +from asgiref.compatibility import guarantee_single_callable + +from opentelemetry import context, propagators, trace +from opentelemetry.ext.asgi.version import __version__ # noqa +from opentelemetry.trace.status import Status, StatusCanonicalCode + + +def get_header_from_scope(scope: dict, header_name: str) -> typing.List[str]: + """Retrieve a HTTP header value from the ASGI scope. + + Returns: + A list with a single string with the header value if it exists, else an empty list. + """ + headers = scope.get("headers") + return [ + value.decode("utf8") + for (key, value) in headers + if key.decode("utf8") == header_name + ] + + +def http_status_to_canonical_code(code: int, allow_redirect: bool = True): + # pylint:disable=too-many-branches,too-many-return-statements + if code < 100: + return StatusCanonicalCode.UNKNOWN + if code <= 299: + return StatusCanonicalCode.OK + if code <= 399: + if allow_redirect: + return StatusCanonicalCode.OK + return StatusCanonicalCode.DEADLINE_EXCEEDED + if code <= 499: + if code == 401: # HTTPStatus.UNAUTHORIZED: + return StatusCanonicalCode.UNAUTHENTICATED + if code == 403: # HTTPStatus.FORBIDDEN: + return StatusCanonicalCode.PERMISSION_DENIED + if code == 404: # HTTPStatus.NOT_FOUND: + return StatusCanonicalCode.NOT_FOUND + if code == 429: # HTTPStatus.TOO_MANY_REQUESTS: + return StatusCanonicalCode.RESOURCE_EXHAUSTED + return StatusCanonicalCode.INVALID_ARGUMENT + if code <= 599: + if code == 501: # HTTPStatus.NOT_IMPLEMENTED: + return StatusCanonicalCode.UNIMPLEMENTED + if code == 503: # HTTPStatus.SERVICE_UNAVAILABLE: + return StatusCanonicalCode.UNAVAILABLE + if code == 504: # HTTPStatus.GATEWAY_TIMEOUT: + return StatusCanonicalCode.DEADLINE_EXCEEDED + return StatusCanonicalCode.INTERNAL + return StatusCanonicalCode.UNKNOWN + + +def collect_request_attributes(scope): + """Collects HTTP request attributes from the ASGI scope and returns a + dictionary to be used as span creation attributes.""" + server = scope.get("server") or ["0.0.0.0", 80] + port = server[1] + server_host = server[0] + (":" + str(port) if port != 80 else "") + full_path = scope.get("root_path", "") + scope.get("path", "") + http_url = scope.get("scheme", "http") + "://" + server_host + full_path + query_string = scope.get("query_string") + if query_string and http_url: + if isinstance(query_string, bytes): + query_string = query_string.decode("utf8") + http_url = http_url + ("?" + urllib.parse.unquote(query_string)) + + result = { + "component": scope["type"], + "http.scheme": scope.get("scheme"), + "http.host": server_host, + "host.port": port, + "http.flavor": scope.get("http_version"), + "http.target": scope.get("path"), + "http.url": http_url, + } + http_method = scope.get("method") + if http_method: + result["http.method"] = http_method + http_host_value = ",".join(get_header_from_scope(scope, "host")) + if http_host_value: + result["http.server_name"] = http_host_value + http_user_agent = get_header_from_scope(scope, "user-agent") + if len(http_user_agent) > 0: + result["http.user_agent"] = http_user_agent[0] + + if "client" in scope and scope["client"] is not None: + result["net.peer.ip"] = scope.get("client")[0] + result["net.peer.port"] = scope.get("client")[1] + + # remove None values + result = {k: v for k, v in result.items() if v is not None} + + return result + + +def set_status_code(span, status_code): + """Adds HTTP response attributes to span using the status_code argument.""" + try: + status_code = int(status_code) + except ValueError: + span.set_status( + Status( + StatusCanonicalCode.UNKNOWN, + "Non-integer HTTP status: " + repr(status_code), + ) + ) + else: + span.set_attribute("http.status_code", status_code) + span.set_status(Status(http_status_to_canonical_code(status_code))) + + +def get_default_span_name(scope): + """Default implementation for name_callback""" + method_or_path = scope.get("method") or scope.get("path") + + return method_or_path + + +class OpenTelemetryMiddleware: + """The ASGI application middleware. + + This class is an ASGI middleware that starts and annotates spans for any + requests it is invoked with. + + Args: + app: The ASGI application callable to forward requests to. + name_callback: Callback which calculates a generic span name for an + incoming HTTP request based on the ASGI scope. + Optional: Defaults to get_default_span_name. + """ + + def __init__(self, app, name_callback=None): + self.app = guarantee_single_callable(app) + self.tracer = trace.get_tracer(__name__, __version__) + self.name_callback = name_callback or get_default_span_name + + async def __call__(self, scope, receive, send): + """The ASGI application + + Args: + scope: A ASGI environment. + receive: An awaitable callable yielding dictionaries + send: An awaitable callable taking a single dictionary as argument. + """ + if scope["type"] not in ("http", "websocket"): + return await self.app(scope, receive, send) + + token = context.attach( + propagators.extract(get_header_from_scope, scope) + ) + span_name = self.name_callback(scope) + + try: + with self.tracer.start_as_current_span( + span_name + " asgi", + kind=trace.SpanKind.SERVER, + attributes=collect_request_attributes(scope), + ): + + @wraps(receive) + async def wrapped_receive(): + with self.tracer.start_as_current_span( + span_name + " asgi." + scope["type"] + ".receive" + ) as receive_span: + message = await receive() + if message["type"] == "websocket.receive": + set_status_code(receive_span, 200) + receive_span.set_attribute("type", message["type"]) + return message + + @wraps(send) + async def wrapped_send(message): + with self.tracer.start_as_current_span( + span_name + " asgi." + scope["type"] + ".send" + ) as send_span: + if message["type"] == "http.response.start": + status_code = message["status"] + set_status_code(send_span, status_code) + elif message["type"] == "websocket.send": + set_status_code(send_span, 200) + send_span.set_attribute("type", message["type"]) + await send(message) + + await self.app(scope, wrapped_receive, wrapped_send) + finally: + context.detach(token) diff --git a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py new file mode 100644 index 0000000000..bcf6a35777 --- /dev/null +++ b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-asgi/tests/__init__.py b/ext/opentelemetry-ext-asgi/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-asgi/tests/test_asgi_middleware.py b/ext/opentelemetry-ext-asgi/tests/test_asgi_middleware.py new file mode 100644 index 0000000000..edc90444a7 --- /dev/null +++ b/ext/opentelemetry-ext-asgi/tests/test_asgi_middleware.py @@ -0,0 +1,345 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import unittest +import unittest.mock as mock + +import opentelemetry.ext.asgi as otel_asgi +from opentelemetry import trace as trace_api +from opentelemetry.test.asgitestutil import ( + AsgiTestBase, + setup_testing_defaults, +) + + +async def http_app(scope, receive, send): + message = await receive() + assert scope["type"] == "http" + if message.get("type") == "http.request": + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [[b"Content-Type", b"text/plain"]], + } + ) + await send({"type": "http.response.body", "body": b"*"}) + + +async def websocket_app(scope, receive, send): + assert scope["type"] == "websocket" + while True: + message = await receive() + if message.get("type") == "websocket.connect": + await send({"type": "websocket.accept"}) + + if message.get("type") == "websocket.receive": + if message.get("text") == "ping": + await send({"type": "websocket.send", "text": "pong"}) + + if message.get("type") == "websocket.disconnect": + break + + +async def simple_asgi(scope, receive, send): + assert isinstance(scope, dict) + if scope["type"] == "http": + await http_app(scope, receive, send) + elif scope["type"] == "websocket": + await websocket_app(scope, receive, send) + + +async def error_asgi(scope, receive, send): + assert isinstance(scope, dict) + assert scope["type"] == "http" + message = await receive() + if message.get("type") == "http.request": + try: + raise ValueError + except ValueError: + scope["hack_exc_info"] = sys.exc_info() + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [[b"Content-Type", b"text/plain"]], + } + ) + await send({"type": "http.response.body", "body": b"*"}) + + +class TestAsgiApplication(AsgiTestBase): + def validate_outputs(self, outputs, error=None, modifiers=None): + # Ensure modifiers is a list + modifiers = modifiers or [] + # Check for expected outputs + self.assertEqual(len(outputs), 2) + response_start = outputs[0] + response_body = outputs[1] + self.assertEqual(response_start["type"], "http.response.start") + self.assertEqual(response_body["type"], "http.response.body") + + # Check http response body + self.assertEqual(response_body["body"], b"*") + + # Check http response start + self.assertEqual(response_start["status"], 200) + self.assertEqual( + response_start["headers"], [[b"Content-Type", b"text/plain"]] + ) + + exc_info = self.scope.get("hack_exc_info") + if error: + self.assertIs(exc_info[0], error) + self.assertIsInstance(exc_info[1], error) + self.assertIsNotNone(exc_info[2]) + else: + self.assertIsNone(exc_info) + + # Check spans + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 4) + expected = [ + { + "name": "GET asgi.http.receive", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": {"type": "http.request"}, + }, + { + "name": "GET asgi.http.send", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": { + "http.status_code": 200, + "type": "http.response.start", + }, + }, + { + "name": "GET asgi.http.send", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": {"type": "http.response.body"}, + }, + { + "name": "GET asgi", + "kind": trace_api.SpanKind.SERVER, + "attributes": { + "component": "http", + "http.method": "GET", + "http.scheme": "http", + "host.port": 80, + "http.host": "127.0.0.1", + "http.flavor": "1.0", + "http.target": "/", + "http.url": "http://127.0.0.1/", + "net.peer.ip": "127.0.0.1", + "net.peer.port": 32767, + }, + }, + ] + # Run our expected modifiers + for modifier in modifiers: + expected = modifier(expected) + # Check that output matches + for span, expected in zip(span_list, expected): + self.assertEqual(span.name, expected["name"]) + self.assertEqual(span.kind, expected["kind"]) + self.assertDictEqual(dict(span.attributes), expected["attributes"]) + + def test_basic_asgi_call(self): + """Test that spans are emitted as expected.""" + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + outputs = self.get_all_output() + self.validate_outputs(outputs) + + def test_asgi_exc_info(self): + """Test that exception information is emitted as expected.""" + app = otel_asgi.OpenTelemetryMiddleware(error_asgi) + self.seed_app(app) + self.send_default_request() + outputs = self.get_all_output() + self.validate_outputs(outputs, error=ValueError) + + def test_override_span_name(self): + """Test that span_names can be overwritten by our callback function.""" + span_name = "Dymaxion" + + # pylint:disable=unused-argument + def get_predefined_span_name(scope): + return span_name + + def update_expected_span_name(expected): + for entry in expected: + entry["name"] = " ".join( + [span_name] + entry["name"].split(" ")[-1:] + ) + return expected + + app = otel_asgi.OpenTelemetryMiddleware( + simple_asgi, name_callback=get_predefined_span_name + ) + self.seed_app(app) + self.send_default_request() + outputs = self.get_all_output() + self.validate_outputs(outputs, modifiers=[update_expected_span_name]) + + def test_behavior_with_scope_server_as_none(self): + """Test that middleware is ok when server is none in scope.""" + + def update_expected_server(expected): + expected[3]["attributes"].update( + { + "http.host": "0.0.0.0", + "host.port": 80, + "http.url": "http://0.0.0.0/", + } + ) + return expected + + self.scope["server"] = None + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + outputs = self.get_all_output() + self.validate_outputs(outputs, modifiers=[update_expected_server]) + + def test_host_header(self): + """Test that host header is converted to http.server_name.""" + hostname = b"server_name_1" + + def update_expected_server(expected): + expected[3]["attributes"].update( + {"http.server_name": hostname.decode("utf8")} + ) + return expected + + self.scope["headers"].append([b"host", hostname]) + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + outputs = self.get_all_output() + self.validate_outputs(outputs, modifiers=[update_expected_server]) + + def test_user_agent(self): + """Test that host header is converted to http.server_name.""" + user_agent = b"test-agent" + + def update_expected_user_agent(expected): + expected[3]["attributes"].update( + {"http.user_agent": user_agent.decode("utf8")} + ) + return expected + + self.scope["headers"].append([b"user-agent", user_agent]) + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + outputs = self.get_all_output() + self.validate_outputs(outputs, modifiers=[update_expected_user_agent]) + + def test_websocket(self): + self.scope = { + "type": "websocket", + "http_version": "1.1", + "scheme": "ws", + "path": "/", + "query_string": b"", + "headers": [], + "client": ("127.0.0.1", 32767), + "server": ("127.0.0.1", 80), + } + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_input({"type": "websocket.connect"}) + self.send_input({"type": "websocket.receive", "text": "ping"}) + self.send_input({"type": "websocket.disconnect"}) + self.get_all_output() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 6) + expected = [ + "/ asgi.websocket.receive", + "/ asgi.websocket.send", + "/ asgi.websocket.receive", + "/ asgi.websocket.send", + "/ asgi.websocket.receive", + "/ asgi", + ] + actual = [span.name for span in span_list] + self.assertListEqual(actual, expected) + + def test_lifespan(self): + self.scope["type"] = "lifespan" + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + + +class TestAsgiAttributes(unittest.TestCase): + def setUp(self): + self.scope = {} + setup_testing_defaults(self.scope) + self.span = mock.create_autospec(trace_api.Span, spec_set=True) + + def test_request_attributes(self): + self.scope["query_string"] = b"foo=bar" + + attrs = otel_asgi.collect_request_attributes(self.scope) + self.assertDictEqual( + attrs, + { + "component": "http", + "http.method": "GET", + "http.host": "127.0.0.1", + "http.target": "/", + "http.url": "http://127.0.0.1/?foo=bar", + "host.port": 80, + "http.scheme": "http", + "http.flavor": "1.0", + "net.peer.ip": "127.0.0.1", + "net.peer.port": 32767, + }, + ) + + def test_query_string(self): + self.scope["query_string"] = b"foo=bar" + attrs = otel_asgi.collect_request_attributes(self.scope) + self.assertEqual(attrs["http.url"], "http://127.0.0.1/?foo=bar") + + def test_query_string_percent_bytes(self): + self.scope["query_string"] = b"foo%3Dbar" + attrs = otel_asgi.collect_request_attributes(self.scope) + self.assertEqual(attrs["http.url"], "http://127.0.0.1/?foo=bar") + + def test_query_string_percent_str(self): + self.scope["query_string"] = "foo%3Dbar" + attrs = otel_asgi.collect_request_attributes(self.scope) + self.assertEqual(attrs["http.url"], "http://127.0.0.1/?foo=bar") + + def test_response_attributes(self): + otel_asgi.set_status_code(self.span, 404) + expected = (mock.call("http.status_code", 404),) + self.assertEqual(self.span.set_attribute.call_count, 1) + self.assertEqual(self.span.set_attribute.call_count, 1) + self.span.set_attribute.assert_has_calls(expected, any_order=True) + + def test_response_attributes_invalid_status_code(self): + otel_asgi.set_status_code(self.span, "Invalid Status Code") + self.assertEqual(self.span.set_status.call_count, 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 248e5faea8..8e09ae23a3 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -31,6 +31,9 @@ cov docs/examples/opentelemetry-example-app # aiohttp is only supported on Python 3.5+. if [ ${PYTHON_VERSION_INFO[1]} -gt 4 ]; then cov ext/opentelemetry-ext-aiohttp-client +# ext-asgi is only supported on Python 3.5+. +if [ ${PYTHON_VERSION_INFO[1]} -gt 4 ]; then + cov ext/opentelemetry-ext-asgi fi coverage report --show-missing diff --git a/tests/util/src/opentelemetry/test/asgitestutil.py b/tests/util/src/opentelemetry/test/asgitestutil.py new file mode 100644 index 0000000000..7d23039bf3 --- /dev/null +++ b/tests/util/src/opentelemetry/test/asgitestutil.py @@ -0,0 +1,76 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio + +from asgiref.testing import ApplicationCommunicator + +from opentelemetry.test.spantestutil import SpanTestBase + + +def setup_testing_defaults(scope): + scope.update( + { + "client": ("127.0.0.1", 32767), + "headers": [], + "http_version": "1.0", + "method": "GET", + "path": "/", + "query_string": b"", + "scheme": "http", + "server": ("127.0.0.1", 80), + "type": "http", + } + ) + + +class AsgiTestBase(SpanTestBase): + def setUp(self): + super().setUp() + + self.scope = {} + setup_testing_defaults(self.scope) + self.communicator = None + + def tearDown(self): + if self.communicator: + asyncio.get_event_loop().run_until_complete( + self.communicator.wait() + ) + + def seed_app(self, app): + self.communicator = ApplicationCommunicator(app, self.scope) + + def send_input(self, message): + asyncio.get_event_loop().run_until_complete( + self.communicator.send_input(message) + ) + + def send_default_request(self): + self.send_input({"type": "http.request", "body": b""}) + + def get_output(self): + output = asyncio.get_event_loop().run_until_complete( + self.communicator.receive_output(0) + ) + return output + + def get_all_output(self): + outputs = [] + while True: + try: + outputs.append(self.get_output()) + except asyncio.TimeoutError: + break + return outputs diff --git a/tests/util/src/opentelemetry/test/spantestutil.py b/tests/util/src/opentelemetry/test/spantestutil.py new file mode 100644 index 0000000000..4ae4669a04 --- /dev/null +++ b/tests/util/src/opentelemetry/test/spantestutil.py @@ -0,0 +1,43 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from importlib import reload + +from opentelemetry import trace as trace_api +from opentelemetry.sdk.trace import TracerProvider, export +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +_MEMORY_EXPORTER = None + + +class SpanTestBase(unittest.TestCase): + @classmethod + def setUpClass(cls): + global _MEMORY_EXPORTER # pylint:disable=global-statement + trace_api.set_tracer_provider(TracerProvider()) + tracer_provider = trace_api.get_tracer_provider() + _MEMORY_EXPORTER = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(_MEMORY_EXPORTER) + tracer_provider.add_span_processor(span_processor) + + @classmethod + def tearDownClass(cls): + reload(trace_api) + + def setUp(self): + self.memory_exporter = _MEMORY_EXPORTER + self.memory_exporter.clear() diff --git a/tests/util/src/opentelemetry/test/wsgitestutil.py b/tests/util/src/opentelemetry/test/wsgitestutil.py index cdce28b907..349db8f694 100644 --- a/tests/util/src/opentelemetry/test/wsgitestutil.py +++ b/tests/util/src/opentelemetry/test/wsgitestutil.py @@ -1,35 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import io -import unittest import wsgiref.util as wsgiref_util -from importlib import reload - -from opentelemetry import trace as trace_api -from opentelemetry.sdk.trace import TracerProvider, export -from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, -) - -_MEMORY_EXPORTER = None +from opentelemetry.test.spantestutil import SpanTestBase -class WsgiTestBase(unittest.TestCase): - @classmethod - def setUpClass(cls): - global _MEMORY_EXPORTER # pylint:disable=global-statement - trace_api.set_tracer_provider(TracerProvider()) - tracer_provider = trace_api.get_tracer_provider() - _MEMORY_EXPORTER = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(_MEMORY_EXPORTER) - tracer_provider.add_span_processor(span_processor) - - @classmethod - def tearDownClass(cls): - reload(trace_api) +class WsgiTestBase(SpanTestBase): def setUp(self): - - self.memory_exporter = _MEMORY_EXPORTER - self.memory_exporter.clear() + super().setUp() self.write_buffer = io.BytesIO() self.write = self.write_buffer.write diff --git a/tox.ini b/tox.ini index 918e99f7ca..abb1355be9 100644 --- a/tox.ini +++ b/tox.ini @@ -79,6 +79,10 @@ envlist = py3{4,5,6,7,8}-test-ext-pymysql pypy3-test-ext-pymysql + ; opentelemetry-ext-asgi + py3{5,6,7,8}-test-ext-asgi + pypy3-test-ext-asgi + ; opentelemetry-ext-sqlite3 py3{4,5,6,7,8}-test-ext-sqlite3 pypy3-test-ext-sqlite3 @@ -148,6 +152,7 @@ changedir = test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests test-ext-pymysql: ext/opentelemetry-ext-pymysql/tests + test-ext-asgi: ext/opentelemetry-ext-asgi/tests test-ext-sqlite3: ext/opentelemetry-ext-sqlite3/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests @@ -173,9 +178,10 @@ commands_pre = grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc[test] - wsgi,flask,django: pip install {toxinidir}/tests/util + wsgi,flask,django,asgi: pip install {toxinidir}/tests/util wsgi,flask,django: pip install {toxinidir}/ext/opentelemetry-ext-wsgi flask,django: pip install {toxinidir}/opentelemetry-auto-instrumentation + asgi: pip install {toxinidir}/ext/opentelemetry-ext-asgi flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi[test] From 8a946b5e272ef803f134067d005556912a6d7d19 Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Wed, 27 May 2020 12:40:40 -0400 Subject: [PATCH 0370/1517] bugfix: deep copy empty attributes (#714) Addresses #713. Previously it was possible for a user (acting against the api) to mutate a default variable. --- opentelemetry-sdk/CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/__init__.py | 36 +++++++++---------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index bece33dfca..c4f09760d9 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -10,6 +10,8 @@ ([#724](https://github.com/open-telemetry/opentelemetry-python/pull/724)) - bugfix: Fix error message ([#729](https://github.com/open-telemetry/opentelemetry-python/pull/729)) +- deep copy empty attributes + ([#714](https://github.com/open-telemetry/opentelemetry-python/pull/714)) ## 0.7b1 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 66e13d9cc8..20c50a849f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -252,11 +252,6 @@ class Span(trace_api.Span): this `Span`. """ - # Initialize these lazily assuming most spans won't have them. - _empty_attributes = BoundedDict(MAX_NUM_ATTRIBUTES) - _empty_events = BoundedList(MAX_NUM_EVENTS) - _empty_links = BoundedList(MAX_NUM_LINKS) - def __init__( self, name: str, @@ -289,22 +284,20 @@ def __init__( self._filter_attribute_values(attributes) if not attributes: - self.attributes = Span._empty_attributes + self.attributes = self._new_attributes() else: self.attributes = BoundedDict.from_map( MAX_NUM_ATTRIBUTES, attributes ) - if events is None: - self.events = Span._empty_events - else: - self.events = BoundedList(MAX_NUM_EVENTS) + self.events = self._new_events() + if events: for event in events: self._filter_attribute_values(event.attributes) self.events.append(event) if links is None: - self.links = Span._empty_links + self.links = self._new_links() else: self.links = BoundedList.from_seq(MAX_NUM_LINKS, links) @@ -325,6 +318,18 @@ def __repr__(self): type(self).__name__, self.name, self.context ) + @staticmethod + def _new_attributes(): + return BoundedDict(MAX_NUM_ATTRIBUTES) + + @staticmethod + def _new_events(): + return BoundedList(MAX_NUM_EVENTS) + + @staticmethod + def _new_links(): + return BoundedList(MAX_NUM_LINKS) + @staticmethod def _format_context(context): x_ctx = OrderedDict() @@ -407,9 +412,6 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: if not self.is_recording_events(): return has_ended = self.end_time is not None - if not has_ended: - if self.attributes is Span._empty_attributes: - self.attributes = BoundedDict(MAX_NUM_ATTRIBUTES) if has_ended: logger.warning("Setting attribute on ended span.") return @@ -442,9 +444,7 @@ def _add_event(self, event: EventBase) -> None: if not self.is_recording_events(): return has_ended = self.end_time is not None - if not has_ended: - if self.events is Span._empty_events: - self.events = BoundedList(MAX_NUM_EVENTS) + if has_ended: logger.warning("Calling add_event() on an ended span.") return @@ -458,7 +458,7 @@ def add_event( ) -> None: self._filter_attribute_values(attributes) if not attributes: - attributes = Span._empty_attributes + attributes = self._new_attributes() self._add_event( Event( name=name, From 807e106b9fba9fed3b3eea913bd94e8438c90908 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 27 May 2020 12:38:30 -0700 Subject: [PATCH 0371/1517] django: Add exclude lists (#670) Similar to flask, enabling exclusion of spans based on host and path. Co-authored-by: Diego Hurtado Co-authored-by: alrex Co-authored-by: Yusuke Tsutsumi --- ext/opentelemetry-ext-django/CHANGELOG.md | 4 +- ext/opentelemetry-ext-django/README.rst | 10 ++ .../opentelemetry/ext/django/middleware.py | 29 +++++ .../tests/test_middleware.py | 7 +- ext/opentelemetry-ext-django/tests/views.py | 8 ++ .../src/opentelemetry/ext/flask/__init__.py | 50 ++++---- .../tests/base_test.py | 108 ------------------ .../tests/test_programmatic.py | 90 ++++++++++++++- .../src/opentelemetry/util/__init__.py | 8 ++ 9 files changed, 179 insertions(+), 135 deletions(-) diff --git a/ext/opentelemetry-ext-django/CHANGELOG.md b/ext/opentelemetry-ext-django/CHANGELOG.md index f46a42c223..c7c4966dc9 100644 --- a/ext/opentelemetry-ext-django/CHANGELOG.md +++ b/ext/opentelemetry-ext-django/CHANGELOG.md @@ -2,7 +2,9 @@ ## Unreleased -- Add support for django >= 1.10 (#717) +- Add exclude list for paths and hosts to prevent from tracing + ([#670](https://github.com/open-telemetry/opentelemetry-python/pull/670)) +- Add support for django >= 1.10 (#717) ## 0.7b1 diff --git a/ext/opentelemetry-ext-django/README.rst b/ext/opentelemetry-ext-django/README.rst index b922046ca6..834bd63249 100644 --- a/ext/opentelemetry-ext-django/README.rst +++ b/ext/opentelemetry-ext-django/README.rst @@ -15,6 +15,16 @@ Installation pip install opentelemetry-ext-django +Configuration +------------- + +Exclude lists +************* +Excludes certain hosts and paths from being tracked. Pass in comma delimited string into environment variables. +Host refers to the entire url and path refers to the part of the url after the domain. Host matches the exact string that is given, where as path matches if the url starts with the given excluded path. + +Excluded hosts: OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_HOSTS +Excluded paths: OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_PATHS References ---------- diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py index 912ce0f1a2..0ce03b97cb 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py @@ -14,6 +14,7 @@ from logging import getLogger +from opentelemetry.configuration import Configuration from opentelemetry.context import attach, detach from opentelemetry.ext.django.version import __version__ from opentelemetry.ext.wsgi import ( @@ -23,6 +24,7 @@ ) from opentelemetry.propagators import extract from opentelemetry.trace import SpanKind, get_tracer +from opentelemetry.util import disable_trace try: from django.utils.deprecation import MiddlewareMixin @@ -42,6 +44,13 @@ class _DjangoMiddleware(MiddlewareMixin): _environ_token = "opentelemetry-instrumentor-django.token" _environ_span_key = "opentelemetry-instrumentor-django.span_key" + _excluded_hosts = Configuration().DJANGO_EXCLUDED_HOSTS or [] + _excluded_paths = Configuration().DJANGO_EXCLUDED_PATHS or [] + if _excluded_hosts: + _excluded_hosts = str.split(_excluded_hosts, ",") + if _excluded_paths: + _excluded_paths = str.split(_excluded_paths, ",") + def process_view( self, request, view_func, view_args, view_kwargs ): # pylint: disable=unused-argument @@ -53,6 +62,12 @@ def process_view( # key.lower().replace('_', '-').replace("http-", "", 1): value # for key, value in request.META.items() # } + if disable_trace( + request.build_absolute_uri("?"), + self._excluded_hosts, + self._excluded_paths, + ): + return environ = request.META @@ -82,6 +97,13 @@ def process_exception(self, request, exception): # Django can call this method and process_response later. In order # to avoid __exit__ and detach from being called twice then, the # respective keys are being removed here. + if disable_trace( + request.build_absolute_uri("?"), + self._excluded_hosts, + self._excluded_paths, + ): + return + if self._environ_activation_key in request.META.keys(): request.META[self._environ_activation_key].__exit__( type(exception), @@ -94,6 +116,13 @@ def process_exception(self, request, exception): request.META.pop(self._environ_token, None) def process_response(self, request, response): + if disable_trace( + request.build_absolute_uri("?"), + self._excluded_hosts, + self._excluded_paths, + ): + return response + if ( self._environ_activation_key in request.META.keys() and self._environ_span_key in request.META.keys() diff --git a/ext/opentelemetry-ext-django/tests/test_middleware.py b/ext/opentelemetry-ext-django/tests/test_middleware.py index afee6acf77..e0ac92de52 100644 --- a/ext/opentelemetry-ext-django/tests/test_middleware.py +++ b/ext/opentelemetry-ext-django/tests/test_middleware.py @@ -19,16 +19,20 @@ from django.test import Client from django.test.utils import setup_test_environment, teardown_test_environment +from opentelemetry.configuration import Configuration from opentelemetry.ext.django import DjangoInstrumentor from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import SpanKind from opentelemetry.trace.status import StatusCanonicalCode -from .views import error, traced # pylint: disable=import-error +# pylint: disable=import-error +from .views import error, excluded, excluded2, traced urlpatterns = [ url(r"^traced/", traced), url(r"^error/", error), + url(r"^excluded/", excluded), + url(r"^excluded2/", excluded2), ] _django_instrumentor = DjangoInstrumentor() @@ -43,6 +47,7 @@ def setUp(self): super().setUp() setup_test_environment() _django_instrumentor.instrument() + Configuration._reset() # pylint: disable=protected-access def tearDown(self): super().tearDown() diff --git a/ext/opentelemetry-ext-django/tests/views.py b/ext/opentelemetry-ext-django/tests/views.py index 498a4518ed..9035f0ea03 100644 --- a/ext/opentelemetry-ext-django/tests/views.py +++ b/ext/opentelemetry-ext-django/tests/views.py @@ -7,3 +7,11 @@ def traced(request): # pylint: disable=unused-argument def error(request): # pylint: disable=unused-argument raise ValueError("error") + + +def excluded(request): # pylint: disable=unused-argument + return HttpResponse() + + +def excluded2(request): # pylint: disable=unused-argument + return HttpResponse() diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index 040c8770c6..cd94dc7e47 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -55,11 +55,7 @@ def hello(): from opentelemetry import configuration, context, propagators, trace from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.flask.version import __version__ -from opentelemetry.util import ( - disable_tracing_hostname, - disable_tracing_path, - time_ns, -) +from opentelemetry.util import disable_trace, time_ns _logger = getLogger(__name__) @@ -69,6 +65,24 @@ def hello(): _ENVIRON_TOKEN = "opentelemetry-flask.token" +def get_excluded_hosts(): + hosts = configuration.Configuration().FLASK_EXCLUDED_HOSTS or [] + if hosts: + hosts = str.split(hosts, ",") + return hosts + + +def get_excluded_paths(): + paths = configuration.Configuration().FLASK_EXCLUDED_PATHS or [] + if paths: + paths = str.split(paths, ",") + return paths + + +_excluded_hosts = get_excluded_hosts() +_excluded_paths = get_excluded_paths() + + def _rewrapped_app(wsgi_app): def _wrapped_app(environ, start_response): # We want to measure the time for route matching, etc. @@ -78,9 +92,9 @@ def _wrapped_app(environ, start_response): environ[_ENVIRON_STARTTIME_KEY] = time_ns() def _start_response(status, response_headers, *args, **kwargs): - - if not _disable_trace(flask.request.url): - + if not disable_trace( + flask.request.url, _excluded_hosts, _excluded_paths + ): span = flask.request.environ.get(_ENVIRON_SPAN_KEY) if span: @@ -102,7 +116,7 @@ def _start_response(status, response_headers, *args, **kwargs): def _before_request(): - if _disable_trace(flask.request.url): + if disable_trace(flask.request.url, _excluded_hosts, _excluded_paths): return environ = flask.request.environ @@ -134,6 +148,9 @@ def _before_request(): def _teardown_request(exc): + if disable_trace(flask.request.url, _excluded_hosts, _excluded_paths): + return + activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY) if not activation: _logger.warning( @@ -163,21 +180,6 @@ def __init__(self, *args, **kwargs): self.teardown_request(_teardown_request) -def _disable_trace(url): - excluded_hosts = configuration.Configuration().FLASK_EXCLUDED_HOSTS - excluded_paths = configuration.Configuration().FLASK_EXCLUDED_PATHS - - if excluded_hosts: - excluded_hosts = str.split(excluded_hosts, ",") - if disable_tracing_hostname(url, excluded_hosts): - return True - if excluded_paths: - excluded_paths = str.split(excluded_paths, ",") - if disable_tracing_path(url, excluded_paths): - return True - return False - - class FlaskInstrumentor(BaseInstrumentor): # pylint: disable=protected-access,attribute-defined-outside-init """An instrumentor for flask.Flask diff --git a/ext/opentelemetry-ext-flask/tests/base_test.py b/ext/opentelemetry-ext-flask/tests/base_test.py index 42341826df..c2bc646e1b 100644 --- a/ext/opentelemetry-ext-flask/tests/base_test.py +++ b/ext/opentelemetry-ext-flask/tests/base_test.py @@ -12,34 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest.mock import patch - -from flask import request from werkzeug.test import Client from werkzeug.wrappers import BaseResponse -from opentelemetry import trace from opentelemetry.configuration import Configuration -def expected_attributes(override_attributes): - default_attributes = { - "component": "http", - "http.method": "GET", - "http.server_name": "localhost", - "http.scheme": "http", - "host.port": 80, - "http.host": "localhost", - "http.target": "/", - "http.flavor": "1.1", - "http.status_text": "OK", - "http.status_code": 200, - } - for key, val in override_attributes.items(): - default_attributes[key] = val - return default_attributes - - class InstrumentationTest: def setUp(self): # pylint: disable=invalid-name super().setUp() # pylint: disable=no-member @@ -66,89 +44,3 @@ def excluded2_endpoint(): # pylint: disable=attribute-defined-outside-init self.client = Client(self.app, BaseResponse) - - # pylint: disable=no-member - def test_only_strings_in_environ(self): - """ - Some WSGI servers (such as Gunicorn) expect keys in the environ object - to be strings - - OpenTelemetry should adhere to this convention. - """ - nonstring_keys = set() - - def assert_environ(): - for key in request.environ: - if not isinstance(key, str): - nonstring_keys.add(key) - return "hi" - - self.app.route("/assert_environ")(assert_environ) - self.client.get("/assert_environ") - self.assertEqual(nonstring_keys, set()) - - def test_simple(self): - expected_attrs = expected_attributes( - {"http.target": "/hello/123", "http.route": "/hello/"} - ) - self.client.get("/hello/123") - - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "_hello_endpoint") - self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) - self.assertEqual(span_list[0].attributes, expected_attrs) - - def test_404(self): - expected_attrs = expected_attributes( - { - "http.method": "POST", - "http.target": "/bye", - "http.status_text": "NOT FOUND", - "http.status_code": 404, - } - ) - - resp = self.client.post("/bye") - self.assertEqual(404, resp.status_code) - resp.close() - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "/bye") - self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) - self.assertEqual(span_list[0].attributes, expected_attrs) - - def test_internal_error(self): - expected_attrs = expected_attributes( - { - "http.target": "/hello/500", - "http.route": "/hello/", - "http.status_text": "INTERNAL SERVER ERROR", - "http.status_code": 500, - } - ) - resp = self.client.get("/hello/500") - self.assertEqual(500, resp.status_code) - resp.close() - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "_hello_endpoint") - self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) - self.assertEqual(span_list[0].attributes, expected_attrs) - - @patch.dict( - "os.environ", # type: ignore - { - "OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_HOSTS": ( - "http://localhost/excluded" - ), - "OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_PATHS": "excluded2", - }, - ) - def test_excluded_path(self): - self.client.get("/hello/123") - self.client.get("/excluded") - self.client.get("/excluded2") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "_hello_endpoint") diff --git a/ext/opentelemetry-ext-flask/tests/test_programmatic.py b/ext/opentelemetry-ext-flask/tests/test_programmatic.py index 4e17f25fdc..95f82373e3 100644 --- a/ext/opentelemetry-ext-flask/tests/test_programmatic.py +++ b/ext/opentelemetry-ext-flask/tests/test_programmatic.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from flask import Flask +from flask import Flask, request +from opentelemetry import trace from opentelemetry.ext.flask import FlaskInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase @@ -22,6 +23,24 @@ from .base_test import InstrumentationTest +def expected_attributes(override_attributes): + default_attributes = { + "component": "http", + "http.method": "GET", + "http.server_name": "localhost", + "http.scheme": "http", + "host.port": 80, + "http.host": "localhost", + "http.target": "/", + "http.flavor": "1.1", + "http.status_text": "OK", + "http.status_code": 200, + } + for key, val in override_attributes.items(): + default_attributes[key] = val + return default_attributes + + class TestProgrammatic(InstrumentationTest, TestBase, WsgiTestBase): def setUp(self): super().setUp() @@ -51,3 +70,72 @@ def test_uninstrument(self): self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) + + # pylint: disable=no-member + def test_only_strings_in_environ(self): + """ + Some WSGI servers (such as Gunicorn) expect keys in the environ object + to be strings + + OpenTelemetry should adhere to this convention. + """ + nonstring_keys = set() + + def assert_environ(): + for key in request.environ: + if not isinstance(key, str): + nonstring_keys.add(key) + return "hi" + + self.app.route("/assert_environ")(assert_environ) + self.client.get("/assert_environ") + self.assertEqual(nonstring_keys, set()) + + def test_simple(self): + expected_attrs = expected_attributes( + {"http.target": "/hello/123", "http.route": "/hello/"} + ) + self.client.get("/hello/123") + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "_hello_endpoint") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) + + def test_404(self): + expected_attrs = expected_attributes( + { + "http.method": "POST", + "http.target": "/bye", + "http.status_text": "NOT FOUND", + "http.status_code": 404, + } + ) + + resp = self.client.post("/bye") + self.assertEqual(404, resp.status_code) + resp.close() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "/bye") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) + + def test_internal_error(self): + expected_attrs = expected_attributes( + { + "http.target": "/hello/500", + "http.route": "/hello/", + "http.status_text": "INTERNAL SERVER ERROR", + "http.status_code": 500, + } + ) + resp = self.client.get("/hello/500") + self.assertEqual(500, resp.status_code) + resp.close() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "_hello_endpoint") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index 48c350730e..bab42b0bde 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -70,3 +70,11 @@ def disable_tracing_hostname( url: str, excluded_hostnames: Sequence[str] ) -> bool: return url in excluded_hostnames + + +def disable_trace( + url: str, excluded_hosts: Sequence[str], excluded_paths: Sequence[str] +) -> bool: + return disable_tracing_hostname( + url, excluded_hosts + ) or disable_tracing_path(url, excluded_paths) From f7a7f64dbab578a54e0deb6c97264169adb4c874 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 28 May 2020 08:17:05 -0700 Subject: [PATCH 0372/1517] chore: update master to 0.9.dev0 (#745) --- docs/examples/opentelemetry-example-app/setup.py | 2 +- ext/opentelemetry-ext-aiohttp-client/setup.cfg | 2 +- .../src/opentelemetry/ext/aiohttp_client/version.py | 2 +- ext/opentelemetry-ext-asgi/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-asgi/setup.cfg | 2 +- .../src/opentelemetry/ext/asgi/version.py | 2 +- ext/opentelemetry-ext-datadog/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-datadog/setup.cfg | 4 ++-- .../src/opentelemetry/ext/datadog/version.py | 2 +- ext/opentelemetry-ext-dbapi/setup.cfg | 4 ++-- .../src/opentelemetry/ext/dbapi/version.py | 2 +- ext/opentelemetry-ext-django/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-django/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/django/version.py | 2 +- ext/opentelemetry-ext-flask/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/flask/version.py | 2 +- ext/opentelemetry-ext-grpc/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-grpc/setup.cfg | 6 +++--- .../src/opentelemetry/ext/grpc/version.py | 2 +- ext/opentelemetry-ext-jaeger/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/ext/jaeger/version.py | 2 +- ext/opentelemetry-ext-jinja2/setup.cfg | 6 +++--- .../src/opentelemetry/ext/jinja2/version.py | 2 +- ext/opentelemetry-ext-mysql/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/mysql/version.py | 2 +- ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-opencensusexporter/setup.cfg | 4 ++-- .../src/opentelemetry/ext/opencensusexporter/version.py | 2 +- ext/opentelemetry-ext-opentracing-shim/setup.cfg | 4 ++-- .../src/opentelemetry/ext/opentracing_shim/version.py | 2 +- ext/opentelemetry-ext-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/ext/prometheus/version.py | 2 +- ext/opentelemetry-ext-psycopg2/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-psycopg2/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/psycopg2/version.py | 2 +- ext/opentelemetry-ext-pymongo/setup.cfg | 6 +++--- .../src/opentelemetry/ext/pymongo/version.py | 2 +- ext/opentelemetry-ext-pymysql/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/pymysql/version.py | 2 +- ext/opentelemetry-ext-redis/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/redis/version.py | 2 +- ext/opentelemetry-ext-requests/setup.cfg | 6 +++--- .../src/opentelemetry/ext/requests/version.py | 2 +- ext/opentelemetry-ext-sqlalchemy/setup.cfg | 6 +++--- .../src/opentelemetry/ext/sqlalchemy/version.py | 2 +- ext/opentelemetry-ext-sqlite3/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-sqlite3/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/sqlite3/version.py | 2 +- ext/opentelemetry-ext-wsgi/setup.cfg | 4 ++-- .../src/opentelemetry/ext/wsgi/version.py | 2 +- ext/opentelemetry-ext-zipkin/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/ext/zipkin/version.py | 2 +- opentelemetry-api/CHANGELOG.md | 4 ++++ opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-auto-instrumentation/CHANGELOG.md | 4 ++++ opentelemetry-auto-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/auto_instrumentation/version.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 4 ++++ opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 63 files changed, 138 insertions(+), 90 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py index ced6d54e4d..4d87c27afc 100644 --- a/docs/examples/opentelemetry-example-app/setup.py +++ b/docs/examples/opentelemetry-example-app/setup.py @@ -16,7 +16,7 @@ setuptools.setup( name="opentelemetry-example-app", - version="0.8.dev0", + version="0.9.dev0", author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ diff --git a/ext/opentelemetry-ext-aiohttp-client/setup.cfg b/ext/opentelemetry-ext-aiohttp-client/setup.cfg index 80d53a1f89..af4839fc3d 100644 --- a/ext/opentelemetry-ext-aiohttp-client/setup.cfg +++ b/ext/opentelemetry-ext-aiohttp-client/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.8.dev0 + opentelemetry-api >= 0.9.dev0 aiohttp ~= 3.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py index 6a0169eecf..410979c2eb 100644 --- a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py +++ b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-asgi/CHANGELOG.md b/ext/opentelemetry-ext-asgi/CHANGELOG.md index 7936dd50fc..886cbff8f1 100644 --- a/ext/opentelemetry-ext-asgi/CHANGELOG.md +++ b/ext/opentelemetry-ext-asgi/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.8b0 + +Released 2020-05-27 + - Add ASGI middleware ([#716](https://github.com/open-telemetry/opentelemetry-python/pull/716)) diff --git a/ext/opentelemetry-ext-asgi/setup.cfg b/ext/opentelemetry-ext-asgi/setup.cfg index f834737735..2be30ce41c 100644 --- a/ext/opentelemetry-ext-asgi/setup.cfg +++ b/ext/opentelemetry-ext-asgi/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.8.dev0 + opentelemetry-api == 0.9.dev0 asgiref ~= 3.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py +++ b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-datadog/CHANGELOG.md b/ext/opentelemetry-ext-datadog/CHANGELOG.md index 333b2b3f8b..15c6ff4b00 100644 --- a/ext/opentelemetry-ext-datadog/CHANGELOG.md +++ b/ext/opentelemetry-ext-datadog/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.8b0 + +Released 2020-05-27 + - Add exporter to Datadog ([#572](https://github.com/open-telemetry/opentelemetry-python/pull/572)) diff --git a/ext/opentelemetry-ext-datadog/setup.cfg b/ext/opentelemetry-ext-datadog/setup.cfg index 667491eb8f..afdd739d12 100644 --- a/ext/opentelemetry-ext-datadog/setup.cfg +++ b/ext/opentelemetry-ext-datadog/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = ddtrace>=0.34.0 - opentelemetry-api==0.8.dev0 - opentelemetry-sdk==0.8.dev0 + opentelemetry-api==0.9.dev0 + opentelemetry-sdk==0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index e7ae7aa502..e352201bb6 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.8.dev0 + opentelemetry-api == 0.9.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.8.dev0 + opentelemetry-test == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-django/CHANGELOG.md b/ext/opentelemetry-ext-django/CHANGELOG.md index c7c4966dc9..9c578d962f 100644 --- a/ext/opentelemetry-ext-django/CHANGELOG.md +++ b/ext/opentelemetry-ext-django/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.8b0 + +Released 2020-05-27 + - Add exclude list for paths and hosts to prevent from tracing ([#670](https://github.com/open-telemetry/opentelemetry-python/pull/670)) - Add support for django >= 1.10 (#717) diff --git a/ext/opentelemetry-ext-django/setup.cfg b/ext/opentelemetry-ext-django/setup.cfg index b5847272c5..bf72f9d981 100644 --- a/ext/opentelemetry-ext-django/setup.cfg +++ b/ext/opentelemetry-ext-django/setup.cfg @@ -41,13 +41,13 @@ package_dir= packages=find_namespace: install_requires = django >= 1.10 - opentelemetry-ext-wsgi == 0.8.dev0 - opentelemetry-auto-instrumentation == 0.8.dev0 - opentelemetry-api == 0.8.dev0 + opentelemetry-ext-wsgi == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9.dev0 [options.extras_require] test = - opentelemetry-test == 0.8.dev0 + opentelemetry-test == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index 4ce79b6696..3258717419 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-ext-wsgi == 0.8.dev0 - opentelemetry-auto-instrumentation == 0.8.dev0 - opentelemetry-api == 0.8.dev0 + opentelemetry-ext-wsgi == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9.dev0 [options.extras_require] test = flask~=1.0 - opentelemetry-test == 0.8.dev0 + opentelemetry-test == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-grpc/CHANGELOG.md b/ext/opentelemetry-ext-grpc/CHANGELOG.md index 2bf8ae44af..2302714790 100644 --- a/ext/opentelemetry-ext-grpc/CHANGELOG.md +++ b/ext/opentelemetry-ext-grpc/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.8b0 + +Released 2020-05-27 + - lint: version of grpc causes lint issues ([#696](https://github.com/open-telemetry/opentelemetry-python/pull/696)) diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index 93fac771bf..a45ee3c647 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.8.dev0 + opentelemetry-api == 0.9.dev0 grpcio ~= 1.27 [options.extras_require] test = - opentelemetry-test == 0.8.dev0 - opentelemetry-sdk == 0.8.dev0 + opentelemetry-test == 0.9.dev0 + opentelemetry-sdk == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-jaeger/CHANGELOG.md b/ext/opentelemetry-ext-jaeger/CHANGELOG.md index b1e9e84485..9ed679ee1c 100644 --- a/ext/opentelemetry-ext-jaeger/CHANGELOG.md +++ b/ext/opentelemetry-ext-jaeger/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.8b0 + +Released 2020-05-27 + - Transform resource to tags when exporting ([#645](https://github.com/open-telemetry/opentelemetry-python/pull/645)) diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/ext/opentelemetry-ext-jaeger/setup.cfg index 9d61b6185f..339ac32c28 100644 --- a/ext/opentelemetry-ext-jaeger/setup.cfg +++ b/ext/opentelemetry-ext-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.8.dev0 - opentelemetry-sdk == 0.8.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-sdk == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index f646da0a24..3f34fbd58e 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-jinja2/setup.cfg b/ext/opentelemetry-ext-jinja2/setup.cfg index f592870cfe..a9428aec60 100644 --- a/ext/opentelemetry-ext-jinja2/setup.cfg +++ b/ext/opentelemetry-ext-jinja2/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.8.dev0 - opentelemetry-auto-instrumentation == 0.8.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.8.dev0 + opentelemetry-test == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py +++ b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 25bf824339..afe53a651f 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.8.dev0 - opentelemetry-ext-dbapi == 0.8.dev0 - opentelemetry-auto-instrumentation == 0.8.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-ext-dbapi == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.8.dev0 + opentelemetry-test == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md b/ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md index 6662cf1b54..4d1c9a97b8 100644 --- a/ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md +++ b/ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.8b0 + +Released 2020-05-27 + - Rename otcollector to opencensus ([#695](https://github.com/open-telemetry/opentelemetry-python/pull/695)) diff --git a/ext/opentelemetry-ext-opencensusexporter/setup.cfg b/ext/opentelemetry-ext-opencensusexporter/setup.cfg index 850368fd0d..11fc3a61c7 100644 --- a/ext/opentelemetry-ext-opencensusexporter/setup.cfg +++ b/ext/opentelemetry-ext-opencensusexporter/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.8.dev0 - opentelemetry-sdk == 0.8.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-sdk == 0.9.dev0 protobuf >= 3.8.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py +++ b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg index 1d67b11983..2ad18381c5 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.8.dev0 + opentelemetry-api == 0.9.dev0 [options.extras_require] test = - opentelemetry-test == 0.8.dev0 + opentelemetry-test == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/ext/opentelemetry-ext-prometheus/setup.cfg index c35ad29dcb..653c72a404 100644 --- a/ext/opentelemetry-ext-prometheus/setup.cfg +++ b/ext/opentelemetry-ext-prometheus/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.8.dev0 - opentelemetry-sdk == 0.8.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-sdk == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-psycopg2/CHANGELOG.md b/ext/opentelemetry-ext-psycopg2/CHANGELOG.md index 1a6748f7ba..2106b42550 100644 --- a/ext/opentelemetry-ext-psycopg2/CHANGELOG.md +++ b/ext/opentelemetry-ext-psycopg2/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.8b0 + +Released 2020-05-27 + - Implement instrumentor interface, enabling auto-instrumentation ([#694]https://github.com/open-telemetry/opentelemetry-python/pull/694) ## 0.4a0 diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index 2a6e69605d..5de40eac26 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.8.dev0 - opentelemetry-ext-dbapi == 0.8.dev0 - opentelemetry-auto-instrumentation == 0.8.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-ext-dbapi == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.8.dev0 + opentelemetry-test == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index cf9350e4c3..ba1ea4026a 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.8.dev0 - opentelemetry-auto-instrumentation == 0.8.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 pymongo ~= 3.1 [options.extras_require] test = - opentelemetry-test == 0.8.dev0 + opentelemetry-test == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-pymysql/setup.cfg b/ext/opentelemetry-ext-pymysql/setup.cfg index 5b0df1b937..f08e4d3ee1 100644 --- a/ext/opentelemetry-ext-pymysql/setup.cfg +++ b/ext/opentelemetry-ext-pymysql/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.8.dev0 - opentelemetry-ext-dbapi == 0.8.dev0 - opentelemetry-auto-instrumentation == 0.8.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-ext-dbapi == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 PyMySQL ~= 0.9.3 [options.extras_require] test = - opentelemetry-test == 0.8.dev0 + opentelemetry-test == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-redis/setup.cfg b/ext/opentelemetry-ext-redis/setup.cfg index 5ebd72f6d0..2694d85d52 100644 --- a/ext/opentelemetry-ext-redis/setup.cfg +++ b/ext/opentelemetry-ext-redis/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.8.dev0 - opentelemetry-auto-instrumentation == 0.8.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 redis >= 2.6 wrapt >= 1.12.1 [options.extras_require] test = - opentelemetry-test == 0.8.dev0 - opentelemetry-sdk == 0.8.dev0 + opentelemetry-test == 0.9.dev0 + opentelemetry-sdk == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py +++ b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-requests/setup.cfg b/ext/opentelemetry-ext-requests/setup.cfg index 3fe0ef66d2..ca5dc5ceba 100644 --- a/ext/opentelemetry-ext-requests/setup.cfg +++ b/ext/opentelemetry-ext-requests/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.8.dev0 - opentelemetry-auto-instrumentation == 0.8.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 requests ~= 2.0 [options.extras_require] test = - opentelemetry-test == 0.8.dev0 + opentelemetry-test == 0.9.dev0 httpretty ~= 1.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-sqlalchemy/setup.cfg b/ext/opentelemetry-ext-sqlalchemy/setup.cfg index 68473b383f..dfbf655e6d 100644 --- a/ext/opentelemetry-ext-sqlalchemy/setup.cfg +++ b/ext/opentelemetry-ext-sqlalchemy/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.8.dev0 - opentelemetry-auto-instrumentation == 0.8.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 wrapt >= 1.11.2 sqlalchemy [options.extras_require] test = - opentelemetry-sdk == 0.8.dev0 + opentelemetry-sdk == 0.9.dev0 pytest [options.packages.find] diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py +++ b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-sqlite3/CHANGELOG.md b/ext/opentelemetry-ext-sqlite3/CHANGELOG.md index 33144da913..c2ac2d3e02 100644 --- a/ext/opentelemetry-ext-sqlite3/CHANGELOG.md +++ b/ext/opentelemetry-ext-sqlite3/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.8b0 + +Released 2020-05-27 + - Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-sqlite3/setup.cfg b/ext/opentelemetry-ext-sqlite3/setup.cfg index 836d9d17d3..b32dd1cf68 100644 --- a/ext/opentelemetry-ext-sqlite3/setup.cfg +++ b/ext/opentelemetry-ext-sqlite3/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.8.dev0 - opentelemetry-ext-dbapi == 0.8.dev0 - opentelemetry-auto-instrumentation == 0.8.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-ext-dbapi == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.8.dev0 + opentelemetry-test == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py +++ b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index 87e117589c..288ebcaa64 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -40,11 +40,11 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.8.dev0 + opentelemetry-api == 0.9.dev0 [options.extras_require] test = - opentelemetry-test == 0.8.dev0 + opentelemetry-test == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-zipkin/CHANGELOG.md b/ext/opentelemetry-ext-zipkin/CHANGELOG.md index 49111e22c2..efad477d48 100644 --- a/ext/opentelemetry-ext-zipkin/CHANGELOG.md +++ b/ext/opentelemetry-ext-zipkin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.8b0 + +Released 2020-05-27 + - Transform resource to tags when exporting ([#707](https://github.com/open-telemetry/opentelemetry-python/pull/707)) diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg index 25f8d51c8b..809db51237 100644 --- a/ext/opentelemetry-ext-zipkin/setup.cfg +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 0.8.dev0 - opentelemetry-sdk == 0.8.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-sdk == 0.9.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index daa6c7286a..386bd75d47 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.8b0 + +Released 2020-05-27 + - Handle boolean, integer and float values in Configuration ([#662](https://github.com/open-telemetry/opentelemetry-python/pull/662)) - bugfix: ensure status is always string diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index bcf6a35777..603bf0b7e5 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/opentelemetry-auto-instrumentation/CHANGELOG.md b/opentelemetry-auto-instrumentation/CHANGELOG.md index f637c6f568..d2521d157c 100644 --- a/opentelemetry-auto-instrumentation/CHANGELOG.md +++ b/opentelemetry-auto-instrumentation/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.8b0 + +Released 2020-05-27 + - Add a new bootstrap command that enables automatic instrument installations. ([#650](https://github.com/open-telemetry/opentelemetry-python/pull/650)) diff --git a/opentelemetry-auto-instrumentation/setup.cfg b/opentelemetry-auto-instrumentation/setup.cfg index 9829c84134..0e9207922b 100644 --- a/opentelemetry-auto-instrumentation/setup.cfg +++ b/opentelemetry-auto-instrumentation/setup.cfg @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: zip_safe = False include_package_data = True -install_requires = opentelemetry-api == 0.8.dev0 +install_requires = opentelemetry-api == 0.9.dev0 [options.packages.find] where = src diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py index bcf6a35777..603bf0b7e5 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index c4f09760d9..50ea751e15 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.8b0 + +Released 2020-05-27 + - Validate span attribute types in SDK ([#678](https://github.com/open-telemetry/opentelemetry-python/pull/678)) - Specify to_json indent from arguments diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 5bb12fc655..323bca728f 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: zip_safe = False include_package_data = True -install_requires = opentelemetry-api==0.8.dev0 +install_requires = opentelemetry-api==0.9.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index bcf6a35777..603bf0b7e5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 82d720c51c..962d67c24b 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" From 960d0a35e48d65aaa144a8c9a74456e2ca3100ad Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 28 May 2020 08:42:19 -0700 Subject: [PATCH 0373/1517] chore: updating release markdown and script to fetch master (#743) --- RELEASING.md | 4 +++- scripts/prepare_release.sh | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index d4749dedbe..8b9abee354 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,6 +2,7 @@ This document explains how to publish all OT modules at version x.y.z. Ensure that you’re following semver when choosing a version number. Release Process: +* [Checkout a clean repo](#checkout-a-clean-repo) * [Create a new branch](#create-a-new-branch) * [Open a Pull Request](#open-a-pull-request) * [Create a Release](#Create-a-Release) @@ -10,6 +11,8 @@ Release Process: * [Check PyPI](#Check-PyPI) * [Troubleshooting](#troubleshooting) +## Checkout a clean repo +- To avoid pushing untracked changes, check out the repo in a new dir ## Create a new branch The following script does the following: @@ -74,7 +77,6 @@ git commit -m If for some reason the action failed, do it manually: -- To avoid pushing untracked changes, check out the repo in a new dir - Switch to the release branch (important so we don't publish packages with "dev" versions) - Build distributions with `./scripts/build.sh` - Delete distributions we don't want to push (e.g. `testutil`) diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh index bf8eb3e961..a74bd17a8a 100755 --- a/scripts/prepare_release.sh +++ b/scripts/prepare_release.sh @@ -1,4 +1,4 @@ -#!/bin/bash -x +#!/bin/bash # # This script: # 1. parses the version number from the branch name @@ -73,6 +73,7 @@ function update_changelog() { } # create the release branch +git fetch origin master git checkout master git reset --hard origin/master git checkout -b release/${VERSION} From 30c953d6eef7800160ead0b9f3145a3fdbecdfce Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Thu, 28 May 2020 17:34:53 -0400 Subject: [PATCH 0374/1517] datadog: set sampling rate (#740) Set the sampling rate in the Datadog exported span if span was sampled with the ProbabilitySampler. Co-authored-by: Diego Hurtado --- .../opentelemetry/ext/datadog/constants.py | 2 ++ .../src/opentelemetry/ext/datadog/exporter.py | 16 +++++++++- .../tests/test_datadog_exporter.py | 32 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py index 54d7946ab4..d2864c1076 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py @@ -2,3 +2,5 @@ AUTO_REJECT = 0 AUTO_KEEP = 1 USER_KEEP = 2 +SAMPLE_RATE_METRIC_KEY = "_sample_rate" +SAMPLING_PRIORITY_KEY = "_sampling_priority_v1" diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py index 01a0191996..35d0f98203 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py @@ -25,7 +25,7 @@ from opentelemetry.trace.status import StatusCanonicalCode # pylint:disable=relative-beyond-top-level -from .constants import DD_ORIGIN +from .constants import DD_ORIGIN, SAMPLE_RATE_METRIC_KEY logger = logging.getLogger(__name__) @@ -136,6 +136,10 @@ def _translate_to_datadog(self, spans): if origin and parent_id == 0: datadog_span.set_tag(DD_ORIGIN, origin) + sampling_rate = _get_sampling_rate(span) + if sampling_rate is not None: + datadog_span.set_metric(SAMPLE_RATE_METRIC_KEY, sampling_rate) + # span events and span links are not supported datadog_spans.append(datadog_span) @@ -216,3 +220,13 @@ def _get_origin(span): ctx = span.get_context() origin = ctx.trace_state.get(DD_ORIGIN) return origin + + +def _get_sampling_rate(span): + ctx = span.get_context() + return ( + span.sampler.rate + if ctx.trace_flags.sampled + and isinstance(span.sampler, trace_api.sampling.ProbabilitySampler) + else None + ) diff --git a/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py b/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py index 14fd550789..378bef4147 100644 --- a/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py +++ b/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py @@ -440,3 +440,35 @@ def test_origin(self): ] expected = ["origin-service", None] self.assertListEqual(actual, expected) + + def test_sampling_rate(self): + context = trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x34BF92DEEFC58C92, + is_remote=False, + trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), + ) + sampler = trace_api.sampling.ProbabilitySampler(0.5) + + span = trace.Span( + name="sampled", context=context, parent=None, sampler=sampler + ) + span.start() + span.end() + + # pylint: disable=protected-access + exporter = datadog.DatadogSpanExporter() + datadog_spans = [ + span.to_dict() for span in exporter._translate_to_datadog([span]) + ] + + self.assertEqual(len(datadog_spans), 1) + + actual = [ + span["metrics"].get(datadog.constants.SAMPLE_RATE_METRIC_KEY) + if "metrics" in span + else None + for span in datadog_spans + ] + expected = [0.5] + self.assertListEqual(actual, expected) From f8f2ca4a83627d829642069e002e697a1a2a1a1b Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Fri, 29 May 2020 08:53:22 -0700 Subject: [PATCH 0375/1517] chore: Update approvers/maintainers for recent changes (#748) --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6e47958fe1..b297a77cd1 100644 --- a/README.md +++ b/README.md @@ -103,9 +103,10 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): -- [Alex Boten](https://github.com/codeboten), LightStep - [Carlos Alberto Cortez](https://github.com/carlosalberto), LightStep +- [Chris Kleinknecht](https://github.com/c24t), Google - [Christian Neumüller](https://github.com/Oberon00), Dynatrace +- [Diego Hurtado](https://github.com/ocelotl) - [Hector Hernandez](https://github.com/hectorhdzg), Microsoft - [Leighton Chen](https://github.com/lzchen), Microsoft - [Mauricio Vásquez](https://github.com/mauriciovasquezbernal), Kinvolk @@ -115,7 +116,7 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-telemetry/teams/python-maintainers)): -- [Chris Kleinknecht](https://github.com/c24t), Google +- [Alex Boten](https://github.com/codeboten), LightStep - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Zillow Group *Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).* From bfc54bff58717fb05dc894912e54bd9dcb27d42b Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 29 May 2020 14:02:37 -0700 Subject: [PATCH 0376/1517] ext/system-metrics: adding instrumentation to collect system metrics (#652) Adding an extension to provide users an easy mechanism to collect metrics for their system. --- docs-requirements.txt | 5 +- docs/ext/system_metrics/system_metrics.rst | 7 + .../CHANGELOG.md | 5 + ext/opentelemetry-ext-system-metrics/LICENSE | 201 ++++++++++++++++ .../MANIFEST.in | 9 + .../README.rst | 24 ++ .../setup.cfg | 51 ++++ ext/opentelemetry-ext-system-metrics/setup.py | 26 ++ .../ext/system_metrics/__init__.py | 225 ++++++++++++++++++ .../ext/system_metrics/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/test_system_metrics.py | 203 ++++++++++++++++ .../export/in_memory_metrics_exporter.py | 1 + tox.ini | 10 + 14 files changed, 780 insertions(+), 2 deletions(-) create mode 100644 docs/ext/system_metrics/system_metrics.rst create mode 100644 ext/opentelemetry-ext-system-metrics/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-system-metrics/LICENSE create mode 100644 ext/opentelemetry-ext-system-metrics/MANIFEST.in create mode 100644 ext/opentelemetry-ext-system-metrics/README.rst create mode 100644 ext/opentelemetry-ext-system-metrics/setup.cfg create mode 100644 ext/opentelemetry-ext-system-metrics/setup.py create mode 100644 ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py create mode 100644 ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py create mode 100644 ext/opentelemetry-ext-system-metrics/tests/__init__.py create mode 100644 ext/opentelemetry-ext-system-metrics/tests/test_system_metrics.py diff --git a/docs-requirements.txt b/docs-requirements.txt index 358316fa7e..cc999f1383 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -5,7 +5,7 @@ sphinx-autodoc-typehints~=1.10.2 # Required by ext packages asgiref~=3.0 ddtrace>=0.34.0 -aiohttp ~= 3.0 +aiohttp~= 3.0 Deprecated>=1.2.6 django>=2.2 PyMySQL~=0.9.3 @@ -18,4 +18,5 @@ pymongo~=3.1 redis>=2.6 sqlalchemy>=1.0 thrift>=0.10.0 -wrapt >=1.0.0,<2.0.0 +wrapt>=1.0.0,<2.0.0 +psutil~=5.7.0 diff --git a/docs/ext/system_metrics/system_metrics.rst b/docs/ext/system_metrics/system_metrics.rst new file mode 100644 index 0000000000..db6f9da70f --- /dev/null +++ b/docs/ext/system_metrics/system_metrics.rst @@ -0,0 +1,7 @@ +OpenTelemetry System Metrics Instrumentation +============================================ + +.. automodule:: opentelemetry.ext.system_metrics + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-system-metrics/CHANGELOG.md b/ext/opentelemetry-ext-system-metrics/CHANGELOG.md new file mode 100644 index 0000000000..2f3c3ab382 --- /dev/null +++ b/ext/opentelemetry-ext-system-metrics/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release (https://github.com/open-telemetry/opentelemetry-python/pull/652) diff --git a/ext/opentelemetry-ext-system-metrics/LICENSE b/ext/opentelemetry-ext-system-metrics/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-system-metrics/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-system-metrics/MANIFEST.in b/ext/opentelemetry-ext-system-metrics/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-system-metrics/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-system-metrics/README.rst b/ext/opentelemetry-ext-system-metrics/README.rst new file mode 100644 index 0000000000..796f3f13f2 --- /dev/null +++ b/ext/opentelemetry-ext-system-metrics/README.rst @@ -0,0 +1,24 @@ +OpenTelemetry System Metrics Instrumentation +============================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-system-metrics.svg + :target: https://pypi.org/project/opentelemetry-ext-system-metrics/ + +Instrumentation to collect system performance metrics. + + +Installation +------------ + +:: + + pip install opentelemetry-ext-system-metrics + + +References +---------- +* `OpenTelemetry System Metrics Instrumentation `_ +* `OpenTelemetry Project `_ + diff --git a/ext/opentelemetry-ext-system-metrics/setup.cfg b/ext/opentelemetry-ext-system-metrics/setup.cfg new file mode 100644 index 0000000000..25515eaf0d --- /dev/null +++ b/ext/opentelemetry-ext-system-metrics/setup.cfg @@ -0,0 +1,51 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-system-metrics +description = OpenTelemetry System Metrics Instrumentation +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-system-metrics +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.9.dev0 + psutil ~= 5.7.0 + +[options.extras_require] +test = + opentelemetry-test == 0.9.dev0 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-system-metrics/setup.py b/ext/opentelemetry-ext-system-metrics/setup.py new file mode 100644 index 0000000000..370399094d --- /dev/null +++ b/ext/opentelemetry-ext-system-metrics/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "system_metrics", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py new file mode 100644 index 0000000000..88f36f4ac4 --- /dev/null +++ b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py @@ -0,0 +1,225 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Instrument to report system (CPU, memory, network) and +process (CPU, memory, garbage collection) metrics. By default, the +following metrics are configured: + +"system_memory": ["total", "available", "used", "free"], +"system_cpu": ["user", "system", "idle"], +"network_bytes": ["bytes_recv", "bytes_sent"], +"runtime_memory": ["rss", "vms"], +"runtime_cpu": ["user", "system"], + + +Usage +----- + +.. code:: python + + from opentelemetry.ext.system_metrics import SystemMetrics + from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter + + exporter = ConsoleMetricsExporter() + SystemMetrics(exporter) + + # metrics are collected asynchronously + input("...") + + # to configure custom metrics + configuration = { + "system_memory": ["total", "available", "used", "free", "active", "inactive", "wired"], + "system_cpu": ["user", "nice", "system", "idle"], + "network_bytes": ["bytes_recv", "bytes_sent"], + "runtime_memory": ["rss", "vms"], + "runtime_cpu": ["user", "system"], + } + SystemMetrics(exporter, config=configuration) + +API +--- +""" + +import gc +import os +import typing + +import psutil + +from opentelemetry import metrics +from opentelemetry.sdk.metrics.export import MetricsExporter +from opentelemetry.sdk.metrics.export.controller import PushController + + +class SystemMetrics: + def __init__( + self, + exporter: MetricsExporter, + interval: int = 30, + labels: typing.Optional[typing.Dict[str, str]] = None, + config: typing.Optional[typing.Dict[str, typing.List[str]]] = None, + ): + self._labels = {} if labels is None else labels + self.meter = metrics.get_meter(__name__) + self.controller = PushController( + meter=self.meter, exporter=exporter, interval=interval + ) + if config is None: + self._config = { + "system_memory": ["total", "available", "used", "free"], + "system_cpu": ["user", "system", "idle"], + "network_bytes": ["bytes_recv", "bytes_sent"], + "runtime_memory": ["rss", "vms"], + "runtime_cpu": ["user", "system"], + } + else: + self._config = config + self._proc = psutil.Process(os.getpid()) + self._system_memory_labels = {} + self._system_cpu_labels = {} + self._network_bytes_labels = {} + self._runtime_memory_labels = {} + self._runtime_cpu_labels = {} + self._runtime_gc_labels = {} + # create the label set for each observer once + for key, value in self._labels.items(): + self._system_memory_labels[key] = value + self._system_cpu_labels[key] = value + self._network_bytes_labels[key] = value + self._runtime_memory_labels[key] = value + self._runtime_gc_labels[key] = value + + self.meter.register_observer( + callback=self._get_system_memory, + name="system.mem", + description="System memory", + unit="bytes", + value_type=int, + ) + + self.meter.register_observer( + callback=self._get_system_cpu, + name="system.cpu", + description="System CPU", + unit="seconds", + value_type=float, + ) + + self.meter.register_observer( + callback=self._get_network_bytes, + name="system.net.bytes", + description="System network bytes", + unit="bytes", + value_type=int, + ) + + self.meter.register_observer( + callback=self._get_runtime_memory, + name="runtime.python.mem", + description="Runtime memory", + unit="bytes", + value_type=int, + ) + + self.meter.register_observer( + callback=self._get_runtime_cpu, + name="runtime.python.cpu", + description="Runtime CPU", + unit="seconds", + value_type=float, + ) + + self.meter.register_observer( + callback=self._get_runtime_gc_count, + name="runtime.python.gc.count", + description="Runtime: gc objects", + unit="objects", + value_type=int, + ) + + def _get_system_memory(self, observer: metrics.Observer) -> None: + """Observer callback for memory available + + Args: + observer: the observer to update + """ + system_memory = psutil.virtual_memory() + for metric in self._config["system_memory"]: + self._system_memory_labels["type"] = metric + observer.observe( + getattr(system_memory, metric), self._system_memory_labels + ) + + def _get_system_cpu(self, observer: metrics.Observer) -> None: + """Observer callback for system cpu + + Args: + observer: the observer to update + """ + cpu_times = psutil.cpu_times() + for _type in self._config["system_cpu"]: + self._system_cpu_labels["type"] = _type + observer.observe( + getattr(cpu_times, _type), self._system_cpu_labels + ) + + def _get_network_bytes(self, observer: metrics.Observer) -> None: + """Observer callback for network bytes + + Args: + observer: the observer to update + """ + net_io = psutil.net_io_counters() + for _type in self._config["network_bytes"]: + self._network_bytes_labels["type"] = _type + observer.observe( + getattr(net_io, _type), self._network_bytes_labels + ) + + def _get_runtime_memory(self, observer: metrics.Observer) -> None: + """Observer callback for runtime memory + + Args: + observer: the observer to update + """ + proc_memory = self._proc.memory_info() + for _type in self._config["runtime_memory"]: + self._runtime_memory_labels["type"] = _type + observer.observe( + getattr(proc_memory, _type), self._runtime_memory_labels + ) + + def _get_runtime_cpu(self, observer: metrics.Observer) -> None: + """Observer callback for runtime CPU + + Args: + observer: the observer to update + """ + proc_cpu = self._proc.cpu_times() + for _type in self._config["runtime_cpu"]: + self._runtime_cpu_labels["type"] = _type + observer.observe( + getattr(proc_cpu, _type), self._runtime_cpu_labels + ) + + def _get_runtime_gc_count(self, observer: metrics.Observer) -> None: + """Observer callback for garbage collection + + Args: + observer: the observer to update + """ + gc_count = gc.get_count() + for index, count in enumerate(gc_count): + self._runtime_gc_labels["count"] = str(index) + observer.observe(count, self._runtime_gc_labels) diff --git a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py new file mode 100644 index 0000000000..603bf0b7e5 --- /dev/null +++ b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-system-metrics/tests/__init__.py b/ext/opentelemetry-ext-system-metrics/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-system-metrics/tests/test_system_metrics.py b/ext/opentelemetry-ext-system-metrics/tests/test_system_metrics.py new file mode 100644 index 0000000000..70ead2c515 --- /dev/null +++ b/ext/opentelemetry-ext-system-metrics/tests/test_system_metrics.py @@ -0,0 +1,203 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import namedtuple +from unittest import mock + +from opentelemetry import metrics +from opentelemetry.ext.system_metrics import SystemMetrics +from opentelemetry.test.test_base import TestBase + + +class TestSystemMetrics(TestBase): + def setUp(self): + super().setUp() + self.memory_metrics_exporter.clear() + + def test_system_metrics_constructor(self): + # ensure the observers have been registered + meter = metrics.get_meter(__name__) + with mock.patch("opentelemetry.metrics.get_meter") as mock_get_meter: + mock_get_meter.return_value = meter + SystemMetrics(self.memory_metrics_exporter) + self.assertEqual(len(meter.observers), 6) + observer_names = [ + "system.mem", + "system.cpu", + "system.net.bytes", + "runtime.python.mem", + "runtime.python.cpu", + "runtime.python.gc.count", + ] + for observer in meter.observers: + self.assertIn(observer.name, observer_names) + observer_names.remove(observer.name) + + def _assert_metrics(self, observer_name, system_metrics, expected): + system_metrics.controller.tick() + assertions = 0 + for ( + metric + ) in ( + self.memory_metrics_exporter._exported_metrics # pylint: disable=protected-access + ): + if ( + metric.labels in expected + and metric.metric.name == observer_name + ): + self.assertEqual( + metric.aggregator.checkpoint.last, expected[metric.labels], + ) + assertions += 1 + self.assertEqual(len(expected), assertions) + + def _test_metrics(self, observer_name, expected): + meter = self.meter_provider.get_meter(__name__) + + with mock.patch("opentelemetry.metrics.get_meter") as mock_get_meter: + mock_get_meter.return_value = meter + system_metrics = SystemMetrics(self.memory_metrics_exporter) + self._assert_metrics(observer_name, system_metrics, expected) + + @mock.patch("psutil.cpu_times") + def test_system_cpu(self, mock_cpu_times): + CPUTimes = namedtuple("CPUTimes", ["user", "nice", "system", "idle"]) + mock_cpu_times.return_value = CPUTimes( + user=332277.48, nice=0.0, system=309836.43, idle=6724698.94 + ) + + expected = { + (("type", "user"),): 332277.48, + (("type", "system"),): 309836.43, + (("type", "idle"),): 6724698.94, + } + self._test_metrics("system.cpu", expected) + + @mock.patch("psutil.virtual_memory") + def test_system_memory(self, mock_virtual_memory): + VirtualMemory = namedtuple( + "VirtualMemory", + [ + "total", + "available", + "percent", + "used", + "free", + "active", + "inactive", + "wired", + ], + ) + mock_virtual_memory.return_value = VirtualMemory( + total=17179869184, + available=5520928768, + percent=67.9, + used=10263990272, + free=266964992, + active=5282459648, + inactive=5148700672, + wired=4981530624, + ) + + expected = { + (("type", "total"),): 17179869184, + (("type", "used"),): 10263990272, + (("type", "available"),): 5520928768, + (("type", "free"),): 266964992, + } + self._test_metrics("system.mem", expected) + + @mock.patch("psutil.net_io_counters") + def test_network_bytes(self, mock_net_io_counters): + NetworkIO = namedtuple( + "NetworkIO", + ["bytes_sent", "bytes_recv", "packets_recv", "packets_sent"], + ) + mock_net_io_counters.return_value = NetworkIO( + bytes_sent=23920188416, + bytes_recv=46798894080, + packets_sent=53127118, + packets_recv=53205738, + ) + + expected = { + (("type", "bytes_recv"),): 46798894080, + (("type", "bytes_sent"),): 23920188416, + } + self._test_metrics("system.net.bytes", expected) + + def test_runtime_memory(self): + meter = self.meter_provider.get_meter(__name__) + with mock.patch("opentelemetry.metrics.get_meter") as mock_get_meter: + mock_get_meter.return_value = meter + system_metrics = SystemMetrics(self.memory_metrics_exporter) + + with mock.patch.object( + system_metrics._proc, # pylint: disable=protected-access + "memory_info", + ) as mock_runtime_memory: + RuntimeMemory = namedtuple( + "RuntimeMemory", ["rss", "vms", "pfaults", "pageins"], + ) + mock_runtime_memory.return_value = RuntimeMemory( + rss=9777152, vms=4385665024, pfaults=2631, pageins=49 + ) + expected = { + (("type", "rss"),): 9777152, + (("type", "vms"),): 4385665024, + } + self._assert_metrics( + "runtime.python.mem", system_metrics, expected + ) + + def test_runtime_cpu(self): + meter = self.meter_provider.get_meter(__name__) + with mock.patch("opentelemetry.metrics.get_meter") as mock_get_meter: + mock_get_meter.return_value = meter + system_metrics = SystemMetrics(self.memory_metrics_exporter) + + with mock.patch.object( + system_metrics._proc, # pylint: disable=protected-access + "cpu_times", + ) as mock_runtime_cpu_times: + RuntimeCPU = namedtuple( + "RuntimeCPU", ["user", "nice", "system"] + ) + mock_runtime_cpu_times.return_value = RuntimeCPU( + user=100.48, nice=0.0, system=200.43 + ) + + expected = { + (("type", "user"),): 100.48, + (("type", "system"),): 200.43, + } + + self._assert_metrics( + "runtime.python.cpu", system_metrics, expected + ) + + @mock.patch("gc.get_count") + def test_runtime_gc_count(self, mock_gc): + mock_gc.return_value = [ + 100, # gen0 + 50, # gen1 + 10, # gen2 + ] + + expected = { + (("count", "0"),): 100, + (("count", "1"),): 50, + (("count", "2"),): 10, + } + self._test_metrics("runtime.python.gc.count", expected) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py index a5df2bb1d8..58ea6eba2a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py @@ -41,6 +41,7 @@ def export( ) -> MetricsExportResult: if self._stopped: return MetricsExportResult.FAILURE + with self._lock: self._exported_metrics.extend(metric_records) return MetricsExportResult.SUCCESS diff --git a/tox.ini b/tox.ini index abb1355be9..3cec9fcfbd 100644 --- a/tox.ini +++ b/tox.ini @@ -114,6 +114,11 @@ envlist = py3{4,5,6,7,8}-test-ext-redis pypy3-test-ext-redis + ; opentelemetry-ext-system-metrics + py3{4,5,6,7,8}-test-ext-system-metrics + ; ext-system-metrics intentionally excluded from pypy3 + ; known limitation: gc.get_count won't work under pypy + ; Coverage is temporarily disabled for pypy3 due to the pytest bug. ; pypy3-coverage @@ -162,6 +167,7 @@ changedir = test-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests test-ext-sqlalchemy: ext/opentelemetry-ext-sqlalchemy/tests test-ext-redis: ext/opentelemetry-ext-redis/tests + test-ext-system-metrics: ext/opentelemetry-ext-system-metrics/tests commands_pre = ; Install without -e to test the actual installation @@ -220,6 +226,9 @@ commands_pre = sqlalchemy: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-sqlalchemy + system-metrics: pip install {toxinidir}/opentelemetry-auto-instrumentation + system-metrics: pip install {toxinidir}/ext/opentelemetry-ext-system-metrics[test] + ; In order to get a healthy coverage report, ; we have to install packages in editable mode. coverage: python {toxinidir}/scripts/eachdist.py install --editable @@ -317,6 +326,7 @@ commands_pre = -e {toxinidir}/ext/opentelemetry-ext-pymysql \ -e {toxinidir}/ext/opentelemetry-ext-sqlalchemy \ -e {toxinidir}/ext/opentelemetry-ext-redis \ + -e {toxinidir}/ext/opentelemetry-ext-system-metrics \ -e {toxinidir}/ext/opentelemetry-ext-opencensusexporter docker-compose up -d python check_availability.py From 602ddb0b413dfbcc0553f2483bcc03ec6916f59d Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 29 May 2020 22:42:16 -0400 Subject: [PATCH 0377/1517] sdk: Flush metrics on exit (#749) In PushController before exit, flush the meter by calling tick(), ensuring that all metrics are flushed. Co-authored-by: alrex --- .../src/opentelemetry/sdk/metrics/export/controller.py | 2 ++ opentelemetry-sdk/tests/metrics/export/test_export.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py index 5cbb44e2fd..88abed410a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py @@ -44,6 +44,8 @@ def run(self): def shutdown(self): self.finished.set() + # Run one more collection pass to flush metrics batched in the meter + self.tick() self.exporter.shutdown() if self._atexit_handler is not None: atexit.unregister(self._atexit_handler) diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 3cf305eb7e..c77a132459 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -597,10 +597,15 @@ def test_push_controller(self): controller = PushController(meter, exporter, 5.0) meter.collect.assert_not_called() exporter.export.assert_not_called() + controller.shutdown() self.assertTrue(controller.finished.isSet()) exporter.shutdown.assert_any_call() + # shutdown should flush the meter + self.assertEqual(meter.collect.call_count, 1) + self.assertEqual(exporter.export.call_count, 1) + def test_push_controller_suppress_instrumentation(self): meter = mock.Mock() exporter = mock.Mock() From 24a564d6a38aed98ef5a4dc77ce98811d2ae3723 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Mon, 1 Jun 2020 13:04:15 -0700 Subject: [PATCH 0378/1517] docs: Fix broken link (#763) --- docs/examples/basic_meter/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/basic_meter/README.rst b/docs/examples/basic_meter/README.rst index a48e5bb612..d77fbb7c4b 100644 --- a/docs/examples/basic_meter/README.rst +++ b/docs/examples/basic_meter/README.rst @@ -13,7 +13,7 @@ There are three different examples: * observer: Shows how to use the observer instrument. -The source files of these examples are available :scm_web:`here `. +The source files of these examples are available :scm_web:`here `. Installation ------------ From ce269a36f1678deaa1c2053352499b34fa65f4ca Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 2 Jun 2020 09:49:58 -0700 Subject: [PATCH 0379/1517] Rename Measure to ValueRecorder (#761) --- docs/examples/basic_meter/basic_metrics.py | 4 +- .../basic_meter/calling_conventions.py | 4 +- .../test_otcollector_metrics_exporter.py | 10 +-- .../opentelemetry/ext/prometheus/__init__.py | 4 +- opentelemetry-api/CHANGELOG.md | 3 + .../src/opentelemetry/metrics/__init__.py | 55 +++++------- .../tests/metrics/test_metrics.py | 20 ++--- opentelemetry-sdk/CHANGELOG.md | 3 + .../src/opentelemetry/sdk/metrics/__init__.py | 12 +-- .../sdk/metrics/export/aggregate.py | 2 +- .../sdk/metrics/export/batcher.py | 4 +- .../tests/metrics/test_metrics.py | 84 ++++++++++--------- 12 files changed, 103 insertions(+), 102 deletions(-) diff --git a/docs/examples/basic_meter/basic_metrics.py b/docs/examples/basic_meter/basic_metrics.py index 5d137bf01e..6e8a5c040f 100644 --- a/docs/examples/basic_meter/basic_metrics.py +++ b/docs/examples/basic_meter/basic_metrics.py @@ -24,7 +24,7 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider +from opentelemetry.sdk.metrics import Counter, MeterProvider, ValueRecorder from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController @@ -80,7 +80,7 @@ def usage(argv): description="size of requests", unit="1", value_type=int, - metric_type=Measure, + metric_type=ValueRecorder, label_keys=("environment",), ) diff --git a/docs/examples/basic_meter/calling_conventions.py b/docs/examples/basic_meter/calling_conventions.py index 15b57fdafc..f8cc3dddbb 100644 --- a/docs/examples/basic_meter/calling_conventions.py +++ b/docs/examples/basic_meter/calling_conventions.py @@ -19,7 +19,7 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider +from opentelemetry.sdk.metrics import Counter, MeterProvider, ValueRecorder from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController @@ -43,7 +43,7 @@ description="size of requests", unit="1", value_type=int, - metric_type=Measure, + metric_type=ValueRecorder, label_keys=("environment",), ) diff --git a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py index 63ea28cd93..18b4a32806 100644 --- a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py +++ b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py @@ -23,8 +23,8 @@ from opentelemetry.ext.opencensusexporter import metrics_exporter from opentelemetry.sdk.metrics import ( Counter, - Measure, MeterProvider, + ValueRecorder, get_labels_as_key, ) from opentelemetry.sdk.metrics.export import ( @@ -76,7 +76,7 @@ def test_get_collector_metric_type(self): ) self.assertIs(result, metrics_pb2.MetricDescriptor.CUMULATIVE_DOUBLE) result = metrics_exporter.get_collector_metric_type( - Measure("testName", "testDescription", "unit", None, None) + ValueRecorder("testName", "testDescription", "unit", None, None) ) self.assertIs(result, metrics_pb2.MetricDescriptor.UNSPECIFIED) @@ -88,8 +88,8 @@ def test_get_collector_point(self): float_counter = self._meter.create_metric( "testName", "testDescription", "unit", float, Counter ) - measure = self._meter.create_metric( - "testName", "testDescription", "unit", float, Measure + valuerecorder = self._meter.create_metric( + "testName", "testDescription", "unit", float, ValueRecorder ) result = metrics_exporter.get_collector_point( MetricRecord(aggregator, self._key_labels, int_counter) @@ -106,7 +106,7 @@ def test_get_collector_point(self): self.assertRaises( TypeError, metrics_exporter.get_collector_point( - MetricRecord(aggregator, self._key_labels, measure) + MetricRecord(aggregator, self._key_labels, valuerecorder) ), ) diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py index f6f91fc586..cc44621ac4 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py @@ -79,7 +79,7 @@ UnknownMetricFamily, ) -from opentelemetry.metrics import Counter, Measure, Metric +from opentelemetry.metrics import Counter, Metric, ValueRecorder from opentelemetry.sdk.metrics.export import ( MetricRecord, MetricsExporter, @@ -164,7 +164,7 @@ def _translate_to_prometheus(self, metric_record: MetricRecord): labels=label_values, value=metric_record.aggregator.checkpoint ) # TODO: Add support for histograms when supported in OT - elif isinstance(metric_record.metric, Measure): + elif isinstance(metric_record.metric, ValueRecorder): prometheus_metric = UnknownMetricFamily( name=metric_name, documentation=metric_record.metric.description, diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 386bd75d47..4b678cd422 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Rename Measure to ValueRecorder in metrics + ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) + ## 0.8b0 Released 2020-05-27 diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index b7ad62adb2..a91a4ce7b7 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -19,16 +19,13 @@ The `Meter` class is used to construct `Metric` s to record raw statistics as well as metrics with predefined aggregation. +`Meter` s can be obtained via the `MeterProvider`, corresponding to the name +of the instrumenting library and (optionally) a version. + See the `metrics api`_ spec for terminology and context clarification. .. _metrics api: - https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-metrics.md - -.. versionadded:: 0.1.0 -.. versionchanged:: 0.5.0 - ``meter_provider`` was replaced by `get_meter_provider`, - ``set_preferred_meter_provider_implementation`` was replaced by - `set_meter_provider`. + https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/metrics/api.md """ import abc from logging import getLogger @@ -54,7 +51,7 @@ def add(self, value: ValueT) -> None: """ def record(self, value: ValueT) -> None: - """No-op implementation of `BoundMeasure` record. + """No-op implementation of `BoundValueRecorder` record. Args: value: The value to record to the bound metric instrument. @@ -73,12 +70,12 @@ def add(self, value: ValueT) -> None: """ -class BoundMeasure: +class BoundValueRecorder: def record(self, value: ValueT) -> None: - """Records the given ``value`` to this bound measure. + """Records the given ``value`` to this bound valuerecorder. Args: - value: The value to record to the bound measure. + value: The value to record to the bound valuerecorder. """ @@ -94,12 +91,7 @@ def bind(self, labels: Dict[str, str]) -> "object": """Gets a bound metric instrument. Bound metric instruments are useful to reduce the cost of repeatedly - recording a metric with a pre-defined set of label values. All metric - kinds (counter, measure) support declaring a set of required label - keys. The values corresponding to these keys should be specified in - every bound metric instrument. "Unspecified" label values, in cases - where a bound metric instrument is requested but a value was not - provided are permitted. + recording a metric with a pre-defined set of label values. Args: labels: Labels to associate with the bound instrument. @@ -126,10 +118,10 @@ def add(self, value: ValueT, labels: Dict[str, str]) -> None: """ def record(self, value: ValueT, labels: Dict[str, str]) -> None: - """No-op implementation of `Measure` record. + """No-op implementation of `ValueRecorder` record. Args: - value: The value to record to this measure metric. + value: The value to record to this valuerecorder metric. labels: Labels to associate with the bound instrument. """ @@ -150,21 +142,18 @@ def add(self, value: ValueT, labels: Dict[str, str]) -> None: """ -class Measure(Metric): - """A measure type metric that represent raw stats that are recorded. - - Measure metrics represent raw statistics that are recorded. - """ +class ValueRecorder(Metric): + """A valuerecorder type metric that represent raw stats.""" - def bind(self, labels: Dict[str, str]) -> "BoundMeasure": - """Gets a `BoundMeasure`.""" - return BoundMeasure() + def bind(self, labels: Dict[str, str]) -> "BoundValueRecorder": + """Gets a `BoundValueRecorder`.""" + return BoundValueRecorder() def record(self, value: ValueT, labels: Dict[str, str]) -> None: - """Records the ``value`` to the measure. + """Records the ``value`` to the valuerecorder. Args: - value: The value to record to this measure metric. + value: The value to record to this valuerecorder metric. labels: Labels to associate with the bound instrument. """ @@ -251,7 +240,7 @@ def get_meter( return DefaultMeter() -MetricT = TypeVar("MetricT", Counter, Measure, Observer) +MetricT = TypeVar("MetricT", Counter, ValueRecorder, Observer) ObserverCallbackT = Callable[[Observer], None] @@ -259,9 +248,9 @@ def get_meter( class Meter(abc.ABC): """An interface to allow the recording of metrics. - `Metric` s are used for recording pre-defined aggregation (counter), - or raw values (measure) in which the aggregation and labels - for the exported metric are deferred. + `Metric` s or metric instruments, are devices used for capturing raw + measurements. Each metric instrument supports a single method, each with + fixed interpretation to capture measurements. """ @abc.abstractmethod diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 3e760d3d98..897c7492e4 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -35,14 +35,14 @@ def test_counter_add(self): counter = metrics.Counter() counter.add(1, {}) - def test_measure(self): - measure = metrics.Measure() - bound_measure = measure.bind({}) - self.assertIsInstance(bound_measure, metrics.BoundMeasure) + def test_valuerecorder(self): + valuerecorder = metrics.ValueRecorder() + bound_valuerecorder = valuerecorder.bind({}) + self.assertIsInstance(bound_valuerecorder, metrics.BoundValueRecorder) - def test_measure_record(self): - measure = metrics.Measure() - measure.record(1, {}) + def test_valuerecorder_record(self): + valuerecorder = metrics.ValueRecorder() + valuerecorder.record(1, {}) def test_default_bound_metric(self): bound_instrument = metrics.DefaultBoundInstrument() @@ -52,9 +52,9 @@ def test_bound_counter(self): bound_counter = metrics.BoundCounter() bound_counter.add(1) - def test_bound_measure(self): - bound_measure = metrics.BoundMeasure() - bound_measure.record(1) + def test_bound_valuerecorder(self): + bound_valuerecorder = metrics.BoundValueRecorder() + bound_valuerecorder.record(1) def test_observer(self): observer = metrics.DefaultObserver() diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 50ea751e15..29aaed01dc 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Rename Measure to ValueRecorder in metrics + ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) + ## 0.8b0 Released 2020-05-27 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 1d35648fd3..b9284bae9f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -97,9 +97,9 @@ def add(self, value: metrics_api.ValueT) -> None: self.update(value) -class BoundMeasure(metrics_api.BoundMeasure, BaseBoundInstrument): +class BoundValueRecorder(metrics_api.BoundValueRecorder, BaseBoundInstrument): def record(self, value: metrics_api.ValueT) -> None: - """See `opentelemetry.metrics.BoundMeasure.record`.""" + """See `opentelemetry.metrics.BoundValueRecorder.record`.""" if self._validate_update(value): self.update(value) @@ -174,15 +174,15 @@ def add(self, value: metrics_api.ValueT, labels: Dict[str, str]) -> None: UPDATE_FUNCTION = add -class Measure(Metric, metrics_api.Measure): - """See `opentelemetry.metrics.Measure`.""" +class ValueRecorder(Metric, metrics_api.ValueRecorder): + """See `opentelemetry.metrics.ValueRecorder`.""" - BOUND_INSTR_TYPE = BoundMeasure + BOUND_INSTR_TYPE = BoundValueRecorder def record( self, value: metrics_api.ValueT, labels: Dict[str, str] ) -> None: - """See `opentelemetry.metrics.Measure.record`.""" + """See `opentelemetry.metrics.ValueRecorder.record`.""" bound_intrument = self.bind(labels) bound_intrument.record(value) bound_intrument.release() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index ea8c40a7e7..7e1baba2c7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -72,7 +72,7 @@ def merge(self, other): class MinMaxSumCountAggregator(Aggregator): - """Agregator for Measure metrics that keeps min, max, sum and count.""" + """Aggregator for ValueRecorder metrics that keeps min, max, sum, count.""" _TYPE = namedtuple("minmaxsumcount", "min max sum count") _EMPTY = _TYPE(None, None, None, 0) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 7b599f4c7d..eda504d568 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -15,7 +15,7 @@ import abc from typing import Sequence, Type -from opentelemetry.metrics import Counter, Measure, MetricT, Observer +from opentelemetry.metrics import Counter, MetricT, Observer, ValueRecorder from opentelemetry.sdk.metrics.export import MetricRecord from opentelemetry.sdk.metrics.export.aggregate import ( Aggregator, @@ -49,7 +49,7 @@ def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: # pylint:disable=R0201 if issubclass(metric_type, Counter): return CounterAggregator() - if issubclass(metric_type, Measure): + if issubclass(metric_type, ValueRecorder): return MinMaxSumCountAggregator() if issubclass(metric_type, Observer): return ObserverAggregator() diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 3298064705..a3c0f4294d 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -109,14 +109,14 @@ def test_record_batch_multiple(self): counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) - measure = metrics.Measure( + valuerecorder = metrics.ValueRecorder( "name", "desc", "unit", float, meter, label_keys ) - record_tuples = [(counter, 1.0), (measure, 3.0)] + record_tuples = [(counter, 1.0), (valuerecorder, 3.0)] meter.record_batch(labels, record_tuples) self.assertEqual(counter.bind(labels).aggregator.current, 1.0) self.assertEqual( - measure.bind(labels).aggregator.current, (3.0, 3.0, 3.0, 1) + valuerecorder.bind(labels).aggregator.current, (3.0, 3.0, 3.0, 1) ) def test_record_batch_exists(self): @@ -145,14 +145,14 @@ def test_create_metric(self): self.assertEqual(counter.name, "name") self.assertIs(counter.meter.resource, resource) - def test_create_measure(self): + def test_create_valuerecorder(self): meter = metrics.MeterProvider().get_meter(__name__) - measure = meter.create_metric( - "name", "desc", "unit", float, metrics.Measure, () + valuerecorder = meter.create_metric( + "name", "desc", "unit", float, metrics.ValueRecorder, () ) - self.assertIsInstance(measure, metrics.Measure) - self.assertEqual(measure.value_type, float) - self.assertEqual(measure.name, "name") + self.assertIsInstance(valuerecorder, metrics.ValueRecorder) + self.assertEqual(valuerecorder.value_type, float) + self.assertEqual(valuerecorder.name, "name") def test_register_observer(self): meter = metrics.MeterProvider().get_meter(__name__) @@ -197,19 +197,19 @@ def test_direct_call_release_bound_instrument(self): meter.metrics.add(counter) counter.add(4.0, labels) - measure = metrics.Measure( + valuerecorder = metrics.ValueRecorder( "name", "desc", "unit", float, meter, label_keys ) - meter.metrics.add(measure) - measure.record(42.0, labels) + meter.metrics.add(valuerecorder) + valuerecorder.record(42.0, labels) self.assertEqual(len(counter.bound_instruments), 1) - self.assertEqual(len(measure.bound_instruments), 1) + self.assertEqual(len(valuerecorder.bound_instruments), 1) meter.collect() self.assertEqual(len(counter.bound_instruments), 0) - self.assertEqual(len(measure.bound_instruments), 0) + self.assertEqual(len(valuerecorder.bound_instruments), 0) def test_release_bound_instrument(self): meter = metrics.MeterProvider().get_meter(__name__) @@ -223,30 +223,30 @@ def test_release_bound_instrument(self): bound_counter = counter.bind(labels) bound_counter.add(4.0) - measure = metrics.Measure( + valuerecorder = metrics.ValueRecorder( "name", "desc", "unit", float, meter, label_keys ) - meter.metrics.add(measure) - bound_measure = measure.bind(labels) - bound_measure.record(42) + meter.metrics.add(valuerecorder) + bound_valuerecorder = valuerecorder.bind(labels) + bound_valuerecorder.record(42) bound_counter.release() - bound_measure.release() + bound_valuerecorder.release() # be sure that bound instruments are only released after collection self.assertEqual(len(counter.bound_instruments), 1) - self.assertEqual(len(measure.bound_instruments), 1) + self.assertEqual(len(valuerecorder.bound_instruments), 1) meter.collect() self.assertEqual(len(counter.bound_instruments), 0) - self.assertEqual(len(measure.bound_instruments), 0) + self.assertEqual(len(valuerecorder.bound_instruments), 0) class TestMetric(unittest.TestCase): def test_bind(self): meter = metrics.MeterProvider().get_meter(__name__) - metric_types = [metrics.Counter, metrics.Measure] + metric_types = [metrics.Counter, metrics.ValueRecorder] labels = {"key": "value"} key_labels = tuple(sorted(labels.items())) for _type in metric_types: @@ -268,17 +268,19 @@ def test_add(self): self.assertEqual(bound_counter.aggregator.current, 5) -class TestMeasure(unittest.TestCase): +class TestValueRecorder(unittest.TestCase): def test_record(self): meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.Measure("name", "desc", "unit", int, meter, ("key",)) + metric = metrics.ValueRecorder( + "name", "desc", "unit", int, meter, ("key",) + ) labels = {"key": "value"} - bound_measure = metric.bind(labels) + bound_valuerecorder = metric.bind(labels) values = (37, 42, 7) for val in values: metric.record(val, labels) self.assertEqual( - bound_measure.aggregator.current, + bound_valuerecorder.aggregator.current, (min(values), max(values), sum(values), len(values)), ) @@ -375,33 +377,37 @@ def test_update(self): self.assertEqual(bound_counter.aggregator.current, 4.0) -class TestBoundMeasure(unittest.TestCase): +class TestBoundValueRecorder(unittest.TestCase): def test_record(self): aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_measure = metrics.BoundMeasure(int, True, aggregator) - bound_measure.record(3) - self.assertEqual(bound_measure.aggregator.current, (3, 3, 3, 1)) + bound_valuerecorder = metrics.BoundValueRecorder(int, True, aggregator) + bound_valuerecorder.record(3) + self.assertEqual(bound_valuerecorder.aggregator.current, (3, 3, 3, 1)) def test_record_disabled(self): aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_measure = metrics.BoundMeasure(int, False, aggregator) - bound_measure.record(3) + bound_valuerecorder = metrics.BoundValueRecorder( + int, False, aggregator + ) + bound_valuerecorder.record(3) self.assertEqual( - bound_measure.aggregator.current, (None, None, None, 0) + bound_valuerecorder.aggregator.current, (None, None, None, 0) ) @mock.patch("opentelemetry.sdk.metrics.logger") def test_record_incorrect_type(self, logger_mock): aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_measure = metrics.BoundMeasure(int, True, aggregator) - bound_measure.record(3.0) + bound_valuerecorder = metrics.BoundValueRecorder(int, True, aggregator) + bound_valuerecorder.record(3.0) self.assertEqual( - bound_measure.aggregator.current, (None, None, None, 0) + bound_valuerecorder.aggregator.current, (None, None, None, 0) ) self.assertTrue(logger_mock.warning.called) def test_update(self): aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_measure = metrics.BoundMeasure(int, True, aggregator) - bound_measure.update(4.0) - self.assertEqual(bound_measure.aggregator.current, (4.0, 4.0, 4.0, 1)) + bound_valuerecorder = metrics.BoundValueRecorder(int, True, aggregator) + bound_valuerecorder.update(4.0) + self.assertEqual( + bound_valuerecorder.aggregator.current, (4.0, 4.0, 4.0, 1) + ) From 3ad6ac50ff7d84507ff8e9aaf0437484d1de8b24 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 2 Jun 2020 16:37:56 -0600 Subject: [PATCH 0380/1517] ext/boto: Add boto instrumentation (#665) --- docs-requirements.txt | 1 + docs/ext/boto/boto.rst | 7 + ext/opentelemetry-ext-boto/CHANGELOG.md | 5 + ext/opentelemetry-ext-boto/LICENSE | 201 ++++++++++++++ ext/opentelemetry-ext-boto/MANIFEST.in | 9 + ext/opentelemetry-ext-boto/README.rst | 23 ++ ext/opentelemetry-ext-boto/setup.cfg | 58 +++++ ext/opentelemetry-ext-boto/setup.py | 26 ++ .../src/opentelemetry/ext/boto/__init__.py | 245 ++++++++++++++++++ .../src/opentelemetry/ext/boto/version.py | 15 ++ ext/opentelemetry-ext-boto/tests/__init__.py | 0 ext/opentelemetry-ext-boto/tests/conftest.py | 31 +++ .../tests/test_boto_instrumentation.py | 242 +++++++++++++++++ tox.ini | 9 + 14 files changed, 872 insertions(+) create mode 100644 docs/ext/boto/boto.rst create mode 100644 ext/opentelemetry-ext-boto/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-boto/LICENSE create mode 100644 ext/opentelemetry-ext-boto/MANIFEST.in create mode 100644 ext/opentelemetry-ext-boto/README.rst create mode 100644 ext/opentelemetry-ext-boto/setup.cfg create mode 100644 ext/opentelemetry-ext-boto/setup.py create mode 100644 ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py create mode 100644 ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py create mode 100644 ext/opentelemetry-ext-boto/tests/__init__.py create mode 100644 ext/opentelemetry-ext-boto/tests/conftest.py create mode 100644 ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py diff --git a/docs-requirements.txt b/docs-requirements.txt index cc999f1383..a61f0beedb 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -20,3 +20,4 @@ sqlalchemy>=1.0 thrift>=0.10.0 wrapt>=1.0.0,<2.0.0 psutil~=5.7.0 +boto~=2.0 diff --git a/docs/ext/boto/boto.rst b/docs/ext/boto/boto.rst new file mode 100644 index 0000000000..8bf40c7566 --- /dev/null +++ b/docs/ext/boto/boto.rst @@ -0,0 +1,7 @@ +OpenTelemetry Boto Integration +============================== + +.. automodule:: opentelemetry.ext.boto + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-boto/CHANGELOG.md b/ext/opentelemetry-ext-boto/CHANGELOG.md new file mode 100644 index 0000000000..3e04402cea --- /dev/null +++ b/ext/opentelemetry-ext-boto/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release diff --git a/ext/opentelemetry-ext-boto/LICENSE b/ext/opentelemetry-ext-boto/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-boto/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-boto/MANIFEST.in b/ext/opentelemetry-ext-boto/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-boto/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-boto/README.rst b/ext/opentelemetry-ext-boto/README.rst new file mode 100644 index 0000000000..e149ec424e --- /dev/null +++ b/ext/opentelemetry-ext-boto/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry Boto Tracing +========================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-boto.svg + :target: https://pypi.org/project/opentelemetry-ext-boto/ + +This library allows tracing requests made by the Boto library. + +Installation +------------ + +:: + + pip install opentelemetry-ext-boto + + +References +---------- + +* `OpenTelemetry Boto Tracing `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-boto/setup.cfg b/ext/opentelemetry-ext-boto/setup.cfg new file mode 100644 index 0000000000..529e79be99 --- /dev/null +++ b/ext/opentelemetry-ext-boto/setup.cfg @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-boto +description = Boto tracing for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-boto +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + boto ~= 2.0 + opentelemetry-api == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 + +[options.extras_require] +test = + boto~=2.0 + moto~=1.0 + opentelemetry-test == 0.9.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + django = opentelemetry.ext.boto:BotoInstrumentor diff --git a/ext/opentelemetry-ext-boto/setup.py b/ext/opentelemetry-ext-boto/setup.py new file mode 100644 index 0000000000..4c78e9b35f --- /dev/null +++ b/ext/opentelemetry-ext-boto/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "boto", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py new file mode 100644 index 0000000000..fa66fda61d --- /dev/null +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py @@ -0,0 +1,245 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Instrument `Boto`_ to trace service requests. + +There are two options for instrumenting code. The first option is to use the +``opentelemetry-auto-instrumentation`` executable which will automatically +instrument your Boto client. The second is to programmatically enable +instrumentation via the following code: + +.. _boto: https://pypi.org/project/boto/ + +Usage +----- + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext.boto import BotoInstrumentor + from opentelemetry.sdk.trace import TracerProvider + import boto + + trace.set_tracer_provider(TracerProvider()) + + # Instrument Boto + BotoInstrumentor().instrument(tracer_provider=trace.get_tracer_provider()) + + # This will create a span with Boto-specific attributes + ec2 = boto.ec2.connect_to_region("us-west-2") + ec2.get_all_instances() + +API +--- +""" + +import logging +from inspect import currentframe + +from boto.connection import AWSAuthConnection, AWSQueryConnection +from wrapt import ObjectProxy, wrap_function_wrapper + +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.ext.boto.version import __version__ +from opentelemetry.trace import SpanKind, get_tracer + +logger = logging.getLogger(__name__) + + +def _get_instance_region_name(instance): + region = getattr(instance, "region", None) + + if not region: + return None + if isinstance(region, str): + return region.split(":")[1] + return region.name + + +class BotoInstrumentor(BaseInstrumentor): + """A instrumentor for Boto + + See `BaseInstrumentor` + """ + + def __init__(self): + super().__init__() + self._original_boto = None + + def _instrument(self, **kwargs): + # AWSQueryConnection and AWSAuthConnection are two different classes + # called by different services for connection. + # For exemple EC2 uses AWSQueryConnection and S3 uses + # AWSAuthConnection + + # FIXME should the tracer provider be accessed via Configuration + # instead? + # pylint: disable=attribute-defined-outside-init + self._tracer = get_tracer( + __name__, __version__, kwargs.get("tracer_provider") + ) + + wrap_function_wrapper( + "boto.connection", + "AWSQueryConnection.make_request", + self._patched_query_request, + ) + wrap_function_wrapper( + "boto.connection", + "AWSAuthConnection.make_request", + self._patched_auth_request, + ) + + def _uninstrument(self, **kwargs): + unwrap(AWSQueryConnection, "make_request") + unwrap(AWSAuthConnection, "make_request") + + def _common_request( # pylint: disable=too-many-locals + self, + args_name, + traced_args, + operation_name, + original_func, + instance, + args, + kwargs, + ): + + endpoint_name = getattr(instance, "host").split(".")[0] + + with self._tracer.start_as_current_span( + "{}.command".format(endpoint_name), kind=SpanKind.CONSUMER, + ) as span: + if args: + http_method = args[0] + span.resource = "%s.%s" % (endpoint_name, http_method.lower()) + else: + span.resource = endpoint_name + + add_span_arg_tags( + span, endpoint_name, args, args_name, traced_args, + ) + + # Obtaining region name + region_name = _get_instance_region_name(instance) + + meta = { + "aws.agent": "boto", + "aws.operation": operation_name, + } + if region_name: + meta["aws.region"] = region_name + + for key, value in meta.items(): + span.set_attribute(key, value) + + # Original func returns a boto.connection.HTTPResponse object + result = original_func(*args, **kwargs) + span.set_attribute("http.status_code", getattr(result, "status")) + span.set_attribute("http.method", getattr(result, "_method")) + + return result + + def _patched_query_request(self, original_func, instance, args, kwargs): + + return self._common_request( + ("operation_name", "params", "path", "verb"), + ["operation_name", "params", "path"], + args[0] if args else None, + original_func, + instance, + args, + kwargs, + ) + + def _patched_auth_request(self, original_func, instance, args, kwargs): + operation_name = None + + frame = currentframe().f_back + operation_name = None + while frame: + if frame.f_code.co_name == "make_request": + operation_name = frame.f_back.f_code.co_name + break + frame = frame.f_back + + return self._common_request( + ( + "method", + "path", + "headers", + "data", + "host", + "auth_path", + "sender", + ), + ["path", "data", "host"], + operation_name, + original_func, + instance, + args, + kwargs, + ) + + +def truncate_arg_value(value, max_len=1024): + """Truncate values which are bytes and greater than ``max_len``. + Useful for parameters like "Body" in ``put_object`` operations. + """ + if isinstance(value, bytes) and len(value) > max_len: + return b"..." + + return value + + +def add_span_arg_tags(span, endpoint_name, args, args_names, args_traced): + if endpoint_name not in ["kms", "sts"]: + tags = dict( + (name, value) + for (name, value) in zip(args_names, args) + if name in args_traced + ) + tags = flatten_dict(tags) + for key, value in { + k: truncate_arg_value(v) + for k, v in tags.items() + if k not in {"s3": ["params.Body"]}.get(endpoint_name, []) + }.items(): + span.set_attribute(key, value) + + +def flatten_dict(dict_, sep=".", prefix=""): + """ + Returns a normalized dict of depth 1 with keys in order of embedding + """ + # adapted from https://stackoverflow.com/a/19647596 + return ( + { + prefix + sep + k if prefix else k: v + for kk, vv in dict_.items() + for k, v in flatten_dict(vv, sep, kk).items() + } + if isinstance(dict_, dict) + else {prefix: dict_} + ) + + +def unwrap(obj, attr): + function = getattr(obj, attr, None) + if ( + function + and isinstance(function, ObjectProxy) + and hasattr(function, "__wrapped__") + ): + setattr(obj, attr, function.__wrapped__) diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py new file mode 100644 index 0000000000..bcf6a35777 --- /dev/null +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-boto/tests/__init__.py b/ext/opentelemetry-ext-boto/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-boto/tests/conftest.py b/ext/opentelemetry-ext-boto/tests/conftest.py new file mode 100644 index 0000000000..884c6753c1 --- /dev/null +++ b/ext/opentelemetry-ext-boto/tests/conftest.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import environ + + +def pytest_sessionstart(session): + # pylint: disable=unused-argument + environ["AWS_ACCESS_KEY_ID"] = "testing" + environ["AWS_SECRET_ACCESS_KEY"] = "testing" + environ["AWS_SECURITY_TOKEN"] = "testing" + environ["AWS_SESSION_TOKEN"] = "testing" + + +def pytest_sessionfinish(session): + # pylint: disable=unused-argument + environ.pop("AWS_ACCESS_KEY_ID") + environ.pop("AWS_SECRET_ACCESS_KEY") + environ.pop("AWS_SECURITY_TOKEN") + environ.pop("AWS_SESSION_TOKEN") diff --git a/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py b/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py new file mode 100644 index 0000000000..492fac5a88 --- /dev/null +++ b/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py @@ -0,0 +1,242 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import skipUnless + +import boto.awslambda +import boto.ec2 +import boto.elasticache +import boto.s3 +import boto.sts + +from moto import ( # pylint: disable=import-error + mock_ec2_deprecated, + mock_lambda_deprecated, + mock_s3_deprecated, + mock_sts_deprecated, +) +from opentelemetry.ext.boto import BotoInstrumentor +from opentelemetry.test.test_base import TestBase + + +def assert_span_http_status_code(span, code): + """Assert on the span's 'http.status_code' tag""" + tag = span.attributes["http.status_code"] + assert tag == code, "%r != %r" % (tag, code) + + +class TestBotoInstrumentor(TestBase): + """Botocore integration testsuite""" + + def setUp(self): + super().setUp() + BotoInstrumentor().instrument() + + def tearDown(self): + BotoInstrumentor().uninstrument() + + @mock_ec2_deprecated + def test_ec2_client(self): + ec2 = boto.ec2.connect_to_region("us-west-2") + + ec2.get_all_instances() + + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.attributes["aws.operation"], "DescribeInstances") + assert_span_http_status_code(span, 200) + self.assertEqual(span.attributes["http.method"], "POST") + self.assertEqual(span.attributes["aws.region"], "us-west-2") + + # Create an instance + ec2.run_instances(21) + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 2) + span = spans[1] + self.assertEqual(span.attributes["aws.operation"], "RunInstances") + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "ec2.runinstances") + self.assertEqual(span.attributes["http.method"], "POST") + self.assertEqual(span.attributes["aws.region"], "us-west-2") + self.assertEqual(span.name, "ec2.command") + + @mock_ec2_deprecated + def test_analytics_enabled_with_rate(self): + ec2 = boto.ec2.connect_to_region("us-west-2") + + ec2.get_all_instances() + + spans = self.memory_exporter.get_finished_spans() + assert spans + + @mock_ec2_deprecated + def test_analytics_enabled_without_rate(self): + ec2 = boto.ec2.connect_to_region("us-west-2") + + ec2.get_all_instances() + + spans = self.memory_exporter.get_finished_spans() + assert spans + + @mock_s3_deprecated + def test_s3_client(self): + s3 = boto.s3.connect_to_region("us-east-1") + + s3.get_all_buckets() + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 1) + span = spans[0] + assert_span_http_status_code(span, 200) + self.assertEqual(span.attributes["http.method"], "GET") + self.assertEqual(span.attributes["aws.operation"], "get_all_buckets") + + # Create a bucket command + s3.create_bucket("cheese") + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 2) + span = spans[1] + assert_span_http_status_code(span, 200) + self.assertEqual(span.attributes["http.method"], "PUT") + self.assertEqual(span.attributes["path"], "/") + self.assertEqual(span.attributes["aws.operation"], "create_bucket") + + # Get the created bucket + s3.get_bucket("cheese") + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 3) + span = spans[2] + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "s3.head") + self.assertEqual(span.attributes["http.method"], "HEAD") + self.assertEqual(span.attributes["aws.operation"], "head_bucket") + self.assertEqual(span.name, "s3.command") + + # Checking for resource incase of error + try: + s3.get_bucket("big_bucket") + except Exception: # pylint: disable=broad-except + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[2] + self.assertEqual(span.resource, "s3.head") + + @mock_s3_deprecated + def test_s3_put(self): + s3 = boto.s3.connect_to_region("us-east-1") + s3.create_bucket("mybucket") + bucket = s3.get_bucket("mybucket") + key = boto.s3.key.Key(bucket) + key.key = "foo" + key.set_contents_from_string("bar") + + spans = self.memory_exporter.get_finished_spans() + assert spans + # create bucket + self.assertEqual(len(spans), 3) + self.assertEqual(spans[0].attributes["aws.operation"], "create_bucket") + assert_span_http_status_code(spans[0], 200) + self.assertEqual(spans[0].resource, "s3.put") + # get bucket + self.assertEqual(spans[1].attributes["aws.operation"], "head_bucket") + self.assertEqual(spans[1].resource, "s3.head") + # put object + self.assertEqual( + spans[2].attributes["aws.operation"], "_send_file_internal" + ) + self.assertEqual(spans[2].resource, "s3.put") + + @mock_lambda_deprecated + def test_unpatch(self): + + lamb = boto.awslambda.connect_to_region("us-east-2") + + BotoInstrumentor().uninstrument() + + # multiple calls + lamb.list_functions() + spans = self.memory_exporter.get_finished_spans() + assert not spans, spans + + @mock_s3_deprecated + def test_double_patch(self): + s3 = boto.s3.connect_to_region("us-east-1") + + BotoInstrumentor().instrument() + BotoInstrumentor().instrument() + + # Get the created bucket + s3.create_bucket("cheese") + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 1) + + @mock_lambda_deprecated + def test_lambda_client(self): + lamb = boto.awslambda.connect_to_region("us-east-2") + + # multiple calls + lamb.list_functions() + lamb.list_functions() + + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 2) + span = spans[0] + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "lambda.get") + self.assertEqual(span.attributes["http.method"], "GET") + self.assertEqual(span.attributes["aws.region"], "us-east-2") + self.assertEqual(span.attributes["aws.operation"], "list_functions") + + @mock_sts_deprecated + def test_sts_client(self): + sts = boto.sts.connect_to_region("us-west-2") + + sts.get_federation_token(12, duration=10) + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(span.resource, "sts.getfederationtoken") + self.assertEqual(span.attributes["aws.region"], "us-west-2") + self.assertEqual( + span.attributes["aws.operation"], "GetFederationToken" + ) + + # checking for protection on sts against security leak + self.assertTrue("args.path" not in span.attributes.keys()) + + @skipUnless( + False, + ( + "Test to reproduce the case where args sent to patched function " + "are None, can't be mocked: needs AWS credentials" + ), + ) + def test_elasticache_client(self): + elasticache = boto.elasticache.connect_to_region("us-west-2") + + elasticache.describe_cache_clusters() + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(span.resource, "elasticache") + self.assertEqual(span.attributes["aws.region"], "us-west-2") diff --git a/tox.ini b/tox.ini index 3cec9fcfbd..978155616a 100644 --- a/tox.ini +++ b/tox.ini @@ -36,6 +36,10 @@ envlist = py3{4,5,6,7,8}-test-ext-dbapi pypy3-test-ext-dbapi + ; opentelemetry-ext-boto + py3{5,6,7,8}-test-ext-boto + pypy3-test-ext-boto + ; opentelemetry-ext-flask py3{4,5,6,7,8}-test-ext-flask pypy3-test-ext-flask @@ -161,6 +165,7 @@ changedir = test-ext-sqlite3: ext/opentelemetry-ext-sqlite3/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests + test-ext-boto: ext/opentelemetry-ext-boto/tests test-ext-flask: ext/opentelemetry-ext-flask/tests test-example-app: docs/examples/opentelemetry-example-app/tests test-getting-started: docs/getting_started/tests @@ -188,6 +193,10 @@ commands_pre = wsgi,flask,django: pip install {toxinidir}/ext/opentelemetry-ext-wsgi flask,django: pip install {toxinidir}/opentelemetry-auto-instrumentation asgi: pip install {toxinidir}/ext/opentelemetry-ext-asgi + + boto: pip install {toxinidir}/opentelemetry-auto-instrumentation + boto: pip install {toxinidir}/ext/opentelemetry-ext-boto[test] + flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi[test] From acb7f48b76c23e29ec0f9737acf7b80d964cb93b Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 2 Jun 2020 20:37:36 -0700 Subject: [PATCH 0381/1517] opentracing-shim: add testbed for otshim (#727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit ports the OpenTracing testbed[1] to check that the ot-shim is working as expected using different frameworks. Gevent doesn't support context vars yet[2], so those tests are not compatible with opentelemetry and were not ported. [1] https://github.com/opentracing/opentracing-python/tree/master/testbed [2] https://github.com/gevent/gevent/issues/1407 Co-authored-by: Mauricio Vásquez Co-authored-by: alrex --- dev-requirements.txt | 1 + .../tests/testbed/README.rst | 47 ++++++ .../tests/testbed/__init__.py | 0 .../tests/testbed/otel_ot_shim_tracer.py | 26 ++++ .../test_active_span_replacement/README.rst | 20 +++ .../test_active_span_replacement/__init__.py | 0 .../test_asyncio.py | 54 +++++++ .../test_threads.py | 50 +++++++ .../testbed/test_client_server/README.rst | 19 +++ .../testbed/test_client_server/__init__.py | 0 .../test_client_server/test_asyncio.py | 79 ++++++++++ .../test_client_server/test_threads.py | 75 ++++++++++ .../test_common_request_handler/README.rst | 23 +++ .../test_common_request_handler/__init__.py | 0 .../request_handler.py | 38 +++++ .../test_asyncio.py | 136 ++++++++++++++++++ .../test_threads.py | 119 +++++++++++++++ .../testbed/test_late_span_finish/README.rst | 18 +++ .../testbed/test_late_span_finish/__init__.py | 0 .../test_late_span_finish/test_asyncio.py | 51 +++++++ .../test_late_span_finish/test_threads.py | 44 ++++++ .../test_listener_per_request/README.rst | 19 +++ .../test_listener_per_request/__init__.py | 0 .../response_listener.py | 7 + .../test_listener_per_request/test_asyncio.py | 45 ++++++ .../test_listener_per_request/test_threads.py | 45 ++++++ .../test_multiple_callbacks/README.rst | 44 ++++++ .../test_multiple_callbacks/__init__.py | 0 .../test_multiple_callbacks/test_asyncio.py | 59 ++++++++ .../test_multiple_callbacks/test_threads.py | 59 ++++++++ .../testbed/test_nested_callbacks/README.rst | 47 ++++++ .../testbed/test_nested_callbacks/__init__.py | 0 .../test_nested_callbacks/test_asyncio.py | 57 ++++++++ .../test_nested_callbacks/test_threads.py | 59 ++++++++ .../test_subtask_span_propagation/README.rst | 42 ++++++ .../test_subtask_span_propagation/__init__.py | 0 .../test_asyncio.py | 32 +++++ .../test_threads.py | 33 +++++ .../tests/testbed/testcase.py | 46 ++++++ .../tests/testbed/utils.py | 78 ++++++++++ opentelemetry-api/tests/__init__.py | 9 ++ scripts/coverage.sh | 26 +++- tox.ini | 8 +- 43 files changed, 1506 insertions(+), 9 deletions(-) create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py diff --git a/dev-requirements.txt b/dev-requirements.txt index b8ae14c89c..be74d804b3 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,3 +10,4 @@ pytest!=5.2.3 pytest-cov>=2.8 readme-renderer~=24.0 httpretty~=1.0 +opentracing~=2.2.0 \ No newline at end of file diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst new file mode 100644 index 0000000000..ba7119cd68 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst @@ -0,0 +1,47 @@ + +Testbed suite for the OpenTelemetry-OpenTracing Bridge +====================================================== + +Testbed suite designed to test the API changes. + +Build and test. +--------------- + +.. code-block:: sh + + tox -e py37-test-opentracing-shim + +Alternatively, due to the organization of the suite, it's possible to run directly the tests using ``py.test``\ : + +.. code-block:: sh + + py.test -s testbed/test_multiple_callbacks/test_threads.py + +Tested frameworks +----------------- + +Currently the examples cover ``threading`` and ``asyncio``. + +List of patterns +---------------- + + +* `Active Span replacement `_ - Start an isolated task and query for its results in another task/thread. +* `Client-Server `_ - Typical client-server example. +* `Common Request Handler `_ - One request handler for all requests. +* `Late Span finish `_ - Late parent ``Span`` finish. +* `Multiple callbacks `_ - Multiple callbacks spawned at the same time. +* `Nested callbacks `_ - One callback at a time, defined in a pipeline fashion. +* `Subtask Span propagation `_ - ``Span`` propagation for subtasks/coroutines. + +Adding new patterns +------------------- + +A new pattern is composed of a directory under *testbed* with the *test_* prefix, and containing the files for each platform, also with the *test_* prefix: + +.. code-block:: + + testbed/ + test_new_pattern/ + test_threads.py + test_asyncio.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py new file mode 100644 index 0000000000..b3b4271f02 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py @@ -0,0 +1,26 @@ +import opentelemetry.ext.opentracing_shim as opentracingshim +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + + +class MockTracer(opentracingshim.TracerShim): + """Wrapper of `opentracingshim.TracerShim`. + + MockTracer extends `opentracingshim.TracerShim` by adding a in memory + span exporter that can be used to get the list of finished spans.""" + + def __init__(self): + tracer_provider = trace.TracerProvider() + oteltracer = tracer_provider.get_tracer(__name__) + super(MockTracer, self).__init__(oteltracer) + exporter = InMemorySpanExporter() + span_processor = SimpleExportSpanProcessor(exporter) + tracer_provider.add_span_processor(span_processor) + + self.exporter = exporter + + def finished_spans(self): + return self.exporter.get_finished_spans() diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst new file mode 100644 index 0000000000..6bb4d2f35c --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst @@ -0,0 +1,20 @@ + +Active Span replacement example. +================================ + +This example shows a ``Span`` being created and then passed to an asynchronous task, which will temporary activate it to finish its processing, and further restore the previously active ``Span``. + +``threading`` implementation: + +.. code-block:: python + + # Create a new Span for this task + with self.tracer.start_active_span("task"): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span("subtask"): + pass diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py new file mode 100644 index 0000000000..cb555dc109 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py @@ -0,0 +1,54 @@ +from __future__ import print_function + +import asyncio + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import stop_loop_when + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Start an isolated task and query for its result -and finish it- + # in another task/thread + span = self.tracer.start_span("initial") + self.submit_another_task(span) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 3, + timeout=5.0, + ) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ["initial", "subtask", "task"]) + + # task/subtask are part of the same trace, + # and subtask is a child of task + self.assertSameTrace(spans[1], spans[2]) + self.assertIsChildOf(spans[1], spans[2]) + + # initial task is not related in any way to those two tasks + self.assertNotSameTrace(spans[0], spans[1]) + self.assertEqual(spans[0].parent, None) + + async def task(self, span): + # Create a new Span for this task + with self.tracer.start_active_span("task"): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span("subtask"): + pass + + def submit_another_task(self, span): + self.loop.create_task(self.task(span)) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py new file mode 100644 index 0000000000..e382d5d716 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py @@ -0,0 +1,50 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + # use max_workers=3 as a general example even if only one would suffice + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + # Start an isolated task and query for its result -and finish it- + # in another task/thread + span = self.tracer.start_span("initial") + self.submit_another_task(span) + + self.executor.shutdown(True) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ["initial", "subtask", "task"]) + + # task/subtask are part of the same trace, + # and subtask is a child of task + self.assertSameTrace(spans[1], spans[2]) + self.assertIsChildOf(spans[1], spans[2]) + + # initial task is not related in any way to those two tasks + self.assertNotSameTrace(spans[0], spans[1]) + self.assertEqual(spans[0].parent, None) + self.assertEqual(spans[2].parent, None) + + def task(self, span): + # Create a new Span for this task + with self.tracer.start_active_span("task"): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span("subtask"): + pass + + def submit_another_task(self, span): + self.executor.submit(self.task, span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst new file mode 100644 index 0000000000..730fd9295d --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst @@ -0,0 +1,19 @@ + +Client-Server example. +====================== + +This example shows a ``Span`` created by a ``Client``, which will send a ``Message`` / ``SpanContext`` to a ``Server``, which will in turn extract such context and use it as parent of a new (server-side) ``Span``. + +``Client.send()`` is used to send messages and inject the ``SpanContext`` using the ``TEXT_MAP`` format, and ``Server.process()`` will process received messages and will extract the context used as parent. + +.. code-block:: python + + def send(self): + with self.tracer.start_active_span("send") as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject(scope.span.context, + opentracing.Format.TEXT_MAP, + message) + self.queue.put(message) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py new file mode 100644 index 0000000000..5379584719 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py @@ -0,0 +1,79 @@ +from __future__ import print_function + +import asyncio + +import opentracing +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, get_one_by_tag, stop_loop_when + +logger = get_logger(__name__) + + +class Server: + def __init__(self, *args, **kwargs): + tracer = kwargs.pop("tracer") + queue = kwargs.pop("queue") + super(Server, self).__init__(*args, **kwargs) + + self.tracer = tracer + self.queue = queue + + async def run(self): + value = await self.queue.get() + self.process(value) + + def process(self, message): + logger.info("Processing message in server") + + ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) + with self.tracer.start_active_span("receive", child_of=ctx) as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + + +class Client: + def __init__(self, tracer, queue): + self.tracer = tracer + self.queue = queue + + async def send(self): + with self.tracer.start_active_span("send") as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject( + scope.span.context, opentracing.Format.TEXT_MAP, message + ) + await self.queue.put(message) + + logger.info("Sent message from client") + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.queue = asyncio.Queue() + self.loop = asyncio.get_event_loop() + self.server = Server(tracer=self.tracer, queue=self.queue) + + def test(self): + client = Client(self.tracer, self.queue) + self.loop.create_task(self.server.run()) + self.loop.create_task(client.send()) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 2, + timeout=5.0, + ) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertIsNotNone( + get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + ) + self.assertIsNotNone( + get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + ) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py new file mode 100644 index 0000000000..619781edec --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py @@ -0,0 +1,75 @@ +from __future__ import print_function + +from queue import Queue +from threading import Thread + +import opentracing +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import await_until, get_logger, get_one_by_tag + +logger = get_logger(__name__) + + +class Server(Thread): + def __init__(self, *args, **kwargs): + tracer = kwargs.pop("tracer") + queue = kwargs.pop("queue") + super(Server, self).__init__(*args, **kwargs) + + self.daemon = True + self.tracer = tracer + self.queue = queue + + def run(self): + value = self.queue.get() + self.process(value) + + def process(self, message): + logger.info("Processing message in server") + + ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) + with self.tracer.start_active_span("receive", child_of=ctx) as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + + +class Client: + def __init__(self, tracer, queue): + self.tracer = tracer + self.queue = queue + + def send(self): + with self.tracer.start_active_span("send") as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject( + scope.span.context, opentracing.Format.TEXT_MAP, message + ) + self.queue.put(message) + + logger.info("Sent message from client") + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.queue = Queue() + self.server = Server(tracer=self.tracer, queue=self.queue) + self.server.start() + + def test(self): + client = Client(self.tracer, self.queue) + client.send() + + await_until(lambda: len(self.tracer.finished_spans()) >= 2) + + spans = self.tracer.finished_spans() + self.assertIsNotNone( + get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + ) + self.assertIsNotNone( + get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + ) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst new file mode 100644 index 0000000000..1bcda539bb --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst @@ -0,0 +1,23 @@ + +Common Request Handler example. +=============================== + +This example shows a ``Span`` used with ``RequestHandler``, which is used as a middleware (as in web frameworks) to manage a new ``Span`` per operation through its ``before_request()`` / ``after_response()`` methods. + +Implementation details: + + +* For ``threading``, no active ``Span`` is consumed as the tasks may be run concurrently on different threads, and an explicit ``SpanContext`` has to be saved to be used as parent. + +RequestHandler implementation: + +.. code-block:: python + + def before_request(self, request, request_context): + + # If we should ignore the active Span, use any passed SpanContext + # as the parent. Else, use the active one. + span = self.tracer.start_span("send", + child_of=self.context, + ignore_active_span=True) + diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py new file mode 100644 index 0000000000..47ff088025 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py @@ -0,0 +1,38 @@ +from __future__ import print_function + +from opentracing.ext import tags + +from ..utils import get_logger + +logger = get_logger(__name__) + + +class RequestHandler: + def __init__(self, tracer, context=None, ignore_active_span=True): + self.tracer = tracer + self.context = context + self.ignore_active_span = ignore_active_span + + def before_request(self, request, request_context): + logger.info("Before request %s", request) + + # If we should ignore the active Span, use any passed SpanContext + # as the parent. Else, use the active one. + if self.ignore_active_span: + span = self.tracer.start_span( + "send", child_of=self.context, ignore_active_span=True + ) + else: + span = self.tracer.start_span("send") + + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + request_context["span"] = span + + def after_request(self, request, request_context): + # pylint: disable=no-self-use + logger.info("After request %s", request) + + span = request_context.get("span") + if span is not None: + span.finish() diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py new file mode 100644 index 0000000000..b0216dd756 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py @@ -0,0 +1,136 @@ +from __future__ import print_function + +import asyncio + +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, get_one_by_operation_name, stop_loop_when +from .request_handler import RequestHandler + +logger = get_logger(__name__) + + +class Client: + def __init__(self, request_handler, loop): + self.request_handler = request_handler + self.loop = loop + + async def send_task(self, message): + request_context = {} + + async def before_handler(): + self.request_handler.before_request(message, request_context) + + async def after_handler(): + self.request_handler.after_request(message, request_context) + + await before_handler() + await after_handler() + + return "%s::response" % message + + def send(self, message): + return self.send_task(message) + + def send_sync(self, message): + return self.loop.run_until_complete(self.send_task(message)) + + +class TestAsyncio(OpenTelemetryTestCase): + """ + There is only one instance of 'RequestHandler' per 'Client'. Methods of + 'RequestHandler' are executed in different Tasks, and no Span propagation + among them is done automatically. + Therefore we cannot use current active span and activate span. + So one issue here is setting correct parent span. + """ + + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + self.client = Client(RequestHandler(self.tracer), self.loop) + + def test_two_callbacks(self): + res_future1 = self.loop.create_task(self.client.send("message1")) + res_future2 = self.loop.create_task(self.client.send("message2")) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 2, + timeout=5.0, + ) + self.loop.run_forever() + + self.assertEqual("message1::response", res_future1.result()) + self.assertEqual("message2::response", res_future2.result()) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + + for span in spans: + self.assertEqual( + span.attributes.get(tags.SPAN_KIND, None), + tags.SPAN_KIND_RPC_CLIENT, + ) + + self.assertNotSameTrace(spans[0], spans[1]) + self.assertIsNone(spans[0].parent) + self.assertIsNone(spans[1].parent) + + def test_parent_not_picked(self): + """Active parent should not be picked up by child.""" + + async def do_task(): + with self.tracer.start_active_span("parent"): + response = await self.client.send_task("no_parent") + self.assertEqual("no_parent::response", response) + + self.loop.run_until_complete(do_task()) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + + child_span = get_one_by_operation_name(spans, "send") + self.assertIsNotNone(child_span) + + parent_span = get_one_by_operation_name(spans, "parent") + self.assertIsNotNone(parent_span) + + # Here check that there is no parent-child relation. + self.assertIsNotChildOf(child_span, parent_span) + + def test_good_solution_to_set_parent(self): + """Asyncio and contextvars are integrated, in this case it is not needed + to activate current span by hand. + """ + + async def do_task(): + with self.tracer.start_active_span("parent"): + # Set ignore_active_span to False indicating that the + # framework will do it for us. + req_handler = RequestHandler( + self.tracer, ignore_active_span=False, + ) + client = Client(req_handler, self.loop) + response = await client.send_task("correct_parent") + + self.assertEqual("correct_parent::response", response) + + # Send second request, now there is no active parent, + # but it will be set, ups + response = await client.send_task("wrong_parent") + self.assertEqual("wrong_parent::response", response) + + self.loop.run_until_complete(do_task()) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + + spans = sorted(spans, key=lambda x: x.start_time) + parent_span = get_one_by_operation_name(spans, "parent") + self.assertIsNotNone(parent_span) + + self.assertIsChildOf(spans[1], parent_span) + self.assertIsNotChildOf(spans[2], parent_span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py new file mode 100644 index 0000000000..4ab8b2a075 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py @@ -0,0 +1,119 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, get_one_by_operation_name +from .request_handler import RequestHandler + +logger = get_logger(__name__) + + +class Client: + def __init__(self, request_handler, executor): + self.request_handler = request_handler + self.executor = executor + + def send_task(self, message): + request_context = {} + + def before_handler(): + self.request_handler.before_request(message, request_context) + + def after_handler(): + self.request_handler.after_request(message, request_context) + + self.executor.submit(before_handler).result() + self.executor.submit(after_handler).result() + + return "%s::response" % message + + def send(self, message): + return self.executor.submit(self.send_task, message) + + def send_sync(self, message, timeout=5.0): + fut = self.executor.submit(self.send_task, message) + return fut.result(timeout=timeout) + + +class TestThreads(OpenTelemetryTestCase): + """ + There is only one instance of 'RequestHandler' per 'Client'. Methods of + 'RequestHandler' are executed concurrently in different threads which are + reused (executor). Therefore we cannot use current active span and + activate span. So one issue here is setting correct parent span. + """ + + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + self.client = Client(RequestHandler(self.tracer), self.executor) + + def test_two_callbacks(self): + response_future1 = self.client.send("message1") + response_future2 = self.client.send("message2") + + self.assertEqual("message1::response", response_future1.result(5.0)) + self.assertEqual("message2::response", response_future2.result(5.0)) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + + for span in spans: + self.assertEqual( + span.attributes.get(tags.SPAN_KIND, None), + tags.SPAN_KIND_RPC_CLIENT, + ) + + self.assertNotSameTrace(spans[0], spans[1]) + self.assertIsNone(spans[0].parent) + self.assertIsNone(spans[1].parent) + + def test_parent_not_picked(self): + """Active parent should not be picked up by child.""" + + with self.tracer.start_active_span("parent"): + response = self.client.send_sync("no_parent") + self.assertEqual("no_parent::response", response) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + + child_span = get_one_by_operation_name(spans, "send") + self.assertIsNotNone(child_span) + + parent_span = get_one_by_operation_name(spans, "parent") + self.assertIsNotNone(parent_span) + + # Here check that there is no parent-child relation. + self.assertIsNotChildOf(child_span, parent_span) + + def test_bad_solution_to_set_parent(self): + """Solution is bad because parent is per client and is not automatically + activated depending on the context. + """ + + with self.tracer.start_active_span("parent") as scope: + client = Client( + # Pass a span context to be used ad the parent. + RequestHandler(self.tracer, scope.span.context), + self.executor, + ) + response = client.send_sync("correct_parent") + self.assertEqual("correct_parent::response", response) + + response = client.send_sync("wrong_parent") + self.assertEqual("wrong_parent::response", response) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + + spans = sorted(spans, key=lambda x: x.start_time) + parent_span = get_one_by_operation_name(spans, "parent") + self.assertIsNotNone(parent_span) + + self.assertIsChildOf(spans[1], parent_span) + self.assertIsChildOf(spans[2], parent_span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst new file mode 100644 index 0000000000..8c4ffd864a --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst @@ -0,0 +1,18 @@ + +Late Span finish example. +========================= + +This example shows a ``Span`` for a top-level operation, with independent, unknown lifetime, acting as parent of a few asynchronous subtasks (which must re-activate it but not finish it). + +.. code-block:: python + + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + def task(name, interval): + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + time.sleep(interval) + + self.executor.submit(task, "task1", 0.1) + self.executor.submit(task, "task2", 0.3) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py new file mode 100644 index 0000000000..128073b056 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py @@ -0,0 +1,51 @@ +from __future__ import print_function + +import asyncio + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, stop_loop_when + +logger = get_logger(__name__) + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Create a Span and use it as (explicit) parent of a pair of subtasks. + parent_span = self.tracer.start_span("parent") + self.submit_subtasks(parent_span) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 2, + timeout=5.0, + ) + self.loop.run_forever() + + # Late-finish the parent Span now. + parent_span.finish() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ["task1", "task2", "parent"]) + + for idx in range(2): + self.assertSameTrace(spans[idx], spans[-1]) + self.assertIsChildOf(spans[idx], spans[-1]) + self.assertTrue(spans[idx].end_time <= spans[-1].end_time) + + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + async def task(name): + logger.info("Running %s", name) + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + await asyncio.sleep(0.1) + + self.loop.create_task(task("task1")) + self.loop.create_task(task("task2")) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py new file mode 100644 index 0000000000..5972eb8b92 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py @@ -0,0 +1,44 @@ +from __future__ import print_function + +import time +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + # Create a Span and use it as (explicit) parent of a pair of subtasks. + parent_span = self.tracer.start_span("parent") + self.submit_subtasks(parent_span) + + # Wait for the threadpool to be done. + self.executor.shutdown(True) + + # Late-finish the parent Span now. + parent_span.finish() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ["task1", "task2", "parent"]) + + for idx in range(2): + self.assertSameTrace(spans[idx], spans[-1]) + self.assertIsChildOf(spans[idx], spans[-1]) + self.assertTrue(spans[idx].end_time <= spans[-1].end_time) + + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + def task(name, interval): + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + time.sleep(interval) + + self.executor.submit(task, "task1", 0.1) + self.executor.submit(task, "task2", 0.3) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst new file mode 100644 index 0000000000..952d1ec51d --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst @@ -0,0 +1,19 @@ + +Listener Response example. +========================== + +This example shows a ``Span`` created upon a message being sent to a ``Client``, and its handling along a related, **not shared** ``ResponseListener`` object with a ``on_response(self, response)`` method to finish it. + +.. code-block:: python + + def _task(self, message, listener): + res = "%s::response" % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span("send") + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + return self.executor.submit(self._task, message, listener).result() diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py new file mode 100644 index 0000000000..dd143c20b8 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py @@ -0,0 +1,7 @@ +class ResponseListener: + def __init__(self, span): + self.span = span + + def on_response(self, res): + del res + self.span.finish() diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py new file mode 100644 index 0000000000..085c0ea813 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py @@ -0,0 +1,45 @@ +from __future__ import print_function + +import asyncio + +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_one_by_tag +from .response_listener import ResponseListener + + +class Client: + def __init__(self, tracer, loop): + self.tracer = tracer + self.loop = loop + + async def task(self, message, listener): + res = "%s::response" % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span("send") + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + return self.loop.run_until_complete(self.task(message, listener)) + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + client = Client(self.tracer, self.loop) + res = client.send_sync("message") + self.assertEqual(res, "message::response") + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + + span = get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + self.assertIsNotNone(span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py new file mode 100644 index 0000000000..8f82e1fb15 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py @@ -0,0 +1,45 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_one_by_tag +from .response_listener import ResponseListener + + +class Client: + def __init__(self, tracer): + self.tracer = tracer + self.executor = ThreadPoolExecutor(max_workers=3) + + def _task(self, message, listener): + # pylint: disable=no-self-use + res = "%s::response" % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span("send") + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + return self.executor.submit(self._task, message, listener).result() + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + + def test_main(self): + client = Client(self.tracer) + res = client.send_sync("message") + self.assertEqual(res, "message::response") + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + + span = get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + self.assertIsNotNone(span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst new file mode 100644 index 0000000000..204f282cf2 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst @@ -0,0 +1,44 @@ + +Multiple callbacks example. +=========================== + +This example shows a ``Span`` created for a top-level operation, covering a set of asynchronous operations (representing callbacks), and have this ``Span`` finished when **all** of them have been executed. + +``Client.send()`` is used to create a new asynchronous operation (callback), and in turn every operation both restores the active ``Span``, and creates a child ``Span`` (useful for measuring the performance of each callback). + +Implementation details: + + +* For ``threading``, a thread-safe counter is put in each ``Span`` to keep track of the pending callbacks, and call ``Span.finish()`` when the count becomes 0. +* For ``asyncio`` the children corotuines representing the subtasks are simply yielded over, so no counter is needed. + +``threading`` implementation: + +.. code-block:: python + + def task(self, interval, parent_span): + logger.info("Starting task") + + try: + scope = self.tracer.scope_manager.activate(parent_span, False) + with self.tracer.start_active_span("task"): + time.sleep(interval) + finally: + scope.close() + if parent_span._ref_count.decr() == 0: + parent_span.finish() + +``asyncio`` implementation: + +.. code-block:: python + + async def task(self, interval, parent_span): + logger.info("Starting task") + + with self.tracer.start_active_span("task"): + await asyncio.sleep(interval) + + # Invoke and yield over the corotuines. + with self.tracer.start_active_span("parent"): + tasks = self.submit_callbacks() + await asyncio.gather(*tasks) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py new file mode 100644 index 0000000000..36043d0a9b --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py @@ -0,0 +1,59 @@ +from __future__ import print_function + +import asyncio +import random + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, stop_loop_when + +random.seed() +logger = get_logger(__name__) + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Need to run within a Task, as the scope manager depends + # on Task.current_task() + async def main_task(): + with self.tracer.start_active_span("parent"): + tasks = self.submit_callbacks() + await asyncio.gather(*tasks) + + self.loop.create_task(main_task()) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 4, + timeout=5.0, + ) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 4) + self.assertNamesEqual(spans, ["task", "task", "task", "parent"]) + + for idx in range(3): + self.assertSameTrace(spans[idx], spans[-1]) + self.assertIsChildOf(spans[idx], spans[-1]) + + async def task(self, interval, parent_span): + logger.info("Starting task") + + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span("task"): + await asyncio.sleep(interval) + + def submit_callbacks(self): + parent_span = self.tracer.scope_manager.active.span + tasks = [] + for _ in range(3): + interval = 0.1 + random.randint(200, 500) * 0.001 + task = self.loop.create_task(self.task(interval, parent_span)) + tasks.append(task) + + return tasks diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py new file mode 100644 index 0000000000..b24ae643e3 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py @@ -0,0 +1,59 @@ +from __future__ import print_function + +import random +import time +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import RefCount, get_logger + +random.seed() +logger = get_logger(__name__) + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + try: + scope = self.tracer.start_active_span( + "parent", finish_on_close=False + ) + scope.span.ref_count = RefCount(1) + self.submit_callbacks(scope.span) + finally: + scope.close() + if scope.span.ref_count.decr() == 0: + scope.span.finish() + + self.executor.shutdown(True) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 4) + self.assertNamesEqual(spans, ["task", "task", "task", "parent"]) + + for idx in range(3): + self.assertSameTrace(spans[idx], spans[-1]) + self.assertIsChildOf(spans[idx], spans[-1]) + + def task(self, interval, parent_span): + logger.info("Starting task") + + try: + scope = self.tracer.scope_manager.activate(parent_span, False) + with self.tracer.start_active_span("task"): + time.sleep(interval) + finally: + scope.close() + if parent_span.ref_count.decr() == 0: + parent_span.finish() + + def submit_callbacks(self, parent_span): + for _ in range(3): + parent_span.ref_count.incr() + self.executor.submit( + self.task, 0.1 + random.randint(200, 500) * 0.001, parent_span + ) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst new file mode 100644 index 0000000000..c191431ccc --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst @@ -0,0 +1,47 @@ + +Nested callbacks example. +========================= + +This example shows a ``Span`` for a top-level operation, and how it can be passed down on a list of nested callbacks (always one at a time), have it as the active one for each of them, and finished **only** when the last one executes. For Python, we have decided to do it in a **fire-and-forget** fashion. + +Implementation details: + + +* For ``threading``, the ``Span`` is manually activatted it in each corotuine/task. +* For ``asyncio``, the active ``Span`` is not activated down the chain as the ``Context`` automatically propagates it. + +``threading`` implementation: + +.. code-block:: python + + def submit(self): + span = self.tracer.scope_manager.active.span + + def task1(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag("key1", "1") + + def task2(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag("key2", "2") + ... + +``asyncio`` implementation: + +.. code-block:: python + + async def task1(): + span.set_tag("key1", "1") + + async def task2(): + span.set_tag("key2", "2") + + async def task3(): + span.set_tag("key3", "3") + span.finish() + + self.loop.create_task(task3()) + + self.loop.create_task(task2()) + + self.loop.create_task(task1()) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py new file mode 100644 index 0000000000..12eb436277 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py @@ -0,0 +1,57 @@ +from __future__ import print_function + +import asyncio + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import stop_loop_when + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Start a Span and let the callback-chain + # finish it when the task is done + async def task(): + with self.tracer.start_active_span("one", finish_on_close=False): + self.submit() + + self.loop.create_task(task()) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) == 1, + timeout=5.0, + ) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].name, "one") + + for idx in range(1, 4): + self.assertEqual( + spans[0].attributes.get("key%s" % idx, None), str(idx) + ) + + def submit(self): + span = self.tracer.scope_manager.active.span + + async def task1(): + span.set_tag("key1", "1") + + async def task2(): + span.set_tag("key2", "2") + + async def task3(): + span.set_tag("key3", "3") + span.finish() + + self.loop.create_task(task3()) + + self.loop.create_task(task2()) + + self.loop.create_task(task1()) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py new file mode 100644 index 0000000000..a1d35c35d8 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py @@ -0,0 +1,59 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import await_until + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def tearDown(self): + self.executor.shutdown(False) + + def test_main(self): + # Start a Span and let the callback-chain + # finish it when the task is done + with self.tracer.start_active_span("one", finish_on_close=False): + self.submit() + + # Cannot shutdown the executor and wait for the callbacks + # to be run, as in such case only the first will be executed, + # and the rest will get canceled. + await_until(lambda: len(self.tracer.finished_spans()) == 1, 5) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].name, "one") + + for idx in range(1, 4): + self.assertEqual( + spans[0].attributes.get("key%s" % idx, None), str(idx) + ) + + def submit(self): + span = self.tracer.scope_manager.active.span + + def task1(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag("key1", "1") + + def task2(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag("key2", "2") + + def task3(): + with self.tracer.scope_manager.activate( + span, True + ): + span.set_tag("key3", "3") + + self.executor.submit(task3) + + self.executor.submit(task2) + + self.executor.submit(task1) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst new file mode 100644 index 0000000000..eaeda8e6f8 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst @@ -0,0 +1,42 @@ + +Subtask Span propagation example. +================================= + +This example shows an active ``Span`` being simply propagated to the subtasks -either threads or coroutines-, and finished **by** the parent task. In real-life scenarios instrumentation libraries may help with ``Span`` propagation **if** not offered by default (see implementation details below), but we show here the case without such help. + +Implementation details: + +* For ``threading``, the ``Span`` is manually passed down the call chain, activating it in each corotuine/task. +* For ``asyncio``, the active ``Span`` is not passed nor activated down the chain as the ``Context`` automatically propagates it. + +``threading`` implementation: + +.. code-block:: python + + def parent_task(self, message): + with self.tracer.start_active_span("parent") as scope: + f = self.executor.submit(self.child_task, message, scope.span) + res = f.result() + + return res + + def child_task(self, message, span): + with self.tracer.scope_manager.activate(span, False): + with self.tracer.start_active_span("child"): + return "%s::response" % message + +``asyncio`` implementation: + +.. code-block:: python + + async def parent_task(self, message): # noqa + with self.tracer.start_active_span("parent"): + res = await self.child_task(message) + + return res + + async def child_task(self, message): + # No need to pass/activate the parent Span, as it stays in the context. + with self.tracer.start_active_span("child"): + return "%s::response" % message + diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py new file mode 100644 index 0000000000..6e54456070 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py @@ -0,0 +1,32 @@ +from __future__ import absolute_import, print_function + +import asyncio + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + res = self.loop.run_until_complete(self.parent_task("message")) + self.assertEqual(res, "message::response") + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + self.assertNamesEqual(spans, ["child", "parent"]) + self.assertIsChildOf(spans[0], spans[1]) + + async def parent_task(self, message): # noqa + with self.tracer.start_active_span("parent"): + res = await self.child_task(message) + + return res + + async def child_task(self, message): + # No need to pass/activate the parent Span, as it stays in the context. + with self.tracer.start_active_span("child"): + return "%s::response" % message diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py new file mode 100644 index 0000000000..1ba5f697ca --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import, print_function + +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + res = self.executor.submit(self.parent_task, "message").result() + self.assertEqual(res, "message::response") + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + self.assertNamesEqual(spans, ["child", "parent"]) + self.assertIsChildOf(spans[0], spans[1]) + + def parent_task(self, message): + with self.tracer.start_active_span("parent") as scope: + fut = self.executor.submit(self.child_task, message, scope.span) + res = fut.result() + + return res + + def child_task(self, message, span): + with self.tracer.scope_manager.activate(span, False): + with self.tracer.start_active_span("child"): + return "%s::response" % message diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py new file mode 100644 index 0000000000..c1ce6ea5ab --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py @@ -0,0 +1,46 @@ +import unittest + +import opentelemetry.trace as trace_api + + +# pylint: disable=C0103 +class OpenTelemetryTestCase(unittest.TestCase): + def assertSameTrace(self, spanA, spanB): + return self.assertEqual(spanA.context.trace_id, spanB.context.trace_id) + + def assertNotSameTrace(self, spanA, spanB): + return self.assertNotEqual( + spanA.context.trace_id, spanB.context.trace_id + ) + + def assertIsChildOf(self, spanA, spanB): + # spanA is child of spanB + self.assertIsNotNone(spanA.parent) + + ctxA = spanA.parent + if isinstance(spanA.parent, trace_api.Span): + ctxA = spanA.parent.context + + ctxB = spanB + if isinstance(ctxB, trace_api.Span): + ctxB = spanB.context + + return self.assertEqual(ctxA.span_id, ctxB.span_id) + + def assertIsNotChildOf(self, spanA, spanB): + # spanA is NOT child of spanB + if spanA.parent is None: + return + + ctxA = spanA.parent + if isinstance(spanA.parent, trace_api.Span): + ctxA = spanA.parent.context + + ctxB = spanB + if isinstance(ctxB, trace_api.Span): + ctxB = spanB.context + + self.assertNotEqual(ctxA.span_id, ctxB.span_id) + + def assertNamesEqual(self, spans, names): + self.assertEqual(list(map(lambda x: x.name, spans)), names) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py new file mode 100644 index 0000000000..a7b977f3d7 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py @@ -0,0 +1,78 @@ +from __future__ import print_function + +import logging +import threading +import time + + +class RefCount: + """Thread-safe counter""" + + def __init__(self, count=1): + self._lock = threading.Lock() + self._count = count + + def incr(self): + with self._lock: + self._count += 1 + return self._count + + def decr(self): + with self._lock: + self._count -= 1 + return self._count + + +def await_until(func, timeout=5.0): + """Polls for func() to return True""" + end_time = time.time() + timeout + while time.time() < end_time and not func(): + time.sleep(0.01) + + +def stop_loop_when(loop, cond_func, timeout=5.0): + """ + Registers a periodic callback that stops the loop when cond_func() == True. + Compatible with both Tornado and asyncio. + """ + if cond_func() or timeout <= 0.0: + loop.stop() + return + + timeout -= 0.1 + loop.call_later(0.1, stop_loop_when, loop, cond_func, timeout) + + +def get_logger(name): + """Returns a logger with log level set to INFO""" + logging.basicConfig(level=logging.INFO) + return logging.getLogger(name) + + +def get_one_by_tag(spans, key, value): + """Return a single Span with a tag value/key from a list, + errors if more than one is found.""" + + found = [] + for span in spans: + if span.attributes.get(key) == value: + found.append(span) + + if len(found) > 1: + raise RuntimeError("Too many values") + + return found[0] if len(found) > 0 else None + + +def get_one_by_operation_name(spans, name): + """Return a single Span with a name from a list, + errors if more than one is found.""" + found = [] + for span in spans: + if span.name == name: + found.append(span) + + if len(found) > 1: + raise RuntimeError("Too many values") + + return found[0] if len(found) > 0 else None diff --git a/opentelemetry-api/tests/__init__.py b/opentelemetry-api/tests/__init__.py index b0a6f42841..bc48946761 100644 --- a/opentelemetry-api/tests/__init__.py +++ b/opentelemetry-api/tests/__init__.py @@ -11,3 +11,12 @@ # 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 pkg_resources + +# naming the tests module as a namespace package ensures that +# relative imports will resolve properly for other test packages, +# as it enables searching for a composite of multiple test modules. +# +# only the opentelemetry-api directory needs this code, as it is +# the first tests module found by pylint during eachdist.py lint +pkg_resources.declare_namespace(__name__) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 8e09ae23a3..0b45fbf643 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -3,13 +3,25 @@ set -e function cov { - pytest \ - --ignore-glob=*/setup.py \ - --cov ${1} \ - --cov-append \ - --cov-branch \ - --cov-report='' \ - ${1} + if [ ${TOX_ENV_NAME:0:4} == "py34" ] + then + pytest \ + --ignore-glob=*/setup.py \ + --ignore-glob=ext/opentelemetry-ext-opentracing-shim/tests/testbed/* \ + --cov ${1} \ + --cov-append \ + --cov-branch \ + --cov-report='' \ + ${1} + else + pytest \ + --ignore-glob=*/setup.py \ + --cov ${1} \ + --cov-append \ + --cov-branch \ + --cov-report='' \ + ${1} + fi } PYTHON_VERSION=$(python -c 'import sys; print(".".join(map(str, sys.version_info[:3])))') diff --git a/tox.ini b/tox.ini index 978155616a..381e8604e1 100644 --- a/tox.ini +++ b/tox.ini @@ -227,10 +227,11 @@ commands_pre = jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger - datadog: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-datadog - + opentracing-shim: pip install {toxinidir}/opentelemetry-sdk opentracing-shim: pip install {toxinidir}/ext/opentelemetry-ext-opentracing-shim + datadog: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-datadog + zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin sqlalchemy: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-sqlalchemy @@ -258,6 +259,9 @@ commands = ; implicit Any due to unfollowed import would result). mypyinstalled: mypy --namespace-packages opentelemetry-api/tests/mypysmoke.py --strict +[testenv:py34-test-opentracing-shim] +commands = + pytest --ignore-glob='*[asyncio].py' [testenv:lint] basepython: python3.8 From 2ad9f497c56117c26fd49ec63d996ed85df36e0f Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 2 Jun 2020 21:44:55 -0700 Subject: [PATCH 0382/1517] chore: removing Oberon00 from approvers (#770) Co-authored-by: Yusuke Tsutsumi --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b297a77cd1..613b4e6acb 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,6 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Carlos Alberto Cortez](https://github.com/carlosalberto), LightStep - [Chris Kleinknecht](https://github.com/c24t), Google -- [Christian Neumüller](https://github.com/Oberon00), Dynatrace - [Diego Hurtado](https://github.com/ocelotl) - [Hector Hernandez](https://github.com/hectorhdzg), Microsoft - [Leighton Chen](https://github.com/lzchen), Microsoft @@ -119,6 +118,12 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t - [Alex Boten](https://github.com/codeboten), LightStep - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Zillow Group +### Thanks to all the people who already contributed! + + + + + *Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).* ## Release Schedule From c42749aeb6eea1184929390a37674ea37857fd31 Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Thu, 4 Jun 2020 00:33:36 -0400 Subject: [PATCH 0383/1517] cloud-trace: Cloud Trace exporter (#698) Co-authored-by: Cheng-Lung Sung --- docs-requirements.txt | 1 + docs/examples/cloud_trace_exporter/README.rst | 34 ++ .../cloud_trace_exporter/basic_trace.py | 14 + docs/ext/cloud_trace/cloud_trace.rst | 7 + .../README.rst | 43 ++ .../setup.cfg | 47 ++ .../setup.py | 26 ++ .../exporter/cloud_trace/__init__.py | 332 +++++++++++++++ .../exporter/cloud_trace/version.py | 15 + .../tests/__init__.py | 0 .../tests/test_cloud_trace_exporter.py | 401 ++++++++++++++++++ scripts/coverage.sh | 1 + tox.ini | 1 + 13 files changed, 922 insertions(+) create mode 100644 docs/examples/cloud_trace_exporter/README.rst create mode 100644 docs/examples/cloud_trace_exporter/basic_trace.py create mode 100644 docs/ext/cloud_trace/cloud_trace.rst create mode 100644 ext/opentelemetry-exporter-cloud-trace/README.rst create mode 100644 ext/opentelemetry-exporter-cloud-trace/setup.cfg create mode 100644 ext/opentelemetry-exporter-cloud-trace/setup.py create mode 100644 ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py create mode 100644 ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py create mode 100644 ext/opentelemetry-exporter-cloud-trace/tests/__init__.py create mode 100644 ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py diff --git a/docs-requirements.txt b/docs-requirements.txt index a61f0beedb..db10f6f9ee 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -21,3 +21,4 @@ thrift>=0.10.0 wrapt>=1.0.0,<2.0.0 psutil~=5.7.0 boto~=2.0 +google-cloud-trace >=0.23.0 diff --git a/docs/examples/cloud_trace_exporter/README.rst b/docs/examples/cloud_trace_exporter/README.rst new file mode 100644 index 0000000000..871422356a --- /dev/null +++ b/docs/examples/cloud_trace_exporter/README.rst @@ -0,0 +1,34 @@ +Cloud Trace Exporter Example +============================ + +These examples show how to use OpenTelemetry to send tracing data to Cloud Trace. + + +Basic Example +------------- + +To use this exporter you first need to: + * A Google Cloud project. You can `create one here. `_ + * Enable Cloud Trace API (aka StackDriver Trace API) in the project `here. `_ + * Enable `Default Application Credentials. `_ + +* Installation + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + pip install opentelemetry-exporter-cloud-trace + +* Run example + +.. code-block:: sh + + python basic_trace.py + +Checking Output +-------------------------- + +After running any of these examples, you can go to `Cloud Trace overview `_ to see the results. + +* `More information about exporters in general `_ \ No newline at end of file diff --git a/docs/examples/cloud_trace_exporter/basic_trace.py b/docs/examples/cloud_trace_exporter/basic_trace.py new file mode 100644 index 0000000000..76840a291e --- /dev/null +++ b/docs/examples/cloud_trace_exporter/basic_trace.py @@ -0,0 +1,14 @@ +from opentelemetry import trace +from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor + +trace.set_tracer_provider(TracerProvider()) + +cloud_trace_exporter = CloudTraceSpanExporter() +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(cloud_trace_exporter) +) +tracer = trace.get_tracer(__name__) +with tracer.start_as_current_span("foo"): + print("Hello world!") diff --git a/docs/ext/cloud_trace/cloud_trace.rst b/docs/ext/cloud_trace/cloud_trace.rst new file mode 100644 index 0000000000..5914b00d1a --- /dev/null +++ b/docs/ext/cloud_trace/cloud_trace.rst @@ -0,0 +1,7 @@ +OpenTelemetry Cloud Trace Exporter +================================== + +.. automodule:: opentelemetry.exporter.cloud_trace + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-exporter-cloud-trace/README.rst b/ext/opentelemetry-exporter-cloud-trace/README.rst new file mode 100644 index 0000000000..001f163007 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/README.rst @@ -0,0 +1,43 @@ +OpenTelemetry Cloud Trace Exporters +=================================== + +This library provides classes for exporting trace data to Google Cloud Trace. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-cloud-trace + +Usage +----- + +.. code:: python + + from opentelemetry import trace + from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import ( + SimpleExportSpanProcessor, + ) + + trace.set_tracer_provider(TracerProvider()) + + cloud_trace_exporter = CloudTraceSpanExporter( + project_id='my-gcloud-project', + ) + trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(cloud_trace_exporter) + ) + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span('foo'): + print('Hello world!') + + + +References +---------- + +* `Cloud Trace `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-exporter-cloud-trace/setup.cfg b/ext/opentelemetry-exporter-cloud-trace/setup.cfg new file mode 100644 index 0000000000..df6c2ce587 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/setup.cfg @@ -0,0 +1,47 @@ +# Copyright OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-exporter-cloud-trace +description = Cloud Trace integration for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-exporter-cloud-trace +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api + opentelemetry-sdk + google-cloud-trace + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-exporter-cloud-trace/setup.py b/ext/opentelemetry-exporter-cloud-trace/setup.py new file mode 100644 index 0000000000..332cf41d01 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/setup.py @@ -0,0 +1,26 @@ +# Copyright OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "exporter", "cloud_trace", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py new file mode 100644 index 0000000000..7e7aa017cf --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py @@ -0,0 +1,332 @@ +# Copyright OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Cloud Trace Span Exporter for OpenTelemetry. Uses Cloud Trace Client's REST +API to export traces and spans for viewing in Cloud Trace. + +Usage +----- + +.. code-block:: python + + from opentelemetry import trace + from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor + + trace.set_tracer_provider(TracerProvider()) + + cloud_trace_exporter = CloudTraceSpanExporter() + trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(cloud_trace_exporter) + ) + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span("foo"): + print("Hello world!") + + +API +--- +""" + +import logging +from typing import Any, Dict, List, Optional, Sequence, Tuple + +import google.auth +from google.cloud.trace_v2 import TraceServiceClient +from google.cloud.trace_v2.proto.trace_pb2 import AttributeValue +from google.cloud.trace_v2.proto.trace_pb2 import Span as ProtoSpan +from google.cloud.trace_v2.proto.trace_pb2 import TruncatableString +from google.rpc.status_pb2 import Status + +import opentelemetry.trace as trace_api +from opentelemetry.sdk.trace import Event +from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult +from opentelemetry.sdk.util import BoundedDict +from opentelemetry.util import types + +logger = logging.getLogger(__name__) + +MAX_NUM_LINKS = 128 +MAX_NUM_EVENTS = 32 +MAX_EVENT_ATTRS = 4 +MAX_LINK_ATTRS = 32 +MAX_SPAN_ATTRS = 32 + + +class CloudTraceSpanExporter(SpanExporter): + """Cloud Trace span exporter for OpenTelemetry. + + Args: + project_id: ID of the cloud project that will receive the traces. + client: Cloud Trace client. If not given, will be taken from gcloud + default credentials + """ + + def __init__( + self, project_id=None, client=None, + ): + self.client = client or TraceServiceClient() + if not project_id: + _, self.project_id = google.auth.default() + else: + self.project_id = project_id + + def export(self, spans: Sequence[Span]) -> SpanExportResult: + """Export the spans to Cloud Trace. + + See: https://cloud.google.com/trace/docs/reference/v2/rest/v2/projects.traces/batchWrite + + Args: + spans: Tuple of spans to export + """ + cloud_trace_spans = [] + for span in self._translate_to_cloud_trace(spans): + try: + cloud_trace_spans.append(self.client.create_span(**span)) + # pylint: disable=broad-except + except Exception as ex: + logger.error("Error when creating span %s", span, exc_info=ex) + + try: + self.client.batch_write_spans( + "projects/{}".format(self.project_id), cloud_trace_spans, + ) + # pylint: disable=broad-except + except Exception as ex: + logger.error("Error while writing to Cloud Trace", exc_info=ex) + return SpanExportResult.FAILURE + + return SpanExportResult.SUCCESS + + def _translate_to_cloud_trace( + self, spans: Sequence[Span] + ) -> List[Dict[str, Any]]: + """Translate the spans to Cloud Trace format. + + Args: + spans: Tuple of spans to convert + """ + + cloud_trace_spans = [] + + for span in spans: + ctx = span.get_context() + trace_id = _get_hexadecimal_trace_id(ctx.trace_id) + span_id = _get_hexadecimal_span_id(ctx.span_id) + span_name = "projects/{}/traces/{}/spans/{}".format( + self.project_id, trace_id, span_id + ) + + parent_id = None + if span.parent: + parent_id = _get_hexadecimal_span_id(span.parent.span_id) + + start_time = _get_time_from_ns(span.start_time) + end_time = _get_time_from_ns(span.end_time) + + if len(span.attributes) > MAX_SPAN_ATTRS: + logger.warning( + "Span has more then %s attributes, some will be truncated", + MAX_SPAN_ATTRS, + ) + + cloud_trace_spans.append( + { + "name": span_name, + "span_id": span_id, + "display_name": _get_truncatable_str_object( + span.name, 128 + ), + "start_time": start_time, + "end_time": end_time, + "parent_span_id": parent_id, + "attributes": _extract_attributes( + span.attributes, MAX_SPAN_ATTRS + ), + "links": _extract_links(span.links), + "status": _extract_status(span.status), + "time_events": _extract_events(span.events), + } + ) + # TODO: Leverage more of the Cloud Trace API, e.g. + # same_process_as_parent_span and child_span_count + + return cloud_trace_spans + + def shutdown(self): + pass + + +def _get_hexadecimal_trace_id(trace_id: int) -> str: + return "{:032x}".format(trace_id) + + +def _get_hexadecimal_span_id(span_id: int) -> str: + return "{:016x}".format(span_id) + + +def _get_time_from_ns(nanoseconds: int) -> Dict: + """Given epoch nanoseconds, split into epoch milliseconds and remaining + nanoseconds""" + if not nanoseconds: + return None + seconds, nanos = divmod(nanoseconds, 1e9) + return {"seconds": int(seconds), "nanos": int(nanos)} + + +def _get_truncatable_str_object(str_to_convert: str, max_length: int): + """Truncate the string if it exceeds the length limit and record the + truncated bytes count.""" + truncated, truncated_byte_count = _truncate_str(str_to_convert, max_length) + + return TruncatableString( + value=truncated, truncated_byte_count=truncated_byte_count + ) + + +def _truncate_str(str_to_check: str, limit: int) -> Tuple[str, int]: + """Check the length of a string. If exceeds limit, then truncate it.""" + encoded = str_to_check.encode("utf-8") + truncated_str = encoded[:limit].decode("utf-8", errors="ignore") + return truncated_str, len(encoded) - len(truncated_str.encode("utf-8")) + + +def _extract_status(status: trace_api.Status) -> Optional[Status]: + """Convert a Status object to protobuf object.""" + if not status: + return None + status_dict = {"details": None, "code": status.canonical_code.value} + + if status.description is not None: + status_dict["message"] = status.description + + return Status(**status_dict) + + +def _extract_links(links: Sequence[trace_api.Link]) -> ProtoSpan.Links: + """Convert span.links""" + if not links: + return None + extracted_links = [] + dropped_links = 0 + if len(links) > MAX_NUM_LINKS: + logger.warning( + "Exporting more then %s links, some will be truncated", + MAX_NUM_LINKS, + ) + dropped_links = len(links) - MAX_NUM_LINKS + links = links[:MAX_NUM_LINKS] + for link in links: + if len(link.attributes) > MAX_LINK_ATTRS: + logger.warning( + "Link has more then %s attributes, some will be truncated", + MAX_LINK_ATTRS, + ) + trace_id = _get_hexadecimal_trace_id(link.context.trace_id) + span_id = _get_hexadecimal_span_id(link.context.span_id) + extracted_links.append( + { + "trace_id": trace_id, + "span_id": span_id, + "type": "TYPE_UNSPECIFIED", + "attributes": _extract_attributes( + link.attributes, MAX_LINK_ATTRS + ), + } + ) + return ProtoSpan.Links( + link=extracted_links, dropped_links_count=dropped_links + ) + + +def _extract_events(events: Sequence[Event]) -> ProtoSpan.TimeEvents: + """Convert span.events to dict.""" + if not events: + return None + logs = [] + dropped_annontations = 0 + if len(events) > MAX_NUM_EVENTS: + logger.warning( + "Exporting more then %s annotations, some will be truncated", + MAX_NUM_EVENTS, + ) + dropped_annontations = len(events) - MAX_NUM_EVENTS + events = events[:MAX_NUM_EVENTS] + for event in events: + if len(event.attributes) > MAX_EVENT_ATTRS: + logger.warning( + "Event %s has more then %s attributes, some will be truncated", + event.name, + MAX_EVENT_ATTRS, + ) + logs.append( + { + "time": _get_time_from_ns(event.timestamp), + "annotation": { + "description": _get_truncatable_str_object( + event.name, 256 + ), + "attributes": _extract_attributes( + event.attributes, MAX_EVENT_ATTRS + ), + }, + } + ) + return ProtoSpan.TimeEvents( + time_event=logs, + dropped_annotations_count=dropped_annontations, + dropped_message_events_count=0, + ) + + +def _extract_attributes( + attrs: types.Attributes, num_attrs_limit: int +) -> ProtoSpan.Attributes: + """Convert span.attributes to dict.""" + attributes_dict = BoundedDict(num_attrs_limit) + + for key, value in attrs.items(): + key = _truncate_str(key, 128)[0] + value = _format_attribute_value(value) + + if value is not None: + attributes_dict[key] = value + return ProtoSpan.Attributes( + attribute_map=attributes_dict, + dropped_attributes_count=len(attrs) - len(attributes_dict), + ) + + +def _format_attribute_value(value: types.AttributeValue) -> AttributeValue: + if isinstance(value, bool): + value_type = "bool_value" + elif isinstance(value, int): + value_type = "int_value" + elif isinstance(value, str): + value_type = "string_value" + value = _get_truncatable_str_object(value, 256) + elif isinstance(value, float): + value_type = "string_value" + value = _get_truncatable_str_object("{:0.4f}".format(value), 256) + else: + logger.warning( + "ignoring attribute value %s of type %s. Values type must be one " + "of bool, int, string or float", + value, + type(value), + ) + return None + + return AttributeValue(**{value_type: value}) diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py new file mode 100644 index 0000000000..f83f20e7ba --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py @@ -0,0 +1,15 @@ +# Copyright OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/__init__.py b/ext/opentelemetry-exporter-cloud-trace/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py new file mode 100644 index 0000000000..5ebd5f3b64 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py @@ -0,0 +1,401 @@ +# Copyright OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from google.cloud.trace_v2.proto.trace_pb2 import AttributeValue +from google.cloud.trace_v2.proto.trace_pb2 import Span as ProtoSpan +from google.cloud.trace_v2.proto.trace_pb2 import TruncatableString +from google.rpc.status_pb2 import Status + +from opentelemetry.exporter.cloud_trace import ( + MAX_EVENT_ATTRS, + MAX_LINK_ATTRS, + MAX_NUM_EVENTS, + MAX_NUM_LINKS, + CloudTraceSpanExporter, + _extract_attributes, + _extract_events, + _extract_links, + _extract_status, + _format_attribute_value, + _truncate_str, +) +from opentelemetry.sdk.trace import Event, Span +from opentelemetry.trace import Link, SpanContext, SpanKind +from opentelemetry.trace.status import Status as SpanStatus +from opentelemetry.trace.status import StatusCanonicalCode + + +class TestCloudTraceSpanExporter(unittest.TestCase): + def setUp(self): + self.client_patcher = mock.patch( + "opentelemetry.exporter.cloud_trace.TraceServiceClient" + ) + self.client_patcher.start() + self.project_id = "PROJECT" + self.attributes_variety_pack = { + "str_key": "str_value", + "bool_key": False, + "double_key": 1.421, + "int_key": 123, + } + self.extracted_attributes_variety_pack = ProtoSpan.Attributes( + attribute_map={ + "str_key": AttributeValue( + string_value=TruncatableString( + value="str_value", truncated_byte_count=0 + ) + ), + "bool_key": AttributeValue(bool_value=False), + "double_key": AttributeValue( + string_value=TruncatableString( + value="1.4210", truncated_byte_count=0 + ) + ), + "int_key": AttributeValue(int_value=123), + } + ) + + def tearDown(self): + self.client_patcher.stop() + + def test_constructor_default(self): + exporter = CloudTraceSpanExporter(self.project_id) + self.assertEqual(exporter.project_id, self.project_id) + + def test_constructor_explicit(self): + client = mock.Mock() + exporter = CloudTraceSpanExporter(self.project_id, client=client) + + self.assertIs(exporter.client, client) + self.assertEqual(exporter.project_id, self.project_id) + + def test_export(self): + trace_id = "6e0c63257de34c92bf9efcd03927272e" + span_id = "95bb5edabd45950f" + span_datas = [ + Span( + name="span_name", + context=SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id, 16), + is_remote=False, + ), + parent=None, + kind=SpanKind.INTERNAL, + ) + ] + + cloud_trace_spans = { + "name": "projects/{}/traces/{}/spans/{}".format( + self.project_id, trace_id, span_id + ), + "span_id": span_id, + "parent_span_id": None, + "display_name": TruncatableString( + value="span_name", truncated_byte_count=0 + ), + "attributes": ProtoSpan.Attributes(attribute_map={}), + "links": None, + "status": None, + "time_events": None, + "start_time": None, + "end_time": None, + } + + client = mock.Mock() + + exporter = CloudTraceSpanExporter(self.project_id, client=client) + + exporter.export(span_datas) + + client.create_span.assert_called_with(**cloud_trace_spans) + self.assertTrue(client.create_span.called) + + def test_extract_status(self): + self.assertIsNone(_extract_status(None)) + self.assertEqual( + _extract_status(SpanStatus(canonical_code=StatusCanonicalCode.OK)), + Status(details=None, code=0), + ) + self.assertEqual( + _extract_status( + SpanStatus( + canonical_code=StatusCanonicalCode.UNKNOWN, + description="error_desc", + ) + ), + Status(details=None, code=2, message="error_desc"), + ) + + def test_extract_attributes(self): + self.assertEqual( + _extract_attributes({}, 4), ProtoSpan.Attributes(attribute_map={}) + ) + self.assertEqual( + _extract_attributes(self.attributes_variety_pack, 4), + self.extracted_attributes_variety_pack, + ) + # Test ignoring attributes with illegal value type + self.assertEqual( + _extract_attributes({"illegal_attribute_value": dict()}, 4), + ProtoSpan.Attributes(attribute_map={}, dropped_attributes_count=1), + ) + + too_many_attrs = {} + for attr_key in range(5): + too_many_attrs[str(attr_key)] = 0 + proto_attrs = _extract_attributes(too_many_attrs, 4) + self.assertEqual(proto_attrs.dropped_attributes_count, 1) + + def test_extract_events(self): + self.assertIsNone(_extract_events([])) + time_in_ns1 = 1589919268850900051 + time_in_ms_and_ns1 = {"seconds": 1589919268, "nanos": 850899968} + time_in_ns2 = 1589919438550020326 + time_in_ms_and_ns2 = {"seconds": 1589919438, "nanos": 550020352} + event1 = Event( + name="event1", + attributes=self.attributes_variety_pack, + timestamp=time_in_ns1, + ) + event2 = Event( + name="event2", + attributes={"illegal_attr_value": dict()}, + timestamp=time_in_ns2, + ) + self.assertEqual( + _extract_events([event1, event2]), + ProtoSpan.TimeEvents( + time_event=[ + { + "time": time_in_ms_and_ns1, + "annotation": { + "description": TruncatableString( + value="event1", truncated_byte_count=0 + ), + "attributes": self.extracted_attributes_variety_pack, + }, + }, + { + "time": time_in_ms_and_ns2, + "annotation": { + "description": TruncatableString( + value="event2", truncated_byte_count=0 + ), + "attributes": ProtoSpan.Attributes( + attribute_map={}, dropped_attributes_count=1 + ), + }, + }, + ] + ), + ) + + def test_extract_links(self): + self.assertIsNone(_extract_links([])) + trace_id = "6e0c63257de34c92bf9efcd03927272e" + span_id1 = "95bb5edabd45950f" + span_id2 = "b6b86ad2915c9ddc" + link1 = Link( + context=SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id1, 16), + is_remote=False, + ), + attributes={}, + ) + link2 = Link( + context=SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id1, 16), + is_remote=False, + ), + attributes=self.attributes_variety_pack, + ) + link3 = Link( + context=SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id2, 16), + is_remote=False, + ), + attributes={"illegal_attr_value": dict(), "int_attr_value": 123}, + ) + self.assertEqual( + _extract_links([link1, link2, link3]), + ProtoSpan.Links( + link=[ + { + "trace_id": trace_id, + "span_id": span_id1, + "type": "TYPE_UNSPECIFIED", + "attributes": ProtoSpan.Attributes(attribute_map={}), + }, + { + "trace_id": trace_id, + "span_id": span_id1, + "type": "TYPE_UNSPECIFIED", + "attributes": self.extracted_attributes_variety_pack, + }, + { + "trace_id": trace_id, + "span_id": span_id2, + "type": "TYPE_UNSPECIFIED", + "attributes": { + "attribute_map": { + "int_attr_value": AttributeValue(int_value=123) + }, + "dropped_attributes_count": 1, + }, + }, + ] + ), + ) + + # pylint:disable=too-many-locals + def test_truncate(self): + """Cloud Trace API imposes limits on the length of many things, + e.g. strings, number of events, number of attributes. We truncate + these things before sending it to the API as an optimization. + """ + str_300 = "a" * 300 + str_256 = "a" * 256 + str_128 = "a" * 128 + self.assertEqual(_truncate_str("aaaa", 1), ("a", 3)) + self.assertEqual(_truncate_str("aaaa", 5), ("aaaa", 0)) + self.assertEqual(_truncate_str("aaaa", 4), ("aaaa", 0)) + self.assertEqual(_truncate_str("中文翻译", 4), ("中", 9)) + + self.assertEqual( + _format_attribute_value(str_300), + AttributeValue( + string_value=TruncatableString( + value=str_256, truncated_byte_count=300 - 256 + ) + ), + ) + + self.assertEqual( + _extract_attributes({str_300: str_300}, 4), + ProtoSpan.Attributes( + attribute_map={ + str_128: AttributeValue( + string_value=TruncatableString( + value=str_256, truncated_byte_count=300 - 256 + ) + ) + } + ), + ) + + time_in_ns1 = 1589919268850900051 + time_in_ms_and_ns1 = {"seconds": 1589919268, "nanos": 850899968} + event1 = Event(name=str_300, attributes={}, timestamp=time_in_ns1) + self.assertEqual( + _extract_events([event1]), + ProtoSpan.TimeEvents( + time_event=[ + { + "time": time_in_ms_and_ns1, + "annotation": { + "description": TruncatableString( + value=str_256, truncated_byte_count=300 - 256 + ), + "attributes": {}, + }, + }, + ] + ), + ) + + trace_id = "6e0c63257de34c92bf9efcd03927272e" + span_id = "95bb5edabd45950f" + link = Link( + context=SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id, 16), + is_remote=False, + ), + attributes={}, + ) + too_many_links = [link] * (MAX_NUM_LINKS + 1) + self.assertEqual( + _extract_links(too_many_links), + ProtoSpan.Links( + link=[ + { + "trace_id": trace_id, + "span_id": span_id, + "type": "TYPE_UNSPECIFIED", + "attributes": {}, + } + ] + * MAX_NUM_LINKS, + dropped_links_count=len(too_many_links) - MAX_NUM_LINKS, + ), + ) + + link_attrs = {} + for attr_key in range(MAX_LINK_ATTRS + 1): + link_attrs[str(attr_key)] = 0 + attr_link = Link( + context=SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id, 16), + is_remote=False, + ), + attributes=link_attrs, + ) + + proto_link = _extract_links([attr_link]) + self.assertEqual( + len(proto_link.link[0].attributes.attribute_map), MAX_LINK_ATTRS + ) + + too_many_events = [event1] * (MAX_NUM_EVENTS + 1) + self.assertEqual( + _extract_events(too_many_events), + ProtoSpan.TimeEvents( + time_event=[ + { + "time": time_in_ms_and_ns1, + "annotation": { + "description": TruncatableString( + value=str_256, truncated_byte_count=300 - 256 + ), + "attributes": {}, + }, + }, + ] + * MAX_NUM_EVENTS, + dropped_annotations_count=len(too_many_events) + - MAX_NUM_EVENTS, + ), + ) + + time_in_ns1 = 1589919268850900051 + event_attrs = {} + for attr_key in range(MAX_EVENT_ATTRS + 1): + event_attrs[str(attr_key)] = 0 + proto_events = _extract_events( + [Event(name="a", attributes=event_attrs, timestamp=time_in_ns1)] + ) + self.assertEqual( + len( + proto_events.time_event[0].annotation.attributes.attribute_map + ), + MAX_EVENT_ATTRS, + ) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 0b45fbf643..1794cdf01b 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -36,6 +36,7 @@ cov ext/opentelemetry-ext-flask cov ext/opentelemetry-ext-requests cov ext/opentelemetry-ext-jaeger cov ext/opentelemetry-ext-opentracing-shim +cov ext/opentelemetry-exporter-cloud-trace cov ext/opentelemetry-ext-wsgi cov ext/opentelemetry-ext-zipkin cov docs/examples/opentelemetry-example-app diff --git a/tox.ini b/tox.ini index 381e8604e1..026d01cc0c 100644 --- a/tox.ini +++ b/tox.ini @@ -159,6 +159,7 @@ changedir = test-ext-opencensusexporter: ext/opentelemetry-ext-opencensusexporter/tests test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests + test-exporter-cloud-trace: ext/opentelemetry-exporter-cloud-trace/tests test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests test-ext-pymysql: ext/opentelemetry-ext-pymysql/tests test-ext-asgi: ext/opentelemetry-ext-asgi/tests From 75a131194adc2942d75c84fed7533e8c61251818 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 4 Jun 2020 10:02:37 -0700 Subject: [PATCH 0384/1517] metrics api/sdk: Move "config" out from Meter into MeterProvider (#751) --- docs/examples/basic_meter/basic_metrics.py | 28 ++++--------------- docs/examples/basic_meter/observer.py | 2 -- opentelemetry-api/CHANGELOG.md | 2 ++ .../src/opentelemetry/metrics/__init__.py | 20 ++++++------- opentelemetry-sdk/CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/metrics/__init__.py | 25 +++++++++++------ .../tests/metrics/test_metrics.py | 5 ++++ 7 files changed, 39 insertions(+), 45 deletions(-) diff --git a/docs/examples/basic_meter/basic_metrics.py b/docs/examples/basic_meter/basic_metrics.py index 6e8a5c040f..b9ff8d8741 100644 --- a/docs/examples/basic_meter/basic_metrics.py +++ b/docs/examples/basic_meter/basic_metrics.py @@ -30,33 +30,17 @@ stateful = True - -def usage(argv): - print("usage:") - print("{} [mode]".format(argv[0])) - print("mode: stateful (default) or stateless") - - -if len(sys.argv) >= 2: - batcher_mode = sys.argv[1] - if batcher_mode not in ("stateful", "stateless"): - print("bad mode specified.") - usage(sys.argv) - sys.exit(1) - stateful = batcher_mode == "stateful" - print( "Starting example, values will be printed to the console every 5 seconds." ) - +# Stateful determines whether how metrics are collected: if true, metrics +# accumulate over the process lifetime. If false, metrics are reset at the +# beginning of each collection interval. +metrics.set_meter_provider(MeterProvider(stateful)) # The Meter is responsible for creating and recording metrics. Each meter has a -# unique name, which we set as the module's name here. The second argument -# determines whether how metrics are collected: if true, metrics accumulate -# over the process lifetime. If false, metrics are reset at the beginning of -# each collection interval. -metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__, batcher_mode == "stateful") +# unique name, which we set as the module's name here. +meter = metrics.get_meter(__name__) # Exporter to export metrics to the console exporter = ConsoleMetricsExporter() diff --git a/docs/examples/basic_meter/observer.py b/docs/examples/basic_meter/observer.py index 0490fbe8ef..aa70abe2a4 100644 --- a/docs/examples/basic_meter/observer.py +++ b/docs/examples/basic_meter/observer.py @@ -24,8 +24,6 @@ from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.metrics.export.controller import PushController -# Configure a stateful batcher -batcher = UngroupedBatcher(stateful=True) metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__) exporter = ConsoleMetricsExporter() diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 4b678cd422..a67c840190 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Move stateful from Meter to MeterProvider + ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index a91a4ce7b7..16ac4c096f 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -29,7 +29,7 @@ """ import abc from logging import getLogger -from typing import Callable, Dict, Sequence, Tuple, Type, TypeVar +from typing import Callable, Dict, Optional, Sequence, Tuple, Type, TypeVar from opentelemetry.util import _load_provider @@ -195,7 +195,6 @@ class MeterProvider(abc.ABC): def get_meter( self, instrumenting_module_name: str, - stateful: bool = True, instrumenting_library_version: str = "", ) -> "Meter": """Returns a `Meter` for use by the given instrumentation library. @@ -212,12 +211,6 @@ def get_meter( E.g., instead of ``"requests"``, use ``"opentelemetry.ext.requests"``. - stateful: True/False to indicate whether the meter will be - stateful. True indicates the meter computes checkpoints - from over the process lifetime. False indicates the meter - computes checkpoints which describe the updates of a single - collection period (deltas). - instrumenting_library_version: Optional. The version string of the instrumenting library. Usually this should be the same as ``pkg_resources.get_distribution(instrumenting_library_name).version``. @@ -233,7 +226,6 @@ class DefaultMeterProvider(MeterProvider): def get_meter( self, instrumenting_module_name: str, - stateful: bool = True, instrumenting_library_version: str = "", ) -> "Meter": # pylint:disable=no-self-use,unused-argument @@ -376,15 +368,19 @@ def unregister_observer(self, observer: "Observer") -> None: def get_meter( instrumenting_module_name: str, - stateful: bool = True, instrumenting_library_version: str = "", + meter_provider: Optional[MeterProvider] = None, ) -> "Meter": """Returns a `Meter` for use by the given instrumentation library. This function is a convenience wrapper for opentelemetry.metrics.get_meter_provider().get_meter + + If meter_provider is omitted the current configured one is used. """ - return get_meter_provider().get_meter( - instrumenting_module_name, stateful, instrumenting_library_version + if meter_provider is None: + meter_provider = get_meter_provider() + return meter_provider.get_meter( + instrumenting_module_name, instrumenting_library_version ) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 29aaed01dc..3e54a63b3b 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Move stateful & resource from Meter to MeterProvider + ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index b9284bae9f..f231182cc2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -270,22 +270,21 @@ class Meter(metrics_api.Meter): """See `opentelemetry.metrics.Meter`. Args: + source: The `MeterProvider` that created this meter. instrumentation_info: The `InstrumentationInfo` for this meter. - stateful: Indicates whether the meter is stateful. """ def __init__( self, + source: "MeterProvider", instrumentation_info: "InstrumentationInfo", - stateful: bool, - resource: Resource = Resource.create_empty(), ): self.instrumentation_info = instrumentation_info + self.batcher = UngroupedBatcher(source.stateful) + self.resource = source.resource self.metrics = set() self.observers = set() - self.batcher = UngroupedBatcher(stateful) self.observers_lock = threading.Lock() - self.resource = resource def collect(self) -> None: """Collects all the metrics created with this `Meter` for export. @@ -398,21 +397,29 @@ def unregister_observer(self, observer: "Observer") -> None: class MeterProvider(metrics_api.MeterProvider): - def __init__(self, resource: Resource = Resource.create_empty()): + """See `opentelemetry.metrics.MeterProvider`. + + Args: + stateful: Indicates whether meters created are going to be stateful + resource: Resource for this MeterProvider + """ + + def __init__( + self, stateful=True, resource: Resource = Resource.create_empty(), + ): + self.stateful = stateful self.resource = resource def get_meter( self, instrumenting_module_name: str, - stateful=True, instrumenting_library_version: str = "", ) -> "metrics_api.Meter": if not instrumenting_module_name: # Reject empty strings too. raise ValueError("get_meter called with missing module name.") return Meter( + self, InstrumentationInfo( instrumenting_module_name, instrumenting_library_version ), - stateful=stateful, - resource=self.resource, ) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index a3c0f4294d..9d5d2b15d8 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -21,6 +21,11 @@ class TestMeterProvider(unittest.TestCase): + def test_stateful(self): + meter_provider = metrics.MeterProvider(stateful=False) + meter = meter_provider.get_meter(__name__) + self.assertIs(meter.batcher.stateful, False) + def test_resource(self): resource = resources.Resource.create({}) meter_provider = metrics.MeterProvider(resource=resource) From 91f656f2b393551c43f5c199871eae721853563c Mon Sep 17 00:00:00 2001 From: HiveTraum Date: Fri, 5 Jun 2020 22:48:24 +0500 Subject: [PATCH 0385/1517] requests: better exception handling (#765) Canonical codes for different types of exceptions Span attributes extracted from exception Correct span closing for requests with raised errors (A span wasn't closed due to a RequestException) Co-authored-by: alrex --- .../opentelemetry/ext/requests/__init__.py | 59 ++++++++++--- .../tests/test_requests_integration.py | 86 +++++++++++++++++-- 2 files changed, 123 insertions(+), 22 deletions(-) diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py index 1621c4a95e..65bc21370f 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py @@ -45,12 +45,14 @@ import types from urllib.parse import urlparse +from requests import Timeout, URLRequired +from requests.exceptions import InvalidSchema, InvalidURL, MissingSchema from requests.sessions import Session from opentelemetry import context, propagators, trace from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.requests.version import __version__ -from opentelemetry.trace import SpanKind, get_tracer +from opentelemetry.trace import SpanKind from opentelemetry.trace.status import Status, StatusCanonicalCode @@ -80,31 +82,49 @@ def instrumented_request(self, method, url, *args, **kwargs): # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#http-client try: parsed_url = urlparse(url) + span_name = parsed_url.path except ValueError as exc: # Invalid URL - path = "".format(exc) - else: - if parsed_url is None: - path = "" - path = parsed_url.path + span_name = "".format(exc) - with tracer.start_as_current_span(path, kind=SpanKind.CLIENT) as span: + exception = None + + with tracer.start_as_current_span( + span_name, kind=SpanKind.CLIENT + ) as span: span.set_attribute("component", "http") span.set_attribute("http.method", method.upper()) span.set_attribute("http.url", url) headers = kwargs.setdefault("headers", {}) propagators.inject(type(headers).__setitem__, headers) - result = wrapped(self, method, url, *args, **kwargs) # *** PROCEED - span.set_attribute("http.status_code", result.status_code) - span.set_attribute("http.status_text", result.reason) - span.set_status( - Status(_http_status_to_canonical_code(result.status_code)) - ) + try: + result = wrapped( + self, method, url, *args, **kwargs + ) # *** PROCEED + except Exception as exc: # pylint: disable=W0703 + exception = exc + result = getattr(exc, "response", None) + + if exception is not None: + span.set_status( + Status(_exception_to_canonical_code(exception)) + ) + + if result is not None: + span.set_attribute("http.status_code", result.status_code) + span.set_attribute("http.status_text", result.reason) + span.set_status( + Status(_http_status_to_canonical_code(result.status_code)) + ) + if span_callback is not None: span_callback(span, result) - return result + if exception is not None: + raise exception.with_traceback(exception.__traceback__) + + return result instrumented_request.opentelemetry_ext_requests_applied = True @@ -157,6 +177,17 @@ def _http_status_to_canonical_code(code: int, allow_redirect: bool = True): return StatusCanonicalCode.UNKNOWN +def _exception_to_canonical_code(exc: Exception) -> StatusCanonicalCode: + if isinstance( + exc, + (InvalidURL, InvalidSchema, MissingSchema, URLRequired, ValueError), + ): + return StatusCanonicalCode.INVALID_ARGUMENT + if isinstance(exc, Timeout): + return StatusCanonicalCode.DEADLINE_EXCEEDED + return StatusCanonicalCode.UNKNOWN + + class RequestsInstrumentor(BaseInstrumentor): """An instrumentor for requests See `BaseInstrumentor` diff --git a/ext/opentelemetry-ext-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-requests/tests/test_requests_integration.py index 28359d8f38..0ad5b9d19d 100644 --- a/ext/opentelemetry-ext-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-requests/tests/test_requests_integration.py @@ -12,11 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys +from unittest import mock import httpretty import requests -import urllib3 import opentelemetry.ext.requests from opentelemetry import context, propagators, trace @@ -24,6 +23,7 @@ from opentelemetry.sdk import resources from opentelemetry.test.mock_httptextformat import MockHTTPTextFormat from opentelemetry.test.test_base import TestBase +from opentelemetry.trace.status import StatusCanonicalCode class TestRequestsIntegration(TestBase): @@ -92,13 +92,8 @@ def test_not_foundbasic(self): def test_invalid_url(self): url = "http://[::1/nope" - exception_type = requests.exceptions.InvalidURL - if sys.version_info[:2] < (3, 5) and tuple( - map(int, urllib3.__version__.split(".")[:2]) - ) < (1, 25): - exception_type = ValueError - with self.assertRaises(exception_type): + with self.assertRaises(ValueError): requests.post(url) span_list = self.memory_exporter.get_finished_spans() @@ -110,6 +105,9 @@ def test_invalid_url(self): span.attributes, {"component": "http", "http.method": "POST", "http.url": url}, ) + self.assertEqual( + span.status.canonical_code, StatusCanonicalCode.INVALID_ARGUMENT + ) def test_uninstrument(self): RequestsInstrumentor().uninstrument() @@ -229,3 +227,75 @@ def test_custom_tracer_provider(self): span = span_list[0] self.assertIs(span.resource, resource) + + @mock.patch("requests.Session.send", side_effect=requests.RequestException) + def test_requests_exception_without_response(self, *_, **__): + + with self.assertRaises(requests.RequestException): + requests.get(self.URL) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + span = span_list[0] + self.assertEqual( + span.attributes, + {"component": "http", "http.method": "GET", "http.url": self.URL}, + ) + self.assertEqual( + span.status.canonical_code, StatusCanonicalCode.UNKNOWN + ) + + mocked_response = requests.Response() + mocked_response.status_code = 500 + mocked_response.reason = "Internal Server Error" + + @mock.patch( + "requests.Session.send", + side_effect=requests.RequestException(response=mocked_response), + ) + def test_requests_exception_with_response(self, *_, **__): + + with self.assertRaises(requests.RequestException): + requests.get(self.URL) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + span = span_list[0] + self.assertEqual( + span.attributes, + { + "component": "http", + "http.method": "GET", + "http.url": self.URL, + "http.status_code": 500, + "http.status_text": "Internal Server Error", + }, + ) + self.assertEqual( + span.status.canonical_code, StatusCanonicalCode.INTERNAL + ) + + @mock.patch("requests.Session.send", side_effect=Exception) + def test_requests_basic_exception(self, *_, **__): + + with self.assertRaises(Exception): + requests.get(self.URL) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual( + span_list[0].status.canonical_code, StatusCanonicalCode.UNKNOWN + ) + + @mock.patch("requests.Session.send", side_effect=requests.Timeout) + def test_requests_timeout_exception(self, *_, **__): + + with self.assertRaises(Exception): + requests.get(self.URL) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual( + span_list[0].status.canonical_code, + StatusCanonicalCode.DEADLINE_EXCEEDED, + ) From b108596d967d162cf59142233c039fd9dcfb0626 Mon Sep 17 00:00:00 2001 From: Seth Maxwell Date: Fri, 5 Jun 2020 20:10:07 +0000 Subject: [PATCH 0386/1517] bugfix: Fix for byte type attributes crashing spans (#775) --- opentelemetry-sdk/CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/__init__.py | 6 ++++++ opentelemetry-sdk/tests/trace/test_trace.py | 16 ++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 3e54a63b3b..0e982b6bc1 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -6,6 +6,8 @@ ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) +- bugfix: byte type attributes are decoded before adding to attributes dict + ([#775](https://github.com/open-telemetry/opentelemetry-python/pull/775)) ## 0.8b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 20c50a849f..980caed37d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -424,6 +424,12 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: # Freeze mutable sequences defensively if isinstance(value, MutableSequence): value = tuple(value) + if isinstance(value, bytes): + try: + value = value.decode() + except ValueError: + logger.warning("Byte attribute could not be decoded.") + return with self._lock: self.attributes[key] = value diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 7e6ab49c39..32348d87d9 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -487,6 +487,22 @@ def test_invalid_attribute_values(self): self.assertEqual(len(root.attributes), 0) + def test_byte_type_attribute_value(self): + with self.tracer.start_as_current_span("root") as root: + with self.assertLogs(level=WARNING): + root.set_attribute( + "invalid-byte-type-attribute", + b"\xd8\xe1\xb7\xeb\xa8\xe5 \xd2\xb7\xe1", + ) + self.assertFalse( + "invalid-byte-type-attribute" in root.attributes + ) + + root.set_attribute("valid-byte-type-attribute", b"valid byte") + self.assertTrue( + isinstance(root.attributes["valid-byte-type-attribute"], str) + ) + def test_check_attribute_helper(self): # pylint: disable=protected-access self.assertFalse(trace._is_valid_attribute_value([1, 2, 3.4, "ss", 4])) From 20cf4cb087371c1e255e9efb7072a78fdad55333 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 8 Jun 2020 12:39:57 -0400 Subject: [PATCH 0387/1517] refactor: Typing fixes #768, upgrade mypy to 0.770 (#769) --- dev-requirements.txt | 2 +- .../src/opentelemetry/__init__.pyi | 0 .../opentelemetry/configuration/__init__.py | 73 ++++++++++--------- .../src/opentelemetry/metrics/__init__.py | 4 +- .../src/opentelemetry/trace/__init__.py | 4 +- .../src/opentelemetry/util/__init__.py | 36 ++++++--- .../tests/configuration/test_configuration.py | 27 ++++--- 7 files changed, 83 insertions(+), 63 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/__init__.pyi diff --git a/dev-requirements.txt b/dev-requirements.txt index be74d804b3..253c089507 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,7 +2,7 @@ pylint==2.4.4 flake8==3.7.9 isort~=4.3 black>=19.3b0,==19.* -mypy==0.740 +mypy==0.770 sphinx~=2.1 sphinx-rtd-theme~=0.4 sphinx-autodoc-typehints~=1.10.2 diff --git a/opentelemetry-api/src/opentelemetry/__init__.pyi b/opentelemetry-api/src/opentelemetry/__init__.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index 4809341502..0bd2bce8d6 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -12,9 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# FIXME find a better way to avoid all those "Expression has type "Any"" errors -# type: ignore - """ Simple configuration manager @@ -95,17 +92,23 @@ from os import environ from re import fullmatch +from typing import ClassVar, Dict, Optional, TypeVar, Union +ConfigValue = Union[str, bool, int, float] +_T = TypeVar("_T", ConfigValue, Optional[ConfigValue]) -class Configuration: - _instance = None - __slots__ = [] +class Configuration: + _instance = None # type: ClassVar[Optional[Configuration]] + _config_map = {} # type: ClassVar[Dict[str, ConfigValue]] def __new__(cls) -> "Configuration": - if Configuration._instance is None: + if cls._instance is not None: + instance = cls._instance + else: - for key, value in environ.items(): + instance = super().__new__(cls) + for key, value_str in environ.items(): match = fullmatch( r"OPENTELEMETRY_PYTHON_([A-Za-z_][\w_]*)", key @@ -114,45 +117,47 @@ def __new__(cls) -> "Configuration": if match is not None: key = match.group(1) + value = value_str # type: ConfigValue - if value == "True": + if value_str == "True": value = True - elif value == "False": + elif value_str == "False": value = False else: try: - value = int(value) + value = int(value_str) except ValueError: pass try: - value = float(value) + value = float(value_str) except ValueError: pass - setattr(Configuration, "_{}".format(key), value) - setattr( - Configuration, - key, - property( - fget=lambda cls, key=key: getattr( - cls, "_{}".format(key) - ) - ), - ) + instance._config_map[key] = value - Configuration.__slots__.append(key) + cls._instance = instance - Configuration.__slots__ = tuple(Configuration.__slots__) + return instance - Configuration._instance = object.__new__(cls) + def __getattr__(self, name: str) -> Optional[ConfigValue]: + return self._config_map.get(name) - return cls._instance + def __setattr__(self, key: str, val: ConfigValue) -> None: + if key == "_config_map": + super().__setattr__(key, val) + else: + raise AttributeError(key) - def __getattr__(self, name): - return None + def get(self, name: str, default: _T) -> _T: + """Use this typed method for dynamic access instead of `getattr` + + :rtype: str or bool or int or float or None + """ + val = self._config_map.get(name, default) + return val @classmethod - def _reset(cls): + def _reset(cls) -> None: """ This method "resets" the global configuration attributes @@ -160,10 +165,6 @@ def _reset(cls): only. """ - for slot in cls.__slots__: - if slot in cls.__dict__.keys(): - delattr(cls, slot) - delattr(cls, "_{}".format(slot)) - - cls.__slots__ = [] - cls._instance = None + if cls._instance: + cls._instance._config_map.clear() # pylint: disable=protected-access + cls._instance = None diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 16ac4c096f..7d16c56a98 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -31,7 +31,7 @@ from logging import getLogger from typing import Callable, Dict, Optional, Sequence, Tuple, Type, TypeVar -from opentelemetry.util import _load_provider +from opentelemetry.util import _load_meter_provider logger = getLogger(__name__) ValueT = TypeVar("ValueT", int, float) @@ -395,6 +395,6 @@ def get_meter_provider() -> MeterProvider: global _METER_PROVIDER # pylint: disable=global-statement if _METER_PROVIDER is None: - _METER_PROVIDER = _load_provider("meter_provider") + _METER_PROVIDER = _load_meter_provider("meter_provider") return _METER_PROVIDER diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 13aaf2c6a4..25af91a400 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -78,7 +78,7 @@ from logging import getLogger from opentelemetry.trace.status import Status -from opentelemetry.util import _load_provider, types +from opentelemetry.util import _load_trace_provider, types logger = getLogger(__name__) @@ -706,6 +706,6 @@ def get_tracer_provider() -> TracerProvider: global _TRACER_PROVIDER # pylint: disable=global-statement if _TRACER_PROVIDER is None: - _TRACER_PROVIDER = _load_provider("tracer_provider") + _TRACER_PROVIDER = _load_trace_provider("tracer_provider") return _TRACER_PROVIDER diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index bab42b0bde..ed1268fcb6 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -14,11 +14,17 @@ import re import time from logging import getLogger -from typing import Sequence, Union +from typing import TYPE_CHECKING, Sequence, Union, cast from pkg_resources import iter_entry_points -from opentelemetry.configuration import Configuration # type: ignore +from opentelemetry.configuration import Configuration + +if TYPE_CHECKING: + from opentelemetry.trace import TracerProvider + from opentelemetry.metrics import MeterProvider + +Provider = Union["TracerProvider", "MeterProvider"] logger = getLogger(__name__) @@ -34,25 +40,33 @@ def time_ns() -> int: return int(time.time() * 1e9) -def _load_provider( - provider: str, -) -> Union["TracerProvider", "MeterProvider"]: # type: ignore +def _load_provider(provider: str) -> Provider: try: - return next( # type: ignore + entry_point = next( iter_entry_points( "opentelemetry_{}".format(provider), - name=getattr( - Configuration(), # type: ignore - provider, - "default_{}".format(provider), + name=cast( + str, + Configuration().get( + provider, "default_{}".format(provider), + ), ), ) - ).load()() + ) + return cast(Provider, entry_point.load()(),) except Exception: # pylint: disable=broad-except logger.error("Failed to load configured provider %s", provider) raise +def _load_meter_provider(provider: str) -> "MeterProvider": + return cast("MeterProvider", _load_provider(provider)) + + +def _load_trace_provider(provider: str) -> "TracerProvider": + return cast("TracerProvider", _load_provider(provider)) + + # Pattern for matching up until the first '/' after the 'https://' part. _URL_PATTERN = r"(https?|ftp)://.*?/" diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index a817a1bf43..32b62e619d 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -16,16 +16,16 @@ from unittest import TestCase from unittest.mock import patch -from opentelemetry.configuration import Configuration # type: ignore +from opentelemetry.configuration import Configuration class TestConfiguration(TestCase): - def tearDown(self): + def tearDown(self) -> None: # This call resets the attributes of the Configuration class so that # each test is executed in the same conditions. Configuration._reset() - def test_singleton(self): + def test_singleton(self) -> None: self.assertIsInstance(Configuration(), Configuration) self.assertIs(Configuration(), Configuration()) @@ -39,7 +39,7 @@ def test_singleton(self): "OPENTELEMETRY_PTHON_TRACEX_PROVIDER": "tracex_provider", }, ) - def test_environment_variables(self): # type: ignore + def test_environment_variables(self): self.assertEqual( Configuration().METER_PROVIDER, "meter_provider" ) # pylint: disable=no-member @@ -62,16 +62,21 @@ def test_property(self): with self.assertRaises(AttributeError): Configuration().TRACER_PROVIDER = "new_tracer_provider" - def test_slots(self): + def test_slots(self) -> None: with self.assertRaises(AttributeError): Configuration().XYZ = "xyz" # pylint: disable=assigning-non-slot - def test_getattr(self): + def test_getattr(self) -> None: + # literal access self.assertIsNone(Configuration().XYZ) - def test_reset(self): + # dynamic access + self.assertIsNone(getattr(Configuration(), "XYZ")) + self.assertIsNone(Configuration().get("XYZ", None)) + + def test_reset(self) -> None: environ_patcher = patch.dict( - "os.environ", # type: ignore + "os.environ", {"OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider"}, ) @@ -96,7 +101,7 @@ def test_reset(self): "OPENTELEMETRY_PYTHON_FALSE": "False", }, ) - def test_boolean(self): + def test_boolean(self) -> None: self.assertIsInstance( Configuration().TRUE, bool ) # pylint: disable=no-member @@ -114,7 +119,7 @@ def test_boolean(self): "OPENTELEMETRY_PYTHON_NON_INTEGER": "-12z3", }, ) - def test_integer(self): + def test_integer(self) -> None: self.assertEqual( Configuration().POSITIVE_INTEGER, 123 ) # pylint: disable=no-member @@ -133,7 +138,7 @@ def test_integer(self): "OPENTELEMETRY_PYTHON_NON_FLOAT": "-12z3.123", }, ) - def test_float(self): + def test_float(self) -> None: self.assertEqual( Configuration().POSITIVE_FLOAT, 123.123 ) # pylint: disable=no-member From d7553759979dadf8b661a01981607fa4fac47ddc Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 8 Jun 2020 10:47:57 -0700 Subject: [PATCH 0388/1517] api/sdk: Observer to ValueObserver (#764) --- docs/examples/basic_meter/observer.py | 3 +- .../metrics_exporter/__init__.py | 14 ++--- .../test_otcollector_metrics_exporter.py | 10 ++-- .../opentelemetry/ext/prometheus/__init__.py | 12 ++-- .../tests/test_prometheus_exporter.py | 6 +- .../ext/system_metrics/__init__.py | 19 +++++-- .../tests/test_system_metrics.py | 2 +- opentelemetry-api/CHANGELOG.md | 2 + .../src/opentelemetry/metrics/__init__.py | 20 ++++++- .../tests/test_implementation.py | 4 +- opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/metrics/__init__.py | 13 +++-- .../sdk/metrics/export/__init__.py | 10 ++-- .../sdk/metrics/export/aggregate.py | 2 +- .../sdk/metrics/export/batcher.py | 29 ++++++---- .../tests/metrics/export/test_export.py | 56 ++++++++++++------- .../tests/metrics/test_metrics.py | 18 +++--- 17 files changed, 136 insertions(+), 86 deletions(-) diff --git a/docs/examples/basic_meter/observer.py b/docs/examples/basic_meter/observer.py index aa70abe2a4..b61b9e4db8 100644 --- a/docs/examples/basic_meter/observer.py +++ b/docs/examples/basic_meter/observer.py @@ -19,7 +19,7 @@ import psutil from opentelemetry import metrics -from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics import MeterProvider, ValueObserver from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.metrics.export.controller import PushController @@ -43,6 +43,7 @@ def get_cpu_usage_callback(observer): description="per-cpu usage", unit="1", value_type=float, + observer_type=ValueObserver, label_keys=("cpu_number",), ) diff --git a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/metrics_exporter/__init__.py b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/metrics_exporter/__init__.py index faa0788c7f..bb1a1ee888 100644 --- a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/metrics_exporter/__init__.py +++ b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/metrics_exporter/__init__.py @@ -114,10 +114,10 @@ def translate_to_collector( ) metric_descriptor = metrics_pb2.MetricDescriptor( - name=metric_record.metric.name, - description=metric_record.metric.description, - unit=metric_record.metric.unit, - type=get_collector_metric_type(metric_record.metric), + name=metric_record.instrument.name, + description=metric_record.instrument.description, + unit=metric_record.instrument.unit, + type=get_collector_metric_type(metric_record.instrument), label_keys=label_keys, ) @@ -151,14 +151,14 @@ def get_collector_point(metric_record: MetricRecord) -> metrics_pb2.Point: metric_record.aggregator.last_update_timestamp ) ) - if metric_record.metric.value_type == int: + if metric_record.instrument.value_type == int: point.int64_value = metric_record.aggregator.checkpoint - elif metric_record.metric.value_type == float: + elif metric_record.instrument.value_type == float: point.double_value = metric_record.aggregator.checkpoint else: raise TypeError( "Unsupported metric type: {}".format( - metric_record.metric.value_type + metric_record.instrument.value_type ) ) return point diff --git a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py index 18b4a32806..f907012647 100644 --- a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py +++ b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py @@ -92,7 +92,7 @@ def test_get_collector_point(self): "testName", "testDescription", "unit", float, ValueRecorder ) result = metrics_exporter.get_collector_point( - MetricRecord(aggregator, self._key_labels, int_counter) + MetricRecord(int_counter, self._key_labels, aggregator) ) self.assertIsInstance(result, metrics_pb2.Point) self.assertIsInstance(result.timestamp, Timestamp) @@ -100,13 +100,13 @@ def test_get_collector_point(self): aggregator.update(123.5) aggregator.take_checkpoint() result = metrics_exporter.get_collector_point( - MetricRecord(aggregator, self._key_labels, float_counter) + MetricRecord(float_counter, self._key_labels, aggregator) ) self.assertEqual(result.double_value, 123.5) self.assertRaises( TypeError, metrics_exporter.get_collector_point( - MetricRecord(aggregator, self._key_labels, valuerecorder) + MetricRecord(valuerecorder, self._key_labels, aggregator) ), ) @@ -122,7 +122,7 @@ def test_export(self): "testname", "testdesc", "unit", int, Counter, ["environment"] ) record = MetricRecord( - aggregate.CounterAggregator(), self._key_labels, test_metric + test_metric, self._key_labels, aggregate.CounterAggregator(), ) result = collector_exporter.export([record]) @@ -147,7 +147,7 @@ def test_translate_to_collector(self): aggregator = aggregate.CounterAggregator() aggregator.update(123) aggregator.take_checkpoint() - record = MetricRecord(aggregator, self._key_labels, test_metric) + record = MetricRecord(test_metric, self._key_labels, aggregator,) output_metrics = metrics_exporter.translate_to_collector([record]) self.assertEqual(len(output_metrics), 1) self.assertIsInstance(output_metrics[0], metrics_pb2.Metric) diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py index cc44621ac4..59ef3f1708 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py @@ -152,22 +152,22 @@ def _translate_to_prometheus(self, metric_record: MetricRecord): metric_name = "" if self._prefix != "": metric_name = self._prefix + "_" - metric_name += self._sanitize(metric_record.metric.name) + metric_name += self._sanitize(metric_record.instrument.name) - if isinstance(metric_record.metric, Counter): + if isinstance(metric_record.instrument, Counter): prometheus_metric = CounterMetricFamily( name=metric_name, - documentation=metric_record.metric.description, + documentation=metric_record.instrument.description, labels=label_keys, ) prometheus_metric.add_metric( labels=label_values, value=metric_record.aggregator.checkpoint ) # TODO: Add support for histograms when supported in OT - elif isinstance(metric_record.metric, ValueRecorder): + elif isinstance(metric_record.instrument, ValueRecorder): prometheus_metric = UnknownMetricFamily( name=metric_name, - documentation=metric_record.metric.description, + documentation=metric_record.instrument.description, labels=label_keys, ) prometheus_metric.add_metric( @@ -176,7 +176,7 @@ def _translate_to_prometheus(self, metric_record: MetricRecord): else: logger.warning( - "Unsupported metric type. %s", type(metric_record.metric) + "Unsupported metric type. %s", type(metric_record.instrument) ) return prometheus_metric diff --git a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py index f986e0c4f5..1862f789c0 100644 --- a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py +++ b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py @@ -67,7 +67,7 @@ def test_shutdown(self): def test_export(self): with self._registry_register_patch: record = MetricRecord( - CounterAggregator(), self._labels_key, self._test_metric + self._test_metric, self._labels_key, CounterAggregator(), ) exporter = PrometheusMetricsExporter() result = exporter.export([record]) @@ -90,7 +90,7 @@ def test_counter_to_prometheus(self): aggregator = CounterAggregator() aggregator.update(123) aggregator.take_checkpoint() - record = MetricRecord(aggregator, key_labels, metric) + record = MetricRecord(metric, key_labels, aggregator) collector = CustomCollector("testprefix") collector.add_metrics_data([record]) @@ -118,7 +118,7 @@ def test_invalid_metric(self): ) labels = {"environment": "staging"} key_labels = metrics.get_labels_as_key(labels) - record = MetricRecord(None, key_labels, metric) + record = MetricRecord(metric, key_labels, None) collector = CustomCollector("testprefix") collector.add_metrics_data([record]) collector.collect() diff --git a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py index 88f36f4ac4..09c633f14b 100644 --- a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py +++ b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py @@ -58,6 +58,7 @@ import psutil from opentelemetry import metrics +from opentelemetry.sdk.metrics import ValueObserver from opentelemetry.sdk.metrics.export import MetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController @@ -106,6 +107,7 @@ def __init__( description="System memory", unit="bytes", value_type=int, + observer_type=ValueObserver, ) self.meter.register_observer( @@ -114,6 +116,7 @@ def __init__( description="System CPU", unit="seconds", value_type=float, + observer_type=ValueObserver, ) self.meter.register_observer( @@ -122,6 +125,7 @@ def __init__( description="System network bytes", unit="bytes", value_type=int, + observer_type=ValueObserver, ) self.meter.register_observer( @@ -130,6 +134,7 @@ def __init__( description="Runtime memory", unit="bytes", value_type=int, + observer_type=ValueObserver, ) self.meter.register_observer( @@ -138,6 +143,7 @@ def __init__( description="Runtime CPU", unit="seconds", value_type=float, + observer_type=ValueObserver, ) self.meter.register_observer( @@ -146,9 +152,10 @@ def __init__( description="Runtime: gc objects", unit="objects", value_type=int, + observer_type=ValueObserver, ) - def _get_system_memory(self, observer: metrics.Observer) -> None: + def _get_system_memory(self, observer: metrics.ValueObserver) -> None: """Observer callback for memory available Args: @@ -161,7 +168,7 @@ def _get_system_memory(self, observer: metrics.Observer) -> None: getattr(system_memory, metric), self._system_memory_labels ) - def _get_system_cpu(self, observer: metrics.Observer) -> None: + def _get_system_cpu(self, observer: metrics.ValueObserver) -> None: """Observer callback for system cpu Args: @@ -174,7 +181,7 @@ def _get_system_cpu(self, observer: metrics.Observer) -> None: getattr(cpu_times, _type), self._system_cpu_labels ) - def _get_network_bytes(self, observer: metrics.Observer) -> None: + def _get_network_bytes(self, observer: metrics.ValueObserver) -> None: """Observer callback for network bytes Args: @@ -187,7 +194,7 @@ def _get_network_bytes(self, observer: metrics.Observer) -> None: getattr(net_io, _type), self._network_bytes_labels ) - def _get_runtime_memory(self, observer: metrics.Observer) -> None: + def _get_runtime_memory(self, observer: metrics.ValueObserver) -> None: """Observer callback for runtime memory Args: @@ -200,7 +207,7 @@ def _get_runtime_memory(self, observer: metrics.Observer) -> None: getattr(proc_memory, _type), self._runtime_memory_labels ) - def _get_runtime_cpu(self, observer: metrics.Observer) -> None: + def _get_runtime_cpu(self, observer: metrics.ValueObserver) -> None: """Observer callback for runtime CPU Args: @@ -213,7 +220,7 @@ def _get_runtime_cpu(self, observer: metrics.Observer) -> None: getattr(proc_cpu, _type), self._runtime_cpu_labels ) - def _get_runtime_gc_count(self, observer: metrics.Observer) -> None: + def _get_runtime_gc_count(self, observer: metrics.ValueObserver) -> None: """Observer callback for garbage collection Args: diff --git a/ext/opentelemetry-ext-system-metrics/tests/test_system_metrics.py b/ext/opentelemetry-ext-system-metrics/tests/test_system_metrics.py index 70ead2c515..b2d6bab401 100644 --- a/ext/opentelemetry-ext-system-metrics/tests/test_system_metrics.py +++ b/ext/opentelemetry-ext-system-metrics/tests/test_system_metrics.py @@ -54,7 +54,7 @@ def _assert_metrics(self, observer_name, system_metrics, expected): ): if ( metric.labels in expected - and metric.metric.name == observer_name + and metric.instrument.name == observer_name ): self.assertEqual( metric.aggregator.checkpoint.last, expected[metric.labels], diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index a67c840190..4503489fa8 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -6,6 +6,8 @@ ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) +- Rename Observer to ValueObserver + ([#764](https://github.com/open-telemetry/opentelemetry-python/pull/764)) ## 0.8b0 diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 7d16c56a98..da47356e05 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -162,7 +162,6 @@ class Observer(abc.ABC): """An observer type metric instrument used to capture a current set of values. - Observer instruments are asynchronous, a callback is invoked with the observer instrument as argument allowing the user to capture multiple values per collection interval. @@ -190,6 +189,18 @@ def observe(self, value: ValueT, labels: Dict[str, str]) -> None: """ +class ValueObserver(Observer): + """No-op implementation of ``ValueObserver``.""" + + def observe(self, value: ValueT, labels: Dict[str, str]) -> None: + """Captures ``value`` to the valueobserver. + + Args: + value: The value to capture to this valueobserver metric. + labels: Labels associated to ``value``. + """ + + class MeterProvider(abc.ABC): @abc.abstractmethod def get_meter( @@ -232,7 +243,9 @@ def get_meter( return DefaultMeter() -MetricT = TypeVar("MetricT", Counter, ValueRecorder, Observer) +MetricT = TypeVar("MetricT", Counter, ValueRecorder) +InstrumentT = TypeVar("InstrumentT", Counter, Observer, ValueRecorder) +ObserverT = TypeVar("ObserverT", bound=Observer) ObserverCallbackT = Callable[[Observer], None] @@ -297,6 +310,7 @@ def register_observer( description: str, unit: str, value_type: Type[ValueT], + observer_type: Type[ObserverT], label_keys: Sequence[str] = (), enabled: bool = True, ) -> "Observer": @@ -310,6 +324,7 @@ def register_observer( unit: Unit of the metric values following the UCUM convention (https://unitsofmeasure.org/ucum.html). value_type: The type of values being recorded by the metric. + observer_type: The type of observer being registered. label_keys: The keys for the labels with dynamic values. enabled: Whether to report the metric by default. Returns: A new ``Observer`` metric instrument. @@ -354,6 +369,7 @@ def register_observer( description: str, unit: str, value_type: Type[ValueT], + observer_type: Type[ObserverT], label_keys: Sequence[str] = (), enabled: bool = True, ) -> "Observer": diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index 735ac4a683..d0f9404a91 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -83,7 +83,9 @@ def test_create_metric(self): def test_register_observer(self): meter = metrics.DefaultMeter() callback = mock.Mock() - observer = meter.register_observer(callback, "", "", "", int, (), True) + observer = meter.register_observer( + callback, "", "", "", int, metrics.ValueObserver + ) self.assertIsInstance(observer, metrics.DefaultObserver) def test_unregister_observer(self): diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 0e982b6bc1..d8eb23ab52 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -8,6 +8,8 @@ ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) - bugfix: byte type attributes are decoded before adding to attributes dict ([#775](https://github.com/open-telemetry/opentelemetry-python/pull/775)) +- Rename Observer to ValueObserver + ([#764](https://github.com/open-telemetry/opentelemetry-python/pull/764)) ## 0.8b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index f231182cc2..507e00d8ea 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -190,8 +190,8 @@ def record( UPDATE_FUNCTION = record -class Observer(metrics_api.Observer): - """See `opentelemetry.metrics.Observer`.""" +class ValueObserver(metrics_api.ValueObserver): + """See `opentelemetry.metrics.ValueObserver`.""" def __init__( self, @@ -257,11 +257,11 @@ class Record: def __init__( self, - metric: metrics_api.MetricT, + instrument: metrics_api.InstrumentT, labels: Dict[str, str], aggregator: Aggregator, ): - self.metric = metric + self.instrument = instrument self.labels = labels self.aggregator = aggregator @@ -374,10 +374,11 @@ def register_observer( description: str, unit: str, value_type: Type[metrics_api.ValueT], + observer_type=Type[metrics_api.ObserverT], label_keys: Sequence[str] = (), enabled: bool = True, ) -> metrics_api.Observer: - ob = Observer( + ob = observer_type( callback, name, description, @@ -391,7 +392,7 @@ def register_observer( self.observers.add(ob) return ob - def unregister_observer(self, observer: "Observer") -> None: + def unregister_observer(self, observer: metrics_api.Observer) -> None: with self.observers_lock: self.observers.remove(observer) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index f5a8693268..16911f94ef 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -27,13 +27,13 @@ class MetricsExportResult(Enum): class MetricRecord: def __init__( self, - aggregator: Aggregator, + instrument: metrics_api.InstrumentT, labels: Tuple[Tuple[str, str]], - metric: metrics_api.MetricT, + aggregator: Aggregator, ): - self.aggregator = aggregator + self.instrument = instrument self.labels = labels - self.metric = metric + self.aggregator = aggregator class MetricsExporter: @@ -79,7 +79,7 @@ def export( print( '{}(data="{}", labels="{}", value={})'.format( type(self).__name__, - record.metric, + record.instrument, record.labels, record.aggregator.checkpoint, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 7e1baba2c7..1745d854e9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -125,7 +125,7 @@ def merge(self, other): ) -class ObserverAggregator(Aggregator): +class ValueObserverAggregator(Aggregator): """Same as MinMaxSumCount but also with last value.""" _TYPE = namedtuple("minmaxsumcountlast", "min max sum count last") diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index eda504d568..db3675ecd6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -15,13 +15,18 @@ import abc from typing import Sequence, Type -from opentelemetry.metrics import Counter, MetricT, Observer, ValueRecorder +from opentelemetry.metrics import ( + Counter, + InstrumentT, + ValueObserver, + ValueRecorder, +) from opentelemetry.sdk.metrics.export import MetricRecord from opentelemetry.sdk.metrics.export.aggregate import ( Aggregator, CounterAggregator, MinMaxSumCountAggregator, - ObserverAggregator, + ValueObserverAggregator, ) @@ -41,18 +46,18 @@ def __init__(self, stateful: bool): # (deltas) self.stateful = stateful - def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: - """Returns an aggregator based on metric type. + def aggregator_for(self, instrument_type: Type[InstrumentT]) -> Aggregator: + """Returns an aggregator based on metric instrument type. Aggregators keep track of and updates values when metrics get updated. """ # pylint:disable=R0201 - if issubclass(metric_type, Counter): + if issubclass(instrument_type, Counter): return CounterAggregator() - if issubclass(metric_type, ValueRecorder): + if issubclass(instrument_type, ValueRecorder): return MinMaxSumCountAggregator() - if issubclass(metric_type, Observer): - return ObserverAggregator() + if issubclass(instrument_type, ValueObserver): + return ValueObserverAggregator() # TODO: Add other aggregators return CounterAggregator() @@ -63,8 +68,8 @@ def checkpoint_set(self) -> Sequence[MetricRecord]: data in all of the aggregators in this batcher. """ metric_records = [] - for (metric, labels), aggregator in self._batch_map.items(): - metric_records.append(MetricRecord(aggregator, labels, metric)) + for (instrument, labels), aggregator in self._batch_map.items(): + metric_records.append(MetricRecord(instrument, labels, aggregator)) return metric_records def finished_collection(self): @@ -90,7 +95,7 @@ class UngroupedBatcher(Batcher): def process(self, record): # Checkpoints the current aggregator value to be collected for export record.aggregator.take_checkpoint() - batch_key = (record.metric, record.labels) + batch_key = (record.instrument, record.labels) batch_value = self._batch_map.get(batch_key) aggregator = record.aggregator if batch_value: @@ -101,6 +106,6 @@ def process(self, record): if self.stateful: # if stateful batcher, create a copy of the aggregator and update # it with the current checkpointed value for long-term storage - aggregator = self.aggregator_for(record.metric.__class__) + aggregator = self.aggregator_for(record.instrument.__class__) aggregator.merge(record.aggregator) self._batch_map[batch_key] = aggregator diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index c77a132459..178e41b213 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -26,7 +26,7 @@ from opentelemetry.sdk.metrics.export.aggregate import ( CounterAggregator, MinMaxSumCountAggregator, - ObserverAggregator, + ValueObserverAggregator, ) from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.metrics.export.controller import PushController @@ -48,7 +48,7 @@ def test_export(self): ) labels = {"environment": "staging"} aggregator = CounterAggregator() - record = MetricRecord(aggregator, labels, metric) + record = MetricRecord(metric, labels, aggregator) result = '{}(data="{}", labels="{}", value={})'.format( ConsoleMetricsExporter.__name__, metric, @@ -90,7 +90,7 @@ def test_checkpoint_set(self): batcher._batch_map = _batch_map records = batcher.checkpoint_set() self.assertEqual(len(records), 1) - self.assertEqual(records[0].metric, metric) + self.assertEqual(records[0].instrument, metric) self.assertEqual(records[0].labels, labels) self.assertEqual(records[0].aggregator, aggregator) @@ -432,11 +432,11 @@ def test_concurrent_update_and_checkpoint(self): self.assertEqual(checkpoint_total, fut.result()) -class TestObserverAggregator(unittest.TestCase): +class TestValueObserverAggregator(unittest.TestCase): @mock.patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") def test_update(self, time_mock): time_mock.return_value = 123 - observer = ObserverAggregator() + observer = ValueObserverAggregator() # test current values without any update self.assertEqual(observer.mmsc.current, (None, None, None, 0)) self.assertIsNone(observer.current) @@ -455,7 +455,7 @@ def test_update(self, time_mock): self.assertEqual(observer.current, values[-1]) def test_checkpoint(self): - observer = ObserverAggregator() + observer = ValueObserverAggregator() # take checkpoint wihtout any update observer.take_checkpoint() @@ -473,15 +473,19 @@ def test_checkpoint(self): ) def test_merge(self): - observer1 = ObserverAggregator() - observer2 = ObserverAggregator() + observer1 = ValueObserverAggregator() + observer2 = ValueObserverAggregator() mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) mmsc_checkpoint2 = MinMaxSumCountAggregator._TYPE(1, 33, 44, 2) - checkpoint1 = ObserverAggregator._TYPE(*(mmsc_checkpoint1 + (23,))) + checkpoint1 = ValueObserverAggregator._TYPE( + *(mmsc_checkpoint1 + (23,)) + ) - checkpoint2 = ObserverAggregator._TYPE(*(mmsc_checkpoint2 + (27,))) + checkpoint2 = ValueObserverAggregator._TYPE( + *(mmsc_checkpoint2 + (27,)) + ) observer1.mmsc.checkpoint = mmsc_checkpoint1 observer2.mmsc.checkpoint = mmsc_checkpoint2 @@ -507,15 +511,19 @@ def test_merge(self): self.assertEqual(observer1.last_update_timestamp, 123) def test_merge_last_updated(self): - observer1 = ObserverAggregator() - observer2 = ObserverAggregator() + observer1 = ValueObserverAggregator() + observer2 = ValueObserverAggregator() mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) mmsc_checkpoint2 = MinMaxSumCountAggregator._TYPE(1, 33, 44, 2) - checkpoint1 = ObserverAggregator._TYPE(*(mmsc_checkpoint1 + (23,))) + checkpoint1 = ValueObserverAggregator._TYPE( + *(mmsc_checkpoint1 + (23,)) + ) - checkpoint2 = ObserverAggregator._TYPE(*(mmsc_checkpoint2 + (27,))) + checkpoint2 = ValueObserverAggregator._TYPE( + *(mmsc_checkpoint2 + (27,)) + ) observer1.mmsc.checkpoint = mmsc_checkpoint1 observer2.mmsc.checkpoint = mmsc_checkpoint2 @@ -541,15 +549,19 @@ def test_merge_last_updated(self): self.assertEqual(observer1.last_update_timestamp, 123) def test_merge_last_updated_none(self): - observer1 = ObserverAggregator() - observer2 = ObserverAggregator() + observer1 = ValueObserverAggregator() + observer2 = ValueObserverAggregator() mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) mmsc_checkpoint2 = MinMaxSumCountAggregator._TYPE(1, 33, 44, 2) - checkpoint1 = ObserverAggregator._TYPE(*(mmsc_checkpoint1 + (23,))) + checkpoint1 = ValueObserverAggregator._TYPE( + *(mmsc_checkpoint1 + (23,)) + ) - checkpoint2 = ObserverAggregator._TYPE(*(mmsc_checkpoint2 + (27,))) + checkpoint2 = ValueObserverAggregator._TYPE( + *(mmsc_checkpoint2 + (27,)) + ) observer1.mmsc.checkpoint = mmsc_checkpoint1 observer2.mmsc.checkpoint = mmsc_checkpoint2 @@ -575,11 +587,13 @@ def test_merge_last_updated_none(self): self.assertEqual(observer1.last_update_timestamp, 100) def test_merge_with_empty(self): - observer1 = ObserverAggregator() - observer2 = ObserverAggregator() + observer1 = ValueObserverAggregator() + observer2 = ValueObserverAggregator() mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) - checkpoint1 = ObserverAggregator._TYPE(*(mmsc_checkpoint1 + (23,))) + checkpoint1 = ValueObserverAggregator._TYPE( + *(mmsc_checkpoint1 + (23,)) + ) observer1.mmsc.checkpoint = mmsc_checkpoint1 observer1.checkpoint = checkpoint1 diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 9d5d2b15d8..4c2d691549 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -88,7 +88,7 @@ def callback(observer): self.assertIsInstance(observer, metrics_api.Observer) observer.observe(45, {}) - observer = metrics.Observer( + observer = metrics.ValueObserver( callback, "name", "desc", "unit", int, meter, (), True ) @@ -165,7 +165,7 @@ def test_register_observer(self): callback = mock.Mock() observer = meter.register_observer( - callback, "name", "desc", "unit", int, (), True + callback, "name", "desc", "unit", int, metrics.ValueObserver ) self.assertIsInstance(observer, metrics_api.Observer) @@ -185,7 +185,7 @@ def test_unregister_observer(self): callback = mock.Mock() observer = meter.register_observer( - callback, "name", "desc", "unit", int, (), True + callback, "name", "desc", "unit", int, metrics.ValueObserver ) meter.unregister_observer(observer) @@ -290,10 +290,10 @@ def test_record(self): ) -class TestObserver(unittest.TestCase): +class TestValueObserver(unittest.TestCase): def test_observe(self): meter = metrics.MeterProvider().get_meter(__name__) - observer = metrics.Observer( + observer = metrics.ValueObserver( None, "name", "desc", "unit", int, meter, ("key",), True ) labels = {"key": "value"} @@ -310,7 +310,7 @@ def test_observe(self): def test_observe_disabled(self): meter = metrics.MeterProvider().get_meter(__name__) - observer = metrics.Observer( + observer = metrics.ValueObserver( None, "name", "desc", "unit", int, meter, ("key",), False ) labels = {"key": "value"} @@ -320,7 +320,7 @@ def test_observe_disabled(self): @mock.patch("opentelemetry.sdk.metrics.logger") def test_observe_incorrect_type(self, logger_mock): meter = metrics.MeterProvider().get_meter(__name__) - observer = metrics.Observer( + observer = metrics.ValueObserver( None, "name", "desc", "unit", int, meter, ("key",), True ) labels = {"key": "value"} @@ -332,7 +332,7 @@ def test_run(self): meter = metrics.MeterProvider().get_meter(__name__) callback = mock.Mock() - observer = metrics.Observer( + observer = metrics.ValueObserver( callback, "name", "desc", "unit", int, meter, (), True ) @@ -346,7 +346,7 @@ def test_run_exception(self, logger_mock): callback = mock.Mock() callback.side_effect = Exception("We have a problem!") - observer = metrics.Observer( + observer = metrics.ValueObserver( callback, "name", "desc", "unit", int, meter, (), True ) From 7ad8bbc45b5dee9838b61a640d80321b7ee3a57c Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 8 Jun 2020 14:41:18 -0700 Subject: [PATCH 0389/1517] Add lzchen to maintainers (#784) * Add lzchen to maintainers Closes #778. Co-authored-by: alrex --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 613b4e6acb..63cba3eb3b 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,6 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Chris Kleinknecht](https://github.com/c24t), Google - [Diego Hurtado](https://github.com/ocelotl) - [Hector Hernandez](https://github.com/hectorhdzg), Microsoft -- [Leighton Chen](https://github.com/lzchen), Microsoft - [Mauricio Vásquez](https://github.com/mauriciovasquezbernal), Kinvolk - [Reiley Yang](https://github.com/reyang), Microsoft @@ -116,16 +115,17 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-telemetry/teams/python-maintainers)): - [Alex Boten](https://github.com/codeboten), LightStep +- [Leighton Chen](https://github.com/lzchen), Microsoft - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Zillow Group +*Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).* + ### Thanks to all the people who already contributed! -*Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).* - ## Release Schedule OpenTelemetry Python is under active development. From 1041e1152e729f6da064cf56653ec6bcf47443f3 Mon Sep 17 00:00:00 2001 From: Connor Adams Date: Mon, 8 Jun 2020 18:52:38 -0400 Subject: [PATCH 0390/1517] refactor: Add common utils to opentelemetry-auto-instrumentation, rename opentelemetry-auto-instrumentation (#741) --- .../auto_instrumentation.rst | 15 ----- docs/auto_instrumentation/instrumentor.rst | 7 -- docs/conf.py | 2 +- docs/examples/auto-instrumentation/README.rst | 4 +- docs/examples/datadog_exporter/README.rst | 8 +-- docs/index.rst | 2 +- docs/instrumentation/instrumentation.rst | 15 +++++ docs/instrumentation/instrumentor.rst | 7 ++ eachdist.ini | 2 +- .../setup.cfg | 1 + .../ext/aiohttp_client/__init__.py | 29 +------- .../tests/test_aiohttp_client_integration.py | 37 ---------- ext/opentelemetry-ext-boto/setup.cfg | 2 +- .../src/opentelemetry/ext/boto/__init__.py | 4 +- ext/opentelemetry-ext-dbapi/setup.cfg | 1 + .../src/opentelemetry/ext/dbapi/__init__.py | 5 +- ext/opentelemetry-ext-django/setup.cfg | 2 +- .../src/opentelemetry/ext/django/__init__.py | 2 +- ext/opentelemetry-ext-flask/setup.cfg | 2 +- .../src/opentelemetry/ext/flask/__init__.py | 2 +- ext/opentelemetry-ext-jinja2/setup.cfg | 2 +- .../src/opentelemetry/ext/jinja2/__init__.py | 17 ++--- ext/opentelemetry-ext-mysql/setup.cfg | 2 +- .../src/opentelemetry/ext/mysql/__init__.py | 2 +- ext/opentelemetry-ext-psycopg2/setup.cfg | 2 +- .../opentelemetry/ext/psycopg2/__init__.py | 2 +- ext/opentelemetry-ext-pymongo/setup.cfg | 2 +- .../src/opentelemetry/ext/pymongo/__init__.py | 2 +- ext/opentelemetry-ext-pymysql/setup.cfg | 2 +- .../src/opentelemetry/ext/pymysql/__init__.py | 2 +- ext/opentelemetry-ext-redis/setup.cfg | 2 +- .../src/opentelemetry/ext/redis/__init__.py | 29 ++++---- ext/opentelemetry-ext-requests/setup.cfg | 2 +- .../opentelemetry/ext/requests/__init__.py | 38 ++--------- ext/opentelemetry-ext-sqlalchemy/setup.cfg | 2 +- .../opentelemetry/ext/sqlalchemy/__init__.py | 19 ++---- ext/opentelemetry-ext-sqlite3/setup.cfg | 2 +- .../src/opentelemetry/ext/sqlite3/__init__.py | 2 +- ext/opentelemetry-ext-wsgi/setup.cfg | 1 + .../src/opentelemetry/ext/wsgi/__init__.py | 32 +-------- opentelemetry-auto-instrumentation/README.rst | 19 ------ .../CHANGELOG.md | 3 + .../MANIFEST.in | 0 opentelemetry-instrumentation/README.rst | 19 ++++++ .../setup.cfg | 14 ++-- .../setup.py | 2 +- .../instrumentation}/__init__.py | 4 +- .../instrumentation}/auto_instrumentation.py | 0 .../instrumentation}/bootstrap.py | 0 .../instrumentation}/instrumentor.py | 6 +- .../instrumentation}/sitecustomize.py | 0 .../opentelemetry/instrumentation/utils.py | 67 +++++++++++++++++++ .../opentelemetry/instrumentation}/version.py | 0 .../tests/__init__.py | 0 .../tests/test_bootstrap.py | 12 ++-- .../tests/test_instrumentor.py | 2 +- .../tests/test_run.py | 14 ++-- .../tests/test_utils.py | 56 ++++++++++++++++ scripts/build.sh | 2 +- tox.ini | 41 ++++++------ 60 files changed, 279 insertions(+), 295 deletions(-) delete mode 100644 docs/auto_instrumentation/auto_instrumentation.rst delete mode 100644 docs/auto_instrumentation/instrumentor.rst create mode 100644 docs/instrumentation/instrumentation.rst create mode 100644 docs/instrumentation/instrumentor.rst delete mode 100644 opentelemetry-auto-instrumentation/README.rst rename {opentelemetry-auto-instrumentation => opentelemetry-instrumentation}/CHANGELOG.md (78%) rename {opentelemetry-auto-instrumentation => opentelemetry-instrumentation}/MANIFEST.in (100%) create mode 100644 opentelemetry-instrumentation/README.rst rename {opentelemetry-auto-instrumentation => opentelemetry-instrumentation}/setup.cfg (78%) rename {opentelemetry-auto-instrumentation => opentelemetry-instrumentation}/setup.py (91%) rename {opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation => opentelemetry-instrumentation/src/opentelemetry/instrumentation}/__init__.py (93%) rename {opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation => opentelemetry-instrumentation/src/opentelemetry/instrumentation}/auto_instrumentation.py (100%) rename {opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation => opentelemetry-instrumentation/src/opentelemetry/instrumentation}/bootstrap.py (100%) rename {opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation => opentelemetry-instrumentation/src/opentelemetry/instrumentation}/instrumentor.py (92%) rename {opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation => opentelemetry-instrumentation/src/opentelemetry/instrumentation}/sitecustomize.py (100%) create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py rename {opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation => opentelemetry-instrumentation/src/opentelemetry/instrumentation}/version.py (100%) rename {opentelemetry-auto-instrumentation => opentelemetry-instrumentation}/tests/__init__.py (100%) rename {opentelemetry-auto-instrumentation => opentelemetry-instrumentation}/tests/test_bootstrap.py (89%) rename {opentelemetry-auto-instrumentation => opentelemetry-instrumentation}/tests/test_instrumentor.py (95%) rename {opentelemetry-auto-instrumentation => opentelemetry-instrumentation}/tests/test_run.py (84%) create mode 100644 opentelemetry-instrumentation/tests/test_utils.py diff --git a/docs/auto_instrumentation/auto_instrumentation.rst b/docs/auto_instrumentation/auto_instrumentation.rst deleted file mode 100644 index c1fd0eff2a..0000000000 --- a/docs/auto_instrumentation/auto_instrumentation.rst +++ /dev/null @@ -1,15 +0,0 @@ -OpenTelemetry Python Autoinstrumentation -======================================== - -.. automodule:: opentelemetry.auto_instrumentation - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 1 - - instrumentor diff --git a/docs/auto_instrumentation/instrumentor.rst b/docs/auto_instrumentation/instrumentor.rst deleted file mode 100644 index c94c0237f5..0000000000 --- a/docs/auto_instrumentation/instrumentor.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.auto_instrumentation.instrumentor package -======================================================= - -.. automodule:: opentelemetry.auto_instrumentation.instrumentor - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py index 42fcf29f4f..74ae754c60 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ source_dirs = [ os.path.abspath("../opentelemetry-api/src/"), os.path.abspath("../opentelemetry-sdk/src/"), - os.path.abspath("../opentelemetry-auto-instrumentation/src/"), + os.path.abspath("../opentelemetry-instrumentation/src/"), ] ext = "../ext" diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index 6592ecb958..b5a7649503 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -72,7 +72,7 @@ Installation .. code:: sh $ pip install opentelemetry-sdk - $ pip install opentelemetry-auto-instrumentation + $ pip install opentelemetry-instrumentation $ pip install opentelemetry-ext-flask $ pip install requests @@ -138,7 +138,7 @@ and run this instead: .. code:: sh - $ opentelemetry-auto-instrumentation python server_uninstrumented.py + $ opentelemetry-instrument python server_uninstrumented.py In the console where you previously executed ``client.py``, run again this again: diff --git a/docs/examples/datadog_exporter/README.rst b/docs/examples/datadog_exporter/README.rst index 961ac9ca05..40c12aeccf 100644 --- a/docs/examples/datadog_exporter/README.rst +++ b/docs/examples/datadog_exporter/README.rst @@ -44,7 +44,7 @@ Auto-Instrumention Example pip install opentelemetry-api pip install opentelemetry-sdk pip install opentelemetry-ext-datadog - pip install opentelemetry-auto-instrumentation + pip install opentelemetry-instrumentation pip install opentelemetry-ext-flask pip install flask pip install requests @@ -66,16 +66,16 @@ Auto-Instrumention Example .. code-block:: sh - opentelemetry-auto-instrumentation python server.py + opentelemetry-instrument python server.py * Run client .. code-block:: sh - opentelemetry-auto-instrumentation python client.py testing + opentelemetry-instrument python client.py testing * Run client with parameter to raise error .. code-block:: sh - opentelemetry-auto-instrumentation python client.py error + opentelemetry-instrument python client.py error diff --git a/docs/index.rst b/docs/index.rst index 2d26e24f83..b25efa8acc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -68,7 +68,7 @@ install api/api sdk/sdk - auto_instrumentation/auto_instrumentation + instrumentation/instrumentation .. toctree:: :maxdepth: 2 diff --git a/docs/instrumentation/instrumentation.rst b/docs/instrumentation/instrumentation.rst new file mode 100644 index 0000000000..bcb85043f4 --- /dev/null +++ b/docs/instrumentation/instrumentation.rst @@ -0,0 +1,15 @@ +OpenTelemetry Python Instrumentation +==================================== + +.. automodule:: opentelemetry.instrumentation + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 1 + + instrumentor diff --git a/docs/instrumentation/instrumentor.rst b/docs/instrumentation/instrumentor.rst new file mode 100644 index 0000000000..5c0c010ff6 --- /dev/null +++ b/docs/instrumentation/instrumentor.rst @@ -0,0 +1,7 @@ +opentelemetry.instrumentation.instrumentor package +================================================== + +.. automodule:: opentelemetry.instrumentation.instrumentor + :members: + :undoc-members: + :show-inheritance: diff --git a/eachdist.ini b/eachdist.ini index b573b838fd..f88cb1b180 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -4,7 +4,7 @@ sortfirst= opentelemetry-api opentelemetry-sdk - opentelemetry-auto-instrumentation + opentelemetry-instrumentation ext/opentelemetry-ext-wsgi ext/opentelemetry-ext-dbapi ext/* diff --git a/ext/opentelemetry-ext-aiohttp-client/setup.cfg b/ext/opentelemetry-ext-aiohttp-client/setup.cfg index af4839fc3d..e2a6493922 100644 --- a/ext/opentelemetry-ext-aiohttp-client/setup.cfg +++ b/ext/opentelemetry-ext-aiohttp-client/setup.cfg @@ -40,6 +40,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api >= 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 aiohttp ~= 3.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py index 77dadbd645..bc6c361f9b 100644 --- a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py +++ b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py @@ -52,38 +52,11 @@ def strip_query_params(url: yarl.URL) -> str: from opentelemetry import context as context_api from opentelemetry import propagators, trace from opentelemetry.ext.aiohttp_client.version import __version__ +from opentelemetry.instrumentation.utils import http_status_to_canonical_code from opentelemetry.trace import SpanKind from opentelemetry.trace.status import Status, StatusCanonicalCode -# TODO: refactor this code to some common utility -def http_status_to_canonical_code(status: int) -> StatusCanonicalCode: - # pylint:disable=too-many-branches,too-many-return-statements - if status < 100: - return StatusCanonicalCode.UNKNOWN - if status <= 399: - return StatusCanonicalCode.OK - if status <= 499: - if status == 401: # HTTPStatus.UNAUTHORIZED: - return StatusCanonicalCode.UNAUTHENTICATED - if status == 403: # HTTPStatus.FORBIDDEN: - return StatusCanonicalCode.PERMISSION_DENIED - if status == 404: # HTTPStatus.NOT_FOUND: - return StatusCanonicalCode.NOT_FOUND - if status == 429: # HTTPStatus.TOO_MANY_REQUESTS: - return StatusCanonicalCode.RESOURCE_EXHAUSTED - return StatusCanonicalCode.INVALID_ARGUMENT - if status <= 599: - if status == 501: # HTTPStatus.NOT_IMPLEMENTED: - return StatusCanonicalCode.UNIMPLEMENTED - if status == 503: # HTTPStatus.SERVICE_UNAVAILABLE: - return StatusCanonicalCode.UNAVAILABLE - if status == 504: # HTTPStatus.GATEWAY_TIMEOUT: - return StatusCanonicalCode.DEADLINE_EXCEEDED - return StatusCanonicalCode.INTERNAL - return StatusCanonicalCode.UNKNOWN - - def url_path_span_name(params: aiohttp.TraceRequestStartParams) -> str: """Extract a span name from the request URL path. diff --git a/ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py b/ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py index 8cf5048e58..8db20c227e 100644 --- a/ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -98,43 +98,6 @@ async def default_handler(request): loop = asyncio.get_event_loop() return loop.run_until_complete(do_request()) - def test_http_status_to_canonical_code(self): - for status_code, expected in ( - (HTTPStatus.OK, StatusCanonicalCode.OK), - (HTTPStatus.ACCEPTED, StatusCanonicalCode.OK), - (HTTPStatus.IM_USED, StatusCanonicalCode.OK), - (HTTPStatus.MULTIPLE_CHOICES, StatusCanonicalCode.OK), - (HTTPStatus.BAD_REQUEST, StatusCanonicalCode.INVALID_ARGUMENT), - (HTTPStatus.UNAUTHORIZED, StatusCanonicalCode.UNAUTHENTICATED), - (HTTPStatus.FORBIDDEN, StatusCanonicalCode.PERMISSION_DENIED), - (HTTPStatus.NOT_FOUND, StatusCanonicalCode.NOT_FOUND), - ( - HTTPStatus.UNPROCESSABLE_ENTITY, - StatusCanonicalCode.INVALID_ARGUMENT, - ), - ( - HTTPStatus.TOO_MANY_REQUESTS, - StatusCanonicalCode.RESOURCE_EXHAUSTED, - ), - (HTTPStatus.NOT_IMPLEMENTED, StatusCanonicalCode.UNIMPLEMENTED), - (HTTPStatus.SERVICE_UNAVAILABLE, StatusCanonicalCode.UNAVAILABLE), - ( - HTTPStatus.GATEWAY_TIMEOUT, - StatusCanonicalCode.DEADLINE_EXCEEDED, - ), - ( - HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, - StatusCanonicalCode.INTERNAL, - ), - (600, StatusCanonicalCode.UNKNOWN), - (99, StatusCanonicalCode.UNKNOWN), - ): - with self.subTest(status_code=status_code): - actual = opentelemetry.ext.aiohttp_client.http_status_to_canonical_code( - int(status_code) - ) - self.assertEqual(actual, expected, status_code) - def test_status_codes(self): for status_code, span_status in ( (HTTPStatus.OK, StatusCanonicalCode.OK), diff --git a/ext/opentelemetry-ext-boto/setup.cfg b/ext/opentelemetry-ext-boto/setup.cfg index 529e79be99..d66b59875f 100644 --- a/ext/opentelemetry-ext-boto/setup.cfg +++ b/ext/opentelemetry-ext-boto/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: install_requires = boto ~= 2.0 opentelemetry-api == 0.9.dev0 - opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 [options.extras_require] test = diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py index fa66fda61d..97d4922658 100644 --- a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py @@ -15,7 +15,7 @@ Instrument `Boto`_ to trace service requests. There are two options for instrumenting code. The first option is to use the -``opentelemetry-auto-instrumentation`` executable which will automatically +``opentelemetry-instrument`` executable which will automatically instrument your Boto client. The second is to programmatically enable instrumentation via the following code: @@ -50,8 +50,8 @@ from boto.connection import AWSAuthConnection, AWSQueryConnection from wrapt import ObjectProxy, wrap_function_wrapper -from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.boto.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.trace import SpanKind, get_tracer logger = logging.getLogger(__name__) diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index e352201bb6..680e6c819c 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -41,6 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index a5fda62dea..6b81bedf03 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -47,6 +47,7 @@ import wrapt from opentelemetry.ext.dbapi.version import __version__ +from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import SpanKind, Tracer, TracerProvider, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode @@ -141,9 +142,7 @@ def unwrap_connect( connect_module: Module name where the connect method is available. connect_method_name: The connect method name. """ - conn = getattr(connect_module, connect_method_name, None) - if isinstance(conn, wrapt.ObjectProxy): - setattr(connect_module, connect_method_name, conn.__wrapped__) + unwrap(connect_module, connect_method_name) def instrument_connection( diff --git a/ext/opentelemetry-ext-django/setup.cfg b/ext/opentelemetry-ext-django/setup.cfg index bf72f9d981..e590cdf9a8 100644 --- a/ext/opentelemetry-ext-django/setup.cfg +++ b/ext/opentelemetry-ext-django/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: install_requires = django >= 1.10 opentelemetry-ext-wsgi == 0.9.dev0 - opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 opentelemetry-api == 0.9.dev0 [options.extras_require] diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py index d4e78351aa..3eb3b2dd72 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py @@ -16,9 +16,9 @@ from django.conf import settings -from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.configuration import Configuration from opentelemetry.ext.django.middleware import _DjangoMiddleware +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor _logger = getLogger(__name__) diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index 3258717419..57ee7b5ed9 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: install_requires = flask ~= 1.0 opentelemetry-ext-wsgi == 0.9.dev0 - opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 opentelemetry-api == 0.9.dev0 [options.extras_require] diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index cd94dc7e47..20d6501a93 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -53,8 +53,8 @@ def hello(): import opentelemetry.ext.wsgi as otel_wsgi from opentelemetry import configuration, context, propagators, trace -from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.flask.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.util import disable_trace, time_ns _logger = getLogger(__name__) diff --git a/ext/opentelemetry-ext-jinja2/setup.cfg b/ext/opentelemetry-ext-jinja2/setup.cfg index a9428aec60..7179cdd931 100644 --- a/ext/opentelemetry-ext-jinja2/setup.cfg +++ b/ext/opentelemetry-ext-jinja2/setup.cfg @@ -40,7 +40,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.9.dev0 - opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/__init__.py b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/__init__.py index 4aabb832ba..06be28873b 100644 --- a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/__init__.py +++ b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/__init__.py @@ -48,8 +48,9 @@ from wrapt import ObjectProxy from wrapt import wrap_function_wrapper as _wrap -from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.jinja2.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import SpanKind, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode @@ -115,12 +116,6 @@ def _wrap_load_template(tracer, wrapped, _, args, kwargs): ) -def _unwrap(obj, attr): - func = getattr(obj, attr, None) - if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"): - setattr(obj, attr, func.__wrapped__) - - class Jinja2Instrumentor(BaseInstrumentor): """An instrumentor for jinja2 @@ -141,7 +136,7 @@ def _instrument(self, **kwargs): ) def _uninstrument(self, **kwargs): - _unwrap(jinja2.Template, "render") - _unwrap(jinja2.Template, "generate") - _unwrap(jinja2.Environment, "compile") - _unwrap(jinja2.Environment, "_load_template") + unwrap(jinja2.Template, "render") + unwrap(jinja2.Template, "generate") + unwrap(jinja2.Environment, "compile") + unwrap(jinja2.Environment, "_load_template") diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index afe53a651f..95fe29b864 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: install_requires = opentelemetry-api == 0.9.dev0 opentelemetry-ext-dbapi == 0.9.dev0 - opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index a70c1d46ea..daf90c4bbf 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -46,9 +46,9 @@ import mysql.connector -from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext import dbapi from opentelemetry.ext.mysql.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.trace import TracerProvider, get_tracer diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index 5de40eac26..ec4852a15b 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: install_requires = opentelemetry-api == 0.9.dev0 opentelemetry-ext-dbapi == 0.9.dev0 - opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py index c82343f4de..871356ab2c 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py @@ -47,9 +47,9 @@ import psycopg2 import wrapt -from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext import dbapi from opentelemetry.ext.psycopg2.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.trace import TracerProvider, get_tracer diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index ba1ea4026a..39d4849297 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.9.dev0 - opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 pymongo ~= 3.1 [options.extras_require] diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index ebe52ed5f1..fa8a599539 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -43,8 +43,8 @@ from pymongo import monitoring from opentelemetry import trace -from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.pymongo.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.trace import SpanKind, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode diff --git a/ext/opentelemetry-ext-pymysql/setup.cfg b/ext/opentelemetry-ext-pymysql/setup.cfg index f08e4d3ee1..13a0c7e350 100644 --- a/ext/opentelemetry-ext-pymysql/setup.cfg +++ b/ext/opentelemetry-ext-pymysql/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: install_requires = opentelemetry-api == 0.9.dev0 opentelemetry-ext-dbapi == 0.9.dev0 - opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 PyMySQL ~= 0.9.3 [options.extras_require] diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py index 14a320fdd0..b4d75543c6 100644 --- a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py @@ -47,9 +47,9 @@ import pymysql -from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext import dbapi from opentelemetry.ext.pymysql.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.trace import TracerProvider, get_tracer diff --git a/ext/opentelemetry-ext-redis/setup.cfg b/ext/opentelemetry-ext-redis/setup.cfg index 2694d85d52..fab8cbd8f3 100644 --- a/ext/opentelemetry-ext-redis/setup.cfg +++ b/ext/opentelemetry-ext-redis/setup.cfg @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.9.dev0 - opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 redis >= 2.6 wrapt >= 1.12.1 diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/__init__.py b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/__init__.py index e653936ca5..0b7f6e28eb 100644 --- a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/__init__.py +++ b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/__init__.py @@ -16,7 +16,7 @@ Instrument `redis`_ to report Redis queries. There are two options for instrumenting code. The first option is to use the -``opentelemetry-auto-instrumentation`` executable which will automatically +``opentelemetry-instrumentation`` executable which will automatically instrument your Redis client. The second is to programmatically enable instrumentation via the following code: @@ -49,12 +49,13 @@ from wrapt import ObjectProxy, wrap_function_wrapper from opentelemetry import trace -from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.redis.util import ( _extract_conn_attributes, _format_command_args, ) from opentelemetry.ext.redis.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap _DEFAULT_SERVICE = "redis" _RAWCMD = "db.statement" @@ -68,12 +69,6 @@ def _set_connection_attributes(span, conn): span.set_attribute(key, value) -def _unwrap(obj, attr): - func = getattr(obj, attr, None) - if isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"): - setattr(obj, attr, func.__wrapped__) - - def _traced_execute_command(func, instance, args, kwargs): tracer = getattr(redis, "_opentelemetry_tracer") query = _format_command_args(args) @@ -145,19 +140,19 @@ def _instrument(self, **kwargs): def _uninstrument(self, **kwargs): if redis.VERSION < (3, 0, 0): - _unwrap(redis.StrictRedis, "execute_command") - _unwrap(redis.StrictRedis, "pipeline") - _unwrap(redis.Redis, "pipeline") - _unwrap( + unwrap(redis.StrictRedis, "execute_command") + unwrap(redis.StrictRedis, "pipeline") + unwrap(redis.Redis, "pipeline") + unwrap( redis.client.BasePipeline, # pylint:disable=no-member "execute", ) - _unwrap( + unwrap( redis.client.BasePipeline, # pylint:disable=no-member "immediate_execute_command", ) else: - _unwrap(redis.Redis, "execute_command") - _unwrap(redis.Redis, "pipeline") - _unwrap(redis.client.Pipeline, "execute") - _unwrap(redis.client.Pipeline, "immediate_execute_command") + unwrap(redis.Redis, "execute_command") + unwrap(redis.Redis, "pipeline") + unwrap(redis.client.Pipeline, "execute") + unwrap(redis.client.Pipeline, "immediate_execute_command") diff --git a/ext/opentelemetry-ext-requests/setup.cfg b/ext/opentelemetry-ext-requests/setup.cfg index ca5dc5ceba..8a02b74662 100644 --- a/ext/opentelemetry-ext-requests/setup.cfg +++ b/ext/opentelemetry-ext-requests/setup.cfg @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.9.dev0 - opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 requests ~= 2.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py index 65bc21370f..ee4796f168 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py @@ -50,9 +50,10 @@ from requests.sessions import Session from opentelemetry import context, propagators, trace -from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.requests.version import __version__ -from opentelemetry.trace import SpanKind +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import http_status_to_canonical_code +from opentelemetry.trace import SpanKind, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode @@ -115,7 +116,7 @@ def instrumented_request(self, method, url, *args, **kwargs): span.set_attribute("http.status_code", result.status_code) span.set_attribute("http.status_text", result.reason) span.set_status( - Status(_http_status_to_canonical_code(result.status_code)) + Status(http_status_to_canonical_code(result.status_code)) ) if span_callback is not None: @@ -146,37 +147,6 @@ def _uninstrument(): Session.request = original -def _http_status_to_canonical_code(code: int, allow_redirect: bool = True): - # pylint:disable=too-many-branches,too-many-return-statements - if code < 100: - return StatusCanonicalCode.UNKNOWN - if code <= 299: - return StatusCanonicalCode.OK - if code <= 399: - if allow_redirect: - return StatusCanonicalCode.OK - return StatusCanonicalCode.DEADLINE_EXCEEDED - if code <= 499: - if code == 401: # HTTPStatus.UNAUTHORIZED: - return StatusCanonicalCode.UNAUTHENTICATED - if code == 403: # HTTPStatus.FORBIDDEN: - return StatusCanonicalCode.PERMISSION_DENIED - if code == 404: # HTTPStatus.NOT_FOUND: - return StatusCanonicalCode.NOT_FOUND - if code == 429: # HTTPStatus.TOO_MANY_REQUESTS: - return StatusCanonicalCode.RESOURCE_EXHAUSTED - return StatusCanonicalCode.INVALID_ARGUMENT - if code <= 599: - if code == 501: # HTTPStatus.NOT_IMPLEMENTED: - return StatusCanonicalCode.UNIMPLEMENTED - if code == 503: # HTTPStatus.SERVICE_UNAVAILABLE: - return StatusCanonicalCode.UNAVAILABLE - if code == 504: # HTTPStatus.GATEWAY_TIMEOUT: - return StatusCanonicalCode.DEADLINE_EXCEEDED - return StatusCanonicalCode.INTERNAL - return StatusCanonicalCode.UNKNOWN - - def _exception_to_canonical_code(exc: Exception) -> StatusCanonicalCode: if isinstance( exc, diff --git a/ext/opentelemetry-ext-sqlalchemy/setup.cfg b/ext/opentelemetry-ext-sqlalchemy/setup.cfg index dfbf655e6d..851adddb76 100644 --- a/ext/opentelemetry-ext-sqlalchemy/setup.cfg +++ b/ext/opentelemetry-ext-sqlalchemy/setup.cfg @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.9.dev0 - opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 wrapt >= 1.11.2 sqlalchemy diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py index 05b384429b..05d5e625ef 100644 --- a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py +++ b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py @@ -16,7 +16,7 @@ Instrument `sqlalchemy`_ to report SQL queries. There are two options for instrumenting code. The first option is to use -the ``opentelemetry-auto-instrumentation`` executable which will automatically +the ``opentelemetry-instrument`` executable which will automatically instrument your SQLAlchemy engine. The second is to programmatically enable instrumentation via the following code: @@ -47,22 +47,13 @@ import wrapt from wrapt import wrap_function_wrapper as _w -from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.sqlalchemy.engine import ( EngineTracer, _get_tracer, _wrap_create_engine, ) - - -def _unwrap(obj, attr): - func = getattr(obj, attr, None) - if ( - func - and isinstance(func, wrapt.ObjectProxy) - and hasattr(func, "__wrapped__") - ): - setattr(obj, attr, func.__wrapped__) +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap class SQLAlchemyInstrumentor(BaseInstrumentor): @@ -96,5 +87,5 @@ def _instrument(self, **kwargs): return None def _uninstrument(self, **kwargs): - _unwrap(sqlalchemy, "create_engine") - _unwrap(sqlalchemy.engine, "create_engine") + unwrap(sqlalchemy, "create_engine") + unwrap(sqlalchemy.engine, "create_engine") diff --git a/ext/opentelemetry-ext-sqlite3/setup.cfg b/ext/opentelemetry-ext-sqlite3/setup.cfg index b32dd1cf68..d5bf161f55 100644 --- a/ext/opentelemetry-ext-sqlite3/setup.cfg +++ b/ext/opentelemetry-ext-sqlite3/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: install_requires = opentelemetry-api == 0.9.dev0 opentelemetry-ext-dbapi == 0.9.dev0 - opentelemetry-auto-instrumentation == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py index 5c9a009360..174fbba116 100644 --- a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py +++ b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py @@ -45,9 +45,9 @@ import sqlite3 import typing -from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext import dbapi from opentelemetry.ext.sqlite3.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.trace import TracerProvider, get_tracer diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index 288ebcaa64..270f278717 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -41,6 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 [options.extras_require] test = diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 61e8126c19..0895bc75b0 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -61,6 +61,7 @@ def hello(): from opentelemetry import context, propagators, trace from opentelemetry.ext.wsgi.version import __version__ +from opentelemetry.instrumentation.utils import http_status_to_canonical_code from opentelemetry.trace.propagation import get_span_from_context from opentelemetry.trace.status import Status, StatusCanonicalCode @@ -87,37 +88,6 @@ def setifnotnone(dic, key, value): dic[key] = value -def http_status_to_canonical_code(code: int, allow_redirect: bool = True): - # pylint:disable=too-many-branches,too-many-return-statements - if code < 100: - return StatusCanonicalCode.UNKNOWN - if code <= 299: - return StatusCanonicalCode.OK - if code <= 399: - if allow_redirect: - return StatusCanonicalCode.OK - return StatusCanonicalCode.DEADLINE_EXCEEDED - if code <= 499: - if code == 401: # HTTPStatus.UNAUTHORIZED: - return StatusCanonicalCode.UNAUTHENTICATED - if code == 403: # HTTPStatus.FORBIDDEN: - return StatusCanonicalCode.PERMISSION_DENIED - if code == 404: # HTTPStatus.NOT_FOUND: - return StatusCanonicalCode.NOT_FOUND - if code == 429: # HTTPStatus.TOO_MANY_REQUESTS: - return StatusCanonicalCode.RESOURCE_EXHAUSTED - return StatusCanonicalCode.INVALID_ARGUMENT - if code <= 599: - if code == 501: # HTTPStatus.NOT_IMPLEMENTED: - return StatusCanonicalCode.UNIMPLEMENTED - if code == 503: # HTTPStatus.SERVICE_UNAVAILABLE: - return StatusCanonicalCode.UNAVAILABLE - if code == 504: # HTTPStatus.GATEWAY_TIMEOUT: - return StatusCanonicalCode.DEADLINE_EXCEEDED - return StatusCanonicalCode.INTERNAL - return StatusCanonicalCode.UNKNOWN - - def collect_request_attributes(environ): """Collects HTTP request attributes from the PEP3333-conforming WSGI environ and returns a dictionary to be used as span creation attributes.""" diff --git a/opentelemetry-auto-instrumentation/README.rst b/opentelemetry-auto-instrumentation/README.rst deleted file mode 100644 index b153072ae5..0000000000 --- a/opentelemetry-auto-instrumentation/README.rst +++ /dev/null @@ -1,19 +0,0 @@ -OpenTelemetry Auto Instrumentation -================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-auto-instrumentation.svg - :target: https://pypi.org/project/opentelemetry-auto-instrumentation/ - -Installation ------------- - -:: - - pip install opentelemetry-auto-instrumentation - -References ----------- - -* `OpenTelemetry Project `_ diff --git a/opentelemetry-auto-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md similarity index 78% rename from opentelemetry-auto-instrumentation/CHANGELOG.md rename to opentelemetry-instrumentation/CHANGELOG.md index d2521d157c..b3d117db06 100644 --- a/opentelemetry-auto-instrumentation/CHANGELOG.md +++ b/opentelemetry-instrumentation/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Rename opentelemetry-auto-instrumentation to opentelemetry-instrumentation, + and console script `opentelemetry-auto-instrumentation` to `opentelemetry-instrument` + ## 0.8b0 Released 2020-05-27 diff --git a/opentelemetry-auto-instrumentation/MANIFEST.in b/opentelemetry-instrumentation/MANIFEST.in similarity index 100% rename from opentelemetry-auto-instrumentation/MANIFEST.in rename to opentelemetry-instrumentation/MANIFEST.in diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst new file mode 100644 index 0000000000..d52ed037b7 --- /dev/null +++ b/opentelemetry-instrumentation/README.rst @@ -0,0 +1,19 @@ +OpenTelemetry Instrumentation +============================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation.svg + :target: https://pypi.org/project/opentelemetry-instrumentation/ + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/opentelemetry-auto-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg similarity index 78% rename from opentelemetry-auto-instrumentation/setup.cfg rename to opentelemetry-instrumentation/setup.cfg index 0e9207922b..119beeec11 100644 --- a/opentelemetry-auto-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-auto-instrumentation -description = Auto Instrumentation for OpenTelemetry Python +name = opentelemetry-instrumentation +description = Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-auto-instrumentation +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-instrumentation platforms = any license = Apache-2.0 classifiers = @@ -41,12 +41,14 @@ package_dir= packages=find_namespace: zip_safe = False include_package_data = True -install_requires = opentelemetry-api == 0.9.dev0 +install_requires = + opentelemetry-api == 0.9.dev0 + wrapt >= 1.0.0, < 2.0.0 [options.packages.find] where = src [options.entry_points] console_scripts = - opentelemetry-auto-instrumentation = opentelemetry.auto_instrumentation.auto_instrumentation:run - opentelemetry-bootstrap = opentelemetry.auto_instrumentation.bootstrap:run + opentelemetry-instrument = opentelemetry.instrumentation.auto_instrumentation:run + opentelemetry-bootstrap = opentelemetry.instrumentation.bootstrap:run diff --git a/opentelemetry-auto-instrumentation/setup.py b/opentelemetry-instrumentation/setup.py similarity index 91% rename from opentelemetry-auto-instrumentation/setup.py rename to opentelemetry-instrumentation/setup.py index 86f8faedbc..fb3c8ff9f1 100644 --- a/opentelemetry-auto-instrumentation/setup.py +++ b/opentelemetry-instrumentation/setup.py @@ -18,7 +18,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "auto_instrumentation", "version.py" + BASE_DIR, "src", "opentelemetry", "instrumentation", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/__init__.py similarity index 93% rename from opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py rename to opentelemetry-instrumentation/src/opentelemetry/instrumentation/__init__.py index 590bac8b3c..149a9b46c9 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/__init__.py @@ -16,12 +16,12 @@ This package provides a couple of commands that help automatically instruments a program: -opentelemetry-auto-instrumentation +opentelemetry-instrument ----------------------------------- :: - opentelemetry-auto-instrumentation python program.py + opentelemetry-instrument python program.py The code in ``program.py`` needs to use one of the packages for which there is an OpenTelemetry integration. For a list of the available integrations please diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation.py similarity index 100% rename from opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py rename to opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation.py diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py similarity index 100% rename from opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/bootstrap.py rename to opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py similarity index 92% rename from opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py rename to opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py index dd58775bc7..ccd14d142a 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py @@ -28,7 +28,7 @@ class BaseInstrumentor(ABC): Child classes of this ABC should instrument specific third party libraries or frameworks either by using the - ``opentelemetry-auto-instrumentation`` command or by calling their methods + ``opentelemetry-instrument`` command or by calling their methods directly. Since every third party library or framework is different and has different @@ -58,7 +58,7 @@ def instrument(self, **kwargs): """Instrument the library This method will be called without any optional arguments by the - ``opentelemetry-auto-instrumentation`` command. The configuration of + ``opentelemetry-instrument`` command. The configuration of the instrumentation when done in this way should be done by previously setting the configuration (using environment variables or any other mechanism) that will be used later by the code in the ``instrument`` @@ -69,7 +69,7 @@ def instrument(self, **kwargs): This means that calling this method directly without passing any optional values should do the very same thing that the - ``opentelemetry-auto-instrumentation`` command does. This approach is + ``opentelemetry-instrument`` command does. This approach is followed because the ``Configuration`` object is immutable. """ diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/sitecustomize.py similarity index 100% rename from opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/sitecustomize.py rename to opentelemetry-instrumentation/src/opentelemetry/instrumentation/sitecustomize.py diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py new file mode 100644 index 0000000000..8553b1bc63 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -0,0 +1,67 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from wrapt import ObjectProxy + +from opentelemetry.trace.status import StatusCanonicalCode + + +def http_status_to_canonical_code( + status: int, allow_redirect: bool = True +) -> StatusCanonicalCode: + """Converts an HTTP status code to an OpenTelemetry canonical status code + + Args: + status (int): HTTP status code + """ + # pylint:disable=too-many-branches,too-many-return-statements + if status < 100: + return StatusCanonicalCode.UNKNOWN + if status <= 299: + return StatusCanonicalCode.OK + if status <= 399: + if allow_redirect: + return StatusCanonicalCode.OK + return StatusCanonicalCode.DEADLINE_EXCEEDED + if status <= 499: + if status == 401: # HTTPStatus.UNAUTHORIZED: + return StatusCanonicalCode.UNAUTHENTICATED + if status == 403: # HTTPStatus.FORBIDDEN: + return StatusCanonicalCode.PERMISSION_DENIED + if status == 404: # HTTPStatus.NOT_FOUND: + return StatusCanonicalCode.NOT_FOUND + if status == 429: # HTTPStatus.TOO_MANY_REQUESTS: + return StatusCanonicalCode.RESOURCE_EXHAUSTED + return StatusCanonicalCode.INVALID_ARGUMENT + if status <= 599: + if status == 501: # HTTPStatus.NOT_IMPLEMENTED: + return StatusCanonicalCode.UNIMPLEMENTED + if status == 503: # HTTPStatus.SERVICE_UNAVAILABLE: + return StatusCanonicalCode.UNAVAILABLE + if status == 504: # HTTPStatus.GATEWAY_TIMEOUT: + return StatusCanonicalCode.DEADLINE_EXCEEDED + return StatusCanonicalCode.INTERNAL + return StatusCanonicalCode.UNKNOWN + + +def unwrap(obj, attr: str): + """Given a function that was wrapped by wrapt.wrap_function_wrapper, unwrap it + + Args: + obj: Object that holds a reference to the wrapped function + attr (str): Name of the wrapped function + """ + func = getattr(obj, attr, None) + if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"): + setattr(obj, attr, func.__wrapped__) diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py similarity index 100% rename from opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py rename to opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py diff --git a/opentelemetry-auto-instrumentation/tests/__init__.py b/opentelemetry-instrumentation/tests/__init__.py similarity index 100% rename from opentelemetry-auto-instrumentation/tests/__init__.py rename to opentelemetry-instrumentation/tests/__init__.py diff --git a/opentelemetry-auto-instrumentation/tests/test_bootstrap.py b/opentelemetry-instrumentation/tests/test_bootstrap.py similarity index 89% rename from opentelemetry-auto-instrumentation/tests/test_bootstrap.py rename to opentelemetry-instrumentation/tests/test_bootstrap.py index 8af684084b..e5a1a86dda 100644 --- a/opentelemetry-auto-instrumentation/tests/test_bootstrap.py +++ b/opentelemetry-instrumentation/tests/test_bootstrap.py @@ -19,7 +19,7 @@ from unittest import TestCase from unittest.mock import call, patch -from opentelemetry.auto_instrumentation import bootstrap +from opentelemetry.instrumentation import bootstrap def sample_packages(packages, rate): @@ -45,7 +45,7 @@ def setUpClass(cls): ) cls.pkg_patcher = patch( - "opentelemetry.auto_instrumentation.bootstrap._find_installed_libraries", + "opentelemetry.instrumentation.bootstrap._find_installed_libraries", return_value=cls.installed_libraries, ) @@ -57,17 +57,17 @@ def setUpClass(cls): pip_freeze_output.append(inst) cls.pip_freeze_patcher = patch( - "opentelemetry.auto_instrumentation.bootstrap._sys_pip_freeze", + "opentelemetry.instrumentation.bootstrap._sys_pip_freeze", return_value="\n".join(pip_freeze_output), ) cls.pip_install_patcher = patch( - "opentelemetry.auto_instrumentation.bootstrap._sys_pip_install", + "opentelemetry.instrumentation.bootstrap._sys_pip_install", ) cls.pip_uninstall_patcher = patch( - "opentelemetry.auto_instrumentation.bootstrap._sys_pip_uninstall", + "opentelemetry.instrumentation.bootstrap._sys_pip_uninstall", ) cls.pip_check_patcher = patch( - "opentelemetry.auto_instrumentation.bootstrap._pip_check", + "opentelemetry.instrumentation.bootstrap._pip_check", ) cls.pkg_patcher.start() diff --git a/opentelemetry-auto-instrumentation/tests/test_instrumentor.py b/opentelemetry-instrumentation/tests/test_instrumentor.py similarity index 95% rename from opentelemetry-auto-instrumentation/tests/test_instrumentor.py rename to opentelemetry-instrumentation/tests/test_instrumentor.py index a768a40eb4..19104a3246 100644 --- a/opentelemetry-auto-instrumentation/tests/test_instrumentor.py +++ b/opentelemetry-instrumentation/tests/test_instrumentor.py @@ -16,7 +16,7 @@ from logging import WARNING from unittest import TestCase -from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor class TestInstrumentor(TestCase): diff --git a/opentelemetry-auto-instrumentation/tests/test_run.py b/opentelemetry-instrumentation/tests/test_run.py similarity index 84% rename from opentelemetry-auto-instrumentation/tests/test_run.py rename to opentelemetry-instrumentation/tests/test_run.py index 8b37882f5b..21f53babc6 100644 --- a/opentelemetry-auto-instrumentation/tests/test_run.py +++ b/opentelemetry-instrumentation/tests/test_run.py @@ -18,7 +18,7 @@ from unittest import TestCase from unittest.mock import patch -from opentelemetry.auto_instrumentation import auto_instrumentation +from opentelemetry.instrumentation import auto_instrumentation class TestRun(TestCase): @@ -27,13 +27,13 @@ class TestRun(TestCase): @classmethod def setUpClass(cls): cls.argv_patcher = patch( - "opentelemetry.auto_instrumentation.auto_instrumentation.argv" + "opentelemetry.instrumentation.auto_instrumentation.argv" ) cls.execl_patcher = patch( - "opentelemetry.auto_instrumentation.auto_instrumentation.execl" + "opentelemetry.instrumentation.auto_instrumentation.execl" ) cls.which_patcher = patch( - "opentelemetry.auto_instrumentation.auto_instrumentation.which" + "opentelemetry.instrumentation.auto_instrumentation.which" ) cls.argv_patcher.start() @@ -91,11 +91,11 @@ def test_single_path(self): class TestExecl(TestCase): @patch( - "opentelemetry.auto_instrumentation.auto_instrumentation.argv", + "opentelemetry.instrumentation.auto_instrumentation.argv", new=[1, 2, 3], ) - @patch("opentelemetry.auto_instrumentation.auto_instrumentation.which") - @patch("opentelemetry.auto_instrumentation.auto_instrumentation.execl") + @patch("opentelemetry.instrumentation.auto_instrumentation.which") + @patch("opentelemetry.instrumentation.auto_instrumentation.execl") def test_execl( self, mock_execl, mock_which ): # pylint: disable=no-self-use diff --git a/opentelemetry-instrumentation/tests/test_utils.py b/opentelemetry-instrumentation/tests/test_utils.py new file mode 100644 index 0000000000..660a6bbe86 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_utils.py @@ -0,0 +1,56 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from http import HTTPStatus + +from opentelemetry.instrumentation.utils import http_status_to_canonical_code +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace.status import StatusCanonicalCode + + +class TestUtils(TestBase): + def test_http_status_to_canonical_code(self): + for status_code, expected in ( + (HTTPStatus.OK, StatusCanonicalCode.OK), + (HTTPStatus.ACCEPTED, StatusCanonicalCode.OK), + (HTTPStatus.IM_USED, StatusCanonicalCode.OK), + (HTTPStatus.MULTIPLE_CHOICES, StatusCanonicalCode.OK), + (HTTPStatus.BAD_REQUEST, StatusCanonicalCode.INVALID_ARGUMENT), + (HTTPStatus.UNAUTHORIZED, StatusCanonicalCode.UNAUTHENTICATED), + (HTTPStatus.FORBIDDEN, StatusCanonicalCode.PERMISSION_DENIED), + (HTTPStatus.NOT_FOUND, StatusCanonicalCode.NOT_FOUND), + ( + HTTPStatus.UNPROCESSABLE_ENTITY, + StatusCanonicalCode.INVALID_ARGUMENT, + ), + ( + HTTPStatus.TOO_MANY_REQUESTS, + StatusCanonicalCode.RESOURCE_EXHAUSTED, + ), + (HTTPStatus.NOT_IMPLEMENTED, StatusCanonicalCode.UNIMPLEMENTED), + (HTTPStatus.SERVICE_UNAVAILABLE, StatusCanonicalCode.UNAVAILABLE), + ( + HTTPStatus.GATEWAY_TIMEOUT, + StatusCanonicalCode.DEADLINE_EXCEEDED, + ), + ( + HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, + StatusCanonicalCode.INTERNAL, + ), + (600, StatusCanonicalCode.UNKNOWN), + (99, StatusCanonicalCode.UNKNOWN), + ): + with self.subTest(status_code=status_code): + actual = http_status_to_canonical_code(int(status_code)) + self.assertEqual(actual, expected, status_code) diff --git a/scripts/build.sh b/scripts/build.sh index 682276561b..b7a771789a 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-auto-instrumentation/ ext/*/ ; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ ext/*/ ; do ( echo "building $d" cd "$d" diff --git a/tox.ini b/tox.ini index 026d01cc0c..d73875d895 100644 --- a/tox.ini +++ b/tox.ini @@ -12,9 +12,9 @@ envlist = py3{4,5,6,7,8}-test-sdk pypy3-test-sdk - ; opentelemetry-auto-instrumentation - py3{4,5,6,7,8}-test-auto-instrumentation - pypy3-test-auto-instrumentation + ; opentelemetry-instrumentation + py3{5,6,7,8}-test-instrumentation + pypy3-test-instrumentation ; opentelemetry-example-app py3{4,5,6,7,8}-test-example-app @@ -146,7 +146,7 @@ setenv = changedir = test-api: opentelemetry-api/tests test-sdk: opentelemetry-sdk/tests - test-auto-instrumentation: opentelemetry-auto-instrumentation/tests + test-instrumentation: opentelemetry-instrumentation/tests test-ext-grpc: ext/opentelemetry-ext-grpc/tests test-ext-aiohttp-client: ext/opentelemetry-ext-aiohttp-client/tests test-ext-requests: ext/opentelemetry-ext-requests/tests @@ -182,20 +182,19 @@ commands_pre = ; cases but it saves a lot of boilerplate in this file. test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util - test-auto-instrumentation: pip install {toxinidir}/opentelemetry-auto-instrumentation + ext,instrumentation: pip install {toxinidir}/opentelemetry-instrumentation - example-app: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-requests {toxinidir}/ext/opentelemetry-ext-wsgi {toxinidir}/ext/opentelemetry-ext-flask {toxinidir}/docs/examples/opentelemetry-example-app + example-app: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/ext/opentelemetry-ext-requests {toxinidir}/ext/opentelemetry-ext-wsgi {toxinidir}/ext/opentelemetry-ext-flask {toxinidir}/docs/examples/opentelemetry-example-app - getting-started: pip install -e {toxinidir}/opentelemetry-auto-instrumentation -e {toxinidir}/ext/opentelemetry-ext-requests -e {toxinidir}/ext/opentelemetry-ext-wsgi -e {toxinidir}/ext/opentelemetry-ext-flask + getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/ext/opentelemetry-ext-requests -e {toxinidir}/ext/opentelemetry-ext-wsgi -e {toxinidir}/ext/opentelemetry-ext-flask grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc[test] wsgi,flask,django,asgi: pip install {toxinidir}/tests/util wsgi,flask,django: pip install {toxinidir}/ext/opentelemetry-ext-wsgi - flask,django: pip install {toxinidir}/opentelemetry-auto-instrumentation + asgi: pip install {toxinidir}/ext/opentelemetry-ext-asgi - boto: pip install {toxinidir}/opentelemetry-auto-instrumentation boto: pip install {toxinidir}/ext/opentelemetry-ext-boto[test] flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] @@ -204,25 +203,25 @@ commands_pre = django: pip install {toxinidir}/ext/opentelemetry-ext-django[test] - mysql: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-mysql[test] + mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-mysql[test] opencensusexporter: pip install {toxinidir}/ext/opentelemetry-ext-opencensusexporter prometheus: pip install {toxinidir}/ext/opentelemetry-ext-prometheus - pymongo: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-pymongo[test] + pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo[test] - psycopg2: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-psycopg2 {toxinidir}/ext/opentelemetry-ext-psycopg2[test] + psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-psycopg2 {toxinidir}/ext/opentelemetry-ext-psycopg2[test] - pymysql: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-pymysql[test] + pymysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-pymysql[test] - sqlite3: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-sqlite3[test] + sqlite3: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-sqlite3[test] - redis: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-redis[test] + redis: pip install {toxinidir}/ext/opentelemetry-ext-redis[test] - requests: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-requests[test] + requests: pip install {toxinidir}/ext/opentelemetry-ext-requests[test] - jinja2: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-jinja2[test] + jinja2: pip install {toxinidir}/ext/opentelemetry-ext-jinja2[test] aiohttp-client: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-aiohttp-client @@ -235,9 +234,8 @@ commands_pre = zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin - sqlalchemy: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-sqlalchemy + sqlalchemy: pip install {toxinidir}/ext/opentelemetry-ext-sqlalchemy - system-metrics: pip install {toxinidir}/opentelemetry-auto-instrumentation system-metrics: pip install {toxinidir}/ext/opentelemetry-ext-system-metrics[test] ; In order to get a healthy coverage report, @@ -304,9 +302,8 @@ deps = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ - -e {toxinidir}/opentelemetry-auto-instrumentation \ + -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ - -e {toxinidir}/opentelemetry-auto-instrumentation \ -e {toxinidir}/ext/opentelemetry-ext-requests \ -e {toxinidir}/ext/opentelemetry-ext-wsgi \ -e {toxinidir}/ext/opentelemetry-ext-flask @@ -331,7 +328,7 @@ changedir = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-sdk \ - -e {toxinidir}/opentelemetry-auto-instrumentation \ + -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/tests/util \ -e {toxinidir}/ext/opentelemetry-ext-dbapi \ -e {toxinidir}/ext/opentelemetry-ext-mysql \ From 93194e1d4a36a812f87ddd07bbaac877c4a0ab2d Mon Sep 17 00:00:00 2001 From: HiveTraum Date: Tue, 9 Jun 2020 09:16:23 +0500 Subject: [PATCH 0391/1517] fix: requests headers none argument fix (#708) --- .../src/opentelemetry/ext/requests/__init__.py | 3 ++- .../tests/test_requests_integration.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py index ee4796f168..0437c1bf54 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py @@ -96,8 +96,9 @@ def instrumented_request(self, method, url, *args, **kwargs): span.set_attribute("http.method", method.upper()) span.set_attribute("http.url", url) - headers = kwargs.setdefault("headers", {}) + headers = kwargs.get("headers", {}) or {} propagators.inject(type(headers).__setitem__, headers) + kwargs["headers"] = headers try: result = wrapped( diff --git a/ext/opentelemetry-ext-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-requests/tests/test_requests_integration.py index 0ad5b9d19d..75a63f3cf8 100644 --- a/ext/opentelemetry-ext-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-requests/tests/test_requests_integration.py @@ -228,6 +228,10 @@ def test_custom_tracer_provider(self): self.assertIs(span.resource, resource) + def test_if_headers_equals_none(self): + result = requests.get(self.URL, headers=None) + self.assertEqual(result.text, "Hello!") + @mock.patch("requests.Session.send", side_effect=requests.RequestException) def test_requests_exception_without_response(self, *_, **__): From 3fa15aa2ab86a8808406f0bc07c18732bf1bc7b0 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 9 Jun 2020 09:11:07 -0700 Subject: [PATCH 0392/1517] api: adding trace.get_current_span (#552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The span context is no longer coupled with the tracer itself. As such, providing a get_current_span method bound to the trace api module rather than a specific tracer is semantically correct, and removes a hurdle where someone who wants to retrieve the current trace would have to create a tracer to do so. renaming and exporting get_span_in_context to get_current_span, as the intention of the API is similar, and reduces unneeded aliasing and duplication. set_span_in_context is not renamed, as set_current_span would have implied that the span would then be active in the default context, which is only true after attaching the resulting context returned by set_span_in_context. Keeping that name at least implies some asymmetric behavior from get_current_span. After discussion in the SIG, we decided to remove the legacy get_current_span APIs from Tracer and TracerProvider to reduce long-term confusion of how to idiomatically retrieve the span. Co-authored-by: alrex Co-authored-by: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Co-authored-by: Leighton Chen Co-authored-by: Diego Hurtado Co-authored-by: Mauricio Vásquez Co-authored-by: Andrew Xue Co-authored-by: Cheng-Lung Sung --- docs/api/trace.rst | 3 +- docs/api/trace.span.rst | 7 + .../ext/aiohttp_client/__init__.py | 2 +- .../opentelemetry/ext/datadog/propagator.py | 7 +- .../tests/test_datadog_format.py | 15 +- .../tests/test_server_interceptor.py | 16 +- .../ext/opentracing_shim/__init__.py | 14 +- .../tests/test_shim.py | 19 +- .../src/opentelemetry/ext/wsgi/__init__.py | 1 - opentelemetry-api/CHANGELOG.md | 2 + .../src/opentelemetry/trace/__init__.py | 347 +++--------------- .../trace/propagation/__init__.py | 28 +- .../propagation/tracecontexthttptextformat.py | 26 +- .../src/opentelemetry/trace/span.py | 274 ++++++++++++++ .../propagators/test_global_httptextformat.py | 7 +- .../test_tracecontexthttptextformat.py | 28 +- opentelemetry-api/tests/trace/test_globals.py | 22 +- opentelemetry-api/tests/trace/test_tracer.py | 4 - opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 11 +- .../sdk/trace/propagation/b3_format.py | 14 +- .../tests/trace/propagation/test_b3_format.py | 22 +- opentelemetry-sdk/tests/trace/test_trace.py | 56 +-- .../opentelemetry/test/mock_httptextformat.py | 36 +- 24 files changed, 522 insertions(+), 441 deletions(-) create mode 100644 docs/api/trace.span.rst create mode 100644 opentelemetry-api/src/opentelemetry/trace/span.py diff --git a/docs/api/trace.rst b/docs/api/trace.rst index 00823aa036..411e31023e 100644 --- a/docs/api/trace.rst +++ b/docs/api/trace.rst @@ -8,8 +8,9 @@ Submodules trace.sampling trace.status + trace.span Module contents --------------- -.. automodule:: opentelemetry.trace +.. automodule:: opentelemetry.trace \ No newline at end of file diff --git a/docs/api/trace.span.rst b/docs/api/trace.span.rst new file mode 100644 index 0000000000..94b36930df --- /dev/null +++ b/docs/api/trace.span.rst @@ -0,0 +1,7 @@ +opentelemetry.trace.span +======================== + +.. automodule:: opentelemetry.trace.span + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py index bc6c361f9b..a05e7c0601 100644 --- a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py +++ b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py @@ -145,7 +145,7 @@ async def on_request_start( ) trace_config_ctx.token = context_api.attach( - trace.propagation.set_span_in_context(trace_config_ctx.span) + trace.set_span_in_context(trace_config_ctx.span) ) propagators.inject(type(params.headers).__setitem__, params.headers) diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py index d6595e8d93..6ff192f425 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py @@ -16,10 +16,7 @@ from opentelemetry import trace from opentelemetry.context import Context -from opentelemetry.trace.propagation import ( - get_span_from_context, - set_span_in_context, -) +from opentelemetry.trace import get_current_span, set_span_in_context from opentelemetry.trace.propagation.httptextformat import ( Getter, HTTPTextFormat, @@ -88,7 +85,7 @@ def inject( carrier: HTTPTextFormatT, context: typing.Optional[Context] = None, ) -> None: - span = get_span_from_context(context=context) + span = get_current_span(context) sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 set_in_carrier( carrier, self.TRACE_ID_KEY, format_trace_id(span.context.trace_id), diff --git a/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py b/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py index cf2fbf4220..31633f8370 100644 --- a/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py +++ b/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py @@ -17,10 +17,7 @@ from opentelemetry import trace as trace_api from opentelemetry.ext.datadog import constants, propagator from opentelemetry.sdk import trace -from opentelemetry.trace.propagation import ( - get_span_from_context, - set_span_in_context, -) +from opentelemetry.trace import get_current_span, set_span_in_context FORMAT = propagator.DatadogFormat() @@ -45,7 +42,7 @@ def test_malformed_headers(self): """Test with no Datadog headers""" malformed_trace_id_key = FORMAT.TRACE_ID_KEY + "-x" malformed_parent_id_key = FORMAT.PARENT_ID_KEY + "-x" - context = get_span_from_context( + context = get_current_span( FORMAT.extract( get_as_list, { @@ -66,7 +63,7 @@ def test_missing_trace_id(self): } ctx = FORMAT.extract(get_as_list, carrier) - span_context = get_span_from_context(ctx).get_context() + span_context = get_current_span(ctx).get_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) def test_missing_parent_id(self): @@ -76,12 +73,12 @@ def test_missing_parent_id(self): } ctx = FORMAT.extract(get_as_list, carrier) - span_context = get_span_from_context(ctx).get_context() + span_context = get_current_span(ctx).get_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) def test_context_propagation(self): """Test the propagation of Datadog headers.""" - parent_context = get_span_from_context( + parent_context = get_current_span( FORMAT.extract( get_as_list, { @@ -138,7 +135,7 @@ def test_context_propagation(self): def test_sampling_priority_auto_reject(self): """Test sampling priority rejected.""" - parent_context = get_span_from_context( + parent_context = get_current_span( FORMAT.extract( get_as_list, { diff --git a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py index 6c055c863c..67ff0053bf 100644 --- a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py +++ b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py @@ -91,15 +91,13 @@ def test_span_lifetime(self): """Check that the span is active for the duration of the call.""" interceptor = server_interceptor() - tracer = self.tracer_provider.get_tracer(__name__) # To capture the current span at the time the handler is called active_span_in_handler = None def handler(request, context): nonlocal active_span_in_handler - # The current span is shared among all the tracers. - active_span_in_handler = tracer.get_current_span() + active_span_in_handler = trace.get_current_span() return b"" server = grpc.server( @@ -112,13 +110,13 @@ def handler(request, context): port = server.add_insecure_port("[::]:0") channel = grpc.insecure_channel("localhost:{:d}".format(port)) - active_span_before_call = tracer.get_current_span() + active_span_before_call = trace.get_current_span() try: server.start() channel.unary_unary("")(b"") finally: server.stop(None) - active_span_after_call = tracer.get_current_span() + active_span_after_call = trace.get_current_span() self.assertIsNone(active_span_before_call) self.assertIsNone(active_span_after_call) @@ -128,15 +126,13 @@ def handler(request, context): def test_sequential_server_spans(self): """Check that sequential RPCs get separate server spans.""" - tracer = self.tracer_provider.get_tracer(__name__) - interceptor = server_interceptor() # Capture the currently active span in each thread active_spans_in_handler = [] def handler(request, context): - active_spans_in_handler.append(tracer.get_current_span()) + active_spans_in_handler.append(trace.get_current_span()) return b"" server = grpc.server( @@ -175,8 +171,6 @@ def test_concurrent_server_spans(self): context. """ - tracer = self.tracer_provider.get_tracer(__name__) - interceptor = server_interceptor() # Capture the currently active span in each thread @@ -185,7 +179,7 @@ def test_concurrent_server_spans(self): def handler(request, context): latch() - active_spans_in_handler.append(tracer.get_current_span()) + active_spans_in_handler.append(trace.get_current_span()) return b"" server = grpc.server( diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index c62f063de2..81f25013ff 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -94,11 +94,7 @@ from opentelemetry import propagators from opentelemetry.ext.opentracing_shim import util from opentelemetry.ext.opentracing_shim.version import __version__ -from opentelemetry.trace import DefaultSpan -from opentelemetry.trace.propagation import ( - get_span_from_context, - set_span_in_context, -) +from opentelemetry.trace import DefaultSpan, set_span_in_context logger = logging.getLogger(__name__) @@ -473,7 +469,7 @@ def active(self): shim and is likely to be handled in future versions. """ - span = self._tracer.unwrap().get_current_span() + span = trace_api.get_current_span() if span is None: return None @@ -703,6 +699,10 @@ def get_as_list(dict_object, key): propagator = propagators.get_global_httptextformat() ctx = propagator.extract(get_as_list, carrier) - otel_context = get_span_from_context(ctx).get_context() + span = trace_api.get_current_span(ctx) + if span is not None: + otel_context = span.get_context() + else: + otel_context = trace_api.INVALID_SPAN_CONTEXT return SpanContextShim(otel_context) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index abca2e052b..941d428069 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -24,7 +24,10 @@ from opentelemetry import propagators, trace from opentelemetry.ext.opentracing_shim import util from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.test.mock_httptextformat import MockHTTPTextFormat +from opentelemetry.test.mock_httptextformat import ( + MockHTTPTextFormat, + NOOPHTTPTextFormat, +) class TestShim(TestCase): @@ -515,6 +518,20 @@ def test_extract_http_headers(self): self.assertEqual(ctx.unwrap().trace_id, 1220) self.assertEqual(ctx.unwrap().span_id, 7478) + def test_extract_empty_context_returns_invalid_context(self): + """In the case where the propagator cannot extract a + SpanContext, extract should return and invalid span context. + """ + _old_propagator = propagators.get_global_httptextformat() + propagators.set_global_httptextformat(NOOPHTTPTextFormat()) + try: + carrier = {} + + ctx = self.shim.extract(opentracing.Format.HTTP_HEADERS, carrier) + self.assertEqual(ctx.unwrap(), trace.INVALID_SPAN_CONTEXT) + finally: + propagators.set_global_httptextformat(_old_propagator) + def test_extract_text_map(self): """Test `extract()` method for Format.TEXT_MAP.""" diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 0895bc75b0..c5b0216869 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -62,7 +62,6 @@ def hello(): from opentelemetry import context, propagators, trace from opentelemetry.ext.wsgi.version import __version__ from opentelemetry.instrumentation.utils import http_status_to_canonical_code -from opentelemetry.trace.propagation import get_span_from_context from opentelemetry.trace.status import Status, StatusCanonicalCode _HTTP_VERSION_PREFIX = "HTTP/" diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 4503489fa8..b1610ab2c3 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -6,6 +6,8 @@ ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) +- Adding trace.get_current_span, Removing Tracer.get_current_span + ([#552](https://github.com/open-telemetry/opentelemetry-python/pull/552)) - Rename Observer to ValueObserver ([#764](https://github.com/open-telemetry/opentelemetry-python/pull/764)) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 25af91a400..fa0bc376e7 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -70,6 +70,36 @@ `set_tracer_provider`. """ +__all__ = [ + "DEFAULT_TRACE_OPTIONS", + "DEFAULT_TRACE_STATE", + "INVALID_SPAN", + "INVALID_SPAN_CONTEXT", + "INVALID_SPAN_ID", + "INVALID_TRACE_ID", + "DefaultSpan", + "DefaultTracer", + "DefaultTracerProvider", + "LazyLink", + "Link", + "LinkBase", + "ParentSpan", + "Span", + "SpanContext", + "SpanKind", + "TraceFlags", + "TraceState", + "TracerProvider", + "Tracer", + "format_span_id", + "format_trace_id", + "get_current_span", + "get_tracer", + "get_tracer_provider", + "set_tracer_provider", + "set_span_in_context", +] + import abc import enum import types as python_types @@ -77,6 +107,26 @@ from contextlib import contextmanager from logging import getLogger +from opentelemetry.configuration import Configuration +from opentelemetry.trace.propagation import ( + get_current_span, + set_span_in_context, +) +from opentelemetry.trace.span import ( + DEFAULT_TRACE_OPTIONS, + DEFAULT_TRACE_STATE, + INVALID_SPAN, + INVALID_SPAN_CONTEXT, + INVALID_SPAN_ID, + INVALID_TRACE_ID, + DefaultSpan, + Span, + SpanContext, + TraceFlags, + TraceState, + format_span_id, + format_trace_id, +) from opentelemetry.trace.status import Status from opentelemetry.util import _load_trace_provider, types @@ -170,275 +220,6 @@ class SpanKind(enum.Enum): CONSUMER = 4 -class Span(abc.ABC): - """A span represents a single operation within a trace.""" - - @abc.abstractmethod - def end(self, end_time: typing.Optional[int] = None) -> None: - """Sets the current time as the span's end time. - - The span's end time is the wall time at which the operation finished. - - Only the first call to `end` should modify the span, and - implementations are free to ignore or raise on further calls. - """ - - @abc.abstractmethod - def get_context(self) -> "SpanContext": - """Gets the span's SpanContext. - - Get an immutable, serializable identifier for this span that can be - used to create new child spans. - - Returns: - A :class:`.SpanContext` with a copy of this span's immutable state. - """ - - @abc.abstractmethod - def set_attribute(self, key: str, value: types.AttributeValue) -> None: - """Sets an Attribute. - - Sets a single Attribute with the key and value passed as arguments. - """ - - @abc.abstractmethod - def add_event( - self, - name: str, - attributes: types.Attributes = None, - timestamp: typing.Optional[int] = None, - ) -> None: - """Adds an `Event`. - - Adds a single `Event` with the name and, optionally, a timestamp and - attributes passed as arguments. Implementations should generate a - timestamp if the `timestamp` argument is omitted. - """ - - @abc.abstractmethod - def add_lazy_event( - self, - name: str, - event_formatter: types.AttributesFormatter, - timestamp: typing.Optional[int] = None, - ) -> None: - """Adds an `Event`. - - Adds a single `Event` with the name, an event formatter that calculates - the attributes lazily and, optionally, a timestamp. Implementations - should generate a timestamp if the `timestamp` argument is omitted. - """ - - @abc.abstractmethod - def update_name(self, name: str) -> None: - """Updates the `Span` name. - - This will override the name provided via :func:`Tracer.start_span`. - - Upon this update, any sampling behavior based on Span name will depend - on the implementation. - """ - - @abc.abstractmethod - def is_recording_events(self) -> bool: - """Returns whether this span will be recorded. - - Returns true if this Span is active and recording information like - events with the add_event operation and attributes using set_attribute. - """ - - @abc.abstractmethod - def set_status(self, status: Status) -> None: - """Sets the Status of the Span. If used, this will override the default - Span status, which is OK. - """ - - def __enter__(self) -> "Span": - """Invoked when `Span` is used as a context manager. - - Returns the `Span` itself. - """ - return self - - def __exit__( - self, - exc_type: typing.Optional[typing.Type[BaseException]], - exc_val: typing.Optional[BaseException], - exc_tb: typing.Optional[python_types.TracebackType], - ) -> None: - """Ends context manager and calls `end` on the `Span`.""" - - self.end() - - -class TraceFlags(int): - """A bitmask that represents options specific to the trace. - - The only supported option is the "sampled" flag (``0x01``). If set, this - flag indicates that the trace may have been sampled upstream. - - See the `W3C Trace Context - Traceparent`_ spec for details. - - .. _W3C Trace Context - Traceparent: - https://www.w3.org/TR/trace-context/#trace-flags - """ - - DEFAULT = 0x00 - SAMPLED = 0x01 - - @classmethod - def get_default(cls) -> "TraceFlags": - return cls(cls.DEFAULT) - - @property - def sampled(self) -> bool: - return bool(self & TraceFlags.SAMPLED) - - -DEFAULT_TRACE_OPTIONS = TraceFlags.get_default() - - -class TraceState(typing.Dict[str, str]): - """A list of key-value pairs representing vendor-specific trace info. - - Keys and values are strings of up to 256 printable US-ASCII characters. - Implementations should conform to the `W3C Trace Context - Tracestate`_ - spec, which describes additional restrictions on valid field values. - - .. _W3C Trace Context - Tracestate: - https://www.w3.org/TR/trace-context/#tracestate-field - """ - - @classmethod - def get_default(cls) -> "TraceState": - return cls() - - -DEFAULT_TRACE_STATE = TraceState.get_default() - - -def format_trace_id(trace_id: int) -> str: - return "0x{:032x}".format(trace_id) - - -def format_span_id(span_id: int) -> str: - return "0x{:016x}".format(span_id) - - -class SpanContext: - """The state of a Span to propagate between processes. - - This class includes the immutable attributes of a :class:`.Span` that must - be propagated to a span's children and across process boundaries. - - Args: - trace_id: The ID of the trace that this span belongs to. - span_id: This span's ID. - trace_flags: Trace options to propagate. - trace_state: Tracing-system-specific info to propagate. - is_remote: True if propagated from a remote parent. - """ - - def __init__( - self, - trace_id: int, - span_id: int, - is_remote: bool, - trace_flags: "TraceFlags" = DEFAULT_TRACE_OPTIONS, - trace_state: "TraceState" = DEFAULT_TRACE_STATE, - ) -> None: - if trace_flags is None: - trace_flags = DEFAULT_TRACE_OPTIONS - if trace_state is None: - trace_state = DEFAULT_TRACE_STATE - self.trace_id = trace_id - self.span_id = span_id - self.trace_flags = trace_flags - self.trace_state = trace_state - self.is_remote = is_remote - - def __repr__(self) -> str: - return ( - "{}(trace_id={}, span_id={}, trace_state={!r}, is_remote={})" - ).format( - type(self).__name__, - format_trace_id(self.trace_id), - format_span_id(self.span_id), - self.trace_state, - self.is_remote, - ) - - def is_valid(self) -> bool: - """Get whether this `SpanContext` is valid. - - A `SpanContext` is said to be invalid if its trace ID or span ID is - invalid (i.e. ``0``). - - Returns: - True if the `SpanContext` is valid, false otherwise. - """ - return ( - self.trace_id != INVALID_TRACE_ID - and self.span_id != INVALID_SPAN_ID - ) - - -class DefaultSpan(Span): - """The default Span that is used when no Span implementation is available. - - All operations are no-op except context propagation. - """ - - def __init__(self, context: "SpanContext") -> None: - self._context = context - - def get_context(self) -> "SpanContext": - return self._context - - def is_recording_events(self) -> bool: - return False - - def end(self, end_time: typing.Optional[int] = None) -> None: - pass - - def set_attribute(self, key: str, value: types.AttributeValue) -> None: - pass - - def add_event( - self, - name: str, - attributes: types.Attributes = None, - timestamp: typing.Optional[int] = None, - ) -> None: - pass - - def add_lazy_event( - self, - name: str, - event_formatter: types.AttributesFormatter, - timestamp: typing.Optional[int] = None, - ) -> None: - pass - - def update_name(self, name: str) -> None: - pass - - def set_status(self, status: Status) -> None: - pass - - -INVALID_SPAN_ID = 0x0000000000000000 -INVALID_TRACE_ID = 0x00000000000000000000000000000000 -INVALID_SPAN_CONTEXT = SpanContext( - trace_id=INVALID_TRACE_ID, - span_id=INVALID_SPAN_ID, - is_remote=False, - trace_flags=DEFAULT_TRACE_OPTIONS, - trace_state=DEFAULT_TRACE_STATE, -) -INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) - - class TracerProvider(abc.ABC): @abc.abstractmethod def get_tracer( @@ -495,18 +276,6 @@ class Tracer(abc.ABC): # This is the default behavior when creating spans. CURRENT_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) - @abc.abstractmethod - def get_current_span(self) -> "Span": - """Gets the currently active span from the context. - - If there is no current span, return a placeholder span with an invalid - context. - - Returns: - The currently active :class:`.Span`, or a placeholder span with an - invalid :class:`.SpanContext`. - """ - @abc.abstractmethod def start_span( self, @@ -524,7 +293,7 @@ def start_span( span in this tracer's context. By default the current span will be used as parent, but an explicit - parent can also be specified, either a `Span` or a `SpanContext`. If + parent can also be specified, either a `Span` or a `opentelemetry.trace.SpanContext`. If the specified value is `None`, the created span will be a root span. The span can be used as context manager. On exiting, the span will be @@ -532,7 +301,7 @@ def start_span( Example:: - # tracer.get_current_span() will be used as the implicit parent. + # trace.get_current_span() will be used as the implicit parent. # If none is found, the created span will be a root instance. with tracer.start_span("one") as child: child.add_event("child's event") @@ -578,11 +347,11 @@ def start_as_current_span( with tracer.start_as_current_span("one") as parent: parent.add_event("parent's event") - with tracer.start_as_current_span("two") as child: + with trace.start_as_current_span("two") as child: child.add_event("child's event") - tracer.get_current_span() # returns child - tracer.get_current_span() # returns parent - tracer.get_current_span() # returns previously active span + trace.get_current_span() # returns child + trace.get_current_span() # returns parent + trace.get_current_span() # returns previously active span This is a convenience method for creating spans attached to the tracer's context. Applications that need more control over the span @@ -636,10 +405,6 @@ class DefaultTracer(Tracer): All operations are no-op. """ - def get_current_span(self) -> "Span": - # pylint: disable=no-self-use - return INVALID_SPAN - def start_span( self, name: str, diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index f17350c745..45a07c920d 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -13,22 +13,40 @@ # limitations under the License. from typing import Optional -from opentelemetry import trace as trace_api from opentelemetry.context import get_value, set_value from opentelemetry.context.context import Context +from opentelemetry.trace.span import INVALID_SPAN, Span SPAN_KEY = "current-span" def set_span_in_context( - span: trace_api.Span, context: Optional[Context] = None + span: Span, context: Optional[Context] = None ) -> Context: + """Set the span in the given context. + + Args: + span: The Span to set. + context: a Context object. if one is not passed, the + default current context is used instead. + """ ctx = set_value(SPAN_KEY, span, context=context) return ctx -def get_span_from_context(context: Optional[Context] = None) -> trace_api.Span: +def get_current_span(context: Optional[Context] = None) -> Optional[Span]: + """Retrieve the current span. + + Args: + context: A Context object. If one is not passed, the + default current context is used instead. + + Returns: + The Span set in the context if it exists. None otherwise. + """ span = get_value(SPAN_KEY, context=context) - if not isinstance(span, trace_api.Span): - return trace_api.INVALID_SPAN + if span is None: + return None + if not isinstance(span, Span): + return INVALID_SPAN return span diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py index 732ce96c66..fa2fae8703 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py @@ -17,11 +17,7 @@ import opentelemetry.trace as trace from opentelemetry.context.context import Context -from opentelemetry.trace.propagation import ( - get_span_from_context, - httptextformat, - set_span_in_context, -) +from opentelemetry.trace.propagation import httptextformat # Keys and values are strings of up to 256 printable US-ASCII characters. # Implementations should conform to the `W3C Trace Context - Tracestate`_ @@ -77,11 +73,11 @@ def extract( header = get_from_carrier(carrier, self._TRACEPARENT_HEADER_NAME) if not header: - return set_span_in_context(trace.INVALID_SPAN, context) + return trace.set_span_in_context(trace.INVALID_SPAN, context) match = re.search(self._TRACEPARENT_HEADER_FORMAT_RE, header[0]) if not match: - return set_span_in_context(trace.INVALID_SPAN, context) + return trace.set_span_in_context(trace.INVALID_SPAN, context) version = match.group(1) trace_id = match.group(2) @@ -89,13 +85,13 @@ def extract( trace_flags = match.group(4) if trace_id == "0" * 32 or span_id == "0" * 16: - return set_span_in_context(trace.INVALID_SPAN, context) + return trace.set_span_in_context(trace.INVALID_SPAN, context) if version == "00": if match.group(5): - return set_span_in_context(trace.INVALID_SPAN, context) + return trace.set_span_in_context(trace.INVALID_SPAN, context) if version == "ff": - return set_span_in_context(trace.INVALID_SPAN, context) + return trace.set_span_in_context(trace.INVALID_SPAN, context) tracestate_headers = get_from_carrier( carrier, self._TRACESTATE_HEADER_NAME @@ -109,7 +105,9 @@ def extract( trace_flags=trace.TraceFlags(trace_flags), trace_state=tracestate, ) - return set_span_in_context(trace.DefaultSpan(span_context), context) + return trace.set_span_in_context( + trace.DefaultSpan(span_context), context + ) def inject( self, @@ -121,8 +119,10 @@ def inject( See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` """ - span_context = get_span_from_context(context).get_context() - + span = trace.get_current_span(context) + if span is None: + return + span_context = span.get_context() if span_context == trace.INVALID_SPAN_CONTEXT: return traceparent_string = "00-{:032x}-{:016x}-{:02x}".format( diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py new file mode 100644 index 0000000000..7f0f39d92a --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -0,0 +1,274 @@ +import abc +import types as python_types +import typing + +from opentelemetry.trace.status import Status +from opentelemetry.util import types + + +class Span(abc.ABC): + """A span represents a single operation within a trace.""" + + @abc.abstractmethod + def end(self, end_time: typing.Optional[int] = None) -> None: + """Sets the current time as the span's end time. + + The span's end time is the wall time at which the operation finished. + + Only the first call to `end` should modify the span, and + implementations are free to ignore or raise on further calls. + """ + + @abc.abstractmethod + def get_context(self) -> "SpanContext": + """Gets the span's SpanContext. + + Get an immutable, serializable identifier for this span that can be + used to create new child spans. + + Returns: + A :class:`opentelemetry.trace.SpanContext` with a copy of this span's immutable state. + """ + + @abc.abstractmethod + def set_attribute(self, key: str, value: types.AttributeValue) -> None: + """Sets an Attribute. + + Sets a single Attribute with the key and value passed as arguments. + """ + + @abc.abstractmethod + def add_event( + self, + name: str, + attributes: types.Attributes = None, + timestamp: typing.Optional[int] = None, + ) -> None: + """Adds an `Event`. + + Adds a single `Event` with the name and, optionally, a timestamp and + attributes passed as arguments. Implementations should generate a + timestamp if the `timestamp` argument is omitted. + """ + + @abc.abstractmethod + def add_lazy_event( + self, + name: str, + event_formatter: types.AttributesFormatter, + timestamp: typing.Optional[int] = None, + ) -> None: + """Adds an `Event`. + Adds a single `Event` with the name, an event formatter that calculates + the attributes lazily and, optionally, a timestamp. Implementations + should generate a timestamp if the `timestamp` argument is omitted. + """ + + @abc.abstractmethod + def update_name(self, name: str) -> None: + """Updates the `Span` name. + + This will override the name provided via :func:`opentelemetry.trace.Tracer.start_span`. + + Upon this update, any sampling behavior based on Span name will depend + on the implementation. + """ + + @abc.abstractmethod + def is_recording_events(self) -> bool: + """Returns whether this span will be recorded. + + Returns true if this Span is active and recording information like + events with the add_event operation and attributes using set_attribute. + """ + + @abc.abstractmethod + def set_status(self, status: Status) -> None: + """Sets the Status of the Span. If used, this will override the default + Span status, which is OK. + """ + + def __enter__(self) -> "Span": + """Invoked when `Span` is used as a context manager. + + Returns the `Span` itself. + """ + return self + + def __exit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc_val: typing.Optional[BaseException], + exc_tb: typing.Optional[python_types.TracebackType], + ) -> None: + """Ends context manager and calls `end` on the `Span`.""" + + self.end() + + +class TraceFlags(int): + """A bitmask that represents options specific to the trace. + + The only supported option is the "sampled" flag (``0x01``). If set, this + flag indicates that the trace may have been sampled upstream. + + See the `W3C Trace Context - Traceparent`_ spec for details. + + .. _W3C Trace Context - Traceparent: + https://www.w3.org/TR/trace-context/#trace-flags + """ + + DEFAULT = 0x00 + SAMPLED = 0x01 + + @classmethod + def get_default(cls) -> "TraceFlags": + return cls(cls.DEFAULT) + + @property + def sampled(self) -> bool: + return bool(self & TraceFlags.SAMPLED) + + +DEFAULT_TRACE_OPTIONS = TraceFlags.get_default() + + +class TraceState(typing.Dict[str, str]): + """A list of key-value pairs representing vendor-specific trace info. + + Keys and values are strings of up to 256 printable US-ASCII characters. + Implementations should conform to the `W3C Trace Context - Tracestate`_ + spec, which describes additional restrictions on valid field values. + + .. _W3C Trace Context - Tracestate: + https://www.w3.org/TR/trace-context/#tracestate-field + """ + + @classmethod + def get_default(cls) -> "TraceState": + return cls() + + +DEFAULT_TRACE_STATE = TraceState.get_default() + + +class SpanContext: + """The state of a Span to propagate between processes. + + This class includes the immutable attributes of a :class:`.Span` that must + be propagated to a span's children and across process boundaries. + + Args: + trace_id: The ID of the trace that this span belongs to. + span_id: This span's ID. + trace_flags: Trace options to propagate. + trace_state: Tracing-system-specific info to propagate. + is_remote: True if propagated from a remote parent. + """ + + def __init__( + self, + trace_id: int, + span_id: int, + is_remote: bool, + trace_flags: "TraceFlags" = DEFAULT_TRACE_OPTIONS, + trace_state: "TraceState" = DEFAULT_TRACE_STATE, + ) -> None: + if trace_flags is None: + trace_flags = DEFAULT_TRACE_OPTIONS + if trace_state is None: + trace_state = DEFAULT_TRACE_STATE + self.trace_id = trace_id + self.span_id = span_id + self.trace_flags = trace_flags + self.trace_state = trace_state + self.is_remote = is_remote + + def __repr__(self) -> str: + return ( + "{}(trace_id={}, span_id={}, trace_state={!r}, is_remote={})" + ).format( + type(self).__name__, + format_trace_id(self.trace_id), + format_span_id(self.span_id), + self.trace_state, + self.is_remote, + ) + + def is_valid(self) -> bool: + """Get whether this `SpanContext` is valid. + + A `SpanContext` is said to be invalid if its trace ID or span ID is + invalid (i.e. ``0``). + + Returns: + True if the `SpanContext` is valid, false otherwise. + """ + return ( + self.trace_id != INVALID_TRACE_ID + and self.span_id != INVALID_SPAN_ID + ) + + +class DefaultSpan(Span): + """The default Span that is used when no Span implementation is available. + + All operations are no-op except context propagation. + """ + + def __init__(self, context: "SpanContext") -> None: + self._context = context + + def get_context(self) -> "SpanContext": + return self._context + + def is_recording_events(self) -> bool: + return False + + def end(self, end_time: typing.Optional[int] = None) -> None: + pass + + def set_attribute(self, key: str, value: types.AttributeValue) -> None: + pass + + def add_event( + self, + name: str, + attributes: types.Attributes = None, + timestamp: typing.Optional[int] = None, + ) -> None: + pass + + def add_lazy_event( + self, + name: str, + event_formatter: types.AttributesFormatter, + timestamp: typing.Optional[int] = None, + ) -> None: + pass + + def update_name(self, name: str) -> None: + pass + + def set_status(self, status: Status) -> None: + pass + + +INVALID_SPAN_ID = 0x0000000000000000 +INVALID_TRACE_ID = 0x00000000000000000000000000000000 +INVALID_SPAN_CONTEXT = SpanContext( + trace_id=INVALID_TRACE_ID, + span_id=INVALID_SPAN_ID, + is_remote=False, + trace_flags=DEFAULT_TRACE_OPTIONS, + trace_state=DEFAULT_TRACE_STATE, +) +INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) + + +def format_trace_id(trace_id: int) -> str: + return "0x{:032x}".format(trace_id) + + +def format_span_id(span_id: int) -> str: + return "0x{:016x}".format(span_id) diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index cac24f30a0..a7e9430223 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -17,10 +17,7 @@ from opentelemetry import correlationcontext, trace from opentelemetry.propagators import extract, inject -from opentelemetry.trace.propagation import ( - get_span_from_context, - set_span_in_context, -) +from opentelemetry.trace import get_current_span, set_span_in_context def get_as_list( @@ -52,7 +49,7 @@ def test_propagation(self): correlations = correlationcontext.get_correlations(context=ctx) expected = {"key1": "val1", "key2": "val2"} self.assertEqual(correlations, expected) - span_context = get_span_from_context(context=ctx).get_context() + span_context = get_current_span(context=ctx).get_context() self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 11a8ecd56e..5adc180d9f 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -16,11 +16,7 @@ import unittest from opentelemetry import trace -from opentelemetry.trace.propagation import ( - get_span_from_context, - set_span_in_context, - tracecontexthttptextformat, -) +from opentelemetry.trace.propagation import tracecontexthttptextformat FORMAT = tracecontexthttptextformat.TraceContextHTTPTextFormat() @@ -46,7 +42,7 @@ def test_no_traceparent_header(self): trace-id and parent-id that represents the current request. """ output = {} # type:typing.Dict[str, typing.List[str]] - span = get_span_from_context(FORMAT.extract(get_as_list, output)) + span = trace.get_current_span(FORMAT.extract(get_as_list, output)) self.assertIsInstance(span.get_context(), trace.SpanContext) def test_headers_with_tracestate(self): @@ -58,7 +54,7 @@ def test_headers_with_tracestate(self): span_id=format(self.SPAN_ID, "016x"), ) tracestate_value = "foo=1,bar=2,baz=3" - span_context = get_span_from_context( + span_context = trace.get_current_span( FORMAT.extract( get_as_list, { @@ -76,7 +72,7 @@ def test_headers_with_tracestate(self): output = {} # type:typing.Dict[str, str] span = trace.DefaultSpan(span_context) - ctx = set_span_in_context(span) + ctx = trace.set_span_in_context(span) FORMAT.inject(dict.__setitem__, output, ctx) self.assertEqual(output["traceparent"], traceparent_value) for pair in ["foo=1", "bar=2", "baz=3"]: @@ -102,7 +98,7 @@ def test_invalid_trace_id(self): Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. """ - span = get_span_from_context( + span = trace.get_current_span( FORMAT.extract( get_as_list, { @@ -133,7 +129,7 @@ def test_invalid_parent_id(self): Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. """ - span = get_span_from_context( + span = trace.get_current_span( FORMAT.extract( get_as_list, { @@ -158,7 +154,7 @@ def test_no_send_empty_tracestate(self): span = trace.DefaultSpan( trace.SpanContext(self.TRACE_ID, self.SPAN_ID, is_remote=False) ) - ctx = set_span_in_context(span) + ctx = trace.set_span_in_context(span) FORMAT.inject(dict.__setitem__, output, ctx) self.assertTrue("traceparent" in output) self.assertFalse("tracestate" in output) @@ -171,7 +167,7 @@ def test_format_not_supported(self): If the version cannot be parsed, return an invalid trace header. """ - span = get_span_from_context( + span = trace.get_current_span( FORMAT.extract( get_as_list, { @@ -188,14 +184,14 @@ def test_format_not_supported(self): def test_propagate_invalid_context(self): """Do not propagate invalid trace context.""" output = {} # type:typing.Dict[str, str] - ctx = set_span_in_context(trace.INVALID_SPAN) + ctx = trace.set_span_in_context(trace.INVALID_SPAN) FORMAT.inject(dict.__setitem__, output, context=ctx) self.assertFalse("traceparent" in output) def test_tracestate_empty_header(self): """Test tracestate with an additional empty header (should be ignored) """ - span = get_span_from_context( + span = trace.get_current_span( FORMAT.extract( get_as_list, { @@ -211,7 +207,7 @@ def test_tracestate_empty_header(self): def test_tracestate_header_with_trailing_comma(self): """Do not propagate invalid trace context. """ - span = get_span_from_context( + span = trace.get_current_span( FORMAT.extract( get_as_list, { @@ -235,7 +231,7 @@ def test_tracestate_keys(self): "foo-_*/bar=bar4", ] ) - span = get_span_from_context( + span = trace.get_current_span( FORMAT.extract( get_as_list, { diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 4f38f99ee8..2f0f88fb28 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import patch -from opentelemetry import trace +from opentelemetry import context, trace class TestGlobals(unittest.TestCase): @@ -16,7 +16,25 @@ def test_get_tracer(self): """trace.get_tracer should proxy to the global tracer provider.""" trace.get_tracer("foo", "var") self._mock_tracer_provider.get_tracer.assert_called_with("foo", "var") - mock_provider = unittest.mock.Mock() trace.get_tracer("foo", "var", mock_provider) mock_provider.get_tracer.assert_called_with("foo", "var") + + +class TestTracer(unittest.TestCase): + def setUp(self): + self.tracer = trace.DefaultTracer() + + def test_get_current_span(self): + """DefaultTracer's start_span will also + be retrievable via get_current_span + """ + self.assertIs(trace.get_current_span(), None) + span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + try: + self.assertIs(trace.get_current_span(), span) + finally: + context.detach(token) + self.assertIs(trace.get_current_span(), None) diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py index 4fe3d20f78..1eb1506230 100644 --- a/opentelemetry-api/tests/trace/test_tracer.py +++ b/opentelemetry-api/tests/trace/test_tracer.py @@ -21,10 +21,6 @@ class TestTracer(unittest.TestCase): def setUp(self): self.tracer = trace.DefaultTracer() - def test_get_current_span(self): - span = self.tracer.get_current_span() - self.assertIsInstance(span, trace.Span) - def test_start_span(self): with self.tracer.start_span("") as span: self.assertIsInstance(span, trace.Span) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index d8eb23ab52..713580dbd6 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -6,6 +6,8 @@ ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) +- Adding trace.get_current_span, Removing Tracer.get_current_span + ([#552](https://github.com/open-telemetry/opentelemetry-python/pull/552)) - bugfix: byte type attributes are decoded before adding to attributes dict ([#775](https://github.com/open-telemetry/opentelemetry-python/pull/775)) - Rename Observer to ValueObserver diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 980caed37d..77f4df266b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -240,7 +240,7 @@ class Span(trace_api.Span): Args: name: The name of the operation this span represents context: The immutable span context - parent: This span's parent's `SpanContext`, or + parent: This span's parent's `opentelemetry.trace.SpanContext`, or null if this is a root span sampler: The sampler used to create this span trace_config: TODO @@ -599,9 +599,6 @@ def __init__( self.source = source self.instrumentation_info = instrumentation_info - def get_current_span(self): - return self.source.get_current_span() - def start_as_current_span( self, name: str, @@ -624,7 +621,7 @@ def start_span( # pylint: disable=too-many-locals set_status_on_exception: bool = True, ) -> trace_api.Span: if parent is Tracer.CURRENT_SPAN: - parent = self.get_current_span() + parent = trace_api.get_current_span() parent_context = parent if isinstance(parent_context, trace_api.Span): @@ -756,10 +753,6 @@ def get_tracer( ), ) - @staticmethod - def get_current_span() -> Span: - return context_api.get_value(SPAN_KEY) # type: ignore - def add_span_processor(self, span_processor: SpanProcessor) -> None: """Registers a new :class:`SpanProcessor` for this `TracerProvider`. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py index c0a080e631..3a9722bc36 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py @@ -16,10 +16,6 @@ import opentelemetry.trace as trace from opentelemetry.context import Context -from opentelemetry.trace.propagation import ( - get_span_from_context, - set_span_in_context, -) from opentelemetry.trace.propagation.httptextformat import ( Getter, HTTPTextFormat, @@ -72,7 +68,7 @@ def extract( elif len(fields) == 4: trace_id, span_id, sampled, _ = fields else: - return set_span_in_context(trace.INVALID_SPAN) + return trace.set_span_in_context(trace.INVALID_SPAN) else: trace_id = ( _extract_first_element( @@ -106,7 +102,7 @@ def extract( # header is set to allow. if sampled in self._SAMPLE_PROPAGATE_VALUES or flags == "1": options |= trace.TraceFlags.SAMPLED - return set_span_in_context( + return trace.set_span_in_context( trace.DefaultSpan( trace.SpanContext( # trace an span ids are encoded in hex, so must be converted @@ -125,7 +121,11 @@ def inject( carrier: HTTPTextFormatT, context: typing.Optional[Context] = None, ) -> None: - span = get_span_from_context(context=context) + span = trace.get_current_span(context=context) + + if span is None: + return + sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 set_in_carrier( carrier, self.TRACE_ID_KEY, format_trace_id(span.context.trace_id), diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index e6c644dcdb..a5bd1baaa4 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -17,10 +17,7 @@ import opentelemetry.sdk.trace as trace import opentelemetry.sdk.trace.propagation.b3_format as b3_format import opentelemetry.trace as trace_api -from opentelemetry.trace.propagation import ( - get_span_from_context, - set_span_in_context, -) +from opentelemetry.context import get_current FORMAT = b3_format.B3Format() @@ -33,7 +30,7 @@ def get_as_list(dict_object, key): def get_child_parent_new_carrier(old_carrier): ctx = FORMAT.extract(get_as_list, old_carrier) - parent_context = get_span_from_context(ctx).get_context() + parent_context = trace_api.get_current_span(ctx).get_context() parent = trace.Span("parent", parent_context) child = trace.Span( @@ -49,7 +46,7 @@ def get_child_parent_new_carrier(old_carrier): ) new_carrier = {} - ctx = set_span_in_context(child) + ctx = trace_api.set_span_in_context(child) FORMAT.inject(dict.__setitem__, new_carrier, context=ctx) return child, parent, new_carrier @@ -233,7 +230,7 @@ def test_invalid_single_header(self): """ carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} ctx = FORMAT.extract(get_as_list, carrier) - span_context = get_span_from_context(ctx).get_context() + span_context = trace_api.get_current_span(ctx).get_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) @@ -245,7 +242,7 @@ def test_missing_trace_id(self): } ctx = FORMAT.extract(get_as_list, carrier) - span_context = get_span_from_context(ctx).get_context() + span_context = trace_api.get_current_span(ctx).get_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) def test_missing_span_id(self): @@ -256,5 +253,12 @@ def test_missing_span_id(self): } ctx = FORMAT.extract(get_as_list, carrier) - span_context = get_span_from_context(ctx).get_context() + span_context = trace_api.get_current_span(ctx).get_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) + + @staticmethod + def test_inject_empty_context(): + """If the current context has no span, don't add headers""" + new_carrier = {} + FORMAT.inject(dict.__setitem__, new_carrier, get_current()) + assert len(new_carrier) == 0 diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 32348d87d9..b3600f042f 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -211,26 +211,10 @@ def test_span_processor_for_source(self): span2.span_processor, tracer_provider._active_span_processor ) - def test_get_current_span_multiple_tracers(self): - """In the case where there are multiple tracers, - get_current_span will return the same active span - for both tracers. - """ - tracer_1 = new_tracer() - tracer_2 = new_tracer() - root = tracer_1.start_span("root") - with tracer_1.use_span(root, True): - self.assertIs(tracer_1.get_current_span(), root) - self.assertIs(tracer_2.get_current_span(), root) - - # outside of the loop, both should not reference a span. - self.assertIs(tracer_1.get_current_span(), None) - self.assertIs(tracer_2.get_current_span(), None) - def test_start_span_implicit(self): tracer = new_tracer() - self.assertIsNone(tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) root = tracer.start_span("root") self.assertIsNotNone(root.start_time) @@ -238,7 +222,7 @@ def test_start_span_implicit(self): self.assertEqual(root.kind, trace_api.SpanKind.INTERNAL) with tracer.use_span(root, True): - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) with tracer.start_span( "child", kind=trace_api.SpanKind.CLIENT @@ -265,11 +249,11 @@ def test_start_span_implicit(self): ) # Verify start_span() did not set the current span. - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) - self.assertIsNone(tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) self.assertIsNotNone(root.end_time) def test_start_span_explicit(self): @@ -282,7 +266,7 @@ def test_start_span_explicit(self): trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) - self.assertIsNone(tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) root = tracer.start_span("root") self.assertIsNotNone(root.start_time) @@ -290,7 +274,7 @@ def test_start_span_explicit(self): # Test with the implicit root span with tracer.use_span(root, True): - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) with tracer.start_span("stepchild", other_parent) as child: # The child's parent should be the one passed in, @@ -316,30 +300,30 @@ def test_start_span_explicit(self): ) # Verify start_span() did not set the current span. - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) # Verify ending the child did not set the current span. - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) def test_start_as_current_span_implicit(self): tracer = new_tracer() - self.assertIsNone(tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) with tracer.start_as_current_span("root") as root: - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) with tracer.start_as_current_span("child") as child: - self.assertIs(tracer.get_current_span(), child) + self.assertIs(trace_api.get_current_span(), child) self.assertIs(child.parent, root.get_context()) # After exiting the child's scope the parent should become the # current span again. - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) - self.assertIsNone(tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) self.assertIsNotNone(root.end_time) def test_start_as_current_span_explicit(self): @@ -351,11 +335,11 @@ def test_start_as_current_span_explicit(self): is_remote=False, ) - self.assertIsNone(tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) # Test with the implicit root span with tracer.start_as_current_span("root") as root: - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(root.start_time) self.assertIsNone(root.end_time) @@ -366,14 +350,14 @@ def test_start_as_current_span_explicit(self): # The child should become the current span as usual, but its # parent should be the one passed in, not the # previously-current span. - self.assertIs(tracer.get_current_span(), child) + self.assertIs(trace_api.get_current_span(), child) self.assertNotEqual(child.parent, root) self.assertIs(child.parent, other_parent) # After exiting the child's scope the last span on the stack should # become current, not the child's parent. - self.assertNotEqual(tracer.get_current_span(), other_parent) - self.assertIs(tracer.get_current_span(), root) + self.assertNotEqual(trace_api.get_current_span(), other_parent) + self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) def test_explicit_span_resource(self): @@ -557,7 +541,7 @@ def test_sampling_attributes(self): self.assertEqual(root.attributes["attr-in-both"], "decision-attr") def test_events(self): - self.assertIsNone(self.tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) with self.tracer.start_as_current_span("root") as root: # only event name @@ -613,7 +597,7 @@ def event_formatter(): self.assertEqual(root.events[4].timestamp, now) def test_invalid_event_attributes(self): - self.assertIsNone(self.tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) with self.tracer.start_as_current_span("root") as root: root.add_event("event0", {"attr1": True, "attr2": ["hi", False]}) diff --git a/tests/util/src/opentelemetry/test/mock_httptextformat.py b/tests/util/src/opentelemetry/test/mock_httptextformat.py index 1d4b1d5d51..76165c3e4b 100644 --- a/tests/util/src/opentelemetry/test/mock_httptextformat.py +++ b/tests/util/src/opentelemetry/test/mock_httptextformat.py @@ -15,11 +15,7 @@ import typing from opentelemetry import trace -from opentelemetry.context import Context -from opentelemetry.trace.propagation import ( - get_span_from_context, - set_span_in_context, -) +from opentelemetry.context import Context, get_current from opentelemetry.trace.propagation.httptextformat import ( Getter, HTTPTextFormat, @@ -28,6 +24,30 @@ ) +class NOOPHTTPTextFormat(HTTPTextFormat): + """A propagator that does not extract nor inject. + + This class is useful for catching edge cases assuming + a SpanContext will always be present. + """ + + def extract( + self, + get_from_carrier: Getter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + return get_current() + + def inject( + self, + set_in_carrier: Setter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> None: + return None + + class MockHTTPTextFormat(HTTPTextFormat): """Mock propagator for testing purposes.""" @@ -44,9 +64,9 @@ def extract( span_id_list = get_from_carrier(carrier, self.SPAN_ID_KEY) if not trace_id_list or not span_id_list: - return set_span_in_context(trace.INVALID_SPAN) + return trace.set_span_in_context(trace.INVALID_SPAN) - return set_span_in_context( + return trace.set_span_in_context( trace.DefaultSpan( trace.SpanContext( trace_id=int(trace_id_list[0]), @@ -62,7 +82,7 @@ def inject( carrier: HTTPTextFormatT, context: typing.Optional[Context] = None, ) -> None: - span = get_span_from_context(context) + span = trace.get_current_span(context) set_in_carrier( carrier, self.TRACE_ID_KEY, str(span.get_context().trace_id) ) From 7acb5652893c83e2f83ad9cb2ecee97512f4d0df Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 9 Jun 2020 11:05:21 -0600 Subject: [PATCH 0393/1517] proto: Add proto files (#728) Initial addition of opentelemetry-proto based protobufs. Co-authored-by: alrex Co-authored-by: Yusuke Tsutsumi --- .flake8 | 1 + .isort.cfg | 2 +- .pylintrc | 2 +- opentelemetry-proto/CHANGELOG.md | 5 + opentelemetry-proto/LICENSE | 201 +++++ opentelemetry-proto/MANIFEST.in | 9 + opentelemetry-proto/README.rst | 30 + opentelemetry-proto/setup.cfg | 48 ++ opentelemetry-proto/setup.py | 27 + .../src/opentelemetry/proto/__init__.py | 0 .../opentelemetry/proto/collector/__init__.py | 0 .../proto/collector/metrics/__init__.py | 0 .../proto/collector/metrics/v1/__init__.py | 0 .../metrics/v1/metrics_service_pb2.py | 128 +++ .../metrics/v1/metrics_service_pb2_grpc.py | 75 ++ .../proto/collector/trace/__init__.py | 0 .../proto/collector/trace/v1/__init__.py | 0 .../collector/trace/v1/trace_service_pb2.py | 128 +++ .../trace/v1/trace_service_pb2_grpc.py | 75 ++ .../opentelemetry/proto/common/__init__.py | 0 .../opentelemetry/proto/common/v1/__init__.py | 0 .../proto/common/v1/common_pb2.py | 238 ++++++ .../opentelemetry/proto/metrics/__init__.py | 0 .../proto/metrics/v1/__init__.py | 0 .../proto/metrics/v1/metrics_pb2.py | 789 ++++++++++++++++++ .../opentelemetry/proto/resource/__init__.py | 0 .../proto/resource/v1/__init__.py | 0 .../proto/resource/v1/resource_pb2.py | 81 ++ .../src/opentelemetry/proto/trace/__init__.py | 0 .../opentelemetry/proto/trace/v1/__init__.py | 0 .../proto/trace/v1/trace_config_pb2.py | 290 +++++++ .../opentelemetry/proto/trace/v1/trace_pb2.py | 603 +++++++++++++ .../src/opentelemetry/proto/version.py | 15 + opentelemetry-proto/tests/__init__.py | 0 opentelemetry-proto/tests/test_proto.py | 27 + pyproject.toml | 7 +- tox.ini | 6 + 37 files changed, 2784 insertions(+), 3 deletions(-) create mode 100644 opentelemetry-proto/CHANGELOG.md create mode 100644 opentelemetry-proto/LICENSE create mode 100644 opentelemetry-proto/MANIFEST.in create mode 100644 opentelemetry-proto/README.rst create mode 100644 opentelemetry-proto/setup.cfg create mode 100644 opentelemetry-proto/setup.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/metrics/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/trace/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/common/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/common/v1/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/v1/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/resource/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/resource/v1/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/trace/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/trace/v1/__init__.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/version.py create mode 100644 opentelemetry-proto/tests/__init__.py create mode 100644 opentelemetry-proto/tests/test_proto.py diff --git a/.flake8 b/.flake8 index f511c4c3e0..39288e9b1b 100644 --- a/.flake8 +++ b/.flake8 @@ -20,3 +20,4 @@ exclude = ext/opentelemetry-ext-jaeger/build/* docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ docs/examples/opentelemetry-example-app/build/* + opentelemetry-proto/src/opentelemetry/proto/ diff --git a/.isort.cfg b/.isort.cfg index ae2dfc3252..fd2ecc7865 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -13,6 +13,6 @@ line_length=79 ; docs: https://github.com/timothycrosley/isort#multi-line-output-modes multi_line_output=3 skip=target -skip_glob=**/gen/*,.venv*/*,venv*/* +skip_glob=**/gen/*,.venv*/*,venv*/*,**/proto/* known_first_party=opentelemetry,opentelemetry_example_app known_third_party=psutil,pytest,redis,redis_opentracing diff --git a/.pylintrc b/.pylintrc index 1aa1e10d0b..01c96ea395 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,7 +7,7 @@ extension-pkg-whitelist= # Add files or directories to the blacklist. They should be base names, not # paths. -ignore=CVS,gen +ignore=CVS,gen,proto # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. diff --git a/opentelemetry-proto/CHANGELOG.md b/opentelemetry-proto/CHANGELOG.md new file mode 100644 index 0000000000..3e04402cea --- /dev/null +++ b/opentelemetry-proto/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release diff --git a/opentelemetry-proto/LICENSE b/opentelemetry-proto/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/opentelemetry-proto/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/opentelemetry-proto/MANIFEST.in b/opentelemetry-proto/MANIFEST.in new file mode 100644 index 0000000000..5da7b7fa51 --- /dev/null +++ b/opentelemetry-proto/MANIFEST.in @@ -0,0 +1,9 @@ +graft gen +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/opentelemetry-proto/README.rst b/opentelemetry-proto/README.rst new file mode 100644 index 0000000000..48b9661b20 --- /dev/null +++ b/opentelemetry-proto/README.rst @@ -0,0 +1,30 @@ +OpenTelemetry Python Proto +========================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-proto.svg + :target: https://pypi.org/project/opentelemetry-proto/ + +Installation +------------ + +:: + + pip install opentelemetry-proto + +Code Generation +--------------- + +These files were generated automatically. More details on how to generate them +are available here_. + + +.. _here: https://github.com/open-telemetry/opentelemetry-proto + + +References +---------- + +* `OpenTelemetry Project `_ +* `OpenTelemetry Proto `_ diff --git a/opentelemetry-proto/setup.cfg b/opentelemetry-proto/setup.cfg new file mode 100644 index 0000000000..3796b5ef49 --- /dev/null +++ b/opentelemetry-proto/setup.cfg @@ -0,0 +1,48 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-proto +description = OpenTelemetry Python Proto +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-proto +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +zip_safe = False +include_package_data = True +install_requires = + protobuf~=3.12.2 + +[options.packages.find] +where = src diff --git a/opentelemetry-proto/setup.py b/opentelemetry-proto/setup.py new file mode 100644 index 0000000000..0bb2e6012e --- /dev/null +++ b/opentelemetry-proto/setup.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "proto", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"],) diff --git a/opentelemetry-proto/src/opentelemetry/proto/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/collector/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py new file mode 100644 index 0000000000..96bb34bc8f --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/collector/metrics/v1/metrics_service.proto + +from google.protobuf import descriptor as _descriptor +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() + + +from opentelemetry.proto.metrics.v1 import metrics_pb2 as opentelemetry_dot_proto_dot_metrics_dot_v1_dot_metrics__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='opentelemetry/proto/collector/metrics/v1/metrics_service.proto', + package='opentelemetry.proto.collector.metrics.v1', + syntax='proto3', + serialized_options=b'\n+io.opentelemetry.proto.collector.metrics.v1B\023MetricsServiceProtoP\001ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/metrics/v1', + serialized_pb=b'\n>opentelemetry/proto/collector/metrics/v1/metrics_service.proto\x12(opentelemetry.proto.collector.metrics.v1\x1a,opentelemetry/proto/metrics/v1/metrics.proto\"h\n\x1b\x45xportMetricsServiceRequest\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\x1e\n\x1c\x45xportMetricsServiceResponse2\xac\x01\n\x0eMetricsService\x12\x99\x01\n\x06\x45xport\x12\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest\x1a\x46.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse\"\x00\x42\x8f\x01\n+io.opentelemetry.proto.collector.metrics.v1B\x13MetricsServiceProtoP\x01ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/metrics/v1b\x06proto3' + , + dependencies=[opentelemetry_dot_proto_dot_metrics_dot_v1_dot_metrics__pb2.DESCRIPTOR,]) + + + + +_EXPORTMETRICSSERVICEREQUEST = _descriptor.Descriptor( + name='ExportMetricsServiceRequest', + full_name='opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='resource_metrics', full_name='opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest.resource_metrics', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=154, + serialized_end=258, +) + + +_EXPORTMETRICSSERVICERESPONSE = _descriptor.Descriptor( + name='ExportMetricsServiceResponse', + full_name='opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=260, + serialized_end=290, +) + +_EXPORTMETRICSSERVICEREQUEST.fields_by_name['resource_metrics'].message_type = opentelemetry_dot_proto_dot_metrics_dot_v1_dot_metrics__pb2._RESOURCEMETRICS +DESCRIPTOR.message_types_by_name['ExportMetricsServiceRequest'] = _EXPORTMETRICSSERVICEREQUEST +DESCRIPTOR.message_types_by_name['ExportMetricsServiceResponse'] = _EXPORTMETRICSSERVICERESPONSE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +ExportMetricsServiceRequest = _reflection.GeneratedProtocolMessageType('ExportMetricsServiceRequest', (_message.Message,), { + 'DESCRIPTOR' : _EXPORTMETRICSSERVICEREQUEST, + '__module__' : 'opentelemetry.proto.collector.metrics.v1.metrics_service_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest) + }) +_sym_db.RegisterMessage(ExportMetricsServiceRequest) + +ExportMetricsServiceResponse = _reflection.GeneratedProtocolMessageType('ExportMetricsServiceResponse', (_message.Message,), { + 'DESCRIPTOR' : _EXPORTMETRICSSERVICERESPONSE, + '__module__' : 'opentelemetry.proto.collector.metrics.v1.metrics_service_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse) + }) +_sym_db.RegisterMessage(ExportMetricsServiceResponse) + + +DESCRIPTOR._options = None + +_METRICSSERVICE = _descriptor.ServiceDescriptor( + name='MetricsService', + full_name='opentelemetry.proto.collector.metrics.v1.MetricsService', + file=DESCRIPTOR, + index=0, + serialized_options=None, + serialized_start=293, + serialized_end=465, + methods=[ + _descriptor.MethodDescriptor( + name='Export', + full_name='opentelemetry.proto.collector.metrics.v1.MetricsService.Export', + index=0, + containing_service=None, + input_type=_EXPORTMETRICSSERVICEREQUEST, + output_type=_EXPORTMETRICSSERVICERESPONSE, + serialized_options=None, + ), +]) +_sym_db.RegisterServiceDescriptor(_METRICSSERVICE) + +DESCRIPTOR.services_by_name['MetricsService'] = _METRICSSERVICE + +# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py new file mode 100644 index 0000000000..c58f717616 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py @@ -0,0 +1,75 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +import grpc + +from opentelemetry.proto.collector.metrics.v1 import metrics_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2 + + +class MetricsServiceStub(object): + """Service that can be used to push metrics between one Application + instrumented with OpenTelemetry and a collector, or between a collector and a + central collector. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Export = channel.unary_unary( + '/opentelemetry.proto.collector.metrics.v1.MetricsService/Export', + request_serializer=opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceRequest.SerializeToString, + response_deserializer=opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceResponse.FromString, + ) + + +class MetricsServiceServicer(object): + """Service that can be used to push metrics between one Application + instrumented with OpenTelemetry and a collector, or between a collector and a + central collector. + """ + + def Export(self, request, context): + """For performance reasons, it is recommended to keep this RPC + alive for the entire life of the application. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_MetricsServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Export': grpc.unary_unary_rpc_method_handler( + servicer.Export, + request_deserializer=opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceRequest.FromString, + response_serializer=opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'opentelemetry.proto.collector.metrics.v1.MetricsService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class MetricsService(object): + """Service that can be used to push metrics between one Application + instrumented with OpenTelemetry and a collector, or between a collector and a + central collector. + """ + + @staticmethod + def Export(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/opentelemetry.proto.collector.metrics.v1.MetricsService/Export', + opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceRequest.SerializeToString, + opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceResponse.FromString, + options, channel_credentials, + call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py new file mode 100644 index 0000000000..ccdfb345d9 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/collector/trace/v1/trace_service.proto + +from google.protobuf import descriptor as _descriptor +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() + + +from opentelemetry.proto.trace.v1 import trace_pb2 as opentelemetry_dot_proto_dot_trace_dot_v1_dot_trace__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='opentelemetry/proto/collector/trace/v1/trace_service.proto', + package='opentelemetry.proto.collector.trace.v1', + syntax='proto3', + serialized_options=b'\n)io.opentelemetry.proto.collector.trace.v1B\021TraceServiceProtoP\001ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1', + serialized_pb=b'\n:opentelemetry/proto/collector/trace/v1/trace_service.proto\x12&opentelemetry.proto.collector.trace.v1\x1a(opentelemetry/proto/trace/v1/trace.proto\"`\n\x19\x45xportTraceServiceRequest\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\x1c\n\x1a\x45xportTraceServiceResponse2\xa2\x01\n\x0cTraceService\x12\x91\x01\n\x06\x45xport\x12\x41.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\x1a\x42.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse\"\x00\x42\x89\x01\n)io.opentelemetry.proto.collector.trace.v1B\x11TraceServiceProtoP\x01ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1b\x06proto3' + , + dependencies=[opentelemetry_dot_proto_dot_trace_dot_v1_dot_trace__pb2.DESCRIPTOR,]) + + + + +_EXPORTTRACESERVICEREQUEST = _descriptor.Descriptor( + name='ExportTraceServiceRequest', + full_name='opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='resource_spans', full_name='opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest.resource_spans', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=144, + serialized_end=240, +) + + +_EXPORTTRACESERVICERESPONSE = _descriptor.Descriptor( + name='ExportTraceServiceResponse', + full_name='opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=242, + serialized_end=270, +) + +_EXPORTTRACESERVICEREQUEST.fields_by_name['resource_spans'].message_type = opentelemetry_dot_proto_dot_trace_dot_v1_dot_trace__pb2._RESOURCESPANS +DESCRIPTOR.message_types_by_name['ExportTraceServiceRequest'] = _EXPORTTRACESERVICEREQUEST +DESCRIPTOR.message_types_by_name['ExportTraceServiceResponse'] = _EXPORTTRACESERVICERESPONSE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +ExportTraceServiceRequest = _reflection.GeneratedProtocolMessageType('ExportTraceServiceRequest', (_message.Message,), { + 'DESCRIPTOR' : _EXPORTTRACESERVICEREQUEST, + '__module__' : 'opentelemetry.proto.collector.trace.v1.trace_service_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest) + }) +_sym_db.RegisterMessage(ExportTraceServiceRequest) + +ExportTraceServiceResponse = _reflection.GeneratedProtocolMessageType('ExportTraceServiceResponse', (_message.Message,), { + 'DESCRIPTOR' : _EXPORTTRACESERVICERESPONSE, + '__module__' : 'opentelemetry.proto.collector.trace.v1.trace_service_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse) + }) +_sym_db.RegisterMessage(ExportTraceServiceResponse) + + +DESCRIPTOR._options = None + +_TRACESERVICE = _descriptor.ServiceDescriptor( + name='TraceService', + full_name='opentelemetry.proto.collector.trace.v1.TraceService', + file=DESCRIPTOR, + index=0, + serialized_options=None, + serialized_start=273, + serialized_end=435, + methods=[ + _descriptor.MethodDescriptor( + name='Export', + full_name='opentelemetry.proto.collector.trace.v1.TraceService.Export', + index=0, + containing_service=None, + input_type=_EXPORTTRACESERVICEREQUEST, + output_type=_EXPORTTRACESERVICERESPONSE, + serialized_options=None, + ), +]) +_sym_db.RegisterServiceDescriptor(_TRACESERVICE) + +DESCRIPTOR.services_by_name['TraceService'] = _TRACESERVICE + +# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py new file mode 100644 index 0000000000..1d86433946 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py @@ -0,0 +1,75 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +import grpc + +from opentelemetry.proto.collector.trace.v1 import trace_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2 + + +class TraceServiceStub(object): + """Service that can be used to push spans between one Application instrumented with + OpenTelemetry and an collector, or between an collector and a central collector (in this + case spans are sent/received to/from multiple Applications). + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Export = channel.unary_unary( + '/opentelemetry.proto.collector.trace.v1.TraceService/Export', + request_serializer=opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceRequest.SerializeToString, + response_deserializer=opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceResponse.FromString, + ) + + +class TraceServiceServicer(object): + """Service that can be used to push spans between one Application instrumented with + OpenTelemetry and an collector, or between an collector and a central collector (in this + case spans are sent/received to/from multiple Applications). + """ + + def Export(self, request, context): + """For performance reasons, it is recommended to keep this RPC + alive for the entire life of the application. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_TraceServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Export': grpc.unary_unary_rpc_method_handler( + servicer.Export, + request_deserializer=opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceRequest.FromString, + response_serializer=opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'opentelemetry.proto.collector.trace.v1.TraceService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class TraceService(object): + """Service that can be used to push spans between one Application instrumented with + OpenTelemetry and an collector, or between an collector and a central collector (in this + case spans are sent/received to/from multiple Applications). + """ + + @staticmethod + def Export(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/opentelemetry.proto.collector.trace.v1.TraceService/Export', + opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceRequest.SerializeToString, + opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceResponse.FromString, + options, channel_credentials, + call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/common/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/common/v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py new file mode 100644 index 0000000000..62e951269c --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/common/v1/common.proto + +from google.protobuf import descriptor as _descriptor +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.FileDescriptor( + name='opentelemetry/proto/common/v1/common.proto', + package='opentelemetry.proto.common.v1', + syntax='proto3', + serialized_options=b'\n io.opentelemetry.proto.common.v1B\013CommonProtoP\001Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\xf5\x01\n\x11\x41ttributeKeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12H\n\x04type\x18\x02 \x01(\x0e\x32:.opentelemetry.proto.common.v1.AttributeKeyValue.ValueType\x12\x14\n\x0cstring_value\x18\x03 \x01(\t\x12\x11\n\tint_value\x18\x04 \x01(\x03\x12\x14\n\x0c\x64ouble_value\x18\x05 \x01(\x01\x12\x12\n\nbool_value\x18\x06 \x01(\x08\"6\n\tValueType\x12\n\n\x06STRING\x10\x00\x12\x07\n\x03INT\x10\x01\x12\n\n\x06\x44OUBLE\x10\x02\x12\x08\n\x04\x42OOL\x10\x03\",\n\x0eStringKeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"7\n\x16InstrumentationLibrary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\tBq\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1b\x06proto3' +) + + + +_ATTRIBUTEKEYVALUE_VALUETYPE = _descriptor.EnumDescriptor( + name='ValueType', + full_name='opentelemetry.proto.common.v1.AttributeKeyValue.ValueType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='STRING', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='INT', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DOUBLE', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='BOOL', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=269, + serialized_end=323, +) +_sym_db.RegisterEnumDescriptor(_ATTRIBUTEKEYVALUE_VALUETYPE) + + +_ATTRIBUTEKEYVALUE = _descriptor.Descriptor( + name='AttributeKeyValue', + full_name='opentelemetry.proto.common.v1.AttributeKeyValue', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.type', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='string_value', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.string_value', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='int_value', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.int_value', index=3, + number=4, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='double_value', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.double_value', index=4, + number=5, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='bool_value', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.bool_value', index=5, + number=6, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _ATTRIBUTEKEYVALUE_VALUETYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=78, + serialized_end=323, +) + + +_STRINGKEYVALUE = _descriptor.Descriptor( + name='StringKeyValue', + full_name='opentelemetry.proto.common.v1.StringKeyValue', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='opentelemetry.proto.common.v1.StringKeyValue.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='value', full_name='opentelemetry.proto.common.v1.StringKeyValue.value', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=325, + serialized_end=369, +) + + +_INSTRUMENTATIONLIBRARY = _descriptor.Descriptor( + name='InstrumentationLibrary', + full_name='opentelemetry.proto.common.v1.InstrumentationLibrary', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='opentelemetry.proto.common.v1.InstrumentationLibrary.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='version', full_name='opentelemetry.proto.common.v1.InstrumentationLibrary.version', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=371, + serialized_end=426, +) + +_ATTRIBUTEKEYVALUE.fields_by_name['type'].enum_type = _ATTRIBUTEKEYVALUE_VALUETYPE +_ATTRIBUTEKEYVALUE_VALUETYPE.containing_type = _ATTRIBUTEKEYVALUE +DESCRIPTOR.message_types_by_name['AttributeKeyValue'] = _ATTRIBUTEKEYVALUE +DESCRIPTOR.message_types_by_name['StringKeyValue'] = _STRINGKEYVALUE +DESCRIPTOR.message_types_by_name['InstrumentationLibrary'] = _INSTRUMENTATIONLIBRARY +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +AttributeKeyValue = _reflection.GeneratedProtocolMessageType('AttributeKeyValue', (_message.Message,), { + 'DESCRIPTOR' : _ATTRIBUTEKEYVALUE, + '__module__' : 'opentelemetry.proto.common.v1.common_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.common.v1.AttributeKeyValue) + }) +_sym_db.RegisterMessage(AttributeKeyValue) + +StringKeyValue = _reflection.GeneratedProtocolMessageType('StringKeyValue', (_message.Message,), { + 'DESCRIPTOR' : _STRINGKEYVALUE, + '__module__' : 'opentelemetry.proto.common.v1.common_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.common.v1.StringKeyValue) + }) +_sym_db.RegisterMessage(StringKeyValue) + +InstrumentationLibrary = _reflection.GeneratedProtocolMessageType('InstrumentationLibrary', (_message.Message,), { + 'DESCRIPTOR' : _INSTRUMENTATIONLIBRARY, + '__module__' : 'opentelemetry.proto.common.v1.common_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.common.v1.InstrumentationLibrary) + }) +_sym_db.RegisterMessage(InstrumentationLibrary) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py new file mode 100644 index 0000000000..4edaa3609d --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py @@ -0,0 +1,789 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/metrics/v1/metrics.proto + +from google.protobuf import descriptor as _descriptor +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() + + +from opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 +from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='opentelemetry/proto/metrics/v1/metrics.proto', + package='opentelemetry.proto.metrics.v1', + syntax='proto3', + serialized_options=b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xb6\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x66\n\x1finstrumentation_library_metrics\x18\x02 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics\"\xb0\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\"\x8f\x03\n\x06Metric\x12K\n\x11metric_descriptor\x18\x01 \x01(\x0b\x32\x30.opentelemetry.proto.metrics.v1.MetricDescriptor\x12I\n\x11int64_data_points\x18\x02 \x03(\x0b\x32..opentelemetry.proto.metrics.v1.Int64DataPoint\x12K\n\x12\x64ouble_data_points\x18\x03 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleDataPoint\x12Q\n\x15histogram_data_points\x18\x04 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12M\n\x13summary_data_points\x18\x05 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\xfe\x02\n\x10MetricDescriptor\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x43\n\x04type\x18\x04 \x01(\x0e\x32\x35.opentelemetry.proto.metrics.v1.MetricDescriptor.Type\x12Q\n\x0btemporality\x18\x05 \x01(\x0e\x32<.opentelemetry.proto.metrics.v1.MetricDescriptor.Temporality\"K\n\x04Type\x12\x10\n\x0cINVALID_TYPE\x10\x00\x12\t\n\x05INT64\x10\x01\x12\n\n\x06\x44OUBLE\x10\x02\x12\r\n\tHISTOGRAM\x10\x03\x12\x0b\n\x07SUMMARY\x10\x04\"T\n\x0bTemporality\x12\x17\n\x13INVALID_TEMPORALITY\x10\x00\x12\x11\n\rINSTANTANEOUS\x10\x01\x12\t\n\x05\x44\x45LTA\x10\x02\x12\x0e\n\nCUMULATIVE\x10\x03\"\x94\x01\n\x0eInt64DataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x03\"\x95\x01\n\x0f\x44oubleDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x01\"\xf1\x03\n\x12HistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x04\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12J\n\x07\x62uckets\x18\x06 \x03(\x0b\x32\x39.opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x1a\xe4\x01\n\x06\x42ucket\x12\r\n\x05\x63ount\x18\x01 \x01(\x04\x12T\n\x08\x65xemplar\x18\x02 \x01(\x0b\x32\x42.opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar\x1au\n\x08\x45xemplar\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x42\n\x0b\x61ttachments\x18\x03 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\"\xba\x02\n\x10SummaryDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x04\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12]\n\x11percentile_values\x18\x06 \x03(\x0b\x32\x42.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile\x1a\x36\n\x11ValueAtPercentile\x12\x12\n\npercentile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01\x42t\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1b\x06proto3' + , + dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) + + + +_METRICDESCRIPTOR_TYPE = _descriptor.EnumDescriptor( + name='Type', + full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.Type', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='INVALID_TYPE', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='INT64', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DOUBLE', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='HISTOGRAM', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='SUMMARY', index=4, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=1160, + serialized_end=1235, +) +_sym_db.RegisterEnumDescriptor(_METRICDESCRIPTOR_TYPE) + +_METRICDESCRIPTOR_TEMPORALITY = _descriptor.EnumDescriptor( + name='Temporality', + full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.Temporality', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='INVALID_TEMPORALITY', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='INSTANTANEOUS', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DELTA', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CUMULATIVE', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=1237, + serialized_end=1321, +) +_sym_db.RegisterEnumDescriptor(_METRICDESCRIPTOR_TEMPORALITY) + + +_RESOURCEMETRICS = _descriptor.Descriptor( + name='ResourceMetrics', + full_name='opentelemetry.proto.metrics.v1.ResourceMetrics', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='resource', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.resource', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='instrumentation_library_metrics', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.instrumentation_library_metrics', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=173, + serialized_end=355, +) + + +_INSTRUMENTATIONLIBRARYMETRICS = _descriptor.Descriptor( + name='InstrumentationLibraryMetrics', + full_name='opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='instrumentation_library', full_name='opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics.instrumentation_library', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='metrics', full_name='opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics.metrics', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=358, + serialized_end=534, +) + + +_METRIC = _descriptor.Descriptor( + name='Metric', + full_name='opentelemetry.proto.metrics.v1.Metric', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='metric_descriptor', full_name='opentelemetry.proto.metrics.v1.Metric.metric_descriptor', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='int64_data_points', full_name='opentelemetry.proto.metrics.v1.Metric.int64_data_points', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='double_data_points', full_name='opentelemetry.proto.metrics.v1.Metric.double_data_points', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='histogram_data_points', full_name='opentelemetry.proto.metrics.v1.Metric.histogram_data_points', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='summary_data_points', full_name='opentelemetry.proto.metrics.v1.Metric.summary_data_points', index=4, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=537, + serialized_end=936, +) + + +_METRICDESCRIPTOR = _descriptor.Descriptor( + name='MetricDescriptor', + full_name='opentelemetry.proto.metrics.v1.MetricDescriptor', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='description', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.description', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='unit', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.unit', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.type', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='temporality', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.temporality', index=4, + number=5, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _METRICDESCRIPTOR_TYPE, + _METRICDESCRIPTOR_TEMPORALITY, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=939, + serialized_end=1321, +) + + +_INT64DATAPOINT = _descriptor.Descriptor( + name='Int64DataPoint', + full_name='opentelemetry.proto.metrics.v1.Int64DataPoint', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='labels', full_name='opentelemetry.proto.metrics.v1.Int64DataPoint.labels', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.Int64DataPoint.start_time_unix_nano', index=1, + number=2, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.Int64DataPoint.time_unix_nano', index=2, + number=3, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='value', full_name='opentelemetry.proto.metrics.v1.Int64DataPoint.value', index=3, + number=4, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1324, + serialized_end=1472, +) + + +_DOUBLEDATAPOINT = _descriptor.Descriptor( + name='DoubleDataPoint', + full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='labels', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.labels', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.start_time_unix_nano', index=1, + number=2, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.time_unix_nano', index=2, + number=3, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='value', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.value', index=3, + number=4, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1475, + serialized_end=1624, +) + + +_HISTOGRAMDATAPOINT_BUCKET_EXEMPLAR = _descriptor.Descriptor( + name='Exemplar', + full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='value', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar.value', index=0, + number=1, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar.time_unix_nano', index=1, + number=2, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='attachments', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar.attachments', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2007, + serialized_end=2124, +) + +_HISTOGRAMDATAPOINT_BUCKET = _descriptor.Descriptor( + name='Bucket', + full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='count', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.count', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='exemplar', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.exemplar', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_HISTOGRAMDATAPOINT_BUCKET_EXEMPLAR, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1896, + serialized_end=2124, +) + +_HISTOGRAMDATAPOINT = _descriptor.Descriptor( + name='HistogramDataPoint', + full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='labels', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.labels', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.start_time_unix_nano', index=1, + number=2, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.time_unix_nano', index=2, + number=3, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='count', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.count', index=3, + number=4, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='sum', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.sum', index=4, + number=5, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='buckets', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.buckets', index=5, + number=6, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='explicit_bounds', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.explicit_bounds', index=6, + number=7, type=1, cpp_type=5, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_HISTOGRAMDATAPOINT_BUCKET, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1627, + serialized_end=2124, +) + + +_SUMMARYDATAPOINT_VALUEATPERCENTILE = _descriptor.Descriptor( + name='ValueAtPercentile', + full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='percentile', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile.percentile', index=0, + number=1, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='value', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile.value', index=1, + number=2, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2387, + serialized_end=2441, +) + +_SUMMARYDATAPOINT = _descriptor.Descriptor( + name='SummaryDataPoint', + full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='labels', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.labels', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.start_time_unix_nano', index=1, + number=2, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.time_unix_nano', index=2, + number=3, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='count', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.count', index=3, + number=4, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='sum', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.sum', index=4, + number=5, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='percentile_values', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.percentile_values', index=5, + number=6, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_SUMMARYDATAPOINT_VALUEATPERCENTILE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2127, + serialized_end=2441, +) + +_RESOURCEMETRICS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE +_RESOURCEMETRICS.fields_by_name['instrumentation_library_metrics'].message_type = _INSTRUMENTATIONLIBRARYMETRICS +_INSTRUMENTATIONLIBRARYMETRICS.fields_by_name['instrumentation_library'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONLIBRARY +_INSTRUMENTATIONLIBRARYMETRICS.fields_by_name['metrics'].message_type = _METRIC +_METRIC.fields_by_name['metric_descriptor'].message_type = _METRICDESCRIPTOR +_METRIC.fields_by_name['int64_data_points'].message_type = _INT64DATAPOINT +_METRIC.fields_by_name['double_data_points'].message_type = _DOUBLEDATAPOINT +_METRIC.fields_by_name['histogram_data_points'].message_type = _HISTOGRAMDATAPOINT +_METRIC.fields_by_name['summary_data_points'].message_type = _SUMMARYDATAPOINT +_METRICDESCRIPTOR.fields_by_name['type'].enum_type = _METRICDESCRIPTOR_TYPE +_METRICDESCRIPTOR.fields_by_name['temporality'].enum_type = _METRICDESCRIPTOR_TEMPORALITY +_METRICDESCRIPTOR_TYPE.containing_type = _METRICDESCRIPTOR +_METRICDESCRIPTOR_TEMPORALITY.containing_type = _METRICDESCRIPTOR +_INT64DATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_DOUBLEDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_HISTOGRAMDATAPOINT_BUCKET_EXEMPLAR.fields_by_name['attachments'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_HISTOGRAMDATAPOINT_BUCKET_EXEMPLAR.containing_type = _HISTOGRAMDATAPOINT_BUCKET +_HISTOGRAMDATAPOINT_BUCKET.fields_by_name['exemplar'].message_type = _HISTOGRAMDATAPOINT_BUCKET_EXEMPLAR +_HISTOGRAMDATAPOINT_BUCKET.containing_type = _HISTOGRAMDATAPOINT +_HISTOGRAMDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_HISTOGRAMDATAPOINT.fields_by_name['buckets'].message_type = _HISTOGRAMDATAPOINT_BUCKET +_SUMMARYDATAPOINT_VALUEATPERCENTILE.containing_type = _SUMMARYDATAPOINT +_SUMMARYDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_SUMMARYDATAPOINT.fields_by_name['percentile_values'].message_type = _SUMMARYDATAPOINT_VALUEATPERCENTILE +DESCRIPTOR.message_types_by_name['ResourceMetrics'] = _RESOURCEMETRICS +DESCRIPTOR.message_types_by_name['InstrumentationLibraryMetrics'] = _INSTRUMENTATIONLIBRARYMETRICS +DESCRIPTOR.message_types_by_name['Metric'] = _METRIC +DESCRIPTOR.message_types_by_name['MetricDescriptor'] = _METRICDESCRIPTOR +DESCRIPTOR.message_types_by_name['Int64DataPoint'] = _INT64DATAPOINT +DESCRIPTOR.message_types_by_name['DoubleDataPoint'] = _DOUBLEDATAPOINT +DESCRIPTOR.message_types_by_name['HistogramDataPoint'] = _HISTOGRAMDATAPOINT +DESCRIPTOR.message_types_by_name['SummaryDataPoint'] = _SUMMARYDATAPOINT +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +ResourceMetrics = _reflection.GeneratedProtocolMessageType('ResourceMetrics', (_message.Message,), { + 'DESCRIPTOR' : _RESOURCEMETRICS, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.ResourceMetrics) + }) +_sym_db.RegisterMessage(ResourceMetrics) + +InstrumentationLibraryMetrics = _reflection.GeneratedProtocolMessageType('InstrumentationLibraryMetrics', (_message.Message,), { + 'DESCRIPTOR' : _INSTRUMENTATIONLIBRARYMETRICS, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics) + }) +_sym_db.RegisterMessage(InstrumentationLibraryMetrics) + +Metric = _reflection.GeneratedProtocolMessageType('Metric', (_message.Message,), { + 'DESCRIPTOR' : _METRIC, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.Metric) + }) +_sym_db.RegisterMessage(Metric) + +MetricDescriptor = _reflection.GeneratedProtocolMessageType('MetricDescriptor', (_message.Message,), { + 'DESCRIPTOR' : _METRICDESCRIPTOR, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.MetricDescriptor) + }) +_sym_db.RegisterMessage(MetricDescriptor) + +Int64DataPoint = _reflection.GeneratedProtocolMessageType('Int64DataPoint', (_message.Message,), { + 'DESCRIPTOR' : _INT64DATAPOINT, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.Int64DataPoint) + }) +_sym_db.RegisterMessage(Int64DataPoint) + +DoubleDataPoint = _reflection.GeneratedProtocolMessageType('DoubleDataPoint', (_message.Message,), { + 'DESCRIPTOR' : _DOUBLEDATAPOINT, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleDataPoint) + }) +_sym_db.RegisterMessage(DoubleDataPoint) + +HistogramDataPoint = _reflection.GeneratedProtocolMessageType('HistogramDataPoint', (_message.Message,), { + + 'Bucket' : _reflection.GeneratedProtocolMessageType('Bucket', (_message.Message,), { + + 'Exemplar' : _reflection.GeneratedProtocolMessageType('Exemplar', (_message.Message,), { + 'DESCRIPTOR' : _HISTOGRAMDATAPOINT_BUCKET_EXEMPLAR, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar) + }) + , + 'DESCRIPTOR' : _HISTOGRAMDATAPOINT_BUCKET, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket) + }) + , + 'DESCRIPTOR' : _HISTOGRAMDATAPOINT, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.HistogramDataPoint) + }) +_sym_db.RegisterMessage(HistogramDataPoint) +_sym_db.RegisterMessage(HistogramDataPoint.Bucket) +_sym_db.RegisterMessage(HistogramDataPoint.Bucket.Exemplar) + +SummaryDataPoint = _reflection.GeneratedProtocolMessageType('SummaryDataPoint', (_message.Message,), { + + 'ValueAtPercentile' : _reflection.GeneratedProtocolMessageType('ValueAtPercentile', (_message.Message,), { + 'DESCRIPTOR' : _SUMMARYDATAPOINT_VALUEATPERCENTILE, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile) + }) + , + 'DESCRIPTOR' : _SUMMARYDATAPOINT, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.SummaryDataPoint) + }) +_sym_db.RegisterMessage(SummaryDataPoint) +_sym_db.RegisterMessage(SummaryDataPoint.ValueAtPercentile) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/resource/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py new file mode 100644 index 0000000000..5126a1b76e --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/resource/v1/resource.proto + +from google.protobuf import descriptor as _descriptor +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() + + +from opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='opentelemetry/proto/resource/v1/resource.proto', + package='opentelemetry.proto.resource.v1', + syntax='proto3', + serialized_options=b'\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\001Z@github.com/open-telemetry/opentelemetry-proto/gen/go/resource/v1', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"r\n\x08Resource\x12\x44\n\nattributes\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.common.v1.AttributeKeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\rBw\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z@github.com/open-telemetry/opentelemetry-proto/gen/go/resource/v1b\x06proto3' + , + dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,]) + + + + +_RESOURCE = _descriptor.Descriptor( + name='Resource', + full_name='opentelemetry.proto.resource.v1.Resource', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='attributes', full_name='opentelemetry.proto.resource.v1.Resource.attributes', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='dropped_attributes_count', full_name='opentelemetry.proto.resource.v1.Resource.dropped_attributes_count', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=127, + serialized_end=241, +) + +_RESOURCE.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._ATTRIBUTEKEYVALUE +DESCRIPTOR.message_types_by_name['Resource'] = _RESOURCE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Resource = _reflection.GeneratedProtocolMessageType('Resource', (_message.Message,), { + 'DESCRIPTOR' : _RESOURCE, + '__module__' : 'opentelemetry.proto.resource.v1.resource_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.resource.v1.Resource) + }) +_sym_db.RegisterMessage(Resource) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/trace/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py new file mode 100644 index 0000000000..ad1857d37b --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/trace/v1/trace_config.proto + +from google.protobuf import descriptor as _descriptor +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.FileDescriptor( + name='opentelemetry/proto/trace/v1/trace_config.proto', + package='opentelemetry.proto.trace.v1', + syntax='proto3', + serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\020TraceConfigProtoP\001ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n/opentelemetry/proto/trace/v1/trace_config.proto\x12\x1copentelemetry.proto.trace.v1\"\xc8\x03\n\x0bTraceConfig\x12I\n\x10\x63onstant_sampler\x18\x01 \x01(\x0b\x32-.opentelemetry.proto.trace.v1.ConstantSamplerH\x00\x12O\n\x13probability_sampler\x18\x02 \x01(\x0b\x32\x30.opentelemetry.proto.trace.v1.ProbabilitySamplerH\x00\x12R\n\x15rate_limiting_sampler\x18\x03 \x01(\x0b\x32\x31.opentelemetry.proto.trace.v1.RateLimitingSamplerH\x00\x12 \n\x18max_number_of_attributes\x18\x04 \x01(\x03\x12\"\n\x1amax_number_of_timed_events\x18\x05 \x01(\x03\x12\x30\n(max_number_of_attributes_per_timed_event\x18\x06 \x01(\x03\x12\x1b\n\x13max_number_of_links\x18\x07 \x01(\x03\x12)\n!max_number_of_attributes_per_link\x18\x08 \x01(\x03\x42\t\n\x07sampler\"\xa9\x01\n\x0f\x43onstantSampler\x12P\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32>.opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision\"D\n\x10\x43onstantDecision\x12\x0e\n\nALWAYS_OFF\x10\x00\x12\r\n\tALWAYS_ON\x10\x01\x12\x11\n\rALWAYS_PARENT\x10\x02\"1\n\x12ProbabilitySampler\x12\x1b\n\x13samplingProbability\x18\x01 \x01(\x01\"\"\n\x13RateLimitingSampler\x12\x0b\n\x03qps\x18\x01 \x01(\x03\x42~\n\x1fio.opentelemetry.proto.trace.v1B\x10TraceConfigProtoP\x01ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1b\x06proto3' +) + + + +_CONSTANTSAMPLER_CONSTANTDECISION = _descriptor.EnumDescriptor( + name='ConstantDecision', + full_name='opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='ALWAYS_OFF', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='ALWAYS_ON', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='ALWAYS_PARENT', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=642, + serialized_end=710, +) +_sym_db.RegisterEnumDescriptor(_CONSTANTSAMPLER_CONSTANTDECISION) + + +_TRACECONFIG = _descriptor.Descriptor( + name='TraceConfig', + full_name='opentelemetry.proto.trace.v1.TraceConfig', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='constant_sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.constant_sampler', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='probability_sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.probability_sampler', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='rate_limiting_sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.rate_limiting_sampler', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='max_number_of_attributes', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_attributes', index=3, + number=4, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='max_number_of_timed_events', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_timed_events', index=4, + number=5, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='max_number_of_attributes_per_timed_event', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_attributes_per_timed_event', index=5, + number=6, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='max_number_of_links', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_links', index=6, + number=7, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='max_number_of_attributes_per_link', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_attributes_per_link', index=7, + number=8, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.sampler', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + ], + serialized_start=82, + serialized_end=538, +) + + +_CONSTANTSAMPLER = _descriptor.Descriptor( + name='ConstantSampler', + full_name='opentelemetry.proto.trace.v1.ConstantSampler', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='decision', full_name='opentelemetry.proto.trace.v1.ConstantSampler.decision', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _CONSTANTSAMPLER_CONSTANTDECISION, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=541, + serialized_end=710, +) + + +_PROBABILITYSAMPLER = _descriptor.Descriptor( + name='ProbabilitySampler', + full_name='opentelemetry.proto.trace.v1.ProbabilitySampler', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='samplingProbability', full_name='opentelemetry.proto.trace.v1.ProbabilitySampler.samplingProbability', index=0, + number=1, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=712, + serialized_end=761, +) + + +_RATELIMITINGSAMPLER = _descriptor.Descriptor( + name='RateLimitingSampler', + full_name='opentelemetry.proto.trace.v1.RateLimitingSampler', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='qps', full_name='opentelemetry.proto.trace.v1.RateLimitingSampler.qps', index=0, + number=1, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=763, + serialized_end=797, +) + +_TRACECONFIG.fields_by_name['constant_sampler'].message_type = _CONSTANTSAMPLER +_TRACECONFIG.fields_by_name['probability_sampler'].message_type = _PROBABILITYSAMPLER +_TRACECONFIG.fields_by_name['rate_limiting_sampler'].message_type = _RATELIMITINGSAMPLER +_TRACECONFIG.oneofs_by_name['sampler'].fields.append( + _TRACECONFIG.fields_by_name['constant_sampler']) +_TRACECONFIG.fields_by_name['constant_sampler'].containing_oneof = _TRACECONFIG.oneofs_by_name['sampler'] +_TRACECONFIG.oneofs_by_name['sampler'].fields.append( + _TRACECONFIG.fields_by_name['probability_sampler']) +_TRACECONFIG.fields_by_name['probability_sampler'].containing_oneof = _TRACECONFIG.oneofs_by_name['sampler'] +_TRACECONFIG.oneofs_by_name['sampler'].fields.append( + _TRACECONFIG.fields_by_name['rate_limiting_sampler']) +_TRACECONFIG.fields_by_name['rate_limiting_sampler'].containing_oneof = _TRACECONFIG.oneofs_by_name['sampler'] +_CONSTANTSAMPLER.fields_by_name['decision'].enum_type = _CONSTANTSAMPLER_CONSTANTDECISION +_CONSTANTSAMPLER_CONSTANTDECISION.containing_type = _CONSTANTSAMPLER +DESCRIPTOR.message_types_by_name['TraceConfig'] = _TRACECONFIG +DESCRIPTOR.message_types_by_name['ConstantSampler'] = _CONSTANTSAMPLER +DESCRIPTOR.message_types_by_name['ProbabilitySampler'] = _PROBABILITYSAMPLER +DESCRIPTOR.message_types_by_name['RateLimitingSampler'] = _RATELIMITINGSAMPLER +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +TraceConfig = _reflection.GeneratedProtocolMessageType('TraceConfig', (_message.Message,), { + 'DESCRIPTOR' : _TRACECONFIG, + '__module__' : 'opentelemetry.proto.trace.v1.trace_config_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.TraceConfig) + }) +_sym_db.RegisterMessage(TraceConfig) + +ConstantSampler = _reflection.GeneratedProtocolMessageType('ConstantSampler', (_message.Message,), { + 'DESCRIPTOR' : _CONSTANTSAMPLER, + '__module__' : 'opentelemetry.proto.trace.v1.trace_config_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.ConstantSampler) + }) +_sym_db.RegisterMessage(ConstantSampler) + +ProbabilitySampler = _reflection.GeneratedProtocolMessageType('ProbabilitySampler', (_message.Message,), { + 'DESCRIPTOR' : _PROBABILITYSAMPLER, + '__module__' : 'opentelemetry.proto.trace.v1.trace_config_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.ProbabilitySampler) + }) +_sym_db.RegisterMessage(ProbabilitySampler) + +RateLimitingSampler = _reflection.GeneratedProtocolMessageType('RateLimitingSampler', (_message.Message,), { + 'DESCRIPTOR' : _RATELIMITINGSAMPLER, + '__module__' : 'opentelemetry.proto.trace.v1.trace_config_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.RateLimitingSampler) + }) +_sym_db.RegisterMessage(RateLimitingSampler) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py new file mode 100644 index 0000000000..cea946eebe --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py @@ -0,0 +1,603 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/trace/v1/trace.proto + +from google.protobuf import descriptor as _descriptor +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() + + +from opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 +from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='opentelemetry/proto/trace/v1/trace.proto', + package='opentelemetry.proto.trace.v1', + syntax='proto3', + serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xae\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\"\xa8\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\"\xce\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12\x44\n\nattributes\x18\t \x03(\x0b\x32\x30.opentelemetry.proto.common.v1.AttributeKeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x95\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x44\n\nattributes\x18\x03 \x03(\x0b\x32\x30.opentelemetry.proto.common.v1.AttributeKeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\xa6\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x44\n\nattributes\x18\x04 \x03(\x0b\x32\x30.opentelemetry.proto.common.v1.AttributeKeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"g\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x0c\n\x08INTERNAL\x10\x01\x12\n\n\x06SERVER\x10\x02\x12\n\n\x06\x43LIENT\x10\x03\x12\x0c\n\x08PRODUCER\x10\x04\x12\x0c\n\x08\x43ONSUMER\x10\x05\"\x98\x03\n\x06Status\x12=\n\x04\x63ode\x18\x01 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xbd\x02\n\nStatusCode\x12\x06\n\x02Ok\x10\x00\x12\r\n\tCancelled\x10\x01\x12\x10\n\x0cUnknownError\x10\x02\x12\x13\n\x0fInvalidArgument\x10\x03\x12\x14\n\x10\x44\x65\x61\x64lineExceeded\x10\x04\x12\x0c\n\x08NotFound\x10\x05\x12\x11\n\rAlreadyExists\x10\x06\x12\x14\n\x10PermissionDenied\x10\x07\x12\x15\n\x11ResourceExhausted\x10\x08\x12\x16\n\x12\x46\x61iledPrecondition\x10\t\x12\x0b\n\x07\x41\x62orted\x10\n\x12\x0e\n\nOutOfRange\x10\x0b\x12\x11\n\rUnimplemented\x10\x0c\x12\x11\n\rInternalError\x10\r\x12\x0f\n\x0bUnavailable\x10\x0e\x12\x0c\n\x08\x44\x61taLoss\x10\x0f\x12\x13\n\x0fUnauthenticated\x10\x10\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' + , + dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) + + + +_SPAN_SPANKIND = _descriptor.EnumDescriptor( + name='SpanKind', + full_name='opentelemetry.proto.trace.v1.Span.SpanKind', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='SPAN_KIND_UNSPECIFIED', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='INTERNAL', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='SERVER', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CLIENT', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='PRODUCER', index=4, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CONSUMER', index=5, number=5, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=1386, + serialized_end=1489, +) +_sym_db.RegisterEnumDescriptor(_SPAN_SPANKIND) + +_STATUS_STATUSCODE = _descriptor.EnumDescriptor( + name='StatusCode', + full_name='opentelemetry.proto.trace.v1.Status.StatusCode', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='Ok', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='Cancelled', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='UnknownError', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='InvalidArgument', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DeadlineExceeded', index=4, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='NotFound', index=5, number=5, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='AlreadyExists', index=6, number=6, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='PermissionDenied', index=7, number=7, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='ResourceExhausted', index=8, number=8, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='FailedPrecondition', index=9, number=9, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='Aborted', index=10, number=10, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='OutOfRange', index=11, number=11, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='Unimplemented', index=12, number=12, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='InternalError', index=13, number=13, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='Unavailable', index=14, number=14, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DataLoss', index=15, number=15, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='Unauthenticated', index=16, number=16, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=1583, + serialized_end=1900, +) +_sym_db.RegisterEnumDescriptor(_STATUS_STATUSCODE) + + +_RESOURCESPANS = _descriptor.Descriptor( + name='ResourceSpans', + full_name='opentelemetry.proto.trace.v1.ResourceSpans', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='resource', full_name='opentelemetry.proto.trace.v1.ResourceSpans.resource', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='instrumentation_library_spans', full_name='opentelemetry.proto.trace.v1.ResourceSpans.instrumentation_library_spans', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=167, + serialized_end=341, +) + + +_INSTRUMENTATIONLIBRARYSPANS = _descriptor.Descriptor( + name='InstrumentationLibrarySpans', + full_name='opentelemetry.proto.trace.v1.InstrumentationLibrarySpans', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='instrumentation_library', full_name='opentelemetry.proto.trace.v1.InstrumentationLibrarySpans.instrumentation_library', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='spans', full_name='opentelemetry.proto.trace.v1.InstrumentationLibrarySpans.spans', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=344, + serialized_end=512, +) + + +_SPAN_EVENT = _descriptor.Descriptor( + name='Event', + full_name='opentelemetry.proto.trace.v1.Span.Event', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='time_unix_nano', full_name='opentelemetry.proto.trace.v1.Span.Event.time_unix_nano', index=0, + number=1, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='name', full_name='opentelemetry.proto.trace.v1.Span.Event.name', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='attributes', full_name='opentelemetry.proto.trace.v1.Span.Event.attributes', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='dropped_attributes_count', full_name='opentelemetry.proto.trace.v1.Span.Event.dropped_attributes_count', index=3, + number=4, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1066, + serialized_end=1215, +) + +_SPAN_LINK = _descriptor.Descriptor( + name='Link', + full_name='opentelemetry.proto.trace.v1.Span.Link', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='trace_id', full_name='opentelemetry.proto.trace.v1.Span.Link.trace_id', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='span_id', full_name='opentelemetry.proto.trace.v1.Span.Link.span_id', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trace_state', full_name='opentelemetry.proto.trace.v1.Span.Link.trace_state', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='attributes', full_name='opentelemetry.proto.trace.v1.Span.Link.attributes', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='dropped_attributes_count', full_name='opentelemetry.proto.trace.v1.Span.Link.dropped_attributes_count', index=4, + number=5, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1218, + serialized_end=1384, +) + +_SPAN = _descriptor.Descriptor( + name='Span', + full_name='opentelemetry.proto.trace.v1.Span', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='trace_id', full_name='opentelemetry.proto.trace.v1.Span.trace_id', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='span_id', full_name='opentelemetry.proto.trace.v1.Span.span_id', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trace_state', full_name='opentelemetry.proto.trace.v1.Span.trace_state', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='parent_span_id', full_name='opentelemetry.proto.trace.v1.Span.parent_span_id', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='name', full_name='opentelemetry.proto.trace.v1.Span.name', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='kind', full_name='opentelemetry.proto.trace.v1.Span.kind', index=5, + number=6, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='start_time_unix_nano', full_name='opentelemetry.proto.trace.v1.Span.start_time_unix_nano', index=6, + number=7, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end_time_unix_nano', full_name='opentelemetry.proto.trace.v1.Span.end_time_unix_nano', index=7, + number=8, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='attributes', full_name='opentelemetry.proto.trace.v1.Span.attributes', index=8, + number=9, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='dropped_attributes_count', full_name='opentelemetry.proto.trace.v1.Span.dropped_attributes_count', index=9, + number=10, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='events', full_name='opentelemetry.proto.trace.v1.Span.events', index=10, + number=11, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='dropped_events_count', full_name='opentelemetry.proto.trace.v1.Span.dropped_events_count', index=11, + number=12, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='links', full_name='opentelemetry.proto.trace.v1.Span.links', index=12, + number=13, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='dropped_links_count', full_name='opentelemetry.proto.trace.v1.Span.dropped_links_count', index=13, + number=14, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='status', full_name='opentelemetry.proto.trace.v1.Span.status', index=14, + number=15, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_SPAN_EVENT, _SPAN_LINK, ], + enum_types=[ + _SPAN_SPANKIND, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=515, + serialized_end=1489, +) + + +_STATUS = _descriptor.Descriptor( + name='Status', + full_name='opentelemetry.proto.trace.v1.Status', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='code', full_name='opentelemetry.proto.trace.v1.Status.code', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='message', full_name='opentelemetry.proto.trace.v1.Status.message', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _STATUS_STATUSCODE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1492, + serialized_end=1900, +) + +_RESOURCESPANS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE +_RESOURCESPANS.fields_by_name['instrumentation_library_spans'].message_type = _INSTRUMENTATIONLIBRARYSPANS +_INSTRUMENTATIONLIBRARYSPANS.fields_by_name['instrumentation_library'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONLIBRARY +_INSTRUMENTATIONLIBRARYSPANS.fields_by_name['spans'].message_type = _SPAN +_SPAN_EVENT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._ATTRIBUTEKEYVALUE +_SPAN_EVENT.containing_type = _SPAN +_SPAN_LINK.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._ATTRIBUTEKEYVALUE +_SPAN_LINK.containing_type = _SPAN +_SPAN.fields_by_name['kind'].enum_type = _SPAN_SPANKIND +_SPAN.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._ATTRIBUTEKEYVALUE +_SPAN.fields_by_name['events'].message_type = _SPAN_EVENT +_SPAN.fields_by_name['links'].message_type = _SPAN_LINK +_SPAN.fields_by_name['status'].message_type = _STATUS +_SPAN_SPANKIND.containing_type = _SPAN +_STATUS.fields_by_name['code'].enum_type = _STATUS_STATUSCODE +_STATUS_STATUSCODE.containing_type = _STATUS +DESCRIPTOR.message_types_by_name['ResourceSpans'] = _RESOURCESPANS +DESCRIPTOR.message_types_by_name['InstrumentationLibrarySpans'] = _INSTRUMENTATIONLIBRARYSPANS +DESCRIPTOR.message_types_by_name['Span'] = _SPAN +DESCRIPTOR.message_types_by_name['Status'] = _STATUS +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +ResourceSpans = _reflection.GeneratedProtocolMessageType('ResourceSpans', (_message.Message,), { + 'DESCRIPTOR' : _RESOURCESPANS, + '__module__' : 'opentelemetry.proto.trace.v1.trace_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.ResourceSpans) + }) +_sym_db.RegisterMessage(ResourceSpans) + +InstrumentationLibrarySpans = _reflection.GeneratedProtocolMessageType('InstrumentationLibrarySpans', (_message.Message,), { + 'DESCRIPTOR' : _INSTRUMENTATIONLIBRARYSPANS, + '__module__' : 'opentelemetry.proto.trace.v1.trace_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.InstrumentationLibrarySpans) + }) +_sym_db.RegisterMessage(InstrumentationLibrarySpans) + +Span = _reflection.GeneratedProtocolMessageType('Span', (_message.Message,), { + + 'Event' : _reflection.GeneratedProtocolMessageType('Event', (_message.Message,), { + 'DESCRIPTOR' : _SPAN_EVENT, + '__module__' : 'opentelemetry.proto.trace.v1.trace_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.Span.Event) + }) + , + + 'Link' : _reflection.GeneratedProtocolMessageType('Link', (_message.Message,), { + 'DESCRIPTOR' : _SPAN_LINK, + '__module__' : 'opentelemetry.proto.trace.v1.trace_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.Span.Link) + }) + , + 'DESCRIPTOR' : _SPAN, + '__module__' : 'opentelemetry.proto.trace.v1.trace_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.Span) + }) +_sym_db.RegisterMessage(Span) +_sym_db.RegisterMessage(Span.Event) +_sym_db.RegisterMessage(Span.Link) + +Status = _reflection.GeneratedProtocolMessageType('Status', (_message.Message,), { + 'DESCRIPTOR' : _STATUS, + '__module__' : 'opentelemetry.proto.trace.v1.trace_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.Status) + }) +_sym_db.RegisterMessage(Status) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py new file mode 100644 index 0000000000..603bf0b7e5 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.9.dev0" diff --git a/opentelemetry-proto/tests/__init__.py b/opentelemetry-proto/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-proto/tests/test_proto.py b/opentelemetry-proto/tests/test_proto.py new file mode 100644 index 0000000000..6551e4640f --- /dev/null +++ b/opentelemetry-proto/tests/test_proto.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from unittest import TestCase + +from pkg_resources import DistributionNotFound, require + + +class TestInstrumentor(TestCase): + def test_proto(self): + + try: + require(["opentelemetry-proto"]) + except DistributionNotFound: + self.fail("opentelemetry-proto not installed") diff --git a/pyproject.toml b/pyproject.toml index 961304074c..5911dc4e9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,12 @@ exclude = ''' ( /( # generated files docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen| - ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen + ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen| + opentelemetry-proto/src/opentelemetry/proto/collector| + opentelemetry-proto/src/opentelemetry/proto/common| + opentelemetry-proto/src/opentelemetry/proto/metrics| + opentelemetry-proto/src/opentelemetry/proto/resource| + opentelemetry-proto/src/opentelemetry/proto/trace )/ ) ''' diff --git a/tox.ini b/tox.ini index d73875d895..332662a5c6 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,10 @@ envlist = py3{4,5,6,7,8}-test-api pypy3-test-api + ; opentelemetry-proto + py3{4,5,6,7,8}-test-proto + pypy3-test-proto + ; opentelemetry-sdk py3{4,5,6,7,8}-test-sdk pypy3-test-sdk @@ -146,6 +150,7 @@ setenv = changedir = test-api: opentelemetry-api/tests test-sdk: opentelemetry-sdk/tests + test-proto: opentelemetry-proto/tests test-instrumentation: opentelemetry-instrumentation/tests test-ext-grpc: ext/opentelemetry-ext-grpc/tests test-ext-aiohttp-client: ext/opentelemetry-ext-aiohttp-client/tests @@ -182,6 +187,7 @@ commands_pre = ; cases but it saves a lot of boilerplate in this file. test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util + test-proto: pip install {toxinidir}/opentelemetry-proto ext,instrumentation: pip install {toxinidir}/opentelemetry-instrumentation example-app: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/ext/opentelemetry-ext-requests {toxinidir}/ext/opentelemetry-ext-wsgi {toxinidir}/ext/opentelemetry-ext-flask {toxinidir}/docs/examples/opentelemetry-example-app From cde0b0fdd57fa4d7f242f4d94072c987b8ef42e9 Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Tue, 9 Jun 2020 13:42:19 -0400 Subject: [PATCH 0394/1517] cloud-monitor: Add cloud monitoring exporter (#739) Adding an exporter that supports sending metrics data to cloud monitoring (formerly known as stackdriver). Co-authored-by: Chris Kleinknecht Co-authored-by: Yusuke Tsutsumi --- docs-requirements.txt | 1 + docs/examples/cloud_monitoring/README.rst | 35 +++ .../cloud_monitoring/basic_metrics.py | 45 +++ .../ext/cloud_monitoring/cloud_monitoring.rst | 7 + .../README.rst | 18 ++ .../setup.cfg | 48 +++ .../setup.py | 31 ++ .../exporter/cloud_monitoring/__init__.py | 169 +++++++++++ .../exporter/cloud_monitoring/version.py | 15 + .../tests/__init__.py | 0 .../tests/test_cloud_monitoring.py | 285 ++++++++++++++++++ 11 files changed, 654 insertions(+) create mode 100644 docs/examples/cloud_monitoring/README.rst create mode 100644 docs/examples/cloud_monitoring/basic_metrics.py create mode 100644 docs/ext/cloud_monitoring/cloud_monitoring.rst create mode 100644 ext/opentelemetry-exporter-cloud-monitoring/README.rst create mode 100644 ext/opentelemetry-exporter-cloud-monitoring/setup.cfg create mode 100644 ext/opentelemetry-exporter-cloud-monitoring/setup.py create mode 100644 ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py create mode 100644 ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py create mode 100644 ext/opentelemetry-exporter-cloud-monitoring/tests/__init__.py create mode 100644 ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py diff --git a/docs-requirements.txt b/docs-requirements.txt index db10f6f9ee..23a7047e8a 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -22,3 +22,4 @@ wrapt>=1.0.0,<2.0.0 psutil~=5.7.0 boto~=2.0 google-cloud-trace >=0.23.0 +google-cloud-monitoring>=0.36.0 diff --git a/docs/examples/cloud_monitoring/README.rst b/docs/examples/cloud_monitoring/README.rst new file mode 100644 index 0000000000..5ad5421794 --- /dev/null +++ b/docs/examples/cloud_monitoring/README.rst @@ -0,0 +1,35 @@ +Cloud Monitoring Exporter Example +================================= + +These examples show how to use OpenTelemetry to send metrics data to Cloud Monitoring. + + +Basic Example +------------- + +To use this exporter you first need to: + * `Create a Google Cloud project `_. + * Enable the Cloud Monitoring API (aka Stackdriver Monitoring API) in the project `here `_. + * Enable `Default Application Credentials `_. + +* Installation + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + pip install opentelemetry-exporter-cloud-monitoring + +* Run example + +.. code-block:: sh + + python basic_metrics.py + +Viewing Output +-------------------------- + +After running the example: + * Go to the `Cloud Monitoring Metrics Explorer page `_. + * In "Find resource type and metric" enter "OpenTelemetry/request_counter". + * You can filter by labels and change the graphical output here as well. diff --git a/docs/examples/cloud_monitoring/basic_metrics.py b/docs/examples/cloud_monitoring/basic_metrics.py new file mode 100644 index 0000000000..e0ceb420b6 --- /dev/null +++ b/docs/examples/cloud_monitoring/basic_metrics.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +from opentelemetry import metrics +from opentelemetry.exporter.cloud_monitoring import ( + CloudMonitoringMetricsExporter, +) +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export.controller import PushController + +meter = metrics.get_meter(__name__, True) + +# Gather and export metrics every 5 seconds +controller = PushController( + meter=meter, exporter=CloudMonitoringMetricsExporter(), interval=5 +) + +requests_counter = meter.create_metric( + name="request_counter", + description="number of requests", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment"), +) + +staging_labels = {"environment": "staging"} + +for i in range(20): + requests_counter.add(25, staging_labels) + time.sleep(10) diff --git a/docs/ext/cloud_monitoring/cloud_monitoring.rst b/docs/ext/cloud_monitoring/cloud_monitoring.rst new file mode 100644 index 0000000000..a3a4f5660a --- /dev/null +++ b/docs/ext/cloud_monitoring/cloud_monitoring.rst @@ -0,0 +1,7 @@ +OpenTelemetry Cloud Monitoring Exporter +======================================= + +.. automodule:: opentelemetry.exporter.cloud_monitoring + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/ext/opentelemetry-exporter-cloud-monitoring/README.rst b/ext/opentelemetry-exporter-cloud-monitoring/README.rst new file mode 100644 index 0000000000..f1fd52528c --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-monitoring/README.rst @@ -0,0 +1,18 @@ +OpenTelemetry Cloud Monitoring Exporters +======================================== + +This library provides classes for exporting metrics data to Google Cloud Monitoring. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-cloud-monitoring + +References +---------- + +* `OpenTelemetry Cloud Monitoring Exporter `_ +* `Cloud Monitoring `_ +* `OpenTelemetry Project `_ \ No newline at end of file diff --git a/ext/opentelemetry-exporter-cloud-monitoring/setup.cfg b/ext/opentelemetry-exporter-cloud-monitoring/setup.cfg new file mode 100644 index 0000000000..37665ee48b --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-monitoring/setup.cfg @@ -0,0 +1,48 @@ + +# Copyright OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-exporter-cloud-monitoring +description = Cloud Monitoring integration for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-exporter-cloud-monitoring +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api + opentelemetry-sdk + google-cloud-monitoring + +[options.packages.find] +where = src \ No newline at end of file diff --git a/ext/opentelemetry-exporter-cloud-monitoring/setup.py b/ext/opentelemetry-exporter-cloud-monitoring/setup.py new file mode 100644 index 0000000000..0ca88bc330 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-monitoring/setup.py @@ -0,0 +1,31 @@ +# Copyright OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "exporter", + "cloud_monitoring", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py new file mode 100644 index 0000000000..6d6af26677 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py @@ -0,0 +1,169 @@ +import logging +from typing import Optional, Sequence + +import google.auth +from google.api.label_pb2 import LabelDescriptor +from google.api.metric_pb2 import MetricDescriptor +from google.cloud.monitoring_v3 import MetricServiceClient +from google.cloud.monitoring_v3.proto.metric_pb2 import TimeSeries + +from opentelemetry.sdk.metrics.export import ( + MetricRecord, + MetricsExporter, + MetricsExportResult, +) +from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator + +logger = logging.getLogger(__name__) +MAX_BATCH_WRITE = 200 +WRITE_INTERVAL = 10 + + +# pylint is unable to resolve members of protobuf objects +# pylint: disable=no-member +class CloudMonitoringMetricsExporter(MetricsExporter): + """ Implementation of Metrics Exporter to Google Cloud Monitoring""" + + def __init__(self, project_id=None, client=None): + self.client = client or MetricServiceClient() + if not project_id: + _, self.project_id = google.auth.default() + else: + self.project_id = project_id + self.project_name = self.client.project_path(self.project_id) + self._metric_descriptors = {} + self._last_updated = {} + + def _add_resource_info(self, series: TimeSeries) -> None: + """Add Google resource specific information (e.g. instance id, region). + + Args: + series: ProtoBuf TimeSeries + """ + # TODO: Leverage this better + + def _batch_write(self, series: TimeSeries) -> None: + """ Cloud Monitoring allows writing up to 200 time series at once + + :param series: ProtoBuf TimeSeries + :return: + """ + write_ind = 0 + while write_ind < len(series): + self.client.create_time_series( + self.project_name, + series[write_ind : write_ind + MAX_BATCH_WRITE], + ) + write_ind += MAX_BATCH_WRITE + + def _get_metric_descriptor( + self, record: MetricRecord + ) -> Optional[MetricDescriptor]: + """ We can map Metric to MetricDescriptor using Metric.name or + MetricDescriptor.type. We create the MetricDescriptor if it doesn't + exist already and cache it. Note that recreating MetricDescriptors is + a no-op if it already exists. + + :param record: + :return: + """ + descriptor_type = "custom.googleapis.com/OpenTelemetry/{}".format( + record.metric.name + ) + if descriptor_type in self._metric_descriptors: + return self._metric_descriptors[descriptor_type] + descriptor = { + "name": None, + "type": descriptor_type, + "display_name": record.metric.name, + "description": record.metric.description, + "labels": [], + } + for key, value in record.labels: + if isinstance(value, str): + descriptor["labels"].append( + LabelDescriptor(key=key, value_type="STRING") + ) + elif isinstance(value, bool): + descriptor["labels"].append( + LabelDescriptor(key=key, value_type="BOOL") + ) + elif isinstance(value, int): + descriptor["labels"].append( + LabelDescriptor(key=key, value_type="INT64") + ) + else: + logger.warning( + "Label value %s is not a string, bool or integer", value + ) + if isinstance(record.aggregator, CounterAggregator): + descriptor["metric_kind"] = MetricDescriptor.MetricKind.GAUGE + else: + logger.warning( + "Unsupported aggregation type %s, ignoring it", + type(record.aggregator).__name__, + ) + return None + if record.metric.value_type == int: + descriptor["value_type"] = MetricDescriptor.ValueType.INT64 + elif record.metric.value_type == float: + descriptor["value_type"] = MetricDescriptor.ValueType.DOUBLE + proto_descriptor = MetricDescriptor(**descriptor) + try: + descriptor = self.client.create_metric_descriptor( + self.project_name, proto_descriptor + ) + # pylint: disable=broad-except + except Exception as ex: + logger.error( + "Failed to create metric descriptor %s", + proto_descriptor, + exc_info=ex, + ) + return None + self._metric_descriptors[descriptor_type] = descriptor + return descriptor + + def export( + self, metric_records: Sequence[MetricRecord] + ) -> "MetricsExportResult": + all_series = [] + for record in metric_records: + metric_descriptor = self._get_metric_descriptor(record) + if not metric_descriptor: + continue + + series = TimeSeries() + self._add_resource_info(series) + series.metric.type = metric_descriptor.type + for key, value in record.labels: + series.metric.labels[key] = str(value) + + point = series.points.add() + if record.metric.value_type == int: + point.value.int64_value = record.aggregator.checkpoint + elif record.metric.value_type == float: + point.value.double_value = record.aggregator.checkpoint + seconds, nanos = divmod( + record.aggregator.last_update_timestamp, 1e9 + ) + + # Cloud Monitoring API allows, for any combination of labels and + # metric name, one update per WRITE_INTERVAL seconds + updated_key = (metric_descriptor.type, record.labels) + last_updated_seconds = self._last_updated.get(updated_key, 0) + if seconds <= last_updated_seconds + WRITE_INTERVAL: + continue + self._last_updated[updated_key] = seconds + point.interval.end_time.seconds = int(seconds) + point.interval.end_time.nanos = int(nanos) + all_series.append(series) + try: + self._batch_write(all_series) + # pylint: disable=broad-except + except Exception as ex: + logger.error( + "Error while writing to Cloud Monitoring", exc_info=ex + ) + return MetricsExportResult.FAILURE + return MetricsExportResult.SUCCESS diff --git a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py new file mode 100644 index 0000000000..f83f20e7ba --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py @@ -0,0 +1,15 @@ +# Copyright OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-exporter-cloud-monitoring/tests/__init__.py b/ext/opentelemetry-exporter-cloud-monitoring/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py b/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py new file mode 100644 index 0000000000..d7f98e024a --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py @@ -0,0 +1,285 @@ +# Copyright OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from google.api.label_pb2 import LabelDescriptor +from google.api.metric_pb2 import MetricDescriptor +from google.cloud.monitoring_v3.proto.metric_pb2 import TimeSeries + +from opentelemetry.exporter.cloud_monitoring import ( + MAX_BATCH_WRITE, + WRITE_INTERVAL, + CloudMonitoringMetricsExporter, +) +from opentelemetry.sdk.metrics.export import MetricRecord +from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator + + +class UnsupportedAggregator: + pass + + +class MockMetric: + def __init__(self, name="name", description="description", value_type=int): + self.name = name + self.description = description + self.value_type = value_type + + +# pylint: disable=protected-access +# pylint can't deal with ProtoBuf object members +# pylint: disable=no-member + + +class TestCloudMonitoringMetricsExporter(unittest.TestCase): + def setUp(self): + self.client_patcher = mock.patch( + "opentelemetry.exporter.cloud_monitoring.MetricServiceClient" + ) + self.client_patcher.start() + self.project_id = "PROJECT" + self.project_name = "PROJECT_NAME" + + def tearDown(self): + self.client_patcher.stop() + + def test_constructor_default(self): + exporter = CloudMonitoringMetricsExporter(self.project_id) + self.assertEqual(exporter.project_id, self.project_id) + + def test_constructor_explicit(self): + client = mock.Mock() + exporter = CloudMonitoringMetricsExporter( + self.project_id, client=client + ) + + self.assertIs(exporter.client, client) + self.assertEqual(exporter.project_id, self.project_id) + + def test_batch_write(self): + client = mock.Mock() + exporter = CloudMonitoringMetricsExporter(client=client) + exporter.project_name = self.project_name + exporter._batch_write(range(2 * MAX_BATCH_WRITE + 1)) + client.create_time_series.assert_has_calls( + [ + mock.call(self.project_name, range(MAX_BATCH_WRITE)), + mock.call( + self.project_name, + range(MAX_BATCH_WRITE, 2 * MAX_BATCH_WRITE), + ), + mock.call( + self.project_name, + range(2 * MAX_BATCH_WRITE, 2 * MAX_BATCH_WRITE + 1), + ), + ] + ) + + exporter._batch_write(range(MAX_BATCH_WRITE)) + client.create_time_series.assert_has_calls( + [mock.call(self.project_name, range(MAX_BATCH_WRITE))] + ) + + exporter._batch_write(range(MAX_BATCH_WRITE - 1)) + client.create_time_series.assert_has_calls( + [mock.call(self.project_name, range(MAX_BATCH_WRITE - 1))] + ) + + def test_get_metric_descriptor(self): + client = mock.Mock() + exporter = CloudMonitoringMetricsExporter(client=client) + exporter.project_name = self.project_name + + self.assertIsNone( + exporter._get_metric_descriptor( + MetricRecord(UnsupportedAggregator(), (), MockMetric()) + ) + ) + + record = MetricRecord( + CounterAggregator(), (("label1", "value1"),), MockMetric() + ) + metric_descriptor = exporter._get_metric_descriptor(record) + client.create_metric_descriptor.assert_called_with( + self.project_name, + MetricDescriptor( + **{ + "name": None, + "type": "custom.googleapis.com/OpenTelemetry/name", + "display_name": "name", + "description": "description", + "labels": [ + LabelDescriptor(key="label1", value_type="STRING") + ], + "metric_kind": "GAUGE", + "value_type": "INT64", + } + ), + ) + + # Getting a cached metric descriptor shouldn't use another call + cached_metric_descriptor = exporter._get_metric_descriptor(record) + client.create_metric_descriptor.assert_called_once() + self.assertEqual(metric_descriptor, cached_metric_descriptor) + + # Drop labels with values that aren't string, int or bool + exporter._get_metric_descriptor( + MetricRecord( + CounterAggregator(), + ( + ("label1", "value1"), + ("label2", dict()), + ("label3", 3), + ("label4", False), + ), + MockMetric(name="name2", value_type=float), + ) + ) + client.create_metric_descriptor.assert_called_with( + self.project_name, + MetricDescriptor( + **{ + "name": None, + "type": "custom.googleapis.com/OpenTelemetry/name2", + "display_name": "name2", + "description": "description", + "labels": [ + LabelDescriptor(key="label1", value_type="STRING"), + LabelDescriptor(key="label3", value_type="INT64"), + LabelDescriptor(key="label4", value_type="BOOL"), + ], + "metric_kind": "GAUGE", + "value_type": "DOUBLE", + } + ), + ) + + def test_export(self): + client = mock.Mock() + exporter = CloudMonitoringMetricsExporter(client=client) + exporter.project_name = self.project_name + + exporter.export( + [ + MetricRecord( + UnsupportedAggregator(), + (("label1", "value1"),), + MockMetric(), + ) + ] + ) + client.create_time_series.assert_not_called() + + client.create_metric_descriptor.return_value = MetricDescriptor( + **{ + "name": None, + "type": "custom.googleapis.com/OpenTelemetry/name", + "display_name": "name", + "description": "description", + "labels": [ + LabelDescriptor(key="label1", value_type="STRING"), + LabelDescriptor(key="label2", value_type="INT64"), + ], + "metric_kind": "GAUGE", + "value_type": "DOUBLE", + } + ) + + counter_one = CounterAggregator() + counter_one.checkpoint = 1 + counter_one.last_update_timestamp = (WRITE_INTERVAL + 1) * 1e9 + exporter.export( + [ + MetricRecord( + counter_one, + (("label1", "value1"), ("label2", 1),), + MockMetric(), + ), + MetricRecord( + counter_one, + (("label1", "value2"), ("label2", 2),), + MockMetric(), + ), + ] + ) + series1 = TimeSeries() + series1.metric.type = "custom.googleapis.com/OpenTelemetry/name" + series1.metric.labels["label1"] = "value1" + series1.metric.labels["label2"] = "1" + point = series1.points.add() + point.value.int64_value = 1 + point.interval.end_time.seconds = WRITE_INTERVAL + 1 + point.interval.end_time.nanos = 0 + + series2 = TimeSeries() + series2.metric.type = "custom.googleapis.com/OpenTelemetry/name" + series2.metric.labels["label1"] = "value2" + series2.metric.labels["label2"] = "2" + point = series2.points.add() + point.value.int64_value = 1 + point.interval.end_time.seconds = WRITE_INTERVAL + 1 + point.interval.end_time.nanos = 0 + client.create_time_series.assert_has_calls( + [mock.call(self.project_name, [series1, series2])] + ) + + # Attempting to export too soon after another export with the exact + # same labels leads to it being dropped + + counter_two = CounterAggregator() + counter_two.checkpoint = 1 + counter_two.last_update_timestamp = (WRITE_INTERVAL + 2) * 1e9 + exporter.export( + [ + MetricRecord( + counter_two, + (("label1", "value1"), ("label2", 1),), + MockMetric(), + ), + MetricRecord( + counter_two, + (("label1", "value2"), ("label2", 2),), + MockMetric(), + ), + ] + ) + self.assertEqual(client.create_time_series.call_count, 1) + + # But exporting with different labels is fine + counter_two.checkpoint = 2 + exporter.export( + [ + MetricRecord( + counter_two, + (("label1", "changed_label"), ("label2", 2),), + MockMetric(), + ), + ] + ) + series3 = TimeSeries() + series3.metric.type = "custom.googleapis.com/OpenTelemetry/name" + series3.metric.labels["label1"] = "changed_label" + series3.metric.labels["label2"] = "2" + point = series3.points.add() + point.value.int64_value = 2 + point.interval.end_time.seconds = WRITE_INTERVAL + 2 + point.interval.end_time.nanos = 0 + client.create_time_series.assert_has_calls( + [ + mock.call(self.project_name, [series1, series2]), + mock.call(self.project_name, [series3]), + ] + ) From 5c17a0678b942d2f16982eafd12e05cb939eb2ad Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 9 Jun 2020 15:18:03 -0600 Subject: [PATCH 0395/1517] botocore: Add botocore instrumentation (#689) Adding initial boto core implementation. Co-authored-by: alrex --- docs-requirements.txt | 1 + docs/ext/botocore/botocore.rst | 7 + ext/opentelemetry-ext-botocore/CHANGELOG.md | 5 + ext/opentelemetry-ext-botocore/LICENSE | 201 +++++++++++++++++ ext/opentelemetry-ext-botocore/MANIFEST.in | 9 + ext/opentelemetry-ext-botocore/README.rst | 23 ++ ext/opentelemetry-ext-botocore/setup.cfg | 57 +++++ ext/opentelemetry-ext-botocore/setup.py | 26 +++ .../opentelemetry/ext/botocore/__init__.py | 209 ++++++++++++++++++ .../src/opentelemetry/ext/botocore/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/test_botocore_instrumentation.py | 197 +++++++++++++++++ tox.ini | 8 + 13 files changed, 758 insertions(+) create mode 100644 docs/ext/botocore/botocore.rst create mode 100644 ext/opentelemetry-ext-botocore/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-botocore/LICENSE create mode 100644 ext/opentelemetry-ext-botocore/MANIFEST.in create mode 100644 ext/opentelemetry-ext-botocore/README.rst create mode 100644 ext/opentelemetry-ext-botocore/setup.cfg create mode 100644 ext/opentelemetry-ext-botocore/setup.py create mode 100644 ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py create mode 100644 ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py create mode 100644 ext/opentelemetry-ext-botocore/tests/__init__.py create mode 100644 ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py diff --git a/docs-requirements.txt b/docs-requirements.txt index 23a7047e8a..60f8cdc2cf 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -23,3 +23,4 @@ psutil~=5.7.0 boto~=2.0 google-cloud-trace >=0.23.0 google-cloud-monitoring>=0.36.0 +botocore~=1.0 diff --git a/docs/ext/botocore/botocore.rst b/docs/ext/botocore/botocore.rst new file mode 100644 index 0000000000..43f702a904 --- /dev/null +++ b/docs/ext/botocore/botocore.rst @@ -0,0 +1,7 @@ +OpenTelemetry Botocore Integration +================================== + +.. automodule:: opentelemetry.ext.botocore + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-botocore/CHANGELOG.md b/ext/opentelemetry-ext-botocore/CHANGELOG.md new file mode 100644 index 0000000000..3e04402cea --- /dev/null +++ b/ext/opentelemetry-ext-botocore/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release diff --git a/ext/opentelemetry-ext-botocore/LICENSE b/ext/opentelemetry-ext-botocore/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-botocore/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-botocore/MANIFEST.in b/ext/opentelemetry-ext-botocore/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-botocore/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-botocore/README.rst b/ext/opentelemetry-ext-botocore/README.rst new file mode 100644 index 0000000000..0b50819d32 --- /dev/null +++ b/ext/opentelemetry-ext-botocore/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry Botocore Tracing +============================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-botocore.svg + :target: https://pypi.org/project/opentelemetry-ext-botocore/ + +This library allows tracing requests made by the Botocore library. + +Installation +------------ + +:: + + pip install opentelemetry-ext-botocore + + +References +---------- + +* `OpenTelemetry Botocore Tracing `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-botocore/setup.cfg b/ext/opentelemetry-ext-botocore/setup.cfg new file mode 100644 index 0000000000..2cc134969e --- /dev/null +++ b/ext/opentelemetry-ext-botocore/setup.cfg @@ -0,0 +1,57 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-botocore +description = Botocore tracing for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-botocore +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + botocore ~= 1.0 + opentelemetry-api == 0.9.dev0 + opentelemetry-instrumentation == 0.9.dev0 + +[options.extras_require] +test = + moto ~= 1.0 + opentelemetry-test == 0.9.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + django = opentelemetry.ext.botocore:BotoCoreInstrumentor diff --git a/ext/opentelemetry-ext-botocore/setup.py b/ext/opentelemetry-ext-botocore/setup.py new file mode 100644 index 0000000000..35b47b1b00 --- /dev/null +++ b/ext/opentelemetry-ext-botocore/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "botocore", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py new file mode 100644 index 0000000000..97615d974f --- /dev/null +++ b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py @@ -0,0 +1,209 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Instrument `Botocore`_ to trace service requests. + +There are two options for instrumenting code. The first option is to use the +``opentelemetry-instrument`` executable which will automatically +instrument your Botocore client. The second is to programmatically enable +instrumentation via the following code: + +.. _Botocore: https://pypi.org/project/botocore/ + +Usage +----- + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext.botocore import BotocoreInstrumentor + from opentelemetry.sdk.trace import TracerProvider + import botocore + + trace.set_tracer_provider(TracerProvider()) + + # Instrument Botocore + BotocoreInstrumentor().instrument( + tracer_provider=trace.get_tracer_provider() + ) + + # This will create a span with Botocore-specific attributes + session = botocore.session.get_session() + session.set_credentials( + access_key="access-key", secret_key="secret-key" + ) + ec2 = self.session.create_client("ec2", region_name="us-west-2") + ec2.describe_instances() + +API +--- +""" + +import logging + +from botocore.client import BaseClient +from wrapt import ObjectProxy, wrap_function_wrapper + +from opentelemetry.ext.botocore.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.trace import SpanKind, get_tracer + +logger = logging.getLogger(__name__) + + +class BotocoreInstrumentor(BaseInstrumentor): + """A instrumentor for Botocore + + See `BaseInstrumentor` + """ + + def _instrument(self, **kwargs): + + # FIXME should the tracer provider be accessed via Configuration + # instead? + # pylint: disable=attribute-defined-outside-init + self._tracer = get_tracer( + __name__, __version__, kwargs.get("tracer_provider") + ) + + wrap_function_wrapper( + "botocore.client", + "BaseClient._make_api_call", + self._patched_api_call, + ) + + def _uninstrument(self, **kwargs): + unwrap(BaseClient, "_make_api_call") + + def _patched_api_call(self, original_func, instance, args, kwargs): + + endpoint_name = deep_getattr(instance, "_endpoint._endpoint_prefix") + + with self._tracer.start_as_current_span( + "{}.command".format(endpoint_name), kind=SpanKind.CONSUMER, + ) as span: + + operation = None + if args: + operation = args[0] + span.resource = "%s.%s" % (endpoint_name, operation.lower()) + + else: + span.resource = endpoint_name + + add_span_arg_tags( + span, + endpoint_name, + args, + ("action", "params", "path", "verb"), + {"params", "path", "verb"}, + ) + + region_name = deep_getattr(instance, "meta.region_name") + + meta = { + "aws.agent": "botocore", + "aws.operation": operation, + "aws.region": region_name, + } + for key, value in meta.items(): + span.set_attribute(key, value) + + result = original_func(*args, **kwargs) + + span.set_attribute( + "http.status_code", + result["ResponseMetadata"]["HTTPStatusCode"], + ) + span.set_attribute( + "retry_attempts", result["ResponseMetadata"]["RetryAttempts"], + ) + + return result + + +def unwrap(obj, attr): + function = getattr(obj, attr, None) + if ( + function + and isinstance(function, ObjectProxy) + and hasattr(function, "__wrapped__") + ): + setattr(obj, attr, function.__wrapped__) + + +def add_span_arg_tags(span, endpoint_name, args, args_names, args_traced): + def truncate_arg_value(value, max_len=1024): + """Truncate values which are bytes and greater than `max_len`. + Useful for parameters like "Body" in `put_object` operations. + """ + if isinstance(value, bytes) and len(value) > max_len: + return b"..." + + return value + + if endpoint_name not in {"kms", "sts"}: + tags = dict( + (name, value) + for (name, value) in zip(args_names, args) + if name in args_traced + ) + tags = flatten_dict(tags) + for key, value in { + k: truncate_arg_value(v) + for k, v in tags.items() + if k not in {"s3": ["params.Body"]}.get(endpoint_name, []) + }.items(): + span.set_attribute(key, value) + + +def flatten_dict(dict_, sep=".", prefix=""): + """ + Returns a normalized dict of depth 1 with keys in order of embedding + """ + # adapted from https://stackoverflow.com/a/19647596 + return ( + { + prefix + sep + k if prefix else k: v + for kk, vv in dict_.items() + for k, v in flatten_dict(vv, sep, kk).items() + } + if isinstance(dict_, dict) + else {prefix: dict_} + ) + + +def deep_getattr(obj, attr_string, default=None): + """ + Returns the attribute of ``obj`` at the dotted path given by + ``attr_string``, if no such attribute is reachable, returns ``default``. + + >>> deep_getattr(cass, "cluster") + >> deep_getattr(cass, "cluster.metadata.partitioner") + u"org.apache.cassandra.dht.Murmur3Partitioner" + + >>> deep_getattr(cass, "i.dont.exist", default="default") + "default" + """ + attrs = attr_string.split(".") + for attr in attrs: + try: + obj = getattr(obj, attr) + except AttributeError: + return default + + return obj diff --git a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py new file mode 100644 index 0000000000..603bf0b7e5 --- /dev/null +++ b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-botocore/tests/__init__.py b/ext/opentelemetry-ext-botocore/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py b/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py new file mode 100644 index 0000000000..56b136ea29 --- /dev/null +++ b/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py @@ -0,0 +1,197 @@ +import botocore.session +from botocore.exceptions import ParamValidationError + +from moto import ( # pylint: disable=import-error + mock_ec2, + mock_kinesis, + mock_kms, + mock_lambda, + mock_s3, + mock_sqs, +) +from opentelemetry.ext.botocore import BotocoreInstrumentor +from opentelemetry.test.test_base import TestBase + + +def assert_span_http_status_code(span, code): + """Assert on the span"s "http.status_code" tag""" + tag = span.attributes["http.status_code"] + assert tag == code, "%r != %r" % (tag, code) + + +class TestBotocoreInstrumentor(TestBase): + """Botocore integration testsuite""" + + def setUp(self): + super().setUp() + BotocoreInstrumentor().instrument() + + self.session = botocore.session.get_session() + self.session.set_credentials( + access_key="access-key", secret_key="secret-key" + ) + + def tearDown(self): + super().tearDown() + BotocoreInstrumentor().uninstrument() + + @mock_ec2 + def test_traced_client(self): + ec2 = self.session.create_client("ec2", region_name="us-west-2") + + ec2.describe_instances() + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(len(spans), 1) + self.assertEqual(span.attributes["aws.agent"], "botocore") + self.assertEqual(span.attributes["aws.region"], "us-west-2") + self.assertEqual(span.attributes["aws.operation"], "DescribeInstances") + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "ec2.describeinstances") + self.assertEqual(span.name, "ec2.command") + + @mock_ec2 + def test_traced_client_analytics(self): + ec2 = self.session.create_client("ec2", region_name="us-west-2") + ec2.describe_instances() + + spans = self.memory_exporter.get_finished_spans() + assert spans + + @mock_s3 + def test_s3_client(self): + s3 = self.session.create_client("s3", region_name="us-west-2") + + s3.list_buckets() + s3.list_buckets() + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(len(spans), 2) + self.assertEqual(span.attributes["aws.operation"], "ListBuckets") + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "s3.listbuckets") + + # testing for span error + self.memory_exporter.get_finished_spans() + with self.assertRaises(ParamValidationError): + s3.list_objects(bucket="mybucket") + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[2] + self.assertEqual(span.resource, "s3.listobjects") + + @mock_s3 + def test_s3_put(self): + params = dict(Key="foo", Bucket="mybucket", Body=b"bar") + s3 = self.session.create_client("s3", region_name="us-west-2") + s3.create_bucket(Bucket="mybucket") + s3.put_object(**params) + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(len(spans), 2) + self.assertEqual(span.attributes["aws.operation"], "CreateBucket") + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "s3.createbucket") + self.assertEqual(spans[1].attributes["aws.operation"], "PutObject") + self.assertEqual(spans[1].resource, "s3.putobject") + self.assertEqual(spans[1].attributes["params.Key"], str(params["Key"])) + self.assertEqual( + spans[1].attributes["params.Bucket"], str(params["Bucket"]) + ) + self.assertTrue("params.Body" not in spans[1].attributes.keys()) + + @mock_sqs + def test_sqs_client(self): + sqs = self.session.create_client("sqs", region_name="us-east-1") + + sqs.list_queues() + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(len(spans), 1) + self.assertEqual(span.attributes["aws.region"], "us-east-1") + self.assertEqual(span.attributes["aws.operation"], "ListQueues") + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "sqs.listqueues") + + @mock_kinesis + def test_kinesis_client(self): + kinesis = self.session.create_client( + "kinesis", region_name="us-east-1" + ) + + kinesis.list_streams() + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(len(spans), 1) + self.assertEqual(span.attributes["aws.region"], "us-east-1") + self.assertEqual(span.attributes["aws.operation"], "ListStreams") + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "kinesis.liststreams") + + @mock_kinesis + def test_unpatch(self): + kinesis = self.session.create_client( + "kinesis", region_name="us-east-1" + ) + + BotocoreInstrumentor().uninstrument() + + kinesis.list_streams() + spans = self.memory_exporter.get_finished_spans() + assert not spans, spans + + @mock_sqs + def test_double_patch(self): + sqs = self.session.create_client("sqs", region_name="us-east-1") + + BotocoreInstrumentor().instrument() + BotocoreInstrumentor().instrument() + + sqs.list_queues() + + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 1) + + @mock_lambda + def test_lambda_client(self): + lamb = self.session.create_client("lambda", region_name="us-east-1") + + lamb.list_functions() + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(len(spans), 1) + self.assertEqual(span.attributes["aws.region"], "us-east-1") + self.assertEqual(span.attributes["aws.operation"], "ListFunctions") + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "lambda.listfunctions") + + @mock_kms + def test_kms_client(self): + kms = self.session.create_client("kms", region_name="us-east-1") + + kms.list_keys(Limit=21) + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(len(spans), 1) + self.assertEqual(span.attributes["aws.region"], "us-east-1") + self.assertEqual(span.attributes["aws.operation"], "ListKeys") + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "kms.listkeys") + + # checking for protection on sts against security leak + self.assertTrue("params" not in span.attributes.keys()) diff --git a/tox.ini b/tox.ini index 332662a5c6..de7fe82ca1 100644 --- a/tox.ini +++ b/tox.ini @@ -32,6 +32,10 @@ envlist = py3{5,6,7,8}-test-ext-aiohttp-client pypy3-test-ext-aiohttp-client + ; opentelemetry-ext-botocore + py3{6,7,8}-test-ext-botocore + pypy3-test-ext-botocore + ; opentelemetry-ext-django py3{4,5,6,7,8}-test-ext-django pypy3-test-ext-django @@ -172,6 +176,7 @@ changedir = test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests test-ext-boto: ext/opentelemetry-ext-boto/tests + test-ext-botocore: ext/opentelemetry-ext-botocore/tests test-ext-flask: ext/opentelemetry-ext-flask/tests test-example-app: docs/examples/opentelemetry-example-app/tests test-getting-started: docs/getting_started/tests @@ -205,6 +210,9 @@ commands_pre = flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] + botocore: pip install {toxinidir}/opentelemetry-instrumentation + botocore: pip install {toxinidir}/ext/opentelemetry-ext-botocore[test] + dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi[test] django: pip install {toxinidir}/ext/opentelemetry-ext-django[test] From 31e29fc680cf7dbe72224bc477cd615f70196be1 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 9 Jun 2020 15:53:52 -0700 Subject: [PATCH 0396/1517] Add SumObserver and UpDownSumObserver instruments (#789) --- docs/examples/basic_meter/observer.py | 1 + opentelemetry-api/CHANGELOG.md | 2 + .../src/opentelemetry/metrics/__init__.py | 24 ++++ .../tests/metrics/test_metrics.py | 14 +- opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/metrics/__init__.py | 74 ++++++++-- .../sdk/metrics/export/aggregate.py | 28 ++++ .../sdk/metrics/export/batcher.py | 5 + .../tests/metrics/test_metrics.py | 132 ++++++++++++++++++ 9 files changed, 267 insertions(+), 15 deletions(-) diff --git a/docs/examples/basic_meter/observer.py b/docs/examples/basic_meter/observer.py index b61b9e4db8..d3aa02168d 100644 --- a/docs/examples/basic_meter/observer.py +++ b/docs/examples/basic_meter/observer.py @@ -60,6 +60,7 @@ def get_ram_usage_callback(observer): description="RAM memory usage", unit="1", value_type=float, + observer_type=ValueObserver, label_keys=(), ) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index b1610ab2c3..670e3d7a7f 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -10,6 +10,8 @@ ([#552](https://github.com/open-telemetry/opentelemetry-python/pull/552)) - Rename Observer to ValueObserver ([#764](https://github.com/open-telemetry/opentelemetry-python/pull/764)) +- Add SumObserver and UpDownSumObserver in metrics + ([#789](https://github.com/open-telemetry/opentelemetry-python/pull/789)) ## 0.8b0 diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index da47356e05..569930d6f3 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -189,6 +189,30 @@ def observe(self, value: ValueT, labels: Dict[str, str]) -> None: """ +class SumObserver(Observer): + """No-op implementation of ``SumObserver``.""" + + def observe(self, value: ValueT, labels: Dict[str, str]) -> None: + """Captures ``value`` to the sumobserver. + + Args: + value: The value to capture to this sumobserver metric. + labels: Labels associated to ``value``. + """ + + +class UpDownSumObserver(Observer): + """No-op implementation of ``UpDownSumObserver``.""" + + def observe(self, value: ValueT, labels: Dict[str, str]) -> None: + """Captures ``value`` to the updownsumobserver. + + Args: + value: The value to capture to this updownsumobserver metric. + labels: Labels associated to ``value``. + """ + + class ValueObserver(Observer): """No-op implementation of ``ValueObserver``.""" diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 897c7492e4..b3cbdefd15 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -56,6 +56,18 @@ def test_bound_valuerecorder(self): bound_valuerecorder = metrics.BoundValueRecorder() bound_valuerecorder.record(1) - def test_observer(self): + def test_default_observer(self): observer = metrics.DefaultObserver() observer.observe(1, {}) + + def test_sum_observer(self): + observer = metrics.SumObserver() + observer.observe(1, {}) + + def test_updown_sum_observer(self): + observer = metrics.UpDownSumObserver() + observer.observe(1, {}) + + def test_value_observer(self): + observer = metrics.ValueObserver() + observer.observe(1, {}) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 713580dbd6..23007dde5a 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -12,6 +12,8 @@ ([#775](https://github.com/open-telemetry/opentelemetry-python/pull/775)) - Rename Observer to ValueObserver ([#764](https://github.com/open-telemetry/opentelemetry-python/pull/764)) +- Add SumObserver, UpDownSumObserver and LastValueAggregator in metrics + ([#789](https://github.com/open-telemetry/opentelemetry-python/pull/789)) ## 0.8b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 507e00d8ea..7156f68c16 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -105,12 +105,16 @@ def record(self, value: metrics_api.ValueT) -> None: class Metric(metrics_api.Metric): - """Base class for all metric types. + """Base class for all synchronous metric types. - Also known as metric instrument. This is the class that is used to - represent a metric that is to be continuously recorded and tracked. Each - metric has a set of bound metrics that are created from the metric. See - `BaseBoundInstrument` for information on bound metric instruments. + This is the class that is used to represent a metric that is to be + synchronously recorded and tracked. Synchronous instruments are called + inside a request, meaning they have an associated distributed context + (i.e. Span context, correlation context). Multiple metric events may occur + for a synchronous instrument within a give collection interval. + + Each metric has a set of bound metrics that are created from the metric. + See `BaseBoundInstrument` for information on bound metric instruments. """ BOUND_INSTR_TYPE = BaseBoundInstrument @@ -190,8 +194,14 @@ def record( UPDATE_FUNCTION = record -class ValueObserver(metrics_api.ValueObserver): - """See `opentelemetry.metrics.ValueObserver`.""" +class Observer(metrics_api.Observer): + """Base class for all asynchronous metric types. + + Also known as Observers, observer metric instruments are asynchronous in + that they are reported by a callback, once per collection interval, and + lack context. They are permitted to report only one value per distinct + label set per period. + """ def __init__( self, @@ -218,15 +228,10 @@ def __init__( def observe( self, value: metrics_api.ValueT, labels: Dict[str, str] ) -> None: - if not self.enabled: - return - if not isinstance(value, self.value_type): - logger.warning( - "Invalid value passed for %s.", self.value_type.__name__ - ) + key = get_labels_as_key(labels) + if not self._validate_observe(value, key): return - key = get_labels_as_key(labels) if key not in self.aggregators: # TODO: how to cleanup aggregators? self.aggregators[key] = self.meter.batcher.aggregator_for( @@ -235,6 +240,20 @@ def observe( aggregator = self.aggregators[key] aggregator.update(value) + # pylint: disable=W0613 + def _validate_observe( + self, value: metrics_api.ValueT, key: Tuple[Tuple[str, str]], + ) -> bool: + if not self.enabled: + return False + if not isinstance(value, self.value_type): + logger.warning( + "Invalid value passed for %s.", self.value_type.__name__ + ) + return False + + return True + def run(self) -> bool: try: self.callback(self) @@ -252,6 +271,33 @@ def __repr__(self): ) +class SumObserver(Observer, metrics_api.SumObserver): + """See `opentelemetry.metrics.SumObserver`.""" + + def _validate_observe( + self, value: metrics_api.ValueT, key: Tuple[Tuple[str, str]], + ) -> bool: + if not super()._validate_observe(value, key): + return False + # Must be non-decreasing because monotonic + if ( + key in self.aggregators + and self.aggregators[key].current is not None + ): + if value < self.aggregators[key].current: + logger.warning("Value passed must be non-decreasing.") + return False + return True + + +class UpDownSumObserver(Observer, metrics_api.UpDownSumObserver): + """See `opentelemetry.metrics.UpDownSumObserver`.""" + + +class ValueObserver(Observer, metrics_api.ValueObserver): + """See `opentelemetry.metrics.ValueObserver`.""" + + class Record: """Container class used for processing in the `Batcher`""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 1745d854e9..ad728d8c50 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -125,6 +125,34 @@ def merge(self, other): ) +class LastValueAggregator(Aggregator): + """Aggregator that stores last value results.""" + + def __init__(self): + super().__init__() + self._lock = threading.Lock() + self.last_update_timestamp = None + + def update(self, value): + with self._lock: + self.current = value + self.last_update_timestamp = time_ns() + + def take_checkpoint(self): + with self._lock: + self.checkpoint = self.current + self.current = None + + def merge(self, other): + last = self.checkpoint.last + self.last_update_timestamp = get_latest_timestamp( + self.last_update_timestamp, other.last_update_timestamp + ) + if self.last_update_timestamp == other.last_update_timestamp: + last = other.checkpoint.last + self.checkpoint = last + + class ValueObserverAggregator(Aggregator): """Same as MinMaxSumCount but also with last value.""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index db3675ecd6..c0405d1ffb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -18,6 +18,8 @@ from opentelemetry.metrics import ( Counter, InstrumentT, + SumObserver, + UpDownSumObserver, ValueObserver, ValueRecorder, ) @@ -25,6 +27,7 @@ from opentelemetry.sdk.metrics.export.aggregate import ( Aggregator, CounterAggregator, + LastValueAggregator, MinMaxSumCountAggregator, ValueObserverAggregator, ) @@ -54,6 +57,8 @@ def aggregator_for(self, instrument_type: Type[InstrumentT]) -> Aggregator: # pylint:disable=R0201 if issubclass(instrument_type, Counter): return CounterAggregator() + if issubclass(instrument_type, (SumObserver, UpDownSumObserver)): + return LastValueAggregator() if issubclass(instrument_type, ValueRecorder): return MinMaxSumCountAggregator() if issubclass(instrument_type, ValueObserver): diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 4c2d691549..02cf2c9354 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -290,6 +290,138 @@ def test_record(self): ) +class TestSumObserver(unittest.TestCase): + def test_observe(self): + meter = metrics.MeterProvider().get_meter(__name__) + observer = metrics.SumObserver( + None, "name", "desc", "unit", int, meter, ("key",), True + ) + labels = {"key": "value"} + key_labels = tuple(sorted(labels.items())) + values = (37, 42, 60, 100) + for val in values: + observer.observe(val, labels) + + self.assertEqual(observer.aggregators[key_labels].current, values[-1]) + + def test_observe_disabled(self): + meter = metrics.MeterProvider().get_meter(__name__) + observer = metrics.SumObserver( + None, "name", "desc", "unit", int, meter, ("key",), False + ) + labels = {"key": "value"} + observer.observe(37, labels) + self.assertEqual(len(observer.aggregators), 0) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_observe_incorrect_type(self, logger_mock): + meter = metrics.MeterProvider().get_meter(__name__) + observer = metrics.SumObserver( + None, "name", "desc", "unit", int, meter, ("key",), True + ) + labels = {"key": "value"} + observer.observe(37.0, labels) + self.assertEqual(len(observer.aggregators), 0) + self.assertTrue(logger_mock.warning.called) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_observe_non_decreasing_error(self, logger_mock): + meter = metrics.MeterProvider().get_meter(__name__) + observer = metrics.SumObserver( + None, "name", "desc", "unit", int, meter, ("key",), True + ) + labels = {"key": "value"} + observer.observe(37, labels) + observer.observe(14, labels) + self.assertEqual(len(observer.aggregators), 1) + self.assertTrue(logger_mock.warning.called) + + def test_run(self): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = mock.Mock() + observer = metrics.SumObserver( + callback, "name", "desc", "unit", int, meter, (), True + ) + + self.assertTrue(observer.run()) + callback.assert_called_once_with(observer) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_run_exception(self, logger_mock): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = mock.Mock() + callback.side_effect = Exception("We have a problem!") + + observer = metrics.SumObserver( + callback, "name", "desc", "unit", int, meter, (), True + ) + + self.assertFalse(observer.run()) + self.assertTrue(logger_mock.warning.called) + + +class TestUpDownSumObserver(unittest.TestCase): + def test_observe(self): + meter = metrics.MeterProvider().get_meter(__name__) + observer = metrics.UpDownSumObserver( + None, "name", "desc", "unit", int, meter, ("key",), True + ) + labels = {"key": "value"} + key_labels = tuple(sorted(labels.items())) + values = (37, 42, 14, 30) + for val in values: + observer.observe(val, labels) + + self.assertEqual(observer.aggregators[key_labels].current, values[-1]) + + def test_observe_disabled(self): + meter = metrics.MeterProvider().get_meter(__name__) + observer = metrics.UpDownSumObserver( + None, "name", "desc", "unit", int, meter, ("key",), False + ) + labels = {"key": "value"} + observer.observe(37, labels) + self.assertEqual(len(observer.aggregators), 0) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_observe_incorrect_type(self, logger_mock): + meter = metrics.MeterProvider().get_meter(__name__) + observer = metrics.UpDownSumObserver( + None, "name", "desc", "unit", int, meter, ("key",), True + ) + labels = {"key": "value"} + observer.observe(37.0, labels) + self.assertEqual(len(observer.aggregators), 0) + self.assertTrue(logger_mock.warning.called) + + def test_run(self): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = mock.Mock() + observer = metrics.UpDownSumObserver( + callback, "name", "desc", "unit", int, meter, (), True + ) + + self.assertTrue(observer.run()) + callback.assert_called_once_with(observer) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_run_exception(self, logger_mock): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = mock.Mock() + callback.side_effect = Exception("We have a problem!") + + observer = metrics.UpDownSumObserver( + callback, "name", "desc", "unit", int, meter, (), True + ) + + self.assertFalse(observer.run()) + self.assertTrue(logger_mock.warning.called) + + class TestValueObserver(unittest.TestCase): def test_observe(self): meter = metrics.MeterProvider().get_meter(__name__) From 672a89c7c6b2b302216cfdeb05b4105ca8db67c6 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 9 Jun 2020 16:33:51 -0700 Subject: [PATCH 0397/1517] Add start_pipeline to MeterProvider in SDK, atexit moved to MeterProvider (#791) --- docs/examples/basic_meter/basic_metrics.py | 17 +++--- .../basic_meter/calling_conventions.py | 6 +-- docs/examples/basic_meter/observer.py | 5 +- .../cloud_monitoring/basic_metrics.py | 10 ++-- .../opencensus-exporter-metrics/collector.py | 3 +- .../opentelemetry/ext/prometheus/__init__.py | 8 ++- .../ext/system_metrics/__init__.py | 3 ++ opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/metrics/__init__.py | 52 +++++++++++++++++-- .../sdk/metrics/export/controller.py | 25 ++++----- .../tests/metrics/export/test_export.py | 1 - .../tests/metrics/test_metrics.py | 24 +++++++++ 12 files changed, 111 insertions(+), 45 deletions(-) diff --git a/docs/examples/basic_meter/basic_metrics.py b/docs/examples/basic_meter/basic_metrics.py index b9ff8d8741..e65aa788b8 100644 --- a/docs/examples/basic_meter/basic_metrics.py +++ b/docs/examples/basic_meter/basic_metrics.py @@ -26,9 +26,6 @@ from opentelemetry import metrics from opentelemetry.sdk.metrics import Counter, MeterProvider, ValueRecorder from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.controller import PushController - -stateful = True print( "Starting example, values will be printed to the console every 5 seconds." @@ -37,7 +34,11 @@ # Stateful determines whether how metrics are collected: if true, metrics # accumulate over the process lifetime. If false, metrics are reset at the # beginning of each collection interval. -metrics.set_meter_provider(MeterProvider(stateful)) +stateful = True + +# Sets the global MeterProvider instance +metrics.set_meter_provider(MeterProvider()) + # The Meter is responsible for creating and recording metrics. Each meter has a # unique name, which we set as the module's name here. meter = metrics.get_meter(__name__) @@ -45,9 +46,9 @@ # Exporter to export metrics to the console exporter = ConsoleMetricsExporter() -# A PushController collects metrics created from meter and exports it via the -# exporter every interval -controller = PushController(meter=meter, exporter=exporter, interval=5) +# start_pipeline will notify the MeterProvider to begin collecting/exporting +# metrics with the given meter, exporter and interval in seconds +metrics.get_meter_provider().start_pipeline(meter, exporter, 5) # Metric instruments allow to capture measurements requests_counter = meter.create_metric( @@ -77,7 +78,7 @@ # Update the metric instruments using the direct calling convention requests_counter.add(25, staging_labels) requests_size.record(100, staging_labels) -time.sleep(5) +time.sleep(10) requests_counter.add(50, staging_labels) requests_size.record(5000, staging_labels) diff --git a/docs/examples/basic_meter/calling_conventions.py b/docs/examples/basic_meter/calling_conventions.py index f8cc3dddbb..3615f60d7d 100644 --- a/docs/examples/basic_meter/calling_conventions.py +++ b/docs/examples/basic_meter/calling_conventions.py @@ -21,13 +21,11 @@ from opentelemetry import metrics from opentelemetry.sdk.metrics import Counter, MeterProvider, ValueRecorder from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.controller import PushController # Use the meter type provided by the SDK package metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__) -exporter = ConsoleMetricsExporter() -controller = PushController(meter=meter, exporter=exporter, interval=5) +metrics.get_meter_provider().start_pipeline(meter, ConsoleMetricsExporter(), 5) requests_counter = meter.create_metric( name="requests", @@ -62,7 +60,7 @@ # You can record metrics directly using the metric instrument. You pass in # labels that you would like to record for. requests_counter.add(25, labels) -time.sleep(5) +time.sleep(10) print("Updating using a bound instrument...") # You can record metrics with bound metric instruments. Bound metric diff --git a/docs/examples/basic_meter/observer.py b/docs/examples/basic_meter/observer.py index d3aa02168d..076c416c0a 100644 --- a/docs/examples/basic_meter/observer.py +++ b/docs/examples/basic_meter/observer.py @@ -21,13 +21,10 @@ from opentelemetry import metrics from opentelemetry.sdk.metrics import MeterProvider, ValueObserver from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher -from opentelemetry.sdk.metrics.export.controller import PushController metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__) -exporter = ConsoleMetricsExporter() -controller = PushController(meter=meter, exporter=exporter, interval=2) +metrics.get_meter_provider().start_pipeline(meter, ConsoleMetricsExporter(), 5) # Callback to gather cpu usage diff --git a/docs/examples/cloud_monitoring/basic_metrics.py b/docs/examples/cloud_monitoring/basic_metrics.py index e0ceb420b6..fa00fc068b 100644 --- a/docs/examples/cloud_monitoring/basic_metrics.py +++ b/docs/examples/cloud_monitoring/basic_metrics.py @@ -20,13 +20,11 @@ CloudMonitoringMetricsExporter, ) from opentelemetry.sdk.metrics import Counter, MeterProvider -from opentelemetry.sdk.metrics.export.controller import PushController -meter = metrics.get_meter(__name__, True) - -# Gather and export metrics every 5 seconds -controller = PushController( - meter=meter, exporter=CloudMonitoringMetricsExporter(), interval=5 +metrics.set_meter_provider(MeterProvider()) +meter = metrics.get_meter(__name__) +metrics.get_meter_provider().start_pipeline( + meter, CloudMonitoringMetricsExporter(), 5 ) requests_counter = meter.create_metric( diff --git a/docs/examples/opencensus-exporter-metrics/collector.py b/docs/examples/opencensus-exporter-metrics/collector.py index 89dabd12ea..725f07b77a 100644 --- a/docs/examples/opencensus-exporter-metrics/collector.py +++ b/docs/examples/opencensus-exporter-metrics/collector.py @@ -21,7 +21,6 @@ OpenCensusMetricsExporter, ) from opentelemetry.sdk.metrics import Counter, MeterProvider -from opentelemetry.sdk.metrics.export.controller import PushController exporter = OpenCensusMetricsExporter( service_name="basic-service", endpoint="localhost:55678" @@ -29,7 +28,7 @@ metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__) -controller = PushController(meter, exporter, 5) +metrics.get_meter_provider().start_pipeline(meter, exporter, 5) requests_counter = meter.create_metric( name="requests", diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py index 59ef3f1708..da22042dcc 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py @@ -29,7 +29,6 @@ from opentelemetry import metrics from opentelemetry.ext.prometheus import PrometheusMetricsExporter from opentelemetry.sdk.metrics import Counter, Meter - from opentelemetry.sdk.metrics.export.controller import PushController from prometheus_client import start_http_server # Start Prometheus client @@ -37,13 +36,12 @@ # Meter is responsible for creating and recording metrics metrics.set_meter_provider(MeterProvider()) - meter = metrics.meter() + meter = metrics.get_meter(__name__) # exporter to export metrics to Prometheus prefix = "MyAppPrefix" exporter = PrometheusMetricsExporter(prefix) - # controller collects metrics created from meter and exports it via the - # exporter every interval - controller = PushController(meter, exporter, 5) + # Starts the collect/export pipeline for metrics + metrics.get_meter_provider().start_pipeline(meter, exporter, 5) counter = meter.create_metric( "requests", diff --git a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py index 09c633f14b..10b0979557 100644 --- a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py +++ b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py @@ -28,9 +28,12 @@ .. code:: python + from opentelemetry import metrics from opentelemetry.ext.system_metrics import SystemMetrics + from opentelemetry.sdk.metrics import MeterProvider, from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter + metrics.set_meter_provider(MeterProvider()) exporter = ConsoleMetricsExporter() SystemMetrics(exporter) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 23007dde5a..746f1ab4a5 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -14,6 +14,8 @@ ([#764](https://github.com/open-telemetry/opentelemetry-python/pull/764)) - Add SumObserver, UpDownSumObserver and LastValueAggregator in metrics ([#789](https://github.com/open-telemetry/opentelemetry-python/pull/789)) +- Add start_pipeline to MeterProvider + ([#791](https://github.com/open-telemetry/opentelemetry-python/pull/791)) ## 0.8b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 7156f68c16..e19d33580b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -12,13 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +import atexit import logging import threading from typing import Dict, Sequence, Tuple, Type from opentelemetry import metrics as metrics_api +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricsExporter, + MetricsExporter, +) from opentelemetry.sdk.metrics.export.aggregate import Aggregator from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.controller import PushController from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -449,24 +455,64 @@ class MeterProvider(metrics_api.MeterProvider): Args: stateful: Indicates whether meters created are going to be stateful resource: Resource for this MeterProvider + shutdown_on_exit: Register an atexit hook to shut down when the + application exists """ def __init__( - self, stateful=True, resource: Resource = Resource.create_empty(), + self, + stateful=True, + resource: Resource = Resource.create_empty(), + shutdown_on_exit: bool = True, ): self.stateful = stateful self.resource = resource + self._controllers = [] + self._exporters = set() + self._atexit_handler = None + if shutdown_on_exit: + self._atexit_handler = atexit.register(self.shutdown) def get_meter( self, instrumenting_module_name: str, instrumenting_library_version: str = "", ) -> "metrics_api.Meter": + """See `opentelemetry.metrics.MeterProvider`.get_meter.""" if not instrumenting_module_name: # Reject empty strings too. - raise ValueError("get_meter called with missing module name.") + instrumenting_module_name = "ERROR:MISSING MODULE NAME" + logger.error("get_meter called with missing module name.") return Meter( self, InstrumentationInfo( - instrumenting_module_name, instrumenting_library_version + instrumenting_module_name, instrumenting_library_version, ), ) + + def start_pipeline( + self, + meter: metrics_api.Meter, + exporter: MetricsExporter = None, + interval: float = 15.0, + ) -> None: + """Method to begin the collect/export pipeline. + + Args: + meter: The meter to collect metrics from. + exporter: The exporter to export metrics to. + interval: The collect/export interval in seconds. + """ + if not exporter: + exporter = ConsoleMetricsExporter() + self._exporters.add(exporter) + # TODO: Controller type configurable? + self._controllers.append(PushController(meter, exporter, interval)) + + def shutdown(self) -> None: + for controller in self._controllers: + controller.shutdown() + for exporter in self._exporters: + exporter.shutdown() + if self._atexit_handler is not None: + atexit.unregister(self._atexit_handler) + self._atexit_handler = None diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py index 88abed410a..7448f353c4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py @@ -12,30 +12,35 @@ # See the License for the specific language governing permissions and # limitations under the License. -import atexit import threading from opentelemetry.context import attach, detach, set_value +from opentelemetry.metrics import Meter +from opentelemetry.sdk.metrics.export import MetricsExporter class PushController(threading.Thread): - """A push based controller, used for exporting. + """A push based controller, used for collecting and exporting. Uses a worker thread that periodically collects metrics for exporting, exports them and performs some post-processing. + + Args: + meter: The meter used to collect metrics. + exporter: The exporter used to export metrics. + interval: The collect/export interval in seconds. """ daemon = True - def __init__(self, meter, exporter, interval, shutdown_on_exit=True): + def __init__( + self, meter: Meter, exporter: MetricsExporter, interval: float + ): super().__init__() self.meter = meter self.exporter = exporter self.interval = interval self.finished = threading.Event() - self._atexit_handler = None - if shutdown_on_exit: - self._atexit_handler = atexit.register(self.shutdown) self.start() def run(self): @@ -46,17 +51,13 @@ def shutdown(self): self.finished.set() # Run one more collection pass to flush metrics batched in the meter self.tick() - self.exporter.shutdown() - if self._atexit_handler is not None: - atexit.unregister(self._atexit_handler) - self._atexit_handler = None def tick(self): # Collect all of the meter's metrics to be exported self.meter.collect() + # Export the collected metrics token = attach(set_value("suppress_instrumentation", True)) - # Export the given metrics in the batcher self.exporter.export(self.meter.batcher.checkpoint_set()) detach(token) - # Perform post-exporting logic based on batcher configuration + # Perform post-exporting logic self.meter.batcher.finished_collection() diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 178e41b213..901d5a9404 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -614,7 +614,6 @@ def test_push_controller(self): controller.shutdown() self.assertTrue(controller.finished.isSet()) - exporter.shutdown.assert_any_call() # shutdown should flush the meter self.assertEqual(meter.collect.call_count, 1) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 02cf2c9354..a92af748ac 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -38,6 +38,30 @@ def test_resource_empty(self): # pylint: disable=protected-access self.assertIs(meter.resource, resources._EMPTY_RESOURCE) + def test_start_pipeline(self): + exporter = mock.Mock() + meter_provider = metrics.MeterProvider() + meter = meter_provider.get_meter(__name__) + # pylint: disable=protected-access + meter_provider.start_pipeline(meter, exporter, 6) + try: + self.assertEqual(len(meter_provider._exporters), 1) + self.assertEqual(len(meter_provider._controllers), 1) + finally: + meter_provider.shutdown() + + def test_shutdown(self): + controller = mock.Mock() + exporter = mock.Mock() + meter_provider = metrics.MeterProvider() + # pylint: disable=protected-access + meter_provider._controllers = [controller] + meter_provider._exporters = [exporter] + meter_provider.shutdown() + self.assertEqual(controller.shutdown.call_count, 1) + self.assertEqual(exporter.shutdown.call_count, 1) + self.assertIsNone(meter_provider._atexit_handler) + class TestMeter(unittest.TestCase): def test_extends_api(self): From 507261d0065eaf930dca5ef2393a6f9be4a229ea Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 10 Jun 2020 10:12:41 -0700 Subject: [PATCH 0398/1517] chore: Add majorgreys to approvers (#802) --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 63cba3eb3b..6f1be8fbce 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,8 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): -- [Carlos Alberto Cortez](https://github.com/carlosalberto), LightStep +- [Carlos Alberto Cortez](https://github.com/carlosalberto), Lightstep +- [Tahir H. Butt](https://github.com/majorgreys) DataDog - [Chris Kleinknecht](https://github.com/c24t), Google - [Diego Hurtado](https://github.com/ocelotl) - [Hector Hernandez](https://github.com/hectorhdzg), Microsoft @@ -114,7 +115,7 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-telemetry/teams/python-maintainers)): -- [Alex Boten](https://github.com/codeboten), LightStep +- [Alex Boten](https://github.com/codeboten), Lightstep - [Leighton Chen](https://github.com/lzchen), Microsoft - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Zillow Group From d27979f305394048f797e90ff70ebb45781b456b Mon Sep 17 00:00:00 2001 From: Connor Adams Date: Wed, 10 Jun 2020 17:00:40 -0400 Subject: [PATCH 0399/1517] Instrumentation for Pyramid (#776) Co-authored-by: Yusuke Tsutsumi --- ext/opentelemetry-ext-pyramid/CHANGELOG.md | 5 + ext/opentelemetry-ext-pyramid/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-pyramid/MANIFEST.in | 9 + ext/opentelemetry-ext-pyramid/README.rst | 21 ++ ext/opentelemetry-ext-pyramid/setup.cfg | 59 +++++ ext/opentelemetry-ext-pyramid/setup.py | 26 +++ .../src/opentelemetry/ext/pyramid/__init__.py | 141 ++++++++++++ .../opentelemetry/ext/pyramid/callbacks.py | 172 +++++++++++++++ .../src/opentelemetry/ext/pyramid/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/pyramid_base_test.py | 54 +++++ .../tests/test_automatic.py | 79 +++++++ .../tests/test_programmatic.py | 194 +++++++++++++++++ tox.ini | 11 +- 14 files changed, 985 insertions(+), 2 deletions(-) create mode 100644 ext/opentelemetry-ext-pyramid/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-pyramid/LICENSE create mode 100644 ext/opentelemetry-ext-pyramid/MANIFEST.in create mode 100644 ext/opentelemetry-ext-pyramid/README.rst create mode 100644 ext/opentelemetry-ext-pyramid/setup.cfg create mode 100644 ext/opentelemetry-ext-pyramid/setup.py create mode 100644 ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/__init__.py create mode 100644 ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/callbacks.py create mode 100644 ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py create mode 100644 ext/opentelemetry-ext-pyramid/tests/__init__.py create mode 100644 ext/opentelemetry-ext-pyramid/tests/pyramid_base_test.py create mode 100644 ext/opentelemetry-ext-pyramid/tests/test_automatic.py create mode 100644 ext/opentelemetry-ext-pyramid/tests/test_programmatic.py diff --git a/ext/opentelemetry-ext-pyramid/CHANGELOG.md b/ext/opentelemetry-ext-pyramid/CHANGELOG.md new file mode 100644 index 0000000000..33144da913 --- /dev/null +++ b/ext/opentelemetry-ext-pyramid/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-pyramid/LICENSE b/ext/opentelemetry-ext-pyramid/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-pyramid/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-pyramid/MANIFEST.in b/ext/opentelemetry-ext-pyramid/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-pyramid/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-pyramid/README.rst b/ext/opentelemetry-ext-pyramid/README.rst new file mode 100644 index 0000000000..c7890ad095 --- /dev/null +++ b/ext/opentelemetry-ext-pyramid/README.rst @@ -0,0 +1,21 @@ +OpenTelemetry Pyramid Integration +================================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-pyramid.svg + :target: https://pypi.org/project/opentelemetry-ext-pyramid/ + +Installation +------------ + +:: + + pip install opentelemetry-ext-pyramid + + +References +---------- +* `OpenTelemetry Pyramid Integration `_ +* `OpenTelemetry Project `_ + diff --git a/ext/opentelemetry-ext-pyramid/setup.cfg b/ext/opentelemetry-ext-pyramid/setup.cfg new file mode 100644 index 0000000000..7170bcf582 --- /dev/null +++ b/ext/opentelemetry-ext-pyramid/setup.cfg @@ -0,0 +1,59 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-pyramid +description = OpenTelemetry Pyramid integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-pyramid +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + pyramid >= 1.7 + opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9.dev0 + opentelemetry-ext-wsgi == 0.9.dev0 + wrapt >= 1.0.0, < 2.0.0 + +[options.extras_require] +test = + werkzeug == 0.16.1 + opentelemetry-test == 0.9.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + pyramid = opentelemetry.ext.pyramid:PyramidInstrumentor diff --git a/ext/opentelemetry-ext-pyramid/setup.py b/ext/opentelemetry-ext-pyramid/setup.py new file mode 100644 index 0000000000..175db54ed5 --- /dev/null +++ b/ext/opentelemetry-ext-pyramid/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "pyramid", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/__init__.py b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/__init__.py new file mode 100644 index 0000000000..6f63634bc7 --- /dev/null +++ b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/__init__.py @@ -0,0 +1,141 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Pyramid instrumentation supporting `pyramid`_, it can be enabled by +using ``PyramidInstrumentor``. + +.. _pyramid: https://docs.pylonsproject.org/projects/pyramid/en/latest/ + +Usage +----- + There are two methods to instrument Pyramid: + +Method 1 (Instrument all Configurators): +---------------------------------------- +.. code:: python + + from pyramid.config import Configurator + from opentelemetry.ext.pyramid import PyramidInstrumentor + + PyramidInstrumentor.instrument() + + config = Configurator() + + # use your config as normal + config.add_route('index', '/') + +Method 2 (Instrument one Configurator): +--------------------------------------- +.. code:: python + + from pyramid.config import Configurator + from opentelemetry.ext.pyramid import PyramidInstrumentor + + config = Configurator() + PyramidInstrumentor().instrument_config(config) + + # use your config as normal + config.add_route('index', '/') + +Using ``pyramid.tweens`` settings: +---------------------------------- + If you use Method 2 and then set tweens for your application with the ``pyramid.tweens`` setting, + you need to add ``opentelemetry.ext.pyramid.trace_tween_factory`` explicity to the list, + *as well as* instrumenting the config with `PyramidInstrumentor().instrument_config(config)`. + + For example: +.. code:: python + settings = { + 'pyramid.tweens', 'opentelemetry.ext.pyramid.trace_tween_factory\\nyour_tween_no_1\\nyour_tween_no_2', + } + config = Configurator(settings=settings) + PyramidInstrumentor.instrument_config(config) + + # use your config as normal. + config.add_route('index', '/') +--- +""" + +import typing + +from pyramid.config import Configurator +from pyramid.path import caller_package +from pyramid.settings import aslist +from wrapt import ObjectProxy +from wrapt import wrap_function_wrapper as _wrap + +from opentelemetry.ext.pyramid.callbacks import ( + SETTING_TRACE_ENABLED, + TWEEN_NAME, + trace_tween_factory, +) +from opentelemetry.ext.pyramid.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.trace import TracerProvider, get_tracer + + +def traced_init(wrapped, instance, args, kwargs): + settings = kwargs.get("settings", {}) + tweens = aslist(settings.get("pyramid.tweens", [])) + + if tweens and TWEEN_NAME not in settings: + # pyramid.tweens.EXCVIEW is the name of built-in exception view provided by + # pyramid. We need our tween to be before it, otherwise unhandled + # exceptions will be caught before they reach our tween. + tweens = [TWEEN_NAME] + tweens + + settings["pyramid.tweens"] = "\n".join(tweens) + + kwargs["settings"] = settings + + # `caller_package` works by walking a fixed amount of frames up the stack + # to find the calling package. So if we let the original `__init__` + # function call it, our wrapper will mess things up. + if not kwargs.get("package", None): + # Get the package for the third frame up from this one. + # Default is `level=2` which will give us the package from `wrapt` + # instead of the desired package (the caller) + kwargs["package"] = caller_package(level=3) + + wrapped(*args, **kwargs) + instance.include("opentelemetry.ext.pyramid.callbacks") + + +class PyramidInstrumentor(BaseInstrumentor): + def _instrument(self, **kwargs): + """Integrate with Pyramid Python library. + https://docs.pylonsproject.org/projects/pyramid/en/latest/ + """ + _wrap("pyramid.config", "Configurator.__init__", traced_init) + + def _uninstrument(self, **kwargs): + """"Disable Pyramid instrumentation""" + unwrap(Configurator, "__init__") + + # pylint:disable=no-self-use + def instrument_config(self, config): + """Enable instrumentation in a Pyramid configurator. + + Args: + config: The Configurator to instrument. + + Returns: + An instrumented Configurator. + """ + config.include("opentelemetry.ext.pyramid.callbacks") + + def uninstrument_config(self, config): + config.add_settings({SETTING_TRACE_ENABLED: False}) diff --git a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/callbacks.py b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/callbacks.py new file mode 100644 index 0000000000..e19e447bee --- /dev/null +++ b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/callbacks.py @@ -0,0 +1,172 @@ +from logging import getLogger + +from pyramid.events import BeforeTraversal +from pyramid.httpexceptions import HTTPException +from pyramid.settings import asbool +from pyramid.tweens import EXCVIEW + +import opentelemetry.ext.wsgi as otel_wsgi +from opentelemetry import configuration, context, propagators, trace +from opentelemetry.ext.pyramid.version import __version__ +from opentelemetry.util import disable_trace, time_ns + +TWEEN_NAME = "opentelemetry.ext.pyramid.trace_tween_factory" +SETTING_TRACE_ENABLED = "opentelemetry-pyramid.trace_enabled" + +_ENVIRON_STARTTIME_KEY = "opentelemetry-pyramid.starttime_key" +_ENVIRON_SPAN_KEY = "opentelemetry-pyramid.span_key" +_ENVIRON_ACTIVATION_KEY = "opentelemetry-pyramid.activation_key" +_ENVIRON_ENABLED_KEY = "opentelemetry-pyramid.tracing_enabled_key" +_ENVIRON_TOKEN = "opentelemetry-pyramid.token" + +_logger = getLogger(__name__) + + +def get_excluded_hosts(): + hosts = configuration.Configuration().PYRAMID_EXCLUDED_HOSTS or [] + if hosts: + hosts = str.split(hosts, ",") + return hosts + + +def get_excluded_paths(): + paths = configuration.Configuration().PYRAMID_EXCLUDED_PATHS or [] + if paths: + paths = str.split(paths, ",") + return paths + + +_excluded_hosts = get_excluded_hosts() +_excluded_paths = get_excluded_paths() + + +def includeme(config): + config.add_settings({SETTING_TRACE_ENABLED: True}) + + config.add_subscriber(_before_traversal, BeforeTraversal) + _insert_tween(config) + + +def _insert_tween(config): + settings = config.get_settings() + tweens = settings.get("pyramid.tweens") + # If the list is empty, pyramid does not consider the tweens have been + # set explicitly. And if our tween is already there, nothing to do + if not tweens or not tweens.strip(): + # Add our tween just before the default exception handler + config.add_tween(TWEEN_NAME, over=EXCVIEW) + + +def _before_traversal(event): + request = event.request + environ = request.environ + span_name = otel_wsgi.get_default_span_name(environ) + + enabled = environ.get(_ENVIRON_ENABLED_KEY) + if enabled is None: + _logger.warning( + "Opentelemetry pyramid tween 'opentelemetry.ext.pyramid.trace_tween_factory'" + "was not called. Make sure that the tween is included in 'pyramid.tweens' if" + "the tween list was created manually" + ) + return + + if not enabled: + # Tracing not enabled, return + return + + start_time = environ.get(_ENVIRON_STARTTIME_KEY) + + token = context.attach( + propagators.extract(otel_wsgi.get_header_from_environ, environ) + ) + tracer = trace.get_tracer(__name__, __version__) + + attributes = otel_wsgi.collect_request_attributes(environ) + + if request.matched_route: + span_name = request.matched_route.pattern + attributes["http.route"] = request.matched_route.pattern + else: + span_name = otel_wsgi.get_default_span_name(environ) + + span = tracer.start_span( + span_name, + kind=trace.SpanKind.SERVER, + attributes=attributes, + start_time=start_time, + ) + + activation = tracer.use_span(span, end_on_exit=True) + activation.__enter__() + environ[_ENVIRON_ACTIVATION_KEY] = activation + environ[_ENVIRON_SPAN_KEY] = span + environ[_ENVIRON_TOKEN] = token + + +def trace_tween_factory(handler, registry): + settings = registry.settings + enabled = asbool(settings.get(SETTING_TRACE_ENABLED, True)) + + if not enabled: + # If disabled, make a tween that signals to the + # BeforeTraversal subscriber that tracing is disabled + def disabled_tween(request): + request.environ[_ENVIRON_ENABLED_KEY] = False + return handler(request) + + return disabled_tween + + # make a request tracing function + def trace_tween(request): + if disable_trace(request.url, _excluded_hosts, _excluded_paths): + request.environ[_ENVIRON_ENABLED_KEY] = False + # short-circuit when we don't want to trace anything + return handler(request) + + request.environ[_ENVIRON_ENABLED_KEY] = True + request.environ[_ENVIRON_STARTTIME_KEY] = time_ns() + + try: + response = handler(request) + response_or_exception = response + except HTTPException as exc: + # If the exception is a pyramid HTTPException, + # that's still valuable information that isn't necessarily + # a 500. For instance, HTTPFound is a 302. + # As described in docs, Pyramid exceptions are all valid + # response types + response_or_exception = exc + raise + finally: + span = request.environ.get(_ENVIRON_SPAN_KEY) + enabled = request.environ.get(_ENVIRON_ENABLED_KEY) + if not span and enabled: + _logger.warning( + "Pyramid environ's OpenTelemetry span missing." + "If the OpenTelemetry tween was added manually, make sure" + "PyramidInstrumentor().instrument_config(config) is called" + ) + elif enabled: + otel_wsgi.add_response_attributes( + span, + response_or_exception.status, + response_or_exception.headers, + ) + + activation = request.environ.get(_ENVIRON_ACTIVATION_KEY) + + if isinstance(response_or_exception, HTTPException): + activation.__exit__( + type(response_or_exception), + response_or_exception, + getattr(response_or_exception, "__traceback__", None), + ) + else: + activation.__exit__(None, None, None) + + context.detach(request.environ.get(_ENVIRON_TOKEN)) + + return response + + return trace_tween diff --git a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py new file mode 100644 index 0000000000..603bf0b7e5 --- /dev/null +++ b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-pyramid/tests/__init__.py b/ext/opentelemetry-ext-pyramid/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-pyramid/tests/pyramid_base_test.py b/ext/opentelemetry-ext-pyramid/tests/pyramid_base_test.py new file mode 100644 index 0000000000..21a6a1ab95 --- /dev/null +++ b/ext/opentelemetry-ext-pyramid/tests/pyramid_base_test.py @@ -0,0 +1,54 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pyramid.httpexceptions as exc +from pyramid.response import Response +from werkzeug.test import Client +from werkzeug.wrappers import BaseResponse + +from opentelemetry.configuration import Configuration + + +class InstrumentationTest: + def setUp(self): # pylint: disable=invalid-name + super().setUp() # pylint: disable=no-member + Configuration._reset() # pylint: disable=protected-access + + @staticmethod + def _hello_endpoint(request): + helloid = int(request.matchdict["helloid"]) + if helloid == 500: + raise exc.HTTPInternalServerError() + return Response("Hello: " + str(helloid)) + + def _common_initialization(self, config): + # pylint: disable=unused-argument + def excluded_endpoint(request): + return Response("excluded") + + # pylint: disable=unused-argument + def excluded2_endpoint(request): + return Response("excluded2") + + config.add_route("hello", "/hello/{helloid}") + config.add_view(self._hello_endpoint, route_name="hello") + config.add_route("excluded_arg", "/excluded/{helloid}") + config.add_view(self._hello_endpoint, route_name="excluded_arg") + config.add_route("excluded", "/excluded_noarg") + config.add_view(excluded_endpoint, route_name="excluded") + config.add_route("excluded2", "/excluded_noarg2") + config.add_view(excluded2_endpoint, route_name="excluded2") + + # pylint: disable=attribute-defined-outside-init + self.client = Client(config.make_wsgi_app(), BaseResponse) diff --git a/ext/opentelemetry-ext-pyramid/tests/test_automatic.py b/ext/opentelemetry-ext-pyramid/tests/test_automatic.py new file mode 100644 index 0000000000..d19da3c2b3 --- /dev/null +++ b/ext/opentelemetry-ext-pyramid/tests/test_automatic.py @@ -0,0 +1,79 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pyramid.config import Configurator + +from opentelemetry.ext.pyramid import PyramidInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.test.wsgitestutil import WsgiTestBase + +# pylint: disable=import-error +from .pyramid_base_test import InstrumentationTest + + +class TestAutomatic(InstrumentationTest, TestBase, WsgiTestBase): + def setUp(self): + super().setUp() + + PyramidInstrumentor().instrument() + + self.config = Configurator() + + self._common_initialization(self.config) + + def tearDown(self): + super().tearDown() + with self.disable_logging(): + PyramidInstrumentor().uninstrument() + + def test_uninstrument(self): + # pylint: disable=access-member-before-definition + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + PyramidInstrumentor().uninstrument() + self.config = Configurator() + + self._common_initialization(self.config) + + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + def test_tween_list(self): + tween_list = "pyramid.tweens.excview_tween_factory" + config = Configurator(settings={"pyramid.tweens": tween_list}) + self._common_initialization(config) + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + PyramidInstrumentor().uninstrument() + + self.config = Configurator() + + self._common_initialization(self.config) + + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) diff --git a/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py b/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py new file mode 100644 index 0000000000..b1ecbb38bb --- /dev/null +++ b/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py @@ -0,0 +1,194 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest.mock import patch + +from pyramid.config import Configurator + +from opentelemetry import trace +from opentelemetry.ext.pyramid import PyramidInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.test.wsgitestutil import WsgiTestBase + +# pylint: disable=import-error +from .pyramid_base_test import InstrumentationTest + + +def expected_attributes(override_attributes): + default_attributes = { + "component": "http", + "http.method": "GET", + "http.server_name": "localhost", + "http.scheme": "http", + "host.port": 80, + "http.host": "localhost", + "http.target": "/", + "http.flavor": "1.1", + "http.status_text": "OK", + "http.status_code": 200, + } + for key, val in override_attributes.items(): + default_attributes[key] = val + return default_attributes + + +class TestProgrammatic(InstrumentationTest, TestBase, WsgiTestBase): + def setUp(self): + super().setUp() + config = Configurator() + PyramidInstrumentor().instrument_config(config) + + self.config = config + + self._common_initialization(self.config) + + def tearDown(self): + super().tearDown() + with self.disable_logging(): + PyramidInstrumentor().uninstrument_config(self.config) + + def test_uninstrument(self): + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + PyramidInstrumentor().uninstrument_config(self.config) + # Need to remake the WSGI app export + self._common_initialization(self.config) + + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + def test_simple(self): + expected_attrs = expected_attributes( + {"http.target": "/hello/123", "http.route": "/hello/{helloid}"} + ) + self.client.get("/hello/123") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "/hello/{helloid}") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) + + def test_404(self): + expected_attrs = expected_attributes( + { + "http.method": "POST", + "http.target": "/bye", + "http.status_text": "Not Found", + "http.status_code": 404, + } + ) + + resp = self.client.post("/bye") + self.assertEqual(404, resp.status_code) + resp.close() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "/bye") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) + + def test_internal_error(self): + expected_attrs = expected_attributes( + { + "http.target": "/hello/500", + "http.route": "/hello/{helloid}", + "http.status_text": "Internal Server Error", + "http.status_code": 500, + } + ) + resp = self.client.get("/hello/500") + self.assertEqual(500, resp.status_code) + resp.close() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "/hello/{helloid}") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) + + def test_tween_list(self): + tween_list = "opentelemetry.ext.pyramid.trace_tween_factory\npyramid.tweens.excview_tween_factory" + config = Configurator(settings={"pyramid.tweens": tween_list}) + PyramidInstrumentor().instrument_config(config) + self._common_initialization(config) + + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + PyramidInstrumentor().uninstrument_config(config) + # Need to remake the WSGI app export + self._common_initialization(config) + + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + @patch("opentelemetry.ext.pyramid.callbacks._logger") + def test_warnings(self, mock_logger): + tween_list = "pyramid.tweens.excview_tween_factory" + config = Configurator(settings={"pyramid.tweens": tween_list}) + PyramidInstrumentor().instrument_config(config) + self._common_initialization(config) + + self.client.get("/hello/123") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + self.assertEqual(mock_logger.warning.called, True) + + mock_logger.warning.called = False + + tween_list = "opentelemetry.ext.pyramid.trace_tween_factory" + config = Configurator(settings={"pyramid.tweens": tween_list}) + self._common_initialization(config) + + self.client.get("/hello/123") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + self.assertEqual(mock_logger.warning.called, True) + + @patch( + "opentelemetry.ext.pyramid.callbacks._excluded_hosts", + ["http://localhost/excluded_arg/123"], + ) + @patch( + "opentelemetry.ext.pyramid.callbacks._excluded_paths", + ["excluded_noarg"], + ) + def test_exclude_lists(self): + self.client.get("/excluded_arg/123") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + + self.client.get("/excluded_arg/125") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + self.client.get("/excluded_noarg") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + self.client.get("/excluded_noarg2") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) diff --git a/tox.ini b/tox.ini index de7fe82ca1..2d05a98288 100644 --- a/tox.ini +++ b/tox.ini @@ -91,6 +91,10 @@ envlist = py3{4,5,6,7,8}-test-ext-pymysql pypy3-test-ext-pymysql + ; opentelemetry-ext-pyramid + py3{4,5,6,7,8}-test-ext-pyramid + pypy3-test-ext-pyramid + ; opentelemetry-ext-asgi py3{5,6,7,8}-test-ext-asgi pypy3-test-ext-asgi @@ -171,6 +175,7 @@ changedir = test-exporter-cloud-trace: ext/opentelemetry-exporter-cloud-trace/tests test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests test-ext-pymysql: ext/opentelemetry-ext-pymysql/tests + test-ext-pyramid: ext/opentelemetry-ext-pyramid/tests test-ext-asgi: ext/opentelemetry-ext-asgi/tests test-ext-sqlite3: ext/opentelemetry-ext-sqlite3/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests @@ -201,8 +206,8 @@ commands_pre = grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc[test] - wsgi,flask,django,asgi: pip install {toxinidir}/tests/util - wsgi,flask,django: pip install {toxinidir}/ext/opentelemetry-ext-wsgi + wsgi,flask,django,asgi,pyramid: pip install {toxinidir}/tests/util + wsgi,flask,django,pyramid: pip install {toxinidir}/ext/opentelemetry-ext-wsgi asgi: pip install {toxinidir}/ext/opentelemetry-ext-asgi @@ -229,6 +234,8 @@ commands_pre = pymysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-pymysql[test] + pyramid: pip install {toxinidir}/ext/opentelemetry-ext-pyramid[test] + sqlite3: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-sqlite3[test] redis: pip install {toxinidir}/ext/opentelemetry-ext-redis[test] From db27c7c3feb0db9fea75353434160b34ee1ff1de Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Wed, 10 Jun 2020 23:33:54 +0200 Subject: [PATCH 0400/1517] Make force_flush available on SDK's tracer provider (#594) Co-authored-by: Yusuke Tsutsumi --- .../src/opentelemetry/sdk/trace/__init__.py | 157 ++++++++- .../tests/trace/test_span_processor.py | 302 ++++++++++++++++++ opentelemetry-sdk/tests/trace/test_trace.py | 11 + 3 files changed, 463 insertions(+), 7 deletions(-) create mode 100644 opentelemetry-sdk/tests/trace/test_span_processor.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 77f4df266b..ef8570c7db 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -15,6 +15,7 @@ import abc import atexit +import concurrent.futures import json import logging import random @@ -22,7 +23,17 @@ from collections import OrderedDict from contextlib import contextmanager from types import TracebackType -from typing import Iterator, MutableSequence, Optional, Sequence, Tuple, Type +from typing import ( + Any, + Callable, + Iterator, + MutableSequence, + Optional, + Sequence, + Tuple, + Type, + Union, +) from opentelemetry import context as context_api from opentelemetry import trace as trace_api @@ -89,9 +100,12 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: """ -class MultiSpanProcessor(SpanProcessor): - """Implementation of :class:`SpanProcessor` that forwards all received - events to a list of `SpanProcessor`. +class SynchronousMultiSpanProcessor(SpanProcessor): + """Implementation of class:`SpanProcessor` that forwards all received + events to a list of span processors sequentially. + + The underlying span processors are called in sequential order as they were + added. """ def __init__(self): @@ -114,9 +128,113 @@ def on_end(self, span: "Span") -> None: sp.on_end(span) def shutdown(self) -> None: + """Sequentially shuts down all underlying span processors. + """ for sp in self._span_processors: sp.shutdown() + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Sequentially calls force_flush on all underlying + :class:`SpanProcessor` + + Args: + timeout_millis: The maximum amount of time over all span processors + to wait for spans to be exported. In case the first n span + processors exceeded the timeout followup span processors will be + skipped. + + Returns: + True if all span processors flushed their spans within the + given timeout, False otherwise. + """ + deadline_ns = time_ns() + timeout_millis * 1000000 + for sp in self._span_processors: + current_time_ns = time_ns() + if current_time_ns >= deadline_ns: + return False + + if not sp.force_flush((deadline_ns - current_time_ns) // 1000000): + return False + + return True + + +class ConcurrentMultiSpanProcessor(SpanProcessor): + """Implementation of :class:`SpanProcessor` that forwards all received + events to a list of span processors in parallel. + + Calls to the underlying span processors are forwarded in parallel by + submitting them to a thread pool executor and waiting until each span + processor finished its work. + + Args: + num_threads: The number of threads managed by the thread pool executor + and thus defining how many span processors can work in parallel. + """ + + def __init__(self, num_threads: int = 2): + # use a tuple to avoid race conditions when adding a new span and + # iterating through it on "on_start" and "on_end". + self._span_processors = () # type: Tuple[SpanProcessor, ...] + self._lock = threading.Lock() + self._executor = concurrent.futures.ThreadPoolExecutor( + max_workers=num_threads + ) + + def add_span_processor(self, span_processor: SpanProcessor) -> None: + """Adds a SpanProcessor to the list handled by this instance.""" + with self._lock: + self._span_processors = self._span_processors + (span_processor,) + + def _submit_and_await( + self, func: Callable[[SpanProcessor], Callable[..., None]], *args: Any + ): + futures = [] + for sp in self._span_processors: + future = self._executor.submit(func(sp), *args) + futures.append(future) + for future in futures: + future.result() + + def on_start(self, span: "Span") -> None: + self._submit_and_await(lambda sp: sp.on_start, span) + + def on_end(self, span: "Span") -> None: + self._submit_and_await(lambda sp: sp.on_end, span) + + def shutdown(self) -> None: + """Shuts down all underlying span processors in parallel.""" + self._submit_and_await(lambda sp: sp.shutdown) + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Calls force_flush on all underlying span processors in parallel. + + Args: + timeout_millis: The maximum amount of time to wait for spans to be + exported. + + Returns: + True if all span processors flushed their spans within the given + timeout, False otherwise. + """ + futures = [] + for sp in self._span_processors: # type: SpanProcessor + future = self._executor.submit(sp.force_flush, timeout_millis) + futures.append(future) + + timeout_sec = timeout_millis / 1e3 + done_futures, not_done_futures = concurrent.futures.wait( + futures, timeout_sec + ) + if not_done_futures: + return False + + for future in done_futures: + if not future.result(): + return False + + return True + class EventBase(abc.ABC): def __init__(self, name: str, timestamp: Optional[int] = None) -> None: @@ -730,8 +848,13 @@ def __init__( sampler: sampling.Sampler = trace_api.sampling.ALWAYS_ON, resource: Resource = Resource.create_empty(), shutdown_on_exit: bool = True, + active_span_processor: Union[ + SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor + ] = None, ): - self._active_span_processor = MultiSpanProcessor() + self._active_span_processor = ( + active_span_processor or SynchronousMultiSpanProcessor() + ) self.resource = resource self.sampler = sampler self._atexit_handler = None @@ -759,8 +882,8 @@ def add_span_processor(self, span_processor: SpanProcessor) -> None: The span processors are invoked in the same order they are registered. """ - # no lock here because MultiSpanProcessor.add_span_processor is - # thread safe + # no lock here because add_span_processor is thread safe for both + # SynchronousMultiSpanProcessor and ConcurrentMultiSpanProcessor. self._active_span_processor.add_span_processor(span_processor) def shutdown(self): @@ -769,3 +892,23 @@ def shutdown(self): if self._atexit_handler is not None: atexit.unregister(self._atexit_handler) self._atexit_handler = None + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Requests the active span processor to process all spans that have not + yet been processed. + + By default force flush is called sequentially on all added span + processors. This means that span processors further back in the list + have less time to flush their spans. + To have span processors flush their spans in parallel it is possible to + initialize the tracer provider with an instance of + `ConcurrentMultiSpanProcessor` at the cost of using multiple threads. + + Args: + timeout_millis: The maximum amount of time to wait for spans to be + processed. + + Returns: + False if the timeout is exceeded, True otherwise. + """ + return self._active_span_processor.force_flush(timeout_millis) diff --git a/opentelemetry-sdk/tests/trace/test_span_processor.py b/opentelemetry-sdk/tests/trace/test_span_processor.py new file mode 100644 index 0000000000..90b4003ca6 --- /dev/null +++ b/opentelemetry-sdk/tests/trace/test_span_processor.py @@ -0,0 +1,302 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import time +import typing +import unittest +from threading import Event +from unittest import mock + +from opentelemetry import trace as trace_api +from opentelemetry.sdk import trace + + +def span_event_start_fmt(span_processor_name, span_name): + return span_processor_name + ":" + span_name + ":start" + + +def span_event_end_fmt(span_processor_name, span_name): + return span_processor_name + ":" + span_name + ":end" + + +class MySpanProcessor(trace.SpanProcessor): + def __init__(self, name, span_list): + self.name = name + self.span_list = span_list + + def on_start(self, span: "trace.Span") -> None: + self.span_list.append(span_event_start_fmt(self.name, span.name)) + + def on_end(self, span: "trace.Span") -> None: + self.span_list.append(span_event_end_fmt(self.name, span.name)) + + +class TestSpanProcessor(unittest.TestCase): + def test_span_processor(self): + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) + + spans_calls_list = [] # filled by MySpanProcessor + expected_list = [] # filled by hand + + # Span processors are created but not added to the tracer yet + sp1 = MySpanProcessor("SP1", spans_calls_list) + sp2 = MySpanProcessor("SP2", spans_calls_list) + + with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): + pass + + # at this point lists must be empty + self.assertEqual(len(spans_calls_list), 0) + + # add single span processor + tracer_provider.add_span_processor(sp1) + + with tracer.start_as_current_span("foo"): + expected_list.append(span_event_start_fmt("SP1", "foo")) + + with tracer.start_as_current_span("bar"): + expected_list.append(span_event_start_fmt("SP1", "bar")) + + with tracer.start_as_current_span("baz"): + expected_list.append(span_event_start_fmt("SP1", "baz")) + + expected_list.append(span_event_end_fmt("SP1", "baz")) + + expected_list.append(span_event_end_fmt("SP1", "bar")) + + expected_list.append(span_event_end_fmt("SP1", "foo")) + + self.assertListEqual(spans_calls_list, expected_list) + + spans_calls_list.clear() + expected_list.clear() + + # go for multiple span processors + tracer_provider.add_span_processor(sp2) + + with tracer.start_as_current_span("foo"): + expected_list.append(span_event_start_fmt("SP1", "foo")) + expected_list.append(span_event_start_fmt("SP2", "foo")) + + with tracer.start_as_current_span("bar"): + expected_list.append(span_event_start_fmt("SP1", "bar")) + expected_list.append(span_event_start_fmt("SP2", "bar")) + + with tracer.start_as_current_span("baz"): + expected_list.append(span_event_start_fmt("SP1", "baz")) + expected_list.append(span_event_start_fmt("SP2", "baz")) + + expected_list.append(span_event_end_fmt("SP1", "baz")) + expected_list.append(span_event_end_fmt("SP2", "baz")) + + expected_list.append(span_event_end_fmt("SP1", "bar")) + expected_list.append(span_event_end_fmt("SP2", "bar")) + + expected_list.append(span_event_end_fmt("SP1", "foo")) + expected_list.append(span_event_end_fmt("SP2", "foo")) + + # compare if two lists are the same + self.assertListEqual(spans_calls_list, expected_list) + + def test_add_span_processor_after_span_creation(self): + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) + + spans_calls_list = [] # filled by MySpanProcessor + expected_list = [] # filled by hand + + # Span processors are created but not added to the tracer yet + sp = MySpanProcessor("SP1", spans_calls_list) + + with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): + # add span processor after spans have been created + tracer_provider.add_span_processor(sp) + + expected_list.append(span_event_end_fmt("SP1", "baz")) + + expected_list.append(span_event_end_fmt("SP1", "bar")) + + expected_list.append(span_event_end_fmt("SP1", "foo")) + + self.assertListEqual(spans_calls_list, expected_list) + + +class MultiSpanProcessorTestBase(abc.ABC): + @abc.abstractmethod + def create_multi_span_processor( + self, + ) -> typing.Union[ + trace.SynchronousMultiSpanProcessor, trace.ConcurrentMultiSpanProcessor + ]: + pass + + @staticmethod + def create_default_span() -> trace_api.Span: + span_context = trace_api.SpanContext(37, 73, is_remote=False) + return trace_api.DefaultSpan(span_context) + + def test_on_start(self): + multi_processor = self.create_multi_span_processor() + + mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(0, 5)] + for mock_processor in mocks: + multi_processor.add_span_processor(mock_processor) + + span = self.create_default_span() + multi_processor.on_start(span) + + for mock_processor in mocks: + mock_processor.on_start.assert_called_once_with(span) + multi_processor.shutdown() + + def test_on_end(self): + multi_processor = self.create_multi_span_processor() + + mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(0, 5)] + for mock_processor in mocks: + multi_processor.add_span_processor(mock_processor) + + span = self.create_default_span() + multi_processor.on_end(span) + + for mock_processor in mocks: + mock_processor.on_end.assert_called_once_with(span) + multi_processor.shutdown() + + def test_on_shutdown(self): + multi_processor = self.create_multi_span_processor() + + mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(0, 5)] + for mock_processor in mocks: + multi_processor.add_span_processor(mock_processor) + + multi_processor.shutdown() + + for mock_processor in mocks: + mock_processor.shutdown.assert_called_once_with() + + def test_force_flush(self): + multi_processor = self.create_multi_span_processor() + + mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(0, 5)] + for mock_processor in mocks: + multi_processor.add_span_processor(mock_processor) + timeout_millis = 100 + + flushed = multi_processor.force_flush(timeout_millis) + + # pylint: disable=no-member + self.assertTrue(flushed) + for mock_processor in mocks: + # pylint: disable=no-member + self.assertEqual(1, mock_processor.force_flush.call_count) + multi_processor.shutdown() + + +class TestSynchronousMultiSpanProcessor( + MultiSpanProcessorTestBase, unittest.TestCase +): + def create_multi_span_processor( + self, + ) -> trace.SynchronousMultiSpanProcessor: + return trace.SynchronousMultiSpanProcessor() + + def test_force_flush_late_by_timeout(self): + multi_processor = trace.SynchronousMultiSpanProcessor() + + def delayed_flush(_): + time.sleep(0.055) + + mock_processor1 = mock.Mock(spec=trace.SpanProcessor) + mock_processor1.force_flush = mock.Mock(side_effect=delayed_flush) + multi_processor.add_span_processor(mock_processor1) + mock_processor2 = mock.Mock(spec=trace.SpanProcessor) + multi_processor.add_span_processor(mock_processor2) + + flushed = multi_processor.force_flush(50) + + self.assertFalse(flushed) + self.assertEqual(1, mock_processor1.force_flush.call_count) + self.assertEqual(0, mock_processor2.force_flush.call_count) + + def test_force_flush_late_by_span_processor(self): + multi_processor = trace.SynchronousMultiSpanProcessor() + + mock_processor1 = mock.Mock(spec=trace.SpanProcessor) + mock_processor1.force_flush = mock.Mock(return_value=False) + multi_processor.add_span_processor(mock_processor1) + mock_processor2 = mock.Mock(spec=trace.SpanProcessor) + multi_processor.add_span_processor(mock_processor2) + + flushed = multi_processor.force_flush(50) + self.assertFalse(flushed) + self.assertEqual(1, mock_processor1.force_flush.call_count) + self.assertEqual(0, mock_processor2.force_flush.call_count) + + +class TestConcurrentMultiSpanProcessor( + MultiSpanProcessorTestBase, unittest.TestCase +): + def create_multi_span_processor( + self, + ) -> trace.ConcurrentMultiSpanProcessor: + return trace.ConcurrentMultiSpanProcessor(3) + + def test_force_flush_late_by_timeout(self): + multi_processor = trace.ConcurrentMultiSpanProcessor(5) + wait_event = Event() + + def delayed_flush(_): + wait_event.wait() + + late_mock = mock.Mock(spec=trace.SpanProcessor) + late_mock.force_flush = mock.Mock(side_effect=delayed_flush) + mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(0, 4)] + mocks.insert(0, late_mock) + + for mock_processor in mocks: + multi_processor.add_span_processor(mock_processor) + + flushed = multi_processor.force_flush(timeout_millis=10) + # let the thread executing the late_mock continue + wait_event.set() + + self.assertFalse(flushed) + for mock_processor in mocks: + self.assertEqual(1, mock_processor.force_flush.call_count) + multi_processor.shutdown() + + def test_force_flush_late_by_span_processor(self): + multi_processor = trace.ConcurrentMultiSpanProcessor(5) + + late_mock = mock.Mock(spec=trace.SpanProcessor) + late_mock.force_flush = mock.Mock(return_value=False) + mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(0, 4)] + mocks.insert(0, late_mock) + + for mock_processor in mocks: + multi_processor.add_span_processor(mock_processor) + + flushed = multi_processor.force_flush() + + self.assertFalse(flushed) + for mock_processor in mocks: + self.assertEqual(1, mock_processor.force_flush.call_count) + multi_processor.shutdown() diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index b3600f042f..2a8c834e75 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -117,6 +117,17 @@ class TestUseSpanException(Exception): with tracer.use_span(default_span): raise TestUseSpanException() + def test_tracer_provider_accepts_concurrent_multi_span_processor(self): + span_processor = trace.ConcurrentMultiSpanProcessor(2) + tracer_provider = trace.TracerProvider( + active_span_processor=span_processor + ) + + # pylint: disable=protected-access + self.assertEqual( + span_processor, tracer_provider._active_span_processor + ) + class TestTracerSampling(unittest.TestCase): def test_default_sampler(self): From 94aabcba1a0164238e30b4b5475306cb6b17025a Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Wed, 10 Jun 2020 18:40:28 -0400 Subject: [PATCH 0401/1517] api/sdk: Implement UpDownCounter instrument (#796) --- .../src/opentelemetry/metrics/__init__.py | 39 ++++++++++++- .../tests/metrics/test_metrics.py | 10 ++++ .../src/opentelemetry/sdk/metrics/__init__.py | 34 +++++++++++ .../sdk/metrics/export/batcher.py | 3 +- .../tests/metrics/export/test_export.py | 9 +++ .../tests/metrics/test_metrics.py | 56 +++++++++++++++++++ 6 files changed, 147 insertions(+), 4 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 569930d6f3..aa2988bce4 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -66,7 +66,17 @@ def add(self, value: ValueT) -> None: """Increases the value of the bound counter by ``value``. Args: - value: The value to add to the bound counter. + value: The value to add to the bound counter. Must be positive. + """ + + +class BoundUpDownCounter: + def add(self, value: ValueT) -> None: + """Increases the value of the bound counter by ``value``. + + Args: + value: The value to add to the bound counter. Can be positive or + negative. """ @@ -137,7 +147,28 @@ def add(self, value: ValueT, labels: Dict[str, str]) -> None: """Increases the value of the counter by ``value``. Args: - value: The value to add to the counter metric. + value: The value to add to the counter metric. Should be positive + or zero. For a Counter that can decrease, use + `UpDownCounter`. + labels: Labels to associate with the bound instrument. + """ + + +class UpDownCounter(Metric): + """A counter type metric that expresses the computation of a sum, + allowing negative increments.""" + + def bind(self, labels: Dict[str, str]) -> "BoundUpDownCounter": + """Gets a `BoundUpDownCounter`.""" + return BoundUpDownCounter() + + def add(self, value: ValueT, labels: Dict[str, str]) -> None: + """Increases the value of the counter by ``value``. + + Args: + value: The value to add to the counter metric. Can be positive or + negative. For a Counter that is never decreasing, use + `Counter`. labels: Labels to associate with the bound instrument. """ @@ -268,7 +299,9 @@ def get_meter( MetricT = TypeVar("MetricT", Counter, ValueRecorder) -InstrumentT = TypeVar("InstrumentT", Counter, Observer, ValueRecorder) +InstrumentT = TypeVar( + "InstrumentT", Counter, UpDownCounter, Observer, ValueRecorder +) ObserverT = TypeVar("ObserverT", bound=Observer) ObserverCallbackT = Callable[[Observer], None] diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index b3cbdefd15..aeec4b4ff4 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -35,6 +35,16 @@ def test_counter_add(self): counter = metrics.Counter() counter.add(1, {}) + def test_updowncounter(self): + counter = metrics.UpDownCounter() + bound_counter = counter.bind({}) + self.assertIsInstance(bound_counter, metrics.BoundUpDownCounter) + + def test_updowncounter_add(self): + counter = metrics.Counter() + counter.add(1, {}) + counter.add(-1, {}) + def test_valuerecorder(self): valuerecorder = metrics.ValueRecorder() bound_valuerecorder = valuerecorder.bind({}) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index e19d33580b..62c616a3e2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -102,6 +102,25 @@ def add(self, value: metrics_api.ValueT) -> None: if self._validate_update(value): self.update(value) + def _validate_update(self, value: metrics_api.ValueT) -> bool: + if not super()._validate_update(value): + return False + if value < 0: + logger.warning( + "Invalid value %s passed to Counter, value must be non-negative. " + "For a Counter that can decrease, use UpDownCounter.", + value, + ) + return False + return True + + +class BoundUpDownCounter(metrics_api.BoundUpDownCounter, BaseBoundInstrument): + def add(self, value: metrics_api.ValueT) -> None: + """See `opentelemetry.metrics.BoundUpDownCounter.add`.""" + if self._validate_update(value): + self.update(value) + class BoundValueRecorder(metrics_api.BoundValueRecorder, BaseBoundInstrument): def record(self, value: metrics_api.ValueT) -> None: @@ -184,6 +203,21 @@ def add(self, value: metrics_api.ValueT, labels: Dict[str, str]) -> None: UPDATE_FUNCTION = add +class UpDownCounter(Metric, metrics_api.UpDownCounter): + """See `opentelemetry.metrics.UpDownCounter`. + """ + + BOUND_INSTR_TYPE = BoundUpDownCounter + + def add(self, value: metrics_api.ValueT, labels: Dict[str, str]) -> None: + """See `opentelemetry.metrics.UpDownCounter.add`.""" + bound_intrument = self.bind(labels) + bound_intrument.add(value) + bound_intrument.release() + + UPDATE_FUNCTION = add + + class ValueRecorder(Metric, metrics_api.ValueRecorder): """See `opentelemetry.metrics.ValueRecorder`.""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index c0405d1ffb..0741082d99 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -19,6 +19,7 @@ Counter, InstrumentT, SumObserver, + UpDownCounter, UpDownSumObserver, ValueObserver, ValueRecorder, @@ -55,7 +56,7 @@ def aggregator_for(self, instrument_type: Type[InstrumentT]) -> Aggregator: Aggregators keep track of and updates values when metrics get updated. """ # pylint:disable=R0201 - if issubclass(instrument_type, Counter): + if issubclass(instrument_type, (Counter, UpDownCounter)): return CounterAggregator() if issubclass(instrument_type, (SumObserver, UpDownSumObserver)): return LastValueAggregator() diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 901d5a9404..c9a8f64d64 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -69,6 +69,15 @@ def test_aggregator_for_counter(self): ) ) + def test_aggregator_for_updowncounter(self): + batcher = UngroupedBatcher(True) + self.assertTrue( + isinstance( + batcher.aggregator_for(metrics.UpDownCounter), + CounterAggregator, + ) + ) + # TODO: Add other aggregator tests def test_checkpoint_set(self): diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index a92af748ac..cba10726b2 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -174,6 +174,15 @@ def test_create_metric(self): self.assertEqual(counter.name, "name") self.assertIs(counter.meter.resource, resource) + def test_create_updowncounter(self): + meter = metrics.MeterProvider().get_meter(__name__) + updowncounter = meter.create_metric( + "name", "desc", "unit", float, metrics.UpDownCounter, () + ) + self.assertIsInstance(updowncounter, metrics.UpDownCounter) + self.assertEqual(updowncounter.value_type, float) + self.assertEqual(updowncounter.name, "name") + def test_create_valuerecorder(self): meter = metrics.MeterProvider().get_meter(__name__) valuerecorder = meter.create_metric( @@ -296,6 +305,53 @@ def test_add(self): metric.add(2, labels) self.assertEqual(bound_counter.aggregator.current, 5) + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_add_non_decreasing_int_error(self, logger_mock): + meter = metrics.MeterProvider().get_meter(__name__) + metric = metrics.Counter("name", "desc", "unit", int, meter, ("key",)) + labels = {"key": "value"} + bound_counter = metric.bind(labels) + metric.add(3, labels) + metric.add(0, labels) + metric.add(-1, labels) + self.assertEqual(bound_counter.aggregator.current, 3) + self.assertEqual(logger_mock.warning.call_count, 1) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_add_non_decreasing_float_error(self, logger_mock): + meter = metrics.MeterProvider().get_meter(__name__) + metric = metrics.Counter( + "name", "desc", "unit", float, meter, ("key",) + ) + labels = {"key": "value"} + bound_counter = metric.bind(labels) + metric.add(3.3, labels) + metric.add(0.0, labels) + metric.add(0.1, labels) + metric.add(-0.1, labels) + self.assertEqual(bound_counter.aggregator.current, 3.4) + self.assertEqual(logger_mock.warning.call_count, 1) + + +class TestUpDownCounter(unittest.TestCase): + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_add(self, logger_mock): + meter = metrics.MeterProvider().get_meter(__name__) + metric = metrics.UpDownCounter( + "name", "desc", "unit", int, meter, ("key",) + ) + labels = {"key": "value"} + bound_counter = metric.bind(labels) + metric.add(3, labels) + metric.add(2, labels) + self.assertEqual(bound_counter.aggregator.current, 5) + + metric.add(0, labels) + metric.add(-3, labels) + metric.add(-1, labels) + self.assertEqual(bound_counter.aggregator.current, 1) + self.assertEqual(logger_mock.warning.call_count, 0) + class TestValueRecorder(unittest.TestCase): def test_record(self): From 731f57784a9d593dc8ac7f181b4906fe3d46d265 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Wed, 10 Jun 2020 16:14:33 -0700 Subject: [PATCH 0402/1517] chore: installation of test packages in eachdist (#794) eachdist.py did not support the installation of test packages, (as defined by the extra_requires:test package group). As a result, test packages were being added to dev-requirements.txt By having eachdist.py develop install test packages, and moving develop/test package definitions to the individual instrumentations, it is easier to determine which packages require which dependencies for testing purposes, and enables support for existing dependencies that follow the extra_requires:test pattern. --- dev-requirements.txt | 4 +--- .../examples/opentelemetry-example-app/setup.py | 1 + .../setup.cfg | 5 ++++- .../setup.cfg | 3 +++ ext/opentelemetry-ext-aiohttp-client/setup.cfg | 3 +++ ext/opentelemetry-ext-datadog/setup.cfg | 3 +++ ext/opentelemetry-ext-jaeger/setup.cfg | 3 +++ .../setup.cfg | 3 +++ .../setup.cfg | 1 + ext/opentelemetry-ext-prometheus/setup.cfg | 3 +++ ext/opentelemetry-ext-zipkin/setup.cfg | 3 +++ opentelemetry-api/setup.cfg | 3 +++ opentelemetry-instrumentation/setup.cfg | 3 +++ opentelemetry-proto/setup.cfg | 3 +++ opentelemetry-sdk/setup.cfg | 3 +++ scripts/eachdist.py | 17 +++++++++++++++-- 16 files changed, 55 insertions(+), 6 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 253c089507..8ea405d92a 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -8,6 +8,4 @@ sphinx-rtd-theme~=0.4 sphinx-autodoc-typehints~=1.10.2 pytest!=5.2.3 pytest-cov>=2.8 -readme-renderer~=24.0 -httpretty~=1.0 -opentracing~=2.2.0 \ No newline at end of file +readme-renderer~=24.0 \ No newline at end of file diff --git a/docs/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py index 4d87c27afc..02333384af 100644 --- a/docs/examples/opentelemetry-example-app/setup.py +++ b/docs/examples/opentelemetry-example-app/setup.py @@ -43,6 +43,7 @@ "requests", "protobuf~=3.11", ], + extras_require={"test": []}, license="Apache-2.0", package_dir={"": "src"}, packages=setuptools.find_namespace_packages(where="src"), diff --git a/ext/opentelemetry-exporter-cloud-monitoring/setup.cfg b/ext/opentelemetry-exporter-cloud-monitoring/setup.cfg index 37665ee48b..8875937751 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/setup.cfg +++ b/ext/opentelemetry-exporter-cloud-monitoring/setup.cfg @@ -45,4 +45,7 @@ install_requires = google-cloud-monitoring [options.packages.find] -where = src \ No newline at end of file +where = src + +[options.extras_require] +test = diff --git a/ext/opentelemetry-exporter-cloud-trace/setup.cfg b/ext/opentelemetry-exporter-cloud-trace/setup.cfg index df6c2ce587..41ffc4116a 100644 --- a/ext/opentelemetry-exporter-cloud-trace/setup.cfg +++ b/ext/opentelemetry-exporter-cloud-trace/setup.cfg @@ -45,3 +45,6 @@ install_requires = [options.packages.find] where = src + +[options.extras_require] +test = diff --git a/ext/opentelemetry-ext-aiohttp-client/setup.cfg b/ext/opentelemetry-ext-aiohttp-client/setup.cfg index e2a6493922..74fc3a9a86 100644 --- a/ext/opentelemetry-ext-aiohttp-client/setup.cfg +++ b/ext/opentelemetry-ext-aiohttp-client/setup.cfg @@ -45,3 +45,6 @@ install_requires = [options.packages.find] where = src + +[options.extras_require] +test = diff --git a/ext/opentelemetry-ext-datadog/setup.cfg b/ext/opentelemetry-ext-datadog/setup.cfg index afdd739d12..074206dc82 100644 --- a/ext/opentelemetry-ext-datadog/setup.cfg +++ b/ext/opentelemetry-ext-datadog/setup.cfg @@ -45,3 +45,6 @@ install_requires = [options.packages.find] where = src + +[options.extras_require] +test = diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/ext/opentelemetry-ext-jaeger/setup.cfg index 339ac32c28..9d734606bb 100644 --- a/ext/opentelemetry-ext-jaeger/setup.cfg +++ b/ext/opentelemetry-ext-jaeger/setup.cfg @@ -46,3 +46,6 @@ install_requires = [options.packages.find] where = src + +[options.extras_require] +test = diff --git a/ext/opentelemetry-ext-opencensusexporter/setup.cfg b/ext/opentelemetry-ext-opencensusexporter/setup.cfg index 11fc3a61c7..96c30ac9af 100644 --- a/ext/opentelemetry-ext-opencensusexporter/setup.cfg +++ b/ext/opentelemetry-ext-opencensusexporter/setup.cfg @@ -48,3 +48,6 @@ install_requires = [options.packages.find] where = src + +[options.extras_require] +test = diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg index 2ad18381c5..ceb7d4c0ad 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -47,6 +47,7 @@ install_requires = [options.extras_require] test = opentelemetry-test == 0.9.dev0 + opentracing ~= 2.2.0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/ext/opentelemetry-ext-prometheus/setup.cfg index 653c72a404..14b9f409a4 100644 --- a/ext/opentelemetry-ext-prometheus/setup.cfg +++ b/ext/opentelemetry-ext-prometheus/setup.cfg @@ -46,3 +46,6 @@ install_requires = [options.packages.find] where = src + +[options.extras_require] +test = diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg index 809db51237..cc945f8ba3 100644 --- a/ext/opentelemetry-ext-zipkin/setup.cfg +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -46,3 +46,6 @@ install_requires = [options.packages.find] where = src + +[options.extras_require] +test = diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index d6d68a710d..8300be0d0a 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -57,3 +57,6 @@ opentelemetry_meter_provider = default_meter_provider = opentelemetry.metrics:DefaultMeterProvider opentelemetry_tracer_provider = default_tracer_provider = opentelemetry.trace:DefaultTracerProvider + +[options.extras_require] +test = diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 119beeec11..98c1fa4096 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -52,3 +52,6 @@ where = src console_scripts = opentelemetry-instrument = opentelemetry.instrumentation.auto_instrumentation:run opentelemetry-bootstrap = opentelemetry.instrumentation.bootstrap:run + +[options.extras_require] +test = diff --git a/opentelemetry-proto/setup.cfg b/opentelemetry-proto/setup.cfg index 3796b5ef49..59b0309ad4 100644 --- a/opentelemetry-proto/setup.cfg +++ b/opentelemetry-proto/setup.cfg @@ -46,3 +46,6 @@ install_requires = [options.packages.find] where = src + +[options.extras_require] +test = diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 323bca728f..a5cff106af 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -52,3 +52,6 @@ opentelemetry_meter_provider = sdk_meter_provider = opentelemetry.sdk.metrics:MeterProvider opentelemetry_tracer_provider = sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider + +[options.extras_require] +test = diff --git a/scripts/eachdist.py b/scripts/eachdist.py index f1c5e18b60..15b9b8edcb 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -205,6 +205,7 @@ def setup_instparser(instparser): setup_instparser(instparser) instparser.add_argument("--editable", "-e", action="store_true") + instparser.add_argument("--with-test-deps", action="store_true") instparser.add_argument("--with-dev-deps", action="store_true") instparser.add_argument("--eager-upgrades", action="store_true") @@ -214,7 +215,10 @@ def setup_instparser(instparser): ) setup_instparser(devparser) devparser.set_defaults( - editable=True, with_dev_deps=True, eager_upgrades=True + editable=True, + with_dev_deps=True, + eager_upgrades=True, + with_test_deps=True, ) lintparser = subparsers.add_parser( @@ -424,7 +428,16 @@ def install_args(args): check=True, ) - allfmt = "-e 'file://{}'" if args.editable else "'file://{}'" + allfmt = "-e 'file://{}" if args.editable else "'file://{}" + # packages should provide an extra_requires that is named + # 'test', to denote test dependencies. + extras = [] + if args.with_test_deps: + extras.append("test") + if extras: + allfmt += "[{}]".format(",".join(extras)) + # note the trailing single quote, to close the quote opened above. + allfmt += "'" execute_args( parse_subargs( args, From 376c43e547bec98f804b42423d4639ebe6005965 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 10 Jun 2020 17:44:29 -0600 Subject: [PATCH 0403/1517] ext/otlp: Add OTLP span exporter (#787) Co-authored-by: Leighton Chen Co-authored-by: alrex --- docs/ext/otlp/otlp.rst | 7 + eachdist.ini | 1 + ext/opentelemetry-ext-otlp/CHANGELOG.md | 5 + ext/opentelemetry-ext-otlp/LICENSE | 201 +++++++++++ ext/opentelemetry-ext-otlp/MANIFEST.in | 9 + ext/opentelemetry-ext-otlp/README.rst | 25 ++ ext/opentelemetry-ext-otlp/setup.cfg | 54 +++ ext/opentelemetry-ext-otlp/setup.py | 26 ++ .../src/opentelemetry/ext/otlp/__init__.py | 50 +++ .../ext/otlp/trace_exporter/__init__.py | 338 ++++++++++++++++++ .../src/opentelemetry/ext/otlp/version.py | 15 + ext/opentelemetry-ext-otlp/tests/__init__.py | 0 .../tests/test_otlp_trace_exporter.py | 261 ++++++++++++++ .../opentelemetry/sdk/resources/__init__.py | 4 + .../src/opentelemetry/sdk/trace/__init__.py | 3 +- scripts/build.sh | 2 +- tox.ini | 8 + 17 files changed, 1007 insertions(+), 2 deletions(-) create mode 100644 docs/ext/otlp/otlp.rst create mode 100644 ext/opentelemetry-ext-otlp/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-otlp/LICENSE create mode 100644 ext/opentelemetry-ext-otlp/MANIFEST.in create mode 100644 ext/opentelemetry-ext-otlp/README.rst create mode 100644 ext/opentelemetry-ext-otlp/setup.cfg create mode 100644 ext/opentelemetry-ext-otlp/setup.py create mode 100644 ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/__init__.py create mode 100644 ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/trace_exporter/__init__.py create mode 100644 ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py create mode 100644 ext/opentelemetry-ext-otlp/tests/__init__.py create mode 100644 ext/opentelemetry-ext-otlp/tests/test_otlp_trace_exporter.py diff --git a/docs/ext/otlp/otlp.rst b/docs/ext/otlp/otlp.rst new file mode 100644 index 0000000000..4739d21a58 --- /dev/null +++ b/docs/ext/otlp/otlp.rst @@ -0,0 +1,7 @@ +Opentelemetry OTLP Exporter +=========================== + +.. automodule:: opentelemetry.ext.otlp + :members: + :undoc-members: + :show-inheritance: diff --git a/eachdist.ini b/eachdist.ini index f88cb1b180..e052f9cf85 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -5,6 +5,7 @@ sortfirst= opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation + opentelemetry-proto ext/opentelemetry-ext-wsgi ext/opentelemetry-ext-dbapi ext/* diff --git a/ext/opentelemetry-ext-otlp/CHANGELOG.md b/ext/opentelemetry-ext-otlp/CHANGELOG.md new file mode 100644 index 0000000000..3e04402cea --- /dev/null +++ b/ext/opentelemetry-ext-otlp/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release diff --git a/ext/opentelemetry-ext-otlp/LICENSE b/ext/opentelemetry-ext-otlp/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-otlp/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-otlp/MANIFEST.in b/ext/opentelemetry-ext-otlp/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-otlp/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-otlp/README.rst b/ext/opentelemetry-ext-otlp/README.rst new file mode 100644 index 0000000000..ab233cbc62 --- /dev/null +++ b/ext/opentelemetry-ext-otlp/README.rst @@ -0,0 +1,25 @@ +OpenTelemetry Collector Exporter +================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-otlp.svg + :target: https://pypi.org/project/opentelemetry-ext-otlp/ + +This library allows to export data to the OpenTelemetry Collector using the OpenTelemetry Protocol. + +Installation +------------ + +:: + + pip install opentelemetry-ext-otlp + + +References +---------- + +* `OpenTelemetry Collector Exporter `_ +* `OpenTelemetry Collector `_ +* `OpenTelemetry `_ +* `OpenTelemetry Protocol Specification `_ diff --git a/ext/opentelemetry-ext-otlp/setup.cfg b/ext/opentelemetry-ext-otlp/setup.cfg new file mode 100644 index 0000000000..4117da11e1 --- /dev/null +++ b/ext/opentelemetry-ext-otlp/setup.cfg @@ -0,0 +1,54 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-otlp +description = OpenTelemetry Collector Exporter +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-otlp +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + grpcio >= 1.0.0, < 2.0.0 + googleapis-common-protos ~= 1.52.0 + opentelemetry-api == 0.9.dev0 + opentelemetry-sdk == 0.9.dev0 + opentelemetry-proto == 0.9.dev0 + backoff ~= 1.10.0 + +[options.extras_require] +test = + pytest-grpc + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-otlp/setup.py b/ext/opentelemetry-ext-otlp/setup.py new file mode 100644 index 0000000000..64c30afbab --- /dev/null +++ b/ext/opentelemetry-ext-otlp/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "otlp", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/__init__.py b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/__init__.py new file mode 100644 index 0000000000..55c5dd69ef --- /dev/null +++ b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/__init__.py @@ -0,0 +1,50 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +""" +This library allows to export tracing data to an OTLP collector. + +Usage +----- + +The **OTLP Span Exporter** allows to export `OpenTelemetry`_ traces to the +`OTLP`_ collector. + + +.. _OTLP: https://github.com/open-telemetry/opentelemetry-collector/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext.otlp.trace_exporter import OTLPSpanExporter + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + + trace.set_tracer_provider(TracerProvider()) + tracer = trace.get_tracer(__name__) + + otlp_exporter = OTLPSpanExporter(endpoint="localhost:55678") + + span_processor = BatchExportSpanProcessor(otlp_exporter) + + trace.get_tracer_provider().add_span_processor(span_processor) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + +API +--- +""" diff --git a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/trace_exporter/__init__.py b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/trace_exporter/__init__.py new file mode 100644 index 0000000000..a25f785366 --- /dev/null +++ b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/trace_exporter/__init__.py @@ -0,0 +1,338 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""OTLP Span Exporter""" + +import logging +from time import sleep +from typing import Sequence + +from backoff import expo +from google.rpc.error_details_pb2 import RetryInfo +from grpc import ( + ChannelCredentials, + RpcError, + StatusCode, + insecure_channel, + secure_channel, +) + +from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( + ExportTraceServiceRequest, +) +from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( + TraceServiceStub, +) +from opentelemetry.proto.common.v1.common_pb2 import AttributeKeyValue +from opentelemetry.proto.resource.v1.resource_pb2 import Resource +from opentelemetry.proto.trace.v1.trace_pb2 import ( + InstrumentationLibrarySpans, + ResourceSpans, +) +from opentelemetry.proto.trace.v1.trace_pb2 import Span as CollectorSpan +from opentelemetry.proto.trace.v1.trace_pb2 import Status +from opentelemetry.sdk.trace import Span as SDKSpan +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult + +logger = logging.getLogger(__name__) + + +def _translate_key_values(key, value): + key_value = {"key": key} + + if isinstance(value, bool): + key_value["bool_value"] = value + + elif isinstance(value, str): + key_value["string_value"] = value + + elif isinstance(value, int): + key_value["int_value"] = value + + elif isinstance(value, float): + key_value["double_value"] = value + + else: + raise Exception( + "Invalid type {} of value {}".format(type(value), value) + ) + + return key_value + + +# pylint: disable=no-member +class OTLPSpanExporter(SpanExporter): + """OTLP span exporter + + Args: + endpoint: OpenTelemetry Collector receiver endpoint + credentials: Credentials object for server authentication + metadata: Metadata to send when exporting + """ + + def __init__( + self, + endpoint="localhost:55678", + credentials: ChannelCredentials = None, + metadata=None, + ): + super().__init__() + + self._metadata = metadata + self._collector_span_kwargs = None + + if credentials is None: + self._client = TraceServiceStub(insecure_channel(endpoint)) + else: + self._client = TraceServiceStub( + secure_channel(endpoint, credentials) + ) + + def _translate_name(self, sdk_span): + self._collector_span_kwargs["name"] = sdk_span.name + + def _translate_start_time(self, sdk_span): + self._collector_span_kwargs[ + "start_time_unix_nano" + ] = sdk_span.start_time + + def _translate_end_time(self, sdk_span): + self._collector_span_kwargs["end_time_unix_nano"] = sdk_span.end_time + + def _translate_span_id(self, sdk_span): + self._collector_span_kwargs[ + "span_id" + ] = sdk_span.context.span_id.to_bytes(8, "big") + + def _translate_trace_id(self, sdk_span): + self._collector_span_kwargs[ + "trace_id" + ] = sdk_span.context.trace_id.to_bytes(16, "big") + + def _translate_parent(self, sdk_span): + if sdk_span.parent is not None: + self._collector_span_kwargs[ + "parent_span_id" + ] = sdk_span.parent.span_id.to_bytes(8, "big") + + def _translate_context_trace_state(self, sdk_span): + if sdk_span.context.trace_state is not None: + self._collector_span_kwargs["trace_state"] = ",".join( + [ + "{}={}".format(key, value) + for key, value in (sdk_span.context.trace_state.items()) + ] + ) + + def _translate_attributes(self, sdk_span): + if sdk_span.attributes: + + self._collector_span_kwargs["attributes"] = [] + + for key, value in sdk_span.attributes.items(): + + try: + self._collector_span_kwargs["attributes"].append( + AttributeKeyValue(**_translate_key_values(key, value)) + ) + except Exception as error: # pylint: disable=broad-except + logger.exception(error) + + def _translate_events(self, sdk_span): + if sdk_span.events: + self._collector_span_kwargs["events"] = [] + + for sdk_span_event in sdk_span.events: + + collector_span_event = CollectorSpan.Event( + name=sdk_span_event.name, + time_unix_nano=sdk_span_event.timestamp, + ) + + for key, value in sdk_span_event.attributes.items(): + try: + collector_span_event.attributes.append( + AttributeKeyValue( + **_translate_key_values(key, value) + ) + ) + # pylint: disable=broad-except + except Exception as error: + logger.exception(error) + + self._collector_span_kwargs["events"].append( + collector_span_event + ) + + def _translate_links(self, sdk_span): + if sdk_span.links: + self._collector_span_kwargs["links"] = [] + + for sdk_span_link in sdk_span.links: + + collector_span_link = CollectorSpan.Link( + trace_id=( + sdk_span_link.context.trace_id.to_bytes(16, "big") + ), + span_id=(sdk_span_link.context.span_id.to_bytes(8, "big")), + ) + + for key, value in sdk_span_link.attributes.items(): + try: + collector_span_link.attributes.append( + AttributeKeyValue( + **_translate_key_values(key, value) + ) + ) + # pylint: disable=broad-except + except Exception as error: + logger.exception(error) + + self._collector_span_kwargs["links"].append( + collector_span_link + ) + + def _translate_status(self, sdk_span): + if sdk_span.status is not None: + self._collector_span_kwargs["status"] = Status( + code=sdk_span.status.canonical_code.value, + message=sdk_span.status.description, + ) + + def _translate_spans( + self, sdk_spans: Sequence[SDKSpan], + ) -> ExportTraceServiceRequest: + + sdk_resource_instrumentation_library_spans = {} + + for sdk_span in sdk_spans: + + if sdk_span.resource not in ( + sdk_resource_instrumentation_library_spans.keys() + ): + sdk_resource_instrumentation_library_spans[ + sdk_span.resource + ] = InstrumentationLibrarySpans() + + self._collector_span_kwargs = {} + + self._translate_name(sdk_span) + self._translate_start_time(sdk_span) + self._translate_end_time(sdk_span) + self._translate_span_id(sdk_span) + self._translate_trace_id(sdk_span) + self._translate_parent(sdk_span) + self._translate_context_trace_state(sdk_span) + self._translate_attributes(sdk_span) + self._translate_events(sdk_span) + self._translate_links(sdk_span) + self._translate_status(sdk_span) + + self._collector_span_kwargs["kind"] = getattr( + CollectorSpan.SpanKind, sdk_span.kind.name + ) + + sdk_resource_instrumentation_library_spans[ + sdk_span.resource + ].spans.append(CollectorSpan(**self._collector_span_kwargs)) + + resource_spans = [] + + for ( + sdk_resource, + instrumentation_library_spans, + ) in sdk_resource_instrumentation_library_spans.items(): + + collector_resource = Resource() + + for key, value in sdk_resource.labels.items(): + + try: + collector_resource.attributes.append( + AttributeKeyValue(**_translate_key_values(key, value)) + ) + except Exception as error: # pylint: disable=broad-except + logger.exception(error) + + resource_spans.append( + ResourceSpans( + resource=collector_resource, + instrumentation_library_spans=[ + instrumentation_library_spans + ], + ) + ) + + return ExportTraceServiceRequest(resource_spans=resource_spans) + + def export(self, spans: Sequence[SDKSpan]) -> SpanExportResult: + # expo returns a generator that yields delay values which grow + # exponentially. Once delay is greater than max_value, the yielded + # value will remain constant. + # max_value is set to 900 (900 seconds is 15 minutes) to use the same + # value as used in the Go implementation. + + max_value = 900 + + for delay in expo(max_value=max_value): + + if delay == max_value: + return SpanExportResult.FAILURE + + try: + self._client.Export( + request=self._translate_spans(spans), + metadata=self._metadata, + ) + + return SpanExportResult.SUCCESS + + except RpcError as error: + + if error.code() in [ + StatusCode.CANCELLED, + StatusCode.DEADLINE_EXCEEDED, + StatusCode.PERMISSION_DENIED, + StatusCode.UNAUTHENTICATED, + StatusCode.RESOURCE_EXHAUSTED, + StatusCode.ABORTED, + StatusCode.OUT_OF_RANGE, + StatusCode.UNAVAILABLE, + StatusCode.DATA_LOSS, + ]: + + retry_info_bin = dict(error.trailing_metadata()).get( + "google.rpc.retryinfo-bin" + ) + if retry_info_bin is not None: + retry_info = RetryInfo() + retry_info.ParseFromString(retry_info_bin) + delay = ( + retry_info.retry_delay.seconds + + retry_info.retry_delay.nanos / 1.0e9 + ) + + logger.debug("Waiting %ss before retrying export of span") + sleep(delay) + continue + + if error.code() == StatusCode.OK: + return SpanExportResult.SUCCESS + + return SpanExportResult.FAILURE + + return SpanExportResult.FAILURE + + def shutdown(self): + pass diff --git a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py new file mode 100644 index 0000000000..603bf0b7e5 --- /dev/null +++ b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-otlp/tests/__init__.py b/ext/opentelemetry-ext-otlp/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-otlp/tests/test_otlp_trace_exporter.py b/ext/opentelemetry-ext-otlp/tests/test_otlp_trace_exporter.py new file mode 100644 index 0000000000..db42f8ff6d --- /dev/null +++ b/ext/opentelemetry-ext-otlp/tests/test_otlp_trace_exporter.py @@ -0,0 +1,261 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import OrderedDict +from concurrent.futures import ThreadPoolExecutor +from unittest import TestCase +from unittest.mock import Mock, PropertyMock, patch + +from google.protobuf.duration_pb2 import Duration +from google.rpc.error_details_pb2 import RetryInfo +from grpc import StatusCode, server + +from opentelemetry.ext.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( + ExportTraceServiceRequest, + ExportTraceServiceResponse, +) +from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( + TraceServiceServicer, + add_TraceServiceServicer_to_server, +) +from opentelemetry.proto.common.v1.common_pb2 import AttributeKeyValue +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as CollectorResource, +) +from opentelemetry.proto.trace.v1.trace_pb2 import ( + InstrumentationLibrarySpans, + ResourceSpans, +) +from opentelemetry.proto.trace.v1.trace_pb2 import Span as CollectorSpan +from opentelemetry.proto.trace.v1.trace_pb2 import Status +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.trace import Span, TracerProvider +from opentelemetry.sdk.trace.export import ( + SimpleExportSpanProcessor, + SpanExportResult, +) +from opentelemetry.trace import SpanKind + + +class TraceServiceServicerUNAVAILABLEDelay(TraceServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.UNAVAILABLE) + + context.send_initial_metadata( + (("google.rpc.retryinfo-bin", RetryInfo().SerializeToString()),) + ) + context.set_trailing_metadata( + ( + ( + "google.rpc.retryinfo-bin", + RetryInfo( + retry_delay=Duration(seconds=4) + ).SerializeToString(), + ), + ) + ) + + return ExportTraceServiceResponse() + + +class TraceServiceServicerUNAVAILABLE(TraceServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.UNAVAILABLE) + + return ExportTraceServiceResponse() + + +class TraceServiceServicerSUCCESS(TraceServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.OK) + + return ExportTraceServiceResponse() + + +class TestOTLPSpanExporter(TestCase): + def setUp(self): + tracer_provider = TracerProvider() + self.exporter = OTLPSpanExporter() + tracer_provider.add_span_processor( + SimpleExportSpanProcessor(self.exporter) + ) + self.tracer = tracer_provider.get_tracer(__name__) + + self.server = server(ThreadPoolExecutor(max_workers=10)) + + self.server.add_insecure_port("[::]:55678") + + self.server.start() + + event_mock = Mock( + **{ + "timestamp": 1591240820506462784, + "attributes": OrderedDict([("a", 1), ("b", False)]), + } + ) + + type(event_mock).name = PropertyMock(return_value="a") + + self.span = Span( + "a", + context=Mock( + **{ + "trace_state": OrderedDict([("a", "b"), ("c", "d")]), + "span_id": 10217189687419569865, + "trace_id": 67545097771067222548457157018666467027, + } + ), + resource=SDKResource(OrderedDict([("a", 1), ("b", False)])), + parent=Mock(**{"span_id": 12345}), + attributes=OrderedDict([("a", 1), ("b", True)]), + events=[event_mock], + links=[ + Mock( + **{ + "context.trace_id": 1, + "context.span_id": 2, + "attributes": OrderedDict([("a", 1), ("b", False)]), + "kind": SpanKind.INTERNAL, + } + ) + ], + ) + + self.span.start() + self.span.end() + + def tearDown(self): + self.server.stop(None) + + @patch("opentelemetry.ext.otlp.trace_exporter.expo") + @patch("opentelemetry.ext.otlp.trace_exporter.sleep") + def test_unavailable(self, mock_sleep, mock_expo): + + mock_expo.configure_mock(**{"return_value": [1]}) + + add_TraceServiceServicer_to_server( + TraceServiceServicerUNAVAILABLE(), self.server + ) + self.assertEqual( + self.exporter.export([self.span]), SpanExportResult.FAILURE + ) + mock_sleep.assert_called_with(1) + + @patch("opentelemetry.ext.otlp.trace_exporter.expo") + @patch("opentelemetry.ext.otlp.trace_exporter.sleep") + def test_unavailable_delay(self, mock_sleep, mock_expo): + + mock_expo.configure_mock(**{"return_value": [1]}) + + add_TraceServiceServicer_to_server( + TraceServiceServicerUNAVAILABLEDelay(), self.server + ) + self.assertEqual( + self.exporter.export([self.span]), SpanExportResult.FAILURE + ) + mock_sleep.assert_called_with(4) + + def test_success(self): + add_TraceServiceServicer_to_server( + TraceServiceServicerSUCCESS(), self.server + ) + self.assertEqual( + self.exporter.export([self.span]), SpanExportResult.SUCCESS + ) + + def test_translate_spans(self): + + expected = ExportTraceServiceRequest( + resource_spans=[ + ResourceSpans( + resource=CollectorResource( + attributes=[ + AttributeKeyValue(key="a", int_value=1), + AttributeKeyValue(key="b", bool_value=False), + ] + ), + instrumentation_library_spans=[ + InstrumentationLibrarySpans( + spans=[ + CollectorSpan( + # pylint: disable=no-member + name="a", + start_time_unix_nano=self.span.start_time, + end_time_unix_nano=self.span.end_time, + trace_state="a=b,c=d", + span_id=int.to_bytes( + 10217189687419569865, 8, "big" + ), + trace_id=int.to_bytes( + 67545097771067222548457157018666467027, + 16, + "big", + ), + parent_span_id=( + b"\000\000\000\000\000\00009" + ), + kind=CollectorSpan.SpanKind.INTERNAL, + attributes=[ + AttributeKeyValue( + key="a", int_value=1 + ), + AttributeKeyValue( + key="b", bool_value=True + ), + ], + events=[ + CollectorSpan.Event( + name="a", + time_unix_nano=1591240820506462784, + attributes=[ + AttributeKeyValue( + key="a", int_value=1 + ), + AttributeKeyValue( + key="b", int_value=False + ), + ], + ) + ], + status=Status(code=0, message=""), + links=[ + CollectorSpan.Link( + trace_id=int.to_bytes( + 1, 16, "big" + ), + span_id=int.to_bytes(2, 8, "big"), + attributes=[ + AttributeKeyValue( + key="a", int_value=1 + ), + AttributeKeyValue( + key="b", bool_value=False + ), + ], + ) + ], + ) + ] + ) + ], + ), + ] + ) + + # pylint: disable=protected-access + self.assertEqual(expected, self.exporter._translate_spans([self.span])) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 193f283175..91c491f09f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. import typing +from json import dumps LabelValue = typing.Union[str, bool, int, float] Labels = typing.Dict[str, LabelValue] @@ -49,5 +50,8 @@ def __eq__(self, other: object) -> bool: return False return self._labels == other._labels + def __hash__(self): + return hash(dumps(self._labels, sort_keys=True)) + _EMPTY_RESOURCE = Resource({}) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index ef8570c7db..45aa491fa1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -791,6 +791,7 @@ def start_span( # pylint: disable=too-many-locals # apply sampling decision attributes after initial attributes span_attributes = attributes.copy() span_attributes.update(sampling_decision.attributes) + # pylint:disable=protected-access span = Span( name=name, context=context, @@ -798,7 +799,7 @@ def start_span( # pylint: disable=too-many-locals sampler=self.source.sampler, resource=self.source.resource, attributes=span_attributes, - span_processor=self.source._active_span_processor, # pylint:disable=protected-access + span_processor=self.source._active_span_processor, kind=kind, links=links, instrumentation_info=self.instrumentation_info, diff --git a/scripts/build.sh b/scripts/build.sh index b7a771789a..7a27105d76 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ ext/*/ ; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ ext/*/ ; do ( echo "building $d" cd "$d" diff --git a/tox.ini b/tox.ini index 2d05a98288..ac5b278219 100644 --- a/tox.ini +++ b/tox.ini @@ -75,6 +75,10 @@ envlist = py3{4,5,6,7,8}-test-ext-opencensusexporter ; ext-opencensusexporter intentionally excluded from pypy3 + ; opentelemetry-ext-otlp + py3{5,6,7,8}-test-ext-otlp + ; ext-otlp intentionally excluded from pypy3 + ; opentelemetry-ext-prometheus py3{4,5,6,7,8}-test-ext-prometheus pypy3-test-ext-prometheus @@ -170,6 +174,7 @@ changedir = test-ext-django: ext/opentelemetry-ext-django/tests test-ext-mysql: ext/opentelemetry-ext-mysql/tests test-ext-opencensusexporter: ext/opentelemetry-ext-opencensusexporter/tests + test-ext-otlp: ext/opentelemetry-ext-otlp/tests test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-exporter-cloud-trace: ext/opentelemetry-exporter-cloud-trace/tests @@ -226,6 +231,9 @@ commands_pre = opencensusexporter: pip install {toxinidir}/ext/opentelemetry-ext-opencensusexporter + otlp: pip install {toxinidir}/opentelemetry-proto + otlp: pip install {toxinidir}/ext/opentelemetry-ext-otlp + prometheus: pip install {toxinidir}/ext/opentelemetry-ext-prometheus pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo[test] From a9f004a10ff77ef0fbf3d62ed1c1187b389c7b2a Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Wed, 10 Jun 2020 23:15:47 -0400 Subject: [PATCH 0404/1517] chore: add opentelemetry-test as a dependancy (#809) opentelemetry-test was not listed as a test dependency in the asgi instrumentation. --- eachdist.ini | 1 + ext/opentelemetry-ext-asgi/setup.cfg | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/eachdist.ini b/eachdist.ini index e052f9cf85..a2822d97b4 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -6,6 +6,7 @@ sortfirst= opentelemetry-sdk opentelemetry-instrumentation opentelemetry-proto + tests/util ext/opentelemetry-ext-wsgi ext/opentelemetry-ext-dbapi ext/* diff --git a/ext/opentelemetry-ext-asgi/setup.cfg b/ext/opentelemetry-ext-asgi/setup.cfg index 2be30ce41c..deac9d0f22 100644 --- a/ext/opentelemetry-ext-asgi/setup.cfg +++ b/ext/opentelemetry-ext-asgi/setup.cfg @@ -44,7 +44,7 @@ install_requires = [options.extras_require] test = - opentelemetry-ext-testutil + opentelemetry-test [options.packages.find] where = src From 5ea4ccfde6c07df494de71f3e19109f90a5d7bd6 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 10 Jun 2020 20:47:21 -0700 Subject: [PATCH 0405/1517] chore: standardizing setup.cfg files (#807) Minor cleanup of setup.cfg files. Updated the example app to move configuration from setup.py into setup.cfg as well. Co-authored-by: Diego Hurtado Co-authored-by: Yusuke Tsutsumi --- .../opentelemetry-example-app/setup.cfg | 62 +++++++++++++++++++ .../opentelemetry-example-app/setup.py | 47 +++----------- .../src/opentelemetry_example_app/version.py | 15 +++++ .../src/opentelemetry/ext/boto/version.py | 2 +- ext/opentelemetry-ext-datadog/setup.cfg | 6 +- opentelemetry-sdk/setup.cfg | 3 +- 6 files changed, 92 insertions(+), 43 deletions(-) create mode 100644 docs/examples/opentelemetry-example-app/setup.cfg create mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg new file mode 100644 index 0000000000..f00f246e40 --- /dev/null +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -0,0 +1,62 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-example-app +description = OpenTelemetry Example App +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-example-app +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages = find_namespace: +zip_safe = False +include_package_data = True +install_requires = + typing; python_version<'3.5' + opentelemetry-api == 0.9.dev0 + opentelemetry-sdk == 0.9.dev0 + opentelemetry-ext-requests == 0.9.dev0 + opentelemetry-ext-flask == 0.9.dev0 + flask + requests + protobuf~=3.11 + +[options.packages.find] +where = src +include = opentelemetry_example_app + +[options.entry_points] +opentelemetry_meter_provider = + sdk_meter_provider = opentelemetry.sdk.metrics:MeterProvider +opentelemetry_tracer_provider = + sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider diff --git a/docs/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py index 02333384af..07b731b24e 100644 --- a/docs/examples/opentelemetry-example-app/setup.py +++ b/docs/examples/opentelemetry-example-app/setup.py @@ -11,45 +11,16 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import os import setuptools -setuptools.setup( - name="opentelemetry-example-app", - version="0.9.dev0", - author="OpenTelemetry Authors", - author_email="cncf-opentelemetry-contributors@lists.cncf.io", - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - ], - description="OpenTelemetry Python API", - include_package_data=True, - long_description=open("README.rst").read(), - install_requires=[ - "typing; python_version<'3.5'", - "opentelemetry-api", - "opentelemetry-sdk", - "opentelemetry-ext-requests", - "opentelemetry-ext-flask", - "flask", - "requests", - "protobuf~=3.11", - ], - extras_require={"test": []}, - license="Apache-2.0", - package_dir={"": "src"}, - packages=setuptools.find_namespace_packages(where="src"), - url=( - "https://github.com/open-telemetry/opentelemetry-python" - "/tree/master/opentelemetry-example-app" - ), - zip_safe=False, +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry_example_app", "version.py" ) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"],) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py new file mode 100644 index 0000000000..603bf0b7e5 --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py index bcf6a35777..603bf0b7e5 100644 --- a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.dev0" +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-ext-datadog/setup.cfg b/ext/opentelemetry-ext-datadog/setup.cfg index 074206dc82..3677c88108 100644 --- a/ext/opentelemetry-ext-datadog/setup.cfg +++ b/ext/opentelemetry-ext-datadog/setup.cfg @@ -39,9 +39,9 @@ package_dir= =src packages=find_namespace: install_requires = - ddtrace>=0.34.0 - opentelemetry-api==0.9.dev0 - opentelemetry-sdk==0.9.dev0 + ddtrace >= 0.34.0 + opentelemetry-api == 0.9.dev0 + opentelemetry-sdk == 0.9.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index a5cff106af..c7209fa981 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,7 +41,8 @@ package_dir= packages=find_namespace: zip_safe = False include_package_data = True -install_requires = opentelemetry-api==0.9.dev0 +install_requires = + opentelemetry-api == 0.9.dev0 [options.packages.find] where = src From dff34f4935028875360673540d1d9bfe24dab5e0 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 10 Jun 2020 21:35:03 -0700 Subject: [PATCH 0406/1517] release: updating version numbers and changelogs (#812) --- .../opentelemetry-example-app/setup.py | 41 +++++++++++++++++-- .../exporter/cloud_monitoring/version.py | 2 +- .../exporter/cloud_trace/version.py | 2 +- .../setup.cfg | 4 +- .../ext/aiohttp_client/version.py | 2 +- ext/opentelemetry-ext-asgi/setup.cfg | 2 +- .../src/opentelemetry/ext/asgi/version.py | 2 +- ext/opentelemetry-ext-boto/CHANGELOG.md | 4 ++ ext/opentelemetry-ext-boto/setup.cfg | 6 +-- .../src/opentelemetry/ext/boto/version.py | 2 +- ext/opentelemetry-ext-botocore/CHANGELOG.md | 4 ++ ext/opentelemetry-ext-botocore/setup.cfg | 6 +-- .../src/opentelemetry/ext/botocore/version.py | 2 +- ext/opentelemetry-ext-datadog/setup.cfg | 6 +-- .../src/opentelemetry/ext/datadog/version.py | 2 +- ext/opentelemetry-ext-dbapi/setup.cfg | 6 +-- .../src/opentelemetry/ext/dbapi/version.py | 2 +- ext/opentelemetry-ext-django/setup.cfg | 8 ++-- .../src/opentelemetry/ext/django/version.py | 2 +- ext/opentelemetry-ext-flask/setup.cfg | 8 ++-- .../src/opentelemetry/ext/flask/version.py | 2 +- ext/opentelemetry-ext-grpc/setup.cfg | 6 +-- .../src/opentelemetry/ext/grpc/version.py | 2 +- ext/opentelemetry-ext-jaeger/setup.cfg | 4 +- .../src/opentelemetry/ext/jaeger/version.py | 2 +- ext/opentelemetry-ext-jinja2/setup.cfg | 6 +-- .../src/opentelemetry/ext/jinja2/version.py | 2 +- ext/opentelemetry-ext-mysql/setup.cfg | 8 ++-- .../src/opentelemetry/ext/mysql/version.py | 2 +- .../setup.cfg | 4 +- .../ext/opencensusexporter/version.py | 2 +- .../setup.cfg | 4 +- .../ext/opentracing_shim/version.py | 2 +- ext/opentelemetry-ext-otlp/CHANGELOG.md | 4 ++ ext/opentelemetry-ext-otlp/setup.cfg | 6 +-- .../src/opentelemetry/ext/otlp/version.py | 2 +- ext/opentelemetry-ext-prometheus/setup.cfg | 4 +- .../opentelemetry/ext/prometheus/version.py | 2 +- ext/opentelemetry-ext-psycopg2/setup.cfg | 8 ++-- .../src/opentelemetry/ext/psycopg2/version.py | 2 +- ext/opentelemetry-ext-pymongo/setup.cfg | 6 +-- .../src/opentelemetry/ext/pymongo/version.py | 2 +- ext/opentelemetry-ext-pymysql/setup.cfg | 8 ++-- .../src/opentelemetry/ext/pymysql/version.py | 2 +- ext/opentelemetry-ext-pyramid/CHANGELOG.md | 4 ++ ext/opentelemetry-ext-pyramid/setup.cfg | 8 ++-- .../src/opentelemetry/ext/pyramid/version.py | 2 +- ext/opentelemetry-ext-redis/setup.cfg | 8 ++-- .../src/opentelemetry/ext/redis/version.py | 2 +- ext/opentelemetry-ext-requests/setup.cfg | 6 +-- .../src/opentelemetry/ext/requests/version.py | 2 +- ext/opentelemetry-ext-sqlalchemy/setup.cfg | 6 +-- .../opentelemetry/ext/sqlalchemy/version.py | 2 +- ext/opentelemetry-ext-sqlite3/setup.cfg | 8 ++-- .../src/opentelemetry/ext/sqlite3/version.py | 2 +- .../CHANGELOG.md | 4 ++ .../setup.cfg | 4 +- .../ext/system_metrics/version.py | 2 +- ext/opentelemetry-ext-wsgi/setup.cfg | 6 +-- .../src/opentelemetry/ext/wsgi/version.py | 2 +- ext/opentelemetry-ext-zipkin/setup.cfg | 4 +- .../src/opentelemetry/ext/zipkin/version.py | 2 +- opentelemetry-api/CHANGELOG.md | 4 ++ .../src/opentelemetry/version.py | 2 +- opentelemetry-instrumentation/CHANGELOG.md | 4 ++ opentelemetry-instrumentation/setup.cfg | 2 +- .../opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/CHANGELOG.md | 4 ++ .../src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 4 ++ opentelemetry-sdk/setup.cfg | 3 +- .../src/opentelemetry/sdk/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 73 files changed, 190 insertions(+), 120 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py index 07b731b24e..a4dc33c217 100644 --- a/docs/examples/opentelemetry-example-app/setup.py +++ b/docs/examples/opentelemetry-example-app/setup.py @@ -15,9 +15,44 @@ import setuptools -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry_example_app", "version.py" +setuptools.setup( + name="opentelemetry-example-app", + version="0.9b0", + author="OpenTelemetry Authors", + author_email="cncf-opentelemetry-contributors@lists.cncf.io", + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + ], + description="OpenTelemetry Python API", + include_package_data=True, + long_description=open("README.rst").read(), + install_requires=[ + "typing; python_version<'3.5'", + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-ext-requests", + "opentelemetry-ext-flask", + "flask", + "requests", + "protobuf~=3.11", + ], + extras_require={"test": []}, + license="Apache-2.0", + package_dir={"": "src"}, + packages=setuptools.find_namespace_packages(where="src"), + url=( + "https://github.com/open-telemetry/opentelemetry-python" + "/tree/master/opentelemetry-example-app" + ), + zip_safe=False, ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py index f83f20e7ba..197f5f2372 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py +++ b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py index f83f20e7ba..197f5f2372 100644 --- a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-aiohttp-client/setup.cfg b/ext/opentelemetry-ext-aiohttp-client/setup.cfg index 74fc3a9a86..8efccb6834 100644 --- a/ext/opentelemetry-ext-aiohttp-client/setup.cfg +++ b/ext/opentelemetry-ext-aiohttp-client/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api >= 0.9b0 + opentelemetry-instrumentation == 0.9b0 aiohttp ~= 3.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py index 410979c2eb..8625eb744e 100644 --- a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py +++ b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-asgi/setup.cfg b/ext/opentelemetry-ext-asgi/setup.cfg index deac9d0f22..2f98c80ed2 100644 --- a/ext/opentelemetry-ext-asgi/setup.cfg +++ b/ext/opentelemetry-ext-asgi/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 + opentelemetry-api == 0.9b0 asgiref ~= 3.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py +++ b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-boto/CHANGELOG.md b/ext/opentelemetry-ext-boto/CHANGELOG.md index 3e04402cea..896a782491 100644 --- a/ext/opentelemetry-ext-boto/CHANGELOG.md +++ b/ext/opentelemetry-ext-boto/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.9b0 + +Released 2020-06-10 + - Initial release diff --git a/ext/opentelemetry-ext-boto/setup.cfg b/ext/opentelemetry-ext-boto/setup.cfg index d66b59875f..a3bc7aa934 100644 --- a/ext/opentelemetry-ext-boto/setup.cfg +++ b/ext/opentelemetry-ext-boto/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = boto ~= 2.0 - opentelemetry-api == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-instrumentation == 0.9b0 [options.extras_require] test = boto~=2.0 moto~=1.0 - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-botocore/CHANGELOG.md b/ext/opentelemetry-ext-botocore/CHANGELOG.md index 3e04402cea..896a782491 100644 --- a/ext/opentelemetry-ext-botocore/CHANGELOG.md +++ b/ext/opentelemetry-ext-botocore/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.9b0 + +Released 2020-06-10 + - Initial release diff --git a/ext/opentelemetry-ext-botocore/setup.cfg b/ext/opentelemetry-ext-botocore/setup.cfg index 2cc134969e..df69901826 100644 --- a/ext/opentelemetry-ext-botocore/setup.cfg +++ b/ext/opentelemetry-ext-botocore/setup.cfg @@ -41,13 +41,13 @@ package_dir= packages=find_namespace: install_requires = botocore ~= 1.0 - opentelemetry-api == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-instrumentation == 0.9b0 [options.extras_require] test = moto ~= 1.0 - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py +++ b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-datadog/setup.cfg b/ext/opentelemetry-ext-datadog/setup.cfg index 3677c88108..778b9e89dd 100644 --- a/ext/opentelemetry-ext-datadog/setup.cfg +++ b/ext/opentelemetry-ext-datadog/setup.cfg @@ -39,9 +39,9 @@ package_dir= =src packages=find_namespace: install_requires = - ddtrace >= 0.34.0 - opentelemetry-api == 0.9.dev0 - opentelemetry-sdk == 0.9.dev0 + ddtrace>=0.34.0 + opentelemetry-api==0.9b0 + opentelemetry-sdk==0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 680e6c819c..10dd2e361b 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-instrumentation == 0.9b0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-django/setup.cfg b/ext/opentelemetry-ext-django/setup.cfg index e590cdf9a8..091c49fcf6 100644 --- a/ext/opentelemetry-ext-django/setup.cfg +++ b/ext/opentelemetry-ext-django/setup.cfg @@ -41,13 +41,13 @@ package_dir= packages=find_namespace: install_requires = django >= 1.10 - opentelemetry-ext-wsgi == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 - opentelemetry-api == 0.9.dev0 + opentelemetry-ext-wsgi == 0.9b0 + opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.9b0 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index 57ee7b5ed9..1b183adc3d 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-ext-wsgi == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 - opentelemetry-api == 0.9.dev0 + opentelemetry-ext-wsgi == 0.9b0 + opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.9b0 [options.extras_require] test = flask~=1.0 - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index a45ee3c647..0ec3d09645 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 + opentelemetry-api == 0.9b0 grpcio ~= 1.27 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 - opentelemetry-sdk == 0.9.dev0 + opentelemetry-test == 0.9b0 + opentelemetry-sdk == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/ext/opentelemetry-ext-jaeger/setup.cfg index 9d734606bb..a9e84f1649 100644 --- a/ext/opentelemetry-ext-jaeger/setup.cfg +++ b/ext/opentelemetry-ext-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.9.dev0 - opentelemetry-sdk == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-sdk == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index 3f34fbd58e..8dc13ff7f5 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-jinja2/setup.cfg b/ext/opentelemetry-ext-jinja2/setup.cfg index 7179cdd931..54fb026b6f 100644 --- a/ext/opentelemetry-ext-jinja2/setup.cfg +++ b/ext/opentelemetry-ext-jinja2/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-instrumentation == 0.9b0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py +++ b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 95fe29b864..8f32093c88 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 - opentelemetry-ext-dbapi == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-ext-dbapi == 0.9b0 + opentelemetry-instrumentation == 0.9b0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-opencensusexporter/setup.cfg b/ext/opentelemetry-ext-opencensusexporter/setup.cfg index 96c30ac9af..6e5b1a29e6 100644 --- a/ext/opentelemetry-ext-opencensusexporter/setup.cfg +++ b/ext/opentelemetry-ext-opencensusexporter/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.9.dev0 - opentelemetry-sdk == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-sdk == 0.9b0 protobuf >= 3.8.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py +++ b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg index ceb7d4c0ad..2e70d6c10a 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.9.dev0 + opentelemetry-api == 0.9b0 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-otlp/CHANGELOG.md b/ext/opentelemetry-ext-otlp/CHANGELOG.md index 3e04402cea..896a782491 100644 --- a/ext/opentelemetry-ext-otlp/CHANGELOG.md +++ b/ext/opentelemetry-ext-otlp/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.9b0 + +Released 2020-06-10 + - Initial release diff --git a/ext/opentelemetry-ext-otlp/setup.cfg b/ext/opentelemetry-ext-otlp/setup.cfg index 4117da11e1..2d4a9b495a 100644 --- a/ext/opentelemetry-ext-otlp/setup.cfg +++ b/ext/opentelemetry-ext-otlp/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.9.dev0 - opentelemetry-sdk == 0.9.dev0 - opentelemetry-proto == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-sdk == 0.9b0 + opentelemetry-proto == 0.9b0 backoff ~= 1.10.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py +++ b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/ext/opentelemetry-ext-prometheus/setup.cfg index 14b9f409a4..95d098de60 100644 --- a/ext/opentelemetry-ext-prometheus/setup.cfg +++ b/ext/opentelemetry-ext-prometheus/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.9.dev0 - opentelemetry-sdk == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-sdk == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index ec4852a15b..d51985b86e 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 - opentelemetry-ext-dbapi == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-ext-dbapi == 0.9b0 + opentelemetry-instrumentation == 0.9b0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index 39d4849297..b27a266aa2 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-instrumentation == 0.9b0 pymongo ~= 3.1 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-pymysql/setup.cfg b/ext/opentelemetry-ext-pymysql/setup.cfg index 13a0c7e350..e157fdb1ba 100644 --- a/ext/opentelemetry-ext-pymysql/setup.cfg +++ b/ext/opentelemetry-ext-pymysql/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 - opentelemetry-ext-dbapi == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-ext-dbapi == 0.9b0 + opentelemetry-instrumentation == 0.9b0 PyMySQL ~= 0.9.3 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-pyramid/CHANGELOG.md b/ext/opentelemetry-ext-pyramid/CHANGELOG.md index 33144da913..efb1a08bbc 100644 --- a/ext/opentelemetry-ext-pyramid/CHANGELOG.md +++ b/ext/opentelemetry-ext-pyramid/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.9b0 + +Released 2020-06-10 + - Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-pyramid/setup.cfg b/ext/opentelemetry-ext-pyramid/setup.cfg index 7170bcf582..206376e1eb 100644 --- a/ext/opentelemetry-ext-pyramid/setup.cfg +++ b/ext/opentelemetry-ext-pyramid/setup.cfg @@ -41,15 +41,15 @@ package_dir= packages=find_namespace: install_requires = pyramid >= 1.7 - opentelemetry-instrumentation == 0.9.dev0 - opentelemetry-api == 0.9.dev0 - opentelemetry-ext-wsgi == 0.9.dev0 + opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.9b0 + opentelemetry-ext-wsgi == 0.9b0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = werkzeug == 0.16.1 - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py +++ b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-redis/setup.cfg b/ext/opentelemetry-ext-redis/setup.cfg index fab8cbd8f3..6536a7ad34 100644 --- a/ext/opentelemetry-ext-redis/setup.cfg +++ b/ext/opentelemetry-ext-redis/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-instrumentation == 0.9b0 redis >= 2.6 wrapt >= 1.12.1 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 - opentelemetry-sdk == 0.9.dev0 + opentelemetry-test == 0.9b0 + opentelemetry-sdk == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py +++ b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-requests/setup.cfg b/ext/opentelemetry-ext-requests/setup.cfg index 8a02b74662..d24d82f298 100644 --- a/ext/opentelemetry-ext-requests/setup.cfg +++ b/ext/opentelemetry-ext-requests/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-instrumentation == 0.9b0 requests ~= 2.0 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 httpretty ~= 1.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-sqlalchemy/setup.cfg b/ext/opentelemetry-ext-sqlalchemy/setup.cfg index 851adddb76..21fb9d549b 100644 --- a/ext/opentelemetry-ext-sqlalchemy/setup.cfg +++ b/ext/opentelemetry-ext-sqlalchemy/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-instrumentation == 0.9b0 wrapt >= 1.11.2 sqlalchemy [options.extras_require] test = - opentelemetry-sdk == 0.9.dev0 + opentelemetry-sdk == 0.9b0 pytest [options.packages.find] diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py +++ b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-sqlite3/setup.cfg b/ext/opentelemetry-ext-sqlite3/setup.cfg index d5bf161f55..03f5425192 100644 --- a/ext/opentelemetry-ext-sqlite3/setup.cfg +++ b/ext/opentelemetry-ext-sqlite3/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 - opentelemetry-ext-dbapi == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-ext-dbapi == 0.9b0 + opentelemetry-instrumentation == 0.9b0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py +++ b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-system-metrics/CHANGELOG.md b/ext/opentelemetry-ext-system-metrics/CHANGELOG.md index 2f3c3ab382..12d51bb800 100644 --- a/ext/opentelemetry-ext-system-metrics/CHANGELOG.md +++ b/ext/opentelemetry-ext-system-metrics/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.9b0 + +Released 2020-06-10 + - Initial release (https://github.com/open-telemetry/opentelemetry-python/pull/652) diff --git a/ext/opentelemetry-ext-system-metrics/setup.cfg b/ext/opentelemetry-ext-system-metrics/setup.cfg index 25515eaf0d..83b469d551 100644 --- a/ext/opentelemetry-ext-system-metrics/setup.cfg +++ b/ext/opentelemetry-ext-system-metrics/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 + opentelemetry-api == 0.9b0 psutil ~= 5.7.0 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py +++ b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index 270f278717..606e1033ab 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9.dev0 - opentelemetry-instrumentation == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-instrumentation == 0.9b0 [options.extras_require] test = - opentelemetry-test == 0.9.dev0 + opentelemetry-test == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg index cc945f8ba3..26a7f5b4f1 100644 --- a/ext/opentelemetry-ext-zipkin/setup.cfg +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 0.9.dev0 - opentelemetry-sdk == 0.9.dev0 + opentelemetry-api == 0.9b0 + opentelemetry-sdk == 0.9b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py index 603bf0b7e5..12f784a08d 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 670e3d7a7f..10c08c8574 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.9b0 + +Released 2020-06-10 + - Move stateful from Meter to MeterProvider ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 603bf0b7e5..12f784a08d 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/opentelemetry-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md index b3d117db06..d6f04ae069 100644 --- a/opentelemetry-instrumentation/CHANGELOG.md +++ b/opentelemetry-instrumentation/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.9b0 + +Released 2020-06-10 + - Rename opentelemetry-auto-instrumentation to opentelemetry-instrumentation, and console script `opentelemetry-auto-instrumentation` to `opentelemetry-instrument` diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 98c1fa4096..2faa921df6 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.9.dev0 + opentelemetry-api == 0.9b0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 603bf0b7e5..12f784a08d 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/opentelemetry-proto/CHANGELOG.md b/opentelemetry-proto/CHANGELOG.md index 3e04402cea..896a782491 100644 --- a/opentelemetry-proto/CHANGELOG.md +++ b/opentelemetry-proto/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## 0.9b0 + +Released 2020-06-10 + - Initial release diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 603bf0b7e5..12f784a08d 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 746f1ab4a5..b2ddc0d01f 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.9b0 + +Released 2020-06-10 + - Move stateful & resource from Meter to MeterProvider ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index c7209fa981..4653a3bf01 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,8 +41,7 @@ package_dir= packages=find_namespace: zip_safe = False include_package_data = True -install_requires = - opentelemetry-api == 0.9.dev0 +install_requires = opentelemetry-api==0.9b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 603bf0b7e5..12f784a08d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.9b0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 962d67c24b..2133483e25 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.9.dev0" +__version__ = "0.9b0" From 0ce8cb69261adcbfb7e6ac1758248947ac2c8a68 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 10 Jun 2020 21:46:44 -0700 Subject: [PATCH 0407/1517] chore: bump dev version number --- .../opentelemetry-example-app/setup.cfg | 8 ++-- .../opentelemetry-example-app/setup.py | 41 ++----------------- .../exporter/cloud_monitoring/version.py | 2 +- .../exporter/cloud_trace/version.py | 2 +- .../setup.cfg | 4 +- .../ext/aiohttp_client/version.py | 2 +- ext/opentelemetry-ext-asgi/setup.cfg | 2 +- .../src/opentelemetry/ext/asgi/version.py | 2 +- ext/opentelemetry-ext-boto/setup.cfg | 6 +-- .../src/opentelemetry/ext/boto/version.py | 2 +- ext/opentelemetry-ext-botocore/setup.cfg | 6 +-- .../src/opentelemetry/ext/botocore/version.py | 2 +- ext/opentelemetry-ext-datadog/setup.cfg | 4 +- .../src/opentelemetry/ext/datadog/version.py | 2 +- ext/opentelemetry-ext-dbapi/setup.cfg | 6 +-- .../src/opentelemetry/ext/dbapi/version.py | 2 +- ext/opentelemetry-ext-django/setup.cfg | 8 ++-- .../src/opentelemetry/ext/django/version.py | 2 +- ext/opentelemetry-ext-flask/setup.cfg | 8 ++-- .../src/opentelemetry/ext/flask/version.py | 2 +- ext/opentelemetry-ext-grpc/setup.cfg | 6 +-- .../src/opentelemetry/ext/grpc/version.py | 2 +- ext/opentelemetry-ext-jaeger/setup.cfg | 4 +- .../src/opentelemetry/ext/jaeger/version.py | 2 +- ext/opentelemetry-ext-jinja2/setup.cfg | 6 +-- .../src/opentelemetry/ext/jinja2/version.py | 2 +- ext/opentelemetry-ext-mysql/setup.cfg | 8 ++-- .../src/opentelemetry/ext/mysql/version.py | 2 +- .../setup.cfg | 4 +- .../ext/opencensusexporter/version.py | 2 +- .../setup.cfg | 4 +- .../ext/opentracing_shim/version.py | 2 +- ext/opentelemetry-ext-otlp/setup.cfg | 6 +-- .../src/opentelemetry/ext/otlp/version.py | 2 +- ext/opentelemetry-ext-prometheus/setup.cfg | 4 +- .../opentelemetry/ext/prometheus/version.py | 2 +- ext/opentelemetry-ext-psycopg2/setup.cfg | 8 ++-- .../src/opentelemetry/ext/psycopg2/version.py | 2 +- ext/opentelemetry-ext-pymongo/setup.cfg | 6 +-- .../src/opentelemetry/ext/pymongo/version.py | 2 +- ext/opentelemetry-ext-pymysql/setup.cfg | 8 ++-- .../src/opentelemetry/ext/pymysql/version.py | 2 +- ext/opentelemetry-ext-pyramid/setup.cfg | 8 ++-- .../src/opentelemetry/ext/pyramid/version.py | 2 +- ext/opentelemetry-ext-redis/setup.cfg | 8 ++-- .../src/opentelemetry/ext/redis/version.py | 2 +- ext/opentelemetry-ext-requests/setup.cfg | 6 +-- .../src/opentelemetry/ext/requests/version.py | 2 +- ext/opentelemetry-ext-sqlalchemy/setup.cfg | 6 +-- .../opentelemetry/ext/sqlalchemy/version.py | 2 +- ext/opentelemetry-ext-sqlite3/setup.cfg | 8 ++-- .../src/opentelemetry/ext/sqlite3/version.py | 2 +- .../setup.cfg | 4 +- .../ext/system_metrics/version.py | 2 +- ext/opentelemetry-ext-wsgi/setup.cfg | 6 +-- .../src/opentelemetry/ext/wsgi/version.py | 2 +- ext/opentelemetry-ext-zipkin/setup.cfg | 4 +- .../src/opentelemetry/ext/zipkin/version.py | 2 +- .../src/opentelemetry/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../opentelemetry/instrumentation/version.py | 2 +- .../src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 3 +- .../src/opentelemetry/sdk/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 65 files changed, 123 insertions(+), 157 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index f00f246e40..88aa507518 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -43,10 +43,10 @@ zip_safe = False include_package_data = True install_requires = typing; python_version<'3.5' - opentelemetry-api == 0.9.dev0 - opentelemetry-sdk == 0.9.dev0 - opentelemetry-ext-requests == 0.9.dev0 - opentelemetry-ext-flask == 0.9.dev0 + opentelemetry-api == 0.10.dev0 + opentelemetry-sdk == 0.10.dev0 + opentelemetry-ext-requests == 0.10.dev0 + opentelemetry-ext-flask == 0.10.dev0 flask requests protobuf~=3.11 diff --git a/docs/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py index a4dc33c217..07b731b24e 100644 --- a/docs/examples/opentelemetry-example-app/setup.py +++ b/docs/examples/opentelemetry-example-app/setup.py @@ -15,44 +15,9 @@ import setuptools -setuptools.setup( - name="opentelemetry-example-app", - version="0.9b0", - author="OpenTelemetry Authors", - author_email="cncf-opentelemetry-contributors@lists.cncf.io", - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - ], - description="OpenTelemetry Python API", - include_package_data=True, - long_description=open("README.rst").read(), - install_requires=[ - "typing; python_version<'3.5'", - "opentelemetry-api", - "opentelemetry-sdk", - "opentelemetry-ext-requests", - "opentelemetry-ext-flask", - "flask", - "requests", - "protobuf~=3.11", - ], - extras_require={"test": []}, - license="Apache-2.0", - package_dir={"": "src"}, - packages=setuptools.find_namespace_packages(where="src"), - url=( - "https://github.com/open-telemetry/opentelemetry-python" - "/tree/master/opentelemetry-example-app" - ), - zip_safe=False, +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry_example_app", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py index 197f5f2372..3096170ce8 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py +++ b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py index 197f5f2372..3096170ce8 100644 --- a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-aiohttp-client/setup.cfg b/ext/opentelemetry-ext-aiohttp-client/setup.cfg index 8efccb6834..431e92273e 100644 --- a/ext/opentelemetry-ext-aiohttp-client/setup.cfg +++ b/ext/opentelemetry-ext-aiohttp-client/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api >= 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 aiohttp ~= 3.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py index 8625eb744e..63ed5cd81a 100644 --- a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py +++ b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-asgi/setup.cfg b/ext/opentelemetry-ext-asgi/setup.cfg index 2f98c80ed2..8ccd7bd5e4 100644 --- a/ext/opentelemetry-ext-asgi/setup.cfg +++ b/ext/opentelemetry-ext-asgi/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 + opentelemetry-api == 0.10.dev0 asgiref ~= 3.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py +++ b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-boto/setup.cfg b/ext/opentelemetry-ext-boto/setup.cfg index a3bc7aa934..53e26fcf14 100644 --- a/ext/opentelemetry-ext-boto/setup.cfg +++ b/ext/opentelemetry-ext-boto/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = boto ~= 2.0 - opentelemetry-api == 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 [options.extras_require] test = boto~=2.0 moto~=1.0 - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-botocore/setup.cfg b/ext/opentelemetry-ext-botocore/setup.cfg index df69901826..59cb7f2271 100644 --- a/ext/opentelemetry-ext-botocore/setup.cfg +++ b/ext/opentelemetry-ext-botocore/setup.cfg @@ -41,13 +41,13 @@ package_dir= packages=find_namespace: install_requires = botocore ~= 1.0 - opentelemetry-api == 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 [options.extras_require] test = moto ~= 1.0 - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py +++ b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-datadog/setup.cfg b/ext/opentelemetry-ext-datadog/setup.cfg index 778b9e89dd..2d663de89b 100644 --- a/ext/opentelemetry-ext-datadog/setup.cfg +++ b/ext/opentelemetry-ext-datadog/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = ddtrace>=0.34.0 - opentelemetry-api==0.9b0 - opentelemetry-sdk==0.9b0 + opentelemetry-api==0.10.dev0 + opentelemetry-sdk==0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 10dd2e361b..4ea5913e17 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-django/setup.cfg b/ext/opentelemetry-ext-django/setup.cfg index 091c49fcf6..8ed134ff19 100644 --- a/ext/opentelemetry-ext-django/setup.cfg +++ b/ext/opentelemetry-ext-django/setup.cfg @@ -41,13 +41,13 @@ package_dir= packages=find_namespace: install_requires = django >= 1.10 - opentelemetry-ext-wsgi == 0.9b0 - opentelemetry-instrumentation == 0.9b0 - opentelemetry-api == 0.9b0 + opentelemetry-ext-wsgi == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.10.dev0 [options.extras_require] test = - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index 1b183adc3d..4c2602a7b2 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-ext-wsgi == 0.9b0 - opentelemetry-instrumentation == 0.9b0 - opentelemetry-api == 0.9b0 + opentelemetry-ext-wsgi == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.10.dev0 [options.extras_require] test = flask~=1.0 - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index 0ec3d09645..41042ce1b8 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 + opentelemetry-api == 0.10.dev0 grpcio ~= 1.27 [options.extras_require] test = - opentelemetry-test == 0.9b0 - opentelemetry-sdk == 0.9b0 + opentelemetry-test == 0.10.dev0 + opentelemetry-sdk == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/ext/opentelemetry-ext-jaeger/setup.cfg index a9e84f1649..e213e4558e 100644 --- a/ext/opentelemetry-ext-jaeger/setup.cfg +++ b/ext/opentelemetry-ext-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.9b0 - opentelemetry-sdk == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-sdk == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index 8dc13ff7f5..d21fcb8168 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-jinja2/setup.cfg b/ext/opentelemetry-ext-jinja2/setup.cfg index 54fb026b6f..41d6ff2fb8 100644 --- a/ext/opentelemetry-ext-jinja2/setup.cfg +++ b/ext/opentelemetry-ext-jinja2/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py +++ b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 8f32093c88..a00193b2f2 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 - opentelemetry-ext-dbapi == 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-ext-dbapi == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-opencensusexporter/setup.cfg b/ext/opentelemetry-ext-opencensusexporter/setup.cfg index 6e5b1a29e6..c85f3b3d00 100644 --- a/ext/opentelemetry-ext-opencensusexporter/setup.cfg +++ b/ext/opentelemetry-ext-opencensusexporter/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.9b0 - opentelemetry-sdk == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-sdk == 0.10.dev0 protobuf >= 3.8.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py +++ b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg index 2e70d6c10a..7f6b093ea0 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.9b0 + opentelemetry-api == 0.10.dev0 [options.extras_require] test = - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-otlp/setup.cfg b/ext/opentelemetry-ext-otlp/setup.cfg index 2d4a9b495a..57fecc6fed 100644 --- a/ext/opentelemetry-ext-otlp/setup.cfg +++ b/ext/opentelemetry-ext-otlp/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.9b0 - opentelemetry-sdk == 0.9b0 - opentelemetry-proto == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-sdk == 0.10.dev0 + opentelemetry-proto == 0.10.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py +++ b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/ext/opentelemetry-ext-prometheus/setup.cfg index 95d098de60..3e5d5cd957 100644 --- a/ext/opentelemetry-ext-prometheus/setup.cfg +++ b/ext/opentelemetry-ext-prometheus/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.9b0 - opentelemetry-sdk == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-sdk == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index d51985b86e..252ff290d8 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 - opentelemetry-ext-dbapi == 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-ext-dbapi == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index b27a266aa2..6cb5c8cc08 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 pymongo ~= 3.1 [options.extras_require] test = - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-pymysql/setup.cfg b/ext/opentelemetry-ext-pymysql/setup.cfg index e157fdb1ba..031a6562b3 100644 --- a/ext/opentelemetry-ext-pymysql/setup.cfg +++ b/ext/opentelemetry-ext-pymysql/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 - opentelemetry-ext-dbapi == 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-ext-dbapi == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 PyMySQL ~= 0.9.3 [options.extras_require] test = - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-pyramid/setup.cfg b/ext/opentelemetry-ext-pyramid/setup.cfg index 206376e1eb..521f7c85ee 100644 --- a/ext/opentelemetry-ext-pyramid/setup.cfg +++ b/ext/opentelemetry-ext-pyramid/setup.cfg @@ -41,15 +41,15 @@ package_dir= packages=find_namespace: install_requires = pyramid >= 1.7 - opentelemetry-instrumentation == 0.9b0 - opentelemetry-api == 0.9b0 - opentelemetry-ext-wsgi == 0.9b0 + opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.10.dev0 + opentelemetry-ext-wsgi == 0.10.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = werkzeug == 0.16.1 - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py +++ b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-redis/setup.cfg b/ext/opentelemetry-ext-redis/setup.cfg index 6536a7ad34..f6fbbafaaa 100644 --- a/ext/opentelemetry-ext-redis/setup.cfg +++ b/ext/opentelemetry-ext-redis/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 redis >= 2.6 wrapt >= 1.12.1 [options.extras_require] test = - opentelemetry-test == 0.9b0 - opentelemetry-sdk == 0.9b0 + opentelemetry-test == 0.10.dev0 + opentelemetry-sdk == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py +++ b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-requests/setup.cfg b/ext/opentelemetry-ext-requests/setup.cfg index d24d82f298..7bb84b42ba 100644 --- a/ext/opentelemetry-ext-requests/setup.cfg +++ b/ext/opentelemetry-ext-requests/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 requests ~= 2.0 [options.extras_require] test = - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 httpretty ~= 1.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-sqlalchemy/setup.cfg b/ext/opentelemetry-ext-sqlalchemy/setup.cfg index 21fb9d549b..c964b1327b 100644 --- a/ext/opentelemetry-ext-sqlalchemy/setup.cfg +++ b/ext/opentelemetry-ext-sqlalchemy/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 wrapt >= 1.11.2 sqlalchemy [options.extras_require] test = - opentelemetry-sdk == 0.9b0 + opentelemetry-sdk == 0.10.dev0 pytest [options.packages.find] diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py +++ b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-sqlite3/setup.cfg b/ext/opentelemetry-ext-sqlite3/setup.cfg index 03f5425192..a1128ca88f 100644 --- a/ext/opentelemetry-ext-sqlite3/setup.cfg +++ b/ext/opentelemetry-ext-sqlite3/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 - opentelemetry-ext-dbapi == 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-ext-dbapi == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py +++ b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-system-metrics/setup.cfg b/ext/opentelemetry-ext-system-metrics/setup.cfg index 83b469d551..063e7e91b2 100644 --- a/ext/opentelemetry-ext-system-metrics/setup.cfg +++ b/ext/opentelemetry-ext-system-metrics/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 + opentelemetry-api == 0.10.dev0 psutil ~= 5.7.0 [options.extras_require] test = - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py +++ b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index 606e1033ab..48b0697d11 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.9b0 - opentelemetry-instrumentation == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 [options.extras_require] test = - opentelemetry-test == 0.9b0 + opentelemetry-test == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg index 26a7f5b4f1..89d40aaeaa 100644 --- a/ext/opentelemetry-ext-zipkin/setup.cfg +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 0.9b0 - opentelemetry-sdk == 0.9b0 + opentelemetry-api == 0.10.dev0 + opentelemetry-sdk == 0.10.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py index 12f784a08d..6d4fefa599 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 12f784a08d..6d4fefa599 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 2faa921df6..46f956f317 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.9b0 + opentelemetry-api == 0.10.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 12f784a08d..6d4fefa599 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 12f784a08d..6d4fefa599 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 4653a3bf01..68c9984dc9 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,7 +41,8 @@ package_dir= packages=find_namespace: zip_safe = False include_package_data = True -install_requires = opentelemetry-api==0.9b0 +install_requires = + opentelemetry-api == 0.10.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 12f784a08d..6d4fefa599 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9b0" +__version__ = "0.10.dev0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 2133483e25..804c927ded 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.9b0" +__version__ = "0.10.dev0" From 43e78ae8f4aaab7a0aba86699b1b187dab2b777e Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Thu, 11 Jun 2020 15:06:10 -0400 Subject: [PATCH 0408/1517] cloud-trace: add troubleshooting (#795) adding some troubleshooting tips for users with common failures --- docs/examples/cloud_trace_exporter/README.rst | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/examples/cloud_trace_exporter/README.rst b/docs/examples/cloud_trace_exporter/README.rst index 871422356a..7c6c2c4c40 100644 --- a/docs/examples/cloud_trace_exporter/README.rst +++ b/docs/examples/cloud_trace_exporter/README.rst @@ -9,7 +9,7 @@ Basic Example To use this exporter you first need to: * A Google Cloud project. You can `create one here. `_ - * Enable Cloud Trace API (aka StackDriver Trace API) in the project `here. `_ + * Enable Cloud Trace API (aka Stackdriver Trace API) in the project `here. `_ * Enable `Default Application Credentials. `_ * Installation @@ -20,10 +20,11 @@ To use this exporter you first need to: pip install opentelemetry-sdk pip install opentelemetry-exporter-cloud-trace -* Run example +* Run example locally .. code-block:: sh + cd opentelemetry-python/docs/examples/cloud_trace_exporter python basic_trace.py Checking Output @@ -31,4 +32,19 @@ Checking Output After running any of these examples, you can go to `Cloud Trace overview `_ to see the results. -* `More information about exporters in general `_ \ No newline at end of file + +Further Reading +-------------------------- + +* `More information about exporters in general `_ + +Troubleshooting +-------------------------- + +Running basic_trace.py hangs: +############################# + * Make sure you've setup Application Default Credentials. Either run ``gcloud auth application-default login`` or set the ``GOOGLE_APPLICATION_CREDENTIALS`` environment variable to be a path to a service account token file. + +Getting error ``google.api_core.exceptions.ResourceExhausted: 429 Resource has been exhausted``: +################################################################################################ + * Check that you've enabled the `Cloud Trace (Stackdriver Trace) API `_ \ No newline at end of file From ba2e62d40c149aa9f592cd9b20fcf55947ea858e Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Thu, 11 Jun 2020 17:41:22 -0400 Subject: [PATCH 0409/1517] chore: add test coverage for Cloud Monitoring exporter (#804) Previously cloud monitoring was missing coverage. --- .../exporter/cloud_monitoring/__init__.py | 16 ++++--- .../tests/test_cloud_monitoring.py | 46 +++++++++++-------- scripts/coverage.sh | 1 + tox.ini | 10 ++++ 4 files changed, 46 insertions(+), 27 deletions(-) diff --git a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py index 6d6af26677..e0d8ced757 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py +++ b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py @@ -67,16 +67,17 @@ def _get_metric_descriptor( :param record: :return: """ + instrument = record.instrument descriptor_type = "custom.googleapis.com/OpenTelemetry/{}".format( - record.metric.name + instrument.name ) if descriptor_type in self._metric_descriptors: return self._metric_descriptors[descriptor_type] descriptor = { "name": None, "type": descriptor_type, - "display_name": record.metric.name, - "description": record.metric.description, + "display_name": instrument.name, + "description": instrument.description, "labels": [], } for key, value in record.labels: @@ -104,9 +105,9 @@ def _get_metric_descriptor( type(record.aggregator).__name__, ) return None - if record.metric.value_type == int: + if instrument.value_type == int: descriptor["value_type"] = MetricDescriptor.ValueType.INT64 - elif record.metric.value_type == float: + elif instrument.value_type == float: descriptor["value_type"] = MetricDescriptor.ValueType.DOUBLE proto_descriptor = MetricDescriptor(**descriptor) try: @@ -129,6 +130,7 @@ def export( ) -> "MetricsExportResult": all_series = [] for record in metric_records: + instrument = record.instrument metric_descriptor = self._get_metric_descriptor(record) if not metric_descriptor: continue @@ -140,9 +142,9 @@ def export( series.metric.labels[key] = str(value) point = series.points.add() - if record.metric.value_type == int: + if instrument.value_type == int: point.value.int64_value = record.aggregator.checkpoint - elif record.metric.value_type == float: + elif instrument.value_type == float: point.value.double_value = record.aggregator.checkpoint seconds, nanos = divmod( record.aggregator.last_update_timestamp, 1e9 diff --git a/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py b/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py index d7f98e024a..df435738ac 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py +++ b/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py @@ -71,7 +71,9 @@ def test_constructor_explicit(self): def test_batch_write(self): client = mock.Mock() - exporter = CloudMonitoringMetricsExporter(client=client) + exporter = CloudMonitoringMetricsExporter( + project_id=self.project_id, client=client + ) exporter.project_name = self.project_name exporter._batch_write(range(2 * MAX_BATCH_WRITE + 1)) client.create_time_series.assert_has_calls( @@ -100,17 +102,19 @@ def test_batch_write(self): def test_get_metric_descriptor(self): client = mock.Mock() - exporter = CloudMonitoringMetricsExporter(client=client) + exporter = CloudMonitoringMetricsExporter( + project_id=self.project_id, client=client + ) exporter.project_name = self.project_name self.assertIsNone( exporter._get_metric_descriptor( - MetricRecord(UnsupportedAggregator(), (), MockMetric()) + MetricRecord(MockMetric(), (), UnsupportedAggregator()) ) ) record = MetricRecord( - CounterAggregator(), (("label1", "value1"),), MockMetric() + MockMetric(), (("label1", "value1"),), CounterAggregator(), ) metric_descriptor = exporter._get_metric_descriptor(record) client.create_metric_descriptor.assert_called_with( @@ -132,20 +136,20 @@ def test_get_metric_descriptor(self): # Getting a cached metric descriptor shouldn't use another call cached_metric_descriptor = exporter._get_metric_descriptor(record) - client.create_metric_descriptor.assert_called_once() + self.assertEqual(client.create_metric_descriptor.call_count, 1) self.assertEqual(metric_descriptor, cached_metric_descriptor) # Drop labels with values that aren't string, int or bool exporter._get_metric_descriptor( MetricRecord( - CounterAggregator(), + MockMetric(name="name2", value_type=float), ( ("label1", "value1"), ("label2", dict()), ("label3", 3), ("label4", False), ), - MockMetric(name="name2", value_type=float), + CounterAggregator(), ) ) client.create_metric_descriptor.assert_called_with( @@ -169,15 +173,17 @@ def test_get_metric_descriptor(self): def test_export(self): client = mock.Mock() - exporter = CloudMonitoringMetricsExporter(client=client) + exporter = CloudMonitoringMetricsExporter( + project_id=self.project_id, client=client + ) exporter.project_name = self.project_name exporter.export( [ MetricRecord( - UnsupportedAggregator(), - (("label1", "value1"),), MockMetric(), + (("label1", "value1"),), + UnsupportedAggregator(), ) ] ) @@ -204,14 +210,14 @@ def test_export(self): exporter.export( [ MetricRecord( - counter_one, - (("label1", "value1"), ("label2", 1),), MockMetric(), + (("label1", "value1"), ("label2", 1),), + counter_one, ), MetricRecord( - counter_one, - (("label1", "value2"), ("label2", 2),), MockMetric(), + (("label1", "value2"), ("label2", 2),), + counter_one, ), ] ) @@ -245,14 +251,14 @@ def test_export(self): exporter.export( [ MetricRecord( - counter_two, - (("label1", "value1"), ("label2", 1),), MockMetric(), + (("label1", "value1"), ("label2", 1),), + counter_two, ), MetricRecord( - counter_two, - (("label1", "value2"), ("label2", 2),), MockMetric(), + (("label1", "value2"), ("label2", 2),), + counter_two, ), ] ) @@ -263,9 +269,9 @@ def test_export(self): exporter.export( [ MetricRecord( - counter_two, - (("label1", "changed_label"), ("label2", 2),), MockMetric(), + (("label1", "changed_label"), ("label2", 2),), + counter_two, ), ] ) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 1794cdf01b..a88aca7475 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -37,6 +37,7 @@ cov ext/opentelemetry-ext-requests cov ext/opentelemetry-ext-jaeger cov ext/opentelemetry-ext-opentracing-shim cov ext/opentelemetry-exporter-cloud-trace +cov ext/opentelemetry-exporter-cloud-monitoring cov ext/opentelemetry-ext-wsgi cov ext/opentelemetry-ext-zipkin cov docs/examples/opentelemetry-example-app diff --git a/tox.ini b/tox.ini index ac5b278219..a46026a244 100644 --- a/tox.ini +++ b/tox.ini @@ -142,6 +142,12 @@ envlist = ; Coverage is temporarily disabled for pypy3 due to the pytest bug. ; pypy3-coverage + ; opentelemetry-exporter-cloud-monitoring + py3{4,5,6,7,8}-test-exporter-cloud-monitoring + + ; opentelemetry-exporter-cloud-trace + py3{4,5,6,7,8}-test-exporter-cloud-trace + lint py38-tracecontext py38-{mypy,mypyinstalled} @@ -178,6 +184,7 @@ changedir = test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-exporter-cloud-trace: ext/opentelemetry-exporter-cloud-trace/tests + test-exporter-cloud-monitoring: ext/opentelemetry-exporter-cloud-monitoring/tests test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests test-ext-pymysql: ext/opentelemetry-ext-pymysql/tests test-ext-pyramid: ext/opentelemetry-ext-pyramid/tests @@ -267,6 +274,9 @@ commands_pre = system-metrics: pip install {toxinidir}/ext/opentelemetry-ext-system-metrics[test] + exporter-cloud-monitoring: pip install {toxinidir}/ext/opentelemetry-exporter-cloud-monitoring + exporter-cloud-trace: pip install {toxinidir}/ext/opentelemetry-exporter-cloud-trace + ; In order to get a healthy coverage report, ; we have to install packages in editable mode. coverage: python {toxinidir}/scripts/eachdist.py install --editable From d22ed887e5681f43e411e901cd3eafa78ad19e28 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 11 Jun 2020 21:47:02 -0700 Subject: [PATCH 0410/1517] api: Adding record_error to span API (#790) As per open-telemetry/opentelemetry-specification#427, we need an interface for users to record errors. --- opentelemetry-api/src/opentelemetry/trace/span.py | 7 +++++++ .../src/opentelemetry/sdk/trace/__init__.py | 12 ++++++++++++ opentelemetry-sdk/tests/trace/test_trace.py | 14 ++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 7f0f39d92a..b20979397d 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -88,6 +88,10 @@ def set_status(self, status: Status) -> None: Span status, which is OK. """ + @abc.abstractmethod + def record_error(self, err: Exception) -> None: + """Records an error as a span event.""" + def __enter__(self) -> "Span": """Invoked when `Span` is used as a context manager. @@ -253,6 +257,9 @@ def update_name(self, name: str) -> None: def set_status(self, status: Status) -> None: pass + def record_error(self, err: Exception) -> None: + pass + INVALID_SPAN_ID = 0x0000000000000000 INVALID_TRACE_ID = 0x00000000000000000000000000000000 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 45aa491fa1..db377c0924 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -20,6 +20,7 @@ import logging import random import threading +import traceback from collections import OrderedDict from contextlib import contextmanager from types import TracebackType @@ -681,6 +682,17 @@ def __exit__( super().__exit__(exc_type, exc_val, exc_tb) + def record_error(self, err: Exception) -> None: + """Records an error as a span event.""" + self.add_event( + name="error", + attributes={ + "error.type": err.__class__.__name__, + "error.message": str(err), + "error.stack": traceback.format_exc(), + }, + ) + def generate_span_id() -> int: """Get a new random span ID. diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 2a8c834e75..d68d5ca420 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -801,6 +801,20 @@ def error_status_test(context): .start_as_current_span("root") ) + def test_record_error(self): + span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + try: + raise ValueError("invalid") + except ValueError as err: + span.record_error(err) + error_event = span.events[0] + self.assertEqual("error", error_event.name) + self.assertEqual("invalid", error_event.attributes["error.message"]) + self.assertEqual("ValueError", error_event.attributes["error.type"]) + self.assertIn( + "ValueError: invalid", error_event.attributes["error.stack"] + ) + def span_event_start_fmt(span_processor_name, span_name): return span_processor_name + ":" + span_name + ":start" From ca232c9326a1568be45566817e1b518edcea7563 Mon Sep 17 00:00:00 2001 From: Eric Mustin Date: Fri, 12 Jun 2020 16:45:28 +0200 Subject: [PATCH 0411/1517] pymemcache: Add pymemcache instrumentation (#772) initial implementation --- docs-requirements.txt | 1 + docs/ext/pymemcache/pymemcache.rst | 7 + .../src/opentelemetry/ext/datadog/exporter.py | 1 + ext/opentelemetry-ext-pymemcache/CHANGELOG.md | 5 + ext/opentelemetry-ext-pymemcache/LICENSE | 201 +++++++ ext/opentelemetry-ext-pymemcache/MANIFEST.IN | 9 + ext/opentelemetry-ext-pymemcache/README.rst | 20 + ext/opentelemetry-ext-pymemcache/setup.cfg | 57 ++ ext/opentelemetry-ext-pymemcache/setup.py | 27 + .../opentelemetry/ext/pymemcache/__init__.py | 197 +++++++ .../opentelemetry/ext/pymemcache/version.py | 15 + .../tests/__init__.py | 0 .../tests/test_pymemcache.py | 524 ++++++++++++++++++ .../tests/utils.py | 74 +++ tox.ini | 7 + 15 files changed, 1145 insertions(+) create mode 100644 docs/ext/pymemcache/pymemcache.rst create mode 100644 ext/opentelemetry-ext-pymemcache/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-pymemcache/LICENSE create mode 100644 ext/opentelemetry-ext-pymemcache/MANIFEST.IN create mode 100644 ext/opentelemetry-ext-pymemcache/README.rst create mode 100644 ext/opentelemetry-ext-pymemcache/setup.cfg create mode 100644 ext/opentelemetry-ext-pymemcache/setup.py create mode 100644 ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/__init__.py create mode 100644 ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py create mode 100644 ext/opentelemetry-ext-pymemcache/tests/__init__.py create mode 100644 ext/opentelemetry-ext-pymemcache/tests/test_pymemcache.py create mode 100644 ext/opentelemetry-ext-pymemcache/tests/utils.py diff --git a/docs-requirements.txt b/docs-requirements.txt index 60f8cdc2cf..b045ed589b 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -14,6 +14,7 @@ mysql-connector-python~=8.0 opentracing~=2.2.0 prometheus_client>=0.5.0,<1.0.0 psycopg2-binary>=2.7.3.1 +pymemcache~=1.3 pymongo~=3.1 redis>=2.6 sqlalchemy>=1.0 diff --git a/docs/ext/pymemcache/pymemcache.rst b/docs/ext/pymemcache/pymemcache.rst new file mode 100644 index 0000000000..c64e00cb59 --- /dev/null +++ b/docs/ext/pymemcache/pymemcache.rst @@ -0,0 +1,7 @@ +OpenTelemetry pymemcache Integration +==================================== + +.. automodule:: opentelemetry.ext.pymemcache + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py index 35d0f98203..14e27dea2a 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py @@ -40,6 +40,7 @@ "opentelemetry.ext.jinja2": DatadogSpanTypes.TEMPLATE, "opentelemetry.ext.mysql": DatadogSpanTypes.SQL, "opentelemetry.ext.psycopg2": DatadogSpanTypes.SQL, + "opentelemetry.ext.pymemcache": DatadogSpanTypes.CACHE, "opentelemetry.ext.pymongo": DatadogSpanTypes.MONGODB, "opentelemetry.ext.pymysql": DatadogSpanTypes.SQL, "opentelemetry.ext.redis": DatadogSpanTypes.REDIS, diff --git a/ext/opentelemetry-ext-pymemcache/CHANGELOG.md b/ext/opentelemetry-ext-pymemcache/CHANGELOG.md new file mode 100644 index 0000000000..33144da913 --- /dev/null +++ b/ext/opentelemetry-ext-pymemcache/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-pymemcache/LICENSE b/ext/opentelemetry-ext-pymemcache/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-pymemcache/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-pymemcache/MANIFEST.IN b/ext/opentelemetry-ext-pymemcache/MANIFEST.IN new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-pymemcache/MANIFEST.IN @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-pymemcache/README.rst b/ext/opentelemetry-ext-pymemcache/README.rst new file mode 100644 index 0000000000..6328ff5f01 --- /dev/null +++ b/ext/opentelemetry-ext-pymemcache/README.rst @@ -0,0 +1,20 @@ +OpenTelemetry pymemcache Integration +==================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-pymemcache.svg + :target: https://pypi.org/project/opentelemetry-ext-pymemcache/ + +Installation +------------ + +:: + + pip install opentelemetry-ext-pymemcache + + +References +---------- +* `OpenTelemetry Pymemcache Integration `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-pymemcache/setup.cfg b/ext/opentelemetry-ext-pymemcache/setup.cfg new file mode 100644 index 0000000000..631d47579b --- /dev/null +++ b/ext/opentelemetry-ext-pymemcache/setup.cfg @@ -0,0 +1,57 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-pymemcache +description = OpenTelemetry pymemcache integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-pymemcache +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 + pymemcache ~= 1.3 + wrapt >= 1.0.0, < 2.0.0 + +[options.extras_require] +test = + opentelemetry-test == 0.10.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + pymemcache = opentelemetry.ext.pymemcache:PymemcacheInstrumentor diff --git a/ext/opentelemetry-ext-pymemcache/setup.py b/ext/opentelemetry-ext-pymemcache/setup.py new file mode 100644 index 0000000000..4b730b4f31 --- /dev/null +++ b/ext/opentelemetry-ext-pymemcache/setup.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "pymemcache", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/__init__.py b/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/__init__.py new file mode 100644 index 0000000000..e63bbe6f13 --- /dev/null +++ b/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/__init__.py @@ -0,0 +1,197 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" + +Usage +----- + +The OpenTelemetry ``pymemcache`` integration traces pymemcache client operations + +Usage +----- + +.. code-block:: python + + from opentelemetry import trace + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.ext.pymemcache import PymemcacheInstrumentor + trace.set_tracer_provider(TracerProvider()) + PymemcacheInstrumentor().instrument() + from pymemcache.client.base import Client + client = Client(('localhost', 11211)) + client.set('some_key', 'some_value') + +API +--- +""" +# pylint: disable=no-value-for-parameter + +import logging + +import pymemcache +from wrapt import ObjectProxy +from wrapt import wrap_function_wrapper as _wrap + +from opentelemetry.ext.pymemcache.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.trace import SpanKind, get_tracer + +logger = logging.getLogger(__name__) + +# Network attribute semantic convention here: +# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/span-general.md#general-network-connection-attributes +_HOST = "net.peer.name" +_PORT = "net.peer.port" +# Database semantic conventions here: +# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/database.md +_DB = "db.type" +_URL = "db.url" + +_DEFAULT_SERVICE = "memcached" +_RAWCMD = "db.statement" +_CMD = "memcached.command" +COMMANDS = [ + "set", + "set_many", + "add", + "replace", + "append", + "prepend", + "cas", + "get", + "get_many", + "gets", + "gets_many", + "delete", + "delete_many", + "incr", + "decr", + "touch", + "stats", + "version", + "flush_all", + "quit", + "set_multi", + "get_multi", +] + + +def _set_connection_attributes(span, instance): + for key, value in _get_address_attributes(instance).items(): + span.set_attribute(key, value) + + +def _with_tracer_wrapper(func): + """Helper for providing tracer for wrapper functions. + """ + + def _with_tracer(tracer, cmd): + def wrapper(wrapped, instance, args, kwargs): + # prevent double wrapping + if hasattr(wrapped, "__wrapped__"): + return wrapped(*args, **kwargs) + + return func(tracer, cmd, wrapped, instance, args, kwargs) + + return wrapper + + return _with_tracer + + +@_with_tracer_wrapper +def _wrap_cmd(tracer, cmd, wrapped, instance, args, kwargs): + with tracer.start_as_current_span( + _CMD, kind=SpanKind.INTERNAL, attributes={} + ) as span: + try: + if not args: + vals = "" + else: + vals = _get_query_string(args[0]) + + query = "{}{}{}".format(cmd, " " if vals else "", vals) + span.set_attribute(_RAWCMD, query) + + _set_connection_attributes(span, instance) + except Exception as ex: # pylint: disable=broad-except + logger.warning( + "Failed to set attributes for pymemcache span %s", str(ex) + ) + + return wrapped(*args, **kwargs) + + +def _get_query_string(arg): + + """Return the query values given the first argument to a pymemcache command. + + If there are multiple query values, they are joined together + space-separated. + """ + keys = "" + + if isinstance(arg, dict): + arg = list(arg) + + if isinstance(arg, str): + keys = arg + elif isinstance(arg, bytes): + keys = arg.decode() + elif isinstance(arg, list) and len(arg) >= 1: + if isinstance(arg[0], str): + keys = " ".join(arg) + elif isinstance(arg[0], bytes): + keys = b" ".join(arg).decode() + + return keys + + +def _get_address_attributes(instance): + """Attempt to get host and port from Client instance.""" + address_attributes = {} + address_attributes[_DB] = "memcached" + + # client.base.Client contains server attribute which is either a host/port tuple, or unix socket path string + # https://github.com/pinterest/pymemcache/blob/f02ddf73a28c09256589b8afbb3ee50f1171cac7/pymemcache/client/base.py#L228 + if hasattr(instance, "server"): + if isinstance(instance.server, tuple): + host, port = instance.server + address_attributes[_HOST] = host + address_attributes[_PORT] = port + address_attributes[_URL] = "memcached://{}:{}".format(host, port) + elif isinstance(instance.server, str): + address_attributes[_URL] = "memcached://{}".format(instance.server) + + return address_attributes + + +class PymemcacheInstrumentor(BaseInstrumentor): + """An instrumentor for pymemcache See `BaseInstrumentor`""" + + def _instrument(self, **kwargs): + tracer_provider = kwargs.get("tracer_provider") + tracer = get_tracer(__name__, __version__, tracer_provider) + + for cmd in COMMANDS: + _wrap( + "pymemcache.client.base", + "Client.{}".format(cmd), + _wrap_cmd(tracer, cmd), + ) + + def _uninstrument(self, **kwargs): + for command in COMMANDS: + unwrap(pymemcache.client.base.Client, "{}".format(command)) diff --git a/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py b/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py new file mode 100644 index 0000000000..6d4fefa599 --- /dev/null +++ b/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-pymemcache/tests/__init__.py b/ext/opentelemetry-ext-pymemcache/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-pymemcache/tests/test_pymemcache.py b/ext/opentelemetry-ext-pymemcache/tests/test_pymemcache.py new file mode 100644 index 0000000000..aea4ad82f1 --- /dev/null +++ b/ext/opentelemetry-ext-pymemcache/tests/test_pymemcache.py @@ -0,0 +1,524 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pymemcache +from pymemcache.exceptions import ( + MemcacheClientError, + MemcacheIllegalInputError, + MemcacheServerError, + MemcacheUnknownCommandError, + MemcacheUnknownError, +) + +from opentelemetry import trace as trace_api +from opentelemetry.ext.pymemcache import PymemcacheInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import get_tracer +from opentelemetry.trace.status import StatusCanonicalCode + +from .utils import MockSocket, _str + +TEST_HOST = "localhost" +TEST_PORT = 117711 + + +class PymemcacheClientTestCase( + TestBase +): # pylint: disable=too-many-public-methods + """ Tests for a patched pymemcache.client.base.Client. """ + + def setUp(self): + super().setUp() + PymemcacheInstrumentor().instrument() + + # pylint: disable=protected-access + self.tracer = get_tracer(__name__) + + def tearDown(self): + super().tearDown() + PymemcacheInstrumentor().uninstrument() + + def make_client(self, mock_socket_values, **kwargs): + # pylint: disable=attribute-defined-outside-init + self.client = pymemcache.client.base.Client( + (TEST_HOST, TEST_PORT), **kwargs + ) + self.client.sock = MockSocket(list(mock_socket_values)) + return self.client + + def check_spans(self, spans, num_expected, queries_expected): + """A helper for validating basic span information.""" + self.assertEqual(num_expected, len(spans)) + + for span, query in zip(spans, queries_expected): + self.assertEqual(span.name, "memcached.command") + self.assertIs(span.kind, trace_api.SpanKind.INTERNAL) + self.assertEqual( + span.attributes["net.peer.name"], "{}".format(TEST_HOST) + ) + self.assertEqual(span.attributes["net.peer.port"], TEST_PORT) + self.assertEqual(span.attributes["db.type"], "memcached") + self.assertEqual( + span.attributes["db.url"], + "memcached://{}:{}".format(TEST_HOST, TEST_PORT), + ) + self.assertEqual(span.attributes["db.statement"], query) + + def test_set_success(self): + client = self.make_client([b"STORED\r\n"]) + result = client.set(b"key", b"value", noreply=False) + self.assertTrue(result) + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["set key"]) + + def test_get_many_none_found(self): + client = self.make_client([b"END\r\n"]) + result = client.get_many([b"key1", b"key2"]) + assert result == {} + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["get_many key1 key2"]) + + def test_get_multi_none_found(self): + client = self.make_client([b"END\r\n"]) + # alias for get_many + result = client.get_multi([b"key1", b"key2"]) + assert result == {} + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["get_multi key1 key2"]) + + def test_set_multi_success(self): + client = self.make_client([b"STORED\r\n"]) + # Alias for set_many, a convienance function that calls set for every key + result = client.set_multi({b"key": b"value"}, noreply=False) + self.assertTrue(result) + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 2, ["set key", "set_multi key"]) + + def test_delete_not_found(self): + client = self.make_client([b"NOT_FOUND\r\n"]) + result = client.delete(b"key", noreply=False) + assert result is False + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["delete key"]) + + def test_incr_found(self): + client = self.make_client([b"STORED\r\n", b"1\r\n"]) + client.set(b"key", 0, noreply=False) + result = client.incr(b"key", 1, noreply=False) + assert result == 1 + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 2, ["set key", "incr key"]) + + def test_get_found(self): + client = self.make_client( + [b"STORED\r\n", b"VALUE key 0 5\r\nvalue\r\nEND\r\n"] + ) + result = client.set(b"key", b"value", noreply=False) + result = client.get(b"key") + assert result == b"value" + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 2, ["set key", "get key"]) + + def test_decr_found(self): + client = self.make_client([b"STORED\r\n", b"1\r\n"]) + client.set(b"key", 2, noreply=False) + result = client.decr(b"key", 1, noreply=False) + assert result == 1 + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 2, ["set key", "decr key"]) + + def test_add_stored(self): + client = self.make_client([b"STORED\r", b"\n"]) + result = client.add(b"key", b"value", noreply=False) + self.assertTrue(result) + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["add key"]) + + def test_delete_many_found(self): + client = self.make_client([b"STORED\r", b"\n", b"DELETED\r\n"]) + result = client.add(b"key", b"value", noreply=False) + # a convienance function that calls delete for every key + result = client.delete_many([b"key"], noreply=False) + self.assertTrue(result) + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans( + spans, 3, ["add key", "delete key", "delete_many key"] + ) + + def test_set_many_success(self): + client = self.make_client([b"STORED\r\n"]) + # a convienance function that calls set for every key + result = client.set_many({b"key": b"value"}, noreply=False) + self.assertTrue(result) + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 2, ["set key", "set_many key"]) + + def test_set_get(self): + client = self.make_client( + [b"STORED\r\n", b"VALUE key 0 5\r\nvalue\r\nEND\r\n"] + ) + client.set(b"key", b"value", noreply=False) + result = client.get(b"key") + assert _str(result) == "value" + + spans = self.memory_exporter.get_finished_spans() + + self.assertEqual(len(spans), 2) + self.assertEqual( + spans[0].attributes["db.url"], + "memcached://{}:{}".format(TEST_HOST, TEST_PORT), + ) + + def test_append_stored(self): + client = self.make_client([b"STORED\r\n"]) + result = client.append(b"key", b"value", noreply=False) + self.assertTrue(result) + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["append key"]) + + def test_prepend_stored(self): + client = self.make_client([b"STORED\r\n"]) + result = client.prepend(b"key", b"value", noreply=False) + self.assertTrue(result) + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["prepend key"]) + + def test_cas_stored(self): + client = self.make_client([b"STORED\r\n"]) + result = client.cas(b"key", b"value", b"cas", noreply=False) + self.assertTrue(result) + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["cas key"]) + + def test_cas_exists(self): + client = self.make_client([b"EXISTS\r\n"]) + result = client.cas(b"key", b"value", b"cas", noreply=False) + assert result is False + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["cas key"]) + + def test_cas_not_found(self): + client = self.make_client([b"NOT_FOUND\r\n"]) + result = client.cas(b"key", b"value", b"cas", noreply=False) + assert result is None + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["cas key"]) + + def test_delete_exception(self): + client = self.make_client([Exception("fail")]) + + def _delete(): + client.delete(b"key", noreply=False) + + with self.assertRaises(Exception): + _delete() + + spans = self.memory_exporter.get_finished_spans() + + span = spans[0] + + self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + + self.check_spans(spans, 1, ["delete key"]) + + def test_flush_all(self): + client = self.make_client([b"OK\r\n"]) + result = client.flush_all(noreply=False) + self.assertTrue(result) + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["flush_all"]) + + def test_incr_exception(self): + client = self.make_client([Exception("fail")]) + + def _incr(): + client.incr(b"key", 1) + + with self.assertRaises(Exception): + _incr() + + spans = self.memory_exporter.get_finished_spans() + + span = spans[0] + + self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + + self.check_spans(spans, 1, ["incr key"]) + + def test_get_error(self): + client = self.make_client([b"ERROR\r\n"]) + + def _get(): + client.get(b"key") + + with self.assertRaises(MemcacheUnknownCommandError): + _get() + + spans = self.memory_exporter.get_finished_spans() + + span = spans[0] + + self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + + self.check_spans(spans, 1, ["get key"]) + + def test_get_unknown_error(self): + client = self.make_client([b"foobarbaz\r\n"]) + + def _get(): + client.get(b"key") + + with self.assertRaises(MemcacheUnknownError): + _get() + + spans = self.memory_exporter.get_finished_spans() + + span = spans[0] + + self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + + self.check_spans(spans, 1, ["get key"]) + + def test_gets_found(self): + client = self.make_client([b"VALUE key 0 5 10\r\nvalue\r\nEND\r\n"]) + result = client.gets(b"key") + assert result == (b"value", b"10") + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["gets key"]) + + def test_touch_not_found(self): + client = self.make_client([b"NOT_FOUND\r\n"]) + result = client.touch(b"key", noreply=False) + assert result is False + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["touch key"]) + + def test_set_client_error(self): + client = self.make_client([b"CLIENT_ERROR some message\r\n"]) + + def _set(): + client.set("key", "value", noreply=False) + + with self.assertRaises(MemcacheClientError): + _set() + + spans = self.memory_exporter.get_finished_spans() + + span = spans[0] + + self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + + self.check_spans(spans, 1, ["set key"]) + + def test_set_server_error(self): + client = self.make_client([b"SERVER_ERROR some message\r\n"]) + + def _set(): + client.set(b"key", b"value", noreply=False) + + with self.assertRaises(MemcacheServerError): + _set() + + spans = self.memory_exporter.get_finished_spans() + + span = spans[0] + + self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + + self.check_spans(spans, 1, ["set key"]) + + def test_set_key_with_space(self): + client = self.make_client([b""]) + + def _set(): + client.set(b"key has space", b"value", noreply=False) + + with self.assertRaises(MemcacheIllegalInputError): + _set() + + spans = self.memory_exporter.get_finished_spans() + + span = spans[0] + + self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + + self.check_spans(spans, 1, ["set key has space"]) + + def test_quit(self): + client = self.make_client([]) + assert client.quit() is None + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["quit"]) + + def test_replace_not_stored(self): + client = self.make_client([b"NOT_STORED\r\n"]) + result = client.replace(b"key", b"value", noreply=False) + assert result is False + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["replace key"]) + + def test_version_success(self): + client = self.make_client( + [b"VERSION 1.2.3\r\n"], default_noreply=False + ) + result = client.version() + assert result == b"1.2.3" + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["version"]) + + def test_stats(self): + client = self.make_client([b"STAT fake_stats 1\r\n", b"END\r\n"]) + result = client.stats() + assert client.sock.send_bufs == [b"stats \r\n"] + assert result == {b"fake_stats": 1} + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 1, ["stats"]) + + def test_uninstrumented(self): + PymemcacheInstrumentor().uninstrument() + + client = self.make_client( + [b"STORED\r\n", b"VALUE key 0 5\r\nvalue\r\nEND\r\n"] + ) + client.set(b"key", b"value", noreply=False) + result = client.get(b"key") + assert _str(result) == "value" + + spans = self.memory_exporter.get_finished_spans() + + self.assertEqual(len(spans), 0) + + PymemcacheInstrumentor().instrument() + + +class PymemcacheHashClientTestCase(TestBase): + """ Tests for a patched pymemcache.client.hash.HashClient. """ + + def setUp(self): + super().setUp() + PymemcacheInstrumentor().instrument() + + # pylint: disable=protected-access + self.tracer = get_tracer(__name__) + + def tearDown(self): + super().tearDown() + PymemcacheInstrumentor().uninstrument() + + def make_client_pool( + self, hostname, mock_socket_values, serializer=None, **kwargs + ): # pylint: disable=no-self-use + mock_client = pymemcache.client.base.Client( + hostname, serializer=serializer, **kwargs + ) + mock_client.sock = MockSocket(mock_socket_values) + client = pymemcache.client.base.PooledClient( + hostname, serializer=serializer + ) + client.client_pool = pymemcache.pool.ObjectPool(lambda: mock_client) + return mock_client + + def make_client(self, *mock_socket_values, **kwargs): + current_port = TEST_PORT + + # pylint: disable=import-outside-toplevel + from pymemcache.client.hash import HashClient + + # pylint: disable=attribute-defined-outside-init + self.client = HashClient([], **kwargs) + ip = TEST_HOST + + for vals in mock_socket_values: + url_string = "{}:{}".format(ip, current_port) + clnt_pool = self.make_client_pool( + (ip, current_port), vals, **kwargs + ) + self.client.clients[url_string] = clnt_pool + self.client.hasher.add_node(url_string) + current_port += 1 + return self.client + + def check_spans(self, spans, num_expected, queries_expected): + """A helper for validating basic span information.""" + self.assertEqual(num_expected, len(spans)) + + for span, query in zip(spans, queries_expected): + self.assertEqual(span.name, "memcached.command") + self.assertIs(span.kind, trace_api.SpanKind.INTERNAL) + self.assertEqual( + span.attributes["net.peer.name"], "{}".format(TEST_HOST) + ) + self.assertEqual(span.attributes["net.peer.port"], TEST_PORT) + self.assertEqual(span.attributes["db.type"], "memcached") + self.assertEqual( + span.attributes["db.url"], + "memcached://{}:{}".format(TEST_HOST, TEST_PORT), + ) + self.assertEqual(span.attributes["db.statement"], query) + + def test_delete_many_found(self): + client = self.make_client([b"STORED\r", b"\n", b"DELETED\r\n"]) + result = client.add(b"key", b"value", noreply=False) + result = client.delete_many([b"key"], noreply=False) + self.assertTrue(result) + + spans = self.memory_exporter.get_finished_spans() + + self.check_spans(spans, 2, ["add key", "delete key"]) diff --git a/ext/opentelemetry-ext-pymemcache/tests/utils.py b/ext/opentelemetry-ext-pymemcache/tests/utils.py new file mode 100644 index 0000000000..361fb6e68c --- /dev/null +++ b/ext/opentelemetry-ext-pymemcache/tests/utils.py @@ -0,0 +1,74 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import socket + + +class MockSocket: + def __init__(self, recv_bufs, connect_failure=None): + self.recv_bufs = collections.deque(recv_bufs) + self.send_bufs = [] + self.closed = False + self.timeouts = [] + self.connect_failure = connect_failure + self.connections = [] + self.socket_options = [] + + def sendall(self, value): + self.send_bufs.append(value) + + def close(self): + self.closed = True + + def recv(self, size): # pylint: disable=unused-argument + value = self.recv_bufs.popleft() + if isinstance(value, Exception): + raise value + return value + + def settimeout(self, timeout): + self.timeouts.append(timeout) + + def connect(self, server): + if isinstance(self.connect_failure, Exception): + raise self.connect_failure + self.connections.append(server) + + def setsockopt(self, level, option, value): + self.socket_options.append((level, option, value)) + + +class MockSocketModule: + def __init__(self, connect_failure=None): + self.connect_failure = connect_failure + self.sockets = [] + + def socket(self): # noqa: A002 + soket = MockSocket([], connect_failure=self.connect_failure) + self.sockets.append(soket) + return soket + + def __getattr__(self, name): + return getattr(socket, name) + + +# Compatibility to get a string back from a request +def _str(string_input): + if isinstance(string_input, str): + return string_input + if isinstance(string_input, bytes): + return string_input.decode() + + return str(string_input) diff --git a/tox.ini b/tox.ini index a46026a244..cab2031aa7 100644 --- a/tox.ini +++ b/tox.ini @@ -87,6 +87,10 @@ envlist = py3{4,5,6,7,8}-test-ext-psycopg2 ; ext-psycopg2 intentionally excluded from pypy3 + ; opentelemetry-ext-pymemcache + py3{4,5,6,7,8}-test-ext-pymemcache + pypy3-test-ext-pymemcache + ; opentelemetry-ext-pymongo py3{4,5,6,7,8}-test-ext-pymongo pypy3-test-ext-pymongo @@ -182,6 +186,7 @@ changedir = test-ext-opencensusexporter: ext/opentelemetry-ext-opencensusexporter/tests test-ext-otlp: ext/opentelemetry-ext-otlp/tests test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests + test-ext-pymemcache: ext/opentelemetry-ext-pymemcache/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-exporter-cloud-trace: ext/opentelemetry-exporter-cloud-trace/tests test-exporter-cloud-monitoring: ext/opentelemetry-exporter-cloud-monitoring/tests @@ -243,6 +248,8 @@ commands_pre = prometheus: pip install {toxinidir}/ext/opentelemetry-ext-prometheus + pymemcache: pip install {toxinidir}/ext/opentelemetry-ext-pymemcache[test] + pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo[test] psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-psycopg2 {toxinidir}/ext/opentelemetry-ext-psycopg2[test] From 74f2159e103140c25e226a7f3bdc52f04c105071 Mon Sep 17 00:00:00 2001 From: Connor Adams Date: Sat, 13 Jun 2020 00:08:46 -0400 Subject: [PATCH 0412/1517] docs: Docs for Pyramid and SQLite3 (#806) Adding missing documentation for Pyramid and SQLite3 --- docs-requirements.txt | 1 + docs/ext/pyramid/pyramid.rst | 7 ++++ docs/ext/sqlite3/sqlite3.rst | 7 ++++ .../src/opentelemetry/ext/pyramid/__init__.py | 35 +++++++++++-------- 4 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 docs/ext/pyramid/pyramid.rst create mode 100644 docs/ext/sqlite3/sqlite3.rst diff --git a/docs-requirements.txt b/docs-requirements.txt index b045ed589b..17d71b91af 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -16,6 +16,7 @@ prometheus_client>=0.5.0,<1.0.0 psycopg2-binary>=2.7.3.1 pymemcache~=1.3 pymongo~=3.1 +pyramid>=1.7 redis>=2.6 sqlalchemy>=1.0 thrift>=0.10.0 diff --git a/docs/ext/pyramid/pyramid.rst b/docs/ext/pyramid/pyramid.rst new file mode 100644 index 0000000000..b46718c387 --- /dev/null +++ b/docs/ext/pyramid/pyramid.rst @@ -0,0 +1,7 @@ +OpenTelemetry Pyramid Integration +================================= + +.. automodule:: opentelemetry.ext.pyramid + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ext/sqlite3/sqlite3.rst b/docs/ext/sqlite3/sqlite3.rst new file mode 100644 index 0000000000..9537ff58bf --- /dev/null +++ b/docs/ext/sqlite3/sqlite3.rst @@ -0,0 +1,7 @@ +OpenTelemetry SQLite3 Integration +================================= + +.. automodule:: opentelemetry.ext.sqlite3 + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/__init__.py b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/__init__.py index 6f63634bc7..c3db25ee7c 100644 --- a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/__init__.py +++ b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/__init__.py @@ -20,16 +20,17 @@ Usage ----- - There are two methods to instrument Pyramid: +There are two methods to instrument Pyramid: Method 1 (Instrument all Configurators): ---------------------------------------- + .. code:: python from pyramid.config import Configurator from opentelemetry.ext.pyramid import PyramidInstrumentor - PyramidInstrumentor.instrument() + PyramidInstrumentor().instrument() config = Configurator() @@ -38,6 +39,7 @@ Method 2 (Instrument one Configurator): --------------------------------------- + .. code:: python from pyramid.config import Configurator @@ -49,22 +51,30 @@ # use your config as normal config.add_route('index', '/') -Using ``pyramid.tweens`` settings: ----------------------------------- - If you use Method 2 and then set tweens for your application with the ``pyramid.tweens`` setting, - you need to add ``opentelemetry.ext.pyramid.trace_tween_factory`` explicity to the list, - *as well as* instrumenting the config with `PyramidInstrumentor().instrument_config(config)`. +Using ``pyramid.tweens`` setting: +--------------------------------- + +If you use Method 2 and then set tweens for your application with the ``pyramid.tweens`` setting, +you need to add ``opentelemetry.ext.pyramid.trace_tween_factory`` explicity to the list, +*as well as* instrumenting the config as shown above. + +For example: - For example: .. code:: python + + from pyramid.config import Configurator + from opentelemetry.ext.pyramid import PyramidInstrumentor + settings = { 'pyramid.tweens', 'opentelemetry.ext.pyramid.trace_tween_factory\\nyour_tween_no_1\\nyour_tween_no_2', } config = Configurator(settings=settings) - PyramidInstrumentor.instrument_config(config) + PyramidInstrumentor().instrument_config(config) # use your config as normal. config.add_route('index', '/') + +API --- """ @@ -87,7 +97,7 @@ from opentelemetry.trace import TracerProvider, get_tracer -def traced_init(wrapped, instance, args, kwargs): +def _traced_init(wrapped, instance, args, kwargs): settings = kwargs.get("settings", {}) tweens = aslist(settings.get("pyramid.tweens", [])) @@ -119,7 +129,7 @@ def _instrument(self, **kwargs): """Integrate with Pyramid Python library. https://docs.pylonsproject.org/projects/pyramid/en/latest/ """ - _wrap("pyramid.config", "Configurator.__init__", traced_init) + _wrap("pyramid.config", "Configurator.__init__", _traced_init) def _uninstrument(self, **kwargs): """"Disable Pyramid instrumentation""" @@ -131,9 +141,6 @@ def instrument_config(self, config): Args: config: The Configurator to instrument. - - Returns: - An instrumented Configurator. """ config.include("opentelemetry.ext.pyramid.callbacks") From 0b823a19d2e241f7020f33993080ab5cf4d6a28e Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Mon, 15 Jun 2020 02:03:30 -0400 Subject: [PATCH 0413/1517] docs: update datadog docs (#803) Update the Datadog docs for installation and using propagation. --- docs/examples/datadog_exporter/README.rst | 20 +++++++++++--- .../{datadog_exporter.py => basic_example.py} | 1 + .../datadog_exporter/datadog_client.py | 23 ++++++++++++++++ docs/examples/datadog_exporter/server.py | 18 ++++++++++++- .../src/opentelemetry/ext/datadog/__init__.py | 27 ++++++++++++++++++- 5 files changed, 84 insertions(+), 5 deletions(-) rename docs/examples/datadog_exporter/{datadog_exporter.py => basic_example.py} (99%) create mode 100644 docs/examples/datadog_exporter/datadog_client.py diff --git a/docs/examples/datadog_exporter/README.rst b/docs/examples/datadog_exporter/README.rst index 40c12aeccf..d851550b27 100644 --- a/docs/examples/datadog_exporter/README.rst +++ b/docs/examples/datadog_exporter/README.rst @@ -32,10 +32,15 @@ Basic Example .. code-block:: sh - python datadog_exporter.py + python basic_example.py -Auto-Instrumention Example --------------------------- + +.. code-block:: sh + + python basic_example.py + +Distributed Example +------------------- * Installation @@ -79,3 +84,12 @@ Auto-Instrumention Example .. code-block:: sh opentelemetry-instrument python client.py error + +* Run Datadog instrumented client + +The OpenTelemetry instrumented server is set up with propagation of Datadog trace context. + +.. code-block:: sh + + pip install ddtrace + ddtrace-run python datadog_client.py testing diff --git a/docs/examples/datadog_exporter/datadog_exporter.py b/docs/examples/datadog_exporter/basic_example.py similarity index 99% rename from docs/examples/datadog_exporter/datadog_exporter.py rename to docs/examples/datadog_exporter/basic_example.py index 0b3af99223..a41f9e0462 100644 --- a/docs/examples/datadog_exporter/datadog_exporter.py +++ b/docs/examples/datadog_exporter/basic_example.py @@ -31,6 +31,7 @@ span_processor = DatadogExportSpanProcessor(exporter) trace.get_tracer_provider().add_span_processor(span_processor) + with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): diff --git a/docs/examples/datadog_exporter/datadog_client.py b/docs/examples/datadog_exporter/datadog_client.py new file mode 100644 index 0000000000..26c463c3f5 --- /dev/null +++ b/docs/examples/datadog_exporter/datadog_client.py @@ -0,0 +1,23 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sys import argv + +import requests + +requested = requests.get( + "http://localhost:8082/server_request", params={"param": argv[1]} +) +assert requested.status_code == 200 +print(requested.text) diff --git a/docs/examples/datadog_exporter/server.py b/docs/examples/datadog_exporter/server.py index 0d545e2b7b..e2099fdf25 100644 --- a/docs/examples/datadog_exporter/server.py +++ b/docs/examples/datadog_exporter/server.py @@ -14,11 +14,12 @@ from flask import Flask, request -from opentelemetry import trace +from opentelemetry import propagators, trace from opentelemetry.ext.datadog import ( DatadogExportSpanProcessor, DatadogSpanExporter, ) +from opentelemetry.ext.datadog.propagator import DatadogFormat from opentelemetry.sdk.trace import TracerProvider app = Flask(__name__) @@ -33,6 +34,21 @@ ) ) +# append Datadog format for propagation to and from Datadog instrumented services +global_httptextformat = propagators.get_global_httptextformat() +if isinstance( + global_httptextformat, propagators.composite.CompositeHTTPPropagator +) and not any( + isinstance(p, DatadogFormat) for p in global_httptextformat._propagators +): + propagators.set_global_httptextformat( + propagators.composite.CompositeHTTPPropagator( + global_httptextformat._propagators + [DatadogFormat()] + ) + ) +else: + propagators.set_global_httptextformat(DatadogFormat()) + tracer = trace.get_tracer(__name__) diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/__init__.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/__init__.py index 0c01cf7fba..85bdaea40a 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/__init__.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/__init__.py @@ -16,13 +16,27 @@ The **OpenTelemetry Datadog Exporter** provides a span exporter from `OpenTelemetry`_ traces to `Datadog`_ by using the Datadog Agent. +Installation +------------ + +:: + + pip install opentelemetry-ext-datadog + + Usage ----- +The Datadog exporter provides a span processor that must be added along with the +exporter. In addition, a formatter is provided to handle propagation of trace +context between OpenTelemetry-instrumented and Datadog-instrumented services in +a distributed trace. + .. code:: python - from opentelemetry import trace + from opentelemetry import propagators, trace from opentelemetry.ext.datadog import DatadogExportSpanProcessor, DatadogSpanExporter + from opentelemetry.ext.datadog.propagator import DatadogFormat from opentelemetry.sdk.trace import TracerProvider trace.set_tracer_provider(TracerProvider()) @@ -35,13 +49,24 @@ span_processor = DatadogExportSpanProcessor(exporter) trace.get_tracer_provider().add_span_processor(span_processor) + # Optional: use Datadog format for propagation in distributed traces + propagators.set_global_httptextformat(DatadogFormat()) + with tracer.start_as_current_span("foo"): print("Hello world!") + +Examples +-------- + +The `docs/examples/datadog_exporter`_ includes examples for using the Datadog +exporter with OpenTelemetry instrumented applications. + API --- .. _Datadog: https://www.datadoghq.com/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +.. _docs/examples/datadog_exporter: https://github.com/open-telemetry/opentelemetry-python/tree/master/docs/examples/datadog_exporter """ # pylint: disable=import-error From b70450e6a39d533d70c0636ba3b6cf4d3b7de3f2 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 15 Jun 2020 14:58:18 -0400 Subject: [PATCH 0414/1517] api/sdk: Rename CounterAggregator -> SumAggregator (#816) Enables broader usage of the aggregator, across multiple counter types. --- .../exporter/cloud_monitoring/__init__.py | 4 +- .../tests/test_cloud_monitoring.py | 30 +++---- .../test_otcollector_metrics_exporter.py | 6 +- .../tests/test_prometheus_exporter.py | 6 +- opentelemetry-sdk/CHANGELOG.md | 3 + .../sdk/metrics/export/aggregate.py | 2 +- .../sdk/metrics/export/batcher.py | 6 +- .../tests/metrics/export/test_export.py | 89 +++++++++---------- .../tests/metrics/test_metrics.py | 8 +- 9 files changed, 77 insertions(+), 77 deletions(-) diff --git a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py index e0d8ced757..072301a274 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py +++ b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py @@ -12,7 +12,7 @@ MetricsExporter, MetricsExportResult, ) -from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator +from opentelemetry.sdk.metrics.export.aggregate import SumAggregator logger = logging.getLogger(__name__) MAX_BATCH_WRITE = 200 @@ -97,7 +97,7 @@ def _get_metric_descriptor( logger.warning( "Label value %s is not a string, bool or integer", value ) - if isinstance(record.aggregator, CounterAggregator): + if isinstance(record.aggregator, SumAggregator): descriptor["metric_kind"] = MetricDescriptor.MetricKind.GAUGE else: logger.warning( diff --git a/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py b/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py index df435738ac..ccc90d2c2f 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py +++ b/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py @@ -25,7 +25,7 @@ CloudMonitoringMetricsExporter, ) from opentelemetry.sdk.metrics.export import MetricRecord -from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator +from opentelemetry.sdk.metrics.export.aggregate import SumAggregator class UnsupportedAggregator: @@ -114,7 +114,7 @@ def test_get_metric_descriptor(self): ) record = MetricRecord( - MockMetric(), (("label1", "value1"),), CounterAggregator(), + MockMetric(), (("label1", "value1"),), SumAggregator(), ) metric_descriptor = exporter._get_metric_descriptor(record) client.create_metric_descriptor.assert_called_with( @@ -149,7 +149,7 @@ def test_get_metric_descriptor(self): ("label3", 3), ("label4", False), ), - CounterAggregator(), + SumAggregator(), ) ) client.create_metric_descriptor.assert_called_with( @@ -204,20 +204,20 @@ def test_export(self): } ) - counter_one = CounterAggregator() - counter_one.checkpoint = 1 - counter_one.last_update_timestamp = (WRITE_INTERVAL + 1) * 1e9 + sum_agg_one = SumAggregator() + sum_agg_one.checkpoint = 1 + sum_agg_one.last_update_timestamp = (WRITE_INTERVAL + 1) * 1e9 exporter.export( [ MetricRecord( MockMetric(), (("label1", "value1"), ("label2", 1),), - counter_one, + sum_agg_one, ), MetricRecord( MockMetric(), (("label1", "value2"), ("label2", 2),), - counter_one, + sum_agg_one, ), ] ) @@ -245,33 +245,33 @@ def test_export(self): # Attempting to export too soon after another export with the exact # same labels leads to it being dropped - counter_two = CounterAggregator() - counter_two.checkpoint = 1 - counter_two.last_update_timestamp = (WRITE_INTERVAL + 2) * 1e9 + sum_agg_two = SumAggregator() + sum_agg_two.checkpoint = 1 + sum_agg_two.last_update_timestamp = (WRITE_INTERVAL + 2) * 1e9 exporter.export( [ MetricRecord( MockMetric(), (("label1", "value1"), ("label2", 1),), - counter_two, + sum_agg_two, ), MetricRecord( MockMetric(), (("label1", "value2"), ("label2", 2),), - counter_two, + sum_agg_two, ), ] ) self.assertEqual(client.create_time_series.call_count, 1) # But exporting with different labels is fine - counter_two.checkpoint = 2 + sum_agg_two.checkpoint = 2 exporter.export( [ MetricRecord( MockMetric(), (("label1", "changed_label"), ("label2", 2),), - counter_two, + sum_agg_two, ), ] ) diff --git a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py index f907012647..f538e5acec 100644 --- a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py +++ b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py @@ -81,7 +81,7 @@ def test_get_collector_metric_type(self): self.assertIs(result, metrics_pb2.MetricDescriptor.UNSPECIFIED) def test_get_collector_point(self): - aggregator = aggregate.CounterAggregator() + aggregator = aggregate.SumAggregator() int_counter = self._meter.create_metric( "testName", "testDescription", "unit", int, Counter ) @@ -122,7 +122,7 @@ def test_export(self): "testname", "testdesc", "unit", int, Counter, ["environment"] ) record = MetricRecord( - test_metric, self._key_labels, aggregate.CounterAggregator(), + test_metric, self._key_labels, aggregate.SumAggregator(), ) result = collector_exporter.export([record]) @@ -144,7 +144,7 @@ def test_translate_to_collector(self): test_metric = self._meter.create_metric( "testname", "testdesc", "unit", int, Counter, ["environment"] ) - aggregator = aggregate.CounterAggregator() + aggregator = aggregate.SumAggregator() aggregator.update(123) aggregator.take_checkpoint() record = MetricRecord(test_metric, self._key_labels, aggregator,) diff --git a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py index 1862f789c0..2ba6b70121 100644 --- a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py +++ b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py @@ -24,7 +24,7 @@ from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk import metrics from opentelemetry.sdk.metrics.export import MetricRecord, MetricsExportResult -from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator +from opentelemetry.sdk.metrics.export.aggregate import SumAggregator class TestPrometheusMetricExporter(unittest.TestCase): @@ -67,7 +67,7 @@ def test_shutdown(self): def test_export(self): with self._registry_register_patch: record = MetricRecord( - self._test_metric, self._labels_key, CounterAggregator(), + self._test_metric, self._labels_key, SumAggregator(), ) exporter = PrometheusMetricsExporter() result = exporter.export([record]) @@ -87,7 +87,7 @@ def test_counter_to_prometheus(self): ) labels = {"environment@": "staging", "os": "Windows"} key_labels = metrics.get_labels_as_key(labels) - aggregator = CounterAggregator() + aggregator = SumAggregator() aggregator.update(123) aggregator.take_checkpoint() record = MetricRecord(metric, key_labels, aggregator) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index b2ddc0d01f..7c077dbf5e 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Rename CounterAggregator -> SumAggregator + ([#816](https://github.com/open-telemetry/opentelemetry-python/pull/816)) + ## 0.9b0 Released 2020-06-10 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index ad728d8c50..cfea391019 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -43,7 +43,7 @@ def merge(self, other): """Combines two aggregator values.""" -class CounterAggregator(Aggregator): +class SumAggregator(Aggregator): """Aggregator for Counter metrics.""" def __init__(self): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 0741082d99..527760d51b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -27,9 +27,9 @@ from opentelemetry.sdk.metrics.export import MetricRecord from opentelemetry.sdk.metrics.export.aggregate import ( Aggregator, - CounterAggregator, LastValueAggregator, MinMaxSumCountAggregator, + SumAggregator, ValueObserverAggregator, ) @@ -57,7 +57,7 @@ def aggregator_for(self, instrument_type: Type[InstrumentT]) -> Aggregator: """ # pylint:disable=R0201 if issubclass(instrument_type, (Counter, UpDownCounter)): - return CounterAggregator() + return SumAggregator() if issubclass(instrument_type, (SumObserver, UpDownSumObserver)): return LastValueAggregator() if issubclass(instrument_type, ValueRecorder): @@ -65,7 +65,7 @@ def aggregator_for(self, instrument_type: Type[InstrumentT]) -> Aggregator: if issubclass(instrument_type, ValueObserver): return ValueObserverAggregator() # TODO: Add other aggregators - return CounterAggregator() + return SumAggregator() def checkpoint_set(self) -> Sequence[MetricRecord]: """Returns a list of MetricRecords used for exporting. diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index c9a8f64d64..2f251f4976 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -24,8 +24,8 @@ MetricRecord, ) from opentelemetry.sdk.metrics.export.aggregate import ( - CounterAggregator, MinMaxSumCountAggregator, + SumAggregator, ValueObserverAggregator, ) from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher @@ -47,7 +47,7 @@ def test_export(self): ("environment",), ) labels = {"environment": "staging"} - aggregator = CounterAggregator() + aggregator = SumAggregator() record = MetricRecord(metric, labels, aggregator) result = '{}(data="{}", labels="{}", value={})'.format( ConsoleMetricsExporter.__name__, @@ -64,17 +64,14 @@ class TestBatcher(unittest.TestCase): def test_aggregator_for_counter(self): batcher = UngroupedBatcher(True) self.assertTrue( - isinstance( - batcher.aggregator_for(metrics.Counter), CounterAggregator - ) + isinstance(batcher.aggregator_for(metrics.Counter), SumAggregator) ) def test_aggregator_for_updowncounter(self): batcher = UngroupedBatcher(True) self.assertTrue( isinstance( - batcher.aggregator_for(metrics.UpDownCounter), - CounterAggregator, + batcher.aggregator_for(metrics.UpDownCounter), SumAggregator, ) ) @@ -83,7 +80,7 @@ def test_aggregator_for_updowncounter(self): def test_checkpoint_set(self): meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) - aggregator = CounterAggregator() + aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", @@ -111,7 +108,7 @@ def test_checkpoint_set_empty(self): def test_finished_collection_stateless(self): meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(False) - aggregator = CounterAggregator() + aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", @@ -131,7 +128,7 @@ def test_finished_collection_stateless(self): def test_finished_collection_stateful(self): meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) - aggregator = CounterAggregator() + aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", @@ -152,8 +149,8 @@ def test_finished_collection_stateful(self): def test_ungrouped_batcher_process_exists(self): meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) - aggregator = CounterAggregator() - aggregator2 = CounterAggregator() + aggregator = SumAggregator() + aggregator2 = SumAggregator() metric = metrics.Counter( "available memory", "available memory", @@ -179,7 +176,7 @@ def test_ungrouped_batcher_process_exists(self): def test_ungrouped_batcher_process_not_exists(self): meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) - aggregator = CounterAggregator() + aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", @@ -204,7 +201,7 @@ def test_ungrouped_batcher_process_not_exists(self): def test_ungrouped_batcher_process_not_stateful(self): meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) - aggregator = CounterAggregator() + aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", @@ -227,67 +224,67 @@ def test_ungrouped_batcher_process_not_stateful(self): ) -class TestCounterAggregator(unittest.TestCase): +class TestSumAggregator(unittest.TestCase): @staticmethod - def call_update(counter): + def call_update(sum_agg): update_total = 0 for _ in range(0, 100000): val = random.getrandbits(32) - counter.update(val) + sum_agg.update(val) update_total += val return update_total @mock.patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") def test_update(self, time_mock): time_mock.return_value = 123 - counter = CounterAggregator() - counter.update(1.0) - counter.update(2.0) - self.assertEqual(counter.current, 3.0) - self.assertEqual(counter.last_update_timestamp, 123) + sum_agg = SumAggregator() + sum_agg.update(1.0) + sum_agg.update(2.0) + self.assertEqual(sum_agg.current, 3.0) + self.assertEqual(sum_agg.last_update_timestamp, 123) def test_checkpoint(self): - counter = CounterAggregator() - counter.update(2.0) - counter.take_checkpoint() - self.assertEqual(counter.current, 0) - self.assertEqual(counter.checkpoint, 2.0) + sum_agg = SumAggregator() + sum_agg.update(2.0) + sum_agg.take_checkpoint() + self.assertEqual(sum_agg.current, 0) + self.assertEqual(sum_agg.checkpoint, 2.0) def test_merge(self): - counter = CounterAggregator() - counter2 = CounterAggregator() - counter.checkpoint = 1.0 - counter2.checkpoint = 3.0 - counter2.last_update_timestamp = 123 - counter.merge(counter2) - self.assertEqual(counter.checkpoint, 4.0) - self.assertEqual(counter.last_update_timestamp, 123) + sum_agg = SumAggregator() + sum_agg2 = SumAggregator() + sum_agg.checkpoint = 1.0 + sum_agg2.checkpoint = 3.0 + sum_agg2.last_update_timestamp = 123 + sum_agg.merge(sum_agg2) + self.assertEqual(sum_agg.checkpoint, 4.0) + self.assertEqual(sum_agg.last_update_timestamp, 123) def test_concurrent_update(self): - counter = CounterAggregator() + sum_agg = SumAggregator() with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: - fut1 = executor.submit(self.call_update, counter) - fut2 = executor.submit(self.call_update, counter) + fut1 = executor.submit(self.call_update, sum_agg) + fut2 = executor.submit(self.call_update, sum_agg) updapte_total = fut1.result() + fut2.result() - counter.take_checkpoint() - self.assertEqual(updapte_total, counter.checkpoint) + sum_agg.take_checkpoint() + self.assertEqual(updapte_total, sum_agg.checkpoint) def test_concurrent_update_and_checkpoint(self): - counter = CounterAggregator() + sum_agg = SumAggregator() checkpoint_total = 0 with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: - fut = executor.submit(self.call_update, counter) + fut = executor.submit(self.call_update, sum_agg) while not fut.done(): - counter.take_checkpoint() - checkpoint_total += counter.checkpoint + sum_agg.take_checkpoint() + checkpoint_total += sum_agg.checkpoint - counter.take_checkpoint() - checkpoint_total += counter.checkpoint + sum_agg.take_checkpoint() + checkpoint_total += sum_agg.checkpoint self.assertEqual(fut.result(), checkpoint_total) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index cba10726b2..ae07c23341 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -568,27 +568,27 @@ def test_run_exception(self, logger_mock): class TestBoundCounter(unittest.TestCase): def test_add(self): - aggregator = export.aggregate.CounterAggregator() + aggregator = export.aggregate.SumAggregator() bound_metric = metrics.BoundCounter(int, True, aggregator) bound_metric.add(3) self.assertEqual(bound_metric.aggregator.current, 3) def test_add_disabled(self): - aggregator = export.aggregate.CounterAggregator() + aggregator = export.aggregate.SumAggregator() bound_counter = metrics.BoundCounter(int, False, aggregator) bound_counter.add(3) self.assertEqual(bound_counter.aggregator.current, 0) @mock.patch("opentelemetry.sdk.metrics.logger") def test_add_incorrect_type(self, logger_mock): - aggregator = export.aggregate.CounterAggregator() + aggregator = export.aggregate.SumAggregator() bound_counter = metrics.BoundCounter(int, True, aggregator) bound_counter.add(3.0) self.assertEqual(bound_counter.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) def test_update(self): - aggregator = export.aggregate.CounterAggregator() + aggregator = export.aggregate.SumAggregator() bound_counter = metrics.BoundCounter(int, True, aggregator) bound_counter.update(4.0) self.assertEqual(bound_counter.aggregator.current, 4.0) From 39fa078312e6f41c403aa8cad1868264011f7546 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 15 Jun 2020 13:59:57 -0700 Subject: [PATCH 0415/1517] starlette instrumentation (#777) adding an initial starlette instrumentation. tox does exact match on fields delimited by a dash. Thus, any instrumentation that includes "instrumentation" in the name would collide with testing of the "opentelemetry-instrumentation" package. Renaming opentelemetry-instrumentation to opentelemetry-instrumentation-base to fix that. Co-authored-by: Leighton Chen Co-authored-by: alrex --- docs-requirements.txt | 1 + .../src/opentelemetry_example_app/version.py | 2 +- docs/ext/starlette/starlette.rst | 9 ++ ext/opentelemetry-ext-asgi/setup.cfg | 1 + .../src/opentelemetry/ext/asgi/__init__.py | 66 +++++------- .../tests/test_asgi_middleware.py | 7 +- .../tests/test_boto_instrumentation.py | 2 +- .../tests/test_botocore_instrumentation.py | 2 +- .../src/opentelemetry/ext/datadog/exporter.py | 1 + .../CHANGELOG.md | 5 + .../README.rst | 45 ++++++++ .../setup.cfg | 55 ++++++++++ .../setup.py | 31 ++++++ .../instrumentation/starlette/__init__.py | 82 ++++++++++++++ .../instrumentation/starlette/version.py | 15 +++ .../tests/__init__.py | 0 .../tests/test_starlette_instrumentation.py | 102 ++++++++++++++++++ opentelemetry-instrumentation/README.rst | 28 +++++ .../opentelemetry/instrumentation/__init__.py | 43 -------- scripts/check_for_valid_readme.py | 2 +- tox.ini | 25 +++-- 21 files changed, 423 insertions(+), 101 deletions(-) create mode 100644 docs/ext/starlette/starlette.rst create mode 100644 ext/opentelemetry-instrumentation-starlette/CHANGELOG.md create mode 100644 ext/opentelemetry-instrumentation-starlette/README.rst create mode 100644 ext/opentelemetry-instrumentation-starlette/setup.cfg create mode 100644 ext/opentelemetry-instrumentation-starlette/setup.py create mode 100644 ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py create mode 100644 ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py create mode 100644 ext/opentelemetry-instrumentation-starlette/tests/__init__.py create mode 100644 ext/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/__init__.py diff --git a/docs-requirements.txt b/docs-requirements.txt index 17d71b91af..10ccf1b21c 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -26,3 +26,4 @@ boto~=2.0 google-cloud-trace >=0.23.0 google-cloud-monitoring>=0.36.0 botocore~=1.0 +starlette~=0.13 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py index 603bf0b7e5..6d4fefa599 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.dev0" +__version__ = "0.10.dev0" diff --git a/docs/ext/starlette/starlette.rst b/docs/ext/starlette/starlette.rst new file mode 100644 index 0000000000..8e2d1d7bc8 --- /dev/null +++ b/docs/ext/starlette/starlette.rst @@ -0,0 +1,9 @@ +.. include:: ../../../ext/opentelemetry-instrumentation-starlette/README.rst + +API +--- + +.. automodule:: opentelemetry.instrumentation.starlette + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/ext/opentelemetry-ext-asgi/setup.cfg b/ext/opentelemetry-ext-asgi/setup.cfg index 8ccd7bd5e4..ab0e3e7f47 100644 --- a/ext/opentelemetry-ext-asgi/setup.cfg +++ b/ext/opentelemetry-ext-asgi/setup.cfg @@ -40,6 +40,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 asgiref ~= 3.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/__init__.py b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/__init__.py index 69c30848da..43b8804c24 100644 --- a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/__init__.py +++ b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/__init__.py @@ -22,11 +22,13 @@ import typing import urllib from functools import wraps +from typing import Tuple from asgiref.compatibility import guarantee_single_callable from opentelemetry import context, propagators, trace from opentelemetry.ext.asgi.version import __version__ # noqa +from opentelemetry.instrumentation.utils import http_status_to_canonical_code from opentelemetry.trace.status import Status, StatusCanonicalCode @@ -44,37 +46,6 @@ def get_header_from_scope(scope: dict, header_name: str) -> typing.List[str]: ] -def http_status_to_canonical_code(code: int, allow_redirect: bool = True): - # pylint:disable=too-many-branches,too-many-return-statements - if code < 100: - return StatusCanonicalCode.UNKNOWN - if code <= 299: - return StatusCanonicalCode.OK - if code <= 399: - if allow_redirect: - return StatusCanonicalCode.OK - return StatusCanonicalCode.DEADLINE_EXCEEDED - if code <= 499: - if code == 401: # HTTPStatus.UNAUTHORIZED: - return StatusCanonicalCode.UNAUTHENTICATED - if code == 403: # HTTPStatus.FORBIDDEN: - return StatusCanonicalCode.PERMISSION_DENIED - if code == 404: # HTTPStatus.NOT_FOUND: - return StatusCanonicalCode.NOT_FOUND - if code == 429: # HTTPStatus.TOO_MANY_REQUESTS: - return StatusCanonicalCode.RESOURCE_EXHAUSTED - return StatusCanonicalCode.INVALID_ARGUMENT - if code <= 599: - if code == 501: # HTTPStatus.NOT_IMPLEMENTED: - return StatusCanonicalCode.UNIMPLEMENTED - if code == 503: # HTTPStatus.SERVICE_UNAVAILABLE: - return StatusCanonicalCode.UNAVAILABLE - if code == 504: # HTTPStatus.GATEWAY_TIMEOUT: - return StatusCanonicalCode.DEADLINE_EXCEEDED - return StatusCanonicalCode.INTERNAL - return StatusCanonicalCode.UNKNOWN - - def collect_request_attributes(scope): """Collects HTTP request attributes from the ASGI scope and returns a dictionary to be used as span creation attributes.""" @@ -134,11 +105,19 @@ def set_status_code(span, status_code): span.set_status(Status(http_status_to_canonical_code(status_code))) -def get_default_span_name(scope): - """Default implementation for name_callback""" +def get_default_span_details(scope: dict) -> Tuple[str, dict]: + """Default implementation for span_details_callback + + Args: + scope: the asgi scope dictionary + + Returns: + a tuple of the span, and any attributes to attach to the + span. + """ method_or_path = scope.get("method") or scope.get("path") - return method_or_path + return method_or_path, {} class OpenTelemetryMiddleware: @@ -149,15 +128,18 @@ class OpenTelemetryMiddleware: Args: app: The ASGI application callable to forward requests to. - name_callback: Callback which calculates a generic span name for an - incoming HTTP request based on the ASGI scope. - Optional: Defaults to get_default_span_name. + span_details_callback: Callback which should return a string + and a tuple, representing the desired span name and a + dictionary with any additional span attributes to set. + Optional: Defaults to get_default_span_details. """ - def __init__(self, app, name_callback=None): + def __init__(self, app, span_details_callback=None): self.app = guarantee_single_callable(app) self.tracer = trace.get_tracer(__name__, __version__) - self.name_callback = name_callback or get_default_span_name + self.span_details_callback = ( + span_details_callback or get_default_span_details + ) async def __call__(self, scope, receive, send): """The ASGI application @@ -173,13 +155,15 @@ async def __call__(self, scope, receive, send): token = context.attach( propagators.extract(get_header_from_scope, scope) ) - span_name = self.name_callback(scope) + span_name, additional_attributes = self.span_details_callback(scope) + attributes = collect_request_attributes(scope) + attributes.update(additional_attributes) try: with self.tracer.start_as_current_span( span_name + " asgi", kind=trace.SpanKind.SERVER, - attributes=collect_request_attributes(scope), + attributes=attributes, ): @wraps(receive) diff --git a/ext/opentelemetry-ext-asgi/tests/test_asgi_middleware.py b/ext/opentelemetry-ext-asgi/tests/test_asgi_middleware.py index edc90444a7..05aa84b3c4 100644 --- a/ext/opentelemetry-ext-asgi/tests/test_asgi_middleware.py +++ b/ext/opentelemetry-ext-asgi/tests/test_asgi_middleware.py @@ -176,9 +176,8 @@ def test_override_span_name(self): """Test that span_names can be overwritten by our callback function.""" span_name = "Dymaxion" - # pylint:disable=unused-argument - def get_predefined_span_name(scope): - return span_name + def get_predefined_span_details(_): + return span_name, {} def update_expected_span_name(expected): for entry in expected: @@ -188,7 +187,7 @@ def update_expected_span_name(expected): return expected app = otel_asgi.OpenTelemetryMiddleware( - simple_asgi, name_callback=get_predefined_span_name + simple_asgi, span_details_callback=get_predefined_span_details ) self.seed_app(app) self.send_default_request() diff --git a/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py b/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py index 492fac5a88..a629b10870 100644 --- a/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py +++ b/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py @@ -19,13 +19,13 @@ import boto.elasticache import boto.s3 import boto.sts - from moto import ( # pylint: disable=import-error mock_ec2_deprecated, mock_lambda_deprecated, mock_s3_deprecated, mock_sts_deprecated, ) + from opentelemetry.ext.boto import BotoInstrumentor from opentelemetry.test.test_base import TestBase diff --git a/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py b/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py index 56b136ea29..64d0c3d7b7 100644 --- a/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py +++ b/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py @@ -1,6 +1,5 @@ import botocore.session from botocore.exceptions import ParamValidationError - from moto import ( # pylint: disable=import-error mock_ec2, mock_kinesis, @@ -9,6 +8,7 @@ mock_s3, mock_sqs, ) + from opentelemetry.ext.botocore import BotocoreInstrumentor from opentelemetry.test.test_base import TestBase diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py index 14e27dea2a..e267910a9d 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py @@ -33,6 +33,7 @@ DEFAULT_AGENT_URL = "http://localhost:8126" _INSTRUMENTATION_SPAN_TYPES = { "opentelemetry.ext.aiohttp-client": DatadogSpanTypes.HTTP, + "opentelemetry.ext.asgi": DatadogSpanTypes.WEB, "opentelemetry.ext.dbapi": DatadogSpanTypes.SQL, "opentelemetry.ext.django": DatadogSpanTypes.WEB, "opentelemetry.ext.flask": DatadogSpanTypes.WEB, diff --git a/ext/opentelemetry-instrumentation-starlette/CHANGELOG.md b/ext/opentelemetry-instrumentation-starlette/CHANGELOG.md new file mode 100644 index 0000000000..f7132ca830 --- /dev/null +++ b/ext/opentelemetry-instrumentation-starlette/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release ([#777](https://github.com/open-telemetry/opentelemetry-python/pull/777)) \ No newline at end of file diff --git a/ext/opentelemetry-instrumentation-starlette/README.rst b/ext/opentelemetry-instrumentation-starlette/README.rst new file mode 100644 index 0000000000..1d05c0b717 --- /dev/null +++ b/ext/opentelemetry-instrumentation-starlette/README.rst @@ -0,0 +1,45 @@ +OpenTelemetry Starlette Instrumentation +======================================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-starlette.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-starlette/ + + +This library provides automatic and manual instrumentation of Starlette web frameworks, +instrumenting http requests served by applications utilizing the framework. + +auto-instrumentation using the opentelemetry-instrumentation package is also supported. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-starlette + + +Usage +----- + +.. code-block:: python + + from opentelemetry.instrumentation.starlette import StarletteInstrumentor + from starlette import applications + from starlette.responses import PlainTextResponse + from starlette.routing import Route + + def home(request): + return PlainTextResponse("hi") + + app = applications.Starlette( + routes=[Route("/foobar", home)] + ) + StarletteInstrumentor.instrument_app(app) + + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-instrumentation-starlette/setup.cfg b/ext/opentelemetry-instrumentation-starlette/setup.cfg new file mode 100644 index 0000000000..7659c9fadb --- /dev/null +++ b/ext/opentelemetry-instrumentation-starlette/setup.cfg @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-instrumentation-starlette +description = OpenTelemetry Starlette Instrumentation +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-instrumentation-starlette +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.6 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.10.dev0 + opentelemetry-ext-asgi == 0.10.dev0 + +[options.entry_points] +opentelemetry_instrumentor = + starlette = opentelemetry.instrumentation.starlette:StarletteInstrumentor + +[options.extras_require] +test = + opentelemetry-test == 0.10.dev0 + starlette ~= 0.13.0 + requests ~= 2.23.0 # needed for testclient + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-instrumentation-starlette/setup.py b/ext/opentelemetry-instrumentation-starlette/setup.py new file mode 100644 index 0000000000..0232a6f448 --- /dev/null +++ b/ext/opentelemetry-instrumentation-starlette/setup.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "starlette", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py b/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py new file mode 100644 index 0000000000..197a38d759 --- /dev/null +++ b/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py @@ -0,0 +1,82 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Optional + +from starlette import applications +from starlette.routing import Match + +from opentelemetry.ext.asgi import OpenTelemetryMiddleware +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.starlette.version import __version__ # noqa + + +class StarletteInstrumentor(BaseInstrumentor): + """An instrumentor for starlette + + See `BaseInstrumentor` + """ + + _original_starlette = None + + @staticmethod + def instrument_app(app: applications.Starlette): + """Instrument a previously instrumented Starlette application. + """ + if not getattr(app, "is_instrumented_by_opentelemetry", False): + app.add_middleware( + OpenTelemetryMiddleware, + span_details_callback=_get_route_details, + ) + app.is_instrumented_by_opentelemetry = True + + def _instrument(self, **kwargs): + self._original_starlette = applications.Starlette + applications.Starlette = _InstrumentedStarlette + + def _uninstrument(self, **kwargs): + applications.Starlette = self._original_starlette + + +class _InstrumentedStarlette(applications.Starlette): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.add_middleware( + OpenTelemetryMiddleware, span_details_callback=_get_route_details + ) + + +def _get_route_details(scope): + """Callback to retrieve the starlette route being served. + + TODO: there is currently no way to retrieve http.route from + a starlette application from scope. + + See: https://github.com/encode/starlette/pull/804 + """ + app = scope["app"] + route = None + for starlette_route in app.routes: + match, _ = starlette_route.matches(scope) + if match == Match.FULL: + route = starlette_route.path + break + if match == Match.PARTIAL: + route = starlette_route.path + # method only exists for http, if websocket + # leave it blank. + span_name = route or scope.get("method", "") + attributes = {} + if route: + attributes["http.route"] = route + return span_name, attributes diff --git a/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py new file mode 100644 index 0000000000..6d4fefa599 --- /dev/null +++ b/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-instrumentation-starlette/tests/__init__.py b/ext/opentelemetry-instrumentation-starlette/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py b/ext/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py new file mode 100644 index 0000000000..a49db07c9a --- /dev/null +++ b/ext/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py @@ -0,0 +1,102 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from starlette import applications +from starlette.responses import PlainTextResponse +from starlette.routing import Route +from starlette.testclient import TestClient + +import opentelemetry.instrumentation.starlette as otel_starlette +from opentelemetry.test.test_base import TestBase + + +class TestStarletteManualInstrumentation(TestBase): + def _create_app(self): + app = self._create_starlette_app() + self._instrumentor.instrument_app(app) + return app + + def setUp(self): + super().setUp() + self._instrumentor = otel_starlette.StarletteInstrumentor() + self._app = self._create_app() + self._client = TestClient(self._app) + + def test_basic_starlette_call(self): + self._client.get("/foobar") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 3) + for span in spans: + self.assertIn("/foobar", span.name) + + def test_starlette_route_attribute_added(self): + """Ensure that starlette routes are used as the span name.""" + self._client.get("/user/123") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 3) + for span in spans: + self.assertIn("/user/{username}", span.name) + self.assertEqual( + spans[-1].attributes["http.route"], "/user/{username}" + ) + # ensure that at least one attribute that is populated by + # the asgi instrumentation is successfully feeding though. + self.assertEqual(spans[-1].attributes["http.flavor"], "1.1") + + @staticmethod + def _create_starlette_app(): + def home(_): + return PlainTextResponse("hi") + + app = applications.Starlette( + routes=[Route("/foobar", home), Route("/user/{username}", home)] + ) + return app + + +class TestAutoInstrumentation(TestStarletteManualInstrumentation): + """Test the auto-instrumented variant + + Extending the manual instrumentation as most test cases apply + to both. + """ + + def _create_app(self): + # instrumentation is handled by the instrument call + self._instrumentor.instrument() + return self._create_starlette_app() + + def tearDown(self): + self._instrumentor.uninstrument() + super().tearDown() + + +class TestAutoInstrumentationLogic(unittest.TestCase): + def test_instrumentation(self): + """Verify that instrumentation methods are instrumenting and + removing as expected. + """ + instrumentor = otel_starlette.StarletteInstrumentor() + original = applications.Starlette + instrumentor.instrument() + try: + instrumented = applications.Starlette + self.assertIsNot(original, instrumented) + finally: + instrumentor.uninstrument() + + should_be_original = applications.Starlette + self.assertIs(original, should_be_original) diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index d52ed037b7..6be744251b 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -13,6 +13,34 @@ Installation pip install opentelemetry-instrumentation + +This package provides a couple of commands that help automatically instruments a program: + +opentelemetry-instrument +------------------------ + +:: + + opentelemetry-instrument python program.py + +The code in ``program.py`` needs to use one of the packages for which there is +an OpenTelemetry integration. For a list of the available integrations please +check `here `_ + + +opentelemetry-bootstrap +----------------------- + +:: + + opentelemetry-bootstrap --action=install|requirements + +This commands inspects the active Python site-packages and figures out which +instrumentation packages the user might want to install. By default it prints out +a list of the suggested instrumentation packages which can be added to a requirements.txt +file. It also supports installing the suggested packages when run with :code:`--action=install` +flag. + References ---------- diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/__init__.py deleted file mode 100644 index 149a9b46c9..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" - -This package provides a couple of commands that help automatically instruments a program: - -opentelemetry-instrument ------------------------------------ - -:: - - opentelemetry-instrument python program.py - -The code in ``program.py`` needs to use one of the packages for which there is -an OpenTelemetry integration. For a list of the available integrations please -check :doc:`here <../../index>`. - - -opentelemetry-bootstrap ------------------------- - -:: - - opentelemetry-bootstrap --action=install|requirements - -This commands inspects the active Python site-packages and figures out which -instrumentation packages the user might want to install. By default it prints out -a list of the suggested instrumentation packages which can be added to a requirements.txt -file. It also supports installing the suggested packages when run with :code:`--action=install` -flag. -""" diff --git a/scripts/check_for_valid_readme.py b/scripts/check_for_valid_readme.py index edf94d9c3e..d555b4fa2f 100644 --- a/scripts/check_for_valid_readme.py +++ b/scripts/check_for_valid_readme.py @@ -10,7 +10,7 @@ def is_valid_rst(path): """Checks if RST can be rendered on PyPI.""" with open(path) as readme_file: markup = readme_file.read() - return readme_renderer.rst.render(markup) is not None + return readme_renderer.rst.render(markup, stream=sys.stderr) is not None def parse_args(): diff --git a/tox.ini b/tox.ini index cab2031aa7..875f63fc3f 100644 --- a/tox.ini +++ b/tox.ini @@ -17,8 +17,8 @@ envlist = pypy3-test-sdk ; opentelemetry-instrumentation - py3{5,6,7,8}-test-instrumentation - pypy3-test-instrumentation + py3{5,6,7,8}-test-instrumentation-base + pypy3-test-instrumentation-base ; opentelemetry-example-app py3{4,5,6,7,8}-test-example-app @@ -56,6 +56,11 @@ envlist = py3{4,5,6,7,8}-test-ext-requests pypy3-test-ext-requests + ; opentelemetry-instrumentation-starlette. + ; starlette only supports 3.6 and above. + py3{6,7,8}-test-instrumentation-starlette + pypy3-test-instrumentation-starlette + ; opentelemetry-ext-jinja2 py3{4,5,6,7,8}-test-ext-jinja2 pypy3-test-ext-jinja2 @@ -102,7 +107,7 @@ envlist = ; opentelemetry-ext-pyramid py3{4,5,6,7,8}-test-ext-pyramid pypy3-test-ext-pyramid - + ; opentelemetry-ext-asgi py3{5,6,7,8}-test-ext-asgi pypy3-test-ext-asgi @@ -172,8 +177,9 @@ setenv = changedir = test-api: opentelemetry-api/tests test-sdk: opentelemetry-sdk/tests + instrumentation-base: opentelemetry-instrumentation/tests + test-instrumentation-starlette: ext/opentelemetry-instrumentation-starlette/tests test-proto: opentelemetry-proto/tests - test-instrumentation: opentelemetry-instrumentation/tests test-ext-grpc: ext/opentelemetry-ext-grpc/tests test-ext-aiohttp-client: ext/opentelemetry-ext-aiohttp-client/tests test-ext-requests: ext/opentelemetry-ext-requests/tests @@ -223,10 +229,9 @@ commands_pre = grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc[test] - wsgi,flask,django,asgi,pyramid: pip install {toxinidir}/tests/util + wsgi,flask,django,asgi,pyramid,starlette: pip install {toxinidir}/tests/util wsgi,flask,django,pyramid: pip install {toxinidir}/ext/opentelemetry-ext-wsgi - - asgi: pip install {toxinidir}/ext/opentelemetry-ext-asgi + asgi,starlette: pip install {toxinidir}/ext/opentelemetry-ext-asgi boto: pip install {toxinidir}/ext/opentelemetry-ext-boto[test] @@ -264,6 +269,8 @@ commands_pre = requests: pip install {toxinidir}/ext/opentelemetry-ext-requests[test] + starlette: pip install {toxinidir}/ext/opentelemetry-instrumentation-starlette[test] + jinja2: pip install {toxinidir}/ext/opentelemetry-ext-jinja2[test] aiohttp-client: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-aiohttp-client @@ -274,7 +281,7 @@ commands_pre = opentracing-shim: pip install {toxinidir}/ext/opentelemetry-ext-opentracing-shim datadog: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-datadog - + zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin sqlalchemy: pip install {toxinidir}/ext/opentelemetry-ext-sqlalchemy @@ -322,7 +329,7 @@ deps = httpretty commands_pre = - python scripts/eachdist.py install --editable + python scripts/eachdist.py install --editable --with-test-deps commands = python scripts/eachdist.py lint --check-only From 521115f835f4c7f95559402815ae07db79fba921 Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Tue, 16 Jun 2020 13:37:10 -0400 Subject: [PATCH 0416/1517] fix(tests): install asgi early (#831) The lint job raising an error at installation: ERROR: Could not find a version that satisfies the requirement opentelemetry-ext-asgi==0.10.dev0 (from opentelemetry-instrumentation-starlette==0.10.dev0) (from versions: 0.8b0, 0.9b0) ERROR: No matching distribution found for opentelemetry-ext-asgi==0.10.dev0 (from opentelemetry-instrumentation-starlette==0.10.dev0) The opentelemetry-ext-asgi should be installed before depending modules. --- eachdist.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/eachdist.ini b/eachdist.ini index a2822d97b4..c77d82f345 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -9,6 +9,7 @@ sortfirst= tests/util ext/opentelemetry-ext-wsgi ext/opentelemetry-ext-dbapi + ext/opentelemetry-ext-asgi ext/* [lintroots] From 02b196809e9de7dad34ade9bc52315cf55ae96cd Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 16 Jun 2020 12:49:49 -0700 Subject: [PATCH 0417/1517] chore: adding circleci config for docs/lint (#829) --- .circleci/config.yml | 57 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000000..64070fd605 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,57 @@ +version: 2.1 + +executors: + python38: + docker: + - image: circleci/python:3.8 + +commands: + setup_tox: + description: "Install tox" + steps: + - run: pip install -U tox-factor + + restore_tox_cache: + description: "Restore .tox directory from previous runs for faster installs" + steps: + - restore_cache: + # In the cache key: + # - .Environment.CIRCLE_JOB: We do separate tox environments by job name, so caching and restoring is + # much faster. + key: tox-cache-{{ .Environment.CIRCLE_JOB }}-{{ checksum "tox.ini" }}-{{ checksum "dev-requirements.txt" }} + + save_tox_cache: + description: "Save .tox directory into cache for faster installs next time" + steps: + - save_cache: + # In the cache key: + # - .Environment.CIRCLE_JOB: We do separate tox environments by job name, so caching and restoring is + # much faster. + key: tox-cache-{{ .Environment.CIRCLE_JOB }}-{{ checksum "tox.ini" }}-{{ checksum "dev-requirements.txt" }} + paths: + - ".tox" + +jobs: + docs: + executor: python38 + steps: + - checkout + - setup_tox + - restore_tox_cache + - run: tox -e docs + - save_tox_cache + + lint: + executor: python38 + steps: + - checkout + - setup_tox + - restore_tox_cache + - run: tox -e lint + - save_tox_cache + +workflows: + main: + jobs: + - docs + - lint From fe4f3418621e8a4d2482b94c6b4d77be506f6be4 Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Tue, 16 Jun 2020 16:39:35 -0400 Subject: [PATCH 0418/1517] ext/cloudtrace: added more Cloud Trace troubleshooting tips (#827) Co-authored-by: Shawn Brunsting Co-authored-by: Chris Kleinknecht --- docs/examples/cloud_monitoring/README.rst | 6 +-- docs/examples/cloud_trace_exporter/README.rst | 47 +++++++++++++++---- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/docs/examples/cloud_monitoring/README.rst b/docs/examples/cloud_monitoring/README.rst index 5ad5421794..8b2ccedcee 100644 --- a/docs/examples/cloud_monitoring/README.rst +++ b/docs/examples/cloud_monitoring/README.rst @@ -22,9 +22,9 @@ To use this exporter you first need to: * Run example -.. code-block:: sh - - python basic_metrics.py +.. literalinclude:: basic_metrics.py + :language: python + :lines: 1- Viewing Output -------------------------- diff --git a/docs/examples/cloud_trace_exporter/README.rst b/docs/examples/cloud_trace_exporter/README.rst index 7c6c2c4c40..65674759b6 100644 --- a/docs/examples/cloud_trace_exporter/README.rst +++ b/docs/examples/cloud_trace_exporter/README.rst @@ -8,9 +8,10 @@ Basic Example ------------- To use this exporter you first need to: - * A Google Cloud project. You can `create one here. `_ - * Enable Cloud Trace API (aka Stackdriver Trace API) in the project `here. `_ - * Enable `Default Application Credentials. `_ + * A Google Cloud project. You can `create one here `_. + * Enable Cloud Trace API (listed in the Cloud Console as Stackdriver Trace API) in the project `here `_. + * If the page says "API Enabled" then you're done! No need to do anything. + * Enable Default Application Credentials by creating setting `GOOGLE_APPLICATION_CREDENTIALS `_ or by `installing gcloud sdk `_ and calling ``gcloud auth application-default login``. * Installation @@ -20,12 +21,11 @@ To use this exporter you first need to: pip install opentelemetry-sdk pip install opentelemetry-exporter-cloud-trace -* Run example locally +* Run an example locally -.. code-block:: sh - - cd opentelemetry-python/docs/examples/cloud_trace_exporter - python basic_trace.py +.. literalinclude:: basic_trace.py + :language: python + :lines: 1- Checking Output -------------------------- @@ -47,4 +47,33 @@ Running basic_trace.py hangs: Getting error ``google.api_core.exceptions.ResourceExhausted: 429 Resource has been exhausted``: ################################################################################################ - * Check that you've enabled the `Cloud Trace (Stackdriver Trace) API `_ \ No newline at end of file + * Check that you've enabled the `Cloud Trace (Stackdriver Trace) API `_ + +bash: pip: command not found: +############################# + * `Install pip `_ + * If your machine uses python2 by default, pip will also be the python2 version. Try using ``pip3`` instead of ``pip``. + +pip install is hanging +###################### +Try upgrading pip + +.. code-block:: sh + + pip install --upgrade pip + +``pip install grcpio`` has been known to hang when you aren't using an upgraded version. + +ImportError: No module named opentelemetry +########################################## +Make sure you are using python3. If + +.. code-block:: sh + + python --version + +returns ``Python 2.X.X`` try calling + +.. code-block:: sh + + python3 basic_trace.py From 17558b802bcca0448d4b2d272724e6bde411ecfa Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 16 Jun 2020 15:51:56 -0700 Subject: [PATCH 0419/1517] chore: moving code to prepare the release to eachdist (#808) --- scripts/eachdist.py | 124 +++++++++++++++++++++++++++++++++++++ scripts/prepare_release.sh | 62 +++---------------- 2 files changed, 133 insertions(+), 53 deletions(-) diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 15b9b8edcb..c8b7f7c3fc 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -1,12 +1,15 @@ #!/usr/bin/env python3 import argparse +import os +import re import shlex import shutil import subprocess import sys from collections import namedtuple from configparser import ConfigParser +from datetime import datetime from inspect import cleandoc from itertools import chain from pathlib import Path, PurePath @@ -236,6 +239,15 @@ def setup_instparser(instparser): "pytestargs", nargs=argparse.REMAINDER, help=extraargs_help("pytest") ) + releaseparser = subparsers.add_parser( + "release", help="Prepares release, used by maintainers and CI", + ) + releaseparser.set_defaults(func=release_args) + releaseparser.add_argument("--version", required=True) + releaseparser.add_argument( + "releaseargs", nargs=argparse.REMAINDER, help=extraargs_help("pytest") + ) + return parser.parse_args(args) @@ -503,6 +515,118 @@ def lint_args(args): ) +def update_changelog(path, version, new_entry): + unreleased_changes = False + try: + with open(path) as changelog: + text = changelog.read() + if "## Version {}".format(version) in text: + raise AttributeError( + "{} already contans version {}".format(path, version) + ) + with open(path) as changelog: + for line in changelog: + if line.startswith("## Unreleased"): + unreleased_changes = False + elif line.startswith("## "): + break + elif len(line.strip()) > 0: + unreleased_changes = True + + except FileNotFoundError: + print("file missing: {}".format(path)) + return + + if unreleased_changes: + print("updating: {}".format(path)) + text = re.sub("## Unreleased", new_entry, text) + with open(path, "w") as changelog: + changelog.write(text) + + +def update_changelogs(targets, version): + print("updating CHANGELOG") + today = datetime.now().strftime("%Y-%m-%d") + new_entry = "## Unreleased\n\n## Version {}\n\nReleased {}".format( + version, today + ) + errors = False + for target in targets: + try: + update_changelog( + "{}/CHANGELOG.md".format(target), version, new_entry + ) + except Exception as err: # pylint: disable=broad-except + print(str(err)) + errors = True + + if errors: + sys.exit(1) + + +def find(name, path): + for root, _, files in os.walk(path): + if name in files: + return os.path.join(root, name) + return None + + +def update_version_files(targets, version): + print("updating version.py files") + update_files( + targets, + version, + "version.py", + "__version__ .*", + '__version__ = "{}"'.format(version), + ) + + +def update_dependencies(targets, version): + print("updating dependencies") + update_files( + targets, + version, + "setup.cfg", + r"(opentelemetry-.*)= (.*)", + r"\1= " + version, + ) + + +def update_files(targets, version, filename, search, replace): + errors = False + for target in targets: + curr_file = find(filename, target) + if curr_file is None: + print("file missing: {}/{}".format(target, filename)) + continue + + with open(curr_file) as _file: + text = _file.read() + + if version in text: + print("{} already contans version {}".format(curr_file, version)) + errors = True + continue + + with open(curr_file, "w") as _file: + _file.write(re.sub(search, replace, text)) + + if errors: + sys.exit(1) + + +def release_args(args): + print("preparing release") + + rootpath = find_projectroot() + targets = list(find_targets_unordered(rootpath)) + version = args.version + update_dependencies(targets, version) + update_version_files(targets, version) + update_changelogs(targets, version) + + def test_args(args): clean_remainder_args(args.pytestargs) execute_args( diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh index a74bd17a8a..e75278eb5e 100755 --- a/scripts/prepare_release.sh +++ b/scripts/prepare_release.sh @@ -21,57 +21,6 @@ if [[ ! "${VERSION}" =~ ^([0-9])(\.*[0-9]{1,5}[a-b]*){1,3}$ ]]; then exit 1 fi -function update_version_file() { - errors=0 - for f in `find . -name version.py`; do - # check if version is already in version.py - grep -q ${VERSION} $f; - rc=$? - if [ $rc == 0 ]; then - errors=1 - echo "${f} already contains ${VERSION}" - continue - fi - # update version.py - perl -i -pe "s/__version__.*/__version__ = \"${VERSION}\"/g" ${f}; - git add ${f}; - echo "Updating ${f}" - done - if [ ${errors} != 0 ]; then - echo "::set-output name=version_updated::0" - exit 0 - fi -} - -function update_changelog() { - errors=0 - RELEASE_DATE=`date +%F` - for f in `find . -name CHANGELOG.md`; do - # check if version is already in CHANGELOG - grep -q ${VERSION} $f; - rc=$? - if [ $rc == 0 ]; then - errors=1 - echo "${f} already contains ${VERSION}" - continue - fi - # check if changelog contains any new details - changes=`sed -n '/## Unreleased/,/^##/p' ${f} | grep -v '^##' | wc -w | awk '{$1=$1;print}'` - if [ ${changes} != "0" ]; then - # update CHANGELOG.md - perl -i -pe 's/## Unreleased.*/## Unreleased\n\n## '${VERSION}'\n\nReleased '${RELEASE_DATE}'/' ${f}; - git add ${f}; - echo "Updating ${f}" - else - echo "Skipping ${f}, no changes detected" - fi - done - if [ ${errors} != 0 ]; then - echo "::set-output name=version_updated::0" - exit 0 - fi -} - # create the release branch git fetch origin master git checkout master @@ -81,8 +30,15 @@ git push origin release/${VERSION} # create a temporary branch to create a PR for updated version and changelogs git checkout -b release/${VERSION}-auto -update_version_file -update_changelog +./scripts/eachdist.py release --version ${VERSION} +rc=$? +if [ $rc != 0 ]; then + echo "::set-output name=version_updated::0" + exit 0 +fi + +git add **/version.py **/setup.cfg **/CHANGELOG.md + git commit -m "updating changelogs and version to ${VERSION}" echo "Time to create a release, here's a sample title:" From f2bc613e2b8ce7f53c7e3b7d0485460ab542ec6b Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 16 Jun 2020 19:07:25 -0700 Subject: [PATCH 0420/1517] Fix last value aggregation bug + add tests (#834) --- .../sdk/metrics/export/aggregate.py | 4 +- .../tests/metrics/export/test_export.py | 90 +++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index cfea391019..3292529aed 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -144,12 +144,12 @@ def take_checkpoint(self): self.current = None def merge(self, other): - last = self.checkpoint.last + last = self.checkpoint self.last_update_timestamp = get_latest_timestamp( self.last_update_timestamp, other.last_update_timestamp ) if self.last_update_timestamp == other.last_update_timestamp: - last = other.checkpoint.last + last = other.checkpoint self.checkpoint = last diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 2f251f4976..4d790bb6c0 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -24,6 +24,7 @@ MetricRecord, ) from opentelemetry.sdk.metrics.export.aggregate import ( + LastValueAggregator, MinMaxSumCountAggregator, SumAggregator, ValueObserverAggregator, @@ -610,6 +611,95 @@ def test_merge_with_empty(self): self.assertEqual(observer1.checkpoint, checkpoint1) +class TestLastValueAggregator(unittest.TestCase): + @mock.patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") + def test_update(self, time_mock): + time_mock.return_value = 123 + observer = LastValueAggregator() + # test current values without any update + self.assertIsNone(observer.current) + + # call update with some values + values = (3, 50, 3, 97, 27) + for val in values: + observer.update(val) + + self.assertEqual(observer.last_update_timestamp, 123) + self.assertEqual(observer.current, values[-1]) + + def test_checkpoint(self): + observer = LastValueAggregator() + + # take checkpoint without any update + observer.take_checkpoint() + self.assertEqual(observer.checkpoint, None) + + # call update with some values + values = (3, 50, 3, 97) + for val in values: + observer.update(val) + + observer.take_checkpoint() + self.assertEqual(observer.checkpoint, 97) + + def test_merge(self): + observer1 = LastValueAggregator() + observer2 = LastValueAggregator() + + observer1.checkpoint = 23 + observer2.checkpoint = 47 + + observer1.last_update_timestamp = 100 + observer2.last_update_timestamp = 123 + + observer1.merge(observer2) + + self.assertEqual(observer1.checkpoint, 47) + self.assertEqual(observer1.last_update_timestamp, 123) + + def test_merge_last_updated(self): + observer1 = LastValueAggregator() + observer2 = LastValueAggregator() + + observer1.checkpoint = 23 + observer2.checkpoint = 47 + + observer1.last_update_timestamp = 123 + observer2.last_update_timestamp = 100 + + observer1.merge(observer2) + + self.assertEqual(observer1.checkpoint, 23) + self.assertEqual(observer1.last_update_timestamp, 123) + + def test_merge_last_updated_none(self): + observer1 = LastValueAggregator() + observer2 = LastValueAggregator() + + observer1.checkpoint = 23 + observer2.checkpoint = 47 + + observer1.last_update_timestamp = None + observer2.last_update_timestamp = 100 + + observer1.merge(observer2) + + self.assertEqual(observer1.checkpoint, 47) + self.assertEqual(observer1.last_update_timestamp, 100) + + def test_merge_with_empty(self): + observer1 = LastValueAggregator() + observer2 = LastValueAggregator() + + observer1.checkpoint = 23 + observer1.last_update_timestamp = 100 + + observer1.merge(observer2) + + self.assertEqual(observer1.checkpoint, 23) + self.assertEqual(observer1.last_update_timestamp, 100) + + class TestController(unittest.TestCase): def test_push_controller(self): meter = mock.Mock() From e35d84f9ae50c16add1fee06130d63bca91b4a21 Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Tue, 16 Jun 2020 23:55:23 -0400 Subject: [PATCH 0421/1517] fix(datadog): use http.target not http.path (#810) --- .../src/opentelemetry/ext/datadog/exporter.py | 4 +--- .../tests/test_datadog_exporter.py | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py index e267910a9d..a1788b74a8 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py @@ -191,9 +191,7 @@ def _get_span_name(span): def _get_resource(span): """Get resource name for span""" if "http.method" in span.attributes: - route = span.attributes.get( - "http.route", span.attributes.get("http.path") - ) + route = span.attributes.get("http.route") return ( span.attributes["http.method"] + " " + route if route diff --git a/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py b/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py index 378bef4147..d99f9db2c2 100644 --- a/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py +++ b/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py @@ -222,8 +222,8 @@ def test_export(self): def test_resources(self): test_attributes = [ {}, - {"http.method": "GET", "http.route": "/foo"}, - {"http.method": "GET", "http.path": "/foo"}, + {"http.method": "GET", "http.route": "/foo/"}, + {"http.method": "GET", "http.target": "/foo/200"}, ] for index, test in enumerate(test_attributes): @@ -235,7 +235,7 @@ def test_resources(self): self.assertEqual(len(datadog_spans), 3) actual = [span["resource"] for span in datadog_spans] - expected = ["0", "GET /foo", "GET /foo"] + expected = ["0", "GET /foo/", "GET"] self.assertEqual(actual, expected) From 9a2216a31c75b8fb911f9756a88950d280ff4e0e Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 17 Jun 2020 10:39:43 +0530 Subject: [PATCH 0422/1517] Initial elasticsearch instrumentation (#747) This commit adds auto-instrumentation for elasticsearch. The instrumentation has been mostly ported over from OpenTracing elasticsearch instrumentation. Co-authored-by: Yusuke Tsutsumi Co-authored-by: alrex --- .../CHANGELOG.md | 5 + ext/opentelemetry-ext-elasticsearch/LICENSE | 201 ++++++++++++ .../MANIFEST.in | 9 + .../README.rst | 23 ++ ext/opentelemetry-ext-elasticsearch/setup.cfg | 58 ++++ ext/opentelemetry-ext-elasticsearch/setup.py | 26 ++ .../ext/elasticsearch/__init__.py | 164 ++++++++++ .../ext/elasticsearch/version.py | 15 + .../tests/__init__.py | 0 .../tests/helpers_es2.py | 33 ++ .../tests/helpers_es5.py | 33 ++ .../tests/helpers_es6.py | 33 ++ .../tests/helpers_es7.py | 31 ++ .../tests/test_elasticsearch.py | 307 ++++++++++++++++++ .../instrumentation/instrumentor.py | 4 +- scripts/eachdist.py | 1 + .../util/src/opentelemetry/test/test_base.py | 5 + tox.ini | 14 + 18 files changed, 960 insertions(+), 2 deletions(-) create mode 100644 ext/opentelemetry-ext-elasticsearch/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-elasticsearch/LICENSE create mode 100644 ext/opentelemetry-ext-elasticsearch/MANIFEST.in create mode 100644 ext/opentelemetry-ext-elasticsearch/README.rst create mode 100644 ext/opentelemetry-ext-elasticsearch/setup.cfg create mode 100644 ext/opentelemetry-ext-elasticsearch/setup.py create mode 100644 ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/__init__.py create mode 100644 ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py create mode 100644 ext/opentelemetry-ext-elasticsearch/tests/__init__.py create mode 100644 ext/opentelemetry-ext-elasticsearch/tests/helpers_es2.py create mode 100644 ext/opentelemetry-ext-elasticsearch/tests/helpers_es5.py create mode 100644 ext/opentelemetry-ext-elasticsearch/tests/helpers_es6.py create mode 100644 ext/opentelemetry-ext-elasticsearch/tests/helpers_es7.py create mode 100644 ext/opentelemetry-ext-elasticsearch/tests/test_elasticsearch.py diff --git a/ext/opentelemetry-ext-elasticsearch/CHANGELOG.md b/ext/opentelemetry-ext-elasticsearch/CHANGELOG.md new file mode 100644 index 0000000000..33144da913 --- /dev/null +++ b/ext/opentelemetry-ext-elasticsearch/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-elasticsearch/LICENSE b/ext/opentelemetry-ext-elasticsearch/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-elasticsearch/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-elasticsearch/MANIFEST.in b/ext/opentelemetry-ext-elasticsearch/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-elasticsearch/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-elasticsearch/README.rst b/ext/opentelemetry-ext-elasticsearch/README.rst new file mode 100644 index 0000000000..88e9bb41af --- /dev/null +++ b/ext/opentelemetry-ext-elasticsearch/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry elasticsearch Integration +======================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-elasticsearch.svg + :target: https://pypi.org/project/opentelemetry-ext-elasticsearch/ + +This library allows tracing elasticsearch made by the +`elasticsearch `_ library. + +Installation +------------ + +:: + + pip install opentelemetry-ext-elasticsearch + +References +---------- + +* `OpenTelemetry elasticsearch Integration `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-elasticsearch/setup.cfg b/ext/opentelemetry-ext-elasticsearch/setup.cfg new file mode 100644 index 0000000000..07be1c622e --- /dev/null +++ b/ext/opentelemetry-ext-elasticsearch/setup.cfg @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-elasticsearch +description = OpenTelemetry elasticsearch integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-elasticsearch +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 + wrapt >= 1.0.0, < 2.0.0 + elasticsearch >= 2.0 + +[options.extras_require] +test = + opentelemetry-test == 0.10.dev0 + elasticsearch-dsl >= 2.0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + elasticsearch = opentelemetry.ext.elasticsearch:ElasticsearchInstrumentor diff --git a/ext/opentelemetry-ext-elasticsearch/setup.py b/ext/opentelemetry-ext-elasticsearch/setup.py new file mode 100644 index 0000000000..af750c941f --- /dev/null +++ b/ext/opentelemetry-ext-elasticsearch/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "elasticsearch", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/__init__.py b/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/__init__.py new file mode 100644 index 0000000000..7c41ba3f0d --- /dev/null +++ b/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/__init__.py @@ -0,0 +1,164 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This library allows tracing HTTP elasticsearch made by the +`elasticsearch `_ library. + +Usage +----- + +.. code-block:: python + + from opentelemetry import trace + from opentelemetry.ext.elasticsearch import ElasticSearchInstrumentor + from opentelemetry.sdk.trace import TracerProvider + import elasticsearch + + trace.set_tracer_provider(TracerProvider()) + + # instrument elasticsearch + ElasticSearchInstrumentor().instrument(tracer_provider=trace.get_tracer_provider()) + + # Using elasticsearch as normal now will automatically generate spans + es = elasticsearch.Elasticsearch() + es.index(index='my-index', doc_type='my-type', id=1, body={'my': 'data', 'timestamp': datetime.now()}) + es.get(index='my-index', doc_type='my-type', id=1) + +API +--- + +Elasticsearch instrumentation prefixes operation names with the string "Elasticsearch". This +can be changed to a different string by either setting the `OPENTELEMETRY_PYTHON_ELASTICSEARCH_NAME_PREFIX` +environment variable or by passing the prefix as an argument to the instrumentor. For example, + + +.. code-block:: python + + ElasticSearchInstrumentor("my-custom-prefix").instrument() +""" + +import functools +import types +from logging import getLogger +from os import environ + +import elasticsearch +import elasticsearch.exceptions +from wrapt import ObjectProxy +from wrapt import wrap_function_wrapper as _wrap + +from opentelemetry import context, propagators, trace +from opentelemetry.ext.elasticsearch.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.trace import SpanKind, get_tracer +from opentelemetry.trace.status import Status, StatusCanonicalCode + +logger = getLogger(__name__) + + +# Values to add as tags from the actual +# payload returned by Elasticsearch, if any. +_ATTRIBUTES_FROM_RESULT = [ + "found", + "timed_out", + "took", +] + +_DEFALT_OP_NAME = "request" + + +class ElasticsearchInstrumentor(BaseInstrumentor): + """An instrumentor for elasticsearch + See `BaseInstrumentor` + """ + + def __init__(self, span_name_prefix=None): + if not span_name_prefix: + span_name_prefix = environ.get( + "OPENTELEMETRY_PYTHON_ELASTICSEARCH_NAME_PREFIX", + "Elasticsearch", + ) + self._span_name_prefix = span_name_prefix.strip() + super().__init__() + + def _instrument(self, **kwargs): + """ + Instruments elasticsarch module + """ + tracer_provider = kwargs.get("tracer_provider") + tracer = get_tracer(__name__, __version__, tracer_provider) + _wrap( + elasticsearch, + "Transport.perform_request", + _wrap_perform_request(tracer, self._span_name_prefix), + ) + + def _uninstrument(self, **kwargs): + unwrap(elasticsearch.Transport, "perform_request") + + +def _wrap_perform_request(tracer, span_name_prefix): + def wrapper(wrapped, _, args, kwargs): + method = url = None + try: + method, url, *_ = args + except IndexError: + logger.warning( + "expected perform_request to receive two positional arguments. " + "Got %d", + len(args), + ) + + op_name = span_name_prefix + (url or method or _DEFALT_OP_NAME) + params = kwargs.get("params", {}) + body = kwargs.get("body", None) + + attributes = { + "component": "elasticsearch-py", + "db.type": "elasticsearch", + } + + if url: + attributes["elasticsearch.url"] = url + if method: + attributes["elasticsearch.method"] = method + if body: + attributes["db.statement"] = str(body) + if params: + attributes["elasticsearch.params"] = str(params) + + with tracer.start_as_current_span( + op_name, kind=SpanKind.CLIENT, attributes=attributes + ) as span: + try: + rv = wrapped(*args, **kwargs) + if isinstance(rv, dict): + for member in _ATTRIBUTES_FROM_RESULT: + if member in rv: + span.set_attribute( + "elasticsearch.{0}".format(member), + str(rv[member]), + ) + return rv + except Exception as ex: # pylint: disable=broad-except + if isinstance(ex, elasticsearch.exceptions.NotFoundError): + status = StatusCanonicalCode.NOT_FOUND + else: + status = StatusCanonicalCode.UNKNOWN + span.set_status(Status(status, str(ex))) + raise ex + + return wrapper diff --git a/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py b/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py new file mode 100644 index 0000000000..6d4fefa599 --- /dev/null +++ b/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-elasticsearch/tests/__init__.py b/ext/opentelemetry-ext-elasticsearch/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-elasticsearch/tests/helpers_es2.py b/ext/opentelemetry-ext-elasticsearch/tests/helpers_es2.py new file mode 100644 index 0000000000..008a95d671 --- /dev/null +++ b/ext/opentelemetry-ext-elasticsearch/tests/helpers_es2.py @@ -0,0 +1,33 @@ +from elasticsearch_dsl import ( # pylint: disable=no-name-in-module + DocType, + String, +) + + +class Article(DocType): + title = String(analyzer="snowball", fields={"raw": String()}) + body = String(analyzer="snowball") + + class Meta: + index = "test-index" + + +dsl_create_statement = { + "mappings": { + "article": { + "properties": { + "title": { + "analyzer": "snowball", + "fields": {"raw": {"type": "string"}}, + "type": "string", + }, + "body": {"analyzer": "snowball", "type": "string"}, + } + } + }, + "settings": {"analysis": {}}, +} +dsl_index_result = (1, {}, '{"created": true}') +dsl_index_span_name = "Elasticsearch/test-index/article/2" +dsl_index_url = "/test-index/article/2" +dsl_search_method = "GET" diff --git a/ext/opentelemetry-ext-elasticsearch/tests/helpers_es5.py b/ext/opentelemetry-ext-elasticsearch/tests/helpers_es5.py new file mode 100644 index 0000000000..cf32d98863 --- /dev/null +++ b/ext/opentelemetry-ext-elasticsearch/tests/helpers_es5.py @@ -0,0 +1,33 @@ +from elasticsearch_dsl import ( # pylint: disable=no-name-in-module + DocType, + Keyword, + Text, +) + + +class Article(DocType): + title = Text(analyzer="snowball", fields={"raw": Keyword()}) + body = Text(analyzer="snowball") + + class Meta: + index = "test-index" + + +dsl_create_statement = { + "mappings": { + "article": { + "properties": { + "title": { + "analyzer": "snowball", + "fields": {"raw": {"type": "keyword"}}, + "type": "text", + }, + "body": {"analyzer": "snowball", "type": "text"}, + } + } + }, +} +dsl_index_result = (1, {}, '{"created": true}') +dsl_index_span_name = "Elasticsearch/test-index/article/2" +dsl_index_url = "/test-index/article/2" +dsl_search_method = "GET" diff --git a/ext/opentelemetry-ext-elasticsearch/tests/helpers_es6.py b/ext/opentelemetry-ext-elasticsearch/tests/helpers_es6.py new file mode 100644 index 0000000000..b27d291ba3 --- /dev/null +++ b/ext/opentelemetry-ext-elasticsearch/tests/helpers_es6.py @@ -0,0 +1,33 @@ +from elasticsearch_dsl import ( # pylint: disable=unused-import + Document, + Keyword, + Text, +) + + +class Article(Document): + title = Text(analyzer="snowball", fields={"raw": Keyword()}) + body = Text(analyzer="snowball") + + class Index: + name = "test-index" + + +dsl_create_statement = { + "mappings": { + "doc": { + "properties": { + "title": { + "analyzer": "snowball", + "fields": {"raw": {"type": "keyword"}}, + "type": "text", + }, + "body": {"analyzer": "snowball", "type": "text"}, + } + } + } +} +dsl_index_result = (1, {}, '{"result": "created"}') +dsl_index_span_name = "Elasticsearch/test-index/doc/2" +dsl_index_url = "/test-index/doc/2" +dsl_search_method = "GET" diff --git a/ext/opentelemetry-ext-elasticsearch/tests/helpers_es7.py b/ext/opentelemetry-ext-elasticsearch/tests/helpers_es7.py new file mode 100644 index 0000000000..a2d37a54a9 --- /dev/null +++ b/ext/opentelemetry-ext-elasticsearch/tests/helpers_es7.py @@ -0,0 +1,31 @@ +from elasticsearch_dsl import ( # pylint: disable=unused-import + Document, + Keyword, + Text, +) + + +class Article(Document): + title = Text(analyzer="snowball", fields={"raw": Keyword()}) + body = Text(analyzer="snowball") + + class Index: + name = "test-index" + + +dsl_create_statement = { + "mappings": { + "properties": { + "title": { + "analyzer": "snowball", + "fields": {"raw": {"type": "keyword"}}, + "type": "text", + }, + "body": {"analyzer": "snowball", "type": "text"}, + } + } +} +dsl_index_result = (1, {}, '{"result": "created"}') +dsl_index_span_name = "Elasticsearch/test-index/_doc/2" +dsl_index_url = "/test-index/_doc/2" +dsl_search_method = "POST" diff --git a/ext/opentelemetry-ext-elasticsearch/tests/test_elasticsearch.py b/ext/opentelemetry-ext-elasticsearch/tests/test_elasticsearch.py new file mode 100644 index 0000000000..f6ca6bdae1 --- /dev/null +++ b/ext/opentelemetry-ext-elasticsearch/tests/test_elasticsearch.py @@ -0,0 +1,307 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import threading +from ast import literal_eval +from unittest import mock + +import elasticsearch +import elasticsearch.exceptions +from elasticsearch import Elasticsearch +from elasticsearch_dsl import Search + +import opentelemetry.ext.elasticsearch +from opentelemetry.ext.elasticsearch import ElasticsearchInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace.status import StatusCanonicalCode + +major_version = elasticsearch.VERSION[0] + +if major_version == 7: + from . import helpers_es7 as helpers # pylint: disable=no-name-in-module +elif major_version == 6: + from . import helpers_es6 as helpers # pylint: disable=no-name-in-module +elif major_version == 5: + from . import helpers_es5 as helpers # pylint: disable=no-name-in-module +else: + from . import helpers_es2 as helpers # pylint: disable=no-name-in-module + + +Article = helpers.Article + + +@mock.patch( + "elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request" +) +class TestElasticsearchIntegration(TestBase): + def setUp(self): + super().setUp() + self.tracer = self.tracer_provider.get_tracer(__name__) + ElasticsearchInstrumentor().instrument() + + def tearDown(self): + super().tearDown() + with self.disable_logging(): + ElasticsearchInstrumentor().uninstrument() + + def get_ordered_finished_spans(self): + return sorted( + self.memory_exporter.get_finished_spans(), + key=lambda s: s.start_time, + ) + + def test_instrumentor(self, request_mock): + request_mock.return_value = (1, {}, {}) + + es = Elasticsearch() + es.index(index="sw", doc_type="people", id=1, body={"name": "adam"}) + + spans_list = self.get_ordered_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check version and name in span's instrumentation info + # self.check_span_instrumentation_info(span, opentelemetry.ext.elasticsearch) + self.check_span_instrumentation_info( + span, opentelemetry.ext.elasticsearch + ) + + # check that no spans are generated after uninstrument + ElasticsearchInstrumentor().uninstrument() + + es.index(index="sw", doc_type="people", id=1, body={"name": "adam"}) + + spans_list = self.get_ordered_finished_spans() + self.assertEqual(len(spans_list), 1) + + def test_prefix_arg(self, request_mock): + prefix = "prefix-from-env" + ElasticsearchInstrumentor().uninstrument() + ElasticsearchInstrumentor(span_name_prefix=prefix).instrument() + request_mock.return_value = (1, {}, {}) + self._test_prefix(prefix) + + def test_prefix_env(self, request_mock): + prefix = "prefix-from-args" + env_var = "OPENTELEMETRY_PYTHON_ELASTICSEARCH_NAME_PREFIX" + os.environ[env_var] = prefix + ElasticsearchInstrumentor().uninstrument() + ElasticsearchInstrumentor().instrument() + request_mock.return_value = (1, {}, {}) + del os.environ[env_var] + self._test_prefix(prefix) + + def _test_prefix(self, prefix): + es = Elasticsearch() + es.index(index="sw", doc_type="people", id=1, body={"name": "adam"}) + + spans_list = self.get_ordered_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertTrue(span.name.startswith(prefix)) + + def test_result_values(self, request_mock): + request_mock.return_value = ( + 1, + {}, + '{"found": false, "timed_out": true, "took": 7}', + ) + es = Elasticsearch() + es.get(index="test-index", doc_type="tweet", id=1) + + spans = self.get_ordered_finished_spans() + + self.assertEqual(1, len(spans)) + self.assertEqual("False", spans[0].attributes["elasticsearch.found"]) + self.assertEqual( + "True", spans[0].attributes["elasticsearch.timed_out"] + ) + self.assertEqual("7", spans[0].attributes["elasticsearch.took"]) + + def test_trace_error_unknown(self, request_mock): + exc = RuntimeError("custom error") + request_mock.side_effect = exc + self._test_trace_error(StatusCanonicalCode.UNKNOWN, exc) + + def test_trace_error_not_found(self, request_mock): + msg = "record not found" + exc = elasticsearch.exceptions.NotFoundError(404, msg) + request_mock.return_value = (1, {}, {}) + request_mock.side_effect = exc + self._test_trace_error(StatusCanonicalCode.NOT_FOUND, exc) + + def _test_trace_error(self, code, exc): + es = Elasticsearch() + try: + es.get(index="test-index", doc_type="tweet", id=1) + except Exception: # pylint: disable=broad-except + pass + + spans = self.get_ordered_finished_spans() + self.assertEqual(1, len(spans)) + span = spans[0] + self.assertFalse(span.status.is_ok) + self.assertEqual(span.status.canonical_code, code) + self.assertEqual(span.status.description, str(exc)) + + def test_parent(self, request_mock): + request_mock.return_value = (1, {}, {}) + es = Elasticsearch() + with self.tracer.start_as_current_span("parent"): + es.index( + index="sw", doc_type="people", id=1, body={"name": "adam"} + ) + + spans = self.get_ordered_finished_spans() + self.assertEqual(len(spans), 2) + + self.assertEqual(spans[0].name, "parent") + self.assertEqual(spans[1].name, "Elasticsearch/sw/people/1") + self.assertIsNotNone(spans[1].parent) + self.assertEqual(spans[1].parent.span_id, spans[0].context.span_id) + + def test_multithread(self, request_mock): + request_mock.return_value = (1, {}, {}) + es = Elasticsearch() + ev = threading.Event() + + # 1. Start tracing from thread-1; make thread-2 wait + # 2. Trace something from thread-2, make thread-1 join before finishing. + # 3. Check the spans got different parents, and are in the expected order. + def target1(parent_span): + with self.tracer.use_span(parent_span): + es.get(index="test-index", doc_type="tweet", id=1) + ev.set() + ev.wait() + + def target2(): + ev.wait() + es.get(index="test-index", doc_type="tweet", id=2) + ev.set() + + with self.tracer.start_as_current_span("parent") as span: + t1 = threading.Thread(target=target1, args=(span,)) + t1.start() + + t2 = threading.Thread(target=target2) + t2.start() + t1.join() + t2.join() + + spans = self.get_ordered_finished_spans() + self.assertEqual(3, len(spans)) + s1, s2, s3 = spans + + self.assertEqual(s1.name, "parent") + + self.assertEqual(s2.name, "Elasticsearch/test-index/tweet/1") + self.assertIsNotNone(s2.parent) + self.assertEqual(s2.parent.span_id, s1.context.span_id) + self.assertEqual(s3.name, "Elasticsearch/test-index/tweet/2") + self.assertIsNone(s3.parent) + + def test_dsl_search(self, request_mock): + request_mock.return_value = (1, {}, '{"hits": {"hits": []}}') + + client = Elasticsearch() + search = Search(using=client, index="test-index").filter( + "term", author="testing" + ) + search.execute() + spans = self.get_ordered_finished_spans() + span = spans[0] + self.assertEqual(1, len(spans)) + self.assertEqual(span.name, "Elasticsearch/test-index/_search") + self.assertIsNotNone(span.end_time) + self.assertEqual( + span.attributes, + { + "component": "elasticsearch-py", + "db.type": "elasticsearch", + "elasticsearch.url": "/test-index/_search", + "elasticsearch.method": helpers.dsl_search_method, + "db.statement": str( + { + "query": { + "bool": { + "filter": [{"term": {"author": "testing"}}] + } + } + } + ), + }, + ) + + def test_dsl_create(self, request_mock): + request_mock.return_value = (1, {}, {}) + client = Elasticsearch() + Article.init(using=client) + + spans = self.get_ordered_finished_spans() + self.assertEqual(2, len(spans)) + span1, span2 = spans + self.assertEqual(span1.name, "Elasticsearch/test-index") + self.assertEqual( + span1.attributes, + { + "component": "elasticsearch-py", + "db.type": "elasticsearch", + "elasticsearch.url": "/test-index", + "elasticsearch.method": "HEAD", + }, + ) + + self.assertEqual(span2.name, "Elasticsearch/test-index") + attributes = { + "component": "elasticsearch-py", + "db.type": "elasticsearch", + "elasticsearch.url": "/test-index", + "elasticsearch.method": "PUT", + } + self.assert_span_has_attributes(span2, attributes) + self.assertEqual( + literal_eval(span2.attributes["db.statement"]), + helpers.dsl_create_statement, + ) + + def test_dsl_index(self, request_mock): + request_mock.return_value = helpers.dsl_index_result + + client = Elasticsearch() + article = Article( + meta={"id": 2}, + title="About searching", + body="A few words here, a few words there", + ) + res = article.save(using=client) + self.assertTrue(res) + spans = self.get_ordered_finished_spans() + self.assertEqual(1, len(spans)) + span = spans[0] + self.assertEqual(span.name, helpers.dsl_index_span_name) + attributes = { + "component": "elasticsearch-py", + "db.type": "elasticsearch", + "elasticsearch.url": helpers.dsl_index_url, + "elasticsearch.method": "PUT", + } + self.assert_span_has_attributes(span, attributes) + self.assertEqual( + literal_eval(span.attributes["db.statement"]), + { + "body": "A few words here, a few words there", + "title": "About searching", + }, + ) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py index ccd14d142a..dedeca2003 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py @@ -39,10 +39,10 @@ class BaseInstrumentor(ABC): _instance = None _is_instrumented = False - def __new__(cls): + def __new__(cls, *args, **kwargs): if cls._instance is None: - cls._instance = object.__new__(cls) + cls._instance = object.__new__(cls, *args, **kwargs) return cls._instance diff --git a/scripts/eachdist.py b/scripts/eachdist.py index c8b7f7c3fc..57560b9b84 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -450,6 +450,7 @@ def install_args(args): allfmt += "[{}]".format(",".join(extras)) # note the trailing single quote, to close the quote opened above. allfmt += "'" + execute_args( parse_subargs( args, diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index 4b40d52ccb..96d98cff3d 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -52,6 +52,11 @@ def check_span_instrumentation_info(self, span, module): self.assertEqual(span.instrumentation_info.name, module.__name__) self.assertEqual(span.instrumentation_info.version, module.__version__) + def assert_span_has_attributes(self, span, attributes): + for key, val in attributes.items(): + self.assertIn(key, span.attributes) + self.assertEqual(val, span.attributes[key]) + @staticmethod def create_tracer_provider(**kwargs): """Helper to create a configured tracer provider. diff --git a/tox.ini b/tox.ini index 875f63fc3f..c8327d6f21 100644 --- a/tox.ini +++ b/tox.ini @@ -48,6 +48,10 @@ envlist = py3{5,6,7,8}-test-ext-boto pypy3-test-ext-boto + ; opentelemetry-ext-elasticsearch + py3{4,5,6,7,8}-test-ext-elasticsearch{2,5,6,7} + pypy3-test-ext-elasticsearch{2,5,6,7} + ; opentelemetry-ext-flask py3{4,5,6,7,8}-test-ext-flask pypy3-test-ext-flask @@ -170,6 +174,14 @@ deps = coverage: pytest coverage: pytest-cov mypy,mypyinstalled: mypy + elasticsearch2: elasticsearch-dsl>=2.0,<3.0 + elasticsearch2: elasticsearch>=2.0,<3.0 + elasticsearch5: elasticsearch-dsl>=5.0,<6.0 + elasticsearch5: elasticsearch>=5.0,<6.0 + elasticsearch6: elasticsearch-dsl>=6.0,<7.0 + elasticsearch6: elasticsearch>=6.0,<7.0 + elasticsearch7: elasticsearch-dsl>=7.0,<8.0 + elasticsearch7: elasticsearch>=7.0,<8.0 setenv = mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ @@ -187,6 +199,7 @@ changedir = test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-datadog: ext/opentelemetry-ext-datadog/tests test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests + test-ext-elasticsearch{2,5,6,7}: ext/opentelemetry-ext-elasticsearch/tests test-ext-django: ext/opentelemetry-ext-django/tests test-ext-mysql: ext/opentelemetry-ext-mysql/tests test-ext-opencensusexporter: ext/opentelemetry-ext-opencensusexporter/tests @@ -290,6 +303,7 @@ commands_pre = exporter-cloud-monitoring: pip install {toxinidir}/ext/opentelemetry-exporter-cloud-monitoring exporter-cloud-trace: pip install {toxinidir}/ext/opentelemetry-exporter-cloud-trace + elasticsearch{2,5,6,7}: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/ext/opentelemetry-ext-elasticsearch[test] ; In order to get a healthy coverage report, ; we have to install packages in editable mode. From a284367f33170ee50a6869d567e3817cde896859 Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Wed, 17 Jun 2020 01:13:23 -0400 Subject: [PATCH 0423/1517] Add instrumentation for Celery (#780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ported from the DataDog instrumentation. Co-authored-by: Diego Hurtado Co-authored-by: Mauricio Vásquez Co-authored-by: alrex --- .pylintrc | 2 +- dev-requirements.txt | 2 +- docs-requirements.txt | 1 + docs/ext/celery/celery.rst | 7 + ext/opentelemetry-ext-celery/CHANGELOG.md | 5 + ext/opentelemetry-ext-celery/LICENSE | 201 +++++++ ext/opentelemetry-ext-celery/MANIFEST.in | 9 + ext/opentelemetry-ext-celery/README.rst | 50 ++ ext/opentelemetry-ext-celery/setup.cfg | 56 ++ ext/opentelemetry-ext-celery/setup.py | 26 + .../src/opentelemetry/ext/celery/__init__.py | 223 ++++++++ .../src/opentelemetry/ext/celery/utils.py | 219 ++++++++ .../src/opentelemetry/ext/celery/version.py | 15 + .../tests/__init__.py | 0 .../tests/test_utils.py | 208 +++++++ .../tests/celery/conftest.py | 92 +++ .../tests/celery/test_celery_functional.py | 523 ++++++++++++++++++ tox.ini | 9 + 18 files changed, 1646 insertions(+), 2 deletions(-) create mode 100644 docs/ext/celery/celery.rst create mode 100644 ext/opentelemetry-ext-celery/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-celery/LICENSE create mode 100644 ext/opentelemetry-ext-celery/MANIFEST.in create mode 100644 ext/opentelemetry-ext-celery/README.rst create mode 100644 ext/opentelemetry-ext-celery/setup.cfg create mode 100644 ext/opentelemetry-ext-celery/setup.py create mode 100644 ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/__init__.py create mode 100644 ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/utils.py create mode 100644 ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py create mode 100644 ext/opentelemetry-ext-celery/tests/__init__.py create mode 100644 ext/opentelemetry-ext-celery/tests/test_utils.py create mode 100644 ext/opentelemetry-ext-docker-tests/tests/celery/conftest.py create mode 100644 ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py diff --git a/.pylintrc b/.pylintrc index 01c96ea395..5f9463df7d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -226,7 +226,7 @@ dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ +ignored-argument-names=_.*|^ignored_|^unused_|^kwargs|^args # Tells whether we should check for unused import in __init__ files. init-import=no diff --git a/dev-requirements.txt b/dev-requirements.txt index 8ea405d92a..3808b02143 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -8,4 +8,4 @@ sphinx-rtd-theme~=0.4 sphinx-autodoc-typehints~=1.10.2 pytest!=5.2.3 pytest-cov>=2.8 -readme-renderer~=24.0 \ No newline at end of file +readme-renderer~=24.0 diff --git a/docs-requirements.txt b/docs-requirements.txt index 10ccf1b21c..84f80b30e5 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -21,6 +21,7 @@ redis>=2.6 sqlalchemy>=1.0 thrift>=0.10.0 wrapt>=1.0.0,<2.0.0 +celery>=4.0 psutil~=5.7.0 boto~=2.0 google-cloud-trace >=0.23.0 diff --git a/docs/ext/celery/celery.rst b/docs/ext/celery/celery.rst new file mode 100644 index 0000000000..125233e006 --- /dev/null +++ b/docs/ext/celery/celery.rst @@ -0,0 +1,7 @@ +OpenTelemetry Celery Instrumentation +==================================== + +.. automodule:: opentelemetry.ext.celery + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-celery/CHANGELOG.md b/ext/opentelemetry-ext-celery/CHANGELOG.md new file mode 100644 index 0000000000..dd6bf18c9d --- /dev/null +++ b/ext/opentelemetry-ext-celery/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Add instrumentation for Celery ([#780](https://github.com/open-telemetry/opentelemetry-python/pull/780)) diff --git a/ext/opentelemetry-ext-celery/LICENSE b/ext/opentelemetry-ext-celery/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-celery/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-celery/MANIFEST.in b/ext/opentelemetry-ext-celery/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-celery/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-celery/README.rst b/ext/opentelemetry-ext-celery/README.rst new file mode 100644 index 0000000000..feabf6809f --- /dev/null +++ b/ext/opentelemetry-ext-celery/README.rst @@ -0,0 +1,50 @@ +OpenTelemetry Celery Instrumentation +==================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-celery.svg + :target: https://pypi.org/project/opentelemetry-ext-celery/ + +Instrumentation for Celery. + + +Installation +------------ + +:: + + pip install opentelemetry-ext-celery + +Usage +----- + +* Start broker backend + +:: + docker run -p 5672:5672 rabbitmq + + +* Run instrumented task + +.. code-block:: python + + from opentelemetry.ext.celery import CeleryInstrumentor + + CeleryInstrumentor().instrument() + + from celery import Celery + + app = Celery("tasks", broker="amqp://localhost") + + @app.task + def add(x, y): + return x + y + + add.delay(42, 50) + +References +---------- +* `OpenTelemetry Celery Instrumentation `_ +* `OpenTelemetry Project `_ + diff --git a/ext/opentelemetry-ext-celery/setup.cfg b/ext/opentelemetry-ext-celery/setup.cfg new file mode 100644 index 0000000000..ecb42b7fb7 --- /dev/null +++ b/ext/opentelemetry-ext-celery/setup.cfg @@ -0,0 +1,56 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-celery +description = OpenTelemetry Celery Instrumentation +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-celery +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 + celery ~= 4.0 + +[options.extras_require] +test = + pytest + opentelemetry-test == 0.10.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + celery = opentelemetry.ext.celery:CeleryInstrumentor diff --git a/ext/opentelemetry-ext-celery/setup.py b/ext/opentelemetry-ext-celery/setup.py new file mode 100644 index 0000000000..40d1d7aaba --- /dev/null +++ b/ext/opentelemetry-ext-celery/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "celery", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/__init__.py b/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/__init__.py new file mode 100644 index 0000000000..9ce31f34f6 --- /dev/null +++ b/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/__init__.py @@ -0,0 +1,223 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Instrument `celery`_ to trace Celery applications. + +.. _celery: https://pypi.org/project/celery/ + +Usage +----- + +* Start broker backend + +.. code:: + + docker run -p 5672:5672 rabbitmq + + +* Run instrumented task + +.. code:: python + + from opentelemetry.ext.celery import CeleryInstrumentor + + CeleryInstrumentor().instrument() + + from celery import Celery + + app = Celery("tasks", broker="amqp://localhost") + + @app.task + def add(x, y): + return x + y + + add.delay(42, 50) + +API +--- +""" + +import logging +import signal + +from celery import signals # pylint: disable=no-name-in-module + +from opentelemetry import trace +from opentelemetry.ext.celery import utils +from opentelemetry.ext.celery.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.trace.status import Status, StatusCanonicalCode + +logger = logging.getLogger(__name__) + +# Task operations +_TASK_TAG_KEY = "celery.action" +_TASK_APPLY_ASYNC = "apply_async" +_TASK_RUN = "run" + +_TASK_RETRY_REASON_KEY = "celery.retry.reason" +_TASK_REVOKED_REASON_KEY = "celery.revoked.reason" +_TASK_REVOKED_TERMINATED_SIGNAL_KEY = "celery.terminated.signal" +_TASK_NAME_KEY = "celery.task_name" +_MESSAGE_ID_ATTRIBUTE_NAME = "messaging.message_id" + + +class CeleryInstrumentor(BaseInstrumentor): + def _instrument(self, **kwargs): + tracer_provider = kwargs.get("tracer_provider") + + # pylint: disable=attribute-defined-outside-init + self._tracer = trace.get_tracer(__name__, __version__, tracer_provider) + + signals.task_prerun.connect(self._trace_prerun, weak=False) + signals.task_postrun.connect(self._trace_postrun, weak=False) + signals.before_task_publish.connect( + self._trace_before_publish, weak=False + ) + signals.after_task_publish.connect( + self._trace_after_publish, weak=False + ) + signals.task_failure.connect(self._trace_failure, weak=False) + signals.task_retry.connect(self._trace_retry, weak=False) + + def _uninstrument(self, **kwargs): + signals.task_prerun.disconnect(self._trace_prerun) + signals.task_postrun.disconnect(self._trace_postrun) + signals.before_task_publish.disconnect(self._trace_before_publish) + signals.after_task_publish.disconnect(self._trace_after_publish) + signals.task_failure.disconnect(self._trace_failure) + signals.task_retry.disconnect(self._trace_retry) + + def _trace_prerun(self, *args, **kwargs): + task = utils.retrieve_task(kwargs) + task_id = utils.retrieve_task_id(kwargs) + + if task is None or task_id is None: + return + + logger.debug("prerun signal start task_id=%s", task_id) + + span = self._tracer.start_span(task.name, kind=trace.SpanKind.CONSUMER) + + activation = self._tracer.use_span(span, end_on_exit=True) + activation.__enter__() + utils.attach_span(task, task_id, (span, activation)) + + @staticmethod + def _trace_postrun(*args, **kwargs): + task = utils.retrieve_task(kwargs) + task_id = utils.retrieve_task_id(kwargs) + + if task is None or task_id is None: + return + + logger.debug("postrun signal task_id=%s", task_id) + + # retrieve and finish the Span + span, activation = utils.retrieve_span(task, task_id) + if span is None: + logger.warning("no existing span found for task_id=%s", task_id) + return + + # request context tags + span.set_attribute(_TASK_TAG_KEY, _TASK_RUN) + utils.set_attributes_from_context(span, kwargs) + utils.set_attributes_from_context(span, task.request) + span.set_attribute(_TASK_NAME_KEY, task.name) + + activation.__exit__(None, None, None) + utils.detach_span(task, task_id) + + def _trace_before_publish(self, *args, **kwargs): + task = utils.retrieve_task_from_sender(kwargs) + task_id = utils.retrieve_task_id_from_message(kwargs) + + if task is None or task_id is None: + return + + span = self._tracer.start_span(task.name, kind=trace.SpanKind.PRODUCER) + + # apply some attributes here because most of the data is not available + span.set_attribute(_TASK_TAG_KEY, _TASK_APPLY_ASYNC) + span.set_attribute(_MESSAGE_ID_ATTRIBUTE_NAME, task_id) + span.set_attribute(_TASK_NAME_KEY, task.name) + utils.set_attributes_from_context(span, kwargs) + + activation = self._tracer.use_span(span, end_on_exit=True) + activation.__enter__() + utils.attach_span(task, task_id, (span, activation), is_publish=True) + + @staticmethod + def _trace_after_publish(*args, **kwargs): + task = utils.retrieve_task_from_sender(kwargs) + task_id = utils.retrieve_task_id_from_message(kwargs) + + if task is None or task_id is None: + return + + # retrieve and finish the Span + _, activation = utils.retrieve_span(task, task_id, is_publish=True) + if activation is None: + logger.warning("no existing span found for task_id=%s", task_id) + return + + activation.__exit__(None, None, None) + utils.detach_span(task, task_id, is_publish=True) + + @staticmethod + def _trace_failure(*args, **kwargs): + task = utils.retrieve_task_from_sender(kwargs) + task_id = utils.retrieve_task_id(kwargs) + + if task is None or task_id is None: + return + + # retrieve and pass exception info to activation + span, _ = utils.retrieve_span(task, task_id) + if span is None: + return + + status_kwargs = {"canonical_code": StatusCanonicalCode.UNKNOWN} + + ex = kwargs.get("einfo") + + if ( + hasattr(task, "throws") + and ex is not None + and isinstance(ex.exception, task.throws) + ): + return + + if ex is not None: + status_kwargs["description"] = str(ex) + + span.set_status(Status(**status_kwargs)) + + @staticmethod + def _trace_retry(*args, **kwargs): + task = utils.retrieve_task_from_sender(kwargs) + task_id = utils.retrieve_task_id_from_request(kwargs) + reason = utils.retrieve_reason(kwargs) + + if task is None or task_id is None or reason is None: + return + + span, _ = utils.retrieve_span(task, task_id) + if span is None: + return + + # Add retry reason metadata to span + # Use `str(reason)` instead of `reason.message` in case we get + # something that isn't an `Exception` + span.set_attribute(_TASK_RETRY_REASON_KEY, str(reason)) diff --git a/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/utils.py b/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/utils.py new file mode 100644 index 0000000000..60fe52f04e --- /dev/null +++ b/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/utils.py @@ -0,0 +1,219 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from celery import registry # pylint: disable=no-name-in-module + +logger = logging.getLogger(__name__) + +# Celery Context key +CTX_KEY = "__otel_task_span" + +# Celery Context attributes +CELERY_CONTEXT_ATTRIBUTES = ( + "compression", + "correlation_id", + "countdown", + "delivery_info", + "declare", + "eta", + "exchange", + "expires", + "hostname", + "id", + "priority", + "queue", + "reply_to", + "retries", + "routing_key", + "serializer", + "timelimit", + "origin", + "state", +) + + +# pylint:disable=too-many-branches +def set_attributes_from_context(span, context): + """Helper to extract meta values from a Celery Context""" + for key in CELERY_CONTEXT_ATTRIBUTES: + value = context.get(key) + + # Skip this key if it is not set + if value is None or value == "": + continue + + # Skip `timelimit` if it is not set (it's default/unset value is a + # tuple or a list of `None` values + if key == "timelimit" and value in [(None, None), [None, None]]: + continue + + # Skip `retries` if it's value is `0` + if key == "retries" and value == 0: + continue + + attribute_name = None + + # Celery 4.0 uses `origin` instead of `hostname`; this change preserves + # the same name for the tag despite Celery version + if key == "origin": + key = "hostname" + + elif key == "delivery_info": + # Get also destination from this + routing_key = value.get("routing_key") + if routing_key is not None: + span.set_attribute("messaging.destination", routing_key) + value = str(value) + + elif key == "id": + attribute_name = "messaging.message_id" + + elif key == "correlation_id": + attribute_name = "messaging.conversation_id" + + elif key == "routing_key": + attribute_name = "messaging.destination" + + # according to https://docs.celeryproject.org/en/stable/userguide/routing.html#exchange-types + elif key == "declare": + attribute_name = "messaging.destination_kind" + for declare in value: + if declare.exchange.type == "direct": + value = "queue" + break + if declare.exchange.type == "topic": + value = "topic" + break + + # set attribute name if not set specially for a key + if attribute_name is None: + attribute_name = "celery.{}".format(key) + + span.set_attribute(attribute_name, value) + + +def attach_span(task, task_id, span, is_publish=False): + """Helper to propagate a `Span` for the given `Task` instance. This + function uses a `dict` that stores the Span using the + `(task_id, is_publish)` as a key. This is useful when information must be + propagated from one Celery signal to another. + + We use (task_id, is_publish) for the key to ensure that publishing a + task from within another task does not cause any conflicts. + + This mostly happens when either a task fails and a retry policy is in place, + or when a task is manually retries (e.g. `task.retry()`), we end up trying + to publish a task with the same id as the task currently running. + + Previously publishing the new task would overwrite the existing `celery.run` span + in the `dict` causing that span to be forgotten and never finished + NOTE: We cannot test for this well yet, because we do not run a celery worker, + and cannot run `task.apply_async()` + """ + span_dict = getattr(task, CTX_KEY, None) + if span_dict is None: + span_dict = dict() + setattr(task, CTX_KEY, span_dict) + + span_dict[(task_id, is_publish)] = span + + +def detach_span(task, task_id, is_publish=False): + """Helper to remove a `Span` in a Celery task when it's propagated. + This function handles tasks where the `Span` is not attached. + """ + span_dict = getattr(task, CTX_KEY, None) + if span_dict is None: + return + + # See note in `attach_span` for key info + span_dict.pop((task_id, is_publish), (None, None)) + + +def retrieve_span(task, task_id, is_publish=False): + """Helper to retrieve an active `Span` stored in a `Task` + instance + """ + span_dict = getattr(task, CTX_KEY, None) + if span_dict is None: + return (None, None) + + # See note in `attach_span` for key info + return span_dict.get((task_id, is_publish), (None, None)) + + +def retrieve_task(kwargs): + task = kwargs.get("task") + if task is None: + logger.debug("Unable to retrieve task from signal arguments") + return task + + +def retrieve_task_from_sender(kwargs): + sender = kwargs.get("sender") + if sender is None: + logger.debug("Unable to retrieve the sender from signal arguments") + + # before and after publish signals sender is the task name + # for retry and failure signals sender is the task object + if isinstance(sender, str): + sender = registry.tasks.get(sender) + if sender is None: + logger.debug("Unable to retrieve the task from sender=%s", sender) + + return sender + + +def retrieve_task_id(kwargs): + task_id = kwargs.get("task_id") + if task_id is None: + logger.debug("Unable to retrieve task_id from signal arguments") + return task_id + + +def retrieve_task_id_from_request(kwargs): + # retry signal does not include task_id as argument so use request argument + request = kwargs.get("request") + if request is None: + logger.debug("Unable to retrieve the request from signal arguments") + + task_id = getattr(request, "id") + if task_id is None: + logger.debug("Unable to retrieve the task_id from the request") + + return task_id + + +def retrieve_task_id_from_message(kwargs): + """Helper to retrieve the `Task` identifier from the message `body`. + This helper supports Protocol Version 1 and 2. The Protocol is well + detailed in the official documentation: + http://docs.celeryproject.org/en/latest/internals/protocol.html + """ + headers = kwargs.get("headers") + body = kwargs.get("body") + if headers is not None and len(headers) > 0: + # Protocol Version 2 (default from Celery 4.0) + return headers.get("id") + # Protocol Version 1 + return body.get("id") + + +def retrieve_reason(kwargs): + reason = kwargs.get("reason") + if not reason: + logger.debug("Unable to retrieve the retry reason") + return reason diff --git a/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py b/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py new file mode 100644 index 0000000000..6d4fefa599 --- /dev/null +++ b/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-celery/tests/__init__.py b/ext/opentelemetry-ext-celery/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-celery/tests/test_utils.py b/ext/opentelemetry-ext-celery/tests/test_utils.py new file mode 100644 index 0000000000..b5e8163def --- /dev/null +++ b/ext/opentelemetry-ext-celery/tests/test_utils.py @@ -0,0 +1,208 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from celery import Celery + +from opentelemetry import trace as trace_api +from opentelemetry.ext.celery import utils +from opentelemetry.sdk import trace + + +class TestUtils(unittest.TestCase): + def setUp(self): + self.app = Celery("celery.test_app") + + def test_set_attributes_from_context(self): + # it should extract only relevant keys + context = { + "correlation_id": "44b7f305", + "delivery_info": {"eager": True}, + "eta": "soon", + "expires": "later", + "hostname": "localhost", + "id": "44b7f305", + "reply_to": "44b7f305", + "retries": 4, + "timelimit": ("now", "later"), + "custom_meta": "custom_value", + "routing_key": "celery", + } + + span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + utils.set_attributes_from_context(span, context) + + self.assertEqual( + span.attributes.get("messaging.message_id"), "44b7f305" + ) + self.assertEqual( + span.attributes.get("messaging.conversation_id"), "44b7f305" + ) + self.assertEqual( + span.attributes.get("messaging.destination"), "celery" + ) + + self.assertEqual( + span.attributes["celery.delivery_info"], str({"eager": True}) + ) + self.assertEqual(span.attributes.get("celery.eta"), "soon") + self.assertEqual(span.attributes.get("celery.expires"), "later") + self.assertEqual(span.attributes.get("celery.hostname"), "localhost") + + self.assertEqual(span.attributes.get("celery.reply_to"), "44b7f305") + self.assertEqual(span.attributes.get("celery.retries"), 4) + self.assertEqual( + span.attributes.get("celery.timelimit"), ("now", "later") + ) + self.assertNotIn("custom_meta", span.attributes) + + def test_set_attributes_from_context_empty_keys(self): + # it should not extract empty keys + context = { + "correlation_id": None, + "exchange": "", + "timelimit": (None, None), + "retries": 0, + } + + span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + utils.set_attributes_from_context(span, context) + + self.assertEqual(len(span.attributes), 0) + # edge case: `timelimit` can also be a list of None values + context = { + "timelimit": [None, None], + } + + utils.set_attributes_from_context(span, context) + + self.assertEqual(len(span.attributes), 0) + + def test_span_propagation(self): + # ensure spans getter and setter works properly + @self.app.task + def fn_task(): + return 42 + + # propagate and retrieve a Span + task_id = "7c6731af-9533-40c3-83a9-25b58f0d837f" + span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + utils.attach_span(fn_task, task_id, span) + span_after = utils.retrieve_span(fn_task, task_id) + self.assertIs(span, span_after) + + def test_span_delete(self): + # ensure the helper removes properly a propagated Span + @self.app.task + def fn_task(): + return 42 + + # propagate a Span + task_id = "7c6731af-9533-40c3-83a9-25b58f0d837f" + span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + utils.attach_span(fn_task, task_id, span) + # delete the Span + utils.detach_span(fn_task, task_id) + self.assertEqual(utils.retrieve_span(fn_task, task_id), (None, None)) + + def test_span_delete_empty(self): + # ensure detach_span doesn't raise an exception if span is not present + @self.app.task + def fn_task(): + return 42 + + # delete the Span + task_id = "7c6731af-9533-40c3-83a9-25b58f0d837f" + try: + utils.detach_span(fn_task, task_id) + self.assertEqual( + utils.retrieve_span(fn_task, task_id), (None, None) + ) + except Exception as ex: # pylint: disable=broad-except + self.fail("Exception was raised: %s" % ex) + + def test_task_id_from_protocol_v1(self): + # ensures a `task_id` is properly returned when Protocol v1 is used. + # `context` is an example of an emitted Signal with Protocol v1 + context = { + "body": { + "expires": None, + "utc": True, + "args": ["user"], + "chord": None, + "callbacks": None, + "errbacks": None, + "taskset": None, + "id": "dffcaec1-dd92-4a1a-b3ab-d6512f4beeb7", + "retries": 0, + "task": "tests.contrib.celery.test_integration.fn_task_parameters", + "timelimit": (None, None), + "eta": None, + "kwargs": {"force_logout": True}, + }, + "sender": "tests.contrib.celery.test_integration.fn_task_parameters", + "exchange": "celery", + "routing_key": "celery", + "retry_policy": None, + "headers": {}, + "properties": {}, + } + + task_id = utils.retrieve_task_id_from_message(context) + self.assertEqual(task_id, "dffcaec1-dd92-4a1a-b3ab-d6512f4beeb7") + + def test_task_id_from_protocol_v2(self): + # ensures a `task_id` is properly returned when Protocol v2 is used. + # `context` is an example of an emitted Signal with Protocol v2 + context = { + "body": ( + ["user"], + {"force_logout": True}, + { + u"chord": None, + u"callbacks": None, + u"errbacks": None, + u"chain": None, + }, + ), + "sender": u"tests.contrib.celery.test_integration.fn_task_parameters", + "exchange": u"", + "routing_key": u"celery", + "retry_policy": None, + "headers": { + u"origin": u"gen83744@hostname", + u"root_id": "7e917b83-4018-431d-9832-73a28e1fb6c0", + u"expires": None, + u"shadow": None, + u"id": "7e917b83-4018-431d-9832-73a28e1fb6c0", + u"kwargsrepr": u"{'force_logout': True}", + u"lang": u"py", + u"retries": 0, + u"task": u"tests.contrib.celery.test_integration.fn_task_parameters", + u"group": None, + u"timelimit": [None, None], + u"parent_id": None, + u"argsrepr": u"['user']", + u"eta": None, + }, + "properties": { + u"reply_to": "c3054a07-5b28-3855-b18c-1623a24aaeca", + u"correlation_id": "7e917b83-4018-431d-9832-73a28e1fb6c0", + }, + } + + task_id = utils.retrieve_task_id_from_message(context) + self.assertEqual(task_id, "7e917b83-4018-431d-9832-73a28e1fb6c0") diff --git a/ext/opentelemetry-ext-docker-tests/tests/celery/conftest.py b/ext/opentelemetry-ext-docker-tests/tests/celery/conftest.py new file mode 100644 index 0000000000..0e6976382e --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/celery/conftest.py @@ -0,0 +1,92 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from functools import wraps + +import pytest + +from opentelemetry import trace as trace_api +from opentelemetry.ext.celery import CeleryInstrumentor +from opentelemetry.sdk.trace import TracerProvider, export +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +REDIS_HOST = os.getenv("REDIS_HOST", "localhost") +REDIS_PORT = int(os.getenv("REDIS_PORT ", "6379")) +REDIS_URL = "redis://{host}:{port}".format(host=REDIS_HOST, port=REDIS_PORT) +BROKER_URL = "{redis}/{db}".format(redis=REDIS_URL, db=0) +BACKEND_URL = "{redis}/{db}".format(redis=REDIS_URL, db=1) + + +@pytest.fixture(scope="session") +def celery_config(): + return {"broker_url": BROKER_URL, "result_backend": BACKEND_URL} + + +@pytest.fixture +def celery_worker_parameters(): + return { + # See https://github.com/celery/celery/issues/3642#issuecomment-457773294 + "perform_ping_check": False, + } + + +@pytest.fixture(autouse=True) +def patch_celery_app(celery_app, celery_worker): + """Patch task decorator on app fixture to reload worker""" + # See https://github.com/celery/celery/issues/3642 + def wrap_task(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + result = fn(*args, **kwargs) + celery_worker.reload() + return result + + return wrapper + + celery_app.task = wrap_task(celery_app.task) + + +@pytest.fixture(autouse=True) +def instrument(tracer_provider, memory_exporter): + CeleryInstrumentor().instrument(tracer_provider=tracer_provider) + memory_exporter.clear() + + yield + + CeleryInstrumentor().uninstrument() + + +@pytest.fixture(scope="session") +def tracer_provider(memory_exporter): + original_tracer_provider = trace_api.get_tracer_provider() + + tracer_provider = TracerProvider() + + span_processor = export.SimpleExportSpanProcessor(memory_exporter) + tracer_provider.add_span_processor(span_processor) + + trace_api.set_tracer_provider(tracer_provider) + + yield tracer_provider + + trace_api.set_tracer_provider(original_tracer_provider) + + +@pytest.fixture(scope="session") +def memory_exporter(): + memory_exporter = InMemorySpanExporter() + return memory_exporter diff --git a/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py b/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py new file mode 100644 index 0000000000..570a12b758 --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py @@ -0,0 +1,523 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import time + +import celery +import pytest +from celery import signals +from celery.exceptions import Retry + +import opentelemetry.ext.celery +from opentelemetry import trace as trace_api +from opentelemetry.ext.celery import CeleryInstrumentor +from opentelemetry.sdk import resources +from opentelemetry.sdk.trace import TracerProvider, export +from opentelemetry.trace.status import StatusCanonicalCode + + +class MyException(Exception): + pass + + +def test_instrumentation_info(celery_app, memory_exporter): + @celery_app.task + def fn_task(): + return 42 + + result = fn_task.apply_async() + assert result.get() == 42 + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 2 + + async_span, run_span = spans + + assert ( + async_span.instrumentation_info.name + == opentelemetry.ext.celery.__name__ + ) + assert ( + async_span.instrumentation_info.version + == opentelemetry.ext.celery.__version__ + ) + assert ( + run_span.instrumentation_info.name == opentelemetry.ext.celery.__name__ + ) + assert ( + run_span.instrumentation_info.version + == opentelemetry.ext.celery.__version__ + ) + + +def test_fn_task_run(celery_app, memory_exporter): + @celery_app.task + def fn_task(): + return 42 + + t = fn_task.run() + assert t == 42 + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 0 + + +def test_fn_task_call(celery_app, memory_exporter): + @celery_app.task + def fn_task(): + return 42 + + t = fn_task() + assert t == 42 + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 0 + + +def test_fn_task_apply(celery_app, memory_exporter): + @celery_app.task + def fn_task(): + return 42 + + t = fn_task.apply() + assert t.successful() is True + assert t.result == 42 + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 1 + + span = spans[0] + + assert span.status.is_ok is True + assert span.name == "test_celery_functional.fn_task" + assert span.attributes.get("messaging.message_id") == t.task_id + assert ( + span.attributes.get("celery.task_name") + == "test_celery_functional.fn_task" + ) + assert span.attributes.get("celery.action") == "run" + assert span.attributes.get("celery.state") == "SUCCESS" + + +def test_fn_task_apply_bind(celery_app, memory_exporter): + @celery_app.task(bind=True) + def fn_task(self): + return self + + t = fn_task.apply() + assert t.successful() is True + assert "fn_task" in t.result.name + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 1 + + span = spans[0] + + assert span.status.is_ok is True + assert span.name == "test_celery_functional.fn_task" + assert span.attributes.get("messaging.message_id") == t.task_id + assert ( + span.attributes.get("celery.task_name") + == "test_celery_functional.fn_task" + ) + assert span.attributes.get("celery.action") == "run" + assert span.attributes.get("celery.state") == "SUCCESS" + + +def test_fn_task_apply_async(celery_app, memory_exporter): + @celery_app.task + def fn_task_parameters(user, force_logout=False): + return (user, force_logout) + + result = fn_task_parameters.apply_async( + args=["user"], kwargs={"force_logout": True} + ) + assert result.get(timeout=10) == ["user", True] + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 2 + + async_span, run_span = spans + + assert run_span.context.trace_id != async_span.context.trace_id + + assert async_span.status.is_ok is True + assert async_span.name == "test_celery_functional.fn_task_parameters" + assert async_span.attributes.get("celery.action") == "apply_async" + assert async_span.attributes.get("messaging.message_id") == result.task_id + assert ( + async_span.attributes.get("celery.task_name") + == "test_celery_functional.fn_task_parameters" + ) + + assert run_span.status.is_ok is True + assert run_span.name == "test_celery_functional.fn_task_parameters" + assert run_span.attributes.get("celery.action") == "run" + assert run_span.attributes.get("celery.state") == "SUCCESS" + assert run_span.attributes.get("messaging.message_id") == result.task_id + assert ( + run_span.attributes.get("celery.task_name") + == "test_celery_functional.fn_task_parameters" + ) + + +def test_concurrent_delays(celery_app, memory_exporter): + @celery_app.task + def fn_task(): + return 42 + + results = [fn_task.delay() for _ in range(100)] + + for result in results: + assert result.get(timeout=1) == 42 + + spans = memory_exporter.get_finished_spans() + + assert len(spans) == 200 + + +def test_fn_task_delay(celery_app, memory_exporter): + @celery_app.task + def fn_task_parameters(user, force_logout=False): + return (user, force_logout) + + result = fn_task_parameters.delay("user", force_logout=True) + assert result.get(timeout=10) == ["user", True] + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 2 + + async_span, run_span = spans + + assert run_span.context.trace_id != async_span.context.trace_id + + assert async_span.status.is_ok is True + assert async_span.name == "test_celery_functional.fn_task_parameters" + assert async_span.attributes.get("celery.action") == "apply_async" + assert async_span.attributes.get("messaging.message_id") == result.task_id + assert ( + async_span.attributes.get("celery.task_name") + == "test_celery_functional.fn_task_parameters" + ) + + assert run_span.status.is_ok is True + assert run_span.name == "test_celery_functional.fn_task_parameters" + assert run_span.attributes.get("celery.action") == "run" + assert run_span.attributes.get("celery.state") == "SUCCESS" + assert run_span.attributes.get("messaging.message_id") == result.task_id + assert ( + run_span.attributes.get("celery.task_name") + == "test_celery_functional.fn_task_parameters" + ) + + +def test_fn_exception(celery_app, memory_exporter): + @celery_app.task + def fn_exception(): + raise Exception("Task class is failing") + + result = fn_exception.apply() + + assert result.failed() is True + assert "Task class is failing" in result.traceback + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 1 + + span = spans[0] + + assert span.status.is_ok is False + assert span.name == "test_celery_functional.fn_exception" + assert span.attributes.get("celery.action") == "run" + assert span.attributes.get("celery.state") == "FAILURE" + assert ( + span.attributes.get("celery.task_name") + == "test_celery_functional.fn_exception" + ) + assert span.status.canonical_code == StatusCanonicalCode.UNKNOWN + assert span.attributes.get("messaging.message_id") == result.task_id + assert "Task class is failing" in span.status.description + + +def test_fn_exception_expected(celery_app, memory_exporter): + @celery_app.task(throws=(MyException,)) + def fn_exception(): + raise MyException("Task class is failing") + + result = fn_exception.apply() + + assert result.failed() is True + assert "Task class is failing" in result.traceback + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 1 + + span = spans[0] + + assert span.status.is_ok is True + assert span.status.canonical_code == StatusCanonicalCode.OK + assert span.name == "test_celery_functional.fn_exception" + assert span.attributes.get("celery.action") == "run" + assert span.attributes.get("celery.state") == "FAILURE" + assert ( + span.attributes.get("celery.task_name") + == "test_celery_functional.fn_exception" + ) + assert span.attributes.get("messaging.message_id") == result.task_id + + +def test_fn_retry_exception(celery_app, memory_exporter): + @celery_app.task + def fn_exception(): + raise Retry("Task class is being retried") + + result = fn_exception.apply() + + assert result.failed() is False + assert "Task class is being retried" in result.traceback + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 1 + + span = spans[0] + + assert span.status.is_ok is True + assert span.status.canonical_code == StatusCanonicalCode.OK + assert span.name == "test_celery_functional.fn_exception" + assert span.attributes.get("celery.action") == "run" + assert span.attributes.get("celery.state") == "RETRY" + assert ( + span.attributes.get("celery.task_name") + == "test_celery_functional.fn_exception" + ) + assert span.attributes.get("messaging.message_id") == result.task_id + + +def test_class_task(celery_app, memory_exporter): + class BaseTask(celery_app.Task): + def run(self): + return 42 + + task = BaseTask() + # register the Task class if it's available (required in Celery 4.0+) + register_task = getattr(celery_app, "register_task", None) + if register_task is not None: + register_task(task) + + result = task.apply() + + assert result.successful() is True + assert result.result == 42 + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 1 + + span = spans[0] + + assert span.status.is_ok is True + assert span.name == "test_celery_functional.BaseTask" + assert ( + span.attributes.get("celery.task_name") + == "test_celery_functional.BaseTask" + ) + assert span.attributes.get("celery.action") == "run" + assert span.attributes.get("celery.state") == "SUCCESS" + assert span.attributes.get("messaging.message_id") == result.task_id + + +def test_class_task_exception(celery_app, memory_exporter): + class BaseTask(celery_app.Task): + def run(self): + raise Exception("Task class is failing") + + task = BaseTask() + # register the Task class if it's available (required in Celery 4.0+) + register_task = getattr(celery_app, "register_task", None) + if register_task is not None: + register_task(task) + + result = task.apply() + + assert result.failed() is True + assert "Task class is failing" in result.traceback + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 1 + + span = spans[0] + + assert span.status.is_ok is False + assert span.name == "test_celery_functional.BaseTask" + assert ( + span.attributes.get("celery.task_name") + == "test_celery_functional.BaseTask" + ) + assert span.attributes.get("celery.action") == "run" + assert span.attributes.get("celery.state") == "FAILURE" + assert span.status.canonical_code == StatusCanonicalCode.UNKNOWN + assert span.attributes.get("messaging.message_id") == result.task_id + assert "Task class is failing" in span.status.description + + +def test_class_task_exception_excepted(celery_app, memory_exporter): + class BaseTask(celery_app.Task): + throws = (MyException,) + + def run(self): + raise MyException("Task class is failing") + + task = BaseTask() + # register the Task class if it's available (required in Celery 4.0+) + register_task = getattr(celery_app, "register_task", None) + if register_task is not None: + register_task(task) + + result = task.apply() + + assert result.failed() is True + assert "Task class is failing" in result.traceback + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 1 + + span = spans[0] + + assert span.status.is_ok is True + assert span.status.canonical_code == StatusCanonicalCode.OK + assert span.name == "test_celery_functional.BaseTask" + assert span.attributes.get("celery.action") == "run" + assert span.attributes.get("celery.state") == "FAILURE" + assert span.attributes.get("messaging.message_id") == result.task_id + + +def test_shared_task(celery_app, memory_exporter): + """Ensure Django Shared Task are supported""" + + @celery.shared_task + def add(x, y): + return x + y + + result = add.apply([2, 2]) + assert result.result == 4 + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 1 + + span = spans[0] + + assert span.status.is_ok is True + assert span.name == "test_celery_functional.add" + assert ( + span.attributes.get("celery.task_name") == "test_celery_functional.add" + ) + assert span.attributes.get("celery.action") == "run" + assert span.attributes.get("celery.state") == "SUCCESS" + assert span.attributes.get("messaging.message_id") == result.task_id + + +def test_apply_async_previous_style_tasks( + celery_app, celery_worker, memory_exporter +): + """Ensures apply_async is properly patched if Celery 1.0 style tasks are + used even in newer versions. This should extend support to previous versions + of Celery.""" + + class CelerySuperClass(celery.task.Task): + abstract = True + + @classmethod + def apply_async(cls, args=None, kwargs=None, **kwargs_): + return super(CelerySuperClass, cls).apply_async( + args=args, kwargs=kwargs, **kwargs_ + ) + + def run(self, *args, **kwargs): + if "stop" in kwargs: + # avoid call loop + return + CelerySubClass.apply_async(args=[], kwargs={"stop": True}).get( + timeout=10 + ) + + class CelerySubClass(CelerySuperClass): + pass + + celery_worker.reload() + + task = CelerySubClass() + result = task.apply() + + spans = memory_exporter.get_finished_spans() + assert len(spans) == 3 + + async_span, async_run_span, run_span = spans + + assert run_span.status.is_ok is True + assert run_span.name == "test_celery_functional.CelerySubClass" + assert ( + run_span.attributes.get("celery.task_name") + == "test_celery_functional.CelerySubClass" + ) + assert run_span.attributes.get("celery.action") == "run" + assert run_span.attributes.get("celery.state") == "SUCCESS" + assert run_span.attributes.get("messaging.message_id") == result.task_id + + assert async_run_span.status.is_ok is True + assert async_run_span.name == "test_celery_functional.CelerySubClass" + assert ( + async_run_span.attributes.get("celery.task_name") + == "test_celery_functional.CelerySubClass" + ) + assert async_run_span.attributes.get("celery.action") == "run" + assert async_run_span.attributes.get("celery.state") == "SUCCESS" + assert ( + async_run_span.attributes.get("messaging.message_id") != result.task_id + ) + + assert async_span.status.is_ok is True + assert async_span.name == "test_celery_functional.CelerySubClass" + assert ( + async_span.attributes.get("celery.task_name") + == "test_celery_functional.CelerySubClass" + ) + assert async_span.attributes.get("celery.action") == "apply_async" + assert async_span.attributes.get("messaging.message_id") != result.task_id + assert async_span.attributes.get( + "messaging.message_id" + ) == async_run_span.attributes.get("messaging.message_id") + + +def test_custom_tracer_provider(celery_app, memory_exporter): + @celery_app.task + def fn_task(): + return 42 + + resource = resources.Resource.create({}) + tracer_provider = TracerProvider(resource=resource) + span_processor = export.SimpleExportSpanProcessor(memory_exporter) + tracer_provider.add_span_processor(span_processor) + + trace_api.set_tracer_provider(tracer_provider) + + CeleryInstrumentor().uninstrument() + CeleryInstrumentor().instrument(tracer_provider=tracer_provider) + + fn_task.delay() + + spans_list = memory_exporter.get_finished_spans() + assert len(spans_list) == 1 + + span = spans_list[0] + assert span.resource == resource diff --git a/tox.ini b/tox.ini index c8327d6f21..771305f2c1 100644 --- a/tox.ini +++ b/tox.ini @@ -147,6 +147,10 @@ envlist = py3{4,5,6,7,8}-test-ext-redis pypy3-test-ext-redis + ; opentelemetry-ext-celery + py3{5,6,7,8}-test-ext-celery + pypy3-test-ext-celery + ; opentelemetry-ext-system-metrics py3{4,5,6,7,8}-test-ext-system-metrics ; ext-system-metrics intentionally excluded from pypy3 @@ -224,6 +228,7 @@ changedir = test-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests test-ext-sqlalchemy: ext/opentelemetry-ext-sqlalchemy/tests test-ext-redis: ext/opentelemetry-ext-redis/tests + test-ext-celery: ext/opentelemetry-ext-celery/tests test-ext-system-metrics: ext/opentelemetry-ext-system-metrics/tests commands_pre = @@ -240,6 +245,8 @@ commands_pre = getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/ext/opentelemetry-ext-requests -e {toxinidir}/ext/opentelemetry-ext-wsgi -e {toxinidir}/ext/opentelemetry-ext-flask + celery: pip install {toxinidir}/ext/opentelemetry-ext-celery[test] + grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc[test] wsgi,flask,django,asgi,pyramid,starlette: pip install {toxinidir}/tests/util @@ -388,6 +395,7 @@ deps = psycopg2-binary ~= 2.8.4 sqlalchemy ~= 1.3.16 redis ~= 3.3.11 + celery ~= 4.0, != 4.4.4 changedir = ext/opentelemetry-ext-docker-tests/tests @@ -397,6 +405,7 @@ commands_pre = -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/tests/util \ + -e {toxinidir}/ext/opentelemetry-ext-celery \ -e {toxinidir}/ext/opentelemetry-ext-dbapi \ -e {toxinidir}/ext/opentelemetry-ext-mysql \ -e {toxinidir}/ext/opentelemetry-ext-psycopg2 \ From 9d7491845064dfb5d8f305cbb1d5cc1d479fe8fd Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Wed, 17 Jun 2020 14:53:04 -0400 Subject: [PATCH 0424/1517] add cloud trace propagator (#819) Adding initial cloud trace propagator Co-authored-by: Aaron Abbott Co-authored-by: Diego Hurtado --- .../exporter/cloud_trace/__init__.py | 22 +- .../cloud_trace/cloud_trace_propagator.py | 91 +++++++ .../tests/test_cloud_trace_propagator.py | 238 ++++++++++++++++++ .../src/opentelemetry/trace/span.py | 8 + 4 files changed, 346 insertions(+), 13 deletions(-) create mode 100644 ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/cloud_trace_propagator.py create mode 100644 ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_propagator.py diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py index 7e7aa017cf..31e20d997e 100644 --- a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py @@ -54,6 +54,10 @@ from opentelemetry.sdk.trace import Event from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult from opentelemetry.sdk.util import BoundedDict +from opentelemetry.trace.span import ( + get_hexadecimal_span_id, + get_hexadecimal_trace_id, +) from opentelemetry.util import types logger = logging.getLogger(__name__) @@ -123,15 +127,15 @@ def _translate_to_cloud_trace( for span in spans: ctx = span.get_context() - trace_id = _get_hexadecimal_trace_id(ctx.trace_id) - span_id = _get_hexadecimal_span_id(ctx.span_id) + trace_id = get_hexadecimal_trace_id(ctx.trace_id) + span_id = get_hexadecimal_span_id(ctx.span_id) span_name = "projects/{}/traces/{}/spans/{}".format( self.project_id, trace_id, span_id ) parent_id = None if span.parent: - parent_id = _get_hexadecimal_span_id(span.parent.span_id) + parent_id = get_hexadecimal_span_id(span.parent.span_id) start_time = _get_time_from_ns(span.start_time) end_time = _get_time_from_ns(span.end_time) @@ -169,14 +173,6 @@ def shutdown(self): pass -def _get_hexadecimal_trace_id(trace_id: int) -> str: - return "{:032x}".format(trace_id) - - -def _get_hexadecimal_span_id(span_id: int) -> str: - return "{:016x}".format(span_id) - - def _get_time_from_ns(nanoseconds: int) -> Dict: """Given epoch nanoseconds, split into epoch milliseconds and remaining nanoseconds""" @@ -234,8 +230,8 @@ def _extract_links(links: Sequence[trace_api.Link]) -> ProtoSpan.Links: "Link has more then %s attributes, some will be truncated", MAX_LINK_ATTRS, ) - trace_id = _get_hexadecimal_trace_id(link.context.trace_id) - span_id = _get_hexadecimal_span_id(link.context.span_id) + trace_id = get_hexadecimal_trace_id(link.context.trace_id) + span_id = get_hexadecimal_span_id(link.context.span_id) extracted_links.append( { "trace_id": trace_id, diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/cloud_trace_propagator.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/cloud_trace_propagator.py new file mode 100644 index 0000000000..124863e39e --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/cloud_trace_propagator.py @@ -0,0 +1,91 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import re +import typing + +import opentelemetry.trace as trace +from opentelemetry.context.context import Context +from opentelemetry.trace.propagation import httptextformat +from opentelemetry.trace.span import ( + SpanContext, + TraceFlags, + get_hexadecimal_trace_id, +) + +_TRACE_CONTEXT_HEADER_NAME = "X-Cloud-Trace-Context" +_TRACE_CONTEXT_HEADER_FORMAT = r"(?P[0-9a-f]{32})\/(?P[\d]{1,20});o=(?P\d+)" +_TRACE_CONTEXT_HEADER_RE = re.compile(_TRACE_CONTEXT_HEADER_FORMAT) + + +class CloudTraceFormatPropagator(httptextformat.HTTPTextFormat): + """This class is for injecting into a carrier the SpanContext in Google + Cloud format, or extracting the SpanContext from a carrier using Google + Cloud format. + """ + + def extract( + self, + get_from_carrier: httptextformat.Getter[ + httptextformat.HTTPTextFormatT + ], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + header = get_from_carrier(carrier, _TRACE_CONTEXT_HEADER_NAME) + + if not header: + return trace.set_span_in_context(trace.INVALID_SPAN, context) + + match = re.fullmatch(_TRACE_CONTEXT_HEADER_RE, header[0]) + if match is None: + return trace.set_span_in_context(trace.INVALID_SPAN, context) + + trace_id = match.group("trace_id") + span_id = match.group("span_id") + trace_options = match.group("trace_flags") + + if trace_id == "0" * 32 or int(span_id) == 0: + return trace.set_span_in_context(trace.INVALID_SPAN, context) + + span_context = SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id), + is_remote=True, + trace_flags=TraceFlags(trace_options), + ) + return trace.set_span_in_context( + trace.DefaultSpan(span_context), context + ) + + def inject( + self, + set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> None: + span = trace.get_current_span(context) + if span is None: + return + span_context = span.get_context() + if span_context == trace.INVALID_SPAN_CONTEXT: + return + + header = "{}/{};o={}".format( + get_hexadecimal_trace_id(span_context.trace_id), + span_context.span_id, + int(span_context.trace_flags.sampled), + ) + set_in_carrier(carrier, _TRACE_CONTEXT_HEADER_NAME, header) diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_propagator.py b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_propagator.py new file mode 100644 index 0000000000..a94127b7b7 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_propagator.py @@ -0,0 +1,238 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import typing +import unittest + +import opentelemetry.trace as trace +from opentelemetry.context import get_current +from opentelemetry.exporter.cloud_trace.cloud_trace_propagator import ( + _TRACE_CONTEXT_HEADER_NAME, + CloudTraceFormatPropagator, +) +from opentelemetry.trace.span import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, + SpanContext, + TraceFlags, + get_hexadecimal_trace_id, +) + + +def get_dict_value(dict_object: typing.Dict[str, str], key: str) -> str: + return dict_object.get(key, "") + + +class TestCloudTraceFormatPropagator(unittest.TestCase): + def setUp(self): + self.propagator = CloudTraceFormatPropagator() + self.valid_trace_id = 281017822499060589596062859815111849546 + self.valid_span_id = 17725314949316355921 + self.too_long_id = 111111111111111111111111111111111111111111111 + + def _extract(self, header_value): + """Test helper""" + header = {_TRACE_CONTEXT_HEADER_NAME: [header_value]} + new_context = self.propagator.extract(get_dict_value, header) + return trace.get_current_span(new_context).get_context() + + def _inject(self, span=None): + """Test helper""" + ctx = get_current() + if span is not None: + ctx = trace.set_span_in_context(span, ctx) + output = {} + self.propagator.inject(dict.__setitem__, output, context=ctx) + return output.get(_TRACE_CONTEXT_HEADER_NAME) + + def test_no_context_header(self): + header = {} + new_context = self.propagator.extract(get_dict_value, header) + self.assertEqual( + trace.get_current_span(new_context).get_context(), + trace.INVALID_SPAN.get_context(), + ) + + def test_empty_context_header(self): + header = "" + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + def test_valid_header(self): + header = "{}/{};o=1".format( + get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id + ) + new_span_context = self._extract(header) + self.assertEqual(new_span_context.trace_id, self.valid_trace_id) + self.assertEqual(new_span_context.span_id, self.valid_span_id) + self.assertEqual(new_span_context.trace_flags, TraceFlags(1)) + self.assertTrue(new_span_context.is_remote) + + header = "{}/{};o=10".format( + get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id + ) + new_span_context = self._extract(header) + self.assertEqual(new_span_context.trace_id, self.valid_trace_id) + self.assertEqual(new_span_context.span_id, self.valid_span_id) + self.assertEqual(new_span_context.trace_flags, TraceFlags(10)) + self.assertTrue(new_span_context.is_remote) + + header = "{}/{};o=0".format( + get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id + ) + new_span_context = self._extract(header) + self.assertEqual(new_span_context.trace_id, self.valid_trace_id) + self.assertEqual(new_span_context.span_id, self.valid_span_id) + self.assertEqual(new_span_context.trace_flags, TraceFlags(0)) + self.assertTrue(new_span_context.is_remote) + + header = "{}/{};o=0".format( + get_hexadecimal_trace_id(self.valid_trace_id), 345 + ) + new_span_context = self._extract(header) + self.assertEqual(new_span_context.trace_id, self.valid_trace_id) + self.assertEqual(new_span_context.span_id, 345) + self.assertEqual(new_span_context.trace_flags, TraceFlags(0)) + self.assertTrue(new_span_context.is_remote) + + def test_invalid_header_format(self): + header = "invalid_header" + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + header = "{}/{};o=".format( + get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id + ) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + header = "extra_chars/{}/{};o=1".format( + get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id + ) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + header = "{}/{}extra_chars;o=1".format( + get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id + ) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + header = "{}/{};o=1extra_chars".format( + get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id + ) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + header = "{}/;o=1".format( + get_hexadecimal_trace_id(self.valid_trace_id) + ) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + header = "/{};o=1".format(self.valid_span_id) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + header = "{}/{};o={}".format("123", "34", "4") + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + def test_invalid_trace_id(self): + header = "{}/{};o={}".format(INVALID_TRACE_ID, self.valid_span_id, 1) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + header = "{}/{};o={}".format("0" * 32, self.valid_span_id, 1) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + header = "0/{};o={}".format(self.valid_span_id, 1) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + header = "234/{};o={}".format(self.valid_span_id, 1) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + header = "{}/{};o={}".format(self.too_long_id, self.valid_span_id, 1) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + def test_invalid_span_id(self): + header = "{}/{};o={}".format( + get_hexadecimal_trace_id(self.valid_trace_id), INVALID_SPAN_ID, 1 + ) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + header = "{}/{};o={}".format( + get_hexadecimal_trace_id(self.valid_trace_id), "0" * 16, 1 + ) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + header = "{}/{};o={}".format( + get_hexadecimal_trace_id(self.valid_trace_id), "0", 1 + ) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + header = "{}/{};o={}".format( + get_hexadecimal_trace_id(self.valid_trace_id), self.too_long_id, 1 + ) + self.assertEqual( + self._extract(header), trace.INVALID_SPAN.get_context() + ) + + def test_inject_with_no_context(self): + output = self._inject() + self.assertIsNone(output) + + def test_inject_with_invalid_context(self): + output = self._inject(trace.INVALID_SPAN) + self.assertIsNone(output) + + def test_inject_with_valid_context(self): + span_context = SpanContext( + trace_id=self.valid_trace_id, + span_id=self.valid_span_id, + is_remote=True, + trace_flags=TraceFlags(1), + ) + output = self._inject(trace.DefaultSpan(span_context)) + self.assertEqual( + output, + "{}/{};o={}".format( + get_hexadecimal_trace_id(self.valid_trace_id), + self.valid_span_id, + 1, + ), + ) diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index b20979397d..baea6670f6 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -279,3 +279,11 @@ def format_trace_id(trace_id: int) -> str: def format_span_id(span_id: int) -> str: return "0x{:016x}".format(span_id) + + +def get_hexadecimal_trace_id(trace_id: int) -> str: + return "{:032x}".format(trace_id) + + +def get_hexadecimal_span_id(span_id: int) -> str: + return "{:016x}".format(span_id) From a7f0b75354ba327e28935bd370f5b91ceee26fb4 Mon Sep 17 00:00:00 2001 From: HiveTraum Date: Thu, 18 Jun 2020 03:27:13 +0500 Subject: [PATCH 0425/1517] Instrumentation for asyncpg (#814) Co-authored-by: Yusuke Tsutsumi --- docs-requirements.txt | 1 + docs/ext/asyncpg/asyncpg.rst | 10 + ext/opentelemetry-ext-asyncpg/CHANGELOG.md | 5 + ext/opentelemetry-ext-asyncpg/README.rst | 23 ++ ext/opentelemetry-ext-asyncpg/setup.cfg | 55 ++++ ext/opentelemetry-ext-asyncpg/setup.py | 26 ++ .../src/opentelemetry/ext/asyncpg/__init__.py | 136 ++++++++++ .../src/opentelemetry/ext/asyncpg/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/test_asyncpg_wrapper.py | 35 +++ .../tests/asyncpg/test_asyncpg_functional.py | 240 ++++++++++++++++++ tox.ini | 9 + 12 files changed, 555 insertions(+) create mode 100644 docs/ext/asyncpg/asyncpg.rst create mode 100644 ext/opentelemetry-ext-asyncpg/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-asyncpg/README.rst create mode 100644 ext/opentelemetry-ext-asyncpg/setup.cfg create mode 100644 ext/opentelemetry-ext-asyncpg/setup.py create mode 100644 ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/__init__.py create mode 100644 ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py create mode 100644 ext/opentelemetry-ext-asyncpg/tests/__init__.py create mode 100644 ext/opentelemetry-ext-asyncpg/tests/test_asyncpg_wrapper.py create mode 100644 ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py diff --git a/docs-requirements.txt b/docs-requirements.txt index 84f80b30e5..230c76149c 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -4,6 +4,7 @@ sphinx-autodoc-typehints~=1.10.2 # Required by ext packages asgiref~=3.0 +asyncpg>=0.12.0 ddtrace>=0.34.0 aiohttp~= 3.0 Deprecated>=1.2.6 diff --git a/docs/ext/asyncpg/asyncpg.rst b/docs/ext/asyncpg/asyncpg.rst new file mode 100644 index 0000000000..3a4a9b3c4e --- /dev/null +++ b/docs/ext/asyncpg/asyncpg.rst @@ -0,0 +1,10 @@ +opentelemetry.ext.asyncpg package +================================= + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.asyncpg + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-asyncpg/CHANGELOG.md b/ext/opentelemetry-ext-asyncpg/CHANGELOG.md new file mode 100644 index 0000000000..f4390b09ab --- /dev/null +++ b/ext/opentelemetry-ext-asyncpg/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial Release ([#814](https://github.com/open-telemetry/opentelemetry-python/pull/814)) diff --git a/ext/opentelemetry-ext-asyncpg/README.rst b/ext/opentelemetry-ext-asyncpg/README.rst new file mode 100644 index 0000000000..f852bfdbb2 --- /dev/null +++ b/ext/opentelemetry-ext-asyncpg/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry asyncpg Integration +================================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-asyncpg.svg + :target: https://pypi.org/project/opentelemetry-ext-asyncpg/ + +This library allows tracing PostgreSQL queries made by the +`asyncpg `_ library. + +Installation +------------ + +:: + + pip install opentelemetry-ext-asyncpg + +References +---------- + +* `OpenTelemetry asyncpg Integration `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-asyncpg/setup.cfg b/ext/opentelemetry-ext-asyncpg/setup.cfg new file mode 100644 index 0000000000..df00ed7db4 --- /dev/null +++ b/ext/opentelemetry-ext-asyncpg/setup.cfg @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-asyncpg +description = OpenTelemetry instrumentation for AsyncPG +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-asyncpg +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.10.dev0 + opentelemetry-instrumentation == 0.10.dev0 + asyncpg >= 0.12.0 + +[options.extras_require] +test = + opentelemetry-test == 0.10.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + asyncpg = opentelemetry.ext.asyncpg:AsyncPGInstrumentor diff --git a/ext/opentelemetry-ext-asyncpg/setup.py b/ext/opentelemetry-ext-asyncpg/setup.py new file mode 100644 index 0000000000..8172205c1a --- /dev/null +++ b/ext/opentelemetry-ext-asyncpg/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "asyncpg", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/__init__.py b/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/__init__.py new file mode 100644 index 0000000000..c373d7194d --- /dev/null +++ b/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/__init__.py @@ -0,0 +1,136 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This library allows tracing PostgreSQL queries made by the +`asyncpg `_ library. + +Usage +----- + +.. code-block:: python + + import asyncpg + from opentelemetry.ext.asyncpg import AsyncPGInstrumentor + + # You can optionally pass a custom TracerProvider to AsyncPGInstrumentor.instrument() + AsyncPGInstrumentor().instrument() + conn = await asyncpg.connect(user='user', password='password', + database='database', host='127.0.0.1') + values = await conn.fetch('''SELECT 42;''') + +API +--- +""" + +import asyncpg +import wrapt +from asyncpg import exceptions + +from opentelemetry import trace +from opentelemetry.ext.asyncpg.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.trace import SpanKind +from opentelemetry.trace.status import Status, StatusCanonicalCode + +_APPLIED = "_opentelemetry_tracer" + + +def _exception_to_canonical_code(exc: Exception) -> StatusCanonicalCode: + if isinstance( + exc, (exceptions.InterfaceError, exceptions.SyntaxOrAccessError), + ): + return StatusCanonicalCode.INVALID_ARGUMENT + if isinstance(exc, exceptions.IdleInTransactionSessionTimeoutError): + return StatusCanonicalCode.DEADLINE_EXCEEDED + return StatusCanonicalCode.UNKNOWN + + +def _hydrate_span_from_args(connection, query, parameters) -> dict: + span_attributes = {"db.type": "sql"} + + params = getattr(connection, "_params", None) + span_attributes["db.instance"] = getattr(params, "database", None) + span_attributes["db.user"] = getattr(params, "user", None) + + if query is not None: + span_attributes["db.statement"] = query + + if parameters is not None and len(parameters) > 0: + span_attributes["db.statement.parameters"] = str(parameters) + + return span_attributes + + +async def _do_execute(func, instance, args, kwargs): + span_attributes = _hydrate_span_from_args(instance, args[0], args[1:]) + tracer = getattr(asyncpg, _APPLIED) + + exception = None + + with tracer.start_as_current_span( + "postgresql", kind=SpanKind.CLIENT + ) as span: + + for attribute, value in span_attributes.items(): + span.set_attribute(attribute, value) + + try: + result = await func(*args, **kwargs) + except Exception as exc: # pylint: disable=W0703 + exception = exc + raise + finally: + if exception is not None: + span.set_status( + Status(_exception_to_canonical_code(exception)) + ) + else: + span.set_status(Status(StatusCanonicalCode.OK)) + + return result + + +class AsyncPGInstrumentor(BaseInstrumentor): + def _instrument(self, **kwargs): + tracer_provider = kwargs.get( + "tracer_provider", trace.get_tracer_provider() + ) + setattr( + asyncpg, + _APPLIED, + tracer_provider.get_tracer("asyncpg", __version__), + ) + for method in [ + "Connection.execute", + "Connection.executemany", + "Connection.fetch", + "Connection.fetchval", + "Connection.fetchrow", + ]: + wrapt.wrap_function_wrapper( + "asyncpg.connection", method, _do_execute + ) + + def _uninstrument(self, **__): + delattr(asyncpg, _APPLIED) + for method in [ + "execute", + "executemany", + "fetch", + "fetchval", + "fetchrow", + ]: + unwrap(asyncpg.Connection, method) diff --git a/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py b/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py new file mode 100644 index 0000000000..6d4fefa599 --- /dev/null +++ b/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.10.dev0" diff --git a/ext/opentelemetry-ext-asyncpg/tests/__init__.py b/ext/opentelemetry-ext-asyncpg/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-asyncpg/tests/test_asyncpg_wrapper.py b/ext/opentelemetry-ext-asyncpg/tests/test_asyncpg_wrapper.py new file mode 100644 index 0000000000..cd0d8e35f0 --- /dev/null +++ b/ext/opentelemetry-ext-asyncpg/tests/test_asyncpg_wrapper.py @@ -0,0 +1,35 @@ +import asyncpg +from asyncpg import Connection + +from opentelemetry.ext.asyncpg import AsyncPGInstrumentor +from opentelemetry.test.test_base import TestBase + + +class TestAsyncPGInstrumentation(TestBase): + def test_instrumentation_flags(self): + AsyncPGInstrumentor().instrument() + self.assertTrue(hasattr(asyncpg, "_opentelemetry_tracer")) + AsyncPGInstrumentor().uninstrument() + self.assertFalse(hasattr(asyncpg, "_opentelemetry_tracer")) + + def test_duplicated_instrumentation(self): + AsyncPGInstrumentor().instrument() + AsyncPGInstrumentor().instrument() + AsyncPGInstrumentor().instrument() + AsyncPGInstrumentor().uninstrument() + for method_name in ["execute", "fetch"]: + method = getattr(Connection, method_name, None) + self.assertFalse( + hasattr(method, "_opentelemetry_ext_asyncpg_applied") + ) + + def test_duplicated_uninstrumentation(self): + AsyncPGInstrumentor().instrument() + AsyncPGInstrumentor().uninstrument() + AsyncPGInstrumentor().uninstrument() + AsyncPGInstrumentor().uninstrument() + for method_name in ["execute", "fetch"]: + method = getattr(Connection, method_name, None) + self.assertFalse( + hasattr(method, "_opentelemetry_ext_asyncpg_applied") + ) diff --git a/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py b/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py new file mode 100644 index 0000000000..408e50feb8 --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py @@ -0,0 +1,240 @@ +import asyncio +import os + +import asyncpg +import pytest + +from opentelemetry.ext.asyncpg import AsyncPGInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace.status import StatusCanonicalCode + +POSTGRES_HOST = os.getenv("POSTGRESQL_HOST ", "localhost") +POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT ", "5432")) +POSTGRES_DB_NAME = os.getenv("POSTGRESQL_DB_NAME ", "opentelemetry-tests") +POSTGRES_PASSWORD = os.getenv("POSTGRESQL_HOST ", "testpassword") +POSTGRES_USER = os.getenv("POSTGRESQL_HOST ", "testuser") + + +def _await(coro): + loop = asyncio.get_event_loop() + return loop.run_until_complete(coro) + + +class TestFunctionalPsycopg(TestBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._connection = None + cls._cursor = None + cls._tracer = cls.tracer_provider.get_tracer(__name__) + AsyncPGInstrumentor().instrument(tracer_provider=cls.tracer_provider) + cls._connection = _await( + asyncpg.connect( + database=POSTGRES_DB_NAME, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD, + host=POSTGRES_HOST, + port=POSTGRES_PORT, + ) + ) + + @classmethod + def tearDownClass(cls): + AsyncPGInstrumentor().uninstrument() + + @pytest.mark.asyncpg + def test_instrumented_execute_method_without_arguments(self, *_, **__): + _await(self._connection.execute("SELECT 42;")) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual( + StatusCanonicalCode.OK, spans[0].status.canonical_code + ) + self.assertEqual( + spans[0].attributes, + { + "db.type": "sql", + "db.user": POSTGRES_USER, + "db.instance": POSTGRES_DB_NAME, + "db.statement": "SELECT 42;", + }, + ) + + @pytest.mark.asyncpg + def test_instrumented_execute_method_with_arguments(self, *_, **__): + _await(self._connection.execute("SELECT $1;", "1")) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual( + StatusCanonicalCode.OK, spans[0].status.canonical_code + ) + self.assertEqual( + spans[0].attributes, + { + "db.type": "sql", + "db.user": POSTGRES_USER, + "db.statement.parameters": "('1',)", + "db.instance": POSTGRES_DB_NAME, + "db.statement": "SELECT $1;", + }, + ) + + @pytest.mark.asyncpg + def test_instrumented_fetch_method_without_arguments(self, *_, **__): + _await(self._connection.fetch("SELECT 42;")) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual( + spans[0].attributes, + { + "db.type": "sql", + "db.user": POSTGRES_USER, + "db.instance": POSTGRES_DB_NAME, + "db.statement": "SELECT 42;", + }, + ) + + @pytest.mark.asyncpg + def test_instrumented_fetch_method_with_arguments(self, *_, **__): + _await(self._connection.fetch("SELECT $1;", "1")) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual( + spans[0].attributes, + { + "db.type": "sql", + "db.user": POSTGRES_USER, + "db.statement.parameters": "('1',)", + "db.instance": POSTGRES_DB_NAME, + "db.statement": "SELECT $1;", + }, + ) + + @pytest.mark.asyncpg + def test_instrumented_executemany_method_with_arguments(self, *_, **__): + _await(self._connection.executemany("SELECT $1;", [["1"], ["2"]])) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual( + { + "db.type": "sql", + "db.statement": "SELECT $1;", + "db.statement.parameters": "([['1'], ['2']],)", + "db.user": POSTGRES_USER, + "db.instance": POSTGRES_DB_NAME, + }, + spans[0].attributes, + ) + + @pytest.mark.asyncpg + def test_instrumented_execute_interface_error_method(self, *_, **__): + with self.assertRaises(asyncpg.InterfaceError): + _await(self._connection.execute("SELECT 42;", 1, 2, 3)) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual( + spans[0].attributes, + { + "db.type": "sql", + "db.instance": POSTGRES_DB_NAME, + "db.user": POSTGRES_USER, + "db.statement.parameters": "(1, 2, 3)", + "db.statement": "SELECT 42;", + }, + ) + + @pytest.mark.asyncpg + def test_instrumented_transaction_method(self, *_, **__): + async def _transaction_execute(): + async with self._connection.transaction(): + await self._connection.execute("SELECT 42;") + + _await(_transaction_execute()) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(3, len(spans)) + self.assertEqual( + { + "db.instance": POSTGRES_DB_NAME, + "db.user": POSTGRES_USER, + "db.type": "sql", + "db.statement": "BEGIN;", + }, + spans[0].attributes, + ) + self.assertEqual( + StatusCanonicalCode.OK, spans[0].status.canonical_code + ) + self.assertEqual( + { + "db.instance": POSTGRES_DB_NAME, + "db.user": POSTGRES_USER, + "db.type": "sql", + "db.statement": "SELECT 42;", + }, + spans[1].attributes, + ) + self.assertEqual( + StatusCanonicalCode.OK, spans[1].status.canonical_code + ) + self.assertEqual( + { + "db.instance": POSTGRES_DB_NAME, + "db.user": POSTGRES_USER, + "db.type": "sql", + "db.statement": "COMMIT;", + }, + spans[2].attributes, + ) + self.assertEqual( + StatusCanonicalCode.OK, spans[2].status.canonical_code + ) + + @pytest.mark.asyncpg + def test_instrumented_failed_transaction_method(self, *_, **__): + async def _transaction_execute(): + async with self._connection.transaction(): + await self._connection.execute("SELECT 42::uuid;") + + with self.assertRaises(asyncpg.CannotCoerceError): + _await(_transaction_execute()) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(3, len(spans)) + self.assertEqual( + { + "db.instance": POSTGRES_DB_NAME, + "db.user": POSTGRES_USER, + "db.type": "sql", + "db.statement": "BEGIN;", + }, + spans[0].attributes, + ) + self.assertEqual( + StatusCanonicalCode.OK, spans[0].status.canonical_code + ) + self.assertEqual( + { + "db.instance": POSTGRES_DB_NAME, + "db.user": POSTGRES_USER, + "db.type": "sql", + "db.statement": "SELECT 42::uuid;", + }, + spans[1].attributes, + ) + self.assertEqual( + StatusCanonicalCode.INVALID_ARGUMENT, + spans[1].status.canonical_code, + ) + self.assertEqual( + { + "db.instance": POSTGRES_DB_NAME, + "db.user": POSTGRES_USER, + "db.type": "sql", + "db.statement": "ROLLBACK;", + }, + spans[2].attributes, + ) + self.assertEqual( + StatusCanonicalCode.OK, spans[2].status.canonical_code + ) diff --git a/tox.ini b/tox.ini index 771305f2c1..e4ad8e1709 100644 --- a/tox.ini +++ b/tox.ini @@ -116,6 +116,10 @@ envlist = py3{5,6,7,8}-test-ext-asgi pypy3-test-ext-asgi + ; opentelemetry-ext-asyncpg + py3{5,6,7,8}-test-ext-asyncpg + ; ext-asyncpg intentionally excluded from pypy3 + ; opentelemetry-ext-sqlite3 py3{4,5,6,7,8}-test-ext-sqlite3 pypy3-test-ext-sqlite3 @@ -217,6 +221,7 @@ changedir = test-ext-pymysql: ext/opentelemetry-ext-pymysql/tests test-ext-pyramid: ext/opentelemetry-ext-pyramid/tests test-ext-asgi: ext/opentelemetry-ext-asgi/tests + test-ext-asyncpg: ext/opentelemetry-ext-asyncpg/tests test-ext-sqlite3: ext/opentelemetry-ext-sqlite3/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests @@ -253,6 +258,8 @@ commands_pre = wsgi,flask,django,pyramid: pip install {toxinidir}/ext/opentelemetry-ext-wsgi asgi,starlette: pip install {toxinidir}/ext/opentelemetry-ext-asgi + asyncpg: pip install {toxinidir}/ext/opentelemetry-ext-asyncpg + boto: pip install {toxinidir}/ext/opentelemetry-ext-boto[test] flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] @@ -388,6 +395,7 @@ commands = [testenv:docker-tests] deps = pytest + asyncpg==0.20.1 docker-compose >= 1.25.2 mysql-connector-python ~= 8.0 pymongo ~= 3.1 @@ -405,6 +413,7 @@ commands_pre = -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/tests/util \ + -e {toxinidir}/ext/opentelemetry-ext-asyncpg \ -e {toxinidir}/ext/opentelemetry-ext-celery \ -e {toxinidir}/ext/opentelemetry-ext-dbapi \ -e {toxinidir}/ext/opentelemetry-ext-mysql \ From 99cb04687dbc606d8226a6e0a5b1ae7dde9a2174 Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Thu, 18 Jun 2020 14:31:16 -0400 Subject: [PATCH 0426/1517] ext/datadog: add environment variable configuration (#836) --- .../opentelemetry/ext/datadog/constants.py | 2 + .../src/opentelemetry/ext/datadog/exporter.py | 59 ++++++++++++++++-- .../tests/test_datadog_exporter.py | 61 +++++++++++++++++-- 3 files changed, 112 insertions(+), 10 deletions(-) diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py index d2864c1076..92d736c918 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py @@ -4,3 +4,5 @@ USER_KEEP = 2 SAMPLE_RATE_METRIC_KEY = "_sample_rate" SAMPLING_PRIORITY_KEY = "_sampling_priority_v1" +ENV_KEY = "env" +VERSION_KEY = "version" diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py index a1788b74a8..e11772d0d9 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py @@ -25,7 +25,7 @@ from opentelemetry.trace.status import StatusCanonicalCode # pylint:disable=relative-beyond-top-level -from .constants import DD_ORIGIN, SAMPLE_RATE_METRIC_KEY +from .constants import DD_ORIGIN, ENV_KEY, SAMPLE_RATE_METRIC_KEY, VERSION_KEY logger = logging.getLogger(__name__) @@ -56,16 +56,24 @@ class DatadogSpanExporter(SpanExporter): Args: agent_url: The url of the Datadog Agent or use ``DD_TRACE_AGENT_URL`` environment variable - service: The service to be used for the application or use ``DD_SERVICE`` environment variable + service: The service name to be used for the application or use ``DD_SERVICE`` environment variable + env: Set the application’s environment or use ``DD_ENV`` environment variable + version: Set the application’s version or use ``DD_VERSION`` environment variable + tags: A list of default tags to be added to every span or use ``DD_TAGS`` environment variable """ - def __init__(self, agent_url=None, service=None): + def __init__( + self, agent_url=None, service=None, env=None, version=None, tags=None + ): self.agent_url = ( agent_url if agent_url else os.environ.get("DD_TRACE_AGENT_URL", DEFAULT_AGENT_URL) ) - self.service = service if service else os.environ.get("DD_SERVICE") + self.service = service or os.environ.get("DD_SERVICE") + self.env = env or os.environ.get("DD_ENV") + self.version = version or os.environ.get("DD_VERSION") + self.tags = _parse_tags_str(tags or os.environ.get("DD_TAGS")) self._agent_writer = None @property @@ -133,6 +141,17 @@ def _translate_to_datadog(self, spans): datadog_span.set_tags(span.attributes) + # add configured env tag + if self.env is not None: + datadog_span.set_tag(ENV_KEY, self.env) + + # add configured application version tag to only root span + if self.version is not None and parent_id == 0: + datadog_span.set_tag(VERSION_KEY, self.version) + + # add configured global tags + datadog_span.set_tags(self.tags) + # add origin to root span origin = _get_origin(span) if origin and parent_id == 0: @@ -230,3 +249,35 @@ def _get_sampling_rate(span): and isinstance(span.sampler, trace_api.sampling.ProbabilitySampler) else None ) + + +def _parse_tags_str(tags_str): + """Parse a string of tags typically provided via environment variables. + + The expected string is of the form:: + "key1:value1,key2:value2" + + :param tags_str: A string of the above form to parse tags from. + :return: A dict containing the tags that were parsed. + """ + parsed_tags = {} + if not tags_str: + return parsed_tags + + for tag in tags_str.split(","): + try: + key, value = tag.split(":", 1) + + # Validate the tag + if key == "" or value == "" or value.endswith(":"): + raise ValueError + except ValueError: + logger.error( + "Malformed tag in tag pair '%s' from tag string '%s'.", + tag, + tags_str, + ) + else: + parsed_tags[key] = value + + return parsed_tags diff --git a/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py b/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py index d99f9db2c2..5306d517b7 100644 --- a/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py +++ b/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py @@ -72,26 +72,73 @@ def test_constructor_explicit(self): """Test the constructor passing all the options.""" agent_url = "http://localhost:8126" exporter = datadog.DatadogSpanExporter( - agent_url=agent_url, service="explicit" + agent_url=agent_url, service="explicit", ) self.assertEqual(exporter.agent_url, agent_url) self.assertEqual(exporter.service, "explicit") - self.assertIsNotNone(exporter.agent_writer) + self.assertIsNone(exporter.env) + self.assertIsNone(exporter.version) + self.assertEqual(exporter.tags, {}) + + exporter = datadog.DatadogSpanExporter( + agent_url=agent_url, + service="explicit", + env="test", + version="0.0.1", + tags="", + ) + + self.assertEqual(exporter.agent_url, agent_url) + self.assertEqual(exporter.service, "explicit") + self.assertEqual(exporter.env, "test") + self.assertEqual(exporter.version, "0.0.1") + self.assertEqual(exporter.tags, {}) + + exporter = datadog.DatadogSpanExporter( + agent_url=agent_url, + service="explicit", + env="test", + version="0.0.1", + tags="team:testers,layer:app", + ) + + self.assertEqual(exporter.agent_url, agent_url) + self.assertEqual(exporter.service, "explicit") + self.assertEqual(exporter.env, "test") + self.assertEqual(exporter.version, "0.0.1") + self.assertEqual(exporter.tags, {"team": "testers", "layer": "app"}) @mock.patch.dict( "os.environ", - {"DD_TRACE_AGENT_URL": "http://agent:8126", "DD_SERVICE": "environ"}, + { + "DD_TRACE_AGENT_URL": "http://agent:8126", + "DD_SERVICE": "test-service", + "DD_ENV": "test", + "DD_VERSION": "0.0.1", + "DD_TAGS": "team:testers", + }, ) def test_constructor_environ(self): exporter = datadog.DatadogSpanExporter() self.assertEqual(exporter.agent_url, "http://agent:8126") - self.assertEqual(exporter.service, "environ") + self.assertEqual(exporter.service, "test-service") + self.assertEqual(exporter.env, "test") + self.assertEqual(exporter.version, "0.0.1") + self.assertEqual(exporter.tags, {"team": "testers"}) self.assertIsNotNone(exporter.agent_writer) # pylint: disable=too-many-locals - @mock.patch.dict("os.environ", {"DD_SERVICE": "test-service"}) + @mock.patch.dict( + "os.environ", + { + "DD_SERVICE": "test-service", + "DD_ENV": "test", + "DD_VERSION": "0.0.1", + "DD_TAGS": "team:testers", + }, + ) def test_translate_to_datadog(self): # pylint: disable=invalid-name self.maxDiff = None @@ -174,6 +221,7 @@ def test_translate_to_datadog(self): duration=durations[0], error=0, service="test-service", + meta={"env": "test", "team": "testers"}, ), dict( trace_id=trace_id_low, @@ -185,6 +233,7 @@ def test_translate_to_datadog(self): duration=durations[1], error=0, service="test-service", + meta={"env": "test", "team": "testers", "version": "0.0.1"}, ), dict( trace_id=trace_id_low, @@ -196,12 +245,12 @@ def test_translate_to_datadog(self): duration=durations[2], error=0, service="test-service", + meta={"env": "test", "team": "testers", "version": "0.0.1"}, ), ] self.assertEqual(datadog_spans, expected_spans) - @mock.patch.dict("os.environ", {"DD_SERVICE": "test-service"}) def test_export(self): """Test that agent and/or collector are invoked""" # create and save span to be used in tests From 1238fc8a171775e95571080fc878233ffd070baa Mon Sep 17 00:00:00 2001 From: HiveTraum Date: Mon, 22 Jun 2020 20:25:58 +0500 Subject: [PATCH 0427/1517] OTLP Exporter documentation example misleading (#838) --- .../src/opentelemetry/ext/otlp/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/__init__.py b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/__init__.py index 55c5dd69ef..1b315c5847 100644 --- a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/__init__.py +++ b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/__init__.py @@ -30,10 +30,17 @@ from opentelemetry import trace from opentelemetry.ext.otlp.trace_exporter import OTLPSpanExporter + from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - trace.set_tracer_provider(TracerProvider()) + # Resource can be required for some backends, e.g. Jaeger + # If resource wouldn't be set - traces wouldn't appears in Jaeger + resource = Resource(labels=labels={ + "service.name": "service" + }) + + trace.set_tracer_provider(TracerProvider(resource=resource))) tracer = trace.get_tracer(__name__) otlp_exporter = OTLPSpanExporter(endpoint="localhost:55678") From 9e20f749258198ff8044d70094ea15f3f86abc49 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 22 Jun 2020 17:47:07 +0000 Subject: [PATCH 0428/1517] Script to regenerate proto code + pyi stubs (#823) --- .flake8 | 1 + .gitattributes | 2 + dev-requirements.txt | 3 + opentelemetry-proto/CHANGELOG.md | 3 + opentelemetry-proto/MANIFEST.in | 2 +- .../metrics/v1/metrics_service_pb2.pyi | 72 ++++ .../collector/trace/v1/trace_service_pb2.pyi | 72 ++++ .../proto/common/v1/common_pb2.py | 37 +- .../proto/common/v1/common_pb2.pyi | 127 ++++++ .../proto/metrics/v1/metrics_pb2.py | 177 ++++---- .../proto/metrics/v1/metrics_pb2.pyi | 394 ++++++++++++++++++ .../proto/resource/v1/resource_pb2.py | 6 +- .../proto/resource/v1/resource_pb2.pyi | 61 +++ .../proto/trace/v1/trace_config_pb2.py | 41 +- .../proto/trace/v1/trace_config_pb2.pyi | 149 +++++++ .../opentelemetry/proto/trace/v1/trace_pb2.py | 138 +++--- .../proto/trace/v1/trace_pb2.pyi | 303 ++++++++++++++ scripts/proto_codegen.sh | 64 +++ 18 files changed, 1417 insertions(+), 235 deletions(-) create mode 100644 .gitattributes create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi create mode 100644 opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi create mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi create mode 100644 opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi create mode 100644 opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi create mode 100644 opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi create mode 100755 scripts/proto_codegen.sh diff --git a/.flake8 b/.flake8 index 39288e9b1b..8555d626c6 100644 --- a/.flake8 +++ b/.flake8 @@ -20,4 +20,5 @@ exclude = ext/opentelemetry-ext-jaeger/build/* docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ docs/examples/opentelemetry-example-app/build/* + opentelemetry-proto/build/* opentelemetry-proto/src/opentelemetry/proto/ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..6279d7a9cb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# tells github that proto code is generated +opentelemetry-proto/src/**/*_pb2*.py* linguist-generated=true diff --git a/dev-requirements.txt b/dev-requirements.txt index 3808b02143..dd4d2e37c9 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -9,3 +9,6 @@ sphinx-autodoc-typehints~=1.10.2 pytest!=5.2.3 pytest-cov>=2.8 readme-renderer~=24.0 +grpcio-tools==1.29.0 +mypy-protobuf==1.21 +protobuf==3.12.2 diff --git a/opentelemetry-proto/CHANGELOG.md b/opentelemetry-proto/CHANGELOG.md index 896a782491..aac3fa1d9f 100644 --- a/opentelemetry-proto/CHANGELOG.md +++ b/opentelemetry-proto/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Regenerate proto code and add pyi stubs + ([#823](https://github.com/open-telemetry/opentelemetry-python/pull/823)) + ## 0.9b0 Released 2020-06-10 diff --git a/opentelemetry-proto/MANIFEST.in b/opentelemetry-proto/MANIFEST.in index 5da7b7fa51..aed3e33273 100644 --- a/opentelemetry-proto/MANIFEST.in +++ b/opentelemetry-proto/MANIFEST.in @@ -1,4 +1,4 @@ -graft gen +graft src graft tests global-exclude *.pyc global-exclude *.pyo diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi new file mode 100644 index 0000000000..7c42c410f4 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi @@ -0,0 +1,72 @@ +# @generated by generate_proto_mypy_stubs.py. Do not edit! +import sys +from google.protobuf.descriptor import ( + Descriptor as google___protobuf___descriptor___Descriptor, + FileDescriptor as google___protobuf___descriptor___FileDescriptor, +) + +from google.protobuf.internal.containers import ( + RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, +) + +from google.protobuf.message import ( + Message as google___protobuf___message___Message, +) + +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + ResourceMetrics as opentelemetry___proto___metrics___v1___metrics_pb2___ResourceMetrics, +) + +from typing import ( + Iterable as typing___Iterable, + Optional as typing___Optional, + Union as typing___Union, +) + +from typing_extensions import ( + Literal as typing_extensions___Literal, +) + + +builtin___bool = bool +builtin___bytes = bytes +builtin___float = float +builtin___int = int +if sys.version_info < (3,): + builtin___buffer = buffer + builtin___unicode = unicode + + +DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... + +class ExportMetricsServiceRequest(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + @property + def resource_metrics(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___metrics___v1___metrics_pb2___ResourceMetrics]: ... + + def __init__(self, + *, + resource_metrics : typing___Optional[typing___Iterable[opentelemetry___proto___metrics___v1___metrics_pb2___ResourceMetrics]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> ExportMetricsServiceRequest: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ExportMetricsServiceRequest: ... + def ClearField(self, field_name: typing_extensions___Literal[u"resource_metrics",b"resource_metrics"]) -> None: ... +type___ExportMetricsServiceRequest = ExportMetricsServiceRequest + +class ExportMetricsServiceResponse(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + def __init__(self, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> ExportMetricsServiceResponse: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ExportMetricsServiceResponse: ... +type___ExportMetricsServiceResponse = ExportMetricsServiceResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi new file mode 100644 index 0000000000..540a82ebc5 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi @@ -0,0 +1,72 @@ +# @generated by generate_proto_mypy_stubs.py. Do not edit! +import sys +from google.protobuf.descriptor import ( + Descriptor as google___protobuf___descriptor___Descriptor, + FileDescriptor as google___protobuf___descriptor___FileDescriptor, +) + +from google.protobuf.internal.containers import ( + RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, +) + +from google.protobuf.message import ( + Message as google___protobuf___message___Message, +) + +from opentelemetry.proto.trace.v1.trace_pb2 import ( + ResourceSpans as opentelemetry___proto___trace___v1___trace_pb2___ResourceSpans, +) + +from typing import ( + Iterable as typing___Iterable, + Optional as typing___Optional, + Union as typing___Union, +) + +from typing_extensions import ( + Literal as typing_extensions___Literal, +) + + +builtin___bool = bool +builtin___bytes = bytes +builtin___float = float +builtin___int = int +if sys.version_info < (3,): + builtin___buffer = buffer + builtin___unicode = unicode + + +DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... + +class ExportTraceServiceRequest(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + @property + def resource_spans(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___trace___v1___trace_pb2___ResourceSpans]: ... + + def __init__(self, + *, + resource_spans : typing___Optional[typing___Iterable[opentelemetry___proto___trace___v1___trace_pb2___ResourceSpans]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> ExportTraceServiceRequest: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ExportTraceServiceRequest: ... + def ClearField(self, field_name: typing_extensions___Literal[u"resource_spans",b"resource_spans"]) -> None: ... +type___ExportTraceServiceRequest = ExportTraceServiceRequest + +class ExportTraceServiceResponse(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + def __init__(self, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> ExportTraceServiceResponse: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ExportTraceServiceResponse: ... +type___ExportTraceServiceResponse = ExportTraceServiceResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py index 62e951269c..dd08ccf6ad 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py @@ -18,7 +18,6 @@ package='opentelemetry.proto.common.v1', syntax='proto3', serialized_options=b'\n io.opentelemetry.proto.common.v1B\013CommonProtoP\001Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1', - create_key=_descriptor._internal_create_key, serialized_pb=b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\xf5\x01\n\x11\x41ttributeKeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12H\n\x04type\x18\x02 \x01(\x0e\x32:.opentelemetry.proto.common.v1.AttributeKeyValue.ValueType\x12\x14\n\x0cstring_value\x18\x03 \x01(\t\x12\x11\n\tint_value\x18\x04 \x01(\x03\x12\x14\n\x0c\x64ouble_value\x18\x05 \x01(\x01\x12\x12\n\nbool_value\x18\x06 \x01(\x08\"6\n\tValueType\x12\n\n\x06STRING\x10\x00\x12\x07\n\x03INT\x10\x01\x12\n\n\x06\x44OUBLE\x10\x02\x12\x08\n\x04\x42OOL\x10\x03\",\n\x0eStringKeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"7\n\x16InstrumentationLibrary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\tBq\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1b\x06proto3' ) @@ -29,28 +28,23 @@ full_name='opentelemetry.proto.common.v1.AttributeKeyValue.ValueType', filename=None, file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='STRING', index=0, number=0, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='INT', index=1, number=1, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='DOUBLE', index=2, number=2, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='BOOL', index=3, number=3, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), ], containing_type=None, serialized_options=None, @@ -66,7 +60,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='key', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.key', index=0, @@ -74,42 +67,42 @@ has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='type', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.type', index=1, number=2, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='string_value', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.string_value', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='int_value', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.int_value', index=3, number=4, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='double_value', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.double_value', index=4, number=5, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='bool_value', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.bool_value', index=5, number=6, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -134,7 +127,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='key', full_name='opentelemetry.proto.common.v1.StringKeyValue.key', index=0, @@ -142,14 +134,14 @@ has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='value', full_name='opentelemetry.proto.common.v1.StringKeyValue.value', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -173,7 +165,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='opentelemetry.proto.common.v1.InstrumentationLibrary.name', index=0, @@ -181,14 +172,14 @@ has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='version', full_name='opentelemetry.proto.common.v1.InstrumentationLibrary.version', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi new file mode 100644 index 0000000000..1061616570 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi @@ -0,0 +1,127 @@ +# @generated by generate_proto_mypy_stubs.py. Do not edit! +import sys +from google.protobuf.descriptor import ( + Descriptor as google___protobuf___descriptor___Descriptor, + EnumDescriptor as google___protobuf___descriptor___EnumDescriptor, + FileDescriptor as google___protobuf___descriptor___FileDescriptor, +) + +from google.protobuf.message import ( + Message as google___protobuf___message___Message, +) + +from typing import ( + List as typing___List, + NewType as typing___NewType, + Optional as typing___Optional, + Text as typing___Text, + Tuple as typing___Tuple, + Union as typing___Union, + cast as typing___cast, +) + +from typing_extensions import ( + Literal as typing_extensions___Literal, +) + + +builtin___bool = bool +builtin___bytes = bytes +builtin___float = float +builtin___int = int +builtin___str = str +if sys.version_info < (3,): + builtin___buffer = buffer + builtin___unicode = unicode + + +DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... + +class AttributeKeyValue(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + ValueTypeValue = typing___NewType('ValueTypeValue', builtin___int) + type___ValueTypeValue = ValueTypeValue + class ValueType(object): + DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... + @classmethod + def Name(cls, number: builtin___int) -> builtin___str: ... + @classmethod + def Value(cls, name: builtin___str) -> AttributeKeyValue.ValueTypeValue: ... + @classmethod + def keys(cls) -> typing___List[builtin___str]: ... + @classmethod + def values(cls) -> typing___List[AttributeKeyValue.ValueTypeValue]: ... + @classmethod + def items(cls) -> typing___List[typing___Tuple[builtin___str, AttributeKeyValue.ValueTypeValue]]: ... + STRING = typing___cast(AttributeKeyValue.ValueTypeValue, 0) + INT = typing___cast(AttributeKeyValue.ValueTypeValue, 1) + DOUBLE = typing___cast(AttributeKeyValue.ValueTypeValue, 2) + BOOL = typing___cast(AttributeKeyValue.ValueTypeValue, 3) + STRING = typing___cast(AttributeKeyValue.ValueTypeValue, 0) + INT = typing___cast(AttributeKeyValue.ValueTypeValue, 1) + DOUBLE = typing___cast(AttributeKeyValue.ValueTypeValue, 2) + BOOL = typing___cast(AttributeKeyValue.ValueTypeValue, 3) + type___ValueType = ValueType + + key: typing___Text = ... + type: type___AttributeKeyValue.ValueTypeValue = ... + string_value: typing___Text = ... + int_value: builtin___int = ... + double_value: builtin___float = ... + bool_value: builtin___bool = ... + + def __init__(self, + *, + key : typing___Optional[typing___Text] = None, + type : typing___Optional[type___AttributeKeyValue.ValueTypeValue] = None, + string_value : typing___Optional[typing___Text] = None, + int_value : typing___Optional[builtin___int] = None, + double_value : typing___Optional[builtin___float] = None, + bool_value : typing___Optional[builtin___bool] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> AttributeKeyValue: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> AttributeKeyValue: ... + def ClearField(self, field_name: typing_extensions___Literal[u"bool_value",b"bool_value",u"double_value",b"double_value",u"int_value",b"int_value",u"key",b"key",u"string_value",b"string_value",u"type",b"type"]) -> None: ... +type___AttributeKeyValue = AttributeKeyValue + +class StringKeyValue(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + key: typing___Text = ... + value: typing___Text = ... + + def __init__(self, + *, + key : typing___Optional[typing___Text] = None, + value : typing___Optional[typing___Text] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> StringKeyValue: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> StringKeyValue: ... + def ClearField(self, field_name: typing_extensions___Literal[u"key",b"key",u"value",b"value"]) -> None: ... +type___StringKeyValue = StringKeyValue + +class InstrumentationLibrary(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + name: typing___Text = ... + version: typing___Text = ... + + def __init__(self, + *, + name : typing___Optional[typing___Text] = None, + version : typing___Optional[typing___Text] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> InstrumentationLibrary: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> InstrumentationLibrary: ... + def ClearField(self, field_name: typing_extensions___Literal[u"name",b"name",u"version",b"version"]) -> None: ... +type___InstrumentationLibrary = InstrumentationLibrary diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py index 4edaa3609d..e4b62369a0 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py @@ -20,8 +20,7 @@ package='opentelemetry.proto.metrics.v1', syntax='proto3', serialized_options=b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xb6\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x66\n\x1finstrumentation_library_metrics\x18\x02 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics\"\xb0\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\"\x8f\x03\n\x06Metric\x12K\n\x11metric_descriptor\x18\x01 \x01(\x0b\x32\x30.opentelemetry.proto.metrics.v1.MetricDescriptor\x12I\n\x11int64_data_points\x18\x02 \x03(\x0b\x32..opentelemetry.proto.metrics.v1.Int64DataPoint\x12K\n\x12\x64ouble_data_points\x18\x03 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleDataPoint\x12Q\n\x15histogram_data_points\x18\x04 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12M\n\x13summary_data_points\x18\x05 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\xfe\x02\n\x10MetricDescriptor\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x43\n\x04type\x18\x04 \x01(\x0e\x32\x35.opentelemetry.proto.metrics.v1.MetricDescriptor.Type\x12Q\n\x0btemporality\x18\x05 \x01(\x0e\x32<.opentelemetry.proto.metrics.v1.MetricDescriptor.Temporality\"K\n\x04Type\x12\x10\n\x0cINVALID_TYPE\x10\x00\x12\t\n\x05INT64\x10\x01\x12\n\n\x06\x44OUBLE\x10\x02\x12\r\n\tHISTOGRAM\x10\x03\x12\x0b\n\x07SUMMARY\x10\x04\"T\n\x0bTemporality\x12\x17\n\x13INVALID_TEMPORALITY\x10\x00\x12\x11\n\rINSTANTANEOUS\x10\x01\x12\t\n\x05\x44\x45LTA\x10\x02\x12\x0e\n\nCUMULATIVE\x10\x03\"\x94\x01\n\x0eInt64DataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x03\"\x95\x01\n\x0f\x44oubleDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x01\"\xf1\x03\n\x12HistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x04\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12J\n\x07\x62uckets\x18\x06 \x03(\x0b\x32\x39.opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x1a\xe4\x01\n\x06\x42ucket\x12\r\n\x05\x63ount\x18\x01 \x01(\x04\x12T\n\x08\x65xemplar\x18\x02 \x01(\x0b\x32\x42.opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar\x1au\n\x08\x45xemplar\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x42\n\x0b\x61ttachments\x18\x03 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\"\xba\x02\n\x10SummaryDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x04\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12]\n\x11percentile_values\x18\x06 \x03(\x0b\x32\x42.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile\x1a\x36\n\x11ValueAtPercentile\x12\x12\n\npercentile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01\x42t\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1b\x06proto3' + serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xb6\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x66\n\x1finstrumentation_library_metrics\x18\x02 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics\"\xb0\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\"\x8f\x03\n\x06Metric\x12K\n\x11metric_descriptor\x18\x01 \x01(\x0b\x32\x30.opentelemetry.proto.metrics.v1.MetricDescriptor\x12I\n\x11int64_data_points\x18\x02 \x03(\x0b\x32..opentelemetry.proto.metrics.v1.Int64DataPoint\x12K\n\x12\x64ouble_data_points\x18\x03 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleDataPoint\x12Q\n\x15histogram_data_points\x18\x04 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12M\n\x13summary_data_points\x18\x05 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\xa9\x03\n\x10MetricDescriptor\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x43\n\x04type\x18\x04 \x01(\x0e\x32\x35.opentelemetry.proto.metrics.v1.MetricDescriptor.Type\x12Q\n\x0btemporality\x18\x05 \x01(\x0e\x32<.opentelemetry.proto.metrics.v1.MetricDescriptor.Temporality\"v\n\x04Type\x12\x10\n\x0cINVALID_TYPE\x10\x00\x12\t\n\x05INT64\x10\x01\x12\x13\n\x0fMONOTONIC_INT64\x10\x02\x12\n\n\x06\x44OUBLE\x10\x03\x12\x14\n\x10MONOTONIC_DOUBLE\x10\x04\x12\r\n\tHISTOGRAM\x10\x05\x12\x0b\n\x07SUMMARY\x10\x06\"T\n\x0bTemporality\x12\x17\n\x13INVALID_TEMPORALITY\x10\x00\x12\x11\n\rINSTANTANEOUS\x10\x01\x12\t\n\x05\x44\x45LTA\x10\x02\x12\x0e\n\nCUMULATIVE\x10\x03\"\x94\x01\n\x0eInt64DataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x03\"\x95\x01\n\x0f\x44oubleDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x01\"\xf1\x03\n\x12HistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x04\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12J\n\x07\x62uckets\x18\x06 \x03(\x0b\x32\x39.opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x1a\xe4\x01\n\x06\x42ucket\x12\r\n\x05\x63ount\x18\x01 \x01(\x04\x12T\n\x08\x65xemplar\x18\x02 \x01(\x0b\x32\x42.opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar\x1au\n\x08\x45xemplar\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x42\n\x0b\x61ttachments\x18\x03 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\"\xba\x02\n\x10SummaryDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x04\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12]\n\x11percentile_values\x18\x06 \x03(\x0b\x32\x42.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile\x1a\x36\n\x11ValueAtPercentile\x12\x12\n\npercentile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01\x42t\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -32,38 +31,40 @@ full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.Type', filename=None, file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='INVALID_TYPE', index=0, number=0, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='INT64', index=1, number=1, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( - name='DOUBLE', index=2, number=2, + name='MONOTONIC_INT64', index=2, number=2, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( - name='HISTOGRAM', index=3, number=3, + name='DOUBLE', index=3, number=3, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( - name='SUMMARY', index=4, number=4, + name='MONOTONIC_DOUBLE', index=4, number=4, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), + _descriptor.EnumValueDescriptor( + name='HISTOGRAM', index=5, number=5, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SUMMARY', index=6, number=6, + serialized_options=None, + type=None), ], containing_type=None, serialized_options=None, serialized_start=1160, - serialized_end=1235, + serialized_end=1278, ) _sym_db.RegisterEnumDescriptor(_METRICDESCRIPTOR_TYPE) @@ -72,33 +73,28 @@ full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.Temporality', filename=None, file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='INVALID_TEMPORALITY', index=0, number=0, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='INSTANTANEOUS', index=1, number=1, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='DELTA', index=2, number=2, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='CUMULATIVE', index=3, number=3, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), ], containing_type=None, serialized_options=None, - serialized_start=1237, - serialized_end=1321, + serialized_start=1280, + serialized_end=1364, ) _sym_db.RegisterEnumDescriptor(_METRICDESCRIPTOR_TEMPORALITY) @@ -109,7 +105,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='resource', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.resource', index=0, @@ -117,14 +112,14 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='instrumentation_library_metrics', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.instrumentation_library_metrics', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -148,7 +143,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='instrumentation_library', full_name='opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics.instrumentation_library', index=0, @@ -156,14 +150,14 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='metrics', full_name='opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics.metrics', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -187,7 +181,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='metric_descriptor', full_name='opentelemetry.proto.metrics.v1.Metric.metric_descriptor', index=0, @@ -195,35 +188,35 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='int64_data_points', full_name='opentelemetry.proto.metrics.v1.Metric.int64_data_points', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='double_data_points', full_name='opentelemetry.proto.metrics.v1.Metric.double_data_points', index=2, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='histogram_data_points', full_name='opentelemetry.proto.metrics.v1.Metric.histogram_data_points', index=3, number=4, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='summary_data_points', full_name='opentelemetry.proto.metrics.v1.Metric.summary_data_points', index=4, number=5, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -247,7 +240,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.name', index=0, @@ -255,35 +247,35 @@ has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='description', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.description', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='unit', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.unit', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='type', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.type', index=3, number=4, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='temporality', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.temporality', index=4, number=5, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -299,7 +291,7 @@ oneofs=[ ], serialized_start=939, - serialized_end=1321, + serialized_end=1364, ) @@ -309,7 +301,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='labels', full_name='opentelemetry.proto.metrics.v1.Int64DataPoint.labels', index=0, @@ -317,28 +308,28 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.Int64DataPoint.start_time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.Int64DataPoint.time_unix_nano', index=2, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='value', full_name='opentelemetry.proto.metrics.v1.Int64DataPoint.value', index=3, number=4, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -351,8 +342,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1324, - serialized_end=1472, + serialized_start=1367, + serialized_end=1515, ) @@ -362,7 +353,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='labels', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.labels', index=0, @@ -370,28 +360,28 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.start_time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.time_unix_nano', index=2, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='value', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.value', index=3, number=4, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -404,8 +394,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1475, - serialized_end=1624, + serialized_start=1518, + serialized_end=1667, ) @@ -415,7 +405,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='value', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar.value', index=0, @@ -423,21 +412,21 @@ has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar.time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='attachments', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar.attachments', index=2, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -450,8 +439,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2007, - serialized_end=2124, + serialized_start=2050, + serialized_end=2167, ) _HISTOGRAMDATAPOINT_BUCKET = _descriptor.Descriptor( @@ -460,7 +449,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='count', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.count', index=0, @@ -468,14 +456,14 @@ has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='exemplar', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.exemplar', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -488,8 +476,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1896, - serialized_end=2124, + serialized_start=1939, + serialized_end=2167, ) _HISTOGRAMDATAPOINT = _descriptor.Descriptor( @@ -498,7 +486,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='labels', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.labels', index=0, @@ -506,49 +493,49 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.start_time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.time_unix_nano', index=2, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='count', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.count', index=3, number=4, type=4, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='sum', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.sum', index=4, number=5, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='buckets', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.buckets', index=5, number=6, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='explicit_bounds', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.explicit_bounds', index=6, number=7, type=1, cpp_type=5, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -561,8 +548,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1627, - serialized_end=2124, + serialized_start=1670, + serialized_end=2167, ) @@ -572,7 +559,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='percentile', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile.percentile', index=0, @@ -580,14 +566,14 @@ has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='value', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile.value', index=1, number=2, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -600,8 +586,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2387, - serialized_end=2441, + serialized_start=2430, + serialized_end=2484, ) _SUMMARYDATAPOINT = _descriptor.Descriptor( @@ -610,7 +596,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='labels', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.labels', index=0, @@ -618,42 +603,42 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.start_time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.time_unix_nano', index=2, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='count', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.count', index=3, number=4, type=4, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='sum', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.sum', index=4, number=5, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='percentile_values', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.percentile_values', index=5, number=6, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -666,8 +651,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2127, - serialized_end=2441, + serialized_start=2170, + serialized_end=2484, ) _RESOURCEMETRICS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi new file mode 100644 index 0000000000..964e23e3a8 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi @@ -0,0 +1,394 @@ +# @generated by generate_proto_mypy_stubs.py. Do not edit! +import sys +from google.protobuf.descriptor import ( + Descriptor as google___protobuf___descriptor___Descriptor, + EnumDescriptor as google___protobuf___descriptor___EnumDescriptor, + FileDescriptor as google___protobuf___descriptor___FileDescriptor, +) + +from google.protobuf.internal.containers import ( + RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, + RepeatedScalarFieldContainer as google___protobuf___internal___containers___RepeatedScalarFieldContainer, +) + +from google.protobuf.message import ( + Message as google___protobuf___message___Message, +) + +from opentelemetry.proto.common.v1.common_pb2 import ( + InstrumentationLibrary as opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary, + StringKeyValue as opentelemetry___proto___common___v1___common_pb2___StringKeyValue, +) + +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as opentelemetry___proto___resource___v1___resource_pb2___Resource, +) + +from typing import ( + Iterable as typing___Iterable, + List as typing___List, + NewType as typing___NewType, + Optional as typing___Optional, + Text as typing___Text, + Tuple as typing___Tuple, + Union as typing___Union, + cast as typing___cast, +) + +from typing_extensions import ( + Literal as typing_extensions___Literal, +) + + +builtin___bool = bool +builtin___bytes = bytes +builtin___float = float +builtin___int = int +builtin___str = str +if sys.version_info < (3,): + builtin___buffer = buffer + builtin___unicode = unicode + + +DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... + +class ResourceMetrics(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + @property + def resource(self) -> opentelemetry___proto___resource___v1___resource_pb2___Resource: ... + + @property + def instrumentation_library_metrics(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___InstrumentationLibraryMetrics]: ... + + def __init__(self, + *, + resource : typing___Optional[opentelemetry___proto___resource___v1___resource_pb2___Resource] = None, + instrumentation_library_metrics : typing___Optional[typing___Iterable[type___InstrumentationLibraryMetrics]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> ResourceMetrics: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ResourceMetrics: ... + def HasField(self, field_name: typing_extensions___Literal[u"resource",b"resource"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"instrumentation_library_metrics",b"instrumentation_library_metrics",u"resource",b"resource"]) -> None: ... +type___ResourceMetrics = ResourceMetrics + +class InstrumentationLibraryMetrics(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + @property + def instrumentation_library(self) -> opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary: ... + + @property + def metrics(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Metric]: ... + + def __init__(self, + *, + instrumentation_library : typing___Optional[opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary] = None, + metrics : typing___Optional[typing___Iterable[type___Metric]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> InstrumentationLibraryMetrics: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> InstrumentationLibraryMetrics: ... + def HasField(self, field_name: typing_extensions___Literal[u"instrumentation_library",b"instrumentation_library"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"instrumentation_library",b"instrumentation_library",u"metrics",b"metrics"]) -> None: ... +type___InstrumentationLibraryMetrics = InstrumentationLibraryMetrics + +class Metric(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + @property + def metric_descriptor(self) -> type___MetricDescriptor: ... + + @property + def int64_data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Int64DataPoint]: ... + + @property + def double_data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___DoubleDataPoint]: ... + + @property + def histogram_data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___HistogramDataPoint]: ... + + @property + def summary_data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___SummaryDataPoint]: ... + + def __init__(self, + *, + metric_descriptor : typing___Optional[type___MetricDescriptor] = None, + int64_data_points : typing___Optional[typing___Iterable[type___Int64DataPoint]] = None, + double_data_points : typing___Optional[typing___Iterable[type___DoubleDataPoint]] = None, + histogram_data_points : typing___Optional[typing___Iterable[type___HistogramDataPoint]] = None, + summary_data_points : typing___Optional[typing___Iterable[type___SummaryDataPoint]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> Metric: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Metric: ... + def HasField(self, field_name: typing_extensions___Literal[u"metric_descriptor",b"metric_descriptor"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"double_data_points",b"double_data_points",u"histogram_data_points",b"histogram_data_points",u"int64_data_points",b"int64_data_points",u"metric_descriptor",b"metric_descriptor",u"summary_data_points",b"summary_data_points"]) -> None: ... +type___Metric = Metric + +class MetricDescriptor(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + TypeValue = typing___NewType('TypeValue', builtin___int) + type___TypeValue = TypeValue + class Type(object): + DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... + @classmethod + def Name(cls, number: builtin___int) -> builtin___str: ... + @classmethod + def Value(cls, name: builtin___str) -> MetricDescriptor.TypeValue: ... + @classmethod + def keys(cls) -> typing___List[builtin___str]: ... + @classmethod + def values(cls) -> typing___List[MetricDescriptor.TypeValue]: ... + @classmethod + def items(cls) -> typing___List[typing___Tuple[builtin___str, MetricDescriptor.TypeValue]]: ... + INVALID_TYPE = typing___cast(MetricDescriptor.TypeValue, 0) + INT64 = typing___cast(MetricDescriptor.TypeValue, 1) + MONOTONIC_INT64 = typing___cast(MetricDescriptor.TypeValue, 2) + DOUBLE = typing___cast(MetricDescriptor.TypeValue, 3) + MONOTONIC_DOUBLE = typing___cast(MetricDescriptor.TypeValue, 4) + HISTOGRAM = typing___cast(MetricDescriptor.TypeValue, 5) + SUMMARY = typing___cast(MetricDescriptor.TypeValue, 6) + INVALID_TYPE = typing___cast(MetricDescriptor.TypeValue, 0) + INT64 = typing___cast(MetricDescriptor.TypeValue, 1) + MONOTONIC_INT64 = typing___cast(MetricDescriptor.TypeValue, 2) + DOUBLE = typing___cast(MetricDescriptor.TypeValue, 3) + MONOTONIC_DOUBLE = typing___cast(MetricDescriptor.TypeValue, 4) + HISTOGRAM = typing___cast(MetricDescriptor.TypeValue, 5) + SUMMARY = typing___cast(MetricDescriptor.TypeValue, 6) + type___Type = Type + + TemporalityValue = typing___NewType('TemporalityValue', builtin___int) + type___TemporalityValue = TemporalityValue + class Temporality(object): + DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... + @classmethod + def Name(cls, number: builtin___int) -> builtin___str: ... + @classmethod + def Value(cls, name: builtin___str) -> MetricDescriptor.TemporalityValue: ... + @classmethod + def keys(cls) -> typing___List[builtin___str]: ... + @classmethod + def values(cls) -> typing___List[MetricDescriptor.TemporalityValue]: ... + @classmethod + def items(cls) -> typing___List[typing___Tuple[builtin___str, MetricDescriptor.TemporalityValue]]: ... + INVALID_TEMPORALITY = typing___cast(MetricDescriptor.TemporalityValue, 0) + INSTANTANEOUS = typing___cast(MetricDescriptor.TemporalityValue, 1) + DELTA = typing___cast(MetricDescriptor.TemporalityValue, 2) + CUMULATIVE = typing___cast(MetricDescriptor.TemporalityValue, 3) + INVALID_TEMPORALITY = typing___cast(MetricDescriptor.TemporalityValue, 0) + INSTANTANEOUS = typing___cast(MetricDescriptor.TemporalityValue, 1) + DELTA = typing___cast(MetricDescriptor.TemporalityValue, 2) + CUMULATIVE = typing___cast(MetricDescriptor.TemporalityValue, 3) + type___Temporality = Temporality + + name: typing___Text = ... + description: typing___Text = ... + unit: typing___Text = ... + type: type___MetricDescriptor.TypeValue = ... + temporality: type___MetricDescriptor.TemporalityValue = ... + + def __init__(self, + *, + name : typing___Optional[typing___Text] = None, + description : typing___Optional[typing___Text] = None, + unit : typing___Optional[typing___Text] = None, + type : typing___Optional[type___MetricDescriptor.TypeValue] = None, + temporality : typing___Optional[type___MetricDescriptor.TemporalityValue] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> MetricDescriptor: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> MetricDescriptor: ... + def ClearField(self, field_name: typing_extensions___Literal[u"description",b"description",u"name",b"name",u"temporality",b"temporality",u"type",b"type",u"unit",b"unit"]) -> None: ... +type___MetricDescriptor = MetricDescriptor + +class Int64DataPoint(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + start_time_unix_nano: builtin___int = ... + time_unix_nano: builtin___int = ... + value: builtin___int = ... + + @property + def labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + + def __init__(self, + *, + labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, + start_time_unix_nano : typing___Optional[builtin___int] = None, + time_unix_nano : typing___Optional[builtin___int] = None, + value : typing___Optional[builtin___int] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> Int64DataPoint: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Int64DataPoint: ... + def ClearField(self, field_name: typing_extensions___Literal[u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... +type___Int64DataPoint = Int64DataPoint + +class DoubleDataPoint(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + start_time_unix_nano: builtin___int = ... + time_unix_nano: builtin___int = ... + value: builtin___float = ... + + @property + def labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + + def __init__(self, + *, + labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, + start_time_unix_nano : typing___Optional[builtin___int] = None, + time_unix_nano : typing___Optional[builtin___int] = None, + value : typing___Optional[builtin___float] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> DoubleDataPoint: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> DoubleDataPoint: ... + def ClearField(self, field_name: typing_extensions___Literal[u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... +type___DoubleDataPoint = DoubleDataPoint + +class HistogramDataPoint(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + class Bucket(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + class Exemplar(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + value: builtin___float = ... + time_unix_nano: builtin___int = ... + + @property + def attachments(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + + def __init__(self, + *, + value : typing___Optional[builtin___float] = None, + time_unix_nano : typing___Optional[builtin___int] = None, + attachments : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> HistogramDataPoint.Bucket.Exemplar: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> HistogramDataPoint.Bucket.Exemplar: ... + def ClearField(self, field_name: typing_extensions___Literal[u"attachments",b"attachments",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... + type___Exemplar = Exemplar + + count: builtin___int = ... + + @property + def exemplar(self) -> type___HistogramDataPoint.Bucket.Exemplar: ... + + def __init__(self, + *, + count : typing___Optional[builtin___int] = None, + exemplar : typing___Optional[type___HistogramDataPoint.Bucket.Exemplar] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> HistogramDataPoint.Bucket: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> HistogramDataPoint.Bucket: ... + def HasField(self, field_name: typing_extensions___Literal[u"exemplar",b"exemplar"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"count",b"count",u"exemplar",b"exemplar"]) -> None: ... + type___Bucket = Bucket + + start_time_unix_nano: builtin___int = ... + time_unix_nano: builtin___int = ... + count: builtin___int = ... + sum: builtin___float = ... + explicit_bounds: google___protobuf___internal___containers___RepeatedScalarFieldContainer[builtin___float] = ... + + @property + def labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + + @property + def buckets(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___HistogramDataPoint.Bucket]: ... + + def __init__(self, + *, + labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, + start_time_unix_nano : typing___Optional[builtin___int] = None, + time_unix_nano : typing___Optional[builtin___int] = None, + count : typing___Optional[builtin___int] = None, + sum : typing___Optional[builtin___float] = None, + buckets : typing___Optional[typing___Iterable[type___HistogramDataPoint.Bucket]] = None, + explicit_bounds : typing___Optional[typing___Iterable[builtin___float]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> HistogramDataPoint: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> HistogramDataPoint: ... + def ClearField(self, field_name: typing_extensions___Literal[u"buckets",b"buckets",u"count",b"count",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... +type___HistogramDataPoint = HistogramDataPoint + +class SummaryDataPoint(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + class ValueAtPercentile(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + percentile: builtin___float = ... + value: builtin___float = ... + + def __init__(self, + *, + percentile : typing___Optional[builtin___float] = None, + value : typing___Optional[builtin___float] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> SummaryDataPoint.ValueAtPercentile: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> SummaryDataPoint.ValueAtPercentile: ... + def ClearField(self, field_name: typing_extensions___Literal[u"percentile",b"percentile",u"value",b"value"]) -> None: ... + type___ValueAtPercentile = ValueAtPercentile + + start_time_unix_nano: builtin___int = ... + time_unix_nano: builtin___int = ... + count: builtin___int = ... + sum: builtin___float = ... + + @property + def labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + + @property + def percentile_values(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___SummaryDataPoint.ValueAtPercentile]: ... + + def __init__(self, + *, + labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, + start_time_unix_nano : typing___Optional[builtin___int] = None, + time_unix_nano : typing___Optional[builtin___int] = None, + count : typing___Optional[builtin___int] = None, + sum : typing___Optional[builtin___float] = None, + percentile_values : typing___Optional[typing___Iterable[type___SummaryDataPoint.ValueAtPercentile]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> SummaryDataPoint: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> SummaryDataPoint: ... + def ClearField(self, field_name: typing_extensions___Literal[u"count",b"count",u"labels",b"labels",u"percentile_values",b"percentile_values",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... +type___SummaryDataPoint = SummaryDataPoint diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py index 5126a1b76e..95dcf479ff 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py @@ -19,7 +19,6 @@ package='opentelemetry.proto.resource.v1', syntax='proto3', serialized_options=b'\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\001Z@github.com/open-telemetry/opentelemetry-proto/gen/go/resource/v1', - create_key=_descriptor._internal_create_key, serialized_pb=b'\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"r\n\x08Resource\x12\x44\n\nattributes\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.common.v1.AttributeKeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\rBw\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z@github.com/open-telemetry/opentelemetry-proto/gen/go/resource/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,]) @@ -33,7 +32,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='attributes', full_name='opentelemetry.proto.resource.v1.Resource.attributes', index=0, @@ -41,14 +39,14 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='dropped_attributes_count', full_name='opentelemetry.proto.resource.v1.Resource.dropped_attributes_count', index=1, number=2, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi new file mode 100644 index 0000000000..2f9f5afca2 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi @@ -0,0 +1,61 @@ +# @generated by generate_proto_mypy_stubs.py. Do not edit! +import sys +from google.protobuf.descriptor import ( + Descriptor as google___protobuf___descriptor___Descriptor, + FileDescriptor as google___protobuf___descriptor___FileDescriptor, +) + +from google.protobuf.internal.containers import ( + RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, +) + +from google.protobuf.message import ( + Message as google___protobuf___message___Message, +) + +from opentelemetry.proto.common.v1.common_pb2 import ( + AttributeKeyValue as opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue, +) + +from typing import ( + Iterable as typing___Iterable, + Optional as typing___Optional, + Union as typing___Union, +) + +from typing_extensions import ( + Literal as typing_extensions___Literal, +) + + +builtin___bool = bool +builtin___bytes = bytes +builtin___float = float +builtin___int = int +if sys.version_info < (3,): + builtin___buffer = buffer + builtin___unicode = unicode + + +DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... + +class Resource(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + dropped_attributes_count: builtin___int = ... + + @property + def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]: ... + + def __init__(self, + *, + attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]] = None, + dropped_attributes_count : typing___Optional[builtin___int] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> Resource: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Resource: ... + def ClearField(self, field_name: typing_extensions___Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count"]) -> None: ... +type___Resource = Resource diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py index ad1857d37b..de6ca90f5f 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py @@ -18,7 +18,6 @@ package='opentelemetry.proto.trace.v1', syntax='proto3', serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\020TraceConfigProtoP\001ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1', - create_key=_descriptor._internal_create_key, serialized_pb=b'\n/opentelemetry/proto/trace/v1/trace_config.proto\x12\x1copentelemetry.proto.trace.v1\"\xc8\x03\n\x0bTraceConfig\x12I\n\x10\x63onstant_sampler\x18\x01 \x01(\x0b\x32-.opentelemetry.proto.trace.v1.ConstantSamplerH\x00\x12O\n\x13probability_sampler\x18\x02 \x01(\x0b\x32\x30.opentelemetry.proto.trace.v1.ProbabilitySamplerH\x00\x12R\n\x15rate_limiting_sampler\x18\x03 \x01(\x0b\x32\x31.opentelemetry.proto.trace.v1.RateLimitingSamplerH\x00\x12 \n\x18max_number_of_attributes\x18\x04 \x01(\x03\x12\"\n\x1amax_number_of_timed_events\x18\x05 \x01(\x03\x12\x30\n(max_number_of_attributes_per_timed_event\x18\x06 \x01(\x03\x12\x1b\n\x13max_number_of_links\x18\x07 \x01(\x03\x12)\n!max_number_of_attributes_per_link\x18\x08 \x01(\x03\x42\t\n\x07sampler\"\xa9\x01\n\x0f\x43onstantSampler\x12P\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32>.opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision\"D\n\x10\x43onstantDecision\x12\x0e\n\nALWAYS_OFF\x10\x00\x12\r\n\tALWAYS_ON\x10\x01\x12\x11\n\rALWAYS_PARENT\x10\x02\"1\n\x12ProbabilitySampler\x12\x1b\n\x13samplingProbability\x18\x01 \x01(\x01\"\"\n\x13RateLimitingSampler\x12\x0b\n\x03qps\x18\x01 \x01(\x03\x42~\n\x1fio.opentelemetry.proto.trace.v1B\x10TraceConfigProtoP\x01ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1b\x06proto3' ) @@ -29,23 +28,19 @@ full_name='opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision', filename=None, file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='ALWAYS_OFF', index=0, number=0, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='ALWAYS_ON', index=1, number=1, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='ALWAYS_PARENT', index=2, number=2, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), ], containing_type=None, serialized_options=None, @@ -61,7 +56,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='constant_sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.constant_sampler', index=0, @@ -69,56 +63,56 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='probability_sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.probability_sampler', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='rate_limiting_sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.rate_limiting_sampler', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='max_number_of_attributes', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_attributes', index=3, number=4, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='max_number_of_timed_events', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_timed_events', index=4, number=5, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='max_number_of_attributes_per_timed_event', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_attributes_per_timed_event', index=5, number=6, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='max_number_of_links', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_links', index=6, number=7, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='max_number_of_attributes_per_link', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_attributes_per_link', index=7, number=8, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -132,9 +126,7 @@ oneofs=[ _descriptor.OneofDescriptor( name='sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.sampler', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), + index=0, containing_type=None, fields=[]), ], serialized_start=82, serialized_end=538, @@ -147,7 +139,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='decision', full_name='opentelemetry.proto.trace.v1.ConstantSampler.decision', index=0, @@ -155,7 +146,7 @@ has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -180,7 +171,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='samplingProbability', full_name='opentelemetry.proto.trace.v1.ProbabilitySampler.samplingProbability', index=0, @@ -188,7 +178,7 @@ has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -212,7 +202,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='qps', full_name='opentelemetry.proto.trace.v1.RateLimitingSampler.qps', index=0, @@ -220,7 +209,7 @@ has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi new file mode 100644 index 0000000000..9cee74883c --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi @@ -0,0 +1,149 @@ +# @generated by generate_proto_mypy_stubs.py. Do not edit! +import sys +from google.protobuf.descriptor import ( + Descriptor as google___protobuf___descriptor___Descriptor, + EnumDescriptor as google___protobuf___descriptor___EnumDescriptor, + FileDescriptor as google___protobuf___descriptor___FileDescriptor, +) + +from google.protobuf.message import ( + Message as google___protobuf___message___Message, +) + +from typing import ( + List as typing___List, + NewType as typing___NewType, + Optional as typing___Optional, + Tuple as typing___Tuple, + Union as typing___Union, + cast as typing___cast, +) + +from typing_extensions import ( + Literal as typing_extensions___Literal, +) + + +builtin___bool = bool +builtin___bytes = bytes +builtin___float = float +builtin___int = int +builtin___str = str +if sys.version_info < (3,): + builtin___buffer = buffer + builtin___unicode = unicode + + +DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... + +class TraceConfig(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + max_number_of_attributes: builtin___int = ... + max_number_of_timed_events: builtin___int = ... + max_number_of_attributes_per_timed_event: builtin___int = ... + max_number_of_links: builtin___int = ... + max_number_of_attributes_per_link: builtin___int = ... + + @property + def constant_sampler(self) -> type___ConstantSampler: ... + + @property + def probability_sampler(self) -> type___ProbabilitySampler: ... + + @property + def rate_limiting_sampler(self) -> type___RateLimitingSampler: ... + + def __init__(self, + *, + constant_sampler : typing___Optional[type___ConstantSampler] = None, + probability_sampler : typing___Optional[type___ProbabilitySampler] = None, + rate_limiting_sampler : typing___Optional[type___RateLimitingSampler] = None, + max_number_of_attributes : typing___Optional[builtin___int] = None, + max_number_of_timed_events : typing___Optional[builtin___int] = None, + max_number_of_attributes_per_timed_event : typing___Optional[builtin___int] = None, + max_number_of_links : typing___Optional[builtin___int] = None, + max_number_of_attributes_per_link : typing___Optional[builtin___int] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> TraceConfig: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> TraceConfig: ... + def HasField(self, field_name: typing_extensions___Literal[u"constant_sampler",b"constant_sampler",u"probability_sampler",b"probability_sampler",u"rate_limiting_sampler",b"rate_limiting_sampler",u"sampler",b"sampler"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"constant_sampler",b"constant_sampler",u"max_number_of_attributes",b"max_number_of_attributes",u"max_number_of_attributes_per_link",b"max_number_of_attributes_per_link",u"max_number_of_attributes_per_timed_event",b"max_number_of_attributes_per_timed_event",u"max_number_of_links",b"max_number_of_links",u"max_number_of_timed_events",b"max_number_of_timed_events",u"probability_sampler",b"probability_sampler",u"rate_limiting_sampler",b"rate_limiting_sampler",u"sampler",b"sampler"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions___Literal[u"sampler",b"sampler"]) -> typing_extensions___Literal["constant_sampler","probability_sampler","rate_limiting_sampler"]: ... +type___TraceConfig = TraceConfig + +class ConstantSampler(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + ConstantDecisionValue = typing___NewType('ConstantDecisionValue', builtin___int) + type___ConstantDecisionValue = ConstantDecisionValue + class ConstantDecision(object): + DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... + @classmethod + def Name(cls, number: builtin___int) -> builtin___str: ... + @classmethod + def Value(cls, name: builtin___str) -> ConstantSampler.ConstantDecisionValue: ... + @classmethod + def keys(cls) -> typing___List[builtin___str]: ... + @classmethod + def values(cls) -> typing___List[ConstantSampler.ConstantDecisionValue]: ... + @classmethod + def items(cls) -> typing___List[typing___Tuple[builtin___str, ConstantSampler.ConstantDecisionValue]]: ... + ALWAYS_OFF = typing___cast(ConstantSampler.ConstantDecisionValue, 0) + ALWAYS_ON = typing___cast(ConstantSampler.ConstantDecisionValue, 1) + ALWAYS_PARENT = typing___cast(ConstantSampler.ConstantDecisionValue, 2) + ALWAYS_OFF = typing___cast(ConstantSampler.ConstantDecisionValue, 0) + ALWAYS_ON = typing___cast(ConstantSampler.ConstantDecisionValue, 1) + ALWAYS_PARENT = typing___cast(ConstantSampler.ConstantDecisionValue, 2) + type___ConstantDecision = ConstantDecision + + decision: type___ConstantSampler.ConstantDecisionValue = ... + + def __init__(self, + *, + decision : typing___Optional[type___ConstantSampler.ConstantDecisionValue] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> ConstantSampler: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ConstantSampler: ... + def ClearField(self, field_name: typing_extensions___Literal[u"decision",b"decision"]) -> None: ... +type___ConstantSampler = ConstantSampler + +class ProbabilitySampler(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + samplingProbability: builtin___float = ... + + def __init__(self, + *, + samplingProbability : typing___Optional[builtin___float] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> ProbabilitySampler: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ProbabilitySampler: ... + def ClearField(self, field_name: typing_extensions___Literal[u"samplingProbability",b"samplingProbability"]) -> None: ... +type___ProbabilitySampler = ProbabilitySampler + +class RateLimitingSampler(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + qps: builtin___int = ... + + def __init__(self, + *, + qps : typing___Optional[builtin___int] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> RateLimitingSampler: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> RateLimitingSampler: ... + def ClearField(self, field_name: typing_extensions___Literal[u"qps",b"qps"]) -> None: ... +type___RateLimitingSampler = RateLimitingSampler diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py index cea946eebe..0b8bc15248 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py @@ -20,7 +20,6 @@ package='opentelemetry.proto.trace.v1', syntax='proto3', serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1', - create_key=_descriptor._internal_create_key, serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xae\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\"\xa8\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\"\xce\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12\x44\n\nattributes\x18\t \x03(\x0b\x32\x30.opentelemetry.proto.common.v1.AttributeKeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x95\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x44\n\nattributes\x18\x03 \x03(\x0b\x32\x30.opentelemetry.proto.common.v1.AttributeKeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\xa6\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x44\n\nattributes\x18\x04 \x03(\x0b\x32\x30.opentelemetry.proto.common.v1.AttributeKeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"g\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x0c\n\x08INTERNAL\x10\x01\x12\n\n\x06SERVER\x10\x02\x12\n\n\x06\x43LIENT\x10\x03\x12\x0c\n\x08PRODUCER\x10\x04\x12\x0c\n\x08\x43ONSUMER\x10\x05\"\x98\x03\n\x06Status\x12=\n\x04\x63ode\x18\x01 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xbd\x02\n\nStatusCode\x12\x06\n\x02Ok\x10\x00\x12\r\n\tCancelled\x10\x01\x12\x10\n\x0cUnknownError\x10\x02\x12\x13\n\x0fInvalidArgument\x10\x03\x12\x14\n\x10\x44\x65\x61\x64lineExceeded\x10\x04\x12\x0c\n\x08NotFound\x10\x05\x12\x11\n\rAlreadyExists\x10\x06\x12\x14\n\x10PermissionDenied\x10\x07\x12\x15\n\x11ResourceExhausted\x10\x08\x12\x16\n\x12\x46\x61iledPrecondition\x10\t\x12\x0b\n\x07\x41\x62orted\x10\n\x12\x0e\n\nOutOfRange\x10\x0b\x12\x11\n\rUnimplemented\x10\x0c\x12\x11\n\rInternalError\x10\r\x12\x0f\n\x0bUnavailable\x10\x0e\x12\x0c\n\x08\x44\x61taLoss\x10\x0f\x12\x13\n\x0fUnauthenticated\x10\x10\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -32,38 +31,31 @@ full_name='opentelemetry.proto.trace.v1.Span.SpanKind', filename=None, file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='SPAN_KIND_UNSPECIFIED', index=0, number=0, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='INTERNAL', index=1, number=1, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='SERVER', index=2, number=2, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='CLIENT', index=3, number=3, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='PRODUCER', index=4, number=4, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='CONSUMER', index=5, number=5, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), ], containing_type=None, serialized_options=None, @@ -77,93 +69,75 @@ full_name='opentelemetry.proto.trace.v1.Status.StatusCode', filename=None, file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='Ok', index=0, number=0, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='Cancelled', index=1, number=1, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='UnknownError', index=2, number=2, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='InvalidArgument', index=3, number=3, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='DeadlineExceeded', index=4, number=4, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='NotFound', index=5, number=5, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='AlreadyExists', index=6, number=6, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='PermissionDenied', index=7, number=7, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='ResourceExhausted', index=8, number=8, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='FailedPrecondition', index=9, number=9, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='Aborted', index=10, number=10, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='OutOfRange', index=11, number=11, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='Unimplemented', index=12, number=12, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='InternalError', index=13, number=13, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='Unavailable', index=14, number=14, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='DataLoss', index=15, number=15, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='Unauthenticated', index=16, number=16, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), ], containing_type=None, serialized_options=None, @@ -179,7 +153,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='resource', full_name='opentelemetry.proto.trace.v1.ResourceSpans.resource', index=0, @@ -187,14 +160,14 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='instrumentation_library_spans', full_name='opentelemetry.proto.trace.v1.ResourceSpans.instrumentation_library_spans', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -218,7 +191,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='instrumentation_library', full_name='opentelemetry.proto.trace.v1.InstrumentationLibrarySpans.instrumentation_library', index=0, @@ -226,14 +198,14 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='spans', full_name='opentelemetry.proto.trace.v1.InstrumentationLibrarySpans.spans', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -257,7 +229,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.trace.v1.Span.Event.time_unix_nano', index=0, @@ -265,28 +236,28 @@ has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='name', full_name='opentelemetry.proto.trace.v1.Span.Event.name', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='attributes', full_name='opentelemetry.proto.trace.v1.Span.Event.attributes', index=2, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='dropped_attributes_count', full_name='opentelemetry.proto.trace.v1.Span.Event.dropped_attributes_count', index=3, number=4, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -309,7 +280,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='trace_id', full_name='opentelemetry.proto.trace.v1.Span.Link.trace_id', index=0, @@ -317,35 +287,35 @@ has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='span_id', full_name='opentelemetry.proto.trace.v1.Span.Link.span_id', index=1, number=2, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='trace_state', full_name='opentelemetry.proto.trace.v1.Span.Link.trace_state', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='attributes', full_name='opentelemetry.proto.trace.v1.Span.Link.attributes', index=3, number=4, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='dropped_attributes_count', full_name='opentelemetry.proto.trace.v1.Span.Link.dropped_attributes_count', index=4, number=5, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -368,7 +338,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='trace_id', full_name='opentelemetry.proto.trace.v1.Span.trace_id', index=0, @@ -376,105 +345,105 @@ has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='span_id', full_name='opentelemetry.proto.trace.v1.Span.span_id', index=1, number=2, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='trace_state', full_name='opentelemetry.proto.trace.v1.Span.trace_state', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='parent_span_id', full_name='opentelemetry.proto.trace.v1.Span.parent_span_id', index=3, number=4, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='name', full_name='opentelemetry.proto.trace.v1.Span.name', index=4, number=5, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='kind', full_name='opentelemetry.proto.trace.v1.Span.kind', index=5, number=6, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='start_time_unix_nano', full_name='opentelemetry.proto.trace.v1.Span.start_time_unix_nano', index=6, number=7, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='end_time_unix_nano', full_name='opentelemetry.proto.trace.v1.Span.end_time_unix_nano', index=7, number=8, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='attributes', full_name='opentelemetry.proto.trace.v1.Span.attributes', index=8, number=9, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='dropped_attributes_count', full_name='opentelemetry.proto.trace.v1.Span.dropped_attributes_count', index=9, number=10, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='events', full_name='opentelemetry.proto.trace.v1.Span.events', index=10, number=11, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='dropped_events_count', full_name='opentelemetry.proto.trace.v1.Span.dropped_events_count', index=11, number=12, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='links', full_name='opentelemetry.proto.trace.v1.Span.links', index=12, number=13, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='dropped_links_count', full_name='opentelemetry.proto.trace.v1.Span.dropped_links_count', index=13, number=14, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='status', full_name='opentelemetry.proto.trace.v1.Span.status', index=14, number=15, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -499,7 +468,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='code', full_name='opentelemetry.proto.trace.v1.Status.code', index=0, @@ -507,14 +475,14 @@ has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='message', full_name='opentelemetry.proto.trace.v1.Status.message', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi new file mode 100644 index 0000000000..adcc1dade3 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi @@ -0,0 +1,303 @@ +# @generated by generate_proto_mypy_stubs.py. Do not edit! +import sys +from google.protobuf.descriptor import ( + Descriptor as google___protobuf___descriptor___Descriptor, + EnumDescriptor as google___protobuf___descriptor___EnumDescriptor, + FileDescriptor as google___protobuf___descriptor___FileDescriptor, +) + +from google.protobuf.internal.containers import ( + RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, +) + +from google.protobuf.message import ( + Message as google___protobuf___message___Message, +) + +from opentelemetry.proto.common.v1.common_pb2 import ( + AttributeKeyValue as opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue, + InstrumentationLibrary as opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary, +) + +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as opentelemetry___proto___resource___v1___resource_pb2___Resource, +) + +from typing import ( + Iterable as typing___Iterable, + List as typing___List, + NewType as typing___NewType, + Optional as typing___Optional, + Text as typing___Text, + Tuple as typing___Tuple, + Union as typing___Union, + cast as typing___cast, +) + +from typing_extensions import ( + Literal as typing_extensions___Literal, +) + + +builtin___bool = bool +builtin___bytes = bytes +builtin___float = float +builtin___int = int +builtin___str = str +if sys.version_info < (3,): + builtin___buffer = buffer + builtin___unicode = unicode + + +DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... + +class ResourceSpans(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + @property + def resource(self) -> opentelemetry___proto___resource___v1___resource_pb2___Resource: ... + + @property + def instrumentation_library_spans(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___InstrumentationLibrarySpans]: ... + + def __init__(self, + *, + resource : typing___Optional[opentelemetry___proto___resource___v1___resource_pb2___Resource] = None, + instrumentation_library_spans : typing___Optional[typing___Iterable[type___InstrumentationLibrarySpans]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> ResourceSpans: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ResourceSpans: ... + def HasField(self, field_name: typing_extensions___Literal[u"resource",b"resource"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"instrumentation_library_spans",b"instrumentation_library_spans",u"resource",b"resource"]) -> None: ... +type___ResourceSpans = ResourceSpans + +class InstrumentationLibrarySpans(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + @property + def instrumentation_library(self) -> opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary: ... + + @property + def spans(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Span]: ... + + def __init__(self, + *, + instrumentation_library : typing___Optional[opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary] = None, + spans : typing___Optional[typing___Iterable[type___Span]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> InstrumentationLibrarySpans: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> InstrumentationLibrarySpans: ... + def HasField(self, field_name: typing_extensions___Literal[u"instrumentation_library",b"instrumentation_library"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"instrumentation_library",b"instrumentation_library",u"spans",b"spans"]) -> None: ... +type___InstrumentationLibrarySpans = InstrumentationLibrarySpans + +class Span(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + SpanKindValue = typing___NewType('SpanKindValue', builtin___int) + type___SpanKindValue = SpanKindValue + class SpanKind(object): + DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... + @classmethod + def Name(cls, number: builtin___int) -> builtin___str: ... + @classmethod + def Value(cls, name: builtin___str) -> Span.SpanKindValue: ... + @classmethod + def keys(cls) -> typing___List[builtin___str]: ... + @classmethod + def values(cls) -> typing___List[Span.SpanKindValue]: ... + @classmethod + def items(cls) -> typing___List[typing___Tuple[builtin___str, Span.SpanKindValue]]: ... + SPAN_KIND_UNSPECIFIED = typing___cast(Span.SpanKindValue, 0) + INTERNAL = typing___cast(Span.SpanKindValue, 1) + SERVER = typing___cast(Span.SpanKindValue, 2) + CLIENT = typing___cast(Span.SpanKindValue, 3) + PRODUCER = typing___cast(Span.SpanKindValue, 4) + CONSUMER = typing___cast(Span.SpanKindValue, 5) + SPAN_KIND_UNSPECIFIED = typing___cast(Span.SpanKindValue, 0) + INTERNAL = typing___cast(Span.SpanKindValue, 1) + SERVER = typing___cast(Span.SpanKindValue, 2) + CLIENT = typing___cast(Span.SpanKindValue, 3) + PRODUCER = typing___cast(Span.SpanKindValue, 4) + CONSUMER = typing___cast(Span.SpanKindValue, 5) + type___SpanKind = SpanKind + + class Event(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + time_unix_nano: builtin___int = ... + name: typing___Text = ... + dropped_attributes_count: builtin___int = ... + + @property + def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]: ... + + def __init__(self, + *, + time_unix_nano : typing___Optional[builtin___int] = None, + name : typing___Optional[typing___Text] = None, + attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]] = None, + dropped_attributes_count : typing___Optional[builtin___int] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> Span.Event: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Span.Event: ... + def ClearField(self, field_name: typing_extensions___Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"name",b"name",u"time_unix_nano",b"time_unix_nano"]) -> None: ... + type___Event = Event + + class Link(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + trace_id: builtin___bytes = ... + span_id: builtin___bytes = ... + trace_state: typing___Text = ... + dropped_attributes_count: builtin___int = ... + + @property + def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]: ... + + def __init__(self, + *, + trace_id : typing___Optional[builtin___bytes] = None, + span_id : typing___Optional[builtin___bytes] = None, + trace_state : typing___Optional[typing___Text] = None, + attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]] = None, + dropped_attributes_count : typing___Optional[builtin___int] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> Span.Link: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Span.Link: ... + def ClearField(self, field_name: typing_extensions___Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"span_id",b"span_id",u"trace_id",b"trace_id",u"trace_state",b"trace_state"]) -> None: ... + type___Link = Link + + trace_id: builtin___bytes = ... + span_id: builtin___bytes = ... + trace_state: typing___Text = ... + parent_span_id: builtin___bytes = ... + name: typing___Text = ... + kind: type___Span.SpanKindValue = ... + start_time_unix_nano: builtin___int = ... + end_time_unix_nano: builtin___int = ... + dropped_attributes_count: builtin___int = ... + dropped_events_count: builtin___int = ... + dropped_links_count: builtin___int = ... + + @property + def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]: ... + + @property + def events(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Span.Event]: ... + + @property + def links(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Span.Link]: ... + + @property + def status(self) -> type___Status: ... + + def __init__(self, + *, + trace_id : typing___Optional[builtin___bytes] = None, + span_id : typing___Optional[builtin___bytes] = None, + trace_state : typing___Optional[typing___Text] = None, + parent_span_id : typing___Optional[builtin___bytes] = None, + name : typing___Optional[typing___Text] = None, + kind : typing___Optional[type___Span.SpanKindValue] = None, + start_time_unix_nano : typing___Optional[builtin___int] = None, + end_time_unix_nano : typing___Optional[builtin___int] = None, + attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]] = None, + dropped_attributes_count : typing___Optional[builtin___int] = None, + events : typing___Optional[typing___Iterable[type___Span.Event]] = None, + dropped_events_count : typing___Optional[builtin___int] = None, + links : typing___Optional[typing___Iterable[type___Span.Link]] = None, + dropped_links_count : typing___Optional[builtin___int] = None, + status : typing___Optional[type___Status] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> Span: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Span: ... + def HasField(self, field_name: typing_extensions___Literal[u"status",b"status"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"dropped_events_count",b"dropped_events_count",u"dropped_links_count",b"dropped_links_count",u"end_time_unix_nano",b"end_time_unix_nano",u"events",b"events",u"kind",b"kind",u"links",b"links",u"name",b"name",u"parent_span_id",b"parent_span_id",u"span_id",b"span_id",u"start_time_unix_nano",b"start_time_unix_nano",u"status",b"status",u"trace_id",b"trace_id",u"trace_state",b"trace_state"]) -> None: ... +type___Span = Span + +class Status(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + StatusCodeValue = typing___NewType('StatusCodeValue', builtin___int) + type___StatusCodeValue = StatusCodeValue + class StatusCode(object): + DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... + @classmethod + def Name(cls, number: builtin___int) -> builtin___str: ... + @classmethod + def Value(cls, name: builtin___str) -> Status.StatusCodeValue: ... + @classmethod + def keys(cls) -> typing___List[builtin___str]: ... + @classmethod + def values(cls) -> typing___List[Status.StatusCodeValue]: ... + @classmethod + def items(cls) -> typing___List[typing___Tuple[builtin___str, Status.StatusCodeValue]]: ... + Ok = typing___cast(Status.StatusCodeValue, 0) + Cancelled = typing___cast(Status.StatusCodeValue, 1) + UnknownError = typing___cast(Status.StatusCodeValue, 2) + InvalidArgument = typing___cast(Status.StatusCodeValue, 3) + DeadlineExceeded = typing___cast(Status.StatusCodeValue, 4) + NotFound = typing___cast(Status.StatusCodeValue, 5) + AlreadyExists = typing___cast(Status.StatusCodeValue, 6) + PermissionDenied = typing___cast(Status.StatusCodeValue, 7) + ResourceExhausted = typing___cast(Status.StatusCodeValue, 8) + FailedPrecondition = typing___cast(Status.StatusCodeValue, 9) + Aborted = typing___cast(Status.StatusCodeValue, 10) + OutOfRange = typing___cast(Status.StatusCodeValue, 11) + Unimplemented = typing___cast(Status.StatusCodeValue, 12) + InternalError = typing___cast(Status.StatusCodeValue, 13) + Unavailable = typing___cast(Status.StatusCodeValue, 14) + DataLoss = typing___cast(Status.StatusCodeValue, 15) + Unauthenticated = typing___cast(Status.StatusCodeValue, 16) + Ok = typing___cast(Status.StatusCodeValue, 0) + Cancelled = typing___cast(Status.StatusCodeValue, 1) + UnknownError = typing___cast(Status.StatusCodeValue, 2) + InvalidArgument = typing___cast(Status.StatusCodeValue, 3) + DeadlineExceeded = typing___cast(Status.StatusCodeValue, 4) + NotFound = typing___cast(Status.StatusCodeValue, 5) + AlreadyExists = typing___cast(Status.StatusCodeValue, 6) + PermissionDenied = typing___cast(Status.StatusCodeValue, 7) + ResourceExhausted = typing___cast(Status.StatusCodeValue, 8) + FailedPrecondition = typing___cast(Status.StatusCodeValue, 9) + Aborted = typing___cast(Status.StatusCodeValue, 10) + OutOfRange = typing___cast(Status.StatusCodeValue, 11) + Unimplemented = typing___cast(Status.StatusCodeValue, 12) + InternalError = typing___cast(Status.StatusCodeValue, 13) + Unavailable = typing___cast(Status.StatusCodeValue, 14) + DataLoss = typing___cast(Status.StatusCodeValue, 15) + Unauthenticated = typing___cast(Status.StatusCodeValue, 16) + type___StatusCode = StatusCode + + code: type___Status.StatusCodeValue = ... + message: typing___Text = ... + + def __init__(self, + *, + code : typing___Optional[type___Status.StatusCodeValue] = None, + message : typing___Optional[typing___Text] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> Status: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Status: ... + def ClearField(self, field_name: typing_extensions___Literal[u"code",b"code",u"message",b"message"]) -> None: ... +type___Status = Status diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh new file mode 100755 index 0000000000..c66371f42e --- /dev/null +++ b/scripts/proto_codegen.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# +# Regenerate python code from OTLP protos in +# https://github.com/open-telemetry/opentelemetry-proto +# +# To use, update PROTO_REPO_BRANCH_OR_COMMIT variable below to a commit hash or +# tag in opentelemtry-proto repo that you want to build off of. Then, just run +# this script to update the proto files. Commit the changes as well as any +# fixes needed in the OTLP exporter. +# +# Optional envars: +# PROTO_REPO_DIR - the path to an existing checkout of the opentelemetry-proto repo + +# Pinned commit/branch/tag for the current version used in opentelemetry-proto python package. +PROTO_REPO_BRANCH_OR_COMMIT="b54688569186e0b862bf7462a983ccf2c50c0547" + +set -e + +if [ -z "$VIRTUAL_ENV" ]; then + echo '$VIRTUAL_ENV is not set, you probably forgot to source it. Exiting...' + exit 1 +fi + +PROTO_REPO_DIR=${PROTO_REPO_DIR:-"/tmp/opentelemetry-proto"} +# root of opentelemetry-python repo +repo_root="$(git rev-parse --show-toplevel)" + +python -m pip install -r $repo_root/dev-requirements.txt + +# Clone the proto repo if it doesn't exist +if [ ! -d "$PROTO_REPO_DIR" ]; then + git clone https://github.com/open-telemetry/opentelemetry-proto.git $PROTO_REPO_DIR +fi + +# Pull in changes and switch to requested branch +( + cd $PROTO_REPO_DIR + git fetch --all + git checkout $PROTO_REPO_BRANCH_OR_COMMIT + # pull if PROTO_REPO_BRANCH_OR_COMMIT is not a detached head + git symbolic-ref -q HEAD && git pull --ff-only || true +) + +cd $repo_root/opentelemetry-proto/src + +# clean up old generated code +find opentelemetry/ -regex ".*_pb2.*\.pyi?" -exec rm {} + + +# generate proto code for all protos +all_protos=$(find $PROTO_REPO_DIR/ -iname "*.proto") +python -m grpc_tools.protoc \ + -I $PROTO_REPO_DIR \ + --python_out=. \ + --mypy_out=. \ + $all_protos + +# generate grpc output only for protos with service definitions +service_protos=$(grep -REl "service \w+ {" $PROTO_REPO_DIR/opentelemetry/) +python -m grpc_tools.protoc \ + -I $PROTO_REPO_DIR \ + --python_out=. \ + --mypy_out=. \ + --grpc_python_out=. \ + $service_protos From b2c0d5356538b30ed336b8e2775de5c9556e03c1 Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Mon, 22 Jun 2020 15:48:15 -0400 Subject: [PATCH 0429/1517] add gco agent span label (#833) --- .../exporter/cloud_trace/__init__.py | 26 +++++++++++++++---- .../tests/test_cloud_trace_exporter.py | 20 ++++++++++++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py index 31e20d997e..f6d1e78b5a 100644 --- a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py @@ -44,6 +44,7 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple import google.auth +import pkg_resources from google.cloud.trace_v2 import TraceServiceClient from google.cloud.trace_v2.proto.trace_pb2 import AttributeValue from google.cloud.trace_v2.proto.trace_pb2 import Span as ProtoSpan @@ -51,6 +52,9 @@ from google.rpc.status_pb2 import Status import opentelemetry.trace as trace_api +from opentelemetry.exporter.cloud_trace.version import ( + __version__ as cloud_trace_version, +) from opentelemetry.sdk.trace import Event from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult from opentelemetry.sdk.util import BoundedDict @@ -157,7 +161,7 @@ def _translate_to_cloud_trace( "end_time": end_time, "parent_span_id": parent_id, "attributes": _extract_attributes( - span.attributes, MAX_SPAN_ATTRS + span.attributes, MAX_SPAN_ATTRS, add_agent_attr=True ), "links": _extract_links(span.links), "status": _extract_status(span.status), @@ -288,20 +292,32 @@ def _extract_events(events: Sequence[Event]) -> ProtoSpan.TimeEvents: def _extract_attributes( - attrs: types.Attributes, num_attrs_limit: int + attrs: types.Attributes, + num_attrs_limit: int, + add_agent_attr: bool = False, ) -> ProtoSpan.Attributes: """Convert span.attributes to dict.""" attributes_dict = BoundedDict(num_attrs_limit) - + invalid_value_dropped_count = 0 for key, value in attrs.items(): key = _truncate_str(key, 128)[0] value = _format_attribute_value(value) - if value is not None: + if value: attributes_dict[key] = value + else: + invalid_value_dropped_count += 1 + if add_agent_attr: + attributes_dict["g.co/agent"] = _format_attribute_value( + "opentelemetry-python {}; google-cloud-trace-exporter {}".format( + pkg_resources.get_distribution("opentelemetry-sdk").version, + cloud_trace_version, + ) + ) return ProtoSpan.Attributes( attribute_map=attributes_dict, - dropped_attributes_count=len(attrs) - len(attributes_dict), + dropped_attributes_count=attributes_dict.dropped + + invalid_value_dropped_count, ) diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py index 5ebd5f3b64..bd5a9f073d 100644 --- a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py +++ b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py @@ -15,6 +15,7 @@ import unittest from unittest import mock +import pkg_resources from google.cloud.trace_v2.proto.trace_pb2 import AttributeValue from google.cloud.trace_v2.proto.trace_pb2 import Span as ProtoSpan from google.cloud.trace_v2.proto.trace_pb2 import TruncatableString @@ -33,7 +34,11 @@ _format_attribute_value, _truncate_str, ) -from opentelemetry.sdk.trace import Event, Span +from opentelemetry.exporter.cloud_trace.version import ( + __version__ as cloud_trace_version, +) +from opentelemetry.sdk.trace import Event +from opentelemetry.sdk.trace.export import Span from opentelemetry.trace import Link, SpanContext, SpanKind from opentelemetry.trace.status import Status as SpanStatus from opentelemetry.trace.status import StatusCanonicalCode @@ -108,7 +113,18 @@ def test_export(self): "display_name": TruncatableString( value="span_name", truncated_byte_count=0 ), - "attributes": ProtoSpan.Attributes(attribute_map={}), + "attributes": ProtoSpan.Attributes( + attribute_map={ + "g.co/agent": _format_attribute_value( + "opentelemetry-python {}; google-cloud-trace-exporter {}".format( + pkg_resources.get_distribution( + "opentelemetry-sdk" + ).version, + cloud_trace_version, + ) + ) + } + ), "links": None, "status": None, "time_events": None, From 3c9f99d313f1dc88d8a57c088739ef495257bc94 Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Mon, 22 Jun 2020 22:24:07 -0400 Subject: [PATCH 0430/1517] feat(wsgi): low cardinality span name and name override (#837) --- .../tests/test_programmatic.py | 2 +- .../tests/test_programmatic.py | 2 +- .../src/opentelemetry/ext/wsgi/__init__.py | 27 +++++---- .../tests/test_wsgi_middleware.py | 56 +++++++++++++------ 4 files changed, 56 insertions(+), 31 deletions(-) diff --git a/ext/opentelemetry-ext-flask/tests/test_programmatic.py b/ext/opentelemetry-ext-flask/tests/test_programmatic.py index 95f82373e3..30fa9c4eb2 100644 --- a/ext/opentelemetry-ext-flask/tests/test_programmatic.py +++ b/ext/opentelemetry-ext-flask/tests/test_programmatic.py @@ -118,7 +118,7 @@ def test_404(self): resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "/bye") + self.assertEqual(span_list[0].name, "HTTP POST") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) diff --git a/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py b/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py index b1ecbb38bb..e8937e8f5e 100644 --- a/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py +++ b/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py @@ -101,7 +101,7 @@ def test_404(self): resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "/bye") + self.assertEqual(span_list[0].name, "HTTP POST") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index c5b0216869..9968a0c1c8 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -93,12 +93,15 @@ def collect_request_attributes(environ): result = { "component": "http", - "http.method": environ["REQUEST_METHOD"], - "http.server_name": environ["SERVER_NAME"], - "http.scheme": environ["wsgi.url_scheme"], - "host.port": int(environ["SERVER_PORT"]), + "http.method": environ.get("REQUEST_METHOD"), + "http.server_name": environ.get("SERVER_NAME"), + "http.scheme": environ.get("wsgi.url_scheme"), } + host_port = environ.get("SERVER_PORT") + if host_port is not None: + result.update({"host.port": int(host_port)}) + setifnotnone(result, "http.host", environ.get("HTTP_HOST")) target = environ.get("RAW_URI") if target is None: # Note: `"" or None is None` @@ -149,12 +152,8 @@ def add_response_attributes( def get_default_span_name(environ): - """Calculates a (generic) span name for an incoming HTTP request based on the PEP3333 conforming WSGI environ.""" - - # TODO: Update once - # https://github.com/open-telemetry/opentelemetry-specification/issues/270 - # is resolved - return environ.get("PATH_INFO", "/") + """Default implementation for name_callback, returns HTTP {METHOD_NAME}.""" + return "HTTP {}".format(environ.get("REQUEST_METHOD", "")).strip() class OpenTelemetryMiddleware: @@ -165,11 +164,15 @@ class OpenTelemetryMiddleware: Args: wsgi: The WSGI application callable to forward requests to. + name_callback: Callback which calculates a generic span name for an + incoming HTTP request based on the PEP3333 WSGI environ. + Optional: Defaults to get_default_span_name. """ - def __init__(self, wsgi): + def __init__(self, wsgi, name_callback=get_default_span_name): self.wsgi = wsgi self.tracer = trace.get_tracer(__name__, __version__) + self.name_callback = name_callback @staticmethod def _create_start_response(span, start_response): @@ -191,7 +194,7 @@ def __call__(self, environ, start_response): token = context.attach( propagators.extract(get_header_from_environ, environ) ) - span_name = get_default_span_name(environ) + span_name = self.name_callback(environ) span = self.tracer.start_span( span_name, diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index e56410b612..0d018b6841 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -74,7 +74,9 @@ def error_wsgi(environ, start_response): class TestWsgiApplication(WsgiTestBase): - def validate_response(self, response, error=None): + def validate_response( + self, response, error=None, span_name="HTTP GET", http_method="GET" + ): while True: try: value = next(response) @@ -95,23 +97,22 @@ def validate_response(self, response, error=None): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "/") + self.assertEqual(span_list[0].name, span_name) self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) - self.assertEqual( - span_list[0].attributes, - { - "component": "http", - "http.method": "GET", - "http.server_name": "127.0.0.1", - "http.scheme": "http", - "host.port": 80, - "http.host": "127.0.0.1", - "http.flavor": "1.0", - "http.url": "http://127.0.0.1/", - "http.status_text": "OK", - "http.status_code": 200, - }, - ) + expected_attributes = { + "component": "http", + "http.server_name": "127.0.0.1", + "http.scheme": "http", + "host.port": 80, + "http.host": "127.0.0.1", + "http.flavor": "1.0", + "http.url": "http://127.0.0.1/", + "http.status_text": "OK", + "http.status_code": 200, + } + if http_method is not None: + expected_attributes["http.method"] = http_method + self.assertEqual(span_list[0].attributes, expected_attributes) def test_basic_wsgi_call(self): app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) @@ -147,6 +148,27 @@ def test_wsgi_exc_info(self): response = app(self.environ, self.start_response) self.validate_response(response, error=ValueError) + def test_override_span_name(self): + """Test that span_names can be overwritten by our callback function.""" + span_name = "Dymaxion" + + def get_predefined_span_name(scope): + # pylint: disable=unused-argument + return span_name + + app = otel_wsgi.OpenTelemetryMiddleware( + simple_wsgi, name_callback=get_predefined_span_name + ) + response = app(self.environ, self.start_response) + self.validate_response(response, span_name=span_name) + + def test_default_span_name_missing_request_method(self): + """Test that default span_names with missing request method.""" + self.environ.pop("REQUEST_METHOD") + app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) + response = app(self.environ, self.start_response) + self.validate_response(response, span_name="HTTP", http_method=None) + class TestWsgiAttributes(unittest.TestCase): def setUp(self): From d35e6268ada565ee4dba851e65a65986824cc856 Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Tue, 23 Jun 2020 01:00:49 -0400 Subject: [PATCH 0431/1517] Add unique identifier to Cloud Monitoring exporter (#841) --- docs/examples/cloud_monitoring/README.rst | 13 +++++ .../exporter/cloud_monitoring/__init__.py | 39 ++++++++++++-- .../tests/test_cloud_monitoring.py | 53 +++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/docs/examples/cloud_monitoring/README.rst b/docs/examples/cloud_monitoring/README.rst index 8b2ccedcee..818446dc15 100644 --- a/docs/examples/cloud_monitoring/README.rst +++ b/docs/examples/cloud_monitoring/README.rst @@ -33,3 +33,16 @@ After running the example: * Go to the `Cloud Monitoring Metrics Explorer page `_. * In "Find resource type and metric" enter "OpenTelemetry/request_counter". * You can filter by labels and change the graphical output here as well. + +Troubleshooting +-------------------------- + +``One or more points were written more frequently than the maximum sampling period configured for the metric`` +############################################################################################################## + +Currently, Cloud Monitoring allows one write every 10 seconds for any unique tuple (metric_name, metric_label_value_1, metric_label_value_2, ...). The exporter should rate limit on its own but issues arise if: + + * You are restarting the server more than once every 10 seconds. + * You have a multiple exporters (possibly on different threads) writing to the same tuple. + +For both cases, you can pass ``add_unique_identifier=True`` to the CloudMonitoringMetricsExporter constructor. This adds a UUID label_value, making the tuple unique again. For the first case, you can also choose to just wait longer than 10 seconds between restarts. diff --git a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py index 072301a274..8e71e5b250 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py +++ b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py @@ -1,4 +1,5 @@ import logging +import random from typing import Optional, Sequence import google.auth @@ -17,14 +18,30 @@ logger = logging.getLogger(__name__) MAX_BATCH_WRITE = 200 WRITE_INTERVAL = 10 +UNIQUE_IDENTIFIER_KEY = "opentelemetry_id" # pylint is unable to resolve members of protobuf objects # pylint: disable=no-member class CloudMonitoringMetricsExporter(MetricsExporter): - """ Implementation of Metrics Exporter to Google Cloud Monitoring""" - - def __init__(self, project_id=None, client=None): + """ Implementation of Metrics Exporter to Google Cloud Monitoring + + You can manually pass in project_id and client, or else the + Exporter will take that information from Application Default + Credentials. + + Args: + project_id: project id of your Google Cloud project. + client: Client to upload metrics to Google Cloud Monitoring. + add_unique_identifier: Add an identifier to each exporter metric. This + must be used when there exist two (or more) exporters that may + export to the same metric name within WRITE_INTERVAL seconds of + each other. + """ + + def __init__( + self, project_id=None, client=None, add_unique_identifier=False + ): self.client = client or MetricServiceClient() if not project_id: _, self.project_id = google.auth.default() @@ -33,6 +50,11 @@ def __init__(self, project_id=None, client=None): self.project_name = self.client.project_path(self.project_id) self._metric_descriptors = {} self._last_updated = {} + self.unique_identifier = None + if add_unique_identifier: + self.unique_identifier = "{:08x}".format( + random.randint(0, 16 ** 8) + ) def _add_resource_info(self, series: TimeSeries) -> None: """Add Google resource specific information (e.g. instance id, region). @@ -97,6 +119,12 @@ def _get_metric_descriptor( logger.warning( "Label value %s is not a string, bool or integer", value ) + + if self.unique_identifier: + descriptor["labels"].append( + LabelDescriptor(key=UNIQUE_IDENTIFIER_KEY, value_type="STRING") + ) + if isinstance(record.aggregator, SumAggregator): descriptor["metric_kind"] = MetricDescriptor.MetricKind.GAUGE else: @@ -141,6 +169,11 @@ def export( for key, value in record.labels: series.metric.labels[key] = str(value) + if self.unique_identifier: + series.metric.labels[ + UNIQUE_IDENTIFIER_KEY + ] = self.unique_identifier + point = series.points.add() if instrument.value_type == int: point.value.int64_value = record.aggregator.checkpoint diff --git a/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py b/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py index ccc90d2c2f..1b36661699 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py +++ b/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py @@ -21,6 +21,7 @@ from opentelemetry.exporter.cloud_monitoring import ( MAX_BATCH_WRITE, + UNIQUE_IDENTIFIER_KEY, WRITE_INTERVAL, CloudMonitoringMetricsExporter, ) @@ -289,3 +290,55 @@ def test_export(self): mock.call(self.project_name, [series3]), ] ) + + def test_unique_identifier(self): + client = mock.Mock() + exporter1 = CloudMonitoringMetricsExporter( + project_id=self.project_id, + client=client, + add_unique_identifier=True, + ) + exporter2 = CloudMonitoringMetricsExporter( + project_id=self.project_id, + client=client, + add_unique_identifier=True, + ) + exporter1.project_name = self.project_name + exporter2.project_name = self.project_name + + client.create_metric_descriptor.return_value = MetricDescriptor( + **{ + "name": None, + "type": "custom.googleapis.com/OpenTelemetry/name", + "display_name": "name", + "description": "description", + "labels": [ + LabelDescriptor( + key=UNIQUE_IDENTIFIER_KEY, value_type="STRING" + ), + ], + "metric_kind": "GAUGE", + "value_type": "DOUBLE", + } + ) + + sum_agg_one = SumAggregator() + sum_agg_one.update(1) + metric_record = MetricRecord(MockMetric(), (), sum_agg_one,) + exporter1.export([metric_record]) + exporter2.export([metric_record]) + + ( + first_call, + second_call, + ) = client.create_metric_descriptor.call_args_list + self.assertEqual(first_call[0][1].labels[0].key, UNIQUE_IDENTIFIER_KEY) + self.assertEqual( + second_call[0][1].labels[0].key, UNIQUE_IDENTIFIER_KEY + ) + + first_call, second_call = client.create_time_series.call_args_list + self.assertNotEqual( + first_call[0][1][0].metric.labels[UNIQUE_IDENTIFIER_KEY], + second_call[0][1][0].metric.labels[UNIQUE_IDENTIFIER_KEY], + ) From 8be986cd6c94368ed769149022a7862f0ca47f35 Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Tue, 23 Jun 2020 17:06:11 -0400 Subject: [PATCH 0432/1517] add changelog for cloud exporters (#849) --- .../CHANGELOG.md | 14 ++++++++++++++ .../CHANGELOG.md | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md create mode 100644 ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md diff --git a/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md b/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md new file mode 100644 index 0000000000..7968d01ce4 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +## Unreleased + +- Add ability for exporter to add unique identifier + ([#841](https://github.com/open-telemetry/opentelemetry-python/pull/841)) +- Added tests to tox coverage files + ([#804](https://github.com/open-telemetry/opentelemetry-python/pull/804)) + +## 0.9b0 + +Released 2020-06-10 + +- Initial release diff --git a/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md b/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md new file mode 100644 index 0000000000..71f4379b35 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +## Unreleased + +- Add g.co/agent label for Google internal metrics tracking + ([#833](https://github.com/open-telemetry/opentelemetry-python/pull/833)) +- Adding trouble-shooting tips + ([#827](https://github.com/open-telemetry/opentelemetry-python/pull/827)) +- Added Cloud Trace context propagation + ([#819](https://github.com/open-telemetry/opentelemetry-python/pull/819)) +- Added tests to tox coverage files + ([#804](https://github.com/open-telemetry/opentelemetry-python/pull/804)) + +## 0.9b0 + +Released 2020-06-10 + +- Initial release From f8e947c66fc7ab69d8969a2e9564040b0bea4ae4 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 24 Jun 2020 21:25:17 -0700 Subject: [PATCH 0433/1517] chore: v0.10b0 master update + dev version bump (#850) --- docs/examples/opentelemetry-example-app/setup.cfg | 8 ++++---- .../src/opentelemetry_example_app/version.py | 2 +- ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md | 6 +++++- .../opentelemetry/exporter/cloud_monitoring/version.py | 2 +- ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md | 4 ++++ .../src/opentelemetry/exporter/cloud_trace/version.py | 2 +- ext/opentelemetry-ext-aiohttp-client/setup.cfg | 4 ++-- .../src/opentelemetry/ext/aiohttp_client/version.py | 2 +- ext/opentelemetry-ext-asgi/setup.cfg | 4 ++-- .../src/opentelemetry/ext/asgi/version.py | 2 +- ext/opentelemetry-ext-asyncpg/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-asyncpg/setup.cfg | 6 +++--- .../src/opentelemetry/ext/asyncpg/version.py | 2 +- ext/opentelemetry-ext-boto/setup.cfg | 6 +++--- .../src/opentelemetry/ext/boto/version.py | 2 +- ext/opentelemetry-ext-botocore/setup.cfg | 6 +++--- .../src/opentelemetry/ext/botocore/version.py | 2 +- ext/opentelemetry-ext-celery/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-celery/setup.cfg | 6 +++--- .../src/opentelemetry/ext/celery/version.py | 2 +- .../src/opentelemetry/ext/datadog/version.py | 2 +- ext/opentelemetry-ext-dbapi/setup.cfg | 6 +++--- .../src/opentelemetry/ext/dbapi/version.py | 2 +- ext/opentelemetry-ext-django/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/django/version.py | 2 +- ext/opentelemetry-ext-elasticsearch/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-elasticsearch/setup.cfg | 6 +++--- .../src/opentelemetry/ext/elasticsearch/version.py | 2 +- ext/opentelemetry-ext-flask/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/flask/version.py | 2 +- ext/opentelemetry-ext-grpc/setup.cfg | 6 +++--- .../src/opentelemetry/ext/grpc/version.py | 2 +- ext/opentelemetry-ext-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/ext/jaeger/version.py | 2 +- ext/opentelemetry-ext-jinja2/setup.cfg | 6 +++--- .../src/opentelemetry/ext/jinja2/version.py | 2 +- ext/opentelemetry-ext-mysql/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/mysql/version.py | 2 +- ext/opentelemetry-ext-opencensusexporter/setup.cfg | 4 ++-- .../src/opentelemetry/ext/opencensusexporter/version.py | 2 +- ext/opentelemetry-ext-opentracing-shim/setup.cfg | 4 ++-- .../src/opentelemetry/ext/opentracing_shim/version.py | 2 +- ext/opentelemetry-ext-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/ext/otlp/version.py | 2 +- ext/opentelemetry-ext-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/ext/prometheus/version.py | 2 +- ext/opentelemetry-ext-psycopg2/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/psycopg2/version.py | 2 +- ext/opentelemetry-ext-pymemcache/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-pymemcache/setup.cfg | 6 +++--- .../src/opentelemetry/ext/pymemcache/version.py | 2 +- ext/opentelemetry-ext-pymongo/setup.cfg | 6 +++--- .../src/opentelemetry/ext/pymongo/version.py | 2 +- ext/opentelemetry-ext-pymysql/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/pymysql/version.py | 2 +- ext/opentelemetry-ext-pyramid/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/pyramid/version.py | 2 +- ext/opentelemetry-ext-redis/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/redis/version.py | 2 +- ext/opentelemetry-ext-requests/setup.cfg | 6 +++--- .../src/opentelemetry/ext/requests/version.py | 2 +- ext/opentelemetry-ext-sqlalchemy/setup.cfg | 6 +++--- .../src/opentelemetry/ext/sqlalchemy/version.py | 2 +- ext/opentelemetry-ext-sqlite3/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/sqlite3/version.py | 2 +- ext/opentelemetry-ext-system-metrics/setup.cfg | 4 ++-- .../src/opentelemetry/ext/system_metrics/version.py | 2 +- ext/opentelemetry-ext-wsgi/setup.cfg | 6 +++--- .../src/opentelemetry/ext/wsgi/version.py | 2 +- ext/opentelemetry-ext-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/ext/zipkin/version.py | 2 +- ext/opentelemetry-instrumentation-starlette/CHANGELOG.md | 4 ++++ ext/opentelemetry-instrumentation-starlette/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/starlette/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/CHANGELOG.md | 4 ++++ opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 4 ++++ opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 83 files changed, 176 insertions(+), 140 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index 88aa507518..185229dc92 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -43,10 +43,10 @@ zip_safe = False include_package_data = True install_requires = typing; python_version<'3.5' - opentelemetry-api == 0.10.dev0 - opentelemetry-sdk == 0.10.dev0 - opentelemetry-ext-requests == 0.10.dev0 - opentelemetry-ext-flask == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-sdk == 0.11.dev0 + opentelemetry-ext-requests == 0.11.dev0 + opentelemetry-ext-flask == 0.11.dev0 flask requests protobuf~=3.11 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py index 6d4fefa599..858e73960f 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md b/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md index 7968d01ce4..d51e8aa749 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md +++ b/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md @@ -2,7 +2,11 @@ ## Unreleased -- Add ability for exporter to add unique identifier +## Version 0.10b0 + +Released 2020-06-23 + +- Add ability for exporter to add unique identifier ([#841](https://github.com/open-telemetry/opentelemetry-python/pull/841)) - Added tests to tox coverage files ([#804](https://github.com/open-telemetry/opentelemetry-python/pull/804)) diff --git a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py index 3096170ce8..8d3a82b3e5 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py +++ b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md b/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md index 71f4379b35..5ff312743f 100644 --- a/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md +++ b/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.10b0 + +Released 2020-06-23 + - Add g.co/agent label for Google internal metrics tracking ([#833](https://github.com/open-telemetry/opentelemetry-python/pull/833)) - Adding trouble-shooting tips diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py index 3096170ce8..8d3a82b3e5 100644 --- a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-aiohttp-client/setup.cfg b/ext/opentelemetry-ext-aiohttp-client/setup.cfg index 431e92273e..157b691803 100644 --- a/ext/opentelemetry-ext-aiohttp-client/setup.cfg +++ b/ext/opentelemetry-ext-aiohttp-client/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api >= 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 aiohttp ~= 3.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py index 63ed5cd81a..5924968fb2 100644 --- a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py +++ b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-asgi/setup.cfg b/ext/opentelemetry-ext-asgi/setup.cfg index ab0e3e7f47..75b4c12f83 100644 --- a/ext/opentelemetry-ext-asgi/setup.cfg +++ b/ext/opentelemetry-ext-asgi/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 asgiref ~= 3.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py +++ b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-asyncpg/CHANGELOG.md b/ext/opentelemetry-ext-asyncpg/CHANGELOG.md index f4390b09ab..e81079bfb8 100644 --- a/ext/opentelemetry-ext-asyncpg/CHANGELOG.md +++ b/ext/opentelemetry-ext-asyncpg/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## Version 0.10b0 + +Released 2020-06-23 + - Initial Release ([#814](https://github.com/open-telemetry/opentelemetry-python/pull/814)) diff --git a/ext/opentelemetry-ext-asyncpg/setup.cfg b/ext/opentelemetry-ext-asyncpg/setup.cfg index df00ed7db4..786e69f6ee 100644 --- a/ext/opentelemetry-ext-asyncpg/setup.cfg +++ b/ext/opentelemetry-ext-asyncpg/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 asyncpg >= 0.12.0 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py b/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py +++ b/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-boto/setup.cfg b/ext/opentelemetry-ext-boto/setup.cfg index 53e26fcf14..64a9932756 100644 --- a/ext/opentelemetry-ext-boto/setup.cfg +++ b/ext/opentelemetry-ext-boto/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = boto ~= 2.0 - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 [options.extras_require] test = boto~=2.0 moto~=1.0 - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-botocore/setup.cfg b/ext/opentelemetry-ext-botocore/setup.cfg index 59cb7f2271..9e0aabfd23 100644 --- a/ext/opentelemetry-ext-botocore/setup.cfg +++ b/ext/opentelemetry-ext-botocore/setup.cfg @@ -41,13 +41,13 @@ package_dir= packages=find_namespace: install_requires = botocore ~= 1.0 - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 [options.extras_require] test = moto ~= 1.0 - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py +++ b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-celery/CHANGELOG.md b/ext/opentelemetry-ext-celery/CHANGELOG.md index dd6bf18c9d..54990c1955 100644 --- a/ext/opentelemetry-ext-celery/CHANGELOG.md +++ b/ext/opentelemetry-ext-celery/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## Version 0.10b0 + +Released 2020-06-23 + - Add instrumentation for Celery ([#780](https://github.com/open-telemetry/opentelemetry-python/pull/780)) diff --git a/ext/opentelemetry-ext-celery/setup.cfg b/ext/opentelemetry-ext-celery/setup.cfg index ecb42b7fb7..67609b2c2c 100644 --- a/ext/opentelemetry-ext-celery/setup.cfg +++ b/ext/opentelemetry-ext-celery/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 celery ~= 4.0 [options.extras_require] test = pytest - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py b/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py +++ b/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 4ea5913e17..bf7af968f7 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-django/setup.cfg b/ext/opentelemetry-ext-django/setup.cfg index 8ed134ff19..e9cea61ced 100644 --- a/ext/opentelemetry-ext-django/setup.cfg +++ b/ext/opentelemetry-ext-django/setup.cfg @@ -41,13 +41,13 @@ package_dir= packages=find_namespace: install_requires = django >= 1.10 - opentelemetry-ext-wsgi == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 - opentelemetry-api == 0.10.dev0 + opentelemetry-ext-wsgi == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11.dev0 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-elasticsearch/CHANGELOG.md b/ext/opentelemetry-ext-elasticsearch/CHANGELOG.md index 33144da913..7425aa5e1e 100644 --- a/ext/opentelemetry-ext-elasticsearch/CHANGELOG.md +++ b/ext/opentelemetry-ext-elasticsearch/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## Version 0.10b0 + +Released 2020-06-23 + - Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-elasticsearch/setup.cfg b/ext/opentelemetry-ext-elasticsearch/setup.cfg index 07be1c622e..40f3b392b4 100644 --- a/ext/opentelemetry-ext-elasticsearch/setup.cfg +++ b/ext/opentelemetry-ext-elasticsearch/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 wrapt >= 1.0.0, < 2.0.0 elasticsearch >= 2.0 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 elasticsearch-dsl >= 2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py b/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py +++ b/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index 4c2602a7b2..663b2169a5 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-ext-wsgi == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 - opentelemetry-api == 0.10.dev0 + opentelemetry-ext-wsgi == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11.dev0 [options.extras_require] test = flask~=1.0 - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index 41042ce1b8..012b3541d2 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 + opentelemetry-api == 0.11.dev0 grpcio ~= 1.27 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 - opentelemetry-sdk == 0.10.dev0 + opentelemetry-test == 0.11.dev0 + opentelemetry-sdk == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/ext/opentelemetry-ext-jaeger/setup.cfg index e213e4558e..c19107eedd 100644 --- a/ext/opentelemetry-ext-jaeger/setup.cfg +++ b/ext/opentelemetry-ext-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.10.dev0 - opentelemetry-sdk == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-sdk == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index d21fcb8168..60f2f9e9d2 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-jinja2/setup.cfg b/ext/opentelemetry-ext-jinja2/setup.cfg index 41d6ff2fb8..54a7515150 100644 --- a/ext/opentelemetry-ext-jinja2/setup.cfg +++ b/ext/opentelemetry-ext-jinja2/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py +++ b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index a00193b2f2..941c425343 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-ext-dbapi == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-ext-dbapi == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-opencensusexporter/setup.cfg b/ext/opentelemetry-ext-opencensusexporter/setup.cfg index c85f3b3d00..ea963dce18 100644 --- a/ext/opentelemetry-ext-opencensusexporter/setup.cfg +++ b/ext/opentelemetry-ext-opencensusexporter/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.10.dev0 - opentelemetry-sdk == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-sdk == 0.11.dev0 protobuf >= 3.8.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py +++ b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg index 7f6b093ea0..abc052a5aa 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.10.dev0 + opentelemetry-api == 0.11.dev0 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-otlp/setup.cfg b/ext/opentelemetry-ext-otlp/setup.cfg index 57fecc6fed..65908e0a3c 100644 --- a/ext/opentelemetry-ext-otlp/setup.cfg +++ b/ext/opentelemetry-ext-otlp/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.10.dev0 - opentelemetry-sdk == 0.10.dev0 - opentelemetry-proto == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-sdk == 0.11.dev0 + opentelemetry-proto == 0.11.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py +++ b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/ext/opentelemetry-ext-prometheus/setup.cfg index 3e5d5cd957..5dae17f5cb 100644 --- a/ext/opentelemetry-ext-prometheus/setup.cfg +++ b/ext/opentelemetry-ext-prometheus/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.10.dev0 - opentelemetry-sdk == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-sdk == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index 252ff290d8..bb0fd9a57c 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-ext-dbapi == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-ext-dbapi == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-pymemcache/CHANGELOG.md b/ext/opentelemetry-ext-pymemcache/CHANGELOG.md index 33144da913..7425aa5e1e 100644 --- a/ext/opentelemetry-ext-pymemcache/CHANGELOG.md +++ b/ext/opentelemetry-ext-pymemcache/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## Version 0.10b0 + +Released 2020-06-23 + - Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-pymemcache/setup.cfg b/ext/opentelemetry-ext-pymemcache/setup.cfg index 631d47579b..c2252274b1 100644 --- a/ext/opentelemetry-ext-pymemcache/setup.cfg +++ b/ext/opentelemetry-ext-pymemcache/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 pymemcache ~= 1.3 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py b/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py +++ b/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index 6cb5c8cc08..5e08d98776 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 pymongo ~= 3.1 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-pymysql/setup.cfg b/ext/opentelemetry-ext-pymysql/setup.cfg index 031a6562b3..db28ef9581 100644 --- a/ext/opentelemetry-ext-pymysql/setup.cfg +++ b/ext/opentelemetry-ext-pymysql/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-ext-dbapi == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-ext-dbapi == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 PyMySQL ~= 0.9.3 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-pyramid/setup.cfg b/ext/opentelemetry-ext-pyramid/setup.cfg index 521f7c85ee..bcdf850e87 100644 --- a/ext/opentelemetry-ext-pyramid/setup.cfg +++ b/ext/opentelemetry-ext-pyramid/setup.cfg @@ -41,15 +41,15 @@ package_dir= packages=find_namespace: install_requires = pyramid >= 1.7 - opentelemetry-instrumentation == 0.10.dev0 - opentelemetry-api == 0.10.dev0 - opentelemetry-ext-wsgi == 0.10.dev0 + opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-ext-wsgi == 0.11.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = werkzeug == 0.16.1 - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py +++ b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-redis/setup.cfg b/ext/opentelemetry-ext-redis/setup.cfg index f6fbbafaaa..26159c6568 100644 --- a/ext/opentelemetry-ext-redis/setup.cfg +++ b/ext/opentelemetry-ext-redis/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 redis >= 2.6 wrapt >= 1.12.1 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 - opentelemetry-sdk == 0.10.dev0 + opentelemetry-test == 0.11.dev0 + opentelemetry-sdk == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py +++ b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-requests/setup.cfg b/ext/opentelemetry-ext-requests/setup.cfg index 7bb84b42ba..714a1b3231 100644 --- a/ext/opentelemetry-ext-requests/setup.cfg +++ b/ext/opentelemetry-ext-requests/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 requests ~= 2.0 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 httpretty ~= 1.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-sqlalchemy/setup.cfg b/ext/opentelemetry-ext-sqlalchemy/setup.cfg index c964b1327b..76178b8245 100644 --- a/ext/opentelemetry-ext-sqlalchemy/setup.cfg +++ b/ext/opentelemetry-ext-sqlalchemy/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 wrapt >= 1.11.2 sqlalchemy [options.extras_require] test = - opentelemetry-sdk == 0.10.dev0 + opentelemetry-sdk == 0.11.dev0 pytest [options.packages.find] diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py +++ b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-sqlite3/setup.cfg b/ext/opentelemetry-ext-sqlite3/setup.cfg index a1128ca88f..95f60988ea 100644 --- a/ext/opentelemetry-ext-sqlite3/setup.cfg +++ b/ext/opentelemetry-ext-sqlite3/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-ext-dbapi == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-ext-dbapi == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py +++ b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-system-metrics/setup.cfg b/ext/opentelemetry-ext-system-metrics/setup.cfg index 063e7e91b2..4006290514 100644 --- a/ext/opentelemetry-ext-system-metrics/setup.cfg +++ b/ext/opentelemetry-ext-system-metrics/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 + opentelemetry-api == 0.11.dev0 psutil ~= 5.7.0 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py +++ b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index 48b0697d11..e7f3087053 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-instrumentation == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg index 89d40aaeaa..164f0d84c2 100644 --- a/ext/opentelemetry-ext-zipkin/setup.cfg +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 0.10.dev0 - opentelemetry-sdk == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-sdk == 0.11.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-instrumentation-starlette/CHANGELOG.md b/ext/opentelemetry-instrumentation-starlette/CHANGELOG.md index f7132ca830..1991025f6c 100644 --- a/ext/opentelemetry-instrumentation-starlette/CHANGELOG.md +++ b/ext/opentelemetry-instrumentation-starlette/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## Version 0.10b0 + +Released 2020-06-23 + - Initial release ([#777](https://github.com/open-telemetry/opentelemetry-python/pull/777)) \ No newline at end of file diff --git a/ext/opentelemetry-instrumentation-starlette/setup.cfg b/ext/opentelemetry-instrumentation-starlette/setup.cfg index 7659c9fadb..cea0cc1188 100644 --- a/ext/opentelemetry-instrumentation-starlette/setup.cfg +++ b/ext/opentelemetry-instrumentation-starlette/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.10.dev0 - opentelemetry-ext-asgi == 0.10.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-ext-asgi == 0.11.dev0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.10.dev0 + opentelemetry-test == 0.11.dev0 starlette ~= 0.13.0 requests ~= 2.23.0 # needed for testclient diff --git a/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py index 6d4fefa599..858e73960f 100644 --- a/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py +++ b/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 6d4fefa599..858e73960f 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 46f956f317..a51705177b 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.10.dev0 + opentelemetry-api == 0.11.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 6d4fefa599..858e73960f 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/opentelemetry-proto/CHANGELOG.md b/opentelemetry-proto/CHANGELOG.md index aac3fa1d9f..3bc89fca99 100644 --- a/opentelemetry-proto/CHANGELOG.md +++ b/opentelemetry-proto/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.10b0 + +Released 2020-06-23 + - Regenerate proto code and add pyi stubs ([#823](https://github.com/open-telemetry/opentelemetry-python/pull/823)) diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 6d4fefa599..858e73960f 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 7c077dbf5e..c7d791ccc4 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.10b0 + +Released 2020-06-23 + - Rename CounterAggregator -> SumAggregator ([#816](https://github.com/open-telemetry/opentelemetry-python/pull/816)) diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 68c9984dc9..b4e7ed6a9d 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.10.dev0 + opentelemetry-api == 0.11.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 6d4fefa599..858e73960f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 804c927ded..c41ce2492a 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.10.dev0" +__version__ = "0.11.dev0" From 3c366b2a65b41be9c90c34d13cc9a26cb76310ab Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Fri, 26 Jun 2020 12:01:57 -0400 Subject: [PATCH 0434/1517] fix(celery): increase async timeout for tests (#857) --- .../tests/celery/test_celery_functional.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py b/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py index 570a12b758..abba19fc39 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py @@ -27,6 +27,9 @@ from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.trace.status import StatusCanonicalCode +# set a high timeout for async executions due to issues in CI +ASYNC_GET_TIMEOUT = 120 + class MyException(Exception): pass @@ -38,7 +41,7 @@ def fn_task(): return 42 result = fn_task.apply_async() - assert result.get() == 42 + assert result.get(timeout=ASYNC_GET_TIMEOUT) == 42 spans = memory_exporter.get_finished_spans() assert len(spans) == 2 @@ -144,7 +147,7 @@ def fn_task_parameters(user, force_logout=False): result = fn_task_parameters.apply_async( args=["user"], kwargs={"force_logout": True} ) - assert result.get(timeout=10) == ["user", True] + assert result.get(timeout=ASYNC_GET_TIMEOUT) == ["user", True] spans = memory_exporter.get_finished_spans() assert len(spans) == 2 @@ -181,7 +184,7 @@ def fn_task(): results = [fn_task.delay() for _ in range(100)] for result in results: - assert result.get(timeout=1) == 42 + assert result.get(timeout=ASYNC_GET_TIMEOUT) == 42 spans = memory_exporter.get_finished_spans() @@ -194,7 +197,7 @@ def fn_task_parameters(user, force_logout=False): return (user, force_logout) result = fn_task_parameters.delay("user", force_logout=True) - assert result.get(timeout=10) == ["user", True] + assert result.get(timeout=ASYNC_GET_TIMEOUT) == ["user", True] spans = memory_exporter.get_finished_spans() assert len(spans) == 2 @@ -448,7 +451,7 @@ def run(self, *args, **kwargs): # avoid call loop return CelerySubClass.apply_async(args=[], kwargs={"stop": True}).get( - timeout=10 + timeout=ASYNC_GET_TIMEOUT ) class CelerySubClass(CelerySuperClass): From fb37885caeeff332dbd11ab8bd852d1920b1e8fc Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Mon, 29 Jun 2020 17:28:56 +0200 Subject: [PATCH 0435/1517] ext/datadog: Fix API/SDK dependencies in datadog exporter (#859) --- ext/opentelemetry-ext-datadog/setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/opentelemetry-ext-datadog/setup.cfg b/ext/opentelemetry-ext-datadog/setup.cfg index 2d663de89b..905dcda2ee 100644 --- a/ext/opentelemetry-ext-datadog/setup.cfg +++ b/ext/opentelemetry-ext-datadog/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = ddtrace>=0.34.0 - opentelemetry-api==0.10.dev0 - opentelemetry-sdk==0.10.dev0 + opentelemetry-api==0.11.dev0 + opentelemetry-sdk==0.11.dev0 [options.packages.find] where = src From 0c102a1bf04a59ddba929252b6017e9fd2c91d71 Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Tue, 30 Jun 2020 18:18:36 +0200 Subject: [PATCH 0436/1517] mysql: Fix auto instrumentation entry point (#858) entry point for the mysql instrumentation pointed to pymysql instrumentation which caused loading of the instrumentation to fail since it does not exist. Co-authored-by: alrex Co-authored-by: Yusuke Tsutsumi --- ext/opentelemetry-ext-mysql/CHANGELOG.md | 3 +++ ext/opentelemetry-ext-mysql/setup.cfg | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-mysql/CHANGELOG.md b/ext/opentelemetry-ext-mysql/CHANGELOG.md index 8c1b06faa6..a4ab480939 100644 --- a/ext/opentelemetry-ext-mysql/CHANGELOG.md +++ b/ext/opentelemetry-ext-mysql/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- bugfix: Fix auto-instrumentation entry point for mysql + ([#858](https://github.com/open-telemetry/opentelemetry-python/pull/858)) + ## 0.7b1 Released 2020-05-12 diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 941c425343..ceb985e8b4 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -55,4 +55,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - mysql = opentelemetry.ext.pymysql:MySQLInstrumentor + mysql = opentelemetry.ext.mysql:MySQLInstrumentor From d20ae9b92c63856020ddcfb907e898b0158de544 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 30 Jun 2020 09:36:03 -0700 Subject: [PATCH 0437/1517] chore: use token for test package upload --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 13ee1e24c4..66ae00849b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -25,8 +25,8 @@ jobs: # might have already been updated, this would be bad. - name: Publish to TestPyPI env: - TWINE_USERNAME: ${{ secrets.test_pypi_username }} - TWINE_PASSWORD: ${{ secrets.test_pypi_password }} + TWINE_USERNAME: '__token__' + TWINE_PASSWORD: ${{ secrets.test_pypi_token }} run: | twine upload --repository testpypi --skip-existing --verbose dist/* - name: Publish to PyPI From b56fe2acc67a29d2c4e51523d92d748635045782 Mon Sep 17 00:00:00 2001 From: bitspradp Date: Tue, 30 Jun 2020 19:31:22 +0100 Subject: [PATCH 0438/1517] Added a warning log when the existing TracerProvider and MeterProvider are overridden (#856) --- opentelemetry-api/CHANGELOG.md | 2 ++ .../src/opentelemetry/metrics/__init__.py | 4 ++++ .../src/opentelemetry/trace/__init__.py | 4 ++++ .../tests/metrics/test_globals.py | 23 +++++++++++++++++++ opentelemetry-api/tests/trace/test_globals.py | 17 ++++++++++++++ 5 files changed, 50 insertions(+) create mode 100644 opentelemetry-api/tests/metrics/test_globals.py diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 10c08c8574..9a2647da26 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -16,6 +16,8 @@ Released 2020-06-10 ([#764](https://github.com/open-telemetry/opentelemetry-python/pull/764)) - Add SumObserver and UpDownSumObserver in metrics ([#789](https://github.com/open-telemetry/opentelemetry-python/pull/789)) +- Log a warning when replacing the global Tracer/Meter provider + ([#856](https://github.com/open-telemetry/opentelemetry-python/pull/856)) ## 0.8b0 diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index aa2988bce4..90f2f03f56 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -460,6 +460,10 @@ def get_meter( def set_meter_provider(meter_provider: MeterProvider) -> None: """Sets the current global :class:`~.MeterProvider` object.""" global _METER_PROVIDER # pylint: disable=global-statement + + if _METER_PROVIDER is not None: + logger.warning("Overriding current MeterProvider") + _METER_PROVIDER = meter_provider diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index fa0bc376e7..196cf3390c 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -463,6 +463,10 @@ def get_tracer( def set_tracer_provider(tracer_provider: TracerProvider) -> None: """Sets the current global :class:`~.TracerProvider` object.""" global _TRACER_PROVIDER # pylint: disable=global-statement + + if _TRACER_PROVIDER is not None: + logger.warning("Overriding current TracerProvider") + _TRACER_PROVIDER = tracer_provider diff --git a/opentelemetry-api/tests/metrics/test_globals.py b/opentelemetry-api/tests/metrics/test_globals.py new file mode 100644 index 0000000000..9b9cfb94d9 --- /dev/null +++ b/opentelemetry-api/tests/metrics/test_globals.py @@ -0,0 +1,23 @@ +# type:ignore +import unittest +from logging import WARNING + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import MeterProvider + + +class TestGlobals(unittest.TestCase): + def test_meter_provider_override_warning(self): + """metrics.set_meter_provider should throw a warning when overridden""" + metrics.set_meter_provider(MeterProvider()) + with self.assertLogs(level=WARNING) as test: + metrics.set_meter_provider(MeterProvider()) + self.assertEqual( + test.output, + [ + ( + "WARNING:opentelemetry.metrics:Overriding current " + "MeterProvider" + ) + ], + ) diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 2f0f88fb28..5042dd2b72 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -1,7 +1,9 @@ import unittest +from logging import WARNING from unittest.mock import patch from opentelemetry import context, trace +from opentelemetry.sdk.trace import TracerProvider # type:ignore class TestGlobals(unittest.TestCase): @@ -20,6 +22,21 @@ def test_get_tracer(self): trace.get_tracer("foo", "var", mock_provider) mock_provider.get_tracer.assert_called_with("foo", "var") + def test_tracer_provider_override_warning(self): + """trace.set_tracer_provider should throw a warning when overridden""" + trace.set_tracer_provider(TracerProvider()) + with self.assertLogs(level=WARNING) as test: + trace.set_tracer_provider(TracerProvider()) + self.assertEqual( + test.output, + [ + ( + "WARNING:opentelemetry.trace:Overriding current " + "TracerProvider" + ) + ], + ) + class TestTracer(unittest.TestCase): def setUp(self): From 170c2c70903df669ce0978b9a56c81e7c75472c6 Mon Sep 17 00:00:00 2001 From: bitspradp Date: Tue, 30 Jun 2020 21:13:58 +0100 Subject: [PATCH 0439/1517] ext/boto ext/botocore:Converted resource to hold Resource attribute than a string (#866) --- ext/opentelemetry-ext-boto/CHANGELOG.md | 4 ++ .../src/opentelemetry/ext/boto/__init__.py | 10 +++- .../tests/test_boto_instrumentation.py | 49 +++++++++++++---- ext/opentelemetry-ext-botocore/CHANGELOG.md | 3 ++ .../opentelemetry/ext/botocore/__init__.py | 10 +++- .../tests/test_botocore_instrumentation.py | 52 +++++++++++++++---- 6 files changed, 106 insertions(+), 22 deletions(-) diff --git a/ext/opentelemetry-ext-boto/CHANGELOG.md b/ext/opentelemetry-ext-boto/CHANGELOG.md index 896a782491..612b73b39a 100644 --- a/ext/opentelemetry-ext-boto/CHANGELOG.md +++ b/ext/opentelemetry-ext-boto/CHANGELOG.md @@ -2,8 +2,12 @@ ## Unreleased +- ext/boto and ext/botocore: fails to export spans via jaeger +([#866](https://github.com/open-telemetry/opentelemetry-python/pull/866)) + ## 0.9b0 Released 2020-06-10 - Initial release + diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py index 97d4922658..637200b078 100644 --- a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py @@ -52,6 +52,7 @@ from opentelemetry.ext.boto.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.sdk.trace import Resource from opentelemetry.trace import SpanKind, get_tracer logger = logging.getLogger(__name__) @@ -123,9 +124,14 @@ def _common_request( # pylint: disable=too-many-locals ) as span: if args: http_method = args[0] - span.resource = "%s.%s" % (endpoint_name, http_method.lower()) + span.resource = Resource( + labels={ + "endpoint": endpoint_name, + "http_method": http_method.lower(), + } + ) else: - span.resource = endpoint_name + span.resource = Resource(labels={"endpoint": endpoint_name}) add_span_arg_tags( span, endpoint_name, args, args_name, traced_args, diff --git a/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py b/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py index a629b10870..7cfbf018c6 100644 --- a/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py +++ b/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py @@ -27,6 +27,7 @@ ) from opentelemetry.ext.boto import BotoInstrumentor +from opentelemetry.sdk.resources import Resource from opentelemetry.test.test_base import TestBase @@ -69,7 +70,12 @@ def test_ec2_client(self): span = spans[1] self.assertEqual(span.attributes["aws.operation"], "RunInstances") assert_span_http_status_code(span, 200) - self.assertEqual(span.resource, "ec2.runinstances") + self.assertEqual( + span.resource, + Resource( + labels={"endpoint": "ec2", "http_method": "runinstances"} + ), + ) self.assertEqual(span.attributes["http.method"], "POST") self.assertEqual(span.attributes["aws.region"], "us-west-2") self.assertEqual(span.name, "ec2.command") @@ -123,7 +129,10 @@ def test_s3_client(self): self.assertEqual(len(spans), 3) span = spans[2] assert_span_http_status_code(span, 200) - self.assertEqual(span.resource, "s3.head") + self.assertEqual( + span.resource, + Resource(labels={"endpoint": "s3", "http_method": "head"}), + ) self.assertEqual(span.attributes["http.method"], "HEAD") self.assertEqual(span.attributes["aws.operation"], "head_bucket") self.assertEqual(span.name, "s3.command") @@ -135,7 +144,10 @@ def test_s3_client(self): spans = self.memory_exporter.get_finished_spans() assert spans span = spans[2] - self.assertEqual(span.resource, "s3.head") + self.assertEqual( + span.resource, + Resource(labels={"endpoint": "s3", "http_method": "head"}), + ) @mock_s3_deprecated def test_s3_put(self): @@ -152,15 +164,24 @@ def test_s3_put(self): self.assertEqual(len(spans), 3) self.assertEqual(spans[0].attributes["aws.operation"], "create_bucket") assert_span_http_status_code(spans[0], 200) - self.assertEqual(spans[0].resource, "s3.put") + self.assertEqual( + spans[0].resource, + Resource(labels={"endpoint": "s3", "http_method": "put"}), + ) # get bucket self.assertEqual(spans[1].attributes["aws.operation"], "head_bucket") - self.assertEqual(spans[1].resource, "s3.head") + self.assertEqual( + spans[1].resource, + Resource(labels={"endpoint": "s3", "http_method": "head"}), + ) # put object self.assertEqual( spans[2].attributes["aws.operation"], "_send_file_internal" ) - self.assertEqual(spans[2].resource, "s3.put") + self.assertEqual( + spans[2].resource, + Resource(labels={"endpoint": "s3", "http_method": "put"}), + ) @mock_lambda_deprecated def test_unpatch(self): @@ -200,7 +221,10 @@ def test_lambda_client(self): self.assertEqual(len(spans), 2) span = spans[0] assert_span_http_status_code(span, 200) - self.assertEqual(span.resource, "lambda.get") + self.assertEqual( + span.resource, + Resource(labels={"endpoint": "lambda", "http_method": "get"}), + ) self.assertEqual(span.attributes["http.method"], "GET") self.assertEqual(span.attributes["aws.region"], "us-east-2") self.assertEqual(span.attributes["aws.operation"], "list_functions") @@ -214,7 +238,12 @@ def test_sts_client(self): spans = self.memory_exporter.get_finished_spans() assert spans span = spans[0] - self.assertEqual(span.resource, "sts.getfederationtoken") + self.assertEqual( + span.resource, + Resource( + labels={"endpoint": "sts", "http_method": "getfederationtoken"} + ), + ) self.assertEqual(span.attributes["aws.region"], "us-west-2") self.assertEqual( span.attributes["aws.operation"], "GetFederationToken" @@ -238,5 +267,7 @@ def test_elasticache_client(self): spans = self.memory_exporter.get_finished_spans() assert spans span = spans[0] - self.assertEqual(span.resource, "elasticache") + self.assertEqual( + span.resource, Resource(labels={"endpoint": "elasticcache"}) + ) self.assertEqual(span.attributes["aws.region"], "us-west-2") diff --git a/ext/opentelemetry-ext-botocore/CHANGELOG.md b/ext/opentelemetry-ext-botocore/CHANGELOG.md index 896a782491..ccdc954a6b 100644 --- a/ext/opentelemetry-ext-botocore/CHANGELOG.md +++ b/ext/opentelemetry-ext-botocore/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- ext/boto and ext/botocore: fails to export spans via jaeger +([#866](https://github.com/open-telemetry/opentelemetry-python/pull/866)) + ## 0.9b0 Released 2020-06-10 diff --git a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py index 97615d974f..f9da154d97 100644 --- a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py +++ b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py @@ -58,6 +58,7 @@ from opentelemetry.ext.botocore.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.sdk.trace import Resource from opentelemetry.trace import SpanKind, get_tracer logger = logging.getLogger(__name__) @@ -98,10 +99,15 @@ def _patched_api_call(self, original_func, instance, args, kwargs): operation = None if args: operation = args[0] - span.resource = "%s.%s" % (endpoint_name, operation.lower()) + span.resource = Resource( + labels={ + "endpoint": endpoint_name, + "operation": operation.lower(), + } + ) else: - span.resource = endpoint_name + span.resource = Resource(labels={"endpoint": endpoint_name}) add_span_arg_tags( span, diff --git a/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py b/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py index 64d0c3d7b7..e0b687ad8b 100644 --- a/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py +++ b/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py @@ -10,6 +10,7 @@ ) from opentelemetry.ext.botocore import BotocoreInstrumentor +from opentelemetry.sdk.resources import Resource from opentelemetry.test.test_base import TestBase @@ -49,7 +50,12 @@ def test_traced_client(self): self.assertEqual(span.attributes["aws.region"], "us-west-2") self.assertEqual(span.attributes["aws.operation"], "DescribeInstances") assert_span_http_status_code(span, 200) - self.assertEqual(span.resource, "ec2.describeinstances") + self.assertEqual( + span.resource, + Resource( + labels={"endpoint": "ec2", "operation": "describeinstances"} + ), + ) self.assertEqual(span.name, "ec2.command") @mock_ec2 @@ -73,7 +79,10 @@ def test_s3_client(self): self.assertEqual(len(spans), 2) self.assertEqual(span.attributes["aws.operation"], "ListBuckets") assert_span_http_status_code(span, 200) - self.assertEqual(span.resource, "s3.listbuckets") + self.assertEqual( + span.resource, + Resource(labels={"endpoint": "s3", "operation": "listbuckets"}), + ) # testing for span error self.memory_exporter.get_finished_spans() @@ -82,7 +91,10 @@ def test_s3_client(self): spans = self.memory_exporter.get_finished_spans() assert spans span = spans[2] - self.assertEqual(span.resource, "s3.listobjects") + self.assertEqual( + span.resource, + Resource(labels={"endpoint": "s3", "operation": "listobjects"}), + ) @mock_s3 def test_s3_put(self): @@ -97,9 +109,15 @@ def test_s3_put(self): self.assertEqual(len(spans), 2) self.assertEqual(span.attributes["aws.operation"], "CreateBucket") assert_span_http_status_code(span, 200) - self.assertEqual(span.resource, "s3.createbucket") + self.assertEqual( + span.resource, + Resource(labels={"endpoint": "s3", "operation": "createbucket"}), + ) self.assertEqual(spans[1].attributes["aws.operation"], "PutObject") - self.assertEqual(spans[1].resource, "s3.putobject") + self.assertEqual( + spans[1].resource, + Resource(labels={"endpoint": "s3", "operation": "putobject"}), + ) self.assertEqual(spans[1].attributes["params.Key"], str(params["Key"])) self.assertEqual( spans[1].attributes["params.Bucket"], str(params["Bucket"]) @@ -119,7 +137,10 @@ def test_sqs_client(self): self.assertEqual(span.attributes["aws.region"], "us-east-1") self.assertEqual(span.attributes["aws.operation"], "ListQueues") assert_span_http_status_code(span, 200) - self.assertEqual(span.resource, "sqs.listqueues") + self.assertEqual( + span.resource, + Resource(labels={"endpoint": "sqs", "operation": "listqueues"}), + ) @mock_kinesis def test_kinesis_client(self): @@ -136,7 +157,12 @@ def test_kinesis_client(self): self.assertEqual(span.attributes["aws.region"], "us-east-1") self.assertEqual(span.attributes["aws.operation"], "ListStreams") assert_span_http_status_code(span, 200) - self.assertEqual(span.resource, "kinesis.liststreams") + self.assertEqual( + span.resource, + Resource( + labels={"endpoint": "kinesis", "operation": "liststreams"} + ), + ) @mock_kinesis def test_unpatch(self): @@ -176,7 +202,12 @@ def test_lambda_client(self): self.assertEqual(span.attributes["aws.region"], "us-east-1") self.assertEqual(span.attributes["aws.operation"], "ListFunctions") assert_span_http_status_code(span, 200) - self.assertEqual(span.resource, "lambda.listfunctions") + self.assertEqual( + span.resource, + Resource( + labels={"endpoint": "lambda", "operation": "listfunctions"} + ), + ) @mock_kms def test_kms_client(self): @@ -191,7 +222,10 @@ def test_kms_client(self): self.assertEqual(span.attributes["aws.region"], "us-east-1") self.assertEqual(span.attributes["aws.operation"], "ListKeys") assert_span_http_status_code(span, 200) - self.assertEqual(span.resource, "kms.listkeys") + self.assertEqual( + span.resource, + Resource(labels={"endpoint": "kms", "operation": "listkeys"}), + ) # checking for protection on sts against security leak self.assertTrue("params" not in span.attributes.keys()) From 7a46b2df1eb507d615c3e2c9baada1794fd04cb8 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 1 Jul 2020 11:03:19 -0600 Subject: [PATCH 0440/1517] Make it possible to set new values in Configuration (#863) --- .../src/opentelemetry/configuration/__init__.py | 11 +++++------ .../tests/configuration/test_configuration.py | 11 +++++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index 0bd2bce8d6..36c0950c0b 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -142,19 +142,18 @@ def __new__(cls) -> "Configuration": def __getattr__(self, name: str) -> Optional[ConfigValue]: return self._config_map.get(name) - def __setattr__(self, key: str, val: ConfigValue) -> None: - if key == "_config_map": - super().__setattr__(key, val) + def __setattr__(self, name: str, value: ConfigValue) -> None: + if name not in self._config_map.keys(): + self._config_map[name] = value else: - raise AttributeError(key) + raise AttributeError(name) def get(self, name: str, default: _T) -> _T: """Use this typed method for dynamic access instead of `getattr` :rtype: str or bool or int or float or None """ - val = self._config_map.get(name, default) - return val + return self._config_map.get(name, default) @classmethod def _reset(cls) -> None: diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index 32b62e619d..316847a004 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -39,7 +39,7 @@ def test_singleton(self) -> None: "OPENTELEMETRY_PTHON_TRACEX_PROVIDER": "tracex_provider", }, ) - def test_environment_variables(self): + def test_environment_variables(self) -> None: self.assertEqual( Configuration().METER_PROVIDER, "meter_provider" ) # pylint: disable=no-member @@ -58,13 +58,16 @@ def test_environment_variables(self): "os.environ", # type: ignore {"OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider"}, ) - def test_property(self): + def test_property(self) -> None: with self.assertRaises(AttributeError): Configuration().TRACER_PROVIDER = "new_tracer_provider" - def test_slots(self) -> None: + def test_set_once(self) -> None: + + Configuration().XYZ = "xyz" + with self.assertRaises(AttributeError): - Configuration().XYZ = "xyz" # pylint: disable=assigning-non-slot + Configuration().XYZ = "abc" # pylint: disable=assigning-non-slot def test_getattr(self) -> None: # literal access From 6ea3f232a6d815cb9e4e2d2d69a2aa1e8a76c9dc Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 2 Jul 2020 09:26:13 -0700 Subject: [PATCH 0441/1517] instrument: moving auto-instrumentation to a submodule (#873) --- .../{auto_instrumentation.py => auto_instrumentation/__init__.py} | 0 .../instrumentation/{ => auto_instrumentation}/sitecustomize.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename opentelemetry-instrumentation/src/opentelemetry/instrumentation/{auto_instrumentation.py => auto_instrumentation/__init__.py} (100%) rename opentelemetry-instrumentation/src/opentelemetry/instrumentation/{ => auto_instrumentation}/sitecustomize.py (100%) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py similarity index 100% rename from opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation.py rename to opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py similarity index 100% rename from opentelemetry-instrumentation/src/opentelemetry/instrumentation/sitecustomize.py rename to opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py From 2a9c3ac6e3e0bfa8038e07c60e19cab0975a5636 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 2 Jul 2020 09:45:56 -0700 Subject: [PATCH 0442/1517] chore: Making eachdist release catch more deps (#867) In the last release, eachdist missed updating dependencies on ext-datadog, which has a slightly different, but valid, way of specifying the version (omitting some spaces). Making the eachdist regex more lenient to catch other valid version specifications. Also modifying the ext-datadog dependency specification to match the format of all the others. Co-authored-by: Leighton Chen --- ext/opentelemetry-ext-datadog/setup.cfg | 4 ++-- scripts/eachdist.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/opentelemetry-ext-datadog/setup.cfg b/ext/opentelemetry-ext-datadog/setup.cfg index 905dcda2ee..d97bcdaa69 100644 --- a/ext/opentelemetry-ext-datadog/setup.cfg +++ b/ext/opentelemetry-ext-datadog/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = ddtrace>=0.34.0 - opentelemetry-api==0.11.dev0 - opentelemetry-sdk==0.11.dev0 + opentelemetry-api == 0.11.dev0 + opentelemetry-sdk == 0.11.dev0 [options.packages.find] where = src diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 57560b9b84..724061690d 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -589,8 +589,8 @@ def update_dependencies(targets, version): targets, version, "setup.cfg", - r"(opentelemetry-.*)= (.*)", - r"\1= " + version, + r"(opentelemetry-.*)==(.*)", + r"\1== " + version, ) From 112bade11f1c1eca473e274947b5806afba58dcf Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Fri, 3 Jul 2020 12:42:56 -0400 Subject: [PATCH 0443/1517] Strip letters (#877) --- .../exporter/cloud_trace/__init__.py | 10 ++++++++-- .../tests/test_cloud_trace_exporter.py | 18 ++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py index f6d1e78b5a..647ad21040 100644 --- a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py @@ -291,6 +291,10 @@ def _extract_events(events: Sequence[Event]) -> ProtoSpan.TimeEvents: ) +def _strip_characters(ot_version): + return "".join(filter(lambda x: x.isdigit() or x == ".", ot_version)) + + def _extract_attributes( attrs: types.Attributes, num_attrs_limit: int, @@ -310,8 +314,10 @@ def _extract_attributes( if add_agent_attr: attributes_dict["g.co/agent"] = _format_attribute_value( "opentelemetry-python {}; google-cloud-trace-exporter {}".format( - pkg_resources.get_distribution("opentelemetry-sdk").version, - cloud_trace_version, + _strip_characters( + pkg_resources.get_distribution("opentelemetry-sdk").version + ), + _strip_characters(cloud_trace_version), ) ) return ProtoSpan.Attributes( diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py index bd5a9f073d..3b89b55046 100644 --- a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py +++ b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py @@ -32,6 +32,7 @@ _extract_links, _extract_status, _format_attribute_value, + _strip_characters, _truncate_str, ) from opentelemetry.exporter.cloud_trace.version import ( @@ -117,10 +118,12 @@ def test_export(self): attribute_map={ "g.co/agent": _format_attribute_value( "opentelemetry-python {}; google-cloud-trace-exporter {}".format( - pkg_resources.get_distribution( - "opentelemetry-sdk" - ).version, - cloud_trace_version, + _strip_characters( + pkg_resources.get_distribution( + "opentelemetry-sdk" + ).version + ), + _strip_characters(cloud_trace_version), ) ) } @@ -415,3 +418,10 @@ def test_truncate(self): ), MAX_EVENT_ATTRS, ) + + def test_strip_characters(self): + self.assertEqual("0.10.0", _strip_characters("0.10.0b")) + self.assertEqual("1.20.5", _strip_characters("1.20.5")) + self.assertEqual("3.1.0", _strip_characters("3.1.0beta")) + self.assertEqual("4.2.0", _strip_characters("4b.2rc.0a")) + self.assertEqual("6.20.15", _strip_characters("b6.20.15")) From ca8c0970f88b083b57c85322c997595d0ffc3702 Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Sun, 5 Jul 2020 02:40:40 -0400 Subject: [PATCH 0444/1517] Resources prototype (#853) --- docs/examples/basic_tracer/README.rst | 37 +++++++ docs/examples/basic_tracer/basic_trace.py | 14 +++ docs/examples/basic_tracer/resources.py | 19 ++++ .../CHANGELOG.md | 3 + .../exporter/cloud_monitoring/__init__.py | 42 +++++++- .../tests/test_cloud_monitoring.py | 102 +++++++++++++++++- .../CHANGELOG.md | 3 + .../setup.cfg | 1 + .../exporter/cloud_trace/__init__.py | 36 ++++++- .../opentelemetry/tools/resource_detector.py | 47 ++++++++ .../tests/test_cloud_trace_exporter.py | 81 +++++++++++++- .../tests/test_gcp_resource_detector.py | 83 ++++++++++++++ opentelemetry-sdk/CHANGELOG.md | 3 + .../opentelemetry/sdk/resources/__init__.py | 63 +++++++++++ .../src/opentelemetry/sdk/trace/__init__.py | 1 + .../tests/resources/test_resources.py | 93 ++++++++++++++++ opentelemetry-sdk/tests/trace/test_trace.py | 5 +- 17 files changed, 617 insertions(+), 16 deletions(-) create mode 100644 docs/examples/basic_tracer/README.rst create mode 100644 docs/examples/basic_tracer/basic_trace.py create mode 100644 docs/examples/basic_tracer/resources.py create mode 100644 ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/tools/resource_detector.py create mode 100644 ext/opentelemetry-exporter-cloud-trace/tests/test_gcp_resource_detector.py diff --git a/docs/examples/basic_tracer/README.rst b/docs/examples/basic_tracer/README.rst new file mode 100644 index 0000000000..57d431a387 --- /dev/null +++ b/docs/examples/basic_tracer/README.rst @@ -0,0 +1,37 @@ +Basic Trace +=========== + +These examples show how to use OpenTelemetry to create and export Spans. + +There are two different examples: + +* basic_trace: Shows how to configure a SpanProcessor and Exporter, and how to create a tracer and span. + +* resources: Shows how to add resource information to a Provider. Note that this must be run on a Google Compute Engine instance. + +The source files of these examples are available :scm_web:`here `. + +Installation +------------ + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + +Run the Example +--------------- + +.. code-block:: sh + + python .py + +The output will be shown in the console. + +Useful links +------------ + +- OpenTelemetry_ +- :doc:`../../api/trace` + +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/docs/examples/basic_tracer/basic_trace.py b/docs/examples/basic_tracer/basic_trace.py new file mode 100644 index 0000000000..f7df424da3 --- /dev/null +++ b/docs/examples/basic_tracer/basic_trace.py @@ -0,0 +1,14 @@ +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) +tracer = trace.get_tracer(__name__) +with tracer.start_as_current_span("foo"): + print("Hello world!") diff --git a/docs/examples/basic_tracer/resources.py b/docs/examples/basic_tracer/resources.py new file mode 100644 index 0000000000..f37e73531d --- /dev/null +++ b/docs/examples/basic_tracer/resources.py @@ -0,0 +1,19 @@ +from opentelemetry import trace +from opentelemetry.sdk.resources import get_aggregated_resources +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) +from opentelemetry.tools.resource_detector import GoogleCloudResourceDetector + +resources = get_aggregated_resources([GoogleCloudResourceDetector()]) + +trace.set_tracer_provider(TracerProvider(resource=resources)) + +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) +tracer = trace.get_tracer(__name__) +with tracer.start_as_current_span("foo"): + print("Hello world!") diff --git a/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md b/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md index d51e8aa749..e5f01ee5b0 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md +++ b/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add support for resources + ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) + ## Version 0.10b0 Released 2020-06-23 diff --git a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py index 8e71e5b250..2c6e3abdb0 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py +++ b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py @@ -5,6 +5,7 @@ import google.auth from google.api.label_pb2 import LabelDescriptor from google.api.metric_pb2 import MetricDescriptor +from google.api.monitored_resource_pb2 import MonitoredResource from google.cloud.monitoring_v3 import MetricServiceClient from google.cloud.monitoring_v3.proto.metric_pb2 import TimeSeries @@ -14,12 +15,21 @@ MetricsExportResult, ) from opentelemetry.sdk.metrics.export.aggregate import SumAggregator +from opentelemetry.sdk.resources import Resource logger = logging.getLogger(__name__) MAX_BATCH_WRITE = 200 WRITE_INTERVAL = 10 UNIQUE_IDENTIFIER_KEY = "opentelemetry_id" +OT_RESOURCE_LABEL_TO_GCP = { + "gce_instance": { + "cloud.account.id": "project_id", + "host.id": "instance_id", + "cloud.zone": "zone", + } +} + # pylint is unable to resolve members of protobuf objects # pylint: disable=no-member @@ -56,13 +66,33 @@ def __init__( random.randint(0, 16 ** 8) ) - def _add_resource_info(self, series: TimeSeries) -> None: + @staticmethod + def _get_monitored_resource( + resource: Resource, + ) -> Optional[MonitoredResource]: """Add Google resource specific information (e.g. instance id, region). + See + https://cloud.google.com/monitoring/custom-metrics/creating-metrics#custom-metric-resources + for supported types Args: series: ProtoBuf TimeSeries """ - # TODO: Leverage this better + + if resource.labels.get("cloud.provider") != "gcp": + return None + resource_type = resource.labels["gcp.resource_type"] + if resource_type not in OT_RESOURCE_LABEL_TO_GCP: + return None + return MonitoredResource( + type=resource_type, + labels={ + gcp_label: str(resource.labels[ot_label]) + for ot_label, gcp_label in OT_RESOURCE_LABEL_TO_GCP[ + resource_type + ].items() + }, + ) def _batch_write(self, series: TimeSeries) -> None: """ Cloud Monitoring allows writing up to 200 time series at once @@ -162,9 +192,11 @@ def export( metric_descriptor = self._get_metric_descriptor(record) if not metric_descriptor: continue - - series = TimeSeries() - self._add_resource_info(series) + series = TimeSeries( + resource=self._get_monitored_resource( + record.instrument.meter.resource + ) + ) series.metric.type = metric_descriptor.type for key, value in record.labels: series.metric.labels[key] = str(value) diff --git a/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py b/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py index 1b36661699..bc695f6d1f 100644 --- a/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py +++ b/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py @@ -17,6 +17,7 @@ from google.api.label_pb2 import LabelDescriptor from google.api.metric_pb2 import MetricDescriptor +from google.api.monitored_resource_pb2 import MonitoredResource from google.cloud.monitoring_v3.proto.metric_pb2 import TimeSeries from opentelemetry.exporter.cloud_monitoring import ( @@ -27,17 +28,30 @@ ) from opentelemetry.sdk.metrics.export import MetricRecord from opentelemetry.sdk.metrics.export.aggregate import SumAggregator +from opentelemetry.sdk.resources import Resource class UnsupportedAggregator: pass +class MockMeter: + def __init__(self, resource=Resource.create_empty()): + self.resource = resource + + class MockMetric: - def __init__(self, name="name", description="description", value_type=int): + def __init__( + self, + name="name", + description="description", + value_type=int, + meter=None, + ): self.name = name self.description = description self.value_type = value_type + self.meter = meter or MockMeter() # pylint: disable=protected-access @@ -205,24 +219,41 @@ def test_export(self): } ) + resource = Resource( + labels={ + "cloud.account.id": 123, + "host.id": "host", + "cloud.zone": "US", + "cloud.provider": "gcp", + "extra_info": "extra", + "gcp.resource_type": "gce_instance", + "not_gcp_resource": "value", + } + ) + sum_agg_one = SumAggregator() sum_agg_one.checkpoint = 1 sum_agg_one.last_update_timestamp = (WRITE_INTERVAL + 1) * 1e9 exporter.export( [ MetricRecord( - MockMetric(), + MockMetric(meter=MockMeter(resource=resource)), (("label1", "value1"), ("label2", 1),), sum_agg_one, ), MetricRecord( - MockMetric(), + MockMetric(meter=MockMeter(resource=resource)), (("label1", "value2"), ("label2", 2),), sum_agg_one, ), ] ) - series1 = TimeSeries() + expected_resource = MonitoredResource( + type="gce_instance", + labels={"project_id": "123", "instance_id": "host", "zone": "US"}, + ) + + series1 = TimeSeries(resource=expected_resource) series1.metric.type = "custom.googleapis.com/OpenTelemetry/name" series1.metric.labels["label1"] = "value1" series1.metric.labels["label2"] = "1" @@ -231,7 +262,7 @@ def test_export(self): point.interval.end_time.seconds = WRITE_INTERVAL + 1 point.interval.end_time.nanos = 0 - series2 = TimeSeries() + series2 = TimeSeries(resource=expected_resource) series2.metric.type = "custom.googleapis.com/OpenTelemetry/name" series2.metric.labels["label1"] = "value2" series2.metric.labels["label2"] = "2" @@ -342,3 +373,64 @@ def test_unique_identifier(self): first_call[0][1][0].metric.labels[UNIQUE_IDENTIFIER_KEY], second_call[0][1][0].metric.labels[UNIQUE_IDENTIFIER_KEY], ) + + def test_extract_resources(self): + exporter = CloudMonitoringMetricsExporter(project_id=self.project_id) + + self.assertIsNone( + exporter._get_monitored_resource(Resource.create_empty()) + ) + resource = Resource( + labels={ + "cloud.account.id": 123, + "host.id": "host", + "cloud.zone": "US", + "cloud.provider": "gcp", + "extra_info": "extra", + "gcp.resource_type": "gce_instance", + "not_gcp_resource": "value", + } + ) + expected_extract = MonitoredResource( + type="gce_instance", + labels={"project_id": "123", "instance_id": "host", "zone": "US"}, + ) + self.assertEqual( + exporter._get_monitored_resource(resource), expected_extract + ) + + resource = Resource( + labels={ + "cloud.account.id": "123", + "host.id": "host", + "extra_info": "extra", + "not_gcp_resource": "value", + "gcp.resource_type": "gce_instance", + "cloud.provider": "gcp", + } + ) + # Should throw when passed a malformed GCP resource dict + self.assertRaises(KeyError, exporter._get_monitored_resource, resource) + + resource = Resource( + labels={ + "cloud.account.id": "123", + "host.id": "host", + "extra_info": "extra", + "not_gcp_resource": "value", + "gcp.resource_type": "unsupported_gcp_resource", + "cloud.provider": "gcp", + } + ) + self.assertIsNone(exporter._get_monitored_resource(resource)) + + resource = Resource( + labels={ + "cloud.account.id": "123", + "host.id": "host", + "extra_info": "extra", + "not_gcp_resource": "value", + "cloud.provider": "aws", + } + ) + self.assertIsNone(exporter._get_monitored_resource(resource)) diff --git a/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md b/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md index 5ff312743f..22eac3e4bf 100644 --- a/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md +++ b/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add support for resources and resource detector + ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) + ## Version 0.10b0 Released 2020-06-23 diff --git a/ext/opentelemetry-exporter-cloud-trace/setup.cfg b/ext/opentelemetry-exporter-cloud-trace/setup.cfg index 41ffc4116a..4437f40fdd 100644 --- a/ext/opentelemetry-exporter-cloud-trace/setup.cfg +++ b/ext/opentelemetry-exporter-cloud-trace/setup.cfg @@ -39,6 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = + requests opentelemetry-api opentelemetry-sdk google-cloud-trace diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py index 647ad21040..ea36d22727 100644 --- a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py @@ -55,6 +55,7 @@ from opentelemetry.exporter.cloud_trace.version import ( __version__ as cloud_trace_version, ) +from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import Event from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult from opentelemetry.sdk.util import BoundedDict @@ -106,7 +107,6 @@ def export(self, spans: Sequence[Span]) -> SpanExportResult: # pylint: disable=broad-except except Exception as ex: logger.error("Error when creating span %s", span, exc_info=ex) - try: self.client.batch_write_spans( "projects/{}".format(self.project_id), cloud_trace_spans, @@ -150,6 +150,11 @@ def _translate_to_cloud_trace( MAX_SPAN_ATTRS, ) + # Span does not support a MonitoredResource object. We put the + # information into labels instead. + resources_and_attrs = _extract_resources(span.resource) + resources_and_attrs.update(span.attributes) + cloud_trace_spans.append( { "name": span_name, @@ -161,7 +166,9 @@ def _translate_to_cloud_trace( "end_time": end_time, "parent_span_id": parent_id, "attributes": _extract_attributes( - span.attributes, MAX_SPAN_ATTRS, add_agent_attr=True + resources_and_attrs, + MAX_SPAN_ATTRS, + add_agent_attr=True, ), "links": _extract_links(span.links), "status": _extract_status(span.status), @@ -295,6 +302,31 @@ def _strip_characters(ot_version): return "".join(filter(lambda x: x.isdigit() or x == ".", ot_version)) +OT_RESOURCE_LABEL_TO_GCP = { + "gce_instance": { + "cloud.account.id": "project_id", + "host.id": "instance_id", + "cloud.zone": "zone", + } +} + + +def _extract_resources(resource: Resource) -> Dict[str, str]: + if resource.labels.get("cloud.provider") != "gcp": + return {} + resource_type = resource.labels["gcp.resource_type"] + if resource_type not in OT_RESOURCE_LABEL_TO_GCP: + return {} + return { + "g.co/r/{}/{}".format(resource_type, gcp_resource_key,): str( + resource.labels[ot_resource_key] + ) + for ot_resource_key, gcp_resource_key in OT_RESOURCE_LABEL_TO_GCP[ + resource_type + ].items() + } + + def _extract_attributes( attrs: types.Attributes, num_attrs_limit: int, diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/tools/resource_detector.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/tools/resource_detector.py new file mode 100644 index 0000000000..5dbf7627f8 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/tools/resource_detector.py @@ -0,0 +1,47 @@ +import requests + +from opentelemetry.context import attach, detach, set_value +from opentelemetry.sdk.resources import Resource, ResourceDetector + +_GCP_METADATA_URL = ( + "http://metadata.google.internal/computeMetadata/v1/?recursive=true" +) +_GCP_METADATA_URL_HEADER = {"Metadata-Flavor": "Google"} + + +def get_gce_resources(): + """ Resource finder for common GCE attributes + + See: https://cloud.google.com/compute/docs/storing-retrieving-metadata + """ + token = attach(set_value("suppress_instrumentation", True)) + all_metadata = requests.get( + _GCP_METADATA_URL, headers=_GCP_METADATA_URL_HEADER + ).json() + detach(token) + gce_resources = { + "host.id": all_metadata["instance"]["id"], + "cloud.account.id": all_metadata["project"]["projectId"], + "cloud.zone": all_metadata["instance"]["zone"].split("/")[-1], + "cloud.provider": "gcp", + "gcp.resource_type": "gce_instance", + } + return gce_resources + + +_RESOURCE_FINDERS = [get_gce_resources] + + +class GoogleCloudResourceDetector(ResourceDetector): + def __init__(self, raise_on_error=False): + super().__init__(raise_on_error) + self.cached = False + self.gcp_resources = {} + + def detect(self) -> "Resource": + if not self.cached: + self.cached = True + for resource_finder in _RESOURCE_FINDERS: + found_resources = resource_finder() + self.gcp_resources.update(found_resources) + return Resource(self.gcp_resources) diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py index 3b89b55046..c5444241b6 100644 --- a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py +++ b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py @@ -1,4 +1,4 @@ -# Copyright OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ _extract_attributes, _extract_events, _extract_links, + _extract_resources, _extract_status, _format_attribute_value, _strip_characters, @@ -38,6 +39,7 @@ from opentelemetry.exporter.cloud_trace.version import ( __version__ as cloud_trace_version, ) +from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import Event from opentelemetry.sdk.trace.export import Span from opentelemetry.trace import Link, SpanContext, SpanKind @@ -92,6 +94,15 @@ def test_constructor_explicit(self): def test_export(self): trace_id = "6e0c63257de34c92bf9efcd03927272e" span_id = "95bb5edabd45950f" + resource_info = Resource( + { + "cloud.account.id": 123, + "host.id": "host", + "cloud.zone": "US", + "cloud.provider": "gcp", + "gcp.resource_type": "gce_instance", + } + ) span_datas = [ Span( name="span_name", @@ -102,6 +113,8 @@ def test_export(self): ), parent=None, kind=SpanKind.INTERNAL, + resource=resource_info, + attributes={"attr_key": "attr_value"}, ) ] @@ -116,6 +129,13 @@ def test_export(self): ), "attributes": ProtoSpan.Attributes( attribute_map={ + "g.co/r/gce_instance/zone": _format_attribute_value("US"), + "g.co/r/gce_instance/instance_id": _format_attribute_value( + "host" + ), + "g.co/r/gce_instance/project_id": _format_attribute_value( + "123" + ), "g.co/agent": _format_attribute_value( "opentelemetry-python {}; google-cloud-trace-exporter {}".format( _strip_characters( @@ -125,7 +145,8 @@ def test_export(self): ), _strip_characters(cloud_trace_version), ) - ) + ), + "attr_key": _format_attribute_value("attr_value"), } ), "links": None, @@ -284,6 +305,62 @@ def test_extract_links(self): ), ) + def test_extract_resources(self): + self.assertEqual(_extract_resources(Resource.create_empty()), {}) + resource = Resource( + labels={ + "cloud.account.id": 123, + "host.id": "host", + "cloud.zone": "US", + "cloud.provider": "gcp", + "extra_info": "extra", + "gcp.resource_type": "gce_instance", + "not_gcp_resource": "value", + } + ) + expected_extract = { + "g.co/r/gce_instance/project_id": "123", + "g.co/r/gce_instance/instance_id": "host", + "g.co/r/gce_instance/zone": "US", + } + self.assertEqual(_extract_resources(resource), expected_extract) + + resource = Resource( + labels={ + "cloud.account.id": "123", + "host.id": "host", + "extra_info": "extra", + "not_gcp_resource": "value", + "gcp.resource_type": "gce_instance", + "cloud.provider": "gcp", + } + ) + # Should throw when passed a malformed GCP resource dict + self.assertRaises(KeyError, _extract_resources, resource) + + resource = Resource( + labels={ + "cloud.account.id": "123", + "host.id": "host", + "extra_info": "extra", + "not_gcp_resource": "value", + "gcp.resource_type": "unsupported_gcp_resource", + "cloud.provider": "gcp", + } + ) + self.assertEqual(_extract_resources(resource), {}) + + resource = Resource( + labels={ + "cloud.account.id": "123", + "host.id": "host", + "extra_info": "extra", + "not_gcp_resource": "value", + "cloud.provider": "aws", + } + ) + self.assertEqual(_extract_resources(resource), {}) + # pylint:disable=too-many-locals def test_truncate(self): """Cloud Trace API imposes limits on the length of many things, diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/test_gcp_resource_detector.py b/ext/opentelemetry-exporter-cloud-trace/tests/test_gcp_resource_detector.py new file mode 100644 index 0000000000..df82121330 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/tests/test_gcp_resource_detector.py @@ -0,0 +1,83 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from opentelemetry.sdk.resources import Resource +from opentelemetry.tools.resource_detector import ( + _GCP_METADATA_URL, + GoogleCloudResourceDetector, + get_gce_resources, +) + +RESOURCES_JSON_STRING = { + "instance": {"id": "instance_id", "zone": "projects/123/zones/zone"}, + "project": {"projectId": "project_id"}, +} + + +class TestGCEResourceFinder(unittest.TestCase): + @mock.patch("opentelemetry.tools.resource_detector.requests.get") + def test_finding_gce_resources(self, getter): + getter.return_value.json.return_value = RESOURCES_JSON_STRING + found_resources = get_gce_resources() + self.assertEqual(getter.call_args_list[0][0][0], _GCP_METADATA_URL) + self.assertEqual( + found_resources, + { + "host.id": "instance_id", + "cloud.provider": "gcp", + "cloud.account.id": "project_id", + "cloud.zone": "zone", + "gcp.resource_type": "gce_instance", + }, + ) + + +class TestGoogleCloudResourceDetector(unittest.TestCase): + @mock.patch("opentelemetry.tools.resource_detector.requests.get") + def test_finding_resources(self, getter): + resource_finder = GoogleCloudResourceDetector() + getter.return_value.json.return_value = RESOURCES_JSON_STRING + found_resources = resource_finder.detect() + self.assertEqual(getter.call_args_list[0][0][0], _GCP_METADATA_URL) + self.assertEqual( + found_resources, + Resource( + labels={ + "host.id": "instance_id", + "cloud.provider": "gcp", + "cloud.account.id": "project_id", + "cloud.zone": "zone", + "gcp.resource_type": "gce_instance", + } + ), + ) + self.assertEqual(getter.call_count, 1) + + found_resources = resource_finder.detect() + self.assertEqual(getter.call_count, 1) + self.assertEqual( + found_resources, + Resource( + labels={ + "host.id": "instance_id", + "cloud.provider": "gcp", + "cloud.account.id": "project_id", + "cloud.zone": "zone", + "gcp.resource_type": "gce_instance", + } + ), + ) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index c7d791ccc4..46ad8b8e29 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add support for resources and resource detector + ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) + ## Version 0.10b0 Released 2020-06-23 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 91c491f09f..a8e9ac65be 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -12,11 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +import abc +import concurrent.futures +import logging +import os import typing from json import dumps LabelValue = typing.Union[str, bool, int, float] Labels = typing.Dict[str, LabelValue] +logger = logging.getLogger(__name__) class Resource: @@ -55,3 +60,61 @@ def __hash__(self): _EMPTY_RESOURCE = Resource({}) + + +class ResourceDetector(abc.ABC): + def __init__(self, raise_on_error=False): + self.raise_on_error = raise_on_error + + @abc.abstractmethod + def detect(self) -> "Resource": + raise NotImplementedError() + + +class OTELResourceDetector(ResourceDetector): + # pylint: disable=no-self-use + def detect(self) -> "Resource": + env_resources_items = os.environ.get("OTEL_RESOURCE") + env_resource_map = {} + if env_resources_items: + env_resource_map = { + key.strip(): value.strip() + for key, value in ( + item.split("=") for item in env_resources_items.split(",") + ) + } + return Resource(env_resource_map) + + +def get_aggregated_resources( + detectors: typing.List["ResourceDetector"], + initial_resource: typing.Optional[Resource] = None, + timeout=5, +) -> "Resource": + """ Retrieves resources from detectors in the order that they were passed + + :param detectors: List of resources in order of priority + :param initial_resource: Static resource. This has highest priority + :param timeout: Number of seconds to wait for each detector to return + :return: + """ + final_resource = initial_resource or _EMPTY_RESOURCE + detectors = [OTELResourceDetector()] + detectors + + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: + futures = [executor.submit(detector.detect) for detector in detectors] + for detector_ind, future in enumerate(futures): + detector = detectors[detector_ind] + try: + detected_resources = future.result(timeout=timeout) + # pylint: disable=broad-except + except Exception as ex: + if detector.raise_on_error: + raise ex + logger.warning( + "Exception %s in detector %s, ignoring", ex, detector + ) + detected_resources = _EMPTY_RESOURCE + finally: + final_resource = final_resource.merge(detected_resources) + return final_resource diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index db377c0924..1118b42488 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -520,6 +520,7 @@ def to_json(self, indent=4): f_span["attributes"] = self._format_attributes(self.attributes) f_span["events"] = self._format_events(self.events) f_span["links"] = self._format_links(self.links) + f_span["resource"] = self.resource.labels return json.dumps(f_span, indent=indent) diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 959e23f0de..698b4fdda3 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -14,7 +14,9 @@ # pylint: disable=protected-access +import os import unittest +from unittest import mock from opentelemetry.sdk import resources @@ -83,3 +85,94 @@ def test_immutability(self): labels["cost"] = 999.91 self.assertEqual(resource.labels, labels_copy) + + def test_otel_resource_detector(self): + detector = resources.OTELResourceDetector() + self.assertEqual(detector.detect(), resources.Resource.create_empty()) + os.environ["OTEL_RESOURCE"] = "k=v" + self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) + os.environ["OTEL_RESOURCE"] = " k = v " + self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) + os.environ["OTEL_RESOURCE"] = "k=v,k2=v2" + self.assertEqual( + detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) + ) + os.environ["OTEL_RESOURCE"] = " k = v , k2 = v2 " + self.assertEqual( + detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) + ) + + def test_aggregated_resources(self): + aggregated_resources = resources.get_aggregated_resources([]) + self.assertEqual( + aggregated_resources, resources.Resource.create_empty() + ) + + static_resource = resources.Resource({"static_key": "static_value"}) + + self.assertEqual( + resources.get_aggregated_resources( + [], initial_resource=static_resource + ), + static_resource, + ) + + resource_detector = mock.Mock(spec=resources.ResourceDetector) + resource_detector.detect.return_value = resources.Resource( + {"static_key": "overwrite_existing_value", "key": "value"} + ) + self.assertEqual( + resources.get_aggregated_resources( + [resource_detector], initial_resource=static_resource + ), + resources.Resource({"static_key": "static_value", "key": "value"}), + ) + + resource_detector1 = mock.Mock(spec=resources.ResourceDetector) + resource_detector1.detect.return_value = resources.Resource( + {"key1": "value1"} + ) + resource_detector2 = mock.Mock(spec=resources.ResourceDetector) + resource_detector2.detect.return_value = resources.Resource( + {"key2": "value2", "key3": "value3"} + ) + resource_detector3 = mock.Mock(spec=resources.ResourceDetector) + resource_detector3.detect.return_value = resources.Resource( + { + "key2": "overwrite_existing_value", + "key3": "overwrite_existing_value", + "key4": "value4", + } + ) + self.assertEqual( + resources.get_aggregated_resources( + [resource_detector1, resource_detector2, resource_detector3] + ), + resources.Resource( + { + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + } + ), + ) + + resource_detector = mock.Mock(spec=resources.ResourceDetector) + resource_detector.detect.side_effect = Exception() + resource_detector.raise_on_error = False + self.assertEqual( + resources.get_aggregated_resources( + [resource_detector, resource_detector1] + ), + resources.Resource({"key1": "value1"}), + ) + + resource_detector = mock.Mock(spec=resources.ResourceDetector) + resource_detector.detect.side_effect = Exception() + resource_detector.raise_on_error = True + self.assertRaises( + Exception, + resources.get_aggregated_resources, + [resource_detector, resource_detector1], + ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index d68d5ca420..5203ae0afc 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -954,10 +954,11 @@ def test_to_json(self): "end_time": null, "attributes": {}, "events": [], - "links": [] + "links": [], + "resource": {} }""", ) self.assertEqual( span.to_json(indent=None), - '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "{}"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "attributes": {}, "events": [], "links": []}', + '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "{}"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "attributes": {}, "events": [], "links": [], "resource": {}}', ) From a8a0ed40df851b8e6477d188c393b89c84f15e42 Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Sun, 5 Jul 2020 22:06:58 -0400 Subject: [PATCH 0445/1517] add license and manifest (#880) --- .../LICENSE | 201 ++++++++++++++++++ .../MANIFEST.in | 9 + .../LICENSE | 201 ++++++++++++++++++ .../MANIFEST.in | 9 + 4 files changed, 420 insertions(+) create mode 100644 ext/opentelemetry-exporter-cloud-monitoring/LICENSE create mode 100644 ext/opentelemetry-exporter-cloud-monitoring/MANIFEST.in create mode 100644 ext/opentelemetry-exporter-cloud-trace/LICENSE create mode 100644 ext/opentelemetry-exporter-cloud-trace/MANIFEST.in diff --git a/ext/opentelemetry-exporter-cloud-monitoring/LICENSE b/ext/opentelemetry-exporter-cloud-monitoring/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-monitoring/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-exporter-cloud-monitoring/MANIFEST.in b/ext/opentelemetry-exporter-cloud-monitoring/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-monitoring/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-exporter-cloud-trace/LICENSE b/ext/opentelemetry-exporter-cloud-trace/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-exporter-cloud-trace/MANIFEST.in b/ext/opentelemetry-exporter-cloud-trace/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE From c0a5be5f1cd90ad8b19482a6cdbdeed8c583b210 Mon Sep 17 00:00:00 2001 From: Abhilash Gnan Date: Tue, 7 Jul 2020 17:34:15 +0200 Subject: [PATCH 0446/1517] ext/wsgi: Set span status on wsgi errors (#864) --- ext/opentelemetry-ext-wsgi/CHANGELOG.md | 3 +++ .../src/opentelemetry/ext/wsgi/__init__.py | 5 ++--- .../tests/test_wsgi_middleware.py | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/ext/opentelemetry-ext-wsgi/CHANGELOG.md b/ext/opentelemetry-ext-wsgi/CHANGELOG.md index a82a5de7c8..da9df382f4 100644 --- a/ext/opentelemetry-ext-wsgi/CHANGELOG.md +++ b/ext/opentelemetry-ext-wsgi/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Set span status on wsgi errors + ([#864](https://github.com/open-telemetry/opentelemetry-python/pull/864)) + ## 0.4a0 Released 2020-02-21 diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 9968a0c1c8..709de24edf 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ This library provides a WSGI middleware that can be used on any WSGI framework (such as Django / Flask) to track requests timing through OpenTelemetry. @@ -211,8 +210,8 @@ def __call__(self, environ, start_response): return _end_span_after_iterating( iterable, span, self.tracer, token ) - except: # noqa - # TODO Set span status (cf. https://github.com/open-telemetry/opentelemetry-python/issues/292) + except Exception as ex: + span.set_status(Status(StatusCanonicalCode.INTERNAL, str(ex))) span.end() context.detach(token) raise diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index 0d018b6841..2a2d518513 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -21,6 +21,7 @@ import opentelemetry.ext.wsgi as otel_wsgi from opentelemetry import trace as trace_api from opentelemetry.test.wsgitestutil import WsgiTestBase +from opentelemetry.trace.status import StatusCanonicalCode class Response: @@ -73,6 +74,11 @@ def error_wsgi(environ, start_response): return [b"*"] +def error_wsgi_unhandled(environ, start_response): + assert isinstance(environ, dict) + raise ValueError + + class TestWsgiApplication(WsgiTestBase): def validate_response( self, response, error=None, span_name="HTTP GET", http_method="GET" @@ -148,6 +154,15 @@ def test_wsgi_exc_info(self): response = app(self.environ, self.start_response) self.validate_response(response, error=ValueError) + def test_wsgi_internal_error(self): + app = otel_wsgi.OpenTelemetryMiddleware(error_wsgi_unhandled) + self.assertRaises(ValueError, app, self.environ, self.start_response) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual( + span_list[0].status.canonical_code, StatusCanonicalCode.INTERNAL, + ) + def test_override_span_name(self): """Test that span_names can be overwritten by our callback function.""" span_name = "Dymaxion" From d0e125ae52e713d18a90873ed3f42703f8d2317e Mon Sep 17 00:00:00 2001 From: Connor Adams Date: Tue, 7 Jul 2020 15:23:32 -0400 Subject: [PATCH 0447/1517] Change exclude lists to just exclude_urls, add tests for flask and django (#872) --- ext/opentelemetry-ext-django/CHANGELOG.md | 2 + ext/opentelemetry-ext-django/README.rst | 12 ++-- .../opentelemetry/ext/django/middleware.py | 31 +++------- .../tests/test_middleware.py | 31 +++++++++- ext/opentelemetry-ext-django/tests/views.py | 6 +- ext/opentelemetry-ext-flask/CHANGELOG.md | 2 + ext/opentelemetry-ext-flask/README.rst | 11 ++-- .../src/opentelemetry/ext/flask/__init__.py | 30 +++------ .../tests/test_programmatic.py | 24 ++++++++ ext/opentelemetry-ext-pyramid/CHANGELOG.md | 2 + ext/opentelemetry-ext-pyramid/README.rst | 11 ++++ .../opentelemetry/ext/pyramid/callbacks.py | 24 +++----- .../tests/test_programmatic.py | 9 +-- .../src/opentelemetry/util/__init__.py | 31 +++------- .../tests/util/test_exclude_list.py | 61 +++++++++++++++++++ 15 files changed, 188 insertions(+), 99 deletions(-) create mode 100644 opentelemetry-api/tests/util/test_exclude_list.py diff --git a/ext/opentelemetry-ext-django/CHANGELOG.md b/ext/opentelemetry-ext-django/CHANGELOG.md index 9c578d962f..abbf90451b 100644 --- a/ext/opentelemetry-ext-django/CHANGELOG.md +++ b/ext/opentelemetry-ext-django/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) + ## 0.8b0 Released 2020-05-27 diff --git a/ext/opentelemetry-ext-django/README.rst b/ext/opentelemetry-ext-django/README.rst index 834bd63249..1759cb6602 100644 --- a/ext/opentelemetry-ext-django/README.rst +++ b/ext/opentelemetry-ext-django/README.rst @@ -20,11 +20,15 @@ Configuration Exclude lists ************* -Excludes certain hosts and paths from being tracked. Pass in comma delimited string into environment variables. -Host refers to the entire url and path refers to the part of the url after the domain. Host matches the exact string that is given, where as path matches if the url starts with the given excluded path. +To exclude certain URLs from being tracked, set the environment variable ``OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. -Excluded hosts: OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_HOSTS -Excluded paths: OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_PATHS +For example, + +:: + + export OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_URLS="client/.*/info,healthcheck" + +will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. References ---------- diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py index 0ce03b97cb..c0cb244756 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py @@ -24,7 +24,7 @@ ) from opentelemetry.propagators import extract from opentelemetry.trace import SpanKind, get_tracer -from opentelemetry.util import disable_trace +from opentelemetry.util import ExcludeList try: from django.utils.deprecation import MiddlewareMixin @@ -44,12 +44,11 @@ class _DjangoMiddleware(MiddlewareMixin): _environ_token = "opentelemetry-instrumentor-django.token" _environ_span_key = "opentelemetry-instrumentor-django.span_key" - _excluded_hosts = Configuration().DJANGO_EXCLUDED_HOSTS or [] - _excluded_paths = Configuration().DJANGO_EXCLUDED_PATHS or [] - if _excluded_hosts: - _excluded_hosts = str.split(_excluded_hosts, ",") - if _excluded_paths: - _excluded_paths = str.split(_excluded_paths, ",") + _excluded_urls = Configuration().DJANGO_EXCLUDED_URLS or [] + if _excluded_urls: + _excluded_urls = ExcludeList(str.split(_excluded_urls, ",")) + else: + _excluded_urls = ExcludeList(_excluded_urls) def process_view( self, request, view_func, view_args, view_kwargs @@ -62,11 +61,7 @@ def process_view( # key.lower().replace('_', '-').replace("http-", "", 1): value # for key, value in request.META.items() # } - if disable_trace( - request.build_absolute_uri("?"), - self._excluded_hosts, - self._excluded_paths, - ): + if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return environ = request.META @@ -97,11 +92,7 @@ def process_exception(self, request, exception): # Django can call this method and process_response later. In order # to avoid __exit__ and detach from being called twice then, the # respective keys are being removed here. - if disable_trace( - request.build_absolute_uri("?"), - self._excluded_hosts, - self._excluded_paths, - ): + if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return if self._environ_activation_key in request.META.keys(): @@ -116,11 +107,7 @@ def process_exception(self, request, exception): request.META.pop(self._environ_token, None) def process_response(self, request, response): - if disable_trace( - request.build_absolute_uri("?"), - self._excluded_hosts, - self._excluded_paths, - ): + if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return response if ( diff --git a/ext/opentelemetry-ext-django/tests/test_middleware.py b/ext/opentelemetry-ext-django/tests/test_middleware.py index e0ac92de52..e5c3e10d03 100644 --- a/ext/opentelemetry-ext-django/tests/test_middleware.py +++ b/ext/opentelemetry-ext-django/tests/test_middleware.py @@ -13,6 +13,7 @@ # limitations under the License. from sys import modules +from unittest.mock import patch from django.conf import settings from django.conf.urls import url @@ -24,15 +25,17 @@ from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import SpanKind from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.util import ExcludeList # pylint: disable=import-error -from .views import error, excluded, excluded2, traced +from .views import error, excluded, excluded_noarg, excluded_noarg2, traced urlpatterns = [ url(r"^traced/", traced), url(r"^error/", error), - url(r"^excluded/", excluded), - url(r"^excluded2/", excluded2), + url(r"^excluded_arg/", excluded), + url(r"^excluded_noarg/", excluded_noarg), + url(r"^excluded_noarg2/", excluded_noarg2), ] _django_instrumentor = DjangoInstrumentor() @@ -111,3 +114,25 @@ def test_error(self): span.attributes["http.url"], "http://testserver/error/" ) self.assertEqual(span.attributes["http.scheme"], "http") + + @patch( + "opentelemetry.ext.django.middleware._DjangoMiddleware._excluded_urls", + ExcludeList(["http://testserver/excluded_arg/123", "excluded_noarg"]), + ) + def test_exclude_lists(self): + client = Client() + client.get("/excluded_arg/123") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + + client.get("/excluded_arg/125") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + client.get("/excluded_noarg/") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + client.get("/excluded_noarg2/") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) diff --git a/ext/opentelemetry-ext-django/tests/views.py b/ext/opentelemetry-ext-django/tests/views.py index 9035f0ea03..b5a2930404 100644 --- a/ext/opentelemetry-ext-django/tests/views.py +++ b/ext/opentelemetry-ext-django/tests/views.py @@ -13,5 +13,9 @@ def excluded(request): # pylint: disable=unused-argument return HttpResponse() -def excluded2(request): # pylint: disable=unused-argument +def excluded_noarg(request): # pylint: disable=unused-argument + return HttpResponse() + + +def excluded_noarg2(request): # pylint: disable=unused-argument return HttpResponse() diff --git a/ext/opentelemetry-ext-flask/CHANGELOG.md b/ext/opentelemetry-ext-flask/CHANGELOG.md index d5b1635919..1db23d4e80 100644 --- a/ext/opentelemetry-ext-flask/CHANGELOG.md +++ b/ext/opentelemetry-ext-flask/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) + ## 0.7b1 Released 2020-05-12 diff --git a/ext/opentelemetry-ext-flask/README.rst b/ext/opentelemetry-ext-flask/README.rst index 0a4c894077..395053972e 100644 --- a/ext/opentelemetry-ext-flask/README.rst +++ b/ext/opentelemetry-ext-flask/README.rst @@ -21,12 +21,15 @@ Configuration Exclude lists ************* -Excludes certain hosts and paths from being tracked. Pass in comma delimited string into environment variables. -Host refers to the entire url and path refers to the part of the url after the domain. Host matches the exact string that is given, where as path matches if the url starts with the given excluded path. +To exclude certain URLs from being tracked, set the environment variable ``OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. -Excluded hosts: OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_HOSTS -Excluded paths: OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_PATHS +For example, +:: + + export OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_URLS="client/.*/info,healthcheck" + +will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. References ---------- diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index 20d6501a93..464958a7e9 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -55,7 +55,7 @@ def hello(): from opentelemetry import configuration, context, propagators, trace from opentelemetry.ext.flask.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.util import disable_trace, time_ns +from opentelemetry.util import ExcludeList, time_ns _logger = getLogger(__name__) @@ -65,22 +65,14 @@ def hello(): _ENVIRON_TOKEN = "opentelemetry-flask.token" -def get_excluded_hosts(): - hosts = configuration.Configuration().FLASK_EXCLUDED_HOSTS or [] - if hosts: - hosts = str.split(hosts, ",") - return hosts +def get_excluded_urls(): + urls = configuration.Configuration().FLASK_EXCLUDED_URLS or [] + if urls: + urls = str.split(urls, ",") + return ExcludeList(urls) -def get_excluded_paths(): - paths = configuration.Configuration().FLASK_EXCLUDED_PATHS or [] - if paths: - paths = str.split(paths, ",") - return paths - - -_excluded_hosts = get_excluded_hosts() -_excluded_paths = get_excluded_paths() +_excluded_urls = get_excluded_urls() def _rewrapped_app(wsgi_app): @@ -92,9 +84,7 @@ def _wrapped_app(environ, start_response): environ[_ENVIRON_STARTTIME_KEY] = time_ns() def _start_response(status, response_headers, *args, **kwargs): - if not disable_trace( - flask.request.url, _excluded_hosts, _excluded_paths - ): + if not _excluded_urls.url_disabled(flask.request.url): span = flask.request.environ.get(_ENVIRON_SPAN_KEY) if span: @@ -116,7 +106,7 @@ def _start_response(status, response_headers, *args, **kwargs): def _before_request(): - if disable_trace(flask.request.url, _excluded_hosts, _excluded_paths): + if _excluded_urls.url_disabled(flask.request.url): return environ = flask.request.environ @@ -148,7 +138,7 @@ def _before_request(): def _teardown_request(exc): - if disable_trace(flask.request.url, _excluded_hosts, _excluded_paths): + if _excluded_urls.url_disabled(flask.request.url): return activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY) diff --git a/ext/opentelemetry-ext-flask/tests/test_programmatic.py b/ext/opentelemetry-ext-flask/tests/test_programmatic.py index 30fa9c4eb2..3a15979cb4 100644 --- a/ext/opentelemetry-ext-flask/tests/test_programmatic.py +++ b/ext/opentelemetry-ext-flask/tests/test_programmatic.py @@ -12,12 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +from unittest.mock import patch + from flask import Flask, request from opentelemetry import trace from opentelemetry.ext.flask import FlaskInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase +from opentelemetry.util import ExcludeList # pylint: disable=import-error from .base_test import InstrumentationTest @@ -139,3 +142,24 @@ def test_internal_error(self): self.assertEqual(span_list[0].name, "_hello_endpoint") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) + + @patch( + "opentelemetry.ext.flask._excluded_urls", + ExcludeList(["http://localhost/excluded_arg/123", "excluded_noarg"]), + ) + def test_exclude_lists(self): + self.client.get("/excluded_arg/123") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + + self.client.get("/excluded_arg/125") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + self.client.get("/excluded_noarg") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + self.client.get("/excluded_noarg2") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) diff --git a/ext/opentelemetry-ext-pyramid/CHANGELOG.md b/ext/opentelemetry-ext-pyramid/CHANGELOG.md index efb1a08bbc..586f8bb5ce 100644 --- a/ext/opentelemetry-ext-pyramid/CHANGELOG.md +++ b/ext/opentelemetry-ext-pyramid/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) + ## 0.9b0 Released 2020-06-10 diff --git a/ext/opentelemetry-ext-pyramid/README.rst b/ext/opentelemetry-ext-pyramid/README.rst index c7890ad095..319d6bff4c 100644 --- a/ext/opentelemetry-ext-pyramid/README.rst +++ b/ext/opentelemetry-ext-pyramid/README.rst @@ -13,6 +13,17 @@ Installation pip install opentelemetry-ext-pyramid +Exclude lists +************* +To exclude certain URLs from being tracked, set the environment variable ``OPENTELEMETRY_PYTHON_PYRAMID_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. + +For example, + +:: + + export OPENTELEMETRY_PYTHON_PYRAMID_EXCLUDED_URLS="client/.*/info,healthcheck" + +will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. References ---------- diff --git a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/callbacks.py b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/callbacks.py index e19e447bee..4d3b81fdab 100644 --- a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/callbacks.py +++ b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/callbacks.py @@ -8,7 +8,7 @@ import opentelemetry.ext.wsgi as otel_wsgi from opentelemetry import configuration, context, propagators, trace from opentelemetry.ext.pyramid.version import __version__ -from opentelemetry.util import disable_trace, time_ns +from opentelemetry.util import ExcludeList, time_ns TWEEN_NAME = "opentelemetry.ext.pyramid.trace_tween_factory" SETTING_TRACE_ENABLED = "opentelemetry-pyramid.trace_enabled" @@ -22,22 +22,14 @@ _logger = getLogger(__name__) -def get_excluded_hosts(): - hosts = configuration.Configuration().PYRAMID_EXCLUDED_HOSTS or [] - if hosts: - hosts = str.split(hosts, ",") - return hosts +def get_excluded_urls(): + urls = configuration.Configuration().PYRAMID_EXCLUDED_URLS or [] + if urls: + urls = str.split(urls, ",") + return ExcludeList(urls) -def get_excluded_paths(): - paths = configuration.Configuration().PYRAMID_EXCLUDED_PATHS or [] - if paths: - paths = str.split(paths, ",") - return paths - - -_excluded_hosts = get_excluded_hosts() -_excluded_paths = get_excluded_paths() +_excluded_urls = get_excluded_urls() def includeme(config): @@ -119,7 +111,7 @@ def disabled_tween(request): # make a request tracing function def trace_tween(request): - if disable_trace(request.url, _excluded_hosts, _excluded_paths): + if _excluded_urls.url_disabled(request.url): request.environ[_ENVIRON_ENABLED_KEY] = False # short-circuit when we don't want to trace anything return handler(request) diff --git a/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py b/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py index e8937e8f5e..d086fc37eb 100644 --- a/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py +++ b/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py @@ -20,6 +20,7 @@ from opentelemetry.ext.pyramid import PyramidInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase +from opentelemetry.util import ExcludeList # pylint: disable=import-error from .pyramid_base_test import InstrumentationTest @@ -169,12 +170,8 @@ def test_warnings(self, mock_logger): self.assertEqual(mock_logger.warning.called, True) @patch( - "opentelemetry.ext.pyramid.callbacks._excluded_hosts", - ["http://localhost/excluded_arg/123"], - ) - @patch( - "opentelemetry.ext.pyramid.callbacks._excluded_paths", - ["excluded_noarg"], + "opentelemetry.ext.pyramid.callbacks._excluded_urls", + ExcludeList(["http://localhost/excluded_arg/123", "excluded_noarg"]), ) def test_exclude_lists(self): self.client.get("/excluded_arg/123") diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index ed1268fcb6..1e4097cb9e 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -67,28 +67,13 @@ def _load_trace_provider(provider: str) -> "TracerProvider": return cast("TracerProvider", _load_provider(provider)) -# Pattern for matching up until the first '/' after the 'https://' part. -_URL_PATTERN = r"(https?|ftp)://.*?/" +class ExcludeList: + """Class to exclude certain paths (given as a list of regexes) from tracing requests""" + def __init__(self, excluded_urls: Sequence[str]): + self._non_empty = len(excluded_urls) > 0 + if self._non_empty: + self._regex = re.compile("|".join(excluded_urls)) -def disable_tracing_path(url: str, excluded_paths: Sequence[str]) -> bool: - if excluded_paths: - # Match only the part after the first '/' that is not in _URL_PATTERN - regex = "{}({})".format(_URL_PATTERN, "|".join(excluded_paths)) - if re.match(regex, url): - return True - return False - - -def disable_tracing_hostname( - url: str, excluded_hostnames: Sequence[str] -) -> bool: - return url in excluded_hostnames - - -def disable_trace( - url: str, excluded_hosts: Sequence[str], excluded_paths: Sequence[str] -) -> bool: - return disable_tracing_hostname( - url, excluded_hosts - ) or disable_tracing_path(url, excluded_paths) + def url_disabled(self, url: str) -> bool: + return bool(self._non_empty and re.search(self._regex, url)) diff --git a/opentelemetry-api/tests/util/test_exclude_list.py b/opentelemetry-api/tests/util/test_exclude_list.py new file mode 100644 index 0000000000..da51478de3 --- /dev/null +++ b/opentelemetry-api/tests/util/test_exclude_list.py @@ -0,0 +1,61 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from opentelemetry.util import ExcludeList + + +class TestExcludeList(unittest.TestCase): + def test_basic(self): + regexes = ExcludeList(["path/123", "http://site.com/other_path"]) + self.assertTrue(regexes.url_disabled("http://site.com/path/123")) + self.assertTrue(regexes.url_disabled("http://site.com/path/123/abc")) + self.assertTrue( + regexes.url_disabled("https://site.com/path/123?arg=other") + ) + + self.assertFalse(regexes.url_disabled("https://site.com/path/abc/123")) + self.assertFalse(regexes.url_disabled("https://site.com/path")) + + self.assertTrue(regexes.url_disabled("http://site.com/other_path")) + self.assertTrue(regexes.url_disabled("http://site.com/other_path?abc")) + + self.assertFalse(regexes.url_disabled("https://site.com/other_path")) + self.assertFalse( + regexes.url_disabled("https://site.com/abc/other_path") + ) + + def test_regex(self): + regexes = ExcludeList( + [r"^https?://site\.com/path/123$", r"^http://.*\?arg=foo"] + ) + + self.assertTrue(regexes.url_disabled("http://site.com/path/123")) + self.assertTrue(regexes.url_disabled("https://site.com/path/123")) + + self.assertFalse(regexes.url_disabled("http://site.com/path/123/abc")) + self.assertFalse(regexes.url_disabled("http://site,com/path/123")) + + self.assertTrue( + regexes.url_disabled("http://site.com/path/123?arg=foo") + ) + self.assertTrue( + regexes.url_disabled("http://site.com/path/123?arg=foo,arg2=foo2") + ) + + self.assertFalse( + regexes.url_disabled("https://site.com/path/123?arg=foo") + ) From 34698ff82dd0003c78893fbf78e3ec6e9022b68a Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 7 Jul 2020 16:50:40 -0600 Subject: [PATCH 0448/1517] chore: Remove unused pytest marks (#884) --- .../tests/asyncpg/test_asyncpg_functional.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py b/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py index 408e50feb8..c5f5557438 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py @@ -2,7 +2,6 @@ import os import asyncpg -import pytest from opentelemetry.ext.asyncpg import AsyncPGInstrumentor from opentelemetry.test.test_base import TestBase @@ -42,7 +41,6 @@ def setUpClass(cls): def tearDownClass(cls): AsyncPGInstrumentor().uninstrument() - @pytest.mark.asyncpg def test_instrumented_execute_method_without_arguments(self, *_, **__): _await(self._connection.execute("SELECT 42;")) spans = self.memory_exporter.get_finished_spans() @@ -60,7 +58,6 @@ def test_instrumented_execute_method_without_arguments(self, *_, **__): }, ) - @pytest.mark.asyncpg def test_instrumented_execute_method_with_arguments(self, *_, **__): _await(self._connection.execute("SELECT $1;", "1")) spans = self.memory_exporter.get_finished_spans() @@ -79,7 +76,6 @@ def test_instrumented_execute_method_with_arguments(self, *_, **__): }, ) - @pytest.mark.asyncpg def test_instrumented_fetch_method_without_arguments(self, *_, **__): _await(self._connection.fetch("SELECT 42;")) spans = self.memory_exporter.get_finished_spans() @@ -94,7 +90,6 @@ def test_instrumented_fetch_method_without_arguments(self, *_, **__): }, ) - @pytest.mark.asyncpg def test_instrumented_fetch_method_with_arguments(self, *_, **__): _await(self._connection.fetch("SELECT $1;", "1")) spans = self.memory_exporter.get_finished_spans() @@ -110,7 +105,6 @@ def test_instrumented_fetch_method_with_arguments(self, *_, **__): }, ) - @pytest.mark.asyncpg def test_instrumented_executemany_method_with_arguments(self, *_, **__): _await(self._connection.executemany("SELECT $1;", [["1"], ["2"]])) spans = self.memory_exporter.get_finished_spans() @@ -126,7 +120,6 @@ def test_instrumented_executemany_method_with_arguments(self, *_, **__): spans[0].attributes, ) - @pytest.mark.asyncpg def test_instrumented_execute_interface_error_method(self, *_, **__): with self.assertRaises(asyncpg.InterfaceError): _await(self._connection.execute("SELECT 42;", 1, 2, 3)) @@ -143,7 +136,6 @@ def test_instrumented_execute_interface_error_method(self, *_, **__): }, ) - @pytest.mark.asyncpg def test_instrumented_transaction_method(self, *_, **__): async def _transaction_execute(): async with self._connection.transaction(): @@ -190,7 +182,6 @@ async def _transaction_execute(): StatusCanonicalCode.OK, spans[2].status.canonical_code ) - @pytest.mark.asyncpg def test_instrumented_failed_transaction_method(self, *_, **__): async def _transaction_execute(): async with self._connection.transaction(): From 09df35c949948471e07de62cec31436aa0cb2c85 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 8 Jul 2020 00:06:08 -0600 Subject: [PATCH 0449/1517] Update instrumentors to use span processors (#852) Fixes #832. By having tracer creation occur on demand, late tracer provider configuration will be honored. This resolves issues with instrumentation occurring before tracer providers are set by the application developer, which would result in the no-op tracer used for the lifetime of the instrumentation. Co-authored-by: alrex Co-authored-by: Leighton Chen Co-authored-by: Yusuke Tsutsumi --- .../src/opentelemetry/ext/dbapi/__init__.py | 41 ++++++++++++++----- .../src/opentelemetry/ext/mysql/__init__.py | 10 ++--- .../opentelemetry/ext/psycopg2/__init__.py | 11 ++--- .../src/opentelemetry/ext/pymysql/__init__.py | 13 +++--- .../opentelemetry/ext/requests/__init__.py | 13 +++--- .../src/opentelemetry/ext/sqlite3/__init__.py | 9 ++-- .../src/opentelemetry/util/__init__.py | 2 +- 7 files changed, 55 insertions(+), 44 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index 6b81bedf03..b34e41b2b4 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -48,7 +48,7 @@ from opentelemetry.ext.dbapi.version import __version__ from opentelemetry.instrumentation.utils import unwrap -from opentelemetry.trace import SpanKind, Tracer, TracerProvider, get_tracer +from opentelemetry.trace import SpanKind, TracerProvider, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode logger = logging.getLogger(__name__) @@ -76,24 +76,27 @@ def trace_integration( tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to use. If ommited the current configured one is used. """ - tracer = get_tracer(__name__, __version__, tracer_provider) wrap_connect( - tracer, + __name__, connect_module, connect_method_name, database_component, database_type, connection_attributes, + version=__version__, + tracer_provider=tracer_provider, ) def wrap_connect( - tracer: Tracer, + name: str, connect_module: typing.Callable[..., typing.Any], connect_method_name: str, database_component: str, database_type: str = "", connection_attributes: typing.Dict = None, + version: str = "", + tracer_provider: typing.Optional[TracerProvider] = None, ): """Integrate with DB API library. https://www.python.org/dev/peps/pep-0249/ @@ -117,10 +120,12 @@ def wrap_connect_( kwargs: typing.Dict[typing.Any, typing.Any], ): db_integration = DatabaseApiIntegration( - tracer, + name, database_component, database_type=database_type, connection_attributes=connection_attributes, + version=version, + tracer_provider=tracer_provider, ) return db_integration.wrapped_connection(wrapped, args, kwargs) @@ -146,11 +151,13 @@ def unwrap_connect( def instrument_connection( - tracer, + name: str, connection, database_component: str, database_type: str = "", connection_attributes: typing.Dict = None, + version: str = "", + tracer_provider: typing.Optional[TracerProvider] = None, ): """Enable instrumentation in a database connection. @@ -167,10 +174,12 @@ def instrument_connection( An instrumented connection. """ db_integration = DatabaseApiIntegration( - tracer, + name, database_component, database_type, connection_attributes=connection_attributes, + version=version, + tracer_provider=tracer_provider, ) db_integration.get_connection_attributes(connection) return get_traced_connection_proxy(connection, db_integration) @@ -195,10 +204,12 @@ def uninstrument_connection(connection): class DatabaseApiIntegration: def __init__( self, - tracer: Tracer, + name: str, database_component: str, database_type: str = "sql", connection_attributes=None, + version: str = "", + tracer_provider: typing.Optional[TracerProvider] = None, ): self.connection_attributes = connection_attributes if self.connection_attributes is None: @@ -208,7 +219,9 @@ def __init__( "host": "host", "user": "user", } - self.tracer = tracer + self._name = name + self._version = version + self._tracer_provider = tracer_provider self.database_component = database_component self.database_type = database_type self.connection_props = {} @@ -216,6 +229,13 @@ def __init__( self.name = "" self.database = "" + def get_tracer(self): + return get_tracer( + self._name, + instrumenting_library_version=self._version, + tracer_provider=self._tracer_provider, + ) + def wrapped_connection( self, connect_method: typing.Callable[..., typing.Any], @@ -288,7 +308,7 @@ def traced_execution( ): statement = args[0] if args else "" - with self._db_api_integration.tracer.start_as_current_span( + with self._db_api_integration.get_tracer().start_as_current_span( self._db_api_integration.name, kind=SpanKind.CLIENT ) as span: span.set_attribute( @@ -322,6 +342,7 @@ def traced_execution( def get_traced_cursor_proxy(cursor, db_api_integration, *args, **kwargs): _traced_cursor = TracedCursor(db_api_integration) + # pylint: disable=abstract-method class TracedCursorProxy(wrapt.ObjectProxy): diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index daf90c4bbf..883a0aa9bb 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -42,14 +42,12 @@ --- """ -import typing - import mysql.connector from opentelemetry.ext import dbapi from opentelemetry.ext.mysql.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.trace import TracerProvider, get_tracer +from opentelemetry.trace import get_tracer class MySQLInstrumentor(BaseInstrumentor): @@ -69,15 +67,15 @@ def _instrument(self, **kwargs): """ tracer_provider = kwargs.get("tracer_provider") - tracer = get_tracer(__name__, __version__, tracer_provider) - dbapi.wrap_connect( - tracer, + __name__, mysql.connector, "connect", self._DATABASE_COMPONENT, self._DATABASE_TYPE, self._CONNECTION_ATTRIBUTES, + version=__version__, + tracer_provider=tracer_provider, ) def _uninstrument(self, **kwargs): diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py index 871356ab2c..ec6c4c82ad 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py @@ -42,15 +42,12 @@ --- """ -import typing - import psycopg2 -import wrapt from opentelemetry.ext import dbapi from opentelemetry.ext.psycopg2.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.trace import TracerProvider, get_tracer +from opentelemetry.trace import get_tracer class Psycopg2Instrumentor(BaseInstrumentor): @@ -71,15 +68,15 @@ def _instrument(self, **kwargs): tracer_provider = kwargs.get("tracer_provider") - tracer = get_tracer(__name__, __version__, tracer_provider) - dbapi.wrap_connect( - tracer, + __name__, psycopg2, "connect", self._DATABASE_COMPONENT, self._DATABASE_TYPE, self._CONNECTION_ATTRIBUTES, + version=__version__, + tracer_provider=tracer_provider, ) def _uninstrument(self, **kwargs): diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py index b4d75543c6..e44e08e189 100644 --- a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py @@ -43,14 +43,11 @@ --- """ -import typing - import pymysql from opentelemetry.ext import dbapi from opentelemetry.ext.pymysql.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.trace import TracerProvider, get_tracer class PyMySQLInstrumentor(BaseInstrumentor): @@ -70,15 +67,15 @@ def _instrument(self, **kwargs): """ tracer_provider = kwargs.get("tracer_provider") - tracer = get_tracer(__name__, __version__, tracer_provider) - dbapi.wrap_connect( - tracer, + __name__, pymysql, "connect", self._DATABASE_COMPONENT, self._DATABASE_TYPE, self._CONNECTION_ATTRIBUTES, + version=__version__, + tracer_provider=tracer_provider, ) def _uninstrument(self, **kwargs): @@ -95,14 +92,14 @@ def instrument_connection(self, connection): Returns: An instrumented connection. """ - tracer = get_tracer(__name__, __version__) return dbapi.instrument_connection( - tracer, + __name__, connection, self._DATABASE_COMPONENT, self._DATABASE_TYPE, self._CONNECTION_ATTRIBUTES, + version=__version__, ) def uninstrument_connection(self, connection): diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py index 0437c1bf54..a4db0591c8 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py @@ -24,7 +24,8 @@ import requests import opentelemetry.ext.requests - # You can optionally pass a custom TracerProvider to RequestInstrumentor.instrument() + # You can optionally pass a custom TracerProvider to + RequestInstrumentor.instrument() opentelemetry.ext.requests.RequestsInstrumentor().instrument() response = requests.get(url="https://www.example.org/") @@ -49,7 +50,7 @@ from requests.exceptions import InvalidSchema, InvalidURL, MissingSchema from requests.sessions import Session -from opentelemetry import context, propagators, trace +from opentelemetry import context, propagators from opentelemetry.ext.requests.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import http_status_to_canonical_code @@ -72,8 +73,6 @@ def _instrument(tracer_provider=None, span_callback=None): wrapped = Session.request - tracer = trace.get_tracer(__name__, __version__, tracer_provider) - @functools.wraps(wrapped) def instrumented_request(self, method, url, *args, **kwargs): if context.get_value("suppress_instrumentation"): @@ -89,9 +88,9 @@ def instrumented_request(self, method, url, *args, **kwargs): exception = None - with tracer.start_as_current_span( - span_name, kind=SpanKind.CLIENT - ) as span: + with get_tracer( + __name__, __version__, tracer_provider + ).start_as_current_span(span_name, kind=SpanKind.CLIENT) as span: span.set_attribute("component", "http") span.set_attribute("http.method", method.upper()) span.set_attribute("http.url", url) diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py index 174fbba116..3cdfd88f75 100644 --- a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py +++ b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py @@ -43,12 +43,11 @@ """ import sqlite3 -import typing from opentelemetry.ext import dbapi from opentelemetry.ext.sqlite3.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.trace import TracerProvider, get_tracer +from opentelemetry.trace import get_tracer class SQLite3Instrumentor(BaseInstrumentor): @@ -64,15 +63,15 @@ def _instrument(self, **kwargs): """ tracer_provider = kwargs.get("tracer_provider") - tracer = get_tracer(__name__, __version__, tracer_provider) - dbapi.wrap_connect( - tracer, + __name__, sqlite3, "connect", self._DATABASE_COMPONENT, self._DATABASE_TYPE, self._CONNECTION_ATTRIBUTES, + version=__version__, + tracer_provider=tracer_provider, ) def _uninstrument(self, **kwargs): diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index 1e4097cb9e..5d98ba96bf 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -48,7 +48,7 @@ def _load_provider(provider: str) -> Provider: name=cast( str, Configuration().get( - provider, "default_{}".format(provider), + provider.upper(), "default_{}".format(provider), ), ), ) From 8cbd9d4db04f0cd0b5a32c8c8c80299b4a44f9bd Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Fri, 10 Jul 2020 12:13:40 -0400 Subject: [PATCH 0450/1517] Update resources tests (#893) --- .../tests/test_cloud_trace_exporter.py | 14 ++-- .../tests/test_gcp_resource_detector.py | 1 + .../tests/resources/test_resources.py | 78 ++++++++++++------- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py index c5444241b6..7400207ff4 100644 --- a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py +++ b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py @@ -305,8 +305,10 @@ def test_extract_links(self): ), ) - def test_extract_resources(self): + def test_extract_empty_resources(self): self.assertEqual(_extract_resources(Resource.create_empty()), {}) + + def test_extract_well_formed_resources(self): resource = Resource( labels={ "cloud.account.id": 123, @@ -325,12 +327,11 @@ def test_extract_resources(self): } self.assertEqual(_extract_resources(resource), expected_extract) + def test_extract_malformed_resources(self): + # This resource doesn't have all the fields required for a gce_instance + # Specifically its missing "host.id", "cloud.zone", "cloud.account.id" resource = Resource( labels={ - "cloud.account.id": "123", - "host.id": "host", - "extra_info": "extra", - "not_gcp_resource": "value", "gcp.resource_type": "gce_instance", "cloud.provider": "gcp", } @@ -338,6 +339,7 @@ def test_extract_resources(self): # Should throw when passed a malformed GCP resource dict self.assertRaises(KeyError, _extract_resources, resource) + def test_extract_unsupported_gcp_resources(self): resource = Resource( labels={ "cloud.account.id": "123", @@ -350,6 +352,8 @@ def test_extract_resources(self): ) self.assertEqual(_extract_resources(resource), {}) + def test_extract_unsupported_provider_resources(self): + # Resources with currently unsupported providers will be ignored resource = Resource( labels={ "cloud.account.id": "123", diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/test_gcp_resource_detector.py b/ext/opentelemetry-exporter-cloud-trace/tests/test_gcp_resource_detector.py index df82121330..de618acd61 100644 --- a/ext/opentelemetry-exporter-cloud-trace/tests/test_gcp_resource_detector.py +++ b/ext/opentelemetry-exporter-cloud-trace/tests/test_gcp_resource_detector.py @@ -67,6 +67,7 @@ def test_finding_resources(self, getter): ) self.assertEqual(getter.call_count, 1) + # Found resources should be cached and not require another network call found_resources = resource_finder.detect() self.assertEqual(getter.call_count, 1) self.assertEqual( diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 698b4fdda3..3aafad1cf3 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -86,28 +86,13 @@ def test_immutability(self): labels["cost"] = 999.91 self.assertEqual(resource.labels, labels_copy) - def test_otel_resource_detector(self): - detector = resources.OTELResourceDetector() - self.assertEqual(detector.detect(), resources.Resource.create_empty()) - os.environ["OTEL_RESOURCE"] = "k=v" - self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) - os.environ["OTEL_RESOURCE"] = " k = v " - self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) - os.environ["OTEL_RESOURCE"] = "k=v,k2=v2" - self.assertEqual( - detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) - ) - os.environ["OTEL_RESOURCE"] = " k = v , k2 = v2 " - self.assertEqual( - detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) - ) - - def test_aggregated_resources(self): + def test_aggregated_resources_no_detectors(self): aggregated_resources = resources.get_aggregated_resources([]) self.assertEqual( aggregated_resources, resources.Resource.create_empty() ) + def test_aggregated_resources_with_static_resource(self): static_resource = resources.Resource({"static_key": "static_value"}) self.assertEqual( @@ -117,9 +102,10 @@ def test_aggregated_resources(self): static_resource, ) + # Static resource values should never be overwritten resource_detector = mock.Mock(spec=resources.ResourceDetector) resource_detector.detect.return_value = resources.Resource( - {"static_key": "overwrite_existing_value", "key": "value"} + {"static_key": "try_to_overwrite_existing_value", "key": "value"} ) self.assertEqual( resources.get_aggregated_resources( @@ -128,6 +114,7 @@ def test_aggregated_resources(self): resources.Resource({"static_key": "static_value", "key": "value"}), ) + def test_aggregated_resources_multiple_detectors(self): resource_detector1 = mock.Mock(spec=resources.ResourceDetector) resource_detector1.detect.return_value = resources.Resource( {"key1": "value1"} @@ -139,11 +126,13 @@ def test_aggregated_resources(self): resource_detector3 = mock.Mock(spec=resources.ResourceDetector) resource_detector3.detect.return_value = resources.Resource( { - "key2": "overwrite_existing_value", - "key3": "overwrite_existing_value", + "key2": "try_to_overwrite_existing_value", + "key3": "try_to_overwrite_existing_value", "key4": "value4", } ) + + # New values should not overwrite existing values self.assertEqual( resources.get_aggregated_resources( [resource_detector1, resource_detector2, resource_detector3] @@ -158,21 +147,56 @@ def test_aggregated_resources(self): ), ) + def test_resource_detector_ignore_error(self): resource_detector = mock.Mock(spec=resources.ResourceDetector) resource_detector.detect.side_effect = Exception() resource_detector.raise_on_error = False self.assertEqual( - resources.get_aggregated_resources( - [resource_detector, resource_detector1] - ), - resources.Resource({"key1": "value1"}), + resources.get_aggregated_resources([resource_detector]), + resources.Resource.create_empty(), ) + def test_resource_detector_raise_error(self): resource_detector = mock.Mock(spec=resources.ResourceDetector) resource_detector.detect.side_effect = Exception() resource_detector.raise_on_error = True self.assertRaises( - Exception, - resources.get_aggregated_resources, - [resource_detector, resource_detector1], + Exception, resources.get_aggregated_resources, [resource_detector], + ) + + +class TestOTELResourceDetector(unittest.TestCase): + def setUp(self) -> None: + os.environ["OTEL_RESOURCE"] = "" + + def tearDown(self) -> None: + os.environ.pop("OTEL_RESOURCE") + + def test_empty(self): + detector = resources.OTELResourceDetector() + os.environ["OTEL_RESOURCE"] = "" + self.assertEqual(detector.detect(), resources.Resource.create_empty()) + + def test_one(self): + detector = resources.OTELResourceDetector() + os.environ["OTEL_RESOURCE"] = "k=v" + self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) + + def test_one_with_whitespace(self): + detector = resources.OTELResourceDetector() + os.environ["OTEL_RESOURCE"] = " k = v " + self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) + + def test_multiple(self): + detector = resources.OTELResourceDetector() + os.environ["OTEL_RESOURCE"] = "k=v,k2=v2" + self.assertEqual( + detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) + ) + + def test_multiple_with_whitespace(self): + detector = resources.OTELResourceDetector() + os.environ["OTEL_RESOURCE"] = " k = v , k2 = v2 " + self.assertEqual( + detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) ) From 61edbfaefd039546f8c86455b5867bdb60af399a Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 10 Jul 2020 11:24:24 -0700 Subject: [PATCH 0451/1517] Return INVALID_SPAN on get_current_span instead of None (#891) --- .../cloud_trace/cloud_trace_propagator.py | 2 -- .../src/opentelemetry/ext/datadog/propagator.py | 3 +++ .../tests/test_server_interceptor.py | 4 ++-- .../ext/opentracing_shim/__init__.py | 2 +- opentelemetry-api/CHANGELOG.md | 3 +++ .../opentelemetry/trace/propagation/__init__.py | 8 +++----- .../propagation/tracecontexthttptextformat.py | 2 -- opentelemetry-api/tests/trace/test_globals.py | 4 ++-- opentelemetry-api/tests/trace/test_tracer.py | 6 ++++++ .../sdk/trace/propagation/b3_format.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 16 ++++++++-------- 11 files changed, 29 insertions(+), 23 deletions(-) diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/cloud_trace_propagator.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/cloud_trace_propagator.py index 124863e39e..3f00c66d69 100644 --- a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/cloud_trace_propagator.py +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/cloud_trace_propagator.py @@ -77,8 +77,6 @@ def inject( context: typing.Optional[Context] = None, ) -> None: span = trace.get_current_span(context) - if span is None: - return span_context = span.get_context() if span_context == trace.INVALID_SPAN_CONTEXT: return diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py index 6ff192f425..5935557de8 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py @@ -86,6 +86,9 @@ def inject( context: typing.Optional[Context] = None, ) -> None: span = get_current_span(context) + span_context = span.get_context() + if span_context == trace.INVALID_SPAN_CONTEXT: + return sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 set_in_carrier( carrier, self.TRACE_ID_KEY, format_trace_id(span.context.trace_id), diff --git a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py index 67ff0053bf..ebe2a8c160 100644 --- a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py +++ b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py @@ -118,8 +118,8 @@ def handler(request, context): server.stop(None) active_span_after_call = trace.get_current_span() - self.assertIsNone(active_span_before_call) - self.assertIsNone(active_span_after_call) + self.assertEqual(active_span_before_call, trace.INVALID_SPAN) + self.assertEqual(active_span_after_call, trace.INVALID_SPAN) self.assertIsInstance(active_span_in_handler, trace_sdk.Span) self.assertIsNone(active_span_in_handler.parent) diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index 81f25013ff..0b948216ff 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -470,7 +470,7 @@ def active(self): """ span = trace_api.get_current_span() - if span is None: + if span.get_context() == trace_api.INVALID_SPAN_CONTEXT: return None span_context = SpanContextShim(span.get_context()) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 9a2647da26..413946b85b 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Return INVALID_SPAN if no TracerProvider set for get_current_span + ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) + ## 0.9b0 Released 2020-06-10 diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 45a07c920d..51146adeb3 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -34,7 +34,7 @@ def set_span_in_context( return ctx -def get_current_span(context: Optional[Context] = None) -> Optional[Span]: +def get_current_span(context: Optional[Context] = None) -> Span: """Retrieve the current span. Args: @@ -42,11 +42,9 @@ def get_current_span(context: Optional[Context] = None) -> Optional[Span]: default current context is used instead. Returns: - The Span set in the context if it exists. None otherwise. + The Span set in the context if it exists. INVALID_SPAN otherwise. """ span = get_value(SPAN_KEY, context=context) - if span is None: - return None - if not isinstance(span, Span): + if span is None or not isinstance(span, Span): return INVALID_SPAN return span diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py index fa2fae8703..1cfd0704e2 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py @@ -120,8 +120,6 @@ def inject( See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` """ span = trace.get_current_span(context) - if span is None: - return span_context = span.get_context() if span_context == trace.INVALID_SPAN_CONTEXT: return diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 5042dd2b72..4e105eb1f3 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -46,7 +46,7 @@ def test_get_current_span(self): """DefaultTracer's start_span will also be retrievable via get_current_span """ - self.assertIs(trace.get_current_span(), None) + self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN) span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) ctx = trace.set_span_in_context(span) token = context.attach(ctx) @@ -54,4 +54,4 @@ def test_get_current_span(self): self.assertIs(trace.get_current_span(), span) finally: context.detach(token) - self.assertIs(trace.get_current_span(), None) + self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN) diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py index 1eb1506230..73909fd8dd 100644 --- a/opentelemetry-api/tests/trace/test_tracer.py +++ b/opentelemetry-api/tests/trace/test_tracer.py @@ -33,3 +33,9 @@ def test_use_span(self): span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) with self.tracer.use_span(span): pass + + def test_get_current_span(self): + with self.tracer.start_as_current_span("test") as span: + trace.get_current_span().set_attribute("test", "test") + self.assertEqual(span, trace.INVALID_SPAN) + self.assertFalse(hasattr("span", "attributes")) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py index 3a9722bc36..c1f71d4d87 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py @@ -123,7 +123,7 @@ def inject( ) -> None: span = trace.get_current_span(context=context) - if span is None: + if span.get_context() == trace.INVALID_SPAN_CONTEXT: return sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 5203ae0afc..a43db73a3d 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -225,7 +225,7 @@ def test_span_processor_for_source(self): def test_start_span_implicit(self): tracer = new_tracer() - self.assertIsNone(trace_api.get_current_span()) + self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) root = tracer.start_span("root") self.assertIsNotNone(root.start_time) @@ -264,7 +264,7 @@ def test_start_span_implicit(self): self.assertIsNotNone(child.end_time) - self.assertIsNone(trace_api.get_current_span()) + self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) self.assertIsNotNone(root.end_time) def test_start_span_explicit(self): @@ -277,7 +277,7 @@ def test_start_span_explicit(self): trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) - self.assertIsNone(trace_api.get_current_span()) + self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) root = tracer.start_span("root") self.assertIsNotNone(root.start_time) @@ -320,7 +320,7 @@ def test_start_span_explicit(self): def test_start_as_current_span_implicit(self): tracer = new_tracer() - self.assertIsNone(trace_api.get_current_span()) + self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) with tracer.start_as_current_span("root") as root: self.assertIs(trace_api.get_current_span(), root) @@ -334,7 +334,7 @@ def test_start_as_current_span_implicit(self): self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) - self.assertIsNone(trace_api.get_current_span()) + self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) self.assertIsNotNone(root.end_time) def test_start_as_current_span_explicit(self): @@ -346,7 +346,7 @@ def test_start_as_current_span_explicit(self): is_remote=False, ) - self.assertIsNone(trace_api.get_current_span()) + self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) # Test with the implicit root span with tracer.start_as_current_span("root") as root: @@ -552,7 +552,7 @@ def test_sampling_attributes(self): self.assertEqual(root.attributes["attr-in-both"], "decision-attr") def test_events(self): - self.assertIsNone(trace_api.get_current_span()) + self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) with self.tracer.start_as_current_span("root") as root: # only event name @@ -608,7 +608,7 @@ def event_formatter(): self.assertEqual(root.events[4].timestamp, now) def test_invalid_event_attributes(self): - self.assertIsNone(trace_api.get_current_span()) + self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) with self.tracer.start_as_current_span("root") as root: root.add_event("event0", {"attr1": True, "attr2": ["hi", False]}) From e0f1410a6844f5883146679f5bd64cd5ee8d67d8 Mon Sep 17 00:00:00 2001 From: alrex Date: Sun, 12 Jul 2020 21:01:11 -0700 Subject: [PATCH 0452/1517] disabling tests that have failed inconsistently (#900) --- .../tests/celery/test_celery_functional.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py b/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py index abba19fc39..03fadb09ae 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py @@ -35,6 +35,7 @@ class MyException(Exception): pass +@pytest.mark.skip(reason="inconsistent test results") def test_instrumentation_info(celery_app, memory_exporter): @celery_app.task def fn_task(): @@ -139,6 +140,7 @@ def fn_task(self): assert span.attributes.get("celery.state") == "SUCCESS" +@pytest.mark.skip(reason="inconsistent test results") def test_fn_task_apply_async(celery_app, memory_exporter): @celery_app.task def fn_task_parameters(user, force_logout=False): @@ -191,6 +193,7 @@ def fn_task(): assert len(spans) == 200 +@pytest.mark.skip(reason="inconsistent test results") def test_fn_task_delay(celery_app, memory_exporter): @celery_app.task def fn_task_parameters(user, force_logout=False): @@ -430,6 +433,7 @@ def add(x, y): assert span.attributes.get("messaging.message_id") == result.task_id +@pytest.mark.skip(reason="inconsistent test results") def test_apply_async_previous_style_tasks( celery_app, celery_worker, memory_exporter ): From b085f376e0b0bd1c76e8ff4e3f09e995927f7311 Mon Sep 17 00:00:00 2001 From: Jonah Rosenblum Date: Mon, 13 Jul 2020 05:00:34 +0000 Subject: [PATCH 0453/1517] fix typo in BatchExportSpanProcessor value error (#897) Co-authored-by: Leighton Chen Co-authored-by: Yusuke Tsutsumi --- .../src/opentelemetry/sdk/trace/export/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index f06f5d2914..9fe55ed7fd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -120,7 +120,7 @@ def __init__( if max_export_batch_size > max_queue_size: raise ValueError( - "max_export_batch_size must be less than and equal to max_queue_size." + "max_export_batch_size must be less than or equal to max_queue_size." ) self.span_exporter = span_exporter From e31c4bbf4e155e735b4fce273953406957602d91 Mon Sep 17 00:00:00 2001 From: bitspradp Date: Mon, 13 Jul 2020 06:50:13 +0100 Subject: [PATCH 0454/1517] ext/jaeger: Fixing the serialization of a tuple element by coercing it into a string (#865) Could not serialize attribute aws.region to tag when exporting via jaeger Serialize tuple type values by coercing them into a string, since Jaeger does not support tuple types. --- ext/opentelemetry-ext-jaeger/CHANGELOG.md | 4 ++++ .../src/opentelemetry/ext/jaeger/__init__.py | 2 ++ ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/ext/opentelemetry-ext-jaeger/CHANGELOG.md b/ext/opentelemetry-ext-jaeger/CHANGELOG.md index 9ed679ee1c..dada0101e0 100644 --- a/ext/opentelemetry-ext-jaeger/CHANGELOG.md +++ b/ext/opentelemetry-ext-jaeger/CHANGELOG.md @@ -8,6 +8,10 @@ Released 2020-05-27 - Transform resource to tags when exporting ([#645](https://github.com/open-telemetry/opentelemetry-python/pull/645)) +- ext/boto: Could not serialize attribute aws.region to tag when exporting via jaeger + Serialize tuple type values by coercing them into a string, since Jaeger does not + support tuple types. + ([#865](https://github.com/open-telemetry/opentelemetry-python/pull/865)) ## 0.6b0 diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py index 18ec0dc7bf..f12031d510 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -314,6 +314,8 @@ def _convert_attribute_to_tag(key, attr): return jaeger.Tag(key=key, vLong=attr, vType=jaeger.TagType.LONG) if isinstance(attr, float): return jaeger.Tag(key=key, vDouble=attr, vType=jaeger.TagType.DOUBLE) + if isinstance(attr, tuple): + return jaeger.Tag(key=key, vStr=str(attr), vType=jaeger.TagType.STRING) logger.warning("Could not serialize attribute %s:%r to tag", key, attr) return None diff --git a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py index 904ca9bff1..7bca8e8317 100644 --- a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py +++ b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py @@ -200,6 +200,7 @@ def test_translate_to_jaeger(self): otel_spans[0].set_attribute("key_bool", False) otel_spans[0].set_attribute("key_string", "hello_world") otel_spans[0].set_attribute("key_float", 111.22) + otel_spans[0].set_attribute("key_tuple", ("tuple_element",)) otel_spans[0].resource = Resource( labels={"key_resource": "some_resource"} ) @@ -241,6 +242,11 @@ def test_translate_to_jaeger(self): vType=jaeger.TagType.DOUBLE, vDouble=111.22, ), + jaeger.Tag( + key="key_tuple", + vType=jaeger.TagType.STRING, + vStr="('tuple_element',)", + ), jaeger.Tag( key="key_resource", vType=jaeger.TagType.STRING, From 7c987d4f55fd277026a32cd0f6757895e2628f59 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 13 Jul 2020 15:57:09 +0000 Subject: [PATCH 0455/1517] step down toumorokoshi to approver (#901) As discussed in the SIG, toumorokoshi is stepping down as a maintainer. Co-authored-by: alrex --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f1be8fbce..47e8189399 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Hector Hernandez](https://github.com/hectorhdzg), Microsoft - [Mauricio Vásquez](https://github.com/mauriciovasquezbernal), Kinvolk - [Reiley Yang](https://github.com/reyang), Microsoft +- [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google *Find more about the approver role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver).* @@ -117,7 +118,6 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t - [Alex Boten](https://github.com/codeboten), Lightstep - [Leighton Chen](https://github.com/lzchen), Microsoft -- [Yusuke Tsutsumi](https://github.com/toumorokoshi), Zillow Group *Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).* From 675334b930a44e2da80a257033aa0af15f8b8785 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 13 Jul 2020 11:15:53 -0600 Subject: [PATCH 0456/1517] Update to OpenTelemetry Proto 0.4.0 (#889) --- ext/opentelemetry-ext-otlp/CHANGELOG.md | 2 + .../ext/otlp/trace_exporter/__init__.py | 38 +-- .../tests/test_otlp_trace_exporter.py | 46 ++-- .../proto/common/v1/common_pb2.py | 241 +++++++++++++----- .../proto/common/v1/common_pb2.pyi | 119 ++++++--- .../proto/resource/v1/resource_pb2.py | 6 +- .../proto/resource/v1/resource_pb2.pyi | 6 +- .../opentelemetry/proto/trace/v1/trace_pb2.py | 30 +-- .../proto/trace/v1/trace_pb2.pyi | 14 +- scripts/proto_codegen.sh | 2 +- 10 files changed, 341 insertions(+), 163 deletions(-) diff --git a/ext/opentelemetry-ext-otlp/CHANGELOG.md b/ext/opentelemetry-ext-otlp/CHANGELOG.md index 896a782491..fbfff4496e 100644 --- a/ext/opentelemetry-ext-otlp/CHANGELOG.md +++ b/ext/opentelemetry-ext-otlp/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Update span exporter to use OpenTelemetry Proto v0.4.0 ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/889)) + ## 0.9b0 Released 2020-06-10 diff --git a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/trace_exporter/__init__.py b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/trace_exporter/__init__.py index a25f785366..47a862908e 100644 --- a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/trace_exporter/__init__.py +++ b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/trace_exporter/__init__.py @@ -15,8 +15,9 @@ """OTLP Span Exporter""" import logging +from collections.abc import Mapping, Sequence from time import sleep -from typing import Sequence +from typing import Sequence as TypingSequence from backoff import expo from google.rpc.error_details_pb2 import RetryInfo @@ -34,7 +35,7 @@ from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( TraceServiceStub, ) -from opentelemetry.proto.common.v1.common_pb2 import AttributeKeyValue +from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue from opentelemetry.proto.resource.v1.resource_pb2 import Resource from opentelemetry.proto.trace.v1.trace_pb2 import ( InstrumentationLibrarySpans, @@ -49,26 +50,31 @@ def _translate_key_values(key, value): - key_value = {"key": key} if isinstance(value, bool): - key_value["bool_value"] = value + any_value = AnyValue(bool_value=value) elif isinstance(value, str): - key_value["string_value"] = value + any_value = AnyValue(string_value=value) elif isinstance(value, int): - key_value["int_value"] = value + any_value = AnyValue(int_value=value) elif isinstance(value, float): - key_value["double_value"] = value + any_value = AnyValue(double_value=value) + + elif isinstance(value, Sequence): + any_value = AnyValue(array_value=value) + + elif isinstance(value, Mapping): + any_value = AnyValue(kvlist_value=value) else: raise Exception( "Invalid type {} of value {}".format(type(value), value) ) - return key_value + return KeyValue(key=key, value=any_value) # pylint: disable=no-member @@ -144,7 +150,7 @@ def _translate_attributes(self, sdk_span): try: self._collector_span_kwargs["attributes"].append( - AttributeKeyValue(**_translate_key_values(key, value)) + _translate_key_values(key, value) ) except Exception as error: # pylint: disable=broad-except logger.exception(error) @@ -163,9 +169,7 @@ def _translate_events(self, sdk_span): for key, value in sdk_span_event.attributes.items(): try: collector_span_event.attributes.append( - AttributeKeyValue( - **_translate_key_values(key, value) - ) + _translate_key_values(key, value) ) # pylint: disable=broad-except except Exception as error: @@ -191,9 +195,7 @@ def _translate_links(self, sdk_span): for key, value in sdk_span_link.attributes.items(): try: collector_span_link.attributes.append( - AttributeKeyValue( - **_translate_key_values(key, value) - ) + _translate_key_values(key, value) ) # pylint: disable=broad-except except Exception as error: @@ -211,7 +213,7 @@ def _translate_status(self, sdk_span): ) def _translate_spans( - self, sdk_spans: Sequence[SDKSpan], + self, sdk_spans: TypingSequence[SDKSpan], ) -> ExportTraceServiceRequest: sdk_resource_instrumentation_library_spans = {} @@ -260,7 +262,7 @@ def _translate_spans( try: collector_resource.attributes.append( - AttributeKeyValue(**_translate_key_values(key, value)) + _translate_key_values(key, value) ) except Exception as error: # pylint: disable=broad-except logger.exception(error) @@ -276,7 +278,7 @@ def _translate_spans( return ExportTraceServiceRequest(resource_spans=resource_spans) - def export(self, spans: Sequence[SDKSpan]) -> SpanExportResult: + def export(self, spans: TypingSequence[SDKSpan]) -> SpanExportResult: # expo returns a generator that yields delay values which grow # exponentially. Once delay is greater than max_value, the yielded # value will remain constant. diff --git a/ext/opentelemetry-ext-otlp/tests/test_otlp_trace_exporter.py b/ext/opentelemetry-ext-otlp/tests/test_otlp_trace_exporter.py index db42f8ff6d..3e7affb221 100644 --- a/ext/opentelemetry-ext-otlp/tests/test_otlp_trace_exporter.py +++ b/ext/opentelemetry-ext-otlp/tests/test_otlp_trace_exporter.py @@ -30,7 +30,7 @@ TraceServiceServicer, add_TraceServiceServicer_to_server, ) -from opentelemetry.proto.common.v1.common_pb2 import AttributeKeyValue +from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as CollectorResource, ) @@ -185,8 +185,10 @@ def test_translate_spans(self): ResourceSpans( resource=CollectorResource( attributes=[ - AttributeKeyValue(key="a", int_value=1), - AttributeKeyValue(key="b", bool_value=False), + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), ] ), instrumentation_library_spans=[ @@ -211,11 +213,13 @@ def test_translate_spans(self): ), kind=CollectorSpan.SpanKind.INTERNAL, attributes=[ - AttributeKeyValue( - key="a", int_value=1 + KeyValue( + key="a", + value=AnyValue(int_value=1), ), - AttributeKeyValue( - key="b", bool_value=True + KeyValue( + key="b", + value=AnyValue(bool_value=True), ), ], events=[ @@ -223,11 +227,17 @@ def test_translate_spans(self): name="a", time_unix_nano=1591240820506462784, attributes=[ - AttributeKeyValue( - key="a", int_value=1 + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), ), - AttributeKeyValue( - key="b", int_value=False + KeyValue( + key="b", + value=AnyValue( + bool_value=False + ), ), ], ) @@ -240,11 +250,17 @@ def test_translate_spans(self): ), span_id=int.to_bytes(2, 8, "big"), attributes=[ - AttributeKeyValue( - key="a", int_value=1 + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), ), - AttributeKeyValue( - key="b", bool_value=False + KeyValue( + key="b", + value=AnyValue( + bool_value=False + ), ), ], ) diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py index dd08ccf6ad..21501a30c0 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py @@ -18,88 +18,58 @@ package='opentelemetry.proto.common.v1', syntax='proto3', serialized_options=b'\n io.opentelemetry.proto.common.v1B\013CommonProtoP\001Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1', - serialized_pb=b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\xf5\x01\n\x11\x41ttributeKeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12H\n\x04type\x18\x02 \x01(\x0e\x32:.opentelemetry.proto.common.v1.AttributeKeyValue.ValueType\x12\x14\n\x0cstring_value\x18\x03 \x01(\t\x12\x11\n\tint_value\x18\x04 \x01(\x03\x12\x14\n\x0c\x64ouble_value\x18\x05 \x01(\x01\x12\x12\n\nbool_value\x18\x06 \x01(\x08\"6\n\tValueType\x12\n\n\x06STRING\x10\x00\x12\x07\n\x03INT\x10\x01\x12\n\n\x06\x44OUBLE\x10\x02\x12\x08\n\x04\x42OOL\x10\x03\",\n\x0eStringKeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"7\n\x16InstrumentationLibrary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\tBq\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1b\x06proto3' + serialized_pb=b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\xf5\x01\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"O\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\",\n\x0eStringKeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"7\n\x16InstrumentationLibrary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\tBq\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1b\x06proto3' ) -_ATTRIBUTEKEYVALUE_VALUETYPE = _descriptor.EnumDescriptor( - name='ValueType', - full_name='opentelemetry.proto.common.v1.AttributeKeyValue.ValueType', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='STRING', index=0, number=0, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='INT', index=1, number=1, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='DOUBLE', index=2, number=2, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='BOOL', index=3, number=3, - serialized_options=None, - type=None), - ], - containing_type=None, - serialized_options=None, - serialized_start=269, - serialized_end=323, -) -_sym_db.RegisterEnumDescriptor(_ATTRIBUTEKEYVALUE_VALUETYPE) - -_ATTRIBUTEKEYVALUE = _descriptor.Descriptor( - name='AttributeKeyValue', - full_name='opentelemetry.proto.common.v1.AttributeKeyValue', +_ANYVALUE = _descriptor.Descriptor( + name='AnyValue', + full_name='opentelemetry.proto.common.v1.AnyValue', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='key', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.key', index=0, + name='string_value', full_name='opentelemetry.proto.common.v1.AnyValue.string_value', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='type', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.type', index=1, - number=2, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, + name='bool_value', full_name='opentelemetry.proto.common.v1.AnyValue.bool_value', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='string_value', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.string_value', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), + name='int_value', full_name='opentelemetry.proto.common.v1.AnyValue.int_value', index=2, + number=3, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='int_value', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.int_value', index=3, - number=4, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, + name='double_value', full_name='opentelemetry.proto.common.v1.AnyValue.double_value', index=3, + number=4, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='double_value', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.double_value', index=4, - number=5, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), + name='array_value', full_name='opentelemetry.proto.common.v1.AnyValue.array_value', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='bool_value', full_name='opentelemetry.proto.common.v1.AttributeKeyValue.bool_value', index=5, - number=6, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, + name='kvlist_value', full_name='opentelemetry.proto.common.v1.AnyValue.kvlist_value', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), @@ -108,19 +78,121 @@ ], nested_types=[], enum_types=[ - _ATTRIBUTEKEYVALUE_VALUETYPE, ], serialized_options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='value', full_name='opentelemetry.proto.common.v1.AnyValue.value', + index=0, containing_type=None, fields=[]), ], serialized_start=78, serialized_end=323, ) +_ARRAYVALUE = _descriptor.Descriptor( + name='ArrayValue', + full_name='opentelemetry.proto.common.v1.ArrayValue', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='values', full_name='opentelemetry.proto.common.v1.ArrayValue.values', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=325, + serialized_end=394, +) + + +_KEYVALUELIST = _descriptor.Descriptor( + name='KeyValueList', + full_name='opentelemetry.proto.common.v1.KeyValueList', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='values', full_name='opentelemetry.proto.common.v1.KeyValueList.values', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=396, + serialized_end=467, +) + + +_KEYVALUE = _descriptor.Descriptor( + name='KeyValue', + full_name='opentelemetry.proto.common.v1.KeyValue', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='opentelemetry.proto.common.v1.KeyValue.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='opentelemetry.proto.common.v1.KeyValue.value', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=469, + serialized_end=548, +) + + _STRINGKEYVALUE = _descriptor.Descriptor( name='StringKeyValue', full_name='opentelemetry.proto.common.v1.StringKeyValue', @@ -154,8 +226,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=325, - serialized_end=369, + serialized_start=550, + serialized_end=594, ) @@ -192,23 +264,68 @@ extension_ranges=[], oneofs=[ ], - serialized_start=371, - serialized_end=426, + serialized_start=596, + serialized_end=651, ) -_ATTRIBUTEKEYVALUE.fields_by_name['type'].enum_type = _ATTRIBUTEKEYVALUE_VALUETYPE -_ATTRIBUTEKEYVALUE_VALUETYPE.containing_type = _ATTRIBUTEKEYVALUE -DESCRIPTOR.message_types_by_name['AttributeKeyValue'] = _ATTRIBUTEKEYVALUE +_ANYVALUE.fields_by_name['array_value'].message_type = _ARRAYVALUE +_ANYVALUE.fields_by_name['kvlist_value'].message_type = _KEYVALUELIST +_ANYVALUE.oneofs_by_name['value'].fields.append( + _ANYVALUE.fields_by_name['string_value']) +_ANYVALUE.fields_by_name['string_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] +_ANYVALUE.oneofs_by_name['value'].fields.append( + _ANYVALUE.fields_by_name['bool_value']) +_ANYVALUE.fields_by_name['bool_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] +_ANYVALUE.oneofs_by_name['value'].fields.append( + _ANYVALUE.fields_by_name['int_value']) +_ANYVALUE.fields_by_name['int_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] +_ANYVALUE.oneofs_by_name['value'].fields.append( + _ANYVALUE.fields_by_name['double_value']) +_ANYVALUE.fields_by_name['double_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] +_ANYVALUE.oneofs_by_name['value'].fields.append( + _ANYVALUE.fields_by_name['array_value']) +_ANYVALUE.fields_by_name['array_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] +_ANYVALUE.oneofs_by_name['value'].fields.append( + _ANYVALUE.fields_by_name['kvlist_value']) +_ANYVALUE.fields_by_name['kvlist_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] +_ARRAYVALUE.fields_by_name['values'].message_type = _ANYVALUE +_KEYVALUELIST.fields_by_name['values'].message_type = _KEYVALUE +_KEYVALUE.fields_by_name['value'].message_type = _ANYVALUE +DESCRIPTOR.message_types_by_name['AnyValue'] = _ANYVALUE +DESCRIPTOR.message_types_by_name['ArrayValue'] = _ARRAYVALUE +DESCRIPTOR.message_types_by_name['KeyValueList'] = _KEYVALUELIST +DESCRIPTOR.message_types_by_name['KeyValue'] = _KEYVALUE DESCRIPTOR.message_types_by_name['StringKeyValue'] = _STRINGKEYVALUE DESCRIPTOR.message_types_by_name['InstrumentationLibrary'] = _INSTRUMENTATIONLIBRARY _sym_db.RegisterFileDescriptor(DESCRIPTOR) -AttributeKeyValue = _reflection.GeneratedProtocolMessageType('AttributeKeyValue', (_message.Message,), { - 'DESCRIPTOR' : _ATTRIBUTEKEYVALUE, +AnyValue = _reflection.GeneratedProtocolMessageType('AnyValue', (_message.Message,), { + 'DESCRIPTOR' : _ANYVALUE, + '__module__' : 'opentelemetry.proto.common.v1.common_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.common.v1.AnyValue) + }) +_sym_db.RegisterMessage(AnyValue) + +ArrayValue = _reflection.GeneratedProtocolMessageType('ArrayValue', (_message.Message,), { + 'DESCRIPTOR' : _ARRAYVALUE, + '__module__' : 'opentelemetry.proto.common.v1.common_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.common.v1.ArrayValue) + }) +_sym_db.RegisterMessage(ArrayValue) + +KeyValueList = _reflection.GeneratedProtocolMessageType('KeyValueList', (_message.Message,), { + 'DESCRIPTOR' : _KEYVALUELIST, + '__module__' : 'opentelemetry.proto.common.v1.common_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.common.v1.KeyValueList) + }) +_sym_db.RegisterMessage(KeyValueList) + +KeyValue = _reflection.GeneratedProtocolMessageType('KeyValue', (_message.Message,), { + 'DESCRIPTOR' : _KEYVALUE, '__module__' : 'opentelemetry.proto.common.v1.common_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.common.v1.AttributeKeyValue) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.common.v1.KeyValue) }) -_sym_db.RegisterMessage(AttributeKeyValue) +_sym_db.RegisterMessage(KeyValue) StringKeyValue = _reflection.GeneratedProtocolMessageType('StringKeyValue', (_message.Message,), { 'DESCRIPTOR' : _STRINGKEYVALUE, diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi index 1061616570..12d629436d 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi @@ -2,22 +2,22 @@ import sys from google.protobuf.descriptor import ( Descriptor as google___protobuf___descriptor___Descriptor, - EnumDescriptor as google___protobuf___descriptor___EnumDescriptor, FileDescriptor as google___protobuf___descriptor___FileDescriptor, ) +from google.protobuf.internal.containers import ( + RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, +) + from google.protobuf.message import ( Message as google___protobuf___message___Message, ) from typing import ( - List as typing___List, - NewType as typing___NewType, + Iterable as typing___Iterable, Optional as typing___Optional, Text as typing___Text, - Tuple as typing___Tuple, Union as typing___Union, - cast as typing___cast, ) from typing_extensions import ( @@ -29,7 +29,6 @@ builtin___bool = bool builtin___bytes = bytes builtin___float = float builtin___int = int -builtin___str = str if sys.version_info < (3,): builtin___buffer = buffer builtin___unicode = unicode @@ -37,56 +36,98 @@ if sys.version_info < (3,): DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... -class AttributeKeyValue(google___protobuf___message___Message): +class AnyValue(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - ValueTypeValue = typing___NewType('ValueTypeValue', builtin___int) - type___ValueTypeValue = ValueTypeValue - class ValueType(object): - DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... + string_value: typing___Text = ... + bool_value: builtin___bool = ... + int_value: builtin___int = ... + double_value: builtin___float = ... + + @property + def array_value(self) -> type___ArrayValue: ... + + @property + def kvlist_value(self) -> type___KeyValueList: ... + + def __init__(self, + *, + string_value : typing___Optional[typing___Text] = None, + bool_value : typing___Optional[builtin___bool] = None, + int_value : typing___Optional[builtin___int] = None, + double_value : typing___Optional[builtin___float] = None, + array_value : typing___Optional[type___ArrayValue] = None, + kvlist_value : typing___Optional[type___KeyValueList] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> AnyValue: ... + else: @classmethod - def Name(cls, number: builtin___int) -> builtin___str: ... + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> AnyValue: ... + def HasField(self, field_name: typing_extensions___Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions___Literal[u"value",b"value"]) -> typing_extensions___Literal["string_value","bool_value","int_value","double_value","array_value","kvlist_value"]: ... +type___AnyValue = AnyValue + +class ArrayValue(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + @property + def values(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___AnyValue]: ... + + def __init__(self, + *, + values : typing___Optional[typing___Iterable[type___AnyValue]] = None, + ) -> None: ... + if sys.version_info >= (3,): @classmethod - def Value(cls, name: builtin___str) -> AttributeKeyValue.ValueTypeValue: ... + def FromString(cls, s: builtin___bytes) -> ArrayValue: ... + else: @classmethod - def keys(cls) -> typing___List[builtin___str]: ... + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ArrayValue: ... + def ClearField(self, field_name: typing_extensions___Literal[u"values",b"values"]) -> None: ... +type___ArrayValue = ArrayValue + +class KeyValueList(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + @property + def values(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___KeyValue]: ... + + def __init__(self, + *, + values : typing___Optional[typing___Iterable[type___KeyValue]] = None, + ) -> None: ... + if sys.version_info >= (3,): @classmethod - def values(cls) -> typing___List[AttributeKeyValue.ValueTypeValue]: ... + def FromString(cls, s: builtin___bytes) -> KeyValueList: ... + else: @classmethod - def items(cls) -> typing___List[typing___Tuple[builtin___str, AttributeKeyValue.ValueTypeValue]]: ... - STRING = typing___cast(AttributeKeyValue.ValueTypeValue, 0) - INT = typing___cast(AttributeKeyValue.ValueTypeValue, 1) - DOUBLE = typing___cast(AttributeKeyValue.ValueTypeValue, 2) - BOOL = typing___cast(AttributeKeyValue.ValueTypeValue, 3) - STRING = typing___cast(AttributeKeyValue.ValueTypeValue, 0) - INT = typing___cast(AttributeKeyValue.ValueTypeValue, 1) - DOUBLE = typing___cast(AttributeKeyValue.ValueTypeValue, 2) - BOOL = typing___cast(AttributeKeyValue.ValueTypeValue, 3) - type___ValueType = ValueType + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> KeyValueList: ... + def ClearField(self, field_name: typing_extensions___Literal[u"values",b"values"]) -> None: ... +type___KeyValueList = KeyValueList +class KeyValue(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... key: typing___Text = ... - type: type___AttributeKeyValue.ValueTypeValue = ... - string_value: typing___Text = ... - int_value: builtin___int = ... - double_value: builtin___float = ... - bool_value: builtin___bool = ... + + @property + def value(self) -> type___AnyValue: ... def __init__(self, *, key : typing___Optional[typing___Text] = None, - type : typing___Optional[type___AttributeKeyValue.ValueTypeValue] = None, - string_value : typing___Optional[typing___Text] = None, - int_value : typing___Optional[builtin___int] = None, - double_value : typing___Optional[builtin___float] = None, - bool_value : typing___Optional[builtin___bool] = None, + value : typing___Optional[type___AnyValue] = None, ) -> None: ... if sys.version_info >= (3,): @classmethod - def FromString(cls, s: builtin___bytes) -> AttributeKeyValue: ... + def FromString(cls, s: builtin___bytes) -> KeyValue: ... else: @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> AttributeKeyValue: ... - def ClearField(self, field_name: typing_extensions___Literal[u"bool_value",b"bool_value",u"double_value",b"double_value",u"int_value",b"int_value",u"key",b"key",u"string_value",b"string_value",u"type",b"type"]) -> None: ... -type___AttributeKeyValue = AttributeKeyValue + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> KeyValue: ... + def HasField(self, field_name: typing_extensions___Literal[u"value",b"value"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"key",b"key",u"value",b"value"]) -> None: ... +type___KeyValue = KeyValue class StringKeyValue(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py index 95dcf479ff..cf7af9fbb8 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py @@ -19,7 +19,7 @@ package='opentelemetry.proto.resource.v1', syntax='proto3', serialized_options=b'\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\001Z@github.com/open-telemetry/opentelemetry-proto/gen/go/resource/v1', - serialized_pb=b'\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"r\n\x08Resource\x12\x44\n\nattributes\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.common.v1.AttributeKeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\rBw\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z@github.com/open-telemetry/opentelemetry-proto/gen/go/resource/v1b\x06proto3' + serialized_pb=b'\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"i\n\x08Resource\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\rBw\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z@github.com/open-telemetry/opentelemetry-proto/gen/go/resource/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,]) @@ -60,10 +60,10 @@ oneofs=[ ], serialized_start=127, - serialized_end=241, + serialized_end=232, ) -_RESOURCE.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._ATTRIBUTEKEYVALUE +_RESOURCE.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE DESCRIPTOR.message_types_by_name['Resource'] = _RESOURCE _sym_db.RegisterFileDescriptor(DESCRIPTOR) diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi index 2f9f5afca2..0d65464986 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi @@ -14,7 +14,7 @@ from google.protobuf.message import ( ) from opentelemetry.proto.common.v1.common_pb2 import ( - AttributeKeyValue as opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue, + KeyValue as opentelemetry___proto___common___v1___common_pb2___KeyValue, ) from typing import ( @@ -44,11 +44,11 @@ class Resource(google___protobuf___message___Message): dropped_attributes_count: builtin___int = ... @property - def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]: ... + def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___KeyValue]: ... def __init__(self, *, - attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]] = None, + attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___KeyValue]] = None, dropped_attributes_count : typing___Optional[builtin___int] = None, ) -> None: ... if sys.version_info >= (3,): diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py index 0b8bc15248..636c441144 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py @@ -20,7 +20,7 @@ package='opentelemetry.proto.trace.v1', syntax='proto3', serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1', - serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xae\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\"\xa8\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\"\xce\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12\x44\n\nattributes\x18\t \x03(\x0b\x32\x30.opentelemetry.proto.common.v1.AttributeKeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x95\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x44\n\nattributes\x18\x03 \x03(\x0b\x32\x30.opentelemetry.proto.common.v1.AttributeKeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\xa6\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x44\n\nattributes\x18\x04 \x03(\x0b\x32\x30.opentelemetry.proto.common.v1.AttributeKeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"g\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x0c\n\x08INTERNAL\x10\x01\x12\n\n\x06SERVER\x10\x02\x12\n\n\x06\x43LIENT\x10\x03\x12\x0c\n\x08PRODUCER\x10\x04\x12\x0c\n\x08\x43ONSUMER\x10\x05\"\x98\x03\n\x06Status\x12=\n\x04\x63ode\x18\x01 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xbd\x02\n\nStatusCode\x12\x06\n\x02Ok\x10\x00\x12\r\n\tCancelled\x10\x01\x12\x10\n\x0cUnknownError\x10\x02\x12\x13\n\x0fInvalidArgument\x10\x03\x12\x14\n\x10\x44\x65\x61\x64lineExceeded\x10\x04\x12\x0c\n\x08NotFound\x10\x05\x12\x11\n\rAlreadyExists\x10\x06\x12\x14\n\x10PermissionDenied\x10\x07\x12\x15\n\x11ResourceExhausted\x10\x08\x12\x16\n\x12\x46\x61iledPrecondition\x10\t\x12\x0b\n\x07\x41\x62orted\x10\n\x12\x0e\n\nOutOfRange\x10\x0b\x12\x11\n\rUnimplemented\x10\x0c\x12\x11\n\rInternalError\x10\r\x12\x0f\n\x0bUnavailable\x10\x0e\x12\x0c\n\x08\x44\x61taLoss\x10\x0f\x12\x13\n\x0fUnauthenticated\x10\x10\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' + serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xae\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\"\xa8\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\"\xb3\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"g\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x0c\n\x08INTERNAL\x10\x01\x12\n\n\x06SERVER\x10\x02\x12\n\n\x06\x43LIENT\x10\x03\x12\x0c\n\x08PRODUCER\x10\x04\x12\x0c\n\x08\x43ONSUMER\x10\x05\"\x98\x03\n\x06Status\x12=\n\x04\x63ode\x18\x01 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xbd\x02\n\nStatusCode\x12\x06\n\x02Ok\x10\x00\x12\r\n\tCancelled\x10\x01\x12\x10\n\x0cUnknownError\x10\x02\x12\x13\n\x0fInvalidArgument\x10\x03\x12\x14\n\x10\x44\x65\x61\x64lineExceeded\x10\x04\x12\x0c\n\x08NotFound\x10\x05\x12\x11\n\rAlreadyExists\x10\x06\x12\x14\n\x10PermissionDenied\x10\x07\x12\x15\n\x11ResourceExhausted\x10\x08\x12\x16\n\x12\x46\x61iledPrecondition\x10\t\x12\x0b\n\x07\x41\x62orted\x10\n\x12\x0e\n\nOutOfRange\x10\x0b\x12\x11\n\rUnimplemented\x10\x0c\x12\x11\n\rInternalError\x10\r\x12\x0f\n\x0bUnavailable\x10\x0e\x12\x0c\n\x08\x44\x61taLoss\x10\x0f\x12\x13\n\x0fUnauthenticated\x10\x10\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -59,8 +59,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1386, - serialized_end=1489, + serialized_start=1359, + serialized_end=1462, ) _sym_db.RegisterEnumDescriptor(_SPAN_SPANKIND) @@ -141,8 +141,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1583, - serialized_end=1900, + serialized_start=1556, + serialized_end=1873, ) _sym_db.RegisterEnumDescriptor(_STATUS_STATUSCODE) @@ -270,8 +270,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1066, - serialized_end=1215, + serialized_start=1057, + serialized_end=1197, ) _SPAN_LINK = _descriptor.Descriptor( @@ -328,8 +328,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1218, - serialized_end=1384, + serialized_start=1200, + serialized_end=1357, ) _SPAN = _descriptor.Descriptor( @@ -458,7 +458,7 @@ oneofs=[ ], serialized_start=515, - serialized_end=1489, + serialized_end=1462, ) @@ -496,20 +496,20 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1492, - serialized_end=1900, + serialized_start=1465, + serialized_end=1873, ) _RESOURCESPANS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE _RESOURCESPANS.fields_by_name['instrumentation_library_spans'].message_type = _INSTRUMENTATIONLIBRARYSPANS _INSTRUMENTATIONLIBRARYSPANS.fields_by_name['instrumentation_library'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONLIBRARY _INSTRUMENTATIONLIBRARYSPANS.fields_by_name['spans'].message_type = _SPAN -_SPAN_EVENT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._ATTRIBUTEKEYVALUE +_SPAN_EVENT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE _SPAN_EVENT.containing_type = _SPAN -_SPAN_LINK.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._ATTRIBUTEKEYVALUE +_SPAN_LINK.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE _SPAN_LINK.containing_type = _SPAN _SPAN.fields_by_name['kind'].enum_type = _SPAN_SPANKIND -_SPAN.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._ATTRIBUTEKEYVALUE +_SPAN.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE _SPAN.fields_by_name['events'].message_type = _SPAN_EVENT _SPAN.fields_by_name['links'].message_type = _SPAN_LINK _SPAN.fields_by_name['status'].message_type = _STATUS diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi index adcc1dade3..e0726557d6 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi @@ -15,8 +15,8 @@ from google.protobuf.message import ( ) from opentelemetry.proto.common.v1.common_pb2 import ( - AttributeKeyValue as opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue, InstrumentationLibrary as opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary, + KeyValue as opentelemetry___proto___common___v1___common_pb2___KeyValue, ) from opentelemetry.proto.resource.v1.resource_pb2 import ( @@ -136,13 +136,13 @@ class Span(google___protobuf___message___Message): dropped_attributes_count: builtin___int = ... @property - def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]: ... + def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___KeyValue]: ... def __init__(self, *, time_unix_nano : typing___Optional[builtin___int] = None, name : typing___Optional[typing___Text] = None, - attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]] = None, + attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___KeyValue]] = None, dropped_attributes_count : typing___Optional[builtin___int] = None, ) -> None: ... if sys.version_info >= (3,): @@ -162,14 +162,14 @@ class Span(google___protobuf___message___Message): dropped_attributes_count: builtin___int = ... @property - def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]: ... + def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___KeyValue]: ... def __init__(self, *, trace_id : typing___Optional[builtin___bytes] = None, span_id : typing___Optional[builtin___bytes] = None, trace_state : typing___Optional[typing___Text] = None, - attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]] = None, + attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___KeyValue]] = None, dropped_attributes_count : typing___Optional[builtin___int] = None, ) -> None: ... if sys.version_info >= (3,): @@ -194,7 +194,7 @@ class Span(google___protobuf___message___Message): dropped_links_count: builtin___int = ... @property - def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]: ... + def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___KeyValue]: ... @property def events(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Span.Event]: ... @@ -215,7 +215,7 @@ class Span(google___protobuf___message___Message): kind : typing___Optional[type___Span.SpanKindValue] = None, start_time_unix_nano : typing___Optional[builtin___int] = None, end_time_unix_nano : typing___Optional[builtin___int] = None, - attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___AttributeKeyValue]] = None, + attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___KeyValue]] = None, dropped_attributes_count : typing___Optional[builtin___int] = None, events : typing___Optional[typing___Iterable[type___Span.Event]] = None, dropped_events_count : typing___Optional[builtin___int] = None, diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index c66371f42e..b840f8452b 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -12,7 +12,7 @@ # PROTO_REPO_DIR - the path to an existing checkout of the opentelemetry-proto repo # Pinned commit/branch/tag for the current version used in opentelemetry-proto python package. -PROTO_REPO_BRANCH_OR_COMMIT="b54688569186e0b862bf7462a983ccf2c50c0547" +PROTO_REPO_BRANCH_OR_COMMIT="v0.4.0" set -e From 1dde6ffded3df7d391fd736abbb3757227814aa1 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 13 Jul 2020 16:03:34 -0700 Subject: [PATCH 0457/1517] chore: migrate to circleci (#828) --- .circleci/config.yml | 94 +++++++++++++++++- tox.ini | 226 +++++++++++++++++++++---------------------- 2 files changed, 203 insertions(+), 117 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 64070fd605..a00e513de2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,9 +1,24 @@ version: 2.1 executors: - python38: + py38: docker: - image: circleci/python:3.8 + py37: + docker: + - image: circleci/python:3.7 + py36: + docker: + - image: circleci/python:3.6 + py35: + docker: + - image: circleci/python:3.5 + py34: + docker: + - image: circleci/python:3.4 + pypy3: + docker: + - image: pypy:3 commands: setup_tox: @@ -31,9 +46,23 @@ commands: paths: - ".tox" + run_tox_scenario: + description: "Run scripts/run-tox-scenario with setup, caching and persistence" + parameters: + pattern: + type: string + steps: + - checkout + - setup_tox + - restore_tox_cache + - run: + name: "Run scripts/run-tox-scenario" + command: tox -f '<< parameters.pattern >>' + - save_tox_cache + jobs: docs: - executor: python38 + executor: py38 steps: - checkout - setup_tox @@ -41,8 +70,27 @@ jobs: - run: tox -e docs - save_tox_cache + docker-tests: + machine: + image: ubuntu-1604:201903-01 + steps: + - checkout + - run: + name: "Get pyenv list" + command: pyenv versions + - run: + name: "Set Python Version" + command: pyenv global 3.7.0 + - run: + name: "Update pip" + command: pip install -U pip + - setup_tox + - restore_tox_cache + - run: tox -e docker-tests + - save_tox_cache + lint: - executor: python38 + executor: py38 steps: - checkout - setup_tox @@ -50,8 +98,48 @@ jobs: - run: tox -e lint - save_tox_cache + build-py34: + parameters: + package: + type: string + default: "core" + executor: py34 + steps: + - checkout + - run: + name: "Install tox" + command: sudo pip install -U tox virtualenv tox-factor + - restore_tox_cache + - run: + name: "Run scripts/run-tox-scenario" + command: tox -f py34-<< parameters.package >> + - save_tox_cache + + build: + parameters: + version: + type: string + default: "py38" + package: + type: string + default: "core" + executor: << parameters.version >> + steps: + - run_tox_scenario: + pattern: << parameters.version >>-<< parameters.package >> + workflows: main: jobs: + - build: + matrix: + parameters: + version: ["py38", "py37", "py36", "py35", "pypy3"] + package: ["core", "exporter", "instrumentation"] + - build-py34: + matrix: + parameters: + package: ["core", "exporter", "instrumentation"] - docs - lint + - docker-tests diff --git a/tox.ini b/tox.ini index e4ad8e1709..ed4122a3ce 100644 --- a/tox.ini +++ b/tox.ini @@ -5,60 +5,60 @@ envlist = ; Environments are organized by individual package, allowing ; for specifying supported Python versions per package. ; opentelemetry-api - py3{4,5,6,7,8}-test-api - pypy3-test-api + py3{4,5,6,7,8}-test-core-api + pypy3-test-core-api ; opentelemetry-proto - py3{4,5,6,7,8}-test-proto - pypy3-test-proto + py3{4,5,6,7,8}-test-core-proto + pypy3-test-core-proto ; opentelemetry-sdk - py3{4,5,6,7,8}-test-sdk - pypy3-test-sdk + py3{4,5,6,7,8}-test-core-sdk + pypy3-test-core-sdk ; opentelemetry-instrumentation - py3{5,6,7,8}-test-instrumentation-base - pypy3-test-instrumentation-base - - ; opentelemetry-example-app - py3{4,5,6,7,8}-test-example-app - pypy3-test-example-app + py3{5,6,7,8}-test-core-instrumentation + pypy3-test-core-instrumentation ; docs/getting-started - py3{4,5,6,7,8}-test-getting-started - pypy3-test-getting-started + py3{4,5,6,7,8}-test-core-getting-started + pypy3-test-core-getting-started + + ; opentelemetry-example-app + py3{4,5,6,7,8}-test-instrumentation-example-app + pypy3-test-instrumentation-example-app ; opentelemetry-ext-aiohttp-client - py3{5,6,7,8}-test-ext-aiohttp-client - pypy3-test-ext-aiohttp-client + py3{5,6,7,8}-test-instrumentation-aiohttp-client + pypy3-test-instrumentation-aiohttp-client ; opentelemetry-ext-botocore - py3{6,7,8}-test-ext-botocore - pypy3-test-ext-botocore + py3{6,7,8}-test-instrumentation-botocore + pypy3-test-instrumentation-botocore ; opentelemetry-ext-django - py3{4,5,6,7,8}-test-ext-django - pypy3-test-ext-django + py3{4,5,6,7,8}-test-instrumentation-django + pypy3-test-instrumentation-django ; opentelemetry-ext-dbapi - py3{4,5,6,7,8}-test-ext-dbapi - pypy3-test-ext-dbapi + py3{4,5,6,7,8}-test-instrumentation-dbapi + pypy3-test-instrumentation-dbapi ; opentelemetry-ext-boto - py3{5,6,7,8}-test-ext-boto - pypy3-test-ext-boto + py3{5,6,7,8}-test-instrumentation-boto + pypy3-test-instrumentation-boto - ; opentelemetry-ext-elasticsearch - py3{4,5,6,7,8}-test-ext-elasticsearch{2,5,6,7} - pypy3-test-ext-elasticsearch{2,5,6,7} + ; opentelemetry-instrumentation-elasticsearch + py3{4,5,6,7,8}-test-instrumentation-elasticsearch{2,5,6,7} + pypy3-test-instrumentation-elasticsearch{2,5,6,7} ; opentelemetry-ext-flask - py3{4,5,6,7,8}-test-ext-flask - pypy3-test-ext-flask + py3{4,5,6,7,8}-test-instrumentation-flask + pypy3-test-instrumentation-flask ; opentelemetry-ext-requests - py3{4,5,6,7,8}-test-ext-requests - pypy3-test-ext-requests + py3{4,5,6,7,8}-test-instrumentation-requests + pypy3-test-instrumentation-requests ; opentelemetry-instrumentation-starlette. ; starlette only supports 3.6 and above. @@ -66,97 +66,93 @@ envlist = pypy3-test-instrumentation-starlette ; opentelemetry-ext-jinja2 - py3{4,5,6,7,8}-test-ext-jinja2 - pypy3-test-ext-jinja2 + py3{4,5,6,7,8}-test-instrumentation-jinja2 + pypy3-test-instrumentation-jinja2 ; opentelemetry-ext-jaeger - py3{4,5,6,7,8}-test-ext-jaeger - pypy3-test-ext-jaeger + py3{4,5,6,7,8}-test-exporter-jaeger + pypy3-test-exporter-jaeger ; opentelemetry-ext-datadog - py3{5,6,7,8}-test-ext-datadog + py3{5,6,7,8}-test-exporter-datadog ; opentelemetry-ext-mysql - py3{4,5,6,7,8}-test-ext-mysql - pypy3-test-ext-mysql + py3{4,5,6,7,8}-test-instrumentation-mysql + pypy3-test-instrumentation-mysql ; opentelemetry-ext-opencensusexporter - py3{4,5,6,7,8}-test-ext-opencensusexporter + py3{4,5,6,7,8}-test-exporter-opencensusexporter ; ext-opencensusexporter intentionally excluded from pypy3 ; opentelemetry-ext-otlp - py3{5,6,7,8}-test-ext-otlp + py3{5,6,7,8}-test-exporter-otlp ; ext-otlp intentionally excluded from pypy3 ; opentelemetry-ext-prometheus - py3{4,5,6,7,8}-test-ext-prometheus - pypy3-test-ext-prometheus + py3{4,5,6,7,8}-test-exporter-prometheus + pypy3-test-exporter-prometheus ; opentelemetry-ext-psycopg2 - py3{4,5,6,7,8}-test-ext-psycopg2 + py3{4,5,6,7,8}-test-instrumentation-psycopg2 ; ext-psycopg2 intentionally excluded from pypy3 ; opentelemetry-ext-pymemcache - py3{4,5,6,7,8}-test-ext-pymemcache - pypy3-test-ext-pymemcache + py3{4,5,6,7,8}-test-instrumentation-pymemcache + pypy3-test-instrumentation-pymemcache ; opentelemetry-ext-pymongo - py3{4,5,6,7,8}-test-ext-pymongo - pypy3-test-ext-pymongo + py3{4,5,6,7,8}-test-instrumentation-pymongo + pypy3-test-instrumentation-pymongo ; opentelemetry-ext-pymysql - py3{4,5,6,7,8}-test-ext-pymysql - pypy3-test-ext-pymysql + py3{4,5,6,7,8}-test-instrumentation-pymysql + pypy3-test-instrumentation-pymysql ; opentelemetry-ext-pyramid - py3{4,5,6,7,8}-test-ext-pyramid - pypy3-test-ext-pyramid - + py3{4,5,6,7,8}-test-instrumentation-pyramid + pypy3-test-instrumentation-pyramid + ; opentelemetry-ext-asgi - py3{5,6,7,8}-test-ext-asgi - pypy3-test-ext-asgi + py3{5,6,7,8}-test-instrumentation-asgi + pypy3-test-instrumentation-asgi ; opentelemetry-ext-asyncpg - py3{5,6,7,8}-test-ext-asyncpg + py3{5,6,7,8}-test-instrumentation-asyncpg ; ext-asyncpg intentionally excluded from pypy3 ; opentelemetry-ext-sqlite3 - py3{4,5,6,7,8}-test-ext-sqlite3 - pypy3-test-ext-sqlite3 + py3{4,5,6,7,8}-test-instrumentation-sqlite3 + pypy3-test-instrumentation-sqlite3 ; opentelemetry-ext-wsgi - py3{4,5,6,7,8}-test-ext-wsgi - pypy3-test-ext-wsgi + py3{4,5,6,7,8}-test-instrumentation-wsgi + pypy3-test-instrumentation-wsgi ; opentelemetry-ext-zipkin - py3{4,5,6,7,8}-test-ext-zipkin - pypy3-test-ext-zipkin - - ; opentelemetry-opentracing-shim - py3{4,5,6,7,8}-test-opentracing-shim - pypy3-test-opentracing-shim + py3{4,5,6,7,8}-test-exporter-zipkin + pypy3-test-exporter-zipkin ; opentelemetry-opentracing-shim - py3{4,5,6,7,8}-test-opentracing-shim - pypy3-test-opentracing-shim + py3{4,5,6,7,8}-test-core-opentracing-shim + pypy3-test-core-opentracing-shim ; opentelemetry-ext-grpc - py3{4,5,6,7,8}-test-ext-grpc + py3{4,5,6,7,8}-test-instrumentation-grpc ; opentelemetry-ext-sqlalchemy - py3{4,5,6,7,8}-test-ext-sqlalchemy - pypy3-test-ext-sqlalchemy + py3{4,5,6,7,8}-test-instrumentation-sqlalchemy + pypy3-test-instrumentation-sqlalchemy ; opentelemetry-ext-redis - py3{4,5,6,7,8}-test-ext-redis - pypy3-test-ext-redis + py3{4,5,6,7,8}-test-instrumentation-redis + pypy3-test-instrumentation-redis ; opentelemetry-ext-celery - py3{5,6,7,8}-test-ext-celery - pypy3-test-ext-celery + py3{5,6,7,8}-test-instrumentation-celery + pypy3-test-instrumentation-celery ; opentelemetry-ext-system-metrics - py3{4,5,6,7,8}-test-ext-system-metrics + py3{4,5,6,7,8}-test-instrumentation-system-metrics ; ext-system-metrics intentionally excluded from pypy3 ; known limitation: gc.get_count won't work under pypy @@ -195,46 +191,48 @@ setenv = mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ changedir = - test-api: opentelemetry-api/tests - test-sdk: opentelemetry-sdk/tests - instrumentation-base: opentelemetry-instrumentation/tests + test-core-api: opentelemetry-api/tests + test-core-sdk: opentelemetry-sdk/tests + test-core-proto: opentelemetry-proto/tests + test-core-instrumentation: opentelemetry-instrumentation/tests + test-core-getting-started: docs/getting_started/tests + test-core-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests + + test-instrumentation-grpc: ext/opentelemetry-ext-grpc/tests + test-instrumentation-aiohttp-client: ext/opentelemetry-ext-aiohttp-client/tests + test-instrumentation-requests: ext/opentelemetry-ext-requests/tests + test-instrumentation-jinja2: ext/opentelemetry-ext-jinja2/tests + test-instrumentation-dbapi: ext/opentelemetry-ext-dbapi/tests + test-instrumentation-django: ext/opentelemetry-ext-django/tests + test-instrumentation-mysql: ext/opentelemetry-ext-mysql/tests + test-instrumentation-pymemcache: ext/opentelemetry-ext-pymemcache/tests + test-instrumentation-pymongo: ext/opentelemetry-ext-pymongo/tests + test-instrumentation-psycopg2: ext/opentelemetry-ext-psycopg2/tests + test-instrumentation-pymysql: ext/opentelemetry-ext-pymysql/tests + test-instrumentation-pyramid: ext/opentelemetry-ext-pyramid/tests + test-instrumentation-asgi: ext/opentelemetry-ext-asgi/tests + test-instrumentation-sqlite3: ext/opentelemetry-ext-sqlite3/tests + test-instrumentation-wsgi: ext/opentelemetry-ext-wsgi/tests + test-instrumentation-boto: ext/opentelemetry-ext-boto/tests + test-instrumentation-botocore: ext/opentelemetry-ext-botocore/tests + test-instrumentation-flask: ext/opentelemetry-ext-flask/tests + test-instrumentation-example-app: docs/examples/opentelemetry-example-app/tests + test-instrumentation-sqlalchemy: ext/opentelemetry-ext-sqlalchemy/tests + test-instrumentation-redis: ext/opentelemetry-ext-redis/tests test-instrumentation-starlette: ext/opentelemetry-instrumentation-starlette/tests - test-proto: opentelemetry-proto/tests - test-ext-grpc: ext/opentelemetry-ext-grpc/tests - test-ext-aiohttp-client: ext/opentelemetry-ext-aiohttp-client/tests - test-ext-requests: ext/opentelemetry-ext-requests/tests - test-ext-jinja2: ext/opentelemetry-ext-jinja2/tests - test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests - test-ext-datadog: ext/opentelemetry-ext-datadog/tests - test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests - test-ext-elasticsearch{2,5,6,7}: ext/opentelemetry-ext-elasticsearch/tests - test-ext-django: ext/opentelemetry-ext-django/tests - test-ext-mysql: ext/opentelemetry-ext-mysql/tests - test-ext-opencensusexporter: ext/opentelemetry-ext-opencensusexporter/tests - test-ext-otlp: ext/opentelemetry-ext-otlp/tests - test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests - test-ext-pymemcache: ext/opentelemetry-ext-pymemcache/tests - test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests + test-instrumentation-system-metrics: ext/opentelemetry-ext-system-metrics/tests + test-instrumentation-celery: ext/opentelemetry-ext-celery/tests + test-instrumentation-elasticsearch{2,5,6,7}: ext/opentelemetry-ext-elasticsearch/tests + test-instrumentation-asyncpg: ext/opentelemetry-ext-asyncpg/tests + + test-exporter-jaeger: ext/opentelemetry-ext-jaeger/tests + test-exporter-datadog: ext/opentelemetry-ext-datadog/tests + test-exporter-opencensusexporter: ext/opentelemetry-ext-opencensusexporter/tests + test-exporter-otlp: ext/opentelemetry-ext-otlp/tests + test-exporter-prometheus: ext/opentelemetry-ext-prometheus/tests test-exporter-cloud-trace: ext/opentelemetry-exporter-cloud-trace/tests test-exporter-cloud-monitoring: ext/opentelemetry-exporter-cloud-monitoring/tests - test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests - test-ext-pymysql: ext/opentelemetry-ext-pymysql/tests - test-ext-pyramid: ext/opentelemetry-ext-pyramid/tests - test-ext-asgi: ext/opentelemetry-ext-asgi/tests - test-ext-asyncpg: ext/opentelemetry-ext-asyncpg/tests - test-ext-sqlite3: ext/opentelemetry-ext-sqlite3/tests - test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests - test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests - test-ext-boto: ext/opentelemetry-ext-boto/tests - test-ext-botocore: ext/opentelemetry-ext-botocore/tests - test-ext-flask: ext/opentelemetry-ext-flask/tests - test-example-app: docs/examples/opentelemetry-example-app/tests - test-getting-started: docs/getting_started/tests - test-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests - test-ext-sqlalchemy: ext/opentelemetry-ext-sqlalchemy/tests - test-ext-redis: ext/opentelemetry-ext-redis/tests - test-ext-celery: ext/opentelemetry-ext-celery/tests - test-ext-system-metrics: ext/opentelemetry-ext-system-metrics/tests + test-exporter-zipkin: ext/opentelemetry-ext-zipkin/tests commands_pre = ; Install without -e to test the actual installation @@ -243,7 +241,7 @@ commands_pre = ; cases but it saves a lot of boilerplate in this file. test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util - test-proto: pip install {toxinidir}/opentelemetry-proto + test-core-proto: pip install {toxinidir}/opentelemetry-proto ext,instrumentation: pip install {toxinidir}/opentelemetry-instrumentation example-app: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/ext/opentelemetry-ext-requests {toxinidir}/ext/opentelemetry-ext-wsgi {toxinidir}/ext/opentelemetry-ext-flask {toxinidir}/docs/examples/opentelemetry-example-app @@ -339,7 +337,7 @@ commands = ; implicit Any due to unfollowed import would result). mypyinstalled: mypy --namespace-packages opentelemetry-api/tests/mypysmoke.py --strict -[testenv:py34-test-opentracing-shim] +[testenv:py34-test-core-opentracing-shim] commands = pytest --ignore-glob='*[asyncio].py' From 5cb01d6c907948ba56ab4595c4a7d185fbd02980 Mon Sep 17 00:00:00 2001 From: Connor Adams Date: Tue, 14 Jul 2020 10:56:02 -0400 Subject: [PATCH 0458/1517] docs: Documentation for sampling (#882) --- docs/api/trace.sampling.rst | 4 +- .../src/opentelemetry/trace/sampling.py | 64 ++++++++++++++++++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/docs/api/trace.sampling.rst b/docs/api/trace.sampling.rst index 09a77b166e..6280fd1d11 100644 --- a/docs/api/trace.sampling.rst +++ b/docs/api/trace.sampling.rst @@ -1,5 +1,5 @@ -opentelemetry.trace.sampling -============================ +Sampling Traces +=============== .. automodule:: opentelemetry.trace.sampling :members: diff --git a/opentelemetry-api/src/opentelemetry/trace/sampling.py b/opentelemetry-api/src/opentelemetry/trace/sampling.py index 892689783d..868678b05c 100644 --- a/opentelemetry-api/src/opentelemetry/trace/sampling.py +++ b/opentelemetry-api/src/opentelemetry/trace/sampling.py @@ -12,6 +12,52 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +For general information about sampling, see `the specification `_. + +OpenTelemetry provides two types of samplers: + +- `StaticSampler` +- `ProbabilitySampler` + +A `StaticSampler` always returns the same sampling decision regardless of the conditions. Both possible StaticSamplers are already created: + +- Always sample spans: `ALWAYS_ON` +- Never sample spans: `ALWAYS_OFF` + +A `ProbabilitySampler` makes a random sampling decision based on the sampling probability given. If the span being sampled has a parent, `ProbabilitySampler` will respect the parent span's sampling decision. + +Currently, sampling decisions are always made during the creation of the span. However, this might not always be the case in the future (see `OTEP #115 `_). + +Custom samplers can be created by subclassing `Sampler` and implementing `Sampler.should_sample`. + +To use a sampler, pass it into the tracer provider constructor. For example: + +.. code:: python + + from opentelemetry import trace + from opentelemetry.trace.sampling import ProbabilitySampler + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, + ) + + # sample 1 in every 1000 traces + sampler = ProbabilitySampler(1/1000) + + # set the sampler onto the global tracer provider + trace.set_tracer_provider(TracerProvider(sampler=sampler)) + + # set up an exporter for sampled spans + trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) + ) + + # created spans will now be sampled by the ProbabilitySampler + with trace.get_tracer(__name__).start_as_current_span("Test Span"): + ... +""" import abc from typing import Dict, Mapping, Optional, Sequence @@ -78,6 +124,14 @@ def should_sample( class ProbabilitySampler(Sampler): + """ + Sampler that makes sampling decisions probabalistically based on `rate`, + while also respecting the parent span sampling decision. + + Args: + rate: Probability (between 0 and 1) that a span will be sampled + """ + def __init__(self, rate: float): self._rate = rate self._bound = self.get_bound_for_rate(self._rate) @@ -118,11 +172,15 @@ def should_sample( return Decision(trace_id & self.TRACE_ID_LIMIT < self.bound) -# Samplers that ignore the parent sampling decision and never/always sample. ALWAYS_OFF = StaticSampler(Decision(False)) +"""Sampler that never samples spans, regardless of the parent span's sampling decision.""" + ALWAYS_ON = StaticSampler(Decision(True)) +"""Sampler that always samples spans, regardless of the parent span's sampling decision.""" + -# Samplers that respect the parent sampling decision, but otherwise -# never/always sample. DEFAULT_OFF = ProbabilitySampler(0.0) +"""Sampler that respects its parent span's sampling decision, but otherwise never samples.""" + DEFAULT_ON = ProbabilitySampler(1.0) +"""Sampler that respects its parent span's sampling decision, but otherwise always samples.""" From 545068d6eb61beaab031af45b1572d70e6652bc3 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 14 Jul 2020 08:32:33 -0700 Subject: [PATCH 0459/1517] chore: rename workflow to give it more meaning (#909) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a00e513de2..f47690257c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -129,7 +129,7 @@ jobs: pattern: << parameters.version >>-<< parameters.package >> workflows: - main: + circleci-build: jobs: - build: matrix: From 7bec76a22032e498911d06b9e3dab4e33528d995 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Wed, 15 Jul 2020 06:49:12 +0000 Subject: [PATCH 0460/1517] fastapi instrumentation (#890) Co-authored-by: Leighton Chen --- docs-requirements.txt | 1 + docs/ext/fastapi/fastapi.rst | 9 ++ .../CHANGELOG.md | 5 + .../README.rst | 43 ++++++++ .../setup.cfg | 55 +++++++++ .../setup.py | 31 ++++++ .../instrumentation/fastapi/__init__.py | 82 ++++++++++++++ .../instrumentation/fastapi/version.py | 15 +++ .../tests/__init__.py | 0 .../tests/test_fastapi_instrumentation.py | 104 ++++++++++++++++++ .../setup.cfg | 2 +- .../instrumentation/starlette/__init__.py | 2 +- tox.ini | 16 ++- 13 files changed, 358 insertions(+), 7 deletions(-) create mode 100644 docs/ext/fastapi/fastapi.rst create mode 100644 ext/opentelemetry-instrumentation-fastapi/CHANGELOG.md create mode 100644 ext/opentelemetry-instrumentation-fastapi/README.rst create mode 100644 ext/opentelemetry-instrumentation-fastapi/setup.cfg create mode 100644 ext/opentelemetry-instrumentation-fastapi/setup.py create mode 100644 ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py create mode 100644 ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py create mode 100644 ext/opentelemetry-instrumentation-fastapi/tests/__init__.py create mode 100644 ext/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py diff --git a/docs-requirements.txt b/docs-requirements.txt index 230c76149c..169bbb7f0b 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -29,3 +29,4 @@ google-cloud-trace >=0.23.0 google-cloud-monitoring>=0.36.0 botocore~=1.0 starlette~=0.13 +fastapi~=0.58.1 \ No newline at end of file diff --git a/docs/ext/fastapi/fastapi.rst b/docs/ext/fastapi/fastapi.rst new file mode 100644 index 0000000000..9295261584 --- /dev/null +++ b/docs/ext/fastapi/fastapi.rst @@ -0,0 +1,9 @@ +.. include:: ../../../ext/opentelemetry-instrumentation-fastapi/README.rst + +API +--- + +.. automodule:: opentelemetry.instrumentation.fastapi + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/ext/opentelemetry-instrumentation-fastapi/CHANGELOG.md b/ext/opentelemetry-instrumentation-fastapi/CHANGELOG.md new file mode 100644 index 0000000000..684dece0c6 --- /dev/null +++ b/ext/opentelemetry-instrumentation-fastapi/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release ([#890](https://github.com/open-telemetry/opentelemetry-python/pull/890)) \ No newline at end of file diff --git a/ext/opentelemetry-instrumentation-fastapi/README.rst b/ext/opentelemetry-instrumentation-fastapi/README.rst new file mode 100644 index 0000000000..4cc612da76 --- /dev/null +++ b/ext/opentelemetry-instrumentation-fastapi/README.rst @@ -0,0 +1,43 @@ +OpenTelemetry FastAPI Instrumentation +======================================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-fastapi.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-fastapi/ + + +This library provides automatic and manual instrumentation of FastAPI web frameworks, +instrumenting http requests served by applications utilizing the framework. + +auto-instrumentation using the opentelemetry-instrumentation package is also supported. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-fastapi + + +Usage +----- + +.. code-block:: python + + import fastapi + from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor + + app = fastapi.FastAPI() + + @app.get("/foobar") + async def foobar(): + return {"message": "hello world"} + + FastAPIInstrumentor.instrument_app(app) + + +References +---------- + +* `OpenTelemetry Project `_ \ No newline at end of file diff --git a/ext/opentelemetry-instrumentation-fastapi/setup.cfg b/ext/opentelemetry-instrumentation-fastapi/setup.cfg new file mode 100644 index 0000000000..5e7c2fafa6 --- /dev/null +++ b/ext/opentelemetry-instrumentation-fastapi/setup.cfg @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-instrumentation-fastapi +description = OpenTelemetry FastAPI Instrumentation +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-instrumentation-fastapi +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.6 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.11.dev0 + opentelemetry-ext-asgi == 0.11.dev0 + +[options.entry_points] +opentelemetry_instrumentor = + fastapi = opentelemetry.instrumentation.fastapi:FastAPIInstrumentor + +[options.extras_require] +test = + opentelemetry-test == 0.11.dev0 + fastapi ~= 0.58.1 + requests ~= 2.23.0 # needed for testclient + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-instrumentation-fastapi/setup.py b/ext/opentelemetry-instrumentation-fastapi/setup.py new file mode 100644 index 0000000000..13c7c5a99c --- /dev/null +++ b/ext/opentelemetry-instrumentation-fastapi/setup.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "fastapi", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py b/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py new file mode 100644 index 0000000000..65d608393d --- /dev/null +++ b/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py @@ -0,0 +1,82 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Optional + +import fastapi +from starlette.routing import Match + +from opentelemetry.ext.asgi import OpenTelemetryMiddleware +from opentelemetry.instrumentation.fastapi.version import __version__ # noqa +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + + +class FastAPIInstrumentor(BaseInstrumentor): + """An instrumentor for FastAPI + + See `BaseInstrumentor` + """ + + _original_fastapi = None + + @staticmethod + def instrument_app(app: fastapi.FastAPI): + """Instrument an uninstrumented FastAPI application. + """ + if not getattr(app, "is_instrumented_by_opentelemetry", False): + app.add_middleware( + OpenTelemetryMiddleware, + span_details_callback=_get_route_details, + ) + app.is_instrumented_by_opentelemetry = True + + def _instrument(self, **kwargs): + self._original_fastapi = fastapi.FastAPI + fastapi.FastAPI = _InstrumentedFastAPI + + def _uninstrument(self, **kwargs): + fastapi.FastAPI = self._original_fastapi + + +class _InstrumentedFastAPI(fastapi.FastAPI): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.add_middleware( + OpenTelemetryMiddleware, span_details_callback=_get_route_details + ) + + +def _get_route_details(scope): + """Callback to retrieve the fastapi route being served. + + TODO: there is currently no way to retrieve http.route from + a starlette application from scope. + + See: https://github.com/encode/starlette/pull/804 + """ + app = scope["app"] + route = None + for starlette_route in app.routes: + match, _ = starlette_route.matches(scope) + if match == Match.FULL: + route = starlette_route.path + break + if match == Match.PARTIAL: + route = starlette_route.path + # method only exists for http, if websocket + # leave it blank. + span_name = route or scope.get("method", "") + attributes = {} + if route: + attributes["http.route"] = route + return span_name, attributes diff --git a/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py b/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py new file mode 100644 index 0000000000..858e73960f --- /dev/null +++ b/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-instrumentation-fastapi/tests/__init__.py b/ext/opentelemetry-instrumentation-fastapi/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/ext/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py new file mode 100644 index 0000000000..47617d4e95 --- /dev/null +++ b/ext/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -0,0 +1,104 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import fastapi +from fastapi.testclient import TestClient + +import opentelemetry.instrumentation.fastapi as otel_fastapi +from opentelemetry.test.test_base import TestBase + + +class TestFastAPIManualInstrumentation(TestBase): + def _create_app(self): + app = self._create_fastapi_app() + self._instrumentor.instrument_app(app) + return app + + def setUp(self): + super().setUp() + self._instrumentor = otel_fastapi.FastAPIInstrumentor() + self._app = self._create_app() + self._client = TestClient(self._app) + + def test_basic_fastapi_call(self): + self._client.get("/foobar") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 3) + for span in spans: + self.assertIn("/foobar", span.name) + + def test_fastapi_route_attribute_added(self): + """Ensure that fastapi routes are used as the span name.""" + self._client.get("/user/123") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 3) + for span in spans: + self.assertIn("/user/{username}", span.name) + self.assertEqual( + spans[-1].attributes["http.route"], "/user/{username}" + ) + # ensure that at least one attribute that is populated by + # the asgi instrumentation is successfully feeding though. + self.assertEqual(spans[-1].attributes["http.flavor"], "1.1") + + @staticmethod + def _create_fastapi_app(): + app = fastapi.FastAPI() + + @app.get("/foobar") + async def _(): + return {"message": "hello world"} + + @app.get("/user/{username}") + async def _(username: str): + return {"message": username} + + return app + + +class TestAutoInstrumentation(TestFastAPIManualInstrumentation): + """Test the auto-instrumented variant + + Extending the manual instrumentation as most test cases apply + to both. + """ + + def _create_app(self): + # instrumentation is handled by the instrument call + self._instrumentor.instrument() + return self._create_fastapi_app() + + def tearDown(self): + self._instrumentor.uninstrument() + super().tearDown() + + +class TestAutoInstrumentationLogic(unittest.TestCase): + def test_instrumentation(self): + """Verify that instrumentation methods are instrumenting and + removing as expected. + """ + instrumentor = otel_fastapi.FastAPIInstrumentor() + original = fastapi.FastAPI + instrumentor.instrument() + try: + instrumented = fastapi.FastAPI + self.assertIsNot(original, instrumented) + finally: + instrumentor.uninstrument() + + should_be_original = fastapi.FastAPI + self.assertIs(original, should_be_original) diff --git a/ext/opentelemetry-instrumentation-starlette/setup.cfg b/ext/opentelemetry-instrumentation-starlette/setup.cfg index cea0cc1188..4c777a18c5 100644 --- a/ext/opentelemetry-instrumentation-starlette/setup.cfg +++ b/ext/opentelemetry-instrumentation-starlette/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-instrumentation-starlette +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-instrumentation-starlette platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py b/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py index 197a38d759..b8763bba05 100644 --- a/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py +++ b/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py @@ -31,7 +31,7 @@ class StarletteInstrumentor(BaseInstrumentor): @staticmethod def instrument_app(app: applications.Starlette): - """Instrument a previously instrumented Starlette application. + """Instrument an uninstrumented Starlette application. """ if not getattr(app, "is_instrumented_by_opentelemetry", False): app.add_middleware( diff --git a/tox.ini b/tox.ini index ed4122a3ce..4b9ed90479 100644 --- a/tox.ini +++ b/tox.ini @@ -52,6 +52,11 @@ envlist = py3{4,5,6,7,8}-test-instrumentation-elasticsearch{2,5,6,7} pypy3-test-instrumentation-elasticsearch{2,5,6,7} + ; opentelemetry-instrumentation-fastapi + ; fastapi only supports 3.6 and above. + py3{6,7,8}-test-instrumentation-fastapi + pypy3-test-instrumentation-fastapi + ; opentelemetry-ext-flask py3{4,5,6,7,8}-test-instrumentation-flask pypy3-test-instrumentation-flask @@ -187,8 +192,7 @@ deps = elasticsearch7: elasticsearch-dsl>=7.0,<8.0 elasticsearch7: elasticsearch>=7.0,<8.0 -setenv = - mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ +setenv = mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ changedir = test-core-api: opentelemetry-api/tests @@ -215,6 +219,7 @@ changedir = test-instrumentation-wsgi: ext/opentelemetry-ext-wsgi/tests test-instrumentation-boto: ext/opentelemetry-ext-boto/tests test-instrumentation-botocore: ext/opentelemetry-ext-botocore/tests + test-instrumentation-fastapi: ext/opentelemetry-instrumentation-fastapi/tests test-instrumentation-flask: ext/opentelemetry-ext-flask/tests test-instrumentation-example-app: docs/examples/opentelemetry-example-app/tests test-instrumentation-sqlalchemy: ext/opentelemetry-ext-sqlalchemy/tests @@ -236,7 +241,7 @@ changedir = commands_pre = ; Install without -e to test the actual installation - py3{4,5,6,7}: python -m pip install -U pip setuptools wheel + py3{4,5,6,7,8}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util @@ -252,9 +257,8 @@ commands_pre = grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc[test] - wsgi,flask,django,asgi,pyramid,starlette: pip install {toxinidir}/tests/util wsgi,flask,django,pyramid: pip install {toxinidir}/ext/opentelemetry-ext-wsgi - asgi,starlette: pip install {toxinidir}/ext/opentelemetry-ext-asgi + asgi,starlette,fastapi: pip install {toxinidir}/ext/opentelemetry-ext-asgi asyncpg: pip install {toxinidir}/ext/opentelemetry-ext-asyncpg @@ -269,6 +273,8 @@ commands_pre = django: pip install {toxinidir}/ext/opentelemetry-ext-django[test] + fastapi: pip install {toxinidir}/ext/opentelemetry-instrumentation-fastapi[test] + mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-mysql[test] opencensusexporter: pip install {toxinidir}/ext/opentelemetry-ext-opencensusexporter From 5d82b0aa30725d2d78ffb9f96cd0dd6742b96a96 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 15 Jul 2020 10:18:33 -0700 Subject: [PATCH 0461/1517] chore: update bootstrap with new instrumentations (#913) --- .../instrumentation/bootstrap.py | 26 +++++++++++++-- tox.ini | 32 +++++++++---------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index e313ca9631..f624a2e4dd 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -25,35 +25,57 @@ # target library to desired instrumentor path/versioned package name instrumentations = { + "asgi": "opentelemetry-ext-asgi>=0.11b0", + "asyncpg": "opentelemetry-ext-asyncpg>=0.11b0", + "boto": "opentelemetry-ext-boto>=0.11b0", + "botocore": "opentelemetry-ext-botocore>=0.11b0", + "celery": "opentelemetry-ext-celery>=0.11b0", "dbapi": "opentelemetry-ext-dbapi>=0.8b0", "django": "opentelemetry-ext-django>=0.8b0", + "elasticsearch": "opentelemetry-ext-elasticsearch>=0.11b0", + "fastapi": "opentelemetry-instrumentation-fastapi>=0.11b0", "flask": "opentelemetry-ext-flask>=0.8b0", "grpc": "opentelemetry-ext-grpc>=0.8b0", - "requests": "opentelemetry-ext-requests>=0.8b0", "jinja2": "opentelemetry-ext-jinja2>=0.8b0", "mysql": "opentelemetry-ext-mysql>=0.8b0", "psycopg2": "opentelemetry-ext-psycopg2>=0.8b0", + "pymemcache": "opentelemetry-ext-pymemcache>=0.11b0", "pymongo": "opentelemetry-ext-pymongo>=0.8b0", "pymysql": "opentelemetry-ext-pymysql>=0.8b0", + "pyramid": "opentelemetry-ext-pyramid>=0.11b0", "redis": "opentelemetry-ext-redis>=0.8b0", + "requests": "opentelemetry-ext-requests>=0.8b0", "sqlalchemy": "opentelemetry-ext-sqlalchemy>=0.8b0", + "sqlite3": "opentelemetry-ext-sqlite3>=0.11b0", + "starlette": "opentelemetry-instrumentation-starlette>=0.11b0", "wsgi": "opentelemetry-ext-wsgi>=0.8b0", } # relevant instrumentors and tracers to uninstall and check for conflicts for target libraries libraries = { + "asgi": ("opentelemetry-ext-asgi",), + "asyncpg": ("opentelemetry-ext-asyncpg",), + "boto": ("opentelemetry-ext-boto",), + "botocore": ("opentelemetry-ext-botocore",), + "celery": ("opentelemetry-ext-celery",), "dbapi": ("opentelemetry-ext-dbapi",), "django": ("opentelemetry-ext-django",), + "elasticsearch": ("opentelemetry-ext-elasticsearch",), + "fastapi": ("opentelemetry-instrumentation-fastapi",), "flask": ("opentelemetry-ext-flask",), "grpc": ("opentelemetry-ext-grpc",), - "requests": ("opentelemetry-ext-requests",), "jinja2": ("opentelemetry-ext-jinja2",), "mysql": ("opentelemetry-ext-mysql",), "psycopg2": ("opentelemetry-ext-psycopg2",), + "pymemcache": ("opentelemetry-ext-pymemcache",), "pymongo": ("opentelemetry-ext-pymongo",), "pymysql": ("opentelemetry-ext-pymysql",), + "pyramid": ("opentelemetry-ext-pyramid",), "redis": ("opentelemetry-ext-redis",), + "requests": ("opentelemetry-ext-requests",), "sqlalchemy": ("opentelemetry-ext-sqlalchemy",), + "sqlite3": ("opentelemetry-ext-sqlite3",), + "starlette": ("opentelemetry-instrumentation-starlette",), "wsgi": ("opentelemetry-ext-wsgi",), } diff --git a/tox.ini b/tox.ini index 4b9ed90479..ea3a5c2913 100644 --- a/tox.ini +++ b/tox.ini @@ -202,33 +202,33 @@ changedir = test-core-getting-started: docs/getting_started/tests test-core-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests - test-instrumentation-grpc: ext/opentelemetry-ext-grpc/tests test-instrumentation-aiohttp-client: ext/opentelemetry-ext-aiohttp-client/tests - test-instrumentation-requests: ext/opentelemetry-ext-requests/tests - test-instrumentation-jinja2: ext/opentelemetry-ext-jinja2/tests + test-instrumentation-asgi: ext/opentelemetry-ext-asgi/tests + test-instrumentation-asyncpg: ext/opentelemetry-ext-asyncpg/tests + test-instrumentation-boto: ext/opentelemetry-ext-boto/tests + test-instrumentation-botocore: ext/opentelemetry-ext-botocore/tests + test-instrumentation-celery: ext/opentelemetry-ext-celery/tests test-instrumentation-dbapi: ext/opentelemetry-ext-dbapi/tests test-instrumentation-django: ext/opentelemetry-ext-django/tests + test-instrumentation-example-app: docs/examples/opentelemetry-example-app/tests + test-instrumentation-elasticsearch{2,5,6,7}: ext/opentelemetry-ext-elasticsearch/tests + test-instrumentation-fastapi: ext/opentelemetry-instrumentation-fastapi/tests + test-instrumentation-flask: ext/opentelemetry-ext-flask/tests + test-instrumentation-grpc: ext/opentelemetry-ext-grpc/tests + test-instrumentation-jinja2: ext/opentelemetry-ext-jinja2/tests test-instrumentation-mysql: ext/opentelemetry-ext-mysql/tests + test-instrumentation-psycopg2: ext/opentelemetry-ext-psycopg2/tests test-instrumentation-pymemcache: ext/opentelemetry-ext-pymemcache/tests test-instrumentation-pymongo: ext/opentelemetry-ext-pymongo/tests - test-instrumentation-psycopg2: ext/opentelemetry-ext-psycopg2/tests test-instrumentation-pymysql: ext/opentelemetry-ext-pymysql/tests test-instrumentation-pyramid: ext/opentelemetry-ext-pyramid/tests - test-instrumentation-asgi: ext/opentelemetry-ext-asgi/tests - test-instrumentation-sqlite3: ext/opentelemetry-ext-sqlite3/tests - test-instrumentation-wsgi: ext/opentelemetry-ext-wsgi/tests - test-instrumentation-boto: ext/opentelemetry-ext-boto/tests - test-instrumentation-botocore: ext/opentelemetry-ext-botocore/tests - test-instrumentation-fastapi: ext/opentelemetry-instrumentation-fastapi/tests - test-instrumentation-flask: ext/opentelemetry-ext-flask/tests - test-instrumentation-example-app: docs/examples/opentelemetry-example-app/tests - test-instrumentation-sqlalchemy: ext/opentelemetry-ext-sqlalchemy/tests test-instrumentation-redis: ext/opentelemetry-ext-redis/tests + test-instrumentation-requests: ext/opentelemetry-ext-requests/tests + test-instrumentation-sqlalchemy: ext/opentelemetry-ext-sqlalchemy/tests + test-instrumentation-sqlite3: ext/opentelemetry-ext-sqlite3/tests test-instrumentation-starlette: ext/opentelemetry-instrumentation-starlette/tests test-instrumentation-system-metrics: ext/opentelemetry-ext-system-metrics/tests - test-instrumentation-celery: ext/opentelemetry-ext-celery/tests - test-instrumentation-elasticsearch{2,5,6,7}: ext/opentelemetry-ext-elasticsearch/tests - test-instrumentation-asyncpg: ext/opentelemetry-ext-asyncpg/tests + test-instrumentation-wsgi: ext/opentelemetry-ext-wsgi/tests test-exporter-jaeger: ext/opentelemetry-ext-jaeger/tests test-exporter-datadog: ext/opentelemetry-ext-datadog/tests From 2a952b3d4540fa767974c48afdb781d9e9340edd Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 15 Jul 2020 12:39:40 -0600 Subject: [PATCH 0462/1517] Fix a few issues with the Django example (#915) --- docs/examples/django/README.rst | 2 +- docs/examples/django/pages/views.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 95c106d32f..e8a043684f 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -56,7 +56,7 @@ Open up a new console and activate the previous virtual environment there too: ``source django_auto_instrumentation/bin/activate`` -Go to ``opentelemetry-python/ext/opentelemetry-ext-django/example``, once there +Go to ``opentelemetry-python/docs/examples/django``, once there run the client with: ``python client.py hello`` diff --git a/docs/examples/django/pages/views.py b/docs/examples/django/pages/views.py index d54633c329..4083888e17 100644 --- a/docs/examples/django/pages/views.py +++ b/docs/examples/django/pages/views.py @@ -21,7 +21,6 @@ ) trace.set_tracer_provider(TracerProvider()) -tracer = trace.get_tracer_provider().get_tracer(__name__) trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) From ee9d28a71ca825a7518ff052dc5bafd3aa5bf2db Mon Sep 17 00:00:00 2001 From: Connor Adams Date: Thu, 16 Jul 2020 11:43:04 -0400 Subject: [PATCH 0463/1517] instrumentation/grpc: Testing for gRPC Client Interceptor (#896) --- ext/opentelemetry-ext-grpc/CHANGELOG.md | 3 + ext/opentelemetry-ext-grpc/setup.cfg | 1 + .../src/opentelemetry/ext/grpc/_client.py | 40 +++- ext/opentelemetry-ext-grpc/tests/_client.py | 57 +++++ ext/opentelemetry-ext-grpc/tests/_server.py | 87 +++++++ .../tests/protobuf/test_server.proto | 34 +++ .../tests/protobuf/test_server_pb2.py | 215 ++++++++++++++++++ .../tests/protobuf/test_server_pb2_grpc.py | 205 +++++++++++++++++ .../tests/test_client_interceptor.py | 139 +++++++++++ tox.ini | 2 +- 10 files changed, 775 insertions(+), 8 deletions(-) create mode 100644 ext/opentelemetry-ext-grpc/tests/_client.py create mode 100644 ext/opentelemetry-ext-grpc/tests/_server.py create mode 100644 ext/opentelemetry-ext-grpc/tests/protobuf/test_server.proto create mode 100644 ext/opentelemetry-ext-grpc/tests/protobuf/test_server_pb2.py create mode 100644 ext/opentelemetry-ext-grpc/tests/protobuf/test_server_pb2_grpc.py create mode 100644 ext/opentelemetry-ext-grpc/tests/test_client_interceptor.py diff --git a/ext/opentelemetry-ext-grpc/CHANGELOG.md b/ext/opentelemetry-ext-grpc/CHANGELOG.md index 2302714790..3be32e05e5 100644 --- a/ext/opentelemetry-ext-grpc/CHANGELOG.md +++ b/ext/opentelemetry-ext-grpc/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add status code to gRPC client spans + ([896](https://github.com/open-telemetry/opentelemetry-python/pull/896)) + ## 0.8b0 Released 2020-05-27 diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index 012b3541d2..2721a3b257 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -47,6 +47,7 @@ install_requires = test = opentelemetry-test == 0.11.dev0 opentelemetry-sdk == 0.11.dev0 + protobuf == 3.12.2 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py index ebf455910c..373d8f345c 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py @@ -25,6 +25,7 @@ import grpc from opentelemetry import propagators, trace +from opentelemetry.trace.status import Status, StatusCanonicalCode from . import grpcext from ._utilities import RpcInfo @@ -33,14 +34,16 @@ class _GuardedSpan: def __init__(self, span): self.span = span + self.generated_span = None self._engaged = True def __enter__(self): - self.span.__enter__() + self.generated_span = self.span.__enter__() return self def __exit__(self, *args, **kwargs): if self._engaged: + self.generated_span = None return self.span.__exit__(*args, **kwargs) return False @@ -122,7 +125,15 @@ def intercept_unary(self, request, metadata, client_info, invoker): timeout=client_info.timeout, request=request, ) - result = invoker(request, metadata) + + try: + result = invoker(request, metadata) + except grpc.RpcError as exc: + guarded_span.generated_span.set_status( + Status(StatusCanonicalCode(exc.code().value[0])) + ) + raise + return self._trace_result(guarded_span, rpc_info, result) # For RPCs that stream responses, the result can be a generator. To record @@ -136,7 +147,7 @@ def _intercept_server_stream( else: mutable_metadata = OrderedDict(metadata) - with self._start_span(client_info.full_method): + with self._start_span(client_info.full_method) as span: _inject_span_context(mutable_metadata) metadata = tuple(mutable_metadata.items()) rpc_info = RpcInfo( @@ -146,9 +157,16 @@ def _intercept_server_stream( ) if client_info.is_client_stream: rpc_info.request = request_or_iterator - result = invoker(request_or_iterator, metadata) - for response in result: - yield response + + try: + result = invoker(request_or_iterator, metadata) + for response in result: + yield response + except grpc.RpcError as exc: + span.set_status( + Status(StatusCanonicalCode(exc.code().value[0])) + ) + raise def intercept_stream( self, request_or_iterator, metadata, client_info, invoker @@ -172,5 +190,13 @@ def intercept_stream( timeout=client_info.timeout, request=request_or_iterator, ) - result = invoker(request_or_iterator, metadata) + + try: + result = invoker(request_or_iterator, metadata) + except grpc.RpcError as exc: + guarded_span.generated_span.set_status( + Status(StatusCanonicalCode(exc.code().value[0])) + ) + raise + return self._trace_result(guarded_span, rpc_info, result) diff --git a/ext/opentelemetry-ext-grpc/tests/_client.py b/ext/opentelemetry-ext-grpc/tests/_client.py new file mode 100644 index 0000000000..43310b5f65 --- /dev/null +++ b/ext/opentelemetry-ext-grpc/tests/_client.py @@ -0,0 +1,57 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .protobuf.test_server_pb2 import Request + +CLIENT_ID = 1 + + +def simple_method(stub, error=False): + request = Request( + client_id=CLIENT_ID, request_data="error" if error else "data" + ) + stub.SimpleMethod(request) + + +def client_streaming_method(stub, error=False): + # create a generator + def request_messages(): + for _ in range(5): + request = Request( + client_id=CLIENT_ID, request_data="error" if error else "data" + ) + yield request + + stub.ClientStreamingMethod(request_messages()) + + +def server_streaming_method(stub, error=False): + request = Request( + client_id=CLIENT_ID, request_data="error" if error else "data" + ) + response_iterator = stub.ServerStreamingMethod(request) + list(response_iterator) + + +def bidirectional_streaming_method(stub, error=False): + def request_messages(): + for _ in range(5): + request = Request( + client_id=CLIENT_ID, request_data="error" if error else "data" + ) + yield request + + response_iterator = stub.BidirectionalStreamingMethod(request_messages()) + + list(response_iterator) diff --git a/ext/opentelemetry-ext-grpc/tests/_server.py b/ext/opentelemetry-ext-grpc/tests/_server.py new file mode 100644 index 0000000000..a4e1c266b8 --- /dev/null +++ b/ext/opentelemetry-ext-grpc/tests/_server.py @@ -0,0 +1,87 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from concurrent import futures + +import grpc + +from .protobuf import test_server_pb2, test_server_pb2_grpc + +SERVER_ID = 1 + + +class TestServer(test_server_pb2_grpc.GRPCTestServerServicer): + def SimpleMethod(self, request, context): + if request.request_data == "error": + context.set_code(grpc.StatusCode.INVALID_ARGUMENT) + return test_server_pb2.Response() + response = test_server_pb2.Response( + server_id=SERVER_ID, response_data="data" + ) + return response + + def ClientStreamingMethod(self, request_iterator, context): + data = list(request_iterator) + if data[0].request_data == "error": + context.set_code(grpc.StatusCode.INVALID_ARGUMENT) + return test_server_pb2.Response() + response = test_server_pb2.Response( + server_id=SERVER_ID, response_data="data" + ) + return response + + def ServerStreamingMethod(self, request, context): + if request.request_data == "error": + + context.abort( + code=grpc.StatusCode.INVALID_ARGUMENT, + details="server stream error", + ) + return test_server_pb2.Response() + + # create a generator + def response_messages(): + for _ in range(5): + response = test_server_pb2.Response( + server_id=SERVER_ID, response_data="data" + ) + yield response + + return response_messages() + + def BidirectionalStreamingMethod(self, request_iterator, context): + data = list(request_iterator) + if data[0].request_data == "error": + context.abort( + code=grpc.StatusCode.INVALID_ARGUMENT, + details="bidirectional error", + ) + return + + for _ in range(5): + yield test_server_pb2.Response( + server_id=SERVER_ID, response_data="data" + ) + + +def create_test_server(port): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + + test_server_pb2_grpc.add_GRPCTestServerServicer_to_server( + TestServer(), server + ) + + server.add_insecure_port("localhost:{}".format(port)) + + return server diff --git a/ext/opentelemetry-ext-grpc/tests/protobuf/test_server.proto b/ext/opentelemetry-ext-grpc/tests/protobuf/test_server.proto new file mode 100644 index 0000000000..790a7675de --- /dev/null +++ b/ext/opentelemetry-ext-grpc/tests/protobuf/test_server.proto @@ -0,0 +1,34 @@ +// Copyright 2019 gRPC 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. +syntax = "proto3"; + +message Request { + int64 client_id = 1; + string request_data = 2; +} + +message Response { + int64 server_id = 1; + string response_data = 2; +} + +service GRPCTestServer { + rpc SimpleMethod (Request) returns (Response); + + rpc ClientStreamingMethod (stream Request) returns (Response); + + rpc ServerStreamingMethod (Request) returns (stream Response); + + rpc BidirectionalStreamingMethod (stream Request) returns (stream Response); +} diff --git a/ext/opentelemetry-ext-grpc/tests/protobuf/test_server_pb2.py b/ext/opentelemetry-ext-grpc/tests/protobuf/test_server_pb2.py new file mode 100644 index 0000000000..735206f850 --- /dev/null +++ b/ext/opentelemetry-ext-grpc/tests/protobuf/test_server_pb2.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: test_server.proto + +from google.protobuf import descriptor as _descriptor +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.FileDescriptor( + name="test_server.proto", + package="", + syntax="proto3", + serialized_options=None, + serialized_pb=b'\n\x11test_server.proto"2\n\x07Request\x12\x11\n\tclient_id\x18\x01 \x01(\x03\x12\x14\n\x0crequest_data\x18\x02 \x01(\t"4\n\x08Response\x12\x11\n\tserver_id\x18\x01 \x01(\x03\x12\x15\n\rresponse_data\x18\x02 \x01(\t2\xce\x01\n\x0eGRPCTestServer\x12#\n\x0cSimpleMethod\x12\x08.Request\x1a\t.Response\x12.\n\x15\x43lientStreamingMethod\x12\x08.Request\x1a\t.Response(\x01\x12.\n\x15ServerStreamingMethod\x12\x08.Request\x1a\t.Response0\x01\x12\x37\n\x1c\x42idirectionalStreamingMethod\x12\x08.Request\x1a\t.Response(\x01\x30\x01\x62\x06proto3', +) + + +_REQUEST = _descriptor.Descriptor( + name="Request", + full_name="Request", + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name="client_id", + full_name="Request.client_id", + index=0, + number=1, + type=3, + cpp_type=2, + label=1, + has_default_value=False, + default_value=0, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + ), + _descriptor.FieldDescriptor( + name="request_data", + full_name="Request.request_data", + index=1, + number=2, + type=9, + cpp_type=9, + label=1, + has_default_value=False, + default_value=b"".decode("utf-8"), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + ), + ], + extensions=[], + nested_types=[], + enum_types=[], + serialized_options=None, + is_extendable=False, + syntax="proto3", + extension_ranges=[], + oneofs=[], + serialized_start=21, + serialized_end=71, +) + + +_RESPONSE = _descriptor.Descriptor( + name="Response", + full_name="Response", + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name="server_id", + full_name="Response.server_id", + index=0, + number=1, + type=3, + cpp_type=2, + label=1, + has_default_value=False, + default_value=0, + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + ), + _descriptor.FieldDescriptor( + name="response_data", + full_name="Response.response_data", + index=1, + number=2, + type=9, + cpp_type=9, + label=1, + has_default_value=False, + default_value=b"".decode("utf-8"), + message_type=None, + enum_type=None, + containing_type=None, + is_extension=False, + extension_scope=None, + serialized_options=None, + file=DESCRIPTOR, + ), + ], + extensions=[], + nested_types=[], + enum_types=[], + serialized_options=None, + is_extendable=False, + syntax="proto3", + extension_ranges=[], + oneofs=[], + serialized_start=73, + serialized_end=125, +) + +DESCRIPTOR.message_types_by_name["Request"] = _REQUEST +DESCRIPTOR.message_types_by_name["Response"] = _RESPONSE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Request = _reflection.GeneratedProtocolMessageType( + "Request", + (_message.Message,), + { + "DESCRIPTOR": _REQUEST, + "__module__": "test_server_pb2" + # @@protoc_insertion_point(class_scope:Request) + }, +) +_sym_db.RegisterMessage(Request) + +Response = _reflection.GeneratedProtocolMessageType( + "Response", + (_message.Message,), + { + "DESCRIPTOR": _RESPONSE, + "__module__": "test_server_pb2" + # @@protoc_insertion_point(class_scope:Response) + }, +) +_sym_db.RegisterMessage(Response) + + +_GRPCTESTSERVER = _descriptor.ServiceDescriptor( + name="GRPCTestServer", + full_name="GRPCTestServer", + file=DESCRIPTOR, + index=0, + serialized_options=None, + serialized_start=128, + serialized_end=334, + methods=[ + _descriptor.MethodDescriptor( + name="SimpleMethod", + full_name="GRPCTestServer.SimpleMethod", + index=0, + containing_service=None, + input_type=_REQUEST, + output_type=_RESPONSE, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name="ClientStreamingMethod", + full_name="GRPCTestServer.ClientStreamingMethod", + index=1, + containing_service=None, + input_type=_REQUEST, + output_type=_RESPONSE, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name="ServerStreamingMethod", + full_name="GRPCTestServer.ServerStreamingMethod", + index=2, + containing_service=None, + input_type=_REQUEST, + output_type=_RESPONSE, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name="BidirectionalStreamingMethod", + full_name="GRPCTestServer.BidirectionalStreamingMethod", + index=3, + containing_service=None, + input_type=_REQUEST, + output_type=_RESPONSE, + serialized_options=None, + ), + ], +) +_sym_db.RegisterServiceDescriptor(_GRPCTESTSERVER) + +DESCRIPTOR.services_by_name["GRPCTestServer"] = _GRPCTESTSERVER + +# @@protoc_insertion_point(module_scope) diff --git a/ext/opentelemetry-ext-grpc/tests/protobuf/test_server_pb2_grpc.py b/ext/opentelemetry-ext-grpc/tests/protobuf/test_server_pb2_grpc.py new file mode 100644 index 0000000000..d0a6fd5184 --- /dev/null +++ b/ext/opentelemetry-ext-grpc/tests/protobuf/test_server_pb2_grpc.py @@ -0,0 +1,205 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +import grpc + +from tests.protobuf import test_server_pb2 as test__server__pb2 + + +class GRPCTestServerStub(object): + """Missing associated documentation comment in .proto file""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.SimpleMethod = channel.unary_unary( + "/GRPCTestServer/SimpleMethod", + request_serializer=test__server__pb2.Request.SerializeToString, + response_deserializer=test__server__pb2.Response.FromString, + ) + self.ClientStreamingMethod = channel.stream_unary( + "/GRPCTestServer/ClientStreamingMethod", + request_serializer=test__server__pb2.Request.SerializeToString, + response_deserializer=test__server__pb2.Response.FromString, + ) + self.ServerStreamingMethod = channel.unary_stream( + "/GRPCTestServer/ServerStreamingMethod", + request_serializer=test__server__pb2.Request.SerializeToString, + response_deserializer=test__server__pb2.Response.FromString, + ) + self.BidirectionalStreamingMethod = channel.stream_stream( + "/GRPCTestServer/BidirectionalStreamingMethod", + request_serializer=test__server__pb2.Request.SerializeToString, + response_deserializer=test__server__pb2.Response.FromString, + ) + + +class GRPCTestServerServicer(object): + """Missing associated documentation comment in .proto file""" + + def SimpleMethod(self, request, context): + """Missing associated documentation comment in .proto file""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def ClientStreamingMethod(self, request_iterator, context): + """Missing associated documentation comment in .proto file""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def ServerStreamingMethod(self, request, context): + """Missing associated documentation comment in .proto file""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def BidirectionalStreamingMethod(self, request_iterator, context): + """Missing associated documentation comment in .proto file""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + +def add_GRPCTestServerServicer_to_server(servicer, server): + rpc_method_handlers = { + "SimpleMethod": grpc.unary_unary_rpc_method_handler( + servicer.SimpleMethod, + request_deserializer=test__server__pb2.Request.FromString, + response_serializer=test__server__pb2.Response.SerializeToString, + ), + "ClientStreamingMethod": grpc.stream_unary_rpc_method_handler( + servicer.ClientStreamingMethod, + request_deserializer=test__server__pb2.Request.FromString, + response_serializer=test__server__pb2.Response.SerializeToString, + ), + "ServerStreamingMethod": grpc.unary_stream_rpc_method_handler( + servicer.ServerStreamingMethod, + request_deserializer=test__server__pb2.Request.FromString, + response_serializer=test__server__pb2.Response.SerializeToString, + ), + "BidirectionalStreamingMethod": grpc.stream_stream_rpc_method_handler( + servicer.BidirectionalStreamingMethod, + request_deserializer=test__server__pb2.Request.FromString, + response_serializer=test__server__pb2.Response.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + "GRPCTestServer", rpc_method_handlers + ) + server.add_generic_rpc_handlers((generic_handler,)) + + +# This class is part of an EXPERIMENTAL API. +class GRPCTestServer(object): + """Missing associated documentation comment in .proto file""" + + @staticmethod + def SimpleMethod( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/GRPCTestServer/SimpleMethod", + test__server__pb2.Request.SerializeToString, + test__server__pb2.Response.FromString, + options, + channel_credentials, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def ClientStreamingMethod( + request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.stream_unary( + request_iterator, + target, + "/GRPCTestServer/ClientStreamingMethod", + test__server__pb2.Request.SerializeToString, + test__server__pb2.Response.FromString, + options, + channel_credentials, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def ServerStreamingMethod( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_stream( + request, + target, + "/GRPCTestServer/ServerStreamingMethod", + test__server__pb2.Request.SerializeToString, + test__server__pb2.Response.FromString, + options, + channel_credentials, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) + + @staticmethod + def BidirectionalStreamingMethod( + request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.stream_stream( + request_iterator, + target, + "/GRPCTestServer/BidirectionalStreamingMethod", + test__server__pb2.Request.SerializeToString, + test__server__pb2.Response.FromString, + options, + channel_credentials, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) diff --git a/ext/opentelemetry-ext-grpc/tests/test_client_interceptor.py b/ext/opentelemetry-ext-grpc/tests/test_client_interceptor.py new file mode 100644 index 0000000000..47dc9fa0bb --- /dev/null +++ b/ext/opentelemetry-ext-grpc/tests/test_client_interceptor.py @@ -0,0 +1,139 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import grpc + +import opentelemetry.ext.grpc +from opentelemetry import metrics, trace +from opentelemetry.ext.grpc import client_interceptor +from opentelemetry.ext.grpc.grpcext import intercept_channel +from opentelemetry.sdk.metrics.export.controller import PushController +from opentelemetry.test.test_base import TestBase +from tests.protobuf import test_server_pb2_grpc + +from ._client import ( + bidirectional_streaming_method, + client_streaming_method, + server_streaming_method, + simple_method, +) +from ._server import create_test_server + + +class TestClientProto(TestBase): + def setUp(self): + super().setUp() + self.server = create_test_server(25565) + self.server.start() + meter = metrics.get_meter(__name__) + interceptor = client_interceptor() + self.channel = intercept_channel( + grpc.insecure_channel("localhost:25565"), interceptor + ) + self._stub = test_server_pb2_grpc.GRPCTestServerStub(self.channel) + + self._controller = PushController( + meter, self.memory_metrics_exporter, 30 + ) + + def tearDown(self): + super().tearDown() + self.memory_metrics_exporter.clear() + self.server.stop(None) + + def test_unary_stream(self): + server_streaming_method(self._stub) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + + self.assertEqual(span.name, "/GRPCTestServer/ServerStreamingMethod") + self.assertIs(span.kind, trace.SpanKind.CLIENT) + + # Check version and name in span's instrumentation info + self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + + def test_stream_unary(self): + client_streaming_method(self._stub) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + + self.assertEqual(span.name, "/GRPCTestServer/ClientStreamingMethod") + self.assertIs(span.kind, trace.SpanKind.CLIENT) + + # Check version and name in span's instrumentation info + self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + + def test_stream_stream(self): + bidirectional_streaming_method(self._stub) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + + self.assertEqual( + span.name, "/GRPCTestServer/BidirectionalStreamingMethod" + ) + self.assertIs(span.kind, trace.SpanKind.CLIENT) + + # Check version and name in span's instrumentation info + self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + + def test_error_simple(self): + with self.assertRaises(grpc.RpcError): + simple_method(self._stub, error=True) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual( + span.status.canonical_code.value, + grpc.StatusCode.INVALID_ARGUMENT.value[0], + ) + + def test_error_stream_unary(self): + with self.assertRaises(grpc.RpcError): + client_streaming_method(self._stub, error=True) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual( + span.status.canonical_code.value, + grpc.StatusCode.INVALID_ARGUMENT.value[0], + ) + + def test_error_unary_stream(self): + with self.assertRaises(grpc.RpcError): + server_streaming_method(self._stub, error=True) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual( + span.status.canonical_code.value, + grpc.StatusCode.INVALID_ARGUMENT.value[0], + ) + + def test_error_stream_stream(self): + with self.assertRaises(grpc.RpcError): + bidirectional_streaming_method(self._stub, error=True) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual( + span.status.canonical_code.value, + grpc.StatusCode.INVALID_ARGUMENT.value[0], + ) diff --git a/tox.ini b/tox.ini index ea3a5c2913..2c87f69246 100644 --- a/tox.ini +++ b/tox.ini @@ -142,7 +142,7 @@ envlist = pypy3-test-core-opentracing-shim ; opentelemetry-ext-grpc - py3{4,5,6,7,8}-test-instrumentation-grpc + py3{5,6,7,8}-test-instrumentation-grpc ; opentelemetry-ext-sqlalchemy py3{4,5,6,7,8}-test-instrumentation-sqlalchemy From cfd9225f8ea6599cadd322f2a8818b1cb502a0ca Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 16 Jul 2020 18:07:59 +0000 Subject: [PATCH 0464/1517] Add instructions to proto package README (#905) --- opentelemetry-proto/README.rst | 11 ++++++++--- scripts/proto_codegen.sh | 20 ++++++++++++++------ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/opentelemetry-proto/README.rst b/opentelemetry-proto/README.rst index 48b9661b20..6825aa8823 100644 --- a/opentelemetry-proto/README.rst +++ b/opentelemetry-proto/README.rst @@ -16,11 +16,15 @@ Installation Code Generation --------------- -These files were generated automatically. More details on how to generate them -are available here_. +These files were generated automatically from code in opentelemetry-proto_. +To regenerate the code, run ``../scripts/proto_codegen.sh``. +To build against a new release or specific commit of opentelemetry-proto_, +update the ``PROTO_REPO_BRANCH_OR_COMMIT`` variable in +``../scripts/proto_codegen.sh``. Then run the script and commit the changes +as well as any fixes needed in the OTLP exporter. -.. _here: https://github.com/open-telemetry/opentelemetry-proto +.. _opentelemetry-proto: https://github.com/open-telemetry/opentelemetry-proto References @@ -28,3 +32,4 @@ References * `OpenTelemetry Project `_ * `OpenTelemetry Proto `_ +* `proto_codegen.sh script `_ diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index b840f8452b..f7742db6f6 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -16,16 +16,24 @@ PROTO_REPO_BRANCH_OR_COMMIT="v0.4.0" set -e -if [ -z "$VIRTUAL_ENV" ]; then - echo '$VIRTUAL_ENV is not set, you probably forgot to source it. Exiting...' - exit 1 -fi - PROTO_REPO_DIR=${PROTO_REPO_DIR:-"/tmp/opentelemetry-proto"} # root of opentelemetry-python repo repo_root="$(git rev-parse --show-toplevel)" +venv_dir="/tmp/proto_codegen_venv" + +# run on exit even if crash +cleanup() { + echo "Deleting $venv_dir" + rm -rf $venv_dir +} +trap cleanup EXIT -python -m pip install -r $repo_root/dev-requirements.txt +echo "Creating temporary virtualenv at $venv_dir using $(python3 --version)" +python3 -m venv $venv_dir +source $venv_dir/bin/activate +python -m pip install \ + -c $repo_root/dev-requirements.txt \ + grpcio-tools mypy-protobuf # Clone the proto repo if it doesn't exist if [ ! -d "$PROTO_REPO_DIR" ]; then From 23e577e5504384432ed37b31149b61627004e3e3 Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Thu, 16 Jul 2020 16:12:44 -0400 Subject: [PATCH 0465/1517] remove google exporter files (#918) --- docs-requirements.txt | 6 +- .../CHANGELOG.md | 21 - .../LICENSE | 201 ------- .../MANIFEST.in | 9 - .../README.rst | 18 - .../setup.cfg | 51 -- .../setup.py | 31 -- .../exporter/cloud_monitoring/__init__.py | 236 -------- .../exporter/cloud_monitoring/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_cloud_monitoring.py | 436 --------------- .../CHANGELOG.md | 25 - .../LICENSE | 201 ------- .../MANIFEST.in | 9 - .../README.rst | 43 -- .../setup.cfg | 51 -- .../setup.py | 26 - .../exporter/cloud_trace/__init__.py | 382 ------------- .../cloud_trace/cloud_trace_propagator.py | 89 --- .../exporter/cloud_trace/version.py | 15 - .../opentelemetry/tools/resource_detector.py | 47 -- .../tests/__init__.py | 0 .../tests/test_cloud_trace_exporter.py | 508 ------------------ .../tests/test_cloud_trace_propagator.py | 238 -------- .../tests/test_gcp_resource_detector.py | 84 --- scripts/coverage.sh | 2 - tox.ini | 13 - 27 files changed, 3 insertions(+), 2754 deletions(-) delete mode 100644 ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md delete mode 100644 ext/opentelemetry-exporter-cloud-monitoring/LICENSE delete mode 100644 ext/opentelemetry-exporter-cloud-monitoring/MANIFEST.in delete mode 100644 ext/opentelemetry-exporter-cloud-monitoring/README.rst delete mode 100644 ext/opentelemetry-exporter-cloud-monitoring/setup.cfg delete mode 100644 ext/opentelemetry-exporter-cloud-monitoring/setup.py delete mode 100644 ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py delete mode 100644 ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py delete mode 100644 ext/opentelemetry-exporter-cloud-monitoring/tests/__init__.py delete mode 100644 ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py delete mode 100644 ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md delete mode 100644 ext/opentelemetry-exporter-cloud-trace/LICENSE delete mode 100644 ext/opentelemetry-exporter-cloud-trace/MANIFEST.in delete mode 100644 ext/opentelemetry-exporter-cloud-trace/README.rst delete mode 100644 ext/opentelemetry-exporter-cloud-trace/setup.cfg delete mode 100644 ext/opentelemetry-exporter-cloud-trace/setup.py delete mode 100644 ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py delete mode 100644 ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/cloud_trace_propagator.py delete mode 100644 ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py delete mode 100644 ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/tools/resource_detector.py delete mode 100644 ext/opentelemetry-exporter-cloud-trace/tests/__init__.py delete mode 100644 ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py delete mode 100644 ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_propagator.py delete mode 100644 ext/opentelemetry-exporter-cloud-trace/tests/test_gcp_resource_detector.py diff --git a/docs-requirements.txt b/docs-requirements.txt index 169bbb7f0b..0fa078ef23 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -25,8 +25,8 @@ wrapt>=1.0.0,<2.0.0 celery>=4.0 psutil~=5.7.0 boto~=2.0 -google-cloud-trace >=0.23.0 -google-cloud-monitoring>=0.36.0 botocore~=1.0 starlette~=0.13 -fastapi~=0.58.1 \ No newline at end of file +fastapi~=0.58.1 +opentelemetry-exporter-cloud-trace==0.10b0 +opentelemetry-exporter-cloud-monitoring==0.10b0 \ No newline at end of file diff --git a/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md b/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md deleted file mode 100644 index e5f01ee5b0..0000000000 --- a/ext/opentelemetry-exporter-cloud-monitoring/CHANGELOG.md +++ /dev/null @@ -1,21 +0,0 @@ -# Changelog - -## Unreleased - -- Add support for resources - ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) - -## Version 0.10b0 - -Released 2020-06-23 - -- Add ability for exporter to add unique identifier - ([#841](https://github.com/open-telemetry/opentelemetry-python/pull/841)) -- Added tests to tox coverage files - ([#804](https://github.com/open-telemetry/opentelemetry-python/pull/804)) - -## 0.9b0 - -Released 2020-06-10 - -- Initial release diff --git a/ext/opentelemetry-exporter-cloud-monitoring/LICENSE b/ext/opentelemetry-exporter-cloud-monitoring/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/ext/opentelemetry-exporter-cloud-monitoring/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/ext/opentelemetry-exporter-cloud-monitoring/MANIFEST.in b/ext/opentelemetry-exporter-cloud-monitoring/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/ext/opentelemetry-exporter-cloud-monitoring/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/ext/opentelemetry-exporter-cloud-monitoring/README.rst b/ext/opentelemetry-exporter-cloud-monitoring/README.rst deleted file mode 100644 index f1fd52528c..0000000000 --- a/ext/opentelemetry-exporter-cloud-monitoring/README.rst +++ /dev/null @@ -1,18 +0,0 @@ -OpenTelemetry Cloud Monitoring Exporters -======================================== - -This library provides classes for exporting metrics data to Google Cloud Monitoring. - -Installation ------------- - -:: - - pip install opentelemetry-exporter-cloud-monitoring - -References ----------- - -* `OpenTelemetry Cloud Monitoring Exporter `_ -* `Cloud Monitoring `_ -* `OpenTelemetry Project `_ \ No newline at end of file diff --git a/ext/opentelemetry-exporter-cloud-monitoring/setup.cfg b/ext/opentelemetry-exporter-cloud-monitoring/setup.cfg deleted file mode 100644 index 8875937751..0000000000 --- a/ext/opentelemetry-exporter-cloud-monitoring/setup.cfg +++ /dev/null @@ -1,51 +0,0 @@ - -# Copyright OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-cloud-monitoring -description = Cloud Monitoring integration for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-exporter-cloud-monitoring -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - -[options] -python_requires = >=3.4 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api - opentelemetry-sdk - google-cloud-monitoring - -[options.packages.find] -where = src - -[options.extras_require] -test = diff --git a/ext/opentelemetry-exporter-cloud-monitoring/setup.py b/ext/opentelemetry-exporter-cloud-monitoring/setup.py deleted file mode 100644 index 0ca88bc330..0000000000 --- a/ext/opentelemetry-exporter-cloud-monitoring/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "exporter", - "cloud_monitoring", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py deleted file mode 100644 index 2c6e3abdb0..0000000000 --- a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py +++ /dev/null @@ -1,236 +0,0 @@ -import logging -import random -from typing import Optional, Sequence - -import google.auth -from google.api.label_pb2 import LabelDescriptor -from google.api.metric_pb2 import MetricDescriptor -from google.api.monitored_resource_pb2 import MonitoredResource -from google.cloud.monitoring_v3 import MetricServiceClient -from google.cloud.monitoring_v3.proto.metric_pb2 import TimeSeries - -from opentelemetry.sdk.metrics.export import ( - MetricRecord, - MetricsExporter, - MetricsExportResult, -) -from opentelemetry.sdk.metrics.export.aggregate import SumAggregator -from opentelemetry.sdk.resources import Resource - -logger = logging.getLogger(__name__) -MAX_BATCH_WRITE = 200 -WRITE_INTERVAL = 10 -UNIQUE_IDENTIFIER_KEY = "opentelemetry_id" - -OT_RESOURCE_LABEL_TO_GCP = { - "gce_instance": { - "cloud.account.id": "project_id", - "host.id": "instance_id", - "cloud.zone": "zone", - } -} - - -# pylint is unable to resolve members of protobuf objects -# pylint: disable=no-member -class CloudMonitoringMetricsExporter(MetricsExporter): - """ Implementation of Metrics Exporter to Google Cloud Monitoring - - You can manually pass in project_id and client, or else the - Exporter will take that information from Application Default - Credentials. - - Args: - project_id: project id of your Google Cloud project. - client: Client to upload metrics to Google Cloud Monitoring. - add_unique_identifier: Add an identifier to each exporter metric. This - must be used when there exist two (or more) exporters that may - export to the same metric name within WRITE_INTERVAL seconds of - each other. - """ - - def __init__( - self, project_id=None, client=None, add_unique_identifier=False - ): - self.client = client or MetricServiceClient() - if not project_id: - _, self.project_id = google.auth.default() - else: - self.project_id = project_id - self.project_name = self.client.project_path(self.project_id) - self._metric_descriptors = {} - self._last_updated = {} - self.unique_identifier = None - if add_unique_identifier: - self.unique_identifier = "{:08x}".format( - random.randint(0, 16 ** 8) - ) - - @staticmethod - def _get_monitored_resource( - resource: Resource, - ) -> Optional[MonitoredResource]: - """Add Google resource specific information (e.g. instance id, region). - - See - https://cloud.google.com/monitoring/custom-metrics/creating-metrics#custom-metric-resources - for supported types - Args: - series: ProtoBuf TimeSeries - """ - - if resource.labels.get("cloud.provider") != "gcp": - return None - resource_type = resource.labels["gcp.resource_type"] - if resource_type not in OT_RESOURCE_LABEL_TO_GCP: - return None - return MonitoredResource( - type=resource_type, - labels={ - gcp_label: str(resource.labels[ot_label]) - for ot_label, gcp_label in OT_RESOURCE_LABEL_TO_GCP[ - resource_type - ].items() - }, - ) - - def _batch_write(self, series: TimeSeries) -> None: - """ Cloud Monitoring allows writing up to 200 time series at once - - :param series: ProtoBuf TimeSeries - :return: - """ - write_ind = 0 - while write_ind < len(series): - self.client.create_time_series( - self.project_name, - series[write_ind : write_ind + MAX_BATCH_WRITE], - ) - write_ind += MAX_BATCH_WRITE - - def _get_metric_descriptor( - self, record: MetricRecord - ) -> Optional[MetricDescriptor]: - """ We can map Metric to MetricDescriptor using Metric.name or - MetricDescriptor.type. We create the MetricDescriptor if it doesn't - exist already and cache it. Note that recreating MetricDescriptors is - a no-op if it already exists. - - :param record: - :return: - """ - instrument = record.instrument - descriptor_type = "custom.googleapis.com/OpenTelemetry/{}".format( - instrument.name - ) - if descriptor_type in self._metric_descriptors: - return self._metric_descriptors[descriptor_type] - descriptor = { - "name": None, - "type": descriptor_type, - "display_name": instrument.name, - "description": instrument.description, - "labels": [], - } - for key, value in record.labels: - if isinstance(value, str): - descriptor["labels"].append( - LabelDescriptor(key=key, value_type="STRING") - ) - elif isinstance(value, bool): - descriptor["labels"].append( - LabelDescriptor(key=key, value_type="BOOL") - ) - elif isinstance(value, int): - descriptor["labels"].append( - LabelDescriptor(key=key, value_type="INT64") - ) - else: - logger.warning( - "Label value %s is not a string, bool or integer", value - ) - - if self.unique_identifier: - descriptor["labels"].append( - LabelDescriptor(key=UNIQUE_IDENTIFIER_KEY, value_type="STRING") - ) - - if isinstance(record.aggregator, SumAggregator): - descriptor["metric_kind"] = MetricDescriptor.MetricKind.GAUGE - else: - logger.warning( - "Unsupported aggregation type %s, ignoring it", - type(record.aggregator).__name__, - ) - return None - if instrument.value_type == int: - descriptor["value_type"] = MetricDescriptor.ValueType.INT64 - elif instrument.value_type == float: - descriptor["value_type"] = MetricDescriptor.ValueType.DOUBLE - proto_descriptor = MetricDescriptor(**descriptor) - try: - descriptor = self.client.create_metric_descriptor( - self.project_name, proto_descriptor - ) - # pylint: disable=broad-except - except Exception as ex: - logger.error( - "Failed to create metric descriptor %s", - proto_descriptor, - exc_info=ex, - ) - return None - self._metric_descriptors[descriptor_type] = descriptor - return descriptor - - def export( - self, metric_records: Sequence[MetricRecord] - ) -> "MetricsExportResult": - all_series = [] - for record in metric_records: - instrument = record.instrument - metric_descriptor = self._get_metric_descriptor(record) - if not metric_descriptor: - continue - series = TimeSeries( - resource=self._get_monitored_resource( - record.instrument.meter.resource - ) - ) - series.metric.type = metric_descriptor.type - for key, value in record.labels: - series.metric.labels[key] = str(value) - - if self.unique_identifier: - series.metric.labels[ - UNIQUE_IDENTIFIER_KEY - ] = self.unique_identifier - - point = series.points.add() - if instrument.value_type == int: - point.value.int64_value = record.aggregator.checkpoint - elif instrument.value_type == float: - point.value.double_value = record.aggregator.checkpoint - seconds, nanos = divmod( - record.aggregator.last_update_timestamp, 1e9 - ) - - # Cloud Monitoring API allows, for any combination of labels and - # metric name, one update per WRITE_INTERVAL seconds - updated_key = (metric_descriptor.type, record.labels) - last_updated_seconds = self._last_updated.get(updated_key, 0) - if seconds <= last_updated_seconds + WRITE_INTERVAL: - continue - self._last_updated[updated_key] = seconds - point.interval.end_time.seconds = int(seconds) - point.interval.end_time.nanos = int(nanos) - all_series.append(series) - try: - self._batch_write(all_series) - # pylint: disable=broad-except - except Exception as ex: - logger.error( - "Error while writing to Cloud Monitoring", exc_info=ex - ) - return MetricsExportResult.FAILURE - return MetricsExportResult.SUCCESS diff --git a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py b/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py deleted file mode 100644 index 8d3a82b3e5..0000000000 --- a/ext/opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-exporter-cloud-monitoring/tests/__init__.py b/ext/opentelemetry-exporter-cloud-monitoring/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py b/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py deleted file mode 100644 index bc695f6d1f..0000000000 --- a/ext/opentelemetry-exporter-cloud-monitoring/tests/test_cloud_monitoring.py +++ /dev/null @@ -1,436 +0,0 @@ -# Copyright OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -from unittest import mock - -from google.api.label_pb2 import LabelDescriptor -from google.api.metric_pb2 import MetricDescriptor -from google.api.monitored_resource_pb2 import MonitoredResource -from google.cloud.monitoring_v3.proto.metric_pb2 import TimeSeries - -from opentelemetry.exporter.cloud_monitoring import ( - MAX_BATCH_WRITE, - UNIQUE_IDENTIFIER_KEY, - WRITE_INTERVAL, - CloudMonitoringMetricsExporter, -) -from opentelemetry.sdk.metrics.export import MetricRecord -from opentelemetry.sdk.metrics.export.aggregate import SumAggregator -from opentelemetry.sdk.resources import Resource - - -class UnsupportedAggregator: - pass - - -class MockMeter: - def __init__(self, resource=Resource.create_empty()): - self.resource = resource - - -class MockMetric: - def __init__( - self, - name="name", - description="description", - value_type=int, - meter=None, - ): - self.name = name - self.description = description - self.value_type = value_type - self.meter = meter or MockMeter() - - -# pylint: disable=protected-access -# pylint can't deal with ProtoBuf object members -# pylint: disable=no-member - - -class TestCloudMonitoringMetricsExporter(unittest.TestCase): - def setUp(self): - self.client_patcher = mock.patch( - "opentelemetry.exporter.cloud_monitoring.MetricServiceClient" - ) - self.client_patcher.start() - self.project_id = "PROJECT" - self.project_name = "PROJECT_NAME" - - def tearDown(self): - self.client_patcher.stop() - - def test_constructor_default(self): - exporter = CloudMonitoringMetricsExporter(self.project_id) - self.assertEqual(exporter.project_id, self.project_id) - - def test_constructor_explicit(self): - client = mock.Mock() - exporter = CloudMonitoringMetricsExporter( - self.project_id, client=client - ) - - self.assertIs(exporter.client, client) - self.assertEqual(exporter.project_id, self.project_id) - - def test_batch_write(self): - client = mock.Mock() - exporter = CloudMonitoringMetricsExporter( - project_id=self.project_id, client=client - ) - exporter.project_name = self.project_name - exporter._batch_write(range(2 * MAX_BATCH_WRITE + 1)) - client.create_time_series.assert_has_calls( - [ - mock.call(self.project_name, range(MAX_BATCH_WRITE)), - mock.call( - self.project_name, - range(MAX_BATCH_WRITE, 2 * MAX_BATCH_WRITE), - ), - mock.call( - self.project_name, - range(2 * MAX_BATCH_WRITE, 2 * MAX_BATCH_WRITE + 1), - ), - ] - ) - - exporter._batch_write(range(MAX_BATCH_WRITE)) - client.create_time_series.assert_has_calls( - [mock.call(self.project_name, range(MAX_BATCH_WRITE))] - ) - - exporter._batch_write(range(MAX_BATCH_WRITE - 1)) - client.create_time_series.assert_has_calls( - [mock.call(self.project_name, range(MAX_BATCH_WRITE - 1))] - ) - - def test_get_metric_descriptor(self): - client = mock.Mock() - exporter = CloudMonitoringMetricsExporter( - project_id=self.project_id, client=client - ) - exporter.project_name = self.project_name - - self.assertIsNone( - exporter._get_metric_descriptor( - MetricRecord(MockMetric(), (), UnsupportedAggregator()) - ) - ) - - record = MetricRecord( - MockMetric(), (("label1", "value1"),), SumAggregator(), - ) - metric_descriptor = exporter._get_metric_descriptor(record) - client.create_metric_descriptor.assert_called_with( - self.project_name, - MetricDescriptor( - **{ - "name": None, - "type": "custom.googleapis.com/OpenTelemetry/name", - "display_name": "name", - "description": "description", - "labels": [ - LabelDescriptor(key="label1", value_type="STRING") - ], - "metric_kind": "GAUGE", - "value_type": "INT64", - } - ), - ) - - # Getting a cached metric descriptor shouldn't use another call - cached_metric_descriptor = exporter._get_metric_descriptor(record) - self.assertEqual(client.create_metric_descriptor.call_count, 1) - self.assertEqual(metric_descriptor, cached_metric_descriptor) - - # Drop labels with values that aren't string, int or bool - exporter._get_metric_descriptor( - MetricRecord( - MockMetric(name="name2", value_type=float), - ( - ("label1", "value1"), - ("label2", dict()), - ("label3", 3), - ("label4", False), - ), - SumAggregator(), - ) - ) - client.create_metric_descriptor.assert_called_with( - self.project_name, - MetricDescriptor( - **{ - "name": None, - "type": "custom.googleapis.com/OpenTelemetry/name2", - "display_name": "name2", - "description": "description", - "labels": [ - LabelDescriptor(key="label1", value_type="STRING"), - LabelDescriptor(key="label3", value_type="INT64"), - LabelDescriptor(key="label4", value_type="BOOL"), - ], - "metric_kind": "GAUGE", - "value_type": "DOUBLE", - } - ), - ) - - def test_export(self): - client = mock.Mock() - exporter = CloudMonitoringMetricsExporter( - project_id=self.project_id, client=client - ) - exporter.project_name = self.project_name - - exporter.export( - [ - MetricRecord( - MockMetric(), - (("label1", "value1"),), - UnsupportedAggregator(), - ) - ] - ) - client.create_time_series.assert_not_called() - - client.create_metric_descriptor.return_value = MetricDescriptor( - **{ - "name": None, - "type": "custom.googleapis.com/OpenTelemetry/name", - "display_name": "name", - "description": "description", - "labels": [ - LabelDescriptor(key="label1", value_type="STRING"), - LabelDescriptor(key="label2", value_type="INT64"), - ], - "metric_kind": "GAUGE", - "value_type": "DOUBLE", - } - ) - - resource = Resource( - labels={ - "cloud.account.id": 123, - "host.id": "host", - "cloud.zone": "US", - "cloud.provider": "gcp", - "extra_info": "extra", - "gcp.resource_type": "gce_instance", - "not_gcp_resource": "value", - } - ) - - sum_agg_one = SumAggregator() - sum_agg_one.checkpoint = 1 - sum_agg_one.last_update_timestamp = (WRITE_INTERVAL + 1) * 1e9 - exporter.export( - [ - MetricRecord( - MockMetric(meter=MockMeter(resource=resource)), - (("label1", "value1"), ("label2", 1),), - sum_agg_one, - ), - MetricRecord( - MockMetric(meter=MockMeter(resource=resource)), - (("label1", "value2"), ("label2", 2),), - sum_agg_one, - ), - ] - ) - expected_resource = MonitoredResource( - type="gce_instance", - labels={"project_id": "123", "instance_id": "host", "zone": "US"}, - ) - - series1 = TimeSeries(resource=expected_resource) - series1.metric.type = "custom.googleapis.com/OpenTelemetry/name" - series1.metric.labels["label1"] = "value1" - series1.metric.labels["label2"] = "1" - point = series1.points.add() - point.value.int64_value = 1 - point.interval.end_time.seconds = WRITE_INTERVAL + 1 - point.interval.end_time.nanos = 0 - - series2 = TimeSeries(resource=expected_resource) - series2.metric.type = "custom.googleapis.com/OpenTelemetry/name" - series2.metric.labels["label1"] = "value2" - series2.metric.labels["label2"] = "2" - point = series2.points.add() - point.value.int64_value = 1 - point.interval.end_time.seconds = WRITE_INTERVAL + 1 - point.interval.end_time.nanos = 0 - client.create_time_series.assert_has_calls( - [mock.call(self.project_name, [series1, series2])] - ) - - # Attempting to export too soon after another export with the exact - # same labels leads to it being dropped - - sum_agg_two = SumAggregator() - sum_agg_two.checkpoint = 1 - sum_agg_two.last_update_timestamp = (WRITE_INTERVAL + 2) * 1e9 - exporter.export( - [ - MetricRecord( - MockMetric(), - (("label1", "value1"), ("label2", 1),), - sum_agg_two, - ), - MetricRecord( - MockMetric(), - (("label1", "value2"), ("label2", 2),), - sum_agg_two, - ), - ] - ) - self.assertEqual(client.create_time_series.call_count, 1) - - # But exporting with different labels is fine - sum_agg_two.checkpoint = 2 - exporter.export( - [ - MetricRecord( - MockMetric(), - (("label1", "changed_label"), ("label2", 2),), - sum_agg_two, - ), - ] - ) - series3 = TimeSeries() - series3.metric.type = "custom.googleapis.com/OpenTelemetry/name" - series3.metric.labels["label1"] = "changed_label" - series3.metric.labels["label2"] = "2" - point = series3.points.add() - point.value.int64_value = 2 - point.interval.end_time.seconds = WRITE_INTERVAL + 2 - point.interval.end_time.nanos = 0 - client.create_time_series.assert_has_calls( - [ - mock.call(self.project_name, [series1, series2]), - mock.call(self.project_name, [series3]), - ] - ) - - def test_unique_identifier(self): - client = mock.Mock() - exporter1 = CloudMonitoringMetricsExporter( - project_id=self.project_id, - client=client, - add_unique_identifier=True, - ) - exporter2 = CloudMonitoringMetricsExporter( - project_id=self.project_id, - client=client, - add_unique_identifier=True, - ) - exporter1.project_name = self.project_name - exporter2.project_name = self.project_name - - client.create_metric_descriptor.return_value = MetricDescriptor( - **{ - "name": None, - "type": "custom.googleapis.com/OpenTelemetry/name", - "display_name": "name", - "description": "description", - "labels": [ - LabelDescriptor( - key=UNIQUE_IDENTIFIER_KEY, value_type="STRING" - ), - ], - "metric_kind": "GAUGE", - "value_type": "DOUBLE", - } - ) - - sum_agg_one = SumAggregator() - sum_agg_one.update(1) - metric_record = MetricRecord(MockMetric(), (), sum_agg_one,) - exporter1.export([metric_record]) - exporter2.export([metric_record]) - - ( - first_call, - second_call, - ) = client.create_metric_descriptor.call_args_list - self.assertEqual(first_call[0][1].labels[0].key, UNIQUE_IDENTIFIER_KEY) - self.assertEqual( - second_call[0][1].labels[0].key, UNIQUE_IDENTIFIER_KEY - ) - - first_call, second_call = client.create_time_series.call_args_list - self.assertNotEqual( - first_call[0][1][0].metric.labels[UNIQUE_IDENTIFIER_KEY], - second_call[0][1][0].metric.labels[UNIQUE_IDENTIFIER_KEY], - ) - - def test_extract_resources(self): - exporter = CloudMonitoringMetricsExporter(project_id=self.project_id) - - self.assertIsNone( - exporter._get_monitored_resource(Resource.create_empty()) - ) - resource = Resource( - labels={ - "cloud.account.id": 123, - "host.id": "host", - "cloud.zone": "US", - "cloud.provider": "gcp", - "extra_info": "extra", - "gcp.resource_type": "gce_instance", - "not_gcp_resource": "value", - } - ) - expected_extract = MonitoredResource( - type="gce_instance", - labels={"project_id": "123", "instance_id": "host", "zone": "US"}, - ) - self.assertEqual( - exporter._get_monitored_resource(resource), expected_extract - ) - - resource = Resource( - labels={ - "cloud.account.id": "123", - "host.id": "host", - "extra_info": "extra", - "not_gcp_resource": "value", - "gcp.resource_type": "gce_instance", - "cloud.provider": "gcp", - } - ) - # Should throw when passed a malformed GCP resource dict - self.assertRaises(KeyError, exporter._get_monitored_resource, resource) - - resource = Resource( - labels={ - "cloud.account.id": "123", - "host.id": "host", - "extra_info": "extra", - "not_gcp_resource": "value", - "gcp.resource_type": "unsupported_gcp_resource", - "cloud.provider": "gcp", - } - ) - self.assertIsNone(exporter._get_monitored_resource(resource)) - - resource = Resource( - labels={ - "cloud.account.id": "123", - "host.id": "host", - "extra_info": "extra", - "not_gcp_resource": "value", - "cloud.provider": "aws", - } - ) - self.assertIsNone(exporter._get_monitored_resource(resource)) diff --git a/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md b/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md deleted file mode 100644 index 22eac3e4bf..0000000000 --- a/ext/opentelemetry-exporter-cloud-trace/CHANGELOG.md +++ /dev/null @@ -1,25 +0,0 @@ -# Changelog - -## Unreleased - -- Add support for resources and resource detector - ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) - -## Version 0.10b0 - -Released 2020-06-23 - -- Add g.co/agent label for Google internal metrics tracking - ([#833](https://github.com/open-telemetry/opentelemetry-python/pull/833)) -- Adding trouble-shooting tips - ([#827](https://github.com/open-telemetry/opentelemetry-python/pull/827)) -- Added Cloud Trace context propagation - ([#819](https://github.com/open-telemetry/opentelemetry-python/pull/819)) -- Added tests to tox coverage files - ([#804](https://github.com/open-telemetry/opentelemetry-python/pull/804)) - -## 0.9b0 - -Released 2020-06-10 - -- Initial release diff --git a/ext/opentelemetry-exporter-cloud-trace/LICENSE b/ext/opentelemetry-exporter-cloud-trace/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/ext/opentelemetry-exporter-cloud-trace/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/ext/opentelemetry-exporter-cloud-trace/MANIFEST.in b/ext/opentelemetry-exporter-cloud-trace/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/ext/opentelemetry-exporter-cloud-trace/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/ext/opentelemetry-exporter-cloud-trace/README.rst b/ext/opentelemetry-exporter-cloud-trace/README.rst deleted file mode 100644 index 001f163007..0000000000 --- a/ext/opentelemetry-exporter-cloud-trace/README.rst +++ /dev/null @@ -1,43 +0,0 @@ -OpenTelemetry Cloud Trace Exporters -=================================== - -This library provides classes for exporting trace data to Google Cloud Trace. - -Installation ------------- - -:: - - pip install opentelemetry-exporter-cloud-trace - -Usage ------ - -.. code:: python - - from opentelemetry import trace - from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import ( - SimpleExportSpanProcessor, - ) - - trace.set_tracer_provider(TracerProvider()) - - cloud_trace_exporter = CloudTraceSpanExporter( - project_id='my-gcloud-project', - ) - trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(cloud_trace_exporter) - ) - tracer = trace.get_tracer(__name__) - with tracer.start_as_current_span('foo'): - print('Hello world!') - - - -References ----------- - -* `Cloud Trace `_ -* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-exporter-cloud-trace/setup.cfg b/ext/opentelemetry-exporter-cloud-trace/setup.cfg deleted file mode 100644 index 4437f40fdd..0000000000 --- a/ext/opentelemetry-exporter-cloud-trace/setup.cfg +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-cloud-trace -description = Cloud Trace integration for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-exporter-cloud-trace -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - -[options] -python_requires = >=3.4 -package_dir= - =src -packages=find_namespace: -install_requires = - requests - opentelemetry-api - opentelemetry-sdk - google-cloud-trace - -[options.packages.find] -where = src - -[options.extras_require] -test = diff --git a/ext/opentelemetry-exporter-cloud-trace/setup.py b/ext/opentelemetry-exporter-cloud-trace/setup.py deleted file mode 100644 index 332cf41d01..0000000000 --- a/ext/opentelemetry-exporter-cloud-trace/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "exporter", "cloud_trace", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py deleted file mode 100644 index ea36d22727..0000000000 --- a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py +++ /dev/null @@ -1,382 +0,0 @@ -# Copyright OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Cloud Trace Span Exporter for OpenTelemetry. Uses Cloud Trace Client's REST -API to export traces and spans for viewing in Cloud Trace. - -Usage ------ - -.. code-block:: python - - from opentelemetry import trace - from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor - - trace.set_tracer_provider(TracerProvider()) - - cloud_trace_exporter = CloudTraceSpanExporter() - trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(cloud_trace_exporter) - ) - tracer = trace.get_tracer(__name__) - with tracer.start_as_current_span("foo"): - print("Hello world!") - - -API ---- -""" - -import logging -from typing import Any, Dict, List, Optional, Sequence, Tuple - -import google.auth -import pkg_resources -from google.cloud.trace_v2 import TraceServiceClient -from google.cloud.trace_v2.proto.trace_pb2 import AttributeValue -from google.cloud.trace_v2.proto.trace_pb2 import Span as ProtoSpan -from google.cloud.trace_v2.proto.trace_pb2 import TruncatableString -from google.rpc.status_pb2 import Status - -import opentelemetry.trace as trace_api -from opentelemetry.exporter.cloud_trace.version import ( - __version__ as cloud_trace_version, -) -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.trace import Event -from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult -from opentelemetry.sdk.util import BoundedDict -from opentelemetry.trace.span import ( - get_hexadecimal_span_id, - get_hexadecimal_trace_id, -) -from opentelemetry.util import types - -logger = logging.getLogger(__name__) - -MAX_NUM_LINKS = 128 -MAX_NUM_EVENTS = 32 -MAX_EVENT_ATTRS = 4 -MAX_LINK_ATTRS = 32 -MAX_SPAN_ATTRS = 32 - - -class CloudTraceSpanExporter(SpanExporter): - """Cloud Trace span exporter for OpenTelemetry. - - Args: - project_id: ID of the cloud project that will receive the traces. - client: Cloud Trace client. If not given, will be taken from gcloud - default credentials - """ - - def __init__( - self, project_id=None, client=None, - ): - self.client = client or TraceServiceClient() - if not project_id: - _, self.project_id = google.auth.default() - else: - self.project_id = project_id - - def export(self, spans: Sequence[Span]) -> SpanExportResult: - """Export the spans to Cloud Trace. - - See: https://cloud.google.com/trace/docs/reference/v2/rest/v2/projects.traces/batchWrite - - Args: - spans: Tuple of spans to export - """ - cloud_trace_spans = [] - for span in self._translate_to_cloud_trace(spans): - try: - cloud_trace_spans.append(self.client.create_span(**span)) - # pylint: disable=broad-except - except Exception as ex: - logger.error("Error when creating span %s", span, exc_info=ex) - try: - self.client.batch_write_spans( - "projects/{}".format(self.project_id), cloud_trace_spans, - ) - # pylint: disable=broad-except - except Exception as ex: - logger.error("Error while writing to Cloud Trace", exc_info=ex) - return SpanExportResult.FAILURE - - return SpanExportResult.SUCCESS - - def _translate_to_cloud_trace( - self, spans: Sequence[Span] - ) -> List[Dict[str, Any]]: - """Translate the spans to Cloud Trace format. - - Args: - spans: Tuple of spans to convert - """ - - cloud_trace_spans = [] - - for span in spans: - ctx = span.get_context() - trace_id = get_hexadecimal_trace_id(ctx.trace_id) - span_id = get_hexadecimal_span_id(ctx.span_id) - span_name = "projects/{}/traces/{}/spans/{}".format( - self.project_id, trace_id, span_id - ) - - parent_id = None - if span.parent: - parent_id = get_hexadecimal_span_id(span.parent.span_id) - - start_time = _get_time_from_ns(span.start_time) - end_time = _get_time_from_ns(span.end_time) - - if len(span.attributes) > MAX_SPAN_ATTRS: - logger.warning( - "Span has more then %s attributes, some will be truncated", - MAX_SPAN_ATTRS, - ) - - # Span does not support a MonitoredResource object. We put the - # information into labels instead. - resources_and_attrs = _extract_resources(span.resource) - resources_and_attrs.update(span.attributes) - - cloud_trace_spans.append( - { - "name": span_name, - "span_id": span_id, - "display_name": _get_truncatable_str_object( - span.name, 128 - ), - "start_time": start_time, - "end_time": end_time, - "parent_span_id": parent_id, - "attributes": _extract_attributes( - resources_and_attrs, - MAX_SPAN_ATTRS, - add_agent_attr=True, - ), - "links": _extract_links(span.links), - "status": _extract_status(span.status), - "time_events": _extract_events(span.events), - } - ) - # TODO: Leverage more of the Cloud Trace API, e.g. - # same_process_as_parent_span and child_span_count - - return cloud_trace_spans - - def shutdown(self): - pass - - -def _get_time_from_ns(nanoseconds: int) -> Dict: - """Given epoch nanoseconds, split into epoch milliseconds and remaining - nanoseconds""" - if not nanoseconds: - return None - seconds, nanos = divmod(nanoseconds, 1e9) - return {"seconds": int(seconds), "nanos": int(nanos)} - - -def _get_truncatable_str_object(str_to_convert: str, max_length: int): - """Truncate the string if it exceeds the length limit and record the - truncated bytes count.""" - truncated, truncated_byte_count = _truncate_str(str_to_convert, max_length) - - return TruncatableString( - value=truncated, truncated_byte_count=truncated_byte_count - ) - - -def _truncate_str(str_to_check: str, limit: int) -> Tuple[str, int]: - """Check the length of a string. If exceeds limit, then truncate it.""" - encoded = str_to_check.encode("utf-8") - truncated_str = encoded[:limit].decode("utf-8", errors="ignore") - return truncated_str, len(encoded) - len(truncated_str.encode("utf-8")) - - -def _extract_status(status: trace_api.Status) -> Optional[Status]: - """Convert a Status object to protobuf object.""" - if not status: - return None - status_dict = {"details": None, "code": status.canonical_code.value} - - if status.description is not None: - status_dict["message"] = status.description - - return Status(**status_dict) - - -def _extract_links(links: Sequence[trace_api.Link]) -> ProtoSpan.Links: - """Convert span.links""" - if not links: - return None - extracted_links = [] - dropped_links = 0 - if len(links) > MAX_NUM_LINKS: - logger.warning( - "Exporting more then %s links, some will be truncated", - MAX_NUM_LINKS, - ) - dropped_links = len(links) - MAX_NUM_LINKS - links = links[:MAX_NUM_LINKS] - for link in links: - if len(link.attributes) > MAX_LINK_ATTRS: - logger.warning( - "Link has more then %s attributes, some will be truncated", - MAX_LINK_ATTRS, - ) - trace_id = get_hexadecimal_trace_id(link.context.trace_id) - span_id = get_hexadecimal_span_id(link.context.span_id) - extracted_links.append( - { - "trace_id": trace_id, - "span_id": span_id, - "type": "TYPE_UNSPECIFIED", - "attributes": _extract_attributes( - link.attributes, MAX_LINK_ATTRS - ), - } - ) - return ProtoSpan.Links( - link=extracted_links, dropped_links_count=dropped_links - ) - - -def _extract_events(events: Sequence[Event]) -> ProtoSpan.TimeEvents: - """Convert span.events to dict.""" - if not events: - return None - logs = [] - dropped_annontations = 0 - if len(events) > MAX_NUM_EVENTS: - logger.warning( - "Exporting more then %s annotations, some will be truncated", - MAX_NUM_EVENTS, - ) - dropped_annontations = len(events) - MAX_NUM_EVENTS - events = events[:MAX_NUM_EVENTS] - for event in events: - if len(event.attributes) > MAX_EVENT_ATTRS: - logger.warning( - "Event %s has more then %s attributes, some will be truncated", - event.name, - MAX_EVENT_ATTRS, - ) - logs.append( - { - "time": _get_time_from_ns(event.timestamp), - "annotation": { - "description": _get_truncatable_str_object( - event.name, 256 - ), - "attributes": _extract_attributes( - event.attributes, MAX_EVENT_ATTRS - ), - }, - } - ) - return ProtoSpan.TimeEvents( - time_event=logs, - dropped_annotations_count=dropped_annontations, - dropped_message_events_count=0, - ) - - -def _strip_characters(ot_version): - return "".join(filter(lambda x: x.isdigit() or x == ".", ot_version)) - - -OT_RESOURCE_LABEL_TO_GCP = { - "gce_instance": { - "cloud.account.id": "project_id", - "host.id": "instance_id", - "cloud.zone": "zone", - } -} - - -def _extract_resources(resource: Resource) -> Dict[str, str]: - if resource.labels.get("cloud.provider") != "gcp": - return {} - resource_type = resource.labels["gcp.resource_type"] - if resource_type not in OT_RESOURCE_LABEL_TO_GCP: - return {} - return { - "g.co/r/{}/{}".format(resource_type, gcp_resource_key,): str( - resource.labels[ot_resource_key] - ) - for ot_resource_key, gcp_resource_key in OT_RESOURCE_LABEL_TO_GCP[ - resource_type - ].items() - } - - -def _extract_attributes( - attrs: types.Attributes, - num_attrs_limit: int, - add_agent_attr: bool = False, -) -> ProtoSpan.Attributes: - """Convert span.attributes to dict.""" - attributes_dict = BoundedDict(num_attrs_limit) - invalid_value_dropped_count = 0 - for key, value in attrs.items(): - key = _truncate_str(key, 128)[0] - value = _format_attribute_value(value) - - if value: - attributes_dict[key] = value - else: - invalid_value_dropped_count += 1 - if add_agent_attr: - attributes_dict["g.co/agent"] = _format_attribute_value( - "opentelemetry-python {}; google-cloud-trace-exporter {}".format( - _strip_characters( - pkg_resources.get_distribution("opentelemetry-sdk").version - ), - _strip_characters(cloud_trace_version), - ) - ) - return ProtoSpan.Attributes( - attribute_map=attributes_dict, - dropped_attributes_count=attributes_dict.dropped - + invalid_value_dropped_count, - ) - - -def _format_attribute_value(value: types.AttributeValue) -> AttributeValue: - if isinstance(value, bool): - value_type = "bool_value" - elif isinstance(value, int): - value_type = "int_value" - elif isinstance(value, str): - value_type = "string_value" - value = _get_truncatable_str_object(value, 256) - elif isinstance(value, float): - value_type = "string_value" - value = _get_truncatable_str_object("{:0.4f}".format(value), 256) - else: - logger.warning( - "ignoring attribute value %s of type %s. Values type must be one " - "of bool, int, string or float", - value, - type(value), - ) - return None - - return AttributeValue(**{value_type: value}) diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/cloud_trace_propagator.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/cloud_trace_propagator.py deleted file mode 100644 index 3f00c66d69..0000000000 --- a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/cloud_trace_propagator.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import re -import typing - -import opentelemetry.trace as trace -from opentelemetry.context.context import Context -from opentelemetry.trace.propagation import httptextformat -from opentelemetry.trace.span import ( - SpanContext, - TraceFlags, - get_hexadecimal_trace_id, -) - -_TRACE_CONTEXT_HEADER_NAME = "X-Cloud-Trace-Context" -_TRACE_CONTEXT_HEADER_FORMAT = r"(?P[0-9a-f]{32})\/(?P[\d]{1,20});o=(?P\d+)" -_TRACE_CONTEXT_HEADER_RE = re.compile(_TRACE_CONTEXT_HEADER_FORMAT) - - -class CloudTraceFormatPropagator(httptextformat.HTTPTextFormat): - """This class is for injecting into a carrier the SpanContext in Google - Cloud format, or extracting the SpanContext from a carrier using Google - Cloud format. - """ - - def extract( - self, - get_from_carrier: httptextformat.Getter[ - httptextformat.HTTPTextFormatT - ], - carrier: httptextformat.HTTPTextFormatT, - context: typing.Optional[Context] = None, - ) -> Context: - header = get_from_carrier(carrier, _TRACE_CONTEXT_HEADER_NAME) - - if not header: - return trace.set_span_in_context(trace.INVALID_SPAN, context) - - match = re.fullmatch(_TRACE_CONTEXT_HEADER_RE, header[0]) - if match is None: - return trace.set_span_in_context(trace.INVALID_SPAN, context) - - trace_id = match.group("trace_id") - span_id = match.group("span_id") - trace_options = match.group("trace_flags") - - if trace_id == "0" * 32 or int(span_id) == 0: - return trace.set_span_in_context(trace.INVALID_SPAN, context) - - span_context = SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id), - is_remote=True, - trace_flags=TraceFlags(trace_options), - ) - return trace.set_span_in_context( - trace.DefaultSpan(span_context), context - ) - - def inject( - self, - set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], - carrier: httptextformat.HTTPTextFormatT, - context: typing.Optional[Context] = None, - ) -> None: - span = trace.get_current_span(context) - span_context = span.get_context() - if span_context == trace.INVALID_SPAN_CONTEXT: - return - - header = "{}/{};o={}".format( - get_hexadecimal_trace_id(span_context.trace_id), - span_context.span_id, - int(span_context.trace_flags.sampled), - ) - set_in_carrier(carrier, _TRACE_CONTEXT_HEADER_NAME, header) diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py deleted file mode 100644 index 8d3a82b3e5..0000000000 --- a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/tools/resource_detector.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/tools/resource_detector.py deleted file mode 100644 index 5dbf7627f8..0000000000 --- a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/tools/resource_detector.py +++ /dev/null @@ -1,47 +0,0 @@ -import requests - -from opentelemetry.context import attach, detach, set_value -from opentelemetry.sdk.resources import Resource, ResourceDetector - -_GCP_METADATA_URL = ( - "http://metadata.google.internal/computeMetadata/v1/?recursive=true" -) -_GCP_METADATA_URL_HEADER = {"Metadata-Flavor": "Google"} - - -def get_gce_resources(): - """ Resource finder for common GCE attributes - - See: https://cloud.google.com/compute/docs/storing-retrieving-metadata - """ - token = attach(set_value("suppress_instrumentation", True)) - all_metadata = requests.get( - _GCP_METADATA_URL, headers=_GCP_METADATA_URL_HEADER - ).json() - detach(token) - gce_resources = { - "host.id": all_metadata["instance"]["id"], - "cloud.account.id": all_metadata["project"]["projectId"], - "cloud.zone": all_metadata["instance"]["zone"].split("/")[-1], - "cloud.provider": "gcp", - "gcp.resource_type": "gce_instance", - } - return gce_resources - - -_RESOURCE_FINDERS = [get_gce_resources] - - -class GoogleCloudResourceDetector(ResourceDetector): - def __init__(self, raise_on_error=False): - super().__init__(raise_on_error) - self.cached = False - self.gcp_resources = {} - - def detect(self) -> "Resource": - if not self.cached: - self.cached = True - for resource_finder in _RESOURCE_FINDERS: - found_resources = resource_finder() - self.gcp_resources.update(found_resources) - return Resource(self.gcp_resources) diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/__init__.py b/ext/opentelemetry-exporter-cloud-trace/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py deleted file mode 100644 index 7400207ff4..0000000000 --- a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py +++ /dev/null @@ -1,508 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -from unittest import mock - -import pkg_resources -from google.cloud.trace_v2.proto.trace_pb2 import AttributeValue -from google.cloud.trace_v2.proto.trace_pb2 import Span as ProtoSpan -from google.cloud.trace_v2.proto.trace_pb2 import TruncatableString -from google.rpc.status_pb2 import Status - -from opentelemetry.exporter.cloud_trace import ( - MAX_EVENT_ATTRS, - MAX_LINK_ATTRS, - MAX_NUM_EVENTS, - MAX_NUM_LINKS, - CloudTraceSpanExporter, - _extract_attributes, - _extract_events, - _extract_links, - _extract_resources, - _extract_status, - _format_attribute_value, - _strip_characters, - _truncate_str, -) -from opentelemetry.exporter.cloud_trace.version import ( - __version__ as cloud_trace_version, -) -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.trace import Event -from opentelemetry.sdk.trace.export import Span -from opentelemetry.trace import Link, SpanContext, SpanKind -from opentelemetry.trace.status import Status as SpanStatus -from opentelemetry.trace.status import StatusCanonicalCode - - -class TestCloudTraceSpanExporter(unittest.TestCase): - def setUp(self): - self.client_patcher = mock.patch( - "opentelemetry.exporter.cloud_trace.TraceServiceClient" - ) - self.client_patcher.start() - self.project_id = "PROJECT" - self.attributes_variety_pack = { - "str_key": "str_value", - "bool_key": False, - "double_key": 1.421, - "int_key": 123, - } - self.extracted_attributes_variety_pack = ProtoSpan.Attributes( - attribute_map={ - "str_key": AttributeValue( - string_value=TruncatableString( - value="str_value", truncated_byte_count=0 - ) - ), - "bool_key": AttributeValue(bool_value=False), - "double_key": AttributeValue( - string_value=TruncatableString( - value="1.4210", truncated_byte_count=0 - ) - ), - "int_key": AttributeValue(int_value=123), - } - ) - - def tearDown(self): - self.client_patcher.stop() - - def test_constructor_default(self): - exporter = CloudTraceSpanExporter(self.project_id) - self.assertEqual(exporter.project_id, self.project_id) - - def test_constructor_explicit(self): - client = mock.Mock() - exporter = CloudTraceSpanExporter(self.project_id, client=client) - - self.assertIs(exporter.client, client) - self.assertEqual(exporter.project_id, self.project_id) - - def test_export(self): - trace_id = "6e0c63257de34c92bf9efcd03927272e" - span_id = "95bb5edabd45950f" - resource_info = Resource( - { - "cloud.account.id": 123, - "host.id": "host", - "cloud.zone": "US", - "cloud.provider": "gcp", - "gcp.resource_type": "gce_instance", - } - ) - span_datas = [ - Span( - name="span_name", - context=SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), - is_remote=False, - ), - parent=None, - kind=SpanKind.INTERNAL, - resource=resource_info, - attributes={"attr_key": "attr_value"}, - ) - ] - - cloud_trace_spans = { - "name": "projects/{}/traces/{}/spans/{}".format( - self.project_id, trace_id, span_id - ), - "span_id": span_id, - "parent_span_id": None, - "display_name": TruncatableString( - value="span_name", truncated_byte_count=0 - ), - "attributes": ProtoSpan.Attributes( - attribute_map={ - "g.co/r/gce_instance/zone": _format_attribute_value("US"), - "g.co/r/gce_instance/instance_id": _format_attribute_value( - "host" - ), - "g.co/r/gce_instance/project_id": _format_attribute_value( - "123" - ), - "g.co/agent": _format_attribute_value( - "opentelemetry-python {}; google-cloud-trace-exporter {}".format( - _strip_characters( - pkg_resources.get_distribution( - "opentelemetry-sdk" - ).version - ), - _strip_characters(cloud_trace_version), - ) - ), - "attr_key": _format_attribute_value("attr_value"), - } - ), - "links": None, - "status": None, - "time_events": None, - "start_time": None, - "end_time": None, - } - - client = mock.Mock() - - exporter = CloudTraceSpanExporter(self.project_id, client=client) - - exporter.export(span_datas) - - client.create_span.assert_called_with(**cloud_trace_spans) - self.assertTrue(client.create_span.called) - - def test_extract_status(self): - self.assertIsNone(_extract_status(None)) - self.assertEqual( - _extract_status(SpanStatus(canonical_code=StatusCanonicalCode.OK)), - Status(details=None, code=0), - ) - self.assertEqual( - _extract_status( - SpanStatus( - canonical_code=StatusCanonicalCode.UNKNOWN, - description="error_desc", - ) - ), - Status(details=None, code=2, message="error_desc"), - ) - - def test_extract_attributes(self): - self.assertEqual( - _extract_attributes({}, 4), ProtoSpan.Attributes(attribute_map={}) - ) - self.assertEqual( - _extract_attributes(self.attributes_variety_pack, 4), - self.extracted_attributes_variety_pack, - ) - # Test ignoring attributes with illegal value type - self.assertEqual( - _extract_attributes({"illegal_attribute_value": dict()}, 4), - ProtoSpan.Attributes(attribute_map={}, dropped_attributes_count=1), - ) - - too_many_attrs = {} - for attr_key in range(5): - too_many_attrs[str(attr_key)] = 0 - proto_attrs = _extract_attributes(too_many_attrs, 4) - self.assertEqual(proto_attrs.dropped_attributes_count, 1) - - def test_extract_events(self): - self.assertIsNone(_extract_events([])) - time_in_ns1 = 1589919268850900051 - time_in_ms_and_ns1 = {"seconds": 1589919268, "nanos": 850899968} - time_in_ns2 = 1589919438550020326 - time_in_ms_and_ns2 = {"seconds": 1589919438, "nanos": 550020352} - event1 = Event( - name="event1", - attributes=self.attributes_variety_pack, - timestamp=time_in_ns1, - ) - event2 = Event( - name="event2", - attributes={"illegal_attr_value": dict()}, - timestamp=time_in_ns2, - ) - self.assertEqual( - _extract_events([event1, event2]), - ProtoSpan.TimeEvents( - time_event=[ - { - "time": time_in_ms_and_ns1, - "annotation": { - "description": TruncatableString( - value="event1", truncated_byte_count=0 - ), - "attributes": self.extracted_attributes_variety_pack, - }, - }, - { - "time": time_in_ms_and_ns2, - "annotation": { - "description": TruncatableString( - value="event2", truncated_byte_count=0 - ), - "attributes": ProtoSpan.Attributes( - attribute_map={}, dropped_attributes_count=1 - ), - }, - }, - ] - ), - ) - - def test_extract_links(self): - self.assertIsNone(_extract_links([])) - trace_id = "6e0c63257de34c92bf9efcd03927272e" - span_id1 = "95bb5edabd45950f" - span_id2 = "b6b86ad2915c9ddc" - link1 = Link( - context=SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id1, 16), - is_remote=False, - ), - attributes={}, - ) - link2 = Link( - context=SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id1, 16), - is_remote=False, - ), - attributes=self.attributes_variety_pack, - ) - link3 = Link( - context=SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id2, 16), - is_remote=False, - ), - attributes={"illegal_attr_value": dict(), "int_attr_value": 123}, - ) - self.assertEqual( - _extract_links([link1, link2, link3]), - ProtoSpan.Links( - link=[ - { - "trace_id": trace_id, - "span_id": span_id1, - "type": "TYPE_UNSPECIFIED", - "attributes": ProtoSpan.Attributes(attribute_map={}), - }, - { - "trace_id": trace_id, - "span_id": span_id1, - "type": "TYPE_UNSPECIFIED", - "attributes": self.extracted_attributes_variety_pack, - }, - { - "trace_id": trace_id, - "span_id": span_id2, - "type": "TYPE_UNSPECIFIED", - "attributes": { - "attribute_map": { - "int_attr_value": AttributeValue(int_value=123) - }, - "dropped_attributes_count": 1, - }, - }, - ] - ), - ) - - def test_extract_empty_resources(self): - self.assertEqual(_extract_resources(Resource.create_empty()), {}) - - def test_extract_well_formed_resources(self): - resource = Resource( - labels={ - "cloud.account.id": 123, - "host.id": "host", - "cloud.zone": "US", - "cloud.provider": "gcp", - "extra_info": "extra", - "gcp.resource_type": "gce_instance", - "not_gcp_resource": "value", - } - ) - expected_extract = { - "g.co/r/gce_instance/project_id": "123", - "g.co/r/gce_instance/instance_id": "host", - "g.co/r/gce_instance/zone": "US", - } - self.assertEqual(_extract_resources(resource), expected_extract) - - def test_extract_malformed_resources(self): - # This resource doesn't have all the fields required for a gce_instance - # Specifically its missing "host.id", "cloud.zone", "cloud.account.id" - resource = Resource( - labels={ - "gcp.resource_type": "gce_instance", - "cloud.provider": "gcp", - } - ) - # Should throw when passed a malformed GCP resource dict - self.assertRaises(KeyError, _extract_resources, resource) - - def test_extract_unsupported_gcp_resources(self): - resource = Resource( - labels={ - "cloud.account.id": "123", - "host.id": "host", - "extra_info": "extra", - "not_gcp_resource": "value", - "gcp.resource_type": "unsupported_gcp_resource", - "cloud.provider": "gcp", - } - ) - self.assertEqual(_extract_resources(resource), {}) - - def test_extract_unsupported_provider_resources(self): - # Resources with currently unsupported providers will be ignored - resource = Resource( - labels={ - "cloud.account.id": "123", - "host.id": "host", - "extra_info": "extra", - "not_gcp_resource": "value", - "cloud.provider": "aws", - } - ) - self.assertEqual(_extract_resources(resource), {}) - - # pylint:disable=too-many-locals - def test_truncate(self): - """Cloud Trace API imposes limits on the length of many things, - e.g. strings, number of events, number of attributes. We truncate - these things before sending it to the API as an optimization. - """ - str_300 = "a" * 300 - str_256 = "a" * 256 - str_128 = "a" * 128 - self.assertEqual(_truncate_str("aaaa", 1), ("a", 3)) - self.assertEqual(_truncate_str("aaaa", 5), ("aaaa", 0)) - self.assertEqual(_truncate_str("aaaa", 4), ("aaaa", 0)) - self.assertEqual(_truncate_str("中文翻译", 4), ("中", 9)) - - self.assertEqual( - _format_attribute_value(str_300), - AttributeValue( - string_value=TruncatableString( - value=str_256, truncated_byte_count=300 - 256 - ) - ), - ) - - self.assertEqual( - _extract_attributes({str_300: str_300}, 4), - ProtoSpan.Attributes( - attribute_map={ - str_128: AttributeValue( - string_value=TruncatableString( - value=str_256, truncated_byte_count=300 - 256 - ) - ) - } - ), - ) - - time_in_ns1 = 1589919268850900051 - time_in_ms_and_ns1 = {"seconds": 1589919268, "nanos": 850899968} - event1 = Event(name=str_300, attributes={}, timestamp=time_in_ns1) - self.assertEqual( - _extract_events([event1]), - ProtoSpan.TimeEvents( - time_event=[ - { - "time": time_in_ms_and_ns1, - "annotation": { - "description": TruncatableString( - value=str_256, truncated_byte_count=300 - 256 - ), - "attributes": {}, - }, - }, - ] - ), - ) - - trace_id = "6e0c63257de34c92bf9efcd03927272e" - span_id = "95bb5edabd45950f" - link = Link( - context=SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), - is_remote=False, - ), - attributes={}, - ) - too_many_links = [link] * (MAX_NUM_LINKS + 1) - self.assertEqual( - _extract_links(too_many_links), - ProtoSpan.Links( - link=[ - { - "trace_id": trace_id, - "span_id": span_id, - "type": "TYPE_UNSPECIFIED", - "attributes": {}, - } - ] - * MAX_NUM_LINKS, - dropped_links_count=len(too_many_links) - MAX_NUM_LINKS, - ), - ) - - link_attrs = {} - for attr_key in range(MAX_LINK_ATTRS + 1): - link_attrs[str(attr_key)] = 0 - attr_link = Link( - context=SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), - is_remote=False, - ), - attributes=link_attrs, - ) - - proto_link = _extract_links([attr_link]) - self.assertEqual( - len(proto_link.link[0].attributes.attribute_map), MAX_LINK_ATTRS - ) - - too_many_events = [event1] * (MAX_NUM_EVENTS + 1) - self.assertEqual( - _extract_events(too_many_events), - ProtoSpan.TimeEvents( - time_event=[ - { - "time": time_in_ms_and_ns1, - "annotation": { - "description": TruncatableString( - value=str_256, truncated_byte_count=300 - 256 - ), - "attributes": {}, - }, - }, - ] - * MAX_NUM_EVENTS, - dropped_annotations_count=len(too_many_events) - - MAX_NUM_EVENTS, - ), - ) - - time_in_ns1 = 1589919268850900051 - event_attrs = {} - for attr_key in range(MAX_EVENT_ATTRS + 1): - event_attrs[str(attr_key)] = 0 - proto_events = _extract_events( - [Event(name="a", attributes=event_attrs, timestamp=time_in_ns1)] - ) - self.assertEqual( - len( - proto_events.time_event[0].annotation.attributes.attribute_map - ), - MAX_EVENT_ATTRS, - ) - - def test_strip_characters(self): - self.assertEqual("0.10.0", _strip_characters("0.10.0b")) - self.assertEqual("1.20.5", _strip_characters("1.20.5")) - self.assertEqual("3.1.0", _strip_characters("3.1.0beta")) - self.assertEqual("4.2.0", _strip_characters("4b.2rc.0a")) - self.assertEqual("6.20.15", _strip_characters("b6.20.15")) diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_propagator.py b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_propagator.py deleted file mode 100644 index a94127b7b7..0000000000 --- a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_propagator.py +++ /dev/null @@ -1,238 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -import typing -import unittest - -import opentelemetry.trace as trace -from opentelemetry.context import get_current -from opentelemetry.exporter.cloud_trace.cloud_trace_propagator import ( - _TRACE_CONTEXT_HEADER_NAME, - CloudTraceFormatPropagator, -) -from opentelemetry.trace.span import ( - INVALID_SPAN_ID, - INVALID_TRACE_ID, - SpanContext, - TraceFlags, - get_hexadecimal_trace_id, -) - - -def get_dict_value(dict_object: typing.Dict[str, str], key: str) -> str: - return dict_object.get(key, "") - - -class TestCloudTraceFormatPropagator(unittest.TestCase): - def setUp(self): - self.propagator = CloudTraceFormatPropagator() - self.valid_trace_id = 281017822499060589596062859815111849546 - self.valid_span_id = 17725314949316355921 - self.too_long_id = 111111111111111111111111111111111111111111111 - - def _extract(self, header_value): - """Test helper""" - header = {_TRACE_CONTEXT_HEADER_NAME: [header_value]} - new_context = self.propagator.extract(get_dict_value, header) - return trace.get_current_span(new_context).get_context() - - def _inject(self, span=None): - """Test helper""" - ctx = get_current() - if span is not None: - ctx = trace.set_span_in_context(span, ctx) - output = {} - self.propagator.inject(dict.__setitem__, output, context=ctx) - return output.get(_TRACE_CONTEXT_HEADER_NAME) - - def test_no_context_header(self): - header = {} - new_context = self.propagator.extract(get_dict_value, header) - self.assertEqual( - trace.get_current_span(new_context).get_context(), - trace.INVALID_SPAN.get_context(), - ) - - def test_empty_context_header(self): - header = "" - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - def test_valid_header(self): - header = "{}/{};o=1".format( - get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id - ) - new_span_context = self._extract(header) - self.assertEqual(new_span_context.trace_id, self.valid_trace_id) - self.assertEqual(new_span_context.span_id, self.valid_span_id) - self.assertEqual(new_span_context.trace_flags, TraceFlags(1)) - self.assertTrue(new_span_context.is_remote) - - header = "{}/{};o=10".format( - get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id - ) - new_span_context = self._extract(header) - self.assertEqual(new_span_context.trace_id, self.valid_trace_id) - self.assertEqual(new_span_context.span_id, self.valid_span_id) - self.assertEqual(new_span_context.trace_flags, TraceFlags(10)) - self.assertTrue(new_span_context.is_remote) - - header = "{}/{};o=0".format( - get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id - ) - new_span_context = self._extract(header) - self.assertEqual(new_span_context.trace_id, self.valid_trace_id) - self.assertEqual(new_span_context.span_id, self.valid_span_id) - self.assertEqual(new_span_context.trace_flags, TraceFlags(0)) - self.assertTrue(new_span_context.is_remote) - - header = "{}/{};o=0".format( - get_hexadecimal_trace_id(self.valid_trace_id), 345 - ) - new_span_context = self._extract(header) - self.assertEqual(new_span_context.trace_id, self.valid_trace_id) - self.assertEqual(new_span_context.span_id, 345) - self.assertEqual(new_span_context.trace_flags, TraceFlags(0)) - self.assertTrue(new_span_context.is_remote) - - def test_invalid_header_format(self): - header = "invalid_header" - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - header = "{}/{};o=".format( - get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id - ) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - header = "extra_chars/{}/{};o=1".format( - get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id - ) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - header = "{}/{}extra_chars;o=1".format( - get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id - ) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - header = "{}/{};o=1extra_chars".format( - get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id - ) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - header = "{}/;o=1".format( - get_hexadecimal_trace_id(self.valid_trace_id) - ) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - header = "/{};o=1".format(self.valid_span_id) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - header = "{}/{};o={}".format("123", "34", "4") - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - def test_invalid_trace_id(self): - header = "{}/{};o={}".format(INVALID_TRACE_ID, self.valid_span_id, 1) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - header = "{}/{};o={}".format("0" * 32, self.valid_span_id, 1) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - header = "0/{};o={}".format(self.valid_span_id, 1) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - header = "234/{};o={}".format(self.valid_span_id, 1) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - header = "{}/{};o={}".format(self.too_long_id, self.valid_span_id, 1) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - def test_invalid_span_id(self): - header = "{}/{};o={}".format( - get_hexadecimal_trace_id(self.valid_trace_id), INVALID_SPAN_ID, 1 - ) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - header = "{}/{};o={}".format( - get_hexadecimal_trace_id(self.valid_trace_id), "0" * 16, 1 - ) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - header = "{}/{};o={}".format( - get_hexadecimal_trace_id(self.valid_trace_id), "0", 1 - ) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - header = "{}/{};o={}".format( - get_hexadecimal_trace_id(self.valid_trace_id), self.too_long_id, 1 - ) - self.assertEqual( - self._extract(header), trace.INVALID_SPAN.get_context() - ) - - def test_inject_with_no_context(self): - output = self._inject() - self.assertIsNone(output) - - def test_inject_with_invalid_context(self): - output = self._inject(trace.INVALID_SPAN) - self.assertIsNone(output) - - def test_inject_with_valid_context(self): - span_context = SpanContext( - trace_id=self.valid_trace_id, - span_id=self.valid_span_id, - is_remote=True, - trace_flags=TraceFlags(1), - ) - output = self._inject(trace.DefaultSpan(span_context)) - self.assertEqual( - output, - "{}/{};o={}".format( - get_hexadecimal_trace_id(self.valid_trace_id), - self.valid_span_id, - 1, - ), - ) diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/test_gcp_resource_detector.py b/ext/opentelemetry-exporter-cloud-trace/tests/test_gcp_resource_detector.py deleted file mode 100644 index de618acd61..0000000000 --- a/ext/opentelemetry-exporter-cloud-trace/tests/test_gcp_resource_detector.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -from unittest import mock - -from opentelemetry.sdk.resources import Resource -from opentelemetry.tools.resource_detector import ( - _GCP_METADATA_URL, - GoogleCloudResourceDetector, - get_gce_resources, -) - -RESOURCES_JSON_STRING = { - "instance": {"id": "instance_id", "zone": "projects/123/zones/zone"}, - "project": {"projectId": "project_id"}, -} - - -class TestGCEResourceFinder(unittest.TestCase): - @mock.patch("opentelemetry.tools.resource_detector.requests.get") - def test_finding_gce_resources(self, getter): - getter.return_value.json.return_value = RESOURCES_JSON_STRING - found_resources = get_gce_resources() - self.assertEqual(getter.call_args_list[0][0][0], _GCP_METADATA_URL) - self.assertEqual( - found_resources, - { - "host.id": "instance_id", - "cloud.provider": "gcp", - "cloud.account.id": "project_id", - "cloud.zone": "zone", - "gcp.resource_type": "gce_instance", - }, - ) - - -class TestGoogleCloudResourceDetector(unittest.TestCase): - @mock.patch("opentelemetry.tools.resource_detector.requests.get") - def test_finding_resources(self, getter): - resource_finder = GoogleCloudResourceDetector() - getter.return_value.json.return_value = RESOURCES_JSON_STRING - found_resources = resource_finder.detect() - self.assertEqual(getter.call_args_list[0][0][0], _GCP_METADATA_URL) - self.assertEqual( - found_resources, - Resource( - labels={ - "host.id": "instance_id", - "cloud.provider": "gcp", - "cloud.account.id": "project_id", - "cloud.zone": "zone", - "gcp.resource_type": "gce_instance", - } - ), - ) - self.assertEqual(getter.call_count, 1) - - # Found resources should be cached and not require another network call - found_resources = resource_finder.detect() - self.assertEqual(getter.call_count, 1) - self.assertEqual( - found_resources, - Resource( - labels={ - "host.id": "instance_id", - "cloud.provider": "gcp", - "cloud.account.id": "project_id", - "cloud.zone": "zone", - "gcp.resource_type": "gce_instance", - } - ), - ) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index a88aca7475..0b45fbf643 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -36,8 +36,6 @@ cov ext/opentelemetry-ext-flask cov ext/opentelemetry-ext-requests cov ext/opentelemetry-ext-jaeger cov ext/opentelemetry-ext-opentracing-shim -cov ext/opentelemetry-exporter-cloud-trace -cov ext/opentelemetry-exporter-cloud-monitoring cov ext/opentelemetry-ext-wsgi cov ext/opentelemetry-ext-zipkin cov docs/examples/opentelemetry-example-app diff --git a/tox.ini b/tox.ini index 2c87f69246..ad7b8ad3e9 100644 --- a/tox.ini +++ b/tox.ini @@ -161,15 +161,6 @@ envlist = ; ext-system-metrics intentionally excluded from pypy3 ; known limitation: gc.get_count won't work under pypy - ; Coverage is temporarily disabled for pypy3 due to the pytest bug. - ; pypy3-coverage - - ; opentelemetry-exporter-cloud-monitoring - py3{4,5,6,7,8}-test-exporter-cloud-monitoring - - ; opentelemetry-exporter-cloud-trace - py3{4,5,6,7,8}-test-exporter-cloud-trace - lint py38-tracecontext py38-{mypy,mypyinstalled} @@ -235,8 +226,6 @@ changedir = test-exporter-opencensusexporter: ext/opentelemetry-ext-opencensusexporter/tests test-exporter-otlp: ext/opentelemetry-ext-otlp/tests test-exporter-prometheus: ext/opentelemetry-ext-prometheus/tests - test-exporter-cloud-trace: ext/opentelemetry-exporter-cloud-trace/tests - test-exporter-cloud-monitoring: ext/opentelemetry-exporter-cloud-monitoring/tests test-exporter-zipkin: ext/opentelemetry-ext-zipkin/tests commands_pre = @@ -319,8 +308,6 @@ commands_pre = system-metrics: pip install {toxinidir}/ext/opentelemetry-ext-system-metrics[test] - exporter-cloud-monitoring: pip install {toxinidir}/ext/opentelemetry-exporter-cloud-monitoring - exporter-cloud-trace: pip install {toxinidir}/ext/opentelemetry-exporter-cloud-trace elasticsearch{2,5,6,7}: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/ext/opentelemetry-ext-elasticsearch[test] ; In order to get a healthy coverage report, From a818cc602d39b8537dda256d90576f0517e668d6 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 20 Jul 2020 08:47:10 -0700 Subject: [PATCH 0466/1517] one more inconsistent test to skip (#919) --- .../tests/celery/test_celery_functional.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py b/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py index 03fadb09ae..a3967a5612 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py @@ -178,6 +178,7 @@ def fn_task_parameters(user, force_logout=False): ) +@pytest.mark.skip(reason="inconsistent test results") def test_concurrent_delays(celery_app, memory_exporter): @celery_app.task def fn_task(): From ce5cb96efc0da1a6ac3488109a9421575357f429 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 20 Jul 2020 09:16:34 -0700 Subject: [PATCH 0467/1517] chore: finish migrating to circleci (#920) Co-authored-by: Leighton Chen --- .travis.yml | 37 ------------------------------------- README.md | 4 ++-- 2 files changed, 2 insertions(+), 39 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fda383970a..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,37 +0,0 @@ -dist: xenial - -language: python - -cache: pip - -matrix: - include: - - python: 3.8 - env: TOXENV=lint - - python: pypy3 - - python: 3.8 - - python: 3.4 - - python: 3.5 - - python: 3.6 - - python: 3.7 - - python: 3.8 - env: TOXENV=docker-tests - - python: 3.8 - env: TOXENV=docs - -#matrix: -# allow_failures: -# - python: '3.8-dev' - -services: - - docker - -install: - - pip install tox-travis - -script: - - tox - -after_success: - - pip install codecov - - codecov -v diff --git a/README.md b/README.md index 47e8189399..c4e335589d 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ license
- - Build Status + + Build Status Beta

From 69740b1c61365fcd79b7ac4e60eb5d3514b51d89 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 20 Jul 2020 09:40:40 -0700 Subject: [PATCH 0468/1517] chore: updating circle ci to add tracecontext and mypy jobs (#921) * chore: updating circle ci to add tracecontext and mypy jobs --- .circleci/config.yml | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f47690257c..bebfc4dbc1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -64,11 +64,8 @@ jobs: docs: executor: py38 steps: - - checkout - - setup_tox - - restore_tox_cache - - run: tox -e docs - - save_tox_cache + - run_tox_scenario: + pattern: docs docker-tests: machine: @@ -92,11 +89,17 @@ jobs: lint: executor: py38 steps: - - checkout - - setup_tox - - restore_tox_cache - - run: tox -e lint - - save_tox_cache + - run_tox_scenario: + pattern: lint + + build-py38: + parameters: + package: + type: string + executor: py38 + steps: + - run_tox_scenario: + pattern: py38-<< parameters.package >> build-py34: parameters: @@ -131,6 +134,10 @@ jobs: workflows: circleci-build: jobs: + - build-py38: + matrix: + parameters: + package: ["core", "exporter", "instrumentation", "tracecontext", "mypy", "mypyinstalled"] - build: matrix: parameters: From e9527dac5d8cb028c2a06c4b083558166e166352 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 21 Jul 2020 17:37:50 -0600 Subject: [PATCH 0469/1517] ext/opentracing: Implement get_attribute and set_attribute (#912) --- .../ext/opentracing_shim/__init__.py | 67 ++++++++----------- .../tests/test_shim.py | 36 ++++++++-- 2 files changed, 56 insertions(+), 47 deletions(-) diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index 0b948216ff..8d8afca5ca 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -90,11 +90,18 @@ import opentracing from deprecated import deprecated -import opentelemetry.trace as trace_api from opentelemetry import propagators +from opentelemetry.context import Context +from opentelemetry.correlationcontext import get_correlation, set_correlation from opentelemetry.ext.opentracing_shim import util from opentelemetry.ext.opentracing_shim.version import __version__ -from opentelemetry.trace import DefaultSpan, set_span_in_context +from opentelemetry.trace import ( + INVALID_SPAN_CONTEXT, + DefaultSpan, + Link, + get_current_span, + set_span_in_context, +) logger = logging.getLogger(__name__) @@ -130,6 +137,8 @@ class SpanContextShim(opentracing.SpanContext): def __init__(self, otel_context): self._otel_context = otel_context + # Context is being used here since it must be immutable. + self._baggage = Context() def unwrap(self): """Returns the wrapped :class:`opentelemetry.trace.SpanContext` @@ -144,17 +153,9 @@ def unwrap(self): @property def baggage(self): - """Implements the ``baggage`` property from the base class. + """Implements the ``baggage`` property from the base class.""" - Warning: - Not implemented yet. - """ - - logger.warning( - "Using unimplemented property baggage on class %s.", - self.__class__.__name__, - ) - # TODO: Implement. + return self._baggage class SpanShim(opentracing.Span): @@ -270,31 +271,17 @@ def log(self, **kwargs): def log_event(self, event, payload=None): super().log_event(event, payload=payload) - def set_baggage_item(self, key, value): # pylint:disable=unused-argument - """Implements the ``set_baggage_item()`` method from the base class. - - Warning: - Not implemented yet. - """ - - logger.warning( - "Calling unimplemented method set_baggage_item() on class %s", - self.__class__.__name__, + def set_baggage_item(self, key, value): + """Implements the ``set_baggage_item`` method from the base class.""" + # pylint: disable=protected-access + self._context._baggage = set_correlation( + key, value, context=self._context._baggage ) - # TODO: Implement. - - def get_baggage_item(self, key): # pylint:disable=unused-argument - """Implements the ``get_baggage_item()`` method from the base class. - - Warning: - Not implemented yet. - """ - logger.warning( - "Calling unimplemented method get_baggage_item() on class %s", - self.__class__.__name__, - ) - # TODO: Implement. + def get_baggage_item(self, key): + """Implements the ``get_baggage_item`` method from the base class.""" + # pylint: disable=protected-access + return get_correlation(key, context=self._context._baggage) class ScopeShim(opentracing.Scope): @@ -469,8 +456,8 @@ def active(self): shim and is likely to be handled in future versions. """ - span = trace_api.get_current_span() - if span.get_context() == trace_api.INVALID_SPAN_CONTEXT: + span = get_current_span() + if span.get_context() == INVALID_SPAN_CONTEXT: return None span_context = SpanContextShim(span.get_context()) @@ -643,7 +630,7 @@ def start_span( links = [] if references: for ref in references: - links.append(trace_api.Link(ref.referenced_context.unwrap())) + links.append(Link(ref.referenced_context.unwrap())) # The OpenTracing API expects time values to be `float` values which # represent the number of seconds since the epoch. OpenTelemetry @@ -699,10 +686,10 @@ def get_as_list(dict_object, key): propagator = propagators.get_global_httptextformat() ctx = propagator.extract(get_as_list, carrier) - span = trace_api.get_current_span(ctx) + span = get_current_span(ctx) if span is not None: otel_context = span.get_context() else: - otel_context = trace_api.INVALID_SPAN_CONTEXT + otel_context = INVALID_SPAN_CONTEXT return SpanContextShim(otel_context) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index 941d428069..635907bc90 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -17,12 +17,17 @@ import time from unittest import TestCase +from unittest.mock import Mock import opentracing -import opentelemetry.ext.opentracing_shim as opentracingshim from opentelemetry import propagators, trace -from opentelemetry.ext.opentracing_shim import util +from opentelemetry.ext.opentracing_shim import ( + SpanContextShim, + SpanShim, + create_tracer, + util, +) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.test.mock_httptextformat import ( MockHTTPTextFormat, @@ -36,7 +41,7 @@ class TestShim(TestCase): def setUp(self): """Create an OpenTelemetry tracer and a shim before every test case.""" trace.set_tracer_provider(TracerProvider()) - self.shim = opentracingshim.create_tracer(trace.get_tracer_provider()) + self.shim = create_tracer(trace.get_tracer_provider()) @classmethod def setUpClass(cls): @@ -448,7 +453,7 @@ def test_span_context(self): """Test construction of `SpanContextShim` objects.""" otel_context = trace.SpanContext(1234, 5678, is_remote=False) - context = opentracingshim.SpanContextShim(otel_context) + context = SpanContextShim(otel_context) self.assertIsInstance(context, opentracing.SpanContext) self.assertEqual(context.unwrap().trace_id, 1234) @@ -473,7 +478,7 @@ def test_inject_http_headers(self): otel_context = trace.SpanContext( trace_id=1220, span_id=7478, is_remote=False ) - context = opentracingshim.SpanContextShim(otel_context) + context = SpanContextShim(otel_context) headers = {} self.shim.inject(context, opentracing.Format.HTTP_HEADERS, headers) @@ -486,7 +491,7 @@ def test_inject_text_map(self): otel_context = trace.SpanContext( trace_id=1220, span_id=7478, is_remote=False ) - context = opentracingshim.SpanContextShim(otel_context) + context = SpanContextShim(otel_context) # Verify Format.TEXT_MAP text_map = {} @@ -500,7 +505,7 @@ def test_inject_binary(self): otel_context = trace.SpanContext( trace_id=1220, span_id=7478, is_remote=False ) - context = opentracingshim.SpanContextShim(otel_context) + context = SpanContextShim(otel_context) # Verify exception for non supported binary format. with self.assertRaises(opentracing.UnsupportedFormatException): @@ -550,3 +555,20 @@ def test_extract_binary(self): # Verify exception for non supported binary format. with self.assertRaises(opentracing.UnsupportedFormatException): self.shim.extract(opentracing.Format.BINARY, bytearray()) + + def test_baggage(self): + + span_context_shim = SpanContextShim( + trace.SpanContext(1234, 5678, is_remote=False) + ) + + baggage = span_context_shim.baggage + + with self.assertRaises(ValueError): + baggage[1] = 3 + + span_shim = SpanShim(Mock(), span_context_shim, Mock()) + + span_shim.set_baggage_item(1, 2) + + self.assertTrue(span_shim.get_baggage_item(1), 2) From 5ff9600a62f9c82fc120b03e5729a53341465626 Mon Sep 17 00:00:00 2001 From: Aravin <34178459+aravinsiva@users.noreply.github.com> Date: Wed, 22 Jul 2020 12:27:56 -0400 Subject: [PATCH 0470/1517] Adding gRPC instrumentor (#788) --- ext/opentelemetry-ext-grpc/CHANGELOG.md | 2 + ext/opentelemetry-ext-grpc/setup.cfg | 5 + .../src/opentelemetry/ext/grpc/__init__.py | 141 +++++++++++++++++- .../tests/test_server_interceptor.py | 58 ++++++- 4 files changed, 203 insertions(+), 3 deletions(-) diff --git a/ext/opentelemetry-ext-grpc/CHANGELOG.md b/ext/opentelemetry-ext-grpc/CHANGELOG.md index 3be32e05e5..4221ab5371 100644 --- a/ext/opentelemetry-ext-grpc/CHANGELOG.md +++ b/ext/opentelemetry-ext-grpc/CHANGELOG.md @@ -4,6 +4,8 @@ - Add status code to gRPC client spans ([896](https://github.com/open-telemetry/opentelemetry-python/pull/896)) +- Add gRPC client and server instrumentors + ([788](https://github.com/open-telemetry/opentelemetry-python/pull/788)) ## 0.8b0 diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index 2721a3b257..dd29603247 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -51,3 +51,8 @@ test = [options.packages.find] where = src + +[options.entry_points] +opentelemetry_instrumentor = + grpc_client = opentelemetry.ext.grpc:GrpcInstrumentorClient + grpc_server = opentelemetry.ext.grpc:GrpcInstrumentorServer diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py index 0e9b19ef51..368ae55f2e 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py @@ -12,13 +12,150 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint:disable=import-outside-toplevel -# pylint:disable=import-self # pylint:disable=no-name-in-module # pylint:disable=relative-beyond-top-level +# pylint:disable=import-error +# pylint:disable=no-self-use +""" +Usage Client +------------ +.. code-block:: python + + import logging + + import grpc + + from opentelemetry import trace + from opentelemetry.ext.grpc import GrpcInstrumentorClient, client_interceptor + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, + ) + + try: + from .gen import helloworld_pb2, helloworld_pb2_grpc + except ImportError: + from gen import helloworld_pb2, helloworld_pb2_grpc + + trace.set_tracer_provider(TracerProvider()) + trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) + ) + instrumentor = GrpcInstrumentorClient() + instrumentor.instrument() + + def run(): + with grpc.insecure_channel("localhost:50051") as channel: + + stub = helloworld_pb2_grpc.GreeterStub(channel) + response = stub.SayHello(helloworld_pb2.HelloRequest(name="YOU")) + + print("Greeter client received: " + response.message) + + + if __name__ == "__main__": + logging.basicConfig() + run() + +Usage Server +------------ +.. code-block:: python + + import logging + from concurrent import futures + + import grpc + + from opentelemetry import trace + from opentelemetry.ext.grpc import GrpcInstrumentorServer, server_interceptor + from opentelemetry.ext.grpc.grpcext import intercept_server + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, + ) + + try: + from .gen import helloworld_pb2, helloworld_pb2_grpc + except ImportError: + from gen import helloworld_pb2, helloworld_pb2_grpc + + trace.set_tracer_provider(TracerProvider()) + trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) + ) + grpc_server_instrumentor = GrpcInstrumentorServer() + grpc_server_instrumentor.instrument() + + + class Greeter(helloworld_pb2_grpc.GreeterServicer): + def SayHello(self, request, context): + return helloworld_pb2.HelloReply(message="Hello, %s!" % request.name) + + + def serve(): + + server = grpc.server(futures.ThreadPoolExecutor()) + + helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server) + server.add_insecure_port("[::]:50051") + server.start() + server.wait_for_termination() + + + if __name__ == "__main__": + logging.basicConfig() + serve() +""" +from contextlib import contextmanager + +import grpc +from wrapt import wrap_function_wrapper as _wrap from opentelemetry import trace +from opentelemetry.ext.grpc.grpcext import intercept_channel, intercept_server from opentelemetry.ext.grpc.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap + +# pylint:disable=import-outside-toplevel +# pylint:disable=import-self +# pylint:disable=unused-argument +# isort:skip + + +class GrpcInstrumentorServer(BaseInstrumentor): + def _instrument(self, **kwargs): + _wrap("grpc", "server", self.wrapper_fn) + + def _uninstrument(self, **kwargs): + unwrap(grpc, "server") + + def wrapper_fn(self, original_func, instance, args, kwargs): + server = original_func(*args, **kwargs) + return intercept_server(server, server_interceptor()) + + +class GrpcInstrumentorClient(BaseInstrumentor): + def _instrument(self, **kwargs): + if kwargs.get("channel_type") == "secure": + _wrap("grpc", "secure_channel", self.wrapper_fn) + + else: + _wrap("grpc", "insecure_channel", self.wrapper_fn) + + def _uninstrument(self, **kwargs): + if kwargs.get("channel_type") == "secure": + unwrap(grpc, "secure_channel") + + else: + unwrap(grpc, "insecure_channel") + + @contextmanager + def wrapper_fn(self, original_func, instance, args, kwargs): + with original_func(*args, **kwargs) as channel: + yield intercept_channel(channel, client_interceptor()) def client_interceptor(tracer_provider=None): diff --git a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py index ebe2a8c160..0ba57a4322 100644 --- a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py +++ b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py @@ -22,7 +22,7 @@ import opentelemetry.ext.grpc from opentelemetry import trace -from opentelemetry.ext.grpc import server_interceptor +from opentelemetry.ext.grpc import GrpcInstrumentorServer, server_interceptor from opentelemetry.ext.grpc.grpcext import intercept_server from opentelemetry.sdk import trace as trace_sdk from opentelemetry.test.test_base import TestBase @@ -49,6 +49,62 @@ def service(self, handler_call_details): class TestOpenTelemetryServerInterceptor(TestBase): + def test_instrumentor(self): + def handler(request, context): + return b"" + + grpc_server_instrumentor = GrpcInstrumentorServer() + grpc_server_instrumentor.instrument() + server = grpc.server( + futures.ThreadPoolExecutor(max_workers=1), + options=(("grpc.so_reuseport", 0),), + ) + + server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) + + port = server.add_insecure_port("[::]:0") + channel = grpc.insecure_channel("localhost:{:d}".format(port)) + + try: + server.start() + channel.unary_unary("test")(b"test") + finally: + server.stop(None) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "test") + self.assertIs(span.kind, trace.SpanKind.SERVER) + self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + grpc_server_instrumentor.uninstrument() + + def test_uninstrument(self): + def handler(request, context): + return b"" + + grpc_server_instrumentor = GrpcInstrumentorServer() + grpc_server_instrumentor.instrument() + grpc_server_instrumentor.uninstrument() + server = grpc.server( + futures.ThreadPoolExecutor(max_workers=1), + options=(("grpc.so_reuseport", 0),), + ) + + server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) + + port = server.add_insecure_port("[::]:0") + channel = grpc.insecure_channel("localhost:{:d}".format(port)) + + try: + server.start() + channel.unary_unary("test")(b"test") + finally: + server.stop(None) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) + def test_create_span(self): """Check that the interceptor wraps calls with spans server-side.""" From f2c6c8579453ddcb30ed4b47dddda1c4971e2e26 Mon Sep 17 00:00:00 2001 From: sartx Date: Fri, 24 Jul 2020 20:35:38 +0500 Subject: [PATCH 0471/1517] ext/aiopg: Add instrumentation for aiopg (#801) Co-authored-by: Leighton Chen Co-authored-by: Alex Boten --- docs-requirements.txt | 1 + docs/ext/aiopg/aiopg.rst | 7 + .../src/opentelemetry/ext/dbapi/__init__.py | 43 +- .../tests/postgres/test_aiopg_functional.py | 200 ++++++++ .../CHANGELOG.md | 5 + .../LICENSE | 201 ++++++++ .../MANIFEST.in | 9 + .../README.rst | 21 + .../setup.cfg | 58 +++ .../setup.py | 26 + .../instrumentation/aiopg/__init__.py | 121 +++++ .../aiopg/aiopg_integration.py | 144 ++++++ .../instrumentation/aiopg/version.py | 15 + .../instrumentation/aiopg/wrappers.py | 223 +++++++++ .../tests/__init__.py | 0 .../tests/test_aiopg_integration.py | 472 ++++++++++++++++++ tox.ini | 11 +- 17 files changed, 1535 insertions(+), 22 deletions(-) create mode 100644 docs/ext/aiopg/aiopg.rst create mode 100644 ext/opentelemetry-ext-docker-tests/tests/postgres/test_aiopg_functional.py create mode 100644 ext/opentelemetry-instrumentation-aiopg/CHANGELOG.md create mode 100644 ext/opentelemetry-instrumentation-aiopg/LICENSE create mode 100644 ext/opentelemetry-instrumentation-aiopg/MANIFEST.in create mode 100644 ext/opentelemetry-instrumentation-aiopg/README.rst create mode 100644 ext/opentelemetry-instrumentation-aiopg/setup.cfg create mode 100644 ext/opentelemetry-instrumentation-aiopg/setup.py create mode 100644 ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py create mode 100644 ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py create mode 100644 ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py create mode 100644 ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py create mode 100644 ext/opentelemetry-instrumentation-aiopg/tests/__init__.py create mode 100644 ext/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py diff --git a/docs-requirements.txt b/docs-requirements.txt index 0fa078ef23..10ecacb2fd 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -7,6 +7,7 @@ asgiref~=3.0 asyncpg>=0.12.0 ddtrace>=0.34.0 aiohttp~= 3.0 +aiopg>=0.13.0 Deprecated>=1.2.6 django>=2.2 PyMySQL~=0.9.3 diff --git a/docs/ext/aiopg/aiopg.rst b/docs/ext/aiopg/aiopg.rst new file mode 100644 index 0000000000..ff8d91ed11 --- /dev/null +++ b/docs/ext/aiopg/aiopg.rst @@ -0,0 +1,7 @@ +OpenTelemetry aiopg instrumentation +=================================== + +.. automodule:: opentelemetry.instrumentation.aiopg + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index b34e41b2b4..3838dc1b15 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -46,6 +46,7 @@ import wrapt +from opentelemetry import trace as trace_api from opentelemetry.ext.dbapi.version import __version__ from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import SpanKind, TracerProvider, get_tracer @@ -300,6 +301,26 @@ class TracedCursor: def __init__(self, db_api_integration: DatabaseApiIntegration): self._db_api_integration = db_api_integration + def _populate_span( + self, span: trace_api.Span, *args: typing.Tuple[typing.Any, typing.Any] + ): + statement = args[0] if args else "" + span.set_attribute( + "component", self._db_api_integration.database_component + ) + span.set_attribute("db.type", self._db_api_integration.database_type) + span.set_attribute("db.instance", self._db_api_integration.database) + span.set_attribute("db.statement", statement) + + for ( + attribute_key, + attribute_value, + ) in self._db_api_integration.span_attributes.items(): + span.set_attribute(attribute_key, attribute_value) + + if len(args) > 1: + span.set_attribute("db.statement.parameters", str(args[1])) + def traced_execution( self, query_method: typing.Callable[..., typing.Any], @@ -307,30 +328,10 @@ def traced_execution( **kwargs: typing.Dict[typing.Any, typing.Any] ): - statement = args[0] if args else "" with self._db_api_integration.get_tracer().start_as_current_span( self._db_api_integration.name, kind=SpanKind.CLIENT ) as span: - span.set_attribute( - "component", self._db_api_integration.database_component - ) - span.set_attribute( - "db.type", self._db_api_integration.database_type - ) - span.set_attribute( - "db.instance", self._db_api_integration.database - ) - span.set_attribute("db.statement", statement) - - for ( - attribute_key, - attribute_value, - ) in self._db_api_integration.span_attributes.items(): - span.set_attribute(attribute_key, attribute_value) - - if len(args) > 1: - span.set_attribute("db.statement.parameters", str(args[1])) - + self._populate_span(span, *args) try: result = query_method(*args, **kwargs) span.set_status(Status(StatusCanonicalCode.OK)) diff --git a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_aiopg_functional.py b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_aiopg_functional.py new file mode 100644 index 0000000000..1762da1d09 --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_aiopg_functional.py @@ -0,0 +1,200 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +import os +import time + +import aiopg +import psycopg2 +import pytest + +from opentelemetry import trace as trace_api +from opentelemetry.instrumentation.aiopg import AiopgInstrumentor +from opentelemetry.test.test_base import TestBase + +POSTGRES_HOST = os.getenv("POSTGRESQL_HOST ", "localhost") +POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT ", "5432")) +POSTGRES_DB_NAME = os.getenv("POSTGRESQL_DB_NAME ", "opentelemetry-tests") +POSTGRES_PASSWORD = os.getenv("POSTGRESQL_HOST ", "testpassword") +POSTGRES_USER = os.getenv("POSTGRESQL_HOST ", "testuser") + + +def async_call(coro): + loop = asyncio.get_event_loop() + return loop.run_until_complete(coro) + + +class TestFunctionalAiopgConnect(TestBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._connection = None + cls._cursor = None + cls._tracer = cls.tracer_provider.get_tracer(__name__) + AiopgInstrumentor().instrument(tracer_provider=cls.tracer_provider) + cls._connection = async_call( + aiopg.connect( + dbname=POSTGRES_DB_NAME, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD, + host=POSTGRES_HOST, + port=POSTGRES_PORT, + ) + ) + cls._cursor = async_call(cls._connection.cursor()) + + @classmethod + def tearDownClass(cls): + if cls._cursor: + cls._cursor.close() + if cls._connection: + cls._connection.close() + AiopgInstrumentor().uninstrument() + + def validate_spans(self): + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + for span in spans: + if span.name == "rootSpan": + root_span = span + else: + child_span = span + self.assertIsInstance(span.start_time, int) + self.assertIsInstance(span.end_time, int) + self.assertIsNotNone(root_span) + self.assertIsNotNone(child_span) + self.assertEqual(root_span.name, "rootSpan") + self.assertEqual(child_span.name, "postgresql.opentelemetry-tests") + self.assertIsNotNone(child_span.parent) + self.assertIs(child_span.parent, root_span.get_context()) + self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual( + child_span.attributes["db.instance"], POSTGRES_DB_NAME + ) + self.assertEqual(child_span.attributes["net.peer.name"], POSTGRES_HOST) + self.assertEqual(child_span.attributes["net.peer.port"], POSTGRES_PORT) + + def test_execute(self): + """Should create a child span for execute method + """ + with self._tracer.start_as_current_span("rootSpan"): + async_call( + self._cursor.execute( + "CREATE TABLE IF NOT EXISTS test (id integer)" + ) + ) + self.validate_spans() + + def test_executemany(self): + """Should create a child span for executemany + """ + with pytest.raises(psycopg2.ProgrammingError): + with self._tracer.start_as_current_span("rootSpan"): + data = (("1",), ("2",), ("3",)) + stmt = "INSERT INTO test (id) VALUES (%s)" + async_call(self._cursor.executemany(stmt, data)) + self.validate_spans() + + def test_callproc(self): + """Should create a child span for callproc + """ + with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( + Exception + ): + async_call(self._cursor.callproc("test", ())) + self.validate_spans() + + +class TestFunctionalAiopgCreatePool(TestBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._connection = None + cls._cursor = None + cls._tracer = cls.tracer_provider.get_tracer(__name__) + AiopgInstrumentor().instrument(tracer_provider=cls.tracer_provider) + cls._pool = async_call( + aiopg.create_pool( + dbname=POSTGRES_DB_NAME, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD, + host=POSTGRES_HOST, + port=POSTGRES_PORT, + ) + ) + cls._connection = async_call(cls._pool.acquire()) + cls._cursor = async_call(cls._connection.cursor()) + + @classmethod + def tearDownClass(cls): + if cls._cursor: + cls._cursor.close() + if cls._connection: + cls._connection.close() + if cls._pool: + cls._pool.close() + AiopgInstrumentor().uninstrument() + + def validate_spans(self): + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + for span in spans: + if span.name == "rootSpan": + root_span = span + else: + child_span = span + self.assertIsInstance(span.start_time, int) + self.assertIsInstance(span.end_time, int) + self.assertIsNotNone(root_span) + self.assertIsNotNone(child_span) + self.assertEqual(root_span.name, "rootSpan") + self.assertEqual(child_span.name, "postgresql.opentelemetry-tests") + self.assertIsNotNone(child_span.parent) + self.assertIs(child_span.parent, root_span.get_context()) + self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual( + child_span.attributes["db.instance"], POSTGRES_DB_NAME + ) + self.assertEqual(child_span.attributes["net.peer.name"], POSTGRES_HOST) + self.assertEqual(child_span.attributes["net.peer.port"], POSTGRES_PORT) + + def test_execute(self): + """Should create a child span for execute method + """ + with self._tracer.start_as_current_span("rootSpan"): + async_call( + self._cursor.execute( + "CREATE TABLE IF NOT EXISTS test (id integer)" + ) + ) + self.validate_spans() + + def test_executemany(self): + """Should create a child span for executemany + """ + with pytest.raises(psycopg2.ProgrammingError): + with self._tracer.start_as_current_span("rootSpan"): + data = (("1",), ("2",), ("3",)) + stmt = "INSERT INTO test (id) VALUES (%s)" + async_call(self._cursor.executemany(stmt, data)) + self.validate_spans() + + def test_callproc(self): + """Should create a child span for callproc + """ + with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( + Exception + ): + async_call(self._cursor.callproc("test", ())) + self.validate_spans() diff --git a/ext/opentelemetry-instrumentation-aiopg/CHANGELOG.md b/ext/opentelemetry-instrumentation-aiopg/CHANGELOG.md new file mode 100644 index 0000000000..3e04402cea --- /dev/null +++ b/ext/opentelemetry-instrumentation-aiopg/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release diff --git a/ext/opentelemetry-instrumentation-aiopg/LICENSE b/ext/opentelemetry-instrumentation-aiopg/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-instrumentation-aiopg/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-instrumentation-aiopg/MANIFEST.in b/ext/opentelemetry-instrumentation-aiopg/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-instrumentation-aiopg/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-instrumentation-aiopg/README.rst b/ext/opentelemetry-instrumentation-aiopg/README.rst new file mode 100644 index 0000000000..0e9248ec1d --- /dev/null +++ b/ext/opentelemetry-instrumentation-aiopg/README.rst @@ -0,0 +1,21 @@ +OpenTelemetry aiopg instrumentation +=================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aiopg.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-aiopg/ + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-aiopg + + +References +---------- + +* `OpenTelemetry aiopg instrumentation `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-instrumentation-aiopg/setup.cfg b/ext/opentelemetry-instrumentation-aiopg/setup.cfg new file mode 100644 index 0000000000..4dade66644 --- /dev/null +++ b/ext/opentelemetry-instrumentation-aiopg/setup.cfg @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-instrumentaation-aiopg +description = OpenTelemetry aiopg instrumentation +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-instrumentation-aiopg +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.11.dev0 + opentelemetry-ext-dbapi == 0.11.dev0 + opentelemetry-instrumentation == 0.11.dev0 + aiopg >= 0.13.0 + wrapt >= 1.0.0, < 2.0.0 + +[options.extras_require] +test = + opentelemetry-test == 0.11.dev0 + +[options.packages.find] +where = src + + +[options.entry_points] +opentelemetry_instrumentor = + aiopg = opentelemetry.instrumentation.aiopg:AiopgInstrumentor diff --git a/ext/opentelemetry-instrumentation-aiopg/setup.py b/ext/opentelemetry-instrumentation-aiopg/setup.py new file mode 100644 index 0000000000..dfd463e5ab --- /dev/null +++ b/ext/opentelemetry-instrumentation-aiopg/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "instrumentation", "aiopg", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py b/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py new file mode 100644 index 0000000000..d2cc90902e --- /dev/null +++ b/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py @@ -0,0 +1,121 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The integration with PostgreSQL supports the aiopg library, +it can be enabled by using ``AiopgInstrumentor``. + +.. aiopg: https://github.com/aio-libs/aiopg + +Usage +----- + +.. code-block:: python + + import aiopg + from opentelemetry.instrumentation.aiopg import AiopgInstrumentor + + AiopgInstrumentor().instrument() + + cnx = await aiopg.connect(database='Database') + cursor = await cnx.cursor() + await cursor.execute("INSERT INTO test (testField) VALUES (123)") + cursor.close() + cnx.close() + + pool = await aiopg.create_pool(database='Database') + cnx = await pool.acquire() + cursor = await cnx.cursor() + await cursor.execute("INSERT INTO test (testField) VALUES (123)") + cursor.close() + cnx.close() + +API +--- +""" + +from opentelemetry.instrumentation.aiopg import wrappers +from opentelemetry.instrumentation.aiopg.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + + +class AiopgInstrumentor(BaseInstrumentor): + _CONNECTION_ATTRIBUTES = { + "database": "info.dbname", + "port": "info.port", + "host": "info.host", + "user": "info.user", + } + + _DATABASE_COMPONENT = "postgresql" + _DATABASE_TYPE = "sql" + + def _instrument(self, **kwargs): + """Integrate with PostgreSQL aiopg library. + aiopg: https://github.com/aio-libs/aiopg + """ + + tracer_provider = kwargs.get("tracer_provider") + + wrappers.wrap_connect( + __name__, + self._DATABASE_COMPONENT, + self._DATABASE_TYPE, + self._CONNECTION_ATTRIBUTES, + version=__version__, + tracer_provider=tracer_provider, + ) + + wrappers.wrap_create_pool( + __name__, + self._DATABASE_COMPONENT, + self._DATABASE_TYPE, + self._CONNECTION_ATTRIBUTES, + version=__version__, + tracer_provider=tracer_provider, + ) + + def _uninstrument(self, **kwargs): + """"Disable aiopg instrumentation""" + wrappers.unwrap_connect() + wrappers.unwrap_create_pool() + + # pylint:disable=no-self-use + def instrument_connection(self, connection): + """Enable instrumentation in a aiopg connection. + + Args: + connection: The connection to instrument. + + Returns: + An instrumented connection. + """ + return wrappers.instrument_connection( + __name__, + connection, + self._DATABASE_COMPONENT, + self._DATABASE_TYPE, + self._CONNECTION_ATTRIBUTES, + ) + + def uninstrument_connection(self, connection): + """Disable instrumentation in a aiopg connection. + + Args: + connection: The connection to uninstrument. + + Returns: + An uninstrumented connection. + """ + return wrappers.uninstrument_connection(connection) diff --git a/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py b/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py new file mode 100644 index 0000000000..def4a72c3d --- /dev/null +++ b/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py @@ -0,0 +1,144 @@ +import typing + +import wrapt +from aiopg.utils import _ContextManager, _PoolAcquireContextManager + +from opentelemetry.ext.dbapi import DatabaseApiIntegration, TracedCursor +from opentelemetry.trace import SpanKind +from opentelemetry.trace.status import Status, StatusCanonicalCode + + +# pylint: disable=abstract-method +class AsyncProxyObject(wrapt.ObjectProxy): + def __aiter__(self): + return self.__wrapped__.__aiter__() + + async def __anext__(self): + result = await self.__wrapped__.__anext__() + return result + + async def __aenter__(self): + return await self.__wrapped__.__aenter__() + + async def __aexit__(self, exc_type, exc_val, exc_tb): + return await self.__wrapped__.__aexit__(exc_type, exc_val, exc_tb) + + def __await__(self): + return self.__wrapped__.__await__() + + +class AiopgIntegration(DatabaseApiIntegration): + async def wrapped_connection( + self, + connect_method: typing.Callable[..., typing.Any], + args: typing.Tuple[typing.Any, typing.Any], + kwargs: typing.Dict[typing.Any, typing.Any], + ): + """Add object proxy to connection object. + """ + connection = await connect_method(*args, **kwargs) + # pylint: disable=protected-access + self.get_connection_attributes(connection._conn) + return get_traced_connection_proxy(connection, self) + + async def wrapped_pool(self, create_pool_method, args, kwargs): + pool = await create_pool_method(*args, **kwargs) + async with pool.acquire() as connection: + # pylint: disable=protected-access + self.get_connection_attributes(connection._conn) + return get_traced_pool_proxy(pool, self) + + +def get_traced_connection_proxy( + connection, db_api_integration, *args, **kwargs +): + # pylint: disable=abstract-method + class TracedConnectionProxy(AsyncProxyObject): + # pylint: disable=unused-argument + def __init__(self, connection, *args, **kwargs): + super().__init__(connection) + + def cursor(self, *args, **kwargs): + coro = self._cursor(*args, **kwargs) + return _ContextManager(coro) + + async def _cursor(self, *args, **kwargs): + # pylint: disable=protected-access + cursor = await self.__wrapped__._cursor(*args, **kwargs) + return get_traced_cursor_proxy(cursor, db_api_integration) + + return TracedConnectionProxy(connection, *args, **kwargs) + + +def get_traced_pool_proxy(pool, db_api_integration, *args, **kwargs): + # pylint: disable=abstract-method + class TracedPoolProxy(AsyncProxyObject): + # pylint: disable=unused-argument + def __init__(self, pool, *args, **kwargs): + super().__init__(pool) + + def acquire(self): + """Acquire free connection from the pool.""" + coro = self._acquire() + return _PoolAcquireContextManager(coro, self) + + async def _acquire(self): + # pylint: disable=protected-access + connection = await self.__wrapped__._acquire() + return get_traced_connection_proxy( + connection, db_api_integration, *args, **kwargs + ) + + return TracedPoolProxy(pool, *args, **kwargs) + + +class AsyncTracedCursor(TracedCursor): + async def traced_execution( + self, + query_method: typing.Callable[..., typing.Any], + *args: typing.Tuple[typing.Any, typing.Any], + **kwargs: typing.Dict[typing.Any, typing.Any] + ): + + with self._db_api_integration.get_tracer().start_as_current_span( + self._db_api_integration.name, kind=SpanKind.CLIENT + ) as span: + self._populate_span(span, *args) + try: + result = await query_method(*args, **kwargs) + span.set_status(Status(StatusCanonicalCode.OK)) + return result + except Exception as ex: # pylint: disable=broad-except + span.set_status(Status(StatusCanonicalCode.UNKNOWN, str(ex))) + raise ex + + +def get_traced_cursor_proxy(cursor, db_api_integration, *args, **kwargs): + _traced_cursor = AsyncTracedCursor(db_api_integration) + + # pylint: disable=abstract-method + class AsyncTracedCursorProxy(AsyncProxyObject): + + # pylint: disable=unused-argument + def __init__(self, cursor, *args, **kwargs): + super().__init__(cursor) + + async def execute(self, *args, **kwargs): + result = await _traced_cursor.traced_execution( + self.__wrapped__.execute, *args, **kwargs + ) + return result + + async def executemany(self, *args, **kwargs): + result = await _traced_cursor.traced_execution( + self.__wrapped__.executemany, *args, **kwargs + ) + return result + + async def callproc(self, *args, **kwargs): + result = await _traced_cursor.traced_execution( + self.__wrapped__.callproc, *args, **kwargs + ) + return result + + return AsyncTracedCursorProxy(cursor, *args, **kwargs) diff --git a/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py b/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py new file mode 100644 index 0000000000..858e73960f --- /dev/null +++ b/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.11.dev0" diff --git a/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py b/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py new file mode 100644 index 0000000000..473c5039c3 --- /dev/null +++ b/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py @@ -0,0 +1,223 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The trace integration with aiopg based on dbapi integration, +where replaced sync wrap methods to async + +Usage +----- + +.. code-block:: python + + from opentelemetry import trace + from opentelemetry.instrumentation.aiopg import trace_integration + from opentelemetry.trace import TracerProvider + + trace.set_tracer_provider(TracerProvider()) + + trace_integration(aiopg.connection, "_connect", "postgresql", "sql") + +API +--- +""" +import logging +import typing + +import aiopg +import wrapt + +from opentelemetry.instrumentation.aiopg.aiopg_integration import ( + AiopgIntegration, + get_traced_connection_proxy, +) +from opentelemetry.instrumentation.aiopg.version import __version__ +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.trace import TracerProvider + +logger = logging.getLogger(__name__) + + +def trace_integration( + database_component: str, + database_type: str = "", + connection_attributes: typing.Dict = None, + tracer_provider: typing.Optional[TracerProvider] = None, +): + """Integrate with aiopg library. + based on dbapi integration, where replaced sync wrap methods to async + + Args: + database_component: Database driver name or + database name "postgreSQL". + database_type: The Database type. For any SQL database, "sql". + connection_attributes: Attribute names for database, port, host and + user in Connection object. + tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to + use. If ommited the current configured one is used. + """ + + wrap_connect( + __name__, + database_component, + database_type, + connection_attributes, + __version__, + tracer_provider, + ) + + +def wrap_connect( + name: str, + database_component: str, + database_type: str = "", + connection_attributes: typing.Dict = None, + version: str = "", + tracer_provider: typing.Optional[TracerProvider] = None, +): + """Integrate with aiopg library. + https://github.com/aio-libs/aiopg + + Args: + name: Name of opentelemetry extension for aiopg. + database_component: Database driver name + or database name "postgreSQL". + database_type: The Database type. For any SQL database, "sql". + connection_attributes: Attribute names for database, port, host and + user in Connection object. + version: Version of opentelemetry extension for aiopg. + tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to + use. If ommited the current configured one is used. + """ + + # pylint: disable=unused-argument + async def wrap_connect_( + wrapped: typing.Callable[..., typing.Any], + instance: typing.Any, + args: typing.Tuple[typing.Any, typing.Any], + kwargs: typing.Dict[typing.Any, typing.Any], + ): + db_integration = AiopgIntegration( + name, + database_component, + database_type=database_type, + connection_attributes=connection_attributes, + version=version, + tracer_provider=tracer_provider, + ) + return await db_integration.wrapped_connection(wrapped, args, kwargs) + + try: + wrapt.wrap_function_wrapper(aiopg, "connect", wrap_connect_) + except Exception as ex: # pylint: disable=broad-except + logger.warning("Failed to integrate with aiopg. %s", str(ex)) + + +def unwrap_connect(): + """"Disable integration with aiopg library. + https://github.com/aio-libs/aiopg + """ + + unwrap(aiopg, "connect") + + +def instrument_connection( + name: str, + connection, + database_component: str, + database_type: str = "", + connection_attributes: typing.Dict = None, + version: str = "", + tracer_provider: typing.Optional[TracerProvider] = None, +): + """Enable instrumentation in a database connection. + + Args: + name: Name of opentelemetry extension for aiopg. + connection: The connection to instrument. + database_component: Database driver name or database name "postgreSQL". + database_type: The Database type. For any SQL database, "sql". + connection_attributes: Attribute names for database, port, host and + user in a connection object. + version: Version of opentelemetry extension for aiopg. + tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to + use. If ommited the current configured one is used. + + Returns: + An instrumented connection. + """ + db_integration = AiopgIntegration( + name, + database_component, + database_type, + connection_attributes=connection_attributes, + version=version, + tracer_provider=tracer_provider, + ) + db_integration.get_connection_attributes(connection) + return get_traced_connection_proxy(connection, db_integration) + + +def uninstrument_connection(connection): + """Disable instrumentation in a database connection. + + Args: + connection: The connection to uninstrument. + + Returns: + An uninstrumented connection. + """ + if isinstance(connection, wrapt.ObjectProxy): + return connection.__wrapped__ + + logger.warning("Connection is not instrumented") + return connection + + +def wrap_create_pool( + name: str, + database_component: str, + database_type: str = "", + connection_attributes: typing.Dict = None, + version: str = "", + tracer_provider: typing.Optional[TracerProvider] = None, +): + # pylint: disable=unused-argument + async def wrap_create_pool_( + wrapped: typing.Callable[..., typing.Any], + instance: typing.Any, + args: typing.Tuple[typing.Any, typing.Any], + kwargs: typing.Dict[typing.Any, typing.Any], + ): + db_integration = AiopgIntegration( + name, + database_component, + database_type, + connection_attributes=connection_attributes, + version=version, + tracer_provider=tracer_provider, + ) + return await db_integration.wrapped_pool(wrapped, args, kwargs) + + try: + wrapt.wrap_function_wrapper(aiopg, "create_pool", wrap_create_pool_) + except Exception as ex: # pylint: disable=broad-except + logger.warning("Failed to integrate with DB API. %s", str(ex)) + + +def unwrap_create_pool(): + """"Disable integration with aiopg library. + https://github.com/aio-libs/aiopg + """ + unwrap(aiopg, "create_pool") diff --git a/ext/opentelemetry-instrumentation-aiopg/tests/__init__.py b/ext/opentelemetry-instrumentation-aiopg/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py b/ext/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py new file mode 100644 index 0000000000..f7daf7ccc0 --- /dev/null +++ b/ext/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py @@ -0,0 +1,472 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +import logging +from unittest import mock +from unittest.mock import MagicMock + +import aiopg +from aiopg.utils import _ContextManager, _PoolAcquireContextManager + +import opentelemetry.instrumentation.aiopg +from opentelemetry import trace as trace_api +from opentelemetry.instrumentation.aiopg import AiopgInstrumentor, wrappers +from opentelemetry.instrumentation.aiopg.aiopg_integration import ( + AiopgIntegration, +) +from opentelemetry.sdk import resources +from opentelemetry.test.test_base import TestBase + + +def async_call(coro): + loop = asyncio.get_event_loop() + return loop.run_until_complete(coro) + + +class TestAiopgInstrumentor(TestBase): + def setUp(self): + super().setUp() + self.origin_aiopg_connect = aiopg.connect + self.origin_aiopg_create_pool = aiopg.create_pool + aiopg.connect = mock_connect + aiopg.create_pool = mock_create_pool + + def tearDown(self): + super().tearDown() + aiopg.connect = self.origin_aiopg_connect + aiopg.create_pool = self.origin_aiopg_create_pool + with self.disable_logging(): + AiopgInstrumentor().uninstrument() + + def test_instrumentor_connect(self): + AiopgInstrumentor().instrument() + + cnx = async_call(aiopg.connect(database="test")) + + cursor = async_call(cnx.cursor()) + + query = "SELECT * FROM test" + async_call(cursor.execute(query)) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check version and name in span's instrumentation info + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.aiopg + ) + + # check that no spans are generated after uninstrument + AiopgInstrumentor().uninstrument() + + cnx = async_call(aiopg.connect(database="test")) + cursor = async_call(cnx.cursor()) + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + def test_instrumentor_create_pool(self): + AiopgInstrumentor().instrument() + + pool = async_call(aiopg.create_pool(database="test")) + cnx = async_call(pool.acquire()) + cursor = async_call(cnx.cursor()) + + query = "SELECT * FROM test" + async_call(cursor.execute(query)) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check version and name in span's instrumentation info + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.aiopg + ) + + # check that no spans are generated after uninstrument + AiopgInstrumentor().uninstrument() + + pool = async_call(aiopg.create_pool(database="test")) + cnx = async_call(pool.acquire()) + cursor = async_call(cnx.cursor()) + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + def test_custom_tracer_provider_connect(self): + resource = resources.Resource.create({}) + result = self.create_tracer_provider(resource=resource) + tracer_provider, exporter = result + + AiopgInstrumentor().instrument(tracer_provider=tracer_provider) + + cnx = async_call(aiopg.connect(database="test")) + cursor = async_call(cnx.cursor()) + query = "SELECT * FROM test" + async_call(cursor.execute(query)) + + spans_list = exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertIs(span.resource, resource) + + def test_custom_tracer_provider_create_pool(self): + resource = resources.Resource.create({}) + result = self.create_tracer_provider(resource=resource) + tracer_provider, exporter = result + + AiopgInstrumentor().instrument(tracer_provider=tracer_provider) + + pool = async_call(aiopg.create_pool(database="test")) + cnx = async_call(pool.acquire()) + cursor = async_call(cnx.cursor()) + query = "SELECT * FROM test" + async_call(cursor.execute(query)) + + spans_list = exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertIs(span.resource, resource) + + def test_instrument_connection(self): + cnx = async_call(aiopg.connect(database="test")) + query = "SELECT * FROM test" + cursor = async_call(cnx.cursor()) + async_call(cursor.execute(query)) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) + + cnx = AiopgInstrumentor().instrument_connection(cnx) + cursor = async_call(cnx.cursor()) + async_call(cursor.execute(query)) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + def test_uninstrument_connection(self): + AiopgInstrumentor().instrument() + cnx = async_call(aiopg.connect(database="test")) + query = "SELECT * FROM test" + cursor = async_call(cnx.cursor()) + async_call(cursor.execute(query)) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + cnx = AiopgInstrumentor().uninstrument_connection(cnx) + cursor = async_call(cnx.cursor()) + async_call(cursor.execute(query)) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + +class TestAiopgIntegration(TestBase): + def setUp(self): + super().setUp() + self.tracer = self.tracer_provider.get_tracer(__name__) + + def test_span_succeeded(self): + connection_props = { + "database": "testdatabase", + "server_host": "testhost", + "server_port": 123, + "user": "testuser", + } + connection_attributes = { + "database": "database", + "port": "server_port", + "host": "server_host", + "user": "user", + } + db_integration = AiopgIntegration( + self.tracer, "testcomponent", "testtype", connection_attributes + ) + mock_connection = async_call( + db_integration.wrapped_connection( + mock_connect, {}, connection_props + ) + ) + cursor = async_call(mock_connection.cursor()) + async_call(cursor.execute("Test query", ("param1Value", False))) + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "testcomponent.testdatabase") + self.assertIs(span.kind, trace_api.SpanKind.CLIENT) + + self.assertEqual(span.attributes["component"], "testcomponent") + self.assertEqual(span.attributes["db.type"], "testtype") + self.assertEqual(span.attributes["db.instance"], "testdatabase") + self.assertEqual(span.attributes["db.statement"], "Test query") + self.assertEqual( + span.attributes["db.statement.parameters"], + "('param1Value', False)", + ) + self.assertEqual(span.attributes["db.user"], "testuser") + self.assertEqual(span.attributes["net.peer.name"], "testhost") + self.assertEqual(span.attributes["net.peer.port"], 123) + self.assertIs( + span.status.canonical_code, + trace_api.status.StatusCanonicalCode.OK, + ) + + def test_span_failed(self): + db_integration = AiopgIntegration(self.tracer, "testcomponent") + mock_connection = async_call( + db_integration.wrapped_connection(mock_connect, {}, {}) + ) + cursor = async_call(mock_connection.cursor()) + with self.assertRaises(Exception): + async_call(cursor.execute("Test query", throw_exception=True)) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.attributes["db.statement"], "Test query") + self.assertIs( + span.status.canonical_code, + trace_api.status.StatusCanonicalCode.UNKNOWN, + ) + self.assertEqual(span.status.description, "Test Exception") + + def test_executemany(self): + db_integration = AiopgIntegration(self.tracer, "testcomponent") + mock_connection = async_call( + db_integration.wrapped_connection(mock_connect, {}, {}) + ) + cursor = async_call(mock_connection.cursor()) + async_call(cursor.executemany("Test query")) + spans_list = self.memory_exporter.get_finished_spans() + + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.attributes["db.statement"], "Test query") + + def test_callproc(self): + db_integration = AiopgIntegration(self.tracer, "testcomponent") + mock_connection = async_call( + db_integration.wrapped_connection(mock_connect, {}, {}) + ) + cursor = async_call(mock_connection.cursor()) + async_call(cursor.callproc("Test stored procedure")) + spans_list = self.memory_exporter.get_finished_spans() + + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual( + span.attributes["db.statement"], "Test stored procedure" + ) + + def test_wrap_connect(self): + aiopg_mock = AiopgMock() + with mock.patch("aiopg.connect", aiopg_mock.connect): + wrappers.wrap_connect(self.tracer, "-") + connection = async_call(aiopg.connect()) + self.assertEqual(aiopg_mock.connect_call_count, 1) + self.assertIsInstance(connection.__wrapped__, mock.Mock) + + def test_unwrap_connect(self): + wrappers.wrap_connect(self.tracer, "-") + aiopg_mock = AiopgMock() + with mock.patch("aiopg.connect", aiopg_mock.connect): + connection = async_call(aiopg.connect()) + self.assertEqual(aiopg_mock.connect_call_count, 1) + wrappers.unwrap_connect() + connection = async_call(aiopg.connect()) + self.assertEqual(aiopg_mock.connect_call_count, 2) + self.assertIsInstance(connection, mock.Mock) + + def test_wrap_create_pool(self): + async def check_connection(pool): + async with pool.acquire() as connection: + self.assertEqual(aiopg_mock.create_pool_call_count, 1) + self.assertIsInstance( + connection.__wrapped__, AiopgConnectionMock + ) + + aiopg_mock = AiopgMock() + with mock.patch("aiopg.create_pool", aiopg_mock.create_pool): + wrappers.wrap_create_pool(self.tracer, "-") + pool = async_call(aiopg.create_pool()) + async_call(check_connection(pool)) + + def test_unwrap_create_pool(self): + async def check_connection(pool): + async with pool.acquire() as connection: + self.assertEqual(aiopg_mock.create_pool_call_count, 2) + self.assertIsInstance(connection, AiopgConnectionMock) + + aiopg_mock = AiopgMock() + with mock.patch("aiopg.create_pool", aiopg_mock.create_pool): + wrappers.wrap_create_pool(self.tracer, "-") + pool = async_call(aiopg.create_pool()) + self.assertEqual(aiopg_mock.create_pool_call_count, 1) + + wrappers.unwrap_create_pool() + pool = async_call(aiopg.create_pool()) + async_call(check_connection(pool)) + + def test_instrument_connection(self): + connection = mock.Mock() + # Avoid get_attributes failing because can't concatenate mock + connection.database = "-" + connection2 = wrappers.instrument_connection( + self.tracer, connection, "-" + ) + self.assertIs(connection2.__wrapped__, connection) + + def test_uninstrument_connection(self): + connection = mock.Mock() + # Set connection.database to avoid a failure because mock can't + # be concatenated + connection.database = "-" + connection2 = wrappers.instrument_connection( + self.tracer, connection, "-" + ) + self.assertIs(connection2.__wrapped__, connection) + + connection3 = wrappers.uninstrument_connection(connection2) + self.assertIs(connection3, connection) + + with self.assertLogs(level=logging.WARNING): + connection4 = wrappers.uninstrument_connection(connection) + self.assertIs(connection4, connection) + + +# pylint: disable=unused-argument +async def mock_connect(*args, **kwargs): + database = kwargs.get("database") + server_host = kwargs.get("server_host") + server_port = kwargs.get("server_port") + user = kwargs.get("user") + return MockConnection(database, server_port, server_host, user) + + +# pylint: disable=unused-argument +async def mock_create_pool(*args, **kwargs): + database = kwargs.get("database") + server_host = kwargs.get("server_host") + server_port = kwargs.get("server_port") + user = kwargs.get("user") + return MockPool(database, server_port, server_host, user) + + +class MockPool: + def __init__(self, database, server_port, server_host, user): + self.database = database + self.server_port = server_port + self.server_host = server_host + self.user = user + + async def release(self, conn): + return conn + + def acquire(self): + """Acquire free connection from the pool.""" + coro = self._acquire() + return _PoolAcquireContextManager(coro, self) + + async def _acquire(self): + connect = await mock_connect( + self.database, self.server_port, self.server_host, self.user + ) + return connect + + +class MockPsycopg2Connection: + def __init__(self, database, server_port, server_host, user): + self.database = database + self.server_port = server_port + self.server_host = server_host + self.user = user + + +class MockConnection: + def __init__(self, database, server_port, server_host, user): + self._conn = MockPsycopg2Connection( + database, server_port, server_host, user + ) + + # pylint: disable=no-self-use + def cursor(self): + coro = self._cursor() + return _ContextManager(coro) + + async def _cursor(self): + return MockCursor() + + def close(self): + pass + + +class MockCursor: + # pylint: disable=unused-argument, no-self-use + async def execute(self, query, params=None, throw_exception=False): + if throw_exception: + raise Exception("Test Exception") + + # pylint: disable=unused-argument, no-self-use + async def executemany(self, query, params=None, throw_exception=False): + if throw_exception: + raise Exception("Test Exception") + + # pylint: disable=unused-argument, no-self-use + async def callproc(self, query, params=None, throw_exception=False): + if throw_exception: + raise Exception("Test Exception") + + +class AiopgConnectionMock: + _conn = MagicMock() + + async def __aexit__(self, exc_type, exc_val, exc_tb): + pass + + async def __aenter__(self): + return MagicMock() + + +class AiopgPoolMock: + async def release(self, conn): + return conn + + def acquire(self): + coro = self._acquire() + return _PoolAcquireContextManager(coro, self) + + async def _acquire(self): + return AiopgConnectionMock() + + +class AiopgMock: + def __init__(self): + self.connect_call_count = 0 + self.create_pool_call_count = 0 + + async def connect(self, *args, **kwargs): + self.connect_call_count += 1 + return MagicMock() + + async def create_pool(self, *args, **kwargs): + self.create_pool_call_count += 1 + return AiopgPoolMock() diff --git a/tox.ini b/tox.ini index ad7b8ad3e9..f956733d56 100644 --- a/tox.ini +++ b/tox.ini @@ -32,6 +32,10 @@ envlist = py3{5,6,7,8}-test-instrumentation-aiohttp-client pypy3-test-instrumentation-aiohttp-client + ; opentelemetry-instrumentation-aiopg + py3{5,6,7,8}-test-instrumentation-aiopg + ; instrumentation-aiopg intentionally excluded from pypy3 + ; opentelemetry-ext-botocore py3{6,7,8}-test-instrumentation-botocore pypy3-test-instrumentation-botocore @@ -116,7 +120,7 @@ envlist = ; opentelemetry-ext-pyramid py3{4,5,6,7,8}-test-instrumentation-pyramid pypy3-test-instrumentation-pyramid - + ; opentelemetry-ext-asgi py3{5,6,7,8}-test-instrumentation-asgi pypy3-test-instrumentation-asgi @@ -194,6 +198,7 @@ changedir = test-core-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests test-instrumentation-aiohttp-client: ext/opentelemetry-ext-aiohttp-client/tests + test-instrumentation-aiopg: ext/opentelemetry-instrumentation-aiopg/tests test-instrumentation-asgi: ext/opentelemetry-ext-asgi/tests test-instrumentation-asyncpg: ext/opentelemetry-ext-asyncpg/tests test-instrumentation-boto: ext/opentelemetry-ext-boto/tests @@ -295,6 +300,8 @@ commands_pre = aiohttp-client: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-aiohttp-client + aiopg: pip install {toxinidir}/ext/opentelemetry-ext-dbapi pip install {toxinidir}/ext/opentelemetry-instrumentation-aiopg[test] + jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk @@ -392,6 +399,7 @@ deps = pymongo ~= 3.1 pymysql ~= 0.9.3 psycopg2-binary ~= 2.8.4 + aiopg >= 0.13.0 sqlalchemy ~= 1.3.16 redis ~= 3.3.11 celery ~= 4.0, != 4.4.4 @@ -412,6 +420,7 @@ commands_pre = -e {toxinidir}/ext/opentelemetry-ext-pymongo \ -e {toxinidir}/ext/opentelemetry-ext-pymysql \ -e {toxinidir}/ext/opentelemetry-ext-sqlalchemy \ + -e {toxinidir}/ext/opentelemetry-instrumentation-aiopg \ -e {toxinidir}/ext/opentelemetry-ext-redis \ -e {toxinidir}/ext/opentelemetry-ext-system-metrics \ -e {toxinidir}/ext/opentelemetry-ext-opencensusexporter From 935280c3b8820351d9761c786148f1ff0e9b120d Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 24 Jul 2020 08:48:44 -0700 Subject: [PATCH 0472/1517] api/sdk: Rename record_error to record_exception per spec (#927) --- opentelemetry-api/CHANGELOG.md | 2 ++ .../src/opentelemetry/trace/span.py | 6 +++--- opentelemetry-sdk/CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/__init__.py | 12 ++++++------ opentelemetry-sdk/tests/trace/test_trace.py | 19 ++++++++++++------- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 413946b85b..d634845991 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -4,6 +4,8 @@ - Return INVALID_SPAN if no TracerProvider set for get_current_span ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) +- Rename record_error to record_exception + ([#927](https://github.com/open-telemetry/opentelemetry-python/pull/927)) ## 0.9b0 diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index baea6670f6..d207ecf565 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -89,8 +89,8 @@ def set_status(self, status: Status) -> None: """ @abc.abstractmethod - def record_error(self, err: Exception) -> None: - """Records an error as a span event.""" + def record_exception(self, exception: Exception) -> None: + """Records an exception as a span event.""" def __enter__(self) -> "Span": """Invoked when `Span` is used as a context manager. @@ -257,7 +257,7 @@ def update_name(self, name: str) -> None: def set_status(self, status: Status) -> None: pass - def record_error(self, err: Exception) -> None: + def record_exception(self, exception: Exception) -> None: pass diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 46ad8b8e29..6427b2c9b5 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -4,6 +4,8 @@ - Add support for resources and resource detector ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) +- Rename record_error to record_exception + ([#927](https://github.com/open-telemetry/opentelemetry-python/pull/927)) ## Version 0.10b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 1118b42488..848f571e6a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -683,14 +683,14 @@ def __exit__( super().__exit__(exc_type, exc_val, exc_tb) - def record_error(self, err: Exception) -> None: - """Records an error as a span event.""" + def record_exception(self, exception: Exception) -> None: + """Records an exception as a span event.""" self.add_event( - name="error", + name="exception", attributes={ - "error.type": err.__class__.__name__, - "error.message": str(err), - "error.stack": traceback.format_exc(), + "exception.type": exception.__class__.__name__, + "exception.message": str(exception), + "exception.stacktrace": traceback.format_exc(), }, ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index a43db73a3d..02cc836e88 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -801,18 +801,23 @@ def error_status_test(context): .start_as_current_span("root") ) - def test_record_error(self): + def test_record_exception(self): span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) try: raise ValueError("invalid") except ValueError as err: - span.record_error(err) - error_event = span.events[0] - self.assertEqual("error", error_event.name) - self.assertEqual("invalid", error_event.attributes["error.message"]) - self.assertEqual("ValueError", error_event.attributes["error.type"]) + span.record_exception(err) + exception_event = span.events[0] + self.assertEqual("exception", exception_event.name) + self.assertEqual( + "invalid", exception_event.attributes["exception.message"] + ) + self.assertEqual( + "ValueError", exception_event.attributes["exception.type"] + ) self.assertIn( - "ValueError: invalid", error_event.attributes["error.stack"] + "ValueError: invalid", + exception_event.attributes["exception.stacktrace"], ) From 004896f1987e84b2d8f5c84bb1505099e178694c Mon Sep 17 00:00:00 2001 From: achronak Date: Fri, 24 Jul 2020 21:43:19 +0300 Subject: [PATCH 0473/1517] docs: Update README.rst (#935) --- docs/examples/basic_meter/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/basic_meter/README.rst b/docs/examples/basic_meter/README.rst index d77fbb7c4b..46efe36388 100644 --- a/docs/examples/basic_meter/README.rst +++ b/docs/examples/basic_meter/README.rst @@ -5,7 +5,7 @@ These examples show how to use OpenTelemetry to capture and report metrics. There are three different examples: -* basic_metrics: Shows to how create a metric instrument, how to configure an +* basic_metrics: Shows how to create a metric instrument, how to configure an exporter and a controller and also how to capture data by using the direct calling convention. From 4a922d6b4f3a0f3c624b401a7d249fca0af996ce Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 27 Jul 2020 08:05:09 -0700 Subject: [PATCH 0474/1517] meta: adding aabmass as approver (#941) once approved the PR, aabmass will be added to the approvers team --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c4e335589d..624166d25e 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): +- [Aaron Abbott](https://github.com/aabmass), Google - [Carlos Alberto Cortez](https://github.com/carlosalberto), Lightstep - [Tahir H. Butt](https://github.com/majorgreys) DataDog - [Chris Kleinknecht](https://github.com/c24t), Google From 090fb7c68417270339a4486574f51b6960d82944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 27 Jul 2020 17:44:39 -0500 Subject: [PATCH 0475/1517] Drop mauriciovasquezbernal from approvers (#948) I haven't contributed to the project in a while and I don't expect to start contributing again. This commit removes myself as an approver of the project. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 624166d25e..a402e5c73e 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,6 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Chris Kleinknecht](https://github.com/c24t), Google - [Diego Hurtado](https://github.com/ocelotl) - [Hector Hernandez](https://github.com/hectorhdzg), Microsoft -- [Mauricio Vásquez](https://github.com/mauriciovasquezbernal), Kinvolk - [Reiley Yang](https://github.com/reyang), Microsoft - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google From 7f74a890086de2dc0fd34a6ba6d926b3b3fd82e5 Mon Sep 17 00:00:00 2001 From: Thomas Desrosiers <681004+thomasdesr@users.noreply.github.com> Date: Tue, 28 Jul 2020 11:40:32 -0400 Subject: [PATCH 0476/1517] ext/asyncpg: Shouldn't capture query parameters by default (#854) * Update CHANGELOG.md Co-authored-by: alrex --- ext/opentelemetry-ext-asyncpg/CHANGELOG.md | 3 + .../src/opentelemetry/ext/asyncpg/__init__.py | 66 ++++--- .../tests/asyncpg/test_asyncpg_functional.py | 176 +++++++++++------- 3 files changed, 150 insertions(+), 95 deletions(-) diff --git a/ext/opentelemetry-ext-asyncpg/CHANGELOG.md b/ext/opentelemetry-ext-asyncpg/CHANGELOG.md index e81079bfb8..052b66ea4e 100644 --- a/ext/opentelemetry-ext-asyncpg/CHANGELOG.md +++ b/ext/opentelemetry-ext-asyncpg/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Shouldn't capture query parameters by default + ([#854](https://github.com/open-telemetry/opentelemetry-python/pull/854)) + ## Version 0.10b0 Released 2020-06-23 diff --git a/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/__init__.py b/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/__init__.py index c373d7194d..4a3a51ac08 100644 --- a/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/__init__.py +++ b/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/__init__.py @@ -74,36 +74,11 @@ def _hydrate_span_from_args(connection, query, parameters) -> dict: return span_attributes -async def _do_execute(func, instance, args, kwargs): - span_attributes = _hydrate_span_from_args(instance, args[0], args[1:]) - tracer = getattr(asyncpg, _APPLIED) - - exception = None - - with tracer.start_as_current_span( - "postgresql", kind=SpanKind.CLIENT - ) as span: - - for attribute, value in span_attributes.items(): - span.set_attribute(attribute, value) - - try: - result = await func(*args, **kwargs) - except Exception as exc: # pylint: disable=W0703 - exception = exc - raise - finally: - if exception is not None: - span.set_status( - Status(_exception_to_canonical_code(exception)) - ) - else: - span.set_status(Status(StatusCanonicalCode.OK)) - - return result - - class AsyncPGInstrumentor(BaseInstrumentor): + def __init__(self, capture_parameters=False): + super().__init__() + self.capture_parameters = capture_parameters + def _instrument(self, **kwargs): tracer_provider = kwargs.get( "tracer_provider", trace.get_tracer_provider() @@ -113,6 +88,7 @@ def _instrument(self, **kwargs): _APPLIED, tracer_provider.get_tracer("asyncpg", __version__), ) + for method in [ "Connection.execute", "Connection.executemany", @@ -121,7 +97,7 @@ def _instrument(self, **kwargs): "Connection.fetchrow", ]: wrapt.wrap_function_wrapper( - "asyncpg.connection", method, _do_execute + "asyncpg.connection", method, self._do_execute ) def _uninstrument(self, **__): @@ -134,3 +110,33 @@ def _uninstrument(self, **__): "fetchrow", ]: unwrap(asyncpg.Connection, method) + + async def _do_execute(self, func, instance, args, kwargs): + span_attributes = _hydrate_span_from_args( + instance, args[0], args[1:] if self.capture_parameters else None, + ) + tracer = getattr(asyncpg, _APPLIED) + + exception = None + + with tracer.start_as_current_span( + "postgresql", kind=SpanKind.CLIENT + ) as span: + + for attribute, value in span_attributes.items(): + span.set_attribute(attribute, value) + + try: + result = await func(*args, **kwargs) + except Exception as exc: # pylint: disable=W0703 + exception = exc + raise + finally: + if exception is not None: + span.set_status( + Status(_exception_to_canonical_code(exception)) + ) + else: + span.set_status(Status(StatusCanonicalCode.OK)) + + return result diff --git a/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py b/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py index c5f5557438..d3060592a6 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py @@ -19,7 +19,7 @@ def _await(coro): return loop.run_until_complete(coro) -class TestFunctionalPsycopg(TestBase): +class TestFunctionalAsyncPG(TestBase): @classmethod def setUpClass(cls): super().setUpClass() @@ -58,24 +58,6 @@ def test_instrumented_execute_method_without_arguments(self, *_, **__): }, ) - def test_instrumented_execute_method_with_arguments(self, *_, **__): - _await(self._connection.execute("SELECT $1;", "1")) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - self.assertEqual( - StatusCanonicalCode.OK, spans[0].status.canonical_code - ) - self.assertEqual( - spans[0].attributes, - { - "db.type": "sql", - "db.user": POSTGRES_USER, - "db.statement.parameters": "('1',)", - "db.instance": POSTGRES_DB_NAME, - "db.statement": "SELECT $1;", - }, - ) - def test_instrumented_fetch_method_without_arguments(self, *_, **__): _await(self._connection.fetch("SELECT 42;")) spans = self.memory_exporter.get_finished_spans() @@ -90,52 +72,6 @@ def test_instrumented_fetch_method_without_arguments(self, *_, **__): }, ) - def test_instrumented_fetch_method_with_arguments(self, *_, **__): - _await(self._connection.fetch("SELECT $1;", "1")) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - self.assertEqual( - spans[0].attributes, - { - "db.type": "sql", - "db.user": POSTGRES_USER, - "db.statement.parameters": "('1',)", - "db.instance": POSTGRES_DB_NAME, - "db.statement": "SELECT $1;", - }, - ) - - def test_instrumented_executemany_method_with_arguments(self, *_, **__): - _await(self._connection.executemany("SELECT $1;", [["1"], ["2"]])) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - self.assertEqual( - { - "db.type": "sql", - "db.statement": "SELECT $1;", - "db.statement.parameters": "([['1'], ['2']],)", - "db.user": POSTGRES_USER, - "db.instance": POSTGRES_DB_NAME, - }, - spans[0].attributes, - ) - - def test_instrumented_execute_interface_error_method(self, *_, **__): - with self.assertRaises(asyncpg.InterfaceError): - _await(self._connection.execute("SELECT 42;", 1, 2, 3)) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - self.assertEqual( - spans[0].attributes, - { - "db.type": "sql", - "db.instance": POSTGRES_DB_NAME, - "db.user": POSTGRES_USER, - "db.statement.parameters": "(1, 2, 3)", - "db.statement": "SELECT 42;", - }, - ) - def test_instrumented_transaction_method(self, *_, **__): async def _transaction_execute(): async with self._connection.transaction(): @@ -229,3 +165,113 @@ async def _transaction_execute(): self.assertEqual( StatusCanonicalCode.OK, spans[2].status.canonical_code ) + + def test_instrumented_method_doesnt_capture_parameters(self, *_, **__): + _await(self._connection.execute("SELECT $1;", "1")) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual( + StatusCanonicalCode.OK, spans[0].status.canonical_code + ) + self.assertEqual( + spans[0].attributes, + { + "db.type": "sql", + "db.user": POSTGRES_USER, + # This shouldn't be set because we don't capture parameters by + # default + # + # "db.statement.parameters": "('1',)", + "db.instance": POSTGRES_DB_NAME, + "db.statement": "SELECT $1;", + }, + ) + + +class TestFunctionalAsyncPG_CaptureParameters(TestBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._connection = None + cls._cursor = None + cls._tracer = cls.tracer_provider.get_tracer(__name__) + AsyncPGInstrumentor(capture_parameters=True).instrument( + tracer_provider=cls.tracer_provider + ) + cls._connection = _await( + asyncpg.connect( + database=POSTGRES_DB_NAME, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD, + host=POSTGRES_HOST, + port=POSTGRES_PORT, + ) + ) + + @classmethod + def tearDownClass(cls): + AsyncPGInstrumentor().uninstrument() + + def test_instrumented_execute_method_with_arguments(self, *_, **__): + _await(self._connection.execute("SELECT $1;", "1")) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual( + StatusCanonicalCode.OK, spans[0].status.canonical_code + ) + self.assertEqual( + spans[0].attributes, + { + "db.type": "sql", + "db.user": POSTGRES_USER, + "db.statement.parameters": "('1',)", + "db.instance": POSTGRES_DB_NAME, + "db.statement": "SELECT $1;", + }, + ) + + def test_instrumented_fetch_method_with_arguments(self, *_, **__): + _await(self._connection.fetch("SELECT $1;", "1")) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual( + spans[0].attributes, + { + "db.type": "sql", + "db.user": POSTGRES_USER, + "db.statement.parameters": "('1',)", + "db.instance": POSTGRES_DB_NAME, + "db.statement": "SELECT $1;", + }, + ) + + def test_instrumented_executemany_method_with_arguments(self, *_, **__): + _await(self._connection.executemany("SELECT $1;", [["1"], ["2"]])) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual( + { + "db.type": "sql", + "db.statement": "SELECT $1;", + "db.statement.parameters": "([['1'], ['2']],)", + "db.user": POSTGRES_USER, + "db.instance": POSTGRES_DB_NAME, + }, + spans[0].attributes, + ) + + def test_instrumented_execute_interface_error_method(self, *_, **__): + with self.assertRaises(asyncpg.InterfaceError): + _await(self._connection.execute("SELECT 42;", 1, 2, 3)) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual( + spans[0].attributes, + { + "db.type": "sql", + "db.instance": POSTGRES_DB_NAME, + "db.user": POSTGRES_USER, + "db.statement.parameters": "(1, 2, 3)", + "db.statement": "SELECT 42;", + }, + ) From 8bc7786b191b4f1b1f161de983db71643bf9f51e Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 28 Jul 2020 09:18:19 -0700 Subject: [PATCH 0477/1517] chore: create pull_request_template.md (#952) --- .github/pull_request_template.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..2281d05e17 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,27 @@ +# Description + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. + +Fixes # (issue) + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +# How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + +- [ ] Test A + +# Checklist: + +- [ ] Followed the style guidelines of this project +- [ ] Changelogs have been updated +- [ ] Unit tests have been added +- [ ] Documentation has been updated From f682cf57a90828f24969281a98980a7dafe0ee26 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 28 Jul 2020 13:26:40 -0700 Subject: [PATCH 0478/1517] release: updating versions and changelogs (#954) --- docs/examples/opentelemetry-example-app/setup.cfg | 8 ++++---- .../src/opentelemetry_example_app/version.py | 2 +- ext/opentelemetry-ext-aiohttp-client/setup.cfg | 4 ++-- .../src/opentelemetry/ext/aiohttp_client/version.py | 2 +- ext/opentelemetry-ext-asgi/setup.cfg | 4 ++-- .../src/opentelemetry/ext/asgi/version.py | 2 +- ext/opentelemetry-ext-asyncpg/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-asyncpg/setup.cfg | 6 +++--- .../src/opentelemetry/ext/asyncpg/version.py | 2 +- ext/opentelemetry-ext-boto/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-boto/setup.cfg | 6 +++--- .../src/opentelemetry/ext/boto/version.py | 2 +- ext/opentelemetry-ext-botocore/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-botocore/setup.cfg | 6 +++--- .../src/opentelemetry/ext/botocore/version.py | 2 +- ext/opentelemetry-ext-celery/setup.cfg | 6 +++--- .../src/opentelemetry/ext/celery/version.py | 2 +- ext/opentelemetry-ext-datadog/setup.cfg | 4 ++-- .../src/opentelemetry/ext/datadog/version.py | 2 +- ext/opentelemetry-ext-dbapi/setup.cfg | 6 +++--- .../src/opentelemetry/ext/dbapi/version.py | 2 +- ext/opentelemetry-ext-django/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-django/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/django/version.py | 2 +- ext/opentelemetry-ext-elasticsearch/setup.cfg | 6 +++--- .../src/opentelemetry/ext/elasticsearch/version.py | 2 +- ext/opentelemetry-ext-flask/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-flask/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/flask/version.py | 2 +- ext/opentelemetry-ext-grpc/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-grpc/setup.cfg | 6 +++--- .../src/opentelemetry/ext/grpc/version.py | 2 +- ext/opentelemetry-ext-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/ext/jaeger/version.py | 2 +- ext/opentelemetry-ext-jinja2/setup.cfg | 6 +++--- .../src/opentelemetry/ext/jinja2/version.py | 2 +- ext/opentelemetry-ext-mysql/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-mysql/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/mysql/version.py | 2 +- ext/opentelemetry-ext-opencensusexporter/setup.cfg | 4 ++-- .../src/opentelemetry/ext/opencensusexporter/version.py | 2 +- ext/opentelemetry-ext-opentracing-shim/setup.cfg | 4 ++-- .../src/opentelemetry/ext/opentracing_shim/version.py | 2 +- ext/opentelemetry-ext-otlp/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/ext/otlp/version.py | 2 +- ext/opentelemetry-ext-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/ext/prometheus/version.py | 2 +- ext/opentelemetry-ext-psycopg2/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/psycopg2/version.py | 2 +- ext/opentelemetry-ext-pymemcache/setup.cfg | 6 +++--- .../src/opentelemetry/ext/pymemcache/version.py | 2 +- ext/opentelemetry-ext-pymongo/setup.cfg | 6 +++--- .../src/opentelemetry/ext/pymongo/version.py | 2 +- ext/opentelemetry-ext-pymysql/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/pymysql/version.py | 2 +- ext/opentelemetry-ext-pyramid/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-pyramid/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/pyramid/version.py | 2 +- ext/opentelemetry-ext-redis/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/redis/version.py | 2 +- ext/opentelemetry-ext-requests/setup.cfg | 6 +++--- .../src/opentelemetry/ext/requests/version.py | 2 +- ext/opentelemetry-ext-sqlalchemy/setup.cfg | 6 +++--- .../src/opentelemetry/ext/sqlalchemy/version.py | 2 +- ext/opentelemetry-ext-sqlite3/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/sqlite3/version.py | 2 +- ext/opentelemetry-ext-system-metrics/setup.cfg | 4 ++-- .../src/opentelemetry/ext/system_metrics/version.py | 2 +- ext/opentelemetry-ext-wsgi/CHANGELOG.md | 4 ++++ ext/opentelemetry-ext-wsgi/setup.cfg | 6 +++--- .../src/opentelemetry/ext/wsgi/version.py | 2 +- ext/opentelemetry-ext-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/ext/zipkin/version.py | 2 +- ext/opentelemetry-instrumentation-aiopg/CHANGELOG.md | 4 ++++ ext/opentelemetry-instrumentation-aiopg/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/aiopg/version.py | 2 +- ext/opentelemetry-instrumentation-fastapi/CHANGELOG.md | 4 ++++ ext/opentelemetry-instrumentation-fastapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/fastapi/version.py | 2 +- ext/opentelemetry-instrumentation-starlette/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/starlette/version.py | 2 +- opentelemetry-api/CHANGELOG.md | 4 ++++ opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 4 ++++ opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 91 files changed, 204 insertions(+), 148 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index 185229dc92..8905023bbf 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -43,10 +43,10 @@ zip_safe = False include_package_data = True install_requires = typing; python_version<'3.5' - opentelemetry-api == 0.11.dev0 - opentelemetry-sdk == 0.11.dev0 - opentelemetry-ext-requests == 0.11.dev0 - opentelemetry-ext-flask == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-sdk == 0.11b0 + opentelemetry-ext-requests == 0.11b0 + opentelemetry-ext-flask == 0.11b0 flask requests protobuf~=3.11 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py index 858e73960f..15e8d9536d 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-aiohttp-client/setup.cfg b/ext/opentelemetry-ext-aiohttp-client/setup.cfg index 157b691803..9044083ff7 100644 --- a/ext/opentelemetry-ext-aiohttp-client/setup.cfg +++ b/ext/opentelemetry-ext-aiohttp-client/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api >= 0.11b0 + opentelemetry-instrumentation == 0.11b0 aiohttp ~= 3.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py index 5924968fb2..ae93e51455 100644 --- a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py +++ b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-asgi/setup.cfg b/ext/opentelemetry-ext-asgi/setup.cfg index 75b4c12f83..9183287f0d 100644 --- a/ext/opentelemetry-ext-asgi/setup.cfg +++ b/ext/opentelemetry-ext-asgi/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 asgiref ~= 3.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py +++ b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-asyncpg/CHANGELOG.md b/ext/opentelemetry-ext-asyncpg/CHANGELOG.md index 052b66ea4e..2dc9ad6f04 100644 --- a/ext/opentelemetry-ext-asyncpg/CHANGELOG.md +++ b/ext/opentelemetry-ext-asyncpg/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - Shouldn't capture query parameters by default ([#854](https://github.com/open-telemetry/opentelemetry-python/pull/854)) diff --git a/ext/opentelemetry-ext-asyncpg/setup.cfg b/ext/opentelemetry-ext-asyncpg/setup.cfg index 786e69f6ee..2eaa54b847 100644 --- a/ext/opentelemetry-ext-asyncpg/setup.cfg +++ b/ext/opentelemetry-ext-asyncpg/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 asyncpg >= 0.12.0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py b/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py +++ b/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-boto/CHANGELOG.md b/ext/opentelemetry-ext-boto/CHANGELOG.md index 612b73b39a..ec62b40595 100644 --- a/ext/opentelemetry-ext-boto/CHANGELOG.md +++ b/ext/opentelemetry-ext-boto/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - ext/boto and ext/botocore: fails to export spans via jaeger ([#866](https://github.com/open-telemetry/opentelemetry-python/pull/866)) diff --git a/ext/opentelemetry-ext-boto/setup.cfg b/ext/opentelemetry-ext-boto/setup.cfg index 64a9932756..a9e0ec9f4e 100644 --- a/ext/opentelemetry-ext-boto/setup.cfg +++ b/ext/opentelemetry-ext-boto/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = boto ~= 2.0 - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 [options.extras_require] test = boto~=2.0 moto~=1.0 - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-botocore/CHANGELOG.md b/ext/opentelemetry-ext-botocore/CHANGELOG.md index ccdc954a6b..ac8d9c4a51 100644 --- a/ext/opentelemetry-ext-botocore/CHANGELOG.md +++ b/ext/opentelemetry-ext-botocore/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - ext/boto and ext/botocore: fails to export spans via jaeger ([#866](https://github.com/open-telemetry/opentelemetry-python/pull/866)) diff --git a/ext/opentelemetry-ext-botocore/setup.cfg b/ext/opentelemetry-ext-botocore/setup.cfg index 9e0aabfd23..5e2d0e0b35 100644 --- a/ext/opentelemetry-ext-botocore/setup.cfg +++ b/ext/opentelemetry-ext-botocore/setup.cfg @@ -41,13 +41,13 @@ package_dir= packages=find_namespace: install_requires = botocore ~= 1.0 - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 [options.extras_require] test = moto ~= 1.0 - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py +++ b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-celery/setup.cfg b/ext/opentelemetry-ext-celery/setup.cfg index 67609b2c2c..d70e0f7b01 100644 --- a/ext/opentelemetry-ext-celery/setup.cfg +++ b/ext/opentelemetry-ext-celery/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 celery ~= 4.0 [options.extras_require] test = pytest - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py b/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py +++ b/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-datadog/setup.cfg b/ext/opentelemetry-ext-datadog/setup.cfg index d97bcdaa69..62ffd1c907 100644 --- a/ext/opentelemetry-ext-datadog/setup.cfg +++ b/ext/opentelemetry-ext-datadog/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = ddtrace>=0.34.0 - opentelemetry-api == 0.11.dev0 - opentelemetry-sdk == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-sdk == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index bf7af968f7..5ee74e322a 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-django/CHANGELOG.md b/ext/opentelemetry-ext-django/CHANGELOG.md index abbf90451b..6e3cc9c982 100644 --- a/ext/opentelemetry-ext-django/CHANGELOG.md +++ b/ext/opentelemetry-ext-django/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) ## 0.8b0 diff --git a/ext/opentelemetry-ext-django/setup.cfg b/ext/opentelemetry-ext-django/setup.cfg index e9cea61ced..8474194ef5 100644 --- a/ext/opentelemetry-ext-django/setup.cfg +++ b/ext/opentelemetry-ext-django/setup.cfg @@ -41,13 +41,13 @@ package_dir= packages=find_namespace: install_requires = django >= 1.10 - opentelemetry-ext-wsgi == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 - opentelemetry-api == 0.11.dev0 + opentelemetry-ext-wsgi == 0.11b0 + opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.11b0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-elasticsearch/setup.cfg b/ext/opentelemetry-ext-elasticsearch/setup.cfg index 40f3b392b4..46f32b13ef 100644 --- a/ext/opentelemetry-ext-elasticsearch/setup.cfg +++ b/ext/opentelemetry-ext-elasticsearch/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 wrapt >= 1.0.0, < 2.0.0 elasticsearch >= 2.0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 elasticsearch-dsl >= 2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py b/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py +++ b/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-flask/CHANGELOG.md b/ext/opentelemetry-ext-flask/CHANGELOG.md index 1db23d4e80..3109c15902 100644 --- a/ext/opentelemetry-ext-flask/CHANGELOG.md +++ b/ext/opentelemetry-ext-flask/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) ## 0.7b1 diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index 663b2169a5..3d99a53a8c 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-ext-wsgi == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 - opentelemetry-api == 0.11.dev0 + opentelemetry-ext-wsgi == 0.11b0 + opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.11b0 [options.extras_require] test = flask~=1.0 - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-grpc/CHANGELOG.md b/ext/opentelemetry-ext-grpc/CHANGELOG.md index 4221ab5371..fff83ab5a7 100644 --- a/ext/opentelemetry-ext-grpc/CHANGELOG.md +++ b/ext/opentelemetry-ext-grpc/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - Add status code to gRPC client spans ([896](https://github.com/open-telemetry/opentelemetry-python/pull/896)) - Add gRPC client and server instrumentors diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index dd29603247..4dd808a19e 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 + opentelemetry-api == 0.11b0 grpcio ~= 1.27 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 - opentelemetry-sdk == 0.11.dev0 + opentelemetry-test == 0.11b0 + opentelemetry-sdk == 0.11b0 protobuf == 3.12.2 [options.packages.find] diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/ext/opentelemetry-ext-jaeger/setup.cfg index c19107eedd..1c0a006529 100644 --- a/ext/opentelemetry-ext-jaeger/setup.cfg +++ b/ext/opentelemetry-ext-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.11.dev0 - opentelemetry-sdk == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-sdk == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index 60f2f9e9d2..13bf32b8f8 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-jinja2/setup.cfg b/ext/opentelemetry-ext-jinja2/setup.cfg index 54a7515150..1fbd74a3d0 100644 --- a/ext/opentelemetry-ext-jinja2/setup.cfg +++ b/ext/opentelemetry-ext-jinja2/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py +++ b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-mysql/CHANGELOG.md b/ext/opentelemetry-ext-mysql/CHANGELOG.md index a4ab480939..46b9d327dd 100644 --- a/ext/opentelemetry-ext-mysql/CHANGELOG.md +++ b/ext/opentelemetry-ext-mysql/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - bugfix: Fix auto-instrumentation entry point for mysql ([#858](https://github.com/open-telemetry/opentelemetry-python/pull/858)) diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index ceb985e8b4..0e7209b168 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-ext-dbapi == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-ext-dbapi == 0.11b0 + opentelemetry-instrumentation == 0.11b0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-opencensusexporter/setup.cfg b/ext/opentelemetry-ext-opencensusexporter/setup.cfg index ea963dce18..1cf9d1a3cf 100644 --- a/ext/opentelemetry-ext-opencensusexporter/setup.cfg +++ b/ext/opentelemetry-ext-opencensusexporter/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.11.dev0 - opentelemetry-sdk == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-sdk == 0.11b0 protobuf >= 3.8.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py +++ b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg index abc052a5aa..d2b9cf28cc 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.11.dev0 + opentelemetry-api == 0.11b0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-otlp/CHANGELOG.md b/ext/opentelemetry-ext-otlp/CHANGELOG.md index fbfff4496e..32e75eda55 100644 --- a/ext/opentelemetry-ext-otlp/CHANGELOG.md +++ b/ext/opentelemetry-ext-otlp/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - Update span exporter to use OpenTelemetry Proto v0.4.0 ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/889)) ## 0.9b0 diff --git a/ext/opentelemetry-ext-otlp/setup.cfg b/ext/opentelemetry-ext-otlp/setup.cfg index 65908e0a3c..b6f7068d7f 100644 --- a/ext/opentelemetry-ext-otlp/setup.cfg +++ b/ext/opentelemetry-ext-otlp/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.11.dev0 - opentelemetry-sdk == 0.11.dev0 - opentelemetry-proto == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-sdk == 0.11b0 + opentelemetry-proto == 0.11b0 backoff ~= 1.10.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py +++ b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/ext/opentelemetry-ext-prometheus/setup.cfg index 5dae17f5cb..3c55f3121c 100644 --- a/ext/opentelemetry-ext-prometheus/setup.cfg +++ b/ext/opentelemetry-ext-prometheus/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.11.dev0 - opentelemetry-sdk == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-sdk == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index bb0fd9a57c..9cf7b560c7 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-ext-dbapi == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-ext-dbapi == 0.11b0 + opentelemetry-instrumentation == 0.11b0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-pymemcache/setup.cfg b/ext/opentelemetry-ext-pymemcache/setup.cfg index c2252274b1..0fbc8d60ad 100644 --- a/ext/opentelemetry-ext-pymemcache/setup.cfg +++ b/ext/opentelemetry-ext-pymemcache/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 pymemcache ~= 1.3 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py b/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py +++ b/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index 5e08d98776..a1f7284605 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 pymongo ~= 3.1 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-pymysql/setup.cfg b/ext/opentelemetry-ext-pymysql/setup.cfg index db28ef9581..adb51131b7 100644 --- a/ext/opentelemetry-ext-pymysql/setup.cfg +++ b/ext/opentelemetry-ext-pymysql/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-ext-dbapi == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-ext-dbapi == 0.11b0 + opentelemetry-instrumentation == 0.11b0 PyMySQL ~= 0.9.3 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-pyramid/CHANGELOG.md b/ext/opentelemetry-ext-pyramid/CHANGELOG.md index 586f8bb5ce..f871a0aa5f 100644 --- a/ext/opentelemetry-ext-pyramid/CHANGELOG.md +++ b/ext/opentelemetry-ext-pyramid/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) ## 0.9b0 diff --git a/ext/opentelemetry-ext-pyramid/setup.cfg b/ext/opentelemetry-ext-pyramid/setup.cfg index bcdf850e87..1255dbad14 100644 --- a/ext/opentelemetry-ext-pyramid/setup.cfg +++ b/ext/opentelemetry-ext-pyramid/setup.cfg @@ -41,15 +41,15 @@ package_dir= packages=find_namespace: install_requires = pyramid >= 1.7 - opentelemetry-instrumentation == 0.11.dev0 - opentelemetry-api == 0.11.dev0 - opentelemetry-ext-wsgi == 0.11.dev0 + opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.11b0 + opentelemetry-ext-wsgi == 0.11b0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = werkzeug == 0.16.1 - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py +++ b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-redis/setup.cfg b/ext/opentelemetry-ext-redis/setup.cfg index 26159c6568..e654b81add 100644 --- a/ext/opentelemetry-ext-redis/setup.cfg +++ b/ext/opentelemetry-ext-redis/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 redis >= 2.6 wrapt >= 1.12.1 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 - opentelemetry-sdk == 0.11.dev0 + opentelemetry-test == 0.11b0 + opentelemetry-sdk == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py +++ b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-requests/setup.cfg b/ext/opentelemetry-ext-requests/setup.cfg index 714a1b3231..4ded4aa45d 100644 --- a/ext/opentelemetry-ext-requests/setup.cfg +++ b/ext/opentelemetry-ext-requests/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 requests ~= 2.0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 httpretty ~= 1.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-sqlalchemy/setup.cfg b/ext/opentelemetry-ext-sqlalchemy/setup.cfg index 76178b8245..153ebf831e 100644 --- a/ext/opentelemetry-ext-sqlalchemy/setup.cfg +++ b/ext/opentelemetry-ext-sqlalchemy/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 wrapt >= 1.11.2 sqlalchemy [options.extras_require] test = - opentelemetry-sdk == 0.11.dev0 + opentelemetry-sdk == 0.11b0 pytest [options.packages.find] diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py +++ b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-sqlite3/setup.cfg b/ext/opentelemetry-ext-sqlite3/setup.cfg index 95f60988ea..c986e1f067 100644 --- a/ext/opentelemetry-ext-sqlite3/setup.cfg +++ b/ext/opentelemetry-ext-sqlite3/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-ext-dbapi == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-ext-dbapi == 0.11b0 + opentelemetry-instrumentation == 0.11b0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py +++ b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-system-metrics/setup.cfg b/ext/opentelemetry-ext-system-metrics/setup.cfg index 4006290514..d26d13ae62 100644 --- a/ext/opentelemetry-ext-system-metrics/setup.cfg +++ b/ext/opentelemetry-ext-system-metrics/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 + opentelemetry-api == 0.11b0 psutil ~= 5.7.0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py +++ b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-wsgi/CHANGELOG.md b/ext/opentelemetry-ext-wsgi/CHANGELOG.md index da9df382f4..739ee7b0cc 100644 --- a/ext/opentelemetry-ext-wsgi/CHANGELOG.md +++ b/ext/opentelemetry-ext-wsgi/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - Set span status on wsgi errors ([#864](https://github.com/open-telemetry/opentelemetry-python/pull/864)) diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index e7f3087053..3938845145 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-instrumentation == 0.11b0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg index 164f0d84c2..a280c9cca3 100644 --- a/ext/opentelemetry-ext-zipkin/setup.cfg +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 0.11.dev0 - opentelemetry-sdk == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-sdk == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-instrumentation-aiopg/CHANGELOG.md b/ext/opentelemetry-instrumentation-aiopg/CHANGELOG.md index 3e04402cea..c62fac0617 100644 --- a/ext/opentelemetry-instrumentation-aiopg/CHANGELOG.md +++ b/ext/opentelemetry-instrumentation-aiopg/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - Initial release diff --git a/ext/opentelemetry-instrumentation-aiopg/setup.cfg b/ext/opentelemetry-instrumentation-aiopg/setup.cfg index 4dade66644..8c2e79eaca 100644 --- a/ext/opentelemetry-instrumentation-aiopg/setup.cfg +++ b/ext/opentelemetry-instrumentation-aiopg/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-ext-dbapi == 0.11.dev0 - opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-ext-dbapi == 0.11b0 + opentelemetry-instrumentation == 0.11b0 aiopg >= 0.13.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 [options.packages.find] where = src diff --git a/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py b/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py +++ b/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-instrumentation-fastapi/CHANGELOG.md b/ext/opentelemetry-instrumentation-fastapi/CHANGELOG.md index 684dece0c6..c8c5cea0d3 100644 --- a/ext/opentelemetry-instrumentation-fastapi/CHANGELOG.md +++ b/ext/opentelemetry-instrumentation-fastapi/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - Initial release ([#890](https://github.com/open-telemetry/opentelemetry-python/pull/890)) \ No newline at end of file diff --git a/ext/opentelemetry-instrumentation-fastapi/setup.cfg b/ext/opentelemetry-instrumentation-fastapi/setup.cfg index 5e7c2fafa6..dab3342804 100644 --- a/ext/opentelemetry-instrumentation-fastapi/setup.cfg +++ b/ext/opentelemetry-instrumentation-fastapi/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-ext-asgi == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-ext-asgi == 0.11b0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 fastapi ~= 0.58.1 requests ~= 2.23.0 # needed for testclient diff --git a/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py b/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py +++ b/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/ext/opentelemetry-instrumentation-starlette/setup.cfg b/ext/opentelemetry-instrumentation-starlette/setup.cfg index 4c777a18c5..26ae27198b 100644 --- a/ext/opentelemetry-instrumentation-starlette/setup.cfg +++ b/ext/opentelemetry-instrumentation-starlette/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11.dev0 - opentelemetry-ext-asgi == 0.11.dev0 + opentelemetry-api == 0.11b0 + opentelemetry-ext-asgi == 0.11b0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.11.dev0 + opentelemetry-test == 0.11b0 starlette ~= 0.13.0 requests ~= 2.23.0 # needed for testclient diff --git a/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py index 858e73960f..15e8d9536d 100644 --- a/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py +++ b/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index d634845991..da976ed37e 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - Return INVALID_SPAN if no TracerProvider set for get_current_span ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename record_error to record_exception diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 858e73960f..15e8d9536d 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index a51705177b..8744c8cdaa 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.11.dev0 + opentelemetry-api == 0.11b0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 858e73960f..15e8d9536d 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 858e73960f..15e8d9536d 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 6427b2c9b5..cbb96eabcd 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.11b0 + +Released 2020-07-28 + - Add support for resources and resource detector ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) - Rename record_error to record_exception diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index b4e7ed6a9d..cb2cb5184e 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.11.dev0 + opentelemetry-api == 0.11b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 858e73960f..15e8d9536d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.dev0" +__version__ = "0.11b0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index c41ce2492a..f3cf7aea33 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.11.dev0" +__version__ = "0.11b0" From 158d6c984750cf905165c7137e5cbc6790c947ff Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 28 Jul 2020 16:04:31 -0600 Subject: [PATCH 0479/1517] Move duplicated code to a dependency (#942) --- eachdist.ini | 1 + ext/opentelemetry-ext-boto/setup.cfg | 1 + .../src/opentelemetry/ext/boto/__init__.py | 55 +------------------ .../opentelemetry/ext/botocore/__init__.py | 31 +++++------ tox.ini | 2 +- 5 files changed, 20 insertions(+), 70 deletions(-) diff --git a/eachdist.ini b/eachdist.ini index c77d82f345..4a197ad291 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -10,6 +10,7 @@ sortfirst= ext/opentelemetry-ext-wsgi ext/opentelemetry-ext-dbapi ext/opentelemetry-ext-asgi + ext/opentelemetry-ext-botocore ext/* [lintroots] diff --git a/ext/opentelemetry-ext-boto/setup.cfg b/ext/opentelemetry-ext-boto/setup.cfg index 64a9932756..67956bad83 100644 --- a/ext/opentelemetry-ext-boto/setup.cfg +++ b/ext/opentelemetry-ext-boto/setup.cfg @@ -43,6 +43,7 @@ install_requires = boto ~= 2.0 opentelemetry-api == 0.11.dev0 opentelemetry-instrumentation == 0.11.dev0 + opentelemetry-ext-botocore == 0.11.dev0 [options.extras_require] test = diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py index 637200b078..f1f04723af 100644 --- a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py @@ -48,9 +48,10 @@ from inspect import currentframe from boto.connection import AWSAuthConnection, AWSQueryConnection -from wrapt import ObjectProxy, wrap_function_wrapper +from wrapt import wrap_function_wrapper from opentelemetry.ext.boto.version import __version__ +from opentelemetry.ext.botocore import add_span_arg_tags, unwrap from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.sdk.trace import Resource from opentelemetry.trace import SpanKind, get_tracer @@ -197,55 +198,3 @@ def _patched_auth_request(self, original_func, instance, args, kwargs): args, kwargs, ) - - -def truncate_arg_value(value, max_len=1024): - """Truncate values which are bytes and greater than ``max_len``. - Useful for parameters like "Body" in ``put_object`` operations. - """ - if isinstance(value, bytes) and len(value) > max_len: - return b"..." - - return value - - -def add_span_arg_tags(span, endpoint_name, args, args_names, args_traced): - if endpoint_name not in ["kms", "sts"]: - tags = dict( - (name, value) - for (name, value) in zip(args_names, args) - if name in args_traced - ) - tags = flatten_dict(tags) - for key, value in { - k: truncate_arg_value(v) - for k, v in tags.items() - if k not in {"s3": ["params.Body"]}.get(endpoint_name, []) - }.items(): - span.set_attribute(key, value) - - -def flatten_dict(dict_, sep=".", prefix=""): - """ - Returns a normalized dict of depth 1 with keys in order of embedding - """ - # adapted from https://stackoverflow.com/a/19647596 - return ( - { - prefix + sep + k if prefix else k: v - for kk, vv in dict_.items() - for k, v in flatten_dict(vv, sep, kk).items() - } - if isinstance(dict_, dict) - else {prefix: dict_} - ) - - -def unwrap(obj, attr): - function = getattr(obj, attr, None) - if ( - function - and isinstance(function, ObjectProxy) - and hasattr(function, "__wrapped__") - ): - setattr(obj, attr, function.__wrapped__) diff --git a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py index f9da154d97..70e790d9bf 100644 --- a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py +++ b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py @@ -160,6 +160,21 @@ def truncate_arg_value(value, max_len=1024): return value + def flatten_dict(dict_, sep=".", prefix=""): + """ + Returns a normalized dict of depth 1 with keys in order of embedding + """ + # adapted from https://stackoverflow.com/a/19647596 + return ( + { + prefix + sep + k if prefix else k: v + for kk, vv in dict_.items() + for k, v in flatten_dict(vv, sep, kk).items() + } + if isinstance(dict_, dict) + else {prefix: dict_} + ) + if endpoint_name not in {"kms", "sts"}: tags = dict( (name, value) @@ -175,22 +190,6 @@ def truncate_arg_value(value, max_len=1024): span.set_attribute(key, value) -def flatten_dict(dict_, sep=".", prefix=""): - """ - Returns a normalized dict of depth 1 with keys in order of embedding - """ - # adapted from https://stackoverflow.com/a/19647596 - return ( - { - prefix + sep + k if prefix else k: v - for kk, vv in dict_.items() - for k, v in flatten_dict(vv, sep, kk).items() - } - if isinstance(dict_, dict) - else {prefix: dict_} - ) - - def deep_getattr(obj, attr_string, default=None): """ Returns the attribute of ``obj`` at the dotted path given by diff --git a/tox.ini b/tox.ini index f956733d56..07a0bf42bd 100644 --- a/tox.ini +++ b/tox.ini @@ -256,11 +256,11 @@ commands_pre = asyncpg: pip install {toxinidir}/ext/opentelemetry-ext-asyncpg + boto: pip install {toxinidir}/ext/opentelemetry-ext-botocore[test] boto: pip install {toxinidir}/ext/opentelemetry-ext-boto[test] flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] - botocore: pip install {toxinidir}/opentelemetry-instrumentation botocore: pip install {toxinidir}/ext/opentelemetry-ext-botocore[test] dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi[test] From d67ad7b7344af0e58dafeaa5d881eb1d16ddaa46 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 28 Jul 2020 15:26:00 -0700 Subject: [PATCH 0480/1517] bumping version to 0.12.dev0 --- docs/examples/opentelemetry-example-app/setup.cfg | 8 ++++---- .../src/opentelemetry_example_app/version.py | 2 +- ext/opentelemetry-ext-aiohttp-client/setup.cfg | 4 ++-- .../src/opentelemetry/ext/aiohttp_client/version.py | 2 +- ext/opentelemetry-ext-asgi/setup.cfg | 4 ++-- .../src/opentelemetry/ext/asgi/version.py | 2 +- ext/opentelemetry-ext-asyncpg/setup.cfg | 6 +++--- .../src/opentelemetry/ext/asyncpg/version.py | 2 +- ext/opentelemetry-ext-boto/setup.cfg | 6 +++--- .../src/opentelemetry/ext/boto/version.py | 2 +- ext/opentelemetry-ext-botocore/setup.cfg | 6 +++--- .../src/opentelemetry/ext/botocore/version.py | 2 +- ext/opentelemetry-ext-celery/setup.cfg | 6 +++--- .../src/opentelemetry/ext/celery/version.py | 2 +- ext/opentelemetry-ext-datadog/setup.cfg | 4 ++-- .../src/opentelemetry/ext/datadog/version.py | 2 +- ext/opentelemetry-ext-dbapi/setup.cfg | 6 +++--- .../src/opentelemetry/ext/dbapi/version.py | 2 +- ext/opentelemetry-ext-django/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/django/version.py | 2 +- ext/opentelemetry-ext-elasticsearch/setup.cfg | 6 +++--- .../src/opentelemetry/ext/elasticsearch/version.py | 2 +- ext/opentelemetry-ext-flask/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/flask/version.py | 2 +- ext/opentelemetry-ext-grpc/setup.cfg | 6 +++--- .../src/opentelemetry/ext/grpc/version.py | 2 +- ext/opentelemetry-ext-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/ext/jaeger/version.py | 2 +- ext/opentelemetry-ext-jinja2/setup.cfg | 6 +++--- .../src/opentelemetry/ext/jinja2/version.py | 2 +- ext/opentelemetry-ext-mysql/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/mysql/version.py | 2 +- ext/opentelemetry-ext-opencensusexporter/setup.cfg | 4 ++-- .../src/opentelemetry/ext/opencensusexporter/version.py | 2 +- ext/opentelemetry-ext-opentracing-shim/setup.cfg | 4 ++-- .../src/opentelemetry/ext/opentracing_shim/version.py | 2 +- ext/opentelemetry-ext-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/ext/otlp/version.py | 2 +- ext/opentelemetry-ext-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/ext/prometheus/version.py | 2 +- ext/opentelemetry-ext-psycopg2/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/psycopg2/version.py | 2 +- ext/opentelemetry-ext-pymemcache/setup.cfg | 6 +++--- .../src/opentelemetry/ext/pymemcache/version.py | 2 +- ext/opentelemetry-ext-pymongo/setup.cfg | 6 +++--- .../src/opentelemetry/ext/pymongo/version.py | 2 +- ext/opentelemetry-ext-pymysql/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/pymysql/version.py | 2 +- ext/opentelemetry-ext-pyramid/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/pyramid/version.py | 2 +- ext/opentelemetry-ext-redis/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/redis/version.py | 2 +- ext/opentelemetry-ext-requests/setup.cfg | 6 +++--- .../src/opentelemetry/ext/requests/version.py | 2 +- ext/opentelemetry-ext-sqlalchemy/setup.cfg | 6 +++--- .../src/opentelemetry/ext/sqlalchemy/version.py | 2 +- ext/opentelemetry-ext-sqlite3/setup.cfg | 8 ++++---- .../src/opentelemetry/ext/sqlite3/version.py | 2 +- ext/opentelemetry-ext-system-metrics/setup.cfg | 4 ++-- .../src/opentelemetry/ext/system_metrics/version.py | 2 +- ext/opentelemetry-ext-wsgi/setup.cfg | 6 +++--- .../src/opentelemetry/ext/wsgi/version.py | 2 +- ext/opentelemetry-ext-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/ext/zipkin/version.py | 2 +- ext/opentelemetry-instrumentation-aiopg/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/aiopg/version.py | 2 +- ext/opentelemetry-instrumentation-fastapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/fastapi/version.py | 2 +- ext/opentelemetry-instrumentation-starlette/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/starlette/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 77 files changed, 148 insertions(+), 148 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index 8905023bbf..f7dd1eccdf 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -43,10 +43,10 @@ zip_safe = False include_package_data = True install_requires = typing; python_version<'3.5' - opentelemetry-api == 0.11b0 - opentelemetry-sdk == 0.11b0 - opentelemetry-ext-requests == 0.11b0 - opentelemetry-ext-flask == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-sdk == 0.12.dev0 + opentelemetry-ext-requests == 0.12.dev0 + opentelemetry-ext-flask == 0.12.dev0 flask requests protobuf~=3.11 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py index 15e8d9536d..780a92b6a1 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-aiohttp-client/setup.cfg b/ext/opentelemetry-ext-aiohttp-client/setup.cfg index 9044083ff7..d2be7e8c6e 100644 --- a/ext/opentelemetry-ext-aiohttp-client/setup.cfg +++ b/ext/opentelemetry-ext-aiohttp-client/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api >= 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 aiohttp ~= 3.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py index ae93e51455..8d947df443 100644 --- a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py +++ b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-asgi/setup.cfg b/ext/opentelemetry-ext-asgi/setup.cfg index 9183287f0d..175e264995 100644 --- a/ext/opentelemetry-ext-asgi/setup.cfg +++ b/ext/opentelemetry-ext-asgi/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 asgiref ~= 3.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py +++ b/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-asyncpg/setup.cfg b/ext/opentelemetry-ext-asyncpg/setup.cfg index 2eaa54b847..bf1670172b 100644 --- a/ext/opentelemetry-ext-asyncpg/setup.cfg +++ b/ext/opentelemetry-ext-asyncpg/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 asyncpg >= 0.12.0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py b/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py +++ b/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-boto/setup.cfg b/ext/opentelemetry-ext-boto/setup.cfg index a9e0ec9f4e..9d447b8135 100644 --- a/ext/opentelemetry-ext-boto/setup.cfg +++ b/ext/opentelemetry-ext-boto/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = boto ~= 2.0 - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 [options.extras_require] test = boto~=2.0 moto~=1.0 - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-botocore/setup.cfg b/ext/opentelemetry-ext-botocore/setup.cfg index 5e2d0e0b35..0d8b9e4ad4 100644 --- a/ext/opentelemetry-ext-botocore/setup.cfg +++ b/ext/opentelemetry-ext-botocore/setup.cfg @@ -41,13 +41,13 @@ package_dir= packages=find_namespace: install_requires = botocore ~= 1.0 - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 [options.extras_require] test = moto ~= 1.0 - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py +++ b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-celery/setup.cfg b/ext/opentelemetry-ext-celery/setup.cfg index d70e0f7b01..39e019e83a 100644 --- a/ext/opentelemetry-ext-celery/setup.cfg +++ b/ext/opentelemetry-ext-celery/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 celery ~= 4.0 [options.extras_require] test = pytest - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py b/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py +++ b/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-datadog/setup.cfg b/ext/opentelemetry-ext-datadog/setup.cfg index 62ffd1c907..95b0bb0857 100644 --- a/ext/opentelemetry-ext-datadog/setup.cfg +++ b/ext/opentelemetry-ext-datadog/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = ddtrace>=0.34.0 - opentelemetry-api == 0.11b0 - opentelemetry-sdk == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-sdk == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 5ee74e322a..34542525dd 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-django/setup.cfg b/ext/opentelemetry-ext-django/setup.cfg index 8474194ef5..602a2cf7ba 100644 --- a/ext/opentelemetry-ext-django/setup.cfg +++ b/ext/opentelemetry-ext-django/setup.cfg @@ -41,13 +41,13 @@ package_dir= packages=find_namespace: install_requires = django >= 1.10 - opentelemetry-ext-wsgi == 0.11b0 - opentelemetry-instrumentation == 0.11b0 - opentelemetry-api == 0.11b0 + opentelemetry-ext-wsgi == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.12.dev0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-elasticsearch/setup.cfg b/ext/opentelemetry-ext-elasticsearch/setup.cfg index 46f32b13ef..877f62872c 100644 --- a/ext/opentelemetry-ext-elasticsearch/setup.cfg +++ b/ext/opentelemetry-ext-elasticsearch/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 wrapt >= 1.0.0, < 2.0.0 elasticsearch >= 2.0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 elasticsearch-dsl >= 2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py b/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py +++ b/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index 3d99a53a8c..e4b978645b 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-ext-wsgi == 0.11b0 - opentelemetry-instrumentation == 0.11b0 - opentelemetry-api == 0.11b0 + opentelemetry-ext-wsgi == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.12.dev0 [options.extras_require] test = flask~=1.0 - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index 4dd808a19e..0a4e4e5b67 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 + opentelemetry-api == 0.12.dev0 grpcio ~= 1.27 [options.extras_require] test = - opentelemetry-test == 0.11b0 - opentelemetry-sdk == 0.11b0 + opentelemetry-test == 0.12.dev0 + opentelemetry-sdk == 0.12.dev0 protobuf == 3.12.2 [options.packages.find] diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/ext/opentelemetry-ext-jaeger/setup.cfg index 1c0a006529..fc581301b6 100644 --- a/ext/opentelemetry-ext-jaeger/setup.cfg +++ b/ext/opentelemetry-ext-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.11b0 - opentelemetry-sdk == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-sdk == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index 13bf32b8f8..4a2d033215 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-jinja2/setup.cfg b/ext/opentelemetry-ext-jinja2/setup.cfg index 1fbd74a3d0..fc0222a0e4 100644 --- a/ext/opentelemetry-ext-jinja2/setup.cfg +++ b/ext/opentelemetry-ext-jinja2/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py +++ b/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 0e7209b168..e114ae1dbe 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-ext-dbapi == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-ext-dbapi == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-opencensusexporter/setup.cfg b/ext/opentelemetry-ext-opencensusexporter/setup.cfg index 1cf9d1a3cf..0a3376083a 100644 --- a/ext/opentelemetry-ext-opencensusexporter/setup.cfg +++ b/ext/opentelemetry-ext-opencensusexporter/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.11b0 - opentelemetry-sdk == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-sdk == 0.12.dev0 protobuf >= 3.8.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py +++ b/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg index d2b9cf28cc..f0878b82b6 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.11b0 + opentelemetry-api == 0.12.dev0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-otlp/setup.cfg b/ext/opentelemetry-ext-otlp/setup.cfg index b6f7068d7f..b6bfac669b 100644 --- a/ext/opentelemetry-ext-otlp/setup.cfg +++ b/ext/opentelemetry-ext-otlp/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.11b0 - opentelemetry-sdk == 0.11b0 - opentelemetry-proto == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-sdk == 0.12.dev0 + opentelemetry-proto == 0.12.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py +++ b/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/ext/opentelemetry-ext-prometheus/setup.cfg index 3c55f3121c..3571f93c54 100644 --- a/ext/opentelemetry-ext-prometheus/setup.cfg +++ b/ext/opentelemetry-ext-prometheus/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.11b0 - opentelemetry-sdk == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-sdk == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index 9cf7b560c7..73d541cad6 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-ext-dbapi == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-ext-dbapi == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-pymemcache/setup.cfg b/ext/opentelemetry-ext-pymemcache/setup.cfg index 0fbc8d60ad..798988461c 100644 --- a/ext/opentelemetry-ext-pymemcache/setup.cfg +++ b/ext/opentelemetry-ext-pymemcache/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 pymemcache ~= 1.3 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py b/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py +++ b/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index a1f7284605..4aae442aa1 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 pymongo ~= 3.1 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-pymysql/setup.cfg b/ext/opentelemetry-ext-pymysql/setup.cfg index adb51131b7..9d8ccdff6f 100644 --- a/ext/opentelemetry-ext-pymysql/setup.cfg +++ b/ext/opentelemetry-ext-pymysql/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-ext-dbapi == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-ext-dbapi == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 PyMySQL ~= 0.9.3 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-pyramid/setup.cfg b/ext/opentelemetry-ext-pyramid/setup.cfg index 1255dbad14..21a4910ac4 100644 --- a/ext/opentelemetry-ext-pyramid/setup.cfg +++ b/ext/opentelemetry-ext-pyramid/setup.cfg @@ -41,15 +41,15 @@ package_dir= packages=find_namespace: install_requires = pyramid >= 1.7 - opentelemetry-instrumentation == 0.11b0 - opentelemetry-api == 0.11b0 - opentelemetry-ext-wsgi == 0.11b0 + opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.12.dev0 + opentelemetry-ext-wsgi == 0.12.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = werkzeug == 0.16.1 - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py +++ b/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-redis/setup.cfg b/ext/opentelemetry-ext-redis/setup.cfg index e654b81add..0b245f341a 100644 --- a/ext/opentelemetry-ext-redis/setup.cfg +++ b/ext/opentelemetry-ext-redis/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 redis >= 2.6 wrapt >= 1.12.1 [options.extras_require] test = - opentelemetry-test == 0.11b0 - opentelemetry-sdk == 0.11b0 + opentelemetry-test == 0.12.dev0 + opentelemetry-sdk == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py +++ b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-requests/setup.cfg b/ext/opentelemetry-ext-requests/setup.cfg index 4ded4aa45d..52d6f96739 100644 --- a/ext/opentelemetry-ext-requests/setup.cfg +++ b/ext/opentelemetry-ext-requests/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 requests ~= 2.0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 httpretty ~= 1.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-sqlalchemy/setup.cfg b/ext/opentelemetry-ext-sqlalchemy/setup.cfg index 153ebf831e..16416f37ef 100644 --- a/ext/opentelemetry-ext-sqlalchemy/setup.cfg +++ b/ext/opentelemetry-ext-sqlalchemy/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 wrapt >= 1.11.2 sqlalchemy [options.extras_require] test = - opentelemetry-sdk == 0.11b0 + opentelemetry-sdk == 0.12.dev0 pytest [options.packages.find] diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py +++ b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-sqlite3/setup.cfg b/ext/opentelemetry-ext-sqlite3/setup.cfg index c986e1f067..51f6b2f30d 100644 --- a/ext/opentelemetry-ext-sqlite3/setup.cfg +++ b/ext/opentelemetry-ext-sqlite3/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-ext-dbapi == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-ext-dbapi == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py +++ b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-system-metrics/setup.cfg b/ext/opentelemetry-ext-system-metrics/setup.cfg index d26d13ae62..08a1fe8f1c 100644 --- a/ext/opentelemetry-ext-system-metrics/setup.cfg +++ b/ext/opentelemetry-ext-system-metrics/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 + opentelemetry-api == 0.12.dev0 psutil ~= 5.7.0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py +++ b/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index 3938845145..97445a847c 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg index a280c9cca3..7674cab536 100644 --- a/ext/opentelemetry-ext-zipkin/setup.cfg +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 0.11b0 - opentelemetry-sdk == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-sdk == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-instrumentation-aiopg/setup.cfg b/ext/opentelemetry-instrumentation-aiopg/setup.cfg index 8c2e79eaca..f2428301b5 100644 --- a/ext/opentelemetry-instrumentation-aiopg/setup.cfg +++ b/ext/opentelemetry-instrumentation-aiopg/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-ext-dbapi == 0.11b0 - opentelemetry-instrumentation == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-ext-dbapi == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 aiopg >= 0.13.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py b/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py +++ b/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-instrumentation-fastapi/setup.cfg b/ext/opentelemetry-instrumentation-fastapi/setup.cfg index dab3342804..b700b3d724 100644 --- a/ext/opentelemetry-instrumentation-fastapi/setup.cfg +++ b/ext/opentelemetry-instrumentation-fastapi/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-ext-asgi == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-ext-asgi == 0.12.dev0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 fastapi ~= 0.58.1 requests ~= 2.23.0 # needed for testclient diff --git a/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py b/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py +++ b/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/ext/opentelemetry-instrumentation-starlette/setup.cfg b/ext/opentelemetry-instrumentation-starlette/setup.cfg index 26ae27198b..f1613a8cd8 100644 --- a/ext/opentelemetry-instrumentation-starlette/setup.cfg +++ b/ext/opentelemetry-instrumentation-starlette/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.11b0 - opentelemetry-ext-asgi == 0.11b0 + opentelemetry-api == 0.12.dev0 + opentelemetry-ext-asgi == 0.12.dev0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.11b0 + opentelemetry-test == 0.12.dev0 starlette ~= 0.13.0 requests ~= 2.23.0 # needed for testclient diff --git a/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py index 15e8d9536d..780a92b6a1 100644 --- a/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py +++ b/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 15e8d9536d..780a92b6a1 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 8744c8cdaa..4dee7c3cf8 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.11b0 + opentelemetry-api == 0.12.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 15e8d9536d..780a92b6a1 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 15e8d9536d..780a92b6a1 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index cb2cb5184e..6ea60117a1 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.11b0 + opentelemetry-api == 0.12.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 15e8d9536d..780a92b6a1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11b0" +__version__ = "0.12.dev0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index f3cf7aea33..1ad7bc603e 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.11b0" +__version__ = "0.12.dev0" From eec050ba92340459be8c489fe5e906fe8ba58899 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 29 Jul 2020 10:03:46 -0700 Subject: [PATCH 0481/1517] Rename exporter packages from "ext" to "exporter" (#953) --- .flake8 | 4 +- .github/workflows/docs.yml | 1 + docs/conf.py | 9 ++- docs/examples/cloud_monitoring/README.rst | 48 ----------- .../cloud_monitoring/basic_metrics.py | 43 ---------- docs/examples/cloud_trace_exporter/README.rst | 79 ------------------- .../cloud_trace_exporter/basic_trace.py | 14 ---- docs/examples/datadog_exporter/README.rst | 4 +- .../datadog_exporter/basic_example.py | 2 +- docs/examples/datadog_exporter/client.py | 2 +- docs/examples/datadog_exporter/server.py | 4 +- .../opencensus-exporter-metrics/README.rst | 4 +- .../opencensus-exporter-metrics/collector.py | 2 +- .../opencensus-exporter-tracer/README.rst | 4 +- .../opencensus-exporter-tracer/collector.py | 2 +- docs/examples/opentracing/main.py | 2 +- docs/examples/opentracing/requirements.txt | 2 +- docs/{ext => exporter}/datadog/datadog.rst | 2 +- docs/{ext => exporter}/jaeger/jaeger.rst | 4 +- .../opencensus/opencensus.rst} | 2 +- docs/{ext => exporter}/otlp/otlp.rst | 2 +- .../prometheus/prometheus.rst | 2 +- docs/{ext => exporter}/zipkin/zipkin.rst | 2 +- .../ext/cloud_monitoring/cloud_monitoring.rst | 7 -- docs/ext/cloud_trace/cloud_trace.rst | 7 -- docs/getting_started/jaeger_example.py | 2 +- docs/getting_started/prometheus_example.py | 2 +- docs/index.rst | 12 ++- eachdist.ini | 1 + .../CHANGELOG.md | 3 + .../README.rst | 6 +- .../opentelemetry-exporter-datadog}/setup.cfg | 4 +- .../opentelemetry-exporter-datadog}/setup.py | 3 +- .../exporter}/datadog/__init__.py | 4 +- .../exporter}/datadog/constants.py | 0 .../exporter}/datadog/exporter.py | 0 .../exporter}/datadog/propagator.py | 0 .../exporter}/datadog/spanprocessor.py | 0 .../exporter}/datadog/version.py | 0 .../tests/__init__.py | 0 .../tests/test_datadog_exporter.py | 2 +- .../tests/test_datadog_format.py | 2 +- .../CHANGELOG.md | 3 + .../opentelemetry-exporter-jaeger}/LICENSE | 0 .../MANIFEST.in | 0 .../opentelemetry-exporter-jaeger}/README.rst | 8 +- .../examples/jaeger_exporter_example.py | 2 +- .../opentelemetry-exporter-jaeger}/setup.cfg | 4 +- .../opentelemetry-exporter-jaeger}/setup.py | 2 +- .../exporter}/jaeger/__init__.py | 6 +- .../exporter}/jaeger/gen/__init__.py | 0 .../exporter}/jaeger/gen/agent/Agent-remote | 0 .../exporter}/jaeger/gen/agent/Agent.py | 0 .../exporter}/jaeger/gen/agent/__init__.py | 0 .../exporter}/jaeger/gen/agent/constants.py | 0 .../exporter}/jaeger/gen/agent/ttypes.py | 0 .../jaeger/gen/jaeger/Collector-remote | 0 .../exporter}/jaeger/gen/jaeger/Collector.py | 0 .../exporter}/jaeger/gen/jaeger/__init__.py | 0 .../exporter}/jaeger/gen/jaeger/constants.py | 0 .../exporter}/jaeger/gen/jaeger/ttypes.py | 0 .../gen/zipkincore/ZipkinCollector-remote | 0 .../jaeger/gen/zipkincore/ZipkinCollector.py | 0 .../jaeger/gen/zipkincore/__init__.py | 0 .../jaeger/gen/zipkincore/constants.py | 0 .../exporter}/jaeger/gen/zipkincore/ttypes.py | 0 .../opentelemetry/exporter}/jaeger/version.py | 0 .../tests/__init__.py | 0 .../tests/test_jaeger_exporter.py | 4 +- .../thrift/agent.thrift | 0 .../thrift/jaeger.thrift | 0 .../thrift/zipkincore.thrift | 0 .../CHANGELOG.md | 3 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 8 +- .../setup.cfg | 4 +- .../setup.py | 26 ++++++ .../exporter/opencensus}/__init__.py | 0 .../opencensus}/metrics_exporter/__init__.py | 2 +- .../opencensus}/trace_exporter/__init__.py | 2 +- .../exporter/opencensus}/util.py | 2 +- .../exporter/opencensus}/version.py | 0 .../tests/__init__.py | 0 .../test_otcollector_metrics_exporter.py | 4 +- .../tests/test_otcollector_trace_exporter.py | 6 +- .../opentelemetry-exporter-otlp}/CHANGELOG.md | 3 + .../opentelemetry-exporter-otlp}/LICENSE | 0 .../opentelemetry-exporter-otlp}/MANIFEST.in | 0 .../opentelemetry-exporter-otlp}/README.rst | 8 +- .../opentelemetry-exporter-otlp}/setup.cfg | 4 +- .../opentelemetry-exporter-otlp}/setup.py | 3 +- .../opentelemetry/exporter}/otlp/__init__.py | 2 +- .../exporter}/otlp/trace_exporter/__init__.py | 0 .../opentelemetry/exporter}/otlp/version.py | 0 .../tests/__init__.py | 0 .../tests/test_otlp_trace_exporter.py | 10 +-- .../CHANGELOG.md | 12 +++ .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 8 +- .../setup.cfg | 4 +- .../setup.py | 26 ++++++ .../exporter}/prometheus/__init__.py | 2 +- .../exporter}/prometheus/version.py | 0 .../tests/__init__.py | 0 .../tests/test_prometheus_exporter.py | 4 +- .../CHANGELOG.md | 3 + .../opentelemetry-exporter-zipkin}/LICENSE | 0 .../MANIFEST.in | 0 .../opentelemetry-exporter-zipkin}/README.rst | 8 +- .../opentelemetry-exporter-zipkin}/setup.cfg | 4 +- .../opentelemetry-exporter-zipkin}/setup.py | 2 +- .../exporter}/zipkin/__init__.py | 2 +- .../opentelemetry/exporter}/zipkin/version.py | 0 .../tests/__init__.py | 0 .../tests/test_zipkin_exporter.py | 2 +- .../test_opencensusexporter_functional.py | 2 +- .../setup.py | 26 ------ ext/opentelemetry-ext-prometheus/CHANGELOG.md | 9 --- ext/opentelemetry-ext-zipkin/setup.py | 26 ------ pyproject.toml | 2 +- scripts/build.sh | 2 +- scripts/coverage.sh | 6 +- tox.ini | 44 +++++------ 125 files changed, 221 insertions(+), 384 deletions(-) delete mode 100644 docs/examples/cloud_monitoring/README.rst delete mode 100644 docs/examples/cloud_monitoring/basic_metrics.py delete mode 100644 docs/examples/cloud_trace_exporter/README.rst delete mode 100644 docs/examples/cloud_trace_exporter/basic_trace.py rename docs/{ext => exporter}/datadog/datadog.rst (71%) rename docs/{ext => exporter}/jaeger/jaeger.rst (64%) rename docs/{ext/opencensusexporter/opencensusexporter.rst => exporter/opencensus/opencensus.rst} (64%) rename docs/{ext => exporter}/otlp/otlp.rst (72%) rename docs/{ext => exporter}/prometheus/prometheus.rst (71%) rename docs/{ext => exporter}/zipkin/zipkin.rst (71%) delete mode 100644 docs/ext/cloud_monitoring/cloud_monitoring.rst delete mode 100644 docs/ext/cloud_trace/cloud_trace.rst rename {ext/opentelemetry-ext-datadog => exporter/opentelemetry-exporter-datadog}/CHANGELOG.md (55%) rename {ext/opentelemetry-ext-datadog => exporter/opentelemetry-exporter-datadog}/README.rst (71%) rename {ext/opentelemetry-ext-datadog => exporter/opentelemetry-exporter-datadog}/setup.cfg (91%) rename {ext/opentelemetry-ext-jaeger => exporter/opentelemetry-exporter-datadog}/setup.py (91%) rename {ext/opentelemetry-ext-datadog/src/opentelemetry/ext => exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter}/datadog/__init__.py (93%) rename {ext/opentelemetry-ext-datadog/src/opentelemetry/ext => exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter}/datadog/constants.py (100%) rename {ext/opentelemetry-ext-datadog/src/opentelemetry/ext => exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter}/datadog/exporter.py (100%) rename {ext/opentelemetry-ext-datadog/src/opentelemetry/ext => exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter}/datadog/propagator.py (100%) rename {ext/opentelemetry-ext-datadog/src/opentelemetry/ext => exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter}/datadog/spanprocessor.py (100%) rename {ext/opentelemetry-ext-datadog/src/opentelemetry/ext => exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter}/datadog/version.py (100%) rename {ext/opentelemetry-ext-datadog => exporter/opentelemetry-exporter-datadog}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-datadog => exporter/opentelemetry-exporter-datadog}/tests/test_datadog_exporter.py (99%) rename {ext/opentelemetry-ext-datadog => exporter/opentelemetry-exporter-datadog}/tests/test_datadog_format.py (98%) rename {ext/opentelemetry-ext-jaeger => exporter/opentelemetry-exporter-jaeger}/CHANGELOG.md (87%) rename {ext/opentelemetry-ext-jaeger => exporter/opentelemetry-exporter-jaeger}/LICENSE (100%) rename {ext/opentelemetry-ext-jaeger => exporter/opentelemetry-exporter-jaeger}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-jaeger => exporter/opentelemetry-exporter-jaeger}/README.rst (66%) rename {ext/opentelemetry-ext-jaeger => exporter/opentelemetry-exporter-jaeger}/examples/jaeger_exporter_example.py (97%) rename {ext/opentelemetry-ext-jaeger => exporter/opentelemetry-exporter-jaeger}/setup.cfg (94%) rename {ext/opentelemetry-ext-otlp => exporter/opentelemetry-exporter-jaeger}/setup.py (91%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/__init__.py (98%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/__init__.py (100%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/agent/Agent-remote (100%) mode change 100755 => 100644 rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/agent/Agent.py (100%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/agent/__init__.py (100%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/agent/constants.py (100%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/agent/ttypes.py (100%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/jaeger/Collector-remote (100%) mode change 100755 => 100644 rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/jaeger/Collector.py (100%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/jaeger/__init__.py (100%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/jaeger/constants.py (100%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/jaeger/ttypes.py (100%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/zipkincore/ZipkinCollector-remote (100%) mode change 100755 => 100644 rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/zipkincore/ZipkinCollector.py (100%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/zipkincore/__init__.py (100%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/zipkincore/constants.py (100%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/gen/zipkincore/ttypes.py (100%) rename {ext/opentelemetry-ext-jaeger/src/opentelemetry/ext => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter}/jaeger/version.py (100%) rename {ext/opentelemetry-ext-jaeger => exporter/opentelemetry-exporter-jaeger}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-jaeger => exporter/opentelemetry-exporter-jaeger}/tests/test_jaeger_exporter.py (99%) rename {ext/opentelemetry-ext-jaeger => exporter/opentelemetry-exporter-jaeger}/thrift/agent.thrift (100%) rename {ext/opentelemetry-ext-jaeger => exporter/opentelemetry-exporter-jaeger}/thrift/jaeger.thrift (100%) rename {ext/opentelemetry-ext-jaeger => exporter/opentelemetry-exporter-jaeger}/thrift/zipkincore.thrift (100%) rename {ext/opentelemetry-ext-opencensusexporter => exporter/opentelemetry-exporter-opencensus}/CHANGELOG.md (61%) rename {ext/opentelemetry-ext-opencensusexporter => exporter/opentelemetry-exporter-opencensus}/LICENSE (100%) rename {ext/opentelemetry-ext-opencensusexporter => exporter/opentelemetry-exporter-opencensus}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-opencensusexporter => exporter/opentelemetry-exporter-opencensus}/README.rst (57%) rename {ext/opentelemetry-ext-opencensusexporter => exporter/opentelemetry-exporter-opencensus}/setup.cfg (94%) create mode 100644 exporter/opentelemetry-exporter-opencensus/setup.py rename {ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter => exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus}/__init__.py (100%) rename {ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter => exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus}/metrics_exporter/__init__.py (98%) rename {ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter => exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus}/trace_exporter/__init__.py (99%) rename {ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter => exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus}/util.py (98%) rename {ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter => exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus}/version.py (100%) rename {ext/opentelemetry-ext-opencensusexporter => exporter/opentelemetry-exporter-opencensus}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-opencensusexporter => exporter/opentelemetry-exporter-opencensus}/tests/test_otcollector_metrics_exporter.py (98%) rename {ext/opentelemetry-ext-opencensusexporter => exporter/opentelemetry-exporter-opencensus}/tests/test_otcollector_trace_exporter.py (98%) rename {ext/opentelemetry-ext-otlp => exporter/opentelemetry-exporter-otlp}/CHANGELOG.md (65%) rename {ext/opentelemetry-ext-otlp => exporter/opentelemetry-exporter-otlp}/LICENSE (100%) rename {ext/opentelemetry-ext-otlp => exporter/opentelemetry-exporter-otlp}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-otlp => exporter/opentelemetry-exporter-otlp}/README.rst (70%) rename {ext/opentelemetry-ext-otlp => exporter/opentelemetry-exporter-otlp}/setup.cfg (95%) rename {ext/opentelemetry-ext-datadog => exporter/opentelemetry-exporter-otlp}/setup.py (92%) rename {ext/opentelemetry-ext-otlp/src/opentelemetry/ext => exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter}/otlp/__init__.py (95%) rename {ext/opentelemetry-ext-otlp/src/opentelemetry/ext => exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter}/otlp/trace_exporter/__init__.py (100%) rename {ext/opentelemetry-ext-otlp/src/opentelemetry/ext => exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter}/otlp/version.py (100%) rename {ext/opentelemetry-ext-otlp => exporter/opentelemetry-exporter-otlp}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-otlp => exporter/opentelemetry-exporter-otlp}/tests/test_otlp_trace_exporter.py (97%) create mode 100644 exporter/opentelemetry-exporter-prometheus/CHANGELOG.md rename {ext/opentelemetry-ext-prometheus => exporter/opentelemetry-exporter-prometheus}/LICENSE (100%) rename {ext/opentelemetry-ext-prometheus => exporter/opentelemetry-exporter-prometheus}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-prometheus => exporter/opentelemetry-exporter-prometheus}/README.rst (58%) rename {ext/opentelemetry-ext-prometheus => exporter/opentelemetry-exporter-prometheus}/setup.cfg (94%) create mode 100644 exporter/opentelemetry-exporter-prometheus/setup.py rename {ext/opentelemetry-ext-prometheus/src/opentelemetry/ext => exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter}/prometheus/__init__.py (98%) rename {ext/opentelemetry-ext-prometheus/src/opentelemetry/ext => exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter}/prometheus/version.py (100%) rename {ext/opentelemetry-ext-prometheus => exporter/opentelemetry-exporter-prometheus}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-prometheus => exporter/opentelemetry-exporter-prometheus}/tests/test_prometheus_exporter.py (97%) rename {ext/opentelemetry-ext-zipkin => exporter/opentelemetry-exporter-zipkin}/CHANGELOG.md (73%) rename {ext/opentelemetry-ext-zipkin => exporter/opentelemetry-exporter-zipkin}/LICENSE (100%) rename {ext/opentelemetry-ext-zipkin => exporter/opentelemetry-exporter-zipkin}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-zipkin => exporter/opentelemetry-exporter-zipkin}/README.rst (59%) rename {ext/opentelemetry-ext-zipkin => exporter/opentelemetry-exporter-zipkin}/setup.cfg (95%) rename {ext/opentelemetry-ext-prometheus => exporter/opentelemetry-exporter-zipkin}/setup.py (91%) rename {ext/opentelemetry-ext-zipkin/src/opentelemetry/ext => exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter}/zipkin/__init__.py (99%) rename {ext/opentelemetry-ext-zipkin/src/opentelemetry/ext => exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter}/zipkin/version.py (100%) rename {ext/opentelemetry-ext-zipkin => exporter/opentelemetry-exporter-zipkin}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-zipkin => exporter/opentelemetry-exporter-zipkin}/tests/test_zipkin_exporter.py (99%) delete mode 100644 ext/opentelemetry-ext-opencensusexporter/setup.py delete mode 100644 ext/opentelemetry-ext-prometheus/CHANGELOG.md delete mode 100644 ext/opentelemetry-ext-zipkin/setup.py diff --git a/.flake8 b/.flake8 index 8555d626c6..2780677a64 100644 --- a/.flake8 +++ b/.flake8 @@ -16,8 +16,8 @@ exclude = venv*/ target __pycache__ - ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/ - ext/opentelemetry-ext-jaeger/build/* + exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/ + exporter/opentelemetry-exporter-jaeger/build/* docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ docs/examples/opentelemetry-example-app/build/* opentelemetry-proto/build/* diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 59c825e7e2..1860c696a0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,6 +6,7 @@ on: - master paths: - 'docs/**' + - 'exporter/**' - 'ext/**' - 'opentelemetry-python/opentelemetry-api/src/opentelemetry/**' - 'opentelemetry-python/opentelemetry-sdk/src/opentelemetry/sdk/**' diff --git a/docs/conf.py b/docs/conf.py index 74ae754c60..d3af10b7d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,13 +30,20 @@ os.path.abspath("../opentelemetry-instrumentation/src/"), ] +exp = "../exporter" +exp_dirs = [ + os.path.abspath("/".join(["../exporter", f, "src"])) + for f in listdir(exp) + if isdir(join(exp, f)) +] + ext = "../ext" ext_dirs = [ os.path.abspath("/".join(["../ext", f, "src"])) for f in listdir(ext) if isdir(join(ext, f)) ] -sys.path[:0] = source_dirs + ext_dirs +sys.path[:0] = source_dirs + exp_dirs + ext_dirs # -- Project information ----------------------------------------------------- diff --git a/docs/examples/cloud_monitoring/README.rst b/docs/examples/cloud_monitoring/README.rst deleted file mode 100644 index 818446dc15..0000000000 --- a/docs/examples/cloud_monitoring/README.rst +++ /dev/null @@ -1,48 +0,0 @@ -Cloud Monitoring Exporter Example -================================= - -These examples show how to use OpenTelemetry to send metrics data to Cloud Monitoring. - - -Basic Example -------------- - -To use this exporter you first need to: - * `Create a Google Cloud project `_. - * Enable the Cloud Monitoring API (aka Stackdriver Monitoring API) in the project `here `_. - * Enable `Default Application Credentials `_. - -* Installation - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - pip install opentelemetry-exporter-cloud-monitoring - -* Run example - -.. literalinclude:: basic_metrics.py - :language: python - :lines: 1- - -Viewing Output --------------------------- - -After running the example: - * Go to the `Cloud Monitoring Metrics Explorer page `_. - * In "Find resource type and metric" enter "OpenTelemetry/request_counter". - * You can filter by labels and change the graphical output here as well. - -Troubleshooting --------------------------- - -``One or more points were written more frequently than the maximum sampling period configured for the metric`` -############################################################################################################## - -Currently, Cloud Monitoring allows one write every 10 seconds for any unique tuple (metric_name, metric_label_value_1, metric_label_value_2, ...). The exporter should rate limit on its own but issues arise if: - - * You are restarting the server more than once every 10 seconds. - * You have a multiple exporters (possibly on different threads) writing to the same tuple. - -For both cases, you can pass ``add_unique_identifier=True`` to the CloudMonitoringMetricsExporter constructor. This adds a UUID label_value, making the tuple unique again. For the first case, you can also choose to just wait longer than 10 seconds between restarts. diff --git a/docs/examples/cloud_monitoring/basic_metrics.py b/docs/examples/cloud_monitoring/basic_metrics.py deleted file mode 100644 index fa00fc068b..0000000000 --- a/docs/examples/cloud_monitoring/basic_metrics.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -from opentelemetry import metrics -from opentelemetry.exporter.cloud_monitoring import ( - CloudMonitoringMetricsExporter, -) -from opentelemetry.sdk.metrics import Counter, MeterProvider - -metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__) -metrics.get_meter_provider().start_pipeline( - meter, CloudMonitoringMetricsExporter(), 5 -) - -requests_counter = meter.create_metric( - name="request_counter", - description="number of requests", - unit="1", - value_type=int, - metric_type=Counter, - label_keys=("environment"), -) - -staging_labels = {"environment": "staging"} - -for i in range(20): - requests_counter.add(25, staging_labels) - time.sleep(10) diff --git a/docs/examples/cloud_trace_exporter/README.rst b/docs/examples/cloud_trace_exporter/README.rst deleted file mode 100644 index 65674759b6..0000000000 --- a/docs/examples/cloud_trace_exporter/README.rst +++ /dev/null @@ -1,79 +0,0 @@ -Cloud Trace Exporter Example -============================ - -These examples show how to use OpenTelemetry to send tracing data to Cloud Trace. - - -Basic Example -------------- - -To use this exporter you first need to: - * A Google Cloud project. You can `create one here `_. - * Enable Cloud Trace API (listed in the Cloud Console as Stackdriver Trace API) in the project `here `_. - * If the page says "API Enabled" then you're done! No need to do anything. - * Enable Default Application Credentials by creating setting `GOOGLE_APPLICATION_CREDENTIALS `_ or by `installing gcloud sdk `_ and calling ``gcloud auth application-default login``. - -* Installation - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - pip install opentelemetry-exporter-cloud-trace - -* Run an example locally - -.. literalinclude:: basic_trace.py - :language: python - :lines: 1- - -Checking Output --------------------------- - -After running any of these examples, you can go to `Cloud Trace overview `_ to see the results. - - -Further Reading --------------------------- - -* `More information about exporters in general `_ - -Troubleshooting --------------------------- - -Running basic_trace.py hangs: -############################# - * Make sure you've setup Application Default Credentials. Either run ``gcloud auth application-default login`` or set the ``GOOGLE_APPLICATION_CREDENTIALS`` environment variable to be a path to a service account token file. - -Getting error ``google.api_core.exceptions.ResourceExhausted: 429 Resource has been exhausted``: -################################################################################################ - * Check that you've enabled the `Cloud Trace (Stackdriver Trace) API `_ - -bash: pip: command not found: -############################# - * `Install pip `_ - * If your machine uses python2 by default, pip will also be the python2 version. Try using ``pip3`` instead of ``pip``. - -pip install is hanging -###################### -Try upgrading pip - -.. code-block:: sh - - pip install --upgrade pip - -``pip install grcpio`` has been known to hang when you aren't using an upgraded version. - -ImportError: No module named opentelemetry -########################################## -Make sure you are using python3. If - -.. code-block:: sh - - python --version - -returns ``Python 2.X.X`` try calling - -.. code-block:: sh - - python3 basic_trace.py diff --git a/docs/examples/cloud_trace_exporter/basic_trace.py b/docs/examples/cloud_trace_exporter/basic_trace.py deleted file mode 100644 index 76840a291e..0000000000 --- a/docs/examples/cloud_trace_exporter/basic_trace.py +++ /dev/null @@ -1,14 +0,0 @@ -from opentelemetry import trace -from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor - -trace.set_tracer_provider(TracerProvider()) - -cloud_trace_exporter = CloudTraceSpanExporter() -trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(cloud_trace_exporter) -) -tracer = trace.get_tracer(__name__) -with tracer.start_as_current_span("foo"): - print("Hello world!") diff --git a/docs/examples/datadog_exporter/README.rst b/docs/examples/datadog_exporter/README.rst index d851550b27..250eec2b84 100644 --- a/docs/examples/datadog_exporter/README.rst +++ b/docs/examples/datadog_exporter/README.rst @@ -13,7 +13,7 @@ Basic Example pip install opentelemetry-api pip install opentelemetry-sdk - pip install opentelemetry-ext-datadog + pip install opentelemetry-exporter-datadog * Start Datadog Agent @@ -48,7 +48,7 @@ Distributed Example pip install opentelemetry-api pip install opentelemetry-sdk - pip install opentelemetry-ext-datadog + pip install opentelemetry-exporter-datadog pip install opentelemetry-instrumentation pip install opentelemetry-ext-flask pip install flask diff --git a/docs/examples/datadog_exporter/basic_example.py b/docs/examples/datadog_exporter/basic_example.py index a41f9e0462..5eb470719b 100644 --- a/docs/examples/datadog_exporter/basic_example.py +++ b/docs/examples/datadog_exporter/basic_example.py @@ -15,7 +15,7 @@ # limitations under the License. from opentelemetry import trace -from opentelemetry.ext.datadog import ( +from opentelemetry.exporter.datadog import ( DatadogExportSpanProcessor, DatadogSpanExporter, ) diff --git a/docs/examples/datadog_exporter/client.py b/docs/examples/datadog_exporter/client.py index 3969ef04d9..2570c426d5 100644 --- a/docs/examples/datadog_exporter/client.py +++ b/docs/examples/datadog_exporter/client.py @@ -17,7 +17,7 @@ from requests import get from opentelemetry import propagators, trace -from opentelemetry.ext.datadog import ( +from opentelemetry.exporter.datadog import ( DatadogExportSpanProcessor, DatadogSpanExporter, ) diff --git a/docs/examples/datadog_exporter/server.py b/docs/examples/datadog_exporter/server.py index e2099fdf25..15d10f3493 100644 --- a/docs/examples/datadog_exporter/server.py +++ b/docs/examples/datadog_exporter/server.py @@ -15,11 +15,11 @@ from flask import Flask, request from opentelemetry import propagators, trace -from opentelemetry.ext.datadog import ( +from opentelemetry.exporter.datadog import ( DatadogExportSpanProcessor, DatadogSpanExporter, ) -from opentelemetry.ext.datadog.propagator import DatadogFormat +from opentelemetry.exporter.datadog.propagator import DatadogFormat from opentelemetry.sdk.trace import TracerProvider app = Flask(__name__) diff --git a/docs/examples/opencensus-exporter-metrics/README.rst b/docs/examples/opencensus-exporter-metrics/README.rst index 30961e061d..0a71685ee9 100644 --- a/docs/examples/opencensus-exporter-metrics/README.rst +++ b/docs/examples/opencensus-exporter-metrics/README.rst @@ -13,7 +13,7 @@ Installation pip install opentelemetry-api pip install opentelemetry-sdk - pip install opentelemetry-ext-opencensusexporter + pip install opentelemetry-exporter-opencensus Run the Example --------------- @@ -46,7 +46,7 @@ Useful links - OpenTelemetry_ - `OpenTelemetry Collector`_ - :doc:`../../api/trace` -- :doc:`../../ext/opencensusexporter/opencensusexporter` +- :doc:`../../exporter/opencensus/opencensus` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. _OpenTelemetry Collector: https://github.com/open-telemetry/opentelemetry-collector diff --git a/docs/examples/opencensus-exporter-metrics/collector.py b/docs/examples/opencensus-exporter-metrics/collector.py index 725f07b77a..dc39f0cf4c 100644 --- a/docs/examples/opencensus-exporter-metrics/collector.py +++ b/docs/examples/opencensus-exporter-metrics/collector.py @@ -17,7 +17,7 @@ """ from opentelemetry import metrics -from opentelemetry.ext.opencensusexporter.metrics_exporter import ( +from opentelemetry.exporter.opencensus.metrics_exporter import ( OpenCensusMetricsExporter, ) from opentelemetry.sdk.metrics import Counter, MeterProvider diff --git a/docs/examples/opencensus-exporter-tracer/README.rst b/docs/examples/opencensus-exporter-tracer/README.rst index 0019994308..d147f008d4 100644 --- a/docs/examples/opencensus-exporter-tracer/README.rst +++ b/docs/examples/opencensus-exporter-tracer/README.rst @@ -13,7 +13,7 @@ Installation pip install opentelemetry-api pip install opentelemetry-sdk - pip install opentelemetry-ext-opencensusexporter + pip install opentelemetry-exporter-opencensus Run the Example --------------- @@ -45,7 +45,7 @@ Useful links - OpenTelemetry_ - `OpenTelemetry Collector`_ - :doc:`../../api/trace` -- :doc:`../../ext/opencensusexporter/opencensusexporter` +- :doc:`../../exporter/opencensus/opencensus` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. _OpenTelemetry Collector: https://github.com/open-telemetry/opentelemetry-collector diff --git a/docs/examples/opencensus-exporter-tracer/collector.py b/docs/examples/opencensus-exporter-tracer/collector.py index 3f0c18aaf7..d404cfbce5 100644 --- a/docs/examples/opencensus-exporter-tracer/collector.py +++ b/docs/examples/opencensus-exporter-tracer/collector.py @@ -15,7 +15,7 @@ # limitations under the License. from opentelemetry import trace -from opentelemetry.ext.opencensusexporter.trace_exporter import ( +from opentelemetry.exporter.opencensus.trace_exporter import ( OpenCensusSpanExporter, ) from opentelemetry.sdk.trace import TracerProvider diff --git a/docs/examples/opentracing/main.py b/docs/examples/opentracing/main.py index 8586e0b789..9ecbbba8ca 100755 --- a/docs/examples/opentracing/main.py +++ b/docs/examples/opentracing/main.py @@ -1,8 +1,8 @@ #!/usr/bin/env python from opentelemetry import trace +from opentelemetry.exporter.jaeger import JaegerSpanExporter from opentelemetry.ext import opentracing_shim -from opentelemetry.ext.jaeger import JaegerSpanExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor from rediscache import RedisCache diff --git a/docs/examples/opentracing/requirements.txt b/docs/examples/opentracing/requirements.txt index d87842f4d8..fa4b520936 100644 --- a/docs/examples/opentracing/requirements.txt +++ b/docs/examples/opentracing/requirements.txt @@ -1,6 +1,6 @@ opentelemetry-api opentelemetry-sdk -opentelemetry-ext-jaeger +opentelemetry-exporter-jaeger opentelemetry-opentracing-shim redis redis_opentracing diff --git a/docs/ext/datadog/datadog.rst b/docs/exporter/datadog/datadog.rst similarity index 71% rename from docs/ext/datadog/datadog.rst rename to docs/exporter/datadog/datadog.rst index 5ae7e04289..3b43c2bdf7 100644 --- a/docs/ext/datadog/datadog.rst +++ b/docs/exporter/datadog/datadog.rst @@ -1,7 +1,7 @@ OpenTelemetry Datadog Exporter ============================== -.. automodule:: opentelemetry.ext.datadog +.. automodule:: opentelemetry.exporter.datadog :members: :undoc-members: :show-inheritance: diff --git a/docs/ext/jaeger/jaeger.rst b/docs/exporter/jaeger/jaeger.rst similarity index 64% rename from docs/ext/jaeger/jaeger.rst rename to docs/exporter/jaeger/jaeger.rst index d7b93a6f10..6988d8cea9 100644 --- a/docs/ext/jaeger/jaeger.rst +++ b/docs/exporter/jaeger/jaeger.rst @@ -1,7 +1,7 @@ Opentelemetry Jaeger Exporter ============================= -.. automodule:: opentelemetry.ext.jaeger +.. automodule:: opentelemetry.exporter.jaeger :members: :undoc-members: :show-inheritance: @@ -10,7 +10,7 @@ Opentelemetry Jaeger Exporter Submodules ---------- -.. automodule:: opentelemetry.ext.jaeger.gen.jaeger.ttypes +.. automodule:: opentelemetry.exporter.jaeger.gen.jaeger.ttypes :members: :undoc-members: :show-inheritance: diff --git a/docs/ext/opencensusexporter/opencensusexporter.rst b/docs/exporter/opencensus/opencensus.rst similarity index 64% rename from docs/ext/opencensusexporter/opencensusexporter.rst rename to docs/exporter/opencensus/opencensus.rst index 07b9a85558..6bdcd6a873 100644 --- a/docs/ext/opencensusexporter/opencensusexporter.rst +++ b/docs/exporter/opencensus/opencensus.rst @@ -1,7 +1,7 @@ OpenCensus Exporter =================== -.. automodule:: opentelemetry.ext.opencensusexporter +.. automodule:: opentelemetry.exporter.opencensus :members: :undoc-members: :show-inheritance: diff --git a/docs/ext/otlp/otlp.rst b/docs/exporter/otlp/otlp.rst similarity index 72% rename from docs/ext/otlp/otlp.rst rename to docs/exporter/otlp/otlp.rst index 4739d21a58..7663ec9489 100644 --- a/docs/ext/otlp/otlp.rst +++ b/docs/exporter/otlp/otlp.rst @@ -1,7 +1,7 @@ Opentelemetry OTLP Exporter =========================== -.. automodule:: opentelemetry.ext.otlp +.. automodule:: opentelemetry.exporter.otlp :members: :undoc-members: :show-inheritance: diff --git a/docs/ext/prometheus/prometheus.rst b/docs/exporter/prometheus/prometheus.rst similarity index 71% rename from docs/ext/prometheus/prometheus.rst rename to docs/exporter/prometheus/prometheus.rst index 9ca7754af9..f4ad1a8245 100644 --- a/docs/ext/prometheus/prometheus.rst +++ b/docs/exporter/prometheus/prometheus.rst @@ -1,7 +1,7 @@ OpenTelemetry Prometheus Exporter ================================= -.. automodule:: opentelemetry.ext.prometheus +.. automodule:: opentelemetry.exporter.prometheus :members: :undoc-members: :show-inheritance: diff --git a/docs/ext/zipkin/zipkin.rst b/docs/exporter/zipkin/zipkin.rst similarity index 71% rename from docs/ext/zipkin/zipkin.rst rename to docs/exporter/zipkin/zipkin.rst index 8a5191a86d..18042022a4 100644 --- a/docs/ext/zipkin/zipkin.rst +++ b/docs/exporter/zipkin/zipkin.rst @@ -1,7 +1,7 @@ Opentelemetry Zipkin Exporter ============================= -.. automodule:: opentelemetry.ext.zipkin +.. automodule:: opentelemetry.exporter.zipkin :members: :undoc-members: :show-inheritance: diff --git a/docs/ext/cloud_monitoring/cloud_monitoring.rst b/docs/ext/cloud_monitoring/cloud_monitoring.rst deleted file mode 100644 index a3a4f5660a..0000000000 --- a/docs/ext/cloud_monitoring/cloud_monitoring.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Cloud Monitoring Exporter -======================================= - -.. automodule:: opentelemetry.exporter.cloud_monitoring - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/ext/cloud_trace/cloud_trace.rst b/docs/ext/cloud_trace/cloud_trace.rst deleted file mode 100644 index 5914b00d1a..0000000000 --- a/docs/ext/cloud_trace/cloud_trace.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Cloud Trace Exporter -================================== - -.. automodule:: opentelemetry.exporter.cloud_trace - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/getting_started/jaeger_example.py b/docs/getting_started/jaeger_example.py index 8ddb4a0586..f9c8e5cd05 100644 --- a/docs/getting_started/jaeger_example.py +++ b/docs/getting_started/jaeger_example.py @@ -14,7 +14,7 @@ # jaeger_example.py from opentelemetry import trace -from opentelemetry.ext import jaeger +from opentelemetry.exporter import jaeger from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor diff --git a/docs/getting_started/prometheus_example.py b/docs/getting_started/prometheus_example.py index ce658d3419..0377e570b8 100644 --- a/docs/getting_started/prometheus_example.py +++ b/docs/getting_started/prometheus_example.py @@ -19,7 +19,7 @@ from prometheus_client import start_http_server from opentelemetry import metrics -from opentelemetry.ext.prometheus import PrometheusMetricsExporter +from opentelemetry.exporter.prometheus import PrometheusMetricsExporter from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController diff --git a/docs/index.rst b/docs/index.rst index b25efa8acc..b59af3a003 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -72,8 +72,16 @@ install .. toctree:: :maxdepth: 2 - :caption: OpenTelemetry Integrations - :name: integrations + :caption: OpenTelemetry Exporters + :name: exporters + :glob: + + exporter/** + +.. toctree:: + :maxdepth: 2 + :caption: OpenTelemetry Instrumentations + :name: Instrumentations :glob: ext/** diff --git a/eachdist.ini b/eachdist.ini index 4a197ad291..82fc0271ed 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -7,6 +7,7 @@ sortfirst= opentelemetry-instrumentation opentelemetry-proto tests/util + exporter/* ext/opentelemetry-ext-wsgi ext/opentelemetry-ext-dbapi ext/opentelemetry-ext-asgi diff --git a/ext/opentelemetry-ext-datadog/CHANGELOG.md b/exporter/opentelemetry-exporter-datadog/CHANGELOG.md similarity index 55% rename from ext/opentelemetry-ext-datadog/CHANGELOG.md rename to exporter/opentelemetry-exporter-datadog/CHANGELOG.md index 15c6ff4b00..d15d5a4b5d 100644 --- a/ext/opentelemetry-ext-datadog/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-datadog/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-exporter-datadog + ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) + ## 0.8b0 Released 2020-05-27 diff --git a/ext/opentelemetry-ext-datadog/README.rst b/exporter/opentelemetry-exporter-datadog/README.rst similarity index 71% rename from ext/opentelemetry-ext-datadog/README.rst rename to exporter/opentelemetry-exporter-datadog/README.rst index 9f9a2aeb88..cb97e5997f 100644 --- a/ext/opentelemetry-ext-datadog/README.rst +++ b/exporter/opentelemetry-exporter-datadog/README.rst @@ -3,8 +3,8 @@ OpenTelemetry Datadog Exporter |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-datadog.svg - :target: https://pypi.org/project/opentelemetry-ext-datadog/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-datadog.svg + :target: https://pypi.org/project/opentelemetry-exporter-datadog/ This library allows to export tracing data to `Datadog `_. OpenTelemetry span event and links are not @@ -15,7 +15,7 @@ Installation :: - pip install opentelemetry-ext-datadog + pip install opentelemetry-exporter-datadog .. _Datadog: https://www.datadoghq.com/ diff --git a/ext/opentelemetry-ext-datadog/setup.cfg b/exporter/opentelemetry-exporter-datadog/setup.cfg similarity index 91% rename from ext/opentelemetry-ext-datadog/setup.cfg rename to exporter/opentelemetry-exporter-datadog/setup.cfg index 95b0bb0857..266abe9e0b 100644 --- a/ext/opentelemetry-ext-datadog/setup.cfg +++ b/exporter/opentelemetry-exporter-datadog/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-datadog +name = opentelemetry-exporter-datadog description = Datadog Span Exporter for OpenTelemetry long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-datadog +url = https://github.com/open-telemetry/opentelemetry-python/exporter/opentelemetry-exporter-datadog platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-jaeger/setup.py b/exporter/opentelemetry-exporter-datadog/setup.py similarity index 91% rename from ext/opentelemetry-ext-jaeger/setup.py rename to exporter/opentelemetry-exporter-datadog/setup.py index 842a6f6416..0c3bdf453f 100644 --- a/ext/opentelemetry-ext-jaeger/setup.py +++ b/exporter/opentelemetry-exporter-datadog/setup.py @@ -11,13 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import os import setuptools BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "jaeger", "version.py" + BASE_DIR, "src", "opentelemetry", "exporter", "datadog", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/__init__.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/__init__.py similarity index 93% rename from ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/__init__.py rename to exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/__init__.py index 85bdaea40a..7adde5df50 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/__init__.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/__init__.py @@ -35,8 +35,8 @@ .. code:: python from opentelemetry import propagators, trace - from opentelemetry.ext.datadog import DatadogExportSpanProcessor, DatadogSpanExporter - from opentelemetry.ext.datadog.propagator import DatadogFormat + from opentelemetry.exporter.datadog import DatadogExportSpanProcessor, DatadogSpanExporter + from opentelemetry.exporter.datadog.propagator import DatadogFormat from opentelemetry.sdk.trace import TracerProvider trace.set_tracer_provider(TracerProvider()) diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/constants.py similarity index 100% rename from ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py rename to exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/constants.py diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py similarity index 100% rename from ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py rename to exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py similarity index 100% rename from ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py rename to exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/spanprocessor.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py similarity index 100% rename from ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/spanprocessor.py rename to exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py similarity index 100% rename from ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/version.py rename to exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py diff --git a/ext/opentelemetry-ext-datadog/tests/__init__.py b/exporter/opentelemetry-exporter-datadog/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-datadog/tests/__init__.py rename to exporter/opentelemetry-exporter-datadog/tests/__init__.py diff --git a/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py similarity index 99% rename from ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py rename to exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py index 5306d517b7..a3e67790d9 100644 --- a/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py @@ -21,7 +21,7 @@ from ddtrace.internal.writer import AgentWriter from opentelemetry import trace as trace_api -from opentelemetry.ext import datadog +from opentelemetry.exporter import datadog from opentelemetry.sdk import trace from opentelemetry.sdk.util.instrumentation import InstrumentationInfo diff --git a/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py similarity index 98% rename from ext/opentelemetry-ext-datadog/tests/test_datadog_format.py rename to exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py index 31633f8370..1a398745b8 100644 --- a/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py @@ -15,7 +15,7 @@ import unittest from opentelemetry import trace as trace_api -from opentelemetry.ext.datadog import constants, propagator +from opentelemetry.exporter.datadog import constants, propagator from opentelemetry.sdk import trace from opentelemetry.trace import get_current_span, set_span_in_context diff --git a/ext/opentelemetry-ext-jaeger/CHANGELOG.md b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md similarity index 87% rename from ext/opentelemetry-ext-jaeger/CHANGELOG.md rename to exporter/opentelemetry-exporter-jaeger/CHANGELOG.md index dada0101e0..7b9a03d312 100644 --- a/ext/opentelemetry-ext-jaeger/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-exporter-jaeger + ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) + ## 0.8b0 Released 2020-05-27 diff --git a/ext/opentelemetry-ext-jaeger/LICENSE b/exporter/opentelemetry-exporter-jaeger/LICENSE similarity index 100% rename from ext/opentelemetry-ext-jaeger/LICENSE rename to exporter/opentelemetry-exporter-jaeger/LICENSE diff --git a/ext/opentelemetry-ext-jaeger/MANIFEST.in b/exporter/opentelemetry-exporter-jaeger/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-jaeger/MANIFEST.in rename to exporter/opentelemetry-exporter-jaeger/MANIFEST.in diff --git a/ext/opentelemetry-ext-jaeger/README.rst b/exporter/opentelemetry-exporter-jaeger/README.rst similarity index 66% rename from ext/opentelemetry-ext-jaeger/README.rst rename to exporter/opentelemetry-exporter-jaeger/README.rst index caa3afa932..0069d130cd 100644 --- a/ext/opentelemetry-ext-jaeger/README.rst +++ b/exporter/opentelemetry-exporter-jaeger/README.rst @@ -3,8 +3,8 @@ OpenTelemetry Jaeger Exporter |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-jaeger.svg - :target: https://pypi.org/project/opentelemetry-ext-jaeger/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger.svg + :target: https://pypi.org/project/opentelemetry-exporter-jaeger/ This library allows to export tracing data to `Jaeger `_. @@ -13,7 +13,7 @@ Installation :: - pip install opentelemetry-ext-jaeger + pip install opentelemetry-exporter-jaeger .. _Jaeger: https://www.jaegertracing.io/ @@ -23,6 +23,6 @@ Installation References ---------- -* `OpenTelemetry Jaeger Exporter `_ +* `OpenTelemetry Jaeger Exporter `_ * `Jaeger `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py b/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py similarity index 97% rename from ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py rename to exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py index 1aca62fc23..68453fd9fa 100644 --- a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py +++ b/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py @@ -1,7 +1,7 @@ import time from opentelemetry import trace -from opentelemetry.ext import jaeger +from opentelemetry.exporter import jaeger from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg similarity index 94% rename from ext/opentelemetry-ext-jaeger/setup.cfg rename to exporter/opentelemetry-exporter-jaeger/setup.cfg index fc581301b6..b660e8ec1c 100644 --- a/ext/opentelemetry-ext-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-jaeger +name = opentelemetry-exporter-jaeger description = Jaeger Exporter for OpenTelemetry long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-jaeger +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/exporter/opentelemetry-exporter-jaeger platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-otlp/setup.py b/exporter/opentelemetry-exporter-jaeger/setup.py similarity index 91% rename from ext/opentelemetry-ext-otlp/setup.py rename to exporter/opentelemetry-exporter-jaeger/setup.py index 64c30afbab..1bd3970329 100644 --- a/ext/opentelemetry-ext-otlp/setup.py +++ b/exporter/opentelemetry-exporter-jaeger/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "otlp", "version.py" + BASE_DIR, "src", "opentelemetry", "exporter", "jaeger", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py similarity index 98% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index f12031d510..993bf4f087 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -25,7 +25,7 @@ .. code:: python from opentelemetry import trace - from opentelemetry.ext import jaeger + from opentelemetry.exporter import jaeger from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor @@ -69,8 +69,8 @@ from thrift.transport import THttpClient, TTransport import opentelemetry.trace as trace_api -from opentelemetry.ext.jaeger.gen.agent import Agent as agent -from opentelemetry.ext.jaeger.gen.jaeger import Collector as jaeger +from opentelemetry.exporter.jaeger.gen.agent import Agent as agent +from opentelemetry.exporter.jaeger.gen.jaeger import Collector as jaeger from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult from opentelemetry.trace.status import StatusCanonicalCode diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/__init__.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/__init__.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/__init__.py diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/Agent-remote b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/Agent-remote old mode 100755 new mode 100644 similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/Agent-remote rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/Agent-remote diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/Agent.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/Agent.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/Agent.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/Agent.py diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/__init__.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/__init__.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/__init__.py diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/constants.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/constants.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/constants.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/constants.py diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/ttypes.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/ttypes.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/agent/ttypes.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/ttypes.py diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/Collector-remote b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/Collector-remote old mode 100755 new mode 100644 similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/Collector-remote rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/Collector-remote diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/Collector.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/Collector.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/Collector.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/Collector.py diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/__init__.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/__init__.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/__init__.py diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/constants.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/constants.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/constants.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/constants.py diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/ttypes.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/ttypes.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/jaeger/ttypes.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/ttypes.py diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ZipkinCollector-remote b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/ZipkinCollector-remote old mode 100755 new mode 100644 similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ZipkinCollector-remote rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/ZipkinCollector-remote diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ZipkinCollector.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/ZipkinCollector.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ZipkinCollector.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/ZipkinCollector.py diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/__init__.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/__init__.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/__init__.py diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/constants.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/constants.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/constants.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/constants.py diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ttypes.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/ttypes.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/zipkincore/ttypes.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/ttypes.py diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py diff --git a/ext/opentelemetry-ext-jaeger/tests/__init__.py b/exporter/opentelemetry-exporter-jaeger/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-jaeger/tests/__init__.py rename to exporter/opentelemetry-exporter-jaeger/tests/__init__.py diff --git a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py similarity index 99% rename from ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py rename to exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py index 7bca8e8317..30b7c85826 100644 --- a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py @@ -18,9 +18,9 @@ # pylint:disable=no-name-in-module # pylint:disable=import-error -import opentelemetry.ext.jaeger as jaeger_exporter +import opentelemetry.exporter.jaeger as jaeger_exporter from opentelemetry import trace as trace_api -from opentelemetry.ext.jaeger.gen.jaeger import ttypes as jaeger +from opentelemetry.exporter.jaeger.gen.jaeger import ttypes as jaeger from opentelemetry.sdk import trace from opentelemetry.sdk.trace import Resource from opentelemetry.trace.status import Status, StatusCanonicalCode diff --git a/ext/opentelemetry-ext-jaeger/thrift/agent.thrift b/exporter/opentelemetry-exporter-jaeger/thrift/agent.thrift similarity index 100% rename from ext/opentelemetry-ext-jaeger/thrift/agent.thrift rename to exporter/opentelemetry-exporter-jaeger/thrift/agent.thrift diff --git a/ext/opentelemetry-ext-jaeger/thrift/jaeger.thrift b/exporter/opentelemetry-exporter-jaeger/thrift/jaeger.thrift similarity index 100% rename from ext/opentelemetry-ext-jaeger/thrift/jaeger.thrift rename to exporter/opentelemetry-exporter-jaeger/thrift/jaeger.thrift diff --git a/ext/opentelemetry-ext-jaeger/thrift/zipkincore.thrift b/exporter/opentelemetry-exporter-jaeger/thrift/zipkincore.thrift similarity index 100% rename from ext/opentelemetry-ext-jaeger/thrift/zipkincore.thrift rename to exporter/opentelemetry-exporter-jaeger/thrift/zipkincore.thrift diff --git a/ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md similarity index 61% rename from ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md rename to exporter/opentelemetry-exporter-opencensus/CHANGELOG.md index 4d1c9a97b8..c7f0cf69b5 100644 --- a/ext/opentelemetry-ext-opencensusexporter/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-exporter-opencensus + ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) + ## 0.8b0 Released 2020-05-27 diff --git a/ext/opentelemetry-ext-opencensusexporter/LICENSE b/exporter/opentelemetry-exporter-opencensus/LICENSE similarity index 100% rename from ext/opentelemetry-ext-opencensusexporter/LICENSE rename to exporter/opentelemetry-exporter-opencensus/LICENSE diff --git a/ext/opentelemetry-ext-opencensusexporter/MANIFEST.in b/exporter/opentelemetry-exporter-opencensus/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-opencensusexporter/MANIFEST.in rename to exporter/opentelemetry-exporter-opencensus/MANIFEST.in diff --git a/ext/opentelemetry-ext-opencensusexporter/README.rst b/exporter/opentelemetry-exporter-opencensus/README.rst similarity index 57% rename from ext/opentelemetry-ext-opencensusexporter/README.rst rename to exporter/opentelemetry-exporter-opencensus/README.rst index 75563053d1..7f282d307e 100644 --- a/ext/opentelemetry-ext-opencensusexporter/README.rst +++ b/exporter/opentelemetry-exporter-opencensus/README.rst @@ -3,8 +3,8 @@ OpenCensus Exporter |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-opencensusexporter.svg - :target: https://pypi.org/project/opentelemetry-ext-opencensusexporter/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-opencensus.svg + :target: https://pypi.org/project/opentelemetry-exporter-opencensus/ This library allows to export traces and metrics using OpenCensus. @@ -13,12 +13,12 @@ Installation :: - pip install opentelemetry-ext-opencensusexporter + pip install opentelemetry-exporter-opencensus References ---------- -* `OpenCensus Exporter `_ +* `OpenCensus Exporter `_ * `OpenTelemetry Collector `_ * `OpenTelemetry `_ diff --git a/ext/opentelemetry-ext-opencensusexporter/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg similarity index 94% rename from ext/opentelemetry-ext-opencensusexporter/setup.cfg rename to exporter/opentelemetry-exporter-opencensus/setup.cfg index 0a3376083a..64b4b4a228 100644 --- a/ext/opentelemetry-ext-opencensusexporter/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-opencensusexporter +name = opentelemetry-exporter-opencensus description = OpenCensus Exporter long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-opencensusexporter +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/exporter/opentelemetry-exporter-opencensus platforms = any license = Apache-2.0 classifiers = diff --git a/exporter/opentelemetry-exporter-opencensus/setup.py b/exporter/opentelemetry-exporter-opencensus/setup.py new file mode 100644 index 0000000000..65c91aba9f --- /dev/null +++ b/exporter/opentelemetry-exporter-opencensus/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "exporter", "opencensus", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/__init__.py similarity index 100% rename from ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/__init__.py rename to exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/__init__.py diff --git a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py similarity index 98% rename from ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/metrics_exporter/__init__.py rename to exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py index bb1a1ee888..e83e779df6 100644 --- a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py @@ -24,7 +24,7 @@ ) from opencensus.proto.metrics.v1 import metrics_pb2 -import opentelemetry.ext.opencensusexporter.util as utils +import opentelemetry.exporter.opencensus.util as utils from opentelemetry.sdk.metrics import Counter, Metric from opentelemetry.sdk.metrics.export import ( MetricRecord, diff --git a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py similarity index 99% rename from ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/trace_exporter/__init__.py rename to exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py index adadef1666..e5eb4eaf77 100644 --- a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py @@ -24,7 +24,7 @@ ) from opencensus.proto.trace.v1 import trace_pb2 -import opentelemetry.ext.opencensusexporter.util as utils +import opentelemetry.exporter.opencensus.util as utils from opentelemetry.sdk.trace import Span from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult diff --git a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/util.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/util.py similarity index 98% rename from ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/util.py rename to exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/util.py index 88ff6258f7..9b9e720190 100644 --- a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/util.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/util.py @@ -21,7 +21,7 @@ from opencensus.proto.agent.common.v1 import common_pb2 from opencensus.proto.trace.v1 import trace_pb2 -from opentelemetry.ext.opencensusexporter.version import ( +from opentelemetry.exporter.opencensus.version import ( __version__ as opencensusexporter_exporter_version, ) from opentelemetry.trace import SpanKind diff --git a/ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py similarity index 100% rename from ext/opentelemetry-ext-opencensusexporter/src/opentelemetry/ext/opencensusexporter/version.py rename to exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py diff --git a/ext/opentelemetry-ext-opencensusexporter/tests/__init__.py b/exporter/opentelemetry-exporter-opencensus/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-opencensusexporter/tests/__init__.py rename to exporter/opentelemetry-exporter-opencensus/tests/__init__.py diff --git a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py similarity index 98% rename from ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py rename to exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py index f538e5acec..eddaf96360 100644 --- a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py @@ -20,7 +20,7 @@ from opencensus.proto.metrics.v1 import metrics_pb2 from opentelemetry import metrics -from opentelemetry.ext.opencensusexporter import metrics_exporter +from opentelemetry.exporter.opencensus import metrics_exporter from opentelemetry.sdk.metrics import ( Counter, MeterProvider, @@ -47,7 +47,7 @@ def setUpClass(cls): def test_constructor(self): mock_get_node = mock.Mock() patch = mock.patch( - "opentelemetry.ext.opencensusexporter.util.get_node", + "opentelemetry.exporter.opencensus.util.get_node", side_effect=mock_get_node, ) service_name = "testServiceName" diff --git a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_trace_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py similarity index 98% rename from ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_trace_exporter.py rename to exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py index 0801d6d0c1..d07fd053b4 100644 --- a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_trace_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py @@ -19,9 +19,9 @@ from google.protobuf.timestamp_pb2 import Timestamp from opencensus.proto.trace.v1 import trace_pb2 -import opentelemetry.ext.opencensusexporter.util as utils +import opentelemetry.exporter.opencensus.util as utils from opentelemetry import trace as trace_api -from opentelemetry.ext.opencensusexporter.trace_exporter import ( +from opentelemetry.exporter.opencensus.trace_exporter import ( OpenCensusSpanExporter, translate_to_collector, ) @@ -35,7 +35,7 @@ class TestCollectorSpanExporter(unittest.TestCase): def test_constructor(self): mock_get_node = mock.Mock() patch = mock.patch( - "opentelemetry.ext.opencensusexporter.util.get_node", + "opentelemetry.exporter.opencensus.util.get_node", side_effect=mock_get_node, ) service_name = "testServiceName" diff --git a/ext/opentelemetry-ext-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md similarity index 65% rename from ext/opentelemetry-ext-otlp/CHANGELOG.md rename to exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 32e75eda55..bcaa7d1181 100644 --- a/ext/opentelemetry-ext-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-exporter-otlp + ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) + ## Version 0.11b0 Released 2020-07-28 diff --git a/ext/opentelemetry-ext-otlp/LICENSE b/exporter/opentelemetry-exporter-otlp/LICENSE similarity index 100% rename from ext/opentelemetry-ext-otlp/LICENSE rename to exporter/opentelemetry-exporter-otlp/LICENSE diff --git a/ext/opentelemetry-ext-otlp/MANIFEST.in b/exporter/opentelemetry-exporter-otlp/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-otlp/MANIFEST.in rename to exporter/opentelemetry-exporter-otlp/MANIFEST.in diff --git a/ext/opentelemetry-ext-otlp/README.rst b/exporter/opentelemetry-exporter-otlp/README.rst similarity index 70% rename from ext/opentelemetry-ext-otlp/README.rst rename to exporter/opentelemetry-exporter-otlp/README.rst index ab233cbc62..8adf657181 100644 --- a/ext/opentelemetry-ext-otlp/README.rst +++ b/exporter/opentelemetry-exporter-otlp/README.rst @@ -3,8 +3,8 @@ OpenTelemetry Collector Exporter |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-otlp.svg - :target: https://pypi.org/project/opentelemetry-ext-otlp/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp.svg + :target: https://pypi.org/project/opentelemetry-exporter-otlp/ This library allows to export data to the OpenTelemetry Collector using the OpenTelemetry Protocol. @@ -13,13 +13,13 @@ Installation :: - pip install opentelemetry-ext-otlp + pip install opentelemetry-exporter-otlp References ---------- -* `OpenTelemetry Collector Exporter `_ +* `OpenTelemetry Collector Exporter `_ * `OpenTelemetry Collector `_ * `OpenTelemetry `_ * `OpenTelemetry Protocol Specification `_ diff --git a/ext/opentelemetry-ext-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg similarity index 95% rename from ext/opentelemetry-ext-otlp/setup.cfg rename to exporter/opentelemetry-exporter-otlp/setup.cfg index b6bfac669b..262ac02008 100644 --- a/ext/opentelemetry-ext-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-otlp +name = opentelemetry-exporter-otlp description = OpenTelemetry Collector Exporter long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-otlp +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/exporter/opentelemetry-exporter-otlp platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-datadog/setup.py b/exporter/opentelemetry-exporter-otlp/setup.py similarity index 92% rename from ext/opentelemetry-ext-datadog/setup.py rename to exporter/opentelemetry-exporter-otlp/setup.py index f657391104..c04c30fca4 100644 --- a/ext/opentelemetry-ext-datadog/setup.py +++ b/exporter/opentelemetry-exporter-otlp/setup.py @@ -11,14 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import os import setuptools BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "datadog", "version.py" + BASE_DIR, "src", "opentelemetry", "exporter", "otlp", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py similarity index 95% rename from ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/__init__.py rename to exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py index 1b315c5847..dca0042a68 100644 --- a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py @@ -29,7 +29,7 @@ .. code:: python from opentelemetry import trace - from opentelemetry.ext.otlp.trace_exporter import OTLPSpanExporter + from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor diff --git a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py similarity index 100% rename from ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/trace_exporter/__init__.py rename to exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py diff --git a/ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py similarity index 100% rename from ext/opentelemetry-ext-otlp/src/opentelemetry/ext/otlp/version.py rename to exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py diff --git a/ext/opentelemetry-ext-otlp/tests/__init__.py b/exporter/opentelemetry-exporter-otlp/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-otlp/tests/__init__.py rename to exporter/opentelemetry-exporter-otlp/tests/__init__.py diff --git a/ext/opentelemetry-ext-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py similarity index 97% rename from ext/opentelemetry-ext-otlp/tests/test_otlp_trace_exporter.py rename to exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index 3e7affb221..c7e26508b2 100644 --- a/ext/opentelemetry-ext-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -21,7 +21,7 @@ from google.rpc.error_details_pb2 import RetryInfo from grpc import StatusCode, server -from opentelemetry.ext.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest, ExportTraceServiceResponse, @@ -142,8 +142,8 @@ def setUp(self): def tearDown(self): self.server.stop(None) - @patch("opentelemetry.ext.otlp.trace_exporter.expo") - @patch("opentelemetry.ext.otlp.trace_exporter.sleep") + @patch("opentelemetry.exporter.otlp.trace_exporter.expo") + @patch("opentelemetry.exporter.otlp.trace_exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): mock_expo.configure_mock(**{"return_value": [1]}) @@ -156,8 +156,8 @@ def test_unavailable(self, mock_sleep, mock_expo): ) mock_sleep.assert_called_with(1) - @patch("opentelemetry.ext.otlp.trace_exporter.expo") - @patch("opentelemetry.ext.otlp.trace_exporter.sleep") + @patch("opentelemetry.exporter.otlp.trace_exporter.expo") + @patch("opentelemetry.exporter.otlp.trace_exporter.sleep") def test_unavailable_delay(self, mock_sleep, mock_expo): mock_expo.configure_mock(**{"return_value": [1]}) diff --git a/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md b/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md new file mode 100644 index 0000000000..5c978d394d --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## Unreleased + +- Change package name to opentelemetry-exporter-prometheus + ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) + +## 0.4a0 + +Released 2020-02-21 + +- Initial release diff --git a/ext/opentelemetry-ext-prometheus/LICENSE b/exporter/opentelemetry-exporter-prometheus/LICENSE similarity index 100% rename from ext/opentelemetry-ext-prometheus/LICENSE rename to exporter/opentelemetry-exporter-prometheus/LICENSE diff --git a/ext/opentelemetry-ext-prometheus/MANIFEST.in b/exporter/opentelemetry-exporter-prometheus/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-prometheus/MANIFEST.in rename to exporter/opentelemetry-exporter-prometheus/MANIFEST.in diff --git a/ext/opentelemetry-ext-prometheus/README.rst b/exporter/opentelemetry-exporter-prometheus/README.rst similarity index 58% rename from ext/opentelemetry-ext-prometheus/README.rst rename to exporter/opentelemetry-exporter-prometheus/README.rst index 5a85f03582..a3eb920000 100644 --- a/ext/opentelemetry-ext-prometheus/README.rst +++ b/exporter/opentelemetry-exporter-prometheus/README.rst @@ -3,8 +3,8 @@ OpenTelemetry Prometheus Exporter |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-prometheus.svg - :target: https://pypi.org/project/opentelemetry-ext-prometheus/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-prometheus.svg + :target: https://pypi.org/project/opentelemetry-exporter-prometheus/ This library allows to export metrics data to `Prometheus `_. @@ -13,11 +13,11 @@ Installation :: - pip install opentelemetry-ext-prometheus + pip install opentelemetry-exporter-prometheus References ---------- -* `OpenTelemetry Prometheus Exporter `_ +* `OpenTelemetry Prometheus Exporter `_ * `Prometheus `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg similarity index 94% rename from ext/opentelemetry-ext-prometheus/setup.cfg rename to exporter/opentelemetry-exporter-prometheus/setup.cfg index 3571f93c54..94359e9641 100644 --- a/ext/opentelemetry-ext-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-prometheus +name = opentelemetry-exporter-prometheus description = Prometheus Metric Exporter for OpenTelemetry long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-prometheus +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/exporter/opentelemetry-exporter-prometheus platforms = any license = Apache-2.0 classifiers = diff --git a/exporter/opentelemetry-exporter-prometheus/setup.py b/exporter/opentelemetry-exporter-prometheus/setup.py new file mode 100644 index 0000000000..86067a2bd5 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "exporter", "prometheus", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py similarity index 98% rename from ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py rename to exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index da22042dcc..e03c23a99e 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -27,7 +27,7 @@ .. code:: python from opentelemetry import metrics - from opentelemetry.ext.prometheus import PrometheusMetricsExporter + from opentelemetry.exporter.prometheus import PrometheusMetricsExporter from opentelemetry.sdk.metrics import Counter, Meter from prometheus_client import start_http_server diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py similarity index 100% rename from ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py rename to exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py diff --git a/ext/opentelemetry-ext-prometheus/tests/__init__.py b/exporter/opentelemetry-exporter-prometheus/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-prometheus/tests/__init__.py rename to exporter/opentelemetry-exporter-prometheus/tests/__init__.py diff --git a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py similarity index 97% rename from ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py rename to exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 2ba6b70121..d624086ce3 100644 --- a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -17,7 +17,7 @@ from prometheus_client.core import CounterMetricFamily -from opentelemetry.ext.prometheus import ( +from opentelemetry.exporter.prometheus import ( CustomCollector, PrometheusMetricsExporter, ) @@ -122,7 +122,7 @@ def test_invalid_metric(self): collector = CustomCollector("testprefix") collector.add_metrics_data([record]) collector.collect() - self.assertLogs("opentelemetry.ext.prometheus", level="WARNING") + self.assertLogs("opentelemetry.exporter.prometheus", level="WARNING") def test_sanitize(self): collector = CustomCollector("testprefix") diff --git a/ext/opentelemetry-ext-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md similarity index 73% rename from ext/opentelemetry-ext-zipkin/CHANGELOG.md rename to exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index efad477d48..2218d44c20 100644 --- a/ext/opentelemetry-ext-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-exporter-zipkin + ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) + ## 0.8b0 Released 2020-05-27 diff --git a/ext/opentelemetry-ext-zipkin/LICENSE b/exporter/opentelemetry-exporter-zipkin/LICENSE similarity index 100% rename from ext/opentelemetry-ext-zipkin/LICENSE rename to exporter/opentelemetry-exporter-zipkin/LICENSE diff --git a/ext/opentelemetry-ext-zipkin/MANIFEST.in b/exporter/opentelemetry-exporter-zipkin/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-zipkin/MANIFEST.in rename to exporter/opentelemetry-exporter-zipkin/MANIFEST.in diff --git a/ext/opentelemetry-ext-zipkin/README.rst b/exporter/opentelemetry-exporter-zipkin/README.rst similarity index 59% rename from ext/opentelemetry-ext-zipkin/README.rst rename to exporter/opentelemetry-exporter-zipkin/README.rst index c746051992..a23df5eeec 100644 --- a/ext/opentelemetry-ext-zipkin/README.rst +++ b/exporter/opentelemetry-exporter-zipkin/README.rst @@ -3,8 +3,8 @@ OpenTelemetry Zipkin Exporter |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-zipkin.svg - :target: https://pypi.org/project/opentelemetry-ext-zipkin/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-zipkin.svg + :target: https://pypi.org/project/opentelemetry-exporter-zipkin/ This library allows to export tracing data to `Zipkin `_. @@ -13,12 +13,12 @@ Installation :: - pip install opentelemetry-ext-zipkin + pip install opentelemetry-exporter-zipkin References ---------- -* `OpenTelemetry Zipkin Exporter `_ +* `OpenTelemetry Zipkin Exporter `_ * `Zipkin `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg similarity index 95% rename from ext/opentelemetry-ext-zipkin/setup.cfg rename to exporter/opentelemetry-exporter-zipkin/setup.cfg index 7674cab536..f1246813e3 100644 --- a/ext/opentelemetry-ext-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-zipkin +name = opentelemetry-exporter-zipkin description = Zipkin Span Exporter for OpenTelemetry long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-zipkin +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-exporter-zipkin platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-prometheus/setup.py b/exporter/opentelemetry-exporter-zipkin/setup.py similarity index 91% rename from ext/opentelemetry-ext-prometheus/setup.py rename to exporter/opentelemetry-exporter-zipkin/setup.py index 0276cd6655..9822805d9d 100644 --- a/ext/opentelemetry-ext-prometheus/setup.py +++ b/exporter/opentelemetry-exporter-zipkin/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "prometheus", "version.py" + BASE_DIR, "src", "opentelemetry", "exporter", "zipkin", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py similarity index 99% rename from ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py rename to exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 8a487290ce..10bd6e93e9 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -28,7 +28,7 @@ .. code:: python from opentelemetry import trace - from opentelemetry.ext import zipkin + from opentelemetry.exporter import zipkin from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py similarity index 100% rename from ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py rename to exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py diff --git a/ext/opentelemetry-ext-zipkin/tests/__init__.py b/exporter/opentelemetry-exporter-zipkin/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-zipkin/tests/__init__.py rename to exporter/opentelemetry-exporter-zipkin/tests/__init__.py diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py similarity index 99% rename from ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py rename to exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 1f2d53d304..9f6da2e240 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -17,7 +17,7 @@ from unittest.mock import MagicMock, patch from opentelemetry import trace as trace_api -from opentelemetry.ext.zipkin import ZipkinSpanExporter +from opentelemetry.exporter.zipkin import ZipkinSpanExporter from opentelemetry.sdk import trace from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.trace.export import SpanExportResult diff --git a/ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py b/ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py index af0f049bb0..c794891783 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py @@ -14,7 +14,7 @@ from opentelemetry import trace from opentelemetry.context import attach, detach, set_value -from opentelemetry.ext.opencensusexporter.trace_exporter import ( +from opentelemetry.exporter.opencensus.trace_exporter import ( OpenCensusSpanExporter, ) from opentelemetry.sdk.trace import TracerProvider diff --git a/ext/opentelemetry-ext-opencensusexporter/setup.py b/ext/opentelemetry-ext-opencensusexporter/setup.py deleted file mode 100644 index dfb8fd82bf..0000000000 --- a/ext/opentelemetry-ext-opencensusexporter/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "opencensusexporter", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-prometheus/CHANGELOG.md b/ext/opentelemetry-ext-prometheus/CHANGELOG.md deleted file mode 100644 index d5a548aa13..0000000000 --- a/ext/opentelemetry-ext-prometheus/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -## Unreleased - -## 0.4a0 - -Released 2020-02-21 - -- Initial release diff --git a/ext/opentelemetry-ext-zipkin/setup.py b/ext/opentelemetry-ext-zipkin/setup.py deleted file mode 100644 index a01df70c9d..0000000000 --- a/ext/opentelemetry-ext-zipkin/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "zipkin", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/pyproject.toml b/pyproject.toml index 5911dc4e9f..db4df73b69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ exclude = ''' ( /( # generated files docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen| - ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen| + exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen| opentelemetry-proto/src/opentelemetry/proto/collector| opentelemetry-proto/src/opentelemetry/proto/common| opentelemetry-proto/src/opentelemetry/proto/metrics| diff --git a/scripts/build.sh b/scripts/build.sh index 7a27105d76..0d481b8165 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ ext/*/ ; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ exporter/*/ ext/*/ ; do ( echo "building $d" cd "$d" diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 0b45fbf643..5cf393e7e6 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -31,13 +31,13 @@ coverage erase cov opentelemetry-api cov opentelemetry-sdk -cov ext/opentelemetry-ext-datadog +cov exporter/opentelemetry-exporter-datadog cov ext/opentelemetry-ext-flask cov ext/opentelemetry-ext-requests -cov ext/opentelemetry-ext-jaeger +cov exporter/opentelemetry-exporter-jaeger cov ext/opentelemetry-ext-opentracing-shim cov ext/opentelemetry-ext-wsgi -cov ext/opentelemetry-ext-zipkin +cov exporter/opentelemetry-exporter-zipkin cov docs/examples/opentelemetry-example-app # aiohttp is only supported on Python 3.5+. diff --git a/tox.ini b/tox.ini index 07a0bf42bd..a018ec7a89 100644 --- a/tox.ini +++ b/tox.ini @@ -78,26 +78,26 @@ envlist = py3{4,5,6,7,8}-test-instrumentation-jinja2 pypy3-test-instrumentation-jinja2 - ; opentelemetry-ext-jaeger + ; opentelemetry-exporter-jaeger py3{4,5,6,7,8}-test-exporter-jaeger pypy3-test-exporter-jaeger - ; opentelemetry-ext-datadog + ; opentelemetry-exporter-datadog py3{5,6,7,8}-test-exporter-datadog ; opentelemetry-ext-mysql py3{4,5,6,7,8}-test-instrumentation-mysql pypy3-test-instrumentation-mysql - ; opentelemetry-ext-opencensusexporter - py3{4,5,6,7,8}-test-exporter-opencensusexporter - ; ext-opencensusexporter intentionally excluded from pypy3 + ; opentelemetry-exporter-opencensus + py3{4,5,6,7,8}-test-exporter-opencensus + ; exporter-opencensus intentionally excluded from pypy3 - ; opentelemetry-ext-otlp + ; opentelemetry-exporter-otlp py3{5,6,7,8}-test-exporter-otlp - ; ext-otlp intentionally excluded from pypy3 + ; exporter-otlp intentionally excluded from pypy3 - ; opentelemetry-ext-prometheus + ; opentelemetry-exporter-prometheus py3{4,5,6,7,8}-test-exporter-prometheus pypy3-test-exporter-prometheus @@ -137,7 +137,7 @@ envlist = py3{4,5,6,7,8}-test-instrumentation-wsgi pypy3-test-instrumentation-wsgi - ; opentelemetry-ext-zipkin + ; opentelemetry-exporter-zipkin py3{4,5,6,7,8}-test-exporter-zipkin pypy3-test-exporter-zipkin @@ -226,12 +226,12 @@ changedir = test-instrumentation-system-metrics: ext/opentelemetry-ext-system-metrics/tests test-instrumentation-wsgi: ext/opentelemetry-ext-wsgi/tests - test-exporter-jaeger: ext/opentelemetry-ext-jaeger/tests - test-exporter-datadog: ext/opentelemetry-ext-datadog/tests - test-exporter-opencensusexporter: ext/opentelemetry-ext-opencensusexporter/tests - test-exporter-otlp: ext/opentelemetry-ext-otlp/tests - test-exporter-prometheus: ext/opentelemetry-ext-prometheus/tests - test-exporter-zipkin: ext/opentelemetry-ext-zipkin/tests + test-exporter-jaeger: exporter/opentelemetry-exporter-jaeger/tests + test-exporter-datadog: exporter/opentelemetry-exporter-datadog/tests + test-exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests + test-exporter-otlp: exporter/opentelemetry-exporter-otlp/tests + test-exporter-prometheus: exporter/opentelemetry-exporter-prometheus/tests + test-exporter-zipkin: exporter/opentelemetry-exporter-zipkin/tests commands_pre = ; Install without -e to test the actual installation @@ -271,12 +271,12 @@ commands_pre = mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-mysql[test] - opencensusexporter: pip install {toxinidir}/ext/opentelemetry-ext-opencensusexporter + opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus otlp: pip install {toxinidir}/opentelemetry-proto - otlp: pip install {toxinidir}/ext/opentelemetry-ext-otlp + otlp: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp - prometheus: pip install {toxinidir}/ext/opentelemetry-ext-prometheus + prometheus: pip install {toxinidir}/exporter/opentelemetry-exporter-prometheus pymemcache: pip install {toxinidir}/ext/opentelemetry-ext-pymemcache[test] @@ -302,14 +302,14 @@ commands_pre = aiopg: pip install {toxinidir}/ext/opentelemetry-ext-dbapi pip install {toxinidir}/ext/opentelemetry-instrumentation-aiopg[test] - jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger + jaeger: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk opentracing-shim: pip install {toxinidir}/ext/opentelemetry-ext-opentracing-shim - datadog: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-datadog + datadog: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/exporter/opentelemetry-exporter-datadog - zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin + zipkin: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin sqlalchemy: pip install {toxinidir}/ext/opentelemetry-ext-sqlalchemy @@ -423,7 +423,7 @@ commands_pre = -e {toxinidir}/ext/opentelemetry-instrumentation-aiopg \ -e {toxinidir}/ext/opentelemetry-ext-redis \ -e {toxinidir}/ext/opentelemetry-ext-system-metrics \ - -e {toxinidir}/ext/opentelemetry-ext-opencensusexporter + -e {toxinidir}/exporter/opentelemetry-exporter-opencensus docker-compose up -d python check_availability.py commands = From fc58032694fd96b17df18f08532f63e9f60f0e3a Mon Sep 17 00:00:00 2001 From: Connor Adams Date: Wed, 29 Jul 2020 16:58:51 -0400 Subject: [PATCH 0482/1517] automatic metrics for gRPC client interceptor (#917) --- ext/opentelemetry-ext-grpc/CHANGELOG.md | 2 + ext/opentelemetry-ext-grpc/setup.cfg | 1 + .../src/opentelemetry/ext/grpc/__init__.py | 53 +++-- .../src/opentelemetry/ext/grpc/_client.py | 185 ++++++++++++------ .../src/opentelemetry/ext/grpc/_utilities.py | 79 ++++++++ .../tests/test_client_interceptor.py | 175 +++++++++++++++-- 6 files changed, 415 insertions(+), 80 deletions(-) diff --git a/ext/opentelemetry-ext-grpc/CHANGELOG.md b/ext/opentelemetry-ext-grpc/CHANGELOG.md index fff83ab5a7..5f272a80f8 100644 --- a/ext/opentelemetry-ext-grpc/CHANGELOG.md +++ b/ext/opentelemetry-ext-grpc/CHANGELOG.md @@ -11,6 +11,8 @@ Released 2020-07-28 - Add gRPC client and server instrumentors ([788](https://github.com/open-telemetry/opentelemetry-python/pull/788)) +- Add metric recording (bytes in/out, errors, latency) to gRPC client + ## 0.8b0 Released 2020-05-27 diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index 0a4e4e5b67..c7dac4c356 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -41,6 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.12.dev0 + opentelemetry-sdk == 0.12.dev0 grpcio ~= 1.27 [options.extras_require] diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py index 368ae55f2e..1a66566281 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py @@ -33,6 +33,10 @@ SimpleExportSpanProcessor, ) + from opentelemetry import metrics + from opentelemetry.sdk.metrics import MeterProvider + from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter + try: from .gen import helloworld_pb2, helloworld_pb2_grpc except ImportError: @@ -42,7 +46,12 @@ trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) - instrumentor = GrpcInstrumentorClient() + + # Set meter provider to opentelemetry-sdk's MeterProvider + metrics.set_meter_provider(MeterProvider()) + + # Optional - export GRPC specific metrics (latency, bytes in/out, errors) by passing an exporter + instrumentor = GrpcInstrumentorClient(exporter=ConsoleMetricsExporter(), interval=10) instrumentor.instrument() def run(): @@ -109,6 +118,7 @@ def serve(): serve() """ from contextlib import contextmanager +from functools import partial import grpc from wrapt import wrap_function_wrapper as _wrap @@ -139,11 +149,21 @@ def wrapper_fn(self, original_func, instance, args, kwargs): class GrpcInstrumentorClient(BaseInstrumentor): def _instrument(self, **kwargs): + exporter = kwargs.get("exporter", None) + interval = kwargs.get("interval", 30) if kwargs.get("channel_type") == "secure": - _wrap("grpc", "secure_channel", self.wrapper_fn) + _wrap( + "grpc", + "secure_channel", + partial(self.wrapper_fn, exporter, interval), + ) else: - _wrap("grpc", "insecure_channel", self.wrapper_fn) + _wrap( + "grpc", + "insecure_channel", + partial(self.wrapper_fn, exporter, interval), + ) def _uninstrument(self, **kwargs): if kwargs.get("channel_type") == "secure": @@ -152,17 +172,28 @@ def _uninstrument(self, **kwargs): else: unwrap(grpc, "insecure_channel") - @contextmanager - def wrapper_fn(self, original_func, instance, args, kwargs): - with original_func(*args, **kwargs) as channel: - yield intercept_channel(channel, client_interceptor()) - - -def client_interceptor(tracer_provider=None): + def wrapper_fn( + self, exporter, interval, original_func, instance, args, kwargs + ): + channel = original_func(*args, **kwargs) + tracer_provider = kwargs.get("tracer_provider") + return intercept_channel( + channel, + client_interceptor( + tracer_provider=tracer_provider, + exporter=exporter, + interval=interval, + ), + ) + + +def client_interceptor(tracer_provider=None, exporter=None, interval=30): """Create a gRPC client channel interceptor. Args: tracer: The tracer to use to create client-side spans. + exporter: The exporter that will receive client metrics + interval: Time between every export call Returns: An invocation-side interceptor object. @@ -171,7 +202,7 @@ def client_interceptor(tracer_provider=None): tracer = trace.get_tracer(__name__, __version__, tracer_provider) - return _client.OpenTelemetryClientInterceptor(tracer) + return _client.OpenTelemetryClientInterceptor(tracer, exporter, interval) def server_interceptor(tracer_provider=None): diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py index 373d8f345c..028804f599 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py @@ -24,11 +24,12 @@ import grpc -from opentelemetry import propagators, trace +from opentelemetry import metrics, propagators, trace +from opentelemetry.sdk.metrics.export.controller import PushController from opentelemetry.trace.status import Status, StatusCanonicalCode from . import grpcext -from ._utilities import RpcInfo +from ._utilities import RpcInfo, TimedMetricRecorder class _GuardedSpan: @@ -63,7 +64,7 @@ def append_metadata( propagators.inject(append_metadata, metadata) -def _make_future_done_callback(span, rpc_info): +def _make_future_done_callback(span, rpc_info, client_info, metrics_recorder): def callback(response_future): with span: code = response_future.code() @@ -72,6 +73,10 @@ def callback(response_future): return response = response_future.result() rpc_info.response = response + if "ByteSize" in dir(response): + metrics_recorder.record_bytes_in( + response.ByteSize(), client_info.full_method + ) return callback @@ -79,21 +84,34 @@ def callback(response_future): class OpenTelemetryClientInterceptor( grpcext.UnaryClientInterceptor, grpcext.StreamClientInterceptor ): - def __init__(self, tracer): + def __init__(self, tracer, exporter, interval): self._tracer = tracer + self._meter = None + if exporter and interval: + self._meter = metrics.get_meter(__name__) + self.controller = PushController( + meter=self._meter, exporter=exporter, interval=interval + ) + self._metrics_recorder = TimedMetricRecorder(self._meter, "client") + def _start_span(self, method): return self._tracer.start_as_current_span( name=method, kind=trace.SpanKind.CLIENT ) # pylint:disable=no-self-use - def _trace_result(self, guarded_span, rpc_info, result): + def _trace_result(self, guarded_span, rpc_info, result, client_info): # If the RPC is called asynchronously, release the guard and add a # callback so that the span can be finished once the future is done. if isinstance(result, grpc.Future): result.add_done_callback( - _make_future_done_callback(guarded_span.release(), rpc_info) + _make_future_done_callback( + guarded_span.release(), + rpc_info, + client_info, + self._metrics_recorder, + ) ) return result response = result @@ -104,11 +122,24 @@ def _trace_result(self, guarded_span, rpc_info, result): if isinstance(result, tuple): response = result[0] rpc_info.response = response + + if "ByteSize" in dir(response): + self._metrics_recorder.record_bytes_in( + response.ByteSize(), client_info.full_method + ) return result def _start_guarded_span(self, *args, **kwargs): return _GuardedSpan(self._start_span(*args, **kwargs)) + def _bytes_out_iterator_wrapper(self, iterator, client_info): + for request in iterator: + if "ByteSize" in dir(request): + self._metrics_recorder.record_bytes_out( + request.ByteSize(), client_info.full_method + ) + yield request + def intercept_unary(self, request, metadata, client_info, invoker): if not metadata: mutable_metadata = OrderedDict() @@ -116,25 +147,37 @@ def intercept_unary(self, request, metadata, client_info, invoker): mutable_metadata = OrderedDict(metadata) with self._start_guarded_span(client_info.full_method) as guarded_span: - _inject_span_context(mutable_metadata) - metadata = tuple(mutable_metadata.items()) - - rpc_info = RpcInfo( - full_method=client_info.full_method, - metadata=metadata, - timeout=client_info.timeout, - request=request, - ) - - try: - result = invoker(request, metadata) - except grpc.RpcError as exc: - guarded_span.generated_span.set_status( - Status(StatusCanonicalCode(exc.code().value[0])) + with self._metrics_recorder.record_latency( + client_info.full_method + ): + _inject_span_context(mutable_metadata) + metadata = tuple(mutable_metadata.items()) + + # If protobuf is used, we can record the bytes in/out. Otherwise, we have no way + # to get the size of the request/response properly, so don't record anything + if "ByteSize" in dir(request): + self._metrics_recorder.record_bytes_out( + request.ByteSize(), client_info.full_method + ) + + rpc_info = RpcInfo( + full_method=client_info.full_method, + metadata=metadata, + timeout=client_info.timeout, + request=request, ) - raise - return self._trace_result(guarded_span, rpc_info, result) + try: + result = invoker(request, metadata) + except grpc.RpcError as exc: + guarded_span.generated_span.set_status( + Status(StatusCanonicalCode(exc.code().value[0])) + ) + raise + + return self._trace_result( + guarded_span, rpc_info, result, client_info + ) # For RPCs that stream responses, the result can be a generator. To record # the span across the generated responses and detect any errors, we wrap @@ -148,25 +191,44 @@ def _intercept_server_stream( mutable_metadata = OrderedDict(metadata) with self._start_span(client_info.full_method) as span: - _inject_span_context(mutable_metadata) - metadata = tuple(mutable_metadata.items()) - rpc_info = RpcInfo( - full_method=client_info.full_method, - metadata=metadata, - timeout=client_info.timeout, - ) - if client_info.is_client_stream: - rpc_info.request = request_or_iterator - - try: - result = invoker(request_or_iterator, metadata) - for response in result: - yield response - except grpc.RpcError as exc: - span.set_status( - Status(StatusCanonicalCode(exc.code().value[0])) + with self._metrics_recorder.record_latency( + client_info.full_method + ): + _inject_span_context(mutable_metadata) + metadata = tuple(mutable_metadata.items()) + rpc_info = RpcInfo( + full_method=client_info.full_method, + metadata=metadata, + timeout=client_info.timeout, ) - raise + + if client_info.is_client_stream: + rpc_info.request = request_or_iterator + request_or_iterator = self._bytes_out_iterator_wrapper( + request_or_iterator, client_info + ) + else: + if "ByteSize" in dir(request_or_iterator): + self._metrics_recorder.record_bytes_out( + request_or_iterator.ByteSize(), + client_info.full_method, + ) + + try: + result = invoker(request_or_iterator, metadata) + + # Rewrap the result stream into a generator, and record the bytes received + for response in result: + if "ByteSize" in dir(response): + self._metrics_recorder.record_bytes_in( + response.ByteSize(), client_info.full_method + ) + yield response + except grpc.RpcError as exc: + span.set_status( + Status(StatusCanonicalCode(exc.code().value[0])) + ) + raise def intercept_stream( self, request_or_iterator, metadata, client_info, invoker @@ -182,21 +244,32 @@ def intercept_stream( mutable_metadata = OrderedDict(metadata) with self._start_guarded_span(client_info.full_method) as guarded_span: - _inject_span_context(mutable_metadata) - metadata = tuple(mutable_metadata.items()) - rpc_info = RpcInfo( - full_method=client_info.full_method, - metadata=metadata, - timeout=client_info.timeout, - request=request_or_iterator, - ) + with self._metrics_recorder.record_latency( + client_info.full_method + ): + _inject_span_context(mutable_metadata) + metadata = tuple(mutable_metadata.items()) + rpc_info = RpcInfo( + full_method=client_info.full_method, + metadata=metadata, + timeout=client_info.timeout, + request=request_or_iterator, + ) + + rpc_info.request = request_or_iterator - try: - result = invoker(request_or_iterator, metadata) - except grpc.RpcError as exc: - guarded_span.generated_span.set_status( - Status(StatusCanonicalCode(exc.code().value[0])) + request_or_iterator = self._bytes_out_iterator_wrapper( + request_or_iterator, client_info ) - raise - return self._trace_result(guarded_span, rpc_info, result) + try: + result = invoker(request_or_iterator, metadata) + except grpc.RpcError as exc: + guarded_span.generated_span.set_status( + Status(StatusCanonicalCode(exc.code().value[0])) + ) + raise + + return self._trace_result( + guarded_span, rpc_info, result, client_info + ) diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py index b6ff7d311a..1dfe31ec99 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py @@ -14,6 +14,13 @@ """Internal utilities.""" +from contextlib import contextmanager +from time import time + +import grpc + +from opentelemetry.sdk.metrics import Counter, ValueRecorder + class RpcInfo: def __init__( @@ -31,3 +38,75 @@ def __init__( self.request = request self.response = response self.error = error + + +class TimedMetricRecorder: + def __init__(self, meter, span_kind): + self._meter = meter + service_name = "grpcio" + self._span_kind = span_kind + base_attributes = ["method"] + + if self._meter: + self._duration = self._meter.create_metric( + name="{}/{}/duration".format(service_name, span_kind), + description="Duration of grpc requests to the server", + unit="ms", + value_type=float, + metric_type=ValueRecorder, + label_keys=base_attributes + ["error", "status_code"], + ) + self._error_count = self._meter.create_metric( + name="{}/{}/errors".format(service_name, span_kind), + description="Number of errors that were returned from the server", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=base_attributes + ["status_code"], + ) + self._bytes_in = self._meter.create_metric( + name="{}/{}/bytes_in".format(service_name, span_kind), + description="Number of bytes received from the server", + unit="by", + value_type=int, + metric_type=Counter, + label_keys=base_attributes, + ) + self._bytes_out = self._meter.create_metric( + name="{}/{}/bytes_out".format(service_name, span_kind), + description="Number of bytes sent out through gRPC", + unit="by", + value_type=int, + metric_type=Counter, + label_keys=base_attributes, + ) + + def record_bytes_in(self, bytes_in, method): + if self._meter: + labels = {"method": method} + self._bytes_in.add(bytes_in, labels) + + def record_bytes_out(self, bytes_out, method): + if self._meter: + labels = {"method": method} + self._bytes_out.add(bytes_out, labels) + + @contextmanager + def record_latency(self, method): + start_time = time() + labels = {"method": method, "status_code": grpc.StatusCode.OK} + try: + yield labels + except grpc.RpcError as exc: + if self._meter: + # pylint: disable=no-member + labels["status_code"] = exc.code() + self._error_count.add(1, labels) + labels["error"] = True + raise + finally: + if self._meter: + if "error" not in labels: + labels["error"] = False + elapsed_time = (time() - start_time) * 1000 + self._duration.record(elapsed_time, labels) diff --git a/ext/opentelemetry-ext-grpc/tests/test_client_interceptor.py b/ext/opentelemetry-ext-grpc/tests/test_client_interceptor.py index 47dc9fa0bb..a668f05ca7 100644 --- a/ext/opentelemetry-ext-grpc/tests/test_client_interceptor.py +++ b/ext/opentelemetry-ext-grpc/tests/test_client_interceptor.py @@ -15,10 +15,12 @@ import grpc import opentelemetry.ext.grpc -from opentelemetry import metrics, trace -from opentelemetry.ext.grpc import client_interceptor -from opentelemetry.ext.grpc.grpcext import intercept_channel -from opentelemetry.sdk.metrics.export.controller import PushController +from opentelemetry import trace +from opentelemetry.ext.grpc import GrpcInstrumentorClient +from opentelemetry.sdk.metrics.export.aggregate import ( + MinMaxSumCountAggregator, + SumAggregator, +) from opentelemetry.test.test_base import TestBase from tests.protobuf import test_server_pb2_grpc @@ -34,24 +36,81 @@ class TestClientProto(TestBase): def setUp(self): super().setUp() + GrpcInstrumentorClient().instrument( + exporter=self.memory_metrics_exporter + ) self.server = create_test_server(25565) self.server.start() - meter = metrics.get_meter(__name__) - interceptor = client_interceptor() - self.channel = intercept_channel( - grpc.insecure_channel("localhost:25565"), interceptor - ) + self.channel = grpc.insecure_channel("localhost:25565") self._stub = test_server_pb2_grpc.GRPCTestServerStub(self.channel) - self._controller = PushController( - meter, self.memory_metrics_exporter, 30 - ) - def tearDown(self): super().tearDown() + GrpcInstrumentorClient().uninstrument() self.memory_metrics_exporter.clear() self.server.stop(None) + def _verify_success_records(self, num_bytes_out, num_bytes_in, method): + # pylint: disable=protected-access,no-member + self.channel._interceptor.controller.tick() + records = self.memory_metrics_exporter.get_exported_metrics() + self.assertEqual(len(records), 3) + + bytes_out = None + bytes_in = None + duration = None + + for record in records: + if record.instrument.name == "grpcio/client/duration": + duration = record + elif record.instrument.name == "grpcio/client/bytes_out": + bytes_out = record + elif record.instrument.name == "grpcio/client/bytes_in": + bytes_in = record + + self.assertIsNotNone(bytes_out) + self.assertEqual(bytes_out.instrument.name, "grpcio/client/bytes_out") + self.assertEqual(bytes_out.labels, (("method", method),)) + + self.assertIsNotNone(bytes_in) + self.assertEqual(bytes_in.instrument.name, "grpcio/client/bytes_in") + self.assertEqual(bytes_in.labels, (("method", method),)) + + self.assertIsNotNone(duration) + self.assertEqual(duration.instrument.name, "grpcio/client/duration") + self.assertEqual( + duration.labels, + ( + ("error", False), + ("method", method), + ("status_code", grpc.StatusCode.OK), + ), + ) + + self.assertEqual(type(bytes_out.aggregator), SumAggregator) + self.assertEqual(type(bytes_in.aggregator), SumAggregator) + self.assertEqual(type(duration.aggregator), MinMaxSumCountAggregator) + + self.assertEqual(bytes_out.aggregator.checkpoint, num_bytes_out) + self.assertEqual(bytes_in.aggregator.checkpoint, num_bytes_in) + + self.assertEqual(duration.aggregator.checkpoint.count, 1) + self.assertGreaterEqual(duration.aggregator.checkpoint.sum, 0) + + def test_unary_unary(self): + simple_method(self._stub) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + + self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") + self.assertIs(span.kind, trace.SpanKind.CLIENT) + + # Check version and name in span's instrumentation info + self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + + self._verify_success_records(8, 8, "/GRPCTestServer/SimpleMethod") + def test_unary_stream(self): server_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() @@ -64,6 +123,10 @@ def test_unary_stream(self): # Check version and name in span's instrumentation info self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + self._verify_success_records( + 8, 40, "/GRPCTestServer/ServerStreamingMethod" + ) + def test_stream_unary(self): client_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() @@ -76,6 +139,10 @@ def test_stream_unary(self): # Check version and name in span's instrumentation info self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + self._verify_success_records( + 40, 8, "/GRPCTestServer/ClientStreamingMethod" + ) + def test_stream_stream(self): bidirectional_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() @@ -90,10 +157,57 @@ def test_stream_stream(self): # Check version and name in span's instrumentation info self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + self._verify_success_records( + 40, 40, "/GRPCTestServer/BidirectionalStreamingMethod" + ) + + def _verify_error_records(self, method): + # pylint: disable=protected-access,no-member + self.channel._interceptor.controller.tick() + records = self.memory_metrics_exporter.get_exported_metrics() + self.assertEqual(len(records), 3) + + bytes_out = None + errors = None + duration = None + + for record in records: + if record.instrument.name == "grpcio/client/duration": + duration = record + elif record.instrument.name == "grpcio/client/bytes_out": + bytes_out = record + elif record.instrument.name == "grpcio/client/errors": + errors = record + + self.assertIsNotNone(bytes_out) + self.assertIsNotNone(errors) + self.assertIsNotNone(duration) + + self.assertEqual(errors.instrument.name, "grpcio/client/errors") + self.assertEqual( + errors.labels, + ( + ("method", method), + ("status_code", grpc.StatusCode.INVALID_ARGUMENT), + ), + ) + self.assertEqual(errors.aggregator.checkpoint, 1) + + self.assertEqual( + duration.labels, + ( + ("error", True), + ("method", method), + ("status_code", grpc.StatusCode.INVALID_ARGUMENT), + ), + ) + def test_error_simple(self): with self.assertRaises(grpc.RpcError): simple_method(self._stub, error=True) + self._verify_error_records("/GRPCTestServer/SimpleMethod") + spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] @@ -106,6 +220,7 @@ def test_error_stream_unary(self): with self.assertRaises(grpc.RpcError): client_streaming_method(self._stub, error=True) + self._verify_error_records("/GRPCTestServer/ClientStreamingMethod") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] @@ -118,6 +233,8 @@ def test_error_unary_stream(self): with self.assertRaises(grpc.RpcError): server_streaming_method(self._stub, error=True) + self._verify_error_records("/GRPCTestServer/ServerStreamingMethod") + spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] @@ -130,6 +247,10 @@ def test_error_stream_stream(self): with self.assertRaises(grpc.RpcError): bidirectional_streaming_method(self._stub, error=True) + self._verify_error_records( + "/GRPCTestServer/BidirectionalStreamingMethod" + ) + spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] @@ -137,3 +258,31 @@ def test_error_stream_stream(self): span.status.canonical_code.value, grpc.StatusCode.INVALID_ARGUMENT.value[0], ) + + +class TestClientNoMetrics(TestBase): + def setUp(self): + super().setUp() + GrpcInstrumentorClient().instrument() + self.server = create_test_server(25565) + self.server.start() + self.channel = grpc.insecure_channel("localhost:25565") + self._stub = test_server_pb2_grpc.GRPCTestServerStub(self.channel) + + def tearDown(self): + super().tearDown() + GrpcInstrumentorClient().uninstrument() + self.memory_metrics_exporter.clear() + self.server.stop(None) + + def test_unary_unary(self): + simple_method(self._stub) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + + self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") + self.assertIs(span.kind, trace.SpanKind.CLIENT) + + # Check version and name in span's instrumentation info + self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) From 8c4fca5c754bb98fcb0101e5865e5b7e70ecf659 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 30 Jul 2020 08:56:11 -0600 Subject: [PATCH 0483/1517] Update environment variable names (#904) --- docs/examples/django/README.rst | 2 +- ext/opentelemetry-ext-django/CHANGELOG.md | 2 + ext/opentelemetry-ext-django/README.rst | 4 +- .../tests/conftest.py | 2 +- .../CHANGELOG.md | 4 +- .../ext/elasticsearch/__init__.py | 5 +-- .../tests/test_elasticsearch.py | 2 +- ext/opentelemetry-ext-flask/CHANGELOG.md | 4 +- ext/opentelemetry-ext-flask/README.rst | 4 +- ext/opentelemetry-ext-pyramid/CHANGELOG.md | 6 +-- ext/opentelemetry-ext-pyramid/README.rst | 4 +- opentelemetry-api/CHANGELOG.md | 5 ++- .../opentelemetry/configuration/__init__.py | 37 ++++++++++--------- .../src/opentelemetry/context/__init__.py | 2 +- .../tests/configuration/test_configuration.py | 30 +++++++-------- opentelemetry-sdk/CHANGELOG.md | 5 ++- opentelemetry-sdk/tests/conftest.py | 6 +-- 17 files changed, 63 insertions(+), 61 deletions(-) diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index e8a043684f..6f441dd333 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -36,7 +36,7 @@ Execution of the Django app Set these environment variables first: -#. ``export OPENTELEMETRY_PYTHON_DJANGO_INSTRUMENT=True`` +#. ``export OTEL_PYTHON_DJANGO_INSTRUMENT=True`` #. ``export DJANGO_SETTINGS_MODULE=instrumentation_example.settings`` The way to achieve OpenTelemetry instrumentation for your Django app is to use diff --git a/ext/opentelemetry-ext-django/CHANGELOG.md b/ext/opentelemetry-ext-django/CHANGELOG.md index 6e3cc9c982..0328ccaa20 100644 --- a/ext/opentelemetry-ext-django/CHANGELOG.md +++ b/ext/opentelemetry-ext-django/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) + ## Version 0.11b0 Released 2020-07-28 diff --git a/ext/opentelemetry-ext-django/README.rst b/ext/opentelemetry-ext-django/README.rst index 1759cb6602..b7a822f197 100644 --- a/ext/opentelemetry-ext-django/README.rst +++ b/ext/opentelemetry-ext-django/README.rst @@ -20,13 +20,13 @@ Configuration Exclude lists ************* -To exclude certain URLs from being tracked, set the environment variable ``OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. +To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_DJANGO_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. For example, :: - export OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_URLS="client/.*/info,healthcheck" + export OTEL_PYTHON_DJANGO_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. diff --git a/ext/opentelemetry-ext-django/tests/conftest.py b/ext/opentelemetry-ext-django/tests/conftest.py index b2b39bc049..8797bfc306 100644 --- a/ext/opentelemetry-ext-django/tests/conftest.py +++ b/ext/opentelemetry-ext-django/tests/conftest.py @@ -16,4 +16,4 @@ def pytest_sessionstart(session): # pylint: disable=unused-argument - environ.setdefault("OPENTELEMETRY_PYTHON_DJANGO_INSTRUMENT", "True") + environ.setdefault("OTEL_PYTHON_DJANGO_INSTRUMENT", "True") diff --git a/ext/opentelemetry-ext-elasticsearch/CHANGELOG.md b/ext/opentelemetry-ext-elasticsearch/CHANGELOG.md index 7425aa5e1e..ede33c6646 100644 --- a/ext/opentelemetry-ext-elasticsearch/CHANGELOG.md +++ b/ext/opentelemetry-ext-elasticsearch/CHANGELOG.md @@ -2,8 +2,10 @@ ## Unreleased +- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) + ## Version 0.10b0 Released 2020-06-23 -- Initial release \ No newline at end of file +- Initial release diff --git a/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/__init__.py b/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/__init__.py index 7c41ba3f0d..6746d1ac70 100644 --- a/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/__init__.py +++ b/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/__init__.py @@ -40,7 +40,7 @@ --- Elasticsearch instrumentation prefixes operation names with the string "Elasticsearch". This -can be changed to a different string by either setting the `OPENTELEMETRY_PYTHON_ELASTICSEARCH_NAME_PREFIX` +can be changed to a different string by either setting the `OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX` environment variable or by passing the prefix as an argument to the instrumentor. For example, @@ -88,8 +88,7 @@ class ElasticsearchInstrumentor(BaseInstrumentor): def __init__(self, span_name_prefix=None): if not span_name_prefix: span_name_prefix = environ.get( - "OPENTELEMETRY_PYTHON_ELASTICSEARCH_NAME_PREFIX", - "Elasticsearch", + "OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX", "Elasticsearch", ) self._span_name_prefix = span_name_prefix.strip() super().__init__() diff --git a/ext/opentelemetry-ext-elasticsearch/tests/test_elasticsearch.py b/ext/opentelemetry-ext-elasticsearch/tests/test_elasticsearch.py index f6ca6bdae1..3c4fe7f70f 100644 --- a/ext/opentelemetry-ext-elasticsearch/tests/test_elasticsearch.py +++ b/ext/opentelemetry-ext-elasticsearch/tests/test_elasticsearch.py @@ -95,7 +95,7 @@ def test_prefix_arg(self, request_mock): def test_prefix_env(self, request_mock): prefix = "prefix-from-args" - env_var = "OPENTELEMETRY_PYTHON_ELASTICSEARCH_NAME_PREFIX" + env_var = "OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX" os.environ[env_var] = prefix ElasticsearchInstrumentor().uninstrument() ElasticsearchInstrumentor().instrument() diff --git a/ext/opentelemetry-ext-flask/CHANGELOG.md b/ext/opentelemetry-ext-flask/CHANGELOG.md index 3109c15902..334084c149 100644 --- a/ext/opentelemetry-ext-flask/CHANGELOG.md +++ b/ext/opentelemetry-ext-flask/CHANGELOG.md @@ -2,9 +2,9 @@ ## Unreleased -## Version 0.11b0 +- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) -Released 2020-07-28 +## Version 0.11b0 - Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) diff --git a/ext/opentelemetry-ext-flask/README.rst b/ext/opentelemetry-ext-flask/README.rst index 395053972e..a75a0cfbaa 100644 --- a/ext/opentelemetry-ext-flask/README.rst +++ b/ext/opentelemetry-ext-flask/README.rst @@ -21,13 +21,13 @@ Configuration Exclude lists ************* -To exclude certain URLs from being tracked, set the environment variable ``OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. +To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FLASK_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. For example, :: - export OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_URLS="client/.*/info,healthcheck" + export OTEL_PYTHON_FLASK_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. diff --git a/ext/opentelemetry-ext-pyramid/CHANGELOG.md b/ext/opentelemetry-ext-pyramid/CHANGELOG.md index f871a0aa5f..2e20d5a0bd 100644 --- a/ext/opentelemetry-ext-pyramid/CHANGELOG.md +++ b/ext/opentelemetry-ext-pyramid/CHANGELOG.md @@ -2,9 +2,9 @@ ## Unreleased -## Version 0.11b0 +- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) -Released 2020-07-28 +## Version 0.11b0 - Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) @@ -12,4 +12,4 @@ Released 2020-07-28 Released 2020-06-10 -- Initial release \ No newline at end of file +- Initial release diff --git a/ext/opentelemetry-ext-pyramid/README.rst b/ext/opentelemetry-ext-pyramid/README.rst index 319d6bff4c..6ec38b83c9 100644 --- a/ext/opentelemetry-ext-pyramid/README.rst +++ b/ext/opentelemetry-ext-pyramid/README.rst @@ -15,13 +15,13 @@ Installation Exclude lists ************* -To exclude certain URLs from being tracked, set the environment variable ``OPENTELEMETRY_PYTHON_PYRAMID_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. +To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_PYRAMID_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. For example, :: - export OPENTELEMETRY_PYTHON_PYRAMID_EXCLUDED_URLS="client/.*/info,healthcheck" + export OTEL_PYTHON_PYRAMID_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index da976ed37e..2226b5d027 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,9 +2,10 @@ ## Unreleased -## Version 0.11b0 +- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` + ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) -Released 2020-07-28 +## Version 0.11b0 - Return INVALID_SPAN if no TracerProvider set for get_current_span ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index 36c0950c0b..0c606b5af4 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -16,30 +16,33 @@ Simple configuration manager This is a configuration manager for OpenTelemetry. It reads configuration -values from environment variables prefixed with ``OPENTELEMETRY_PYTHON_`` whose -characters are only alphanumeric characters and unserscores, except for the -first character after ``OPENTELEMETRY_PYTHON_`` which must not be a number. +values from environment variables prefixed with ``OTEL_`` (for environment +variables that apply to any OpenTelemetry implementation) or with +``OTEL_PYTHON_`` (for environment variables that are specific to the Python +implementation of OpenTelemetry) whose characters are only alphanumeric +characters and unserscores, except for the first character after ``OTEL_`` or +``OTEL_PYTHON_`` which must not be a number. For example, these environment variables will be read: -1. ``OPENTELEMETRY_PYTHON_SOMETHING`` -2. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_`` -3. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND__ELSE`` -4. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND_else`` -5. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND_else2`` +1. ``OTEL_SOMETHING`` +2. ``OTEL_SOMETHING_ELSE_`` +3. ``OTEL_SOMETHING_ELSE_AND__ELSE`` +4. ``OTEL_SOMETHING_ELSE_AND_else`` +5. ``OTEL_SOMETHING_ELSE_AND_else2`` These won't: 1. ``OPENTELEMETRY_PYTH_SOMETHING`` -2. ``OPENTELEMETRY_PYTHON_2_SOMETHING_AND__ELSE`` -3. ``OPENTELEMETRY_PYTHON_SOMETHING_%_ELSE`` +2. ``OTEL_2_SOMETHING_AND__ELSE`` +3. ``OTEL_SOMETHING_%_ELSE`` The values stored in the environment variables can be found in an instance of ``opentelemetry.configuration.Configuration``. This class can be instantiated freely because instantiating it returns always the same object. For example, if the environment variable -``OPENTELEMETRY_PYTHON_METER_PROVIDER`` value is ``my_meter_provider``, then +``OTEL_PYTHON_METER_PROVIDER`` value is ``my_meter_provider``, then ``Configuration().meter_provider == "my_meter_provider"`` would be ``True``. Non defined attributes will always return ``None``. This is intended to make it @@ -49,8 +52,8 @@ Environment variables used by OpenTelemetry ------------------------------------------- -1. OPENTELEMETRY_PYTHON_METER_PROVIDER -2. OPENTELEMETRY_PYTHON_TRACER_PROVIDER +1. OTEL_PYTHON_METER_PROVIDER +2. OTEL_PYTHON_TRACER_PROVIDER The value of these environment variables should be the name of the entry point that points to the class that implements either provider. This OpenTelemetry @@ -70,7 +73,7 @@ } To use the meter provider above, then the -``OPENTELEMETRY_PYTHON_METER_PROVIDER`` should be set to +``OTEL_PYTHON_METER_PROVIDER`` should be set to ``"default_meter_provider"`` (this is not actually necessary since the OpenTelemetry API provided providers are the default ones used if no configuration is found in the environment variables). @@ -110,13 +113,11 @@ def __new__(cls) -> "Configuration": instance = super().__new__(cls) for key, value_str in environ.items(): - match = fullmatch( - r"OPENTELEMETRY_PYTHON_([A-Za-z_][\w_]*)", key - ) + match = fullmatch(r"OTEL_(PYTHON_)?([A-Za-z_][\w_]*)", key) if match is not None: - key = match.group(1) + key = match.group(2) value = value_str # type: ConfigValue if value_str == "True": diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index adf1bc0869..a5f24118d3 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -55,7 +55,7 @@ def wrapper( default_context = "contextvars_context" configured_context = environ.get( - "OPENTELEMETRY_CONTEXT", default_context + "OTEL_CONTEXT", default_context ) # type: str try: _RUNTIME_CONTEXT = next( diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index 316847a004..557591513b 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -32,10 +32,10 @@ def test_singleton(self) -> None: @patch.dict( "os.environ", # type: ignore { - "OPENTELEMETRY_PYTHON_METER_PROVIDER": "meter_provider", - "OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider", - "OPENTELEMETRY_PYTHON_OThER": "other", - "OPENTELEMETRY_PYTHON_OTHER_7": "other_7", + "OTEL_PYTHON_METER_PROVIDER": "meter_provider", + "OTEL_PYTHON_TRACER_PROVIDER": "tracer_provider", + "OTEL_OThER": "other", + "OTEL_OTHER_7": "other_7", "OPENTELEMETRY_PTHON_TRACEX_PROVIDER": "tracex_provider", }, ) @@ -56,7 +56,7 @@ def test_environment_variables(self) -> None: @patch.dict( "os.environ", # type: ignore - {"OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider"}, + {"OTEL_PYTHON_TRACER_PROVIDER": "tracer_provider"}, ) def test_property(self) -> None: with self.assertRaises(AttributeError): @@ -79,8 +79,7 @@ def test_getattr(self) -> None: def test_reset(self) -> None: environ_patcher = patch.dict( - "os.environ", - {"OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider"}, + "os.environ", {"OTEL_PYTHON_TRACER_PROVIDER": "tracer_provider"}, ) environ_patcher.start() @@ -99,10 +98,7 @@ def test_reset(self) -> None: @patch.dict( "os.environ", # type: ignore - { - "OPENTELEMETRY_PYTHON_TRUE": "True", - "OPENTELEMETRY_PYTHON_FALSE": "False", - }, + {"OTEL_TRUE": "True", "OTEL_FALSE": "False"}, ) def test_boolean(self) -> None: self.assertIsInstance( @@ -117,9 +113,9 @@ def test_boolean(self) -> None: @patch.dict( "os.environ", # type: ignore { - "OPENTELEMETRY_PYTHON_POSITIVE_INTEGER": "123", - "OPENTELEMETRY_PYTHON_NEGATIVE_INTEGER": "-123", - "OPENTELEMETRY_PYTHON_NON_INTEGER": "-12z3", + "OTEL_POSITIVE_INTEGER": "123", + "OTEL_NEGATIVE_INTEGER": "-123", + "OTEL_NON_INTEGER": "-12z3", }, ) def test_integer(self) -> None: @@ -136,9 +132,9 @@ def test_integer(self) -> None: @patch.dict( "os.environ", # type: ignore { - "OPENTELEMETRY_PYTHON_POSITIVE_FLOAT": "123.123", - "OPENTELEMETRY_PYTHON_NEGATIVE_FLOAT": "-123.123", - "OPENTELEMETRY_PYTHON_NON_FLOAT": "-12z3.123", + "OTEL_POSITIVE_FLOAT": "123.123", + "OTEL_NEGATIVE_FLOAT": "-123.123", + "OTEL_NON_FLOAT": "-12z3.123", }, ) def test_float(self) -> None: diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index cbb96eabcd..bf7618e77f 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,9 +2,10 @@ ## Unreleased -## Version 0.11b0 +- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` + ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) -Released 2020-07-28 +## Version 0.11b0 - Add support for resources and resource detector ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) diff --git a/opentelemetry-sdk/tests/conftest.py b/opentelemetry-sdk/tests/conftest.py index 80a79222a1..ed5853b43d 100644 --- a/opentelemetry-sdk/tests/conftest.py +++ b/opentelemetry-sdk/tests/conftest.py @@ -20,11 +20,11 @@ def pytest_sessionstart(session): # pylint: disable=unused-argument if version_info < (3, 5): # contextvars are not supported in 3.4, use thread-local storage - environ["OPENTELEMETRY_CONTEXT"] = "threadlocal_context" + environ["OTEL_CONTEXT"] = "threadlocal_context" else: - environ["OPENTELEMETRY_CONTEXT"] = "contextvars_context" + environ["OTEL_CONTEXT"] = "contextvars_context" def pytest_sessionfinish(session): # pylint: disable=unused-argument - environ.pop("OPENTELEMETRY_CONTEXT") + environ.pop("OTEL_CONTEXT") From 1112792b18bb177ba58fa11ebaecc9597041a73c Mon Sep 17 00:00:00 2001 From: Emmanuel Courreges Date: Thu, 30 Jul 2020 17:18:38 +0200 Subject: [PATCH 0484/1517] Add proper length zero padding to hex strings of traceId, spanId, parentId sent on the wire (#908) --- .../CHANGELOG.md | 4 +- .../opentelemetry/exporter/zipkin/__init__.py | 9 +-- .../tests/test_zipkin_exporter.py | 66 +++++++++++++++++++ 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index 2218d44c20..1063d35cf5 100644 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -4,6 +4,8 @@ - Change package name to opentelemetry-exporter-zipkin ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) +- Add proper length zero padding to hex strings of traceId, spanId, parentId sent on the wire, for compatibility with jaeger-collector + ([#908](https://github.com/open-telemetry/opentelemetry-python/pull/908)) ## 0.8b0 @@ -23,4 +25,4 @@ Released 2020-05-12 Released 2020-02-21 -- Initial release \ No newline at end of file +- Initial release diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 10bd6e93e9..b0eb1bce0f 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -168,8 +168,9 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): duration_mus = _nsec_to_usec_round(span.end_time - span.start_time) zipkin_span = { - "traceId": format(trace_id, "x"), - "id": format(span_id, "x"), + # Ensure left-zero-padding of traceId, spanId, parentId + "traceId": format(trace_id, "032x"), + "id": format(span_id, "016x"), "name": span.name, "timestamp": start_timestamp_mus, "duration": duration_mus, @@ -184,10 +185,10 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): if isinstance(span.parent, Span): zipkin_span["parentId"] = format( - span.parent.get_context().span_id, "x" + span.parent.get_context().span_id, "016x" ) elif isinstance(span.parent, SpanContext): - zipkin_span["parentId"] = format(span.parent.span_id, "x") + zipkin_span["parentId"] = format(span.parent.span_id, "016x") zipkin_spans.append(zipkin_span) return zipkin_spans diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 9f6da2e240..f6e24a1495 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -264,6 +264,72 @@ def test_export(self): headers={"Content-Type": "application/json"}, ) + # pylint: disable=too-many-locals + def test_zero_padding(self): + """test that hex ids starting with 0 + are properly padded to 16 or 32 hex chars + when exported + """ + + span_names = "testZeroes" + trace_id = 0x0E0C63257DE34C926F9EFCD03927272E + span_id = 0x04BF92DEEFC58C92 + parent_id = 0x0AAAAAAAAAAAAAAA + + start_time = 683647322 * 10 ** 9 # in ns + duration = 50 * 10 ** 6 + end_time = start_time + duration + + span_context = trace_api.SpanContext( + trace_id, + span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + parent_context = trace_api.SpanContext( + trace_id, parent_id, is_remote=False + ) + + otel_span = trace.Span( + name=span_names[0], context=span_context, parent=parent_context, + ) + + otel_span.start(start_time=start_time) + otel_span.end(end_time=end_time) + + service_name = "test-service" + local_endpoint = {"serviceName": service_name, "port": 9411} + + exporter = ZipkinSpanExporter(service_name) + # Check traceId are properly lowercase 16 or 32 hex + expected = [ + { + "traceId": "0e0c63257de34c926f9efcd03927272e", + "id": "04bf92deefc58c92", + "name": span_names[0], + "timestamp": start_time // 10 ** 3, + "duration": duration // 10 ** 3, + "localEndpoint": local_endpoint, + "kind": None, + "tags": {}, + "annotations": None, + "debug": True, + "parentId": "0aaaaaaaaaaaaaaa", + } + ] + + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export([otel_span]) + self.assertEqual(SpanExportResult.SUCCESS, status) + + mock_post.assert_called_with( + url="http://localhost:9411/api/v2/spans", + data=json.dumps(expected), + headers={"Content-Type": "application/json"}, + ) + @patch("requests.post") def test_invalid_response(self, mock_post): mock_post.return_value = MockResponse(404) From 242d5a7314886b8a30b6c69051dd8450f00d2baf Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 3 Aug 2020 01:46:23 -0700 Subject: [PATCH 0485/1517] [WIP] Views API Prototype (#596) --- docs/examples/basic_meter/basic_metrics.py | 6 +- .../basic_meter/calling_conventions.py | 5 +- docs/examples/basic_meter/observer.py | 2 - docs/examples/basic_meter/view.py | 112 ++++++ .../test_otcollector_metrics_exporter.py | 8 +- .../tests/test_prometheus_exporter.py | 29 +- .../src/opentelemetry/ext/grpc/_utilities.py | 5 - .../src/opentelemetry/metrics/__init__.py | 3 - opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/metrics/__init__.py | 136 +++---- .../sdk/metrics/export/aggregate.py | 159 ++++++-- .../sdk/metrics/export/batcher.py | 89 ++--- .../src/opentelemetry/sdk/metrics/view.py | 186 +++++++++ .../src/opentelemetry/sdk/util/__init__.py | 5 + .../tests/metrics/export/test_export.py | 116 ++---- .../tests/metrics/test_metrics.py | 357 ++++++++---------- opentelemetry-sdk/tests/metrics/test_view.py | 191 ++++++++++ 17 files changed, 910 insertions(+), 501 deletions(-) create mode 100644 docs/examples/basic_meter/view.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/view.py create mode 100644 opentelemetry-sdk/tests/metrics/test_view.py diff --git a/docs/examples/basic_meter/basic_metrics.py b/docs/examples/basic_meter/basic_metrics.py index e65aa788b8..ff050d5cde 100644 --- a/docs/examples/basic_meter/basic_metrics.py +++ b/docs/examples/basic_meter/basic_metrics.py @@ -19,6 +19,7 @@ - How to configure a meter passing a sateful or stateless. - How to configure an exporter and how to create a controller. - How to create some metrics intruments and how to capture data with them. +- How to use views to specify aggregation types for each metric instrument. """ import sys import time @@ -57,7 +58,6 @@ unit="1", value_type=int, metric_type=Counter, - label_keys=("environment",), ) requests_size = meter.create_metric( @@ -66,7 +66,6 @@ unit="1", value_type=int, metric_type=ValueRecorder, - label_keys=("environment",), ) # Labels are used to identify key-values that are associated with a specific @@ -86,4 +85,5 @@ requests_counter.add(35, testing_labels) requests_size.record(2, testing_labels) -time.sleep(5) + +input("...\n") diff --git a/docs/examples/basic_meter/calling_conventions.py b/docs/examples/basic_meter/calling_conventions.py index 3615f60d7d..376f030e31 100644 --- a/docs/examples/basic_meter/calling_conventions.py +++ b/docs/examples/basic_meter/calling_conventions.py @@ -33,7 +33,6 @@ unit="1", value_type=int, metric_type=Counter, - label_keys=("environment",), ) requests_size = meter.create_metric( @@ -42,7 +41,6 @@ unit="1", value_type=int, metric_type=ValueRecorder, - label_keys=("environment",), ) clicks_counter = meter.create_metric( @@ -51,7 +49,6 @@ unit="1", value_type=int, metric_type=Counter, - label_keys=("environment",), ) labels = {"environment": "staging"} @@ -78,3 +75,5 @@ # specified labels for each. meter.record_batch(labels, ((requests_counter, 50), (clicks_counter, 70))) time.sleep(5) + +input("...\n") diff --git a/docs/examples/basic_meter/observer.py b/docs/examples/basic_meter/observer.py index 076c416c0a..a53f37d1ce 100644 --- a/docs/examples/basic_meter/observer.py +++ b/docs/examples/basic_meter/observer.py @@ -41,7 +41,6 @@ def get_cpu_usage_callback(observer): unit="1", value_type=float, observer_type=ValueObserver, - label_keys=("cpu_number",), ) @@ -58,7 +57,6 @@ def get_ram_usage_callback(observer): unit="1", value_type=float, observer_type=ValueObserver, - label_keys=(), ) input("Metrics will be printed soon. Press a key to finish...\n") diff --git a/docs/examples/basic_meter/view.py b/docs/examples/basic_meter/view.py new file mode 100644 index 0000000000..cc4ba1a683 --- /dev/null +++ b/docs/examples/basic_meter/view.py @@ -0,0 +1,112 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This example shows how to use the different modes to capture metrics. +It shows the usage of the direct, bound and batch calling conventions. +""" +from opentelemetry import metrics +from opentelemetry.sdk.metrics import ( + MeterProvider, + UpDownCounter, + ValueRecorder, +) +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.aggregate import ( + HistogramAggregator, + LastValueAggregator, + MinMaxSumCountAggregator, + SumAggregator, +) +from opentelemetry.sdk.metrics.view import View, ViewConfig + +# Use the meter type provided by the SDK package +metrics.set_meter_provider(MeterProvider()) +meter = metrics.get_meter(__name__) +metrics.get_meter_provider().start_pipeline(meter, ConsoleMetricsExporter(), 5) + +requests_counter = meter.create_metric( + name="requests", + description="number of requests", + unit="1", + value_type=int, + metric_type=UpDownCounter, +) + +requests_size = meter.create_metric( + name="requests_size", + description="size of requests", + unit="1", + value_type=int, + metric_type=ValueRecorder, +) + +# Views are used to define an aggregation type and label keys to aggregate by +# for a given metric + +# Two views with the same metric and aggregation type but different label keys +# With ViewConfig.LABEL_KEYS, all labels but the ones defined in label_keys are +# dropped from the aggregation +counter_view1 = View( + requests_counter, + SumAggregator, + label_keys=["environment"], + view_config=ViewConfig.LABEL_KEYS, +) +counter_view2 = View( + requests_counter, + MinMaxSumCountAggregator, + label_keys=["os_type"], + view_config=ViewConfig.LABEL_KEYS, +) +# This view has ViewConfig set to UNGROUPED, meaning all recorded metrics take +# the labels directly without and consideration for label_keys +counter_view3 = View( + requests_counter, + LastValueAggregator, + label_keys=["environment"], # is not used due to ViewConfig.UNGROUPED + view_config=ViewConfig.UNGROUPED, +) +# This view uses the HistogramAggregator which accepts an option config +# parameter to specify the bucket ranges +size_view = View( + requests_size, + HistogramAggregator, + label_keys=["environment"], # is not used due to ViewConfig.UNGROUPED + aggregator_config={"bounds": [20, 40, 60, 80, 100]}, + view_config=ViewConfig.UNGROUPED, +) + +# Register the views to the view manager to use the views. Views MUST be +# registered before recording metrics. Metrics that are recorded without +# views defined for them will use a default for that type of metric +meter.register_view(counter_view1) +meter.register_view(counter_view2) +meter.register_view(counter_view3) +meter.register_view(size_view) + +# The views will evaluate the labels passed into the record and aggregate upon +# the unique labels that are generated +# view1 labels will evaluate to {"environment": "staging"} +# view2 labels will evaluate to {"os_type": linux} +# view3 labels will evaluate to {"environment": "staging", "os_type": "linux"} +requests_counter.add(100, {"environment": "staging", "os_type": "linux"}) + +# Since this is using the HistogramAggregator, the bucket counts will be reflected +# with each record +requests_size.record(25, {"test": "value"}) +requests_size.record(-3, {"test": "value"}) +requests_size.record(200, {"test": "value"}) + +input("...\n") diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py index eddaf96360..1403c6c59e 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py @@ -25,7 +25,7 @@ Counter, MeterProvider, ValueRecorder, - get_labels_as_key, + get_dict_as_key, ) from opentelemetry.sdk.metrics.export import ( MetricRecord, @@ -42,7 +42,7 @@ def setUpClass(cls): metrics.set_meter_provider(MeterProvider()) cls._meter = metrics.get_meter(__name__) cls._labels = {"environment": "staging"} - cls._key_labels = get_labels_as_key(cls._labels) + cls._key_labels = get_dict_as_key(cls._labels) def test_constructor(self): mock_get_node = mock.Mock() @@ -119,7 +119,7 @@ def test_export(self): client=mock_client, host_name=host_name ) test_metric = self._meter.create_metric( - "testname", "testdesc", "unit", int, Counter, ["environment"] + "testname", "testdesc", "unit", int, Counter, ) record = MetricRecord( test_metric, self._key_labels, aggregate.SumAggregator(), @@ -142,7 +142,7 @@ def test_export(self): def test_translate_to_collector(self): test_metric = self._meter.create_metric( - "testname", "testdesc", "unit", int, Counter, ["environment"] + "testname", "testdesc", "unit", int, Counter, ) aggregator = aggregate.SumAggregator() aggregator.update(123) diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index d624086ce3..56ab29b70d 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -32,15 +32,10 @@ def setUp(self): set_meter_provider(metrics.MeterProvider()) self._meter = get_meter_provider().get_meter(__name__) self._test_metric = self._meter.create_metric( - "testname", - "testdesc", - "unit", - int, - metrics.Counter, - ["environment"], + "testname", "testdesc", "unit", int, metrics.Counter, ) labels = {"environment": "staging"} - self._labels_key = metrics.get_labels_as_key(labels) + self._labels_key = metrics.get_dict_as_key(labels) self._mock_registry_register = mock.Mock() self._registry_register_patch = mock.patch( @@ -78,15 +73,10 @@ def test_export(self): def test_counter_to_prometheus(self): meter = get_meter_provider().get_meter(__name__) metric = meter.create_metric( - "test@name", - "testdesc", - "unit", - int, - metrics.Counter, - ["environment@", "os"], + "test@name", "testdesc", "unit", int, metrics.Counter, ) labels = {"environment@": "staging", "os": "Windows"} - key_labels = metrics.get_labels_as_key(labels) + key_labels = metrics.get_dict_as_key(labels) aggregator = SumAggregator() aggregator.update(123) aggregator.take_checkpoint() @@ -117,7 +107,7 @@ def test_invalid_metric(self): "tesname", "testdesc", "unit", int, StubMetric ) labels = {"environment": "staging"} - key_labels = metrics.get_labels_as_key(labels) + key_labels = metrics.get_dict_as_key(labels) record = MetricRecord(metric, key_labels, None) collector = CustomCollector("testprefix") collector.add_metrics_data([record]) @@ -143,15 +133,8 @@ def __init__( unit: str, value_type, meter, - label_keys, enabled: bool = True, ): super().__init__( - name, - description, - unit, - value_type, - meter, - label_keys=label_keys, - enabled=enabled, + name, description, unit, value_type, meter, enabled=enabled, ) diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py index 1dfe31ec99..a577635582 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py @@ -45,7 +45,6 @@ def __init__(self, meter, span_kind): self._meter = meter service_name = "grpcio" self._span_kind = span_kind - base_attributes = ["method"] if self._meter: self._duration = self._meter.create_metric( @@ -54,7 +53,6 @@ def __init__(self, meter, span_kind): unit="ms", value_type=float, metric_type=ValueRecorder, - label_keys=base_attributes + ["error", "status_code"], ) self._error_count = self._meter.create_metric( name="{}/{}/errors".format(service_name, span_kind), @@ -62,7 +60,6 @@ def __init__(self, meter, span_kind): unit="1", value_type=int, metric_type=Counter, - label_keys=base_attributes + ["status_code"], ) self._bytes_in = self._meter.create_metric( name="{}/{}/bytes_in".format(service_name, span_kind), @@ -70,7 +67,6 @@ def __init__(self, meter, span_kind): unit="by", value_type=int, metric_type=Counter, - label_keys=base_attributes, ) self._bytes_out = self._meter.create_metric( name="{}/{}/bytes_out".format(service_name, span_kind), @@ -78,7 +74,6 @@ def __init__(self, meter, span_kind): unit="by", value_type=int, metric_type=Counter, - label_keys=base_attributes, ) def record_bytes_in(self, bytes_in, method): diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 90f2f03f56..22b7b880b4 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -342,7 +342,6 @@ def create_metric( unit: str, value_type: Type[ValueT], metric_type: Type[MetricT], - label_keys: Sequence[str] = (), enabled: bool = True, ) -> "Metric": """Creates a ``metric_kind`` metric with type ``value_type``. @@ -354,7 +353,6 @@ def create_metric( (https://unitsofmeasure.org/ucum.html). value_type: The type of values being recorded by the metric. metric_type: The type of metric being created. - label_keys: The keys for the labels with dynamic values. enabled: Whether to report the metric by default. Returns: A new ``metric_type`` metric with values of ``value_type``. """ @@ -413,7 +411,6 @@ def create_metric( unit: str, value_type: Type[ValueT], metric_type: Type[MetricT], - label_keys: Sequence[str] = (), enabled: bool = True, ) -> "Metric": # pylint: disable=no-self-use diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index bf7618e77f..d4b46202e5 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -4,6 +4,8 @@ - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) +- Implement Views in metrics SDK + ([#596](https://github.com/open-telemetry/opentelemetry-python/pull/596)) ## Version 0.11b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 62c616a3e2..2af8a551ee 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -23,19 +23,20 @@ MetricsExporter, ) from opentelemetry.sdk.metrics.export.aggregate import Aggregator -from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.batcher import Batcher from opentelemetry.sdk.metrics.export.controller import PushController +from opentelemetry.sdk.metrics.view import ( + ViewData, + ViewManager, + get_default_aggregator, +) from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util import get_dict_as_key from opentelemetry.sdk.util.instrumentation import InstrumentationInfo logger = logging.getLogger(__name__) -def get_labels_as_key(labels: Dict[str, str]) -> Tuple[Tuple[str, str]]: - """Gets a list of labels that can be used as a key in a dictionary.""" - return tuple(sorted(labels.items())) - - class BaseBoundInstrument: """Class containing common behavior for all bound metric instruments. @@ -43,37 +44,38 @@ class BaseBoundInstrument: instruments for a specific set of labels. Args: - value_type: The type of values for this bound instrument (int, float). - enabled: True if the originating instrument is enabled. - aggregator: The aggregator for this bound metric instrument. Will - handle aggregation upon updates and checkpointing of values for - exporting. + labels: A set of labels as keys that bind this metric instrument. + metric: The metric that created this bound instrument. """ def __init__( - self, - value_type: Type[metrics_api.ValueT], - enabled: bool, - aggregator: Aggregator, + self, labels: Tuple[Tuple[str, str]], metric: metrics_api.MetricT ): - self.value_type = value_type - self.enabled = enabled - self.aggregator = aggregator + self._labels = labels + self._metric = metric + self.view_datas = metric.meter.view_manager.get_view_datas( + metric, labels + ) + self._view_datas_lock = threading.Lock() self._ref_count = 0 self._ref_count_lock = threading.Lock() def _validate_update(self, value: metrics_api.ValueT) -> bool: - if not self.enabled: + if not self._metric.enabled: return False - if not isinstance(value, self.value_type): + if not isinstance(value, self._metric.value_type): logger.warning( - "Invalid value passed for %s.", self.value_type.__name__ + "Invalid value passed for %s.", + self._metric.value_type.__name__, ) return False return True def update(self, value: metrics_api.ValueT): - self.aggregator.update(value) + with self._view_datas_lock: + # record the value for each view_data belonging to this aggregator + for view_data in self.view_datas: + view_data.record(value) def release(self): self.decrease_ref_count() @@ -90,11 +92,6 @@ def ref_count(self): with self._ref_count_lock: return self._ref_count - def __repr__(self): - return '{}(data="{}")'.format( - type(self).__name__, self.aggregator.current - ) - class BoundCounter(metrics_api.BoundCounter, BaseBoundInstrument): def add(self, value: metrics_api.ValueT) -> None: @@ -151,7 +148,6 @@ def __init__( unit: str, value_type: Type[metrics_api.ValueT], meter: "Meter", - label_keys: Sequence[str] = (), enabled: bool = True, ): self.name = name @@ -159,23 +155,17 @@ def __init__( self.unit = unit self.value_type = value_type self.meter = meter - self.label_keys = label_keys self.enabled = enabled self.bound_instruments = {} self.bound_instruments_lock = threading.Lock() def bind(self, labels: Dict[str, str]) -> BaseBoundInstrument: """See `opentelemetry.metrics.Metric.bind`.""" - key = get_labels_as_key(labels) + key = get_dict_as_key(labels) with self.bound_instruments_lock: bound_instrument = self.bound_instruments.get(key) if bound_instrument is None: - bound_instrument = self.BOUND_INSTR_TYPE( - self.value_type, - self.enabled, - # Aggregator will be created based off type of metric - self.meter.batcher.aggregator_for(self.__class__), - ) + bound_instrument = self.BOUND_INSTR_TYPE(key, self) self.bound_instruments[key] = bound_instrument bound_instrument.increase_ref_count() return bound_instrument @@ -250,7 +240,6 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], - meter: "Meter", label_keys: Sequence[str] = (), enabled: bool = True, ): @@ -259,7 +248,6 @@ def __init__( self.description = description self.unit = unit self.value_type = value_type - self.meter = meter self.label_keys = label_keys self.enabled = enabled @@ -268,21 +256,19 @@ def __init__( def observe( self, value: metrics_api.ValueT, labels: Dict[str, str] ) -> None: - key = get_labels_as_key(labels) + key = get_dict_as_key(labels) if not self._validate_observe(value, key): return if key not in self.aggregators: # TODO: how to cleanup aggregators? - self.aggregators[key] = self.meter.batcher.aggregator_for( - self.__class__ - ) + self.aggregators[key] = get_default_aggregator(self)() aggregator = self.aggregators[key] aggregator.update(value) # pylint: disable=W0613 def _validate_observe( - self, value: metrics_api.ValueT, key: Tuple[Tuple[str, str]], + self, value: metrics_api.ValueT, key: Tuple[Tuple[str, str]] ) -> bool: if not self.enabled: return False @@ -315,7 +301,7 @@ class SumObserver(Observer, metrics_api.SumObserver): """See `opentelemetry.metrics.SumObserver`.""" def _validate_observe( - self, value: metrics_api.ValueT, key: Tuple[Tuple[str, str]], + self, value: metrics_api.ValueT, key: Tuple[Tuple[str, str]] ) -> bool: if not super()._validate_observe(value, key): return False @@ -344,7 +330,7 @@ class Record: def __init__( self, instrument: metrics_api.InstrumentT, - labels: Dict[str, str], + labels: Tuple[Tuple[str, str]], aggregator: Aggregator, ): self.instrument = instrument @@ -366,11 +352,13 @@ def __init__( instrumentation_info: "InstrumentationInfo", ): self.instrumentation_info = instrumentation_info - self.batcher = UngroupedBatcher(source.stateful) + self.batcher = Batcher(source.stateful) self.resource = source.resource self.metrics = set() self.observers = set() + self.metrics_lock = threading.Lock() self.observers_lock = threading.Lock() + self.view_manager = ViewManager() def collect(self) -> None: """Collects all the metrics created with this `Meter` for export. @@ -387,23 +375,24 @@ def _collect_metrics(self) -> None: for metric in self.metrics: if not metric.enabled: continue - to_remove = [] - with metric.bound_instruments_lock: - for labels, bound_instr in metric.bound_instruments.items(): - # TODO: Consider storing records in memory? - record = Record(metric, labels, bound_instr.aggregator) - # Checkpoints the current aggregators - # Applies different batching logic based on type of batcher - self.batcher.process(record) - - if bound_instr.ref_count() == 0: + for ( + labels, + bound_instrument, + ) in metric.bound_instruments.items(): + for view_data in bound_instrument.view_datas: + record = Record( + metric, view_data.labels, view_data.aggregator + ) + self.batcher.process(record) + + if bound_instrument.ref_count() == 0: to_remove.append(labels) - # Remove handles that were released - for labels in to_remove: - del metric.bound_instruments[labels] + # Remove handles that were released + for labels in to_remove: + del metric.bound_instruments[labels] def _collect_observers(self) -> None: with self.observers_lock: @@ -436,21 +425,15 @@ def create_metric( unit: str, value_type: Type[metrics_api.ValueT], metric_type: Type[metrics_api.MetricT], - label_keys: Sequence[str] = (), enabled: bool = True, ) -> metrics_api.MetricT: """See `opentelemetry.metrics.Meter.create_metric`.""" # Ignore type b/c of mypy bug in addition to missing annotations metric = metric_type( # type: ignore - name, - description, - unit, - value_type, - self, - label_keys=label_keys, - enabled=enabled, + name, description, unit, value_type, self, enabled=enabled ) - self.metrics.add(metric) + with self.metrics_lock: + self.metrics.add(metric) return metric def register_observer( @@ -465,14 +448,7 @@ def register_observer( enabled: bool = True, ) -> metrics_api.Observer: ob = observer_type( - callback, - name, - description, - unit, - value_type, - self, - label_keys, - enabled, + callback, name, description, unit, value_type, label_keys, enabled ) with self.observers_lock: self.observers.add(ob) @@ -482,6 +458,12 @@ def unregister_observer(self, observer: metrics_api.Observer) -> None: with self.observers_lock: self.observers.remove(observer) + def register_view(self, view): + self.view_manager.register_view(view) + + def unregister_view(self, view): + self.view_manager.unregister_view(view) + class MeterProvider(metrics_api.MeterProvider): """See `opentelemetry.metrics.MeterProvider`. @@ -519,7 +501,7 @@ def get_meter( return Meter( self, InstrumentationInfo( - instrumenting_module_name, instrumenting_library_version, + instrumenting_module_name, instrumenting_library_version ), ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 3292529aed..121f39a98b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -13,11 +13,14 @@ # limitations under the License. import abc +import logging import threading -from collections import namedtuple +from collections import OrderedDict, namedtuple from opentelemetry.util import time_ns +logger = logging.getLogger(__name__) + class Aggregator(abc.ABC): """Base class for aggregators. @@ -26,9 +29,13 @@ class Aggregator(abc.ABC): snapshot of these values upon export (checkpoint). """ - def __init__(self): + def __init__(self, config=None): self.current = None self.checkpoint = None + if config: + self.config = config + else: + self.config = {} @abc.abstractmethod def update(self, value): @@ -44,10 +51,10 @@ def merge(self, other): class SumAggregator(Aggregator): - """Aggregator for Counter metrics.""" + """Aggregator for counter metrics.""" - def __init__(self): - super().__init__() + def __init__(self, config=None): + super().__init__(config=config) self.current = 0 self.checkpoint = 0 self._lock = threading.Lock() @@ -64,11 +71,12 @@ def take_checkpoint(self): self.current = 0 def merge(self, other): - with self._lock: - self.checkpoint += other.checkpoint - self.last_update_timestamp = get_latest_timestamp( - self.last_update_timestamp, other.last_update_timestamp - ) + if verify_type(self, other): + with self._lock: + self.checkpoint += other.checkpoint + self.last_update_timestamp = get_latest_timestamp( + self.last_update_timestamp, other.last_update_timestamp + ) class MinMaxSumCountAggregator(Aggregator): @@ -90,8 +98,8 @@ def _merge_checkpoint(cls, val1, val2): val1.count + val2.count, ) - def __init__(self): - super().__init__() + def __init__(self, config=None): + super().__init__(config=config) self.current = self._EMPTY self.checkpoint = self._EMPTY self._lock = threading.Lock() @@ -116,20 +124,97 @@ def take_checkpoint(self): self.current = self._EMPTY def merge(self, other): - with self._lock: - self.checkpoint = self._merge_checkpoint( - self.checkpoint, other.checkpoint - ) - self.last_update_timestamp = get_latest_timestamp( - self.last_update_timestamp, other.last_update_timestamp + if verify_type(self, other): + with self._lock: + self.checkpoint = self._merge_checkpoint( + self.checkpoint, other.checkpoint + ) + self.last_update_timestamp = get_latest_timestamp( + self.last_update_timestamp, other.last_update_timestamp + ) + + +class HistogramAggregator(Aggregator): + """Agregator for ValueRecorder metrics that keeps a histogram of values.""" + + def __init__(self, config=None): + super().__init__(config=config) + self._lock = threading.Lock() + self.last_update_timestamp = None + boundaries = self.config.get("bounds") + if boundaries and self._validate_boundaries(boundaries): + self._boundaries = boundaries + else: + # no buckets except < 0 and > + self._boundaries = (0,) + + self.current = OrderedDict([(bb, 0) for bb in self._boundaries]) + self.checkpoint = OrderedDict([(bb, 0) for bb in self._boundaries]) + + self.current[">"] = 0 + self.checkpoint[">"] = 0 + + # pylint: disable=R0201 + def _validate_boundaries(self, boundaries): + if not boundaries: + logger.warning("Bounds is empty. Using default.") + return False + if not all( + boundaries[ii] < boundaries[ii + 1] + for ii in range(len(boundaries) - 1) + ): + logger.warning( + "Bounds must be sorted in increasing order. Using default." ) + return False + return True + + @classmethod + def _merge_checkpoint(cls, val1, val2): + if val1.keys() == val2.keys(): + for ii, bb in val2.items(): + val1[ii] += bb + else: + logger.warning("Cannot merge histograms with different buckets.") + return val1 + + def update(self, value): + with self._lock: + if self.current is None: + self.current = [0 for ii in range(len(self._boundaries) + 1)] + # greater than max value + if value >= self._boundaries[len(self._boundaries) - 1]: + self.current[">"] += 1 + else: + for bb in self._boundaries: + # find first bucket that value is less than + if value < bb: + self.current[bb] += 1 + break + self.last_update_timestamp = time_ns() + + def take_checkpoint(self): + with self._lock: + self.checkpoint = self.current + self.current = OrderedDict([(bb, 0) for bb in self._boundaries]) + self.current[">"] = 0 + + def merge(self, other): + if verify_type(self, other): + with self._lock: + self.checkpoint = self._merge_checkpoint( + self.checkpoint, other.checkpoint + ) + self.last_update_timestamp = get_latest_timestamp( + self.last_update_timestamp, other.last_update_timestamp + ) class LastValueAggregator(Aggregator): """Aggregator that stores last value results.""" - def __init__(self): - super().__init__() + def __init__(self, config=None): + super().__init__(config=config) self._lock = threading.Lock() self.last_update_timestamp = None @@ -158,8 +243,8 @@ class ValueObserverAggregator(Aggregator): _TYPE = namedtuple("minmaxsumcountlast", "min max sum count last") - def __init__(self): - super().__init__() + def __init__(self, config=None): + super().__init__(config=config) self.mmsc = MinMaxSumCountAggregator() self.current = None self.checkpoint = self._TYPE(None, None, None, 0, None) @@ -175,14 +260,15 @@ def take_checkpoint(self): self.checkpoint = self._TYPE(*(self.mmsc.checkpoint + (self.current,))) def merge(self, other): - self.mmsc.merge(other.mmsc) - last = self.checkpoint.last - self.last_update_timestamp = get_latest_timestamp( - self.last_update_timestamp, other.last_update_timestamp - ) - if self.last_update_timestamp == other.last_update_timestamp: - last = other.checkpoint.last - self.checkpoint = self._TYPE(*(self.mmsc.checkpoint + (last,))) + if verify_type(self, other): + self.mmsc.merge(other.mmsc) + last = self.checkpoint.last + self.last_update_timestamp = get_latest_timestamp( + self.last_update_timestamp, other.last_update_timestamp + ) + if self.last_update_timestamp == other.last_update_timestamp: + last = other.checkpoint.last + self.checkpoint = self._TYPE(*(self.mmsc.checkpoint + (last,))) def get_latest_timestamp(time_stamp, other_timestamp): @@ -192,3 +278,16 @@ def get_latest_timestamp(time_stamp, other_timestamp): if time_stamp < other_timestamp: return other_timestamp return time_stamp + + +# pylint: disable=R1705 +def verify_type(this, other): + if isinstance(other, this.__class__): + return True + else: + logger.warning( + "Error in merging %s with %s.", + this.__class__.__name__, + other.__class__.__name__, + ) + return False diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 527760d51b..79b27674c0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -12,29 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import abc -from typing import Sequence, Type +from typing import Sequence -from opentelemetry.metrics import ( - Counter, - InstrumentT, - SumObserver, - UpDownCounter, - UpDownSumObserver, - ValueObserver, - ValueRecorder, -) from opentelemetry.sdk.metrics.export import MetricRecord -from opentelemetry.sdk.metrics.export.aggregate import ( - Aggregator, - LastValueAggregator, - MinMaxSumCountAggregator, - SumAggregator, - ValueObserverAggregator, -) +from opentelemetry.sdk.util import get_dict_as_key -class Batcher(abc.ABC): +class Batcher: """Base class for all batcher types. The batcher is responsible for storing the aggregators and aggregated @@ -50,23 +34,6 @@ def __init__(self, stateful: bool): # (deltas) self.stateful = stateful - def aggregator_for(self, instrument_type: Type[InstrumentT]) -> Aggregator: - """Returns an aggregator based on metric instrument type. - - Aggregators keep track of and updates values when metrics get updated. - """ - # pylint:disable=R0201 - if issubclass(instrument_type, (Counter, UpDownCounter)): - return SumAggregator() - if issubclass(instrument_type, (SumObserver, UpDownSumObserver)): - return LastValueAggregator() - if issubclass(instrument_type, ValueRecorder): - return MinMaxSumCountAggregator() - if issubclass(instrument_type, ValueObserver): - return ValueObserverAggregator() - # TODO: Add other aggregators - return SumAggregator() - def checkpoint_set(self) -> Sequence[MetricRecord]: """Returns a list of MetricRecords used for exporting. @@ -74,7 +41,11 @@ def checkpoint_set(self) -> Sequence[MetricRecord]: data in all of the aggregators in this batcher. """ metric_records = [] - for (instrument, labels), aggregator in self._batch_map.items(): + # pylint: disable=W0612 + for ( + (instrument, aggregator_type, _, labels), + aggregator, + ) in self._batch_map.items(): metric_records.append(MetricRecord(instrument, labels, aggregator)) return metric_records @@ -86,32 +57,34 @@ def finished_collection(self): if not self.stateful: self._batch_map = {} - @abc.abstractmethod def process(self, record) -> None: - """Stores record information to be ready for exporting. - - Depending on type of batcher, performs pre-export logic, such as - filtering records based off of keys. - """ - - -class UngroupedBatcher(Batcher): - """Accepts all records and passes them for exporting""" - - def process(self, record): + """Stores record information to be ready for exporting.""" # Checkpoints the current aggregator value to be collected for export - record.aggregator.take_checkpoint() - batch_key = (record.instrument, record.labels) - batch_value = self._batch_map.get(batch_key) aggregator = record.aggregator + + # The uniqueness of a batch record is defined by a specific metric + # using an aggregator type with a specific set of labels. + # If two aggregators are the same but with different configs, they are still two valid unique records + # (for example, two histogram views with different buckets) + key = ( + record.instrument, + aggregator.__class__, + get_dict_as_key(aggregator.config), + record.labels, + ) + batch_value = self._batch_map.get(key) if batch_value: - # Update the stored checkpointed value if exists. The call to merge - # here combines only identical records (same key). - batch_value.merge(aggregator) - return + if batch_value != aggregator: + aggregator.take_checkpoint() + batch_value.merge(aggregator) + else: + aggregator.take_checkpoint() + if self.stateful: # if stateful batcher, create a copy of the aggregator and update # it with the current checkpointed value for long-term storage - aggregator = self.aggregator_for(record.instrument.__class__) + aggregator = record.aggregator.__class__( + config=record.aggregator.config + ) aggregator.merge(record.aggregator) - self._batch_map[batch_key] = aggregator + self._batch_map[key] = aggregator diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view.py new file mode 100644 index 0000000000..0dd75c6887 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view.py @@ -0,0 +1,186 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import threading +from collections import defaultdict +from typing import Optional, Sequence, Tuple + +from opentelemetry.metrics import ( + Counter, + InstrumentT, + SumObserver, + UpDownCounter, + UpDownSumObserver, + ValueObserver, + ValueRecorder, + ValueT, +) +from opentelemetry.sdk.metrics.export.aggregate import ( + Aggregator, + LastValueAggregator, + MinMaxSumCountAggregator, + SumAggregator, + ValueObserverAggregator, +) + +logger = logging.getLogger(__name__) + + +class ViewData: + def __init__(self, labels: Tuple[Tuple[str, str]], aggregator: Aggregator): + self.labels = labels + self.aggregator = aggregator + + def record(self, value: ValueT): + self.aggregator.update(value) + + # Uniqueness is based on labels and aggregator type + def __hash__(self): + return hash((self.labels, self.aggregator.__class__)) + + def __eq__(self, other): + return ( + self.labels == other.labels + and self.aggregator.__class__ == other.aggregator.__class__ + ) + + +class ViewConfig: + + UNGROUPED = 0 + LABEL_KEYS = 1 + DROP_ALL = 2 + + +class View: + def __init__( + self, + metric: InstrumentT, + aggregator: type, + aggregator_config: Optional[dict] = None, + label_keys: Optional[Sequence[str]] = None, + view_config: ViewConfig = ViewConfig.UNGROUPED, + ): + self.metric = metric + self.aggregator = aggregator + if aggregator_config is None: + aggregator_config = {} + self.aggregator_config = aggregator_config + if label_keys is None: + label_keys = [] + self.label_keys = sorted(label_keys) + self.view_config = view_config + self.view_datas = set() + + def get_view_data(self, labels): + """Find an existing ViewData for this set of labels. If that ViewData + does not exist, create a new one to represent the labels + """ + active_labels = [] + if self.view_config == ViewConfig.LABEL_KEYS: + # reduce the set of labels to only labels specified in label_keys + active_labels = { + (lk, lv) for lk, lv in labels if lk in set(self.label_keys) + } + active_labels = tuple(active_labels) + elif self.view_config == ViewConfig.UNGROUPED: + active_labels = labels + + for view_data in self.view_datas: + if view_data.labels == active_labels: + return view_data + new_view_data = ViewData( + active_labels, self.aggregator(self.aggregator_config) + ) + self.view_datas.add(new_view_data) + return new_view_data + + # Uniqueness is based on metric, aggregator type, aggregator config, + # ordered label keys and ViewConfig + def __hash__(self): + return hash( + ( + self.metric, + self.aggregator.__class__, + tuple(self.label_keys), + tuple(self.aggregator_config), + self.view_config, + ) + ) + + def __eq__(self, other): + return ( + self.metric == other.metric + and self.aggregator.__class__ == other.aggregator.__class__ + and self.label_keys == other.label_keys + and self.aggregator_config == other.aggregator_config + and self.view_config == other.view_config + ) + + +class ViewManager: + def __init__(self): + self.views = defaultdict(set) # Map[Metric, Set] + self._view_lock = threading.Lock() + self.view_datas = set() + + def register_view(self, view): + with self._view_lock: + if view not in self.views[view.metric]: + self.views[view.metric].add(view) + else: + logger.warning("View already registered.") + return + + def unregister_view(self, view): + with self._view_lock: + if self.views.get(view.metric) is None: + logger.warning("Metric for view does not exist.") + elif view in self.views.get(view.metric): + self.views.get(view.metric).remove(view) + + def get_view_datas(self, metric, labels): + view_datas = set() + views = self.views.get(metric) + # No views configured, use default aggregations + if views is None: + # make a default view for the metric + default_view = View(metric, get_default_aggregator(metric)) + self.register_view(default_view) + views = [default_view] + + for view in views: + view_datas.add(view.get_view_data(labels)) + + return view_datas + + +def get_default_aggregator(instrument: InstrumentT) -> Aggregator: + """Returns an aggregator based on metric instrument's type. + + Aggregators keep track of and updates values when metrics get updated. + """ + # pylint:disable=R0201 + instrument_type = instrument.__class__ + if issubclass(instrument_type, (Counter, UpDownCounter)): + return SumAggregator + if issubclass(instrument_type, (SumObserver, UpDownSumObserver)): + return LastValueAggregator + if issubclass(instrument_type, ValueRecorder): + return MinMaxSumCountAggregator + if issubclass(instrument_type, ValueObserver): + return ValueObserverAggregator + logger.warning("No default aggregator configured for: %s", instrument_type) + return SumAggregator diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index b8ebd22fc8..2e0d2e85a8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -31,6 +31,11 @@ def ns_to_iso_str(nanoseconds): return ts.strftime("%Y-%m-%dT%H:%M:%S.%fZ") +def get_dict_as_key(labels): + """Converts a dict to be used as a unique key""" + return tuple(sorted(labels.items())) + + class BoundedList(Sequence): """An append only list with a fixed max size. diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 4d790bb6c0..99aa9c4a62 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -29,7 +29,7 @@ SumAggregator, ValueObserverAggregator, ) -from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.batcher import Batcher from opentelemetry.sdk.metrics.export.controller import PushController @@ -62,38 +62,17 @@ def test_export(self): class TestBatcher(unittest.TestCase): - def test_aggregator_for_counter(self): - batcher = UngroupedBatcher(True) - self.assertTrue( - isinstance(batcher.aggregator_for(metrics.Counter), SumAggregator) - ) - - def test_aggregator_for_updowncounter(self): - batcher = UngroupedBatcher(True) - self.assertTrue( - isinstance( - batcher.aggregator_for(metrics.UpDownCounter), SumAggregator, - ) - ) - - # TODO: Add other aggregator tests - def test_checkpoint_set(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher = UngroupedBatcher(True) + batcher = Batcher(True) aggregator = SumAggregator() metric = metrics.Counter( - "available memory", - "available memory", - "bytes", - int, - meter, - ("environment",), + "available memory", "available memory", "bytes", int, meter ) aggregator.update(1.0) labels = () _batch_map = {} - _batch_map[(metric, labels)] = aggregator + _batch_map[(metric, SumAggregator, tuple(), labels)] = aggregator batcher._batch_map = _batch_map records = batcher.checkpoint_set() self.assertEqual(len(records), 1) @@ -102,127 +81,98 @@ def test_checkpoint_set(self): self.assertEqual(records[0].aggregator, aggregator) def test_checkpoint_set_empty(self): - batcher = UngroupedBatcher(True) + batcher = Batcher(True) records = batcher.checkpoint_set() self.assertEqual(len(records), 0) def test_finished_collection_stateless(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher = UngroupedBatcher(False) + batcher = Batcher(False) aggregator = SumAggregator() metric = metrics.Counter( - "available memory", - "available memory", - "bytes", - int, - meter, - ("environment",), + "available memory", "available memory", "bytes", int, meter ) aggregator.update(1.0) labels = () _batch_map = {} - _batch_map[(metric, labels)] = aggregator + _batch_map[(metric, SumAggregator, tuple(), labels)] = aggregator batcher._batch_map = _batch_map batcher.finished_collection() self.assertEqual(len(batcher._batch_map), 0) def test_finished_collection_stateful(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher = UngroupedBatcher(True) + batcher = Batcher(True) aggregator = SumAggregator() metric = metrics.Counter( - "available memory", - "available memory", - "bytes", - int, - meter, - ("environment",), + "available memory", "available memory", "bytes", int, meter ) aggregator.update(1.0) labels = () _batch_map = {} - _batch_map[(metric, labels)] = aggregator + _batch_map[(metric, SumAggregator, tuple(), labels)] = aggregator batcher._batch_map = _batch_map batcher.finished_collection() self.assertEqual(len(batcher._batch_map), 1) - # TODO: Abstract the logic once other batchers implemented - def test_ungrouped_batcher_process_exists(self): + def test_batcher_process_exists(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher = UngroupedBatcher(True) + batcher = Batcher(True) aggregator = SumAggregator() aggregator2 = SumAggregator() metric = metrics.Counter( - "available memory", - "available memory", - "bytes", - int, - meter, - ("environment",), + "available memory", "available memory", "bytes", int, meter ) labels = () _batch_map = {} - _batch_map[(metric, labels)] = aggregator + batch_key = (metric, SumAggregator, tuple(), labels) + _batch_map[batch_key] = aggregator aggregator2.update(1.0) batcher._batch_map = _batch_map record = metrics.Record(metric, labels, aggregator2) batcher.process(record) self.assertEqual(len(batcher._batch_map), 1) - self.assertIsNotNone(batcher._batch_map.get((metric, labels))) - self.assertEqual(batcher._batch_map.get((metric, labels)).current, 0) - self.assertEqual( - batcher._batch_map.get((metric, labels)).checkpoint, 1.0 - ) + self.assertIsNotNone(batcher._batch_map.get(batch_key)) + self.assertEqual(batcher._batch_map.get(batch_key).current, 0) + self.assertEqual(batcher._batch_map.get(batch_key).checkpoint, 1.0) - def test_ungrouped_batcher_process_not_exists(self): + def test_batcher_process_not_exists(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher = UngroupedBatcher(True) + batcher = Batcher(True) aggregator = SumAggregator() metric = metrics.Counter( - "available memory", - "available memory", - "bytes", - int, - meter, - ("environment",), + "available memory", "available memory", "bytes", int, meter ) labels = () _batch_map = {} + batch_key = (metric, SumAggregator, tuple(), labels) aggregator.update(1.0) batcher._batch_map = _batch_map record = metrics.Record(metric, labels, aggregator) batcher.process(record) self.assertEqual(len(batcher._batch_map), 1) - self.assertIsNotNone(batcher._batch_map.get((metric, labels))) - self.assertEqual(batcher._batch_map.get((metric, labels)).current, 0) - self.assertEqual( - batcher._batch_map.get((metric, labels)).checkpoint, 1.0 - ) + self.assertIsNotNone(batcher._batch_map.get(batch_key)) + self.assertEqual(batcher._batch_map.get(batch_key).current, 0) + self.assertEqual(batcher._batch_map.get(batch_key).checkpoint, 1.0) - def test_ungrouped_batcher_process_not_stateful(self): + def test_batcher_process_not_stateful(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher = UngroupedBatcher(True) + batcher = Batcher(True) aggregator = SumAggregator() metric = metrics.Counter( - "available memory", - "available memory", - "bytes", - int, - meter, - ("environment",), + "available memory", "available memory", "bytes", int, meter ) labels = () _batch_map = {} + batch_key = (metric, SumAggregator, tuple(), labels) aggregator.update(1.0) batcher._batch_map = _batch_map record = metrics.Record(metric, labels, aggregator) batcher.process(record) self.assertEqual(len(batcher._batch_map), 1) - self.assertIsNotNone(batcher._batch_map.get((metric, labels))) - self.assertEqual(batcher._batch_map.get((metric, labels)).current, 0) - self.assertEqual( - batcher._batch_map.get((metric, labels)).checkpoint, 1.0 - ) + self.assertIsNotNone(batcher._batch_map.get(batch_key)) + self.assertEqual(batcher._batch_map.get(batch_key).current, 0) + self.assertEqual(batcher._batch_map.get(batch_key).checkpoint, 1.0) class TestSumAggregator(unittest.TestCase): diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index ae07c23341..b854f2d5db 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -17,7 +17,11 @@ from opentelemetry import metrics as metrics_api from opentelemetry.sdk import metrics, resources -from opentelemetry.sdk.metrics import export +from opentelemetry.sdk.metrics.export.aggregate import ( + MinMaxSumCountAggregator, + SumAggregator, +) +from opentelemetry.sdk.metrics.view import View class TestMeterProvider(unittest.TestCase): @@ -68,17 +72,16 @@ def test_extends_api(self): meter = metrics.MeterProvider().get_meter(__name__) self.assertIsInstance(meter, metrics_api.Meter) - def test_collect(self): + def test_collect_metrics(self): meter = metrics.MeterProvider().get_meter(__name__) batcher_mock = mock.Mock() meter.batcher = batcher_mock - label_keys = ("key1",) - counter = metrics.Counter( - "name", "desc", "unit", float, meter, label_keys + counter = meter.create_metric( + "name", "desc", "unit", float, metrics.Counter ) labels = {"key1": "value1"} + meter.register_view(View(counter, SumAggregator)) counter.add(1.0, labels) - meter.metrics.add(counter) meter.collect() self.assertTrue(batcher_mock.process.called) @@ -89,17 +92,24 @@ def test_collect_no_metrics(self): meter.collect() self.assertFalse(batcher_mock.process.called) + def test_collect_not_registered(self): + meter = metrics.MeterProvider().get_meter(__name__) + batcher_mock = mock.Mock() + meter.batcher = batcher_mock + counter = metrics.Counter("name", "desc", "unit", float, meter) + labels = {"key1": "value1"} + counter.add(1.0, labels) + meter.collect() + self.assertFalse(batcher_mock.process.called) + def test_collect_disabled_metric(self): meter = metrics.MeterProvider().get_meter(__name__) batcher_mock = mock.Mock() meter.batcher = batcher_mock - label_keys = ("key1",) - counter = metrics.Counter( - "name", "desc", "unit", float, meter, label_keys, False - ) + counter = metrics.Counter("name", "desc", "unit", float, meter, False) labels = {"key1": "value1"} + meter.register_view(View(counter, SumAggregator)) counter.add(1.0, labels) - meter.metrics.add(counter) meter.collect() self.assertFalse(batcher_mock.process.called) @@ -113,7 +123,7 @@ def callback(observer): observer.observe(45, {}) observer = metrics.ValueObserver( - callback, "name", "desc", "unit", int, meter, (), True + callback, "name", "desc", "unit", int, (), True ) meter.observers.add(observer) @@ -122,57 +132,43 @@ def callback(observer): def test_record_batch(self): meter = metrics.MeterProvider().get_meter(__name__) - label_keys = ("key1",) - labels = {"key1": "value1"} - counter = metrics.Counter( - "name", "desc", "unit", float, meter, label_keys - ) - record_tuples = [(counter, 1.0)] - meter.record_batch(labels, record_tuples) - self.assertEqual(counter.bind(labels).aggregator.current, 1.0) - - def test_record_batch_multiple(self): - meter = metrics.MeterProvider().get_meter(__name__) - label_keys = ("key1", "key2", "key3") labels = {"key1": "value1", "key2": "value2", "key3": "value3"} - counter = metrics.Counter( - "name", "desc", "unit", float, meter, label_keys - ) + counter = metrics.Counter("name", "desc", "unit", float, meter) valuerecorder = metrics.ValueRecorder( - "name", "desc", "unit", float, meter, label_keys + "name", "desc", "unit", float, meter ) + counter_v = View(counter, SumAggregator) + measure_v = View(valuerecorder, MinMaxSumCountAggregator) + meter.register_view(counter_v) + meter.register_view(measure_v) record_tuples = [(counter, 1.0), (valuerecorder, 3.0)] meter.record_batch(labels, record_tuples) - self.assertEqual(counter.bind(labels).aggregator.current, 1.0) + labels_key = metrics.get_dict_as_key(labels) self.assertEqual( - valuerecorder.bind(labels).aggregator.current, (3.0, 3.0, 3.0, 1) + counter.bound_instruments[labels_key] + .view_datas.pop() + .aggregator.current, + 1.0, ) - - def test_record_batch_exists(self): - meter = metrics.MeterProvider().get_meter(__name__) - label_keys = ("key1",) - labels = {"key1": "value1"} - counter = metrics.Counter( - "name", "desc", "unit", float, meter, label_keys + self.assertEqual( + valuerecorder.bound_instruments[labels_key] + .view_datas.pop() + .aggregator.current, + (3.0, 3.0, 3.0, 1), ) - counter.add(1.0, labels) - bound_counter = counter.bind(labels) - record_tuples = [(counter, 1.0)] - meter.record_batch(labels, record_tuples) - self.assertEqual(counter.bind(labels), bound_counter) - self.assertEqual(bound_counter.aggregator.current, 2.0) def test_create_metric(self): resource = mock.Mock(spec=resources.Resource) meter_provider = metrics.MeterProvider(resource=resource) meter = meter_provider.get_meter(__name__) counter = meter.create_metric( - "name", "desc", "unit", int, metrics.Counter, () + "name", "desc", "unit", int, metrics.Counter ) self.assertIsInstance(counter, metrics.Counter) self.assertEqual(counter.value_type, int) self.assertEqual(counter.name, "name") self.assertIs(counter.meter.resource, resource) + self.assertEqual(counter.meter, meter) def test_create_updowncounter(self): meter = metrics.MeterProvider().get_meter(__name__) @@ -186,11 +182,12 @@ def test_create_updowncounter(self): def test_create_valuerecorder(self): meter = metrics.MeterProvider().get_meter(__name__) valuerecorder = meter.create_metric( - "name", "desc", "unit", float, metrics.ValueRecorder, () + "name", "desc", "unit", float, metrics.ValueRecorder ) self.assertIsInstance(valuerecorder, metrics.ValueRecorder) self.assertEqual(valuerecorder.value_type, float) self.assertEqual(valuerecorder.name, "name") + self.assertEqual(valuerecorder.meter, meter) def test_register_observer(self): meter = metrics.MeterProvider().get_meter(__name__) @@ -224,71 +221,15 @@ def test_unregister_observer(self): meter.unregister_observer(observer) self.assertEqual(len(meter.observers), 0) - def test_direct_call_release_bound_instrument(self): - meter = metrics.MeterProvider().get_meter(__name__) - label_keys = ("key1",) - labels = {"key1": "value1"} - - counter = metrics.Counter( - "name", "desc", "unit", float, meter, label_keys - ) - meter.metrics.add(counter) - counter.add(4.0, labels) - - valuerecorder = metrics.ValueRecorder( - "name", "desc", "unit", float, meter, label_keys - ) - meter.metrics.add(valuerecorder) - valuerecorder.record(42.0, labels) - - self.assertEqual(len(counter.bound_instruments), 1) - self.assertEqual(len(valuerecorder.bound_instruments), 1) - - meter.collect() - - self.assertEqual(len(counter.bound_instruments), 0) - self.assertEqual(len(valuerecorder.bound_instruments), 0) - - def test_release_bound_instrument(self): - meter = metrics.MeterProvider().get_meter(__name__) - label_keys = ("key1",) - labels = {"key1": "value1"} - - counter = metrics.Counter( - "name", "desc", "unit", float, meter, label_keys - ) - meter.metrics.add(counter) - bound_counter = counter.bind(labels) - bound_counter.add(4.0) - - valuerecorder = metrics.ValueRecorder( - "name", "desc", "unit", float, meter, label_keys - ) - meter.metrics.add(valuerecorder) - bound_valuerecorder = valuerecorder.bind(labels) - bound_valuerecorder.record(42) - - bound_counter.release() - bound_valuerecorder.release() - - # be sure that bound instruments are only released after collection - self.assertEqual(len(counter.bound_instruments), 1) - self.assertEqual(len(valuerecorder.bound_instruments), 1) - - meter.collect() - - self.assertEqual(len(counter.bound_instruments), 0) - self.assertEqual(len(valuerecorder.bound_instruments), 0) - class TestMetric(unittest.TestCase): def test_bind(self): meter = metrics.MeterProvider().get_meter(__name__) metric_types = [metrics.Counter, metrics.ValueRecorder] labels = {"key": "value"} - key_labels = tuple(sorted(labels.items())) + key_labels = metrics.get_dict_as_key(labels) for _type in metric_types: - metric = _type("name", "desc", "unit", int, meter, ("key",)) + metric = _type("name", "desc", "unit", int, meter) bound_instrument = metric.bind(labels) self.assertEqual( metric.bound_instruments.get(key_labels), bound_instrument @@ -298,86 +239,84 @@ def test_bind(self): class TestCounter(unittest.TestCase): def test_add(self): meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.Counter("name", "desc", "unit", int, meter, ("key",)) + metric = metrics.Counter("name", "desc", "unit", int, meter) labels = {"key": "value"} - bound_counter = metric.bind(labels) + counter_v = View(metric, SumAggregator) + meter.register_view(counter_v) + bound_mock = metric.bind(labels) metric.add(3, labels) metric.add(2, labels) - self.assertEqual(bound_counter.aggregator.current, 5) + self.assertEqual(bound_mock.view_datas.pop().aggregator.current, 5) @mock.patch("opentelemetry.sdk.metrics.logger") def test_add_non_decreasing_int_error(self, logger_mock): meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.Counter("name", "desc", "unit", int, meter, ("key",)) + metric = metrics.Counter("name", "desc", "unit", int, meter) labels = {"key": "value"} + counter_v = View(metric, SumAggregator) + meter.register_view(counter_v) bound_counter = metric.bind(labels) metric.add(3, labels) metric.add(0, labels) metric.add(-1, labels) - self.assertEqual(bound_counter.aggregator.current, 3) + self.assertEqual(bound_counter.view_datas.pop().aggregator.current, 3) self.assertEqual(logger_mock.warning.call_count, 1) @mock.patch("opentelemetry.sdk.metrics.logger") def test_add_non_decreasing_float_error(self, logger_mock): meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.Counter( - "name", "desc", "unit", float, meter, ("key",) - ) + metric = metrics.Counter("name", "desc", "unit", float, meter) labels = {"key": "value"} + counter_v = View(metric, SumAggregator) + meter.register_view(counter_v) bound_counter = metric.bind(labels) metric.add(3.3, labels) metric.add(0.0, labels) metric.add(0.1, labels) metric.add(-0.1, labels) - self.assertEqual(bound_counter.aggregator.current, 3.4) + self.assertEqual( + bound_counter.view_datas.pop().aggregator.current, 3.4 + ) self.assertEqual(logger_mock.warning.call_count, 1) class TestUpDownCounter(unittest.TestCase): - @mock.patch("opentelemetry.sdk.metrics.logger") - def test_add(self, logger_mock): + def test_add(self): meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.UpDownCounter( - "name", "desc", "unit", int, meter, ("key",) - ) + metric = metrics.UpDownCounter("name", "desc", "unit", int, meter) labels = {"key": "value"} bound_counter = metric.bind(labels) + counter_v = View(metric, SumAggregator) + meter.register_view(counter_v) metric.add(3, labels) metric.add(2, labels) - self.assertEqual(bound_counter.aggregator.current, 5) - - metric.add(0, labels) - metric.add(-3, labels) - metric.add(-1, labels) - self.assertEqual(bound_counter.aggregator.current, 1) - self.assertEqual(logger_mock.warning.call_count, 0) + self.assertEqual(bound_counter.view_datas.pop().aggregator.current, 5) class TestValueRecorder(unittest.TestCase): def test_record(self): meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.ValueRecorder( - "name", "desc", "unit", int, meter, ("key",) - ) + metric = metrics.ValueRecorder("name", "desc", "unit", int, meter) labels = {"key": "value"} + measure_v = View(metric, MinMaxSumCountAggregator) bound_valuerecorder = metric.bind(labels) + meter.register_view(measure_v) values = (37, 42, 7) for val in values: metric.record(val, labels) self.assertEqual( - bound_valuerecorder.aggregator.current, + bound_valuerecorder.view_datas.pop().aggregator.current, (min(values), max(values), sum(values), len(values)), ) class TestSumObserver(unittest.TestCase): def test_observe(self): - meter = metrics.MeterProvider().get_meter(__name__) observer = metrics.SumObserver( - None, "name", "desc", "unit", int, meter, ("key",), True + None, "name", "desc", "unit", int, ("key",), True ) labels = {"key": "value"} - key_labels = tuple(sorted(labels.items())) + key_labels = metrics.get_dict_as_key(labels) values = (37, 42, 60, 100) for val in values: observer.observe(val, labels) @@ -385,9 +324,8 @@ def test_observe(self): self.assertEqual(observer.aggregators[key_labels].current, values[-1]) def test_observe_disabled(self): - meter = metrics.MeterProvider().get_meter(__name__) observer = metrics.SumObserver( - None, "name", "desc", "unit", int, meter, ("key",), False + None, "name", "desc", "unit", int, ("key",), False ) labels = {"key": "value"} observer.observe(37, labels) @@ -395,9 +333,8 @@ def test_observe_disabled(self): @mock.patch("opentelemetry.sdk.metrics.logger") def test_observe_incorrect_type(self, logger_mock): - meter = metrics.MeterProvider().get_meter(__name__) observer = metrics.SumObserver( - None, "name", "desc", "unit", int, meter, ("key",), True + None, "name", "desc", "unit", int, ("key",), True ) labels = {"key": "value"} observer.observe(37.0, labels) @@ -406,9 +343,8 @@ def test_observe_incorrect_type(self, logger_mock): @mock.patch("opentelemetry.sdk.metrics.logger") def test_observe_non_decreasing_error(self, logger_mock): - meter = metrics.MeterProvider().get_meter(__name__) observer = metrics.SumObserver( - None, "name", "desc", "unit", int, meter, ("key",), True + None, "name", "desc", "unit", int, ("key",), True ) labels = {"key": "value"} observer.observe(37, labels) @@ -417,11 +353,9 @@ def test_observe_non_decreasing_error(self, logger_mock): self.assertTrue(logger_mock.warning.called) def test_run(self): - meter = metrics.MeterProvider().get_meter(__name__) - callback = mock.Mock() observer = metrics.SumObserver( - callback, "name", "desc", "unit", int, meter, (), True + callback, "name", "desc", "unit", int, (), True ) self.assertTrue(observer.run()) @@ -429,13 +363,11 @@ def test_run(self): @mock.patch("opentelemetry.sdk.metrics.logger") def test_run_exception(self, logger_mock): - meter = metrics.MeterProvider().get_meter(__name__) - callback = mock.Mock() callback.side_effect = Exception("We have a problem!") observer = metrics.SumObserver( - callback, "name", "desc", "unit", int, meter, (), True + callback, "name", "desc", "unit", int, (), True ) self.assertFalse(observer.run()) @@ -444,12 +376,11 @@ def test_run_exception(self, logger_mock): class TestUpDownSumObserver(unittest.TestCase): def test_observe(self): - meter = metrics.MeterProvider().get_meter(__name__) observer = metrics.UpDownSumObserver( - None, "name", "desc", "unit", int, meter, ("key",), True + None, "name", "desc", "unit", int, ("key",), True ) labels = {"key": "value"} - key_labels = tuple(sorted(labels.items())) + key_labels = metrics.get_dict_as_key(labels) values = (37, 42, 14, 30) for val in values: observer.observe(val, labels) @@ -457,9 +388,8 @@ def test_observe(self): self.assertEqual(observer.aggregators[key_labels].current, values[-1]) def test_observe_disabled(self): - meter = metrics.MeterProvider().get_meter(__name__) observer = metrics.UpDownSumObserver( - None, "name", "desc", "unit", int, meter, ("key",), False + None, "name", "desc", "unit", int, ("key",), False ) labels = {"key": "value"} observer.observe(37, labels) @@ -467,9 +397,8 @@ def test_observe_disabled(self): @mock.patch("opentelemetry.sdk.metrics.logger") def test_observe_incorrect_type(self, logger_mock): - meter = metrics.MeterProvider().get_meter(__name__) observer = metrics.UpDownSumObserver( - None, "name", "desc", "unit", int, meter, ("key",), True + None, "name", "desc", "unit", int, ("key",), True ) labels = {"key": "value"} observer.observe(37.0, labels) @@ -477,11 +406,9 @@ def test_observe_incorrect_type(self, logger_mock): self.assertTrue(logger_mock.warning.called) def test_run(self): - meter = metrics.MeterProvider().get_meter(__name__) - callback = mock.Mock() observer = metrics.UpDownSumObserver( - callback, "name", "desc", "unit", int, meter, (), True + callback, "name", "desc", "unit", int, (), True ) self.assertTrue(observer.run()) @@ -489,13 +416,11 @@ def test_run(self): @mock.patch("opentelemetry.sdk.metrics.logger") def test_run_exception(self, logger_mock): - meter = metrics.MeterProvider().get_meter(__name__) - callback = mock.Mock() callback.side_effect = Exception("We have a problem!") observer = metrics.UpDownSumObserver( - callback, "name", "desc", "unit", int, meter, (), True + callback, "name", "desc", "unit", int, (), True ) self.assertFalse(observer.run()) @@ -504,12 +429,11 @@ def test_run_exception(self, logger_mock): class TestValueObserver(unittest.TestCase): def test_observe(self): - meter = metrics.MeterProvider().get_meter(__name__) observer = metrics.ValueObserver( - None, "name", "desc", "unit", int, meter, ("key",), True + None, "name", "desc", "unit", int, ("key",), True ) labels = {"key": "value"} - key_labels = tuple(sorted(labels.items())) + key_labels = metrics.get_dict_as_key(labels) values = (37, 42, 7, 21) for val in values: observer.observe(val, labels) @@ -521,9 +445,8 @@ def test_observe(self): self.assertEqual(observer.aggregators[key_labels].current, values[-1]) def test_observe_disabled(self): - meter = metrics.MeterProvider().get_meter(__name__) observer = metrics.ValueObserver( - None, "name", "desc", "unit", int, meter, ("key",), False + None, "name", "desc", "unit", int, ("key",), False ) labels = {"key": "value"} observer.observe(37, labels) @@ -531,9 +454,8 @@ def test_observe_disabled(self): @mock.patch("opentelemetry.sdk.metrics.logger") def test_observe_incorrect_type(self, logger_mock): - meter = metrics.MeterProvider().get_meter(__name__) observer = metrics.ValueObserver( - None, "name", "desc", "unit", int, meter, ("key",), True + None, "name", "desc", "unit", int, ("key",), True ) labels = {"key": "value"} observer.observe(37.0, labels) @@ -541,11 +463,9 @@ def test_observe_incorrect_type(self, logger_mock): self.assertTrue(logger_mock.warning.called) def test_run(self): - meter = metrics.MeterProvider().get_meter(__name__) - callback = mock.Mock() observer = metrics.ValueObserver( - callback, "name", "desc", "unit", int, meter, (), True + callback, "name", "desc", "unit", int, (), True ) self.assertTrue(observer.run()) @@ -553,78 +473,95 @@ def test_run(self): @mock.patch("opentelemetry.sdk.metrics.logger") def test_run_exception(self, logger_mock): - meter = metrics.MeterProvider().get_meter(__name__) - callback = mock.Mock() callback.side_effect = Exception("We have a problem!") observer = metrics.ValueObserver( - callback, "name", "desc", "unit", int, meter, (), True + callback, "name", "desc", "unit", int, (), True ) self.assertFalse(observer.run()) self.assertTrue(logger_mock.warning.called) +# pylint: disable=no-self-use class TestBoundCounter(unittest.TestCase): def test_add(self): - aggregator = export.aggregate.SumAggregator() - bound_metric = metrics.BoundCounter(int, True, aggregator) + meter_mock = mock.Mock() + metric_mock = mock.Mock() + metric_mock.enabled = True + metric_mock.value_type = int + metric_mock.meter = meter_mock + bound_metric = metrics.BoundCounter((), metric_mock) + view_datas_mock = mock.Mock() + bound_metric.view_datas = [view_datas_mock] bound_metric.add(3) - self.assertEqual(bound_metric.aggregator.current, 3) + view_datas_mock.record.assert_called_once_with(3) def test_add_disabled(self): - aggregator = export.aggregate.SumAggregator() - bound_counter = metrics.BoundCounter(int, False, aggregator) - bound_counter.add(3) - self.assertEqual(bound_counter.aggregator.current, 0) + meter_mock = mock.Mock() + metric_mock = mock.Mock() + metric_mock.enabled = False + metric_mock.value_type = int + metric_mock.meter = meter_mock + bound_metric = metrics.BoundCounter((), metric_mock) + view_datas_mock = mock.Mock() + bound_metric.view_datas = [view_datas_mock] + bound_metric.add(3) + view_datas_mock.record.update_view.assert_not_called() @mock.patch("opentelemetry.sdk.metrics.logger") def test_add_incorrect_type(self, logger_mock): - aggregator = export.aggregate.SumAggregator() - bound_counter = metrics.BoundCounter(int, True, aggregator) - bound_counter.add(3.0) - self.assertEqual(bound_counter.aggregator.current, 0) + meter_mock = mock.Mock() + viewm_mock = mock.Mock() + meter_mock.view_manager = viewm_mock + metric_mock = mock.Mock() + metric_mock.enabled = True + metric_mock.value_type = float + metric_mock.meter = meter_mock + bound_metric = metrics.BoundCounter((), metric_mock) + view_datas_mock = mock.Mock() + bound_metric.view_datas = [view_datas_mock] + bound_metric.add(3) + view_datas_mock.record.update_view.assert_not_called() self.assertTrue(logger_mock.warning.called) - def test_update(self): - aggregator = export.aggregate.SumAggregator() - bound_counter = metrics.BoundCounter(int, True, aggregator) - bound_counter.update(4.0) - self.assertEqual(bound_counter.aggregator.current, 4.0) - class TestBoundValueRecorder(unittest.TestCase): def test_record(self): - aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_valuerecorder = metrics.BoundValueRecorder(int, True, aggregator) + meter_mock = mock.Mock() + metric_mock = mock.Mock() + metric_mock.enabled = True + metric_mock.value_type = int + metric_mock.meter = meter_mock + bound_valuerecorder = metrics.BoundValueRecorder((), metric_mock) + view_datas_mock = mock.Mock() + bound_valuerecorder.view_datas = [view_datas_mock] bound_valuerecorder.record(3) - self.assertEqual(bound_valuerecorder.aggregator.current, (3, 3, 3, 1)) + view_datas_mock.record.assert_called_once_with(3) def test_record_disabled(self): - aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_valuerecorder = metrics.BoundValueRecorder( - int, False, aggregator - ) + meter_mock = mock.Mock() + metric_mock = mock.Mock() + metric_mock.enabled = False + metric_mock.value_type = int + metric_mock.meter = meter_mock + bound_valuerecorder = metrics.BoundValueRecorder((), metric_mock) + view_datas_mock = mock.Mock() + bound_valuerecorder.view_datas = [view_datas_mock] bound_valuerecorder.record(3) - self.assertEqual( - bound_valuerecorder.aggregator.current, (None, None, None, 0) - ) + view_datas_mock.record.update_view.assert_not_called() @mock.patch("opentelemetry.sdk.metrics.logger") def test_record_incorrect_type(self, logger_mock): - aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_valuerecorder = metrics.BoundValueRecorder(int, True, aggregator) - bound_valuerecorder.record(3.0) - self.assertEqual( - bound_valuerecorder.aggregator.current, (None, None, None, 0) - ) + meter_mock = mock.Mock() + metric_mock = mock.Mock() + metric_mock.enabled = True + metric_mock.value_type = float + metric_mock.meter = meter_mock + bound_valuerecorder = metrics.BoundValueRecorder((), metric_mock) + view_datas_mock = mock.Mock() + bound_valuerecorder.view_datas = [view_datas_mock] + bound_valuerecorder.record(3) + view_datas_mock.record.update_view.assert_not_called() self.assertTrue(logger_mock.warning.called) - - def test_update(self): - aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_valuerecorder = metrics.BoundValueRecorder(int, True, aggregator) - bound_valuerecorder.update(4.0) - self.assertEqual( - bound_valuerecorder.aggregator.current, (4.0, 4.0, 4.0, 1) - ) diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py new file mode 100644 index 0000000000..7dd7571d38 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -0,0 +1,191 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from opentelemetry.sdk import metrics +from opentelemetry.sdk.metrics import Counter, view +from opentelemetry.sdk.metrics.export import aggregate +from opentelemetry.sdk.metrics.export.aggregate import ( + MinMaxSumCountAggregator, + SumAggregator, +) +from opentelemetry.sdk.metrics.export.controller import PushController +from opentelemetry.sdk.metrics.export.in_memory_metrics_exporter import ( + InMemoryMetricsExporter, +) +from opentelemetry.sdk.metrics.view import View, ViewConfig + + +class TestUtil(unittest.TestCase): + @mock.patch("opentelemetry.sdk.metrics.view.logger") + def test_default_aggregator(self, logger_mock): + meter = metrics.MeterProvider().get_meter(__name__) + counter = metrics.Counter("", "", "1", int, meter) + self.assertEqual( + view.get_default_aggregator(counter), aggregate.SumAggregator + ) + ud_counter = metrics.UpDownCounter("", "", "1", int, meter) + self.assertEqual( + view.get_default_aggregator(ud_counter), aggregate.SumAggregator + ) + observer = metrics.SumObserver(lambda: None, "", "", "1", int) + self.assertEqual( + view.get_default_aggregator(observer), + aggregate.LastValueAggregator, + ) + ud_observer = metrics.SumObserver(lambda: None, "", "", "1", int) + self.assertEqual( + view.get_default_aggregator(ud_observer), + aggregate.LastValueAggregator, + ) + recorder = metrics.ValueRecorder("", "", "1", int, meter) + self.assertEqual( + view.get_default_aggregator(recorder), + aggregate.MinMaxSumCountAggregator, + ) + v_observer = metrics.ValueObserver(lambda: None, "", "", "1", int) + self.assertEqual( + view.get_default_aggregator(v_observer), + aggregate.ValueObserverAggregator, + ) + self.assertEqual( + view.get_default_aggregator(DummyMetric()), aggregate.SumAggregator + ) + self.assertEqual(logger_mock.warning.call_count, 1) + + +class TestStateless(unittest.TestCase): + def setUp(self): + self.meter = metrics.MeterProvider(stateful=False).get_meter(__name__) + self.exporter = InMemoryMetricsExporter() + self.controller = PushController(self.meter, self.exporter, 30) + + def tearDown(self): + self.controller.shutdown() + + def test_label_keys(self): + test_counter = self.meter.create_metric( + name="test_counter", + description="description", + unit="By", + value_type=int, + metric_type=Counter, + ) + counter_view = View( + test_counter, + SumAggregator, + label_keys=["environment"], + view_config=ViewConfig.LABEL_KEYS, + ) + + self.meter.register_view(counter_view) + test_counter.add(6, {"environment": "production", "customer_id": 123}) + test_counter.add(5, {"environment": "production", "customer_id": 247}) + + self.controller.tick() + + metric_data = self.exporter.get_exported_metrics() + self.assertEqual(len(metric_data), 1) + self.assertEqual( + metric_data[0].labels, (("environment", "production"),) + ) + self.assertEqual(metric_data[0].aggregator.checkpoint, 11) + + def test_ungrouped(self): + test_counter = self.meter.create_metric( + name="test_counter", + description="description", + unit="By", + value_type=int, + metric_type=Counter, + ) + counter_view = View( + test_counter, + SumAggregator, + label_keys=["environment"], + view_config=ViewConfig.UNGROUPED, + ) + + self.meter.register_view(counter_view) + test_counter.add(6, {"environment": "production", "customer_id": 123}) + test_counter.add(5, {"environment": "production", "customer_id": 247}) + + self.controller.tick() + + metric_data = self.exporter.get_exported_metrics() + data_set = set() + for data in metric_data: + data_set.add((data.labels, data.aggregator.checkpoint)) + self.assertEqual(len(metric_data), 2) + label1 = (("customer_id", 123), ("environment", "production")) + label2 = (("customer_id", 247), ("environment", "production")) + self.assertTrue((label1, 6) in data_set) + self.assertTrue((label2, 5) in data_set) + + def test_multiple_views(self): + test_counter = self.meter.create_metric( + name="test_counter", + description="description", + unit="By", + value_type=int, + metric_type=Counter, + ) + + counter_view = View( + test_counter, + SumAggregator, + label_keys=["environment"], + view_config=ViewConfig.UNGROUPED, + ) + + mmsc_view = View( + test_counter, + MinMaxSumCountAggregator, + label_keys=["environment"], + view_config=ViewConfig.LABEL_KEYS, + ) + + self.meter.register_view(counter_view) + self.meter.register_view(mmsc_view) + test_counter.add(6, {"environment": "production", "customer_id": 123}) + test_counter.add(5, {"environment": "production", "customer_id": 247}) + + self.controller.tick() + + metric_data = self.exporter.get_exported_metrics() + sum_set = set() + mmsc_set = set() + for data in metric_data: + if isinstance(data.aggregator, SumAggregator): + tup = (data.labels, data.aggregator.checkpoint) + sum_set.add(tup) + elif isinstance(data.aggregator, MinMaxSumCountAggregator): + mmsc_set.add(data) + self.assertEqual(data.labels, (("environment", "production"),)) + self.assertEqual(data.aggregator.checkpoint.sum, 11) + # we have to assert this way because order is unknown + self.assertEqual(len(sum_set), 2) + self.assertEqual(len(mmsc_set), 1) + label1 = (("customer_id", 123), ("environment", "production")) + label2 = (("customer_id", 247), ("environment", "production")) + self.assertTrue((label1, 6) in sum_set) + self.assertTrue((label2, 5) in sum_set) + + +class DummyMetric(metrics.Metric): + # pylint: disable=W0231 + def __init__(self): + pass From 28d0cdf9316830e2b1a35b96b59c62bfe2a233ab Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 3 Aug 2020 10:10:45 -0700 Subject: [PATCH 0486/1517] Rename web framework packages from "ext" to "instrumentation" (#961) --- docs-requirements.txt | 3 +- docs/conf.py | 10 ++- docs/examples/auto-instrumentation/README.rst | 2 +- .../server_instrumented.py | 2 +- docs/examples/datadog_exporter/README.rst | 2 +- docs/examples/django/README.rst | 8 +-- docs/examples/django/manage.py | 2 +- .../opentelemetry-example-app/setup.cfg | 4 +- .../flask_example.py | 6 +- docs/ext/aiohttp_client/aiohttp_client.rst | 7 -- docs/ext/asgi/asgi.rst | 10 --- docs/ext/flask/flask.rst | 7 -- docs/ext/grpc/grpc.rst | 4 +- docs/ext/pyramid/pyramid.rst | 7 -- docs/ext/wsgi/wsgi.rst | 7 -- docs/getting-started.rst | 4 +- docs/getting_started/flask_example.py | 4 +- docs/index.rst | 8 +++ .../aiohttp_client/aiohttp_client.rst | 7 ++ docs/{ext => instrumentation}/aiopg/aiopg.rst | 2 +- docs/instrumentation/asgi/asgi.rst | 10 +++ .../django/django.rst | 2 +- .../fastapi/fastapi.rst | 2 +- docs/instrumentation/flask/flask.rst | 7 ++ docs/instrumentation/instrumentation.rst | 4 +- docs/instrumentation/pyramid/pyramid.rst | 7 ++ docs/instrumentation/requests/requests.rst | 7 ++ .../starlette/starlette.rst | 2 +- .../wsgi/wsgi.rst} | 4 +- eachdist.ini | 7 +- .../exporter/datadog/exporter.py | 12 ++-- .../tests/test_datadog_exporter.py | 10 +-- .../CHANGELOG.md | 9 --- ext/opentelemetry-ext-grpc/setup.cfg | 2 +- ext/opentelemetry-ext-requests/README.rst | 23 ------- .../CHANGELOG.md | 12 ++++ .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 6 +- .../setup.cfg | 6 +- .../setup.py | 7 +- .../aiohttp_client/__init__.py | 10 +-- .../aiohttp_client/version.py | 0 .../tests/__init__.py | 0 .../tests/test_aiohttp_client_integration.py | 16 ++--- .../CHANGELOG.md | 0 .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 2 +- .../setup.cfg | 4 +- .../setup.py | 0 .../instrumentation/aiopg/__init__.py | 0 .../aiopg/aiopg_integration.py | 0 .../instrumentation/aiopg}/version.py | 0 .../instrumentation/aiopg/wrappers.py | 0 .../tests/__init__.py | 0 .../tests/test_aiopg_integration.py | 0 .../CHANGELOG.md | 3 + .../README.rst | 10 +-- .../setup.cfg | 6 +- .../setup.py | 2 +- .../instrumentation}/asgi/__init__.py | 4 +- .../instrumentation/asgi}/version.py | 0 .../tests/__init__.py | 0 .../tests/test_asgi_middleware.py | 2 +- .../CHANGELOG.md | 2 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 8 +-- .../setup.cfg | 8 +-- .../setup.py | 2 +- .../instrumentation}/django/__init__.py | 2 +- .../instrumentation}/django/middleware.py | 4 +- .../instrumentation/django}/version.py | 0 .../tests/__init__.py | 0 .../tests/conftest.py | 0 .../tests/test_middleware.py | 4 +- .../tests/views.py | 0 .../CHANGELOG.md | 0 .../README.rst | 0 .../setup.cfg | 4 +- .../setup.py | 0 .../instrumentation/fastapi/__init__.py | 2 +- .../instrumentation/fastapi}/version.py | 0 .../tests/__init__.py | 0 .../tests/test_fastapi_instrumentation.py | 0 .../CHANGELOG.md | 2 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 8 +-- .../setup.cfg | 8 +-- .../setup.py | 4 +- .../instrumentation}/flask/__init__.py | 8 +-- .../instrumentation/flask}/version.py | 0 .../tests/__init__.py | 0 .../tests/base_test.py | 0 .../tests/test_automatic.py | 2 +- .../tests/test_programmatic.py | 4 +- .../CHANGELOG.md | 0 .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 12 ++-- .../setup.cfg | 10 +-- .../setup.py | 7 +- .../instrumentation}/pyramid/__init__.py | 20 +++--- .../instrumentation}/pyramid/callbacks.py | 8 +-- .../instrumentation/pyramid}/version.py | 0 .../tests/__init__.py | 0 .../tests/pyramid_base_test.py | 0 .../tests/test_automatic.py | 2 +- .../tests/test_programmatic.py | 12 ++-- .../CHANGELOG.md | 3 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 23 +++++++ .../setup.cfg | 8 +-- .../setup.py | 7 +- .../instrumentation}/requests/__init__.py | 6 +- .../instrumentation/requests}/version.py | 0 .../tests/__init__.py | 0 .../tests/test_requests_integration.py | 8 ++- .../CHANGELOG.md | 0 .../README.rst | 0 .../setup.cfg | 4 +- .../setup.py | 0 .../instrumentation/starlette/__init__.py | 2 +- .../instrumentation/starlette}/version.py | 0 .../tests/__init__.py | 0 .../tests/test_starlette_instrumentation.py | 0 .../CHANGELOG.md | 3 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 8 +-- .../setup.cfg | 4 +- .../setup.py | 2 +- .../instrumentation}/wsgi/__init__.py | 6 +- .../instrumentation/wsgi}/version.py | 0 .../tests/__init__.py | 0 .../tests/test_wsgi_middleware.py | 2 +- .../src/opentelemetry/metrics/__init__.py | 2 +- .../src/opentelemetry/trace/__init__.py | 2 +- .../instrumentation/bootstrap.py | 34 +++++----- scripts/build.sh | 2 +- scripts/coverage.sh | 10 +-- tests/w3c_tracecontext_validation_server.py | 4 +- tox.ini | 66 +++++++++---------- 146 files changed, 352 insertions(+), 301 deletions(-) delete mode 100644 docs/ext/aiohttp_client/aiohttp_client.rst delete mode 100644 docs/ext/asgi/asgi.rst delete mode 100644 docs/ext/flask/flask.rst delete mode 100644 docs/ext/pyramid/pyramid.rst delete mode 100644 docs/ext/wsgi/wsgi.rst create mode 100644 docs/instrumentation/aiohttp_client/aiohttp_client.rst rename docs/{ext => instrumentation}/aiopg/aiopg.rst (80%) create mode 100644 docs/instrumentation/asgi/asgi.rst rename docs/{ext => instrumentation}/django/django.rst (71%) rename docs/{ext => instrumentation}/fastapi/fastapi.rst (57%) create mode 100644 docs/instrumentation/flask/flask.rst create mode 100644 docs/instrumentation/pyramid/pyramid.rst create mode 100644 docs/instrumentation/requests/requests.rst rename docs/{ext => instrumentation}/starlette/starlette.rst (57%) rename docs/{ext/requests/requests.rst => instrumentation/wsgi/wsgi.rst} (51%) delete mode 100644 ext/opentelemetry-ext-aiohttp-client/CHANGELOG.md delete mode 100644 ext/opentelemetry-ext-requests/README.rst create mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md rename {ext/opentelemetry-ext-aiohttp-client => instrumentation/opentelemetry-instrumentation-aiohttp-client}/LICENSE (100%) rename {ext/opentelemetry-ext-aiohttp-client => instrumentation/opentelemetry-instrumentation-aiohttp-client}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-aiohttp-client => instrumentation/opentelemetry-instrumentation-aiohttp-client}/README.rst (64%) rename {ext/opentelemetry-ext-aiohttp-client => instrumentation/opentelemetry-instrumentation-aiohttp-client}/setup.cfg (86%) rename {ext/opentelemetry-ext-aiohttp-client => instrumentation/opentelemetry-instrumentation-aiohttp-client}/setup.py (88%) rename {ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation}/aiohttp_client/__init__.py (95%) rename {ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation}/aiohttp_client/version.py (100%) rename {ext/opentelemetry-ext-aiohttp-client => instrumentation/opentelemetry-instrumentation-aiohttp-client}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-aiohttp-client => instrumentation/opentelemetry-instrumentation-aiohttp-client}/tests/test_aiohttp_client_integration.py (93%) rename {ext => instrumentation}/opentelemetry-instrumentation-aiopg/CHANGELOG.md (100%) rename {ext/opentelemetry-ext-django => instrumentation/opentelemetry-instrumentation-aiopg}/LICENSE (100%) rename {ext/opentelemetry-ext-django => instrumentation/opentelemetry-instrumentation-aiopg}/MANIFEST.in (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-aiopg/README.rst (75%) rename {ext => instrumentation}/opentelemetry-instrumentation-aiopg/setup.cfg (94%) rename {ext => instrumentation}/opentelemetry-instrumentation-aiopg/setup.py (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py (100%) rename {ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi => instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg}/version.py (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py (100%) rename {ext/opentelemetry-ext-asgi => instrumentation/opentelemetry-instrumentation-aiopg}/tests/__init__.py (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py (100%) rename {ext/opentelemetry-ext-asgi => instrumentation/opentelemetry-instrumentation-asgi}/CHANGELOG.md (53%) rename {ext/opentelemetry-ext-asgi => instrumentation/opentelemetry-instrumentation-asgi}/README.rst (74%) rename {ext/opentelemetry-ext-asgi => instrumentation/opentelemetry-instrumentation-asgi}/setup.cfg (87%) rename {ext/opentelemetry-ext-asgi => instrumentation/opentelemetry-instrumentation-asgi}/setup.py (91%) rename {ext/opentelemetry-ext-asgi/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation}/asgi/__init__.py (97%) rename {ext/opentelemetry-ext-django/src/opentelemetry/ext/django => instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi}/version.py (100%) rename {ext/opentelemetry-ext-django => instrumentation/opentelemetry-instrumentation-asgi}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-asgi => instrumentation/opentelemetry-instrumentation-asgi}/tests/test_asgi_middleware.py (99%) rename {ext/opentelemetry-ext-django => instrumentation/opentelemetry-instrumentation-django}/CHANGELOG.md (81%) rename {ext/opentelemetry-ext-flask => instrumentation/opentelemetry-instrumentation-django}/LICENSE (100%) rename {ext/opentelemetry-ext-flask => instrumentation/opentelemetry-instrumentation-django}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-django => instrumentation/opentelemetry-instrumentation-django}/README.rst (67%) rename {ext/opentelemetry-ext-django => instrumentation/opentelemetry-instrumentation-django}/setup.cfg (88%) rename {ext/opentelemetry-ext-django => instrumentation/opentelemetry-instrumentation-django}/setup.py (97%) rename {ext/opentelemetry-ext-django/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation}/django/__init__.py (97%) rename {ext/opentelemetry-ext-django/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation}/django/middleware.py (97%) rename {ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask => instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django}/version.py (100%) rename {ext/opentelemetry-ext-flask => instrumentation/opentelemetry-instrumentation-django}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-django => instrumentation/opentelemetry-instrumentation-django}/tests/conftest.py (100%) rename {ext/opentelemetry-ext-django => instrumentation/opentelemetry-instrumentation-django}/tests/test_middleware.py (96%) rename {ext/opentelemetry-ext-django => instrumentation/opentelemetry-instrumentation-django}/tests/views.py (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-fastapi/CHANGELOG.md (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-fastapi/README.rst (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-fastapi/setup.cfg (93%) rename {ext => instrumentation}/opentelemetry-instrumentation-fastapi/setup.py (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py (97%) rename {ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid => instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi}/version.py (100%) rename {ext/opentelemetry-ext-pyramid => instrumentation/opentelemetry-instrumentation-fastapi}/tests/__init__.py (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py (100%) rename {ext/opentelemetry-ext-flask => instrumentation/opentelemetry-instrumentation-flask}/CHANGELOG.md (85%) rename {ext/opentelemetry-ext-pyramid => instrumentation/opentelemetry-instrumentation-flask}/LICENSE (100%) rename {ext/opentelemetry-ext-pyramid => instrumentation/opentelemetry-instrumentation-flask}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-flask => instrumentation/opentelemetry-instrumentation-flask}/README.rst (67%) rename {ext/opentelemetry-ext-flask => instrumentation/opentelemetry-instrumentation-flask}/setup.cfg (88%) rename {ext/opentelemetry-ext-flask => instrumentation/opentelemetry-instrumentation-flask}/setup.py (85%) rename {ext/opentelemetry-ext-flask/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation}/flask/__init__.py (95%) rename {ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests => instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask}/version.py (100%) rename {ext/opentelemetry-ext-requests => instrumentation/opentelemetry-instrumentation-flask}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-flask => instrumentation/opentelemetry-instrumentation-flask}/tests/base_test.py (100%) rename {ext/opentelemetry-ext-flask => instrumentation/opentelemetry-instrumentation-flask}/tests/test_automatic.py (96%) rename {ext/opentelemetry-ext-flask => instrumentation/opentelemetry-instrumentation-flask}/tests/test_programmatic.py (97%) rename {ext/opentelemetry-ext-pyramid => instrumentation/opentelemetry-instrumentation-pyramid}/CHANGELOG.md (100%) rename {ext/opentelemetry-ext-requests => instrumentation/opentelemetry-instrumentation-pyramid}/LICENSE (100%) rename {ext/opentelemetry-ext-requests => instrumentation/opentelemetry-instrumentation-pyramid}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-pyramid => instrumentation/opentelemetry-instrumentation-pyramid}/README.rst (54%) rename {ext/opentelemetry-ext-pyramid => instrumentation/opentelemetry-instrumentation-pyramid}/setup.cfg (85%) rename {ext/opentelemetry-ext-wsgi => instrumentation/opentelemetry-instrumentation-pyramid}/setup.py (89%) rename {ext/opentelemetry-ext-pyramid/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation}/pyramid/__init__.py (85%) rename {ext/opentelemetry-ext-pyramid/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation}/pyramid/callbacks.py (94%) rename {ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi => instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid}/version.py (100%) rename {ext/opentelemetry-ext-wsgi => instrumentation/opentelemetry-instrumentation-pyramid}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-pyramid => instrumentation/opentelemetry-instrumentation-pyramid}/tests/pyramid_base_test.py (100%) rename {ext/opentelemetry-ext-pyramid => instrumentation/opentelemetry-instrumentation-pyramid}/tests/test_automatic.py (97%) rename {ext/opentelemetry-ext-pyramid => instrumentation/opentelemetry-instrumentation-pyramid}/tests/test_programmatic.py (94%) rename {ext/opentelemetry-ext-requests => instrumentation/opentelemetry-instrumentation-requests}/CHANGELOG.md (83%) rename {ext/opentelemetry-ext-wsgi => instrumentation/opentelemetry-instrumentation-requests}/LICENSE (100%) rename {ext/opentelemetry-ext-wsgi => instrumentation/opentelemetry-instrumentation-requests}/MANIFEST.in (100%) create mode 100644 instrumentation/opentelemetry-instrumentation-requests/README.rst rename {ext/opentelemetry-ext-requests => instrumentation/opentelemetry-instrumentation-requests}/setup.cfg (87%) rename {ext/opentelemetry-ext-requests => instrumentation/opentelemetry-instrumentation-requests}/setup.py (88%) rename {ext/opentelemetry-ext-requests/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation}/requests/__init__.py (97%) rename {ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg => instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests}/version.py (100%) rename {ext/opentelemetry-instrumentation-aiopg => instrumentation/opentelemetry-instrumentation-requests}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-requests => instrumentation/opentelemetry-instrumentation-requests}/tests/test_requests_integration.py (97%) rename {ext => instrumentation}/opentelemetry-instrumentation-starlette/CHANGELOG.md (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-starlette/README.rst (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-starlette/setup.cfg (93%) rename {ext => instrumentation}/opentelemetry-instrumentation-starlette/setup.py (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py (97%) rename {ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi => instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette}/version.py (100%) rename {ext/opentelemetry-instrumentation-fastapi => instrumentation/opentelemetry-instrumentation-starlette}/tests/__init__.py (100%) rename {ext => instrumentation}/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py (100%) rename {ext/opentelemetry-ext-wsgi => instrumentation/opentelemetry-instrumentation-wsgi}/CHANGELOG.md (82%) rename {ext/opentelemetry-instrumentation-aiopg => instrumentation/opentelemetry-instrumentation-wsgi}/LICENSE (100%) rename {ext/opentelemetry-instrumentation-aiopg => instrumentation/opentelemetry-instrumentation-wsgi}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-wsgi => instrumentation/opentelemetry-instrumentation-wsgi}/README.rst (63%) rename {ext/opentelemetry-ext-wsgi => instrumentation/opentelemetry-instrumentation-wsgi}/setup.cfg (94%) rename {ext/opentelemetry-ext-pyramid => instrumentation/opentelemetry-instrumentation-wsgi}/setup.py (91%) rename {ext/opentelemetry-ext-wsgi/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation}/wsgi/__init__.py (97%) rename {ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette => instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi}/version.py (100%) rename {ext/opentelemetry-instrumentation-starlette => instrumentation/opentelemetry-instrumentation-wsgi}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-wsgi => instrumentation/opentelemetry-instrumentation-wsgi}/tests/test_wsgi_middleware.py (99%) diff --git a/docs-requirements.txt b/docs-requirements.txt index 10ecacb2fd..e98a0d35df 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -8,6 +8,7 @@ asyncpg>=0.12.0 ddtrace>=0.34.0 aiohttp~= 3.0 aiopg>=0.13.0 +grpcio~=1.27 Deprecated>=1.2.6 django>=2.2 PyMySQL~=0.9.3 @@ -29,5 +30,3 @@ boto~=2.0 botocore~=1.0 starlette~=0.13 fastapi~=0.58.1 -opentelemetry-exporter-cloud-trace==0.10b0 -opentelemetry-exporter-cloud-monitoring==0.10b0 \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index d3af10b7d6..db836ea27e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,15 @@ for f in listdir(ext) if isdir(join(ext, f)) ] -sys.path[:0] = source_dirs + exp_dirs + ext_dirs + +instr = "../instrumentation" +instr_dirs = [ + os.path.abspath("/".join(["../instrumentation", f, "src"])) + for f in listdir(instr) + if isdir(join(instr, f)) +] + +sys.path[:0] = source_dirs + exp_dirs + ext_dirs + instr_dirs # -- Project information ----------------------------------------------------- diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index b5a7649503..e4af3c6bd5 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -73,7 +73,7 @@ Installation $ pip install opentelemetry-sdk $ pip install opentelemetry-instrumentation - $ pip install opentelemetry-ext-flask + $ pip install opentelemetry-instrumentation-flask $ pip install requests Execution diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_instrumented.py index 528b107e03..c8562828be 100644 --- a/docs/examples/auto-instrumentation/server_instrumented.py +++ b/docs/examples/auto-instrumentation/server_instrumented.py @@ -15,7 +15,7 @@ from flask import Flask, request from opentelemetry import propagators, trace -from opentelemetry.ext.wsgi import collect_request_attributes +from opentelemetry.instrumentation.wsgi import collect_request_attributes from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, diff --git a/docs/examples/datadog_exporter/README.rst b/docs/examples/datadog_exporter/README.rst index 250eec2b84..03f485da15 100644 --- a/docs/examples/datadog_exporter/README.rst +++ b/docs/examples/datadog_exporter/README.rst @@ -50,7 +50,7 @@ Distributed Example pip install opentelemetry-sdk pip install opentelemetry-exporter-datadog pip install opentelemetry-instrumentation - pip install opentelemetry-ext-flask + pip install opentelemetry-instrumentation-flask pip install flask pip install requests diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 6f441dd333..38ef365b32 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -1,7 +1,7 @@ OpenTelemetry Django Instrumentation Example ============================================ -This shows how to use ``opentelemetry-ext-django`` to automatically instrument a +This shows how to use ``opentelemetry-instrumentation-django`` to automatically instrument a Django app. For more user convenience, a Django app is already provided in this directory. @@ -24,7 +24,7 @@ Installation .. code-block:: $ pip install opentelemetry-sdk - $ pip install opentelemetry-ext-django + $ pip install opentelemetry-instrumentation-django $ pip install requests @@ -40,7 +40,7 @@ Set these environment variables first: #. ``export DJANGO_SETTINGS_MODULE=instrumentation_example.settings`` The way to achieve OpenTelemetry instrumentation for your Django app is to use -an ``opentelemetry.ext.django.DjangoInstrumentor`` to instrument the app. +an ``opentelemetry.instrumentation.django.DjangoInstrumentor`` to instrument the app. Clone the ``opentelemetry-python`` repository and go to ``opentelemetry-python/docs/examples/django``. @@ -105,4 +105,4 @@ References * `Django `_ * `OpenTelemetry Project `_ -* `OpenTelemetry Django extension `_ +* `OpenTelemetry Django extension `_ diff --git a/docs/examples/django/manage.py b/docs/examples/django/manage.py index fdf32287c5..3a67dbf829 100755 --- a/docs/examples/django/manage.py +++ b/docs/examples/django/manage.py @@ -17,7 +17,7 @@ import os import sys -from opentelemetry.ext.django import DjangoInstrumentor +from opentelemetry.instrumentation.django import DjangoInstrumentor def main(): diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index f7dd1eccdf..e77fbae376 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -45,8 +45,8 @@ install_requires = typing; python_version<'3.5' opentelemetry-api == 0.12.dev0 opentelemetry-sdk == 0.12.dev0 - opentelemetry-ext-requests == 0.12.dev0 - opentelemetry-ext-flask == 0.12.dev0 + opentelemetry-instrumentation-requests == 0.12.dev0 + opentelemetry-instrumentation-flask == 0.12.dev0 flask requests protobuf~=3.11 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 8f44273b6e..9838336504 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -19,9 +19,9 @@ import flask import requests -import opentelemetry.ext.requests +import opentelemetry.instrumentation.requests from opentelemetry import trace -from opentelemetry.ext.flask import FlaskInstrumentor +from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -33,7 +33,7 @@ # It must be done before instrumenting any library trace.set_tracer_provider(TracerProvider()) -opentelemetry.ext.requests.RequestsInstrumentor().instrument() +opentelemetry.instrumentation.requests.RequestsInstrumentor().instrument() trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) diff --git a/docs/ext/aiohttp_client/aiohttp_client.rst b/docs/ext/aiohttp_client/aiohttp_client.rst deleted file mode 100644 index e5ab26b0ea..0000000000 --- a/docs/ext/aiohttp_client/aiohttp_client.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry aiohttp client Integration -======================================== - -.. automodule:: opentelemetry.ext.aiohttp_client - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/ext/asgi/asgi.rst b/docs/ext/asgi/asgi.rst deleted file mode 100644 index 82c9833069..0000000000 --- a/docs/ext/asgi/asgi.rst +++ /dev/null @@ -1,10 +0,0 @@ -opentelemetry.ext.asgi package -============================== - -Module contents ---------------- - -.. automodule:: opentelemetry.ext.asgi - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/ext/flask/flask.rst b/docs/ext/flask/flask.rst deleted file mode 100644 index 2fc19de3ec..0000000000 --- a/docs/ext/flask/flask.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Flask Integration -=============================== - -.. automodule:: opentelemetry.ext.flask - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/ext/grpc/grpc.rst b/docs/ext/grpc/grpc.rst index 8a0775f28f..351d3bfb0d 100644 --- a/docs/ext/grpc/grpc.rst +++ b/docs/ext/grpc/grpc.rst @@ -1,5 +1,5 @@ -OpenTelemetry gRPC Integration -============================== +OpenTelemetry gRPC Instrumentation +================================== Module contents --------------- diff --git a/docs/ext/pyramid/pyramid.rst b/docs/ext/pyramid/pyramid.rst deleted file mode 100644 index b46718c387..0000000000 --- a/docs/ext/pyramid/pyramid.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Pyramid Integration -================================= - -.. automodule:: opentelemetry.ext.pyramid - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/ext/wsgi/wsgi.rst b/docs/ext/wsgi/wsgi.rst deleted file mode 100644 index af2bd4dd36..0000000000 --- a/docs/ext/wsgi/wsgi.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry WSGI Middleware -============================= - -.. automodule:: opentelemetry.ext.wsgi - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/getting-started.rst b/docs/getting-started.rst index c4ed47ad33..11e3a08fec 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -156,8 +156,8 @@ We will now instrument a basic Flask application that uses the requests library .. code-block:: sh - pip install opentelemetry-ext-flask - pip install opentelemetry-ext-requests + pip install opentelemetry-instrumentation-flask + pip install opentelemetry-instrumentation-requests And let's write a small Flask application that sends an HTTP request, activating each instrumentation during the initialization: diff --git a/docs/getting_started/flask_example.py b/docs/getting_started/flask_example.py index 77a50a799b..c4a172389c 100644 --- a/docs/getting_started/flask_example.py +++ b/docs/getting_started/flask_example.py @@ -17,8 +17,8 @@ import requests from opentelemetry import trace -from opentelemetry.ext.flask import FlaskInstrumentor -from opentelemetry.ext.requests import RequestsInstrumentor +from opentelemetry.instrumentation.flask import FlaskInstrumentor +from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, diff --git a/docs/index.rst b/docs/index.rst index b59af3a003..3378daac07 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -84,6 +84,14 @@ install :name: Instrumentations :glob: + instrumentation/** + +.. toctree:: + :maxdepth: 2 + :caption: OpenTelemetry Integrations + :name: integrations + :glob: + ext/** .. toctree:: diff --git a/docs/instrumentation/aiohttp_client/aiohttp_client.rst b/docs/instrumentation/aiohttp_client/aiohttp_client.rst new file mode 100644 index 0000000000..f8549f07fa --- /dev/null +++ b/docs/instrumentation/aiohttp_client/aiohttp_client.rst @@ -0,0 +1,7 @@ +OpenTelemetry aiohttp client Instrumentation +============================================ + +.. automodule:: opentelemetry.instrumentation.aiohttp_client + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ext/aiopg/aiopg.rst b/docs/instrumentation/aiopg/aiopg.rst similarity index 80% rename from docs/ext/aiopg/aiopg.rst rename to docs/instrumentation/aiopg/aiopg.rst index ff8d91ed11..9da450c4e7 100644 --- a/docs/ext/aiopg/aiopg.rst +++ b/docs/instrumentation/aiopg/aiopg.rst @@ -1,4 +1,4 @@ -OpenTelemetry aiopg instrumentation +OpenTelemetry aiopg Instrumentation =================================== .. automodule:: opentelemetry.instrumentation.aiopg diff --git a/docs/instrumentation/asgi/asgi.rst b/docs/instrumentation/asgi/asgi.rst new file mode 100644 index 0000000000..abb1621973 --- /dev/null +++ b/docs/instrumentation/asgi/asgi.rst @@ -0,0 +1,10 @@ +OpenTelemetry asgi Instrumentation +=================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.instrumentation.asgi + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ext/django/django.rst b/docs/instrumentation/django/django.rst similarity index 71% rename from docs/ext/django/django.rst rename to docs/instrumentation/django/django.rst index 1a2c844e28..8076730843 100644 --- a/docs/ext/django/django.rst +++ b/docs/instrumentation/django/django.rst @@ -1,7 +1,7 @@ OpenTelemetry Django Instrumentation ==================================== -.. automodule:: opentelemetry.ext.django +.. automodule:: opentelemetry.instrumentation.django :members: :undoc-members: :show-inheritance: diff --git a/docs/ext/fastapi/fastapi.rst b/docs/instrumentation/fastapi/fastapi.rst similarity index 57% rename from docs/ext/fastapi/fastapi.rst rename to docs/instrumentation/fastapi/fastapi.rst index 9295261584..09eb8a828b 100644 --- a/docs/ext/fastapi/fastapi.rst +++ b/docs/instrumentation/fastapi/fastapi.rst @@ -1,4 +1,4 @@ -.. include:: ../../../ext/opentelemetry-instrumentation-fastapi/README.rst +.. include:: ../../../instrumentation/opentelemetry-instrumentation-fastapi/README.rst API --- diff --git a/docs/instrumentation/flask/flask.rst b/docs/instrumentation/flask/flask.rst new file mode 100644 index 0000000000..ac8932acd4 --- /dev/null +++ b/docs/instrumentation/flask/flask.rst @@ -0,0 +1,7 @@ +OpenTelemetry Flask Instrumentation +=================================== + +.. automodule:: opentelemetry.instrumentation.flask + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/instrumentation/instrumentation.rst b/docs/instrumentation/instrumentation.rst index bcb85043f4..9c01b6b6f4 100644 --- a/docs/instrumentation/instrumentation.rst +++ b/docs/instrumentation/instrumentation.rst @@ -1,5 +1,5 @@ -OpenTelemetry Python Instrumentation -==================================== +OpenTelemetry Python Instrumentor +================================= .. automodule:: opentelemetry.instrumentation :members: diff --git a/docs/instrumentation/pyramid/pyramid.rst b/docs/instrumentation/pyramid/pyramid.rst new file mode 100644 index 0000000000..56abd2800b --- /dev/null +++ b/docs/instrumentation/pyramid/pyramid.rst @@ -0,0 +1,7 @@ +OpenTelemetry Pyramid Instrumentation +===================================== + +.. automodule:: opentelemetry.instrumentation.pyramid + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/instrumentation/requests/requests.rst b/docs/instrumentation/requests/requests.rst new file mode 100644 index 0000000000..7a0665cd99 --- /dev/null +++ b/docs/instrumentation/requests/requests.rst @@ -0,0 +1,7 @@ +OpenTelemetry requests Instrumentation +====================================== + +.. automodule:: opentelemetry.instrumentation.requests + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ext/starlette/starlette.rst b/docs/instrumentation/starlette/starlette.rst similarity index 57% rename from docs/ext/starlette/starlette.rst rename to docs/instrumentation/starlette/starlette.rst index 8e2d1d7bc8..0efa8cce83 100644 --- a/docs/ext/starlette/starlette.rst +++ b/docs/instrumentation/starlette/starlette.rst @@ -1,4 +1,4 @@ -.. include:: ../../../ext/opentelemetry-instrumentation-starlette/README.rst +.. include:: ../../../instrumentation/opentelemetry-instrumentation-starlette/README.rst API --- diff --git a/docs/ext/requests/requests.rst b/docs/instrumentation/wsgi/wsgi.rst similarity index 51% rename from docs/ext/requests/requests.rst rename to docs/instrumentation/wsgi/wsgi.rst index 5959d4c924..39ad5ffd58 100644 --- a/docs/ext/requests/requests.rst +++ b/docs/instrumentation/wsgi/wsgi.rst @@ -1,7 +1,7 @@ -OpenTelemetry requests Integration +OpenTelemetry WSGI Instrumentation ================================== -.. automodule:: opentelemetry.ext.requests +.. automodule:: opentelemetry.instrumentation.wsgi :members: :undoc-members: :show-inheritance: diff --git a/eachdist.ini b/eachdist.ini index 82fc0271ed..06d940378c 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -7,11 +7,12 @@ sortfirst= opentelemetry-instrumentation opentelemetry-proto tests/util - exporter/* - ext/opentelemetry-ext-wsgi + instrumentation/opentelemetry-instrumentation-wsgi ext/opentelemetry-ext-dbapi - ext/opentelemetry-ext-asgi + instrumentation/opentelemetry-instrumentation-asgi ext/opentelemetry-ext-botocore + instrumentation/* + exporter/* ext/* [lintroots] diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py index e11772d0d9..785bccccce 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py @@ -32,11 +32,11 @@ DEFAULT_AGENT_URL = "http://localhost:8126" _INSTRUMENTATION_SPAN_TYPES = { - "opentelemetry.ext.aiohttp-client": DatadogSpanTypes.HTTP, - "opentelemetry.ext.asgi": DatadogSpanTypes.WEB, + "opentelemetry.instrumentation.aiohttp-client": DatadogSpanTypes.HTTP, + "opentelemetry.instrumentation.asgi": DatadogSpanTypes.WEB, "opentelemetry.ext.dbapi": DatadogSpanTypes.SQL, - "opentelemetry.ext.django": DatadogSpanTypes.WEB, - "opentelemetry.ext.flask": DatadogSpanTypes.WEB, + "opentelemetry.instrumentation.django": DatadogSpanTypes.WEB, + "opentelemetry.instrumentation.flask": DatadogSpanTypes.WEB, "opentelemetry.ext.grpc": DatadogSpanTypes.GRPC, "opentelemetry.ext.jinja2": DatadogSpanTypes.TEMPLATE, "opentelemetry.ext.mysql": DatadogSpanTypes.SQL, @@ -45,9 +45,9 @@ "opentelemetry.ext.pymongo": DatadogSpanTypes.MONGODB, "opentelemetry.ext.pymysql": DatadogSpanTypes.SQL, "opentelemetry.ext.redis": DatadogSpanTypes.REDIS, - "opentelemetry.ext.requests": DatadogSpanTypes.HTTP, + "opentelemetry.instrumentation.requests": DatadogSpanTypes.HTTP, "opentelemetry.ext.sqlalchemy": DatadogSpanTypes.SQL, - "opentelemetry.ext.wsgi": DatadogSpanTypes.WEB, + "opentelemetry.instrumentation.wsgi": DatadogSpanTypes.WEB, } diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py index a3e67790d9..cf3c3c7256 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py @@ -290,10 +290,10 @@ def test_resources(self): def test_span_types(self): test_instrumentations = [ - "opentelemetry.ext.aiohttp-client", + "opentelemetry.instrumentation.aiohttp-client", "opentelemetry.ext.dbapi", - "opentelemetry.ext.django", - "opentelemetry.ext.flask", + "opentelemetry.instrumentation.django", + "opentelemetry.instrumentation.flask", "opentelemetry.ext.grpc", "opentelemetry.ext.jinja2", "opentelemetry.ext.mysql", @@ -301,9 +301,9 @@ def test_span_types(self): "opentelemetry.ext.pymongo", "opentelemetry.ext.pymysql", "opentelemetry.ext.redis", - "opentelemetry.ext.requests", + "opentelemetry.instrumentation.requests", "opentelemetry.ext.sqlalchemy", - "opentelemetry.ext.wsgi", + "opentelemetry.instrumentation.wsgi", ] for index, instrumentation in enumerate(test_instrumentations): diff --git a/ext/opentelemetry-ext-aiohttp-client/CHANGELOG.md b/ext/opentelemetry-ext-aiohttp-client/CHANGELOG.md deleted file mode 100644 index 43990fab16..0000000000 --- a/ext/opentelemetry-ext-aiohttp-client/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -## Unreleased - -## 0.7b1 - -Released 2020-05-12 - -- Initial release diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index c7dac4c356..a3533bdf59 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -14,7 +14,7 @@ [metadata] name = opentelemetry-ext-grpc -description = OpenTelemetry gRPC Integration +description = OpenTelemetry gRPC instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors diff --git a/ext/opentelemetry-ext-requests/README.rst b/ext/opentelemetry-ext-requests/README.rst deleted file mode 100644 index 95d80d4997..0000000000 --- a/ext/opentelemetry-ext-requests/README.rst +++ /dev/null @@ -1,23 +0,0 @@ -OpenTelemetry requests Integration -================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-requests.svg - :target: https://pypi.org/project/opentelemetry-ext-requests/ - -This library allows tracing HTTP requests made by the -`requests `_ library. - -Installation ------------- - -:: - - pip install opentelemetry-ext-requests - -References ----------- - -* `OpenTelemetry requests Integration `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md new file mode 100644 index 0000000000..d7dce5f65c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## Unreleased + +- Change package name to opentelemetry-instrumentation-aiohttp-client + ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) + +## 0.7b1 + +Released 2020-05-12 + +- Initial release diff --git a/ext/opentelemetry-ext-aiohttp-client/LICENSE b/instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE similarity index 100% rename from ext/opentelemetry-ext-aiohttp-client/LICENSE rename to instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE diff --git a/ext/opentelemetry-ext-aiohttp-client/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-aiohttp-client/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-aiohttp-client/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-aiohttp-client/MANIFEST.in diff --git a/ext/opentelemetry-ext-aiohttp-client/README.rst b/instrumentation/opentelemetry-instrumentation-aiohttp-client/README.rst similarity index 64% rename from ext/opentelemetry-ext-aiohttp-client/README.rst rename to instrumentation/opentelemetry-instrumentation-aiohttp-client/README.rst index c257639cf3..bc44e0e262 100644 --- a/ext/opentelemetry-ext-aiohttp-client/README.rst +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/README.rst @@ -3,8 +3,8 @@ OpenTelemetry aiohttp client Integration |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-aiohttp-client.svg - :target: https://pypi.org/project/opentelemetry-ext-aiohttp-client/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aiohttp-client.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-aiohttp-client/ This library allows tracing HTTP requests made by the `aiohttp client `_ library. @@ -14,7 +14,7 @@ Installation :: - pip install opentelemetry-ext-aiohttp-client + pip install opentelemetry-instrumentation-aiohttp-client References diff --git a/ext/opentelemetry-ext-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg similarity index 86% rename from ext/opentelemetry-ext-aiohttp-client/setup.cfg rename to instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index d2be7e8c6e..318721ba64 100644 --- a/ext/opentelemetry-ext-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-aiohttp-client -description = OpenTelemetry aiohttp client integration +name = opentelemetry-instrumentation-aiohttp-client +description = OpenTelemetry aiohttp client instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-aiohttp-client +url = https://github.com/open-telemetry/opentelemetry-python/instrumentation/opentelemetry-instrumentation-aiohttp-client platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-aiohttp-client/setup.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.py similarity index 88% rename from ext/opentelemetry-ext-aiohttp-client/setup.py rename to instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.py index 1b44425f06..fe74e23235 100644 --- a/ext/opentelemetry-ext-aiohttp-client/setup.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.py @@ -17,7 +17,12 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "aiohttp_client", "version.py" + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "aiohttp_client", + "version.py", ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py similarity index 95% rename from ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py rename to instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py index a05e7c0601..2d9b8bd7a5 100644 --- a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py @@ -13,8 +13,8 @@ # limitations under the License. """ -The opentelemetry-ext-aiohttp-client package allows tracing HTTP requests -made by the aiohttp client library. +The opentelemetry-instrumentation-aiohttp-client package allows tracing HTTP +requests made by the aiohttp client library. Usage ----- @@ -22,7 +22,7 @@ .. code:: python import aiohttp - from opentelemetry.ext.aiohttp_client import ( + from opentelemetry.instrumentation.aiohttp_client import ( create_trace_config, url_path_span_name ) @@ -51,7 +51,7 @@ def strip_query_params(url: yarl.URL) -> str: from opentelemetry import context as context_api from opentelemetry import propagators, trace -from opentelemetry.ext.aiohttp_client.version import __version__ +from opentelemetry.instrumentation.aiohttp_client.version import __version__ from opentelemetry.instrumentation.utils import http_status_to_canonical_code from opentelemetry.trace import SpanKind from opentelemetry.trace.status import Status, StatusCanonicalCode @@ -92,7 +92,7 @@ def create_trace_config( .. code:: python import aiohttp - from opentelemetry.ext.aiohttp_client import create_trace_config + from opentelemetry.instrumentation.aiohttp_client import create_trace_config async with aiohttp.ClientSession(trace_configs=[create_trace_config()]) as session: async with session.get(url) as response: diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py similarity index 100% rename from ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/version.py rename to instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py diff --git a/ext/opentelemetry-ext-aiohttp-client/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-aiohttp-client/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/__init__.py diff --git a/ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py similarity index 93% rename from ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py rename to instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py index 8db20c227e..f44e3df2da 100644 --- a/ext/opentelemetry-ext-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -22,7 +22,7 @@ import aiohttp.test_utils import yarl -import opentelemetry.ext.aiohttp_client +import opentelemetry.instrumentation.aiohttp_client from opentelemetry.test.test_base import TestBase from opentelemetry.trace.status import StatusCanonicalCode @@ -53,7 +53,7 @@ def test_url_path_span_name(self): ): with self.subTest(url=url): params = aiohttp.TraceRequestStartParams("METHOD", url, {}) - actual = opentelemetry.ext.aiohttp_client.url_path_span_name( + actual = opentelemetry.instrumentation.aiohttp_client.url_path_span_name( params ) self.assertEqual(actual, expected) @@ -110,7 +110,7 @@ def test_status_codes(self): ): with self.subTest(status_code=status_code): host, port = self._http_request( - trace_config=opentelemetry.ext.aiohttp_client.create_trace_config(), + trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), url="/test-path?query=param#foobar", status_code=status_code, ) @@ -149,7 +149,7 @@ def test_span_name_option(self): ): with self.subTest(span_name=span_name, method=method, path=path): host, port = self._http_request( - trace_config=opentelemetry.ext.aiohttp_client.create_trace_config( + trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config( span_name=span_name ), method=method, @@ -182,7 +182,7 @@ def strip_query_params(url: yarl.URL) -> str: return str(url.with_query(None)) host, port = self._http_request( - trace_config=opentelemetry.ext.aiohttp_client.create_trace_config( + trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config( url_filter=strip_query_params ), url="/some/path?query=param&other=param2", @@ -209,7 +209,7 @@ def strip_query_params(url: yarl.URL) -> str: def test_connection_errors(self): trace_configs = [ - opentelemetry.ext.aiohttp_client.create_trace_config() + opentelemetry.instrumentation.aiohttp_client.create_trace_config() ] for url, expected_status in ( @@ -251,7 +251,7 @@ async def request_handler(request): return aiohttp.web.Response() host, port = self._http_request( - trace_config=opentelemetry.ext.aiohttp_client.create_trace_config(), + trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), url="/test_timeout", request_handler=request_handler, timeout=aiohttp.ClientTimeout(sock_read=0.01), @@ -281,7 +281,7 @@ async def request_handler(request): raise aiohttp.web.HTTPFound(location=location) host, port = self._http_request( - trace_config=opentelemetry.ext.aiohttp_client.create_trace_config(), + trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), url="/test_too_many_redirects", request_handler=request_handler, max_redirects=2, diff --git a/ext/opentelemetry-instrumentation-aiopg/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-aiopg/CHANGELOG.md similarity index 100% rename from ext/opentelemetry-instrumentation-aiopg/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-aiopg/CHANGELOG.md diff --git a/ext/opentelemetry-ext-django/LICENSE b/instrumentation/opentelemetry-instrumentation-aiopg/LICENSE similarity index 100% rename from ext/opentelemetry-ext-django/LICENSE rename to instrumentation/opentelemetry-instrumentation-aiopg/LICENSE diff --git a/ext/opentelemetry-ext-django/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-aiopg/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-django/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-aiopg/MANIFEST.in diff --git a/ext/opentelemetry-instrumentation-aiopg/README.rst b/instrumentation/opentelemetry-instrumentation-aiopg/README.rst similarity index 75% rename from ext/opentelemetry-instrumentation-aiopg/README.rst rename to instrumentation/opentelemetry-instrumentation-aiopg/README.rst index 0e9248ec1d..f7a66579df 100644 --- a/ext/opentelemetry-instrumentation-aiopg/README.rst +++ b/instrumentation/opentelemetry-instrumentation-aiopg/README.rst @@ -17,5 +17,5 @@ Installation References ---------- -* `OpenTelemetry aiopg instrumentation `_ +* `OpenTelemetry aiopg Instrumentation `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-instrumentation-aiopg/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg similarity index 94% rename from ext/opentelemetry-instrumentation-aiopg/setup.cfg rename to instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg index f2428301b5..bd9a06c524 100644 --- a/ext/opentelemetry-instrumentation-aiopg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-instrumentaation-aiopg +name = opentelemetry-instrumentation-aiopg description = OpenTelemetry aiopg instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-instrumentation-aiopg +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-aiopg platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-instrumentation-aiopg/setup.py b/instrumentation/opentelemetry-instrumentation-aiopg/setup.py similarity index 100% rename from ext/opentelemetry-instrumentation-aiopg/setup.py rename to instrumentation/opentelemetry-instrumentation-aiopg/setup.py diff --git a/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py similarity index 100% rename from ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py rename to instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py diff --git a/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py similarity index 100% rename from ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py rename to instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py diff --git a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py similarity index 100% rename from ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/version.py rename to instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py diff --git a/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py similarity index 100% rename from ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py rename to instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py diff --git a/ext/opentelemetry-ext-asgi/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-aiopg/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-asgi/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-aiopg/tests/__init__.py diff --git a/ext/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py similarity index 100% rename from ext/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py rename to instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py diff --git a/ext/opentelemetry-ext-asgi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-asgi/CHANGELOG.md similarity index 53% rename from ext/opentelemetry-ext-asgi/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-asgi/CHANGELOG.md index 886cbff8f1..7f2812bd05 100644 --- a/ext/opentelemetry-ext-asgi/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-asgi/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-asgi + ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) + ## 0.8b0 Released 2020-05-27 diff --git a/ext/opentelemetry-ext-asgi/README.rst b/instrumentation/opentelemetry-instrumentation-asgi/README.rst similarity index 74% rename from ext/opentelemetry-ext-asgi/README.rst rename to instrumentation/opentelemetry-instrumentation-asgi/README.rst index bc286e5c8b..f2b760976a 100644 --- a/ext/opentelemetry-ext-asgi/README.rst +++ b/instrumentation/opentelemetry-instrumentation-asgi/README.rst @@ -3,8 +3,8 @@ OpenTelemetry ASGI Middleware |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-asgi.svg - :target: https://pypi.org/project/opentelemetry-ext-asgi/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-asgi.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-asgi/ This library provides a ASGI middleware that can be used on any ASGI framework @@ -15,7 +15,7 @@ Installation :: - pip install opentelemetry-ext-asgi + pip install opentelemetry-instrumentation-asgi Usage (Quart) @@ -24,7 +24,7 @@ Usage (Quart) .. code-block:: python from quart import Quart - from opentelemetry.ext.asgi import OpenTelemetryMiddleware + from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware app = Quart(__name__) app.asgi_app = OpenTelemetryMiddleware(app.asgi_app) @@ -46,7 +46,7 @@ Modify the application's ``asgi.py`` file as shown below. import os from django.core.asgi import get_asgi_application - from opentelemetry.ext.asgi import OpenTelemetryMiddleware + from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'asgi_example.settings') diff --git a/ext/opentelemetry-ext-asgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg similarity index 87% rename from ext/opentelemetry-ext-asgi/setup.cfg rename to instrumentation/opentelemetry-instrumentation-asgi/setup.cfg index 175e264995..fdd1f813fb 100644 --- a/ext/opentelemetry-ext-asgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-asgi -description = ASGI Middleware for OpenTelemetry +name = opentelemetry-instrumentation-asgi +description = ASGI instrumentation for OpenTelemetry long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-asgi +url = https://github.com/open-telemetry/opentelemetry-python/instrumentation/opentelemetry-instrumentation-asgi platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-asgi/setup.py b/instrumentation/opentelemetry-instrumentation-asgi/setup.py similarity index 91% rename from ext/opentelemetry-ext-asgi/setup.py rename to instrumentation/opentelemetry-instrumentation-asgi/setup.py index 8bf19bd422..3369352fe1 100644 --- a/ext/opentelemetry-ext-asgi/setup.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "asgi", "version.py" + BASE_DIR, "src", "opentelemetry", "instrumentation", "asgi", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py similarity index 97% rename from ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/__init__.py rename to instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index 43b8804c24..02aabfea95 100644 --- a/ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -13,7 +13,7 @@ # limitations under the License. """ -The opentelemetry-ext-asgi package provides an ASGI middleware that can be used +The opentelemetry-instrumentation-asgi package provides an ASGI middleware that can be used on any ASGI framework (such as Django-channels / Quart) to track requests timing through OpenTelemetry. """ @@ -27,7 +27,7 @@ from asgiref.compatibility import guarantee_single_callable from opentelemetry import context, propagators, trace -from opentelemetry.ext.asgi.version import __version__ # noqa +from opentelemetry.instrumentation.asgi.version import __version__ # noqa from opentelemetry.instrumentation.utils import http_status_to_canonical_code from opentelemetry.trace.status import Status, StatusCanonicalCode diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py similarity index 100% rename from ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py rename to instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py diff --git a/ext/opentelemetry-ext-django/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-django/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-asgi/tests/__init__.py diff --git a/ext/opentelemetry-ext-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py similarity index 99% rename from ext/opentelemetry-ext-asgi/tests/test_asgi_middleware.py rename to instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py index 05aa84b3c4..f994e25966 100644 --- a/ext/opentelemetry-ext-asgi/tests/test_asgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py @@ -16,7 +16,7 @@ import unittest import unittest.mock as mock -import opentelemetry.ext.asgi as otel_asgi +import opentelemetry.instrumentation.asgi as otel_asgi from opentelemetry import trace as trace_api from opentelemetry.test.asgitestutil import ( AsgiTestBase, diff --git a/ext/opentelemetry-ext-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md similarity index 81% rename from ext/opentelemetry-ext-django/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index 0328ccaa20..ae682630db 100644 --- a/ext/opentelemetry-ext-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-django + ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) ## Version 0.11b0 diff --git a/ext/opentelemetry-ext-flask/LICENSE b/instrumentation/opentelemetry-instrumentation-django/LICENSE similarity index 100% rename from ext/opentelemetry-ext-flask/LICENSE rename to instrumentation/opentelemetry-instrumentation-django/LICENSE diff --git a/ext/opentelemetry-ext-flask/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-django/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-flask/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-django/MANIFEST.in diff --git a/ext/opentelemetry-ext-django/README.rst b/instrumentation/opentelemetry-instrumentation-django/README.rst similarity index 67% rename from ext/opentelemetry-ext-django/README.rst rename to instrumentation/opentelemetry-instrumentation-django/README.rst index b7a822f197..5cb570c7e9 100644 --- a/ext/opentelemetry-ext-django/README.rst +++ b/instrumentation/opentelemetry-instrumentation-django/README.rst @@ -3,8 +3,8 @@ OpenTelemetry Django Tracing |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-django.svg - :target: https://pypi.org/project/opentelemetry-ext-django/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-django.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-django/ This library allows tracing requests for Django applications. @@ -13,7 +13,7 @@ Installation :: - pip install opentelemetry-ext-django + pip install opentelemetry-instrumentation-django Configuration ------------- @@ -34,5 +34,5 @@ References ---------- * `Django `_ -* `OpenTelemetry Django Tracing `_ +* `OpenTelemetry Instrumentation for Django `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-django/setup.cfg b/instrumentation/opentelemetry-instrumentation-django/setup.cfg similarity index 88% rename from ext/opentelemetry-ext-django/setup.cfg rename to instrumentation/opentelemetry-instrumentation-django/setup.cfg index 602a2cf7ba..b2ccaf22b8 100644 --- a/ext/opentelemetry-ext-django/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-django/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-django +name = opentelemetry-instrumentation-django description = OpenTelemetry Instrumentation for Django long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-django +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-django platforms = any license = Apache-2.0 classifiers = @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: install_requires = django >= 1.10 - opentelemetry-ext-wsgi == 0.12.dev0 + opentelemetry-instrumentation-wsgi == 0.12.dev0 opentelemetry-instrumentation == 0.12.dev0 opentelemetry-api == 0.12.dev0 @@ -54,4 +54,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - django = opentelemetry.ext.django:DjangoInstrumentor + django = opentelemetry.instrumentation.django:DjangoInstrumentor diff --git a/ext/opentelemetry-ext-django/setup.py b/instrumentation/opentelemetry-instrumentation-django/setup.py similarity index 97% rename from ext/opentelemetry-ext-django/setup.py rename to instrumentation/opentelemetry-instrumentation-django/setup.py index 45cc68c0f4..fb9d615ce3 100644 --- a/ext/opentelemetry-ext-django/setup.py +++ b/instrumentation/opentelemetry-instrumentation-django/setup.py @@ -22,7 +22,7 @@ dirname(__file__), "src", "opentelemetry", - "ext", + "instrumentation", "django", "version.py", ) diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py similarity index 97% rename from ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py rename to instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index 3eb3b2dd72..e0992e9a30 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -17,7 +17,7 @@ from django.conf import settings from opentelemetry.configuration import Configuration -from opentelemetry.ext.django.middleware import _DjangoMiddleware +from opentelemetry.instrumentation.django.middleware import _DjangoMiddleware from opentelemetry.instrumentation.instrumentor import BaseInstrumentor _logger = getLogger(__name__) diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py similarity index 97% rename from ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py rename to instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index c0cb244756..2c155b1be1 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -16,8 +16,8 @@ from opentelemetry.configuration import Configuration from opentelemetry.context import attach, detach -from opentelemetry.ext.django.version import __version__ -from opentelemetry.ext.wsgi import ( +from opentelemetry.instrumentation.django.version import __version__ +from opentelemetry.instrumentation.wsgi import ( add_response_attributes, collect_request_attributes, get_header_from_environ, diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py similarity index 100% rename from ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py rename to instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py diff --git a/ext/opentelemetry-ext-flask/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-django/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-flask/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-django/tests/__init__.py diff --git a/ext/opentelemetry-ext-django/tests/conftest.py b/instrumentation/opentelemetry-instrumentation-django/tests/conftest.py similarity index 100% rename from ext/opentelemetry-ext-django/tests/conftest.py rename to instrumentation/opentelemetry-instrumentation-django/tests/conftest.py diff --git a/ext/opentelemetry-ext-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py similarity index 96% rename from ext/opentelemetry-ext-django/tests/test_middleware.py rename to instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index e5c3e10d03..3250ac0c1c 100644 --- a/ext/opentelemetry-ext-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -21,7 +21,7 @@ from django.test.utils import setup_test_environment, teardown_test_environment from opentelemetry.configuration import Configuration -from opentelemetry.ext.django import DjangoInstrumentor +from opentelemetry.instrumentation.django import DjangoInstrumentor from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import SpanKind from opentelemetry.trace.status import StatusCanonicalCode @@ -116,7 +116,7 @@ def test_error(self): self.assertEqual(span.attributes["http.scheme"], "http") @patch( - "opentelemetry.ext.django.middleware._DjangoMiddleware._excluded_urls", + "opentelemetry.instrumentation.django.middleware._DjangoMiddleware._excluded_urls", ExcludeList(["http://testserver/excluded_arg/123", "excluded_noarg"]), ) def test_exclude_lists(self): diff --git a/ext/opentelemetry-ext-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py similarity index 100% rename from ext/opentelemetry-ext-django/tests/views.py rename to instrumentation/opentelemetry-instrumentation-django/tests/views.py diff --git a/ext/opentelemetry-instrumentation-fastapi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-fastapi/CHANGELOG.md similarity index 100% rename from ext/opentelemetry-instrumentation-fastapi/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-fastapi/CHANGELOG.md diff --git a/ext/opentelemetry-instrumentation-fastapi/README.rst b/instrumentation/opentelemetry-instrumentation-fastapi/README.rst similarity index 100% rename from ext/opentelemetry-instrumentation-fastapi/README.rst rename to instrumentation/opentelemetry-instrumentation-fastapi/README.rst diff --git a/ext/opentelemetry-instrumentation-fastapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg similarity index 93% rename from ext/opentelemetry-instrumentation-fastapi/setup.cfg rename to instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg index b700b3d724..f4c64744f8 100644 --- a/ext/opentelemetry-instrumentation-fastapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-instrumentation-fastapi +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-fastapi platforms = any license = Apache-2.0 classifiers = @@ -39,7 +39,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.12.dev0 - opentelemetry-ext-asgi == 0.12.dev0 + opentelemetry-instrumentation-asgi == 0.12.dev0 [options.entry_points] opentelemetry_instrumentor = diff --git a/ext/opentelemetry-instrumentation-fastapi/setup.py b/instrumentation/opentelemetry-instrumentation-fastapi/setup.py similarity index 100% rename from ext/opentelemetry-instrumentation-fastapi/setup.py rename to instrumentation/opentelemetry-instrumentation-fastapi/setup.py diff --git a/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py similarity index 97% rename from ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py rename to instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py index 65d608393d..57c9a5bfc7 100644 --- a/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py @@ -16,7 +16,7 @@ import fastapi from starlette.routing import Match -from opentelemetry.ext.asgi import OpenTelemetryMiddleware +from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.fastapi.version import __version__ # noqa from opentelemetry.instrumentation.instrumentor import BaseInstrumentor diff --git a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py similarity index 100% rename from ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/version.py rename to instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py diff --git a/ext/opentelemetry-ext-pyramid/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-pyramid/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-fastapi/tests/__init__.py diff --git a/ext/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py similarity index 100% rename from ext/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py rename to instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py diff --git a/ext/opentelemetry-ext-flask/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md similarity index 85% rename from ext/opentelemetry-ext-flask/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md index 334084c149..156cf9db31 100644 --- a/ext/opentelemetry-ext-flask/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-flask + ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) ## Version 0.11b0 diff --git a/ext/opentelemetry-ext-pyramid/LICENSE b/instrumentation/opentelemetry-instrumentation-flask/LICENSE similarity index 100% rename from ext/opentelemetry-ext-pyramid/LICENSE rename to instrumentation/opentelemetry-instrumentation-flask/LICENSE diff --git a/ext/opentelemetry-ext-pyramid/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-flask/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-pyramid/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-flask/MANIFEST.in diff --git a/ext/opentelemetry-ext-flask/README.rst b/instrumentation/opentelemetry-instrumentation-flask/README.rst similarity index 67% rename from ext/opentelemetry-ext-flask/README.rst rename to instrumentation/opentelemetry-instrumentation-flask/README.rst index a75a0cfbaa..f79d8fd604 100644 --- a/ext/opentelemetry-ext-flask/README.rst +++ b/instrumentation/opentelemetry-instrumentation-flask/README.rst @@ -3,8 +3,8 @@ OpenTelemetry Flask Tracing |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-flask.svg - :target: https://pypi.org/project/opentelemetry-ext-flask/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-flask.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-flask/ This library builds on the OpenTelemetry WSGI middleware to track web requests in Flask applications. @@ -14,7 +14,7 @@ Installation :: - pip install opentelemetry-ext-flask + pip install opentelemetry-instrumentation-flask Configuration ------------- @@ -34,5 +34,5 @@ will exclude requests such as ``https://site/client/123/info`` and ``https://sit References ---------- -* `OpenTelemetry Flask Tracing `_ +* `OpenTelemetry Flask Instrumentation `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg similarity index 88% rename from ext/opentelemetry-ext-flask/setup.cfg rename to instrumentation/opentelemetry-instrumentation-flask/setup.cfg index e4b978645b..281a11249d 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-flask -description = Flask tracing for OpenTelemetry (based on opentelemetry-ext-wsgi) +name = opentelemetry-instrumentation-flask +description = Flask instrumentation for OpenTelemetry long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-flask +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-flask platforms = any license = Apache-2.0 classifiers = @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-ext-wsgi == 0.12.dev0 + opentelemetry-instrumentation-wsgi == 0.12.dev0 opentelemetry-instrumentation == 0.12.dev0 opentelemetry-api == 0.12.dev0 diff --git a/ext/opentelemetry-ext-flask/setup.py b/instrumentation/opentelemetry-instrumentation-flask/setup.py similarity index 85% rename from ext/opentelemetry-ext-flask/setup.py rename to instrumentation/opentelemetry-instrumentation-flask/setup.py index 84b33c23b2..587b697a7b 100644 --- a/ext/opentelemetry-ext-flask/setup.py +++ b/instrumentation/opentelemetry-instrumentation-flask/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "flask", "version.py" + BASE_DIR, "src", "opentelemetry", "instrumentation", "flask", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: @@ -27,7 +27,7 @@ version=PACKAGE_INFO["__version__"], entry_points={ "opentelemetry_instrumentor": [ - "flask = opentelemetry.ext.flask:FlaskInstrumentor" + "flask = opentelemetry.instrumentation.flask:FlaskInstrumentor" ] }, ) diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py similarity index 95% rename from ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py rename to instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py index 464958a7e9..d1936b1cd3 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py @@ -17,7 +17,7 @@ """ This library builds on the OpenTelemetry WSGI middleware to track web requests -in Flask applications. In addition to opentelemetry-ext-wsgi, it supports +in Flask applications. In addition to opentelemetry-instrumentation-wsgi, it supports flask-specific features such as: * The Flask endpoint name is used as the Span name. @@ -30,7 +30,7 @@ .. code-block:: python from flask import Flask - from opentelemetry.ext.flask import FlaskInstrumentor + from opentelemetry.instrumentation.flask import FlaskInstrumentor app = Flask(__name__) @@ -51,9 +51,9 @@ def hello(): import flask -import opentelemetry.ext.wsgi as otel_wsgi +import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry import configuration, context, propagators, trace -from opentelemetry.ext.flask.version import __version__ +from opentelemetry.instrumentation.flask.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.util import ExcludeList, time_ns diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py similarity index 100% rename from ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py rename to instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py diff --git a/ext/opentelemetry-ext-requests/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-flask/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-requests/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-flask/tests/__init__.py diff --git a/ext/opentelemetry-ext-flask/tests/base_test.py b/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py similarity index 100% rename from ext/opentelemetry-ext-flask/tests/base_test.py rename to instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py diff --git a/ext/opentelemetry-ext-flask/tests/test_automatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_automatic.py similarity index 96% rename from ext/opentelemetry-ext-flask/tests/test_automatic.py rename to instrumentation/opentelemetry-instrumentation-flask/tests/test_automatic.py index b94c7b33d6..d081bed9ee 100644 --- a/ext/opentelemetry-ext-flask/tests/test_automatic.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_automatic.py @@ -16,7 +16,7 @@ from werkzeug.test import Client from werkzeug.wrappers import BaseResponse -from opentelemetry.ext.flask import FlaskInstrumentor +from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase diff --git a/ext/opentelemetry-ext-flask/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py similarity index 97% rename from ext/opentelemetry-ext-flask/tests/test_programmatic.py rename to instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py index 3a15979cb4..181f4e1e61 100644 --- a/ext/opentelemetry-ext-flask/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py @@ -17,7 +17,7 @@ from flask import Flask, request from opentelemetry import trace -from opentelemetry.ext.flask import FlaskInstrumentor +from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.util import ExcludeList @@ -144,7 +144,7 @@ def test_internal_error(self): self.assertEqual(span_list[0].attributes, expected_attrs) @patch( - "opentelemetry.ext.flask._excluded_urls", + "opentelemetry.instrumentation.flask._excluded_urls", ExcludeList(["http://localhost/excluded_arg/123", "excluded_noarg"]), ) def test_exclude_lists(self): diff --git a/ext/opentelemetry-ext-pyramid/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md similarity index 100% rename from ext/opentelemetry-ext-pyramid/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md diff --git a/ext/opentelemetry-ext-requests/LICENSE b/instrumentation/opentelemetry-instrumentation-pyramid/LICENSE similarity index 100% rename from ext/opentelemetry-ext-requests/LICENSE rename to instrumentation/opentelemetry-instrumentation-pyramid/LICENSE diff --git a/ext/opentelemetry-ext-requests/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-pyramid/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-requests/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-pyramid/MANIFEST.in diff --git a/ext/opentelemetry-ext-pyramid/README.rst b/instrumentation/opentelemetry-instrumentation-pyramid/README.rst similarity index 54% rename from ext/opentelemetry-ext-pyramid/README.rst rename to instrumentation/opentelemetry-instrumentation-pyramid/README.rst index 6ec38b83c9..931486773a 100644 --- a/ext/opentelemetry-ext-pyramid/README.rst +++ b/instrumentation/opentelemetry-instrumentation-pyramid/README.rst @@ -1,17 +1,17 @@ -OpenTelemetry Pyramid Integration -================================= +OpenTelemetry Pyramid Instrumentation +===================================== |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-pyramid.svg - :target: https://pypi.org/project/opentelemetry-ext-pyramid/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pyramid.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-pyramid/ Installation ------------ :: - pip install opentelemetry-ext-pyramid + pip install opentelemetry-instrumentation-pyramid Exclude lists ************* @@ -27,6 +27,6 @@ will exclude requests such as ``https://site/client/123/info`` and ``https://sit References ---------- -* `OpenTelemetry Pyramid Integration `_ +* `OpenTelemetry Pyramid Instrumentation `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-pyramid/setup.cfg b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg similarity index 85% rename from ext/opentelemetry-ext-pyramid/setup.cfg rename to instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg index 21a4910ac4..7f23f27ca5 100644 --- a/ext/opentelemetry-ext-pyramid/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-pyramid -description = OpenTelemetry Pyramid integration +name = opentelemetry-instrumentation-pyramid +description = OpenTelemetry Pyramid instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-pyramid +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-pyramid platforms = any license = Apache-2.0 classifiers = @@ -43,7 +43,7 @@ install_requires = pyramid >= 1.7 opentelemetry-instrumentation == 0.12.dev0 opentelemetry-api == 0.12.dev0 - opentelemetry-ext-wsgi == 0.12.dev0 + opentelemetry-instrumentation-wsgi == 0.12.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] @@ -56,4 +56,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - pyramid = opentelemetry.ext.pyramid:PyramidInstrumentor + pyramid = opentelemetry.instrumentation.pyramid:PyramidInstrumentor diff --git a/ext/opentelemetry-ext-wsgi/setup.py b/instrumentation/opentelemetry-instrumentation-pyramid/setup.py similarity index 89% rename from ext/opentelemetry-ext-wsgi/setup.py rename to instrumentation/opentelemetry-instrumentation-pyramid/setup.py index 886800eacf..7141a89813 100644 --- a/ext/opentelemetry-ext-wsgi/setup.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/setup.py @@ -17,7 +17,12 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "wsgi", "version.py" + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "pyramid", + "version.py", ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/__init__.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/__init__.py similarity index 85% rename from ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/__init__.py rename to instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/__init__.py index c3db25ee7c..4125453153 100644 --- a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/__init__.py @@ -28,7 +28,7 @@ .. code:: python from pyramid.config import Configurator - from opentelemetry.ext.pyramid import PyramidInstrumentor + from opentelemetry.instrumentation.pyramid import PyramidInstrumentor PyramidInstrumentor().instrument() @@ -43,7 +43,7 @@ .. code:: python from pyramid.config import Configurator - from opentelemetry.ext.pyramid import PyramidInstrumentor + from opentelemetry.instrumentation.pyramid import PyramidInstrumentor config = Configurator() PyramidInstrumentor().instrument_config(config) @@ -55,7 +55,7 @@ --------------------------------- If you use Method 2 and then set tweens for your application with the ``pyramid.tweens`` setting, -you need to add ``opentelemetry.ext.pyramid.trace_tween_factory`` explicity to the list, +you need to add ``opentelemetry.instrumentation.pyramid.trace_tween_factory`` explicity to the list, *as well as* instrumenting the config as shown above. For example: @@ -63,10 +63,10 @@ .. code:: python from pyramid.config import Configurator - from opentelemetry.ext.pyramid import PyramidInstrumentor + from opentelemetry.instrumentation.pyramid import PyramidInstrumentor settings = { - 'pyramid.tweens', 'opentelemetry.ext.pyramid.trace_tween_factory\\nyour_tween_no_1\\nyour_tween_no_2', + 'pyramid.tweens', 'opentelemetry.instrumentation.pyramid.trace_tween_factory\\nyour_tween_no_1\\nyour_tween_no_2', } config = Configurator(settings=settings) PyramidInstrumentor().instrument_config(config) @@ -86,13 +86,13 @@ from wrapt import ObjectProxy from wrapt import wrap_function_wrapper as _wrap -from opentelemetry.ext.pyramid.callbacks import ( +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.pyramid.callbacks import ( SETTING_TRACE_ENABLED, TWEEN_NAME, trace_tween_factory, ) -from opentelemetry.ext.pyramid.version import __version__ -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.pyramid.version import __version__ from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import TracerProvider, get_tracer @@ -121,7 +121,7 @@ def _traced_init(wrapped, instance, args, kwargs): kwargs["package"] = caller_package(level=3) wrapped(*args, **kwargs) - instance.include("opentelemetry.ext.pyramid.callbacks") + instance.include("opentelemetry.instrumentation.pyramid.callbacks") class PyramidInstrumentor(BaseInstrumentor): @@ -142,7 +142,7 @@ def instrument_config(self, config): Args: config: The Configurator to instrument. """ - config.include("opentelemetry.ext.pyramid.callbacks") + config.include("opentelemetry.instrumentation.pyramid.callbacks") def uninstrument_config(self, config): config.add_settings({SETTING_TRACE_ENABLED: False}) diff --git a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/callbacks.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py similarity index 94% rename from ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/callbacks.py rename to instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py index 4d3b81fdab..fe45c39e2a 100644 --- a/ext/opentelemetry-ext-pyramid/src/opentelemetry/ext/pyramid/callbacks.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py @@ -5,12 +5,12 @@ from pyramid.settings import asbool from pyramid.tweens import EXCVIEW -import opentelemetry.ext.wsgi as otel_wsgi +import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry import configuration, context, propagators, trace -from opentelemetry.ext.pyramid.version import __version__ +from opentelemetry.instrumentation.pyramid.version import __version__ from opentelemetry.util import ExcludeList, time_ns -TWEEN_NAME = "opentelemetry.ext.pyramid.trace_tween_factory" +TWEEN_NAME = "opentelemetry.instrumentation.pyramid.trace_tween_factory" SETTING_TRACE_ENABLED = "opentelemetry-pyramid.trace_enabled" _ENVIRON_STARTTIME_KEY = "opentelemetry-pyramid.starttime_key" @@ -57,7 +57,7 @@ def _before_traversal(event): enabled = environ.get(_ENVIRON_ENABLED_KEY) if enabled is None: _logger.warning( - "Opentelemetry pyramid tween 'opentelemetry.ext.pyramid.trace_tween_factory'" + "Opentelemetry pyramid tween 'opentelemetry.instrumentation.pyramid.trace_tween_factory'" "was not called. Make sure that the tween is included in 'pyramid.tweens' if" "the tween list was created manually" ) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py similarity index 100% rename from ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py rename to instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py diff --git a/ext/opentelemetry-ext-wsgi/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-wsgi/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-pyramid/tests/__init__.py diff --git a/ext/opentelemetry-ext-pyramid/tests/pyramid_base_test.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py similarity index 100% rename from ext/opentelemetry-ext-pyramid/tests/pyramid_base_test.py rename to instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py diff --git a/ext/opentelemetry-ext-pyramid/tests/test_automatic.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py similarity index 97% rename from ext/opentelemetry-ext-pyramid/tests/test_automatic.py rename to instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py index d19da3c2b3..b065e26064 100644 --- a/ext/opentelemetry-ext-pyramid/tests/test_automatic.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py @@ -14,7 +14,7 @@ from pyramid.config import Configurator -from opentelemetry.ext.pyramid import PyramidInstrumentor +from opentelemetry.instrumentation.pyramid import PyramidInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase diff --git a/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py similarity index 94% rename from ext/opentelemetry-ext-pyramid/tests/test_programmatic.py rename to instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py index d086fc37eb..add4660caa 100644 --- a/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py @@ -17,7 +17,7 @@ from pyramid.config import Configurator from opentelemetry import trace -from opentelemetry.ext.pyramid import PyramidInstrumentor +from opentelemetry.instrumentation.pyramid import PyramidInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.util import ExcludeList @@ -125,7 +125,7 @@ def test_internal_error(self): self.assertEqual(span_list[0].attributes, expected_attrs) def test_tween_list(self): - tween_list = "opentelemetry.ext.pyramid.trace_tween_factory\npyramid.tweens.excview_tween_factory" + tween_list = "opentelemetry.instrumentation.pyramid.trace_tween_factory\npyramid.tweens.excview_tween_factory" config = Configurator(settings={"pyramid.tweens": tween_list}) PyramidInstrumentor().instrument_config(config) self._common_initialization(config) @@ -146,7 +146,7 @@ def test_tween_list(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) - @patch("opentelemetry.ext.pyramid.callbacks._logger") + @patch("opentelemetry.instrumentation.pyramid.callbacks._logger") def test_warnings(self, mock_logger): tween_list = "pyramid.tweens.excview_tween_factory" config = Configurator(settings={"pyramid.tweens": tween_list}) @@ -160,7 +160,9 @@ def test_warnings(self, mock_logger): mock_logger.warning.called = False - tween_list = "opentelemetry.ext.pyramid.trace_tween_factory" + tween_list = ( + "opentelemetry.instrumentation.pyramid.trace_tween_factory" + ) config = Configurator(settings={"pyramid.tweens": tween_list}) self._common_initialization(config) @@ -170,7 +172,7 @@ def test_warnings(self, mock_logger): self.assertEqual(mock_logger.warning.called, True) @patch( - "opentelemetry.ext.pyramid.callbacks._excluded_urls", + "opentelemetry.instrumentation.pyramid.callbacks._excluded_urls", ExcludeList(["http://localhost/excluded_arg/123", "excluded_noarg"]), ) def test_exclude_lists(self): diff --git a/ext/opentelemetry-ext-requests/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md similarity index 83% rename from ext/opentelemetry-ext-requests/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md index c7bb877431..dda179d918 100644 --- a/ext/opentelemetry-ext-requests/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-requests + ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) + ## 0.7b1 Released 2020-05-12 diff --git a/ext/opentelemetry-ext-wsgi/LICENSE b/instrumentation/opentelemetry-instrumentation-requests/LICENSE similarity index 100% rename from ext/opentelemetry-ext-wsgi/LICENSE rename to instrumentation/opentelemetry-instrumentation-requests/LICENSE diff --git a/ext/opentelemetry-ext-wsgi/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-requests/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-wsgi/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-requests/MANIFEST.in diff --git a/instrumentation/opentelemetry-instrumentation-requests/README.rst b/instrumentation/opentelemetry-instrumentation-requests/README.rst new file mode 100644 index 0000000000..d4944d3526 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-requests/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry Requests Instrumentation +====================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-requests.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-requests/ + +This library allows tracing HTTP requests made by the +`requests `_ library. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-requests + +References +---------- + +* `OpenTelemetry requests Instrumentation `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-requests/setup.cfg b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg similarity index 87% rename from ext/opentelemetry-ext-requests/setup.cfg rename to instrumentation/opentelemetry-instrumentation-requests/setup.cfg index 52d6f96739..77205b5816 100644 --- a/ext/opentelemetry-ext-requests/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-requests -description = OpenTelemetry requests integration +name = opentelemetry-instrumentation-requests +description = OpenTelemetry requests instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-requests +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-requests platforms = any license = Apache-2.0 classifiers = @@ -54,4 +54,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - requests = opentelemetry.ext.requests:RequestsInstrumentor + requests = opentelemetry.instrumentation.requests:RequestsInstrumentor diff --git a/ext/opentelemetry-ext-requests/setup.py b/instrumentation/opentelemetry-instrumentation-requests/setup.py similarity index 88% rename from ext/opentelemetry-ext-requests/setup.py rename to instrumentation/opentelemetry-instrumentation-requests/setup.py index a71a8d44b5..237fef583b 100644 --- a/ext/opentelemetry-ext-requests/setup.py +++ b/instrumentation/opentelemetry-instrumentation-requests/setup.py @@ -17,7 +17,12 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "requests", "version.py" + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "requests", + "version.py", ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py similarity index 97% rename from ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py rename to instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index a4db0591c8..a12f05999e 100644 --- a/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -22,11 +22,11 @@ .. code-block:: python import requests - import opentelemetry.ext.requests + import opentelemetry.instrumentation.requests # You can optionally pass a custom TracerProvider to RequestInstrumentor.instrument() - opentelemetry.ext.requests.RequestsInstrumentor().instrument() + opentelemetry.instrumentation.requests.RequestsInstrumentor().instrument() response = requests.get(url="https://www.example.org/") Limitations @@ -51,8 +51,8 @@ from requests.sessions import Session from opentelemetry import context, propagators -from opentelemetry.ext.requests.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.requests.version import __version__ from opentelemetry.instrumentation.utils import http_status_to_canonical_code from opentelemetry.trace import SpanKind, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode diff --git a/ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py similarity index 100% rename from ext/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py rename to instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py diff --git a/ext/opentelemetry-instrumentation-aiopg/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/tests/__init__.py similarity index 100% rename from ext/opentelemetry-instrumentation-aiopg/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-requests/tests/__init__.py diff --git a/ext/opentelemetry-ext-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py similarity index 97% rename from ext/opentelemetry-ext-requests/tests/test_requests_integration.py rename to instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py index 75a63f3cf8..afec0a88d0 100644 --- a/ext/opentelemetry-ext-requests/tests/test_requests_integration.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py @@ -17,9 +17,9 @@ import httpretty import requests -import opentelemetry.ext.requests +import opentelemetry.instrumentation.requests from opentelemetry import context, propagators, trace -from opentelemetry.ext.requests import RequestsInstrumentor +from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.sdk import resources from opentelemetry.test.mock_httptextformat import MockHTTPTextFormat from opentelemetry.test.test_base import TestBase @@ -68,7 +68,9 @@ def test_basic(self): span.status.canonical_code, trace.status.StatusCanonicalCode.OK ) - self.check_span_instrumentation_info(span, opentelemetry.ext.requests) + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.requests + ) def test_not_foundbasic(self): url_404 = "http://httpbin.org/status/404" diff --git a/ext/opentelemetry-instrumentation-starlette/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-starlette/CHANGELOG.md similarity index 100% rename from ext/opentelemetry-instrumentation-starlette/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-starlette/CHANGELOG.md diff --git a/ext/opentelemetry-instrumentation-starlette/README.rst b/instrumentation/opentelemetry-instrumentation-starlette/README.rst similarity index 100% rename from ext/opentelemetry-instrumentation-starlette/README.rst rename to instrumentation/opentelemetry-instrumentation-starlette/README.rst diff --git a/ext/opentelemetry-instrumentation-starlette/setup.cfg b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg similarity index 93% rename from ext/opentelemetry-instrumentation-starlette/setup.cfg rename to instrumentation/opentelemetry-instrumentation-starlette/setup.cfg index f1613a8cd8..905f4992e4 100644 --- a/ext/opentelemetry-instrumentation-starlette/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-instrumentation-starlette +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-starlette platforms = any license = Apache-2.0 classifiers = @@ -39,7 +39,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.12.dev0 - opentelemetry-ext-asgi == 0.12.dev0 + opentelemetry-instrumentation-asgi == 0.12.dev0 [options.entry_points] opentelemetry_instrumentor = diff --git a/ext/opentelemetry-instrumentation-starlette/setup.py b/instrumentation/opentelemetry-instrumentation-starlette/setup.py similarity index 100% rename from ext/opentelemetry-instrumentation-starlette/setup.py rename to instrumentation/opentelemetry-instrumentation-starlette/setup.py diff --git a/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py similarity index 97% rename from ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py rename to instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py index b8763bba05..f469447f3a 100644 --- a/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py @@ -16,7 +16,7 @@ from starlette import applications from starlette.routing import Match -from opentelemetry.ext.asgi import OpenTelemetryMiddleware +from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.starlette.version import __version__ # noqa diff --git a/ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py similarity index 100% rename from ext/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py rename to instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py diff --git a/ext/opentelemetry-instrumentation-fastapi/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-starlette/tests/__init__.py similarity index 100% rename from ext/opentelemetry-instrumentation-fastapi/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-starlette/tests/__init__.py diff --git a/ext/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py similarity index 100% rename from ext/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py rename to instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py diff --git a/ext/opentelemetry-ext-wsgi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md similarity index 82% rename from ext/opentelemetry-ext-wsgi/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md index 739ee7b0cc..6f1085b8bb 100644 --- a/ext/opentelemetry-ext-wsgi/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-wsgi + ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) + ## Version 0.11b0 Released 2020-07-28 diff --git a/ext/opentelemetry-instrumentation-aiopg/LICENSE b/instrumentation/opentelemetry-instrumentation-wsgi/LICENSE similarity index 100% rename from ext/opentelemetry-instrumentation-aiopg/LICENSE rename to instrumentation/opentelemetry-instrumentation-wsgi/LICENSE diff --git a/ext/opentelemetry-instrumentation-aiopg/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-wsgi/MANIFEST.in similarity index 100% rename from ext/opentelemetry-instrumentation-aiopg/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-wsgi/MANIFEST.in diff --git a/ext/opentelemetry-ext-wsgi/README.rst b/instrumentation/opentelemetry-instrumentation-wsgi/README.rst similarity index 63% rename from ext/opentelemetry-ext-wsgi/README.rst rename to instrumentation/opentelemetry-instrumentation-wsgi/README.rst index 12ee81cb09..ac39dac0c7 100644 --- a/ext/opentelemetry-ext-wsgi/README.rst +++ b/instrumentation/opentelemetry-instrumentation-wsgi/README.rst @@ -3,8 +3,8 @@ OpenTelemetry WSGI Middleware |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-wsgi.svg - :target: https://pypi.org/project/opentelemetry-ext-wsgi/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-wsgi.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-wsgi/ This library provides a WSGI middleware that can be used on any WSGI framework @@ -15,12 +15,12 @@ Installation :: - pip install opentelemetry-ext-wsgi + pip install opentelemetry-instrumentation-wsgi References ---------- -* `OpenTelemetry WSGI Middleware `_ +* `OpenTelemetry WSGI Middleware `_ * `OpenTelemetry Project `_ * `WSGI `_ diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg similarity index 94% rename from ext/opentelemetry-ext-wsgi/setup.cfg rename to instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg index 97445a847c..b48d61dc84 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-wsgi +name = opentelemetry-instrumentation-wsgi description = WSGI Middleware for OpenTelemetry long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-wsgi +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-wsgi platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-pyramid/setup.py b/instrumentation/opentelemetry-instrumentation-wsgi/setup.py similarity index 91% rename from ext/opentelemetry-ext-pyramid/setup.py rename to instrumentation/opentelemetry-instrumentation-wsgi/setup.py index 175db54ed5..fb4ffa7437 100644 --- a/ext/opentelemetry-ext-pyramid/setup.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "pyramid", "version.py" + BASE_DIR, "src", "opentelemetry", "instrumentation", "wsgi", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py similarity index 97% rename from ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py rename to instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index 709de24edf..8a2d0bec08 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -21,7 +21,7 @@ .. code-block:: python from flask import Flask - from opentelemetry.ext.wsgi import OpenTelemetryMiddleware + from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware app = Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) @@ -42,7 +42,7 @@ def hello(): .. code-block:: python import os - from opentelemetry.ext.wsgi import OpenTelemetryMiddleware + from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware from django.core.wsgi import get_wsgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') @@ -59,8 +59,8 @@ def hello(): import wsgiref.util as wsgiref_util from opentelemetry import context, propagators, trace -from opentelemetry.ext.wsgi.version import __version__ from opentelemetry.instrumentation.utils import http_status_to_canonical_code +from opentelemetry.instrumentation.wsgi.version import __version__ from opentelemetry.trace.status import Status, StatusCanonicalCode _HTTP_VERSION_PREFIX = "HTTP/" diff --git a/ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py similarity index 100% rename from ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py rename to instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py diff --git a/ext/opentelemetry-instrumentation-starlette/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/__init__.py similarity index 100% rename from ext/opentelemetry-instrumentation-starlette/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-wsgi/tests/__init__.py diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py similarity index 99% rename from ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py rename to instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index 2a2d518513..5734027b0a 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -18,7 +18,7 @@ import wsgiref.util as wsgiref_util from urllib.parse import urlsplit -import opentelemetry.ext.wsgi as otel_wsgi +import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry import trace as trace_api from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace.status import StatusCanonicalCode diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 22b7b880b4..bd2e3b8573 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -275,7 +275,7 @@ def get_meter( This should *not* be the name of the module that is instrumented but the name of the module doing the instrumentation. E.g., instead of ``"requests"``, use - ``"opentelemetry.ext.requests"``. + ``"opentelemetry.instrumentation.requests"``. instrumenting_library_version: Optional. The version string of the instrumenting library. Usually this should be the same as diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 196cf3390c..ffda0a15e7 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -242,7 +242,7 @@ def get_tracer( This should *not* be the name of the module that is instrumented but the name of the module doing the instrumentation. E.g., instead of ``"requests"``, use - ``"opentelemetry.ext.requests"``. + ``"opentelemetry.instrumentation.requests"``. instrumenting_library_version: Optional. The version string of the instrumenting library. Usually this should be the same as diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index f624a2e4dd..857ceb84fe 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -25,16 +25,16 @@ # target library to desired instrumentor path/versioned package name instrumentations = { - "asgi": "opentelemetry-ext-asgi>=0.11b0", + "asgi": "opentelemetry-instrumentation-asgi>=0.11b0", "asyncpg": "opentelemetry-ext-asyncpg>=0.11b0", "boto": "opentelemetry-ext-boto>=0.11b0", "botocore": "opentelemetry-ext-botocore>=0.11b0", "celery": "opentelemetry-ext-celery>=0.11b0", "dbapi": "opentelemetry-ext-dbapi>=0.8b0", - "django": "opentelemetry-ext-django>=0.8b0", + "django": "opentelemetry-instrumentation-django>=0.8b0", "elasticsearch": "opentelemetry-ext-elasticsearch>=0.11b0", "fastapi": "opentelemetry-instrumentation-fastapi>=0.11b0", - "flask": "opentelemetry-ext-flask>=0.8b0", + "flask": "opentelemetry-instrumentation-flask>=0.8b0", "grpc": "opentelemetry-ext-grpc>=0.8b0", "jinja2": "opentelemetry-ext-jinja2>=0.8b0", "mysql": "opentelemetry-ext-mysql>=0.8b0", @@ -42,27 +42,27 @@ "pymemcache": "opentelemetry-ext-pymemcache>=0.11b0", "pymongo": "opentelemetry-ext-pymongo>=0.8b0", "pymysql": "opentelemetry-ext-pymysql>=0.8b0", - "pyramid": "opentelemetry-ext-pyramid>=0.11b0", + "pyramid": "opentelemetry-instrumentation-pyramid>=0.11b0", "redis": "opentelemetry-ext-redis>=0.8b0", - "requests": "opentelemetry-ext-requests>=0.8b0", + "requests": "opentelemetry-instrumentation-requests>=0.8b0", "sqlalchemy": "opentelemetry-ext-sqlalchemy>=0.8b0", "sqlite3": "opentelemetry-ext-sqlite3>=0.11b0", "starlette": "opentelemetry-instrumentation-starlette>=0.11b0", - "wsgi": "opentelemetry-ext-wsgi>=0.8b0", + "wsgi": "opentelemetry-instrumentation-wsgi>=0.8b0", } # relevant instrumentors and tracers to uninstall and check for conflicts for target libraries libraries = { - "asgi": ("opentelemetry-ext-asgi",), + "asgi": ("opentelemetry-instrumentation-asgi",), "asyncpg": ("opentelemetry-ext-asyncpg",), "boto": ("opentelemetry-ext-boto",), "botocore": ("opentelemetry-ext-botocore",), "celery": ("opentelemetry-ext-celery",), "dbapi": ("opentelemetry-ext-dbapi",), - "django": ("opentelemetry-ext-django",), + "django": ("opentelemetry-instrumentation-django",), "elasticsearch": ("opentelemetry-ext-elasticsearch",), "fastapi": ("opentelemetry-instrumentation-fastapi",), - "flask": ("opentelemetry-ext-flask",), + "flask": ("opentelemetry-instrumentation-flask",), "grpc": ("opentelemetry-ext-grpc",), "jinja2": ("opentelemetry-ext-jinja2",), "mysql": ("opentelemetry-ext-mysql",), @@ -70,13 +70,13 @@ "pymemcache": ("opentelemetry-ext-pymemcache",), "pymongo": ("opentelemetry-ext-pymongo",), "pymysql": ("opentelemetry-ext-pymysql",), - "pyramid": ("opentelemetry-ext-pyramid",), + "pyramid": ("opentelemetry-instrumentation-pyramid",), "redis": ("opentelemetry-ext-redis",), - "requests": ("opentelemetry-ext-requests",), + "requests": ("opentelemetry-instrumentation-requests",), "sqlalchemy": ("opentelemetry-ext-sqlalchemy",), "sqlite3": ("opentelemetry-ext-sqlite3",), "starlette": ("opentelemetry-instrumentation-starlette",), - "wsgi": ("opentelemetry-ext-wsgi",), + "wsgi": ("opentelemetry-instrumentation-wsgi",), } @@ -87,10 +87,10 @@ def _install_package(library, instrumentation): OpenTelemetry auto-instrumentation packages often have traced libraries - as instrumentation dependency (e.g. flask for opentelemetry-ext-flask), - so using -I on library could cause likely undesired Flask upgrade. - Using --no-dependencies alone would leave potential for nonfunctional - installations. + as instrumentation dependency (e.g. flask for + opentelemetry-instrumentation-flask), so using -I on library could cause + likely undesired Flask upgrade.Using --no-dependencies alone would leave + potential for nonfunctional installations. """ pip_list = _sys_pip_freeze() for package in libraries[library]: @@ -159,7 +159,7 @@ def _pip_check(): Clean check reported as: 'No broken requirements found.' Dependency conflicts are reported as: - 'opentelemetry-ext-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' + 'opentelemetry-instrumentation-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' To not be too restrictive, we'll only check for relevant packages. """ check_pipe = subprocess.Popen( diff --git a/scripts/build.sh b/scripts/build.sh index 0d481b8165..056998ebc1 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ exporter/*/ ext/*/ ; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ exporter/*/ ext/*/ instrumentation/*/ ; do ( echo "building $d" cd "$d" diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 5cf393e7e6..830381225f 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -32,20 +32,20 @@ coverage erase cov opentelemetry-api cov opentelemetry-sdk cov exporter/opentelemetry-exporter-datadog -cov ext/opentelemetry-ext-flask -cov ext/opentelemetry-ext-requests +cov instrumentation/opentelemetry-instrumentation-flask +cov instrumentation/opentelemetry-instrumentation-requests cov exporter/opentelemetry-exporter-jaeger cov ext/opentelemetry-ext-opentracing-shim -cov ext/opentelemetry-ext-wsgi +cov instrumentation/opentelemetry-instrumentation-wsgi cov exporter/opentelemetry-exporter-zipkin cov docs/examples/opentelemetry-example-app # aiohttp is only supported on Python 3.5+. if [ ${PYTHON_VERSION_INFO[1]} -gt 4 ]; then - cov ext/opentelemetry-ext-aiohttp-client + cov instrumentation/opentelemetry-instrumentation-aiohttp-client # ext-asgi is only supported on Python 3.5+. if [ ${PYTHON_VERSION_INFO[1]} -gt 4 ]; then - cov ext/opentelemetry-ext-asgi + cov instrumentation/opentelemetry-instrumentation-asgi fi coverage report --show-missing diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py index 5961a3b039..f2fb14f206 100644 --- a/tests/w3c_tracecontext_validation_server.py +++ b/tests/w3c_tracecontext_validation_server.py @@ -24,8 +24,8 @@ import requests from opentelemetry import trace -from opentelemetry.ext.requests import RequestsInstrumentor -from opentelemetry.ext.wsgi import OpenTelemetryMiddleware +from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, diff --git a/tox.ini b/tox.ini index a018ec7a89..6dab08f703 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,7 @@ envlist = py3{4,5,6,7,8}-test-instrumentation-example-app pypy3-test-instrumentation-example-app - ; opentelemetry-ext-aiohttp-client + ; opentelemetry-instrumentation-aiohttp-client py3{5,6,7,8}-test-instrumentation-aiohttp-client pypy3-test-instrumentation-aiohttp-client @@ -40,7 +40,7 @@ envlist = py3{6,7,8}-test-instrumentation-botocore pypy3-test-instrumentation-botocore - ; opentelemetry-ext-django + ; opentelemetry-instrumentation-django py3{4,5,6,7,8}-test-instrumentation-django pypy3-test-instrumentation-django @@ -61,11 +61,11 @@ envlist = py3{6,7,8}-test-instrumentation-fastapi pypy3-test-instrumentation-fastapi - ; opentelemetry-ext-flask + ; opentelemetry-instrumentation-flask py3{4,5,6,7,8}-test-instrumentation-flask pypy3-test-instrumentation-flask - ; opentelemetry-ext-requests + ; opentelemetry-instrumentation-requests py3{4,5,6,7,8}-test-instrumentation-requests pypy3-test-instrumentation-requests @@ -117,11 +117,11 @@ envlist = py3{4,5,6,7,8}-test-instrumentation-pymysql pypy3-test-instrumentation-pymysql - ; opentelemetry-ext-pyramid + ; opentelemetry-instrumentation-pyramid py3{4,5,6,7,8}-test-instrumentation-pyramid pypy3-test-instrumentation-pyramid - ; opentelemetry-ext-asgi + ; opentelemetry-instrumentation-asgi py3{5,6,7,8}-test-instrumentation-asgi pypy3-test-instrumentation-asgi @@ -133,7 +133,7 @@ envlist = py3{4,5,6,7,8}-test-instrumentation-sqlite3 pypy3-test-instrumentation-sqlite3 - ; opentelemetry-ext-wsgi + ; opentelemetry-instrumentation-wsgi py3{4,5,6,7,8}-test-instrumentation-wsgi pypy3-test-instrumentation-wsgi @@ -197,19 +197,19 @@ changedir = test-core-getting-started: docs/getting_started/tests test-core-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests - test-instrumentation-aiohttp-client: ext/opentelemetry-ext-aiohttp-client/tests - test-instrumentation-aiopg: ext/opentelemetry-instrumentation-aiopg/tests - test-instrumentation-asgi: ext/opentelemetry-ext-asgi/tests + test-instrumentation-aiohttp-client: instrumentation/opentelemetry-instrumentation-aiohttp-client/tests + test-instrumentation-aiopg: instrumentation/opentelemetry-instrumentation-aiopg/tests + test-instrumentation-asgi: instrumentation/opentelemetry-instrumentation-asgi/tests test-instrumentation-asyncpg: ext/opentelemetry-ext-asyncpg/tests test-instrumentation-boto: ext/opentelemetry-ext-boto/tests test-instrumentation-botocore: ext/opentelemetry-ext-botocore/tests test-instrumentation-celery: ext/opentelemetry-ext-celery/tests test-instrumentation-dbapi: ext/opentelemetry-ext-dbapi/tests - test-instrumentation-django: ext/opentelemetry-ext-django/tests + test-instrumentation-django: instrumentation/opentelemetry-instrumentation-django/tests test-instrumentation-example-app: docs/examples/opentelemetry-example-app/tests test-instrumentation-elasticsearch{2,5,6,7}: ext/opentelemetry-ext-elasticsearch/tests - test-instrumentation-fastapi: ext/opentelemetry-instrumentation-fastapi/tests - test-instrumentation-flask: ext/opentelemetry-ext-flask/tests + test-instrumentation-fastapi: instrumentation/opentelemetry-instrumentation-fastapi/tests + test-instrumentation-flask: instrumentation/opentelemetry-instrumentation-flask/tests test-instrumentation-grpc: ext/opentelemetry-ext-grpc/tests test-instrumentation-jinja2: ext/opentelemetry-ext-jinja2/tests test-instrumentation-mysql: ext/opentelemetry-ext-mysql/tests @@ -217,14 +217,14 @@ changedir = test-instrumentation-pymemcache: ext/opentelemetry-ext-pymemcache/tests test-instrumentation-pymongo: ext/opentelemetry-ext-pymongo/tests test-instrumentation-pymysql: ext/opentelemetry-ext-pymysql/tests - test-instrumentation-pyramid: ext/opentelemetry-ext-pyramid/tests + test-instrumentation-pyramid: instrumentation/opentelemetry-instrumentation-pyramid/tests test-instrumentation-redis: ext/opentelemetry-ext-redis/tests - test-instrumentation-requests: ext/opentelemetry-ext-requests/tests + test-instrumentation-requests: instrumentation/opentelemetry-instrumentation-requests/tests test-instrumentation-sqlalchemy: ext/opentelemetry-ext-sqlalchemy/tests test-instrumentation-sqlite3: ext/opentelemetry-ext-sqlite3/tests - test-instrumentation-starlette: ext/opentelemetry-instrumentation-starlette/tests + test-instrumentation-starlette: instrumentation/opentelemetry-instrumentation-starlette/tests test-instrumentation-system-metrics: ext/opentelemetry-ext-system-metrics/tests - test-instrumentation-wsgi: ext/opentelemetry-ext-wsgi/tests + test-instrumentation-wsgi: instrumentation/opentelemetry-instrumentation-wsgi/tests test-exporter-jaeger: exporter/opentelemetry-exporter-jaeger/tests test-exporter-datadog: exporter/opentelemetry-exporter-datadog/tests @@ -243,31 +243,31 @@ commands_pre = test-core-proto: pip install {toxinidir}/opentelemetry-proto ext,instrumentation: pip install {toxinidir}/opentelemetry-instrumentation - example-app: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/ext/opentelemetry-ext-requests {toxinidir}/ext/opentelemetry-ext-wsgi {toxinidir}/ext/opentelemetry-ext-flask {toxinidir}/docs/examples/opentelemetry-example-app + example-app: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi {toxinidir}/instrumentation/opentelemetry-instrumentation-flask {toxinidir}/docs/examples/opentelemetry-example-app - getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/ext/opentelemetry-ext-requests -e {toxinidir}/ext/opentelemetry-ext-wsgi -e {toxinidir}/ext/opentelemetry-ext-flask + getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/instrumentation/opentelemetry-instrumentation-requests -e {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/instrumentation/opentelemetry-instrumentation-flask celery: pip install {toxinidir}/ext/opentelemetry-ext-celery[test] grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc[test] - wsgi,flask,django,pyramid: pip install {toxinidir}/ext/opentelemetry-ext-wsgi - asgi,starlette,fastapi: pip install {toxinidir}/ext/opentelemetry-ext-asgi + wsgi,flask,django,pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi + asgi,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi asyncpg: pip install {toxinidir}/ext/opentelemetry-ext-asyncpg boto: pip install {toxinidir}/ext/opentelemetry-ext-botocore[test] boto: pip install {toxinidir}/ext/opentelemetry-ext-boto[test] - flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] + flask: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test] botocore: pip install {toxinidir}/ext/opentelemetry-ext-botocore[test] dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi[test] - django: pip install {toxinidir}/ext/opentelemetry-ext-django[test] + django: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-django[test] - fastapi: pip install {toxinidir}/ext/opentelemetry-instrumentation-fastapi[test] + fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi[test] mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-mysql[test] @@ -286,21 +286,21 @@ commands_pre = pymysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-pymysql[test] - pyramid: pip install {toxinidir}/ext/opentelemetry-ext-pyramid[test] + pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid[test] sqlite3: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-sqlite3[test] redis: pip install {toxinidir}/ext/opentelemetry-ext-redis[test] - requests: pip install {toxinidir}/ext/opentelemetry-ext-requests[test] + requests: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-requests[test] - starlette: pip install {toxinidir}/ext/opentelemetry-instrumentation-starlette[test] + starlette: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette[test] jinja2: pip install {toxinidir}/ext/opentelemetry-ext-jinja2[test] - aiohttp-client: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-aiohttp-client + aiohttp-client: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client - aiopg: pip install {toxinidir}/ext/opentelemetry-ext-dbapi pip install {toxinidir}/ext/opentelemetry-instrumentation-aiopg[test] + aiopg: pip install {toxinidir}/ext/opentelemetry-ext-dbapi pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg[test] jaeger: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger @@ -383,9 +383,9 @@ commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ - -e {toxinidir}/ext/opentelemetry-ext-requests \ - -e {toxinidir}/ext/opentelemetry-ext-wsgi \ - -e {toxinidir}/ext/opentelemetry-ext-flask + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-requests \ + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi \ + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-flask commands = {toxinidir}/scripts/tracecontext-integration-test.sh @@ -420,7 +420,7 @@ commands_pre = -e {toxinidir}/ext/opentelemetry-ext-pymongo \ -e {toxinidir}/ext/opentelemetry-ext-pymysql \ -e {toxinidir}/ext/opentelemetry-ext-sqlalchemy \ - -e {toxinidir}/ext/opentelemetry-instrumentation-aiopg \ + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg \ -e {toxinidir}/ext/opentelemetry-ext-redis \ -e {toxinidir}/ext/opentelemetry-ext-system-metrics \ -e {toxinidir}/exporter/opentelemetry-exporter-opencensus From 6e34dfe2e5fdddb1f9a08d2409b56fb3ba96bf47 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 3 Aug 2020 17:48:44 -0700 Subject: [PATCH 0487/1517] Rename db framework packages from "ext" to "instrumentation" (#966) --- docs/ext/asyncpg/asyncpg.rst | 10 --- docs/ext/dbapi/dbapi.rst | 7 -- docs/ext/mysql/mysql.rst | 7 -- docs/ext/psycopg2/psycopg2.rst | 7 -- docs/ext/pymemcache/pymemcache.rst | 7 -- docs/ext/pymongo/pymongo.rst | 7 -- docs/ext/pymysql/pymysql.rst | 7 -- docs/ext/sqlite3/sqlite3.rst | 7 -- docs/instrumentation/asyncpg/asyncpg.rst | 10 +++ docs/instrumentation/dbapi/dbapi.rst | 7 ++ docs/instrumentation/mysql/mysql.rst | 7 ++ docs/instrumentation/psycopg2/psycopg2.rst | 7 ++ .../instrumentation/pymemcache/pymemcache.rst | 7 ++ docs/instrumentation/pymongo/pymongo.rst | 7 ++ docs/instrumentation/pymysql/pymysql.rst | 7 ++ docs/{ext => instrumentation}/redis/redis.rst | 2 +- .../sqlalchemy/sqlalchemy.rst | 2 +- docs/instrumentation/sqlite3/sqlite3.rst | 7 ++ eachdist.ini | 2 +- .../exporter/datadog/exporter.py | 16 ++-- .../tests/test_datadog_exporter.py | 14 ++-- ext/opentelemetry-ext-asyncpg/README.rst | 23 ------ ext/opentelemetry-ext-dbapi/README.rst | 21 ----- .../tests/asyncpg/test_asyncpg_functional.py | 2 +- .../tests/mysql/test_mysql_functional.py | 2 +- .../tests/postgres/test_psycopg_functional.py | 2 +- .../tests/pymongo/test_pymongo_functional.py | 2 +- .../tests/pymysql/test_pymysql_functional.py | 2 +- .../tests/redis/test_redis_functional.py | 2 +- .../tests/sqlalchemy_tests/mixins.py | 4 +- .../tests/sqlalchemy_tests/test_instrument.py | 2 +- .../tests/sqlalchemy_tests/test_mysql.py | 8 +- .../tests/sqlalchemy_tests/test_postgres.py | 8 +- .../tests/sqlalchemy_tests/test_sqlite.py | 2 +- ext/opentelemetry-ext-mysql/README.rst | 25 ------ ext/opentelemetry-ext-psycopg2/README.rst | 20 ----- ext/opentelemetry-ext-pymemcache/CHANGELOG.md | 9 --- ext/opentelemetry-ext-pymemcache/README.rst | 20 ----- ext/opentelemetry-ext-pymongo/README.rst | 21 ----- ext/opentelemetry-ext-pymongo/setup.py | 26 ------- ext/opentelemetry-ext-pymysql/CHANGELOG.md | 9 --- ext/opentelemetry-ext-pymysql/README.rst | 20 ----- ext/opentelemetry-ext-pymysql/setup.py | 26 ------- ext/opentelemetry-ext-redis/CHANGELOG.md | 9 --- ext/opentelemetry-ext-redis/README.rst | 23 ------ ext/opentelemetry-ext-redis/setup.py | 26 ------- ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md | 9 --- ext/opentelemetry-ext-sqlalchemy/README.rst | 24 ------ ext/opentelemetry-ext-sqlalchemy/setup.py | 26 ------- ext/opentelemetry-ext-sqlite3/CHANGELOG.md | 9 --- ext/opentelemetry-ext-sqlite3/README.rst | 21 ----- ext/opentelemetry-ext-sqlite3/setup.py | 26 ------- .../setup.cfg | 2 +- .../aiopg/aiopg_integration.py | 5 +- .../CHANGELOG.md | 3 + .../README.rst | 23 ++++++ .../setup.cfg | 6 +- .../setup.py | 7 +- .../instrumentation}/asyncpg/__init__.py | 4 +- .../instrumentation}/asyncpg/version.py | 0 .../tests/__init__.py | 0 .../tests/test_asyncpg_wrapper.py | 2 +- .../CHANGELOG.md | 3 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 21 +++++ .../setup.cfg | 6 +- .../setup.py | 2 +- .../instrumentation}/dbapi/__init__.py | 4 +- .../instrumentation}/dbapi/version.py | 0 .../tests/__init__.py | 0 .../tests/test_dbapi_integration.py | 6 +- .../CHANGELOG.md | 3 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 25 ++++++ .../setup.cfg | 10 +-- .../setup.py | 2 +- .../instrumentation}/mysql/__init__.py | 6 +- .../instrumentation}/mysql/version.py | 0 .../tests/__init__.py | 0 .../tests/test_mysql_integration.py | 8 +- .../CHANGELOG.md | 3 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 20 +++++ .../setup.cfg | 10 +-- .../setup.py | 31 ++++++++ .../instrumentation}/psycopg2/__init__.py | 6 +- .../instrumentation}/psycopg2/version.py | 0 .../tests/__init__.py | 0 .../tests/test_psycopg2_integration.py | 8 +- .../CHANGELOG.md | 12 +++ .../LICENSE | 0 .../MANIFEST.IN | 0 .../README.rst | 20 +++++ .../setup.cfg | 8 +- .../setup.py | 7 +- .../instrumentation}/pymemcache/__init__.py | 4 +- .../instrumentation}/pymemcache/version.py | 0 .../tests/__init__.py | 0 .../tests/test_pymemcache.py | 2 +- .../tests/utils.py | 0 .../CHANGELOG.md | 3 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 21 +++++ .../setup.cfg | 8 +- .../setup.py | 31 ++++++++ .../instrumentation}/pymongo/__init__.py | 4 +- .../instrumentation}/pymongo/version.py | 0 .../tests/__init__.py | 0 .../tests/test_pymongo.py | 5 +- .../CHANGELOG.md | 12 +++ .../README.rst | 20 +++++ .../setup.cfg | 10 +-- .../setup.py | 31 ++++++++ .../instrumentation}/pymysql/__init__.py | 6 +- .../instrumentation}/pymysql/version.py | 0 .../tests/__init__.py | 0 .../tests/test_pymysql_integration.py | 8 +- .../CHANGELOG.md | 12 +++ .../MANIFEST.in | 0 .../README.rst | 23 ++++++ .../setup.cfg | 8 +- .../setup.py | 2 +- .../instrumentation}/redis/__init__.py | 8 +- .../instrumentation}/redis/util.py | 0 .../instrumentation}/redis/version.py | 0 .../tests/__init__.py | 0 .../tests/test_redis.py | 2 +- .../CHANGELOG.md | 12 +++ .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 24 ++++++ .../setup.cfg | 8 +- .../setup.py | 31 ++++++++ .../instrumentation}/sqlalchemy/__init__.py | 6 +- .../instrumentation}/sqlalchemy/engine.py | 2 +- .../instrumentation}/sqlalchemy/version.py | 0 .../tests/__init__.py | 0 .../tests/test_sqlalchemy.py | 2 +- .../CHANGELOG.md | 12 +++ .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 21 +++++ .../setup.cfg | 10 +-- .../setup.py | 31 ++++++++ .../instrumentation}/sqlite3/__init__.py | 6 +- .../instrumentation}/sqlite3/version.py | 0 .../tests/__init__.py | 0 .../tests/test_sqlite3.py | 2 +- .../instrumentation/bootstrap.py | 40 +++++----- tox.ini | 78 +++++++++---------- 154 files changed, 725 insertions(+), 636 deletions(-) delete mode 100644 docs/ext/asyncpg/asyncpg.rst delete mode 100644 docs/ext/dbapi/dbapi.rst delete mode 100644 docs/ext/mysql/mysql.rst delete mode 100644 docs/ext/psycopg2/psycopg2.rst delete mode 100644 docs/ext/pymemcache/pymemcache.rst delete mode 100644 docs/ext/pymongo/pymongo.rst delete mode 100644 docs/ext/pymysql/pymysql.rst delete mode 100644 docs/ext/sqlite3/sqlite3.rst create mode 100644 docs/instrumentation/asyncpg/asyncpg.rst create mode 100644 docs/instrumentation/dbapi/dbapi.rst create mode 100644 docs/instrumentation/mysql/mysql.rst create mode 100644 docs/instrumentation/psycopg2/psycopg2.rst create mode 100644 docs/instrumentation/pymemcache/pymemcache.rst create mode 100644 docs/instrumentation/pymongo/pymongo.rst create mode 100644 docs/instrumentation/pymysql/pymysql.rst rename docs/{ext => instrumentation}/redis/redis.rst (71%) rename docs/{ext => instrumentation}/sqlalchemy/sqlalchemy.rst (71%) create mode 100644 docs/instrumentation/sqlite3/sqlite3.rst delete mode 100644 ext/opentelemetry-ext-asyncpg/README.rst delete mode 100644 ext/opentelemetry-ext-dbapi/README.rst delete mode 100644 ext/opentelemetry-ext-mysql/README.rst delete mode 100644 ext/opentelemetry-ext-psycopg2/README.rst delete mode 100644 ext/opentelemetry-ext-pymemcache/CHANGELOG.md delete mode 100644 ext/opentelemetry-ext-pymemcache/README.rst delete mode 100644 ext/opentelemetry-ext-pymongo/README.rst delete mode 100644 ext/opentelemetry-ext-pymongo/setup.py delete mode 100644 ext/opentelemetry-ext-pymysql/CHANGELOG.md delete mode 100644 ext/opentelemetry-ext-pymysql/README.rst delete mode 100644 ext/opentelemetry-ext-pymysql/setup.py delete mode 100644 ext/opentelemetry-ext-redis/CHANGELOG.md delete mode 100644 ext/opentelemetry-ext-redis/README.rst delete mode 100644 ext/opentelemetry-ext-redis/setup.py delete mode 100644 ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md delete mode 100644 ext/opentelemetry-ext-sqlalchemy/README.rst delete mode 100644 ext/opentelemetry-ext-sqlalchemy/setup.py delete mode 100644 ext/opentelemetry-ext-sqlite3/CHANGELOG.md delete mode 100644 ext/opentelemetry-ext-sqlite3/README.rst delete mode 100644 ext/opentelemetry-ext-sqlite3/setup.py rename {ext/opentelemetry-ext-asyncpg => instrumentation/opentelemetry-instrumentation-asyncpg}/CHANGELOG.md (69%) create mode 100644 instrumentation/opentelemetry-instrumentation-asyncpg/README.rst rename {ext/opentelemetry-ext-asyncpg => instrumentation/opentelemetry-instrumentation-asyncpg}/setup.cfg (87%) rename {ext/opentelemetry-ext-dbapi => instrumentation/opentelemetry-instrumentation-asyncpg}/setup.py (89%) rename {ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation}/asyncpg/__init__.py (96%) rename {ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation}/asyncpg/version.py (100%) rename {ext/opentelemetry-ext-asyncpg => instrumentation/opentelemetry-instrumentation-asyncpg}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-asyncpg => instrumentation/opentelemetry-instrumentation-asyncpg}/tests/test_asyncpg_wrapper.py (94%) rename {ext/opentelemetry-ext-dbapi => instrumentation/opentelemetry-instrumentation-dbapi}/CHANGELOG.md (63%) rename {ext/opentelemetry-ext-dbapi => instrumentation/opentelemetry-instrumentation-dbapi}/LICENSE (100%) rename {ext/opentelemetry-ext-dbapi => instrumentation/opentelemetry-instrumentation-dbapi}/MANIFEST.in (100%) create mode 100644 instrumentation/opentelemetry-instrumentation-dbapi/README.rst rename {ext/opentelemetry-ext-dbapi => instrumentation/opentelemetry-instrumentation-dbapi}/setup.cfg (90%) rename {ext/opentelemetry-ext-psycopg2 => instrumentation/opentelemetry-instrumentation-dbapi}/setup.py (91%) rename {ext/opentelemetry-ext-dbapi/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation}/dbapi/__init__.py (98%) rename {ext/opentelemetry-ext-dbapi/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation}/dbapi/version.py (100%) rename {ext/opentelemetry-ext-dbapi => instrumentation/opentelemetry-instrumentation-dbapi}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-dbapi => instrumentation/opentelemetry-instrumentation-dbapi}/tests/test_dbapi_integration.py (97%) rename {ext/opentelemetry-ext-mysql => instrumentation/opentelemetry-instrumentation-mysql}/CHANGELOG.md (73%) rename {ext/opentelemetry-ext-mysql => instrumentation/opentelemetry-instrumentation-mysql}/LICENSE (100%) rename {ext/opentelemetry-ext-mysql => instrumentation/opentelemetry-instrumentation-mysql}/MANIFEST.in (100%) create mode 100644 instrumentation/opentelemetry-instrumentation-mysql/README.rst rename {ext/opentelemetry-ext-mysql => instrumentation/opentelemetry-instrumentation-mysql}/setup.cfg (86%) rename {ext/opentelemetry-ext-mysql => instrumentation/opentelemetry-instrumentation-mysql}/setup.py (91%) rename {ext/opentelemetry-ext-mysql/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation}/mysql/__init__.py (94%) rename {ext/opentelemetry-ext-mysql/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation}/mysql/version.py (100%) rename {ext/opentelemetry-ext-mysql => instrumentation/opentelemetry-instrumentation-mysql}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-mysql => instrumentation/opentelemetry-instrumentation-mysql}/tests/test_mysql_integration.py (94%) rename {ext/opentelemetry-ext-psycopg2 => instrumentation/opentelemetry-instrumentation-psycopg2}/CHANGELOG.md (63%) rename {ext/opentelemetry-ext-psycopg2 => instrumentation/opentelemetry-instrumentation-psycopg2}/LICENSE (100%) rename {ext/opentelemetry-ext-psycopg2 => instrumentation/opentelemetry-instrumentation-psycopg2}/MANIFEST.in (100%) create mode 100644 instrumentation/opentelemetry-instrumentation-psycopg2/README.rst rename {ext/opentelemetry-ext-psycopg2 => instrumentation/opentelemetry-instrumentation-psycopg2}/setup.cfg (85%) create mode 100644 instrumentation/opentelemetry-instrumentation-psycopg2/setup.py rename {ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation}/psycopg2/__init__.py (94%) rename {ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation}/psycopg2/version.py (100%) rename {ext/opentelemetry-ext-psycopg2 => instrumentation/opentelemetry-instrumentation-psycopg2}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-psycopg2 => instrumentation/opentelemetry-instrumentation-psycopg2}/tests/test_psycopg2_integration.py (94%) create mode 100644 instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md rename {ext/opentelemetry-ext-pymemcache => instrumentation/opentelemetry-instrumentation-pymemcache}/LICENSE (100%) rename {ext/opentelemetry-ext-pymemcache => instrumentation/opentelemetry-instrumentation-pymemcache}/MANIFEST.IN (100%) create mode 100644 instrumentation/opentelemetry-instrumentation-pymemcache/README.rst rename {ext/opentelemetry-ext-pymemcache => instrumentation/opentelemetry-instrumentation-pymemcache}/setup.cfg (84%) rename {ext/opentelemetry-ext-pymemcache => instrumentation/opentelemetry-instrumentation-pymemcache}/setup.py (88%) rename {ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation}/pymemcache/__init__.py (97%) rename {ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation}/pymemcache/version.py (100%) rename {ext/opentelemetry-ext-pymemcache => instrumentation/opentelemetry-instrumentation-pymemcache}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-pymemcache => instrumentation/opentelemetry-instrumentation-pymemcache}/tests/test_pymemcache.py (99%) rename {ext/opentelemetry-ext-pymemcache => instrumentation/opentelemetry-instrumentation-pymemcache}/tests/utils.py (100%) rename {ext/opentelemetry-ext-pymongo => instrumentation/opentelemetry-instrumentation-pymongo}/CHANGELOG.md (72%) rename {ext/opentelemetry-ext-pymongo => instrumentation/opentelemetry-instrumentation-pymongo}/LICENSE (100%) rename {ext/opentelemetry-ext-pymongo => instrumentation/opentelemetry-instrumentation-pymongo}/MANIFEST.in (100%) create mode 100644 instrumentation/opentelemetry-instrumentation-pymongo/README.rst rename {ext/opentelemetry-ext-pymongo => instrumentation/opentelemetry-instrumentation-pymongo}/setup.cfg (87%) create mode 100644 instrumentation/opentelemetry-instrumentation-pymongo/setup.py rename {ext/opentelemetry-ext-pymongo/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation}/pymongo/__init__.py (97%) rename {ext/opentelemetry-ext-pymongo/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation}/pymongo/version.py (100%) rename {ext/opentelemetry-ext-pymongo => instrumentation/opentelemetry-instrumentation-pymongo}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-pymongo => instrumentation/opentelemetry-instrumentation-pymongo}/tests/test_pymongo.py (98%) create mode 100644 instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md create mode 100644 instrumentation/opentelemetry-instrumentation-pymysql/README.rst rename {ext/opentelemetry-ext-pymysql => instrumentation/opentelemetry-instrumentation-pymysql}/setup.cfg (85%) create mode 100644 instrumentation/opentelemetry-instrumentation-pymysql/setup.py rename {ext/opentelemetry-ext-pymysql/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation}/pymysql/__init__.py (93%) rename {ext/opentelemetry-ext-pymysql/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation}/pymysql/version.py (100%) rename {ext/opentelemetry-ext-pymysql => instrumentation/opentelemetry-instrumentation-pymysql}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-pymysql => instrumentation/opentelemetry-instrumentation-pymysql}/tests/test_pymysql_integration.py (94%) create mode 100644 instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md rename {ext/opentelemetry-ext-redis => instrumentation/opentelemetry-instrumentation-redis}/MANIFEST.in (100%) create mode 100644 instrumentation/opentelemetry-instrumentation-redis/README.rst rename {ext/opentelemetry-ext-redis => instrumentation/opentelemetry-instrumentation-redis}/setup.cfg (88%) rename {ext/opentelemetry-ext-asyncpg => instrumentation/opentelemetry-instrumentation-redis}/setup.py (91%) rename {ext/opentelemetry-ext-redis/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation}/redis/__init__.py (96%) rename {ext/opentelemetry-ext-redis/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation}/redis/util.py (100%) rename {ext/opentelemetry-ext-redis/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation}/redis/version.py (100%) rename {ext/opentelemetry-ext-redis => instrumentation/opentelemetry-instrumentation-redis}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-redis => instrumentation/opentelemetry-instrumentation-redis}/tests/test_redis.py (96%) create mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md rename {ext/opentelemetry-ext-sqlalchemy => instrumentation/opentelemetry-instrumentation-sqlalchemy}/LICENSE (100%) rename {ext/opentelemetry-ext-sqlalchemy => instrumentation/opentelemetry-instrumentation-sqlalchemy}/MANIFEST.in (100%) create mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/README.rst rename {ext/opentelemetry-ext-sqlalchemy => instrumentation/opentelemetry-instrumentation-sqlalchemy}/setup.cfg (87%) create mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.py rename {ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation}/sqlalchemy/__init__.py (95%) rename {ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation}/sqlalchemy/engine.py (98%) rename {ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation}/sqlalchemy/version.py (100%) rename {ext/opentelemetry-ext-sqlalchemy => instrumentation/opentelemetry-instrumentation-sqlalchemy}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-sqlalchemy => instrumentation/opentelemetry-instrumentation-sqlalchemy}/tests/test_sqlalchemy.py (95%) create mode 100644 instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md rename {ext/opentelemetry-ext-sqlite3 => instrumentation/opentelemetry-instrumentation-sqlite3}/LICENSE (100%) rename {ext/opentelemetry-ext-sqlite3 => instrumentation/opentelemetry-instrumentation-sqlite3}/MANIFEST.in (100%) create mode 100644 instrumentation/opentelemetry-instrumentation-sqlite3/README.rst rename {ext/opentelemetry-ext-sqlite3 => instrumentation/opentelemetry-instrumentation-sqlite3}/setup.cfg (85%) create mode 100644 instrumentation/opentelemetry-instrumentation-sqlite3/setup.py rename {ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation}/sqlite3/__init__.py (93%) rename {ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation}/sqlite3/version.py (100%) rename {ext/opentelemetry-ext-sqlite3 => instrumentation/opentelemetry-instrumentation-sqlite3}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-sqlite3 => instrumentation/opentelemetry-instrumentation-sqlite3}/tests/test_sqlite3.py (97%) diff --git a/docs/ext/asyncpg/asyncpg.rst b/docs/ext/asyncpg/asyncpg.rst deleted file mode 100644 index 3a4a9b3c4e..0000000000 --- a/docs/ext/asyncpg/asyncpg.rst +++ /dev/null @@ -1,10 +0,0 @@ -opentelemetry.ext.asyncpg package -================================= - -Module contents ---------------- - -.. automodule:: opentelemetry.ext.asyncpg - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/ext/dbapi/dbapi.rst b/docs/ext/dbapi/dbapi.rst deleted file mode 100644 index d87d968b4b..0000000000 --- a/docs/ext/dbapi/dbapi.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Database API Integration -====================================== - -.. automodule:: opentelemetry.ext.dbapi - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/ext/mysql/mysql.rst b/docs/ext/mysql/mysql.rst deleted file mode 100644 index 4fd4749731..0000000000 --- a/docs/ext/mysql/mysql.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry MySQL Integration -=============================== - -.. automodule:: opentelemetry.ext.mysql - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/ext/psycopg2/psycopg2.rst b/docs/ext/psycopg2/psycopg2.rst deleted file mode 100644 index c9c0037546..0000000000 --- a/docs/ext/psycopg2/psycopg2.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Psycopg Integration -================================= - -.. automodule:: opentelemetry.ext.psycopg2 - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/ext/pymemcache/pymemcache.rst b/docs/ext/pymemcache/pymemcache.rst deleted file mode 100644 index c64e00cb59..0000000000 --- a/docs/ext/pymemcache/pymemcache.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry pymemcache Integration -==================================== - -.. automodule:: opentelemetry.ext.pymemcache - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/ext/pymongo/pymongo.rst b/docs/ext/pymongo/pymongo.rst deleted file mode 100644 index e75f4f4168..0000000000 --- a/docs/ext/pymongo/pymongo.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry pymongo Integration -================================= - -.. automodule:: opentelemetry.ext.pymongo - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/ext/pymysql/pymysql.rst b/docs/ext/pymysql/pymysql.rst deleted file mode 100644 index 23dca80c4f..0000000000 --- a/docs/ext/pymysql/pymysql.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry PyMySQL Integration -================================= - -.. automodule:: opentelemetry.ext.pymysql - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/ext/sqlite3/sqlite3.rst b/docs/ext/sqlite3/sqlite3.rst deleted file mode 100644 index 9537ff58bf..0000000000 --- a/docs/ext/sqlite3/sqlite3.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry SQLite3 Integration -================================= - -.. automodule:: opentelemetry.ext.sqlite3 - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/asyncpg/asyncpg.rst b/docs/instrumentation/asyncpg/asyncpg.rst new file mode 100644 index 0000000000..745e83f51d --- /dev/null +++ b/docs/instrumentation/asyncpg/asyncpg.rst @@ -0,0 +1,10 @@ +Opentelemetry asyncpg Instrumentation +===================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.instrumentation.asyncpg + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/instrumentation/dbapi/dbapi.rst b/docs/instrumentation/dbapi/dbapi.rst new file mode 100644 index 0000000000..a20be63097 --- /dev/null +++ b/docs/instrumentation/dbapi/dbapi.rst @@ -0,0 +1,7 @@ +OpenTelemetry Database API Instrumentation +========================================== + +.. automodule:: opentelemetry.instrumentation.dbapi + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/instrumentation/mysql/mysql.rst b/docs/instrumentation/mysql/mysql.rst new file mode 100644 index 0000000000..3a4a41542a --- /dev/null +++ b/docs/instrumentation/mysql/mysql.rst @@ -0,0 +1,7 @@ +OpenTelemetry MySQL Instrumentation +=================================== + +.. automodule:: opentelemetry.instrumentation.mysql + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/instrumentation/psycopg2/psycopg2.rst b/docs/instrumentation/psycopg2/psycopg2.rst new file mode 100644 index 0000000000..69be31b2d1 --- /dev/null +++ b/docs/instrumentation/psycopg2/psycopg2.rst @@ -0,0 +1,7 @@ +OpenTelemetry Psycopg Instrumentation +===================================== + +.. automodule:: opentelemetry.instrumentation.psycopg2 + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/instrumentation/pymemcache/pymemcache.rst b/docs/instrumentation/pymemcache/pymemcache.rst new file mode 100644 index 0000000000..2a77b829d9 --- /dev/null +++ b/docs/instrumentation/pymemcache/pymemcache.rst @@ -0,0 +1,7 @@ +OpenTelemetry pymemcache Instrumentation +======================================== + +.. automodule:: opentelemetry.instrumentation.pymemcache + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/instrumentation/pymongo/pymongo.rst b/docs/instrumentation/pymongo/pymongo.rst new file mode 100644 index 0000000000..4eb68be27b --- /dev/null +++ b/docs/instrumentation/pymongo/pymongo.rst @@ -0,0 +1,7 @@ +OpenTelemetry pymongo Instrumentation +===================================== + +.. automodule:: opentelemetry.instrumentation.pymongo + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/instrumentation/pymysql/pymysql.rst b/docs/instrumentation/pymysql/pymysql.rst new file mode 100644 index 0000000000..26482292fe --- /dev/null +++ b/docs/instrumentation/pymysql/pymysql.rst @@ -0,0 +1,7 @@ +OpenTelemetry PyMySQL Instrumentation +===================================== + +.. automodule:: opentelemetry.instrumentation.pymysql + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ext/redis/redis.rst b/docs/instrumentation/redis/redis.rst similarity index 71% rename from docs/ext/redis/redis.rst rename to docs/instrumentation/redis/redis.rst index 38a72ad52f..4e21bce24b 100644 --- a/docs/ext/redis/redis.rst +++ b/docs/instrumentation/redis/redis.rst @@ -1,7 +1,7 @@ OpenTelemetry Redis Instrumentation =================================== -.. automodule:: opentelemetry.ext.redis +.. automodule:: opentelemetry.instrumentation.redis :members: :undoc-members: :show-inheritance: diff --git a/docs/ext/sqlalchemy/sqlalchemy.rst b/docs/instrumentation/sqlalchemy/sqlalchemy.rst similarity index 71% rename from docs/ext/sqlalchemy/sqlalchemy.rst rename to docs/instrumentation/sqlalchemy/sqlalchemy.rst index 5a3afbb3bb..1a1895ea6b 100644 --- a/docs/ext/sqlalchemy/sqlalchemy.rst +++ b/docs/instrumentation/sqlalchemy/sqlalchemy.rst @@ -1,7 +1,7 @@ OpenTelemetry SQLAlchemy Instrumentation ======================================== -.. automodule:: opentelemetry.ext.sqlalchemy +.. automodule:: opentelemetry.instrumentation.sqlalchemy :members: :undoc-members: :show-inheritance: diff --git a/docs/instrumentation/sqlite3/sqlite3.rst b/docs/instrumentation/sqlite3/sqlite3.rst new file mode 100644 index 0000000000..36b541ccd1 --- /dev/null +++ b/docs/instrumentation/sqlite3/sqlite3.rst @@ -0,0 +1,7 @@ +OpenTelemetry SQLite3 Instrumentation +===================================== + +.. automodule:: opentelemetry.instrumentation.sqlite3 + :members: + :undoc-members: + :show-inheritance: diff --git a/eachdist.ini b/eachdist.ini index 06d940378c..abf9e620aa 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -8,7 +8,7 @@ sortfirst= opentelemetry-proto tests/util instrumentation/opentelemetry-instrumentation-wsgi - ext/opentelemetry-ext-dbapi + instrumentation/opentelemetry-instrumentation-dbapi instrumentation/opentelemetry-instrumentation-asgi ext/opentelemetry-ext-botocore instrumentation/* diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py index 785bccccce..7c94e173f7 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py @@ -34,19 +34,19 @@ _INSTRUMENTATION_SPAN_TYPES = { "opentelemetry.instrumentation.aiohttp-client": DatadogSpanTypes.HTTP, "opentelemetry.instrumentation.asgi": DatadogSpanTypes.WEB, - "opentelemetry.ext.dbapi": DatadogSpanTypes.SQL, + "opentelemetry.instrumentation.dbapi": DatadogSpanTypes.SQL, "opentelemetry.instrumentation.django": DatadogSpanTypes.WEB, "opentelemetry.instrumentation.flask": DatadogSpanTypes.WEB, "opentelemetry.ext.grpc": DatadogSpanTypes.GRPC, "opentelemetry.ext.jinja2": DatadogSpanTypes.TEMPLATE, - "opentelemetry.ext.mysql": DatadogSpanTypes.SQL, - "opentelemetry.ext.psycopg2": DatadogSpanTypes.SQL, - "opentelemetry.ext.pymemcache": DatadogSpanTypes.CACHE, - "opentelemetry.ext.pymongo": DatadogSpanTypes.MONGODB, - "opentelemetry.ext.pymysql": DatadogSpanTypes.SQL, - "opentelemetry.ext.redis": DatadogSpanTypes.REDIS, + "opentelemetry.instrumentation.mysql": DatadogSpanTypes.SQL, + "opentelemetry.instrumentation.psycopg2": DatadogSpanTypes.SQL, + "opentelemetry.instrumentation.pymemcache": DatadogSpanTypes.CACHE, + "opentelemetry.instrumentation.pymongo": DatadogSpanTypes.MONGODB, + "opentelemetry.instrumentation.pymysql": DatadogSpanTypes.SQL, + "opentelemetry.instrumentation.redis": DatadogSpanTypes.REDIS, "opentelemetry.instrumentation.requests": DatadogSpanTypes.HTTP, - "opentelemetry.ext.sqlalchemy": DatadogSpanTypes.SQL, + "opentelemetry.instrumentation.sqlalchemy": DatadogSpanTypes.SQL, "opentelemetry.instrumentation.wsgi": DatadogSpanTypes.WEB, } diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py index cf3c3c7256..4b7d2391bc 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py @@ -291,18 +291,18 @@ def test_resources(self): def test_span_types(self): test_instrumentations = [ "opentelemetry.instrumentation.aiohttp-client", - "opentelemetry.ext.dbapi", + "opentelemetry.instrumentation.dbapi", "opentelemetry.instrumentation.django", "opentelemetry.instrumentation.flask", "opentelemetry.ext.grpc", "opentelemetry.ext.jinja2", - "opentelemetry.ext.mysql", - "opentelemetry.ext.psycopg2", - "opentelemetry.ext.pymongo", - "opentelemetry.ext.pymysql", - "opentelemetry.ext.redis", + "opentelemetry.instrumentation.mysql", + "opentelemetry.instrumentation.psycopg2", + "opentelemetry.instrumentation.pymongo", + "opentelemetry.instrumentation.pymysql", + "opentelemetry.instrumentation.redis", "opentelemetry.instrumentation.requests", - "opentelemetry.ext.sqlalchemy", + "opentelemetry.instrumentation.sqlalchemy", "opentelemetry.instrumentation.wsgi", ] diff --git a/ext/opentelemetry-ext-asyncpg/README.rst b/ext/opentelemetry-ext-asyncpg/README.rst deleted file mode 100644 index f852bfdbb2..0000000000 --- a/ext/opentelemetry-ext-asyncpg/README.rst +++ /dev/null @@ -1,23 +0,0 @@ -OpenTelemetry asyncpg Integration -================================= - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-asyncpg.svg - :target: https://pypi.org/project/opentelemetry-ext-asyncpg/ - -This library allows tracing PostgreSQL queries made by the -`asyncpg `_ library. - -Installation ------------- - -:: - - pip install opentelemetry-ext-asyncpg - -References ----------- - -* `OpenTelemetry asyncpg Integration `_ -* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst deleted file mode 100644 index 1ff464cb48..0000000000 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ /dev/null @@ -1,21 +0,0 @@ -OpenTelemetry Database API integration -====================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-dbapi.svg - :target: https://pypi.org/project/opentelemetry-ext-dbapi/ - -Installation ------------- - -:: - - pip install opentelemetry-ext-dbapi - - -References ----------- - -* `OpenTelemetry Database API integration `_ -* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py b/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py index d3060592a6..87382702f2 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py @@ -3,7 +3,7 @@ import asyncpg -from opentelemetry.ext.asyncpg import AsyncPGInstrumentor +from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.trace.status import StatusCanonicalCode diff --git a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py index d1f4c689f9..f2b07293bf 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py @@ -18,7 +18,7 @@ import mysql.connector from opentelemetry import trace as trace_api -from opentelemetry.ext.mysql import MySQLInstrumentor +from opentelemetry.instrumentation.mysql import MySQLInstrumentor from opentelemetry.test.test_base import TestBase MYSQL_USER = os.getenv("MYSQL_USER ", "testuser") diff --git a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py index d0bdd685f4..a8e07ddb27 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py @@ -18,7 +18,7 @@ import psycopg2 from opentelemetry import trace as trace_api -from opentelemetry.ext.psycopg2 import Psycopg2Instrumentor +from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor from opentelemetry.test.test_base import TestBase POSTGRES_HOST = os.getenv("POSTGRESQL_HOST ", "localhost") diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py index f85a32d450..8c52ad0656 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py @@ -17,7 +17,7 @@ from pymongo import MongoClient from opentelemetry import trace as trace_api -from opentelemetry.ext.pymongo import PymongoInstrumentor +from opentelemetry.instrumentation.pymongo import PymongoInstrumentor from opentelemetry.test.test_base import TestBase MONGODB_HOST = os.getenv("MONGODB_HOST ", "localhost") diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py index 5b004fe18f..1636f85fba 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py @@ -17,7 +17,7 @@ import pymysql as pymy from opentelemetry import trace as trace_api -from opentelemetry.ext.pymysql import PyMySQLInstrumentor +from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor from opentelemetry.test.test_base import TestBase MYSQL_USER = os.getenv("MYSQL_USER ", "testuser") diff --git a/ext/opentelemetry-ext-docker-tests/tests/redis/test_redis_functional.py b/ext/opentelemetry-ext-docker-tests/tests/redis/test_redis_functional.py index 45283c442c..7e6ea2e044 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/redis/test_redis_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/redis/test_redis_functional.py @@ -15,7 +15,7 @@ import redis from opentelemetry import trace -from opentelemetry.ext.redis import RedisInstrumentor +from opentelemetry.instrumentation.redis import RedisInstrumentor from opentelemetry.test.test_base import TestBase diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/mixins.py b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/mixins.py index 84c6fd05f9..a438f58eb9 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/mixins.py +++ b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/mixins.py @@ -19,8 +19,8 @@ from sqlalchemy.orm import sessionmaker from opentelemetry import trace -from opentelemetry.ext.sqlalchemy import SQLAlchemyInstrumentor -from opentelemetry.ext.sqlalchemy.engine import _DB, _ROWS, _STMT +from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor +from opentelemetry.instrumentation.sqlalchemy.engine import _DB, _ROWS, _STMT from opentelemetry.test.test_base import TestBase Base = declarative_base() diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_instrument.py b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_instrument.py index 845bf26cfa..20c837c03b 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_instrument.py +++ b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_instrument.py @@ -18,7 +18,7 @@ import sqlalchemy from opentelemetry import trace -from opentelemetry.ext.sqlalchemy import SQLAlchemyInstrumentor +from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.test.test_base import TestBase POSTGRES_CONFIG = { diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_mysql.py b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_mysql.py index 3b8adc8c62..44c3501b1d 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_mysql.py +++ b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_mysql.py @@ -19,7 +19,13 @@ from sqlalchemy.exc import ProgrammingError from opentelemetry import trace -from opentelemetry.ext.sqlalchemy.engine import _DB, _HOST, _PORT, _ROWS, _STMT +from opentelemetry.instrumentation.sqlalchemy.engine import ( + _DB, + _HOST, + _PORT, + _ROWS, + _STMT, +) from .mixins import SQLAlchemyTestMixin diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_postgres.py b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_postgres.py index 125c925209..615a196f5b 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_postgres.py +++ b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_postgres.py @@ -20,7 +20,13 @@ from sqlalchemy.exc import ProgrammingError from opentelemetry import trace -from opentelemetry.ext.sqlalchemy.engine import _DB, _HOST, _PORT, _ROWS, _STMT +from opentelemetry.instrumentation.sqlalchemy.engine import ( + _DB, + _HOST, + _PORT, + _ROWS, + _STMT, +) from .mixins import SQLAlchemyTestMixin diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_sqlite.py b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_sqlite.py index 7d8a54368f..4295fc045c 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_sqlite.py +++ b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_sqlite.py @@ -18,7 +18,7 @@ from sqlalchemy.exc import OperationalError from opentelemetry import trace -from opentelemetry.ext.sqlalchemy.engine import _DB, _ROWS, _STMT +from opentelemetry.instrumentation.sqlalchemy.engine import _DB, _ROWS, _STMT from .mixins import SQLAlchemyTestMixin diff --git a/ext/opentelemetry-ext-mysql/README.rst b/ext/opentelemetry-ext-mysql/README.rst deleted file mode 100644 index 0a8577e867..0000000000 --- a/ext/opentelemetry-ext-mysql/README.rst +++ /dev/null @@ -1,25 +0,0 @@ -OpenTelemetry MySQL Integration -=============================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-mysql.svg - :target: https://pypi.org/project/opentelemetry-ext-mysql/ - -Integration with MySQL that supports the mysql-connector library and is -specified to trace_integration using 'MySQL'. - - -Installation ------------- - -:: - - pip install opentelemetry-ext-mysql - - -References ----------- -* `OpenTelemetry MySQL Integration `_ -* `OpenTelemetry Project `_ - diff --git a/ext/opentelemetry-ext-psycopg2/README.rst b/ext/opentelemetry-ext-psycopg2/README.rst deleted file mode 100644 index 18cee0c2b2..0000000000 --- a/ext/opentelemetry-ext-psycopg2/README.rst +++ /dev/null @@ -1,20 +0,0 @@ -OpenTelemetry Psycopg Integration -================================= - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-psycopg2.svg - :target: https://pypi.org/project/opentelemetry-ext-psycopg2/ - -Installation ------------- - -:: - - pip install opentelemetry-ext-psycopg2 - - -References ----------- -* `OpenTelemetry Psycopg Integration `_ -* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-pymemcache/CHANGELOG.md b/ext/opentelemetry-ext-pymemcache/CHANGELOG.md deleted file mode 100644 index 7425aa5e1e..0000000000 --- a/ext/opentelemetry-ext-pymemcache/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.10b0 - -Released 2020-06-23 - -- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-pymemcache/README.rst b/ext/opentelemetry-ext-pymemcache/README.rst deleted file mode 100644 index 6328ff5f01..0000000000 --- a/ext/opentelemetry-ext-pymemcache/README.rst +++ /dev/null @@ -1,20 +0,0 @@ -OpenTelemetry pymemcache Integration -==================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-pymemcache.svg - :target: https://pypi.org/project/opentelemetry-ext-pymemcache/ - -Installation ------------- - -:: - - pip install opentelemetry-ext-pymemcache - - -References ----------- -* `OpenTelemetry Pymemcache Integration `_ -* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-pymongo/README.rst b/ext/opentelemetry-ext-pymongo/README.rst deleted file mode 100644 index ada56efed9..0000000000 --- a/ext/opentelemetry-ext-pymongo/README.rst +++ /dev/null @@ -1,21 +0,0 @@ -OpenTelemetry pymongo Integration -================================= - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-pymongo.svg - :target: https://pypi.org/project/opentelemetry-ext-pymongo/ - -Installation ------------- - -:: - - pip install opentelemetry-ext-pymongo - - -References ----------- -* `OpenTelemetry pymongo Integration `_ -* `OpenTelemetry Project `_ - diff --git a/ext/opentelemetry-ext-pymongo/setup.py b/ext/opentelemetry-ext-pymongo/setup.py deleted file mode 100644 index 301aded338..0000000000 --- a/ext/opentelemetry-ext-pymongo/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "pymongo", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-pymysql/CHANGELOG.md b/ext/opentelemetry-ext-pymysql/CHANGELOG.md deleted file mode 100644 index 654146fece..0000000000 --- a/ext/opentelemetry-ext-pymysql/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -## Unreleased - -## 0.7b1 - -Released 2020-05-12 - -- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-pymysql/README.rst b/ext/opentelemetry-ext-pymysql/README.rst deleted file mode 100644 index 455d8fa7bd..0000000000 --- a/ext/opentelemetry-ext-pymysql/README.rst +++ /dev/null @@ -1,20 +0,0 @@ -OpenTelemetry PyMySQL integration -================================= - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-pymysql.svg - :target: https://pypi.org/project/opentelemetry-ext-pymysql/ - -Installation ------------- - -:: - - pip install opentelemetry-ext-pymysql - - -References ----------- -* `OpenTelemetry PyMySQL Integration `_ -* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-pymysql/setup.py b/ext/opentelemetry-ext-pymysql/setup.py deleted file mode 100644 index a3f057b310..0000000000 --- a/ext/opentelemetry-ext-pymysql/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "pymysql", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-redis/CHANGELOG.md b/ext/opentelemetry-ext-redis/CHANGELOG.md deleted file mode 100644 index 654146fece..0000000000 --- a/ext/opentelemetry-ext-redis/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -## Unreleased - -## 0.7b1 - -Released 2020-05-12 - -- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-redis/README.rst b/ext/opentelemetry-ext-redis/README.rst deleted file mode 100644 index 49cc95f6b1..0000000000 --- a/ext/opentelemetry-ext-redis/README.rst +++ /dev/null @@ -1,23 +0,0 @@ -OpenTelemetry Redis Instrumentation -=================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-redis.svg - :target: https://pypi.org/project/opentelemetry-ext-redis/ - -This library allows tracing requests made by the Redis library. - -Installation ------------- - -:: - - pip install opentelemetry-ext-redis - - -References ----------- - -* `OpenTelemetry Redis Instrumentation `_ -* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-redis/setup.py b/ext/opentelemetry-ext-redis/setup.py deleted file mode 100644 index d09847efd8..0000000000 --- a/ext/opentelemetry-ext-redis/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "redis", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md b/ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md deleted file mode 100644 index 654146fece..0000000000 --- a/ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -## Unreleased - -## 0.7b1 - -Released 2020-05-12 - -- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-sqlalchemy/README.rst b/ext/opentelemetry-ext-sqlalchemy/README.rst deleted file mode 100644 index 2485c96a58..0000000000 --- a/ext/opentelemetry-ext-sqlalchemy/README.rst +++ /dev/null @@ -1,24 +0,0 @@ -OpenTelemetry SQLAlchemy Tracing -================================ - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-sqlalchemy.svg - :target: https://pypi.org/project/opentelemetry-ext-sqlalchemy/ - -This library allows tracing requests made by the SQLAlchemy library. - -Installation ------------- - -:: - - pip install opentelemetry-ext-sqlalchemy - - -References ----------- - -* `SQLAlchemy Project `_ -* `OpenTelemetry SQLAlchemy Tracing `_ -* `OpenTelemetry Project `_ \ No newline at end of file diff --git a/ext/opentelemetry-ext-sqlalchemy/setup.py b/ext/opentelemetry-ext-sqlalchemy/setup.py deleted file mode 100644 index d776a90e82..0000000000 --- a/ext/opentelemetry-ext-sqlalchemy/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "sqlalchemy", "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-sqlite3/CHANGELOG.md b/ext/opentelemetry-ext-sqlite3/CHANGELOG.md deleted file mode 100644 index c2ac2d3e02..0000000000 --- a/ext/opentelemetry-ext-sqlite3/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -## Unreleased - -## 0.8b0 - -Released 2020-05-27 - -- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-sqlite3/README.rst b/ext/opentelemetry-ext-sqlite3/README.rst deleted file mode 100644 index 3620434b20..0000000000 --- a/ext/opentelemetry-ext-sqlite3/README.rst +++ /dev/null @@ -1,21 +0,0 @@ -OpenTelemetry SQLite3 Integration -================================= - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-sqlite3.svg - :target: https://pypi.org/project/opentelemetry-ext-sqlite3/ - -Installation ------------- - -:: - - pip install opentelemetry-ext-sqlite3 - - -References ----------- -* `OpenTelemetry SQLite3 Integration `_ -* `OpenTelemetry Project `_ - diff --git a/ext/opentelemetry-ext-sqlite3/setup.py b/ext/opentelemetry-ext-sqlite3/setup.py deleted file mode 100644 index dd89f058db..0000000000 --- a/ext/opentelemetry-ext-sqlite3/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "sqlite3", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg index bd9a06c524..58c9b38496 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg @@ -40,7 +40,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.12.dev0 - opentelemetry-ext-dbapi == 0.12.dev0 + opentelemetry-instrumentation-dbapi == 0.12.dev0 opentelemetry-instrumentation == 0.12.dev0 aiopg >= 0.13.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py index def4a72c3d..b6992120f3 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py @@ -3,7 +3,10 @@ import wrapt from aiopg.utils import _ContextManager, _PoolAcquireContextManager -from opentelemetry.ext.dbapi import DatabaseApiIntegration, TracedCursor +from opentelemetry.instrumentation.dbapi import ( + DatabaseApiIntegration, + TracedCursor, +) from opentelemetry.trace import SpanKind from opentelemetry.trace.status import Status, StatusCanonicalCode diff --git a/ext/opentelemetry-ext-asyncpg/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md similarity index 69% rename from ext/opentelemetry-ext-asyncpg/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md index 2dc9ad6f04..08464e8cd9 100644 --- a/ext/opentelemetry-ext-asyncpg/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-asyncpg + ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ## Version 0.11b0 Released 2020-07-28 diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/README.rst b/instrumentation/opentelemetry-instrumentation-asyncpg/README.rst new file mode 100644 index 0000000000..33c60852cd --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry asyncpg Instrumentation +===================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-asyncpg.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-asyncpg/ + +This library allows tracing PostgreSQL queries made by the +`asyncpg `_ library. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-asyncpg + +References +---------- + +* `OpenTelemetry asyncpg Instrumentation `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-asyncpg/setup.cfg b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg similarity index 87% rename from ext/opentelemetry-ext-asyncpg/setup.cfg rename to instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg index bf1670172b..1cc707df1c 100644 --- a/ext/opentelemetry-ext-asyncpg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-asyncpg +name = opentelemetry-instrumentation-asyncpg description = OpenTelemetry instrumentation for AsyncPG long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-asyncpg +url = https://github.com/open-telemetry/opentelemetry-python/instrumentation/opentelemetry-instrumentation-asyncpg platforms = any license = Apache-2.0 classifiers = @@ -52,4 +52,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - asyncpg = opentelemetry.ext.asyncpg:AsyncPGInstrumentor + asyncpg = opentelemetry.instrumentation.asyncpg:AsyncPGInstrumentor diff --git a/ext/opentelemetry-ext-dbapi/setup.py b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.py similarity index 89% rename from ext/opentelemetry-ext-dbapi/setup.py rename to instrumentation/opentelemetry-instrumentation-asyncpg/setup.py index f5c1b3fa81..2ad47ac9d9 100644 --- a/ext/opentelemetry-ext-dbapi/setup.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.py @@ -17,7 +17,12 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "dbapi", "version.py" + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "asyncpg", + "version.py", ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py similarity index 96% rename from ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/__init__.py rename to instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py index 4a3a51ac08..189809809d 100644 --- a/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py @@ -22,7 +22,7 @@ .. code-block:: python import asyncpg - from opentelemetry.ext.asyncpg import AsyncPGInstrumentor + from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor # You can optionally pass a custom TracerProvider to AsyncPGInstrumentor.instrument() AsyncPGInstrumentor().instrument() @@ -39,7 +39,7 @@ from asyncpg import exceptions from opentelemetry import trace -from opentelemetry.ext.asyncpg.version import __version__ +from opentelemetry.instrumentation.asyncpg.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import SpanKind diff --git a/ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py similarity index 100% rename from ext/opentelemetry-ext-asyncpg/src/opentelemetry/ext/asyncpg/version.py rename to instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py diff --git a/ext/opentelemetry-ext-asyncpg/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncpg/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-asyncpg/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-asyncpg/tests/__init__.py diff --git a/ext/opentelemetry-ext-asyncpg/tests/test_asyncpg_wrapper.py b/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py similarity index 94% rename from ext/opentelemetry-ext-asyncpg/tests/test_asyncpg_wrapper.py rename to instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py index cd0d8e35f0..33b121ce53 100644 --- a/ext/opentelemetry-ext-asyncpg/tests/test_asyncpg_wrapper.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py @@ -1,7 +1,7 @@ import asyncpg from asyncpg import Connection -from opentelemetry.ext.asyncpg import AsyncPGInstrumentor +from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor from opentelemetry.test.test_base import TestBase diff --git a/ext/opentelemetry-ext-dbapi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md similarity index 63% rename from ext/opentelemetry-ext-dbapi/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md index 288334d6ba..c0a38d855f 100644 --- a/ext/opentelemetry-ext-dbapi/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-dbapi + ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ## 0.7b1 Released 2020-05-12 diff --git a/ext/opentelemetry-ext-dbapi/LICENSE b/instrumentation/opentelemetry-instrumentation-dbapi/LICENSE similarity index 100% rename from ext/opentelemetry-ext-dbapi/LICENSE rename to instrumentation/opentelemetry-instrumentation-dbapi/LICENSE diff --git a/ext/opentelemetry-ext-dbapi/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-dbapi/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-dbapi/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-dbapi/MANIFEST.in diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/README.rst b/instrumentation/opentelemetry-instrumentation-dbapi/README.rst new file mode 100644 index 0000000000..5137a1c1ff --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-dbapi/README.rst @@ -0,0 +1,21 @@ +OpenTelemetry Database API instrumentation +========================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-dbapi.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-dbapi/ + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-dbapi + + +References +---------- + +* `OpenTelemetry Database API Instrumentation `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg similarity index 90% rename from ext/opentelemetry-ext-dbapi/setup.cfg rename to instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg index 34542525dd..29044f3ba6 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-dbapi -description = OpenTelemetry Database API integration +name = opentelemetry-instrumentation-dbapi +description = OpenTelemetry Database API instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-dbapi +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-dbapi platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-psycopg2/setup.py b/instrumentation/opentelemetry-instrumentation-dbapi/setup.py similarity index 91% rename from ext/opentelemetry-ext-psycopg2/setup.py rename to instrumentation/opentelemetry-instrumentation-dbapi/setup.py index df7f7c2128..cfe98f3895 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "psycopg2", "version.py" + BASE_DIR, "src", "opentelemetry", "instrumentation", "dbapi", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py similarity index 98% rename from ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py rename to instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py index 3838dc1b15..035c823bcf 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -26,7 +26,7 @@ import pyodbc from opentelemetry import trace - from opentelemetry.ext.dbapi import trace_integration + from opentelemetry.instrumentation.dbapi import trace_integration from opentelemetry.trace import TracerProvider trace.set_tracer_provider(TracerProvider()) @@ -47,7 +47,7 @@ import wrapt from opentelemetry import trace as trace_api -from opentelemetry.ext.dbapi.version import __version__ +from opentelemetry.instrumentation.dbapi.version import __version__ from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import SpanKind, TracerProvider, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py similarity index 100% rename from ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py rename to instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py diff --git a/ext/opentelemetry-ext-dbapi/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-dbapi/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-dbapi/tests/__init__.py diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py similarity index 97% rename from ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py rename to instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py index a2ba9f7d89..2991570333 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py @@ -17,7 +17,7 @@ from unittest import mock from opentelemetry import trace as trace_api -from opentelemetry.ext import dbapi +from opentelemetry.instrumentation import dbapi from opentelemetry.test.test_base import TestBase @@ -120,14 +120,14 @@ def test_callproc(self): span.attributes["db.statement"], "Test stored procedure" ) - @mock.patch("opentelemetry.ext.dbapi") + @mock.patch("opentelemetry.instrumentation.dbapi") def test_wrap_connect(self, mock_dbapi): dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-") connection = mock_dbapi.connect() self.assertEqual(mock_dbapi.connect.call_count, 1) self.assertIsInstance(connection.__wrapped__, mock.Mock) - @mock.patch("opentelemetry.ext.dbapi") + @mock.patch("opentelemetry.instrumentation.dbapi") def test_unwrap_connect(self, mock_dbapi): dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-") connection = mock_dbapi.connect() diff --git a/ext/opentelemetry-ext-mysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md similarity index 73% rename from ext/opentelemetry-ext-mysql/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md index 46b9d327dd..434a612abc 100644 --- a/ext/opentelemetry-ext-mysql/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-mysql + ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ## Version 0.11b0 Released 2020-07-28 diff --git a/ext/opentelemetry-ext-mysql/LICENSE b/instrumentation/opentelemetry-instrumentation-mysql/LICENSE similarity index 100% rename from ext/opentelemetry-ext-mysql/LICENSE rename to instrumentation/opentelemetry-instrumentation-mysql/LICENSE diff --git a/ext/opentelemetry-ext-mysql/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-mysql/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-mysql/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-mysql/MANIFEST.in diff --git a/instrumentation/opentelemetry-instrumentation-mysql/README.rst b/instrumentation/opentelemetry-instrumentation-mysql/README.rst new file mode 100644 index 0000000000..9558f64bd9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-mysql/README.rst @@ -0,0 +1,25 @@ +OpenTelemetry MySQL Instrumentation +=================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-mysql.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-mysql/ + +Instrumentation with MySQL that supports the mysql-connector library and is +specified to trace_integration using 'MySQL'. + + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-mysql + + +References +---------- +* `OpenTelemetry MySQL Instrumentation `_ +* `OpenTelemetry Project `_ + diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg similarity index 86% rename from ext/opentelemetry-ext-mysql/setup.cfg rename to instrumentation/opentelemetry-instrumentation-mysql/setup.cfg index e114ae1dbe..2242709dab 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-mysql -description = OpenTelemetry MySQL integration +name = opentelemetry-instrumentation-mysql +description = OpenTelemetry MySQL instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-mysql +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-mysql platforms = any license = Apache-2.0 classifiers = @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.12.dev0 - opentelemetry-ext-dbapi == 0.12.dev0 + opentelemetry-instrumentation-dbapi == 0.12.dev0 opentelemetry-instrumentation == 0.12.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 @@ -55,4 +55,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - mysql = opentelemetry.ext.mysql:MySQLInstrumentor + mysql = opentelemetry.instrumentation.mysql:MySQLInstrumentor diff --git a/ext/opentelemetry-ext-mysql/setup.py b/instrumentation/opentelemetry-instrumentation-mysql/setup.py similarity index 91% rename from ext/opentelemetry-ext-mysql/setup.py rename to instrumentation/opentelemetry-instrumentation-mysql/setup.py index 4ba48fb404..955f75d71e 100644 --- a/ext/opentelemetry-ext-mysql/setup.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "mysql", "version.py" + BASE_DIR, "src", "opentelemetry", "instrumentation", "mysql", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py similarity index 94% rename from ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py rename to instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py index 883a0aa9bb..1cbd240cc5 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py @@ -26,7 +26,7 @@ import mysql.connector from opentelemetry import trace from opentelemetry.trace import TracerProvider - from opentelemetry.ext.mysql import MySQLInstrumentor + from opentelemetry.instrumentation.mysql import MySQLInstrumentor trace.set_tracer_provider(TracerProvider()) @@ -44,9 +44,9 @@ import mysql.connector -from opentelemetry.ext import dbapi -from opentelemetry.ext.mysql.version import __version__ +from opentelemetry.instrumentation import dbapi from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.mysql.version import __version__ from opentelemetry.trace import get_tracer diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py similarity index 100% rename from ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py rename to instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py diff --git a/ext/opentelemetry-ext-mysql/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-mysql/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-mysql/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-mysql/tests/__init__.py diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py similarity index 94% rename from ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py rename to instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py index 6a86ef8927..6c114d969f 100644 --- a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py @@ -16,8 +16,8 @@ import mysql.connector -import opentelemetry.ext.mysql -from opentelemetry.ext.mysql import MySQLInstrumentor +import opentelemetry.instrumentation.mysql +from opentelemetry.instrumentation.mysql import MySQLInstrumentor from opentelemetry.sdk import resources from opentelemetry.test.test_base import TestBase @@ -43,7 +43,9 @@ def test_instrumentor(self, mock_connect): span = spans_list[0] # Check version and name in span's instrumentation info - self.check_span_instrumentation_info(span, opentelemetry.ext.mysql) + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.mysql + ) # check that no spans are generated after uninstrumen MySQLInstrumentor().uninstrument() diff --git a/ext/opentelemetry-ext-psycopg2/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md similarity index 63% rename from ext/opentelemetry-ext-psycopg2/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md index 2106b42550..c997f23945 100644 --- a/ext/opentelemetry-ext-psycopg2/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-psycopg2 + ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ## 0.8b0 Released 2020-05-27 diff --git a/ext/opentelemetry-ext-psycopg2/LICENSE b/instrumentation/opentelemetry-instrumentation-psycopg2/LICENSE similarity index 100% rename from ext/opentelemetry-ext-psycopg2/LICENSE rename to instrumentation/opentelemetry-instrumentation-psycopg2/LICENSE diff --git a/ext/opentelemetry-ext-psycopg2/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-psycopg2/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-psycopg2/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-psycopg2/MANIFEST.in diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/README.rst b/instrumentation/opentelemetry-instrumentation-psycopg2/README.rst new file mode 100644 index 0000000000..3ab1025eae --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/README.rst @@ -0,0 +1,20 @@ +OpenTelemetry Psycopg Instrumentation +===================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-psycopg2.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-psycopg2/ + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-psycopg2 + + +References +---------- +* `OpenTelemetry Psycopg Instrumentation `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg similarity index 85% rename from ext/opentelemetry-ext-psycopg2/setup.cfg rename to instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg index 73d541cad6..c20693a96c 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-psycopg2 -description = OpenTelemetry psycopg2 integration +name = opentelemetry-instrumentation-psycopg2 +description = OpenTelemetry psycopg2 instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-psycopg2 +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-psycopg2 platforms = any license = Apache-2.0 classifiers = @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.12.dev0 - opentelemetry-ext-dbapi == 0.12.dev0 + opentelemetry-instrumentation-dbapi == 0.12.dev0 opentelemetry-instrumentation == 0.12.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 @@ -55,4 +55,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - psycopg2 = opentelemetry.ext.psycopg2:Psycopg2Instrumentor + psycopg2 = opentelemetry.instrumentation.psycopg2:Psycopg2Instrumentor diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.py b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.py new file mode 100644 index 0000000000..4bd0e16be5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "psycopg2", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py similarity index 94% rename from ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py rename to instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py index ec6c4c82ad..7782f6fe63 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py @@ -26,7 +26,7 @@ import psycopg2 from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.ext.psycopg2 import Psycopg2Instrumentor + from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor trace.set_tracer_provider(TracerProvider()) @@ -44,9 +44,9 @@ import psycopg2 -from opentelemetry.ext import dbapi -from opentelemetry.ext.psycopg2.version import __version__ +from opentelemetry.instrumentation import dbapi from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.psycopg2.version import __version__ from opentelemetry.trace import get_tracer diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py similarity index 100% rename from ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py rename to instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py diff --git a/ext/opentelemetry-ext-psycopg2/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-psycopg2/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-psycopg2/tests/__init__.py diff --git a/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py similarity index 94% rename from ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py rename to instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py index f854787bd9..629b2f62b6 100644 --- a/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py @@ -16,8 +16,8 @@ import psycopg2 -import opentelemetry.ext.psycopg2 -from opentelemetry.ext.psycopg2 import Psycopg2Instrumentor +import opentelemetry.instrumentation.psycopg2 +from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor from opentelemetry.sdk import resources from opentelemetry.test.test_base import TestBase @@ -45,7 +45,9 @@ def test_instrumentor(self, mock_connect): span = spans_list[0] # Check version and name in span's instrumentation info - self.check_span_instrumentation_info(span, opentelemetry.ext.psycopg2) + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.psycopg2 + ) # check that no spans are generated after uninstrument Psycopg2Instrumentor().uninstrument() diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md new file mode 100644 index 0000000000..c549095d2e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## Unreleased + +- Change package name to opentelemetry-instrumentation-pymemcache + ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + +## Version 0.10b0 + +Released 2020-06-23 + +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-pymemcache/LICENSE b/instrumentation/opentelemetry-instrumentation-pymemcache/LICENSE similarity index 100% rename from ext/opentelemetry-ext-pymemcache/LICENSE rename to instrumentation/opentelemetry-instrumentation-pymemcache/LICENSE diff --git a/ext/opentelemetry-ext-pymemcache/MANIFEST.IN b/instrumentation/opentelemetry-instrumentation-pymemcache/MANIFEST.IN similarity index 100% rename from ext/opentelemetry-ext-pymemcache/MANIFEST.IN rename to instrumentation/opentelemetry-instrumentation-pymemcache/MANIFEST.IN diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/README.rst b/instrumentation/opentelemetry-instrumentation-pymemcache/README.rst new file mode 100644 index 0000000000..f126f4246d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/README.rst @@ -0,0 +1,20 @@ +OpenTelemetry pymemcache Instrumentation +======================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pymemcache.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-pymemcache/ + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-pymemcache + + +References +---------- +* `OpenTelemetry Pymemcache Instrumentation `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-pymemcache/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg similarity index 84% rename from ext/opentelemetry-ext-pymemcache/setup.cfg rename to instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg index 798988461c..aff71c8245 100644 --- a/ext/opentelemetry-ext-pymemcache/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-pymemcache -description = OpenTelemetry pymemcache integration +name = opentelemetry-instrumentation-pymemcache +description = OpenTelemetry pymemcache instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-pymemcache +url = https://github.com/open-telemetry/opentelemetry-python/instrumentation/opentelemetry-instrumentation-pymemcache platforms = any license = Apache-2.0 classifiers = @@ -54,4 +54,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - pymemcache = opentelemetry.ext.pymemcache:PymemcacheInstrumentor + pymemcache = opentelemetry.instrumentation.pymemcache:PymemcacheInstrumentor diff --git a/ext/opentelemetry-ext-pymemcache/setup.py b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.py similarity index 88% rename from ext/opentelemetry-ext-pymemcache/setup.py rename to instrumentation/opentelemetry-instrumentation-pymemcache/setup.py index 4b730b4f31..46bf607933 100644 --- a/ext/opentelemetry-ext-pymemcache/setup.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.py @@ -18,7 +18,12 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "pymemcache", "version.py" + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "pymemcache", + "version.py", ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/__init__.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py similarity index 97% rename from ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/__init__.py rename to instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py index e63bbe6f13..332e92ccdd 100644 --- a/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py @@ -26,7 +26,7 @@ from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.ext.pymemcache import PymemcacheInstrumentor + from opentelemetry.instrumentation.pymemcache import PymemcacheInstrumentor trace.set_tracer_provider(TracerProvider()) PymemcacheInstrumentor().instrument() from pymemcache.client.base import Client @@ -44,8 +44,8 @@ from wrapt import ObjectProxy from wrapt import wrap_function_wrapper as _wrap -from opentelemetry.ext.pymemcache.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.pymemcache.version import __version__ from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import SpanKind, get_tracer diff --git a/ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py similarity index 100% rename from ext/opentelemetry-ext-pymemcache/src/opentelemetry/ext/pymemcache/version.py rename to instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py diff --git a/ext/opentelemetry-ext-pymemcache/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-pymemcache/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-pymemcache/tests/__init__.py diff --git a/ext/opentelemetry-ext-pymemcache/tests/test_pymemcache.py b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py similarity index 99% rename from ext/opentelemetry-ext-pymemcache/tests/test_pymemcache.py rename to instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py index aea4ad82f1..794da9972b 100644 --- a/ext/opentelemetry-ext-pymemcache/tests/test_pymemcache.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py @@ -22,7 +22,7 @@ ) from opentelemetry import trace as trace_api -from opentelemetry.ext.pymemcache import PymemcacheInstrumentor +from opentelemetry.instrumentation.pymemcache import PymemcacheInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer from opentelemetry.trace.status import StatusCanonicalCode diff --git a/ext/opentelemetry-ext-pymemcache/tests/utils.py b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/utils.py similarity index 100% rename from ext/opentelemetry-ext-pymemcache/tests/utils.py rename to instrumentation/opentelemetry-instrumentation-pymemcache/tests/utils.py diff --git a/ext/opentelemetry-ext-pymongo/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md similarity index 72% rename from ext/opentelemetry-ext-pymongo/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md index 83a310c143..59f8e35551 100644 --- a/ext/opentelemetry-ext-pymongo/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-pymongo + ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ## 0.7b1 Released 2020-05-12 diff --git a/ext/opentelemetry-ext-pymongo/LICENSE b/instrumentation/opentelemetry-instrumentation-pymongo/LICENSE similarity index 100% rename from ext/opentelemetry-ext-pymongo/LICENSE rename to instrumentation/opentelemetry-instrumentation-pymongo/LICENSE diff --git a/ext/opentelemetry-ext-pymongo/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-pymongo/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-pymongo/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-pymongo/MANIFEST.in diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/README.rst b/instrumentation/opentelemetry-instrumentation-pymongo/README.rst new file mode 100644 index 0000000000..7791810e97 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymongo/README.rst @@ -0,0 +1,21 @@ +OpenTelemetry pymongo Instrumentation +===================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pymongo.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-pymongo/ + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-pymongo + + +References +---------- +* `OpenTelemetry pymongo Instrumentation `_ +* `OpenTelemetry Project `_ + diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg similarity index 87% rename from ext/opentelemetry-ext-pymongo/setup.cfg rename to instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg index 4aae442aa1..a480f05d6f 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-pymongo -description = OpenTelemetry pymongo integration +name = opentelemetry-instrumentation-pymongo +description = OpenTelemetry pymongo instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-pymongo +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-pymongo platforms = any license = Apache-2.0 classifiers = @@ -53,4 +53,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - pymongo = opentelemetry.ext.pymongo:PymongoInstrumentor + pymongo = opentelemetry.instrumentation.pymongo:PymongoInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/setup.py b/instrumentation/opentelemetry-instrumentation-pymongo/setup.py new file mode 100644 index 0000000000..7b862ae2aa --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymongo/setup.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "pymongo", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py similarity index 97% rename from ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py rename to instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py index fa8a599539..0d5ddc10ee 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py @@ -26,7 +26,7 @@ from pymongo import MongoClient from opentelemetry import trace from opentelemetry.trace import TracerProvider - from opentelemetry.ext.pymongo import PymongoInstrumentor + from opentelemetry.instrumentation.pymongo import PymongoInstrumentor trace.set_tracer_provider(TracerProvider()) @@ -43,8 +43,8 @@ from pymongo import monitoring from opentelemetry import trace -from opentelemetry.ext.pymongo.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.pymongo.version import __version__ from opentelemetry.trace import SpanKind, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py similarity index 100% rename from ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py rename to instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py diff --git a/ext/opentelemetry-ext-pymongo/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-pymongo/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-pymongo/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-pymongo/tests/__init__.py diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py b/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py similarity index 98% rename from ext/opentelemetry-ext-pymongo/tests/test_pymongo.py rename to instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py index abb4e8ab50..57f91e131b 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py @@ -15,7 +15,10 @@ from unittest import mock from opentelemetry import trace as trace_api -from opentelemetry.ext.pymongo import CommandTracer, PymongoInstrumentor +from opentelemetry.instrumentation.pymongo import ( + CommandTracer, + PymongoInstrumentor, +) from opentelemetry.test.test_base import TestBase diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md new file mode 100644 index 0000000000..f9b59fcf7a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## Unreleased + +- Change package name to opentelemetry-instrumentation-pymysql + ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + +## 0.7b1 + +Released 2020-05-12 + +- Initial release \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/README.rst b/instrumentation/opentelemetry-instrumentation-pymysql/README.rst new file mode 100644 index 0000000000..0b566d2a94 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymysql/README.rst @@ -0,0 +1,20 @@ +OpenTelemetry PyMySQL Instrumentation +===================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pymysql.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-pymysql/ + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-pymysql + + +References +---------- +* `OpenTelemetry PyMySQL Instrumentation `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-pymysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg similarity index 85% rename from ext/opentelemetry-ext-pymysql/setup.cfg rename to instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg index 9d8ccdff6f..b00276c9d2 100644 --- a/ext/opentelemetry-ext-pymysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-pymysql -description = OpenTelemetry PyMySQL integration +name = opentelemetry-instrumentation-pymysql +description = OpenTelemetry PyMySQL instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-pymysql +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-pymysql platforms = any license = Apache-2.0 classifiers = @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.12.dev0 - opentelemetry-ext-dbapi == 0.12.dev0 + opentelemetry-instrumentation-dbapi == 0.12.dev0 opentelemetry-instrumentation == 0.12.dev0 PyMySQL ~= 0.9.3 @@ -54,4 +54,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - pymysql = opentelemetry.ext.pymysql:PyMySQLInstrumentor + pymysql = opentelemetry.instrumentation.pymysql:PyMySQLInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/setup.py b/instrumentation/opentelemetry-instrumentation-pymysql/setup.py new file mode 100644 index 0000000000..74cd948fee --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymysql/setup.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "pymysql", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py similarity index 93% rename from ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py rename to instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py index e44e08e189..5d299505d6 100644 --- a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py @@ -25,7 +25,7 @@ import pymysql from opentelemetry import trace - from opentelemetry.ext.pymysql import PyMySQLInstrumentor + from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor from opentelemetry.sdk.trace import TracerProvider trace.set_tracer_provider(TracerProvider()) @@ -45,9 +45,9 @@ import pymysql -from opentelemetry.ext import dbapi -from opentelemetry.ext.pymysql.version import __version__ +from opentelemetry.instrumentation import dbapi from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.pymysql.version import __version__ class PyMySQLInstrumentor(BaseInstrumentor): diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py similarity index 100% rename from ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py rename to instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py diff --git a/ext/opentelemetry-ext-pymysql/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-pymysql/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-pymysql/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-pymysql/tests/__init__.py diff --git a/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py similarity index 94% rename from ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py rename to instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py index 60c060f117..35c9f4d32b 100644 --- a/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py @@ -16,8 +16,8 @@ import pymysql -import opentelemetry.ext.pymysql -from opentelemetry.ext.pymysql import PyMySQLInstrumentor +import opentelemetry.instrumentation.pymysql +from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor from opentelemetry.sdk import resources from opentelemetry.test.test_base import TestBase @@ -43,7 +43,9 @@ def test_instrumentor(self, mock_connect): span = spans_list[0] # Check version and name in span's instrumentation info - self.check_span_instrumentation_info(span, opentelemetry.ext.pymysql) + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.pymysql + ) # check that no spans are generated after uninstrument PyMySQLInstrumentor().uninstrument() diff --git a/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md new file mode 100644 index 0000000000..79c4e6bf92 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## Unreleased + +- Change package name to opentelemetry-instrumentation-redis + ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + +## 0.7b1 + +Released 2020-05-12 + +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-redis/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-redis/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-redis/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-redis/MANIFEST.in diff --git a/instrumentation/opentelemetry-instrumentation-redis/README.rst b/instrumentation/opentelemetry-instrumentation-redis/README.rst new file mode 100644 index 0000000000..289a0be0c3 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-redis/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry Redis Instrumentation +=================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-redis.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-redis/ + +This library allows tracing requests made by the Redis library. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-redis + + +References +---------- + +* `OpenTelemetry Redis Instrumentation `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-redis/setup.cfg b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg similarity index 88% rename from ext/opentelemetry-ext-redis/setup.cfg rename to instrumentation/opentelemetry-instrumentation-redis/setup.cfg index 0b245f341a..27793a5bed 100644 --- a/ext/opentelemetry-ext-redis/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-redis -description = Redis tracing for OpenTelemetry +name = opentelemetry-instrumentation-redis +description = OpenTelemetry Redis instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-redis +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-redis platforms = any license = Apache-2.0 classifiers = @@ -55,4 +55,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - redis = opentelemetry.ext.redis:RedisInstrumentor + redis = opentelemetry.instrumentation.redis:RedisInstrumentor diff --git a/ext/opentelemetry-ext-asyncpg/setup.py b/instrumentation/opentelemetry-instrumentation-redis/setup.py similarity index 91% rename from ext/opentelemetry-ext-asyncpg/setup.py rename to instrumentation/opentelemetry-instrumentation-redis/setup.py index 8172205c1a..df80a8fd1a 100644 --- a/ext/opentelemetry-ext-asyncpg/setup.py +++ b/instrumentation/opentelemetry-instrumentation-redis/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "asyncpg", "version.py" + BASE_DIR, "src", "opentelemetry", "instrumentation", "redis", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/__init__.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py similarity index 96% rename from ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/__init__.py rename to instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py index 0b7f6e28eb..fef856041e 100644 --- a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py @@ -28,7 +28,7 @@ .. code:: python from opentelemetry import trace - from opentelemetry.ext.redis import RedisInstrumentor + from opentelemetry.instrumentation.redis import RedisInstrumentor from opentelemetry.sdk.trace import TracerProvider import redis @@ -49,12 +49,12 @@ from wrapt import ObjectProxy, wrap_function_wrapper from opentelemetry import trace -from opentelemetry.ext.redis.util import ( +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.redis.util import ( _extract_conn_attributes, _format_command_args, ) -from opentelemetry.ext.redis.version import __version__ -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.redis.version import __version__ from opentelemetry.instrumentation.utils import unwrap _DEFAULT_SERVICE = "redis" diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/util.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py similarity index 100% rename from ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/util.py rename to instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py similarity index 100% rename from ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py rename to instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py diff --git a/ext/opentelemetry-ext-redis/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-redis/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-redis/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-redis/tests/__init__.py diff --git a/ext/opentelemetry-ext-redis/tests/test_redis.py b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py similarity index 96% rename from ext/opentelemetry-ext-redis/tests/test_redis.py rename to instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py index b36e96a627..86b2459c11 100644 --- a/ext/opentelemetry-ext-redis/tests/test_redis.py +++ b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py @@ -15,7 +15,7 @@ import redis -from opentelemetry.ext.redis import RedisInstrumentor +from opentelemetry.instrumentation.redis import RedisInstrumentor from opentelemetry.test.test_base import TestBase diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md new file mode 100644 index 0000000000..3022345ac7 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## Unreleased + +- Change package name to opentelemetry-instrumentation-sqlalchemy + ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + +## 0.7b1 + +Released 2020-05-12 + +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-sqlalchemy/LICENSE b/instrumentation/opentelemetry-instrumentation-sqlalchemy/LICENSE similarity index 100% rename from ext/opentelemetry-ext-sqlalchemy/LICENSE rename to instrumentation/opentelemetry-instrumentation-sqlalchemy/LICENSE diff --git a/ext/opentelemetry-ext-sqlalchemy/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-sqlalchemy/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-sqlalchemy/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-sqlalchemy/MANIFEST.in diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/README.rst b/instrumentation/opentelemetry-instrumentation-sqlalchemy/README.rst new file mode 100644 index 0000000000..f29cbe9fff --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/README.rst @@ -0,0 +1,24 @@ +OpenTelemetry SQLAlchemy Instrumentation +======================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-sqlalchemy.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-sqlalchemy/ + +This library allows tracing requests made by the SQLAlchemy library. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-sqlalchemy + + +References +---------- + +* `SQLAlchemy Project `_ +* `OpenTelemetry SQLAlchemy Tracing `_ +* `OpenTelemetry Project `_ \ No newline at end of file diff --git a/ext/opentelemetry-ext-sqlalchemy/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg similarity index 87% rename from ext/opentelemetry-ext-sqlalchemy/setup.cfg rename to instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg index 16416f37ef..e10a684d65 100644 --- a/ext/opentelemetry-ext-sqlalchemy/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-sqlalchemy -description = SQLAlchemy tracing for OpenTelemetry +name = opentelemetry-instrumentation-sqlalchemy +description = OpenTelemetry SQLAlchemy instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-sqlalchemy +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-sqlalchemy platforms = any license = Apache-2.0 classifiers = @@ -55,4 +55,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - sqlalchemy = opentelemetry.ext.sqlalchemy:SQLAlchemyInstrumentor + sqlalchemy = opentelemetry.instrumentation.sqlalchemy:SQLAlchemyInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.py new file mode 100644 index 0000000000..26d3ef4fc9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "sqlalchemy", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py similarity index 95% rename from ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py rename to instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py index 05d5e625ef..aad6dbfc07 100644 --- a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py @@ -29,7 +29,7 @@ from sqlalchemy import create_engine from opentelemetry import trace - from opentelemetry.ext.sqlalchemy import SQLAlchemyInstrumentor + from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.sdk.trace import TracerProvider import sqlalchemy @@ -47,12 +47,12 @@ import wrapt from wrapt import wrap_function_wrapper as _w -from opentelemetry.ext.sqlalchemy.engine import ( +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.sqlalchemy.engine import ( EngineTracer, _get_tracer, _wrap_create_engine, ) -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py similarity index 98% rename from ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/engine.py rename to instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py index 890bbc7c8d..df80c4841c 100644 --- a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/engine.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py @@ -15,7 +15,7 @@ from sqlalchemy.event import listen from opentelemetry import trace -from opentelemetry.ext.sqlalchemy.version import __version__ +from opentelemetry.instrumentation.sqlalchemy.version import __version__ from opentelemetry.trace.status import Status, StatusCanonicalCode # Network attribute semantic convention here: diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py similarity index 100% rename from ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py rename to instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py diff --git a/ext/opentelemetry-ext-sqlalchemy/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-sqlalchemy/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/__init__.py diff --git a/ext/opentelemetry-ext-sqlalchemy/tests/test_sqlalchemy.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py similarity index 95% rename from ext/opentelemetry-ext-sqlalchemy/tests/test_sqlalchemy.py rename to instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py index 858cc652ef..06593da94f 100644 --- a/ext/opentelemetry-ext-sqlalchemy/tests/test_sqlalchemy.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py @@ -14,7 +14,7 @@ from sqlalchemy import create_engine -from opentelemetry.ext.sqlalchemy import SQLAlchemyInstrumentor +from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.test.test_base import TestBase diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md new file mode 100644 index 0000000000..7804da9051 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## Unreleased + +- Change package name to opentelemetry-instrumentation-sqlite3 + ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + +## 0.8b0 + +Released 2020-05-27 + +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-sqlite3/LICENSE b/instrumentation/opentelemetry-instrumentation-sqlite3/LICENSE similarity index 100% rename from ext/opentelemetry-ext-sqlite3/LICENSE rename to instrumentation/opentelemetry-instrumentation-sqlite3/LICENSE diff --git a/ext/opentelemetry-ext-sqlite3/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-sqlite3/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-sqlite3/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-sqlite3/MANIFEST.in diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/README.rst b/instrumentation/opentelemetry-instrumentation-sqlite3/README.rst new file mode 100644 index 0000000000..0d2aa2dd99 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/README.rst @@ -0,0 +1,21 @@ +OpenTelemetry SQLite3 Instrumentation +===================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-sqlite3.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-sqlite3/ + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-sqlite3 + + +References +---------- +* `OpenTelemetry SQLite3 Instrumentation `_ +* `OpenTelemetry Project `_ + diff --git a/ext/opentelemetry-ext-sqlite3/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg similarity index 85% rename from ext/opentelemetry-ext-sqlite3/setup.cfg rename to instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg index 51f6b2f30d..841c8d3b17 100644 --- a/ext/opentelemetry-ext-sqlite3/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-sqlite3 -description = OpenTelemetry SQLite3 integration +name = opentelemetry-instrumentation-sqlite3 +description = OpenTelemetry SQLite3 instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-sqlite3 +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-sqlite3 platforms = any license = Apache-2.0 classifiers = @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.12.dev0 - opentelemetry-ext-dbapi == 0.12.dev0 + opentelemetry-instrumentation-dbapi == 0.12.dev0 opentelemetry-instrumentation == 0.12.dev0 wrapt >= 1.0.0, < 2.0.0 @@ -54,4 +54,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - sqlite3 = opentelemetry.ext.sqlite3:SQLite3Instrumentor + sqlite3 = opentelemetry.instrumentation.sqlite3:SQLite3Instrumentor diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.py b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.py new file mode 100644 index 0000000000..67ce350fab --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "sqlite3", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py similarity index 93% rename from ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py rename to instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py index 3cdfd88f75..568e83f0d6 100644 --- a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py @@ -26,7 +26,7 @@ import sqlite3 from opentelemetry import trace from opentelemetry.trace import TracerProvider - from opentelemetry.ext.sqlite3 import SQLite3Instrumentor + from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor trace.set_tracer_provider(TracerProvider()) @@ -44,9 +44,9 @@ import sqlite3 -from opentelemetry.ext import dbapi -from opentelemetry.ext.sqlite3.version import __version__ +from opentelemetry.instrumentation import dbapi from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.sqlite3.version import __version__ from opentelemetry.trace import get_tracer diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py similarity index 100% rename from ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py rename to instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py diff --git a/ext/opentelemetry-ext-sqlite3/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlite3/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-sqlite3/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-sqlite3/tests/__init__.py diff --git a/ext/opentelemetry-ext-sqlite3/tests/test_sqlite3.py b/instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py similarity index 97% rename from ext/opentelemetry-ext-sqlite3/tests/test_sqlite3.py rename to instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py index 7f4793bda3..a4444e7d93 100644 --- a/ext/opentelemetry-ext-sqlite3/tests/test_sqlite3.py +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py @@ -15,7 +15,7 @@ import sqlite3 from opentelemetry import trace as trace_api -from opentelemetry.ext.sqlite3 import SQLite3Instrumentor +from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor from opentelemetry.test.test_base import TestBase diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 857ceb84fe..7b512a84ce 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -26,27 +26,27 @@ # target library to desired instrumentor path/versioned package name instrumentations = { "asgi": "opentelemetry-instrumentation-asgi>=0.11b0", - "asyncpg": "opentelemetry-ext-asyncpg>=0.11b0", + "asyncpg": "opentelemetry-instrumentation-asyncpg>=0.11b0", "boto": "opentelemetry-ext-boto>=0.11b0", "botocore": "opentelemetry-ext-botocore>=0.11b0", "celery": "opentelemetry-ext-celery>=0.11b0", - "dbapi": "opentelemetry-ext-dbapi>=0.8b0", + "dbapi": "opentelemetry-instrumentation-dbapi>=0.8b0", "django": "opentelemetry-instrumentation-django>=0.8b0", "elasticsearch": "opentelemetry-ext-elasticsearch>=0.11b0", "fastapi": "opentelemetry-instrumentation-fastapi>=0.11b0", "flask": "opentelemetry-instrumentation-flask>=0.8b0", "grpc": "opentelemetry-ext-grpc>=0.8b0", "jinja2": "opentelemetry-ext-jinja2>=0.8b0", - "mysql": "opentelemetry-ext-mysql>=0.8b0", - "psycopg2": "opentelemetry-ext-psycopg2>=0.8b0", - "pymemcache": "opentelemetry-ext-pymemcache>=0.11b0", - "pymongo": "opentelemetry-ext-pymongo>=0.8b0", - "pymysql": "opentelemetry-ext-pymysql>=0.8b0", + "mysql": "opentelemetry-instrumentation-mysql>=0.8b0", + "psycopg2": "opentelemetry-instrumentation-psycopg2>=0.8b0", + "pymemcache": "opentelemetry-instrumentation-pymemcache>=0.11b0", + "pymongo": "opentelemetry-instrumentation-pymongo>=0.8b0", + "pymysql": "opentelemetry-instrumentation-pymysql>=0.8b0", "pyramid": "opentelemetry-instrumentation-pyramid>=0.11b0", - "redis": "opentelemetry-ext-redis>=0.8b0", + "redis": "opentelemetry-instrumentation-redis>=0.8b0", "requests": "opentelemetry-instrumentation-requests>=0.8b0", - "sqlalchemy": "opentelemetry-ext-sqlalchemy>=0.8b0", - "sqlite3": "opentelemetry-ext-sqlite3>=0.11b0", + "sqlalchemy": "opentelemetry-instrumentation-sqlalchemy>=0.8b0", + "sqlite3": "opentelemetry-instrumentation-sqlite3>=0.11b0", "starlette": "opentelemetry-instrumentation-starlette>=0.11b0", "wsgi": "opentelemetry-instrumentation-wsgi>=0.8b0", } @@ -54,27 +54,27 @@ # relevant instrumentors and tracers to uninstall and check for conflicts for target libraries libraries = { "asgi": ("opentelemetry-instrumentation-asgi",), - "asyncpg": ("opentelemetry-ext-asyncpg",), + "asyncpg": ("opentelemetry-instrumentation-asyncpg",), "boto": ("opentelemetry-ext-boto",), "botocore": ("opentelemetry-ext-botocore",), "celery": ("opentelemetry-ext-celery",), - "dbapi": ("opentelemetry-ext-dbapi",), + "dbapi": ("opentelemetry-instrumentation-dbapi",), "django": ("opentelemetry-instrumentation-django",), "elasticsearch": ("opentelemetry-ext-elasticsearch",), "fastapi": ("opentelemetry-instrumentation-fastapi",), "flask": ("opentelemetry-instrumentation-flask",), "grpc": ("opentelemetry-ext-grpc",), "jinja2": ("opentelemetry-ext-jinja2",), - "mysql": ("opentelemetry-ext-mysql",), - "psycopg2": ("opentelemetry-ext-psycopg2",), - "pymemcache": ("opentelemetry-ext-pymemcache",), - "pymongo": ("opentelemetry-ext-pymongo",), - "pymysql": ("opentelemetry-ext-pymysql",), + "mysql": ("opentelemetry-instrumentation-mysql",), + "psycopg2": ("opentelemetry-instrumentation-psycopg2",), + "pymemcache": ("opentelemetry-instrumentation-pymemcache",), + "pymongo": ("opentelemetry-instrumentation-pymongo",), + "pymysql": ("opentelemetry-instrumentation-pymysql",), "pyramid": ("opentelemetry-instrumentation-pyramid",), - "redis": ("opentelemetry-ext-redis",), + "redis": ("opentelemetry-instrumentation-redis",), "requests": ("opentelemetry-instrumentation-requests",), - "sqlalchemy": ("opentelemetry-ext-sqlalchemy",), - "sqlite3": ("opentelemetry-ext-sqlite3",), + "sqlalchemy": ("opentelemetry-instrumentation-sqlalchemy",), + "sqlite3": ("opentelemetry-instrumentation-sqlite3",), "starlette": ("opentelemetry-instrumentation-starlette",), "wsgi": ("opentelemetry-instrumentation-wsgi",), } diff --git a/tox.ini b/tox.ini index 6dab08f703..8528b63f60 100644 --- a/tox.ini +++ b/tox.ini @@ -44,7 +44,7 @@ envlist = py3{4,5,6,7,8}-test-instrumentation-django pypy3-test-instrumentation-django - ; opentelemetry-ext-dbapi + ; opentelemetry-instrumentation-dbapi py3{4,5,6,7,8}-test-instrumentation-dbapi pypy3-test-instrumentation-dbapi @@ -85,7 +85,7 @@ envlist = ; opentelemetry-exporter-datadog py3{5,6,7,8}-test-exporter-datadog - ; opentelemetry-ext-mysql + ; opentelemetry-instrumentation-mysql py3{4,5,6,7,8}-test-instrumentation-mysql pypy3-test-instrumentation-mysql @@ -101,19 +101,19 @@ envlist = py3{4,5,6,7,8}-test-exporter-prometheus pypy3-test-exporter-prometheus - ; opentelemetry-ext-psycopg2 + ; opentelemetry-instrumentation-psycopg2 py3{4,5,6,7,8}-test-instrumentation-psycopg2 ; ext-psycopg2 intentionally excluded from pypy3 - ; opentelemetry-ext-pymemcache + ; opentelemetry-instrumentation-pymemcache py3{4,5,6,7,8}-test-instrumentation-pymemcache pypy3-test-instrumentation-pymemcache - ; opentelemetry-ext-pymongo + ; opentelemetry-instrumentation-pymongo py3{4,5,6,7,8}-test-instrumentation-pymongo pypy3-test-instrumentation-pymongo - ; opentelemetry-ext-pymysql + ; opentelemetry-instrumentation-pymysql py3{4,5,6,7,8}-test-instrumentation-pymysql pypy3-test-instrumentation-pymysql @@ -125,11 +125,11 @@ envlist = py3{5,6,7,8}-test-instrumentation-asgi pypy3-test-instrumentation-asgi - ; opentelemetry-ext-asyncpg + ; opentelemetry-instrumentation-asyncpg py3{5,6,7,8}-test-instrumentation-asyncpg ; ext-asyncpg intentionally excluded from pypy3 - ; opentelemetry-ext-sqlite3 + ; opentelemetry-instrumentation-sqlite3 py3{4,5,6,7,8}-test-instrumentation-sqlite3 pypy3-test-instrumentation-sqlite3 @@ -148,11 +148,11 @@ envlist = ; opentelemetry-ext-grpc py3{5,6,7,8}-test-instrumentation-grpc - ; opentelemetry-ext-sqlalchemy + ; opentelemetry-instrumentation-sqlalchemy py3{4,5,6,7,8}-test-instrumentation-sqlalchemy pypy3-test-instrumentation-sqlalchemy - ; opentelemetry-ext-redis + ; opentelemetry-instrumentation-redis py3{4,5,6,7,8}-test-instrumentation-redis pypy3-test-instrumentation-redis @@ -200,11 +200,11 @@ changedir = test-instrumentation-aiohttp-client: instrumentation/opentelemetry-instrumentation-aiohttp-client/tests test-instrumentation-aiopg: instrumentation/opentelemetry-instrumentation-aiopg/tests test-instrumentation-asgi: instrumentation/opentelemetry-instrumentation-asgi/tests - test-instrumentation-asyncpg: ext/opentelemetry-ext-asyncpg/tests + test-instrumentation-asyncpg: instrumentation/opentelemetry-instrumentation-asyncpg/tests test-instrumentation-boto: ext/opentelemetry-ext-boto/tests test-instrumentation-botocore: ext/opentelemetry-ext-botocore/tests test-instrumentation-celery: ext/opentelemetry-ext-celery/tests - test-instrumentation-dbapi: ext/opentelemetry-ext-dbapi/tests + test-instrumentation-dbapi: instrumentation/opentelemetry-instrumentation-dbapi/tests test-instrumentation-django: instrumentation/opentelemetry-instrumentation-django/tests test-instrumentation-example-app: docs/examples/opentelemetry-example-app/tests test-instrumentation-elasticsearch{2,5,6,7}: ext/opentelemetry-ext-elasticsearch/tests @@ -212,16 +212,16 @@ changedir = test-instrumentation-flask: instrumentation/opentelemetry-instrumentation-flask/tests test-instrumentation-grpc: ext/opentelemetry-ext-grpc/tests test-instrumentation-jinja2: ext/opentelemetry-ext-jinja2/tests - test-instrumentation-mysql: ext/opentelemetry-ext-mysql/tests - test-instrumentation-psycopg2: ext/opentelemetry-ext-psycopg2/tests - test-instrumentation-pymemcache: ext/opentelemetry-ext-pymemcache/tests - test-instrumentation-pymongo: ext/opentelemetry-ext-pymongo/tests - test-instrumentation-pymysql: ext/opentelemetry-ext-pymysql/tests + test-instrumentation-mysql: instrumentation/opentelemetry-instrumentation-mysql/tests + test-instrumentation-psycopg2: instrumentation/opentelemetry-instrumentation-psycopg2/tests + test-instrumentation-pymemcache: instrumentation/opentelemetry-instrumentation-pymemcache/tests + test-instrumentation-pymongo: instrumentation/opentelemetry-instrumentation-pymongo/tests + test-instrumentation-pymysql: instrumentation/opentelemetry-instrumentation-pymysql/tests test-instrumentation-pyramid: instrumentation/opentelemetry-instrumentation-pyramid/tests - test-instrumentation-redis: ext/opentelemetry-ext-redis/tests + test-instrumentation-redis: instrumentation/opentelemetry-instrumentation-redis/tests test-instrumentation-requests: instrumentation/opentelemetry-instrumentation-requests/tests - test-instrumentation-sqlalchemy: ext/opentelemetry-ext-sqlalchemy/tests - test-instrumentation-sqlite3: ext/opentelemetry-ext-sqlite3/tests + test-instrumentation-sqlalchemy: instrumentation/opentelemetry-instrumentation-sqlalchemy/tests + test-instrumentation-sqlite3: instrumentation/opentelemetry-instrumentation-sqlite3/tests test-instrumentation-starlette: instrumentation/opentelemetry-instrumentation-starlette/tests test-instrumentation-system-metrics: ext/opentelemetry-ext-system-metrics/tests test-instrumentation-wsgi: instrumentation/opentelemetry-instrumentation-wsgi/tests @@ -254,7 +254,7 @@ commands_pre = wsgi,flask,django,pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi asgi,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi - asyncpg: pip install {toxinidir}/ext/opentelemetry-ext-asyncpg + asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg boto: pip install {toxinidir}/ext/opentelemetry-ext-botocore[test] boto: pip install {toxinidir}/ext/opentelemetry-ext-boto[test] @@ -263,13 +263,13 @@ commands_pre = botocore: pip install {toxinidir}/ext/opentelemetry-ext-botocore[test] - dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi[test] + dbapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi[test] django: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-django[test] fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi[test] - mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-mysql[test] + mysql: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql[test] opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus @@ -278,19 +278,19 @@ commands_pre = prometheus: pip install {toxinidir}/exporter/opentelemetry-exporter-prometheus - pymemcache: pip install {toxinidir}/ext/opentelemetry-ext-pymemcache[test] + pymemcache: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache[test] - pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo[test] + pymongo: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo[test] - psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-psycopg2 {toxinidir}/ext/opentelemetry-ext-psycopg2[test] + psycopg2: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2[test] - pymysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-pymysql[test] + pymysql: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql[test] pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid[test] - sqlite3: pip install {toxinidir}/ext/opentelemetry-ext-dbapi {toxinidir}/ext/opentelemetry-ext-sqlite3[test] + sqlite3: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3[test] - redis: pip install {toxinidir}/ext/opentelemetry-ext-redis[test] + redis: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-redis[test] requests: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-requests[test] @@ -300,7 +300,7 @@ commands_pre = aiohttp-client: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client - aiopg: pip install {toxinidir}/ext/opentelemetry-ext-dbapi pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg[test] + aiopg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg[test] jaeger: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger @@ -311,7 +311,7 @@ commands_pre = zipkin: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin - sqlalchemy: pip install {toxinidir}/ext/opentelemetry-ext-sqlalchemy + sqlalchemy: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy system-metrics: pip install {toxinidir}/ext/opentelemetry-ext-system-metrics[test] @@ -412,16 +412,16 @@ commands_pre = -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/tests/util \ - -e {toxinidir}/ext/opentelemetry-ext-asyncpg \ + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg \ -e {toxinidir}/ext/opentelemetry-ext-celery \ - -e {toxinidir}/ext/opentelemetry-ext-dbapi \ - -e {toxinidir}/ext/opentelemetry-ext-mysql \ - -e {toxinidir}/ext/opentelemetry-ext-psycopg2 \ - -e {toxinidir}/ext/opentelemetry-ext-pymongo \ - -e {toxinidir}/ext/opentelemetry-ext-pymysql \ - -e {toxinidir}/ext/opentelemetry-ext-sqlalchemy \ + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi \ + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql \ + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2 \ + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo \ + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql \ + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy \ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg \ - -e {toxinidir}/ext/opentelemetry-ext-redis \ + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-redis \ -e {toxinidir}/ext/opentelemetry-ext-system-metrics \ -e {toxinidir}/exporter/opentelemetry-exporter-opencensus docker-compose up -d From 3143a4ba16af096584bf1bffbe2d673a87fecc34 Mon Sep 17 00:00:00 2001 From: MitchellDumovic Date: Mon, 3 Aug 2020 18:53:15 -0700 Subject: [PATCH 0488/1517] Change default Sampler to ParentOrElse(AlwaysOn) (#960) --- opentelemetry-sdk/CHANGELOG.md | 1 + opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index d4b46202e5..34032424ff 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Changed default Sampler to `ParentOrElse(AlwaysOn)` - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - Implement Views in metrics SDK diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 848f571e6a..b76ba49ad4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -859,7 +859,7 @@ def use_span( class TracerProvider(trace_api.TracerProvider): def __init__( self, - sampler: sampling.Sampler = trace_api.sampling.ALWAYS_ON, + sampler: sampling.Sampler = trace_api.sampling.DEFAULT_ON, resource: Resource = Resource.create_empty(), shutdown_on_exit: bool = True, active_span_processor: Union[ diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 02cc836e88..56bb9cfa57 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -344,6 +344,7 @@ def test_start_as_current_span_explicit(self): trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, + trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) From 57ca703c56ac226f42620a26afe9b2f50bb41217 Mon Sep 17 00:00:00 2001 From: Abhilash Gnan Date: Tue, 4 Aug 2020 18:22:22 +0200 Subject: [PATCH 0489/1517] Add HTTP user-agent in WSGI instrumentation (#964) --- .../src/opentelemetry/instrumentation/wsgi/__init__.py | 4 ++++ .../tests/test_wsgi_middleware.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index 8a2d0bec08..289016fe52 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -117,6 +117,10 @@ def collect_request_attributes(environ): if remote_host and remote_host != remote_addr: result["net.peer.name"] = remote_host + user_agent = environ.get("HTTP_USER_AGENT") + if user_agent is not None and len(user_agent) > 0: + result["http.user_agent"] = user_agent + setifnotnone(result, "net.peer.port", environ.get("REMOTE_PORT")) flavor = environ.get("SERVER_PROTOCOL", "") if flavor.upper().startswith(_HTTP_VERSION_PREFIX): diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index 5734027b0a..b06d915b53 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -313,6 +313,14 @@ def test_request_attributes_with_full_request_uri(self): expected.items(), ) + def test_http_user_agent_attribute(self): + self.environ["HTTP_USER_AGENT"] = "test-useragent" + expected = {"http.user_agent": "test-useragent"} + self.assertGreaterEqual( + otel_wsgi.collect_request_attributes(self.environ).items(), + expected.items(), + ) + def test_response_attributes(self): otel_wsgi.add_response_attributes(self.span, "404 Not Found", {}) expected = ( From eb8b1eaf5f13aace24b1b63e4936d38a8884c77e Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 4 Aug 2020 19:10:51 -0700 Subject: [PATCH 0490/1517] Rename remaining framework packages from "ext" to "instrumentation" (#969) --- README.md | 17 ++++-- docs/conf.py | 9 +-- .../grpc/hello_world_client.py | 8 +-- .../grpc/hello_world_server.py | 8 +-- .../grpc/route_guide_client.py | 8 +-- .../grpc/route_guide_server.py | 8 +-- docs/examples/opentracing/README.rst | 8 +-- docs/examples/opentracing/main.py | 2 +- docs/ext/boto/boto.rst | 7 --- docs/getting-started.rst | 6 +- docs/index.rst | 16 ++---- .../boto/boto.rst} | 4 +- docs/instrumentation/botocore/botocore.rst | 7 +++ .../celery/celery.rst | 2 +- docs/{ext => instrumentation}/grpc/grpc.rst | 2 +- .../jinja2/jinja2.rst | 2 +- .../opentracing_shim/opentracing_shim.rst | 2 +- .../system_metrics/system_metrics.rst | 2 +- eachdist.ini | 2 +- .../exporter/datadog/__init__.py | 2 +- .../exporter/datadog/exporter.py | 4 +- .../tests/test_datadog_exporter.py | 4 +- ext/opentelemetry-ext-jinja2/README.rst | 21 ------- ext/opentelemetry-ext-jinja2/setup.py | 26 --------- .../CHANGELOG.md | 9 --- ext/opentelemetry-ext-system-metrics/setup.py | 26 --------- .../CHANGELOG.md | 2 +- .../CHANGELOG.md | 3 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 8 +-- .../setup.cfg | 10 ++-- .../setup.py | 2 +- .../instrumentation}/boto/__init__.py | 6 +- .../instrumentation}/boto/version.py | 0 .../tests/__init__.py | 0 .../tests/conftest.py | 0 .../tests/test_boto_instrumentation.py | 2 +- .../CHANGELOG.md | 3 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 8 +-- .../setup.cfg | 8 +-- .../setup.py | 7 ++- .../instrumentation}/botocore/__init__.py | 4 +- .../instrumentation}/botocore/version.py | 0 .../tests/__init__.py | 0 .../tests/test_botocore_instrumentation.py | 2 +- .../CHANGELOG.md | 3 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 8 +-- .../setup.cfg | 6 +- .../setup.py | 2 +- .../instrumentation}/celery/__init__.py | 6 +- .../instrumentation}/celery/utils.py | 0 .../instrumentation}/celery/version.py | 0 .../tests/__init__.py | 0 .../tests/test_utils.py | 2 +- .../CHANGELOG.md | 2 +- .../CHANGELOG.md | 2 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 8 +-- .../setup.cfg | 8 +-- .../setup.py | 7 ++- .../elasticsearch/__init__.py | 4 +- .../instrumentation}/elasticsearch/version.py | 0 .../tests/__init__.py | 0 .../tests/helpers_es2.py | 0 .../tests/helpers_es5.py | 0 .../tests/helpers_es6.py | 0 .../tests/helpers_es7.py | 0 .../tests/test_elasticsearch.py | 10 ++-- .../CHANGELOG.md | 3 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 6 +- .../setup.cfg | 8 +-- .../setup.py | 2 +- .../instrumentation}/grpc/__init__.py | 13 +++-- .../instrumentation}/grpc/_client.py | 0 .../instrumentation}/grpc/_server.py | 0 .../instrumentation}/grpc/_utilities.py | 0 .../instrumentation}/grpc/grpcext/__init__.py | 0 .../grpc/grpcext/_interceptor.py | 0 .../instrumentation}/grpc/version.py | 0 .../tests}/__init__.py | 0 .../tests/_client.py | 0 .../tests/_server.py | 0 .../tests/protobuf/test_server.proto | 0 .../tests/protobuf/test_server_pb2.py | 0 .../tests/protobuf/test_server_pb2_grpc.py | 0 .../tests/test_client_interceptor.py | 24 +++++--- .../tests/test_server_interceptor.py | 17 ++++-- .../CHANGELOG.md | 3 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 21 +++++++ .../setup.cfg | 8 +-- .../setup.py | 2 +- .../instrumentation}/jinja2/__init__.py | 4 +- .../instrumentation}/jinja2/version.py | 0 .../tests/__init__.py | 0 .../tests/templates/base.html | 0 .../tests/templates/template.html | 0 .../tests/test_jinja2.py | 2 +- .../CHANGELOG.md | 2 +- .../CHANGELOG.md | 3 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 2 +- .../setup.cfg | 2 +- .../setup.py | 31 ++++++++++ .../opentracing_shim/__init__.py | 6 +- .../instrumentation}/opentracing_shim/util.py | 0 .../opentracing_shim/version.py | 0 .../tests/__init__.py | 0 .../tests/test_shim.py | 8 +-- .../tests/test_util.py | 2 +- .../tests/testbed/README.rst | 0 .../tests/testbed/__init__.py | 0 .../tests/testbed/otel_ot_shim_tracer.py | 2 +- .../test_active_span_replacement/README.rst | 0 .../test_active_span_replacement/__init__.py | 0 .../test_asyncio.py | 0 .../test_threads.py | 0 .../testbed/test_client_server/README.rst | 0 .../testbed/test_client_server/__init__.py | 0 .../test_client_server/test_asyncio.py | 0 .../test_client_server/test_threads.py | 0 .../test_common_request_handler/README.rst | 0 .../test_common_request_handler/__init__.py | 0 .../request_handler.py | 0 .../test_asyncio.py | 0 .../test_threads.py | 0 .../testbed/test_late_span_finish/README.rst | 0 .../testbed/test_late_span_finish/__init__.py | 0 .../test_late_span_finish/test_asyncio.py | 0 .../test_late_span_finish/test_threads.py | 0 .../test_listener_per_request/README.rst | 0 .../test_listener_per_request/__init__.py | 0 .../response_listener.py | 0 .../test_listener_per_request/test_asyncio.py | 0 .../test_listener_per_request/test_threads.py | 0 .../test_multiple_callbacks/README.rst | 0 .../test_multiple_callbacks/__init__.py | 0 .../test_multiple_callbacks/test_asyncio.py | 0 .../test_multiple_callbacks/test_threads.py | 0 .../testbed/test_nested_callbacks/README.rst | 0 .../testbed/test_nested_callbacks/__init__.py | 0 .../test_nested_callbacks/test_asyncio.py | 0 .../test_nested_callbacks/test_threads.py | 0 .../test_subtask_span_propagation/README.rst | 0 .../test_subtask_span_propagation/__init__.py | 0 .../test_asyncio.py | 0 .../test_threads.py | 0 .../tests/testbed/testcase.py | 0 .../tests/testbed/utils.py | 0 .../CHANGELOG.md | 2 +- .../CHANGELOG.md | 2 +- .../CHANGELOG.md | 2 +- .../CHANGELOG.md | 2 +- .../CHANGELOG.md | 2 +- .../CHANGELOG.md | 2 +- .../CHANGELOG.md | 2 +- .../CHANGELOG.md | 12 ++++ .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 8 +-- .../setup.cfg | 4 +- .../setup.py | 31 ++++++++++ .../system_metrics/__init__.py | 2 +- .../system_metrics/version.py | 0 .../tests/__init__.py | 0 .../tests/test_system_metrics.py | 2 +- .../instrumentation/bootstrap.py | 24 ++++---- scripts/coverage.sh | 4 +- .../tests/asyncpg/test_asyncpg_functional.py | 0 .../tests/celery/conftest.py | 2 +- .../tests/celery/test_celery_functional.py | 13 +++-- .../tests/check_availability.py | 0 .../tests/docker-compose.yml | 0 .../tests/mysql/test_mysql_functional.py | 0 .../test_opencensusexporter_functional.py | 0 .../tests/postgres/test_aiopg_functional.py | 0 .../tests/postgres/test_psycopg_functional.py | 0 .../tests/pymongo/test_pymongo_functional.py | 0 .../tests/pymysql/test_pymysql_functional.py | 0 .../tests/redis/test_redis_functional.py | 0 .../tests/sqlalchemy_tests}/__init__.py | 0 .../tests/sqlalchemy_tests/mixins.py | 0 .../tests/sqlalchemy_tests/test_instrument.py | 0 .../tests/sqlalchemy_tests/test_mysql.py | 0 .../tests/sqlalchemy_tests/test_postgres.py | 0 .../tests/sqlalchemy_tests/test_sqlite.py | 0 tox.ini | 56 +++++++++---------- 197 files changed, 374 insertions(+), 314 deletions(-) delete mode 100644 docs/ext/boto/boto.rst rename docs/{ext/botocore/botocore.rst => instrumentation/boto/boto.rst} (51%) create mode 100644 docs/instrumentation/botocore/botocore.rst rename docs/{ext => instrumentation}/celery/celery.rst (71%) rename docs/{ext => instrumentation}/grpc/grpc.rst (75%) rename docs/{ext => instrumentation}/jinja2/jinja2.rst (71%) rename docs/{ext => instrumentation}/opentracing_shim/opentracing_shim.rst (60%) rename docs/{ext => instrumentation}/system_metrics/system_metrics.rst (70%) delete mode 100644 ext/opentelemetry-ext-jinja2/README.rst delete mode 100644 ext/opentelemetry-ext-jinja2/setup.py delete mode 100644 ext/opentelemetry-ext-system-metrics/CHANGELOG.md delete mode 100644 ext/opentelemetry-ext-system-metrics/setup.py rename {ext/opentelemetry-ext-boto => instrumentation/opentelemetry-instrumentation-boto}/CHANGELOG.md (65%) rename {ext/opentelemetry-ext-boto => instrumentation/opentelemetry-instrumentation-boto}/LICENSE (100%) rename {ext/opentelemetry-ext-boto => instrumentation/opentelemetry-instrumentation-boto}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-boto => instrumentation/opentelemetry-instrumentation-boto}/README.rst (53%) rename {ext/opentelemetry-ext-boto => instrumentation/opentelemetry-instrumentation-boto}/setup.cfg (86%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-boto}/setup.py (92%) rename {ext/opentelemetry-ext-boto/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation}/boto/__init__.py (96%) rename {ext/opentelemetry-ext-boto/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation}/boto/version.py (100%) rename {ext/opentelemetry-ext-boto => instrumentation/opentelemetry-instrumentation-boto}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-boto => instrumentation/opentelemetry-instrumentation-boto}/tests/conftest.py (100%) rename {ext/opentelemetry-ext-boto => instrumentation/opentelemetry-instrumentation-boto}/tests/test_boto_instrumentation.py (99%) rename {ext/opentelemetry-ext-botocore => instrumentation/opentelemetry-instrumentation-botocore}/CHANGELOG.md (64%) rename {ext/opentelemetry-ext-botocore => instrumentation/opentelemetry-instrumentation-botocore}/LICENSE (100%) rename {ext/opentelemetry-ext-botocore => instrumentation/opentelemetry-instrumentation-botocore}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-botocore => instrumentation/opentelemetry-instrumentation-botocore}/README.rst (52%) rename {ext/opentelemetry-ext-botocore => instrumentation/opentelemetry-instrumentation-botocore}/setup.cfg (87%) rename {ext/opentelemetry-ext-botocore => instrumentation/opentelemetry-instrumentation-botocore}/setup.py (88%) rename {ext/opentelemetry-ext-botocore/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation}/botocore/__init__.py (97%) rename {ext/opentelemetry-ext-botocore/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation}/botocore/version.py (100%) rename {ext/opentelemetry-ext-botocore => instrumentation/opentelemetry-instrumentation-botocore}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-botocore => instrumentation/opentelemetry-instrumentation-botocore}/tests/test_botocore_instrumentation.py (99%) rename {ext/opentelemetry-ext-celery => instrumentation/opentelemetry-instrumentation-celery}/CHANGELOG.md (55%) rename {ext/opentelemetry-ext-celery => instrumentation/opentelemetry-instrumentation-celery}/LICENSE (100%) rename {ext/opentelemetry-ext-celery => instrumentation/opentelemetry-instrumentation-celery}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-celery => instrumentation/opentelemetry-instrumentation-celery}/README.rst (70%) rename {ext/opentelemetry-ext-celery => instrumentation/opentelemetry-instrumentation-celery}/setup.cfg (90%) rename {ext/opentelemetry-ext-elasticsearch => instrumentation/opentelemetry-instrumentation-celery}/setup.py (91%) rename {ext/opentelemetry-ext-celery/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation}/celery/__init__.py (97%) rename {ext/opentelemetry-ext-celery/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation}/celery/utils.py (100%) rename {ext/opentelemetry-ext-celery/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation}/celery/version.py (100%) rename {ext/opentelemetry-ext-celery => instrumentation/opentelemetry-instrumentation-celery}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-celery => instrumentation/opentelemetry-instrumentation-celery}/tests/test_utils.py (99%) rename {ext/opentelemetry-ext-elasticsearch => instrumentation/opentelemetry-instrumentation-elasticsearch}/CHANGELOG.md (62%) rename {ext/opentelemetry-ext-elasticsearch => instrumentation/opentelemetry-instrumentation-elasticsearch}/LICENSE (100%) rename {ext/opentelemetry-ext-elasticsearch => instrumentation/opentelemetry-instrumentation-elasticsearch}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-elasticsearch => instrumentation/opentelemetry-instrumentation-elasticsearch}/README.rst (55%) rename {ext/opentelemetry-ext-elasticsearch => instrumentation/opentelemetry-instrumentation-elasticsearch}/setup.cfg (86%) rename {ext/opentelemetry-ext-celery => instrumentation/opentelemetry-instrumentation-elasticsearch}/setup.py (88%) rename {ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation}/elasticsearch/__init__.py (97%) rename {ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation}/elasticsearch/version.py (100%) rename {ext/opentelemetry-ext-elasticsearch => instrumentation/opentelemetry-instrumentation-elasticsearch}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-elasticsearch => instrumentation/opentelemetry-instrumentation-elasticsearch}/tests/helpers_es2.py (100%) rename {ext/opentelemetry-ext-elasticsearch => instrumentation/opentelemetry-instrumentation-elasticsearch}/tests/helpers_es5.py (100%) rename {ext/opentelemetry-ext-elasticsearch => instrumentation/opentelemetry-instrumentation-elasticsearch}/tests/helpers_es6.py (100%) rename {ext/opentelemetry-ext-elasticsearch => instrumentation/opentelemetry-instrumentation-elasticsearch}/tests/helpers_es7.py (100%) rename {ext/opentelemetry-ext-elasticsearch => instrumentation/opentelemetry-instrumentation-elasticsearch}/tests/test_elasticsearch.py (97%) rename {ext/opentelemetry-ext-grpc => instrumentation/opentelemetry-instrumentation-grpc}/CHANGELOG.md (83%) rename {ext/opentelemetry-ext-grpc => instrumentation/opentelemetry-instrumentation-grpc}/LICENSE (100%) rename {ext/opentelemetry-ext-grpc => instrumentation/opentelemetry-instrumentation-grpc}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-grpc => instrumentation/opentelemetry-instrumentation-grpc}/README.rst (51%) rename {ext/opentelemetry-ext-grpc => instrumentation/opentelemetry-instrumentation-grpc}/setup.cfg (87%) rename {ext/opentelemetry-ext-grpc => instrumentation/opentelemetry-instrumentation-grpc}/setup.py (91%) rename {ext/opentelemetry-ext-grpc/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation}/grpc/__init__.py (93%) rename {ext/opentelemetry-ext-grpc/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation}/grpc/_client.py (100%) rename {ext/opentelemetry-ext-grpc/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation}/grpc/_server.py (100%) rename {ext/opentelemetry-ext-grpc/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation}/grpc/_utilities.py (100%) rename {ext/opentelemetry-ext-grpc/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation}/grpc/grpcext/__init__.py (100%) rename {ext/opentelemetry-ext-grpc/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation}/grpc/grpcext/_interceptor.py (100%) rename {ext/opentelemetry-ext-grpc/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation}/grpc/version.py (100%) rename {ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests => instrumentation/opentelemetry-instrumentation-grpc/tests}/__init__.py (100%) rename {ext/opentelemetry-ext-grpc => instrumentation/opentelemetry-instrumentation-grpc}/tests/_client.py (100%) rename {ext/opentelemetry-ext-grpc => instrumentation/opentelemetry-instrumentation-grpc}/tests/_server.py (100%) rename {ext/opentelemetry-ext-grpc => instrumentation/opentelemetry-instrumentation-grpc}/tests/protobuf/test_server.proto (100%) rename {ext/opentelemetry-ext-grpc => instrumentation/opentelemetry-instrumentation-grpc}/tests/protobuf/test_server_pb2.py (100%) rename {ext/opentelemetry-ext-grpc => instrumentation/opentelemetry-instrumentation-grpc}/tests/protobuf/test_server_pb2_grpc.py (100%) rename {ext/opentelemetry-ext-grpc => instrumentation/opentelemetry-instrumentation-grpc}/tests/test_client_interceptor.py (93%) rename {ext/opentelemetry-ext-grpc => instrumentation/opentelemetry-instrumentation-grpc}/tests/test_server_interceptor.py (95%) rename {ext/opentelemetry-ext-jinja2 => instrumentation/opentelemetry-instrumentation-jinja2}/CHANGELOG.md (53%) rename {ext/opentelemetry-ext-jinja2 => instrumentation/opentelemetry-instrumentation-jinja2}/LICENSE (100%) rename {ext/opentelemetry-ext-jinja2 => instrumentation/opentelemetry-instrumentation-jinja2}/MANIFEST.in (100%) create mode 100644 instrumentation/opentelemetry-instrumentation-jinja2/README.rst rename {ext/opentelemetry-ext-jinja2 => instrumentation/opentelemetry-instrumentation-jinja2}/setup.cfg (87%) rename {ext/opentelemetry-ext-boto => instrumentation/opentelemetry-instrumentation-jinja2}/setup.py (91%) rename {ext/opentelemetry-ext-jinja2/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation}/jinja2/__init__.py (96%) rename {ext/opentelemetry-ext-jinja2/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation}/jinja2/version.py (100%) rename {ext/opentelemetry-ext-jinja2 => instrumentation/opentelemetry-instrumentation-jinja2}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-jinja2 => instrumentation/opentelemetry-instrumentation-jinja2}/tests/templates/base.html (100%) rename {ext/opentelemetry-ext-jinja2 => instrumentation/opentelemetry-instrumentation-jinja2}/tests/templates/template.html (100%) rename {ext/opentelemetry-ext-jinja2 => instrumentation/opentelemetry-instrumentation-jinja2}/tests/test_jinja2.py (99%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/CHANGELOG.md (63%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/LICENSE (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/README.rst (83%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/setup.cfg (95%) create mode 100644 instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.py rename {ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation}/opentracing_shim/__init__.py (99%) rename {ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation}/opentracing_shim/util.py (100%) rename {ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation}/opentracing_shim/version.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/test_shim.py (98%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/test_util.py (97%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/README.rst (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/__init__.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/otel_ot_shim_tracer.py (92%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_active_span_replacement/README.rst (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_active_span_replacement/__init__.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_active_span_replacement/test_asyncio.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_active_span_replacement/test_threads.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_client_server/README.rst (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_client_server/__init__.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_client_server/test_asyncio.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_client_server/test_threads.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_common_request_handler/README.rst (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_common_request_handler/__init__.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_common_request_handler/request_handler.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_common_request_handler/test_asyncio.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_common_request_handler/test_threads.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_late_span_finish/README.rst (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_late_span_finish/__init__.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_late_span_finish/test_asyncio.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_late_span_finish/test_threads.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_listener_per_request/README.rst (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_listener_per_request/__init__.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_listener_per_request/response_listener.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_listener_per_request/test_asyncio.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_listener_per_request/test_threads.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_multiple_callbacks/README.rst (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_multiple_callbacks/__init__.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_multiple_callbacks/test_asyncio.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_multiple_callbacks/test_threads.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_nested_callbacks/README.rst (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_nested_callbacks/__init__.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_nested_callbacks/test_asyncio.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_nested_callbacks/test_threads.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_subtask_span_propagation/README.rst (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_subtask_span_propagation/__init__.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_subtask_span_propagation/test_asyncio.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/test_subtask_span_propagation/test_threads.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/testcase.py (100%) rename {ext/opentelemetry-ext-opentracing-shim => instrumentation/opentelemetry-instrumentation-opentracing-shim}/tests/testbed/utils.py (100%) create mode 100644 instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md rename {ext/opentelemetry-ext-system-metrics => instrumentation/opentelemetry-instrumentation-system-metrics}/LICENSE (100%) rename {ext/opentelemetry-ext-system-metrics => instrumentation/opentelemetry-instrumentation-system-metrics}/MANIFEST.in (100%) rename {ext/opentelemetry-ext-system-metrics => instrumentation/opentelemetry-instrumentation-system-metrics}/README.rst (50%) rename {ext/opentelemetry-ext-system-metrics => instrumentation/opentelemetry-instrumentation-system-metrics}/setup.cfg (92%) create mode 100644 instrumentation/opentelemetry-instrumentation-system-metrics/setup.py rename {ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation}/system_metrics/__init__.py (99%) rename {ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext => instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation}/system_metrics/version.py (100%) rename {ext/opentelemetry-ext-system-metrics => instrumentation/opentelemetry-instrumentation-system-metrics}/tests/__init__.py (100%) rename {ext/opentelemetry-ext-system-metrics => instrumentation/opentelemetry-instrumentation-system-metrics}/tests/test_system_metrics.py (99%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/asyncpg/test_asyncpg_functional.py (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/celery/conftest.py (97%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/celery/test_celery_functional.py (97%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/check_availability.py (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/docker-compose.yml (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/mysql/test_mysql_functional.py (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/opencensus/test_opencensusexporter_functional.py (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/postgres/test_aiopg_functional.py (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/postgres/test_psycopg_functional.py (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/pymongo/test_pymongo_functional.py (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/pymysql/test_pymysql_functional.py (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/redis/test_redis_functional.py (100%) rename {ext/opentelemetry-ext-grpc/tests => tests/opentelemetry-docker-tests/tests/sqlalchemy_tests}/__init__.py (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/sqlalchemy_tests/mixins.py (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/sqlalchemy_tests/test_instrument.py (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/sqlalchemy_tests/test_mysql.py (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/sqlalchemy_tests/test_postgres.py (100%) rename {ext/opentelemetry-ext-docker-tests => tests/opentelemetry-docker-tests}/tests/sqlalchemy_tests/test_sqlite.py (100%) diff --git a/README.md b/README.md index a402e5c73e..cc6b7f10b6 100644 --- a/README.md +++ b/README.md @@ -63,12 +63,19 @@ pip install opentelemetry-sdk ``` The -[`ext/`](https://github.com/open-telemetry/opentelemetry-python/tree/master/ext) -directory includes OpenTelemetry integration packages, which can be installed -separately as: +[`instrumentation/`](https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation) +directory includes OpenTelemetry instrumentation packages, which can be installed separately as: ```sh -pip install opentelemetry-ext-{integration} +pip install opentelemetry-instrumentation-{instrumentation} +``` + +The +[`exporter/`](https://github.com/open-telemetry/opentelemetry-python/tree/master/exporter) +directory includes OpenTelemetry exporter packages, which can be installed separately as: + +```sh +pip install opentelemetry-exporter-{exporter} ``` To install the development versions of these packages instead, clone or fork @@ -78,7 +85,7 @@ install](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs) ```sh pip install -e ./opentelemetry-api pip install -e ./opentelemetry-sdk -pip install -e ./ext/opentelemetry-ext-{integration} +pip install -e ./ext/opentelemetry-instrumentation-{instrumentation} ``` ## Documentation diff --git a/docs/conf.py b/docs/conf.py index db836ea27e..4b9753c96c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,13 +37,6 @@ if isdir(join(exp, f)) ] -ext = "../ext" -ext_dirs = [ - os.path.abspath("/".join(["../ext", f, "src"])) - for f in listdir(ext) - if isdir(join(ext, f)) -] - instr = "../instrumentation" instr_dirs = [ os.path.abspath("/".join(["../instrumentation", f, "src"])) @@ -51,7 +44,7 @@ if isdir(join(instr, f)) ] -sys.path[:0] = source_dirs + exp_dirs + ext_dirs + instr_dirs +sys.path[:0] = source_dirs + exp_dirs + instr_dirs # -- Project information ----------------------------------------------------- diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py index 7457c63148..59dd1fa610 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py @@ -17,11 +17,11 @@ """The Python implementation of the GRPC helloworld.Greeter client. -Note that you need ``opentelemetry-ext-grpc`` and ``protobuf`` to be installed +Note that you need ``opentelemetry-instrumentation-grpc`` and ``protobuf`` to be installed to run these examples. To run this script in the context of the example app, install ``opentelemetry-example-app``:: - pip install -e ext/opentelemetry-ext-grpc/ + pip install -e ext/opentelemetry-instrumentation-grpc/ pip install -e docs/examples/opentelemetry-example-app Then run the server in one shell:: @@ -42,8 +42,8 @@ import grpc from opentelemetry import trace -from opentelemetry.ext.grpc import client_interceptor -from opentelemetry.ext.grpc.grpcext import intercept_channel +from opentelemetry.instrumentation.grpc import client_interceptor +from opentelemetry.instrumentation.grpc.grpcext import intercept_channel from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py index 858426a468..ae4562e581 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py @@ -17,11 +17,11 @@ """The Python implementation of the GRPC helloworld.Greeter server. -Note that you need ``opentelemetry-ext-grpc`` and ``protobuf`` to be installed +Note that you need ``opentelemetry-instrumentation-grpc`` and ``protobuf`` to be installed to run these examples. To run this script in the context of the example app, install ``opentelemetry-example-app``:: - pip install -e ext/opentelemetry-ext-grpc/ + pip install -e ext/opentelemetry-instrumentation-grpc/ pip install -e docs/examples/opentelemetry-example-app Then run the server in one shell:: @@ -42,8 +42,8 @@ import grpc from opentelemetry import trace -from opentelemetry.ext.grpc import server_interceptor -from opentelemetry.ext.grpc.grpcext import intercept_server +from opentelemetry.instrumentation.grpc import server_interceptor +from opentelemetry.instrumentation.grpc.grpcext import intercept_server from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py index d24875913d..110c68d113 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py @@ -17,11 +17,11 @@ """The Python implementation of the gRPC route guide client. -Note that you need ``opentelemetry-ext-grpc`` and ``protobuf`` to be installed +Note that you need ``opentelemetry-instrumentation-grpc`` and ``protobuf`` to be installed to run these examples. To run this script in the context of the example app, install ``opentelemetry-example-app``:: - pip install -e ext/opentelemetry-ext-grpc/ + pip install -e ext/opentelemetry-instrumentation-grpc/ pip install -e docs/examples/opentelemetry-example-app Then run the server in one shell:: @@ -43,8 +43,8 @@ import grpc from opentelemetry import trace -from opentelemetry.ext.grpc import client_interceptor -from opentelemetry.ext.grpc.grpcext import intercept_channel +from opentelemetry.instrumentation.grpc import client_interceptor +from opentelemetry.instrumentation.grpc.grpcext import intercept_channel from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py index 54a68b9f5a..a8b2a95e81 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py @@ -18,11 +18,11 @@ """The Python implementation of the gRPC route guide server. -Note that you need ``opentelemetry-ext-grpc`` and ``protobuf`` to be installed +Note that you need ``opentelemetry-instrumentation-grpc`` and ``protobuf`` to be installed to run these examples. To run this script in the context of the example app, install ``opentelemetry-example-app``:: - pip install -e ext/opentelemetry-ext-grpc/ + pip install -e ext/opentelemetry-instrumentation-grpc/ pip install -e docs/examples/opentelemetry-example-app Then run the server in one shell:: @@ -45,8 +45,8 @@ import grpc from opentelemetry import trace -from opentelemetry.ext.grpc import server_interceptor -from opentelemetry.ext.grpc.grpcext import intercept_server +from opentelemetry.instrumentation.grpc import server_interceptor +from opentelemetry.instrumentation.grpc.grpcext import intercept_server from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, diff --git a/docs/examples/opentracing/README.rst b/docs/examples/opentracing/README.rst index 06c767b199..f56bd3e5fc 100644 --- a/docs/examples/opentracing/README.rst +++ b/docs/examples/opentracing/README.rst @@ -1,8 +1,8 @@ OpenTracing Shim Example ========================== -This example shows how to use the :doc:`opentelemetry-ext-opentracing-shim -package <../../ext/opentracing_shim/opentracing_shim>` +This example shows how to use the :doc:`opentelemetry-instrumentation-opentracing-shim +package <../../instrumentation/opentracing_shim/opentracing_shim>` to interact with libraries instrumented with `opentracing-python `_. @@ -61,7 +61,7 @@ Alternatively, you can install the Python dependencies separately: pip install \ opentelemetry-api \ opentelemetry-sdk \ - opentelemetry-ext-jaeger \ + opentelemetry-exporter-jaeger \ opentelemetry-opentracing-shim \ redis \ redis_opentracing @@ -100,6 +100,6 @@ Useful links ------------ - OpenTelemetry_ -- :doc:`../../ext/opentracing_shim/opentracing_shim` +- :doc:`../../instrumentation/opentracing_shim/opentracing_shim` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/docs/examples/opentracing/main.py b/docs/examples/opentracing/main.py index 9ecbbba8ca..0653ca82ec 100755 --- a/docs/examples/opentracing/main.py +++ b/docs/examples/opentracing/main.py @@ -2,7 +2,7 @@ from opentelemetry import trace from opentelemetry.exporter.jaeger import JaegerSpanExporter -from opentelemetry.ext import opentracing_shim +from opentelemetry.instrumentation import opentracing_shim from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor from rediscache import RedisCache diff --git a/docs/ext/boto/boto.rst b/docs/ext/boto/boto.rst deleted file mode 100644 index 8bf40c7566..0000000000 --- a/docs/ext/boto/boto.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Boto Integration -============================== - -.. automodule:: opentelemetry.ext.boto - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 11e3a08fec..e9dd831e90 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -122,7 +122,7 @@ for Jaeger, but you can install that as a separate package: .. code-block:: sh - pip install opentelemetry-ext-jaeger + pip install opentelemetry-exporter-jaeger Once installed, update your code to import the Jaeger exporter, and use that instead: @@ -228,7 +228,7 @@ For our Python application, we will need to install an exporter specific to Prom .. code-block:: sh - pip install opentelemetry-ext-prometheus + pip install opentelemetry-exporter-prometheus And use that instead of the `ConsoleMetricsExporter`: @@ -289,7 +289,7 @@ Install the OpenTelemetry Collector exporter: .. code-block:: sh - pip install opentelemetry-ext-otcollector + pip install opentelemetry-instrumentation-otcollector And execute the following script: diff --git a/docs/index.rst b/docs/index.rst index 3378daac07..a127bf4c42 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,9 +26,11 @@ The API and SDK packages are available on PyPI, and can installed via pip: In addition, there are several extension packages which can be installed separately as:: - pip install opentelemetry-ext-{integration} + pip install opentelemetry-exporter-{exporter} + pip install opentelemetry-instrumentation-{instrumentation} -The extension packages can be found in :scm_web:`ext/ directory of the repository `. +These are for exporter and instrumentation packages respectively. +The packages can be found in :scm_web:`instrumentation/ directory of the repository `. Extensions ---------- @@ -51,7 +53,7 @@ install cd opentelemetry-python pip install -e ./opentelemetry-api pip install -e ./opentelemetry-sdk - pip install -e ./ext/opentelemetry-ext-{integration} + pip install -e ./instrumentation/opentelemetry-instrumentation-{instrumentation} .. toctree:: @@ -86,14 +88,6 @@ install instrumentation/** -.. toctree:: - :maxdepth: 2 - :caption: OpenTelemetry Integrations - :name: integrations - :glob: - - ext/** - .. toctree:: :maxdepth: 1 :caption: Examples diff --git a/docs/ext/botocore/botocore.rst b/docs/instrumentation/boto/boto.rst similarity index 51% rename from docs/ext/botocore/botocore.rst rename to docs/instrumentation/boto/boto.rst index 43f702a904..c438c2466c 100644 --- a/docs/ext/botocore/botocore.rst +++ b/docs/instrumentation/boto/boto.rst @@ -1,7 +1,7 @@ -OpenTelemetry Botocore Integration +OpenTelemetry Boto Instrumentation ================================== -.. automodule:: opentelemetry.ext.botocore +.. automodule:: opentelemetry.instrumentation.boto :members: :undoc-members: :show-inheritance: diff --git a/docs/instrumentation/botocore/botocore.rst b/docs/instrumentation/botocore/botocore.rst new file mode 100644 index 0000000000..eb8ea6bcf7 --- /dev/null +++ b/docs/instrumentation/botocore/botocore.rst @@ -0,0 +1,7 @@ +OpenTelemetry Botocore Instrumentation +====================================== + +.. automodule:: opentelemetry.instrumentation.botocore + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ext/celery/celery.rst b/docs/instrumentation/celery/celery.rst similarity index 71% rename from docs/ext/celery/celery.rst rename to docs/instrumentation/celery/celery.rst index 125233e006..c85f3adb59 100644 --- a/docs/ext/celery/celery.rst +++ b/docs/instrumentation/celery/celery.rst @@ -1,7 +1,7 @@ OpenTelemetry Celery Instrumentation ==================================== -.. automodule:: opentelemetry.ext.celery +.. automodule:: opentelemetry.instrumentation.celery :members: :undoc-members: :show-inheritance: diff --git a/docs/ext/grpc/grpc.rst b/docs/instrumentation/grpc/grpc.rst similarity index 75% rename from docs/ext/grpc/grpc.rst rename to docs/instrumentation/grpc/grpc.rst index 351d3bfb0d..243f696143 100644 --- a/docs/ext/grpc/grpc.rst +++ b/docs/instrumentation/grpc/grpc.rst @@ -4,7 +4,7 @@ OpenTelemetry gRPC Instrumentation Module contents --------------- -.. automodule:: opentelemetry.ext.grpc +.. automodule:: opentelemetry.instrumentation.grpc :members: :undoc-members: :show-inheritance: diff --git a/docs/ext/jinja2/jinja2.rst b/docs/instrumentation/jinja2/jinja2.rst similarity index 71% rename from docs/ext/jinja2/jinja2.rst rename to docs/instrumentation/jinja2/jinja2.rst index d9b461627d..5c7143724c 100644 --- a/docs/ext/jinja2/jinja2.rst +++ b/docs/instrumentation/jinja2/jinja2.rst @@ -1,7 +1,7 @@ OpenTelemetry Jinja2 Instrumentation ==================================== -.. automodule:: opentelemetry.ext.jinja2 +.. automodule:: opentelemetry.instrumentation.jinja2 :members: :undoc-members: :show-inheritance: diff --git a/docs/ext/opentracing_shim/opentracing_shim.rst b/docs/instrumentation/opentracing_shim/opentracing_shim.rst similarity index 60% rename from docs/ext/opentracing_shim/opentracing_shim.rst rename to docs/instrumentation/opentracing_shim/opentracing_shim.rst index f27974b9c0..fad4e04bbe 100644 --- a/docs/ext/opentracing_shim/opentracing_shim.rst +++ b/docs/instrumentation/opentracing_shim/opentracing_shim.rst @@ -1,5 +1,5 @@ OpenTracing Shim for OpenTelemetry ================================== -.. automodule:: opentelemetry.ext.opentracing_shim +.. automodule:: opentelemetry.instrumentation.opentracing_shim :no-show-inheritance: diff --git a/docs/ext/system_metrics/system_metrics.rst b/docs/instrumentation/system_metrics/system_metrics.rst similarity index 70% rename from docs/ext/system_metrics/system_metrics.rst rename to docs/instrumentation/system_metrics/system_metrics.rst index db6f9da70f..96b39d9267 100644 --- a/docs/ext/system_metrics/system_metrics.rst +++ b/docs/instrumentation/system_metrics/system_metrics.rst @@ -1,7 +1,7 @@ OpenTelemetry System Metrics Instrumentation ============================================ -.. automodule:: opentelemetry.ext.system_metrics +.. automodule:: opentelemetry.instrumentation.system_metrics :members: :undoc-members: :show-inheritance: diff --git a/eachdist.ini b/eachdist.ini index abf9e620aa..076c8a4a4a 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -10,7 +10,7 @@ sortfirst= instrumentation/opentelemetry-instrumentation-wsgi instrumentation/opentelemetry-instrumentation-dbapi instrumentation/opentelemetry-instrumentation-asgi - ext/opentelemetry-ext-botocore + instrumentation/opentelemetry-instrumentation-botocore instrumentation/* exporter/* ext/* diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/__init__.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/__init__.py index 7adde5df50..5a73c55d69 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/__init__.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/__init__.py @@ -21,7 +21,7 @@ :: - pip install opentelemetry-ext-datadog + pip install opentelemetry-exporter-datadog Usage diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py index 7c94e173f7..49dab7c686 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py @@ -37,8 +37,8 @@ "opentelemetry.instrumentation.dbapi": DatadogSpanTypes.SQL, "opentelemetry.instrumentation.django": DatadogSpanTypes.WEB, "opentelemetry.instrumentation.flask": DatadogSpanTypes.WEB, - "opentelemetry.ext.grpc": DatadogSpanTypes.GRPC, - "opentelemetry.ext.jinja2": DatadogSpanTypes.TEMPLATE, + "opentelemetry.instrumentation.grpc": DatadogSpanTypes.GRPC, + "opentelemetry.instrumentation.jinja2": DatadogSpanTypes.TEMPLATE, "opentelemetry.instrumentation.mysql": DatadogSpanTypes.SQL, "opentelemetry.instrumentation.psycopg2": DatadogSpanTypes.SQL, "opentelemetry.instrumentation.pymemcache": DatadogSpanTypes.CACHE, diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py index 4b7d2391bc..45ce9417e1 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py @@ -294,8 +294,8 @@ def test_span_types(self): "opentelemetry.instrumentation.dbapi", "opentelemetry.instrumentation.django", "opentelemetry.instrumentation.flask", - "opentelemetry.ext.grpc", - "opentelemetry.ext.jinja2", + "opentelemetry.instrumentation.grpc", + "opentelemetry.instrumentation.jinja2", "opentelemetry.instrumentation.mysql", "opentelemetry.instrumentation.psycopg2", "opentelemetry.instrumentation.pymongo", diff --git a/ext/opentelemetry-ext-jinja2/README.rst b/ext/opentelemetry-ext-jinja2/README.rst deleted file mode 100644 index 09e74e21d3..0000000000 --- a/ext/opentelemetry-ext-jinja2/README.rst +++ /dev/null @@ -1,21 +0,0 @@ -OpenTelemetry jinja2 integration -================================ - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-jinja2.svg - :target: https://pypi.org/project/opentelemetry-ext-jinja2/ - -Installation ------------- - -:: - - pip install opentelemetry-ext-jinja2 - - -References ----------- - -* `OpenTelemetry jinja2 integration `_ -* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-jinja2/setup.py b/ext/opentelemetry-ext-jinja2/setup.py deleted file mode 100644 index 323c1b3353..0000000000 --- a/ext/opentelemetry-ext-jinja2/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "jinja2", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-system-metrics/CHANGELOG.md b/ext/opentelemetry-ext-system-metrics/CHANGELOG.md deleted file mode 100644 index 12d51bb800..0000000000 --- a/ext/opentelemetry-ext-system-metrics/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -## Unreleased - -## 0.9b0 - -Released 2020-06-10 - -- Initial release (https://github.com/open-telemetry/opentelemetry-python/pull/652) diff --git a/ext/opentelemetry-ext-system-metrics/setup.py b/ext/opentelemetry-ext-system-metrics/setup.py deleted file mode 100644 index 370399094d..0000000000 --- a/ext/opentelemetry-ext-system-metrics/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "system_metrics", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md index 08464e8cd9..530bc71da7 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased - Change package name to opentelemetry-instrumentation-asyncpg - ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) ## Version 0.11b0 diff --git a/ext/opentelemetry-ext-boto/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md similarity index 65% rename from ext/opentelemetry-ext-boto/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md index ec62b40595..a1c2e9465f 100644 --- a/ext/opentelemetry-ext-boto/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-boto + ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) + ## Version 0.11b0 Released 2020-07-28 diff --git a/ext/opentelemetry-ext-boto/LICENSE b/instrumentation/opentelemetry-instrumentation-boto/LICENSE similarity index 100% rename from ext/opentelemetry-ext-boto/LICENSE rename to instrumentation/opentelemetry-instrumentation-boto/LICENSE diff --git a/ext/opentelemetry-ext-boto/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-boto/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-boto/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-boto/MANIFEST.in diff --git a/ext/opentelemetry-ext-boto/README.rst b/instrumentation/opentelemetry-instrumentation-boto/README.rst similarity index 53% rename from ext/opentelemetry-ext-boto/README.rst rename to instrumentation/opentelemetry-instrumentation-boto/README.rst index e149ec424e..2b40321c00 100644 --- a/ext/opentelemetry-ext-boto/README.rst +++ b/instrumentation/opentelemetry-instrumentation-boto/README.rst @@ -3,8 +3,8 @@ OpenTelemetry Boto Tracing |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-boto.svg - :target: https://pypi.org/project/opentelemetry-ext-boto/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-boto.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-boto/ This library allows tracing requests made by the Boto library. @@ -13,11 +13,11 @@ Installation :: - pip install opentelemetry-ext-boto + pip install opentelemetry-instrumentation-boto References ---------- -* `OpenTelemetry Boto Tracing `_ +* `OpenTelemetry Boto Tracing `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-boto/setup.cfg b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg similarity index 86% rename from ext/opentelemetry-ext-boto/setup.cfg rename to instrumentation/opentelemetry-instrumentation-boto/setup.cfg index 1c38775509..ee47a919cd 100644 --- a/ext/opentelemetry-ext-boto/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-boto -description = Boto tracing for OpenTelemetry +name = opentelemetry-instrumentation-boto +description = OpenTelemetry Boto instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-boto +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-boto platforms = any license = Apache-2.0 classifiers = @@ -43,7 +43,7 @@ install_requires = boto ~= 2.0 opentelemetry-api == 0.12.dev0 opentelemetry-instrumentation == 0.12.dev0 - opentelemetry-ext-botocore == 0.12.dev0 + opentelemetry-instrumentation-botocore == 0.12.dev0 [options.extras_require] test = @@ -56,4 +56,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - django = opentelemetry.ext.boto:BotoInstrumentor + django = opentelemetry.instrumentation.boto:BotoInstrumentor diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.py b/instrumentation/opentelemetry-instrumentation-boto/setup.py similarity index 92% rename from ext/opentelemetry-ext-opentracing-shim/setup.py rename to instrumentation/opentelemetry-instrumentation-boto/setup.py index 5924121c43..2bd68894f3 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.py +++ b/instrumentation/opentelemetry-instrumentation-boto/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "opentracing_shim", "version.py" + BASE_DIR, "src", "opentelemetry", "instrumentation", "boto", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py similarity index 96% rename from ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py rename to instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py index f1f04723af..e6a0e351e5 100644 --- a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py @@ -27,7 +27,7 @@ .. code:: python from opentelemetry import trace - from opentelemetry.ext.boto import BotoInstrumentor + from opentelemetry.instrumentation.boto import BotoInstrumentor from opentelemetry.sdk.trace import TracerProvider import boto @@ -50,8 +50,8 @@ from boto.connection import AWSAuthConnection, AWSQueryConnection from wrapt import wrap_function_wrapper -from opentelemetry.ext.boto.version import __version__ -from opentelemetry.ext.botocore import add_span_arg_tags, unwrap +from opentelemetry.instrumentation.boto.version import __version__ +from opentelemetry.instrumentation.botocore import add_span_arg_tags, unwrap from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.sdk.trace import Resource from opentelemetry.trace import SpanKind, get_tracer diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py similarity index 100% rename from ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py rename to instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py diff --git a/ext/opentelemetry-ext-boto/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-boto/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-boto/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-boto/tests/__init__.py diff --git a/ext/opentelemetry-ext-boto/tests/conftest.py b/instrumentation/opentelemetry-instrumentation-boto/tests/conftest.py similarity index 100% rename from ext/opentelemetry-ext-boto/tests/conftest.py rename to instrumentation/opentelemetry-instrumentation-boto/tests/conftest.py diff --git a/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py b/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py similarity index 99% rename from ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py rename to instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py index 7cfbf018c6..7ed8775501 100644 --- a/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py @@ -26,7 +26,7 @@ mock_sts_deprecated, ) -from opentelemetry.ext.boto import BotoInstrumentor +from opentelemetry.instrumentation.boto import BotoInstrumentor from opentelemetry.sdk.resources import Resource from opentelemetry.test.test_base import TestBase diff --git a/ext/opentelemetry-ext-botocore/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md similarity index 64% rename from ext/opentelemetry-ext-botocore/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md index ac8d9c4a51..e89b3fddd2 100644 --- a/ext/opentelemetry-ext-botocore/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-botocore + ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) + ## Version 0.11b0 Released 2020-07-28 diff --git a/ext/opentelemetry-ext-botocore/LICENSE b/instrumentation/opentelemetry-instrumentation-botocore/LICENSE similarity index 100% rename from ext/opentelemetry-ext-botocore/LICENSE rename to instrumentation/opentelemetry-instrumentation-botocore/LICENSE diff --git a/ext/opentelemetry-ext-botocore/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-botocore/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-botocore/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-botocore/MANIFEST.in diff --git a/ext/opentelemetry-ext-botocore/README.rst b/instrumentation/opentelemetry-instrumentation-botocore/README.rst similarity index 52% rename from ext/opentelemetry-ext-botocore/README.rst rename to instrumentation/opentelemetry-instrumentation-botocore/README.rst index 0b50819d32..4f5eb9d9af 100644 --- a/ext/opentelemetry-ext-botocore/README.rst +++ b/instrumentation/opentelemetry-instrumentation-botocore/README.rst @@ -3,8 +3,8 @@ OpenTelemetry Botocore Tracing |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-botocore.svg - :target: https://pypi.org/project/opentelemetry-ext-botocore/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-botocore.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-botocore/ This library allows tracing requests made by the Botocore library. @@ -13,11 +13,11 @@ Installation :: - pip install opentelemetry-ext-botocore + pip install opentelemetry-instrumentation-botocore References ---------- -* `OpenTelemetry Botocore Tracing `_ +* `OpenTelemetry Botocore Tracing `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-botocore/setup.cfg b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg similarity index 87% rename from ext/opentelemetry-ext-botocore/setup.cfg rename to instrumentation/opentelemetry-instrumentation-botocore/setup.cfg index 0d8b9e4ad4..86f3e8d0a4 100644 --- a/ext/opentelemetry-ext-botocore/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-botocore -description = Botocore tracing for OpenTelemetry +name = opentelemetry-instrumentation-botocore +description = OpenTelemetry Botocore instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-botocore +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-botocore platforms = any license = Apache-2.0 classifiers = @@ -54,4 +54,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - django = opentelemetry.ext.botocore:BotoCoreInstrumentor + django = opentelemetry.instrumentation.botocore:BotoCoreInstrumentor diff --git a/ext/opentelemetry-ext-botocore/setup.py b/instrumentation/opentelemetry-instrumentation-botocore/setup.py similarity index 88% rename from ext/opentelemetry-ext-botocore/setup.py rename to instrumentation/opentelemetry-instrumentation-botocore/setup.py index 35b47b1b00..fd5045efaa 100644 --- a/ext/opentelemetry-ext-botocore/setup.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/setup.py @@ -17,7 +17,12 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "botocore", "version.py" + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "botocore", + "version.py", ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py similarity index 97% rename from ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py rename to instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py index 70e790d9bf..9b9b1e9a80 100644 --- a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py @@ -28,7 +28,7 @@ .. code:: python from opentelemetry import trace - from opentelemetry.ext.botocore import BotocoreInstrumentor + from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.sdk.trace import TracerProvider import botocore @@ -56,7 +56,7 @@ from botocore.client import BaseClient from wrapt import ObjectProxy, wrap_function_wrapper -from opentelemetry.ext.botocore.version import __version__ +from opentelemetry.instrumentation.botocore.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.sdk.trace import Resource from opentelemetry.trace import SpanKind, get_tracer diff --git a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py similarity index 100% rename from ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/version.py rename to instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py diff --git a/ext/opentelemetry-ext-botocore/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-botocore/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-botocore/tests/__init__.py diff --git a/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py similarity index 99% rename from ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py rename to instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py index e0b687ad8b..47073478fc 100644 --- a/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py @@ -9,7 +9,7 @@ mock_sqs, ) -from opentelemetry.ext.botocore import BotocoreInstrumentor +from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.sdk.resources import Resource from opentelemetry.test.test_base import TestBase diff --git a/ext/opentelemetry-ext-celery/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md similarity index 55% rename from ext/opentelemetry-ext-celery/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md index 54990c1955..0de1991b96 100644 --- a/ext/opentelemetry-ext-celery/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-celery + ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) + ## Version 0.10b0 Released 2020-06-23 diff --git a/ext/opentelemetry-ext-celery/LICENSE b/instrumentation/opentelemetry-instrumentation-celery/LICENSE similarity index 100% rename from ext/opentelemetry-ext-celery/LICENSE rename to instrumentation/opentelemetry-instrumentation-celery/LICENSE diff --git a/ext/opentelemetry-ext-celery/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-celery/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-celery/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-celery/MANIFEST.in diff --git a/ext/opentelemetry-ext-celery/README.rst b/instrumentation/opentelemetry-instrumentation-celery/README.rst similarity index 70% rename from ext/opentelemetry-ext-celery/README.rst rename to instrumentation/opentelemetry-instrumentation-celery/README.rst index feabf6809f..42fe6646d1 100644 --- a/ext/opentelemetry-ext-celery/README.rst +++ b/instrumentation/opentelemetry-instrumentation-celery/README.rst @@ -3,8 +3,8 @@ OpenTelemetry Celery Instrumentation |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-celery.svg - :target: https://pypi.org/project/opentelemetry-ext-celery/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-celery.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-celery/ Instrumentation for Celery. @@ -14,7 +14,7 @@ Installation :: - pip install opentelemetry-ext-celery + pip install opentelemetry-instrumentation-celery Usage ----- @@ -29,7 +29,7 @@ Usage .. code-block:: python - from opentelemetry.ext.celery import CeleryInstrumentor + from opentelemetry.instrumentation.celery import CeleryInstrumentor CeleryInstrumentor().instrument() diff --git a/ext/opentelemetry-ext-celery/setup.cfg b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg similarity index 90% rename from ext/opentelemetry-ext-celery/setup.cfg rename to instrumentation/opentelemetry-instrumentation-celery/setup.cfg index 39e019e83a..4198f15569 100644 --- a/ext/opentelemetry-ext-celery/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-celery +name = opentelemetry-instrumentation-celery description = OpenTelemetry Celery Instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-celery +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-celery platforms = any license = Apache-2.0 classifiers = @@ -53,4 +53,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - celery = opentelemetry.ext.celery:CeleryInstrumentor + celery = opentelemetry.instrumentation.celery:CeleryInstrumentor diff --git a/ext/opentelemetry-ext-elasticsearch/setup.py b/instrumentation/opentelemetry-instrumentation-celery/setup.py similarity index 91% rename from ext/opentelemetry-ext-elasticsearch/setup.py rename to instrumentation/opentelemetry-instrumentation-celery/setup.py index af750c941f..ca67930660 100644 --- a/ext/opentelemetry-ext-elasticsearch/setup.py +++ b/instrumentation/opentelemetry-instrumentation-celery/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "elasticsearch", "version.py" + BASE_DIR, "src", "opentelemetry", "instrumentation", "celery", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/__init__.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py similarity index 97% rename from ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/__init__.py rename to instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py index 9ce31f34f6..7e2551142e 100644 --- a/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py @@ -30,7 +30,7 @@ .. code:: python - from opentelemetry.ext.celery import CeleryInstrumentor + from opentelemetry.instrumentation.celery import CeleryInstrumentor CeleryInstrumentor().instrument() @@ -54,8 +54,8 @@ def add(x, y): from celery import signals # pylint: disable=no-name-in-module from opentelemetry import trace -from opentelemetry.ext.celery import utils -from opentelemetry.ext.celery.version import __version__ +from opentelemetry.instrumentation.celery import utils +from opentelemetry.instrumentation.celery.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.trace.status import Status, StatusCanonicalCode diff --git a/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/utils.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py similarity index 100% rename from ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/utils.py rename to instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py diff --git a/ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py similarity index 100% rename from ext/opentelemetry-ext-celery/src/opentelemetry/ext/celery/version.py rename to instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py diff --git a/ext/opentelemetry-ext-celery/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-celery/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-celery/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-celery/tests/__init__.py diff --git a/ext/opentelemetry-ext-celery/tests/test_utils.py b/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py similarity index 99% rename from ext/opentelemetry-ext-celery/tests/test_utils.py rename to instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py index b5e8163def..0842890e89 100644 --- a/ext/opentelemetry-ext-celery/tests/test_utils.py +++ b/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py @@ -18,7 +18,7 @@ from celery import Celery from opentelemetry import trace as trace_api -from opentelemetry.ext.celery import utils +from opentelemetry.instrumentation.celery import utils from opentelemetry.sdk import trace diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md index c0a38d855f..555b227db3 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased - Change package name to opentelemetry-instrumentation-dbapi - ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) ## 0.7b1 diff --git a/ext/opentelemetry-ext-elasticsearch/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md similarity index 62% rename from ext/opentelemetry-ext-elasticsearch/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md index ede33c6646..5579a36a62 100644 --- a/ext/opentelemetry-ext-elasticsearch/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) +- Change package name to opentelemetry-instrumentation-elasticsearch + ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) ## Version 0.10b0 diff --git a/ext/opentelemetry-ext-elasticsearch/LICENSE b/instrumentation/opentelemetry-instrumentation-elasticsearch/LICENSE similarity index 100% rename from ext/opentelemetry-ext-elasticsearch/LICENSE rename to instrumentation/opentelemetry-instrumentation-elasticsearch/LICENSE diff --git a/ext/opentelemetry-ext-elasticsearch/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-elasticsearch/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-elasticsearch/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-elasticsearch/MANIFEST.in diff --git a/ext/opentelemetry-ext-elasticsearch/README.rst b/instrumentation/opentelemetry-instrumentation-elasticsearch/README.rst similarity index 55% rename from ext/opentelemetry-ext-elasticsearch/README.rst rename to instrumentation/opentelemetry-instrumentation-elasticsearch/README.rst index 88e9bb41af..9f898e7835 100644 --- a/ext/opentelemetry-ext-elasticsearch/README.rst +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/README.rst @@ -3,8 +3,8 @@ OpenTelemetry elasticsearch Integration |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-elasticsearch.svg - :target: https://pypi.org/project/opentelemetry-ext-elasticsearch/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-elasticsearch.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-elasticsearch/ This library allows tracing elasticsearch made by the `elasticsearch `_ library. @@ -14,10 +14,10 @@ Installation :: - pip install opentelemetry-ext-elasticsearch + pip install opentelemetry-instrumentation-elasticsearch References ---------- -* `OpenTelemetry elasticsearch Integration `_ +* `OpenTelemetry elasticsearch Integration `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-elasticsearch/setup.cfg b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg similarity index 86% rename from ext/opentelemetry-ext-elasticsearch/setup.cfg rename to instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg index 877f62872c..fc5f862a2c 100644 --- a/ext/opentelemetry-ext-elasticsearch/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-elasticsearch -description = OpenTelemetry elasticsearch integration +name = opentelemetry-instrumentation-elasticsearch +description = OpenTelemetry elasticsearch instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-elasticsearch +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-elasticsearch platforms = any license = Apache-2.0 classifiers = @@ -55,4 +55,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - elasticsearch = opentelemetry.ext.elasticsearch:ElasticsearchInstrumentor + elasticsearch = opentelemetry.instrumentation.elasticsearch:ElasticsearchInstrumentor diff --git a/ext/opentelemetry-ext-celery/setup.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.py similarity index 88% rename from ext/opentelemetry-ext-celery/setup.py rename to instrumentation/opentelemetry-instrumentation-elasticsearch/setup.py index 40d1d7aaba..cd7a7f1012 100644 --- a/ext/opentelemetry-ext-celery/setup.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.py @@ -17,7 +17,12 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "celery", "version.py" + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "elasticsearch", + "version.py", ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py similarity index 97% rename from ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/__init__.py rename to instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py index 6746d1ac70..f350a7dc28 100644 --- a/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py @@ -22,7 +22,7 @@ .. code-block:: python from opentelemetry import trace - from opentelemetry.ext.elasticsearch import ElasticSearchInstrumentor + from opentelemetry.instrumentation.elasticsearch import ElasticSearchInstrumentor from opentelemetry.sdk.trace import TracerProvider import elasticsearch @@ -60,7 +60,7 @@ from wrapt import wrap_function_wrapper as _wrap from opentelemetry import context, propagators, trace -from opentelemetry.ext.elasticsearch.version import __version__ +from opentelemetry.instrumentation.elasticsearch.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import SpanKind, get_tracer diff --git a/ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py similarity index 100% rename from ext/opentelemetry-ext-elasticsearch/src/opentelemetry/ext/elasticsearch/version.py rename to instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py diff --git a/ext/opentelemetry-ext-elasticsearch/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-elasticsearch/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-elasticsearch/tests/__init__.py diff --git a/ext/opentelemetry-ext-elasticsearch/tests/helpers_es2.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es2.py similarity index 100% rename from ext/opentelemetry-ext-elasticsearch/tests/helpers_es2.py rename to instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es2.py diff --git a/ext/opentelemetry-ext-elasticsearch/tests/helpers_es5.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es5.py similarity index 100% rename from ext/opentelemetry-ext-elasticsearch/tests/helpers_es5.py rename to instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es5.py diff --git a/ext/opentelemetry-ext-elasticsearch/tests/helpers_es6.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py similarity index 100% rename from ext/opentelemetry-ext-elasticsearch/tests/helpers_es6.py rename to instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py diff --git a/ext/opentelemetry-ext-elasticsearch/tests/helpers_es7.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py similarity index 100% rename from ext/opentelemetry-ext-elasticsearch/tests/helpers_es7.py rename to instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py diff --git a/ext/opentelemetry-ext-elasticsearch/tests/test_elasticsearch.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py similarity index 97% rename from ext/opentelemetry-ext-elasticsearch/tests/test_elasticsearch.py rename to instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py index 3c4fe7f70f..cc1d314774 100644 --- a/ext/opentelemetry-ext-elasticsearch/tests/test_elasticsearch.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py @@ -22,8 +22,10 @@ from elasticsearch import Elasticsearch from elasticsearch_dsl import Search -import opentelemetry.ext.elasticsearch -from opentelemetry.ext.elasticsearch import ElasticsearchInstrumentor +import opentelemetry.instrumentation.elasticsearch +from opentelemetry.instrumentation.elasticsearch import ( + ElasticsearchInstrumentor, +) from opentelemetry.test.test_base import TestBase from opentelemetry.trace.status import StatusCanonicalCode @@ -73,9 +75,9 @@ def test_instrumentor(self, request_mock): span = spans_list[0] # Check version and name in span's instrumentation info - # self.check_span_instrumentation_info(span, opentelemetry.ext.elasticsearch) + # self.check_span_instrumentation_info(span, opentelemetry.instrumentation.elasticsearch) self.check_span_instrumentation_info( - span, opentelemetry.ext.elasticsearch + span, opentelemetry.instrumentation.elasticsearch ) # check that no spans are generated after uninstrument diff --git a/ext/opentelemetry-ext-grpc/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md similarity index 83% rename from ext/opentelemetry-ext-grpc/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md index 5f272a80f8..b6b28ecd9f 100644 --- a/ext/opentelemetry-ext-grpc/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-grpc + ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) + ## Version 0.11b0 Released 2020-07-28 diff --git a/ext/opentelemetry-ext-grpc/LICENSE b/instrumentation/opentelemetry-instrumentation-grpc/LICENSE similarity index 100% rename from ext/opentelemetry-ext-grpc/LICENSE rename to instrumentation/opentelemetry-instrumentation-grpc/LICENSE diff --git a/ext/opentelemetry-ext-grpc/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-grpc/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-grpc/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-grpc/MANIFEST.in diff --git a/ext/opentelemetry-ext-grpc/README.rst b/instrumentation/opentelemetry-instrumentation-grpc/README.rst similarity index 51% rename from ext/opentelemetry-ext-grpc/README.rst rename to instrumentation/opentelemetry-instrumentation-grpc/README.rst index 335c03614b..176bdf1a39 100644 --- a/ext/opentelemetry-ext-grpc/README.rst +++ b/instrumentation/opentelemetry-instrumentation-grpc/README.rst @@ -3,8 +3,8 @@ OpenTelemetry gRPC Integration |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-grpc.svg - :target: https://pypi.org/project/opentelemetry-ext-grpc/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-grpc.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-grpc/ Client and server interceptors for `gRPC Python`_. @@ -15,4 +15,4 @@ Installation :: - pip install opentelemetry-ext-grpc + pip install opentelemetry-instrumentation-grpc diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg similarity index 87% rename from ext/opentelemetry-ext-grpc/setup.cfg rename to instrumentation/opentelemetry-instrumentation-grpc/setup.cfg index a3533bdf59..0308a229f8 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. [metadata] -name = opentelemetry-ext-grpc +name = opentelemetry-instrumentation-grpc description = OpenTelemetry gRPC instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-grpc +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-grpc platforms = any license = Apache-2.0 classifiers = @@ -55,5 +55,5 @@ where = src [options.entry_points] opentelemetry_instrumentor = - grpc_client = opentelemetry.ext.grpc:GrpcInstrumentorClient - grpc_server = opentelemetry.ext.grpc:GrpcInstrumentorServer + grpc_client = opentelemetry.instrumentation.grpc:GrpcInstrumentorClient + grpc_server = opentelemetry.instrumentation.grpc:GrpcInstrumentorServer diff --git a/ext/opentelemetry-ext-grpc/setup.py b/instrumentation/opentelemetry-instrumentation-grpc/setup.py similarity index 91% rename from ext/opentelemetry-ext-grpc/setup.py rename to instrumentation/opentelemetry-instrumentation-grpc/setup.py index 9a0a4b5d1e..87c720aea2 100644 --- a/ext/opentelemetry-ext-grpc/setup.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/setup.py @@ -18,7 +18,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "grpc", "version.py" + BASE_DIR, "src", "opentelemetry", "instrumentation", "grpc", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py similarity index 93% rename from ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py rename to instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py index 1a66566281..9b6862eba0 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py @@ -26,7 +26,7 @@ import grpc from opentelemetry import trace - from opentelemetry.ext.grpc import GrpcInstrumentorClient, client_interceptor + from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient, client_interceptor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -77,8 +77,8 @@ def run(): import grpc from opentelemetry import trace - from opentelemetry.ext.grpc import GrpcInstrumentorServer, server_interceptor - from opentelemetry.ext.grpc.grpcext import intercept_server + from opentelemetry.instrumentation.grpc import GrpcInstrumentorServer, server_interceptor + from opentelemetry.instrumentation.grpc.grpcext import intercept_server from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -124,8 +124,11 @@ def serve(): from wrapt import wrap_function_wrapper as _wrap from opentelemetry import trace -from opentelemetry.ext.grpc.grpcext import intercept_channel, intercept_server -from opentelemetry.ext.grpc.version import __version__ +from opentelemetry.instrumentation.grpc.grpcext import ( + intercept_channel, + intercept_server, +) +from opentelemetry.instrumentation.grpc.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py similarity index 100% rename from ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py rename to instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_server.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py similarity index 100% rename from ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_server.py rename to instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py similarity index 100% rename from ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py rename to instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/__init__.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/__init__.py similarity index 100% rename from ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/__init__.py rename to instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/__init__.py diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/_interceptor.py similarity index 100% rename from ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/_interceptor.py rename to instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/_interceptor.py diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py similarity index 100% rename from ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py rename to instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/__init__.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-grpc/tests/__init__.py diff --git a/ext/opentelemetry-ext-grpc/tests/_client.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/_client.py similarity index 100% rename from ext/opentelemetry-ext-grpc/tests/_client.py rename to instrumentation/opentelemetry-instrumentation-grpc/tests/_client.py diff --git a/ext/opentelemetry-ext-grpc/tests/_server.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/_server.py similarity index 100% rename from ext/opentelemetry-ext-grpc/tests/_server.py rename to instrumentation/opentelemetry-instrumentation-grpc/tests/_server.py diff --git a/ext/opentelemetry-ext-grpc/tests/protobuf/test_server.proto b/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server.proto similarity index 100% rename from ext/opentelemetry-ext-grpc/tests/protobuf/test_server.proto rename to instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server.proto diff --git a/ext/opentelemetry-ext-grpc/tests/protobuf/test_server_pb2.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2.py similarity index 100% rename from ext/opentelemetry-ext-grpc/tests/protobuf/test_server_pb2.py rename to instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2.py diff --git a/ext/opentelemetry-ext-grpc/tests/protobuf/test_server_pb2_grpc.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2_grpc.py similarity index 100% rename from ext/opentelemetry-ext-grpc/tests/protobuf/test_server_pb2_grpc.py rename to instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2_grpc.py diff --git a/ext/opentelemetry-ext-grpc/tests/test_client_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py similarity index 93% rename from ext/opentelemetry-ext-grpc/tests/test_client_interceptor.py rename to instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py index a668f05ca7..458f32e047 100644 --- a/ext/opentelemetry-ext-grpc/tests/test_client_interceptor.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py @@ -14,9 +14,9 @@ import grpc -import opentelemetry.ext.grpc +import opentelemetry.instrumentation.grpc from opentelemetry import trace -from opentelemetry.ext.grpc import GrpcInstrumentorClient +from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient from opentelemetry.sdk.metrics.export.aggregate import ( MinMaxSumCountAggregator, SumAggregator, @@ -107,7 +107,9 @@ def test_unary_unary(self): self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info - self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.grpc + ) self._verify_success_records(8, 8, "/GRPCTestServer/SimpleMethod") @@ -121,7 +123,9 @@ def test_unary_stream(self): self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info - self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.grpc + ) self._verify_success_records( 8, 40, "/GRPCTestServer/ServerStreamingMethod" @@ -137,7 +141,9 @@ def test_stream_unary(self): self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info - self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.grpc + ) self._verify_success_records( 40, 8, "/GRPCTestServer/ClientStreamingMethod" @@ -155,7 +161,9 @@ def test_stream_stream(self): self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info - self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.grpc + ) self._verify_success_records( 40, 40, "/GRPCTestServer/BidirectionalStreamingMethod" @@ -285,4 +293,6 @@ def test_unary_unary(self): self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info - self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.grpc + ) diff --git a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_server_interceptor.py similarity index 95% rename from ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py rename to instrumentation/opentelemetry-instrumentation-grpc/tests/test_server_interceptor.py index 0ba57a4322..a41da47ae9 100644 --- a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_server_interceptor.py @@ -20,10 +20,13 @@ import grpc -import opentelemetry.ext.grpc +import opentelemetry.instrumentation.grpc from opentelemetry import trace -from opentelemetry.ext.grpc import GrpcInstrumentorServer, server_interceptor -from opentelemetry.ext.grpc.grpcext import intercept_server +from opentelemetry.instrumentation.grpc import ( + GrpcInstrumentorServer, + server_interceptor, +) +from opentelemetry.instrumentation.grpc.grpcext import intercept_server from opentelemetry.sdk import trace as trace_sdk from opentelemetry.test.test_base import TestBase @@ -76,7 +79,9 @@ def handler(request, context): span = spans_list[0] self.assertEqual(span.name, "test") self.assertIs(span.kind, trace.SpanKind.SERVER) - self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.grpc + ) grpc_server_instrumentor.uninstrument() def test_uninstrument(self): @@ -141,7 +146,9 @@ def handler(request, context): self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info - self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.grpc + ) def test_span_lifetime(self): """Check that the span is active for the duration of the call.""" diff --git a/ext/opentelemetry-ext-jinja2/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md similarity index 53% rename from ext/opentelemetry-ext-jinja2/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md index ededf47daf..a503fe367e 100644 --- a/ext/opentelemetry-ext-jinja2/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change package name to opentelemetry-instrumentation-jinja2 + ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) + ## 0.7b1 Released 2020-05-12 diff --git a/ext/opentelemetry-ext-jinja2/LICENSE b/instrumentation/opentelemetry-instrumentation-jinja2/LICENSE similarity index 100% rename from ext/opentelemetry-ext-jinja2/LICENSE rename to instrumentation/opentelemetry-instrumentation-jinja2/LICENSE diff --git a/ext/opentelemetry-ext-jinja2/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-jinja2/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-jinja2/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-jinja2/MANIFEST.in diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/README.rst b/instrumentation/opentelemetry-instrumentation-jinja2/README.rst new file mode 100644 index 0000000000..c74faeb32e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-jinja2/README.rst @@ -0,0 +1,21 @@ +OpenTelemetry jinja2 integration +================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-jinja2.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-jinja2/ + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-jinja2 + + +References +---------- + +* `OpenTelemetry jinja2 integration `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-jinja2/setup.cfg b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg similarity index 87% rename from ext/opentelemetry-ext-jinja2/setup.cfg rename to instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg index fc0222a0e4..76b9b87920 100644 --- a/ext/opentelemetry-ext-jinja2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-jinja2 -description = OpenTelemetry jinja2 integration +name = opentelemetry-instrumentation-jinja2 +description = OpenTelemetry jinja2 instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-jinja2 +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-jinja2 platforms = any license = Apache-2.0 classifiers = @@ -53,4 +53,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - jinja2 = opentelemetry.ext.jinja2:Jinja2Instrumentor + jinja2 = opentelemetry.instrumentation.jinja2:Jinja2Instrumentor diff --git a/ext/opentelemetry-ext-boto/setup.py b/instrumentation/opentelemetry-instrumentation-jinja2/setup.py similarity index 91% rename from ext/opentelemetry-ext-boto/setup.py rename to instrumentation/opentelemetry-instrumentation-jinja2/setup.py index 4c78e9b35f..e9d484e1eb 100644 --- a/ext/opentelemetry-ext-boto/setup.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "boto", "version.py" + BASE_DIR, "src", "opentelemetry", "instrumentation", "jinja2", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/__init__.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py similarity index 96% rename from ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/__init__.py rename to instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py index 06be28873b..8ad883b279 100644 --- a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py @@ -26,7 +26,7 @@ .. code-block:: python from jinja2 import Environment, FileSystemLoader - from opentelemetry.ext.jinja2 import Jinja2Instrumentor + from opentelemetry.instrumentation.jinja2 import Jinja2Instrumentor from opentelemetry import trace from opentelemetry.trace import TracerProvider @@ -48,8 +48,8 @@ from wrapt import ObjectProxy from wrapt import wrap_function_wrapper as _wrap -from opentelemetry.ext.jinja2.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.jinja2.version import __version__ from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import SpanKind, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode diff --git a/ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py similarity index 100% rename from ext/opentelemetry-ext-jinja2/src/opentelemetry/ext/jinja2/version.py rename to instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py diff --git a/ext/opentelemetry-ext-jinja2/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-jinja2/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-jinja2/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-jinja2/tests/__init__.py diff --git a/ext/opentelemetry-ext-jinja2/tests/templates/base.html b/instrumentation/opentelemetry-instrumentation-jinja2/tests/templates/base.html similarity index 100% rename from ext/opentelemetry-ext-jinja2/tests/templates/base.html rename to instrumentation/opentelemetry-instrumentation-jinja2/tests/templates/base.html diff --git a/ext/opentelemetry-ext-jinja2/tests/templates/template.html b/instrumentation/opentelemetry-instrumentation-jinja2/tests/templates/template.html similarity index 100% rename from ext/opentelemetry-ext-jinja2/tests/templates/template.html rename to instrumentation/opentelemetry-instrumentation-jinja2/tests/templates/template.html diff --git a/ext/opentelemetry-ext-jinja2/tests/test_jinja2.py b/instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py similarity index 99% rename from ext/opentelemetry-ext-jinja2/tests/test_jinja2.py rename to instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py index 6abfa9a836..7f5b52c936 100644 --- a/ext/opentelemetry-ext-jinja2/tests/test_jinja2.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py @@ -17,7 +17,7 @@ import jinja2 from opentelemetry import trace as trace_api -from opentelemetry.ext.jinja2 import Jinja2Instrumentor +from opentelemetry.instrumentation.jinja2 import Jinja2Instrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer diff --git a/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md index 434a612abc..944b34d5ad 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased - Change package name to opentelemetry-instrumentation-mysql - ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) ## Version 0.11b0 diff --git a/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md similarity index 63% rename from ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md index af8ddc4ae8..47bbc0ada0 100644 --- a/ext/opentelemetry-ext-opentracing-shim/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Change reference names to opentelemetry-instrumentation-opentracing-shim + ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) + ## 0.3a0 Released 2019-12-11 diff --git a/ext/opentelemetry-ext-opentracing-shim/LICENSE b/instrumentation/opentelemetry-instrumentation-opentracing-shim/LICENSE similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/LICENSE rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/LICENSE diff --git a/ext/opentelemetry-ext-opentracing-shim/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-opentracing-shim/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/MANIFEST.in diff --git a/ext/opentelemetry-ext-opentracing-shim/README.rst b/instrumentation/opentelemetry-instrumentation-opentracing-shim/README.rst similarity index 83% rename from ext/opentelemetry-ext-opentracing-shim/README.rst rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/README.rst index 3bba15f167..7a8413ef59 100644 --- a/ext/opentelemetry-ext-opentracing-shim/README.rst +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/README.rst @@ -16,5 +16,5 @@ Installation References ---------- -* `OpenTracing Shim for OpenTelemetry `_ +* `OpenTracing Shim for OpenTelemetry `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg similarity index 95% rename from ext/opentelemetry-ext-opentracing-shim/setup.cfg rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg index f0878b82b6..1a822d49ce 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-opentracing-shim +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-opentracing-shim platforms = any license = Apache-2.0 classifiers = diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.py new file mode 100644 index 0000000000..f5d71a86b5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "opentracing_shim", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py similarity index 99% rename from ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py index 8d8afca5ca..22c67c53ea 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py @@ -30,7 +30,7 @@ from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.ext.opentracing_shim import create_tracer + from opentelemetry.instrumentation.opentracing_shim import create_tracer # Tell OpenTelemetry which Tracer implementation to use. trace.set_tracer_provider(TracerProvider()) @@ -93,8 +93,8 @@ from opentelemetry import propagators from opentelemetry.context import Context from opentelemetry.correlationcontext import get_correlation, set_correlation -from opentelemetry.ext.opentracing_shim import util -from opentelemetry.ext.opentracing_shim.version import __version__ +from opentelemetry.instrumentation.opentracing_shim import util +from opentelemetry.instrumentation.opentracing_shim.version import __version__ from opentelemetry.trace import ( INVALID_SPAN_CONTEXT, DefaultSpan, diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/util.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/util.py diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/__init__.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py similarity index 98% rename from ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py index 635907bc90..8b46a4bcb3 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py @@ -22,7 +22,7 @@ import opentracing from opentelemetry import propagators, trace -from opentelemetry.ext.opentracing_shim import ( +from opentelemetry.instrumentation.opentracing_shim import ( SpanContextShim, SpanShim, create_tracer, @@ -137,7 +137,7 @@ def test_explicit_start_time(self): with self.shim.start_active_span("TestSpan", start_time=now) as scope: result = util.time_seconds_from_ns(scope.span.unwrap().start_time) # Tolerate inaccuracies of less than a microsecond. See Note: - # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.ext.opentracing_shim.html + # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.instrumentation.opentracing_shim.html # TODO: This seems to work consistently, but we should find out the # biggest possible loss of precision. self.assertAlmostEqual(result, now, places=6) @@ -151,7 +151,7 @@ def test_explicit_end_time(self): end_time = util.time_seconds_from_ns(span.unwrap().end_time) # Tolerate inaccuracies of less than a microsecond. See Note: - # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.ext.opentracing_shim.html + # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.instrumentation.opentracing_shim.html # TODO: This seems to work consistently, but we should find out the # biggest possible loss of precision. self.assertAlmostEqual(end_time, now, places=6) @@ -422,7 +422,7 @@ def test_log_kv(self): ) self.assertEqual(span.unwrap().events[1].attributes["foo"], "bar") # Tolerate inaccuracies of less than a microsecond. See Note: - # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.ext.opentracing_shim.html + # https://open-telemetry.github.io/opentelemetry-python/instrumentation/opentracing_shim/opentracing_shim.html # TODO: This seems to work consistently, but we should find out the # biggest possible loss of precision. self.assertAlmostEqual(result, now, places=6) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_util.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_util.py similarity index 97% rename from ext/opentelemetry-ext-opentracing-shim/tests/test_util.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_util.py index cbbd4b075a..806a8da609 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_util.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_util.py @@ -15,7 +15,7 @@ import time import unittest -from opentelemetry.ext.opentracing_shim import util +from opentelemetry.instrumentation.opentracing_shim import util from opentelemetry.util import time_ns diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/README.rst similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/README.rst diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/__init__.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/__init__.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/__init__.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py similarity index 92% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py index b3b4271f02..c12bbfa029 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py @@ -1,4 +1,4 @@ -import opentelemetry.ext.opentracing_shim as opentracingshim +import opentelemetry.instrumentation.opentracing_shim as opentracingshim from opentelemetry.sdk import trace from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/README.rst similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/README.rst diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/__init__.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/__init__.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/__init__.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/test_threads.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/test_threads.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/README.rst similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/README.rst diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/README.rst similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/README.rst diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/README.rst similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/README.rst diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/testcase.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/testcase.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/utils.py similarity index 100% rename from ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py rename to instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/utils.py diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md index c997f23945..e5f81a558b 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased - Change package name to opentelemetry-instrumentation-psycopg2 - ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) ## 0.8b0 diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md index c549095d2e..a01e0e2c53 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased - Change package name to opentelemetry-instrumentation-pymemcache - ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) ## Version 0.10b0 diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md index 59f8e35551..a92d2b53da 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased - Change package name to opentelemetry-instrumentation-pymongo - ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) ## 0.7b1 diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md index f9b59fcf7a..9fea902d18 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased - Change package name to opentelemetry-instrumentation-pymysql - ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) ## 0.7b1 diff --git a/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md index 79c4e6bf92..bac3153a81 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased - Change package name to opentelemetry-instrumentation-redis - ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) ## 0.7b1 diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md index 3022345ac7..f46080a9b9 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased - Change package name to opentelemetry-instrumentation-sqlalchemy - ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) ## 0.7b1 diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md index 7804da9051..12d001bdf1 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased - Change package name to opentelemetry-instrumentation-sqlite3 - ([#999](https://github.com/open-telemetry/opentelemetry-python/pull/999)) + ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) ## 0.8b0 diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md new file mode 100644 index 0000000000..f752d8314d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## Unreleased + +- Change package name to opentelemetry-instrumentation-system-metrics + ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) + +## 0.9b0 + +Released 2020-06-10 + +- Initial release (https://github.com/open-telemetry/opentelemetry-python/pull/652) diff --git a/ext/opentelemetry-ext-system-metrics/LICENSE b/instrumentation/opentelemetry-instrumentation-system-metrics/LICENSE similarity index 100% rename from ext/opentelemetry-ext-system-metrics/LICENSE rename to instrumentation/opentelemetry-instrumentation-system-metrics/LICENSE diff --git a/ext/opentelemetry-ext-system-metrics/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-system-metrics/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-system-metrics/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-system-metrics/MANIFEST.in diff --git a/ext/opentelemetry-ext-system-metrics/README.rst b/instrumentation/opentelemetry-instrumentation-system-metrics/README.rst similarity index 50% rename from ext/opentelemetry-ext-system-metrics/README.rst rename to instrumentation/opentelemetry-instrumentation-system-metrics/README.rst index 796f3f13f2..fc984256bf 100644 --- a/ext/opentelemetry-ext-system-metrics/README.rst +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/README.rst @@ -3,8 +3,8 @@ OpenTelemetry System Metrics Instrumentation |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-system-metrics.svg - :target: https://pypi.org/project/opentelemetry-ext-system-metrics/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-system-metrics.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-system-metrics/ Instrumentation to collect system performance metrics. @@ -14,11 +14,11 @@ Installation :: - pip install opentelemetry-ext-system-metrics + pip install opentelemetry-instrumentation-system-metrics References ---------- -* `OpenTelemetry System Metrics Instrumentation `_ +* `OpenTelemetry System Metrics Instrumentation `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-system-metrics/setup.cfg b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg similarity index 92% rename from ext/opentelemetry-ext-system-metrics/setup.cfg rename to instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg index 08a1fe8f1c..4b6f4f07f5 100644 --- a/ext/opentelemetry-ext-system-metrics/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-system-metrics +name = opentelemetry-instrumentation-system-metrics description = OpenTelemetry System Metrics Instrumentation long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-system-metrics +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-system-metrics platforms = any license = Apache-2.0 classifiers = diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.py b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.py new file mode 100644 index 0000000000..f0bbf9eff0 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "system_metrics", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py similarity index 99% rename from ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py rename to instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py index 10b0979557..fcd96f8210 100644 --- a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py @@ -29,7 +29,7 @@ .. code:: python from opentelemetry import metrics - from opentelemetry.ext.system_metrics import SystemMetrics + from opentelemetry.instrumentation.system_metrics import SystemMetrics from opentelemetry.sdk.metrics import MeterProvider, from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter diff --git a/ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py similarity index 100% rename from ext/opentelemetry-ext-system-metrics/src/opentelemetry/ext/system_metrics/version.py rename to instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py diff --git a/ext/opentelemetry-ext-system-metrics/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-system-metrics/tests/__init__.py rename to instrumentation/opentelemetry-instrumentation-system-metrics/tests/__init__.py diff --git a/ext/opentelemetry-ext-system-metrics/tests/test_system_metrics.py b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py similarity index 99% rename from ext/opentelemetry-ext-system-metrics/tests/test_system_metrics.py rename to instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py index b2d6bab401..b9ae662af1 100644 --- a/ext/opentelemetry-ext-system-metrics/tests/test_system_metrics.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py @@ -16,7 +16,7 @@ from unittest import mock from opentelemetry import metrics -from opentelemetry.ext.system_metrics import SystemMetrics +from opentelemetry.instrumentation.system_metrics import SystemMetrics from opentelemetry.test.test_base import TestBase diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 7b512a84ce..125032b95d 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -27,16 +27,16 @@ instrumentations = { "asgi": "opentelemetry-instrumentation-asgi>=0.11b0", "asyncpg": "opentelemetry-instrumentation-asyncpg>=0.11b0", - "boto": "opentelemetry-ext-boto>=0.11b0", - "botocore": "opentelemetry-ext-botocore>=0.11b0", - "celery": "opentelemetry-ext-celery>=0.11b0", + "boto": "opentelemetry-instrumentation-boto>=0.11b0", + "botocore": "opentelemetry-instrumentation-botocore>=0.11b0", + "celery": "opentelemetry-instrumentation-celery>=0.11b0", "dbapi": "opentelemetry-instrumentation-dbapi>=0.8b0", "django": "opentelemetry-instrumentation-django>=0.8b0", - "elasticsearch": "opentelemetry-ext-elasticsearch>=0.11b0", + "elasticsearch": "opentelemetry-instrumentation-elasticsearch>=0.11b0", "fastapi": "opentelemetry-instrumentation-fastapi>=0.11b0", "flask": "opentelemetry-instrumentation-flask>=0.8b0", - "grpc": "opentelemetry-ext-grpc>=0.8b0", - "jinja2": "opentelemetry-ext-jinja2>=0.8b0", + "grpc": "opentelemetry-instrumentation-grpc>=0.8b0", + "jinja2": "opentelemetry-instrumentation-jinja2>=0.8b0", "mysql": "opentelemetry-instrumentation-mysql>=0.8b0", "psycopg2": "opentelemetry-instrumentation-psycopg2>=0.8b0", "pymemcache": "opentelemetry-instrumentation-pymemcache>=0.11b0", @@ -55,16 +55,16 @@ libraries = { "asgi": ("opentelemetry-instrumentation-asgi",), "asyncpg": ("opentelemetry-instrumentation-asyncpg",), - "boto": ("opentelemetry-ext-boto",), - "botocore": ("opentelemetry-ext-botocore",), - "celery": ("opentelemetry-ext-celery",), + "boto": ("opentelemetry-instrumentation-boto",), + "botocore": ("opentelemetry-instrumentation-botocore",), + "celery": ("opentelemetry-instrumentation-celery",), "dbapi": ("opentelemetry-instrumentation-dbapi",), "django": ("opentelemetry-instrumentation-django",), - "elasticsearch": ("opentelemetry-ext-elasticsearch",), + "elasticsearch": ("opentelemetry-instrumentation-elasticsearch",), "fastapi": ("opentelemetry-instrumentation-fastapi",), "flask": ("opentelemetry-instrumentation-flask",), - "grpc": ("opentelemetry-ext-grpc",), - "jinja2": ("opentelemetry-ext-jinja2",), + "grpc": ("opentelemetry-instrumentation-grpc",), + "jinja2": ("opentelemetry-instrumentation-jinja2",), "mysql": ("opentelemetry-instrumentation-mysql",), "psycopg2": ("opentelemetry-instrumentation-psycopg2",), "pymemcache": ("opentelemetry-instrumentation-pymemcache",), diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 830381225f..7d3daa2af7 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -7,7 +7,7 @@ function cov { then pytest \ --ignore-glob=*/setup.py \ - --ignore-glob=ext/opentelemetry-ext-opentracing-shim/tests/testbed/* \ + --ignore-glob=instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/* \ --cov ${1} \ --cov-append \ --cov-branch \ @@ -35,7 +35,7 @@ cov exporter/opentelemetry-exporter-datadog cov instrumentation/opentelemetry-instrumentation-flask cov instrumentation/opentelemetry-instrumentation-requests cov exporter/opentelemetry-exporter-jaeger -cov ext/opentelemetry-ext-opentracing-shim +cov instrumentation/opentelemetry-instrumentation-opentracing-shim cov instrumentation/opentelemetry-instrumentation-wsgi cov exporter/opentelemetry-exporter-zipkin cov docs/examples/opentelemetry-example-app diff --git a/ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py b/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/asyncpg/test_asyncpg_functional.py rename to tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/celery/conftest.py b/tests/opentelemetry-docker-tests/tests/celery/conftest.py similarity index 97% rename from ext/opentelemetry-ext-docker-tests/tests/celery/conftest.py rename to tests/opentelemetry-docker-tests/tests/celery/conftest.py index 0e6976382e..085fe3bab1 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/celery/conftest.py +++ b/tests/opentelemetry-docker-tests/tests/celery/conftest.py @@ -18,7 +18,7 @@ import pytest from opentelemetry import trace as trace_api -from opentelemetry.ext.celery import CeleryInstrumentor +from opentelemetry.instrumentation.celery import CeleryInstrumentor from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, diff --git a/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py b/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py similarity index 97% rename from ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py rename to tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py index a3967a5612..2714c8ee03 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/celery/test_celery_functional.py +++ b/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py @@ -20,9 +20,9 @@ from celery import signals from celery.exceptions import Retry -import opentelemetry.ext.celery +import opentelemetry.instrumentation.celery from opentelemetry import trace as trace_api -from opentelemetry.ext.celery import CeleryInstrumentor +from opentelemetry.instrumentation.celery import CeleryInstrumentor from opentelemetry.sdk import resources from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.trace.status import StatusCanonicalCode @@ -51,18 +51,19 @@ def fn_task(): assert ( async_span.instrumentation_info.name - == opentelemetry.ext.celery.__name__ + == opentelemetry.instrumentation.celery.__name__ ) assert ( async_span.instrumentation_info.version - == opentelemetry.ext.celery.__version__ + == opentelemetry.instrumentation.celery.__version__ ) assert ( - run_span.instrumentation_info.name == opentelemetry.ext.celery.__name__ + run_span.instrumentation_info.name + == opentelemetry.instrumentation.celery.__name__ ) assert ( run_span.instrumentation_info.version - == opentelemetry.ext.celery.__version__ + == opentelemetry.instrumentation.celery.__version__ ) diff --git a/ext/opentelemetry-ext-docker-tests/tests/check_availability.py b/tests/opentelemetry-docker-tests/tests/check_availability.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/check_availability.py rename to tests/opentelemetry-docker-tests/tests/check_availability.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml b/tests/opentelemetry-docker-tests/tests/docker-compose.yml similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml rename to tests/opentelemetry-docker-tests/tests/docker-compose.yml diff --git a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py b/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py rename to tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py b/tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/opencensus/test_opencensusexporter_functional.py rename to tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_aiopg_functional.py b/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/postgres/test_aiopg_functional.py rename to tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py b/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py rename to tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py b/tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py rename to tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py rename to tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/redis/test_redis_functional.py b/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/redis/test_redis_functional.py rename to tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py diff --git a/ext/opentelemetry-ext-grpc/tests/__init__.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-grpc/tests/__init__.py rename to tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/__init__.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/mixins.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/mixins.py rename to tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_instrument.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_instrument.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_instrument.py rename to tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_instrument.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_mysql.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_mysql.py rename to tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_postgres.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_postgres.py rename to tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_sqlite.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py similarity index 100% rename from ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_sqlite.py rename to tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py diff --git a/tox.ini b/tox.ini index 8528b63f60..91024f7f3d 100644 --- a/tox.ini +++ b/tox.ini @@ -36,7 +36,7 @@ envlist = py3{5,6,7,8}-test-instrumentation-aiopg ; instrumentation-aiopg intentionally excluded from pypy3 - ; opentelemetry-ext-botocore + ; opentelemetry-instrumentation-botocore py3{6,7,8}-test-instrumentation-botocore pypy3-test-instrumentation-botocore @@ -48,7 +48,7 @@ envlist = py3{4,5,6,7,8}-test-instrumentation-dbapi pypy3-test-instrumentation-dbapi - ; opentelemetry-ext-boto + ; opentelemetry-instrumentation-boto py3{5,6,7,8}-test-instrumentation-boto pypy3-test-instrumentation-boto @@ -74,7 +74,7 @@ envlist = py3{6,7,8}-test-instrumentation-starlette pypy3-test-instrumentation-starlette - ; opentelemetry-ext-jinja2 + ; opentelemetry-instrumentation-jinja2 py3{4,5,6,7,8}-test-instrumentation-jinja2 pypy3-test-instrumentation-jinja2 @@ -145,7 +145,7 @@ envlist = py3{4,5,6,7,8}-test-core-opentracing-shim pypy3-test-core-opentracing-shim - ; opentelemetry-ext-grpc + ; opentelemetry-instrumentation-grpc py3{5,6,7,8}-test-instrumentation-grpc ; opentelemetry-instrumentation-sqlalchemy @@ -156,13 +156,13 @@ envlist = py3{4,5,6,7,8}-test-instrumentation-redis pypy3-test-instrumentation-redis - ; opentelemetry-ext-celery + ; opentelemetry-instrumentation-celery py3{5,6,7,8}-test-instrumentation-celery pypy3-test-instrumentation-celery - ; opentelemetry-ext-system-metrics + ; opentelemetry-instrumentation-system-metrics py3{4,5,6,7,8}-test-instrumentation-system-metrics - ; ext-system-metrics intentionally excluded from pypy3 + ; instrumentation-system-metrics intentionally excluded from pypy3 ; known limitation: gc.get_count won't work under pypy lint @@ -195,23 +195,23 @@ changedir = test-core-proto: opentelemetry-proto/tests test-core-instrumentation: opentelemetry-instrumentation/tests test-core-getting-started: docs/getting_started/tests - test-core-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests + test-core-opentracing-shim: instrumentation/opentelemetry-instrumentation-opentracing-shim/tests test-instrumentation-aiohttp-client: instrumentation/opentelemetry-instrumentation-aiohttp-client/tests test-instrumentation-aiopg: instrumentation/opentelemetry-instrumentation-aiopg/tests test-instrumentation-asgi: instrumentation/opentelemetry-instrumentation-asgi/tests test-instrumentation-asyncpg: instrumentation/opentelemetry-instrumentation-asyncpg/tests - test-instrumentation-boto: ext/opentelemetry-ext-boto/tests - test-instrumentation-botocore: ext/opentelemetry-ext-botocore/tests - test-instrumentation-celery: ext/opentelemetry-ext-celery/tests + test-instrumentation-boto: instrumentation/opentelemetry-instrumentation-boto/tests + test-instrumentation-botocore: instrumentation/opentelemetry-instrumentation-botocore/tests + test-instrumentation-celery: instrumentation/opentelemetry-instrumentation-celery/tests test-instrumentation-dbapi: instrumentation/opentelemetry-instrumentation-dbapi/tests test-instrumentation-django: instrumentation/opentelemetry-instrumentation-django/tests test-instrumentation-example-app: docs/examples/opentelemetry-example-app/tests - test-instrumentation-elasticsearch{2,5,6,7}: ext/opentelemetry-ext-elasticsearch/tests + test-instrumentation-elasticsearch{2,5,6,7}: instrumentation/opentelemetry-instrumentation-elasticsearch/tests test-instrumentation-fastapi: instrumentation/opentelemetry-instrumentation-fastapi/tests test-instrumentation-flask: instrumentation/opentelemetry-instrumentation-flask/tests - test-instrumentation-grpc: ext/opentelemetry-ext-grpc/tests - test-instrumentation-jinja2: ext/opentelemetry-ext-jinja2/tests + test-instrumentation-grpc: instrumentation/opentelemetry-instrumentation-grpc/tests + test-instrumentation-jinja2: instrumentation/opentelemetry-instrumentation-jinja2/tests test-instrumentation-mysql: instrumentation/opentelemetry-instrumentation-mysql/tests test-instrumentation-psycopg2: instrumentation/opentelemetry-instrumentation-psycopg2/tests test-instrumentation-pymemcache: instrumentation/opentelemetry-instrumentation-pymemcache/tests @@ -223,7 +223,7 @@ changedir = test-instrumentation-sqlalchemy: instrumentation/opentelemetry-instrumentation-sqlalchemy/tests test-instrumentation-sqlite3: instrumentation/opentelemetry-instrumentation-sqlite3/tests test-instrumentation-starlette: instrumentation/opentelemetry-instrumentation-starlette/tests - test-instrumentation-system-metrics: ext/opentelemetry-ext-system-metrics/tests + test-instrumentation-system-metrics: instrumentation/opentelemetry-instrumentation-system-metrics/tests test-instrumentation-wsgi: instrumentation/opentelemetry-instrumentation-wsgi/tests test-exporter-jaeger: exporter/opentelemetry-exporter-jaeger/tests @@ -241,27 +241,27 @@ commands_pre = test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util test-core-proto: pip install {toxinidir}/opentelemetry-proto - ext,instrumentation: pip install {toxinidir}/opentelemetry-instrumentation + instrumentation: pip install {toxinidir}/opentelemetry-instrumentation example-app: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi {toxinidir}/instrumentation/opentelemetry-instrumentation-flask {toxinidir}/docs/examples/opentelemetry-example-app getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/instrumentation/opentelemetry-instrumentation-requests -e {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/instrumentation/opentelemetry-instrumentation-flask - celery: pip install {toxinidir}/ext/opentelemetry-ext-celery[test] + celery: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-celery[test] - grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc[test] + grpc: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc[test] wsgi,flask,django,pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi asgi,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg - boto: pip install {toxinidir}/ext/opentelemetry-ext-botocore[test] - boto: pip install {toxinidir}/ext/opentelemetry-ext-boto[test] + boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test] + boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-boto[test] flask: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test] - botocore: pip install {toxinidir}/ext/opentelemetry-ext-botocore[test] + botocore: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test] dbapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi[test] @@ -296,7 +296,7 @@ commands_pre = starlette: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette[test] - jinja2: pip install {toxinidir}/ext/opentelemetry-ext-jinja2[test] + jinja2: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2[test] aiohttp-client: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client @@ -305,7 +305,7 @@ commands_pre = jaeger: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk - opentracing-shim: pip install {toxinidir}/ext/opentelemetry-ext-opentracing-shim + opentracing-shim: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-opentracing-shim datadog: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/exporter/opentelemetry-exporter-datadog @@ -313,9 +313,9 @@ commands_pre = sqlalchemy: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy - system-metrics: pip install {toxinidir}/ext/opentelemetry-ext-system-metrics[test] + system-metrics: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics[test] - elasticsearch{2,5,6,7}: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/ext/opentelemetry-ext-elasticsearch[test] + elasticsearch{2,5,6,7}: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch[test] ; In order to get a healthy coverage report, ; we have to install packages in editable mode. @@ -405,7 +405,7 @@ deps = celery ~= 4.0, != 4.4.4 changedir = - ext/opentelemetry-ext-docker-tests/tests + tests/opentelemetry-docker-tests/tests commands_pre = pip install -e {toxinidir}/opentelemetry-api \ @@ -413,7 +413,7 @@ commands_pre = -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/tests/util \ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg \ - -e {toxinidir}/ext/opentelemetry-ext-celery \ + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-celery \ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi \ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql \ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2 \ @@ -422,7 +422,7 @@ commands_pre = -e {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy \ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg \ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-redis \ - -e {toxinidir}/ext/opentelemetry-ext-system-metrics \ + -e {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics \ -e {toxinidir}/exporter/opentelemetry-exporter-opencensus docker-compose up -d python check_availability.py From 955890092249b6050e0dc7b919038ca275dcfa30 Mon Sep 17 00:00:00 2001 From: Alec Koumjian Date: Wed, 5 Aug 2020 11:03:07 -0400 Subject: [PATCH 0491/1517] instrumentation/redis: Change Redis instrument to use SpanKind.CLIENT (#965) --- .../CHANGELOG.md | 3 ++- .../instrumentation/redis/__init__.py | 8 ++++++-- .../tests/test_redis.py | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md index bac3153a81..e7f8d24874 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Update default SpanKind to `SpanKind.CLIENT` ([#965](https://github.com/open-telemetry/opentelemetry-python/pull/965)) - Change package name to opentelemetry-instrumentation-redis ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) @@ -9,4 +10,4 @@ Released 2020-05-12 -- Initial release \ No newline at end of file +- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py index fef856041e..e2ab78b535 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py @@ -72,7 +72,9 @@ def _set_connection_attributes(span, conn): def _traced_execute_command(func, instance, args, kwargs): tracer = getattr(redis, "_opentelemetry_tracer") query = _format_command_args(args) - with tracer.start_as_current_span(_CMD) as span: + with tracer.start_as_current_span( + _CMD, kind=trace.SpanKind.CLIENT + ) as span: span.set_attribute("service", tracer.instrumentation_info.name) span.set_attribute(_RAWCMD, query) _set_connection_attributes(span, instance) @@ -86,7 +88,9 @@ def _traced_execute_pipeline(func, instance, args, kwargs): cmds = [_format_command_args(c) for c, _ in instance.command_stack] resource = "\n".join(cmds) - with tracer.start_as_current_span(_CMD) as span: + with tracer.start_as_current_span( + _CMD, kind=trace.SpanKind.CLIENT + ) as span: span.set_attribute("service", tracer.instrumentation_info.name) span.set_attribute(_RAWCMD, resource) _set_connection_attributes(span, instance) diff --git a/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py index 86b2459c11..c306dca363 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py +++ b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py @@ -17,9 +17,23 @@ from opentelemetry.instrumentation.redis import RedisInstrumentor from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import SpanKind class TestRedis(TestBase): + def test_span_properties(self): + redis_client = redis.Redis() + RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) + + with mock.patch.object(redis_client, "connection"): + redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.name, "redis.command") + self.assertEqual(span.kind, SpanKind.CLIENT) + def test_instrument_uninstrument(self): redis_client = redis.Redis() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) From 81025e1e159dc341d96a7c90df8f6e7b26e1cb90 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Wed, 5 Aug 2020 09:27:09 -0700 Subject: [PATCH 0492/1517] docs: Add propagators section to getting started (#963) --- docs/getting-started.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/getting-started.rst b/docs/getting-started.rst index e9dd831e90..198bd0f1e3 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -174,6 +174,29 @@ Now run the above script, hit the root url (http://localhost:5000/) a few times, python flask_example.py +Configure Your HTTP Propagator (b3, CorrelationContext) +------------------------------------------------------- + +A major feature of distributed tracing is the ability to correlate a trace across +multiple services. However, those services need to propagate information about a +trace from one service to the other. + +To enable this, OpenTelemetry has the concept of `propagators `_, +which provide a common method to encode and decode span information from a request and response, +respectively. + +By default, opentelemetry-python is configured to use the `W3C Trace Context `_ +HTTP headers for HTTP requests. This can be configured to leverage different propagators. Here's +an example using Zipkin's `b3 propagation `_: + +.. code-block:: python + + from opentelemetry import propagators + from opentelemetry.sdk.trace.propagation.b3_format import B3Format + + propagators.set_global_httptextformat(B3Format()) + + Adding Metrics -------------- From 2e700889e64710149b2ebdb6b0ce07d15a708425 Mon Sep 17 00:00:00 2001 From: Simon THOBY Date: Wed, 5 Aug 2020 20:29:02 +0200 Subject: [PATCH 0493/1517] ext/django: accept middlewares declared as tuples (#950) --- .../src/opentelemetry/instrumentation/django/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index e0992e9a30..d9566affc0 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -51,8 +51,12 @@ def _instrument(self, **kwargs): # https://docs.djangoproject.com/en/3.0/ref/middleware/#middleware-ordering settings_middleware = getattr(settings, "MIDDLEWARE", []) - settings_middleware.append(self._opentelemetry_middleware) + # Django allows to specify middlewares as a tuple, so we convert this tuple to a + # list, otherwise we wouldn't be able to call append/remove + if isinstance(settings_middleware, tuple): + settings_middleware = list(settings_middleware) + settings_middleware.append(self._opentelemetry_middleware) setattr(settings, "MIDDLEWARE", settings_middleware) def _uninstrument(self, **kwargs): From 8b1da35bc58fa64b92ab042b0e95aac6d03e4b72 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 5 Aug 2020 13:18:33 -0600 Subject: [PATCH 0494/1517] opentracing-shim: Return consistent ScopeShim objects (#922) This uses the OpenTelemetry context management mechanism to store a ScopeShim object in order to make active return the same object as the one returned by start_active_span. --- .../opentracing_shim/__init__.py | 19 +++---- .../tests/test_shim.py | 50 +++++++++++++------ 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py index 22c67c53ea..6c76444e6e 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py @@ -91,7 +91,7 @@ from deprecated import deprecated from opentelemetry import propagators -from opentelemetry.context import Context +from opentelemetry.context import Context, attach, detach, get_value, set_value from opentelemetry.correlationcontext import get_correlation, set_correlation from opentelemetry.instrumentation.opentracing_shim import util from opentelemetry.instrumentation.opentracing_shim.version import __version__ @@ -327,6 +327,7 @@ class ScopeShim(opentracing.Scope): def __init__(self, manager, span, span_cm=None): super().__init__(manager, span) self._span_cm = span_cm + self._token = attach(set_value("scope_shim", self)) # TODO: Change type of `manager` argument to `opentracing.ScopeManager`? We # need to get rid of `manager.tracer` for this. @@ -382,6 +383,8 @@ def close(self): *finish_on_close* when activating the span. """ + detach(self._token) + if self._span_cm is not None: # We don't have error information to pass to `__exit__()` so we # pass `None` in all arguments. If the OpenTelemetry tracer @@ -460,14 +463,12 @@ def active(self): if span.get_context() == INVALID_SPAN_CONTEXT: return None - span_context = SpanContextShim(span.get_context()) - wrapped_span = SpanShim(self._tracer, span_context, span) - return ScopeShim(self, span=wrapped_span) - # TODO: The returned `ScopeShim` instance here always ends the - # corresponding span, regardless of the `finish_on_close` value used - # when activating the span. This is because here we return a *new* - # `ScopeShim` rather than returning a saved instance of `ScopeShim`. - # https://github.com/open-telemetry/opentelemetry-python/pull/211/files#r335398792 + try: + return get_value("scope_shim") + except KeyError: + span_context = SpanContextShim(span.get_context()) + wrapped_span = SpanShim(self._tracer, span_context, span) + return ScopeShim(self, span=wrapped_span) @property def tracer(self): diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py index 8b46a4bcb3..445ce6ce16 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py @@ -63,7 +63,7 @@ def test_shim_type(self): def test_start_active_span(self): """Test span creation and activation using `start_active_span()`.""" - with self.shim.start_active_span("TestSpan") as scope: + with self.shim.start_active_span("TestSpan0") as scope: # Verify correct type of Scope and Span objects. self.assertIsInstance(scope, opentracing.Scope) self.assertIsInstance(scope.span, opentracing.Span) @@ -91,7 +91,7 @@ def test_start_active_span(self): def test_start_span(self): """Test span creation using `start_span()`.""" - with self.shim.start_span("TestSpan") as span: + with self.shim.start_span("TestSpan1") as span: # Verify correct type of Span object. self.assertIsInstance(span, opentracing.Span) @@ -107,7 +107,7 @@ def test_start_span(self): def test_start_span_no_contextmanager(self): """Test `start_span()` without a `with` statement.""" - span = self.shim.start_span("TestSpan") + span = self.shim.start_span("TestSpan2") # Verify span is started. self.assertIsNotNone(span.unwrap().start_time) @@ -120,7 +120,7 @@ def test_start_span_no_contextmanager(self): def test_explicit_span_finish(self): """Test `finish()` method on `Span` objects.""" - span = self.shim.start_span("TestSpan") + span = self.shim.start_span("TestSpan3") # Verify span hasn't ended. self.assertIsNone(span.unwrap().end_time) @@ -134,7 +134,7 @@ def test_explicit_start_time(self): """Test `start_time` argument.""" now = time.time() - with self.shim.start_active_span("TestSpan", start_time=now) as scope: + with self.shim.start_active_span("TestSpan4", start_time=now) as scope: result = util.time_seconds_from_ns(scope.span.unwrap().start_time) # Tolerate inaccuracies of less than a microsecond. See Note: # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.instrumentation.opentracing_shim.html @@ -145,7 +145,7 @@ def test_explicit_start_time(self): def test_explicit_end_time(self): """Test `end_time` argument of `finish()` method.""" - span = self.shim.start_span("TestSpan") + span = self.shim.start_span("TestSpan5") now = time.time() span.finish(now) @@ -159,7 +159,7 @@ def test_explicit_end_time(self): def test_explicit_span_activation(self): """Test manual activation and deactivation of a span.""" - span = self.shim.start_span("TestSpan") + span = self.shim.start_span("TestSpan6") # Verify no span is currently active. self.assertIsNone(self.shim.active_span) @@ -180,7 +180,7 @@ def test_start_active_span_finish_on_close(self): """Test `finish_on_close` argument of `start_active_span()`.""" with self.shim.start_active_span( - "TestSpan", finish_on_close=True + "TestSpan7", finish_on_close=True ) as scope: # Verify span hasn't ended. self.assertIsNone(scope.span.unwrap().end_time) @@ -189,7 +189,7 @@ def test_start_active_span_finish_on_close(self): self.assertIsNotNone(scope.span.unwrap().end_time) with self.shim.start_active_span( - "TestSpan", finish_on_close=False + "TestSpan8", finish_on_close=False ) as scope: # Verify span hasn't ended. self.assertIsNone(scope.span.unwrap().end_time) @@ -202,7 +202,7 @@ def test_start_active_span_finish_on_close(self): def test_activate_finish_on_close(self): """Test `finish_on_close` argument of `activate()`.""" - span = self.shim.start_span("TestSpan") + span = self.shim.start_span("TestSpan9") with self.shim.scope_manager.activate( span, finish_on_close=True @@ -216,7 +216,7 @@ def test_activate_finish_on_close(self): # Verify span has ended. self.assertIsNotNone(span.unwrap().end_time) - span = self.shim.start_span("TestSpan") + span = self.shim.start_span("TestSpan10") with self.shim.scope_manager.activate( span, finish_on_close=False @@ -402,13 +402,13 @@ def test_tags(self): def test_span_tracer(self): """Test the `tracer` property on `Span` objects.""" - with self.shim.start_active_span("TestSpan") as scope: + with self.shim.start_active_span("TestSpan11") as scope: self.assertEqual(scope.span.tracer, self.shim) def test_log_kv(self): """Test the `log_kv()` method on `Span` objects.""" - with self.shim.start_span("TestSpan") as span: + with self.shim.start_span("TestSpan12") as span: span.log_kv({"foo": "bar"}) self.assertEqual(span.unwrap().events[0].attributes["foo"], "bar") # Verify timestamp was generated automatically. @@ -430,7 +430,7 @@ def test_log_kv(self): def test_log(self): """Test the deprecated `log` method on `Span` objects.""" - with self.shim.start_span("TestSpan") as span: + with self.shim.start_span("TestSpan13") as span: with self.assertWarns(DeprecationWarning): span.log(event="foo", payload="bar") @@ -441,7 +441,7 @@ def test_log(self): def test_log_event(self): """Test the deprecated `log_event` method on `Span` objects.""" - with self.shim.start_span("TestSpan") as span: + with self.shim.start_span("TestSpan14") as span: with self.assertWarns(DeprecationWarning): span.log_event("foo", "bar") @@ -557,6 +557,7 @@ def test_extract_binary(self): self.shim.extract(opentracing.Format.BINARY, bytearray()) def test_baggage(self): + """Test SpanShim baggage being set and being immutable""" span_context_shim = SpanContextShim( trace.SpanContext(1234, 5678, is_remote=False) @@ -572,3 +573,22 @@ def test_baggage(self): span_shim.set_baggage_item(1, 2) self.assertTrue(span_shim.get_baggage_item(1), 2) + + def test_active(self): + """Test that the active property and start_active_span return the same + object""" + + # Verify no span is currently active. + self.assertIsNone(self.shim.active_span) + + with self.shim.start_active_span("TestSpan15") as scope: + # Verify span is active. + self.assertEqual( + self.shim.active_span.context.unwrap(), + scope.span.context.unwrap(), + ) + + self.assertIs(self.shim.scope_manager.active, scope) + + # Verify no span is active. + self.assertIsNone(self.shim.active_span) From 590c32cffc61762894e2ea782cfc2d00889478e3 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 5 Aug 2020 23:01:13 -0600 Subject: [PATCH 0495/1517] Handle B3 trace_id and span_id correctly (#934) --- .../sdk/trace/propagation/b3_format.py | 21 ++++++++- .../tests/trace/propagation/test_b3_format.py | 45 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py index c1f71d4d87..901a5772f8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py @@ -13,9 +13,11 @@ # limitations under the License. import typing +from re import compile as re_compile import opentelemetry.trace as trace from opentelemetry.context import Context +from opentelemetry.sdk.trace import generate_span_id, generate_trace_id from opentelemetry.trace.propagation.httptextformat import ( Getter, HTTPTextFormat, @@ -37,6 +39,8 @@ class B3Format(HTTPTextFormat): SAMPLED_KEY = "x-b3-sampled" FLAGS_KEY = "x-b3-flags" _SAMPLE_PROPAGATE_VALUES = set(["1", "True", "true", "d"]) + _trace_id_regex = re_compile(r"[\da-fA-F]{16}|[\da-fA-F]{32}") + _span_id_regex = re_compile(r"[\da-fA-F]{16}") def extract( self, @@ -95,6 +99,18 @@ def extract( or flags ) + if ( + self._trace_id_regex.fullmatch(trace_id) is None + or self._span_id_regex.fullmatch(span_id) is None + ): + trace_id = generate_trace_id() + span_id = generate_span_id() + sampled = "0" + + else: + trace_id = int(trace_id, 16) + span_id = int(span_id, 16) + options = 0 # The b3 spec provides no defined behavior for both sample and # flag values set. Since the setting of at least one implies @@ -102,12 +118,13 @@ def extract( # header is set to allow. if sampled in self._SAMPLE_PROPAGATE_VALUES or flags == "1": options |= trace.TraceFlags.SAMPLED + return trace.set_span_in_context( trace.DefaultSpan( trace.SpanContext( # trace an span ids are encoded in hex, so must be converted - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), + trace_id=trace_id, + span_id=span_id, is_remote=True, trace_flags=trace.TraceFlags(options), trace_state=trace.TraceState(), diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index a5bd1baaa4..bc508f3fd9 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest +from unittest.mock import patch import opentelemetry.sdk.trace as trace import opentelemetry.sdk.trace.propagation.b3_format as b3_format @@ -245,6 +246,50 @@ def test_missing_trace_id(self): span_context = trace_api.get_current_span(ctx).get_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) + @patch("opentelemetry.sdk.trace.propagation.b3_format.generate_trace_id") + @patch("opentelemetry.sdk.trace.propagation.b3_format.generate_span_id") + def test_invalid_trace_id( + self, mock_generate_span_id, mock_generate_trace_id + ): + """If a trace id is invalid, generate a trace id.""" + + mock_generate_trace_id.configure_mock(return_value=1) + mock_generate_span_id.configure_mock(return_value=2) + + carrier = { + FORMAT.TRACE_ID_KEY: "abc123", + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1", + } + + ctx = FORMAT.extract(get_as_list, carrier) + span_context = trace_api.get_current_span(ctx).get_context() + + self.assertEqual(span_context.trace_id, 1) + self.assertEqual(span_context.span_id, 2) + + @patch("opentelemetry.sdk.trace.propagation.b3_format.generate_trace_id") + @patch("opentelemetry.sdk.trace.propagation.b3_format.generate_span_id") + def test_invalid_span_id( + self, mock_generate_span_id, mock_generate_trace_id + ): + """If a span id is invalid, generate a trace id.""" + + mock_generate_trace_id.configure_mock(return_value=1) + mock_generate_span_id.configure_mock(return_value=2) + + carrier = { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: "abc123", + FORMAT.FLAGS_KEY: "1", + } + + ctx = FORMAT.extract(get_as_list, carrier) + span_context = trace_api.get_current_span(ctx).get_context() + + self.assertEqual(span_context.trace_id, 1) + self.assertEqual(span_context.span_id, 2) + def test_missing_span_id(self): """If a trace id is missing, populate an invalid trace id.""" carrier = { From 85a2f435c0d1344210c38196ed43bc1a4aba984c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 6 Aug 2020 09:19:57 -0600 Subject: [PATCH 0496/1517] Stop TracerProvider from being overridden (#959) Co-authored-by: Leighton Chen --- opentelemetry-api/CHANGELOG.md | 2 + .../src/opentelemetry/metrics/__init__.py | 3 +- .../src/opentelemetry/trace/__init__.py | 72 ++++++++++--------- .../tests/metrics/test_globals.py | 4 +- opentelemetry-api/tests/trace/test_globals.py | 6 +- .../util/src/opentelemetry/test/test_base.py | 12 ++++ 6 files changed, 61 insertions(+), 38 deletions(-) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 2226b5d027..a04aa8a48f 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -4,6 +4,8 @@ - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) +- Stop TracerProvider and MeterProvider from being overridden + ([#959](https://github.com/open-telemetry/opentelemetry-python/pull/959)) ## Version 0.11b0 diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index bd2e3b8573..7d279731ea 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -459,7 +459,8 @@ def set_meter_provider(meter_provider: MeterProvider) -> None: global _METER_PROVIDER # pylint: disable=global-statement if _METER_PROVIDER is not None: - logger.warning("Overriding current MeterProvider") + logger.warning("Overriding of current MeterProvider is not allowed") + return _METER_PROVIDER = meter_provider diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index ffda0a15e7..6c1bf46cc9 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -70,44 +70,13 @@ `set_tracer_provider`. """ -__all__ = [ - "DEFAULT_TRACE_OPTIONS", - "DEFAULT_TRACE_STATE", - "INVALID_SPAN", - "INVALID_SPAN_CONTEXT", - "INVALID_SPAN_ID", - "INVALID_TRACE_ID", - "DefaultSpan", - "DefaultTracer", - "DefaultTracerProvider", - "LazyLink", - "Link", - "LinkBase", - "ParentSpan", - "Span", - "SpanContext", - "SpanKind", - "TraceFlags", - "TraceState", - "TracerProvider", - "Tracer", - "format_span_id", - "format_trace_id", - "get_current_span", - "get_tracer", - "get_tracer_provider", - "set_tracer_provider", - "set_span_in_context", -] import abc import enum -import types as python_types import typing from contextlib import contextmanager from logging import getLogger -from opentelemetry.configuration import Configuration from opentelemetry.trace.propagation import ( get_current_span, set_span_in_context, @@ -461,11 +430,16 @@ def get_tracer( def set_tracer_provider(tracer_provider: TracerProvider) -> None: - """Sets the current global :class:`~.TracerProvider` object.""" + """Sets the current global :class:`~.TracerProvider` object. + + This can only be done once, a warning will be logged if any furter attempt + is made. + """ global _TRACER_PROVIDER # pylint: disable=global-statement if _TRACER_PROVIDER is not None: - logger.warning("Overriding current TracerProvider") + logger.warning("Overriding of current TracerProvider is not allowed") + return _TRACER_PROVIDER = tracer_provider @@ -478,3 +452,35 @@ def get_tracer_provider() -> TracerProvider: _TRACER_PROVIDER = _load_trace_provider("tracer_provider") return _TRACER_PROVIDER + + +__all__ = [ + "DEFAULT_TRACE_OPTIONS", + "DEFAULT_TRACE_STATE", + "INVALID_SPAN", + "INVALID_SPAN_CONTEXT", + "INVALID_SPAN_ID", + "INVALID_TRACE_ID", + "DefaultSpan", + "DefaultTracer", + "DefaultTracerProvider", + "LazyLink", + "Link", + "LinkBase", + "ParentSpan", + "Span", + "SpanContext", + "SpanKind", + "TraceFlags", + "TraceState", + "TracerProvider", + "Tracer", + "format_span_id", + "format_trace_id", + "get_current_span", + "get_tracer", + "get_tracer_provider", + "set_tracer_provider", + "set_span_in_context", + "Status", +] diff --git a/opentelemetry-api/tests/metrics/test_globals.py b/opentelemetry-api/tests/metrics/test_globals.py index 9b9cfb94d9..513dd7dd2d 100644 --- a/opentelemetry-api/tests/metrics/test_globals.py +++ b/opentelemetry-api/tests/metrics/test_globals.py @@ -16,8 +16,8 @@ def test_meter_provider_override_warning(self): test.output, [ ( - "WARNING:opentelemetry.metrics:Overriding current " - "MeterProvider" + "WARNING:opentelemetry.metrics:Overriding of current " + "MeterProvider is not allowed" ) ], ) diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 4e105eb1f3..dc7c02e513 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -25,17 +25,19 @@ def test_get_tracer(self): def test_tracer_provider_override_warning(self): """trace.set_tracer_provider should throw a warning when overridden""" trace.set_tracer_provider(TracerProvider()) + tracer_provider = trace.get_tracer_provider() with self.assertLogs(level=WARNING) as test: trace.set_tracer_provider(TracerProvider()) self.assertEqual( test.output, [ ( - "WARNING:opentelemetry.trace:Overriding current " - "TracerProvider" + "WARNING:opentelemetry.trace:Overriding of current " + "TracerProvider is not allowed" ) ], ) + self.assertIs(tracer_provider, trace.get_tracer_provider()) class TestTracer(unittest.TestCase): diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index 96d98cff3d..e2d99c0acf 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -34,15 +34,27 @@ def setUpClass(cls): cls.original_tracer_provider = trace_api.get_tracer_provider() result = cls.create_tracer_provider() cls.tracer_provider, cls.memory_exporter = result + # This is done because set_tracer_provider cannot override the + # current tracer provider. + trace_api._TRACER_PROVIDER = None # pylint: disable=protected-access trace_api.set_tracer_provider(cls.tracer_provider) cls.original_meter_provider = metrics_api.get_meter_provider() result = cls.create_meter_provider() cls.meter_provider, cls.memory_metrics_exporter = result + # This is done because set_meter_provider cannot override the + # current meter provider. + metrics_api._METER_PROVIDER = None # pylint: disable=protected-access metrics_api.set_meter_provider(cls.meter_provider) @classmethod def tearDownClass(cls): + # This is done because set_tracer_provider cannot override the + # current tracer provider. + trace_api._TRACER_PROVIDER = None # pylint: disable=protected-access trace_api.set_tracer_provider(cls.original_tracer_provider) + # This is done because set_meter_provider cannot override the + # current meter provider. + metrics_api._METER_PROVIDER = None # pylint: disable=protected-access metrics_api.set_meter_provider(cls.original_meter_provider) def setUp(self): From c42c5f3b4ba0bcc46269a803239de344f8ba91be Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 6 Aug 2020 17:10:35 -0600 Subject: [PATCH 0497/1517] Fix grpc version to previous version (#975) --- instrumentation/opentelemetry-instrumentation-grpc/setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg index 0308a229f8..4d40278237 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: install_requires = opentelemetry-api == 0.12.dev0 opentelemetry-sdk == 0.12.dev0 - grpcio ~= 1.27 + grpcio == 1.30 [options.extras_require] test = From b3c2a0372e27eff4594891db8ad04c438e9fdf4c Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 7 Aug 2020 14:57:04 -0700 Subject: [PATCH 0498/1517] fix: update default OTLP port to 55680 (#977) --- exporter/opentelemetry-exporter-otlp/CHANGELOG.md | 2 ++ .../src/opentelemetry/exporter/otlp/__init__.py | 2 +- .../src/opentelemetry/exporter/otlp/trace_exporter/__init__.py | 2 +- .../tests/test_otlp_trace_exporter.py | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index bcaa7d1181..2379a94cb3 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -4,6 +4,8 @@ - Change package name to opentelemetry-exporter-otlp ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) +- Update default port to 55680 + ([#977](https://github.com/open-telemetry/opentelemetry-python/pull/977)) ## Version 0.11b0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py index dca0042a68..a078cb7ccc 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py @@ -43,7 +43,7 @@ trace.set_tracer_provider(TracerProvider(resource=resource))) tracer = trace.get_tracer(__name__) - otlp_exporter = OTLPSpanExporter(endpoint="localhost:55678") + otlp_exporter = OTLPSpanExporter(endpoint="localhost:55680") span_processor = BatchExportSpanProcessor(otlp_exporter) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index 47a862908e..be1419fa39 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -89,7 +89,7 @@ class OTLPSpanExporter(SpanExporter): def __init__( self, - endpoint="localhost:55678", + endpoint="localhost:55680", credentials: ChannelCredentials = None, metadata=None, ): diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index c7e26508b2..9058937f87 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -98,7 +98,7 @@ def setUp(self): self.server = server(ThreadPoolExecutor(max_workers=10)) - self.server.add_insecure_port("[::]:55678") + self.server.add_insecure_port("[::]:55680") self.server.start() From 76b1ed0d6f4d41df0075953dcfbece72dd4bb344 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 12 Aug 2020 10:29:56 -0500 Subject: [PATCH 0499/1517] Renaming OTEL_RESOURCE env var (#973) --- opentelemetry-sdk/CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/resources/__init__.py | 2 +- .../tests/resources/test_resources.py | 14 +++++++------- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 34032424ff..923087f421 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -7,6 +7,8 @@ ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - Implement Views in metrics SDK ([#596](https://github.com/open-telemetry/opentelemetry-python/pull/596)) +- Update environment variable OTEL_RESOURCE to OTEL_RESOURCE_ATTRIBUTES as per + the specification ## Version 0.11b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index a8e9ac65be..d9752e3b3c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -74,7 +74,7 @@ def detect(self) -> "Resource": class OTELResourceDetector(ResourceDetector): # pylint: disable=no-self-use def detect(self) -> "Resource": - env_resources_items = os.environ.get("OTEL_RESOURCE") + env_resources_items = os.environ.get("OTEL_RESOURCE_ATTRIBUTES") env_resource_map = {} if env_resources_items: env_resource_map = { diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 3aafad1cf3..84d0cf2ae5 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -167,36 +167,36 @@ def test_resource_detector_raise_error(self): class TestOTELResourceDetector(unittest.TestCase): def setUp(self) -> None: - os.environ["OTEL_RESOURCE"] = "" + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "" def tearDown(self) -> None: - os.environ.pop("OTEL_RESOURCE") + os.environ.pop("OTEL_RESOURCE_ATTRIBUTES") def test_empty(self): detector = resources.OTELResourceDetector() - os.environ["OTEL_RESOURCE"] = "" + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "" self.assertEqual(detector.detect(), resources.Resource.create_empty()) def test_one(self): detector = resources.OTELResourceDetector() - os.environ["OTEL_RESOURCE"] = "k=v" + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "k=v" self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) def test_one_with_whitespace(self): detector = resources.OTELResourceDetector() - os.environ["OTEL_RESOURCE"] = " k = v " + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = " k = v " self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) def test_multiple(self): detector = resources.OTELResourceDetector() - os.environ["OTEL_RESOURCE"] = "k=v,k2=v2" + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "k=v,k2=v2" self.assertEqual( detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) ) def test_multiple_with_whitespace(self): detector = resources.OTELResourceDetector() - os.environ["OTEL_RESOURCE"] = " k = v , k2 = v2 " + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = " k = v , k2 = v2 " self.assertEqual( detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) ) From cffc016baa53e0732a26120668554c6f5954f370 Mon Sep 17 00:00:00 2001 From: bitspradp Date: Thu, 13 Aug 2020 18:30:18 +0100 Subject: [PATCH 0500/1517] Add protocol as an argument to the Jaeger exporter constructor (#978) --- .../opentelemetry-exporter-jaeger/CHANGELOG.md | 3 +++ .../src/opentelemetry/exporter/jaeger/__init__.py | 14 ++++++++++---- .../tests/test_jaeger_exporter.py | 5 +++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md index 7b9a03d312..48f7afd397 100644 --- a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md @@ -5,6 +5,9 @@ - Change package name to opentelemetry-exporter-jaeger ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) +- Thrift URL for Jaeger exporter doesn't allow HTTPS (hardcoded to HTTP) + ([#978] (https://github.com/open-telemetry/opentelemetry-python/pull/978)) + ## 0.8b0 Released 2020-05-27 diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index 993bf4f087..afa0b2578f 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -42,6 +42,7 @@ # collector_host_name='localhost', # collector_port=14268, # collector_endpoint='/api/traces?format=jaeger.thrift', + # collector_protocol='http', # username=xxxx, # optional # password=xxxx, # optional ) @@ -77,6 +78,7 @@ DEFAULT_AGENT_HOST_NAME = "localhost" DEFAULT_AGENT_PORT = 6831 DEFAULT_COLLECTOR_ENDPOINT = "/api/traces?format=jaeger.thrift" +DEFAULT_COLLECTOR_PROTOCOL = "http" UDP_PACKET_MAX_LENGTH = 65000 @@ -91,10 +93,11 @@ class JaegerSpanExporter(SpanExporter): when query for spans. agent_host_name: The host name of the Jaeger-Agent. agent_port: The port of the Jaeger-Agent. - collector_host_name: The host name of the Jaeger-Collector HTTP + collector_host_name: The host name of the Jaeger-Collector HTTP/HTTPS Thrift. - collector_port: The port of the Jaeger-Collector HTTP Thrift. - collector_endpoint: The endpoint of the Jaeger-Collector HTTP Thrift. + collector_port: The port of the Jaeger-Collector HTTP/HTTPS Thrift. + collector_endpoint: The endpoint of the Jaeger-Collector HTTP/HTTPS Thrift. + collector_protocol: The transfer protocol for the Jaeger-Collector(HTTP or HTTPS). username: The user name of the Basic Auth if authentication is required. password: The password of the Basic Auth if authentication is @@ -109,6 +112,7 @@ def __init__( collector_host_name=None, collector_port=None, collector_endpoint=DEFAULT_COLLECTOR_ENDPOINT, + collector_protocol=DEFAULT_COLLECTOR_PROTOCOL, username=None, password=None, ): @@ -119,6 +123,7 @@ def __init__( self.collector_host_name = collector_host_name self.collector_port = collector_port self.collector_endpoint = collector_endpoint + self.collector_protocol = collector_protocol self.username = username self.password = password self._collector = None @@ -139,7 +144,8 @@ def collector(self): if self.collector_host_name is None or self.collector_port is None: return None - thrift_url = "http://{}:{}{}".format( + thrift_url = "{}://{}:{}{}".format( + self.collector_protocol, self.collector_host_name, self.collector_port, self.collector_endpoint, diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py index 30b7c85826..7b3916c5b9 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py @@ -46,6 +46,7 @@ def test_constructor_default(self): thrift_port = None agent_port = 6831 collector_endpoint = "/api/traces?format=jaeger.thrift" + collector_protocol = "http" exporter = jaeger_exporter.JaegerSpanExporter(service_name) self.assertEqual(exporter.service_name, service_name) @@ -53,6 +54,7 @@ def test_constructor_default(self): self.assertEqual(exporter.agent_host_name, host_name) self.assertEqual(exporter.agent_port, agent_port) self.assertEqual(exporter.collector_port, thrift_port) + self.assertEqual(exporter.collector_protocol, collector_protocol) self.assertEqual(exporter.collector_endpoint, collector_endpoint) self.assertEqual(exporter.username, None) self.assertEqual(exporter.password, None) @@ -65,6 +67,7 @@ def test_constructor_explicit(self): collector_host_name = "opentelemetry.io" collector_port = 15875 collector_endpoint = "/myapi/traces?format=jaeger.thrift" + collector_protocol = "https" agent_port = 14268 agent_host_name = "opentelemetry.io" @@ -78,6 +81,7 @@ def test_constructor_explicit(self): collector_host_name=collector_host_name, collector_port=collector_port, collector_endpoint=collector_endpoint, + collector_protocol="https", agent_host_name=agent_host_name, agent_port=agent_port, username=username, @@ -88,6 +92,7 @@ def test_constructor_explicit(self): self.assertEqual(exporter.agent_port, agent_port) self.assertEqual(exporter.collector_host_name, collector_host_name) self.assertEqual(exporter.collector_port, collector_port) + self.assertEqual(exporter.collector_protocol, collector_protocol) self.assertTrue(exporter.collector is not None) self.assertEqual(exporter.collector.auth, auth) # property should not construct new object From 3efe8541c886c55a70a6505c0b0f2503f210ee8e Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 13 Aug 2020 15:35:38 -0400 Subject: [PATCH 0501/1517] OC Exporter - send start_timestamp, resource labels, and convert labels to strings (#937) --- .../CHANGELOG.md | 2 + .../opencensus/metrics_exporter/__init__.py | 53 ++++++++- .../test_otcollector_metrics_exporter.py | 111 ++++++++++++++++-- 3 files changed, 155 insertions(+), 11 deletions(-) diff --git a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md index c7f0cf69b5..13924e489b 100644 --- a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md @@ -4,6 +4,8 @@ - Change package name to opentelemetry-exporter-opencensus ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) +- Send start_timestamp and convert labels to strings + ([#937](https://github.com/open-telemetry/opentelemetry-python/pull/937)) ## 0.8b0 diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py index e83e779df6..76986a8a59 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py @@ -15,14 +15,16 @@ """OpenCensus Collector Metrics Exporter.""" import logging -from typing import Sequence +from typing import Dict, Sequence import grpc +from google.protobuf.timestamp_pb2 import Timestamp from opencensus.proto.agent.metrics.v1 import ( metrics_service_pb2, metrics_service_pb2_grpc, ) from opencensus.proto.metrics.v1 import metrics_pb2 +from opencensus.proto.resource.v1 import resource_pb2 import opentelemetry.exporter.opencensus.util as utils from opentelemetry.sdk.metrics import Counter, Metric @@ -34,6 +36,14 @@ DEFAULT_ENDPOINT = "localhost:55678" +# In priority order. See collector impl https://bit.ly/2DvJW6y +_OT_LABEL_PRESENCE_TO_RESOURCE_TYPE = ( + ("container.name", "container"), + ("k8s.pod.name", "k8s"), + ("host.name", "host"), + ("cloud.provider", "cloud"), +) + logger = logging.getLogger(__name__) @@ -65,6 +75,8 @@ def __init__( self.client = client self.node = utils.get_node(service_name, host_name) + self.exporter_start_timestamp = Timestamp() + self.exporter_start_timestamp.GetCurrentTime() def export( self, metric_records: Sequence[MetricRecord] @@ -89,7 +101,9 @@ def shutdown(self) -> None: def generate_metrics_requests( self, metrics: Sequence[MetricRecord] ) -> metrics_service_pb2.ExportMetricsServiceRequest: - collector_metrics = translate_to_collector(metrics) + collector_metrics = translate_to_collector( + metrics, self.exporter_start_timestamp + ) service_request = metrics_service_pb2.ExportMetricsServiceRequest( node=self.node, metrics=collector_metrics ) @@ -99,6 +113,7 @@ def generate_metrics_requests( # pylint: disable=too-many-branches def translate_to_collector( metric_records: Sequence[MetricRecord], + exporter_start_timestamp: Timestamp, ) -> Sequence[metrics_pb2.Metric]: collector_metrics = [] for metric_record in metric_records: @@ -109,7 +124,8 @@ def translate_to_collector( label_keys.append(metrics_pb2.LabelKey(key=label_tuple[0])) label_values.append( metrics_pb2.LabelValue( - has_value=label_tuple[1] is not None, value=label_tuple[1] + has_value=label_tuple[1] is not None, + value=str(label_tuple[1]), ) ) @@ -121,13 +137,23 @@ def translate_to_collector( label_keys=label_keys, ) + # If cumulative and stateful, explicitly set the start_timestamp to + # exporter start time. + if metric_record.instrument.meter.batcher.stateful: + start_timestamp = exporter_start_timestamp + else: + start_timestamp = None + timeseries = metrics_pb2.TimeSeries( label_values=label_values, points=[get_collector_point(metric_record)], + start_timestamp=start_timestamp, ) collector_metrics.append( metrics_pb2.Metric( - metric_descriptor=metric_descriptor, timeseries=[timeseries] + metric_descriptor=metric_descriptor, + timeseries=[timeseries], + resource=get_resource(metric_record), ) ) return collector_metrics @@ -162,3 +188,22 @@ def get_collector_point(metric_record: MetricRecord) -> metrics_pb2.Point: ) ) return point + + +def get_resource(metric_record: MetricRecord) -> resource_pb2.Resource: + resource_labels = metric_record.instrument.meter.resource.labels + return resource_pb2.Resource( + type=infer_oc_resource_type(resource_labels), + labels={k: str(v) for k, v in resource_labels.items()}, + ) + + +def infer_oc_resource_type(resource_labels: Dict[str, str]) -> str: + """Convert from OT resource labels to OC resource type""" + for ( + ot_resource_key, + oc_resource_type, + ) in _OT_LABEL_PRESENCE_TO_RESOURCE_TYPE: + if ot_resource_key in resource_labels: + return oc_resource_type + return "" diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py index 1403c6c59e..1ec1a57448 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py @@ -32,6 +32,7 @@ MetricsExportResult, aggregate, ) +from opentelemetry.sdk.resources import Resource # pylint: disable=no-member @@ -39,9 +40,16 @@ class TestCollectorMetricsExporter(unittest.TestCase): @classmethod def setUpClass(cls): # pylint: disable=protected-access - metrics.set_meter_provider(MeterProvider()) + cls._resource_labels = { + "key_with_str_value": "some string", + "key_with_int_val": 321, + "key_with_true": True, + } + metrics.set_meter_provider( + MeterProvider(resource=Resource(cls._resource_labels)) + ) cls._meter = metrics.get_meter(__name__) - cls._labels = {"environment": "staging"} + cls._labels = {"environment": "staging", "number": 321} cls._key_labels = get_dict_as_key(cls._labels) def test_constructor(self): @@ -119,7 +127,7 @@ def test_export(self): client=mock_client, host_name=host_name ) test_metric = self._meter.create_metric( - "testname", "testdesc", "unit", int, Counter, + "testname", "testdesc", "unit", int, Counter, self._labels.keys(), ) record = MetricRecord( test_metric, self._key_labels, aggregate.SumAggregator(), @@ -142,13 +150,16 @@ def test_export(self): def test_translate_to_collector(self): test_metric = self._meter.create_metric( - "testname", "testdesc", "unit", int, Counter, + "testname", "testdesc", "unit", int, Counter, self._labels.keys() ) aggregator = aggregate.SumAggregator() aggregator.update(123) aggregator.take_checkpoint() record = MetricRecord(test_metric, self._key_labels, aggregator,) - output_metrics = metrics_exporter.translate_to_collector([record]) + start_timestamp = Timestamp() + output_metrics = metrics_exporter.translate_to_collector( + [record], start_timestamp, + ) self.assertEqual(len(output_metrics), 1) self.assertIsInstance(output_metrics[0], metrics_pb2.Metric) self.assertEqual(output_metrics[0].metric_descriptor.name, "testname") @@ -161,14 +172,44 @@ def test_translate_to_collector(self): metrics_pb2.MetricDescriptor.CUMULATIVE_INT64, ) self.assertEqual( - len(output_metrics[0].metric_descriptor.label_keys), 1 + len(output_metrics[0].metric_descriptor.label_keys), 2 ) self.assertEqual( output_metrics[0].metric_descriptor.label_keys[0].key, "environment", ) + self.assertEqual( + output_metrics[0].metric_descriptor.label_keys[1].key, "number", + ) + + self.assertIsNotNone(output_metrics[0].resource) + self.assertEqual( + output_metrics[0].resource.type, "", + ) + self.assertEqual( + output_metrics[0].resource.labels["key_with_str_value"], + self._resource_labels["key_with_str_value"], + ) + self.assertIsInstance( + output_metrics[0].resource.labels["key_with_int_val"], str, + ) + self.assertEqual( + output_metrics[0].resource.labels["key_with_int_val"], + str(self._resource_labels["key_with_int_val"]), + ) + self.assertIsInstance( + output_metrics[0].resource.labels["key_with_true"], str, + ) + self.assertEqual( + output_metrics[0].resource.labels["key_with_true"], + str(self._resource_labels["key_with_true"]), + ) + self.assertEqual(len(output_metrics[0].timeseries), 1) - self.assertEqual(len(output_metrics[0].timeseries[0].label_values), 1) + self.assertEqual(len(output_metrics[0].timeseries[0].label_values), 2) + self.assertEqual( + output_metrics[0].timeseries[0].start_timestamp, start_timestamp + ) self.assertEqual( output_metrics[0].timeseries[0].label_values[0].has_value, True ) @@ -187,3 +228,59 @@ def test_translate_to_collector(self): self.assertEqual( output_metrics[0].timeseries[0].points[0].int64_value, 123 ) + + def test_infer_ot_resource_type(self): + # empty resource + self.assertEqual(metrics_exporter.infer_oc_resource_type({}), "") + + # container + self.assertEqual( + metrics_exporter.infer_oc_resource_type( + { + "k8s.cluster.name": "cluster1", + "k8s.pod.name": "pod1", + "k8s.namespace.name": "namespace1", + "container.name": "container-name1", + "cloud.account.id": "proj1", + "cloud.zone": "zone1", + } + ), + "container", + ) + + # k8s pod + self.assertEqual( + metrics_exporter.infer_oc_resource_type( + { + "k8s.cluster.name": "cluster1", + "k8s.pod.name": "pod1", + "k8s.namespace.name": "namespace1", + "cloud.zone": "zone1", + } + ), + "k8s", + ) + + # host + self.assertEqual( + metrics_exporter.infer_oc_resource_type( + { + "k8s.cluster.name": "cluster1", + "cloud.zone": "zone1", + "host.name": "node1", + } + ), + "host", + ) + + # cloud + self.assertEqual( + metrics_exporter.infer_oc_resource_type( + { + "cloud.provider": "gcp", + "host.id": "inst1", + "cloud.zone": "zone1", + } + ), + "cloud", + ) From e8f7b6ff684318730a64d1f330653d52919c99d6 Mon Sep 17 00:00:00 2001 From: Connor Adams Date: Thu, 13 Aug 2020 22:13:36 -0400 Subject: [PATCH 0502/1517] views: properly hash config dict, don't copy aggregator when stateful (#967) --- .../sdk/metrics/export/batcher.py | 28 +++-- .../src/opentelemetry/sdk/util/__init__.py | 11 +- opentelemetry-sdk/tests/metrics/test_view.py | 114 +++++++++++++++++- 3 files changed, 138 insertions(+), 15 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 79b27674c0..1c1858ebba 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -61,6 +61,7 @@ def process(self, record) -> None: """Stores record information to be ready for exporting.""" # Checkpoints the current aggregator value to be collected for export aggregator = record.aggregator + aggregator.take_checkpoint() # The uniqueness of a batch record is defined by a specific metric # using an aggregator type with a specific set of labels. @@ -72,19 +73,20 @@ def process(self, record) -> None: get_dict_as_key(aggregator.config), record.labels, ) + batch_value = self._batch_map.get(key) + if batch_value: - if batch_value != aggregator: - aggregator.take_checkpoint() - batch_value.merge(aggregator) - else: - aggregator.take_checkpoint() - - if self.stateful: - # if stateful batcher, create a copy of the aggregator and update - # it with the current checkpointed value for long-term storage - aggregator = record.aggregator.__class__( - config=record.aggregator.config - ) - aggregator.merge(record.aggregator) + # Update the stored checkpointed value if exists. The call to merge + # here combines only identical records (same key). + batch_value.merge(aggregator) + return + + # create a copy of the aggregator and update + # it with the current checkpointed value for long-term storage + aggregator = record.aggregator.__class__( + config=record.aggregator.config + ) + aggregator.merge(record.aggregator) + self._batch_map[key] = aggregator diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index 2e0d2e85a8..09d7283cab 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -33,7 +33,16 @@ def ns_to_iso_str(nanoseconds): def get_dict_as_key(labels): """Converts a dict to be used as a unique key""" - return tuple(sorted(labels.items())) + return tuple( + sorted( + map( + lambda kv: (kv[0], tuple(kv[1])) + if isinstance(kv[1], list) + else kv, + labels.items(), + ) + ) + ) class BoundedList(Sequence): diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py index 7dd7571d38..0de6b22731 100644 --- a/opentelemetry-sdk/tests/metrics/test_view.py +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -16,9 +16,10 @@ from unittest import mock from opentelemetry.sdk import metrics -from opentelemetry.sdk.metrics import Counter, view +from opentelemetry.sdk.metrics import Counter, ValueRecorder, view from opentelemetry.sdk.metrics.export import aggregate from opentelemetry.sdk.metrics.export.aggregate import ( + HistogramAggregator, MinMaxSumCountAggregator, SumAggregator, ) @@ -185,6 +186,117 @@ def test_multiple_views(self): self.assertTrue((label2, 5) in sum_set) +class TestHistogramView(unittest.TestCase): + def test_histogram_stateful(self): + meter = metrics.MeterProvider(stateful=True).get_meter(__name__) + exporter = InMemoryMetricsExporter() + controller = PushController(meter, exporter, 30) + + requests_size = meter.create_metric( + name="requests_size", + description="size of requests", + unit="1", + value_type=int, + metric_type=ValueRecorder, + ) + + size_view = View( + requests_size, + HistogramAggregator, + aggregator_config={"bounds": [20, 40, 60, 80, 100]}, + label_keys=["environment"], + view_config=ViewConfig.LABEL_KEYS, + ) + + meter.register_view(size_view) + + # Since this is using the HistogramAggregator, the bucket counts will be reflected + # with each record + requests_size.record(25, {"environment": "staging", "test": "value"}) + requests_size.record(1, {"environment": "staging", "test": "value2"}) + requests_size.record(200, {"environment": "staging", "test": "value3"}) + + controller.tick() + + metrics_list = exporter.get_exported_metrics() + self.assertEqual(len(metrics_list), 1) + checkpoint = metrics_list[0].aggregator.checkpoint + self.assertEqual( + tuple(checkpoint.items()), + ((20, 1), (40, 1), (60, 0), (80, 0), (100, 0), (">", 1)), + ) + exporter.clear() + + requests_size.record(25, {"environment": "staging", "test": "value"}) + requests_size.record(1, {"environment": "staging", "test": "value2"}) + requests_size.record(200, {"environment": "staging", "test": "value3"}) + + controller.tick() + + metrics_list = exporter.get_exported_metrics() + self.assertEqual(len(metrics_list), 1) + checkpoint = metrics_list[0].aggregator.checkpoint + self.assertEqual( + tuple(checkpoint.items()), + ((20, 2), (40, 2), (60, 0), (80, 0), (100, 0), (">", 2)), + ) + + def test_histogram_stateless(self): + # Use the meter type provided by the SDK package + meter = metrics.MeterProvider(stateful=False).get_meter(__name__) + exporter = InMemoryMetricsExporter() + controller = PushController(meter, exporter, 30) + + requests_size = meter.create_metric( + name="requests_size", + description="size of requests", + unit="1", + value_type=int, + metric_type=ValueRecorder, + ) + + size_view = View( + requests_size, + HistogramAggregator, + aggregator_config={"bounds": [20, 40, 60, 80, 100]}, + label_keys=["environment"], + view_config=ViewConfig.LABEL_KEYS, + ) + + meter.register_view(size_view) + + # Since this is using the HistogramAggregator, the bucket counts will be reflected + # with each record + requests_size.record(25, {"environment": "staging", "test": "value"}) + requests_size.record(1, {"environment": "staging", "test": "value2"}) + requests_size.record(200, {"environment": "staging", "test": "value3"}) + + controller.tick() + + metrics_list = exporter.get_exported_metrics() + self.assertEqual(len(metrics_list), 1) + checkpoint = metrics_list[0].aggregator.checkpoint + self.assertEqual( + tuple(checkpoint.items()), + ((20, 1), (40, 1), (60, 0), (80, 0), (100, 0), (">", 1)), + ) + exporter.clear() + + requests_size.record(25, {"environment": "staging", "test": "value"}) + requests_size.record(1, {"environment": "staging", "test": "value2"}) + requests_size.record(200, {"environment": "staging", "test": "value3"}) + + controller.tick() + + metrics_list = exporter.get_exported_metrics() + self.assertEqual(len(metrics_list), 1) + checkpoint = metrics_list[0].aggregator.checkpoint + self.assertEqual( + tuple(checkpoint.items()), + ((20, 1), (40, 1), (60, 0), (80, 0), (100, 0), (">", 1)), + ) + + class DummyMetric(metrics.Metric): # pylint: disable=W0231 def __init__(self): From 3341a8eb049cfc0348a5144d2188c68ee009087c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 14 Aug 2020 09:24:58 -0600 Subject: [PATCH 0503/1517] instrumentation/opentracing-shim: Preserve parent-child relationship between OpenTelemetry and OpenTracing spans (#924) --- .../opentracing_shim/__init__.py | 5 ++++ .../tests/test_shim.py | 29 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py index 6c76444e6e..dd05feecff 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py @@ -570,6 +570,11 @@ def start_active_span( :class:`ScopeManagerShim`. """ + current_span = get_current_span() + + if child_of is None and current_span is not INVALID_SPAN_CONTEXT: + child_of = SpanShim(None, None, current_span) + span = self.start_span( operation_name=operation_name, child_of=child_of, diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py index 445ce6ce16..c880913a87 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py @@ -557,7 +557,6 @@ def test_extract_binary(self): self.shim.extract(opentracing.Format.BINARY, bytearray()) def test_baggage(self): - """Test SpanShim baggage being set and being immutable""" span_context_shim = SpanContextShim( trace.SpanContext(1234, 5678, is_remote=False) @@ -592,3 +591,31 @@ def test_active(self): # Verify no span is active. self.assertIsNone(self.shim.active_span) + + def test_mixed_mode(self): + """Test that span parent-child relationship is kept between + OpenTelemetry and the OpenTracing shim""" + + span_shim = self.shim.start_span("TestSpan16") + + with self.shim.scope_manager.activate(span_shim, finish_on_close=True): + + with ( + TracerProvider() + .get_tracer(__name__) + .start_as_current_span("abc") + ) as opentelemetry_span: + + self.assertIs( + span_shim.unwrap().context, opentelemetry_span.parent, + ) + + with ( + TracerProvider().get_tracer(__name__).start_as_current_span("abc") + ) as opentelemetry_span: + + with self.shim.start_active_span("TestSpan17") as scope: + + self.assertIs( + scope.span.unwrap().parent, opentelemetry_span.context, + ) From 2bc7d35ee10eb9e5c4e03cc28472321f4f822509 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 14 Aug 2020 10:47:12 -0600 Subject: [PATCH 0504/1517] Revisit OpenTracing Shim docs (#925) Co-authored-by: Yusuke Tsutsumi --- .../opentracing_shim/__init__.py | 308 ++++++++++-------- 1 file changed, 164 insertions(+), 144 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py index dd05feecff..90d7f0a30c 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py @@ -32,7 +32,7 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.instrumentation.opentracing_shim import create_tracer - # Tell OpenTelemetry which Tracer implementation to use. + # Define which OpenTelemetry Tracer provider implementation to use. trace.set_tracer_provider(TracerProvider()) # Create an OpenTelemetry Tracer. @@ -86,27 +86,39 @@ # pylint:disable=no-member import logging +from typing import Optional, TypeVar, Union -import opentracing from deprecated import deprecated +from opentracing import ( + Format, + Scope, + ScopeManager, + Span, + SpanContext, + Tracer, + UnsupportedFormatException, +) from opentelemetry import propagators from opentelemetry.context import Context, attach, detach, get_value, set_value from opentelemetry.correlationcontext import get_correlation, set_correlation from opentelemetry.instrumentation.opentracing_shim import util from opentelemetry.instrumentation.opentracing_shim.version import __version__ +from opentelemetry.trace import INVALID_SPAN_CONTEXT, DefaultSpan, Link +from opentelemetry.trace import SpanContext as OtelSpanContext +from opentelemetry.trace import Tracer as OtelTracer from opentelemetry.trace import ( - INVALID_SPAN_CONTEXT, - DefaultSpan, - Link, + TracerProvider, get_current_span, set_span_in_context, ) +from opentelemetry.util.types import Attributes +ValueT = TypeVar("ValueT", int, float, bool, str) logger = logging.getLogger(__name__) -def create_tracer(otel_tracer_provider): +def create_tracer(otel_tracer_provider: TracerProvider) -> "TracerShim": """Creates a :class:`TracerShim` object from the provided OpenTelemetry :class:`opentelemetry.trace.TracerProvider`. @@ -114,10 +126,9 @@ def create_tracer(otel_tracer_provider): :class:`opentracing.Tracer` using OpenTelemetry under the hood. Args: - otel_tracer_provider: A :class:`opentelemetry.trace.TracerProvider` to be - used for constructing the :class:`TracerShim`. A tracer from this - source will be used to perform the actual tracing when user code is - instrumented using the OpenTracing API. + otel_tracer_provider: A tracer from this provider will be used to + perform the actual tracing when user code is instrumented using the + OpenTracing API. Returns: The created :class:`TracerShim`. @@ -126,7 +137,7 @@ def create_tracer(otel_tracer_provider): return TracerShim(otel_tracer_provider.get_tracer(__name__, __version__)) -class SpanContextShim(opentracing.SpanContext): +class SpanContextShim(SpanContext): """Implements :class:`opentracing.SpanContext` by wrapping a :class:`opentelemetry.trace.SpanContext` object. @@ -135,12 +146,12 @@ class SpanContextShim(opentracing.SpanContext): constructing the :class:`SpanContextShim`. """ - def __init__(self, otel_context): + def __init__(self, otel_context: OtelSpanContext): self._otel_context = otel_context # Context is being used here since it must be immutable. self._baggage = Context() - def unwrap(self): + def unwrap(self) -> OtelSpanContext: """Returns the wrapped :class:`opentelemetry.trace.SpanContext` object. @@ -152,15 +163,14 @@ def unwrap(self): return self._otel_context @property - def baggage(self): - """Implements the ``baggage`` property from the base class.""" + def baggage(self) -> Context: + """Returns the ``baggage`` associated with this object""" return self._baggage -class SpanShim(opentracing.Span): - """Implements :class:`opentracing.Span` by wrapping a - :class:`opentelemetry.trace.Span` object. +class SpanShim(Span): + """Wraps a :class:`opentelemetry.trace.Span` object. Args: tracer: The :class:`opentracing.Tracer` that created this `SpanShim`. @@ -169,7 +179,7 @@ class SpanShim(opentracing.Span): span: A :class:`opentelemetry.trace.Span` to wrap. """ - def __init__(self, tracer, context, span): + def __init__(self, tracer, context: SpanContextShim, span): super().__init__(tracer, context) self._otel_span = span @@ -183,10 +193,12 @@ def unwrap(self): return self._otel_span - def set_operation_name(self, operation_name): - """Implements the ``set_operation_name()`` method from the base class. + def set_operation_name(self, operation_name: str) -> "SpanShim": + """Updates the name of the wrapped OpenTelemetry span. - Updates the name of the wrapped OpenTelemetry span. + Args: + operation_name: The new name to be used for the underlying + :class:`opentelemetry.trace.Span` object. Returns: Returns this :class:`SpanShim` instance to allow call chaining. @@ -195,10 +207,8 @@ def set_operation_name(self, operation_name): self._otel_span.update_name(operation_name) return self - def finish(self, finish_time=None): - """Implements the ``finish()`` method from the base class. - - Ends the OpenTelemetry span wrapped by this :class:`SpanShim`. + def finish(self, finish_time: float = None): + """Ends the OpenTelemetry span wrapped by this :class:`SpanShim`. If *finish_time* is provided, the time value is converted to the OpenTelemetry time format (number of nanoseconds since the epoch, @@ -208,9 +218,9 @@ def finish(self, finish_time=None): when ending the span. Args: - finish_time(:obj:`float`, optional): An explicit finish time - expressed as the number of seconds since the epoch as returned - by :func:`time.time()`. Defaults to `None`. + finish_time: A value that represents the finish time expressed as + the number of seconds since the epoch as returned by + :func:`time.time()`. """ end_time = finish_time @@ -218,15 +228,12 @@ def finish(self, finish_time=None): end_time = util.time_seconds_to_ns(finish_time) self._otel_span.end(end_time=end_time) - def set_tag(self, key, value): - """Implements the ``set_tag()`` method from the base class. - - Sets an OpenTelemetry attribute on the wrapped OpenTelemetry span. + def set_tag(self, key: str, value: ValueT) -> "SpanShim": + """Sets an OpenTelemetry attribute on the wrapped OpenTelemetry span. Args: - key(:obj:`str`): A tag key. - value: A tag value. Can be one of :obj:`str`, :obj:`bool`, - :obj:`int`, :obj:`float` + key: A tag key. + value: A tag value. Returns: Returns this :class:`SpanShim` instance to allow call chaining. @@ -235,20 +242,23 @@ def set_tag(self, key, value): self._otel_span.set_attribute(key, value) return self - def log_kv(self, key_values, timestamp=None): - """Implements the ``log_kv()`` method from the base class. - - Logs an event for the wrapped OpenTelemetry span. + def log_kv( + self, key_values: Attributes, timestamp: float = None + ) -> "SpanShim": + """Logs an event for the wrapped OpenTelemetry span. Note: The OpenTracing API defines the values of *key_values* to be of any type. However, the OpenTelemetry API requires that the values be - one of :obj:`str`, :obj:`bool`, :obj:`float`. Therefore, only these - types are supported as values. + any one of the types defined in + ``opentelemetry.trace.util.Attributes`` therefore, only these types + are supported as values. Args: - key_values(:obj:`dict`): A dict with :obj:`str` keys and values of - type :obj:`str`, :obj:`bool` or :obj:`float`. + key_values: A dictionary as specified in + ``opentelemetry.trace.util.Attributes``. + timestamp: Timestamp of the OpenTelemetry event, will be generated + automatically if omitted. Returns: Returns this :class:`SpanShim` instance to allow call chaining. @@ -271,20 +281,32 @@ def log(self, **kwargs): def log_event(self, event, payload=None): super().log_event(event, payload=payload) - def set_baggage_item(self, key, value): - """Implements the ``set_baggage_item`` method from the base class.""" + def set_baggage_item(self, key: str, value: str): + """Stores a Baggage item in the span as a key/value + pair. + + Args: + key: A tag key. + value: A tag value. + """ # pylint: disable=protected-access self._context._baggage = set_correlation( key, value, context=self._context._baggage ) - def get_baggage_item(self, key): - """Implements the ``get_baggage_item`` method from the base class.""" + def get_baggage_item(self, key: str) -> Optional[object]: + """Retrieves value of the baggage item with the given key. + + Args: + key: A tag key. + Returns: + Returns this :class:`SpanShim` instance to allow call chaining. + """ # pylint: disable=protected-access return get_correlation(key, context=self._context._baggage) -class ScopeShim(opentracing.Scope): +class ScopeShim(Scope): """A `ScopeShim` wraps the OpenTelemetry functionality related to span activation/deactivation while using OpenTracing :class:`opentracing.Scope` objects for presentation. @@ -313,18 +335,16 @@ class ScopeShim(opentracing.Scope): manager: The :class:`ScopeManagerShim` that created this :class:`ScopeShim`. span: The :class:`SpanShim` this :class:`ScopeShim` controls. - span_cm(:class:`contextlib.AbstractContextManager`, optional): A - Python context manager which yields an OpenTelemetry + span_cm: A Python context manager which yields an OpenTelemetry `opentelemetry.trace.Span` from its ``__enter__()`` method. Used by :meth:`from_context_manager` to store the context manager as an attribute so that it can later be closed by calling its ``__exit__()`` method. Defaults to `None`. - - TODO: Is :class:`contextlib.AbstractContextManager` the correct - type for *span_cm*? """ - def __init__(self, manager, span, span_cm=None): + def __init__( + self, manager: "ScopeManagerShim", span: SpanShim, span_cm=None + ): super().__init__(manager, span) self._span_cm = span_cm self._token = attach(set_value("scope_shim", self)) @@ -332,7 +352,7 @@ def __init__(self, manager, span, span_cm=None): # TODO: Change type of `manager` argument to `opentracing.ScopeManager`? We # need to get rid of `manager.tracer` for this. @classmethod - def from_context_manager(cls, manager, span_cm): + def from_context_manager(cls, manager: "ScopeManagerShim", span_cm): """Constructs a :class:`ScopeShim` from an OpenTelemetry `opentelemetry.trace.Span` context manager. @@ -363,10 +383,8 @@ def from_context_manager(cls, manager, span_cm): return cls(manager, span, span_cm) def close(self): - """Implements the `close()` method from :class:`opentracing.Scope`. - - Closes the `ScopeShim`. If the `ScopeShim` was created from a context - manager, calling this method sets the active span in the + """Closes the `ScopeShim`. If the `ScopeShim` was created from a + context manager, calling this method sets the active span in the OpenTelemetry tracer back to the span which was active before this `ScopeShim` was created. In addition, if the span represented by this `ScopeShim` was activated with the *finish_on_close* argument set to @@ -396,7 +414,7 @@ def close(self): self._span.unwrap().end() -class ScopeManagerShim(opentracing.ScopeManager): +class ScopeManagerShim(ScopeManager): """Implements :class:`opentracing.ScopeManager` by setting and getting the active `opentelemetry.trace.Span` in the OpenTelemetry tracer. @@ -412,17 +430,15 @@ class ScopeManagerShim(opentracing.ScopeManager): span state. """ - def __init__(self, tracer): + def __init__(self, tracer: "TracerShim"): # The only thing the ``__init__()``` method on the base class does is # initialize `self._noop_span` and `self._noop_scope` with no-op # objects. Therefore, it doesn't seem useful to call it. # pylint: disable=super-init-not-called self._tracer = tracer - def activate(self, span, finish_on_close): - """Implements the ``activate()`` method from the base class. - - Activates a :class:`SpanShim` and returns a :class:`ScopeShim` which + def activate(self, span: SpanShim, finish_on_close: bool) -> "ScopeShim": + """Activates a :class:`SpanShim` and returns a :class:`ScopeShim` which represents the active span. Args: @@ -441,11 +457,9 @@ def activate(self, span, finish_on_close): return ScopeShim.from_context_manager(self, span_cm=span_cm) @property - def active(self): - """Implements the ``active`` property from the base class. - - Returns a :class:`ScopeShim` object representing the currently-active - span in the OpenTelemetry tracer. + def active(self) -> "ScopeShim": + """Returns a :class:`ScopeShim` object representing the + currently-active span in the OpenTelemetry tracer. Returns: A :class:`ScopeShim` representing the active span in the @@ -471,7 +485,7 @@ def active(self): return ScopeShim(self, span=wrapped_span) @property - def tracer(self): + def tracer(self) -> "TracerShim": """Returns the :class:`TracerShim` reference used by this :class:`ScopeManagerShim` for setting and getting the active span from the OpenTelemetry tracer. @@ -489,9 +503,8 @@ def tracer(self): return self._tracer -class TracerShim(opentracing.Tracer): - """Implements :class:`opentracing.Tracer` by wrapping a - :class:`opentelemetry.trace.Tracer` object. +class TracerShim(Tracer): + """Wraps a :class:`opentelemetry.trace.Tracer` object. This wrapper class allows using an OpenTelemetry tracer as if it were an OpenTracing tracer. It exposes the same methods as an "ordinary" @@ -507,12 +520,12 @@ class TracerShim(opentracing.Tracer): tracer will be invoked by the shim to create actual spans. """ - def __init__(self, tracer): + def __init__(self, tracer: OtelTracer): super().__init__(scope_manager=ScopeManagerShim(self)) self._otel_tracer = tracer self._supported_formats = ( - opentracing.Format.TEXT_MAP, - opentracing.Format.HTTP_HEADERS, + Format.TEXT_MAP, + Format.HTTP_HEADERS, ) def unwrap(self): @@ -527,43 +540,34 @@ def unwrap(self): def start_active_span( self, - operation_name, - child_of=None, - references=None, - tags=None, - start_time=None, - ignore_active_span=False, - finish_on_close=True, - ): - """Implements the ``start_active_span()`` method from the base class. - - Starts and activates a span. In terms of functionality, this method + operation_name: str, + child_of: Union[SpanShim, SpanContextShim] = None, + references: list = None, + tags: Attributes = None, + start_time: float = None, + ignore_active_span: bool = False, + finish_on_close: bool = True, + ) -> "ScopeShim": + """Starts and activates a span. In terms of functionality, this method behaves exactly like the same method on a "regular" OpenTracing tracer. See :meth:`opentracing.Tracer.start_active_span` for more details. Args: - operation_name(:obj:`str`): Name of the operation represented by + operation_name: Name of the operation represented by the new span from the perspective of the current service. - child_of(:class:`SpanShim` or :class:`SpanContextShim`, optional): - A :class:`SpanShim` or :class:`SpanContextShim` representing - the parent in a "child of" reference. If specified, the - *references* parameter must be omitted. Defaults to `None`. - references(:obj:`list`, optional): A list of - :class:`opentracing.Reference` objects that identify one or - more parents of type :class:`SpanContextShim`. Defaults to - `None`. - tags(:obj:`dict`, optional): A dictionary of tags. The keys must be - of type :obj:`str`. The values may be one of :obj:`str`, - :obj:`bool`, :obj:`int`, :obj:`float`. Defaults to `None`. - start_time(:obj:`float`, optional): An explicit start time - expressed as the number of seconds since the epoch as returned - by :func:`time.time()`. Defaults to `None`. - ignore_active_span(:obj:`bool`, optional): Ignore the - currently-active span in the OpenTelemetry tracer and make the - created span the root span of a new trace. Defaults to `False`. - finish_on_close(:obj:`bool`, optional): Determines whether the - created span should end automatically when closing the returned - :class:`ScopeShim`. Defaults to `True`. + child_of: A :class:`SpanShim` or :class:`SpanContextShim` + representing the parent in a "child of" reference. If + specified, the *references* parameter must be omitted. + references: A list of :class:`opentracing.Reference` objects that + identify one or more parents of type :class:`SpanContextShim`. + tags: A dictionary of tags. + start_time: An explicit start time expressed as the number of + seconds since the epoch as returned by :func:`time.time()`. + ignore_active_span: Ignore the currently-active span in the + OpenTelemetry tracer and make the created span the root span of + a new trace. + finish_on_close: Determines whether the created span should end + automatically when closing the returned :class:`ScopeShim`. Returns: A :class:`ScopeShim` that is already activated by the @@ -587,13 +591,13 @@ def start_active_span( def start_span( self, - operation_name=None, - child_of=None, - references=None, - tags=None, - start_time=None, - ignore_active_span=False, - ): + operation_name: str = None, + child_of: Union[SpanShim, SpanContextShim] = None, + references: list = None, + tags: Attributes = None, + start_time: float = None, + ignore_active_span: bool = False, + ) -> SpanShim: """Implements the ``start_span()`` method from the base class. Starts a span. In terms of functionality, this method behaves exactly @@ -601,25 +605,19 @@ def start_span( :meth:`opentracing.Tracer.start_span` for more details. Args: - operation_name(:obj:`str`): Name of the operation represented by - the new span from the perspective of the current service. - child_of(:class:`SpanShim` or :class:`SpanContextShim`, optional): - A :class:`SpanShim` or :class:`SpanContextShim` representing - the parent in a "child of" reference. If specified, the - *references* parameter must be omitted. Defaults to `None`. - references(:obj:`list`, optional): A list of - :class:`opentracing.Reference` objects that identify one or - more parents of type :class:`SpanContextShim`. Defaults to - `None`. - tags(:obj:`dict`, optional): A dictionary of tags. The keys must be - of type :obj:`str`. The values may be one of :obj:`str`, - :obj:`bool`, :obj:`int`, :obj:`float`. Defaults to `None`. - start_time(:obj:`float`, optional): An explicit start time - expressed as the number of seconds since the epoch as returned - by :func:`time.time()`. Defaults to `None`. - ignore_active_span(:obj:`bool`, optional): Ignore the - currently-active span in the OpenTelemetry tracer and make the - created span the root span of a new trace. Defaults to `False`. + operation_name: Name of the operation represented by the new span + from the perspective of the current service. + child_of: A :class:`SpanShim` or :class:`SpanContextShim` + representing the parent in a "child of" reference. If + specified, the *references* parameter must be omitted. + references: A list of :class:`opentracing.Reference` objects that + identify one or more parents of type :class:`SpanContextShim`. + tags: A dictionary of tags. + start_time: An explicit start time expressed as the number of + seconds since the epoch as returned by :func:`time.time()`. + ignore_active_span: Ignore the currently-active span in the + OpenTelemetry tracer and make the created span the root span of + a new trace. Returns: An already-started :class:`SpanShim` instance. @@ -656,10 +654,19 @@ def start_span( context = SpanContextShim(span.get_context()) return SpanShim(self, context, span) - def inject(self, span_context, format, carrier): - """Implements the ``inject`` method from the base class.""" + def inject(self, span_context, format: object, carrier: object): + """Injects ``span_context`` into ``carrier``. + + See base class for more details. + + Args: + span_context: The ``opentracing.SpanContext`` to inject. + format: a Python object instance that represents a given + carrier format. `format` may be of any type, and `format` + equality is defined by Python ``==`` operator. + carrier: the format-specific carrier object to inject into + """ - # TODO: Finish documentation. # pylint: disable=redefined-builtin # This implementation does not perform the injecting by itself but # uses the configured propagators in opentelemetry.propagators. @@ -667,24 +674,37 @@ def inject(self, span_context, format, carrier): # opentelemetry-python. if format not in self._supported_formats: - raise opentracing.UnsupportedFormatException + raise UnsupportedFormatException propagator = propagators.get_global_httptextformat() ctx = set_span_in_context(DefaultSpan(span_context.unwrap())) propagator.inject(type(carrier).__setitem__, carrier, context=ctx) - def extract(self, format, carrier): - """Implements the ``extract`` method from the base class.""" + def extract(self, format: object, carrier: object): + """Returns an ``opentracing.SpanContext`` instance extracted from a + ``carrier``. + + See base class for more details. + + Args: + format: a Python object instance that represents a given + carrier format. ``format`` may be of any type, and ``format`` + equality is defined by python ``==`` operator. + carrier: the format-specific carrier object to extract from + + Returns: + An ``opentracing.SpanContext`` extracted from ``carrier`` or + ``None`` if no such ``SpanContext`` could be found. + """ - # TODO: Finish documentation. # pylint: disable=redefined-builtin # This implementation does not perform the extracing by itself but # uses the configured propagators in opentelemetry.propagators. # TODO: Support Format.BINARY once it is supported in # opentelemetry-python. if format not in self._supported_formats: - raise opentracing.UnsupportedFormatException + raise UnsupportedFormatException def get_as_list(dict_object, key): value = dict_object.get(key) From 47229e94f4a56987e5100e8767df38b15ca0009d Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Sat, 15 Aug 2020 18:06:27 -0700 Subject: [PATCH 0505/1517] chore: 0.13.dev0 version update (#991) --- docs/examples/opentelemetry-example-app/setup.cfg | 8 ++++---- .../src/opentelemetry_example_app/version.py | 2 +- exporter/opentelemetry-exporter-datadog/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-datadog/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/datadog/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-prometheus/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- .../CHANGELOG.md | 4 ++++ .../setup.cfg | 2 +- .../instrumentation/aiohttp_client/version.py | 2 +- .../opentelemetry-instrumentation-aiopg/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/aiopg/version.py | 2 +- .../opentelemetry-instrumentation-asgi/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-asgi/setup.cfg | 4 ++-- .../src/opentelemetry/instrumentation/asgi/version.py | 2 +- .../opentelemetry-instrumentation-asyncpg/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-asyncpg/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/asyncpg/version.py | 2 +- .../opentelemetry-instrumentation-boto/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-boto/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/boto/version.py | 2 +- .../opentelemetry-instrumentation-botocore/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-botocore/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/botocore/version.py | 2 +- .../opentelemetry-instrumentation-celery/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-celery/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/celery/version.py | 2 +- .../opentelemetry-instrumentation-dbapi/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-dbapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/dbapi/version.py | 2 +- .../opentelemetry-instrumentation-django/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-django/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/django/version.py | 2 +- .../CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-elasticsearch/setup.cfg | 6 +++--- .../instrumentation/elasticsearch/version.py | 2 +- .../opentelemetry-instrumentation-fastapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/fastapi/version.py | 2 +- .../opentelemetry-instrumentation-flask/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-flask/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/flask/version.py | 2 +- .../opentelemetry-instrumentation-grpc/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-grpc/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/grpc/version.py | 2 +- .../opentelemetry-instrumentation-jinja2/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-jinja2/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/jinja2/version.py | 2 +- .../opentelemetry-instrumentation-mysql/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-mysql/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/mysql/version.py | 2 +- .../CHANGELOG.md | 4 ++++ .../setup.cfg | 4 ++-- .../instrumentation/opentracing_shim/version.py | 2 +- .../opentelemetry-instrumentation-psycopg2/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-psycopg2/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/psycopg2/version.py | 2 +- .../opentelemetry-instrumentation-pymemcache/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-pymemcache/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/pymemcache/version.py | 2 +- .../opentelemetry-instrumentation-pymongo/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-pymongo/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/pymongo/version.py | 2 +- .../opentelemetry-instrumentation-pymysql/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-pymysql/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/pymysql/version.py | 2 +- .../opentelemetry-instrumentation-pyramid/CHANGELOG.md | 5 +++++ .../opentelemetry-instrumentation-pyramid/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/pyramid/version.py | 2 +- .../opentelemetry-instrumentation-redis/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-redis/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/redis/version.py | 2 +- .../opentelemetry-instrumentation-requests/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-requests/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/requests/version.py | 2 +- .../opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-sqlalchemy/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/sqlalchemy/version.py | 2 +- .../opentelemetry-instrumentation-sqlite3/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-sqlite3/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/sqlite3/version.py | 2 +- .../opentelemetry-instrumentation-starlette/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/starlette/version.py | 2 +- .../CHANGELOG.md | 4 ++++ .../setup.cfg | 4 ++-- .../instrumentation/system_metrics/version.py | 2 +- .../opentelemetry-instrumentation-wsgi/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-wsgi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/wsgi/version.py | 2 +- opentelemetry-api/CHANGELOG.md | 4 ++++ opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 4 ++++ opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- scripts/build.sh | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 111 files changed, 283 insertions(+), 150 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index e77fbae376..b38efb5939 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -43,10 +43,10 @@ zip_safe = False include_package_data = True install_requires = typing; python_version<'3.5' - opentelemetry-api == 0.12.dev0 - opentelemetry-sdk == 0.12.dev0 - opentelemetry-instrumentation-requests == 0.12.dev0 - opentelemetry-instrumentation-flask == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-sdk == 0.13dev0 + opentelemetry-instrumentation-requests == 0.13dev0 + opentelemetry-instrumentation-flask == 0.13dev0 flask requests protobuf~=3.11 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py index 780a92b6a1..9cc445d09e 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/exporter/opentelemetry-exporter-datadog/CHANGELOG.md b/exporter/opentelemetry-exporter-datadog/CHANGELOG.md index d15d5a4b5d..728c081090 100644 --- a/exporter/opentelemetry-exporter-datadog/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-datadog/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-exporter-datadog ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) diff --git a/exporter/opentelemetry-exporter-datadog/setup.cfg b/exporter/opentelemetry-exporter-datadog/setup.cfg index 266abe9e0b..8975f25097 100644 --- a/exporter/opentelemetry-exporter-datadog/setup.cfg +++ b/exporter/opentelemetry-exporter-datadog/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = ddtrace>=0.34.0 - opentelemetry-api == 0.12.dev0 - opentelemetry-sdk == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-sdk == 0.13dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py index 780a92b6a1..9cc445d09e 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md index 48f7afd397..ffb185bbec 100644 --- a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-exporter-jaeger ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index b660e8ec1c..28d97005fe 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.12.dev0 - opentelemetry-sdk == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-sdk == 0.13dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 4a2d033215..db7a75909b 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md index 13924e489b..efa456f0ca 100644 --- a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-exporter-opencensus ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) - Send start_timestamp and convert labels to strings diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 64b4b4a228..6ac4b5b9cd 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.12.dev0 - opentelemetry-sdk == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-sdk == 0.13dev0 protobuf >= 3.8.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 780a92b6a1..9cc445d09e 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 2379a94cb3..24eb8bcf58 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-exporter-otlp ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) - Update default port to 55680 diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 262ac02008..3a6ca8f957 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.12.dev0 - opentelemetry-sdk == 0.12.dev0 - opentelemetry-proto == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-sdk == 0.13dev0 + opentelemetry-proto == 0.13dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 780a92b6a1..9cc445d09e 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md b/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md index 5c978d394d..563756856d 100644 --- a/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-exporter-prometheus ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index 94359e9641..107e4d9891 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.12.dev0 - opentelemetry-sdk == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-sdk == 0.13dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 780a92b6a1..9cc445d09e 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index 1063d35cf5..5667883314 100644 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-exporter-zipkin ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) - Add proper length zero padding to hex strings of traceId, spanId, parentId sent on the wire, for compatibility with jaeger-collector diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index f1246813e3..0b802a9348 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 0.12.dev0 - opentelemetry-sdk == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-sdk == 0.13dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 780a92b6a1..9cc445d09e 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md index d7dce5f65c..cdbc621d31 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-aiohttp-client ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index 318721ba64..557b8d9a08 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -40,7 +40,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api >= 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-instrumentation == 0.13dev0 aiohttp ~= 3.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py index 8d947df443..1a40cfa0f1 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg index 58c9b38496..f7e89a248c 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation-dbapi == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation-dbapi == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 aiopg >= 0.13.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-asgi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-asgi/CHANGELOG.md index 7f2812bd05..686b8cf830 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-asgi/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-asgi ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg index fdd1f813fb..533a636d51 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 asgiref ~= 3.0 [options.extras_require] diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md index 530bc71da7..56f261c9b5 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-asyncpg ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg index 1cc707df1c..2f267b3f49 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 asyncpg >= 0.12.0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md index a1c2e9465f..35bd5d6027 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-boto ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) diff --git a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg index ee47a919cd..0bf6820702 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg @@ -41,15 +41,15 @@ package_dir= packages=find_namespace: install_requires = boto ~= 2.0 - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 - opentelemetry-instrumentation-botocore == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 + opentelemetry-instrumentation-botocore == 0.13dev0 [options.extras_require] test = boto~=2.0 moto~=1.0 - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py +++ b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md index e89b3fddd2..128729e21d 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-botocore ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg index 86f3e8d0a4..bad58662ea 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg @@ -41,13 +41,13 @@ package_dir= packages=find_namespace: install_requires = botocore ~= 1.0 - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 [options.extras_require] test = moto ~= 1.0 - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md index 0de1991b96..e164a89134 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-celery ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) diff --git a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg index 4198f15569..5b027f6fe3 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 celery ~= 4.0 [options.extras_require] test = pytest - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md index 555b227db3..bdb9236acb 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-dbapi ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg index 29044f3ba6..2c78659343 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index ae682630db..3e4e42e55b 100644 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-django ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) diff --git a/instrumentation/opentelemetry-instrumentation-django/setup.cfg b/instrumentation/opentelemetry-instrumentation-django/setup.cfg index b2ccaf22b8..eb53010b62 100644 --- a/instrumentation/opentelemetry-instrumentation-django/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-django/setup.cfg @@ -41,13 +41,13 @@ package_dir= packages=find_namespace: install_requires = django >= 1.10 - opentelemetry-instrumentation-wsgi == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 - opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation-wsgi == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13dev0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md index 5579a36a62..daa52ff3ec 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - Change package name to opentelemetry-instrumentation-elasticsearch ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg index fc5f862a2c..0640edce12 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 wrapt >= 1.0.0, < 2.0.0 elasticsearch >= 2.0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 elasticsearch-dsl >= 2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg index f4c64744f8..514b2a3754 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation-asgi == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation-asgi == 0.13dev0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 fastapi ~= 0.58.1 requests ~= 2.23.0 # needed for testclient diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md index 156cf9db31..989e06730d 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-flask ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) diff --git a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg index 281a11249d..e08d20e215 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-instrumentation-wsgi == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 - opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation-wsgi == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13dev0 [options.extras_require] test = flask~=1.0 - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md index b6b28ecd9f..ced6b9c345 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-grpc ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg index 4d40278237..590cb5e9cf 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-sdk == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-sdk == 0.13dev0 grpcio == 1.30 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 - opentelemetry-sdk == 0.12.dev0 + opentelemetry-test == 0.13dev0 + opentelemetry-sdk == 0.13dev0 protobuf == 3.12.2 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md index a503fe367e..79a4aba1ae 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-jinja2 ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg index 76b9b87920..026dba36e7 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md index 944b34d5ad..8912788b2f 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-mysql ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) diff --git a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg index 2242709dab..aed70b99f1 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation-dbapi == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation-dbapi == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md index 47bbc0ada0..36bfb122f6 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change reference names to opentelemetry-instrumentation-opentracing-shim ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg index 1a822d49ce..2e0e57bb30 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.12.dev0 + opentelemetry-api == 0.13dev0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md index e5f81a558b..3ee8fa7f3e 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-psycopg2 ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg index c20693a96c..f0948b17c6 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation-dbapi == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation-dbapi == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md index a01e0e2c53..5e05b3d45f 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-pymemcache ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg index aff71c8245..dd27b8901a 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 pymemcache ~= 1.3 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md index a92d2b53da..23ea58f0b1 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-pymongo ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg index a480f05d6f..d5302d9511 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 pymongo ~= 3.1 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md index 9fea902d18..7a9cee91bf 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-pymysql ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg index b00276c9d2..ec491fd4cc 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation-dbapi == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation-dbapi == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 PyMySQL ~= 0.9.3 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md index 2e20d5a0bd..d4f6601fcb 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + +- Change package name to opentelemetry-instrumentation-pyramid ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) ## Version 0.11b0 diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg index 7f23f27ca5..2546682691 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg @@ -41,15 +41,15 @@ package_dir= packages=find_namespace: install_requires = pyramid >= 1.7 - opentelemetry-instrumentation == 0.12.dev0 - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation-wsgi == 0.12.dev0 + opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation-wsgi == 0.13dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = werkzeug == 0.16.1 - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md index e7f8d24874..72508d7b1f 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Update default SpanKind to `SpanKind.CLIENT` ([#965](https://github.com/open-telemetry/opentelemetry-python/pull/965)) - Change package name to opentelemetry-instrumentation-redis ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) diff --git a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg index 27793a5bed..9e1562104e 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg @@ -40,15 +40,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 redis >= 2.6 wrapt >= 1.12.1 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 - opentelemetry-sdk == 0.12.dev0 + opentelemetry-test == 0.13dev0 + opentelemetry-sdk == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md index dda179d918..179f0f6877 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-requests ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) diff --git a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg index 77205b5816..ece5013173 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg @@ -40,13 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 requests ~= 2.0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 httpretty ~= 1.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md index f46080a9b9..d64d247d21 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-sqlalchemy ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg index e10a684d65..bed8f6209c 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 wrapt >= 1.11.2 sqlalchemy [options.extras_require] test = - opentelemetry-sdk == 0.12.dev0 + opentelemetry-sdk == 0.13dev0 pytest [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md index 12d001bdf1..08071b1522 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-sqlite3 ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg index 841c8d3b17..a13fb57ea3 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation-dbapi == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation-dbapi == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg index 905f4992e4..c94d76c88c 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation-asgi == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation-asgi == 0.13dev0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 starlette ~= 0.13.0 requests ~= 2.23.0 # needed for testclient diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md index f752d8314d..4bd1389ccc 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-system-metrics ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg index 4b6f4f07f5..4a93873ce1 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 + opentelemetry-api == 0.13dev0 psutil ~= 5.7.0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md index 6f1085b8bb..50b3afedfd 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-wsgi ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg index b48d61dc84..789f81ba8d 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg @@ -40,12 +40,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 [options.extras_require] test = - opentelemetry-test == 0.12.dev0 + opentelemetry-test == 0.13dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py index 780a92b6a1..9cc445d09e 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index a04aa8a48f..2defb0a59a 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - Stop TracerProvider and MeterProvider from being overridden diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 780a92b6a1..9cc445d09e 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 4dee7c3cf8..78973ab127 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.12.dev0 + opentelemetry-api == 0.13dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 780a92b6a1..9cc445d09e 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 780a92b6a1..9cc445d09e 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 923087f421..a5fe59878b 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Changed default Sampler to `ParentOrElse(AlwaysOn)` - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 6ea60117a1..821c7dd785 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.12.dev0 + opentelemetry-api == 0.13dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 780a92b6a1..9cc445d09e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" diff --git a/scripts/build.sh b/scripts/build.sh index 056998ebc1..0dec4a27ba 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ exporter/*/ ext/*/ instrumentation/*/ ; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ exporter/*/ instrumentation/*/ ; do ( echo "building $d" cd "$d" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 1ad7bc603e..70efafb1a0 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.12.dev0" +__version__ = "0.13dev0" From 1576a0d6cfa862dcbf28995bc7f5f83a9e4b0568 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 17 Aug 2020 11:41:20 -0400 Subject: [PATCH 0506/1517] in types.Attributes, change Dict -> Mapping (#989) --- opentelemetry-api/src/opentelemetry/util/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index 6830a0dcd1..4221c7f1b9 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -13,7 +13,7 @@ # limitations under the License. -from typing import Callable, Dict, Optional, Sequence, Union +from typing import Callable, Mapping, Optional, Sequence, Union AttributeValue = Union[ str, @@ -25,5 +25,5 @@ Sequence[int], Sequence[float], ] -Attributes = Optional[Dict[str, AttributeValue]] -AttributesFormatter = Callable[[], Optional[Dict[str, AttributeValue]]] +Attributes = Optional[Mapping[str, AttributeValue]] +AttributesFormatter = Callable[[], Attributes] From 5aa1d811039de774f4a84db4c9c656afe11827dd Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Mon, 17 Aug 2020 18:06:33 +0200 Subject: [PATCH 0507/1517] Remove redundant circleci matrix parameter (#993) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bebfc4dbc1..fd45dce81f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -141,7 +141,7 @@ workflows: - build: matrix: parameters: - version: ["py38", "py37", "py36", "py35", "pypy3"] + version: ["py37", "py36", "py35", "pypy3"] package: ["core", "exporter", "instrumentation"] - build-py34: matrix: From 452be59bebe20c81b64c4dbc391ee77472498360 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 17 Aug 2020 22:06:05 -0600 Subject: [PATCH 0508/1517] exporter/otlp: Add OTLP metric exporter (#835) --- .pylintrc | 1 + docs/getting-started.rst | 4 +- ...or_example.py => otlpcollector_example.py} | 28 ++- .../opentelemetry-exporter-otlp/CHANGELOG.md | 5 +- .../opentelemetry/exporter/otlp/exporter.py | 194 +++++++++++++++++ .../otlp/metrics_exporter/__init__.py | 198 ++++++++++++++++++ .../exporter/otlp/trace_exporter/__init__.py | 172 ++------------- .../tests/test_otlp_metric_exporter.py | 116 ++++++++++ .../tests/test_otlp_trace_exporter.py | 10 +- 9 files changed, 550 insertions(+), 178 deletions(-) rename docs/getting_started/{otcollector_example.py => otlpcollector_example.py} (78%) create mode 100644 exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py create mode 100644 exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py diff --git a/.pylintrc b/.pylintrc index 5f9463df7d..8f29b634f1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -65,6 +65,7 @@ disable=missing-docstring, too-few-public-methods, # Might be good to re-enable this later. too-many-instance-attributes, too-many-arguments, + duplicate-code, ungrouped-imports, # Leave this up to isort wrong-import-order, # Leave this up to isort bad-continuation, # Leave this up to black diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 198bd0f1e3..8c27ddfa4d 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -312,10 +312,10 @@ Install the OpenTelemetry Collector exporter: .. code-block:: sh - pip install opentelemetry-instrumentation-otcollector + pip install opentelemetry-exporter-otlp And execute the following script: -.. literalinclude:: getting_started/otcollector_example.py +.. literalinclude:: getting_started/otlpcollector_example.py :language: python :lines: 15- diff --git a/docs/getting_started/otcollector_example.py b/docs/getting_started/otlpcollector_example.py similarity index 78% rename from docs/getting_started/otcollector_example.py rename to docs/getting_started/otlpcollector_example.py index b1887c3d0c..15254c7cc5 100644 --- a/docs/getting_started/otcollector_example.py +++ b/docs/getting_started/otlpcollector_example.py @@ -16,33 +16,29 @@ import time from opentelemetry import metrics, trace -from opentelemetry.ext.otcollector.metrics_exporter import ( - CollectorMetricsExporter, -) -from opentelemetry.ext.otcollector.trace_exporter import CollectorSpanExporter +from opentelemetry.exporter.otlp.metrics_exporter import OTLPMetricsExporter +from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export.controller import PushController from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor -# create a CollectorSpanExporter -span_exporter = CollectorSpanExporter( - # optional: - # endpoint="myCollectorUrl:55678", - # service_name="test_service", - # host_name="machine/container name", +span_exporter = OTLPSpanExporter( + # optional + # endpoint:="myCollectorURL:55678", + # credentials=ChannelCredentials(credentials), + # metadata=(("metadata", "metadata")), ) tracer_provider = TracerProvider() trace.set_tracer_provider(tracer_provider) span_processor = BatchExportSpanProcessor(span_exporter) tracer_provider.add_span_processor(span_processor) -# create a CollectorMetricsExporter -metric_exporter = CollectorMetricsExporter( - # optional: - # endpoint="myCollectorUrl:55678", - # service_name="test_service", - # host_name="machine/container name", +metric_exporter = OTLPMetricsExporter( + # optional + # endpoint:="myCollectorURL:55678", + # credentials=ChannelCredentials(credentials), + # metadata=(("metadata", "metadata")), ) # Meter is responsible for creating and recording metrics diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 24eb8bcf58..ed6aee3b4c 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,9 +2,10 @@ ## Unreleased -## Version 0.12b0 +- Add metric OTLP exporter + ([#835](https://github.com/open-telemetry/opentelemetry-python/pull/835)) -Released 2020-08-14 +## Version 0.12b0 - Change package name to opentelemetry-exporter-otlp ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py new file mode 100644 index 0000000000..0ce7ef6617 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -0,0 +1,194 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""OTLP Exporter""" + +import logging +from abc import ABC, abstractmethod +from collections.abc import Mapping, Sequence +from time import sleep + +from backoff import expo +from google.rpc.error_details_pb2 import RetryInfo +from grpc import ( + ChannelCredentials, + RpcError, + StatusCode, + insecure_channel, + secure_channel, +) + +from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue +from opentelemetry.proto.resource.v1.resource_pb2 import Resource + +logger = logging.getLogger(__name__) + + +def _translate_key_values(key, value): + + if isinstance(value, bool): + any_value = AnyValue(bool_value=value) + + elif isinstance(value, str): + any_value = AnyValue(string_value=value) + + elif isinstance(value, int): + any_value = AnyValue(int_value=value) + + elif isinstance(value, float): + any_value = AnyValue(double_value=value) + + elif isinstance(value, Sequence): + any_value = AnyValue(array_value=value) + + elif isinstance(value, Mapping): + any_value = AnyValue(kvlist_value=value) + + else: + raise Exception( + "Invalid type {} of value {}".format(type(value), value) + ) + + return KeyValue(key=key, value=any_value) + + +def _get_resource_data( + sdk_resource_instrumentation_library_data, resource_class, name +): + + resource_data = [] + + for ( + sdk_resource, + instrumentation_library_data, + ) in sdk_resource_instrumentation_library_data.items(): + + collector_resource = Resource() + + for key, value in sdk_resource.labels.items(): + + try: + # pylint: disable=no-member + collector_resource.attributes.append( + _translate_key_values(key, value) + ) + except Exception as error: # pylint: disable=broad-except + logger.exception(error) + + resource_data.append( + resource_class( + **{ + "resource": collector_resource, + "instrumentation_library_{}".format(name): [ + instrumentation_library_data + ], + } + ) + ) + + return resource_data + + +# pylint: disable=no-member +class OTLPExporterMixin(ABC): + """OTLP span/metric exporter + + Args: + endpoint: OpenTelemetry Collector receiver endpoint + credentials: ChannelCredentials object for server authentication + metadata: Metadata to send when exporting + """ + + def __init__( + self, + endpoint: str = "localhost:55680", + credentials: ChannelCredentials = None, + metadata: tuple = None, + ): + super().__init__() + + self._metadata = metadata + self._collector_span_kwargs = None + + if credentials is None: + self._client = self._stub(insecure_channel(endpoint)) + else: + self._client = self._stub(secure_channel(endpoint, credentials)) + + @abstractmethod + def _translate_data(self, data): + pass + + def _export(self, data): + # expo returns a generator that yields delay values which grow + # exponentially. Once delay is greater than max_value, the yielded + # value will remain constant. + # max_value is set to 900 (900 seconds is 15 minutes) to use the same + # value as used in the Go implementation. + + max_value = 900 + + for delay in expo(max_value=max_value): + + if delay == max_value: + return self._result.FAILURE + + try: + self._client.Export( + request=self._translate_data(data), + metadata=self._metadata, + ) + + return self._result.SUCCESS + + except RpcError as error: + + if error.code() in [ + StatusCode.CANCELLED, + StatusCode.DEADLINE_EXCEEDED, + StatusCode.PERMISSION_DENIED, + StatusCode.UNAUTHENTICATED, + StatusCode.RESOURCE_EXHAUSTED, + StatusCode.ABORTED, + StatusCode.OUT_OF_RANGE, + StatusCode.UNAVAILABLE, + StatusCode.DATA_LOSS, + ]: + + retry_info_bin = dict(error.trailing_metadata()).get( + "google.rpc.retryinfo-bin" + ) + if retry_info_bin is not None: + retry_info = RetryInfo() + retry_info.ParseFromString(retry_info_bin) + delay = ( + retry_info.retry_delay.seconds + + retry_info.retry_delay.nanos / 1.0e9 + ) + + logger.debug( + "Waiting %ss before retrying export of span", delay + ) + sleep(delay) + continue + + if error.code() == StatusCode.OK: + return self._result.SUCCESS + + return self.result.FAILURE + + return self._result.FAILURE + + def shutdown(self): + pass diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py new file mode 100644 index 0000000000..944428e37d --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -0,0 +1,198 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""OTLP Metrics Exporter""" + +import logging +from typing import Sequence + +# pylint: disable=duplicate-code +from opentelemetry.exporter.otlp.exporter import ( + OTLPExporterMixin, + _get_resource_data, +) +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( + MetricsServiceStub, +) +from opentelemetry.proto.common.v1.common_pb2 import StringKeyValue +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + DoubleDataPoint, + InstrumentationLibraryMetrics, + Int64DataPoint, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + Metric as CollectorMetric, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + MetricDescriptor, + ResourceMetrics, +) +from opentelemetry.sdk.metrics import Counter +from opentelemetry.sdk.metrics import Metric as SDKMetric +from opentelemetry.sdk.metrics import ( + SumObserver, + UpDownCounter, + UpDownSumObserver, + ValueObserver, + ValueRecorder, +) +from opentelemetry.sdk.metrics.export import ( + MetricsExporter, + MetricsExportResult, +) + +logger = logging.getLogger(__name__) + + +def _get_data_points(sdk_metric, data_point_class): + + data_points = [] + + for ( + label, + bound_counter, + ) in sdk_metric.instrument.bound_instruments.items(): + + string_key_values = [] + + for label_key, label_value in label: + string_key_values.append( + StringKeyValue(key=label_key, value=label_value) + ) + + for view_data in bound_counter.view_datas: + + if view_data.labels == label: + + data_points.append( + data_point_class( + labels=string_key_values, + value=view_data.aggregator.current, + ) + ) + break + + return data_points + + +def _get_temporality(instrument): + # pylint: disable=no-member + if isinstance(instrument, (Counter, UpDownCounter)): + temporality = MetricDescriptor.Temporality.DELTA + elif isinstance(instrument, (ValueRecorder, ValueObserver)): + temporality = MetricDescriptor.Temporality.INSTANTANEOUS + elif isinstance(instrument, (SumObserver, UpDownSumObserver)): + temporality = MetricDescriptor.Temporality.CUMULATIVE + else: + raise Exception( + "No temporality defined for instrument type {}".format( + type(instrument) + ) + ) + + return temporality + + +def _get_type(value_type): + # pylint: disable=no-member + if value_type is int: + type_ = MetricDescriptor.Type.INT64 + + elif value_type is float: + type_ = MetricDescriptor.Type.DOUBLE + + # FIXME What are the types that correspond with + # MetricDescriptor.Type.HISTOGRAM and + # MetricDescriptor.Type.SUMMARY? + else: + raise Exception( + "No type defined for valie type {}".format(type(value_type)) + ) + + return type_ + + +class OTLPMetricsExporter(MetricsExporter, OTLPExporterMixin): + """OTLP metrics exporter + + Args: + endpoint: OpenTelemetry Collector receiver endpoint + credentials: Credentials object for server authentication + metadata: Metadata to send when exporting + """ + + _stub = MetricsServiceStub + _result = MetricsExportResult + + def _translate_data(self, data): + # pylint: disable=too-many-locals,no-member + # pylint: disable=attribute-defined-outside-init + + sdk_resource_instrumentation_library_metrics = {} + + for sdk_metric in data: + + if sdk_metric.instrument.meter.resource not in ( + sdk_resource_instrumentation_library_metrics.keys() + ): + sdk_resource_instrumentation_library_metrics[ + sdk_metric.instrument.meter.resource + ] = InstrumentationLibraryMetrics() + + self._metric_descriptor_kwargs = {} + + metric_descriptor = MetricDescriptor( + name=sdk_metric.instrument.name, + description=sdk_metric.instrument.description, + unit=sdk_metric.instrument.unit, + type=_get_type(sdk_metric.instrument.value_type), + temporality=_get_temporality(sdk_metric.instrument), + ) + + if metric_descriptor.type == MetricDescriptor.Type.INT64: + + collector_metric = CollectorMetric( + metric_descriptor=metric_descriptor, + int64_data_points=_get_data_points( + sdk_metric, Int64DataPoint + ), + ) + + elif metric_descriptor.type == MetricDescriptor.Type.DOUBLE: + + collector_metric = CollectorMetric( + metric_descriptor=metric_descriptor, + double_data_points=_get_data_points( + sdk_metric, DoubleDataPoint + ), + ) + + sdk_resource_instrumentation_library_metrics[ + sdk_metric.instrument.meter.resource + ].metrics.append(collector_metric) + + return ExportMetricsServiceRequest( + resource_metrics=_get_resource_data( + sdk_resource_instrumentation_library_metrics, + ResourceMetrics, + "metrics", + ) + ) + + def export(self, metrics: Sequence[SDKMetric]) -> MetricsExportResult: + # pylint: disable=arguments-differ + return self._export(metrics) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index be1419fa39..5a9a74a304 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -1,5 +1,4 @@ # Copyright The OpenTelemetry Authors -# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -15,28 +14,19 @@ """OTLP Span Exporter""" import logging -from collections.abc import Mapping, Sequence -from time import sleep -from typing import Sequence as TypingSequence - -from backoff import expo -from google.rpc.error_details_pb2 import RetryInfo -from grpc import ( - ChannelCredentials, - RpcError, - StatusCode, - insecure_channel, - secure_channel, -) +from typing import Sequence +from opentelemetry.exporter.otlp.exporter import ( + OTLPExporterMixin, + _get_resource_data, + _translate_key_values, +) from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest, ) from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( TraceServiceStub, ) -from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue -from opentelemetry.proto.resource.v1.resource_pb2 import Resource from opentelemetry.proto.trace.v1.trace_pb2 import ( InstrumentationLibrarySpans, ResourceSpans, @@ -49,36 +39,8 @@ logger = logging.getLogger(__name__) -def _translate_key_values(key, value): - - if isinstance(value, bool): - any_value = AnyValue(bool_value=value) - - elif isinstance(value, str): - any_value = AnyValue(string_value=value) - - elif isinstance(value, int): - any_value = AnyValue(int_value=value) - - elif isinstance(value, float): - any_value = AnyValue(double_value=value) - - elif isinstance(value, Sequence): - any_value = AnyValue(array_value=value) - - elif isinstance(value, Mapping): - any_value = AnyValue(kvlist_value=value) - - else: - raise Exception( - "Invalid type {} of value {}".format(type(value), value) - ) - - return KeyValue(key=key, value=any_value) - - # pylint: disable=no-member -class OTLPSpanExporter(SpanExporter): +class OTLPSpanExporter(SpanExporter, OTLPExporterMixin): """OTLP span exporter Args: @@ -87,23 +49,8 @@ class OTLPSpanExporter(SpanExporter): metadata: Metadata to send when exporting """ - def __init__( - self, - endpoint="localhost:55680", - credentials: ChannelCredentials = None, - metadata=None, - ): - super().__init__() - - self._metadata = metadata - self._collector_span_kwargs = None - - if credentials is None: - self._client = TraceServiceStub(insecure_channel(endpoint)) - else: - self._client = TraceServiceStub( - secure_channel(endpoint, credentials) - ) + _result = SpanExportResult + _stub = TraceServiceStub def _translate_name(self, sdk_span): self._collector_span_kwargs["name"] = sdk_span.name @@ -212,13 +159,11 @@ def _translate_status(self, sdk_span): message=sdk_span.status.description, ) - def _translate_spans( - self, sdk_spans: TypingSequence[SDKSpan], - ) -> ExportTraceServiceRequest: + def _translate_data(self, data) -> ExportTraceServiceRequest: sdk_resource_instrumentation_library_spans = {} - for sdk_span in sdk_spans: + for sdk_span in data: if sdk_span.resource not in ( sdk_resource_instrumentation_library_spans.keys() @@ -249,92 +194,13 @@ def _translate_spans( sdk_span.resource ].spans.append(CollectorSpan(**self._collector_span_kwargs)) - resource_spans = [] - - for ( - sdk_resource, - instrumentation_library_spans, - ) in sdk_resource_instrumentation_library_spans.items(): - - collector_resource = Resource() - - for key, value in sdk_resource.labels.items(): - - try: - collector_resource.attributes.append( - _translate_key_values(key, value) - ) - except Exception as error: # pylint: disable=broad-except - logger.exception(error) - - resource_spans.append( - ResourceSpans( - resource=collector_resource, - instrumentation_library_spans=[ - instrumentation_library_spans - ], - ) + return ExportTraceServiceRequest( + resource_spans=_get_resource_data( + sdk_resource_instrumentation_library_spans, + ResourceSpans, + "spans", ) + ) - return ExportTraceServiceRequest(resource_spans=resource_spans) - - def export(self, spans: TypingSequence[SDKSpan]) -> SpanExportResult: - # expo returns a generator that yields delay values which grow - # exponentially. Once delay is greater than max_value, the yielded - # value will remain constant. - # max_value is set to 900 (900 seconds is 15 minutes) to use the same - # value as used in the Go implementation. - - max_value = 900 - - for delay in expo(max_value=max_value): - - if delay == max_value: - return SpanExportResult.FAILURE - - try: - self._client.Export( - request=self._translate_spans(spans), - metadata=self._metadata, - ) - - return SpanExportResult.SUCCESS - - except RpcError as error: - - if error.code() in [ - StatusCode.CANCELLED, - StatusCode.DEADLINE_EXCEEDED, - StatusCode.PERMISSION_DENIED, - StatusCode.UNAUTHENTICATED, - StatusCode.RESOURCE_EXHAUSTED, - StatusCode.ABORTED, - StatusCode.OUT_OF_RANGE, - StatusCode.UNAVAILABLE, - StatusCode.DATA_LOSS, - ]: - - retry_info_bin = dict(error.trailing_metadata()).get( - "google.rpc.retryinfo-bin" - ) - if retry_info_bin is not None: - retry_info = RetryInfo() - retry_info.ParseFromString(retry_info_bin) - delay = ( - retry_info.retry_delay.seconds - + retry_info.retry_delay.nanos / 1.0e9 - ) - - logger.debug("Waiting %ss before retrying export of span") - sleep(delay) - continue - - if error.code() == StatusCode.OK: - return SpanExportResult.SUCCESS - - return SpanExportResult.FAILURE - - return SpanExportResult.FAILURE - - def shutdown(self): - pass + def export(self, spans: Sequence[SDKSpan]) -> SpanExportResult: + return self._export(spans) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py new file mode 100644 index 0000000000..20fecd44a2 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -0,0 +1,116 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import OrderedDict +from unittest import TestCase + +from opentelemetry.exporter.otlp.metrics_exporter import OTLPMetricsExporter +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue, + KeyValue, + StringKeyValue, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + InstrumentationLibraryMetrics, + Int64DataPoint, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + Metric as CollectorMetric, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + MetricDescriptor, + ResourceMetrics, +) +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as CollectorResource, +) +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export import MetricRecord +from opentelemetry.sdk.metrics.export.aggregate import SumAggregator +from opentelemetry.sdk.resources import Resource as SDKResource + + +class TestOTLPMetricExporter(TestCase): + def setUp(self): + self.exporter = OTLPMetricsExporter() + + self.counter_metric_record = MetricRecord( + Counter( + "a", + "b", + "c", + int, + MeterProvider( + resource=SDKResource(OrderedDict([("a", 1), ("b", False)])) + ).get_meter(__name__), + ("d",), + ), + OrderedDict([("e", "f")]), + SumAggregator(), + ) + + def test_translate_metrics(self): + # pylint: disable=no-member + + self.counter_metric_record.instrument.add(1, OrderedDict([("a", "b")])) + + expected = ExportMetricsServiceRequest( + resource_metrics=[ + ResourceMetrics( + resource=CollectorResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_metrics=[ + InstrumentationLibraryMetrics( + metrics=[ + CollectorMetric( + metric_descriptor=MetricDescriptor( + name="a", + description="b", + unit="c", + type=MetricDescriptor.Type.INT64, + temporality=( + MetricDescriptor.Temporality.DELTA + ), + ), + int64_data_points=[ + Int64DataPoint( + labels=[ + StringKeyValue( + key="a", value="b" + ) + ], + value=1, + ) + ], + ) + ] + ) + ], + ) + ] + ) + + # pylint: disable=protected-access + actual = self.exporter._translate_data([self.counter_metric_record]) + + self.assertEqual(expected, actual) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index 9058937f87..b0ec8e4517 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -142,8 +142,8 @@ def setUp(self): def tearDown(self): self.server.stop(None) - @patch("opentelemetry.exporter.otlp.trace_exporter.expo") - @patch("opentelemetry.exporter.otlp.trace_exporter.sleep") + @patch("opentelemetry.exporter.otlp.exporter.expo") + @patch("opentelemetry.exporter.otlp.exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): mock_expo.configure_mock(**{"return_value": [1]}) @@ -156,8 +156,8 @@ def test_unavailable(self, mock_sleep, mock_expo): ) mock_sleep.assert_called_with(1) - @patch("opentelemetry.exporter.otlp.trace_exporter.expo") - @patch("opentelemetry.exporter.otlp.trace_exporter.sleep") + @patch("opentelemetry.exporter.otlp.exporter.expo") + @patch("opentelemetry.exporter.otlp.exporter.sleep") def test_unavailable_delay(self, mock_sleep, mock_expo): mock_expo.configure_mock(**{"return_value": [1]}) @@ -274,4 +274,4 @@ def test_translate_spans(self): ) # pylint: disable=protected-access - self.assertEqual(expected, self.exporter._translate_spans([self.span])) + self.assertEqual(expected, self.exporter._translate_data([self.span])) From c9709d216d3dcabf6b18d2344f5ebf6b4bc0213b Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 17 Aug 2020 23:50:09 -0500 Subject: [PATCH 0509/1517] Span name updated to follow semantic conventions to reduce cardinality (#972) --- docs/getting_started/tests/test_flask.py | 2 +- .../CHANGELOG.md | 3 +++ .../instrumentation/aiohttp_client/__init__.py | 2 +- .../tests/test_aiohttp_client_integration.py | 10 +++++----- .../CHANGELOG.md | 2 ++ .../opentelemetry/instrumentation/requests/__init__.py | 7 ++----- .../tests/test_requests_integration.py | 4 ++-- .../src/opentelemetry/sdk/trace/__init__.py | 10 +++++++++- 8 files changed, 25 insertions(+), 15 deletions(-) diff --git a/docs/getting_started/tests/test_flask.py b/docs/getting_started/tests/test_flask.py index 4c0430d0a7..321098ce97 100644 --- a/docs/getting_started/tests/test_flask.py +++ b/docs/getting_started/tests/test_flask.py @@ -38,6 +38,6 @@ def test_flask(self): server.terminate() output = str(server.stdout.read()) - self.assertIn('"name": ""', output) + self.assertIn('"name": "HTTP get"', output) self.assertIn('"name": "example-request"', output) self.assertIn('"name": "hello"', output) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md index cdbc621d31..78b989563f 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Updating span name to match semantic conventions + ([#972](https://github.com/open-telemetry/opentelemetry-python/pull/972)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py index 2d9b8bd7a5..397d5dc80e 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py @@ -126,7 +126,7 @@ async def on_request_start( ): http_method = params.method.upper() if trace_config_ctx.span_name is None: - request_span_name = http_method + request_span_name = "HTTP {}".format(http_method) elif callable(trace_config_ctx.span_name): request_span_name = str(trace_config_ctx.span_name(params)) else: diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py index f44e3df2da..4a48c38ff7 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -118,7 +118,7 @@ def test_status_codes(self): self.assert_spans( [ ( - "GET", + "HTTP GET", (span_status, None), { "component": "http", @@ -192,7 +192,7 @@ def strip_query_params(url: yarl.URL) -> str: self.assert_spans( [ ( - "GET", + "HTTP GET", (StatusCanonicalCode.OK, None), { "component": "http", @@ -232,7 +232,7 @@ async def do_request(url): self.assert_spans( [ ( - "GET", + "HTTP GET", (expected_status, None), { "component": "http", @@ -260,7 +260,7 @@ async def request_handler(request): self.assert_spans( [ ( - "GET", + "HTTP GET", (StatusCanonicalCode.DEADLINE_EXCEEDED, None), { "component": "http", @@ -290,7 +290,7 @@ async def request_handler(request): self.assert_spans( [ ( - "GET", + "HTTP GET", (StatusCanonicalCode.DEADLINE_EXCEEDED, None), { "component": "http", diff --git a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md index 179f0f6877..3f18f6101b 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md @@ -8,6 +8,8 @@ Released 2020-08-14 - Change package name to opentelemetry-instrumentation-requests ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) +- Span name reported updated to follow semantic conventions to reduce + cardinality ([#972](https://github.com/open-telemetry/opentelemetry-python/pull/972)) ## 0.7b1 diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index a12f05999e..e2c54b7f1b 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -80,11 +80,7 @@ def instrumented_request(self, method, url, *args, **kwargs): # See # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#http-client - try: - parsed_url = urlparse(url) - span_name = parsed_url.path - except ValueError as exc: # Invalid URL - span_name = "".format(exc) + span_name = "HTTP {}".format(method) exception = None @@ -111,6 +107,7 @@ def instrumented_request(self, method, url, *args, **kwargs): span.set_status( Status(_exception_to_canonical_code(exception)) ) + span.record_exception(exception) if result is not None: span.set_attribute("http.status_code", result.status_code) diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py index afec0a88d0..da09118e5b 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py @@ -51,7 +51,7 @@ def test_basic(self): span = span_list[0] self.assertIs(span.kind, trace.SpanKind.CLIENT) - self.assertEqual(span.name, "/status/200") + self.assertEqual(span.name, "HTTP get") self.assertEqual( span.attributes, @@ -102,7 +102,7 @@ def test_invalid_url(self): self.assertEqual(len(span_list), 1) span = span_list[0] - self.assertTrue(span.name.startswith(" None: """Records an exception as a span event.""" + try: + stacktrace = traceback.format_exc() + except Exception: # pylint: disable=broad-except + # workaround for python 3.4, format_exc can raise + # an AttributeError if the __context__ on + # an exception is None + stacktrace = "Exception occurred on stacktrace formatting" + self.add_event( name="exception", attributes={ "exception.type": exception.__class__.__name__, "exception.message": str(exception), - "exception.stacktrace": traceback.format_exc(), + "exception.stacktrace": stacktrace, }, ) From 3cae0775ba12a2f7b4214b8b8c062c5e81002a19 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 18 Aug 2020 10:14:32 -0700 Subject: [PATCH 0510/1517] bug (#1011) --- instrumentation/opentelemetry-instrumentation-boto/setup.cfg | 2 +- .../opentelemetry-instrumentation-botocore/setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg index 0bf6820702..994d5fe784 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg @@ -56,4 +56,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - django = opentelemetry.instrumentation.boto:BotoInstrumentor + boto = opentelemetry.instrumentation.boto:BotoInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg index bad58662ea..c1198d356c 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg @@ -54,4 +54,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - django = opentelemetry.instrumentation.botocore:BotoCoreInstrumentor + botocore = opentelemetry.instrumentation.botocore:BotocoreInstrumentor From eaf5bba24fcfd8aa5fdc4097d46cd4048d12438e Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Wed, 19 Aug 2020 20:38:33 +0200 Subject: [PATCH 0511/1517] Revert "pin flake8 at 3.7.9 (#679)" (#1017) This reverts commit 021723a5c1cdde8cd15b542179e1fd83bd32819b. Underlying issue was fixed by commit 20cf4cb087371. --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index dd4d2e37c9..6537a9a444 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,5 @@ pylint==2.4.4 -flake8==3.7.9 +flake8~=3.7 isort~=4.3 black>=19.3b0,==19.* mypy==0.770 From 0d5f15ae7fc6493a0a87c704358765eda6622d4d Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 19 Aug 2020 13:55:15 -0500 Subject: [PATCH 0512/1517] Update README.md (#1021) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc6b7f10b6..28938a3cba 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ See the [OpenTelemetry registry](https://opentelemetry.io/registry/?s=python) fo See [CONTRIBUTING.md](CONTRIBUTING.md) -We meet weekly on Thursday, and the time of the meeting alternates between 9AM PT and 4PM PT. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates. +We meet weekly on Thursday at 9AM PT. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates. Meetings take place via [Zoom video conference](https://zoom.us/j/6729396170). From ee094c24da3d3d2dee508d8f57517f623b014b50 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 19 Aug 2020 17:24:51 -0700 Subject: [PATCH 0513/1517] Filter attributes on lazy event add (#1014) --- .../src/opentelemetry/sdk/trace/__init__.py | 34 ++++++++++--------- opentelemetry-sdk/tests/trace/test_trace.py | 14 ++++++++ 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 65a60025c8..7a53fbdae3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -305,7 +305,11 @@ def __init__( @property def attributes(self) -> types.Attributes: - return self._event_formatter() + attributes = self._event_formatter() + _filter_attribute_values(attributes) + if not attributes: + attributes = Span._new_attributes() + return attributes def _is_valid_attribute_value(value: types.AttributeValue) -> bool: @@ -350,6 +354,16 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool: return True +def _filter_attribute_values(attributes: types.Attributes): + if attributes: + for attr_key, attr_value in list(attributes.items()): + if _is_valid_attribute_value(attr_value): + if isinstance(attr_value, MutableSequence): + attributes[attr_key] = tuple(attr_value) + else: + attributes.pop(attr_key) + + class Span(trace_api.Span): """See `opentelemetry.trace.Span`. @@ -401,7 +415,7 @@ def __init__( self.status = None self._lock = threading.Lock() - self._filter_attribute_values(attributes) + _filter_attribute_values(attributes) if not attributes: self.attributes = self._new_attributes() else: @@ -412,7 +426,7 @@ def __init__( self.events = self._new_events() if events: for event in events: - self._filter_attribute_values(event.attributes) + _filter_attribute_values(event.attributes) self.events.append(event) if links is None: @@ -553,18 +567,6 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: with self._lock: self.attributes[key] = value - @staticmethod - def _filter_attribute_values(attributes: types.Attributes): - if attributes: - for attr_key, attr_value in list(attributes.items()): - if _is_valid_attribute_value(attr_value): - if isinstance(attr_value, MutableSequence): - attributes[attr_key] = tuple(attr_value) - else: - attributes[attr_key] = attr_value - else: - attributes.pop(attr_key) - def _add_event(self, event: EventBase) -> None: with self._lock: if not self.is_recording_events(): @@ -582,7 +584,7 @@ def add_event( attributes: types.Attributes = None, timestamp: Optional[int] = None, ) -> None: - self._filter_attribute_values(attributes) + _filter_attribute_values(attributes) if not attributes: attributes = self._new_attributes() self._add_event( diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 56bb9cfa57..b494ab8ee2 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -610,6 +610,7 @@ def event_formatter(): def test_invalid_event_attributes(self): self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) + now = time_ns() with self.tracer.start_as_current_span("root") as root: root.add_event("event0", {"attr1": True, "attr2": ["hi", False]}) @@ -623,6 +624,19 @@ def test_invalid_event_attributes(self): self.assertEqual(root.events[2].attributes, {}) self.assertEqual(root.events[3].attributes, {"attr2": (1, 2)}) + def event_formatter(): + properties = {} + properties["attr1"] = dict() + properties["attr2"] = "hello" + return properties + + root.add_lazy_event("event4", event_formatter, now) + + self.assertEqual(len(root.events), 5) + self.assertEqual(root.events[4].name, "event4") + self.assertEqual(root.events[4].attributes, {"attr2": "hello"}) + self.assertEqual(root.events[4].timestamp, now) + def test_links(self): other_context1 = trace_api.SpanContext( trace_id=trace.generate_trace_id(), From 163564a76859f8317a36cfad3da75403610efd9d Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Thu, 20 Aug 2020 18:25:14 +0200 Subject: [PATCH 0514/1517] Increase docker-tests retry count (#1026) --- tests/opentelemetry-docker-tests/tests/check_availability.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/opentelemetry-docker-tests/tests/check_availability.py b/tests/opentelemetry-docker-tests/tests/check_availability.py index 91b8e5539d..3082572193 100644 --- a/tests/opentelemetry-docker-tests/tests/check_availability.py +++ b/tests/opentelemetry-docker-tests/tests/check_availability.py @@ -36,7 +36,7 @@ POSTGRES_USER = os.getenv("POSTGRESQL_HOST", "testuser") REDIS_HOST = os.getenv("REDIS_HOST", "localhost") REDIS_PORT = int(os.getenv("REDIS_PORT ", "6379")) -RETRY_COUNT = 5 +RETRY_COUNT = 8 RETRY_INTERVAL = 5 # Seconds logger = logging.getLogger(__name__) From 281a0e630d368f186c99e2a1f5b6bb9933d70cc9 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 20 Aug 2020 14:02:55 -0700 Subject: [PATCH 0515/1517] Move samplers to SDK package (#1023) --- docs/api/trace.rst | 1 - docs/api/trace.sampling.rst | 7 ------- docs/sdk/trace.rst | 1 + docs/sdk/trace.sampling.rst | 7 +++++++ .../src/opentelemetry/exporter/datadog/exporter.py | 3 ++- .../tests/test_datadog_exporter.py | 3 ++- opentelemetry-api/CHANGELOG.md | 3 +++ opentelemetry-sdk/CHANGELOG.md | 4 ++++ opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 5 +++-- .../src/opentelemetry/sdk}/trace/sampling.py | 2 +- .../tests/trace/test_sampling.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- 12 files changed, 25 insertions(+), 15 deletions(-) delete mode 100644 docs/api/trace.sampling.rst create mode 100644 docs/sdk/trace.sampling.rst rename {opentelemetry-api/src/opentelemetry => opentelemetry-sdk/src/opentelemetry/sdk}/trace/sampling.py (98%) rename {opentelemetry-api => opentelemetry-sdk}/tests/trace/test_sampling.py (99%) diff --git a/docs/api/trace.rst b/docs/api/trace.rst index 411e31023e..65d9b4d8c8 100644 --- a/docs/api/trace.rst +++ b/docs/api/trace.rst @@ -6,7 +6,6 @@ Submodules .. toctree:: - trace.sampling trace.status trace.span diff --git a/docs/api/trace.sampling.rst b/docs/api/trace.sampling.rst deleted file mode 100644 index 6280fd1d11..0000000000 --- a/docs/api/trace.sampling.rst +++ /dev/null @@ -1,7 +0,0 @@ -Sampling Traces -=============== - -.. automodule:: opentelemetry.trace.sampling - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/sdk/trace.rst b/docs/sdk/trace.rst index ce06fb4abb..0b53444e3b 100644 --- a/docs/sdk/trace.rst +++ b/docs/sdk/trace.rst @@ -7,6 +7,7 @@ Submodules .. toctree:: trace.export + trace.sampling util.instrumentation .. automodule:: opentelemetry.sdk.trace diff --git a/docs/sdk/trace.sampling.rst b/docs/sdk/trace.sampling.rst new file mode 100644 index 0000000000..f9c2fffa25 --- /dev/null +++ b/docs/sdk/trace.sampling.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.trace.sampling +========================================== + +.. automodule:: opentelemetry.sdk.trace.sampling + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py index 49dab7c686..c23288442d 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py @@ -21,6 +21,7 @@ from ddtrace.span import Span as DatadogSpan import opentelemetry.trace as trace_api +from opentelemetry.sdk.trace import sampling from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace.status import StatusCanonicalCode @@ -246,7 +247,7 @@ def _get_sampling_rate(span): return ( span.sampler.rate if ctx.trace_flags.sampled - and isinstance(span.sampler, trace_api.sampling.ProbabilitySampler) + and isinstance(span.sampler, sampling.ProbabilitySampler) else None ) diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py index 45ce9417e1..47a0280c0a 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py @@ -23,6 +23,7 @@ from opentelemetry import trace as trace_api from opentelemetry.exporter import datadog from opentelemetry.sdk import trace +from opentelemetry.sdk.trace import sampling from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -497,7 +498,7 @@ def test_sampling_rate(self): is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) - sampler = trace_api.sampling.ProbabilitySampler(0.5) + sampler = sampling.ProbabilitySampler(0.5) span = trace.Span( name="sampled", context=context, parent=None, sampler=sampler diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 2defb0a59a..d5a69d4db4 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Moved samplers from API to SDK + ([#1023](https://github.com/open-telemetry/opentelemetry-python/pull/1023)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index a5fe59878b..33d9d6e2e6 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,11 +2,15 @@ ## Unreleased +- Moved samplers from API to SDK + ([#1023](https://github.com/open-telemetry/opentelemetry-python/pull/1023)) + ## Version 0.12b0 Released 2020-08-14 - Changed default Sampler to `ParentOrElse(AlwaysOn)` + ([#960](https://github.com/open-telemetry/opentelemetry-python/pull/960)) - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - Implement Views in metrics SDK diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 7a53fbdae3..034acd0f44 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -40,9 +40,10 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import util from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import sampling from opentelemetry.sdk.util import BoundedDict, BoundedList from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -from opentelemetry.trace import SpanContext, sampling +from opentelemetry.trace import SpanContext from opentelemetry.trace.propagation import SPAN_KEY from opentelemetry.trace.status import Status, StatusCanonicalCode from opentelemetry.util import time_ns, types @@ -869,7 +870,7 @@ def use_span( class TracerProvider(trace_api.TracerProvider): def __init__( self, - sampler: sampling.Sampler = trace_api.sampling.DEFAULT_ON, + sampler: sampling.Sampler = sampling.DEFAULT_ON, resource: Resource = Resource.create_empty(), shutdown_on_exit: bool = True, active_span_processor: Union[ diff --git a/opentelemetry-api/src/opentelemetry/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py similarity index 98% rename from opentelemetry-api/src/opentelemetry/trace/sampling.py rename to opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 868678b05c..4429e33a48 100644 --- a/opentelemetry-api/src/opentelemetry/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -36,12 +36,12 @@ .. code:: python from opentelemetry import trace - from opentelemetry.trace.sampling import ProbabilitySampler from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleExportSpanProcessor, ) + from opentelemetry.sdk.trace.sampling import ProbabilitySampler # sample 1 in every 1000 traces sampler = ProbabilitySampler(1/1000) diff --git a/opentelemetry-api/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py similarity index 99% rename from opentelemetry-api/tests/trace/test_sampling.py rename to opentelemetry-sdk/tests/trace/test_sampling.py index 0be222f3dc..a198bd3296 100644 --- a/opentelemetry-api/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -16,7 +16,7 @@ import unittest from opentelemetry import trace -from opentelemetry.trace import sampling +from opentelemetry.sdk.trace import sampling TO_DEFAULT = trace.TraceFlags(trace.TraceFlags.DEFAULT) TO_SAMPLED = trace.TraceFlags(trace.TraceFlags.SAMPLED) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index b494ab8ee2..9915860cf0 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -20,8 +20,8 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import resources, trace +from opentelemetry.sdk.trace import sampling from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -from opentelemetry.trace import sampling from opentelemetry.trace.status import StatusCanonicalCode from opentelemetry.util import time_ns From dfc7aa52f5fc1d733f3db177676bd4a0949e7ffc Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Fri, 21 Aug 2020 17:20:04 +0200 Subject: [PATCH 0516/1517] Fix grpc tests when running from cmd-line/eachdist script (#1027) * when running the grpc tests with pytest or eachdist from the command line the 2nd test trying to connect to the test server failed with a connection refused message. Seems like the connection from the previous test was still alive due to the channel not being properly closed. --- instrumentation/opentelemetry-instrumentation-grpc/setup.cfg | 2 +- .../tests/test_client_interceptor.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg index 590cb5e9cf..6a7db72aa7 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: install_requires = opentelemetry-api == 0.13dev0 opentelemetry-sdk == 0.13dev0 - grpcio == 1.30 + grpcio ~= 1.27 [options.extras_require] test = diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py index 458f32e047..3ed40c141c 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py @@ -49,6 +49,7 @@ def tearDown(self): GrpcInstrumentorClient().uninstrument() self.memory_metrics_exporter.clear() self.server.stop(None) + self.channel.close() def _verify_success_records(self, num_bytes_out, num_bytes_in, method): # pylint: disable=protected-access,no-member From e21ee21d2fe77e6a07a5d680204f25e230d8110f Mon Sep 17 00:00:00 2001 From: vtdat Date: Thu, 27 Aug 2020 04:22:59 +0700 Subject: [PATCH 0517/1517] exporter/jaeger: Either collector submit batch or Agent emit batch (#982) * Either collector submit batch or Agent emit batch --- .../src/opentelemetry/exporter/jaeger/__init__.py | 3 ++- .../tests/test_jaeger_exporter.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index afa0b2578f..ccb530b7a1 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -168,7 +168,8 @@ def export(self, spans): if self.collector is not None: self.collector.submit(batch) - self.agent_client.emit(batch) + else: + self.agent_client.emit(batch) return SpanExportResult.SUCCESS diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py index 7b3916c5b9..a8809f88aa 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py @@ -367,7 +367,7 @@ def test_export(self): exporter._collector = collector_mock exporter.export((self._test_span,)) - self.assertEqual(agent_client_mock.emit.call_count, 2) + self.assertEqual(agent_client_mock.emit.call_count, 1) self.assertEqual(collector_mock.submit.call_count, 1) def test_agent_client(self): From f6a658ec4c0204b4e65ea1085a46ba79275bf08c Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Thu, 27 Aug 2020 19:11:27 +0200 Subject: [PATCH 0518/1517] Refactor is_valid to be an instance attribute (#1005) --- opentelemetry-api/CHANGELOG.md | 2 ++ .../src/opentelemetry/trace/span.py | 18 ++++-------------- .../tests/trace/test_defaultspan.py | 2 +- .../src/opentelemetry/sdk/trace/__init__.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index d5a69d4db4..8cb29e355d 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Refactor `SpanContext.is_valid` from a method to a data attribute + ([#1005](https://github.com/open-telemetry/opentelemetry-python/pull/1005)) - Moved samplers from API to SDK ([#1023](https://github.com/open-telemetry/opentelemetry-python/pull/1023)) diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index d207ecf565..1c00a3f5d9 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -187,6 +187,10 @@ def __init__( self.trace_flags = trace_flags self.trace_state = trace_state self.is_remote = is_remote + self.is_valid = ( + self.trace_id != INVALID_TRACE_ID + and self.span_id != INVALID_SPAN_ID + ) def __repr__(self) -> str: return ( @@ -199,20 +203,6 @@ def __repr__(self) -> str: self.is_remote, ) - def is_valid(self) -> bool: - """Get whether this `SpanContext` is valid. - - A `SpanContext` is said to be invalid if its trace ID or span ID is - invalid (i.e. ``0``). - - Returns: - True if the `SpanContext` is valid, false otherwise. - """ - return ( - self.trace_id != INVALID_TRACE_ID - and self.span_id != INVALID_SPAN_ID - ) - class DefaultSpan(Span): """The default Span that is used when no Span implementation is available. diff --git a/opentelemetry-api/tests/trace/test_defaultspan.py b/opentelemetry-api/tests/trace/test_defaultspan.py index d27f2b1bbc..67c2fc3352 100644 --- a/opentelemetry-api/tests/trace/test_defaultspan.py +++ b/opentelemetry-api/tests/trace/test_defaultspan.py @@ -32,4 +32,4 @@ def test_ctor(self): def test_invalid_span(self): self.assertIsNotNone(trace.INVALID_SPAN) self.assertIsNotNone(trace.INVALID_SPAN.get_context()) - self.assertFalse(trace.INVALID_SPAN.get_context().is_valid()) + self.assertFalse(trace.INVALID_SPAN.get_context().is_valid) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 034acd0f44..21cd6f518d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -774,7 +774,7 @@ def start_span( # pylint: disable=too-many-locals ): raise TypeError("parent must be a Span, SpanContext or None.") - if parent_context is None or not parent_context.is_valid(): + if parent_context is None or not parent_context.is_valid: parent = parent_context = None trace_id = generate_trace_id() trace_flags = None diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 9915860cf0..8005dffabd 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -165,7 +165,7 @@ def test_start_span_invalid_spancontext(self): new_span = tracer.start_span( "root", parent=trace_api.INVALID_SPAN_CONTEXT ) - self.assertTrue(new_span.context.is_valid()) + self.assertTrue(new_span.context.is_valid) self.assertIsNone(new_span.parent) def test_instrumentation_info(self): From 9b41a57b2e8a91ae38ba6f268b90944adcbebd15 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 27 Aug 2020 11:18:34 -0700 Subject: [PATCH 0519/1517] Remove lazy Event and Link API from Span interface (#1045) --- opentelemetry-api/CHANGELOG.md | 2 + .../src/opentelemetry/trace/__init__.py | 23 ---------- .../src/opentelemetry/trace/span.py | 21 --------- opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 43 ------------------- opentelemetry-sdk/tests/trace/test_trace.py | 41 +----------------- 6 files changed, 6 insertions(+), 126 deletions(-) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 8cb29e355d..fc39c9ef64 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -6,6 +6,8 @@ ([#1005](https://github.com/open-telemetry/opentelemetry-python/pull/1005)) - Moved samplers from API to SDK ([#1023](https://github.com/open-telemetry/opentelemetry-python/pull/1023)) +- Remove lazy Event and Link API from Span interface + ([#1045](https://github.com/open-telemetry/opentelemetry-python/pull/1045)) ## Version 0.12b0 diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 6c1bf46cc9..254c6de341 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -138,28 +138,6 @@ def attributes(self) -> types.Attributes: return self._attributes -class LazyLink(LinkBase): - """A lazy link to a `Span`. - - Args: - context: `SpanContext` of the `Span` to link to. - link_formatter: Callable object that returns the attributes of the - Link. - """ - - def __init__( - self, - context: "SpanContext", - link_formatter: types.AttributesFormatter, - ) -> None: - super().__init__(context) - self._link_formatter = link_formatter - - @property - def attributes(self) -> types.Attributes: - return self._link_formatter() - - class SpanKind(enum.Enum): """Specifies additional details on how this span relates to its parent span. @@ -464,7 +442,6 @@ def get_tracer_provider() -> TracerProvider: "DefaultSpan", "DefaultTracer", "DefaultTracerProvider", - "LazyLink", "Link", "LinkBase", "ParentSpan", diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 1c00a3f5d9..ecec757282 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -51,19 +51,6 @@ def add_event( timestamp if the `timestamp` argument is omitted. """ - @abc.abstractmethod - def add_lazy_event( - self, - name: str, - event_formatter: types.AttributesFormatter, - timestamp: typing.Optional[int] = None, - ) -> None: - """Adds an `Event`. - Adds a single `Event` with the name, an event formatter that calculates - the attributes lazily and, optionally, a timestamp. Implementations - should generate a timestamp if the `timestamp` argument is omitted. - """ - @abc.abstractmethod def update_name(self, name: str) -> None: """Updates the `Span` name. @@ -233,14 +220,6 @@ def add_event( ) -> None: pass - def add_lazy_event( - self, - name: str, - event_formatter: types.AttributesFormatter, - timestamp: typing.Optional[int] = None, - ) -> None: - pass - def update_name(self, name: str) -> None: pass diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 33d9d6e2e6..625e60ce3e 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -4,6 +4,8 @@ - Moved samplers from API to SDK ([#1023](https://github.com/open-telemetry/opentelemetry-python/pull/1023)) +- Remove lazy Event and Link API from Span interface + ([#1045](https://github.com/open-telemetry/opentelemetry-python/pull/1045)) ## Version 0.12b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 21cd6f518d..1a1efa96bf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -284,35 +284,6 @@ def attributes(self) -> types.Attributes: return self._attributes -class LazyEvent(EventBase): - """A text annotation with a set of attributes. - - Args: - name: Name of the event. - event_formatter: Callable object that returns the attributes of the - event. - timestamp: Timestamp of the event. If `None` it will filled - automatically. - """ - - def __init__( - self, - name: str, - event_formatter: types.AttributesFormatter, - timestamp: Optional[int] = None, - ) -> None: - super().__init__(name, timestamp) - self._event_formatter = event_formatter - - @property - def attributes(self) -> types.Attributes: - attributes = self._event_formatter() - _filter_attribute_values(attributes) - if not attributes: - attributes = Span._new_attributes() - return attributes - - def _is_valid_attribute_value(value: types.AttributeValue) -> bool: """Checks if attribute value is valid. @@ -596,20 +567,6 @@ def add_event( ) ) - def add_lazy_event( - self, - name: str, - event_formatter: types.AttributesFormatter, - timestamp: Optional[int] = None, - ) -> None: - self._add_event( - LazyEvent( - name=name, - event_formatter=event_formatter, - timestamp=time_ns() if timestamp is None else timestamp, - ) - ) - def start(self, start_time: Optional[int] = None) -> None: with self._lock: if not self.is_recording_events(): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 8005dffabd..acc2005826 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -572,13 +572,7 @@ def test_events(self): mutable_list = ["original_contents"] root.add_event("event3", {"name": mutable_list}) - def event_formatter(): - return {"name": "hello"} - - # lazy event - root.add_lazy_event("event4", event_formatter, now) - - self.assertEqual(len(root.events), 5) + self.assertEqual(len(root.events), 4) self.assertEqual(root.events[0].name, "event0") self.assertEqual(root.events[0].attributes, {}) @@ -604,13 +598,8 @@ def event_formatter(): root.events[3].attributes, {"name": ("original_contents",)} ) - self.assertEqual(root.events[4].name, "event4") - self.assertEqual(root.events[4].attributes, {"name": "hello"}) - self.assertEqual(root.events[4].timestamp, now) - def test_invalid_event_attributes(self): self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) - now = time_ns() with self.tracer.start_as_current_span("root") as root: root.add_event("event0", {"attr1": True, "attr2": ["hi", False]}) @@ -624,19 +613,6 @@ def test_invalid_event_attributes(self): self.assertEqual(root.events[2].attributes, {}) self.assertEqual(root.events[3].attributes, {"attr2": (1, 2)}) - def event_formatter(): - properties = {} - properties["attr1"] = dict() - properties["attr2"] = "hello" - return properties - - root.add_lazy_event("event4", event_formatter, now) - - self.assertEqual(len(root.events), 5) - self.assertEqual(root.events[4].name, "event4") - self.assertEqual(root.events[4].attributes, {"attr2": "hello"}) - self.assertEqual(root.events[4].timestamp, now) - def test_links(self): other_context1 = trace_api.SpanContext( trace_id=trace.generate_trace_id(), @@ -648,23 +624,14 @@ def test_links(self): span_id=trace.generate_span_id(), is_remote=False, ) - other_context3 = trace_api.SpanContext( - trace_id=trace.generate_trace_id(), - span_id=trace.generate_span_id(), - is_remote=False, - ) - - def get_link_attributes(): - return {"component": "http"} links = ( trace_api.Link(other_context1), trace_api.Link(other_context2, {"name": "neighbor"}), - trace_api.LazyLink(other_context3, get_link_attributes), ) with self.tracer.start_as_current_span("root", links=links) as root: - self.assertEqual(len(root.links), 3) + self.assertEqual(len(root.links), 2) self.assertEqual( root.links[0].context.trace_id, other_context1.trace_id ) @@ -679,10 +646,6 @@ def get_link_attributes(): root.links[1].context.span_id, other_context2.span_id ) self.assertEqual(root.links[1].attributes, {"name": "neighbor"}) - self.assertEqual( - root.links[2].context.span_id, other_context3.span_id - ) - self.assertEqual(root.links[2].attributes, {"component": "http"}) def test_update_name(self): with self.tracer.start_as_current_span("root") as root: From 6ba985a053d21d013bd7f51d8b4b37d92b1e1eaa Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Thu, 27 Aug 2020 22:00:02 +0200 Subject: [PATCH 0520/1517] Make return value of `get_correlations` immutable (#1024) * Make return value of `get_correlations` immutable --- opentelemetry-api/CHANGELOG.md | 2 ++ .../src/opentelemetry/correlationcontext/__init__.py | 11 ++++++----- .../correlationcontext/propagation/__init__.py | 2 +- .../correlationcontext/test_correlation_context.py | 3 ++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index fc39c9ef64..048da1f166 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -6,6 +6,8 @@ ([#1005](https://github.com/open-telemetry/opentelemetry-python/pull/1005)) - Moved samplers from API to SDK ([#1023](https://github.com/open-telemetry/opentelemetry-python/pull/1023)) +- Change return value type of `correlationcontext.get_correlations` to immutable `MappingProxyType` + ([#1024](https://github.com/open-telemetry/opentelemetry-python/pull/1024)) - Remove lazy Event and Link API from Span interface ([#1045](https://github.com/open-telemetry/opentelemetry-python/pull/1045)) diff --git a/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py b/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py index c16d75162a..8dbb357495 100644 --- a/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. import typing +from types import MappingProxyType from opentelemetry.context import get_value, set_value from opentelemetry.context.context import Context @@ -22,7 +23,7 @@ def get_correlations( context: typing.Optional[Context] = None, -) -> typing.Dict[str, object]: +) -> typing.Mapping[str, object]: """Returns the name/value pairs in the CorrelationContext Args: @@ -33,8 +34,8 @@ def get_correlations( """ correlations = get_value(_CORRELATION_CONTEXT_KEY, context=context) if isinstance(correlations, dict): - return correlations.copy() - return {} + return MappingProxyType(correlations.copy()) + return MappingProxyType({}) def get_correlation( @@ -67,7 +68,7 @@ def set_correlation( Returns: A Context with the value updated """ - correlations = get_correlations(context=context) + correlations = dict(get_correlations(context=context)) correlations[name] = value return set_value(_CORRELATION_CONTEXT_KEY, correlations, context=context) @@ -84,7 +85,7 @@ def remove_correlation( Returns: A Context with the name/value removed """ - correlations = get_correlations(context=context) + correlations = dict(get_correlations(context=context)) correlations.pop(name, None) return set_value(_CORRELATION_CONTEXT_KEY, correlations, context=context) diff --git a/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py index fca9465fbb..4032394ce7 100644 --- a/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py @@ -94,7 +94,7 @@ def inject( ) -def _format_correlations(correlations: typing.Dict[str, object]) -> str: +def _format_correlations(correlations: typing.Mapping[str, object]) -> str: return ",".join( key + "=" + urllib.parse.quote_plus(str(value)) for key, value in correlations.items() diff --git a/opentelemetry-api/tests/correlationcontext/test_correlation_context.py b/opentelemetry-api/tests/correlationcontext/test_correlation_context.py index 31996c6913..aaa5d9fa92 100644 --- a/opentelemetry-api/tests/correlationcontext/test_correlation_context.py +++ b/opentelemetry-api/tests/correlationcontext/test_correlation_context.py @@ -48,7 +48,8 @@ def test_modifying_correlations(self): ctx = cctx.set_correlation("test", "value") self.assertEqual(cctx.get_correlation("test", context=ctx), "value") correlations = cctx.get_correlations(context=ctx) - correlations["test"] = "mess-this-up" + with self.assertRaises(TypeError): + correlations["test"] = "mess-this-up" self.assertEqual(cctx.get_correlation("test", context=ctx), "value") def test_remove_correlations(self): From 7b6aba0af8a68133cf240fe6a0efe9d300259a02 Mon Sep 17 00:00:00 2001 From: Gunnlaugur Thor Briem Date: Fri, 28 Aug 2020 21:45:06 +0000 Subject: [PATCH 0521/1517] docs: fix outdated alpha statement (#1047) `README.md` and the opentelemetry website say this library is in beta, and releases have been called betas since March, so update `docs/index.rst` to be consistent with that. --- docs/index.rst | 4 ++-- .../opentelemetry-instrumentation-aiohttp-client/setup.cfg | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index a127bf4c42..b31088374c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,8 +11,8 @@ The Python `OpenTelemetry `_ client. This documentation describes the :doc:`opentelemetry-api `, :doc:`opentelemetry-sdk `, and several `integration packages <#integrations>`_. -**Please note** that this library is currently in alpha, and shouldn't be -used in production environments. +**Please note** that this library is currently in _beta_, and shouldn't +generally be used in production environments. Installation ------------ diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index 557b8d9a08..a222f323c0 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/instrumentation/ope platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python From dd47cd4f01e58d4d5451063442cbc73e6d61f518 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 31 Aug 2020 08:45:22 -0700 Subject: [PATCH 0522/1517] docs: updating readme (#1052) * docs: updating readme --- README.md | 94 ++++++------------------------------------------------- 1 file changed, 9 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 28938a3cba..ca9fd652ac 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,13 @@ --- -## About this project +## OpenTelemetry Python -The Python [OpenTelemetry](https://opentelemetry.io/) client. +The Python [OpenTelemetry](https://opentelemetry.io/) implementation. -## Installation +## Getting started + +OpenTelemetry's goal is to provide a single set of APIs to capture distributed traces and metrics from your application and send them to an observability platform. This project allows you to do just that for applications written in Python. This repository includes multiple installable packages. The `opentelemetry-api` package includes abstract classes and no-op implementations that comprise the OpenTelemetry API following @@ -52,9 +54,6 @@ Libraries that produce telemetry data should only depend on `opentelemetry-api`, and defer the choice of the SDK to the application developer. Applications may depend on `opentelemetry-sdk` or another package that implements the API. -**Please note** that this library is currently in _beta_, and shouldn't -generally be used in production environments. - The API and SDK packages are available on PyPI, and can installed via `pip`: ```sh @@ -94,13 +93,9 @@ The online documentation is available at https://opentelemetry-python.readthedoc if you want to access the documentation for the latest version use https://opentelemetry-python.readthedocs.io/en/latest/. -## Compatible Exporters - -See the [OpenTelemetry registry](https://opentelemetry.io/registry/?s=python) for a list of exporters available. - ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) +See [CONTRIBUTING.md](CONTRIBUTING.md). We meet weekly on Thursday at 9AM PT. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates. @@ -134,78 +129,7 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t -## Release Schedule - -OpenTelemetry Python is under active development. - -The library is not yet _generally available_, and releases aren't guaranteed to -conform to a specific version of the specification. Future releases will not -attempt to maintain backwards compatibility with previous releases. Each alpha -and beta release includes significant changes to the API and SDK packages, -making them incompatible with each other. - -The [v0.1 alpha -release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.1.0) -includes: - -- Tracing API -- Tracing SDK -- Metrics API -- Metrics SDK (Partial) -- W3C Trace Context Propagation -- B3 Context Propagation -- HTTP Integrations - -The [v0.2 alpha -release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.2.0) -includes: - -- OpenTracing Bridge -- Jaeger Trace Exporter -- Trace Sampling - -The [v0.3 alpha -release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.3.0) -includes: - -- Metrics Instruments and Labels -- Flask Integration -- PyMongo Integration - -The [v0.4 alpha -release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.4.0) -includes: - -- Metrics MinMaxSumCount Aggregator -- Context API -- Full Metrics SDK Pipeline -- Metrics STDOUT Exporter -- Dbapi2 Integration -- MySQL Integration -- Psycopg2 Integration -- Zipkin Exporter -- Prometheus Metrics Exporter -- New Examples and Improvements to Existing Examples - -The [v0.5 beta -release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.5.0) -includes: - -- W3C Correlation Context Propagation -- OpenTelemetry Collector Exporter Integration for both metrics and traces -- Metrics SDK -- Global configuration module -- Documentation improvements - -The [v0.6 beta -release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.6.0) -includes: - -- API changes and bugfixes -- An autoinstrumentation package and updated Flask instrumentation -- gRPC integration - -See the [project -milestones](https://github.com/open-telemetry/opentelemetry-python/milestones) -for details on upcoming releases. The dates and features described in issues +## Project Status + +Project [boards](https://github.com/open-telemetry/opentelemetry-python/projects) and [milestones](https://github.com/open-telemetry/opentelemetry-python/milestones) can be found at the respective links. We try to keep these accurate and should be the best place to go for answers on project status. The dates and features described in issues and milestones are estimates, and subject to change. From c435600ea465b6de0c6f49a18ba3c95a4ead734e Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 31 Aug 2020 12:07:32 -0700 Subject: [PATCH 0523/1517] Align sampling specs in SDK (#1034) --- .../exporter/datadog/exporter.py | 2 +- .../tests/test_datadog_exporter.py | 2 +- opentelemetry-api/CHANGELOG.md | 2 + .../src/opentelemetry/trace/__init__.py | 8 +- .../src/opentelemetry/trace/span.py | 4 +- .../tests/test_implementation.py | 6 +- opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 55 +++---- .../src/opentelemetry/sdk/trace/sampling.py | 153 +++++++++++++----- .../tests/trace/test_implementation.py | 6 +- .../tests/trace/test_sampling.py | 147 ++++++++++------- opentelemetry-sdk/tests/trace/test_trace.py | 39 ++--- 12 files changed, 261 insertions(+), 165 deletions(-) diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py index c23288442d..37c78187f8 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py @@ -247,7 +247,7 @@ def _get_sampling_rate(span): return ( span.sampler.rate if ctx.trace_flags.sampled - and isinstance(span.sampler, sampling.ProbabilitySampler) + and isinstance(span.sampler, sampling.TraceIdRatioBased) else None ) diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py index 47a0280c0a..73c8cb3bf8 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py @@ -498,7 +498,7 @@ def test_sampling_rate(self): is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) - sampler = sampling.ProbabilitySampler(0.5) + sampler = sampling.TraceIdRatioBased(0.5) span = trace.Span( name="sampled", context=context, parent=None, sampler=sampler diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 048da1f166..a22f98585e 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -8,6 +8,8 @@ ([#1023](https://github.com/open-telemetry/opentelemetry-python/pull/1023)) - Change return value type of `correlationcontext.get_correlations` to immutable `MappingProxyType` ([#1024](https://github.com/open-telemetry/opentelemetry-python/pull/1024)) +- Change is_recording_events to is_recording + ([#1034](https://github.com/open-telemetry/opentelemetry-python/pull/1034)) - Remove lazy Event and Link API from Span interface ([#1045](https://github.com/open-telemetry/opentelemetry-python/pull/1045)) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 254c6de341..1795192254 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -229,7 +229,7 @@ def start_span( name: str, parent: ParentSpan = CURRENT_SPAN, kind: SpanKind = SpanKind.INTERNAL, - attributes: typing.Optional[types.Attributes] = None, + attributes: types.Attributes = None, links: typing.Sequence[Link] = (), start_time: typing.Optional[int] = None, set_status_on_exception: bool = True, @@ -281,7 +281,7 @@ def start_as_current_span( name: str, parent: ParentSpan = CURRENT_SPAN, kind: SpanKind = SpanKind.INTERNAL, - attributes: typing.Optional[types.Attributes] = None, + attributes: types.Attributes = None, links: typing.Sequence[Link] = (), ) -> typing.Iterator["Span"]: """Context manager for creating a new span and set it @@ -357,7 +357,7 @@ def start_span( name: str, parent: ParentSpan = Tracer.CURRENT_SPAN, kind: SpanKind = SpanKind.INTERNAL, - attributes: typing.Optional[types.Attributes] = None, + attributes: types.Attributes = None, links: typing.Sequence[Link] = (), start_time: typing.Optional[int] = None, set_status_on_exception: bool = True, @@ -371,7 +371,7 @@ def start_as_current_span( name: str, parent: ParentSpan = Tracer.CURRENT_SPAN, kind: SpanKind = SpanKind.INTERNAL, - attributes: typing.Optional[types.Attributes] = None, + attributes: types.Attributes = None, links: typing.Sequence[Link] = (), ) -> typing.Iterator["Span"]: # pylint: disable=unused-argument,no-self-use diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index ecec757282..27bbc22336 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -62,7 +62,7 @@ def update_name(self, name: str) -> None: """ @abc.abstractmethod - def is_recording_events(self) -> bool: + def is_recording(self) -> bool: """Returns whether this span will be recorded. Returns true if this Span is active and recording information like @@ -203,7 +203,7 @@ def __init__(self, context: "SpanContext") -> None: def get_context(self) -> "SpanContext": return self._context - def is_recording_events(self) -> bool: + def is_recording(self) -> bool: return False def end(self, end_time: typing.Optional[int] = None) -> None: diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index d0f9404a91..0d5b22b18f 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -39,13 +39,13 @@ def test_default_tracer(self): with tracer.start_span("test") as span: self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) self.assertEqual(span, trace.INVALID_SPAN) - self.assertIs(span.is_recording_events(), False) + self.assertIs(span.is_recording(), False) with tracer.start_span("test2") as span2: self.assertEqual( span2.get_context(), trace.INVALID_SPAN_CONTEXT ) self.assertEqual(span2, trace.INVALID_SPAN) - self.assertIs(span2.is_recording_events(), False) + self.assertIs(span2.is_recording(), False) def test_span(self): with self.assertRaises(TypeError): @@ -55,7 +55,7 @@ def test_span(self): def test_default_span(self): span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) - self.assertIs(span.is_recording_events(), False) + self.assertIs(span.is_recording(), False) # METER diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 625e60ce3e..acd1ce1d7d 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -4,6 +4,8 @@ - Moved samplers from API to SDK ([#1023](https://github.com/open-telemetry/opentelemetry-python/pull/1023)) +- Sampling spec changes + ([#1034](https://github.com/open-telemetry/opentelemetry-python/pull/1034)) - Remove lazy Event and Link API from Span interface ([#1045](https://github.com/open-telemetry/opentelemetry-python/pull/1045)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 1a1efa96bf..95d04d8589 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -515,7 +515,7 @@ def get_context(self): def set_attribute(self, key: str, value: types.AttributeValue) -> None: with self._lock: - if not self.is_recording_events(): + if not self.is_recording(): return has_ended = self.end_time is not None if has_ended: @@ -541,7 +541,7 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: def _add_event(self, event: EventBase) -> None: with self._lock: - if not self.is_recording_events(): + if not self.is_recording(): return has_ended = self.end_time is not None @@ -569,7 +569,7 @@ def add_event( def start(self, start_time: Optional[int] = None) -> None: with self._lock: - if not self.is_recording_events(): + if not self.is_recording(): return has_started = self.start_time is not None if not has_started: @@ -583,7 +583,7 @@ def start(self, start_time: Optional[int] = None) -> None: def end(self, end_time: Optional[int] = None) -> None: with self._lock: - if not self.is_recording_events(): + if not self.is_recording(): return if self.start_time is None: raise RuntimeError("Calling end() on a not started span.") @@ -610,7 +610,7 @@ def update_name(self, name: str) -> None: return self.name = name - def is_recording_events(self) -> bool: + def is_recording(self) -> bool: return True def set_status(self, status: trace_api.Status) -> None: @@ -703,7 +703,7 @@ def start_as_current_span( name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, - attributes: Optional[types.Attributes] = None, + attributes: types.Attributes = None, links: Sequence[trace_api.Link] = (), ) -> Iterator[trace_api.Span]: span = self.start_span(name, parent, kind, attributes, links) @@ -714,7 +714,7 @@ def start_span( # pylint: disable=too-many-locals name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, - attributes: Optional[types.Attributes] = None, + attributes: types.Attributes = None, links: Sequence[trace_api.Link] = (), start_time: Optional[int] = None, set_status_on_exception: bool = True, @@ -741,6 +741,20 @@ def start_span( # pylint: disable=too-many-locals trace_flags = parent_context.trace_flags trace_state = parent_context.trace_state + # The sampler decides whether to create a real or no-op span at the + # time of span creation. No-op spans do not record events, and are not + # exported. + # The sampler may also add attributes to the newly-created span, e.g. + # to include information about the sampling result. + sampling_result = self.source.sampler.should_sample( + parent_context, trace_id, name, attributes, links, + ) + + trace_flags = ( + trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED) + if sampling_result.decision.is_sampled() + else trace_api.TraceFlags(trace_api.TraceFlags.DEFAULT) + ) context = trace_api.SpanContext( trace_id, generate_span_id(), @@ -749,29 +763,8 @@ def start_span( # pylint: disable=too-many-locals trace_state=trace_state, ) - # The sampler decides whether to create a real or no-op span at the - # time of span creation. No-op spans do not record events, and are not - # exported. - # The sampler may also add attributes to the newly-created span, e.g. - # to include information about the sampling decision. - sampling_decision = self.source.sampler.should_sample( - parent_context, - context.trace_id, - context.span_id, - name, - attributes, - links, - ) - - if sampling_decision.sampled: - options = context.trace_flags | trace_api.TraceFlags.SAMPLED - context.trace_flags = trace_api.TraceFlags(options) - if attributes is None: - span_attributes = sampling_decision.attributes - else: - # apply sampling decision attributes after initial attributes - span_attributes = attributes.copy() - span_attributes.update(sampling_decision.attributes) + # Only record if is_recording() is true + if sampling_result.decision.is_recording(): # pylint:disable=protected-access span = Span( name=name, @@ -779,7 +772,7 @@ def start_span( # pylint: disable=too-many-locals parent=parent_context, sampler=self.source.sampler, resource=self.source.resource, - attributes=span_attributes, + attributes=sampling_result.attributes, span_processor=self.source._active_span_processor, kind=kind, links=links, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 4429e33a48..c723e18adb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -18,18 +18,20 @@ OpenTelemetry provides two types of samplers: - `StaticSampler` -- `ProbabilitySampler` +- `TraceIdRatioBased` -A `StaticSampler` always returns the same sampling decision regardless of the conditions. Both possible StaticSamplers are already created: +A `StaticSampler` always returns the same sampling result regardless of the conditions. Both possible StaticSamplers are already created: -- Always sample spans: `ALWAYS_ON` -- Never sample spans: `ALWAYS_OFF` +- Always sample spans: ALWAYS_ON +- Never sample spans: ALWAYS_OFF -A `ProbabilitySampler` makes a random sampling decision based on the sampling probability given. If the span being sampled has a parent, `ProbabilitySampler` will respect the parent span's sampling decision. +A `TraceIdRatioBased` sampler makes a random sampling result based on the sampling probability given. -Currently, sampling decisions are always made during the creation of the span. However, this might not always be the case in the future (see `OTEP #115 `_). +If the span being sampled has a parent, `ParentBased` will respect the parent span's sampling result. Otherwise, it returns the sampling result from the given delegate sampler. -Custom samplers can be created by subclassing `Sampler` and implementing `Sampler.should_sample`. +Currently, sampling results are always made during the creation of the span. However, this might not always be the case in the future (see `OTEP #115 `_). + +Custom samplers can be created by subclassing `Sampler` and implementing `Sampler.should_sample` as well as `Sampler.get_description`. To use a sampler, pass it into the tracer provider constructor. For example: @@ -41,10 +43,10 @@ ConsoleSpanExporter, SimpleExportSpanProcessor, ) - from opentelemetry.sdk.trace.sampling import ProbabilitySampler + from opentelemetry.sdk.trace.sampling import TraceIdRatioBased # sample 1 in every 1000 traces - sampler = ProbabilitySampler(1/1000) + sampler = TraceIdRatioBased(1/1000) # set the sampler onto the global tracer provider trace.set_tracer_provider(TracerProvider(sampler=sampler)) @@ -54,41 +56,57 @@ SimpleExportSpanProcessor(ConsoleSpanExporter()) ) - # created spans will now be sampled by the ProbabilitySampler + # created spans will now be sampled by the TraceIdRatioBased sampler with trace.get_tracer(__name__).start_as_current_span("Test Span"): ... """ import abc -from typing import Dict, Mapping, Optional, Sequence +import enum +from typing import Optional, Sequence # pylint: disable=unused-import from opentelemetry.trace import Link, SpanContext -from opentelemetry.util.types import Attributes, AttributeValue +from opentelemetry.util.types import Attributes + + +class Decision(enum.Enum): + # IsRecording() == false, span will not be recorded and all events and attributes will be dropped. + NOT_RECORD = 0 + # IsRecording() == true, but Sampled flag MUST NOT be set. + RECORD = 1 + # IsRecording() == true AND Sampled flag` MUST be set. + RECORD_AND_SAMPLED = 2 + + def is_recording(self): + return self in (Decision.RECORD, Decision.RECORD_AND_SAMPLED) + def is_sampled(self): + return self is Decision.RECORD_AND_SAMPLED -class Decision: - """A sampling decision as applied to a newly-created Span. + +class SamplingResult: + """A sampling result as applied to a newly-created Span. Args: - sampled: Whether the `opentelemetry.trace.Span` should be sampled. + decision: A sampling decision based off of whether the span is recorded + and the sampled flag in trace flags in the span context. attributes: Attributes to add to the `opentelemetry.trace.Span`. """ def __repr__(self) -> str: return "{}({}, attributes={})".format( - type(self).__name__, str(self.sampled), str(self.attributes) + type(self).__name__, str(self.decision), str(self.attributes) ) def __init__( - self, - sampled: bool = False, - attributes: Optional[Mapping[str, "AttributeValue"]] = None, + self, decision: Decision, attributes: Attributes = None, ) -> None: - self.sampled = sampled # type: bool + self.decision = decision + # TODO: attributes must be immutable if attributes is None: - self.attributes = {} # type: Dict[str, "AttributeValue"] + self.attributes = {} else: - self.attributes = dict(attributes) + self.attributes = attributes class Sampler(abc.ABC): @@ -97,11 +115,14 @@ def should_sample( self, parent_context: Optional["SpanContext"], trace_id: int, - span_id: int, name: str, - attributes: Optional[Attributes] = None, + attributes: Attributes = None, links: Sequence["Link"] = (), - ) -> "Decision": + ) -> "SamplingResult": + pass + + @abc.abstractmethod + def get_description(self) -> str: pass @@ -115,15 +136,21 @@ def should_sample( self, parent_context: Optional["SpanContext"], trace_id: int, - span_id: int, name: str, - attributes: Optional[Attributes] = None, + attributes: Attributes = None, links: Sequence["Link"] = (), - ) -> "Decision": - return self._decision + ) -> "SamplingResult": + if self._decision is Decision.NOT_RECORD: + return SamplingResult(self._decision) + return SamplingResult(self._decision, attributes) + def get_description(self) -> str: + if self._decision is Decision.NOT_RECORD: + return "AlwaysOffSampler" + return "AlwaysOnSampler" -class ProbabilitySampler(Sampler): + +class TraceIdRatioBased(Sampler): """ Sampler that makes sampling decisions probabalistically based on `rate`, while also respecting the parent span sampling decision. @@ -133,6 +160,8 @@ class ProbabilitySampler(Sampler): """ def __init__(self, rate: float): + if rate < 0.0 or rate > 1.0: + raise ValueError("Probability must be in range [0.0, 1.0].") self._rate = rate self._bound = self.get_bound_for_rate(self._rate) @@ -161,26 +190,70 @@ def should_sample( self, parent_context: Optional["SpanContext"], trace_id: int, - span_id: int, name: str, - attributes: Optional[Attributes] = None, # TODO + attributes: Attributes = None, # TODO links: Sequence["Link"] = (), - ) -> "Decision": + ) -> "SamplingResult": + decision = Decision.NOT_RECORD + if trace_id & self.TRACE_ID_LIMIT < self.bound: + decision = Decision.RECORD_AND_SAMPLED + if decision is Decision.NOT_RECORD: + return SamplingResult(decision) + return SamplingResult(decision, attributes) + + def get_description(self) -> str: + return "TraceIdRatioBased{{{}}}".format(self._rate) + + +class ParentBased(Sampler): + """ + If a parent is set, follows the same sampling decision as the parent. + Otherwise, uses the delegate provided at initialization to make a + decision. + + Args: + delegate: The delegate sampler to use if parent is not set. + """ + + def __init__(self, delegate: Sampler): + self._delegate = delegate + + def should_sample( + self, + parent_context: Optional["SpanContext"], + trace_id: int, + name: str, + attributes: Attributes = None, # TODO + links: Sequence["Link"] = (), + ) -> "SamplingResult": if parent_context is not None: - return Decision(parent_context.trace_flags.sampled) + if ( + not parent_context.is_valid + or not parent_context.trace_flags.sampled + ): + return SamplingResult(Decision.NOT_RECORD) + return SamplingResult(Decision.RECORD_AND_SAMPLED, attributes) + + return self._delegate.should_sample( + parent_context=parent_context, + trace_id=trace_id, + name=name, + attributes=attributes, + links=links, + ) - return Decision(trace_id & self.TRACE_ID_LIMIT < self.bound) + def get_description(self): + return "ParentBased{{{}}}".format(self._delegate.get_description()) -ALWAYS_OFF = StaticSampler(Decision(False)) +ALWAYS_OFF = StaticSampler(Decision.NOT_RECORD) """Sampler that never samples spans, regardless of the parent span's sampling decision.""" -ALWAYS_ON = StaticSampler(Decision(True)) +ALWAYS_ON = StaticSampler(Decision.RECORD_AND_SAMPLED) """Sampler that always samples spans, regardless of the parent span's sampling decision.""" - -DEFAULT_OFF = ProbabilitySampler(0.0) +DEFAULT_OFF = ParentBased(ALWAYS_OFF) """Sampler that respects its parent span's sampling decision, but otherwise never samples.""" -DEFAULT_ON = ProbabilitySampler(1.0) +DEFAULT_ON = ParentBased(ALWAYS_ON) """Sampler that respects its parent span's sampling decision, but otherwise always samples.""" diff --git a/opentelemetry-sdk/tests/trace/test_implementation.py b/opentelemetry-sdk/tests/trace/test_implementation.py index cd53a64395..4582b06957 100644 --- a/opentelemetry-sdk/tests/trace/test_implementation.py +++ b/opentelemetry-sdk/tests/trace/test_implementation.py @@ -31,11 +31,11 @@ def test_tracer(self): with tracer.start_span("test") as span: self.assertNotEqual(span.get_context(), INVALID_SPAN_CONTEXT) self.assertNotEqual(span, INVALID_SPAN) - self.assertIs(span.is_recording_events(), True) + self.assertIs(span.is_recording(), True) with tracer.start_span("test2") as span2: self.assertNotEqual(span2.get_context(), INVALID_SPAN_CONTEXT) self.assertNotEqual(span2, INVALID_SPAN) - self.assertIs(span2.is_recording_events(), True) + self.assertIs(span2.is_recording(), True) def test_span(self): with self.assertRaises(Exception): @@ -44,4 +44,4 @@ def test_span(self): span = trace.Span("name", INVALID_SPAN_CONTEXT) self.assertEqual(span.get_context(), INVALID_SPAN_CONTEXT) - self.assertIs(span.is_recording_events(), True) + self.assertIs(span.is_recording(), True) diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index a198bd3296..2c03f0bf73 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -30,10 +30,12 @@ def test_always_on(self): ), 0xDEADBEF1, 0xDEADBEF2, - "unsampled parent, sampling on", + {"unsampled parent": "sampling on"}, + ) + self.assertTrue(no_record_always_on.decision.is_sampled()) + self.assertEqual( + no_record_always_on.attributes, {"unsampled parent": "sampling on"} ) - self.assertTrue(no_record_always_on.sampled) - self.assertEqual(no_record_always_on.attributes, {}) sampled_always_on = sampling.ALWAYS_ON.should_sample( trace.SpanContext( @@ -41,10 +43,12 @@ def test_always_on(self): ), 0xDEADBEF1, 0xDEADBEF2, - "sampled parent, sampling on", + {"sampled parent": "sampling on"}, + ) + self.assertTrue(no_record_always_on.decision.is_sampled()) + self.assertEqual( + sampled_always_on.attributes, {"sampled parent": "sampling on"} ) - self.assertTrue(sampled_always_on.sampled) - self.assertEqual(sampled_always_on.attributes, {}) def test_always_off(self): no_record_always_off = sampling.ALWAYS_OFF.should_sample( @@ -55,7 +59,7 @@ def test_always_off(self): 0xDEADBEF2, "unsampled parent, sampling off", ) - self.assertFalse(no_record_always_off.sampled) + self.assertFalse(no_record_always_off.decision.is_sampled()) self.assertEqual(no_record_always_off.attributes, {}) sampled_always_on = sampling.ALWAYS_OFF.should_sample( @@ -66,7 +70,7 @@ def test_always_off(self): 0xDEADBEF2, "sampled parent, sampling off", ) - self.assertFalse(sampled_always_on.sampled) + self.assertFalse(sampled_always_on.decision.is_sampled()) self.assertEqual(sampled_always_on.attributes, {}) def test_default_on(self): @@ -78,7 +82,7 @@ def test_default_on(self): 0xDEADBEF2, "unsampled parent, sampling on", ) - self.assertFalse(no_record_default_on.sampled) + self.assertFalse(no_record_default_on.decision.is_sampled()) self.assertEqual(no_record_default_on.attributes, {}) sampled_default_on = sampling.DEFAULT_ON.should_sample( @@ -87,10 +91,20 @@ def test_default_on(self): ), 0xDEADBEF1, 0xDEADBEF2, - "sampled parent, sampling on", + {"sampled parent": "sampling on"}, + ) + self.assertTrue(sampled_default_on.decision.is_sampled()) + self.assertEqual( + sampled_default_on.attributes, {"sampled parent": "sampling on"} + ) + + default_on = sampling.DEFAULT_ON.should_sample( + None, 0xDEADBEF1, 0xDEADBEF2, {"sampled parent": "sampling on"}, + ) + self.assertTrue(default_on.decision.is_sampled()) + self.assertEqual( + default_on.attributes, {"sampled parent": "sampling on"} ) - self.assertTrue(sampled_default_on.sampled) - self.assertEqual(sampled_default_on.attributes, {}) def test_default_off(self): no_record_default_off = sampling.DEFAULT_OFF.should_sample( @@ -101,7 +115,7 @@ def test_default_off(self): 0xDEADBEF2, "unsampled parent, sampling off", ) - self.assertFalse(no_record_default_off.sampled) + self.assertFalse(no_record_default_off.decision.is_sampled()) self.assertEqual(no_record_default_off.attributes, {}) sampled_default_off = sampling.DEFAULT_OFF.should_sample( @@ -110,70 +124,49 @@ def test_default_off(self): ), 0xDEADBEF1, 0xDEADBEF2, - "sampled parent, sampling off", + {"sampled parent": "sampling on"}, + ) + self.assertTrue(sampled_default_off.decision.is_sampled()) + self.assertEqual( + sampled_default_off.attributes, {"sampled parent": "sampling on"} + ) + + default_off = sampling.DEFAULT_OFF.should_sample( + None, 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling off", ) - self.assertTrue(sampled_default_off.sampled) - self.assertEqual(sampled_default_off.attributes, {}) + self.assertFalse(default_off.decision.is_sampled()) + self.assertEqual(default_off.attributes, {}) def test_probability_sampler(self): - sampler = sampling.ProbabilitySampler(0.5) + sampler = sampling.TraceIdRatioBased(0.5) # Check that we sample based on the trace ID if the parent context is # null self.assertTrue( sampler.should_sample( None, 0x7FFFFFFFFFFFFFFF, 0xDEADBEEF, "span name" - ).sampled + ).decision.is_sampled() ) self.assertFalse( sampler.should_sample( None, 0x8000000000000000, 0xDEADBEEF, "span name" - ).sampled - ) - - # Check that the sampling decision matches the parent context if given, - # and that the sampler ignores the trace ID - self.assertFalse( - sampler.should_sample( - trace.SpanContext( - 0xDEADBEF0, - 0xDEADBEF1, - is_remote=False, - trace_flags=TO_DEFAULT, - ), - 0x7FFFFFFFFFFFFFFF, - 0xDEADBEEF, - "span name", - ).sampled - ) - self.assertTrue( - sampler.should_sample( - trace.SpanContext( - 0xDEADBEF0, - 0xDEADBEF1, - is_remote=False, - trace_flags=TO_SAMPLED, - ), - 0x8000000000000000, - 0xDEADBEEF, - "span name", - ).sampled + ).decision.is_sampled() ) def test_probability_sampler_zero(self): - default_off = sampling.ProbabilitySampler(0.0) + default_off = sampling.TraceIdRatioBased(0.0) self.assertFalse( default_off.should_sample( None, 0x0, 0xDEADBEEF, "span name" - ).sampled + ).decision.is_sampled() ) def test_probability_sampler_one(self): - default_off = sampling.ProbabilitySampler(1.0) + default_off = sampling.TraceIdRatioBased(1.0) self.assertTrue( default_off.should_sample( None, 0xFFFFFFFFFFFFFFFF, 0xDEADBEEF, "span name" - ).sampled + ).decision.is_sampled() ) def test_probability_sampler_limits(self): @@ -181,19 +174,19 @@ def test_probability_sampler_limits(self): # Sample one of every 2^64 (= 5e-20) traces. This is the lowest # possible meaningful sampling rate, only traces with trace ID 0x0 # should get sampled. - almost_always_off = sampling.ProbabilitySampler(2 ** -64) + almost_always_off = sampling.TraceIdRatioBased(2 ** -64) self.assertTrue( almost_always_off.should_sample( None, 0x0, 0xDEADBEEF, "span name" - ).sampled + ).decision.is_sampled() ) self.assertFalse( almost_always_off.should_sample( None, 0x1, 0xDEADBEEF, "span name" - ).sampled + ).decision.is_sampled() ) self.assertEqual( - sampling.ProbabilitySampler.get_bound_for_rate(2 ** -64), 0x1 + sampling.TraceIdRatioBased.get_bound_for_rate(2 ** -64), 0x1 ) # Sample every trace with trace ID less than 0xffffffffffffffff. In @@ -204,11 +197,11 @@ def test_probability_sampler_limits(self): # # 1 - sys.float_info.epsilon - almost_always_on = sampling.ProbabilitySampler(1 - 2 ** -64) + almost_always_on = sampling.TraceIdRatioBased(1 - 2 ** -64) self.assertTrue( almost_always_on.should_sample( None, 0xFFFFFFFFFFFFFFFE, 0xDEADBEEF, "span name" - ).sampled + ).decision.is_sampled() ) # These tests are logically consistent, but fail because of the float @@ -224,19 +217,19 @@ def test_probability_sampler_limits(self): # ).sampled # ) # self.assertEqual( - # sampling.ProbabilitySampler.get_bound_for_rate(1 - 2 ** -64)), + # sampling.TraceIdRatioBased.get_bound_for_rate(1 - 2 ** -64)), # 0xFFFFFFFFFFFFFFFF, # ) # Check that a sampler with the highest effective sampling rate < 1 # refuses to sample traces with trace ID 0xffffffffffffffff. - almost_almost_always_on = sampling.ProbabilitySampler( + almost_almost_always_on = sampling.TraceIdRatioBased( 1 - sys.float_info.epsilon ) self.assertFalse( almost_almost_always_on.should_sample( None, 0xFFFFFFFFFFFFFFFF, 0xDEADBEEF, "span name" - ).sampled + ).decision.is_sampled() ) # Check that the higest effective sampling rate is actually lower than # the highest theoretical sampling rate. If this test fails the test @@ -244,3 +237,35 @@ def test_probability_sampler_limits(self): self.assertLess( almost_almost_always_on.bound, 0xFFFFFFFFFFFFFFFF, ) + + def test_parent_based(self): + sampler = sampling.ParentBased(sampling.ALWAYS_ON) + # Check that the sampling decision matches the parent context if given + self.assertFalse( + sampler.should_sample( + trace.SpanContext( + 0xDEADBEF0, + 0xDEADBEF1, + is_remote=False, + trace_flags=TO_DEFAULT, + ), + 0x7FFFFFFFFFFFFFFF, + 0xDEADBEEF, + "span name", + ).decision.is_sampled() + ) + + sampler2 = sampling.ParentBased(sampling.ALWAYS_OFF) + self.assertTrue( + sampler2.should_sample( + trace.SpanContext( + 0xDEADBEF0, + 0xDEADBEF1, + is_remote=False, + trace_flags=TO_SAMPLED, + ), + 0x8000000000000000, + 0xDEADBEEF, + "span name", + ).decision.is_sampled() + ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index acc2005826..1c272048ca 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -140,6 +140,12 @@ def test_default_sampler(self): child_span = tracer.start_span(name="child span", parent=root_span) self.assertIsInstance(child_span, trace.Span) self.assertTrue(root_span.context.trace_flags.sampled) + self.assertEqual( + root_span.get_context().trace_flags, trace_api.TraceFlags.SAMPLED + ) + self.assertEqual( + child_span.get_context().trace_flags, trace_api.TraceFlags.SAMPLED + ) def test_sampler_no_sampling(self): tracer_provider = trace.TracerProvider(sampling.ALWAYS_OFF) @@ -151,6 +157,12 @@ def test_sampler_no_sampling(self): self.assertIsInstance(root_span, trace_api.DefaultSpan) child_span = tracer.start_span(name="child span", parent=root_span) self.assertIsInstance(child_span, trace_api.DefaultSpan) + self.assertEqual( + root_span.get_context().trace_flags, trace_api.TraceFlags.DEFAULT + ) + self.assertEqual( + child_span.get_context().trace_flags, trace_api.TraceFlags.DEFAULT + ) class TestSpanCreation(unittest.TestCase): @@ -201,7 +213,7 @@ def test_invalid_instrumentation_info(self): tracer1.instrumentation_info, InstrumentationInfo ) span1 = tracer1.start_span("foo") - self.assertTrue(span1.is_recording_events()) + self.assertTrue(span1.is_recording()) self.assertEqual(tracer1.instrumentation_info.version, "") self.assertEqual( tracer1.instrumentation_info.name, "ERROR:MISSING MODULE NAME" @@ -521,36 +533,25 @@ def test_check_attribute_helper(self): self.assertTrue(trace._is_valid_attribute_value(15)) def test_sampling_attributes(self): - decision_attributes = { + sampling_attributes = { "sampler-attr": "sample-val", "attr-in-both": "decision-attr", } tracer_provider = trace.TracerProvider( - sampling.StaticSampler( - sampling.Decision(sampled=True, attributes=decision_attributes) - ) + sampling.StaticSampler(sampling.Decision.RECORD_AND_SAMPLED,) ) self.tracer = tracer_provider.get_tracer(__name__) - with self.tracer.start_as_current_span("root2") as root: - self.assertEqual(len(root.attributes), 2) - self.assertEqual(root.attributes["sampler-attr"], "sample-val") - self.assertEqual(root.attributes["attr-in-both"], "decision-attr") - - attributes = { - "attr-key": "val", - "attr-key2": "val2", - "attr-in-both": "span-attr", - } with self.tracer.start_as_current_span( - "root2", attributes=attributes + name="root2", attributes=sampling_attributes ) as root: - self.assertEqual(len(root.attributes), 4) - self.assertEqual(root.attributes["attr-key"], "val") - self.assertEqual(root.attributes["attr-key2"], "val2") + self.assertEqual(len(root.attributes), 2) self.assertEqual(root.attributes["sampler-attr"], "sample-val") self.assertEqual(root.attributes["attr-in-both"], "decision-attr") + self.assertEqual( + root.get_context().trace_flags, trace_api.TraceFlags.SAMPLED + ) def test_events(self): self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) From 140e5f9bb4c8e815e7836b8d7380ee9d66844d11 Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Wed, 2 Sep 2020 07:42:01 +0200 Subject: [PATCH 0524/1517] RequestsInstrumentor: Add support for prepared requests (#1040) in addition to Session.request instrument Session.send to also create spans for prepared requests. --- docs/getting_started/tests/test_flask.py | 2 +- .../CHANGELOG.md | 3 + .../instrumentation/requests/__init__.py | 110 +++++++--- .../tests/test_requests_integration.py | 196 ++++++++++-------- 4 files changed, 185 insertions(+), 126 deletions(-) diff --git a/docs/getting_started/tests/test_flask.py b/docs/getting_started/tests/test_flask.py index 321098ce97..6ba296a5c0 100644 --- a/docs/getting_started/tests/test_flask.py +++ b/docs/getting_started/tests/test_flask.py @@ -38,6 +38,6 @@ def test_flask(self): server.terminate() output = str(server.stdout.read()) - self.assertIn('"name": "HTTP get"', output) + self.assertIn('"name": "HTTP GET"', output) self.assertIn('"name": "example-request"', output) self.assertIn('"name": "hello"', output) diff --git a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md index 3f18f6101b..eeb2e83725 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add support for instrumenting prepared requests + ([#1040](https://github.com/open-telemetry/opentelemetry-python/pull/1040)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index e2c54b7f1b..16e8952fea 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -29,26 +29,17 @@ opentelemetry.instrumentation.requests.RequestsInstrumentor().instrument() response = requests.get(url="https://www.example.org/") -Limitations ------------ - -Note that calls that do not use the higher-level APIs but use -:code:`requests.sessions.Session.send` (or an alias thereof) directly, are -currently not traced. If you find any other way to trigger an untraced HTTP -request, please report it via a GitHub issue with :code:`[requests: untraced -API]` in the title. - API --- """ import functools import types -from urllib.parse import urlparse from requests import Timeout, URLRequired from requests.exceptions import InvalidSchema, InvalidURL, MissingSchema from requests.sessions import Session +from requests.structures import CaseInsensitiveDict from opentelemetry import context, propagators from opentelemetry.instrumentation.instrumentor import BaseInstrumentor @@ -57,6 +48,10 @@ from opentelemetry.trace import SpanKind, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode +# A key to a context variable to avoid creating duplicate spans when instrumenting +# both, Session.request and Session.send, since Session.request calls into Session.send +_SUPPRESS_REQUESTS_INSTRUMENTATION_KEY = "suppress_requests_instrumentation" + # pylint: disable=unused-argument def _instrument(tracer_provider=None, span_callback=None): @@ -71,15 +66,54 @@ def _instrument(tracer_provider=None, span_callback=None): # before v1.0.0, Dec 17, 2012, see # https://github.com/psf/requests/commit/4e5c4a6ab7bb0195dececdd19bb8505b872fe120) - wrapped = Session.request + wrapped_request = Session.request + wrapped_send = Session.send - @functools.wraps(wrapped) + @functools.wraps(wrapped_request) def instrumented_request(self, method, url, *args, **kwargs): - if context.get_value("suppress_instrumentation"): - return wrapped(self, method, url, *args, **kwargs) + def get_or_create_headers(): + headers = kwargs.get("headers") + if headers is None: + headers = {} + kwargs["headers"] = headers + + return headers + + def call_wrapped(): + return wrapped_request(self, method, url, *args, **kwargs) + + return _instrumented_requests_call( + method, url, call_wrapped, get_or_create_headers + ) + + @functools.wraps(wrapped_send) + def instrumented_send(self, request, **kwargs): + def get_or_create_headers(): + request.headers = ( + request.headers + if request.headers is not None + else CaseInsensitiveDict() + ) + return request.headers + + def call_wrapped(): + return wrapped_send(self, request, **kwargs) + + return _instrumented_requests_call( + request.method, request.url, call_wrapped, get_or_create_headers + ) + + def _instrumented_requests_call( + method: str, url: str, call_wrapped, get_or_create_headers + ): + if context.get_value("suppress_instrumentation") or context.get_value( + _SUPPRESS_REQUESTS_INSTRUMENTATION_KEY + ): + return call_wrapped() # See # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#http-client + method = method.upper() span_name = "HTTP {}".format(method) exception = None @@ -91,17 +125,19 @@ def instrumented_request(self, method, url, *args, **kwargs): span.set_attribute("http.method", method.upper()) span.set_attribute("http.url", url) - headers = kwargs.get("headers", {}) or {} + headers = get_or_create_headers() propagators.inject(type(headers).__setitem__, headers) - kwargs["headers"] = headers + token = context.attach( + context.set_value(_SUPPRESS_REQUESTS_INSTRUMENTATION_KEY, True) + ) try: - result = wrapped( - self, method, url, *args, **kwargs - ) # *** PROCEED + result = call_wrapped() # *** PROCEED except Exception as exc: # pylint: disable=W0703 exception = exc result = getattr(exc, "response", None) + finally: + context.detach(token) if exception is not None: span.set_status( @@ -124,24 +160,34 @@ def instrumented_request(self, method, url, *args, **kwargs): return result - instrumented_request.opentelemetry_ext_requests_applied = True - + instrumented_request.opentelemetry_instrumentation_requests_applied = True Session.request = instrumented_request - # TODO: We should also instrument requests.sessions.Session.send - # but to avoid doubled spans, we would need some context-local - # state (i.e., only create a Span if the current context's URL is - # different, then push the current URL, pop it afterwards) + instrumented_send.opentelemetry_instrumentation_requests_applied = True + Session.send = instrumented_send def _uninstrument(): - # pylint: disable=global-statement """Disables instrumentation of :code:`requests` through this module. Note that this only works if no other module also patches requests.""" - if getattr(Session.request, "opentelemetry_ext_requests_applied", False): - original = Session.request.__wrapped__ # pylint:disable=no-member - Session.request = original + _uninstrument_from(Session) + + +def _uninstrument_from(instr_root, restore_as_bound_func=False): + for instr_func_name in ("request", "send"): + instr_func = getattr(instr_root, instr_func_name) + if not getattr( + instr_func, + "opentelemetry_instrumentation_requests_applied", + False, + ): + continue + + original = instr_func.__wrapped__ # pylint:disable=no-member + if restore_as_bound_func: + original = types.MethodType(original, instr_root) + setattr(instr_root, instr_func_name, original) def _exception_to_canonical_code(exc: Exception) -> StatusCanonicalCode: @@ -179,8 +225,4 @@ def _uninstrument(self, **kwargs): @staticmethod def uninstrument_session(session): """Disables instrumentation on the session object.""" - if getattr( - session.request, "opentelemetry_ext_requests_applied", False - ): - original = session.request.__wrapped__ # pylint:disable=no-member - session.request = types.MethodType(original, session) + _uninstrument_from(session, restore_as_bound_func=True) diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py index da09118e5b..0e0492f47e 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import abc from unittest import mock import httpretty @@ -26,32 +27,47 @@ from opentelemetry.trace.status import StatusCanonicalCode -class TestRequestsIntegration(TestBase): +class RequestsIntegrationTestBase(abc.ABC): + # pylint: disable=no-member + URL = "http://httpbin.org/status/200" + # pylint: disable=invalid-name def setUp(self): super().setUp() RequestsInstrumentor().instrument() httpretty.enable() - httpretty.register_uri( - httpretty.GET, self.URL, body="Hello!", - ) + httpretty.register_uri(httpretty.GET, self.URL, body="Hello!") + # pylint: disable=invalid-name def tearDown(self): super().tearDown() RequestsInstrumentor().uninstrument() httpretty.disable() + def assert_span(self, exporter=None, num_spans=1): + if exporter is None: + exporter = self.memory_exporter + span_list = exporter.get_finished_spans() + self.assertEqual(num_spans, len(span_list)) + if num_spans == 0: + return None + if num_spans == 1: + return span_list[0] + return span_list + + @staticmethod + @abc.abstractmethod + def perform_request(url: str, session: requests.Session = None): + pass + def test_basic(self): - result = requests.get(self.URL) + result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") - - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] + span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) - self.assertEqual(span.name, "HTTP get") + self.assertEqual(span.name, "HTTP GET") self.assertEqual( span.attributes, @@ -77,12 +93,10 @@ def test_not_foundbasic(self): httpretty.register_uri( httpretty.GET, url_404, status=404, ) - result = requests.get(url_404) + result = self.perform_request(url_404) self.assertEqual(result.status_code, 404) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] + span = self.assert_span() self.assertEqual(span.attributes.get("http.status_code"), 404) self.assertEqual(span.attributes.get("http.status_text"), "Not Found") @@ -92,31 +106,11 @@ def test_not_foundbasic(self): trace.status.StatusCanonicalCode.NOT_FOUND, ) - def test_invalid_url(self): - url = "http://[::1/nope" - - with self.assertRaises(ValueError): - requests.post(url) - - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] - - self.assertEqual(span.name, "HTTP post") - self.assertEqual( - span.attributes, - {"component": "http", "http.method": "POST", "http.url": url}, - ) - self.assertEqual( - span.status.canonical_code, StatusCanonicalCode.INVALID_ARGUMENT - ) - def test_uninstrument(self): RequestsInstrumentor().uninstrument() - result = requests.get(self.URL) + result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) + self.assert_span(num_spans=0) # instrument again to avoid annoying warning message RequestsInstrumentor().instrument() @@ -124,49 +118,43 @@ def test_uninstrument_session(self): session1 = requests.Session() RequestsInstrumentor().uninstrument_session(session1) - result = session1.get(self.URL) + result = self.perform_request(self.URL, session1) self.assertEqual(result.text, "Hello!") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) + self.assert_span(num_spans=0) # Test that other sessions as well as global requests is still # instrumented session2 = requests.Session() - result = session2.get(self.URL) + result = self.perform_request(self.URL, session2) self.assertEqual(result.text, "Hello!") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) + self.assert_span() self.memory_exporter.clear() - result = requests.get(self.URL) + result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) + self.assert_span() def test_suppress_instrumentation(self): token = context.attach( context.set_value("suppress_instrumentation", True) ) try: - result = requests.get(self.URL) + result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") finally: context.detach(token) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) + self.assert_span(num_spans=0) def test_distributed_context(self): previous_propagator = propagators.get_global_httptextformat() try: propagators.set_global_httptextformat(MockHTTPTextFormat()) - result = requests.get(self.URL) + result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] + span = self.assert_span() headers = dict(httpretty.last_request().headers) self.assertIn(MockHTTPTextFormat.TRACE_ID_KEY, headers) @@ -195,13 +183,10 @@ def span_callback(span, result: requests.Response): tracer_provider=self.tracer_provider, span_callback=span_callback, ) - result = requests.get(self.URL) + result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] - + span = self.assert_span() self.assertEqual( span.attributes, { @@ -221,28 +206,21 @@ def test_custom_tracer_provider(self): RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument(tracer_provider=tracer_provider) - result = requests.get(self.URL) + result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") - span_list = exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] - + span = self.assert_span(exporter=exporter) self.assertIs(span.resource, resource) - def test_if_headers_equals_none(self): - result = requests.get(self.URL, headers=None) - self.assertEqual(result.text, "Hello!") - - @mock.patch("requests.Session.send", side_effect=requests.RequestException) + @mock.patch( + "requests.adapters.HTTPAdapter.send", + side_effect=requests.RequestException, + ) def test_requests_exception_without_response(self, *_, **__): - with self.assertRaises(requests.RequestException): - requests.get(self.URL) + self.perform_request(self.URL) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] + span = self.assert_span() self.assertEqual( span.attributes, {"component": "http", "http.method": "GET", "http.url": self.URL}, @@ -256,17 +234,14 @@ def test_requests_exception_without_response(self, *_, **__): mocked_response.reason = "Internal Server Error" @mock.patch( - "requests.Session.send", + "requests.adapters.HTTPAdapter.send", side_effect=requests.RequestException(response=mocked_response), ) def test_requests_exception_with_response(self, *_, **__): - with self.assertRaises(requests.RequestException): - requests.get(self.URL) + self.perform_request(self.URL) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] + span = self.assert_span() self.assertEqual( span.attributes, { @@ -281,27 +256,66 @@ def test_requests_exception_with_response(self, *_, **__): span.status.canonical_code, StatusCanonicalCode.INTERNAL ) - @mock.patch("requests.Session.send", side_effect=Exception) + @mock.patch("requests.adapters.HTTPAdapter.send", side_effect=Exception) def test_requests_basic_exception(self, *_, **__): - with self.assertRaises(Exception): - requests.get(self.URL) + self.perform_request(self.URL) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) + span = self.assert_span() self.assertEqual( - span_list[0].status.canonical_code, StatusCanonicalCode.UNKNOWN + span.status.canonical_code, StatusCanonicalCode.UNKNOWN ) - @mock.patch("requests.Session.send", side_effect=requests.Timeout) + @mock.patch( + "requests.adapters.HTTPAdapter.send", side_effect=requests.Timeout + ) def test_requests_timeout_exception(self, *_, **__): - with self.assertRaises(Exception): - requests.get(self.URL) + self.perform_request(self.URL) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) + span = self.assert_span() self.assertEqual( - span_list[0].status.canonical_code, - StatusCanonicalCode.DEADLINE_EXCEEDED, + span.status.canonical_code, StatusCanonicalCode.DEADLINE_EXCEEDED ) + + +class TestRequestsIntegration(RequestsIntegrationTestBase, TestBase): + @staticmethod + def perform_request(url: str, session: requests.Session = None): + if session is None: + return requests.get(url) + return session.get(url) + + def test_invalid_url(self): + url = "http://[::1/nope" + + with self.assertRaises(ValueError): + requests.post(url) + + span = self.assert_span() + + self.assertEqual(span.name, "HTTP POST") + self.assertEqual( + span.attributes, + {"component": "http", "http.method": "POST", "http.url": url}, + ) + self.assertEqual( + span.status.canonical_code, StatusCanonicalCode.INVALID_ARGUMENT + ) + + def test_if_headers_equals_none(self): + result = requests.get(self.URL, headers=None) + self.assertEqual(result.text, "Hello!") + self.assert_span() + + +class TestRequestsIntegrationPreparedRequest( + RequestsIntegrationTestBase, TestBase +): + @staticmethod + def perform_request(url: str, session: requests.Session = None): + if session is None: + session = requests.Session() + request = requests.Request("GET", url) + prepared_request = session.prepare_request(request) + return session.send(prepared_request) From e650fe7f593dad84d5832a1574746326f9b0e2a0 Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Wed, 2 Sep 2020 17:21:58 +0200 Subject: [PATCH 0525/1517] api: Rename CorrelationContext to Baggage (#1060) --- docs/api/api.rst | 2 +- docs/api/baggage.propagation.rst | 7 ++ docs/api/baggage.rst | 14 ++++ docs/api/correlationcontext.propagation.rst | 7 -- docs/api/correlationcontext.rst | 14 ---- docs/getting-started.rst | 2 +- .../opentracing_shim/__init__.py | 6 +- opentelemetry-api/CHANGELOG.md | 2 + .../__init__.py | 48 ++++++------- .../propagation/__init__.py | 42 ++++++----- .../src/opentelemetry/propagators/__init__.py | 8 +-- .../tests/correlationcontext/test_baggage.py | 69 ++++++++++++++++++ ...agation.py => test_baggage_propagation.py} | 34 ++++----- .../test_correlation_context.py | 71 ------------------- .../propagators/test_global_httptextformat.py | 16 ++--- .../src/opentelemetry/sdk/metrics/__init__.py | 2 +- 16 files changed, 167 insertions(+), 177 deletions(-) create mode 100644 docs/api/baggage.propagation.rst create mode 100644 docs/api/baggage.rst delete mode 100644 docs/api/correlationcontext.propagation.rst delete mode 100644 docs/api/correlationcontext.rst rename opentelemetry-api/src/opentelemetry/{correlationcontext => baggage}/__init__.py (63%) rename opentelemetry-api/src/opentelemetry/{correlationcontext => baggage}/propagation/__init__.py (68%) create mode 100644 opentelemetry-api/tests/correlationcontext/test_baggage.py rename opentelemetry-api/tests/correlationcontext/{test_correlation_context_propagation.py => test_baggage_propagation.py} (80%) delete mode 100644 opentelemetry-api/tests/correlationcontext/test_correlation_context.py diff --git a/docs/api/api.rst b/docs/api/api.rst index e1e8211429..ec6d8b03aa 100644 --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -6,8 +6,8 @@ OpenTelemetry Python API .. toctree:: :maxdepth: 1 + baggage configuration context - correlationcontext metrics trace diff --git a/docs/api/baggage.propagation.rst b/docs/api/baggage.propagation.rst new file mode 100644 index 0000000000..7c8eba7940 --- /dev/null +++ b/docs/api/baggage.propagation.rst @@ -0,0 +1,7 @@ +opentelemetry.baggage.propagation package +==================================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.baggage.propagation diff --git a/docs/api/baggage.rst b/docs/api/baggage.rst new file mode 100644 index 0000000000..34712e78bd --- /dev/null +++ b/docs/api/baggage.rst @@ -0,0 +1,14 @@ +opentelemetry.baggage package +======================================== + +Subpackages +----------- + +.. toctree:: + + baggage.propagation + +Module contents +--------------- + +.. automodule:: opentelemetry.baggage diff --git a/docs/api/correlationcontext.propagation.rst b/docs/api/correlationcontext.propagation.rst deleted file mode 100644 index a9b94aa4fb..0000000000 --- a/docs/api/correlationcontext.propagation.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.correlationcontext.propagation package -==================================================== - -Module contents ---------------- - -.. automodule:: opentelemetry.correlationcontext.propagation diff --git a/docs/api/correlationcontext.rst b/docs/api/correlationcontext.rst deleted file mode 100644 index 10e7b2e573..0000000000 --- a/docs/api/correlationcontext.rst +++ /dev/null @@ -1,14 +0,0 @@ -opentelemetry.correlationcontext package -======================================== - -Subpackages ------------ - -.. toctree:: - - correlationcontext.propagation - -Module contents ---------------- - -.. automodule:: opentelemetry.correlationcontext diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 8c27ddfa4d..4d643eca62 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -174,7 +174,7 @@ Now run the above script, hit the root url (http://localhost:5000/) a few times, python flask_example.py -Configure Your HTTP Propagator (b3, CorrelationContext) +Configure Your HTTP Propagator (b3, Baggage) ------------------------------------------------------- A major feature of distributed tracing is the ability to correlate a trace across diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py index 90d7f0a30c..78672444bd 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py @@ -100,8 +100,8 @@ ) from opentelemetry import propagators +from opentelemetry.baggage import get_baggage, set_baggage from opentelemetry.context import Context, attach, detach, get_value, set_value -from opentelemetry.correlationcontext import get_correlation, set_correlation from opentelemetry.instrumentation.opentracing_shim import util from opentelemetry.instrumentation.opentracing_shim.version import __version__ from opentelemetry.trace import INVALID_SPAN_CONTEXT, DefaultSpan, Link @@ -290,7 +290,7 @@ def set_baggage_item(self, key: str, value: str): value: A tag value. """ # pylint: disable=protected-access - self._context._baggage = set_correlation( + self._context._baggage = set_baggage( key, value, context=self._context._baggage ) @@ -303,7 +303,7 @@ def get_baggage_item(self, key: str) -> Optional[object]: Returns this :class:`SpanShim` instance to allow call chaining. """ # pylint: disable=protected-access - return get_correlation(key, context=self._context._baggage) + return get_baggage(key, context=self._context._baggage) class ScopeShim(Scope): diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index a22f98585e..5ee9d96e97 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -12,6 +12,8 @@ ([#1034](https://github.com/open-telemetry/opentelemetry-python/pull/1034)) - Remove lazy Event and Link API from Span interface ([#1045](https://github.com/open-telemetry/opentelemetry-python/pull/1045)) +- Rename CorrelationContext to Baggage + ([#1060](https://github.com/open-telemetry/opentelemetry-python/pull/1060)) ## Version 0.12b0 diff --git a/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/__init__.py similarity index 63% rename from opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py rename to opentelemetry-api/src/opentelemetry/baggage/__init__.py index 8dbb357495..cdb2196f74 100644 --- a/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/__init__.py @@ -18,31 +18,31 @@ from opentelemetry.context import get_value, set_value from opentelemetry.context.context import Context -_CORRELATION_CONTEXT_KEY = "correlation-context" +_BAGGAGE_KEY = "baggage" -def get_correlations( +def get_all( context: typing.Optional[Context] = None, ) -> typing.Mapping[str, object]: - """Returns the name/value pairs in the CorrelationContext + """Returns the name/value pairs in the Baggage Args: context: The Context to use. If not set, uses current Context Returns: - Name/value pairs in the CorrelationContext + The name/value pairs in the Baggage """ - correlations = get_value(_CORRELATION_CONTEXT_KEY, context=context) - if isinstance(correlations, dict): - return MappingProxyType(correlations.copy()) + baggage = get_value(_BAGGAGE_KEY, context=context) + if isinstance(baggage, dict): + return MappingProxyType(baggage.copy()) return MappingProxyType({}) -def get_correlation( +def get_baggage( name: str, context: typing.Optional[Context] = None ) -> typing.Optional[object]: """Provides access to the value for a name/value pair in the - CorrelationContext + Baggage Args: name: The name of the value to retrieve @@ -52,13 +52,13 @@ def get_correlation( The value associated with the given name, or null if the given name is not present. """ - return get_correlations(context=context).get(name) + return get_all(context=context).get(name) -def set_correlation( +def set_baggage( name: str, value: object, context: typing.Optional[Context] = None ) -> Context: - """Sets a value in the CorrelationContext + """Sets a value in the Baggage Args: name: The name of the value to set @@ -68,15 +68,15 @@ def set_correlation( Returns: A Context with the value updated """ - correlations = dict(get_correlations(context=context)) - correlations[name] = value - return set_value(_CORRELATION_CONTEXT_KEY, correlations, context=context) + baggage = dict(get_all(context=context)) + baggage[name] = value + return set_value(_BAGGAGE_KEY, baggage, context=context) -def remove_correlation( +def remove_baggage( name: str, context: typing.Optional[Context] = None ) -> Context: - """Removes a value from the CorrelationContext + """Removes a value from the Baggage Args: name: The name of the value to remove @@ -85,19 +85,19 @@ def remove_correlation( Returns: A Context with the name/value removed """ - correlations = dict(get_correlations(context=context)) - correlations.pop(name, None) + baggage = dict(get_all(context=context)) + baggage.pop(name, None) - return set_value(_CORRELATION_CONTEXT_KEY, correlations, context=context) + return set_value(_BAGGAGE_KEY, baggage, context=context) -def clear_correlations(context: typing.Optional[Context] = None) -> Context: - """Removes all values from the CorrelationContext +def clear(context: typing.Optional[Context] = None) -> Context: + """Removes all values from the Baggage Args: context: The Context to use. If not set, uses current Context Returns: - A Context with all correlations removed + A Context with all baggage entries removed """ - return set_value(_CORRELATION_CONTEXT_KEY, {}, context=context) + return set_value(_BAGGAGE_KEY, {}, context=context) diff --git a/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py similarity index 68% rename from opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py rename to opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index 4032394ce7..4d0009892a 100644 --- a/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -15,17 +15,17 @@ import typing import urllib.parse -from opentelemetry import correlationcontext +from opentelemetry import baggage from opentelemetry.context import get_current from opentelemetry.context.context import Context from opentelemetry.trace.propagation import httptextformat -class CorrelationContextPropagator(httptextformat.HTTPTextFormat): +class BaggagePropagator(httptextformat.HTTPTextFormat): MAX_HEADER_LENGTH = 8192 MAX_PAIR_LENGTH = 4096 MAX_PAIRS = 180 - _CORRELATION_CONTEXT_HEADER_NAME = "otcorrelationcontext" + _BAGGAGE_HEADER_NAME = "otcorrelations" def extract( self, @@ -35,7 +35,7 @@ def extract( carrier: httptextformat.HTTPTextFormatT, context: typing.Optional[Context] = None, ) -> Context: - """Extract CorrelationContext from the carrier. + """Extract Baggage from the carrier. See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract` @@ -45,25 +45,25 @@ def extract( context = get_current() header = _extract_first_element( - get_from_carrier(carrier, self._CORRELATION_CONTEXT_HEADER_NAME) + get_from_carrier(carrier, self._BAGGAGE_HEADER_NAME) ) if not header or len(header) > self.MAX_HEADER_LENGTH: return context - correlations = header.split(",") - total_correlations = self.MAX_PAIRS - for correlation in correlations: - if total_correlations <= 0: + baggage_entries = header.split(",") + total_baggage_entries = self.MAX_PAIRS + for entry in baggage_entries: + if total_baggage_entries <= 0: return context - total_correlations -= 1 - if len(correlation) > self.MAX_PAIR_LENGTH: + total_baggage_entries -= 1 + if len(entry) > self.MAX_PAIR_LENGTH: continue try: - name, value = correlation.split("=", 1) + name, value = entry.split("=", 1) except Exception: # pylint: disable=broad-except continue - context = correlationcontext.set_correlation( + context = baggage.set_baggage( urllib.parse.unquote(name).strip(), urllib.parse.unquote(value).strip(), context=context, @@ -77,27 +77,25 @@ def inject( carrier: httptextformat.HTTPTextFormatT, context: typing.Optional[Context] = None, ) -> None: - """Injects CorrelationContext into the carrier. + """Injects Baggage into the carrier. See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` """ - correlations = correlationcontext.get_correlations(context=context) - if not correlations: + baggage_entries = baggage.get_all(context=context) + if not baggage_entries: return - correlation_context_string = _format_correlations(correlations) + baggage_string = _format_baggage(baggage_entries) set_in_carrier( - carrier, - self._CORRELATION_CONTEXT_HEADER_NAME, - correlation_context_string, + carrier, self._BAGGAGE_HEADER_NAME, baggage_string, ) -def _format_correlations(correlations: typing.Mapping[str, object]) -> str: +def _format_baggage(baggage_entries: typing.Mapping[str, object]) -> str: return ",".join( key + "=" + urllib.parse.quote_plus(str(value)) - for key, value in correlations.items() + for key, value in baggage_entries.items() ) diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index 5aa53e25dc..8f5a38df76 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -55,12 +55,8 @@ def example_route(): import typing -import opentelemetry.trace as trace -from opentelemetry.context import get_current +from opentelemetry.baggage.propagation import BaggagePropagator from opentelemetry.context.context import Context -from opentelemetry.correlationcontext.propagation import ( - CorrelationContextPropagator, -) from opentelemetry.propagators import composite from opentelemetry.trace.propagation import httptextformat from opentelemetry.trace.propagation.tracecontexthttptextformat import ( @@ -111,7 +107,7 @@ def inject( _HTTP_TEXT_FORMAT = composite.CompositeHTTPPropagator( - [TraceContextHTTPTextFormat(), CorrelationContextPropagator()], + [TraceContextHTTPTextFormat(), BaggagePropagator()], ) # type: httptextformat.HTTPTextFormat diff --git a/opentelemetry-api/tests/correlationcontext/test_baggage.py b/opentelemetry-api/tests/correlationcontext/test_baggage.py new file mode 100644 index 0000000000..276d2bc8b0 --- /dev/null +++ b/opentelemetry-api/tests/correlationcontext/test_baggage.py @@ -0,0 +1,69 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import baggage, context + + +class TestBaggageManager(unittest.TestCase): + def test_set_baggage(self): + self.assertEqual({}, baggage.get_all()) + + ctx = baggage.set_baggage("test", "value") + self.assertEqual(baggage.get_baggage("test", context=ctx), "value") + + ctx = baggage.set_baggage("test", "value2", context=ctx) + self.assertEqual(baggage.get_baggage("test", context=ctx), "value2") + + def test_baggages_current_context(self): + token = context.attach(baggage.set_baggage("test", "value")) + self.assertEqual(baggage.get_baggage("test"), "value") + context.detach(token) + self.assertEqual(baggage.get_baggage("test"), None) + + def test_set_multiple_baggage_entries(self): + ctx = baggage.set_baggage("test", "value") + ctx = baggage.set_baggage("test2", "value2", context=ctx) + self.assertEqual(baggage.get_baggage("test", context=ctx), "value") + self.assertEqual(baggage.get_baggage("test2", context=ctx), "value2") + self.assertEqual( + baggage.get_all(context=ctx), {"test": "value", "test2": "value2"}, + ) + + def test_modifying_baggage(self): + ctx = baggage.set_baggage("test", "value") + self.assertEqual(baggage.get_baggage("test", context=ctx), "value") + baggage_entries = baggage.get_all(context=ctx) + with self.assertRaises(TypeError): + baggage_entries["test"] = "mess-this-up" + self.assertEqual(baggage.get_baggage("test", context=ctx), "value") + + def test_remove_baggage_entry(self): + self.assertEqual({}, baggage.get_all()) + + ctx = baggage.set_baggage("test", "value") + ctx = baggage.set_baggage("test2", "value2", context=ctx) + ctx = baggage.remove_baggage("test", context=ctx) + self.assertEqual(baggage.get_baggage("test", context=ctx), None) + self.assertEqual(baggage.get_baggage("test2", context=ctx), "value2") + + def test_clear_baggage(self): + self.assertEqual({}, baggage.get_all()) + + ctx = baggage.set_baggage("test", "value") + self.assertEqual(baggage.get_baggage("test", context=ctx), "value") + + ctx = baggage.clear(context=ctx) + self.assertEqual(baggage.get_all(context=ctx), {}) diff --git a/opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py b/opentelemetry-api/tests/correlationcontext/test_baggage_propagation.py similarity index 80% rename from opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py rename to opentelemetry-api/tests/correlationcontext/test_baggage_propagation.py index c33326b173..e8bd45d065 100644 --- a/opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py +++ b/opentelemetry-api/tests/correlationcontext/test_baggage_propagation.py @@ -15,11 +15,9 @@ import typing import unittest -from opentelemetry import correlationcontext +from opentelemetry import baggage +from opentelemetry.baggage.propagation import BaggagePropagator from opentelemetry.context import get_current -from opentelemetry.correlationcontext.propagation import ( - CorrelationContextPropagator, -) def get_as_list( @@ -28,31 +26,29 @@ def get_as_list( return dict_object.get(key, []) -class TestCorrelationContextPropagation(unittest.TestCase): +class TestBaggagePropagation(unittest.TestCase): def setUp(self): - self.propagator = CorrelationContextPropagator() + self.propagator = BaggagePropagator() def _extract(self, header_value): """Test helper""" - header = {"otcorrelationcontext": [header_value]} - return correlationcontext.get_correlations( - self.propagator.extract(get_as_list, header) - ) + header = {"otcorrelations": [header_value]} + return baggage.get_all(self.propagator.extract(get_as_list, header)) def _inject(self, values): """Test helper""" ctx = get_current() for k, v in values.items(): - ctx = correlationcontext.set_correlation(k, v, context=ctx) + ctx = baggage.set_baggage(k, v, context=ctx) output = {} self.propagator.inject(dict.__setitem__, output, context=ctx) - return output.get("otcorrelationcontext") + return output.get("otcorrelations") def test_no_context_header(self): - correlations = correlationcontext.get_correlations( + baggage_entries = baggage.get_all( self.propagator.extract(get_as_list, {}) ) - self.assertEqual(correlations, {}) + self.assertEqual(baggage_entries, {}) def test_empty_context_header(self): header = "" @@ -94,7 +90,7 @@ def test_invalid_header(self): self.assertEqual(self._extract(header), expected) def test_header_too_long(self): - long_value = "s" * (CorrelationContextPropagator.MAX_HEADER_LENGTH + 1) + long_value = "s" * (BaggagePropagator.MAX_HEADER_LENGTH + 1) header = "key1={}".format(long_value) expected = {} self.assertEqual(self._extract(header), expected) @@ -103,20 +99,20 @@ def test_header_contains_too_many_entries(self): header = ",".join( [ "key{}=val".format(k) - for k in range(CorrelationContextPropagator.MAX_PAIRS + 1) + for k in range(BaggagePropagator.MAX_PAIRS + 1) ] ) self.assertEqual( - len(self._extract(header)), CorrelationContextPropagator.MAX_PAIRS + len(self._extract(header)), BaggagePropagator.MAX_PAIRS ) def test_header_contains_pair_too_long(self): - long_value = "s" * (CorrelationContextPropagator.MAX_PAIR_LENGTH + 1) + long_value = "s" * (BaggagePropagator.MAX_PAIR_LENGTH + 1) header = "key1=value1,key2={},key3=value3".format(long_value) expected = {"key1": "value1", "key3": "value3"} self.assertEqual(self._extract(header), expected) - def test_inject_no_correlations(self): + def test_inject_no_baggage_entries(self): values = {} output = self._inject(values) self.assertEqual(None, output) diff --git a/opentelemetry-api/tests/correlationcontext/test_correlation_context.py b/opentelemetry-api/tests/correlationcontext/test_correlation_context.py deleted file mode 100644 index aaa5d9fa92..0000000000 --- a/opentelemetry-api/tests/correlationcontext/test_correlation_context.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -from opentelemetry import context -from opentelemetry import correlationcontext as cctx - - -class TestCorrelationContextManager(unittest.TestCase): - def test_set_correlation(self): - self.assertEqual({}, cctx.get_correlations()) - - ctx = cctx.set_correlation("test", "value") - self.assertEqual(cctx.get_correlation("test", context=ctx), "value") - - ctx = cctx.set_correlation("test", "value2", context=ctx) - self.assertEqual(cctx.get_correlation("test", context=ctx), "value2") - - def test_correlations_current_context(self): - token = context.attach(cctx.set_correlation("test", "value")) - self.assertEqual(cctx.get_correlation("test"), "value") - context.detach(token) - self.assertEqual(cctx.get_correlation("test"), None) - - def test_set_multiple_correlations(self): - ctx = cctx.set_correlation("test", "value") - ctx = cctx.set_correlation("test2", "value2", context=ctx) - self.assertEqual(cctx.get_correlation("test", context=ctx), "value") - self.assertEqual(cctx.get_correlation("test2", context=ctx), "value2") - self.assertEqual( - cctx.get_correlations(context=ctx), - {"test": "value", "test2": "value2"}, - ) - - def test_modifying_correlations(self): - ctx = cctx.set_correlation("test", "value") - self.assertEqual(cctx.get_correlation("test", context=ctx), "value") - correlations = cctx.get_correlations(context=ctx) - with self.assertRaises(TypeError): - correlations["test"] = "mess-this-up" - self.assertEqual(cctx.get_correlation("test", context=ctx), "value") - - def test_remove_correlations(self): - self.assertEqual({}, cctx.get_correlations()) - - ctx = cctx.set_correlation("test", "value") - ctx = cctx.set_correlation("test2", "value2", context=ctx) - ctx = cctx.remove_correlation("test", context=ctx) - self.assertEqual(cctx.get_correlation("test", context=ctx), None) - self.assertEqual(cctx.get_correlation("test2", context=ctx), "value2") - - def test_clear_correlations(self): - self.assertEqual({}, cctx.get_correlations()) - - ctx = cctx.set_correlation("test", "value") - self.assertEqual(cctx.get_correlation("test", context=ctx), "value") - - ctx = cctx.clear_correlations(context=ctx) - self.assertEqual(cctx.get_correlations(context=ctx), {}) diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index a7e9430223..a1c58a4c3a 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -15,7 +15,7 @@ import typing import unittest -from opentelemetry import correlationcontext, trace +from opentelemetry import baggage, trace from opentelemetry.propagators import extract, inject from opentelemetry.trace import get_current_span, set_span_in_context @@ -41,28 +41,28 @@ def test_propagation(self): ) tracestate_value = "foo=1,bar=2,baz=3" headers = { - "otcorrelationcontext": ["key1=val1,key2=val2"], + "otcorrelations": ["key1=val1,key2=val2"], "traceparent": [traceparent_value], "tracestate": [tracestate_value], } ctx = extract(get_as_list, headers) - correlations = correlationcontext.get_correlations(context=ctx) + baggage_entries = baggage.get_all(context=ctx) expected = {"key1": "val1", "key2": "val2"} - self.assertEqual(correlations, expected) + self.assertEqual(baggage_entries, expected) span_context = get_current_span(context=ctx).get_context() self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) span = trace.DefaultSpan(span_context) - ctx = correlationcontext.set_correlation("key3", "val3") - ctx = correlationcontext.set_correlation("key4", "val4", context=ctx) + ctx = baggage.set_baggage("key3", "val3") + ctx = baggage.set_baggage("key4", "val4", context=ctx) ctx = set_span_in_context(span, context=ctx) output = {} inject(dict.__setitem__, output, context=ctx) self.assertEqual(traceparent_value, output["traceparent"]) - self.assertIn("key3=val3", output["otcorrelationcontext"]) - self.assertIn("key4=val4", output["otcorrelationcontext"]) + self.assertIn("key3=val3", output["otcorrelations"]) + self.assertIn("key4=val4", output["otcorrelations"]) self.assertIn("foo=1", output["tracestate"]) self.assertIn("bar=2", output["tracestate"]) self.assertIn("baz=3", output["tracestate"]) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 2af8a551ee..f3d31195f6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -132,7 +132,7 @@ class Metric(metrics_api.Metric): This is the class that is used to represent a metric that is to be synchronously recorded and tracked. Synchronous instruments are called inside a request, meaning they have an associated distributed context - (i.e. Span context, correlation context). Multiple metric events may occur + (i.e. Span context, baggage). Multiple metric events may occur for a synchronous instrument within a give collection interval. Each metric has a set of bound metrics that are created from the metric. From 224555871fcc76acc166888a44f81fcd1d1f6cbe Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Wed, 2 Sep 2020 20:26:16 +0200 Subject: [PATCH 0526/1517] Add support for db cursors and connections in context managers (#1028) Here is an example snippet that will not report tracing without this patch: with psycopg2.connect(...) as conn, conn.cursor() as cursor: cursor.execute("select 1;") Co-authored-by: Carl Bordum Hansen --- .../CHANGELOG.md | 5 ++- .../instrumentation/dbapi/__init__.py | 14 +++++++ .../tests/asyncpg/test_asyncpg_functional.py | 24 ++++++------ .../tests/celery/test_celery_functional.py | 3 -- .../tests/mysql/test_mysql_functional.py | 37 ++++++++++++++----- .../tests/postgres/test_aiopg_functional.py | 1 - .../tests/postgres/test_psycopg_functional.py | 19 +++++++++- .../tests/pymysql/test_pymysql_functional.py | 8 ++++ tox.ini | 2 +- 9 files changed, 85 insertions(+), 28 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md index bdb9236acb..99e1e09b55 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- bugfix: cursors and connections now produce spans when used with context managers + ([#1028](https://github.com/open-telemetry/opentelemetry-python/pull/1028)) + ## Version 0.12b0 Released 2020-08-14 @@ -19,4 +22,4 @@ Released 2020-05-12 Released 2020-02-21 -- Initial release \ No newline at end of file +- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py index 035c823bcf..551f71555a 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -294,6 +294,13 @@ def cursor(self, *args, **kwargs): self.__wrapped__.cursor(*args, **kwargs), db_api_integration ) + def __enter__(self): + self.__wrapped__.__enter__() + return self + + def __exit__(self, *args, **kwargs): + self.__wrapped__.__exit__(*args, **kwargs) + return TracedConnectionProxy(connection, *args, **kwargs) @@ -366,4 +373,11 @@ def callproc(self, *args, **kwargs): self.__wrapped__.callproc, *args, **kwargs ) + def __enter__(self): + self.__wrapped__.__enter__() + return self + + def __exit__(self, *args, **kwargs): + self.__wrapped__.__exit__(*args, **kwargs) + return TracedCursorProxy(cursor, *args, **kwargs) diff --git a/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py b/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py index 87382702f2..cb9080e62c 100644 --- a/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py +++ b/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py @@ -14,7 +14,7 @@ POSTGRES_USER = os.getenv("POSTGRESQL_HOST ", "testuser") -def _await(coro): +def async_call(coro): loop = asyncio.get_event_loop() return loop.run_until_complete(coro) @@ -27,7 +27,7 @@ def setUpClass(cls): cls._cursor = None cls._tracer = cls.tracer_provider.get_tracer(__name__) AsyncPGInstrumentor().instrument(tracer_provider=cls.tracer_provider) - cls._connection = _await( + cls._connection = async_call( asyncpg.connect( database=POSTGRES_DB_NAME, user=POSTGRES_USER, @@ -42,7 +42,7 @@ def tearDownClass(cls): AsyncPGInstrumentor().uninstrument() def test_instrumented_execute_method_without_arguments(self, *_, **__): - _await(self._connection.execute("SELECT 42;")) + async_call(self._connection.execute("SELECT 42;")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual( @@ -59,7 +59,7 @@ def test_instrumented_execute_method_without_arguments(self, *_, **__): ) def test_instrumented_fetch_method_without_arguments(self, *_, **__): - _await(self._connection.fetch("SELECT 42;")) + async_call(self._connection.fetch("SELECT 42;")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual( @@ -77,7 +77,7 @@ async def _transaction_execute(): async with self._connection.transaction(): await self._connection.execute("SELECT 42;") - _await(_transaction_execute()) + async_call(_transaction_execute()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(3, len(spans)) @@ -124,7 +124,7 @@ async def _transaction_execute(): await self._connection.execute("SELECT 42::uuid;") with self.assertRaises(asyncpg.CannotCoerceError): - _await(_transaction_execute()) + async_call(_transaction_execute()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(3, len(spans)) @@ -167,7 +167,7 @@ async def _transaction_execute(): ) def test_instrumented_method_doesnt_capture_parameters(self, *_, **__): - _await(self._connection.execute("SELECT $1;", "1")) + async_call(self._connection.execute("SELECT $1;", "1")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual( @@ -198,7 +198,7 @@ def setUpClass(cls): AsyncPGInstrumentor(capture_parameters=True).instrument( tracer_provider=cls.tracer_provider ) - cls._connection = _await( + cls._connection = async_call( asyncpg.connect( database=POSTGRES_DB_NAME, user=POSTGRES_USER, @@ -213,7 +213,7 @@ def tearDownClass(cls): AsyncPGInstrumentor().uninstrument() def test_instrumented_execute_method_with_arguments(self, *_, **__): - _await(self._connection.execute("SELECT $1;", "1")) + async_call(self._connection.execute("SELECT $1;", "1")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual( @@ -231,7 +231,7 @@ def test_instrumented_execute_method_with_arguments(self, *_, **__): ) def test_instrumented_fetch_method_with_arguments(self, *_, **__): - _await(self._connection.fetch("SELECT $1;", "1")) + async_call(self._connection.fetch("SELECT $1;", "1")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual( @@ -246,7 +246,7 @@ def test_instrumented_fetch_method_with_arguments(self, *_, **__): ) def test_instrumented_executemany_method_with_arguments(self, *_, **__): - _await(self._connection.executemany("SELECT $1;", [["1"], ["2"]])) + async_call(self._connection.executemany("SELECT $1;", [["1"], ["2"]])) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual( @@ -262,7 +262,7 @@ def test_instrumented_executemany_method_with_arguments(self, *_, **__): def test_instrumented_execute_interface_error_method(self, *_, **__): with self.assertRaises(asyncpg.InterfaceError): - _await(self._connection.execute("SELECT 42;", 1, 2, 3)) + async_call(self._connection.execute("SELECT 42;", 1, 2, 3)) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual( diff --git a/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py b/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py index 2714c8ee03..f18c6cdba1 100644 --- a/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py +++ b/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py @@ -13,11 +13,8 @@ # limitations under the License. -import time - import celery import pytest -from celery import signals from celery.exceptions import Retry import opentelemetry.instrumentation.celery diff --git a/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py b/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py index f2b07293bf..4116f4a19e 100644 --- a/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py +++ b/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py @@ -13,7 +13,6 @@ # limitations under the License. import os -import time import mysql.connector @@ -36,14 +35,6 @@ def setUpClass(cls): cls._cursor = None cls._tracer = cls.tracer_provider.get_tracer(__name__) MySQLInstrumentor().instrument() - cls._connection = mysql.connector.connect( - user=MYSQL_USER, - password=MYSQL_PASSWORD, - host=MYSQL_HOST, - port=MYSQL_PORT, - database=MYSQL_DB_NAME, - ) - cls._cursor = cls._connection.cursor() @classmethod def tearDownClass(cls): @@ -51,6 +42,17 @@ def tearDownClass(cls): cls._connection.close() MySQLInstrumentor().uninstrument() + def setUp(self): + super().setUp() + self._connection = mysql.connector.connect( + user=MYSQL_USER, + password=MYSQL_PASSWORD, + host=MYSQL_HOST, + port=MYSQL_PORT, + database=MYSQL_DB_NAME, + ) + self._cursor = self._connection.cursor() + def validate_spans(self): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) @@ -79,6 +81,23 @@ def test_execute(self): self._cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") self.validate_spans() + def test_execute_with_connection_context_manager(self): + """Should create a child span for execute with connection context + """ + with self._tracer.start_as_current_span("rootSpan"): + with self._connection as conn: + cursor = conn.cursor() + cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") + self.validate_spans() + + def test_execute_with_cursor_context_manager(self): + """Should create a child span for execute with cursor context + """ + with self._tracer.start_as_current_span("rootSpan"): + with self._connection.cursor() as cursor: + cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") + self.validate_spans() + def test_executemany(self): """Should create a child span for executemany """ diff --git a/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py b/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py index 1762da1d09..9eb209636d 100644 --- a/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py +++ b/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py @@ -13,7 +13,6 @@ # limitations under the License. import asyncio import os -import time import aiopg import psycopg2 diff --git a/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py b/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py index a8e07ddb27..8a703b0094 100644 --- a/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py +++ b/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py @@ -13,7 +13,6 @@ # limitations under the License. import os -import time import psycopg2 @@ -86,6 +85,24 @@ def test_execute(self): ) self.validate_spans() + def test_execute_with_connection_context_manager(self): + """Should create a child span for execute with connection context + """ + with self._tracer.start_as_current_span("rootSpan"): + with self._connection as conn: + cursor = conn.cursor() + cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") + self.validate_spans() + + def test_execute_with_cursor_context_manager(self): + """Should create a child span for execute with cursor context + """ + with self._tracer.start_as_current_span("rootSpan"): + with self._connection.cursor() as cursor: + cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") + self.validate_spans() + self.assertTrue(cursor.closed) + def test_executemany(self): """Should create a child span for executemany """ diff --git a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py index 1636f85fba..7b0cb5b0c0 100644 --- a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py +++ b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py @@ -78,6 +78,14 @@ def test_execute(self): self._cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") self.validate_spans() + def test_execute_with_cursor_context_manager(self): + """Should create a child span for execute with cursor context + """ + with self._tracer.start_as_current_span("rootSpan"): + with self._connection.cursor() as cursor: + cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") + self.validate_spans() + def test_executemany(self): """Should create a child span for executemany """ diff --git a/tox.ini b/tox.ini index 91024f7f3d..424b1d694e 100644 --- a/tox.ini +++ b/tox.ini @@ -430,4 +430,4 @@ commands = pytest {posargs} commands_post = - docker-compose down + docker-compose down -v From e45354c12894b6538ff4f081786afd20cc145ef8 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 2 Sep 2020 13:41:11 -0700 Subject: [PATCH 0527/1517] chore: fix intermittent build failures (#1067) This test was causing pypy3-core-getting-started jobs to fail intermittently. Removing the arbitrary sleep and replacing it with a retry strategy. --- docs/getting_started/tests/test_flask.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/getting_started/tests/test_flask.py b/docs/getting_started/tests/test_flask.py index 6ba296a5c0..4c409edbac 100644 --- a/docs/getting_started/tests/test_flask.py +++ b/docs/getting_started/tests/test_flask.py @@ -18,6 +18,8 @@ from time import sleep import requests +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry class TestFlask(unittest.TestCase): @@ -27,10 +29,13 @@ def test_flask(self): server = subprocess.Popen( [sys.executable, server_script], stdout=subprocess.PIPE, ) - sleep(1) + retry_strategy = Retry(total=10, backoff_factor=1) + adapter = HTTPAdapter(max_retries=retry_strategy) + http = requests.Session() + http.mount("http://", adapter) try: - result = requests.get("http://localhost:5000") + result = http.get("http://localhost:5000") self.assertEqual(result.status_code, 200) sleep(0.1) From 3bbab028f70cf84e2222e39e12777e8adb5fb517 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 3 Sep 2020 09:07:03 -0700 Subject: [PATCH 0528/1517] Attributes returned from SamplingResult must be immutable (#1066) --- .../setup.cfg | 1 + .../test_baggage.py | 0 .../test_baggage_propagation.py | 0 .../src/opentelemetry/sdk/trace/__init__.py | 2 +- .../src/opentelemetry/sdk/trace/sampling.py | 6 +-- .../tests/trace/test_sampling.py | 37 +++++++++++++++++++ 6 files changed, 42 insertions(+), 4 deletions(-) rename opentelemetry-api/tests/{correlationcontext => baggage}/test_baggage.py (100%) rename opentelemetry-api/tests/{correlationcontext => baggage}/test_baggage_propagation.py (100%) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg index 4a93873ce1..70c784c606 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg @@ -41,6 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.13dev0 + opentelemetry-sdk == 0.13dev0 psutil ~= 5.7.0 [options.extras_require] diff --git a/opentelemetry-api/tests/correlationcontext/test_baggage.py b/opentelemetry-api/tests/baggage/test_baggage.py similarity index 100% rename from opentelemetry-api/tests/correlationcontext/test_baggage.py rename to opentelemetry-api/tests/baggage/test_baggage.py diff --git a/opentelemetry-api/tests/correlationcontext/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py similarity index 100% rename from opentelemetry-api/tests/correlationcontext/test_baggage_propagation.py rename to opentelemetry-api/tests/baggage/test_baggage_propagation.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 95d04d8589..a082cffa7b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -772,7 +772,7 @@ def start_span( # pylint: disable=too-many-locals parent=parent_context, sampler=self.source.sampler, resource=self.source.resource, - attributes=sampling_result.attributes, + attributes=sampling_result.attributes.copy(), span_processor=self.source._active_span_processor, kind=kind, links=links, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index c723e18adb..2a1d6e89db 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -62,6 +62,7 @@ """ import abc import enum +from types import MappingProxyType from typing import Optional, Sequence # pylint: disable=unused-import @@ -102,11 +103,10 @@ def __init__( self, decision: Decision, attributes: Attributes = None, ) -> None: self.decision = decision - # TODO: attributes must be immutable if attributes is None: - self.attributes = {} + self.attributes = MappingProxyType({}) else: - self.attributes = attributes + self.attributes = MappingProxyType(attributes) class Sampler(abc.ABC): diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index 2c03f0bf73..de1c019fb5 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -22,6 +22,43 @@ TO_SAMPLED = trace.TraceFlags(trace.TraceFlags.SAMPLED) +class TestDecision(unittest.TestCase): + def test_is_recording(self): + self.assertTrue( + sampling.Decision.is_recording(sampling.Decision.RECORD) + ) + self.assertTrue( + sampling.Decision.is_recording( + sampling.Decision.RECORD_AND_SAMPLED + ) + ) + self.assertFalse( + sampling.Decision.is_recording(sampling.Decision.NOT_RECORD) + ) + + def test_is_sampled(self): + self.assertFalse( + sampling.Decision.is_sampled(sampling.Decision.RECORD) + ) + self.assertTrue( + sampling.Decision.is_sampled(sampling.Decision.RECORD_AND_SAMPLED) + ) + self.assertFalse( + sampling.Decision.is_sampled(sampling.Decision.NOT_RECORD) + ) + + +class TestSamplingResult(unittest.TestCase): + def test_ctr(self): + attributes = {"asd": "test"} + result = sampling.SamplingResult(sampling.Decision.RECORD, attributes) + self.assertIs(result.decision, sampling.Decision.RECORD) + with self.assertRaises(TypeError): + result.attributes["test"] = "mess-this-up" + self.assertTrue(len(result.attributes), 1) + self.assertEqual(result.attributes["asd"], "test") + + class TestSampler(unittest.TestCase): def test_always_on(self): no_record_always_on = sampling.ALWAYS_ON.should_sample( From 899274102506414d64df603a576d81bb241b377d Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 3 Sep 2020 16:02:02 -0700 Subject: [PATCH 0529/1517] Populate resource attributes as per semantic conventions (#1053) As per semantic conventions, set the `telemetry.sdk` parameters. --- .../tests/test_jaeger_exporter.py | 2 ++ .../tests/test_zipkin_exporter.py | 3 +++ opentelemetry-sdk/CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/metrics/__init__.py | 2 +- .../opentelemetry/sdk/resources/__init__.py | 22 ++++++++++++++++-- .../src/opentelemetry/sdk/trace/__init__.py | 4 ++-- .../tests/metrics/test_metrics.py | 2 +- .../tests/resources/test_resources.py | 23 ++++++++++++++++--- opentelemetry-sdk/tests/trace/test_trace.py | 5 ++-- tox.ini | 4 ++++ 10 files changed, 58 insertions(+), 11 deletions(-) diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py index a8809f88aa..f2717a8bcf 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py @@ -215,9 +215,11 @@ def test_translate_to_jaeger(self): otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) + otel_spans[1].resource = Resource({}) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) + otel_spans[2].resource = Resource({}) otel_spans[2].end(end_time=end_times[2]) # pylint: disable=protected-access diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index f6e24a1495..220acb8fa4 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -165,6 +165,7 @@ def test_export(self): ] otel_spans[0].start(start_time=start_times[0]) + otel_spans[0].resource = Resource({}) # added here to preserve order otel_spans[0].set_attribute("key_bool", False) otel_spans[0].set_attribute("key_string", "hello_world") @@ -185,6 +186,7 @@ def test_export(self): otel_spans[2].end(end_time=end_times[2]) otel_spans[3].start(start_time=start_times[3]) + otel_spans[3].resource = Resource({}) otel_spans[3].end(end_time=end_times[3]) service_name = "test-service" @@ -295,6 +297,7 @@ def test_zero_padding(self): ) otel_span.start(start_time=start_time) + otel_span.resource = Resource({}) otel_span.end(end_time=end_time) service_name = "test-service" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index acd1ce1d7d..60116ec731 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -8,6 +8,8 @@ ([#1034](https://github.com/open-telemetry/opentelemetry-python/pull/1034)) - Remove lazy Event and Link API from Span interface ([#1045](https://github.com/open-telemetry/opentelemetry-python/pull/1045)) +- Populate resource attributes as per semantic conventions + ([#1053](https://github.com/open-telemetry/opentelemetry-python/pull/1053)) ## Version 0.12b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index f3d31195f6..092f456faf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -478,7 +478,7 @@ class MeterProvider(metrics_api.MeterProvider): def __init__( self, stateful=True, - resource: Resource = Resource.create_empty(), + resource: Resource = Resource.create({}), shutdown_on_exit: bool = True, ): self.stateful = stateful diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index d9752e3b3c..3053a18867 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -19,11 +19,22 @@ import typing from json import dumps +import pkg_resources + LabelValue = typing.Union[str, bool, int, float] Labels = typing.Dict[str, LabelValue] logger = logging.getLogger(__name__) +TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language" +TELEMETRY_SDK_NAME = "telemetry.sdk.name" +TELEMETRY_SDK_VERSION = "telemetry.sdk.version" + +OPENTELEMETRY_SDK_VERSION = pkg_resources.get_distribution( + "opentelemetry-sdk" +).version + + class Resource: def __init__(self, labels: Labels): self._labels = labels.copy() @@ -31,8 +42,8 @@ def __init__(self, labels: Labels): @staticmethod def create(labels: Labels) -> "Resource": if not labels: - return _EMPTY_RESOURCE - return Resource(labels) + return _DEFAULT_RESOURCE + return _DEFAULT_RESOURCE.merge(Resource(labels)) @staticmethod def create_empty() -> "Resource": @@ -60,6 +71,13 @@ def __hash__(self): _EMPTY_RESOURCE = Resource({}) +_DEFAULT_RESOURCE = Resource( + { + TELEMETRY_SDK_LANGUAGE: "python", + TELEMETRY_SDK_NAME: "opentelemetry", + TELEMETRY_SDK_VERSION: OPENTELEMETRY_SDK_VERSION, + } +) class ResourceDetector(abc.ABC): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index a082cffa7b..e42f4ceb0b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -364,7 +364,7 @@ def __init__( parent: Optional[trace_api.SpanContext] = None, sampler: Optional[sampling.Sampler] = None, trace_config: None = None, # TODO - resource: Resource = Resource.create_empty(), + resource: Resource = Resource.create({}), attributes: types.Attributes = None, # TODO events: Sequence[Event] = None, # TODO links: Sequence[trace_api.Link] = (), @@ -821,7 +821,7 @@ class TracerProvider(trace_api.TracerProvider): def __init__( self, sampler: sampling.Sampler = sampling.DEFAULT_ON, - resource: Resource = Resource.create_empty(), + resource: Resource = Resource.create({}), shutdown_on_exit: bool = True, active_span_processor: Union[ SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index b854f2d5db..0197476520 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -40,7 +40,7 @@ def test_resource_empty(self): meter_provider = metrics.MeterProvider() meter = meter_provider.get_meter(__name__) # pylint: disable=protected-access - self.assertIs(meter.resource, resources._EMPTY_RESOURCE) + self.assertIs(meter.resource, resources._DEFAULT_RESOURCE) def test_start_pipeline(self): exporter = mock.Mock() diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 84d0cf2ae5..be677f19fc 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -30,18 +30,28 @@ def test_create(self): "cost": 112.12, } + expected_labels = { + "service": "ui", + "version": 1, + "has_bugs": True, + "cost": 112.12, + resources.TELEMETRY_SDK_NAME: "opentelemetry", + resources.TELEMETRY_SDK_LANGUAGE: "python", + resources.TELEMETRY_SDK_VERSION: resources.OPENTELEMETRY_SDK_VERSION, + } + resource = resources.Resource.create(labels) self.assertIsInstance(resource, resources.Resource) - self.assertEqual(resource.labels, labels) + self.assertEqual(resource.labels, expected_labels) resource = resources.Resource.create_empty() self.assertIs(resource, resources._EMPTY_RESOURCE) resource = resources.Resource.create(None) - self.assertIs(resource, resources._EMPTY_RESOURCE) + self.assertIs(resource, resources._DEFAULT_RESOURCE) resource = resources.Resource.create({}) - self.assertIs(resource, resources._EMPTY_RESOURCE) + self.assertIs(resource, resources._DEFAULT_RESOURCE) def test_resource_merge(self): left = resources.Resource({"service": "ui"}) @@ -75,7 +85,14 @@ def test_immutability(self): "cost": 112.12, } + default_labels = { + resources.TELEMETRY_SDK_NAME: "opentelemetry", + resources.TELEMETRY_SDK_LANGUAGE: "python", + resources.TELEMETRY_SDK_VERSION: resources.OPENTELEMETRY_SDK_VERSION, + } + labels_copy = labels.copy() + labels_copy.update(default_labels) resource = resources.Resource.create(labels) self.assertEqual(resource.labels, labels_copy) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 1c272048ca..a6b4fa93e9 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -20,7 +20,7 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import resources, trace -from opentelemetry.sdk.trace import sampling +from opentelemetry.sdk.trace import Resource, sampling from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace.status import StatusCanonicalCode from opentelemetry.util import time_ns @@ -396,7 +396,7 @@ def test_default_span_resource(self): tracer = tracer_provider.get_tracer(__name__) span = tracer.start_span("root") # pylint: disable=protected-access - self.assertIs(span.resource, resources._EMPTY_RESOURCE) + self.assertIs(span.resource, resources._DEFAULT_RESOURCE) def test_span_context_remote_flag(self): tracer = new_tracer() @@ -922,6 +922,7 @@ def test_to_json(self): trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) span = trace.Span("span-name", context) + span.resource = Resource({}) self.assertEqual( span.to_json(), diff --git a/tox.ini b/tox.ini index 424b1d694e..0fbaabce83 100644 --- a/tox.ini +++ b/tox.ini @@ -365,6 +365,10 @@ deps = -c {toxinidir}/dev-requirements.txt -r {toxinidir}/docs-requirements.txt +commands_pre = + pip install -e {toxinidir}/opentelemetry-api \ + -e {toxinidir}/opentelemetry-sdk + changedir = docs commands = From 8b3479acd9944cd892cdc53c9f5d46206b288fdc Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 3 Sep 2020 17:44:01 -0600 Subject: [PATCH 0530/1517] Add missing underscore (#1073) Fixes #1072 --- .../src/opentelemetry/exporter/otlp/exporter.py | 2 +- .../tests/test_otlp_trace_exporter.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index 0ce7ef6617..e0914f262f 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -186,7 +186,7 @@ def _export(self, data): if error.code() == StatusCode.OK: return self._result.SUCCESS - return self.result.FAILURE + return self._result.FAILURE return self._result.FAILURE diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index b0ec8e4517..a7f572323e 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -87,6 +87,14 @@ def Export(self, request, context): return ExportTraceServiceResponse() +class TraceServiceServicerALREADY_EXISTS(TraceServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.ALREADY_EXISTS) + + return ExportTraceServiceResponse() + + class TestOTLPSpanExporter(TestCase): def setUp(self): tracer_provider = TracerProvider() @@ -178,6 +186,14 @@ def test_success(self): self.exporter.export([self.span]), SpanExportResult.SUCCESS ) + def test_failure(self): + add_TraceServiceServicer_to_server( + TraceServiceServicerALREADY_EXISTS(), self.server + ) + self.assertEqual( + self.exporter.export([self.span]), SpanExportResult.FAILURE + ) + def test_translate_spans(self): expected = ExportTraceServiceRequest( From 74ed13cfb8993fc73ddb004f21fb843297726256 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 3 Sep 2020 18:19:13 -0600 Subject: [PATCH 0531/1517] instrumentation/system-metrics: Update System Metrics (#1019) Fixes #1006 --- .../system_metrics/__init__.py | 624 ++++++++++++--- .../tests/test_system_metrics.py | 712 +++++++++++++++--- 2 files changed, 1146 insertions(+), 190 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py index fcd96f8210..9ca36d00b2 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py @@ -16,12 +16,27 @@ process (CPU, memory, garbage collection) metrics. By default, the following metrics are configured: -"system_memory": ["total", "available", "used", "free"], -"system_cpu": ["user", "system", "idle"], -"network_bytes": ["bytes_recv", "bytes_sent"], -"runtime_memory": ["rss", "vms"], -"runtime_cpu": ["user", "system"], +.. code:: python + { + "system.cpu.time": ["idle", "user", "system", "irq"], + "system.cpu.utilization": ["idle", "user", "system", "irq"], + "system.memory.usage": ["used", "free", "cached"], + "system.memory.utilization": ["used", "free", "cached"], + "system.swap.usage": ["used", "free"], + "system.swap.utilization": ["used", "free"], + "system.disk.io": ["read", "write"], + "system.disk.operations": ["read", "write"], + "system.disk.time": ["read", "write"], + "system.disk.merged": ["read", "write"], + "system.network.dropped.packets": ["transmit", "receive"], + "system.network.packets": ["transmit", "receive"], + "system.network.errors": ["transmit", "receive"], + "system.network.io": ["trasmit", "receive"], + "system.network.connections": ["family", "type"], + "runtime.memory": ["rss", "vms"], + "runtime.cpu.time": ["user", "system"], + } Usage ----- @@ -42,11 +57,11 @@ # to configure custom metrics configuration = { - "system_memory": ["total", "available", "used", "free", "active", "inactive", "wired"], - "system_cpu": ["user", "nice", "system", "idle"], - "network_bytes": ["bytes_recv", "bytes_sent"], - "runtime_memory": ["rss", "vms"], - "runtime_cpu": ["user", "system"], + "system.memory.usage": ["used", "free", "cached"], + "system.cpu.time": ["idle", "user", "system", "irq"], + "system.network.io": ["trasmit", "receive"], + "runtime.memory": ["rss", "vms"], + "runtime.cpu.time": ["user", "system"], } SystemMetrics(exporter, config=configuration) @@ -57,16 +72,23 @@ import gc import os import typing +from platform import python_implementation import psutil from opentelemetry import metrics -from opentelemetry.sdk.metrics import ValueObserver +from opentelemetry.sdk.metrics import ( + SumObserver, + UpDownSumObserver, + ValueObserver, +) from opentelemetry.sdk.metrics.export import MetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController +from opentelemetry.sdk.util import get_dict_as_key class SystemMetrics: + # pylint: disable=too-many-statements def __init__( self, exporter: MetricsExporter, @@ -79,157 +101,591 @@ def __init__( self.controller = PushController( meter=self.meter, exporter=exporter, interval=interval ) + self._python_implementation = python_implementation().lower() if config is None: self._config = { - "system_memory": ["total", "available", "used", "free"], - "system_cpu": ["user", "system", "idle"], - "network_bytes": ["bytes_recv", "bytes_sent"], - "runtime_memory": ["rss", "vms"], - "runtime_cpu": ["user", "system"], + "system.cpu.time": ["idle", "user", "system", "irq"], + "system.cpu.utilization": ["idle", "user", "system", "irq"], + "system.memory.usage": ["used", "free", "cached"], + "system.memory.utilization": ["used", "free", "cached"], + "system.swap.usage": ["used", "free"], + "system.swap.utilization": ["used", "free"], + # system.swap.page.faults: [], + # system.swap.page.operations: [], + "system.disk.io": ["read", "write"], + "system.disk.operations": ["read", "write"], + "system.disk.time": ["read", "write"], + "system.disk.merged": ["read", "write"], + # "system.filesystem.usage": [], + # "system.filesystem.utilization": [], + "system.network.dropped.packets": ["transmit", "receive"], + "system.network.packets": ["transmit", "receive"], + "system.network.errors": ["transmit", "receive"], + "system.network.io": ["trasmit", "receive"], + "system.network.connections": ["family", "type"], + "runtime.memory": ["rss", "vms"], + "runtime.cpu.time": ["user", "system"], } else: self._config = config + self._proc = psutil.Process(os.getpid()) - self._system_memory_labels = {} - self._system_cpu_labels = {} - self._network_bytes_labels = {} - self._runtime_memory_labels = {} - self._runtime_cpu_labels = {} - self._runtime_gc_labels = {} - # create the label set for each observer once - for key, value in self._labels.items(): - self._system_memory_labels[key] = value - self._system_cpu_labels[key] = value - self._network_bytes_labels[key] = value - self._runtime_memory_labels[key] = value - self._runtime_gc_labels[key] = value - - self.meter.register_observer( - callback=self._get_system_memory, - name="system.mem", - description="System memory", + + self._system_cpu_time_labels = self._labels.copy() + self._system_cpu_utilization_labels = self._labels.copy() + + self._system_memory_usage_labels = self._labels.copy() + self._system_memory_utilization_labels = self._labels.copy() + + self._system_swap_usage_labels = self._labels.copy() + self._system_swap_utilization_labels = self._labels.copy() + # self._system_swap_page_faults = self._labels.copy() + # self._system_swap_page_operations = self._labels.copy() + + self._system_disk_io_labels = self._labels.copy() + self._system_disk_operations_labels = self._labels.copy() + self._system_disk_time_labels = self._labels.copy() + self._system_disk_merged_labels = self._labels.copy() + + # self._system_filesystem_usage_labels = self._labels.copy() + # self._system_filesystem_utilization_labels = self._labels.copy() + + self._system_network_dropped_packets_labels = self._labels.copy() + self._system_network_packets_labels = self._labels.copy() + self._system_network_errors_labels = self._labels.copy() + self._system_network_io_labels = self._labels.copy() + self._system_network_connections_labels = self._labels.copy() + + self._runtime_memory_labels = self._labels.copy() + self._runtime_cpu_time_labels = self._labels.copy() + self._runtime_gc_count_labels = self._labels.copy() + + self.meter.register_observer( + callback=self._get_system_cpu_time, + name="system.cpu.time", + description="System CPU time", + unit="seconds", + value_type=float, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_cpu_utilization, + name="system.cpu.utilization", + description="System CPU utilization", + unit="1", + value_type=float, + observer_type=ValueObserver, + ) + + self.meter.register_observer( + callback=self._get_system_memory_usage, + name="system.memory.usage", + description="System memory usage", unit="bytes", value_type=int, observer_type=ValueObserver, ) self.meter.register_observer( - callback=self._get_system_cpu, - name="system.cpu", - description="System CPU", - unit="seconds", + callback=self._get_system_memory_utilization, + name="system.memory.utilization", + description="System memory utilization", + unit="1", value_type=float, observer_type=ValueObserver, ) self.meter.register_observer( - callback=self._get_network_bytes, - name="system.net.bytes", - description="System network bytes", - unit="bytes", + callback=self._get_system_swap_usage, + name="system.swap.usage", + description="System swap usage", + unit="pages", value_type=int, observer_type=ValueObserver, ) + self.meter.register_observer( + callback=self._get_system_swap_utilization, + name="system.swap.utilization", + description="System swap utilization", + unit="1", + value_type=float, + observer_type=ValueObserver, + ) + + # self.meter.register_observer( + # callback=self._get_system_swap_page_faults, + # name="system.swap.page_faults", + # description="System swap page faults", + # unit="faults", + # value_type=int, + # observer_type=SumObserver, + # ) + + # self.meter.register_observer( + # callback=self._get_system_swap_page_operations, + # name="system.swap.page_operations", + # description="System swap page operations", + # unit="operations", + # value_type=int, + # observer_type=SumObserver, + # ) + + self.meter.register_observer( + callback=self._get_system_disk_io, + name="system.disk.io", + description="System disk IO", + unit="bytes", + value_type=int, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_disk_operations, + name="system.disk.operations", + description="System disk operations", + unit="operations", + value_type=int, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_disk_time, + name="system.disk.time", + description="System disk time", + unit="seconds", + value_type=float, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_disk_merged, + name="system.disk.merged", + description="System disk merged", + unit="1", + value_type=int, + observer_type=SumObserver, + ) + + # self.meter.register_observer( + # callback=self._get_system_filesystem_usage, + # name="system.filesystem.usage", + # description="System filesystem usage", + # unit="bytes", + # value_type=int, + # observer_type=ValueObserver, + # ) + + # self.meter.register_observer( + # callback=self._get_system_filesystem_utilization, + # name="system.filesystem.utilization", + # description="System filesystem utilization", + # unit="1", + # value_type=float, + # observer_type=ValueObserver, + # ) + + self.meter.register_observer( + callback=self._get_system_network_dropped_packets, + name="system.network.dropped_packets", + description="System network dropped_packets", + unit="packets", + value_type=int, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_network_packets, + name="system.network.packets", + description="System network packets", + unit="packets", + value_type=int, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_network_errors, + name="system.network.errors", + description="System network errors", + unit="errors", + value_type=int, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_network_io, + name="system.network.io", + description="System network io", + unit="bytes", + value_type=int, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_network_connections, + name="system.network.connections", + description="System network connections", + unit="connections", + value_type=int, + observer_type=UpDownSumObserver, + ) + self.meter.register_observer( callback=self._get_runtime_memory, - name="runtime.python.mem", - description="Runtime memory", + name="runtime.{}.memory".format(self._python_implementation), + description="Runtime {} memory".format( + self._python_implementation + ), unit="bytes", value_type=int, - observer_type=ValueObserver, + observer_type=SumObserver, ) self.meter.register_observer( - callback=self._get_runtime_cpu, - name="runtime.python.cpu", - description="Runtime CPU", + callback=self._get_runtime_cpu_time, + name="runtime.{}.cpu_time".format(self._python_implementation), + description="Runtime {} CPU time".format( + self._python_implementation + ), unit="seconds", value_type=float, - observer_type=ValueObserver, + observer_type=SumObserver, ) self.meter.register_observer( callback=self._get_runtime_gc_count, - name="runtime.python.gc.count", - description="Runtime: gc objects", - unit="objects", + name="runtime.{}.gc_count".format(self._python_implementation), + description="Runtime {} GC count".format( + self._python_implementation + ), + unit="bytes", value_type=int, - observer_type=ValueObserver, + observer_type=SumObserver, ) - def _get_system_memory(self, observer: metrics.ValueObserver) -> None: - """Observer callback for memory available + def _get_system_cpu_time(self, observer: metrics.ValueObserver) -> None: + """Observer callback for system CPU time + + Args: + observer: the observer to update + """ + for cpu, times in enumerate(psutil.cpu_times(percpu=True)): + for metric in self._config["system.cpu.time"]: + self._system_cpu_time_labels["state"] = metric + self._system_cpu_time_labels["cpu"] = cpu + 1 + observer.observe( + getattr(times, metric), self._system_cpu_time_labels + ) + + def _get_system_cpu_utilization( + self, observer: metrics.ValueObserver + ) -> None: + """Observer callback for system CPU utilization + + Args: + observer: the observer to update + """ + + for cpu, times_percent in enumerate( + psutil.cpu_times_percent(percpu=True) + ): + for metric in self._config["system.cpu.utilization"]: + self._system_cpu_utilization_labels["state"] = metric + self._system_cpu_utilization_labels["cpu"] = cpu + 1 + observer.observe( + getattr(times_percent, metric) / 100, + self._system_cpu_utilization_labels, + ) + + def _get_system_memory_usage( + self, observer: metrics.ValueObserver + ) -> None: + """Observer callback for memory usage + + Args: + observer: the observer to update + """ + virtual_memory = psutil.virtual_memory() + for metric in self._config["system.memory.usage"]: + self._system_memory_usage_labels["state"] = metric + observer.observe( + getattr(virtual_memory, metric), + self._system_memory_usage_labels, + ) + + def _get_system_memory_utilization( + self, observer: metrics.ValueObserver + ) -> None: + """Observer callback for memory utilization Args: observer: the observer to update """ system_memory = psutil.virtual_memory() - for metric in self._config["system_memory"]: - self._system_memory_labels["type"] = metric + + for metric in self._config["system.memory.utilization"]: + self._system_memory_utilization_labels["state"] = metric + observer.observe( + getattr(system_memory, metric) / system_memory.total, + self._system_memory_utilization_labels, + ) + + def _get_system_swap_usage(self, observer: metrics.ValueObserver) -> None: + """Observer callback for swap usage + + Args: + observer: the observer to update + """ + system_swap = psutil.swap_memory() + + for metric in self._config["system.swap.usage"]: + self._system_swap_usage_labels["state"] = metric observer.observe( - getattr(system_memory, metric), self._system_memory_labels + getattr(system_swap, metric), self._system_swap_usage_labels ) - def _get_system_cpu(self, observer: metrics.ValueObserver) -> None: - """Observer callback for system cpu + def _get_system_swap_utilization( + self, observer: metrics.ValueObserver + ) -> None: + """Observer callback for swap utilization Args: observer: the observer to update """ - cpu_times = psutil.cpu_times() - for _type in self._config["system_cpu"]: - self._system_cpu_labels["type"] = _type + system_swap = psutil.swap_memory() + + for metric in self._config["system.swap.utilization"]: + self._system_swap_utilization_labels["state"] = metric observer.observe( - getattr(cpu_times, _type), self._system_cpu_labels + getattr(system_swap, metric) / system_swap.total, + self._system_swap_utilization_labels, ) - def _get_network_bytes(self, observer: metrics.ValueObserver) -> None: - """Observer callback for network bytes + # TODO Add _get_system_swap_page_faults + # TODO Add _get_system_swap_page_operations + + def _get_system_disk_io(self, observer: metrics.SumObserver) -> None: + """Observer callback for disk IO Args: observer: the observer to update """ - net_io = psutil.net_io_counters() - for _type in self._config["network_bytes"]: - self._network_bytes_labels["type"] = _type + for device, counters in psutil.disk_io_counters(perdisk=True).items(): + for metric in self._config["system.disk.io"]: + self._system_disk_io_labels["device"] = device + self._system_disk_io_labels["direction"] = metric + observer.observe( + getattr(counters, "{}_bytes".format(metric)), + self._system_disk_io_labels, + ) + + def _get_system_disk_operations( + self, observer: metrics.SumObserver + ) -> None: + """Observer callback for disk operations + + Args: + observer: the observer to update + """ + for device, counters in psutil.disk_io_counters(perdisk=True).items(): + for metric in self._config["system.disk.operations"]: + self._system_disk_operations_labels["device"] = device + self._system_disk_operations_labels["direction"] = metric + observer.observe( + getattr(counters, "{}_count".format(metric)), + self._system_disk_operations_labels, + ) + + def _get_system_disk_time(self, observer: metrics.SumObserver) -> None: + """Observer callback for disk time + + Args: + observer: the observer to update + """ + for device, counters in psutil.disk_io_counters(perdisk=True).items(): + for metric in self._config["system.disk.time"]: + self._system_disk_time_labels["device"] = device + self._system_disk_time_labels["direction"] = metric + observer.observe( + getattr(counters, "{}_time".format(metric)) / 1000, + self._system_disk_time_labels, + ) + + def _get_system_disk_merged(self, observer: metrics.SumObserver) -> None: + """Observer callback for disk merged operations + + Args: + observer: the observer to update + """ + + # FIXME The units in the spec is 1, it seems like it should be + # operations or the value type should be Double + + for device, counters in psutil.disk_io_counters(perdisk=True).items(): + for metric in self._config["system.disk.time"]: + self._system_disk_merged_labels["device"] = device + self._system_disk_merged_labels["direction"] = metric + observer.observe( + getattr(counters, "{}_merged_count".format(metric)), + self._system_disk_merged_labels, + ) + + # TODO Add _get_system_filesystem_usage + # TODO Add _get_system_filesystem_utilization + # TODO Filesystem information can be obtained with os.statvfs in Unix-like + # OSs, how to do the same in Windows? + + def _get_system_network_dropped_packets( + self, observer: metrics.SumObserver + ) -> None: + """Observer callback for network dropped packets + + Args: + observer: the observer to update + """ + + for device, counters in psutil.net_io_counters(pernic=True).items(): + for metric in self._config["system.network.dropped.packets"]: + in_out = {"receive": "in", "transmit": "out"}[metric] + self._system_network_dropped_packets_labels["device"] = device + self._system_network_dropped_packets_labels[ + "direction" + ] = metric + observer.observe( + getattr(counters, "drop{}".format(in_out)), + self._system_network_dropped_packets_labels, + ) + + def _get_system_network_packets( + self, observer: metrics.SumObserver + ) -> None: + """Observer callback for network packets + + Args: + observer: the observer to update + """ + + for device, counters in psutil.net_io_counters(pernic=True).items(): + for metric in self._config["system.network.dropped.packets"]: + recv_sent = {"receive": "recv", "transmit": "sent"}[metric] + self._system_network_packets_labels["device"] = device + self._system_network_packets_labels["direction"] = metric + observer.observe( + getattr(counters, "packets_{}".format(recv_sent)), + self._system_network_packets_labels, + ) + + def _get_system_network_errors( + self, observer: metrics.SumObserver + ) -> None: + """Observer callback for network errors + + Args: + observer: the observer to update + """ + for device, counters in psutil.net_io_counters(pernic=True).items(): + for metric in self._config["system.network.errors"]: + in_out = {"receive": "in", "transmit": "out"}[metric] + self._system_network_errors_labels["device"] = device + self._system_network_errors_labels["direction"] = metric + observer.observe( + getattr(counters, "err{}".format(in_out)), + self._system_network_errors_labels, + ) + + def _get_system_network_io(self, observer: metrics.SumObserver) -> None: + """Observer callback for network IO + + Args: + observer: the observer to update + """ + + for device, counters in psutil.net_io_counters(pernic=True).items(): + for metric in self._config["system.network.dropped.packets"]: + recv_sent = {"receive": "recv", "transmit": "sent"}[metric] + self._system_network_io_labels["device"] = device + self._system_network_io_labels["direction"] = metric + observer.observe( + getattr(counters, "bytes_{}".format(recv_sent)), + self._system_network_io_labels, + ) + + def _get_system_network_connections( + self, observer: metrics.UpDownSumObserver + ) -> None: + """Observer callback for network connections + + Args: + observer: the observer to update + """ + # TODO How to find the device identifier for a particular + # connection? + + connection_counters = {} + + for net_connection in psutil.net_connections(): + for metric in self._config["system.network.connections"]: + self._system_network_connections_labels["protocol"] = { + 1: "tcp", + 2: "udp", + }[net_connection.type.value] + self._system_network_connections_labels[ + "state" + ] = net_connection.status + self._system_network_connections_labels[metric] = getattr( + net_connection, metric + ) + + connection_counters_key = get_dict_as_key( + self._system_network_connections_labels + ) + + if connection_counters_key in connection_counters.keys(): + connection_counters[connection_counters_key]["counter"] += 1 + else: + connection_counters[connection_counters_key] = { + "counter": 1, + "labels": self._system_network_connections_labels.copy(), + } + + for connection_counter in connection_counters.values(): observer.observe( - getattr(net_io, _type), self._network_bytes_labels + connection_counter["counter"], connection_counter["labels"], ) - def _get_runtime_memory(self, observer: metrics.ValueObserver) -> None: + def _get_runtime_memory(self, observer: metrics.SumObserver) -> None: """Observer callback for runtime memory Args: observer: the observer to update """ proc_memory = self._proc.memory_info() - for _type in self._config["runtime_memory"]: - self._runtime_memory_labels["type"] = _type + for metric in self._config["runtime.memory"]: + self._runtime_memory_labels["type"] = metric observer.observe( - getattr(proc_memory, _type), self._runtime_memory_labels + getattr(proc_memory, metric), self._runtime_memory_labels, ) - def _get_runtime_cpu(self, observer: metrics.ValueObserver) -> None: - """Observer callback for runtime CPU + def _get_runtime_cpu_time(self, observer: metrics.SumObserver) -> None: + """Observer callback for runtime CPU time Args: observer: the observer to update """ proc_cpu = self._proc.cpu_times() - for _type in self._config["runtime_cpu"]: - self._runtime_cpu_labels["type"] = _type + for metric in self._config["runtime.cpu.time"]: + self._runtime_cpu_time_labels["type"] = metric observer.observe( - getattr(proc_cpu, _type), self._runtime_cpu_labels + getattr(proc_cpu, metric), self._runtime_cpu_time_labels, ) - def _get_runtime_gc_count(self, observer: metrics.ValueObserver) -> None: + def _get_runtime_gc_count(self, observer: metrics.SumObserver) -> None: """Observer callback for garbage collection Args: observer: the observer to update """ - gc_count = gc.get_count() - for index, count in enumerate(gc_count): - self._runtime_gc_labels["count"] = str(index) - observer.observe(count, self._runtime_gc_labels) + for index, count in enumerate(gc.get_count()): + self._runtime_gc_count_labels["count"] = str(index) + observer.observe(count, self._runtime_gc_count_labels) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py index b9ae662af1..2f155383f4 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py @@ -12,11 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=protected-access + from collections import namedtuple +from platform import python_implementation from unittest import mock from opentelemetry import metrics from opentelemetry.instrumentation.system_metrics import SystemMetrics +from opentelemetry.sdk.metrics.export.aggregate import ValueObserverAggregator from opentelemetry.test.test_base import TestBase @@ -24,6 +28,7 @@ class TestSystemMetrics(TestBase): def setUp(self): super().setUp() self.memory_metrics_exporter.clear() + self.implementation = python_implementation().lower() def test_system_metrics_constructor(self): # ensure the observers have been registered @@ -31,15 +36,30 @@ def test_system_metrics_constructor(self): with mock.patch("opentelemetry.metrics.get_meter") as mock_get_meter: mock_get_meter.return_value = meter SystemMetrics(self.memory_metrics_exporter) - self.assertEqual(len(meter.observers), 6) + + self.assertEqual(len(meter.observers), 18) + observer_names = [ - "system.mem", - "system.cpu", - "system.net.bytes", - "runtime.python.mem", - "runtime.python.cpu", - "runtime.python.gc.count", + "system.cpu.time", + "system.cpu.utilization", + "system.memory.usage", + "system.memory.utilization", + "system.swap.usage", + "system.swap.utilization", + "system.disk.io", + "system.disk.operations", + "system.disk.time", + "system.disk.merged", + "system.network.dropped_packets", + "system.network.packets", + "system.network.errors", + "system.network.io", + "system.network.connections", + "runtime.{}.memory".format(self.implementation), + "runtime.{}.cpu_time".format(self.implementation), + "runtime.{}.gc_count".format(self.implementation), ] + for observer in meter.observers: self.assertIn(observer.name, observer_names) observer_names.remove(observer.name) @@ -57,7 +77,7 @@ def _assert_metrics(self, observer_name, system_metrics, expected): and metric.instrument.name == observer_name ): self.assertEqual( - metric.aggregator.checkpoint.last, expected[metric.labels], + metric.aggregator.checkpoint, expected[metric.labels], ) assertions += 1 self.assertEqual(len(expected), assertions) @@ -70,134 +90,614 @@ def _test_metrics(self, observer_name, expected): system_metrics = SystemMetrics(self.memory_metrics_exporter) self._assert_metrics(observer_name, system_metrics, expected) + # When this test case is executed, _get_system_cpu_utilization gets run + # too because of the controller thread which runs all observers. This patch + # is added here to stop a warning that would otherwise be raised. + # pylint: disable=unused-argument + @mock.patch("psutil.cpu_times_percent") @mock.patch("psutil.cpu_times") - def test_system_cpu(self, mock_cpu_times): - CPUTimes = namedtuple("CPUTimes", ["user", "nice", "system", "idle"]) - mock_cpu_times.return_value = CPUTimes( - user=332277.48, nice=0.0, system=309836.43, idle=6724698.94 + def test_system_cpu_time(self, mock_cpu_times, mock_cpu_times_percent): + CPUTimes = namedtuple("CPUTimes", ["idle", "user", "system", "irq"]) + mock_cpu_times.return_value = [ + CPUTimes(idle=1.2, user=3.4, system=5.6, irq=7.8), + CPUTimes(idle=1.2, user=3.4, system=5.6, irq=7.8), + ] + + expected = { + (("cpu", 1), ("state", "idle"),): 1.2, + (("cpu", 1), ("state", "user"),): 3.4, + (("cpu", 1), ("state", "system"),): 5.6, + (("cpu", 1), ("state", "irq"),): 7.8, + (("cpu", 2), ("state", "idle"),): 1.2, + (("cpu", 2), ("state", "user"),): 3.4, + (("cpu", 2), ("state", "system"),): 5.6, + (("cpu", 2), ("state", "irq"),): 7.8, + } + self._test_metrics("system.cpu.time", expected) + + @mock.patch("psutil.cpu_times_percent") + def test_system_cpu_utilization(self, mock_cpu_times_percent): + CPUTimesPercent = namedtuple( + "CPUTimesPercent", ["idle", "user", "system", "irq"] ) + mock_cpu_times_percent.return_value = [ + CPUTimesPercent(idle=1.2, user=3.4, system=5.6, irq=7.8), + CPUTimesPercent(idle=1.2, user=3.4, system=5.6, irq=7.8), + ] expected = { - (("type", "user"),): 332277.48, - (("type", "system"),): 309836.43, - (("type", "idle"),): 6724698.94, + (("cpu", 1), ("state", "idle"),): ValueObserverAggregator._TYPE( + min=1.2 / 100, + max=1.2 / 100, + sum=1.2 / 100, + count=1, + last=1.2 / 100, + ), + (("cpu", 1), ("state", "user"),): ValueObserverAggregator._TYPE( + min=3.4 / 100, + max=3.4 / 100, + sum=3.4 / 100, + count=1, + last=3.4 / 100, + ), + (("cpu", 1), ("state", "system"),): ValueObserverAggregator._TYPE( + min=5.6 / 100, + max=5.6 / 100, + sum=5.6 / 100, + count=1, + last=5.6 / 100, + ), + (("cpu", 1), ("state", "irq"),): ValueObserverAggregator._TYPE( + min=7.8 / 100, + max=7.8 / 100, + sum=7.8 / 100, + count=1, + last=7.8 / 100, + ), + (("cpu", 2), ("state", "idle"),): ValueObserverAggregator._TYPE( + min=1.2 / 100, + max=1.2 / 100, + sum=1.2 / 100, + count=1, + last=1.2 / 100, + ), + (("cpu", 2), ("state", "user"),): ValueObserverAggregator._TYPE( + min=3.4 / 100, + max=3.4 / 100, + sum=3.4 / 100, + count=1, + last=3.4 / 100, + ), + (("cpu", 2), ("state", "system"),): ValueObserverAggregator._TYPE( + min=5.6 / 100, + max=5.6 / 100, + sum=5.6 / 100, + count=1, + last=5.6 / 100, + ), + (("cpu", 2), ("state", "irq"),): ValueObserverAggregator._TYPE( + min=7.8 / 100, + max=7.8 / 100, + sum=7.8 / 100, + count=1, + last=7.8 / 100, + ), } - self._test_metrics("system.cpu", expected) + self._test_metrics("system.cpu.utilization", expected) @mock.patch("psutil.virtual_memory") - def test_system_memory(self, mock_virtual_memory): + def test_system_memory_usage(self, mock_virtual_memory): VirtualMemory = namedtuple( - "VirtualMemory", + "VirtualMemory", ["used", "free", "cached", "total"] + ) + mock_virtual_memory.return_value = VirtualMemory( + used=1, free=2, cached=3, total=4 + ) + + expected = { + (("state", "used"),): ValueObserverAggregator._TYPE( + min=1, max=1, sum=1, count=1, last=1 + ), + (("state", "free"),): ValueObserverAggregator._TYPE( + min=2, max=2, sum=2, count=1, last=2 + ), + (("state", "cached"),): ValueObserverAggregator._TYPE( + min=3, max=3, sum=3, count=1, last=3 + ), + } + self._test_metrics("system.memory.usage", expected) + + @mock.patch("psutil.virtual_memory") + def test_system_memory_utilization(self, mock_virtual_memory): + VirtualMemory = namedtuple( + "VirtualMemory", ["used", "free", "cached", "total"] + ) + mock_virtual_memory.return_value = VirtualMemory( + used=1, free=2, cached=3, total=4 + ) + + expected = { + (("state", "used"),): ValueObserverAggregator._TYPE( + min=1 / 4, max=1 / 4, sum=1 / 4, count=1, last=1 / 4 + ), + (("state", "free"),): ValueObserverAggregator._TYPE( + min=2 / 4, max=2 / 4, sum=2 / 4, count=1, last=2 / 4 + ), + (("state", "cached"),): ValueObserverAggregator._TYPE( + min=3 / 4, max=3 / 4, sum=3 / 4, count=1, last=3 / 4 + ), + } + self._test_metrics("system.memory.utilization", expected) + + @mock.patch("psutil.swap_memory") + def test_system_swap_usage(self, mock_swap_memory): + SwapMemory = namedtuple("SwapMemory", ["used", "free", "total"]) + mock_swap_memory.return_value = SwapMemory(used=1, free=2, total=3) + + expected = { + (("state", "used"),): ValueObserverAggregator._TYPE( + min=1, max=1, sum=1, count=1, last=1 + ), + (("state", "free"),): ValueObserverAggregator._TYPE( + min=2, max=2, sum=2, count=1, last=2 + ), + } + self._test_metrics("system.swap.usage", expected) + + @mock.patch("psutil.swap_memory") + def test_system_swap_utilization(self, mock_swap_memory): + SwapMemory = namedtuple("SwapMemory", ["used", "free", "total"]) + mock_swap_memory.return_value = SwapMemory(used=1, free=2, total=3) + + expected = { + (("state", "used"),): ValueObserverAggregator._TYPE( + min=1 / 3, max=1 / 3, sum=1 / 3, count=1, last=1 / 3 + ), + (("state", "free"),): ValueObserverAggregator._TYPE( + min=2 / 3, max=2 / 3, sum=2 / 3, count=1, last=2 / 3 + ), + } + self._test_metrics("system.swap.utilization", expected) + + @mock.patch("psutil.disk_io_counters") + def test_system_disk_io(self, mock_disk_io_counters): + DiskIO = namedtuple( + "DiskIO", [ - "total", - "available", - "percent", - "used", - "free", - "active", - "inactive", - "wired", + "read_count", + "write_count", + "read_bytes", + "write_bytes", + "read_time", + "write_time", + "read_merged_count", + "write_merged_count", ], ) - mock_virtual_memory.return_value = VirtualMemory( - total=17179869184, - available=5520928768, - percent=67.9, - used=10263990272, - free=266964992, - active=5282459648, - inactive=5148700672, - wired=4981530624, + mock_disk_io_counters.return_value = { + "sda": DiskIO( + read_count=1, + write_count=2, + read_bytes=3, + write_bytes=4, + read_time=5, + write_time=6, + read_merged_count=7, + write_merged_count=8, + ), + "sdb": DiskIO( + read_count=9, + write_count=10, + read_bytes=11, + write_bytes=12, + read_time=13, + write_time=14, + read_merged_count=15, + write_merged_count=16, + ), + } + + expected = { + (("device", "sda"), ("direction", "read"),): 3, + (("device", "sda"), ("direction", "write"),): 4, + (("device", "sdb"), ("direction", "read"),): 11, + (("device", "sdb"), ("direction", "write"),): 12, + } + self._test_metrics("system.disk.io", expected) + + @mock.patch("psutil.disk_io_counters") + def test_system_disk_operations(self, mock_disk_io_counters): + DiskIO = namedtuple( + "DiskIO", + [ + "read_count", + "write_count", + "read_bytes", + "write_bytes", + "read_time", + "write_time", + "read_merged_count", + "write_merged_count", + ], + ) + mock_disk_io_counters.return_value = { + "sda": DiskIO( + read_count=1, + write_count=2, + read_bytes=3, + write_bytes=4, + read_time=5, + write_time=6, + read_merged_count=7, + write_merged_count=8, + ), + "sdb": DiskIO( + read_count=9, + write_count=10, + read_bytes=11, + write_bytes=12, + read_time=13, + write_time=14, + read_merged_count=15, + write_merged_count=16, + ), + } + + expected = { + (("device", "sda"), ("direction", "read"),): 1, + (("device", "sda"), ("direction", "write"),): 2, + (("device", "sdb"), ("direction", "read"),): 9, + (("device", "sdb"), ("direction", "write"),): 10, + } + self._test_metrics("system.disk.operations", expected) + + @mock.patch("psutil.disk_io_counters") + def test_system_disk_time(self, mock_disk_io_counters): + DiskIO = namedtuple( + "DiskIO", + [ + "read_count", + "write_count", + "read_bytes", + "write_bytes", + "read_time", + "write_time", + "read_merged_count", + "write_merged_count", + ], ) + mock_disk_io_counters.return_value = { + "sda": DiskIO( + read_count=1, + write_count=2, + read_bytes=3, + write_bytes=4, + read_time=5, + write_time=6, + read_merged_count=7, + write_merged_count=8, + ), + "sdb": DiskIO( + read_count=9, + write_count=10, + read_bytes=11, + write_bytes=12, + read_time=13, + write_time=14, + read_merged_count=15, + write_merged_count=16, + ), + } expected = { - (("type", "total"),): 17179869184, - (("type", "used"),): 10263990272, - (("type", "available"),): 5520928768, - (("type", "free"),): 266964992, + (("device", "sda"), ("direction", "read"),): 5 / 1000, + (("device", "sda"), ("direction", "write"),): 6 / 1000, + (("device", "sdb"), ("direction", "read"),): 13 / 1000, + (("device", "sdb"), ("direction", "write"),): 14 / 1000, } - self._test_metrics("system.mem", expected) + self._test_metrics("system.disk.time", expected) + + @mock.patch("psutil.disk_io_counters") + def test_system_disk_merged(self, mock_disk_io_counters): + DiskIO = namedtuple( + "DiskIO", + [ + "read_count", + "write_count", + "read_bytes", + "write_bytes", + "read_time", + "write_time", + "read_merged_count", + "write_merged_count", + ], + ) + mock_disk_io_counters.return_value = { + "sda": DiskIO( + read_count=1, + write_count=2, + read_bytes=3, + write_bytes=4, + read_time=5, + write_time=6, + read_merged_count=7, + write_merged_count=8, + ), + "sdb": DiskIO( + read_count=9, + write_count=10, + read_bytes=11, + write_bytes=12, + read_time=13, + write_time=14, + read_merged_count=15, + write_merged_count=16, + ), + } + + expected = { + (("device", "sda"), ("direction", "read"),): 7, + (("device", "sda"), ("direction", "write"),): 8, + (("device", "sdb"), ("direction", "read"),): 15, + (("device", "sdb"), ("direction", "write"),): 16, + } + self._test_metrics("system.disk.merged", expected) @mock.patch("psutil.net_io_counters") - def test_network_bytes(self, mock_net_io_counters): - NetworkIO = namedtuple( - "NetworkIO", - ["bytes_sent", "bytes_recv", "packets_recv", "packets_sent"], + def test_system_network_dropped_packets(self, mock_net_io_counters): + NetIO = namedtuple( + "NetIO", + [ + "dropin", + "dropout", + "packets_sent", + "packets_recv", + "errin", + "errout", + "bytes_sent", + "bytes_recv", + ], ) - mock_net_io_counters.return_value = NetworkIO( - bytes_sent=23920188416, - bytes_recv=46798894080, - packets_sent=53127118, - packets_recv=53205738, + mock_net_io_counters.return_value = { + "eth0": NetIO( + dropin=1, + dropout=2, + packets_sent=3, + packets_recv=4, + errin=5, + errout=6, + bytes_sent=7, + bytes_recv=8, + ), + "eth1": NetIO( + dropin=9, + dropout=10, + packets_sent=11, + packets_recv=12, + errin=13, + errout=14, + bytes_sent=15, + bytes_recv=16, + ), + } + + expected = { + (("device", "eth0"), ("direction", "receive"),): 1, + (("device", "eth0"), ("direction", "transmit"),): 2, + (("device", "eth1"), ("direction", "receive"),): 9, + (("device", "eth1"), ("direction", "transmit"),): 10, + } + self._test_metrics("system.network.dropped_packets", expected) + + @mock.patch("psutil.net_io_counters") + def test_system_network_packets(self, mock_net_io_counters): + NetIO = namedtuple( + "NetIO", + [ + "dropin", + "dropout", + "packets_sent", + "packets_recv", + "errin", + "errout", + "bytes_sent", + "bytes_recv", + ], ) + mock_net_io_counters.return_value = { + "eth0": NetIO( + dropin=1, + dropout=2, + packets_sent=3, + packets_recv=4, + errin=5, + errout=6, + bytes_sent=7, + bytes_recv=8, + ), + "eth1": NetIO( + dropin=9, + dropout=10, + packets_sent=11, + packets_recv=12, + errin=13, + errout=14, + bytes_sent=15, + bytes_recv=16, + ), + } expected = { - (("type", "bytes_recv"),): 46798894080, - (("type", "bytes_sent"),): 23920188416, + (("device", "eth0"), ("direction", "receive"),): 4, + (("device", "eth0"), ("direction", "transmit"),): 3, + (("device", "eth1"), ("direction", "receive"),): 12, + (("device", "eth1"), ("direction", "transmit"),): 11, } - self._test_metrics("system.net.bytes", expected) + self._test_metrics("system.network.packets", expected) - def test_runtime_memory(self): - meter = self.meter_provider.get_meter(__name__) - with mock.patch("opentelemetry.metrics.get_meter") as mock_get_meter: - mock_get_meter.return_value = meter - system_metrics = SystemMetrics(self.memory_metrics_exporter) + @mock.patch("psutil.net_io_counters") + def test_system_network_errors(self, mock_net_io_counters): + NetIO = namedtuple( + "NetIO", + [ + "dropin", + "dropout", + "packets_sent", + "packets_recv", + "errin", + "errout", + "bytes_sent", + "bytes_recv", + ], + ) + mock_net_io_counters.return_value = { + "eth0": NetIO( + dropin=1, + dropout=2, + packets_sent=3, + packets_recv=4, + errin=5, + errout=6, + bytes_sent=7, + bytes_recv=8, + ), + "eth1": NetIO( + dropin=9, + dropout=10, + packets_sent=11, + packets_recv=12, + errin=13, + errout=14, + bytes_sent=15, + bytes_recv=16, + ), + } - with mock.patch.object( - system_metrics._proc, # pylint: disable=protected-access - "memory_info", - ) as mock_runtime_memory: - RuntimeMemory = namedtuple( - "RuntimeMemory", ["rss", "vms", "pfaults", "pageins"], - ) - mock_runtime_memory.return_value = RuntimeMemory( - rss=9777152, vms=4385665024, pfaults=2631, pageins=49 - ) - expected = { - (("type", "rss"),): 9777152, - (("type", "vms"),): 4385665024, - } - self._assert_metrics( - "runtime.python.mem", system_metrics, expected - ) + expected = { + (("device", "eth0"), ("direction", "receive"),): 5, + (("device", "eth0"), ("direction", "transmit"),): 6, + (("device", "eth1"), ("direction", "receive"),): 13, + (("device", "eth1"), ("direction", "transmit"),): 14, + } + self._test_metrics("system.network.errors", expected) - def test_runtime_cpu(self): - meter = self.meter_provider.get_meter(__name__) - with mock.patch("opentelemetry.metrics.get_meter") as mock_get_meter: - mock_get_meter.return_value = meter - system_metrics = SystemMetrics(self.memory_metrics_exporter) + @mock.patch("psutil.net_io_counters") + def test_system_network_io(self, mock_net_io_counters): + NetIO = namedtuple( + "NetIO", + [ + "dropin", + "dropout", + "packets_sent", + "packets_recv", + "errin", + "errout", + "bytes_sent", + "bytes_recv", + ], + ) + mock_net_io_counters.return_value = { + "eth0": NetIO( + dropin=1, + dropout=2, + packets_sent=3, + packets_recv=4, + errin=5, + errout=6, + bytes_sent=7, + bytes_recv=8, + ), + "eth1": NetIO( + dropin=9, + dropout=10, + packets_sent=11, + packets_recv=12, + errin=13, + errout=14, + bytes_sent=15, + bytes_recv=16, + ), + } - with mock.patch.object( - system_metrics._proc, # pylint: disable=protected-access - "cpu_times", - ) as mock_runtime_cpu_times: - RuntimeCPU = namedtuple( - "RuntimeCPU", ["user", "nice", "system"] - ) - mock_runtime_cpu_times.return_value = RuntimeCPU( - user=100.48, nice=0.0, system=200.43 - ) + expected = { + (("device", "eth0"), ("direction", "receive"),): 8, + (("device", "eth0"), ("direction", "transmit"),): 7, + (("device", "eth1"), ("direction", "receive"),): 16, + (("device", "eth1"), ("direction", "transmit"),): 15, + } + self._test_metrics("system.network.io", expected) - expected = { - (("type", "user"),): 100.48, - (("type", "system"),): 200.43, - } + @mock.patch("psutil.net_connections") + def test_system_network_connections(self, mock_net_connections): + NetConnection = namedtuple( + "NetworkConnection", ["family", "type", "status"] + ) + Type = namedtuple("Type", ["value"]) + mock_net_connections.return_value = [ + NetConnection(family=1, status="ESTABLISHED", type=Type(value=2),), + NetConnection(family=1, status="ESTABLISHED", type=Type(value=1),), + ] - self._assert_metrics( - "runtime.python.cpu", system_metrics, expected - ) + expected = { + ( + ("family", 1), + ("protocol", "udp"), + ("state", "ESTABLISHED"), + ("type", Type(value=2)), + ): 1, + ( + ("family", 1), + ("protocol", "tcp"), + ("state", "ESTABLISHED"), + ("type", Type(value=1)), + ): 1, + } + self._test_metrics("system.network.connections", expected) + + @mock.patch("psutil.Process.memory_info") + def test_runtime_memory(self, mock_process_memory_info): + + PMem = namedtuple("PMem", ["rss", "vms"]) + + mock_process_memory_info.configure_mock( + **{"return_value": PMem(rss=1, vms=2)} + ) + + expected = { + (("type", "rss"),): 1, + (("type", "vms"),): 2, + } + self._test_metrics( + "runtime.{}.memory".format(self.implementation), expected + ) + + @mock.patch("psutil.Process.cpu_times") + def test_runtime_cpu_time(self, mock_process_cpu_times): + + PCPUTimes = namedtuple("PCPUTimes", ["user", "system"]) + + mock_process_cpu_times.configure_mock( + **{"return_value": PCPUTimes(user=1.1, system=2.2)} + ) + + expected = { + (("type", "user"),): 1.1, + (("type", "system"),): 2.2, + } + self._test_metrics( + "runtime.{}.cpu_time".format(self.implementation), expected + ) @mock.patch("gc.get_count") - def test_runtime_gc_count(self, mock_gc): - mock_gc.return_value = [ - 100, # gen0 - 50, # gen1 - 10, # gen2 - ] + def test_runtime_get_count(self, mock_gc_get_count): + + mock_gc_get_count.configure_mock(**{"return_value": (1, 2, 3)}) expected = { - (("count", "0"),): 100, - (("count", "1"),): 50, - (("count", "2"),): 10, + (("count", "0"),): 1, + (("count", "1"),): 2, + (("count", "2"),): 3, } - self._test_metrics("runtime.python.gc.count", expected) + self._test_metrics( + "runtime.{}.gc_count".format(self.implementation), expected + ) From 63685b11c85717cd7bda1f6586bf53bc036f150f Mon Sep 17 00:00:00 2001 From: Steve Flanders Date: Tue, 8 Sep 2020 11:05:34 -0400 Subject: [PATCH 0532/1517] Remove non-inclusive language from comments in pylint (#1077) * Remove non-inclusive language from comments in pylint Co-authored-by: Daniel <61800298+ffe4@users.noreply.github.com> --- .pylintrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pylintrc b/.pylintrc index 8f29b634f1..b23bef8e66 100644 --- a/.pylintrc +++ b/.pylintrc @@ -5,11 +5,11 @@ # run arbitrary code. extension-pkg-whitelist= -# Add files or directories to the blacklist. They should be base names, not +# Add list of files or directories to be excluded. They should be base names, not # paths. ignore=CVS,gen,proto -# Add files or directories matching the regex patterns to the blacklist. The +# Add files or directories matching the regex patterns to be excluded. The # regex matches against base names, not paths. ignore-patterns= From f6e07054e5c86880e3b09d7885a9ba9f07aab539 Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Wed, 9 Sep 2020 04:15:01 +0200 Subject: [PATCH 0533/1517] Improve BatchExportSpanProcessor (#1062) * it was possible for force flush calls to miss the flush finished notifications by the worker thread. in case a flush token got added in the main thread and the worker thread processed and notified the flush condition before the main thread called wait on the flush condition, the wakup is missed and the main thread has to wait the full flush timeout * calls to force flush were not really thread safe since the state if a flush operation is in progress was indictated by a single boolean flag which gets reset when the first force flush call finishes. * instead of having a boolean flag to indicate a flush request use an Event. When a call to force flush is made it is looked up if a flush request event is currently pending or a new one is created. The worker thread will check if a flush request event exists, unset it and use a local reference for signaling once the export operation finished. Force flush calls will wait in the meantime on the flush request event until they are signaled by the worker thread. This also makes calls to force flush thread safe since multiple threads can/might wait on one event. --- opentelemetry-sdk/CHANGELOG.md | 2 + .../sdk/trace/export/__init__.py | 145 +++++++++++++----- .../tests/trace/export/test_export.py | 55 ++++++- 3 files changed, 159 insertions(+), 43 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 60116ec731..214766253e 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -8,6 +8,8 @@ ([#1034](https://github.com/open-telemetry/opentelemetry-python/pull/1034)) - Remove lazy Event and Link API from Span interface ([#1045](https://github.com/open-telemetry/opentelemetry-python/pull/1045)) +- Improve BatchExportSpanProcessor + ([#1062](https://github.com/open-telemetry/opentelemetry-python/pull/1062)) - Populate resource attributes as per semantic conventions ([#1053](https://github.com/open-telemetry/opentelemetry-python/pull/1053)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 9fe55ed7fd..7c1e51f3ec 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -20,8 +20,7 @@ import typing from enum import Enum -from opentelemetry.context import attach, detach, get_current, set_value -from opentelemetry.trace import DefaultSpan +from opentelemetry.context import attach, detach, set_value from opentelemetry.util import time_ns from .. import Span, SpanProcessor @@ -91,6 +90,16 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: return True +class _FlushRequest: + """Represents a request for the BatchExportSpanProcessor to flush spans.""" + + __slots__ = ["event", "num_spans"] + + def __init__(self): + self.event = threading.Event() + self.num_spans = 0 + + class BatchExportSpanProcessor(SpanProcessor): """Batch span processor implementation. @@ -98,8 +107,6 @@ class BatchExportSpanProcessor(SpanProcessor): batches ended spans and pushes them to the configured `SpanExporter`. """ - _FLUSH_TOKEN_SPAN = DefaultSpan(context=None) - def __init__( self, span_exporter: SpanExporter, @@ -129,9 +136,7 @@ def __init__( ) # type: typing.Deque[Span] self.worker_thread = threading.Thread(target=self.worker, daemon=True) self.condition = threading.Condition(threading.Lock()) - self.flush_condition = threading.Condition(threading.Lock()) - # flag to indicate that there is a flush operation on progress - self._flushing = False + self._flush_request = None # type: typing.Optional[_FlushRequest] self.schedule_delay_millis = schedule_delay_millis self.max_export_batch_size = max_export_batch_size self.max_queue_size = max_queue_size @@ -164,60 +169,128 @@ def on_end(self, span: Span) -> None: def worker(self): timeout = self.schedule_delay_millis / 1e3 + flush_request = None # type: typing.Optional[_FlushRequest] while not self.done: - if ( - len(self.queue) < self.max_export_batch_size - and not self._flushing - ): - with self.condition: + with self.condition: + if self.done: + # done flag may have changed, avoid waiting + break + flush_request = self._get_and_unset_flush_request() + if ( + len(self.queue) < self.max_export_batch_size + and flush_request is None + ): + self.condition.wait(timeout) + flush_request = self._get_and_unset_flush_request() if not self.queue: # spurious notification, let's wait again + self._notify_flush_request_finished(flush_request) + flush_request = None continue if self.done: # missing spans will be sent when calling flush break - # substract the duration of this export call to the next timeout + # subtract the duration of this export call to the next timeout start = time_ns() - self.export() + self._export(flush_request) end = time_ns() duration = (end - start) / 1e9 timeout = self.schedule_delay_millis / 1e3 - duration + self._notify_flush_request_finished(flush_request) + flush_request = None + + # there might have been a new flush request while export was running + # and before the done flag switched to true + with self.condition: + shutdown_flush_request = self._get_and_unset_flush_request() + # be sure that all spans are sent self._drain_queue() + self._notify_flush_request_finished(flush_request) + self._notify_flush_request_finished(shutdown_flush_request) + + def _get_and_unset_flush_request(self,) -> typing.Optional[_FlushRequest]: + """Returns the current flush request and makes it invisible to the + worker thread for subsequent calls. + """ + flush_request = self._flush_request + self._flush_request = None + if flush_request is not None: + flush_request.num_spans = len(self.queue) + return flush_request + + @staticmethod + def _notify_flush_request_finished( + flush_request: typing.Optional[_FlushRequest], + ): + """Notifies the flush initiator(s) waiting on the given request/event + that the flush operation was finished. + """ + if flush_request is not None: + flush_request.event.set() + + def _get_or_create_flush_request(self) -> _FlushRequest: + """Either returns the current active flush event or creates a new one. - def export(self) -> None: - """Exports at most max_export_batch_size spans.""" + The flush event will be visible and read by the worker thread before an + export operation starts. Callers of a flush operation may wait on the + returned event to be notified when the flush/export operation was + finished. + + This method is not thread-safe, i.e. callers need to take care about + synchronization/locking. + """ + if self._flush_request is None: + self._flush_request = _FlushRequest() + return self._flush_request + + def _export(self, flush_request: typing.Optional[_FlushRequest]): + """Exports spans considering the given flush_request. + + In case of a given flush_requests spans are exported in batches until + the number of exported spans reached or exceeded the number of spans in + the flush request. + In no flush_request was given at most max_export_batch_size spans are + exported. + """ + if not flush_request: + self._export_batch() + return + + num_spans = flush_request.num_spans + while self.queue: + num_exported = self._export_batch() + num_spans -= num_exported + + if num_spans <= 0: + break + + def _export_batch(self) -> int: + """Exports at most max_export_batch_size spans and returns the number of + exported spans. + """ idx = 0 - notify_flush = False # currently only a single thread acts as consumer, so queue.pop() will # not raise an exception while idx < self.max_export_batch_size and self.queue: - span = self.queue.pop() - if span is self._FLUSH_TOKEN_SPAN: - notify_flush = True - else: - self.spans_list[idx] = span - idx += 1 + self.spans_list[idx] = self.queue.pop() + idx += 1 token = attach(set_value("suppress_instrumentation", True)) try: # Ignore type b/c the Optional[None]+slicing is too "clever" # for mypy self.span_exporter.export(self.spans_list[:idx]) # type: ignore - # pylint: disable=broad-except - except Exception: + except Exception: # pylint: disable=broad-except logger.exception("Exception while exporting Span batch.") detach(token) - if notify_flush: - with self.flush_condition: - self.flush_condition.notify() - # clean up list for index in range(idx): self.spans_list[index] = None + return idx def _drain_queue(self): """"Export all elements until queue is empty. @@ -226,26 +299,20 @@ def _drain_queue(self): `export` that is not thread safe. """ while self.queue: - self.export() + self._export_batch() def force_flush(self, timeout_millis: int = 30000) -> bool: if self.done: logger.warning("Already shutdown, ignoring call to force_flush().") return True - self._flushing = True - self.queue.appendleft(self._FLUSH_TOKEN_SPAN) - - # wake up worker thread with self.condition: + flush_request = self._get_or_create_flush_request() + # signal the worker thread to flush and wait for it to finish self.condition.notify_all() # wait for token to be processed - with self.flush_condition: - ret = self.flush_condition.wait(timeout_millis / 1e3) - - self._flushing = False - + ret = flush_request.event.wait(timeout_millis / 1e3) if not ret: logger.warning("Timeout was exceeded in force_flush().") return ret diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 43b7893951..34e1d14d23 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -13,8 +13,10 @@ # limitations under the License. import os +import threading import time import unittest +from concurrent.futures import ThreadPoolExecutor from logging import WARNING from unittest import mock @@ -31,11 +33,13 @@ def __init__( destination, max_export_batch_size=None, export_timeout_millis=0.0, + export_event: threading.Event = None, ): self.destination = destination self.max_export_batch_size = max_export_batch_size self.is_shutdown = False self.export_timeout = export_timeout_millis / 1e3 + self.export_event = export_event def export(self, spans: trace.Span) -> export.SpanExportResult: if ( @@ -45,6 +49,8 @@ def export(self, spans: trace.Span) -> export.SpanExportResult: raise ValueError("Batch is too big") time.sleep(self.export_timeout) self.destination.extend(span.name for span in spans) + if self.export_event: + self.export_event.set() return export.SpanExportResult.SUCCESS def shutdown(self): @@ -148,6 +154,42 @@ def test_flush(self): span_processor.shutdown() + def test_flush_empty(self): + spans_names_list = [] + + my_exporter = MySpanExporter(destination=spans_names_list) + span_processor = export.BatchExportSpanProcessor(my_exporter) + + self.assertTrue(span_processor.force_flush()) + + def test_flush_from_multiple_threads(self): + num_threads = 50 + num_spans = 10 + + span_list = [] + + my_exporter = MySpanExporter(destination=span_list) + span_processor = export.BatchExportSpanProcessor( + my_exporter, max_queue_size=512, max_export_batch_size=128 + ) + + def create_spans_and_flush(tno: int): + for span_idx in range(num_spans): + _create_start_and_end_span( + "Span {}-{}".format(tno, span_idx), span_processor + ) + self.assertTrue(span_processor.force_flush()) + + with ThreadPoolExecutor(max_workers=num_threads) as executor: + future_list = [] + for thread_no in range(num_threads): + future = executor.submit(create_spans_and_flush, thread_no) + future_list.append(future) + + executor.shutdown() + + self.assertEqual(num_threads * num_spans, len(span_list)) + def test_flush_timeout(self): spans_names_list = [] @@ -209,17 +251,22 @@ def test_batch_span_processor_scheduled_delay(self): """Test that spans are exported each schedule_delay_millis""" spans_names_list = [] - my_exporter = MySpanExporter(destination=spans_names_list) + export_event = threading.Event() + my_exporter = MySpanExporter( + destination=spans_names_list, export_event=export_event + ) span_processor = export.BatchExportSpanProcessor( - my_exporter, schedule_delay_millis=50 + my_exporter, schedule_delay_millis=50, ) # create single span + start_time = time.time() _create_start_and_end_span("foo", span_processor) - time.sleep(0.05 + 0.02) - # span should be already exported + self.assertTrue(export_event.wait(2)) + export_time = time.time() self.assertEqual(len(spans_names_list), 1) + self.assertGreaterEqual((export_time - start_time) * 1e3, 50) span_processor.shutdown() From 370cc6be6f739da64971588c21518a77eda19547 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 9 Sep 2020 07:59:44 -0700 Subject: [PATCH 0534/1517] exporter/zipkin: adding support for env var OTEL_EXPORTER_ZIPKIN_ENDPOINT (#1064) * add support for env var: OTEL_EXPORTER_ZIPKIN_ENDPOINT * update method signature to use url --- .../CHANGELOG.md | 6 +++ .../opentelemetry/exporter/zipkin/__init__.py | 41 ++++++++----------- .../tests/test_zipkin_exporter.py | 41 ++++++++++--------- 3 files changed, 46 insertions(+), 42 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index 5667883314..b1066d081a 100644 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +- Add support for OTEL_EXPORTER_ZIPKIN_ENDPOINT env var. As part of this change, the + configuration of the ZipkinSpanExporter exposes a `url` argument to replace `host_name`, + `port`, `protocol`, `endpoint`. This brings this implementation inline with other + implementations. + ([#1064](https://github.com/open-telemetry/opentelemetry-python/pull/1064)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index b0eb1bce0f..c62e07e414 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -24,6 +24,7 @@ .. _Zipkin: https://zipkin.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +.. _Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/sdk-environment-variables.md#zipkin-exporter .. code:: python @@ -39,10 +40,7 @@ zipkin_exporter = zipkin.ZipkinSpanExporter( service_name="my-helloworld-service", # optional: - # host_name="localhost", - # port=9411, - # endpoint="/api/v2/spans", - # protocol="http", + # url="http://localhost:9411/api/v2/spans", # ipv4="", # ipv6="", # retry=False, @@ -57,24 +55,25 @@ with tracer.start_as_current_span("foo"): print("Hello world!") +The exporter supports endpoint configuration via the OTEL_EXPORTER_ZIPKIN_ENDPOINT environment variables as defined in the `Specification`_ + API --- """ import json import logging +import os from typing import Optional, Sequence +from urllib.parse import urlparse import requests from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace import Span, SpanContext, SpanKind -DEFAULT_ENDPOINT = "/api/v2/spans" -DEFAULT_HOST_NAME = "localhost" -DEFAULT_PORT = 9411 -DEFAULT_PROTOCOL = "http" DEFAULT_RETRY = False +DEFAULT_URL = "http://localhost:9411/api/v2/spans" ZIPKIN_HEADERS = {"Content-Type": "application/json"} SPAN_KIND_MAP = { @@ -96,10 +95,7 @@ class ZipkinSpanExporter(SpanExporter): Args: service_name: Service that logged an annotation in a trace.Classifier when query for spans. - host_name: The host name of the Zipkin server - port: The port of the Zipkin server - endpoint: The endpoint of the Zipkin server - protocol: The protocol used for the request. + url: The Zipkin endpoint URL ipv4: Primary IPv4 address associated with this connection. ipv6: Primary IPv6 address associated with this connection. retry: Set to True to configure the exporter to retry on failure. @@ -108,22 +104,21 @@ class ZipkinSpanExporter(SpanExporter): def __init__( self, service_name: str, - host_name: str = DEFAULT_HOST_NAME, - port: int = DEFAULT_PORT, - endpoint: str = DEFAULT_ENDPOINT, - protocol: str = DEFAULT_PROTOCOL, + url: str = None, ipv4: Optional[str] = None, ipv6: Optional[str] = None, retry: Optional[str] = DEFAULT_RETRY, ): self.service_name = service_name - self.host_name = host_name - self.port = port - self.endpoint = endpoint - self.protocol = protocol - self.url = "{}://{}:{}{}".format( - self.protocol, self.host_name, self.port, self.endpoint - ) + if url is None: + self.url = os.environ.get( + "OTEL_EXPORTER_ZIPKIN_ENDPOINT", DEFAULT_URL + ) + else: + self.url = url + + self.port = urlparse(self.url).port + self.ipv4 = ipv4 self.ipv6 = ipv6 self.retry = retry diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 220acb8fa4..709ca29d6a 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -13,6 +13,7 @@ # limitations under the License. import json +import os import unittest from unittest.mock import MagicMock, patch @@ -43,54 +44,56 @@ def setUp(self): self._test_span.start() self._test_span.end() + def tearDown(self): + if "OTEL_EXPORTER_ZIPKIN_ENDPOINT" in os.environ: + del os.environ["OTEL_EXPORTER_ZIPKIN_ENDPOINT"] + + def test_constructor_env_var(self): + """Test the default values assigned by constructor.""" + url = "https://foo:9911/path" + os.environ["OTEL_EXPORTER_ZIPKIN_ENDPOINT"] = url + service_name = "my-service-name" + port = 9911 + exporter = ZipkinSpanExporter(service_name) + ipv4 = None + ipv6 = None + + self.assertEqual(exporter.service_name, service_name) + self.assertEqual(exporter.ipv4, ipv4) + self.assertEqual(exporter.ipv6, ipv6) + self.assertEqual(exporter.url, url) + self.assertEqual(exporter.port, port) + def test_constructor_default(self): """Test the default values assigned by constructor.""" service_name = "my-service-name" - host_name = "localhost" port = 9411 - endpoint = "/api/v2/spans" exporter = ZipkinSpanExporter(service_name) ipv4 = None ipv6 = None - protocol = "http" url = "http://localhost:9411/api/v2/spans" self.assertEqual(exporter.service_name, service_name) - self.assertEqual(exporter.host_name, host_name) self.assertEqual(exporter.port, port) - self.assertEqual(exporter.endpoint, endpoint) self.assertEqual(exporter.ipv4, ipv4) self.assertEqual(exporter.ipv6, ipv6) - self.assertEqual(exporter.protocol, protocol) self.assertEqual(exporter.url, url) def test_constructor_explicit(self): """Test the constructor passing all the options.""" service_name = "my-opentelemetry-zipkin" - host_name = "opentelemetry.io" port = 15875 - endpoint = "/myapi/traces?format=zipkin" ipv4 = "1.2.3.4" ipv6 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334" - protocol = "https" url = "https://opentelemetry.io:15875/myapi/traces?format=zipkin" exporter = ZipkinSpanExporter( - service_name=service_name, - host_name=host_name, - port=port, - endpoint=endpoint, - ipv4=ipv4, - ipv6=ipv6, - protocol=protocol, + service_name=service_name, url=url, ipv4=ipv4, ipv6=ipv6, ) self.assertEqual(exporter.service_name, service_name) - self.assertEqual(exporter.host_name, host_name) self.assertEqual(exporter.port, port) - self.assertEqual(exporter.endpoint, endpoint) self.assertEqual(exporter.ipv4, ipv4) self.assertEqual(exporter.ipv6, ipv6) - self.assertEqual(exporter.protocol, protocol) self.assertEqual(exporter.url, url) # pylint: disable=too-many-locals From 50478c2e9015da8e54bfe2f7ab384a430f9bf597 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 9 Sep 2020 08:23:18 -0700 Subject: [PATCH 0535/1517] sdk: rename resource labels to attributes (#1082) This aligns with the specification for Resources --- .../opentelemetry/exporter/jaeger/__init__.py | 2 +- .../tests/test_jaeger_exporter.py | 2 +- .../opencensus/metrics_exporter/__init__.py | 10 +++---- .../opentelemetry/exporter/otlp/__init__.py | 2 +- .../opentelemetry/exporter/otlp/exporter.py | 2 +- .../opentelemetry/exporter/zipkin/__init__.py | 2 +- .../tests/test_zipkin_exporter.py | 4 +-- .../instrumentation/boto/__init__.py | 6 ++-- .../tests/test_boto_instrumentation.py | 21 +++++++------ .../instrumentation/botocore/__init__.py | 6 ++-- .../tests/test_botocore_instrumentation.py | 29 ++++++++++++------ opentelemetry-sdk/CHANGELOG.md | 2 ++ .../opentelemetry/sdk/resources/__init__.py | 30 +++++++++---------- .../src/opentelemetry/sdk/trace/__init__.py | 2 +- .../tests/resources/test_resources.py | 30 +++++++++---------- 15 files changed, 85 insertions(+), 65 deletions(-) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index ccb530b7a1..b998a6aecf 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -204,7 +204,7 @@ def _translate_to_jaeger(spans: Span): parent_id = span.parent.span_id if span.parent else 0 tags = _extract_tags(span.attributes) - tags.extend(_extract_tags(span.resource.labels)) + tags.extend(_extract_tags(span.resource.attributes)) tags.extend( [ diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py index f2717a8bcf..bb852a0798 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py @@ -207,7 +207,7 @@ def test_translate_to_jaeger(self): otel_spans[0].set_attribute("key_float", 111.22) otel_spans[0].set_attribute("key_tuple", ("tuple_element",)) otel_spans[0].resource = Resource( - labels={"key_resource": "some_resource"} + attributes={"key_resource": "some_resource"} ) otel_spans[0].set_status( Status(StatusCanonicalCode.UNKNOWN, "Example description") diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py index 76986a8a59..204a7c5476 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py @@ -191,19 +191,19 @@ def get_collector_point(metric_record: MetricRecord) -> metrics_pb2.Point: def get_resource(metric_record: MetricRecord) -> resource_pb2.Resource: - resource_labels = metric_record.instrument.meter.resource.labels + resource_attributes = metric_record.instrument.meter.resource.attributes return resource_pb2.Resource( - type=infer_oc_resource_type(resource_labels), - labels={k: str(v) for k, v in resource_labels.items()}, + type=infer_oc_resource_type(resource_attributes), + labels={k: str(v) for k, v in resource_attributes.items()}, ) -def infer_oc_resource_type(resource_labels: Dict[str, str]) -> str: +def infer_oc_resource_type(resource_attributes: Dict[str, str]) -> str: """Convert from OT resource labels to OC resource type""" for ( ot_resource_key, oc_resource_type, ) in _OT_LABEL_PRESENCE_TO_RESOURCE_TYPE: - if ot_resource_key in resource_labels: + if ot_resource_key in resource_attributes: return oc_resource_type return "" diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py index a078cb7ccc..a4d8f46d4c 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py @@ -36,7 +36,7 @@ # Resource can be required for some backends, e.g. Jaeger # If resource wouldn't be set - traces wouldn't appears in Jaeger - resource = Resource(labels=labels={ + resource = Resource(attributes={ "service.name": "service" }) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index e0914f262f..7cd9f905e0 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -76,7 +76,7 @@ def _get_resource_data( collector_resource = Resource() - for key, value in sdk_resource.labels.items(): + for key, value in sdk_resource.attributes.items(): try: # pylint: disable=no-member diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index c62e07e414..6b3ce2df9e 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -211,7 +211,7 @@ def _extract_tags_from_dict(tags_dict): def _extract_tags_from_span(span: Span): tags = _extract_tags_from_dict(getattr(span, "attributes", None)) if span.resource: - tags.update(_extract_tags_from_dict(span.resource.labels)) + tags.update(_extract_tags_from_dict(span.resource.attributes)) return tags diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 709ca29d6a..96586d91e0 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -177,14 +177,14 @@ def test_export(self): otel_spans[1].start(start_time=start_times[1]) otel_spans[1].resource = Resource( - labels={"key_resource": "some_resource"} + attributes={"key_resource": "some_resource"} ) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) otel_spans[2].set_attribute("key_string", "hello_world") otel_spans[2].resource = Resource( - labels={"key_resource": "some_resource"} + attributes={"key_resource": "some_resource"} ) otel_spans[2].end(end_time=end_times[2]) diff --git a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py index e6a0e351e5..8e03cd6e74 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py @@ -126,13 +126,15 @@ def _common_request( # pylint: disable=too-many-locals if args: http_method = args[0] span.resource = Resource( - labels={ + attributes={ "endpoint": endpoint_name, "http_method": http_method.lower(), } ) else: - span.resource = Resource(labels={"endpoint": endpoint_name}) + span.resource = Resource( + attributes={"endpoint": endpoint_name} + ) add_span_arg_tags( span, endpoint_name, args, args_name, traced_args, diff --git a/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py b/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py index 7ed8775501..0a4a4b8869 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py @@ -73,7 +73,7 @@ def test_ec2_client(self): self.assertEqual( span.resource, Resource( - labels={"endpoint": "ec2", "http_method": "runinstances"} + attributes={"endpoint": "ec2", "http_method": "runinstances"} ), ) self.assertEqual(span.attributes["http.method"], "POST") @@ -131,7 +131,7 @@ def test_s3_client(self): assert_span_http_status_code(span, 200) self.assertEqual( span.resource, - Resource(labels={"endpoint": "s3", "http_method": "head"}), + Resource(attributes={"endpoint": "s3", "http_method": "head"}), ) self.assertEqual(span.attributes["http.method"], "HEAD") self.assertEqual(span.attributes["aws.operation"], "head_bucket") @@ -146,7 +146,7 @@ def test_s3_client(self): span = spans[2] self.assertEqual( span.resource, - Resource(labels={"endpoint": "s3", "http_method": "head"}), + Resource(attributes={"endpoint": "s3", "http_method": "head"}), ) @mock_s3_deprecated @@ -166,13 +166,13 @@ def test_s3_put(self): assert_span_http_status_code(spans[0], 200) self.assertEqual( spans[0].resource, - Resource(labels={"endpoint": "s3", "http_method": "put"}), + Resource(attributes={"endpoint": "s3", "http_method": "put"}), ) # get bucket self.assertEqual(spans[1].attributes["aws.operation"], "head_bucket") self.assertEqual( spans[1].resource, - Resource(labels={"endpoint": "s3", "http_method": "head"}), + Resource(attributes={"endpoint": "s3", "http_method": "head"}), ) # put object self.assertEqual( @@ -180,7 +180,7 @@ def test_s3_put(self): ) self.assertEqual( spans[2].resource, - Resource(labels={"endpoint": "s3", "http_method": "put"}), + Resource(attributes={"endpoint": "s3", "http_method": "put"}), ) @mock_lambda_deprecated @@ -223,7 +223,7 @@ def test_lambda_client(self): assert_span_http_status_code(span, 200) self.assertEqual( span.resource, - Resource(labels={"endpoint": "lambda", "http_method": "get"}), + Resource(attributes={"endpoint": "lambda", "http_method": "get"}), ) self.assertEqual(span.attributes["http.method"], "GET") self.assertEqual(span.attributes["aws.region"], "us-east-2") @@ -241,7 +241,10 @@ def test_sts_client(self): self.assertEqual( span.resource, Resource( - labels={"endpoint": "sts", "http_method": "getfederationtoken"} + attributes={ + "endpoint": "sts", + "http_method": "getfederationtoken", + } ), ) self.assertEqual(span.attributes["aws.region"], "us-west-2") @@ -268,6 +271,6 @@ def test_elasticache_client(self): assert spans span = spans[0] self.assertEqual( - span.resource, Resource(labels={"endpoint": "elasticcache"}) + span.resource, Resource(attributes={"endpoint": "elasticcache"}) ) self.assertEqual(span.attributes["aws.region"], "us-west-2") diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py index 9b9b1e9a80..d716c90d68 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py @@ -100,14 +100,16 @@ def _patched_api_call(self, original_func, instance, args, kwargs): if args: operation = args[0] span.resource = Resource( - labels={ + attributes={ "endpoint": endpoint_name, "operation": operation.lower(), } ) else: - span.resource = Resource(labels={"endpoint": endpoint_name}) + span.resource = Resource( + attributes={"endpoint": endpoint_name} + ) add_span_arg_tags( span, diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py index 47073478fc..541917344f 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py @@ -53,7 +53,10 @@ def test_traced_client(self): self.assertEqual( span.resource, Resource( - labels={"endpoint": "ec2", "operation": "describeinstances"} + attributes={ + "endpoint": "ec2", + "operation": "describeinstances", + } ), ) self.assertEqual(span.name, "ec2.command") @@ -81,7 +84,9 @@ def test_s3_client(self): assert_span_http_status_code(span, 200) self.assertEqual( span.resource, - Resource(labels={"endpoint": "s3", "operation": "listbuckets"}), + Resource( + attributes={"endpoint": "s3", "operation": "listbuckets"} + ), ) # testing for span error @@ -93,7 +98,9 @@ def test_s3_client(self): span = spans[2] self.assertEqual( span.resource, - Resource(labels={"endpoint": "s3", "operation": "listobjects"}), + Resource( + attributes={"endpoint": "s3", "operation": "listobjects"} + ), ) @mock_s3 @@ -111,12 +118,14 @@ def test_s3_put(self): assert_span_http_status_code(span, 200) self.assertEqual( span.resource, - Resource(labels={"endpoint": "s3", "operation": "createbucket"}), + Resource( + attributes={"endpoint": "s3", "operation": "createbucket"} + ), ) self.assertEqual(spans[1].attributes["aws.operation"], "PutObject") self.assertEqual( spans[1].resource, - Resource(labels={"endpoint": "s3", "operation": "putobject"}), + Resource(attributes={"endpoint": "s3", "operation": "putobject"}), ) self.assertEqual(spans[1].attributes["params.Key"], str(params["Key"])) self.assertEqual( @@ -139,7 +148,9 @@ def test_sqs_client(self): assert_span_http_status_code(span, 200) self.assertEqual( span.resource, - Resource(labels={"endpoint": "sqs", "operation": "listqueues"}), + Resource( + attributes={"endpoint": "sqs", "operation": "listqueues"} + ), ) @mock_kinesis @@ -160,7 +171,7 @@ def test_kinesis_client(self): self.assertEqual( span.resource, Resource( - labels={"endpoint": "kinesis", "operation": "liststreams"} + attributes={"endpoint": "kinesis", "operation": "liststreams"} ), ) @@ -205,7 +216,7 @@ def test_lambda_client(self): self.assertEqual( span.resource, Resource( - labels={"endpoint": "lambda", "operation": "listfunctions"} + attributes={"endpoint": "lambda", "operation": "listfunctions"} ), ) @@ -224,7 +235,7 @@ def test_kms_client(self): assert_span_http_status_code(span, 200) self.assertEqual( span.resource, - Resource(labels={"endpoint": "kms", "operation": "listkeys"}), + Resource(attributes={"endpoint": "kms", "operation": "listkeys"}), ) # checking for protection on sts against security leak diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 214766253e..e31387a0c4 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -12,6 +12,8 @@ ([#1062](https://github.com/open-telemetry/opentelemetry-python/pull/1062)) - Populate resource attributes as per semantic conventions ([#1053](https://github.com/open-telemetry/opentelemetry-python/pull/1053)) +- Rename Resource labels to attributes + ([#1082](https://github.com/open-telemetry/opentelemetry-python/pull/1082)) ## Version 0.12b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 3053a18867..e14d781168 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -22,7 +22,7 @@ import pkg_resources LabelValue = typing.Union[str, bool, int, float] -Labels = typing.Dict[str, LabelValue] +Attributes = typing.Dict[str, LabelValue] logger = logging.getLogger(__name__) @@ -36,38 +36,38 @@ class Resource: - def __init__(self, labels: Labels): - self._labels = labels.copy() + def __init__(self, attributes: Attributes): + self._attributes = attributes.copy() @staticmethod - def create(labels: Labels) -> "Resource": - if not labels: + def create(attributes: Attributes) -> "Resource": + if not attributes: return _DEFAULT_RESOURCE - return _DEFAULT_RESOURCE.merge(Resource(labels)) + return _DEFAULT_RESOURCE.merge(Resource(attributes)) @staticmethod def create_empty() -> "Resource": return _EMPTY_RESOURCE @property - def labels(self) -> Labels: - return self._labels.copy() + def attributes(self) -> Attributes: + return self._attributes.copy() def merge(self, other: "Resource") -> "Resource": - merged_labels = self.labels + merged_attributes = self.attributes # pylint: disable=protected-access - for key, value in other._labels.items(): - if key not in merged_labels or merged_labels[key] == "": - merged_labels[key] = value - return Resource(merged_labels) + for key, value in other._attributes.items(): + if key not in merged_attributes or merged_attributes[key] == "": + merged_attributes[key] = value + return Resource(merged_attributes) def __eq__(self, other: object) -> bool: if not isinstance(other, Resource): return False - return self._labels == other._labels + return self._attributes == other._attributes def __hash__(self): - return hash(dumps(self._labels, sort_keys=True)) + return hash(dumps(self._attributes, sort_keys=True)) _EMPTY_RESOURCE = Resource({}) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index e42f4ceb0b..13819ed35b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -506,7 +506,7 @@ def to_json(self, indent=4): f_span["attributes"] = self._format_attributes(self.attributes) f_span["events"] = self._format_events(self.events) f_span["links"] = self._format_links(self.links) - f_span["resource"] = self.resource.labels + f_span["resource"] = self.resource.attributes return json.dumps(f_span, indent=indent) diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index be677f19fc..3166e3350e 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -23,14 +23,14 @@ class TestResources(unittest.TestCase): def test_create(self): - labels = { + attributes = { "service": "ui", "version": 1, "has_bugs": True, "cost": 112.12, } - expected_labels = { + expected_attributes = { "service": "ui", "version": 1, "has_bugs": True, @@ -40,9 +40,9 @@ def test_create(self): resources.TELEMETRY_SDK_VERSION: resources.OPENTELEMETRY_SDK_VERSION, } - resource = resources.Resource.create(labels) + resource = resources.Resource.create(attributes) self.assertIsInstance(resource, resources.Resource) - self.assertEqual(resource.labels, expected_labels) + self.assertEqual(resource.attributes, expected_attributes) resource = resources.Resource.create_empty() self.assertIs(resource, resources._EMPTY_RESOURCE) @@ -64,7 +64,7 @@ def test_resource_merge(self): def test_resource_merge_empty_string(self): """Verify Resource.merge behavior with the empty string. - Labels from the source Resource take precedence, with + Attributes from the source Resource take precedence, with the exception of the empty string. """ @@ -78,30 +78,30 @@ def test_resource_merge_empty_string(self): ) def test_immutability(self): - labels = { + attributes = { "service": "ui", "version": 1, "has_bugs": True, "cost": 112.12, } - default_labels = { + default_attributes = { resources.TELEMETRY_SDK_NAME: "opentelemetry", resources.TELEMETRY_SDK_LANGUAGE: "python", resources.TELEMETRY_SDK_VERSION: resources.OPENTELEMETRY_SDK_VERSION, } - labels_copy = labels.copy() - labels_copy.update(default_labels) + attributes_copy = attributes.copy() + attributes_copy.update(default_attributes) - resource = resources.Resource.create(labels) - self.assertEqual(resource.labels, labels_copy) + resource = resources.Resource.create(attributes) + self.assertEqual(resource.attributes, attributes_copy) - resource.labels["has_bugs"] = False - self.assertEqual(resource.labels, labels_copy) + resource.attributes["has_bugs"] = False + self.assertEqual(resource.attributes, attributes_copy) - labels["cost"] = 999.91 - self.assertEqual(resource.labels, labels_copy) + attributes["cost"] = 999.91 + self.assertEqual(resource.attributes, attributes_copy) def test_aggregated_resources_no_detectors(self): aggregated_resources = resources.get_aggregated_resources([]) From 21994dfbaae462276bb57809c39cf3d382eea488 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 9 Sep 2020 13:23:56 -0700 Subject: [PATCH 0536/1517] Rename HTTPTextFormat to TextMapPropagator (#1085) --- docs/conf.py | 9 ++--- docs/examples/datadog_exporter/server.py | 12 +++--- docs/getting-started.rst | 2 +- .../exporter/datadog/__init__.py | 2 +- .../exporter/datadog/propagator.py | 20 +++++----- .../opentracing_shim/__init__.py | 4 +- .../tests/test_shim.py | 40 +++++++++++-------- .../tests/test_requests_integration.py | 16 ++++---- opentelemetry-api/CHANGELOG.md | 3 ++ .../baggage/propagation/__init__.py | 22 +++++----- .../src/opentelemetry/propagators/__init__.py | 32 +++++++-------- .../opentelemetry/propagators/composite.py | 20 +++++----- .../{httptextformat.py => textmap.py} | 18 ++++----- ...ntexthttptextformat.py => tracecontext.py} | 18 ++++----- .../test_tracecontexthttptextformat.py | 4 +- .../sdk/trace/propagation/b3_format.py | 20 +++++----- ...mock_httptextformat.py => mock_textmap.py} | 26 ++++++------ 17 files changed, 132 insertions(+), 136 deletions(-) rename opentelemetry-api/src/opentelemetry/trace/propagation/{httptextformat.py => textmap.py} (85%) rename opentelemetry-api/src/opentelemetry/trace/propagation/{tracecontexthttptextformat.py => tracecontext.py} (91%) rename tests/util/src/opentelemetry/test/{mock_httptextformat.py => mock_textmap.py} (80%) diff --git a/docs/conf.py b/docs/conf.py index 4b9753c96c..d15d8b2ed5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -103,17 +103,14 @@ # with "class reference target not found: ObjectProxy". ("py:class", "ObjectProxy"), # TODO: Understand why sphinx is not able to find this local class - ( - "py:class", - "opentelemetry.trace.propagation.httptextformat.HTTPTextFormat", - ), + ("py:class", "opentelemetry.trace.propagation.textmap.TextMapPropagator",), ( "any", - "opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract", + "opentelemetry.trace.propagation.textmap.TextMapPropagator.extract", ), ( "any", - "opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject", + "opentelemetry.trace.propagation.textmap.TextMapPropagator.inject", ), ] diff --git a/docs/examples/datadog_exporter/server.py b/docs/examples/datadog_exporter/server.py index 15d10f3493..9c83de8bb8 100644 --- a/docs/examples/datadog_exporter/server.py +++ b/docs/examples/datadog_exporter/server.py @@ -35,19 +35,19 @@ ) # append Datadog format for propagation to and from Datadog instrumented services -global_httptextformat = propagators.get_global_httptextformat() +global_textmap = propagators.get_global_textmap() if isinstance( - global_httptextformat, propagators.composite.CompositeHTTPPropagator + global_textmap, propagators.composite.CompositeHTTPPropagator ) and not any( - isinstance(p, DatadogFormat) for p in global_httptextformat._propagators + isinstance(p, DatadogFormat) for p in global_textmap._propagators ): - propagators.set_global_httptextformat( + propagators.set_global_textmap( propagators.composite.CompositeHTTPPropagator( - global_httptextformat._propagators + [DatadogFormat()] + global_textmap._propagators + [DatadogFormat()] ) ) else: - propagators.set_global_httptextformat(DatadogFormat()) + propagators.set_global_textmap(DatadogFormat()) tracer = trace.get_tracer(__name__) diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 4d643eca62..213989fbe5 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -194,7 +194,7 @@ an example using Zipkin's `b3 propagation Context: trace_id = extract_first_element( @@ -81,8 +81,8 @@ def extract( def inject( self, - set_in_carrier: Setter[HTTPTextFormatT], - carrier: HTTPTextFormatT, + set_in_carrier: Setter[TextMapPropagatorT], + carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> None: span = get_current_span(context) @@ -120,8 +120,8 @@ def format_span_id(span_id: int) -> str: def extract_first_element( - items: typing.Iterable[HTTPTextFormatT], -) -> typing.Optional[HTTPTextFormatT]: + items: typing.Iterable[TextMapPropagatorT], +) -> typing.Optional[TextMapPropagatorT]: if items is None: return None return next(iter(items), None) diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py index 78672444bd..6bb22130d8 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py @@ -676,7 +676,7 @@ def inject(self, span_context, format: object, carrier: object): if format not in self._supported_formats: raise UnsupportedFormatException - propagator = propagators.get_global_httptextformat() + propagator = propagators.get_global_textmap() ctx = set_span_in_context(DefaultSpan(span_context.unwrap())) propagator.inject(type(carrier).__setitem__, carrier, context=ctx) @@ -710,7 +710,7 @@ def get_as_list(dict_object, key): value = dict_object.get(key) return [value] if value is not None else [] - propagator = propagators.get_global_httptextformat() + propagator = propagators.get_global_textmap() ctx = propagator.extract(get_as_list, carrier) span = get_current_span(ctx) if span is not None: diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py index c880913a87..672e7b02f9 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py @@ -29,9 +29,9 @@ util, ) from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.test.mock_httptextformat import ( - MockHTTPTextFormat, - NOOPHTTPTextFormat, +from opentelemetry.test.mock_textmap import ( + MockTextMapPropagator, + NOOPTextMapPropagator, ) @@ -46,15 +46,15 @@ def setUp(self): @classmethod def setUpClass(cls): # Save current propagator to be restored on teardown. - cls._previous_propagator = propagators.get_global_httptextformat() + cls._previous_propagator = propagators.get_global_textmap() # Set mock propagator for testing. - propagators.set_global_httptextformat(MockHTTPTextFormat()) + propagators.set_global_textmap(MockTextMapPropagator()) @classmethod def tearDownClass(cls): # Restore previous propagator. - propagators.set_global_httptextformat(cls._previous_propagator) + propagators.set_global_textmap(cls._previous_propagator) def test_shim_type(self): # Verify shim is an OpenTracing tracer. @@ -482,8 +482,10 @@ def test_inject_http_headers(self): headers = {} self.shim.inject(context, opentracing.Format.HTTP_HEADERS, headers) - self.assertEqual(headers[MockHTTPTextFormat.TRACE_ID_KEY], str(1220)) - self.assertEqual(headers[MockHTTPTextFormat.SPAN_ID_KEY], str(7478)) + self.assertEqual( + headers[MockTextMapPropagator.TRACE_ID_KEY], str(1220) + ) + self.assertEqual(headers[MockTextMapPropagator.SPAN_ID_KEY], str(7478)) def test_inject_text_map(self): """Test `inject()` method for Format.TEXT_MAP.""" @@ -496,8 +498,12 @@ def test_inject_text_map(self): # Verify Format.TEXT_MAP text_map = {} self.shim.inject(context, opentracing.Format.TEXT_MAP, text_map) - self.assertEqual(text_map[MockHTTPTextFormat.TRACE_ID_KEY], str(1220)) - self.assertEqual(text_map[MockHTTPTextFormat.SPAN_ID_KEY], str(7478)) + self.assertEqual( + text_map[MockTextMapPropagator.TRACE_ID_KEY], str(1220) + ) + self.assertEqual( + text_map[MockTextMapPropagator.SPAN_ID_KEY], str(7478) + ) def test_inject_binary(self): """Test `inject()` method for Format.BINARY.""" @@ -515,8 +521,8 @@ def test_extract_http_headers(self): """Test `extract()` method for Format.HTTP_HEADERS.""" carrier = { - MockHTTPTextFormat.TRACE_ID_KEY: 1220, - MockHTTPTextFormat.SPAN_ID_KEY: 7478, + MockTextMapPropagator.TRACE_ID_KEY: 1220, + MockTextMapPropagator.SPAN_ID_KEY: 7478, } ctx = self.shim.extract(opentracing.Format.HTTP_HEADERS, carrier) @@ -527,22 +533,22 @@ def test_extract_empty_context_returns_invalid_context(self): """In the case where the propagator cannot extract a SpanContext, extract should return and invalid span context. """ - _old_propagator = propagators.get_global_httptextformat() - propagators.set_global_httptextformat(NOOPHTTPTextFormat()) + _old_propagator = propagators.get_global_textmap() + propagators.set_global_textmap(NOOPTextMapPropagator()) try: carrier = {} ctx = self.shim.extract(opentracing.Format.HTTP_HEADERS, carrier) self.assertEqual(ctx.unwrap(), trace.INVALID_SPAN_CONTEXT) finally: - propagators.set_global_httptextformat(_old_propagator) + propagators.set_global_textmap(_old_propagator) def test_extract_text_map(self): """Test `extract()` method for Format.TEXT_MAP.""" carrier = { - MockHTTPTextFormat.TRACE_ID_KEY: 1220, - MockHTTPTextFormat.SPAN_ID_KEY: 7478, + MockTextMapPropagator.TRACE_ID_KEY: 1220, + MockTextMapPropagator.SPAN_ID_KEY: 7478, } ctx = self.shim.extract(opentracing.Format.TEXT_MAP, carrier) diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py index 0e0492f47e..41f5bc39d9 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py @@ -22,7 +22,7 @@ from opentelemetry import context, propagators, trace from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.sdk import resources -from opentelemetry.test.mock_httptextformat import MockHTTPTextFormat +from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase from opentelemetry.trace.status import StatusCanonicalCode @@ -148,28 +148,28 @@ def test_suppress_instrumentation(self): self.assert_span(num_spans=0) def test_distributed_context(self): - previous_propagator = propagators.get_global_httptextformat() + previous_propagator = propagators.get_global_textmap() try: - propagators.set_global_httptextformat(MockHTTPTextFormat()) + propagators.set_global_textmap(MockTextMapPropagator()) result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") span = self.assert_span() headers = dict(httpretty.last_request().headers) - self.assertIn(MockHTTPTextFormat.TRACE_ID_KEY, headers) + self.assertIn(MockTextMapPropagator.TRACE_ID_KEY, headers) self.assertEqual( str(span.get_context().trace_id), - headers[MockHTTPTextFormat.TRACE_ID_KEY], + headers[MockTextMapPropagator.TRACE_ID_KEY], ) - self.assertIn(MockHTTPTextFormat.SPAN_ID_KEY, headers) + self.assertIn(MockTextMapPropagator.SPAN_ID_KEY, headers) self.assertEqual( str(span.get_context().span_id), - headers[MockHTTPTextFormat.SPAN_ID_KEY], + headers[MockTextMapPropagator.SPAN_ID_KEY], ) finally: - propagators.set_global_httptextformat(previous_propagator) + propagators.set_global_textmap(previous_propagator) def test_span_callback(self): RequestsInstrumentor().uninstrument() diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 5ee9d96e97..1f9ba4707f 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -14,6 +14,9 @@ ([#1045](https://github.com/open-telemetry/opentelemetry-python/pull/1045)) - Rename CorrelationContext to Baggage ([#1060](https://github.com/open-telemetry/opentelemetry-python/pull/1060)) +- Rename HTTPTextFormat to TextMapPropagator. This change also updates `get_global_httptextformat` and + `set_global_httptextformat` to `get_global_textmap` and `set_global_textmap` + ([#1085](https://github.com/open-telemetry/opentelemetry-python/pull/1085)) ## Version 0.12b0 diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index 4d0009892a..fb14ab9567 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -18,10 +18,10 @@ from opentelemetry import baggage from opentelemetry.context import get_current from opentelemetry.context.context import Context -from opentelemetry.trace.propagation import httptextformat +from opentelemetry.trace.propagation import textmap -class BaggagePropagator(httptextformat.HTTPTextFormat): +class BaggagePropagator(textmap.TextMapPropagator): MAX_HEADER_LENGTH = 8192 MAX_PAIR_LENGTH = 4096 MAX_PAIRS = 180 @@ -29,16 +29,14 @@ class BaggagePropagator(httptextformat.HTTPTextFormat): def extract( self, - get_from_carrier: httptextformat.Getter[ - httptextformat.HTTPTextFormatT - ], - carrier: httptextformat.HTTPTextFormatT, + get_from_carrier: textmap.Getter[textmap.TextMapPropagatorT], + carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: """Extract Baggage from the carrier. See - `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract` + `opentelemetry.trace.propagation.textmap.TextMapPropagator.extract` """ if context is None: @@ -73,14 +71,14 @@ def extract( def inject( self, - set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], - carrier: httptextformat.HTTPTextFormatT, + set_in_carrier: textmap.Setter[textmap.TextMapPropagatorT], + carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> None: """Injects Baggage into the carrier. See - `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` + `opentelemetry.trace.propagation.textmap.TextMapPropagator.inject` """ baggage_entries = baggage.get_all(context=context) if not baggage_entries: @@ -100,8 +98,8 @@ def _format_baggage(baggage_entries: typing.Mapping[str, object]) -> str: def _extract_first_element( - items: typing.Iterable[httptextformat.HTTPTextFormatT], -) -> typing.Optional[httptextformat.HTTPTextFormatT]: + items: typing.Iterable[textmap.TextMapPropagatorT], +) -> typing.Optional[textmap.TextMapPropagatorT]: if items is None: return None return next(iter(items), None) diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index 8f5a38df76..f34e3c588b 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -22,7 +22,7 @@ from opentelemetry import propagators - PROPAGATOR = propagators.get_global_httptextformat() + PROPAGATOR = propagators.get_global_textmap() def get_header_from_flask_request(request, key): @@ -58,15 +58,15 @@ def example_route(): from opentelemetry.baggage.propagation import BaggagePropagator from opentelemetry.context.context import Context from opentelemetry.propagators import composite -from opentelemetry.trace.propagation import httptextformat -from opentelemetry.trace.propagation.tracecontexthttptextformat import ( - TraceContextHTTPTextFormat, +from opentelemetry.trace.propagation import textmap +from opentelemetry.trace.propagation.tracecontext import ( + TraceContextTextMapPropagator, ) def extract( - get_from_carrier: httptextformat.Getter[httptextformat.HTTPTextFormatT], - carrier: httptextformat.HTTPTextFormatT, + get_from_carrier: textmap.Getter[textmap.TextMapPropagatorT], + carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: """ Uses the configured propagator to extract a Context from the carrier. @@ -82,14 +82,12 @@ def extract( context: an optional Context to use. Defaults to current context if not set. """ - return get_global_httptextformat().extract( - get_from_carrier, carrier, context - ) + return get_global_textmap().extract(get_from_carrier, carrier, context) def inject( - set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], - carrier: httptextformat.HTTPTextFormatT, + set_in_carrier: textmap.Setter[textmap.TextMapPropagatorT], + carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> None: """ Uses the configured propagator to inject a Context into the carrier. @@ -103,20 +101,18 @@ def inject( context: an optional Context to use. Defaults to current context if not set. """ - get_global_httptextformat().inject(set_in_carrier, carrier, context) + get_global_textmap().inject(set_in_carrier, carrier, context) _HTTP_TEXT_FORMAT = composite.CompositeHTTPPropagator( - [TraceContextHTTPTextFormat(), BaggagePropagator()], -) # type: httptextformat.HTTPTextFormat + [TraceContextTextMapPropagator(), BaggagePropagator()], +) # type: textmap.TextMapPropagator -def get_global_httptextformat() -> httptextformat.HTTPTextFormat: +def get_global_textmap() -> textmap.TextMapPropagator: return _HTTP_TEXT_FORMAT -def set_global_httptextformat( - http_text_format: httptextformat.HTTPTextFormat, -) -> None: +def set_global_textmap(http_text_format: textmap.TextMapPropagator,) -> None: global _HTTP_TEXT_FORMAT # pylint:disable=global-statement _HTTP_TEXT_FORMAT = http_text_format diff --git a/opentelemetry-api/src/opentelemetry/propagators/composite.py b/opentelemetry-api/src/opentelemetry/propagators/composite.py index 50fba01423..3499d2ea08 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/composite.py +++ b/opentelemetry-api/src/opentelemetry/propagators/composite.py @@ -15,12 +15,12 @@ import typing from opentelemetry.context.context import Context -from opentelemetry.trace.propagation import httptextformat +from opentelemetry.trace.propagation import textmap logger = logging.getLogger(__name__) -class CompositeHTTPPropagator(httptextformat.HTTPTextFormat): +class CompositeHTTPPropagator(textmap.TextMapPropagator): """ CompositeHTTPPropagator provides a mechanism for combining multiple propagators into a single one. @@ -29,16 +29,14 @@ class CompositeHTTPPropagator(httptextformat.HTTPTextFormat): """ def __init__( - self, propagators: typing.Sequence[httptextformat.HTTPTextFormat] + self, propagators: typing.Sequence[textmap.TextMapPropagator] ) -> None: self._propagators = propagators def extract( self, - get_from_carrier: httptextformat.Getter[ - httptextformat.HTTPTextFormatT - ], - carrier: httptextformat.HTTPTextFormatT, + get_from_carrier: textmap.Getter[textmap.TextMapPropagatorT], + carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: """ Run each of the configured propagators with the given context and carrier. @@ -46,7 +44,7 @@ def extract( propagators write the same context key, the propagator later in the list will override previous propagators. - See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract` + See `opentelemetry.trace.propagation.textmap.TextMapPropagator.extract` """ for propagator in self._propagators: context = propagator.extract(get_from_carrier, carrier, context) @@ -54,8 +52,8 @@ def extract( def inject( self, - set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], - carrier: httptextformat.HTTPTextFormatT, + set_in_carrier: textmap.Setter[textmap.TextMapPropagatorT], + carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> None: """ Run each of the configured propagators with the given context and carrier. @@ -63,7 +61,7 @@ def inject( propagators write the same carrier key, the propagator later in the list will override previous propagators. - See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` + See `opentelemetry.trace.propagation.textmap.TextMapPropagator.inject` """ for propagator in self._propagators: propagator.inject(set_in_carrier, carrier, context) diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py similarity index 85% rename from opentelemetry-api/src/opentelemetry/trace/propagation/httptextformat.py rename to opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py index e15e2a0e6d..6f9ed897e1 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py @@ -17,16 +17,16 @@ from opentelemetry.context.context import Context -HTTPTextFormatT = typing.TypeVar("HTTPTextFormatT") +TextMapPropagatorT = typing.TypeVar("TextMapPropagatorT") -Setter = typing.Callable[[HTTPTextFormatT, str, str], None] -Getter = typing.Callable[[HTTPTextFormatT, str], typing.List[str]] +Setter = typing.Callable[[TextMapPropagatorT, str, str], None] +Getter = typing.Callable[[TextMapPropagatorT, str], typing.List[str]] -class HTTPTextFormat(abc.ABC): +class TextMapPropagator(abc.ABC): """This class provides an interface that enables extracting and injecting context into headers of HTTP requests. HTTP frameworks and clients - can integrate with HTTPTextFormat by providing the object containing the + can integrate with TextMapPropagator by providing the object containing the headers, and a getter and setter function for the extraction and injection of values, respectively. @@ -35,8 +35,8 @@ class HTTPTextFormat(abc.ABC): @abc.abstractmethod def extract( self, - get_from_carrier: Getter[HTTPTextFormatT], - carrier: HTTPTextFormatT, + get_from_carrier: Getter[TextMapPropagatorT], + carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: """Create a Context from values in the carrier. @@ -63,8 +63,8 @@ def extract( @abc.abstractmethod def inject( self, - set_in_carrier: Setter[HTTPTextFormatT], - carrier: HTTPTextFormatT, + set_in_carrier: Setter[TextMapPropagatorT], + carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> None: """Inject values from a Context into a carrier. diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py similarity index 91% rename from opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py rename to opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index 1cfd0704e2..8627b9a65c 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -17,7 +17,7 @@ import opentelemetry.trace as trace from opentelemetry.context.context import Context -from opentelemetry.trace.propagation import httptextformat +from opentelemetry.trace.propagation import textmap # Keys and values are strings of up to 256 printable US-ASCII characters. # Implementations should conform to the `W3C Trace Context - Tracestate`_ @@ -46,7 +46,7 @@ _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS = 32 -class TraceContextHTTPTextFormat(httptextformat.HTTPTextFormat): +class TraceContextTextMapPropagator(textmap.TextMapPropagator): """Extracts and injects using w3c TraceContext's headers. """ @@ -60,15 +60,13 @@ class TraceContextHTTPTextFormat(httptextformat.HTTPTextFormat): def extract( self, - get_from_carrier: httptextformat.Getter[ - httptextformat.HTTPTextFormatT - ], - carrier: httptextformat.HTTPTextFormatT, + get_from_carrier: textmap.Getter[textmap.TextMapPropagatorT], + carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: """Extracts SpanContext from the carrier. - See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract` + See `opentelemetry.trace.propagation.textmap.TextMapPropagator.extract` """ header = get_from_carrier(carrier, self._TRACEPARENT_HEADER_NAME) @@ -111,13 +109,13 @@ def extract( def inject( self, - set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], - carrier: httptextformat.HTTPTextFormatT, + set_in_carrier: textmap.Setter[textmap.TextMapPropagatorT], + carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> None: """Injects SpanContext into the carrier. - See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` + See `opentelemetry.trace.propagation.textmap.TextMapPropagator.inject` """ span = trace.get_current_span(context) span_context = span.get_context() diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 5adc180d9f..8abe419387 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -16,9 +16,9 @@ import unittest from opentelemetry import trace -from opentelemetry.trace.propagation import tracecontexthttptextformat +from opentelemetry.trace.propagation import tracecontext -FORMAT = tracecontexthttptextformat.TraceContextHTTPTextFormat() +FORMAT = tracecontext.TraceContextTextMapPropagator() def get_as_list( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py index 901a5772f8..f6d3345ed7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py @@ -18,15 +18,15 @@ import opentelemetry.trace as trace from opentelemetry.context import Context from opentelemetry.sdk.trace import generate_span_id, generate_trace_id -from opentelemetry.trace.propagation.httptextformat import ( +from opentelemetry.trace.propagation.textmap import ( Getter, - HTTPTextFormat, - HTTPTextFormatT, Setter, + TextMapPropagator, + TextMapPropagatorT, ) -class B3Format(HTTPTextFormat): +class B3Format(TextMapPropagator): """Propagator for the B3 HTTP header format. See: https://github.com/openzipkin/b3-propagation @@ -44,8 +44,8 @@ class B3Format(HTTPTextFormat): def extract( self, - get_from_carrier: Getter[HTTPTextFormatT], - carrier: HTTPTextFormatT, + get_from_carrier: Getter[TextMapPropagatorT], + carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: trace_id = format_trace_id(trace.INVALID_TRACE_ID) @@ -134,8 +134,8 @@ def extract( def inject( self, - set_in_carrier: Setter[HTTPTextFormatT], - carrier: HTTPTextFormatT, + set_in_carrier: Setter[TextMapPropagatorT], + carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> None: span = trace.get_current_span(context=context) @@ -170,8 +170,8 @@ def format_span_id(span_id: int) -> str: def _extract_first_element( - items: typing.Iterable[HTTPTextFormatT], -) -> typing.Optional[HTTPTextFormatT]: + items: typing.Iterable[TextMapPropagatorT], +) -> typing.Optional[TextMapPropagatorT]: if items is None: return None return next(iter(items), None) diff --git a/tests/util/src/opentelemetry/test/mock_httptextformat.py b/tests/util/src/opentelemetry/test/mock_textmap.py similarity index 80% rename from tests/util/src/opentelemetry/test/mock_httptextformat.py rename to tests/util/src/opentelemetry/test/mock_textmap.py index 76165c3e4b..92c0f21f0e 100644 --- a/tests/util/src/opentelemetry/test/mock_httptextformat.py +++ b/tests/util/src/opentelemetry/test/mock_textmap.py @@ -16,15 +16,15 @@ from opentelemetry import trace from opentelemetry.context import Context, get_current -from opentelemetry.trace.propagation.httptextformat import ( +from opentelemetry.trace.propagation.textmap import ( Getter, - HTTPTextFormat, - HTTPTextFormatT, Setter, + TextMapPropagator, + TextMapPropagatorT, ) -class NOOPHTTPTextFormat(HTTPTextFormat): +class NOOPTextMapPropagator(TextMapPropagator): """A propagator that does not extract nor inject. This class is useful for catching edge cases assuming @@ -33,22 +33,22 @@ class NOOPHTTPTextFormat(HTTPTextFormat): def extract( self, - get_from_carrier: Getter[HTTPTextFormatT], - carrier: HTTPTextFormatT, + get_from_carrier: Getter[TextMapPropagatorT], + carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: return get_current() def inject( self, - set_in_carrier: Setter[HTTPTextFormatT], - carrier: HTTPTextFormatT, + set_in_carrier: Setter[TextMapPropagatorT], + carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> None: return None -class MockHTTPTextFormat(HTTPTextFormat): +class MockTextMapPropagator(TextMapPropagator): """Mock propagator for testing purposes.""" TRACE_ID_KEY = "mock-traceid" @@ -56,8 +56,8 @@ class MockHTTPTextFormat(HTTPTextFormat): def extract( self, - get_from_carrier: Getter[HTTPTextFormatT], - carrier: HTTPTextFormatT, + get_from_carrier: Getter[TextMapPropagatorT], + carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: trace_id_list = get_from_carrier(carrier, self.TRACE_ID_KEY) @@ -78,8 +78,8 @@ def extract( def inject( self, - set_in_carrier: Setter[HTTPTextFormatT], - carrier: HTTPTextFormatT, + set_in_carrier: Setter[TextMapPropagatorT], + carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> None: span = trace.get_current_span(context) From 6be4ae3648b8c8c6a4b9ba8decaeb30eeb1e6195 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 9 Sep 2020 14:04:19 -0700 Subject: [PATCH 0537/1517] Comment out botocore test (#1089) --- .../tests/test_botocore_instrumentation.py | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py index 541917344f..64c9d02402 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py @@ -103,35 +103,36 @@ def test_s3_client(self): ), ) - @mock_s3 - def test_s3_put(self): - params = dict(Key="foo", Bucket="mybucket", Body=b"bar") - s3 = self.session.create_client("s3", region_name="us-west-2") - s3.create_bucket(Bucket="mybucket") - s3.put_object(**params) - - spans = self.memory_exporter.get_finished_spans() - assert spans - span = spans[0] - self.assertEqual(len(spans), 2) - self.assertEqual(span.attributes["aws.operation"], "CreateBucket") - assert_span_http_status_code(span, 200) - self.assertEqual( - span.resource, - Resource( - attributes={"endpoint": "s3", "operation": "createbucket"} - ), - ) - self.assertEqual(spans[1].attributes["aws.operation"], "PutObject") - self.assertEqual( - spans[1].resource, - Resource(attributes={"endpoint": "s3", "operation": "putobject"}), - ) - self.assertEqual(spans[1].attributes["params.Key"], str(params["Key"])) - self.assertEqual( - spans[1].attributes["params.Bucket"], str(params["Bucket"]) - ) - self.assertTrue("params.Body" not in spans[1].attributes.keys()) + # Comment test for issue 1088 + # @mock_s3 + # def test_s3_put(self): + # params = dict(Key="foo", Bucket="mybucket", Body=b"bar") + # s3 = self.session.create_client("s3", region_name="us-west-2") + # s3.create_bucket(Bucket="mybucket") + # s3.put_object(**params) + + # spans = self.memory_exporter.get_finished_spans() + # assert spans + # span = spans[0] + # self.assertEqual(len(spans), 2) + # self.assertEqual(span.attributes["aws.operation"], "CreateBucket") + # assert_span_http_status_code(span, 200) + # self.assertEqual( + # span.resource, + # Resource( + # attributes={"endpoint": "s3", "operation": "createbucket"} + # ), + # ) + # self.assertEqual(spans[1].attributes["aws.operation"], "PutObject") + # self.assertEqual( + # spans[1].resource, + # Resource(attributes={"endpoint": "s3", "operation": "putobject"}), + # ) + # self.assertEqual(spans[1].attributes["params.Key"], str(params["Key"])) + # self.assertEqual( + # spans[1].attributes["params.Bucket"], str(params["Bucket"]) + # ) + # self.assertTrue("params.Body" not in spans[1].attributes.keys()) @mock_sqs def test_sqs_client(self): From 4726bbfe8a52987ee66fc2a7281fbc30907986f8 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 9 Sep 2020 14:21:22 -0700 Subject: [PATCH 0538/1517] SpanExporter to not receive span if not sampled (#1070) --- .../sdk/trace/export/__init__.py | 5 ++ .../tests/trace/export/test_export.py | 52 ++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 7c1e51f3ec..857537b90a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -21,6 +21,7 @@ from enum import Enum from opentelemetry.context import attach, detach, set_value +from opentelemetry.sdk.trace import sampling from opentelemetry.util import time_ns from .. import Span, SpanProcessor @@ -74,6 +75,8 @@ def on_start(self, span: Span) -> None: pass def on_end(self, span: Span) -> None: + if not span.context.trace_flags.sampled: + return token = attach(set_value("suppress_instrumentation", True)) try: self.span_exporter.export((span,)) @@ -156,6 +159,8 @@ def on_end(self, span: Span) -> None: if self.done: logger.warning("Already shutdown, dropping span.") return + if not span.context.trace_flags.sampled: + return if len(self.queue) == self.max_queue_size: if not self._spans_dropped: logger.warning("Queue is full, likely spans will be dropped.") diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 34e1d14d23..e6fcdb9c22 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -100,11 +100,35 @@ def test_simple_span_processor_no_context(self): self.assertListEqual(["xxx", "bar", "foo"], spans_names_list) + def test_simple_span_processor_not_sampled(self): + tracer_provider = trace.TracerProvider( + sampler=trace.sampling.ALWAYS_OFF + ) + tracer = tracer_provider.get_tracer(__name__) + + spans_names_list = [] + + my_exporter = MySpanExporter(destination=spans_names_list) + span_processor = export.SimpleExportSpanProcessor(my_exporter) + tracer_provider.add_span_processor(span_processor) + + with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("xxx"): + pass + + self.assertListEqual([], spans_names_list) + def _create_start_and_end_span(name, span_processor): span = trace.Span( name, - mock.Mock(spec=trace_api.SpanContext), + trace_api.SpanContext( + 0xDEADBEEF, + 0xDEADBEEF, + is_remote=False, + trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), + ), span_processor=span_processor, ) span.start() @@ -219,6 +243,7 @@ def test_batch_span_processor_lossless(self): for _ in range(512): _create_start_and_end_span("foo", span_processor) + time.sleep(1) self.assertTrue(span_processor.force_flush()) self.assertEqual(len(spans_names_list), 512) span_processor.shutdown() @@ -247,6 +272,31 @@ def test_batch_span_processor_many_spans(self): self.assertEqual(len(spans_names_list), 1024) span_processor.shutdown() + def test_batch_span_processor_not_sampled(self): + tracer_provider = trace.TracerProvider( + sampler=trace.sampling.ALWAYS_OFF + ) + tracer = tracer_provider.get_tracer(__name__) + spans_names_list = [] + + my_exporter = MySpanExporter( + destination=spans_names_list, max_export_batch_size=128 + ) + span_processor = export.BatchExportSpanProcessor( + my_exporter, + max_queue_size=256, + max_export_batch_size=64, + schedule_delay_millis=100, + ) + tracer_provider.add_span_processor(span_processor) + with tracer.start_as_current_span("foo"): + pass + time.sleep(0.05) # give some time for the exporter to upload spans + + self.assertTrue(span_processor.force_flush()) + self.assertEqual(len(spans_names_list), 0) + span_processor.shutdown() + def test_batch_span_processor_scheduled_delay(self): """Test that spans are exported each schedule_delay_millis""" spans_names_list = [] From 272bc0e8cdc7604bb00830db6a099204ebdc7b8a Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Wed, 9 Sep 2020 17:26:06 -0700 Subject: [PATCH 0539/1517] docs: adding faq (#1068) Adding an FAQ section, which can also serve as a quick reference. Not having such a section makes common scenarios harder to discover. --- docs/faq-and-cookbook.rst | 27 +++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 28 insertions(+) create mode 100644 docs/faq-and-cookbook.rst diff --git a/docs/faq-and-cookbook.rst b/docs/faq-and-cookbook.rst new file mode 100644 index 0000000000..c7f630fce7 --- /dev/null +++ b/docs/faq-and-cookbook.rst @@ -0,0 +1,27 @@ +Frequently Asked Questions and Cookbook +======================================= + +This page answers frequently asked questions, and serves as a cookbook +for common scenarios. + +Create a new span +----------------- + +.. code-block:: python + + from opentelemetry import trace + + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span("print") as span: + print("foo") + span.set_attribute("printed_string", "foo") + +Getting and modifying a span +---------------------------- + +.. code-block:: python + + from opentelemetry import trace + + current_span = trace.get_current_span() + current_span.set_attribute("hometown", "seattle") diff --git a/docs/index.rst b/docs/index.rst index b31088374c..085fb573ac 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -62,6 +62,7 @@ install :name: getting-started getting-started + faq-and-cookbook .. toctree:: :maxdepth: 1 From 65ddc2db20885ad8928c167c835dc8b54e4a491c Mon Sep 17 00:00:00 2001 From: Florimond Manca Date: Thu, 10 Sep 2020 18:28:37 +0200 Subject: [PATCH 0540/1517] Update ASGI instrumentation docs (#1090) --- docs/instrumentation/asgi/asgi.rst | 7 +++---- .../opentelemetry-instrumentation-asgi/README.rst | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/instrumentation/asgi/asgi.rst b/docs/instrumentation/asgi/asgi.rst index abb1621973..b988e4de43 100644 --- a/docs/instrumentation/asgi/asgi.rst +++ b/docs/instrumentation/asgi/asgi.rst @@ -1,8 +1,7 @@ -OpenTelemetry asgi Instrumentation -=================================== +.. include:: ../../../instrumentation/opentelemetry-instrumentation-asgi/README.rst -Module contents ---------------- +API +--- .. automodule:: opentelemetry.instrumentation.asgi :members: diff --git a/instrumentation/opentelemetry-instrumentation-asgi/README.rst b/instrumentation/opentelemetry-instrumentation-asgi/README.rst index f2b760976a..3eb8e2dda7 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/README.rst +++ b/instrumentation/opentelemetry-instrumentation-asgi/README.rst @@ -1,5 +1,5 @@ -OpenTelemetry ASGI Middleware -============================= +OpenTelemetry ASGI Instrumentation +================================== |pypi| @@ -54,6 +54,17 @@ Modify the application's ``asgi.py`` file as shown below. application = OpenTelemetryMiddleware(application) +Usage (Raw ASGI) +---------------- + +.. code-block:: python + + from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware + + app = ... # An ASGI application. + app = OpenTelemetryMiddleware(app) + + References ---------- From 12f51bc6714b989324391b67700d50eaa9f3f496 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 11 Sep 2020 14:44:17 -0400 Subject: [PATCH 0541/1517] Fix sdk/__init__.py not being included (#1091) --- opentelemetry-api/CHANGELOG.md | 2 ++ opentelemetry-api/setup.cfg | 1 - opentelemetry-sdk/CHANGELOG.md | 2 ++ opentelemetry-sdk/setup.cfg | 1 - 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 1f9ba4707f..29d9da5e03 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -17,6 +17,8 @@ - Rename HTTPTextFormat to TextMapPropagator. This change also updates `get_global_httptextformat` and `set_global_httptextformat` to `get_global_textmap` and `set_global_textmap` ([#1085](https://github.com/open-telemetry/opentelemetry-python/pull/1085)) +- Fix api/sdk setup.cfg to include missing python files + ([#1091](https://github.com/open-telemetry/opentelemetry-python/pull/1091)) ## Version 0.12b0 diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 8300be0d0a..89da5254d6 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -47,7 +47,6 @@ install_requires = [options.packages.find] where = src -include = opentelemetry.* [options.entry_points] opentelemetry_context = diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index e31387a0c4..6405430c7f 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -14,6 +14,8 @@ ([#1053](https://github.com/open-telemetry/opentelemetry-python/pull/1053)) - Rename Resource labels to attributes ([#1082](https://github.com/open-telemetry/opentelemetry-python/pull/1082)) +- Fix api/sdk setup.cfg to include missing python files + ([#1091](https://github.com/open-telemetry/opentelemetry-python/pull/1091)) ## Version 0.12b0 diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 821c7dd785..a0cd3651e3 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -46,7 +46,6 @@ install_requires = [options.packages.find] where = src -include = opentelemetry.sdk.* [options.entry_points] opentelemetry_meter_provider = From 6588a70aaa337c594a4267aa0f5d481a3f00d510 Mon Sep 17 00:00:00 2001 From: dengliming Date: Mon, 14 Sep 2020 11:07:23 +0800 Subject: [PATCH 0542/1517] Zipkin exporter report instrumentation info (#1097) * Zipkin exporter report instrumentation info --- exporter/opentelemetry-exporter-zipkin/CHANGELOG.md | 2 ++ .../src/opentelemetry/exporter/zipkin/__init__.py | 8 ++++++++ .../tests/test_zipkin_exporter.py | 9 ++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index b1066d081a..b285e4403c 100644 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -7,6 +7,8 @@ `port`, `protocol`, `endpoint`. This brings this implementation inline with other implementations. ([#1064](https://github.com/open-telemetry/opentelemetry-python/pull/1064)) +- Zipkin exporter report instrumentation info. + ([#1097](https://github.com/open-telemetry/opentelemetry-python/pull/1097)) ## Version 0.12b0 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 6b3ce2df9e..8c3c91a6d3 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -175,6 +175,14 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): "annotations": _extract_annotations_from_events(span.events), } + if span.instrumentation_info is not None: + zipkin_span["tags"][ + "otel.instrumentation_library.name" + ] = span.instrumentation_info.name + zipkin_span["tags"][ + "otel.instrumentation_library.version" + ] = span.instrumentation_info.version + if context.trace_flags.sampled: zipkin_span["debug"] = True diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 96586d91e0..309d966805 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -22,6 +22,7 @@ from opentelemetry.sdk import trace from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.trace.export import SpanExportResult +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import TraceFlags @@ -191,6 +192,9 @@ def test_export(self): otel_spans[3].start(start_time=start_times[3]) otel_spans[3].resource = Resource({}) otel_spans[3].end(end_time=end_times[3]) + otel_spans[3].instrumentation_info = InstrumentationInfo( + name="name", version="version" + ) service_name = "test-service" local_endpoint = {"serviceName": service_name, "port": 9411} @@ -252,7 +256,10 @@ def test_export(self): "duration": durations[3] // 10 ** 3, "localEndpoint": local_endpoint, "kind": None, - "tags": {}, + "tags": { + "otel.instrumentation_library.name": "name", + "otel.instrumentation_library.version": "version", + }, "annotations": None, }, ] From 86460c969ce42e90e7188ddf6872f751dca3c854 Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Mon, 14 Sep 2020 17:27:06 +0200 Subject: [PATCH 0543/1517] build/infra: Add GitHub Action for tox runs (#984) The Python 3.5 intrumentation test segfaults when importing the mysql connector. Running this test on Ubuntu-20.04 fixes this issue. --- .github/workflows/test.yml | 78 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..84cbecacec --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,78 @@ +name: Test + +on: [push, pull_request] + +jobs: + build: + env: + # We use these variables to convert between tox and GHA version literals + py34: 3.4 + py35: 3.5 + py36: 3.6 + py37: 3.7 + py38: 3.8 + pypy3: pypy3 + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false # ensures the entire test matrix is run, even if one permutation fails + matrix: + python-version: [ py34, py35, py36, py37, py38, pypy3 ] + package: ["instrumentation", "core", "exporter"] + os: [ ubuntu-latest ] + include: + - python-version: py38 + package: "tracecontext" + os: ubuntu-latest + - python-version: py38 + package: "mypy" + os: ubuntu-latest + - python-version: py38 + package: "mypyinstalled" + os: ubuntu-latest + # py35-instrumentation segfaults on 18.04 so we instead run on 20.04 + - python-version: py35 + package: instrumentation + os: ubuntu-20.04 + exclude: + - os: ubuntu-latest + python-version: py35 + package: instrumentation + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ env[matrix.python-version] }} + uses: actions/setup-python@v2 + with: + python-version: ${{ env[matrix.python-version] }} + - name: Install tox + run: pip install -U tox-factor + - name: Cache tox environment + # Preserves .tox directory between runs for faster installs + uses: actions/cache@v2 + with: + path: .tox + key: tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }} + - name: run tox + run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} + misc: + strategy: + fail-fast: false + matrix: + tox-environment: [ "docker-tests", "lint", "docs" ] + name: ${{ matrix.tox-environment }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install tox + run: pip install -U tox + - name: Cache tox environment + # Preserves .tox directory between runs for faster installs + uses: actions/cache@v2 + with: + path: .tox + key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }} + - name: run tox + run: tox -e ${{ matrix.tox-environment }} From 23cf584f1016146877482daecbc92a4acd546623 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 14 Sep 2020 12:49:52 -0700 Subject: [PATCH 0544/1517] instrumentation/botocore: fix failing test (#1100) The failing test was introduced I believe by a change in behaviour noted in this issue: https://github.com/spulec/moto/issues/3292. The solution is to set a region in the create_bucket call. Fixes #1088 --- .../tests/test_botocore_instrumentation.py | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py index 64c9d02402..ea655c8830 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py @@ -104,35 +104,36 @@ def test_s3_client(self): ) # Comment test for issue 1088 - # @mock_s3 - # def test_s3_put(self): - # params = dict(Key="foo", Bucket="mybucket", Body=b"bar") - # s3 = self.session.create_client("s3", region_name="us-west-2") - # s3.create_bucket(Bucket="mybucket") - # s3.put_object(**params) - - # spans = self.memory_exporter.get_finished_spans() - # assert spans - # span = spans[0] - # self.assertEqual(len(spans), 2) - # self.assertEqual(span.attributes["aws.operation"], "CreateBucket") - # assert_span_http_status_code(span, 200) - # self.assertEqual( - # span.resource, - # Resource( - # attributes={"endpoint": "s3", "operation": "createbucket"} - # ), - # ) - # self.assertEqual(spans[1].attributes["aws.operation"], "PutObject") - # self.assertEqual( - # spans[1].resource, - # Resource(attributes={"endpoint": "s3", "operation": "putobject"}), - # ) - # self.assertEqual(spans[1].attributes["params.Key"], str(params["Key"])) - # self.assertEqual( - # spans[1].attributes["params.Bucket"], str(params["Bucket"]) - # ) - # self.assertTrue("params.Body" not in spans[1].attributes.keys()) + @mock_s3 + def test_s3_put(self): + params = dict(Key="foo", Bucket="mybucket", Body=b"bar") + s3 = self.session.create_client("s3", region_name="us-west-2") + location = {"LocationConstraint": "us-west-2"} + s3.create_bucket(Bucket="mybucket", CreateBucketConfiguration=location) + s3.put_object(**params) + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(len(spans), 2) + self.assertEqual(span.attributes["aws.operation"], "CreateBucket") + assert_span_http_status_code(span, 200) + self.assertEqual( + span.resource, + Resource( + attributes={"endpoint": "s3", "operation": "createbucket"} + ), + ) + self.assertEqual(spans[1].attributes["aws.operation"], "PutObject") + self.assertEqual( + spans[1].resource, + Resource(attributes={"endpoint": "s3", "operation": "putobject"}), + ) + self.assertEqual(spans[1].attributes["params.Key"], str(params["Key"])) + self.assertEqual( + spans[1].attributes["params.Bucket"], str(params["Bucket"]) + ) + self.assertTrue("params.Body" not in spans[1].attributes.keys()) @mock_sqs def test_sqs_client(self): From d376df1b3f2b0ff234b40d41a71a332188fbeaf0 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 14 Sep 2020 13:13:44 -0700 Subject: [PATCH 0545/1517] Use is_recording flag in requests instrumentation (#1087) --- .../instrumentation/requests/__init__.py | 15 ++++++++------- .../tests/test_requests_integration.py | 17 +++++++++++++++++ tests/util/setup.cfg | 1 + 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index 16e8952fea..fef67c5d0d 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -56,8 +56,8 @@ # pylint: disable=unused-argument def _instrument(tracer_provider=None, span_callback=None): """Enables tracing of all requests calls that go through - :code:`requests.session.Session.request` (this includes - :code:`requests.get`, etc.).""" + :code:`requests.session.Session.request` (this includes + :code:`requests.get`, etc.).""" # Since # https://github.com/psf/requests/commit/d72d1162142d1bf8b1b5711c664fbbd674f349d1 @@ -121,9 +121,10 @@ def _instrumented_requests_call( with get_tracer( __name__, __version__, tracer_provider ).start_as_current_span(span_name, kind=SpanKind.CLIENT) as span: - span.set_attribute("component", "http") - span.set_attribute("http.method", method.upper()) - span.set_attribute("http.url", url) + if span.is_recording(): + span.set_attribute("component", "http") + span.set_attribute("http.method", method.upper()) + span.set_attribute("http.url", url) headers = get_or_create_headers() propagators.inject(type(headers).__setitem__, headers) @@ -139,13 +140,13 @@ def _instrumented_requests_call( finally: context.detach(token) - if exception is not None: + if exception is not None and span.is_recording(): span.set_status( Status(_exception_to_canonical_code(exception)) ) span.record_exception(exception) - if result is not None: + if result is not None and span.is_recording(): span.set_attribute("http.status_code", result.status_code) span.set_attribute("http.status_text", result.reason) span.set_status( diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py index 41f5bc39d9..c3457b7392 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py @@ -147,6 +147,23 @@ def test_suppress_instrumentation(self): self.assert_span(num_spans=0) + def test_not_recording(self): + with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span: + RequestsInstrumentor().uninstrument() + # original_tracer_provider returns a default tracer provider, which + # in turn will return an INVALID_SPAN, which is always not recording + RequestsInstrumentor().instrument( + tracer_provider=self.original_tracer_provider + ) + mock_span.is_recording.return_value = False + result = self.perform_request(self.URL) + self.assertEqual(result.text, "Hello!") + self.assert_span(None, 0) + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_distributed_context(self): previous_propagator = propagators.get_global_textmap() try: diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index b68c5e3a62..cfe66aa7df 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -39,6 +39,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api + opentelemetry-sdk [options.extras_require] test = flask~=1.0 From d7ce152ebba48c0a00f26062311567dba789d899 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 14 Sep 2020 15:11:56 -0700 Subject: [PATCH 0546/1517] dropping support for python 3.4 (#1099) * dropping support for python 3.4 --- .circleci/config.yml | 24 -------- .github/workflows/test.yml | 3 +- .../opentelemetry-example-app/setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../opentelemetry-exporter-jaeger/setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 2 + .../opentelemetry-exporter-zipkin/setup.cfg | 3 +- .../CHANGELOG.md | 5 +- .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 2 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 1 - .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 2 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- .../CHANGELOG.md | 3 + .../setup.cfg | 3 +- opentelemetry-api/CHANGELOG.md | 2 + opentelemetry-api/setup.cfg | 3 +- .../src/opentelemetry/context/__init__.py | 7 +-- opentelemetry-instrumentation/CHANGELOG.md | 3 + opentelemetry-instrumentation/setup.cfg | 3 +- opentelemetry-proto/CHANGELOG.md | 3 + opentelemetry-proto/setup.cfg | 3 +- opentelemetry-sdk/CHANGELOG.md | 2 + opentelemetry-sdk/setup.cfg | 3 +- opentelemetry-sdk/tests/conftest.py | 7 +-- tests/util/setup.cfg | 3 +- tox.ini | 60 +++++++++---------- 65 files changed, 144 insertions(+), 132 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fd45dce81f..a718eb04eb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,9 +13,6 @@ executors: py35: docker: - image: circleci/python:3.5 - py34: - docker: - - image: circleci/python:3.4 pypy3: docker: - image: pypy:3 @@ -101,23 +98,6 @@ jobs: - run_tox_scenario: pattern: py38-<< parameters.package >> - build-py34: - parameters: - package: - type: string - default: "core" - executor: py34 - steps: - - checkout - - run: - name: "Install tox" - command: sudo pip install -U tox virtualenv tox-factor - - restore_tox_cache - - run: - name: "Run scripts/run-tox-scenario" - command: tox -f py34-<< parameters.package >> - - save_tox_cache - build: parameters: version: @@ -143,10 +123,6 @@ workflows: parameters: version: ["py37", "py36", "py35", "pypy3"] package: ["core", "exporter", "instrumentation"] - - build-py34: - matrix: - parameters: - package: ["core", "exporter", "instrumentation"] - docs - lint - docker-tests diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84cbecacec..515fdf4331 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,6 @@ jobs: build: env: # We use these variables to convert between tox and GHA version literals - py34: 3.4 py35: 3.5 py36: 3.6 py37: 3.7 @@ -16,7 +15,7 @@ jobs: strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [ py34, py35, py36, py37, py38, pypy3 ] + python-version: [ py35, py36, py37, py38, pypy3 ] package: ["instrumentation", "core", "exporter"] os: [ ubuntu-latest ] include: diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index b38efb5939..effa303039 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages = find_namespace: diff --git a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md index ffb185bbec..15c4d956d3 100644 --- a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 28d97005fe..94e4336b83 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md index efa456f0ca..8e04322d0a 100644 --- a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4' + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 6ac4b5b9cd..a732af025e 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md b/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md index 563756856d..62cd9d3ae9 100644 --- a/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index 107e4d9891..5d35fb64ee 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index b285e4403c..b456edea27 100644 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -9,6 +9,8 @@ ([#1064](https://github.com/open-telemetry/opentelemetry-python/pull/1064)) - Zipkin exporter report instrumentation info. ([#1097](https://github.com/open-telemetry/opentelemetry-python/pull/1097)) +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) ## Version 0.12b0 diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 0b802a9348..86bc98eef4 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md index 35bd5d6027..8aef34f14d 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog -## Unreleased +## Unreleased + +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) ## Version 0.12b0 diff --git a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg index 994d5fe784..863e5c7336 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md index 128729e21d..b209b3a9d0 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg index c1198d356c..84d0e42927 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md index 99e1e09b55..5629a23aae 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md @@ -4,6 +4,8 @@ - bugfix: cursors and connections now produce spans when used with context managers ([#1028](https://github.com/open-telemetry/opentelemetry-python/pull/1028)) +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) ## Version 0.12b0 diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg index 2c78659343..1ba131c8e2 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index 3e4e42e55b..3ab4939604 100644 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-django/setup.cfg b/instrumentation/opentelemetry-instrumentation-django/setup.cfg index eb53010b62..fd6eb37e4b 100644 --- a/instrumentation/opentelemetry-instrumentation-django/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-django/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md index daa52ff3ec..a81d106458 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg index 0640edce12..14053c6295 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md index 989e06730d..bfc3269236 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg index e08d20e215..a8c28f4dd5 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md index ced6b9c345..bbf2e7b189 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg index 6a7db72aa7..665e9a9417 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md index 79a4aba1ae..15fc9bd6f0 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg index 026dba36e7..467cbd93a9 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 diff --git a/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md index 8912788b2f..f024cf6959 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg index aed70b99f1..cc0b435c74 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md index 36bfb122f6..85a7d42c39 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg index 2e0e57bb30..4054344236 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md index 3ee8fa7f3e..506f444865 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg index f0948b17c6..5dffe4abb3 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md index 5e05b3d45f..6a35aece8b 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg index dd27b8901a..dcf76a525c 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md index 23ea58f0b1..787f9cdd69 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg index d5302d9511..b157f7b6a2 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md index 7a9cee91bf..ead662f3b7 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg index ec491fd4cc..39ab09166a 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md index d4f6601fcb..0ef3a2e79c 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg index 2546682691..91eafb965c 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md index 72508d7b1f..22442c0a37 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg index 9e1562104e..6562b86719 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md index eeb2e83725..d42fd8f829 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md @@ -4,6 +4,8 @@ - Add support for instrumenting prepared requests ([#1040](https://github.com/open-telemetry/opentelemetry-python/pull/1040)) +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) ## Version 0.12b0 diff --git a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg index ece5013173..fbfa848ab9 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md index d64d247d21..ff4e540292 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg index bed8f6209c..41bd0d2b6e 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md index 08071b1522..f2fb2af5ed 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg index a13fb57ea3..a0aba2d1b4 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md index 4bd1389ccc..ac192b60c0 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg index 70c784c606..b7fbb90cb1 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md index 50b3afedfd..0bde2bb144 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg index 789f81ba8d..5c4cae94d3 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 29d9da5e03..f058be3702 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -19,6 +19,8 @@ ([#1085](https://github.com/open-telemetry/opentelemetry-python/pull/1085)) - Fix api/sdk setup.cfg to include missing python files ([#1091](https://github.com/open-telemetry/opentelemetry-python/pull/1091)) +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) ## Version 0.12b0 diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 89da5254d6..9435d92f19 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index a5f24118d3..22441c306b 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -17,7 +17,6 @@ import typing from functools import wraps from os import environ -from sys import version_info from pkg_resources import iter_entry_points @@ -48,11 +47,7 @@ def wrapper( if _RUNTIME_CONTEXT is None: # FIXME use a better implementation of a configuration manager to avoid having # to get configuration values straight from environment variables - if version_info < (3, 5): - # contextvars are not supported in 3.4, use thread-local storage - default_context = "threadlocal_context" - else: - default_context = "contextvars_context" + default_context = "contextvars_context" configured_context = environ.get( "OTEL_CONTEXT", default_context diff --git a/opentelemetry-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md index d6f04ae069..91dc011f09 100644 --- a/opentelemetry-instrumentation/CHANGELOG.md +++ b/opentelemetry-instrumentation/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## 0.9b0 Released 2020-06-10 diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 78973ab127..fd131a42cb 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-proto/CHANGELOG.md b/opentelemetry-proto/CHANGELOG.md index 3bc89fca99..5eb4d4561e 100644 --- a/opentelemetry-proto/CHANGELOG.md +++ b/opentelemetry-proto/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + ## Version 0.10b0 Released 2020-06-23 diff --git a/opentelemetry-proto/setup.cfg b/opentelemetry-proto/setup.cfg index 59b0309ad4..3cb2a70195 100644 --- a/opentelemetry-proto/setup.cfg +++ b/opentelemetry-proto/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 6405430c7f..e0f9e219e1 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -16,6 +16,8 @@ ([#1082](https://github.com/open-telemetry/opentelemetry-python/pull/1082)) - Fix api/sdk setup.cfg to include missing python files ([#1091](https://github.com/open-telemetry/opentelemetry-python/pull/1091)) +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) ## Version 0.12b0 diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index a0cd3651e3..f296088422 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-sdk/tests/conftest.py b/opentelemetry-sdk/tests/conftest.py index ed5853b43d..5652fda036 100644 --- a/opentelemetry-sdk/tests/conftest.py +++ b/opentelemetry-sdk/tests/conftest.py @@ -13,16 +13,11 @@ # limitations under the License. from os import environ -from sys import version_info def pytest_sessionstart(session): # pylint: disable=unused-argument - if version_info < (3, 5): - # contextvars are not supported in 3.4, use thread-local storage - environ["OTEL_CONTEXT"] = "threadlocal_context" - else: - environ["OTEL_CONTEXT"] = "contextvars_context" + environ["OTEL_CONTEXT"] = "contextvars_context" def pytest_sessionfinish(session): diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index cfe66aa7df..7ea637d4b0 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -26,14 +26,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.4 +python_requires = >=3.5 package_dir= =src packages=find_namespace: diff --git a/tox.ini b/tox.ini index 0fbaabce83..0575629074 100644 --- a/tox.ini +++ b/tox.ini @@ -5,15 +5,15 @@ envlist = ; Environments are organized by individual package, allowing ; for specifying supported Python versions per package. ; opentelemetry-api - py3{4,5,6,7,8}-test-core-api + py3{5,6,7,8}-test-core-api pypy3-test-core-api ; opentelemetry-proto - py3{4,5,6,7,8}-test-core-proto + py3{5,6,7,8}-test-core-proto pypy3-test-core-proto ; opentelemetry-sdk - py3{4,5,6,7,8}-test-core-sdk + py3{5,6,7,8}-test-core-sdk pypy3-test-core-sdk ; opentelemetry-instrumentation @@ -21,11 +21,11 @@ envlist = pypy3-test-core-instrumentation ; docs/getting-started - py3{4,5,6,7,8}-test-core-getting-started + py3{5,6,7,8}-test-core-getting-started pypy3-test-core-getting-started ; opentelemetry-example-app - py3{4,5,6,7,8}-test-instrumentation-example-app + py3{5,6,7,8}-test-instrumentation-example-app pypy3-test-instrumentation-example-app ; opentelemetry-instrumentation-aiohttp-client @@ -41,11 +41,11 @@ envlist = pypy3-test-instrumentation-botocore ; opentelemetry-instrumentation-django - py3{4,5,6,7,8}-test-instrumentation-django + py3{5,6,7,8}-test-instrumentation-django pypy3-test-instrumentation-django ; opentelemetry-instrumentation-dbapi - py3{4,5,6,7,8}-test-instrumentation-dbapi + py3{5,6,7,8}-test-instrumentation-dbapi pypy3-test-instrumentation-dbapi ; opentelemetry-instrumentation-boto @@ -53,7 +53,7 @@ envlist = pypy3-test-instrumentation-boto ; opentelemetry-instrumentation-elasticsearch - py3{4,5,6,7,8}-test-instrumentation-elasticsearch{2,5,6,7} + py3{5,6,7,8}-test-instrumentation-elasticsearch{2,5,6,7} pypy3-test-instrumentation-elasticsearch{2,5,6,7} ; opentelemetry-instrumentation-fastapi @@ -62,11 +62,11 @@ envlist = pypy3-test-instrumentation-fastapi ; opentelemetry-instrumentation-flask - py3{4,5,6,7,8}-test-instrumentation-flask + py3{5,6,7,8}-test-instrumentation-flask pypy3-test-instrumentation-flask ; opentelemetry-instrumentation-requests - py3{4,5,6,7,8}-test-instrumentation-requests + py3{5,6,7,8}-test-instrumentation-requests pypy3-test-instrumentation-requests ; opentelemetry-instrumentation-starlette. @@ -75,22 +75,22 @@ envlist = pypy3-test-instrumentation-starlette ; opentelemetry-instrumentation-jinja2 - py3{4,5,6,7,8}-test-instrumentation-jinja2 + py3{5,6,7,8}-test-instrumentation-jinja2 pypy3-test-instrumentation-jinja2 ; opentelemetry-exporter-jaeger - py3{4,5,6,7,8}-test-exporter-jaeger + py3{5,6,7,8}-test-exporter-jaeger pypy3-test-exporter-jaeger ; opentelemetry-exporter-datadog py3{5,6,7,8}-test-exporter-datadog ; opentelemetry-instrumentation-mysql - py3{4,5,6,7,8}-test-instrumentation-mysql + py3{5,6,7,8}-test-instrumentation-mysql pypy3-test-instrumentation-mysql ; opentelemetry-exporter-opencensus - py3{4,5,6,7,8}-test-exporter-opencensus + py3{5,6,7,8}-test-exporter-opencensus ; exporter-opencensus intentionally excluded from pypy3 ; opentelemetry-exporter-otlp @@ -98,27 +98,27 @@ envlist = ; exporter-otlp intentionally excluded from pypy3 ; opentelemetry-exporter-prometheus - py3{4,5,6,7,8}-test-exporter-prometheus + py3{5,6,7,8}-test-exporter-prometheus pypy3-test-exporter-prometheus ; opentelemetry-instrumentation-psycopg2 - py3{4,5,6,7,8}-test-instrumentation-psycopg2 + py3{5,6,7,8}-test-instrumentation-psycopg2 ; ext-psycopg2 intentionally excluded from pypy3 ; opentelemetry-instrumentation-pymemcache - py3{4,5,6,7,8}-test-instrumentation-pymemcache + py3{5,6,7,8}-test-instrumentation-pymemcache pypy3-test-instrumentation-pymemcache ; opentelemetry-instrumentation-pymongo - py3{4,5,6,7,8}-test-instrumentation-pymongo + py3{5,6,7,8}-test-instrumentation-pymongo pypy3-test-instrumentation-pymongo ; opentelemetry-instrumentation-pymysql - py3{4,5,6,7,8}-test-instrumentation-pymysql + py3{5,6,7,8}-test-instrumentation-pymysql pypy3-test-instrumentation-pymysql ; opentelemetry-instrumentation-pyramid - py3{4,5,6,7,8}-test-instrumentation-pyramid + py3{5,6,7,8}-test-instrumentation-pyramid pypy3-test-instrumentation-pyramid ; opentelemetry-instrumentation-asgi @@ -130,30 +130,30 @@ envlist = ; ext-asyncpg intentionally excluded from pypy3 ; opentelemetry-instrumentation-sqlite3 - py3{4,5,6,7,8}-test-instrumentation-sqlite3 + py3{5,6,7,8}-test-instrumentation-sqlite3 pypy3-test-instrumentation-sqlite3 ; opentelemetry-instrumentation-wsgi - py3{4,5,6,7,8}-test-instrumentation-wsgi + py3{5,6,7,8}-test-instrumentation-wsgi pypy3-test-instrumentation-wsgi ; opentelemetry-exporter-zipkin - py3{4,5,6,7,8}-test-exporter-zipkin + py3{5,6,7,8}-test-exporter-zipkin pypy3-test-exporter-zipkin ; opentelemetry-opentracing-shim - py3{4,5,6,7,8}-test-core-opentracing-shim + py3{5,6,7,8}-test-core-opentracing-shim pypy3-test-core-opentracing-shim ; opentelemetry-instrumentation-grpc py3{5,6,7,8}-test-instrumentation-grpc ; opentelemetry-instrumentation-sqlalchemy - py3{4,5,6,7,8}-test-instrumentation-sqlalchemy + py3{5,6,7,8}-test-instrumentation-sqlalchemy pypy3-test-instrumentation-sqlalchemy ; opentelemetry-instrumentation-redis - py3{4,5,6,7,8}-test-instrumentation-redis + py3{5,6,7,8}-test-instrumentation-redis pypy3-test-instrumentation-redis ; opentelemetry-instrumentation-celery @@ -161,7 +161,7 @@ envlist = pypy3-test-instrumentation-celery ; opentelemetry-instrumentation-system-metrics - py3{4,5,6,7,8}-test-instrumentation-system-metrics + py3{5,6,7,8}-test-instrumentation-system-metrics ; instrumentation-system-metrics intentionally excluded from pypy3 ; known limitation: gc.get_count won't work under pypy @@ -235,7 +235,7 @@ changedir = commands_pre = ; Install without -e to test the actual installation - py3{4,5,6,7,8}: python -m pip install -U pip setuptools wheel + py3{5,6,7,8}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util @@ -337,10 +337,6 @@ commands = ; implicit Any due to unfollowed import would result). mypyinstalled: mypy --namespace-packages opentelemetry-api/tests/mypysmoke.py --strict -[testenv:py34-test-core-opentracing-shim] -commands = - pytest --ignore-glob='*[asyncio].py' - [testenv:lint] basepython: python3.8 recreate = True From 1e342790a1bd74cc1be0bceabb5424c190622ab1 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 14 Sep 2020 17:15:03 -0600 Subject: [PATCH 0547/1517] exporter/otlp: Add instrumentation info to exported spans (#1095) Fixes #1094 --- .../opentelemetry-exporter-otlp/CHANGELOG.md | 2 ++ .../exporter/otlp/trace_exporter/__init__.py | 16 +++++++++++++++- .../tests/test_otlp_trace_exporter.py | 15 +++++++++++++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index ed6aee3b4c..49e6d8c941 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add instrumentation info to exported spans + ([#1095](https://github.com/open-telemetry/opentelemetry-python/pull/1095)) - Add metric OTLP exporter ([#835](https://github.com/open-telemetry/opentelemetry-python/pull/835)) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index 5a9a74a304..fd1d8e235e 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -27,6 +27,7 @@ from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( TraceServiceStub, ) +from opentelemetry.proto.common.v1.common_pb2 import InstrumentationLibrary from opentelemetry.proto.trace.v1.trace_pb2 import ( InstrumentationLibrarySpans, ResourceSpans, @@ -168,9 +169,22 @@ def _translate_data(self, data) -> ExportTraceServiceRequest: if sdk_span.resource not in ( sdk_resource_instrumentation_library_spans.keys() ): + if sdk_span.instrumentation_info is not None: + instrumentation_library_spans = InstrumentationLibrarySpans( + instrumentation_library=InstrumentationLibrary( + name=sdk_span.instrumentation_info.name, + version=sdk_span.instrumentation_info.version, + ) + ) + + else: + instrumentation_library_spans = ( + InstrumentationLibrarySpans() + ) + sdk_resource_instrumentation_library_spans[ sdk_span.resource - ] = InstrumentationLibrarySpans() + ] = instrumentation_library_spans self._collector_span_kwargs = {} diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index a7f572323e..06a3877b92 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -30,7 +30,11 @@ TraceServiceServicer, add_TraceServiceServicer_to_server, ) -from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue, + InstrumentationLibrary, + KeyValue, +) from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as CollectorResource, ) @@ -46,6 +50,7 @@ SimpleExportSpanProcessor, SpanExportResult, ) +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanKind @@ -142,6 +147,9 @@ def setUp(self): } ) ], + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), ) self.span.start() @@ -209,6 +217,9 @@ def test_translate_spans(self): ), instrumentation_library_spans=[ InstrumentationLibrarySpans( + instrumentation_library=InstrumentationLibrary( + name="name", version="version" + ), spans=[ CollectorSpan( # pylint: disable=no-member @@ -282,7 +293,7 @@ def test_translate_spans(self): ) ], ) - ] + ], ) ], ), From 071b1b30d26963a08de10ee6c9724549ca7621fc Mon Sep 17 00:00:00 2001 From: dengliming Date: Tue, 15 Sep 2020 14:04:09 +0800 Subject: [PATCH 0548/1517] Jaeger exporter report instrumentation info (#1098) --- .../CHANGELOG.md | 3 +- .../opentelemetry/exporter/jaeger/__init__.py | 14 ++++++++ .../tests/test_jaeger_exporter.py | 32 ++++++++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md index 15c4d956d3..fbca139692 100644 --- a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md @@ -1,9 +1,10 @@ # Changelog ## Unreleased - - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) +- Report instrumentation info + ([#1098](https://github.com/open-telemetry/opentelemetry-python/pull/1098)) ## Version 0.12b0 diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index b998a6aecf..354fe8b85c 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -214,6 +214,20 @@ def _translate_to_jaeger(spans: Span): ] ) + if span.instrumentation_info is not None: + tags.extend( + [ + _get_string_tag( + "otel.instrumentation_library.name", + span.instrumentation_info.name, + ), + _get_string_tag( + "otel.instrumentation_library.version", + span.instrumentation_info.version, + ), + ] + ) + # Ensure that if Status.Code is not OK, that we set the "error" tag on the Jaeger span. if status.canonical_code is not StatusCanonicalCode.OK: tags.append(_get_bool_tag("error", True)) diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py index bb852a0798..c787081422 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py @@ -23,6 +23,7 @@ from opentelemetry.exporter.jaeger.gen.jaeger import ttypes as jaeger from opentelemetry.sdk import trace from opentelemetry.sdk.trace import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace.status import Status, StatusCanonicalCode @@ -221,6 +222,9 @@ def test_translate_to_jaeger(self): otel_spans[2].start(start_time=start_times[2]) otel_spans[2].resource = Resource({}) otel_spans[2].end(end_time=end_times[2]) + otel_spans[2].instrumentation_info = InstrumentationInfo( + name="name", version="version" + ) # pylint: disable=protected-access spans = jaeger_exporter._translate_to_jaeger(otel_spans) @@ -334,7 +338,33 @@ def test_translate_to_jaeger(self): startTime=start_times[2] // 10 ** 3, duration=durations[2] // 10 ** 3, flags=0, - tags=default_tags, + tags=[ + jaeger.Tag( + key="status.code", + vType=jaeger.TagType.LONG, + vLong=StatusCanonicalCode.OK.value, + ), + jaeger.Tag( + key="status.message", + vType=jaeger.TagType.STRING, + vStr=None, + ), + jaeger.Tag( + key="span.kind", + vType=jaeger.TagType.STRING, + vStr=trace_api.SpanKind.INTERNAL.name, + ), + jaeger.Tag( + key="otel.instrumentation_library.name", + vType=jaeger.TagType.STRING, + vStr="name", + ), + jaeger.Tag( + key="otel.instrumentation_library.version", + vType=jaeger.TagType.STRING, + vStr="version", + ), + ], ), ] From 751e81389502332e7b5c89cf90b71ca95d71b7c3 Mon Sep 17 00:00:00 2001 From: Michael Stella Date: Tue, 15 Sep 2020 10:20:47 -0400 Subject: [PATCH 0549/1517] Fix missing step in docs (#1113) As far as I can tell, the tutorial in the docs doesn't actually work without this line. --- .../src/opentelemetry/instrumentation/grpc/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py index 9b6862eba0..58d2ebbb1e 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py @@ -106,6 +106,7 @@ def SayHello(self, request, context): def serve(): server = grpc.server(futures.ThreadPoolExecutor()) + server = intercept_server(server, server_interceptor()) helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server) server.add_insecure_port("[::]:50051") From eaa77647acbad798f2758312c7e4fe4bc83e861f Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Wed, 16 Sep 2020 05:33:27 +0200 Subject: [PATCH 0550/1517] Migrate pytest config to pyproject.toml (#1086) --- dev-requirements.txt | 2 +- pyproject.toml | 4 ++++ pytest.ini | 4 ---- 3 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 pytest.ini diff --git a/dev-requirements.txt b/dev-requirements.txt index 6537a9a444..b63248c7f0 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -6,7 +6,7 @@ mypy==0.770 sphinx~=2.1 sphinx-rtd-theme~=0.4 sphinx-autodoc-typehints~=1.10.2 -pytest!=5.2.3 +pytest>=6.0 pytest-cov>=2.8 readme-renderer~=24.0 grpcio-tools==1.29.0 diff --git a/pyproject.toml b/pyproject.toml index db4df73b69..0c79406d05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,3 +13,7 @@ exclude = ''' )/ ) ''' +[tool.pytest.ini_options] +addopts = "-rs -v" +log_cli = true +log_cli_level = "warning" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 013d2c555c..0000000000 --- a/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pytest] -addopts = -rs -v -log_cli = true -log_cli_level = warning From 553f6e8b624d66b6326854f98366928bca29b302 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 16 Sep 2020 21:34:02 +0530 Subject: [PATCH 0551/1517] instrumentation/falcon: Added Falcon 2.0+ instrumentation (#1039) --- .../CHANGELOG.md | 5 + .../LICENSE | 201 ++++++++++++++++++ .../MANIFEST.in | 9 + .../README.rst | 38 ++++ .../setup.cfg | 59 +++++ .../setup.py | 26 +++ .../instrumentation/falcon/__init__.py | 195 +++++++++++++++++ .../instrumentation/falcon/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/app.py | 40 ++++ .../tests/test_falcon.py | 173 +++++++++++++++ .../instrumentation/bootstrap.py | 2 + tox.ini | 9 +- 13 files changed, 771 insertions(+), 1 deletion(-) create mode 100644 instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md create mode 100644 instrumentation/opentelemetry-instrumentation-falcon/LICENSE create mode 100644 instrumentation/opentelemetry-instrumentation-falcon/MANIFEST.in create mode 100644 instrumentation/opentelemetry-instrumentation-falcon/README.rst create mode 100644 instrumentation/opentelemetry-instrumentation-falcon/setup.cfg create mode 100644 instrumentation/opentelemetry-instrumentation-falcon/setup.py create mode 100644 instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py create mode 100644 instrumentation/opentelemetry-instrumentation-falcon/tests/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-falcon/tests/app.py create mode 100644 instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py diff --git a/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md new file mode 100644 index 0000000000..3be1e19dc6 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release. Added instrumentation for Falcon 2.0+ diff --git a/instrumentation/opentelemetry-instrumentation-falcon/LICENSE b/instrumentation/opentelemetry-instrumentation-falcon/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/instrumentation/opentelemetry-instrumentation-falcon/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-falcon/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-falcon/README.rst b/instrumentation/opentelemetry-instrumentation-falcon/README.rst new file mode 100644 index 0000000000..f7d5a99d95 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/README.rst @@ -0,0 +1,38 @@ +OpenTelemetry Falcon Tracing +============================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-falcon.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-falcon/ + +This library builds on the OpenTelemetry WSGI middleware to track web requests +in Falcon applications. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-falcon + +Configuration +------------- + +Exclude lists +************* +To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FALCON_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. + +For example, + +:: + + export OTEL_PYTHON_FALCON_EXCLUDED_URLS="client/.*/info,healthcheck" + +will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. + +References +---------- + +* `OpenTelemetry Falcon Instrumentation `_ +* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg b/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg new file mode 100644 index 0000000000..ed6dc0838b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg @@ -0,0 +1,59 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-instrumentation-falcon +description = Falcon instrumentation for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-falcon +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + falcon ~= 2.0 + opentelemetry-instrumentation-wsgi == 0.13dev0 + opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13dev0 + +[options.extras_require] +test = + falcon ~= 2.0 + opentelemetry-test == 0.13dev0 + parameterized == 0.7.4 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + falcon = opentelemetry.instrumentation.falcon:FalconInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-falcon/setup.py b/instrumentation/opentelemetry-instrumentation-falcon/setup.py new file mode 100644 index 0000000000..eb61edde62 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "instrumentation", "falcon", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py new file mode 100644 index 0000000000..1e67d6101b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py @@ -0,0 +1,195 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This library builds on the OpenTelemetry WSGI middleware to track web requests +in Falcon applications. In addition to opentelemetry-instrumentation-wsgi, +it supports falcon-specific features such as: + +* The Falcon resource and method name is used as the Span name. +* The ``falcon.resource`` Span attribute is set so the matched resource. +* Error from Falcon resources are properly caught and recorded. + +Usage +----- + +.. code-block:: python + + from falcon import API + from opentelemetry.instrumentation.falcon import FalconInstrumentor + + FalconInstrumentor().instrument() + + app = falcon.API() + + class HelloWorldResource(object): + def on_get(self, req, resp): + resp.body = 'Hello World' + + app.add_route('/hello', HelloWorldResource()) + +API +--- +""" + +import sys +from logging import getLogger + +import falcon + +import opentelemetry.instrumentation.wsgi as otel_wsgi +from opentelemetry import configuration, context, propagators, trace +from opentelemetry.instrumentation.falcon.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import http_status_to_canonical_code +from opentelemetry.trace.status import Status +from opentelemetry.util import ExcludeList, time_ns + +_logger = getLogger(__name__) + +_ENVIRON_STARTTIME_KEY = "opentelemetry-falcon.starttime_key" +_ENVIRON_SPAN_KEY = "opentelemetry-falcon.span_key" +_ENVIRON_ACTIVATION_KEY = "opentelemetry-falcon.activation_key" +_ENVIRON_TOKEN = "opentelemetry-falcon.token" +_ENVIRON_EXC = "opentelemetry-falcon.exc" + + +def get_excluded_urls(): + urls = configuration.Configuration().FALCON_EXCLUDED_URLS or "" + if urls: + urls = str.split(urls, ",") + return ExcludeList(urls) + + +_excluded_urls = get_excluded_urls() + + +class FalconInstrumentor(BaseInstrumentor): + # pylint: disable=protected-access,attribute-defined-outside-init + """An instrumentor for falcon.API + + See `BaseInstrumentor` + """ + + def _instrument(self, **kwargs): + self._original_falcon_api = falcon.API + falcon.API = _InstrumentedFalconAPI + + def _uninstrument(self, **kwargs): + falcon.API = self._original_falcon_api + + +class _InstrumentedFalconAPI(falcon.API): + def __init__(self, *args, **kwargs): + mw = kwargs.pop("middleware", []) + if not isinstance(mw, (list, tuple)): + mw = [mw] + + self._tracer = trace.get_tracer(__name__, __version__) + mw.insert(0, _TraceMiddleware(self._tracer)) + kwargs["middleware"] = mw + super().__init__(*args, **kwargs) + + def __call__(self, env, start_response): + if _excluded_urls.url_disabled(env.get("PATH_INFO", "/")): + return super().__call__(env, start_response) + + start_time = time_ns() + + token = context.attach( + propagators.extract(otel_wsgi.get_header_from_environ, env) + ) + attributes = otel_wsgi.collect_request_attributes(env) + span = self._tracer.start_span( + otel_wsgi.get_default_span_name(env), + kind=trace.SpanKind.SERVER, + attributes=attributes, + start_time=start_time, + ) + activation = self._tracer.use_span(span, end_on_exit=True) + activation.__enter__() + env[_ENVIRON_SPAN_KEY] = span + env[_ENVIRON_ACTIVATION_KEY] = activation + + def _start_response(status, response_headers, *args, **kwargs): + otel_wsgi.add_response_attributes(span, status, response_headers) + response = start_response( + status, response_headers, *args, **kwargs + ) + activation.__exit__(None, None, None) + context.detach(token) + return response + + try: + return super().__call__(env, _start_response) + except Exception as exc: + activation.__exit__( + type(exc), exc, getattr(exc, "__traceback__", None), + ) + context.detach(token) + raise + + +class _TraceMiddleware: + # pylint:disable=R0201,W0613 + + def __init__(self, tracer=None): + self.tracer = tracer + + def process_resource(self, req, resp, resource, params): + span = req.env.get(_ENVIRON_SPAN_KEY) + if not span: + return + + resource_name = resource.__class__.__name__ + span.set_attribute("falcon.resource", resource_name) + span.update_name( + "{0}.on_{1}".format(resource_name, req.method.lower()) + ) + + def process_response( + self, req, resp, resource, req_succeeded=None + ): # pylint:disable=R0201 + span = req.env.get(_ENVIRON_SPAN_KEY) + if not span: + return + + status = resp.status + reason = None + if resource is None: + status = "404" + reason = "NotFound" + + exc_type, exc, _ = sys.exc_info() + if exc_type and not req_succeeded: + if "HTTPNotFound" in exc_type.__name__: + status = "404" + reason = "NotFound" + else: + status = "500" + reason = "{}: {}".format(exc_type.__name__, exc) + + status = status.split(" ")[0] + try: + status_code = int(status) + except ValueError: + pass + finally: + span.set_attribute("http.status_code", status_code) + span.set_status( + Status( + canonical_code=http_status_to_canonical_code(status_code), + description=reason, + ) + ) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py new file mode 100644 index 0000000000..9cc445d09e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py new file mode 100644 index 0000000000..76d19ae0d1 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py @@ -0,0 +1,40 @@ +import falcon + +# pylint:disable=R0201,W0613,E0602 + + +class HelloWorldResource: + def _handle_request(self, _, resp): + resp.status = falcon.HTTP_201 + resp.body = "Hello World" + + def on_get(self, req, resp): + self._handle_request(req, resp) + + def on_put(self, req, resp): + self._handle_request(req, resp) + + def on_patch(self, req, resp): + self._handle_request(req, resp) + + def on_post(self, req, resp): + self._handle_request(req, resp) + + def on_delete(self, req, resp): + self._handle_request(req, resp) + + def on_head(self, req, resp): + self._handle_request(req, resp) + + +class ErrorResource: + def on_get(self, req, resp): + print(non_existent_var) # noqa + + +def make_app(): + app = falcon.API() + app.add_route("/hello", HelloWorldResource()) + app.add_route("/ping", HelloWorldResource()) + app.add_route("/error", ErrorResource()) + return app diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py new file mode 100644 index 0000000000..a3d2c5d8d8 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py @@ -0,0 +1,173 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest.mock import patch + +from falcon import testing + +from opentelemetry.instrumentation.falcon import FalconInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.util import ExcludeList + +from .app import make_app + + +class TestFalconInstrumentation(TestBase): + def setUp(self): + super().setUp() + FalconInstrumentor().instrument() + self.app = make_app() + + def client(self): + return testing.TestClient(self.app) + + def tearDown(self): + super().tearDown() + with self.disable_logging(): + FalconInstrumentor().uninstrument() + + def test_get(self): + self._test_method("GET") + + def test_post(self): + self._test_method("POST") + + def test_patch(self): + self._test_method("PATCH") + + def test_put(self): + self._test_method("PUT") + + def test_delete(self): + self._test_method("DELETE") + + def test_head(self): + self._test_method("HEAD") + + def _test_method(self, method): + self.client().simulate_request(method=method, path="/hello") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual( + span.name, "HelloWorldResource.on_{0}".format(method.lower()) + ) + self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assert_span_has_attributes( + span, + { + "component": "http", + "http.method": method, + "http.server_name": "falconframework.org", + "http.scheme": "http", + "host.port": 80, + "http.host": "falconframework.org", + "http.target": "/", + "net.peer.ip": "127.0.0.1", + "net.peer.port": "65133", + "http.flavor": "1.1", + "falcon.resource": "HelloWorldResource", + "http.status_text": "Created", + "http.status_code": 201, + }, + ) + self.memory_exporter.clear() + + def test_404(self): + self.client().simulate_get("/does-not-exist") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.name, "HTTP GET") + self.assertEqual( + span.status.canonical_code, StatusCanonicalCode.NOT_FOUND + ) + self.assert_span_has_attributes( + span, + { + "component": "http", + "http.method": "GET", + "http.server_name": "falconframework.org", + "http.scheme": "http", + "host.port": 80, + "http.host": "falconframework.org", + "http.target": "/", + "net.peer.ip": "127.0.0.1", + "net.peer.port": "65133", + "http.flavor": "1.1", + "http.status_text": "Not Found", + "http.status_code": 404, + }, + ) + + def test_500(self): + try: + self.client().simulate_get("/error") + except NameError: + pass + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.name, "ErrorResource.on_get") + self.assertFalse(span.status.is_ok) + self.assertEqual( + span.status.canonical_code, StatusCanonicalCode.INTERNAL + ) + self.assertEqual( + span.status.description, + "NameError: name 'non_existent_var' is not defined", + ) + self.assert_span_has_attributes( + span, + { + "component": "http", + "http.method": "GET", + "http.server_name": "falconframework.org", + "http.scheme": "http", + "host.port": 80, + "http.host": "falconframework.org", + "http.target": "/", + "net.peer.ip": "127.0.0.1", + "net.peer.port": "65133", + "http.flavor": "1.1", + "http.status_code": 500, + }, + ) + + def test_uninstrument(self): + self.client().simulate_get(path="/hello") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + self.memory_exporter.clear() + + FalconInstrumentor().uninstrument() + self.app = make_app() + self.client().simulate_get(path="/hello") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + + @patch( + "opentelemetry.instrumentation.falcon._excluded_urls", + ExcludeList(["ping"]), + ) + def test_exclude_lists(self): + self.client().simulate_get(path="/ping") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + + self.client().simulate_get(path="/hello") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 125032b95d..c7022d9bc4 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -34,6 +34,7 @@ "django": "opentelemetry-instrumentation-django>=0.8b0", "elasticsearch": "opentelemetry-instrumentation-elasticsearch>=0.11b0", "fastapi": "opentelemetry-instrumentation-fastapi>=0.11b0", + "falcon": "opentelemetry-instrumentation-flask>=0.13b0", "flask": "opentelemetry-instrumentation-flask>=0.8b0", "grpc": "opentelemetry-instrumentation-grpc>=0.8b0", "jinja2": "opentelemetry-instrumentation-jinja2>=0.8b0", @@ -62,6 +63,7 @@ "django": ("opentelemetry-instrumentation-django",), "elasticsearch": ("opentelemetry-instrumentation-elasticsearch",), "fastapi": ("opentelemetry-instrumentation-fastapi",), + "falcon": ("opentelemetry-instrumentation-flask",), "flask": ("opentelemetry-instrumentation-flask",), "grpc": ("opentelemetry-instrumentation-grpc",), "jinja2": ("opentelemetry-instrumentation-jinja2",), diff --git a/tox.ini b/tox.ini index 0575629074..384c43725b 100644 --- a/tox.ini +++ b/tox.ini @@ -56,6 +56,10 @@ envlist = py3{5,6,7,8}-test-instrumentation-elasticsearch{2,5,6,7} pypy3-test-instrumentation-elasticsearch{2,5,6,7} + ; opentelemetry-instrumentation-falcon + py3{4,5,6,7,8}-test-instrumentation-falcon + pypy3-test-instrumentation-falcon + ; opentelemetry-instrumentation-fastapi ; fastapi only supports 3.6 and above. py3{6,7,8}-test-instrumentation-fastapi @@ -208,6 +212,7 @@ changedir = test-instrumentation-django: instrumentation/opentelemetry-instrumentation-django/tests test-instrumentation-example-app: docs/examples/opentelemetry-example-app/tests test-instrumentation-elasticsearch{2,5,6,7}: instrumentation/opentelemetry-instrumentation-elasticsearch/tests + test-instrumentation-falcon: instrumentation/opentelemetry-instrumentation-falcon/tests test-instrumentation-fastapi: instrumentation/opentelemetry-instrumentation-fastapi/tests test-instrumentation-flask: instrumentation/opentelemetry-instrumentation-flask/tests test-instrumentation-grpc: instrumentation/opentelemetry-instrumentation-grpc/tests @@ -251,7 +256,7 @@ commands_pre = grpc: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc[test] - wsgi,flask,django,pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi + wsgi,falcon,flask,django,pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi asgi,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg @@ -259,6 +264,8 @@ commands_pre = boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test] boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-boto[test] + falcon: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon[test] + flask: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test] botocore: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test] From 1abdc02a92cd996f3924e1653896bb72123ac84e Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Wed, 16 Sep 2020 19:00:45 +0200 Subject: [PATCH 0552/1517] Update names of decisions in SamplingResult (#1115) --- opentelemetry-sdk/CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/sampling.py | 28 +++++++++---------- .../tests/trace/test_sampling.py | 20 ++++++------- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index e0f9e219e1..a046d1d919 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -18,6 +18,8 @@ ([#1091](https://github.com/open-telemetry/opentelemetry-python/pull/1091)) - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) +- Rename members of `trace.sampling.Decision` enum + ([#1115](https://github.com/open-telemetry/opentelemetry-python/pull/1115)) ## Version 0.12b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 2a1d6e89db..2fd14a528e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -72,17 +72,17 @@ class Decision(enum.Enum): # IsRecording() == false, span will not be recorded and all events and attributes will be dropped. - NOT_RECORD = 0 + IGNORE = 0 # IsRecording() == true, but Sampled flag MUST NOT be set. - RECORD = 1 + RECORD_ONLY = 1 # IsRecording() == true AND Sampled flag` MUST be set. - RECORD_AND_SAMPLED = 2 + RECORD_AND_SAMPLE = 2 def is_recording(self): - return self in (Decision.RECORD, Decision.RECORD_AND_SAMPLED) + return self in (Decision.RECORD_ONLY, Decision.RECORD_AND_SAMPLE) def is_sampled(self): - return self is Decision.RECORD_AND_SAMPLED + return self is Decision.RECORD_AND_SAMPLE class SamplingResult: @@ -140,12 +140,12 @@ def should_sample( attributes: Attributes = None, links: Sequence["Link"] = (), ) -> "SamplingResult": - if self._decision is Decision.NOT_RECORD: + if self._decision is Decision.IGNORE: return SamplingResult(self._decision) return SamplingResult(self._decision, attributes) def get_description(self) -> str: - if self._decision is Decision.NOT_RECORD: + if self._decision is Decision.IGNORE: return "AlwaysOffSampler" return "AlwaysOnSampler" @@ -194,10 +194,10 @@ def should_sample( attributes: Attributes = None, # TODO links: Sequence["Link"] = (), ) -> "SamplingResult": - decision = Decision.NOT_RECORD + decision = Decision.IGNORE if trace_id & self.TRACE_ID_LIMIT < self.bound: - decision = Decision.RECORD_AND_SAMPLED - if decision is Decision.NOT_RECORD: + decision = Decision.RECORD_AND_SAMPLE + if decision is Decision.IGNORE: return SamplingResult(decision) return SamplingResult(decision, attributes) @@ -231,8 +231,8 @@ def should_sample( not parent_context.is_valid or not parent_context.trace_flags.sampled ): - return SamplingResult(Decision.NOT_RECORD) - return SamplingResult(Decision.RECORD_AND_SAMPLED, attributes) + return SamplingResult(Decision.IGNORE) + return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes) return self._delegate.should_sample( parent_context=parent_context, @@ -246,10 +246,10 @@ def get_description(self): return "ParentBased{{{}}}".format(self._delegate.get_description()) -ALWAYS_OFF = StaticSampler(Decision.NOT_RECORD) +ALWAYS_OFF = StaticSampler(Decision.IGNORE) """Sampler that never samples spans, regardless of the parent span's sampling decision.""" -ALWAYS_ON = StaticSampler(Decision.RECORD_AND_SAMPLED) +ALWAYS_ON = StaticSampler(Decision.RECORD_AND_SAMPLE) """Sampler that always samples spans, regardless of the parent span's sampling decision.""" DEFAULT_OFF = ParentBased(ALWAYS_OFF) diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index de1c019fb5..179b31aef5 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -25,34 +25,34 @@ class TestDecision(unittest.TestCase): def test_is_recording(self): self.assertTrue( - sampling.Decision.is_recording(sampling.Decision.RECORD) + sampling.Decision.is_recording(sampling.Decision.RECORD_ONLY) ) self.assertTrue( - sampling.Decision.is_recording( - sampling.Decision.RECORD_AND_SAMPLED - ) + sampling.Decision.is_recording(sampling.Decision.RECORD_AND_SAMPLE) ) self.assertFalse( - sampling.Decision.is_recording(sampling.Decision.NOT_RECORD) + sampling.Decision.is_recording(sampling.Decision.IGNORE) ) def test_is_sampled(self): self.assertFalse( - sampling.Decision.is_sampled(sampling.Decision.RECORD) + sampling.Decision.is_sampled(sampling.Decision.RECORD_ONLY) ) self.assertTrue( - sampling.Decision.is_sampled(sampling.Decision.RECORD_AND_SAMPLED) + sampling.Decision.is_sampled(sampling.Decision.RECORD_AND_SAMPLE) ) self.assertFalse( - sampling.Decision.is_sampled(sampling.Decision.NOT_RECORD) + sampling.Decision.is_sampled(sampling.Decision.IGNORE) ) class TestSamplingResult(unittest.TestCase): def test_ctr(self): attributes = {"asd": "test"} - result = sampling.SamplingResult(sampling.Decision.RECORD, attributes) - self.assertIs(result.decision, sampling.Decision.RECORD) + result = sampling.SamplingResult( + sampling.Decision.RECORD_ONLY, attributes + ) + self.assertIs(result.decision, sampling.Decision.RECORD_ONLY) with self.assertRaises(TypeError): result.attributes["test"] = "mess-this-up" self.assertTrue(len(result.attributes), 1) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index a6b4fa93e9..fabbc62d2e 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -538,7 +538,7 @@ def test_sampling_attributes(self): "attr-in-both": "decision-attr", } tracer_provider = trace.TracerProvider( - sampling.StaticSampler(sampling.Decision.RECORD_AND_SAMPLED,) + sampling.StaticSampler(sampling.Decision.RECORD_AND_SAMPLE,) ) self.tracer = tracer_provider.get_tracer(__name__) From 547320025c94d8bb03c4c2064cdc61bc1fce3e1c Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Thu, 17 Sep 2020 19:49:27 +0530 Subject: [PATCH 0553/1517] Added instrumentation for Tornado 6 and above (#1018) This commit adds auto-instrumentation for Tornado 6 and above on Python versions 3.5 and above. --- .../CHANGELOG.md | 5 + .../LICENSE | 201 ++++++++++ .../MANIFEST.in | 9 + .../README.rst | 39 ++ .../setup.cfg | 51 +++ .../setup.py | 38 ++ .../instrumentation/tornado/__init__.py | 263 +++++++++++++ .../instrumentation/tornado/client.py | 79 ++++ .../instrumentation/tornado/version.py | 15 + .../tests/__init__.py | 0 .../tests/test_instrumentation.py | 362 ++++++++++++++++++ .../tests/tornado_test_app.py | 99 +++++ .../instrumentation/bootstrap.py | 2 + .../util/src/opentelemetry/test/test_base.py | 7 + tox.ini | 8 + 15 files changed, 1178 insertions(+) create mode 100644 instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md create mode 100644 instrumentation/opentelemetry-instrumentation-tornado/LICENSE create mode 100644 instrumentation/opentelemetry-instrumentation-tornado/MANIFEST.in create mode 100644 instrumentation/opentelemetry-instrumentation-tornado/README.rst create mode 100644 instrumentation/opentelemetry-instrumentation-tornado/setup.cfg create mode 100644 instrumentation/opentelemetry-instrumentation-tornado/setup.py create mode 100644 instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py create mode 100644 instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py create mode 100644 instrumentation/opentelemetry-instrumentation-tornado/tests/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py create mode 100644 instrumentation/opentelemetry-instrumentation-tornado/tests/tornado_test_app.py diff --git a/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md new file mode 100644 index 0000000000..a2711d8a05 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release. Supports Tornado 6.x on Python 3.5 and newer. diff --git a/instrumentation/opentelemetry-instrumentation-tornado/LICENSE b/instrumentation/opentelemetry-instrumentation-tornado/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tornado/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/instrumentation/opentelemetry-instrumentation-tornado/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-tornado/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tornado/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-tornado/README.rst b/instrumentation/opentelemetry-instrumentation-tornado/README.rst new file mode 100644 index 0000000000..d84fbd0412 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tornado/README.rst @@ -0,0 +1,39 @@ +OpenTelemetry Tornado Instrumentation +====================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-tornado.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-tornado/ + +This library builds on the OpenTelemetry WSGI middleware to track web requests +in Tornado applications. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-tornado + +Configuration +------------- + +The following environment variables are supported as configuration options: + +- OTEL_PYTHON_TORNADO_EXCLUDED_URLS + +A comma separated list of paths that should not be automatically traced. For example, if this is set to + +:: + + export OTEL_PYTHON_TORNADO_EXLUDED_URLS='/healthz,/ping' + +Then any requests made to ``/healthz`` and ``/ping`` will not be automatically traced. + + +References +---------- + +* `OpenTelemetry Tornado Instrumentation `_ +* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg b/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg new file mode 100644 index 0000000000..84510f19f9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg @@ -0,0 +1,51 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-instrumentation-tornado +description = Tornado instrumentation for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-tornado +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + tornado >= 6.0 + opentelemetry-instrumentation == 0.13.dev0 + opentelemetry-api == 0.13.dev0 + +[options.extras_require] +test = + tornado >= 6.0 + opentelemetry-test == 0.13.dev0 + +[options.packages.find] +where = src diff --git a/instrumentation/opentelemetry-instrumentation-tornado/setup.py b/instrumentation/opentelemetry-instrumentation-tornado/setup.py new file mode 100644 index 0000000000..bd6814b50e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tornado/setup.py @@ -0,0 +1,38 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "tornado", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup( + version=PACKAGE_INFO["__version__"], + entry_points={ + "opentelemetry_instrumentor": [ + "tornado = opentelemetry.instrumentation.tornado:TornadoInstrumentor" + ] + }, +) diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py new file mode 100644 index 0000000000..6379d841a0 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py @@ -0,0 +1,263 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This library uses OpenTelemetry to track web requests in Tornado applications. + +Usage +----- + +.. code-block:: python + + import tornado.web + from opentelemetry.instrumentation.tornado import TornadoInstrumentor + + # apply tornado instrumentation + TornadoInstrumentor().instrument() + + class Handler(tornado.web.RequestHandler): + def get(self): + self.set_status(200) + + app = tornado.web.Application([(r"/", Handler)]) + app.listen(8080) + tornado.ioloop.IOLoop.current().start() +""" + +import inspect +import typing +from collections import namedtuple +from functools import partial, wraps +from logging import getLogger + +import tornado.web +import wrapt +from tornado.routing import Rule +from wrapt import wrap_function_wrapper + +from opentelemetry import configuration, context, propagators, trace +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.tornado.version import __version__ +from opentelemetry.instrumentation.utils import ( + http_status_to_canonical_code, + unwrap, +) +from opentelemetry.trace.status import Status +from opentelemetry.util import ExcludeList, time_ns + +from .client import fetch_async # pylint: disable=E0401 + +_logger = getLogger(__name__) +_TraceContext = namedtuple("TraceContext", ["activation", "span", "token"]) +_HANDLER_CONTEXT_KEY = "_otel_trace_context_key" +_OTEL_PATCHED_KEY = "_otel_patched_key" + + +def get_excluded_urls(): + urls = configuration.Configuration().TORNADO_EXCLUDED_URLS or "" + if urls: + urls = str.split(urls, ",") + return ExcludeList(urls) + + +_excluded_urls = get_excluded_urls() + + +class TornadoInstrumentor(BaseInstrumentor): + patched_handlers = [] + original_handler_new = None + + def _instrument(self, **kwargs): + """ + _instrument patches tornado.web.RequestHandler and tornado.httpclient.AsyncHTTPClient classes + to automatically instrument requests both received and sent by Tornado. + + We don't patch RequestHandler._execute as it causes some issues with contextvars based context. + Mainly the context fails to detach from within RequestHandler.on_finish() if it is attached inside + RequestHandler._execute. Same issue plagues RequestHandler.initialize. RequestHandler.prepare works + perfectly on the other hand as it executes in the same context as on_finish and log_exection which + are patched to finish a span after a request is served. + + However, we cannot just patch RequestHandler's prepare method because it is supposed to be overwridden + by sub-classes and since the parent prepare method does not do anything special, sub-classes don't + have to call super() when overriding the method. + + In order to work around this, we patch the __init__ method of RequestHandler and then dynamically patch + the prepare, on_finish and log_exception methods of the derived classes _only_ the first time we see them. + Note that the patch does not apply on every single __init__ call, only the first one for the enture + process lifetime. + """ + tracer_provider = kwargs.get("tracer_provider") + tracer = trace.get_tracer(__name__, __version__, tracer_provider) + + def handler_init(init, handler, args, kwargs): + cls = handler.__class__ + if patch_handler_class(tracer, cls): + self.patched_handlers.append(cls) + return init(*args, **kwargs) + + wrap_function_wrapper( + "tornado.web", "RequestHandler.__init__", handler_init + ) + wrap_function_wrapper( + "tornado.httpclient", + "AsyncHTTPClient.fetch", + partial(fetch_async, tracer), + ) + + def _uninstrument(self, **kwargs): + unwrap(tornado.web.RequestHandler, "__init__") + unwrap(tornado.httpclient.AsyncHTTPClient, "fetch") + for handler in self.patched_handlers: + unpatch_handler_class(handler) + self.patched_handlers = [] + + +def patch_handler_class(tracer, cls): + if getattr(cls, _OTEL_PATCHED_KEY, False): + return False + + setattr(cls, _OTEL_PATCHED_KEY, True) + _wrap(cls, "prepare", partial(_prepare, tracer)) + _wrap(cls, "on_finish", partial(_on_finish, tracer)) + _wrap(cls, "log_exception", partial(_log_exception, tracer)) + return True + + +def unpatch_handler_class(cls): + if not getattr(cls, _OTEL_PATCHED_KEY, False): + return + + unwrap(cls, "prepare") + unwrap(cls, "on_finish") + unwrap(cls, "log_exception") + delattr(cls, _OTEL_PATCHED_KEY) + + +def _wrap(cls, method_name, wrapper): + original = getattr(cls, method_name) + wrapper = wrapt.FunctionWrapper(original, wrapper) + wrapt.apply_patch(cls, method_name, wrapper) + + +def _prepare(tracer, func, handler, args, kwargs): + start_time = time_ns() + request = handler.request + if _excluded_urls.url_disabled(request.uri): + return func(*args, **kwargs) + _start_span(tracer, handler, start_time) + return func(*args, **kwargs) + + +def _on_finish(tracer, func, handler, args, kwargs): + _finish_span(tracer, handler) + return func(*args, **kwargs) + + +def _log_exception(tracer, func, handler, args, kwargs): + error = None + if len(args) == 3: + error = args[1] + + _finish_span(tracer, handler, error) + return func(*args, **kwargs) + + +def _get_header_from_request_headers( + headers: dict, header_name: str +) -> typing.List[str]: + header = headers.get(header_name) + return [header] if header else [] + + +def _get_attributes_from_request(request): + attrs = { + "component": "tornado", + "http.method": request.method, + "http.scheme": request.protocol, + "http.host": request.host, + "http.target": request.path, + } + + if request.host: + attrs["http.host"] = request.host + + if request.remote_ip: + attrs["net.peer.ip"] = request.remote_ip + + return attrs + + +def _get_operation_name(handler, request): + full_class_name = type(handler).__name__ + class_name = full_class_name.rsplit(".")[-1] + return "{0}.{1}".format(class_name, request.method.lower()) + + +def _start_span(tracer, handler, start_time) -> _TraceContext: + token = context.attach( + propagators.extract( + _get_header_from_request_headers, handler.request.headers, + ) + ) + span = tracer.start_span( + _get_operation_name(handler, handler.request), + kind=trace.SpanKind.SERVER, + attributes=_get_attributes_from_request(handler.request), + start_time=start_time, + ) + + activation = tracer.use_span(span, end_on_exit=True) + activation.__enter__() + ctx = _TraceContext(activation, span, token) + setattr(handler, _HANDLER_CONTEXT_KEY, ctx) + return ctx + + +def _finish_span(tracer, handler, error=None): + status_code = handler.get_status() + reason = getattr(handler, "_reason") + finish_args = (None, None, None) + ctx = getattr(handler, _HANDLER_CONTEXT_KEY, None) + + if error: + if isinstance(error, tornado.web.HTTPError): + status_code = error.status_code + if not ctx and status_code == 404: + ctx = _start_span(tracer, handler, time_ns()) + if status_code != 404: + finish_args = ( + type(error), + error, + getattr(error, "__traceback__", None), + ) + status_code = 500 + reason = None + + if not ctx: + return + + if reason: + ctx.span.set_attribute("http.status_text", reason) + ctx.span.set_attribute("http.status_code", status_code) + ctx.span.set_status( + Status( + canonical_code=http_status_to_canonical_code(status_code), + description=reason, + ) + ) + + ctx.activation.__exit__(*finish_args) + context.detach(ctx.token) + delattr(handler, _HANDLER_CONTEXT_KEY) diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py new file mode 100644 index 0000000000..1fe8f58a2c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py @@ -0,0 +1,79 @@ +import functools + +from tornado.httpclient import HTTPError, HTTPRequest + +from opentelemetry import propagators, trace +from opentelemetry.instrumentation.utils import http_status_to_canonical_code +from opentelemetry.trace.status import Status +from opentelemetry.util import time_ns + + +def _normalize_request(args, kwargs): + req = args[0] + if not isinstance(req, str): + return (args, kwargs) + + new_kwargs = {} + for param in ("callback", "raise_error"): + if param in kwargs: + new_kwargs[param] = kwargs.pop(param) + + req = HTTPRequest(req, **kwargs) + new_args = [req] + new_args.extend(args[1:]) + return (new_args, new_kwargs) + + +def fetch_async(tracer, func, _, args, kwargs): + start_time = time_ns() + + # Return immediately if no args were provided (error) + # or original_request is set (meaning we are in a redirect step). + if len(args) == 0 or hasattr(args[0], "original_request"): + return func(*args, **kwargs) + + # Force the creation of a HTTPRequest object if needed, + # so we can inject the context into the headers. + args, kwargs = _normalize_request(args, kwargs) + request = args[0] + + span = tracer.start_span( + request.method, + kind=trace.SpanKind.CLIENT, + attributes={ + "component": "tornado", + "http.url": request.url, + "http.method": request.method, + }, + start_time=start_time, + ) + + with tracer.use_span(span): + propagators.inject(type(request.headers).__setitem__, request.headers) + future = func(*args, **kwargs) + future.add_done_callback( + functools.partial(_finish_tracing_callback, span=span) + ) + return future + + +def _finish_tracing_callback(future, span): + status_code = None + description = None + exc = future.exception() + if exc: + if isinstance(exc, HTTPError): + status_code = exc.code + description = "{}: {}".format(type(exc).__name__, exc) + else: + status_code = future.result().code + + if status_code is not None: + span.set_attribute("http.status_code", status_code) + span.set_status( + Status( + canonical_code=http_status_to_canonical_code(status_code), + description=description, + ) + ) + span.end() diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py new file mode 100644 index 0000000000..9cc445d09e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.13dev0" diff --git a/instrumentation/opentelemetry-instrumentation-tornado/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py b/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py new file mode 100644 index 0000000000..2d5ffd00f7 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py @@ -0,0 +1,362 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from unittest.mock import patch + +from tornado.testing import AsyncHTTPTestCase + +from opentelemetry import trace +from opentelemetry.instrumentation.tornado import ( + TornadoInstrumentor, + patch_handler_class, + unpatch_handler_class, +) +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import SpanKind +from opentelemetry.util import ExcludeList + +from .tornado_test_app import ( + AsyncHandler, + DynamicHandler, + MainHandler, + make_app, +) + + +class TornadoTest(AsyncHTTPTestCase, TestBase): + def get_app(self): + tracer = trace.get_tracer(__name__) + app = make_app(tracer) + return app + + def setUp(self): + TornadoInstrumentor().instrument() + super().setUp() + + def tearDown(self): + TornadoInstrumentor().uninstrument() + super().tearDown() + + +class TestTornadoInstrumentor(TornadoTest): + def test_patch_references(self): + self.assertEqual(len(TornadoInstrumentor().patched_handlers), 0) + + self.fetch("/") + self.fetch("/async") + self.assertEqual( + TornadoInstrumentor().patched_handlers, [MainHandler, AsyncHandler] + ) + + self.fetch("/async") + self.fetch("/") + self.assertEqual( + TornadoInstrumentor().patched_handlers, [MainHandler, AsyncHandler] + ) + + TornadoInstrumentor().uninstrument() + self.assertEqual(TornadoInstrumentor().patched_handlers, []) + + def test_patch_applied_only_once(self): + tracer = trace.get_tracer(__name__) + self.assertTrue(patch_handler_class(tracer, AsyncHandler)) + self.assertFalse(patch_handler_class(tracer, AsyncHandler)) + self.assertFalse(patch_handler_class(tracer, AsyncHandler)) + unpatch_handler_class(AsyncHandler) + + +class TestTornadoInstrumentation(TornadoTest): + def test_http_calls(self): + methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] + for method in methods: + self._test_http_method_call(method) + + def _test_http_method_call(self, method): + body = "" if method in ["POST", "PUT", "PATCH"] else None + response = self.fetch("/", method=method, body=body) + self.assertEqual(response.code, 201) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 3) + + manual, server, client = self.sorted_spans(spans) + + self.assertEqual(manual.name, "manual") + self.assertEqual(manual.parent, server.context) + self.assertEqual(manual.context.trace_id, client.context.trace_id) + + self.assertEqual(server.name, "MainHandler." + method.lower()) + self.assertTrue(server.parent.is_remote) + self.assertNotEqual(server.parent, client.context) + self.assertEqual(server.parent.span_id, client.context.span_id) + self.assertEqual(server.context.trace_id, client.context.trace_id) + self.assertEqual(server.kind, SpanKind.SERVER) + self.assert_span_has_attributes( + server, + { + "component": "tornado", + "http.method": method, + "http.scheme": "http", + "http.host": "127.0.0.1:" + str(self.get_http_port()), + "http.target": "/", + "net.peer.ip": "127.0.0.1", + "http.status_text": "Created", + "http.status_code": 201, + }, + ) + + self.assertEqual(client.name, method) + self.assertFalse(client.context.is_remote) + self.assertIsNone(client.parent) + self.assertEqual(client.kind, SpanKind.CLIENT) + self.assert_span_has_attributes( + client, + { + "component": "tornado", + "http.url": self.get_url("/"), + "http.method": method, + "http.status_code": 201, + }, + ) + + self.memory_exporter.clear() + + def test_async_handler(self): + self._test_async_handler("/async", "AsyncHandler") + + def test_coroutine_handler(self): + self._test_async_handler("/cor", "CoroutineHandler") + + def _test_async_handler(self, url, handler_name): + response = self.fetch(url) + self.assertEqual(response.code, 201) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 5) + + sub2, sub1, sub_wrapper, server, client = self.sorted_spans(spans) + + self.assertEqual(sub2.name, "sub-task-2") + self.assertEqual(sub2.parent, sub_wrapper.context) + self.assertEqual(sub2.context.trace_id, client.context.trace_id) + + self.assertEqual(sub1.name, "sub-task-1") + self.assertEqual(sub1.parent, sub_wrapper.context) + self.assertEqual(sub1.context.trace_id, client.context.trace_id) + + self.assertEqual(sub_wrapper.name, "sub-task-wrapper") + self.assertEqual(sub_wrapper.parent, server.context) + self.assertEqual(sub_wrapper.context.trace_id, client.context.trace_id) + + self.assertEqual(server.name, handler_name + ".get") + self.assertTrue(server.parent.is_remote) + self.assertNotEqual(server.parent, client.context) + self.assertEqual(server.parent.span_id, client.context.span_id) + self.assertEqual(server.context.trace_id, client.context.trace_id) + self.assertEqual(server.kind, SpanKind.SERVER) + self.assert_span_has_attributes( + server, + { + "component": "tornado", + "http.method": "GET", + "http.scheme": "http", + "http.host": "127.0.0.1:" + str(self.get_http_port()), + "http.target": url, + "net.peer.ip": "127.0.0.1", + "http.status_text": "Created", + "http.status_code": 201, + }, + ) + + self.assertEqual(client.name, "GET") + self.assertFalse(client.context.is_remote) + self.assertIsNone(client.parent) + self.assertEqual(client.kind, SpanKind.CLIENT) + self.assert_span_has_attributes( + client, + { + "component": "tornado", + "http.url": self.get_url(url), + "http.method": "GET", + "http.status_code": 201, + }, + ) + + def test_500(self): + response = self.fetch("/error") + self.assertEqual(response.code, 500) + + spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) + self.assertEqual(len(spans), 2) + server, client = spans + + self.assertEqual(server.name, "BadHandler.get") + self.assertEqual(server.kind, SpanKind.SERVER) + self.assert_span_has_attributes( + server, + { + "component": "tornado", + "http.method": "GET", + "http.scheme": "http", + "http.host": "127.0.0.1:" + str(self.get_http_port()), + "http.target": "/error", + "net.peer.ip": "127.0.0.1", + "http.status_code": 500, + }, + ) + + self.assertEqual(client.name, "GET") + self.assertEqual(client.kind, SpanKind.CLIENT) + self.assert_span_has_attributes( + client, + { + "component": "tornado", + "http.url": self.get_url("/error"), + "http.method": "GET", + "http.status_code": 500, + }, + ) + + def test_404(self): + response = self.fetch("/missing-url") + self.assertEqual(response.code, 404) + + spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) + self.assertEqual(len(spans), 2) + server, client = spans + + self.assertEqual(server.name, "ErrorHandler.get") + self.assertEqual(server.kind, SpanKind.SERVER) + self.assert_span_has_attributes( + server, + { + "component": "tornado", + "http.method": "GET", + "http.scheme": "http", + "http.host": "127.0.0.1:" + str(self.get_http_port()), + "http.target": "/missing-url", + "net.peer.ip": "127.0.0.1", + "http.status_text": "Not Found", + "http.status_code": 404, + }, + ) + + self.assertEqual(client.name, "GET") + self.assertEqual(client.kind, SpanKind.CLIENT) + self.assert_span_has_attributes( + client, + { + "component": "tornado", + "http.url": self.get_url("/missing-url"), + "http.method": "GET", + "http.status_code": 404, + }, + ) + + def test_dynamic_handler(self): + response = self.fetch("/dyna") + self.assertEqual(response.code, 404) + self.memory_exporter.clear() + + self._app.add_handlers(r".+", [(r"/dyna", DynamicHandler)]) + + response = self.fetch("/dyna") + self.assertEqual(response.code, 202) + + spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) + self.assertEqual(len(spans), 2) + server, client = spans + + self.assertEqual(server.name, "DynamicHandler.get") + self.assertTrue(server.parent.is_remote) + self.assertNotEqual(server.parent, client.context) + self.assertEqual(server.parent.span_id, client.context.span_id) + self.assertEqual(server.context.trace_id, client.context.trace_id) + self.assertEqual(server.kind, SpanKind.SERVER) + self.assert_span_has_attributes( + server, + { + "component": "tornado", + "http.method": "GET", + "http.scheme": "http", + "http.host": "127.0.0.1:" + str(self.get_http_port()), + "http.target": "/dyna", + "net.peer.ip": "127.0.0.1", + "http.status_text": "Accepted", + "http.status_code": 202, + }, + ) + + self.assertEqual(client.name, "GET") + self.assertFalse(client.context.is_remote) + self.assertIsNone(client.parent) + self.assertEqual(client.kind, SpanKind.CLIENT) + self.assert_span_has_attributes( + client, + { + "component": "tornado", + "http.url": self.get_url("/dyna"), + "http.method": "GET", + "http.status_code": 202, + }, + ) + + @patch( + "opentelemetry.instrumentation.tornado._excluded_urls", + ExcludeList(["healthz", "ping"]), + ) + def test_exclude_lists(self): + def test_excluded(path): + self.fetch(path) + spans = self.sorted_spans( + self.memory_exporter.get_finished_spans() + ) + self.assertEqual(len(spans), 1) + client = spans[0] + self.assertEqual(client.name, "GET") + self.assertEqual(client.kind, SpanKind.CLIENT) + self.assert_span_has_attributes( + client, + { + "component": "tornado", + "http.url": self.get_url(path), + "http.method": "GET", + "http.status_code": 200, + }, + ) + self.memory_exporter.clear() + + test_excluded("/healthz") + test_excluded("/ping") + + +class TestTornadoUninstrument(TornadoTest): + def test_uninstrument(self): + response = self.fetch("/") + self.assertEqual(response.code, 201) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 3) + manual, server, client = self.sorted_spans(spans) + self.assertEqual(manual.name, "manual") + self.assertEqual(server.name, "MainHandler.get") + self.assertEqual(client.name, "GET") + self.memory_exporter.clear() + + TornadoInstrumentor().uninstrument() + + response = self.fetch("/") + self.assertEqual(response.code, 201) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + manual = spans[0] + self.assertEqual(manual.name, "manual") diff --git a/instrumentation/opentelemetry-instrumentation-tornado/tests/tornado_test_app.py b/instrumentation/opentelemetry-instrumentation-tornado/tests/tornado_test_app.py new file mode 100644 index 0000000000..307dc60b76 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tornado/tests/tornado_test_app.py @@ -0,0 +1,99 @@ +# pylint: disable=W0223,R0201 + +import tornado.web +from tornado import gen + + +class AsyncHandler(tornado.web.RequestHandler): + async def get(self): + with self.application.tracer.start_as_current_span("sub-task-wrapper"): + await self.do_something1() + await self.do_something2() + self.set_status(201) + self.write("{}") + + async def do_something1(self): + with self.application.tracer.start_as_current_span("sub-task-1"): + tornado.gen.sleep(0.1) + + async def do_something2(self): + with self.application.tracer.start_as_current_span("sub-task-2"): + tornado.gen.sleep(0.1) + + +class CoroutineHandler(tornado.web.RequestHandler): + @gen.coroutine + def get(self): + with self.application.tracer.start_as_current_span("sub-task-wrapper"): + yield self.do_something1() + yield self.do_something2() + self.set_status(201) + self.write("{}") + + @gen.coroutine + def do_something1(self): + with self.application.tracer.start_as_current_span("sub-task-1"): + tornado.gen.sleep(0.1) + + @gen.coroutine + def do_something2(self): + with self.application.tracer.start_as_current_span("sub-task-2"): + tornado.gen.sleep(0.1) + + +class MainHandler(tornado.web.RequestHandler): + def _handler(self): + with self.application.tracer.start_as_current_span("manual"): + self.write("Hello, world") + self.set_status(201) + + def get(self): + return self._handler() + + def post(self): + return self._handler() + + def patch(self): + return self._handler() + + def delete(self): + return self._handler() + + def put(self): + return self._handler() + + def head(self): + return self._handler() + + def options(self): + return self._handler() + + +class BadHandler(tornado.web.RequestHandler): + def get(self): + raise NameError("some random name error") + + +class DynamicHandler(tornado.web.RequestHandler): + def get(self): + self.set_status(202) + + +class HealthCheckHandler(tornado.web.RequestHandler): + def get(self): + self.set_status(200) + + +def make_app(tracer): + app = tornado.web.Application( + [ + (r"/", MainHandler), + (r"/error", BadHandler), + (r"/cor", CoroutineHandler), + (r"/async", AsyncHandler), + (r"/healthz", HealthCheckHandler), + (r"/ping", HealthCheckHandler), + ] + ) + app.tracer = tracer + return app diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index c7022d9bc4..d11535d31c 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -49,6 +49,7 @@ "sqlalchemy": "opentelemetry-instrumentation-sqlalchemy>=0.8b0", "sqlite3": "opentelemetry-instrumentation-sqlite3>=0.11b0", "starlette": "opentelemetry-instrumentation-starlette>=0.11b0", + "tornado": "opentelemetry-instrumentation-tornado>=0.13b0", "wsgi": "opentelemetry-instrumentation-wsgi>=0.8b0", } @@ -78,6 +79,7 @@ "sqlalchemy": ("opentelemetry-instrumentation-sqlalchemy",), "sqlite3": ("opentelemetry-instrumentation-sqlite3",), "starlette": ("opentelemetry-instrumentation-starlette",), + "tornado": ("opentelemetry-instrumentation-tornado",), "wsgi": ("opentelemetry-instrumentation-wsgi",), } diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index e2d99c0acf..e1dbc55e62 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -69,6 +69,13 @@ def assert_span_has_attributes(self, span, attributes): self.assertIn(key, span.attributes) self.assertEqual(val, span.attributes[key]) + def sorted_spans(self, spans): # pylint: disable=R0201 + return sorted( + spans, + key=lambda s: s._start_time, # pylint: disable=W0212 + reverse=True, + ) + @staticmethod def create_tracer_provider(**kwargs): """Helper to create a configured tracer provider. diff --git a/tox.ini b/tox.ini index 384c43725b..b477d5ba74 100644 --- a/tox.ini +++ b/tox.ini @@ -169,6 +169,11 @@ envlist = ; instrumentation-system-metrics intentionally excluded from pypy3 ; known limitation: gc.get_count won't work under pypy + ; opentelemetry-instrumentation-tornado + ; instrumentation supports >=6 on Py 3.5 and above. + py3{5,6,7,8}-test-instrumentation-tornado + pypy3-test-instrumentation-tornado + lint py38-tracecontext py38-{mypy,mypyinstalled} @@ -229,6 +234,7 @@ changedir = test-instrumentation-sqlite3: instrumentation/opentelemetry-instrumentation-sqlite3/tests test-instrumentation-starlette: instrumentation/opentelemetry-instrumentation-starlette/tests test-instrumentation-system-metrics: instrumentation/opentelemetry-instrumentation-system-metrics/tests + test-instrumentation-tornado: instrumentation/opentelemetry-instrumentation-tornado/tests test-instrumentation-wsgi: instrumentation/opentelemetry-instrumentation-wsgi/tests test-exporter-jaeger: exporter/opentelemetry-exporter-jaeger/tests @@ -303,6 +309,8 @@ commands_pre = starlette: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette[test] + tornado: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado + jinja2: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2[test] aiohttp-client: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client From 02aec8f4ed425cb86d15a816c4314decd5bb2707 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 17 Sep 2020 08:23:52 -0700 Subject: [PATCH 0554/1517] release: updating changelogs and version to 0.13b0 (#1129) * updating changelogs and version to 0.13b0 --- docs/examples/opentelemetry-example-app/setup.cfg | 8 ++++---- .../src/opentelemetry_example_app/version.py | 2 +- exporter/opentelemetry-exporter-datadog/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/datadog/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-prometheus/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- .../CHANGELOG.md | 4 ++++ .../setup.cfg | 2 +- .../instrumentation/aiohttp_client/version.py | 2 +- .../opentelemetry-instrumentation-aiopg/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/aiopg/version.py | 2 +- .../opentelemetry-instrumentation-asgi/setup.cfg | 4 ++-- .../src/opentelemetry/instrumentation/asgi/version.py | 2 +- .../opentelemetry-instrumentation-asyncpg/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/asyncpg/version.py | 2 +- .../opentelemetry-instrumentation-boto/CHANGELOG.md | 6 +++++- .../opentelemetry-instrumentation-boto/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/boto/version.py | 2 +- .../opentelemetry-instrumentation-botocore/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-botocore/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/botocore/version.py | 2 +- .../opentelemetry-instrumentation-celery/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/celery/version.py | 2 +- .../opentelemetry-instrumentation-dbapi/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-dbapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/dbapi/version.py | 2 +- .../opentelemetry-instrumentation-django/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-django/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/django/version.py | 2 +- .../CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-elasticsearch/setup.cfg | 6 +++--- .../instrumentation/elasticsearch/version.py | 2 +- .../opentelemetry-instrumentation-falcon/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-falcon/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/falcon/version.py | 2 +- .../opentelemetry-instrumentation-fastapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/fastapi/version.py | 2 +- .../opentelemetry-instrumentation-flask/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-flask/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/flask/version.py | 2 +- .../opentelemetry-instrumentation-grpc/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-grpc/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/grpc/version.py | 2 +- .../opentelemetry-instrumentation-jinja2/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-jinja2/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/jinja2/version.py | 2 +- .../opentelemetry-instrumentation-mysql/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-mysql/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/mysql/version.py | 2 +- .../CHANGELOG.md | 4 ++++ .../setup.cfg | 4 ++-- .../instrumentation/opentracing_shim/version.py | 2 +- .../opentelemetry-instrumentation-psycopg2/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-psycopg2/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/psycopg2/version.py | 2 +- .../opentelemetry-instrumentation-pymemcache/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-pymemcache/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/pymemcache/version.py | 2 +- .../opentelemetry-instrumentation-pymongo/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-pymongo/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/pymongo/version.py | 2 +- .../opentelemetry-instrumentation-pymysql/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-pymysql/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/pymysql/version.py | 2 +- .../opentelemetry-instrumentation-pyramid/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-pyramid/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/pyramid/version.py | 2 +- .../opentelemetry-instrumentation-redis/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-redis/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/redis/version.py | 2 +- .../opentelemetry-instrumentation-requests/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-requests/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/requests/version.py | 2 +- .../opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-sqlalchemy/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/sqlalchemy/version.py | 2 +- .../opentelemetry-instrumentation-sqlite3/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-sqlite3/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/sqlite3/version.py | 2 +- .../opentelemetry-instrumentation-starlette/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/starlette/version.py | 2 +- .../CHANGELOG.md | 4 ++++ .../setup.cfg | 6 +++--- .../instrumentation/system_metrics/version.py | 2 +- .../opentelemetry-instrumentation-tornado/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-tornado/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/tornado/version.py | 2 +- .../opentelemetry-instrumentation-wsgi/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-wsgi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/wsgi/version.py | 2 +- opentelemetry-api/CHANGELOG.md | 4 ++++ opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-instrumentation/CHANGELOG.md | 4 ++++ opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/CHANGELOG.md | 4 ++++ opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 4 ++++ opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 114 files changed, 292 insertions(+), 160 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index effa303039..77c94baf31 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -42,10 +42,10 @@ zip_safe = False include_package_data = True install_requires = typing; python_version<'3.5' - opentelemetry-api == 0.13dev0 - opentelemetry-sdk == 0.13dev0 - opentelemetry-instrumentation-requests == 0.13dev0 - opentelemetry-instrumentation-flask == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-sdk == 0.13b0 + opentelemetry-instrumentation-requests == 0.13b0 + opentelemetry-instrumentation-flask == 0.13b0 flask requests protobuf~=3.11 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py index 9cc445d09e..2015e87c70 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/exporter/opentelemetry-exporter-datadog/setup.cfg b/exporter/opentelemetry-exporter-datadog/setup.cfg index 8975f25097..2b2dbd1c63 100644 --- a/exporter/opentelemetry-exporter-datadog/setup.cfg +++ b/exporter/opentelemetry-exporter-datadog/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = ddtrace>=0.34.0 - opentelemetry-api == 0.13dev0 - opentelemetry-sdk == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-sdk == 0.13b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py index 9cc445d09e..2015e87c70 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md index fbca139692..1545e187fc 100644 --- a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## Unreleased + +## Version 0.13b0 + +Released 2020-09-17 - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - Report instrumentation info diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 94e4336b83..2994f4a7ff 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.13dev0 - opentelemetry-sdk == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-sdk == 0.13b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index db7a75909b..1b292ac483 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md index 8e04322d0a..3e02906563 100644 --- a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4' ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index a732af025e..542d6fb897 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.13dev0 - opentelemetry-sdk == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-sdk == 0.13b0 protobuf >= 3.8.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 9cc445d09e..2015e87c70 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 49e6d8c941..94ae999eca 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Add instrumentation info to exported spans ([#1095](https://github.com/open-telemetry/opentelemetry-python/pull/1095)) - Add metric OTLP exporter diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 3a6ca8f957..af141f1b6b 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.13dev0 - opentelemetry-sdk == 0.13dev0 - opentelemetry-proto == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-sdk == 0.13b0 + opentelemetry-proto == 0.13b0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 9cc445d09e..2015e87c70 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md b/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md index 62cd9d3ae9..618e2c4146 100644 --- a/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index 5d35fb64ee..b806c8079d 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.13dev0 - opentelemetry-sdk == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-sdk == 0.13b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 9cc445d09e..2015e87c70 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index b456edea27..562e5cc124 100644 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Add support for OTEL_EXPORTER_ZIPKIN_ENDPOINT env var. As part of this change, the configuration of the ZipkinSpanExporter exposes a `url` argument to replace `host_name`, `port`, `protocol`, `endpoint`. This brings this implementation inline with other diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 86bc98eef4..4e96debbb0 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 0.13dev0 - opentelemetry-sdk == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-sdk == 0.13b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 9cc445d09e..2015e87c70 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md index 78b989563f..286d4ca642 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Updating span name to match semantic conventions ([#972](https://github.com/open-telemetry/opentelemetry-python/pull/972)) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index a222f323c0..a3e3d27065 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -40,7 +40,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api >= 0.12.dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-instrumentation == 0.13b0 aiohttp ~= 3.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py index 1a40cfa0f1..cbee121c0b 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg index f7e89a248c..d837db3733 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation-dbapi == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation-dbapi == 0.13b0 + opentelemetry-instrumentation == 0.13b0 aiopg >= 0.13.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg index 533a636d51..3375f900a1 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 asgiref ~= 3.0 [options.extras_require] diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg index 2f267b3f49..7f751c5093 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 asyncpg >= 0.12.0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md index 8aef34f14d..ea74ff9de2 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog -## Unreleased +## Unreleased + +## Version 0.13b0 + +Released 2020-09-17 - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg index 863e5c7336..521a8f83c9 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg @@ -40,15 +40,15 @@ package_dir= packages=find_namespace: install_requires = boto ~= 2.0 - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 - opentelemetry-instrumentation-botocore == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 + opentelemetry-instrumentation-botocore == 0.13b0 [options.extras_require] test = boto~=2.0 moto~=1.0 - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py +++ b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md index b209b3a9d0..9a01165b8f 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg index 84d0e42927..bcda00216d 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg @@ -40,13 +40,13 @@ package_dir= packages=find_namespace: install_requires = botocore ~= 1.0 - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 [options.extras_require] test = moto ~= 1.0 - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg index 5b027f6fe3..2b345093bf 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 celery ~= 4.0 [options.extras_require] test = pytest - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md index 5629a23aae..e110055da1 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - bugfix: cursors and connections now produce spans when used with context managers ([#1028](https://github.com/open-telemetry/opentelemetry-python/pull/1028)) - Drop support for Python 3.4 diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg index 1ba131c8e2..fcf4471831 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index 3ab4939604..ea9b3f56a7 100644 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-django/setup.cfg b/instrumentation/opentelemetry-instrumentation-django/setup.cfg index fd6eb37e4b..a5933e326d 100644 --- a/instrumentation/opentelemetry-instrumentation-django/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-django/setup.cfg @@ -40,13 +40,13 @@ package_dir= packages=find_namespace: install_requires = django >= 1.10 - opentelemetry-instrumentation-wsgi == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 - opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation-wsgi == 0.13b0 + opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.13b0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md index a81d106458..f07f514023 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg index 14053c6295..456ff0859f 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 wrapt >= 1.0.0, < 2.0.0 elasticsearch >= 2.0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 elasticsearch-dsl >= 2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md index 3be1e19dc6..e14c730f74 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Initial release. Added instrumentation for Falcon 2.0+ diff --git a/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg b/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg index ed6dc0838b..9812b5b415 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = falcon ~= 2.0 - opentelemetry-instrumentation-wsgi == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 - opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation-wsgi == 0.13b0 + opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.13b0 [options.extras_require] test = falcon ~= 2.0 - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 parameterized == 0.7.4 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg index 514b2a3754..9334235f56 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation-asgi == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation-asgi == 0.13b0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 fastapi ~= 0.58.1 requests ~= 2.23.0 # needed for testclient diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md index bfc3269236..a4641e55b6 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg index a8c28f4dd5..7acef44321 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg @@ -40,14 +40,14 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-instrumentation-wsgi == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 - opentelemetry-api == 0.13dev0 + opentelemetry-instrumentation-wsgi == 0.13b0 + opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.13b0 [options.extras_require] test = flask~=1.0 - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md index bbf2e7b189..ed6889dcb7 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg index 665e9a9417..e511bcf4fb 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-sdk == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-sdk == 0.13b0 grpcio ~= 1.27 [options.extras_require] test = - opentelemetry-test == 0.13dev0 - opentelemetry-sdk == 0.13dev0 + opentelemetry-test == 0.13b0 + opentelemetry-sdk == 0.13b0 protobuf == 3.12.2 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md index 15fc9bd6f0..b1f9ba50e1 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg index 467cbd93a9..9bdd6815df 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg @@ -38,14 +38,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md index f024cf6959..8356377212 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg index cc0b435c74..946cec18c7 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation-dbapi == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation-dbapi == 0.13b0 + opentelemetry-instrumentation == 0.13b0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md index 85a7d42c39..be243d9584 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg index 4054344236..b324742d16 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg @@ -41,11 +41,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.13dev0 + opentelemetry-api == 0.13b0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md index 506f444865..2aa477de2d 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg index 5dffe4abb3..fcbc4d2039 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation-dbapi == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation-dbapi == 0.13b0 + opentelemetry-instrumentation == 0.13b0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md index 6a35aece8b..5cff2cb6e6 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg index dcf76a525c..e39a1a5419 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 pymemcache ~= 1.3 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md index 787f9cdd69..2a6c0308d5 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg index b157f7b6a2..a7a12e0bb4 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 pymongo ~= 3.1 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md index ead662f3b7..135b985936 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg index 39ab09166a..846f7603c7 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation-dbapi == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation-dbapi == 0.13b0 + opentelemetry-instrumentation == 0.13b0 PyMySQL ~= 0.9.3 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md index 0ef3a2e79c..6f78bfc16a 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg index 91eafb965c..870888da73 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg @@ -40,15 +40,15 @@ package_dir= packages=find_namespace: install_requires = pyramid >= 1.7 - opentelemetry-instrumentation == 0.13dev0 - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation-wsgi == 0.13dev0 + opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation-wsgi == 0.13b0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = werkzeug == 0.16.1 - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md index 22442c0a37..8f2d5f7e84 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg index 6562b86719..dd9585d019 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 redis >= 2.6 wrapt >= 1.12.1 [options.extras_require] test = - opentelemetry-test == 0.13dev0 - opentelemetry-sdk == 0.13dev0 + opentelemetry-test == 0.13b0 + opentelemetry-sdk == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md index d42fd8f829..5b876bd4ad 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Add support for instrumenting prepared requests ([#1040](https://github.com/open-telemetry/opentelemetry-python/pull/1040)) - Drop support for Python 3.4 diff --git a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg index fbfa848ab9..e0a9838a00 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 requests ~= 2.0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 httpretty ~= 1.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md index ff4e540292..73fa087a11 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg index 41bd0d2b6e..c0e2e7fd40 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 wrapt >= 1.11.2 sqlalchemy [options.extras_require] test = - opentelemetry-sdk == 0.13dev0 + opentelemetry-sdk == 0.13b0 pytest [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md index f2fb2af5ed..3954560408 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg index a0aba2d1b4..312bd2975d 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation-dbapi == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation-dbapi == 0.13b0 + opentelemetry-instrumentation == 0.13b0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg index c94d76c88c..5c25d37831 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation-asgi == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation-asgi == 0.13b0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 starlette ~= 0.13.0 requests ~= 2.23.0 # needed for testclient diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md index ac192b60c0..7a655ad231 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg index b7fbb90cb1..2aa55ad668 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-sdk == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-sdk == 0.13b0 psutil ~= 5.7.0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md index a2711d8a05..7cc628718d 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md @@ -2,4 +2,8 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Initial release. Supports Tornado 6.x on Python 3.5 and newer. diff --git a/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg b/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg index 84510f19f9..7cb1e3c1fa 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg @@ -39,13 +39,13 @@ package_dir= packages=find_namespace: install_requires = tornado >= 6.0 - opentelemetry-instrumentation == 0.13.dev0 - opentelemetry-api == 0.13.dev0 + opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.13b0 [options.extras_require] test = tornado >= 6.0 - opentelemetry-test == 0.13.dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md index 0bde2bb144..68bd25237c 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg index 5c4cae94d3..bce6df7522 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg @@ -39,12 +39,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.13b0 [options.extras_require] test = - opentelemetry-test == 0.13dev0 + opentelemetry-test == 0.13b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py index 9cc445d09e..2015e87c70 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index f058be3702..54916dc992 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Refactor `SpanContext.is_valid` from a method to a data attribute ([#1005](https://github.com/open-telemetry/opentelemetry-python/pull/1005)) - Moved samplers from API to SDK diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 9cc445d09e..2015e87c70 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/opentelemetry-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md index 91dc011f09..5aa47514e0 100644 --- a/opentelemetry-instrumentation/CHANGELOG.md +++ b/opentelemetry-instrumentation/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index fd131a42cb..78443bb176 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.13dev0 + opentelemetry-api == 0.13b0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 9cc445d09e..2015e87c70 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/opentelemetry-proto/CHANGELOG.md b/opentelemetry-proto/CHANGELOG.md index 5eb4d4561e..0b6ba3f1f1 100644 --- a/opentelemetry-proto/CHANGELOG.md +++ b/opentelemetry-proto/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 9cc445d09e..2015e87c70 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index a046d1d919..5faa74b3cf 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Moved samplers from API to SDK ([#1023](https://github.com/open-telemetry/opentelemetry-python/pull/1023)) - Sampling spec changes diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index f296088422..41077916be 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.13dev0 + opentelemetry-api == 0.13b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 9cc445d09e..2015e87c70 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 70efafb1a0..9fb14ac879 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.13dev0" +__version__ = "0.13b0" From b923c52688f0af39218646840c4458c7bbebb0d8 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 17 Sep 2020 12:21:39 -0700 Subject: [PATCH 0555/1517] chore: bump dev version (#1131) --- docs/examples/opentelemetry-example-app/setup.cfg | 8 ++++---- .../src/opentelemetry_example_app/version.py | 2 +- exporter/opentelemetry-exporter-datadog/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/datadog/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- .../setup.cfg | 2 +- .../instrumentation/aiohttp_client/version.py | 2 +- .../opentelemetry-instrumentation-aiopg/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/aiopg/version.py | 2 +- .../opentelemetry-instrumentation-asgi/setup.cfg | 4 ++-- .../src/opentelemetry/instrumentation/asgi/version.py | 2 +- .../opentelemetry-instrumentation-asyncpg/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/asyncpg/version.py | 2 +- .../opentelemetry-instrumentation-boto/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/boto/version.py | 2 +- .../opentelemetry-instrumentation-botocore/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/botocore/version.py | 2 +- .../opentelemetry-instrumentation-celery/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/celery/version.py | 2 +- .../opentelemetry-instrumentation-dbapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/dbapi/version.py | 2 +- .../opentelemetry-instrumentation-django/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/django/version.py | 2 +- .../opentelemetry-instrumentation-elasticsearch/setup.cfg | 6 +++--- .../instrumentation/elasticsearch/version.py | 2 +- .../opentelemetry-instrumentation-falcon/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/falcon/version.py | 2 +- .../opentelemetry-instrumentation-fastapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/fastapi/version.py | 2 +- .../opentelemetry-instrumentation-flask/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/flask/version.py | 2 +- .../opentelemetry-instrumentation-grpc/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/grpc/version.py | 2 +- .../opentelemetry-instrumentation-jinja2/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/jinja2/version.py | 2 +- .../opentelemetry-instrumentation-mysql/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/mysql/version.py | 2 +- .../setup.cfg | 4 ++-- .../instrumentation/opentracing_shim/version.py | 2 +- .../opentelemetry-instrumentation-psycopg2/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/psycopg2/version.py | 2 +- .../opentelemetry-instrumentation-pymemcache/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/pymemcache/version.py | 2 +- .../opentelemetry-instrumentation-pymongo/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/pymongo/version.py | 2 +- .../opentelemetry-instrumentation-pymysql/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/pymysql/version.py | 2 +- .../opentelemetry-instrumentation-pyramid/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/pyramid/version.py | 2 +- .../opentelemetry-instrumentation-redis/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/redis/version.py | 2 +- .../opentelemetry-instrumentation-requests/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/requests/version.py | 2 +- .../opentelemetry-instrumentation-sqlalchemy/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/sqlalchemy/version.py | 2 +- .../opentelemetry-instrumentation-sqlite3/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/sqlite3/version.py | 2 +- .../opentelemetry-instrumentation-starlette/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/starlette/version.py | 2 +- .../setup.cfg | 6 +++--- .../instrumentation/system_metrics/version.py | 2 +- .../opentelemetry-instrumentation-tornado/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/tornado/version.py | 2 +- .../opentelemetry-instrumentation-wsgi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/wsgi/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 81 files changed, 159 insertions(+), 159 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index 77c94baf31..952bf59380 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -42,10 +42,10 @@ zip_safe = False include_package_data = True install_requires = typing; python_version<'3.5' - opentelemetry-api == 0.13b0 - opentelemetry-sdk == 0.13b0 - opentelemetry-instrumentation-requests == 0.13b0 - opentelemetry-instrumentation-flask == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-sdk == 0.14.dev0 + opentelemetry-instrumentation-requests == 0.14.dev0 + opentelemetry-instrumentation-flask == 0.14.dev0 flask requests protobuf~=3.11 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py index 2015e87c70..0f99027898 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/exporter/opentelemetry-exporter-datadog/setup.cfg b/exporter/opentelemetry-exporter-datadog/setup.cfg index 2b2dbd1c63..b2bfa3771e 100644 --- a/exporter/opentelemetry-exporter-datadog/setup.cfg +++ b/exporter/opentelemetry-exporter-datadog/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = ddtrace>=0.34.0 - opentelemetry-api == 0.13b0 - opentelemetry-sdk == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-sdk == 0.14.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py index 2015e87c70..0f99027898 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 2994f4a7ff..f55c01eaa3 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.13b0 - opentelemetry-sdk == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-sdk == 0.14.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 1b292ac483..94a3dc9bc3 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 542d6fb897..91041fe46e 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.13b0 - opentelemetry-sdk == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-sdk == 0.14.dev0 protobuf >= 3.8.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 2015e87c70..0f99027898 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index af141f1b6b..12fdd62775 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.13b0 - opentelemetry-sdk == 0.13b0 - opentelemetry-proto == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-sdk == 0.14.dev0 + opentelemetry-proto == 0.14.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 2015e87c70..0f99027898 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index b806c8079d..a7c33f9549 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.13b0 - opentelemetry-sdk == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-sdk == 0.14.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 2015e87c70..0f99027898 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 4e96debbb0..544ed13639 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 0.13b0 - opentelemetry-sdk == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-sdk == 0.14.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 2015e87c70..0f99027898 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index a3e3d27065..196b33087d 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -40,7 +40,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api >= 0.12.dev0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-instrumentation == 0.14.dev0 aiohttp ~= 3.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py index cbee121c0b..404790dad7 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg index d837db3733..5d875b0d5c 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation-dbapi == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation-dbapi == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 aiopg >= 0.13.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg index 3375f900a1..aafcfb7199 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 asgiref ~= 3.0 [options.extras_require] diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg index 7f751c5093..083d676db5 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 asyncpg >= 0.12.0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg index 521a8f83c9..1b814df34f 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg @@ -40,15 +40,15 @@ package_dir= packages=find_namespace: install_requires = boto ~= 2.0 - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 - opentelemetry-instrumentation-botocore == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-instrumentation-botocore == 0.14.dev0 [options.extras_require] test = boto~=2.0 moto~=1.0 - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py +++ b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg index bcda00216d..d09975103d 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg @@ -40,13 +40,13 @@ package_dir= packages=find_namespace: install_requires = botocore ~= 1.0 - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 [options.extras_require] test = moto ~= 1.0 - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg index 2b345093bf..79b63d928a 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 celery ~= 4.0 [options.extras_require] test = pytest - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg index fcf4471831..fc1e3fb82b 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-django/setup.cfg b/instrumentation/opentelemetry-instrumentation-django/setup.cfg index a5933e326d..1fad08d240 100644 --- a/instrumentation/opentelemetry-instrumentation-django/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-django/setup.cfg @@ -40,13 +40,13 @@ package_dir= packages=find_namespace: install_requires = django >= 1.10 - opentelemetry-instrumentation-wsgi == 0.13b0 - opentelemetry-instrumentation == 0.13b0 - opentelemetry-api == 0.13b0 + opentelemetry-instrumentation-wsgi == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.14.dev0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg index 456ff0859f..67ef8b8942 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 wrapt >= 1.0.0, < 2.0.0 elasticsearch >= 2.0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 elasticsearch-dsl >= 2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg b/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg index 9812b5b415..e5e6f69fec 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = falcon ~= 2.0 - opentelemetry-instrumentation-wsgi == 0.13b0 - opentelemetry-instrumentation == 0.13b0 - opentelemetry-api == 0.13b0 + opentelemetry-instrumentation-wsgi == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.14.dev0 [options.extras_require] test = falcon ~= 2.0 - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 parameterized == 0.7.4 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg index 9334235f56..c2b59ffa65 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation-asgi == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation-asgi == 0.14.dev0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 fastapi ~= 0.58.1 requests ~= 2.23.0 # needed for testclient diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg index 7acef44321..cca28d3f4b 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg @@ -40,14 +40,14 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-instrumentation-wsgi == 0.13b0 - opentelemetry-instrumentation == 0.13b0 - opentelemetry-api == 0.13b0 + opentelemetry-instrumentation-wsgi == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.14.dev0 [options.extras_require] test = flask~=1.0 - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg index e511bcf4fb..69e91ef9a1 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-sdk == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-sdk == 0.14.dev0 grpcio ~= 1.27 [options.extras_require] test = - opentelemetry-test == 0.13b0 - opentelemetry-sdk == 0.13b0 + opentelemetry-test == 0.14.dev0 + opentelemetry-sdk == 0.14.dev0 protobuf == 3.12.2 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg index 9bdd6815df..10d8b260b5 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg @@ -38,14 +38,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg index 946cec18c7..111f028f68 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation-dbapi == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation-dbapi == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg index b324742d16..26208c7a90 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg @@ -41,11 +41,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.13b0 + opentelemetry-api == 0.14.dev0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg index fcbc4d2039..69735059cd 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation-dbapi == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation-dbapi == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg index e39a1a5419..31d32bac93 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 pymemcache ~= 1.3 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg index a7a12e0bb4..d477105fde 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 pymongo ~= 3.1 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg index 846f7603c7..6f3870b7e3 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation-dbapi == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation-dbapi == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 PyMySQL ~= 0.9.3 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg index 870888da73..ed9f274c8b 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg @@ -40,15 +40,15 @@ package_dir= packages=find_namespace: install_requires = pyramid >= 1.7 - opentelemetry-instrumentation == 0.13b0 - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation-wsgi == 0.13b0 + opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation-wsgi == 0.14.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = werkzeug == 0.16.1 - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg index dd9585d019..2baa2e2e4a 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 redis >= 2.6 wrapt >= 1.12.1 [options.extras_require] test = - opentelemetry-test == 0.13b0 - opentelemetry-sdk == 0.13b0 + opentelemetry-test == 0.14.dev0 + opentelemetry-sdk == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg index e0a9838a00..9020ae58a1 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 requests ~= 2.0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 httpretty ~= 1.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg index c0e2e7fd40..10b9b0867b 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 wrapt >= 1.11.2 sqlalchemy [options.extras_require] test = - opentelemetry-sdk == 0.13b0 + opentelemetry-sdk == 0.14.dev0 pytest [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg index 312bd2975d..2a72c3bcc8 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation-dbapi == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation-dbapi == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg index 5c25d37831..8e1bd8d688 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation-asgi == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation-asgi == 0.14.dev0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 starlette ~= 0.13.0 requests ~= 2.23.0 # needed for testclient diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg index 2aa55ad668..345393ac82 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-sdk == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-sdk == 0.14.dev0 psutil ~= 5.7.0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg b/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg index 7cb1e3c1fa..be1537568a 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg @@ -39,13 +39,13 @@ package_dir= packages=find_namespace: install_requires = tornado >= 6.0 - opentelemetry-instrumentation == 0.13b0 - opentelemetry-api == 0.13b0 + opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.14.dev0 [options.extras_require] test = tornado >= 6.0 - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg index bce6df7522..2de0e784bf 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg @@ -39,12 +39,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.13b0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.14.dev0 [options.extras_require] test = - opentelemetry-test == 0.13b0 + opentelemetry-test == 0.14.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py index 2015e87c70..0f99027898 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 2015e87c70..0f99027898 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 78443bb176..6042f043d9 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.13b0 + opentelemetry-api == 0.14.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 2015e87c70..0f99027898 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 2015e87c70..0f99027898 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 41077916be..dd7c9aee35 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.13b0 + opentelemetry-api == 0.14.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 2015e87c70..0f99027898 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 9fb14ac879..eef24b9b14 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.13b0" +__version__ = "0.14.dev0" From e8f5eb5ca5fce9d6e70f2bae288529a14d2c1191 Mon Sep 17 00:00:00 2001 From: Amos Law Date: Thu, 17 Sep 2020 12:43:36 -0700 Subject: [PATCH 0556/1517] Add type hints to OTLP exporter (#1121) --- .../opentelemetry-exporter-otlp/CHANGELOG.md | 2 ++ .../opentelemetry/exporter/otlp/exporter.py | 33 ++++++++++++----- .../otlp/metrics_exporter/__init__.py | 36 +++++++++++++------ .../exporter/otlp/trace_exporter/__init__.py | 33 ++++++++++------- 4 files changed, 72 insertions(+), 32 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 94ae999eca..a3686e1a10 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -10,6 +10,8 @@ Released 2020-09-17 ([#1095](https://github.com/open-telemetry/opentelemetry-python/pull/1095)) - Add metric OTLP exporter ([#835](https://github.com/open-telemetry/opentelemetry-python/pull/835)) +- Add type hints to OTLP exporter + ([#1121](https://github.com/open-telemetry/opentelemetry-python/pull/1121)) ## Version 0.12b0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index 7cd9f905e0..079557f831 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -18,6 +18,9 @@ from abc import ABC, abstractmethod from collections.abc import Mapping, Sequence from time import sleep +from typing import Any, Callable, Dict, Generic, List, Optional +from typing import Sequence as TypingSequence +from typing import Text, Tuple, TypeVar from backoff import expo from google.rpc.error_details_pb2 import RetryInfo @@ -31,11 +34,17 @@ from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue from opentelemetry.proto.resource.v1.resource_pb2 import Resource +from opentelemetry.sdk.resources import Resource as SDKResource logger = logging.getLogger(__name__) +SDKDataT = TypeVar("SDKDataT") +ResourceDataT = TypeVar("ResourceDataT") +TypingResourceT = TypeVar("TypingResourceT") +ExportServiceRequestT = TypeVar("ExportServiceRequestT") +ExportResultT = TypeVar("ExportResultT") -def _translate_key_values(key, value): +def _translate_key_values(key: Text, value: Any) -> KeyValue: if isinstance(value, bool): any_value = AnyValue(bool_value=value) @@ -64,8 +73,12 @@ def _translate_key_values(key, value): def _get_resource_data( - sdk_resource_instrumentation_library_data, resource_class, name -): + sdk_resource_instrumentation_library_data: Dict[ + SDKResource, ResourceDataT + ], + resource_class: Callable[..., TypingResourceT], + name: str, +) -> List[TypingResourceT]: resource_data = [] @@ -101,7 +114,9 @@ def _get_resource_data( # pylint: disable=no-member -class OTLPExporterMixin(ABC): +class OTLPExporterMixin( + ABC, Generic[SDKDataT, ExportServiceRequestT, ExportResultT] +): """OTLP span/metric exporter Args: @@ -114,7 +129,7 @@ def __init__( self, endpoint: str = "localhost:55680", credentials: ChannelCredentials = None, - metadata: tuple = None, + metadata: Optional[Tuple[Any]] = None, ): super().__init__() @@ -127,10 +142,12 @@ def __init__( self._client = self._stub(secure_channel(endpoint, credentials)) @abstractmethod - def _translate_data(self, data): + def _translate_data( + self, data: TypingSequence[SDKDataT] + ) -> ExportServiceRequestT: pass - def _export(self, data): + def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: # expo returns a generator that yields delay values which grow # exponentially. Once delay is greater than max_value, the yielded # value will remain constant. @@ -190,5 +207,5 @@ def _export(self, data): return self._result.FAILURE - def shutdown(self): + def shutdown(self) -> None: pass diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py index 944428e37d..033de0d6dd 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -15,13 +15,14 @@ """OTLP Metrics Exporter""" import logging -from typing import Sequence +from typing import List, Sequence, Type, TypeVar, Union # pylint: disable=duplicate-code from opentelemetry.exporter.otlp.exporter import ( OTLPExporterMixin, _get_resource_data, ) +from opentelemetry.metrics import InstrumentT from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, ) @@ -41,9 +42,8 @@ MetricDescriptor, ResourceMetrics, ) -from opentelemetry.sdk.metrics import Counter -from opentelemetry.sdk.metrics import Metric as SDKMetric from opentelemetry.sdk.metrics import ( + Counter, SumObserver, UpDownCounter, UpDownSumObserver, @@ -51,14 +51,18 @@ ValueRecorder, ) from opentelemetry.sdk.metrics.export import ( + MetricRecord, MetricsExporter, MetricsExportResult, ) logger = logging.getLogger(__name__) +DataPointT = TypeVar("DataPointT", Int64DataPoint, DoubleDataPoint) -def _get_data_points(sdk_metric, data_point_class): +def _get_data_points( + sdk_metric: MetricRecord, data_point_class: Type[DataPointT] +) -> List[DataPointT]: data_points = [] @@ -89,7 +93,9 @@ def _get_data_points(sdk_metric, data_point_class): return data_points -def _get_temporality(instrument): +def _get_temporality( + instrument: InstrumentT, +) -> "MetricDescriptor.TemporalityValue": # pylint: disable=no-member if isinstance(instrument, (Counter, UpDownCounter)): temporality = MetricDescriptor.Temporality.DELTA @@ -107,12 +113,12 @@ def _get_temporality(instrument): return temporality -def _get_type(value_type): +def _get_type(value_type: Union[int, float]) -> "MetricDescriptor.TypeValue": # pylint: disable=no-member - if value_type is int: + if value_type is int: # type: ignore[comparison-overlap] type_ = MetricDescriptor.Type.INT64 - elif value_type is float: + elif value_type is float: # type: ignore[comparison-overlap] type_ = MetricDescriptor.Type.DOUBLE # FIXME What are the types that correspond with @@ -126,7 +132,13 @@ def _get_type(value_type): return type_ -class OTLPMetricsExporter(MetricsExporter, OTLPExporterMixin): +class OTLPMetricsExporter( + MetricsExporter, + OTLPExporterMixin[ + MetricRecord, ExportMetricsServiceRequest, MetricsExportResult + ], +): + # pylint: disable=unsubscriptable-object """OTLP metrics exporter Args: @@ -138,7 +150,9 @@ class OTLPMetricsExporter(MetricsExporter, OTLPExporterMixin): _stub = MetricsServiceStub _result = MetricsExportResult - def _translate_data(self, data): + def _translate_data( + self, data: Sequence[MetricRecord] + ) -> ExportMetricsServiceRequest: # pylint: disable=too-many-locals,no-member # pylint: disable=attribute-defined-outside-init @@ -193,6 +207,6 @@ def _translate_data(self, data): ) ) - def export(self, metrics: Sequence[SDKMetric]) -> MetricsExportResult: + def export(self, metrics: Sequence[MetricRecord]) -> MetricsExportResult: # pylint: disable=arguments-differ return self._export(metrics) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index fd1d8e235e..c08d6049e1 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -41,7 +41,11 @@ # pylint: disable=no-member -class OTLPSpanExporter(SpanExporter, OTLPExporterMixin): +class OTLPSpanExporter( + SpanExporter, + OTLPExporterMixin[SDKSpan, ExportTraceServiceRequest, SpanExportResult], +): + # pylint: disable=unsubscriptable-object """OTLP span exporter Args: @@ -53,34 +57,34 @@ class OTLPSpanExporter(SpanExporter, OTLPExporterMixin): _result = SpanExportResult _stub = TraceServiceStub - def _translate_name(self, sdk_span): + def _translate_name(self, sdk_span: SDKSpan) -> None: self._collector_span_kwargs["name"] = sdk_span.name - def _translate_start_time(self, sdk_span): + def _translate_start_time(self, sdk_span: SDKSpan) -> None: self._collector_span_kwargs[ "start_time_unix_nano" ] = sdk_span.start_time - def _translate_end_time(self, sdk_span): + def _translate_end_time(self, sdk_span: SDKSpan) -> None: self._collector_span_kwargs["end_time_unix_nano"] = sdk_span.end_time - def _translate_span_id(self, sdk_span): + def _translate_span_id(self, sdk_span: SDKSpan) -> None: self._collector_span_kwargs[ "span_id" ] = sdk_span.context.span_id.to_bytes(8, "big") - def _translate_trace_id(self, sdk_span): + def _translate_trace_id(self, sdk_span: SDKSpan) -> None: self._collector_span_kwargs[ "trace_id" ] = sdk_span.context.trace_id.to_bytes(16, "big") - def _translate_parent(self, sdk_span): + def _translate_parent(self, sdk_span: SDKSpan) -> None: if sdk_span.parent is not None: self._collector_span_kwargs[ "parent_span_id" ] = sdk_span.parent.span_id.to_bytes(8, "big") - def _translate_context_trace_state(self, sdk_span): + def _translate_context_trace_state(self, sdk_span: SDKSpan) -> None: if sdk_span.context.trace_state is not None: self._collector_span_kwargs["trace_state"] = ",".join( [ @@ -89,7 +93,7 @@ def _translate_context_trace_state(self, sdk_span): ] ) - def _translate_attributes(self, sdk_span): + def _translate_attributes(self, sdk_span: SDKSpan) -> None: if sdk_span.attributes: self._collector_span_kwargs["attributes"] = [] @@ -103,7 +107,7 @@ def _translate_attributes(self, sdk_span): except Exception as error: # pylint: disable=broad-except logger.exception(error) - def _translate_events(self, sdk_span): + def _translate_events(self, sdk_span: SDKSpan) -> None: if sdk_span.events: self._collector_span_kwargs["events"] = [] @@ -127,7 +131,7 @@ def _translate_events(self, sdk_span): collector_span_event ) - def _translate_links(self, sdk_span): + def _translate_links(self, sdk_span: SDKSpan) -> None: if sdk_span.links: self._collector_span_kwargs["links"] = [] @@ -153,14 +157,17 @@ def _translate_links(self, sdk_span): collector_span_link ) - def _translate_status(self, sdk_span): + def _translate_status(self, sdk_span: SDKSpan) -> None: if sdk_span.status is not None: self._collector_span_kwargs["status"] = Status( code=sdk_span.status.canonical_code.value, message=sdk_span.status.description, ) - def _translate_data(self, data) -> ExportTraceServiceRequest: + def _translate_data( + self, data: Sequence[SDKSpan] + ) -> ExportTraceServiceRequest: + # pylint: disable=attribute-defined-outside-init sdk_resource_instrumentation_library_spans = {} From a59e268fab1afead2f8d3bf1c6d9490959562019 Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Fri, 18 Sep 2020 05:20:50 +0200 Subject: [PATCH 0557/1517] Update sampling result names (#1128) --- opentelemetry-sdk/CHANGELOG.md | 3 +++ .../src/opentelemetry/sdk/trace/sampling.py | 14 +++++++------- opentelemetry-sdk/tests/trace/test_sampling.py | 6 ++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 5faa74b3cf..efa139f347 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Update sampling result names + ([#1128](https://github.com/open-telemetry/opentelemetry-python/pull/1128)) + ## Version 0.13b0 Released 2020-09-17 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 2fd14a528e..c40d3e691e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -72,7 +72,7 @@ class Decision(enum.Enum): # IsRecording() == false, span will not be recorded and all events and attributes will be dropped. - IGNORE = 0 + DROP = 0 # IsRecording() == true, but Sampled flag MUST NOT be set. RECORD_ONLY = 1 # IsRecording() == true AND Sampled flag` MUST be set. @@ -140,12 +140,12 @@ def should_sample( attributes: Attributes = None, links: Sequence["Link"] = (), ) -> "SamplingResult": - if self._decision is Decision.IGNORE: + if self._decision is Decision.DROP: return SamplingResult(self._decision) return SamplingResult(self._decision, attributes) def get_description(self) -> str: - if self._decision is Decision.IGNORE: + if self._decision is Decision.DROP: return "AlwaysOffSampler" return "AlwaysOnSampler" @@ -194,10 +194,10 @@ def should_sample( attributes: Attributes = None, # TODO links: Sequence["Link"] = (), ) -> "SamplingResult": - decision = Decision.IGNORE + decision = Decision.DROP if trace_id & self.TRACE_ID_LIMIT < self.bound: decision = Decision.RECORD_AND_SAMPLE - if decision is Decision.IGNORE: + if decision is Decision.DROP: return SamplingResult(decision) return SamplingResult(decision, attributes) @@ -231,7 +231,7 @@ def should_sample( not parent_context.is_valid or not parent_context.trace_flags.sampled ): - return SamplingResult(Decision.IGNORE) + return SamplingResult(Decision.DROP) return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes) return self._delegate.should_sample( @@ -246,7 +246,7 @@ def get_description(self): return "ParentBased{{{}}}".format(self._delegate.get_description()) -ALWAYS_OFF = StaticSampler(Decision.IGNORE) +ALWAYS_OFF = StaticSampler(Decision.DROP) """Sampler that never samples spans, regardless of the parent span's sampling decision.""" ALWAYS_ON = StaticSampler(Decision.RECORD_AND_SAMPLE) diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index 179b31aef5..9160ec4a35 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -31,7 +31,7 @@ def test_is_recording(self): sampling.Decision.is_recording(sampling.Decision.RECORD_AND_SAMPLE) ) self.assertFalse( - sampling.Decision.is_recording(sampling.Decision.IGNORE) + sampling.Decision.is_recording(sampling.Decision.DROP) ) def test_is_sampled(self): @@ -41,9 +41,7 @@ def test_is_sampled(self): self.assertTrue( sampling.Decision.is_sampled(sampling.Decision.RECORD_AND_SAMPLE) ) - self.assertFalse( - sampling.Decision.is_sampled(sampling.Decision.IGNORE) - ) + self.assertFalse(sampling.Decision.is_sampled(sampling.Decision.DROP)) class TestSamplingResult(unittest.TestCase): From 6503dafeebf57775880459aff1a461a60877b9bc Mon Sep 17 00:00:00 2001 From: Eric Mustin Date: Fri, 18 Sep 2020 17:51:49 +0200 Subject: [PATCH 0558/1517] exporter/datadog: add support for resource labels and service.name (#1074) * exporter/datadog: add support for resource labels and service.name --- docs/examples/datadog_exporter/client.py | 9 ++++- .../CHANGELOG.md | 2 + .../exporter/datadog/constants.py | 1 + .../exporter/datadog/exporter.py | 40 +++++++++++++++++-- .../tests/test_datadog_exporter.py | 38 +++++++++++++++--- 5 files changed, 80 insertions(+), 10 deletions(-) diff --git a/docs/examples/datadog_exporter/client.py b/docs/examples/datadog_exporter/client.py index 2570c426d5..a5ddf2843c 100644 --- a/docs/examples/datadog_exporter/client.py +++ b/docs/examples/datadog_exporter/client.py @@ -21,14 +21,19 @@ DatadogExportSpanProcessor, DatadogSpanExporter, ) +from opentelemetry.sdk import resources from opentelemetry.sdk.trace import TracerProvider -trace.set_tracer_provider(TracerProvider()) +service_name = "example-client" + +resource = resources.Resource.create({"service.name": service_name}) + +trace.set_tracer_provider(TracerProvider(resource=resource)) trace.get_tracer_provider().add_span_processor( DatadogExportSpanProcessor( DatadogSpanExporter( - agent_url="http://localhost:8126", service="example-client" + agent_url="http://localhost:8126", service=service_name ) ) ) diff --git a/exporter/opentelemetry-exporter-datadog/CHANGELOG.md b/exporter/opentelemetry-exporter-datadog/CHANGELOG.md index 728c081090..0fab809da9 100644 --- a/exporter/opentelemetry-exporter-datadog/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-datadog/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add support for span resource labels and service name + ## Version 0.12b0 Released 2020-08-14 diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/constants.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/constants.py index 92d736c918..2ae5386e84 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/constants.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/constants.py @@ -6,3 +6,4 @@ SAMPLING_PRIORITY_KEY = "_sampling_priority_v1" ENV_KEY = "env" VERSION_KEY = "version" +SERVICE_NAME_TAG = "service.name" diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py index 37c78187f8..2b36299989 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py @@ -26,7 +26,13 @@ from opentelemetry.trace.status import StatusCanonicalCode # pylint:disable=relative-beyond-top-level -from .constants import DD_ORIGIN, ENV_KEY, SAMPLE_RATE_METRIC_KEY, VERSION_KEY +from .constants import ( + DD_ORIGIN, + ENV_KEY, + SAMPLE_RATE_METRIC_KEY, + SERVICE_NAME_TAG, + VERSION_KEY, +) logger = logging.getLogger(__name__) @@ -107,6 +113,7 @@ def shutdown(self): self.agent_writer.stop() self.agent_writer.join(self.agent_writer.exit_timeout) + # pylint: disable=too-many-locals def _translate_to_datadog(self, spans): datadog_spans = [] @@ -119,10 +126,16 @@ def _translate_to_datadog(self, spans): # duration. tracer = None + # extract resource attributes to be used as tags as well as potential service name + [ + resource_tags, + resource_service_name, + ] = _extract_tags_from_resource(span.resource) + datadog_span = DatadogSpan( tracer, _get_span_name(span), - service=self.service, + service=resource_service_name or self.service, resource=_get_resource(span), span_type=_get_span_type(span), trace_id=trace_id, @@ -140,7 +153,12 @@ def _translate_to_datadog(self, spans): datadog_span.set_tag("error.msg", exc_val) datadog_span.set_tag("error.type", exc_type) - datadog_span.set_tags(span.attributes) + # combine resource attributes and span attributes, don't modify existing span attributes + combined_span_tags = {} + combined_span_tags.update(resource_tags) + combined_span_tags.update(span.attributes) + + datadog_span.set_tags(combined_span_tags) # add configured env tag if self.env is not None: @@ -282,3 +300,19 @@ def _parse_tags_str(tags_str): parsed_tags[key] = value return parsed_tags + + +def _extract_tags_from_resource(resource): + """Parse tags from resource.attributes, except service.name which + has special significance within datadog""" + tags = {} + service_name = None + if not (resource and getattr(resource, "attributes", None)): + return [tags, service_name] + + for attribute_key, attribute_value in resource.attributes.items(): + if attribute_key == SERVICE_NAME_TAG: + service_name = attribute_value + else: + tags[attribute_key] = attribute_value + return [tags, service_name] diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py index 73c8cb3bf8..e1973e5ac6 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py @@ -23,7 +23,7 @@ from opentelemetry import trace as trace_api from opentelemetry.exporter import datadog from opentelemetry.sdk import trace -from opentelemetry.sdk.trace import sampling +from opentelemetry.sdk.trace import Resource, sampling from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -144,6 +144,17 @@ def test_translate_to_datadog(self): # pylint: disable=invalid-name self.maxDiff = None + resource = Resource( + attributes={ + "key_resource": "some_resource", + "service.name": "resource_service_name", + } + ) + + resource_without_service = Resource( + attributes={"conflicting_key": "conflicting_value"} + ) + span_names = ("test1", "test2", "test3") trace_id = 0x6E0C63257DE34C926F9EFCD03927272E trace_id_low = 0x6F9EFCD03927272E @@ -183,18 +194,25 @@ def test_translate_to_datadog(self): parent=parent_context, kind=trace_api.SpanKind.CLIENT, instrumentation_info=instrumentation_info, + resource=Resource({}), ), trace.Span( name=span_names[1], context=parent_context, parent=None, instrumentation_info=instrumentation_info, + resource=resource_without_service, ), trace.Span( - name=span_names[2], context=other_context, parent=None, + name=span_names[2], + context=other_context, + parent=None, + resource=resource, ), ] + otel_spans[1].set_attribute("conflicting_key", "original_value") + otel_spans[0].start(start_time=start_times[0]) otel_spans[0].end(end_time=end_times[0]) @@ -234,7 +252,12 @@ def test_translate_to_datadog(self): duration=durations[1], error=0, service="test-service", - meta={"env": "test", "team": "testers", "version": "0.0.1"}, + meta={ + "env": "test", + "team": "testers", + "version": "0.0.1", + "conflicting_key": "original_value", + }, ), dict( trace_id=trace_id_low, @@ -245,8 +268,13 @@ def test_translate_to_datadog(self): start=start_times[2], duration=durations[2], error=0, - service="test-service", - meta={"env": "test", "team": "testers", "version": "0.0.1"}, + service="resource_service_name", + meta={ + "env": "test", + "team": "testers", + "version": "0.0.1", + "key_resource": "some_resource", + }, ), ] From e275d705467ae9dda130230bb8628eb8116afab1 Mon Sep 17 00:00:00 2001 From: Wilbert Guo Date: Fri, 18 Sep 2020 15:03:52 -0700 Subject: [PATCH 0559/1517] exporter/zipkin: Add status to the tags for the Zipkin Exporter (#1124) * [Issue #1111] Add span status to tags for Zipkin exporter --- .../opentelemetry-exporter-zipkin/CHANGELOG.md | 2 ++ .../src/opentelemetry/exporter/zipkin/__init__.py | 9 +++++++++ .../tests/test_zipkin_exporter.py | 15 +++++++++++++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index 562e5cc124..1d03ea25c5 100644 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -15,6 +15,8 @@ Released 2020-09-17 ([#1097](https://github.com/open-telemetry/opentelemetry-python/pull/1097)) - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) +- Add status mapping to tags + ([#1111](https://github.com/open-telemetry/opentelemetry-python/issues/1111)) ## Version 0.12b0 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 8c3c91a6d3..1e544bcdac 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -183,6 +183,15 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): "otel.instrumentation_library.version" ] = span.instrumentation_info.version + if span.status is not None: + zipkin_span["tags"][ + "otel.status_code" + ] = span.status.canonical_code.value + if span.status.description is not None: + zipkin_span["tags"][ + "otel.status_description" + ] = span.status.description + if context.trace_flags.sampled: zipkin_span["debug"] = True diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 309d966805..49d1c2ffe6 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -24,6 +24,7 @@ from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import TraceFlags +from opentelemetry.trace.status import Status, StatusCanonicalCode class MockResponse: @@ -174,6 +175,9 @@ def test_export(self): otel_spans[0].set_attribute("key_bool", False) otel_spans[0].set_attribute("key_string", "hello_world") otel_spans[0].set_attribute("key_float", 111.22) + otel_spans[0].set_status( + Status(StatusCanonicalCode.UNKNOWN, "Example description") + ) otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) @@ -213,6 +217,8 @@ def test_export(self): "key_bool": "False", "key_string": "hello_world", "key_float": "111.22", + "otel.status_code": 2, + "otel.status_description": "Example description", }, "annotations": [ { @@ -231,7 +237,10 @@ def test_export(self): "duration": durations[1] // 10 ** 3, "localEndpoint": local_endpoint, "kind": None, - "tags": {"key_resource": "some_resource"}, + "tags": { + "key_resource": "some_resource", + "otel.status_code": 0, + }, "annotations": None, }, { @@ -245,6 +254,7 @@ def test_export(self): "tags": { "key_string": "hello_world", "key_resource": "some_resource", + "otel.status_code": 0, }, "annotations": None, }, @@ -259,6 +269,7 @@ def test_export(self): "tags": { "otel.instrumentation_library.name": "name", "otel.instrumentation_library.version": "version", + "otel.status_code": 0, }, "annotations": None, }, @@ -324,7 +335,7 @@ def test_zero_padding(self): "duration": duration // 10 ** 3, "localEndpoint": local_endpoint, "kind": None, - "tags": {}, + "tags": {"otel.status_code": 0}, "annotations": None, "debug": True, "parentId": "0aaaaaaaaaaaaaaa", From 497b322d248791f60ad00e729fe666a1090fdfa6 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 18 Sep 2020 18:33:24 -0400 Subject: [PATCH 0560/1517] Merge OTELResourceDetector result when creating resources (#1096) --- opentelemetry-sdk/CHANGELOG.md | 2 + .../opentelemetry/sdk/resources/__init__.py | 76 ++++++++++++++++++- .../tests/metrics/test_metrics.py | 2 +- .../tests/resources/test_resources.py | 36 ++++++--- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- 5 files changed, 102 insertions(+), 16 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index efa139f347..7fd47150ca 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -27,6 +27,8 @@ Released 2020-09-17 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - Rename members of `trace.sampling.Decision` enum ([#1115](https://github.com/open-telemetry/opentelemetry-python/pull/1115)) +- Merge `OTELResourceDetector` result when creating resources + ([#1096](https://github.com/open-telemetry/opentelemetry-python/pull/1096)) ## Version 0.12b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index e14d781168..086a2fdb5a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -12,6 +12,71 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +This package implements `OpenTelemetry Resources +`_: + + *A Resource is an immutable representation of the entity producing + telemetry. For example, a process producing telemetry that is running in + a container on Kubernetes has a Pod name, it is in a namespace and + possibly is part of a Deployment which also has a name. All three of + these attributes can be included in the Resource.* + +Resource objects are created with `Resource.create`, which accepts attributes +(key-values). Resource attributes can also be passed at process invocation in +the :envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable. You should +register your resource with the `opentelemetry.sdk.metrics.MeterProvider` and +`opentelemetry.sdk.trace.TracerProvider` by passing them into their +constructors. The `Resource` passed to a provider is available to the +exporter, which can send on this information as it sees fit. + +.. code-block:: python + + metrics.set_meter_provider( + MeterProvider( + resource=Resource.create({ + "service.name": "shoppingcart", + "service.instance.id": "instance-12", + }), + ), + ) + print(metrics.get_meter_provider().resource.attributes) + + {'telemetry.sdk.language': 'python', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '0.13.dev0', + 'service.name': 'shoppingcart', + 'service.instance.id': 'instance-12'} + +Note that the OpenTelemetry project documents certain `"standard attributes" +`_ +that have prescribed semantic meanings, for example ``service.name`` in the +above example. + +.. envvar:: OTEL_RESOURCE_ATTRIBUTES + +The :envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable allows resource +attributes to be passed to the SDK at process invocation. The attributes from +:envvar:`OTEL_RESOURCE_ATTRIBUTES` are merged with those passed to +`Resource.create`, meaning :envvar:`OTEL_RESOURCE_ATTRIBUTES` takes *lower* +priority. Attributes should be in the format ``key1=value1,key2=value2``. +Additional details are available `in the specification +`_. + +.. code-block:: console + + $ OTEL_RESOURCE_ATTRIBUTES="service.name=shoppingcard,will_be_overridden=foo" python - < "Resource": + def create(attributes: typing.Optional[Attributes] = None) -> "Resource": if not attributes: - return _DEFAULT_RESOURCE - return _DEFAULT_RESOURCE.merge(Resource(attributes)) + resource = _DEFAULT_RESOURCE + else: + resource = _DEFAULT_RESOURCE.merge(Resource(attributes)) + return resource.merge(OTELResourceDetector().detect()) @staticmethod def create_empty() -> "Resource": @@ -92,7 +160,7 @@ def detect(self) -> "Resource": class OTELResourceDetector(ResourceDetector): # pylint: disable=no-self-use def detect(self) -> "Resource": - env_resources_items = os.environ.get("OTEL_RESOURCE_ATTRIBUTES") + env_resources_items = os.environ.get(OTEL_RESOURCE_ATTRIBUTES) env_resource_map = {} if env_resources_items: env_resource_map = { diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 0197476520..8e412f3c5c 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -40,7 +40,7 @@ def test_resource_empty(self): meter_provider = metrics.MeterProvider() meter = meter_provider.get_meter(__name__) # pylint: disable=protected-access - self.assertIs(meter.resource, resources._DEFAULT_RESOURCE) + self.assertEqual(meter.resource, resources._DEFAULT_RESOURCE) def test_start_pipeline(self): exporter = mock.Mock() diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 3166e3350e..35bffb10b8 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -22,6 +22,12 @@ class TestResources(unittest.TestCase): + def setUp(self) -> None: + os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" + + def tearDown(self) -> None: + os.environ.pop(resources.OTEL_RESOURCE_ATTRIBUTES) + def test_create(self): attributes = { "service": "ui", @@ -44,14 +50,22 @@ def test_create(self): self.assertIsInstance(resource, resources.Resource) self.assertEqual(resource.attributes, expected_attributes) + os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "key=value" + resource = resources.Resource.create(attributes) + self.assertIsInstance(resource, resources.Resource) + expected_with_envar = expected_attributes.copy() + expected_with_envar["key"] = "value" + self.assertEqual(resource.attributes, expected_with_envar) + os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" + resource = resources.Resource.create_empty() - self.assertIs(resource, resources._EMPTY_RESOURCE) + self.assertEqual(resource, resources._EMPTY_RESOURCE) resource = resources.Resource.create(None) - self.assertIs(resource, resources._DEFAULT_RESOURCE) + self.assertEqual(resource, resources._DEFAULT_RESOURCE) resource = resources.Resource.create({}) - self.assertIs(resource, resources._DEFAULT_RESOURCE) + self.assertEqual(resource, resources._DEFAULT_RESOURCE) def test_resource_merge(self): left = resources.Resource({"service": "ui"}) @@ -184,36 +198,38 @@ def test_resource_detector_raise_error(self): class TestOTELResourceDetector(unittest.TestCase): def setUp(self) -> None: - os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "" + os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" def tearDown(self) -> None: - os.environ.pop("OTEL_RESOURCE_ATTRIBUTES") + os.environ.pop(resources.OTEL_RESOURCE_ATTRIBUTES) def test_empty(self): detector = resources.OTELResourceDetector() - os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "" + os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" self.assertEqual(detector.detect(), resources.Resource.create_empty()) def test_one(self): detector = resources.OTELResourceDetector() - os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "k=v" + os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "k=v" self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) def test_one_with_whitespace(self): detector = resources.OTELResourceDetector() - os.environ["OTEL_RESOURCE_ATTRIBUTES"] = " k = v " + os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = " k = v " self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) def test_multiple(self): detector = resources.OTELResourceDetector() - os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "k=v,k2=v2" + os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "k=v,k2=v2" self.assertEqual( detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) ) def test_multiple_with_whitespace(self): detector = resources.OTELResourceDetector() - os.environ["OTEL_RESOURCE_ATTRIBUTES"] = " k = v , k2 = v2 " + os.environ[ + resources.OTEL_RESOURCE_ATTRIBUTES + ] = " k = v , k2 = v2 " self.assertEqual( detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index fabbc62d2e..fdf85ef19b 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -396,7 +396,7 @@ def test_default_span_resource(self): tracer = tracer_provider.get_tracer(__name__) span = tracer.start_span("root") # pylint: disable=protected-access - self.assertIs(span.resource, resources._DEFAULT_RESOURCE) + self.assertEqual(span.resource, resources._DEFAULT_RESOURCE) def test_span_context_remote_flag(self): tracer = new_tracer() From efcfbb8962a6ca8d09131e6309f339756832345d Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 21 Sep 2020 08:38:58 -0700 Subject: [PATCH 0561/1517] instrumentation/wsgi: Use is_recording flag in wsgi instrumentation (#1122) --- .../instrumentation/wsgi/__init__.py | 6 ++++-- .../tests/test_wsgi_middleware.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index 289016fe52..56c8b755c5 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -136,7 +136,8 @@ def add_response_attributes( ): # pylint: disable=unused-argument """Adds HTTP response attributes to span using the arguments passed to a PEP3333-conforming start_response callable.""" - + if not span.is_recording(): + return status_code, status_text = start_response_status.split(" ", 1) span.set_attribute("http.status_text", status_text) @@ -215,7 +216,8 @@ def __call__(self, environ, start_response): iterable, span, self.tracer, token ) except Exception as ex: - span.set_status(Status(StatusCanonicalCode.INTERNAL, str(ex))) + if span.is_recording(): + span.set_status(Status(StatusCanonicalCode.INTERNAL, str(ex))) span.end() context.detach(token) raise diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index b06d915b53..baab50d96b 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -125,6 +125,23 @@ def test_basic_wsgi_call(self): response = app(self.environ, self.start_response) self.validate_response(response) + def test_wsgi_not_recording(self): + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = mock_span + with mock.patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) + # pylint: disable=W0612 + response = app(self.environ, self.start_response) # noqa: F841 + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_wsgi_iterable(self): original_response = Response() iter_wsgi = create_iter_wsgi(original_response) From 7f01f054191fb73c42c2f43e5390fd2d972db1f7 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Mon, 21 Sep 2020 21:26:11 +0530 Subject: [PATCH 0562/1517] Fixed typo in boostrap command to install the correct package (#1138) --- opentelemetry-instrumentation/CHANGELOG.md | 2 ++ .../src/opentelemetry/instrumentation/bootstrap.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/opentelemetry-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md index 5aa47514e0..37f5005103 100644 --- a/opentelemetry-instrumentation/CHANGELOG.md +++ b/opentelemetry-instrumentation/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Fixed boostrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask + ## Version 0.13b0 Released 2020-09-17 diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index d11535d31c..4b6a677a84 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -34,7 +34,7 @@ "django": "opentelemetry-instrumentation-django>=0.8b0", "elasticsearch": "opentelemetry-instrumentation-elasticsearch>=0.11b0", "fastapi": "opentelemetry-instrumentation-fastapi>=0.11b0", - "falcon": "opentelemetry-instrumentation-flask>=0.13b0", + "falcon": "opentelemetry-instrumentation-falcon>=0.13b0", "flask": "opentelemetry-instrumentation-flask>=0.8b0", "grpc": "opentelemetry-instrumentation-grpc>=0.8b0", "jinja2": "opentelemetry-instrumentation-jinja2>=0.8b0", @@ -64,7 +64,7 @@ "django": ("opentelemetry-instrumentation-django",), "elasticsearch": ("opentelemetry-instrumentation-elasticsearch",), "fastapi": ("opentelemetry-instrumentation-fastapi",), - "falcon": ("opentelemetry-instrumentation-flask",), + "falcon": ("opentelemetry-instrumentation-falcon",), "flask": ("opentelemetry-instrumentation-flask",), "grpc": ("opentelemetry-instrumentation-grpc",), "jinja2": ("opentelemetry-instrumentation-jinja2",), From 99e8971871635f230cf7c895c08cda434b4f4e21 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 21 Sep 2020 10:20:49 -0600 Subject: [PATCH 0563/1517] Store ints as ints in the configuration object (#1119) Fixes #1118 Co-authored-by: alrex --- opentelemetry-api/CHANGELOG.md | 3 +++ .../src/opentelemetry/configuration/__init__.py | 9 ++++----- .../tests/configuration/test_configuration.py | 15 ++++++--------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 54916dc992..c56919b832 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Store `int`s as `int`s in the global Configuration object + ([#1118](https://github.com/open-telemetry/opentelemetry-python/pull/1118)) + ## Version 0.13b0 Released 2020-09-17 diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index 0c606b5af4..286bfa7ac6 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -128,11 +128,10 @@ def __new__(cls) -> "Configuration": try: value = int(value_str) except ValueError: - pass - try: - value = float(value_str) - except ValueError: - pass + try: + value = float(value_str) + except ValueError: + pass instance._config_map[key] = value diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index 557591513b..45ed9627d8 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -119,15 +119,12 @@ def test_boolean(self) -> None: }, ) def test_integer(self) -> None: - self.assertEqual( - Configuration().POSITIVE_INTEGER, 123 - ) # pylint: disable=no-member - self.assertEqual( - Configuration().NEGATIVE_INTEGER, -123 - ) # pylint: disable=no-member - self.assertEqual( - Configuration().NON_INTEGER, "-12z3" - ) # pylint: disable=no-member + # pylint: disable=no-member + self.assertIsInstance(Configuration().POSITIVE_INTEGER, int) + self.assertEqual(Configuration().POSITIVE_INTEGER, 123) + self.assertIsInstance(Configuration().NEGATIVE_INTEGER, int) + self.assertEqual(Configuration().NEGATIVE_INTEGER, -123) + self.assertEqual(Configuration().NON_INTEGER, "-12z3") @patch.dict( "os.environ", # type: ignore From 9f5de45d24a82142d1d821a53c35031a260c80b4 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 21 Sep 2020 09:56:59 -0700 Subject: [PATCH 0564/1517] chore: only trigger tests on push to master (#1126) --- .github/workflows/test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 515fdf4331..7df9dff5df 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,10 @@ name: Test -on: [push, pull_request] +on: + push: + branches-ignore: + - 'release/*' + pull_request: jobs: build: From ac358580b47910087b61f8edfe898cc3723ed9e6 Mon Sep 17 00:00:00 2001 From: HiveTraum Date: Tue, 22 Sep 2020 01:30:03 +0500 Subject: [PATCH 0565/1517] Django span names according to convention (#992) --- .../CHANGELOG.md | 2 + .../instrumentation/django/middleware.py | 40 ++++++++++++---- .../tests/test_middleware.py | 46 +++++++++++++++++-- .../tests/views.py | 6 +++ 4 files changed, 82 insertions(+), 12 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index ea9b3f56a7..d3de446730 100644 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Changed span name extraction from request to comply semantic convention ([#992](https://github.com/open-telemetry/opentelemetry-python/pull/992)) + ## Version 0.13b0 Released 2020-09-17 diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index 2c155b1be1..59f7e6e622 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -26,6 +26,14 @@ from opentelemetry.trace import SpanKind, get_tracer from opentelemetry.util import ExcludeList +try: + from django.core.urlresolvers import ( # pylint: disable=no-name-in-module + resolve, + Resolver404, + ) +except ImportError: + from django.urls import resolve, Resolver404 + try: from django.utils.deprecation import MiddlewareMixin except ImportError: @@ -50,17 +58,33 @@ class _DjangoMiddleware(MiddlewareMixin): else: _excluded_urls = ExcludeList(_excluded_urls) - def process_view( - self, request, view_func, view_args, view_kwargs - ): # pylint: disable=unused-argument + @staticmethod + def _get_span_name(request): + try: + if getattr(request, "resolver_match"): + match = request.resolver_match + else: + match = resolve(request.get_full_path()) + + if hasattr(match, "route"): + return match.route + + # Instead of using `view_name`, better to use `_func_name` as some applications can use similar + # view names in different modules + if hasattr(match, "_func_name"): + return match._func_name # pylint: disable=protected-access + + # Fallback for safety as `_func_name` private field + return match.view_name + + except Resolver404: + return "HTTP {}".format(request.method) + + def process_request(self, request): # request.META is a dictionary containing all available HTTP headers # Read more about request.META here: # https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpRequest.META - # environ = { - # key.lower().replace('_', '-').replace("http-", "", 1): value - # for key, value in request.META.items() - # } if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return @@ -73,7 +97,7 @@ def process_view( attributes = collect_request_attributes(environ) span = tracer.start_span( - view_func.__name__, + self._get_span_name(request), kind=SpanKind.SERVER, attributes=attributes, start_time=environ.get( diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 3250ac0c1c..ee82c5d7d9 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -15,6 +15,7 @@ from sys import modules from unittest.mock import patch +from django import VERSION from django.conf import settings from django.conf.urls import url from django.test import Client @@ -28,7 +29,16 @@ from opentelemetry.util import ExcludeList # pylint: disable=import-error -from .views import error, excluded, excluded_noarg, excluded_noarg2, traced +from .views import ( + error, + excluded, + excluded_noarg, + excluded_noarg2, + route_span_name, + traced, +) + +DJANGO_2_2 = VERSION >= (2, 2) urlpatterns = [ url(r"^traced/", traced), @@ -36,6 +46,7 @@ url(r"^excluded_arg/", excluded), url(r"^excluded_noarg/", excluded_noarg), url(r"^excluded_noarg2/", excluded_noarg2), + url(r"^span_name/([0-9]{4})/$", route_span_name), ] _django_instrumentor = DjangoInstrumentor() @@ -65,7 +76,9 @@ def test_traced_get(self): span = spans[0] - self.assertEqual(span.name, "traced") + self.assertEqual( + span.name, "^traced/" if DJANGO_2_2 else "tests.views.traced" + ) self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK) self.assertEqual(span.attributes["http.method"], "GET") @@ -84,7 +97,9 @@ def test_traced_post(self): span = spans[0] - self.assertEqual(span.name, "traced") + self.assertEqual( + span.name, "^traced/" if DJANGO_2_2 else "tests.views.traced" + ) self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK) self.assertEqual(span.attributes["http.method"], "POST") @@ -104,7 +119,9 @@ def test_error(self): span = spans[0] - self.assertEqual(span.name, "error") + self.assertEqual( + span.name, "^error/" if DJANGO_2_2 else "tests.views.error" + ) self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual( span.status.canonical_code, StatusCanonicalCode.UNKNOWN @@ -136,3 +153,24 @@ def test_exclude_lists(self): client.get("/excluded_noarg2/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) + + def test_span_name(self): + Client().get("/span_name/1234/") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + span = span_list[0] + self.assertEqual( + span.name, + "^span_name/([0-9]{4})/$" + if DJANGO_2_2 + else "tests.views.route_span_name", + ) + + def test_span_name_404(self): + Client().get("/span_name/1234567890/") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + span = span_list[0] + self.assertEqual(span.name, "HTTP GET") diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py index b5a2930404..e286841011 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/views.py @@ -19,3 +19,9 @@ def excluded_noarg(request): # pylint: disable=unused-argument def excluded_noarg2(request): # pylint: disable=unused-argument return HttpResponse() + + +def route_span_name( + request, *args, **kwargs +): # pylint: disable=unused-argument + return HttpResponse() From 44627a754ec8996333ad5cb8521adb45f714431c Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 22 Sep 2020 08:45:50 -0700 Subject: [PATCH 0566/1517] instrumentation/pymongo: Cast PyMongo commands as strings (#1132) Replacement PR for #1015 --- .../CHANGELOG.md | 3 +++ .../instrumentation/pymongo/__init__.py | 4 ++-- .../tests/test_pymongo.py | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md index 2a6c0308d5..3ed4ea42ba 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Cast PyMongo commands as strings + ([#1132](https://github.com/open-telemetry/opentelemetry-python/pull/1132)) + ## Version 0.13b0 Released 2020-09-17 diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py index 0d5ddc10ee..4cfff745d1 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py @@ -66,8 +66,8 @@ def started(self, event: monitoring.CommandStartedEvent): name = DATABASE_TYPE + "." + event.command_name statement = event.command_name if command: - name += "." + command - statement += " " + command + name += "." + str(command) + statement += " " + str(command) try: span = self._tracer.start_span(name, kind=SpanKind.CLIENT) diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py b/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py index 57f91e131b..a84841b28b 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py @@ -138,6 +138,23 @@ def test_multiple_commands(self): trace_api.status.StatusCanonicalCode.UNKNOWN, ) + def test_int_command(self): + command_attrs = { + "command_name": 123, + } + mock_event = MockEvent(command_attrs) + + command_tracer = CommandTracer(self.tracer) + command_tracer.started(event=mock_event) + command_tracer.succeeded(event=mock_event) + + spans_list = self.memory_exporter.get_finished_spans() + + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertEqual(span.name, "mongodb.command_name.123") + class MockCommand: def __init__(self, command_attrs): From d8edd509c8643ff1a596619967b8b80189ee88e8 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 22 Sep 2020 10:30:39 -0700 Subject: [PATCH 0567/1517] Use is_recording flag in asgi, pyramid, aiohttp instrumentation (#1142) --- .../aiohttp_client/__init__.py | 65 ++++++++++--------- .../tests/test_aiohttp_client_integration.py | 17 +++++ .../instrumentation/asgi/__init__.py | 33 +++++----- .../tests/test_asgi_middleware.py | 17 +++++ .../instrumentation/pyramid/callbacks.py | 15 +++-- .../tests/test_programmatic.py | 18 ++++- 6 files changed, 113 insertions(+), 52 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py index 397d5dc80e..5c48bbd58a 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py @@ -133,16 +133,19 @@ async def on_request_start( request_span_name = str(trace_config_ctx.span_name) trace_config_ctx.span = trace_config_ctx.tracer.start_span( - request_span_name, - kind=SpanKind.CLIENT, - attributes={ + request_span_name, kind=SpanKind.CLIENT, + ) + + if trace_config_ctx.span.is_recording(): + attributes = { "component": "http", "http.method": http_method, "http.url": trace_config_ctx.url_filter(params.url) if callable(trace_config_ctx.url_filter) else str(params.url), - }, - ) + } + for key, value in attributes.items(): + trace_config_ctx.span.set_attribute(key, value) trace_config_ctx.token = context_api.attach( trace.set_span_in_context(trace_config_ctx.span) @@ -155,15 +158,18 @@ async def on_request_end( trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestEndParams, ): - trace_config_ctx.span.set_status( - Status(http_status_to_canonical_code(int(params.response.status))) - ) - trace_config_ctx.span.set_attribute( - "http.status_code", params.response.status - ) - trace_config_ctx.span.set_attribute( - "http.status_text", params.response.reason - ) + if trace_config_ctx.span.is_recording(): + trace_config_ctx.span.set_status( + Status( + http_status_to_canonical_code(int(params.response.status)) + ) + ) + trace_config_ctx.span.set_attribute( + "http.status_code", params.response.status + ) + trace_config_ctx.span.set_attribute( + "http.status_text", params.response.reason + ) _end_trace(trace_config_ctx) async def on_request_exception( @@ -171,21 +177,22 @@ async def on_request_exception( trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestExceptionParams, ): - if isinstance( - params.exception, - (aiohttp.ServerTimeoutError, aiohttp.TooManyRedirects), - ): - status = StatusCanonicalCode.DEADLINE_EXCEEDED - # Assume any getaddrinfo error is a DNS failure. - elif isinstance( - params.exception, aiohttp.ClientConnectorError - ) and isinstance(params.exception.os_error, socket.gaierror): - # DNS resolution failed - status = StatusCanonicalCode.UNKNOWN - else: - status = StatusCanonicalCode.UNAVAILABLE - - trace_config_ctx.span.set_status(Status(status)) + if trace_config_ctx.span.is_recording(): + if isinstance( + params.exception, + (aiohttp.ServerTimeoutError, aiohttp.TooManyRedirects), + ): + status = StatusCanonicalCode.DEADLINE_EXCEEDED + # Assume any getaddrinfo error is a DNS failure. + elif isinstance( + params.exception, aiohttp.ClientConnectorError + ) and isinstance(params.exception.os_error, socket.gaierror): + # DNS resolution failed + status = StatusCanonicalCode.UNKNOWN + else: + status = StatusCanonicalCode.UNAVAILABLE + + trace_config_ctx.span.set_status(Status(status)) _end_trace(trace_config_ctx) def _trace_config_ctx_factory(**kwargs): diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py index 4a48c38ff7..90af17f9e0 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -17,6 +17,7 @@ import typing import urllib.parse from http import HTTPStatus +from unittest import mock import aiohttp import aiohttp.test_utils @@ -135,6 +136,22 @@ def test_status_codes(self): self.memory_exporter.clear() + def test_not_recording(self): + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + with mock.patch("opentelemetry.trace.get_tracer"): + # pylint: disable=W0612 + host, port = self._http_request( + trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), + url="/test-path?query=param#foobar", + ) + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_span_name_option(self): for span_name, method, path, expected in ( ("static", "POST", "/static-span-name", "static"), diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index 02aabfea95..71211907e0 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -156,15 +156,16 @@ async def __call__(self, scope, receive, send): propagators.extract(get_header_from_scope, scope) ) span_name, additional_attributes = self.span_details_callback(scope) - attributes = collect_request_attributes(scope) - attributes.update(additional_attributes) try: with self.tracer.start_as_current_span( - span_name + " asgi", - kind=trace.SpanKind.SERVER, - attributes=attributes, - ): + span_name + " asgi", kind=trace.SpanKind.SERVER, + ) as span: + if span.is_recording(): + attributes = collect_request_attributes(scope) + attributes.update(additional_attributes) + for key, value in attributes.items(): + span.set_attribute(key, value) @wraps(receive) async def wrapped_receive(): @@ -172,9 +173,10 @@ async def wrapped_receive(): span_name + " asgi." + scope["type"] + ".receive" ) as receive_span: message = await receive() - if message["type"] == "websocket.receive": - set_status_code(receive_span, 200) - receive_span.set_attribute("type", message["type"]) + if receive_span.is_recording(): + if message["type"] == "websocket.receive": + set_status_code(receive_span, 200) + receive_span.set_attribute("type", message["type"]) return message @wraps(send) @@ -182,12 +184,13 @@ async def wrapped_send(message): with self.tracer.start_as_current_span( span_name + " asgi." + scope["type"] + ".send" ) as send_span: - if message["type"] == "http.response.start": - status_code = message["status"] - set_status_code(send_span, status_code) - elif message["type"] == "websocket.send": - set_status_code(send_span, 200) - send_span.set_attribute("type", message["type"]) + if send_span.is_recording(): + if message["type"] == "http.response.start": + status_code = message["status"] + set_status_code(send_span, status_code) + elif message["type"] == "websocket.send": + set_status_code(send_span, 200) + send_span.set_attribute("type", message["type"]) await send(message) await self.app(scope, wrapped_receive, wrapped_send) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py index f994e25966..cf8944ce6d 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py @@ -164,6 +164,23 @@ def test_basic_asgi_call(self): outputs = self.get_all_output() self.validate_outputs(outputs) + def test_wsgi_not_recording(self): + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_as_current_span.return_value = mock_span + mock_tracer.start_as_current_span.return_value.__enter__ = mock_span + mock_tracer.start_as_current_span.return_value.__exit__ = mock_span + with mock.patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_asgi_exc_info(self): """Test that exception information is emitted as expected.""" app = otel_asgi.OpenTelemetryMiddleware(error_asgi) diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py index fe45c39e2a..ada239b8e3 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py @@ -74,21 +74,22 @@ def _before_traversal(event): ) tracer = trace.get_tracer(__name__, __version__) - attributes = otel_wsgi.collect_request_attributes(environ) - if request.matched_route: span_name = request.matched_route.pattern - attributes["http.route"] = request.matched_route.pattern else: span_name = otel_wsgi.get_default_span_name(environ) span = tracer.start_span( - span_name, - kind=trace.SpanKind.SERVER, - attributes=attributes, - start_time=start_time, + span_name, kind=trace.SpanKind.SERVER, start_time=start_time, ) + if span.is_recording(): + attributes = otel_wsgi.collect_request_attributes(environ) + if request.matched_route: + attributes["http.route"] = request.matched_route.pattern + for key, value in attributes.items(): + span.set_attribute(key, value) + activation = tracer.use_span(span, end_on_exit=True) activation.__enter__() environ[_ENVIRON_ACTIVATION_KEY] = activation diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py index add4660caa..38ba71cb55 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest.mock import patch +from unittest.mock import Mock, patch from pyramid.config import Configurator @@ -87,6 +87,22 @@ def test_simple(self): self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) + def test_not_recording(self): + mock_tracer = Mock() + mock_span = Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = mock_span + with patch("opentelemetry.trace.get_tracer"): + self.client.get("/hello/123") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_404(self): expected_attrs = expected_attributes( { From 30f79fd85c40685938b2c8d1a13b7ffe350a239b Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 23 Sep 2020 21:02:19 +0530 Subject: [PATCH 0568/1517] exporter/b3: Fix B3 propagator to not crash work with DefaultSpan (#1148) --- .../sdk/trace/propagation/b3_format.py | 14 ++++++++------ .../tests/trace/propagation/test_b3_format.py | 13 +++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py index f6d3345ed7..c2b12f33f5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py @@ -140,21 +140,23 @@ def inject( ) -> None: span = trace.get_current_span(context=context) - if span.get_context() == trace.INVALID_SPAN_CONTEXT: + span_context = span.get_context() + if span_context == trace.INVALID_SPAN_CONTEXT: return - sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 + sampled = (trace.TraceFlags.SAMPLED & span_context.trace_flags) != 0 set_in_carrier( - carrier, self.TRACE_ID_KEY, format_trace_id(span.context.trace_id), + carrier, self.TRACE_ID_KEY, format_trace_id(span_context.trace_id), ) set_in_carrier( - carrier, self.SPAN_ID_KEY, format_span_id(span.context.span_id) + carrier, self.SPAN_ID_KEY, format_span_id(span_context.span_id) ) - if span.parent is not None: + span_parent = getattr(span, "parent", None) + if span_parent is not None: set_in_carrier( carrier, self.PARENT_SPAN_ID_KEY, - format_span_id(span.parent.span_id), + format_span_id(span_parent.span_id), ) set_in_carrier(carrier, self.SAMPLED_KEY, "1" if sampled else "0") diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index bc508f3fd9..77834adec9 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -307,3 +307,16 @@ def test_inject_empty_context(): new_carrier = {} FORMAT.inject(dict.__setitem__, new_carrier, get_current()) assert len(new_carrier) == 0 + + @staticmethod + def test_default_span(): + """Make sure propagator does not crash when working with DefaultSpan""" + + def getter(carrier, key): + return carrier.get(key, None) + + def setter(carrier, key, value): + carrier[key] = value + + ctx = FORMAT.extract(getter, {}) + FORMAT.inject(setter, {}, ctx) From b8a8016e3e5ff319a5224d2ecfa343943bcbd740 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 23 Sep 2020 21:21:48 +0530 Subject: [PATCH 0569/1517] exporter/zipkin: Fix zipkin exporter translation bug (#1149) Zipkin exporter translation was setting `otel.status_code` attribute as an integer which resulted in Otel collector rejecting the spans. This commit casts the value to a string. --- .../src/opentelemetry/exporter/zipkin/__init__.py | 6 +++--- .../tests/test_zipkin_exporter.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 1e544bcdac..9f42336ed3 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -184,9 +184,9 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): ] = span.instrumentation_info.version if span.status is not None: - zipkin_span["tags"][ - "otel.status_code" - ] = span.status.canonical_code.value + zipkin_span["tags"]["otel.status_code"] = str( + span.status.canonical_code.value + ) if span.status.description is not None: zipkin_span["tags"][ "otel.status_description" diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 49d1c2ffe6..b36817f927 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -217,7 +217,7 @@ def test_export(self): "key_bool": "False", "key_string": "hello_world", "key_float": "111.22", - "otel.status_code": 2, + "otel.status_code": "2", "otel.status_description": "Example description", }, "annotations": [ @@ -239,7 +239,7 @@ def test_export(self): "kind": None, "tags": { "key_resource": "some_resource", - "otel.status_code": 0, + "otel.status_code": "0", }, "annotations": None, }, @@ -254,7 +254,7 @@ def test_export(self): "tags": { "key_string": "hello_world", "key_resource": "some_resource", - "otel.status_code": 0, + "otel.status_code": "0", }, "annotations": None, }, @@ -269,7 +269,7 @@ def test_export(self): "tags": { "otel.instrumentation_library.name": "name", "otel.instrumentation_library.version": "version", - "otel.status_code": 0, + "otel.status_code": "0", }, "annotations": None, }, @@ -335,7 +335,7 @@ def test_zero_padding(self): "duration": duration // 10 ** 3, "localEndpoint": local_endpoint, "kind": None, - "tags": {"otel.status_code": 0}, + "tags": {"otel.status_code": "0"}, "annotations": None, "debug": True, "parentId": "0aaaaaaaaaaaaaaa", From 90d74001984ec69b8d129201d8c69d6ca0a708e0 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 23 Sep 2020 16:54:25 -0600 Subject: [PATCH 0570/1517] Add support for OTEL_PROPAGATORS (#1123) Fixes #1058 --- opentelemetry-api/CHANGELOG.md | 2 + opentelemetry-api/setup.cfg | 3 + .../src/opentelemetry/propagators/__init__.py | 49 +++++++++-- .../tests/configuration/test_configuration.py | 7 +- .../tests/propagators/test_propagators.py | 83 +++++++++++++++++++ opentelemetry-sdk/setup.cfg | 2 + 6 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 opentelemetry-api/tests/propagators/test_propagators.py diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index c56919b832..3d0bcd8005 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add support for `OTEL_PROPAGATORS` + ([#1123](https://github.com/open-telemetry/opentelemetry-python/pull/1123)) - Store `int`s as `int`s in the global Configuration object ([#1118](https://github.com/open-telemetry/opentelemetry-python/pull/1118)) diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 9435d92f19..4ad44c6451 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -55,6 +55,9 @@ opentelemetry_meter_provider = default_meter_provider = opentelemetry.metrics:DefaultMeterProvider opentelemetry_tracer_provider = default_tracer_provider = opentelemetry.trace:DefaultTracerProvider +opentelemetry_propagator = + tracecontext = opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator + baggage = opentelemetry.baggage.propagation:BaggagePropagator [options.extras_require] test = diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index f34e3c588b..c274b19f8a 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -15,6 +15,21 @@ """ API for propagation of context. +The propagators for the +``opentelemetry.propagators.composite.CompositeHTTPPropagator`` can be defined +via configuration in the ``OTEL_PROPAGATORS`` environment variable. This +variable should be set to a comma-separated string of names of values for the +``opentelemetry_propagator`` entry point. For example, setting +``OTEL_PROPAGATORS`` to ``tracecontext,baggage`` (which is the default value) +would instantiate +``opentelemetry.propagators.composite.CompositeHTTPPropagator`` with 2 +propagators, one of type +``opentelemetry.trace.propagation.tracecontext.TraceContextTextMapPropagator`` +and other of type ``opentelemetry.baggage.propagation.BaggagePropagator``. +Notice that these propagator classes are defined as +``opentelemetry_propagator`` entry points in the ``setup.cfg`` file of +``opentelemetry``. + Example:: import flask @@ -54,14 +69,16 @@ def example_route(): """ import typing +from logging import getLogger + +from pkg_resources import iter_entry_points -from opentelemetry.baggage.propagation import BaggagePropagator +from opentelemetry.configuration import Configuration from opentelemetry.context.context import Context from opentelemetry.propagators import composite from opentelemetry.trace.propagation import textmap -from opentelemetry.trace.propagation.tracecontext import ( - TraceContextTextMapPropagator, -) + +logger = getLogger(__name__) def extract( @@ -104,9 +121,25 @@ def inject( get_global_textmap().inject(set_in_carrier, carrier, context) -_HTTP_TEXT_FORMAT = composite.CompositeHTTPPropagator( - [TraceContextTextMapPropagator(), BaggagePropagator()], -) # type: textmap.TextMapPropagator +try: + + propagators = [] + + for propagator in ( # type: ignore + Configuration().get("PROPAGATORS", "tracecontext,baggage").split(",") # type: ignore + ): + + propagators.append( # type: ignore + next( # type: ignore + iter_entry_points("opentelemetry_propagator", propagator) # type: ignore + ).load()() + ) + +except Exception: # pylint: disable=broad-except + logger.exception("Failed to load configured propagators") + raise + +_HTTP_TEXT_FORMAT = composite.CompositeHTTPPropagator(propagators) # type: ignore def get_global_textmap() -> textmap.TextMapPropagator: @@ -115,4 +148,4 @@ def get_global_textmap() -> textmap.TextMapPropagator: def set_global_textmap(http_text_format: textmap.TextMapPropagator,) -> None: global _HTTP_TEXT_FORMAT # pylint:disable=global-statement - _HTTP_TEXT_FORMAT = http_text_format + _HTTP_TEXT_FORMAT = http_text_format # type: ignore diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index 45ed9627d8..608a96c96d 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -20,9 +20,10 @@ class TestConfiguration(TestCase): - def tearDown(self) -> None: - # This call resets the attributes of the Configuration class so that - # each test is executed in the same conditions. + + # These calls reset the attributes of the Configuration class so that each + # test is executed in the same conditions. + def setUp(self) -> None: Configuration._reset() def test_singleton(self) -> None: diff --git a/opentelemetry-api/tests/propagators/test_propagators.py b/opentelemetry-api/tests/propagators/test_propagators.py new file mode 100644 index 0000000000..70f4999dff --- /dev/null +++ b/opentelemetry-api/tests/propagators/test_propagators.py @@ -0,0 +1,83 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from importlib import reload +from os import environ +from unittest import TestCase +from unittest.mock import Mock, patch + +from opentelemetry.baggage.propagation import BaggagePropagator +from opentelemetry.configuration import Configuration +from opentelemetry.trace.propagation.tracecontext import ( + TraceContextTextMapPropagator, +) + + +class TestPropagators(TestCase): + @patch("opentelemetry.propagators.composite.CompositeHTTPPropagator") + def test_default_composite_propagators(self, mock_compositehttppropagator): + def test_propagators(propagators): + + propagators = {propagator.__class__ for propagator in propagators} + + self.assertEqual(len(propagators), 2) + self.assertEqual( + propagators, {TraceContextTextMapPropagator, BaggagePropagator} + ) + + mock_compositehttppropagator.configure_mock( + **{"side_effect": test_propagators} + ) + + import opentelemetry.propagators + + reload(opentelemetry.propagators) + + @patch.dict(environ, {"OTEL_PROPAGATORS": "a,b,c"}) + @patch("opentelemetry.propagators.composite.CompositeHTTPPropagator") + @patch("pkg_resources.iter_entry_points") + def test_non_default_propagators( + self, mock_iter_entry_points, mock_compositehttppropagator + ): + + Configuration._reset() + + def iter_entry_points_mock(_, propagator): + return iter( + [ + Mock( + **{ + "load.side_effect": [ + Mock(**{"side_effect": [propagator]}) + ] + } + ) + ] + ) + + mock_iter_entry_points.configure_mock( + **{"side_effect": iter_entry_points_mock} + ) + + def test_propagators(propagators): + + self.assertEqual(propagators, ["a", "b", "c"]) + + mock_compositehttppropagator.configure_mock( + **{"side_effect": test_propagators} + ) + + import opentelemetry.propagators + + reload(opentelemetry.propagators) diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index dd7c9aee35..92a7853ae5 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -51,6 +51,8 @@ opentelemetry_meter_provider = sdk_meter_provider = opentelemetry.sdk.metrics:MeterProvider opentelemetry_tracer_provider = sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider +opentelemetry_propagator = + b3 = opentelemetry.sdk.trace.propagation.b3_format.B3Format [options.extras_require] test = From 4af1341c7966a95bcb53d475f3ae48b831dbbd78 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 23 Sep 2020 22:31:17 -0600 Subject: [PATCH 0571/1517] Add support for OTEL_BSP_* environment variables (#1120) Fixes #1105 --- opentelemetry-sdk/CHANGELOG.md | 2 ++ .../sdk/trace/export/__init__.py | 35 ++++++++++++++++--- .../tests/trace/export/test_export.py | 22 +++++++++++- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 7fd47150ca..c9b4c3538d 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -4,6 +4,8 @@ - Update sampling result names ([#1128](https://github.com/open-telemetry/opentelemetry-python/pull/1128)) +- Add support for `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_SCHEDULE_DELAY_MILLIS`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` and `OTEL_BSP_EXPORT_TIMEOUT_MILLIS` environment variables + ([#1105](https://github.com/open-telemetry/opentelemetry-python/pull/1120)) ## Version 0.13b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 857537b90a..59231e60f3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -20,8 +20,8 @@ import typing from enum import Enum +from opentelemetry.configuration import Configuration from opentelemetry.context import attach, detach, set_value -from opentelemetry.sdk.trace import sampling from opentelemetry.util import time_ns from .. import Span, SpanProcessor @@ -113,10 +113,30 @@ class BatchExportSpanProcessor(SpanProcessor): def __init__( self, span_exporter: SpanExporter, - max_queue_size: int = 2048, - schedule_delay_millis: float = 5000, - max_export_batch_size: int = 512, + max_queue_size: int = None, + schedule_delay_millis: float = None, + max_export_batch_size: int = None, + export_timeout_millis: float = None, ): + + if max_queue_size is None: + max_queue_size = Configuration().get("BSP_MAX_QUEUE_SIZE", 2048) + + if schedule_delay_millis is None: + schedule_delay_millis = Configuration().get( + "BSP_SCHEDULE_DELAY_MILLIS", 5000 + ) + + if max_export_batch_size is None: + max_export_batch_size = Configuration().get( + "BSP_MAX_EXPORT_BATCH_SIZE", 512 + ) + + if export_timeout_millis is None: + export_timeout_millis = Configuration().get( + "BSP_EXPORT_TIMEOUT_MILLIS", 30000 + ) + if max_queue_size <= 0: raise ValueError("max_queue_size must be a positive integer.") @@ -143,6 +163,7 @@ def __init__( self.schedule_delay_millis = schedule_delay_millis self.max_export_batch_size = max_export_batch_size self.max_queue_size = max_queue_size + self.export_timeout_millis = export_timeout_millis self.done = False # flag that indicates that spans are being dropped self._spans_dropped = False @@ -306,7 +327,11 @@ def _drain_queue(self): while self.queue: self._export_batch() - def force_flush(self, timeout_millis: int = 30000) -> bool: + def force_flush(self, timeout_millis: int = None) -> bool: + + if timeout_millis is None: + timeout_millis = self.export_timeout_millis + if self.done: logger.warning("Already shutdown, ignoring call to force_flush().") return True diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index e6fcdb9c22..8c43f731b2 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -136,6 +136,26 @@ def _create_start_and_end_span(name, span_processor): class TestBatchExportSpanProcessor(unittest.TestCase): + @mock.patch.dict( + "os.environ", + { + "OTEL_BSP_MAX_QUEUE_SIZE": "10", + "OTEL_BSP_SCHEDULE_DELAY_MILLIS": "2", + "OTEL_BSP_MAX_EXPORT_BATCH_SIZE": "3", + "OTEL_BSP_EXPORT_TIMEOUT_MILLIS": "4", + }, + ) + def test_batch_span_processor_environment_variables(self): + + batch_span_processor = export.BatchExportSpanProcessor( + MySpanExporter(destination=[]) + ) + + self.assertEqual(batch_span_processor.max_queue_size, 10) + self.assertEqual(batch_span_processor.schedule_delay_millis, 2) + self.assertEqual(batch_span_processor.max_export_batch_size, 3) + self.assertEqual(batch_span_processor.export_timeout_millis, 4) + def test_shutdown(self): spans_names_list = [] @@ -266,7 +286,7 @@ def test_batch_span_processor_many_spans(self): for _ in range(256): _create_start_and_end_span("foo", span_processor) - time.sleep(0.05) # give some time for the exporter to upload spans + time.sleep(0.1) # give some time for the exporter to upload spans self.assertTrue(span_processor.force_flush()) self.assertEqual(len(spans_names_list), 1024) From 76365475ae3094b02dba8a9bbb460d8bc5a19651 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 24 Sep 2020 00:56:04 -0400 Subject: [PATCH 0572/1517] ReadTheDocs and pkg_resources fix (#1145) --- docs-requirements.txt | 5 +++++ docs/conf.py | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index e98a0d35df..a316886277 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -2,6 +2,11 @@ sphinx~=2.4 sphinx-rtd-theme~=0.4 sphinx-autodoc-typehints~=1.10.2 +# Need to install the api/sdk in the venv for autodoc. Modifying sys.path +# doesn't work for pkg_resources. +./opentelemetry-api +./opentelemetry-sdk + # Required by ext packages asgiref~=3.0 asyncpg>=0.12.0 diff --git a/docs/conf.py b/docs/conf.py index d15d8b2ed5..68b871aaac 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,8 +25,6 @@ source_dirs = [ - os.path.abspath("../opentelemetry-api/src/"), - os.path.abspath("../opentelemetry-sdk/src/"), os.path.abspath("../opentelemetry-instrumentation/src/"), ] From 2b46d11b6f248e4ae98bae0a310824945d91053a Mon Sep 17 00:00:00 2001 From: Christoph Brand Date: Thu, 24 Sep 2020 15:47:29 +0200 Subject: [PATCH 0573/1517] fix(ext/prometheus): support minmaxsumcount aggregator (#945) --- .../exporter/prometheus/__init__.py | 50 +++++++++++-------- .../tests/test_prometheus_exporter.py | 32 ++++++++++-- 2 files changed, 58 insertions(+), 24 deletions(-) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index e03c23a99e..3ab1c536f9 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -67,22 +67,22 @@ import collections import logging import re -from typing import Sequence +from typing import Iterable, Optional, Sequence, Union -from prometheus_client import start_http_server from prometheus_client.core import ( REGISTRY, - CollectorRegistry, CounterMetricFamily, + SummaryMetricFamily, UnknownMetricFamily, ) -from opentelemetry.metrics import Counter, Metric, ValueRecorder +from opentelemetry.metrics import Counter, ValueRecorder from opentelemetry.sdk.metrics.export import ( MetricRecord, MetricsExporter, MetricsExportResult, ) +from opentelemetry.sdk.metrics.export.aggregate import MinMaxSumCountAggregator logger = logging.getLogger(__name__) @@ -110,8 +110,8 @@ def shutdown(self) -> None: class CustomCollector: - """ CustomCollector represents the Prometheus Collector object - https://github.com/prometheus/client_python#custom-collectors + """CustomCollector represents the Prometheus Collector object + https://github.com/prometheus/client_python#custom-collectors """ def __init__(self, prefix: str = ""): @@ -121,7 +121,7 @@ def __init__(self, prefix: str = ""): r"[^\w]", re.UNICODE | re.IGNORECASE ) - def add_metrics_data(self, metric_records: Sequence[MetricRecord]): + def add_metrics_data(self, metric_records: Sequence[MetricRecord]) -> None: self._metrics_to_export.append(metric_records) def collect(self): @@ -152,25 +152,35 @@ def _translate_to_prometheus(self, metric_record: MetricRecord): metric_name = self._prefix + "_" metric_name += self._sanitize(metric_record.instrument.name) + description = getattr(metric_record.instrument, "description", "") if isinstance(metric_record.instrument, Counter): prometheus_metric = CounterMetricFamily( - name=metric_name, - documentation=metric_record.instrument.description, - labels=label_keys, + name=metric_name, documentation=description, labels=label_keys ) prometheus_metric.add_metric( labels=label_values, value=metric_record.aggregator.checkpoint ) # TODO: Add support for histograms when supported in OT elif isinstance(metric_record.instrument, ValueRecorder): - prometheus_metric = UnknownMetricFamily( - name=metric_name, - documentation=metric_record.instrument.description, - labels=label_keys, - ) - prometheus_metric.add_metric( - labels=label_values, value=metric_record.aggregator.checkpoint - ) + value = metric_record.aggregator.checkpoint + if isinstance(metric_record.aggregator, MinMaxSumCountAggregator): + prometheus_metric = SummaryMetricFamily( + name=metric_name, + documentation=description, + labels=label_keys, + ) + prometheus_metric.add_metric( + labels=label_values, + count_value=value.count, + sum_value=value.sum, + ) + else: + prometheus_metric = UnknownMetricFamily( + name=metric_name, + documentation=description, + labels=label_keys, + ) + prometheus_metric.add_metric(labels=label_values, value=value) else: logger.warning( @@ -178,8 +188,8 @@ def _translate_to_prometheus(self, metric_record: MetricRecord): ) return prometheus_metric - def _sanitize(self, key): - """ sanitize the given metric name or label according to Prometheus rule. + def _sanitize(self, key: str) -> str: + """sanitize the given metric name or label according to Prometheus rule. Replace all characters other than [A-Za-z0-9_] with '_'. """ return self._non_letters_nor_digits_re.sub("_", key) diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 56ab29b70d..936a6eb822 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -15,6 +15,7 @@ import unittest from unittest import mock +from prometheus_client import generate_latest from prometheus_client.core import CounterMetricFamily from opentelemetry.exporter.prometheus import ( @@ -24,7 +25,11 @@ from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk import metrics from opentelemetry.sdk.metrics.export import MetricRecord, MetricsExportResult -from opentelemetry.sdk.metrics.export.aggregate import SumAggregator +from opentelemetry.sdk.metrics.export.aggregate import ( + MinMaxSumCountAggregator, + SumAggregator, +) +from opentelemetry.sdk.util import get_dict_as_key class TestPrometheusMetricExporter(unittest.TestCase): @@ -35,7 +40,7 @@ def setUp(self): "testname", "testdesc", "unit", int, metrics.Counter, ) labels = {"environment": "staging"} - self._labels_key = metrics.get_dict_as_key(labels) + self._labels_key = get_dict_as_key(labels) self._mock_registry_register = mock.Mock() self._registry_register_patch = mock.patch( @@ -70,13 +75,32 @@ def test_export(self): self.assertEqual(len(exporter._collector._metrics_to_export), 1) self.assertIs(result, MetricsExportResult.SUCCESS) + def test_min_max_sum_aggregator_to_prometheus(self): + meter = get_meter_provider().get_meter(__name__) + metric = meter.create_metric( + "test@name", "testdesc", "unit", int, metrics.ValueRecorder, [] + ) + labels = {} + key_labels = get_dict_as_key(labels) + aggregator = MinMaxSumCountAggregator() + aggregator.update(123) + aggregator.update(456) + aggregator.take_checkpoint() + record = MetricRecord(metric, key_labels, aggregator) + collector = CustomCollector("testprefix") + collector.add_metrics_data([record]) + result_bytes = generate_latest(collector) + result = result_bytes.decode("utf-8") + self.assertIn("testprefix_test_name_count 2.0", result) + self.assertIn("testprefix_test_name_sum 579.0", result) + def test_counter_to_prometheus(self): meter = get_meter_provider().get_meter(__name__) metric = meter.create_metric( "test@name", "testdesc", "unit", int, metrics.Counter, ) labels = {"environment@": "staging", "os": "Windows"} - key_labels = metrics.get_dict_as_key(labels) + key_labels = get_dict_as_key(labels) aggregator = SumAggregator() aggregator.update(123) aggregator.take_checkpoint() @@ -107,7 +131,7 @@ def test_invalid_metric(self): "tesname", "testdesc", "unit", int, StubMetric ) labels = {"environment": "staging"} - key_labels = metrics.get_dict_as_key(labels) + key_labels = get_dict_as_key(labels) record = MetricRecord(metric, key_labels, None) collector = CustomCollector("testprefix") collector.add_metrics_data([record]) From c1ec4445719e3872366f172443de53fd12640d07 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Fri, 25 Sep 2020 04:07:14 +0530 Subject: [PATCH 0574/1517] Make zipkin tag value length configurable (#1151) Zipkin exporter truncates tag values to a maximum length of 128 characters. This commit makes this value configurable while keeping 128 as the default value. --- .../CHANGELOG.md | 3 + .../opentelemetry/exporter/zipkin/__init__.py | 82 +++++++++++-------- .../tests/test_zipkin_exporter.py | 47 +++++++++++ 3 files changed, 96 insertions(+), 36 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index 1d03ea25c5..a980e34568 100644 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Zipkin exporter now accepts a ``max_tag_value_length`` attribute to customize the + maximum allowed size a tag value can have. ([#1151](https://github.com/open-telemetry/opentelemetry-python/pull/1151)) + ## Version 0.13b0 Released 2020-09-17 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 9f42336ed3..5e544275b3 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -74,6 +74,7 @@ DEFAULT_RETRY = False DEFAULT_URL = "http://localhost:9411/api/v2/spans" +DEFAULT_MAX_TAG_VALUE_LENGTH = 128 ZIPKIN_HEADERS = {"Content-Type": "application/json"} SPAN_KIND_MAP = { @@ -108,6 +109,7 @@ def __init__( ipv4: Optional[str] = None, ipv6: Optional[str] = None, retry: Optional[str] = DEFAULT_RETRY, + max_tag_value_length: Optional[int] = DEFAULT_MAX_TAG_VALUE_LENGTH, ): self.service_name = service_name if url is None: @@ -122,6 +124,7 @@ def __init__( self.ipv4 = ipv4 self.ipv6 = ipv6 self.retry = retry + self.max_tag_value_length = max_tag_value_length def export(self, spans: Sequence[Span]) -> SpanExportResult: zipkin_spans = self._translate_to_zipkin(spans) @@ -141,6 +144,9 @@ def export(self, spans: Sequence[Span]) -> SpanExportResult: return SpanExportResult.FAILURE return SpanExportResult.SUCCESS + def shutdown(self) -> None: + pass + def _translate_to_zipkin(self, spans: Sequence[Span]): local_endpoint = {"serviceName": self.service_name, "port": self.port} @@ -171,8 +177,10 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): "duration": duration_mus, "localEndpoint": local_endpoint, "kind": SPAN_KIND_MAP[span.kind], - "tags": _extract_tags_from_span(span), - "annotations": _extract_annotations_from_events(span.events), + "tags": self._extract_tags_from_span(span), + "annotations": self._extract_annotations_from_events( + span.events + ), } if span.instrumentation_info is not None: @@ -205,42 +213,44 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): zipkin_spans.append(zipkin_span) return zipkin_spans - def shutdown(self) -> None: - pass - + def _extract_tags_from_dict(self, tags_dict): + tags = {} + if not tags_dict: + return tags + for attribute_key, attribute_value in tags_dict.items(): + if isinstance(attribute_value, (int, bool, float)): + value = str(attribute_value) + elif isinstance(attribute_value, str): + value = attribute_value + else: + logger.warning("Could not serialize tag %s", attribute_key) + continue + + if self.max_tag_value_length > 0: + value = value[: self.max_tag_value_length] + tags[attribute_key] = value + return tags -def _extract_tags_from_dict(tags_dict): - tags = {} - if not tags_dict: + def _extract_tags_from_span(self, span: Span): + tags = self._extract_tags_from_dict(getattr(span, "attributes", None)) + if span.resource: + tags.update(self._extract_tags_from_dict(span.resource.attributes)) return tags - for attribute_key, attribute_value in tags_dict.items(): - if isinstance(attribute_value, (int, bool, float)): - value = str(attribute_value) - elif isinstance(attribute_value, str): - value = attribute_value[:128] - else: - logger.warning("Could not serialize tag %s", attribute_key) - continue - tags[attribute_key] = value - return tags - - -def _extract_tags_from_span(span: Span): - tags = _extract_tags_from_dict(getattr(span, "attributes", None)) - if span.resource: - tags.update(_extract_tags_from_dict(span.resource.attributes)) - return tags - - -def _extract_annotations_from_events(events): - return ( - [ - {"timestamp": _nsec_to_usec_round(e.timestamp), "value": e.name} - for e in events - ] - if events - else None - ) + + def _extract_annotations_from_events( + self, events + ): # pylint: disable=R0201 + return ( + [ + { + "timestamp": _nsec_to_usec_round(e.timestamp), + "value": e.name, + } + for e in events + ] + if events + else None + ) def _nsec_to_usec_round(nsec): diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index b36817f927..635594868f 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -361,3 +361,50 @@ def test_invalid_response(self, mock_post): exporter = ZipkinSpanExporter("test-service") status = exporter.export(spans) self.assertEqual(SpanExportResult.FAILURE, status) + + def test_max_tag_length(self): + service_name = "test-service" + + span_context = trace_api.SpanContext( + 0x0E0C63257DE34C926F9EFCD03927272E, + 0x04BF92DEEFC58C92, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + + span = trace.Span(name="test-span", context=span_context,) + + span.start() + span.resource = Resource({}) + # added here to preserve order + span.set_attribute("k1", "v" * 500) + span.set_attribute("k2", "v" * 50) + span.set_status( + Status(StatusCanonicalCode.UNKNOWN, "Example description") + ) + span.end() + + exporter = ZipkinSpanExporter(service_name) + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export([span]) + self.assertEqual(SpanExportResult.SUCCESS, status) + + _, kwargs = mock_post.call_args # pylint: disable=E0633 + + tags = json.loads(kwargs["data"])[0]["tags"] + self.assertEqual(len(tags["k1"]), 128) + self.assertEqual(len(tags["k2"]), 50) + + exporter = ZipkinSpanExporter(service_name, max_tag_value_length=2) + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export([span]) + self.assertEqual(SpanExportResult.SUCCESS, status) + + _, kwargs = mock_post.call_args # pylint: disable=E0633 + tags = json.loads(kwargs["data"])[0]["tags"] + self.assertEqual(len(tags["k1"]), 2) + self.assertEqual(len(tags["k2"]), 2) From 872975b6be5b8022ee7501221bdb6261624f035e Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 25 Sep 2020 07:31:35 -0700 Subject: [PATCH 0575/1517] Adding metric collection as part of instrumentations - Requests (#1116) --- docs/examples/basic_meter/http.py | 42 +++++++ docs/instrumentation/instrumentation.rst | 1 + docs/instrumentation/metric.rst | 7 ++ .../CHANGELOG.md | 2 + .../instrumentation/requests/__init__.py | 103 ++++++++++++------ .../tests/test_requests_integration.py | 57 ++++++++++ opentelemetry-instrumentation/CHANGELOG.md | 2 + opentelemetry-instrumentation/setup.cfg | 1 + .../opentelemetry/instrumentation/metric.py | 85 +++++++++++++++ .../tests/test_metric.py | 87 +++++++++++++++ tox.ini | 2 +- 11 files changed, 353 insertions(+), 36 deletions(-) create mode 100644 docs/examples/basic_meter/http.py create mode 100644 docs/instrumentation/metric.rst create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py create mode 100644 opentelemetry-instrumentation/tests/test_metric.py diff --git a/docs/examples/basic_meter/http.py b/docs/examples/basic_meter/http.py new file mode 100644 index 0000000000..8fd6c6294c --- /dev/null +++ b/docs/examples/basic_meter/http.py @@ -0,0 +1,42 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This module shows how you can enable collection and exporting of http metrics +related to instrumentations. +""" +import requests + +from opentelemetry import metrics +from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter + +# Sets the global MeterProvider instance +metrics.set_meter_provider(MeterProvider()) + +# Exporter to export metrics to the console +exporter = ConsoleMetricsExporter() + +# Instrument the requests library +RequestsInstrumentor().instrument() + +# Indicate to start collecting and exporting requests related metrics +metrics.get_meter_provider().start_pipeline( + RequestsInstrumentor().meter, exporter, 5 +) + +response = requests.get("http://example.com") + +input("...\n") diff --git a/docs/instrumentation/instrumentation.rst b/docs/instrumentation/instrumentation.rst index 9c01b6b6f4..16b82a2093 100644 --- a/docs/instrumentation/instrumentation.rst +++ b/docs/instrumentation/instrumentation.rst @@ -13,3 +13,4 @@ Submodules :maxdepth: 1 instrumentor + metric diff --git a/docs/instrumentation/metric.rst b/docs/instrumentation/metric.rst new file mode 100644 index 0000000000..6a69eeeca5 --- /dev/null +++ b/docs/instrumentation/metric.rst @@ -0,0 +1,7 @@ +opentelemetry.instrumentation.metric package +============================================ + +.. automodule:: opentelemetry.instrumentation.metric + :members: + :undoc-members: + :show-inheritance: diff --git a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md index 5b876bd4ad..c2e4dcdbb9 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md @@ -10,6 +10,8 @@ Released 2020-09-17 ([#1040](https://github.com/open-telemetry/opentelemetry-python/pull/1040)) - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) +- Add support for http metrics + ([#1116](https://github.com/open-telemetry/opentelemetry-python/pull/1116)) ## Version 0.12b0 diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index fef67c5d0d..d0336184ed 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -25,7 +25,7 @@ import opentelemetry.instrumentation.requests # You can optionally pass a custom TracerProvider to - RequestInstrumentor.instrument() + # RequestInstrumentor.instrument() opentelemetry.instrumentation.requests.RequestsInstrumentor().instrument() response = requests.get(url="https://www.example.org/") @@ -43,6 +43,10 @@ from opentelemetry import context, propagators from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.metric import ( + HTTPMetricRecorder, + MetricMixin, +) from opentelemetry.instrumentation.requests.version import __version__ from opentelemetry.instrumentation.utils import http_status_to_canonical_code from opentelemetry.trace import SpanKind, get_tracer @@ -54,6 +58,7 @@ # pylint: disable=unused-argument +# pylint: disable=R0915 def _instrument(tracer_provider=None, span_callback=None): """Enables tracing of all requests calls that go through :code:`requests.session.Session.request` (this includes @@ -118,43 +123,66 @@ def _instrumented_requests_call( exception = None + recorder = RequestsInstrumentor().metric_recorder + + labels = {} + labels["http.method"] = method + labels["http.url"] = url + with get_tracer( __name__, __version__, tracer_provider ).start_as_current_span(span_name, kind=SpanKind.CLIENT) as span: - if span.is_recording(): - span.set_attribute("component", "http") - span.set_attribute("http.method", method.upper()) - span.set_attribute("http.url", url) - - headers = get_or_create_headers() - propagators.inject(type(headers).__setitem__, headers) - - token = context.attach( - context.set_value(_SUPPRESS_REQUESTS_INSTRUMENTATION_KEY, True) - ) - try: - result = call_wrapped() # *** PROCEED - except Exception as exc: # pylint: disable=W0703 - exception = exc - result = getattr(exc, "response", None) - finally: - context.detach(token) - - if exception is not None and span.is_recording(): - span.set_status( - Status(_exception_to_canonical_code(exception)) + with recorder.record_duration(labels): + if span.is_recording(): + span.set_attribute("component", "http") + span.set_attribute("http.method", method) + span.set_attribute("http.url", url) + + headers = get_or_create_headers() + propagators.inject(type(headers).__setitem__, headers) + + token = context.attach( + context.set_value( + _SUPPRESS_REQUESTS_INSTRUMENTATION_KEY, True + ) ) - span.record_exception(exception) - - if result is not None and span.is_recording(): - span.set_attribute("http.status_code", result.status_code) - span.set_attribute("http.status_text", result.reason) - span.set_status( - Status(http_status_to_canonical_code(result.status_code)) - ) - - if span_callback is not None: - span_callback(span, result) + try: + result = call_wrapped() # *** PROCEED + except Exception as exc: # pylint: disable=W0703 + exception = exc + result = getattr(exc, "response", None) + finally: + context.detach(token) + + if exception is not None and span.is_recording(): + span.set_status( + Status(_exception_to_canonical_code(exception)) + ) + span.record_exception(exception) + + if result is not None: + if span.is_recording(): + span.set_attribute( + "http.status_code", result.status_code + ) + span.set_attribute("http.status_text", result.reason) + span.set_status( + Status( + http_status_to_canonical_code( + result.status_code + ) + ) + ) + labels["http.status_code"] = str(result.status_code) + labels["http.status_text"] = result.reason + if result.raw and result.raw.version: + labels["http.flavor"] = ( + str(result.raw.version)[:1] + + "." + + str(result.raw.version)[:-1] + ) + if span_callback is not None: + span_callback(span, result) if exception is not None: raise exception.with_traceback(exception.__traceback__) @@ -202,7 +230,7 @@ def _exception_to_canonical_code(exc: Exception) -> StatusCanonicalCode: return StatusCanonicalCode.UNKNOWN -class RequestsInstrumentor(BaseInstrumentor): +class RequestsInstrumentor(BaseInstrumentor, MetricMixin): """An instrumentor for requests See `BaseInstrumentor` """ @@ -219,6 +247,11 @@ def _instrument(self, **kwargs): tracer_provider=kwargs.get("tracer_provider"), span_callback=kwargs.get("span_callback"), ) + self.init_metrics( + __name__, __version__, + ) + # pylint: disable=W0201 + self.metric_recorder = HTTPMetricRecorder(self.meter, SpanKind.CLIENT) def _uninstrument(self, **kwargs): _uninstrument() diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py index c3457b7392..2d3636284b 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py @@ -22,6 +22,7 @@ from opentelemetry import context, propagators, trace from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.sdk import resources +from opentelemetry.sdk.util import get_dict_as_key from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase from opentelemetry.trace.status import StatusCanonicalCode @@ -88,6 +89,27 @@ def test_basic(self): span, opentelemetry.instrumentation.requests ) + self.assertIsNotNone(RequestsInstrumentor().meter) + self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1) + recorder = RequestsInstrumentor().meter.metrics.pop() + match_key = get_dict_as_key( + { + "http.flavor": "1.1", + "http.method": "GET", + "http.status_code": "200", + "http.status_text": "OK", + "http.url": "http://httpbin.org/status/200", + } + ) + for key in recorder.bound_instruments.keys(): + self.assertEqual(key, match_key) + # pylint: disable=protected-access + bound = recorder.bound_instruments.get(key) + for view_data in bound.view_datas: + self.assertEqual(view_data.labels, key) + self.assertEqual(view_data.aggregator.current.count, 1) + self.assertGreater(view_data.aggregator.current.sum, 0) + def test_not_foundbasic(self): url_404 = "http://httpbin.org/status/404" httpretty.register_uri( @@ -246,6 +268,23 @@ def test_requests_exception_without_response(self, *_, **__): span.status.canonical_code, StatusCanonicalCode.UNKNOWN ) + self.assertIsNotNone(RequestsInstrumentor().meter) + self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1) + recorder = RequestsInstrumentor().meter.metrics.pop() + match_key = get_dict_as_key( + { + "http.method": "GET", + "http.url": "http://httpbin.org/status/200", + } + ) + for key in recorder.bound_instruments.keys(): + self.assertEqual(key, match_key) + # pylint: disable=protected-access + bound = recorder.bound_instruments.get(key) + for view_data in bound.view_datas: + self.assertEqual(view_data.labels, key) + self.assertEqual(view_data.aggregator.current.count, 1) + mocked_response = requests.Response() mocked_response.status_code = 500 mocked_response.reason = "Internal Server Error" @@ -272,6 +311,24 @@ def test_requests_exception_with_response(self, *_, **__): self.assertEqual( span.status.canonical_code, StatusCanonicalCode.INTERNAL ) + self.assertIsNotNone(RequestsInstrumentor().meter) + self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1) + recorder = RequestsInstrumentor().meter.metrics.pop() + match_key = get_dict_as_key( + { + "http.method": "GET", + "http.status_code": "500", + "http.status_text": "Internal Server Error", + "http.url": "http://httpbin.org/status/200", + } + ) + for key in recorder.bound_instruments.keys(): + self.assertEqual(key, match_key) + # pylint: disable=protected-access + bound = recorder.bound_instruments.get(key) + for view_data in bound.view_datas: + self.assertEqual(view_data.labels, key) + self.assertEqual(view_data.aggregator.current.count, 1) @mock.patch("requests.adapters.HTTPAdapter.send", side_effect=Exception) def test_requests_basic_exception(self, *_, **__): diff --git a/opentelemetry-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md index 37f5005103..13c01cc32a 100644 --- a/opentelemetry-instrumentation/CHANGELOG.md +++ b/opentelemetry-instrumentation/CHANGELOG.md @@ -10,6 +10,8 @@ Released 2020-09-17 - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) +- Add support for http metrics + ([#1116](https://github.com/open-telemetry/opentelemetry-python/pull/1116)) ## 0.9b0 diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 6042f043d9..167036238f 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -42,6 +42,7 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api == 0.14.dev0 + opentelemetry-sdk == 0.14.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py new file mode 100644 index 0000000000..74445cbff1 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py @@ -0,0 +1,85 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +""" +OpenTelemetry Instrumentation Metric mixin +""" +import enum +from contextlib import contextmanager +from time import time +from typing import Dict, Optional + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import ValueRecorder + + +class HTTPMetricType(enum.Enum): + CLIENT = 0 + SERVER = 1 + # TODO: Add both + + +class MetricMixin: + """Used to record metrics related to instrumentations.""" + + def init_metrics(self, name: str, version: str): + self._meter = metrics.get_meter(name, version) + + @property + def meter(self): + return self._meter + + +class MetricRecorder: + """Base class for metric recorders of different types.""" + + def __init__(self, meter: Optional[metrics.Meter] = None): + self._meter = meter + + +class HTTPMetricRecorder(MetricRecorder): + """Metric recorder for http instrumentations. Tracks duration.""" + + def __init__( + self, meter: Optional[metrics.Meter], http_type: HTTPMetricType, + ): + super().__init__(meter) + self._http_type = http_type + if self._meter: + self._duration = self._meter.create_metric( + name="{}.{}.duration".format( + "http", self._http_type.name.lower() + ), + description="measures the duration of the {} HTTP request".format( + "inbound" + if self._http_type is HTTPMetricType.SERVER + else "outbound" + ), + unit="ms", + value_type=float, + metric_type=ValueRecorder, + ) + + # Conventions for recording duration can be found at: + # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/metrics/semantic_conventions/http-metrics.md + @contextmanager + def record_duration(self, labels: Dict[str, str]): + start_time = time() + try: + yield start_time + finally: + if self._meter: + elapsed_time = (time() - start_time) * 1000 + self._duration.record(elapsed_time, labels) diff --git a/opentelemetry-instrumentation/tests/test_metric.py b/opentelemetry-instrumentation/tests/test_metric.py new file mode 100644 index 0000000000..ea8724fc88 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_metric.py @@ -0,0 +1,87 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from unittest import TestCase, mock + +from opentelemetry import metrics as metrics_api +from opentelemetry.instrumentation.metric import ( + HTTPMetricRecorder, + HTTPMetricType, + MetricMixin, +) +from opentelemetry.metrics import set_meter_provider +from opentelemetry.sdk import metrics +from opentelemetry.sdk.util import get_dict_as_key + + +# pylint: disable=protected-access +class TestMetricMixin(TestCase): + @classmethod + def setUpClass(cls): + metrics_api._METER_PROVIDER = None + set_meter_provider(metrics.MeterProvider()) + + @classmethod + def tearDownClass(cls): + metrics_api._METER_PROVIDER = None + + def test_init(self): + mixin = MetricMixin() + mixin.init_metrics("test", 1.0) + meter = mixin.meter + self.assertTrue(isinstance(meter, metrics.Meter)) + self.assertEqual(meter.instrumentation_info.name, "test") + self.assertEqual(meter.instrumentation_info.version, 1.0) + + +class TestHTTPMetricRecorder(TestCase): + @classmethod + def setUpClass(cls): + metrics_api._METER_PROVIDER = None + set_meter_provider(metrics.MeterProvider()) + + @classmethod + def tearDownClass(cls): + metrics_api._METER_PROVIDER = None + + def test_ctor(self): + meter = metrics_api.get_meter(__name__) + recorder = HTTPMetricRecorder(meter, HTTPMetricType.CLIENT) + # pylint: disable=protected-access + self.assertEqual(recorder._http_type, HTTPMetricType.CLIENT) + self.assertTrue(isinstance(recorder._duration, metrics.ValueRecorder)) + self.assertEqual(recorder._duration.name, "http.client.duration") + self.assertEqual( + recorder._duration.description, + "measures the duration of the outbound HTTP request", + ) + + def test_record_duration(self): + meter = metrics_api.get_meter(__name__) + recorder = HTTPMetricRecorder(meter, HTTPMetricType.CLIENT) + labels = {"test": "asd"} + with mock.patch("time.time") as time_patch: + time_patch.return_value = 5.0 + with recorder.record_duration(labels): + labels["test2"] = "asd2" + match_key = get_dict_as_key({"test": "asd", "test2": "asd2"}) + for key in recorder._duration.bound_instruments.keys(): + self.assertEqual(key, match_key) + # pylint: disable=protected-access + bound = recorder._duration.bound_instruments.get(key) + for view_data in bound.view_datas: + self.assertEqual(view_data.labels, key) + self.assertEqual(view_data.aggregator.current.count, 1) + self.assertGreaterEqual(view_data.aggregator.current.sum, 0) diff --git a/tox.ini b/tox.ini index b477d5ba74..806656ced2 100644 --- a/tox.ini +++ b/tox.ini @@ -396,8 +396,8 @@ deps = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ - -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ + -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-requests \ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi \ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-flask From b2559409b2bf82e693f3e68ed890dd7fd1fa8eae Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Fri, 25 Sep 2020 21:31:41 +0530 Subject: [PATCH 0576/1517] Zipkin: Fix OTLP events to Zipkin annotations translation (#1161) --- .../CHANGELOG.md | 1 + .../opentelemetry/exporter/zipkin/__init__.py | 29 +++++++++++-------- .../tests/test_zipkin_exporter.py | 10 ++++++- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index a980e34568..3801c81dc9 100644 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -4,6 +4,7 @@ - Zipkin exporter now accepts a ``max_tag_value_length`` attribute to customize the maximum allowed size a tag value can have. ([#1151](https://github.com/open-telemetry/opentelemetry-python/pull/1151)) +- Fixed OTLP events to Zipkin annotations translation. ([#1161](https://github.com/open-telemetry/opentelemetry-python/pull/1161)) ## Version 0.13b0 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 5e544275b3..bacfcc278d 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -237,20 +237,25 @@ def _extract_tags_from_span(self, span: Span): tags.update(self._extract_tags_from_dict(span.resource.attributes)) return tags - def _extract_annotations_from_events( - self, events - ): # pylint: disable=R0201 - return ( - [ + def _extract_annotations_from_events(self, events): + if not events: + return None + + annotations = [] + for event in events: + attrs = {} + for key, value in event.attributes.items(): + if isinstance(value, str): + value = value[: self.max_tag_value_length] + attrs[key] = value + + annotations.append( { - "timestamp": _nsec_to_usec_round(e.timestamp), - "value": e.name, + "timestamp": _nsec_to_usec_round(event.timestamp), + "value": json.dumps({event.name: attrs}), } - for e in events - ] - if events - else None - ) + ) + return annotations def _nsec_to_usec_round(nsec): diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 635594868f..1979d8459f 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -223,7 +223,15 @@ def test_export(self): "annotations": [ { "timestamp": event_timestamp // 10 ** 3, - "value": "event0", + "value": json.dumps( + { + "event0": { + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + } + } + ), } ], "debug": True, From c534a2c9f823311fa6a693004dedccee0156017f Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 29 Sep 2020 08:17:16 +0530 Subject: [PATCH 0577/1517] Zipkin: More deterministic unit test for comparing zipkin annotations (#1168) Zipkin annotation values are strings containing JSON documents. We cannot have deterministic ordering of event attributes as they may come in any order and python versions older than 3.7 don't have ordered dicts. We extract the annotations from exported spans, parse the JSON documents into Python dicts and then compare them. --- .../tests/test_zipkin_exporter.py | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 1979d8459f..6120f2dd6e 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -98,9 +98,8 @@ def test_constructor_explicit(self): self.assertEqual(exporter.ipv6, ipv6) self.assertEqual(exporter.url, url) - # pylint: disable=too-many-locals + # pylint: disable=too-many-locals,too-many-statements def test_export(self): - span_names = ("test1", "test2", "test3", "test4") trace_id = 0x6E0C63257DE34C926F9EFCD03927272E span_id = 0x34BF92DEEFC58C92 @@ -204,7 +203,7 @@ def test_export(self): local_endpoint = {"serviceName": service_name, "port": 9411} exporter = ZipkinSpanExporter(service_name) - expected = [ + expected_spans = [ { "traceId": format(trace_id, "x"), "id": format(span_id, "x"), @@ -220,22 +219,20 @@ def test_export(self): "otel.status_code": "2", "otel.status_description": "Example description", }, + "debug": True, + "parentId": format(parent_id, "x"), "annotations": [ { "timestamp": event_timestamp // 10 ** 3, - "value": json.dumps( - { - "event0": { - "annotation_bool": True, - "annotation_string": "annotation_test", - "key_float": 0.3, - } + "value": { + "event0": { + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, } - ), + }, } ], - "debug": True, - "parentId": format(parent_id, "x"), }, { "traceId": format(trace_id, "x"), @@ -289,11 +286,21 @@ def test_export(self): status = exporter.export(otel_spans) self.assertEqual(SpanExportResult.SUCCESS, status) - mock_post.assert_called_with( - url="http://localhost:9411/api/v2/spans", - data=json.dumps(expected), - headers={"Content-Type": "application/json"}, + # pylint: disable=unsubscriptable-object + kwargs = mock_post.call_args[1] + + self.assertEqual(kwargs["url"], "http://localhost:9411/api/v2/spans") + actual_spans = sorted( + json.loads(kwargs["data"]), key=lambda span: span["timestamp"] ) + for expected, actual in zip(expected_spans, actual_spans): + expected_annotations = expected.pop("annotations", None) + actual_annotations = actual.pop("annotations", None) + if actual_annotations: + for annotation in actual_annotations: + annotation["value"] = json.loads(annotation["value"]) + self.assertEqual(expected, actual) + self.assertEqual(expected_annotations, actual_annotations) # pylint: disable=too-many-locals def test_zero_padding(self): From 1522b44d98bd8da7b6390a7dcdff5d585c29caae Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 29 Sep 2020 10:22:11 +0530 Subject: [PATCH 0578/1517] Added ability to extract span attributes from django request objects. (#1154) OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS env var can be set to a command separated list of attributes names that will be extracted from Django's request object and set as attributes on spans. --- .../CHANGELOG.md | 1 + .../README.rst | 15 +++++++++++ .../instrumentation/django/middleware.py | 11 ++++++++ .../tests/test_middleware.py | 27 +++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index d3de446730..30b13c4a22 100644 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Changed span name extraction from request to comply semantic convention ([#992](https://github.com/open-telemetry/opentelemetry-python/pull/992)) +- Added support for `OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS` ([#1154](https://github.com/open-telemetry/opentelemetry-python/pull/1154)) ## Version 0.13b0 diff --git a/instrumentation/opentelemetry-instrumentation-django/README.rst b/instrumentation/opentelemetry-instrumentation-django/README.rst index 5cb570c7e9..a2b98cabf4 100644 --- a/instrumentation/opentelemetry-instrumentation-django/README.rst +++ b/instrumentation/opentelemetry-instrumentation-django/README.rst @@ -30,6 +30,21 @@ For example, will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. +Request attributes +******************** +To extract certain attributes from Django's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS`` to a comma +delimited list of request attribute names. + +For example, + +:: + + export OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS='path_info,content_type' + +will extract path_info and content_type attributes from every traced request and add them as span attritbues. + +Django Request object reference: https://docs.djangoproject.com/en/3.1/ref/request-response/#attributes + References ---------- diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index 59f7e6e622..f468053168 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -58,6 +58,13 @@ class _DjangoMiddleware(MiddlewareMixin): else: _excluded_urls = ExcludeList(_excluded_urls) + _traced_request_attrs = [ + attr.strip() + for attr in (Configuration().DJANGO_TRACED_REQUEST_ATTRS or "").split( + "," + ) + ] + @staticmethod def _get_span_name(request): try: @@ -95,6 +102,10 @@ def process_request(self, request): tracer = get_tracer(__name__, __version__) attributes = collect_request_attributes(environ) + for attr in self._traced_request_attrs: + value = getattr(request, attr, None) + if value is not None: + attributes[attr] = str(value) span = tracer.start_span( self._get_span_name(request), diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index ee82c5d7d9..378139d1c5 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -174,3 +174,30 @@ def test_span_name_404(self): span = span_list[0] self.assertEqual(span.name, "HTTP GET") + + def test_traced_request_attrs(self): + with patch( + "opentelemetry.instrumentation.django.middleware._DjangoMiddleware._traced_request_attrs", + [], + ): + Client().get("/span_name/1234/", CONTENT_TYPE="test/ct") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + span = span_list[0] + self.assertNotIn("path_info", span.attributes) + self.assertNotIn("content_type", span.attributes) + self.memory_exporter.clear() + + with patch( + "opentelemetry.instrumentation.django.middleware._DjangoMiddleware._traced_request_attrs", + ["path_info", "content_type", "non_existing_variable"], + ): + Client().get("/span_name/1234/", CONTENT_TYPE="test/ct") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + span = span_list[0] + self.assertEqual(span.attributes["path_info"], "/span_name/1234/") + self.assertEqual(span.attributes["content_type"], "test/ct") + self.assertNotIn("non_existing_variable", span.attributes) From 28e3a39ebd8b841834a28186bd7179c6c7190c47 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 29 Sep 2020 19:58:20 +0530 Subject: [PATCH 0579/1517] Added ability to extract span attributes from falcon request objects. (#1158) --- .../CHANGELOG.md | 2 ++ .../README.rst | 15 +++++++++ .../instrumentation/falcon/__init__.py | 32 +++++++++++++++---- .../tests/test_falcon.py | 17 ++++++++++ 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md index e14c730f74..f398b7460d 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Added support for `OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS` ([#1158](https://github.com/open-telemetry/opentelemetry-python/pull/1158)) + ## Version 0.13b0 Released 2020-09-17 diff --git a/instrumentation/opentelemetry-instrumentation-falcon/README.rst b/instrumentation/opentelemetry-instrumentation-falcon/README.rst index f7d5a99d95..8230deaf76 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/README.rst +++ b/instrumentation/opentelemetry-instrumentation-falcon/README.rst @@ -31,6 +31,21 @@ For example, will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. +Request attributes +******************** +To extract certain attributes from Falcon's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS`` to a comma +delimited list of request attribute names. + +For example, + +:: + + export OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS='query_string,uri_template' + +will extract path_info and content_type attributes from every traced request and add them as span attritbues. + +Falcon Request object reference: https://falcon.readthedocs.io/en/stable/api/request_and_response.html#id1 + References ---------- diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py index 1e67d6101b..660fc23063 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py @@ -50,6 +50,7 @@ def on_get(self, req, resp): import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry import configuration, context, propagators, trace +from opentelemetry.configuration import Configuration from opentelemetry.instrumentation.falcon.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import http_status_to_canonical_code @@ -92,13 +93,16 @@ def _uninstrument(self, **kwargs): class _InstrumentedFalconAPI(falcon.API): def __init__(self, *args, **kwargs): - mw = kwargs.pop("middleware", []) - if not isinstance(mw, (list, tuple)): - mw = [mw] + middlewares = kwargs.pop("middleware", []) + if not isinstance(middlewares, (list, tuple)): + middlewares = [middlewares] self._tracer = trace.get_tracer(__name__, __version__) - mw.insert(0, _TraceMiddleware(self._tracer)) - kwargs["middleware"] = mw + trace_middleware = _TraceMiddleware( + self._tracer, kwargs.get("traced_request_attributes") + ) + middlewares.insert(0, trace_middleware) + kwargs["middleware"] = middlewares super().__init__(*args, **kwargs) def __call__(self, env, start_response): @@ -144,8 +148,24 @@ def _start_response(status, response_headers, *args, **kwargs): class _TraceMiddleware: # pylint:disable=R0201,W0613 - def __init__(self, tracer=None): + def __init__(self, tracer=None, traced_request_attrs=None): self.tracer = tracer + self._traced_request_attrs = traced_request_attrs or [ + attr.strip() + for attr in ( + Configuration().FALCON_TRACED_REQUEST_ATTRS or "" + ).split(",") + ] + + def process_request(self, req, resp): + span = req.env.get(_ENVIRON_SPAN_KEY) + if not span: + return + + for attr in self._traced_request_attrs: + value = getattr(req, attr, None) + if value is not None: + span.set_attribute(attr, str(value)) def process_resource(self, req, resp, resource, params): span = req.env.get(_ENVIRON_SPAN_KEY) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py index a3d2c5d8d8..5e27e4aacb 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py @@ -171,3 +171,20 @@ def test_exclude_lists(self): self.client().simulate_get(path="/hello") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) + + def test_traced_request_attributes(self): + self.client().simulate_get(path="/hello?q=abc") + span = self.memory_exporter.get_finished_spans()[0] + self.assertNotIn("query_string", span.attributes) + self.memory_exporter.clear() + + middleware = self.app._middleware[0][ # pylint:disable=W0212 + 0 + ].__self__ + with patch.object( + middleware, "_traced_request_attrs", ["query_string"] + ): + self.client().simulate_get(path="/hello?q=abc") + span = self.memory_exporter.get_finished_spans()[0] + self.assertIn("query_string", span.attributes) + self.assertEqual(span.attributes["query_string"], "q=abc") From 9be899e84ccb5edcc9830680d5b00029d2583c28 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 29 Sep 2020 21:06:05 +0530 Subject: [PATCH 0580/1517] Added context propagation support to celery instrumentation (#1135) --- .../CHANGELOG.md | 3 + .../README.rst | 22 +++++- .../setup.cfg | 1 + .../instrumentation/celery/__init__.py | 42 ++++++++-- .../tests/celery_test_tasks.py | 29 +++++++ .../tests/test_tasks.py | 78 +++++++++++++++++++ .../tests/celery/test_celery_functional.py | 62 ++++++++------- .../tests/mysql/test_mysql_functional.py | 15 ++-- .../tests/postgres/test_aiopg_functional.py | 18 ++--- .../tests/postgres/test_psycopg_functional.py | 15 ++-- .../tests/pymongo/test_pymongo_functional.py | 12 +-- .../tests/pymysql/test_pymysql_functional.py | 12 +-- 12 files changed, 227 insertions(+), 82 deletions(-) create mode 100644 instrumentation/opentelemetry-instrumentation-celery/tests/celery_test_tasks.py create mode 100644 instrumentation/opentelemetry-instrumentation-celery/tests/test_tasks.py diff --git a/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md index e164a89134..da615bcc7b 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Span operation names now include the task type. ([#1135](https://github.com/open-telemetry/opentelemetry-python/pull/1135)) +- Added automatic context propagation. ([#1135](https://github.com/open-telemetry/opentelemetry-python/pull/1135)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-celery/README.rst b/instrumentation/opentelemetry-instrumentation-celery/README.rst index 42fe6646d1..307fd352b9 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/README.rst +++ b/instrumentation/opentelemetry-instrumentation-celery/README.rst @@ -29,11 +29,20 @@ Usage .. code-block:: python + from opentelemetry import trace + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor from opentelemetry.instrumentation.celery import CeleryInstrumentor - CeleryInstrumentor().instrument() - from celery import Celery + from celery.signals import worker_process_init + + @worker_process_init.connect(weak=False) + def init_celery_tracing(*args, **kwargs): + trace.set_tracer_provider(TracerProvider()) + span_processor = BatchExportSpanProcessor(ConsoleSpanExporter()) + trace.get_tracer_provider().add_span_processor(span_processor) + CeleryInstrumentor().instrument() app = Celery("tasks", broker="amqp://localhost") @@ -43,6 +52,15 @@ Usage add.delay(42, 50) + +Setting up tracing +-------------------- + +When tracing a celery worker process, tracing and instrumention both must be initialized after the celery worker +process is initialized. This is required for any tracing components that might use threading to work correctly +such as the BatchExportSpanProcessor. Celery provides a signal called ``worker_process_init`` that can be used to +accomplish this as shown in the example above. + References ---------- * `OpenTelemetry Celery Instrumentation `_ diff --git a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg index 79b63d928a..b5a039de11 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg @@ -46,6 +46,7 @@ install_requires = [options.extras_require] test = pytest + celery ~= 4.0 opentelemetry-test == 0.14.dev0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py index 7e2551142e..4768e93d18 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py @@ -30,11 +30,20 @@ .. code:: python + from opentelemetry import trace + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor from opentelemetry.instrumentation.celery import CeleryInstrumentor - CeleryInstrumentor().instrument() - from celery import Celery + from celery.signals import worker_process_init + + @worker_process_init.connect(weak=False) + def init_celery_tracing(*args, **kwargs): + trace.set_tracer_provider(TracerProvider()) + span_processor = BatchExportSpanProcessor(ConsoleSpanExporter()) + trace.get_tracer_provider().add_span_processor(span_processor) + CeleryInstrumentor().instrument() app = Celery("tasks", broker="amqp://localhost") @@ -50,13 +59,15 @@ def add(x, y): import logging import signal +from collections.abc import Iterable from celery import signals # pylint: disable=no-name-in-module -from opentelemetry import trace +from opentelemetry import propagators, trace from opentelemetry.instrumentation.celery import utils from opentelemetry.instrumentation.celery.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.trace.propagation import get_current_span from opentelemetry.trace.status import Status, StatusCanonicalCode logger = logging.getLogger(__name__) @@ -106,9 +117,16 @@ def _trace_prerun(self, *args, **kwargs): if task is None or task_id is None: return + request = task.request + tracectx = propagators.extract(carrier_extractor, request) or {} + parent = get_current_span(tracectx) + logger.debug("prerun signal start task_id=%s", task_id) - span = self._tracer.start_span(task.name, kind=trace.SpanKind.CONSUMER) + operation_name = "{0}/{1}".format(_TASK_RUN, task.name) + span = self._tracer.start_span( + operation_name, parent=parent, kind=trace.SpanKind.CONSUMER + ) activation = self._tracer.use_span(span, end_on_exit=True) activation.__enter__() @@ -146,7 +164,10 @@ def _trace_before_publish(self, *args, **kwargs): if task is None or task_id is None: return - span = self._tracer.start_span(task.name, kind=trace.SpanKind.PRODUCER) + operation_name = "{0}/{1}".format(_TASK_APPLY_ASYNC, task.name) + span = self._tracer.start_span( + operation_name, kind=trace.SpanKind.PRODUCER + ) # apply some attributes here because most of the data is not available span.set_attribute(_TASK_TAG_KEY, _TASK_APPLY_ASYNC) @@ -158,6 +179,10 @@ def _trace_before_publish(self, *args, **kwargs): activation.__enter__() utils.attach_span(task, task_id, (span, activation), is_publish=True) + headers = kwargs.get("headers") + if headers: + propagators.inject(type(headers).__setitem__, headers) + @staticmethod def _trace_after_publish(*args, **kwargs): task = utils.retrieve_task_from_sender(kwargs) @@ -221,3 +246,10 @@ def _trace_retry(*args, **kwargs): # Use `str(reason)` instead of `reason.message` in case we get # something that isn't an `Exception` span.set_attribute(_TASK_RETRY_REASON_KEY, str(reason)) + + +def carrier_extractor(carrier, key): + value = getattr(carrier, key, []) + if isinstance(value, str) or not isinstance(value, Iterable): + value = (value,) + return value diff --git a/instrumentation/opentelemetry-instrumentation-celery/tests/celery_test_tasks.py b/instrumentation/opentelemetry-instrumentation-celery/tests/celery_test_tasks.py new file mode 100644 index 0000000000..d9660412f0 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-celery/tests/celery_test_tasks.py @@ -0,0 +1,29 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from celery import Celery + + +class Config: + result_backend = "rpc" + broker_backend = "memory" + + +app = Celery(broker="memory:///") +app.config_from_object(Config) + + +@app.task +def task_add(num_a, num_b): + return num_a + num_b diff --git a/instrumentation/opentelemetry-instrumentation-celery/tests/test_tasks.py b/instrumentation/opentelemetry-instrumentation-celery/tests/test_tasks.py new file mode 100644 index 0000000000..3a05ebf331 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-celery/tests/test_tasks.py @@ -0,0 +1,78 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import time + +from opentelemetry.instrumentation.celery import CeleryInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import SpanKind + +from .celery_test_tasks import app, task_add + + +class TestCeleryInstrumentation(TestBase): + def setUp(self): + super().setUp() + self._worker = app.Worker(app=app, pool="solo", concurrency=1) + self._thread = threading.Thread(target=self._worker.start) + self._thread.daemon = True + self._thread.start() + + def tearDown(self): + super().tearDown() + self._worker.stop() + self._thread.join() + + def test_task(self): + CeleryInstrumentor().instrument() + + result = task_add.delay(1, 2) + while not result.ready(): + time.sleep(0.05) + + spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) + self.assertEqual(len(spans), 2) + + consumer, producer = spans + + self.assertEqual(consumer.name, "run/tests.celery_test_tasks.task_add") + self.assertEqual(consumer.kind, SpanKind.CONSUMER) + self.assert_span_has_attributes( + consumer, + { + "celery.action": "run", + "celery.state": "SUCCESS", + "messaging.destination": "celery", + "celery.task_name": "tests.celery_test_tasks.task_add", + }, + ) + + self.assertEqual( + producer.name, "apply_async/tests.celery_test_tasks.task_add" + ) + self.assertEqual(producer.kind, SpanKind.PRODUCER) + self.assert_span_has_attributes( + producer, + { + "celery.action": "apply_async", + "celery.task_name": "tests.celery_test_tasks.task_add", + "messaging.destination_kind": "queue", + "messaging.destination": "celery", + }, + ) + + self.assertNotEqual(consumer.parent, producer.context) + self.assertEqual(consumer.parent.span_id, producer.context.span_id) + self.assertEqual(consumer.context.trace_id, producer.context.trace_id) diff --git a/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py b/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py index f18c6cdba1..c4be6762ea 100644 --- a/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py +++ b/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py @@ -46,21 +46,21 @@ def fn_task(): async_span, run_span = spans - assert ( - async_span.instrumentation_info.name - == opentelemetry.instrumentation.celery.__name__ + assert run_span.parent == async_span.context + assert run_span.parent.span_id == async_span.context.span_id + assert run_span.context.trace_id == async_span.context.trace_id + + assert async_span.instrumentation_info.name == "apply_async/{0}".format( + opentelemetry.instrumentation.celery.__name__ ) - assert ( - async_span.instrumentation_info.version - == opentelemetry.instrumentation.celery.__version__ + assert async_span.instrumentation_info.version == "apply_async/{0}".format( + opentelemetry.instrumentation.celery.__version__ ) - assert ( - run_span.instrumentation_info.name - == opentelemetry.instrumentation.celery.__name__ + assert run_span.instrumentation_info.name == "run/{0}".format( + opentelemetry.instrumentation.celery.__name__ ) - assert ( - run_span.instrumentation_info.version - == opentelemetry.instrumentation.celery.__version__ + assert run_span.instrumentation_info.version == "run/{0}".format( + opentelemetry.instrumentation.celery.__version__ ) @@ -103,7 +103,7 @@ def fn_task(): span = spans[0] assert span.status.is_ok is True - assert span.name == "test_celery_functional.fn_task" + assert span.name == "run/test_celery_functional.fn_task" assert span.attributes.get("messaging.message_id") == t.task_id assert ( span.attributes.get("celery.task_name") @@ -128,7 +128,7 @@ def fn_task(self): span = spans[0] assert span.status.is_ok is True - assert span.name == "test_celery_functional.fn_task" + assert span.name == "run/test_celery_functional.fn_task" assert span.attributes.get("messaging.message_id") == t.task_id assert ( span.attributes.get("celery.task_name") @@ -157,7 +157,10 @@ def fn_task_parameters(user, force_logout=False): assert run_span.context.trace_id != async_span.context.trace_id assert async_span.status.is_ok is True - assert async_span.name == "test_celery_functional.fn_task_parameters" + assert ( + async_span.name + == "apply_async/test_celery_functional.fn_task_parameters" + ) assert async_span.attributes.get("celery.action") == "apply_async" assert async_span.attributes.get("messaging.message_id") == result.task_id assert ( @@ -209,7 +212,10 @@ def fn_task_parameters(user, force_logout=False): assert run_span.context.trace_id != async_span.context.trace_id assert async_span.status.is_ok is True - assert async_span.name == "test_celery_functional.fn_task_parameters" + assert ( + async_span.name + == "apply_async/test_celery_functional.fn_task_parameters" + ) assert async_span.attributes.get("celery.action") == "apply_async" assert async_span.attributes.get("messaging.message_id") == result.task_id assert ( @@ -218,7 +224,7 @@ def fn_task_parameters(user, force_logout=False): ) assert run_span.status.is_ok is True - assert run_span.name == "test_celery_functional.fn_task_parameters" + assert run_span.name == "run/test_celery_functional.fn_task_parameters" assert run_span.attributes.get("celery.action") == "run" assert run_span.attributes.get("celery.state") == "SUCCESS" assert run_span.attributes.get("messaging.message_id") == result.task_id @@ -244,7 +250,7 @@ def fn_exception(): span = spans[0] assert span.status.is_ok is False - assert span.name == "test_celery_functional.fn_exception" + assert span.name == "run/test_celery_functional.fn_exception" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "FAILURE" assert ( @@ -273,7 +279,7 @@ def fn_exception(): assert span.status.is_ok is True assert span.status.canonical_code == StatusCanonicalCode.OK - assert span.name == "test_celery_functional.fn_exception" + assert span.name == "run/test_celery_functional.fn_exception" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "FAILURE" assert ( @@ -300,7 +306,7 @@ def fn_exception(): assert span.status.is_ok is True assert span.status.canonical_code == StatusCanonicalCode.OK - assert span.name == "test_celery_functional.fn_exception" + assert span.name == "run/test_celery_functional.fn_exception" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "RETRY" assert ( @@ -332,7 +338,7 @@ def run(self): span = spans[0] assert span.status.is_ok is True - assert span.name == "test_celery_functional.BaseTask" + assert span.name == "run/test_celery_functional.BaseTask" assert ( span.attributes.get("celery.task_name") == "test_celery_functional.BaseTask" @@ -364,7 +370,7 @@ def run(self): span = spans[0] assert span.status.is_ok is False - assert span.name == "test_celery_functional.BaseTask" + assert span.name == "run/test_celery_functional.BaseTask" assert ( span.attributes.get("celery.task_name") == "test_celery_functional.BaseTask" @@ -401,7 +407,7 @@ def run(self): assert span.status.is_ok is True assert span.status.canonical_code == StatusCanonicalCode.OK - assert span.name == "test_celery_functional.BaseTask" + assert span.name == "run/test_celery_functional.BaseTask" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "FAILURE" assert span.attributes.get("messaging.message_id") == result.task_id @@ -423,7 +429,7 @@ def add(x, y): span = spans[0] assert span.status.is_ok is True - assert span.name == "test_celery_functional.add" + assert span.name == "run/test_celery_functional.add" assert ( span.attributes.get("celery.task_name") == "test_celery_functional.add" ) @@ -471,7 +477,7 @@ class CelerySubClass(CelerySuperClass): async_span, async_run_span, run_span = spans assert run_span.status.is_ok is True - assert run_span.name == "test_celery_functional.CelerySubClass" + assert run_span.name == "run/test_celery_functional.CelerySubClass" assert ( run_span.attributes.get("celery.task_name") == "test_celery_functional.CelerySubClass" @@ -481,7 +487,7 @@ class CelerySubClass(CelerySuperClass): assert run_span.attributes.get("messaging.message_id") == result.task_id assert async_run_span.status.is_ok is True - assert async_run_span.name == "test_celery_functional.CelerySubClass" + assert async_run_span.name == "run/test_celery_functional.CelerySubClass" assert ( async_run_span.attributes.get("celery.task_name") == "test_celery_functional.CelerySubClass" @@ -493,7 +499,9 @@ class CelerySubClass(CelerySuperClass): ) assert async_span.status.is_ok is True - assert async_span.name == "test_celery_functional.CelerySubClass" + assert ( + async_span.name == "apply_async/test_celery_functional.CelerySubClass" + ) assert ( async_span.attributes.get("celery.task_name") == "test_celery_functional.CelerySubClass" diff --git a/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py b/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py index 4116f4a19e..5be0be9f0e 100644 --- a/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py +++ b/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py @@ -75,15 +75,13 @@ def validate_spans(self): self.assertEqual(db_span.attributes["net.peer.port"], MYSQL_PORT) def test_execute(self): - """Should create a child span for execute - """ + """Should create a child span for execute""" with self._tracer.start_as_current_span("rootSpan"): self._cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") self.validate_spans() def test_execute_with_connection_context_manager(self): - """Should create a child span for execute with connection context - """ + """Should create a child span for execute with connection context""" with self._tracer.start_as_current_span("rootSpan"): with self._connection as conn: cursor = conn.cursor() @@ -91,16 +89,14 @@ def test_execute_with_connection_context_manager(self): self.validate_spans() def test_execute_with_cursor_context_manager(self): - """Should create a child span for execute with cursor context - """ + """Should create a child span for execute with cursor context""" with self._tracer.start_as_current_span("rootSpan"): with self._connection.cursor() as cursor: cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") self.validate_spans() def test_executemany(self): - """Should create a child span for executemany - """ + """Should create a child span for executemany""" with self._tracer.start_as_current_span("rootSpan"): data = (("1",), ("2",), ("3",)) stmt = "INSERT INTO test (id) VALUES (%s)" @@ -108,8 +104,7 @@ def test_executemany(self): self.validate_spans() def test_callproc(self): - """Should create a child span for callproc - """ + """Should create a child span for callproc""" with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( Exception ): diff --git a/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py b/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py index 9eb209636d..e7a0d39b51 100644 --- a/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py +++ b/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py @@ -85,8 +85,7 @@ def validate_spans(self): self.assertEqual(child_span.attributes["net.peer.port"], POSTGRES_PORT) def test_execute(self): - """Should create a child span for execute method - """ + """Should create a child span for execute method""" with self._tracer.start_as_current_span("rootSpan"): async_call( self._cursor.execute( @@ -96,8 +95,7 @@ def test_execute(self): self.validate_spans() def test_executemany(self): - """Should create a child span for executemany - """ + """Should create a child span for executemany""" with pytest.raises(psycopg2.ProgrammingError): with self._tracer.start_as_current_span("rootSpan"): data = (("1",), ("2",), ("3",)) @@ -106,8 +104,7 @@ def test_executemany(self): self.validate_spans() def test_callproc(self): - """Should create a child span for callproc - """ + """Should create a child span for callproc""" with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( Exception ): @@ -169,8 +166,7 @@ def validate_spans(self): self.assertEqual(child_span.attributes["net.peer.port"], POSTGRES_PORT) def test_execute(self): - """Should create a child span for execute method - """ + """Should create a child span for execute method""" with self._tracer.start_as_current_span("rootSpan"): async_call( self._cursor.execute( @@ -180,8 +176,7 @@ def test_execute(self): self.validate_spans() def test_executemany(self): - """Should create a child span for executemany - """ + """Should create a child span for executemany""" with pytest.raises(psycopg2.ProgrammingError): with self._tracer.start_as_current_span("rootSpan"): data = (("1",), ("2",), ("3",)) @@ -190,8 +185,7 @@ def test_executemany(self): self.validate_spans() def test_callproc(self): - """Should create a child span for callproc - """ + """Should create a child span for callproc""" with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( Exception ): diff --git a/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py b/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py index 8a703b0094..2739164781 100644 --- a/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py +++ b/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py @@ -77,8 +77,7 @@ def validate_spans(self): self.assertEqual(child_span.attributes["net.peer.port"], POSTGRES_PORT) def test_execute(self): - """Should create a child span for execute method - """ + """Should create a child span for execute method""" with self._tracer.start_as_current_span("rootSpan"): self._cursor.execute( "CREATE TABLE IF NOT EXISTS test (id integer)" @@ -86,8 +85,7 @@ def test_execute(self): self.validate_spans() def test_execute_with_connection_context_manager(self): - """Should create a child span for execute with connection context - """ + """Should create a child span for execute with connection context""" with self._tracer.start_as_current_span("rootSpan"): with self._connection as conn: cursor = conn.cursor() @@ -95,8 +93,7 @@ def test_execute_with_connection_context_manager(self): self.validate_spans() def test_execute_with_cursor_context_manager(self): - """Should create a child span for execute with cursor context - """ + """Should create a child span for execute with cursor context""" with self._tracer.start_as_current_span("rootSpan"): with self._connection.cursor() as cursor: cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") @@ -104,8 +101,7 @@ def test_execute_with_cursor_context_manager(self): self.assertTrue(cursor.closed) def test_executemany(self): - """Should create a child span for executemany - """ + """Should create a child span for executemany""" with self._tracer.start_as_current_span("rootSpan"): data = (("1",), ("2",), ("3",)) stmt = "INSERT INTO test (id) VALUES (%s)" @@ -113,8 +109,7 @@ def test_executemany(self): self.validate_spans() def test_callproc(self): - """Should create a child span for callproc - """ + """Should create a child span for callproc""" with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( Exception ): diff --git a/tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py b/tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py index 8c52ad0656..acb60178d0 100644 --- a/tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py +++ b/tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py @@ -64,8 +64,7 @@ def validate_spans(self): ) def test_insert(self): - """Should create a child span for insert - """ + """Should create a child span for insert""" with self._tracer.start_as_current_span("rootSpan"): self._collection.insert_one( {"name": "testName", "value": "testValue"} @@ -73,8 +72,7 @@ def test_insert(self): self.validate_spans() def test_update(self): - """Should create a child span for update - """ + """Should create a child span for update""" with self._tracer.start_as_current_span("rootSpan"): self._collection.update_one( {"name": "testName"}, {"$set": {"value": "someOtherValue"}} @@ -82,15 +80,13 @@ def test_update(self): self.validate_spans() def test_find(self): - """Should create a child span for find - """ + """Should create a child span for find""" with self._tracer.start_as_current_span("rootSpan"): self._collection.find_one() self.validate_spans() def test_delete(self): - """Should create a child span for delete - """ + """Should create a child span for delete""" with self._tracer.start_as_current_span("rootSpan"): self._collection.delete_one({"name": "testName"}) self.validate_spans() diff --git a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py index 7b0cb5b0c0..c5c4d4f449 100644 --- a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py +++ b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py @@ -72,23 +72,20 @@ def validate_spans(self): self.assertEqual(db_span.attributes["net.peer.port"], MYSQL_PORT) def test_execute(self): - """Should create a child span for execute - """ + """Should create a child span for execute""" with self._tracer.start_as_current_span("rootSpan"): self._cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") self.validate_spans() def test_execute_with_cursor_context_manager(self): - """Should create a child span for execute with cursor context - """ + """Should create a child span for execute with cursor context""" with self._tracer.start_as_current_span("rootSpan"): with self._connection.cursor() as cursor: cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") self.validate_spans() def test_executemany(self): - """Should create a child span for executemany - """ + """Should create a child span for executemany""" with self._tracer.start_as_current_span("rootSpan"): data = (("1",), ("2",), ("3",)) stmt = "INSERT INTO test (id) VALUES (%s)" @@ -96,8 +93,7 @@ def test_executemany(self): self.validate_spans() def test_callproc(self): - """Should create a child span for callproc - """ + """Should create a child span for callproc""" with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( Exception ): From f4521fdbcd673468fba319ec9bf24533200fcc8d Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 29 Sep 2020 20:23:33 -0700 Subject: [PATCH 0581/1517] removing the circleci config (#1179) --- .circleci/config.yml | 128 ------------------------------------------- README.md | 4 +- 2 files changed, 2 insertions(+), 130 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index a718eb04eb..0000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,128 +0,0 @@ -version: 2.1 - -executors: - py38: - docker: - - image: circleci/python:3.8 - py37: - docker: - - image: circleci/python:3.7 - py36: - docker: - - image: circleci/python:3.6 - py35: - docker: - - image: circleci/python:3.5 - pypy3: - docker: - - image: pypy:3 - -commands: - setup_tox: - description: "Install tox" - steps: - - run: pip install -U tox-factor - - restore_tox_cache: - description: "Restore .tox directory from previous runs for faster installs" - steps: - - restore_cache: - # In the cache key: - # - .Environment.CIRCLE_JOB: We do separate tox environments by job name, so caching and restoring is - # much faster. - key: tox-cache-{{ .Environment.CIRCLE_JOB }}-{{ checksum "tox.ini" }}-{{ checksum "dev-requirements.txt" }} - - save_tox_cache: - description: "Save .tox directory into cache for faster installs next time" - steps: - - save_cache: - # In the cache key: - # - .Environment.CIRCLE_JOB: We do separate tox environments by job name, so caching and restoring is - # much faster. - key: tox-cache-{{ .Environment.CIRCLE_JOB }}-{{ checksum "tox.ini" }}-{{ checksum "dev-requirements.txt" }} - paths: - - ".tox" - - run_tox_scenario: - description: "Run scripts/run-tox-scenario with setup, caching and persistence" - parameters: - pattern: - type: string - steps: - - checkout - - setup_tox - - restore_tox_cache - - run: - name: "Run scripts/run-tox-scenario" - command: tox -f '<< parameters.pattern >>' - - save_tox_cache - -jobs: - docs: - executor: py38 - steps: - - run_tox_scenario: - pattern: docs - - docker-tests: - machine: - image: ubuntu-1604:201903-01 - steps: - - checkout - - run: - name: "Get pyenv list" - command: pyenv versions - - run: - name: "Set Python Version" - command: pyenv global 3.7.0 - - run: - name: "Update pip" - command: pip install -U pip - - setup_tox - - restore_tox_cache - - run: tox -e docker-tests - - save_tox_cache - - lint: - executor: py38 - steps: - - run_tox_scenario: - pattern: lint - - build-py38: - parameters: - package: - type: string - executor: py38 - steps: - - run_tox_scenario: - pattern: py38-<< parameters.package >> - - build: - parameters: - version: - type: string - default: "py38" - package: - type: string - default: "core" - executor: << parameters.version >> - steps: - - run_tox_scenario: - pattern: << parameters.version >>-<< parameters.package >> - -workflows: - circleci-build: - jobs: - - build-py38: - matrix: - parameters: - package: ["core", "exporter", "instrumentation", "tracecontext", "mypy", "mypyinstalled"] - - build: - matrix: - parameters: - version: ["py37", "py36", "py35", "pypy3"] - package: ["core", "exporter", "instrumentation"] - - docs - - lint - - docker-tests diff --git a/README.md b/README.md index ca9fd652ac..3cea35f45a 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ license
- - Build Status + + Build Status Beta

From a71d383edc8636c9db71c8dd5601d4b9a7654c02 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 29 Sep 2020 21:22:40 -0700 Subject: [PATCH 0582/1517] docs: updating trace API docstrings (#1170) The current docstrings were not factually correct, and different wording provided better clarity: - start_as_current_span shoudl return the current span back to the previous current span in the context, rather than the parent span. This is the behavior of the SDK. - There is not "tracer context", simply the "context" object. So renaming references there. --- .../src/opentelemetry/trace/__init__.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 1795192254..c3c2098ff8 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -237,14 +237,15 @@ def start_span( """Starts a span. Create a new span. Start the span without setting it as the current - span in this tracer's context. + span in the context. To start the span and use the context in a single + method, see :meth:`start_as_current_span`. - By default the current span will be used as parent, but an explicit - parent can also be specified, either a `Span` or a `opentelemetry.trace.SpanContext`. If - the specified value is `None`, the created span will be a root span. + By default the current span in the context will be used as parent, but an + explicit parent can also be specified, either a `Span` or a `opentelemetry.trace.SpanContext`. + If the specified value is `None`, the created span will be a root span. - The span can be used as context manager. On exiting, the span will be - ended. + The span can be used as a context manager. On exiting the context manager, + the span's end() method will be called. Example:: @@ -253,9 +254,6 @@ def start_span( with tracer.start_span("one") as child: child.add_event("child's event") - Applications that need to set the newly created span as the current - instance should use :meth:`start_as_current_span` instead. - Args: name: The name of the span to be created. parent: The span's parent. Defaults to the current span. @@ -287,8 +285,9 @@ def start_as_current_span( """Context manager for creating a new span and set it as the current span in this tracer's context. - On exiting the context manager stops the span and set its parent as the - current span. + Exiting the context manager will call the span's end method, + as well as return the current span to it's previous value by + returning to the previous context. Example:: @@ -330,7 +329,9 @@ def start_as_current_span( def use_span( self, span: "Span", end_on_exit: bool = False ) -> typing.Iterator[None]: - """Context manager for controlling a span's lifetime. + """Context manager for setting the passed span as the + current span in the context, as well as resetting the + context back upon exiting the context manager. Set the given span as the current span in this tracer's context. From c68ce4621ccb2d8227de18a5039ef179c3fe63de Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 30 Sep 2020 08:22:53 -0600 Subject: [PATCH 0583/1517] Add Global Error Handler (#1080) --- docs/examples/error_hander/README.rst | 153 ++++++++++++++++++ .../error_hander/error_handler_0/README.rst | 4 + .../error_hander/error_handler_0/setup.cfg | 46 ++++++ .../error_hander/error_handler_0/setup.py | 26 +++ .../src/error_handler_0/__init__.py | 25 +++ .../src/error_handler_0/version.py | 15 ++ .../error_hander/error_handler_1/README.rst | 4 + .../error_hander/error_handler_1/setup.cfg | 46 ++++++ .../error_hander/error_handler_1/setup.py | 26 +++ .../src/error_handler_1/__init__.py | 30 ++++ .../src/error_handler_1/version.py | 15 ++ docs/examples/error_hander/example.py | 29 ++++ docs/sdk/error_handler.rst | 7 + docs/sdk/sdk.rst | 1 + opentelemetry-sdk/CHANGELOG.md | 2 + .../sdk/error_handler/__init__.py | 149 +++++++++++++++++ .../tests/error_handler/__init__.py | 0 .../tests/error_handler/test_error_handler.py | 133 +++++++++++++++ scripts/check_for_valid_readme.py | 1 + 19 files changed, 712 insertions(+) create mode 100644 docs/examples/error_hander/README.rst create mode 100644 docs/examples/error_hander/error_handler_0/README.rst create mode 100644 docs/examples/error_hander/error_handler_0/setup.cfg create mode 100644 docs/examples/error_hander/error_handler_0/setup.py create mode 100644 docs/examples/error_hander/error_handler_0/src/error_handler_0/__init__.py create mode 100644 docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py create mode 100644 docs/examples/error_hander/error_handler_1/README.rst create mode 100644 docs/examples/error_hander/error_handler_1/setup.cfg create mode 100644 docs/examples/error_hander/error_handler_1/setup.py create mode 100644 docs/examples/error_hander/error_handler_1/src/error_handler_1/__init__.py create mode 100644 docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py create mode 100644 docs/examples/error_hander/example.py create mode 100644 docs/sdk/error_handler.rst create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/error_handler/__init__.py create mode 100644 opentelemetry-sdk/tests/error_handler/__init__.py create mode 100644 opentelemetry-sdk/tests/error_handler/test_error_handler.py diff --git a/docs/examples/error_hander/README.rst b/docs/examples/error_hander/README.rst new file mode 100644 index 0000000000..45b3d0bdac --- /dev/null +++ b/docs/examples/error_hander/README.rst @@ -0,0 +1,153 @@ +Global Error Handler +==================== + +Overview +-------- + +This example shows how to use the global error handler. + + +Preparation +----------- + +This example will be executed in a separate virtual environment: + +.. code:: sh + + $ mkdir global_error_handler + $ virtualenv global_error_handler + $ source global_error_handler/bin/activate + +Installation +------------ + +Here we install first ``opentelemetry-sdk``, the only dependency. Afterwards, 2 +error handlers are installed: ``error_handler_0`` will handle +``ZeroDivisionError`` exceptions, ``error_handler_1`` will handle +``IndexError`` and ``KeyError`` exceptions. + +.. code:: sh + + $ pip install opentelemetry-sdk + $ git clone https://github.com/open-telemetry/opentelemetry-python.git + $ pip install -e opentelemetry-python/docs/examples/error_handler/error_handler_0 + $ pip install -e opentelemetry-python/docs/examples/error_handler/error_handler_1 + +Execution +--------- + +An example is provided in the +``opentelemetry-python/docs/examples/error_handler/example.py``. + +You can just run it, you should get output similar to this one: + +.. code:: pytb + + ErrorHandler0 handling a ZeroDivisionError + Traceback (most recent call last): + File "test.py", line 5, in + 1 / 0 + ZeroDivisionError: division by zero + + ErrorHandler1 handling an IndexError + Traceback (most recent call last): + File "test.py", line 11, in + [1][2] + IndexError: list index out of range + + ErrorHandler1 handling a KeyError + Traceback (most recent call last): + File "test.py", line 17, in + {1: 2}[2] + KeyError: 2 + + Error handled by default error handler: + Traceback (most recent call last): + File "test.py", line 23, in + assert False + AssertionError + + No error raised + +The ``opentelemetry-sdk.error_handler`` module includes documentation that +explains how this works. We recommend you read it also, here is just a small +summary. + +In ``example.py`` we use ``GlobalErrorHandler`` as a context manager in several +places, for example: + + +.. code:: python + + with GlobalErrorHandler(): + {1: 2}[2] + +Running that code will raise a ``KeyError`` exception. +``GlobalErrorHandler`` will "capture" that exception and pass it down to the +registered error handlers. If there is one that handles ``KeyError`` exceptions +then it will handle it. That can be seen in the result of the execution of +``example.py``: + +.. code:: + + ErrorHandler1 handling a KeyError + Traceback (most recent call last): + File "test.py", line 17, in + {1: 2}[2] + KeyError: 2 + +There is no registered error handler that can handle ``AssertionError`` +exceptions so this kind of errors are handled by the default error handler +which just logs the exception to standard logging, as seen here: + +.. code:: + + Error handled by default error handler: + Traceback (most recent call last): + File "test.py", line 23, in + assert False + AssertionError + +When no exception is raised, the code inside the scope of +``GlobalErrorHandler`` is exectued normally: + +.. code:: + + No error raised + +Users can create Python packages that provide their own custom error handlers +and install them in their virtual environments before running their code which +instantiates ``GlobalErrorHandler`` context managers. ``error_handler_0`` and +``error_handler_1`` can be used as examples to create these custom error +handlers. + +In order for the error handlers to be registered, they need to create a class +that inherits from ``opentelemetry.sdk.error_handler.ErrorHandler`` and at +least one ``Exception``-type class. For example, this is an error handler that +handles ``ZeroDivisionError`` exceptions: + +.. code:: python + + from opentelemetry.sdk.error_handler import ErrorHandler + from logging import getLogger + + logger = getLogger(__name__) + + + class ErrorHandler0(ErrorHandler, ZeroDivisionError): + + def handle(self, error: Exception, *args, **kwargs): + + logger.exception("ErrorHandler0 handling a ZeroDivisionError") + +To register this error handler, use the ``opentelemetry_error_handler`` entry +point in the setup of the error handler package: + +.. code:: + + [options.entry_points] + opentelemetry_error_handler = + error_handler_0 = error_handler_0:ErrorHandler0 + +This entry point should point to the error handler class, ``ErrorHandler0`` in +this case. diff --git a/docs/examples/error_hander/error_handler_0/README.rst b/docs/examples/error_hander/error_handler_0/README.rst new file mode 100644 index 0000000000..0c86902e4c --- /dev/null +++ b/docs/examples/error_hander/error_handler_0/README.rst @@ -0,0 +1,4 @@ +Error Handler 0 +=============== + +This is just an error handler for this example. diff --git a/docs/examples/error_hander/error_handler_0/setup.cfg b/docs/examples/error_hander/error_handler_0/setup.cfg new file mode 100644 index 0000000000..61760f5dea --- /dev/null +++ b/docs/examples/error_hander/error_handler_0/setup.cfg @@ -0,0 +1,46 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = error-handler-0 +description = This is just an error handler example package +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-sdk == 0.14.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_error_handler = + error_handler_0 = error_handler_0:ErrorHandler0 diff --git a/docs/examples/error_hander/error_handler_0/setup.py b/docs/examples/error_hander/error_handler_0/setup.py new file mode 100644 index 0000000000..9e174aa7bb --- /dev/null +++ b/docs/examples/error_hander/error_handler_0/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "error_handler_0", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/docs/examples/error_hander/error_handler_0/src/error_handler_0/__init__.py b/docs/examples/error_hander/error_handler_0/src/error_handler_0/__init__.py new file mode 100644 index 0000000000..8b42b7c70e --- /dev/null +++ b/docs/examples/error_hander/error_handler_0/src/error_handler_0/__init__.py @@ -0,0 +1,25 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from logging import getLogger + +from opentelemetry.sdk.error_handler import ErrorHandler + +logger = getLogger(__name__) + + +class ErrorHandler0(ErrorHandler, ZeroDivisionError): + def _handle(self, error: Exception, *args, **kwargs): + + logger.exception("ErrorHandler0 handling a ZeroDivisionError") diff --git a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py new file mode 100644 index 0000000000..0f99027898 --- /dev/null +++ b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.14.dev0" diff --git a/docs/examples/error_hander/error_handler_1/README.rst b/docs/examples/error_hander/error_handler_1/README.rst new file mode 100644 index 0000000000..029b95f5c0 --- /dev/null +++ b/docs/examples/error_hander/error_handler_1/README.rst @@ -0,0 +1,4 @@ +Error Handler 1 +=============== + +This is just an error handler for this example. diff --git a/docs/examples/error_hander/error_handler_1/setup.cfg b/docs/examples/error_hander/error_handler_1/setup.cfg new file mode 100644 index 0000000000..0237f3692e --- /dev/null +++ b/docs/examples/error_hander/error_handler_1/setup.cfg @@ -0,0 +1,46 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = error_handler_1 +description = This is just an error handler example package +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-sdk == 0.14.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_error_handler = + error_handler_1 = error_handler_1:ErrorHandler1 diff --git a/docs/examples/error_hander/error_handler_1/setup.py b/docs/examples/error_hander/error_handler_1/setup.py new file mode 100644 index 0000000000..ccb282dbb2 --- /dev/null +++ b/docs/examples/error_hander/error_handler_1/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "error_handler_1", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/docs/examples/error_hander/error_handler_1/src/error_handler_1/__init__.py b/docs/examples/error_hander/error_handler_1/src/error_handler_1/__init__.py new file mode 100644 index 0000000000..cc63465617 --- /dev/null +++ b/docs/examples/error_hander/error_handler_1/src/error_handler_1/__init__.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from logging import getLogger + +from opentelemetry.sdk.error_handler import ErrorHandler + +logger = getLogger(__name__) + + +# pylint: disable=too-many-ancestors +class ErrorHandler1(ErrorHandler, IndexError, KeyError): + def _handle(self, error: Exception, *args, **kwargs): + + if isinstance(error, IndexError): + logger.exception("ErrorHandler1 handling an IndexError") + + elif isinstance(error, KeyError): + logger.exception("ErrorHandler1 handling a KeyError") diff --git a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py new file mode 100644 index 0000000000..0f99027898 --- /dev/null +++ b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.14.dev0" diff --git a/docs/examples/error_hander/example.py b/docs/examples/error_hander/example.py new file mode 100644 index 0000000000..372c39c16f --- /dev/null +++ b/docs/examples/error_hander/example.py @@ -0,0 +1,29 @@ +from opentelemetry.sdk.error_handler import GlobalErrorHandler + +# ZeroDivisionError to be handled by ErrorHandler0 +with GlobalErrorHandler(): + 1 / 0 + +print() + +# IndexError to be handled by ErrorHandler1 +with GlobalErrorHandler(): + [1][2] + +print() + +# KeyError to be handled by ErrorHandler1 +with GlobalErrorHandler(): + {1: 2}[2] + +print() + +# AssertionError to be handled by DefaultErrorHandler +with GlobalErrorHandler(): + assert False + +print() + +# No error raised +with GlobalErrorHandler(): + print("No error raised") diff --git a/docs/sdk/error_handler.rst b/docs/sdk/error_handler.rst new file mode 100644 index 0000000000..49962bf769 --- /dev/null +++ b/docs/sdk/error_handler.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.error_handler package +======================================= + +.. automodule:: opentelemetry.sdk.error_handler + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sdk/sdk.rst b/docs/sdk/sdk.rst index b1dae535e2..e777aebac6 100644 --- a/docs/sdk/sdk.rst +++ b/docs/sdk/sdk.rst @@ -9,3 +9,4 @@ OpenTelemetry Python SDK metrics resources trace + error_handler diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index c9b4c3538d..1688de79b2 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add Global Error Handler + ([#1080](https://github.com/open-telemetry/opentelemetry-python/pull/1080)) - Update sampling result names ([#1128](https://github.com/open-telemetry/opentelemetry-python/pull/1128)) - Add support for `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_SCHEDULE_DELAY_MILLIS`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` and `OTEL_BSP_EXPORT_TIMEOUT_MILLIS` environment variables diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/error_handler/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/error_handler/__init__.py new file mode 100644 index 0000000000..6afbd7c2f3 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/error_handler/__init__.py @@ -0,0 +1,149 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Global Error Handler + +This module provides a global error handler and an interface that allows +error handlers to be registered with the global error handler via entry points. +A default error handler is also provided. + +To use this feature, users can create an error handler that is registered +using the ``opentelemetry_error_handler`` entry point. A class is to be +registered in this entry point, this class must inherit from the +``opentelemetry.sdk.error_handler.ErrorHandler`` class and implement the +corresponding ``handle`` method. This method will receive the exception object +that is to be handled. The error handler class should also inherit from the +exception classes it wants to handle. For example, this would be an error +handler that handles ``ZeroDivisionError``: + +.. code:: python + + from opentelemetry.sdk.error_handler import ErrorHandler + from logging import getLogger + + logger = getLogger(__name__) + + + class ErrorHandler0(ErrorHandler, ZeroDivisionError): + + def _handle(self, error: Exception, *args, **kwargs): + + logger.exception("ErrorHandler0 handling a ZeroDivisionError") + +To use the global error handler, just instantiate it as a context manager where +you want exceptions to be handled: + + +.. code:: python + + from opentelemetry.sdk.error_handler import GlobalErrorHandler + + with GlobalErrorHandler(): + 1 / 0 + +If the class of the exception raised in the scope of the ``GlobalErrorHandler`` +object is not parent of any registered error handler, then the default error +handler will handle the exception. This default error handler will only log the +exception to standard logging, the exception won't be raised any further. +""" + +from abc import ABC, abstractmethod +from logging import getLogger + +from pkg_resources import iter_entry_points + +logger = getLogger(__name__) + + +class ErrorHandler(ABC): + @abstractmethod + def _handle(self, error: Exception, *args, **kwargs): + """ + Handle an exception + """ + + +class DefaultErrorHandler(ErrorHandler): + """ + Default error handler + + This error handler just logs the exception using standard logging. + """ + + # pylint: disable=useless-return + def _handle(self, error: Exception, *args, **kwargs): + + logger.exception("Error handled by default error handler: ") + return None + + +class GlobalErrorHandler: + """ + Global error handler + + This is a singleton class that can be instantiated anywhere to get the + global error handler. This object provides a handle method that receives + an exception object that will be handled by the registered error handlers. + """ + + _instance = None + + def __new__(cls) -> "GlobalErrorHandler": + if cls._instance is None: + cls._instance = super().__new__(cls) + + return cls._instance + + def __enter__(self): + pass + + # pylint: disable=no-self-use + def __exit__(self, exc_type, exc_value, traceback): + + if exc_value is None: + + return None + + plugin_handled = False + + for error_handler_entry_point in iter_entry_points( + "opentelemetry_error_handler" + ): + + error_handler_class = error_handler_entry_point.load() + + if issubclass(error_handler_class, exc_value.__class__): + + try: + + error_handler_class()._handle(exc_value) + plugin_handled = True + + # pylint: disable=broad-except + except Exception as error_handling_error: + + logger.exception( + "%s error while handling error" + " %s by error handler %s", + error_handling_error.__class__.__name__, + exc_value.__class__.__name__, + error_handler_class.__name__, + ) + + if not plugin_handled: + + DefaultErrorHandler()._handle(exc_value) + + return True diff --git a/opentelemetry-sdk/tests/error_handler/__init__.py b/opentelemetry-sdk/tests/error_handler/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-sdk/tests/error_handler/test_error_handler.py b/opentelemetry-sdk/tests/error_handler/test_error_handler.py new file mode 100644 index 0000000000..7ec572d937 --- /dev/null +++ b/opentelemetry-sdk/tests/error_handler/test_error_handler.py @@ -0,0 +1,133 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: disable=broad-except + +from logging import ERROR +from unittest import TestCase +from unittest.mock import Mock, patch + +from opentelemetry.sdk.error_handler import ( + ErrorHandler, + GlobalErrorHandler, + logger, +) + + +class TestErrorHandler(TestCase): + def test_default_error_handler(self): + + with self.assertLogs(logger, ERROR): + with GlobalErrorHandler(): + raise Exception("some exception") + + # pylint: disable=no-self-use + @patch("opentelemetry.sdk.error_handler.iter_entry_points") + def test_plugin_error_handler(self, mock_iter_entry_points): + class ZeroDivisionErrorHandler(ErrorHandler, ZeroDivisionError): + # pylint: disable=arguments-differ + + _handle = Mock() + + class AssertionErrorHandler(ErrorHandler, AssertionError): + # pylint: disable=arguments-differ + + _handle = Mock() + + mock_entry_point_zero_division_error_handler = Mock() + mock_entry_point_zero_division_error_handler.configure_mock( + **{"load.return_value": ZeroDivisionErrorHandler} + ) + mock_entry_point_assertion_error_handler = Mock() + mock_entry_point_assertion_error_handler.configure_mock( + **{"load.return_value": AssertionErrorHandler} + ) + + mock_iter_entry_points.configure_mock( + **{ + "return_value": [ + mock_entry_point_zero_division_error_handler, + mock_entry_point_assertion_error_handler, + ] + } + ) + + error = ZeroDivisionError() + + with GlobalErrorHandler(): + raise error + + # pylint: disable=protected-access + ZeroDivisionErrorHandler._handle.assert_called_with(error) + + error = AssertionError() + + with GlobalErrorHandler(): + raise error + + AssertionErrorHandler._handle.assert_called_with(error) + + @patch("opentelemetry.sdk.error_handler.iter_entry_points") + def test_error_in_handler(self, mock_iter_entry_points): + class ErrorErrorHandler(ErrorHandler, ZeroDivisionError): + # pylint: disable=arguments-differ + + def _handle(self, error: Exception): + assert False + + mock_entry_point_error_error_handler = Mock() + mock_entry_point_error_error_handler.configure_mock( + **{"load.return_value": ErrorErrorHandler} + ) + + mock_iter_entry_points.configure_mock( + **{"return_value": [mock_entry_point_error_error_handler]} + ) + + error = ZeroDivisionError() + + with self.assertLogs(logger, ERROR): + with GlobalErrorHandler(): + raise error + + # pylint: disable=no-self-use + @patch("opentelemetry.sdk.error_handler.iter_entry_points") + def test_plugin_error_handler_context_manager( + self, mock_iter_entry_points + ): + + mock_error_handler_instance = Mock() + + class MockErrorHandlerClass(IndexError): + def __new__(cls): + return mock_error_handler_instance + + mock_entry_point_error_handler = Mock() + mock_entry_point_error_handler.configure_mock( + **{"load.return_value": MockErrorHandlerClass} + ) + + mock_iter_entry_points.configure_mock( + **{"return_value": [mock_entry_point_error_handler]} + ) + + error = IndexError() + + with GlobalErrorHandler(): + raise error + + with GlobalErrorHandler(): + pass + + # pylint: disable=protected-access + mock_error_handler_instance._handle.assert_called_once_with(error) diff --git a/scripts/check_for_valid_readme.py b/scripts/check_for_valid_readme.py index d555b4fa2f..7eff0ae582 100644 --- a/scripts/check_for_valid_readme.py +++ b/scripts/check_for_valid_readme.py @@ -29,6 +29,7 @@ def main(): error = False for path in map(Path, args.paths): + readme = path / "README.rst" try: if not is_valid_rst(readme): From 5eb05983ebb56f5321f8e46764e9b4f4567e885a Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 30 Sep 2020 10:36:57 -0400 Subject: [PATCH 0584/1517] Use is_recording flag in flask, django, tornado, boto, botocore instrumentations (#1164) --- .../instrumentation/asgi/__init__.py | 2 + .../instrumentation/boto/__init__.py | 38 ++++++++++--------- .../tests/test_boto_instrumentation.py | 18 +++++++++ .../instrumentation/botocore/__init__.py | 38 +++++++++++-------- .../tests/test_botocore_instrumentation.py | 33 ++++++++++++++++ .../instrumentation/django/middleware.py | 16 ++++---- .../tests/test_middleware.py | 17 ++++++++- .../instrumentation/flask/__init__.py | 15 +++++--- .../tests/test_programmatic.py | 17 ++++++++- .../instrumentation/tornado/client.py | 16 ++++---- .../tests/test_instrumentation.py | 17 ++++++++- 11 files changed, 171 insertions(+), 56 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index 71211907e0..879662ddcf 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -91,6 +91,8 @@ def collect_request_attributes(scope): def set_status_code(span, status_code): """Adds HTTP response attributes to span using the status_code argument.""" + if not span.is_recording(): + return try: status_code = int(status_code) except ValueError: diff --git a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py index 8e03cd6e74..3bef955d14 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py @@ -136,27 +136,31 @@ def _common_request( # pylint: disable=too-many-locals attributes={"endpoint": endpoint_name} ) - add_span_arg_tags( - span, endpoint_name, args, args_name, traced_args, - ) + # Original func returns a boto.connection.HTTPResponse object + result = original_func(*args, **kwargs) - # Obtaining region name - region_name = _get_instance_region_name(instance) + if span.is_recording(): + add_span_arg_tags( + span, endpoint_name, args, args_name, traced_args, + ) - meta = { - "aws.agent": "boto", - "aws.operation": operation_name, - } - if region_name: - meta["aws.region"] = region_name + # Obtaining region name + region_name = _get_instance_region_name(instance) - for key, value in meta.items(): - span.set_attribute(key, value) + meta = { + "aws.agent": "boto", + "aws.operation": operation_name, + } + if region_name: + meta["aws.region"] = region_name - # Original func returns a boto.connection.HTTPResponse object - result = original_func(*args, **kwargs) - span.set_attribute("http.status_code", getattr(result, "status")) - span.set_attribute("http.method", getattr(result, "_method")) + for key, value in meta.items(): + span.set_attribute(key, value) + + span.set_attribute( + "http.status_code", getattr(result, "status") + ) + span.set_attribute("http.method", getattr(result, "_method")) return result diff --git a/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py b/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py index 0a4a4b8869..1a8cc2b387 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py @@ -13,6 +13,7 @@ # limitations under the License. from unittest import skipUnless +from unittest.mock import Mock, patch import boto.awslambda import boto.ec2 @@ -80,6 +81,23 @@ def test_ec2_client(self): self.assertEqual(span.attributes["aws.region"], "us-west-2") self.assertEqual(span.name, "ec2.command") + @mock_ec2_deprecated + def test_not_recording(self): + mock_tracer = Mock() + mock_span = Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = mock_span + with patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + ec2 = boto.ec2.connect_to_region("us-west-2") + ec2.get_all_instances() + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + @mock_ec2_deprecated def test_analytics_enabled_with_rate(self): ec2 = boto.ec2.connect_to_region("us-west-2") diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py index d716c90d68..b574b86cfb 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py @@ -97,7 +97,7 @@ def _patched_api_call(self, original_func, instance, args, kwargs): ) as span: operation = None - if args: + if args and span.is_recording(): operation = args[0] span.resource = Resource( attributes={ @@ -119,25 +119,28 @@ def _patched_api_call(self, original_func, instance, args, kwargs): {"params", "path", "verb"}, ) - region_name = deep_getattr(instance, "meta.region_name") + if span.is_recording(): + region_name = deep_getattr(instance, "meta.region_name") - meta = { - "aws.agent": "botocore", - "aws.operation": operation, - "aws.region": region_name, - } - for key, value in meta.items(): - span.set_attribute(key, value) + meta = { + "aws.agent": "botocore", + "aws.operation": operation, + "aws.region": region_name, + } + for key, value in meta.items(): + span.set_attribute(key, value) result = original_func(*args, **kwargs) - span.set_attribute( - "http.status_code", - result["ResponseMetadata"]["HTTPStatusCode"], - ) - span.set_attribute( - "retry_attempts", result["ResponseMetadata"]["RetryAttempts"], - ) + if span.is_recording(): + span.set_attribute( + "http.status_code", + result["ResponseMetadata"]["HTTPStatusCode"], + ) + span.set_attribute( + "retry_attempts", + result["ResponseMetadata"]["RetryAttempts"], + ) return result @@ -177,6 +180,9 @@ def flatten_dict(dict_, sep=".", prefix=""): else {prefix: dict_} ) + if not span.is_recording(): + return + if endpoint_name not in {"kms", "sts"}: tags = dict( (name, value) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py index ea655c8830..9bf691f657 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py @@ -1,3 +1,19 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest.mock import Mock, patch + import botocore.session from botocore.exceptions import ParamValidationError from moto import ( # pylint: disable=import-error @@ -61,6 +77,23 @@ def test_traced_client(self): ) self.assertEqual(span.name, "ec2.command") + @mock_ec2 + def test_not_recording(self): + mock_tracer = Mock() + mock_span = Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = mock_span + with patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + ec2 = self.session.create_client("ec2", region_name="us-west-2") + ec2.describe_instances() + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + @mock_ec2 def test_traced_client_analytics(self): ec2 = self.session.create_client("ec2", region_name="us-west-2") diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index f468053168..07e3eb710b 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -101,21 +101,23 @@ def process_request(self, request): tracer = get_tracer(__name__, __version__) - attributes = collect_request_attributes(environ) - for attr in self._traced_request_attrs: - value = getattr(request, attr, None) - if value is not None: - attributes[attr] = str(value) - span = tracer.start_span( self._get_span_name(request), kind=SpanKind.SERVER, - attributes=attributes, start_time=environ.get( "opentelemetry-instrumentor-django.starttime_key" ), ) + if span.is_recording(): + attributes = collect_request_attributes(environ) + for attr in self._traced_request_attrs: + value = getattr(request, attr, None) + if value is not None: + attributes[attr] = str(value) + for key, value in attributes.items(): + span.set_attribute(key, value) + activation = tracer.use_span(span, end_on_exit=True) activation.__enter__() diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 378139d1c5..7e9ab72b52 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -13,7 +13,7 @@ # limitations under the License. from sys import modules -from unittest.mock import patch +from unittest.mock import Mock, patch from django import VERSION from django.conf import settings @@ -89,6 +89,21 @@ def test_traced_get(self): self.assertEqual(span.attributes["http.status_code"], 200) self.assertEqual(span.attributes["http.status_text"], "OK") + def test_not_recording(self): + mock_tracer = Mock() + mock_span = Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = mock_span + with patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + Client().get("/traced/") + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_traced_post(self): Client().post("/traced/") diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py index d1936b1cd3..90082dd850 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py @@ -119,17 +119,20 @@ def _before_request(): tracer = trace.get_tracer(__name__, __version__) - attributes = otel_wsgi.collect_request_attributes(environ) - if flask.request.url_rule: - # For 404 that result from no route found, etc, we - # don't have a url_rule. - attributes["http.route"] = flask.request.url_rule.rule span = tracer.start_span( span_name, kind=trace.SpanKind.SERVER, - attributes=attributes, start_time=environ.get(_ENVIRON_STARTTIME_KEY), ) + if span.is_recording(): + attributes = otel_wsgi.collect_request_attributes(environ) + if flask.request.url_rule: + # For 404 that result from no route found, etc, we + # don't have a url_rule. + attributes["http.route"] = flask.request.url_rule.rule + for key, value in attributes.items(): + span.set_attribute(key, value) + activation = tracer.use_span(span, end_on_exit=True) activation.__enter__() environ[_ENVIRON_ACTIVATION_KEY] = activation diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py index 181f4e1e61..46861bd681 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest.mock import patch +from unittest.mock import Mock, patch from flask import Flask, request @@ -106,6 +106,21 @@ def test_simple(self): self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) + def test_not_recording(self): + mock_tracer = Mock() + mock_span = Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = mock_span + with patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + self.client.get("/hello/123") + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_404(self): expected_attrs = expected_attributes( { diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py index 1fe8f58a2c..12330c0919 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py @@ -38,15 +38,17 @@ def fetch_async(tracer, func, _, args, kwargs): request = args[0] span = tracer.start_span( - request.method, - kind=trace.SpanKind.CLIENT, - attributes={ + request.method, kind=trace.SpanKind.CLIENT, start_time=start_time, + ) + + if span.is_recording(): + attributes = { "component": "tornado", "http.url": request.url, "http.method": request.method, - }, - start_time=start_time, - ) + } + for key, value in attributes.items(): + span.set_attribute(key, value) with tracer.use_span(span): propagators.inject(type(request.headers).__setitem__, request.headers) @@ -61,7 +63,7 @@ def _finish_tracing_callback(future, span): status_code = None description = None exc = future.exception() - if exc: + if span.is_recording() and exc: if isinstance(exc, HTTPError): status_code = exc.code description = "{}: {}".format(type(exc).__name__, exc) diff --git a/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py b/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py index 2d5ffd00f7..d900b5d360 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py @@ -13,7 +13,7 @@ # limitations under the License. -from unittest.mock import patch +from unittest.mock import Mock, patch from tornado.testing import AsyncHTTPTestCase @@ -132,6 +132,21 @@ def _test_http_method_call(self, method): self.memory_exporter.clear() + def test_not_recording(self): + mock_tracer = Mock() + mock_span = Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = mock_span + with patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + self.fetch("/") + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_async_handler(self): self._test_async_handler("/async", "AsyncHandler") From e50e3a8ea4f99b907ec304e0c2d1f2a74a64465a Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 30 Sep 2020 07:50:42 -0700 Subject: [PATCH 0585/1517] Add support for OTLP v0.5.0 (#1143) --- .../opentelemetry-exporter-otlp/CHANGELOG.md | 3 + .../otlp/metrics_exporter/__init__.py | 170 ++-- .../exporter/otlp/trace_exporter/__init__.py | 3 +- .../tests/test_otlp_metric_exporter.py | 53 +- .../tests/test_otlp_trace_exporter.py | 19 +- .../collector/logs/v1/logs_service_pb2.py | 128 +++ .../collector/logs/v1/logs_service_pb2.pyi | 72 ++ .../logs/v1/logs_service_pb2_grpc.py | 75 ++ .../opentelemetry/proto/logs/v1/logs_pb2.py | 667 ++++++++++++++++ .../opentelemetry/proto/logs/v1/logs_pb2.pyi | 324 ++++++++ .../metrics/experimental/configservice_pb2.py | 267 +++++++ .../experimental/configservice_pb2.pyi | 132 ++++ .../experimental/configservice_pb2_grpc.py | 82 ++ .../proto/metrics/v1/metrics_pb2.py | 747 ++++++++++++------ .../proto/metrics/v1/metrics_pb2.pyi | 396 ++++++---- .../opentelemetry/proto/trace/v1/trace_pb2.py | 60 +- .../proto/trace/v1/trace_pb2.pyi | 88 +-- scripts/proto_codegen.sh | 2 +- 18 files changed, 2700 insertions(+), 588 deletions(-) create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi create mode 100644 opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi create mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi create mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2_grpc.py diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index a3686e1a10..8070b1b118 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Update OpenTelemetry protos to v0.5.0 + ([#1143](https://github.com/open-telemetry/opentelemetry-python/pull/1143)) + ## Version 0.13b0 Released 2020-09-17 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py index 033de0d6dd..08a47c601e 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -15,14 +15,13 @@ """OTLP Metrics Exporter""" import logging -from typing import List, Sequence, Type, TypeVar, Union +from typing import List, Sequence, Type, TypeVar # pylint: disable=duplicate-code from opentelemetry.exporter.otlp.exporter import ( OTLPExporterMixin, _get_resource_data, ) -from opentelemetry.metrics import InstrumentT from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, ) @@ -31,17 +30,17 @@ ) from opentelemetry.proto.common.v1.common_pb2 import StringKeyValue from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + AggregationTemporality, DoubleDataPoint, + DoubleGauge, + DoubleSum, InstrumentationLibraryMetrics, - Int64DataPoint, -) -from opentelemetry.proto.metrics.v1.metrics_pb2 import ( - Metric as CollectorMetric, -) -from opentelemetry.proto.metrics.v1.metrics_pb2 import ( - MetricDescriptor, - ResourceMetrics, + IntDataPoint, + IntGauge, + IntSum, ) +from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric as OTLPMetric +from opentelemetry.proto.metrics.v1.metrics_pb2 import ResourceMetrics from opentelemetry.sdk.metrics import ( Counter, SumObserver, @@ -57,7 +56,7 @@ ) logger = logging.getLogger(__name__) -DataPointT = TypeVar("DataPointT", Int64DataPoint, DoubleDataPoint) +DataPointT = TypeVar("DataPointT", IntDataPoint, DoubleDataPoint) def _get_data_points( @@ -93,45 +92,6 @@ def _get_data_points( return data_points -def _get_temporality( - instrument: InstrumentT, -) -> "MetricDescriptor.TemporalityValue": - # pylint: disable=no-member - if isinstance(instrument, (Counter, UpDownCounter)): - temporality = MetricDescriptor.Temporality.DELTA - elif isinstance(instrument, (ValueRecorder, ValueObserver)): - temporality = MetricDescriptor.Temporality.INSTANTANEOUS - elif isinstance(instrument, (SumObserver, UpDownSumObserver)): - temporality = MetricDescriptor.Temporality.CUMULATIVE - else: - raise Exception( - "No temporality defined for instrument type {}".format( - type(instrument) - ) - ) - - return temporality - - -def _get_type(value_type: Union[int, float]) -> "MetricDescriptor.TypeValue": - # pylint: disable=no-member - if value_type is int: # type: ignore[comparison-overlap] - type_ = MetricDescriptor.Type.INT64 - - elif value_type is float: # type: ignore[comparison-overlap] - type_ = MetricDescriptor.Type.DOUBLE - - # FIXME What are the types that correspond with - # MetricDescriptor.Type.HISTOGRAM and - # MetricDescriptor.Type.SUMMARY? - else: - raise Exception( - "No type defined for valie type {}".format(type(value_type)) - ) - - return type_ - - class OTLPMetricsExporter( MetricsExporter, OTLPExporterMixin[ @@ -150,6 +110,7 @@ class OTLPMetricsExporter( _stub = MetricsServiceStub _result = MetricsExportResult + # pylint: disable=no-self-use def _translate_data( self, data: Sequence[MetricRecord] ) -> ExportMetricsServiceRequest: @@ -158,6 +119,22 @@ def _translate_data( sdk_resource_instrumentation_library_metrics = {} + # The criteria to decide how to translate data is based on this table + # taken directly from OpenTelemetry Proto v0.5.0: + + # TODO: Update table after the decision on: + # https://github.com/open-telemetry/opentelemetry-specification/issues/731. + # By default, metrics recording using the OpenTelemetry API are exported as + # (the table does not include MeasurementValueType to avoid extra rows): + # + # Instrument Type + # ---------------------------------------------- + # Counter Sum(aggregation_temporality=delta;is_monotonic=true) + # UpDownCounter Sum(aggregation_temporality=delta;is_monotonic=false) + # ValueRecorder TBD + # SumObserver Sum(aggregation_temporality=cumulative;is_monotonic=true) + # UpDownSumObserver Sum(aggregation_temporality=cumulative;is_monotonic=false) + # ValueObserver Gauge() for sdk_metric in data: if sdk_metric.instrument.meter.resource not in ( @@ -167,37 +144,90 @@ def _translate_data( sdk_metric.instrument.meter.resource ] = InstrumentationLibraryMetrics() - self._metric_descriptor_kwargs = {} + type_class = { + int: { + "sum": {"class": IntSum, "argument": "int_sum"}, + "gauge": {"class": IntGauge, "argument": "int_gauge"}, + "data_point_class": IntDataPoint, + }, + float: { + "sum": {"class": DoubleSum, "argument": "double_sum"}, + "gauge": { + "class": DoubleGauge, + "argument": "double_gauge", + }, + "data_point_class": DoubleDataPoint, + }, + } + + value_type = sdk_metric.instrument.value_type + + sum_class = type_class[value_type]["sum"]["class"] + gauge_class = type_class[value_type]["gauge"]["class"] + data_point_class = type_class[value_type]["data_point_class"] + + if isinstance(sdk_metric.instrument, Counter): + otlp_metric_data = sum_class( + data_points=_get_data_points(sdk_metric, data_point_class), + aggregation_temporality=( + AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA + ), + is_monotonic=True, + ) + argument = type_class[value_type]["sum"]["argument"] - metric_descriptor = MetricDescriptor( - name=sdk_metric.instrument.name, - description=sdk_metric.instrument.description, - unit=sdk_metric.instrument.unit, - type=_get_type(sdk_metric.instrument.value_type), - temporality=_get_temporality(sdk_metric.instrument), - ) + elif isinstance(sdk_metric.instrument, UpDownCounter): + otlp_metric_data = sum_class( + data_points=_get_data_points(sdk_metric, data_point_class), + aggregation_temporality=( + AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA + ), + is_monotonic=False, + ) + argument = type_class[value_type]["sum"]["argument"] - if metric_descriptor.type == MetricDescriptor.Type.INT64: + elif isinstance(sdk_metric.instrument, (ValueRecorder)): + logger.warning("Skipping exporting of ValueRecorder metric") + continue - collector_metric = CollectorMetric( - metric_descriptor=metric_descriptor, - int64_data_points=_get_data_points( - sdk_metric, Int64DataPoint + elif isinstance(sdk_metric.instrument, SumObserver): + otlp_metric_data = sum_class( + data_points=_get_data_points(sdk_metric, data_point_class), + aggregation_temporality=( + AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE ), + is_monotonic=True, ) + argument = type_class[value_type]["sum"]["argument"] - elif metric_descriptor.type == MetricDescriptor.Type.DOUBLE: - - collector_metric = CollectorMetric( - metric_descriptor=metric_descriptor, - double_data_points=_get_data_points( - sdk_metric, DoubleDataPoint + elif isinstance(sdk_metric.instrument, UpDownSumObserver): + otlp_metric_data = sum_class( + data_points=_get_data_points(sdk_metric, data_point_class), + aggregation_temporality=( + AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE ), + is_monotonic=False, + ) + argument = type_class[value_type]["sum"]["argument"] + + elif isinstance(sdk_metric.instrument, (ValueObserver)): + otlp_metric_data = gauge_class( + data_points=_get_data_points(sdk_metric, data_point_class) ) + argument = type_class[value_type]["gauge"]["argument"] sdk_resource_instrumentation_library_metrics[ sdk_metric.instrument.meter.resource - ].metrics.append(collector_metric) + ].metrics.append( + OTLPMetric( + **{ + "name": sdk_metric.instrument.name, + "description": sdk_metric.instrument.description, + "unit": sdk_metric.instrument.unit, + argument: otlp_metric_data, + } + ) + ) return ExportMetricsServiceRequest( resource_metrics=_get_resource_data( diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index c08d6049e1..e518716d39 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -208,7 +208,8 @@ def _translate_data( self._translate_status(sdk_span) self._collector_span_kwargs["kind"] = getattr( - CollectorSpan.SpanKind, sdk_span.kind.name + CollectorSpan.SpanKind, + "SPAN_KIND_{}".format(sdk_span.kind.name), ) sdk_resource_instrumentation_library_spans[ diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index 20fecd44a2..1218fbbb33 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -25,18 +25,15 @@ StringKeyValue, ) from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + AggregationTemporality, InstrumentationLibraryMetrics, - Int64DataPoint, -) -from opentelemetry.proto.metrics.v1.metrics_pb2 import ( - Metric as CollectorMetric, -) -from opentelemetry.proto.metrics.v1.metrics_pb2 import ( - MetricDescriptor, - ResourceMetrics, + IntDataPoint, + IntSum, ) +from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric as OTLPMetric +from opentelemetry.proto.metrics.v1.metrics_pb2 import ResourceMetrics from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as CollectorResource, + Resource as OTLPResource, ) from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import MetricRecord @@ -71,7 +68,7 @@ def test_translate_metrics(self): expected = ExportMetricsServiceRequest( resource_metrics=[ ResourceMetrics( - resource=CollectorResource( + resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=1)), KeyValue( @@ -82,26 +79,26 @@ def test_translate_metrics(self): instrumentation_library_metrics=[ InstrumentationLibraryMetrics( metrics=[ - CollectorMetric( - metric_descriptor=MetricDescriptor( - name="a", - description="b", - unit="c", - type=MetricDescriptor.Type.INT64, - temporality=( - MetricDescriptor.Temporality.DELTA + OTLPMetric( + name="a", + description="b", + unit="c", + int_sum=IntSum( + data_points=[ + IntDataPoint( + labels=[ + StringKeyValue( + key="a", value="b" + ) + ], + value=1, + ) + ], + aggregation_temporality=( + AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA ), + is_monotonic=True, ), - int64_data_points=[ - Int64DataPoint( - labels=[ - StringKeyValue( - key="a", value="b" - ) - ], - value=1, - ) - ], ) ] ) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index 06a3877b92..dcc0239798 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -36,13 +36,13 @@ KeyValue, ) from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as CollectorResource, + Resource as OTLPResource, ) from opentelemetry.proto.trace.v1.trace_pb2 import ( InstrumentationLibrarySpans, ResourceSpans, ) -from opentelemetry.proto.trace.v1.trace_pb2 import Span as CollectorSpan +from opentelemetry.proto.trace.v1.trace_pb2 import Span as OTLPSpan from opentelemetry.proto.trace.v1.trace_pb2 import Status from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.trace import Span, TracerProvider @@ -51,7 +51,6 @@ SpanExportResult, ) from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -from opentelemetry.trace import SpanKind class TraceServiceServicerUNAVAILABLEDelay(TraceServiceServicer): @@ -143,7 +142,7 @@ def setUp(self): "context.trace_id": 1, "context.span_id": 2, "attributes": OrderedDict([("a", 1), ("b", False)]), - "kind": SpanKind.INTERNAL, + "kind": OTLPSpan.SpanKind.SPAN_KIND_INTERNAL, # pylint: disable=no-member } ) ], @@ -207,7 +206,7 @@ def test_translate_spans(self): expected = ExportTraceServiceRequest( resource_spans=[ ResourceSpans( - resource=CollectorResource( + resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=1)), KeyValue( @@ -221,7 +220,7 @@ def test_translate_spans(self): name="name", version="version" ), spans=[ - CollectorSpan( + OTLPSpan( # pylint: disable=no-member name="a", start_time_unix_nano=self.span.start_time, @@ -238,7 +237,9 @@ def test_translate_spans(self): parent_span_id=( b"\000\000\000\000\000\00009" ), - kind=CollectorSpan.SpanKind.INTERNAL, + kind=( + OTLPSpan.SpanKind.SPAN_KIND_INTERNAL + ), attributes=[ KeyValue( key="a", @@ -250,7 +251,7 @@ def test_translate_spans(self): ), ], events=[ - CollectorSpan.Event( + OTLPSpan.Event( name="a", time_unix_nano=1591240820506462784, attributes=[ @@ -271,7 +272,7 @@ def test_translate_spans(self): ], status=Status(code=0, message=""), links=[ - CollectorSpan.Link( + OTLPSpan.Link( trace_id=int.to_bytes( 1, 16, "big" ), diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py new file mode 100644 index 0000000000..ade8e516c9 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/collector/logs/v1/logs_service.proto + +from google.protobuf import descriptor as _descriptor +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() + + +from opentelemetry.proto.logs.v1 import logs_pb2 as opentelemetry_dot_proto_dot_logs_dot_v1_dot_logs__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='opentelemetry/proto/collector/logs/v1/logs_service.proto', + package='opentelemetry.proto.collector.logs.v1', + syntax='proto3', + serialized_options=b'\n(io.opentelemetry.proto.collector.logs.v1B\020LogsServiceProtoP\001ZFgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/logs/v1', + serialized_pb=b'\n8opentelemetry/proto/collector/logs/v1/logs_service.proto\x12%opentelemetry.proto.collector.logs.v1\x1a&opentelemetry/proto/logs/v1/logs.proto\"\\\n\x18\x45xportLogsServiceRequest\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\x1b\n\x19\x45xportLogsServiceResponse2\x9d\x01\n\x0bLogsService\x12\x8d\x01\n\x06\x45xport\x12?.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest\x1a@.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse\"\x00\x42\x86\x01\n(io.opentelemetry.proto.collector.logs.v1B\x10LogsServiceProtoP\x01ZFgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/logs/v1b\x06proto3' + , + dependencies=[opentelemetry_dot_proto_dot_logs_dot_v1_dot_logs__pb2.DESCRIPTOR,]) + + + + +_EXPORTLOGSSERVICEREQUEST = _descriptor.Descriptor( + name='ExportLogsServiceRequest', + full_name='opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='resource_logs', full_name='opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest.resource_logs', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=139, + serialized_end=231, +) + + +_EXPORTLOGSSERVICERESPONSE = _descriptor.Descriptor( + name='ExportLogsServiceResponse', + full_name='opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=233, + serialized_end=260, +) + +_EXPORTLOGSSERVICEREQUEST.fields_by_name['resource_logs'].message_type = opentelemetry_dot_proto_dot_logs_dot_v1_dot_logs__pb2._RESOURCELOGS +DESCRIPTOR.message_types_by_name['ExportLogsServiceRequest'] = _EXPORTLOGSSERVICEREQUEST +DESCRIPTOR.message_types_by_name['ExportLogsServiceResponse'] = _EXPORTLOGSSERVICERESPONSE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +ExportLogsServiceRequest = _reflection.GeneratedProtocolMessageType('ExportLogsServiceRequest', (_message.Message,), { + 'DESCRIPTOR' : _EXPORTLOGSSERVICEREQUEST, + '__module__' : 'opentelemetry.proto.collector.logs.v1.logs_service_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest) + }) +_sym_db.RegisterMessage(ExportLogsServiceRequest) + +ExportLogsServiceResponse = _reflection.GeneratedProtocolMessageType('ExportLogsServiceResponse', (_message.Message,), { + 'DESCRIPTOR' : _EXPORTLOGSSERVICERESPONSE, + '__module__' : 'opentelemetry.proto.collector.logs.v1.logs_service_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse) + }) +_sym_db.RegisterMessage(ExportLogsServiceResponse) + + +DESCRIPTOR._options = None + +_LOGSSERVICE = _descriptor.ServiceDescriptor( + name='LogsService', + full_name='opentelemetry.proto.collector.logs.v1.LogsService', + file=DESCRIPTOR, + index=0, + serialized_options=None, + serialized_start=263, + serialized_end=420, + methods=[ + _descriptor.MethodDescriptor( + name='Export', + full_name='opentelemetry.proto.collector.logs.v1.LogsService.Export', + index=0, + containing_service=None, + input_type=_EXPORTLOGSSERVICEREQUEST, + output_type=_EXPORTLOGSSERVICERESPONSE, + serialized_options=None, + ), +]) +_sym_db.RegisterServiceDescriptor(_LOGSSERVICE) + +DESCRIPTOR.services_by_name['LogsService'] = _LOGSSERVICE + +# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi new file mode 100644 index 0000000000..a2082a132f --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi @@ -0,0 +1,72 @@ +# @generated by generate_proto_mypy_stubs.py. Do not edit! +import sys +from google.protobuf.descriptor import ( + Descriptor as google___protobuf___descriptor___Descriptor, + FileDescriptor as google___protobuf___descriptor___FileDescriptor, +) + +from google.protobuf.internal.containers import ( + RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, +) + +from google.protobuf.message import ( + Message as google___protobuf___message___Message, +) + +from opentelemetry.proto.logs.v1.logs_pb2 import ( + ResourceLogs as opentelemetry___proto___logs___v1___logs_pb2___ResourceLogs, +) + +from typing import ( + Iterable as typing___Iterable, + Optional as typing___Optional, + Union as typing___Union, +) + +from typing_extensions import ( + Literal as typing_extensions___Literal, +) + + +builtin___bool = bool +builtin___bytes = bytes +builtin___float = float +builtin___int = int +if sys.version_info < (3,): + builtin___buffer = buffer + builtin___unicode = unicode + + +DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... + +class ExportLogsServiceRequest(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + @property + def resource_logs(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___logs___v1___logs_pb2___ResourceLogs]: ... + + def __init__(self, + *, + resource_logs : typing___Optional[typing___Iterable[opentelemetry___proto___logs___v1___logs_pb2___ResourceLogs]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> ExportLogsServiceRequest: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ExportLogsServiceRequest: ... + def ClearField(self, field_name: typing_extensions___Literal[u"resource_logs",b"resource_logs"]) -> None: ... +type___ExportLogsServiceRequest = ExportLogsServiceRequest + +class ExportLogsServiceResponse(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + def __init__(self, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> ExportLogsServiceResponse: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ExportLogsServiceResponse: ... +type___ExportLogsServiceResponse = ExportLogsServiceResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py new file mode 100644 index 0000000000..c929cdc84c --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py @@ -0,0 +1,75 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +import grpc + +from opentelemetry.proto.collector.logs.v1 import logs_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2 + + +class LogsServiceStub(object): + """Service that can be used to push logs between one Application instrumented with + OpenTelemetry and an collector, or between an collector and a central collector (in this + case logs are sent/received to/from multiple Applications). + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Export = channel.unary_unary( + '/opentelemetry.proto.collector.logs.v1.LogsService/Export', + request_serializer=opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceRequest.SerializeToString, + response_deserializer=opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceResponse.FromString, + ) + + +class LogsServiceServicer(object): + """Service that can be used to push logs between one Application instrumented with + OpenTelemetry and an collector, or between an collector and a central collector (in this + case logs are sent/received to/from multiple Applications). + """ + + def Export(self, request, context): + """For performance reasons, it is recommended to keep this RPC + alive for the entire life of the application. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_LogsServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Export': grpc.unary_unary_rpc_method_handler( + servicer.Export, + request_deserializer=opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceRequest.FromString, + response_serializer=opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'opentelemetry.proto.collector.logs.v1.LogsService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class LogsService(object): + """Service that can be used to push logs between one Application instrumented with + OpenTelemetry and an collector, or between an collector and a central collector (in this + case logs are sent/received to/from multiple Applications). + """ + + @staticmethod + def Export(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/opentelemetry.proto.collector.logs.v1.LogsService/Export', + opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceRequest.SerializeToString, + opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceResponse.FromString, + options, channel_credentials, + call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py new file mode 100644 index 0000000000..184f6ae8b2 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py @@ -0,0 +1,667 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/logs/v1/logs.proto + +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +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() + + +from opentelemetry.proto.common.v1 import ( + common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2, +) +from opentelemetry.proto.resource.v1 import ( + resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2, +) + + +DESCRIPTOR = _descriptor.FileDescriptor( + name="opentelemetry/proto/logs/v1/logs.proto", + package="opentelemetry.proto.logs.v1", + syntax="proto3", + serialized_options=b"\n\036io.opentelemetry.proto.logs.v1B\tLogsProtoP\001Z builtin___str: ... + @classmethod + def Value(cls, name: builtin___str) -> SeverityNumberValue: ... + @classmethod + def keys(cls) -> typing___List[builtin___str]: ... + @classmethod + def values(cls) -> typing___List[SeverityNumberValue]: ... + @classmethod + def items( + cls, + ) -> typing___List[typing___Tuple[builtin___str, SeverityNumberValue]]: ... + SEVERITY_NUMBER_UNSPECIFIED = typing___cast(SeverityNumberValue, 0) + SEVERITY_NUMBER_TRACE = typing___cast(SeverityNumberValue, 1) + SEVERITY_NUMBER_TRACE2 = typing___cast(SeverityNumberValue, 2) + SEVERITY_NUMBER_TRACE3 = typing___cast(SeverityNumberValue, 3) + SEVERITY_NUMBER_TRACE4 = typing___cast(SeverityNumberValue, 4) + SEVERITY_NUMBER_DEBUG = typing___cast(SeverityNumberValue, 5) + SEVERITY_NUMBER_DEBUG2 = typing___cast(SeverityNumberValue, 6) + SEVERITY_NUMBER_DEBUG3 = typing___cast(SeverityNumberValue, 7) + SEVERITY_NUMBER_DEBUG4 = typing___cast(SeverityNumberValue, 8) + SEVERITY_NUMBER_INFO = typing___cast(SeverityNumberValue, 9) + SEVERITY_NUMBER_INFO2 = typing___cast(SeverityNumberValue, 10) + SEVERITY_NUMBER_INFO3 = typing___cast(SeverityNumberValue, 11) + SEVERITY_NUMBER_INFO4 = typing___cast(SeverityNumberValue, 12) + SEVERITY_NUMBER_WARN = typing___cast(SeverityNumberValue, 13) + SEVERITY_NUMBER_WARN2 = typing___cast(SeverityNumberValue, 14) + SEVERITY_NUMBER_WARN3 = typing___cast(SeverityNumberValue, 15) + SEVERITY_NUMBER_WARN4 = typing___cast(SeverityNumberValue, 16) + SEVERITY_NUMBER_ERROR = typing___cast(SeverityNumberValue, 17) + SEVERITY_NUMBER_ERROR2 = typing___cast(SeverityNumberValue, 18) + SEVERITY_NUMBER_ERROR3 = typing___cast(SeverityNumberValue, 19) + SEVERITY_NUMBER_ERROR4 = typing___cast(SeverityNumberValue, 20) + SEVERITY_NUMBER_FATAL = typing___cast(SeverityNumberValue, 21) + SEVERITY_NUMBER_FATAL2 = typing___cast(SeverityNumberValue, 22) + SEVERITY_NUMBER_FATAL3 = typing___cast(SeverityNumberValue, 23) + SEVERITY_NUMBER_FATAL4 = typing___cast(SeverityNumberValue, 24) + +SEVERITY_NUMBER_UNSPECIFIED = typing___cast(SeverityNumberValue, 0) +SEVERITY_NUMBER_TRACE = typing___cast(SeverityNumberValue, 1) +SEVERITY_NUMBER_TRACE2 = typing___cast(SeverityNumberValue, 2) +SEVERITY_NUMBER_TRACE3 = typing___cast(SeverityNumberValue, 3) +SEVERITY_NUMBER_TRACE4 = typing___cast(SeverityNumberValue, 4) +SEVERITY_NUMBER_DEBUG = typing___cast(SeverityNumberValue, 5) +SEVERITY_NUMBER_DEBUG2 = typing___cast(SeverityNumberValue, 6) +SEVERITY_NUMBER_DEBUG3 = typing___cast(SeverityNumberValue, 7) +SEVERITY_NUMBER_DEBUG4 = typing___cast(SeverityNumberValue, 8) +SEVERITY_NUMBER_INFO = typing___cast(SeverityNumberValue, 9) +SEVERITY_NUMBER_INFO2 = typing___cast(SeverityNumberValue, 10) +SEVERITY_NUMBER_INFO3 = typing___cast(SeverityNumberValue, 11) +SEVERITY_NUMBER_INFO4 = typing___cast(SeverityNumberValue, 12) +SEVERITY_NUMBER_WARN = typing___cast(SeverityNumberValue, 13) +SEVERITY_NUMBER_WARN2 = typing___cast(SeverityNumberValue, 14) +SEVERITY_NUMBER_WARN3 = typing___cast(SeverityNumberValue, 15) +SEVERITY_NUMBER_WARN4 = typing___cast(SeverityNumberValue, 16) +SEVERITY_NUMBER_ERROR = typing___cast(SeverityNumberValue, 17) +SEVERITY_NUMBER_ERROR2 = typing___cast(SeverityNumberValue, 18) +SEVERITY_NUMBER_ERROR3 = typing___cast(SeverityNumberValue, 19) +SEVERITY_NUMBER_ERROR4 = typing___cast(SeverityNumberValue, 20) +SEVERITY_NUMBER_FATAL = typing___cast(SeverityNumberValue, 21) +SEVERITY_NUMBER_FATAL2 = typing___cast(SeverityNumberValue, 22) +SEVERITY_NUMBER_FATAL3 = typing___cast(SeverityNumberValue, 23) +SEVERITY_NUMBER_FATAL4 = typing___cast(SeverityNumberValue, 24) +type___SeverityNumber = SeverityNumber + +LogRecordFlagsValue = typing___NewType("LogRecordFlagsValue", builtin___int) +type___LogRecordFlagsValue = LogRecordFlagsValue + +class LogRecordFlags(object): + DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... + @classmethod + def Name(cls, number: builtin___int) -> builtin___str: ... + @classmethod + def Value(cls, name: builtin___str) -> LogRecordFlagsValue: ... + @classmethod + def keys(cls) -> typing___List[builtin___str]: ... + @classmethod + def values(cls) -> typing___List[LogRecordFlagsValue]: ... + @classmethod + def items( + cls, + ) -> typing___List[typing___Tuple[builtin___str, LogRecordFlagsValue]]: ... + LOG_RECORD_FLAG_UNSPECIFIED = typing___cast(LogRecordFlagsValue, 0) + LOG_RECORD_FLAG_TRACE_FLAGS_MASK = typing___cast(LogRecordFlagsValue, 255) + +LOG_RECORD_FLAG_UNSPECIFIED = typing___cast(LogRecordFlagsValue, 0) +LOG_RECORD_FLAG_TRACE_FLAGS_MASK = typing___cast(LogRecordFlagsValue, 255) +type___LogRecordFlags = LogRecordFlags + +class ResourceLogs(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + @property + def resource( + self, + ) -> opentelemetry___proto___resource___v1___resource_pb2___Resource: ... + @property + def instrumentation_library_logs( + self, + ) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[ + type___InstrumentationLibraryLogs + ]: ... + def __init__( + self, + *, + resource: typing___Optional[ + opentelemetry___proto___resource___v1___resource_pb2___Resource + ] = None, + instrumentation_library_logs: typing___Optional[ + typing___Iterable[type___InstrumentationLibraryLogs] + ] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> ResourceLogs: ... + else: + @classmethod + def FromString( + cls, + s: typing___Union[ + builtin___bytes, builtin___buffer, builtin___unicode + ], + ) -> ResourceLogs: ... + def HasField( + self, field_name: typing_extensions___Literal["resource", b"resource"] + ) -> builtin___bool: ... + def ClearField( + self, + field_name: typing_extensions___Literal[ + "instrumentation_library_logs", + b"instrumentation_library_logs", + "resource", + b"resource", + ], + ) -> None: ... + +type___ResourceLogs = ResourceLogs + +class InstrumentationLibraryLogs(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + @property + def instrumentation_library( + self, + ) -> opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary: ... + @property + def logs( + self, + ) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[ + type___LogRecord + ]: ... + def __init__( + self, + *, + instrumentation_library: typing___Optional[ + opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary + ] = None, + logs: typing___Optional[typing___Iterable[type___LogRecord]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString( + cls, s: builtin___bytes + ) -> InstrumentationLibraryLogs: ... + else: + @classmethod + def FromString( + cls, + s: typing___Union[ + builtin___bytes, builtin___buffer, builtin___unicode + ], + ) -> InstrumentationLibraryLogs: ... + def HasField( + self, + field_name: typing_extensions___Literal[ + "instrumentation_library", b"instrumentation_library" + ], + ) -> builtin___bool: ... + def ClearField( + self, + field_name: typing_extensions___Literal[ + "instrumentation_library", + b"instrumentation_library", + "logs", + b"logs", + ], + ) -> None: ... + +type___InstrumentationLibraryLogs = InstrumentationLibraryLogs + +class LogRecord(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + time_unix_nano: builtin___int = ... + severity_number: type___SeverityNumberValue = ... + severity_text: typing___Text = ... + name: typing___Text = ... + dropped_attributes_count: builtin___int = ... + flags: builtin___int = ... + trace_id: builtin___bytes = ... + span_id: builtin___bytes = ... + @property + def body( + self, + ) -> opentelemetry___proto___common___v1___common_pb2___AnyValue: ... + @property + def attributes( + self, + ) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[ + opentelemetry___proto___common___v1___common_pb2___KeyValue + ]: ... + def __init__( + self, + *, + time_unix_nano: typing___Optional[builtin___int] = None, + severity_number: typing___Optional[type___SeverityNumberValue] = None, + severity_text: typing___Optional[typing___Text] = None, + name: typing___Optional[typing___Text] = None, + body: typing___Optional[ + opentelemetry___proto___common___v1___common_pb2___AnyValue + ] = None, + attributes: typing___Optional[ + typing___Iterable[ + opentelemetry___proto___common___v1___common_pb2___KeyValue + ] + ] = None, + dropped_attributes_count: typing___Optional[builtin___int] = None, + flags: typing___Optional[builtin___int] = None, + trace_id: typing___Optional[builtin___bytes] = None, + span_id: typing___Optional[builtin___bytes] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> LogRecord: ... + else: + @classmethod + def FromString( + cls, + s: typing___Union[ + builtin___bytes, builtin___buffer, builtin___unicode + ], + ) -> LogRecord: ... + def HasField( + self, field_name: typing_extensions___Literal["body", b"body"] + ) -> builtin___bool: ... + def ClearField( + self, + field_name: typing_extensions___Literal[ + "attributes", + b"attributes", + "body", + b"body", + "dropped_attributes_count", + b"dropped_attributes_count", + "flags", + b"flags", + "name", + b"name", + "severity_number", + b"severity_number", + "severity_text", + b"severity_text", + "span_id", + b"span_id", + "time_unix_nano", + b"time_unix_nano", + "trace_id", + b"trace_id", + ], + ) -> None: ... + +type___LogRecord = LogRecord diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.py new file mode 100644 index 0000000000..c2a8deb607 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/metrics/experimental/configservice.proto + +from google.protobuf import descriptor as _descriptor +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() + + +from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='opentelemetry/proto/metrics/experimental/configservice.proto', + package='opentelemetry.proto.metrics.experimental', + syntax='proto3', + serialized_options=b'\n+io.opentelemetry.proto.metrics.experimentalB\030MetricConfigServiceProtoP\001ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/metrics/experimental', + serialized_pb=b'\n.opentelemetry.proto.metrics.experimental.MetricConfigResponseB\x94\x01\n+io.opentelemetry.proto.metrics.experimentalB\x18MetricConfigServiceProtoP\x01ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/metrics/experimentalb\x06proto3' + , + dependencies=[opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) + + + + +_METRICCONFIGREQUEST = _descriptor.Descriptor( + name='MetricConfigRequest', + full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='resource', full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest.resource', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='last_known_fingerprint', full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest.last_known_fingerprint', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=154, + serialized_end=268, +) + + +_METRICCONFIGRESPONSE_SCHEDULE_PATTERN = _descriptor.Descriptor( + name='Pattern', + full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='equals', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.equals', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='starts_with', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.starts_with', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='match', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.match', + index=0, containing_type=None, fields=[]), + ], + serialized_start=692, + serialized_end=751, +) + +_METRICCONFIGRESPONSE_SCHEDULE = _descriptor.Descriptor( + name='Schedule', + full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='exclusion_patterns', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.exclusion_patterns', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='inclusion_patterns', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.inclusion_patterns', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='period_sec', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.period_sec', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_METRICCONFIGRESPONSE_SCHEDULE_PATTERN, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=442, + serialized_end=751, +) + +_METRICCONFIGRESPONSE = _descriptor.Descriptor( + name='MetricConfigResponse', + full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='fingerprint', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.fingerprint', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='schedules', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.schedules', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='suggested_wait_time_sec', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.suggested_wait_time_sec', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_METRICCONFIGRESPONSE_SCHEDULE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=271, + serialized_end=751, +) + +_METRICCONFIGREQUEST.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE +_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.containing_type = _METRICCONFIGRESPONSE_SCHEDULE +_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'].fields.append( + _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['equals']) +_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['equals'].containing_oneof = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'] +_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'].fields.append( + _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['starts_with']) +_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['starts_with'].containing_oneof = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'] +_METRICCONFIGRESPONSE_SCHEDULE.fields_by_name['exclusion_patterns'].message_type = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN +_METRICCONFIGRESPONSE_SCHEDULE.fields_by_name['inclusion_patterns'].message_type = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN +_METRICCONFIGRESPONSE_SCHEDULE.containing_type = _METRICCONFIGRESPONSE +_METRICCONFIGRESPONSE.fields_by_name['schedules'].message_type = _METRICCONFIGRESPONSE_SCHEDULE +DESCRIPTOR.message_types_by_name['MetricConfigRequest'] = _METRICCONFIGREQUEST +DESCRIPTOR.message_types_by_name['MetricConfigResponse'] = _METRICCONFIGRESPONSE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +MetricConfigRequest = _reflection.GeneratedProtocolMessageType('MetricConfigRequest', (_message.Message,), { + 'DESCRIPTOR' : _METRICCONFIGREQUEST, + '__module__' : 'opentelemetry.proto.metrics.experimental.configservice_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigRequest) + }) +_sym_db.RegisterMessage(MetricConfigRequest) + +MetricConfigResponse = _reflection.GeneratedProtocolMessageType('MetricConfigResponse', (_message.Message,), { + + 'Schedule' : _reflection.GeneratedProtocolMessageType('Schedule', (_message.Message,), { + + 'Pattern' : _reflection.GeneratedProtocolMessageType('Pattern', (_message.Message,), { + 'DESCRIPTOR' : _METRICCONFIGRESPONSE_SCHEDULE_PATTERN, + '__module__' : 'opentelemetry.proto.metrics.experimental.configservice_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern) + }) + , + 'DESCRIPTOR' : _METRICCONFIGRESPONSE_SCHEDULE, + '__module__' : 'opentelemetry.proto.metrics.experimental.configservice_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule) + }) + , + 'DESCRIPTOR' : _METRICCONFIGRESPONSE, + '__module__' : 'opentelemetry.proto.metrics.experimental.configservice_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigResponse) + }) +_sym_db.RegisterMessage(MetricConfigResponse) +_sym_db.RegisterMessage(MetricConfigResponse.Schedule) +_sym_db.RegisterMessage(MetricConfigResponse.Schedule.Pattern) + + +DESCRIPTOR._options = None + +_METRICCONFIG = _descriptor.ServiceDescriptor( + name='MetricConfig', + full_name='opentelemetry.proto.metrics.experimental.MetricConfig', + file=DESCRIPTOR, + index=0, + serialized_options=None, + serialized_start=754, + serialized_end=915, + methods=[ + _descriptor.MethodDescriptor( + name='GetMetricConfig', + full_name='opentelemetry.proto.metrics.experimental.MetricConfig.GetMetricConfig', + index=0, + containing_service=None, + input_type=_METRICCONFIGREQUEST, + output_type=_METRICCONFIGRESPONSE, + serialized_options=None, + ), +]) +_sym_db.RegisterServiceDescriptor(_METRICCONFIG) + +DESCRIPTOR.services_by_name['MetricConfig'] = _METRICCONFIG + +# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi new file mode 100644 index 0000000000..ffe85a7a8b --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi @@ -0,0 +1,132 @@ +# @generated by generate_proto_mypy_stubs.py. Do not edit! +import sys +from google.protobuf.descriptor import ( + Descriptor as google___protobuf___descriptor___Descriptor, + FileDescriptor as google___protobuf___descriptor___FileDescriptor, +) + +from google.protobuf.internal.containers import ( + RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, +) + +from google.protobuf.message import ( + Message as google___protobuf___message___Message, +) + +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as opentelemetry___proto___resource___v1___resource_pb2___Resource, +) + +from typing import ( + Iterable as typing___Iterable, + Optional as typing___Optional, + Text as typing___Text, + Union as typing___Union, +) + +from typing_extensions import ( + Literal as typing_extensions___Literal, +) + + +builtin___bool = bool +builtin___bytes = bytes +builtin___float = float +builtin___int = int +if sys.version_info < (3,): + builtin___buffer = buffer + builtin___unicode = unicode + + +DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... + +class MetricConfigRequest(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + last_known_fingerprint: builtin___bytes = ... + + @property + def resource(self) -> opentelemetry___proto___resource___v1___resource_pb2___Resource: ... + + def __init__(self, + *, + resource : typing___Optional[opentelemetry___proto___resource___v1___resource_pb2___Resource] = None, + last_known_fingerprint : typing___Optional[builtin___bytes] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> MetricConfigRequest: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> MetricConfigRequest: ... + def HasField(self, field_name: typing_extensions___Literal[u"resource",b"resource"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"last_known_fingerprint",b"last_known_fingerprint",u"resource",b"resource"]) -> None: ... +type___MetricConfigRequest = MetricConfigRequest + +class MetricConfigResponse(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + class Schedule(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + class Pattern(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + equals: typing___Text = ... + starts_with: typing___Text = ... + + def __init__(self, + *, + equals : typing___Optional[typing___Text] = None, + starts_with : typing___Optional[typing___Text] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> MetricConfigResponse.Schedule.Pattern: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> MetricConfigResponse.Schedule.Pattern: ... + def HasField(self, field_name: typing_extensions___Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions___Literal[u"match",b"match"]) -> typing_extensions___Literal["equals","starts_with"]: ... + type___Pattern = Pattern + + period_sec: builtin___int = ... + + @property + def exclusion_patterns(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___MetricConfigResponse.Schedule.Pattern]: ... + + @property + def inclusion_patterns(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___MetricConfigResponse.Schedule.Pattern]: ... + + def __init__(self, + *, + exclusion_patterns : typing___Optional[typing___Iterable[type___MetricConfigResponse.Schedule.Pattern]] = None, + inclusion_patterns : typing___Optional[typing___Iterable[type___MetricConfigResponse.Schedule.Pattern]] = None, + period_sec : typing___Optional[builtin___int] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> MetricConfigResponse.Schedule: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> MetricConfigResponse.Schedule: ... + def ClearField(self, field_name: typing_extensions___Literal[u"exclusion_patterns",b"exclusion_patterns",u"inclusion_patterns",b"inclusion_patterns",u"period_sec",b"period_sec"]) -> None: ... + type___Schedule = Schedule + + fingerprint: builtin___bytes = ... + suggested_wait_time_sec: builtin___int = ... + + @property + def schedules(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___MetricConfigResponse.Schedule]: ... + + def __init__(self, + *, + fingerprint : typing___Optional[builtin___bytes] = None, + schedules : typing___Optional[typing___Iterable[type___MetricConfigResponse.Schedule]] = None, + suggested_wait_time_sec : typing___Optional[builtin___int] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> MetricConfigResponse: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> MetricConfigResponse: ... + def ClearField(self, field_name: typing_extensions___Literal[u"fingerprint",b"fingerprint",u"schedules",b"schedules",u"suggested_wait_time_sec",b"suggested_wait_time_sec"]) -> None: ... +type___MetricConfigResponse = MetricConfigResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2_grpc.py new file mode 100644 index 0000000000..a2b2408446 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2_grpc.py @@ -0,0 +1,82 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +import grpc + +from opentelemetry.proto.metrics.experimental import configservice_pb2 as opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2 + + +class MetricConfigStub(object): + """MetricConfig is a service that enables updating metric schedules, trace + parameters, and other configurations on the SDK without having to restart the + instrumented application. The collector can also serve as the configuration + service, acting as a bridge between third-party configuration services and + the SDK, piping updated configs from a third-party source to an instrumented + application. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetMetricConfig = channel.unary_unary( + '/opentelemetry.proto.metrics.experimental.MetricConfig/GetMetricConfig', + request_serializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2.MetricConfigRequest.SerializeToString, + response_deserializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2.MetricConfigResponse.FromString, + ) + + +class MetricConfigServicer(object): + """MetricConfig is a service that enables updating metric schedules, trace + parameters, and other configurations on the SDK without having to restart the + instrumented application. The collector can also serve as the configuration + service, acting as a bridge between third-party configuration services and + the SDK, piping updated configs from a third-party source to an instrumented + application. + """ + + def GetMetricConfig(self, request, context): + """Missing associated documentation comment in .proto file""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_MetricConfigServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetMetricConfig': grpc.unary_unary_rpc_method_handler( + servicer.GetMetricConfig, + request_deserializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2.MetricConfigRequest.FromString, + response_serializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2.MetricConfigResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'opentelemetry.proto.metrics.experimental.MetricConfig', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class MetricConfig(object): + """MetricConfig is a service that enables updating metric schedules, trace + parameters, and other configurations on the SDK without having to restart the + instrumented application. The collector can also serve as the configuration + service, acting as a bridge between third-party configuration services and + the SDK, piping updated configs from a third-party source to an instrumented + application. + """ + + @staticmethod + def GetMetricConfig(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/opentelemetry.proto.metrics.experimental.MetricConfig/GetMetricConfig', + opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2.MetricConfigRequest.SerializeToString, + opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2.MetricConfigResponse.FromString, + options, channel_credentials, + call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py index e4b62369a0..62956103e8 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py @@ -2,6 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/metrics/v1/metrics.proto +from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection @@ -20,83 +21,41 @@ package='opentelemetry.proto.metrics.v1', syntax='proto3', serialized_options=b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1', - serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xb6\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x66\n\x1finstrumentation_library_metrics\x18\x02 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics\"\xb0\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\"\x8f\x03\n\x06Metric\x12K\n\x11metric_descriptor\x18\x01 \x01(\x0b\x32\x30.opentelemetry.proto.metrics.v1.MetricDescriptor\x12I\n\x11int64_data_points\x18\x02 \x03(\x0b\x32..opentelemetry.proto.metrics.v1.Int64DataPoint\x12K\n\x12\x64ouble_data_points\x18\x03 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleDataPoint\x12Q\n\x15histogram_data_points\x18\x04 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12M\n\x13summary_data_points\x18\x05 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\xa9\x03\n\x10MetricDescriptor\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x43\n\x04type\x18\x04 \x01(\x0e\x32\x35.opentelemetry.proto.metrics.v1.MetricDescriptor.Type\x12Q\n\x0btemporality\x18\x05 \x01(\x0e\x32<.opentelemetry.proto.metrics.v1.MetricDescriptor.Temporality\"v\n\x04Type\x12\x10\n\x0cINVALID_TYPE\x10\x00\x12\t\n\x05INT64\x10\x01\x12\x13\n\x0fMONOTONIC_INT64\x10\x02\x12\n\n\x06\x44OUBLE\x10\x03\x12\x14\n\x10MONOTONIC_DOUBLE\x10\x04\x12\r\n\tHISTOGRAM\x10\x05\x12\x0b\n\x07SUMMARY\x10\x06\"T\n\x0bTemporality\x12\x17\n\x13INVALID_TEMPORALITY\x10\x00\x12\x11\n\rINSTANTANEOUS\x10\x01\x12\t\n\x05\x44\x45LTA\x10\x02\x12\x0e\n\nCUMULATIVE\x10\x03\"\x94\x01\n\x0eInt64DataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x03\"\x95\x01\n\x0f\x44oubleDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x01\"\xf1\x03\n\x12HistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x04\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12J\n\x07\x62uckets\x18\x06 \x03(\x0b\x32\x39.opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x1a\xe4\x01\n\x06\x42ucket\x12\r\n\x05\x63ount\x18\x01 \x01(\x04\x12T\n\x08\x65xemplar\x18\x02 \x01(\x0b\x32\x42.opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar\x1au\n\x08\x45xemplar\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x42\n\x0b\x61ttachments\x18\x03 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\"\xba\x02\n\x10SummaryDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x04\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12]\n\x11percentile_values\x18\x06 \x03(\x0b\x32\x42.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile\x1a\x36\n\x11ValueAtPercentile\x12\x12\n\npercentile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01\x42t\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1b\x06proto3' + serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xb6\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x66\n\x1finstrumentation_library_metrics\x18\x02 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics\"\xb0\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\"\xd5\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12=\n\tint_gauge\x18\x04 \x01(\x0b\x32(.opentelemetry.proto.metrics.v1.IntGaugeH\x00\x12\x43\n\x0c\x64ouble_gauge\x18\x05 \x01(\x0b\x32+.opentelemetry.proto.metrics.v1.DoubleGaugeH\x00\x12\x39\n\x07int_sum\x18\x06 \x01(\x0b\x32&.opentelemetry.proto.metrics.v1.IntSumH\x00\x12?\n\ndouble_sum\x18\x07 \x01(\x0b\x32).opentelemetry.proto.metrics.v1.DoubleSumH\x00\x12\x45\n\rint_histogram\x18\x08 \x01(\x0b\x32,.opentelemetry.proto.metrics.v1.IntHistogramH\x00\x12K\n\x10\x64ouble_histogram\x18\t \x01(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleHistogramH\x00\x42\x06\n\x04\x64\x61ta\"M\n\x08IntGauge\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint\"S\n\x0b\x44oubleGauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleDataPoint\"\xba\x01\n\x06IntSum\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xc0\x01\n\tDoubleSum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xb3\x01\n\x0cIntHistogram\x12J\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x35.opentelemetry.proto.metrics.v1.IntHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xb9\x01\n\x0f\x44oubleHistogram\x12M\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x38.opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xd2\x01\n\x0cIntDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x10\x12>\n\texemplars\x18\x05 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar\"\xd8\x01\n\x0f\x44oubleDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x01\x12\x41\n\texemplars\x18\x05 \x03(\x0b\x32..opentelemetry.proto.metrics.v1.DoubleExemplar\"\x98\x02\n\x15IntHistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x10\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12>\n\texemplars\x18\x08 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar\"\x9e\x02\n\x18\x44oubleHistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12\x41\n\texemplars\x18\x08 \x03(\x0b\x32..opentelemetry.proto.metrics.v1.DoubleExemplar\"\x9f\x01\n\x0bIntExemplar\x12\x46\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\r\n\x05value\x18\x03 \x01(\x10\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\"\xa2\x01\n\x0e\x44oubleExemplar\x12\x46\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\r\n\x05value\x18\x03 \x01(\x01\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02\x42t\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) - - -_METRICDESCRIPTOR_TYPE = _descriptor.EnumDescriptor( - name='Type', - full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.Type', +_AGGREGATIONTEMPORALITY = _descriptor.EnumDescriptor( + name='AggregationTemporality', + full_name='opentelemetry.proto.metrics.v1.AggregationTemporality', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( - name='INVALID_TYPE', index=0, number=0, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='INT64', index=1, number=1, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='MONOTONIC_INT64', index=2, number=2, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='DOUBLE', index=3, number=3, + name='AGGREGATION_TEMPORALITY_UNSPECIFIED', index=0, number=0, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='MONOTONIC_DOUBLE', index=4, number=4, + name='AGGREGATION_TEMPORALITY_DELTA', index=1, number=1, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='HISTOGRAM', index=5, number=5, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='SUMMARY', index=6, number=6, + name='AGGREGATION_TEMPORALITY_CUMULATIVE', index=2, number=2, serialized_options=None, type=None), ], containing_type=None, serialized_options=None, - serialized_start=1160, - serialized_end=1278, + serialized_start=3258, + serialized_end=3398, ) -_sym_db.RegisterEnumDescriptor(_METRICDESCRIPTOR_TYPE) +_sym_db.RegisterEnumDescriptor(_AGGREGATIONTEMPORALITY) + +AggregationTemporality = enum_type_wrapper.EnumTypeWrapper(_AGGREGATIONTEMPORALITY) +AGGREGATION_TEMPORALITY_UNSPECIFIED = 0 +AGGREGATION_TEMPORALITY_DELTA = 1 +AGGREGATION_TEMPORALITY_CUMULATIVE = 2 -_METRICDESCRIPTOR_TEMPORALITY = _descriptor.EnumDescriptor( - name='Temporality', - full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.Temporality', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='INVALID_TEMPORALITY', index=0, number=0, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='INSTANTANEOUS', index=1, number=1, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='DELTA', index=2, number=2, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='CUMULATIVE', index=3, number=3, - serialized_options=None, - type=None), - ], - containing_type=None, - serialized_options=None, - serialized_start=1280, - serialized_end=1364, -) -_sym_db.RegisterEnumDescriptor(_METRICDESCRIPTOR_TEMPORALITY) _RESOURCEMETRICS = _descriptor.Descriptor( @@ -183,37 +142,65 @@ containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='metric_descriptor', full_name='opentelemetry.proto.metrics.v1.Metric.metric_descriptor', index=0, - number=1, type=11, cpp_type=10, label=1, + name='name', full_name='opentelemetry.proto.metrics.v1.Metric.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='description', full_name='opentelemetry.proto.metrics.v1.Metric.description', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='unit', full_name='opentelemetry.proto.metrics.v1.Metric.unit', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='int_gauge', full_name='opentelemetry.proto.metrics.v1.Metric.int_gauge', index=3, + number=4, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='int64_data_points', full_name='opentelemetry.proto.metrics.v1.Metric.int64_data_points', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], + name='double_gauge', full_name='opentelemetry.proto.metrics.v1.Metric.double_gauge', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='double_data_points', full_name='opentelemetry.proto.metrics.v1.Metric.double_data_points', index=2, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], + name='int_sum', full_name='opentelemetry.proto.metrics.v1.Metric.int_sum', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='histogram_data_points', full_name='opentelemetry.proto.metrics.v1.Metric.histogram_data_points', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], + name='double_sum', full_name='opentelemetry.proto.metrics.v1.Metric.double_sum', index=6, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='summary_data_points', full_name='opentelemetry.proto.metrics.v1.Metric.summary_data_points', index=4, - number=5, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], + name='int_histogram', full_name='opentelemetry.proto.metrics.v1.Metric.int_histogram', index=7, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='double_histogram', full_name='opentelemetry.proto.metrics.v1.Metric.double_histogram', index=8, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), @@ -228,51 +215,102 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='data', full_name='opentelemetry.proto.metrics.v1.Metric.data', + index=0, containing_type=None, fields=[]), ], serialized_start=537, - serialized_end=936, + serialized_end=1006, ) -_METRICDESCRIPTOR = _descriptor.Descriptor( - name='MetricDescriptor', - full_name='opentelemetry.proto.metrics.v1.MetricDescriptor', +_INTGAUGE = _descriptor.Descriptor( + name='IntGauge', + full_name='opentelemetry.proto.metrics.v1.IntGauge', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='name', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), + name='data_points', full_name='opentelemetry.proto.metrics.v1.IntGauge.data_points', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1008, + serialized_end=1085, +) + + +_DOUBLEGAUGE = _descriptor.Descriptor( + name='DoubleGauge', + full_name='opentelemetry.proto.metrics.v1.DoubleGauge', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ _descriptor.FieldDescriptor( - name='description', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.description', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), + name='data_points', full_name='opentelemetry.proto.metrics.v1.DoubleGauge.data_points', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1087, + serialized_end=1170, +) + + +_INTSUM = _descriptor.Descriptor( + name='IntSum', + full_name='opentelemetry.proto.metrics.v1.IntSum', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ _descriptor.FieldDescriptor( - name='unit', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.unit', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), + name='data_points', full_name='opentelemetry.proto.metrics.v1.IntSum.data_points', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='type', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.type', index=3, - number=4, type=14, cpp_type=8, label=1, + name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.IntSum.aggregation_temporality', index=1, + number=2, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='temporality', full_name='opentelemetry.proto.metrics.v1.MetricDescriptor.temporality', index=4, - number=5, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, + name='is_monotonic', full_name='opentelemetry.proto.metrics.v1.IntSum.is_monotonic', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), @@ -281,8 +319,6 @@ ], nested_types=[], enum_types=[ - _METRICDESCRIPTOR_TYPE, - _METRICDESCRIPTOR_TEMPORALITY, ], serialized_options=None, is_extendable=False, @@ -290,42 +326,111 @@ extension_ranges=[], oneofs=[ ], - serialized_start=939, - serialized_end=1364, + serialized_start=1173, + serialized_end=1359, ) -_INT64DATAPOINT = _descriptor.Descriptor( - name='Int64DataPoint', - full_name='opentelemetry.proto.metrics.v1.Int64DataPoint', +_DOUBLESUM = _descriptor.Descriptor( + name='DoubleSum', + full_name='opentelemetry.proto.metrics.v1.DoubleSum', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='labels', full_name='opentelemetry.proto.metrics.v1.Int64DataPoint.labels', index=0, + name='data_points', full_name='opentelemetry.proto.metrics.v1.DoubleSum.data_points', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.Int64DataPoint.start_time_unix_nano', index=1, - number=2, type=6, cpp_type=4, label=1, + name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.DoubleSum.aggregation_temporality', index=1, + number=2, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.Int64DataPoint.time_unix_nano', index=2, - number=3, type=6, cpp_type=4, label=1, + name='is_monotonic', full_name='opentelemetry.proto.metrics.v1.DoubleSum.is_monotonic', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1362, + serialized_end=1554, +) + + +_INTHISTOGRAM = _descriptor.Descriptor( + name='IntHistogram', + full_name='opentelemetry.proto.metrics.v1.IntHistogram', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='data_points', full_name='opentelemetry.proto.metrics.v1.IntHistogram.data_points', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.IntHistogram.aggregation_temporality', index=1, + number=2, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1557, + serialized_end=1736, +) + + +_DOUBLEHISTOGRAM = _descriptor.Descriptor( + name='DoubleHistogram', + full_name='opentelemetry.proto.metrics.v1.DoubleHistogram', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='data_points', full_name='opentelemetry.proto.metrics.v1.DoubleHistogram.data_points', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='value', full_name='opentelemetry.proto.metrics.v1.Int64DataPoint.value', index=3, - number=4, type=3, cpp_type=2, label=1, + name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.DoubleHistogram.aggregation_temporality', index=1, + number=2, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, @@ -342,43 +447,50 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1367, - serialized_end=1515, + serialized_start=1739, + serialized_end=1924, ) -_DOUBLEDATAPOINT = _descriptor.Descriptor( - name='DoubleDataPoint', - full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint', +_INTDATAPOINT = _descriptor.Descriptor( + name='IntDataPoint', + full_name='opentelemetry.proto.metrics.v1.IntDataPoint', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='labels', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.labels', index=0, + name='labels', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.labels', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.start_time_unix_nano', index=1, + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.start_time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.time_unix_nano', index=2, + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.time_unix_nano', index=2, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='value', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.value', index=3, - number=4, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), + name='value', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.value', index=3, + number=4, type=16, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='exemplars', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.exemplars', index=4, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), @@ -394,35 +506,49 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1518, - serialized_end=1667, + serialized_start=1927, + serialized_end=2137, ) -_HISTOGRAMDATAPOINT_BUCKET_EXEMPLAR = _descriptor.Descriptor( - name='Exemplar', - full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar', +_DOUBLEDATAPOINT = _descriptor.Descriptor( + name='DoubleDataPoint', + full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='value', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar.value', index=0, - number=1, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), + name='labels', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.labels', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar.time_unix_nano', index=1, + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.start_time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='attachments', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar.attachments', index=2, - number=3, type=11, cpp_type=10, label=3, + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.time_unix_nano', index=2, + number=3, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.value', index=3, + number=4, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='exemplars', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.exemplars', index=4, + number=5, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, @@ -439,35 +565,78 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2050, - serialized_end=2167, + serialized_start=2140, + serialized_end=2356, ) -_HISTOGRAMDATAPOINT_BUCKET = _descriptor.Descriptor( - name='Bucket', - full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket', + +_INTHISTOGRAMDATAPOINT = _descriptor.Descriptor( + name='IntHistogramDataPoint', + full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='count', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.count', index=0, - number=1, type=4, cpp_type=4, label=1, + name='labels', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.labels', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.start_time_unix_nano', index=1, + number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='exemplar', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.exemplar', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.time_unix_nano', index=2, + number=3, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='count', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.count', index=3, + number=4, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sum', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.sum', index=4, + number=5, type=16, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='bucket_counts', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.bucket_counts', index=5, + number=6, type=6, cpp_type=4, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='explicit_bounds', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.explicit_bounds', index=6, + number=7, type=1, cpp_type=5, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='exemplars', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.exemplars', index=7, + number=8, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], - nested_types=[_HISTOGRAMDATAPOINT_BUCKET_EXEMPLAR, ], + nested_types=[], enum_types=[ ], serialized_options=None, @@ -476,70 +645,78 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1939, - serialized_end=2167, + serialized_start=2359, + serialized_end=2639, ) -_HISTOGRAMDATAPOINT = _descriptor.Descriptor( - name='HistogramDataPoint', - full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint', + +_DOUBLEHISTOGRAMDATAPOINT = _descriptor.Descriptor( + name='DoubleHistogramDataPoint', + full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='labels', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.labels', index=0, + name='labels', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.labels', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.start_time_unix_nano', index=1, + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.start_time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.time_unix_nano', index=2, + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.time_unix_nano', index=2, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='count', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.count', index=3, - number=4, type=4, cpp_type=4, label=1, + name='count', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.count', index=3, + number=4, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='sum', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.sum', index=4, + name='sum', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.sum', index=4, number=5, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='buckets', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.buckets', index=5, - number=6, type=11, cpp_type=10, label=3, + name='bucket_counts', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.bucket_counts', index=5, + number=6, type=6, cpp_type=4, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='explicit_bounds', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.explicit_bounds', index=6, + name='explicit_bounds', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.explicit_bounds', index=6, number=7, type=1, cpp_type=5, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='exemplars', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.exemplars', index=7, + number=8, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], - nested_types=[_HISTOGRAMDATAPOINT_BUCKET, ], + nested_types=[], enum_types=[ ], serialized_options=None, @@ -548,29 +725,50 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1670, - serialized_end=2167, + serialized_start=2642, + serialized_end=2928, ) -_SUMMARYDATAPOINT_VALUEATPERCENTILE = _descriptor.Descriptor( - name='ValueAtPercentile', - full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile', +_INTEXEMPLAR = _descriptor.Descriptor( + name='IntExemplar', + full_name='opentelemetry.proto.metrics.v1.IntExemplar', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='percentile', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile.percentile', index=0, - number=1, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), + name='filtered_labels', full_name='opentelemetry.proto.metrics.v1.IntExemplar.filtered_labels', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='value', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile.value', index=1, - number=2, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntExemplar.time_unix_nano', index=1, + number=2, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='opentelemetry.proto.metrics.v1.IntExemplar.value', index=2, + number=3, type=16, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='span_id', full_name='opentelemetry.proto.metrics.v1.IntExemplar.span_id', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='trace_id', full_name='opentelemetry.proto.metrics.v1.IntExemplar.trace_id', index=4, + number=5, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), @@ -586,63 +784,57 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2430, - serialized_end=2484, + serialized_start=2931, + serialized_end=3090, ) -_SUMMARYDATAPOINT = _descriptor.Descriptor( - name='SummaryDataPoint', - full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint', + +_DOUBLEEXEMPLAR = _descriptor.Descriptor( + name='DoubleExemplar', + full_name='opentelemetry.proto.metrics.v1.DoubleExemplar', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='labels', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.labels', index=0, + name='filtered_labels', full_name='opentelemetry.proto.metrics.v1.DoubleExemplar.filtered_labels', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.start_time_unix_nano', index=1, + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleExemplar.time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.time_unix_nano', index=2, - number=3, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='count', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.count', index=3, - number=4, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, + name='value', full_name='opentelemetry.proto.metrics.v1.DoubleExemplar.value', index=2, + number=3, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='sum', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.sum', index=4, - number=5, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), + name='span_id', full_name='opentelemetry.proto.metrics.v1.DoubleExemplar.span_id', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='percentile_values', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.percentile_values', index=5, - number=6, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], + name='trace_id', full_name='opentelemetry.proto.metrics.v1.DoubleExemplar.trace_id', index=4, + number=5, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], - nested_types=[_SUMMARYDATAPOINT_VALUEATPERCENTILE, ], + nested_types=[], enum_types=[ ], serialized_options=None, @@ -651,42 +843,74 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2170, - serialized_end=2484, + serialized_start=3093, + serialized_end=3255, ) _RESOURCEMETRICS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE _RESOURCEMETRICS.fields_by_name['instrumentation_library_metrics'].message_type = _INSTRUMENTATIONLIBRARYMETRICS _INSTRUMENTATIONLIBRARYMETRICS.fields_by_name['instrumentation_library'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONLIBRARY _INSTRUMENTATIONLIBRARYMETRICS.fields_by_name['metrics'].message_type = _METRIC -_METRIC.fields_by_name['metric_descriptor'].message_type = _METRICDESCRIPTOR -_METRIC.fields_by_name['int64_data_points'].message_type = _INT64DATAPOINT -_METRIC.fields_by_name['double_data_points'].message_type = _DOUBLEDATAPOINT -_METRIC.fields_by_name['histogram_data_points'].message_type = _HISTOGRAMDATAPOINT -_METRIC.fields_by_name['summary_data_points'].message_type = _SUMMARYDATAPOINT -_METRICDESCRIPTOR.fields_by_name['type'].enum_type = _METRICDESCRIPTOR_TYPE -_METRICDESCRIPTOR.fields_by_name['temporality'].enum_type = _METRICDESCRIPTOR_TEMPORALITY -_METRICDESCRIPTOR_TYPE.containing_type = _METRICDESCRIPTOR -_METRICDESCRIPTOR_TEMPORALITY.containing_type = _METRICDESCRIPTOR -_INT64DATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_METRIC.fields_by_name['int_gauge'].message_type = _INTGAUGE +_METRIC.fields_by_name['double_gauge'].message_type = _DOUBLEGAUGE +_METRIC.fields_by_name['int_sum'].message_type = _INTSUM +_METRIC.fields_by_name['double_sum'].message_type = _DOUBLESUM +_METRIC.fields_by_name['int_histogram'].message_type = _INTHISTOGRAM +_METRIC.fields_by_name['double_histogram'].message_type = _DOUBLEHISTOGRAM +_METRIC.oneofs_by_name['data'].fields.append( + _METRIC.fields_by_name['int_gauge']) +_METRIC.fields_by_name['int_gauge'].containing_oneof = _METRIC.oneofs_by_name['data'] +_METRIC.oneofs_by_name['data'].fields.append( + _METRIC.fields_by_name['double_gauge']) +_METRIC.fields_by_name['double_gauge'].containing_oneof = _METRIC.oneofs_by_name['data'] +_METRIC.oneofs_by_name['data'].fields.append( + _METRIC.fields_by_name['int_sum']) +_METRIC.fields_by_name['int_sum'].containing_oneof = _METRIC.oneofs_by_name['data'] +_METRIC.oneofs_by_name['data'].fields.append( + _METRIC.fields_by_name['double_sum']) +_METRIC.fields_by_name['double_sum'].containing_oneof = _METRIC.oneofs_by_name['data'] +_METRIC.oneofs_by_name['data'].fields.append( + _METRIC.fields_by_name['int_histogram']) +_METRIC.fields_by_name['int_histogram'].containing_oneof = _METRIC.oneofs_by_name['data'] +_METRIC.oneofs_by_name['data'].fields.append( + _METRIC.fields_by_name['double_histogram']) +_METRIC.fields_by_name['double_histogram'].containing_oneof = _METRIC.oneofs_by_name['data'] +_INTGAUGE.fields_by_name['data_points'].message_type = _INTDATAPOINT +_DOUBLEGAUGE.fields_by_name['data_points'].message_type = _DOUBLEDATAPOINT +_INTSUM.fields_by_name['data_points'].message_type = _INTDATAPOINT +_INTSUM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY +_DOUBLESUM.fields_by_name['data_points'].message_type = _DOUBLEDATAPOINT +_DOUBLESUM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY +_INTHISTOGRAM.fields_by_name['data_points'].message_type = _INTHISTOGRAMDATAPOINT +_INTHISTOGRAM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY +_DOUBLEHISTOGRAM.fields_by_name['data_points'].message_type = _DOUBLEHISTOGRAMDATAPOINT +_DOUBLEHISTOGRAM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY +_INTDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_INTDATAPOINT.fields_by_name['exemplars'].message_type = _INTEXEMPLAR _DOUBLEDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE -_HISTOGRAMDATAPOINT_BUCKET_EXEMPLAR.fields_by_name['attachments'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE -_HISTOGRAMDATAPOINT_BUCKET_EXEMPLAR.containing_type = _HISTOGRAMDATAPOINT_BUCKET -_HISTOGRAMDATAPOINT_BUCKET.fields_by_name['exemplar'].message_type = _HISTOGRAMDATAPOINT_BUCKET_EXEMPLAR -_HISTOGRAMDATAPOINT_BUCKET.containing_type = _HISTOGRAMDATAPOINT -_HISTOGRAMDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE -_HISTOGRAMDATAPOINT.fields_by_name['buckets'].message_type = _HISTOGRAMDATAPOINT_BUCKET -_SUMMARYDATAPOINT_VALUEATPERCENTILE.containing_type = _SUMMARYDATAPOINT -_SUMMARYDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE -_SUMMARYDATAPOINT.fields_by_name['percentile_values'].message_type = _SUMMARYDATAPOINT_VALUEATPERCENTILE +_DOUBLEDATAPOINT.fields_by_name['exemplars'].message_type = _DOUBLEEXEMPLAR +_INTHISTOGRAMDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_INTHISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _INTEXEMPLAR +_DOUBLEHISTOGRAMDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_DOUBLEHISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _DOUBLEEXEMPLAR +_INTEXEMPLAR.fields_by_name['filtered_labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_DOUBLEEXEMPLAR.fields_by_name['filtered_labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE DESCRIPTOR.message_types_by_name['ResourceMetrics'] = _RESOURCEMETRICS DESCRIPTOR.message_types_by_name['InstrumentationLibraryMetrics'] = _INSTRUMENTATIONLIBRARYMETRICS DESCRIPTOR.message_types_by_name['Metric'] = _METRIC -DESCRIPTOR.message_types_by_name['MetricDescriptor'] = _METRICDESCRIPTOR -DESCRIPTOR.message_types_by_name['Int64DataPoint'] = _INT64DATAPOINT +DESCRIPTOR.message_types_by_name['IntGauge'] = _INTGAUGE +DESCRIPTOR.message_types_by_name['DoubleGauge'] = _DOUBLEGAUGE +DESCRIPTOR.message_types_by_name['IntSum'] = _INTSUM +DESCRIPTOR.message_types_by_name['DoubleSum'] = _DOUBLESUM +DESCRIPTOR.message_types_by_name['IntHistogram'] = _INTHISTOGRAM +DESCRIPTOR.message_types_by_name['DoubleHistogram'] = _DOUBLEHISTOGRAM +DESCRIPTOR.message_types_by_name['IntDataPoint'] = _INTDATAPOINT DESCRIPTOR.message_types_by_name['DoubleDataPoint'] = _DOUBLEDATAPOINT -DESCRIPTOR.message_types_by_name['HistogramDataPoint'] = _HISTOGRAMDATAPOINT -DESCRIPTOR.message_types_by_name['SummaryDataPoint'] = _SUMMARYDATAPOINT +DESCRIPTOR.message_types_by_name['IntHistogramDataPoint'] = _INTHISTOGRAMDATAPOINT +DESCRIPTOR.message_types_by_name['DoubleHistogramDataPoint'] = _DOUBLEHISTOGRAMDATAPOINT +DESCRIPTOR.message_types_by_name['IntExemplar'] = _INTEXEMPLAR +DESCRIPTOR.message_types_by_name['DoubleExemplar'] = _DOUBLEEXEMPLAR +DESCRIPTOR.enum_types_by_name['AggregationTemporality'] = _AGGREGATIONTEMPORALITY _sym_db.RegisterFileDescriptor(DESCRIPTOR) ResourceMetrics = _reflection.GeneratedProtocolMessageType('ResourceMetrics', (_message.Message,), { @@ -710,19 +934,54 @@ }) _sym_db.RegisterMessage(Metric) -MetricDescriptor = _reflection.GeneratedProtocolMessageType('MetricDescriptor', (_message.Message,), { - 'DESCRIPTOR' : _METRICDESCRIPTOR, +IntGauge = _reflection.GeneratedProtocolMessageType('IntGauge', (_message.Message,), { + 'DESCRIPTOR' : _INTGAUGE, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.IntGauge) + }) +_sym_db.RegisterMessage(IntGauge) + +DoubleGauge = _reflection.GeneratedProtocolMessageType('DoubleGauge', (_message.Message,), { + 'DESCRIPTOR' : _DOUBLEGAUGE, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.MetricDescriptor) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleGauge) }) -_sym_db.RegisterMessage(MetricDescriptor) +_sym_db.RegisterMessage(DoubleGauge) -Int64DataPoint = _reflection.GeneratedProtocolMessageType('Int64DataPoint', (_message.Message,), { - 'DESCRIPTOR' : _INT64DATAPOINT, +IntSum = _reflection.GeneratedProtocolMessageType('IntSum', (_message.Message,), { + 'DESCRIPTOR' : _INTSUM, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.Int64DataPoint) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.IntSum) }) -_sym_db.RegisterMessage(Int64DataPoint) +_sym_db.RegisterMessage(IntSum) + +DoubleSum = _reflection.GeneratedProtocolMessageType('DoubleSum', (_message.Message,), { + 'DESCRIPTOR' : _DOUBLESUM, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleSum) + }) +_sym_db.RegisterMessage(DoubleSum) + +IntHistogram = _reflection.GeneratedProtocolMessageType('IntHistogram', (_message.Message,), { + 'DESCRIPTOR' : _INTHISTOGRAM, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.IntHistogram) + }) +_sym_db.RegisterMessage(IntHistogram) + +DoubleHistogram = _reflection.GeneratedProtocolMessageType('DoubleHistogram', (_message.Message,), { + 'DESCRIPTOR' : _DOUBLEHISTOGRAM, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleHistogram) + }) +_sym_db.RegisterMessage(DoubleHistogram) + +IntDataPoint = _reflection.GeneratedProtocolMessageType('IntDataPoint', (_message.Message,), { + 'DESCRIPTOR' : _INTDATAPOINT, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.IntDataPoint) + }) +_sym_db.RegisterMessage(IntDataPoint) DoubleDataPoint = _reflection.GeneratedProtocolMessageType('DoubleDataPoint', (_message.Message,), { 'DESCRIPTOR' : _DOUBLEDATAPOINT, @@ -731,43 +990,33 @@ }) _sym_db.RegisterMessage(DoubleDataPoint) -HistogramDataPoint = _reflection.GeneratedProtocolMessageType('HistogramDataPoint', (_message.Message,), { - - 'Bucket' : _reflection.GeneratedProtocolMessageType('Bucket', (_message.Message,), { +IntHistogramDataPoint = _reflection.GeneratedProtocolMessageType('IntHistogramDataPoint', (_message.Message,), { + 'DESCRIPTOR' : _INTHISTOGRAMDATAPOINT, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.IntHistogramDataPoint) + }) +_sym_db.RegisterMessage(IntHistogramDataPoint) - 'Exemplar' : _reflection.GeneratedProtocolMessageType('Exemplar', (_message.Message,), { - 'DESCRIPTOR' : _HISTOGRAMDATAPOINT_BUCKET_EXEMPLAR, - '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket.Exemplar) - }) - , - 'DESCRIPTOR' : _HISTOGRAMDATAPOINT_BUCKET, - '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.HistogramDataPoint.Bucket) - }) - , - 'DESCRIPTOR' : _HISTOGRAMDATAPOINT, +DoubleHistogramDataPoint = _reflection.GeneratedProtocolMessageType('DoubleHistogramDataPoint', (_message.Message,), { + 'DESCRIPTOR' : _DOUBLEHISTOGRAMDATAPOINT, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.HistogramDataPoint) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint) }) -_sym_db.RegisterMessage(HistogramDataPoint) -_sym_db.RegisterMessage(HistogramDataPoint.Bucket) -_sym_db.RegisterMessage(HistogramDataPoint.Bucket.Exemplar) +_sym_db.RegisterMessage(DoubleHistogramDataPoint) -SummaryDataPoint = _reflection.GeneratedProtocolMessageType('SummaryDataPoint', (_message.Message,), { +IntExemplar = _reflection.GeneratedProtocolMessageType('IntExemplar', (_message.Message,), { + 'DESCRIPTOR' : _INTEXEMPLAR, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.IntExemplar) + }) +_sym_db.RegisterMessage(IntExemplar) - 'ValueAtPercentile' : _reflection.GeneratedProtocolMessageType('ValueAtPercentile', (_message.Message,), { - 'DESCRIPTOR' : _SUMMARYDATAPOINT_VALUEATPERCENTILE, - '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtPercentile) - }) - , - 'DESCRIPTOR' : _SUMMARYDATAPOINT, +DoubleExemplar = _reflection.GeneratedProtocolMessageType('DoubleExemplar', (_message.Message,), { + 'DESCRIPTOR' : _DOUBLEEXEMPLAR, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.SummaryDataPoint) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleExemplar) }) -_sym_db.RegisterMessage(SummaryDataPoint) -_sym_db.RegisterMessage(SummaryDataPoint.ValueAtPercentile) +_sym_db.RegisterMessage(DoubleExemplar) DESCRIPTOR._options = None diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi index 964e23e3a8..16a6b3d4b5 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi @@ -52,6 +52,28 @@ if sys.version_info < (3,): DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... +AggregationTemporalityValue = typing___NewType('AggregationTemporalityValue', builtin___int) +type___AggregationTemporalityValue = AggregationTemporalityValue +class AggregationTemporality(object): + DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... + @classmethod + def Name(cls, number: builtin___int) -> builtin___str: ... + @classmethod + def Value(cls, name: builtin___str) -> AggregationTemporalityValue: ... + @classmethod + def keys(cls) -> typing___List[builtin___str]: ... + @classmethod + def values(cls) -> typing___List[AggregationTemporalityValue]: ... + @classmethod + def items(cls) -> typing___List[typing___Tuple[builtin___str, AggregationTemporalityValue]]: ... + AGGREGATION_TEMPORALITY_UNSPECIFIED = typing___cast(AggregationTemporalityValue, 0) + AGGREGATION_TEMPORALITY_DELTA = typing___cast(AggregationTemporalityValue, 1) + AGGREGATION_TEMPORALITY_CUMULATIVE = typing___cast(AggregationTemporalityValue, 2) +AGGREGATION_TEMPORALITY_UNSPECIFIED = typing___cast(AggregationTemporalityValue, 0) +AGGREGATION_TEMPORALITY_DELTA = typing___cast(AggregationTemporalityValue, 1) +AGGREGATION_TEMPORALITY_CUMULATIVE = typing___cast(AggregationTemporalityValue, 2) +type___AggregationTemporality = AggregationTemporality + class ResourceMetrics(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... @@ -102,29 +124,39 @@ type___InstrumentationLibraryMetrics = InstrumentationLibraryMetrics class Metric(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + name: typing___Text = ... + description: typing___Text = ... + unit: typing___Text = ... @property - def metric_descriptor(self) -> type___MetricDescriptor: ... + def int_gauge(self) -> type___IntGauge: ... @property - def int64_data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Int64DataPoint]: ... + def double_gauge(self) -> type___DoubleGauge: ... @property - def double_data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___DoubleDataPoint]: ... + def int_sum(self) -> type___IntSum: ... @property - def histogram_data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___HistogramDataPoint]: ... + def double_sum(self) -> type___DoubleSum: ... @property - def summary_data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___SummaryDataPoint]: ... + def int_histogram(self) -> type___IntHistogram: ... + + @property + def double_histogram(self) -> type___DoubleHistogram: ... def __init__(self, *, - metric_descriptor : typing___Optional[type___MetricDescriptor] = None, - int64_data_points : typing___Optional[typing___Iterable[type___Int64DataPoint]] = None, - double_data_points : typing___Optional[typing___Iterable[type___DoubleDataPoint]] = None, - histogram_data_points : typing___Optional[typing___Iterable[type___HistogramDataPoint]] = None, - summary_data_points : typing___Optional[typing___Iterable[type___SummaryDataPoint]] = None, + name : typing___Optional[typing___Text] = None, + description : typing___Optional[typing___Text] = None, + unit : typing___Optional[typing___Text] = None, + int_gauge : typing___Optional[type___IntGauge] = None, + double_gauge : typing___Optional[type___DoubleGauge] = None, + int_sum : typing___Optional[type___IntSum] = None, + double_sum : typing___Optional[type___DoubleSum] = None, + int_histogram : typing___Optional[type___IntHistogram] = None, + double_histogram : typing___Optional[type___DoubleHistogram] = None, ) -> None: ... if sys.version_info >= (3,): @classmethod @@ -132,90 +164,138 @@ class Metric(google___protobuf___message___Message): else: @classmethod def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Metric: ... - def HasField(self, field_name: typing_extensions___Literal[u"metric_descriptor",b"metric_descriptor"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"double_data_points",b"double_data_points",u"histogram_data_points",b"histogram_data_points",u"int64_data_points",b"int64_data_points",u"metric_descriptor",b"metric_descriptor",u"summary_data_points",b"summary_data_points"]) -> None: ... + def HasField(self, field_name: typing_extensions___Literal[u"data",b"data",u"double_gauge",b"double_gauge",u"double_histogram",b"double_histogram",u"double_sum",b"double_sum",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"data",b"data",u"description",b"description",u"double_gauge",b"double_gauge",u"double_histogram",b"double_histogram",u"double_sum",b"double_sum",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum",u"name",b"name",u"unit",b"unit"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions___Literal[u"data",b"data"]) -> typing_extensions___Literal["int_gauge","double_gauge","int_sum","double_sum","int_histogram","double_histogram"]: ... type___Metric = Metric -class MetricDescriptor(google___protobuf___message___Message): +class IntGauge(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - TypeValue = typing___NewType('TypeValue', builtin___int) - type___TypeValue = TypeValue - class Type(object): - DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... + + @property + def data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___IntDataPoint]: ... + + def __init__(self, + *, + data_points : typing___Optional[typing___Iterable[type___IntDataPoint]] = None, + ) -> None: ... + if sys.version_info >= (3,): @classmethod - def Name(cls, number: builtin___int) -> builtin___str: ... + def FromString(cls, s: builtin___bytes) -> IntGauge: ... + else: @classmethod - def Value(cls, name: builtin___str) -> MetricDescriptor.TypeValue: ... + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> IntGauge: ... + def ClearField(self, field_name: typing_extensions___Literal[u"data_points",b"data_points"]) -> None: ... +type___IntGauge = IntGauge + +class DoubleGauge(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + @property + def data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___DoubleDataPoint]: ... + + def __init__(self, + *, + data_points : typing___Optional[typing___Iterable[type___DoubleDataPoint]] = None, + ) -> None: ... + if sys.version_info >= (3,): @classmethod - def keys(cls) -> typing___List[builtin___str]: ... + def FromString(cls, s: builtin___bytes) -> DoubleGauge: ... + else: @classmethod - def values(cls) -> typing___List[MetricDescriptor.TypeValue]: ... + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> DoubleGauge: ... + def ClearField(self, field_name: typing_extensions___Literal[u"data_points",b"data_points"]) -> None: ... +type___DoubleGauge = DoubleGauge + +class IntSum(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + aggregation_temporality: type___AggregationTemporalityValue = ... + is_monotonic: builtin___bool = ... + + @property + def data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___IntDataPoint]: ... + + def __init__(self, + *, + data_points : typing___Optional[typing___Iterable[type___IntDataPoint]] = None, + aggregation_temporality : typing___Optional[type___AggregationTemporalityValue] = None, + is_monotonic : typing___Optional[builtin___bool] = None, + ) -> None: ... + if sys.version_info >= (3,): @classmethod - def items(cls) -> typing___List[typing___Tuple[builtin___str, MetricDescriptor.TypeValue]]: ... - INVALID_TYPE = typing___cast(MetricDescriptor.TypeValue, 0) - INT64 = typing___cast(MetricDescriptor.TypeValue, 1) - MONOTONIC_INT64 = typing___cast(MetricDescriptor.TypeValue, 2) - DOUBLE = typing___cast(MetricDescriptor.TypeValue, 3) - MONOTONIC_DOUBLE = typing___cast(MetricDescriptor.TypeValue, 4) - HISTOGRAM = typing___cast(MetricDescriptor.TypeValue, 5) - SUMMARY = typing___cast(MetricDescriptor.TypeValue, 6) - INVALID_TYPE = typing___cast(MetricDescriptor.TypeValue, 0) - INT64 = typing___cast(MetricDescriptor.TypeValue, 1) - MONOTONIC_INT64 = typing___cast(MetricDescriptor.TypeValue, 2) - DOUBLE = typing___cast(MetricDescriptor.TypeValue, 3) - MONOTONIC_DOUBLE = typing___cast(MetricDescriptor.TypeValue, 4) - HISTOGRAM = typing___cast(MetricDescriptor.TypeValue, 5) - SUMMARY = typing___cast(MetricDescriptor.TypeValue, 6) - type___Type = Type - - TemporalityValue = typing___NewType('TemporalityValue', builtin___int) - type___TemporalityValue = TemporalityValue - class Temporality(object): - DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... + def FromString(cls, s: builtin___bytes) -> IntSum: ... + else: @classmethod - def Name(cls, number: builtin___int) -> builtin___str: ... + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> IntSum: ... + def ClearField(self, field_name: typing_extensions___Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points",u"is_monotonic",b"is_monotonic"]) -> None: ... +type___IntSum = IntSum + +class DoubleSum(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + aggregation_temporality: type___AggregationTemporalityValue = ... + is_monotonic: builtin___bool = ... + + @property + def data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___DoubleDataPoint]: ... + + def __init__(self, + *, + data_points : typing___Optional[typing___Iterable[type___DoubleDataPoint]] = None, + aggregation_temporality : typing___Optional[type___AggregationTemporalityValue] = None, + is_monotonic : typing___Optional[builtin___bool] = None, + ) -> None: ... + if sys.version_info >= (3,): @classmethod - def Value(cls, name: builtin___str) -> MetricDescriptor.TemporalityValue: ... + def FromString(cls, s: builtin___bytes) -> DoubleSum: ... + else: @classmethod - def keys(cls) -> typing___List[builtin___str]: ... + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> DoubleSum: ... + def ClearField(self, field_name: typing_extensions___Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points",u"is_monotonic",b"is_monotonic"]) -> None: ... +type___DoubleSum = DoubleSum + +class IntHistogram(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + aggregation_temporality: type___AggregationTemporalityValue = ... + + @property + def data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___IntHistogramDataPoint]: ... + + def __init__(self, + *, + data_points : typing___Optional[typing___Iterable[type___IntHistogramDataPoint]] = None, + aggregation_temporality : typing___Optional[type___AggregationTemporalityValue] = None, + ) -> None: ... + if sys.version_info >= (3,): @classmethod - def values(cls) -> typing___List[MetricDescriptor.TemporalityValue]: ... + def FromString(cls, s: builtin___bytes) -> IntHistogram: ... + else: @classmethod - def items(cls) -> typing___List[typing___Tuple[builtin___str, MetricDescriptor.TemporalityValue]]: ... - INVALID_TEMPORALITY = typing___cast(MetricDescriptor.TemporalityValue, 0) - INSTANTANEOUS = typing___cast(MetricDescriptor.TemporalityValue, 1) - DELTA = typing___cast(MetricDescriptor.TemporalityValue, 2) - CUMULATIVE = typing___cast(MetricDescriptor.TemporalityValue, 3) - INVALID_TEMPORALITY = typing___cast(MetricDescriptor.TemporalityValue, 0) - INSTANTANEOUS = typing___cast(MetricDescriptor.TemporalityValue, 1) - DELTA = typing___cast(MetricDescriptor.TemporalityValue, 2) - CUMULATIVE = typing___cast(MetricDescriptor.TemporalityValue, 3) - type___Temporality = Temporality + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> IntHistogram: ... + def ClearField(self, field_name: typing_extensions___Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points"]) -> None: ... +type___IntHistogram = IntHistogram - name: typing___Text = ... - description: typing___Text = ... - unit: typing___Text = ... - type: type___MetricDescriptor.TypeValue = ... - temporality: type___MetricDescriptor.TemporalityValue = ... +class DoubleHistogram(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + aggregation_temporality: type___AggregationTemporalityValue = ... + + @property + def data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___DoubleHistogramDataPoint]: ... def __init__(self, *, - name : typing___Optional[typing___Text] = None, - description : typing___Optional[typing___Text] = None, - unit : typing___Optional[typing___Text] = None, - type : typing___Optional[type___MetricDescriptor.TypeValue] = None, - temporality : typing___Optional[type___MetricDescriptor.TemporalityValue] = None, + data_points : typing___Optional[typing___Iterable[type___DoubleHistogramDataPoint]] = None, + aggregation_temporality : typing___Optional[type___AggregationTemporalityValue] = None, ) -> None: ... if sys.version_info >= (3,): @classmethod - def FromString(cls, s: builtin___bytes) -> MetricDescriptor: ... + def FromString(cls, s: builtin___bytes) -> DoubleHistogram: ... else: @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> MetricDescriptor: ... - def ClearField(self, field_name: typing_extensions___Literal[u"description",b"description",u"name",b"name",u"temporality",b"temporality",u"type",b"type",u"unit",b"unit"]) -> None: ... -type___MetricDescriptor = MetricDescriptor + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> DoubleHistogram: ... + def ClearField(self, field_name: typing_extensions___Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points"]) -> None: ... +type___DoubleHistogram = DoubleHistogram -class Int64DataPoint(google___protobuf___message___Message): +class IntDataPoint(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... start_time_unix_nano: builtin___int = ... time_unix_nano: builtin___int = ... @@ -224,21 +304,25 @@ class Int64DataPoint(google___protobuf___message___Message): @property def labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + @property + def exemplars(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___IntExemplar]: ... + def __init__(self, *, labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, start_time_unix_nano : typing___Optional[builtin___int] = None, time_unix_nano : typing___Optional[builtin___int] = None, value : typing___Optional[builtin___int] = None, + exemplars : typing___Optional[typing___Iterable[type___IntExemplar]] = None, ) -> None: ... if sys.version_info >= (3,): @classmethod - def FromString(cls, s: builtin___bytes) -> Int64DataPoint: ... + def FromString(cls, s: builtin___bytes) -> IntDataPoint: ... else: @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Int64DataPoint: ... - def ClearField(self, field_name: typing_extensions___Literal[u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... -type___Int64DataPoint = Int64DataPoint + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> IntDataPoint: ... + def ClearField(self, field_name: typing_extensions___Literal[u"exemplars",b"exemplars",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... +type___IntDataPoint = IntDataPoint class DoubleDataPoint(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... @@ -249,12 +333,16 @@ class DoubleDataPoint(google___protobuf___message___Message): @property def labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + @property + def exemplars(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___DoubleExemplar]: ... + def __init__(self, *, labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, start_time_unix_nano : typing___Optional[builtin___int] = None, time_unix_nano : typing___Optional[builtin___int] = None, value : typing___Optional[builtin___float] = None, + exemplars : typing___Optional[typing___Iterable[type___DoubleExemplar]] = None, ) -> None: ... if sys.version_info >= (3,): @classmethod @@ -262,67 +350,23 @@ class DoubleDataPoint(google___protobuf___message___Message): else: @classmethod def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> DoubleDataPoint: ... - def ClearField(self, field_name: typing_extensions___Literal[u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... + def ClearField(self, field_name: typing_extensions___Literal[u"exemplars",b"exemplars",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... type___DoubleDataPoint = DoubleDataPoint -class HistogramDataPoint(google___protobuf___message___Message): +class IntHistogramDataPoint(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - class Bucket(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - class Exemplar(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - value: builtin___float = ... - time_unix_nano: builtin___int = ... - - @property - def attachments(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... - - def __init__(self, - *, - value : typing___Optional[builtin___float] = None, - time_unix_nano : typing___Optional[builtin___int] = None, - attachments : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, - ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> HistogramDataPoint.Bucket.Exemplar: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> HistogramDataPoint.Bucket.Exemplar: ... - def ClearField(self, field_name: typing_extensions___Literal[u"attachments",b"attachments",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... - type___Exemplar = Exemplar - - count: builtin___int = ... - - @property - def exemplar(self) -> type___HistogramDataPoint.Bucket.Exemplar: ... - - def __init__(self, - *, - count : typing___Optional[builtin___int] = None, - exemplar : typing___Optional[type___HistogramDataPoint.Bucket.Exemplar] = None, - ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> HistogramDataPoint.Bucket: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> HistogramDataPoint.Bucket: ... - def HasField(self, field_name: typing_extensions___Literal[u"exemplar",b"exemplar"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"count",b"count",u"exemplar",b"exemplar"]) -> None: ... - type___Bucket = Bucket - start_time_unix_nano: builtin___int = ... time_unix_nano: builtin___int = ... count: builtin___int = ... - sum: builtin___float = ... + sum: builtin___int = ... + bucket_counts: google___protobuf___internal___containers___RepeatedScalarFieldContainer[builtin___int] = ... explicit_bounds: google___protobuf___internal___containers___RepeatedScalarFieldContainer[builtin___float] = ... @property def labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... @property - def buckets(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___HistogramDataPoint.Bucket]: ... + def exemplars(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___IntExemplar]: ... def __init__(self, *, @@ -330,50 +374,34 @@ class HistogramDataPoint(google___protobuf___message___Message): start_time_unix_nano : typing___Optional[builtin___int] = None, time_unix_nano : typing___Optional[builtin___int] = None, count : typing___Optional[builtin___int] = None, - sum : typing___Optional[builtin___float] = None, - buckets : typing___Optional[typing___Iterable[type___HistogramDataPoint.Bucket]] = None, + sum : typing___Optional[builtin___int] = None, + bucket_counts : typing___Optional[typing___Iterable[builtin___int]] = None, explicit_bounds : typing___Optional[typing___Iterable[builtin___float]] = None, + exemplars : typing___Optional[typing___Iterable[type___IntExemplar]] = None, ) -> None: ... if sys.version_info >= (3,): @classmethod - def FromString(cls, s: builtin___bytes) -> HistogramDataPoint: ... + def FromString(cls, s: builtin___bytes) -> IntHistogramDataPoint: ... else: @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> HistogramDataPoint: ... - def ClearField(self, field_name: typing_extensions___Literal[u"buckets",b"buckets",u"count",b"count",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... -type___HistogramDataPoint = HistogramDataPoint + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> IntHistogramDataPoint: ... + def ClearField(self, field_name: typing_extensions___Literal[u"bucket_counts",b"bucket_counts",u"count",b"count",u"exemplars",b"exemplars",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... +type___IntHistogramDataPoint = IntHistogramDataPoint -class SummaryDataPoint(google___protobuf___message___Message): +class DoubleHistogramDataPoint(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - class ValueAtPercentile(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - percentile: builtin___float = ... - value: builtin___float = ... - - def __init__(self, - *, - percentile : typing___Optional[builtin___float] = None, - value : typing___Optional[builtin___float] = None, - ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> SummaryDataPoint.ValueAtPercentile: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> SummaryDataPoint.ValueAtPercentile: ... - def ClearField(self, field_name: typing_extensions___Literal[u"percentile",b"percentile",u"value",b"value"]) -> None: ... - type___ValueAtPercentile = ValueAtPercentile - start_time_unix_nano: builtin___int = ... time_unix_nano: builtin___int = ... count: builtin___int = ... sum: builtin___float = ... + bucket_counts: google___protobuf___internal___containers___RepeatedScalarFieldContainer[builtin___int] = ... + explicit_bounds: google___protobuf___internal___containers___RepeatedScalarFieldContainer[builtin___float] = ... @property def labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... @property - def percentile_values(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___SummaryDataPoint.ValueAtPercentile]: ... + def exemplars(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___DoubleExemplar]: ... def __init__(self, *, @@ -382,13 +410,69 @@ class SummaryDataPoint(google___protobuf___message___Message): time_unix_nano : typing___Optional[builtin___int] = None, count : typing___Optional[builtin___int] = None, sum : typing___Optional[builtin___float] = None, - percentile_values : typing___Optional[typing___Iterable[type___SummaryDataPoint.ValueAtPercentile]] = None, + bucket_counts : typing___Optional[typing___Iterable[builtin___int]] = None, + explicit_bounds : typing___Optional[typing___Iterable[builtin___float]] = None, + exemplars : typing___Optional[typing___Iterable[type___DoubleExemplar]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> DoubleHistogramDataPoint: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> DoubleHistogramDataPoint: ... + def ClearField(self, field_name: typing_extensions___Literal[u"bucket_counts",b"bucket_counts",u"count",b"count",u"exemplars",b"exemplars",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... +type___DoubleHistogramDataPoint = DoubleHistogramDataPoint + +class IntExemplar(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + time_unix_nano: builtin___int = ... + value: builtin___int = ... + span_id: builtin___bytes = ... + trace_id: builtin___bytes = ... + + @property + def filtered_labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + + def __init__(self, + *, + filtered_labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, + time_unix_nano : typing___Optional[builtin___int] = None, + value : typing___Optional[builtin___int] = None, + span_id : typing___Optional[builtin___bytes] = None, + trace_id : typing___Optional[builtin___bytes] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> IntExemplar: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> IntExemplar: ... + def ClearField(self, field_name: typing_extensions___Literal[u"filtered_labels",b"filtered_labels",u"span_id",b"span_id",u"time_unix_nano",b"time_unix_nano",u"trace_id",b"trace_id",u"value",b"value"]) -> None: ... +type___IntExemplar = IntExemplar + +class DoubleExemplar(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + time_unix_nano: builtin___int = ... + value: builtin___float = ... + span_id: builtin___bytes = ... + trace_id: builtin___bytes = ... + + @property + def filtered_labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + + def __init__(self, + *, + filtered_labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, + time_unix_nano : typing___Optional[builtin___int] = None, + value : typing___Optional[builtin___float] = None, + span_id : typing___Optional[builtin___bytes] = None, + trace_id : typing___Optional[builtin___bytes] = None, ) -> None: ... if sys.version_info >= (3,): @classmethod - def FromString(cls, s: builtin___bytes) -> SummaryDataPoint: ... + def FromString(cls, s: builtin___bytes) -> DoubleExemplar: ... else: @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> SummaryDataPoint: ... - def ClearField(self, field_name: typing_extensions___Literal[u"count",b"count",u"labels",b"labels",u"percentile_values",b"percentile_values",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... -type___SummaryDataPoint = SummaryDataPoint + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> DoubleExemplar: ... + def ClearField(self, field_name: typing_extensions___Literal[u"filtered_labels",b"filtered_labels",u"span_id",b"span_id",u"time_unix_nano",b"time_unix_nano",u"trace_id",b"trace_id",u"value",b"value"]) -> None: ... +type___DoubleExemplar = DoubleExemplar diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py index 636c441144..7fb57c4bf0 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py @@ -20,7 +20,7 @@ package='opentelemetry.proto.trace.v1', syntax='proto3', serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1', - serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xae\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\"\xa8\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\"\xb3\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"g\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x0c\n\x08INTERNAL\x10\x01\x12\n\n\x06SERVER\x10\x02\x12\n\n\x06\x43LIENT\x10\x03\x12\x0c\n\x08PRODUCER\x10\x04\x12\x0c\n\x08\x43ONSUMER\x10\x05\"\x98\x03\n\x06Status\x12=\n\x04\x63ode\x18\x01 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xbd\x02\n\nStatusCode\x12\x06\n\x02Ok\x10\x00\x12\r\n\tCancelled\x10\x01\x12\x10\n\x0cUnknownError\x10\x02\x12\x13\n\x0fInvalidArgument\x10\x03\x12\x14\n\x10\x44\x65\x61\x64lineExceeded\x10\x04\x12\x0c\n\x08NotFound\x10\x05\x12\x11\n\rAlreadyExists\x10\x06\x12\x14\n\x10PermissionDenied\x10\x07\x12\x15\n\x11ResourceExhausted\x10\x08\x12\x16\n\x12\x46\x61iledPrecondition\x10\t\x12\x0b\n\x07\x41\x62orted\x10\n\x12\x0e\n\nOutOfRange\x10\x0b\x12\x11\n\rUnimplemented\x10\x0c\x12\x11\n\rInternalError\x10\r\x12\x0f\n\x0bUnavailable\x10\x0e\x12\x0c\n\x08\x44\x61taLoss\x10\x0f\x12\x13\n\x0fUnauthenticated\x10\x10\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' + serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xae\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\"\xa8\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xf0\x04\n\x06Status\x12=\n\x04\x63ode\x18\x01 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x95\x04\n\nStatusCode\x12\x12\n\x0eSTATUS_CODE_OK\x10\x00\x12\x19\n\x15STATUS_CODE_CANCELLED\x10\x01\x12\x1d\n\x19STATUS_CODE_UNKNOWN_ERROR\x10\x02\x12 \n\x1cSTATUS_CODE_INVALID_ARGUMENT\x10\x03\x12!\n\x1dSTATUS_CODE_DEADLINE_EXCEEDED\x10\x04\x12\x19\n\x15STATUS_CODE_NOT_FOUND\x10\x05\x12\x1e\n\x1aSTATUS_CODE_ALREADY_EXISTS\x10\x06\x12!\n\x1dSTATUS_CODE_PERMISSION_DENIED\x10\x07\x12\"\n\x1eSTATUS_CODE_RESOURCE_EXHAUSTED\x10\x08\x12#\n\x1fSTATUS_CODE_FAILED_PRECONDITION\x10\t\x12\x17\n\x13STATUS_CODE_ABORTED\x10\n\x12\x1c\n\x18STATUS_CODE_OUT_OF_RANGE\x10\x0b\x12\x1d\n\x19STATUS_CODE_UNIMPLEMENTED\x10\x0c\x12\x1e\n\x1aSTATUS_CODE_INTERNAL_ERROR\x10\r\x12\x1b\n\x17STATUS_CODE_UNAVAILABLE\x10\x0e\x12\x19\n\x15STATUS_CODE_DATA_LOSS\x10\x0f\x12\x1f\n\x1bSTATUS_CODE_UNAUTHENTICATED\x10\x10\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -37,30 +37,30 @@ serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='INTERNAL', index=1, number=1, + name='SPAN_KIND_INTERNAL', index=1, number=1, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='SERVER', index=2, number=2, + name='SPAN_KIND_SERVER', index=2, number=2, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='CLIENT', index=3, number=3, + name='SPAN_KIND_CLIENT', index=3, number=3, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='PRODUCER', index=4, number=4, + name='SPAN_KIND_PRODUCER', index=4, number=4, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='CONSUMER', index=5, number=5, + name='SPAN_KIND_CONSUMER', index=5, number=5, serialized_options=None, type=None), ], containing_type=None, serialized_options=None, - serialized_start=1359, - serialized_end=1462, + serialized_start=1360, + serialized_end=1513, ) _sym_db.RegisterEnumDescriptor(_SPAN_SPANKIND) @@ -71,78 +71,78 @@ file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( - name='Ok', index=0, number=0, + name='STATUS_CODE_OK', index=0, number=0, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='Cancelled', index=1, number=1, + name='STATUS_CODE_CANCELLED', index=1, number=1, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='UnknownError', index=2, number=2, + name='STATUS_CODE_UNKNOWN_ERROR', index=2, number=2, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='InvalidArgument', index=3, number=3, + name='STATUS_CODE_INVALID_ARGUMENT', index=3, number=3, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='DeadlineExceeded', index=4, number=4, + name='STATUS_CODE_DEADLINE_EXCEEDED', index=4, number=4, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='NotFound', index=5, number=5, + name='STATUS_CODE_NOT_FOUND', index=5, number=5, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='AlreadyExists', index=6, number=6, + name='STATUS_CODE_ALREADY_EXISTS', index=6, number=6, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='PermissionDenied', index=7, number=7, + name='STATUS_CODE_PERMISSION_DENIED', index=7, number=7, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='ResourceExhausted', index=8, number=8, + name='STATUS_CODE_RESOURCE_EXHAUSTED', index=8, number=8, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='FailedPrecondition', index=9, number=9, + name='STATUS_CODE_FAILED_PRECONDITION', index=9, number=9, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='Aborted', index=10, number=10, + name='STATUS_CODE_ABORTED', index=10, number=10, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='OutOfRange', index=11, number=11, + name='STATUS_CODE_OUT_OF_RANGE', index=11, number=11, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='Unimplemented', index=12, number=12, + name='STATUS_CODE_UNIMPLEMENTED', index=12, number=12, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='InternalError', index=13, number=13, + name='STATUS_CODE_INTERNAL_ERROR', index=13, number=13, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='Unavailable', index=14, number=14, + name='STATUS_CODE_UNAVAILABLE', index=14, number=14, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='DataLoss', index=15, number=15, + name='STATUS_CODE_DATA_LOSS', index=15, number=15, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='Unauthenticated', index=16, number=16, + name='STATUS_CODE_UNAUTHENTICATED', index=16, number=16, serialized_options=None, type=None), ], containing_type=None, serialized_options=None, - serialized_start=1556, - serialized_end=1873, + serialized_start=1607, + serialized_end=2140, ) _sym_db.RegisterEnumDescriptor(_STATUS_STATUSCODE) @@ -458,7 +458,7 @@ oneofs=[ ], serialized_start=515, - serialized_end=1462, + serialized_end=1513, ) @@ -496,8 +496,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1465, - serialized_end=1873, + serialized_start=1516, + serialized_end=2140, ) _RESOURCESPANS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi index e0726557d6..ad2c016705 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi @@ -116,17 +116,17 @@ class Span(google___protobuf___message___Message): @classmethod def items(cls) -> typing___List[typing___Tuple[builtin___str, Span.SpanKindValue]]: ... SPAN_KIND_UNSPECIFIED = typing___cast(Span.SpanKindValue, 0) - INTERNAL = typing___cast(Span.SpanKindValue, 1) - SERVER = typing___cast(Span.SpanKindValue, 2) - CLIENT = typing___cast(Span.SpanKindValue, 3) - PRODUCER = typing___cast(Span.SpanKindValue, 4) - CONSUMER = typing___cast(Span.SpanKindValue, 5) + SPAN_KIND_INTERNAL = typing___cast(Span.SpanKindValue, 1) + SPAN_KIND_SERVER = typing___cast(Span.SpanKindValue, 2) + SPAN_KIND_CLIENT = typing___cast(Span.SpanKindValue, 3) + SPAN_KIND_PRODUCER = typing___cast(Span.SpanKindValue, 4) + SPAN_KIND_CONSUMER = typing___cast(Span.SpanKindValue, 5) SPAN_KIND_UNSPECIFIED = typing___cast(Span.SpanKindValue, 0) - INTERNAL = typing___cast(Span.SpanKindValue, 1) - SERVER = typing___cast(Span.SpanKindValue, 2) - CLIENT = typing___cast(Span.SpanKindValue, 3) - PRODUCER = typing___cast(Span.SpanKindValue, 4) - CONSUMER = typing___cast(Span.SpanKindValue, 5) + SPAN_KIND_INTERNAL = typing___cast(Span.SpanKindValue, 1) + SPAN_KIND_SERVER = typing___cast(Span.SpanKindValue, 2) + SPAN_KIND_CLIENT = typing___cast(Span.SpanKindValue, 3) + SPAN_KIND_PRODUCER = typing___cast(Span.SpanKindValue, 4) + SPAN_KIND_CONSUMER = typing___cast(Span.SpanKindValue, 5) type___SpanKind = SpanKind class Event(google___protobuf___message___Message): @@ -249,40 +249,40 @@ class Status(google___protobuf___message___Message): def values(cls) -> typing___List[Status.StatusCodeValue]: ... @classmethod def items(cls) -> typing___List[typing___Tuple[builtin___str, Status.StatusCodeValue]]: ... - Ok = typing___cast(Status.StatusCodeValue, 0) - Cancelled = typing___cast(Status.StatusCodeValue, 1) - UnknownError = typing___cast(Status.StatusCodeValue, 2) - InvalidArgument = typing___cast(Status.StatusCodeValue, 3) - DeadlineExceeded = typing___cast(Status.StatusCodeValue, 4) - NotFound = typing___cast(Status.StatusCodeValue, 5) - AlreadyExists = typing___cast(Status.StatusCodeValue, 6) - PermissionDenied = typing___cast(Status.StatusCodeValue, 7) - ResourceExhausted = typing___cast(Status.StatusCodeValue, 8) - FailedPrecondition = typing___cast(Status.StatusCodeValue, 9) - Aborted = typing___cast(Status.StatusCodeValue, 10) - OutOfRange = typing___cast(Status.StatusCodeValue, 11) - Unimplemented = typing___cast(Status.StatusCodeValue, 12) - InternalError = typing___cast(Status.StatusCodeValue, 13) - Unavailable = typing___cast(Status.StatusCodeValue, 14) - DataLoss = typing___cast(Status.StatusCodeValue, 15) - Unauthenticated = typing___cast(Status.StatusCodeValue, 16) - Ok = typing___cast(Status.StatusCodeValue, 0) - Cancelled = typing___cast(Status.StatusCodeValue, 1) - UnknownError = typing___cast(Status.StatusCodeValue, 2) - InvalidArgument = typing___cast(Status.StatusCodeValue, 3) - DeadlineExceeded = typing___cast(Status.StatusCodeValue, 4) - NotFound = typing___cast(Status.StatusCodeValue, 5) - AlreadyExists = typing___cast(Status.StatusCodeValue, 6) - PermissionDenied = typing___cast(Status.StatusCodeValue, 7) - ResourceExhausted = typing___cast(Status.StatusCodeValue, 8) - FailedPrecondition = typing___cast(Status.StatusCodeValue, 9) - Aborted = typing___cast(Status.StatusCodeValue, 10) - OutOfRange = typing___cast(Status.StatusCodeValue, 11) - Unimplemented = typing___cast(Status.StatusCodeValue, 12) - InternalError = typing___cast(Status.StatusCodeValue, 13) - Unavailable = typing___cast(Status.StatusCodeValue, 14) - DataLoss = typing___cast(Status.StatusCodeValue, 15) - Unauthenticated = typing___cast(Status.StatusCodeValue, 16) + STATUS_CODE_OK = typing___cast(Status.StatusCodeValue, 0) + STATUS_CODE_CANCELLED = typing___cast(Status.StatusCodeValue, 1) + STATUS_CODE_UNKNOWN_ERROR = typing___cast(Status.StatusCodeValue, 2) + STATUS_CODE_INVALID_ARGUMENT = typing___cast(Status.StatusCodeValue, 3) + STATUS_CODE_DEADLINE_EXCEEDED = typing___cast(Status.StatusCodeValue, 4) + STATUS_CODE_NOT_FOUND = typing___cast(Status.StatusCodeValue, 5) + STATUS_CODE_ALREADY_EXISTS = typing___cast(Status.StatusCodeValue, 6) + STATUS_CODE_PERMISSION_DENIED = typing___cast(Status.StatusCodeValue, 7) + STATUS_CODE_RESOURCE_EXHAUSTED = typing___cast(Status.StatusCodeValue, 8) + STATUS_CODE_FAILED_PRECONDITION = typing___cast(Status.StatusCodeValue, 9) + STATUS_CODE_ABORTED = typing___cast(Status.StatusCodeValue, 10) + STATUS_CODE_OUT_OF_RANGE = typing___cast(Status.StatusCodeValue, 11) + STATUS_CODE_UNIMPLEMENTED = typing___cast(Status.StatusCodeValue, 12) + STATUS_CODE_INTERNAL_ERROR = typing___cast(Status.StatusCodeValue, 13) + STATUS_CODE_UNAVAILABLE = typing___cast(Status.StatusCodeValue, 14) + STATUS_CODE_DATA_LOSS = typing___cast(Status.StatusCodeValue, 15) + STATUS_CODE_UNAUTHENTICATED = typing___cast(Status.StatusCodeValue, 16) + STATUS_CODE_OK = typing___cast(Status.StatusCodeValue, 0) + STATUS_CODE_CANCELLED = typing___cast(Status.StatusCodeValue, 1) + STATUS_CODE_UNKNOWN_ERROR = typing___cast(Status.StatusCodeValue, 2) + STATUS_CODE_INVALID_ARGUMENT = typing___cast(Status.StatusCodeValue, 3) + STATUS_CODE_DEADLINE_EXCEEDED = typing___cast(Status.StatusCodeValue, 4) + STATUS_CODE_NOT_FOUND = typing___cast(Status.StatusCodeValue, 5) + STATUS_CODE_ALREADY_EXISTS = typing___cast(Status.StatusCodeValue, 6) + STATUS_CODE_PERMISSION_DENIED = typing___cast(Status.StatusCodeValue, 7) + STATUS_CODE_RESOURCE_EXHAUSTED = typing___cast(Status.StatusCodeValue, 8) + STATUS_CODE_FAILED_PRECONDITION = typing___cast(Status.StatusCodeValue, 9) + STATUS_CODE_ABORTED = typing___cast(Status.StatusCodeValue, 10) + STATUS_CODE_OUT_OF_RANGE = typing___cast(Status.StatusCodeValue, 11) + STATUS_CODE_UNIMPLEMENTED = typing___cast(Status.StatusCodeValue, 12) + STATUS_CODE_INTERNAL_ERROR = typing___cast(Status.StatusCodeValue, 13) + STATUS_CODE_UNAVAILABLE = typing___cast(Status.StatusCodeValue, 14) + STATUS_CODE_DATA_LOSS = typing___cast(Status.StatusCodeValue, 15) + STATUS_CODE_UNAUTHENTICATED = typing___cast(Status.StatusCodeValue, 16) type___StatusCode = StatusCode code: type___Status.StatusCodeValue = ... diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index f7742db6f6..e06a8a9320 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -12,7 +12,7 @@ # PROTO_REPO_DIR - the path to an existing checkout of the opentelemetry-proto repo # Pinned commit/branch/tag for the current version used in opentelemetry-proto python package. -PROTO_REPO_BRANCH_OR_COMMIT="v0.4.0" +PROTO_REPO_BRANCH_OR_COMMIT="v0.5.0" set -e From 10e2583fb5c020207d4bf30d5c61a93db88c90cc Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 30 Sep 2020 12:00:59 -0600 Subject: [PATCH 0586/1517] Move test that imports SDK from the API (#1185) --- opentelemetry-api/tests/trace/test_globals.py | 19 -------------- .../tests/metrics/test_globals.py | 0 opentelemetry-sdk/tests/trace/test_globals.py | 25 +++++++++++++++++++ 3 files changed, 25 insertions(+), 19 deletions(-) rename {opentelemetry-api => opentelemetry-sdk}/tests/metrics/test_globals.py (100%) create mode 100644 opentelemetry-sdk/tests/trace/test_globals.py diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index dc7c02e513..5d1a4c5c38 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -1,9 +1,7 @@ import unittest -from logging import WARNING from unittest.mock import patch from opentelemetry import context, trace -from opentelemetry.sdk.trace import TracerProvider # type:ignore class TestGlobals(unittest.TestCase): @@ -22,23 +20,6 @@ def test_get_tracer(self): trace.get_tracer("foo", "var", mock_provider) mock_provider.get_tracer.assert_called_with("foo", "var") - def test_tracer_provider_override_warning(self): - """trace.set_tracer_provider should throw a warning when overridden""" - trace.set_tracer_provider(TracerProvider()) - tracer_provider = trace.get_tracer_provider() - with self.assertLogs(level=WARNING) as test: - trace.set_tracer_provider(TracerProvider()) - self.assertEqual( - test.output, - [ - ( - "WARNING:opentelemetry.trace:Overriding of current " - "TracerProvider is not allowed" - ) - ], - ) - self.assertIs(tracer_provider, trace.get_tracer_provider()) - class TestTracer(unittest.TestCase): def setUp(self): diff --git a/opentelemetry-api/tests/metrics/test_globals.py b/opentelemetry-sdk/tests/metrics/test_globals.py similarity index 100% rename from opentelemetry-api/tests/metrics/test_globals.py rename to opentelemetry-sdk/tests/metrics/test_globals.py diff --git a/opentelemetry-sdk/tests/trace/test_globals.py b/opentelemetry-sdk/tests/trace/test_globals.py new file mode 100644 index 0000000000..ab57ff018a --- /dev/null +++ b/opentelemetry-sdk/tests/trace/test_globals.py @@ -0,0 +1,25 @@ +# type:ignore +import unittest +from logging import WARNING + +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider # type:ignore + + +class TestGlobals(unittest.TestCase): + def test_tracer_provider_override_warning(self): + """trace.set_tracer_provider should throw a warning when overridden""" + trace.set_tracer_provider(TracerProvider()) + tracer_provider = trace.get_tracer_provider() + with self.assertLogs(level=WARNING) as test: + trace.set_tracer_provider(TracerProvider()) + self.assertEqual( + test.output, + [ + ( + "WARNING:opentelemetry.trace:Overriding of current " + "TracerProvider is not allowed" + ) + ], + ) + self.assertIs(tracer_provider, trace.get_tracer_provider()) From 982b667ab7d32de45ed3b9cd6981f37a9e04975b Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 30 Sep 2020 15:45:19 -0600 Subject: [PATCH 0587/1517] Use right case (#1187) Fixes #1186 --- opentelemetry-api/src/opentelemetry/configuration/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index 286bfa7ac6..bf641ca836 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -43,7 +43,7 @@ For example, if the environment variable ``OTEL_PYTHON_METER_PROVIDER`` value is ``my_meter_provider``, then -``Configuration().meter_provider == "my_meter_provider"`` would be ``True``. +``Configuration().METER_PROVIDER == "my_meter_provider"`` would be ``True``. Non defined attributes will always return ``None``. This is intended to make it easier to use the ``Configuration`` object in actual code, because it won't be From 33f417f76340f8b2cb501c14e490f589d6a459c1 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 1 Oct 2020 08:55:51 -0700 Subject: [PATCH 0588/1517] docs: updating tox target in contributing doc (#1183) The tox targets were updated a while ago, but the contributing doc was not. --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8173bbd46f..39d720d2db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,8 +53,8 @@ You can run: - `tox` to run all existing tox commands, including unit tests for all packages under multiple Python versions - `tox -e docs` to regenerate the API docs -- `tox -e test-api` and `tox -e test-sdk` to run the API and SDK unit tests -- `tox -e py37-test-api` to e.g. run the API unit tests under a specific +- `tox -e test-core-api` and `tox -e test-core-sdk` to run the API and SDK unit tests +- `tox -e py37-test-core-api` to e.g. run the API unit tests under a specific Python version - `tox -e lint` to run lint checks on all code From 0e0c3e3b514a11ea7e190f8141ec53dfdc7ef2b9 Mon Sep 17 00:00:00 2001 From: E Brake Date: Thu, 1 Oct 2020 09:10:03 -0700 Subject: [PATCH 0589/1517] instrumentation/django: prepend opentelemetry middleware instead of append (#1163) --- .../src/opentelemetry/instrumentation/django/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index d9566affc0..a9bd620e77 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -56,7 +56,7 @@ def _instrument(self, **kwargs): if isinstance(settings_middleware, tuple): settings_middleware = list(settings_middleware) - settings_middleware.append(self._opentelemetry_middleware) + settings_middleware.insert(0, self._opentelemetry_middleware) setattr(settings, "MIDDLEWARE", settings_middleware) def _uninstrument(self, **kwargs): From c8df54be5427b8e026fd9d485b484a55d2be480b Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Thu, 1 Oct 2020 14:29:24 -0700 Subject: [PATCH 0590/1517] Implement IdsGenerator interface for TracerProvider and include default RandomIdsGenerator (#1153) --- docs/api/trace.ids_generator.rst | 7 +++ docs/api/trace.rst | 1 + .../tests/test_flask_example.py | 6 +-- .../tests/test_datadog_format.py | 9 ++-- opentelemetry-api/CHANGELOG.md | 2 + .../src/opentelemetry/trace/__init__.py | 3 ++ .../src/opentelemetry/trace/ids_generator.py | 52 +++++++++++++++++++ opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 27 +++------- .../sdk/trace/propagation/b3_format.py | 6 +-- .../tests/trace/propagation/test_b3_format.py | 25 ++++++--- opentelemetry-sdk/tests/trace/test_trace.py | 9 ++-- 12 files changed, 107 insertions(+), 42 deletions(-) create mode 100644 docs/api/trace.ids_generator.rst create mode 100644 opentelemetry-api/src/opentelemetry/trace/ids_generator.py diff --git a/docs/api/trace.ids_generator.rst b/docs/api/trace.ids_generator.rst new file mode 100644 index 0000000000..8f516bb3b1 --- /dev/null +++ b/docs/api/trace.ids_generator.rst @@ -0,0 +1,7 @@ +opentelemetry.trace.ids_generator +================================= + +.. automodule:: opentelemetry.trace.ids_generator + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/api/trace.rst b/docs/api/trace.rst index 65d9b4d8c8..81f11e3dd0 100644 --- a/docs/api/trace.rst +++ b/docs/api/trace.rst @@ -8,6 +8,7 @@ Submodules trace.status trace.span + trace.ids_generator Module contents --------------- diff --git a/docs/examples/opentelemetry-example-app/tests/test_flask_example.py b/docs/examples/opentelemetry-example-app/tests/test_flask_example.py index bc903a9c60..124a79c0ef 100644 --- a/docs/examples/opentelemetry-example-app/tests/test_flask_example.py +++ b/docs/examples/opentelemetry-example-app/tests/test_flask_example.py @@ -21,7 +21,6 @@ import opentelemetry_example_app.flask_example as flask_example from opentelemetry import trace -from opentelemetry.sdk import trace as trace_sdk class TestFlaskExample(unittest.TestCase): @@ -46,7 +45,8 @@ def tearDown(self): self.send_patcher.stop() def test_full_path(self): - trace_id = trace_sdk.generate_trace_id() + ids_generator = trace.RandomIdsGenerator() + trace_id = ids_generator.generate_trace_id() # We need to use the Werkzeug test app because # The headers are injected at the wsgi layer. # The flask test app will not include these, and @@ -58,7 +58,7 @@ def test_full_path(self): headers={ "traceparent": "00-{:032x}-{:016x}-{:02x}".format( trace_id, - trace_sdk.generate_span_id(), + ids_generator.generate_span_id(), trace.TraceFlags.SAMPLED, ) }, diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py index 1a398745b8..994cac2d60 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py @@ -30,11 +30,12 @@ def get_as_list(dict_object, key): class TestDatadogFormat(unittest.TestCase): @classmethod def setUpClass(cls): + ids_generator = trace_api.RandomIdsGenerator() cls.serialized_trace_id = propagator.format_trace_id( - trace.generate_trace_id() + ids_generator.generate_trace_id() ) cls.serialized_parent_id = propagator.format_span_id( - trace.generate_span_id() + ids_generator.generate_span_id() ) cls.serialized_origin = "origin-service" @@ -107,7 +108,7 @@ def test_context_propagation(self): "child", trace_api.SpanContext( parent_context.trace_id, - trace.generate_span_id(), + trace_api.RandomIdsGenerator().generate_span_id(), is_remote=False, trace_flags=parent_context.trace_flags, trace_state=parent_context.trace_state, @@ -152,7 +153,7 @@ def test_sampling_priority_auto_reject(self): "child", trace_api.SpanContext( parent_context.trace_id, - trace.generate_span_id(), + trace_api.RandomIdsGenerator().generate_span_id(), is_remote=False, trace_flags=parent_context.trace_flags, trace_state=parent_context.trace_state, diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 3d0bcd8005..91a822890b 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -6,6 +6,8 @@ ([#1123](https://github.com/open-telemetry/opentelemetry-python/pull/1123)) - Store `int`s as `int`s in the global Configuration object ([#1118](https://github.com/open-telemetry/opentelemetry-python/pull/1118)) +- Allow for Custom Trace and Span IDs Generation - `IdsGenerator` for TracerProvider + ([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153)) ## Version 0.13b0 diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index c3c2098ff8..301314e28e 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -77,6 +77,7 @@ from contextlib import contextmanager from logging import getLogger +from opentelemetry.trace.ids_generator import IdsGenerator, RandomIdsGenerator from opentelemetry.trace.propagation import ( get_current_span, set_span_in_context, @@ -436,6 +437,7 @@ def get_tracer_provider() -> TracerProvider: __all__ = [ "DEFAULT_TRACE_OPTIONS", "DEFAULT_TRACE_STATE", + "IdsGenerator", "INVALID_SPAN", "INVALID_SPAN_CONTEXT", "INVALID_SPAN_ID", @@ -446,6 +448,7 @@ def get_tracer_provider() -> TracerProvider: "Link", "LinkBase", "ParentSpan", + "RandomIdsGenerator", "Span", "SpanContext", "SpanKind", diff --git a/opentelemetry-api/src/opentelemetry/trace/ids_generator.py b/opentelemetry-api/src/opentelemetry/trace/ids_generator.py new file mode 100644 index 0000000000..31e1fee078 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/trace/ids_generator.py @@ -0,0 +1,52 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import random + + +class IdsGenerator(abc.ABC): + @abc.abstractmethod + def generate_span_id(self) -> int: + """Get a new span ID. + + Returns: + A 64-bit int for use as a span ID + """ + + @abc.abstractmethod + def generate_trace_id(self) -> int: + """Get a new trace ID. + + Implementations should at least make the 64 least significant bits + uniformly random. Samplers like the `TraceIdRatioBased` sampler rely on + this randomness to make sampling decisions. + + See `the specification on TraceIdRatioBased `_. + + Returns: + A 128-bit int for use as a trace ID + """ + + +class RandomIdsGenerator(IdsGenerator): + """The default IDs generator for TracerProvider which randomly generates all + bits when generating IDs. + """ + + def generate_span_id(self) -> int: + return random.getrandbits(64) + + def generate_trace_id(self) -> int: + return random.getrandbits(128) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 1688de79b2..7186428288 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -8,6 +8,8 @@ ([#1128](https://github.com/open-telemetry/opentelemetry-python/pull/1128)) - Add support for `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_SCHEDULE_DELAY_MILLIS`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` and `OTEL_BSP_EXPORT_TIMEOUT_MILLIS` environment variables ([#1105](https://github.com/open-telemetry/opentelemetry-python/pull/1120)) +- Allow for Custom Trace and Span IDs Generation - `IdsGenerator` for TracerProvider + ([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153)) ## Version 0.13b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 13819ed35b..0134ec7e77 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -663,24 +663,6 @@ def record_exception(self, exception: Exception) -> None: ) -def generate_span_id() -> int: - """Get a new random span ID. - - Returns: - A random 64-bit int for use as a span ID - """ - return random.getrandbits(64) - - -def generate_trace_id() -> int: - """Get a new random trace ID. - - Returns: - A random 128-bit int for use as a trace ID - """ - return random.getrandbits(128) - - class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`. @@ -733,7 +715,7 @@ def start_span( # pylint: disable=too-many-locals if parent_context is None or not parent_context.is_valid: parent = parent_context = None - trace_id = generate_trace_id() + trace_id = self.source.ids_generator.generate_trace_id() trace_flags = None trace_state = None else: @@ -757,7 +739,7 @@ def start_span( # pylint: disable=too-many-locals ) context = trace_api.SpanContext( trace_id, - generate_span_id(), + self.source.ids_generator.generate_span_id(), is_remote=False, trace_flags=trace_flags, trace_state=trace_state, @@ -826,10 +808,15 @@ def __init__( active_span_processor: Union[ SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor ] = None, + ids_generator: trace_api.IdsGenerator = None, ): self._active_span_processor = ( active_span_processor or SynchronousMultiSpanProcessor() ) + if ids_generator is None: + self.ids_generator = trace_api.RandomIdsGenerator() + else: + self.ids_generator = ids_generator self.resource = resource self.sampler = sampler self._atexit_handler = None diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py index c2b12f33f5..813b6e8560 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py @@ -17,7 +17,6 @@ import opentelemetry.trace as trace from opentelemetry.context import Context -from opentelemetry.sdk.trace import generate_span_id, generate_trace_id from opentelemetry.trace.propagation.textmap import ( Getter, Setter, @@ -103,8 +102,9 @@ def extract( self._trace_id_regex.fullmatch(trace_id) is None or self._span_id_regex.fullmatch(span_id) is None ): - trace_id = generate_trace_id() - span_id = generate_span_id() + ids_generator = trace.get_tracer_provider().ids_generator + trace_id = ids_generator.generate_trace_id() + span_id = ids_generator.generate_span_id() sampled = "0" else: diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index 77834adec9..07b3010087 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -38,7 +38,7 @@ def get_child_parent_new_carrier(old_carrier): "child", trace_api.SpanContext( parent_context.trace_id, - trace.generate_span_id(), + trace_api.RandomIdsGenerator().generate_span_id(), is_remote=False, trace_flags=parent_context.trace_flags, trace_state=parent_context.trace_state, @@ -56,14 +56,15 @@ def get_child_parent_new_carrier(old_carrier): class TestB3Format(unittest.TestCase): @classmethod def setUpClass(cls): + ids_generator = trace_api.RandomIdsGenerator() cls.serialized_trace_id = b3_format.format_trace_id( - trace.generate_trace_id() + ids_generator.generate_trace_id() ) cls.serialized_span_id = b3_format.format_span_id( - trace.generate_span_id() + ids_generator.generate_span_id() ) cls.serialized_parent_id = b3_format.format_span_id( - trace.generate_span_id() + ids_generator.generate_span_id() ) def test_extract_multi_header(self): @@ -246,8 +247,12 @@ def test_missing_trace_id(self): span_context = trace_api.get_current_span(ctx).get_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) - @patch("opentelemetry.sdk.trace.propagation.b3_format.generate_trace_id") - @patch("opentelemetry.sdk.trace.propagation.b3_format.generate_span_id") + @patch( + "opentelemetry.sdk.trace.propagation.b3_format.trace.RandomIdsGenerator.generate_trace_id" + ) + @patch( + "opentelemetry.sdk.trace.propagation.b3_format.trace.RandomIdsGenerator.generate_span_id" + ) def test_invalid_trace_id( self, mock_generate_span_id, mock_generate_trace_id ): @@ -268,8 +273,12 @@ def test_invalid_trace_id( self.assertEqual(span_context.trace_id, 1) self.assertEqual(span_context.span_id, 2) - @patch("opentelemetry.sdk.trace.propagation.b3_format.generate_trace_id") - @patch("opentelemetry.sdk.trace.propagation.b3_format.generate_span_id") + @patch( + "opentelemetry.sdk.trace.propagation.b3_format.trace.RandomIdsGenerator.generate_trace_id" + ) + @patch( + "opentelemetry.sdk.trace.propagation.b3_format.trace.RandomIdsGenerator.generate_span_id" + ) def test_invalid_span_id( self, mock_generate_span_id, mock_generate_trace_id ): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index fdf85ef19b..8c5544578d 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -615,14 +615,15 @@ def test_invalid_event_attributes(self): self.assertEqual(root.events[3].attributes, {"attr2": (1, 2)}) def test_links(self): + ids_generator = trace_api.RandomIdsGenerator() other_context1 = trace_api.SpanContext( - trace_id=trace.generate_trace_id(), - span_id=trace.generate_span_id(), + trace_id=ids_generator.generate_trace_id(), + span_id=ids_generator.generate_span_id(), is_remote=False, ) other_context2 = trace_api.SpanContext( - trace_id=trace.generate_trace_id(), - span_id=trace.generate_span_id(), + trace_id=ids_generator.generate_trace_id(), + span_id=ids_generator.generate_span_id(), is_remote=False, ) From 308a1a926e7ac983672fb5ad29fd2a8fb939f55b Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 2 Oct 2020 08:00:14 -0700 Subject: [PATCH 0591/1517] meta: adding owais as approver (#1192) Co-authored-by: Leighton Chen --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3cea35f45a..7c714e717c 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Chris Kleinknecht](https://github.com/c24t), Google - [Diego Hurtado](https://github.com/ocelotl) - [Hector Hernandez](https://github.com/hectorhdzg), Microsoft +- [Owais Lone](https://github.com/owais), Splunk - [Reiley Yang](https://github.com/reyang), Microsoft - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google From 14fad78d2b5af53f1957173b7d9fd3b23d5558f3 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 2 Oct 2020 08:22:52 -0700 Subject: [PATCH 0592/1517] update baggage propagation header (#1194) --- opentelemetry-api/CHANGELOG.md | 2 ++ .../src/opentelemetry/baggage/propagation/__init__.py | 6 ++---- .../tests/baggage/test_baggage_propagation.py | 4 ++-- .../tests/propagators/test_global_httptextformat.py | 9 ++++----- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 91a822890b..434bae6404 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -8,6 +8,8 @@ ([#1118](https://github.com/open-telemetry/opentelemetry-python/pull/1118)) - Allow for Custom Trace and Span IDs Generation - `IdsGenerator` for TracerProvider ([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153)) +- Update baggage propagation header + ([#1194](https://github.com/open-telemetry/opentelemetry-python/pull/1194)) ## Version 0.13b0 diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index fb14ab9567..d0920e6859 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -25,7 +25,7 @@ class BaggagePropagator(textmap.TextMapPropagator): MAX_HEADER_LENGTH = 8192 MAX_PAIR_LENGTH = 4096 MAX_PAIRS = 180 - _BAGGAGE_HEADER_NAME = "otcorrelations" + _BAGGAGE_HEADER_NAME = "baggage" def extract( self, @@ -85,9 +85,7 @@ def inject( return baggage_string = _format_baggage(baggage_entries) - set_in_carrier( - carrier, self._BAGGAGE_HEADER_NAME, baggage_string, - ) + set_in_carrier(carrier, self._BAGGAGE_HEADER_NAME, baggage_string) def _format_baggage(baggage_entries: typing.Mapping[str, object]) -> str: diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py index e8bd45d065..d5c16ead5d 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py @@ -32,7 +32,7 @@ def setUp(self): def _extract(self, header_value): """Test helper""" - header = {"otcorrelations": [header_value]} + header = {"baggage": [header_value]} return baggage.get_all(self.propagator.extract(get_as_list, header)) def _inject(self, values): @@ -42,7 +42,7 @@ def _inject(self, values): ctx = baggage.set_baggage(k, v, context=ctx) output = {} self.propagator.inject(dict.__setitem__, output, context=ctx) - return output.get("otcorrelations") + return output.get("baggage") def test_no_context_header(self): baggage_entries = baggage.get_all( diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index a1c58a4c3a..9a97b28129 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -28,8 +28,7 @@ def get_as_list( class TestDefaultGlobalPropagator(unittest.TestCase): - """Test ensures the default global composite propagator works as intended - """ + """Test ensures the default global composite propagator works as intended""" TRACE_ID = int("12345678901234567890123456789012", 16) # type:int SPAN_ID = int("1234567890123456", 16) # type:int @@ -41,7 +40,7 @@ def test_propagation(self): ) tracestate_value = "foo=1,bar=2,baz=3" headers = { - "otcorrelations": ["key1=val1,key2=val2"], + "baggage": ["key1=val1,key2=val2"], "traceparent": [traceparent_value], "tracestate": [tracestate_value], } @@ -61,8 +60,8 @@ def test_propagation(self): output = {} inject(dict.__setitem__, output, context=ctx) self.assertEqual(traceparent_value, output["traceparent"]) - self.assertIn("key3=val3", output["otcorrelations"]) - self.assertIn("key4=val4", output["otcorrelations"]) + self.assertIn("key3=val3", output["baggage"]) + self.assertIn("key4=val4", output["baggage"]) self.assertIn("foo=1", output["tracestate"]) self.assertIn("bar=2", output["tracestate"]) self.assertIn("baz=3", output["tracestate"]) From 568641f5a0c06a9fe5bf29f025f03a8e12a3602a Mon Sep 17 00:00:00 2001 From: Jason Liu Date: Mon, 5 Oct 2020 09:47:01 -0700 Subject: [PATCH 0593/1517] Make Instances of SpanContext Immutable (#1134) --- opentelemetry-api/CHANGELOG.md | 2 + .../src/opentelemetry/trace/span.py | 61 +++++++++++++++---- .../tests/trace/test_immutablespancontext.py | 58 ++++++++++++++++++ 3 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 opentelemetry-api/tests/trace/test_immutablespancontext.py diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 434bae6404..adccf96cdf 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -10,6 +10,8 @@ ([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153)) - Update baggage propagation header ([#1194](https://github.com/open-telemetry/opentelemetry-python/pull/1194)) +- Make instances of SpanContext immutable + ([#1134](https://github.com/open-telemetry/opentelemetry-python/pull/1134)) ## Version 0.13b0 diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 27bbc22336..99620ed144 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -1,10 +1,13 @@ import abc +import logging import types as python_types import typing from opentelemetry.trace.status import Status from opentelemetry.util import types +_logger = logging.getLogger(__name__) + class Span(abc.ABC): """A span represents a single operation within a trace.""" @@ -143,7 +146,9 @@ def get_default(cls) -> "TraceState": DEFAULT_TRACE_STATE = TraceState.get_default() -class SpanContext: +class SpanContext( + typing.Tuple[int, int, bool, "TraceFlags", "TraceState", bool] +): """The state of a Span to propagate between processes. This class includes the immutable attributes of a :class:`.Span` that must @@ -157,26 +162,58 @@ class SpanContext: is_remote: True if propagated from a remote parent. """ - def __init__( - self, + def __new__( + cls, trace_id: int, span_id: int, is_remote: bool, trace_flags: "TraceFlags" = DEFAULT_TRACE_OPTIONS, trace_state: "TraceState" = DEFAULT_TRACE_STATE, - ) -> None: + ) -> "SpanContext": if trace_flags is None: trace_flags = DEFAULT_TRACE_OPTIONS if trace_state is None: trace_state = DEFAULT_TRACE_STATE - self.trace_id = trace_id - self.span_id = span_id - self.trace_flags = trace_flags - self.trace_state = trace_state - self.is_remote = is_remote - self.is_valid = ( - self.trace_id != INVALID_TRACE_ID - and self.span_id != INVALID_SPAN_ID + + is_valid = trace_id != INVALID_TRACE_ID and span_id != INVALID_SPAN_ID + + return tuple.__new__( + cls, + (trace_id, span_id, is_remote, trace_flags, trace_state, is_valid), + ) + + @property + def trace_id(self) -> int: + return self[0] # pylint: disable=unsubscriptable-object + + @property + def span_id(self) -> int: + return self[1] # pylint: disable=unsubscriptable-object + + @property + def is_remote(self) -> bool: + return self[2] # pylint: disable=unsubscriptable-object + + @property + def trace_flags(self) -> "TraceFlags": + return self[3] # pylint: disable=unsubscriptable-object + + @property + def trace_state(self) -> "TraceState": + return self[4] # pylint: disable=unsubscriptable-object + + @property + def is_valid(self) -> bool: + return self[5] # pylint: disable=unsubscriptable-object + + def __setattr__(self, *args: str) -> None: + _logger.debug( + "Immutable type, ignoring call to set attribute", stack_info=True + ) + + def __delattr__(self, *args: str) -> None: + _logger.debug( + "Immutable type, ignoring call to set attribute", stack_info=True ) def __repr__(self) -> str: diff --git a/opentelemetry-api/tests/trace/test_immutablespancontext.py b/opentelemetry-api/tests/trace/test_immutablespancontext.py new file mode 100644 index 0000000000..7e98470e13 --- /dev/null +++ b/opentelemetry-api/tests/trace/test_immutablespancontext.py @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import trace +from opentelemetry.trace import TraceFlags, TraceState + + +class TestImmutableSpanContext(unittest.TestCase): + def test_ctor(self): + context = trace.SpanContext( + 1, + 1, + is_remote=False, + trace_flags=trace.DEFAULT_TRACE_OPTIONS, + trace_state=trace.DEFAULT_TRACE_STATE, + ) + + self.assertEqual(context.trace_id, 1) + self.assertEqual(context.span_id, 1) + self.assertEqual(context.is_remote, False) + self.assertEqual(context.trace_flags, trace.DEFAULT_TRACE_OPTIONS) + self.assertEqual(context.trace_state, trace.DEFAULT_TRACE_STATE) + + def test_attempt_change_attributes(self): + context = trace.SpanContext( + 1, + 2, + is_remote=False, + trace_flags=trace.DEFAULT_TRACE_OPTIONS, + trace_state=trace.DEFAULT_TRACE_STATE, + ) + + # attempt to change the attribute values + context.trace_id = 2 # type: ignore + context.span_id = 3 # type: ignore + context.is_remote = True # type: ignore + context.trace_flags = TraceFlags(3) # type: ignore + context.trace_state = TraceState([("test", "test")]) # type: ignore + + # check if attributes changed + self.assertEqual(context.trace_id, 1) + self.assertEqual(context.span_id, 2) + self.assertEqual(context.is_remote, False) + self.assertEqual(context.trace_flags, trace.DEFAULT_TRACE_OPTIONS) + self.assertEqual(context.trace_state, trace.DEFAULT_TRACE_STATE) From b565d6b643f175faee3f57ef81c8b7edbf50ec41 Mon Sep 17 00:00:00 2001 From: Prajilesh N Date: Tue, 6 Oct 2020 03:52:54 +0530 Subject: [PATCH 0594/1517] docs: updated broken Propagation API Spec link in propagators __init__.py (#1198) --- opentelemetry-api/src/opentelemetry/propagators/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index c274b19f8a..18c322f793 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -65,7 +65,7 @@ def example_route(): .. _Propagation API Specification: - https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md + https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/context/api-propagators.md """ import typing From f9218ca7c5365ac91cc7c8d8c72c55c988939c8b Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 6 Oct 2020 08:23:27 -0700 Subject: [PATCH 0595/1517] Fix issue when metrics are not available (#1207) Not all configured metrics are available on all operating systems, added a hasattr call before calling the observer. --- .../CHANGELOG.md | 3 + .../system_metrics/__init__.py | 189 ++++++++++-------- 2 files changed, 107 insertions(+), 85 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md index 7a655ad231..7405cd91f7 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Fix issue when specific metrics are not available in certain OS + ([#1207](https://github.com/open-telemetry/opentelemetry-python/pull/1207)) + ## Version 0.13b0 Released 2020-09-17 diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py index 9ca36d00b2..71935aa8d9 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py @@ -372,11 +372,12 @@ def _get_system_cpu_time(self, observer: metrics.ValueObserver) -> None: """ for cpu, times in enumerate(psutil.cpu_times(percpu=True)): for metric in self._config["system.cpu.time"]: - self._system_cpu_time_labels["state"] = metric - self._system_cpu_time_labels["cpu"] = cpu + 1 - observer.observe( - getattr(times, metric), self._system_cpu_time_labels - ) + if hasattr(times, metric): + self._system_cpu_time_labels["state"] = metric + self._system_cpu_time_labels["cpu"] = cpu + 1 + observer.observe( + getattr(times, metric), self._system_cpu_time_labels + ) def _get_system_cpu_utilization( self, observer: metrics.ValueObserver @@ -391,12 +392,13 @@ def _get_system_cpu_utilization( psutil.cpu_times_percent(percpu=True) ): for metric in self._config["system.cpu.utilization"]: - self._system_cpu_utilization_labels["state"] = metric - self._system_cpu_utilization_labels["cpu"] = cpu + 1 - observer.observe( - getattr(times_percent, metric) / 100, - self._system_cpu_utilization_labels, - ) + if hasattr(times_percent, metric): + self._system_cpu_utilization_labels["state"] = metric + self._system_cpu_utilization_labels["cpu"] = cpu + 1 + observer.observe( + getattr(times_percent, metric) / 100, + self._system_cpu_utilization_labels, + ) def _get_system_memory_usage( self, observer: metrics.ValueObserver @@ -409,10 +411,11 @@ def _get_system_memory_usage( virtual_memory = psutil.virtual_memory() for metric in self._config["system.memory.usage"]: self._system_memory_usage_labels["state"] = metric - observer.observe( - getattr(virtual_memory, metric), - self._system_memory_usage_labels, - ) + if hasattr(virtual_memory, metric): + observer.observe( + getattr(virtual_memory, metric), + self._system_memory_usage_labels, + ) def _get_system_memory_utilization( self, observer: metrics.ValueObserver @@ -426,10 +429,11 @@ def _get_system_memory_utilization( for metric in self._config["system.memory.utilization"]: self._system_memory_utilization_labels["state"] = metric - observer.observe( - getattr(system_memory, metric) / system_memory.total, - self._system_memory_utilization_labels, - ) + if hasattr(system_memory, metric): + observer.observe( + getattr(system_memory, metric) / system_memory.total, + self._system_memory_utilization_labels, + ) def _get_system_swap_usage(self, observer: metrics.ValueObserver) -> None: """Observer callback for swap usage @@ -441,9 +445,11 @@ def _get_system_swap_usage(self, observer: metrics.ValueObserver) -> None: for metric in self._config["system.swap.usage"]: self._system_swap_usage_labels["state"] = metric - observer.observe( - getattr(system_swap, metric), self._system_swap_usage_labels - ) + if hasattr(system_swap, metric): + observer.observe( + getattr(system_swap, metric), + self._system_swap_usage_labels, + ) def _get_system_swap_utilization( self, observer: metrics.ValueObserver @@ -456,11 +462,12 @@ def _get_system_swap_utilization( system_swap = psutil.swap_memory() for metric in self._config["system.swap.utilization"]: - self._system_swap_utilization_labels["state"] = metric - observer.observe( - getattr(system_swap, metric) / system_swap.total, - self._system_swap_utilization_labels, - ) + if hasattr(system_swap, metric): + self._system_swap_utilization_labels["state"] = metric + observer.observe( + getattr(system_swap, metric) / system_swap.total, + self._system_swap_utilization_labels, + ) # TODO Add _get_system_swap_page_faults # TODO Add _get_system_swap_page_operations @@ -473,12 +480,13 @@ def _get_system_disk_io(self, observer: metrics.SumObserver) -> None: """ for device, counters in psutil.disk_io_counters(perdisk=True).items(): for metric in self._config["system.disk.io"]: - self._system_disk_io_labels["device"] = device - self._system_disk_io_labels["direction"] = metric - observer.observe( - getattr(counters, "{}_bytes".format(metric)), - self._system_disk_io_labels, - ) + if hasattr(counters, "{}_bytes".format(metric)): + self._system_disk_io_labels["device"] = device + self._system_disk_io_labels["direction"] = metric + observer.observe( + getattr(counters, "{}_bytes".format(metric)), + self._system_disk_io_labels, + ) def _get_system_disk_operations( self, observer: metrics.SumObserver @@ -490,12 +498,13 @@ def _get_system_disk_operations( """ for device, counters in psutil.disk_io_counters(perdisk=True).items(): for metric in self._config["system.disk.operations"]: - self._system_disk_operations_labels["device"] = device - self._system_disk_operations_labels["direction"] = metric - observer.observe( - getattr(counters, "{}_count".format(metric)), - self._system_disk_operations_labels, - ) + if hasattr(counters, "{}_count".format(metric)): + self._system_disk_operations_labels["device"] = device + self._system_disk_operations_labels["direction"] = metric + observer.observe( + getattr(counters, "{}_count".format(metric)), + self._system_disk_operations_labels, + ) def _get_system_disk_time(self, observer: metrics.SumObserver) -> None: """Observer callback for disk time @@ -505,12 +514,13 @@ def _get_system_disk_time(self, observer: metrics.SumObserver) -> None: """ for device, counters in psutil.disk_io_counters(perdisk=True).items(): for metric in self._config["system.disk.time"]: - self._system_disk_time_labels["device"] = device - self._system_disk_time_labels["direction"] = metric - observer.observe( - getattr(counters, "{}_time".format(metric)) / 1000, - self._system_disk_time_labels, - ) + if hasattr(counters, "{}_time".format(metric)): + self._system_disk_time_labels["device"] = device + self._system_disk_time_labels["direction"] = metric + observer.observe( + getattr(counters, "{}_time".format(metric)) / 1000, + self._system_disk_time_labels, + ) def _get_system_disk_merged(self, observer: metrics.SumObserver) -> None: """Observer callback for disk merged operations @@ -524,12 +534,13 @@ def _get_system_disk_merged(self, observer: metrics.SumObserver) -> None: for device, counters in psutil.disk_io_counters(perdisk=True).items(): for metric in self._config["system.disk.time"]: - self._system_disk_merged_labels["device"] = device - self._system_disk_merged_labels["direction"] = metric - observer.observe( - getattr(counters, "{}_merged_count".format(metric)), - self._system_disk_merged_labels, - ) + if hasattr(counters, "{}_merged_count".format(metric)): + self._system_disk_merged_labels["device"] = device + self._system_disk_merged_labels["direction"] = metric + observer.observe( + getattr(counters, "{}_merged_count".format(metric)), + self._system_disk_merged_labels, + ) # TODO Add _get_system_filesystem_usage # TODO Add _get_system_filesystem_utilization @@ -548,14 +559,17 @@ def _get_system_network_dropped_packets( for device, counters in psutil.net_io_counters(pernic=True).items(): for metric in self._config["system.network.dropped.packets"]: in_out = {"receive": "in", "transmit": "out"}[metric] - self._system_network_dropped_packets_labels["device"] = device - self._system_network_dropped_packets_labels[ - "direction" - ] = metric - observer.observe( - getattr(counters, "drop{}".format(in_out)), - self._system_network_dropped_packets_labels, - ) + if hasattr(counters, "drop{}".format(in_out)): + self._system_network_dropped_packets_labels[ + "device" + ] = device + self._system_network_dropped_packets_labels[ + "direction" + ] = metric + observer.observe( + getattr(counters, "drop{}".format(in_out)), + self._system_network_dropped_packets_labels, + ) def _get_system_network_packets( self, observer: metrics.SumObserver @@ -569,12 +583,13 @@ def _get_system_network_packets( for device, counters in psutil.net_io_counters(pernic=True).items(): for metric in self._config["system.network.dropped.packets"]: recv_sent = {"receive": "recv", "transmit": "sent"}[metric] - self._system_network_packets_labels["device"] = device - self._system_network_packets_labels["direction"] = metric - observer.observe( - getattr(counters, "packets_{}".format(recv_sent)), - self._system_network_packets_labels, - ) + if hasattr(counters, "packets_{}".format(recv_sent)): + self._system_network_packets_labels["device"] = device + self._system_network_packets_labels["direction"] = metric + observer.observe( + getattr(counters, "packets_{}".format(recv_sent)), + self._system_network_packets_labels, + ) def _get_system_network_errors( self, observer: metrics.SumObserver @@ -587,12 +602,13 @@ def _get_system_network_errors( for device, counters in psutil.net_io_counters(pernic=True).items(): for metric in self._config["system.network.errors"]: in_out = {"receive": "in", "transmit": "out"}[metric] - self._system_network_errors_labels["device"] = device - self._system_network_errors_labels["direction"] = metric - observer.observe( - getattr(counters, "err{}".format(in_out)), - self._system_network_errors_labels, - ) + if hasattr(counters, "err{}".format(in_out)): + self._system_network_errors_labels["device"] = device + self._system_network_errors_labels["direction"] = metric + observer.observe( + getattr(counters, "err{}".format(in_out)), + self._system_network_errors_labels, + ) def _get_system_network_io(self, observer: metrics.SumObserver) -> None: """Observer callback for network IO @@ -604,12 +620,13 @@ def _get_system_network_io(self, observer: metrics.SumObserver) -> None: for device, counters in psutil.net_io_counters(pernic=True).items(): for metric in self._config["system.network.dropped.packets"]: recv_sent = {"receive": "recv", "transmit": "sent"}[metric] - self._system_network_io_labels["device"] = device - self._system_network_io_labels["direction"] = metric - observer.observe( - getattr(counters, "bytes_{}".format(recv_sent)), - self._system_network_io_labels, - ) + if hasattr(counters, "bytes_{}".format(recv_sent)): + self._system_network_io_labels["device"] = device + self._system_network_io_labels["direction"] = metric + observer.observe( + getattr(counters, "bytes_{}".format(recv_sent)), + self._system_network_io_labels, + ) def _get_system_network_connections( self, observer: metrics.UpDownSumObserver @@ -662,10 +679,11 @@ def _get_runtime_memory(self, observer: metrics.SumObserver) -> None: """ proc_memory = self._proc.memory_info() for metric in self._config["runtime.memory"]: - self._runtime_memory_labels["type"] = metric - observer.observe( - getattr(proc_memory, metric), self._runtime_memory_labels, - ) + if hasattr(proc_memory, metric): + self._runtime_memory_labels["type"] = metric + observer.observe( + getattr(proc_memory, metric), self._runtime_memory_labels, + ) def _get_runtime_cpu_time(self, observer: metrics.SumObserver) -> None: """Observer callback for runtime CPU time @@ -675,10 +693,11 @@ def _get_runtime_cpu_time(self, observer: metrics.SumObserver) -> None: """ proc_cpu = self._proc.cpu_times() for metric in self._config["runtime.cpu.time"]: - self._runtime_cpu_time_labels["type"] = metric - observer.observe( - getattr(proc_cpu, metric), self._runtime_cpu_time_labels, - ) + if hasattr(proc_cpu, metric): + self._runtime_cpu_time_labels["type"] = metric + observer.observe( + getattr(proc_cpu, metric), self._runtime_cpu_time_labels, + ) def _get_runtime_gc_count(self, observer: metrics.SumObserver) -> None: """Observer callback for garbage collection From 8ccc5cff1e3590a44ae4c2c97a6ae86e334ec8d5 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 6 Oct 2020 08:47:15 -0700 Subject: [PATCH 0596/1517] Renaming batcher to processor (#1203) --- docs/getting_started/prometheus_example.py | 4 +- ...tcher.rst => metrics.export.processor.rst} | 4 +- docs/sdk/metrics.rst | 2 +- .../opencensus/metrics_exporter/__init__.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/metrics/__init__.py | 12 +-- .../sdk/metrics/export/controller.py | 4 +- .../export/{batcher.py => processor.py} | 14 ++-- .../tests/metrics/export/test_export.py | 78 +++++++++---------- .../tests/metrics/test_metrics.py | 32 ++++---- 10 files changed, 78 insertions(+), 76 deletions(-) rename docs/sdk/{metrics.export.batcher.rst => metrics.export.processor.rst} (56%) rename opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/{batcher.py => processor.py} (88%) diff --git a/docs/getting_started/prometheus_example.py b/docs/getting_started/prometheus_example.py index 0377e570b8..d7111df969 100644 --- a/docs/getting_started/prometheus_example.py +++ b/docs/getting_started/prometheus_example.py @@ -27,9 +27,9 @@ # Start Prometheus client start_http_server(port=8000, addr="localhost") -batcher_mode = "stateful" +processor_mode = "stateful" metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__, batcher_mode == "stateful") +meter = metrics.get_meter(__name__, processor_mode == "stateful") exporter = PrometheusMetricsExporter("MyAppPrefix") controller = PushController(meter, exporter, 5) diff --git a/docs/sdk/metrics.export.batcher.rst b/docs/sdk/metrics.export.processor.rst similarity index 56% rename from docs/sdk/metrics.export.batcher.rst rename to docs/sdk/metrics.export.processor.rst index dab2dd3415..cdae8b2fbe 100644 --- a/docs/sdk/metrics.export.batcher.rst +++ b/docs/sdk/metrics.export.processor.rst @@ -1,11 +1,11 @@ -opentelemetry.sdk.metrics.export.batcher +opentelemetry.sdk.metrics.export.processor ========================================== .. toctree:: metrics.export -.. automodule:: opentelemetry.sdk.metrics.export.batcher +.. automodule:: opentelemetry.sdk.metrics.export.processor :members: :undoc-members: :show-inheritance: diff --git a/docs/sdk/metrics.rst b/docs/sdk/metrics.rst index 7030285982..8e34be5a4b 100644 --- a/docs/sdk/metrics.rst +++ b/docs/sdk/metrics.rst @@ -7,7 +7,7 @@ Submodules .. toctree:: metrics.export.aggregate - metrics.export.batcher + metrics.export.processor util.instrumentation .. automodule:: opentelemetry.sdk.metrics diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py index 204a7c5476..db7af753aa 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py @@ -139,7 +139,7 @@ def translate_to_collector( # If cumulative and stateful, explicitly set the start_timestamp to # exporter start time. - if metric_record.instrument.meter.batcher.stateful: + if metric_record.instrument.meter.processor.stateful: start_timestamp = exporter_start_timestamp else: start_timestamp = None diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 7186428288..d283498612 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -10,6 +10,8 @@ ([#1105](https://github.com/open-telemetry/opentelemetry-python/pull/1120)) - Allow for Custom Trace and Span IDs Generation - `IdsGenerator` for TracerProvider ([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153)) +- Renaming metrics Batcher to Processor + ([#1203](https://github.com/open-telemetry/opentelemetry-python/pull/1203)) ## Version 0.13b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 092f456faf..01242b7d07 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -23,8 +23,8 @@ MetricsExporter, ) from opentelemetry.sdk.metrics.export.aggregate import Aggregator -from opentelemetry.sdk.metrics.export.batcher import Batcher from opentelemetry.sdk.metrics.export.controller import PushController +from opentelemetry.sdk.metrics.export.processor import Processor from opentelemetry.sdk.metrics.view import ( ViewData, ViewManager, @@ -325,7 +325,7 @@ class ValueObserver(Observer, metrics_api.ValueObserver): class Record: - """Container class used for processing in the `Batcher`""" + """Container class used for processing in the `Processor`""" def __init__( self, @@ -352,7 +352,7 @@ def __init__( instrumentation_info: "InstrumentationInfo", ): self.instrumentation_info = instrumentation_info - self.batcher = Batcher(source.stateful) + self.processor = Processor(source.stateful) self.resource = source.resource self.metrics = set() self.observers = set() @@ -363,7 +363,7 @@ def __init__( def collect(self) -> None: """Collects all the metrics created with this `Meter` for export. - Utilizes the batcher to create checkpoints of the current values in + Utilizes the processor to create checkpoints of the current values in each aggregator belonging to the metrics that were created with this meter instance. """ @@ -385,7 +385,7 @@ def _collect_metrics(self) -> None: record = Record( metric, view_data.labels, view_data.aggregator ) - self.batcher.process(record) + self.processor.process(record) if bound_instrument.ref_count() == 0: to_remove.append(labels) @@ -405,7 +405,7 @@ def _collect_observers(self) -> None: for labels, aggregator in observer.aggregators.items(): record = Record(observer, labels, aggregator) - self.batcher.process(record) + self.processor.process(record) def record_batch( self, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py index 7448f353c4..e095ebbb72 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py @@ -57,7 +57,7 @@ def tick(self): self.meter.collect() # Export the collected metrics token = attach(set_value("suppress_instrumentation", True)) - self.exporter.export(self.meter.batcher.checkpoint_set()) + self.exporter.export(self.meter.processor.checkpoint_set()) detach(token) # Perform post-exporting logic - self.meter.batcher.finished_collection() + self.meter.processor.finished_collection() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py similarity index 88% rename from opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py index 1c1858ebba..c012d7382b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py @@ -18,18 +18,18 @@ from opentelemetry.sdk.util import get_dict_as_key -class Batcher: - """Base class for all batcher types. +class Processor: + """Base class for all processor types. - The batcher is responsible for storing the aggregators and aggregated + The processor is responsible for storing the aggregators and aggregated values received from updates from metrics in the meter. The stored values will be sent to an exporter for exporting. """ def __init__(self, stateful: bool): self._batch_map = {} - # stateful=True indicates the batcher computes checkpoints from over - # the process lifetime. False indicates the batcher computes + # stateful=True indicates the processor computes checkpoints from over + # the process lifetime. False indicates the processor computes # checkpoints which describe the updates of a single collection period # (deltas) self.stateful = stateful @@ -38,7 +38,7 @@ def checkpoint_set(self) -> Sequence[MetricRecord]: """Returns a list of MetricRecords used for exporting. The list of MetricRecords is a snapshot created from the current - data in all of the aggregators in this batcher. + data in all of the aggregators in this processor. """ metric_records = [] # pylint: disable=W0612 @@ -52,7 +52,7 @@ def checkpoint_set(self) -> Sequence[MetricRecord]: def finished_collection(self): """Performs certain post-export logic. - For batchers that are stateless, resets the batch map. + For processors that are stateless, resets the batch map. """ if not self.stateful: self._batch_map = {} diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 99aa9c4a62..efa6bcd24e 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -29,8 +29,8 @@ SumAggregator, ValueObserverAggregator, ) -from opentelemetry.sdk.metrics.export.batcher import Batcher from opentelemetry.sdk.metrics.export.controller import PushController +from opentelemetry.sdk.metrics.export.processor import Processor # pylint: disable=protected-access @@ -61,10 +61,10 @@ def test_export(self): mock_stdout.write.assert_any_call(result) -class TestBatcher(unittest.TestCase): +class TestProcessor(unittest.TestCase): def test_checkpoint_set(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher = Batcher(True) + processor = Processor(True) aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", "bytes", int, meter @@ -73,21 +73,21 @@ def test_checkpoint_set(self): labels = () _batch_map = {} _batch_map[(metric, SumAggregator, tuple(), labels)] = aggregator - batcher._batch_map = _batch_map - records = batcher.checkpoint_set() + processor._batch_map = _batch_map + records = processor.checkpoint_set() self.assertEqual(len(records), 1) self.assertEqual(records[0].instrument, metric) self.assertEqual(records[0].labels, labels) self.assertEqual(records[0].aggregator, aggregator) def test_checkpoint_set_empty(self): - batcher = Batcher(True) - records = batcher.checkpoint_set() + processor = Processor(True) + records = processor.checkpoint_set() self.assertEqual(len(records), 0) def test_finished_collection_stateless(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher = Batcher(False) + processor = Processor(False) aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", "bytes", int, meter @@ -96,13 +96,13 @@ def test_finished_collection_stateless(self): labels = () _batch_map = {} _batch_map[(metric, SumAggregator, tuple(), labels)] = aggregator - batcher._batch_map = _batch_map - batcher.finished_collection() - self.assertEqual(len(batcher._batch_map), 0) + processor._batch_map = _batch_map + processor.finished_collection() + self.assertEqual(len(processor._batch_map), 0) def test_finished_collection_stateful(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher = Batcher(True) + processor = Processor(True) aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", "bytes", int, meter @@ -111,13 +111,13 @@ def test_finished_collection_stateful(self): labels = () _batch_map = {} _batch_map[(metric, SumAggregator, tuple(), labels)] = aggregator - batcher._batch_map = _batch_map - batcher.finished_collection() - self.assertEqual(len(batcher._batch_map), 1) + processor._batch_map = _batch_map + processor.finished_collection() + self.assertEqual(len(processor._batch_map), 1) - def test_batcher_process_exists(self): + def test_processor_process_exists(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher = Batcher(True) + processor = Processor(True) aggregator = SumAggregator() aggregator2 = SumAggregator() metric = metrics.Counter( @@ -128,17 +128,17 @@ def test_batcher_process_exists(self): batch_key = (metric, SumAggregator, tuple(), labels) _batch_map[batch_key] = aggregator aggregator2.update(1.0) - batcher._batch_map = _batch_map + processor._batch_map = _batch_map record = metrics.Record(metric, labels, aggregator2) - batcher.process(record) - self.assertEqual(len(batcher._batch_map), 1) - self.assertIsNotNone(batcher._batch_map.get(batch_key)) - self.assertEqual(batcher._batch_map.get(batch_key).current, 0) - self.assertEqual(batcher._batch_map.get(batch_key).checkpoint, 1.0) + processor.process(record) + self.assertEqual(len(processor._batch_map), 1) + self.assertIsNotNone(processor._batch_map.get(batch_key)) + self.assertEqual(processor._batch_map.get(batch_key).current, 0) + self.assertEqual(processor._batch_map.get(batch_key).checkpoint, 1.0) - def test_batcher_process_not_exists(self): + def test_processor_process_not_exists(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher = Batcher(True) + processor = Processor(True) aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", "bytes", int, meter @@ -147,17 +147,17 @@ def test_batcher_process_not_exists(self): _batch_map = {} batch_key = (metric, SumAggregator, tuple(), labels) aggregator.update(1.0) - batcher._batch_map = _batch_map + processor._batch_map = _batch_map record = metrics.Record(metric, labels, aggregator) - batcher.process(record) - self.assertEqual(len(batcher._batch_map), 1) - self.assertIsNotNone(batcher._batch_map.get(batch_key)) - self.assertEqual(batcher._batch_map.get(batch_key).current, 0) - self.assertEqual(batcher._batch_map.get(batch_key).checkpoint, 1.0) + processor.process(record) + self.assertEqual(len(processor._batch_map), 1) + self.assertIsNotNone(processor._batch_map.get(batch_key)) + self.assertEqual(processor._batch_map.get(batch_key).current, 0) + self.assertEqual(processor._batch_map.get(batch_key).checkpoint, 1.0) - def test_batcher_process_not_stateful(self): + def test_processor_process_not_stateful(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher = Batcher(True) + processor = Processor(True) aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", "bytes", int, meter @@ -166,13 +166,13 @@ def test_batcher_process_not_stateful(self): _batch_map = {} batch_key = (metric, SumAggregator, tuple(), labels) aggregator.update(1.0) - batcher._batch_map = _batch_map + processor._batch_map = _batch_map record = metrics.Record(metric, labels, aggregator) - batcher.process(record) - self.assertEqual(len(batcher._batch_map), 1) - self.assertIsNotNone(batcher._batch_map.get(batch_key)) - self.assertEqual(batcher._batch_map.get(batch_key).current, 0) - self.assertEqual(batcher._batch_map.get(batch_key).checkpoint, 1.0) + processor.process(record) + self.assertEqual(len(processor._batch_map), 1) + self.assertIsNotNone(processor._batch_map.get(batch_key)) + self.assertEqual(processor._batch_map.get(batch_key).current, 0) + self.assertEqual(processor._batch_map.get(batch_key).checkpoint, 1.0) class TestSumAggregator(unittest.TestCase): diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 8e412f3c5c..2f833f4782 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -28,7 +28,7 @@ class TestMeterProvider(unittest.TestCase): def test_stateful(self): meter_provider = metrics.MeterProvider(stateful=False) meter = meter_provider.get_meter(__name__) - self.assertIs(meter.batcher.stateful, False) + self.assertIs(meter.processor.stateful, False) def test_resource(self): resource = resources.Resource.create({}) @@ -74,8 +74,8 @@ def test_extends_api(self): def test_collect_metrics(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher_mock = mock.Mock() - meter.batcher = batcher_mock + processor_mock = mock.Mock() + meter.processor = processor_mock counter = meter.create_metric( "name", "desc", "unit", float, metrics.Counter ) @@ -83,40 +83,40 @@ def test_collect_metrics(self): meter.register_view(View(counter, SumAggregator)) counter.add(1.0, labels) meter.collect() - self.assertTrue(batcher_mock.process.called) + self.assertTrue(processor_mock.process.called) def test_collect_no_metrics(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher_mock = mock.Mock() - meter.batcher = batcher_mock + processor_mock = mock.Mock() + meter.processor = processor_mock meter.collect() - self.assertFalse(batcher_mock.process.called) + self.assertFalse(processor_mock.process.called) def test_collect_not_registered(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher_mock = mock.Mock() - meter.batcher = batcher_mock + processor_mock = mock.Mock() + meter.processor = processor_mock counter = metrics.Counter("name", "desc", "unit", float, meter) labels = {"key1": "value1"} counter.add(1.0, labels) meter.collect() - self.assertFalse(batcher_mock.process.called) + self.assertFalse(processor_mock.process.called) def test_collect_disabled_metric(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher_mock = mock.Mock() - meter.batcher = batcher_mock + processor_mock = mock.Mock() + meter.processor = processor_mock counter = metrics.Counter("name", "desc", "unit", float, meter, False) labels = {"key1": "value1"} meter.register_view(View(counter, SumAggregator)) counter.add(1.0, labels) meter.collect() - self.assertFalse(batcher_mock.process.called) + self.assertFalse(processor_mock.process.called) def test_collect_observers(self): meter = metrics.MeterProvider().get_meter(__name__) - batcher_mock = mock.Mock() - meter.batcher = batcher_mock + processor_mock = mock.Mock() + meter.processor = processor_mock def callback(observer): self.assertIsInstance(observer, metrics_api.Observer) @@ -128,7 +128,7 @@ def callback(observer): meter.observers.add(observer) meter.collect() - self.assertTrue(batcher_mock.process.called) + self.assertTrue(processor_mock.process.called) def test_record_batch(self): meter = metrics.MeterProvider().get_meter(__name__) From 6fac795876bade192bf1fdf534dc5a44307a103b Mon Sep 17 00:00:00 2001 From: Amos Law Date: Tue, 6 Oct 2020 17:44:41 -0400 Subject: [PATCH 0597/1517] Protect access to Span implementation (#1188) --- .pylintrc | 3 ++- .../tests/test_datadog_exporter.py | 14 +++++++------- .../tests/test_datadog_format.py | 4 ++-- .../tests/test_jaeger_exporter.py | 10 ++++++---- .../tests/test_otcollector_trace_exporter.py | 8 ++++---- .../tests/test_otlp_trace_exporter.py | 4 ++-- .../tests/test_zipkin_exporter.py | 18 +++++++++++------- .../tests/test_utils.py | 8 ++++---- opentelemetry-sdk/CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/__init__.py | 14 +++++++++++++- .../tests/trace/export/test_export.py | 6 +++--- .../export/test_in_memory_span_exporter.py | 2 +- .../tests/trace/propagation/test_b3_format.py | 4 ++-- .../tests/trace/test_implementation.py | 4 ++-- opentelemetry-sdk/tests/trace/test_trace.py | 15 ++++++++++----- 15 files changed, 71 insertions(+), 45 deletions(-) diff --git a/.pylintrc b/.pylintrc index b23bef8e66..ea54273868 100644 --- a/.pylintrc +++ b/.pylintrc @@ -441,7 +441,8 @@ exclude-protected=_asdict, _fields, _replace, _source, - _make + _make, + _Span # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py index e1973e5ac6..27b4d0fa03 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py @@ -188,7 +188,7 @@ def test_translate_to_datadog(self): instrumentation_info = InstrumentationInfo(__name__, "0") otel_spans = [ - trace.Span( + trace._Span( name=span_names[0], context=span_context, parent=parent_context, @@ -196,14 +196,14 @@ def test_translate_to_datadog(self): instrumentation_info=instrumentation_info, resource=Resource({}), ), - trace.Span( + trace._Span( name=span_names[1], context=parent_context, parent=None, instrumentation_info=instrumentation_info, resource=resource_without_service, ), - trace.Span( + trace._Span( name=span_names[2], context=other_context, parent=None, @@ -289,7 +289,7 @@ def test_export(self): is_remote=False, ) - test_span = trace.Span("test_span", context=context) + test_span = trace._Span("test_span", context=context) test_span.start() test_span.end() @@ -492,8 +492,8 @@ def test_origin(self): ), ) - root_span = trace.Span(name="root", context=context, parent=None) - child_span = trace.Span( + root_span = trace._Span(name="root", context=context, parent=None) + child_span = trace._Span( name="child", context=context, parent=root_span ) root_span.start() @@ -528,7 +528,7 @@ def test_sampling_rate(self): ) sampler = sampling.TraceIdRatioBased(0.5) - span = trace.Span( + span = trace._Span( name="sampled", context=context, parent=None, sampler=sampler ) span.start() diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py index 994cac2d60..3c045539fd 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py @@ -104,7 +104,7 @@ def test_context_propagation(self): ) self.assertTrue(parent_context.is_remote) - child = trace.Span( + child = trace._Span( "child", trace_api.SpanContext( parent_context.trace_id, @@ -149,7 +149,7 @@ def test_sampling_priority_auto_reject(self): self.assertEqual(parent_context.trace_flags, constants.AUTO_REJECT) - child = trace.Span( + child = trace._Span( "child", trace_api.SpanContext( parent_context.trace_id, diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py index c787081422..bb4920f7b3 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py @@ -36,7 +36,7 @@ def setUp(self): is_remote=False, ) - self._test_span = trace.Span("test_span", context=context) + self._test_span = trace._Span("test_span", context=context) self._test_span.start() self._test_span.end() @@ -187,7 +187,7 @@ def test_translate_to_jaeger(self): ] otel_spans = [ - trace.Span( + trace._Span( name=span_names[0], context=span_context, parent=parent_context, @@ -195,10 +195,12 @@ def test_translate_to_jaeger(self): links=(link,), kind=trace_api.SpanKind.CLIENT, ), - trace.Span( + trace._Span( name=span_names[1], context=parent_context, parent=None ), - trace.Span(name=span_names[2], context=other_context, parent=None), + trace._Span( + name=span_names[2], context=other_context, parent=None + ), ] otel_spans[0].start(start_time=start_times[0]) diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py index d07fd053b4..3721139efc 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py @@ -120,7 +120,7 @@ def test_translate_to_collector(self): link_2 = trace_api.Link( context=parent_context, attributes=link_attributes ) - span_1 = trace.Span( + span_1 = trace._Span( name="test1", context=span_context, parent=parent_context, @@ -128,13 +128,13 @@ def test_translate_to_collector(self): links=(link_1,), kind=trace_api.SpanKind.CLIENT, ) - span_2 = trace.Span( + span_2 = trace._Span( name="test2", context=parent_context, parent=None, kind=trace_api.SpanKind.SERVER, ) - span_3 = trace.Span( + span_3 = trace._Span( name="test3", context=other_context, links=(link_2,), @@ -302,7 +302,7 @@ def test_export(self): trace_flags=TraceFlags(TraceFlags.SAMPLED), ) otel_spans = [ - trace.Span( + trace._Span( name="test1", context=span_context, kind=trace_api.SpanKind.CLIENT, diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index dcc0239798..e8c449c9df 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -45,7 +45,7 @@ from opentelemetry.proto.trace.v1.trace_pb2 import Span as OTLPSpan from opentelemetry.proto.trace.v1.trace_pb2 import Status from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.sdk.trace import Span, TracerProvider +from opentelemetry.sdk.trace import TracerProvider, _Span from opentelemetry.sdk.trace.export import ( SimpleExportSpanProcessor, SpanExportResult, @@ -123,7 +123,7 @@ def setUp(self): type(event_mock).name = PropertyMock(return_value="a") - self.span = Span( + self.span = _Span( "a", context=Mock( **{ diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 6120f2dd6e..8d6d8bb860 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -42,7 +42,7 @@ def setUp(self): is_remote=False, ) - self._test_span = trace.Span("test_span", context=context) + self._test_span = trace._Span("test_span", context=context) self._test_span.start() self._test_span.end() @@ -154,18 +154,22 @@ def test_export(self): ) otel_spans = [ - trace.Span( + trace._Span( name=span_names[0], context=span_context, parent=parent_context, events=(event,), links=(link,), ), - trace.Span( + trace._Span( name=span_names[1], context=parent_context, parent=None ), - trace.Span(name=span_names[2], context=other_context, parent=None), - trace.Span(name=span_names[3], context=other_context, parent=None), + trace._Span( + name=span_names[2], context=other_context, parent=None + ), + trace._Span( + name=span_names[3], context=other_context, parent=None + ), ] otel_spans[0].start(start_time=start_times[0]) @@ -328,7 +332,7 @@ def test_zero_padding(self): trace_id, parent_id, is_remote=False ) - otel_span = trace.Span( + otel_span = trace._Span( name=span_names[0], context=span_context, parent=parent_context, ) @@ -387,7 +391,7 @@ def test_max_tag_length(self): trace_flags=TraceFlags(TraceFlags.SAMPLED), ) - span = trace.Span(name="test-span", context=span_context,) + span = trace._Span(name="test-span", context=span_context,) span.start() span.resource = Resource({}) diff --git a/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py b/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py index 0842890e89..3df0f445b3 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py +++ b/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py @@ -42,7 +42,7 @@ def test_set_attributes_from_context(self): "routing_key": "celery", } - span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) utils.set_attributes_from_context(span, context) self.assertEqual( @@ -78,7 +78,7 @@ def test_set_attributes_from_context_empty_keys(self): "retries": 0, } - span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) utils.set_attributes_from_context(span, context) self.assertEqual(len(span.attributes), 0) @@ -99,7 +99,7 @@ def fn_task(): # propagate and retrieve a Span task_id = "7c6731af-9533-40c3-83a9-25b58f0d837f" - span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) utils.attach_span(fn_task, task_id, span) span_after = utils.retrieve_span(fn_task, task_id) self.assertIs(span, span_after) @@ -112,7 +112,7 @@ def fn_task(): # propagate a Span task_id = "7c6731af-9533-40c3-83a9-25b58f0d837f" - span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) utils.attach_span(fn_task, task_id, span) # delete the Span utils.detach_span(fn_task, task_id) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index d283498612..0f366f2c07 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -12,6 +12,8 @@ ([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153)) - Renaming metrics Batcher to Processor ([#1203](https://github.com/open-telemetry/opentelemetry-python/pull/1203)) +- Protect access to Span implementation + ([#1188](https://github.com/open-telemetry/opentelemetry-python/pull/1188)) ## Version 0.13b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0134ec7e77..57f1fb6b77 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -357,6 +357,11 @@ class Span(trace_api.Span): this `Span`. """ + def __new__(cls, *args, **kwargs): + if cls is Span: + raise TypeError("Span must be instantiated via a tracer.") + return super().__new__(cls) + def __init__( self, name: str, @@ -663,6 +668,13 @@ def record_exception(self, exception: Exception) -> None: ) +class _Span(Span): + """Protected implementation of `opentelemetry.trace.Span`. + + This constructor should only be used internally. + """ + + class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`. @@ -748,7 +760,7 @@ def start_span( # pylint: disable=too-many-locals # Only record if is_recording() is true if sampling_result.decision.is_recording(): # pylint:disable=protected-access - span = Span( + span = _Span( name=name, context=context, parent=parent_context, diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 8c43f731b2..4ab3a3aa41 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -121,7 +121,7 @@ def test_simple_span_processor_not_sampled(self): def _create_start_and_end_span(name, span_processor): - span = trace.Span( + span = trace._Span( name, trace_api.SpanContext( 0xDEADBEEF, @@ -403,7 +403,7 @@ def test_export(self): # pylint: disable=no-self-use # Mocking stdout interferes with debugging and test reporting, mock on # the exporter instance instead. - span = trace.Span("span name", trace_api.INVALID_SPAN_CONTEXT) + span = trace._Span("span name", trace_api.INVALID_SPAN_CONTEXT) with mock.patch.object(exporter, "out") as mock_stdout: exporter.export([span]) mock_stdout.write.assert_called_once_with(span.to_json() + os.linesep) @@ -421,5 +421,5 @@ def formatter(span): # pylint: disable=unused-argument exporter = export.ConsoleSpanExporter( out=mock_stdout, formatter=formatter ) - exporter.export([trace.Span("span name", mock.Mock())]) + exporter.export([trace._Span("span name", mock.Mock())]) mock_stdout.write.assert_called_once_with(mock_span_str) diff --git a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py index 4925bdb182..e45e8ee8fd 100644 --- a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py +++ b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py @@ -61,7 +61,7 @@ def test_shutdown(self): self.assertEqual(len(span_list), 3) def test_return_code(self): - span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) span_list = (span,) memory_exporter = InMemorySpanExporter() diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index 07b3010087..b825ea1a75 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -33,8 +33,8 @@ def get_child_parent_new_carrier(old_carrier): ctx = FORMAT.extract(get_as_list, old_carrier) parent_context = trace_api.get_current_span(ctx).get_context() - parent = trace.Span("parent", parent_context) - child = trace.Span( + parent = trace._Span("parent", parent_context) + child = trace._Span( "child", trace_api.SpanContext( parent_context.trace_id, diff --git a/opentelemetry-sdk/tests/trace/test_implementation.py b/opentelemetry-sdk/tests/trace/test_implementation.py index 4582b06957..7e6ede1ae0 100644 --- a/opentelemetry-sdk/tests/trace/test_implementation.py +++ b/opentelemetry-sdk/tests/trace/test_implementation.py @@ -40,8 +40,8 @@ def test_tracer(self): def test_span(self): with self.assertRaises(Exception): # pylint: disable=no-value-for-parameter - span = trace.Span() + span = trace._Span() - span = trace.Span("name", INVALID_SPAN_CONTEXT) + span = trace._Span("name", INVALID_SPAN_CONTEXT) self.assertEqual(span.get_context(), INVALID_SPAN_CONTEXT) self.assertIs(span.is_recording(), True) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 8c5544578d..2ea43659a8 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -404,13 +404,18 @@ def test_span_context_remote_flag(self): span = tracer.start_span("foo") self.assertFalse(span.context.is_remote) + def test_disallow_direct_span_creation(self): + with self.assertRaises(TypeError): + # pylint: disable=abstract-class-instantiated + trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + class TestSpan(unittest.TestCase): def setUp(self): self.tracer = new_tracer() def test_basic_span(self): - span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) self.assertEqual(span.name, "name") def test_attributes(self): @@ -657,7 +662,7 @@ def test_update_name(self): def test_start_span(self): """Start twice, end a not started""" - span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) # end not started span self.assertRaises(RuntimeError, span.end) @@ -683,7 +688,7 @@ def test_start_span(self): def test_span_override_start_and_end_time(self): """Span sending custom start_time and end_time values""" - span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) start_time = 123 span.start(start_time) self.assertEqual(start_time, span.start_time) @@ -782,7 +787,7 @@ def error_status_test(context): ) def test_record_exception(self): - span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) try: raise ValueError("invalid") except ValueError as err: @@ -922,7 +927,7 @@ def test_to_json(self): is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) - span = trace.Span("span-name", context) + span = trace._Span("span-name", context) span.resource = Resource({}) self.assertEqual( From 803f5820d473bd90bd85b99520980e9568f386ef Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 7 Oct 2020 10:22:40 +0530 Subject: [PATCH 0598/1517] Record exception on context manager exit (#1162) --- .../instrumentation/requests/__init__.py | 24 +++++----- .../src/opentelemetry/trace/__init__.py | 16 ++++++- .../src/opentelemetry/trace/status.py | 3 ++ opentelemetry-sdk/CHANGELOG.md | 1 + .../src/opentelemetry/sdk/trace/__init__.py | 48 ++++++++++++------- opentelemetry-sdk/tests/trace/test_trace.py | 32 +++++++++++++ 6 files changed, 94 insertions(+), 30 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index d0336184ed..4b5f73de9e 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -50,7 +50,11 @@ from opentelemetry.instrumentation.requests.version import __version__ from opentelemetry.instrumentation.utils import http_status_to_canonical_code from opentelemetry.trace import SpanKind, get_tracer -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import ( + EXCEPTION_STATUS_FIELD, + Status, + StatusCanonicalCode, +) # A key to a context variable to avoid creating duplicate spans when instrumenting # both, Session.request and Session.send, since Session.request calls into Session.send @@ -121,8 +125,6 @@ def _instrumented_requests_call( method = method.upper() span_name = "HTTP {}".format(method) - exception = None - recorder = RequestsInstrumentor().metric_recorder labels = {} @@ -132,6 +134,7 @@ def _instrumented_requests_call( with get_tracer( __name__, __version__, tracer_provider ).start_as_current_span(span_name, kind=SpanKind.CLIENT) as span: + exception = None with recorder.record_duration(labels): if span.is_recording(): span.set_attribute("component", "http") @@ -150,16 +153,15 @@ def _instrumented_requests_call( result = call_wrapped() # *** PROCEED except Exception as exc: # pylint: disable=W0703 exception = exc + setattr( + exception, + EXCEPTION_STATUS_FIELD, + _exception_to_canonical_code(exception), + ) result = getattr(exc, "response", None) finally: context.detach(token) - if exception is not None and span.is_recording(): - span.set_status( - Status(_exception_to_canonical_code(exception)) - ) - span.record_exception(exception) - if result is not None: if span.is_recording(): span.set_attribute( @@ -184,8 +186,8 @@ def _instrumented_requests_call( if span_callback is not None: span_callback(span, result) - if exception is not None: - raise exception.with_traceback(exception.__traceback__) + if exception is not None: + raise exception.with_traceback(exception.__traceback__) return result diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 301314e28e..5aa5c7f33c 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -282,6 +282,7 @@ def start_as_current_span( kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: typing.Sequence[Link] = (), + record_exception: bool = True, ) -> typing.Iterator["Span"]: """Context manager for creating a new span and set it as the current span in this tracer's context. @@ -320,6 +321,8 @@ def start_as_current_span( meaningful even if there is no parent. attributes: The span's attributes. links: Links span to other spans + record_exception: Whether to record any exceptions raised within the + context as error event on the span. Yields: The newly-created span. @@ -328,7 +331,10 @@ def start_as_current_span( @contextmanager # type: ignore @abc.abstractmethod def use_span( - self, span: "Span", end_on_exit: bool = False + self, + span: "Span", + end_on_exit: bool = False, + record_exception: bool = True, ) -> typing.Iterator[None]: """Context manager for setting the passed span as the current span in the context, as well as resetting the @@ -345,6 +351,8 @@ def use_span( span: The span to start and make current. end_on_exit: Whether to end the span automatically when leaving the context manager. + record_exception: Whether to record any exceptions raised within the + context as error event on the span. """ @@ -375,13 +383,17 @@ def start_as_current_span( kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: typing.Sequence[Link] = (), + record_exception: bool = True, ) -> typing.Iterator["Span"]: # pylint: disable=unused-argument,no-self-use yield INVALID_SPAN @contextmanager # type: ignore def use_span( - self, span: "Span", end_on_exit: bool = False + self, + span: "Span", + end_on_exit: bool = False, + record_exception: bool = True, ) -> typing.Iterator[None]: # pylint: disable=unused-argument,no-self-use yield diff --git a/opentelemetry-api/src/opentelemetry/trace/status.py b/opentelemetry-api/src/opentelemetry/trace/status.py index 4191369dfe..9df5493e3b 100644 --- a/opentelemetry-api/src/opentelemetry/trace/status.py +++ b/opentelemetry-api/src/opentelemetry/trace/status.py @@ -19,6 +19,9 @@ logger = logging.getLogger(__name__) +EXCEPTION_STATUS_FIELD = "_otel_status_code" + + class StatusCanonicalCode(enum.Enum): """Represents the canonical set of status codes of a finished Span.""" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 0f366f2c07..388296fbe3 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -14,6 +14,7 @@ ([#1203](https://github.com/open-telemetry/opentelemetry-python/pull/1203)) - Protect access to Span implementation ([#1188](https://github.com/open-telemetry/opentelemetry-python/pull/1188)) +- `start_as_current_span` and `use_span` can now optionally auto-record any exceptions raised inside the context manager. ([#1162](https://github.com/open-telemetry/opentelemetry-python/pull/1162)) ## Version 0.13b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 57f1fb6b77..5e04d8991b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -45,7 +45,11 @@ from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanContext from opentelemetry.trace.propagation import SPAN_KEY -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import ( + EXCEPTION_STATUS_FIELD, + Status, + StatusCanonicalCode, +) from opentelemetry.util import time_ns, types logger = logging.getLogger(__name__) @@ -699,9 +703,12 @@ def start_as_current_span( kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, attributes: types.Attributes = None, links: Sequence[trace_api.Link] = (), + record_exception: bool = True, ) -> Iterator[trace_api.Span]: - span = self.start_span(name, parent, kind, attributes, links) - return self.use_span(span, end_on_exit=True) + span = self.start_span(name, parent, kind, attributes, links,) + return self.use_span( + span, end_on_exit=True, record_exception=record_exception + ) def start_span( # pylint: disable=too-many-locals self, @@ -780,7 +787,10 @@ def start_span( # pylint: disable=too-many-locals @contextmanager def use_span( - self, span: trace_api.Span, end_on_exit: bool = False + self, + span: trace_api.Span, + end_on_exit: bool = False, + record_exception: bool = True, ) -> Iterator[trace_api.Span]: try: token = context_api.attach(context_api.set_value(SPAN_KEY, span)) @@ -790,20 +800,24 @@ def use_span( context_api.detach(token) except Exception as error: # pylint: disable=broad-except - if ( - isinstance(span, Span) - and span.status is None - and span._set_status_on_exception # pylint:disable=protected-access # noqa - ): - span.set_status( - Status( - canonical_code=StatusCanonicalCode.UNKNOWN, - description="{}: {}".format( - type(error).__name__, error - ), + # pylint:disable=protected-access + if isinstance(span, Span): + if record_exception: + span.record_exception(error) + + if span.status is None and span._set_status_on_exception: + span.set_status( + Status( + canonical_code=getattr( + error, + EXCEPTION_STATUS_FIELD, + StatusCanonicalCode.UNKNOWN, + ), + description="{}: {}".format( + type(error).__name__, error + ), + ) ) - ) - raise finally: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 2ea43659a8..7edcfc79b8 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -805,6 +805,38 @@ def test_record_exception(self): exception_event.attributes["exception.stacktrace"], ) + def test_record_exception_context_manager(self): + try: + with self.tracer.start_as_current_span("span") as span: + raise RuntimeError("example error") + except RuntimeError: + pass + finally: + self.assertEqual(len(span.events), 1) + event = span.events[0] + self.assertEqual("exception", event.name) + self.assertEqual( + "RuntimeError", event.attributes["exception.type"] + ) + self.assertEqual( + "example error", event.attributes["exception.message"] + ) + + stacktrace = """in test_record_exception_context_manager + raise RuntimeError("example error") +RuntimeError: example error""" + self.assertIn(stacktrace, event.attributes["exception.stacktrace"]) + + try: + with self.tracer.start_as_current_span( + "span", record_exception=False + ) as span: + raise RuntimeError("example error") + except RuntimeError: + pass + finally: + self.assertEqual(len(span.events), 0) + def span_event_start_fmt(span_processor_name, span_name): return span_processor_name + ":" + span_name + ":start" From 8d7cba05f401f76babc6acd646a3acee02a715ae Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 7 Oct 2020 09:07:49 -0700 Subject: [PATCH 0599/1517] Adding Resource to MetricRecord (#1209) Co-authored-by: Diego Hurtado --- .../opencensus/metrics_exporter/__init__.py | 2 +- .../test_otcollector_metrics_exporter.py | 33 +++++++++++-- .../otlp/metrics_exporter/__init__.py | 6 +-- .../tests/test_otlp_metric_exporter.py | 7 ++- .../tests/test_prometheus_exporter.py | 17 +++++-- opentelemetry-sdk/CHANGELOG.md | 5 +- .../src/opentelemetry/sdk/metrics/__init__.py | 3 +- .../sdk/metrics/export/__init__.py | 6 ++- .../sdk/metrics/export/processor.py | 8 +++- .../tests/metrics/export/test_export.py | 48 ++++++++++++------- .../tests/metrics/test_metrics.py | 8 ++-- 11 files changed, 98 insertions(+), 45 deletions(-) diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py index db7af753aa..de3c8a8b45 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py @@ -191,7 +191,7 @@ def get_collector_point(metric_record: MetricRecord) -> metrics_pb2.Point: def get_resource(metric_record: MetricRecord) -> resource_pb2.Resource: - resource_attributes = metric_record.instrument.meter.resource.attributes + resource_attributes = metric_record.resource.attributes return resource_pb2.Resource( type=infer_oc_resource_type(resource_attributes), labels={k: str(v) for k, v in resource_attributes.items()}, diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py index 1ec1a57448..4dd34bf1ba 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py @@ -100,7 +100,12 @@ def test_get_collector_point(self): "testName", "testDescription", "unit", float, ValueRecorder ) result = metrics_exporter.get_collector_point( - MetricRecord(int_counter, self._key_labels, aggregator) + MetricRecord( + int_counter, + self._key_labels, + aggregator, + metrics.get_meter_provider().resource, + ) ) self.assertIsInstance(result, metrics_pb2.Point) self.assertIsInstance(result.timestamp, Timestamp) @@ -108,13 +113,23 @@ def test_get_collector_point(self): aggregator.update(123.5) aggregator.take_checkpoint() result = metrics_exporter.get_collector_point( - MetricRecord(float_counter, self._key_labels, aggregator) + MetricRecord( + float_counter, + self._key_labels, + aggregator, + metrics.get_meter_provider().resource, + ) ) self.assertEqual(result.double_value, 123.5) self.assertRaises( TypeError, metrics_exporter.get_collector_point( - MetricRecord(valuerecorder, self._key_labels, aggregator) + MetricRecord( + valuerecorder, + self._key_labels, + aggregator, + metrics.get_meter_provider().resource, + ) ), ) @@ -130,7 +145,10 @@ def test_export(self): "testname", "testdesc", "unit", int, Counter, self._labels.keys(), ) record = MetricRecord( - test_metric, self._key_labels, aggregate.SumAggregator(), + test_metric, + self._key_labels, + aggregate.SumAggregator(), + metrics.get_meter_provider().resource, ) result = collector_exporter.export([record]) @@ -155,7 +173,12 @@ def test_translate_to_collector(self): aggregator = aggregate.SumAggregator() aggregator.update(123) aggregator.take_checkpoint() - record = MetricRecord(test_metric, self._key_labels, aggregator,) + record = MetricRecord( + test_metric, + self._key_labels, + aggregator, + metrics.get_meter_provider().resource, + ) start_timestamp = Timestamp() output_metrics = metrics_exporter.translate_to_collector( [record], start_timestamp, diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py index 08a47c601e..1fa1bf24f1 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -137,11 +137,11 @@ def _translate_data( # ValueObserver Gauge() for sdk_metric in data: - if sdk_metric.instrument.meter.resource not in ( + if sdk_metric.resource not in ( sdk_resource_instrumentation_library_metrics.keys() ): sdk_resource_instrumentation_library_metrics[ - sdk_metric.instrument.meter.resource + sdk_metric.resource ] = InstrumentationLibraryMetrics() type_class = { @@ -217,7 +217,7 @@ def _translate_data( argument = type_class[value_type]["gauge"]["argument"] sdk_resource_instrumentation_library_metrics[ - sdk_metric.instrument.meter.resource + sdk_metric.resource ].metrics.append( OTLPMetric( **{ diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index 1218fbbb33..1eba2bef66 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -44,20 +44,19 @@ class TestOTLPMetricExporter(TestCase): def setUp(self): self.exporter = OTLPMetricsExporter() - + resource = SDKResource(OrderedDict([("a", 1), ("b", False)])) self.counter_metric_record = MetricRecord( Counter( "a", "b", "c", int, - MeterProvider( - resource=SDKResource(OrderedDict([("a", 1), ("b", False)])) - ).get_meter(__name__), + MeterProvider(resource=resource,).get_meter(__name__), ("d",), ), OrderedDict([("e", "f")]), SumAggregator(), + resource, ) def test_translate_metrics(self): diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 936a6eb822..4e2075b8b6 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -67,7 +67,10 @@ def test_shutdown(self): def test_export(self): with self._registry_register_patch: record = MetricRecord( - self._test_metric, self._labels_key, SumAggregator(), + self._test_metric, + self._labels_key, + SumAggregator(), + get_meter_provider().resource, ) exporter = PrometheusMetricsExporter() result = exporter.export([record]) @@ -86,7 +89,9 @@ def test_min_max_sum_aggregator_to_prometheus(self): aggregator.update(123) aggregator.update(456) aggregator.take_checkpoint() - record = MetricRecord(metric, key_labels, aggregator) + record = MetricRecord( + metric, key_labels, aggregator, get_meter_provider().resource + ) collector = CustomCollector("testprefix") collector.add_metrics_data([record]) result_bytes = generate_latest(collector) @@ -104,7 +109,9 @@ def test_counter_to_prometheus(self): aggregator = SumAggregator() aggregator.update(123) aggregator.take_checkpoint() - record = MetricRecord(metric, key_labels, aggregator) + record = MetricRecord( + metric, key_labels, aggregator, get_meter_provider().resource + ) collector = CustomCollector("testprefix") collector.add_metrics_data([record]) @@ -132,7 +139,9 @@ def test_invalid_metric(self): ) labels = {"environment": "staging"} key_labels = get_dict_as_key(labels) - record = MetricRecord(metric, key_labels, None) + record = MetricRecord( + metric, key_labels, None, get_meter_provider().resource + ) collector = CustomCollector("testprefix") collector.add_metrics_data([record]) collector.collect() diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 388296fbe3..6890a2e2b7 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -14,7 +14,10 @@ ([#1203](https://github.com/open-telemetry/opentelemetry-python/pull/1203)) - Protect access to Span implementation ([#1188](https://github.com/open-telemetry/opentelemetry-python/pull/1188)) -- `start_as_current_span` and `use_span` can now optionally auto-record any exceptions raised inside the context manager. ([#1162](https://github.com/open-telemetry/opentelemetry-python/pull/1162)) +- `start_as_current_span` and `use_span` can now optionally auto-record any exceptions raised inside the context manager. + ([#1162](https://github.com/open-telemetry/opentelemetry-python/pull/1162)) +- Adding Resource to MeterRecord + ([#1209](https://github.com/open-telemetry/opentelemetry-python/pull/1209)) ## Version 0.13b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 01242b7d07..382dbeb9b9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -352,8 +352,7 @@ def __init__( instrumentation_info: "InstrumentationInfo", ): self.instrumentation_info = instrumentation_info - self.processor = Processor(source.stateful) - self.resource = source.resource + self.processor = Processor(source.stateful, source.resource) self.metrics = set() self.observers = set() self.metrics_lock = threading.Lock() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index 16911f94ef..e7882217ec 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -17,6 +17,7 @@ from opentelemetry import metrics as metrics_api from opentelemetry.sdk.metrics.export.aggregate import Aggregator +from opentelemetry.sdk.resources import Resource class MetricsExportResult(Enum): @@ -30,10 +31,12 @@ def __init__( instrument: metrics_api.InstrumentT, labels: Tuple[Tuple[str, str]], aggregator: Aggregator, + resource: Resource, ): self.instrument = instrument self.labels = labels self.aggregator = aggregator + self.resource = resource class MetricsExporter: @@ -77,11 +80,12 @@ def export( ) -> "MetricsExportResult": for record in metric_records: print( - '{}(data="{}", labels="{}", value={})'.format( + '{}(data="{}", labels="{}", value={}, resource={})'.format( type(self).__name__, record.instrument, record.labels, record.aggregator.checkpoint, + record.resource.attributes, ) ) return MetricsExportResult.SUCCESS diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py index c012d7382b..fa16f5f4ea 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py @@ -15,6 +15,7 @@ from typing import Sequence from opentelemetry.sdk.metrics.export import MetricRecord +from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util import get_dict_as_key @@ -26,13 +27,14 @@ class Processor: will be sent to an exporter for exporting. """ - def __init__(self, stateful: bool): + def __init__(self, stateful: bool, resource: Resource): self._batch_map = {} # stateful=True indicates the processor computes checkpoints from over # the process lifetime. False indicates the processor computes # checkpoints which describe the updates of a single collection period # (deltas) self.stateful = stateful + self._resource = resource def checkpoint_set(self) -> Sequence[MetricRecord]: """Returns a list of MetricRecords used for exporting. @@ -46,7 +48,9 @@ def checkpoint_set(self) -> Sequence[MetricRecord]: (instrument, aggregator_type, _, labels), aggregator, ) in self._batch_map.items(): - metric_records.append(MetricRecord(instrument, labels, aggregator)) + metric_records.append( + MetricRecord(instrument, labels, aggregator, self._resource) + ) return metric_records def finished_collection(self): diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index efa6bcd24e..b0c74e7093 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -31,13 +31,15 @@ ) from opentelemetry.sdk.metrics.export.controller import PushController from opentelemetry.sdk.metrics.export.processor import Processor +from opentelemetry.sdk.resources import Resource # pylint: disable=protected-access class TestConsoleMetricsExporter(unittest.TestCase): # pylint: disable=no-self-use def test_export(self): - meter = metrics.MeterProvider().get_meter(__name__) + meter_provider = metrics.MeterProvider() + meter = meter_provider.get_meter(__name__) exporter = ConsoleMetricsExporter() metric = metrics.Counter( "available memory", @@ -49,12 +51,15 @@ def test_export(self): ) labels = {"environment": "staging"} aggregator = SumAggregator() - record = MetricRecord(metric, labels, aggregator) - result = '{}(data="{}", labels="{}", value={})'.format( + record = MetricRecord( + metric, labels, aggregator, meter_provider.resource + ) + result = '{}(data="{}", labels="{}", value={}, resource={})'.format( ConsoleMetricsExporter.__name__, metric, labels, aggregator.checkpoint, + meter_provider.resource.attributes, ) with mock.patch("sys.stdout") as mock_stdout: exporter.export([record]) @@ -63,8 +68,9 @@ def test_export(self): class TestProcessor(unittest.TestCase): def test_checkpoint_set(self): - meter = metrics.MeterProvider().get_meter(__name__) - processor = Processor(True) + meter_provider = metrics.MeterProvider() + meter = meter_provider.get_meter(__name__) + processor = Processor(True, meter_provider.resource) aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", "bytes", int, meter @@ -81,13 +87,14 @@ def test_checkpoint_set(self): self.assertEqual(records[0].aggregator, aggregator) def test_checkpoint_set_empty(self): - processor = Processor(True) + processor = Processor(True, Resource.create_empty()) records = processor.checkpoint_set() self.assertEqual(len(records), 0) def test_finished_collection_stateless(self): - meter = metrics.MeterProvider().get_meter(__name__) - processor = Processor(False) + meter_provider = metrics.MeterProvider() + meter = meter_provider.get_meter(__name__) + processor = Processor(False, meter_provider.resource) aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", "bytes", int, meter @@ -101,8 +108,9 @@ def test_finished_collection_stateless(self): self.assertEqual(len(processor._batch_map), 0) def test_finished_collection_stateful(self): - meter = metrics.MeterProvider().get_meter(__name__) - processor = Processor(True) + meter_provider = metrics.MeterProvider() + meter = meter_provider.get_meter(__name__) + processor = Processor(True, meter_provider.resource) aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", "bytes", int, meter @@ -116,8 +124,9 @@ def test_finished_collection_stateful(self): self.assertEqual(len(processor._batch_map), 1) def test_processor_process_exists(self): - meter = metrics.MeterProvider().get_meter(__name__) - processor = Processor(True) + meter_provider = metrics.MeterProvider() + meter = meter_provider.get_meter(__name__) + processor = Processor(True, meter_provider.resource) aggregator = SumAggregator() aggregator2 = SumAggregator() metric = metrics.Counter( @@ -137,8 +146,9 @@ def test_processor_process_exists(self): self.assertEqual(processor._batch_map.get(batch_key).checkpoint, 1.0) def test_processor_process_not_exists(self): - meter = metrics.MeterProvider().get_meter(__name__) - processor = Processor(True) + meter_provider = metrics.MeterProvider() + meter = meter_provider.get_meter(__name__) + processor = Processor(True, meter_provider.resource) aggregator = SumAggregator() metric = metrics.Counter( "available memory", "available memory", "bytes", int, meter @@ -156,11 +166,15 @@ def test_processor_process_not_exists(self): self.assertEqual(processor._batch_map.get(batch_key).checkpoint, 1.0) def test_processor_process_not_stateful(self): - meter = metrics.MeterProvider().get_meter(__name__) - processor = Processor(True) + meter_provider = metrics.MeterProvider() + processor = Processor(True, meter_provider.resource) aggregator = SumAggregator() metric = metrics.Counter( - "available memory", "available memory", "bytes", int, meter + "available memory", + "available memory", + "bytes", + int, + meter_provider.get_meter(__name__), ) labels = () _batch_map = {} diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 2f833f4782..1fcb2bbda1 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -33,14 +33,12 @@ def test_stateful(self): def test_resource(self): resource = resources.Resource.create({}) meter_provider = metrics.MeterProvider(resource=resource) - meter = meter_provider.get_meter(__name__) - self.assertIs(meter.resource, resource) + self.assertIs(meter_provider.resource, resource) def test_resource_empty(self): meter_provider = metrics.MeterProvider() - meter = meter_provider.get_meter(__name__) # pylint: disable=protected-access - self.assertEqual(meter.resource, resources._DEFAULT_RESOURCE) + self.assertEqual(meter_provider.resource, resources._DEFAULT_RESOURCE) def test_start_pipeline(self): exporter = mock.Mock() @@ -167,7 +165,7 @@ def test_create_metric(self): self.assertIsInstance(counter, metrics.Counter) self.assertEqual(counter.value_type, int) self.assertEqual(counter.name, "name") - self.assertIs(counter.meter.resource, resource) + self.assertIs(meter_provider.resource, resource) self.assertEqual(counter.meter, meter) def test_create_updowncounter(self): From 68b7ceac1df2498ee5bfc41a1998102e8b5b0064 Mon Sep 17 00:00:00 2001 From: paulroche Date: Wed, 7 Oct 2020 11:28:04 -0700 Subject: [PATCH 0600/1517] fix link to opentelemetry exporters (#1211) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 085fb573ac..c9c12e8a84 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,7 +30,7 @@ In addition, there are several extension packages which can be installed separat pip install opentelemetry-instrumentation-{instrumentation} These are for exporter and instrumentation packages respectively. -The packages can be found in :scm_web:`instrumentation/ directory of the repository `. +The packages can be found in :scm_web:`instrumentation/ directory of the repository `. Extensions ---------- From 4a66652a53d0953435a0e4b82a39cf3a2ead5f23 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Thu, 8 Oct 2020 08:22:58 +0530 Subject: [PATCH 0601/1517] Added ability to extract span attributes from tornado request objects (#1178) OTEL_PYTHON_TONADO_TRACED_REQUEST_ATTRS env var can be set to a command separated list of attributes names that will be extracted from Tornado's request object and set as attributes on spans. Co-authored-by: (Eliseo) Nathaniel Ruiz Nowell --- .../instrumentation/django/middleware.py | 8 ++++---- .../instrumentation/falcon/__init__.py | 14 +++++++++----- .../CHANGELOG.md | 2 ++ .../README.rst | 12 ++++++++++++ .../instrumentation/tornado/__init__.py | 14 +++++++++++++- .../tests/test_instrumentation.py | 15 +++++++++++++++ .../src/opentelemetry/instrumentation/utils.py | 15 +++++++++++++++ 7 files changed, 70 insertions(+), 10 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index 07e3eb710b..11991413eb 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -17,6 +17,7 @@ from opentelemetry.configuration import Configuration from opentelemetry.context import attach, detach from opentelemetry.instrumentation.django.version import __version__ +from opentelemetry.instrumentation.utils import extract_attributes_from_object from opentelemetry.instrumentation.wsgi import ( add_response_attributes, collect_request_attributes, @@ -111,10 +112,9 @@ def process_request(self, request): if span.is_recording(): attributes = collect_request_attributes(environ) - for attr in self._traced_request_attrs: - value = getattr(request, attr, None) - if value is not None: - attributes[attr] = str(value) + attributes = extract_attributes_from_object( + request, self._traced_request_attrs, attributes + ) for key, value in attributes.items(): span.set_attribute(key, value) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py index 660fc23063..bfcd45a8b5 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py @@ -53,7 +53,10 @@ def on_get(self, req, resp): from opentelemetry.configuration import Configuration from opentelemetry.instrumentation.falcon.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.utils import http_status_to_canonical_code +from opentelemetry.instrumentation.utils import ( + extract_attributes_from_object, + http_status_to_canonical_code, +) from opentelemetry.trace.status import Status from opentelemetry.util import ExcludeList, time_ns @@ -162,10 +165,11 @@ def process_request(self, req, resp): if not span: return - for attr in self._traced_request_attrs: - value = getattr(req, attr, None) - if value is not None: - span.set_attribute(attr, str(value)) + attributes = extract_attributes_from_object( + req, self._traced_request_attrs + ) + for key, value in attributes.items(): + span.set_attribute(key, value) def process_resource(self, req, resp, resource, params): span = req.env.get(_ENVIRON_SPAN_KEY) diff --git a/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md index 7cc628718d..e82446d66d 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Added support for `OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS` ([#1178](https://github.com/open-telemetry/opentelemetry-python/pull/1178)) + ## Version 0.13b0 Released 2020-09-17 diff --git a/instrumentation/opentelemetry-instrumentation-tornado/README.rst b/instrumentation/opentelemetry-instrumentation-tornado/README.rst index d84fbd0412..088c7f0e85 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/README.rst +++ b/instrumentation/opentelemetry-instrumentation-tornado/README.rst @@ -31,6 +31,18 @@ A comma separated list of paths that should not be automatically traced. For exa Then any requests made to ``/healthz`` and ``/ping`` will not be automatically traced. +Request attributes +******************** +To extract certain attributes from Tornado's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS`` to a comma +delimited list of request attribute names. + +For example, + +:: + + export OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS='uri,query' + +will extract path_info and content_type attributes from every traced request and add them as span attributes. References ---------- diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py index 6379d841a0..5357be6d0f 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py @@ -50,6 +50,7 @@ def get(self): from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.tornado.version import __version__ from opentelemetry.instrumentation.utils import ( + extract_attributes_from_object, http_status_to_canonical_code, unwrap, ) @@ -71,7 +72,17 @@ def get_excluded_urls(): return ExcludeList(urls) +def get_traced_request_attrs(): + attrs = configuration.Configuration().TORNADO_TRACED_REQUEST_ATTRS or "" + if attrs: + attrs = [attr.strip() for attr in attrs.split(",")] + else: + attrs = [] + return attrs + + _excluded_urls = get_excluded_urls() +_traced_attrs = get_traced_request_attrs() class TornadoInstrumentor(BaseInstrumentor): @@ -196,7 +207,7 @@ def _get_attributes_from_request(request): if request.remote_ip: attrs["net.peer.ip"] = request.remote_ip - return attrs + return extract_attributes_from_object(request, _traced_attrs, attrs) def _get_operation_name(handler, request): @@ -211,6 +222,7 @@ def _start_span(tracer, handler, start_time) -> _TraceContext: _get_header_from_request_headers, handler.request.headers, ) ) + span = tracer.start_span( _get_operation_name(handler, handler.request), kind=trace.SpanKind.SERVER, diff --git a/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py b/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py index d900b5d360..eb2852f112 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py @@ -354,6 +354,21 @@ def test_excluded(path): test_excluded("/healthz") test_excluded("/ping") + @patch( + "opentelemetry.instrumentation.tornado._traced_attrs", + ["uri", "full_url", "query"], + ) + def test_traced_attrs(self): + self.fetch("/ping?q=abc&b=123") + spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) + self.assertEqual(len(spans), 2) + server_span = spans[0] + self.assertEqual(server_span.kind, SpanKind.SERVER) + self.assert_span_has_attributes( + server_span, {"uri": "/ping?q=abc&b=123", "query": "q=abc&b=123"} + ) + self.memory_exporter.clear() + class TestTornadoUninstrument(TornadoTest): def test_uninstrument(self): diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py index 8553b1bc63..6220854ad5 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -12,11 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, Sequence + from wrapt import ObjectProxy from opentelemetry.trace.status import StatusCanonicalCode +def extract_attributes_from_object( + obj: any, attributes: Sequence[str], existing: Dict[str, str] = None +) -> Dict[str, str]: + extracted = {} + if existing: + extracted.update(existing) + for attr in attributes: + value = getattr(obj, attr, None) + if value is not None: + extracted[attr] = str(value) + return extracted + + def http_status_to_canonical_code( status: int, allow_redirect: bool = True ) -> StatusCanonicalCode: From 0e852ea393aa042d429657b9d22208eb33b52d67 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 8 Oct 2020 08:39:04 -0700 Subject: [PATCH 0602/1517] Parent is now always passed in via Context, intead of Span or SpanContext (#1146) Co-authored-by: Diego Hurtado --- .../exporter/datadog/exporter.py | 8 +- .../exporter/datadog/propagator.py | 2 +- .../exporter/datadog/spanprocessor.py | 4 +- .../tests/test_datadog_exporter.py | 6 +- .../tests/test_datadog_format.py | 44 ++++----- .../examples/jaeger_exporter_example.py | 2 +- .../opentelemetry/exporter/jaeger/__init__.py | 2 +- .../tests/test_jaeger_exporter.py | 6 +- .../tests/test_otcollector_trace_exporter.py | 10 +- .../opentelemetry/exporter/zipkin/__init__.py | 4 +- .../tests/test_zipkin_exporter.py | 12 ++- .../instrumentation/celery/__init__.py | 5 +- .../tests/test_jinja2.py | 18 ++-- .../opentracing_shim/__init__.py | 16 ++-- .../tests/test_shim.py | 35 ++++--- .../tests/test_requests_integration.py | 4 +- .../tests/test_sqlite3.py | 2 +- opentelemetry-api/CHANGELOG.md | 2 + .../src/opentelemetry/trace/__init__.py | 31 +++--- .../trace/propagation/tracecontext.py | 2 +- .../src/opentelemetry/trace/span.py | 4 +- .../propagators/test_global_httptextformat.py | 2 +- .../tests/test_implementation.py | 8 +- .../test_tracecontexthttptextformat.py | 26 ++--- .../tests/trace/test_defaultspan.py | 6 +- opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 40 ++++---- .../sdk/trace/propagation/b3_format.py | 2 +- .../src/opentelemetry/sdk/trace/sampling.py | 16 ++-- .../tests/context/test_asyncio.py | 2 +- .../tests/trace/propagation/test_b3_format.py | 22 ++--- .../tests/trace/test_implementation.py | 8 +- opentelemetry-sdk/tests/trace/test_trace.py | 94 ++++++++++++------- .../tests/mysql/test_mysql_functional.py | 2 +- .../tests/postgres/test_aiopg_functional.py | 4 +- .../tests/postgres/test_psycopg_functional.py | 2 +- .../tests/pymongo/test_pymongo_functional.py | 2 +- .../tests/pymysql/test_pymysql_functional.py | 2 +- .../tests/redis/test_redis_functional.py | 2 +- .../tests/sqlalchemy_tests/mixins.py | 2 +- .../src/opentelemetry/test/mock_textmap.py | 4 +- 41 files changed, 261 insertions(+), 206 deletions(-) diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py index 2b36299989..36335c2358 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py @@ -189,12 +189,12 @@ def _translate_to_datadog(self, spans): def _get_trace_ids(span): """Extract tracer ids from span""" - ctx = span.get_context() + ctx = span.get_span_context() trace_id = ctx.trace_id span_id = ctx.span_id if isinstance(span.parent, trace_api.Span): - parent_id = span.parent.get_context().span_id + parent_id = span.parent.get_span_context().span_id elif isinstance(span.parent, trace_api.SpanContext): parent_id = span.parent.span_id else: @@ -255,13 +255,13 @@ def _get_exc_info(span): def _get_origin(span): - ctx = span.get_context() + ctx = span.get_span_context() origin = ctx.trace_state.get(DD_ORIGIN) return origin def _get_sampling_rate(span): - ctx = span.get_context() + ctx = span.get_span_context() return ( span.sampler.rate if ctx.trace_flags.sampled diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py index d2e60476e6..3ad9fa1ae2 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py @@ -86,7 +86,7 @@ def inject( context: typing.Optional[Context] = None, ) -> None: span = get_current_span(context) - span_context = span.get_context() + span_context = span.get_span_context() if span_context == trace.INVALID_SPAN_CONTEXT: return sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py index 600778c88c..603ea5024e 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py @@ -82,7 +82,7 @@ def __init__( self.worker_thread.start() def on_start(self, span: Span) -> None: - ctx = span.get_context() + ctx = span.get_span_context() trace_id = ctx.trace_id with self.traces_lock: @@ -102,7 +102,7 @@ def on_end(self, span: Span) -> None: logger.warning("Already shutdown, dropping span.") return - ctx = span.get_context() + ctx = span.get_span_context() trace_id = ctx.trace_id with self.traces_lock: diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py index 27b4d0fa03..98e894f94d 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py @@ -178,7 +178,7 @@ def test_translate_to_datadog(self): span_context = trace_api.SpanContext( trace_id, span_id, is_remote=False ) - parent_context = trace_api.SpanContext( + parent_span_context = trace_api.SpanContext( trace_id, parent_id, is_remote=False ) other_context = trace_api.SpanContext( @@ -191,14 +191,14 @@ def test_translate_to_datadog(self): trace._Span( name=span_names[0], context=span_context, - parent=parent_context, + parent=parent_span_context, kind=trace_api.SpanKind.CLIENT, instrumentation_info=instrumentation_info, resource=Resource({}), ), trace._Span( name=span_names[1], - context=parent_context, + context=parent_span_context, parent=None, instrumentation_info=instrumentation_info, resource=resource_without_service, diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py index 3c045539fd..8480374471 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py @@ -51,7 +51,7 @@ def test_malformed_headers(self): malformed_parent_id_key: self.serialized_parent_id, }, ) - ).get_context() + ).get_span_context() self.assertNotEqual(context.trace_id, int(self.serialized_trace_id)) self.assertNotEqual(context.span_id, int(self.serialized_parent_id)) @@ -64,7 +64,7 @@ def test_missing_trace_id(self): } ctx = FORMAT.extract(get_as_list, carrier) - span_context = get_current_span(ctx).get_context() + span_context = get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) def test_missing_parent_id(self): @@ -74,12 +74,12 @@ def test_missing_parent_id(self): } ctx = FORMAT.extract(get_as_list, carrier) - span_context = get_current_span(ctx).get_context() + span_context = get_current_span(ctx).get_span_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) def test_context_propagation(self): """Test the propagation of Datadog headers.""" - parent_context = get_current_span( + parent_span_context = get_current_span( FORMAT.extract( get_as_list, { @@ -89,31 +89,31 @@ def test_context_propagation(self): FORMAT.ORIGIN_KEY: self.serialized_origin, }, ) - ).get_context() + ).get_span_context() self.assertEqual( - parent_context.trace_id, int(self.serialized_trace_id) + parent_span_context.trace_id, int(self.serialized_trace_id) ) self.assertEqual( - parent_context.span_id, int(self.serialized_parent_id) + parent_span_context.span_id, int(self.serialized_parent_id) ) - self.assertEqual(parent_context.trace_flags, constants.AUTO_KEEP) + self.assertEqual(parent_span_context.trace_flags, constants.AUTO_KEEP) self.assertEqual( - parent_context.trace_state.get(constants.DD_ORIGIN), + parent_span_context.trace_state.get(constants.DD_ORIGIN), self.serialized_origin, ) - self.assertTrue(parent_context.is_remote) + self.assertTrue(parent_span_context.is_remote) child = trace._Span( "child", trace_api.SpanContext( - parent_context.trace_id, + parent_span_context.trace_id, trace_api.RandomIdsGenerator().generate_span_id(), is_remote=False, - trace_flags=parent_context.trace_flags, - trace_state=parent_context.trace_state, + trace_flags=parent_span_context.trace_flags, + trace_state=parent_span_context.trace_state, ), - parent=parent_context, + parent=parent_span_context, ) child_carrier = {} @@ -136,7 +136,7 @@ def test_context_propagation(self): def test_sampling_priority_auto_reject(self): """Test sampling priority rejected.""" - parent_context = get_current_span( + parent_span_context = get_current_span( FORMAT.extract( get_as_list, { @@ -145,20 +145,22 @@ def test_sampling_priority_auto_reject(self): FORMAT.SAMPLING_PRIORITY_KEY: str(constants.AUTO_REJECT), }, ) - ).get_context() + ).get_span_context() - self.assertEqual(parent_context.trace_flags, constants.AUTO_REJECT) + self.assertEqual( + parent_span_context.trace_flags, constants.AUTO_REJECT + ) child = trace._Span( "child", trace_api.SpanContext( - parent_context.trace_id, + parent_span_context.trace_id, trace_api.RandomIdsGenerator().generate_span_id(), is_remote=False, - trace_flags=parent_context.trace_flags, - trace_state=parent_context.trace_state, + trace_flags=parent_span_context.trace_flags, + trace_state=parent_span_context.trace_state, ), - parent=parent_context, + parent=parent_span_context, ) child_carrier = {} diff --git a/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py b/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py index 68453fd9fa..0552f75183 100644 --- a/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py +++ b/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py @@ -34,7 +34,7 @@ foo.set_attribute("my_atribbute", True) foo.add_event("event in foo", {"name": "foo1"}) with tracer.start_as_current_span( - "bar", links=[trace.Link(foo.get_context())] + "bar", links=[trace.Link(foo.get_span_context())] ) as bar: time.sleep(0.2) bar.set_attribute("speed", 100.0) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index 354fe8b85c..3cfd3fca43 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -192,7 +192,7 @@ def _translate_to_jaeger(spans: Span): jaeger_spans = [] for span in spans: - ctx = span.get_context() + ctx = span.get_span_context() trace_id = ctx.trace_id span_id = ctx.span_id diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py index bb4920f7b3..0a01bcb234 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py @@ -144,7 +144,7 @@ def test_translate_to_jaeger(self): span_context = trace_api.SpanContext( trace_id, span_id, is_remote=False ) - parent_context = trace_api.SpanContext( + parent_span_context = trace_api.SpanContext( trace_id, parent_id, is_remote=False ) other_context = trace_api.SpanContext( @@ -190,13 +190,13 @@ def test_translate_to_jaeger(self): trace._Span( name=span_names[0], context=span_context, - parent=parent_context, + parent=parent_span_context, events=(event,), links=(link,), kind=trace_api.SpanKind.CLIENT, ), trace._Span( - name=span_names[1], context=parent_context, parent=None + name=span_names[1], context=parent_span_context, parent=None ), trace._Span( name=span_names[2], context=other_context, parent=None diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py index 3721139efc..c49310b30a 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py @@ -96,7 +96,7 @@ def test_translate_to_collector(self): trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state=trace_api.TraceState({"testKey": "testValue"}), ) - parent_context = trace_api.SpanContext( + parent_span_context = trace_api.SpanContext( trace_id, parent_id, is_remote=False ) other_context = trace_api.SpanContext( @@ -118,19 +118,19 @@ def test_translate_to_collector(self): context=other_context, attributes=link_attributes ) link_2 = trace_api.Link( - context=parent_context, attributes=link_attributes + context=parent_span_context, attributes=link_attributes ) span_1 = trace._Span( name="test1", context=span_context, - parent=parent_context, + parent=parent_span_context, events=(event,), links=(link_1,), kind=trace_api.SpanKind.CLIENT, ) span_2 = trace._Span( name="test2", - context=parent_context, + context=parent_span_context, parent=None, kind=trace_api.SpanKind.SERVER, ) @@ -138,7 +138,7 @@ def test_translate_to_collector(self): name="test3", context=other_context, links=(link_2,), - parent=span_2.get_context(), + parent=span_2.get_span_context(), ) otel_spans = [span_1, span_2, span_3] otel_spans[0].start(start_time=start_times[0]) diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index bacfcc278d..c8578c9649 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -159,7 +159,7 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): zipkin_spans = [] for span in spans: - context = span.get_context() + context = span.get_span_context() trace_id = context.trace_id span_id = context.span_id @@ -205,7 +205,7 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): if isinstance(span.parent, Span): zipkin_span["parentId"] = format( - span.parent.get_context().span_id, "016x" + span.parent.get_span_context().span_id, "016x" ) elif isinstance(span.parent, SpanContext): zipkin_span["parentId"] = format(span.parent.span_id, "016x") diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 8d6d8bb860..c3098da884 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -127,7 +127,7 @@ def test_export(self): is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ) - parent_context = trace_api.SpanContext( + parent_span_context = trace_api.SpanContext( trace_id, parent_id, is_remote=False ) other_context = trace_api.SpanContext( @@ -157,12 +157,12 @@ def test_export(self): trace._Span( name=span_names[0], context=span_context, - parent=parent_context, + parent=parent_span_context, events=(event,), links=(link,), ), trace._Span( - name=span_names[1], context=parent_context, parent=None + name=span_names[1], context=parent_span_context, parent=None ), trace._Span( name=span_names[2], context=other_context, parent=None @@ -328,12 +328,14 @@ def test_zero_padding(self): is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ) - parent_context = trace_api.SpanContext( + parent_span_context = trace_api.SpanContext( trace_id, parent_id, is_remote=False ) otel_span = trace._Span( - name=span_names[0], context=span_context, parent=parent_context, + name=span_names[0], + context=span_context, + parent=parent_span_context, ) otel_span.start(start_time=start_time) diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py index 4768e93d18..3bd912c5ed 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py @@ -118,14 +118,13 @@ def _trace_prerun(self, *args, **kwargs): return request = task.request - tracectx = propagators.extract(carrier_extractor, request) or {} - parent = get_current_span(tracectx) + tracectx = propagators.extract(carrier_extractor, request) or None logger.debug("prerun signal start task_id=%s", task_id) operation_name = "{0}/{1}".format(_TASK_RUN, task.name) span = self._tracer.start_span( - operation_name, parent=parent, kind=trace.SpanKind.CONSUMER + operation_name, context=tracectx, kind=trace.SpanKind.CONSUMER ) activation = self._tracer.use_span(span, end_on_exit=True) diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py b/instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py index 7f5b52c936..717431abbb 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py @@ -49,8 +49,8 @@ def test_render_inline_template_with_root(self): # pylint:disable=unbalanced-tuple-unpacking render, template, root = spans[:3] - self.assertIs(render.parent, root.get_context()) - self.assertIs(template.parent, root.get_context()) + self.assertIs(render.parent, root.get_span_context()) + self.assertIs(template.parent, root.get_span_context()) self.assertIsNone(root.parent) def test_render_inline_template(self): @@ -88,8 +88,8 @@ def test_generate_inline_template_with_root(self): # pylint:disable=unbalanced-tuple-unpacking template, generate, root = spans - self.assertIs(generate.parent, root.get_context()) - self.assertIs(template.parent, root.get_context()) + self.assertIs(generate.parent, root.get_span_context()) + self.assertIs(template.parent, root.get_span_context()) self.assertIsNone(root.parent) def test_generate_inline_template(self): @@ -131,11 +131,11 @@ def test_file_template_with_root(self): # pylint:disable=unbalanced-tuple-unpacking compile2, load2, compile1, load1, render, root = spans - self.assertIs(compile2.parent, load2.get_context()) - self.assertIs(load2.parent, root.get_context()) - self.assertIs(compile1.parent, load1.get_context()) - self.assertIs(load1.parent, render.get_context()) - self.assertIs(render.parent, root.get_context()) + self.assertIs(compile2.parent, load2.get_span_context()) + self.assertIs(load2.parent, root.get_span_context()) + self.assertIs(compile1.parent, load1.get_span_context()) + self.assertIs(load1.parent, render.get_span_context()) + self.assertIs(render.parent, root.get_span_context()) self.assertIsNone(root.parent) def test_file_template(self): diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py index 6bb22130d8..63be13fe48 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py @@ -378,7 +378,7 @@ def from_context_manager(cls, manager: "ScopeManagerShim", span_cm): """ otel_span = span_cm.__enter__() - span_context = SpanContextShim(otel_span.get_context()) + span_context = SpanContextShim(otel_span.get_span_context()) span = SpanShim(manager.tracer, span_context, otel_span) return cls(manager, span, span_cm) @@ -474,13 +474,13 @@ def active(self) -> "ScopeShim": """ span = get_current_span() - if span.get_context() == INVALID_SPAN_CONTEXT: + if span.get_span_context() == INVALID_SPAN_CONTEXT: return None try: return get_value("scope_shim") except KeyError: - span_context = SpanContextShim(span.get_context()) + span_context = SpanContextShim(span.get_span_context()) wrapped_span = SpanShim(self._tracer, span_context, span) return ScopeShim(self, span=wrapped_span) @@ -630,6 +630,10 @@ def start_span( # Use the specified parent or the active span if possible. Otherwise, # use a `None` parent, which triggers the creation of a new trace. parent = child_of.unwrap() if child_of else None + if isinstance(parent, OtelSpanContext): + parent = DefaultSpan(parent) + + parent_span_context = set_span_in_context(parent) links = [] if references: @@ -645,13 +649,13 @@ def start_span( span = self._otel_tracer.start_span( operation_name, - parent, + context=parent_span_context, links=links, attributes=tags, start_time=start_time_ns, ) - context = SpanContextShim(span.get_context()) + context = SpanContextShim(span.get_span_context()) return SpanShim(self, context, span) def inject(self, span_context, format: object, carrier: object): @@ -714,7 +718,7 @@ def get_as_list(dict_object, key): ctx = propagator.extract(get_as_list, carrier) span = get_current_span(ctx) if span is not None: - otel_context = span.get_context() + otel_context = span.get_span_context() else: otel_context = INVALID_SPAN_CONTEXT diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py index 672e7b02f9..151ca07ba9 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py @@ -284,13 +284,17 @@ def test_parent_child_implicit(self): ) # Verify parent-child relationship. - parent_trace_id = parent.span.unwrap().get_context().trace_id - child_trace_id = child.span.unwrap().get_context().trace_id + parent_trace_id = ( + parent.span.unwrap().get_span_context().trace_id + ) + child_trace_id = ( + child.span.unwrap().get_span_context().trace_id + ) self.assertEqual(parent_trace_id, child_trace_id) self.assertEqual( child.span.unwrap().parent, - parent.span.unwrap().get_context(), + parent.span.unwrap().get_span_context(), ) # Verify parent span becomes the active span again. @@ -314,23 +318,26 @@ def test_parent_child_explicit_span(self): with self.shim.start_active_span( "ChildSpan", child_of=parent ) as child: - parent_trace_id = parent.unwrap().get_context().trace_id - child_trace_id = child.span.unwrap().get_context().trace_id + parent_trace_id = parent.unwrap().get_span_context().trace_id + child_trace_id = ( + child.span.unwrap().get_span_context().trace_id + ) self.assertEqual(child_trace_id, parent_trace_id) self.assertEqual( - child.span.unwrap().parent, parent.unwrap().get_context() + child.span.unwrap().parent, + parent.unwrap().get_span_context(), ) with self.shim.start_span("ParentSpan") as parent: child = self.shim.start_span("ChildSpan", child_of=parent) - parent_trace_id = parent.unwrap().get_context().trace_id - child_trace_id = child.unwrap().get_context().trace_id + parent_trace_id = parent.unwrap().get_span_context().trace_id + child_trace_id = child.unwrap().get_span_context().trace_id self.assertEqual(child_trace_id, parent_trace_id) self.assertEqual( - child.unwrap().parent, parent.unwrap().get_context() + child.unwrap().parent, parent.unwrap().get_span_context() ) child.finish() @@ -344,8 +351,10 @@ def test_parent_child_explicit_span_context(self): with self.shim.start_active_span( "ChildSpan", child_of=parent.context ) as child: - parent_trace_id = parent.unwrap().get_context().trace_id - child_trace_id = child.span.unwrap().get_context().trace_id + parent_trace_id = parent.unwrap().get_span_context().trace_id + child_trace_id = ( + child.span.unwrap().get_span_context().trace_id + ) self.assertEqual(child_trace_id, parent_trace_id) self.assertEqual( @@ -356,8 +365,8 @@ def test_parent_child_explicit_span_context(self): with self.shim.start_span( "SpanWithContextParent", child_of=parent.context ) as child: - parent_trace_id = parent.unwrap().get_context().trace_id - child_trace_id = child.unwrap().get_context().trace_id + parent_trace_id = parent.unwrap().get_span_context().trace_id + child_trace_id = child.unwrap().get_span_context().trace_id self.assertEqual(child_trace_id, parent_trace_id) self.assertEqual( diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py index 2d3636284b..678499e879 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py @@ -198,12 +198,12 @@ def test_distributed_context(self): headers = dict(httpretty.last_request().headers) self.assertIn(MockTextMapPropagator.TRACE_ID_KEY, headers) self.assertEqual( - str(span.get_context().trace_id), + str(span.get_span_context().trace_id), headers[MockTextMapPropagator.TRACE_ID_KEY], ) self.assertIn(MockTextMapPropagator.SPAN_ID_KEY, headers) self.assertEqual( - str(span.get_context().span_id), + str(span.get_span_context().span_id), headers[MockTextMapPropagator.SPAN_ID_KEY], ) diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py b/instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py index a4444e7d93..0e385cf3e7 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py @@ -52,7 +52,7 @@ def validate_spans(self): self.assertEqual(root_span.name, "rootSpan") self.assertEqual(child_span.name, "sqlite3") self.assertIsNotNone(child_span.parent) - self.assertIs(child_span.parent, root_span.get_context()) + self.assertIs(child_span.parent, root_span.get_span_context()) self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) def test_execute(self): diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index adccf96cdf..6504826c57 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -12,6 +12,8 @@ ([#1194](https://github.com/open-telemetry/opentelemetry-python/pull/1194)) - Make instances of SpanContext immutable ([#1134](https://github.com/open-telemetry/opentelemetry-python/pull/1134)) +- Parent is now always passed in via Context, intead of Span or SpanContext + ([#1146](https://github.com/open-telemetry/opentelemetry-python/pull/1146)) ## Version 0.13b0 diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 5aa5c7f33c..f10cb0312a 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -49,8 +49,11 @@ When creating a span that's "detached" from the context the active span doesn't change, and the caller is responsible for managing the span's lifetime:: - # Explicit parent span assignment - child = tracer.start_span("child", parent=parent) + # Explicit parent span assignment is done via the Context + from opentelemetry.trace import set_span_in_context + + context = set_span_in_context(parent) + child = tracer.start_span("child", context=context) try: do_work(span=child) @@ -77,6 +80,7 @@ from contextlib import contextmanager from logging import getLogger +from opentelemetry.context.context import Context from opentelemetry.trace.ids_generator import IdsGenerator, RandomIdsGenerator from opentelemetry.trace.propagation import ( get_current_span, @@ -102,9 +106,6 @@ logger = getLogger(__name__) -# TODO: quarantine -ParentSpan = typing.Optional[typing.Union["Span", "SpanContext"]] - class LinkBase(abc.ABC): def __init__(self, context: "SpanContext") -> None: @@ -228,7 +229,7 @@ class Tracer(abc.ABC): def start_span( self, name: str, - parent: ParentSpan = CURRENT_SPAN, + context: typing.Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: typing.Sequence[Link] = (), @@ -242,8 +243,9 @@ def start_span( method, see :meth:`start_as_current_span`. By default the current span in the context will be used as parent, but an - explicit parent can also be specified, either a `Span` or a `opentelemetry.trace.SpanContext`. - If the specified value is `None`, the created span will be a root span. + explicit context can also be specified, by passing in a `Context` containing + a current `Span`. If there is no current span in the global `Context` or in + the specified context, the created span will be a root span. The span can be used as a context manager. On exiting the context manager, the span's end() method will be called. @@ -257,7 +259,8 @@ def start_span( Args: name: The name of the span to be created. - parent: The span's parent. Defaults to the current span. + context: An optional Context containing the span's parent. Defaults to the + global context. kind: The span's kind (relationship to parent). Note that is meaningful even if there is no parent. attributes: The span's attributes. @@ -278,7 +281,7 @@ def start_span( def start_as_current_span( self, name: str, - parent: ParentSpan = CURRENT_SPAN, + context: typing.Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: typing.Sequence[Link] = (), @@ -316,7 +319,8 @@ def start_as_current_span( Args: name: The name of the span to be created. - parent: The span's parent. Defaults to the current span. + context: An optional Context containing the span's parent. Defaults to the + global context. kind: The span's kind (relationship to parent). Note that is meaningful even if there is no parent. attributes: The span's attributes. @@ -365,7 +369,7 @@ class DefaultTracer(Tracer): def start_span( self, name: str, - parent: ParentSpan = Tracer.CURRENT_SPAN, + context: typing.Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: typing.Sequence[Link] = (), @@ -379,7 +383,7 @@ def start_span( def start_as_current_span( self, name: str, - parent: ParentSpan = Tracer.CURRENT_SPAN, + context: typing.Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: typing.Sequence[Link] = (), @@ -459,7 +463,6 @@ def get_tracer_provider() -> TracerProvider: "DefaultTracerProvider", "Link", "LinkBase", - "ParentSpan", "RandomIdsGenerator", "Span", "SpanContext", diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index 8627b9a65c..4b77246ac5 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -118,7 +118,7 @@ def inject( See `opentelemetry.trace.propagation.textmap.TextMapPropagator.inject` """ span = trace.get_current_span(context) - span_context = span.get_context() + span_context = span.get_span_context() if span_context == trace.INVALID_SPAN_CONTEXT: return traceparent_string = "00-{:032x}-{:016x}-{:02x}".format( diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 99620ed144..2b206468af 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -23,7 +23,7 @@ def end(self, end_time: typing.Optional[int] = None) -> None: """ @abc.abstractmethod - def get_context(self) -> "SpanContext": + def get_span_context(self) -> "SpanContext": """Gets the span's SpanContext. Get an immutable, serializable identifier for this span that can be @@ -237,7 +237,7 @@ class DefaultSpan(Span): def __init__(self, context: "SpanContext") -> None: self._context = context - def get_context(self) -> "SpanContext": + def get_span_context(self) -> "SpanContext": return self._context def is_recording(self) -> bool: diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index 9a97b28129..2668be27c3 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -48,7 +48,7 @@ def test_propagation(self): baggage_entries = baggage.get_all(context=ctx) expected = {"key1": "val1", "key2": "val2"} self.assertEqual(baggage_entries, expected) - span_context = get_current_span(context=ctx).get_context() + span_context = get_current_span(context=ctx).get_span_context() self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index 0d5b22b18f..8e18fcb48d 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -37,12 +37,14 @@ def test_default_tracer(self): tracer_provider = trace.DefaultTracerProvider() tracer = tracer_provider.get_tracer(__name__) with tracer.start_span("test") as span: - self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) + self.assertEqual( + span.get_span_context(), trace.INVALID_SPAN_CONTEXT + ) self.assertEqual(span, trace.INVALID_SPAN) self.assertIs(span.is_recording(), False) with tracer.start_span("test2") as span2: self.assertEqual( - span2.get_context(), trace.INVALID_SPAN_CONTEXT + span2.get_span_context(), trace.INVALID_SPAN_CONTEXT ) self.assertEqual(span2, trace.INVALID_SPAN) self.assertIs(span2.is_recording(), False) @@ -54,7 +56,7 @@ def test_span(self): def test_default_span(self): span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) - self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) + self.assertEqual(span.get_span_context(), trace.INVALID_SPAN_CONTEXT) self.assertIs(span.is_recording(), False) # METER diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 8abe419387..295e3971a3 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -43,7 +43,7 @@ def test_no_traceparent_header(self): """ output = {} # type:typing.Dict[str, typing.List[str]] span = trace.get_current_span(FORMAT.extract(get_as_list, output)) - self.assertIsInstance(span.get_context(), trace.SpanContext) + self.assertIsInstance(span.get_span_context(), trace.SpanContext) def test_headers_with_tracestate(self): """When there is a traceparent and tracestate header, data from @@ -62,7 +62,7 @@ def test_headers_with_tracestate(self): "tracestate": [tracestate_value], }, ) - ).get_context() + ).get_span_context() self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) self.assertEqual( @@ -109,7 +109,7 @@ def test_invalid_trace_id(self): }, ) ) - self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) + self.assertEqual(span.get_span_context(), trace.INVALID_SPAN_CONTEXT) def test_invalid_parent_id(self): """If the parent id is invalid, we must ignore the full traceparent @@ -140,7 +140,7 @@ def test_invalid_parent_id(self): }, ) ) - self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) + self.assertEqual(span.get_span_context(), trace.INVALID_SPAN_CONTEXT) def test_no_send_empty_tracestate(self): """If the tracestate is empty, do not set the header. @@ -179,7 +179,7 @@ def test_format_not_supported(self): }, ) ) - self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) + self.assertEqual(span.get_span_context(), trace.INVALID_SPAN_CONTEXT) def test_propagate_invalid_context(self): """Do not propagate invalid trace context.""" @@ -202,7 +202,7 @@ def test_tracestate_empty_header(self): }, ) ) - self.assertEqual(span.get_context().trace_state["foo"], "1") + self.assertEqual(span.get_span_context().trace_state["foo"], "1") def test_tracestate_header_with_trailing_comma(self): """Do not propagate invalid trace context. @@ -218,7 +218,7 @@ def test_tracestate_header_with_trailing_comma(self): }, ) ) - self.assertEqual(span.get_context().trace_state["foo"], "1") + self.assertEqual(span.get_span_context().trace_state["foo"], "1") def test_tracestate_keys(self): """Test for valid key patterns in the tracestate @@ -242,9 +242,13 @@ def test_tracestate_keys(self): }, ) ) - self.assertEqual(span.get_context().trace_state["1a-2f@foo"], "bar1") self.assertEqual( - span.get_context().trace_state["1a-_*/2b@foo"], "bar2" + span.get_span_context().trace_state["1a-2f@foo"], "bar1" + ) + self.assertEqual( + span.get_span_context().trace_state["1a-_*/2b@foo"], "bar2" + ) + self.assertEqual(span.get_span_context().trace_state["foo"], "bar3") + self.assertEqual( + span.get_span_context().trace_state["foo-_*/bar"], "bar4" ) - self.assertEqual(span.get_context().trace_state["foo"], "bar3") - self.assertEqual(span.get_context().trace_state["foo-_*/bar"], "bar4") diff --git a/opentelemetry-api/tests/trace/test_defaultspan.py b/opentelemetry-api/tests/trace/test_defaultspan.py index 67c2fc3352..73f068825b 100644 --- a/opentelemetry-api/tests/trace/test_defaultspan.py +++ b/opentelemetry-api/tests/trace/test_defaultspan.py @@ -27,9 +27,9 @@ def test_ctor(self): trace_state=trace.DEFAULT_TRACE_STATE, ) span = trace.DefaultSpan(context) - self.assertEqual(context, span.get_context()) + self.assertEqual(context, span.get_span_context()) def test_invalid_span(self): self.assertIsNotNone(trace.INVALID_SPAN) - self.assertIsNotNone(trace.INVALID_SPAN.get_context()) - self.assertFalse(trace.INVALID_SPAN.get_context().is_valid) + self.assertIsNotNone(trace.INVALID_SPAN.get_span_context()) + self.assertFalse(trace.INVALID_SPAN.get_span_context().is_valid) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 6890a2e2b7..796fa39166 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -18,6 +18,8 @@ ([#1162](https://github.com/open-telemetry/opentelemetry-python/pull/1162)) - Adding Resource to MeterRecord ([#1209](https://github.com/open-telemetry/opentelemetry-python/pull/1209)) +- Parent is now always passed in via Context, intead of Span or SpanContext + ([#1146](https://github.com/open-telemetry/opentelemetry-python/pull/1146)) ## Version 0.13b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 5e04d8991b..d0626a6568 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -350,7 +350,7 @@ class Span(trace_api.Span): name: The name of the operation this span represents context: The immutable span context parent: This span's parent's `opentelemetry.trace.SpanContext`, or - null if this is a root span + None if this is a root span sampler: The sampler used to create this span trace_config: TODO resource: Entity producing telemetry @@ -519,7 +519,7 @@ def to_json(self, indent=4): return json.dumps(f_span, indent=indent) - def get_context(self): + def get_span_context(self): return self.context def set_attribute(self, key: str, value: types.AttributeValue) -> None: @@ -699,13 +699,13 @@ def __init__( def start_as_current_span( self, name: str, - parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, + context: Optional[context_api.Context] = None, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, attributes: types.Attributes = None, links: Sequence[trace_api.Link] = (), record_exception: bool = True, ) -> Iterator[trace_api.Span]: - span = self.start_span(name, parent, kind, attributes, links,) + span = self.start_span(name, context, kind, attributes, links) return self.use_span( span, end_on_exit=True, record_exception=record_exception ) @@ -713,34 +713,34 @@ def start_as_current_span( def start_span( # pylint: disable=too-many-locals self, name: str, - parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, + context: Optional[context_api.Context] = None, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, attributes: types.Attributes = None, links: Sequence[trace_api.Link] = (), start_time: Optional[int] = None, set_status_on_exception: bool = True, ) -> trace_api.Span: - if parent is Tracer.CURRENT_SPAN: - parent = trace_api.get_current_span() - parent_context = parent - if isinstance(parent_context, trace_api.Span): - parent_context = parent.get_context() + parent_span_context = trace_api.get_current_span( + context + ).get_span_context() - if parent_context is not None and not isinstance( - parent_context, trace_api.SpanContext + if parent_span_context is not None and not isinstance( + parent_span_context, trace_api.SpanContext ): - raise TypeError("parent must be a Span, SpanContext or None.") + raise TypeError( + "parent_span_context must be a SpanContext or None." + ) - if parent_context is None or not parent_context.is_valid: - parent = parent_context = None + if parent_span_context is None or not parent_span_context.is_valid: + parent_span_context = None trace_id = self.source.ids_generator.generate_trace_id() trace_flags = None trace_state = None else: - trace_id = parent_context.trace_id - trace_flags = parent_context.trace_flags - trace_state = parent_context.trace_state + trace_id = parent_span_context.trace_id + trace_flags = parent_span_context.trace_flags + trace_state = parent_span_context.trace_state # The sampler decides whether to create a real or no-op span at the # time of span creation. No-op spans do not record events, and are not @@ -748,7 +748,7 @@ def start_span( # pylint: disable=too-many-locals # The sampler may also add attributes to the newly-created span, e.g. # to include information about the sampling result. sampling_result = self.source.sampler.should_sample( - parent_context, trace_id, name, attributes, links, + parent_span_context, trace_id, name, attributes, links, ) trace_flags = ( @@ -770,7 +770,7 @@ def start_span( # pylint: disable=too-many-locals span = _Span( name=name, context=context, - parent=parent_context, + parent=parent_span_context, sampler=self.source.sampler, resource=self.source.resource, attributes=sampling_result.attributes.copy(), diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py index 813b6e8560..8a6b8e2247 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py @@ -140,7 +140,7 @@ def inject( ) -> None: span = trace.get_current_span(context=context) - span_context = span.get_context() + span_context = span.get_span_context() if span_context == trace.INVALID_SPAN_CONTEXT: return diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index c40d3e691e..7a1e10752e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -113,7 +113,7 @@ class Sampler(abc.ABC): @abc.abstractmethod def should_sample( self, - parent_context: Optional["SpanContext"], + parent_span_context: Optional["SpanContext"], trace_id: int, name: str, attributes: Attributes = None, @@ -134,7 +134,7 @@ def __init__(self, decision: "Decision"): def should_sample( self, - parent_context: Optional["SpanContext"], + parent_span_context: Optional["SpanContext"], trace_id: int, name: str, attributes: Attributes = None, @@ -188,7 +188,7 @@ def bound(self) -> int: def should_sample( self, - parent_context: Optional["SpanContext"], + parent_span_context: Optional["SpanContext"], trace_id: int, name: str, attributes: Attributes = None, # TODO @@ -220,22 +220,22 @@ def __init__(self, delegate: Sampler): def should_sample( self, - parent_context: Optional["SpanContext"], + parent_span_context: Optional["SpanContext"], trace_id: int, name: str, attributes: Attributes = None, # TODO links: Sequence["Link"] = (), ) -> "SamplingResult": - if parent_context is not None: + if parent_span_context is not None: if ( - not parent_context.is_valid - or not parent_context.trace_flags.sampled + not parent_span_context.is_valid + or not parent_span_context.trace_flags.sampled ): return SamplingResult(Decision.DROP) return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes) return self._delegate.should_sample( - parent_context=parent_context, + parent_span_context=parent_span_context, trace_id=trace_id, name=name, attributes=attributes, diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index 4fa653205b..452915d86c 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -108,4 +108,4 @@ def test_with_asyncio(self): for span in span_list: if span is expected_parent: continue - self.assertEqual(span.parent, expected_parent.get_context()) + self.assertEqual(span.parent, expected_parent.get_span_context()) diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index b825ea1a75..a788a812c7 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -31,19 +31,19 @@ def get_as_list(dict_object, key): def get_child_parent_new_carrier(old_carrier): ctx = FORMAT.extract(get_as_list, old_carrier) - parent_context = trace_api.get_current_span(ctx).get_context() + parent_span_context = trace_api.get_current_span(ctx).get_span_context() - parent = trace._Span("parent", parent_context) + parent = trace._Span("parent", parent_span_context) child = trace._Span( "child", trace_api.SpanContext( - parent_context.trace_id, + parent_span_context.trace_id, trace_api.RandomIdsGenerator().generate_span_id(), is_remote=False, - trace_flags=parent_context.trace_flags, - trace_state=parent_context.trace_state, + trace_flags=parent_span_context.trace_flags, + trace_state=parent_span_context.trace_state, ), - parent=parent.get_context(), + parent=parent.get_span_context(), ) new_carrier = {} @@ -232,7 +232,7 @@ def test_invalid_single_header(self): """ carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} ctx = FORMAT.extract(get_as_list, carrier) - span_context = trace_api.get_current_span(ctx).get_context() + span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) @@ -244,7 +244,7 @@ def test_missing_trace_id(self): } ctx = FORMAT.extract(get_as_list, carrier) - span_context = trace_api.get_current_span(ctx).get_context() + span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) @patch( @@ -268,7 +268,7 @@ def test_invalid_trace_id( } ctx = FORMAT.extract(get_as_list, carrier) - span_context = trace_api.get_current_span(ctx).get_context() + span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, 1) self.assertEqual(span_context.span_id, 2) @@ -294,7 +294,7 @@ def test_invalid_span_id( } ctx = FORMAT.extract(get_as_list, carrier) - span_context = trace_api.get_current_span(ctx).get_context() + span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, 1) self.assertEqual(span_context.span_id, 2) @@ -307,7 +307,7 @@ def test_missing_span_id(self): } ctx = FORMAT.extract(get_as_list, carrier) - span_context = trace_api.get_current_span(ctx).get_context() + span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) @staticmethod diff --git a/opentelemetry-sdk/tests/trace/test_implementation.py b/opentelemetry-sdk/tests/trace/test_implementation.py index 7e6ede1ae0..961e68d986 100644 --- a/opentelemetry-sdk/tests/trace/test_implementation.py +++ b/opentelemetry-sdk/tests/trace/test_implementation.py @@ -29,11 +29,13 @@ class TestTracerImplementation(unittest.TestCase): def test_tracer(self): tracer = trace.TracerProvider().get_tracer(__name__) with tracer.start_span("test") as span: - self.assertNotEqual(span.get_context(), INVALID_SPAN_CONTEXT) + self.assertNotEqual(span.get_span_context(), INVALID_SPAN_CONTEXT) self.assertNotEqual(span, INVALID_SPAN) self.assertIs(span.is_recording(), True) with tracer.start_span("test2") as span2: - self.assertNotEqual(span2.get_context(), INVALID_SPAN_CONTEXT) + self.assertNotEqual( + span2.get_span_context(), INVALID_SPAN_CONTEXT + ) self.assertNotEqual(span2, INVALID_SPAN) self.assertIs(span2.is_recording(), True) @@ -43,5 +45,5 @@ def test_span(self): span = trace._Span() span = trace._Span("name", INVALID_SPAN_CONTEXT) - self.assertEqual(span.get_context(), INVALID_SPAN_CONTEXT) + self.assertEqual(span.get_span_context(), INVALID_SPAN_CONTEXT) self.assertIs(span.is_recording(), True) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 7edcfc79b8..6a0399c28c 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines import shutil import subprocess import unittest @@ -135,16 +136,19 @@ def test_default_sampler(self): # Check that the default tracer creates real spans via the default # sampler - root_span = tracer.start_span(name="root span", parent=None) + root_span = tracer.start_span(name="root span", context=None) + ctx = trace_api.set_span_in_context(root_span) self.assertIsInstance(root_span, trace.Span) - child_span = tracer.start_span(name="child span", parent=root_span) + child_span = tracer.start_span(name="child span", context=ctx) self.assertIsInstance(child_span, trace.Span) self.assertTrue(root_span.context.trace_flags.sampled) self.assertEqual( - root_span.get_context().trace_flags, trace_api.TraceFlags.SAMPLED + root_span.get_span_context().trace_flags, + trace_api.TraceFlags.SAMPLED, ) self.assertEqual( - child_span.get_context().trace_flags, trace_api.TraceFlags.SAMPLED + child_span.get_span_context().trace_flags, + trace_api.TraceFlags.SAMPLED, ) def test_sampler_no_sampling(self): @@ -153,15 +157,18 @@ def test_sampler_no_sampling(self): # Check that the default tracer creates no-op spans if the sampler # decides not to sampler - root_span = tracer.start_span(name="root span", parent=None) + root_span = tracer.start_span(name="root span", context=None) + ctx = trace_api.set_span_in_context(root_span) self.assertIsInstance(root_span, trace_api.DefaultSpan) - child_span = tracer.start_span(name="child span", parent=root_span) + child_span = tracer.start_span(name="child span", context=ctx) self.assertIsInstance(child_span, trace_api.DefaultSpan) self.assertEqual( - root_span.get_context().trace_flags, trace_api.TraceFlags.DEFAULT + root_span.get_span_context().trace_flags, + trace_api.TraceFlags.DEFAULT, ) self.assertEqual( - child_span.get_context().trace_flags, trace_api.TraceFlags.DEFAULT + child_span.get_span_context().trace_flags, + trace_api.TraceFlags.DEFAULT, ) @@ -174,9 +181,10 @@ def test_start_span_invalid_spancontext(self): eliminates redundant error handling logic in exporters. """ tracer = new_tracer() - new_span = tracer.start_span( - "root", parent=trace_api.INVALID_SPAN_CONTEXT + parent_context = trace_api.set_span_in_context( + trace_api.INVALID_SPAN_CONTEXT ) + new_span = tracer.start_span("root", context=parent_context) self.assertTrue(new_span.context.is_valid) self.assertIsNone(new_span.parent) @@ -250,7 +258,7 @@ def test_start_span_implicit(self): with tracer.start_span( "child", kind=trace_api.SpanKind.CLIENT ) as child: - self.assertIs(child.parent, root.get_context()) + self.assertIs(child.parent, root.get_span_context()) self.assertEqual(child.kind, trace_api.SpanKind.CLIENT) self.assertIsNotNone(child.start_time) @@ -258,8 +266,8 @@ def test_start_span_implicit(self): # The new child span should inherit the parent's context but # get a new span ID. - root_context = root.get_context() - child_context = child.get_context() + root_context = root.get_span_context() + child_context = child.get_span_context() self.assertEqual(root_context.trace_id, child_context.trace_id) self.assertNotEqual( root_context.span_id, child_context.span_id @@ -282,13 +290,18 @@ def test_start_span_implicit(self): def test_start_span_explicit(self): tracer = new_tracer() - other_parent = trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x00000000DEADBEF0, - is_remote=False, - trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), + other_parent = trace._Span( + "name", + trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), + ), ) + other_parent_context = trace_api.set_span_in_context(other_parent) + self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) root = tracer.start_span("root") @@ -299,27 +312,33 @@ def test_start_span_explicit(self): with tracer.use_span(root, True): self.assertIs(trace_api.get_current_span(), root) - with tracer.start_span("stepchild", other_parent) as child: + with tracer.start_span("stepchild", other_parent_context) as child: # The child's parent should be the one passed in, # not the current span. self.assertNotEqual(child.parent, root) - self.assertIs(child.parent, other_parent) + self.assertIs(child.parent, other_parent.get_span_context()) self.assertIsNotNone(child.start_time) self.assertIsNone(child.end_time) # The child should inherit its context from the explicit # parent, not the current span. - child_context = child.get_context() - self.assertEqual(other_parent.trace_id, child_context.trace_id) + child_context = child.get_span_context() + self.assertEqual( + other_parent.get_span_context().trace_id, + child_context.trace_id, + ) self.assertNotEqual( - other_parent.span_id, child_context.span_id + other_parent.get_span_context().span_id, + child_context.span_id, ) self.assertEqual( - other_parent.trace_state, child_context.trace_state + other_parent.get_span_context().trace_state, + child_context.trace_state, ) self.assertEqual( - other_parent.trace_flags, child_context.trace_flags + other_parent.get_span_context().trace_flags, + child_context.trace_flags, ) # Verify start_span() did not set the current span. @@ -339,7 +358,7 @@ def test_start_as_current_span_implicit(self): with tracer.start_as_current_span("child") as child: self.assertIs(trace_api.get_current_span(), child) - self.assertIs(child.parent, root.get_context()) + self.assertIs(child.parent, root.get_span_context()) # After exiting the child's scope the parent should become the # current span again. @@ -352,12 +371,16 @@ def test_start_as_current_span_implicit(self): def test_start_as_current_span_explicit(self): tracer = new_tracer() - other_parent = trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x00000000DEADBEF0, - is_remote=False, - trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), + other_parent = trace._Span( + "name", + trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), + ), ) + other_parent_ctx = trace_api.set_span_in_context(other_parent) self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) @@ -369,14 +392,14 @@ def test_start_as_current_span_explicit(self): self.assertIsNone(root.end_time) with tracer.start_as_current_span( - "stepchild", other_parent + "stepchild", other_parent_ctx ) as child: # The child should become the current span as usual, but its # parent should be the one passed in, not the # previously-current span. self.assertIs(trace_api.get_current_span(), child) self.assertNotEqual(child.parent, root) - self.assertIs(child.parent, other_parent) + self.assertIs(child.parent, other_parent.get_span_context()) # After exiting the child's scope the last span on the stack should # become current, not the child's parent. @@ -543,7 +566,7 @@ def test_sampling_attributes(self): "attr-in-both": "decision-attr", } tracer_provider = trace.TracerProvider( - sampling.StaticSampler(sampling.Decision.RECORD_AND_SAMPLE,) + sampling.StaticSampler(sampling.Decision.RECORD_AND_SAMPLE) ) self.tracer = tracer_provider.get_tracer(__name__) @@ -555,7 +578,8 @@ def test_sampling_attributes(self): self.assertEqual(root.attributes["sampler-attr"], "sample-val") self.assertEqual(root.attributes["attr-in-both"], "decision-attr") self.assertEqual( - root.get_context().trace_flags, trace_api.TraceFlags.SAMPLED + root.get_span_context().trace_flags, + trace_api.TraceFlags.SAMPLED, ) def test_events(self): diff --git a/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py b/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py index 5be0be9f0e..fc237fe12b 100644 --- a/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py +++ b/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py @@ -68,7 +68,7 @@ def validate_spans(self): self.assertEqual(root_span.name, "rootSpan") self.assertEqual(db_span.name, "mysql.opentelemetry-tests") self.assertIsNotNone(db_span.parent) - self.assertIs(db_span.parent, root_span.get_context()) + self.assertIs(db_span.parent, root_span.get_span_context()) self.assertIs(db_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(db_span.attributes["db.instance"], MYSQL_DB_NAME) self.assertEqual(db_span.attributes["net.peer.name"], MYSQL_HOST) diff --git a/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py b/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py index e7a0d39b51..d76cd702ee 100644 --- a/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py +++ b/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py @@ -76,7 +76,7 @@ def validate_spans(self): self.assertEqual(root_span.name, "rootSpan") self.assertEqual(child_span.name, "postgresql.opentelemetry-tests") self.assertIsNotNone(child_span.parent) - self.assertIs(child_span.parent, root_span.get_context()) + self.assertIs(child_span.parent, root_span.get_span_context()) self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual( child_span.attributes["db.instance"], POSTGRES_DB_NAME @@ -157,7 +157,7 @@ def validate_spans(self): self.assertEqual(root_span.name, "rootSpan") self.assertEqual(child_span.name, "postgresql.opentelemetry-tests") self.assertIsNotNone(child_span.parent) - self.assertIs(child_span.parent, root_span.get_context()) + self.assertIs(child_span.parent, root_span.get_span_context()) self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual( child_span.attributes["db.instance"], POSTGRES_DB_NAME diff --git a/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py b/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py index 2739164781..28db4c064f 100644 --- a/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py +++ b/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py @@ -68,7 +68,7 @@ def validate_spans(self): self.assertEqual(root_span.name, "rootSpan") self.assertEqual(child_span.name, "postgresql.opentelemetry-tests") self.assertIsNotNone(child_span.parent) - self.assertIs(child_span.parent, root_span.get_context()) + self.assertIs(child_span.parent, root_span.get_span_context()) self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual( child_span.attributes["db.instance"], POSTGRES_DB_NAME diff --git a/tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py b/tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py index acb60178d0..577477a2ab 100644 --- a/tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py +++ b/tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py @@ -51,7 +51,7 @@ def validate_spans(self): self.assertIsNot(root_span, None) self.assertIsNot(pymongo_span, None) self.assertIsNotNone(pymongo_span.parent) - self.assertIs(pymongo_span.parent, root_span.get_context()) + self.assertIs(pymongo_span.parent, root_span.get_span_context()) self.assertIs(pymongo_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual( pymongo_span.attributes["db.instance"], MONGODB_DB_NAME diff --git a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py index c5c4d4f449..7c09025551 100644 --- a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py +++ b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py @@ -65,7 +65,7 @@ def validate_spans(self): self.assertEqual(root_span.name, "rootSpan") self.assertEqual(db_span.name, "mysql.opentelemetry-tests") self.assertIsNotNone(db_span.parent) - self.assertIs(db_span.parent, root_span.get_context()) + self.assertIs(db_span.parent, root_span.get_span_context()) self.assertIs(db_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(db_span.attributes["db.instance"], MYSQL_DB_NAME) self.assertEqual(db_span.attributes["net.peer.name"], MYSQL_HOST) diff --git a/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py b/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py index 7e6ea2e044..64984e9c4f 100644 --- a/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py +++ b/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py @@ -109,7 +109,7 @@ def test_parent(self): # confirm the parenting self.assertIsNone(parent_span.parent) - self.assertIs(child_span.parent, parent_span.get_context()) + self.assertIs(child_span.parent, parent_span.get_span_context()) self.assertEqual(parent_span.name, "redis_get") self.assertEqual(parent_span.instrumentation_info.name, "redis_svc") diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py index a438f58eb9..b2d8c0abc5 100644 --- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py +++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py @@ -175,7 +175,7 @@ def test_parent(self): # confirm the parenting self.assertIsNone(parent_span.parent) - self.assertIs(child_span.parent, parent_span.get_context()) + self.assertIs(child_span.parent, parent_span.get_span_context()) self.assertEqual(parent_span.name, "sqlalch_op") self.assertEqual(parent_span.instrumentation_info.name, "sqlalch_svc") diff --git a/tests/util/src/opentelemetry/test/mock_textmap.py b/tests/util/src/opentelemetry/test/mock_textmap.py index 92c0f21f0e..bf46ec32fa 100644 --- a/tests/util/src/opentelemetry/test/mock_textmap.py +++ b/tests/util/src/opentelemetry/test/mock_textmap.py @@ -84,8 +84,8 @@ def inject( ) -> None: span = trace.get_current_span(context) set_in_carrier( - carrier, self.TRACE_ID_KEY, str(span.get_context().trace_id) + carrier, self.TRACE_ID_KEY, str(span.get_span_context().trace_id) ) set_in_carrier( - carrier, self.SPAN_ID_KEY, str(span.get_context().span_id) + carrier, self.SPAN_ID_KEY, str(span.get_span_context().span_id) ) From affe911ee38f3926a06e6fe61820a2358e96d185 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 8 Oct 2020 15:25:20 -0400 Subject: [PATCH 0603/1517] Use is_recording flag in aiopg, asyncpg, dbapi, psycopg2, pymemcache, pymongo, redis, sqlalchemy instrumentations (#1212) --- .../instrumentation/aiopg/__init__.py | 2 +- .../aiopg/aiopg_integration.py | 11 ++-- .../instrumentation/aiopg/wrappers.py | 52 +++++++++---------- .../tests/test_aiopg_integration.py | 34 ++++++++++++ .../instrumentation/asyncpg/__init__.py | 27 +++++----- .../tests/test_boto_instrumentation.py | 2 +- .../tests/test_botocore_instrumentation.py | 2 +- .../instrumentation/dbapi/__init__.py | 10 +++- .../tests/test_dbapi_integration.py | 32 ++++++++++++ .../tests/test_middleware.py | 2 +- .../tests/test_psycopg2_integration.py | 23 ++++++++ .../instrumentation/pymemcache/__init__.py | 5 +- .../tests/test_pymemcache.py | 18 +++++++ .../instrumentation/pymongo/__init__.py | 52 +++++++++++-------- .../tests/test_pymongo.py | 16 ++++++ .../tests/test_programmatic.py | 2 +- .../instrumentation/redis/__init__.py | 24 +++++---- .../tests/test_redis.py | 19 +++++++ .../instrumentation/sqlalchemy/engine.py | 49 ++++++++++------- .../tests/test_sqlalchemy.py | 23 ++++++++ .../tests/test_instrumentation.py | 2 +- 21 files changed, 304 insertions(+), 103 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py index d2cc90902e..176fc82b40 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py @@ -63,7 +63,7 @@ class AiopgInstrumentor(BaseInstrumentor): def _instrument(self, **kwargs): """Integrate with PostgreSQL aiopg library. - aiopg: https://github.com/aio-libs/aiopg + aiopg: https://github.com/aio-libs/aiopg """ tracer_provider = kwargs.get("tracer_provider") diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py index b6992120f3..1455f23e62 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py @@ -37,8 +37,7 @@ async def wrapped_connection( args: typing.Tuple[typing.Any, typing.Any], kwargs: typing.Dict[typing.Any, typing.Any], ): - """Add object proxy to connection object. - """ + """Add object proxy to connection object.""" connection = await connect_method(*args, **kwargs) # pylint: disable=protected-access self.get_connection_attributes(connection._conn) @@ -109,10 +108,14 @@ async def traced_execution( self._populate_span(span, *args) try: result = await query_method(*args, **kwargs) - span.set_status(Status(StatusCanonicalCode.OK)) + if span.is_recording(): + span.set_status(Status(StatusCanonicalCode.OK)) return result except Exception as ex: # pylint: disable=broad-except - span.set_status(Status(StatusCanonicalCode.UNKNOWN, str(ex))) + if span.is_recording(): + span.set_status( + Status(StatusCanonicalCode.UNKNOWN, str(ex)) + ) raise ex diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py index 473c5039c3..8a3b6023bd 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py @@ -56,16 +56,16 @@ def trace_integration( tracer_provider: typing.Optional[TracerProvider] = None, ): """Integrate with aiopg library. - based on dbapi integration, where replaced sync wrap methods to async - - Args: - database_component: Database driver name or - database name "postgreSQL". - database_type: The Database type. For any SQL database, "sql". - connection_attributes: Attribute names for database, port, host and - user in Connection object. - tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to - use. If ommited the current configured one is used. + based on dbapi integration, where replaced sync wrap methods to async + + Args: + database_component: Database driver name or + database name "postgreSQL". + database_type: The Database type. For any SQL database, "sql". + connection_attributes: Attribute names for database, port, host and + user in Connection object. + tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to + use. If ommited the current configured one is used. """ wrap_connect( @@ -87,18 +87,18 @@ def wrap_connect( tracer_provider: typing.Optional[TracerProvider] = None, ): """Integrate with aiopg library. - https://github.com/aio-libs/aiopg - - Args: - name: Name of opentelemetry extension for aiopg. - database_component: Database driver name - or database name "postgreSQL". - database_type: The Database type. For any SQL database, "sql". - connection_attributes: Attribute names for database, port, host and - user in Connection object. - version: Version of opentelemetry extension for aiopg. - tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to - use. If ommited the current configured one is used. + https://github.com/aio-libs/aiopg + + Args: + name: Name of opentelemetry extension for aiopg. + database_component: Database driver name + or database name "postgreSQL". + database_type: The Database type. For any SQL database, "sql". + connection_attributes: Attribute names for database, port, host and + user in Connection object. + version: Version of opentelemetry extension for aiopg. + tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to + use. If ommited the current configured one is used. """ # pylint: disable=unused-argument @@ -125,8 +125,8 @@ async def wrap_connect_( def unwrap_connect(): - """"Disable integration with aiopg library. - https://github.com/aio-libs/aiopg + """Disable integration with aiopg library. + https://github.com/aio-libs/aiopg """ unwrap(aiopg, "connect") @@ -217,7 +217,7 @@ async def wrap_create_pool_( def unwrap_create_pool(): - """"Disable integration with aiopg library. - https://github.com/aio-libs/aiopg + """Disable integration with aiopg library. + https://github.com/aio-libs/aiopg """ unwrap(aiopg, "create_pool") diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py index f7daf7ccc0..135f9ee9a7 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py @@ -230,6 +230,40 @@ def test_span_succeeded(self): trace_api.status.StatusCanonicalCode.OK, ) + def test_span_not_recording(self): + connection_props = { + "database": "testdatabase", + "server_host": "testhost", + "server_port": 123, + "user": "testuser", + } + connection_attributes = { + "database": "database", + "port": "server_port", + "host": "server_host", + "user": "user", + } + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = True + db_integration = AiopgIntegration( + mock_tracer, "testcomponent", "testtype", connection_attributes + ) + mock_connection = async_call( + db_integration.wrapped_connection( + mock_connect, {}, connection_props + ) + ) + cursor = async_call(mock_connection.cursor()) + async_call(cursor.execute("Test query", ("param1Value", False))) + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_span_failed(self): db_integration = AiopgIntegration(self.tracer, "testcomponent") mock_connection = async_call( diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py index 189809809d..6af816b39d 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py @@ -112,9 +112,6 @@ def _uninstrument(self, **__): unwrap(asyncpg.Connection, method) async def _do_execute(self, func, instance, args, kwargs): - span_attributes = _hydrate_span_from_args( - instance, args[0], args[1:] if self.capture_parameters else None, - ) tracer = getattr(asyncpg, _APPLIED) exception = None @@ -122,9 +119,14 @@ async def _do_execute(self, func, instance, args, kwargs): with tracer.start_as_current_span( "postgresql", kind=SpanKind.CLIENT ) as span: - - for attribute, value in span_attributes.items(): - span.set_attribute(attribute, value) + if span.is_recording(): + span_attributes = _hydrate_span_from_args( + instance, + args[0], + args[1:] if self.capture_parameters else None, + ) + for attribute, value in span_attributes.items(): + span.set_attribute(attribute, value) try: result = await func(*args, **kwargs) @@ -132,11 +134,12 @@ async def _do_execute(self, func, instance, args, kwargs): exception = exc raise finally: - if exception is not None: - span.set_status( - Status(_exception_to_canonical_code(exception)) - ) - else: - span.set_status(Status(StatusCanonicalCode.OK)) + if span.is_recording(): + if exception is not None: + span.set_status( + Status(_exception_to_canonical_code(exception)) + ) + else: + span.set_status(Status(StatusCanonicalCode.OK)) return result diff --git a/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py b/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py index 1a8cc2b387..cb45514c79 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py @@ -88,7 +88,7 @@ def test_not_recording(self): mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = mock_span + mock_tracer.use_span.return_value.__exit__ = True with patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer ec2 = boto.ec2.connect_to_region("us-west-2") diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py index 9bf691f657..fba0182eec 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py @@ -84,7 +84,7 @@ def test_not_recording(self): mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = mock_span + mock_tracer.use_span.return_value.__exit__ = True with patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer ec2 = self.session.create_client("ec2", region_name="us-west-2") diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py index 551f71555a..0dcdd5ba60 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -311,6 +311,8 @@ def __init__(self, db_api_integration: DatabaseApiIntegration): def _populate_span( self, span: trace_api.Span, *args: typing.Tuple[typing.Any, typing.Any] ): + if not span.is_recording(): + return statement = args[0] if args else "" span.set_attribute( "component", self._db_api_integration.database_component @@ -341,10 +343,14 @@ def traced_execution( self._populate_span(span, *args) try: result = query_method(*args, **kwargs) - span.set_status(Status(StatusCanonicalCode.OK)) + if span.is_recording(): + span.set_status(Status(StatusCanonicalCode.OK)) return result except Exception as ex: # pylint: disable=broad-except - span.set_status(Status(StatusCanonicalCode.UNKNOWN, str(ex))) + if span.is_recording(): + span.set_status( + Status(StatusCanonicalCode.UNKNOWN, str(ex)) + ) raise ex diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py index 2991570333..e342e15aa3 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py @@ -69,6 +69,38 @@ def test_span_succeeded(self): trace_api.status.StatusCanonicalCode.OK, ) + def test_span_not_recording(self): + connection_props = { + "database": "testdatabase", + "server_host": "testhost", + "server_port": 123, + "user": "testuser", + } + connection_attributes = { + "database": "database", + "port": "server_port", + "host": "server_host", + "user": "user", + } + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = True + db_integration = dbapi.DatabaseApiIntegration( + mock_tracer, "testcomponent", "testtype", connection_attributes + ) + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, connection_props + ) + cursor = mock_connection.cursor() + cursor.execute("Test query", ("param1Value", False)) + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_span_failed(self): db_integration = dbapi.DatabaseApiIntegration( self.tracer, "testcomponent" diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 7e9ab72b52..4db6c485de 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -95,7 +95,7 @@ def test_not_recording(self): mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = mock_span + mock_tracer.use_span.return_value.__exit__ = True with patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer Client().get("/traced/") diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py index 629b2f62b6..cb127c7a5e 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py @@ -60,6 +60,29 @@ def test_instrumentor(self, mock_connect): spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) + @mock.patch("psycopg2.connect") + # pylint: disable=unused-argument + def test_not_recording(self, mock_connect): + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = True + Psycopg2Instrumentor().instrument() + with mock.patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + cnx = psycopg2.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + + Psycopg2Instrumentor().uninstrument() + @mock.patch("psycopg2.connect") # pylint: disable=unused-argument def test_custom_tracer_provider(self, mock_connect): diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py index 332e92ccdd..46b188a3df 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py @@ -90,13 +90,14 @@ def _set_connection_attributes(span, instance): + if not span.is_recording(): + return for key, value in _get_address_attributes(instance).items(): span.set_attribute(key, value) def _with_tracer_wrapper(func): - """Helper for providing tracer for wrapper functions. - """ + """Helper for providing tracer for wrapper functions.""" def _with_tracer(tracer, cmd): def wrapper(wrapped, instance, args, kwargs): diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py index 794da9972b..b38bedf3fd 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from unittest import mock import pymemcache from pymemcache.exceptions import ( @@ -84,6 +85,23 @@ def test_set_success(self): self.check_spans(spans, 1, ["set key"]) + def test_set_not_recording(self): + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = True + with mock.patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + client = self.make_client([b"STORED\r\n"]) + result = client.set(b"key", b"value", noreply=False) + self.assertTrue(result) + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_get_many_none_found(self): client = self.make_client([b"END\r\n"]) result = client.get_many([b"key1", b"key2"]) diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py index 4cfff745d1..bb20fd5442 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py @@ -71,28 +71,32 @@ def started(self, event: monitoring.CommandStartedEvent): try: span = self._tracer.start_span(name, kind=SpanKind.CLIENT) - span.set_attribute("component", DATABASE_TYPE) - span.set_attribute("db.type", DATABASE_TYPE) - span.set_attribute("db.instance", event.database_name) - span.set_attribute("db.statement", statement) - if event.connection_id is not None: - span.set_attribute("net.peer.name", event.connection_id[0]) - span.set_attribute("net.peer.port", event.connection_id[1]) - - # pymongo specific, not specified by spec - span.set_attribute("db.mongo.operation_id", event.operation_id) - span.set_attribute("db.mongo.request_id", event.request_id) - - for attr in COMMAND_ATTRIBUTES: - _attr = event.command.get(attr) - if _attr is not None: - span.set_attribute("db.mongo." + attr, str(_attr)) + if span.is_recording(): + span.set_attribute("component", DATABASE_TYPE) + span.set_attribute("db.type", DATABASE_TYPE) + span.set_attribute("db.instance", event.database_name) + span.set_attribute("db.statement", statement) + if event.connection_id is not None: + span.set_attribute("net.peer.name", event.connection_id[0]) + span.set_attribute("net.peer.port", event.connection_id[1]) + + # pymongo specific, not specified by spec + span.set_attribute("db.mongo.operation_id", event.operation_id) + span.set_attribute("db.mongo.request_id", event.request_id) + + for attr in COMMAND_ATTRIBUTES: + _attr = event.command.get(attr) + if _attr is not None: + span.set_attribute("db.mongo." + attr, str(_attr)) # Add Span to dictionary self._span_dict[_get_span_dict_key(event)] = span except Exception as ex: # noqa pylint: disable=broad-except if span is not None: - span.set_status(Status(StatusCanonicalCode.INTERNAL, str(ex))) + if span.is_recording(): + span.set_status( + Status(StatusCanonicalCode.INTERNAL, str(ex)) + ) span.end() self._pop_span(event) @@ -103,8 +107,11 @@ def succeeded(self, event: monitoring.CommandSucceededEvent): span = self._pop_span(event) if span is None: return - span.set_attribute("db.mongo.duration_micros", event.duration_micros) - span.set_status(Status(StatusCanonicalCode.OK, event.reply)) + if span.is_recording(): + span.set_attribute( + "db.mongo.duration_micros", event.duration_micros + ) + span.set_status(Status(StatusCanonicalCode.OK, event.reply)) span.end() def failed(self, event: monitoring.CommandFailedEvent): @@ -114,8 +121,11 @@ def failed(self, event: monitoring.CommandFailedEvent): span = self._pop_span(event) if span is None: return - span.set_attribute("db.mongo.duration_micros", event.duration_micros) - span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure)) + if span.is_recording(): + span.set_attribute( + "db.mongo.duration_micros", event.duration_micros + ) + span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure)) span.end() def _pop_span(self, event): diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py b/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py index a84841b28b..d5f67cafe8 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py @@ -91,6 +91,22 @@ def test_succeeded(self): self.assertEqual(span.status.description, "reply") self.assertIsNotNone(span.end_time) + def test_not_recording(self): + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = True + mock_event = MockEvent({}) + command_tracer = CommandTracer(mock_tracer) + command_tracer.started(event=mock_event) + command_tracer.succeeded(event=mock_event) + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_failed(self): mock_event = MockEvent({}) command_tracer = CommandTracer(self.tracer) diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py index 38ba71cb55..77427b0db7 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py @@ -93,7 +93,7 @@ def test_not_recording(self): mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = mock_span + mock_tracer.use_span.return_value.__exit__ = True with patch("opentelemetry.trace.get_tracer"): self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py index e2ab78b535..e1c5db1e94 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py @@ -63,6 +63,8 @@ def _set_connection_attributes(span, conn): + if not span.is_recording(): + return for key, value in _extract_conn_attributes( conn.connection_pool.connection_kwargs ).items(): @@ -75,10 +77,11 @@ def _traced_execute_command(func, instance, args, kwargs): with tracer.start_as_current_span( _CMD, kind=trace.SpanKind.CLIENT ) as span: - span.set_attribute("service", tracer.instrumentation_info.name) - span.set_attribute(_RAWCMD, query) - _set_connection_attributes(span, instance) - span.set_attribute("redis.args_length", len(args)) + if span.is_recording(): + span.set_attribute("service", tracer.instrumentation_info.name) + span.set_attribute(_RAWCMD, query) + _set_connection_attributes(span, instance) + span.set_attribute("redis.args_length", len(args)) return func(*args, **kwargs) @@ -91,12 +94,13 @@ def _traced_execute_pipeline(func, instance, args, kwargs): with tracer.start_as_current_span( _CMD, kind=trace.SpanKind.CLIENT ) as span: - span.set_attribute("service", tracer.instrumentation_info.name) - span.set_attribute(_RAWCMD, resource) - _set_connection_attributes(span, instance) - span.set_attribute( - "redis.pipeline_length", len(instance.command_stack) - ) + if span.is_recording(): + span.set_attribute("service", tracer.instrumentation_info.name) + span.set_attribute(_RAWCMD, resource) + _set_connection_attributes(span, instance) + span.set_attribute( + "redis.pipeline_length", len(instance.command_stack) + ) return func(*args, **kwargs) diff --git a/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py index c306dca363..3e07ac725e 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py +++ b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py @@ -34,6 +34,25 @@ def test_span_properties(self): self.assertEqual(span.name, "redis.command") self.assertEqual(span.kind, SpanKind.CLIENT) + def test_not_recording(self): + redis_client = redis.Redis() + RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) + + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = True + with mock.patch("opentelemetry.trace.get_tracer") as tracer: + with mock.patch.object(redis_client, "connection"): + tracer.return_value = mock_tracer + redis_client.get("key") + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_instrument_uninstrument(self): redis_client = redis.Redis() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py index df80c4841c..83a5b82b23 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py @@ -79,15 +79,16 @@ def __init__(self, tracer, service, engine): def _before_cur_exec(self, conn, cursor, statement, *args): self.current_span = self.tracer.start_span(self.name) with self.tracer.use_span(self.current_span, end_on_exit=False): - self.current_span.set_attribute("service", self.vendor) - self.current_span.set_attribute(_STMT, statement) + if self.current_span.is_recording(): + self.current_span.set_attribute("service", self.vendor) + self.current_span.set_attribute(_STMT, statement) - if not _set_attributes_from_url( - self.current_span, conn.engine.url - ): - _set_attributes_from_cursor( - self.current_span, self.vendor, cursor - ) + if not _set_attributes_from_url( + self.current_span, conn.engine.url + ): + _set_attributes_from_cursor( + self.current_span, self.vendor, cursor + ) # pylint: disable=unused-argument def _after_cur_exec(self, conn, cursor, statement, *args): @@ -95,7 +96,11 @@ def _after_cur_exec(self, conn, cursor, statement, *args): return try: - if cursor and cursor.rowcount >= 0: + if ( + cursor + and cursor.rowcount >= 0 + and self.current_span.is_recording() + ): self.current_span.set_attribute(_ROWS, cursor.rowcount) finally: self.current_span.end() @@ -105,30 +110,34 @@ def _handle_error(self, context): return try: - self.current_span.set_status( - Status( - StatusCanonicalCode.UNKNOWN, - str(context.original_exception), + if self.current_span.is_recording(): + self.current_span.set_status( + Status( + StatusCanonicalCode.UNKNOWN, + str(context.original_exception), + ) ) - ) finally: self.current_span.end() def _set_attributes_from_url(span: trace.Span, url): """Set connection tags from the url. return true if successful.""" - if url.host: - span.set_attribute(_HOST, url.host) - if url.port: - span.set_attribute(_PORT, url.port) - if url.database: - span.set_attribute(_DB, url.database) + if span.is_recording(): + if url.host: + span.set_attribute(_HOST, url.host) + if url.port: + span.set_attribute(_PORT, url.port) + if url.database: + span.set_attribute(_DB, url.database) return bool(url.host) def _set_attributes_from_cursor(span: trace.Span, vendor, cursor): """Attempt to set db connection attributes by introspecting the cursor.""" + if not span.is_recording(): + return if vendor == "postgres": # pylint: disable=import-outside-toplevel from psycopg2.extensions import parse_dsn diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py index 06593da94f..3b2e3548a5 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from unittest import mock from sqlalchemy import create_engine @@ -37,6 +38,28 @@ def test_trace_integration(self): self.assertEqual(len(spans), 1) self.assertEqual(spans[0].name, "sqlite.query") + def test_not_recording(self): + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = True + with mock.patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + engine = create_engine("sqlite:///:memory:") + SQLAlchemyInstrumentor().instrument( + engine=engine, + tracer_provider=self.tracer_provider, + service="my-database", + ) + cnx = engine.connect() + cnx.execute("SELECT 1 + 1;").fetchall() + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_create_engine_wrapper(self): SQLAlchemyInstrumentor().instrument() from sqlalchemy import create_engine # pylint: disable-all diff --git a/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py b/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py index eb2852f112..5b429766ec 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py @@ -138,7 +138,7 @@ def test_not_recording(self): mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = mock_span + mock_tracer.use_span.return_value.__exit__ = True with patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer self.fetch("/") From c7cc4d997b4fafb67d4932d02bea4b96dbf94159 Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Fri, 9 Oct 2020 16:18:22 +0200 Subject: [PATCH 0604/1517] Add instrumentor and auto instrumentation support for aiohttp (#1075) --- .../CHANGELOG.md | 2 + .../setup.cfg | 7 +- .../aiohttp_client/__init__.py | 173 ++++++++++-- .../tests/test_aiohttp_client_integration.py | 266 +++++++++++++++--- 4 files changed, 378 insertions(+), 70 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md index 286d4ca642..8b1d3ee2c1 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md @@ -8,6 +8,8 @@ Released 2020-09-17 - Updating span name to match semantic conventions ([#972](https://github.com/open-telemetry/opentelemetry-python/pull/972)) +- Add instrumentor and auto instrumentation support for aiohttp + ([#1075](https://github.com/open-telemetry/opentelemetry-python/pull/1075)) ## Version 0.12b0 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index 196b33087d..eae097a62a 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -39,12 +39,17 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.12.dev0 + opentelemetry-api == 0.14.dev0 opentelemetry-instrumentation == 0.14.dev0 aiohttp ~= 3.0 + wrapt >= 1.0.0, < 2.0.0 [options.packages.find] where = src [options.extras_require] test = + +[options.entry_points] +opentelemetry_instrumentor = + aiohttp-client = opentelemetry.instrumentation.aiohttp_client:AioHttpClientInstrumentor \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py index 5c48bbd58a..6606c48331 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py @@ -18,44 +18,73 @@ Usage ----- +Explicitly instrumenting a single client session: - .. code:: python +.. code:: python - import aiohttp - from opentelemetry.instrumentation.aiohttp_client import ( - create_trace_config, - url_path_span_name - ) - import yarl + import aiohttp + from opentelemetry.instrumentation.aiohttp_client import ( + create_trace_config, + url_path_span_name + ) + import yarl - def strip_query_params(url: yarl.URL) -> str: - return str(url.with_query(None)) + def strip_query_params(url: yarl.URL) -> str: + return str(url.with_query(None)) - async with aiohttp.ClientSession(trace_configs=[create_trace_config( - # Remove all query params from the URL attribute on the span. - url_filter=strip_query_params, - # Use the URL's path as the span name. - span_name=url_path_span_name - )]) as session: - async with session.get(url) as response: - await response.text() + async with aiohttp.ClientSession(trace_configs=[create_trace_config( + # Remove all query params from the URL attribute on the span. + url_filter=strip_query_params, + # Use the URL's path as the span name. + span_name=url_path_span_name + )]) as session: + async with session.get(url) as response: + await response.text() + +Instrumenting all client sessions: + +.. code:: python + + import aiohttp + from opentelemetry.instrumentation.aiohttp_client import ( + AioHttpClientInstrumentor + ) + # Enable instrumentation + AioHttpClientInstrumentor().instrument() + + # Create a session and make an HTTP get request + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + await response.text() + +API +--- """ -import contextlib import socket import types import typing import aiohttp +import wrapt from opentelemetry import context as context_api from opentelemetry import propagators, trace from opentelemetry.instrumentation.aiohttp_client.version import __version__ -from opentelemetry.instrumentation.utils import http_status_to_canonical_code -from opentelemetry.trace import SpanKind +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import ( + http_status_to_canonical_code, + unwrap, +) +from opentelemetry.trace import SpanKind, TracerProvider, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode +_UrlFilterT = typing.Optional[typing.Callable[[str], str]] +_SpanNameT = typing.Optional[ + typing.Union[typing.Callable[[aiohttp.TraceRequestStartParams], str], str] +] + def url_path_span_name(params: aiohttp.TraceRequestStartParams) -> str: """Extract a span name from the request URL path. @@ -73,12 +102,9 @@ def url_path_span_name(params: aiohttp.TraceRequestStartParams) -> str: def create_trace_config( - url_filter: typing.Optional[typing.Callable[[str], str]] = None, - span_name: typing.Optional[ - typing.Union[ - typing.Callable[[aiohttp.TraceRequestStartParams], str], str - ] - ] = None, + url_filter: _UrlFilterT = None, + span_name: _SpanNameT = None, + tracer_provider: TracerProvider = None, ) -> aiohttp.TraceConfig: """Create an aiohttp-compatible trace configuration. @@ -104,6 +130,7 @@ def create_trace_config( such as API keys or user personal information. :param str span_name: Override the default span name. + :param tracer_provider: optional TracerProvider from which to get a Tracer :return: An object suitable for use with :py:class:`aiohttp.ClientSession`. :rtype: :py:class:`aiohttp.TraceConfig` @@ -113,7 +140,7 @@ def create_trace_config( # Explicitly specify the type for the `span_name` param and rtype to work # around this issue. - tracer = trace.get_tracer_provider().get_tracer(__name__, __version__) + tracer = get_tracer(__name__, __version__, tracer_provider) def _end_trace(trace_config_ctx: types.SimpleNamespace): context_api.detach(trace_config_ctx.token) @@ -124,6 +151,10 @@ async def on_request_start( trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestStartParams, ): + if context_api.get_value("suppress_instrumentation"): + trace_config_ctx.span = None + return + http_method = params.method.upper() if trace_config_ctx.span_name is None: request_span_name = "HTTP {}".format(http_method) @@ -158,6 +189,9 @@ async def on_request_end( trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestEndParams, ): + if trace_config_ctx.span is None: + return + if trace_config_ctx.span.is_recording(): trace_config_ctx.span.set_status( Status( @@ -177,6 +211,9 @@ async def on_request_exception( trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestExceptionParams, ): + if trace_config_ctx.span is None: + return + if trace_config_ctx.span.is_recording(): if isinstance( params.exception, @@ -193,6 +230,7 @@ async def on_request_exception( status = StatusCanonicalCode.UNAVAILABLE trace_config_ctx.span.set_status(Status(status)) + trace_config_ctx.span.record_exception(params.exception) _end_trace(trace_config_ctx) def _trace_config_ctx_factory(**kwargs): @@ -210,3 +248,84 @@ def _trace_config_ctx_factory(**kwargs): trace_config.on_request_exception.append(on_request_exception) return trace_config + + +def _instrument( + tracer_provider: TracerProvider = None, + url_filter: _UrlFilterT = None, + span_name: _SpanNameT = None, +): + """Enables tracing of all ClientSessions + + When a ClientSession gets created a TraceConfig is automatically added to + the session's trace_configs. + """ + # pylint:disable=unused-argument + def instrumented_init(wrapped, instance, args, kwargs): + if context_api.get_value("suppress_instrumentation"): + return wrapped(*args, **kwargs) + + trace_configs = list(kwargs.get("trace_configs") or ()) + + trace_config = create_trace_config( + url_filter=url_filter, + span_name=span_name, + tracer_provider=tracer_provider, + ) + trace_config.opentelemetry_aiohttp_instrumented = True + trace_configs.append(trace_config) + + kwargs["trace_configs"] = trace_configs + return wrapped(*args, **kwargs) + + wrapt.wrap_function_wrapper( + aiohttp.ClientSession, "__init__", instrumented_init + ) + + +def _uninstrument(): + """Disables instrumenting for all newly created ClientSessions""" + unwrap(aiohttp.ClientSession, "__init__") + + +def _uninstrument_session(client_session: aiohttp.ClientSession): + """Disables instrumentation for the given ClientSession""" + # pylint: disable=protected-access + trace_configs = client_session._trace_configs + client_session._trace_configs = [ + trace_config + for trace_config in trace_configs + if not hasattr(trace_config, "opentelemetry_aiohttp_instrumented") + ] + + +class AioHttpClientInstrumentor(BaseInstrumentor): + """An instrumentor for aiohttp client sessions + + See `BaseInstrumentor` + """ + + def _instrument(self, **kwargs): + """Instruments aiohttp ClientSession + + Args: + **kwargs: Optional arguments + ``tracer_provider``: a TracerProvider, defaults to global + ``url_filter``: A callback to process the requested URL prior to adding + it as a span attribute. This can be useful to remove sensitive data + such as API keys or user personal information. + ``span_name``: Override the default span name. + """ + _instrument( + tracer_provider=kwargs.get("tracer_provider"), + url_filter=kwargs.get("url_filter"), + span_name=kwargs.get("span_name"), + ) + + def _uninstrument(self, **kwargs): + _uninstrument() + + @staticmethod + def uninstrument_session(client_session: aiohttp.ClientSession): + """Disables instrumentation for the given session""" + _uninstrument_session(client_session) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py index 90af17f9e0..fb5b6aac6a 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -15,6 +15,7 @@ import asyncio import contextlib import typing +import unittest import urllib.parse from http import HTTPStatus from unittest import mock @@ -22,15 +23,39 @@ import aiohttp import aiohttp.test_utils import yarl +from pkg_resources import iter_entry_points -import opentelemetry.instrumentation.aiohttp_client +from opentelemetry import context +from opentelemetry.instrumentation import aiohttp_client +from opentelemetry.instrumentation.aiohttp_client import ( + AioHttpClientInstrumentor, +) from opentelemetry.test.test_base import TestBase from opentelemetry.trace.status import StatusCanonicalCode -class TestAioHttpIntegration(TestBase): - maxDiff = None +def run_with_test_server( + runnable: typing.Callable, url: str, handler: typing.Callable +) -> typing.Tuple[str, int]: + async def do_request(): + app = aiohttp.web.Application() + parsed_url = urllib.parse.urlparse(url) + app.add_routes([aiohttp.web.get(parsed_url.path, handler)]) + app.add_routes([aiohttp.web.post(parsed_url.path, handler)]) + app.add_routes([aiohttp.web.patch(parsed_url.path, handler)]) + + with contextlib.suppress(aiohttp.ClientError): + async with aiohttp.test_utils.TestServer(app) as server: + netloc = (server.host, server.port) + await server.start_server() + await runnable(server) + return netloc + + loop = asyncio.get_event_loop() + return loop.run_until_complete(do_request()) + +class TestAioHttpIntegration(TestBase): def assert_spans(self, spans): self.assertEqual( [ @@ -54,9 +79,7 @@ def test_url_path_span_name(self): ): with self.subTest(url=url): params = aiohttp.TraceRequestStartParams("METHOD", url, {}) - actual = opentelemetry.instrumentation.aiohttp_client.url_path_span_name( - params - ) + actual = aiohttp_client.url_path_span_name(params) self.assertEqual(actual, expected) self.assertIsInstance(actual, str) @@ -71,33 +94,20 @@ def _http_request( ) -> typing.Tuple[str, int]: """Helper to start an aiohttp test server and send an actual HTTP request to it.""" - async def do_request(): - async def default_handler(request): - assert "traceparent" in request.headers - return aiohttp.web.Response(status=int(status_code)) - - handler = request_handler or default_handler - - app = aiohttp.web.Application() - parsed_url = urllib.parse.urlparse(url) - app.add_routes([aiohttp.web.get(parsed_url.path, handler)]) - app.add_routes([aiohttp.web.post(parsed_url.path, handler)]) - app.add_routes([aiohttp.web.patch(parsed_url.path, handler)]) - - with contextlib.suppress(aiohttp.ClientError): - async with aiohttp.test_utils.TestServer(app) as server: - netloc = (server.host, server.port) - async with aiohttp.test_utils.TestClient( - server, trace_configs=[trace_config] - ) as client: - await client.start_server() - await client.request( - method, url, trace_request_ctx={}, **kwargs - ) - return netloc + async def default_handler(request): + assert "traceparent" in request.headers + return aiohttp.web.Response(status=int(status_code)) + + async def client_request(server: aiohttp.test_utils.TestServer): + async with aiohttp.test_utils.TestClient( + server, trace_configs=[trace_config] + ) as client: + await client.request( + method, url, trace_request_ctx={}, **kwargs + ) - loop = asyncio.get_event_loop() - return loop.run_until_complete(do_request()) + handler = request_handler or default_handler + return run_with_test_server(client_request, url, handler) def test_status_codes(self): for status_code, span_status in ( @@ -111,7 +121,7 @@ def test_status_codes(self): ): with self.subTest(status_code=status_code): host, port = self._http_request( - trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), + trace_config=aiohttp_client.create_trace_config(), url="/test-path?query=param#foobar", status_code=status_code, ) @@ -144,7 +154,7 @@ def test_not_recording(self): with mock.patch("opentelemetry.trace.get_tracer"): # pylint: disable=W0612 host, port = self._http_request( - trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), + trace_config=aiohttp_client.create_trace_config(), url="/test-path?query=param#foobar", ) self.assertFalse(mock_span.is_recording()) @@ -166,7 +176,7 @@ def test_span_name_option(self): ): with self.subTest(span_name=span_name, method=method, path=path): host, port = self._http_request( - trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config( + trace_config=aiohttp_client.create_trace_config( span_name=span_name ), method=method, @@ -199,7 +209,7 @@ def strip_query_params(url: yarl.URL) -> str: return str(url.with_query(None)) host, port = self._http_request( - trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config( + trace_config=aiohttp_client.create_trace_config( url_filter=strip_query_params ), url="/some/path?query=param&other=param2", @@ -225,9 +235,7 @@ def strip_query_params(url: yarl.URL) -> str: ) def test_connection_errors(self): - trace_configs = [ - opentelemetry.instrumentation.aiohttp_client.create_trace_config() - ] + trace_configs = [aiohttp_client.create_trace_config()] for url, expected_status in ( ("http://this-is-unknown.local/", StatusCanonicalCode.UNKNOWN), @@ -237,7 +245,7 @@ def test_connection_errors(self): async def do_request(url): async with aiohttp.ClientSession( - trace_configs=trace_configs + trace_configs=trace_configs, ) as session: async with session.get(url): pass @@ -268,7 +276,7 @@ async def request_handler(request): return aiohttp.web.Response() host, port = self._http_request( - trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), + trace_config=aiohttp_client.create_trace_config(), url="/test_timeout", request_handler=request_handler, timeout=aiohttp.ClientTimeout(sock_read=0.01), @@ -298,7 +306,7 @@ async def request_handler(request): raise aiohttp.web.HTTPFound(location=location) host, port = self._http_request( - trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), + trace_config=aiohttp_client.create_trace_config(), url="/test_too_many_redirects", request_handler=request_handler, max_redirects=2, @@ -319,3 +327,177 @@ async def request_handler(request): ) ] ) + + +class TestAioHttpClientInstrumentor(TestBase): + URL = "/test-path" + + def setUp(self): + super().setUp() + AioHttpClientInstrumentor().instrument() + + def tearDown(self): + super().tearDown() + AioHttpClientInstrumentor().uninstrument() + + @staticmethod + # pylint:disable=unused-argument + async def default_handler(request): + return aiohttp.web.Response(status=int(200)) + + @staticmethod + def get_default_request(url: str = URL): + async def default_request(server: aiohttp.test_utils.TestServer): + async with aiohttp.test_utils.TestClient(server) as session: + await session.get(url) + + return default_request + + def assert_spans(self, num_spans: int): + finished_spans = self.memory_exporter.get_finished_spans() + self.assertEqual(num_spans, len(finished_spans)) + if num_spans == 0: + return None + if num_spans == 1: + return finished_spans[0] + return finished_spans + + def test_instrument(self): + host, port = run_with_test_server( + self.get_default_request(), self.URL, self.default_handler + ) + span = self.assert_spans(1) + self.assertEqual("http", span.attributes["component"]) + self.assertEqual("GET", span.attributes["http.method"]) + self.assertEqual( + "http://{}:{}/test-path".format(host, port), + span.attributes["http.url"], + ) + self.assertEqual(200, span.attributes["http.status_code"]) + self.assertEqual("OK", span.attributes["http.status_text"]) + + def test_instrument_with_existing_trace_config(self): + trace_config = aiohttp.TraceConfig() + + async def create_session(server: aiohttp.test_utils.TestServer): + async with aiohttp.test_utils.TestClient( + server, trace_configs=[trace_config] + ) as client: + # pylint:disable=protected-access + trace_configs = client.session._trace_configs + self.assertEqual(2, len(trace_configs)) + self.assertTrue(trace_config in trace_configs) + async with client as session: + await session.get(TestAioHttpClientInstrumentor.URL) + + run_with_test_server(create_session, self.URL, self.default_handler) + self.assert_spans(1) + + def test_uninstrument(self): + AioHttpClientInstrumentor().uninstrument() + run_with_test_server( + self.get_default_request(), self.URL, self.default_handler + ) + + self.assert_spans(0) + + AioHttpClientInstrumentor().instrument() + run_with_test_server( + self.get_default_request(), self.URL, self.default_handler + ) + self.assert_spans(1) + + def test_uninstrument_session(self): + async def uninstrument_request(server: aiohttp.test_utils.TestServer): + client = aiohttp.test_utils.TestClient(server) + AioHttpClientInstrumentor().uninstrument_session(client.session) + async with client as session: + await session.get(self.URL) + + run_with_test_server( + uninstrument_request, self.URL, self.default_handler + ) + self.assert_spans(0) + + run_with_test_server( + self.get_default_request(), self.URL, self.default_handler + ) + self.assert_spans(1) + + def test_suppress_instrumentation(self): + token = context.attach( + context.set_value("suppress_instrumentation", True) + ) + try: + run_with_test_server( + self.get_default_request(), self.URL, self.default_handler + ) + finally: + context.detach(token) + self.assert_spans(0) + + @staticmethod + async def suppressed_request(server: aiohttp.test_utils.TestServer): + async with aiohttp.test_utils.TestClient(server) as client: + token = context.attach( + context.set_value("suppress_instrumentation", True) + ) + await client.get(TestAioHttpClientInstrumentor.URL) + context.detach(token) + + def test_suppress_instrumentation_after_creation(self): + run_with_test_server( + self.suppressed_request, self.URL, self.default_handler + ) + self.assert_spans(0) + + def test_suppress_instrumentation_with_server_exception(self): + # pylint:disable=unused-argument + async def raising_handler(request): + raise aiohttp.web.HTTPFound(location=self.URL) + + run_with_test_server( + self.suppressed_request, self.URL, raising_handler + ) + self.assert_spans(0) + + def test_url_filter(self): + def strip_query_params(url: yarl.URL) -> str: + return str(url.with_query(None)) + + AioHttpClientInstrumentor().uninstrument() + AioHttpClientInstrumentor().instrument(url_filter=strip_query_params) + + url = "/test-path?query=params" + host, port = run_with_test_server( + self.get_default_request(url), url, self.default_handler + ) + span = self.assert_spans(1) + self.assertEqual( + "http://{}:{}/test-path".format(host, port), + span.attributes["http.url"], + ) + + def test_span_name(self): + def span_name_callback(params: aiohttp.TraceRequestStartParams) -> str: + return "{} - {}".format(params.method, params.url.path) + + AioHttpClientInstrumentor().uninstrument() + AioHttpClientInstrumentor().instrument(span_name=span_name_callback) + + url = "/test-path" + run_with_test_server( + self.get_default_request(url), url, self.default_handler + ) + span = self.assert_spans(1) + self.assertEqual("GET - /test-path", span.name) + + +class TestLoadingAioHttpInstrumentor(unittest.TestCase): + def test_loading_instrumentor(self): + entry_points = iter_entry_points( + "opentelemetry_instrumentor", "aiohttp-client" + ) + + instrumentor = next(entry_points).load()() + self.assertIsInstance(instrumentor, AioHttpClientInstrumentor) From 5760c0e792572c840613720eb1fdc41c5151c7af Mon Sep 17 00:00:00 2001 From: Eoin Noble Date: Fri, 9 Oct 2020 15:36:30 +0100 Subject: [PATCH 0605/1517] Make event attributes immutable (#1195) --- opentelemetry-sdk/CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/__init__.py | 14 ++++++++--- opentelemetry-sdk/tests/trace/test_trace.py | 25 ++++++++++++++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 796fa39166..c19f0bc647 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -10,6 +10,8 @@ ([#1105](https://github.com/open-telemetry/opentelemetry-python/pull/1120)) - Allow for Custom Trace and Span IDs Generation - `IdsGenerator` for TracerProvider ([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153)) +- Event attributes are now immutable + ([#1195](https://github.com/open-telemetry/opentelemetry-python/pull/1195)) - Renaming metrics Batcher to Processor ([#1203](https://github.com/open-telemetry/opentelemetry-python/pull/1203)) - Protect access to Span implementation diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index d0626a6568..4eb6760a9a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -18,12 +18,11 @@ import concurrent.futures import json import logging -import random import threading import traceback from collections import OrderedDict from contextlib import contextmanager -from types import TracebackType +from types import MappingProxyType, TracebackType from typing import ( Any, Callable, @@ -340,6 +339,10 @@ def _filter_attribute_values(attributes: types.Attributes): attributes.pop(attr_key) +def _create_immutable_attributes(attributes): + return MappingProxyType(attributes.copy() if attributes else {}) + + class Span(trace_api.Span): """See `opentelemetry.trace.Span`. @@ -408,6 +411,10 @@ def __init__( if events: for event in events: _filter_attribute_values(event.attributes) + # pylint: disable=protected-access + event._attributes = _create_immutable_attributes( + event.attributes + ) self.events.append(event) if links is None: @@ -566,8 +573,7 @@ def add_event( timestamp: Optional[int] = None, ) -> None: _filter_attribute_values(attributes) - if not attributes: - attributes = self._new_attributes() + attributes = _create_immutable_attributes(attributes) self._add_event( Event( name=name, diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 6a0399c28c..92dce44e23 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -590,7 +590,6 @@ def test_events(self): root.add_event("event0") # event name and attributes - now = time_ns() root.add_event( "event1", {"name": "pluto", "some_bools": [True, False]} ) @@ -628,6 +627,30 @@ def test_events(self): root.events[3].attributes, {"name": ("original_contents",)} ) + def test_events_are_immutable(self): + event_properties = [ + prop for prop in dir(trace.EventBase) if not prop.startswith("_") + ] + + with self.tracer.start_as_current_span("root") as root: + root.add_event("event0", {"name": ["birthday"]}) + event = root.events[0] + + for prop in event_properties: + with self.assertRaises(AttributeError): + setattr(event, prop, "something") + + def test_event_attributes_are_immutable(self): + with self.tracer.start_as_current_span("root") as root: + root.add_event("event0", {"name": ["birthday"]}) + event = root.events[0] + + with self.assertRaises(TypeError): + event.attributes["name"][0] = "happy" + + with self.assertRaises(TypeError): + event.attributes["name"] = "hello" + def test_invalid_event_attributes(self): self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) From 5f2cbfa529f303071b14958f3e4c2f8c6d53ffc9 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 9 Oct 2020 13:16:46 -0400 Subject: [PATCH 0606/1517] Bump PyMySql Instrumentation version (#1228) --- .../opentelemetry-instrumentation-pymysql/CHANGELOG.md | 3 +++ .../opentelemetry-instrumentation-pymysql/setup.cfg | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md index 135b985936..497948a639 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Bumped version from 0.9.3 to 0.10.1 + ([#1228](https://github.com/open-telemetry/opentelemetry-python/pull/1228)) + ## Version 0.13b0 Released 2020-09-17 diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg index 6f3870b7e3..c7861333bf 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg @@ -42,7 +42,7 @@ install_requires = opentelemetry-api == 0.14.dev0 opentelemetry-instrumentation-dbapi == 0.14.dev0 opentelemetry-instrumentation == 0.14.dev0 - PyMySQL ~= 0.9.3 + PyMySQL ~= 0.10.1 [options.extras_require] test = From 04792d56f196c16d2a0f02f829e259b9ba133887 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 12 Oct 2020 09:09:07 -0600 Subject: [PATCH 0607/1517] Add timestamps to aggregators and OTLP metrics exporter (#1199) --- .../opentelemetry-exporter-otlp/CHANGELOG.md | 2 + .../otlp/metrics_exporter/__init__.py | 6 + .../tests/test_otlp_metric_exporter.py | 7 +- opentelemetry-sdk/CHANGELOG.md | 2 + .../sdk/metrics/export/aggregate.py | 229 +++++++----------- .../tests/metrics/export/test_export.py | 114 +++++---- opentelemetry-sdk/tests/metrics/test_view.py | 9 +- 7 files changed, 183 insertions(+), 186 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 8070b1b118..9c807d3225 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add timestamps to OTLP exporter + ([#1199](https://github.com/open-telemetry/opentelemetry-python/pull/1199)) - Update OpenTelemetry protos to v0.5.0 ([#1143](https://github.com/open-telemetry/opentelemetry-python/pull/1143)) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py index 1fa1bf24f1..40feb222fb 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -85,6 +85,12 @@ def _get_data_points( data_point_class( labels=string_key_values, value=view_data.aggregator.current, + start_time_unix_nano=( + view_data.aggregator.last_checkpoint_timestamp + ), + time_unix_nano=( + view_data.aggregator.last_update_timestamp + ), ) ) break diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index 1eba2bef66..21a718b84e 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -14,6 +14,7 @@ from collections import OrderedDict from unittest import TestCase +from unittest.mock import patch from opentelemetry.exporter.otlp.metrics_exporter import OTLPMetricsExporter from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( @@ -59,9 +60,12 @@ def setUp(self): resource, ) - def test_translate_metrics(self): + @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") + def test_translate_metrics(self, mock_time_ns): # pylint: disable=no-member + mock_time_ns.configure_mock(**{"return_value": 1}) + self.counter_metric_record.instrument.add(1, OrderedDict([("a", "b")])) expected = ExportMetricsServiceRequest( @@ -91,6 +95,7 @@ def test_translate_metrics(self): ) ], value=1, + time_unix_nano=1, ) ], aggregation_temporality=( diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index c19f0bc647..fa954a2cf4 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add timestamps to aggregators + ([#1199](https://github.com/open-telemetry/opentelemetry-python/pull/1199)) - Add Global Error Handler ([#1080](https://github.com/open-telemetry/opentelemetry-python/pull/1080)) - Update sampling result names diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 121f39a98b..7d3ad52df0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -16,6 +16,7 @@ import logging import threading from collections import OrderedDict, namedtuple +from math import inf from opentelemetry.util import time_ns @@ -30,9 +31,10 @@ class Aggregator(abc.ABC): """ def __init__(self, config=None): - self.current = None - self.checkpoint = None - if config: + self._lock = threading.Lock() + self.last_update_timestamp = 0 + self.last_checkpoint_timestamp = 0 + if config is not None: self.config = config else: self.config = {} @@ -40,14 +42,32 @@ def __init__(self, config=None): @abc.abstractmethod def update(self, value): """Updates the current with the new value.""" + self.last_update_timestamp = time_ns() @abc.abstractmethod def take_checkpoint(self): """Stores a snapshot of the current value.""" + self.last_checkpoint_timestamp = time_ns() @abc.abstractmethod def merge(self, other): """Combines two aggregator values.""" + self.last_update_timestamp = max( + self.last_update_timestamp, other.last_update_timestamp + ) + self.last_checkpoint_timestamp = max( + self.last_checkpoint_timestamp, other.last_checkpoint_timestamp + ) + + def _verify_type(self, other): + if isinstance(other, self.__class__): + return True + logger.warning( + "Error in merging %s with %s.", + self.__class__.__name__, + other.__class__.__name__, + ) + return False class SumAggregator(Aggregator): @@ -57,157 +77,116 @@ def __init__(self, config=None): super().__init__(config=config) self.current = 0 self.checkpoint = 0 - self._lock = threading.Lock() - self.last_update_timestamp = None def update(self, value): with self._lock: self.current += value - self.last_update_timestamp = time_ns() + super().update(value) def take_checkpoint(self): with self._lock: self.checkpoint = self.current self.current = 0 + super().take_checkpoint() def merge(self, other): - if verify_type(self, other): + if self._verify_type(other): with self._lock: self.checkpoint += other.checkpoint - self.last_update_timestamp = get_latest_timestamp( - self.last_update_timestamp, other.last_update_timestamp - ) + super().merge(other) class MinMaxSumCountAggregator(Aggregator): """Aggregator for ValueRecorder metrics that keeps min, max, sum, count.""" _TYPE = namedtuple("minmaxsumcount", "min max sum count") - _EMPTY = _TYPE(None, None, None, 0) - - @classmethod - def _merge_checkpoint(cls, val1, val2): - if val1 is cls._EMPTY: - return val2 - if val2 is cls._EMPTY: - return val1 - return cls._TYPE( - min(val1.min, val2.min), - max(val1.max, val2.max), - val1.sum + val2.sum, - val1.count + val2.count, - ) + _EMPTY = _TYPE(inf, -inf, 0, 0) def __init__(self, config=None): super().__init__(config=config) self.current = self._EMPTY self.checkpoint = self._EMPTY - self._lock = threading.Lock() - self.last_update_timestamp = None def update(self, value): with self._lock: - if self.current is self._EMPTY: - self.current = self._TYPE(value, value, value, 1) - else: - self.current = self._TYPE( - min(self.current.min, value), - max(self.current.max, value), - self.current.sum + value, - self.current.count + 1, - ) - self.last_update_timestamp = time_ns() + self.current = self._TYPE( + min(self.current.min, value), + max(self.current.max, value), + self.current.sum + value, + self.current.count + 1, + ) + super().update(value) def take_checkpoint(self): with self._lock: self.checkpoint = self.current self.current = self._EMPTY + super().take_checkpoint() def merge(self, other): - if verify_type(self, other): + if self._verify_type(other): with self._lock: - self.checkpoint = self._merge_checkpoint( - self.checkpoint, other.checkpoint - ) - self.last_update_timestamp = get_latest_timestamp( - self.last_update_timestamp, other.last_update_timestamp + self.checkpoint = self._TYPE( + min(self.checkpoint.min, other.checkpoint.min), + max(self.checkpoint.max, other.checkpoint.max), + self.checkpoint.sum + other.checkpoint.sum, + self.checkpoint.count + other.checkpoint.count, ) + super().merge(other) class HistogramAggregator(Aggregator): - """Agregator for ValueRecorder metrics that keeps a histogram of values.""" + """Aggregator for ValueRecorder metrics that keeps a histogram of values.""" def __init__(self, config=None): super().__init__(config=config) - self._lock = threading.Lock() - self.last_update_timestamp = None - boundaries = self.config.get("bounds") - if boundaries and self._validate_boundaries(boundaries): - self._boundaries = boundaries - else: - # no buckets except < 0 and > - self._boundaries = (0,) - - self.current = OrderedDict([(bb, 0) for bb in self._boundaries]) - self.checkpoint = OrderedDict([(bb, 0) for bb in self._boundaries]) - - self.current[">"] = 0 - self.checkpoint[">"] = 0 - - # pylint: disable=R0201 - def _validate_boundaries(self, boundaries): - if not boundaries: - logger.warning("Bounds is empty. Using default.") - return False - if not all( - boundaries[ii] < boundaries[ii + 1] - for ii in range(len(boundaries) - 1) - ): - logger.warning( - "Bounds must be sorted in increasing order. Using default." - ) - return False - return True - - @classmethod - def _merge_checkpoint(cls, val1, val2): - if val1.keys() == val2.keys(): - for ii, bb in val2.items(): - val1[ii] += bb - else: - logger.warning("Cannot merge histograms with different buckets.") - return val1 + # no buckets except < 0 and > + bounds = (0,) + config_bounds = self.config.get("bounds") + if config_bounds is not None: + if all( + config_bounds[i] < config_bounds[i + 1] + for i in range(len(config_bounds) - 1) + ): + bounds = config_bounds + else: + logger.warning( + "Bounds must be all different and sorted in increasing" + " order. Using default." + ) + + self.current = OrderedDict([(bb, 0) for bb in bounds]) + self.current[inf] = 0 + self.checkpoint = OrderedDict([(bb, 0) for bb in bounds]) + self.checkpoint[inf] = 0 def update(self, value): with self._lock: - if self.current is None: - self.current = [0 for ii in range(len(self._boundaries) + 1)] - # greater than max value - if value >= self._boundaries[len(self._boundaries) - 1]: - self.current[">"] += 1 - else: - for bb in self._boundaries: - # find first bucket that value is less than - if value < bb: - self.current[bb] += 1 - break - self.last_update_timestamp = time_ns() + for bb in self.current.keys(): + # find first bucket that value is less than + if value < bb: + self.current[bb] += 1 + break + super().update(value) def take_checkpoint(self): with self._lock: - self.checkpoint = self.current - self.current = OrderedDict([(bb, 0) for bb in self._boundaries]) - self.current[">"] = 0 + self.checkpoint = self.current.copy() + for bb in self.current.keys(): + self.current[bb] = 0 + super().take_checkpoint() def merge(self, other): - if verify_type(self, other): + if self._verify_type(other): with self._lock: - self.checkpoint = self._merge_checkpoint( - self.checkpoint, other.checkpoint - ) - self.last_update_timestamp = get_latest_timestamp( - self.last_update_timestamp, other.last_update_timestamp - ) + if self.checkpoint.keys() == other.checkpoint.keys(): + for ii, bb in other.checkpoint.items(): + self.checkpoint[ii] += bb + super().merge(other) + else: + logger.warning( + "Cannot merge histograms with different buckets." + ) class LastValueAggregator(Aggregator): @@ -215,24 +194,23 @@ class LastValueAggregator(Aggregator): def __init__(self, config=None): super().__init__(config=config) - self._lock = threading.Lock() - self.last_update_timestamp = None + self.current = None + self.checkpoint = None def update(self, value): with self._lock: self.current = value - self.last_update_timestamp = time_ns() + super().update(value) def take_checkpoint(self): with self._lock: self.checkpoint = self.current self.current = None + super().take_checkpoint() def merge(self, other): last = self.checkpoint - self.last_update_timestamp = get_latest_timestamp( - self.last_update_timestamp, other.last_update_timestamp - ) + super().merge(other) if self.last_update_timestamp == other.last_update_timestamp: last = other.checkpoint self.checkpoint = last @@ -248,46 +226,27 @@ def __init__(self, config=None): self.mmsc = MinMaxSumCountAggregator() self.current = None self.checkpoint = self._TYPE(None, None, None, 0, None) - self.last_update_timestamp = None def update(self, value): self.mmsc.update(value) self.current = value - self.last_update_timestamp = time_ns() + super().update(value) def take_checkpoint(self): self.mmsc.take_checkpoint() self.checkpoint = self._TYPE(*(self.mmsc.checkpoint + (self.current,))) + super().take_checkpoint() def merge(self, other): - if verify_type(self, other): + if self._verify_type(other): self.mmsc.merge(other.mmsc) last = self.checkpoint.last - self.last_update_timestamp = get_latest_timestamp( + + self.last_update_timestamp = max( self.last_update_timestamp, other.last_update_timestamp ) + if self.last_update_timestamp == other.last_update_timestamp: last = other.checkpoint.last self.checkpoint = self._TYPE(*(self.mmsc.checkpoint + (last,))) - - -def get_latest_timestamp(time_stamp, other_timestamp): - if time_stamp is None: - return other_timestamp - if other_timestamp is not None: - if time_stamp < other_timestamp: - return other_timestamp - return time_stamp - - -# pylint: disable=R1705 -def verify_type(this, other): - if isinstance(other, this.__class__): - return True - else: - logger.warning( - "Error in merging %s with %s.", - this.__class__.__name__, - other.__class__.__name__, - ) - return False + super().merge(other) diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index b0c74e7093..a96c8af455 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -15,6 +15,7 @@ import concurrent.futures import random import unittest +from math import inf from unittest import mock from opentelemetry.context import get_value @@ -324,36 +325,58 @@ def test_merge(self): mmsc1.merge(mmsc2) - self.assertEqual( - mmsc1.checkpoint, - MinMaxSumCountAggregator._merge_checkpoint( - checkpoint1, checkpoint2 - ), - ) + mmsc1_checkpoint = mmsc1.checkpoint + mmsc1.checkpoint = checkpoint1 + mmsc2.checkpoint = checkpoint2 + + mmsc1.merge(mmsc2) + + self.assertEqual(mmsc1_checkpoint, mmsc1.checkpoint) + self.assertEqual(mmsc1.last_update_timestamp, 123) def test_merge_checkpoint(self): - func = MinMaxSumCountAggregator._merge_checkpoint - _type = MinMaxSumCountAggregator._TYPE + type_ = MinMaxSumCountAggregator._TYPE empty = MinMaxSumCountAggregator._EMPTY - ret = func(empty, empty) - self.assertEqual(ret, empty) + mmsc0 = MinMaxSumCountAggregator() + mmsc1 = MinMaxSumCountAggregator() + + mmsc0.checkpoint = empty + mmsc1.checkpoint = empty + + mmsc0.merge(mmsc1) + self.assertEqual(mmsc0.checkpoint, mmsc1.checkpoint) - ret = func(empty, _type(0, 0, 0, 0)) - self.assertEqual(ret, _type(0, 0, 0, 0)) + mmsc0.checkpoint = empty + mmsc1.checkpoint = type_(0, 0, 0, 0) - ret = func(_type(0, 0, 0, 0), empty) - self.assertEqual(ret, _type(0, 0, 0, 0)) + mmsc0.merge(mmsc1) + self.assertEqual(mmsc0.checkpoint, mmsc1.checkpoint) - ret = func(_type(0, 0, 0, 0), _type(0, 0, 0, 0)) - self.assertEqual(ret, _type(0, 0, 0, 0)) + mmsc0.checkpoint = type_(0, 0, 0, 0) + mmsc1.checkpoint = empty - ret = func(_type(44, 23, 55, 86), empty) - self.assertEqual(ret, _type(44, 23, 55, 86)) + mmsc1.merge(mmsc0) + self.assertEqual(mmsc1.checkpoint, mmsc0.checkpoint) - ret = func(_type(3, 150, 101, 3), _type(1, 33, 44, 2)) - self.assertEqual(ret, _type(1, 150, 101 + 44, 2 + 3)) + mmsc0.checkpoint = type_(0, 0, 0, 0) + mmsc1.checkpoint = type_(0, 0, 0, 0) + + mmsc0.merge(mmsc1) + self.assertEqual(mmsc1.checkpoint, mmsc0.checkpoint) + + mmsc0.checkpoint = type_(44, 23, 55, 86) + mmsc1.checkpoint = empty + + mmsc0.merge(mmsc1) + self.assertEqual(mmsc0.checkpoint, type_(44, 23, 55, 86)) + + mmsc0.checkpoint = type_(3, 150, 101, 3) + mmsc1.checkpoint = type_(1, 33, 44, 2) + + mmsc0.merge(mmsc1) + self.assertEqual(mmsc0.checkpoint, type_(1, 150, 101 + 44, 2 + 3)) def test_merge_with_empty(self): mmsc1 = MinMaxSumCountAggregator() @@ -367,40 +390,39 @@ def test_merge_with_empty(self): self.assertEqual(mmsc1.checkpoint, checkpoint1) def test_concurrent_update(self): - mmsc = MinMaxSumCountAggregator() + mmsc0 = MinMaxSumCountAggregator() + mmsc1 = MinMaxSumCountAggregator() + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as ex: - fut1 = ex.submit(self.call_update, mmsc) - fut2 = ex.submit(self.call_update, mmsc) + mmsc0.checkpoint = ex.submit(self.call_update, mmsc0).result() + mmsc1.checkpoint = ex.submit(self.call_update, mmsc0).result() + + mmsc0.merge(mmsc1) - ret1 = fut1.result() - ret2 = fut2.result() + mmsc0_checkpoint = mmsc0.checkpoint - update_total = MinMaxSumCountAggregator._merge_checkpoint( - ret1, ret2 - ) - mmsc.take_checkpoint() + mmsc0.take_checkpoint() - self.assertEqual(update_total, mmsc.checkpoint) + self.assertEqual(mmsc0_checkpoint, mmsc0.checkpoint) + self.assertIsNot(mmsc0_checkpoint, mmsc0.checkpoint) def test_concurrent_update_and_checkpoint(self): - mmsc = MinMaxSumCountAggregator() - checkpoint_total = MinMaxSumCountAggregator._TYPE(2 ** 32, 0, 0, 0) + mmsc0 = MinMaxSumCountAggregator() + mmsc1 = MinMaxSumCountAggregator() + mmsc1.checkpoint = MinMaxSumCountAggregator._TYPE(2 ** 32, 0, 0, 0) with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex: - fut = ex.submit(self.call_update, mmsc) + fut = ex.submit(self.call_update, mmsc0) while not fut.done(): - mmsc.take_checkpoint() - checkpoint_total = MinMaxSumCountAggregator._merge_checkpoint( - checkpoint_total, mmsc.checkpoint - ) + mmsc0.take_checkpoint() + mmsc0.merge(mmsc1) + mmsc1.checkpoint = mmsc0.checkpoint - mmsc.take_checkpoint() - checkpoint_total = MinMaxSumCountAggregator._merge_checkpoint( - checkpoint_total, mmsc.checkpoint - ) + mmsc0.take_checkpoint() + mmsc0.merge(mmsc1) - self.assertEqual(checkpoint_total, fut.result()) + self.assertEqual(mmsc0.checkpoint, fut.result()) class TestValueObserverAggregator(unittest.TestCase): @@ -409,7 +431,7 @@ def test_update(self, time_mock): time_mock.return_value = 123 observer = ValueObserverAggregator() # test current values without any update - self.assertEqual(observer.mmsc.current, (None, None, None, 0)) + self.assertEqual(observer.mmsc.current, (inf, -inf, 0, 0)) self.assertIsNone(observer.current) # call update with some values @@ -430,7 +452,7 @@ def test_checkpoint(self): # take checkpoint wihtout any update observer.take_checkpoint() - self.assertEqual(observer.checkpoint, (None, None, None, 0, None)) + self.assertEqual(observer.checkpoint, (inf, -inf, 0, 0, None)) # call update with some values values = (3, 50, 3, 97) @@ -537,7 +559,7 @@ def test_merge_last_updated_none(self): observer1.mmsc.checkpoint = mmsc_checkpoint1 observer2.mmsc.checkpoint = mmsc_checkpoint2 - observer1.last_update_timestamp = None + observer1.last_update_timestamp = 0 observer2.last_update_timestamp = 100 observer1.checkpoint = checkpoint1 @@ -643,7 +665,7 @@ def test_merge_last_updated_none(self): observer1.checkpoint = 23 observer2.checkpoint = 47 - observer1.last_update_timestamp = None + observer1.last_update_timestamp = 0 observer2.last_update_timestamp = 100 observer1.merge(observer2) diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py index 0de6b22731..b4a24d4ea3 100644 --- a/opentelemetry-sdk/tests/metrics/test_view.py +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest +from math import inf from unittest import mock from opentelemetry.sdk import metrics @@ -223,7 +224,7 @@ def test_histogram_stateful(self): checkpoint = metrics_list[0].aggregator.checkpoint self.assertEqual( tuple(checkpoint.items()), - ((20, 1), (40, 1), (60, 0), (80, 0), (100, 0), (">", 1)), + ((20, 1), (40, 1), (60, 0), (80, 0), (100, 0), (inf, 1)), ) exporter.clear() @@ -238,7 +239,7 @@ def test_histogram_stateful(self): checkpoint = metrics_list[0].aggregator.checkpoint self.assertEqual( tuple(checkpoint.items()), - ((20, 2), (40, 2), (60, 0), (80, 0), (100, 0), (">", 2)), + ((20, 2), (40, 2), (60, 0), (80, 0), (100, 0), (inf, 2)), ) def test_histogram_stateless(self): @@ -278,7 +279,7 @@ def test_histogram_stateless(self): checkpoint = metrics_list[0].aggregator.checkpoint self.assertEqual( tuple(checkpoint.items()), - ((20, 1), (40, 1), (60, 0), (80, 0), (100, 0), (">", 1)), + ((20, 1), (40, 1), (60, 0), (80, 0), (100, 0), (inf, 1)), ) exporter.clear() @@ -293,7 +294,7 @@ def test_histogram_stateless(self): checkpoint = metrics_list[0].aggregator.checkpoint self.assertEqual( tuple(checkpoint.items()), - ((20, 1), (40, 1), (60, 0), (80, 0), (100, 0), (">", 1)), + ((20, 1), (40, 1), (60, 0), (80, 0), (100, 0), (inf, 1)), ) From 6019a91980ec84bbf969b0d82d44483c93f3ea4c Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 13 Oct 2020 14:38:09 -0400 Subject: [PATCH 0608/1517] chore: bump dev version (#1235) --- docs/examples/error_hander/error_handler_0/setup.cfg | 2 +- .../error_handler_0/src/error_handler_0/version.py | 2 +- docs/examples/error_hander/error_handler_1/setup.cfg | 2 +- .../error_handler_1/src/error_handler_1/version.py | 2 +- docs/examples/opentelemetry-example-app/setup.cfg | 8 ++++---- .../src/opentelemetry_example_app/version.py | 2 +- exporter/opentelemetry-exporter-datadog/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-datadog/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/datadog/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- .../setup.cfg | 4 ++-- .../instrumentation/aiohttp_client/version.py | 2 +- .../opentelemetry-instrumentation-aiopg/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/aiopg/version.py | 2 +- .../opentelemetry-instrumentation-asgi/setup.cfg | 4 ++-- .../src/opentelemetry/instrumentation/asgi/version.py | 2 +- .../opentelemetry-instrumentation-asyncpg/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/asyncpg/version.py | 2 +- .../opentelemetry-instrumentation-boto/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/boto/version.py | 2 +- .../opentelemetry-instrumentation-botocore/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/botocore/version.py | 2 +- .../opentelemetry-instrumentation-celery/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-celery/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/celery/version.py | 2 +- .../opentelemetry-instrumentation-dbapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/dbapi/version.py | 2 +- .../opentelemetry-instrumentation-django/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-django/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/django/version.py | 2 +- .../opentelemetry-instrumentation-elasticsearch/setup.cfg | 6 +++--- .../instrumentation/elasticsearch/version.py | 2 +- .../opentelemetry-instrumentation-falcon/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-falcon/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/falcon/version.py | 2 +- .../opentelemetry-instrumentation-fastapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/fastapi/version.py | 2 +- .../opentelemetry-instrumentation-flask/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/flask/version.py | 2 +- .../opentelemetry-instrumentation-grpc/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/grpc/version.py | 2 +- .../opentelemetry-instrumentation-jinja2/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/jinja2/version.py | 2 +- .../opentelemetry-instrumentation-mysql/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/mysql/version.py | 2 +- .../setup.cfg | 4 ++-- .../instrumentation/opentracing_shim/version.py | 2 +- .../opentelemetry-instrumentation-psycopg2/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/psycopg2/version.py | 2 +- .../opentelemetry-instrumentation-pymemcache/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/pymemcache/version.py | 2 +- .../opentelemetry-instrumentation-pymongo/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-pymongo/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/pymongo/version.py | 2 +- .../opentelemetry-instrumentation-pymysql/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-pymysql/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/pymysql/version.py | 2 +- .../opentelemetry-instrumentation-pyramid/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/pyramid/version.py | 2 +- .../opentelemetry-instrumentation-redis/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/redis/version.py | 2 +- .../opentelemetry-instrumentation-requests/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/requests/version.py | 2 +- .../opentelemetry-instrumentation-sqlalchemy/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/sqlalchemy/version.py | 2 +- .../opentelemetry-instrumentation-sqlite3/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/sqlite3/version.py | 2 +- .../opentelemetry-instrumentation-starlette/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/starlette/version.py | 2 +- .../CHANGELOG.md | 4 ++++ .../setup.cfg | 6 +++--- .../instrumentation/system_metrics/version.py | 2 +- .../opentelemetry-instrumentation-tornado/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-tornado/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/tornado/version.py | 2 +- .../opentelemetry-instrumentation-wsgi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/wsgi/version.py | 2 +- opentelemetry-api/CHANGELOG.md | 4 ++++ opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-instrumentation/CHANGELOG.md | 4 ++++ opentelemetry-instrumentation/setup.cfg | 4 ++-- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 4 ++++ opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 98 files changed, 217 insertions(+), 165 deletions(-) diff --git a/docs/examples/error_hander/error_handler_0/setup.cfg b/docs/examples/error_hander/error_handler_0/setup.cfg index 61760f5dea..a569d81b2a 100644 --- a/docs/examples/error_hander/error_handler_0/setup.cfg +++ b/docs/examples/error_hander/error_handler_0/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.14.dev0 + opentelemetry-sdk == 0.15.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py index 0f99027898..e7b342d644 100644 --- a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py +++ b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/docs/examples/error_hander/error_handler_1/setup.cfg b/docs/examples/error_hander/error_handler_1/setup.cfg index 0237f3692e..0d3366d3d0 100644 --- a/docs/examples/error_hander/error_handler_1/setup.cfg +++ b/docs/examples/error_hander/error_handler_1/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.14.dev0 + opentelemetry-sdk == 0.15.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py index 0f99027898..e7b342d644 100644 --- a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py +++ b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index 952bf59380..2f42afdb30 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -42,10 +42,10 @@ zip_safe = False include_package_data = True install_requires = typing; python_version<'3.5' - opentelemetry-api == 0.14.dev0 - opentelemetry-sdk == 0.14.dev0 - opentelemetry-instrumentation-requests == 0.14.dev0 - opentelemetry-instrumentation-flask == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 + opentelemetry-instrumentation-requests == 0.15.dev0 + opentelemetry-instrumentation-flask == 0.15.dev0 flask requests protobuf~=3.11 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py index 0f99027898..e7b342d644 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/exporter/opentelemetry-exporter-datadog/CHANGELOG.md b/exporter/opentelemetry-exporter-datadog/CHANGELOG.md index 0fab809da9..673bb28c54 100644 --- a/exporter/opentelemetry-exporter-datadog/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-datadog/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.14b0 + +Released 2020-10-13 + - Add support for span resource labels and service name ## Version 0.12b0 diff --git a/exporter/opentelemetry-exporter-datadog/setup.cfg b/exporter/opentelemetry-exporter-datadog/setup.cfg index b2bfa3771e..d429374a6b 100644 --- a/exporter/opentelemetry-exporter-datadog/setup.cfg +++ b/exporter/opentelemetry-exporter-datadog/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = ddtrace>=0.34.0 - opentelemetry-api == 0.14.dev0 - opentelemetry-sdk == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py index 0f99027898..e7b342d644 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index f55c01eaa3..8294af4caa 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.14.dev0 - opentelemetry-sdk == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 94a3dc9bc3..af61d19db8 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 91041fe46e..2a8ecb2114 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.14.dev0 - opentelemetry-sdk == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 protobuf >= 3.8.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 0f99027898..e7b342d644 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 9c807d3225..479f2105e7 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.14b0 + +Released 2020-10-13 + - Add timestamps to OTLP exporter ([#1199](https://github.com/open-telemetry/opentelemetry-python/pull/1199)) - Update OpenTelemetry protos to v0.5.0 diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 12fdd62775..0f5870813e 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.14.dev0 - opentelemetry-sdk == 0.14.dev0 - opentelemetry-proto == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 + opentelemetry-proto == 0.15.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 0f99027898..e7b342d644 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index a7c33f9549..31ef0160d8 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.14.dev0 - opentelemetry-sdk == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 0f99027898..e7b342d644 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index 3801c81dc9..60d7fbe2ec 100644 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.14b0 + +Released 2020-10-13 + - Zipkin exporter now accepts a ``max_tag_value_length`` attribute to customize the maximum allowed size a tag value can have. ([#1151](https://github.com/open-telemetry/opentelemetry-python/pull/1151)) - Fixed OTLP events to Zipkin annotations translation. ([#1161](https://github.com/open-telemetry/opentelemetry-python/pull/1161)) diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 544ed13639..a071288c14 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 0.14.dev0 - opentelemetry-sdk == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 0f99027898..e7b342d644 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index eae097a62a..27e120d660 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 aiohttp ~= 3.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py index 404790dad7..0fdc34158f 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg index 5d875b0d5c..86a1b170f4 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation-dbapi == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation-dbapi == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 aiopg >= 0.13.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg index aafcfb7199..68ac393560 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 asgiref ~= 3.0 [options.extras_require] diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg index 083d676db5..25e774c247 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 asyncpg >= 0.12.0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg index 1b814df34f..e3572fe526 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg @@ -40,15 +40,15 @@ package_dir= packages=find_namespace: install_requires = boto ~= 2.0 - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 - opentelemetry-instrumentation-botocore == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-instrumentation-botocore == 0.15.dev0 [options.extras_require] test = boto~=2.0 moto~=1.0 - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py +++ b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg index d09975103d..ee7849c143 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg @@ -40,13 +40,13 @@ package_dir= packages=find_namespace: install_requires = botocore ~= 1.0 - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 [options.extras_require] test = moto ~= 1.0 - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md index da615bcc7b..8ebb8c3d24 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.14b0 + +Released 2020-10-13 + - Span operation names now include the task type. ([#1135](https://github.com/open-telemetry/opentelemetry-python/pull/1135)) - Added automatic context propagation. ([#1135](https://github.com/open-telemetry/opentelemetry-python/pull/1135)) diff --git a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg index b5a039de11..9418e25a18 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 celery ~= 4.0 [options.extras_require] test = pytest celery ~= 4.0 - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg index fc1e3fb82b..e644d253a7 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index 30b13c4a22..496d94008b 100644 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.14b0 + +Released 2020-10-13 + - Changed span name extraction from request to comply semantic convention ([#992](https://github.com/open-telemetry/opentelemetry-python/pull/992)) - Added support for `OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS` ([#1154](https://github.com/open-telemetry/opentelemetry-python/pull/1154)) diff --git a/instrumentation/opentelemetry-instrumentation-django/setup.cfg b/instrumentation/opentelemetry-instrumentation-django/setup.cfg index 1fad08d240..37ef9f2a86 100644 --- a/instrumentation/opentelemetry-instrumentation-django/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-django/setup.cfg @@ -40,13 +40,13 @@ package_dir= packages=find_namespace: install_requires = django >= 1.10 - opentelemetry-instrumentation-wsgi == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 - opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation-wsgi == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15.dev0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg index 67ef8b8942..7de55994fa 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 wrapt >= 1.0.0, < 2.0.0 elasticsearch >= 2.0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 elasticsearch-dsl >= 2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md index f398b7460d..85dcb366d0 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.14b0 + +Released 2020-10-13 + - Added support for `OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS` ([#1158](https://github.com/open-telemetry/opentelemetry-python/pull/1158)) ## Version 0.13b0 diff --git a/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg b/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg index e5e6f69fec..57849f5586 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = falcon ~= 2.0 - opentelemetry-instrumentation-wsgi == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 - opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation-wsgi == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15.dev0 [options.extras_require] test = falcon ~= 2.0 - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 parameterized == 0.7.4 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg index c2b59ffa65..ac77c870c0 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation-asgi == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation-asgi == 0.15.dev0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 fastapi ~= 0.58.1 requests ~= 2.23.0 # needed for testclient diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg index cca28d3f4b..1ea37e42ce 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg @@ -40,14 +40,14 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-instrumentation-wsgi == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 - opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation-wsgi == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15.dev0 [options.extras_require] test = flask~=1.0 - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg index 69e91ef9a1..5d39574bfd 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-sdk == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 grpcio ~= 1.27 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 - opentelemetry-sdk == 0.14.dev0 + opentelemetry-test == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 protobuf == 3.12.2 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg index 10d8b260b5..55ee887b5a 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg @@ -38,14 +38,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg index 111f028f68..957119a497 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation-dbapi == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation-dbapi == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg index 26208c7a90..8247d1d517 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg @@ -41,11 +41,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.14.dev0 + opentelemetry-api == 0.15.dev0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg index 69735059cd..e81d9c6b7a 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation-dbapi == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation-dbapi == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg index 31d32bac93..d3cf45713f 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 pymemcache ~= 1.3 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md index 3ed4ea42ba..30e36e0048 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.14b0 + +Released 2020-10-13 + - Cast PyMongo commands as strings ([#1132](https://github.com/open-telemetry/opentelemetry-python/pull/1132)) diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg index d477105fde..f822ecbac8 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 pymongo ~= 3.1 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md index 497948a639..3f78db57b9 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.14b0 + +Released 2020-10-13 + - Bumped version from 0.9.3 to 0.10.1 ([#1228](https://github.com/open-telemetry/opentelemetry-python/pull/1228)) diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg index c7861333bf..c047563e3d 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation-dbapi == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation-dbapi == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 PyMySQL ~= 0.10.1 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg index ed9f274c8b..7e57a7bfae 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg @@ -40,15 +40,15 @@ package_dir= packages=find_namespace: install_requires = pyramid >= 1.7 - opentelemetry-instrumentation == 0.14.dev0 - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation-wsgi == 0.14.dev0 + opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation-wsgi == 0.15.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = werkzeug == 0.16.1 - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg index 2baa2e2e4a..2a8a605eac 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 redis >= 2.6 wrapt >= 1.12.1 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 - opentelemetry-sdk == 0.14.dev0 + opentelemetry-test == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg index 9020ae58a1..cf3c11d7e7 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 requests ~= 2.0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 httpretty ~= 1.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg index 10b9b0867b..f9f9dafb02 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 wrapt >= 1.11.2 sqlalchemy [options.extras_require] test = - opentelemetry-sdk == 0.14.dev0 + opentelemetry-sdk == 0.15.dev0 pytest [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg index 2a72c3bcc8..9b49846669 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation-dbapi == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation-dbapi == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg index 8e1bd8d688..5ad5fb7a82 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation-asgi == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation-asgi == 0.15.dev0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 starlette ~= 0.13.0 requests ~= 2.23.0 # needed for testclient diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md index 7405cd91f7..5f6ff0530c 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.14b0 + +Released 2020-10-13 + - Fix issue when specific metrics are not available in certain OS ([#1207](https://github.com/open-telemetry/opentelemetry-python/pull/1207)) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg index 345393ac82..750b5e07e2 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-sdk == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 psutil ~= 5.7.0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md index e82446d66d..e341a1f5a3 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.14b0 + +Released 2020-10-13 + - Added support for `OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS` ([#1178](https://github.com/open-telemetry/opentelemetry-python/pull/1178)) ## Version 0.13b0 diff --git a/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg b/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg index be1537568a..d3966fce72 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg @@ -39,13 +39,13 @@ package_dir= packages=find_namespace: install_requires = tornado >= 6.0 - opentelemetry-instrumentation == 0.14.dev0 - opentelemetry-api == 0.14.dev0 + opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15.dev0 [options.extras_require] test = tornado >= 6.0 - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg index 2de0e784bf..1570fd5d0e 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg @@ -39,12 +39,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 [options.extras_require] test = - opentelemetry-test == 0.14.dev0 + opentelemetry-test == 0.15.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py index 0f99027898..e7b342d644 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 6504826c57..5f57631832 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.14b0 + +Released 2020-10-13 + - Add support for `OTEL_PROPAGATORS` ([#1123](https://github.com/open-telemetry/opentelemetry-python/pull/1123)) - Store `int`s as `int`s in the global Configuration object diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 0f99027898..e7b342d644 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/opentelemetry-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md index 13c01cc32a..da275415d7 100644 --- a/opentelemetry-instrumentation/CHANGELOG.md +++ b/opentelemetry-instrumentation/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.14b0 + +Released 2020-10-13 + - Fixed boostrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask ## Version 0.13b0 diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 167036238f..e93b1c5bb6 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-sdk == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 0f99027898..e7b342d644 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 0f99027898..e7b342d644 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index fa954a2cf4..5e8f7ebdbe 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.14b0 + +Released 2020-10-13 + - Add timestamps to aggregators ([#1199](https://github.com/open-telemetry/opentelemetry-python/pull/1199)) - Add Global Error Handler diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 92a7853ae5..7035ea83c6 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.14.dev0 + opentelemetry-api == 0.15.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 0f99027898..e7b342d644 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index eef24b9b14..68cfb03e92 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.14.dev0" +__version__ = "0.15.dev0" From c782f145658f40c3c27f6871e06f7412e36efa34 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 14 Oct 2020 21:43:27 +0530 Subject: [PATCH 0609/1517] Allow django to be instrumented automatically (#1239) --- docs/examples/django/README.rst | 8 +++++++- .../CHANGELOG.md | 2 ++ .../instrumentation/django/__init__.py | 2 +- .../tests/conftest.py | 19 ------------------- 4 files changed, 10 insertions(+), 21 deletions(-) delete mode 100644 instrumentation/opentelemetry-instrumentation-django/tests/conftest.py diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 38ef365b32..6f40cf7aeb 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -36,7 +36,6 @@ Execution of the Django app Set these environment variables first: -#. ``export OTEL_PYTHON_DJANGO_INSTRUMENT=True`` #. ``export DJANGO_SETTINGS_MODULE=instrumentation_example.settings`` The way to achieve OpenTelemetry instrumentation for your Django app is to use @@ -100,6 +99,13 @@ output similar to this one: The last output shows spans automatically generated by the OpenTelemetry Django Instrumentation package. +Disabling Django Instrumentation +-------------------------------- + +Django's instrumentation can be disabled by setting the following environment variable. + +#. ``export OTEL_PYTHON_DJANGO_INSTRUMENT=False`` + References ---------- diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index 496d94008b..2248ea35c8 100644 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Django instrumentation is now enabled by default but can be disabled by setting `OTEL_PYTHON_DJANGO_INSTRUMENT` to `False` ([#1239](https://github.com/open-telemetry/opentelemetry-python/pull/1239)) + ## Version 0.14b0 Released 2020-10-13 diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index a9bd620e77..9ba3bbb915 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -41,7 +41,7 @@ def _instrument(self, **kwargs): # built inside the Configuration class itself with the magic method # __bool__ - if not Configuration().DJANGO_INSTRUMENT: + if Configuration().DJANGO_INSTRUMENT is False: return # This can not be solved, but is an inherent problem of this approach: diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/conftest.py b/instrumentation/opentelemetry-instrumentation-django/tests/conftest.py deleted file mode 100644 index 8797bfc306..0000000000 --- a/instrumentation/opentelemetry-instrumentation-django/tests/conftest.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from os import environ - - -def pytest_sessionstart(session): # pylint: disable=unused-argument - environ.setdefault("OTEL_PYTHON_DJANGO_INSTRUMENT", "True") From 535c2e614f7a772e132cdda24d14bf6c1ad5dbdb Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 14 Oct 2020 15:07:23 -0400 Subject: [PATCH 0610/1517] Use is_recording flag in jinja, celery, esearch, falcon instrumentations (#1241) --- .../instrumentation/celery/__init__.py | 22 ++++----- .../instrumentation/celery/utils.py | 2 + .../tests/test_utils.py | 24 ++++++++++ .../instrumentation/elasticsearch/__init__.py | 45 ++++++++++--------- .../tests/test_elasticsearch.py | 18 ++++++++ .../instrumentation/falcon/__init__.py | 13 +++--- .../tests/test_falcon.py | 17 ++++++- .../instrumentation/jinja2/__init__.py | 33 ++++++++------ .../tests/test_jinja2.py | 16 +++++++ .../instrumentation/pymemcache/__init__.py | 15 ++++--- .../instrumentation/tornado/__init__.py | 22 +++++---- 11 files changed, 160 insertions(+), 67 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py index 3bd912c5ed..ab0cdf39b1 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py @@ -148,10 +148,11 @@ def _trace_postrun(*args, **kwargs): return # request context tags - span.set_attribute(_TASK_TAG_KEY, _TASK_RUN) - utils.set_attributes_from_context(span, kwargs) - utils.set_attributes_from_context(span, task.request) - span.set_attribute(_TASK_NAME_KEY, task.name) + if span.is_recording(): + span.set_attribute(_TASK_TAG_KEY, _TASK_RUN) + utils.set_attributes_from_context(span, kwargs) + utils.set_attributes_from_context(span, task.request) + span.set_attribute(_TASK_NAME_KEY, task.name) activation.__exit__(None, None, None) utils.detach_span(task, task_id) @@ -169,10 +170,11 @@ def _trace_before_publish(self, *args, **kwargs): ) # apply some attributes here because most of the data is not available - span.set_attribute(_TASK_TAG_KEY, _TASK_APPLY_ASYNC) - span.set_attribute(_MESSAGE_ID_ATTRIBUTE_NAME, task_id) - span.set_attribute(_TASK_NAME_KEY, task.name) - utils.set_attributes_from_context(span, kwargs) + if span.is_recording(): + span.set_attribute(_TASK_TAG_KEY, _TASK_APPLY_ASYNC) + span.set_attribute(_MESSAGE_ID_ATTRIBUTE_NAME, task_id) + span.set_attribute(_TASK_NAME_KEY, task.name) + utils.set_attributes_from_context(span, kwargs) activation = self._tracer.use_span(span, end_on_exit=True) activation.__enter__() @@ -209,7 +211,7 @@ def _trace_failure(*args, **kwargs): # retrieve and pass exception info to activation span, _ = utils.retrieve_span(task, task_id) - if span is None: + if span is None or not span.is_recording(): return status_kwargs = {"canonical_code": StatusCanonicalCode.UNKNOWN} @@ -238,7 +240,7 @@ def _trace_retry(*args, **kwargs): return span, _ = utils.retrieve_span(task, task_id) - if span is None: + if span is None or not span.is_recording(): return # Add retry reason metadata to span diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py index 60fe52f04e..9a44134598 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py @@ -48,6 +48,8 @@ # pylint:disable=too-many-branches def set_attributes_from_context(span, context): """Helper to extract meta values from a Celery Context""" + if not span.is_recording(): + return for key in CELERY_CONTEXT_ATTRIBUTES: value = context.get(key) diff --git a/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py b/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py index 3df0f445b3..f48f06aafb 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py +++ b/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py @@ -69,6 +69,30 @@ def test_set_attributes_from_context(self): ) self.assertNotIn("custom_meta", span.attributes) + def test_set_attributes_not_recording(self): + # it should extract only relevant keys + context = { + "correlation_id": "44b7f305", + "delivery_info": {"eager": True}, + "eta": "soon", + "expires": "later", + "hostname": "localhost", + "id": "44b7f305", + "reply_to": "44b7f305", + "retries": 4, + "timelimit": ("now", "later"), + "custom_meta": "custom_value", + "routing_key": "celery", + } + + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + utils.set_attributes_from_context(mock_span, context) + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_set_attributes_from_context_empty_keys(self): # it should not extract empty keys context = { diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py index f350a7dc28..6e9f411f8a 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py @@ -110,6 +110,7 @@ def _uninstrument(self, **kwargs): def _wrap_perform_request(tracer, span_name_prefix): + # pylint: disable=R0912 def wrapper(wrapped, _, args, kwargs): method = url = None try: @@ -125,26 +126,27 @@ def wrapper(wrapped, _, args, kwargs): params = kwargs.get("params", {}) body = kwargs.get("body", None) - attributes = { - "component": "elasticsearch-py", - "db.type": "elasticsearch", - } - - if url: - attributes["elasticsearch.url"] = url - if method: - attributes["elasticsearch.method"] = method - if body: - attributes["db.statement"] = str(body) - if params: - attributes["elasticsearch.params"] = str(params) - with tracer.start_as_current_span( - op_name, kind=SpanKind.CLIENT, attributes=attributes + op_name, kind=SpanKind.CLIENT, ) as span: + if span.is_recording(): + attributes = { + "component": "elasticsearch-py", + "db.type": "elasticsearch", + } + if url: + attributes["elasticsearch.url"] = url + if method: + attributes["elasticsearch.method"] = method + if body: + attributes["db.statement"] = str(body) + if params: + attributes["elasticsearch.params"] = str(params) + for key, value in attributes.items(): + span.set_attribute(key, value) try: rv = wrapped(*args, **kwargs) - if isinstance(rv, dict): + if isinstance(rv, dict) and span.is_recording(): for member in _ATTRIBUTES_FROM_RESULT: if member in rv: span.set_attribute( @@ -153,11 +155,12 @@ def wrapper(wrapped, _, args, kwargs): ) return rv except Exception as ex: # pylint: disable=broad-except - if isinstance(ex, elasticsearch.exceptions.NotFoundError): - status = StatusCanonicalCode.NOT_FOUND - else: - status = StatusCanonicalCode.UNKNOWN - span.set_status(Status(status, str(ex))) + if span.is_recording(): + if isinstance(ex, elasticsearch.exceptions.NotFoundError): + status = StatusCanonicalCode.NOT_FOUND + else: + status = StatusCanonicalCode.UNKNOWN + span.set_status(Status(status, str(ex))) raise ex return wrapper diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py index cc1d314774..3d93838fe8 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py @@ -88,6 +88,24 @@ def test_instrumentor(self, request_mock): spans_list = self.get_ordered_finished_spans() self.assertEqual(len(spans_list), 1) + def test_span_not_recording(self, request_mock): + request_mock.return_value = (1, {}, {}) + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = mock_span + with mock.patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + Elasticsearch() + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + + ElasticsearchInstrumentor().uninstrument() + def test_prefix_arg(self, request_mock): prefix = "prefix-from-env" ElasticsearchInstrumentor().uninstrument() diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py index bfcd45a8b5..0a93fe0138 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py @@ -117,13 +117,16 @@ def __call__(self, env, start_response): token = context.attach( propagators.extract(otel_wsgi.get_header_from_environ, env) ) - attributes = otel_wsgi.collect_request_attributes(env) span = self._tracer.start_span( otel_wsgi.get_default_span_name(env), kind=trace.SpanKind.SERVER, - attributes=attributes, start_time=start_time, ) + if span.is_recording(): + attributes = otel_wsgi.collect_request_attributes(env) + for key, value in attributes.items(): + span.set_attribute(key, value) + activation = self._tracer.use_span(span, end_on_exit=True) activation.__enter__() env[_ENVIRON_SPAN_KEY] = span @@ -162,7 +165,7 @@ def __init__(self, tracer=None, traced_request_attrs=None): def process_request(self, req, resp): span = req.env.get(_ENVIRON_SPAN_KEY) - if not span: + if not span or not span.is_recording(): return attributes = extract_attributes_from_object( @@ -173,7 +176,7 @@ def process_request(self, req, resp): def process_resource(self, req, resp, resource, params): span = req.env.get(_ENVIRON_SPAN_KEY) - if not span: + if not span or not span.is_recording(): return resource_name = resource.__class__.__name__ @@ -186,7 +189,7 @@ def process_response( self, req, resp, resource, req_succeeded=None ): # pylint:disable=R0201 span = req.env.get(_ENVIRON_SPAN_KEY) - if not span: + if not span or not span.is_recording(): return status = resp.status diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py index 5e27e4aacb..d64154a777 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest.mock import patch +from unittest.mock import Mock, patch from falcon import testing @@ -188,3 +188,18 @@ def test_traced_request_attributes(self): span = self.memory_exporter.get_finished_spans()[0] self.assertIn("query_string", span.attributes) self.assertEqual(span.attributes["query_string"], "q=abc") + + def test_traced_not_recording(self): + mock_tracer = Mock() + mock_span = Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = mock_span + with patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + self.client().simulate_get(path="/hello?q=abc") + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py index 8ad883b279..4123f7de52 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py @@ -78,39 +78,44 @@ def wrapper(wrapped, instance, args, kwargs): def _wrap_render(tracer, wrapped, instance, args, kwargs): """Wrap `Template.render()` or `Template.generate()` """ - template_name = instance.name or DEFAULT_TEMPLATE_NAME - attributes = {ATTRIBUTE_JINJA2_TEMPLATE_NAME: template_name} with tracer.start_as_current_span( - "jinja2.render", kind=SpanKind.INTERNAL, attributes=attributes - ): + "jinja2.render", kind=SpanKind.INTERNAL, + ) as span: + if span.is_recording(): + template_name = instance.name or DEFAULT_TEMPLATE_NAME + span.set_attribute(ATTRIBUTE_JINJA2_TEMPLATE_NAME, template_name) return wrapped(*args, **kwargs) @_with_tracer_wrapper def _wrap_compile(tracer, wrapped, _, args, kwargs): - template_name = ( - args[1] if len(args) > 1 else kwargs.get("name", DEFAULT_TEMPLATE_NAME) - ) - attributes = {ATTRIBUTE_JINJA2_TEMPLATE_NAME: template_name} with tracer.start_as_current_span( - "jinja2.compile", kind=SpanKind.INTERNAL, attributes=attributes - ): + "jinja2.compile", kind=SpanKind.INTERNAL, + ) as span: + if span.is_recording(): + template_name = ( + args[1] + if len(args) > 1 + else kwargs.get("name", DEFAULT_TEMPLATE_NAME) + ) + span.set_attribute(ATTRIBUTE_JINJA2_TEMPLATE_NAME, template_name) return wrapped(*args, **kwargs) @_with_tracer_wrapper def _wrap_load_template(tracer, wrapped, _, args, kwargs): - template_name = kwargs.get("name", args[0]) - attributes = {ATTRIBUTE_JINJA2_TEMPLATE_NAME: template_name} with tracer.start_as_current_span( - "jinja2.load", kind=SpanKind.INTERNAL, attributes=attributes + "jinja2.load", kind=SpanKind.INTERNAL, ) as span: + if span.is_recording(): + template_name = kwargs.get("name", args[0]) + span.set_attribute(ATTRIBUTE_JINJA2_TEMPLATE_NAME, template_name) template = None try: template = wrapped(*args, **kwargs) return template finally: - if template: + if template and span.is_recording(): span.set_attribute( ATTRIBUTE_JINJA2_TEMPLATE_PATH, template.filename ) diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py b/instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py index 717431abbb..5de1d598cb 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py @@ -13,6 +13,7 @@ # limitations under the License. import os +from unittest import mock import jinja2 @@ -53,6 +54,21 @@ def test_render_inline_template_with_root(self): self.assertIs(template.parent, root.get_span_context()) self.assertIsNone(root.parent) + def test_render_not_recording(self): + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + mock_tracer.use_span.return_value.__enter__ = mock_span + mock_tracer.use_span.return_value.__exit__ = mock_span + with mock.patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + jinja2.environment.Template("Hello {{name}}!") + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_render_inline_template(self): template = jinja2.environment.Template("Hello {{name}}!") self.assertEqual(template.render(name="Jinja"), "Hello Jinja!") diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py index 46b188a3df..a91d3b525a 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py @@ -118,15 +118,16 @@ def _wrap_cmd(tracer, cmd, wrapped, instance, args, kwargs): _CMD, kind=SpanKind.INTERNAL, attributes={} ) as span: try: - if not args: - vals = "" - else: - vals = _get_query_string(args[0]) + if span.is_recording(): + if not args: + vals = "" + else: + vals = _get_query_string(args[0]) - query = "{}{}{}".format(cmd, " " if vals else "", vals) - span.set_attribute(_RAWCMD, query) + query = "{}{}{}".format(cmd, " " if vals else "", vals) + span.set_attribute(_RAWCMD, query) - _set_connection_attributes(span, instance) + _set_connection_attributes(span, instance) except Exception as ex: # pylint: disable=broad-except logger.warning( "Failed to set attributes for pymemcache span %s", str(ex) diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py index 5357be6d0f..f6ae8b321d 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py @@ -226,9 +226,12 @@ def _start_span(tracer, handler, start_time) -> _TraceContext: span = tracer.start_span( _get_operation_name(handler, handler.request), kind=trace.SpanKind.SERVER, - attributes=_get_attributes_from_request(handler.request), start_time=start_time, ) + if span.is_recording(): + attributes = _get_attributes_from_request(handler.request) + for key, value in attributes.items(): + span.set_attribute(key, value) activation = tracer.use_span(span, end_on_exit=True) activation.__enter__() @@ -260,15 +263,16 @@ def _finish_span(tracer, handler, error=None): if not ctx: return - if reason: - ctx.span.set_attribute("http.status_text", reason) - ctx.span.set_attribute("http.status_code", status_code) - ctx.span.set_status( - Status( - canonical_code=http_status_to_canonical_code(status_code), - description=reason, + if ctx.span.is_recording(): + if reason: + ctx.span.set_attribute("http.status_text", reason) + ctx.span.set_attribute("http.status_code", status_code) + ctx.span.set_status( + Status( + canonical_code=http_status_to_canonical_code(status_code), + description=reason, + ) ) - ) ctx.activation.__exit__(*finish_args) context.detach(ctx.token) From f669b679b6c9036ae7075f4eca7b188ff4c09f4b Mon Sep 17 00:00:00 2001 From: Mateusz 'mat' Rumian Date: Thu, 15 Oct 2020 17:48:46 +0200 Subject: [PATCH 0611/1517] Support Environment Variables for JaegerSpanExporter configuration (#1114) --- .../CHANGELOG.md | 3 + .../opentelemetry-exporter-jaeger/README.rst | 8 ++ .../opentelemetry/exporter/jaeger/__init__.py | 84 +++++++++++-------- .../tests/test_jaeger_exporter.py | 76 ++++++++++++----- 4 files changed, 117 insertions(+), 54 deletions(-) diff --git a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md index 1545e187fc..b74ba6ea89 100644 --- a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## Unreleased +- Add support for Jaeger Span Exporter configuration by environment variables and
+ change JaegerSpanExporter constructor parameters + ([#1114](https://github.com/open-telemetry/opentelemetry-python/pull/1114)) ## Version 0.13b0 diff --git a/exporter/opentelemetry-exporter-jaeger/README.rst b/exporter/opentelemetry-exporter-jaeger/README.rst index 0069d130cd..8a9b6b0819 100644 --- a/exporter/opentelemetry-exporter-jaeger/README.rst +++ b/exporter/opentelemetry-exporter-jaeger/README.rst @@ -19,6 +19,14 @@ Installation .. _Jaeger: https://www.jaegertracing.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +Configuration +------------- + +OpenTelemetry Jaeger Exporter can be configured by setting `JaegerSpanExporter parameters +`_ or by setting +`environment variables `_ References ---------- diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index 3cfd3fca43..7c6d8bd679 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -39,10 +39,7 @@ agent_host_name='localhost', agent_port=6831, # optional: configure also collector - # collector_host_name='localhost', - # collector_port=14268, - # collector_endpoint='/api/traces?format=jaeger.thrift', - # collector_protocol='http', + # collector_endpoint='http://localhost:14268/api/traces?format=jaeger.thrift', # username=xxxx, # optional # password=xxxx, # optional ) @@ -69,7 +66,7 @@ from thrift.protocol import TBinaryProtocol, TCompactProtocol from thrift.transport import THttpClient, TTransport -import opentelemetry.trace as trace_api +from opentelemetry.configuration import Configuration from opentelemetry.exporter.jaeger.gen.agent import Agent as agent from opentelemetry.exporter.jaeger.gen.jaeger import Collector as jaeger from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult @@ -77,8 +74,6 @@ DEFAULT_AGENT_HOST_NAME = "localhost" DEFAULT_AGENT_PORT = 6831 -DEFAULT_COLLECTOR_ENDPOINT = "/api/traces?format=jaeger.thrift" -DEFAULT_COLLECTOR_PROTOCOL = "http" UDP_PACKET_MAX_LENGTH = 65000 @@ -93,11 +88,7 @@ class JaegerSpanExporter(SpanExporter): when query for spans. agent_host_name: The host name of the Jaeger-Agent. agent_port: The port of the Jaeger-Agent. - collector_host_name: The host name of the Jaeger-Collector HTTP/HTTPS - Thrift. - collector_port: The port of the Jaeger-Collector HTTP/HTTPS Thrift. collector_endpoint: The endpoint of the Jaeger-Collector HTTP/HTTPS Thrift. - collector_protocol: The transfer protocol for the Jaeger-Collector(HTTP or HTTPS). username: The user name of the Basic Auth if authentication is required. password: The password of the Basic Auth if authentication is @@ -107,25 +98,39 @@ class JaegerSpanExporter(SpanExporter): def __init__( self, service_name, - agent_host_name=DEFAULT_AGENT_HOST_NAME, - agent_port=DEFAULT_AGENT_PORT, - collector_host_name=None, - collector_port=None, - collector_endpoint=DEFAULT_COLLECTOR_ENDPOINT, - collector_protocol=DEFAULT_COLLECTOR_PROTOCOL, + agent_host_name=None, + agent_port=None, + collector_endpoint=None, username=None, password=None, ): self.service_name = service_name - self.agent_host_name = agent_host_name - self.agent_port = agent_port + self.agent_host_name = _parameter_setter( + param=agent_host_name, + env_variable=Configuration().EXPORTER_JAEGER_AGENT_HOST, + default=DEFAULT_AGENT_HOST_NAME, + ) + self.agent_port = _parameter_setter( + param=agent_port, + env_variable=Configuration().EXPORTER_JAEGER_AGENT_PORT, + default=DEFAULT_AGENT_PORT, + ) self._agent_client = None - self.collector_host_name = collector_host_name - self.collector_port = collector_port - self.collector_endpoint = collector_endpoint - self.collector_protocol = collector_protocol - self.username = username - self.password = password + self.collector_endpoint = _parameter_setter( + param=collector_endpoint, + env_variable=Configuration().EXPORTER_JAEGER_ENDPOINT, + default=None, + ) + self.username = _parameter_setter( + param=username, + env_variable=Configuration().EXPORTER_JAEGER_USER, + default=None, + ) + self.password = _parameter_setter( + param=password, + env_variable=Configuration().EXPORTER_JAEGER_PASSWORD, + default=None, + ) self._collector = None @property @@ -141,21 +146,16 @@ def collector(self): if self._collector is not None: return self._collector - if self.collector_host_name is None or self.collector_port is None: + if self.collector_endpoint is None: return None - thrift_url = "{}://{}:{}{}".format( - self.collector_protocol, - self.collector_host_name, - self.collector_port, - self.collector_endpoint, - ) - auth = None if self.username is not None and self.password is not None: auth = (self.username, self.password) - self._collector = Collector(thrift_url=thrift_url, auth=auth) + self._collector = Collector( + thrift_url=self.collector_endpoint, auth=auth + ) return self._collector def export(self, spans): @@ -177,6 +177,22 @@ def shutdown(self): pass +def _parameter_setter(param, env_variable, default): + """Returns value according to the provided data. + + Args: + param: Constructor parameter value + env_variable: Environment variable related to the parameter + default: Constructor parameter default value + """ + if param is None: + res = env_variable or default + else: + res = param + + return res + + def _nsec_to_usec_round(nsec): """Round nanoseconds to microseconds""" return (nsec + 500) // 10 ** 3 diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py index 0a01bcb234..3daeacb7fd 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py @@ -20,6 +20,7 @@ # pylint:disable=import-error import opentelemetry.exporter.jaeger as jaeger_exporter from opentelemetry import trace as trace_api +from opentelemetry.configuration import Configuration from opentelemetry.exporter.jaeger.gen.jaeger import ttypes as jaeger from opentelemetry.sdk import trace from opentelemetry.sdk.trace import Resource @@ -43,20 +44,14 @@ def setUp(self): def test_constructor_default(self): """Test the default values assigned by constructor.""" service_name = "my-service-name" - host_name = "localhost" - thrift_port = None + agent_host_name = "localhost" agent_port = 6831 - collector_endpoint = "/api/traces?format=jaeger.thrift" - collector_protocol = "http" exporter = jaeger_exporter.JaegerSpanExporter(service_name) self.assertEqual(exporter.service_name, service_name) - self.assertEqual(exporter.collector_host_name, None) - self.assertEqual(exporter.agent_host_name, host_name) + self.assertEqual(exporter.agent_host_name, agent_host_name) self.assertEqual(exporter.agent_port, agent_port) - self.assertEqual(exporter.collector_port, thrift_port) - self.assertEqual(exporter.collector_protocol, collector_protocol) - self.assertEqual(exporter.collector_endpoint, collector_endpoint) + self.assertEqual(exporter.collector_endpoint, None) self.assertEqual(exporter.username, None) self.assertEqual(exporter.password, None) self.assertTrue(exporter.collector is None) @@ -65,10 +60,7 @@ def test_constructor_default(self): def test_constructor_explicit(self): """Test the constructor passing all the options.""" service = "my-opentelemetry-jaeger" - collector_host_name = "opentelemetry.io" - collector_port = 15875 - collector_endpoint = "/myapi/traces?format=jaeger.thrift" - collector_protocol = "https" + collector_endpoint = "https://opentelemetry.io:15875" agent_port = 14268 agent_host_name = "opentelemetry.io" @@ -79,21 +71,16 @@ def test_constructor_explicit(self): exporter = jaeger_exporter.JaegerSpanExporter( service_name=service, - collector_host_name=collector_host_name, - collector_port=collector_port, - collector_endpoint=collector_endpoint, - collector_protocol="https", agent_host_name=agent_host_name, agent_port=agent_port, + collector_endpoint=collector_endpoint, username=username, password=password, ) + self.assertEqual(exporter.service_name, service) self.assertEqual(exporter.agent_host_name, agent_host_name) self.assertEqual(exporter.agent_port, agent_port) - self.assertEqual(exporter.collector_host_name, collector_host_name) - self.assertEqual(exporter.collector_port, collector_port) - self.assertEqual(exporter.collector_protocol, collector_protocol) self.assertTrue(exporter.collector is not None) self.assertEqual(exporter.collector.auth, auth) # property should not construct new object @@ -107,6 +94,55 @@ def test_constructor_explicit(self): self.assertNotEqual(exporter.collector, collector) self.assertTrue(exporter.collector.auth is None) + def test_constructor_by_environment_variables(self): + """Test the constructor using Environment Variables.""" + service = "my-opentelemetry-jaeger" + + agent_host_name = "opentelemetry.io" + agent_port = "6831" + + collector_endpoint = "https://opentelemetry.io:15875" + + username = "username" + password = "password" + auth = (username, password) + + environ_patcher = mock.patch.dict( + "os.environ", + { + "OTEL_EXPORTER_JAEGER_AGENT_HOST": agent_host_name, + "OTEL_EXPORTER_JAEGER_AGENT_PORT": agent_port, + "OTEL_EXPORTER_JAEGER_ENDPOINT": collector_endpoint, + "OTEL_EXPORTER_JAEGER_USER": username, + "OTEL_EXPORTER_JAEGER_PASSWORD": password, + }, + ) + + environ_patcher.start() + + exporter = jaeger_exporter.JaegerSpanExporter(service_name=service) + + self.assertEqual(exporter.service_name, service) + self.assertEqual(exporter.agent_host_name, agent_host_name) + self.assertEqual(exporter.agent_port, int(agent_port)) + self.assertTrue(exporter.collector is not None) + self.assertEqual(exporter.collector_endpoint, collector_endpoint) + self.assertEqual(exporter.collector.auth, auth) + # property should not construct new object + collector = exporter.collector + self.assertEqual(exporter.collector, collector) + # property should construct new object + # pylint: disable=protected-access + exporter._collector = None + exporter.username = None + exporter.password = None + self.assertNotEqual(exporter.collector, collector) + self.assertTrue(exporter.collector.auth is None) + + environ_patcher.stop() + + Configuration._reset() + def test_nsec_to_usec_round(self): # pylint: disable=protected-access nsec_to_usec_round = jaeger_exporter._nsec_to_usec_round From b54540edc83af9736f6ab43bb7a266744a77ac82 Mon Sep 17 00:00:00 2001 From: stschenk <72463443+stschenk@users.noreply.github.com> Date: Thu, 15 Oct 2020 12:34:32 -0700 Subject: [PATCH 0612/1517] Add capture of http.route to DjangoInstrumentor middleware (#1226) --- .../CHANGELOG.md | 1 + .../instrumentation/django/middleware.py | 20 +++++++++++ .../tests/test_middleware.py | 34 +++++++++++++++++++ .../tests/views.py | 4 +++ 4 files changed, 59 insertions(+) diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index 2248ea35c8..439b65b17c 100644 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -10,6 +10,7 @@ Released 2020-10-13 - Changed span name extraction from request to comply semantic convention ([#992](https://github.com/open-telemetry/opentelemetry-python/pull/992)) - Added support for `OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS` ([#1154](https://github.com/open-telemetry/opentelemetry-python/pull/1154)) +- Added capture of http.route ([#1226](https://github.com/open-telemetry/opentelemetry-python/issues/1226)) ## Version 0.13b0 diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index 11991413eb..e3cb78dbd8 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -125,6 +125,26 @@ def process_request(self, request): request.META[self._environ_span_key] = span request.META[self._environ_token] = token + # pylint: disable=unused-argument + def process_view(self, request, view_func, *args, **kwargs): + # Process view is executed before the view function, here we get the + # route template from request.resolver_match. It is not set yet in process_request + if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): + return + + if ( + self._environ_activation_key in request.META.keys() + and self._environ_span_key in request.META.keys() + ): + span = request.META[self._environ_span_key] + + if span.is_recording(): + match = getattr(request, "resolver_match") + if match: + route = getattr(match, "route") + if route: + span.set_attribute("http.route", route) + def process_exception(self, request, exception): # Django can call this method and process_response later. In order # to avoid __exit__ and detach from being called twice then, the diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 4db6c485de..6e3196e3ef 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -36,12 +36,14 @@ excluded_noarg2, route_span_name, traced, + traced_template, ) DJANGO_2_2 = VERSION >= (2, 2) urlpatterns = [ url(r"^traced/", traced), + url(r"^route/(?P[0-9]{4})/template/$", traced_template), url(r"^error/", error), url(r"^excluded_arg/", excluded), url(r"^excluded_noarg/", excluded_noarg), @@ -68,6 +70,35 @@ def tearDown(self): teardown_test_environment() _django_instrumentor.uninstrument() + def test_templated_route_get(self): + Client().get("/route/2020/template/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual( + span.name, + "^route/(?P[0-9]{4})/template/$" + if DJANGO_2_2 + else "tests.views.traced", + ) + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertEqual(span.attributes["http.method"], "GET") + self.assertEqual( + span.attributes["http.url"], + "http://testserver/route/2020/template/", + ) + self.assertEqual( + span.attributes["http.route"], + "^route/(?P[0-9]{4})/template/$", + ) + self.assertEqual(span.attributes["http.scheme"], "http") + self.assertEqual(span.attributes["http.status_code"], 200) + self.assertEqual(span.attributes["http.status_text"], "OK") + def test_traced_get(self): Client().get("/traced/") @@ -85,6 +116,7 @@ def test_traced_get(self): self.assertEqual( span.attributes["http.url"], "http://testserver/traced/" ) + self.assertEqual(span.attributes["http.route"], "^traced/") self.assertEqual(span.attributes["http.scheme"], "http") self.assertEqual(span.attributes["http.status_code"], 200) self.assertEqual(span.attributes["http.status_text"], "OK") @@ -121,6 +153,7 @@ def test_traced_post(self): self.assertEqual( span.attributes["http.url"], "http://testserver/traced/" ) + self.assertEqual(span.attributes["http.route"], "^traced/") self.assertEqual(span.attributes["http.scheme"], "http") self.assertEqual(span.attributes["http.status_code"], 200) self.assertEqual(span.attributes["http.status_text"], "OK") @@ -145,6 +178,7 @@ def test_error(self): self.assertEqual( span.attributes["http.url"], "http://testserver/error/" ) + self.assertEqual(span.attributes["http.route"], "^error/") self.assertEqual(span.attributes["http.scheme"], "http") @patch( diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py index e286841011..872222a842 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/views.py @@ -5,6 +5,10 @@ def traced(request): # pylint: disable=unused-argument return HttpResponse() +def traced_template(request, year): # pylint: disable=unused-argument + return HttpResponse() + + def error(request): # pylint: disable=unused-argument raise ValueError("error") From 6f514a30a75b00454c792b665aac48a3bc7de407 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Thu, 15 Oct 2020 12:56:24 -0700 Subject: [PATCH 0613/1517] SDK as namespace package makes it extendable (#1205) --- .../src/opentelemetry/sdk/{__init__.py => __init__.pyi} | 3 --- .../sdk/metrics/export/in_memory_metrics_exporter.py | 6 +++++- .../src/opentelemetry/sdk/trace/export/__init__.py | 3 +-- .../sdk/trace/export/in_memory_span_exporter.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename opentelemetry-sdk/src/opentelemetry/sdk/{__init__.py => __init__.pyi} (89%) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.pyi similarity index 89% rename from opentelemetry-sdk/src/opentelemetry/sdk/__init__.py rename to opentelemetry-sdk/src/opentelemetry/sdk/__init__.pyi index ca21f8b860..e57edc0f58 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.pyi @@ -16,6 +16,3 @@ The OpenTelemetry SDK package is an implementation of the OpenTelemetry API """ -from . import metrics, trace, util - -__all__ = ["metrics", "trace", "util"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py index 58ea6eba2a..e75fd350f7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py @@ -15,7 +15,11 @@ import threading from typing import Sequence -from . import MetricRecord, MetricsExporter, MetricsExportResult +from opentelemetry.sdk.metrics.export import ( + MetricRecord, + MetricsExporter, + MetricsExportResult, +) class InMemoryMetricsExporter(MetricsExporter): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 59231e60f3..3aa0117767 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -22,10 +22,9 @@ from opentelemetry.configuration import Configuration from opentelemetry.context import attach, detach, set_value +from opentelemetry.sdk.trace import Span, SpanProcessor from opentelemetry.util import time_ns -from .. import Span, SpanProcessor - logger = logging.getLogger(__name__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py index f652d56e82..967d29b3a8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py @@ -15,8 +15,8 @@ import threading import typing -from .. import Span -from . import SpanExporter, SpanExportResult +from opentelemetry.sdk.trace import Span +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult class InMemorySpanExporter(SpanExporter): From beccc3b2ea960ede97289cd39e203ac314de3572 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 15 Oct 2020 20:24:29 -0400 Subject: [PATCH 0614/1517] Adding metric collection as part of instrumentations - Django (#1230) --- docs/examples/django/manage.py | 6 +- .../CHANGELOG.md | 2 + .../instrumentation/django/__init__.py | 13 +++- .../instrumentation/django/middleware.py | 51 ++++++++++++++- .../tests/test_middleware.py | 41 +++++++++++- .../CHANGELOG.md | 2 +- .../instrumentation/requests/__init__.py | 8 ++- .../tests/test_requests_integration.py | 4 +- .../opentelemetry/instrumentation/metric.py | 63 +++++++++++++------ .../tests/test_metric.py | 61 +++++++++++++++--- 10 files changed, 210 insertions(+), 41 deletions(-) diff --git a/docs/examples/django/manage.py b/docs/examples/django/manage.py index 3a67dbf829..bc2d44886b 100755 --- a/docs/examples/django/manage.py +++ b/docs/examples/django/manage.py @@ -21,13 +21,13 @@ def main(): + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "instrumentation_example.settings" + ) # This call is what makes the Django application be instrumented DjangoInstrumentor().instrument() - os.environ.setdefault( - "DJANGO_SETTINGS_MODULE", "instrumentation_example.settings" - ) try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index 439b65b17c..36962fbdce 100644 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -11,6 +11,8 @@ Released 2020-10-13 - Changed span name extraction from request to comply semantic convention ([#992](https://github.com/open-telemetry/opentelemetry-python/pull/992)) - Added support for `OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS` ([#1154](https://github.com/open-telemetry/opentelemetry-python/pull/1154)) - Added capture of http.route ([#1226](https://github.com/open-telemetry/opentelemetry-python/issues/1226)) +- Add support for tracking http metrics + ([#1230](https://github.com/open-telemetry/opentelemetry-python/pull/1230)) ## Version 0.13b0 diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index 9ba3bbb915..26e21a1f7f 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -18,12 +18,18 @@ from opentelemetry.configuration import Configuration from opentelemetry.instrumentation.django.middleware import _DjangoMiddleware +from opentelemetry.instrumentation.django.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.metric import ( + HTTPMetricRecorder, + HTTPMetricType, + MetricMixin, +) _logger = getLogger(__name__) -class DjangoInstrumentor(BaseInstrumentor): +class DjangoInstrumentor(BaseInstrumentor, MetricMixin): """An instrumentor for Django See `BaseInstrumentor` @@ -57,6 +63,11 @@ def _instrument(self, **kwargs): settings_middleware = list(settings_middleware) settings_middleware.insert(0, self._opentelemetry_middleware) + self.init_metrics( + __name__, __version__, + ) + metric_recorder = HTTPMetricRecorder(self.meter, HTTPMetricType.SERVER) + setattr(settings, "OTEL_METRIC_RECORDER", metric_recorder) setattr(settings, "MIDDLEWARE", settings_middleware) def _uninstrument(self, **kwargs): diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index e3cb78dbd8..0503d7bbcb 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -12,8 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import time from logging import getLogger +from django.conf import settings + from opentelemetry.configuration import Configuration from opentelemetry.context import attach, detach from opentelemetry.instrumentation.django.version import __version__ @@ -41,11 +44,16 @@ MiddlewareMixin = object _logger = getLogger(__name__) +_attributes_by_preference = [ + ["http.scheme", "http.host", "http.target"], + ["http.scheme", "http.server_name", "net.host.port", "http.target"], + ["http.scheme", "net.host.name", "net.host.port", "http.target"], + ["http.url"], +] class _DjangoMiddleware(MiddlewareMixin): - """Django Middleware for OpenTelemetry - """ + """Django Middleware for OpenTelemetry""" _environ_activation_key = ( "opentelemetry-instrumentor-django.activation_key" @@ -88,6 +96,21 @@ def _get_span_name(request): except Resolver404: return "HTTP {}".format(request.method) + @staticmethod + def _get_metric_labels_from_attributes(attributes): + labels = {} + labels["http.method"] = attributes.get("http.method", "") + for attrs in _attributes_by_preference: + labels_from_attributes = { + attr: attributes.get(attr, None) for attr in attrs + } + if set(attrs).issubset(attributes.keys()): + labels.update(labels_from_attributes) + break + if attributes.get("http.flavor"): + labels["http.flavor"] = attributes.get("http.flavor") + return labels + def process_request(self, request): # request.META is a dictionary containing all available HTTP headers # Read more about request.META here: @@ -96,6 +119,9 @@ def process_request(self, request): if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return + # pylint:disable=W0212 + request._otel_start_time = time.time() + environ = request.META token = attach(extract(get_header_from_environ, environ)) @@ -110,8 +136,13 @@ def process_request(self, request): ), ) + attributes = collect_request_attributes(environ) + # pylint:disable=W0212 + request._otel_labels = self._get_metric_labels_from_attributes( + attributes + ) + if span.is_recording(): - attributes = collect_request_attributes(environ) attributes = extract_attributes_from_object( request, self._traced_request_attrs, attributes ) @@ -176,6 +207,10 @@ def process_response(self, request, response): "{} {}".format(response.status_code, response.reason_phrase), response, ) + # pylint:disable=W0212 + request._otel_labels["http.status_code"] = str( + response.status_code + ) request.META.pop(self._environ_span_key) request.META[self._environ_activation_key].__exit__( @@ -187,4 +222,14 @@ def process_response(self, request, response): detach(request.environ.get(self._environ_token)) request.META.pop(self._environ_token) + try: + metric_recorder = getattr(settings, "OTEL_METRIC_RECORDER", None) + if metric_recorder is not None: + # pylint:disable=W0212 + metric_recorder.record_server_duration_range( + request._otel_start_time, time.time(), request._otel_labels + ) + except Exception as ex: # pylint: disable=W0703 + _logger.warning("Error recording duration metrics: %s", ex) + return response diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 6e3196e3ef..5c034a23af 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -23,6 +23,8 @@ from opentelemetry.configuration import Configuration from opentelemetry.instrumentation.django import DjangoInstrumentor +from opentelemetry.sdk.util import get_dict_as_key +from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import SpanKind from opentelemetry.trace.status import StatusCanonicalCode @@ -53,7 +55,7 @@ _django_instrumentor = DjangoInstrumentor() -class TestMiddleware(WsgiTestBase): +class TestMiddleware(TestBase, WsgiTestBase): @classmethod def setUpClass(cls): super().setUpClass() @@ -121,6 +123,26 @@ def test_traced_get(self): self.assertEqual(span.attributes["http.status_code"], 200) self.assertEqual(span.attributes["http.status_text"], "OK") + self.assertIsNotNone(_django_instrumentor.meter) + self.assertEqual(len(_django_instrumentor.meter.metrics), 1) + recorder = _django_instrumentor.meter.metrics.pop() + match_key = get_dict_as_key( + { + "http.flavor": "1.1", + "http.method": "GET", + "http.status_code": "200", + "http.url": "http://testserver/traced/", + } + ) + for key in recorder.bound_instruments.keys(): + self.assertEqual(key, match_key) + # pylint: disable=protected-access + bound = recorder.bound_instruments.get(key) + for view_data in bound.view_datas: + self.assertEqual(view_data.labels, key) + self.assertEqual(view_data.aggregator.current.count, 1) + self.assertGreaterEqual(view_data.aggregator.current.sum, 0) + def test_not_recording(self): mock_tracer = Mock() mock_span = Mock() @@ -180,6 +202,23 @@ def test_error(self): ) self.assertEqual(span.attributes["http.route"], "^error/") self.assertEqual(span.attributes["http.scheme"], "http") + self.assertIsNotNone(_django_instrumentor.meter) + self.assertEqual(len(_django_instrumentor.meter.metrics), 1) + recorder = _django_instrumentor.meter.metrics.pop() + match_key = get_dict_as_key( + { + "http.flavor": "1.1", + "http.method": "GET", + "http.url": "http://testserver/error/", + } + ) + for key in recorder.bound_instruments.keys(): + self.assertEqual(key, match_key) + # pylint: disable=protected-access + bound = recorder.bound_instruments.get(key) + for view_data in bound.view_datas: + self.assertEqual(view_data.labels, key) + self.assertEqual(view_data.aggregator.current.count, 1) @patch( "opentelemetry.instrumentation.django.middleware._DjangoMiddleware._excluded_urls", diff --git a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md index c2e4dcdbb9..96089a9da1 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md @@ -10,7 +10,7 @@ Released 2020-09-17 ([#1040](https://github.com/open-telemetry/opentelemetry-python/pull/1040)) - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) -- Add support for http metrics +- Add support for tracking http metrics ([#1116](https://github.com/open-telemetry/opentelemetry-python/pull/1116)) ## Version 0.12b0 diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index 4b5f73de9e..770eacf5e2 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -45,6 +45,7 @@ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.metric import ( HTTPMetricRecorder, + HTTPMetricType, MetricMixin, ) from opentelemetry.instrumentation.requests.version import __version__ @@ -135,7 +136,7 @@ def _instrumented_requests_call( __name__, __version__, tracer_provider ).start_as_current_span(span_name, kind=SpanKind.CLIENT) as span: exception = None - with recorder.record_duration(labels): + with recorder.record_client_duration(labels): if span.is_recording(): span.set_attribute("component", "http") span.set_attribute("http.method", method) @@ -176,7 +177,6 @@ def _instrumented_requests_call( ) ) labels["http.status_code"] = str(result.status_code) - labels["http.status_text"] = result.reason if result.raw and result.raw.version: labels["http.flavor"] = ( str(result.raw.version)[:1] @@ -253,7 +253,9 @@ def _instrument(self, **kwargs): __name__, __version__, ) # pylint: disable=W0201 - self.metric_recorder = HTTPMetricRecorder(self.meter, SpanKind.CLIENT) + self.metric_recorder = HTTPMetricRecorder( + self.meter, HTTPMetricType.CLIENT + ) def _uninstrument(self, **kwargs): _uninstrument() diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py index 678499e879..f41e597b23 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py @@ -97,7 +97,6 @@ def test_basic(self): "http.flavor": "1.1", "http.method": "GET", "http.status_code": "200", - "http.status_text": "OK", "http.url": "http://httpbin.org/status/200", } ) @@ -108,7 +107,7 @@ def test_basic(self): for view_data in bound.view_datas: self.assertEqual(view_data.labels, key) self.assertEqual(view_data.aggregator.current.count, 1) - self.assertGreater(view_data.aggregator.current.sum, 0) + self.assertGreaterEqual(view_data.aggregator.current.sum, 0) def test_not_foundbasic(self): url_404 = "http://httpbin.org/status/404" @@ -318,7 +317,6 @@ def test_requests_exception_with_response(self, *_, **__): { "http.method": "GET", "http.status_code": "500", - "http.status_text": "Internal Server Error", "http.url": "http://httpbin.org/status/200", } ) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py index 74445cbff1..6aaca0bcaa 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py @@ -28,7 +28,7 @@ class HTTPMetricType(enum.Enum): CLIENT = 0 SERVER = 1 - # TODO: Add both + BOTH = 2 class MetricMixin: @@ -57,29 +57,54 @@ def __init__( ): super().__init__(meter) self._http_type = http_type - if self._meter: - self._duration = self._meter.create_metric( - name="{}.{}.duration".format( - "http", self._http_type.name.lower() - ), - description="measures the duration of the {} HTTP request".format( - "inbound" - if self._http_type is HTTPMetricType.SERVER - else "outbound" - ), - unit="ms", - value_type=float, - metric_type=ValueRecorder, - ) + self._client_duration = None + self._server_duration = None + if self._meter is not None: + if http_type in (HTTPMetricType.CLIENT, HTTPMetricType.BOTH): + self._client_duration = self._meter.create_metric( + name="{}.{}.duration".format("http", "client"), + description="measures the duration of the outbound HTTP request", + unit="ms", + value_type=float, + metric_type=ValueRecorder, + ) + if http_type is not HTTPMetricType.CLIENT: + self._server_duration = self._meter.create_metric( + name="{}.{}.duration".format("http", "server"), + description="measures the duration of the inbound HTTP request", + unit="ms", + value_type=float, + metric_type=ValueRecorder, + ) # Conventions for recording duration can be found at: # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/metrics/semantic_conventions/http-metrics.md @contextmanager - def record_duration(self, labels: Dict[str, str]): + def record_client_duration(self, labels: Dict[str, str]): start_time = time() try: yield start_time finally: - if self._meter: - elapsed_time = (time() - start_time) * 1000 - self._duration.record(elapsed_time, labels) + self.record_client_duration_range(start_time, time(), labels) + + def record_client_duration_range( + self, start_time, end_time, labels: Dict[str, str] + ): + if self._client_duration is not None: + elapsed_time = (end_time - start_time) * 1000 + self._client_duration.record(elapsed_time, labels) + + @contextmanager + def record_server_duration(self, labels: Dict[str, str]): + start_time = time() + try: + yield start_time + finally: + self.record_server_duration_range(start_time, time(), labels) + + def record_server_duration_range( + self, start_time, end_time, labels: Dict[str, str] + ): + if self._server_duration is not None: + elapsed_time = (end_time - start_time) * 1000 + self._server_duration.record(elapsed_time, labels) diff --git a/opentelemetry-instrumentation/tests/test_metric.py b/opentelemetry-instrumentation/tests/test_metric.py index ea8724fc88..14c39d85e0 100644 --- a/opentelemetry-instrumentation/tests/test_metric.py +++ b/opentelemetry-instrumentation/tests/test_metric.py @@ -61,26 +61,73 @@ def test_ctor(self): recorder = HTTPMetricRecorder(meter, HTTPMetricType.CLIENT) # pylint: disable=protected-access self.assertEqual(recorder._http_type, HTTPMetricType.CLIENT) - self.assertTrue(isinstance(recorder._duration, metrics.ValueRecorder)) - self.assertEqual(recorder._duration.name, "http.client.duration") + self.assertTrue( + isinstance(recorder._client_duration, metrics.ValueRecorder) + ) + self.assertEqual( + recorder._client_duration.name, "http.client.duration" + ) self.assertEqual( - recorder._duration.description, + recorder._client_duration.description, "measures the duration of the outbound HTTP request", ) - def test_record_duration(self): + def test_ctor_types(self): + meter = metrics_api.get_meter(__name__) + recorder = HTTPMetricRecorder(meter, HTTPMetricType.CLIENT) + self.assertEqual(recorder._http_type, HTTPMetricType.CLIENT) + self.assertTrue( + isinstance(recorder._client_duration, metrics.ValueRecorder) + ) + self.assertIsNone(recorder._server_duration) + + recorder = HTTPMetricRecorder(meter, HTTPMetricType.SERVER) + self.assertEqual(recorder._http_type, HTTPMetricType.SERVER) + self.assertTrue( + isinstance(recorder._server_duration, metrics.ValueRecorder) + ) + self.assertIsNone(recorder._client_duration) + + recorder = HTTPMetricRecorder(meter, HTTPMetricType.BOTH) + self.assertEqual(recorder._http_type, HTTPMetricType.BOTH) + self.assertTrue( + isinstance(recorder._client_duration, metrics.ValueRecorder) + ) + self.assertTrue( + isinstance(recorder._server_duration, metrics.ValueRecorder) + ) + + def test_record_client_duration(self): meter = metrics_api.get_meter(__name__) recorder = HTTPMetricRecorder(meter, HTTPMetricType.CLIENT) labels = {"test": "asd"} with mock.patch("time.time") as time_patch: time_patch.return_value = 5.0 - with recorder.record_duration(labels): + with recorder.record_client_duration(labels): + labels["test2"] = "asd2" + match_key = get_dict_as_key({"test": "asd", "test2": "asd2"}) + for key in recorder._client_duration.bound_instruments.keys(): + self.assertEqual(key, match_key) + # pylint: disable=protected-access + bound = recorder._client_duration.bound_instruments.get(key) + for view_data in bound.view_datas: + self.assertEqual(view_data.labels, key) + self.assertEqual(view_data.aggregator.current.count, 1) + self.assertGreaterEqual(view_data.aggregator.current.sum, 0) + + def test_record_server_duration(self): + meter = metrics_api.get_meter(__name__) + recorder = HTTPMetricRecorder(meter, HTTPMetricType.SERVER) + labels = {"test": "asd"} + with mock.patch("time.time") as time_patch: + time_patch.return_value = 5.0 + with recorder.record_server_duration(labels): labels["test2"] = "asd2" match_key = get_dict_as_key({"test": "asd", "test2": "asd2"}) - for key in recorder._duration.bound_instruments.keys(): + for key in recorder._server_duration.bound_instruments.keys(): self.assertEqual(key, match_key) # pylint: disable=protected-access - bound = recorder._duration.bound_instruments.get(key) + bound = recorder._server_duration.bound_instruments.get(key) for view_data in bound.view_datas: self.assertEqual(view_data.labels, key) self.assertEqual(view_data.aggregator.current.count, 1) From 5554152ff87934f60d58c96b15ad90ad81b26229 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 16 Oct 2020 01:39:41 -0400 Subject: [PATCH 0615/1517] Bug fix: Serialize event attributes properly (#1246) --- .../src/opentelemetry/sdk/trace/__init__.py | 2 ++ opentelemetry-sdk/tests/trace/test_trace.py | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 4eb6760a9a..c43ceaf33a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -463,6 +463,8 @@ def _format_context(context): def _format_attributes(attributes): if isinstance(attributes, BoundedDict): return attributes._dict # pylint: disable=protected-access + if isinstance(attributes, MappingProxyType): + return attributes.copy() return attributes @staticmethod diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 92dce44e23..571fe75515 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -22,6 +22,7 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import resources, trace from opentelemetry.sdk.trace import Resource, sampling +from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace.status import StatusCanonicalCode from opentelemetry.util import time_ns @@ -1032,3 +1033,22 @@ def test_to_json(self): span.to_json(indent=None), '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "{}"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "attributes": {}, "events": [], "links": [], "resource": {}}', ) + + def test_attributes_to_json(self): + context = trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), + ) + span = trace._Span("span-name", context) + span.resource = Resource({}) + span.set_attribute("key", "value") + span.add_event("event", {"key2": "value2"}, 123) + date_str = ns_to_iso_str(123) + self.assertEqual( + span.to_json(indent=None), + '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "{}"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "attributes": {"key": "value"}, "events": [{"name": "event", "timestamp": "' + + date_str + + '", "attributes": {"key2": "value2"}}], "links": [], "resource": {}}', + ) From 5fc08ad642dc7b9acd557536647e789b540bc2ff Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 16 Oct 2020 07:35:01 -0700 Subject: [PATCH 0616/1517] Updating limits for attributes, events, links (#1249) --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index c43ceaf33a..1d27e5c739 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -53,9 +53,9 @@ logger = logging.getLogger(__name__) -MAX_NUM_ATTRIBUTES = 32 -MAX_NUM_EVENTS = 128 -MAX_NUM_LINKS = 32 +MAX_NUM_ATTRIBUTES = 1000 +MAX_NUM_EVENTS = 1000 +MAX_NUM_LINKS = 1000 VALID_ATTR_VALUE_TYPES = (bool, str, int, float) From 2d873e580e0f66536a352e0b468429a701c13cff Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Fri, 16 Oct 2020 23:11:20 +0200 Subject: [PATCH 0617/1517] Make SpanProcessor.on_start accept parent Context (#1251) * context from Tracer.start_span is passed through to the SpanProcessor * fix linting issue in falcon test app when linting with eachdist script * fix global error handler test as it read installed extensions * reset global Configuration object state after tests were run --- .../CHANGELOG.md | 3 ++ .../exporter/datadog/spanprocessor.py | 6 ++- .../tests/test_datadog_exporter.py | 16 +++++++ .../tests/app.py | 1 + opentelemetry-sdk/CHANGELOG.md | 3 ++ .../src/opentelemetry/sdk/trace/__init__.py | 48 ++++++++++++++----- .../sdk/trace/export/__init__.py | 10 ++-- .../tests/error_handler/test_error_handler.py | 3 +- .../tests/trace/export/test_export.py | 41 ++++++++++++++++ .../tests/trace/test_span_processor.py | 13 +++-- opentelemetry-sdk/tests/trace/test_trace.py | 22 ++++++++- 11 files changed, 143 insertions(+), 23 deletions(-) diff --git a/exporter/opentelemetry-exporter-datadog/CHANGELOG.md b/exporter/opentelemetry-exporter-datadog/CHANGELOG.md index 673bb28c54..4c9b233a7b 100644 --- a/exporter/opentelemetry-exporter-datadog/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-datadog/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased + - Make `SpanProcessor.on_start` accept parent Context + ([#1251](https://github.com/open-telemetry/opentelemetry-python/pull/1251)) + ## Version 0.14b0 Released 2020-10-13 diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py index 603ea5024e..d94cf0f104 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py @@ -17,7 +17,7 @@ import threading import typing -from opentelemetry.context import attach, detach, set_value +from opentelemetry.context import Context, attach, detach, set_value from opentelemetry.sdk.trace import Span, SpanProcessor from opentelemetry.sdk.trace.export import SpanExporter from opentelemetry.trace import INVALID_TRACE_ID @@ -81,7 +81,9 @@ def __init__( self.done = False self.worker_thread.start() - def on_start(self, span: Span) -> None: + def on_start( + self, span: Span, parent_context: typing.Optional[Context] = None + ) -> None: ctx = span.get_span_context() trace_id = ctx.trace_id diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py index 98e894f94d..bd8370c103 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py @@ -21,6 +21,7 @@ from ddtrace.internal.writer import AgentWriter from opentelemetry import trace as trace_api +from opentelemetry.context import Context from opentelemetry.exporter import datadog from opentelemetry.sdk import trace from opentelemetry.sdk.trace import Resource, sampling @@ -482,6 +483,21 @@ def test_span_processor_scheduled_delay(self): tracer_provider.shutdown() + def test_span_processor_accepts_parent_context(self): + span_processor = mock.Mock( + wraps=datadog.DatadogExportSpanProcessor(self.exporter) + ) + tracer_provider = trace.TracerProvider() + tracer_provider.add_span_processor(span_processor) + tracer = tracer_provider.get_tracer(__name__) + + context = Context() + span = tracer.start_span("foo", context=context) + + span_processor.on_start.assert_called_once_with( + span, parent_context=context + ) + def test_origin(self): context = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py index 76d19ae0d1..dcbfe11b49 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py @@ -5,6 +5,7 @@ class HelloWorldResource: def _handle_request(self, _, resp): + # pylint: disable=no-member resp.status = falcon.HTTP_201 resp.body = "Hello World" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 5e8f7ebdbe..6527bf00ef 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Make `SpanProcessor.on_start` accept parent Context + ([#1251](https://github.com/open-telemetry/opentelemetry-python/pull/1251)) + ## Version 0.14b0 Released 2020-10-13 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 1d27e5c739..04d48826ef 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -68,7 +68,11 @@ class SpanProcessor: in the same order as they were registered. """ - def on_start(self, span: "Span") -> None: + def on_start( + self, + span: "Span", + parent_context: Optional[context_api.Context] = None, + ) -> None: """Called when a :class:`opentelemetry.trace.Span` is started. This method is called synchronously on the thread that starts the @@ -76,6 +80,7 @@ def on_start(self, span: "Span") -> None: Args: span: The :class:`opentelemetry.trace.Span` that just started. + parent_context: The parent context of the span that just started. """ def on_end(self, span: "Span") -> None: @@ -124,9 +129,13 @@ def add_span_processor(self, span_processor: SpanProcessor) -> None: with self._lock: self._span_processors = self._span_processors + (span_processor,) - def on_start(self, span: "Span") -> None: + def on_start( + self, + span: "Span", + parent_context: Optional[context_api.Context] = None, + ) -> None: for sp in self._span_processors: - sp.on_start(span) + sp.on_start(span, parent_context=parent_context) def on_end(self, span: "Span") -> None: for sp in self._span_processors: @@ -192,17 +201,26 @@ def add_span_processor(self, span_processor: SpanProcessor) -> None: self._span_processors = self._span_processors + (span_processor,) def _submit_and_await( - self, func: Callable[[SpanProcessor], Callable[..., None]], *args: Any + self, + func: Callable[[SpanProcessor], Callable[..., None]], + *args: Any, + **kwargs: Any ): futures = [] for sp in self._span_processors: - future = self._executor.submit(func(sp), *args) + future = self._executor.submit(func(sp), *args, **kwargs) futures.append(future) for future in futures: future.result() - def on_start(self, span: "Span") -> None: - self._submit_and_await(lambda sp: sp.on_start, span) + def on_start( + self, + span: "Span", + parent_context: Optional[context_api.Context] = None, + ) -> None: + self._submit_and_await( + lambda sp: sp.on_start, span, parent_context=parent_context + ) def on_end(self, span: "Span") -> None: self._submit_and_await(lambda sp: sp.on_end, span) @@ -584,7 +602,11 @@ def add_event( ) ) - def start(self, start_time: Optional[int] = None) -> None: + def start( + self, + start_time: Optional[int] = None, + parent_context: Optional[context_api.Context] = None, + ) -> None: with self._lock: if not self.is_recording(): return @@ -596,7 +618,7 @@ def start(self, start_time: Optional[int] = None) -> None: if has_started: logger.warning("Calling start() on a started span.") return - self.span_processor.on_start(self) + self.span_processor.on_start(self, parent_context=parent_context) def end(self, end_time: Optional[int] = None) -> None: with self._lock: @@ -764,7 +786,7 @@ def start_span( # pylint: disable=too-many-locals if sampling_result.decision.is_sampled() else trace_api.TraceFlags(trace_api.TraceFlags.DEFAULT) ) - context = trace_api.SpanContext( + span_context = trace_api.SpanContext( trace_id, self.source.ids_generator.generate_span_id(), is_remote=False, @@ -777,7 +799,7 @@ def start_span( # pylint: disable=too-many-locals # pylint:disable=protected-access span = _Span( name=name, - context=context, + context=span_context, parent=parent_span_context, sampler=self.source.sampler, resource=self.source.resource, @@ -788,9 +810,9 @@ def start_span( # pylint: disable=too-many-locals instrumentation_info=self.instrumentation_info, set_status_on_exception=set_status_on_exception, ) - span.start(start_time=start_time) + span.start(start_time=start_time, parent_context=context) else: - span = trace_api.DefaultSpan(context=context) + span = trace_api.DefaultSpan(context=span_context) return span @contextmanager diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 3aa0117767..d7a7184feb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -21,7 +21,7 @@ from enum import Enum from opentelemetry.configuration import Configuration -from opentelemetry.context import attach, detach, set_value +from opentelemetry.context import Context, attach, detach, set_value from opentelemetry.sdk.trace import Span, SpanProcessor from opentelemetry.util import time_ns @@ -70,7 +70,9 @@ class SimpleExportSpanProcessor(SpanProcessor): def __init__(self, span_exporter: SpanExporter): self.span_exporter = span_exporter - def on_start(self, span: Span) -> None: + def on_start( + self, span: Span, parent_context: typing.Optional[Context] = None + ) -> None: pass def on_end(self, span: Span) -> None: @@ -172,7 +174,9 @@ def __init__( ] * self.max_export_batch_size # type: typing.List[typing.Optional[Span]] self.worker_thread.start() - def on_start(self, span: Span) -> None: + def on_start( + self, span: Span, parent_context: typing.Optional[Context] = None + ) -> None: pass def on_end(self, span: Span) -> None: diff --git a/opentelemetry-sdk/tests/error_handler/test_error_handler.py b/opentelemetry-sdk/tests/error_handler/test_error_handler.py index 7ec572d937..1712894464 100644 --- a/opentelemetry-sdk/tests/error_handler/test_error_handler.py +++ b/opentelemetry-sdk/tests/error_handler/test_error_handler.py @@ -25,7 +25,8 @@ class TestErrorHandler(TestCase): - def test_default_error_handler(self): + @patch("opentelemetry.sdk.error_handler.iter_entry_points") + def test_default_error_handler(self, mock_iter_entry_points): with self.assertLogs(logger, ERROR): with GlobalErrorHandler(): diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 4ab3a3aa41..4ce7140ebc 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -21,6 +21,8 @@ from unittest import mock from opentelemetry import trace as trace_api +from opentelemetry.configuration import Configuration +from opentelemetry.context import Context from opentelemetry.sdk import trace from opentelemetry.sdk.trace import export @@ -100,6 +102,23 @@ def test_simple_span_processor_no_context(self): self.assertListEqual(["xxx", "bar", "foo"], spans_names_list) + def test_on_start_accepts_context(self): + # pylint: disable=no-self-use + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) + + exporter = MySpanExporter([]) + span_processor = mock.Mock( + wraps=export.SimpleExportSpanProcessor(exporter) + ) + tracer_provider.add_span_processor(span_processor) + + context = Context() + span = tracer.start_span("foo", context=context) + span_processor.on_start.assert_called_once_with( + span, parent_context=context + ) + def test_simple_span_processor_not_sampled(self): tracer_provider = trace.TracerProvider( sampler=trace.sampling.ALWAYS_OFF @@ -136,6 +155,11 @@ def _create_start_and_end_span(name, span_processor): class TestBatchExportSpanProcessor(unittest.TestCase): + def tearDown(self) -> None: + # reset global state of configuration object + # pylint: disable=protected-access + Configuration._reset() + @mock.patch.dict( "os.environ", { @@ -156,6 +180,23 @@ def test_batch_span_processor_environment_variables(self): self.assertEqual(batch_span_processor.max_export_batch_size, 3) self.assertEqual(batch_span_processor.export_timeout_millis, 4) + def test_on_start_accepts_parent_context(self): + # pylint: disable=no-self-use + my_exporter = MySpanExporter(destination=[]) + span_processor = mock.Mock( + wraps=export.BatchExportSpanProcessor(my_exporter) + ) + tracer_provider = trace.TracerProvider() + tracer_provider.add_span_processor(span_processor) + tracer = tracer_provider.get_tracer(__name__) + + context = Context() + span = tracer.start_span("foo", context=context) + + span_processor.on_start.assert_called_once_with( + span, parent_context=context + ) + def test_shutdown(self): spans_names_list = [] diff --git a/opentelemetry-sdk/tests/trace/test_span_processor.py b/opentelemetry-sdk/tests/trace/test_span_processor.py index 90b4003ca6..fff3509110 100644 --- a/opentelemetry-sdk/tests/trace/test_span_processor.py +++ b/opentelemetry-sdk/tests/trace/test_span_processor.py @@ -17,9 +17,11 @@ import typing import unittest from threading import Event +from typing import Optional from unittest import mock from opentelemetry import trace as trace_api +from opentelemetry.context import Context from opentelemetry.sdk import trace @@ -36,7 +38,9 @@ def __init__(self, name, span_list): self.name = name self.span_list = span_list - def on_start(self, span: "trace.Span") -> None: + def on_start( + self, span: "trace.Span", parent_context: Optional[Context] = None + ) -> None: self.span_list.append(span_event_start_fmt(self.name, span.name)) def on_end(self, span: "trace.Span") -> None: @@ -160,10 +164,13 @@ def test_on_start(self): multi_processor.add_span_processor(mock_processor) span = self.create_default_span() - multi_processor.on_start(span) + context = Context() + multi_processor.on_start(span, parent_context=context) for mock_processor in mocks: - mock_processor.on_start.assert_called_once_with(span) + mock_processor.on_start.assert_called_once_with( + span, parent_context=context + ) multi_processor.shutdown() def test_on_end(self): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 571fe75515..3fa7d22067 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -17,9 +17,11 @@ import subprocess import unittest from logging import ERROR, WARNING +from typing import Optional from unittest import mock from opentelemetry import trace as trace_api +from opentelemetry.context import Context from opentelemetry.sdk import resources, trace from opentelemetry.sdk.trace import Resource, sampling from opentelemetry.sdk.util import ns_to_iso_str @@ -435,6 +437,8 @@ def test_disallow_direct_span_creation(self): class TestSpan(unittest.TestCase): + # pylint: disable=too-many-public-methods + def setUp(self): self.tracer = new_tracer() @@ -734,6 +738,20 @@ def test_start_span(self): ) self.assertIs(span.status.description, "Test description") + def test_start_accepts_context(self): + # pylint: disable=no-self-use + span_processor = mock.Mock(spec=trace.SpanProcessor) + span = trace._Span( + "name", + mock.Mock(spec=trace_api.SpanContext), + span_processor=span_processor, + ) + context = Context() + span.start(parent_context=context) + span_processor.on_start.assert_called_once_with( + span, parent_context=context + ) + def test_span_override_start_and_end_time(self): """Span sending custom start_time and end_time values""" span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) @@ -899,7 +917,9 @@ def __init__(self, name, span_list): self.name = name self.span_list = span_list - def on_start(self, span: "trace.Span") -> None: + def on_start( + self, span: "trace.Span", parent_context: Optional[Context] = None + ) -> None: self.span_list.append(span_event_start_fmt(self.name, span.name)) def on_end(self, span: "trace.Span") -> None: From 937863f8294d6347e131eaaedbc1b877598b13d9 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 16 Oct 2020 17:15:43 -0700 Subject: [PATCH 0618/1517] removing unused test file (#1253) --- .../tests/correlationcontext/__init__.py | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 opentelemetry-sdk/tests/correlationcontext/__init__.py diff --git a/opentelemetry-sdk/tests/correlationcontext/__init__.py b/opentelemetry-sdk/tests/correlationcontext/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/opentelemetry-sdk/tests/correlationcontext/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. From 9642b6d47dc300da1044e56581915e0fd91bca58 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 21 Oct 2020 21:31:49 +0530 Subject: [PATCH 0619/1517] Django: Record status, http.status_code and event on exception (#1257) --- .../instrumentation/django/middleware.py | 29 +++++++++---------- .../tests/test_middleware.py | 11 ++++++- .../CHANGELOG.md | 3 ++ 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index 0503d7bbcb..41343873d0 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -60,6 +60,7 @@ class _DjangoMiddleware(MiddlewareMixin): ) _environ_token = "opentelemetry-instrumentor-django.token" _environ_span_key = "opentelemetry-instrumentor-django.span_key" + _environ_exception_key = "opentelemetry-instrumentor-django.exception_key" _excluded_urls = Configuration().DJANGO_EXCLUDED_URLS or [] if _excluded_urls: @@ -177,22 +178,11 @@ def process_view(self, request, view_func, *args, **kwargs): span.set_attribute("http.route", route) def process_exception(self, request, exception): - # Django can call this method and process_response later. In order - # to avoid __exit__ and detach from being called twice then, the - # respective keys are being removed here. if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return if self._environ_activation_key in request.META.keys(): - request.META[self._environ_activation_key].__exit__( - type(exception), - exception, - getattr(exception, "__traceback__", None), - ) - request.META.pop(self._environ_activation_key) - - detach(request.environ[self._environ_token]) - request.META.pop(self._environ_token, None) + request.META[self._environ_exception_key] = exception def process_response(self, request, response): if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): @@ -213,9 +203,17 @@ def process_response(self, request, response): ) request.META.pop(self._environ_span_key) - request.META[self._environ_activation_key].__exit__( - None, None, None - ) + exception = request.META.pop(self._environ_exception_key, None) + if exception: + request.META[self._environ_activation_key].__exit__( + type(exception), + exception, + getattr(exception, "__traceback__", None), + ) + else: + request.META[self._environ_activation_key].__exit__( + None, None, None + ) request.META.pop(self._environ_activation_key) if self._environ_token in request.META.keys(): @@ -231,5 +229,4 @@ def process_response(self, request, response): ) except Exception as ex: # pylint: disable=W0703 _logger.warning("Error recording duration metrics: %s", ex) - return response diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 5c034a23af..4aa794f0de 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -194,7 +194,7 @@ def test_error(self): ) self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual( - span.status.canonical_code, StatusCanonicalCode.UNKNOWN + span.status.canonical_code, StatusCanonicalCode.INTERNAL ) self.assertEqual(span.attributes["http.method"], "GET") self.assertEqual( @@ -202,13 +202,22 @@ def test_error(self): ) self.assertEqual(span.attributes["http.route"], "^error/") self.assertEqual(span.attributes["http.scheme"], "http") + self.assertEqual(span.attributes["http.status_code"], 500) self.assertIsNotNone(_django_instrumentor.meter) self.assertEqual(len(_django_instrumentor.meter.metrics), 1) + + self.assertEqual(len(span.events), 1) + event = span.events[0] + self.assertEqual(event.name, "exception") + self.assertEqual(event.attributes["exception.type"], "ValueError") + self.assertEqual(event.attributes["exception.message"], "error") + recorder = _django_instrumentor.meter.metrics.pop() match_key = get_dict_as_key( { "http.flavor": "1.1", "http.method": "GET", + "http.status_code": "500", "http.url": "http://testserver/error/", } ) diff --git a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md index a4641e55b6..bd6ea89cb2 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Record span status and http.status_code attribute on exception + ([#1257](https://github.com/open-telemetry/opentelemetry-python/pull/1257)) + ## Version 0.13b0 Released 2020-09-17 From bda3fbb7201978e7d158eb699d3c3a532168893e Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 21 Oct 2020 13:08:33 -0400 Subject: [PATCH 0620/1517] Use url.rule instead of request.endpoint for span name flask instrumentation (#1260) --- docs/getting_started/tests/test_flask.py | 2 +- .../opentelemetry-instrumentation-flask/CHANGELOG.md | 2 ++ .../opentelemetry/instrumentation/flask/__init__.py | 10 +++++++--- .../tests/test_programmatic.py | 4 ++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/getting_started/tests/test_flask.py b/docs/getting_started/tests/test_flask.py index 4c409edbac..fdd5cb330a 100644 --- a/docs/getting_started/tests/test_flask.py +++ b/docs/getting_started/tests/test_flask.py @@ -45,4 +45,4 @@ def test_flask(self): output = str(server.stdout.read()) self.assertIn('"name": "HTTP GET"', output) self.assertIn('"name": "example-request"', output) - self.assertIn('"name": "hello"', output) + self.assertIn('"name": "/"', output) diff --git a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md index bd6ea89cb2..c030e9e8c0 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Use `url.rule` instead of `request.endpoint` for span name + ([#1260](https://github.com/open-telemetry/opentelemetry-python/pull/1260)) - Record span status and http.status_code attribute on exception ([#1257](https://github.com/open-telemetry/opentelemetry-python/pull/1257)) diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py index 90082dd850..c88a760905 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py @@ -110,9 +110,13 @@ def _before_request(): return environ = flask.request.environ - span_name = flask.request.endpoint or otel_wsgi.get_default_span_name( - environ - ) + span_name = None + try: + span_name = flask.request.url_rule.rule + except AttributeError: + pass + if span_name is None: + span_name = otel_wsgi.get_default_span_name(environ) token = context.attach( propagators.extract(otel_wsgi.get_header_from_environ, environ) ) diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py index 46861bd681..a907890523 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py @@ -102,7 +102,7 @@ def test_simple(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "_hello_endpoint") + self.assertEqual(span_list[0].name, "/hello/") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) @@ -154,7 +154,7 @@ def test_internal_error(self): resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "_hello_endpoint") + self.assertEqual(span_list[0].name, "/hello/") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) From 77fe0b75b5d7652e380b5f9217554c25b1330c39 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 21 Oct 2020 13:18:39 -0700 Subject: [PATCH 0621/1517] fix typo in entrypoint definition (#1265) --- opentelemetry-sdk/CHANGELOG.md | 2 ++ opentelemetry-sdk/setup.cfg | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 6527bf00ef..e3c19f59a2 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -4,6 +4,8 @@ - Make `SpanProcessor.on_start` accept parent Context ([#1251](https://github.com/open-telemetry/opentelemetry-python/pull/1251)) +- Fix b3 propagator entrypoint + ([#1265](https://github.com/open-telemetry/opentelemetry-python/pull/1265)) ## Version 0.14b0 diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 7035ea83c6..0995575270 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -52,7 +52,7 @@ opentelemetry_meter_provider = opentelemetry_tracer_provider = sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider opentelemetry_propagator = - b3 = opentelemetry.sdk.trace.propagation.b3_format.B3Format + b3 = opentelemetry.sdk.trace.propagation.b3_format:B3Format [options.extras_require] test = From cdd9fd2c10861d1a84a4b8685352da587c03530c Mon Sep 17 00:00:00 2001 From: Pavan Krishna Date: Wed, 21 Oct 2020 21:30:12 -0700 Subject: [PATCH 0622/1517] naming a file http.py leads to shadowing of http module in urllib (#1271) --- docs/examples/basic_meter/{http.py => http_metrics.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/examples/basic_meter/{http.py => http_metrics.py} (100%) diff --git a/docs/examples/basic_meter/http.py b/docs/examples/basic_meter/http_metrics.py similarity index 100% rename from docs/examples/basic_meter/http.py rename to docs/examples/basic_meter/http_metrics.py From 51ed4576c611316bd3f74d213501f5ffa3e2a5ca Mon Sep 17 00:00:00 2001 From: Amos Law Date: Thu, 22 Oct 2020 02:14:01 -0400 Subject: [PATCH 0623/1517] Add coding examples for context (#1165) --- docs/examples/basic_context/README.rst | 39 +++++++++++++++++++ docs/examples/basic_context/async_context.py | 38 ++++++++++++++++++ docs/examples/basic_context/child_context.py | 29 ++++++++++++++ .../basic_context/implicit_context.py | 25 ++++++++++++ docs/faq-and-cookbook.rst | 18 +++++++++ 5 files changed, 149 insertions(+) create mode 100644 docs/examples/basic_context/README.rst create mode 100644 docs/examples/basic_context/async_context.py create mode 100644 docs/examples/basic_context/child_context.py create mode 100644 docs/examples/basic_context/implicit_context.py diff --git a/docs/examples/basic_context/README.rst b/docs/examples/basic_context/README.rst new file mode 100644 index 0000000000..361192b96f --- /dev/null +++ b/docs/examples/basic_context/README.rst @@ -0,0 +1,39 @@ +Basic Context +============= + +These examples show how context is propagated through Spans in OpenTelemetry. + +There are three different examples: + +* implicit_context: Shows how starting a span implicitly creates context. + +* child_context: Shows how context is propagated through child spans. + +* async_context: Shows how context can be shared in another coroutine. + +The source files of these examples are available :scm_web:`here `. + +Installation +------------ + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + +Run the Example +--------------- + +.. code-block:: sh + + python .py + +The output will be shown in the console. + +Useful links +------------ + +- OpenTelemetry_ +- :doc:`../../api/trace` + +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/docs/examples/basic_context/async_context.py b/docs/examples/basic_context/async_context.py new file mode 100644 index 0000000000..41e049e2a6 --- /dev/null +++ b/docs/examples/basic_context/async_context.py @@ -0,0 +1,38 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio + +from opentelemetry import baggage, trace +from opentelemetry.sdk.trace import TracerProvider + +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer(__name__) + +loop = asyncio.get_event_loop() + + +async def async_span(span): + with tracer.use_span(span): + ctx = baggage.set_baggage("foo", "bar") + return ctx + + +async def main(): + span = tracer.start_span(name="span") + ctx = await async_span(span) + print(baggage.get_all(context=ctx)) + + +loop.run_until_complete(main()) diff --git a/docs/examples/basic_context/child_context.py b/docs/examples/basic_context/child_context.py new file mode 100644 index 0000000000..d2a6d50136 --- /dev/null +++ b/docs/examples/basic_context/child_context.py @@ -0,0 +1,29 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry import baggage, trace + +tracer = trace.get_tracer(__name__) + +global_ctx = baggage.set_baggage("context", "global") +with tracer.start_as_current_span(name="root span") as root_span: + parent_ctx = baggage.set_baggage("context", "parent") + with tracer.start_as_current_span( + name="child span", context=parent_ctx + ) as child_span: + child_ctx = baggage.set_baggage("context", "child") + +print(baggage.get_baggage("context", global_ctx)) +print(baggage.get_baggage("context", parent_ctx)) +print(baggage.get_baggage("context", child_ctx)) diff --git a/docs/examples/basic_context/implicit_context.py b/docs/examples/basic_context/implicit_context.py new file mode 100644 index 0000000000..fbfdf8b55d --- /dev/null +++ b/docs/examples/basic_context/implicit_context.py @@ -0,0 +1,25 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry import baggage, trace +from opentelemetry.sdk.trace import TracerProvider + +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer(__name__) + +with tracer.start_span(name="root span") as root_span: + ctx = baggage.set_baggage("foo", "bar") + +print("Global context baggage: {}".format(baggage.get_all())) +print("Span context baggage: {}".format(baggage.get_all(context=ctx))) diff --git a/docs/faq-and-cookbook.rst b/docs/faq-and-cookbook.rst index c7f630fce7..4b70f2a92f 100644 --- a/docs/faq-and-cookbook.rst +++ b/docs/faq-and-cookbook.rst @@ -25,3 +25,21 @@ Getting and modifying a span current_span = trace.get_current_span() current_span.set_attribute("hometown", "seattle") + +Capturing baggage at different contexts +--------------------------------------- + +.. code-block:: python + + from opentelemetry import trace + + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span(name="root span") as root_span: + parent_ctx = baggage.set_baggage("context", "parent") + with tracer.start_as_current_span( + name="child span", context=parent_ctx + ) as child_span: + child_ctx = baggage.set_baggage("context", "child") + + print(baggage.get_baggage("context", parent_ctx)) + print(baggage.get_baggage("context", child_ctx)) From 97cbc9fbfcf41114347f35607e1ed6ce89080529 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 26 Oct 2020 08:38:31 -0700 Subject: [PATCH 0624/1517] docs workflow not triggered on instrumentation push (#1281) --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1860c696a0..ae87a68078 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,7 +7,7 @@ on: paths: - 'docs/**' - 'exporter/**' - - 'ext/**' + - 'instrumentation/**' - 'opentelemetry-python/opentelemetry-api/src/opentelemetry/**' - 'opentelemetry-python/opentelemetry-sdk/src/opentelemetry/sdk/**' From ddf7eebba303542a743c30af3bd86118791f4dfd Mon Sep 17 00:00:00 2001 From: LetzNico Date: Mon, 26 Oct 2020 18:59:02 +0100 Subject: [PATCH 0625/1517] Allow None in sequence attributes values (#998) (#1256) --- .../src/opentelemetry/util/types.py | 8 ++-- opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 44 ++++++++++++------- opentelemetry-sdk/tests/trace/test_trace.py | 8 ++++ 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index 4221c7f1b9..fa411efb56 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -20,10 +20,10 @@ bool, int, float, - Sequence[str], - Sequence[bool], - Sequence[int], - Sequence[float], + Sequence[Union[None, str]], + Sequence[Union[None, bool]], + Sequence[Union[None, int]], + Sequence[Union[None, float]], ] Attributes = Optional[Mapping[str, AttributeValue]] AttributesFormatter = Callable[[], Attributes] diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index e3c19f59a2..6a630d7d3d 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -6,6 +6,8 @@ ([#1251](https://github.com/open-telemetry/opentelemetry-python/pull/1251)) - Fix b3 propagator entrypoint ([#1265](https://github.com/open-telemetry/opentelemetry-python/pull/1265)) +- Allow None in sequence attributes values + ([#998](https://github.com/open-telemetry/opentelemetry-python/pull/998)) ## Version 0.14b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 04d48826ef..ed4df3b110 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -308,34 +308,44 @@ def attributes(self) -> types.Attributes: def _is_valid_attribute_value(value: types.AttributeValue) -> bool: """Checks if attribute value is valid. - An attribute value is valid if it is one of the valid types. If the value - is a sequence, it is only valid if all items in the sequence are of valid - type, not a sequence, and are of the same type. + An attribute value is valid if it is one of the valid types. + If the value is a sequence, it is only valid if all items in the sequence: + - are of the same valid type or None + - are not a sequence """ if isinstance(value, Sequence): if len(value) == 0: return True - first_element_type = type(value[0]) - - if first_element_type not in VALID_ATTR_VALUE_TYPES: - logger.warning( - "Invalid type %s in attribute value sequence. Expected one of " - "%s or a sequence of those types", - first_element_type.__name__, - [valid_type.__name__ for valid_type in VALID_ATTR_VALUE_TYPES], - ) - return False - - for element in list(value)[1:]: - if not isinstance(element, first_element_type): + sequence_first_valid_type = None + for element in value: + if element is None: + continue + element_type = type(element) + if element_type not in VALID_ATTR_VALUE_TYPES: + logger.warning( + "Invalid type %s in attribute value sequence. Expected one of " + "%s or None", + element_type.__name__, + [ + valid_type.__name__ + for valid_type in VALID_ATTR_VALUE_TYPES + ], + ) + return False + # The type of the sequence must be homogeneous. The first non-None + # element determines the type of the sequence + if sequence_first_valid_type is None: + sequence_first_valid_type = element_type + elif not isinstance(element, sequence_first_valid_type): logger.warning( "Mixed types %s and %s in attribute value sequence", - first_element_type.__name__, + sequence_first_valid_type.__name__, type(element).__name__, ) return False + elif not isinstance(value, VALID_ATTR_VALUE_TYPES): logger.warning( "Invalid type %s for attribute value. Expected one of %s or a " diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 3fa7d22067..35bdf91320 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -564,6 +564,14 @@ def test_check_attribute_helper(self): self.assertTrue(trace._is_valid_attribute_value("hi")) self.assertTrue(trace._is_valid_attribute_value(3.4)) self.assertTrue(trace._is_valid_attribute_value(15)) + # None in sequences are valid + self.assertTrue(trace._is_valid_attribute_value(["A", None, None])) + self.assertTrue( + trace._is_valid_attribute_value(["A", None, None, "B"]) + ) + self.assertTrue(trace._is_valid_attribute_value([None, None])) + self.assertFalse(trace._is_valid_attribute_value(["A", None, 1])) + self.assertFalse(trace._is_valid_attribute_value([None, "A", None, 1])) def test_sampling_attributes(self): sampling_attributes = { From 27dc6916fc5c920c2861eb5c6c4f0f938fea5886 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 26 Oct 2020 11:35:16 -0700 Subject: [PATCH 0626/1517] addressing concurrency issue in Span API (#1259) Before this change, the following would cause unintended behaviour in the Span API's add_event, update_name and set_status methods: - thread A calls set_status, locks to check if a span has ended, and releases the lock - thread B obtains the lock and ends a span - thread A continues its call of set_status This change ensures that the update operations are done with the lock being held. It's done in a decorator, but that is completely optional. --- .../src/opentelemetry/sdk/trace/__init__.py | 81 ++++++++----------- 1 file changed, 33 insertions(+), 48 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index ed4df3b110..585a93c24e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -371,6 +371,17 @@ def _create_immutable_attributes(attributes): return MappingProxyType(attributes.copy() if attributes else {}) +def _check_span_ended(func): + def wrapper(self, *args, **kwargs): + with self._lock: # pylint: disable=protected-access + if self.end_time is not None: + logger.warning("Calling %s on an ended span.", func.__name__) + return + func(self, *args, **kwargs) + + return wrapper + + class Span(trace_api.Span): """See `opentelemetry.trace.Span`. @@ -560,19 +571,18 @@ def get_span_context(self): return self.context def set_attribute(self, key: str, value: types.AttributeValue) -> None: - with self._lock: - if not self.is_recording(): - return - has_ended = self.end_time is not None - if has_ended: - logger.warning("Setting attribute on ended span.") + if not _is_valid_attribute_value(value): return if not key: logger.warning("invalid key (empty or null)") return - if _is_valid_attribute_value(value): + with self._lock: + if self.end_time is not None: + logger.warning("Setting attribute on ended span.") + return + # Freeze mutable sequences defensively if isinstance(value, MutableSequence): value = tuple(value) @@ -582,18 +592,10 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: except ValueError: logger.warning("Byte attribute could not be decoded.") return - with self._lock: - self.attributes[key] = value + self.attributes[key] = value + @_check_span_ended def _add_event(self, event: EventBase) -> None: - with self._lock: - if not self.is_recording(): - return - has_ended = self.end_time is not None - - if has_ended: - logger.warning("Calling add_event() on an ended span.") - return self.events.append(event) def add_event( @@ -618,56 +620,39 @@ def start( parent_context: Optional[context_api.Context] = None, ) -> None: with self._lock: - if not self.is_recording(): + if self.start_time is not None: + logger.warning("Calling start() on a started span.") return - has_started = self.start_time is not None - if not has_started: - self._start_time = ( - start_time if start_time is not None else time_ns() - ) - if has_started: - logger.warning("Calling start() on a started span.") - return + self._start_time = ( + start_time if start_time is not None else time_ns() + ) + self.span_processor.on_start(self, parent_context=parent_context) def end(self, end_time: Optional[int] = None) -> None: with self._lock: - if not self.is_recording(): - return if self.start_time is None: raise RuntimeError("Calling end() on a not started span.") - has_ended = self.end_time is not None - if not has_ended: - if self.status is None: - self.status = Status(canonical_code=StatusCanonicalCode.OK) + if self.end_time is not None: + logger.warning("Calling end() on an ended span.") + return - self._end_time = ( - end_time if end_time is not None else time_ns() - ) + if self.status is None: + self.status = Status(canonical_code=StatusCanonicalCode.OK) - if has_ended: - logger.warning("Calling end() on an ended span.") - return + self._end_time = end_time if end_time is not None else time_ns() self.span_processor.on_end(self) + @_check_span_ended def update_name(self, name: str) -> None: - with self._lock: - has_ended = self.end_time is not None - if has_ended: - logger.warning("Calling update_name() on an ended span.") - return self.name = name def is_recording(self) -> bool: return True + @_check_span_ended def set_status(self, status: trace_api.Status) -> None: - with self._lock: - has_ended = self.end_time is not None - if has_ended: - logger.warning("Calling set_status() on an ended span.") - return self.status = status def __exit__( From d5e51a465354d1ef3dfe450c07b46121abcd34fe Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 26 Oct 2020 17:29:21 -0400 Subject: [PATCH 0627/1517] Pass in context instead of parentcontext into samplers (#1267) Co-authored-by: alrex --- opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 3 +- .../src/opentelemetry/sdk/trace/sampling.py | 36 +++--- .../tests/trace/test_sampling.py | 109 ++++++++++++------ 4 files changed, 96 insertions(+), 54 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 6a630d7d3d..262556132e 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -8,6 +8,8 @@ ([#1265](https://github.com/open-telemetry/opentelemetry-python/pull/1265)) - Allow None in sequence attributes values ([#998](https://github.com/open-telemetry/opentelemetry-python/pull/998)) +- Samplers to accept parent Context + ([#1267](https://github.com/open-telemetry/opentelemetry-python/pull/1267)) ## Version 0.14b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 585a93c24e..4da93ae502 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -757,6 +757,7 @@ def start_span( # pylint: disable=too-many-locals "parent_span_context must be a SpanContext or None." ) + # is_valid determines root span if parent_span_context is None or not parent_span_context.is_valid: parent_span_context = None trace_id = self.source.ids_generator.generate_trace_id() @@ -773,7 +774,7 @@ def start_span( # pylint: disable=too-many-locals # The sampler may also add attributes to the newly-created span, e.g. # to include information about the sampling result. sampling_result = self.source.sampler.should_sample( - parent_span_context, trace_id, name, attributes, links, + context, trace_id, name, attributes, links, ) trace_flags = ( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 7a1e10752e..dbb12d5d63 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -66,7 +66,8 @@ from typing import Optional, Sequence # pylint: disable=unused-import -from opentelemetry.trace import Link, SpanContext +from opentelemetry.context import Context +from opentelemetry.trace import Link, get_current_span from opentelemetry.util.types import Attributes @@ -113,11 +114,11 @@ class Sampler(abc.ABC): @abc.abstractmethod def should_sample( self, - parent_span_context: Optional["SpanContext"], + parent_context: Optional["Context"], trace_id: int, name: str, attributes: Attributes = None, - links: Sequence["Link"] = (), + links: Sequence["Link"] = None, ) -> "SamplingResult": pass @@ -134,11 +135,11 @@ def __init__(self, decision: "Decision"): def should_sample( self, - parent_span_context: Optional["SpanContext"], + parent_context: Optional["Context"], trace_id: int, name: str, attributes: Attributes = None, - links: Sequence["Link"] = (), + links: Sequence["Link"] = None, ) -> "SamplingResult": if self._decision is Decision.DROP: return SamplingResult(self._decision) @@ -188,11 +189,11 @@ def bound(self) -> int: def should_sample( self, - parent_span_context: Optional["SpanContext"], + parent_context: Optional["Context"], trace_id: int, name: str, - attributes: Attributes = None, # TODO - links: Sequence["Link"] = (), + attributes: Attributes = None, + links: Sequence["Link"] = None, ) -> "SamplingResult": decision = Decision.DROP if trace_id & self.TRACE_ID_LIMIT < self.bound: @@ -220,22 +221,27 @@ def __init__(self, delegate: Sampler): def should_sample( self, - parent_span_context: Optional["SpanContext"], + parent_context: Optional["Context"], trace_id: int, name: str, - attributes: Attributes = None, # TODO - links: Sequence["Link"] = (), + attributes: Attributes = None, + links: Sequence["Link"] = None, ) -> "SamplingResult": - if parent_span_context is not None: + if parent_context is not None: + parent_span_context = get_current_span( + parent_context + ).get_span_context() + # only drop if parent exists and is not a root span if ( - not parent_span_context.is_valid - or not parent_span_context.trace_flags.sampled + parent_span_context is not None + and parent_span_context.is_valid + and not parent_span_context.trace_flags.sampled ): return SamplingResult(Decision.DROP) return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes) return self._delegate.should_sample( - parent_span_context=parent_span_context, + parent_context=parent_context, trace_id=trace_id, name=name, attributes=attributes, diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index 9160ec4a35..fad5816ca8 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -109,24 +109,34 @@ def test_always_off(self): self.assertEqual(sampled_always_on.attributes, {}) def test_default_on(self): + context = trace.set_span_in_context( + trace.DefaultSpan( + trace.SpanContext( + 0xDEADBEEF, + 0xDEADBEF0, + is_remote=False, + trace_flags=TO_DEFAULT, + ) + ) + ) no_record_default_on = sampling.DEFAULT_ON.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_DEFAULT - ), - 0xDEADBEF1, - 0xDEADBEF2, - "unsampled parent, sampling on", + context, 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling on", ) self.assertFalse(no_record_default_on.decision.is_sampled()) self.assertEqual(no_record_default_on.attributes, {}) + context = trace.set_span_in_context( + trace.DefaultSpan( + trace.SpanContext( + 0xDEADBEEF, + 0xDEADBEF0, + is_remote=False, + trace_flags=TO_SAMPLED, + ) + ) + ) sampled_default_on = sampling.DEFAULT_ON.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_SAMPLED - ), - 0xDEADBEF1, - 0xDEADBEF2, - {"sampled parent": "sampling on"}, + context, 0xDEADBEF1, 0xDEADBEF2, {"sampled parent": "sampling on"}, ) self.assertTrue(sampled_default_on.decision.is_sampled()) self.assertEqual( @@ -142,24 +152,34 @@ def test_default_on(self): ) def test_default_off(self): + context = trace.set_span_in_context( + trace.DefaultSpan( + trace.SpanContext( + 0xDEADBEEF, + 0xDEADBEF0, + is_remote=False, + trace_flags=TO_DEFAULT, + ) + ) + ) no_record_default_off = sampling.DEFAULT_OFF.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_DEFAULT - ), - 0xDEADBEF1, - 0xDEADBEF2, - "unsampled parent, sampling off", + context, 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling off", ) self.assertFalse(no_record_default_off.decision.is_sampled()) self.assertEqual(no_record_default_off.attributes, {}) + context = trace.set_span_in_context( + trace.DefaultSpan( + trace.SpanContext( + 0xDEADBEEF, + 0xDEADBEF0, + is_remote=False, + trace_flags=TO_SAMPLED, + ) + ) + ) sampled_default_off = sampling.DEFAULT_OFF.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_SAMPLED - ), - 0xDEADBEF1, - 0xDEADBEF2, - {"sampled parent": "sampling on"}, + context, 0xDEADBEF1, 0xDEADBEF2, {"sampled parent": "sampling on"}, ) self.assertTrue(sampled_default_off.decision.is_sampled()) self.assertEqual( @@ -275,32 +295,45 @@ def test_probability_sampler_limits(self): def test_parent_based(self): sampler = sampling.ParentBased(sampling.ALWAYS_ON) - # Check that the sampling decision matches the parent context if given - self.assertFalse( - sampler.should_sample( + context = trace.set_span_in_context( + trace.DefaultSpan( trace.SpanContext( 0xDEADBEF0, 0xDEADBEF1, is_remote=False, trace_flags=TO_DEFAULT, - ), - 0x7FFFFFFFFFFFFFFF, - 0xDEADBEEF, - "span name", + ) + ) + ) + # Check that the sampling decision matches the parent context if given + self.assertFalse( + sampler.should_sample( + context, 0x7FFFFFFFFFFFFFFF, 0xDEADBEEF, "span name", ).decision.is_sampled() ) - sampler2 = sampling.ParentBased(sampling.ALWAYS_OFF) - self.assertTrue( - sampler2.should_sample( + context = trace.set_span_in_context( + trace.DefaultSpan( trace.SpanContext( 0xDEADBEF0, 0xDEADBEF1, is_remote=False, trace_flags=TO_SAMPLED, - ), - 0x8000000000000000, - 0xDEADBEEF, - "span name", + ) + ) + ) + sampler2 = sampling.ParentBased(sampling.ALWAYS_OFF) + self.assertTrue( + sampler2.should_sample( + context, 0x8000000000000000, 0xDEADBEEF, "span name", + ).decision.is_sampled() + ) + + # root span always sampled for parentbased + context = trace.set_span_in_context(trace.INVALID_SPAN) + sampler3 = sampling.ParentBased(sampling.ALWAYS_OFF) + self.assertTrue( + sampler3.should_sample( + context, 0x8000000000000000, 0xDEADBEEF, "span name", ).decision.is_sampled() ) From 82356a8ee94b01003c118944dcbe9923cd35e8d7 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 26 Oct 2020 15:15:27 -0700 Subject: [PATCH 0628/1517] use python 3.8 to build docs (#1284) --- .github/workflows/docs.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ae87a68078..af52dd3665 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,8 +15,11 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions/setup-python@v1 + - uses: actions/checkout@v2 + - name: Set up Python py38 + uses: actions/setup-python@v2 + with: + python-version: py38 - name: Build docs run: | pip install --upgrade tox From 97b602fe7d772fccbead487496d084de17965b6e Mon Sep 17 00:00:00 2001 From: Patrick Yang Date: Tue, 27 Oct 2020 15:46:19 -0700 Subject: [PATCH 0629/1517] Fix BatchExportSpanProcessor not resetting timeout on worker loop (#1218) --- .../exporter/datadog/spanprocessor.py | 3 +- .../tests/test_datadog_exporter.py | 36 ++++++++++++++++++ .../sdk/trace/export/__init__.py | 3 +- .../tests/trace/export/test_export.py | 37 +++++++++++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py index d94cf0f104..3a1188e0bd 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py @@ -119,7 +119,8 @@ def worker(self): with self.condition: self.condition.wait(timeout) if not self.check_traces_queue: - # spurious notification, let's wait again + # spurious notification, let's wait again, reset timeout + timeout = self.schedule_delay_millis / 1e3 continue if self.done: # missing spans will be sent when calling flush diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py index bd8370c103..2a8b753e5f 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py @@ -483,6 +483,42 @@ def test_span_processor_scheduled_delay(self): tracer_provider.shutdown() + def test_batch_span_processor_reset_timeout(self): + """Test that the scheduled timeout is reset on cycles without spans""" + delay = 50 + # pylint: disable=protected-access + exporter = MockDatadogSpanExporter() + exporter._agent_writer.write.side_effect = lambda spans: time.sleep( + 0.05 + ) + span_processor = datadog.DatadogExportSpanProcessor( + exporter, schedule_delay_millis=delay + ) + tracer_provider = trace.TracerProvider() + tracer_provider.add_span_processor(span_processor) + tracer = tracer_provider.get_tracer(__name__) + with mock.patch.object(span_processor.condition, "wait") as mock_wait: + with tracer.start_span("foo"): + pass + + # give some time for exporter to loop + # since wait is mocked it should return immediately + time.sleep(0.1) + mock_wait_calls = list(mock_wait.mock_calls) + + # find the index of the call that processed the singular span + for idx, wait_call in enumerate(mock_wait_calls): + _, args, __ = wait_call + if args[0] <= 0: + after_calls = mock_wait_calls[idx + 1 :] + break + + self.assertTrue( + all(args[0] >= 0.05 for _, args, __ in after_calls) + ) + + span_processor.shutdown() + def test_span_processor_accepts_parent_context(self): span_processor = mock.Mock( wraps=datadog.DatadogExportSpanProcessor(self.exporter) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index d7a7184feb..53f85dcbb2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -213,7 +213,8 @@ def worker(self): self.condition.wait(timeout) flush_request = self._get_and_unset_flush_request() if not self.queue: - # spurious notification, let's wait again + # spurious notification, let's wait again, reset timeout + timeout = self.schedule_delay_millis / 1e3 self._notify_flush_request_finished(flush_request) flush_request = None continue diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 4ce7140ebc..4ad23dd863 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -381,6 +381,43 @@ def test_batch_span_processor_scheduled_delay(self): span_processor.shutdown() + def test_batch_span_processor_reset_timeout(self): + """Test that the scheduled timeout is reset on cycles without spans""" + spans_names_list = [] + + export_event = threading.Event() + my_exporter = MySpanExporter( + destination=spans_names_list, + export_event=export_event, + export_timeout_millis=50, + ) + + span_processor = export.BatchExportSpanProcessor( + my_exporter, schedule_delay_millis=50, + ) + + with mock.patch.object(span_processor.condition, "wait") as mock_wait: + _create_start_and_end_span("foo", span_processor) + self.assertTrue(export_event.wait(2)) + + # give some time for exporter to loop + # since wait is mocked it should return immediately + time.sleep(0.05) + mock_wait_calls = list(mock_wait.mock_calls) + + # find the index of the call that processed the singular span + for idx, wait_call in enumerate(mock_wait_calls): + _, args, __ = wait_call + if args[0] <= 0: + after_calls = mock_wait_calls[idx + 1 :] + break + + self.assertTrue( + all(args[0] >= 0.05 for _, args, __ in after_calls) + ) + + span_processor.shutdown() + def test_batch_span_processor_parameters(self): # zero max_queue_size self.assertRaises( From 26eef9999f5f2e4f09cd557120934cd94f469413 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 27 Oct 2020 22:33:38 -0400 Subject: [PATCH 0630/1517] Split up metric instrument constructors (#1254) --- docs/examples/basic_meter/basic_metrics.py | 8 +- .../basic_meter/calling_conventions.py | 21 +- docs/examples/basic_meter/observer.py | 4 +- docs/examples/basic_meter/view.py | 12 +- .../opencensus-exporter-metrics/collector.py | 3 +- docs/getting_started/metrics_example.py | 5 +- docs/getting_started/otlpcollector_example.py | 5 +- docs/getting_started/prometheus_example.py | 5 +- .../test_otcollector_metrics_exporter.py | 20 +- .../exporter/prometheus/__init__.py | 3 +- .../tests/test_prometheus_exporter.py | 16 +- .../instrumentation/grpc/_utilities.py | 14 +- .../system_metrics/__init__.py | 71 +--- .../src/opentelemetry/metrics/__init__.py | 377 ++++++++++++------ .../tests/metrics/test_metrics.py | 69 ++-- .../tests/test_implementation.py | 40 +- opentelemetry-instrumentation/setup.cfg | 1 - .../opentelemetry/instrumentation/metric.py | 7 +- .../src/opentelemetry/sdk/metrics/__init__.py | 96 ++++- .../tests/metrics/test_implementation.py | 6 +- .../tests/metrics/test_metrics.py | 74 +++- opentelemetry-sdk/tests/metrics/test_view.py | 17 +- 22 files changed, 526 insertions(+), 348 deletions(-) diff --git a/docs/examples/basic_meter/basic_metrics.py b/docs/examples/basic_meter/basic_metrics.py index ff050d5cde..0d57238c2d 100644 --- a/docs/examples/basic_meter/basic_metrics.py +++ b/docs/examples/basic_meter/basic_metrics.py @@ -25,7 +25,7 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, MeterProvider, ValueRecorder +from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter print( @@ -52,20 +52,18 @@ metrics.get_meter_provider().start_pipeline(meter, exporter, 5) # Metric instruments allow to capture measurements -requests_counter = meter.create_metric( +requests_counter = meter.create_counter( name="requests", description="number of requests", unit="1", value_type=int, - metric_type=Counter, ) -requests_size = meter.create_metric( +requests_size = meter.create_valuerecorder( name="requests_size", description="size of requests", unit="1", value_type=int, - metric_type=ValueRecorder, ) # Labels are used to identify key-values that are associated with a specific diff --git a/docs/examples/basic_meter/calling_conventions.py b/docs/examples/basic_meter/calling_conventions.py index 376f030e31..58aeed6c6f 100644 --- a/docs/examples/basic_meter/calling_conventions.py +++ b/docs/examples/basic_meter/calling_conventions.py @@ -19,7 +19,7 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, MeterProvider, ValueRecorder +from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter # Use the meter type provided by the SDK package @@ -27,28 +27,15 @@ meter = metrics.get_meter(__name__) metrics.get_meter_provider().start_pipeline(meter, ConsoleMetricsExporter(), 5) -requests_counter = meter.create_metric( +requests_counter = meter.create_counter( name="requests", description="number of requests", unit="1", value_type=int, - metric_type=Counter, ) -requests_size = meter.create_metric( - name="requests_size", - description="size of requests", - unit="1", - value_type=int, - metric_type=ValueRecorder, -) - -clicks_counter = meter.create_metric( - name="clicks", - description="number of clicks", - unit="1", - value_type=int, - metric_type=Counter, +clicks_counter = meter.create_counter( + name="clicks", description="number of clicks", unit="1", value_type=int, ) labels = {"environment": "staging"} diff --git a/docs/examples/basic_meter/observer.py b/docs/examples/basic_meter/observer.py index a53f37d1ce..7481126ffc 100644 --- a/docs/examples/basic_meter/observer.py +++ b/docs/examples/basic_meter/observer.py @@ -34,7 +34,7 @@ def get_cpu_usage_callback(observer): observer.observe(percent, labels) -meter.register_observer( +meter.register_valueobserver( callback=get_cpu_usage_callback, name="cpu_percent", description="per-cpu usage", @@ -50,7 +50,7 @@ def get_ram_usage_callback(observer): observer.observe(ram_percent, {}) -meter.register_observer( +meter.register_valueobserver( callback=get_ram_usage_callback, name="ram_percent", description="RAM memory usage", diff --git a/docs/examples/basic_meter/view.py b/docs/examples/basic_meter/view.py index cc4ba1a683..81850b1452 100644 --- a/docs/examples/basic_meter/view.py +++ b/docs/examples/basic_meter/view.py @@ -17,11 +17,7 @@ It shows the usage of the direct, bound and batch calling conventions. """ from opentelemetry import metrics -from opentelemetry.sdk.metrics import ( - MeterProvider, - UpDownCounter, - ValueRecorder, -) +from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.aggregate import ( HistogramAggregator, @@ -36,20 +32,18 @@ meter = metrics.get_meter(__name__) metrics.get_meter_provider().start_pipeline(meter, ConsoleMetricsExporter(), 5) -requests_counter = meter.create_metric( +requests_counter = meter.create_counter( name="requests", description="number of requests", unit="1", value_type=int, - metric_type=UpDownCounter, ) -requests_size = meter.create_metric( +requests_size = meter.create_valuerecorder( name="requests_size", description="size of requests", unit="1", value_type=int, - metric_type=ValueRecorder, ) # Views are used to define an aggregation type and label keys to aggregate by diff --git a/docs/examples/opencensus-exporter-metrics/collector.py b/docs/examples/opencensus-exporter-metrics/collector.py index dc39f0cf4c..cf162631b6 100644 --- a/docs/examples/opencensus-exporter-metrics/collector.py +++ b/docs/examples/opencensus-exporter-metrics/collector.py @@ -30,12 +30,11 @@ meter = metrics.get_meter(__name__) metrics.get_meter_provider().start_pipeline(meter, exporter, 5) -requests_counter = meter.create_metric( +requests_counter = meter.create_counter( name="requests", description="number of requests", unit="1", value_type=int, - metric_type=Counter, label_keys=("environment",), ) diff --git a/docs/getting_started/metrics_example.py b/docs/getting_started/metrics_example.py index e9e9b1cf7d..08751e9864 100644 --- a/docs/getting_started/metrics_example.py +++ b/docs/getting_started/metrics_example.py @@ -16,7 +16,7 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController @@ -27,12 +27,11 @@ staging_labels = {"environment": "staging"} -requests_counter = meter.create_metric( +requests_counter = meter.create_counter( name="requests", description="number of requests", unit="1", value_type=int, - metric_type=Counter, label_keys=("environment",), ) diff --git a/docs/getting_started/otlpcollector_example.py b/docs/getting_started/otlpcollector_example.py index 15254c7cc5..fa2407c158 100644 --- a/docs/getting_started/otlpcollector_example.py +++ b/docs/getting_started/otlpcollector_example.py @@ -18,7 +18,7 @@ from opentelemetry import metrics, trace from opentelemetry.exporter.otlp.metrics_exporter import OTLPMetricsExporter from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter -from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export.controller import PushController from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor @@ -54,12 +54,11 @@ with tracer.start_as_current_span("foo"): print("Hello world!") -requests_counter = meter.create_metric( +requests_counter = meter.create_counter( name="requests", description="number of requests", unit="1", value_type=int, - metric_type=Counter, label_keys=("environment",), ) # Labels are used to identify key-values that are associated with a specific diff --git a/docs/getting_started/prometheus_example.py b/docs/getting_started/prometheus_example.py index d7111df969..8a76984f68 100644 --- a/docs/getting_started/prometheus_example.py +++ b/docs/getting_started/prometheus_example.py @@ -20,7 +20,7 @@ from opentelemetry import metrics from opentelemetry.exporter.prometheus import PrometheusMetricsExporter -from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController @@ -35,12 +35,11 @@ staging_labels = {"environment": "staging"} -requests_counter = meter.create_metric( +requests_counter = meter.create_counter( name="requests", description="number of requests", unit="1", value_type=int, - metric_type=Counter, label_keys=("environment",), ) diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py index 4dd34bf1ba..441c75b682 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py @@ -90,14 +90,14 @@ def test_get_collector_metric_type(self): def test_get_collector_point(self): aggregator = aggregate.SumAggregator() - int_counter = self._meter.create_metric( - "testName", "testDescription", "unit", int, Counter + int_counter = self._meter.create_counter( + "testName", "testDescription", "unit", int, ) - float_counter = self._meter.create_metric( - "testName", "testDescription", "unit", float, Counter + float_counter = self._meter.create_counter( + "testName", "testDescription", "unit", float, ) - valuerecorder = self._meter.create_metric( - "testName", "testDescription", "unit", float, ValueRecorder + valuerecorder = self._meter.create_valuerecorder( + "testName", "testDescription", "unit", float, ) result = metrics_exporter.get_collector_point( MetricRecord( @@ -141,8 +141,8 @@ def test_export(self): collector_exporter = metrics_exporter.OpenCensusMetricsExporter( client=mock_client, host_name=host_name ) - test_metric = self._meter.create_metric( - "testname", "testdesc", "unit", int, Counter, self._labels.keys(), + test_metric = self._meter.create_counter( + "testname", "testdesc", "unit", int, self._labels.keys(), ) record = MetricRecord( test_metric, @@ -167,8 +167,8 @@ def test_export(self): ) def test_translate_to_collector(self): - test_metric = self._meter.create_metric( - "testname", "testdesc", "unit", int, Counter, self._labels.keys() + test_metric = self._meter.create_counter( + "testname", "testdesc", "unit", int, self._labels.keys() ) aggregator = aggregate.SumAggregator() aggregator.update(123) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 3ab1c536f9..8ca89a94d2 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -43,12 +43,11 @@ # Starts the collect/export pipeline for metrics metrics.get_meter_provider().start_pipeline(meter, exporter, 5) - counter = meter.create_metric( + counter = meter.create_counter( "requests", "number of requests", "requests", int, - Counter, ("environment",), ) diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 4e2075b8b6..c7c0c5b071 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -36,8 +36,8 @@ class TestPrometheusMetricExporter(unittest.TestCase): def setUp(self): set_meter_provider(metrics.MeterProvider()) self._meter = get_meter_provider().get_meter(__name__) - self._test_metric = self._meter.create_metric( - "testname", "testdesc", "unit", int, metrics.Counter, + self._test_metric = self._meter.create_counter( + "testname", "testdesc", "unit", int, ) labels = {"environment": "staging"} self._labels_key = get_dict_as_key(labels) @@ -80,8 +80,8 @@ def test_export(self): def test_min_max_sum_aggregator_to_prometheus(self): meter = get_meter_provider().get_meter(__name__) - metric = meter.create_metric( - "test@name", "testdesc", "unit", int, metrics.ValueRecorder, [] + metric = meter.create_valuerecorder( + "test@name", "testdesc", "unit", int, [] ) labels = {} key_labels = get_dict_as_key(labels) @@ -101,9 +101,7 @@ def test_min_max_sum_aggregator_to_prometheus(self): def test_counter_to_prometheus(self): meter = get_meter_provider().get_meter(__name__) - metric = meter.create_metric( - "test@name", "testdesc", "unit", int, metrics.Counter, - ) + metric = meter.create_counter("test@name", "testdesc", "unit", int,) labels = {"environment@": "staging", "os": "Windows"} key_labels = get_dict_as_key(labels) aggregator = SumAggregator() @@ -134,9 +132,7 @@ def test_counter_to_prometheus(self): def test_invalid_metric(self): meter = get_meter_provider().get_meter(__name__) - metric = meter.create_metric( - "tesname", "testdesc", "unit", int, StubMetric - ) + metric = StubMetric("tesname", "testdesc", "unit", int, meter) labels = {"environment": "staging"} key_labels = get_dict_as_key(labels) record = MetricRecord( diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py index a577635582..8cf1f957c3 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py @@ -19,8 +19,6 @@ import grpc -from opentelemetry.sdk.metrics import Counter, ValueRecorder - class RpcInfo: def __init__( @@ -47,33 +45,29 @@ def __init__(self, meter, span_kind): self._span_kind = span_kind if self._meter: - self._duration = self._meter.create_metric( + self._duration = self._meter.create_valuerecorder( name="{}/{}/duration".format(service_name, span_kind), description="Duration of grpc requests to the server", unit="ms", value_type=float, - metric_type=ValueRecorder, ) - self._error_count = self._meter.create_metric( + self._error_count = self._meter.create_counter( name="{}/{}/errors".format(service_name, span_kind), description="Number of errors that were returned from the server", unit="1", value_type=int, - metric_type=Counter, ) - self._bytes_in = self._meter.create_metric( + self._bytes_in = self._meter.create_counter( name="{}/{}/bytes_in".format(service_name, span_kind), description="Number of bytes received from the server", unit="by", value_type=int, - metric_type=Counter, ) - self._bytes_out = self._meter.create_metric( + self._bytes_out = self._meter.create_counter( name="{}/{}/bytes_out".format(service_name, span_kind), description="Number of bytes sent out through gRPC", unit="by", value_type=int, - metric_type=Counter, ) def record_bytes_in(self, bytes_in, method): diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py index 71935aa8d9..2b453dbd7d 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py @@ -77,11 +77,6 @@ import psutil from opentelemetry import metrics -from opentelemetry.sdk.metrics import ( - SumObserver, - UpDownSumObserver, - ValueObserver, -) from opentelemetry.sdk.metrics.export import MetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController from opentelemetry.sdk.util import get_dict_as_key @@ -160,178 +155,159 @@ def __init__( self._runtime_cpu_time_labels = self._labels.copy() self._runtime_gc_count_labels = self._labels.copy() - self.meter.register_observer( + self.meter.register_sumobserver( callback=self._get_system_cpu_time, name="system.cpu.time", description="System CPU time", unit="seconds", value_type=float, - observer_type=SumObserver, ) - self.meter.register_observer( + self.meter.register_valueobserver( callback=self._get_system_cpu_utilization, name="system.cpu.utilization", description="System CPU utilization", unit="1", value_type=float, - observer_type=ValueObserver, ) - self.meter.register_observer( + self.meter.register_valueobserver( callback=self._get_system_memory_usage, name="system.memory.usage", description="System memory usage", unit="bytes", value_type=int, - observer_type=ValueObserver, ) - self.meter.register_observer( + self.meter.register_valueobserver( callback=self._get_system_memory_utilization, name="system.memory.utilization", description="System memory utilization", unit="1", value_type=float, - observer_type=ValueObserver, ) - self.meter.register_observer( + self.meter.register_valueobserver( callback=self._get_system_swap_usage, name="system.swap.usage", description="System swap usage", unit="pages", value_type=int, - observer_type=ValueObserver, ) - self.meter.register_observer( + self.meter.register_valueobserver( callback=self._get_system_swap_utilization, name="system.swap.utilization", description="System swap utilization", unit="1", value_type=float, - observer_type=ValueObserver, ) - # self.meter.register_observer( + # self.meter.register_sumobserver( # callback=self._get_system_swap_page_faults, # name="system.swap.page_faults", # description="System swap page faults", # unit="faults", # value_type=int, - # observer_type=SumObserver, # ) - # self.meter.register_observer( + # self.meter.register_sumobserver( # callback=self._get_system_swap_page_operations, # name="system.swap.page_operations", # description="System swap page operations", # unit="operations", # value_type=int, - # observer_type=SumObserver, # ) - self.meter.register_observer( + self.meter.register_sumobserver( callback=self._get_system_disk_io, name="system.disk.io", description="System disk IO", unit="bytes", value_type=int, - observer_type=SumObserver, ) - self.meter.register_observer( + self.meter.register_sumobserver( callback=self._get_system_disk_operations, name="system.disk.operations", description="System disk operations", unit="operations", value_type=int, - observer_type=SumObserver, ) - self.meter.register_observer( + self.meter.register_sumobserver( callback=self._get_system_disk_time, name="system.disk.time", description="System disk time", unit="seconds", value_type=float, - observer_type=SumObserver, ) - self.meter.register_observer( + self.meter.register_sumobserver( callback=self._get_system_disk_merged, name="system.disk.merged", description="System disk merged", unit="1", value_type=int, - observer_type=SumObserver, ) - # self.meter.register_observer( + # self.meter.register_valueobserver( # callback=self._get_system_filesystem_usage, # name="system.filesystem.usage", # description="System filesystem usage", # unit="bytes", # value_type=int, - # observer_type=ValueObserver, # ) - # self.meter.register_observer( + # self.meter.register_valueobserver( # callback=self._get_system_filesystem_utilization, # name="system.filesystem.utilization", # description="System filesystem utilization", # unit="1", # value_type=float, - # observer_type=ValueObserver, # ) - self.meter.register_observer( + self.meter.register_sumobserver( callback=self._get_system_network_dropped_packets, name="system.network.dropped_packets", description="System network dropped_packets", unit="packets", value_type=int, - observer_type=SumObserver, ) - self.meter.register_observer( + self.meter.register_sumobserver( callback=self._get_system_network_packets, name="system.network.packets", description="System network packets", unit="packets", value_type=int, - observer_type=SumObserver, ) - self.meter.register_observer( + self.meter.register_sumobserver( callback=self._get_system_network_errors, name="system.network.errors", description="System network errors", unit="errors", value_type=int, - observer_type=SumObserver, ) - self.meter.register_observer( + self.meter.register_sumobserver( callback=self._get_system_network_io, name="system.network.io", description="System network io", unit="bytes", value_type=int, - observer_type=SumObserver, ) - self.meter.register_observer( + self.meter.register_updownsumobserver( callback=self._get_system_network_connections, name="system.network.connections", description="System network connections", unit="connections", value_type=int, - observer_type=UpDownSumObserver, ) - self.meter.register_observer( + self.meter.register_sumobserver( callback=self._get_runtime_memory, name="runtime.{}.memory".format(self._python_implementation), description="Runtime {} memory".format( @@ -339,10 +315,9 @@ def __init__( ), unit="bytes", value_type=int, - observer_type=SumObserver, ) - self.meter.register_observer( + self.meter.register_sumobserver( callback=self._get_runtime_cpu_time, name="runtime.{}.cpu_time".format(self._python_implementation), description="Runtime {} CPU time".format( @@ -350,10 +325,9 @@ def __init__( ), unit="seconds", value_type=float, - observer_type=SumObserver, ) - self.meter.register_observer( + self.meter.register_sumobserver( callback=self._get_runtime_gc_count, name="runtime.{}.gc_count".format(self._python_implementation), description="Runtime {} GC count".format( @@ -361,7 +335,6 @@ def __init__( ), unit="bytes", value_type=int, - observer_type=SumObserver, ) def _get_system_cpu_time(self, observer: metrics.ValueObserver) -> None: diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 7d279731ea..227a02bc35 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -29,7 +29,16 @@ """ import abc from logging import getLogger -from typing import Callable, Dict, Optional, Sequence, Tuple, Type, TypeVar +from typing import ( + Callable, + Dict, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, +) from opentelemetry.util import _load_meter_provider @@ -37,50 +46,49 @@ ValueT = TypeVar("ValueT", int, float) -class DefaultBoundInstrument: - """The default bound metric instrument. - - Used when no bound instrument implementation is available. - """ - +class BoundCounter(abc.ABC): + @abc.abstractmethod def add(self, value: ValueT) -> None: - """No-op implementation of `BoundCounter` add. + """Increases the value of the bound counter by ``value``. Args: - value: The value to add to the bound metric instrument. + value: The value to add to the bound counter. Must be positive. """ - def record(self, value: ValueT) -> None: - """No-op implementation of `BoundValueRecorder` record. - Args: - value: The value to record to the bound metric instrument. - """ +class DefaultBoundCounter(BoundCounter): + """The default bound counter instrument. - def release(self) -> None: - """No-op implementation of release.""" + Used when no bound counter implementation is available. + """ + + def add(self, value: ValueT) -> None: + pass -class BoundCounter: +class BoundUpDownCounter(abc.ABC): + @abc.abstractmethod def add(self, value: ValueT) -> None: - """Increases the value of the bound counter by ``value``. + """Increases the value of the bound updowncounter by ``value``. Args: - value: The value to add to the bound counter. Must be positive. + value: The value to add to the bound updowncounter. Can be positive + or negative. """ -class BoundUpDownCounter: - def add(self, value: ValueT) -> None: - """Increases the value of the bound counter by ``value``. +class DefaultBoundUpDownCounter(BoundUpDownCounter): + """The default bound updowncounter instrument. - Args: - value: The value to add to the bound counter. Can be positive or - negative. - """ + Used when no bound updowncounter implementation is available. + """ + def add(self, value: ValueT) -> None: + pass -class BoundValueRecorder: + +class BoundValueRecorder(abc.ABC): + @abc.abstractmethod def record(self, value: ValueT) -> None: """Records the given ``value`` to this bound valuerecorder. @@ -89,6 +97,16 @@ def record(self, value: ValueT) -> None: """ +class DefaultBoundValueRecorder(BoundValueRecorder): + """The default bound valuerecorder instrument. + + Used when no bound valuerecorder implementation is available. + """ + + def record(self, value: ValueT) -> None: + pass + + class Metric(abc.ABC): """Base class for various types of metrics. @@ -108,41 +126,10 @@ def bind(self, labels: Dict[str, str]) -> "object": """ -class DefaultMetric(Metric): - """The default Metric used when no Metric implementation is available.""" - - def bind(self, labels: Dict[str, str]) -> "DefaultBoundInstrument": - """Gets a `DefaultBoundInstrument`. - - Args: - labels: Labels to associate with the bound instrument. - """ - return DefaultBoundInstrument() - - def add(self, value: ValueT, labels: Dict[str, str]) -> None: - """No-op implementation of `Counter` add. - - Args: - value: The value to add to the counter metric. - labels: Labels to associate with the bound instrument. - """ - - def record(self, value: ValueT, labels: Dict[str, str]) -> None: - """No-op implementation of `ValueRecorder` record. - - Args: - value: The value to record to this valuerecorder metric. - labels: Labels to associate with the bound instrument. - """ - - class Counter(Metric): """A counter type metric that expresses the computation of a sum.""" - def bind(self, labels: Dict[str, str]) -> "BoundCounter": - """Gets a `BoundCounter`.""" - return BoundCounter() - + @abc.abstractmethod def add(self, value: ValueT, labels: Dict[str, str]) -> None: """Increases the value of the counter by ``value``. @@ -154,14 +141,24 @@ def add(self, value: ValueT, labels: Dict[str, str]) -> None: """ +class DefaultCounter(Counter): + """The default counter instrument. + + Used when no `Counter` implementation is available. + """ + + def bind(self, labels: Dict[str, str]) -> "DefaultBoundCounter": + return DefaultBoundCounter() + + def add(self, value: ValueT, labels: Dict[str, str]) -> None: + pass + + class UpDownCounter(Metric): """A counter type metric that expresses the computation of a sum, allowing negative increments.""" - def bind(self, labels: Dict[str, str]) -> "BoundUpDownCounter": - """Gets a `BoundUpDownCounter`.""" - return BoundUpDownCounter() - + @abc.abstractmethod def add(self, value: ValueT, labels: Dict[str, str]) -> None: """Increases the value of the counter by ``value``. @@ -173,13 +170,23 @@ def add(self, value: ValueT, labels: Dict[str, str]) -> None: """ +class DefaultUpDownCounter(UpDownCounter): + """The default updowncounter instrument. + + Used when no `UpDownCounter` implementation is available. + """ + + def bind(self, labels: Dict[str, str]) -> "DefaultBoundUpDownCounter": + return DefaultBoundUpDownCounter() + + def add(self, value: ValueT, labels: Dict[str, str]) -> None: + pass + + class ValueRecorder(Metric): """A valuerecorder type metric that represent raw stats.""" - def bind(self, labels: Dict[str, str]) -> "BoundValueRecorder": - """Gets a `BoundValueRecorder`.""" - return BoundValueRecorder() - + @abc.abstractmethod def record(self, value: ValueT, labels: Dict[str, str]) -> None: """Records the ``value`` to the valuerecorder. @@ -189,6 +196,19 @@ def record(self, value: ValueT, labels: Dict[str, str]) -> None: """ +class DefaultValueRecorder(ValueRecorder): + """The default valuerecorder instrument. + + Used when no `ValueRecorder` implementation is available. + """ + + def bind(self, labels: Dict[str, str]) -> "DefaultBoundValueRecorder": + return DefaultBoundValueRecorder() + + def record(self, value: ValueT, labels: Dict[str, str]) -> None: + pass + + class Observer(abc.ABC): """An observer type metric instrument used to capture a current set of values. @@ -208,52 +228,40 @@ def observe(self, value: ValueT, labels: Dict[str, str]) -> None: """ -class DefaultObserver(Observer): - """No-op implementation of ``Observer``.""" - - def observe(self, value: ValueT, labels: Dict[str, str]) -> None: - """Captures ``value`` to the observer. - - Args: - value: The value to capture to this observer metric. - labels: Labels associated to ``value``. - """ +# pylint: disable=W0223 +class SumObserver(Observer): + """Asynchronous instrument used to capture a monotonic sum.""" -class SumObserver(Observer): +class DefaultSumObserver(SumObserver): """No-op implementation of ``SumObserver``.""" def observe(self, value: ValueT, labels: Dict[str, str]) -> None: - """Captures ``value`` to the sumobserver. - - Args: - value: The value to capture to this sumobserver metric. - labels: Labels associated to ``value``. - """ + pass +# pylint: disable=W0223 class UpDownSumObserver(Observer): + """Asynchronous instrument used to capture a non-monotonic count.""" + + +class DefaultUpDownSumObserver(UpDownSumObserver): """No-op implementation of ``UpDownSumObserver``.""" def observe(self, value: ValueT, labels: Dict[str, str]) -> None: - """Captures ``value`` to the updownsumobserver. - - Args: - value: The value to capture to this updownsumobserver metric. - labels: Labels associated to ``value``. - """ + pass +# pylint: disable=W0223 class ValueObserver(Observer): + """Asynchronous instrument used to capture grouping measurements.""" + + +class DefaultValueObserver(ValueObserver): """No-op implementation of ``ValueObserver``.""" def observe(self, value: ValueT, labels: Dict[str, str]) -> None: - """Captures ``value`` to the valueobserver. - - Args: - value: The value to capture to this valueobserver metric. - labels: Labels associated to ``value``. - """ + pass class MeterProvider(abc.ABC): @@ -298,11 +306,7 @@ def get_meter( return DefaultMeter() -MetricT = TypeVar("MetricT", Counter, ValueRecorder) -InstrumentT = TypeVar( - "InstrumentT", Counter, UpDownCounter, Observer, ValueRecorder -) -ObserverT = TypeVar("ObserverT", bound=Observer) +InstrumentT = TypeVar("InstrumentT", bound=Union[Metric, Observer]) ObserverCallbackT = Callable[[Observer], None] @@ -335,16 +339,15 @@ def record_batch( """ @abc.abstractmethod - def create_metric( + def create_counter( self, name: str, description: str, unit: str, value_type: Type[ValueT], - metric_type: Type[MetricT], enabled: bool = True, - ) -> "Metric": - """Creates a ``metric_kind`` metric with type ``value_type``. + ) -> "Counter": + """Creates a `Counter` metric with type ``value_type``. Args: name: The name of the metric. @@ -352,24 +355,87 @@ def create_metric( unit: Unit of the metric values following the UCUM convention (https://unitsofmeasure.org/ucum.html). value_type: The type of values being recorded by the metric. - metric_type: The type of metric being created. enabled: Whether to report the metric by default. - Returns: A new ``metric_type`` metric with values of ``value_type``. """ @abc.abstractmethod - def register_observer( + def create_updowncounter( + self, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + enabled: bool = True, + ) -> "UpDownCounter": + """Creates a `UpDownCounter` metric with type ``value_type``. + + Args: + name: The name of the metric. + description: Human-readable description of the metric. + unit: Unit of the metric values following the UCUM convention + (https://unitsofmeasure.org/ucum.html). + value_type: The type of values being recorded by the metric. + enabled: Whether to report the metric by default. + """ + + @abc.abstractmethod + def create_valuerecorder( + self, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + enabled: bool = True, + ) -> "ValueRecorder": + """Creates a `ValueRecorder` metric with type ``value_type``. + + Args: + name: The name of the metric. + description: Human-readable description of the metric. + unit: Unit of the metric values following the UCUM convention + (https://unitsofmeasure.org/ucum.html). + value_type: The type of values being recorded by the metric. + enabled: Whether to report the metric by default. + """ + + @abc.abstractmethod + def register_sumobserver( + self, + callback: ObserverCallbackT, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + label_keys: Sequence[str] = (), + enabled: bool = True, + ) -> "SumObserver": + """Registers an ``SumObserver`` metric instrument. + + Args: + callback: Callback invoked each collection interval with the + observer as argument. + name: The name of the metric. + description: Human-readable description of the metric. + unit: Unit of the metric values following the UCUM convention + (https://unitsofmeasure.org/ucum.html). + value_type: The type of values being recorded by the metric. + label_keys: The keys for the labels with dynamic values. + enabled: Whether to report the metric by default. + Returns: A new ``SumObserver`` metric instrument. + """ + + @abc.abstractmethod + def register_updownsumobserver( self, callback: ObserverCallbackT, name: str, description: str, unit: str, value_type: Type[ValueT], - observer_type: Type[ObserverT], label_keys: Sequence[str] = (), enabled: bool = True, - ) -> "Observer": - """Registers an ``Observer`` metric instrument. + ) -> "UpDownSumObserver": + """Registers an ``UpDownSumObserver`` metric instrument. Args: callback: Callback invoked each collection interval with the @@ -379,10 +445,35 @@ def register_observer( unit: Unit of the metric values following the UCUM convention (https://unitsofmeasure.org/ucum.html). value_type: The type of values being recorded by the metric. - observer_type: The type of observer being registered. label_keys: The keys for the labels with dynamic values. enabled: Whether to report the metric by default. - Returns: A new ``Observer`` metric instrument. + Returns: A new ``UpDownSumObserver`` metric instrument. + """ + + @abc.abstractmethod + def register_valueobserver( + self, + callback: ObserverCallbackT, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + label_keys: Sequence[str] = (), + enabled: bool = True, + ) -> "ValueObserver": + """Registers an ``ValueObserver`` metric instrument. + + Args: + callback: Callback invoked each collection interval with the + observer as argument. + name: The name of the metric. + description: Human-readable description of the metric. + unit: Unit of the metric values following the UCUM convention + (https://unitsofmeasure.org/ucum.html). + value_type: The type of values being recorded by the metric. + label_keys: The keys for the labels with dynamic values. + enabled: Whether to report the metric by default. + Returns: A new ``ValueObserver`` metric instrument. """ @abc.abstractmethod @@ -404,30 +495,74 @@ def record_batch( ) -> None: pass - def create_metric( + def create_counter( + self, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + enabled: bool = True, + ) -> "Counter": + # pylint: disable=no-self-use + return DefaultCounter() + + def create_updowncounter( + self, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + enabled: bool = True, + ) -> "UpDownCounter": + # pylint: disable=no-self-use + return DefaultUpDownCounter() + + def create_valuerecorder( self, name: str, description: str, unit: str, value_type: Type[ValueT], - metric_type: Type[MetricT], enabled: bool = True, - ) -> "Metric": + ) -> "ValueRecorder": # pylint: disable=no-self-use - return DefaultMetric() + return DefaultValueRecorder() + + def register_sumobserver( + self, + callback: ObserverCallbackT, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + label_keys: Sequence[str] = (), + enabled: bool = True, + ) -> "DefaultSumObserver": + return DefaultSumObserver() + + def register_updownsumobserver( + self, + callback: ObserverCallbackT, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + label_keys: Sequence[str] = (), + enabled: bool = True, + ) -> "DefaultUpDownSumObserver": + return DefaultUpDownSumObserver() - def register_observer( + def register_valueobserver( self, callback: ObserverCallbackT, name: str, description: str, unit: str, value_type: Type[ValueT], - observer_type: Type[ObserverT], label_keys: Sequence[str] = (), enabled: bool = True, - ) -> "Observer": - return DefaultObserver() + ) -> "DefaultValueObserver": + return DefaultValueObserver() def unregister_observer(self, observer: "Observer") -> None: pass diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index aeec4b4ff4..12496d0c61 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -19,65 +19,56 @@ # pylint: disable=no-self-use class TestMetrics(unittest.TestCase): - def test_default(self): - default = metrics.DefaultMetric() - bound_metric_instr = default.bind({}) - self.assertIsInstance( - bound_metric_instr, metrics.DefaultBoundInstrument - ) - - def test_counter(self): - counter = metrics.Counter() + def test_default_counter(self): + counter = metrics.DefaultCounter() bound_counter = counter.bind({}) - self.assertIsInstance(bound_counter, metrics.BoundCounter) + self.assertIsInstance(bound_counter, metrics.DefaultBoundCounter) - def test_counter_add(self): - counter = metrics.Counter() + def test_default_counter_add(self): + counter = metrics.DefaultCounter() counter.add(1, {}) - def test_updowncounter(self): - counter = metrics.UpDownCounter() + def test_default_updowncounter(self): + counter = metrics.DefaultUpDownCounter() bound_counter = counter.bind({}) - self.assertIsInstance(bound_counter, metrics.BoundUpDownCounter) + self.assertIsInstance(bound_counter, metrics.DefaultBoundUpDownCounter) - def test_updowncounter_add(self): - counter = metrics.Counter() + def test_default_updowncounter_add(self): + counter = metrics.DefaultUpDownCounter() counter.add(1, {}) counter.add(-1, {}) - def test_valuerecorder(self): - valuerecorder = metrics.ValueRecorder() + def test_default_valuerecorder(self): + valuerecorder = metrics.DefaultValueRecorder() bound_valuerecorder = valuerecorder.bind({}) - self.assertIsInstance(bound_valuerecorder, metrics.BoundValueRecorder) + self.assertIsInstance( + bound_valuerecorder, metrics.DefaultBoundValueRecorder + ) - def test_valuerecorder_record(self): - valuerecorder = metrics.ValueRecorder() + def test_default_valuerecorder_record(self): + valuerecorder = metrics.DefaultValueRecorder() valuerecorder.record(1, {}) - def test_default_bound_metric(self): - bound_instrument = metrics.DefaultBoundInstrument() - bound_instrument.release() + def test_default_bound_counter(self): + bound_counter = metrics.DefaultBoundCounter() + bound_counter.add(1) - def test_bound_counter(self): - bound_counter = metrics.BoundCounter() + def test_default_bound_updowncounter(self): + bound_counter = metrics.DefaultBoundUpDownCounter() bound_counter.add(1) - def test_bound_valuerecorder(self): - bound_valuerecorder = metrics.BoundValueRecorder() + def test_default_bound_valuerecorder(self): + bound_valuerecorder = metrics.DefaultBoundValueRecorder() bound_valuerecorder.record(1) - def test_default_observer(self): - observer = metrics.DefaultObserver() - observer.observe(1, {}) - - def test_sum_observer(self): - observer = metrics.SumObserver() + def test_default_sum_observer(self): + observer = metrics.DefaultSumObserver() observer.observe(1, {}) - def test_updown_sum_observer(self): - observer = metrics.UpDownSumObserver() + def test_default_updown_sum_observer(self): + observer = metrics.DefaultUpDownSumObserver() observer.observe(1, {}) - def test_value_observer(self): - observer = metrics.ValueObserver() + def test_default_value_observer(self): + observer = metrics.DefaultValueObserver() observer.observe(1, {}) diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index 8e18fcb48d..1293e17435 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -74,23 +74,43 @@ def test_default_meter(self): # pylint: disable=no-self-use def test_record_batch(self): meter = metrics.DefaultMeter() - counter = metrics.Counter() + counter = metrics.DefaultCounter() meter.record_batch({}, ((counter, 1),)) - def test_create_metric(self): + def test_create_counter(self): meter = metrics.DefaultMeter() - metric = meter.create_metric("", "", "", float, metrics.Counter) - self.assertIsInstance(metric, metrics.DefaultMetric) + metric = meter.create_counter("", "", "", float) + self.assertIsInstance(metric, metrics.DefaultCounter) - def test_register_observer(self): + def test_create_updowncounter(self): + meter = metrics.DefaultMeter() + metric = meter.create_updowncounter("", "", "", float) + self.assertIsInstance(metric, metrics.DefaultUpDownCounter) + + def test_create_valuerecorder(self): + meter = metrics.DefaultMeter() + metric = meter.create_valuerecorder("", "", "", float) + self.assertIsInstance(metric, metrics.DefaultValueRecorder) + + def test_register_sumobserver(self): + meter = metrics.DefaultMeter() + callback = mock.Mock() + observer = meter.register_sumobserver(callback, "", "", "", int) + self.assertIsInstance(observer, metrics.DefaultSumObserver) + + def test_register_updownsumobserver(self): + meter = metrics.DefaultMeter() + callback = mock.Mock() + observer = meter.register_updownsumobserver(callback, "", "", "", int) + self.assertIsInstance(observer, metrics.DefaultUpDownSumObserver) + + def test_register_valueobserver(self): meter = metrics.DefaultMeter() callback = mock.Mock() - observer = meter.register_observer( - callback, "", "", "", int, metrics.ValueObserver - ) - self.assertIsInstance(observer, metrics.DefaultObserver) + observer = meter.register_valueobserver(callback, "", "", "", int) + self.assertIsInstance(observer, metrics.DefaultValueObserver) def test_unregister_observer(self): meter = metrics.DefaultMeter() - observer = metrics.DefaultObserver() + observer = metrics.DefaultSumObserver() meter.unregister_observer(observer) diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index e93b1c5bb6..a0787235c2 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -42,7 +42,6 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api == 0.15.dev0 - opentelemetry-sdk == 0.15.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py index 6aaca0bcaa..107d5a49fd 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py @@ -22,7 +22,6 @@ from typing import Dict, Optional from opentelemetry import metrics -from opentelemetry.sdk.metrics import ValueRecorder class HTTPMetricType(enum.Enum): @@ -61,20 +60,18 @@ def __init__( self._server_duration = None if self._meter is not None: if http_type in (HTTPMetricType.CLIENT, HTTPMetricType.BOTH): - self._client_duration = self._meter.create_metric( + self._client_duration = self._meter.create_valuerecorder( name="{}.{}.duration".format("http", "client"), description="measures the duration of the outbound HTTP request", unit="ms", value_type=float, - metric_type=ValueRecorder, ) if http_type is not HTTPMetricType.CLIENT: - self._server_duration = self._meter.create_metric( + self._server_duration = self._meter.create_valuerecorder( name="{}.{}.duration".format("http", "server"), description="measures the duration of the inbound HTTP request", unit="ms", value_type=float, - metric_type=ValueRecorder, ) # Conventions for recording duration can be found at: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 382dbeb9b9..8207aa5755 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -15,7 +15,7 @@ import atexit import logging import threading -from typing import Dict, Sequence, Tuple, Type +from typing import Dict, Sequence, Tuple, Type, TypeVar from opentelemetry import metrics as metrics_api from opentelemetry.sdk.metrics.export import ( @@ -48,9 +48,7 @@ class BaseBoundInstrument: metric: The metric that created this bound instrument. """ - def __init__( - self, labels: Tuple[Tuple[str, str]], metric: metrics_api.MetricT - ): + def __init__(self, labels: Tuple[Tuple[str, str]], metric: "MetricT"): self._labels = labels self._metric = metric self.view_datas = metric.meter.view_manager.get_view_datas( @@ -224,6 +222,9 @@ def record( UPDATE_FUNCTION = record +MetricT = TypeVar("MetricT", Counter, UpDownCounter, ValueRecorder) + + class Observer(metrics_api.Observer): """Base class for all asynchronous metric types. @@ -417,36 +418,99 @@ def record_batch( for metric, value in record_tuples: metric.UPDATE_FUNCTION(value, labels) - def create_metric( + def create_counter( + self, + name: str, + description: str, + unit: str, + value_type: Type[metrics_api.ValueT], + enabled: bool = True, + ) -> metrics_api.Counter: + """See `opentelemetry.metrics.Meter.create_counter`.""" + counter = Counter( + name, description, unit, value_type, self, enabled=enabled + ) + with self.metrics_lock: + self.metrics.add(counter) + return counter + + def create_updowncounter( self, name: str, description: str, unit: str, value_type: Type[metrics_api.ValueT], - metric_type: Type[metrics_api.MetricT], enabled: bool = True, - ) -> metrics_api.MetricT: - """See `opentelemetry.metrics.Meter.create_metric`.""" - # Ignore type b/c of mypy bug in addition to missing annotations - metric = metric_type( # type: ignore + ) -> metrics_api.UpDownCounter: + """See `opentelemetry.metrics.Meter.create_updowncounter`.""" + counter = UpDownCounter( name, description, unit, value_type, self, enabled=enabled ) with self.metrics_lock: - self.metrics.add(metric) - return metric + self.metrics.add(counter) + return counter + + def create_valuerecorder( + self, + name: str, + description: str, + unit: str, + value_type: Type[metrics_api.ValueT], + enabled: bool = True, + ) -> metrics_api.ValueRecorder: + """See `opentelemetry.metrics.Meter.create_valuerecorder`.""" + recorder = ValueRecorder( + name, description, unit, value_type, self, enabled=enabled + ) + with self.metrics_lock: + self.metrics.add(recorder) + return recorder + + def register_sumobserver( + self, + callback: metrics_api.ObserverCallbackT, + name: str, + description: str, + unit: str, + value_type: Type[metrics_api.ValueT], + label_keys: Sequence[str] = (), + enabled: bool = True, + ) -> metrics_api.SumObserver: + ob = SumObserver( + callback, name, description, unit, value_type, label_keys, enabled + ) + with self.observers_lock: + self.observers.add(ob) + return ob + + def register_updownsumobserver( + self, + callback: metrics_api.ObserverCallbackT, + name: str, + description: str, + unit: str, + value_type: Type[metrics_api.ValueT], + label_keys: Sequence[str] = (), + enabled: bool = True, + ) -> metrics_api.UpDownSumObserver: + ob = UpDownSumObserver( + callback, name, description, unit, value_type, label_keys, enabled + ) + with self.observers_lock: + self.observers.add(ob) + return ob - def register_observer( + def register_valueobserver( self, callback: metrics_api.ObserverCallbackT, name: str, description: str, unit: str, value_type: Type[metrics_api.ValueT], - observer_type=Type[metrics_api.ObserverT], label_keys: Sequence[str] = (), enabled: bool = True, - ) -> metrics_api.Observer: - ob = observer_type( + ) -> metrics_api.ValueObserver: + ob = ValueObserver( callback, name, description, unit, value_type, label_keys, enabled ) with self.observers_lock: diff --git a/opentelemetry-sdk/tests/metrics/test_implementation.py b/opentelemetry-sdk/tests/metrics/test_implementation.py index 1679f61834..0a2fbd5740 100644 --- a/opentelemetry-sdk/tests/metrics/test_implementation.py +++ b/opentelemetry-sdk/tests/metrics/test_implementation.py @@ -14,7 +14,7 @@ import unittest -from opentelemetry.metrics import DefaultMeter, DefaultMetric +from opentelemetry.metrics import DefaultCounter, DefaultMeter from opentelemetry.sdk import metrics @@ -28,6 +28,6 @@ class TestMeterImplementation(unittest.TestCase): def test_meter(self): meter = metrics.MeterProvider().get_meter(__name__) - metric = meter.create_metric("", "", "", float, metrics.Counter) + metric = meter.create_counter("", "", "", float) self.assertNotIsInstance(meter, DefaultMeter) - self.assertNotIsInstance(metric, DefaultMetric) + self.assertNotIsInstance(metric, DefaultCounter) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 1fcb2bbda1..58cc195cdf 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -74,9 +74,7 @@ def test_collect_metrics(self): meter = metrics.MeterProvider().get_meter(__name__) processor_mock = mock.Mock() meter.processor = processor_mock - counter = meter.create_metric( - "name", "desc", "unit", float, metrics.Counter - ) + counter = meter.create_counter("name", "desc", "unit", float,) labels = {"key1": "value1"} meter.register_view(View(counter, SumAggregator)) counter.add(1.0, labels) @@ -155,13 +153,11 @@ def test_record_batch(self): (3.0, 3.0, 3.0, 1), ) - def test_create_metric(self): + def test_create_counter(self): resource = mock.Mock(spec=resources.Resource) meter_provider = metrics.MeterProvider(resource=resource) meter = meter_provider.get_meter(__name__) - counter = meter.create_metric( - "name", "desc", "unit", int, metrics.Counter - ) + counter = meter.create_counter("name", "desc", "unit", int,) self.assertIsInstance(counter, metrics.Counter) self.assertEqual(counter.value_type, int) self.assertEqual(counter.name, "name") @@ -170,8 +166,8 @@ def test_create_metric(self): def test_create_updowncounter(self): meter = metrics.MeterProvider().get_meter(__name__) - updowncounter = meter.create_metric( - "name", "desc", "unit", float, metrics.UpDownCounter, () + updowncounter = meter.create_updowncounter( + "name", "desc", "unit", float, ) self.assertIsInstance(updowncounter, metrics.UpDownCounter) self.assertEqual(updowncounter.value_type, float) @@ -179,24 +175,64 @@ def test_create_updowncounter(self): def test_create_valuerecorder(self): meter = metrics.MeterProvider().get_meter(__name__) - valuerecorder = meter.create_metric( - "name", "desc", "unit", float, metrics.ValueRecorder + valuerecorder = meter.create_valuerecorder( + "name", "desc", "unit", float, ) self.assertIsInstance(valuerecorder, metrics.ValueRecorder) self.assertEqual(valuerecorder.value_type, float) self.assertEqual(valuerecorder.name, "name") self.assertEqual(valuerecorder.meter, meter) - def test_register_observer(self): + def test_register_sumobserver(self): meter = metrics.MeterProvider().get_meter(__name__) callback = mock.Mock() - observer = meter.register_observer( - callback, "name", "desc", "unit", int, metrics.ValueObserver + observer = meter.register_sumobserver( + callback, "name", "desc", "unit", int, + ) + + self.assertIsInstance(observer, metrics.SumObserver) + self.assertEqual(len(meter.observers), 1) + + self.assertEqual(observer.callback, callback) + self.assertEqual(observer.name, "name") + self.assertEqual(observer.description, "desc") + self.assertEqual(observer.unit, "unit") + self.assertEqual(observer.value_type, int) + self.assertEqual(observer.label_keys, ()) + self.assertTrue(observer.enabled) + + def test_register_updownsumobserver(self): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = mock.Mock() + + observer = meter.register_updownsumobserver( + callback, "name", "desc", "unit", int, + ) + + self.assertIsInstance(observer, metrics.UpDownSumObserver) + self.assertEqual(len(meter.observers), 1) + + self.assertEqual(observer.callback, callback) + self.assertEqual(observer.name, "name") + self.assertEqual(observer.description, "desc") + self.assertEqual(observer.unit, "unit") + self.assertEqual(observer.value_type, int) + self.assertEqual(observer.label_keys, ()) + self.assertTrue(observer.enabled) + + def test_register_valueobserver(self): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = mock.Mock() + + observer = meter.register_valueobserver( + callback, "name", "desc", "unit", int, ) - self.assertIsInstance(observer, metrics_api.Observer) + self.assertIsInstance(observer, metrics.ValueObserver) self.assertEqual(len(meter.observers), 1) self.assertEqual(observer.callback, callback) @@ -212,7 +248,7 @@ def test_unregister_observer(self): callback = mock.Mock() - observer = meter.register_observer( + observer = meter.register_valueobserver( callback, "name", "desc", "unit", int, metrics.ValueObserver ) @@ -223,7 +259,11 @@ def test_unregister_observer(self): class TestMetric(unittest.TestCase): def test_bind(self): meter = metrics.MeterProvider().get_meter(__name__) - metric_types = [metrics.Counter, metrics.ValueRecorder] + metric_types = [ + metrics.Counter, + metrics.UpDownCounter, + metrics.ValueRecorder, + ] labels = {"key": "value"} key_labels = metrics.get_dict_as_key(labels) for _type in metric_types: diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py index b4a24d4ea3..102db6da25 100644 --- a/opentelemetry-sdk/tests/metrics/test_view.py +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -17,7 +17,7 @@ from unittest import mock from opentelemetry.sdk import metrics -from opentelemetry.sdk.metrics import Counter, ValueRecorder, view +from opentelemetry.sdk.metrics import view from opentelemetry.sdk.metrics.export import aggregate from opentelemetry.sdk.metrics.export.aggregate import ( HistogramAggregator, @@ -79,12 +79,11 @@ def tearDown(self): self.controller.shutdown() def test_label_keys(self): - test_counter = self.meter.create_metric( + test_counter = self.meter.create_counter( name="test_counter", description="description", unit="By", value_type=int, - metric_type=Counter, ) counter_view = View( test_counter, @@ -107,12 +106,11 @@ def test_label_keys(self): self.assertEqual(metric_data[0].aggregator.checkpoint, 11) def test_ungrouped(self): - test_counter = self.meter.create_metric( + test_counter = self.meter.create_counter( name="test_counter", description="description", unit="By", value_type=int, - metric_type=Counter, ) counter_view = View( test_counter, @@ -138,12 +136,11 @@ def test_ungrouped(self): self.assertTrue((label2, 5) in data_set) def test_multiple_views(self): - test_counter = self.meter.create_metric( + test_counter = self.meter.create_counter( name="test_counter", description="description", unit="By", value_type=int, - metric_type=Counter, ) counter_view = View( @@ -193,12 +190,11 @@ def test_histogram_stateful(self): exporter = InMemoryMetricsExporter() controller = PushController(meter, exporter, 30) - requests_size = meter.create_metric( + requests_size = meter.create_valuerecorder( name="requests_size", description="size of requests", unit="1", value_type=int, - metric_type=ValueRecorder, ) size_view = View( @@ -248,12 +244,11 @@ def test_histogram_stateless(self): exporter = InMemoryMetricsExporter() controller = PushController(meter, exporter, 30) - requests_size = meter.create_metric( + requests_size = meter.create_valuerecorder( name="requests_size", description="size of requests", unit="1", value_type=int, - metric_type=ValueRecorder, ) size_view = View( From aad033fc33cc695bdbfe0dc02f611da099dbbcd6 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Wed, 28 Oct 2020 09:29:09 -0700 Subject: [PATCH 0631/1517] docs: Fixing observer example (#1292) --- docs/examples/basic_meter/observer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/examples/basic_meter/observer.py b/docs/examples/basic_meter/observer.py index 7481126ffc..ed0eb5e278 100644 --- a/docs/examples/basic_meter/observer.py +++ b/docs/examples/basic_meter/observer.py @@ -40,7 +40,6 @@ def get_cpu_usage_callback(observer): description="per-cpu usage", unit="1", value_type=float, - observer_type=ValueObserver, ) @@ -56,7 +55,6 @@ def get_ram_usage_callback(observer): description="RAM memory usage", unit="1", value_type=float, - observer_type=ValueObserver, ) input("Metrics will be printed soon. Press a key to finish...\n") From fcd04a759d8f3f23e61a8615caba59b60bf17b05 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Wed, 28 Oct 2020 10:23:40 -0700 Subject: [PATCH 0632/1517] docs: using correct python specifier for docs action (#1293) There is a build error with the docs Github action currently, that is due to incorrect Python specifier syntax. Run actions/setup-python@v2 Version py38 was not found in the local cache Error: Version py38 with arch x64 not found The list of all available versions can be found here: https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json --- .github/workflows/docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index af52dd3665..3c8d31ca4e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,7 +10,7 @@ on: - 'instrumentation/**' - 'opentelemetry-python/opentelemetry-api/src/opentelemetry/**' - 'opentelemetry-python/opentelemetry-sdk/src/opentelemetry/sdk/**' - + jobs: docs: runs-on: ubuntu-latest @@ -19,7 +19,7 @@ jobs: - name: Set up Python py38 uses: actions/setup-python@v2 with: - python-version: py38 + python-version: '3.8' - name: Build docs run: | pip install --upgrade tox From 42abfb77e19a37c01aea65722b5d56443739c91b Mon Sep 17 00:00:00 2001 From: Abhilash Gnan Date: Wed, 28 Oct 2020 20:57:32 +0100 Subject: [PATCH 0633/1517] Add Env variables in OTLP exporter (#1101) --- .../opentelemetry-exporter-otlp/CHANGELOG.md | 3 ++ .../opentelemetry/exporter/otlp/exporter.py | 49 ++++++++++++++++--- .../otlp/metrics_exporter/__init__.py | 42 +++++++++++++++- .../exporter/otlp/trace_exporter/__init__.py | 41 +++++++++++++++- .../tests/fixtures/test.cert | 0 .../tests/test_otlp_metric_exporter.py | 33 ++++++++++++- .../tests/test_otlp_trace_exporter.py | 30 +++++++++++- 7 files changed, 186 insertions(+), 12 deletions(-) create mode 100644 exporter/opentelemetry-exporter-otlp/tests/fixtures/test.cert diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 479f2105e7..0e9fbba8a8 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add Env variables in OTLP exporter + ([#1101](https://github.com/open-telemetry/opentelemetry-python/pull/1101)) + ## Version 0.14b0 Released 2020-10-13 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index 079557f831..9595c35f64 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -15,6 +15,7 @@ """OTLP Exporter""" import logging +import os from abc import ABC, abstractmethod from collections.abc import Mapping, Sequence from time import sleep @@ -30,8 +31,10 @@ StatusCode, insecure_channel, secure_channel, + ssl_channel_credentials, ) +from opentelemetry.configuration import Configuration from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue from opentelemetry.proto.resource.v1.resource_pb2 import Resource from opentelemetry.sdk.resources import Resource as SDKResource @@ -113,6 +116,16 @@ def _get_resource_data( return resource_data +def _load_credential_from_file(filepath) -> ChannelCredentials: + try: + with open(filepath, "rb") as f: + credential = f.read() + return ssl_channel_credentials(credential) + except FileNotFoundError: + logger.exception("Failed to read credential file") + return None + + # pylint: disable=no-member class OTLPExporterMixin( ABC, Generic[SDKDataT, ExportServiceRequestT, ExportResultT] @@ -121,24 +134,47 @@ class OTLPExporterMixin( Args: endpoint: OpenTelemetry Collector receiver endpoint + insecure: Connection type credentials: ChannelCredentials object for server authentication metadata: Metadata to send when exporting + timeout: Backend request timeout in seconds """ def __init__( self, - endpoint: str = "localhost:55680", - credentials: ChannelCredentials = None, - metadata: Optional[Tuple[Any]] = None, + endpoint: Optional[str] = None, + insecure: Optional[bool] = None, + credentials: Optional[ChannelCredentials] = None, + headers: Optional[str] = None, + timeout: Optional[int] = None, ): super().__init__() - self._metadata = metadata + endpoint = ( + endpoint + or Configuration().EXPORTER_OTLP_ENDPOINT + or "localhost:55680" + ) + + if insecure is None: + insecure = Configuration().EXPORTER_OTLP_INSECURE + if insecure is None: + insecure = False + + self._headers = headers or Configuration().EXPORTER_OTLP_HEADERS + self._timeout = ( + timeout + or Configuration().EXPORTER_OTLP_TIMEOUT + or 10 # default: 10 seconds + ) self._collector_span_kwargs = None - if credentials is None: + if insecure: self._client = self._stub(insecure_channel(endpoint)) else: + credentials = credentials or _load_credential_from_file( + Configuration().EXPORTER_OTLP_CERTIFICATE + ) self._client = self._stub(secure_channel(endpoint, credentials)) @abstractmethod @@ -164,7 +200,8 @@ def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: try: self._client.Export( request=self._translate_data(data), - metadata=self._metadata, + metadata=self._headers, + timeout=self._timeout, ) return self._result.SUCCESS diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py index 40feb222fb..18ce772ea4 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -15,12 +15,16 @@ """OTLP Metrics Exporter""" import logging -from typing import List, Sequence, Type, TypeVar +import os +from typing import List, Optional, Sequence, Type, TypeVar, Union -# pylint: disable=duplicate-code +from grpc import ChannelCredentials + +from opentelemetry.configuration import Configuration from opentelemetry.exporter.otlp.exporter import ( OTLPExporterMixin, _get_resource_data, + _load_credential_from_file, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, @@ -109,13 +113,47 @@ class OTLPMetricsExporter( Args: endpoint: OpenTelemetry Collector receiver endpoint + insecure: Connection type credentials: Credentials object for server authentication metadata: Metadata to send when exporting + timeout: Backend request timeout in seconds """ _stub = MetricsServiceStub _result = MetricsExportResult + def __init__( + self, + endpoint: Optional[str] = None, + insecure: Optional[bool] = None, + credentials: Optional[ChannelCredentials] = None, + headers: Optional[str] = None, + timeout: Optional[int] = None, + ): + if insecure is None: + insecure = Configuration().EXPORTER_OTLP_METRIC_INSECURE + + if ( + not insecure + and Configuration().EXPORTER_OTLP_METRIC_CERTIFICATE is not None + ): + credentials = credentials or _load_credential_from_file( + Configuration().EXPORTER_OTLP_METRIC_CERTIFICATE + ) + + super().__init__( + **{ + "endpoint": endpoint + or Configuration().EXPORTER_OTLP_METRIC_ENDPOINT, + "insecure": insecure, + "credentials": credentials, + "headers": headers + or Configuration().EXPORTER_OTLP_METRIC_HEADERS, + "timeout": timeout + or Configuration().EXPORTER_OTLP_METRIC_TIMEOUT, + } + ) + # pylint: disable=no-self-use def _translate_data( self, data: Sequence[MetricRecord] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index e518716d39..8acd2dea55 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -14,11 +14,16 @@ """OTLP Span Exporter""" import logging -from typing import Sequence +import os +from typing import Optional, Sequence +from grpc import ChannelCredentials + +from opentelemetry.configuration import Configuration from opentelemetry.exporter.otlp.exporter import ( OTLPExporterMixin, _get_resource_data, + _load_credential_from_file, _translate_key_values, ) from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( @@ -50,13 +55,47 @@ class OTLPSpanExporter( Args: endpoint: OpenTelemetry Collector receiver endpoint + insecure: Connection type credentials: Credentials object for server authentication metadata: Metadata to send when exporting + timeout: Backend request timeout in seconds """ _result = SpanExportResult _stub = TraceServiceStub + def __init__( + self, + endpoint: Optional[str] = None, + insecure: Optional[bool] = None, + credentials: Optional[ChannelCredentials] = None, + headers: Optional[str] = None, + timeout: Optional[int] = None, + ): + if insecure is None: + insecure = Configuration().EXPORTER_OTLP_SPAN_INSECURE + + if ( + not insecure + and Configuration().EXPORTER_OTLP_SPAN_CERTIFICATE is not None + ): + credentials = credentials or _load_credential_from_file( + Configuration().EXPORTER_OTLP_SPAN_CERTIFICATE + ) + + super().__init__( + **{ + "endpoint": endpoint + or Configuration().EXPORTER_OTLP_SPAN_ENDPOINT, + "insecure": insecure, + "credentials": credentials, + "headers": headers + or Configuration().EXPORTER_OTLP_SPAN_HEADERS, + "timeout": timeout + or Configuration().EXPORTER_OTLP_SPAN_TIMEOUT, + } + ) + def _translate_name(self, sdk_span: SDKSpan) -> None: self._collector_span_kwargs["name"] = sdk_span.name diff --git a/exporter/opentelemetry-exporter-otlp/tests/fixtures/test.cert b/exporter/opentelemetry-exporter-otlp/tests/fixtures/test.cert new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index 21a718b84e..530f9a430a 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -16,6 +16,9 @@ from unittest import TestCase from unittest.mock import patch +from grpc import ChannelCredentials + +from opentelemetry.configuration import Configuration from opentelemetry.exporter.otlp.metrics_exporter import OTLPMetricsExporter from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, @@ -44,8 +47,9 @@ class TestOTLPMetricExporter(TestCase): def setUp(self): - self.exporter = OTLPMetricsExporter() + self.exporter = OTLPMetricsExporter(insecure=True) resource = SDKResource(OrderedDict([("a", 1), ("b", False)])) + self.counter_metric_record = MetricRecord( Counter( "a", @@ -60,6 +64,33 @@ def setUp(self): resource, ) + Configuration._reset() # pylint: disable=protected-access + + def tearDown(self): + Configuration._reset() # pylint: disable=protected-access + + @patch.dict( + "os.environ", + { + "OTEL_EXPORTER_OTLP_METRIC_ENDPOINT": "collector:55680", + "OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE": "fixtures/test.cert", + "OTEL_EXPORTER_OTLP_METRIC_HEADERS": "key1:value1;key2:value2", + "OTEL_EXPORTER_OTLP_METRIC_TIMEOUT": "10", + }, + ) + @patch("opentelemetry.exporter.otlp.exporter.OTLPExporterMixin.__init__") + def test_env_variables(self, mock_exporter_mixin): + OTLPMetricsExporter() + + self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) + _, kwargs = mock_exporter_mixin.call_args_list[0] + + self.assertEqual(kwargs["endpoint"], "collector:55680") + self.assertEqual(kwargs["headers"], "key1:value1;key2:value2") + self.assertEqual(kwargs["timeout"], 10) + self.assertIsNotNone(kwargs["credentials"]) + self.assertIsInstance(kwargs["credentials"], ChannelCredentials) + @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") def test_translate_metrics(self, mock_time_ns): # pylint: disable=no-member diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index e8c449c9df..9c99056c44 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -19,8 +19,9 @@ from google.protobuf.duration_pb2 import Duration from google.rpc.error_details_pb2 import RetryInfo -from grpc import StatusCode, server +from grpc import ChannelCredentials, StatusCode, server +from opentelemetry.configuration import Configuration from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest, @@ -102,7 +103,7 @@ def Export(self, request, context): class TestOTLPSpanExporter(TestCase): def setUp(self): tracer_provider = TracerProvider() - self.exporter = OTLPSpanExporter() + self.exporter = OTLPSpanExporter(insecure=True) tracer_provider.add_span_processor( SimpleExportSpanProcessor(self.exporter) ) @@ -154,8 +155,33 @@ def setUp(self): self.span.start() self.span.end() + Configuration._reset() # pylint: disable=protected-access + def tearDown(self): self.server.stop(None) + Configuration._reset() # pylint: disable=protected-access + + @patch.dict( + "os.environ", + { + "OTEL_EXPORTER_OTLP_SPAN_ENDPOINT": "collector:55680", + "OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE": "fixtures/test.cert", + "OTEL_EXPORTER_OTLP_SPAN_HEADERS": "key1:value1;key2:value2", + "OTEL_EXPORTER_OTLP_SPAN_TIMEOUT": "10", + }, + ) + @patch("opentelemetry.exporter.otlp.exporter.OTLPExporterMixin.__init__") + def test_env_variables(self, mock_exporter_mixin): + OTLPSpanExporter() + + self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) + _, kwargs = mock_exporter_mixin.call_args_list[0] + + self.assertEqual(kwargs["endpoint"], "collector:55680") + self.assertEqual(kwargs["headers"], "key1:value1;key2:value2") + self.assertEqual(kwargs["timeout"], 10) + self.assertIsNotNone(kwargs["credentials"]) + self.assertIsInstance(kwargs["credentials"], ChannelCredentials) @patch("opentelemetry.exporter.otlp.exporter.expo") @patch("opentelemetry.exporter.otlp.exporter.sleep") From f3cdfa2cdb9cb1c442189e2ead3788f45d92352d Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 28 Oct 2020 17:28:58 -0400 Subject: [PATCH 0634/1517] Change status codes from grpc status codes, remove setting status in instrumentations except on ERROR (#1282) --- docs/examples/auto-instrumentation/README.rst | 4 +- docs/examples/django/README.rst | 2 +- docs/getting-started.rst | 6 +- .../exporter/datadog/exporter.py | 3 +- .../opentelemetry/exporter/jaeger/__init__.py | 6 +- .../tests/test_jaeger_exporter.py | 13 +- .../opencensus/trace_exporter/__init__.py | 2 +- .../tests/test_otcollector_trace_exporter.py | 10 +- .../exporter/otlp/trace_exporter/__init__.py | 8 +- .../opentelemetry/exporter/zipkin/__init__.py | 2 +- .../tests/test_zipkin_exporter.py | 16 +- .../aiohttp_client/__init__.py | 26 +-- .../tests/test_aiohttp_client_integration.py | 27 ++-- .../aiopg/aiopg_integration.py | 8 +- .../tests/test_aiopg_integration.py | 6 +- .../instrumentation/asgi/__init__.py | 8 +- .../instrumentation/asyncpg/__init__.py | 21 +-- .../instrumentation/celery/__init__.py | 5 +- .../instrumentation/dbapi/__init__.py | 8 +- .../tests/test_dbapi_integration.py | 6 +- .../tests/test_middleware.py | 12 +- .../instrumentation/elasticsearch/__init__.py | 8 +- .../tests/test_elasticsearch.py | 8 +- .../instrumentation/falcon/__init__.py | 4 +- .../tests/test_falcon.py | 12 +- .../instrumentation/grpc/_client.py | 16 +- .../tests/test_client_interceptor.py | 20 +-- .../instrumentation/jinja2/__init__.py | 2 +- .../tests/test_pymemcache.py | 15 +- .../instrumentation/pymongo/__init__.py | 12 +- .../tests/test_pymongo.py | 12 +- .../instrumentation/requests/__init__.py | 23 +-- .../tests/test_requests_integration.py | 29 +--- .../instrumentation/sqlalchemy/engine.py | 7 +- .../instrumentation/tornado/__init__.py | 4 +- .../instrumentation/tornado/client.py | 4 +- .../instrumentation/wsgi/__init__.py | 10 +- .../tests/test_wsgi_middleware.py | 4 +- opentelemetry-api/CHANGELOG.md | 2 + .../src/opentelemetry/trace/__init__.py | 4 +- .../src/opentelemetry/trace/status.py | 152 ++---------------- opentelemetry-api/tests/trace/test_status.py | 10 +- .../opentelemetry/instrumentation/utils.py | 35 ++-- .../tests/test_utils.py | 53 +++--- opentelemetry-sdk/CHANGELOG.md | 1 + .../src/opentelemetry/sdk/trace/__init__.py | 28 ++-- opentelemetry-sdk/tests/trace/test_trace.py | 38 +++-- .../tests/asyncpg/test_asyncpg_functional.py | 37 ++--- .../tests/celery/test_celery_functional.py | 12 +- .../tests/redis/test_redis_functional.py | 4 +- .../tests/sqlalchemy_tests/mixins.py | 4 +- .../tests/sqlalchemy_tests/test_instrument.py | 4 +- .../tests/sqlalchemy_tests/test_mysql.py | 3 +- .../tests/sqlalchemy_tests/test_postgres.py | 3 +- .../tests/sqlalchemy_tests/test_sqlite.py | 3 +- 55 files changed, 265 insertions(+), 517 deletions(-) diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index e4af3c6bd5..a023acaa94 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -112,7 +112,7 @@ similar to: "start_time": "2020-04-30T17:28:57.886397Z", "end_time": "2020-04-30T17:28:57.886490Z", "status": { - "canonical_code": "OK" + "status_code": "OK" }, "attributes": { "component": "http", @@ -164,7 +164,7 @@ similar to: "start_time": "2020-04-30T17:10:02.400604Z", "end_time": "2020-04-30T17:10:02.401858Z", "status": { - "canonical_code": "OK" + "status_code": "OK" }, "attributes": { "component": "http", diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 6f40cf7aeb..64ff0b32cf 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -77,7 +77,7 @@ output similar to this one: "start_time": "2020-04-26T01:49:57.205833Z", "end_time": "2020-04-26T01:49:57.206214Z", "status": { - "canonical_code": "OK" + "status_code": "OK" }, "attributes": { "component": "http", diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 213989fbe5..49d1ad3ee2 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -49,7 +49,7 @@ We can run it, and see the traces print to your console: "start_time": "2020-05-07T14:39:52.906272Z", "end_time": "2020-05-07T14:39:52.906343Z", "status": { - "canonical_code": "OK" + "status_code": "OK" }, "attributes": {}, "events": [], @@ -67,7 +67,7 @@ We can run it, and see the traces print to your console: "start_time": "2020-05-07T14:39:52.906230Z", "end_time": "2020-05-07T14:39:52.906601Z", "status": { - "canonical_code": "OK" + "status_code": "OK" }, "attributes": {}, "events": [], @@ -85,7 +85,7 @@ We can run it, and see the traces print to your console: "start_time": "2020-05-07T14:39:52.906157Z", "end_time": "2020-05-07T14:39:52.906743Z", "status": { - "canonical_code": "OK" + "status_code": "OK" }, "attributes": {}, "events": [], diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py index 36335c2358..2b1bd90041 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py @@ -23,7 +23,6 @@ import opentelemetry.trace as trace_api from opentelemetry.sdk.trace import sampling from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult -from opentelemetry.trace.status import StatusCanonicalCode # pylint:disable=relative-beyond-top-level from .constants import ( @@ -145,7 +144,7 @@ def _translate_to_datadog(self, spans): datadog_span.start_ns = span.start_time datadog_span.duration_ns = span.end_time - span.start_time - if span.status.canonical_code is not StatusCanonicalCode.OK: + if not span.status.is_ok: datadog_span.error = 1 if span.status.description: exc_type, exc_val = _get_exc_info(span) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index 7c6d8bd679..3271fdd5ba 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -70,7 +70,7 @@ from opentelemetry.exporter.jaeger.gen.agent import Agent as agent from opentelemetry.exporter.jaeger.gen.jaeger import Collector as jaeger from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult -from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.trace.status import StatusCode DEFAULT_AGENT_HOST_NAME = "localhost" DEFAULT_AGENT_PORT = 6831 @@ -224,7 +224,7 @@ def _translate_to_jaeger(spans: Span): tags.extend( [ - _get_long_tag("status.code", status.canonical_code.value), + _get_long_tag("status.code", status.status_code.value), _get_string_tag("status.message", status.description), _get_string_tag("span.kind", span.kind.name), ] @@ -245,7 +245,7 @@ def _translate_to_jaeger(spans: Span): ) # Ensure that if Status.Code is not OK, that we set the "error" tag on the Jaeger span. - if status.canonical_code is not StatusCanonicalCode.OK: + if not status.is_ok: tags.append(_get_bool_tag("error", True)) refs = _extract_refs_from_span(span) diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py index 3daeacb7fd..23afdefd78 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py @@ -25,7 +25,7 @@ from opentelemetry.sdk import trace from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode class TestJaegerSpanExporter(unittest.TestCase): @@ -210,7 +210,7 @@ def test_translate_to_jaeger(self): jaeger.Tag( key="status.code", vType=jaeger.TagType.LONG, - vLong=StatusCanonicalCode.OK.value, + vLong=StatusCode.UNSET.value, ), jaeger.Tag( key="status.message", vType=jaeger.TagType.STRING, vStr=None @@ -249,7 +249,7 @@ def test_translate_to_jaeger(self): attributes={"key_resource": "some_resource"} ) otel_spans[0].set_status( - Status(StatusCanonicalCode.UNKNOWN, "Example description") + Status(StatusCode.ERROR, "Example description") ) otel_spans[0].end(end_time=end_times[0]) @@ -259,6 +259,7 @@ def test_translate_to_jaeger(self): otel_spans[2].start(start_time=start_times[2]) otel_spans[2].resource = Resource({}) + otel_spans[2].set_status(Status(StatusCode.OK, "Example description")) otel_spans[2].end(end_time=end_times[2]) otel_spans[2].instrumentation_info = InstrumentationInfo( name="name", version="version" @@ -304,7 +305,7 @@ def test_translate_to_jaeger(self): jaeger.Tag( key="status.code", vType=jaeger.TagType.LONG, - vLong=StatusCanonicalCode.UNKNOWN.value, + vLong=StatusCode.ERROR.value, ), jaeger.Tag( key="status.message", @@ -380,12 +381,12 @@ def test_translate_to_jaeger(self): jaeger.Tag( key="status.code", vType=jaeger.TagType.LONG, - vLong=StatusCanonicalCode.OK.value, + vLong=StatusCode.OK.value, ), jaeger.Tag( key="status.message", vType=jaeger.TagType.STRING, - vStr=None, + vStr="Example description", ), jaeger.Tag( key="span.kind", diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py index e5eb4eaf77..613ee6482b 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py @@ -93,7 +93,7 @@ def translate_to_collector(spans: Sequence[Span]): status = None if span.status is not None: status = trace_pb2.Status( - code=span.status.canonical_code.value, + code=span.status.status_code.value, message=span.status.description, ) diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py index c49310b30a..b61cf333cc 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py @@ -148,15 +148,14 @@ def test_translate_to_collector(self): otel_spans[0].set_attribute("key_int", 333) otel_spans[0].set_status( trace_api.Status( - trace_api.status.StatusCanonicalCode.INTERNAL, - "test description", + trace_api.status.StatusCode.OK, "test description", ) ) otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) otel_spans[1].set_status( trace_api.Status( - trace_api.status.StatusCanonicalCode.INTERNAL, {"test", "val"}, + trace_api.status.StatusCode.ERROR, {"test", "val"}, ) ) otel_spans[1].end(end_time=end_times[1]) @@ -197,8 +196,7 @@ def test_translate_to_collector(self): output_spans[2].parent_span_id, b"\x11\x11\x11\x11\x11\x11\x11\x11" ) self.assertEqual( - output_spans[0].status.code, - trace_api.status.StatusCanonicalCode.INTERNAL.value, + output_spans[0].status.code, trace_api.status.StatusCode.OK.value, ) self.assertEqual(output_spans[0].status.message, "test description") self.assertEqual(len(output_spans[0].tracestate.entries), 1) @@ -270,7 +268,7 @@ def test_translate_to_collector(self): ) self.assertEqual( output_spans[1].status.code, - trace_api.status.StatusCanonicalCode.INTERNAL.value, + trace_api.status.StatusCode.ERROR.value, ) self.assertEqual( output_spans[2].links.link[0].type, diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index 8acd2dea55..870e8fab0f 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -41,6 +41,7 @@ from opentelemetry.proto.trace.v1.trace_pb2 import Status from opentelemetry.sdk.trace import Span as SDKSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.trace.status import StatusCode logger = logging.getLogger(__name__) @@ -198,9 +199,12 @@ def _translate_links(self, sdk_span: SDKSpan) -> None: def _translate_status(self, sdk_span: SDKSpan) -> None: if sdk_span.status is not None: + # TODO: Update this when the proto definitions are updated to include UNSET and ERROR + proto_status_code = Status.STATUS_CODE_OK + if sdk_span.status.status_code is StatusCode.ERROR: + proto_status_code = Status.STATUS_CODE_UNKNOWN_ERROR self._collector_span_kwargs["status"] = Status( - code=sdk_span.status.canonical_code.value, - message=sdk_span.status.description, + code=proto_status_code, message=sdk_span.status.description, ) def _translate_data( diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index c8578c9649..bd3cdbb26a 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -193,7 +193,7 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): if span.status is not None: zipkin_span["tags"]["otel.status_code"] = str( - span.status.canonical_code.value + span.status.status_code.value ) if span.status.description is not None: zipkin_span["tags"][ diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index c3098da884..12d85a0dc4 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -24,7 +24,7 @@ from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import TraceFlags -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode class MockResponse: @@ -179,7 +179,7 @@ def test_export(self): otel_spans[0].set_attribute("key_string", "hello_world") otel_spans[0].set_attribute("key_float", 111.22) otel_spans[0].set_status( - Status(StatusCanonicalCode.UNKNOWN, "Example description") + Status(StatusCode.ERROR, "Example description") ) otel_spans[0].end(end_time=end_times[0]) @@ -248,7 +248,7 @@ def test_export(self): "kind": None, "tags": { "key_resource": "some_resource", - "otel.status_code": "0", + "otel.status_code": "1", }, "annotations": None, }, @@ -263,7 +263,7 @@ def test_export(self): "tags": { "key_string": "hello_world", "key_resource": "some_resource", - "otel.status_code": "0", + "otel.status_code": "1", }, "annotations": None, }, @@ -278,7 +278,7 @@ def test_export(self): "tags": { "otel.instrumentation_library.name": "name", "otel.instrumentation_library.version": "version", - "otel.status_code": "0", + "otel.status_code": "1", }, "annotations": None, }, @@ -356,7 +356,7 @@ def test_zero_padding(self): "duration": duration // 10 ** 3, "localEndpoint": local_endpoint, "kind": None, - "tags": {"otel.status_code": "0"}, + "tags": {"otel.status_code": "1"}, "annotations": None, "debug": True, "parentId": "0aaaaaaaaaaaaaaa", @@ -400,9 +400,7 @@ def test_max_tag_length(self): # added here to preserve order span.set_attribute("k1", "v" * 500) span.set_attribute("k2", "v" * 50) - span.set_status( - Status(StatusCanonicalCode.UNKNOWN, "Example description") - ) + span.set_status(Status(StatusCode.ERROR, "Example description")) span.end() exporter = ZipkinSpanExporter(service_name) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py index 6606c48331..c708802a92 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py @@ -74,11 +74,11 @@ def strip_query_params(url: yarl.URL) -> str: from opentelemetry.instrumentation.aiohttp_client.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import ( - http_status_to_canonical_code, + http_status_to_status_code, unwrap, ) from opentelemetry.trace import SpanKind, TracerProvider, get_tracer -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode _UrlFilterT = typing.Optional[typing.Callable[[str], str]] _SpanNameT = typing.Optional[ @@ -194,9 +194,7 @@ async def on_request_end( if trace_config_ctx.span.is_recording(): trace_config_ctx.span.set_status( - Status( - http_status_to_canonical_code(int(params.response.status)) - ) + Status(http_status_to_status_code(int(params.response.status))) ) trace_config_ctx.span.set_attribute( "http.status_code", params.response.status @@ -214,22 +212,8 @@ async def on_request_exception( if trace_config_ctx.span is None: return - if trace_config_ctx.span.is_recording(): - if isinstance( - params.exception, - (aiohttp.ServerTimeoutError, aiohttp.TooManyRedirects), - ): - status = StatusCanonicalCode.DEADLINE_EXCEEDED - # Assume any getaddrinfo error is a DNS failure. - elif isinstance( - params.exception, aiohttp.ClientConnectorError - ) and isinstance(params.exception.os_error, socket.gaierror): - # DNS resolution failed - status = StatusCanonicalCode.UNKNOWN - else: - status = StatusCanonicalCode.UNAVAILABLE - - trace_config_ctx.span.set_status(Status(status)) + if trace_config_ctx.span.is_recording() and params.exception: + trace_config_ctx.span.set_status(Status(StatusCode.ERROR)) trace_config_ctx.span.record_exception(params.exception) _end_trace(trace_config_ctx) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py index fb5b6aac6a..f073465348 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -31,7 +31,7 @@ AioHttpClientInstrumentor, ) from opentelemetry.test.test_base import TestBase -from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.trace.status import StatusCode def run_with_test_server( @@ -61,7 +61,7 @@ def assert_spans(self, spans): [ ( span.name, - (span.status.canonical_code, span.status.description), + (span.status.status_code, span.status.description), dict(span.attributes), ) for span in self.memory_exporter.get_finished_spans() @@ -111,13 +111,10 @@ async def client_request(server: aiohttp.test_utils.TestServer): def test_status_codes(self): for status_code, span_status in ( - (HTTPStatus.OK, StatusCanonicalCode.OK), - (HTTPStatus.TEMPORARY_REDIRECT, StatusCanonicalCode.OK), - (HTTPStatus.SERVICE_UNAVAILABLE, StatusCanonicalCode.UNAVAILABLE), - ( - HTTPStatus.GATEWAY_TIMEOUT, - StatusCanonicalCode.DEADLINE_EXCEEDED, - ), + (HTTPStatus.OK, StatusCode.UNSET), + (HTTPStatus.TEMPORARY_REDIRECT, StatusCode.UNSET), + (HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR), + (HTTPStatus.GATEWAY_TIMEOUT, StatusCode.ERROR,), ): with self.subTest(status_code=status_code): host, port = self._http_request( @@ -188,7 +185,7 @@ def test_span_name_option(self): [ ( expected, - (StatusCanonicalCode.OK, None), + (StatusCode.UNSET, None), { "component": "http", "http.method": method, @@ -220,7 +217,7 @@ def strip_query_params(url: yarl.URL) -> str: [ ( "HTTP GET", - (StatusCanonicalCode.OK, None), + (StatusCode.UNSET, None), { "component": "http", "http.method": "GET", @@ -238,8 +235,8 @@ def test_connection_errors(self): trace_configs = [aiohttp_client.create_trace_config()] for url, expected_status in ( - ("http://this-is-unknown.local/", StatusCanonicalCode.UNKNOWN), - ("http://127.0.0.1:1/", StatusCanonicalCode.UNAVAILABLE), + ("http://this-is-unknown.local/", StatusCode.ERROR), + ("http://127.0.0.1:1/", StatusCode.ERROR), ): with self.subTest(expected_status=expected_status): @@ -286,7 +283,7 @@ async def request_handler(request): [ ( "HTTP GET", - (StatusCanonicalCode.DEADLINE_EXCEEDED, None), + (StatusCode.ERROR, None), { "component": "http", "http.method": "GET", @@ -316,7 +313,7 @@ async def request_handler(request): [ ( "HTTP GET", - (StatusCanonicalCode.DEADLINE_EXCEEDED, None), + (StatusCode.ERROR, None), { "component": "http", "http.method": "GET", diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py index 1455f23e62..14f986da06 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py @@ -8,7 +8,7 @@ TracedCursor, ) from opentelemetry.trace import SpanKind -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode # pylint: disable=abstract-method @@ -108,14 +108,10 @@ async def traced_execution( self._populate_span(span, *args) try: result = await query_method(*args, **kwargs) - if span.is_recording(): - span.set_status(Status(StatusCanonicalCode.OK)) return result except Exception as ex: # pylint: disable=broad-except if span.is_recording(): - span.set_status( - Status(StatusCanonicalCode.UNKNOWN, str(ex)) - ) + span.set_status(Status(StatusCode.ERROR, str(ex))) raise ex diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py index 135f9ee9a7..78ea4552e2 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py @@ -226,8 +226,7 @@ def test_span_succeeded(self): self.assertEqual(span.attributes["net.peer.name"], "testhost") self.assertEqual(span.attributes["net.peer.port"], 123) self.assertIs( - span.status.canonical_code, - trace_api.status.StatusCanonicalCode.OK, + span.status.status_code, trace_api.status.StatusCode.UNSET, ) def test_span_not_recording(self): @@ -278,8 +277,7 @@ def test_span_failed(self): span = spans_list[0] self.assertEqual(span.attributes["db.statement"], "Test query") self.assertIs( - span.status.canonical_code, - trace_api.status.StatusCanonicalCode.UNKNOWN, + span.status.status_code, trace_api.status.StatusCode.ERROR, ) self.assertEqual(span.status.description, "Test Exception") diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index 879662ddcf..1a0bb47a64 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -28,8 +28,8 @@ from opentelemetry import context, propagators, trace from opentelemetry.instrumentation.asgi.version import __version__ # noqa -from opentelemetry.instrumentation.utils import http_status_to_canonical_code -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.instrumentation.utils import http_status_to_status_code +from opentelemetry.trace.status import Status, StatusCode def get_header_from_scope(scope: dict, header_name: str) -> typing.List[str]: @@ -98,13 +98,13 @@ def set_status_code(span, status_code): except ValueError: span.set_status( Status( - StatusCanonicalCode.UNKNOWN, + StatusCode.ERROR, "Non-integer HTTP status: " + repr(status_code), ) ) else: span.set_attribute("http.status_code", status_code) - span.set_status(Status(http_status_to_canonical_code(status_code))) + span.set_status(Status(http_status_to_status_code(status_code))) def get_default_span_details(scope: dict) -> Tuple[str, dict]: diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py index 6af816b39d..2f4ecaf3af 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py @@ -43,21 +43,11 @@ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import SpanKind -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode _APPLIED = "_opentelemetry_tracer" -def _exception_to_canonical_code(exc: Exception) -> StatusCanonicalCode: - if isinstance( - exc, (exceptions.InterfaceError, exceptions.SyntaxOrAccessError), - ): - return StatusCanonicalCode.INVALID_ARGUMENT - if isinstance(exc, exceptions.IdleInTransactionSessionTimeoutError): - return StatusCanonicalCode.DEADLINE_EXCEEDED - return StatusCanonicalCode.UNKNOWN - - def _hydrate_span_from_args(connection, query, parameters) -> dict: span_attributes = {"db.type": "sql"} @@ -134,12 +124,7 @@ async def _do_execute(self, func, instance, args, kwargs): exception = exc raise finally: - if span.is_recording(): - if exception is not None: - span.set_status( - Status(_exception_to_canonical_code(exception)) - ) - else: - span.set_status(Status(StatusCanonicalCode.OK)) + if span.is_recording() and exception is not None: + span.set_status(Status(StatusCode.ERROR)) return result diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py index ab0cdf39b1..a5897f8b6e 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py @@ -68,7 +68,7 @@ def add(x, y): from opentelemetry.instrumentation.celery.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.trace.propagation import get_current_span -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode logger = logging.getLogger(__name__) @@ -214,7 +214,7 @@ def _trace_failure(*args, **kwargs): if span is None or not span.is_recording(): return - status_kwargs = {"canonical_code": StatusCanonicalCode.UNKNOWN} + status_kwargs = {"status_code": StatusCode.ERROR} ex = kwargs.get("einfo") @@ -227,7 +227,6 @@ def _trace_failure(*args, **kwargs): if ex is not None: status_kwargs["description"] = str(ex) - span.set_status(Status(**status_kwargs)) @staticmethod diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py index 0dcdd5ba60..0047ab1851 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -50,7 +50,7 @@ from opentelemetry.instrumentation.dbapi.version import __version__ from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import SpanKind, TracerProvider, get_tracer -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode logger = logging.getLogger(__name__) @@ -343,14 +343,10 @@ def traced_execution( self._populate_span(span, *args) try: result = query_method(*args, **kwargs) - if span.is_recording(): - span.set_status(Status(StatusCanonicalCode.OK)) return result except Exception as ex: # pylint: disable=broad-except if span.is_recording(): - span.set_status( - Status(StatusCanonicalCode.UNKNOWN, str(ex)) - ) + span.set_status(Status(StatusCode.ERROR, str(ex))) raise ex diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py index e342e15aa3..f2abb8b6dc 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py @@ -65,8 +65,7 @@ def test_span_succeeded(self): self.assertEqual(span.attributes["net.peer.name"], "testhost") self.assertEqual(span.attributes["net.peer.port"], 123) self.assertIs( - span.status.canonical_code, - trace_api.status.StatusCanonicalCode.OK, + span.status.status_code, trace_api.status.StatusCode.UNSET, ) def test_span_not_recording(self): @@ -117,8 +116,7 @@ def test_span_failed(self): span = spans_list[0] self.assertEqual(span.attributes["db.statement"], "Test query") self.assertIs( - span.status.canonical_code, - trace_api.status.StatusCanonicalCode.UNKNOWN, + span.status.status_code, trace_api.status.StatusCode.ERROR, ) self.assertEqual(span.status.description, "Test Exception") diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 4aa794f0de..d087dc2d9d 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -27,7 +27,7 @@ from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import SpanKind -from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.trace.status import StatusCode from opentelemetry.util import ExcludeList # pylint: disable=import-error @@ -87,7 +87,7 @@ def test_templated_route_get(self): else "tests.views.traced", ) self.assertEqual(span.kind, SpanKind.SERVER) - self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes["http.method"], "GET") self.assertEqual( span.attributes["http.url"], @@ -113,7 +113,7 @@ def test_traced_get(self): span.name, "^traced/" if DJANGO_2_2 else "tests.views.traced" ) self.assertEqual(span.kind, SpanKind.SERVER) - self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes["http.method"], "GET") self.assertEqual( span.attributes["http.url"], "http://testserver/traced/" @@ -170,7 +170,7 @@ def test_traced_post(self): span.name, "^traced/" if DJANGO_2_2 else "tests.views.traced" ) self.assertEqual(span.kind, SpanKind.SERVER) - self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes["http.method"], "POST") self.assertEqual( span.attributes["http.url"], "http://testserver/traced/" @@ -193,9 +193,7 @@ def test_error(self): span.name, "^error/" if DJANGO_2_2 else "tests.views.error" ) self.assertEqual(span.kind, SpanKind.SERVER) - self.assertEqual( - span.status.canonical_code, StatusCanonicalCode.INTERNAL - ) + self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(span.attributes["http.method"], "GET") self.assertEqual( span.attributes["http.url"], "http://testserver/error/" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py index 6e9f411f8a..541cdbfa6e 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py @@ -64,7 +64,7 @@ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import SpanKind, get_tracer -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode logger = getLogger(__name__) @@ -156,11 +156,7 @@ def wrapper(wrapped, _, args, kwargs): return rv except Exception as ex: # pylint: disable=broad-except if span.is_recording(): - if isinstance(ex, elasticsearch.exceptions.NotFoundError): - status = StatusCanonicalCode.NOT_FOUND - else: - status = StatusCanonicalCode.UNKNOWN - span.set_status(Status(status, str(ex))) + span.set_status(Status(StatusCode.ERROR, str(ex))) raise ex return wrapper diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py index 3d93838fe8..ea0e6ce2fb 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py @@ -27,7 +27,7 @@ ElasticsearchInstrumentor, ) from opentelemetry.test.test_base import TestBase -from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.trace.status import StatusCode major_version = elasticsearch.VERSION[0] @@ -153,14 +153,14 @@ def test_result_values(self, request_mock): def test_trace_error_unknown(self, request_mock): exc = RuntimeError("custom error") request_mock.side_effect = exc - self._test_trace_error(StatusCanonicalCode.UNKNOWN, exc) + self._test_trace_error(StatusCode.ERROR, exc) def test_trace_error_not_found(self, request_mock): msg = "record not found" exc = elasticsearch.exceptions.NotFoundError(404, msg) request_mock.return_value = (1, {}, {}) request_mock.side_effect = exc - self._test_trace_error(StatusCanonicalCode.NOT_FOUND, exc) + self._test_trace_error(StatusCode.ERROR, exc) def _test_trace_error(self, code, exc): es = Elasticsearch() @@ -173,7 +173,7 @@ def _test_trace_error(self, code, exc): self.assertEqual(1, len(spans)) span = spans[0] self.assertFalse(span.status.is_ok) - self.assertEqual(span.status.canonical_code, code) + self.assertEqual(span.status.status_code, code) self.assertEqual(span.status.description, str(exc)) def test_parent(self, request_mock): diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py index 0a93fe0138..66e6563dff 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py @@ -55,7 +55,7 @@ def on_get(self, req, resp): from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import ( extract_attributes_from_object, - http_status_to_canonical_code, + http_status_to_status_code, ) from opentelemetry.trace.status import Status from opentelemetry.util import ExcludeList, time_ns @@ -216,7 +216,7 @@ def process_response( span.set_attribute("http.status_code", status_code) span.set_status( Status( - canonical_code=http_status_to_canonical_code(status_code), + status_code=http_status_to_status_code(status_code), description=reason, ) ) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py index d64154a777..fe33a2f2dd 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py @@ -18,7 +18,7 @@ from opentelemetry.instrumentation.falcon import FalconInstrumentor from opentelemetry.test.test_base import TestBase -from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.trace.status import StatusCode from opentelemetry.util import ExcludeList from .app import make_app @@ -64,7 +64,7 @@ def _test_method(self, method): self.assertEqual( span.name, "HelloWorldResource.on_{0}".format(method.lower()) ) - self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assert_span_has_attributes( span, { @@ -91,9 +91,7 @@ def test_404(self): self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "HTTP GET") - self.assertEqual( - span.status.canonical_code, StatusCanonicalCode.NOT_FOUND - ) + self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assert_span_has_attributes( span, { @@ -122,9 +120,7 @@ def test_500(self): span = spans[0] self.assertEqual(span.name, "ErrorResource.on_get") self.assertFalse(span.status.is_ok) - self.assertEqual( - span.status.canonical_code, StatusCanonicalCode.INTERNAL - ) + self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual( span.status.description, "NameError: name 'non_existent_var' is not defined", diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py index 028804f599..f8a72931f9 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py @@ -26,7 +26,7 @@ from opentelemetry import metrics, propagators, trace from opentelemetry.sdk.metrics.export.controller import PushController -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode from . import grpcext from ._utilities import RpcInfo, TimedMetricRecorder @@ -169,9 +169,9 @@ def intercept_unary(self, request, metadata, client_info, invoker): try: result = invoker(request, metadata) - except grpc.RpcError as exc: + except grpc.RpcError: guarded_span.generated_span.set_status( - Status(StatusCanonicalCode(exc.code().value[0])) + Status(StatusCode.ERROR) ) raise @@ -224,10 +224,8 @@ def _intercept_server_stream( response.ByteSize(), client_info.full_method ) yield response - except grpc.RpcError as exc: - span.set_status( - Status(StatusCanonicalCode(exc.code().value[0])) - ) + except grpc.RpcError: + span.set_status(Status(StatusCode.ERROR)) raise def intercept_stream( @@ -264,9 +262,9 @@ def intercept_stream( try: result = invoker(request_or_iterator, metadata) - except grpc.RpcError as exc: + except grpc.RpcError: guarded_span.generated_span.set_status( - Status(StatusCanonicalCode(exc.code().value[0])) + Status(StatusCode.ERROR) ) raise diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py index 3ed40c141c..f351c6fdd4 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py @@ -220,9 +220,8 @@ def test_error_simple(self): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] - self.assertEqual( - span.status.canonical_code.value, - grpc.StatusCode.INVALID_ARGUMENT.value[0], + self.assertIs( + span.status.status_code, trace.status.StatusCode.ERROR, ) def test_error_stream_unary(self): @@ -233,9 +232,8 @@ def test_error_stream_unary(self): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] - self.assertEqual( - span.status.canonical_code.value, - grpc.StatusCode.INVALID_ARGUMENT.value[0], + self.assertIs( + span.status.status_code, trace.status.StatusCode.ERROR, ) def test_error_unary_stream(self): @@ -247,9 +245,8 @@ def test_error_unary_stream(self): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] - self.assertEqual( - span.status.canonical_code.value, - grpc.StatusCode.INVALID_ARGUMENT.value[0], + self.assertIs( + span.status.status_code, trace.status.StatusCode.ERROR, ) def test_error_stream_stream(self): @@ -263,9 +260,8 @@ def test_error_stream_stream(self): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] - self.assertEqual( - span.status.canonical_code.value, - grpc.StatusCode.INVALID_ARGUMENT.value[0], + self.assertIs( + span.status.status_code, trace.status.StatusCode.ERROR, ) diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py index 4123f7de52..63f23ae79b 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py @@ -52,7 +52,7 @@ from opentelemetry.instrumentation.jinja2.version import __version__ from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import SpanKind, get_tracer -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode logger = logging.getLogger(__name__) diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py index b38bedf3fd..4efb50eb4f 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py @@ -26,7 +26,6 @@ from opentelemetry.instrumentation.pymemcache import PymemcacheInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer -from opentelemetry.trace.status import StatusCanonicalCode from .utils import MockSocket, _str @@ -278,7 +277,7 @@ def _delete(): span = spans[0] - self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["delete key"]) @@ -304,7 +303,7 @@ def _incr(): span = spans[0] - self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["incr key"]) @@ -321,7 +320,7 @@ def _get(): span = spans[0] - self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["get key"]) @@ -338,7 +337,7 @@ def _get(): span = spans[0] - self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["get key"]) @@ -373,7 +372,7 @@ def _set(): span = spans[0] - self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["set key"]) @@ -390,7 +389,7 @@ def _set(): span = spans[0] - self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["set key"]) @@ -407,7 +406,7 @@ def _set(): span = spans[0] - self.assertNotEqual(span.status.canonical_code, StatusCanonicalCode.OK) + self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["set key has space"]) diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py index bb20fd5442..adc51b1c84 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py @@ -46,7 +46,7 @@ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.pymongo.version import __version__ from opentelemetry.trace import SpanKind, get_tracer -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode DATABASE_TYPE = "mongodb" COMMAND_ATTRIBUTES = ["filter", "sort", "skip", "limit", "pipeline"] @@ -92,11 +92,8 @@ def started(self, event: monitoring.CommandStartedEvent): # Add Span to dictionary self._span_dict[_get_span_dict_key(event)] = span except Exception as ex: # noqa pylint: disable=broad-except - if span is not None: - if span.is_recording(): - span.set_status( - Status(StatusCanonicalCode.INTERNAL, str(ex)) - ) + if span is not None and span.is_recording(): + span.set_status(Status(StatusCode.ERROR, str(ex))) span.end() self._pop_span(event) @@ -111,7 +108,6 @@ def succeeded(self, event: monitoring.CommandSucceededEvent): span.set_attribute( "db.mongo.duration_micros", event.duration_micros ) - span.set_status(Status(StatusCanonicalCode.OK, event.reply)) span.end() def failed(self, event: monitoring.CommandFailedEvent): @@ -125,7 +121,7 @@ def failed(self, event: monitoring.CommandFailedEvent): span.set_attribute( "db.mongo.duration_micros", event.duration_micros ) - span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure)) + span.set_status(Status(StatusCode.ERROR, event.failure)) span.end() def _pop_span(self, event): diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py b/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py index d5f67cafe8..a3bb7b2223 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py @@ -86,9 +86,8 @@ def test_succeeded(self): span.attributes["db.mongo.duration_micros"], "duration_micros" ) self.assertIs( - span.status.canonical_code, trace_api.status.StatusCanonicalCode.OK + span.status.status_code, trace_api.status.StatusCode.UNSET ) - self.assertEqual(span.status.description, "reply") self.assertIsNotNone(span.end_time) def test_not_recording(self): @@ -121,8 +120,7 @@ def test_failed(self): span.attributes["db.mongo.duration_micros"], "duration_micros" ) self.assertIs( - span.status.canonical_code, - trace_api.status.StatusCanonicalCode.UNKNOWN, + span.status.status_code, trace_api.status.StatusCode.ERROR, ) self.assertEqual(span.status.description, "failure") self.assertIsNotNone(span.end_time) @@ -143,15 +141,13 @@ def test_multiple_commands(self): self.assertEqual(first_span.attributes["db.mongo.request_id"], "first") self.assertIs( - first_span.status.canonical_code, - trace_api.status.StatusCanonicalCode.OK, + first_span.status.status_code, trace_api.status.StatusCode.UNSET, ) self.assertEqual( second_span.attributes["db.mongo.request_id"], "second" ) self.assertIs( - second_span.status.canonical_code, - trace_api.status.StatusCanonicalCode.UNKNOWN, + second_span.status.status_code, trace_api.status.StatusCode.ERROR, ) def test_int_command(self): diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index 770eacf5e2..b4738647db 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -49,12 +49,12 @@ MetricMixin, ) from opentelemetry.instrumentation.requests.version import __version__ -from opentelemetry.instrumentation.utils import http_status_to_canonical_code +from opentelemetry.instrumentation.utils import http_status_to_status_code from opentelemetry.trace import SpanKind, get_tracer from opentelemetry.trace.status import ( EXCEPTION_STATUS_FIELD, Status, - StatusCanonicalCode, + StatusCode, ) # A key to a context variable to avoid creating duplicate spans when instrumenting @@ -155,9 +155,7 @@ def _instrumented_requests_call( except Exception as exc: # pylint: disable=W0703 exception = exc setattr( - exception, - EXCEPTION_STATUS_FIELD, - _exception_to_canonical_code(exception), + exception, EXCEPTION_STATUS_FIELD, StatusCode.ERROR, ) result = getattr(exc, "response", None) finally: @@ -171,9 +169,7 @@ def _instrumented_requests_call( span.set_attribute("http.status_text", result.reason) span.set_status( Status( - http_status_to_canonical_code( - result.status_code - ) + http_status_to_status_code(result.status_code) ) ) labels["http.status_code"] = str(result.status_code) @@ -221,17 +217,6 @@ def _uninstrument_from(instr_root, restore_as_bound_func=False): setattr(instr_root, instr_func_name, original) -def _exception_to_canonical_code(exc: Exception) -> StatusCanonicalCode: - if isinstance( - exc, - (InvalidURL, InvalidSchema, MissingSchema, URLRequired, ValueError), - ): - return StatusCanonicalCode.INVALID_ARGUMENT - if isinstance(exc, Timeout): - return StatusCanonicalCode.DEADLINE_EXCEEDED - return StatusCanonicalCode.UNKNOWN - - class RequestsInstrumentor(BaseInstrumentor, MetricMixin): """An instrumentor for requests See `BaseInstrumentor` diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py index f41e597b23..f5209108e3 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py @@ -25,7 +25,7 @@ from opentelemetry.sdk.util import get_dict_as_key from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase -from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.trace.status import StatusCode class RequestsIntegrationTestBase(abc.ABC): @@ -81,9 +81,7 @@ def test_basic(self): }, ) - self.assertIs( - span.status.canonical_code, trace.status.StatusCanonicalCode.OK - ) + self.assertIs(span.status.status_code, trace.status.StatusCode.UNSET) self.check_span_instrumentation_info( span, opentelemetry.instrumentation.requests @@ -123,8 +121,7 @@ def test_not_foundbasic(self): self.assertEqual(span.attributes.get("http.status_text"), "Not Found") self.assertIs( - span.status.canonical_code, - trace.status.StatusCanonicalCode.NOT_FOUND, + span.status.status_code, trace.status.StatusCode.ERROR, ) def test_uninstrument(self): @@ -263,9 +260,7 @@ def test_requests_exception_without_response(self, *_, **__): span.attributes, {"component": "http", "http.method": "GET", "http.url": self.URL}, ) - self.assertEqual( - span.status.canonical_code, StatusCanonicalCode.UNKNOWN - ) + self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertIsNotNone(RequestsInstrumentor().meter) self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1) @@ -307,9 +302,7 @@ def test_requests_exception_with_response(self, *_, **__): "http.status_text": "Internal Server Error", }, ) - self.assertEqual( - span.status.canonical_code, StatusCanonicalCode.INTERNAL - ) + self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertIsNotNone(RequestsInstrumentor().meter) self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1) recorder = RequestsInstrumentor().meter.metrics.pop() @@ -334,9 +327,7 @@ def test_requests_basic_exception(self, *_, **__): self.perform_request(self.URL) span = self.assert_span() - self.assertEqual( - span.status.canonical_code, StatusCanonicalCode.UNKNOWN - ) + self.assertEqual(span.status.status_code, StatusCode.ERROR) @mock.patch( "requests.adapters.HTTPAdapter.send", side_effect=requests.Timeout @@ -346,9 +337,7 @@ def test_requests_timeout_exception(self, *_, **__): self.perform_request(self.URL) span = self.assert_span() - self.assertEqual( - span.status.canonical_code, StatusCanonicalCode.DEADLINE_EXCEEDED - ) + self.assertEqual(span.status.status_code, StatusCode.ERROR) class TestRequestsIntegration(RequestsIntegrationTestBase, TestBase): @@ -371,9 +360,7 @@ def test_invalid_url(self): span.attributes, {"component": "http", "http.method": "POST", "http.url": url}, ) - self.assertEqual( - span.status.canonical_code, StatusCanonicalCode.INVALID_ARGUMENT - ) + self.assertEqual(span.status.status_code, StatusCode.ERROR) def test_if_headers_equals_none(self): result = requests.get(self.URL, headers=None) diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py index 83a5b82b23..7c97c685da 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py @@ -16,7 +16,7 @@ from opentelemetry import trace from opentelemetry.instrumentation.sqlalchemy.version import __version__ -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode # Network attribute semantic convention here: # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/span-general.md#general-network-connection-attributes @@ -112,10 +112,7 @@ def _handle_error(self, context): try: if self.current_span.is_recording(): self.current_span.set_status( - Status( - StatusCanonicalCode.UNKNOWN, - str(context.original_exception), - ) + Status(StatusCode.ERROR, str(context.original_exception),) ) finally: self.current_span.end() diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py index f6ae8b321d..9a7959ab95 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py @@ -51,7 +51,7 @@ def get(self): from opentelemetry.instrumentation.tornado.version import __version__ from opentelemetry.instrumentation.utils import ( extract_attributes_from_object, - http_status_to_canonical_code, + http_status_to_status_code, unwrap, ) from opentelemetry.trace.status import Status @@ -269,7 +269,7 @@ def _finish_span(tracer, handler, error=None): ctx.span.set_attribute("http.status_code", status_code) ctx.span.set_status( Status( - canonical_code=http_status_to_canonical_code(status_code), + status_code=http_status_to_status_code(status_code), description=reason, ) ) diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py index 12330c0919..5ec001bdab 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py @@ -3,7 +3,7 @@ from tornado.httpclient import HTTPError, HTTPRequest from opentelemetry import propagators, trace -from opentelemetry.instrumentation.utils import http_status_to_canonical_code +from opentelemetry.instrumentation.utils import http_status_to_status_code from opentelemetry.trace.status import Status from opentelemetry.util import time_ns @@ -74,7 +74,7 @@ def _finish_tracing_callback(future, span): span.set_attribute("http.status_code", status_code) span.set_status( Status( - canonical_code=http_status_to_canonical_code(status_code), + status_code=http_status_to_status_code(status_code), description=description, ) ) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index 56c8b755c5..62eef43251 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -59,9 +59,9 @@ def hello(): import wsgiref.util as wsgiref_util from opentelemetry import context, propagators, trace -from opentelemetry.instrumentation.utils import http_status_to_canonical_code +from opentelemetry.instrumentation.utils import http_status_to_status_code from opentelemetry.instrumentation.wsgi.version import __version__ -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode _HTTP_VERSION_PREFIX = "HTTP/" @@ -146,13 +146,13 @@ def add_response_attributes( except ValueError: span.set_status( Status( - StatusCanonicalCode.UNKNOWN, + StatusCode.ERROR, "Non-integer HTTP status: " + repr(status_code), ) ) else: span.set_attribute("http.status_code", status_code) - span.set_status(Status(http_status_to_canonical_code(status_code))) + span.set_status(Status(http_status_to_status_code(status_code))) def get_default_span_name(environ): @@ -217,7 +217,7 @@ def __call__(self, environ, start_response): ) except Exception as ex: if span.is_recording(): - span.set_status(Status(StatusCanonicalCode.INTERNAL, str(ex))) + span.set_status(Status(StatusCode.ERROR, str(ex))) span.end() context.detach(token) raise diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index baab50d96b..144b4cf069 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -21,7 +21,7 @@ import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry import trace as trace_api from opentelemetry.test.wsgitestutil import WsgiTestBase -from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.trace.status import StatusCode class Response: @@ -177,7 +177,7 @@ def test_wsgi_internal_error(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual( - span_list[0].status.canonical_code, StatusCanonicalCode.INTERNAL, + span_list[0].status.status_code, StatusCode.ERROR, ) def test_override_span_name(self): diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 5f57631832..977dd6375c 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Updating status codes to adhere to specs ([#1282](https://github.com/open-telemetry/opentelemetry-python/pull/1282)) + ## Version 0.14b0 Released 2020-10-13 diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index f10cb0312a..d747734629 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -268,9 +268,9 @@ def start_span( start_time: Sets the start time of a span set_status_on_exception: Only relevant if the returned span is used in a with/context manager. Defines wether the span status will - be automatically set to UNKNOWN when an uncaught exception is + be automatically set to ERROR when an uncaught exception is raised in the span with block. The span status won't be set by - this mechanism if it was previousy set manually. + this mechanism if it was previously set manually. Returns: The newly-created span. diff --git a/opentelemetry-api/src/opentelemetry/trace/status.py b/opentelemetry-api/src/opentelemetry/trace/status.py index 9df5493e3b..7089793579 100644 --- a/opentelemetry-api/src/opentelemetry/trace/status.py +++ b/opentelemetry-api/src/opentelemetry/trace/status.py @@ -22,157 +22,34 @@ EXCEPTION_STATUS_FIELD = "_otel_status_code" -class StatusCanonicalCode(enum.Enum): +class StatusCode(enum.Enum): """Represents the canonical set of status codes of a finished Span.""" OK = 0 - """Not an error, returned on success.""" + """The operation has been validated by an Application developer or Operator to have completed successfully.""" - CANCELLED = 1 - """The operation was cancelled, typically by the caller.""" + UNSET = 1 + """The default status.""" - UNKNOWN = 2 - """Unknown error. - - For example, this error may be returned when a Status value received from - another address space belongs to an error space that is not known in this - address space. Also errors raised by APIs that do not return enough error - information may be converted to this error. - """ - - INVALID_ARGUMENT = 3 - """The client specified an invalid argument. - - Note that this differs from FAILED_PRECONDITION. INVALID_ARGUMENT indicates - arguments that are problematic regardless of the state of the system (e.g., - a malformed file name). - """ - - DEADLINE_EXCEEDED = 4 - """The deadline expired before the operation could complete. - - For operations that change the state of the system, this error may be - returned even if the operation has completed successfully. For example, a - successful response from a server could have been delayed long - """ - - NOT_FOUND = 5 - """Some requested entity (e.g., file or directory) was not found. - - Note to server developers: if a request is denied for an entire class of - users, such as gradual feature rollout or undocumented whitelist, NOT_FOUND - may be used. If a request is denied for some users within a class of users, - such as user-based access control, PERMISSION_DENIED must be used. - """ - - ALREADY_EXISTS = 6 - """The entity that a client attempted to create (e.g., file or directory) - already exists. - """ - - PERMISSION_DENIED = 7 - """The caller does not have permission to execute the specified operation. - - PERMISSION_DENIED must not be used for rejections caused by exhausting some - resource (use RESOURCE_EXHAUSTED instead for those errors). - PERMISSION_DENIED must not be used if the caller can not be identified (use - UNAUTHENTICATED instead for those errors). This error code does not imply - the request is valid or the requested entity exists or satisfies other - pre-conditions. - """ - - RESOURCE_EXHAUSTED = 8 - """Some resource has been exhausted, perhaps a per-user quota, or perhaps - the entire file system is out of space. - """ - - FAILED_PRECONDITION = 9 - """The operation was rejected because the system is not in a state required - for the operation's execution. - - For example, the directory to be deleted is non-empty, an rmdir operation - is applied to a non-directory, etc. Service implementors can use the - following guidelines to decide between FAILED_PRECONDITION, ABORTED, and - UNAVAILABLE: - - (a) Use UNAVAILABLE if the client can retry just the failing call. - (b) Use ABORTED if the client should retry at a higher level (e.g., - when a client-specified test-and-set fails, indicating the client - should restart a read-modify-write sequence). - (c) Use FAILED_PRECONDITION if the client should not retry until the - system state has been explicitly fixed. - - E.g., if an "rmdir" fails because the directory is non-empty, - FAILED_PRECONDITION should be returned since the client should not retry - unless the files are deleted from the directory. - """ - - ABORTED = 10 - """The operation was aborted, typically due to a concurrency issue such as a - sequencer check failure or transaction abort. - - See the guidelines above for deciding between FAILED_PRECONDITION, ABORTED, - and UNAVAILABLE. - """ - - OUT_OF_RANGE = 11 - """The operation was attempted past the valid range. - - E.g., seeking or reading past end-of-file. Unlike INVALID_ARGUMENT, this - error indicates a problem that may be fixed if the system state changes. - For example, a 32-bit file system will generate INVALID_ARGUMENT if asked - to read at an offset that is not in the range [0,2^32-1],but it will - generate OUT_OF_RANGE if asked to read from an offset past the current file - size. There is a fair bit of overlap between FAILED_PRECONDITION and - OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific error) - when it applies so that callers who are iterating through a space can - easily look for an OUT_OF_RANGE error to detect when they are done. - """ - - UNIMPLEMENTED = 12 - """The operation is not implemented or is not supported/enabled in this - service. - """ - - INTERNAL = 13 - """Internal errors. - - This means that some invariants expected by the underlying system have been - broken. This error code is reserved for serious errors. - """ - - UNAVAILABLE = 14 - """The service is currently unavailable. - - This is most likely a transient condition, which can be corrected by - retrying with a backoff. Note that it is not always safe to retry - non-idempotent operations. - """ - - DATA_LOSS = 15 - """Unrecoverable data loss or corruption.""" - - UNAUTHENTICATED = 16 - """The request does not have valid authentication credentials for the - operation. - """ + ERROR = 2 + """The operation contains an error.""" class Status: """Represents the status of a finished Span. Args: - canonical_code: The canonical status code that describes the result + status_code: The canonical status code that describes the result status of the operation. description: An optional description of the status. """ def __init__( self, - canonical_code: StatusCanonicalCode = StatusCanonicalCode.OK, + status_code: StatusCode = StatusCode.UNSET, description: typing.Optional[str] = None, ): - self._canonical_code = canonical_code + self._status_code = status_code self._description = None if description is not None and not isinstance(description, str): logger.warning("Invalid status description type, expected str") @@ -180,9 +57,9 @@ def __init__( self._description = description @property - def canonical_code(self) -> StatusCanonicalCode: + def status_code(self) -> StatusCode: """Represents the canonical status code of a finished Span.""" - return self._canonical_code + return self._status_code @property def description(self) -> typing.Optional[str]: @@ -192,4 +69,9 @@ def description(self) -> typing.Optional[str]: @property def is_ok(self) -> bool: """Returns false if this represents an error, true otherwise.""" - return self._canonical_code is StatusCanonicalCode.OK + return self.is_unset or self._status_code is StatusCode.OK + + @property + def is_unset(self) -> bool: + """Returns true if unset, false otherwise.""" + return self._status_code is StatusCode.UNSET diff --git a/opentelemetry-api/tests/trace/test_status.py b/opentelemetry-api/tests/trace/test_status.py index 6c086f3ae0..cd0f678d13 100644 --- a/opentelemetry-api/tests/trace/test_status.py +++ b/opentelemetry-api/tests/trace/test_status.py @@ -15,21 +15,21 @@ import unittest from logging import WARNING -from opentelemetry.trace.status import Status, StatusCanonicalCode +from opentelemetry.trace.status import Status, StatusCode class TestStatus(unittest.TestCase): def test_constructor(self): status = Status() - self.assertIs(status.canonical_code, StatusCanonicalCode.OK) + self.assertIs(status.status_code, StatusCode.UNSET) self.assertIsNone(status.description) - status = Status(StatusCanonicalCode.UNAVAILABLE, "unavailable") - self.assertIs(status.canonical_code, StatusCanonicalCode.UNAVAILABLE) + status = Status(StatusCode.ERROR, "unavailable") + self.assertIs(status.status_code, StatusCode.ERROR) self.assertEqual(status.description, "unavailable") def test_invalid_description(self): with self.assertLogs(level=WARNING): status = Status(description={"test": "val"}) # type: ignore - self.assertIs(status.canonical_code, StatusCanonicalCode.OK) + self.assertIs(status.status_code, StatusCode.UNSET) self.assertEqual(status.description, None) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py index 6220854ad5..22ba7a2057 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -16,7 +16,7 @@ from wrapt import ObjectProxy -from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.trace.status import StatusCode def extract_attributes_from_object( @@ -32,42 +32,43 @@ def extract_attributes_from_object( return extracted -def http_status_to_canonical_code( +def http_status_to_status_code( status: int, allow_redirect: bool = True -) -> StatusCanonicalCode: +) -> StatusCode: """Converts an HTTP status code to an OpenTelemetry canonical status code Args: status (int): HTTP status code """ # pylint:disable=too-many-branches,too-many-return-statements + # See: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#status if status < 100: - return StatusCanonicalCode.UNKNOWN + return StatusCode.ERROR if status <= 299: - return StatusCanonicalCode.OK + return StatusCode.UNSET if status <= 399: if allow_redirect: - return StatusCanonicalCode.OK - return StatusCanonicalCode.DEADLINE_EXCEEDED + return StatusCode.UNSET + return StatusCode.ERROR if status <= 499: if status == 401: # HTTPStatus.UNAUTHORIZED: - return StatusCanonicalCode.UNAUTHENTICATED + return StatusCode.ERROR if status == 403: # HTTPStatus.FORBIDDEN: - return StatusCanonicalCode.PERMISSION_DENIED + return StatusCode.ERROR if status == 404: # HTTPStatus.NOT_FOUND: - return StatusCanonicalCode.NOT_FOUND + return StatusCode.ERROR if status == 429: # HTTPStatus.TOO_MANY_REQUESTS: - return StatusCanonicalCode.RESOURCE_EXHAUSTED - return StatusCanonicalCode.INVALID_ARGUMENT + return StatusCode.ERROR + return StatusCode.ERROR if status <= 599: if status == 501: # HTTPStatus.NOT_IMPLEMENTED: - return StatusCanonicalCode.UNIMPLEMENTED + return StatusCode.ERROR if status == 503: # HTTPStatus.SERVICE_UNAVAILABLE: - return StatusCanonicalCode.UNAVAILABLE + return StatusCode.ERROR if status == 504: # HTTPStatus.GATEWAY_TIMEOUT: - return StatusCanonicalCode.DEADLINE_EXCEEDED - return StatusCanonicalCode.INTERNAL - return StatusCanonicalCode.UNKNOWN + return StatusCode.ERROR + return StatusCode.ERROR + return StatusCode.ERROR def unwrap(obj, attr: str): diff --git a/opentelemetry-instrumentation/tests/test_utils.py b/opentelemetry-instrumentation/tests/test_utils.py index 660a6bbe86..edd2320473 100644 --- a/opentelemetry-instrumentation/tests/test_utils.py +++ b/opentelemetry-instrumentation/tests/test_utils.py @@ -14,43 +14,32 @@ from http import HTTPStatus -from opentelemetry.instrumentation.utils import http_status_to_canonical_code +from opentelemetry.instrumentation.utils import http_status_to_status_code from opentelemetry.test.test_base import TestBase -from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.trace.status import StatusCode class TestUtils(TestBase): - def test_http_status_to_canonical_code(self): + # See https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#status + def test_http_status_to_status_code(self): for status_code, expected in ( - (HTTPStatus.OK, StatusCanonicalCode.OK), - (HTTPStatus.ACCEPTED, StatusCanonicalCode.OK), - (HTTPStatus.IM_USED, StatusCanonicalCode.OK), - (HTTPStatus.MULTIPLE_CHOICES, StatusCanonicalCode.OK), - (HTTPStatus.BAD_REQUEST, StatusCanonicalCode.INVALID_ARGUMENT), - (HTTPStatus.UNAUTHORIZED, StatusCanonicalCode.UNAUTHENTICATED), - (HTTPStatus.FORBIDDEN, StatusCanonicalCode.PERMISSION_DENIED), - (HTTPStatus.NOT_FOUND, StatusCanonicalCode.NOT_FOUND), - ( - HTTPStatus.UNPROCESSABLE_ENTITY, - StatusCanonicalCode.INVALID_ARGUMENT, - ), - ( - HTTPStatus.TOO_MANY_REQUESTS, - StatusCanonicalCode.RESOURCE_EXHAUSTED, - ), - (HTTPStatus.NOT_IMPLEMENTED, StatusCanonicalCode.UNIMPLEMENTED), - (HTTPStatus.SERVICE_UNAVAILABLE, StatusCanonicalCode.UNAVAILABLE), - ( - HTTPStatus.GATEWAY_TIMEOUT, - StatusCanonicalCode.DEADLINE_EXCEEDED, - ), - ( - HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, - StatusCanonicalCode.INTERNAL, - ), - (600, StatusCanonicalCode.UNKNOWN), - (99, StatusCanonicalCode.UNKNOWN), + (HTTPStatus.OK, StatusCode.UNSET), + (HTTPStatus.ACCEPTED, StatusCode.UNSET), + (HTTPStatus.IM_USED, StatusCode.UNSET), + (HTTPStatus.MULTIPLE_CHOICES, StatusCode.UNSET), + (HTTPStatus.BAD_REQUEST, StatusCode.ERROR), + (HTTPStatus.UNAUTHORIZED, StatusCode.ERROR), + (HTTPStatus.FORBIDDEN, StatusCode.ERROR), + (HTTPStatus.NOT_FOUND, StatusCode.ERROR), + (HTTPStatus.UNPROCESSABLE_ENTITY, StatusCode.ERROR,), + (HTTPStatus.TOO_MANY_REQUESTS, StatusCode.ERROR,), + (HTTPStatus.NOT_IMPLEMENTED, StatusCode.ERROR), + (HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR), + (HTTPStatus.GATEWAY_TIMEOUT, StatusCode.ERROR,), + (HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, StatusCode.ERROR,), + (600, StatusCode.ERROR), + (99, StatusCode.ERROR), ): with self.subTest(status_code=status_code): - actual = http_status_to_canonical_code(int(status_code)) + actual = http_status_to_status_code(int(status_code)) self.assertEqual(actual, expected, status_code) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 262556132e..0bfa4147a2 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -10,6 +10,7 @@ ([#998](https://github.com/open-telemetry/opentelemetry-python/pull/998)) - Samplers to accept parent Context ([#1267](https://github.com/open-telemetry/opentelemetry-python/pull/1267)) +- Updating status codes to adhere to spec ([#1282](https://github.com/open-telemetry/opentelemetry-python/pull/1282)) ## Version 0.14b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 4da93ae502..e1c9dd17d3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -47,7 +47,7 @@ from opentelemetry.trace.status import ( EXCEPTION_STATUS_FIELD, Status, - StatusCanonicalCode, + StatusCode, ) from opentelemetry.util import time_ns, types @@ -435,7 +435,7 @@ def __init__( self._set_status_on_exception = set_status_on_exception self.span_processor = span_processor - self.status = None + self.status = Status(StatusCode.UNSET) self._lock = threading.Lock() _filter_attribute_values(attributes) @@ -546,7 +546,7 @@ def to_json(self, indent=4): if self.status is not None: status = OrderedDict() - status["canonical_code"] = str(self.status.canonical_code.name) + status["status_code"] = str(self.status.status_code.name) if self.status.description: status["description"] = self.status.description @@ -637,9 +637,6 @@ def end(self, end_time: Optional[int] = None) -> None: logger.warning("Calling end() on an ended span.") return - if self.status is None: - self.status = Status(canonical_code=StatusCanonicalCode.OK) - self._end_time = end_time if end_time is not None else time_ns() self.span_processor.on_end(self) @@ -662,15 +659,17 @@ def __exit__( exc_tb: Optional[TracebackType], ) -> None: """Ends context manager and calls `end` on the `Span`.""" - + # Records status if span is used as context manager + # i.e. with tracer.start_span() as span: + # TODO: Record exception if ( - self.status is None + self.status.status_code is StatusCode.UNSET and self._set_status_on_exception and exc_val is not None ): self.set_status( Status( - canonical_code=StatusCanonicalCode.UNKNOWN, + status_code=StatusCode.ERROR, description="{}: {}".format(exc_type.__name__, exc_val), ) ) @@ -831,13 +830,18 @@ def use_span( if record_exception: span.record_exception(error) - if span.status is None and span._set_status_on_exception: + # Records status if use_span is used + # i.e. with tracer.start_as_current_span() as span: + if ( + span.status.status_code is StatusCode.UNSET + and span._set_status_on_exception + ): span.set_status( Status( - canonical_code=getattr( + status_code=getattr( error, EXCEPTION_STATUS_FIELD, - StatusCanonicalCode.UNKNOWN, + StatusCode.ERROR, ), description="{}: {}".format( type(error).__name__, error diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 35bdf91320..d08e12dee2 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -26,7 +26,7 @@ from opentelemetry.sdk.trace import Resource, sampling from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.trace.status import StatusCode from opentelemetry.util import time_ns @@ -733,16 +733,18 @@ def test_start_span(self): span.start() self.assertEqual(start_time, span.start_time) - self.assertIs(span.status, None) + self.assertIsNotNone(span.status) + self.assertIs( + span.status.status_code, trace_api.status.StatusCode.UNSET + ) # status new_status = trace_api.status.Status( - trace_api.status.StatusCanonicalCode.CANCELLED, "Test description" + trace_api.status.StatusCode.ERROR, "Test description" ) span.set_status(new_status) self.assertIs( - span.status.canonical_code, - trace_api.status.StatusCanonicalCode.CANCELLED, + span.status.status_code, trace_api.status.StatusCode.ERROR, ) self.assertIs(span.status.description, "Test description") @@ -803,13 +805,13 @@ def test_ended_span(self): self.assertEqual(root.name, "root") new_status = trace_api.status.Status( - trace_api.status.StatusCanonicalCode.CANCELLED, "Test description" + trace_api.status.StatusCode.ERROR, "Test description" ) with self.assertLogs(level=WARNING): root.set_status(new_status) self.assertEqual( - root.status.canonical_code, trace_api.status.StatusCanonicalCode.OK + root.status.status_code, trace_api.status.StatusCode.UNSET ) def test_error_status(self): @@ -818,9 +820,7 @@ def error_status_test(context): with context as root: raise AssertionError("unknown") - self.assertIs( - root.status.canonical_code, StatusCanonicalCode.UNKNOWN - ) + self.assertIs(root.status.status_code, StatusCode.ERROR) self.assertEqual( root.status.description, "AssertionError: unknown" ) @@ -839,17 +839,12 @@ def error_status_test(context): with self.assertRaises(AssertionError): with context as root: root.set_status( - trace_api.status.Status( - StatusCanonicalCode.UNAVAILABLE, - "Error: Unavailable", - ) + trace_api.status.Status(StatusCode.OK, "OK",) ) raise AssertionError("unknown") - self.assertIs( - root.status.canonical_code, StatusCanonicalCode.UNAVAILABLE - ) - self.assertEqual(root.status.description, "Error: Unavailable") + self.assertIs(root.status.status_code, StatusCode.OK) + self.assertEqual(root.status.description, "OK") error_status_test( trace.TracerProvider().get_tracer(__name__).start_span("root") @@ -1051,6 +1046,9 @@ def test_to_json(self): "parent_id": null, "start_time": null, "end_time": null, + "status": { + "status_code": "UNSET" + }, "attributes": {}, "events": [], "links": [], @@ -1059,7 +1057,7 @@ def test_to_json(self): ) self.assertEqual( span.to_json(indent=None), - '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "{}"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "attributes": {}, "events": [], "links": [], "resource": {}}', + '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "{}"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {}, "events": [], "links": [], "resource": {}}', ) def test_attributes_to_json(self): @@ -1076,7 +1074,7 @@ def test_attributes_to_json(self): date_str = ns_to_iso_str(123) self.assertEqual( span.to_json(indent=None), - '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "{}"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "attributes": {"key": "value"}, "events": [{"name": "event", "timestamp": "' + '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "{}"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {"key": "value"}, "events": [{"name": "event", "timestamp": "' + date_str + '", "attributes": {"key2": "value2"}}], "links": [], "resource": {}}', ) diff --git a/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py b/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py index cb9080e62c..2e37efe224 100644 --- a/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py +++ b/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py @@ -5,7 +5,7 @@ from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor from opentelemetry.test.test_base import TestBase -from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.trace.status import StatusCode POSTGRES_HOST = os.getenv("POSTGRESQL_HOST ", "localhost") POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT ", "5432")) @@ -45,9 +45,7 @@ def test_instrumented_execute_method_without_arguments(self, *_, **__): async_call(self._connection.execute("SELECT 42;")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) - self.assertEqual( - StatusCanonicalCode.OK, spans[0].status.canonical_code - ) + self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.assertEqual( spans[0].attributes, { @@ -90,9 +88,7 @@ async def _transaction_execute(): }, spans[0].attributes, ) - self.assertEqual( - StatusCanonicalCode.OK, spans[0].status.canonical_code - ) + self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.assertEqual( { "db.instance": POSTGRES_DB_NAME, @@ -102,9 +98,7 @@ async def _transaction_execute(): }, spans[1].attributes, ) - self.assertEqual( - StatusCanonicalCode.OK, spans[1].status.canonical_code - ) + self.assertIs(StatusCode.UNSET, spans[1].status.status_code) self.assertEqual( { "db.instance": POSTGRES_DB_NAME, @@ -114,9 +108,7 @@ async def _transaction_execute(): }, spans[2].attributes, ) - self.assertEqual( - StatusCanonicalCode.OK, spans[2].status.canonical_code - ) + self.assertIs(StatusCode.UNSET, spans[2].status.status_code) def test_instrumented_failed_transaction_method(self, *_, **__): async def _transaction_execute(): @@ -137,9 +129,7 @@ async def _transaction_execute(): }, spans[0].attributes, ) - self.assertEqual( - StatusCanonicalCode.OK, spans[0].status.canonical_code - ) + self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.assertEqual( { "db.instance": POSTGRES_DB_NAME, @@ -150,8 +140,7 @@ async def _transaction_execute(): spans[1].attributes, ) self.assertEqual( - StatusCanonicalCode.INVALID_ARGUMENT, - spans[1].status.canonical_code, + StatusCode.ERROR, spans[1].status.status_code, ) self.assertEqual( { @@ -162,17 +151,13 @@ async def _transaction_execute(): }, spans[2].attributes, ) - self.assertEqual( - StatusCanonicalCode.OK, spans[2].status.canonical_code - ) + self.assertIs(StatusCode.UNSET, spans[2].status.status_code) def test_instrumented_method_doesnt_capture_parameters(self, *_, **__): async_call(self._connection.execute("SELECT $1;", "1")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) - self.assertEqual( - StatusCanonicalCode.OK, spans[0].status.canonical_code - ) + self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.assertEqual( spans[0].attributes, { @@ -216,9 +201,7 @@ def test_instrumented_execute_method_with_arguments(self, *_, **__): async_call(self._connection.execute("SELECT $1;", "1")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) - self.assertEqual( - StatusCanonicalCode.OK, spans[0].status.canonical_code - ) + self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.assertEqual( spans[0].attributes, { diff --git a/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py b/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py index c4be6762ea..7786890a5f 100644 --- a/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py +++ b/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py @@ -22,7 +22,7 @@ from opentelemetry.instrumentation.celery import CeleryInstrumentor from opentelemetry.sdk import resources from opentelemetry.sdk.trace import TracerProvider, export -from opentelemetry.trace.status import StatusCanonicalCode +from opentelemetry.trace.status import StatusCode # set a high timeout for async executions due to issues in CI ASYNC_GET_TIMEOUT = 120 @@ -257,7 +257,7 @@ def fn_exception(): span.attributes.get("celery.task_name") == "test_celery_functional.fn_exception" ) - assert span.status.canonical_code == StatusCanonicalCode.UNKNOWN + assert span.status.status_code == StatusCode.ERROR assert span.attributes.get("messaging.message_id") == result.task_id assert "Task class is failing" in span.status.description @@ -278,7 +278,7 @@ def fn_exception(): span = spans[0] assert span.status.is_ok is True - assert span.status.canonical_code == StatusCanonicalCode.OK + assert span.status.status_code == StatusCode.UNSET assert span.name == "run/test_celery_functional.fn_exception" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "FAILURE" @@ -305,7 +305,7 @@ def fn_exception(): span = spans[0] assert span.status.is_ok is True - assert span.status.canonical_code == StatusCanonicalCode.OK + assert span.status.status_code == StatusCode.UNSET assert span.name == "run/test_celery_functional.fn_exception" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "RETRY" @@ -377,7 +377,7 @@ def run(self): ) assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "FAILURE" - assert span.status.canonical_code == StatusCanonicalCode.UNKNOWN + assert span.status.status_code == StatusCode.ERROR assert span.attributes.get("messaging.message_id") == result.task_id assert "Task class is failing" in span.status.description @@ -406,7 +406,7 @@ def run(self): span = spans[0] assert span.status.is_ok is True - assert span.status.canonical_code == StatusCanonicalCode.OK + assert span.status.status_code == StatusCode.UNSET assert span.name == "run/test_celery_functional.BaseTask" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "FAILURE" diff --git a/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py b/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py index 64984e9c4f..8bdc120105 100644 --- a/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py +++ b/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py @@ -36,9 +36,7 @@ def tearDown(self): def _check_span(self, span): self.assertEqual(span.attributes["service"], self.test_service) self.assertEqual(span.name, "redis.command") - self.assertIs( - span.status.canonical_code, trace.status.StatusCanonicalCode.OK - ) + self.assertIs(span.status.status_code, trace.status.StatusCode.UNSET) self.assertEqual(span.attributes.get("db.instance"), 0) self.assertEqual( span.attributes.get("db.url"), "redis://localhost:6379" diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py index b2d8c0abc5..72137f83e8 100644 --- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py +++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py @@ -113,9 +113,7 @@ def _check_span(self, span): self.assertEqual(span.name, "{}.query".format(self.VENDOR)) self.assertEqual(span.attributes.get("service"), self.SERVICE) self.assertEqual(span.attributes.get(_DB), self.SQL_DB) - self.assertIs( - span.status.canonical_code, trace.status.StatusCanonicalCode.OK - ) + self.assertIs(span.status.status_code, trace.status.StatusCode.UNSET) self.assertGreater((span.end_time - span.start_time), 0) def test_orm_insert(self): diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_instrument.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_instrument.py index 20c837c03b..c408c63d94 100644 --- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_instrument.py +++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_instrument.py @@ -66,7 +66,5 @@ def test_engine_traced(self): # check subset of span fields self.assertEqual(span.name, "postgres.query") self.assertEqual(span.attributes.get("service"), "postgres") - self.assertIs( - span.status.canonical_code, trace.status.StatusCanonicalCode.OK - ) + self.assertIs(span.status.status_code, trace.status.StatusCode.UNSET) self.assertGreater((span.end_time - span.start_time), 0) diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py index 44c3501b1d..310cd91f73 100644 --- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py +++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py @@ -77,7 +77,6 @@ def test_engine_execute_errors(self): self.assertTrue(span.end_time - span.start_time > 0) # check the error self.assertIs( - span.status.canonical_code, - trace.status.StatusCanonicalCode.UNKNOWN, + span.status.status_code, trace.status.StatusCode.ERROR, ) self.assertIn("a_wrong_table", span.status.description) diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py index 615a196f5b..91fb123c97 100644 --- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py +++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py @@ -78,8 +78,7 @@ def test_engine_execute_errors(self): self.assertTrue(span.end_time - span.start_time > 0) # check the error self.assertIs( - span.status.canonical_code, - trace.status.StatusCanonicalCode.UNKNOWN, + span.status.status_code, trace.status.StatusCode.ERROR, ) self.assertIn("a_wrong_table", span.status.description) diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py index 4295fc045c..309dd73ccc 100644 --- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py +++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py @@ -53,8 +53,7 @@ def test_engine_execute_errors(self): self.assertTrue((span.end_time - span.start_time) > 0) # check the error self.assertIs( - span.status.canonical_code, - trace.status.StatusCanonicalCode.UNKNOWN, + span.status.status_code, trace.status.StatusCode.ERROR, ) self.assertEqual( span.status.description, "no such table: a_wrong_table" From bcc53c80066940d5003eb24cfebbdcc984a4dd1c Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Thu, 29 Oct 2020 08:37:07 -0700 Subject: [PATCH 0635/1517] =?UTF-8?q?Span.is=5Frecording()=20now=20based?= =?UTF-8?q?=20off=20self.=5Fend=5Ftime=20and=20returns=20False=20if?= =?UTF-8?q?=E2=80=A6=20(#1289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opentelemetry-sdk/CHANGELOG.md | 5 ++++- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 0bfa4147a2..34a622bb99 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -10,7 +10,10 @@ ([#998](https://github.com/open-telemetry/opentelemetry-python/pull/998)) - Samplers to accept parent Context ([#1267](https://github.com/open-telemetry/opentelemetry-python/pull/1267)) -- Updating status codes to adhere to spec ([#1282](https://github.com/open-telemetry/opentelemetry-python/pull/1282)) +- Updating status codes to adhere to spec + ([#1282](https://github.com/open-telemetry/opentelemetry-python/pull/1282)) +- Span.is_recording() returns false after span has ended + ([#1289](https://github.com/open-telemetry/opentelemetry-python/pull/1289)) ## Version 0.14b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index e1c9dd17d3..400600bec8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -646,7 +646,7 @@ def update_name(self, name: str) -> None: self.name = name def is_recording(self) -> bool: - return True + return self._end_time is None @_check_span_ended def set_status(self, status: trace_api.Status) -> None: From ffa3b39fe47d1395817c12626a61f4009bb920af Mon Sep 17 00:00:00 2001 From: Michael Stella Date: Thu, 29 Oct 2020 16:30:18 -0400 Subject: [PATCH 0636/1517] Rewrite gRPC server interceptor (#1171) Co-authored-by: Aaron Abbott --- .../grpc/hello_world_server.py | 6 +- .../grpc/route_guide_server.py | 7 +- .../instrumentation/grpc/__init__.py | 53 +++- .../instrumentation/grpc/_server.py | 259 ++++++++++-------- .../instrumentation/grpc/grpcext/__init__.py | 203 ++++---------- .../grpc/grpcext/_interceptor.py | 177 ------------ .../tests/test_server_interceptor.py | 12 +- 7 files changed, 256 insertions(+), 461 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py index ae4562e581..e43d38d284 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py @@ -43,7 +43,6 @@ from opentelemetry import trace from opentelemetry.instrumentation.grpc import server_interceptor -from opentelemetry.instrumentation.grpc.grpcext import intercept_server from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -73,8 +72,9 @@ def SayHello(self, request, context): def serve(): - server = grpc.server(futures.ThreadPoolExecutor()) - server = intercept_server(server, server_interceptor()) + server = grpc.server( + futures.ThreadPoolExecutor(), interceptors=[server_interceptor()], + ) helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server) server.add_insecure_port("[::]:50051") diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py index a8b2a95e81..74edbd65b3 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py @@ -46,7 +46,6 @@ from opentelemetry import trace from opentelemetry.instrumentation.grpc import server_interceptor -from opentelemetry.instrumentation.grpc.grpcext import intercept_server from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -162,8 +161,10 @@ def RouteChat(self, request_iterator, context): def serve(): - server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) - server = intercept_server(server, server_interceptor()) + server = grpc.server( + futures.ThreadPoolExecutor(max_workers=10), + interceptors=[server_interceptor()], + ) route_guide_pb2_grpc.add_RouteGuideServicer_to_server( RouteGuideServicer(), server diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py index 58d2ebbb1e..776e29e8e2 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py @@ -77,8 +77,7 @@ def run(): import grpc from opentelemetry import trace - from opentelemetry.instrumentation.grpc import GrpcInstrumentorServer, server_interceptor - from opentelemetry.instrumentation.grpc.grpcext import intercept_server + from opentelemetry.instrumentation.grpc import GrpcInstrumentorServer from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -94,10 +93,10 @@ def run(): trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) + grpc_server_instrumentor = GrpcInstrumentorServer() grpc_server_instrumentor.instrument() - class Greeter(helloworld_pb2_grpc.GreeterServicer): def SayHello(self, request, context): return helloworld_pb2.HelloReply(message="Hello, %s!" % request.name) @@ -106,7 +105,6 @@ def SayHello(self, request, context): def serve(): server = grpc.server(futures.ThreadPoolExecutor()) - server = intercept_server(server, server_interceptor()) helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server) server.add_insecure_port("[::]:50051") @@ -117,18 +115,25 @@ def serve(): if __name__ == "__main__": logging.basicConfig() serve() + +You can also add the instrumentor manually, rather than using +:py:class:`~opentelemetry.instrumentation.grpc.GrpcInstrumentorServer`: + +.. code-block:: python + + from opentelemetry.instrumentation.grpc import server_interceptor + + server = grpc.server(futures.ThreadPoolExecutor(), + interceptors = [server_interceptor()]) + """ -from contextlib import contextmanager from functools import partial import grpc from wrapt import wrap_function_wrapper as _wrap from opentelemetry import trace -from opentelemetry.instrumentation.grpc.grpcext import ( - intercept_channel, - intercept_server, -) +from opentelemetry.instrumentation.grpc.grpcext import intercept_channel from opentelemetry.instrumentation.grpc.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap @@ -140,15 +145,33 @@ def serve(): class GrpcInstrumentorServer(BaseInstrumentor): + """ + Globally instrument the grpc server. + + Usage:: + + grpc_server_instrumentor = GrpcInstrumentorServer() + grpc_server_instrumentor.instrument() + + """ + + # pylint:disable=attribute-defined-outside-init + def _instrument(self, **kwargs): - _wrap("grpc", "server", self.wrapper_fn) + self._original_func = grpc.server - def _uninstrument(self, **kwargs): - unwrap(grpc, "server") + def server(*args, **kwargs): + if "interceptors" in kwargs: + # add our interceptor as the first + kwargs["interceptors"].insert(0, server_interceptor()) + else: + kwargs["interceptors"] = [server_interceptor()] + return self._original_func(*args, **kwargs) - def wrapper_fn(self, original_func, instance, args, kwargs): - server = original_func(*args, **kwargs) - return intercept_server(server, server_interceptor()) + grpc.server = server + + def _uninstrument(self, **kwargs): + grpc.server = self._original_func class GrpcInstrumentorClient(BaseInstrumentor): diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py index cb0e997d36..83cc5824f1 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py @@ -17,12 +17,11 @@ # pylint:disable=no-member # pylint:disable=signature-differs -"""Implementation of the service-side open-telemetry interceptor. - -This library borrows heavily from the OpenTracing gRPC integration: -https://github.com/opentracing-contrib/python-grpc +""" +Implementation of the service-side open-telemetry interceptor. """ +import logging from contextlib import contextmanager from typing import List @@ -30,9 +29,37 @@ from opentelemetry import propagators, trace from opentelemetry.context import attach, detach - -from . import grpcext -from ._utilities import RpcInfo +from opentelemetry.trace.status import Status, StatusCode + +logger = logging.getLogger(__name__) + + +# wrap an RPC call +# see https://github.com/grpc/grpc/issues/18191 +def _wrap_rpc_behavior(handler, continuation): + if handler is None: + return None + + if handler.request_streaming and handler.response_streaming: + behavior_fn = handler.stream_stream + handler_factory = grpc.stream_stream_rpc_method_handler + elif handler.request_streaming and not handler.response_streaming: + behavior_fn = handler.stream_unary + handler_factory = grpc.stream_unary_rpc_method_handler + elif not handler.request_streaming and handler.response_streaming: + behavior_fn = handler.unary_stream + handler_factory = grpc.unary_stream_rpc_method_handler + else: + behavior_fn = handler.unary_unary + handler_factory = grpc.unary_unary_rpc_method_handler + + return handler_factory( + continuation( + behavior_fn, handler.request_streaming, handler.response_streaming + ), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) # pylint:disable=abstract-method @@ -42,7 +69,7 @@ def __init__(self, servicer_context, active_span): self._active_span = active_span self.code = grpc.StatusCode.OK self.details = None - super(_OpenTelemetryServicerContext, self).__init__() + super().__init__() def is_active(self, *args, **kwargs): return self._servicer_context.is_active(*args, **kwargs) @@ -56,20 +83,26 @@ def cancel(self, *args, **kwargs): def add_callback(self, *args, **kwargs): return self._servicer_context.add_callback(*args, **kwargs) + def disable_next_message_compression(self): + return self._service_context.disable_next_message_compression() + def invocation_metadata(self, *args, **kwargs): return self._servicer_context.invocation_metadata(*args, **kwargs) - def peer(self, *args, **kwargs): - return self._servicer_context.peer(*args, **kwargs) + def peer(self): + return self._servicer_context.peer() - def peer_identities(self, *args, **kwargs): - return self._servicer_context.peer_identities(*args, **kwargs) + def peer_identities(self): + return self._servicer_context.peer_identities() - def peer_identity_key(self, *args, **kwargs): - return self._servicer_context.peer_identity_key(*args, **kwargs) + def peer_identity_key(self): + return self._servicer_context.peer_identity_key() - def auth_context(self, *args, **kwargs): - return self._servicer_context.auth_context(*args, **kwargs) + def auth_context(self): + return self._servicer_context.auth_context() + + def set_compression(self, compression): + return self._servicer_context.set_compression(compression) def send_initial_metadata(self, *args, **kwargs): return self._servicer_context.send_initial_metadata(*args, **kwargs) @@ -77,47 +110,62 @@ def send_initial_metadata(self, *args, **kwargs): def set_trailing_metadata(self, *args, **kwargs): return self._servicer_context.set_trailing_metadata(*args, **kwargs) - def abort(self, *args, **kwargs): - if not hasattr(self._servicer_context, "abort"): - raise RuntimeError( - "abort() is not supported with the installed version of grpcio" - ) - return self._servicer_context.abort(*args, **kwargs) + def abort(self, code, details): + self.code = code + self.details = details + self._active_span.set_status( + Status(status_code=StatusCode(code.value[0]), description=details) + ) + return self._servicer_context.abort(code, details) - def abort_with_status(self, *args, **kwargs): - if not hasattr(self._servicer_context, "abort_with_status"): - raise RuntimeError( - "abort_with_status() is not supported with the installed " - "version of grpcio" - ) - return self._servicer_context.abort_with_status(*args, **kwargs) + def abort_with_status(self, status): + return self._servicer_context.abort_with_status(status) def set_code(self, code): self.code = code + # use details if we already have it, otherwise the status description + details = self.details or code.value[1] + self._active_span.set_status( + Status(status_code=StatusCode(code.value[0]), description=details) + ) return self._servicer_context.set_code(code) def set_details(self, details): self.details = details + self._active_span.set_status( + Status( + status_code=StatusCode(self.code.value[0]), + description=details, + ) + ) return self._servicer_context.set_details(details) -# On the service-side, errors can be signaled either by exceptions or by -# calling `set_code` on the `servicer_context`. This function checks for the -# latter and updates the span accordingly. +# pylint:disable=abstract-method +# pylint:disable=no-self-use # pylint:disable=unused-argument -def _check_error_code(span, servicer_context, rpc_info): - if servicer_context.code != grpc.StatusCode.OK: - rpc_info.error = servicer_context.code +class OpenTelemetryServerInterceptor(grpc.ServerInterceptor): + """ + A gRPC server interceptor, to add OpenTelemetry. + + Usage:: + + tracer = some OpenTelemetry tracer + interceptors = [ + OpenTelemetryServerInterceptor(tracer), + ] + + server = grpc.server( + futures.ThreadPoolExecutor(max_workers=concurrency), + interceptors = interceptors) + + """ -class OpenTelemetryServerInterceptor( - grpcext.UnaryServerInterceptor, grpcext.StreamServerInterceptor -): def __init__(self, tracer): self._tracer = tracer @contextmanager - # pylint:disable=no-self-use def _set_remote_context(self, servicer_context): metadata = servicer_context.invocation_metadata() if metadata: @@ -136,74 +184,67 @@ def get_from_grpc_metadata(metadata, key) -> List[str]: else: yield - def _start_span(self, method): - span = self._tracer.start_as_current_span( - name=method, kind=trace.SpanKind.SERVER - ) - return span - - def intercept_unary(self, request, servicer_context, server_info, handler): - - with self._set_remote_context(servicer_context): - with self._start_span(server_info.full_method) as span: - rpc_info = RpcInfo( - full_method=server_info.full_method, - metadata=servicer_context.invocation_metadata(), - timeout=servicer_context.time_remaining(), - request=request, - ) - servicer_context = _OpenTelemetryServicerContext( - servicer_context, span - ) - response = handler(request, servicer_context) - - _check_error_code(span, servicer_context, rpc_info) - - rpc_info.response = response - - return response - - # For RPCs that stream responses, the result can be a generator. To record - # the span across the generated responses and detect any errors, we wrap - # the result in a new generator that yields the response values. - def _intercept_server_stream( - self, request_or_iterator, servicer_context, server_info, handler - ): - with self._set_remote_context(servicer_context): - with self._start_span(server_info.full_method) as span: - rpc_info = RpcInfo( - full_method=server_info.full_method, - metadata=servicer_context.invocation_metadata(), - timeout=servicer_context.time_remaining(), - ) - if not server_info.is_client_stream: - rpc_info.request = request_or_iterator - servicer_context = _OpenTelemetryServicerContext( - servicer_context, span - ) - result = handler(request_or_iterator, servicer_context) - for response in result: - yield response - _check_error_code(span, servicer_context, rpc_info) - - def intercept_stream( - self, request_or_iterator, servicer_context, server_info, handler - ): - if server_info.is_server_stream: - return self._intercept_server_stream( - request_or_iterator, servicer_context, server_info, handler + def _start_span(self, handler_call_details, context): + + attributes = { + "rpc.method": handler_call_details.method, + "rpc.system": "grpc", + } + + metadata = dict(context.invocation_metadata()) + if "user-agent" in metadata: + attributes["rpc.user_agent"] = metadata["user-agent"] + + # Split up the peer to keep with how other telemetry sources + # do it. This looks like: + # * ipv6:[::1]:57284 + # * ipv4:127.0.0.1:57284 + # * ipv4:10.2.1.1:57284,127.0.0.1:57284 + # + try: + host, port = ( + context.peer().split(",")[0].split(":", 1)[1].rsplit(":", 1) ) - with self._set_remote_context(servicer_context): - with self._start_span(server_info.full_method) as span: - rpc_info = RpcInfo( - full_method=server_info.full_method, - metadata=servicer_context.invocation_metadata(), - timeout=servicer_context.time_remaining(), - ) - servicer_context = _OpenTelemetryServicerContext( - servicer_context, span - ) - response = handler(request_or_iterator, servicer_context) - _check_error_code(span, servicer_context, rpc_info) - rpc_info.response = response - return response + + # other telemetry sources convert this, so we will too + if host in ("[::1]", "127.0.0.1"): + host = "localhost" + + attributes.update({"net.peer.name": host, "net.peer.port": port}) + except IndexError: + logger.warning("Failed to parse peer address '%s'", context.peer()) + + return self._tracer.start_as_current_span( + name=handler_call_details.method, + kind=trace.SpanKind.SERVER, + attributes=attributes, + ) + + def intercept_service(self, continuation, handler_call_details): + def telemetry_wrapper(behavior, request_streaming, response_streaming): + def telemetry_interceptor(request_or_iterator, context): + + with self._set_remote_context(context): + with self._start_span( + handler_call_details, context + ) as span: + # wrap the context + context = _OpenTelemetryServicerContext(context, span) + + # And now we run the actual RPC. + try: + return behavior(request_or_iterator, context) + except Exception as error: + # Bare exceptions are likely to be gRPC aborts, which + # we handle in our context wrapper. + # Here, we're interested in uncaught exceptions. + # pylint:disable=unidiomatic-typecheck + if type(error) != Exception: + span.record_exception(error) + raise error + + return telemetry_interceptor + + return _wrap_rpc_behavior( + continuation(handler_call_details), telemetry_wrapper + ) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/__init__.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/__init__.py index fe83467a70..d5e2549bab 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/__init__.py @@ -21,32 +21,32 @@ class UnaryClientInfo(abc.ABC): """Consists of various information about a unary RPC on the - invocation-side. - - Attributes: - full_method: A string of the full RPC method, i.e., - /package.service/method. - timeout: The length of time in seconds to wait for the computation to - terminate or be cancelled, or None if this method should block until - the computation is terminated or is cancelled no matter how long that - takes. - """ + invocation-side. + + Attributes: + full_method: A string of the full RPC method, i.e., + /package.service/method. + timeout: The length of time in seconds to wait for the computation to + terminate or be cancelled, or None if this method should block until + the computation is terminated or is cancelled no matter how long that + takes. + """ class StreamClientInfo(abc.ABC): """Consists of various information about a stream RPC on the - invocation-side. - - Attributes: - full_method: A string of the full RPC method, i.e., - /package.service/method. - is_client_stream: Indicates whether the RPC is client-streaming. - is_server_stream: Indicates whether the RPC is server-streaming. - timeout: The length of time in seconds to wait for the computation to - terminate or be cancelled, or None if this method should block until - the computation is terminated or is cancelled no matter how long that - takes. - """ + invocation-side. + + Attributes: + full_method: A string of the full RPC method, i.e., + /package.service/method. + is_client_stream: Indicates whether the RPC is client-streaming. + is_server_stream: Indicates whether the RPC is server-streaming. + timeout: The length of time in seconds to wait for the computation to + terminate or be cancelled, or None if this method should block until + the computation is terminated or is cancelled no matter how long that + takes. + """ class UnaryClientInterceptor(abc.ABC): @@ -56,18 +56,18 @@ class UnaryClientInterceptor(abc.ABC): def intercept_unary(self, request, metadata, client_info, invoker): """Intercepts unary-unary RPCs on the invocation-side. - Args: - request: The request value for the RPC. - metadata: Optional :term:`metadata` to be transmitted to the - service-side of the RPC. - client_info: A UnaryClientInfo containing various information about - the RPC. - invoker: The handler to complete the RPC on the client. It is the - interceptor's responsibility to call it. - - Returns: - The result from calling invoker(request, metadata). - """ + Args: + request: The request value for the RPC. + metadata: Optional :term:`metadata` to be transmitted to the + service-side of the RPC. + client_info: A UnaryClientInfo containing various information about + the RPC. + invoker: The handler to complete the RPC on the client. It is the + interceptor's responsibility to call it. + + Returns: + The result from calling invoker(request, metadata). + """ raise NotImplementedError() @@ -80,137 +80,46 @@ def intercept_stream( ): """Intercepts stream RPCs on the invocation-side. - Args: - request_or_iterator: The request value for the RPC if - `client_info.is_client_stream` is `false`; otherwise, an iterator of - request values. - metadata: Optional :term:`metadata` to be transmitted to the service-side - of the RPC. - client_info: A StreamClientInfo containing various information about - the RPC. - invoker: The handler to complete the RPC on the client. It is the - interceptor's responsibility to call it. - - Returns: - The result from calling invoker(metadata). - """ + Args: + request_or_iterator: The request value for the RPC if + `client_info.is_client_stream` is `false`; otherwise, an iterator of + request values. + metadata: Optional :term:`metadata` to be transmitted to the service-side + of the RPC. + client_info: A StreamClientInfo containing various information about + the RPC. + invoker: The handler to complete the RPC on the client. It is the + interceptor's responsibility to call it. + + Returns: + The result from calling invoker(metadata). + """ raise NotImplementedError() def intercept_channel(channel, *interceptors): """Creates an intercepted channel. - Args: - channel: A Channel. - interceptors: Zero or more UnaryClientInterceptors or - StreamClientInterceptors - - Returns: - A Channel. - - Raises: - TypeError: If an interceptor derives from neither UnaryClientInterceptor - nor StreamClientInterceptor. - """ - from . import _interceptor - - return _interceptor.intercept_channel(channel, *interceptors) - - -class UnaryServerInfo(abc.ABC): - """Consists of various information about a unary RPC on the service-side. - - Attributes: - full_method: A string of the full RPC method, i.e., - /package.service/method. - """ - - -class StreamServerInfo(abc.ABC): - """Consists of various information about a stream RPC on the service-side. - - Attributes: - full_method: A string of the full RPC method, i.e., - /package.service/method. - is_client_stream: Indicates whether the RPC is client-streaming. - is_server_stream: Indicates whether the RPC is server-streaming. - """ - - -class UnaryServerInterceptor(abc.ABC): - """Affords intercepting unary-unary RPCs on the service-side.""" - - @abc.abstractmethod - def intercept_unary(self, request, servicer_context, server_info, handler): - """Intercepts unary-unary RPCs on the service-side. - Args: - request: The request value for the RPC. - servicer_context: A ServicerContext. - server_info: A UnaryServerInfo containing various information about - the RPC. - handler: The handler to complete the RPC on the server. It is the - interceptor's responsibility to call it. + channel: A Channel. + interceptors: Zero or more UnaryClientInterceptors or + StreamClientInterceptors Returns: - The result from calling handler(request, servicer_context). - """ - raise NotImplementedError() - - -class StreamServerInterceptor(abc.ABC): - """Affords intercepting stream RPCs on the service-side.""" + A Channel. - @abc.abstractmethod - def intercept_stream( - self, request_or_iterator, servicer_context, server_info, handler - ): - """Intercepts stream RPCs on the service-side. - - Args: - request_or_iterator: The request value for the RPC if - `server_info.is_client_stream` is `False`; otherwise, an iterator of - request values. - servicer_context: A ServicerContext. - server_info: A StreamServerInfo containing various information about - the RPC. - handler: The handler to complete the RPC on the server. It is the - interceptor's responsibility to call it. - - Returns: - The result from calling handler(servicer_context). + Raises: + TypeError: If an interceptor derives from neither UnaryClientInterceptor + nor StreamClientInterceptor. """ - raise NotImplementedError() - - -def intercept_server(server, *interceptors): - """Creates an intercepted server. - - Args: - server: A Server. - interceptors: Zero or more UnaryServerInterceptors or - StreamServerInterceptors - - Returns: - A Server. - - Raises: - TypeError: If an interceptor derives from neither UnaryServerInterceptor - nor StreamServerInterceptor. - """ from . import _interceptor - return _interceptor.intercept_server(server, *interceptors) + return _interceptor.intercept_channel(channel, *interceptors) __all__ = ( "UnaryClientInterceptor", "StreamClientInfo", "StreamClientInterceptor", - "UnaryServerInfo", - "StreamServerInfo", - "UnaryServerInterceptor", - "StreamServerInterceptor", "intercept_channel", - "intercept_server", ) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/_interceptor.py index 74861913b9..b9f74fff80 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/_interceptor.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/_interceptor.py @@ -252,180 +252,3 @@ def intercept_channel(channel, *interceptors): ) result = _InterceptorChannel(result, interceptor) return result - - -class _UnaryServerInfo( - collections.namedtuple("_UnaryServerInfo", ("full_method",)) -): - pass - - -class _StreamServerInfo( - collections.namedtuple( - "_StreamServerInfo", - ("full_method", "is_client_stream", "is_server_stream"), - ) -): - pass - - -class _InterceptorRpcMethodHandler(grpc.RpcMethodHandler): - def __init__(self, rpc_method_handler, method, interceptor): - self._rpc_method_handler = rpc_method_handler - self._method = method - self._interceptor = interceptor - - @property - def request_streaming(self): - return self._rpc_method_handler.request_streaming - - @property - def response_streaming(self): - return self._rpc_method_handler.response_streaming - - @property - def request_deserializer(self): - return self._rpc_method_handler.request_deserializer - - @property - def response_serializer(self): - return self._rpc_method_handler.response_serializer - - @property - def unary_unary(self): - if not isinstance(self._interceptor, grpcext.UnaryServerInterceptor): - return self._rpc_method_handler.unary_unary - - def adaptation(request, servicer_context): - def handler(request, servicer_context): - return self._rpc_method_handler.unary_unary( - request, servicer_context - ) - - return self._interceptor.intercept_unary( - request, - servicer_context, - _UnaryServerInfo(self._method), - handler, - ) - - return adaptation - - @property - def unary_stream(self): - if not isinstance(self._interceptor, grpcext.StreamServerInterceptor): - return self._rpc_method_handler.unary_stream - - def adaptation(request, servicer_context): - def handler(request, servicer_context): - return self._rpc_method_handler.unary_stream( - request, servicer_context - ) - - return self._interceptor.intercept_stream( - request, - servicer_context, - _StreamServerInfo(self._method, False, True), - handler, - ) - - return adaptation - - @property - def stream_unary(self): - if not isinstance(self._interceptor, grpcext.StreamServerInterceptor): - return self._rpc_method_handler.stream_unary - - def adaptation(request_iterator, servicer_context): - def handler(request_iterator, servicer_context): - return self._rpc_method_handler.stream_unary( - request_iterator, servicer_context - ) - - return self._interceptor.intercept_stream( - request_iterator, - servicer_context, - _StreamServerInfo(self._method, True, False), - handler, - ) - - return adaptation - - @property - def stream_stream(self): - if not isinstance(self._interceptor, grpcext.StreamServerInterceptor): - return self._rpc_method_handler.stream_stream - - def adaptation(request_iterator, servicer_context): - def handler(request_iterator, servicer_context): - return self._rpc_method_handler.stream_stream( - request_iterator, servicer_context - ) - - return self._interceptor.intercept_stream( - request_iterator, - servicer_context, - _StreamServerInfo(self._method, True, True), - handler, - ) - - return adaptation - - -class _InterceptorGenericRpcHandler(grpc.GenericRpcHandler): - def __init__(self, generic_rpc_handler, interceptor): - self.generic_rpc_handler = generic_rpc_handler - self._interceptor = interceptor - - def service(self, handler_call_details): - result = self.generic_rpc_handler.service(handler_call_details) - if result: - result = _InterceptorRpcMethodHandler( - result, handler_call_details.method, self._interceptor - ) - return result - - -class _InterceptorServer(grpc.Server): - def __init__(self, server, interceptor): - self._server = server - self._interceptor = interceptor - - def add_generic_rpc_handlers(self, generic_rpc_handlers): - generic_rpc_handlers = [ - _InterceptorGenericRpcHandler( - generic_rpc_handler, self._interceptor - ) - for generic_rpc_handler in generic_rpc_handlers - ] - return self._server.add_generic_rpc_handlers(generic_rpc_handlers) - - def add_insecure_port(self, *args, **kwargs): - return self._server.add_insecure_port(*args, **kwargs) - - def add_secure_port(self, *args, **kwargs): - return self._server.add_secure_port(*args, **kwargs) - - def start(self, *args, **kwargs): - return self._server.start(*args, **kwargs) - - def stop(self, *args, **kwargs): - return self._server.stop(*args, **kwargs) - - def wait_for_termination(self, *args, **kwargs): - return self._server.wait_for_termination(*args, **kwargs) - - -def intercept_server(server, *interceptors): - result = server - for interceptor in interceptors: - if not isinstance( - interceptor, grpcext.UnaryServerInterceptor - ) and not isinstance(interceptor, grpcext.StreamServerInterceptor): - raise TypeError( - "interceptor must be either a " - "grpcext.UnaryServerInterceptor or a " - "grpcext.StreamServerInterceptor" - ) - result = _InterceptorServer(result, interceptor) - return result diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_server_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_server_interceptor.py index a41da47ae9..13b535d841 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_server_interceptor.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_server_interceptor.py @@ -26,7 +26,6 @@ GrpcInstrumentorServer, server_interceptor, ) -from opentelemetry.instrumentation.grpc.grpcext import intercept_server from opentelemetry.sdk import trace as trace_sdk from opentelemetry.test.test_base import TestBase @@ -123,10 +122,9 @@ def handler(request, context): server = grpc.server( futures.ThreadPoolExecutor(max_workers=1), options=(("grpc.so_reuseport", 0),), + interceptors=[interceptor], ) - # FIXME: grpcext interceptor doesn't apply to handlers passed to server - # init, should use intercept_service API instead. - server = intercept_server(server, interceptor) + server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) port = server.add_insecure_port("[::]:0") @@ -166,8 +164,8 @@ def handler(request, context): server = grpc.server( futures.ThreadPoolExecutor(max_workers=1), options=(("grpc.so_reuseport", 0),), + interceptors=[interceptor], ) - server = intercept_server(server, interceptor) server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) port = server.add_insecure_port("[::]:0") @@ -201,8 +199,8 @@ def handler(request, context): server = grpc.server( futures.ThreadPoolExecutor(max_workers=1), options=(("grpc.so_reuseport", 0),), + interceptors=[interceptor], ) - server = intercept_server(server, interceptor) server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) port = server.add_insecure_port("[::]:0") @@ -248,8 +246,8 @@ def handler(request, context): server = grpc.server( futures.ThreadPoolExecutor(max_workers=2), options=(("grpc.so_reuseport", 0),), + interceptors=[interceptor], ) - server = intercept_server(server, interceptor) server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) port = server.add_insecure_port("[::]:0") From 7d9b0e520d10e5f0f9cf1df3018f80290ce16090 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 30 Oct 2020 18:56:12 -0600 Subject: [PATCH 0637/1517] Data points in exporter shouldnt use bound instruments (#1237) --- .../opentelemetry-exporter-otlp/CHANGELOG.md | 2 + .../otlp/metrics_exporter/__init__.py | 124 ++++++++++-------- .../tests/test_otlp_metric_exporter.py | 21 +-- opentelemetry-sdk/CHANGELOG.md | 2 + .../sdk/metrics/export/aggregate.py | 13 +- .../util/src/opentelemetry/test/controller.py | 62 +++++++++ 6 files changed, 158 insertions(+), 66 deletions(-) create mode 100644 tests/util/src/opentelemetry/test/controller.py diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 0e9fbba8a8..b1deb74e27 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -4,6 +4,8 @@ - Add Env variables in OTLP exporter ([#1101](https://github.com/open-telemetry/opentelemetry-python/pull/1101)) +- Do not use bound instruments in OTLP exporter + ([#1237](https://github.com/open-telemetry/opentelemetry-python/pull/1237)) ## Version 0.14b0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py index 18ce772ea4..3a7ad586c6 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -58,48 +58,54 @@ MetricsExporter, MetricsExportResult, ) +from opentelemetry.sdk.metrics.export.aggregate import ( + HistogramAggregator, + LastValueAggregator, + MinMaxSumCountAggregator, + SumAggregator, + ValueObserverAggregator, +) logger = logging.getLogger(__name__) DataPointT = TypeVar("DataPointT", IntDataPoint, DoubleDataPoint) def _get_data_points( - sdk_metric: MetricRecord, data_point_class: Type[DataPointT] + sdk_metric_record: MetricRecord, data_point_class: Type[DataPointT] ) -> List[DataPointT]: - data_points = [] - - for ( - label, - bound_counter, - ) in sdk_metric.instrument.bound_instruments.items(): - - string_key_values = [] - - for label_key, label_value in label: - string_key_values.append( - StringKeyValue(key=label_key, value=label_value) - ) - - for view_data in bound_counter.view_datas: - - if view_data.labels == label: - - data_points.append( - data_point_class( - labels=string_key_values, - value=view_data.aggregator.current, - start_time_unix_nano=( - view_data.aggregator.last_checkpoint_timestamp - ), - time_unix_nano=( - view_data.aggregator.last_update_timestamp - ), - ) - ) - break - - return data_points + if isinstance(sdk_metric_record.aggregator, SumAggregator): + value = sdk_metric_record.aggregator.checkpoint + + elif isinstance(sdk_metric_record.aggregator, MinMaxSumCountAggregator): + # FIXME: How are values to be interpreted from this aggregator? + raise Exception("MinMaxSumCount aggregator data not supported") + + elif isinstance(sdk_metric_record.aggregator, HistogramAggregator): + # FIXME: How are values to be interpreted from this aggregator? + raise Exception("Histogram aggregator data not supported") + + elif isinstance(sdk_metric_record.aggregator, LastValueAggregator): + value = sdk_metric_record.aggregator.checkpoint + + elif isinstance(sdk_metric_record.aggregator, ValueObserverAggregator): + value = sdk_metric_record.aggregator.checkpoint.last + + return [ + data_point_class( + labels=[ + StringKeyValue(key=str(label_key), value=str(label_value)) + for label_key, label_value in sdk_metric_record.labels + ], + value=value, + start_time_unix_nano=( + sdk_metric_record.aggregator.initial_checkpoint_timestamp + ), + time_unix_nano=( + sdk_metric_record.aggregator.last_update_timestamp + ), + ) + ] class OTLPMetricsExporter( @@ -179,13 +185,13 @@ def _translate_data( # SumObserver Sum(aggregation_temporality=cumulative;is_monotonic=true) # UpDownSumObserver Sum(aggregation_temporality=cumulative;is_monotonic=false) # ValueObserver Gauge() - for sdk_metric in data: + for sdk_metric_record in data: - if sdk_metric.resource not in ( + if sdk_metric_record.resource not in ( sdk_resource_instrumentation_library_metrics.keys() ): sdk_resource_instrumentation_library_metrics[ - sdk_metric.resource + sdk_metric_record.resource ] = InstrumentationLibraryMetrics() type_class = { @@ -204,15 +210,17 @@ def _translate_data( }, } - value_type = sdk_metric.instrument.value_type + value_type = sdk_metric_record.instrument.value_type sum_class = type_class[value_type]["sum"]["class"] gauge_class = type_class[value_type]["gauge"]["class"] data_point_class = type_class[value_type]["data_point_class"] - if isinstance(sdk_metric.instrument, Counter): + if isinstance(sdk_metric_record.instrument, Counter): otlp_metric_data = sum_class( - data_points=_get_data_points(sdk_metric, data_point_class), + data_points=_get_data_points( + sdk_metric_record, data_point_class + ), aggregation_temporality=( AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA ), @@ -220,9 +228,11 @@ def _translate_data( ) argument = type_class[value_type]["sum"]["argument"] - elif isinstance(sdk_metric.instrument, UpDownCounter): + elif isinstance(sdk_metric_record.instrument, UpDownCounter): otlp_metric_data = sum_class( - data_points=_get_data_points(sdk_metric, data_point_class), + data_points=_get_data_points( + sdk_metric_record, data_point_class + ), aggregation_temporality=( AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA ), @@ -230,13 +240,15 @@ def _translate_data( ) argument = type_class[value_type]["sum"]["argument"] - elif isinstance(sdk_metric.instrument, (ValueRecorder)): + elif isinstance(sdk_metric_record.instrument, (ValueRecorder)): logger.warning("Skipping exporting of ValueRecorder metric") continue - elif isinstance(sdk_metric.instrument, SumObserver): + elif isinstance(sdk_metric_record.instrument, SumObserver): otlp_metric_data = sum_class( - data_points=_get_data_points(sdk_metric, data_point_class), + data_points=_get_data_points( + sdk_metric_record, data_point_class + ), aggregation_temporality=( AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE ), @@ -244,9 +256,11 @@ def _translate_data( ) argument = type_class[value_type]["sum"]["argument"] - elif isinstance(sdk_metric.instrument, UpDownSumObserver): + elif isinstance(sdk_metric_record.instrument, UpDownSumObserver): otlp_metric_data = sum_class( - data_points=_get_data_points(sdk_metric, data_point_class), + data_points=_get_data_points( + sdk_metric_record, data_point_class + ), aggregation_temporality=( AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE ), @@ -254,20 +268,24 @@ def _translate_data( ) argument = type_class[value_type]["sum"]["argument"] - elif isinstance(sdk_metric.instrument, (ValueObserver)): + elif isinstance(sdk_metric_record.instrument, (ValueObserver)): otlp_metric_data = gauge_class( - data_points=_get_data_points(sdk_metric, data_point_class) + data_points=_get_data_points( + sdk_metric_record, data_point_class + ) ) argument = type_class[value_type]["gauge"]["argument"] sdk_resource_instrumentation_library_metrics[ - sdk_metric.resource + sdk_metric_record.resource ].metrics.append( OTLPMetric( **{ - "name": sdk_metric.instrument.name, - "description": sdk_metric.instrument.description, - "unit": sdk_metric.instrument.unit, + "name": sdk_metric_record.instrument.name, + "description": ( + sdk_metric_record.instrument.description + ), + "unit": sdk_metric_record.instrument.unit, argument: otlp_metric_data, } ) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index 530f9a430a..3034fcdf65 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -52,14 +52,14 @@ def setUp(self): self.counter_metric_record = MetricRecord( Counter( - "a", - "b", "c", + "d", + "e", int, MeterProvider(resource=resource,).get_meter(__name__), - ("d",), + ("f",), ), - OrderedDict([("e", "f")]), + [("g", "h")], SumAggregator(), resource, ) @@ -97,7 +97,9 @@ def test_translate_metrics(self, mock_time_ns): mock_time_ns.configure_mock(**{"return_value": 1}) - self.counter_metric_record.instrument.add(1, OrderedDict([("a", "b")])) + self.counter_metric_record.aggregator.checkpoint = 1 + self.counter_metric_record.aggregator.initial_checkpoint_timestamp = 1 + self.counter_metric_record.aggregator.last_update_timestamp = 1 expected = ExportMetricsServiceRequest( resource_metrics=[ @@ -114,19 +116,20 @@ def test_translate_metrics(self, mock_time_ns): InstrumentationLibraryMetrics( metrics=[ OTLPMetric( - name="a", - description="b", - unit="c", + name="c", + description="d", + unit="e", int_sum=IntSum( data_points=[ IntDataPoint( labels=[ StringKeyValue( - key="a", value="b" + key="g", value="h" ) ], value=1, time_unix_nano=1, + start_time_unix_nano=1, ) ], aggregation_temporality=( diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 34a622bb99..2c1c3d971d 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -14,6 +14,8 @@ ([#1282](https://github.com/open-telemetry/opentelemetry-python/pull/1282)) - Span.is_recording() returns false after span has ended ([#1289](https://github.com/open-telemetry/opentelemetry-python/pull/1289)) +- Set initial checkpoint timestamp in aggregators + ([#1237](https://github.com/open-telemetry/opentelemetry-python/pull/1237)) ## Version 0.14b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 7d3ad52df0..84ab518a47 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -33,7 +33,8 @@ class Aggregator(abc.ABC): def __init__(self, config=None): self._lock = threading.Lock() self.last_update_timestamp = 0 - self.last_checkpoint_timestamp = 0 + self.initial_checkpoint_timestamp = 0 + self.checkpointed = True if config is not None: self.config = config else: @@ -42,12 +43,15 @@ def __init__(self, config=None): @abc.abstractmethod def update(self, value): """Updates the current with the new value.""" + if self.checkpointed: + self.initial_checkpoint_timestamp = time_ns() + self.checkpointed = False self.last_update_timestamp = time_ns() @abc.abstractmethod def take_checkpoint(self): """Stores a snapshot of the current value.""" - self.last_checkpoint_timestamp = time_ns() + self.checkpointed = True @abc.abstractmethod def merge(self, other): @@ -55,8 +59,9 @@ def merge(self, other): self.last_update_timestamp = max( self.last_update_timestamp, other.last_update_timestamp ) - self.last_checkpoint_timestamp = max( - self.last_checkpoint_timestamp, other.last_checkpoint_timestamp + self.initial_checkpoint_timestamp = max( + self.initial_checkpoint_timestamp, + other.initial_checkpoint_timestamp, ) def _verify_type(self, other): diff --git a/tests/util/src/opentelemetry/test/controller.py b/tests/util/src/opentelemetry/test/controller.py new file mode 100644 index 0000000000..754d7bf979 --- /dev/null +++ b/tests/util/src/opentelemetry/test/controller.py @@ -0,0 +1,62 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from time import sleep + +from opentelemetry.context import attach, detach, set_value +from opentelemetry.metrics import Meter +from opentelemetry.sdk.metrics.export import MetricsExporter + + +class DebugController: + """A debug controller, used to replace Push controller when debugging + + Push controller uses a thread which makes it hard to use the IPython + debugger. This controller does not use a thread, but relies on the user + manually calling its ``run`` method to start the controller. + + Args: + meter: The meter used to collect metrics. + exporter: The exporter used to export metrics. + interval: The collect/export interval in seconds. + """ + + daemon = True + + def __init__( + self, meter: Meter, exporter: MetricsExporter, interval: float + ): + super().__init__() + self.meter = meter + self.exporter = exporter + self.interval = interval + + def run(self): + while True: + self.tick() + sleep(self.interval) + + def shutdown(self): + # Run one more collection pass to flush metrics batched in the meter + self.tick() + + def tick(self): + # Collect all of the meter's metrics to be exported + self.meter.collect() + # Export the collected metrics + token = attach(set_value("suppress_instrumentation", True)) + self.exporter.export(self.meter.processor.checkpoint_set()) + detach(token) + # Perform post-exporting logic + self.meter.processor.finished_collection() From 8b6087968c58fd5f57d57b971e6330b0f521dccb Mon Sep 17 00:00:00 2001 From: Michael Stella Date: Sun, 1 Nov 2020 22:22:48 -0500 Subject: [PATCH 0638/1517] Rework gRPC status based on new rules (#1308) As of #1214, the status codes changed and no longer line up with gRPC status codes, so now we'll just set `StatusCode.ERROR` and store the actual gRPC status code in the trace as `grpc.status_code`. Specifically this: https://github.com/open-telemetry/opentelemetry-specification/pull/1156 --- .../opentelemetry/instrumentation/grpc/_server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py index 83cc5824f1..5927c99b51 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py @@ -113,8 +113,9 @@ def set_trailing_metadata(self, *args, **kwargs): def abort(self, code, details): self.code = code self.details = details + self._active_span.set_attribute("rpc.grpc.status_code", code.name) self._active_span.set_status( - Status(status_code=StatusCode(code.value[0]), description=details) + Status(status_code=StatusCode.ERROR, description=details) ) return self._servicer_context.abort(code, details) @@ -125,18 +126,16 @@ def set_code(self, code): self.code = code # use details if we already have it, otherwise the status description details = self.details or code.value[1] + self._active_span.set_attribute("rpc.grpc.status_code", code.name) self._active_span.set_status( - Status(status_code=StatusCode(code.value[0]), description=details) + Status(status_code=StatusCode.ERROR, description=details) ) return self._servicer_context.set_code(code) def set_details(self, details): self.details = details self._active_span.set_status( - Status( - status_code=StatusCode(self.code.value[0]), - description=details, - ) + Status(status_code=StatusCode.ERROR, description=details) ) return self._servicer_context.set_details(details) @@ -189,6 +188,7 @@ def _start_span(self, handler_call_details, context): attributes = { "rpc.method": handler_call_details.method, "rpc.system": "grpc", + "rpc.grpc.status_code": grpc.StatusCode.OK, } metadata = dict(context.invocation_metadata()) From a3a75e3d46a996ef46644f99e6788bce3b38971a Mon Sep 17 00:00:00 2001 From: zhutianyu <704735206@qq.com> Date: Mon, 2 Nov 2020 11:50:17 +0800 Subject: [PATCH 0639/1517] Bugfix django instrumentation (#1309) --- .../CHANGELOG.md | 1 + .../instrumentation/django/middleware.py | 2 +- .../tests/test_middleware.py | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index 36962fbdce..2e399b202d 100644 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Django instrumentation is now enabled by default but can be disabled by setting `OTEL_PYTHON_DJANGO_INSTRUMENT` to `False` ([#1239](https://github.com/open-telemetry/opentelemetry-python/pull/1239)) +- Bugfix use request.path replace request.get_full_path(). It will get correct span name ([#1309](https://github.com/open-telemetry/opentelemetry-python/pull/1309#)) ## Version 0.14b0 diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index 41343873d0..5c27ed289a 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -81,7 +81,7 @@ def _get_span_name(request): if getattr(request, "resolver_match"): match = request.resolver_match else: - match = resolve(request.get_full_path()) + match = resolve(request.path) if hasattr(match, "route"): return match.route diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index d087dc2d9d..3f70f62bec 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -250,6 +250,7 @@ def test_exclude_lists(self): self.assertEqual(len(span_list), 1) def test_span_name(self): + # test no query_string Client().get("/span_name/1234/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) @@ -262,6 +263,22 @@ def test_span_name(self): else "tests.views.route_span_name", ) + def test_span_name_for_query_string(self): + """ + request not have query string + """ + Client().get("/span_name/1234/?query=test") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + span = span_list[0] + self.assertEqual( + span.name, + "^span_name/([0-9]{4})/$" + if DJANGO_2_2 + else "tests.views.route_span_name", + ) + def test_span_name_404(self): Client().get("/span_name/1234567890/") span_list = self.memory_exporter.get_finished_spans() From 397f228e0676176739661ee8aecef21abdd99e89 Mon Sep 17 00:00:00 2001 From: Prajilesh N Date: Mon, 2 Nov 2020 09:42:47 +0530 Subject: [PATCH 0640/1517] Converted TextMap propagator getter to a class and added keys method (#1196) Co-authored-by: alrex --- docs/conf.py | 1 + .../exporter/datadog/propagator.py | 12 ++-- .../tests/test_datadog_format.py | 16 +++--- .../instrumentation/asgi/__init__.py | 40 ++++++++------ .../instrumentation/celery/__init__.py | 25 ++++++--- .../instrumentation/django/middleware.py | 4 +- .../instrumentation/falcon/__init__.py | 2 +- .../instrumentation/flask/__init__.py | 2 +- .../instrumentation/grpc/_server.py | 10 +--- .../opentracing_shim/__init__.py | 8 +-- .../instrumentation/pyramid/callbacks.py | 2 +- .../instrumentation/tornado/__init__.py | 14 ++--- .../instrumentation/wsgi/__init__.py | 39 ++++++++----- opentelemetry-api/CHANGELOG.md | 2 + .../baggage/propagation/__init__.py | 4 +- .../src/opentelemetry/propagators/__init__.py | 12 ++-- .../opentelemetry/propagators/composite.py | 4 +- .../trace/propagation/textmap.py | 55 +++++++++++++++++-- .../trace/propagation/tracecontext.py | 9 +-- .../tests/baggage/test_baggage_propagation.py | 11 ++-- .../propagators/test_global_httptextformat.py | 10 +--- .../test_tracecontexthttptextformat.py | 24 ++++---- opentelemetry-sdk/CHANGELOG.md | 2 + .../sdk/trace/propagation/b3_format.py | 20 ++----- .../tests/trace/propagation/test_b3_format.py | 24 ++++---- .../src/opentelemetry/test/mock_textmap.py | 8 +-- 26 files changed, 198 insertions(+), 162 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 68b871aaac..b227cb5181 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -102,6 +102,7 @@ ("py:class", "ObjectProxy"), # TODO: Understand why sphinx is not able to find this local class ("py:class", "opentelemetry.trace.propagation.textmap.TextMapPropagator",), + ("py:class", "opentelemetry.trace.propagation.textmap.DictGetter",), ( "any", "opentelemetry.trace.propagation.textmap.TextMapPropagator.extract", diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py index 3ad9fa1ae2..ab1468c54a 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py @@ -39,25 +39,23 @@ class DatadogFormat(TextMapPropagator): def extract( self, - get_from_carrier: Getter[TextMapPropagatorT], + getter: Getter[TextMapPropagatorT], carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: trace_id = extract_first_element( - get_from_carrier(carrier, self.TRACE_ID_KEY) + getter.get(carrier, self.TRACE_ID_KEY) ) span_id = extract_first_element( - get_from_carrier(carrier, self.PARENT_ID_KEY) + getter.get(carrier, self.PARENT_ID_KEY) ) sampled = extract_first_element( - get_from_carrier(carrier, self.SAMPLING_PRIORITY_KEY) + getter.get(carrier, self.SAMPLING_PRIORITY_KEY) ) - origin = extract_first_element( - get_from_carrier(carrier, self.ORIGIN_KEY) - ) + origin = extract_first_element(getter.get(carrier, self.ORIGIN_KEY)) trace_flags = trace.TraceFlags() if sampled and int(sampled) in ( diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py index 8480374471..bb41fef49b 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py @@ -18,13 +18,11 @@ from opentelemetry.exporter.datadog import constants, propagator from opentelemetry.sdk import trace from opentelemetry.trace import get_current_span, set_span_in_context +from opentelemetry.trace.propagation.textmap import DictGetter FORMAT = propagator.DatadogFormat() - -def get_as_list(dict_object, key): - value = dict_object.get(key) - return [value] if value is not None else [] +carrier_getter = DictGetter() class TestDatadogFormat(unittest.TestCase): @@ -45,7 +43,7 @@ def test_malformed_headers(self): malformed_parent_id_key = FORMAT.PARENT_ID_KEY + "-x" context = get_current_span( FORMAT.extract( - get_as_list, + carrier_getter, { malformed_trace_id_key: self.serialized_trace_id, malformed_parent_id_key: self.serialized_parent_id, @@ -63,7 +61,7 @@ def test_missing_trace_id(self): FORMAT.PARENT_ID_KEY: self.serialized_parent_id, } - ctx = FORMAT.extract(get_as_list, carrier) + ctx = FORMAT.extract(carrier_getter, carrier) span_context = get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) @@ -73,7 +71,7 @@ def test_missing_parent_id(self): FORMAT.TRACE_ID_KEY: self.serialized_trace_id, } - ctx = FORMAT.extract(get_as_list, carrier) + ctx = FORMAT.extract(carrier_getter, carrier) span_context = get_current_span(ctx).get_span_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) @@ -81,7 +79,7 @@ def test_context_propagation(self): """Test the propagation of Datadog headers.""" parent_span_context = get_current_span( FORMAT.extract( - get_as_list, + carrier_getter, { FORMAT.TRACE_ID_KEY: self.serialized_trace_id, FORMAT.PARENT_ID_KEY: self.serialized_parent_id, @@ -138,7 +136,7 @@ def test_sampling_priority_auto_reject(self): """Test sampling priority rejected.""" parent_span_context = get_current_span( FORMAT.extract( - get_as_list, + carrier_getter, { FORMAT.TRACE_ID_KEY: self.serialized_trace_id, FORMAT.PARENT_ID_KEY: self.serialized_parent_id, diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index 1a0bb47a64..1c442889a1 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -29,21 +29,31 @@ from opentelemetry import context, propagators, trace from opentelemetry.instrumentation.asgi.version import __version__ # noqa from opentelemetry.instrumentation.utils import http_status_to_status_code +from opentelemetry.trace.propagation.textmap import DictGetter from opentelemetry.trace.status import Status, StatusCode -def get_header_from_scope(scope: dict, header_name: str) -> typing.List[str]: - """Retrieve a HTTP header value from the ASGI scope. +class CarrierGetter(DictGetter): + def get(self, carrier: dict, key: str) -> typing.List[str]: + """Getter implementation to retrieve a HTTP header value from the ASGI + scope. - Returns: - A list with a single string with the header value if it exists, else an empty list. - """ - headers = scope.get("headers") - return [ - value.decode("utf8") - for (key, value) in headers - if key.decode("utf8") == header_name - ] + Args: + carrier: ASGI scope object + key: header name in scope + Returns: + A list with a single string with the header value if it exists, + else an empty list. + """ + headers = carrier.get("headers") + return [ + _value.decode("utf8") + for (_key, _value) in headers + if _key.decode("utf8") == key + ] + + +carrier_getter = CarrierGetter() def collect_request_attributes(scope): @@ -72,10 +82,10 @@ def collect_request_attributes(scope): http_method = scope.get("method") if http_method: result["http.method"] = http_method - http_host_value = ",".join(get_header_from_scope(scope, "host")) + http_host_value = ",".join(carrier_getter.get(scope, "host")) if http_host_value: result["http.server_name"] = http_host_value - http_user_agent = get_header_from_scope(scope, "user-agent") + http_user_agent = carrier_getter.get(scope, "user-agent") if len(http_user_agent) > 0: result["http.user_agent"] = http_user_agent[0] @@ -154,9 +164,7 @@ async def __call__(self, scope, receive, send): if scope["type"] not in ("http", "websocket"): return await self.app(scope, receive, send) - token = context.attach( - propagators.extract(get_header_from_scope, scope) - ) + token = context.attach(propagators.extract(carrier_getter, scope)) span_name, additional_attributes = self.span_details_callback(scope) try: diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py index a5897f8b6e..d225e6bd06 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py @@ -67,7 +67,7 @@ def add(x, y): from opentelemetry.instrumentation.celery import utils from opentelemetry.instrumentation.celery.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.trace.propagation import get_current_span +from opentelemetry.trace.propagation.textmap import DictGetter from opentelemetry.trace.status import Status, StatusCode logger = logging.getLogger(__name__) @@ -84,6 +84,20 @@ def add(x, y): _MESSAGE_ID_ATTRIBUTE_NAME = "messaging.message_id" +class CarrierGetter(DictGetter): + def get(self, carrier, key): + value = getattr(carrier, key, []) + if isinstance(value, str) or not isinstance(value, Iterable): + value = (value,) + return value + + def keys(self, carrier): + return [] + + +carrier_getter = CarrierGetter() + + class CeleryInstrumentor(BaseInstrumentor): def _instrument(self, **kwargs): tracer_provider = kwargs.get("tracer_provider") @@ -118,7 +132,7 @@ def _trace_prerun(self, *args, **kwargs): return request = task.request - tracectx = propagators.extract(carrier_extractor, request) or None + tracectx = propagators.extract(carrier_getter, request) or None logger.debug("prerun signal start task_id=%s", task_id) @@ -246,10 +260,3 @@ def _trace_retry(*args, **kwargs): # Use `str(reason)` instead of `reason.message` in case we get # something that isn't an `Exception` span.set_attribute(_TASK_RETRY_REASON_KEY, str(reason)) - - -def carrier_extractor(carrier, key): - value = getattr(carrier, key, []) - if isinstance(value, str) or not isinstance(value, Iterable): - value = (value,) - return value diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index 5c27ed289a..1f465ca57a 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -23,8 +23,8 @@ from opentelemetry.instrumentation.utils import extract_attributes_from_object from opentelemetry.instrumentation.wsgi import ( add_response_attributes, + carrier_getter, collect_request_attributes, - get_header_from_environ, ) from opentelemetry.propagators import extract from opentelemetry.trace import SpanKind, get_tracer @@ -125,7 +125,7 @@ def process_request(self, request): environ = request.META - token = attach(extract(get_header_from_environ, environ)) + token = attach(extract(carrier_getter, environ)) tracer = get_tracer(__name__, __version__) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py index 66e6563dff..55f8e98dcb 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py @@ -115,7 +115,7 @@ def __call__(self, env, start_response): start_time = time_ns() token = context.attach( - propagators.extract(otel_wsgi.get_header_from_environ, env) + propagators.extract(otel_wsgi.carrier_getter, env) ) span = self._tracer.start_span( otel_wsgi.get_default_span_name(env), diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py index c88a760905..1235b09a30 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py @@ -118,7 +118,7 @@ def _before_request(): if span_name is None: span_name = otel_wsgi.get_default_span_name(environ) token = context.attach( - propagators.extract(otel_wsgi.get_header_from_environ, environ) + propagators.extract(otel_wsgi.carrier_getter, environ) ) tracer = trace.get_tracer(__name__, __version__) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py index 5927c99b51..087cf4f9cc 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py @@ -23,12 +23,12 @@ import logging from contextlib import contextmanager -from typing import List import grpc from opentelemetry import propagators, trace from opentelemetry.context import attach, detach +from opentelemetry.trace.propagation.textmap import DictGetter from opentelemetry.trace.status import Status, StatusCode logger = logging.getLogger(__name__) @@ -163,18 +163,14 @@ class OpenTelemetryServerInterceptor(grpc.ServerInterceptor): def __init__(self, tracer): self._tracer = tracer + self._carrier_getter = DictGetter() @contextmanager def _set_remote_context(self, servicer_context): metadata = servicer_context.invocation_metadata() if metadata: md_dict = {md.key: md.value for md in metadata} - - def get_from_grpc_metadata(metadata, key) -> List[str]: - return [md_dict[key]] if key in md_dict else [] - - # Update the context with the traceparent from the RPC metadata. - ctx = propagators.extract(get_from_grpc_metadata, metadata) + ctx = propagators.extract(self._carrier_getter, md_dict) token = attach(ctx) try: yield diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py index 63be13fe48..00a0b8d0cb 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py @@ -112,6 +112,7 @@ get_current_span, set_span_in_context, ) +from opentelemetry.trace.propagation.textmap import DictGetter from opentelemetry.util.types import Attributes ValueT = TypeVar("ValueT", int, float, bool, str) @@ -527,6 +528,7 @@ def __init__(self, tracer: OtelTracer): Format.TEXT_MAP, Format.HTTP_HEADERS, ) + self._carrier_getter = DictGetter() def unwrap(self): """Returns the :class:`opentelemetry.trace.Tracer` object that is @@ -710,12 +712,8 @@ def extract(self, format: object, carrier: object): if format not in self._supported_formats: raise UnsupportedFormatException - def get_as_list(dict_object, key): - value = dict_object.get(key) - return [value] if value is not None else [] - propagator = propagators.get_global_textmap() - ctx = propagator.extract(get_as_list, carrier) + ctx = propagator.extract(self._carrier_getter, carrier) span = get_current_span(ctx) if span is not None: otel_context = span.get_span_context() diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py index ada239b8e3..e7110bd2b5 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py @@ -70,7 +70,7 @@ def _before_traversal(event): start_time = environ.get(_ENVIRON_STARTTIME_KEY) token = context.attach( - propagators.extract(otel_wsgi.get_header_from_environ, environ) + propagators.extract(otel_wsgi.carrier_getter, environ) ) tracer = trace.get_tracer(__name__, __version__) diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py index 9a7959ab95..6bb956ecb5 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py @@ -54,6 +54,7 @@ def get(self): http_status_to_status_code, unwrap, ) +from opentelemetry.trace.propagation.textmap import DictGetter from opentelemetry.trace.status import Status from opentelemetry.util import ExcludeList, time_ns @@ -84,6 +85,8 @@ def get_traced_request_attrs(): _excluded_urls = get_excluded_urls() _traced_attrs = get_traced_request_attrs() +carrier_getter = DictGetter() + class TornadoInstrumentor(BaseInstrumentor): patched_handlers = [] @@ -185,13 +188,6 @@ def _log_exception(tracer, func, handler, args, kwargs): return func(*args, **kwargs) -def _get_header_from_request_headers( - headers: dict, header_name: str -) -> typing.List[str]: - header = headers.get(header_name) - return [header] if header else [] - - def _get_attributes_from_request(request): attrs = { "component": "tornado", @@ -218,9 +214,7 @@ def _get_operation_name(handler, request): def _start_span(tracer, handler, start_time) -> _TraceContext: token = context.attach( - propagators.extract( - _get_header_from_request_headers, handler.request.headers, - ) + propagators.extract(carrier_getter, handler.request.headers,) ) span = tracer.start_span( diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index 62eef43251..e1ef92c6ba 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -61,24 +61,35 @@ def hello(): from opentelemetry import context, propagators, trace from opentelemetry.instrumentation.utils import http_status_to_status_code from opentelemetry.instrumentation.wsgi.version import __version__ +from opentelemetry.trace.propagation.textmap import DictGetter from opentelemetry.trace.status import Status, StatusCode _HTTP_VERSION_PREFIX = "HTTP/" -def get_header_from_environ( - environ: dict, header_name: str -) -> typing.List[str]: - """Retrieve a HTTP header value from the PEP3333-conforming WSGI environ. +class CarrierGetter(DictGetter): + def get(self, carrier: dict, key: str) -> typing.List[str]: + """Getter implementation to retrieve a HTTP header value from the + PEP3333-conforming WSGI environ - Returns: - A list with a single string with the header value if it exists, else an empty list. - """ - environ_key = "HTTP_" + header_name.upper().replace("-", "_") - value = environ.get(environ_key) - if value is not None: - return [value] - return [] + Args: + carrier: WSGI environ object + key: header name in environ object + Returns: + A list with a single string with the header value if it exists, + else an empty list. + """ + environ_key = "HTTP_" + key.upper().replace("-", "_") + value = carrier.get(environ_key) + if value is not None: + return [value] + return [] + + def keys(self, carrier): + return [] + + +carrier_getter = CarrierGetter() def setifnotnone(dic, key, value): @@ -195,9 +206,7 @@ def __call__(self, environ, start_response): start_response: The WSGI start_response callable. """ - token = context.attach( - propagators.extract(get_header_from_environ, environ) - ) + token = context.attach(propagators.extract(carrier_getter, environ)) span_name = self.name_callback(environ) span = self.tracer.start_span( diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 977dd6375c..175eba10c3 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -20,6 +20,8 @@ Released 2020-10-13 ([#1134](https://github.com/open-telemetry/opentelemetry-python/pull/1134)) - Parent is now always passed in via Context, intead of Span or SpanContext ([#1146](https://github.com/open-telemetry/opentelemetry-python/pull/1146)) +- Add keys method to TextMap propagator Getter + ([#1196](https://github.com/open-telemetry/opentelemetry-python/issues/1196)) ## Version 0.13b0 diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index d0920e6859..70e73a3b23 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -29,7 +29,7 @@ class BaggagePropagator(textmap.TextMapPropagator): def extract( self, - get_from_carrier: textmap.Getter[textmap.TextMapPropagatorT], + getter: textmap.Getter[textmap.TextMapPropagatorT], carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: @@ -43,7 +43,7 @@ def extract( context = get_current() header = _extract_first_element( - get_from_carrier(carrier, self._BAGGAGE_HEADER_NAME) + getter.get(carrier, self._BAGGAGE_HEADER_NAME) ) if not header or len(header) > self.MAX_HEADER_LENGTH: diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index 18c322f793..fb2863ac8a 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -82,24 +82,24 @@ def example_route(): def extract( - get_from_carrier: textmap.Getter[textmap.TextMapPropagatorT], + getter: textmap.Getter[textmap.TextMapPropagatorT], carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: """ Uses the configured propagator to extract a Context from the carrier. Args: - get_from_carrier: a function that can retrieve zero - or more values from the carrier. In the case that - the value does not exist, return an empty list. + getter: an object which contains a get function that can retrieve zero + or more values from the carrier and a keys function that can get all the keys + from carrier. carrier: and object which contains values that are used to construct a Context. This object - must be paired with an appropriate get_from_carrier + must be paired with an appropriate getter which understands how to extract a value from it. context: an optional Context to use. Defaults to current context if not set. """ - return get_global_textmap().extract(get_from_carrier, carrier, context) + return get_global_textmap().extract(getter, carrier, context) def inject( diff --git a/opentelemetry-api/src/opentelemetry/propagators/composite.py b/opentelemetry-api/src/opentelemetry/propagators/composite.py index 3499d2ea08..441098ec1f 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/composite.py +++ b/opentelemetry-api/src/opentelemetry/propagators/composite.py @@ -35,7 +35,7 @@ def __init__( def extract( self, - get_from_carrier: textmap.Getter[textmap.TextMapPropagatorT], + getter: textmap.Getter[textmap.TextMapPropagatorT], carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: @@ -47,7 +47,7 @@ def extract( See `opentelemetry.trace.propagation.textmap.TextMapPropagator.extract` """ for propagator in self._propagators: - context = propagator.extract(get_from_carrier, carrier, context) + context = propagator.extract(getter, carrier, context) return context # type: ignore def inject( diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py b/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py index 6f9ed897e1..ec505135be 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py @@ -18,9 +18,54 @@ from opentelemetry.context.context import Context TextMapPropagatorT = typing.TypeVar("TextMapPropagatorT") +CarrierValT = typing.Union[typing.List[str], str] Setter = typing.Callable[[TextMapPropagatorT, str, str], None] -Getter = typing.Callable[[TextMapPropagatorT, str], typing.List[str]] + + +class Getter(typing.Generic[TextMapPropagatorT]): + """This class implements a Getter that enables extracting propagated + fields from a carrier + + """ + + def get(self, carrier: TextMapPropagatorT, key: str) -> typing.List[str]: + """Function that can retrieve zero + or more values from the carrier. In the case that + the value does not exist, returns an empty list. + + Args: + carrier: An object which contains values that are used to + construct a Context. + key: key of a field in carrier. + Returns: first value of the propagation key or an empty list if the + key doesn't exist. + """ + raise NotImplementedError() + + def keys(self, carrier: TextMapPropagatorT) -> typing.List[str]: + """Function that can retrieve all the keys in a carrier object. + + Args: + carrier: An object which contains values that are + used to construct a Context. + Returns: + list of keys from the carrier. + """ + raise NotImplementedError() + + +class DictGetter(Getter[typing.Dict[str, CarrierValT]]): + def get( + self, carrier: typing.Dict[str, CarrierValT], key: str + ) -> typing.List[str]: + val = carrier.get(key, []) + if isinstance(val, typing.Iterable) and not isinstance(val, str): + return list(val) + return [val] + + def keys(self, carrier: typing.Dict[str, CarrierValT]) -> typing.List[str]: + return list(carrier.keys()) class TextMapPropagator(abc.ABC): @@ -35,23 +80,23 @@ class TextMapPropagator(abc.ABC): @abc.abstractmethod def extract( self, - get_from_carrier: Getter[TextMapPropagatorT], + getter: Getter[TextMapPropagatorT], carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: """Create a Context from values in the carrier. The extract function should retrieve values from the carrier - object using get_from_carrier, and use values to populate a + object using getter, and use values to populate a Context value and return it. Args: - get_from_carrier: a function that can retrieve zero + getter: a function that can retrieve zero or more values from the carrier. In the case that the value does not exist, return an empty list. carrier: and object which contains values that are used to construct a Context. This object - must be paired with an appropriate get_from_carrier + must be paired with an appropriate getter which understands how to extract a value from it. context: an optional Context to use. Defaults to current context if not set. diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index 4b77246ac5..57933ef9c4 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -26,7 +26,6 @@ # .. _W3C Trace Context - Tracestate: # https://www.w3.org/TR/trace-context/#tracestate-field - _KEY_WITHOUT_VENDOR_FORMAT = r"[a-z][_0-9a-z\-\*\/]{0,255}" _KEY_WITH_VENDOR_FORMAT = ( r"[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}" @@ -60,7 +59,7 @@ class TraceContextTextMapPropagator(textmap.TextMapPropagator): def extract( self, - get_from_carrier: textmap.Getter[textmap.TextMapPropagatorT], + getter: textmap.Getter[textmap.TextMapPropagatorT], carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: @@ -68,7 +67,7 @@ def extract( See `opentelemetry.trace.propagation.textmap.TextMapPropagator.extract` """ - header = get_from_carrier(carrier, self._TRACEPARENT_HEADER_NAME) + header = getter.get(carrier, self._TRACEPARENT_HEADER_NAME) if not header: return trace.set_span_in_context(trace.INVALID_SPAN, context) @@ -91,9 +90,7 @@ def extract( if version == "ff": return trace.set_span_in_context(trace.INVALID_SPAN, context) - tracestate_headers = get_from_carrier( - carrier, self._TRACESTATE_HEADER_NAME - ) + tracestate_headers = getter.get(carrier, self._TRACESTATE_HEADER_NAME) tracestate = _parse_tracestate(tracestate_headers) span_context = trace.SpanContext( diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py index d5c16ead5d..4c7b3de215 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py @@ -18,12 +18,9 @@ from opentelemetry import baggage from opentelemetry.baggage.propagation import BaggagePropagator from opentelemetry.context import get_current +from opentelemetry.trace.propagation.textmap import DictGetter - -def get_as_list( - dict_object: typing.Dict[str, typing.List[str]], key: str -) -> typing.List[str]: - return dict_object.get(key, []) +carrier_getter = DictGetter() class TestBaggagePropagation(unittest.TestCase): @@ -33,7 +30,7 @@ def setUp(self): def _extract(self, header_value): """Test helper""" header = {"baggage": [header_value]} - return baggage.get_all(self.propagator.extract(get_as_list, header)) + return baggage.get_all(self.propagator.extract(carrier_getter, header)) def _inject(self, values): """Test helper""" @@ -46,7 +43,7 @@ def _inject(self, values): def test_no_context_header(self): baggage_entries = baggage.get_all( - self.propagator.extract(get_as_list, {}) + self.propagator.extract(carrier_getter, {}) ) self.assertEqual(baggage_entries, {}) diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index 2668be27c3..b704207ed5 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -18,13 +18,9 @@ from opentelemetry import baggage, trace from opentelemetry.propagators import extract, inject from opentelemetry.trace import get_current_span, set_span_in_context +from opentelemetry.trace.propagation.textmap import DictGetter - -def get_as_list( - dict_object: typing.Dict[str, typing.List[str]], key: str -) -> typing.List[str]: - value = dict_object.get(key) - return value if value is not None else [] +carrier_getter = DictGetter() class TestDefaultGlobalPropagator(unittest.TestCase): @@ -44,7 +40,7 @@ def test_propagation(self): "traceparent": [traceparent_value], "tracestate": [tracestate_value], } - ctx = extract(get_as_list, headers) + ctx = extract(carrier_getter, headers) baggage_entries = baggage.get_all(context=ctx) expected = {"key1": "val1", "key2": "val2"} self.assertEqual(baggage_entries, expected) diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 295e3971a3..f012be2a23 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -17,15 +17,11 @@ from opentelemetry import trace from opentelemetry.trace.propagation import tracecontext +from opentelemetry.trace.propagation.textmap import DictGetter FORMAT = tracecontext.TraceContextTextMapPropagator() - -def get_as_list( - dict_object: typing.Dict[str, typing.List[str]], key: str -) -> typing.List[str]: - value = dict_object.get(key) - return value if value is not None else [] +carrier_getter = DictGetter() class TestTraceContextFormat(unittest.TestCase): @@ -42,7 +38,7 @@ def test_no_traceparent_header(self): trace-id and parent-id that represents the current request. """ output = {} # type:typing.Dict[str, typing.List[str]] - span = trace.get_current_span(FORMAT.extract(get_as_list, output)) + span = trace.get_current_span(FORMAT.extract(carrier_getter, output)) self.assertIsInstance(span.get_span_context(), trace.SpanContext) def test_headers_with_tracestate(self): @@ -56,7 +52,7 @@ def test_headers_with_tracestate(self): tracestate_value = "foo=1,bar=2,baz=3" span_context = trace.get_current_span( FORMAT.extract( - get_as_list, + carrier_getter, { "traceparent": [traceparent_value], "tracestate": [tracestate_value], @@ -100,7 +96,7 @@ def test_invalid_trace_id(self): """ span = trace.get_current_span( FORMAT.extract( - get_as_list, + carrier_getter, { "traceparent": [ "00-00000000000000000000000000000000-1234567890123456-00" @@ -131,7 +127,7 @@ def test_invalid_parent_id(self): """ span = trace.get_current_span( FORMAT.extract( - get_as_list, + carrier_getter, { "traceparent": [ "00-00000000000000000000000000000000-0000000000000000-00" @@ -169,7 +165,7 @@ def test_format_not_supported(self): """ span = trace.get_current_span( FORMAT.extract( - get_as_list, + carrier_getter, { "traceparent": [ "00-12345678901234567890123456789012-" @@ -193,7 +189,7 @@ def test_tracestate_empty_header(self): """ span = trace.get_current_span( FORMAT.extract( - get_as_list, + carrier_getter, { "traceparent": [ "00-12345678901234567890123456789012-1234567890123456-00" @@ -209,7 +205,7 @@ def test_tracestate_header_with_trailing_comma(self): """ span = trace.get_current_span( FORMAT.extract( - get_as_list, + carrier_getter, { "traceparent": [ "00-12345678901234567890123456789012-1234567890123456-00" @@ -233,7 +229,7 @@ def test_tracestate_keys(self): ) span = trace.get_current_span( FORMAT.extract( - get_as_list, + carrier_getter, { "traceparent": [ "00-12345678901234567890123456789012-1234567890123456-00" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 2c1c3d971d..d1aa226114 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -43,6 +43,8 @@ Released 2020-10-13 ([#1209](https://github.com/open-telemetry/opentelemetry-python/pull/1209)) - Parent is now always passed in via Context, intead of Span or SpanContext ([#1146](https://github.com/open-telemetry/opentelemetry-python/pull/1146)) +- Add keys method to TextMap propagator Getter + ([#1196](https://github.com/open-telemetry/opentelemetry-python/issues/1196)) ## Version 0.13b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py index 8a6b8e2247..c629d107d3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py @@ -43,7 +43,7 @@ class B3Format(TextMapPropagator): def extract( self, - get_from_carrier: Getter[TextMapPropagatorT], + getter: Getter[TextMapPropagatorT], carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: @@ -53,7 +53,7 @@ def extract( flags = None single_header = _extract_first_element( - get_from_carrier(carrier, self.SINGLE_HEADER_KEY) + getter.get(carrier, self.SINGLE_HEADER_KEY) ) if single_header: # The b3 spec calls for the sampling state to be @@ -74,27 +74,19 @@ def extract( return trace.set_span_in_context(trace.INVALID_SPAN) else: trace_id = ( - _extract_first_element( - get_from_carrier(carrier, self.TRACE_ID_KEY) - ) + _extract_first_element(getter.get(carrier, self.TRACE_ID_KEY)) or trace_id ) span_id = ( - _extract_first_element( - get_from_carrier(carrier, self.SPAN_ID_KEY) - ) + _extract_first_element(getter.get(carrier, self.SPAN_ID_KEY)) or span_id ) sampled = ( - _extract_first_element( - get_from_carrier(carrier, self.SAMPLED_KEY) - ) + _extract_first_element(getter.get(carrier, self.SAMPLED_KEY)) or sampled ) flags = ( - _extract_first_element( - get_from_carrier(carrier, self.FLAGS_KEY) - ) + _extract_first_element(getter.get(carrier, self.FLAGS_KEY)) or flags ) diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index a788a812c7..79c4618aee 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -19,18 +19,17 @@ import opentelemetry.sdk.trace.propagation.b3_format as b3_format import opentelemetry.trace as trace_api from opentelemetry.context import get_current +from opentelemetry.trace.propagation.textmap import DictGetter FORMAT = b3_format.B3Format() -def get_as_list(dict_object, key): - value = dict_object.get(key) - return [value] if value is not None else [] +carrier_getter = DictGetter() def get_child_parent_new_carrier(old_carrier): - ctx = FORMAT.extract(get_as_list, old_carrier) + ctx = FORMAT.extract(carrier_getter, old_carrier) parent_span_context = trace_api.get_current_span(ctx).get_span_context() parent = trace._Span("parent", parent_span_context) @@ -231,7 +230,7 @@ def test_invalid_single_header(self): invalid SpanContext. """ carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} - ctx = FORMAT.extract(get_as_list, carrier) + ctx = FORMAT.extract(carrier_getter, carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) @@ -243,7 +242,7 @@ def test_missing_trace_id(self): FORMAT.FLAGS_KEY: "1", } - ctx = FORMAT.extract(get_as_list, carrier) + ctx = FORMAT.extract(carrier_getter, carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) @@ -267,7 +266,7 @@ def test_invalid_trace_id( FORMAT.FLAGS_KEY: "1", } - ctx = FORMAT.extract(get_as_list, carrier) + ctx = FORMAT.extract(carrier_getter, carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, 1) @@ -293,7 +292,7 @@ def test_invalid_span_id( FORMAT.FLAGS_KEY: "1", } - ctx = FORMAT.extract(get_as_list, carrier) + ctx = FORMAT.extract(carrier_getter, carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, 1) @@ -306,7 +305,7 @@ def test_missing_span_id(self): FORMAT.FLAGS_KEY: "1", } - ctx = FORMAT.extract(get_as_list, carrier) + ctx = FORMAT.extract(carrier_getter, carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) @@ -321,11 +320,12 @@ def test_inject_empty_context(): def test_default_span(): """Make sure propagator does not crash when working with DefaultSpan""" - def getter(carrier, key): - return carrier.get(key, None) + class CarrierGetter(DictGetter): + def get(self, carrier, key): + return carrier.get(key, None) def setter(carrier, key, value): carrier[key] = value - ctx = FORMAT.extract(getter, {}) + ctx = FORMAT.extract(CarrierGetter(), {}) FORMAT.inject(setter, {}, ctx) diff --git a/tests/util/src/opentelemetry/test/mock_textmap.py b/tests/util/src/opentelemetry/test/mock_textmap.py index bf46ec32fa..1fee6d2a9c 100644 --- a/tests/util/src/opentelemetry/test/mock_textmap.py +++ b/tests/util/src/opentelemetry/test/mock_textmap.py @@ -33,7 +33,7 @@ class NOOPTextMapPropagator(TextMapPropagator): def extract( self, - get_from_carrier: Getter[TextMapPropagatorT], + getter: Getter[TextMapPropagatorT], carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: @@ -56,12 +56,12 @@ class MockTextMapPropagator(TextMapPropagator): def extract( self, - get_from_carrier: Getter[TextMapPropagatorT], + getter: Getter[TextMapPropagatorT], carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: - trace_id_list = get_from_carrier(carrier, self.TRACE_ID_KEY) - span_id_list = get_from_carrier(carrier, self.SPAN_ID_KEY) + trace_id_list = getter.get(carrier, self.TRACE_ID_KEY) + span_id_list = getter.get(carrier, self.SPAN_ID_KEY) if not trace_id_list or not span_id_list: return trace.set_span_in_context(trace.INVALID_SPAN) From 4680594cb5333f27e877fcf3f0a6b40f665cc802 Mon Sep 17 00:00:00 2001 From: Baymax Date: Mon, 2 Nov 2020 11:12:07 +0530 Subject: [PATCH 0641/1517] Update docs (#1312) --- README.md | 2 +- .../src/opentelemetry_example_app/grpc/hello_world_client.py | 2 +- .../src/opentelemetry_example_app/grpc/hello_world_server.py | 2 +- .../src/opentelemetry_example_app/grpc/route_guide_client.py | 2 +- .../src/opentelemetry_example_app/grpc/route_guide_server.py | 2 +- docs/getting-started.rst | 2 +- docs/index.rst | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 2 +- instrumentation/opentelemetry-instrumentation-celery/README.rst | 2 +- instrumentation/opentelemetry-instrumentation-redis/README.rst | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7c714e717c..507ebaf649 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ install](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs) ```sh pip install -e ./opentelemetry-api pip install -e ./opentelemetry-sdk -pip install -e ./ext/opentelemetry-instrumentation-{instrumentation} +pip install -e ./instrumentation/opentelemetry-instrumentation-{instrumentation} ``` ## Documentation diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py index 59dd1fa610..2d07dec829 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py @@ -21,7 +21,7 @@ to run these examples. To run this script in the context of the example app, install ``opentelemetry-example-app``:: - pip install -e ext/opentelemetry-instrumentation-grpc/ + pip install -e instrumentation/opentelemetry-instrumentation-grpc/ pip install -e docs/examples/opentelemetry-example-app Then run the server in one shell:: diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py index e43d38d284..29035f17aa 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py @@ -21,7 +21,7 @@ to run these examples. To run this script in the context of the example app, install ``opentelemetry-example-app``:: - pip install -e ext/opentelemetry-instrumentation-grpc/ + pip install -e instrumentation/opentelemetry-instrumentation-grpc/ pip install -e docs/examples/opentelemetry-example-app Then run the server in one shell:: diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py index 110c68d113..4e8b13704d 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py @@ -21,7 +21,7 @@ to run these examples. To run this script in the context of the example app, install ``opentelemetry-example-app``:: - pip install -e ext/opentelemetry-instrumentation-grpc/ + pip install -e instrumentation/opentelemetry-instrumentation-grpc/ pip install -e docs/examples/opentelemetry-example-app Then run the server in one shell:: diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py index 74edbd65b3..5e53da07c9 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py @@ -22,7 +22,7 @@ to run these examples. To run this script in the context of the example app, install ``opentelemetry-example-app``:: - pip install -e ext/opentelemetry-instrumentation-grpc/ + pip install -e instrumentation/opentelemetry-instrumentation-grpc/ pip install -e docs/examples/opentelemetry-example-app Then run the server in one shell:: diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 49d1ad3ee2..cc5108e979 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -150,7 +150,7 @@ The above is a great example, but it's very manual. Within the telemetry space, * Database calls To help instrument common scenarios, opentelemetry also has the concept of "instrumentations": packages that are designed to interface -with a specific framework or library, such as Flask and psycopg2. A list of the currently curated extension packages can be found :scm_web:`here `. +with a specific framework or library, such as Flask and psycopg2. A list of the currently curated extension packages can be found :scm_web:`here `. We will now instrument a basic Flask application that uses the requests library to send HTTP requests. First, install the instrumentation packages themselves: diff --git a/docs/index.rst b/docs/index.rst index c9c12e8a84..f79b28a1c4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,7 +30,7 @@ In addition, there are several extension packages which can be installed separat pip install opentelemetry-instrumentation-{instrumentation} These are for exporter and instrumentation packages respectively. -The packages can be found in :scm_web:`instrumentation/ directory of the repository `. +The packages can be found in :scm_web:`instrumentation ` and :scm_web:`exporter ` directory of the repository. Extensions ---------- diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index a071288c14..4d5f595836 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-exporter-zipkin +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/exporter/opentelemetry-exporter-zipkin platforms = any license = Apache-2.0 classifiers = diff --git a/instrumentation/opentelemetry-instrumentation-celery/README.rst b/instrumentation/opentelemetry-instrumentation-celery/README.rst index 307fd352b9..92e5a770a0 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/README.rst +++ b/instrumentation/opentelemetry-instrumentation-celery/README.rst @@ -63,6 +63,6 @@ accomplish this as shown in the example above. References ---------- -* `OpenTelemetry Celery Instrumentation `_ +* `OpenTelemetry Celery Instrumentation `_ * `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-redis/README.rst b/instrumentation/opentelemetry-instrumentation-redis/README.rst index 289a0be0c3..1a071ad0fe 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/README.rst +++ b/instrumentation/opentelemetry-instrumentation-redis/README.rst @@ -19,5 +19,5 @@ Installation References ---------- -* `OpenTelemetry Redis Instrumentation `_ +* `OpenTelemetry Redis Instrumentation `_ * `OpenTelemetry Project `_ From ce515a0c010348c25404eb8c1adf1a0656444ac5 Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Mon, 2 Nov 2020 07:37:35 -0800 Subject: [PATCH 0642/1517] Removing TracerProvider coupling from Tracer init (#1295) --- .../tests/test_datadog_exporter.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/__init__.py | 34 +++++++++++-------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py index 2a8b753e5f..7b94704a57 100644 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py +++ b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py @@ -40,7 +40,7 @@ def __init__(self, *args, **kwargs): def get_spans(tracer, exporter, shutdown=True): if shutdown: - tracer.source.shutdown() + tracer.span_processor.shutdown() spans = [ call_args[-1]["spans"] diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index d1aa226114..e5e8f64e30 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -16,6 +16,8 @@ ([#1289](https://github.com/open-telemetry/opentelemetry-python/pull/1289)) - Set initial checkpoint timestamp in aggregators ([#1237](https://github.com/open-telemetry/opentelemetry-python/pull/1237)) +- Remove TracerProvider coupling from Tracer init + ([#1295](https://github.com/open-telemetry/opentelemetry-python/pull/1295)) ## Version 0.14b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 400600bec8..04faa19ec2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -705,19 +705,22 @@ class _Span(Span): class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`. - - Args: - name: The name of the tracer. - shutdown_on_exit: Register an atexit hook to shut down the tracer when - the application exits. """ def __init__( self, - source: "TracerProvider", + sampler: sampling.Sampler, + resource: Resource, + span_processor: Union[ + SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor + ], + ids_generator: trace_api.IdsGenerator, instrumentation_info: InstrumentationInfo, ) -> None: - self.source = source + self.sampler = sampler + self.resource = resource + self.span_processor = span_processor + self.ids_generator = ids_generator self.instrumentation_info = instrumentation_info def start_as_current_span( @@ -759,7 +762,7 @@ def start_span( # pylint: disable=too-many-locals # is_valid determines root span if parent_span_context is None or not parent_span_context.is_valid: parent_span_context = None - trace_id = self.source.ids_generator.generate_trace_id() + trace_id = self.ids_generator.generate_trace_id() trace_flags = None trace_state = None else: @@ -772,7 +775,7 @@ def start_span( # pylint: disable=too-many-locals # exported. # The sampler may also add attributes to the newly-created span, e.g. # to include information about the sampling result. - sampling_result = self.source.sampler.should_sample( + sampling_result = self.sampler.should_sample( context, trace_id, name, attributes, links, ) @@ -783,7 +786,7 @@ def start_span( # pylint: disable=too-many-locals ) span_context = trace_api.SpanContext( trace_id, - self.source.ids_generator.generate_span_id(), + self.ids_generator.generate_span_id(), is_remote=False, trace_flags=trace_flags, trace_state=trace_state, @@ -796,10 +799,10 @@ def start_span( # pylint: disable=too-many-locals name=name, context=span_context, parent=parent_span_context, - sampler=self.source.sampler, - resource=self.source.resource, + sampler=self.sampler, + resource=self.resource, attributes=sampling_result.attributes.copy(), - span_processor=self.source._active_span_processor, + span_processor=self.span_processor, kind=kind, links=links, instrumentation_info=self.instrumentation_info, @@ -888,7 +891,10 @@ def get_tracer( instrumenting_module_name = "ERROR:MISSING MODULE NAME" logger.error("get_tracer called with missing module name.") return Tracer( - self, + self.sampler, + self.resource, + self._active_span_processor, + self.ids_generator, InstrumentationInfo( instrumenting_module_name, instrumenting_library_version ), From 21c23cbe2b3b4ded6a6f119abfa203a6b25b72f0 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 2 Nov 2020 08:01:02 -0800 Subject: [PATCH 0643/1517] fixing changelogs (#1315) Found a few discrepancies in the changelogs that needed to be fixed --- .../CHANGELOG.md | 15 ++++++++++----- .../CHANGELOG.md | 2 -- .../CHANGELOG.md | 5 ++++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index 2e399b202d..5fe7d9c2f7 100644 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -2,8 +2,16 @@ ## Unreleased -- Django instrumentation is now enabled by default but can be disabled by setting `OTEL_PYTHON_DJANGO_INSTRUMENT` to `False` ([#1239](https://github.com/open-telemetry/opentelemetry-python/pull/1239)) -- Bugfix use request.path replace request.get_full_path(). It will get correct span name ([#1309](https://github.com/open-telemetry/opentelemetry-python/pull/1309#)) +- Django instrumentation is now enabled by default but can be disabled by setting `OTEL_PYTHON_DJANGO_INSTRUMENT` to `False` + ([#1239](https://github.com/open-telemetry/opentelemetry-python/pull/1239)) +- Bugfix use request.path replace request.get_full_path(). It will get correct span name + ([#1309](https://github.com/open-telemetry/opentelemetry-python/pull/1309#)) +- Record span status and http.status_code attribute on exception + ([#1257](https://github.com/open-telemetry/opentelemetry-python/pull/1257)) +- Added capture of http.route + ([#1226](https://github.com/open-telemetry/opentelemetry-python/issues/1226)) +- Add support for tracking http metrics + ([#1230](https://github.com/open-telemetry/opentelemetry-python/pull/1230)) ## Version 0.14b0 @@ -11,9 +19,6 @@ Released 2020-10-13 - Changed span name extraction from request to comply semantic convention ([#992](https://github.com/open-telemetry/opentelemetry-python/pull/992)) - Added support for `OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS` ([#1154](https://github.com/open-telemetry/opentelemetry-python/pull/1154)) -- Added capture of http.route ([#1226](https://github.com/open-telemetry/opentelemetry-python/issues/1226)) -- Add support for tracking http metrics - ([#1230](https://github.com/open-telemetry/opentelemetry-python/pull/1230)) ## Version 0.13b0 diff --git a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md index c030e9e8c0..cdb6916d10 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md @@ -4,8 +4,6 @@ - Use `url.rule` instead of `request.endpoint` for span name ([#1260](https://github.com/open-telemetry/opentelemetry-python/pull/1260)) -- Record span status and http.status_code attribute on exception - ([#1257](https://github.com/open-telemetry/opentelemetry-python/pull/1257)) ## Version 0.13b0 diff --git a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md index 96089a9da1..40088edcf3 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add support for tracking http metrics + ([#1230](https://github.com/open-telemetry/opentelemetry-python/pull/1230)) + ## Version 0.13b0 Released 2020-09-17 @@ -10,7 +13,7 @@ Released 2020-09-17 ([#1040](https://github.com/open-telemetry/opentelemetry-python/pull/1040)) - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) -- Add support for tracking http metrics +- Add support for http metrics ([#1116](https://github.com/open-telemetry/opentelemetry-python/pull/1116)) ## Version 0.12b0 From c4e28b9ec9d27782207fb83e41bb49a21de711b0 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 2 Nov 2020 08:33:50 -0800 Subject: [PATCH 0644/1517] Exit the lock before logging (#1317) --- .../src/opentelemetry/sdk/trace/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 04faa19ec2..a350e3687a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -373,11 +373,15 @@ def _create_immutable_attributes(attributes): def _check_span_ended(func): def wrapper(self, *args, **kwargs): + already_ended = False with self._lock: # pylint: disable=protected-access - if self.end_time is not None: - logger.warning("Calling %s on an ended span.", func.__name__) - return - func(self, *args, **kwargs) + if self.end_time is None: + func(self, *args, **kwargs) + else: + already_ended = True + + if already_ended: + logger.warning("Tried calling %s on an ended span.", func.__name__) return wrapper From 725655a20b370c5f2efcab6797dd841bad8e8600 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 2 Nov 2020 09:00:06 -0800 Subject: [PATCH 0645/1517] [pre-release] Update changelogs, version [0.15b0] (#1320) --- docs/examples/error_hander/error_handler_0/setup.cfg | 2 +- .../error_handler_0/src/error_handler_0/version.py | 2 +- docs/examples/error_hander/error_handler_1/setup.cfg | 2 +- .../error_handler_1/src/error_handler_1/version.py | 2 +- docs/examples/opentelemetry-example-app/setup.cfg | 8 ++++---- .../src/opentelemetry_example_app/version.py | 2 +- exporter/opentelemetry-exporter-datadog/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-datadog/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/datadog/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- .../setup.cfg | 4 ++-- .../instrumentation/aiohttp_client/version.py | 2 +- .../opentelemetry-instrumentation-aiopg/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/aiopg/version.py | 2 +- .../opentelemetry-instrumentation-asgi/setup.cfg | 4 ++-- .../src/opentelemetry/instrumentation/asgi/version.py | 2 +- .../opentelemetry-instrumentation-asyncpg/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/asyncpg/version.py | 2 +- .../opentelemetry-instrumentation-boto/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/boto/version.py | 2 +- .../opentelemetry-instrumentation-botocore/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/botocore/version.py | 2 +- .../opentelemetry-instrumentation-celery/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/celery/version.py | 2 +- .../opentelemetry-instrumentation-dbapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/dbapi/version.py | 2 +- .../opentelemetry-instrumentation-django/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-django/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/django/version.py | 2 +- .../opentelemetry-instrumentation-elasticsearch/setup.cfg | 6 +++--- .../instrumentation/elasticsearch/version.py | 2 +- .../opentelemetry-instrumentation-falcon/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/falcon/version.py | 2 +- .../opentelemetry-instrumentation-fastapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/fastapi/version.py | 2 +- .../opentelemetry-instrumentation-flask/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-flask/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/flask/version.py | 2 +- .../opentelemetry-instrumentation-grpc/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/grpc/version.py | 2 +- .../opentelemetry-instrumentation-jinja2/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/jinja2/version.py | 2 +- .../opentelemetry-instrumentation-mysql/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/mysql/version.py | 2 +- .../setup.cfg | 4 ++-- .../instrumentation/opentracing_shim/version.py | 2 +- .../opentelemetry-instrumentation-psycopg2/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/psycopg2/version.py | 2 +- .../opentelemetry-instrumentation-pymemcache/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/pymemcache/version.py | 2 +- .../opentelemetry-instrumentation-pymongo/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/pymongo/version.py | 2 +- .../opentelemetry-instrumentation-pymysql/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/pymysql/version.py | 2 +- .../opentelemetry-instrumentation-pyramid/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/pyramid/version.py | 2 +- .../opentelemetry-instrumentation-redis/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/redis/version.py | 2 +- .../opentelemetry-instrumentation-requests/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-requests/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/requests/version.py | 2 +- .../opentelemetry-instrumentation-sqlalchemy/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/sqlalchemy/version.py | 2 +- .../opentelemetry-instrumentation-sqlite3/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/sqlite3/version.py | 2 +- .../opentelemetry-instrumentation-starlette/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/starlette/version.py | 2 +- .../setup.cfg | 6 +++--- .../instrumentation/system_metrics/version.py | 2 +- .../opentelemetry-instrumentation-tornado/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/tornado/version.py | 2 +- .../opentelemetry-instrumentation-wsgi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/wsgi/version.py | 2 +- opentelemetry-api/CHANGELOG.md | 4 ++++ opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 4 ++++ opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 93 files changed, 196 insertions(+), 164 deletions(-) diff --git a/docs/examples/error_hander/error_handler_0/setup.cfg b/docs/examples/error_hander/error_handler_0/setup.cfg index a569d81b2a..cb4b775459 100644 --- a/docs/examples/error_hander/error_handler_0/setup.cfg +++ b/docs/examples/error_hander/error_handler_0/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.15.dev0 + opentelemetry-sdk == 0.15b0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py index e7b342d644..ff494d225a 100644 --- a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py +++ b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/docs/examples/error_hander/error_handler_1/setup.cfg b/docs/examples/error_hander/error_handler_1/setup.cfg index 0d3366d3d0..90a8283a22 100644 --- a/docs/examples/error_hander/error_handler_1/setup.cfg +++ b/docs/examples/error_hander/error_handler_1/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.15.dev0 + opentelemetry-sdk == 0.15b0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py index e7b342d644..ff494d225a 100644 --- a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py +++ b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index 2f42afdb30..8ac6be04b8 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -42,10 +42,10 @@ zip_safe = False include_package_data = True install_requires = typing; python_version<'3.5' - opentelemetry-api == 0.15.dev0 - opentelemetry-sdk == 0.15.dev0 - opentelemetry-instrumentation-requests == 0.15.dev0 - opentelemetry-instrumentation-flask == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-sdk == 0.15b0 + opentelemetry-instrumentation-requests == 0.15b0 + opentelemetry-instrumentation-flask == 0.15b0 flask requests protobuf~=3.11 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py index e7b342d644..ff494d225a 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/exporter/opentelemetry-exporter-datadog/CHANGELOG.md b/exporter/opentelemetry-exporter-datadog/CHANGELOG.md index 4c9b233a7b..b7308e8e5f 100644 --- a/exporter/opentelemetry-exporter-datadog/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-datadog/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.15b0 + +Released 2020-11-02 + - Make `SpanProcessor.on_start` accept parent Context ([#1251](https://github.com/open-telemetry/opentelemetry-python/pull/1251)) diff --git a/exporter/opentelemetry-exporter-datadog/setup.cfg b/exporter/opentelemetry-exporter-datadog/setup.cfg index d429374a6b..52ae4966b8 100644 --- a/exporter/opentelemetry-exporter-datadog/setup.cfg +++ b/exporter/opentelemetry-exporter-datadog/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = ddtrace>=0.34.0 - opentelemetry-api == 0.15.dev0 - opentelemetry-sdk == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-sdk == 0.15b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py index e7b342d644..ff494d225a 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md index b74ba6ea89..32454b4bca 100644 --- a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## Unreleased + +## Version 0.15b0 + +Released 2020-11-02 - Add support for Jaeger Span Exporter configuration by environment variables and
change JaegerSpanExporter constructor parameters ([#1114](https://github.com/open-telemetry/opentelemetry-python/pull/1114)) diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 8294af4caa..fa1a1b8535 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.15.dev0 - opentelemetry-sdk == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-sdk == 0.15b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index af61d19db8..5c7a3bef0b 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 2a8ecb2114..fd007d99d8 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.15.dev0 - opentelemetry-sdk == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-sdk == 0.15b0 protobuf >= 3.8.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index e7b342d644..ff494d225a 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index b1deb74e27..c67b9e997e 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.15b0 + +Released 2020-11-02 + - Add Env variables in OTLP exporter ([#1101](https://github.com/open-telemetry/opentelemetry-python/pull/1101)) - Do not use bound instruments in OTLP exporter diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 0f5870813e..3270c8a222 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.15.dev0 - opentelemetry-sdk == 0.15.dev0 - opentelemetry-proto == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-sdk == 0.15b0 + opentelemetry-proto == 0.15b0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index e7b342d644..ff494d225a 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index 31ef0160d8..b91be88df4 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.15.dev0 - opentelemetry-sdk == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-sdk == 0.15b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index e7b342d644..ff494d225a 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 4d5f595836..35f7cb2699 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 0.15.dev0 - opentelemetry-sdk == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-sdk == 0.15b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index e7b342d644..ff494d225a 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index 27e120d660..1d70349a5d 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 aiohttp ~= 3.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py index 0fdc34158f..c7ec3bc230 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg index 86a1b170f4..b3fcef8c4f 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation-dbapi == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation-dbapi == 0.15b0 + opentelemetry-instrumentation == 0.15b0 aiopg >= 0.13.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg index 68ac393560..d16de95bbf 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 asgiref ~= 3.0 [options.extras_require] diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg index 25e774c247..8d31ad031a 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 asyncpg >= 0.12.0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg index e3572fe526..18b85a0d5a 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg @@ -40,15 +40,15 @@ package_dir= packages=find_namespace: install_requires = boto ~= 2.0 - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 - opentelemetry-instrumentation-botocore == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 + opentelemetry-instrumentation-botocore == 0.15b0 [options.extras_require] test = boto~=2.0 moto~=1.0 - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py +++ b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg index ee7849c143..a299838577 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg @@ -40,13 +40,13 @@ package_dir= packages=find_namespace: install_requires = botocore ~= 1.0 - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 [options.extras_require] test = moto ~= 1.0 - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg index 9418e25a18..cffeb72c41 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 celery ~= 4.0 [options.extras_require] test = pytest celery ~= 4.0 - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg index e644d253a7..b42a03867c 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index 5fe7d9c2f7..5876936a0b 100644 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.15b0 + +Released 2020-11-02 + - Django instrumentation is now enabled by default but can be disabled by setting `OTEL_PYTHON_DJANGO_INSTRUMENT` to `False` ([#1239](https://github.com/open-telemetry/opentelemetry-python/pull/1239)) - Bugfix use request.path replace request.get_full_path(). It will get correct span name diff --git a/instrumentation/opentelemetry-instrumentation-django/setup.cfg b/instrumentation/opentelemetry-instrumentation-django/setup.cfg index 37ef9f2a86..4cc9b58632 100644 --- a/instrumentation/opentelemetry-instrumentation-django/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-django/setup.cfg @@ -40,13 +40,13 @@ package_dir= packages=find_namespace: install_requires = django >= 1.10 - opentelemetry-instrumentation-wsgi == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 - opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation-wsgi == 0.15b0 + opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.15b0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg index 7de55994fa..a36e03de99 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 wrapt >= 1.0.0, < 2.0.0 elasticsearch >= 2.0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 elasticsearch-dsl >= 2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg b/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg index 57849f5586..025728224e 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = falcon ~= 2.0 - opentelemetry-instrumentation-wsgi == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 - opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation-wsgi == 0.15b0 + opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.15b0 [options.extras_require] test = falcon ~= 2.0 - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 parameterized == 0.7.4 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg index ac77c870c0..c6fe3b54bf 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation-asgi == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation-asgi == 0.15b0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 fastapi ~= 0.58.1 requests ~= 2.23.0 # needed for testclient diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md index cdb6916d10..4c7e7a055b 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.15b0 + +Released 2020-11-02 + - Use `url.rule` instead of `request.endpoint` for span name ([#1260](https://github.com/open-telemetry/opentelemetry-python/pull/1260)) diff --git a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg index 1ea37e42ce..8d7edfe9cd 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg @@ -40,14 +40,14 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-instrumentation-wsgi == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 - opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation-wsgi == 0.15b0 + opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.15b0 [options.extras_require] test = flask~=1.0 - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg index 5d39574bfd..ed3c855836 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-sdk == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-sdk == 0.15b0 grpcio ~= 1.27 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 - opentelemetry-sdk == 0.15.dev0 + opentelemetry-test == 0.15b0 + opentelemetry-sdk == 0.15b0 protobuf == 3.12.2 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg index 55ee887b5a..bc54de3591 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg @@ -38,14 +38,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg index 957119a497..93cd514450 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation-dbapi == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation-dbapi == 0.15b0 + opentelemetry-instrumentation == 0.15b0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg index 8247d1d517..ab560b613c 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg @@ -41,11 +41,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.15.dev0 + opentelemetry-api == 0.15b0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg index e81d9c6b7a..52501cc181 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation-dbapi == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation-dbapi == 0.15b0 + opentelemetry-instrumentation == 0.15b0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg index d3cf45713f..5971f27fe8 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 pymemcache ~= 1.3 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg index f822ecbac8..76a2d0f5b1 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 pymongo ~= 3.1 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg index c047563e3d..59134c0afd 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation-dbapi == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation-dbapi == 0.15b0 + opentelemetry-instrumentation == 0.15b0 PyMySQL ~= 0.10.1 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg index 7e57a7bfae..f1567c24d9 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg @@ -40,15 +40,15 @@ package_dir= packages=find_namespace: install_requires = pyramid >= 1.7 - opentelemetry-instrumentation == 0.15.dev0 - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation-wsgi == 0.15.dev0 + opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation-wsgi == 0.15b0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = werkzeug == 0.16.1 - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg index 2a8a605eac..4d7372d1f4 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 redis >= 2.6 wrapt >= 1.12.1 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 - opentelemetry-sdk == 0.15.dev0 + opentelemetry-test == 0.15b0 + opentelemetry-sdk == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md index 40088edcf3..00d730f4f4 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.15b0 + +Released 2020-11-02 + - Add support for tracking http metrics ([#1230](https://github.com/open-telemetry/opentelemetry-python/pull/1230)) diff --git a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg index cf3c11d7e7..9cdb5b8d05 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 requests ~= 2.0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 httpretty ~= 1.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg index f9f9dafb02..1a4ad47118 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 wrapt >= 1.11.2 sqlalchemy [options.extras_require] test = - opentelemetry-sdk == 0.15.dev0 + opentelemetry-sdk == 0.15b0 pytest [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg index 9b49846669..1bb6ea1237 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation-dbapi == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation-dbapi == 0.15b0 + opentelemetry-instrumentation == 0.15b0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg index 5ad5fb7a82..6441b750f2 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation-asgi == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation-asgi == 0.15b0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 starlette ~= 0.13.0 requests ~= 2.23.0 # needed for testclient diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg index 750b5e07e2..f62eebeb8a 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-sdk == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-sdk == 0.15b0 psutil ~= 5.7.0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg b/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg index d3966fce72..12abd33535 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg @@ -39,13 +39,13 @@ package_dir= packages=find_namespace: install_requires = tornado >= 6.0 - opentelemetry-instrumentation == 0.15.dev0 - opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.15b0 [options.extras_require] test = tornado >= 6.0 - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg index 1570fd5d0e..f40a219bc6 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg @@ -39,12 +39,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.dev0 - opentelemetry-instrumentation == 0.15.dev0 + opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.15b0 [options.extras_require] test = - opentelemetry-test == 0.15.dev0 + opentelemetry-test == 0.15b0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py index e7b342d644..ff494d225a 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 175eba10c3..6f1d2b4f8e 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.15b0 + +Released 2020-11-02 + - Updating status codes to adhere to specs ([#1282](https://github.com/open-telemetry/opentelemetry-python/pull/1282)) ## Version 0.14b0 diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index e7b342d644..ff494d225a 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index a0787235c2..f81b53d6a9 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.15.dev0 + opentelemetry-api == 0.15b0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index e7b342d644..ff494d225a 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index e7b342d644..ff494d225a 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index e5e8f64e30..d6a7828d64 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.15b0 + +Released 2020-11-02 + - Make `SpanProcessor.on_start` accept parent Context ([#1251](https://github.com/open-telemetry/opentelemetry-python/pull/1251)) - Fix b3 propagator entrypoint diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 0995575270..02760cd9cc 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.15.dev0 + opentelemetry-api == 0.15b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index e7b342d644..ff494d225a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.dev0" +__version__ = "0.15b0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 68cfb03e92..fa3fb1a57e 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.15.dev0" +__version__ = "0.15b0" From a357d0a110bbdcaf1a8c35270fdf87211d449b56 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 2 Nov 2020 09:08:48 -0800 Subject: [PATCH 0646/1517] set dev version to 0.16.dev0 --- docs/examples/error_hander/error_handler_0/setup.cfg | 2 +- .../error_handler_0/src/error_handler_0/version.py | 2 +- docs/examples/error_hander/error_handler_1/setup.cfg | 2 +- .../error_handler_1/src/error_handler_1/version.py | 2 +- docs/examples/opentelemetry-example-app/setup.cfg | 8 ++++---- .../src/opentelemetry_example_app/version.py | 2 +- exporter/opentelemetry-exporter-datadog/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/datadog/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- .../setup.cfg | 4 ++-- .../instrumentation/aiohttp_client/version.py | 2 +- .../opentelemetry-instrumentation-aiopg/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/aiopg/version.py | 2 +- .../opentelemetry-instrumentation-asgi/setup.cfg | 4 ++-- .../src/opentelemetry/instrumentation/asgi/version.py | 2 +- .../opentelemetry-instrumentation-asyncpg/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/asyncpg/version.py | 2 +- .../opentelemetry-instrumentation-boto/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/boto/version.py | 2 +- .../opentelemetry-instrumentation-botocore/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/botocore/version.py | 2 +- .../opentelemetry-instrumentation-celery/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/celery/version.py | 2 +- .../opentelemetry-instrumentation-dbapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/dbapi/version.py | 2 +- .../opentelemetry-instrumentation-django/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/django/version.py | 2 +- .../opentelemetry-instrumentation-elasticsearch/setup.cfg | 6 +++--- .../instrumentation/elasticsearch/version.py | 2 +- .../opentelemetry-instrumentation-falcon/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/falcon/version.py | 2 +- .../opentelemetry-instrumentation-fastapi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/fastapi/version.py | 2 +- .../opentelemetry-instrumentation-flask/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/flask/version.py | 2 +- .../opentelemetry-instrumentation-grpc/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/grpc/version.py | 2 +- .../opentelemetry-instrumentation-jinja2/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/jinja2/version.py | 2 +- .../opentelemetry-instrumentation-mysql/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/mysql/version.py | 2 +- .../setup.cfg | 4 ++-- .../instrumentation/opentracing_shim/version.py | 2 +- .../opentelemetry-instrumentation-psycopg2/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/psycopg2/version.py | 2 +- .../opentelemetry-instrumentation-pymemcache/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/pymemcache/version.py | 2 +- .../opentelemetry-instrumentation-pymongo/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/pymongo/version.py | 2 +- .../opentelemetry-instrumentation-pymysql/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/pymysql/version.py | 2 +- .../opentelemetry-instrumentation-pyramid/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/pyramid/version.py | 2 +- .../opentelemetry-instrumentation-redis/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/redis/version.py | 2 +- .../opentelemetry-instrumentation-requests/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/requests/version.py | 2 +- .../opentelemetry-instrumentation-sqlalchemy/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/sqlalchemy/version.py | 2 +- .../opentelemetry-instrumentation-sqlite3/setup.cfg | 8 ++++---- .../src/opentelemetry/instrumentation/sqlite3/version.py | 2 +- .../opentelemetry-instrumentation-starlette/setup.cfg | 6 +++--- .../opentelemetry/instrumentation/starlette/version.py | 2 +- .../setup.cfg | 6 +++--- .../instrumentation/system_metrics/version.py | 2 +- .../opentelemetry-instrumentation-tornado/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/tornado/version.py | 2 +- .../opentelemetry-instrumentation-wsgi/setup.cfg | 6 +++--- .../src/opentelemetry/instrumentation/wsgi/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 85 files changed, 164 insertions(+), 164 deletions(-) diff --git a/docs/examples/error_hander/error_handler_0/setup.cfg b/docs/examples/error_hander/error_handler_0/setup.cfg index cb4b775459..3ea84d43d8 100644 --- a/docs/examples/error_hander/error_handler_0/setup.cfg +++ b/docs/examples/error_hander/error_handler_0/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.15b0 + opentelemetry-sdk == 0.16.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py index ff494d225a..1f98d44fa8 100644 --- a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py +++ b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/docs/examples/error_hander/error_handler_1/setup.cfg b/docs/examples/error_hander/error_handler_1/setup.cfg index 90a8283a22..6b9ab21e4b 100644 --- a/docs/examples/error_hander/error_handler_1/setup.cfg +++ b/docs/examples/error_hander/error_handler_1/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.15b0 + opentelemetry-sdk == 0.16.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py index ff494d225a..1f98d44fa8 100644 --- a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py +++ b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index 8ac6be04b8..8e55d321d4 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -42,10 +42,10 @@ zip_safe = False include_package_data = True install_requires = typing; python_version<'3.5' - opentelemetry-api == 0.15b0 - opentelemetry-sdk == 0.15b0 - opentelemetry-instrumentation-requests == 0.15b0 - opentelemetry-instrumentation-flask == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-sdk == 0.16.dev0 + opentelemetry-instrumentation-requests == 0.16.dev0 + opentelemetry-instrumentation-flask == 0.16.dev0 flask requests protobuf~=3.11 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py index ff494d225a..1f98d44fa8 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/exporter/opentelemetry-exporter-datadog/setup.cfg b/exporter/opentelemetry-exporter-datadog/setup.cfg index 52ae4966b8..45bf200ee4 100644 --- a/exporter/opentelemetry-exporter-datadog/setup.cfg +++ b/exporter/opentelemetry-exporter-datadog/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = ddtrace>=0.34.0 - opentelemetry-api == 0.15b0 - opentelemetry-sdk == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-sdk == 0.16.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py index ff494d225a..1f98d44fa8 100644 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py +++ b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index fa1a1b8535..b13b95653b 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.15b0 - opentelemetry-sdk == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-sdk == 0.16.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 5c7a3bef0b..7b04708d09 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index fd007d99d8..2b7de23b14 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.15b0 - opentelemetry-sdk == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-sdk == 0.16.dev0 protobuf >= 3.8.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index ff494d225a..1f98d44fa8 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 3270c8a222..54f6fc7772 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.15b0 - opentelemetry-sdk == 0.15b0 - opentelemetry-proto == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-sdk == 0.16.dev0 + opentelemetry-proto == 0.16.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index ff494d225a..1f98d44fa8 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index b91be88df4..182ffec5c3 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.15b0 - opentelemetry-sdk == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-sdk == 0.16.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index ff494d225a..1f98d44fa8 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 35f7cb2699..b4b3ee207b 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 0.15b0 - opentelemetry-sdk == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-sdk == 0.16.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index ff494d225a..1f98d44fa8 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index 1d70349a5d..e0fcfbf4b0 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 aiohttp ~= 3.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py index c7ec3bc230..bb32120c79 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg index b3fcef8c4f..c903180e98 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation-dbapi == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation-dbapi == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 aiopg >= 0.13.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg index d16de95bbf..dafb837943 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 asgiref ~= 3.0 [options.extras_require] diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg index 8d31ad031a..0e0e32fc8b 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 asyncpg >= 0.12.0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg index 18b85a0d5a..fadbac4c5c 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg @@ -40,15 +40,15 @@ package_dir= packages=find_namespace: install_requires = boto ~= 2.0 - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 - opentelemetry-instrumentation-botocore == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 + opentelemetry-instrumentation-botocore == 0.16.dev0 [options.extras_require] test = boto~=2.0 moto~=1.0 - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py +++ b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg index a299838577..6bd0190ac2 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg @@ -40,13 +40,13 @@ package_dir= packages=find_namespace: install_requires = botocore ~= 1.0 - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 [options.extras_require] test = moto ~= 1.0 - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg index cffeb72c41..d1f8866a95 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 celery ~= 4.0 [options.extras_require] test = pytest celery ~= 4.0 - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg index b42a03867c..7c4daa4029 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-django/setup.cfg b/instrumentation/opentelemetry-instrumentation-django/setup.cfg index 4cc9b58632..44a921283d 100644 --- a/instrumentation/opentelemetry-instrumentation-django/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-django/setup.cfg @@ -40,13 +40,13 @@ package_dir= packages=find_namespace: install_requires = django >= 1.10 - opentelemetry-instrumentation-wsgi == 0.15b0 - opentelemetry-instrumentation == 0.15b0 - opentelemetry-api == 0.15b0 + opentelemetry-instrumentation-wsgi == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 + opentelemetry-api == 0.16.dev0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg index a36e03de99..b1ebcfe76a 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 wrapt >= 1.0.0, < 2.0.0 elasticsearch >= 2.0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 elasticsearch-dsl >= 2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg b/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg index 025728224e..88e287c607 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg @@ -41,14 +41,14 @@ package_dir= packages=find_namespace: install_requires = falcon ~= 2.0 - opentelemetry-instrumentation-wsgi == 0.15b0 - opentelemetry-instrumentation == 0.15b0 - opentelemetry-api == 0.15b0 + opentelemetry-instrumentation-wsgi == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 + opentelemetry-api == 0.16.dev0 [options.extras_require] test = falcon ~= 2.0 - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 parameterized == 0.7.4 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg index c6fe3b54bf..d6b6bdc54f 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation-asgi == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation-asgi == 0.16.dev0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 fastapi ~= 0.58.1 requests ~= 2.23.0 # needed for testclient diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg index 8d7edfe9cd..2e9f943ceb 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg @@ -40,14 +40,14 @@ package_dir= packages=find_namespace: install_requires = flask ~= 1.0 - opentelemetry-instrumentation-wsgi == 0.15b0 - opentelemetry-instrumentation == 0.15b0 - opentelemetry-api == 0.15b0 + opentelemetry-instrumentation-wsgi == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 + opentelemetry-api == 0.16.dev0 [options.extras_require] test = flask~=1.0 - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg index ed3c855836..da31572e02 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-sdk == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-sdk == 0.16.dev0 grpcio ~= 1.27 [options.extras_require] test = - opentelemetry-test == 0.15b0 - opentelemetry-sdk == 0.15b0 + opentelemetry-test == 0.16.dev0 + opentelemetry-sdk == 0.16.dev0 protobuf == 3.12.2 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg index bc54de3591..08f46ae105 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg @@ -38,14 +38,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 jinja2~=2.7 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg index 93cd514450..ff9984a0c5 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation-dbapi == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation-dbapi == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg index ab560b613c..33cad7b935 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg @@ -41,11 +41,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.15b0 + opentelemetry-api == 0.16.dev0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg index 52501cc181..db98bf1251 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation-dbapi == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation-dbapi == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg index 5971f27fe8..c352ba903c 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 pymemcache ~= 1.3 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg index 76a2d0f5b1..6ccf6f1002 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 pymongo ~= 3.1 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg index 59134c0afd..b84fe20d55 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation-dbapi == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation-dbapi == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 PyMySQL ~= 0.10.1 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg index f1567c24d9..44db6be961 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg @@ -40,15 +40,15 @@ package_dir= packages=find_namespace: install_requires = pyramid >= 1.7 - opentelemetry-instrumentation == 0.15b0 - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation-wsgi == 0.15b0 + opentelemetry-instrumentation == 0.16.dev0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation-wsgi == 0.16.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = werkzeug == 0.16.1 - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg index 4d7372d1f4..186e167dea 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg @@ -39,15 +39,15 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 redis >= 2.6 wrapt >= 1.12.1 [options.extras_require] test = - opentelemetry-test == 0.15b0 - opentelemetry-sdk == 0.15b0 + opentelemetry-test == 0.16.dev0 + opentelemetry-sdk == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg index 9cdb5b8d05..8aaec6e84a 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 requests ~= 2.0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 httpretty ~= 1.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg index 1a4ad47118..c6b44013ce 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 wrapt >= 1.11.2 sqlalchemy [options.extras_require] test = - opentelemetry-sdk == 0.15b0 + opentelemetry-sdk == 0.16.dev0 pytest [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg index 1bb6ea1237..d8145be391 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg @@ -39,14 +39,14 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation-dbapi == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation-dbapi == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 wrapt >= 1.0.0, < 2.0.0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg index 6441b750f2..04b1f3cbec 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation-asgi == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation-asgi == 0.16.dev0 [options.entry_points] opentelemetry_instrumentor = @@ -47,7 +47,7 @@ opentelemetry_instrumentor = [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 starlette ~= 0.13.0 requests ~= 2.23.0 # needed for testclient diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg index f62eebeb8a..f8c27e54d0 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg @@ -39,13 +39,13 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-sdk == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-sdk == 0.16.dev0 psutil ~= 5.7.0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg b/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg index 12abd33535..889eb0e97c 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg @@ -39,13 +39,13 @@ package_dir= packages=find_namespace: install_requires = tornado >= 6.0 - opentelemetry-instrumentation == 0.15b0 - opentelemetry-api == 0.15b0 + opentelemetry-instrumentation == 0.16.dev0 + opentelemetry-api == 0.16.dev0 [options.extras_require] test = tornado >= 6.0 - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg index f40a219bc6..585a513412 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg @@ -39,12 +39,12 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15b0 - opentelemetry-instrumentation == 0.15b0 + opentelemetry-api == 0.16.dev0 + opentelemetry-instrumentation == 0.16.dev0 [options.extras_require] test = - opentelemetry-test == 0.15b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py index ff494d225a..1f98d44fa8 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index ff494d225a..1f98d44fa8 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index f81b53d6a9..6080af2526 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.15b0 + opentelemetry-api == 0.16.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index ff494d225a..1f98d44fa8 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index ff494d225a..1f98d44fa8 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 02760cd9cc..d27a24cf49 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.15b0 + opentelemetry-api == 0.16.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index ff494d225a..1f98d44fa8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15b0" +__version__ = "0.16.dev0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index fa3fb1a57e..4c4951dcda 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.15b0" +__version__ = "0.16.dev0" From 1662ca995eeac4bfe395542a436460debf6770bd Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 2 Nov 2020 13:32:10 -0500 Subject: [PATCH 0647/1517] Allow samplers to modify TraceState (#1319) --- opentelemetry-sdk/CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/__init__.py | 4 ++-- .../src/opentelemetry/sdk/trace/sampling.py | 16 ++++++++++++++-- opentelemetry-sdk/tests/trace/test_sampling.py | 6 +++++- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index d6a7828d64..2d12e54d98 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -20,6 +20,8 @@ Released 2020-11-02 ([#1289](https://github.com/open-telemetry/opentelemetry-python/pull/1289)) - Set initial checkpoint timestamp in aggregators ([#1237](https://github.com/open-telemetry/opentelemetry-python/pull/1237)) +- Allow samplers to modify tracestate + ([#1319](https://github.com/open-telemetry/opentelemetry-python/pull/1319)) - Remove TracerProvider coupling from Tracer init ([#1295](https://github.com/open-telemetry/opentelemetry-python/pull/1295)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index a350e3687a..c0189e807e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -780,7 +780,7 @@ def start_span( # pylint: disable=too-many-locals # The sampler may also add attributes to the newly-created span, e.g. # to include information about the sampling result. sampling_result = self.sampler.should_sample( - context, trace_id, name, attributes, links, + context, trace_id, name, attributes, links, trace_state ) trace_flags = ( @@ -793,7 +793,7 @@ def start_span( # pylint: disable=too-many-locals self.ids_generator.generate_span_id(), is_remote=False, trace_flags=trace_flags, - trace_state=trace_state, + trace_state=sampling_result.trace_state, ) # Only record if is_recording() is true diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index dbb12d5d63..ffa51506ff 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -68,6 +68,7 @@ # pylint: disable=unused-import from opentelemetry.context import Context from opentelemetry.trace import Link, get_current_span +from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes @@ -93,6 +94,8 @@ class SamplingResult: decision: A sampling decision based off of whether the span is recorded and the sampled flag in trace flags in the span context. attributes: Attributes to add to the `opentelemetry.trace.Span`. + trace_state: The tracestate used for the `opentelemetry.trace.Span`. + Could possibly have been modified by the sampler. """ def __repr__(self) -> str: @@ -101,13 +104,17 @@ def __repr__(self) -> str: ) def __init__( - self, decision: Decision, attributes: Attributes = None, + self, + decision: Decision, + attributes: "Attributes" = None, + trace_state: "TraceState" = None, ) -> None: self.decision = decision if attributes is None: self.attributes = MappingProxyType({}) else: self.attributes = MappingProxyType(attributes) + self.trace_state = trace_state class Sampler(abc.ABC): @@ -119,6 +126,7 @@ def should_sample( name: str, attributes: Attributes = None, links: Sequence["Link"] = None, + trace_state: "TraceState" = None, ) -> "SamplingResult": pass @@ -140,10 +148,11 @@ def should_sample( name: str, attributes: Attributes = None, links: Sequence["Link"] = None, + trace_state: "TraceState" = None, ) -> "SamplingResult": if self._decision is Decision.DROP: return SamplingResult(self._decision) - return SamplingResult(self._decision, attributes) + return SamplingResult(self._decision, attributes, trace_state) def get_description(self) -> str: if self._decision is Decision.DROP: @@ -194,6 +203,7 @@ def should_sample( name: str, attributes: Attributes = None, links: Sequence["Link"] = None, + trace_state: "TraceState" = None, ) -> "SamplingResult": decision = Decision.DROP if trace_id & self.TRACE_ID_LIMIT < self.bound: @@ -226,6 +236,7 @@ def should_sample( name: str, attributes: Attributes = None, links: Sequence["Link"] = None, + trace_state: "TraceState" = None, ) -> "SamplingResult": if parent_context is not None: parent_span_context = get_current_span( @@ -246,6 +257,7 @@ def should_sample( name=name, attributes=attributes, links=links, + trace_state=trace_state, ) def get_description(self): diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index fad5816ca8..d51a59c106 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -47,14 +47,18 @@ def test_is_sampled(self): class TestSamplingResult(unittest.TestCase): def test_ctr(self): attributes = {"asd": "test"} + trace_state = dict() + # pylint: disable=E1137 + trace_state["test"] = "123" result = sampling.SamplingResult( - sampling.Decision.RECORD_ONLY, attributes + sampling.Decision.RECORD_ONLY, attributes, trace_state ) self.assertIs(result.decision, sampling.Decision.RECORD_ONLY) with self.assertRaises(TypeError): result.attributes["test"] = "mess-this-up" self.assertTrue(len(result.attributes), 1) self.assertEqual(result.attributes["asd"], "test") + self.assertEqual(result.trace_state["test"], "123") class TestSampler(unittest.TestCase): From f815a728a609ed4ba4fc24c55311ea01a36fc27d Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 2 Nov 2020 16:02:45 -0500 Subject: [PATCH 0648/1517] remove remaining checks in setup.py files for python3.4 (#1326) * remove remaining checks and type comments for python3.4 * remove variable annotation changes, that is still not supported in python3.5 :( --- docs/examples/opentelemetry-example-app/setup.cfg | 1 - opentelemetry-api/setup.cfg | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index 8e55d321d4..552bf7cef7 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -41,7 +41,6 @@ packages = find_namespace: zip_safe = False include_package_data = True install_requires = - typing; python_version<'3.5' opentelemetry-api == 0.16.dev0 opentelemetry-sdk == 0.16.dev0 opentelemetry-instrumentation-requests == 0.16.dev0 diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 4ad44c6451..c697e30854 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -41,8 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - typing; python_version<'3.5' - aiocontextvars; python_version<'3.7' and python_version>='3.5' + aiocontextvars; python_version<'3.7' [options.packages.find] where = src From f050819a659cdb378ed5a73ff49eac9efccabfb0 Mon Sep 17 00:00:00 2001 From: Wilbert Guo Date: Mon, 2 Nov 2020 19:14:35 -0800 Subject: [PATCH 0649/1517] [Issue #1107] Add gzip compression support for OTLP exporter (#1141) Co-authored-by: Aaron Abbott Co-authored-by: Leighton Chen --- .../opentelemetry-exporter-otlp/CHANGELOG.md | 2 + .../opentelemetry/exporter/otlp/__init__.py | 10 +++++ .../opentelemetry/exporter/otlp/exporter.py | 42 +++++++++++++++++-- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index c67b9e997e..4233ddded0 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add Gzip compression for exporter + ([#1141](https://github.com/open-telemetry/opentelemetry-python/pull/1141)) ## Version 0.15b0 Released 2020-11-02 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py index a4d8f46d4c..3aca014eda 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py @@ -26,6 +26,16 @@ .. _OTLP: https://github.com/open-telemetry/opentelemetry-collector/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +.. envvar:: OTEL_EXPORTER_OTLP_COMPRESSION + +The :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` environment variable allows a +compression algorithm to be passed to the OTLP exporter. The compression +algorithms that are supported include gzip and no compression. The value should +be in the format of a string "gzip" for gzip compression, and no value specified +if no compression is the desired choice. +Additional details are available `in the specification +`_. + .. code:: python from opentelemetry import trace diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index 9595c35f64..8a569e79b1 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -14,19 +14,20 @@ """OTLP Exporter""" +import enum import logging -import os from abc import ABC, abstractmethod from collections.abc import Mapping, Sequence from time import sleep from typing import Any, Callable, Dict, Generic, List, Optional from typing import Sequence as TypingSequence -from typing import Text, Tuple, TypeVar +from typing import Text, TypeVar from backoff import expo from google.rpc.error_details_pb2 import RetryInfo from grpc import ( ChannelCredentials, + Compression, RpcError, StatusCode, insecure_channel, @@ -47,6 +48,10 @@ ExportResultT = TypeVar("ExportResultT") +class OTLPCompression(enum.Enum): + gzip = "gzip" + + def _translate_key_values(key: Text, value: Any) -> KeyValue: if isinstance(value, bool): @@ -137,6 +142,7 @@ class OTLPExporterMixin( insecure: Connection type credentials: ChannelCredentials object for server authentication metadata: Metadata to send when exporting + compression: Compression algorithm to be used in channel timeout: Backend request timeout in seconds """ @@ -147,6 +153,7 @@ def __init__( credentials: Optional[ChannelCredentials] = None, headers: Optional[str] = None, timeout: Optional[int] = None, + compression: str = None, ): super().__init__() @@ -169,13 +176,40 @@ def __init__( ) self._collector_span_kwargs = None + if compression is None: + compression_algorithm = Compression.NoCompression + elif ( + compression in OTLPCompression._value2member_map_ + and OTLPCompression(compression) is OTLPCompression.gzip + ): + compression_algorithm = Compression.Gzip + else: + compression_str = Configuration().EXPORTER_OTLP_INSECURE or None + if compression_str is None: + compression_algorithm = Compression.NoCompression + elif ( + compression_str in OTLPCompression._value2member_map_ + and OTLPCompression(compression_str) is OTLPCompression.gzip + ): + compression_algorithm = Compression.Gzip + else: + raise ValueError( + "OTEL_EXPORTER_OTLP_COMPRESSION environment variable does not match gzip." + ) + if insecure: - self._client = self._stub(insecure_channel(endpoint)) + self._client = self._stub( + insecure_channel(endpoint, compression=compression_algorithm) + ) else: credentials = credentials or _load_credential_from_file( Configuration().EXPORTER_OTLP_CERTIFICATE ) - self._client = self._stub(secure_channel(endpoint, credentials)) + self._client = self._stub( + secure_channel( + endpoint, credentials, compression=compression_algorithm + ) + ) @abstractmethod def _translate_data( From 4acad017817bab163580b1dcd8aa69a8de2b55cf Mon Sep 17 00:00:00 2001 From: Shreya Gupta Date: Tue, 3 Nov 2020 21:33:08 +0530 Subject: [PATCH 0650/1517] add optional parameter to record_exception method (#1242) Co-authored-by: alrex --- opentelemetry-api/CHANGELOG.md | 3 ++- opentelemetry-api/src/opentelemetry/trace/span.py | 14 ++++++++++++-- opentelemetry-sdk/CHANGELOG.md | 3 ++- .../src/opentelemetry/sdk/trace/__init__.py | 7 ++++++- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 6f1d2b4f8e..3f4ed4f029 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -11,7 +11,8 @@ Released 2020-11-02 ## Version 0.14b0 Released 2020-10-13 - +- Add optional parameter to `record_exception` method + ([#1242](https://github.com/open-telemetry/opentelemetry-python/pull/1242)) - Add support for `OTEL_PROPAGATORS` ([#1123](https://github.com/open-telemetry/opentelemetry-python/pull/1123)) - Store `int`s as `int`s in the global Configuration object diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 2b206468af..5cf4c36a3e 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -79,7 +79,12 @@ def set_status(self, status: Status) -> None: """ @abc.abstractmethod - def record_exception(self, exception: Exception) -> None: + def record_exception( + self, + exception: Exception, + attributes: types.Attributes = None, + timestamp: typing.Optional[int] = None, + ) -> None: """Records an exception as a span event.""" def __enter__(self) -> "Span": @@ -263,7 +268,12 @@ def update_name(self, name: str) -> None: def set_status(self, status: Status) -> None: pass - def record_exception(self, exception: Exception) -> None: + def record_exception( + self, + exception: Exception, + attributes: types.Attributes = None, + timestamp: typing.Optional[int] = None, + ) -> None: pass diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 2d12e54d98..fc635bc239 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -28,7 +28,8 @@ Released 2020-11-02 ## Version 0.14b0 Released 2020-10-13 - +- Add optional parameter to `record_exception` method + ([#1242](https://github.com/open-telemetry/opentelemetry-python/pull/1242)) - Add timestamps to aggregators ([#1199](https://github.com/open-telemetry/opentelemetry-python/pull/1199)) - Add Global Error Handler diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index c0189e807e..ab90c9316c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -680,7 +680,12 @@ def __exit__( super().__exit__(exc_type, exc_val, exc_tb) - def record_exception(self, exception: Exception) -> None: + def record_exception( + self, + exception: Exception, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + ) -> None: """Records an exception as a span event.""" try: stacktrace = traceback.format_exc() From 28f35b8a6a094103c4aa0c16dd2a8f5b86a9058c Mon Sep 17 00:00:00 2001 From: Baymax Date: Tue, 3 Nov 2020 22:04:10 +0530 Subject: [PATCH 0651/1517] Update Span.record_exception to to adhere to specs (#1314) --- opentelemetry-api/CHANGELOG.md | 2 + opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 15 ++-- opentelemetry-sdk/tests/trace/test_trace.py | 79 +++++++++++++++++++ 4 files changed, 91 insertions(+), 7 deletions(-) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 3f4ed4f029..8054937312 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add optional parameter to `record_exception` method ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) + ## Version 0.15b0 Released 2020-11-02 diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index fc635bc239..f1790c0a58 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add optional parameter to `record_exception` method ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) + ## Version 0.15b0 Released 2020-11-02 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index ab90c9316c..6b99888a46 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -694,14 +694,15 @@ def record_exception( # an AttributeError if the __context__ on # an exception is None stacktrace = "Exception occurred on stacktrace formatting" - + _attributes = { + "exception.type": exception.__class__.__name__, + "exception.message": str(exception), + "exception.stacktrace": stacktrace, + } + if attributes: + _attributes.update(attributes) self.add_event( - name="exception", - attributes={ - "exception.type": exception.__class__.__name__, - "exception.message": str(exception), - "exception.stacktrace": stacktrace, - }, + name="exception", attributes=_attributes, timestamp=timestamp ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index d08e12dee2..521bde00c8 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -874,6 +874,85 @@ def test_record_exception(self): exception_event.attributes["exception.stacktrace"], ) + def test_record_exception_with_attributes(self): + span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) + try: + raise RuntimeError("error") + except RuntimeError as err: + attributes = {"has_additional_attributes": True} + span.record_exception(err, attributes) + exception_event = span.events[0] + self.assertEqual("exception", exception_event.name) + self.assertEqual( + "error", exception_event.attributes["exception.message"] + ) + self.assertEqual( + "RuntimeError", exception_event.attributes["exception.type"] + ) + self.assertIn( + "RuntimeError: error", + exception_event.attributes["exception.stacktrace"], + ) + self.assertIn( + "has_additional_attributes", exception_event.attributes, + ) + self.assertEqual( + True, exception_event.attributes["has_additional_attributes"], + ) + + def test_record_exception_with_timestamp(self): + span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) + try: + raise RuntimeError("error") + except RuntimeError as err: + timestamp = 1604238587112021089 + span.record_exception(err, timestamp=timestamp) + exception_event = span.events[0] + self.assertEqual("exception", exception_event.name) + self.assertEqual( + "error", exception_event.attributes["exception.message"] + ) + self.assertEqual( + "RuntimeError", exception_event.attributes["exception.type"] + ) + self.assertIn( + "RuntimeError: error", + exception_event.attributes["exception.stacktrace"], + ) + self.assertEqual( + 1604238587112021089, exception_event.timestamp, + ) + + def test_record_exception_with_attributes_and_timestamp(self): + span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) + try: + raise RuntimeError("error") + except RuntimeError as err: + attributes = {"has_additional_attributes": True} + timestamp = 1604238587112021089 + span.record_exception(err, attributes, timestamp) + exception_event = span.events[0] + self.assertEqual("exception", exception_event.name) + self.assertEqual( + "error", exception_event.attributes["exception.message"] + ) + self.assertEqual( + "RuntimeError", exception_event.attributes["exception.type"] + ) + self.assertIn( + "RuntimeError: error", + exception_event.attributes["exception.stacktrace"], + ) + self.assertIn( + "has_additional_attributes", exception_event.attributes, + ) + self.assertEqual( + True, exception_event.attributes["has_additional_attributes"], + ) + self.assertEqual( + 1604238587112021089, exception_event.timestamp, + ) + def test_record_exception_context_manager(self): try: with self.tracer.start_as_current_span("span") as span: From b9d07d955ec30ca32fb8096ee35f0a40de1b4149 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 3 Nov 2020 08:51:39 -0800 Subject: [PATCH 0652/1517] updating docs now that metadata has been renamed (#1324) --- docs/getting_started/otlpcollector_example.py | 4 ++-- .../src/opentelemetry/exporter/otlp/exporter.py | 2 +- .../opentelemetry/exporter/otlp/metrics_exporter/__init__.py | 2 +- .../opentelemetry/exporter/otlp/trace_exporter/__init__.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/getting_started/otlpcollector_example.py b/docs/getting_started/otlpcollector_example.py index fa2407c158..f298f08fb1 100644 --- a/docs/getting_started/otlpcollector_example.py +++ b/docs/getting_started/otlpcollector_example.py @@ -27,7 +27,7 @@ # optional # endpoint:="myCollectorURL:55678", # credentials=ChannelCredentials(credentials), - # metadata=(("metadata", "metadata")), + # headers=(("metadata", "metadata")), ) tracer_provider = TracerProvider() trace.set_tracer_provider(tracer_provider) @@ -38,7 +38,7 @@ # optional # endpoint:="myCollectorURL:55678", # credentials=ChannelCredentials(credentials), - # metadata=(("metadata", "metadata")), + # headers=(("metadata", "metadata")), ) # Meter is responsible for creating and recording metrics diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index 8a569e79b1..77c0f0861e 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -141,7 +141,7 @@ class OTLPExporterMixin( endpoint: OpenTelemetry Collector receiver endpoint insecure: Connection type credentials: ChannelCredentials object for server authentication - metadata: Metadata to send when exporting + headers: Headers to send when exporting compression: Compression algorithm to be used in channel timeout: Backend request timeout in seconds """ diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py index 3a7ad586c6..2078664b43 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -121,7 +121,7 @@ class OTLPMetricsExporter( endpoint: OpenTelemetry Collector receiver endpoint insecure: Connection type credentials: Credentials object for server authentication - metadata: Metadata to send when exporting + headers: Headers to send when exporting timeout: Backend request timeout in seconds """ diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index 870e8fab0f..5059e042c3 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -58,7 +58,7 @@ class OTLPSpanExporter( endpoint: OpenTelemetry Collector receiver endpoint insecure: Connection type credentials: Credentials object for server authentication - metadata: Metadata to send when exporting + headers: Headers to send when exporting timeout: Backend request timeout in seconds """ From e3c3b9430e438d26d5cafb69154bb746d75a5294 Mon Sep 17 00:00:00 2001 From: Johannes Liebermann Date: Tue, 3 Nov 2020 19:47:33 +0100 Subject: [PATCH 0653/1517] Add missing "is" in OT shim docs (#1325) --- .../opentelemetry/instrumentation/opentracing_shim/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py index 00a0b8d0cb..65727ed559 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py @@ -496,7 +496,7 @@ def tracer(self) -> "TracerShim": span. Warning: - This property is *not* a part of the OpenTracing API. It used + This property is *not* a part of the OpenTracing API. It is used internally by the current implementation of the OpenTracing shim and will likely be removed in future versions. """ From e3cba025944844bc7793c521f286efbeecf1d83b Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 5 Nov 2020 10:29:41 -0600 Subject: [PATCH 0654/1517] Update protobuf versions (#1356) Fixes #1355 --- dev-requirements.txt | 4 ++-- docs/examples/opentelemetry-example-app/setup.cfg | 2 +- exporter/opentelemetry-exporter-opencensus/CHANGELOG.md | 3 +++ exporter/opentelemetry-exporter-opencensus/setup.cfg | 2 +- .../opentelemetry-instrumentation-grpc/CHANGELOG.md | 3 +++ instrumentation/opentelemetry-instrumentation-grpc/setup.cfg | 2 +- opentelemetry-proto/CHANGELOG.md | 3 +++ opentelemetry-proto/setup.cfg | 2 +- 8 files changed, 15 insertions(+), 6 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index b63248c7f0..489fc33fcb 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,5 +10,5 @@ pytest>=6.0 pytest-cov>=2.8 readme-renderer~=24.0 grpcio-tools==1.29.0 -mypy-protobuf==1.21 -protobuf==3.12.2 +mypy-protobuf>=1.23 +protobuf>=3.13.0 diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index 552bf7cef7..07362a2a81 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -47,7 +47,7 @@ install_requires = opentelemetry-instrumentation-flask == 0.16.dev0 flask requests - protobuf~=3.11 + protobuf>=3.13.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md index 3e02906563..33a182a3fd 100644 --- a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Update protobuf versions + ([#1356](https://github.com/open-telemetry/opentelemetry-python/pull/1356)) + ## Version 0.13b0 Released 2020-09-17 diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 2b7de23b14..a79a6b7e6d 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -43,7 +43,7 @@ install_requires = opencensus-proto >= 0.1.0, < 1.0.0 opentelemetry-api == 0.16.dev0 opentelemetry-sdk == 0.16.dev0 - protobuf >= 3.8.0 + protobuf >= 3.13.0 [options.packages.find] where = src diff --git a/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md index ed6889dcb7..51bceab9e8 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Update protobuf versions + ([#1356](https://github.com/open-telemetry/opentelemetry-python/pull/1356)) + ## Version 0.13b0 Released 2020-09-17 diff --git a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg index da31572e02..d1d3ff52d4 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg @@ -47,7 +47,7 @@ install_requires = test = opentelemetry-test == 0.16.dev0 opentelemetry-sdk == 0.16.dev0 - protobuf == 3.12.2 + protobuf >= 3.13.0 [options.packages.find] where = src diff --git a/opentelemetry-proto/CHANGELOG.md b/opentelemetry-proto/CHANGELOG.md index 0b6ba3f1f1..b8e1d48ce5 100644 --- a/opentelemetry-proto/CHANGELOG.md +++ b/opentelemetry-proto/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Update protobuf versions + ([#1356](https://github.com/open-telemetry/opentelemetry-python/pull/1356)) + ## Version 0.13b0 Released 2020-09-17 diff --git a/opentelemetry-proto/setup.cfg b/opentelemetry-proto/setup.cfg index 3cb2a70195..9f8087df66 100644 --- a/opentelemetry-proto/setup.cfg +++ b/opentelemetry-proto/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - protobuf~=3.12.2 + protobuf>=3.13.0 [options.packages.find] where = src From c47815d31f6a080f001ba7ad9192ff5ae3e47fa7 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Thu, 5 Nov 2020 09:46:24 -0800 Subject: [PATCH 0655/1517] Removes Instrumentation and Exporter Code From Core Repo (#1258) --- .flake8 | 1 + .github/workflows/test.yml | 20 +- .gitignore | 1 + .isort.cfg | 2 +- docs/examples/django/README.rst | 2 +- docs/examples/django/pages/views.py | 1 - .../opentelemetry-example-app/setup.cfg | 4 +- docs/exporter/datadog/datadog.rst | 7 - docs/getting-started.rst | 2 +- docs/index.rst | 6 +- .../aiohttp_client/aiohttp_client.rst | 7 - docs/instrumentation/aiopg/aiopg.rst | 7 - docs/instrumentation/asgi/asgi.rst | 9 - docs/instrumentation/asyncpg/asyncpg.rst | 10 - docs/instrumentation/boto/boto.rst | 7 - docs/instrumentation/botocore/botocore.rst | 7 - docs/instrumentation/celery/celery.rst | 7 - docs/instrumentation/dbapi/dbapi.rst | 7 - docs/instrumentation/django/django.rst | 7 - docs/instrumentation/fastapi/fastapi.rst | 9 - docs/instrumentation/flask/flask.rst | 7 - docs/instrumentation/grpc/grpc.rst | 10 - docs/instrumentation/jinja2/jinja2.rst | 7 - docs/instrumentation/mysql/mysql.rst | 7 - docs/instrumentation/psycopg2/psycopg2.rst | 7 - .../instrumentation/pymemcache/pymemcache.rst | 7 - docs/instrumentation/pymongo/pymongo.rst | 7 - docs/instrumentation/pymysql/pymysql.rst | 7 - docs/instrumentation/pyramid/pyramid.rst | 7 - docs/instrumentation/redis/redis.rst | 7 - docs/instrumentation/requests/requests.rst | 7 - .../instrumentation/sqlalchemy/sqlalchemy.rst | 7 - docs/instrumentation/sqlite3/sqlite3.rst | 7 - docs/instrumentation/starlette/starlette.rst | 9 - .../system_metrics/system_metrics.rst | 7 - docs/instrumentation/wsgi/wsgi.rst | 7 - eachdist.ini | 3 + .../CHANGELOG.md | 31 - .../opentelemetry-exporter-datadog/README.rst | 29 - .../opentelemetry-exporter-datadog/setup.cfg | 50 -- .../opentelemetry-exporter-datadog/setup.py | 27 - .../exporter/datadog/__init__.py | 76 -- .../exporter/datadog/constants.py | 9 - .../exporter/datadog/exporter.py | 317 -------- .../exporter/datadog/propagator.py | 125 ---- .../exporter/datadog/spanprocessor.py | 225 ------ .../opentelemetry/exporter/datadog/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_datadog_exporter.py | 604 --------------- .../tests/test_datadog_format.py | 171 ----- .../CHANGELOG.md | 25 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 24 - .../setup.cfg | 55 -- .../setup.py | 31 - .../aiohttp_client/__init__.py | 315 -------- .../instrumentation/aiohttp_client/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_aiohttp_client_integration.py | 500 ------------- .../CHANGELOG.md | 9 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 21 - .../setup.cfg | 58 -- .../setup.py | 26 - .../instrumentation/aiopg/__init__.py | 121 --- .../aiopg/aiopg_integration.py | 146 ---- .../instrumentation/aiopg/version.py | 15 - .../instrumentation/aiopg/wrappers.py | 223 ------ .../tests/__init__.py | 0 .../tests/test_aiopg_integration.py | 504 ------------- .../CHANGELOG.md | 16 - .../README.rst | 71 -- .../setup.cfg | 51 -- .../setup.py | 26 - .../instrumentation/asgi/__init__.py | 208 ------ .../instrumentation/asgi/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_asgi_middleware.py | 361 --------- .../CHANGELOG.md | 23 - .../README.rst | 23 - .../setup.cfg | 55 -- .../setup.py | 31 - .../instrumentation/asyncpg/__init__.py | 130 ---- .../instrumentation/asyncpg/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_asyncpg_wrapper.py | 35 - .../CHANGELOG.md | 31 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 23 - .../setup.cfg | 58 -- .../setup.py | 26 - .../instrumentation/boto/__init__.py | 206 ----- .../instrumentation/boto/version.py | 15 - .../tests/__init__.py | 0 .../tests/conftest.py | 31 - .../tests/test_boto_instrumentation.py | 294 -------- .../CHANGELOG.md | 30 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 23 - .../setup.cfg | 56 -- .../setup.py | 31 - .../instrumentation/botocore/__init__.py | 222 ------ .../instrumentation/botocore/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_botocore_instrumentation.py | 277 ------- .../CHANGELOG.md | 23 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 68 -- .../setup.cfg | 57 -- .../setup.py | 26 - .../instrumentation/celery/__init__.py | 262 ------- .../instrumentation/celery/utils.py | 221 ------ .../instrumentation/celery/version.py | 15 - .../tests/__init__.py | 0 .../tests/celery_test_tasks.py | 29 - .../tests/test_tasks.py | 78 -- .../tests/test_utils.py | 232 ------ .../CHANGELOG.md | 31 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 21 - .../setup.cfg | 51 -- .../setup.py | 26 - .../instrumentation/dbapi/__init__.py | 385 ---------- .../instrumentation/dbapi/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_dbapi_integration.py | 229 ------ .../CHANGELOG.md | 60 -- .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 53 -- .../setup.cfg | 56 -- .../setup.py | 32 - .../instrumentation/django/__init__.py | 87 --- .../instrumentation/django/middleware.py | 232 ------ .../instrumentation/django/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_middleware.py | 315 -------- .../tests/views.py | 31 - .../CHANGELOG.md | 24 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 23 - .../setup.cfg | 57 -- .../setup.py | 31 - .../instrumentation/elasticsearch/__init__.py | 162 ---- .../instrumentation/elasticsearch/version.py | 15 - .../tests/__init__.py | 0 .../tests/helpers_es2.py | 33 - .../tests/helpers_es5.py | 33 - .../tests/helpers_es6.py | 33 - .../tests/helpers_es7.py | 31 - .../tests/test_elasticsearch.py | 327 -------- .../CHANGELOG.md | 15 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 53 -- .../setup.cfg | 59 -- .../setup.py | 26 - .../instrumentation/falcon/__init__.py | 222 ------ .../instrumentation/falcon/version.py | 15 - .../tests/__init__.py | 0 .../tests/app.py | 41 - .../tests/test_falcon.py | 201 ----- .../CHANGELOG.md | 9 - .../README.rst | 43 -- .../setup.cfg | 55 -- .../setup.py | 31 - .../instrumentation/fastapi/__init__.py | 82 -- .../instrumentation/fastapi/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_fastapi_instrumentation.py | 104 --- .../CHANGELOG.md | 56 -- .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 38 - .../setup.cfg | 53 -- .../setup.py | 33 - .../instrumentation/flask/__init__.py | 227 ------ .../instrumentation/flask/version.py | 15 - .../tests/__init__.py | 0 .../tests/base_test.py | 46 -- .../tests/test_automatic.py | 61 -- .../tests/test_programmatic.py | 180 ----- .../CHANGELOG.md | 46 -- .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 18 - .../setup.cfg | 58 -- .../setup.py | 27 - .../instrumentation/grpc/__init__.py | 248 ------ .../instrumentation/grpc/_client.py | 273 ------- .../instrumentation/grpc/_server.py | 246 ------ .../instrumentation/grpc/_utilities.py | 101 --- .../instrumentation/grpc/grpcext/__init__.py | 125 ---- .../grpc/grpcext/_interceptor.py | 254 ------- .../instrumentation/grpc/version.py | 15 - .../tests/__init__.py | 13 - .../tests/_client.py | 57 -- .../tests/_server.py | 87 --- .../tests/protobuf/test_server.proto | 34 - .../tests/protobuf/test_server_pb2.py | 215 ------ .../tests/protobuf/test_server_pb2_grpc.py | 205 ----- .../tests/test_client_interceptor.py | 295 -------- .../tests/test_server_interceptor.py | 295 -------- .../CHANGELOG.md | 23 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 21 - .../setup.cfg | 55 -- .../setup.py | 26 - .../instrumentation/jinja2/__init__.py | 147 ---- .../instrumentation/jinja2/version.py | 15 - .../tests/__init__.py | 0 .../tests/templates/base.html | 1 - .../tests/templates/template.html | 2 - .../tests/test_jinja2.py | 210 ------ .../CHANGELOG.md | 36 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 25 - .../setup.cfg | 57 -- .../setup.py | 26 - .../instrumentation/mysql/__init__.py | 114 --- .../instrumentation/mysql/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_mysql_integration.py | 115 --- .../CHANGELOG.md | 29 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 20 - .../setup.cfg | 57 -- .../setup.py | 31 - .../instrumentation/psycopg2/__init__.py | 115 --- .../instrumentation/psycopg2/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_psycopg2_integration.py | 141 ---- .../CHANGELOG.md | 23 - .../LICENSE | 201 ----- .../MANIFEST.IN | 9 - .../README.rst | 20 - .../setup.cfg | 56 -- .../setup.py | 32 - .../instrumentation/pymemcache/__init__.py | 199 ----- .../instrumentation/pymemcache/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_pymemcache.py | 541 -------------- .../tests/utils.py | 74 -- .../CHANGELOG.md | 43 -- .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 21 - .../setup.cfg | 55 -- .../setup.py | 31 - .../instrumentation/pymongo/__init__.py | 169 ----- .../instrumentation/pymongo/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_pymongo.py | 186 ----- .../CHANGELOG.md | 30 - .../README.rst | 20 - .../setup.cfg | 56 -- .../setup.py | 31 - .../instrumentation/pymysql/__init__.py | 114 --- .../instrumentation/pymysql/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_pymysql_integration.py | 116 --- .../CHANGELOG.md | 27 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 32 - .../setup.cfg | 58 -- .../setup.py | 31 - .../instrumentation/pyramid/__init__.py | 148 ---- .../instrumentation/pyramid/callbacks.py | 165 ---- .../instrumentation/pyramid/version.py | 15 - .../tests/__init__.py | 0 .../tests/pyramid_base_test.py | 54 -- .../tests/test_automatic.py | 79 -- .../tests/test_programmatic.py | 209 ------ .../CHANGELOG.md | 24 - .../MANIFEST.in | 9 - .../README.rst | 23 - .../setup.cfg | 57 -- .../setup.py | 26 - .../instrumentation/redis/__init__.py | 166 ----- .../instrumentation/redis/util.py | 57 -- .../instrumentation/redis/version.py | 15 - .../tests/__init__.py | 13 - .../tests/test_redis.py | 84 --- .../CHANGELOG.md | 59 -- .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 23 - .../setup.cfg | 56 -- .../setup.py | 31 - .../instrumentation/requests/__init__.py | 251 ------- .../instrumentation/requests/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_requests_integration.py | 380 ---------- .../CHANGELOG.md | 23 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 24 - .../setup.cfg | 57 -- .../setup.py | 31 - .../instrumentation/sqlalchemy/__init__.py | 91 --- .../instrumentation/sqlalchemy/engine.py | 148 ---- .../instrumentation/sqlalchemy/version.py | 15 - .../tests/__init__.py | 13 - .../tests/test_sqlalchemy.py | 73 -- .../CHANGELOG.md | 23 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 21 - .../setup.cfg | 56 -- .../setup.py | 31 - .../instrumentation/sqlite3/__init__.py | 110 --- .../instrumentation/sqlite3/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_sqlite3.py | 83 --- .../CHANGELOG.md | 9 - .../README.rst | 45 -- .../setup.cfg | 55 -- .../setup.py | 31 - .../instrumentation/starlette/__init__.py | 82 -- .../instrumentation/starlette/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_starlette_instrumentation.py | 102 --- .../CHANGELOG.md | 30 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 24 - .../setup.cfg | 51 -- .../setup.py | 31 - .../system_metrics/__init__.py | 683 ----------------- .../instrumentation/system_metrics/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_system_metrics.py | 703 ------------------ .../CHANGELOG.md | 15 - .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 51 -- .../setup.cfg | 51 -- .../setup.py | 38 - .../instrumentation/tornado/__init__.py | 273 ------- .../instrumentation/tornado/client.py | 81 -- .../instrumentation/tornado/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_instrumentation.py | 392 ---------- .../tests/tornado_test_app.py | 99 --- .../CHANGELOG.md | 51 -- .../LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 26 - .../setup.cfg | 50 -- .../setup.py | 26 - .../instrumentation/wsgi/__init__.py | 248 ------ .../instrumentation/wsgi/version.py | 15 - .../tests/__init__.py | 0 .../tests/test_wsgi_middleware.py | 359 --------- scripts/eachdist.py | 12 + .../tests/asyncpg/test_asyncpg_functional.py | 260 ------- .../tests/celery/conftest.py | 92 --- .../tests/celery/test_celery_functional.py | 537 ------------- .../tests/check_availability.py | 115 --- .../tests/docker-compose.yml | 38 - .../tests/mysql/test_mysql_functional.py | 112 --- .../tests/postgres/test_aiopg_functional.py | 193 ----- .../tests/postgres/test_psycopg_functional.py | 117 --- .../tests/pymongo/test_pymongo_functional.py | 112 --- .../tests/pymysql/test_pymysql_functional.py | 101 --- .../tests/redis/test_redis_functional.py | 118 --- .../tests/sqlalchemy_tests/__init__.py | 13 - .../tests/sqlalchemy_tests/mixins.py | 182 ----- .../tests/sqlalchemy_tests/test_instrument.py | 70 -- .../tests/sqlalchemy_tests/test_mysql.py | 82 -- .../tests/sqlalchemy_tests/test_postgres.py | 97 --- .../tests/sqlalchemy_tests/test_sqlite.py | 60 -- tox.ini | 269 +------ 384 files changed, 60 insertions(+), 31214 deletions(-) delete mode 100644 docs/exporter/datadog/datadog.rst delete mode 100644 docs/instrumentation/aiohttp_client/aiohttp_client.rst delete mode 100644 docs/instrumentation/aiopg/aiopg.rst delete mode 100644 docs/instrumentation/asgi/asgi.rst delete mode 100644 docs/instrumentation/asyncpg/asyncpg.rst delete mode 100644 docs/instrumentation/boto/boto.rst delete mode 100644 docs/instrumentation/botocore/botocore.rst delete mode 100644 docs/instrumentation/celery/celery.rst delete mode 100644 docs/instrumentation/dbapi/dbapi.rst delete mode 100644 docs/instrumentation/django/django.rst delete mode 100644 docs/instrumentation/fastapi/fastapi.rst delete mode 100644 docs/instrumentation/flask/flask.rst delete mode 100644 docs/instrumentation/grpc/grpc.rst delete mode 100644 docs/instrumentation/jinja2/jinja2.rst delete mode 100644 docs/instrumentation/mysql/mysql.rst delete mode 100644 docs/instrumentation/psycopg2/psycopg2.rst delete mode 100644 docs/instrumentation/pymemcache/pymemcache.rst delete mode 100644 docs/instrumentation/pymongo/pymongo.rst delete mode 100644 docs/instrumentation/pymysql/pymysql.rst delete mode 100644 docs/instrumentation/pyramid/pyramid.rst delete mode 100644 docs/instrumentation/redis/redis.rst delete mode 100644 docs/instrumentation/requests/requests.rst delete mode 100644 docs/instrumentation/sqlalchemy/sqlalchemy.rst delete mode 100644 docs/instrumentation/sqlite3/sqlite3.rst delete mode 100644 docs/instrumentation/starlette/starlette.rst delete mode 100644 docs/instrumentation/system_metrics/system_metrics.rst delete mode 100644 docs/instrumentation/wsgi/wsgi.rst delete mode 100644 exporter/opentelemetry-exporter-datadog/CHANGELOG.md delete mode 100644 exporter/opentelemetry-exporter-datadog/README.rst delete mode 100644 exporter/opentelemetry-exporter-datadog/setup.cfg delete mode 100644 exporter/opentelemetry-exporter-datadog/setup.py delete mode 100644 exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/__init__.py delete mode 100644 exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/constants.py delete mode 100644 exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py delete mode 100644 exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py delete mode 100644 exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py delete mode 100644 exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py delete mode 100644 exporter/opentelemetry-exporter-datadog/tests/__init__.py delete mode 100644 exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py delete mode 100644 exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aiopg/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-aiopg/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-aiopg/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-aiopg/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-aiopg/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aiopg/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py delete mode 100644 instrumentation/opentelemetry-instrumentation-asgi/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-asgi/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-asgi/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-asgi/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-asgi/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py delete mode 100644 instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-asyncpg/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-asyncpg/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-asyncpg/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py delete mode 100644 instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-boto/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-boto/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-boto/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-boto/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-boto/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-boto/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-boto/tests/conftest.py delete mode 100644 instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py delete mode 100644 instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-botocore/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-botocore/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-botocore/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-botocore/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-botocore/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-botocore/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py delete mode 100644 instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-celery/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-celery/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-celery/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-celery/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-celery/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py delete mode 100644 instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-celery/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-celery/tests/celery_test_tasks.py delete mode 100644 instrumentation/opentelemetry-instrumentation-celery/tests/test_tasks.py delete mode 100644 instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py delete mode 100644 instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-dbapi/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-dbapi/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-dbapi/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-dbapi/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-dbapi/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py delete mode 100644 instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-django/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-django/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-django/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-django/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-django/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py delete mode 100644 instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-django/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py delete mode 100644 instrumentation/opentelemetry-instrumentation-django/tests/views.py delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es2.py delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es5.py delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py delete mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py delete mode 100644 instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-falcon/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-falcon/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-falcon/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-falcon/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-falcon/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-falcon/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-falcon/tests/app.py delete mode 100644 instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py delete mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py delete mode 100644 instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-flask/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-flask/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-flask/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-flask/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-flask/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-flask/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py delete mode 100644 instrumentation/opentelemetry-instrumentation-flask/tests/test_automatic.py delete mode 100644 instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/_interceptor.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/tests/_client.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/tests/_server.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server.proto delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2_grpc.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py delete mode 100644 instrumentation/opentelemetry-instrumentation-grpc/tests/test_server_interceptor.py delete mode 100644 instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-jinja2/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-jinja2/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-jinja2/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-jinja2/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-jinja2/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-jinja2/tests/templates/base.html delete mode 100644 instrumentation/opentelemetry-instrumentation-jinja2/tests/templates/template.html delete mode 100644 instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py delete mode 100644 instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-mysql/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-mysql/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-mysql/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-mysql/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-mysql/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-mysql/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py delete mode 100644 instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-psycopg2/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-psycopg2/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-psycopg2/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-psycopg2/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-psycopg2/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-pymemcache/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-pymemcache/MANIFEST.IN delete mode 100644 instrumentation/opentelemetry-instrumentation-pymemcache/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-pymemcache/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymemcache/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymemcache/tests/utils.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-pymongo/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-pymongo/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-pymongo/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-pymongo/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymongo/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-pymysql/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-pymysql/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymysql/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-pyramid/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-pyramid/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-pyramid/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-pyramid/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pyramid/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py delete mode 100644 instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py delete mode 100644 instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-redis/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-redis/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-redis/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-redis/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py delete mode 100644 instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-redis/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py delete mode 100644 instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-requests/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-requests/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-requests/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-requests/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-requests/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-requests/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlite3/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlite3/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlite3/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlite3/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlite3/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py delete mode 100644 instrumentation/opentelemetry-instrumentation-starlette/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-starlette/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-starlette/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-starlette/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-starlette/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py delete mode 100644 instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-system-metrics/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-system-metrics/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-system-metrics/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-system-metrics/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-system-metrics/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py delete mode 100644 instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-tornado/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-tornado/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-tornado/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-tornado/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-tornado/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py delete mode 100644 instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-tornado/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py delete mode 100644 instrumentation/opentelemetry-instrumentation-tornado/tests/tornado_test_app.py delete mode 100644 instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-wsgi/LICENSE delete mode 100644 instrumentation/opentelemetry-instrumentation-wsgi/MANIFEST.in delete mode 100644 instrumentation/opentelemetry-instrumentation-wsgi/README.rst delete mode 100644 instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg delete mode 100644 instrumentation/opentelemetry-instrumentation-wsgi/setup.py delete mode 100644 instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py delete mode 100644 instrumentation/opentelemetry-instrumentation-wsgi/tests/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py delete mode 100644 tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py delete mode 100644 tests/opentelemetry-docker-tests/tests/celery/conftest.py delete mode 100644 tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py delete mode 100644 tests/opentelemetry-docker-tests/tests/check_availability.py delete mode 100644 tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py delete mode 100644 tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py delete mode 100644 tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py delete mode 100644 tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py delete mode 100644 tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py delete mode 100644 tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py delete mode 100644 tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/__init__.py delete mode 100644 tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py delete mode 100644 tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_instrument.py delete mode 100644 tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py delete mode 100644 tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py delete mode 100644 tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py diff --git a/.flake8 b/.flake8 index 2780677a64..8dabaabc12 100644 --- a/.flake8 +++ b/.flake8 @@ -22,3 +22,4 @@ exclude = docs/examples/opentelemetry-example-app/build/* opentelemetry-proto/build/* opentelemetry-proto/src/opentelemetry/proto/ + opentelemetry-python-contrib/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7df9dff5df..2d21961ee5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,8 @@ on: branches-ignore: - 'release/*' pull_request: +env: + CONTRIB_REPO_SHA: 5c9e043d6921550d82668788e3758a733fb11cb8 jobs: build: @@ -41,7 +43,14 @@ jobs: python-version: py35 package: instrumentation steps: - - uses: actions/checkout@v2 + - name: Checkout Core Repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v2 + - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} + uses: actions/checkout@v2 + with: + repository: open-telemetry/opentelemetry-python-contrib + ref: ${{ env.CONTRIB_REPO_SHA }} + path: opentelemetry-python-contrib - name: Set up Python ${{ env[matrix.python-version] }} uses: actions/setup-python@v2 with: @@ -64,7 +73,14 @@ jobs: name: ${{ matrix.tox-environment }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - name: Checkout Core Repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v2 + - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} + uses: actions/checkout@v2 + with: + repository: open-telemetry/opentelemetry-python-contrib + ref: ${{ env.CONTRIB_REPO_SHA }} + path: opentelemetry-python-contrib - name: Set up Python 3.8 uses: actions/setup-python@v2 with: diff --git a/.gitignore b/.gitignore index 75cdf09293..5378aadb36 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ lib64 __pycache__ venv*/ .venv*/ +opentelemetry-python-contrib/ # Installer logs pip-log.txt diff --git a/.isort.cfg b/.isort.cfg index fd2ecc7865..cbe7a33601 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -13,6 +13,6 @@ line_length=79 ; docs: https://github.com/timothycrosley/isort#multi-line-output-modes multi_line_output=3 skip=target -skip_glob=**/gen/*,.venv*/*,venv*/*,**/proto/* +skip_glob=**/gen/*,.venv*/*,venv*/*,**/proto/*,opentelemetry-python-contrib/* known_first_party=opentelemetry,opentelemetry_example_app known_third_party=psutil,pytest,redis,redis_opentracing diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 64ff0b32cf..4545dd57b3 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -111,4 +111,4 @@ References * `Django `_ * `OpenTelemetry Project `_ -* `OpenTelemetry Django extension `_ +* `OpenTelemetry Django extension `_ diff --git a/docs/examples/django/pages/views.py b/docs/examples/django/pages/views.py index 4083888e17..9d277e2b7f 100644 --- a/docs/examples/django/pages/views.py +++ b/docs/examples/django/pages/views.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. from django.http import HttpResponse - from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index 07362a2a81..b8b981d358 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -43,8 +43,8 @@ include_package_data = True install_requires = opentelemetry-api == 0.16.dev0 opentelemetry-sdk == 0.16.dev0 - opentelemetry-instrumentation-requests == 0.16.dev0 - opentelemetry-instrumentation-flask == 0.16.dev0 + opentelemetry-instrumentation-requests == 0.15.b0 + opentelemetry-instrumentation-flask == 0.15.b0 flask requests protobuf>=3.13.0 diff --git a/docs/exporter/datadog/datadog.rst b/docs/exporter/datadog/datadog.rst deleted file mode 100644 index 3b43c2bdf7..0000000000 --- a/docs/exporter/datadog/datadog.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Datadog Exporter -============================== - -.. automodule:: opentelemetry.exporter.datadog - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/getting-started.rst b/docs/getting-started.rst index cc5108e979..8129dfeac6 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -150,7 +150,7 @@ The above is a great example, but it's very manual. Within the telemetry space, * Database calls To help instrument common scenarios, opentelemetry also has the concept of "instrumentations": packages that are designed to interface -with a specific framework or library, such as Flask and psycopg2. A list of the currently curated extension packages can be found :scm_web:`here `. +with a specific framework or library, such as Flask and psycopg2. A list of the currently curated extension packages can be found `at the Contrib repo `_. We will now instrument a basic Flask application that uses the requests library to send HTTP requests. First, install the instrumentation packages themselves: diff --git a/docs/index.rst b/docs/index.rst index f79b28a1c4..e39b969b2c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,7 +30,10 @@ In addition, there are several extension packages which can be installed separat pip install opentelemetry-instrumentation-{instrumentation} These are for exporter and instrumentation packages respectively. -The packages can be found in :scm_web:`instrumentation ` and :scm_web:`exporter ` directory of the repository. +Some packages can be found in :scm_web:`instrumentation ` and :scm_web:`exporter ` +directory of the repository. The remaining packages can be found at the +`Contrib repo instrumentation `_ +and `Contrib repo exporter `_ directories. Extensions ---------- @@ -53,7 +56,6 @@ install cd opentelemetry-python pip install -e ./opentelemetry-api pip install -e ./opentelemetry-sdk - pip install -e ./instrumentation/opentelemetry-instrumentation-{instrumentation} .. toctree:: diff --git a/docs/instrumentation/aiohttp_client/aiohttp_client.rst b/docs/instrumentation/aiohttp_client/aiohttp_client.rst deleted file mode 100644 index f8549f07fa..0000000000 --- a/docs/instrumentation/aiohttp_client/aiohttp_client.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry aiohttp client Instrumentation -============================================ - -.. automodule:: opentelemetry.instrumentation.aiohttp_client - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/aiopg/aiopg.rst b/docs/instrumentation/aiopg/aiopg.rst deleted file mode 100644 index 9da450c4e7..0000000000 --- a/docs/instrumentation/aiopg/aiopg.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry aiopg Instrumentation -=================================== - -.. automodule:: opentelemetry.instrumentation.aiopg - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/asgi/asgi.rst b/docs/instrumentation/asgi/asgi.rst deleted file mode 100644 index b988e4de43..0000000000 --- a/docs/instrumentation/asgi/asgi.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. include:: ../../../instrumentation/opentelemetry-instrumentation-asgi/README.rst - -API ---- - -.. automodule:: opentelemetry.instrumentation.asgi - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/asyncpg/asyncpg.rst b/docs/instrumentation/asyncpg/asyncpg.rst deleted file mode 100644 index 745e83f51d..0000000000 --- a/docs/instrumentation/asyncpg/asyncpg.rst +++ /dev/null @@ -1,10 +0,0 @@ -Opentelemetry asyncpg Instrumentation -===================================== - -Module contents ---------------- - -.. automodule:: opentelemetry.instrumentation.asyncpg - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/boto/boto.rst b/docs/instrumentation/boto/boto.rst deleted file mode 100644 index c438c2466c..0000000000 --- a/docs/instrumentation/boto/boto.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Boto Instrumentation -================================== - -.. automodule:: opentelemetry.instrumentation.boto - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/botocore/botocore.rst b/docs/instrumentation/botocore/botocore.rst deleted file mode 100644 index eb8ea6bcf7..0000000000 --- a/docs/instrumentation/botocore/botocore.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Botocore Instrumentation -====================================== - -.. automodule:: opentelemetry.instrumentation.botocore - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/celery/celery.rst b/docs/instrumentation/celery/celery.rst deleted file mode 100644 index c85f3adb59..0000000000 --- a/docs/instrumentation/celery/celery.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Celery Instrumentation -==================================== - -.. automodule:: opentelemetry.instrumentation.celery - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/dbapi/dbapi.rst b/docs/instrumentation/dbapi/dbapi.rst deleted file mode 100644 index a20be63097..0000000000 --- a/docs/instrumentation/dbapi/dbapi.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Database API Instrumentation -========================================== - -.. automodule:: opentelemetry.instrumentation.dbapi - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/django/django.rst b/docs/instrumentation/django/django.rst deleted file mode 100644 index 8076730843..0000000000 --- a/docs/instrumentation/django/django.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Django Instrumentation -==================================== - -.. automodule:: opentelemetry.instrumentation.django - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/fastapi/fastapi.rst b/docs/instrumentation/fastapi/fastapi.rst deleted file mode 100644 index 09eb8a828b..0000000000 --- a/docs/instrumentation/fastapi/fastapi.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. include:: ../../../instrumentation/opentelemetry-instrumentation-fastapi/README.rst - -API ---- - -.. automodule:: opentelemetry.instrumentation.fastapi - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/instrumentation/flask/flask.rst b/docs/instrumentation/flask/flask.rst deleted file mode 100644 index ac8932acd4..0000000000 --- a/docs/instrumentation/flask/flask.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Flask Instrumentation -=================================== - -.. automodule:: opentelemetry.instrumentation.flask - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/grpc/grpc.rst b/docs/instrumentation/grpc/grpc.rst deleted file mode 100644 index 243f696143..0000000000 --- a/docs/instrumentation/grpc/grpc.rst +++ /dev/null @@ -1,10 +0,0 @@ -OpenTelemetry gRPC Instrumentation -================================== - -Module contents ---------------- - -.. automodule:: opentelemetry.instrumentation.grpc - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/jinja2/jinja2.rst b/docs/instrumentation/jinja2/jinja2.rst deleted file mode 100644 index 5c7143724c..0000000000 --- a/docs/instrumentation/jinja2/jinja2.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Jinja2 Instrumentation -==================================== - -.. automodule:: opentelemetry.instrumentation.jinja2 - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/mysql/mysql.rst b/docs/instrumentation/mysql/mysql.rst deleted file mode 100644 index 3a4a41542a..0000000000 --- a/docs/instrumentation/mysql/mysql.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry MySQL Instrumentation -=================================== - -.. automodule:: opentelemetry.instrumentation.mysql - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/psycopg2/psycopg2.rst b/docs/instrumentation/psycopg2/psycopg2.rst deleted file mode 100644 index 69be31b2d1..0000000000 --- a/docs/instrumentation/psycopg2/psycopg2.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Psycopg Instrumentation -===================================== - -.. automodule:: opentelemetry.instrumentation.psycopg2 - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/pymemcache/pymemcache.rst b/docs/instrumentation/pymemcache/pymemcache.rst deleted file mode 100644 index 2a77b829d9..0000000000 --- a/docs/instrumentation/pymemcache/pymemcache.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry pymemcache Instrumentation -======================================== - -.. automodule:: opentelemetry.instrumentation.pymemcache - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/pymongo/pymongo.rst b/docs/instrumentation/pymongo/pymongo.rst deleted file mode 100644 index 4eb68be27b..0000000000 --- a/docs/instrumentation/pymongo/pymongo.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry pymongo Instrumentation -===================================== - -.. automodule:: opentelemetry.instrumentation.pymongo - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/pymysql/pymysql.rst b/docs/instrumentation/pymysql/pymysql.rst deleted file mode 100644 index 26482292fe..0000000000 --- a/docs/instrumentation/pymysql/pymysql.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry PyMySQL Instrumentation -===================================== - -.. automodule:: opentelemetry.instrumentation.pymysql - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/pyramid/pyramid.rst b/docs/instrumentation/pyramid/pyramid.rst deleted file mode 100644 index 56abd2800b..0000000000 --- a/docs/instrumentation/pyramid/pyramid.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Pyramid Instrumentation -===================================== - -.. automodule:: opentelemetry.instrumentation.pyramid - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/redis/redis.rst b/docs/instrumentation/redis/redis.rst deleted file mode 100644 index 4e21bce24b..0000000000 --- a/docs/instrumentation/redis/redis.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Redis Instrumentation -=================================== - -.. automodule:: opentelemetry.instrumentation.redis - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/requests/requests.rst b/docs/instrumentation/requests/requests.rst deleted file mode 100644 index 7a0665cd99..0000000000 --- a/docs/instrumentation/requests/requests.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry requests Instrumentation -====================================== - -.. automodule:: opentelemetry.instrumentation.requests - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/sqlalchemy/sqlalchemy.rst b/docs/instrumentation/sqlalchemy/sqlalchemy.rst deleted file mode 100644 index 1a1895ea6b..0000000000 --- a/docs/instrumentation/sqlalchemy/sqlalchemy.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry SQLAlchemy Instrumentation -======================================== - -.. automodule:: opentelemetry.instrumentation.sqlalchemy - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/sqlite3/sqlite3.rst b/docs/instrumentation/sqlite3/sqlite3.rst deleted file mode 100644 index 36b541ccd1..0000000000 --- a/docs/instrumentation/sqlite3/sqlite3.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry SQLite3 Instrumentation -===================================== - -.. automodule:: opentelemetry.instrumentation.sqlite3 - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/starlette/starlette.rst b/docs/instrumentation/starlette/starlette.rst deleted file mode 100644 index 0efa8cce83..0000000000 --- a/docs/instrumentation/starlette/starlette.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. include:: ../../../instrumentation/opentelemetry-instrumentation-starlette/README.rst - -API ---- - -.. automodule:: opentelemetry.instrumentation.starlette - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/instrumentation/system_metrics/system_metrics.rst b/docs/instrumentation/system_metrics/system_metrics.rst deleted file mode 100644 index 96b39d9267..0000000000 --- a/docs/instrumentation/system_metrics/system_metrics.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry System Metrics Instrumentation -============================================ - -.. automodule:: opentelemetry.instrumentation.system_metrics - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/instrumentation/wsgi/wsgi.rst b/docs/instrumentation/wsgi/wsgi.rst deleted file mode 100644 index 39ad5ffd58..0000000000 --- a/docs/instrumentation/wsgi/wsgi.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry WSGI Instrumentation -================================== - -.. automodule:: opentelemetry.instrumentation.wsgi - :members: - :undoc-members: - :show-inheritance: diff --git a/eachdist.ini b/eachdist.ini index 076c8a4a4a..1b809374ad 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -1,6 +1,9 @@ # These will be sorted first in that order. # All packages that are depended upon by others should be listed here. [DEFAULT] +ignore= + opentelemetry-python-contrib + sortfirst= opentelemetry-api opentelemetry-sdk diff --git a/exporter/opentelemetry-exporter-datadog/CHANGELOG.md b/exporter/opentelemetry-exporter-datadog/CHANGELOG.md deleted file mode 100644 index b7308e8e5f..0000000000 --- a/exporter/opentelemetry-exporter-datadog/CHANGELOG.md +++ /dev/null @@ -1,31 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.15b0 - -Released 2020-11-02 - - - Make `SpanProcessor.on_start` accept parent Context - ([#1251](https://github.com/open-telemetry/opentelemetry-python/pull/1251)) - -## Version 0.14b0 - -Released 2020-10-13 - -- Add support for span resource labels and service name - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-exporter-datadog - ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) - -## 0.8b0 - -Released 2020-05-27 - -- Add exporter to Datadog - ([#572](https://github.com/open-telemetry/opentelemetry-python/pull/572)) - diff --git a/exporter/opentelemetry-exporter-datadog/README.rst b/exporter/opentelemetry-exporter-datadog/README.rst deleted file mode 100644 index cb97e5997f..0000000000 --- a/exporter/opentelemetry-exporter-datadog/README.rst +++ /dev/null @@ -1,29 +0,0 @@ -OpenTelemetry Datadog Exporter -============================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-datadog.svg - :target: https://pypi.org/project/opentelemetry-exporter-datadog/ - -This library allows to export tracing data to `Datadog -`_. OpenTelemetry span event and links are not -supported. - -Installation ------------- - -:: - - pip install opentelemetry-exporter-datadog - - -.. _Datadog: https://www.datadoghq.com/ -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ - - -References ----------- - -* `Datadog `_ -* `OpenTelemetry Project `_ diff --git a/exporter/opentelemetry-exporter-datadog/setup.cfg b/exporter/opentelemetry-exporter-datadog/setup.cfg deleted file mode 100644 index 45bf200ee4..0000000000 --- a/exporter/opentelemetry-exporter-datadog/setup.cfg +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-datadog -description = Datadog Span Exporter for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/exporter/opentelemetry-exporter-datadog -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - ddtrace>=0.34.0 - opentelemetry-api == 0.16.dev0 - opentelemetry-sdk == 0.16.dev0 - -[options.packages.find] -where = src - -[options.extras_require] -test = diff --git a/exporter/opentelemetry-exporter-datadog/setup.py b/exporter/opentelemetry-exporter-datadog/setup.py deleted file mode 100644 index 0c3bdf453f..0000000000 --- a/exporter/opentelemetry-exporter-datadog/setup.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "exporter", "datadog", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/__init__.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/__init__.py deleted file mode 100644 index 3294ba4e4e..0000000000 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/__init__.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The **OpenTelemetry Datadog Exporter** provides a span exporter from -`OpenTelemetry`_ traces to `Datadog`_ by using the Datadog Agent. - -Installation ------------- - -:: - - pip install opentelemetry-exporter-datadog - - -Usage ------ - -The Datadog exporter provides a span processor that must be added along with the -exporter. In addition, a formatter is provided to handle propagation of trace -context between OpenTelemetry-instrumented and Datadog-instrumented services in -a distributed trace. - -.. code:: python - - from opentelemetry import propagators, trace - from opentelemetry.exporter.datadog import DatadogExportSpanProcessor, DatadogSpanExporter - from opentelemetry.exporter.datadog.propagator import DatadogFormat - from opentelemetry.sdk.trace import TracerProvider - - trace.set_tracer_provider(TracerProvider()) - tracer = trace.get_tracer(__name__) - - exporter = DatadogSpanExporter( - agent_url="http://agent:8126", service="my-helloworld-service" - ) - - span_processor = DatadogExportSpanProcessor(exporter) - trace.get_tracer_provider().add_span_processor(span_processor) - - # Optional: use Datadog format for propagation in distributed traces - propagators.set_global_textmap(DatadogFormat()) - - with tracer.start_as_current_span("foo"): - print("Hello world!") - - -Examples --------- - -The `docs/examples/datadog_exporter`_ includes examples for using the Datadog -exporter with OpenTelemetry instrumented applications. - -API ---- -.. _Datadog: https://www.datadoghq.com/ -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ -.. _docs/examples/datadog_exporter: https://github.com/open-telemetry/opentelemetry-python/tree/master/docs/examples/datadog_exporter -""" -# pylint: disable=import-error - -from .exporter import DatadogSpanExporter -from .spanprocessor import DatadogExportSpanProcessor - -__all__ = ["DatadogExportSpanProcessor", "DatadogSpanExporter"] diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/constants.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/constants.py deleted file mode 100644 index 2ae5386e84..0000000000 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/constants.py +++ /dev/null @@ -1,9 +0,0 @@ -DD_ORIGIN = "_dd_origin" -AUTO_REJECT = 0 -AUTO_KEEP = 1 -USER_KEEP = 2 -SAMPLE_RATE_METRIC_KEY = "_sample_rate" -SAMPLING_PRIORITY_KEY = "_sampling_priority_v1" -ENV_KEY = "env" -VERSION_KEY = "version" -SERVICE_NAME_TAG = "service.name" diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py deleted file mode 100644 index 2b1bd90041..0000000000 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py +++ /dev/null @@ -1,317 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import os -from urllib.parse import urlparse - -from ddtrace.ext import SpanTypes as DatadogSpanTypes -from ddtrace.internal.writer import AgentWriter -from ddtrace.span import Span as DatadogSpan - -import opentelemetry.trace as trace_api -from opentelemetry.sdk.trace import sampling -from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult - -# pylint:disable=relative-beyond-top-level -from .constants import ( - DD_ORIGIN, - ENV_KEY, - SAMPLE_RATE_METRIC_KEY, - SERVICE_NAME_TAG, - VERSION_KEY, -) - -logger = logging.getLogger(__name__) - - -DEFAULT_AGENT_URL = "http://localhost:8126" -_INSTRUMENTATION_SPAN_TYPES = { - "opentelemetry.instrumentation.aiohttp-client": DatadogSpanTypes.HTTP, - "opentelemetry.instrumentation.asgi": DatadogSpanTypes.WEB, - "opentelemetry.instrumentation.dbapi": DatadogSpanTypes.SQL, - "opentelemetry.instrumentation.django": DatadogSpanTypes.WEB, - "opentelemetry.instrumentation.flask": DatadogSpanTypes.WEB, - "opentelemetry.instrumentation.grpc": DatadogSpanTypes.GRPC, - "opentelemetry.instrumentation.jinja2": DatadogSpanTypes.TEMPLATE, - "opentelemetry.instrumentation.mysql": DatadogSpanTypes.SQL, - "opentelemetry.instrumentation.psycopg2": DatadogSpanTypes.SQL, - "opentelemetry.instrumentation.pymemcache": DatadogSpanTypes.CACHE, - "opentelemetry.instrumentation.pymongo": DatadogSpanTypes.MONGODB, - "opentelemetry.instrumentation.pymysql": DatadogSpanTypes.SQL, - "opentelemetry.instrumentation.redis": DatadogSpanTypes.REDIS, - "opentelemetry.instrumentation.requests": DatadogSpanTypes.HTTP, - "opentelemetry.instrumentation.sqlalchemy": DatadogSpanTypes.SQL, - "opentelemetry.instrumentation.wsgi": DatadogSpanTypes.WEB, -} - - -class DatadogSpanExporter(SpanExporter): - """Datadog span exporter for OpenTelemetry. - - Args: - agent_url: The url of the Datadog Agent or use ``DD_TRACE_AGENT_URL`` environment variable - service: The service name to be used for the application or use ``DD_SERVICE`` environment variable - env: Set the application’s environment or use ``DD_ENV`` environment variable - version: Set the application’s version or use ``DD_VERSION`` environment variable - tags: A list of default tags to be added to every span or use ``DD_TAGS`` environment variable - """ - - def __init__( - self, agent_url=None, service=None, env=None, version=None, tags=None - ): - self.agent_url = ( - agent_url - if agent_url - else os.environ.get("DD_TRACE_AGENT_URL", DEFAULT_AGENT_URL) - ) - self.service = service or os.environ.get("DD_SERVICE") - self.env = env or os.environ.get("DD_ENV") - self.version = version or os.environ.get("DD_VERSION") - self.tags = _parse_tags_str(tags or os.environ.get("DD_TAGS")) - self._agent_writer = None - - @property - def agent_writer(self): - if self._agent_writer is None: - url_parsed = urlparse(self.agent_url) - if url_parsed.scheme in ("http", "https"): - self._agent_writer = AgentWriter( - hostname=url_parsed.hostname, - port=url_parsed.port, - https=url_parsed.scheme == "https", - ) - elif url_parsed.scheme == "unix": - self._agent_writer = AgentWriter(uds_path=url_parsed.path) - else: - raise ValueError( - "Unknown scheme `%s` for agent URL" % url_parsed.scheme - ) - return self._agent_writer - - def export(self, spans): - datadog_spans = self._translate_to_datadog(spans) - - self.agent_writer.write(spans=datadog_spans) - - return SpanExportResult.SUCCESS - - def shutdown(self): - if self.agent_writer.started: - self.agent_writer.stop() - self.agent_writer.join(self.agent_writer.exit_timeout) - - # pylint: disable=too-many-locals - def _translate_to_datadog(self, spans): - datadog_spans = [] - - for span in spans: - trace_id, parent_id, span_id = _get_trace_ids(span) - - # datadog Span is initialized with a reference to the tracer which is - # used to record the span when it is finished. We can skip ignore this - # because we are not calling the finish method and explictly set the - # duration. - tracer = None - - # extract resource attributes to be used as tags as well as potential service name - [ - resource_tags, - resource_service_name, - ] = _extract_tags_from_resource(span.resource) - - datadog_span = DatadogSpan( - tracer, - _get_span_name(span), - service=resource_service_name or self.service, - resource=_get_resource(span), - span_type=_get_span_type(span), - trace_id=trace_id, - span_id=span_id, - parent_id=parent_id, - ) - datadog_span.start_ns = span.start_time - datadog_span.duration_ns = span.end_time - span.start_time - - if not span.status.is_ok: - datadog_span.error = 1 - if span.status.description: - exc_type, exc_val = _get_exc_info(span) - # no mapping for error.stack since traceback not recorded - datadog_span.set_tag("error.msg", exc_val) - datadog_span.set_tag("error.type", exc_type) - - # combine resource attributes and span attributes, don't modify existing span attributes - combined_span_tags = {} - combined_span_tags.update(resource_tags) - combined_span_tags.update(span.attributes) - - datadog_span.set_tags(combined_span_tags) - - # add configured env tag - if self.env is not None: - datadog_span.set_tag(ENV_KEY, self.env) - - # add configured application version tag to only root span - if self.version is not None and parent_id == 0: - datadog_span.set_tag(VERSION_KEY, self.version) - - # add configured global tags - datadog_span.set_tags(self.tags) - - # add origin to root span - origin = _get_origin(span) - if origin and parent_id == 0: - datadog_span.set_tag(DD_ORIGIN, origin) - - sampling_rate = _get_sampling_rate(span) - if sampling_rate is not None: - datadog_span.set_metric(SAMPLE_RATE_METRIC_KEY, sampling_rate) - - # span events and span links are not supported - - datadog_spans.append(datadog_span) - - return datadog_spans - - -def _get_trace_ids(span): - """Extract tracer ids from span""" - ctx = span.get_span_context() - trace_id = ctx.trace_id - span_id = ctx.span_id - - if isinstance(span.parent, trace_api.Span): - parent_id = span.parent.get_span_context().span_id - elif isinstance(span.parent, trace_api.SpanContext): - parent_id = span.parent.span_id - else: - parent_id = 0 - - trace_id = _convert_trace_id_uint64(trace_id) - - return trace_id, parent_id, span_id - - -def _convert_trace_id_uint64(otel_id): - """Convert 128-bit int used for trace_id to 64-bit unsigned int""" - return otel_id & 0xFFFFFFFFFFFFFFFF - - -def _get_span_name(span): - """Get span name by using instrumentation and kind while backing off to - span.name - """ - instrumentation_name = ( - span.instrumentation_info.name if span.instrumentation_info else None - ) - span_kind_name = span.kind.name if span.kind else None - name = ( - "{}.{}".format(instrumentation_name, span_kind_name) - if instrumentation_name and span_kind_name - else span.name - ) - return name - - -def _get_resource(span): - """Get resource name for span""" - if "http.method" in span.attributes: - route = span.attributes.get("http.route") - return ( - span.attributes["http.method"] + " " + route - if route - else span.attributes["http.method"] - ) - - return span.name - - -def _get_span_type(span): - """Get Datadog span type""" - instrumentation_name = ( - span.instrumentation_info.name if span.instrumentation_info else None - ) - span_type = _INSTRUMENTATION_SPAN_TYPES.get(instrumentation_name) - return span_type - - -def _get_exc_info(span): - """Parse span status description for exception type and value""" - exc_type, exc_val = span.status.description.split(":", 1) - return exc_type, exc_val.strip() - - -def _get_origin(span): - ctx = span.get_span_context() - origin = ctx.trace_state.get(DD_ORIGIN) - return origin - - -def _get_sampling_rate(span): - ctx = span.get_span_context() - return ( - span.sampler.rate - if ctx.trace_flags.sampled - and isinstance(span.sampler, sampling.TraceIdRatioBased) - else None - ) - - -def _parse_tags_str(tags_str): - """Parse a string of tags typically provided via environment variables. - - The expected string is of the form:: - "key1:value1,key2:value2" - - :param tags_str: A string of the above form to parse tags from. - :return: A dict containing the tags that were parsed. - """ - parsed_tags = {} - if not tags_str: - return parsed_tags - - for tag in tags_str.split(","): - try: - key, value = tag.split(":", 1) - - # Validate the tag - if key == "" or value == "" or value.endswith(":"): - raise ValueError - except ValueError: - logger.error( - "Malformed tag in tag pair '%s' from tag string '%s'.", - tag, - tags_str, - ) - else: - parsed_tags[key] = value - - return parsed_tags - - -def _extract_tags_from_resource(resource): - """Parse tags from resource.attributes, except service.name which - has special significance within datadog""" - tags = {} - service_name = None - if not (resource and getattr(resource, "attributes", None)): - return [tags, service_name] - - for attribute_key, attribute_value in resource.attributes.items(): - if attribute_key == SERVICE_NAME_TAG: - service_name = attribute_value - else: - tags[attribute_key] = attribute_value - return [tags, service_name] diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py deleted file mode 100644 index ab1468c54a..0000000000 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/propagator.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import typing - -from opentelemetry import trace -from opentelemetry.context import Context -from opentelemetry.trace import get_current_span, set_span_in_context -from opentelemetry.trace.propagation.textmap import ( - Getter, - Setter, - TextMapPropagator, - TextMapPropagatorT, -) - -# pylint:disable=relative-beyond-top-level -from . import constants - - -class DatadogFormat(TextMapPropagator): - """Propagator for the Datadog HTTP header format. - """ - - TRACE_ID_KEY = "x-datadog-trace-id" - PARENT_ID_KEY = "x-datadog-parent-id" - SAMPLING_PRIORITY_KEY = "x-datadog-sampling-priority" - ORIGIN_KEY = "x-datadog-origin" - - def extract( - self, - getter: Getter[TextMapPropagatorT], - carrier: TextMapPropagatorT, - context: typing.Optional[Context] = None, - ) -> Context: - trace_id = extract_first_element( - getter.get(carrier, self.TRACE_ID_KEY) - ) - - span_id = extract_first_element( - getter.get(carrier, self.PARENT_ID_KEY) - ) - - sampled = extract_first_element( - getter.get(carrier, self.SAMPLING_PRIORITY_KEY) - ) - - origin = extract_first_element(getter.get(carrier, self.ORIGIN_KEY)) - - trace_flags = trace.TraceFlags() - if sampled and int(sampled) in ( - constants.AUTO_KEEP, - constants.USER_KEEP, - ): - trace_flags |= trace.TraceFlags.SAMPLED - - if trace_id is None or span_id is None: - return set_span_in_context(trace.INVALID_SPAN, context) - - span_context = trace.SpanContext( - trace_id=int(trace_id), - span_id=int(span_id), - is_remote=True, - trace_flags=trace_flags, - trace_state=trace.TraceState({constants.DD_ORIGIN: origin}), - ) - - return set_span_in_context(trace.DefaultSpan(span_context), context) - - def inject( - self, - set_in_carrier: Setter[TextMapPropagatorT], - carrier: TextMapPropagatorT, - context: typing.Optional[Context] = None, - ) -> None: - span = get_current_span(context) - span_context = span.get_span_context() - if span_context == trace.INVALID_SPAN_CONTEXT: - return - sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 - set_in_carrier( - carrier, self.TRACE_ID_KEY, format_trace_id(span.context.trace_id), - ) - set_in_carrier( - carrier, self.PARENT_ID_KEY, format_span_id(span.context.span_id) - ) - set_in_carrier( - carrier, - self.SAMPLING_PRIORITY_KEY, - str(constants.AUTO_KEEP if sampled else constants.AUTO_REJECT), - ) - if constants.DD_ORIGIN in span.context.trace_state: - set_in_carrier( - carrier, - self.ORIGIN_KEY, - span.context.trace_state[constants.DD_ORIGIN], - ) - - -def format_trace_id(trace_id: int) -> str: - """Format the trace id for Datadog.""" - return str(trace_id & 0xFFFFFFFFFFFFFFFF) - - -def format_span_id(span_id: int) -> str: - """Format the span id for Datadog.""" - return str(span_id) - - -def extract_first_element( - items: typing.Iterable[TextMapPropagatorT], -) -> typing.Optional[TextMapPropagatorT]: - if items is None: - return None - return next(iter(items), None) diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py deleted file mode 100644 index 3a1188e0bd..0000000000 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/spanprocessor.py +++ /dev/null @@ -1,225 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import collections -import logging -import threading -import typing - -from opentelemetry.context import Context, attach, detach, set_value -from opentelemetry.sdk.trace import Span, SpanProcessor -from opentelemetry.sdk.trace.export import SpanExporter -from opentelemetry.trace import INVALID_TRACE_ID -from opentelemetry.util import time_ns - -logger = logging.getLogger(__name__) - - -class DatadogExportSpanProcessor(SpanProcessor): - """Datadog exporter span processor - - DatadogExportSpanProcessor is an implementation of `SpanProcessor` that - batches all opened spans into a list per trace. When all spans for a trace - are ended, the trace is queues up for export. This is required for exporting - to the Datadog Agent which expects to received list of spans for each trace. - """ - - _FLUSH_TOKEN = INVALID_TRACE_ID - - def __init__( - self, - span_exporter: SpanExporter, - schedule_delay_millis: float = 5000, - max_trace_size: int = 4096, - ): - if max_trace_size <= 0: - raise ValueError("max_queue_size must be a positive integer.") - - if schedule_delay_millis <= 0: - raise ValueError("schedule_delay_millis must be positive.") - - self.span_exporter = span_exporter - - # queue trace_ids for traces with recently ended spans for worker thread to check - # for exporting - self.check_traces_queue = ( - collections.deque() - ) # type: typing.Deque[int] - - self.traces_lock = threading.Lock() - # dictionary of trace_ids to a list of spans where the first span is the - # first opened span for the trace - self.traces = collections.defaultdict(list) - # counter to keep track of the number of spans and ended spans for a - # trace_id - self.traces_spans_count = collections.Counter() - self.traces_spans_ended_count = collections.Counter() - - self.worker_thread = threading.Thread(target=self.worker, daemon=True) - - # threading conditions used for flushing and shutdown - self.condition = threading.Condition(threading.Lock()) - self.flush_condition = threading.Condition(threading.Lock()) - - # flag to indicate that there is a flush operation on progress - self._flushing = False - - self.max_trace_size = max_trace_size - self._spans_dropped = False - self.schedule_delay_millis = schedule_delay_millis - self.done = False - self.worker_thread.start() - - def on_start( - self, span: Span, parent_context: typing.Optional[Context] = None - ) -> None: - ctx = span.get_span_context() - trace_id = ctx.trace_id - - with self.traces_lock: - # check upper bound on number of spans for trace before adding new - # span - if self.traces_spans_count[trace_id] == self.max_trace_size: - logger.warning("Max spans for trace, spans will be dropped.") - self._spans_dropped = True - return - - # add span to end of list for a trace and update the counter - self.traces[trace_id].append(span) - self.traces_spans_count[trace_id] += 1 - - def on_end(self, span: Span) -> None: - if self.done: - logger.warning("Already shutdown, dropping span.") - return - - ctx = span.get_span_context() - trace_id = ctx.trace_id - - with self.traces_lock: - self.traces_spans_ended_count[trace_id] += 1 - if self.is_trace_exportable(trace_id): - self.check_traces_queue.appendleft(trace_id) - - def worker(self): - timeout = self.schedule_delay_millis / 1e3 - while not self.done: - if not self._flushing: - with self.condition: - self.condition.wait(timeout) - if not self.check_traces_queue: - # spurious notification, let's wait again, reset timeout - timeout = self.schedule_delay_millis / 1e3 - continue - if self.done: - # missing spans will be sent when calling flush - break - - # substract the duration of this export call to the next timeout - start = time_ns() - self.export() - end = time_ns() - duration = (end - start) / 1e9 - timeout = self.schedule_delay_millis / 1e3 - duration - - # be sure that all spans are sent - self._drain_queue() - - def is_trace_exportable(self, trace_id): - return ( - self.traces_spans_count[trace_id] - - self.traces_spans_ended_count[trace_id] - <= 0 - ) - - def export(self) -> None: - """Exports traces with finished spans.""" - notify_flush = False - export_trace_ids = [] - - while self.check_traces_queue: - trace_id = self.check_traces_queue.pop() - if trace_id is self._FLUSH_TOKEN: - notify_flush = True - else: - with self.traces_lock: - # check whether trace is exportable again in case that new - # spans were started since we last concluded trace was - # exportable - if self.is_trace_exportable(trace_id): - export_trace_ids.append(trace_id) - del self.traces_spans_count[trace_id] - del self.traces_spans_ended_count[trace_id] - - if len(export_trace_ids) > 0: - token = attach(set_value("suppress_instrumentation", True)) - - for trace_id in export_trace_ids: - with self.traces_lock: - try: - # Ignore type b/c the Optional[None]+slicing is too "clever" - # for mypy - self.span_exporter.export(self.traces[trace_id]) # type: ignore - # pylint: disable=broad-except - except Exception: - logger.exception( - "Exception while exporting Span batch." - ) - finally: - del self.traces[trace_id] - - detach(token) - - if notify_flush: - with self.flush_condition: - self.flush_condition.notify() - - def _drain_queue(self): - """"Export all elements until queue is empty. - - Can only be called from the worker thread context because it invokes - `export` that is not thread safe. - """ - while self.check_traces_queue: - self.export() - - def force_flush(self, timeout_millis: int = 30000) -> bool: - if self.done: - logger.warning("Already shutdown, ignoring call to force_flush().") - return True - - self._flushing = True - self.check_traces_queue.appendleft(self._FLUSH_TOKEN) - - # wake up worker thread - with self.condition: - self.condition.notify_all() - - # wait for token to be processed - with self.flush_condition: - ret = self.flush_condition.wait(timeout_millis / 1e3) - - self._flushing = False - - if not ret: - logger.warning("Timeout was exceeded in force_flush().") - return ret - - def shutdown(self) -> None: - # signal the worker thread to finish and then wait for it - self.done = True - with self.condition: - self.condition.notify_all() - self.worker_thread.join() - self.span_exporter.shutdown() diff --git a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/exporter/opentelemetry-exporter-datadog/tests/__init__.py b/exporter/opentelemetry-exporter-datadog/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py deleted file mode 100644 index 7b94704a57..0000000000 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_exporter.py +++ /dev/null @@ -1,604 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import itertools -import logging -import time -import unittest -from unittest import mock - -from ddtrace.internal.writer import AgentWriter - -from opentelemetry import trace as trace_api -from opentelemetry.context import Context -from opentelemetry.exporter import datadog -from opentelemetry.sdk import trace -from opentelemetry.sdk.trace import Resource, sampling -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo - - -class MockDatadogSpanExporter(datadog.DatadogSpanExporter): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - agent_writer_mock = mock.Mock(spec=AgentWriter) - agent_writer_mock.started = True - agent_writer_mock.exit_timeout = 1 - self._agent_writer = agent_writer_mock - - -def get_spans(tracer, exporter, shutdown=True): - if shutdown: - tracer.span_processor.shutdown() - - spans = [ - call_args[-1]["spans"] - for call_args in exporter.agent_writer.write.call_args_list - ] - - return [span.to_dict() for span in itertools.chain.from_iterable(spans)] - - -class TestDatadogSpanExporter(unittest.TestCase): - def setUp(self): - self.exporter = MockDatadogSpanExporter() - self.span_processor = datadog.DatadogExportSpanProcessor(self.exporter) - tracer_provider = trace.TracerProvider() - tracer_provider.add_span_processor(self.span_processor) - self.tracer_provider = tracer_provider - self.tracer = tracer_provider.get_tracer(__name__) - - def tearDown(self): - self.tracer_provider.shutdown() - - def test_constructor_default(self): - """Test the default values assigned by constructor.""" - exporter = datadog.DatadogSpanExporter() - - self.assertEqual(exporter.agent_url, "http://localhost:8126") - self.assertIsNone(exporter.service) - self.assertIsNotNone(exporter.agent_writer) - - def test_constructor_explicit(self): - """Test the constructor passing all the options.""" - agent_url = "http://localhost:8126" - exporter = datadog.DatadogSpanExporter( - agent_url=agent_url, service="explicit", - ) - - self.assertEqual(exporter.agent_url, agent_url) - self.assertEqual(exporter.service, "explicit") - self.assertIsNone(exporter.env) - self.assertIsNone(exporter.version) - self.assertEqual(exporter.tags, {}) - - exporter = datadog.DatadogSpanExporter( - agent_url=agent_url, - service="explicit", - env="test", - version="0.0.1", - tags="", - ) - - self.assertEqual(exporter.agent_url, agent_url) - self.assertEqual(exporter.service, "explicit") - self.assertEqual(exporter.env, "test") - self.assertEqual(exporter.version, "0.0.1") - self.assertEqual(exporter.tags, {}) - - exporter = datadog.DatadogSpanExporter( - agent_url=agent_url, - service="explicit", - env="test", - version="0.0.1", - tags="team:testers,layer:app", - ) - - self.assertEqual(exporter.agent_url, agent_url) - self.assertEqual(exporter.service, "explicit") - self.assertEqual(exporter.env, "test") - self.assertEqual(exporter.version, "0.0.1") - self.assertEqual(exporter.tags, {"team": "testers", "layer": "app"}) - - @mock.patch.dict( - "os.environ", - { - "DD_TRACE_AGENT_URL": "http://agent:8126", - "DD_SERVICE": "test-service", - "DD_ENV": "test", - "DD_VERSION": "0.0.1", - "DD_TAGS": "team:testers", - }, - ) - def test_constructor_environ(self): - exporter = datadog.DatadogSpanExporter() - - self.assertEqual(exporter.agent_url, "http://agent:8126") - self.assertEqual(exporter.service, "test-service") - self.assertEqual(exporter.env, "test") - self.assertEqual(exporter.version, "0.0.1") - self.assertEqual(exporter.tags, {"team": "testers"}) - self.assertIsNotNone(exporter.agent_writer) - - # pylint: disable=too-many-locals - @mock.patch.dict( - "os.environ", - { - "DD_SERVICE": "test-service", - "DD_ENV": "test", - "DD_VERSION": "0.0.1", - "DD_TAGS": "team:testers", - }, - ) - def test_translate_to_datadog(self): - # pylint: disable=invalid-name - self.maxDiff = None - - resource = Resource( - attributes={ - "key_resource": "some_resource", - "service.name": "resource_service_name", - } - ) - - resource_without_service = Resource( - attributes={"conflicting_key": "conflicting_value"} - ) - - span_names = ("test1", "test2", "test3") - trace_id = 0x6E0C63257DE34C926F9EFCD03927272E - trace_id_low = 0x6F9EFCD03927272E - span_id = 0x34BF92DEEFC58C92 - parent_id = 0x1111111111111111 - other_id = 0x2222222222222222 - - base_time = 683647322 * 10 ** 9 # in ns - start_times = ( - base_time, - base_time + 150 * 10 ** 6, - base_time + 300 * 10 ** 6, - ) - durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6) - end_times = ( - start_times[0] + durations[0], - start_times[1] + durations[1], - start_times[2] + durations[2], - ) - - span_context = trace_api.SpanContext( - trace_id, span_id, is_remote=False - ) - parent_span_context = trace_api.SpanContext( - trace_id, parent_id, is_remote=False - ) - other_context = trace_api.SpanContext( - trace_id, other_id, is_remote=False - ) - - instrumentation_info = InstrumentationInfo(__name__, "0") - - otel_spans = [ - trace._Span( - name=span_names[0], - context=span_context, - parent=parent_span_context, - kind=trace_api.SpanKind.CLIENT, - instrumentation_info=instrumentation_info, - resource=Resource({}), - ), - trace._Span( - name=span_names[1], - context=parent_span_context, - parent=None, - instrumentation_info=instrumentation_info, - resource=resource_without_service, - ), - trace._Span( - name=span_names[2], - context=other_context, - parent=None, - resource=resource, - ), - ] - - otel_spans[1].set_attribute("conflicting_key", "original_value") - - otel_spans[0].start(start_time=start_times[0]) - otel_spans[0].end(end_time=end_times[0]) - - otel_spans[1].start(start_time=start_times[1]) - otel_spans[1].end(end_time=end_times[1]) - - otel_spans[2].start(start_time=start_times[2]) - otel_spans[2].end(end_time=end_times[2]) - - # pylint: disable=protected-access - exporter = datadog.DatadogSpanExporter() - datadog_spans = [ - span.to_dict() - for span in exporter._translate_to_datadog(otel_spans) - ] - - expected_spans = [ - dict( - trace_id=trace_id_low, - parent_id=parent_id, - span_id=span_id, - name="tests.test_datadog_exporter.CLIENT", - resource=span_names[0], - start=start_times[0], - duration=durations[0], - error=0, - service="test-service", - meta={"env": "test", "team": "testers"}, - ), - dict( - trace_id=trace_id_low, - parent_id=0, - span_id=parent_id, - name="tests.test_datadog_exporter.INTERNAL", - resource=span_names[1], - start=start_times[1], - duration=durations[1], - error=0, - service="test-service", - meta={ - "env": "test", - "team": "testers", - "version": "0.0.1", - "conflicting_key": "original_value", - }, - ), - dict( - trace_id=trace_id_low, - parent_id=0, - span_id=other_id, - name=span_names[2], - resource=span_names[2], - start=start_times[2], - duration=durations[2], - error=0, - service="resource_service_name", - meta={ - "env": "test", - "team": "testers", - "version": "0.0.1", - "key_resource": "some_resource", - }, - ), - ] - - self.assertEqual(datadog_spans, expected_spans) - - def test_export(self): - """Test that agent and/or collector are invoked""" - # create and save span to be used in tests - context = trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x00000000DEADBEF0, - is_remote=False, - ) - - test_span = trace._Span("test_span", context=context) - test_span.start() - test_span.end() - - self.exporter.export((test_span,)) - - self.assertEqual(self.exporter.agent_writer.write.call_count, 1) - - def test_resources(self): - test_attributes = [ - {}, - {"http.method": "GET", "http.route": "/foo/"}, - {"http.method": "GET", "http.target": "/foo/200"}, - ] - - for index, test in enumerate(test_attributes): - with self.tracer.start_span(str(index), attributes=test): - pass - - datadog_spans = get_spans(self.tracer, self.exporter) - - self.assertEqual(len(datadog_spans), 3) - - actual = [span["resource"] for span in datadog_spans] - expected = ["0", "GET /foo/", "GET"] - - self.assertEqual(actual, expected) - - def test_span_types(self): - test_instrumentations = [ - "opentelemetry.instrumentation.aiohttp-client", - "opentelemetry.instrumentation.dbapi", - "opentelemetry.instrumentation.django", - "opentelemetry.instrumentation.flask", - "opentelemetry.instrumentation.grpc", - "opentelemetry.instrumentation.jinja2", - "opentelemetry.instrumentation.mysql", - "opentelemetry.instrumentation.psycopg2", - "opentelemetry.instrumentation.pymongo", - "opentelemetry.instrumentation.pymysql", - "opentelemetry.instrumentation.redis", - "opentelemetry.instrumentation.requests", - "opentelemetry.instrumentation.sqlalchemy", - "opentelemetry.instrumentation.wsgi", - ] - - for index, instrumentation in enumerate(test_instrumentations): - # change tracer's instrumentation info before starting span - self.tracer.instrumentation_info = InstrumentationInfo( - instrumentation, "0" - ) - with self.tracer.start_span(str(index)): - pass - - datadog_spans = get_spans(self.tracer, self.exporter) - - self.assertEqual(len(datadog_spans), 14) - - actual = [span.get("type") for span in datadog_spans] - expected = [ - "http", - "sql", - "web", - "web", - "grpc", - "template", - "sql", - "sql", - "mongodb", - "sql", - "redis", - "http", - "sql", - "web", - ] - self.assertEqual(actual, expected) - - def test_errors(self): - with self.assertRaises(ValueError): - with self.tracer.start_span("foo"): - raise ValueError("bar") - - datadog_spans = get_spans(self.tracer, self.exporter) - - self.assertEqual(len(datadog_spans), 1) - - span = datadog_spans[0] - self.assertEqual(span["error"], 1) - self.assertEqual(span["meta"]["error.msg"], "bar") - self.assertEqual(span["meta"]["error.type"], "ValueError") - - def test_shutdown(self): - span_names = ["xxx", "bar", "foo"] - - for name in span_names: - with self.tracer.start_span(name): - pass - - self.span_processor.shutdown() - - # check that spans are exported without an explicitly call to - # force_flush() - datadog_spans = get_spans(self.tracer, self.exporter) - actual = [span.get("resource") for span in datadog_spans] - self.assertListEqual(span_names, actual) - - def test_flush(self): - span_names0 = ["xxx", "bar", "foo"] - span_names1 = ["yyy", "baz", "fox"] - - for name in span_names0: - with self.tracer.start_span(name): - pass - - self.assertTrue(self.span_processor.force_flush()) - datadog_spans = get_spans(self.tracer, self.exporter, shutdown=False) - actual0 = [span.get("resource") for span in datadog_spans] - self.assertListEqual(span_names0, actual0) - - # create some more spans to check that span processor still works - for name in span_names1: - with self.tracer.start_span(name): - pass - - self.assertTrue(self.span_processor.force_flush()) - datadog_spans = get_spans(self.tracer, self.exporter) - actual1 = [span.get("resource") for span in datadog_spans] - self.assertListEqual(span_names0 + span_names1, actual1) - - def test_span_processor_lossless(self): - """Test that no spans are lost when sending max_trace_size spans""" - span_processor = datadog.DatadogExportSpanProcessor( - self.exporter, max_trace_size=128 - ) - tracer_provider = trace.TracerProvider() - tracer_provider.add_span_processor(span_processor) - tracer = tracer_provider.get_tracer(__name__) - - with tracer.start_as_current_span("root"): - for _ in range(127): - with tracer.start_span("foo"): - pass - - self.assertTrue(span_processor.force_flush()) - datadog_spans = get_spans(tracer, self.exporter) - self.assertEqual(len(datadog_spans), 128) - tracer_provider.shutdown() - - def test_span_processor_dropped_spans(self): - """Test that spans are lost when exceeding max_trace_size spans""" - span_processor = datadog.DatadogExportSpanProcessor( - self.exporter, max_trace_size=128 - ) - tracer_provider = trace.TracerProvider() - tracer_provider.add_span_processor(span_processor) - tracer = tracer_provider.get_tracer(__name__) - - with tracer.start_as_current_span("root"): - for _ in range(127): - with tracer.start_span("foo"): - pass - with self.assertLogs(level=logging.WARNING): - with tracer.start_span("one-too-many"): - pass - - self.assertTrue(span_processor.force_flush()) - datadog_spans = get_spans(tracer, self.exporter) - self.assertEqual(len(datadog_spans), 128) - tracer_provider.shutdown() - - def test_span_processor_scheduled_delay(self): - """Test that spans are exported each schedule_delay_millis""" - delay = 300 - span_processor = datadog.DatadogExportSpanProcessor( - self.exporter, schedule_delay_millis=delay - ) - tracer_provider = trace.TracerProvider() - tracer_provider.add_span_processor(span_processor) - tracer = tracer_provider.get_tracer(__name__) - - with tracer.start_span("foo"): - pass - - time.sleep(delay / (1e3 * 2)) - datadog_spans = get_spans(tracer, self.exporter, shutdown=False) - self.assertEqual(len(datadog_spans), 0) - - time.sleep(delay / (1e3 * 2) + 0.01) - datadog_spans = get_spans(tracer, self.exporter, shutdown=False) - self.assertEqual(len(datadog_spans), 1) - - tracer_provider.shutdown() - - def test_batch_span_processor_reset_timeout(self): - """Test that the scheduled timeout is reset on cycles without spans""" - delay = 50 - # pylint: disable=protected-access - exporter = MockDatadogSpanExporter() - exporter._agent_writer.write.side_effect = lambda spans: time.sleep( - 0.05 - ) - span_processor = datadog.DatadogExportSpanProcessor( - exporter, schedule_delay_millis=delay - ) - tracer_provider = trace.TracerProvider() - tracer_provider.add_span_processor(span_processor) - tracer = tracer_provider.get_tracer(__name__) - with mock.patch.object(span_processor.condition, "wait") as mock_wait: - with tracer.start_span("foo"): - pass - - # give some time for exporter to loop - # since wait is mocked it should return immediately - time.sleep(0.1) - mock_wait_calls = list(mock_wait.mock_calls) - - # find the index of the call that processed the singular span - for idx, wait_call in enumerate(mock_wait_calls): - _, args, __ = wait_call - if args[0] <= 0: - after_calls = mock_wait_calls[idx + 1 :] - break - - self.assertTrue( - all(args[0] >= 0.05 for _, args, __ in after_calls) - ) - - span_processor.shutdown() - - def test_span_processor_accepts_parent_context(self): - span_processor = mock.Mock( - wraps=datadog.DatadogExportSpanProcessor(self.exporter) - ) - tracer_provider = trace.TracerProvider() - tracer_provider.add_span_processor(span_processor) - tracer = tracer_provider.get_tracer(__name__) - - context = Context() - span = tracer.start_span("foo", context=context) - - span_processor.on_start.assert_called_once_with( - span, parent_context=context - ) - - def test_origin(self): - context = trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=trace_api.INVALID_SPAN, - is_remote=True, - trace_state=trace_api.TraceState( - {datadog.constants.DD_ORIGIN: "origin-service"} - ), - ) - - root_span = trace._Span(name="root", context=context, parent=None) - child_span = trace._Span( - name="child", context=context, parent=root_span - ) - root_span.start() - child_span.start() - child_span.end() - root_span.end() - - # pylint: disable=protected-access - exporter = datadog.DatadogSpanExporter() - datadog_spans = [ - span.to_dict() - for span in exporter._translate_to_datadog([root_span, child_span]) - ] - - self.assertEqual(len(datadog_spans), 2) - - actual = [ - span["meta"].get(datadog.constants.DD_ORIGIN) - if "meta" in span - else None - for span in datadog_spans - ] - expected = ["origin-service", None] - self.assertListEqual(actual, expected) - - def test_sampling_rate(self): - context = trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x34BF92DEEFC58C92, - is_remote=False, - trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), - ) - sampler = sampling.TraceIdRatioBased(0.5) - - span = trace._Span( - name="sampled", context=context, parent=None, sampler=sampler - ) - span.start() - span.end() - - # pylint: disable=protected-access - exporter = datadog.DatadogSpanExporter() - datadog_spans = [ - span.to_dict() for span in exporter._translate_to_datadog([span]) - ] - - self.assertEqual(len(datadog_spans), 1) - - actual = [ - span["metrics"].get(datadog.constants.SAMPLE_RATE_METRIC_KEY) - if "metrics" in span - else None - for span in datadog_spans - ] - expected = [0.5] - self.assertListEqual(actual, expected) diff --git a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py b/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py deleted file mode 100644 index bb41fef49b..0000000000 --- a/exporter/opentelemetry-exporter-datadog/tests/test_datadog_format.py +++ /dev/null @@ -1,171 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -from opentelemetry import trace as trace_api -from opentelemetry.exporter.datadog import constants, propagator -from opentelemetry.sdk import trace -from opentelemetry.trace import get_current_span, set_span_in_context -from opentelemetry.trace.propagation.textmap import DictGetter - -FORMAT = propagator.DatadogFormat() - -carrier_getter = DictGetter() - - -class TestDatadogFormat(unittest.TestCase): - @classmethod - def setUpClass(cls): - ids_generator = trace_api.RandomIdsGenerator() - cls.serialized_trace_id = propagator.format_trace_id( - ids_generator.generate_trace_id() - ) - cls.serialized_parent_id = propagator.format_span_id( - ids_generator.generate_span_id() - ) - cls.serialized_origin = "origin-service" - - def test_malformed_headers(self): - """Test with no Datadog headers""" - malformed_trace_id_key = FORMAT.TRACE_ID_KEY + "-x" - malformed_parent_id_key = FORMAT.PARENT_ID_KEY + "-x" - context = get_current_span( - FORMAT.extract( - carrier_getter, - { - malformed_trace_id_key: self.serialized_trace_id, - malformed_parent_id_key: self.serialized_parent_id, - }, - ) - ).get_span_context() - - self.assertNotEqual(context.trace_id, int(self.serialized_trace_id)) - self.assertNotEqual(context.span_id, int(self.serialized_parent_id)) - self.assertFalse(context.is_remote) - - def test_missing_trace_id(self): - """If a trace id is missing, populate an invalid trace id.""" - carrier = { - FORMAT.PARENT_ID_KEY: self.serialized_parent_id, - } - - ctx = FORMAT.extract(carrier_getter, carrier) - span_context = get_current_span(ctx).get_span_context() - self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) - - def test_missing_parent_id(self): - """If a parent id is missing, populate an invalid trace id.""" - carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - } - - ctx = FORMAT.extract(carrier_getter, carrier) - span_context = get_current_span(ctx).get_span_context() - self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) - - def test_context_propagation(self): - """Test the propagation of Datadog headers.""" - parent_span_context = get_current_span( - FORMAT.extract( - carrier_getter, - { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.PARENT_ID_KEY: self.serialized_parent_id, - FORMAT.SAMPLING_PRIORITY_KEY: str(constants.AUTO_KEEP), - FORMAT.ORIGIN_KEY: self.serialized_origin, - }, - ) - ).get_span_context() - - self.assertEqual( - parent_span_context.trace_id, int(self.serialized_trace_id) - ) - self.assertEqual( - parent_span_context.span_id, int(self.serialized_parent_id) - ) - self.assertEqual(parent_span_context.trace_flags, constants.AUTO_KEEP) - self.assertEqual( - parent_span_context.trace_state.get(constants.DD_ORIGIN), - self.serialized_origin, - ) - self.assertTrue(parent_span_context.is_remote) - - child = trace._Span( - "child", - trace_api.SpanContext( - parent_span_context.trace_id, - trace_api.RandomIdsGenerator().generate_span_id(), - is_remote=False, - trace_flags=parent_span_context.trace_flags, - trace_state=parent_span_context.trace_state, - ), - parent=parent_span_context, - ) - - child_carrier = {} - child_context = set_span_in_context(child) - FORMAT.inject(dict.__setitem__, child_carrier, context=child_context) - - self.assertEqual( - child_carrier[FORMAT.TRACE_ID_KEY], self.serialized_trace_id - ) - self.assertEqual( - child_carrier[FORMAT.PARENT_ID_KEY], str(child.context.span_id) - ) - self.assertEqual( - child_carrier[FORMAT.SAMPLING_PRIORITY_KEY], - str(constants.AUTO_KEEP), - ) - self.assertEqual( - child_carrier.get(FORMAT.ORIGIN_KEY), self.serialized_origin - ) - - def test_sampling_priority_auto_reject(self): - """Test sampling priority rejected.""" - parent_span_context = get_current_span( - FORMAT.extract( - carrier_getter, - { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.PARENT_ID_KEY: self.serialized_parent_id, - FORMAT.SAMPLING_PRIORITY_KEY: str(constants.AUTO_REJECT), - }, - ) - ).get_span_context() - - self.assertEqual( - parent_span_context.trace_flags, constants.AUTO_REJECT - ) - - child = trace._Span( - "child", - trace_api.SpanContext( - parent_span_context.trace_id, - trace_api.RandomIdsGenerator().generate_span_id(), - is_remote=False, - trace_flags=parent_span_context.trace_flags, - trace_state=parent_span_context.trace_state, - ), - parent=parent_span_context, - ) - - child_carrier = {} - child_context = set_span_in_context(child) - FORMAT.inject(dict.__setitem__, child_carrier, context=child_context) - - self.assertEqual( - child_carrier[FORMAT.SAMPLING_PRIORITY_KEY], - str(constants.AUTO_REJECT), - ) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md deleted file mode 100644 index 8b1d3ee2c1..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md +++ /dev/null @@ -1,25 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Updating span name to match semantic conventions - ([#972](https://github.com/open-telemetry/opentelemetry-python/pull/972)) -- Add instrumentor and auto instrumentation support for aiohttp - ([#1075](https://github.com/open-telemetry/opentelemetry-python/pull/1075)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-aiohttp-client - ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) - -## 0.7b1 - -Released 2020-05-12 - -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE b/instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-aiohttp-client/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/README.rst b/instrumentation/opentelemetry-instrumentation-aiohttp-client/README.rst deleted file mode 100644 index bc44e0e262..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/README.rst +++ /dev/null @@ -1,24 +0,0 @@ -OpenTelemetry aiohttp client Integration -======================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aiohttp-client.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-aiohttp-client/ - -This library allows tracing HTTP requests made by the -`aiohttp client `_ library. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-aiohttp-client - - -References ----------- - -* `OpenTelemetry Project `_ -* `aiohttp client Tracing `_ diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg deleted file mode 100644 index e0fcfbf4b0..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2020, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-aiohttp-client -description = OpenTelemetry aiohttp client instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/instrumentation/opentelemetry-instrumentation-aiohttp-client -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5.3 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - aiohttp ~= 3.0 - wrapt >= 1.0.0, < 2.0.0 - -[options.packages.find] -where = src - -[options.extras_require] -test = - -[options.entry_points] -opentelemetry_instrumentor = - aiohttp-client = opentelemetry.instrumentation.aiohttp_client:AioHttpClientInstrumentor \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.py deleted file mode 100644 index fe74e23235..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2020, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "aiohttp_client", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py deleted file mode 100644 index c708802a92..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py +++ /dev/null @@ -1,315 +0,0 @@ -# Copyright 2020, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The opentelemetry-instrumentation-aiohttp-client package allows tracing HTTP -requests made by the aiohttp client library. - -Usage ------ -Explicitly instrumenting a single client session: - -.. code:: python - - import aiohttp - from opentelemetry.instrumentation.aiohttp_client import ( - create_trace_config, - url_path_span_name - ) - import yarl - - def strip_query_params(url: yarl.URL) -> str: - return str(url.with_query(None)) - - async with aiohttp.ClientSession(trace_configs=[create_trace_config( - # Remove all query params from the URL attribute on the span. - url_filter=strip_query_params, - # Use the URL's path as the span name. - span_name=url_path_span_name - )]) as session: - async with session.get(url) as response: - await response.text() - -Instrumenting all client sessions: - -.. code:: python - - import aiohttp - from opentelemetry.instrumentation.aiohttp_client import ( - AioHttpClientInstrumentor - ) - - # Enable instrumentation - AioHttpClientInstrumentor().instrument() - - # Create a session and make an HTTP get request - async with aiohttp.ClientSession() as session: - async with session.get(url) as response: - await response.text() - -API ---- -""" - -import socket -import types -import typing - -import aiohttp -import wrapt - -from opentelemetry import context as context_api -from opentelemetry import propagators, trace -from opentelemetry.instrumentation.aiohttp_client.version import __version__ -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.utils import ( - http_status_to_status_code, - unwrap, -) -from opentelemetry.trace import SpanKind, TracerProvider, get_tracer -from opentelemetry.trace.status import Status, StatusCode - -_UrlFilterT = typing.Optional[typing.Callable[[str], str]] -_SpanNameT = typing.Optional[ - typing.Union[typing.Callable[[aiohttp.TraceRequestStartParams], str], str] -] - - -def url_path_span_name(params: aiohttp.TraceRequestStartParams) -> str: - """Extract a span name from the request URL path. - - A simple callable to extract the path portion of the requested URL - for use as the span name. - - :param aiohttp.TraceRequestStartParams params: Parameters describing - the traced request. - - :return: The URL path. - :rtype: str - """ - return params.url.path - - -def create_trace_config( - url_filter: _UrlFilterT = None, - span_name: _SpanNameT = None, - tracer_provider: TracerProvider = None, -) -> aiohttp.TraceConfig: - """Create an aiohttp-compatible trace configuration. - - One span is created for the entire HTTP request, including initial - TCP/TLS setup if the connection doesn't exist. - - By default the span name is set to the HTTP request method. - - Example usage: - - .. code:: python - - import aiohttp - from opentelemetry.instrumentation.aiohttp_client import create_trace_config - - async with aiohttp.ClientSession(trace_configs=[create_trace_config()]) as session: - async with session.get(url) as response: - await response.text() - - - :param url_filter: A callback to process the requested URL prior to adding - it as a span attribute. This can be useful to remove sensitive data - such as API keys or user personal information. - - :param str span_name: Override the default span name. - :param tracer_provider: optional TracerProvider from which to get a Tracer - - :return: An object suitable for use with :py:class:`aiohttp.ClientSession`. - :rtype: :py:class:`aiohttp.TraceConfig` - """ - # `aiohttp.TraceRequestStartParams` resolves to `aiohttp.tracing.TraceRequestStartParams` - # which doesn't exist in the aiottp intersphinx inventory. - # Explicitly specify the type for the `span_name` param and rtype to work - # around this issue. - - tracer = get_tracer(__name__, __version__, tracer_provider) - - def _end_trace(trace_config_ctx: types.SimpleNamespace): - context_api.detach(trace_config_ctx.token) - trace_config_ctx.span.end() - - async def on_request_start( - unused_session: aiohttp.ClientSession, - trace_config_ctx: types.SimpleNamespace, - params: aiohttp.TraceRequestStartParams, - ): - if context_api.get_value("suppress_instrumentation"): - trace_config_ctx.span = None - return - - http_method = params.method.upper() - if trace_config_ctx.span_name is None: - request_span_name = "HTTP {}".format(http_method) - elif callable(trace_config_ctx.span_name): - request_span_name = str(trace_config_ctx.span_name(params)) - else: - request_span_name = str(trace_config_ctx.span_name) - - trace_config_ctx.span = trace_config_ctx.tracer.start_span( - request_span_name, kind=SpanKind.CLIENT, - ) - - if trace_config_ctx.span.is_recording(): - attributes = { - "component": "http", - "http.method": http_method, - "http.url": trace_config_ctx.url_filter(params.url) - if callable(trace_config_ctx.url_filter) - else str(params.url), - } - for key, value in attributes.items(): - trace_config_ctx.span.set_attribute(key, value) - - trace_config_ctx.token = context_api.attach( - trace.set_span_in_context(trace_config_ctx.span) - ) - - propagators.inject(type(params.headers).__setitem__, params.headers) - - async def on_request_end( - unused_session: aiohttp.ClientSession, - trace_config_ctx: types.SimpleNamespace, - params: aiohttp.TraceRequestEndParams, - ): - if trace_config_ctx.span is None: - return - - if trace_config_ctx.span.is_recording(): - trace_config_ctx.span.set_status( - Status(http_status_to_status_code(int(params.response.status))) - ) - trace_config_ctx.span.set_attribute( - "http.status_code", params.response.status - ) - trace_config_ctx.span.set_attribute( - "http.status_text", params.response.reason - ) - _end_trace(trace_config_ctx) - - async def on_request_exception( - unused_session: aiohttp.ClientSession, - trace_config_ctx: types.SimpleNamespace, - params: aiohttp.TraceRequestExceptionParams, - ): - if trace_config_ctx.span is None: - return - - if trace_config_ctx.span.is_recording() and params.exception: - trace_config_ctx.span.set_status(Status(StatusCode.ERROR)) - trace_config_ctx.span.record_exception(params.exception) - _end_trace(trace_config_ctx) - - def _trace_config_ctx_factory(**kwargs): - kwargs.setdefault("trace_request_ctx", {}) - return types.SimpleNamespace( - span_name=span_name, tracer=tracer, url_filter=url_filter, **kwargs - ) - - trace_config = aiohttp.TraceConfig( - trace_config_ctx_factory=_trace_config_ctx_factory - ) - - trace_config.on_request_start.append(on_request_start) - trace_config.on_request_end.append(on_request_end) - trace_config.on_request_exception.append(on_request_exception) - - return trace_config - - -def _instrument( - tracer_provider: TracerProvider = None, - url_filter: _UrlFilterT = None, - span_name: _SpanNameT = None, -): - """Enables tracing of all ClientSessions - - When a ClientSession gets created a TraceConfig is automatically added to - the session's trace_configs. - """ - # pylint:disable=unused-argument - def instrumented_init(wrapped, instance, args, kwargs): - if context_api.get_value("suppress_instrumentation"): - return wrapped(*args, **kwargs) - - trace_configs = list(kwargs.get("trace_configs") or ()) - - trace_config = create_trace_config( - url_filter=url_filter, - span_name=span_name, - tracer_provider=tracer_provider, - ) - trace_config.opentelemetry_aiohttp_instrumented = True - trace_configs.append(trace_config) - - kwargs["trace_configs"] = trace_configs - return wrapped(*args, **kwargs) - - wrapt.wrap_function_wrapper( - aiohttp.ClientSession, "__init__", instrumented_init - ) - - -def _uninstrument(): - """Disables instrumenting for all newly created ClientSessions""" - unwrap(aiohttp.ClientSession, "__init__") - - -def _uninstrument_session(client_session: aiohttp.ClientSession): - """Disables instrumentation for the given ClientSession""" - # pylint: disable=protected-access - trace_configs = client_session._trace_configs - client_session._trace_configs = [ - trace_config - for trace_config in trace_configs - if not hasattr(trace_config, "opentelemetry_aiohttp_instrumented") - ] - - -class AioHttpClientInstrumentor(BaseInstrumentor): - """An instrumentor for aiohttp client sessions - - See `BaseInstrumentor` - """ - - def _instrument(self, **kwargs): - """Instruments aiohttp ClientSession - - Args: - **kwargs: Optional arguments - ``tracer_provider``: a TracerProvider, defaults to global - ``url_filter``: A callback to process the requested URL prior to adding - it as a span attribute. This can be useful to remove sensitive data - such as API keys or user personal information. - ``span_name``: Override the default span name. - """ - _instrument( - tracer_provider=kwargs.get("tracer_provider"), - url_filter=kwargs.get("url_filter"), - span_name=kwargs.get("span_name"), - ) - - def _uninstrument(self, **kwargs): - _uninstrument() - - @staticmethod - def uninstrument_session(client_session: aiohttp.ClientSession): - """Disables instrumentation for the given session""" - _uninstrument_session(client_session) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py deleted file mode 100644 index bb32120c79..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2020, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py deleted file mode 100644 index f073465348..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py +++ /dev/null @@ -1,500 +0,0 @@ -# Copyright 2020, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import contextlib -import typing -import unittest -import urllib.parse -from http import HTTPStatus -from unittest import mock - -import aiohttp -import aiohttp.test_utils -import yarl -from pkg_resources import iter_entry_points - -from opentelemetry import context -from opentelemetry.instrumentation import aiohttp_client -from opentelemetry.instrumentation.aiohttp_client import ( - AioHttpClientInstrumentor, -) -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace.status import StatusCode - - -def run_with_test_server( - runnable: typing.Callable, url: str, handler: typing.Callable -) -> typing.Tuple[str, int]: - async def do_request(): - app = aiohttp.web.Application() - parsed_url = urllib.parse.urlparse(url) - app.add_routes([aiohttp.web.get(parsed_url.path, handler)]) - app.add_routes([aiohttp.web.post(parsed_url.path, handler)]) - app.add_routes([aiohttp.web.patch(parsed_url.path, handler)]) - - with contextlib.suppress(aiohttp.ClientError): - async with aiohttp.test_utils.TestServer(app) as server: - netloc = (server.host, server.port) - await server.start_server() - await runnable(server) - return netloc - - loop = asyncio.get_event_loop() - return loop.run_until_complete(do_request()) - - -class TestAioHttpIntegration(TestBase): - def assert_spans(self, spans): - self.assertEqual( - [ - ( - span.name, - (span.status.status_code, span.status.description), - dict(span.attributes), - ) - for span in self.memory_exporter.get_finished_spans() - ], - spans, - ) - - def test_url_path_span_name(self): - for url, expected in ( - ( - yarl.URL("http://hostname.local:1234/some/path?query=params"), - "/some/path", - ), - (yarl.URL("http://hostname.local:1234"), "/"), - ): - with self.subTest(url=url): - params = aiohttp.TraceRequestStartParams("METHOD", url, {}) - actual = aiohttp_client.url_path_span_name(params) - self.assertEqual(actual, expected) - self.assertIsInstance(actual, str) - - @staticmethod - def _http_request( - trace_config, - url: str, - method: str = "GET", - status_code: int = HTTPStatus.OK, - request_handler: typing.Callable = None, - **kwargs - ) -> typing.Tuple[str, int]: - """Helper to start an aiohttp test server and send an actual HTTP request to it.""" - - async def default_handler(request): - assert "traceparent" in request.headers - return aiohttp.web.Response(status=int(status_code)) - - async def client_request(server: aiohttp.test_utils.TestServer): - async with aiohttp.test_utils.TestClient( - server, trace_configs=[trace_config] - ) as client: - await client.request( - method, url, trace_request_ctx={}, **kwargs - ) - - handler = request_handler or default_handler - return run_with_test_server(client_request, url, handler) - - def test_status_codes(self): - for status_code, span_status in ( - (HTTPStatus.OK, StatusCode.UNSET), - (HTTPStatus.TEMPORARY_REDIRECT, StatusCode.UNSET), - (HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR), - (HTTPStatus.GATEWAY_TIMEOUT, StatusCode.ERROR,), - ): - with self.subTest(status_code=status_code): - host, port = self._http_request( - trace_config=aiohttp_client.create_trace_config(), - url="/test-path?query=param#foobar", - status_code=status_code, - ) - - self.assert_spans( - [ - ( - "HTTP GET", - (span_status, None), - { - "component": "http", - "http.method": "GET", - "http.url": "http://{}:{}/test-path?query=param#foobar".format( - host, port - ), - "http.status_code": int(status_code), - "http.status_text": status_code.phrase, - }, - ) - ] - ) - - self.memory_exporter.clear() - - def test_not_recording(self): - mock_tracer = mock.Mock() - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - with mock.patch("opentelemetry.trace.get_tracer"): - # pylint: disable=W0612 - host, port = self._http_request( - trace_config=aiohttp_client.create_trace_config(), - url="/test-path?query=param#foobar", - ) - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_span_name_option(self): - for span_name, method, path, expected in ( - ("static", "POST", "/static-span-name", "static"), - ( - lambda params: "{} - {}".format( - params.method, params.url.path - ), - "PATCH", - "/some/path", - "PATCH - /some/path", - ), - ): - with self.subTest(span_name=span_name, method=method, path=path): - host, port = self._http_request( - trace_config=aiohttp_client.create_trace_config( - span_name=span_name - ), - method=method, - url=path, - status_code=HTTPStatus.OK, - ) - - self.assert_spans( - [ - ( - expected, - (StatusCode.UNSET, None), - { - "component": "http", - "http.method": method, - "http.url": "http://{}:{}{}".format( - host, port, path - ), - "http.status_code": int(HTTPStatus.OK), - "http.status_text": HTTPStatus.OK.phrase, - }, - ) - ] - ) - self.memory_exporter.clear() - - def test_url_filter_option(self): - # Strips all query params from URL before adding as a span attribute. - def strip_query_params(url: yarl.URL) -> str: - return str(url.with_query(None)) - - host, port = self._http_request( - trace_config=aiohttp_client.create_trace_config( - url_filter=strip_query_params - ), - url="/some/path?query=param&other=param2", - status_code=HTTPStatus.OK, - ) - - self.assert_spans( - [ - ( - "HTTP GET", - (StatusCode.UNSET, None), - { - "component": "http", - "http.method": "GET", - "http.url": "http://{}:{}/some/path".format( - host, port - ), - "http.status_code": int(HTTPStatus.OK), - "http.status_text": HTTPStatus.OK.phrase, - }, - ) - ] - ) - - def test_connection_errors(self): - trace_configs = [aiohttp_client.create_trace_config()] - - for url, expected_status in ( - ("http://this-is-unknown.local/", StatusCode.ERROR), - ("http://127.0.0.1:1/", StatusCode.ERROR), - ): - with self.subTest(expected_status=expected_status): - - async def do_request(url): - async with aiohttp.ClientSession( - trace_configs=trace_configs, - ) as session: - async with session.get(url): - pass - - loop = asyncio.get_event_loop() - with self.assertRaises(aiohttp.ClientConnectorError): - loop.run_until_complete(do_request(url)) - - self.assert_spans( - [ - ( - "HTTP GET", - (expected_status, None), - { - "component": "http", - "http.method": "GET", - "http.url": url, - }, - ) - ] - ) - self.memory_exporter.clear() - - def test_timeout(self): - async def request_handler(request): - await asyncio.sleep(1) - assert "traceparent" in request.headers - return aiohttp.web.Response() - - host, port = self._http_request( - trace_config=aiohttp_client.create_trace_config(), - url="/test_timeout", - request_handler=request_handler, - timeout=aiohttp.ClientTimeout(sock_read=0.01), - ) - - self.assert_spans( - [ - ( - "HTTP GET", - (StatusCode.ERROR, None), - { - "component": "http", - "http.method": "GET", - "http.url": "http://{}:{}/test_timeout".format( - host, port - ), - }, - ) - ] - ) - - def test_too_many_redirects(self): - async def request_handler(request): - # Create a redirect loop. - location = request.url - assert "traceparent" in request.headers - raise aiohttp.web.HTTPFound(location=location) - - host, port = self._http_request( - trace_config=aiohttp_client.create_trace_config(), - url="/test_too_many_redirects", - request_handler=request_handler, - max_redirects=2, - ) - - self.assert_spans( - [ - ( - "HTTP GET", - (StatusCode.ERROR, None), - { - "component": "http", - "http.method": "GET", - "http.url": "http://{}:{}/test_too_many_redirects".format( - host, port - ), - }, - ) - ] - ) - - -class TestAioHttpClientInstrumentor(TestBase): - URL = "/test-path" - - def setUp(self): - super().setUp() - AioHttpClientInstrumentor().instrument() - - def tearDown(self): - super().tearDown() - AioHttpClientInstrumentor().uninstrument() - - @staticmethod - # pylint:disable=unused-argument - async def default_handler(request): - return aiohttp.web.Response(status=int(200)) - - @staticmethod - def get_default_request(url: str = URL): - async def default_request(server: aiohttp.test_utils.TestServer): - async with aiohttp.test_utils.TestClient(server) as session: - await session.get(url) - - return default_request - - def assert_spans(self, num_spans: int): - finished_spans = self.memory_exporter.get_finished_spans() - self.assertEqual(num_spans, len(finished_spans)) - if num_spans == 0: - return None - if num_spans == 1: - return finished_spans[0] - return finished_spans - - def test_instrument(self): - host, port = run_with_test_server( - self.get_default_request(), self.URL, self.default_handler - ) - span = self.assert_spans(1) - self.assertEqual("http", span.attributes["component"]) - self.assertEqual("GET", span.attributes["http.method"]) - self.assertEqual( - "http://{}:{}/test-path".format(host, port), - span.attributes["http.url"], - ) - self.assertEqual(200, span.attributes["http.status_code"]) - self.assertEqual("OK", span.attributes["http.status_text"]) - - def test_instrument_with_existing_trace_config(self): - trace_config = aiohttp.TraceConfig() - - async def create_session(server: aiohttp.test_utils.TestServer): - async with aiohttp.test_utils.TestClient( - server, trace_configs=[trace_config] - ) as client: - # pylint:disable=protected-access - trace_configs = client.session._trace_configs - self.assertEqual(2, len(trace_configs)) - self.assertTrue(trace_config in trace_configs) - async with client as session: - await session.get(TestAioHttpClientInstrumentor.URL) - - run_with_test_server(create_session, self.URL, self.default_handler) - self.assert_spans(1) - - def test_uninstrument(self): - AioHttpClientInstrumentor().uninstrument() - run_with_test_server( - self.get_default_request(), self.URL, self.default_handler - ) - - self.assert_spans(0) - - AioHttpClientInstrumentor().instrument() - run_with_test_server( - self.get_default_request(), self.URL, self.default_handler - ) - self.assert_spans(1) - - def test_uninstrument_session(self): - async def uninstrument_request(server: aiohttp.test_utils.TestServer): - client = aiohttp.test_utils.TestClient(server) - AioHttpClientInstrumentor().uninstrument_session(client.session) - async with client as session: - await session.get(self.URL) - - run_with_test_server( - uninstrument_request, self.URL, self.default_handler - ) - self.assert_spans(0) - - run_with_test_server( - self.get_default_request(), self.URL, self.default_handler - ) - self.assert_spans(1) - - def test_suppress_instrumentation(self): - token = context.attach( - context.set_value("suppress_instrumentation", True) - ) - try: - run_with_test_server( - self.get_default_request(), self.URL, self.default_handler - ) - finally: - context.detach(token) - self.assert_spans(0) - - @staticmethod - async def suppressed_request(server: aiohttp.test_utils.TestServer): - async with aiohttp.test_utils.TestClient(server) as client: - token = context.attach( - context.set_value("suppress_instrumentation", True) - ) - await client.get(TestAioHttpClientInstrumentor.URL) - context.detach(token) - - def test_suppress_instrumentation_after_creation(self): - run_with_test_server( - self.suppressed_request, self.URL, self.default_handler - ) - self.assert_spans(0) - - def test_suppress_instrumentation_with_server_exception(self): - # pylint:disable=unused-argument - async def raising_handler(request): - raise aiohttp.web.HTTPFound(location=self.URL) - - run_with_test_server( - self.suppressed_request, self.URL, raising_handler - ) - self.assert_spans(0) - - def test_url_filter(self): - def strip_query_params(url: yarl.URL) -> str: - return str(url.with_query(None)) - - AioHttpClientInstrumentor().uninstrument() - AioHttpClientInstrumentor().instrument(url_filter=strip_query_params) - - url = "/test-path?query=params" - host, port = run_with_test_server( - self.get_default_request(url), url, self.default_handler - ) - span = self.assert_spans(1) - self.assertEqual( - "http://{}:{}/test-path".format(host, port), - span.attributes["http.url"], - ) - - def test_span_name(self): - def span_name_callback(params: aiohttp.TraceRequestStartParams) -> str: - return "{} - {}".format(params.method, params.url.path) - - AioHttpClientInstrumentor().uninstrument() - AioHttpClientInstrumentor().instrument(span_name=span_name_callback) - - url = "/test-path" - run_with_test_server( - self.get_default_request(url), url, self.default_handler - ) - span = self.assert_spans(1) - self.assertEqual("GET - /test-path", span.name) - - -class TestLoadingAioHttpInstrumentor(unittest.TestCase): - def test_loading_instrumentor(self): - entry_points = iter_entry_points( - "opentelemetry_instrumentor", "aiohttp-client" - ) - - instrumentor = next(entry_points).load()() - self.assertIsInstance(instrumentor, AioHttpClientInstrumentor) diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-aiopg/CHANGELOG.md deleted file mode 100644 index c62fac0617..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiopg/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.11b0 - -Released 2020-07-28 - -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/LICENSE b/instrumentation/opentelemetry-instrumentation-aiopg/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiopg/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-aiopg/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiopg/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/README.rst b/instrumentation/opentelemetry-instrumentation-aiopg/README.rst deleted file mode 100644 index f7a66579df..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiopg/README.rst +++ /dev/null @@ -1,21 +0,0 @@ -OpenTelemetry aiopg instrumentation -=================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aiopg.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-aiopg/ - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-aiopg - - -References ----------- - -* `OpenTelemetry aiopg Instrumentation `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg deleted file mode 100644 index c903180e98..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiopg/setup.cfg +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-aiopg -description = OpenTelemetry aiopg instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-aiopg -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation-dbapi == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - aiopg >= 0.13.0 - wrapt >= 1.0.0, < 2.0.0 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - - -[options.entry_points] -opentelemetry_instrumentor = - aiopg = opentelemetry.instrumentation.aiopg:AiopgInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/setup.py b/instrumentation/opentelemetry-instrumentation-aiopg/setup.py deleted file mode 100644 index dfd463e5ab..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiopg/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "aiopg", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py deleted file mode 100644 index 176fc82b40..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/__init__.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The integration with PostgreSQL supports the aiopg library, -it can be enabled by using ``AiopgInstrumentor``. - -.. aiopg: https://github.com/aio-libs/aiopg - -Usage ------ - -.. code-block:: python - - import aiopg - from opentelemetry.instrumentation.aiopg import AiopgInstrumentor - - AiopgInstrumentor().instrument() - - cnx = await aiopg.connect(database='Database') - cursor = await cnx.cursor() - await cursor.execute("INSERT INTO test (testField) VALUES (123)") - cursor.close() - cnx.close() - - pool = await aiopg.create_pool(database='Database') - cnx = await pool.acquire() - cursor = await cnx.cursor() - await cursor.execute("INSERT INTO test (testField) VALUES (123)") - cursor.close() - cnx.close() - -API ---- -""" - -from opentelemetry.instrumentation.aiopg import wrappers -from opentelemetry.instrumentation.aiopg.version import __version__ -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor - - -class AiopgInstrumentor(BaseInstrumentor): - _CONNECTION_ATTRIBUTES = { - "database": "info.dbname", - "port": "info.port", - "host": "info.host", - "user": "info.user", - } - - _DATABASE_COMPONENT = "postgresql" - _DATABASE_TYPE = "sql" - - def _instrument(self, **kwargs): - """Integrate with PostgreSQL aiopg library. - aiopg: https://github.com/aio-libs/aiopg - """ - - tracer_provider = kwargs.get("tracer_provider") - - wrappers.wrap_connect( - __name__, - self._DATABASE_COMPONENT, - self._DATABASE_TYPE, - self._CONNECTION_ATTRIBUTES, - version=__version__, - tracer_provider=tracer_provider, - ) - - wrappers.wrap_create_pool( - __name__, - self._DATABASE_COMPONENT, - self._DATABASE_TYPE, - self._CONNECTION_ATTRIBUTES, - version=__version__, - tracer_provider=tracer_provider, - ) - - def _uninstrument(self, **kwargs): - """"Disable aiopg instrumentation""" - wrappers.unwrap_connect() - wrappers.unwrap_create_pool() - - # pylint:disable=no-self-use - def instrument_connection(self, connection): - """Enable instrumentation in a aiopg connection. - - Args: - connection: The connection to instrument. - - Returns: - An instrumented connection. - """ - return wrappers.instrument_connection( - __name__, - connection, - self._DATABASE_COMPONENT, - self._DATABASE_TYPE, - self._CONNECTION_ATTRIBUTES, - ) - - def uninstrument_connection(self, connection): - """Disable instrumentation in a aiopg connection. - - Args: - connection: The connection to uninstrument. - - Returns: - An uninstrumented connection. - """ - return wrappers.uninstrument_connection(connection) diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py deleted file mode 100644 index 14f986da06..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py +++ /dev/null @@ -1,146 +0,0 @@ -import typing - -import wrapt -from aiopg.utils import _ContextManager, _PoolAcquireContextManager - -from opentelemetry.instrumentation.dbapi import ( - DatabaseApiIntegration, - TracedCursor, -) -from opentelemetry.trace import SpanKind -from opentelemetry.trace.status import Status, StatusCode - - -# pylint: disable=abstract-method -class AsyncProxyObject(wrapt.ObjectProxy): - def __aiter__(self): - return self.__wrapped__.__aiter__() - - async def __anext__(self): - result = await self.__wrapped__.__anext__() - return result - - async def __aenter__(self): - return await self.__wrapped__.__aenter__() - - async def __aexit__(self, exc_type, exc_val, exc_tb): - return await self.__wrapped__.__aexit__(exc_type, exc_val, exc_tb) - - def __await__(self): - return self.__wrapped__.__await__() - - -class AiopgIntegration(DatabaseApiIntegration): - async def wrapped_connection( - self, - connect_method: typing.Callable[..., typing.Any], - args: typing.Tuple[typing.Any, typing.Any], - kwargs: typing.Dict[typing.Any, typing.Any], - ): - """Add object proxy to connection object.""" - connection = await connect_method(*args, **kwargs) - # pylint: disable=protected-access - self.get_connection_attributes(connection._conn) - return get_traced_connection_proxy(connection, self) - - async def wrapped_pool(self, create_pool_method, args, kwargs): - pool = await create_pool_method(*args, **kwargs) - async with pool.acquire() as connection: - # pylint: disable=protected-access - self.get_connection_attributes(connection._conn) - return get_traced_pool_proxy(pool, self) - - -def get_traced_connection_proxy( - connection, db_api_integration, *args, **kwargs -): - # pylint: disable=abstract-method - class TracedConnectionProxy(AsyncProxyObject): - # pylint: disable=unused-argument - def __init__(self, connection, *args, **kwargs): - super().__init__(connection) - - def cursor(self, *args, **kwargs): - coro = self._cursor(*args, **kwargs) - return _ContextManager(coro) - - async def _cursor(self, *args, **kwargs): - # pylint: disable=protected-access - cursor = await self.__wrapped__._cursor(*args, **kwargs) - return get_traced_cursor_proxy(cursor, db_api_integration) - - return TracedConnectionProxy(connection, *args, **kwargs) - - -def get_traced_pool_proxy(pool, db_api_integration, *args, **kwargs): - # pylint: disable=abstract-method - class TracedPoolProxy(AsyncProxyObject): - # pylint: disable=unused-argument - def __init__(self, pool, *args, **kwargs): - super().__init__(pool) - - def acquire(self): - """Acquire free connection from the pool.""" - coro = self._acquire() - return _PoolAcquireContextManager(coro, self) - - async def _acquire(self): - # pylint: disable=protected-access - connection = await self.__wrapped__._acquire() - return get_traced_connection_proxy( - connection, db_api_integration, *args, **kwargs - ) - - return TracedPoolProxy(pool, *args, **kwargs) - - -class AsyncTracedCursor(TracedCursor): - async def traced_execution( - self, - query_method: typing.Callable[..., typing.Any], - *args: typing.Tuple[typing.Any, typing.Any], - **kwargs: typing.Dict[typing.Any, typing.Any] - ): - - with self._db_api_integration.get_tracer().start_as_current_span( - self._db_api_integration.name, kind=SpanKind.CLIENT - ) as span: - self._populate_span(span, *args) - try: - result = await query_method(*args, **kwargs) - return result - except Exception as ex: # pylint: disable=broad-except - if span.is_recording(): - span.set_status(Status(StatusCode.ERROR, str(ex))) - raise ex - - -def get_traced_cursor_proxy(cursor, db_api_integration, *args, **kwargs): - _traced_cursor = AsyncTracedCursor(db_api_integration) - - # pylint: disable=abstract-method - class AsyncTracedCursorProxy(AsyncProxyObject): - - # pylint: disable=unused-argument - def __init__(self, cursor, *args, **kwargs): - super().__init__(cursor) - - async def execute(self, *args, **kwargs): - result = await _traced_cursor.traced_execution( - self.__wrapped__.execute, *args, **kwargs - ) - return result - - async def executemany(self, *args, **kwargs): - result = await _traced_cursor.traced_execution( - self.__wrapped__.executemany, *args, **kwargs - ) - return result - - async def callproc(self, *args, **kwargs): - result = await _traced_cursor.traced_execution( - self.__wrapped__.callproc, *args, **kwargs - ) - return result - - return AsyncTracedCursorProxy(cursor, *args, **kwargs) diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py deleted file mode 100644 index 8a3b6023bd..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/wrappers.py +++ /dev/null @@ -1,223 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The trace integration with aiopg based on dbapi integration, -where replaced sync wrap methods to async - -Usage ------ - -.. code-block:: python - - from opentelemetry import trace - from opentelemetry.instrumentation.aiopg import trace_integration - from opentelemetry.trace import TracerProvider - - trace.set_tracer_provider(TracerProvider()) - - trace_integration(aiopg.connection, "_connect", "postgresql", "sql") - -API ---- -""" -import logging -import typing - -import aiopg -import wrapt - -from opentelemetry.instrumentation.aiopg.aiopg_integration import ( - AiopgIntegration, - get_traced_connection_proxy, -) -from opentelemetry.instrumentation.aiopg.version import __version__ -from opentelemetry.instrumentation.utils import unwrap -from opentelemetry.trace import TracerProvider - -logger = logging.getLogger(__name__) - - -def trace_integration( - database_component: str, - database_type: str = "", - connection_attributes: typing.Dict = None, - tracer_provider: typing.Optional[TracerProvider] = None, -): - """Integrate with aiopg library. - based on dbapi integration, where replaced sync wrap methods to async - - Args: - database_component: Database driver name or - database name "postgreSQL". - database_type: The Database type. For any SQL database, "sql". - connection_attributes: Attribute names for database, port, host and - user in Connection object. - tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to - use. If ommited the current configured one is used. - """ - - wrap_connect( - __name__, - database_component, - database_type, - connection_attributes, - __version__, - tracer_provider, - ) - - -def wrap_connect( - name: str, - database_component: str, - database_type: str = "", - connection_attributes: typing.Dict = None, - version: str = "", - tracer_provider: typing.Optional[TracerProvider] = None, -): - """Integrate with aiopg library. - https://github.com/aio-libs/aiopg - - Args: - name: Name of opentelemetry extension for aiopg. - database_component: Database driver name - or database name "postgreSQL". - database_type: The Database type. For any SQL database, "sql". - connection_attributes: Attribute names for database, port, host and - user in Connection object. - version: Version of opentelemetry extension for aiopg. - tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to - use. If ommited the current configured one is used. - """ - - # pylint: disable=unused-argument - async def wrap_connect_( - wrapped: typing.Callable[..., typing.Any], - instance: typing.Any, - args: typing.Tuple[typing.Any, typing.Any], - kwargs: typing.Dict[typing.Any, typing.Any], - ): - db_integration = AiopgIntegration( - name, - database_component, - database_type=database_type, - connection_attributes=connection_attributes, - version=version, - tracer_provider=tracer_provider, - ) - return await db_integration.wrapped_connection(wrapped, args, kwargs) - - try: - wrapt.wrap_function_wrapper(aiopg, "connect", wrap_connect_) - except Exception as ex: # pylint: disable=broad-except - logger.warning("Failed to integrate with aiopg. %s", str(ex)) - - -def unwrap_connect(): - """Disable integration with aiopg library. - https://github.com/aio-libs/aiopg - """ - - unwrap(aiopg, "connect") - - -def instrument_connection( - name: str, - connection, - database_component: str, - database_type: str = "", - connection_attributes: typing.Dict = None, - version: str = "", - tracer_provider: typing.Optional[TracerProvider] = None, -): - """Enable instrumentation in a database connection. - - Args: - name: Name of opentelemetry extension for aiopg. - connection: The connection to instrument. - database_component: Database driver name or database name "postgreSQL". - database_type: The Database type. For any SQL database, "sql". - connection_attributes: Attribute names for database, port, host and - user in a connection object. - version: Version of opentelemetry extension for aiopg. - tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to - use. If ommited the current configured one is used. - - Returns: - An instrumented connection. - """ - db_integration = AiopgIntegration( - name, - database_component, - database_type, - connection_attributes=connection_attributes, - version=version, - tracer_provider=tracer_provider, - ) - db_integration.get_connection_attributes(connection) - return get_traced_connection_proxy(connection, db_integration) - - -def uninstrument_connection(connection): - """Disable instrumentation in a database connection. - - Args: - connection: The connection to uninstrument. - - Returns: - An uninstrumented connection. - """ - if isinstance(connection, wrapt.ObjectProxy): - return connection.__wrapped__ - - logger.warning("Connection is not instrumented") - return connection - - -def wrap_create_pool( - name: str, - database_component: str, - database_type: str = "", - connection_attributes: typing.Dict = None, - version: str = "", - tracer_provider: typing.Optional[TracerProvider] = None, -): - # pylint: disable=unused-argument - async def wrap_create_pool_( - wrapped: typing.Callable[..., typing.Any], - instance: typing.Any, - args: typing.Tuple[typing.Any, typing.Any], - kwargs: typing.Dict[typing.Any, typing.Any], - ): - db_integration = AiopgIntegration( - name, - database_component, - database_type, - connection_attributes=connection_attributes, - version=version, - tracer_provider=tracer_provider, - ) - return await db_integration.wrapped_pool(wrapped, args, kwargs) - - try: - wrapt.wrap_function_wrapper(aiopg, "create_pool", wrap_create_pool_) - except Exception as ex: # pylint: disable=broad-except - logger.warning("Failed to integrate with DB API. %s", str(ex)) - - -def unwrap_create_pool(): - """Disable integration with aiopg library. - https://github.com/aio-libs/aiopg - """ - unwrap(aiopg, "create_pool") diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-aiopg/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py deleted file mode 100644 index 78ea4552e2..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py +++ /dev/null @@ -1,504 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import asyncio -import logging -from unittest import mock -from unittest.mock import MagicMock - -import aiopg -from aiopg.utils import _ContextManager, _PoolAcquireContextManager - -import opentelemetry.instrumentation.aiopg -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.aiopg import AiopgInstrumentor, wrappers -from opentelemetry.instrumentation.aiopg.aiopg_integration import ( - AiopgIntegration, -) -from opentelemetry.sdk import resources -from opentelemetry.test.test_base import TestBase - - -def async_call(coro): - loop = asyncio.get_event_loop() - return loop.run_until_complete(coro) - - -class TestAiopgInstrumentor(TestBase): - def setUp(self): - super().setUp() - self.origin_aiopg_connect = aiopg.connect - self.origin_aiopg_create_pool = aiopg.create_pool - aiopg.connect = mock_connect - aiopg.create_pool = mock_create_pool - - def tearDown(self): - super().tearDown() - aiopg.connect = self.origin_aiopg_connect - aiopg.create_pool = self.origin_aiopg_create_pool - with self.disable_logging(): - AiopgInstrumentor().uninstrument() - - def test_instrumentor_connect(self): - AiopgInstrumentor().instrument() - - cnx = async_call(aiopg.connect(database="test")) - - cursor = async_call(cnx.cursor()) - - query = "SELECT * FROM test" - async_call(cursor.execute(query)) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - - # Check version and name in span's instrumentation info - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.aiopg - ) - - # check that no spans are generated after uninstrument - AiopgInstrumentor().uninstrument() - - cnx = async_call(aiopg.connect(database="test")) - cursor = async_call(cnx.cursor()) - query = "SELECT * FROM test" - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - def test_instrumentor_create_pool(self): - AiopgInstrumentor().instrument() - - pool = async_call(aiopg.create_pool(database="test")) - cnx = async_call(pool.acquire()) - cursor = async_call(cnx.cursor()) - - query = "SELECT * FROM test" - async_call(cursor.execute(query)) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - - # Check version and name in span's instrumentation info - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.aiopg - ) - - # check that no spans are generated after uninstrument - AiopgInstrumentor().uninstrument() - - pool = async_call(aiopg.create_pool(database="test")) - cnx = async_call(pool.acquire()) - cursor = async_call(cnx.cursor()) - query = "SELECT * FROM test" - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - def test_custom_tracer_provider_connect(self): - resource = resources.Resource.create({}) - result = self.create_tracer_provider(resource=resource) - tracer_provider, exporter = result - - AiopgInstrumentor().instrument(tracer_provider=tracer_provider) - - cnx = async_call(aiopg.connect(database="test")) - cursor = async_call(cnx.cursor()) - query = "SELECT * FROM test" - async_call(cursor.execute(query)) - - spans_list = exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - self.assertIs(span.resource, resource) - - def test_custom_tracer_provider_create_pool(self): - resource = resources.Resource.create({}) - result = self.create_tracer_provider(resource=resource) - tracer_provider, exporter = result - - AiopgInstrumentor().instrument(tracer_provider=tracer_provider) - - pool = async_call(aiopg.create_pool(database="test")) - cnx = async_call(pool.acquire()) - cursor = async_call(cnx.cursor()) - query = "SELECT * FROM test" - async_call(cursor.execute(query)) - - spans_list = exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - - self.assertIs(span.resource, resource) - - def test_instrument_connection(self): - cnx = async_call(aiopg.connect(database="test")) - query = "SELECT * FROM test" - cursor = async_call(cnx.cursor()) - async_call(cursor.execute(query)) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 0) - - cnx = AiopgInstrumentor().instrument_connection(cnx) - cursor = async_call(cnx.cursor()) - async_call(cursor.execute(query)) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - def test_uninstrument_connection(self): - AiopgInstrumentor().instrument() - cnx = async_call(aiopg.connect(database="test")) - query = "SELECT * FROM test" - cursor = async_call(cnx.cursor()) - async_call(cursor.execute(query)) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - cnx = AiopgInstrumentor().uninstrument_connection(cnx) - cursor = async_call(cnx.cursor()) - async_call(cursor.execute(query)) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - -class TestAiopgIntegration(TestBase): - def setUp(self): - super().setUp() - self.tracer = self.tracer_provider.get_tracer(__name__) - - def test_span_succeeded(self): - connection_props = { - "database": "testdatabase", - "server_host": "testhost", - "server_port": 123, - "user": "testuser", - } - connection_attributes = { - "database": "database", - "port": "server_port", - "host": "server_host", - "user": "user", - } - db_integration = AiopgIntegration( - self.tracer, "testcomponent", "testtype", connection_attributes - ) - mock_connection = async_call( - db_integration.wrapped_connection( - mock_connect, {}, connection_props - ) - ) - cursor = async_call(mock_connection.cursor()) - async_call(cursor.execute("Test query", ("param1Value", False))) - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - self.assertEqual(span.name, "testcomponent.testdatabase") - self.assertIs(span.kind, trace_api.SpanKind.CLIENT) - - self.assertEqual(span.attributes["component"], "testcomponent") - self.assertEqual(span.attributes["db.type"], "testtype") - self.assertEqual(span.attributes["db.instance"], "testdatabase") - self.assertEqual(span.attributes["db.statement"], "Test query") - self.assertEqual( - span.attributes["db.statement.parameters"], - "('param1Value', False)", - ) - self.assertEqual(span.attributes["db.user"], "testuser") - self.assertEqual(span.attributes["net.peer.name"], "testhost") - self.assertEqual(span.attributes["net.peer.port"], 123) - self.assertIs( - span.status.status_code, trace_api.status.StatusCode.UNSET, - ) - - def test_span_not_recording(self): - connection_props = { - "database": "testdatabase", - "server_host": "testhost", - "server_port": 123, - "user": "testuser", - } - connection_attributes = { - "database": "database", - "port": "server_port", - "host": "server_host", - "user": "user", - } - mock_tracer = mock.Mock() - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = True - db_integration = AiopgIntegration( - mock_tracer, "testcomponent", "testtype", connection_attributes - ) - mock_connection = async_call( - db_integration.wrapped_connection( - mock_connect, {}, connection_props - ) - ) - cursor = async_call(mock_connection.cursor()) - async_call(cursor.execute("Test query", ("param1Value", False))) - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_span_failed(self): - db_integration = AiopgIntegration(self.tracer, "testcomponent") - mock_connection = async_call( - db_integration.wrapped_connection(mock_connect, {}, {}) - ) - cursor = async_call(mock_connection.cursor()) - with self.assertRaises(Exception): - async_call(cursor.execute("Test query", throw_exception=True)) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - self.assertEqual(span.attributes["db.statement"], "Test query") - self.assertIs( - span.status.status_code, trace_api.status.StatusCode.ERROR, - ) - self.assertEqual(span.status.description, "Test Exception") - - def test_executemany(self): - db_integration = AiopgIntegration(self.tracer, "testcomponent") - mock_connection = async_call( - db_integration.wrapped_connection(mock_connect, {}, {}) - ) - cursor = async_call(mock_connection.cursor()) - async_call(cursor.executemany("Test query")) - spans_list = self.memory_exporter.get_finished_spans() - - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - self.assertEqual(span.attributes["db.statement"], "Test query") - - def test_callproc(self): - db_integration = AiopgIntegration(self.tracer, "testcomponent") - mock_connection = async_call( - db_integration.wrapped_connection(mock_connect, {}, {}) - ) - cursor = async_call(mock_connection.cursor()) - async_call(cursor.callproc("Test stored procedure")) - spans_list = self.memory_exporter.get_finished_spans() - - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - self.assertEqual( - span.attributes["db.statement"], "Test stored procedure" - ) - - def test_wrap_connect(self): - aiopg_mock = AiopgMock() - with mock.patch("aiopg.connect", aiopg_mock.connect): - wrappers.wrap_connect(self.tracer, "-") - connection = async_call(aiopg.connect()) - self.assertEqual(aiopg_mock.connect_call_count, 1) - self.assertIsInstance(connection.__wrapped__, mock.Mock) - - def test_unwrap_connect(self): - wrappers.wrap_connect(self.tracer, "-") - aiopg_mock = AiopgMock() - with mock.patch("aiopg.connect", aiopg_mock.connect): - connection = async_call(aiopg.connect()) - self.assertEqual(aiopg_mock.connect_call_count, 1) - wrappers.unwrap_connect() - connection = async_call(aiopg.connect()) - self.assertEqual(aiopg_mock.connect_call_count, 2) - self.assertIsInstance(connection, mock.Mock) - - def test_wrap_create_pool(self): - async def check_connection(pool): - async with pool.acquire() as connection: - self.assertEqual(aiopg_mock.create_pool_call_count, 1) - self.assertIsInstance( - connection.__wrapped__, AiopgConnectionMock - ) - - aiopg_mock = AiopgMock() - with mock.patch("aiopg.create_pool", aiopg_mock.create_pool): - wrappers.wrap_create_pool(self.tracer, "-") - pool = async_call(aiopg.create_pool()) - async_call(check_connection(pool)) - - def test_unwrap_create_pool(self): - async def check_connection(pool): - async with pool.acquire() as connection: - self.assertEqual(aiopg_mock.create_pool_call_count, 2) - self.assertIsInstance(connection, AiopgConnectionMock) - - aiopg_mock = AiopgMock() - with mock.patch("aiopg.create_pool", aiopg_mock.create_pool): - wrappers.wrap_create_pool(self.tracer, "-") - pool = async_call(aiopg.create_pool()) - self.assertEqual(aiopg_mock.create_pool_call_count, 1) - - wrappers.unwrap_create_pool() - pool = async_call(aiopg.create_pool()) - async_call(check_connection(pool)) - - def test_instrument_connection(self): - connection = mock.Mock() - # Avoid get_attributes failing because can't concatenate mock - connection.database = "-" - connection2 = wrappers.instrument_connection( - self.tracer, connection, "-" - ) - self.assertIs(connection2.__wrapped__, connection) - - def test_uninstrument_connection(self): - connection = mock.Mock() - # Set connection.database to avoid a failure because mock can't - # be concatenated - connection.database = "-" - connection2 = wrappers.instrument_connection( - self.tracer, connection, "-" - ) - self.assertIs(connection2.__wrapped__, connection) - - connection3 = wrappers.uninstrument_connection(connection2) - self.assertIs(connection3, connection) - - with self.assertLogs(level=logging.WARNING): - connection4 = wrappers.uninstrument_connection(connection) - self.assertIs(connection4, connection) - - -# pylint: disable=unused-argument -async def mock_connect(*args, **kwargs): - database = kwargs.get("database") - server_host = kwargs.get("server_host") - server_port = kwargs.get("server_port") - user = kwargs.get("user") - return MockConnection(database, server_port, server_host, user) - - -# pylint: disable=unused-argument -async def mock_create_pool(*args, **kwargs): - database = kwargs.get("database") - server_host = kwargs.get("server_host") - server_port = kwargs.get("server_port") - user = kwargs.get("user") - return MockPool(database, server_port, server_host, user) - - -class MockPool: - def __init__(self, database, server_port, server_host, user): - self.database = database - self.server_port = server_port - self.server_host = server_host - self.user = user - - async def release(self, conn): - return conn - - def acquire(self): - """Acquire free connection from the pool.""" - coro = self._acquire() - return _PoolAcquireContextManager(coro, self) - - async def _acquire(self): - connect = await mock_connect( - self.database, self.server_port, self.server_host, self.user - ) - return connect - - -class MockPsycopg2Connection: - def __init__(self, database, server_port, server_host, user): - self.database = database - self.server_port = server_port - self.server_host = server_host - self.user = user - - -class MockConnection: - def __init__(self, database, server_port, server_host, user): - self._conn = MockPsycopg2Connection( - database, server_port, server_host, user - ) - - # pylint: disable=no-self-use - def cursor(self): - coro = self._cursor() - return _ContextManager(coro) - - async def _cursor(self): - return MockCursor() - - def close(self): - pass - - -class MockCursor: - # pylint: disable=unused-argument, no-self-use - async def execute(self, query, params=None, throw_exception=False): - if throw_exception: - raise Exception("Test Exception") - - # pylint: disable=unused-argument, no-self-use - async def executemany(self, query, params=None, throw_exception=False): - if throw_exception: - raise Exception("Test Exception") - - # pylint: disable=unused-argument, no-self-use - async def callproc(self, query, params=None, throw_exception=False): - if throw_exception: - raise Exception("Test Exception") - - -class AiopgConnectionMock: - _conn = MagicMock() - - async def __aexit__(self, exc_type, exc_val, exc_tb): - pass - - async def __aenter__(self): - return MagicMock() - - -class AiopgPoolMock: - async def release(self, conn): - return conn - - def acquire(self): - coro = self._acquire() - return _PoolAcquireContextManager(coro, self) - - async def _acquire(self): - return AiopgConnectionMock() - - -class AiopgMock: - def __init__(self): - self.connect_call_count = 0 - self.create_pool_call_count = 0 - - async def connect(self, *args, **kwargs): - self.connect_call_count += 1 - return MagicMock() - - async def create_pool(self, *args, **kwargs): - self.create_pool_call_count += 1 - return AiopgPoolMock() diff --git a/instrumentation/opentelemetry-instrumentation-asgi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-asgi/CHANGELOG.md deleted file mode 100644 index 686b8cf830..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asgi/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-asgi - ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) - -## 0.8b0 - -Released 2020-05-27 - -- Add ASGI middleware ([#716](https://github.com/open-telemetry/opentelemetry-python/pull/716)) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/README.rst b/instrumentation/opentelemetry-instrumentation-asgi/README.rst deleted file mode 100644 index 3eb8e2dda7..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asgi/README.rst +++ /dev/null @@ -1,71 +0,0 @@ -OpenTelemetry ASGI Instrumentation -================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-asgi.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-asgi/ - - -This library provides a ASGI middleware that can be used on any ASGI framework -(such as Django, Starlette, FastAPI or Quart) to track requests timing through OpenTelemetry. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-asgi - - -Usage (Quart) -------------- - -.. code-block:: python - - from quart import Quart - from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware - - app = Quart(__name__) - app.asgi_app = OpenTelemetryMiddleware(app.asgi_app) - - @app.route("/") - async def hello(): - return "Hello!" - - if __name__ == "__main__": - app.run(debug=True) - - -Usage (Django 3.0) ------------------- - -Modify the application's ``asgi.py`` file as shown below. - -.. code-block:: python - - import os - from django.core.asgi import get_asgi_application - from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware - - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'asgi_example.settings') - - application = get_asgi_application() - application = OpenTelemetryMiddleware(application) - - -Usage (Raw ASGI) ----------------- - -.. code-block:: python - - from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware - - app = ... # An ASGI application. - app = OpenTelemetryMiddleware(app) - - -References ----------- - -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg deleted file mode 100644 index dafb837943..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asgi/setup.cfg +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-asgi -description = ASGI instrumentation for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/instrumentation/opentelemetry-instrumentation-asgi -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - asgiref ~= 3.0 - -[options.extras_require] -test = - opentelemetry-test - -[options.packages.find] -where = src diff --git a/instrumentation/opentelemetry-instrumentation-asgi/setup.py b/instrumentation/opentelemetry-instrumentation-asgi/setup.py deleted file mode 100644 index 3369352fe1..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asgi/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "asgi", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py deleted file mode 100644 index 1c442889a1..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ /dev/null @@ -1,208 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The opentelemetry-instrumentation-asgi package provides an ASGI middleware that can be used -on any ASGI framework (such as Django-channels / Quart) to track requests -timing through OpenTelemetry. -""" - -import operator -import typing -import urllib -from functools import wraps -from typing import Tuple - -from asgiref.compatibility import guarantee_single_callable - -from opentelemetry import context, propagators, trace -from opentelemetry.instrumentation.asgi.version import __version__ # noqa -from opentelemetry.instrumentation.utils import http_status_to_status_code -from opentelemetry.trace.propagation.textmap import DictGetter -from opentelemetry.trace.status import Status, StatusCode - - -class CarrierGetter(DictGetter): - def get(self, carrier: dict, key: str) -> typing.List[str]: - """Getter implementation to retrieve a HTTP header value from the ASGI - scope. - - Args: - carrier: ASGI scope object - key: header name in scope - Returns: - A list with a single string with the header value if it exists, - else an empty list. - """ - headers = carrier.get("headers") - return [ - _value.decode("utf8") - for (_key, _value) in headers - if _key.decode("utf8") == key - ] - - -carrier_getter = CarrierGetter() - - -def collect_request_attributes(scope): - """Collects HTTP request attributes from the ASGI scope and returns a - dictionary to be used as span creation attributes.""" - server = scope.get("server") or ["0.0.0.0", 80] - port = server[1] - server_host = server[0] + (":" + str(port) if port != 80 else "") - full_path = scope.get("root_path", "") + scope.get("path", "") - http_url = scope.get("scheme", "http") + "://" + server_host + full_path - query_string = scope.get("query_string") - if query_string and http_url: - if isinstance(query_string, bytes): - query_string = query_string.decode("utf8") - http_url = http_url + ("?" + urllib.parse.unquote(query_string)) - - result = { - "component": scope["type"], - "http.scheme": scope.get("scheme"), - "http.host": server_host, - "host.port": port, - "http.flavor": scope.get("http_version"), - "http.target": scope.get("path"), - "http.url": http_url, - } - http_method = scope.get("method") - if http_method: - result["http.method"] = http_method - http_host_value = ",".join(carrier_getter.get(scope, "host")) - if http_host_value: - result["http.server_name"] = http_host_value - http_user_agent = carrier_getter.get(scope, "user-agent") - if len(http_user_agent) > 0: - result["http.user_agent"] = http_user_agent[0] - - if "client" in scope and scope["client"] is not None: - result["net.peer.ip"] = scope.get("client")[0] - result["net.peer.port"] = scope.get("client")[1] - - # remove None values - result = {k: v for k, v in result.items() if v is not None} - - return result - - -def set_status_code(span, status_code): - """Adds HTTP response attributes to span using the status_code argument.""" - if not span.is_recording(): - return - try: - status_code = int(status_code) - except ValueError: - span.set_status( - Status( - StatusCode.ERROR, - "Non-integer HTTP status: " + repr(status_code), - ) - ) - else: - span.set_attribute("http.status_code", status_code) - span.set_status(Status(http_status_to_status_code(status_code))) - - -def get_default_span_details(scope: dict) -> Tuple[str, dict]: - """Default implementation for span_details_callback - - Args: - scope: the asgi scope dictionary - - Returns: - a tuple of the span, and any attributes to attach to the - span. - """ - method_or_path = scope.get("method") or scope.get("path") - - return method_or_path, {} - - -class OpenTelemetryMiddleware: - """The ASGI application middleware. - - This class is an ASGI middleware that starts and annotates spans for any - requests it is invoked with. - - Args: - app: The ASGI application callable to forward requests to. - span_details_callback: Callback which should return a string - and a tuple, representing the desired span name and a - dictionary with any additional span attributes to set. - Optional: Defaults to get_default_span_details. - """ - - def __init__(self, app, span_details_callback=None): - self.app = guarantee_single_callable(app) - self.tracer = trace.get_tracer(__name__, __version__) - self.span_details_callback = ( - span_details_callback or get_default_span_details - ) - - async def __call__(self, scope, receive, send): - """The ASGI application - - Args: - scope: A ASGI environment. - receive: An awaitable callable yielding dictionaries - send: An awaitable callable taking a single dictionary as argument. - """ - if scope["type"] not in ("http", "websocket"): - return await self.app(scope, receive, send) - - token = context.attach(propagators.extract(carrier_getter, scope)) - span_name, additional_attributes = self.span_details_callback(scope) - - try: - with self.tracer.start_as_current_span( - span_name + " asgi", kind=trace.SpanKind.SERVER, - ) as span: - if span.is_recording(): - attributes = collect_request_attributes(scope) - attributes.update(additional_attributes) - for key, value in attributes.items(): - span.set_attribute(key, value) - - @wraps(receive) - async def wrapped_receive(): - with self.tracer.start_as_current_span( - span_name + " asgi." + scope["type"] + ".receive" - ) as receive_span: - message = await receive() - if receive_span.is_recording(): - if message["type"] == "websocket.receive": - set_status_code(receive_span, 200) - receive_span.set_attribute("type", message["type"]) - return message - - @wraps(send) - async def wrapped_send(message): - with self.tracer.start_as_current_span( - span_name + " asgi." + scope["type"] + ".send" - ) as send_span: - if send_span.is_recording(): - if message["type"] == "http.response.start": - status_code = message["status"] - set_status_code(send_span, status_code) - elif message["type"] == "websocket.send": - set_status_code(send_span, 200) - send_span.set_attribute("type", message["type"]) - await send(message) - - await self.app(scope, wrapped_receive, wrapped_send) - finally: - context.detach(token) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py deleted file mode 100644 index cf8944ce6d..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py +++ /dev/null @@ -1,361 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import sys -import unittest -import unittest.mock as mock - -import opentelemetry.instrumentation.asgi as otel_asgi -from opentelemetry import trace as trace_api -from opentelemetry.test.asgitestutil import ( - AsgiTestBase, - setup_testing_defaults, -) - - -async def http_app(scope, receive, send): - message = await receive() - assert scope["type"] == "http" - if message.get("type") == "http.request": - await send( - { - "type": "http.response.start", - "status": 200, - "headers": [[b"Content-Type", b"text/plain"]], - } - ) - await send({"type": "http.response.body", "body": b"*"}) - - -async def websocket_app(scope, receive, send): - assert scope["type"] == "websocket" - while True: - message = await receive() - if message.get("type") == "websocket.connect": - await send({"type": "websocket.accept"}) - - if message.get("type") == "websocket.receive": - if message.get("text") == "ping": - await send({"type": "websocket.send", "text": "pong"}) - - if message.get("type") == "websocket.disconnect": - break - - -async def simple_asgi(scope, receive, send): - assert isinstance(scope, dict) - if scope["type"] == "http": - await http_app(scope, receive, send) - elif scope["type"] == "websocket": - await websocket_app(scope, receive, send) - - -async def error_asgi(scope, receive, send): - assert isinstance(scope, dict) - assert scope["type"] == "http" - message = await receive() - if message.get("type") == "http.request": - try: - raise ValueError - except ValueError: - scope["hack_exc_info"] = sys.exc_info() - await send( - { - "type": "http.response.start", - "status": 200, - "headers": [[b"Content-Type", b"text/plain"]], - } - ) - await send({"type": "http.response.body", "body": b"*"}) - - -class TestAsgiApplication(AsgiTestBase): - def validate_outputs(self, outputs, error=None, modifiers=None): - # Ensure modifiers is a list - modifiers = modifiers or [] - # Check for expected outputs - self.assertEqual(len(outputs), 2) - response_start = outputs[0] - response_body = outputs[1] - self.assertEqual(response_start["type"], "http.response.start") - self.assertEqual(response_body["type"], "http.response.body") - - # Check http response body - self.assertEqual(response_body["body"], b"*") - - # Check http response start - self.assertEqual(response_start["status"], 200) - self.assertEqual( - response_start["headers"], [[b"Content-Type", b"text/plain"]] - ) - - exc_info = self.scope.get("hack_exc_info") - if error: - self.assertIs(exc_info[0], error) - self.assertIsInstance(exc_info[1], error) - self.assertIsNotNone(exc_info[2]) - else: - self.assertIsNone(exc_info) - - # Check spans - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 4) - expected = [ - { - "name": "GET asgi.http.receive", - "kind": trace_api.SpanKind.INTERNAL, - "attributes": {"type": "http.request"}, - }, - { - "name": "GET asgi.http.send", - "kind": trace_api.SpanKind.INTERNAL, - "attributes": { - "http.status_code": 200, - "type": "http.response.start", - }, - }, - { - "name": "GET asgi.http.send", - "kind": trace_api.SpanKind.INTERNAL, - "attributes": {"type": "http.response.body"}, - }, - { - "name": "GET asgi", - "kind": trace_api.SpanKind.SERVER, - "attributes": { - "component": "http", - "http.method": "GET", - "http.scheme": "http", - "host.port": 80, - "http.host": "127.0.0.1", - "http.flavor": "1.0", - "http.target": "/", - "http.url": "http://127.0.0.1/", - "net.peer.ip": "127.0.0.1", - "net.peer.port": 32767, - }, - }, - ] - # Run our expected modifiers - for modifier in modifiers: - expected = modifier(expected) - # Check that output matches - for span, expected in zip(span_list, expected): - self.assertEqual(span.name, expected["name"]) - self.assertEqual(span.kind, expected["kind"]) - self.assertDictEqual(dict(span.attributes), expected["attributes"]) - - def test_basic_asgi_call(self): - """Test that spans are emitted as expected.""" - app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) - self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() - self.validate_outputs(outputs) - - def test_wsgi_not_recording(self): - mock_tracer = mock.Mock() - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_as_current_span.return_value = mock_span - mock_tracer.start_as_current_span.return_value.__enter__ = mock_span - mock_tracer.start_as_current_span.return_value.__exit__ = mock_span - with mock.patch("opentelemetry.trace.get_tracer") as tracer: - tracer.return_value = mock_tracer - app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) - self.seed_app(app) - self.send_default_request() - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_asgi_exc_info(self): - """Test that exception information is emitted as expected.""" - app = otel_asgi.OpenTelemetryMiddleware(error_asgi) - self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() - self.validate_outputs(outputs, error=ValueError) - - def test_override_span_name(self): - """Test that span_names can be overwritten by our callback function.""" - span_name = "Dymaxion" - - def get_predefined_span_details(_): - return span_name, {} - - def update_expected_span_name(expected): - for entry in expected: - entry["name"] = " ".join( - [span_name] + entry["name"].split(" ")[-1:] - ) - return expected - - app = otel_asgi.OpenTelemetryMiddleware( - simple_asgi, span_details_callback=get_predefined_span_details - ) - self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() - self.validate_outputs(outputs, modifiers=[update_expected_span_name]) - - def test_behavior_with_scope_server_as_none(self): - """Test that middleware is ok when server is none in scope.""" - - def update_expected_server(expected): - expected[3]["attributes"].update( - { - "http.host": "0.0.0.0", - "host.port": 80, - "http.url": "http://0.0.0.0/", - } - ) - return expected - - self.scope["server"] = None - app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) - self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() - self.validate_outputs(outputs, modifiers=[update_expected_server]) - - def test_host_header(self): - """Test that host header is converted to http.server_name.""" - hostname = b"server_name_1" - - def update_expected_server(expected): - expected[3]["attributes"].update( - {"http.server_name": hostname.decode("utf8")} - ) - return expected - - self.scope["headers"].append([b"host", hostname]) - app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) - self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() - self.validate_outputs(outputs, modifiers=[update_expected_server]) - - def test_user_agent(self): - """Test that host header is converted to http.server_name.""" - user_agent = b"test-agent" - - def update_expected_user_agent(expected): - expected[3]["attributes"].update( - {"http.user_agent": user_agent.decode("utf8")} - ) - return expected - - self.scope["headers"].append([b"user-agent", user_agent]) - app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) - self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() - self.validate_outputs(outputs, modifiers=[update_expected_user_agent]) - - def test_websocket(self): - self.scope = { - "type": "websocket", - "http_version": "1.1", - "scheme": "ws", - "path": "/", - "query_string": b"", - "headers": [], - "client": ("127.0.0.1", 32767), - "server": ("127.0.0.1", 80), - } - app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) - self.seed_app(app) - self.send_input({"type": "websocket.connect"}) - self.send_input({"type": "websocket.receive", "text": "ping"}) - self.send_input({"type": "websocket.disconnect"}) - self.get_all_output() - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 6) - expected = [ - "/ asgi.websocket.receive", - "/ asgi.websocket.send", - "/ asgi.websocket.receive", - "/ asgi.websocket.send", - "/ asgi.websocket.receive", - "/ asgi", - ] - actual = [span.name for span in span_list] - self.assertListEqual(actual, expected) - - def test_lifespan(self): - self.scope["type"] = "lifespan" - app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) - self.seed_app(app) - self.send_default_request() - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) - - -class TestAsgiAttributes(unittest.TestCase): - def setUp(self): - self.scope = {} - setup_testing_defaults(self.scope) - self.span = mock.create_autospec(trace_api.Span, spec_set=True) - - def test_request_attributes(self): - self.scope["query_string"] = b"foo=bar" - - attrs = otel_asgi.collect_request_attributes(self.scope) - self.assertDictEqual( - attrs, - { - "component": "http", - "http.method": "GET", - "http.host": "127.0.0.1", - "http.target": "/", - "http.url": "http://127.0.0.1/?foo=bar", - "host.port": 80, - "http.scheme": "http", - "http.flavor": "1.0", - "net.peer.ip": "127.0.0.1", - "net.peer.port": 32767, - }, - ) - - def test_query_string(self): - self.scope["query_string"] = b"foo=bar" - attrs = otel_asgi.collect_request_attributes(self.scope) - self.assertEqual(attrs["http.url"], "http://127.0.0.1/?foo=bar") - - def test_query_string_percent_bytes(self): - self.scope["query_string"] = b"foo%3Dbar" - attrs = otel_asgi.collect_request_attributes(self.scope) - self.assertEqual(attrs["http.url"], "http://127.0.0.1/?foo=bar") - - def test_query_string_percent_str(self): - self.scope["query_string"] = "foo%3Dbar" - attrs = otel_asgi.collect_request_attributes(self.scope) - self.assertEqual(attrs["http.url"], "http://127.0.0.1/?foo=bar") - - def test_response_attributes(self): - otel_asgi.set_status_code(self.span, 404) - expected = (mock.call("http.status_code", 404),) - self.assertEqual(self.span.set_attribute.call_count, 1) - self.assertEqual(self.span.set_attribute.call_count, 1) - self.span.set_attribute.assert_has_calls(expected, any_order=True) - - def test_response_attributes_invalid_status_code(self): - otel_asgi.set_status_code(self.span, "Invalid Status Code") - self.assertEqual(self.span.set_status.call_count, 1) - - -if __name__ == "__main__": - unittest.main() diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md deleted file mode 100644 index 56f261c9b5..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-asyncpg - ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - -## Version 0.11b0 - -Released 2020-07-28 - -- Shouldn't capture query parameters by default - ([#854](https://github.com/open-telemetry/opentelemetry-python/pull/854)) - -## Version 0.10b0 - -Released 2020-06-23 - -- Initial Release ([#814](https://github.com/open-telemetry/opentelemetry-python/pull/814)) diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/README.rst b/instrumentation/opentelemetry-instrumentation-asyncpg/README.rst deleted file mode 100644 index 33c60852cd..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/README.rst +++ /dev/null @@ -1,23 +0,0 @@ -OpenTelemetry asyncpg Instrumentation -===================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-asyncpg.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-asyncpg/ - -This library allows tracing PostgreSQL queries made by the -`asyncpg `_ library. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-asyncpg - -References ----------- - -* `OpenTelemetry asyncpg Instrumentation `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg deleted file mode 100644 index 0e0e32fc8b..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-asyncpg -description = OpenTelemetry instrumentation for AsyncPG -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/instrumentation/opentelemetry-instrumentation-asyncpg -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - asyncpg >= 0.12.0 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - asyncpg = opentelemetry.instrumentation.asyncpg:AsyncPGInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.py b/instrumentation/opentelemetry-instrumentation-asyncpg/setup.py deleted file mode 100644 index 2ad47ac9d9..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "asyncpg", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py deleted file mode 100644 index 2f4ecaf3af..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This library allows tracing PostgreSQL queries made by the -`asyncpg `_ library. - -Usage ------ - -.. code-block:: python - - import asyncpg - from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor - - # You can optionally pass a custom TracerProvider to AsyncPGInstrumentor.instrument() - AsyncPGInstrumentor().instrument() - conn = await asyncpg.connect(user='user', password='password', - database='database', host='127.0.0.1') - values = await conn.fetch('''SELECT 42;''') - -API ---- -""" - -import asyncpg -import wrapt -from asyncpg import exceptions - -from opentelemetry import trace -from opentelemetry.instrumentation.asyncpg.version import __version__ -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.utils import unwrap -from opentelemetry.trace import SpanKind -from opentelemetry.trace.status import Status, StatusCode - -_APPLIED = "_opentelemetry_tracer" - - -def _hydrate_span_from_args(connection, query, parameters) -> dict: - span_attributes = {"db.type": "sql"} - - params = getattr(connection, "_params", None) - span_attributes["db.instance"] = getattr(params, "database", None) - span_attributes["db.user"] = getattr(params, "user", None) - - if query is not None: - span_attributes["db.statement"] = query - - if parameters is not None and len(parameters) > 0: - span_attributes["db.statement.parameters"] = str(parameters) - - return span_attributes - - -class AsyncPGInstrumentor(BaseInstrumentor): - def __init__(self, capture_parameters=False): - super().__init__() - self.capture_parameters = capture_parameters - - def _instrument(self, **kwargs): - tracer_provider = kwargs.get( - "tracer_provider", trace.get_tracer_provider() - ) - setattr( - asyncpg, - _APPLIED, - tracer_provider.get_tracer("asyncpg", __version__), - ) - - for method in [ - "Connection.execute", - "Connection.executemany", - "Connection.fetch", - "Connection.fetchval", - "Connection.fetchrow", - ]: - wrapt.wrap_function_wrapper( - "asyncpg.connection", method, self._do_execute - ) - - def _uninstrument(self, **__): - delattr(asyncpg, _APPLIED) - for method in [ - "execute", - "executemany", - "fetch", - "fetchval", - "fetchrow", - ]: - unwrap(asyncpg.Connection, method) - - async def _do_execute(self, func, instance, args, kwargs): - tracer = getattr(asyncpg, _APPLIED) - - exception = None - - with tracer.start_as_current_span( - "postgresql", kind=SpanKind.CLIENT - ) as span: - if span.is_recording(): - span_attributes = _hydrate_span_from_args( - instance, - args[0], - args[1:] if self.capture_parameters else None, - ) - for attribute, value in span_attributes.items(): - span.set_attribute(attribute, value) - - try: - result = await func(*args, **kwargs) - except Exception as exc: # pylint: disable=W0703 - exception = exc - raise - finally: - if span.is_recording() and exception is not None: - span.set_status(Status(StatusCode.ERROR)) - - return result diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncpg/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py b/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py deleted file mode 100644 index 33b121ce53..0000000000 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py +++ /dev/null @@ -1,35 +0,0 @@ -import asyncpg -from asyncpg import Connection - -from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor -from opentelemetry.test.test_base import TestBase - - -class TestAsyncPGInstrumentation(TestBase): - def test_instrumentation_flags(self): - AsyncPGInstrumentor().instrument() - self.assertTrue(hasattr(asyncpg, "_opentelemetry_tracer")) - AsyncPGInstrumentor().uninstrument() - self.assertFalse(hasattr(asyncpg, "_opentelemetry_tracer")) - - def test_duplicated_instrumentation(self): - AsyncPGInstrumentor().instrument() - AsyncPGInstrumentor().instrument() - AsyncPGInstrumentor().instrument() - AsyncPGInstrumentor().uninstrument() - for method_name in ["execute", "fetch"]: - method = getattr(Connection, method_name, None) - self.assertFalse( - hasattr(method, "_opentelemetry_ext_asyncpg_applied") - ) - - def test_duplicated_uninstrumentation(self): - AsyncPGInstrumentor().instrument() - AsyncPGInstrumentor().uninstrument() - AsyncPGInstrumentor().uninstrument() - AsyncPGInstrumentor().uninstrument() - for method_name in ["execute", "fetch"]: - method = getattr(Connection, method_name, None) - self.assertFalse( - hasattr(method, "_opentelemetry_ext_asyncpg_applied") - ) diff --git a/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md deleted file mode 100644 index ea74ff9de2..0000000000 --- a/instrumentation/opentelemetry-instrumentation-boto/CHANGELOG.md +++ /dev/null @@ -1,31 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-boto - ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - -## Version 0.11b0 - -Released 2020-07-28 - -- ext/boto and ext/botocore: fails to export spans via jaeger -([#866](https://github.com/open-telemetry/opentelemetry-python/pull/866)) - -## 0.9b0 - -Released 2020-06-10 - -- Initial release - diff --git a/instrumentation/opentelemetry-instrumentation-boto/LICENSE b/instrumentation/opentelemetry-instrumentation-boto/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-boto/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-boto/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-boto/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-boto/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-boto/README.rst b/instrumentation/opentelemetry-instrumentation-boto/README.rst deleted file mode 100644 index 2b40321c00..0000000000 --- a/instrumentation/opentelemetry-instrumentation-boto/README.rst +++ /dev/null @@ -1,23 +0,0 @@ -OpenTelemetry Boto Tracing -========================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-boto.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-boto/ - -This library allows tracing requests made by the Boto library. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-boto - - -References ----------- - -* `OpenTelemetry Boto Tracing `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg b/instrumentation/opentelemetry-instrumentation-boto/setup.cfg deleted file mode 100644 index fadbac4c5c..0000000000 --- a/instrumentation/opentelemetry-instrumentation-boto/setup.cfg +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-boto -description = OpenTelemetry Boto instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-boto -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - boto ~= 2.0 - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - opentelemetry-instrumentation-botocore == 0.16.dev0 - -[options.extras_require] -test = - boto~=2.0 - moto~=1.0 - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - boto = opentelemetry.instrumentation.boto:BotoInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-boto/setup.py b/instrumentation/opentelemetry-instrumentation-boto/setup.py deleted file mode 100644 index 2bd68894f3..0000000000 --- a/instrumentation/opentelemetry-instrumentation-boto/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "boto", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py deleted file mode 100644 index 3bef955d14..0000000000 --- a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py +++ /dev/null @@ -1,206 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Instrument `Boto`_ to trace service requests. - -There are two options for instrumenting code. The first option is to use the -``opentelemetry-instrument`` executable which will automatically -instrument your Boto client. The second is to programmatically enable -instrumentation via the following code: - -.. _boto: https://pypi.org/project/boto/ - -Usage ------ - -.. code:: python - - from opentelemetry import trace - from opentelemetry.instrumentation.boto import BotoInstrumentor - from opentelemetry.sdk.trace import TracerProvider - import boto - - trace.set_tracer_provider(TracerProvider()) - - # Instrument Boto - BotoInstrumentor().instrument(tracer_provider=trace.get_tracer_provider()) - - # This will create a span with Boto-specific attributes - ec2 = boto.ec2.connect_to_region("us-west-2") - ec2.get_all_instances() - -API ---- -""" - -import logging -from inspect import currentframe - -from boto.connection import AWSAuthConnection, AWSQueryConnection -from wrapt import wrap_function_wrapper - -from opentelemetry.instrumentation.boto.version import __version__ -from opentelemetry.instrumentation.botocore import add_span_arg_tags, unwrap -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.sdk.trace import Resource -from opentelemetry.trace import SpanKind, get_tracer - -logger = logging.getLogger(__name__) - - -def _get_instance_region_name(instance): - region = getattr(instance, "region", None) - - if not region: - return None - if isinstance(region, str): - return region.split(":")[1] - return region.name - - -class BotoInstrumentor(BaseInstrumentor): - """A instrumentor for Boto - - See `BaseInstrumentor` - """ - - def __init__(self): - super().__init__() - self._original_boto = None - - def _instrument(self, **kwargs): - # AWSQueryConnection and AWSAuthConnection are two different classes - # called by different services for connection. - # For exemple EC2 uses AWSQueryConnection and S3 uses - # AWSAuthConnection - - # FIXME should the tracer provider be accessed via Configuration - # instead? - # pylint: disable=attribute-defined-outside-init - self._tracer = get_tracer( - __name__, __version__, kwargs.get("tracer_provider") - ) - - wrap_function_wrapper( - "boto.connection", - "AWSQueryConnection.make_request", - self._patched_query_request, - ) - wrap_function_wrapper( - "boto.connection", - "AWSAuthConnection.make_request", - self._patched_auth_request, - ) - - def _uninstrument(self, **kwargs): - unwrap(AWSQueryConnection, "make_request") - unwrap(AWSAuthConnection, "make_request") - - def _common_request( # pylint: disable=too-many-locals - self, - args_name, - traced_args, - operation_name, - original_func, - instance, - args, - kwargs, - ): - - endpoint_name = getattr(instance, "host").split(".")[0] - - with self._tracer.start_as_current_span( - "{}.command".format(endpoint_name), kind=SpanKind.CONSUMER, - ) as span: - if args: - http_method = args[0] - span.resource = Resource( - attributes={ - "endpoint": endpoint_name, - "http_method": http_method.lower(), - } - ) - else: - span.resource = Resource( - attributes={"endpoint": endpoint_name} - ) - - # Original func returns a boto.connection.HTTPResponse object - result = original_func(*args, **kwargs) - - if span.is_recording(): - add_span_arg_tags( - span, endpoint_name, args, args_name, traced_args, - ) - - # Obtaining region name - region_name = _get_instance_region_name(instance) - - meta = { - "aws.agent": "boto", - "aws.operation": operation_name, - } - if region_name: - meta["aws.region"] = region_name - - for key, value in meta.items(): - span.set_attribute(key, value) - - span.set_attribute( - "http.status_code", getattr(result, "status") - ) - span.set_attribute("http.method", getattr(result, "_method")) - - return result - - def _patched_query_request(self, original_func, instance, args, kwargs): - - return self._common_request( - ("operation_name", "params", "path", "verb"), - ["operation_name", "params", "path"], - args[0] if args else None, - original_func, - instance, - args, - kwargs, - ) - - def _patched_auth_request(self, original_func, instance, args, kwargs): - operation_name = None - - frame = currentframe().f_back - operation_name = None - while frame: - if frame.f_code.co_name == "make_request": - operation_name = frame.f_back.f_code.co_name - break - frame = frame.f_back - - return self._common_request( - ( - "method", - "path", - "headers", - "data", - "host", - "auth_path", - "sender", - ), - ["path", "data", "host"], - operation_name, - original_func, - instance, - args, - kwargs, - ) diff --git a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-boto/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-boto/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-boto/tests/conftest.py b/instrumentation/opentelemetry-instrumentation-boto/tests/conftest.py deleted file mode 100644 index 884c6753c1..0000000000 --- a/instrumentation/opentelemetry-instrumentation-boto/tests/conftest.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from os import environ - - -def pytest_sessionstart(session): - # pylint: disable=unused-argument - environ["AWS_ACCESS_KEY_ID"] = "testing" - environ["AWS_SECRET_ACCESS_KEY"] = "testing" - environ["AWS_SECURITY_TOKEN"] = "testing" - environ["AWS_SESSION_TOKEN"] = "testing" - - -def pytest_sessionfinish(session): - # pylint: disable=unused-argument - environ.pop("AWS_ACCESS_KEY_ID") - environ.pop("AWS_SECRET_ACCESS_KEY") - environ.pop("AWS_SECURITY_TOKEN") - environ.pop("AWS_SESSION_TOKEN") diff --git a/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py b/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py deleted file mode 100644 index cb45514c79..0000000000 --- a/instrumentation/opentelemetry-instrumentation-boto/tests/test_boto_instrumentation.py +++ /dev/null @@ -1,294 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest import skipUnless -from unittest.mock import Mock, patch - -import boto.awslambda -import boto.ec2 -import boto.elasticache -import boto.s3 -import boto.sts -from moto import ( # pylint: disable=import-error - mock_ec2_deprecated, - mock_lambda_deprecated, - mock_s3_deprecated, - mock_sts_deprecated, -) - -from opentelemetry.instrumentation.boto import BotoInstrumentor -from opentelemetry.sdk.resources import Resource -from opentelemetry.test.test_base import TestBase - - -def assert_span_http_status_code(span, code): - """Assert on the span's 'http.status_code' tag""" - tag = span.attributes["http.status_code"] - assert tag == code, "%r != %r" % (tag, code) - - -class TestBotoInstrumentor(TestBase): - """Botocore integration testsuite""" - - def setUp(self): - super().setUp() - BotoInstrumentor().instrument() - - def tearDown(self): - BotoInstrumentor().uninstrument() - - @mock_ec2_deprecated - def test_ec2_client(self): - ec2 = boto.ec2.connect_to_region("us-west-2") - - ec2.get_all_instances() - - spans = self.memory_exporter.get_finished_spans() - assert spans - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertEqual(span.attributes["aws.operation"], "DescribeInstances") - assert_span_http_status_code(span, 200) - self.assertEqual(span.attributes["http.method"], "POST") - self.assertEqual(span.attributes["aws.region"], "us-west-2") - - # Create an instance - ec2.run_instances(21) - spans = self.memory_exporter.get_finished_spans() - assert spans - self.assertEqual(len(spans), 2) - span = spans[1] - self.assertEqual(span.attributes["aws.operation"], "RunInstances") - assert_span_http_status_code(span, 200) - self.assertEqual( - span.resource, - Resource( - attributes={"endpoint": "ec2", "http_method": "runinstances"} - ), - ) - self.assertEqual(span.attributes["http.method"], "POST") - self.assertEqual(span.attributes["aws.region"], "us-west-2") - self.assertEqual(span.name, "ec2.command") - - @mock_ec2_deprecated - def test_not_recording(self): - mock_tracer = Mock() - mock_span = Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = True - with patch("opentelemetry.trace.get_tracer") as tracer: - tracer.return_value = mock_tracer - ec2 = boto.ec2.connect_to_region("us-west-2") - ec2.get_all_instances() - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - @mock_ec2_deprecated - def test_analytics_enabled_with_rate(self): - ec2 = boto.ec2.connect_to_region("us-west-2") - - ec2.get_all_instances() - - spans = self.memory_exporter.get_finished_spans() - assert spans - - @mock_ec2_deprecated - def test_analytics_enabled_without_rate(self): - ec2 = boto.ec2.connect_to_region("us-west-2") - - ec2.get_all_instances() - - spans = self.memory_exporter.get_finished_spans() - assert spans - - @mock_s3_deprecated - def test_s3_client(self): - s3 = boto.s3.connect_to_region("us-east-1") - - s3.get_all_buckets() - spans = self.memory_exporter.get_finished_spans() - assert spans - self.assertEqual(len(spans), 1) - span = spans[0] - assert_span_http_status_code(span, 200) - self.assertEqual(span.attributes["http.method"], "GET") - self.assertEqual(span.attributes["aws.operation"], "get_all_buckets") - - # Create a bucket command - s3.create_bucket("cheese") - spans = self.memory_exporter.get_finished_spans() - assert spans - self.assertEqual(len(spans), 2) - span = spans[1] - assert_span_http_status_code(span, 200) - self.assertEqual(span.attributes["http.method"], "PUT") - self.assertEqual(span.attributes["path"], "/") - self.assertEqual(span.attributes["aws.operation"], "create_bucket") - - # Get the created bucket - s3.get_bucket("cheese") - spans = self.memory_exporter.get_finished_spans() - assert spans - self.assertEqual(len(spans), 3) - span = spans[2] - assert_span_http_status_code(span, 200) - self.assertEqual( - span.resource, - Resource(attributes={"endpoint": "s3", "http_method": "head"}), - ) - self.assertEqual(span.attributes["http.method"], "HEAD") - self.assertEqual(span.attributes["aws.operation"], "head_bucket") - self.assertEqual(span.name, "s3.command") - - # Checking for resource incase of error - try: - s3.get_bucket("big_bucket") - except Exception: # pylint: disable=broad-except - spans = self.memory_exporter.get_finished_spans() - assert spans - span = spans[2] - self.assertEqual( - span.resource, - Resource(attributes={"endpoint": "s3", "http_method": "head"}), - ) - - @mock_s3_deprecated - def test_s3_put(self): - s3 = boto.s3.connect_to_region("us-east-1") - s3.create_bucket("mybucket") - bucket = s3.get_bucket("mybucket") - key = boto.s3.key.Key(bucket) - key.key = "foo" - key.set_contents_from_string("bar") - - spans = self.memory_exporter.get_finished_spans() - assert spans - # create bucket - self.assertEqual(len(spans), 3) - self.assertEqual(spans[0].attributes["aws.operation"], "create_bucket") - assert_span_http_status_code(spans[0], 200) - self.assertEqual( - spans[0].resource, - Resource(attributes={"endpoint": "s3", "http_method": "put"}), - ) - # get bucket - self.assertEqual(spans[1].attributes["aws.operation"], "head_bucket") - self.assertEqual( - spans[1].resource, - Resource(attributes={"endpoint": "s3", "http_method": "head"}), - ) - # put object - self.assertEqual( - spans[2].attributes["aws.operation"], "_send_file_internal" - ) - self.assertEqual( - spans[2].resource, - Resource(attributes={"endpoint": "s3", "http_method": "put"}), - ) - - @mock_lambda_deprecated - def test_unpatch(self): - - lamb = boto.awslambda.connect_to_region("us-east-2") - - BotoInstrumentor().uninstrument() - - # multiple calls - lamb.list_functions() - spans = self.memory_exporter.get_finished_spans() - assert not spans, spans - - @mock_s3_deprecated - def test_double_patch(self): - s3 = boto.s3.connect_to_region("us-east-1") - - BotoInstrumentor().instrument() - BotoInstrumentor().instrument() - - # Get the created bucket - s3.create_bucket("cheese") - spans = self.memory_exporter.get_finished_spans() - assert spans - self.assertEqual(len(spans), 1) - - @mock_lambda_deprecated - def test_lambda_client(self): - lamb = boto.awslambda.connect_to_region("us-east-2") - - # multiple calls - lamb.list_functions() - lamb.list_functions() - - spans = self.memory_exporter.get_finished_spans() - assert spans - self.assertEqual(len(spans), 2) - span = spans[0] - assert_span_http_status_code(span, 200) - self.assertEqual( - span.resource, - Resource(attributes={"endpoint": "lambda", "http_method": "get"}), - ) - self.assertEqual(span.attributes["http.method"], "GET") - self.assertEqual(span.attributes["aws.region"], "us-east-2") - self.assertEqual(span.attributes["aws.operation"], "list_functions") - - @mock_sts_deprecated - def test_sts_client(self): - sts = boto.sts.connect_to_region("us-west-2") - - sts.get_federation_token(12, duration=10) - - spans = self.memory_exporter.get_finished_spans() - assert spans - span = spans[0] - self.assertEqual( - span.resource, - Resource( - attributes={ - "endpoint": "sts", - "http_method": "getfederationtoken", - } - ), - ) - self.assertEqual(span.attributes["aws.region"], "us-west-2") - self.assertEqual( - span.attributes["aws.operation"], "GetFederationToken" - ) - - # checking for protection on sts against security leak - self.assertTrue("args.path" not in span.attributes.keys()) - - @skipUnless( - False, - ( - "Test to reproduce the case where args sent to patched function " - "are None, can't be mocked: needs AWS credentials" - ), - ) - def test_elasticache_client(self): - elasticache = boto.elasticache.connect_to_region("us-west-2") - - elasticache.describe_cache_clusters() - - spans = self.memory_exporter.get_finished_spans() - assert spans - span = spans[0] - self.assertEqual( - span.resource, Resource(attributes={"endpoint": "elasticcache"}) - ) - self.assertEqual(span.attributes["aws.region"], "us-west-2") diff --git a/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md deleted file mode 100644 index 9a01165b8f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-botocore/CHANGELOG.md +++ /dev/null @@ -1,30 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-botocore - ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - -## Version 0.11b0 - -Released 2020-07-28 - -- ext/boto and ext/botocore: fails to export spans via jaeger -([#866](https://github.com/open-telemetry/opentelemetry-python/pull/866)) - -## 0.9b0 - -Released 2020-06-10 - -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-botocore/LICENSE b/instrumentation/opentelemetry-instrumentation-botocore/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-botocore/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-botocore/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-botocore/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-botocore/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-botocore/README.rst b/instrumentation/opentelemetry-instrumentation-botocore/README.rst deleted file mode 100644 index 4f5eb9d9af..0000000000 --- a/instrumentation/opentelemetry-instrumentation-botocore/README.rst +++ /dev/null @@ -1,23 +0,0 @@ -OpenTelemetry Botocore Tracing -============================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-botocore.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-botocore/ - -This library allows tracing requests made by the Botocore library. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-botocore - - -References ----------- - -* `OpenTelemetry Botocore Tracing `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg b/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg deleted file mode 100644 index 6bd0190ac2..0000000000 --- a/instrumentation/opentelemetry-instrumentation-botocore/setup.cfg +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-botocore -description = OpenTelemetry Botocore instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-botocore -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - botocore ~= 1.0 - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - -[options.extras_require] -test = - moto ~= 1.0 - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - botocore = opentelemetry.instrumentation.botocore:BotocoreInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-botocore/setup.py b/instrumentation/opentelemetry-instrumentation-botocore/setup.py deleted file mode 100644 index fd5045efaa..0000000000 --- a/instrumentation/opentelemetry-instrumentation-botocore/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "botocore", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py deleted file mode 100644 index b574b86cfb..0000000000 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py +++ /dev/null @@ -1,222 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Instrument `Botocore`_ to trace service requests. - -There are two options for instrumenting code. The first option is to use the -``opentelemetry-instrument`` executable which will automatically -instrument your Botocore client. The second is to programmatically enable -instrumentation via the following code: - -.. _Botocore: https://pypi.org/project/botocore/ - -Usage ------ - -.. code:: python - - from opentelemetry import trace - from opentelemetry.instrumentation.botocore import BotocoreInstrumentor - from opentelemetry.sdk.trace import TracerProvider - import botocore - - trace.set_tracer_provider(TracerProvider()) - - # Instrument Botocore - BotocoreInstrumentor().instrument( - tracer_provider=trace.get_tracer_provider() - ) - - # This will create a span with Botocore-specific attributes - session = botocore.session.get_session() - session.set_credentials( - access_key="access-key", secret_key="secret-key" - ) - ec2 = self.session.create_client("ec2", region_name="us-west-2") - ec2.describe_instances() - -API ---- -""" - -import logging - -from botocore.client import BaseClient -from wrapt import ObjectProxy, wrap_function_wrapper - -from opentelemetry.instrumentation.botocore.version import __version__ -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.sdk.trace import Resource -from opentelemetry.trace import SpanKind, get_tracer - -logger = logging.getLogger(__name__) - - -class BotocoreInstrumentor(BaseInstrumentor): - """A instrumentor for Botocore - - See `BaseInstrumentor` - """ - - def _instrument(self, **kwargs): - - # FIXME should the tracer provider be accessed via Configuration - # instead? - # pylint: disable=attribute-defined-outside-init - self._tracer = get_tracer( - __name__, __version__, kwargs.get("tracer_provider") - ) - - wrap_function_wrapper( - "botocore.client", - "BaseClient._make_api_call", - self._patched_api_call, - ) - - def _uninstrument(self, **kwargs): - unwrap(BaseClient, "_make_api_call") - - def _patched_api_call(self, original_func, instance, args, kwargs): - - endpoint_name = deep_getattr(instance, "_endpoint._endpoint_prefix") - - with self._tracer.start_as_current_span( - "{}.command".format(endpoint_name), kind=SpanKind.CONSUMER, - ) as span: - - operation = None - if args and span.is_recording(): - operation = args[0] - span.resource = Resource( - attributes={ - "endpoint": endpoint_name, - "operation": operation.lower(), - } - ) - - else: - span.resource = Resource( - attributes={"endpoint": endpoint_name} - ) - - add_span_arg_tags( - span, - endpoint_name, - args, - ("action", "params", "path", "verb"), - {"params", "path", "verb"}, - ) - - if span.is_recording(): - region_name = deep_getattr(instance, "meta.region_name") - - meta = { - "aws.agent": "botocore", - "aws.operation": operation, - "aws.region": region_name, - } - for key, value in meta.items(): - span.set_attribute(key, value) - - result = original_func(*args, **kwargs) - - if span.is_recording(): - span.set_attribute( - "http.status_code", - result["ResponseMetadata"]["HTTPStatusCode"], - ) - span.set_attribute( - "retry_attempts", - result["ResponseMetadata"]["RetryAttempts"], - ) - - return result - - -def unwrap(obj, attr): - function = getattr(obj, attr, None) - if ( - function - and isinstance(function, ObjectProxy) - and hasattr(function, "__wrapped__") - ): - setattr(obj, attr, function.__wrapped__) - - -def add_span_arg_tags(span, endpoint_name, args, args_names, args_traced): - def truncate_arg_value(value, max_len=1024): - """Truncate values which are bytes and greater than `max_len`. - Useful for parameters like "Body" in `put_object` operations. - """ - if isinstance(value, bytes) and len(value) > max_len: - return b"..." - - return value - - def flatten_dict(dict_, sep=".", prefix=""): - """ - Returns a normalized dict of depth 1 with keys in order of embedding - """ - # adapted from https://stackoverflow.com/a/19647596 - return ( - { - prefix + sep + k if prefix else k: v - for kk, vv in dict_.items() - for k, v in flatten_dict(vv, sep, kk).items() - } - if isinstance(dict_, dict) - else {prefix: dict_} - ) - - if not span.is_recording(): - return - - if endpoint_name not in {"kms", "sts"}: - tags = dict( - (name, value) - for (name, value) in zip(args_names, args) - if name in args_traced - ) - tags = flatten_dict(tags) - for key, value in { - k: truncate_arg_value(v) - for k, v in tags.items() - if k not in {"s3": ["params.Body"]}.get(endpoint_name, []) - }.items(): - span.set_attribute(key, value) - - -def deep_getattr(obj, attr_string, default=None): - """ - Returns the attribute of ``obj`` at the dotted path given by - ``attr_string``, if no such attribute is reachable, returns ``default``. - - >>> deep_getattr(cass, "cluster") - >> deep_getattr(cass, "cluster.metadata.partitioner") - u"org.apache.cassandra.dht.Murmur3Partitioner" - - >>> deep_getattr(cass, "i.dont.exist", default="default") - "default" - """ - attrs = attr_string.split(".") - for attr in attrs: - try: - obj = getattr(obj, attr) - except AttributeError: - return default - - return obj diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py deleted file mode 100644 index fba0182eec..0000000000 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py +++ /dev/null @@ -1,277 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest.mock import Mock, patch - -import botocore.session -from botocore.exceptions import ParamValidationError -from moto import ( # pylint: disable=import-error - mock_ec2, - mock_kinesis, - mock_kms, - mock_lambda, - mock_s3, - mock_sqs, -) - -from opentelemetry.instrumentation.botocore import BotocoreInstrumentor -from opentelemetry.sdk.resources import Resource -from opentelemetry.test.test_base import TestBase - - -def assert_span_http_status_code(span, code): - """Assert on the span"s "http.status_code" tag""" - tag = span.attributes["http.status_code"] - assert tag == code, "%r != %r" % (tag, code) - - -class TestBotocoreInstrumentor(TestBase): - """Botocore integration testsuite""" - - def setUp(self): - super().setUp() - BotocoreInstrumentor().instrument() - - self.session = botocore.session.get_session() - self.session.set_credentials( - access_key="access-key", secret_key="secret-key" - ) - - def tearDown(self): - super().tearDown() - BotocoreInstrumentor().uninstrument() - - @mock_ec2 - def test_traced_client(self): - ec2 = self.session.create_client("ec2", region_name="us-west-2") - - ec2.describe_instances() - - spans = self.memory_exporter.get_finished_spans() - assert spans - span = spans[0] - self.assertEqual(len(spans), 1) - self.assertEqual(span.attributes["aws.agent"], "botocore") - self.assertEqual(span.attributes["aws.region"], "us-west-2") - self.assertEqual(span.attributes["aws.operation"], "DescribeInstances") - assert_span_http_status_code(span, 200) - self.assertEqual( - span.resource, - Resource( - attributes={ - "endpoint": "ec2", - "operation": "describeinstances", - } - ), - ) - self.assertEqual(span.name, "ec2.command") - - @mock_ec2 - def test_not_recording(self): - mock_tracer = Mock() - mock_span = Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = True - with patch("opentelemetry.trace.get_tracer") as tracer: - tracer.return_value = mock_tracer - ec2 = self.session.create_client("ec2", region_name="us-west-2") - ec2.describe_instances() - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - @mock_ec2 - def test_traced_client_analytics(self): - ec2 = self.session.create_client("ec2", region_name="us-west-2") - ec2.describe_instances() - - spans = self.memory_exporter.get_finished_spans() - assert spans - - @mock_s3 - def test_s3_client(self): - s3 = self.session.create_client("s3", region_name="us-west-2") - - s3.list_buckets() - s3.list_buckets() - - spans = self.memory_exporter.get_finished_spans() - assert spans - span = spans[0] - self.assertEqual(len(spans), 2) - self.assertEqual(span.attributes["aws.operation"], "ListBuckets") - assert_span_http_status_code(span, 200) - self.assertEqual( - span.resource, - Resource( - attributes={"endpoint": "s3", "operation": "listbuckets"} - ), - ) - - # testing for span error - self.memory_exporter.get_finished_spans() - with self.assertRaises(ParamValidationError): - s3.list_objects(bucket="mybucket") - spans = self.memory_exporter.get_finished_spans() - assert spans - span = spans[2] - self.assertEqual( - span.resource, - Resource( - attributes={"endpoint": "s3", "operation": "listobjects"} - ), - ) - - # Comment test for issue 1088 - @mock_s3 - def test_s3_put(self): - params = dict(Key="foo", Bucket="mybucket", Body=b"bar") - s3 = self.session.create_client("s3", region_name="us-west-2") - location = {"LocationConstraint": "us-west-2"} - s3.create_bucket(Bucket="mybucket", CreateBucketConfiguration=location) - s3.put_object(**params) - - spans = self.memory_exporter.get_finished_spans() - assert spans - span = spans[0] - self.assertEqual(len(spans), 2) - self.assertEqual(span.attributes["aws.operation"], "CreateBucket") - assert_span_http_status_code(span, 200) - self.assertEqual( - span.resource, - Resource( - attributes={"endpoint": "s3", "operation": "createbucket"} - ), - ) - self.assertEqual(spans[1].attributes["aws.operation"], "PutObject") - self.assertEqual( - spans[1].resource, - Resource(attributes={"endpoint": "s3", "operation": "putobject"}), - ) - self.assertEqual(spans[1].attributes["params.Key"], str(params["Key"])) - self.assertEqual( - spans[1].attributes["params.Bucket"], str(params["Bucket"]) - ) - self.assertTrue("params.Body" not in spans[1].attributes.keys()) - - @mock_sqs - def test_sqs_client(self): - sqs = self.session.create_client("sqs", region_name="us-east-1") - - sqs.list_queues() - - spans = self.memory_exporter.get_finished_spans() - assert spans - span = spans[0] - self.assertEqual(len(spans), 1) - self.assertEqual(span.attributes["aws.region"], "us-east-1") - self.assertEqual(span.attributes["aws.operation"], "ListQueues") - assert_span_http_status_code(span, 200) - self.assertEqual( - span.resource, - Resource( - attributes={"endpoint": "sqs", "operation": "listqueues"} - ), - ) - - @mock_kinesis - def test_kinesis_client(self): - kinesis = self.session.create_client( - "kinesis", region_name="us-east-1" - ) - - kinesis.list_streams() - - spans = self.memory_exporter.get_finished_spans() - assert spans - span = spans[0] - self.assertEqual(len(spans), 1) - self.assertEqual(span.attributes["aws.region"], "us-east-1") - self.assertEqual(span.attributes["aws.operation"], "ListStreams") - assert_span_http_status_code(span, 200) - self.assertEqual( - span.resource, - Resource( - attributes={"endpoint": "kinesis", "operation": "liststreams"} - ), - ) - - @mock_kinesis - def test_unpatch(self): - kinesis = self.session.create_client( - "kinesis", region_name="us-east-1" - ) - - BotocoreInstrumentor().uninstrument() - - kinesis.list_streams() - spans = self.memory_exporter.get_finished_spans() - assert not spans, spans - - @mock_sqs - def test_double_patch(self): - sqs = self.session.create_client("sqs", region_name="us-east-1") - - BotocoreInstrumentor().instrument() - BotocoreInstrumentor().instrument() - - sqs.list_queues() - - spans = self.memory_exporter.get_finished_spans() - assert spans - self.assertEqual(len(spans), 1) - - @mock_lambda - def test_lambda_client(self): - lamb = self.session.create_client("lambda", region_name="us-east-1") - - lamb.list_functions() - - spans = self.memory_exporter.get_finished_spans() - assert spans - span = spans[0] - self.assertEqual(len(spans), 1) - self.assertEqual(span.attributes["aws.region"], "us-east-1") - self.assertEqual(span.attributes["aws.operation"], "ListFunctions") - assert_span_http_status_code(span, 200) - self.assertEqual( - span.resource, - Resource( - attributes={"endpoint": "lambda", "operation": "listfunctions"} - ), - ) - - @mock_kms - def test_kms_client(self): - kms = self.session.create_client("kms", region_name="us-east-1") - - kms.list_keys(Limit=21) - - spans = self.memory_exporter.get_finished_spans() - assert spans - span = spans[0] - self.assertEqual(len(spans), 1) - self.assertEqual(span.attributes["aws.region"], "us-east-1") - self.assertEqual(span.attributes["aws.operation"], "ListKeys") - assert_span_http_status_code(span, 200) - self.assertEqual( - span.resource, - Resource(attributes={"endpoint": "kms", "operation": "listkeys"}), - ) - - # checking for protection on sts against security leak - self.assertTrue("params" not in span.attributes.keys()) diff --git a/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md deleted file mode 100644 index 8ebb8c3d24..0000000000 --- a/instrumentation/opentelemetry-instrumentation-celery/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.14b0 - -Released 2020-10-13 - -- Span operation names now include the task type. ([#1135](https://github.com/open-telemetry/opentelemetry-python/pull/1135)) -- Added automatic context propagation. ([#1135](https://github.com/open-telemetry/opentelemetry-python/pull/1135)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-celery - ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - -## Version 0.10b0 - -Released 2020-06-23 - -- Add instrumentation for Celery ([#780](https://github.com/open-telemetry/opentelemetry-python/pull/780)) diff --git a/instrumentation/opentelemetry-instrumentation-celery/LICENSE b/instrumentation/opentelemetry-instrumentation-celery/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-celery/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-celery/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-celery/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-celery/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-celery/README.rst b/instrumentation/opentelemetry-instrumentation-celery/README.rst deleted file mode 100644 index 92e5a770a0..0000000000 --- a/instrumentation/opentelemetry-instrumentation-celery/README.rst +++ /dev/null @@ -1,68 +0,0 @@ -OpenTelemetry Celery Instrumentation -==================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-celery.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-celery/ - -Instrumentation for Celery. - - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-celery - -Usage ------ - -* Start broker backend - -:: - docker run -p 5672:5672 rabbitmq - - -* Run instrumented task - -.. code-block:: python - - from opentelemetry import trace - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - from opentelemetry.instrumentation.celery import CeleryInstrumentor - - from celery import Celery - from celery.signals import worker_process_init - - @worker_process_init.connect(weak=False) - def init_celery_tracing(*args, **kwargs): - trace.set_tracer_provider(TracerProvider()) - span_processor = BatchExportSpanProcessor(ConsoleSpanExporter()) - trace.get_tracer_provider().add_span_processor(span_processor) - CeleryInstrumentor().instrument() - - app = Celery("tasks", broker="amqp://localhost") - - @app.task - def add(x, y): - return x + y - - add.delay(42, 50) - - -Setting up tracing --------------------- - -When tracing a celery worker process, tracing and instrumention both must be initialized after the celery worker -process is initialized. This is required for any tracing components that might use threading to work correctly -such as the BatchExportSpanProcessor. Celery provides a signal called ``worker_process_init`` that can be used to -accomplish this as shown in the example above. - -References ----------- -* `OpenTelemetry Celery Instrumentation `_ -* `OpenTelemetry Project `_ - diff --git a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg b/instrumentation/opentelemetry-instrumentation-celery/setup.cfg deleted file mode 100644 index d1f8866a95..0000000000 --- a/instrumentation/opentelemetry-instrumentation-celery/setup.cfg +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-celery -description = OpenTelemetry Celery Instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-celery -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - celery ~= 4.0 - -[options.extras_require] -test = - pytest - celery ~= 4.0 - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - celery = opentelemetry.instrumentation.celery:CeleryInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-celery/setup.py b/instrumentation/opentelemetry-instrumentation-celery/setup.py deleted file mode 100644 index ca67930660..0000000000 --- a/instrumentation/opentelemetry-instrumentation-celery/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "celery", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py deleted file mode 100644 index d225e6bd06..0000000000 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py +++ /dev/null @@ -1,262 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Instrument `celery`_ to trace Celery applications. - -.. _celery: https://pypi.org/project/celery/ - -Usage ------ - -* Start broker backend - -.. code:: - - docker run -p 5672:5672 rabbitmq - - -* Run instrumented task - -.. code:: python - - from opentelemetry import trace - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - from opentelemetry.instrumentation.celery import CeleryInstrumentor - - from celery import Celery - from celery.signals import worker_process_init - - @worker_process_init.connect(weak=False) - def init_celery_tracing(*args, **kwargs): - trace.set_tracer_provider(TracerProvider()) - span_processor = BatchExportSpanProcessor(ConsoleSpanExporter()) - trace.get_tracer_provider().add_span_processor(span_processor) - CeleryInstrumentor().instrument() - - app = Celery("tasks", broker="amqp://localhost") - - @app.task - def add(x, y): - return x + y - - add.delay(42, 50) - -API ---- -""" - -import logging -import signal -from collections.abc import Iterable - -from celery import signals # pylint: disable=no-name-in-module - -from opentelemetry import propagators, trace -from opentelemetry.instrumentation.celery import utils -from opentelemetry.instrumentation.celery.version import __version__ -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.trace.propagation.textmap import DictGetter -from opentelemetry.trace.status import Status, StatusCode - -logger = logging.getLogger(__name__) - -# Task operations -_TASK_TAG_KEY = "celery.action" -_TASK_APPLY_ASYNC = "apply_async" -_TASK_RUN = "run" - -_TASK_RETRY_REASON_KEY = "celery.retry.reason" -_TASK_REVOKED_REASON_KEY = "celery.revoked.reason" -_TASK_REVOKED_TERMINATED_SIGNAL_KEY = "celery.terminated.signal" -_TASK_NAME_KEY = "celery.task_name" -_MESSAGE_ID_ATTRIBUTE_NAME = "messaging.message_id" - - -class CarrierGetter(DictGetter): - def get(self, carrier, key): - value = getattr(carrier, key, []) - if isinstance(value, str) or not isinstance(value, Iterable): - value = (value,) - return value - - def keys(self, carrier): - return [] - - -carrier_getter = CarrierGetter() - - -class CeleryInstrumentor(BaseInstrumentor): - def _instrument(self, **kwargs): - tracer_provider = kwargs.get("tracer_provider") - - # pylint: disable=attribute-defined-outside-init - self._tracer = trace.get_tracer(__name__, __version__, tracer_provider) - - signals.task_prerun.connect(self._trace_prerun, weak=False) - signals.task_postrun.connect(self._trace_postrun, weak=False) - signals.before_task_publish.connect( - self._trace_before_publish, weak=False - ) - signals.after_task_publish.connect( - self._trace_after_publish, weak=False - ) - signals.task_failure.connect(self._trace_failure, weak=False) - signals.task_retry.connect(self._trace_retry, weak=False) - - def _uninstrument(self, **kwargs): - signals.task_prerun.disconnect(self._trace_prerun) - signals.task_postrun.disconnect(self._trace_postrun) - signals.before_task_publish.disconnect(self._trace_before_publish) - signals.after_task_publish.disconnect(self._trace_after_publish) - signals.task_failure.disconnect(self._trace_failure) - signals.task_retry.disconnect(self._trace_retry) - - def _trace_prerun(self, *args, **kwargs): - task = utils.retrieve_task(kwargs) - task_id = utils.retrieve_task_id(kwargs) - - if task is None or task_id is None: - return - - request = task.request - tracectx = propagators.extract(carrier_getter, request) or None - - logger.debug("prerun signal start task_id=%s", task_id) - - operation_name = "{0}/{1}".format(_TASK_RUN, task.name) - span = self._tracer.start_span( - operation_name, context=tracectx, kind=trace.SpanKind.CONSUMER - ) - - activation = self._tracer.use_span(span, end_on_exit=True) - activation.__enter__() - utils.attach_span(task, task_id, (span, activation)) - - @staticmethod - def _trace_postrun(*args, **kwargs): - task = utils.retrieve_task(kwargs) - task_id = utils.retrieve_task_id(kwargs) - - if task is None or task_id is None: - return - - logger.debug("postrun signal task_id=%s", task_id) - - # retrieve and finish the Span - span, activation = utils.retrieve_span(task, task_id) - if span is None: - logger.warning("no existing span found for task_id=%s", task_id) - return - - # request context tags - if span.is_recording(): - span.set_attribute(_TASK_TAG_KEY, _TASK_RUN) - utils.set_attributes_from_context(span, kwargs) - utils.set_attributes_from_context(span, task.request) - span.set_attribute(_TASK_NAME_KEY, task.name) - - activation.__exit__(None, None, None) - utils.detach_span(task, task_id) - - def _trace_before_publish(self, *args, **kwargs): - task = utils.retrieve_task_from_sender(kwargs) - task_id = utils.retrieve_task_id_from_message(kwargs) - - if task is None or task_id is None: - return - - operation_name = "{0}/{1}".format(_TASK_APPLY_ASYNC, task.name) - span = self._tracer.start_span( - operation_name, kind=trace.SpanKind.PRODUCER - ) - - # apply some attributes here because most of the data is not available - if span.is_recording(): - span.set_attribute(_TASK_TAG_KEY, _TASK_APPLY_ASYNC) - span.set_attribute(_MESSAGE_ID_ATTRIBUTE_NAME, task_id) - span.set_attribute(_TASK_NAME_KEY, task.name) - utils.set_attributes_from_context(span, kwargs) - - activation = self._tracer.use_span(span, end_on_exit=True) - activation.__enter__() - utils.attach_span(task, task_id, (span, activation), is_publish=True) - - headers = kwargs.get("headers") - if headers: - propagators.inject(type(headers).__setitem__, headers) - - @staticmethod - def _trace_after_publish(*args, **kwargs): - task = utils.retrieve_task_from_sender(kwargs) - task_id = utils.retrieve_task_id_from_message(kwargs) - - if task is None or task_id is None: - return - - # retrieve and finish the Span - _, activation = utils.retrieve_span(task, task_id, is_publish=True) - if activation is None: - logger.warning("no existing span found for task_id=%s", task_id) - return - - activation.__exit__(None, None, None) - utils.detach_span(task, task_id, is_publish=True) - - @staticmethod - def _trace_failure(*args, **kwargs): - task = utils.retrieve_task_from_sender(kwargs) - task_id = utils.retrieve_task_id(kwargs) - - if task is None or task_id is None: - return - - # retrieve and pass exception info to activation - span, _ = utils.retrieve_span(task, task_id) - if span is None or not span.is_recording(): - return - - status_kwargs = {"status_code": StatusCode.ERROR} - - ex = kwargs.get("einfo") - - if ( - hasattr(task, "throws") - and ex is not None - and isinstance(ex.exception, task.throws) - ): - return - - if ex is not None: - status_kwargs["description"] = str(ex) - span.set_status(Status(**status_kwargs)) - - @staticmethod - def _trace_retry(*args, **kwargs): - task = utils.retrieve_task_from_sender(kwargs) - task_id = utils.retrieve_task_id_from_request(kwargs) - reason = utils.retrieve_reason(kwargs) - - if task is None or task_id is None or reason is None: - return - - span, _ = utils.retrieve_span(task, task_id) - if span is None or not span.is_recording(): - return - - # Add retry reason metadata to span - # Use `str(reason)` instead of `reason.message` in case we get - # something that isn't an `Exception` - span.set_attribute(_TASK_RETRY_REASON_KEY, str(reason)) diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py deleted file mode 100644 index 9a44134598..0000000000 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/utils.py +++ /dev/null @@ -1,221 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from celery import registry # pylint: disable=no-name-in-module - -logger = logging.getLogger(__name__) - -# Celery Context key -CTX_KEY = "__otel_task_span" - -# Celery Context attributes -CELERY_CONTEXT_ATTRIBUTES = ( - "compression", - "correlation_id", - "countdown", - "delivery_info", - "declare", - "eta", - "exchange", - "expires", - "hostname", - "id", - "priority", - "queue", - "reply_to", - "retries", - "routing_key", - "serializer", - "timelimit", - "origin", - "state", -) - - -# pylint:disable=too-many-branches -def set_attributes_from_context(span, context): - """Helper to extract meta values from a Celery Context""" - if not span.is_recording(): - return - for key in CELERY_CONTEXT_ATTRIBUTES: - value = context.get(key) - - # Skip this key if it is not set - if value is None or value == "": - continue - - # Skip `timelimit` if it is not set (it's default/unset value is a - # tuple or a list of `None` values - if key == "timelimit" and value in [(None, None), [None, None]]: - continue - - # Skip `retries` if it's value is `0` - if key == "retries" and value == 0: - continue - - attribute_name = None - - # Celery 4.0 uses `origin` instead of `hostname`; this change preserves - # the same name for the tag despite Celery version - if key == "origin": - key = "hostname" - - elif key == "delivery_info": - # Get also destination from this - routing_key = value.get("routing_key") - if routing_key is not None: - span.set_attribute("messaging.destination", routing_key) - value = str(value) - - elif key == "id": - attribute_name = "messaging.message_id" - - elif key == "correlation_id": - attribute_name = "messaging.conversation_id" - - elif key == "routing_key": - attribute_name = "messaging.destination" - - # according to https://docs.celeryproject.org/en/stable/userguide/routing.html#exchange-types - elif key == "declare": - attribute_name = "messaging.destination_kind" - for declare in value: - if declare.exchange.type == "direct": - value = "queue" - break - if declare.exchange.type == "topic": - value = "topic" - break - - # set attribute name if not set specially for a key - if attribute_name is None: - attribute_name = "celery.{}".format(key) - - span.set_attribute(attribute_name, value) - - -def attach_span(task, task_id, span, is_publish=False): - """Helper to propagate a `Span` for the given `Task` instance. This - function uses a `dict` that stores the Span using the - `(task_id, is_publish)` as a key. This is useful when information must be - propagated from one Celery signal to another. - - We use (task_id, is_publish) for the key to ensure that publishing a - task from within another task does not cause any conflicts. - - This mostly happens when either a task fails and a retry policy is in place, - or when a task is manually retries (e.g. `task.retry()`), we end up trying - to publish a task with the same id as the task currently running. - - Previously publishing the new task would overwrite the existing `celery.run` span - in the `dict` causing that span to be forgotten and never finished - NOTE: We cannot test for this well yet, because we do not run a celery worker, - and cannot run `task.apply_async()` - """ - span_dict = getattr(task, CTX_KEY, None) - if span_dict is None: - span_dict = dict() - setattr(task, CTX_KEY, span_dict) - - span_dict[(task_id, is_publish)] = span - - -def detach_span(task, task_id, is_publish=False): - """Helper to remove a `Span` in a Celery task when it's propagated. - This function handles tasks where the `Span` is not attached. - """ - span_dict = getattr(task, CTX_KEY, None) - if span_dict is None: - return - - # See note in `attach_span` for key info - span_dict.pop((task_id, is_publish), (None, None)) - - -def retrieve_span(task, task_id, is_publish=False): - """Helper to retrieve an active `Span` stored in a `Task` - instance - """ - span_dict = getattr(task, CTX_KEY, None) - if span_dict is None: - return (None, None) - - # See note in `attach_span` for key info - return span_dict.get((task_id, is_publish), (None, None)) - - -def retrieve_task(kwargs): - task = kwargs.get("task") - if task is None: - logger.debug("Unable to retrieve task from signal arguments") - return task - - -def retrieve_task_from_sender(kwargs): - sender = kwargs.get("sender") - if sender is None: - logger.debug("Unable to retrieve the sender from signal arguments") - - # before and after publish signals sender is the task name - # for retry and failure signals sender is the task object - if isinstance(sender, str): - sender = registry.tasks.get(sender) - if sender is None: - logger.debug("Unable to retrieve the task from sender=%s", sender) - - return sender - - -def retrieve_task_id(kwargs): - task_id = kwargs.get("task_id") - if task_id is None: - logger.debug("Unable to retrieve task_id from signal arguments") - return task_id - - -def retrieve_task_id_from_request(kwargs): - # retry signal does not include task_id as argument so use request argument - request = kwargs.get("request") - if request is None: - logger.debug("Unable to retrieve the request from signal arguments") - - task_id = getattr(request, "id") - if task_id is None: - logger.debug("Unable to retrieve the task_id from the request") - - return task_id - - -def retrieve_task_id_from_message(kwargs): - """Helper to retrieve the `Task` identifier from the message `body`. - This helper supports Protocol Version 1 and 2. The Protocol is well - detailed in the official documentation: - http://docs.celeryproject.org/en/latest/internals/protocol.html - """ - headers = kwargs.get("headers") - body = kwargs.get("body") - if headers is not None and len(headers) > 0: - # Protocol Version 2 (default from Celery 4.0) - return headers.get("id") - # Protocol Version 1 - return body.get("id") - - -def retrieve_reason(kwargs): - reason = kwargs.get("reason") - if not reason: - logger.debug("Unable to retrieve the retry reason") - return reason diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-celery/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-celery/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-celery/tests/celery_test_tasks.py b/instrumentation/opentelemetry-instrumentation-celery/tests/celery_test_tasks.py deleted file mode 100644 index d9660412f0..0000000000 --- a/instrumentation/opentelemetry-instrumentation-celery/tests/celery_test_tasks.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from celery import Celery - - -class Config: - result_backend = "rpc" - broker_backend = "memory" - - -app = Celery(broker="memory:///") -app.config_from_object(Config) - - -@app.task -def task_add(num_a, num_b): - return num_a + num_b diff --git a/instrumentation/opentelemetry-instrumentation-celery/tests/test_tasks.py b/instrumentation/opentelemetry-instrumentation-celery/tests/test_tasks.py deleted file mode 100644 index 3a05ebf331..0000000000 --- a/instrumentation/opentelemetry-instrumentation-celery/tests/test_tasks.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading -import time - -from opentelemetry.instrumentation.celery import CeleryInstrumentor -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace import SpanKind - -from .celery_test_tasks import app, task_add - - -class TestCeleryInstrumentation(TestBase): - def setUp(self): - super().setUp() - self._worker = app.Worker(app=app, pool="solo", concurrency=1) - self._thread = threading.Thread(target=self._worker.start) - self._thread.daemon = True - self._thread.start() - - def tearDown(self): - super().tearDown() - self._worker.stop() - self._thread.join() - - def test_task(self): - CeleryInstrumentor().instrument() - - result = task_add.delay(1, 2) - while not result.ready(): - time.sleep(0.05) - - spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) - self.assertEqual(len(spans), 2) - - consumer, producer = spans - - self.assertEqual(consumer.name, "run/tests.celery_test_tasks.task_add") - self.assertEqual(consumer.kind, SpanKind.CONSUMER) - self.assert_span_has_attributes( - consumer, - { - "celery.action": "run", - "celery.state": "SUCCESS", - "messaging.destination": "celery", - "celery.task_name": "tests.celery_test_tasks.task_add", - }, - ) - - self.assertEqual( - producer.name, "apply_async/tests.celery_test_tasks.task_add" - ) - self.assertEqual(producer.kind, SpanKind.PRODUCER) - self.assert_span_has_attributes( - producer, - { - "celery.action": "apply_async", - "celery.task_name": "tests.celery_test_tasks.task_add", - "messaging.destination_kind": "queue", - "messaging.destination": "celery", - }, - ) - - self.assertNotEqual(consumer.parent, producer.context) - self.assertEqual(consumer.parent.span_id, producer.context.span_id) - self.assertEqual(consumer.context.trace_id, producer.context.trace_id) diff --git a/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py b/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py deleted file mode 100644 index f48f06aafb..0000000000 --- a/instrumentation/opentelemetry-instrumentation-celery/tests/test_utils.py +++ /dev/null @@ -1,232 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -from unittest import mock - -from celery import Celery - -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.celery import utils -from opentelemetry.sdk import trace - - -class TestUtils(unittest.TestCase): - def setUp(self): - self.app = Celery("celery.test_app") - - def test_set_attributes_from_context(self): - # it should extract only relevant keys - context = { - "correlation_id": "44b7f305", - "delivery_info": {"eager": True}, - "eta": "soon", - "expires": "later", - "hostname": "localhost", - "id": "44b7f305", - "reply_to": "44b7f305", - "retries": 4, - "timelimit": ("now", "later"), - "custom_meta": "custom_value", - "routing_key": "celery", - } - - span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) - utils.set_attributes_from_context(span, context) - - self.assertEqual( - span.attributes.get("messaging.message_id"), "44b7f305" - ) - self.assertEqual( - span.attributes.get("messaging.conversation_id"), "44b7f305" - ) - self.assertEqual( - span.attributes.get("messaging.destination"), "celery" - ) - - self.assertEqual( - span.attributes["celery.delivery_info"], str({"eager": True}) - ) - self.assertEqual(span.attributes.get("celery.eta"), "soon") - self.assertEqual(span.attributes.get("celery.expires"), "later") - self.assertEqual(span.attributes.get("celery.hostname"), "localhost") - - self.assertEqual(span.attributes.get("celery.reply_to"), "44b7f305") - self.assertEqual(span.attributes.get("celery.retries"), 4) - self.assertEqual( - span.attributes.get("celery.timelimit"), ("now", "later") - ) - self.assertNotIn("custom_meta", span.attributes) - - def test_set_attributes_not_recording(self): - # it should extract only relevant keys - context = { - "correlation_id": "44b7f305", - "delivery_info": {"eager": True}, - "eta": "soon", - "expires": "later", - "hostname": "localhost", - "id": "44b7f305", - "reply_to": "44b7f305", - "retries": 4, - "timelimit": ("now", "later"), - "custom_meta": "custom_value", - "routing_key": "celery", - } - - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - utils.set_attributes_from_context(mock_span, context) - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_set_attributes_from_context_empty_keys(self): - # it should not extract empty keys - context = { - "correlation_id": None, - "exchange": "", - "timelimit": (None, None), - "retries": 0, - } - - span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) - utils.set_attributes_from_context(span, context) - - self.assertEqual(len(span.attributes), 0) - # edge case: `timelimit` can also be a list of None values - context = { - "timelimit": [None, None], - } - - utils.set_attributes_from_context(span, context) - - self.assertEqual(len(span.attributes), 0) - - def test_span_propagation(self): - # ensure spans getter and setter works properly - @self.app.task - def fn_task(): - return 42 - - # propagate and retrieve a Span - task_id = "7c6731af-9533-40c3-83a9-25b58f0d837f" - span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) - utils.attach_span(fn_task, task_id, span) - span_after = utils.retrieve_span(fn_task, task_id) - self.assertIs(span, span_after) - - def test_span_delete(self): - # ensure the helper removes properly a propagated Span - @self.app.task - def fn_task(): - return 42 - - # propagate a Span - task_id = "7c6731af-9533-40c3-83a9-25b58f0d837f" - span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) - utils.attach_span(fn_task, task_id, span) - # delete the Span - utils.detach_span(fn_task, task_id) - self.assertEqual(utils.retrieve_span(fn_task, task_id), (None, None)) - - def test_span_delete_empty(self): - # ensure detach_span doesn't raise an exception if span is not present - @self.app.task - def fn_task(): - return 42 - - # delete the Span - task_id = "7c6731af-9533-40c3-83a9-25b58f0d837f" - try: - utils.detach_span(fn_task, task_id) - self.assertEqual( - utils.retrieve_span(fn_task, task_id), (None, None) - ) - except Exception as ex: # pylint: disable=broad-except - self.fail("Exception was raised: %s" % ex) - - def test_task_id_from_protocol_v1(self): - # ensures a `task_id` is properly returned when Protocol v1 is used. - # `context` is an example of an emitted Signal with Protocol v1 - context = { - "body": { - "expires": None, - "utc": True, - "args": ["user"], - "chord": None, - "callbacks": None, - "errbacks": None, - "taskset": None, - "id": "dffcaec1-dd92-4a1a-b3ab-d6512f4beeb7", - "retries": 0, - "task": "tests.contrib.celery.test_integration.fn_task_parameters", - "timelimit": (None, None), - "eta": None, - "kwargs": {"force_logout": True}, - }, - "sender": "tests.contrib.celery.test_integration.fn_task_parameters", - "exchange": "celery", - "routing_key": "celery", - "retry_policy": None, - "headers": {}, - "properties": {}, - } - - task_id = utils.retrieve_task_id_from_message(context) - self.assertEqual(task_id, "dffcaec1-dd92-4a1a-b3ab-d6512f4beeb7") - - def test_task_id_from_protocol_v2(self): - # ensures a `task_id` is properly returned when Protocol v2 is used. - # `context` is an example of an emitted Signal with Protocol v2 - context = { - "body": ( - ["user"], - {"force_logout": True}, - { - u"chord": None, - u"callbacks": None, - u"errbacks": None, - u"chain": None, - }, - ), - "sender": u"tests.contrib.celery.test_integration.fn_task_parameters", - "exchange": u"", - "routing_key": u"celery", - "retry_policy": None, - "headers": { - u"origin": u"gen83744@hostname", - u"root_id": "7e917b83-4018-431d-9832-73a28e1fb6c0", - u"expires": None, - u"shadow": None, - u"id": "7e917b83-4018-431d-9832-73a28e1fb6c0", - u"kwargsrepr": u"{'force_logout': True}", - u"lang": u"py", - u"retries": 0, - u"task": u"tests.contrib.celery.test_integration.fn_task_parameters", - u"group": None, - u"timelimit": [None, None], - u"parent_id": None, - u"argsrepr": u"['user']", - u"eta": None, - }, - "properties": { - u"reply_to": "c3054a07-5b28-3855-b18c-1623a24aaeca", - u"correlation_id": "7e917b83-4018-431d-9832-73a28e1fb6c0", - }, - } - - task_id = utils.retrieve_task_id_from_message(context) - self.assertEqual(task_id, "7e917b83-4018-431d-9832-73a28e1fb6c0") diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md deleted file mode 100644 index e110055da1..0000000000 --- a/instrumentation/opentelemetry-instrumentation-dbapi/CHANGELOG.md +++ /dev/null @@ -1,31 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- bugfix: cursors and connections now produce spans when used with context managers - ([#1028](https://github.com/open-telemetry/opentelemetry-python/pull/1028)) -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-dbapi - ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - -## 0.7b1 - -Released 2020-05-12 - -- Implement instrument_connection and uninstrument_connection ([#624](https://github.com/open-telemetry/opentelemetry-python/pull/624)) - -## 0.4a0 - -Released 2020-02-21 - -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/LICENSE b/instrumentation/opentelemetry-instrumentation-dbapi/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-dbapi/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-dbapi/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-dbapi/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/README.rst b/instrumentation/opentelemetry-instrumentation-dbapi/README.rst deleted file mode 100644 index 5137a1c1ff..0000000000 --- a/instrumentation/opentelemetry-instrumentation-dbapi/README.rst +++ /dev/null @@ -1,21 +0,0 @@ -OpenTelemetry Database API instrumentation -========================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-dbapi.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-dbapi/ - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-dbapi - - -References ----------- - -* `OpenTelemetry Database API Instrumentation `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg deleted file mode 100644 index 7c4daa4029..0000000000 --- a/instrumentation/opentelemetry-instrumentation-dbapi/setup.cfg +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-dbapi -description = OpenTelemetry Database API instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-dbapi -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - wrapt >= 1.0.0, < 2.0.0 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/setup.py b/instrumentation/opentelemetry-instrumentation-dbapi/setup.py deleted file mode 100644 index cfe98f3895..0000000000 --- a/instrumentation/opentelemetry-instrumentation-dbapi/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "dbapi", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py deleted file mode 100644 index 0047ab1851..0000000000 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ /dev/null @@ -1,385 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The trace integration with Database API supports libraries that follow the -Python Database API Specification v2.0. -``_ - -Usage ------ - -.. code-block:: python - - import mysql.connector - import pyodbc - - from opentelemetry import trace - from opentelemetry.instrumentation.dbapi import trace_integration - from opentelemetry.trace import TracerProvider - - trace.set_tracer_provider(TracerProvider()) - - # Ex: mysql.connector - trace_integration(mysql.connector, "connect", "mysql", "sql") - # Ex: pyodbc - trace_integration(pyodbc, "Connection", "odbc", "sql") - -API ---- -""" - -import functools -import logging -import typing - -import wrapt - -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.dbapi.version import __version__ -from opentelemetry.instrumentation.utils import unwrap -from opentelemetry.trace import SpanKind, TracerProvider, get_tracer -from opentelemetry.trace.status import Status, StatusCode - -logger = logging.getLogger(__name__) - - -def trace_integration( - connect_module: typing.Callable[..., typing.Any], - connect_method_name: str, - database_component: str, - database_type: str = "", - connection_attributes: typing.Dict = None, - tracer_provider: typing.Optional[TracerProvider] = None, -): - """Integrate with DB API library. - https://www.python.org/dev/peps/pep-0249/ - - Args: - connect_module: Module name where connect method is available. - connect_method_name: The connect method name. - database_component: Database driver name or database name "JDBI", - "jdbc", "odbc", "postgreSQL". - database_type: The Database type. For any SQL database, "sql". - connection_attributes: Attribute names for database, port, host and - user in Connection object. - tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to - use. If ommited the current configured one is used. - """ - wrap_connect( - __name__, - connect_module, - connect_method_name, - database_component, - database_type, - connection_attributes, - version=__version__, - tracer_provider=tracer_provider, - ) - - -def wrap_connect( - name: str, - connect_module: typing.Callable[..., typing.Any], - connect_method_name: str, - database_component: str, - database_type: str = "", - connection_attributes: typing.Dict = None, - version: str = "", - tracer_provider: typing.Optional[TracerProvider] = None, -): - """Integrate with DB API library. - https://www.python.org/dev/peps/pep-0249/ - - Args: - tracer: The :class:`opentelemetry.trace.Tracer` to use. - connect_module: Module name where connect method is available. - connect_method_name: The connect method name. - database_component: Database driver name or database name "JDBI", - "jdbc", "odbc", "postgreSQL". - database_type: The Database type. For any SQL database, "sql". - connection_attributes: Attribute names for database, port, host and - user in Connection object. - """ - - # pylint: disable=unused-argument - def wrap_connect_( - wrapped: typing.Callable[..., typing.Any], - instance: typing.Any, - args: typing.Tuple[typing.Any, typing.Any], - kwargs: typing.Dict[typing.Any, typing.Any], - ): - db_integration = DatabaseApiIntegration( - name, - database_component, - database_type=database_type, - connection_attributes=connection_attributes, - version=version, - tracer_provider=tracer_provider, - ) - return db_integration.wrapped_connection(wrapped, args, kwargs) - - try: - wrapt.wrap_function_wrapper( - connect_module, connect_method_name, wrap_connect_ - ) - except Exception as ex: # pylint: disable=broad-except - logger.warning("Failed to integrate with DB API. %s", str(ex)) - - -def unwrap_connect( - connect_module: typing.Callable[..., typing.Any], connect_method_name: str, -): - """Disable integration with DB API library. - https://www.python.org/dev/peps/pep-0249/ - - Args: - connect_module: Module name where the connect method is available. - connect_method_name: The connect method name. - """ - unwrap(connect_module, connect_method_name) - - -def instrument_connection( - name: str, - connection, - database_component: str, - database_type: str = "", - connection_attributes: typing.Dict = None, - version: str = "", - tracer_provider: typing.Optional[TracerProvider] = None, -): - """Enable instrumentation in a database connection. - - Args: - tracer: The :class:`opentelemetry.trace.Tracer` to use. - connection: The connection to instrument. - database_component: Database driver name or database name "JDBI", - "jdbc", "odbc", "postgreSQL". - database_type: The Database type. For any SQL database, "sql". - connection_attributes: Attribute names for database, port, host and - user in a connection object. - - Returns: - An instrumented connection. - """ - db_integration = DatabaseApiIntegration( - name, - database_component, - database_type, - connection_attributes=connection_attributes, - version=version, - tracer_provider=tracer_provider, - ) - db_integration.get_connection_attributes(connection) - return get_traced_connection_proxy(connection, db_integration) - - -def uninstrument_connection(connection): - """Disable instrumentation in a database connection. - - Args: - connection: The connection to uninstrument. - - Returns: - An uninstrumented connection. - """ - if isinstance(connection, wrapt.ObjectProxy): - return connection.__wrapped__ - - logger.warning("Connection is not instrumented") - return connection - - -class DatabaseApiIntegration: - def __init__( - self, - name: str, - database_component: str, - database_type: str = "sql", - connection_attributes=None, - version: str = "", - tracer_provider: typing.Optional[TracerProvider] = None, - ): - self.connection_attributes = connection_attributes - if self.connection_attributes is None: - self.connection_attributes = { - "database": "database", - "port": "port", - "host": "host", - "user": "user", - } - self._name = name - self._version = version - self._tracer_provider = tracer_provider - self.database_component = database_component - self.database_type = database_type - self.connection_props = {} - self.span_attributes = {} - self.name = "" - self.database = "" - - def get_tracer(self): - return get_tracer( - self._name, - instrumenting_library_version=self._version, - tracer_provider=self._tracer_provider, - ) - - def wrapped_connection( - self, - connect_method: typing.Callable[..., typing.Any], - args: typing.Tuple[typing.Any, typing.Any], - kwargs: typing.Dict[typing.Any, typing.Any], - ): - """Add object proxy to connection object. - """ - connection = connect_method(*args, **kwargs) - self.get_connection_attributes(connection) - return get_traced_connection_proxy(connection, self) - - def get_connection_attributes(self, connection): - # Populate span fields using connection - for key, value in self.connection_attributes.items(): - # Allow attributes nested in connection object - attribute = functools.reduce( - lambda attribute, attribute_value: getattr( - attribute, attribute_value, None - ), - value.split("."), - connection, - ) - if attribute: - self.connection_props[key] = attribute - self.name = self.database_component - self.database = self.connection_props.get("database", "") - if self.database: - # PyMySQL encodes names with utf-8 - if hasattr(self.database, "decode"): - self.database = self.database.decode(errors="ignore") - self.name += "." + self.database - user = self.connection_props.get("user") - if user is not None: - self.span_attributes["db.user"] = str(user) - host = self.connection_props.get("host") - if host is not None: - self.span_attributes["net.peer.name"] = host - port = self.connection_props.get("port") - if port is not None: - self.span_attributes["net.peer.port"] = port - - -def get_traced_connection_proxy( - connection, db_api_integration, *args, **kwargs -): - # pylint: disable=abstract-method - class TracedConnectionProxy(wrapt.ObjectProxy): - # pylint: disable=unused-argument - def __init__(self, connection, *args, **kwargs): - wrapt.ObjectProxy.__init__(self, connection) - - def cursor(self, *args, **kwargs): - return get_traced_cursor_proxy( - self.__wrapped__.cursor(*args, **kwargs), db_api_integration - ) - - def __enter__(self): - self.__wrapped__.__enter__() - return self - - def __exit__(self, *args, **kwargs): - self.__wrapped__.__exit__(*args, **kwargs) - - return TracedConnectionProxy(connection, *args, **kwargs) - - -class TracedCursor: - def __init__(self, db_api_integration: DatabaseApiIntegration): - self._db_api_integration = db_api_integration - - def _populate_span( - self, span: trace_api.Span, *args: typing.Tuple[typing.Any, typing.Any] - ): - if not span.is_recording(): - return - statement = args[0] if args else "" - span.set_attribute( - "component", self._db_api_integration.database_component - ) - span.set_attribute("db.type", self._db_api_integration.database_type) - span.set_attribute("db.instance", self._db_api_integration.database) - span.set_attribute("db.statement", statement) - - for ( - attribute_key, - attribute_value, - ) in self._db_api_integration.span_attributes.items(): - span.set_attribute(attribute_key, attribute_value) - - if len(args) > 1: - span.set_attribute("db.statement.parameters", str(args[1])) - - def traced_execution( - self, - query_method: typing.Callable[..., typing.Any], - *args: typing.Tuple[typing.Any, typing.Any], - **kwargs: typing.Dict[typing.Any, typing.Any] - ): - - with self._db_api_integration.get_tracer().start_as_current_span( - self._db_api_integration.name, kind=SpanKind.CLIENT - ) as span: - self._populate_span(span, *args) - try: - result = query_method(*args, **kwargs) - return result - except Exception as ex: # pylint: disable=broad-except - if span.is_recording(): - span.set_status(Status(StatusCode.ERROR, str(ex))) - raise ex - - -def get_traced_cursor_proxy(cursor, db_api_integration, *args, **kwargs): - _traced_cursor = TracedCursor(db_api_integration) - - # pylint: disable=abstract-method - class TracedCursorProxy(wrapt.ObjectProxy): - - # pylint: disable=unused-argument - def __init__(self, cursor, *args, **kwargs): - wrapt.ObjectProxy.__init__(self, cursor) - - def execute(self, *args, **kwargs): - return _traced_cursor.traced_execution( - self.__wrapped__.execute, *args, **kwargs - ) - - def executemany(self, *args, **kwargs): - return _traced_cursor.traced_execution( - self.__wrapped__.executemany, *args, **kwargs - ) - - def callproc(self, *args, **kwargs): - return _traced_cursor.traced_execution( - self.__wrapped__.callproc, *args, **kwargs - ) - - def __enter__(self): - self.__wrapped__.__enter__() - return self - - def __exit__(self, *args, **kwargs): - self.__wrapped__.__exit__(*args, **kwargs) - - return TracedCursorProxy(cursor, *args, **kwargs) diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py deleted file mode 100644 index f2abb8b6dc..0000000000 --- a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py +++ /dev/null @@ -1,229 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import logging -from unittest import mock - -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation import dbapi -from opentelemetry.test.test_base import TestBase - - -class TestDBApiIntegration(TestBase): - def setUp(self): - super().setUp() - self.tracer = self.tracer_provider.get_tracer(__name__) - - def test_span_succeeded(self): - connection_props = { - "database": "testdatabase", - "server_host": "testhost", - "server_port": 123, - "user": "testuser", - } - connection_attributes = { - "database": "database", - "port": "server_port", - "host": "server_host", - "user": "user", - } - db_integration = dbapi.DatabaseApiIntegration( - self.tracer, "testcomponent", "testtype", connection_attributes - ) - mock_connection = db_integration.wrapped_connection( - mock_connect, {}, connection_props - ) - cursor = mock_connection.cursor() - cursor.execute("Test query", ("param1Value", False)) - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - self.assertEqual(span.name, "testcomponent.testdatabase") - self.assertIs(span.kind, trace_api.SpanKind.CLIENT) - - self.assertEqual(span.attributes["component"], "testcomponent") - self.assertEqual(span.attributes["db.type"], "testtype") - self.assertEqual(span.attributes["db.instance"], "testdatabase") - self.assertEqual(span.attributes["db.statement"], "Test query") - self.assertEqual( - span.attributes["db.statement.parameters"], - "('param1Value', False)", - ) - self.assertEqual(span.attributes["db.user"], "testuser") - self.assertEqual(span.attributes["net.peer.name"], "testhost") - self.assertEqual(span.attributes["net.peer.port"], 123) - self.assertIs( - span.status.status_code, trace_api.status.StatusCode.UNSET, - ) - - def test_span_not_recording(self): - connection_props = { - "database": "testdatabase", - "server_host": "testhost", - "server_port": 123, - "user": "testuser", - } - connection_attributes = { - "database": "database", - "port": "server_port", - "host": "server_host", - "user": "user", - } - mock_tracer = mock.Mock() - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = True - db_integration = dbapi.DatabaseApiIntegration( - mock_tracer, "testcomponent", "testtype", connection_attributes - ) - mock_connection = db_integration.wrapped_connection( - mock_connect, {}, connection_props - ) - cursor = mock_connection.cursor() - cursor.execute("Test query", ("param1Value", False)) - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_span_failed(self): - db_integration = dbapi.DatabaseApiIntegration( - self.tracer, "testcomponent" - ) - mock_connection = db_integration.wrapped_connection( - mock_connect, {}, {} - ) - cursor = mock_connection.cursor() - with self.assertRaises(Exception): - cursor.execute("Test query", throw_exception=True) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - self.assertEqual(span.attributes["db.statement"], "Test query") - self.assertIs( - span.status.status_code, trace_api.status.StatusCode.ERROR, - ) - self.assertEqual(span.status.description, "Test Exception") - - def test_executemany(self): - db_integration = dbapi.DatabaseApiIntegration( - self.tracer, "testcomponent" - ) - mock_connection = db_integration.wrapped_connection( - mock_connect, {}, {} - ) - cursor = mock_connection.cursor() - cursor.executemany("Test query") - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - self.assertEqual(span.attributes["db.statement"], "Test query") - - def test_callproc(self): - db_integration = dbapi.DatabaseApiIntegration( - self.tracer, "testcomponent" - ) - mock_connection = db_integration.wrapped_connection( - mock_connect, {}, {} - ) - cursor = mock_connection.cursor() - cursor.callproc("Test stored procedure") - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - self.assertEqual( - span.attributes["db.statement"], "Test stored procedure" - ) - - @mock.patch("opentelemetry.instrumentation.dbapi") - def test_wrap_connect(self, mock_dbapi): - dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-") - connection = mock_dbapi.connect() - self.assertEqual(mock_dbapi.connect.call_count, 1) - self.assertIsInstance(connection.__wrapped__, mock.Mock) - - @mock.patch("opentelemetry.instrumentation.dbapi") - def test_unwrap_connect(self, mock_dbapi): - dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-") - connection = mock_dbapi.connect() - self.assertEqual(mock_dbapi.connect.call_count, 1) - - dbapi.unwrap_connect(mock_dbapi, "connect") - connection = mock_dbapi.connect() - self.assertEqual(mock_dbapi.connect.call_count, 2) - self.assertIsInstance(connection, mock.Mock) - - def test_instrument_connection(self): - connection = mock.Mock() - # Avoid get_attributes failing because can't concatenate mock - connection.database = "-" - connection2 = dbapi.instrument_connection(self.tracer, connection, "-") - self.assertIs(connection2.__wrapped__, connection) - - def test_uninstrument_connection(self): - connection = mock.Mock() - # Set connection.database to avoid a failure because mock can't - # be concatenated - connection.database = "-" - connection2 = dbapi.instrument_connection(self.tracer, connection, "-") - self.assertIs(connection2.__wrapped__, connection) - - connection3 = dbapi.uninstrument_connection(connection2) - self.assertIs(connection3, connection) - - with self.assertLogs(level=logging.WARNING): - connection4 = dbapi.uninstrument_connection(connection) - self.assertIs(connection4, connection) - - -# pylint: disable=unused-argument -def mock_connect(*args, **kwargs): - database = kwargs.get("database") - server_host = kwargs.get("server_host") - server_port = kwargs.get("server_port") - user = kwargs.get("user") - return MockConnection(database, server_port, server_host, user) - - -class MockConnection: - def __init__(self, database, server_port, server_host, user): - self.database = database - self.server_port = server_port - self.server_host = server_host - self.user = user - - # pylint: disable=no-self-use - def cursor(self): - return MockCursor() - - -class MockCursor: - # pylint: disable=unused-argument, no-self-use - def execute(self, query, params=None, throw_exception=False): - if throw_exception: - raise Exception("Test Exception") - - # pylint: disable=unused-argument, no-self-use - def executemany(self, query, params=None, throw_exception=False): - if throw_exception: - raise Exception("Test Exception") - - # pylint: disable=unused-argument, no-self-use - def callproc(self, query, params=None, throw_exception=False): - if throw_exception: - raise Exception("Test Exception") diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md deleted file mode 100644 index 5876936a0b..0000000000 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ /dev/null @@ -1,60 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.15b0 - -Released 2020-11-02 - -- Django instrumentation is now enabled by default but can be disabled by setting `OTEL_PYTHON_DJANGO_INSTRUMENT` to `False` - ([#1239](https://github.com/open-telemetry/opentelemetry-python/pull/1239)) -- Bugfix use request.path replace request.get_full_path(). It will get correct span name - ([#1309](https://github.com/open-telemetry/opentelemetry-python/pull/1309#)) -- Record span status and http.status_code attribute on exception - ([#1257](https://github.com/open-telemetry/opentelemetry-python/pull/1257)) -- Added capture of http.route - ([#1226](https://github.com/open-telemetry/opentelemetry-python/issues/1226)) -- Add support for tracking http metrics - ([#1230](https://github.com/open-telemetry/opentelemetry-python/pull/1230)) - -## Version 0.14b0 - -Released 2020-10-13 - -- Changed span name extraction from request to comply semantic convention ([#992](https://github.com/open-telemetry/opentelemetry-python/pull/992)) -- Added support for `OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS` ([#1154](https://github.com/open-telemetry/opentelemetry-python/pull/1154)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-django - ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) -- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - -## Version 0.11b0 - -Released 2020-07-28 - -- Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) - -## 0.8b0 - -Released 2020-05-27 - -- Add exclude list for paths and hosts to prevent from tracing - ([#670](https://github.com/open-telemetry/opentelemetry-python/pull/670)) -- Add support for django >= 1.10 (#717) - -## 0.7b1 - -Released 2020-05-12 - -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-django/LICENSE b/instrumentation/opentelemetry-instrumentation-django/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-django/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-django/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-django/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-django/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-django/README.rst b/instrumentation/opentelemetry-instrumentation-django/README.rst deleted file mode 100644 index a2b98cabf4..0000000000 --- a/instrumentation/opentelemetry-instrumentation-django/README.rst +++ /dev/null @@ -1,53 +0,0 @@ -OpenTelemetry Django Tracing -============================ - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-django.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-django/ - -This library allows tracing requests for Django applications. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-django - -Configuration -------------- - -Exclude lists -************* -To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_DJANGO_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. - -For example, - -:: - - export OTEL_PYTHON_DJANGO_EXCLUDED_URLS="client/.*/info,healthcheck" - -will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. - -Request attributes -******************** -To extract certain attributes from Django's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS`` to a comma -delimited list of request attribute names. - -For example, - -:: - - export OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS='path_info,content_type' - -will extract path_info and content_type attributes from every traced request and add them as span attritbues. - -Django Request object reference: https://docs.djangoproject.com/en/3.1/ref/request-response/#attributes - -References ----------- - -* `Django `_ -* `OpenTelemetry Instrumentation for Django `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-django/setup.cfg b/instrumentation/opentelemetry-instrumentation-django/setup.cfg deleted file mode 100644 index 44a921283d..0000000000 --- a/instrumentation/opentelemetry-instrumentation-django/setup.cfg +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-django -description = OpenTelemetry Instrumentation for Django -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-django -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - django >= 1.10 - opentelemetry-instrumentation-wsgi == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - opentelemetry-api == 0.16.dev0 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - django = opentelemetry.instrumentation.django:DjangoInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-django/setup.py b/instrumentation/opentelemetry-instrumentation-django/setup.py deleted file mode 100644 index fb9d615ce3..0000000000 --- a/instrumentation/opentelemetry-instrumentation-django/setup.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from os.path import dirname, join - -from setuptools import setup - -PACKAGE_INFO = {} -with open( - join( - dirname(__file__), - "src", - "opentelemetry", - "instrumentation", - "django", - "version.py", - ) -) as f: - exec(f.read(), PACKAGE_INFO) - -setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py deleted file mode 100644 index 26e21a1f7f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from logging import getLogger - -from django.conf import settings - -from opentelemetry.configuration import Configuration -from opentelemetry.instrumentation.django.middleware import _DjangoMiddleware -from opentelemetry.instrumentation.django.version import __version__ -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.metric import ( - HTTPMetricRecorder, - HTTPMetricType, - MetricMixin, -) - -_logger = getLogger(__name__) - - -class DjangoInstrumentor(BaseInstrumentor, MetricMixin): - """An instrumentor for Django - - See `BaseInstrumentor` - """ - - _opentelemetry_middleware = ".".join( - [_DjangoMiddleware.__module__, _DjangoMiddleware.__qualname__] - ) - - def _instrument(self, **kwargs): - - # FIXME this is probably a pattern that will show up in the rest of the - # ext. Find a better way of implementing this. - # FIXME Probably the evaluation of strings into boolean values can be - # built inside the Configuration class itself with the magic method - # __bool__ - - if Configuration().DJANGO_INSTRUMENT is False: - return - - # This can not be solved, but is an inherent problem of this approach: - # the order of middleware entries matters, and here you have no control - # on that: - # https://docs.djangoproject.com/en/3.0/topics/http/middleware/#activating-middleware - # https://docs.djangoproject.com/en/3.0/ref/middleware/#middleware-ordering - - settings_middleware = getattr(settings, "MIDDLEWARE", []) - # Django allows to specify middlewares as a tuple, so we convert this tuple to a - # list, otherwise we wouldn't be able to call append/remove - if isinstance(settings_middleware, tuple): - settings_middleware = list(settings_middleware) - - settings_middleware.insert(0, self._opentelemetry_middleware) - self.init_metrics( - __name__, __version__, - ) - metric_recorder = HTTPMetricRecorder(self.meter, HTTPMetricType.SERVER) - setattr(settings, "OTEL_METRIC_RECORDER", metric_recorder) - setattr(settings, "MIDDLEWARE", settings_middleware) - - def _uninstrument(self, **kwargs): - settings_middleware = getattr(settings, "MIDDLEWARE", None) - - # FIXME This is starting to smell like trouble. We have 2 mechanisms - # that may make this condition be True, one implemented in - # BaseInstrumentor and another one implemented in _instrument. Both - # stop _instrument from running and thus, settings_middleware not being - # set. - if settings_middleware is None or ( - self._opentelemetry_middleware not in settings_middleware - ): - return - - settings_middleware.remove(self._opentelemetry_middleware) - setattr(settings, "MIDDLEWARE", settings_middleware) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py deleted file mode 100644 index 1f465ca57a..0000000000 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ /dev/null @@ -1,232 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time -from logging import getLogger - -from django.conf import settings - -from opentelemetry.configuration import Configuration -from opentelemetry.context import attach, detach -from opentelemetry.instrumentation.django.version import __version__ -from opentelemetry.instrumentation.utils import extract_attributes_from_object -from opentelemetry.instrumentation.wsgi import ( - add_response_attributes, - carrier_getter, - collect_request_attributes, -) -from opentelemetry.propagators import extract -from opentelemetry.trace import SpanKind, get_tracer -from opentelemetry.util import ExcludeList - -try: - from django.core.urlresolvers import ( # pylint: disable=no-name-in-module - resolve, - Resolver404, - ) -except ImportError: - from django.urls import resolve, Resolver404 - -try: - from django.utils.deprecation import MiddlewareMixin -except ImportError: - MiddlewareMixin = object - -_logger = getLogger(__name__) -_attributes_by_preference = [ - ["http.scheme", "http.host", "http.target"], - ["http.scheme", "http.server_name", "net.host.port", "http.target"], - ["http.scheme", "net.host.name", "net.host.port", "http.target"], - ["http.url"], -] - - -class _DjangoMiddleware(MiddlewareMixin): - """Django Middleware for OpenTelemetry""" - - _environ_activation_key = ( - "opentelemetry-instrumentor-django.activation_key" - ) - _environ_token = "opentelemetry-instrumentor-django.token" - _environ_span_key = "opentelemetry-instrumentor-django.span_key" - _environ_exception_key = "opentelemetry-instrumentor-django.exception_key" - - _excluded_urls = Configuration().DJANGO_EXCLUDED_URLS or [] - if _excluded_urls: - _excluded_urls = ExcludeList(str.split(_excluded_urls, ",")) - else: - _excluded_urls = ExcludeList(_excluded_urls) - - _traced_request_attrs = [ - attr.strip() - for attr in (Configuration().DJANGO_TRACED_REQUEST_ATTRS or "").split( - "," - ) - ] - - @staticmethod - def _get_span_name(request): - try: - if getattr(request, "resolver_match"): - match = request.resolver_match - else: - match = resolve(request.path) - - if hasattr(match, "route"): - return match.route - - # Instead of using `view_name`, better to use `_func_name` as some applications can use similar - # view names in different modules - if hasattr(match, "_func_name"): - return match._func_name # pylint: disable=protected-access - - # Fallback for safety as `_func_name` private field - return match.view_name - - except Resolver404: - return "HTTP {}".format(request.method) - - @staticmethod - def _get_metric_labels_from_attributes(attributes): - labels = {} - labels["http.method"] = attributes.get("http.method", "") - for attrs in _attributes_by_preference: - labels_from_attributes = { - attr: attributes.get(attr, None) for attr in attrs - } - if set(attrs).issubset(attributes.keys()): - labels.update(labels_from_attributes) - break - if attributes.get("http.flavor"): - labels["http.flavor"] = attributes.get("http.flavor") - return labels - - def process_request(self, request): - # request.META is a dictionary containing all available HTTP headers - # Read more about request.META here: - # https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpRequest.META - - if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): - return - - # pylint:disable=W0212 - request._otel_start_time = time.time() - - environ = request.META - - token = attach(extract(carrier_getter, environ)) - - tracer = get_tracer(__name__, __version__) - - span = tracer.start_span( - self._get_span_name(request), - kind=SpanKind.SERVER, - start_time=environ.get( - "opentelemetry-instrumentor-django.starttime_key" - ), - ) - - attributes = collect_request_attributes(environ) - # pylint:disable=W0212 - request._otel_labels = self._get_metric_labels_from_attributes( - attributes - ) - - if span.is_recording(): - attributes = extract_attributes_from_object( - request, self._traced_request_attrs, attributes - ) - for key, value in attributes.items(): - span.set_attribute(key, value) - - activation = tracer.use_span(span, end_on_exit=True) - activation.__enter__() - - request.META[self._environ_activation_key] = activation - request.META[self._environ_span_key] = span - request.META[self._environ_token] = token - - # pylint: disable=unused-argument - def process_view(self, request, view_func, *args, **kwargs): - # Process view is executed before the view function, here we get the - # route template from request.resolver_match. It is not set yet in process_request - if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): - return - - if ( - self._environ_activation_key in request.META.keys() - and self._environ_span_key in request.META.keys() - ): - span = request.META[self._environ_span_key] - - if span.is_recording(): - match = getattr(request, "resolver_match") - if match: - route = getattr(match, "route") - if route: - span.set_attribute("http.route", route) - - def process_exception(self, request, exception): - if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): - return - - if self._environ_activation_key in request.META.keys(): - request.META[self._environ_exception_key] = exception - - def process_response(self, request, response): - if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): - return response - - if ( - self._environ_activation_key in request.META.keys() - and self._environ_span_key in request.META.keys() - ): - add_response_attributes( - request.META[self._environ_span_key], - "{} {}".format(response.status_code, response.reason_phrase), - response, - ) - # pylint:disable=W0212 - request._otel_labels["http.status_code"] = str( - response.status_code - ) - request.META.pop(self._environ_span_key) - - exception = request.META.pop(self._environ_exception_key, None) - if exception: - request.META[self._environ_activation_key].__exit__( - type(exception), - exception, - getattr(exception, "__traceback__", None), - ) - else: - request.META[self._environ_activation_key].__exit__( - None, None, None - ) - request.META.pop(self._environ_activation_key) - - if self._environ_token in request.META.keys(): - detach(request.environ.get(self._environ_token)) - request.META.pop(self._environ_token) - - try: - metric_recorder = getattr(settings, "OTEL_METRIC_RECORDER", None) - if metric_recorder is not None: - # pylint:disable=W0212 - metric_recorder.record_server_duration_range( - request._otel_start_time, time.time(), request._otel_labels - ) - except Exception as ex: # pylint: disable=W0703 - _logger.warning("Error recording duration metrics: %s", ex) - return response diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-django/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py deleted file mode 100644 index 3f70f62bec..0000000000 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ /dev/null @@ -1,315 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from sys import modules -from unittest.mock import Mock, patch - -from django import VERSION -from django.conf import settings -from django.conf.urls import url -from django.test import Client -from django.test.utils import setup_test_environment, teardown_test_environment - -from opentelemetry.configuration import Configuration -from opentelemetry.instrumentation.django import DjangoInstrumentor -from opentelemetry.sdk.util import get_dict_as_key -from opentelemetry.test.test_base import TestBase -from opentelemetry.test.wsgitestutil import WsgiTestBase -from opentelemetry.trace import SpanKind -from opentelemetry.trace.status import StatusCode -from opentelemetry.util import ExcludeList - -# pylint: disable=import-error -from .views import ( - error, - excluded, - excluded_noarg, - excluded_noarg2, - route_span_name, - traced, - traced_template, -) - -DJANGO_2_2 = VERSION >= (2, 2) - -urlpatterns = [ - url(r"^traced/", traced), - url(r"^route/(?P[0-9]{4})/template/$", traced_template), - url(r"^error/", error), - url(r"^excluded_arg/", excluded), - url(r"^excluded_noarg/", excluded_noarg), - url(r"^excluded_noarg2/", excluded_noarg2), - url(r"^span_name/([0-9]{4})/$", route_span_name), -] -_django_instrumentor = DjangoInstrumentor() - - -class TestMiddleware(TestBase, WsgiTestBase): - @classmethod - def setUpClass(cls): - super().setUpClass() - settings.configure(ROOT_URLCONF=modules[__name__]) - - def setUp(self): - super().setUp() - setup_test_environment() - _django_instrumentor.instrument() - Configuration._reset() # pylint: disable=protected-access - - def tearDown(self): - super().tearDown() - teardown_test_environment() - _django_instrumentor.uninstrument() - - def test_templated_route_get(self): - Client().get("/route/2020/template/") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - span = spans[0] - - self.assertEqual( - span.name, - "^route/(?P[0-9]{4})/template/$" - if DJANGO_2_2 - else "tests.views.traced", - ) - self.assertEqual(span.kind, SpanKind.SERVER) - self.assertEqual(span.status.status_code, StatusCode.UNSET) - self.assertEqual(span.attributes["http.method"], "GET") - self.assertEqual( - span.attributes["http.url"], - "http://testserver/route/2020/template/", - ) - self.assertEqual( - span.attributes["http.route"], - "^route/(?P[0-9]{4})/template/$", - ) - self.assertEqual(span.attributes["http.scheme"], "http") - self.assertEqual(span.attributes["http.status_code"], 200) - self.assertEqual(span.attributes["http.status_text"], "OK") - - def test_traced_get(self): - Client().get("/traced/") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - span = spans[0] - - self.assertEqual( - span.name, "^traced/" if DJANGO_2_2 else "tests.views.traced" - ) - self.assertEqual(span.kind, SpanKind.SERVER) - self.assertEqual(span.status.status_code, StatusCode.UNSET) - self.assertEqual(span.attributes["http.method"], "GET") - self.assertEqual( - span.attributes["http.url"], "http://testserver/traced/" - ) - self.assertEqual(span.attributes["http.route"], "^traced/") - self.assertEqual(span.attributes["http.scheme"], "http") - self.assertEqual(span.attributes["http.status_code"], 200) - self.assertEqual(span.attributes["http.status_text"], "OK") - - self.assertIsNotNone(_django_instrumentor.meter) - self.assertEqual(len(_django_instrumentor.meter.metrics), 1) - recorder = _django_instrumentor.meter.metrics.pop() - match_key = get_dict_as_key( - { - "http.flavor": "1.1", - "http.method": "GET", - "http.status_code": "200", - "http.url": "http://testserver/traced/", - } - ) - for key in recorder.bound_instruments.keys(): - self.assertEqual(key, match_key) - # pylint: disable=protected-access - bound = recorder.bound_instruments.get(key) - for view_data in bound.view_datas: - self.assertEqual(view_data.labels, key) - self.assertEqual(view_data.aggregator.current.count, 1) - self.assertGreaterEqual(view_data.aggregator.current.sum, 0) - - def test_not_recording(self): - mock_tracer = Mock() - mock_span = Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = True - with patch("opentelemetry.trace.get_tracer") as tracer: - tracer.return_value = mock_tracer - Client().get("/traced/") - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_traced_post(self): - Client().post("/traced/") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - span = spans[0] - - self.assertEqual( - span.name, "^traced/" if DJANGO_2_2 else "tests.views.traced" - ) - self.assertEqual(span.kind, SpanKind.SERVER) - self.assertEqual(span.status.status_code, StatusCode.UNSET) - self.assertEqual(span.attributes["http.method"], "POST") - self.assertEqual( - span.attributes["http.url"], "http://testserver/traced/" - ) - self.assertEqual(span.attributes["http.route"], "^traced/") - self.assertEqual(span.attributes["http.scheme"], "http") - self.assertEqual(span.attributes["http.status_code"], 200) - self.assertEqual(span.attributes["http.status_text"], "OK") - - def test_error(self): - with self.assertRaises(ValueError): - Client().get("/error/") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - span = spans[0] - - self.assertEqual( - span.name, "^error/" if DJANGO_2_2 else "tests.views.error" - ) - self.assertEqual(span.kind, SpanKind.SERVER) - self.assertEqual(span.status.status_code, StatusCode.ERROR) - self.assertEqual(span.attributes["http.method"], "GET") - self.assertEqual( - span.attributes["http.url"], "http://testserver/error/" - ) - self.assertEqual(span.attributes["http.route"], "^error/") - self.assertEqual(span.attributes["http.scheme"], "http") - self.assertEqual(span.attributes["http.status_code"], 500) - self.assertIsNotNone(_django_instrumentor.meter) - self.assertEqual(len(_django_instrumentor.meter.metrics), 1) - - self.assertEqual(len(span.events), 1) - event = span.events[0] - self.assertEqual(event.name, "exception") - self.assertEqual(event.attributes["exception.type"], "ValueError") - self.assertEqual(event.attributes["exception.message"], "error") - - recorder = _django_instrumentor.meter.metrics.pop() - match_key = get_dict_as_key( - { - "http.flavor": "1.1", - "http.method": "GET", - "http.status_code": "500", - "http.url": "http://testserver/error/", - } - ) - for key in recorder.bound_instruments.keys(): - self.assertEqual(key, match_key) - # pylint: disable=protected-access - bound = recorder.bound_instruments.get(key) - for view_data in bound.view_datas: - self.assertEqual(view_data.labels, key) - self.assertEqual(view_data.aggregator.current.count, 1) - - @patch( - "opentelemetry.instrumentation.django.middleware._DjangoMiddleware._excluded_urls", - ExcludeList(["http://testserver/excluded_arg/123", "excluded_noarg"]), - ) - def test_exclude_lists(self): - client = Client() - client.get("/excluded_arg/123") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) - - client.get("/excluded_arg/125") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - client.get("/excluded_noarg/") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - client.get("/excluded_noarg2/") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - def test_span_name(self): - # test no query_string - Client().get("/span_name/1234/") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - span = span_list[0] - self.assertEqual( - span.name, - "^span_name/([0-9]{4})/$" - if DJANGO_2_2 - else "tests.views.route_span_name", - ) - - def test_span_name_for_query_string(self): - """ - request not have query string - """ - Client().get("/span_name/1234/?query=test") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - span = span_list[0] - self.assertEqual( - span.name, - "^span_name/([0-9]{4})/$" - if DJANGO_2_2 - else "tests.views.route_span_name", - ) - - def test_span_name_404(self): - Client().get("/span_name/1234567890/") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - span = span_list[0] - self.assertEqual(span.name, "HTTP GET") - - def test_traced_request_attrs(self): - with patch( - "opentelemetry.instrumentation.django.middleware._DjangoMiddleware._traced_request_attrs", - [], - ): - Client().get("/span_name/1234/", CONTENT_TYPE="test/ct") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - span = span_list[0] - self.assertNotIn("path_info", span.attributes) - self.assertNotIn("content_type", span.attributes) - self.memory_exporter.clear() - - with patch( - "opentelemetry.instrumentation.django.middleware._DjangoMiddleware._traced_request_attrs", - ["path_info", "content_type", "non_existing_variable"], - ): - Client().get("/span_name/1234/", CONTENT_TYPE="test/ct") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - span = span_list[0] - self.assertEqual(span.attributes["path_info"], "/span_name/1234/") - self.assertEqual(span.attributes["content_type"], "test/ct") - self.assertNotIn("non_existing_variable", span.attributes) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py deleted file mode 100644 index 872222a842..0000000000 --- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.http import HttpResponse - - -def traced(request): # pylint: disable=unused-argument - return HttpResponse() - - -def traced_template(request, year): # pylint: disable=unused-argument - return HttpResponse() - - -def error(request): # pylint: disable=unused-argument - raise ValueError("error") - - -def excluded(request): # pylint: disable=unused-argument - return HttpResponse() - - -def excluded_noarg(request): # pylint: disable=unused-argument - return HttpResponse() - - -def excluded_noarg2(request): # pylint: disable=unused-argument - return HttpResponse() - - -def route_span_name( - request, *args, **kwargs -): # pylint: disable=unused-argument - return HttpResponse() diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md deleted file mode 100644 index f07f514023..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md +++ /dev/null @@ -1,24 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) -- Change package name to opentelemetry-instrumentation-elasticsearch - ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - -## Version 0.10b0 - -Released 2020-06-23 - -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/LICENSE b/instrumentation/opentelemetry-instrumentation-elasticsearch/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-elasticsearch/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/README.rst b/instrumentation/opentelemetry-instrumentation-elasticsearch/README.rst deleted file mode 100644 index 9f898e7835..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/README.rst +++ /dev/null @@ -1,23 +0,0 @@ -OpenTelemetry elasticsearch Integration -======================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-elasticsearch.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-elasticsearch/ - -This library allows tracing elasticsearch made by the -`elasticsearch `_ library. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-elasticsearch - -References ----------- - -* `OpenTelemetry elasticsearch Integration `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg deleted file mode 100644 index b1ebcfe76a..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-elasticsearch -description = OpenTelemetry elasticsearch instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-elasticsearch -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - wrapt >= 1.0.0, < 2.0.0 - elasticsearch >= 2.0 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - elasticsearch-dsl >= 2.0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - elasticsearch = opentelemetry.instrumentation.elasticsearch:ElasticsearchInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.py deleted file mode 100644 index cd7a7f1012..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "elasticsearch", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py deleted file mode 100644 index 541cdbfa6e..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This library allows tracing HTTP elasticsearch made by the -`elasticsearch `_ library. - -Usage ------ - -.. code-block:: python - - from opentelemetry import trace - from opentelemetry.instrumentation.elasticsearch import ElasticSearchInstrumentor - from opentelemetry.sdk.trace import TracerProvider - import elasticsearch - - trace.set_tracer_provider(TracerProvider()) - - # instrument elasticsearch - ElasticSearchInstrumentor().instrument(tracer_provider=trace.get_tracer_provider()) - - # Using elasticsearch as normal now will automatically generate spans - es = elasticsearch.Elasticsearch() - es.index(index='my-index', doc_type='my-type', id=1, body={'my': 'data', 'timestamp': datetime.now()}) - es.get(index='my-index', doc_type='my-type', id=1) - -API ---- - -Elasticsearch instrumentation prefixes operation names with the string "Elasticsearch". This -can be changed to a different string by either setting the `OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX` -environment variable or by passing the prefix as an argument to the instrumentor. For example, - - -.. code-block:: python - - ElasticSearchInstrumentor("my-custom-prefix").instrument() -""" - -import functools -import types -from logging import getLogger -from os import environ - -import elasticsearch -import elasticsearch.exceptions -from wrapt import ObjectProxy -from wrapt import wrap_function_wrapper as _wrap - -from opentelemetry import context, propagators, trace -from opentelemetry.instrumentation.elasticsearch.version import __version__ -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.utils import unwrap -from opentelemetry.trace import SpanKind, get_tracer -from opentelemetry.trace.status import Status, StatusCode - -logger = getLogger(__name__) - - -# Values to add as tags from the actual -# payload returned by Elasticsearch, if any. -_ATTRIBUTES_FROM_RESULT = [ - "found", - "timed_out", - "took", -] - -_DEFALT_OP_NAME = "request" - - -class ElasticsearchInstrumentor(BaseInstrumentor): - """An instrumentor for elasticsearch - See `BaseInstrumentor` - """ - - def __init__(self, span_name_prefix=None): - if not span_name_prefix: - span_name_prefix = environ.get( - "OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX", "Elasticsearch", - ) - self._span_name_prefix = span_name_prefix.strip() - super().__init__() - - def _instrument(self, **kwargs): - """ - Instruments elasticsarch module - """ - tracer_provider = kwargs.get("tracer_provider") - tracer = get_tracer(__name__, __version__, tracer_provider) - _wrap( - elasticsearch, - "Transport.perform_request", - _wrap_perform_request(tracer, self._span_name_prefix), - ) - - def _uninstrument(self, **kwargs): - unwrap(elasticsearch.Transport, "perform_request") - - -def _wrap_perform_request(tracer, span_name_prefix): - # pylint: disable=R0912 - def wrapper(wrapped, _, args, kwargs): - method = url = None - try: - method, url, *_ = args - except IndexError: - logger.warning( - "expected perform_request to receive two positional arguments. " - "Got %d", - len(args), - ) - - op_name = span_name_prefix + (url or method or _DEFALT_OP_NAME) - params = kwargs.get("params", {}) - body = kwargs.get("body", None) - - with tracer.start_as_current_span( - op_name, kind=SpanKind.CLIENT, - ) as span: - if span.is_recording(): - attributes = { - "component": "elasticsearch-py", - "db.type": "elasticsearch", - } - if url: - attributes["elasticsearch.url"] = url - if method: - attributes["elasticsearch.method"] = method - if body: - attributes["db.statement"] = str(body) - if params: - attributes["elasticsearch.params"] = str(params) - for key, value in attributes.items(): - span.set_attribute(key, value) - try: - rv = wrapped(*args, **kwargs) - if isinstance(rv, dict) and span.is_recording(): - for member in _ATTRIBUTES_FROM_RESULT: - if member in rv: - span.set_attribute( - "elasticsearch.{0}".format(member), - str(rv[member]), - ) - return rv - except Exception as ex: # pylint: disable=broad-except - if span.is_recording(): - span.set_status(Status(StatusCode.ERROR, str(ex))) - raise ex - - return wrapper diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es2.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es2.py deleted file mode 100644 index 008a95d671..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es2.py +++ /dev/null @@ -1,33 +0,0 @@ -from elasticsearch_dsl import ( # pylint: disable=no-name-in-module - DocType, - String, -) - - -class Article(DocType): - title = String(analyzer="snowball", fields={"raw": String()}) - body = String(analyzer="snowball") - - class Meta: - index = "test-index" - - -dsl_create_statement = { - "mappings": { - "article": { - "properties": { - "title": { - "analyzer": "snowball", - "fields": {"raw": {"type": "string"}}, - "type": "string", - }, - "body": {"analyzer": "snowball", "type": "string"}, - } - } - }, - "settings": {"analysis": {}}, -} -dsl_index_result = (1, {}, '{"created": true}') -dsl_index_span_name = "Elasticsearch/test-index/article/2" -dsl_index_url = "/test-index/article/2" -dsl_search_method = "GET" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es5.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es5.py deleted file mode 100644 index cf32d98863..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es5.py +++ /dev/null @@ -1,33 +0,0 @@ -from elasticsearch_dsl import ( # pylint: disable=no-name-in-module - DocType, - Keyword, - Text, -) - - -class Article(DocType): - title = Text(analyzer="snowball", fields={"raw": Keyword()}) - body = Text(analyzer="snowball") - - class Meta: - index = "test-index" - - -dsl_create_statement = { - "mappings": { - "article": { - "properties": { - "title": { - "analyzer": "snowball", - "fields": {"raw": {"type": "keyword"}}, - "type": "text", - }, - "body": {"analyzer": "snowball", "type": "text"}, - } - } - }, -} -dsl_index_result = (1, {}, '{"created": true}') -dsl_index_span_name = "Elasticsearch/test-index/article/2" -dsl_index_url = "/test-index/article/2" -dsl_search_method = "GET" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py deleted file mode 100644 index b27d291ba3..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py +++ /dev/null @@ -1,33 +0,0 @@ -from elasticsearch_dsl import ( # pylint: disable=unused-import - Document, - Keyword, - Text, -) - - -class Article(Document): - title = Text(analyzer="snowball", fields={"raw": Keyword()}) - body = Text(analyzer="snowball") - - class Index: - name = "test-index" - - -dsl_create_statement = { - "mappings": { - "doc": { - "properties": { - "title": { - "analyzer": "snowball", - "fields": {"raw": {"type": "keyword"}}, - "type": "text", - }, - "body": {"analyzer": "snowball", "type": "text"}, - } - } - } -} -dsl_index_result = (1, {}, '{"result": "created"}') -dsl_index_span_name = "Elasticsearch/test-index/doc/2" -dsl_index_url = "/test-index/doc/2" -dsl_search_method = "GET" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py deleted file mode 100644 index a2d37a54a9..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py +++ /dev/null @@ -1,31 +0,0 @@ -from elasticsearch_dsl import ( # pylint: disable=unused-import - Document, - Keyword, - Text, -) - - -class Article(Document): - title = Text(analyzer="snowball", fields={"raw": Keyword()}) - body = Text(analyzer="snowball") - - class Index: - name = "test-index" - - -dsl_create_statement = { - "mappings": { - "properties": { - "title": { - "analyzer": "snowball", - "fields": {"raw": {"type": "keyword"}}, - "type": "text", - }, - "body": {"analyzer": "snowball", "type": "text"}, - } - } -} -dsl_index_result = (1, {}, '{"result": "created"}') -dsl_index_span_name = "Elasticsearch/test-index/_doc/2" -dsl_index_url = "/test-index/_doc/2" -dsl_search_method = "POST" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py deleted file mode 100644 index ea0e6ce2fb..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py +++ /dev/null @@ -1,327 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import threading -from ast import literal_eval -from unittest import mock - -import elasticsearch -import elasticsearch.exceptions -from elasticsearch import Elasticsearch -from elasticsearch_dsl import Search - -import opentelemetry.instrumentation.elasticsearch -from opentelemetry.instrumentation.elasticsearch import ( - ElasticsearchInstrumentor, -) -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace.status import StatusCode - -major_version = elasticsearch.VERSION[0] - -if major_version == 7: - from . import helpers_es7 as helpers # pylint: disable=no-name-in-module -elif major_version == 6: - from . import helpers_es6 as helpers # pylint: disable=no-name-in-module -elif major_version == 5: - from . import helpers_es5 as helpers # pylint: disable=no-name-in-module -else: - from . import helpers_es2 as helpers # pylint: disable=no-name-in-module - - -Article = helpers.Article - - -@mock.patch( - "elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request" -) -class TestElasticsearchIntegration(TestBase): - def setUp(self): - super().setUp() - self.tracer = self.tracer_provider.get_tracer(__name__) - ElasticsearchInstrumentor().instrument() - - def tearDown(self): - super().tearDown() - with self.disable_logging(): - ElasticsearchInstrumentor().uninstrument() - - def get_ordered_finished_spans(self): - return sorted( - self.memory_exporter.get_finished_spans(), - key=lambda s: s.start_time, - ) - - def test_instrumentor(self, request_mock): - request_mock.return_value = (1, {}, {}) - - es = Elasticsearch() - es.index(index="sw", doc_type="people", id=1, body={"name": "adam"}) - - spans_list = self.get_ordered_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - - # Check version and name in span's instrumentation info - # self.check_span_instrumentation_info(span, opentelemetry.instrumentation.elasticsearch) - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.elasticsearch - ) - - # check that no spans are generated after uninstrument - ElasticsearchInstrumentor().uninstrument() - - es.index(index="sw", doc_type="people", id=1, body={"name": "adam"}) - - spans_list = self.get_ordered_finished_spans() - self.assertEqual(len(spans_list), 1) - - def test_span_not_recording(self, request_mock): - request_mock.return_value = (1, {}, {}) - mock_tracer = mock.Mock() - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = mock_span - with mock.patch("opentelemetry.trace.get_tracer") as tracer: - tracer.return_value = mock_tracer - Elasticsearch() - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - ElasticsearchInstrumentor().uninstrument() - - def test_prefix_arg(self, request_mock): - prefix = "prefix-from-env" - ElasticsearchInstrumentor().uninstrument() - ElasticsearchInstrumentor(span_name_prefix=prefix).instrument() - request_mock.return_value = (1, {}, {}) - self._test_prefix(prefix) - - def test_prefix_env(self, request_mock): - prefix = "prefix-from-args" - env_var = "OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX" - os.environ[env_var] = prefix - ElasticsearchInstrumentor().uninstrument() - ElasticsearchInstrumentor().instrument() - request_mock.return_value = (1, {}, {}) - del os.environ[env_var] - self._test_prefix(prefix) - - def _test_prefix(self, prefix): - es = Elasticsearch() - es.index(index="sw", doc_type="people", id=1, body={"name": "adam"}) - - spans_list = self.get_ordered_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - self.assertTrue(span.name.startswith(prefix)) - - def test_result_values(self, request_mock): - request_mock.return_value = ( - 1, - {}, - '{"found": false, "timed_out": true, "took": 7}', - ) - es = Elasticsearch() - es.get(index="test-index", doc_type="tweet", id=1) - - spans = self.get_ordered_finished_spans() - - self.assertEqual(1, len(spans)) - self.assertEqual("False", spans[0].attributes["elasticsearch.found"]) - self.assertEqual( - "True", spans[0].attributes["elasticsearch.timed_out"] - ) - self.assertEqual("7", spans[0].attributes["elasticsearch.took"]) - - def test_trace_error_unknown(self, request_mock): - exc = RuntimeError("custom error") - request_mock.side_effect = exc - self._test_trace_error(StatusCode.ERROR, exc) - - def test_trace_error_not_found(self, request_mock): - msg = "record not found" - exc = elasticsearch.exceptions.NotFoundError(404, msg) - request_mock.return_value = (1, {}, {}) - request_mock.side_effect = exc - self._test_trace_error(StatusCode.ERROR, exc) - - def _test_trace_error(self, code, exc): - es = Elasticsearch() - try: - es.get(index="test-index", doc_type="tweet", id=1) - except Exception: # pylint: disable=broad-except - pass - - spans = self.get_ordered_finished_spans() - self.assertEqual(1, len(spans)) - span = spans[0] - self.assertFalse(span.status.is_ok) - self.assertEqual(span.status.status_code, code) - self.assertEqual(span.status.description, str(exc)) - - def test_parent(self, request_mock): - request_mock.return_value = (1, {}, {}) - es = Elasticsearch() - with self.tracer.start_as_current_span("parent"): - es.index( - index="sw", doc_type="people", id=1, body={"name": "adam"} - ) - - spans = self.get_ordered_finished_spans() - self.assertEqual(len(spans), 2) - - self.assertEqual(spans[0].name, "parent") - self.assertEqual(spans[1].name, "Elasticsearch/sw/people/1") - self.assertIsNotNone(spans[1].parent) - self.assertEqual(spans[1].parent.span_id, spans[0].context.span_id) - - def test_multithread(self, request_mock): - request_mock.return_value = (1, {}, {}) - es = Elasticsearch() - ev = threading.Event() - - # 1. Start tracing from thread-1; make thread-2 wait - # 2. Trace something from thread-2, make thread-1 join before finishing. - # 3. Check the spans got different parents, and are in the expected order. - def target1(parent_span): - with self.tracer.use_span(parent_span): - es.get(index="test-index", doc_type="tweet", id=1) - ev.set() - ev.wait() - - def target2(): - ev.wait() - es.get(index="test-index", doc_type="tweet", id=2) - ev.set() - - with self.tracer.start_as_current_span("parent") as span: - t1 = threading.Thread(target=target1, args=(span,)) - t1.start() - - t2 = threading.Thread(target=target2) - t2.start() - t1.join() - t2.join() - - spans = self.get_ordered_finished_spans() - self.assertEqual(3, len(spans)) - s1, s2, s3 = spans - - self.assertEqual(s1.name, "parent") - - self.assertEqual(s2.name, "Elasticsearch/test-index/tweet/1") - self.assertIsNotNone(s2.parent) - self.assertEqual(s2.parent.span_id, s1.context.span_id) - self.assertEqual(s3.name, "Elasticsearch/test-index/tweet/2") - self.assertIsNone(s3.parent) - - def test_dsl_search(self, request_mock): - request_mock.return_value = (1, {}, '{"hits": {"hits": []}}') - - client = Elasticsearch() - search = Search(using=client, index="test-index").filter( - "term", author="testing" - ) - search.execute() - spans = self.get_ordered_finished_spans() - span = spans[0] - self.assertEqual(1, len(spans)) - self.assertEqual(span.name, "Elasticsearch/test-index/_search") - self.assertIsNotNone(span.end_time) - self.assertEqual( - span.attributes, - { - "component": "elasticsearch-py", - "db.type": "elasticsearch", - "elasticsearch.url": "/test-index/_search", - "elasticsearch.method": helpers.dsl_search_method, - "db.statement": str( - { - "query": { - "bool": { - "filter": [{"term": {"author": "testing"}}] - } - } - } - ), - }, - ) - - def test_dsl_create(self, request_mock): - request_mock.return_value = (1, {}, {}) - client = Elasticsearch() - Article.init(using=client) - - spans = self.get_ordered_finished_spans() - self.assertEqual(2, len(spans)) - span1, span2 = spans - self.assertEqual(span1.name, "Elasticsearch/test-index") - self.assertEqual( - span1.attributes, - { - "component": "elasticsearch-py", - "db.type": "elasticsearch", - "elasticsearch.url": "/test-index", - "elasticsearch.method": "HEAD", - }, - ) - - self.assertEqual(span2.name, "Elasticsearch/test-index") - attributes = { - "component": "elasticsearch-py", - "db.type": "elasticsearch", - "elasticsearch.url": "/test-index", - "elasticsearch.method": "PUT", - } - self.assert_span_has_attributes(span2, attributes) - self.assertEqual( - literal_eval(span2.attributes["db.statement"]), - helpers.dsl_create_statement, - ) - - def test_dsl_index(self, request_mock): - request_mock.return_value = helpers.dsl_index_result - - client = Elasticsearch() - article = Article( - meta={"id": 2}, - title="About searching", - body="A few words here, a few words there", - ) - res = article.save(using=client) - self.assertTrue(res) - spans = self.get_ordered_finished_spans() - self.assertEqual(1, len(spans)) - span = spans[0] - self.assertEqual(span.name, helpers.dsl_index_span_name) - attributes = { - "component": "elasticsearch-py", - "db.type": "elasticsearch", - "elasticsearch.url": helpers.dsl_index_url, - "elasticsearch.method": "PUT", - } - self.assert_span_has_attributes(span, attributes) - self.assertEqual( - literal_eval(span.attributes["db.statement"]), - { - "body": "A few words here, a few words there", - "title": "About searching", - }, - ) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md deleted file mode 100644 index 85dcb366d0..0000000000 --- a/instrumentation/opentelemetry-instrumentation-falcon/CHANGELOG.md +++ /dev/null @@ -1,15 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.14b0 - -Released 2020-10-13 - -- Added support for `OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS` ([#1158](https://github.com/open-telemetry/opentelemetry-python/pull/1158)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Initial release. Added instrumentation for Falcon 2.0+ diff --git a/instrumentation/opentelemetry-instrumentation-falcon/LICENSE b/instrumentation/opentelemetry-instrumentation-falcon/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-falcon/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-falcon/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-falcon/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-falcon/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-falcon/README.rst b/instrumentation/opentelemetry-instrumentation-falcon/README.rst deleted file mode 100644 index 8230deaf76..0000000000 --- a/instrumentation/opentelemetry-instrumentation-falcon/README.rst +++ /dev/null @@ -1,53 +0,0 @@ -OpenTelemetry Falcon Tracing -============================ - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-falcon.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-falcon/ - -This library builds on the OpenTelemetry WSGI middleware to track web requests -in Falcon applications. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-falcon - -Configuration -------------- - -Exclude lists -************* -To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FALCON_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. - -For example, - -:: - - export OTEL_PYTHON_FALCON_EXCLUDED_URLS="client/.*/info,healthcheck" - -will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. - -Request attributes -******************** -To extract certain attributes from Falcon's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS`` to a comma -delimited list of request attribute names. - -For example, - -:: - - export OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS='query_string,uri_template' - -will extract path_info and content_type attributes from every traced request and add them as span attritbues. - -Falcon Request object reference: https://falcon.readthedocs.io/en/stable/api/request_and_response.html#id1 - -References ----------- - -* `OpenTelemetry Falcon Instrumentation `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg b/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg deleted file mode 100644 index 88e287c607..0000000000 --- a/instrumentation/opentelemetry-instrumentation-falcon/setup.cfg +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-falcon -description = Falcon instrumentation for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-falcon -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.4 -package_dir= - =src -packages=find_namespace: -install_requires = - falcon ~= 2.0 - opentelemetry-instrumentation-wsgi == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - opentelemetry-api == 0.16.dev0 - -[options.extras_require] -test = - falcon ~= 2.0 - opentelemetry-test == 0.16.dev0 - parameterized == 0.7.4 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - falcon = opentelemetry.instrumentation.falcon:FalconInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-falcon/setup.py b/instrumentation/opentelemetry-instrumentation-falcon/setup.py deleted file mode 100644 index eb61edde62..0000000000 --- a/instrumentation/opentelemetry-instrumentation-falcon/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "falcon", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py deleted file mode 100644 index 55f8e98dcb..0000000000 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py +++ /dev/null @@ -1,222 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This library builds on the OpenTelemetry WSGI middleware to track web requests -in Falcon applications. In addition to opentelemetry-instrumentation-wsgi, -it supports falcon-specific features such as: - -* The Falcon resource and method name is used as the Span name. -* The ``falcon.resource`` Span attribute is set so the matched resource. -* Error from Falcon resources are properly caught and recorded. - -Usage ------ - -.. code-block:: python - - from falcon import API - from opentelemetry.instrumentation.falcon import FalconInstrumentor - - FalconInstrumentor().instrument() - - app = falcon.API() - - class HelloWorldResource(object): - def on_get(self, req, resp): - resp.body = 'Hello World' - - app.add_route('/hello', HelloWorldResource()) - -API ---- -""" - -import sys -from logging import getLogger - -import falcon - -import opentelemetry.instrumentation.wsgi as otel_wsgi -from opentelemetry import configuration, context, propagators, trace -from opentelemetry.configuration import Configuration -from opentelemetry.instrumentation.falcon.version import __version__ -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.utils import ( - extract_attributes_from_object, - http_status_to_status_code, -) -from opentelemetry.trace.status import Status -from opentelemetry.util import ExcludeList, time_ns - -_logger = getLogger(__name__) - -_ENVIRON_STARTTIME_KEY = "opentelemetry-falcon.starttime_key" -_ENVIRON_SPAN_KEY = "opentelemetry-falcon.span_key" -_ENVIRON_ACTIVATION_KEY = "opentelemetry-falcon.activation_key" -_ENVIRON_TOKEN = "opentelemetry-falcon.token" -_ENVIRON_EXC = "opentelemetry-falcon.exc" - - -def get_excluded_urls(): - urls = configuration.Configuration().FALCON_EXCLUDED_URLS or "" - if urls: - urls = str.split(urls, ",") - return ExcludeList(urls) - - -_excluded_urls = get_excluded_urls() - - -class FalconInstrumentor(BaseInstrumentor): - # pylint: disable=protected-access,attribute-defined-outside-init - """An instrumentor for falcon.API - - See `BaseInstrumentor` - """ - - def _instrument(self, **kwargs): - self._original_falcon_api = falcon.API - falcon.API = _InstrumentedFalconAPI - - def _uninstrument(self, **kwargs): - falcon.API = self._original_falcon_api - - -class _InstrumentedFalconAPI(falcon.API): - def __init__(self, *args, **kwargs): - middlewares = kwargs.pop("middleware", []) - if not isinstance(middlewares, (list, tuple)): - middlewares = [middlewares] - - self._tracer = trace.get_tracer(__name__, __version__) - trace_middleware = _TraceMiddleware( - self._tracer, kwargs.get("traced_request_attributes") - ) - middlewares.insert(0, trace_middleware) - kwargs["middleware"] = middlewares - super().__init__(*args, **kwargs) - - def __call__(self, env, start_response): - if _excluded_urls.url_disabled(env.get("PATH_INFO", "/")): - return super().__call__(env, start_response) - - start_time = time_ns() - - token = context.attach( - propagators.extract(otel_wsgi.carrier_getter, env) - ) - span = self._tracer.start_span( - otel_wsgi.get_default_span_name(env), - kind=trace.SpanKind.SERVER, - start_time=start_time, - ) - if span.is_recording(): - attributes = otel_wsgi.collect_request_attributes(env) - for key, value in attributes.items(): - span.set_attribute(key, value) - - activation = self._tracer.use_span(span, end_on_exit=True) - activation.__enter__() - env[_ENVIRON_SPAN_KEY] = span - env[_ENVIRON_ACTIVATION_KEY] = activation - - def _start_response(status, response_headers, *args, **kwargs): - otel_wsgi.add_response_attributes(span, status, response_headers) - response = start_response( - status, response_headers, *args, **kwargs - ) - activation.__exit__(None, None, None) - context.detach(token) - return response - - try: - return super().__call__(env, _start_response) - except Exception as exc: - activation.__exit__( - type(exc), exc, getattr(exc, "__traceback__", None), - ) - context.detach(token) - raise - - -class _TraceMiddleware: - # pylint:disable=R0201,W0613 - - def __init__(self, tracer=None, traced_request_attrs=None): - self.tracer = tracer - self._traced_request_attrs = traced_request_attrs or [ - attr.strip() - for attr in ( - Configuration().FALCON_TRACED_REQUEST_ATTRS or "" - ).split(",") - ] - - def process_request(self, req, resp): - span = req.env.get(_ENVIRON_SPAN_KEY) - if not span or not span.is_recording(): - return - - attributes = extract_attributes_from_object( - req, self._traced_request_attrs - ) - for key, value in attributes.items(): - span.set_attribute(key, value) - - def process_resource(self, req, resp, resource, params): - span = req.env.get(_ENVIRON_SPAN_KEY) - if not span or not span.is_recording(): - return - - resource_name = resource.__class__.__name__ - span.set_attribute("falcon.resource", resource_name) - span.update_name( - "{0}.on_{1}".format(resource_name, req.method.lower()) - ) - - def process_response( - self, req, resp, resource, req_succeeded=None - ): # pylint:disable=R0201 - span = req.env.get(_ENVIRON_SPAN_KEY) - if not span or not span.is_recording(): - return - - status = resp.status - reason = None - if resource is None: - status = "404" - reason = "NotFound" - - exc_type, exc, _ = sys.exc_info() - if exc_type and not req_succeeded: - if "HTTPNotFound" in exc_type.__name__: - status = "404" - reason = "NotFound" - else: - status = "500" - reason = "{}: {}".format(exc_type.__name__, exc) - - status = status.split(" ")[0] - try: - status_code = int(status) - except ValueError: - pass - finally: - span.set_attribute("http.status_code", status_code) - span.set_status( - Status( - status_code=http_status_to_status_code(status_code), - description=reason, - ) - ) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py deleted file mode 100644 index dcbfe11b49..0000000000 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py +++ /dev/null @@ -1,41 +0,0 @@ -import falcon - -# pylint:disable=R0201,W0613,E0602 - - -class HelloWorldResource: - def _handle_request(self, _, resp): - # pylint: disable=no-member - resp.status = falcon.HTTP_201 - resp.body = "Hello World" - - def on_get(self, req, resp): - self._handle_request(req, resp) - - def on_put(self, req, resp): - self._handle_request(req, resp) - - def on_patch(self, req, resp): - self._handle_request(req, resp) - - def on_post(self, req, resp): - self._handle_request(req, resp) - - def on_delete(self, req, resp): - self._handle_request(req, resp) - - def on_head(self, req, resp): - self._handle_request(req, resp) - - -class ErrorResource: - def on_get(self, req, resp): - print(non_existent_var) # noqa - - -def make_app(): - app = falcon.API() - app.add_route("/hello", HelloWorldResource()) - app.add_route("/ping", HelloWorldResource()) - app.add_route("/error", ErrorResource()) - return app diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py deleted file mode 100644 index fe33a2f2dd..0000000000 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py +++ /dev/null @@ -1,201 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest.mock import Mock, patch - -from falcon import testing - -from opentelemetry.instrumentation.falcon import FalconInstrumentor -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace.status import StatusCode -from opentelemetry.util import ExcludeList - -from .app import make_app - - -class TestFalconInstrumentation(TestBase): - def setUp(self): - super().setUp() - FalconInstrumentor().instrument() - self.app = make_app() - - def client(self): - return testing.TestClient(self.app) - - def tearDown(self): - super().tearDown() - with self.disable_logging(): - FalconInstrumentor().uninstrument() - - def test_get(self): - self._test_method("GET") - - def test_post(self): - self._test_method("POST") - - def test_patch(self): - self._test_method("PATCH") - - def test_put(self): - self._test_method("PUT") - - def test_delete(self): - self._test_method("DELETE") - - def test_head(self): - self._test_method("HEAD") - - def _test_method(self, method): - self.client().simulate_request(method=method, path="/hello") - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertEqual( - span.name, "HelloWorldResource.on_{0}".format(method.lower()) - ) - self.assertEqual(span.status.status_code, StatusCode.UNSET) - self.assert_span_has_attributes( - span, - { - "component": "http", - "http.method": method, - "http.server_name": "falconframework.org", - "http.scheme": "http", - "host.port": 80, - "http.host": "falconframework.org", - "http.target": "/", - "net.peer.ip": "127.0.0.1", - "net.peer.port": "65133", - "http.flavor": "1.1", - "falcon.resource": "HelloWorldResource", - "http.status_text": "Created", - "http.status_code": 201, - }, - ) - self.memory_exporter.clear() - - def test_404(self): - self.client().simulate_get("/does-not-exist") - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertEqual(span.name, "HTTP GET") - self.assertEqual(span.status.status_code, StatusCode.ERROR) - self.assert_span_has_attributes( - span, - { - "component": "http", - "http.method": "GET", - "http.server_name": "falconframework.org", - "http.scheme": "http", - "host.port": 80, - "http.host": "falconframework.org", - "http.target": "/", - "net.peer.ip": "127.0.0.1", - "net.peer.port": "65133", - "http.flavor": "1.1", - "http.status_text": "Not Found", - "http.status_code": 404, - }, - ) - - def test_500(self): - try: - self.client().simulate_get("/error") - except NameError: - pass - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertEqual(span.name, "ErrorResource.on_get") - self.assertFalse(span.status.is_ok) - self.assertEqual(span.status.status_code, StatusCode.ERROR) - self.assertEqual( - span.status.description, - "NameError: name 'non_existent_var' is not defined", - ) - self.assert_span_has_attributes( - span, - { - "component": "http", - "http.method": "GET", - "http.server_name": "falconframework.org", - "http.scheme": "http", - "host.port": 80, - "http.host": "falconframework.org", - "http.target": "/", - "net.peer.ip": "127.0.0.1", - "net.peer.port": "65133", - "http.flavor": "1.1", - "http.status_code": 500, - }, - ) - - def test_uninstrument(self): - self.client().simulate_get(path="/hello") - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - self.memory_exporter.clear() - - FalconInstrumentor().uninstrument() - self.app = make_app() - self.client().simulate_get(path="/hello") - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 0) - - @patch( - "opentelemetry.instrumentation.falcon._excluded_urls", - ExcludeList(["ping"]), - ) - def test_exclude_lists(self): - self.client().simulate_get(path="/ping") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) - - self.client().simulate_get(path="/hello") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - def test_traced_request_attributes(self): - self.client().simulate_get(path="/hello?q=abc") - span = self.memory_exporter.get_finished_spans()[0] - self.assertNotIn("query_string", span.attributes) - self.memory_exporter.clear() - - middleware = self.app._middleware[0][ # pylint:disable=W0212 - 0 - ].__self__ - with patch.object( - middleware, "_traced_request_attrs", ["query_string"] - ): - self.client().simulate_get(path="/hello?q=abc") - span = self.memory_exporter.get_finished_spans()[0] - self.assertIn("query_string", span.attributes) - self.assertEqual(span.attributes["query_string"], "q=abc") - - def test_traced_not_recording(self): - mock_tracer = Mock() - mock_span = Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = mock_span - with patch("opentelemetry.trace.get_tracer") as tracer: - tracer.return_value = mock_tracer - self.client().simulate_get(path="/hello?q=abc") - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-fastapi/CHANGELOG.md deleted file mode 100644 index c8c5cea0d3..0000000000 --- a/instrumentation/opentelemetry-instrumentation-fastapi/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.11b0 - -Released 2020-07-28 - -- Initial release ([#890](https://github.com/open-telemetry/opentelemetry-python/pull/890)) \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/README.rst b/instrumentation/opentelemetry-instrumentation-fastapi/README.rst deleted file mode 100644 index 4cc612da76..0000000000 --- a/instrumentation/opentelemetry-instrumentation-fastapi/README.rst +++ /dev/null @@ -1,43 +0,0 @@ -OpenTelemetry FastAPI Instrumentation -======================================= - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-fastapi.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-fastapi/ - - -This library provides automatic and manual instrumentation of FastAPI web frameworks, -instrumenting http requests served by applications utilizing the framework. - -auto-instrumentation using the opentelemetry-instrumentation package is also supported. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-fastapi - - -Usage ------ - -.. code-block:: python - - import fastapi - from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor - - app = fastapi.FastAPI() - - @app.get("/foobar") - async def foobar(): - return {"message": "hello world"} - - FastAPIInstrumentor.instrument_app(app) - - -References ----------- - -* `OpenTelemetry Project `_ \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg b/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg deleted file mode 100644 index d6b6bdc54f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-fastapi/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-fastapi -description = OpenTelemetry FastAPI Instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-fastapi -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.6 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation-asgi == 0.16.dev0 - -[options.entry_points] -opentelemetry_instrumentor = - fastapi = opentelemetry.instrumentation.fastapi:FastAPIInstrumentor - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - fastapi ~= 0.58.1 - requests ~= 2.23.0 # needed for testclient - -[options.packages.find] -where = src diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/setup.py b/instrumentation/opentelemetry-instrumentation-fastapi/setup.py deleted file mode 100644 index 13c7c5a99c..0000000000 --- a/instrumentation/opentelemetry-instrumentation-fastapi/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "fastapi", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py deleted file mode 100644 index 57c9a5bfc7..0000000000 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from typing import Optional - -import fastapi -from starlette.routing import Match - -from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware -from opentelemetry.instrumentation.fastapi.version import __version__ # noqa -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor - - -class FastAPIInstrumentor(BaseInstrumentor): - """An instrumentor for FastAPI - - See `BaseInstrumentor` - """ - - _original_fastapi = None - - @staticmethod - def instrument_app(app: fastapi.FastAPI): - """Instrument an uninstrumented FastAPI application. - """ - if not getattr(app, "is_instrumented_by_opentelemetry", False): - app.add_middleware( - OpenTelemetryMiddleware, - span_details_callback=_get_route_details, - ) - app.is_instrumented_by_opentelemetry = True - - def _instrument(self, **kwargs): - self._original_fastapi = fastapi.FastAPI - fastapi.FastAPI = _InstrumentedFastAPI - - def _uninstrument(self, **kwargs): - fastapi.FastAPI = self._original_fastapi - - -class _InstrumentedFastAPI(fastapi.FastAPI): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.add_middleware( - OpenTelemetryMiddleware, span_details_callback=_get_route_details - ) - - -def _get_route_details(scope): - """Callback to retrieve the fastapi route being served. - - TODO: there is currently no way to retrieve http.route from - a starlette application from scope. - - See: https://github.com/encode/starlette/pull/804 - """ - app = scope["app"] - route = None - for starlette_route in app.routes: - match, _ = starlette_route.matches(scope) - if match == Match.FULL: - route = starlette_route.path - break - if match == Match.PARTIAL: - route = starlette_route.path - # method only exists for http, if websocket - # leave it blank. - span_name = route or scope.get("method", "") - attributes = {} - if route: - attributes["http.route"] = route - return span_name, attributes diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py deleted file mode 100644 index 47617d4e95..0000000000 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -import fastapi -from fastapi.testclient import TestClient - -import opentelemetry.instrumentation.fastapi as otel_fastapi -from opentelemetry.test.test_base import TestBase - - -class TestFastAPIManualInstrumentation(TestBase): - def _create_app(self): - app = self._create_fastapi_app() - self._instrumentor.instrument_app(app) - return app - - def setUp(self): - super().setUp() - self._instrumentor = otel_fastapi.FastAPIInstrumentor() - self._app = self._create_app() - self._client = TestClient(self._app) - - def test_basic_fastapi_call(self): - self._client.get("/foobar") - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 3) - for span in spans: - self.assertIn("/foobar", span.name) - - def test_fastapi_route_attribute_added(self): - """Ensure that fastapi routes are used as the span name.""" - self._client.get("/user/123") - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 3) - for span in spans: - self.assertIn("/user/{username}", span.name) - self.assertEqual( - spans[-1].attributes["http.route"], "/user/{username}" - ) - # ensure that at least one attribute that is populated by - # the asgi instrumentation is successfully feeding though. - self.assertEqual(spans[-1].attributes["http.flavor"], "1.1") - - @staticmethod - def _create_fastapi_app(): - app = fastapi.FastAPI() - - @app.get("/foobar") - async def _(): - return {"message": "hello world"} - - @app.get("/user/{username}") - async def _(username: str): - return {"message": username} - - return app - - -class TestAutoInstrumentation(TestFastAPIManualInstrumentation): - """Test the auto-instrumented variant - - Extending the manual instrumentation as most test cases apply - to both. - """ - - def _create_app(self): - # instrumentation is handled by the instrument call - self._instrumentor.instrument() - return self._create_fastapi_app() - - def tearDown(self): - self._instrumentor.uninstrument() - super().tearDown() - - -class TestAutoInstrumentationLogic(unittest.TestCase): - def test_instrumentation(self): - """Verify that instrumentation methods are instrumenting and - removing as expected. - """ - instrumentor = otel_fastapi.FastAPIInstrumentor() - original = fastapi.FastAPI - instrumentor.instrument() - try: - instrumented = fastapi.FastAPI - self.assertIsNot(original, instrumented) - finally: - instrumentor.uninstrument() - - should_be_original = fastapi.FastAPI - self.assertIs(original, should_be_original) diff --git a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md deleted file mode 100644 index 4c7e7a055b..0000000000 --- a/instrumentation/opentelemetry-instrumentation-flask/CHANGELOG.md +++ /dev/null @@ -1,56 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.15b0 - -Released 2020-11-02 - -- Use `url.rule` instead of `request.endpoint` for span name - ([#1260](https://github.com/open-telemetry/opentelemetry-python/pull/1260)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-flask - ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) -- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - -## Version 0.11b0 - -- Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) - -## 0.7b1 - -Released 2020-05-12 - -- Add exclude list for paths and hosts - ([#630](https://github.com/open-telemetry/opentelemetry-python/pull/630)) - -## 0.6b0 - -Released 2020-03-30 - -- Add an entry_point to be usable in auto-instrumentation - ([#327](https://github.com/open-telemetry/opentelemetry-python/pull/327)) - -## 0.4a0 - -Released 2020-02-21 - -- Use string keys for WSGI environ values - ([#366](https://github.com/open-telemetry/opentelemetry-python/pull/366)) - -## 0.3a0 - -Released 2019-12-11 - -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-flask/LICENSE b/instrumentation/opentelemetry-instrumentation-flask/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-flask/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-flask/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-flask/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-flask/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-flask/README.rst b/instrumentation/opentelemetry-instrumentation-flask/README.rst deleted file mode 100644 index f79d8fd604..0000000000 --- a/instrumentation/opentelemetry-instrumentation-flask/README.rst +++ /dev/null @@ -1,38 +0,0 @@ -OpenTelemetry Flask Tracing -=========================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-flask.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-flask/ - -This library builds on the OpenTelemetry WSGI middleware to track web requests -in Flask applications. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-flask - -Configuration -------------- - -Exclude lists -************* -To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FLASK_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. - -For example, - -:: - - export OTEL_PYTHON_FLASK_EXCLUDED_URLS="client/.*/info,healthcheck" - -will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. - -References ----------- - -* `OpenTelemetry Flask Instrumentation `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg b/instrumentation/opentelemetry-instrumentation-flask/setup.cfg deleted file mode 100644 index 2e9f943ceb..0000000000 --- a/instrumentation/opentelemetry-instrumentation-flask/setup.cfg +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-flask -description = Flask instrumentation for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-flask -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - flask ~= 1.0 - opentelemetry-instrumentation-wsgi == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - opentelemetry-api == 0.16.dev0 - -[options.extras_require] -test = - flask~=1.0 - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src diff --git a/instrumentation/opentelemetry-instrumentation-flask/setup.py b/instrumentation/opentelemetry-instrumentation-flask/setup.py deleted file mode 100644 index 587b697a7b..0000000000 --- a/instrumentation/opentelemetry-instrumentation-flask/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "flask", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup( - version=PACKAGE_INFO["__version__"], - entry_points={ - "opentelemetry_instrumentor": [ - "flask = opentelemetry.instrumentation.flask:FlaskInstrumentor" - ] - }, -) diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py deleted file mode 100644 index 1235b09a30..0000000000 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Note: This package is not named "flask" because of -# https://github.com/PyCQA/pylint/issues/2648 - -""" -This library builds on the OpenTelemetry WSGI middleware to track web requests -in Flask applications. In addition to opentelemetry-instrumentation-wsgi, it supports -flask-specific features such as: - -* The Flask endpoint name is used as the Span name. -* The ``http.route`` Span attribute is set so that one can see which URL rule - matched a request. - -Usage ------ - -.. code-block:: python - - from flask import Flask - from opentelemetry.instrumentation.flask import FlaskInstrumentor - - app = Flask(__name__) - - FlaskInstrumentor().instrument_app(app) - - @app.route("/") - def hello(): - return "Hello!" - - if __name__ == "__main__": - app.run(debug=True) - -API ---- -""" - -from logging import getLogger - -import flask - -import opentelemetry.instrumentation.wsgi as otel_wsgi -from opentelemetry import configuration, context, propagators, trace -from opentelemetry.instrumentation.flask.version import __version__ -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.util import ExcludeList, time_ns - -_logger = getLogger(__name__) - -_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key" -_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key" -_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key" -_ENVIRON_TOKEN = "opentelemetry-flask.token" - - -def get_excluded_urls(): - urls = configuration.Configuration().FLASK_EXCLUDED_URLS or [] - if urls: - urls = str.split(urls, ",") - return ExcludeList(urls) - - -_excluded_urls = get_excluded_urls() - - -def _rewrapped_app(wsgi_app): - def _wrapped_app(environ, start_response): - # We want to measure the time for route matching, etc. - # In theory, we could start the span here and use - # update_name later but that API is "highly discouraged" so - # we better avoid it. - environ[_ENVIRON_STARTTIME_KEY] = time_ns() - - def _start_response(status, response_headers, *args, **kwargs): - if not _excluded_urls.url_disabled(flask.request.url): - span = flask.request.environ.get(_ENVIRON_SPAN_KEY) - - if span: - otel_wsgi.add_response_attributes( - span, status, response_headers - ) - else: - _logger.warning( - "Flask environ's OpenTelemetry span " - "missing at _start_response(%s)", - status, - ) - - return start_response(status, response_headers, *args, **kwargs) - - return wsgi_app(environ, _start_response) - - return _wrapped_app - - -def _before_request(): - if _excluded_urls.url_disabled(flask.request.url): - return - - environ = flask.request.environ - span_name = None - try: - span_name = flask.request.url_rule.rule - except AttributeError: - pass - if span_name is None: - span_name = otel_wsgi.get_default_span_name(environ) - token = context.attach( - propagators.extract(otel_wsgi.carrier_getter, environ) - ) - - tracer = trace.get_tracer(__name__, __version__) - - span = tracer.start_span( - span_name, - kind=trace.SpanKind.SERVER, - start_time=environ.get(_ENVIRON_STARTTIME_KEY), - ) - if span.is_recording(): - attributes = otel_wsgi.collect_request_attributes(environ) - if flask.request.url_rule: - # For 404 that result from no route found, etc, we - # don't have a url_rule. - attributes["http.route"] = flask.request.url_rule.rule - for key, value in attributes.items(): - span.set_attribute(key, value) - - activation = tracer.use_span(span, end_on_exit=True) - activation.__enter__() - environ[_ENVIRON_ACTIVATION_KEY] = activation - environ[_ENVIRON_SPAN_KEY] = span - environ[_ENVIRON_TOKEN] = token - - -def _teardown_request(exc): - if _excluded_urls.url_disabled(flask.request.url): - return - - activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY) - if not activation: - _logger.warning( - "Flask environ's OpenTelemetry activation missing" - "at _teardown_flask_request(%s)", - exc, - ) - return - - if exc is None: - activation.__exit__(None, None, None) - else: - activation.__exit__( - type(exc), exc, getattr(exc, "__traceback__", None) - ) - context.detach(flask.request.environ.get(_ENVIRON_TOKEN)) - - -class _InstrumentedFlask(flask.Flask): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self._original_wsgi_ = self.wsgi_app - self.wsgi_app = _rewrapped_app(self.wsgi_app) - - self.before_request(_before_request) - self.teardown_request(_teardown_request) - - -class FlaskInstrumentor(BaseInstrumentor): - # pylint: disable=protected-access,attribute-defined-outside-init - """An instrumentor for flask.Flask - - See `BaseInstrumentor` - """ - - def _instrument(self, **kwargs): - self._original_flask = flask.Flask - flask.Flask = _InstrumentedFlask - - def instrument_app(self, app): # pylint: disable=no-self-use - if not hasattr(app, "_is_instrumented"): - app._is_instrumented = False - - if not app._is_instrumented: - app._original_wsgi_app = app.wsgi_app - app.wsgi_app = _rewrapped_app(app.wsgi_app) - - app.before_request(_before_request) - app.teardown_request(_teardown_request) - app._is_instrumented = True - else: - _logger.warning( - "Attempting to instrument Flask app while already instrumented" - ) - - def _uninstrument(self, **kwargs): - flask.Flask = self._original_flask - - def uninstrument_app(self, app): # pylint: disable=no-self-use - if not hasattr(app, "_is_instrumented"): - app._is_instrumented = False - - if app._is_instrumented: - app.wsgi_app = app._original_wsgi_app - - # FIXME add support for other Flask blueprints that are not None - app.before_request_funcs[None].remove(_before_request) - app.teardown_request_funcs[None].remove(_teardown_request) - del app._original_wsgi_app - - app._is_instrumented = False - else: - _logger.warning( - "Attempting to uninstrument Flask " - "app while already uninstrumented" - ) diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-flask/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py b/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py deleted file mode 100644 index c2bc646e1b..0000000000 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from werkzeug.test import Client -from werkzeug.wrappers import BaseResponse - -from opentelemetry.configuration import Configuration - - -class InstrumentationTest: - def setUp(self): # pylint: disable=invalid-name - super().setUp() # pylint: disable=no-member - Configuration._reset() # pylint: disable=protected-access - - @staticmethod - def _hello_endpoint(helloid): - if helloid == 500: - raise ValueError(":-(") - return "Hello: " + str(helloid) - - def _common_initialization(self): - def excluded_endpoint(): - return "excluded" - - def excluded2_endpoint(): - return "excluded2" - - # pylint: disable=no-member - self.app.route("/hello/")(self._hello_endpoint) - self.app.route("/excluded/")(self._hello_endpoint) - self.app.route("/excluded")(excluded_endpoint) - self.app.route("/excluded2")(excluded2_endpoint) - - # pylint: disable=attribute-defined-outside-init - self.client = Client(self.app, BaseResponse) diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_automatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_automatic.py deleted file mode 100644 index d081bed9ee..0000000000 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_automatic.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import flask -from werkzeug.test import Client -from werkzeug.wrappers import BaseResponse - -from opentelemetry.instrumentation.flask import FlaskInstrumentor -from opentelemetry.test.test_base import TestBase -from opentelemetry.test.wsgitestutil import WsgiTestBase - -# pylint: disable=import-error -from .base_test import InstrumentationTest - - -class TestAutomatic(InstrumentationTest, TestBase, WsgiTestBase): - def setUp(self): - super().setUp() - - FlaskInstrumentor().instrument() - - self.app = flask.Flask(__name__) - - self._common_initialization() - - def tearDown(self): - super().tearDown() - with self.disable_logging(): - FlaskInstrumentor().uninstrument() - - def test_uninstrument(self): - # pylint: disable=access-member-before-definition - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - FlaskInstrumentor().uninstrument() - self.app = flask.Flask(__name__) - - self.app.route("/hello/")(self._hello_endpoint) - # pylint: disable=attribute-defined-outside-init - self.client = Client(self.app, BaseResponse) - - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py deleted file mode 100644 index a907890523..0000000000 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py +++ /dev/null @@ -1,180 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest.mock import Mock, patch - -from flask import Flask, request - -from opentelemetry import trace -from opentelemetry.instrumentation.flask import FlaskInstrumentor -from opentelemetry.test.test_base import TestBase -from opentelemetry.test.wsgitestutil import WsgiTestBase -from opentelemetry.util import ExcludeList - -# pylint: disable=import-error -from .base_test import InstrumentationTest - - -def expected_attributes(override_attributes): - default_attributes = { - "component": "http", - "http.method": "GET", - "http.server_name": "localhost", - "http.scheme": "http", - "host.port": 80, - "http.host": "localhost", - "http.target": "/", - "http.flavor": "1.1", - "http.status_text": "OK", - "http.status_code": 200, - } - for key, val in override_attributes.items(): - default_attributes[key] = val - return default_attributes - - -class TestProgrammatic(InstrumentationTest, TestBase, WsgiTestBase): - def setUp(self): - super().setUp() - - self.app = Flask(__name__) - - FlaskInstrumentor().instrument_app(self.app) - - self._common_initialization() - - def tearDown(self): - super().tearDown() - with self.disable_logging(): - FlaskInstrumentor().uninstrument_app(self.app) - - def test_uninstrument(self): - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - FlaskInstrumentor().uninstrument_app(self.app) - - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - # pylint: disable=no-member - def test_only_strings_in_environ(self): - """ - Some WSGI servers (such as Gunicorn) expect keys in the environ object - to be strings - - OpenTelemetry should adhere to this convention. - """ - nonstring_keys = set() - - def assert_environ(): - for key in request.environ: - if not isinstance(key, str): - nonstring_keys.add(key) - return "hi" - - self.app.route("/assert_environ")(assert_environ) - self.client.get("/assert_environ") - self.assertEqual(nonstring_keys, set()) - - def test_simple(self): - expected_attrs = expected_attributes( - {"http.target": "/hello/123", "http.route": "/hello/"} - ) - self.client.get("/hello/123") - - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "/hello/") - self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) - self.assertEqual(span_list[0].attributes, expected_attrs) - - def test_not_recording(self): - mock_tracer = Mock() - mock_span = Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = mock_span - with patch("opentelemetry.trace.get_tracer") as tracer: - tracer.return_value = mock_tracer - self.client.get("/hello/123") - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_404(self): - expected_attrs = expected_attributes( - { - "http.method": "POST", - "http.target": "/bye", - "http.status_text": "NOT FOUND", - "http.status_code": 404, - } - ) - - resp = self.client.post("/bye") - self.assertEqual(404, resp.status_code) - resp.close() - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "HTTP POST") - self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) - self.assertEqual(span_list[0].attributes, expected_attrs) - - def test_internal_error(self): - expected_attrs = expected_attributes( - { - "http.target": "/hello/500", - "http.route": "/hello/", - "http.status_text": "INTERNAL SERVER ERROR", - "http.status_code": 500, - } - ) - resp = self.client.get("/hello/500") - self.assertEqual(500, resp.status_code) - resp.close() - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "/hello/") - self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) - self.assertEqual(span_list[0].attributes, expected_attrs) - - @patch( - "opentelemetry.instrumentation.flask._excluded_urls", - ExcludeList(["http://localhost/excluded_arg/123", "excluded_noarg"]), - ) - def test_exclude_lists(self): - self.client.get("/excluded_arg/123") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) - - self.client.get("/excluded_arg/125") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - self.client.get("/excluded_noarg") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - self.client.get("/excluded_noarg2") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md deleted file mode 100644 index 51bceab9e8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/CHANGELOG.md +++ /dev/null @@ -1,46 +0,0 @@ -# Changelog - -## Unreleased - -- Update protobuf versions - ([#1356](https://github.com/open-telemetry/opentelemetry-python/pull/1356)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-grpc - ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - -## Version 0.11b0 - -Released 2020-07-28 - -- Add status code to gRPC client spans - ([896](https://github.com/open-telemetry/opentelemetry-python/pull/896)) -- Add gRPC client and server instrumentors - ([788](https://github.com/open-telemetry/opentelemetry-python/pull/788)) - -- Add metric recording (bytes in/out, errors, latency) to gRPC client - -## 0.8b0 - -Released 2020-05-27 - -- lint: version of grpc causes lint issues - ([#696](https://github.com/open-telemetry/opentelemetry-python/pull/696)) - -## 0.6b0 - -Released 2020-03-30 - -- Add gRPC integration - ([#476](https://github.com/open-telemetry/opentelemetry-python/pull/476)) -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-grpc/LICENSE b/instrumentation/opentelemetry-instrumentation-grpc/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-grpc/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-grpc/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-grpc/README.rst b/instrumentation/opentelemetry-instrumentation-grpc/README.rst deleted file mode 100644 index 176bdf1a39..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/README.rst +++ /dev/null @@ -1,18 +0,0 @@ -OpenTelemetry gRPC Integration -============================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-grpc.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-grpc/ - -Client and server interceptors for `gRPC Python`_. - -.. _gRPC Python: https://grpc.github.io/grpc/python/grpc.html - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-grpc diff --git a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg b/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg deleted file mode 100644 index d1d3ff52d4..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/setup.cfg +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -[metadata] -name = opentelemetry-instrumentation-grpc -description = OpenTelemetry gRPC instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-grpc -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-sdk == 0.16.dev0 - grpcio ~= 1.27 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - opentelemetry-sdk == 0.16.dev0 - protobuf >= 3.13.0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - grpc_client = opentelemetry.instrumentation.grpc:GrpcInstrumentorClient - grpc_server = opentelemetry.instrumentation.grpc:GrpcInstrumentorServer diff --git a/instrumentation/opentelemetry-instrumentation-grpc/setup.py b/instrumentation/opentelemetry-instrumentation-grpc/setup.py deleted file mode 100644 index 87c720aea2..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/setup.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "grpc", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py deleted file mode 100644 index 776e29e8e2..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py +++ /dev/null @@ -1,248 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint:disable=no-name-in-module -# pylint:disable=relative-beyond-top-level -# pylint:disable=import-error -# pylint:disable=no-self-use -""" -Usage Client ------------- -.. code-block:: python - - import logging - - import grpc - - from opentelemetry import trace - from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient, client_interceptor - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import ( - ConsoleSpanExporter, - SimpleExportSpanProcessor, - ) - - from opentelemetry import metrics - from opentelemetry.sdk.metrics import MeterProvider - from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter - - try: - from .gen import helloworld_pb2, helloworld_pb2_grpc - except ImportError: - from gen import helloworld_pb2, helloworld_pb2_grpc - - trace.set_tracer_provider(TracerProvider()) - trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) - ) - - # Set meter provider to opentelemetry-sdk's MeterProvider - metrics.set_meter_provider(MeterProvider()) - - # Optional - export GRPC specific metrics (latency, bytes in/out, errors) by passing an exporter - instrumentor = GrpcInstrumentorClient(exporter=ConsoleMetricsExporter(), interval=10) - instrumentor.instrument() - - def run(): - with grpc.insecure_channel("localhost:50051") as channel: - - stub = helloworld_pb2_grpc.GreeterStub(channel) - response = stub.SayHello(helloworld_pb2.HelloRequest(name="YOU")) - - print("Greeter client received: " + response.message) - - - if __name__ == "__main__": - logging.basicConfig() - run() - -Usage Server ------------- -.. code-block:: python - - import logging - from concurrent import futures - - import grpc - - from opentelemetry import trace - from opentelemetry.instrumentation.grpc import GrpcInstrumentorServer - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import ( - ConsoleSpanExporter, - SimpleExportSpanProcessor, - ) - - try: - from .gen import helloworld_pb2, helloworld_pb2_grpc - except ImportError: - from gen import helloworld_pb2, helloworld_pb2_grpc - - trace.set_tracer_provider(TracerProvider()) - trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) - ) - - grpc_server_instrumentor = GrpcInstrumentorServer() - grpc_server_instrumentor.instrument() - - class Greeter(helloworld_pb2_grpc.GreeterServicer): - def SayHello(self, request, context): - return helloworld_pb2.HelloReply(message="Hello, %s!" % request.name) - - - def serve(): - - server = grpc.server(futures.ThreadPoolExecutor()) - - helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server) - server.add_insecure_port("[::]:50051") - server.start() - server.wait_for_termination() - - - if __name__ == "__main__": - logging.basicConfig() - serve() - -You can also add the instrumentor manually, rather than using -:py:class:`~opentelemetry.instrumentation.grpc.GrpcInstrumentorServer`: - -.. code-block:: python - - from opentelemetry.instrumentation.grpc import server_interceptor - - server = grpc.server(futures.ThreadPoolExecutor(), - interceptors = [server_interceptor()]) - -""" -from functools import partial - -import grpc -from wrapt import wrap_function_wrapper as _wrap - -from opentelemetry import trace -from opentelemetry.instrumentation.grpc.grpcext import intercept_channel -from opentelemetry.instrumentation.grpc.version import __version__ -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.utils import unwrap - -# pylint:disable=import-outside-toplevel -# pylint:disable=import-self -# pylint:disable=unused-argument -# isort:skip - - -class GrpcInstrumentorServer(BaseInstrumentor): - """ - Globally instrument the grpc server. - - Usage:: - - grpc_server_instrumentor = GrpcInstrumentorServer() - grpc_server_instrumentor.instrument() - - """ - - # pylint:disable=attribute-defined-outside-init - - def _instrument(self, **kwargs): - self._original_func = grpc.server - - def server(*args, **kwargs): - if "interceptors" in kwargs: - # add our interceptor as the first - kwargs["interceptors"].insert(0, server_interceptor()) - else: - kwargs["interceptors"] = [server_interceptor()] - return self._original_func(*args, **kwargs) - - grpc.server = server - - def _uninstrument(self, **kwargs): - grpc.server = self._original_func - - -class GrpcInstrumentorClient(BaseInstrumentor): - def _instrument(self, **kwargs): - exporter = kwargs.get("exporter", None) - interval = kwargs.get("interval", 30) - if kwargs.get("channel_type") == "secure": - _wrap( - "grpc", - "secure_channel", - partial(self.wrapper_fn, exporter, interval), - ) - - else: - _wrap( - "grpc", - "insecure_channel", - partial(self.wrapper_fn, exporter, interval), - ) - - def _uninstrument(self, **kwargs): - if kwargs.get("channel_type") == "secure": - unwrap(grpc, "secure_channel") - - else: - unwrap(grpc, "insecure_channel") - - def wrapper_fn( - self, exporter, interval, original_func, instance, args, kwargs - ): - channel = original_func(*args, **kwargs) - tracer_provider = kwargs.get("tracer_provider") - return intercept_channel( - channel, - client_interceptor( - tracer_provider=tracer_provider, - exporter=exporter, - interval=interval, - ), - ) - - -def client_interceptor(tracer_provider=None, exporter=None, interval=30): - """Create a gRPC client channel interceptor. - - Args: - tracer: The tracer to use to create client-side spans. - exporter: The exporter that will receive client metrics - interval: Time between every export call - - Returns: - An invocation-side interceptor object. - """ - from . import _client - - tracer = trace.get_tracer(__name__, __version__, tracer_provider) - - return _client.OpenTelemetryClientInterceptor(tracer, exporter, interval) - - -def server_interceptor(tracer_provider=None): - """Create a gRPC server interceptor. - - Args: - tracer: The tracer to use to create server-side spans. - - Returns: - A service-side interceptor object. - """ - from . import _server - - tracer = trace.get_tracer(__name__, __version__, tracer_provider) - - return _server.OpenTelemetryServerInterceptor(tracer) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py deleted file mode 100644 index f8a72931f9..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py +++ /dev/null @@ -1,273 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint:disable=relative-beyond-top-level -# pylint:disable=arguments-differ -# pylint:disable=no-member -# pylint:disable=signature-differs - -"""Implementation of the invocation-side open-telemetry interceptor.""" - -from collections import OrderedDict -from typing import MutableMapping - -import grpc - -from opentelemetry import metrics, propagators, trace -from opentelemetry.sdk.metrics.export.controller import PushController -from opentelemetry.trace.status import Status, StatusCode - -from . import grpcext -from ._utilities import RpcInfo, TimedMetricRecorder - - -class _GuardedSpan: - def __init__(self, span): - self.span = span - self.generated_span = None - self._engaged = True - - def __enter__(self): - self.generated_span = self.span.__enter__() - return self - - def __exit__(self, *args, **kwargs): - if self._engaged: - self.generated_span = None - return self.span.__exit__(*args, **kwargs) - return False - - def release(self): - self._engaged = False - return self.span - - -def _inject_span_context(metadata: MutableMapping[str, str]) -> None: - # pylint:disable=unused-argument - def append_metadata( - carrier: MutableMapping[str, str], key: str, value: str - ): - metadata[key] = value - - # Inject current active span from the context - propagators.inject(append_metadata, metadata) - - -def _make_future_done_callback(span, rpc_info, client_info, metrics_recorder): - def callback(response_future): - with span: - code = response_future.code() - if code != grpc.StatusCode.OK: - rpc_info.error = code - return - response = response_future.result() - rpc_info.response = response - if "ByteSize" in dir(response): - metrics_recorder.record_bytes_in( - response.ByteSize(), client_info.full_method - ) - - return callback - - -class OpenTelemetryClientInterceptor( - grpcext.UnaryClientInterceptor, grpcext.StreamClientInterceptor -): - def __init__(self, tracer, exporter, interval): - self._tracer = tracer - - self._meter = None - if exporter and interval: - self._meter = metrics.get_meter(__name__) - self.controller = PushController( - meter=self._meter, exporter=exporter, interval=interval - ) - self._metrics_recorder = TimedMetricRecorder(self._meter, "client") - - def _start_span(self, method): - return self._tracer.start_as_current_span( - name=method, kind=trace.SpanKind.CLIENT - ) - - # pylint:disable=no-self-use - def _trace_result(self, guarded_span, rpc_info, result, client_info): - # If the RPC is called asynchronously, release the guard and add a - # callback so that the span can be finished once the future is done. - if isinstance(result, grpc.Future): - result.add_done_callback( - _make_future_done_callback( - guarded_span.release(), - rpc_info, - client_info, - self._metrics_recorder, - ) - ) - return result - response = result - # Handle the case when the RPC is initiated via the with_call - # method and the result is a tuple with the first element as the - # response. - # http://www.grpc.io/grpc/python/grpc.html#grpc.UnaryUnaryMultiCallable.with_call - if isinstance(result, tuple): - response = result[0] - rpc_info.response = response - - if "ByteSize" in dir(response): - self._metrics_recorder.record_bytes_in( - response.ByteSize(), client_info.full_method - ) - return result - - def _start_guarded_span(self, *args, **kwargs): - return _GuardedSpan(self._start_span(*args, **kwargs)) - - def _bytes_out_iterator_wrapper(self, iterator, client_info): - for request in iterator: - if "ByteSize" in dir(request): - self._metrics_recorder.record_bytes_out( - request.ByteSize(), client_info.full_method - ) - yield request - - def intercept_unary(self, request, metadata, client_info, invoker): - if not metadata: - mutable_metadata = OrderedDict() - else: - mutable_metadata = OrderedDict(metadata) - - with self._start_guarded_span(client_info.full_method) as guarded_span: - with self._metrics_recorder.record_latency( - client_info.full_method - ): - _inject_span_context(mutable_metadata) - metadata = tuple(mutable_metadata.items()) - - # If protobuf is used, we can record the bytes in/out. Otherwise, we have no way - # to get the size of the request/response properly, so don't record anything - if "ByteSize" in dir(request): - self._metrics_recorder.record_bytes_out( - request.ByteSize(), client_info.full_method - ) - - rpc_info = RpcInfo( - full_method=client_info.full_method, - metadata=metadata, - timeout=client_info.timeout, - request=request, - ) - - try: - result = invoker(request, metadata) - except grpc.RpcError: - guarded_span.generated_span.set_status( - Status(StatusCode.ERROR) - ) - raise - - return self._trace_result( - guarded_span, rpc_info, result, client_info - ) - - # For RPCs that stream responses, the result can be a generator. To record - # the span across the generated responses and detect any errors, we wrap - # the result in a new generator that yields the response values. - def _intercept_server_stream( - self, request_or_iterator, metadata, client_info, invoker - ): - if not metadata: - mutable_metadata = OrderedDict() - else: - mutable_metadata = OrderedDict(metadata) - - with self._start_span(client_info.full_method) as span: - with self._metrics_recorder.record_latency( - client_info.full_method - ): - _inject_span_context(mutable_metadata) - metadata = tuple(mutable_metadata.items()) - rpc_info = RpcInfo( - full_method=client_info.full_method, - metadata=metadata, - timeout=client_info.timeout, - ) - - if client_info.is_client_stream: - rpc_info.request = request_or_iterator - request_or_iterator = self._bytes_out_iterator_wrapper( - request_or_iterator, client_info - ) - else: - if "ByteSize" in dir(request_or_iterator): - self._metrics_recorder.record_bytes_out( - request_or_iterator.ByteSize(), - client_info.full_method, - ) - - try: - result = invoker(request_or_iterator, metadata) - - # Rewrap the result stream into a generator, and record the bytes received - for response in result: - if "ByteSize" in dir(response): - self._metrics_recorder.record_bytes_in( - response.ByteSize(), client_info.full_method - ) - yield response - except grpc.RpcError: - span.set_status(Status(StatusCode.ERROR)) - raise - - def intercept_stream( - self, request_or_iterator, metadata, client_info, invoker - ): - if client_info.is_server_stream: - return self._intercept_server_stream( - request_or_iterator, metadata, client_info, invoker - ) - - if not metadata: - mutable_metadata = OrderedDict() - else: - mutable_metadata = OrderedDict(metadata) - - with self._start_guarded_span(client_info.full_method) as guarded_span: - with self._metrics_recorder.record_latency( - client_info.full_method - ): - _inject_span_context(mutable_metadata) - metadata = tuple(mutable_metadata.items()) - rpc_info = RpcInfo( - full_method=client_info.full_method, - metadata=metadata, - timeout=client_info.timeout, - request=request_or_iterator, - ) - - rpc_info.request = request_or_iterator - - request_or_iterator = self._bytes_out_iterator_wrapper( - request_or_iterator, client_info - ) - - try: - result = invoker(request_or_iterator, metadata) - except grpc.RpcError: - guarded_span.generated_span.set_status( - Status(StatusCode.ERROR) - ) - raise - - return self._trace_result( - guarded_span, rpc_info, result, client_info - ) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py deleted file mode 100644 index 087cf4f9cc..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py +++ /dev/null @@ -1,246 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint:disable=relative-beyond-top-level -# pylint:disable=arguments-differ -# pylint:disable=no-member -# pylint:disable=signature-differs - -""" -Implementation of the service-side open-telemetry interceptor. -""" - -import logging -from contextlib import contextmanager - -import grpc - -from opentelemetry import propagators, trace -from opentelemetry.context import attach, detach -from opentelemetry.trace.propagation.textmap import DictGetter -from opentelemetry.trace.status import Status, StatusCode - -logger = logging.getLogger(__name__) - - -# wrap an RPC call -# see https://github.com/grpc/grpc/issues/18191 -def _wrap_rpc_behavior(handler, continuation): - if handler is None: - return None - - if handler.request_streaming and handler.response_streaming: - behavior_fn = handler.stream_stream - handler_factory = grpc.stream_stream_rpc_method_handler - elif handler.request_streaming and not handler.response_streaming: - behavior_fn = handler.stream_unary - handler_factory = grpc.stream_unary_rpc_method_handler - elif not handler.request_streaming and handler.response_streaming: - behavior_fn = handler.unary_stream - handler_factory = grpc.unary_stream_rpc_method_handler - else: - behavior_fn = handler.unary_unary - handler_factory = grpc.unary_unary_rpc_method_handler - - return handler_factory( - continuation( - behavior_fn, handler.request_streaming, handler.response_streaming - ), - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) - - -# pylint:disable=abstract-method -class _OpenTelemetryServicerContext(grpc.ServicerContext): - def __init__(self, servicer_context, active_span): - self._servicer_context = servicer_context - self._active_span = active_span - self.code = grpc.StatusCode.OK - self.details = None - super().__init__() - - def is_active(self, *args, **kwargs): - return self._servicer_context.is_active(*args, **kwargs) - - def time_remaining(self, *args, **kwargs): - return self._servicer_context.time_remaining(*args, **kwargs) - - def cancel(self, *args, **kwargs): - return self._servicer_context.cancel(*args, **kwargs) - - def add_callback(self, *args, **kwargs): - return self._servicer_context.add_callback(*args, **kwargs) - - def disable_next_message_compression(self): - return self._service_context.disable_next_message_compression() - - def invocation_metadata(self, *args, **kwargs): - return self._servicer_context.invocation_metadata(*args, **kwargs) - - def peer(self): - return self._servicer_context.peer() - - def peer_identities(self): - return self._servicer_context.peer_identities() - - def peer_identity_key(self): - return self._servicer_context.peer_identity_key() - - def auth_context(self): - return self._servicer_context.auth_context() - - def set_compression(self, compression): - return self._servicer_context.set_compression(compression) - - def send_initial_metadata(self, *args, **kwargs): - return self._servicer_context.send_initial_metadata(*args, **kwargs) - - def set_trailing_metadata(self, *args, **kwargs): - return self._servicer_context.set_trailing_metadata(*args, **kwargs) - - def abort(self, code, details): - self.code = code - self.details = details - self._active_span.set_attribute("rpc.grpc.status_code", code.name) - self._active_span.set_status( - Status(status_code=StatusCode.ERROR, description=details) - ) - return self._servicer_context.abort(code, details) - - def abort_with_status(self, status): - return self._servicer_context.abort_with_status(status) - - def set_code(self, code): - self.code = code - # use details if we already have it, otherwise the status description - details = self.details or code.value[1] - self._active_span.set_attribute("rpc.grpc.status_code", code.name) - self._active_span.set_status( - Status(status_code=StatusCode.ERROR, description=details) - ) - return self._servicer_context.set_code(code) - - def set_details(self, details): - self.details = details - self._active_span.set_status( - Status(status_code=StatusCode.ERROR, description=details) - ) - return self._servicer_context.set_details(details) - - -# pylint:disable=abstract-method -# pylint:disable=no-self-use -# pylint:disable=unused-argument -class OpenTelemetryServerInterceptor(grpc.ServerInterceptor): - """ - A gRPC server interceptor, to add OpenTelemetry. - - Usage:: - - tracer = some OpenTelemetry tracer - - interceptors = [ - OpenTelemetryServerInterceptor(tracer), - ] - - server = grpc.server( - futures.ThreadPoolExecutor(max_workers=concurrency), - interceptors = interceptors) - - """ - - def __init__(self, tracer): - self._tracer = tracer - self._carrier_getter = DictGetter() - - @contextmanager - def _set_remote_context(self, servicer_context): - metadata = servicer_context.invocation_metadata() - if metadata: - md_dict = {md.key: md.value for md in metadata} - ctx = propagators.extract(self._carrier_getter, md_dict) - token = attach(ctx) - try: - yield - finally: - detach(token) - else: - yield - - def _start_span(self, handler_call_details, context): - - attributes = { - "rpc.method": handler_call_details.method, - "rpc.system": "grpc", - "rpc.grpc.status_code": grpc.StatusCode.OK, - } - - metadata = dict(context.invocation_metadata()) - if "user-agent" in metadata: - attributes["rpc.user_agent"] = metadata["user-agent"] - - # Split up the peer to keep with how other telemetry sources - # do it. This looks like: - # * ipv6:[::1]:57284 - # * ipv4:127.0.0.1:57284 - # * ipv4:10.2.1.1:57284,127.0.0.1:57284 - # - try: - host, port = ( - context.peer().split(",")[0].split(":", 1)[1].rsplit(":", 1) - ) - - # other telemetry sources convert this, so we will too - if host in ("[::1]", "127.0.0.1"): - host = "localhost" - - attributes.update({"net.peer.name": host, "net.peer.port": port}) - except IndexError: - logger.warning("Failed to parse peer address '%s'", context.peer()) - - return self._tracer.start_as_current_span( - name=handler_call_details.method, - kind=trace.SpanKind.SERVER, - attributes=attributes, - ) - - def intercept_service(self, continuation, handler_call_details): - def telemetry_wrapper(behavior, request_streaming, response_streaming): - def telemetry_interceptor(request_or_iterator, context): - - with self._set_remote_context(context): - with self._start_span( - handler_call_details, context - ) as span: - # wrap the context - context = _OpenTelemetryServicerContext(context, span) - - # And now we run the actual RPC. - try: - return behavior(request_or_iterator, context) - except Exception as error: - # Bare exceptions are likely to be gRPC aborts, which - # we handle in our context wrapper. - # Here, we're interested in uncaught exceptions. - # pylint:disable=unidiomatic-typecheck - if type(error) != Exception: - span.record_exception(error) - raise error - - return telemetry_interceptor - - return _wrap_rpc_behavior( - continuation(handler_call_details), telemetry_wrapper - ) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py deleted file mode 100644 index 8cf1f957c3..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Internal utilities.""" - -from contextlib import contextmanager -from time import time - -import grpc - - -class RpcInfo: - def __init__( - self, - full_method=None, - metadata=None, - timeout=None, - request=None, - response=None, - error=None, - ): - self.full_method = full_method - self.metadata = metadata - self.timeout = timeout - self.request = request - self.response = response - self.error = error - - -class TimedMetricRecorder: - def __init__(self, meter, span_kind): - self._meter = meter - service_name = "grpcio" - self._span_kind = span_kind - - if self._meter: - self._duration = self._meter.create_valuerecorder( - name="{}/{}/duration".format(service_name, span_kind), - description="Duration of grpc requests to the server", - unit="ms", - value_type=float, - ) - self._error_count = self._meter.create_counter( - name="{}/{}/errors".format(service_name, span_kind), - description="Number of errors that were returned from the server", - unit="1", - value_type=int, - ) - self._bytes_in = self._meter.create_counter( - name="{}/{}/bytes_in".format(service_name, span_kind), - description="Number of bytes received from the server", - unit="by", - value_type=int, - ) - self._bytes_out = self._meter.create_counter( - name="{}/{}/bytes_out".format(service_name, span_kind), - description="Number of bytes sent out through gRPC", - unit="by", - value_type=int, - ) - - def record_bytes_in(self, bytes_in, method): - if self._meter: - labels = {"method": method} - self._bytes_in.add(bytes_in, labels) - - def record_bytes_out(self, bytes_out, method): - if self._meter: - labels = {"method": method} - self._bytes_out.add(bytes_out, labels) - - @contextmanager - def record_latency(self, method): - start_time = time() - labels = {"method": method, "status_code": grpc.StatusCode.OK} - try: - yield labels - except grpc.RpcError as exc: - if self._meter: - # pylint: disable=no-member - labels["status_code"] = exc.code() - self._error_count.add(1, labels) - labels["error"] = True - raise - finally: - if self._meter: - if "error" not in labels: - labels["error"] = False - elapsed_time = (time() - start_time) * 1000 - self._duration.record(elapsed_time, labels) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/__init__.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/__init__.py deleted file mode 100644 index d5e2549bab..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/__init__.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint:disable=import-outside-toplevel -# pylint:disable=import-self -# pylint:disable=no-name-in-module - -import abc - - -class UnaryClientInfo(abc.ABC): - """Consists of various information about a unary RPC on the - invocation-side. - - Attributes: - full_method: A string of the full RPC method, i.e., - /package.service/method. - timeout: The length of time in seconds to wait for the computation to - terminate or be cancelled, or None if this method should block until - the computation is terminated or is cancelled no matter how long that - takes. - """ - - -class StreamClientInfo(abc.ABC): - """Consists of various information about a stream RPC on the - invocation-side. - - Attributes: - full_method: A string of the full RPC method, i.e., - /package.service/method. - is_client_stream: Indicates whether the RPC is client-streaming. - is_server_stream: Indicates whether the RPC is server-streaming. - timeout: The length of time in seconds to wait for the computation to - terminate or be cancelled, or None if this method should block until - the computation is terminated or is cancelled no matter how long that - takes. - """ - - -class UnaryClientInterceptor(abc.ABC): - """Affords intercepting unary-unary RPCs on the invocation-side.""" - - @abc.abstractmethod - def intercept_unary(self, request, metadata, client_info, invoker): - """Intercepts unary-unary RPCs on the invocation-side. - - Args: - request: The request value for the RPC. - metadata: Optional :term:`metadata` to be transmitted to the - service-side of the RPC. - client_info: A UnaryClientInfo containing various information about - the RPC. - invoker: The handler to complete the RPC on the client. It is the - interceptor's responsibility to call it. - - Returns: - The result from calling invoker(request, metadata). - """ - raise NotImplementedError() - - -class StreamClientInterceptor(abc.ABC): - """Affords intercepting stream RPCs on the invocation-side.""" - - @abc.abstractmethod - def intercept_stream( - self, request_or_iterator, metadata, client_info, invoker - ): - """Intercepts stream RPCs on the invocation-side. - - Args: - request_or_iterator: The request value for the RPC if - `client_info.is_client_stream` is `false`; otherwise, an iterator of - request values. - metadata: Optional :term:`metadata` to be transmitted to the service-side - of the RPC. - client_info: A StreamClientInfo containing various information about - the RPC. - invoker: The handler to complete the RPC on the client. It is the - interceptor's responsibility to call it. - - Returns: - The result from calling invoker(metadata). - """ - raise NotImplementedError() - - -def intercept_channel(channel, *interceptors): - """Creates an intercepted channel. - - Args: - channel: A Channel. - interceptors: Zero or more UnaryClientInterceptors or - StreamClientInterceptors - - Returns: - A Channel. - - Raises: - TypeError: If an interceptor derives from neither UnaryClientInterceptor - nor StreamClientInterceptor. - """ - from . import _interceptor - - return _interceptor.intercept_channel(channel, *interceptors) - - -__all__ = ( - "UnaryClientInterceptor", - "StreamClientInfo", - "StreamClientInterceptor", - "intercept_channel", -) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/_interceptor.py deleted file mode 100644 index b9f74fff80..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext/_interceptor.py +++ /dev/null @@ -1,254 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint:disable=relative-beyond-top-level -# pylint:disable=arguments-differ -# pylint:disable=no-member -# pylint:disable=signature-differs - -"""Implementation of gRPC Python interceptors.""" - - -import collections - -import grpc - -from .. import grpcext - - -class _UnaryClientInfo( - collections.namedtuple("_UnaryClientInfo", ("full_method", "timeout")) -): - pass - - -class _StreamClientInfo( - collections.namedtuple( - "_StreamClientInfo", - ("full_method", "is_client_stream", "is_server_stream", "timeout"), - ) -): - pass - - -class _InterceptorUnaryUnaryMultiCallable(grpc.UnaryUnaryMultiCallable): - def __init__(self, method, base_callable, interceptor): - self._method = method - self._base_callable = base_callable - self._interceptor = interceptor - - def __call__(self, request, timeout=None, metadata=None, credentials=None): - def invoker(request, metadata): - return self._base_callable(request, timeout, metadata, credentials) - - client_info = _UnaryClientInfo(self._method, timeout) - return self._interceptor.intercept_unary( - request, metadata, client_info, invoker - ) - - def with_call( - self, request, timeout=None, metadata=None, credentials=None - ): - def invoker(request, metadata): - return self._base_callable.with_call( - request, timeout, metadata, credentials - ) - - client_info = _UnaryClientInfo(self._method, timeout) - return self._interceptor.intercept_unary( - request, metadata, client_info, invoker - ) - - def future(self, request, timeout=None, metadata=None, credentials=None): - def invoker(request, metadata): - return self._base_callable.future( - request, timeout, metadata, credentials - ) - - client_info = _UnaryClientInfo(self._method, timeout) - return self._interceptor.intercept_unary( - request, metadata, client_info, invoker - ) - - -class _InterceptorUnaryStreamMultiCallable(grpc.UnaryStreamMultiCallable): - def __init__(self, method, base_callable, interceptor): - self._method = method - self._base_callable = base_callable - self._interceptor = interceptor - - def __call__(self, request, timeout=None, metadata=None, credentials=None): - def invoker(request, metadata): - return self._base_callable(request, timeout, metadata, credentials) - - client_info = _StreamClientInfo(self._method, False, True, timeout) - return self._interceptor.intercept_stream( - request, metadata, client_info, invoker - ) - - -class _InterceptorStreamUnaryMultiCallable(grpc.StreamUnaryMultiCallable): - def __init__(self, method, base_callable, interceptor): - self._method = method - self._base_callable = base_callable - self._interceptor = interceptor - - def __call__( - self, request_iterator, timeout=None, metadata=None, credentials=None - ): - def invoker(request_iterator, metadata): - return self._base_callable( - request_iterator, timeout, metadata, credentials - ) - - client_info = _StreamClientInfo(self._method, True, False, timeout) - return self._interceptor.intercept_stream( - request_iterator, metadata, client_info, invoker - ) - - def with_call( - self, request_iterator, timeout=None, metadata=None, credentials=None - ): - def invoker(request_iterator, metadata): - return self._base_callable.with_call( - request_iterator, timeout, metadata, credentials - ) - - client_info = _StreamClientInfo(self._method, True, False, timeout) - return self._interceptor.intercept_stream( - request_iterator, metadata, client_info, invoker - ) - - def future( - self, request_iterator, timeout=None, metadata=None, credentials=None - ): - def invoker(request_iterator, metadata): - return self._base_callable.future( - request_iterator, timeout, metadata, credentials - ) - - client_info = _StreamClientInfo(self._method, True, False, timeout) - return self._interceptor.intercept_stream( - request_iterator, metadata, client_info, invoker - ) - - -class _InterceptorStreamStreamMultiCallable(grpc.StreamStreamMultiCallable): - def __init__(self, method, base_callable, interceptor): - self._method = method - self._base_callable = base_callable - self._interceptor = interceptor - - def __call__( - self, request_iterator, timeout=None, metadata=None, credentials=None - ): - def invoker(request_iterator, metadata): - return self._base_callable( - request_iterator, timeout, metadata, credentials - ) - - client_info = _StreamClientInfo(self._method, True, True, timeout) - return self._interceptor.intercept_stream( - request_iterator, metadata, client_info, invoker - ) - - -class _InterceptorChannel(grpc.Channel): - def __init__(self, channel, interceptor): - self._channel = channel - self._interceptor = interceptor - - def subscribe(self, *args, **kwargs): - self._channel.subscribe(*args, **kwargs) - - def unsubscribe(self, *args, **kwargs): - self._channel.unsubscribe(*args, **kwargs) - - def unary_unary( - self, method, request_serializer=None, response_deserializer=None - ): - base_callable = self._channel.unary_unary( - method, request_serializer, response_deserializer - ) - if isinstance(self._interceptor, grpcext.UnaryClientInterceptor): - return _InterceptorUnaryUnaryMultiCallable( - method, base_callable, self._interceptor - ) - return base_callable - - def unary_stream( - self, method, request_serializer=None, response_deserializer=None - ): - base_callable = self._channel.unary_stream( - method, request_serializer, response_deserializer - ) - if isinstance(self._interceptor, grpcext.StreamClientInterceptor): - return _InterceptorUnaryStreamMultiCallable( - method, base_callable, self._interceptor - ) - return base_callable - - def stream_unary( - self, method, request_serializer=None, response_deserializer=None - ): - base_callable = self._channel.stream_unary( - method, request_serializer, response_deserializer - ) - if isinstance(self._interceptor, grpcext.StreamClientInterceptor): - return _InterceptorStreamUnaryMultiCallable( - method, base_callable, self._interceptor - ) - return base_callable - - def stream_stream( - self, method, request_serializer=None, response_deserializer=None - ): - base_callable = self._channel.stream_stream( - method, request_serializer, response_deserializer - ) - if isinstance(self._interceptor, grpcext.StreamClientInterceptor): - return _InterceptorStreamStreamMultiCallable( - method, base_callable, self._interceptor - ) - return base_callable - - def close(self): - if not hasattr(self._channel, "close"): - raise RuntimeError( - "close() is not supported with the installed version of grpcio" - ) - self._channel.close() - - def __enter__(self): - """Enters the runtime context related to the channel object.""" - raise NotImplementedError() - - def __exit__(self, exc_type, exc_val, exc_tb): - """Exits the runtime context related to the channel object.""" - raise NotImplementedError() - - -def intercept_channel(channel, *interceptors): - result = channel - for interceptor in interceptors: - if not isinstance( - interceptor, grpcext.UnaryClientInterceptor - ) and not isinstance(interceptor, grpcext.StreamClientInterceptor): - raise TypeError( - "interceptor must be either a " - "grpcext.UnaryClientInterceptor or a " - "grpcext.StreamClientInterceptor" - ) - result = _InterceptorChannel(result, interceptor) - return result diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/_client.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/_client.py deleted file mode 100644 index 43310b5f65..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/_client.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .protobuf.test_server_pb2 import Request - -CLIENT_ID = 1 - - -def simple_method(stub, error=False): - request = Request( - client_id=CLIENT_ID, request_data="error" if error else "data" - ) - stub.SimpleMethod(request) - - -def client_streaming_method(stub, error=False): - # create a generator - def request_messages(): - for _ in range(5): - request = Request( - client_id=CLIENT_ID, request_data="error" if error else "data" - ) - yield request - - stub.ClientStreamingMethod(request_messages()) - - -def server_streaming_method(stub, error=False): - request = Request( - client_id=CLIENT_ID, request_data="error" if error else "data" - ) - response_iterator = stub.ServerStreamingMethod(request) - list(response_iterator) - - -def bidirectional_streaming_method(stub, error=False): - def request_messages(): - for _ in range(5): - request = Request( - client_id=CLIENT_ID, request_data="error" if error else "data" - ) - yield request - - response_iterator = stub.BidirectionalStreamingMethod(request_messages()) - - list(response_iterator) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/_server.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/_server.py deleted file mode 100644 index a4e1c266b8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/_server.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from concurrent import futures - -import grpc - -from .protobuf import test_server_pb2, test_server_pb2_grpc - -SERVER_ID = 1 - - -class TestServer(test_server_pb2_grpc.GRPCTestServerServicer): - def SimpleMethod(self, request, context): - if request.request_data == "error": - context.set_code(grpc.StatusCode.INVALID_ARGUMENT) - return test_server_pb2.Response() - response = test_server_pb2.Response( - server_id=SERVER_ID, response_data="data" - ) - return response - - def ClientStreamingMethod(self, request_iterator, context): - data = list(request_iterator) - if data[0].request_data == "error": - context.set_code(grpc.StatusCode.INVALID_ARGUMENT) - return test_server_pb2.Response() - response = test_server_pb2.Response( - server_id=SERVER_ID, response_data="data" - ) - return response - - def ServerStreamingMethod(self, request, context): - if request.request_data == "error": - - context.abort( - code=grpc.StatusCode.INVALID_ARGUMENT, - details="server stream error", - ) - return test_server_pb2.Response() - - # create a generator - def response_messages(): - for _ in range(5): - response = test_server_pb2.Response( - server_id=SERVER_ID, response_data="data" - ) - yield response - - return response_messages() - - def BidirectionalStreamingMethod(self, request_iterator, context): - data = list(request_iterator) - if data[0].request_data == "error": - context.abort( - code=grpc.StatusCode.INVALID_ARGUMENT, - details="bidirectional error", - ) - return - - for _ in range(5): - yield test_server_pb2.Response( - server_id=SERVER_ID, response_data="data" - ) - - -def create_test_server(port): - server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) - - test_server_pb2_grpc.add_GRPCTestServerServicer_to_server( - TestServer(), server - ) - - server.add_insecure_port("localhost:{}".format(port)) - - return server diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server.proto b/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server.proto deleted file mode 100644 index 790a7675de..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server.proto +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2019 gRPC 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. -syntax = "proto3"; - -message Request { - int64 client_id = 1; - string request_data = 2; -} - -message Response { - int64 server_id = 1; - string response_data = 2; -} - -service GRPCTestServer { - rpc SimpleMethod (Request) returns (Response); - - rpc ClientStreamingMethod (stream Request) returns (Response); - - rpc ServerStreamingMethod (Request) returns (stream Response); - - rpc BidirectionalStreamingMethod (stream Request) returns (stream Response); -} diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2.py deleted file mode 100644 index 735206f850..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2.py +++ /dev/null @@ -1,215 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: test_server.proto - -from google.protobuf import descriptor as _descriptor -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.FileDescriptor( - name="test_server.proto", - package="", - syntax="proto3", - serialized_options=None, - serialized_pb=b'\n\x11test_server.proto"2\n\x07Request\x12\x11\n\tclient_id\x18\x01 \x01(\x03\x12\x14\n\x0crequest_data\x18\x02 \x01(\t"4\n\x08Response\x12\x11\n\tserver_id\x18\x01 \x01(\x03\x12\x15\n\rresponse_data\x18\x02 \x01(\t2\xce\x01\n\x0eGRPCTestServer\x12#\n\x0cSimpleMethod\x12\x08.Request\x1a\t.Response\x12.\n\x15\x43lientStreamingMethod\x12\x08.Request\x1a\t.Response(\x01\x12.\n\x15ServerStreamingMethod\x12\x08.Request\x1a\t.Response0\x01\x12\x37\n\x1c\x42idirectionalStreamingMethod\x12\x08.Request\x1a\t.Response(\x01\x30\x01\x62\x06proto3', -) - - -_REQUEST = _descriptor.Descriptor( - name="Request", - full_name="Request", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="client_id", - full_name="Request.client_id", - index=0, - number=1, - type=3, - cpp_type=2, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="request_data", - full_name="Request.request_data", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=b"".decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=21, - serialized_end=71, -) - - -_RESPONSE = _descriptor.Descriptor( - name="Response", - full_name="Response", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="server_id", - full_name="Response.server_id", - index=0, - number=1, - type=3, - cpp_type=2, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="response_data", - full_name="Response.response_data", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=b"".decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=73, - serialized_end=125, -) - -DESCRIPTOR.message_types_by_name["Request"] = _REQUEST -DESCRIPTOR.message_types_by_name["Response"] = _RESPONSE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -Request = _reflection.GeneratedProtocolMessageType( - "Request", - (_message.Message,), - { - "DESCRIPTOR": _REQUEST, - "__module__": "test_server_pb2" - # @@protoc_insertion_point(class_scope:Request) - }, -) -_sym_db.RegisterMessage(Request) - -Response = _reflection.GeneratedProtocolMessageType( - "Response", - (_message.Message,), - { - "DESCRIPTOR": _RESPONSE, - "__module__": "test_server_pb2" - # @@protoc_insertion_point(class_scope:Response) - }, -) -_sym_db.RegisterMessage(Response) - - -_GRPCTESTSERVER = _descriptor.ServiceDescriptor( - name="GRPCTestServer", - full_name="GRPCTestServer", - file=DESCRIPTOR, - index=0, - serialized_options=None, - serialized_start=128, - serialized_end=334, - methods=[ - _descriptor.MethodDescriptor( - name="SimpleMethod", - full_name="GRPCTestServer.SimpleMethod", - index=0, - containing_service=None, - input_type=_REQUEST, - output_type=_RESPONSE, - serialized_options=None, - ), - _descriptor.MethodDescriptor( - name="ClientStreamingMethod", - full_name="GRPCTestServer.ClientStreamingMethod", - index=1, - containing_service=None, - input_type=_REQUEST, - output_type=_RESPONSE, - serialized_options=None, - ), - _descriptor.MethodDescriptor( - name="ServerStreamingMethod", - full_name="GRPCTestServer.ServerStreamingMethod", - index=2, - containing_service=None, - input_type=_REQUEST, - output_type=_RESPONSE, - serialized_options=None, - ), - _descriptor.MethodDescriptor( - name="BidirectionalStreamingMethod", - full_name="GRPCTestServer.BidirectionalStreamingMethod", - index=3, - containing_service=None, - input_type=_REQUEST, - output_type=_RESPONSE, - serialized_options=None, - ), - ], -) -_sym_db.RegisterServiceDescriptor(_GRPCTESTSERVER) - -DESCRIPTOR.services_by_name["GRPCTestServer"] = _GRPCTESTSERVER - -# @@protoc_insertion_point(module_scope) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2_grpc.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2_grpc.py deleted file mode 100644 index d0a6fd5184..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2_grpc.py +++ /dev/null @@ -1,205 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -import grpc - -from tests.protobuf import test_server_pb2 as test__server__pb2 - - -class GRPCTestServerStub(object): - """Missing associated documentation comment in .proto file""" - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.SimpleMethod = channel.unary_unary( - "/GRPCTestServer/SimpleMethod", - request_serializer=test__server__pb2.Request.SerializeToString, - response_deserializer=test__server__pb2.Response.FromString, - ) - self.ClientStreamingMethod = channel.stream_unary( - "/GRPCTestServer/ClientStreamingMethod", - request_serializer=test__server__pb2.Request.SerializeToString, - response_deserializer=test__server__pb2.Response.FromString, - ) - self.ServerStreamingMethod = channel.unary_stream( - "/GRPCTestServer/ServerStreamingMethod", - request_serializer=test__server__pb2.Request.SerializeToString, - response_deserializer=test__server__pb2.Response.FromString, - ) - self.BidirectionalStreamingMethod = channel.stream_stream( - "/GRPCTestServer/BidirectionalStreamingMethod", - request_serializer=test__server__pb2.Request.SerializeToString, - response_deserializer=test__server__pb2.Response.FromString, - ) - - -class GRPCTestServerServicer(object): - """Missing associated documentation comment in .proto file""" - - def SimpleMethod(self, request, context): - """Missing associated documentation comment in .proto file""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def ClientStreamingMethod(self, request_iterator, context): - """Missing associated documentation comment in .proto file""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def ServerStreamingMethod(self, request, context): - """Missing associated documentation comment in .proto file""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def BidirectionalStreamingMethod(self, request_iterator, context): - """Missing associated documentation comment in .proto file""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - -def add_GRPCTestServerServicer_to_server(servicer, server): - rpc_method_handlers = { - "SimpleMethod": grpc.unary_unary_rpc_method_handler( - servicer.SimpleMethod, - request_deserializer=test__server__pb2.Request.FromString, - response_serializer=test__server__pb2.Response.SerializeToString, - ), - "ClientStreamingMethod": grpc.stream_unary_rpc_method_handler( - servicer.ClientStreamingMethod, - request_deserializer=test__server__pb2.Request.FromString, - response_serializer=test__server__pb2.Response.SerializeToString, - ), - "ServerStreamingMethod": grpc.unary_stream_rpc_method_handler( - servicer.ServerStreamingMethod, - request_deserializer=test__server__pb2.Request.FromString, - response_serializer=test__server__pb2.Response.SerializeToString, - ), - "BidirectionalStreamingMethod": grpc.stream_stream_rpc_method_handler( - servicer.BidirectionalStreamingMethod, - request_deserializer=test__server__pb2.Request.FromString, - response_serializer=test__server__pb2.Response.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - "GRPCTestServer", rpc_method_handlers - ) - server.add_generic_rpc_handlers((generic_handler,)) - - -# This class is part of an EXPERIMENTAL API. -class GRPCTestServer(object): - """Missing associated documentation comment in .proto file""" - - @staticmethod - def SimpleMethod( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( - request, - target, - "/GRPCTestServer/SimpleMethod", - test__server__pb2.Request.SerializeToString, - test__server__pb2.Response.FromString, - options, - channel_credentials, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - ) - - @staticmethod - def ClientStreamingMethod( - request_iterator, - target, - options=(), - channel_credentials=None, - call_credentials=None, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.stream_unary( - request_iterator, - target, - "/GRPCTestServer/ClientStreamingMethod", - test__server__pb2.Request.SerializeToString, - test__server__pb2.Response.FromString, - options, - channel_credentials, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - ) - - @staticmethod - def ServerStreamingMethod( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_stream( - request, - target, - "/GRPCTestServer/ServerStreamingMethod", - test__server__pb2.Request.SerializeToString, - test__server__pb2.Response.FromString, - options, - channel_credentials, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - ) - - @staticmethod - def BidirectionalStreamingMethod( - request_iterator, - target, - options=(), - channel_credentials=None, - call_credentials=None, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.stream_stream( - request_iterator, - target, - "/GRPCTestServer/BidirectionalStreamingMethod", - test__server__pb2.Request.SerializeToString, - test__server__pb2.Response.FromString, - options, - channel_credentials, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - ) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py deleted file mode 100644 index f351c6fdd4..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py +++ /dev/null @@ -1,295 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import grpc - -import opentelemetry.instrumentation.grpc -from opentelemetry import trace -from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient -from opentelemetry.sdk.metrics.export.aggregate import ( - MinMaxSumCountAggregator, - SumAggregator, -) -from opentelemetry.test.test_base import TestBase -from tests.protobuf import test_server_pb2_grpc - -from ._client import ( - bidirectional_streaming_method, - client_streaming_method, - server_streaming_method, - simple_method, -) -from ._server import create_test_server - - -class TestClientProto(TestBase): - def setUp(self): - super().setUp() - GrpcInstrumentorClient().instrument( - exporter=self.memory_metrics_exporter - ) - self.server = create_test_server(25565) - self.server.start() - self.channel = grpc.insecure_channel("localhost:25565") - self._stub = test_server_pb2_grpc.GRPCTestServerStub(self.channel) - - def tearDown(self): - super().tearDown() - GrpcInstrumentorClient().uninstrument() - self.memory_metrics_exporter.clear() - self.server.stop(None) - self.channel.close() - - def _verify_success_records(self, num_bytes_out, num_bytes_in, method): - # pylint: disable=protected-access,no-member - self.channel._interceptor.controller.tick() - records = self.memory_metrics_exporter.get_exported_metrics() - self.assertEqual(len(records), 3) - - bytes_out = None - bytes_in = None - duration = None - - for record in records: - if record.instrument.name == "grpcio/client/duration": - duration = record - elif record.instrument.name == "grpcio/client/bytes_out": - bytes_out = record - elif record.instrument.name == "grpcio/client/bytes_in": - bytes_in = record - - self.assertIsNotNone(bytes_out) - self.assertEqual(bytes_out.instrument.name, "grpcio/client/bytes_out") - self.assertEqual(bytes_out.labels, (("method", method),)) - - self.assertIsNotNone(bytes_in) - self.assertEqual(bytes_in.instrument.name, "grpcio/client/bytes_in") - self.assertEqual(bytes_in.labels, (("method", method),)) - - self.assertIsNotNone(duration) - self.assertEqual(duration.instrument.name, "grpcio/client/duration") - self.assertEqual( - duration.labels, - ( - ("error", False), - ("method", method), - ("status_code", grpc.StatusCode.OK), - ), - ) - - self.assertEqual(type(bytes_out.aggregator), SumAggregator) - self.assertEqual(type(bytes_in.aggregator), SumAggregator) - self.assertEqual(type(duration.aggregator), MinMaxSumCountAggregator) - - self.assertEqual(bytes_out.aggregator.checkpoint, num_bytes_out) - self.assertEqual(bytes_in.aggregator.checkpoint, num_bytes_in) - - self.assertEqual(duration.aggregator.checkpoint.count, 1) - self.assertGreaterEqual(duration.aggregator.checkpoint.sum, 0) - - def test_unary_unary(self): - simple_method(self._stub) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - - self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") - self.assertIs(span.kind, trace.SpanKind.CLIENT) - - # Check version and name in span's instrumentation info - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.grpc - ) - - self._verify_success_records(8, 8, "/GRPCTestServer/SimpleMethod") - - def test_unary_stream(self): - server_streaming_method(self._stub) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - - self.assertEqual(span.name, "/GRPCTestServer/ServerStreamingMethod") - self.assertIs(span.kind, trace.SpanKind.CLIENT) - - # Check version and name in span's instrumentation info - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.grpc - ) - - self._verify_success_records( - 8, 40, "/GRPCTestServer/ServerStreamingMethod" - ) - - def test_stream_unary(self): - client_streaming_method(self._stub) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - - self.assertEqual(span.name, "/GRPCTestServer/ClientStreamingMethod") - self.assertIs(span.kind, trace.SpanKind.CLIENT) - - # Check version and name in span's instrumentation info - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.grpc - ) - - self._verify_success_records( - 40, 8, "/GRPCTestServer/ClientStreamingMethod" - ) - - def test_stream_stream(self): - bidirectional_streaming_method(self._stub) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - - self.assertEqual( - span.name, "/GRPCTestServer/BidirectionalStreamingMethod" - ) - self.assertIs(span.kind, trace.SpanKind.CLIENT) - - # Check version and name in span's instrumentation info - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.grpc - ) - - self._verify_success_records( - 40, 40, "/GRPCTestServer/BidirectionalStreamingMethod" - ) - - def _verify_error_records(self, method): - # pylint: disable=protected-access,no-member - self.channel._interceptor.controller.tick() - records = self.memory_metrics_exporter.get_exported_metrics() - self.assertEqual(len(records), 3) - - bytes_out = None - errors = None - duration = None - - for record in records: - if record.instrument.name == "grpcio/client/duration": - duration = record - elif record.instrument.name == "grpcio/client/bytes_out": - bytes_out = record - elif record.instrument.name == "grpcio/client/errors": - errors = record - - self.assertIsNotNone(bytes_out) - self.assertIsNotNone(errors) - self.assertIsNotNone(duration) - - self.assertEqual(errors.instrument.name, "grpcio/client/errors") - self.assertEqual( - errors.labels, - ( - ("method", method), - ("status_code", grpc.StatusCode.INVALID_ARGUMENT), - ), - ) - self.assertEqual(errors.aggregator.checkpoint, 1) - - self.assertEqual( - duration.labels, - ( - ("error", True), - ("method", method), - ("status_code", grpc.StatusCode.INVALID_ARGUMENT), - ), - ) - - def test_error_simple(self): - with self.assertRaises(grpc.RpcError): - simple_method(self._stub, error=True) - - self._verify_error_records("/GRPCTestServer/SimpleMethod") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertIs( - span.status.status_code, trace.status.StatusCode.ERROR, - ) - - def test_error_stream_unary(self): - with self.assertRaises(grpc.RpcError): - client_streaming_method(self._stub, error=True) - - self._verify_error_records("/GRPCTestServer/ClientStreamingMethod") - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertIs( - span.status.status_code, trace.status.StatusCode.ERROR, - ) - - def test_error_unary_stream(self): - with self.assertRaises(grpc.RpcError): - server_streaming_method(self._stub, error=True) - - self._verify_error_records("/GRPCTestServer/ServerStreamingMethod") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertIs( - span.status.status_code, trace.status.StatusCode.ERROR, - ) - - def test_error_stream_stream(self): - with self.assertRaises(grpc.RpcError): - bidirectional_streaming_method(self._stub, error=True) - - self._verify_error_records( - "/GRPCTestServer/BidirectionalStreamingMethod" - ) - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertIs( - span.status.status_code, trace.status.StatusCode.ERROR, - ) - - -class TestClientNoMetrics(TestBase): - def setUp(self): - super().setUp() - GrpcInstrumentorClient().instrument() - self.server = create_test_server(25565) - self.server.start() - self.channel = grpc.insecure_channel("localhost:25565") - self._stub = test_server_pb2_grpc.GRPCTestServerStub(self.channel) - - def tearDown(self): - super().tearDown() - GrpcInstrumentorClient().uninstrument() - self.memory_metrics_exporter.clear() - self.server.stop(None) - - def test_unary_unary(self): - simple_method(self._stub) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - - self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") - self.assertIs(span.kind, trace.SpanKind.CLIENT) - - # Check version and name in span's instrumentation info - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.grpc - ) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_server_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_server_interceptor.py deleted file mode 100644 index 13b535d841..0000000000 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_server_interceptor.py +++ /dev/null @@ -1,295 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint:disable=unused-argument -# pylint:disable=no-self-use - -import threading -from concurrent import futures - -import grpc - -import opentelemetry.instrumentation.grpc -from opentelemetry import trace -from opentelemetry.instrumentation.grpc import ( - GrpcInstrumentorServer, - server_interceptor, -) -from opentelemetry.sdk import trace as trace_sdk -from opentelemetry.test.test_base import TestBase - - -class UnaryUnaryMethodHandler(grpc.RpcMethodHandler): - def __init__(self, handler): - self.request_streaming = False - self.response_streaming = False - self.request_deserializer = None - self.response_serializer = None - self.unary_unary = handler - self.unary_stream = None - self.stream_unary = None - self.stream_stream = None - - -class UnaryUnaryRpcHandler(grpc.GenericRpcHandler): - def __init__(self, handler): - self._unary_unary_handler = handler - - def service(self, handler_call_details): - return UnaryUnaryMethodHandler(self._unary_unary_handler) - - -class TestOpenTelemetryServerInterceptor(TestBase): - def test_instrumentor(self): - def handler(request, context): - return b"" - - grpc_server_instrumentor = GrpcInstrumentorServer() - grpc_server_instrumentor.instrument() - server = grpc.server( - futures.ThreadPoolExecutor(max_workers=1), - options=(("grpc.so_reuseport", 0),), - ) - - server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) - - port = server.add_insecure_port("[::]:0") - channel = grpc.insecure_channel("localhost:{:d}".format(port)) - - try: - server.start() - channel.unary_unary("test")(b"test") - finally: - server.stop(None) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - self.assertEqual(span.name, "test") - self.assertIs(span.kind, trace.SpanKind.SERVER) - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.grpc - ) - grpc_server_instrumentor.uninstrument() - - def test_uninstrument(self): - def handler(request, context): - return b"" - - grpc_server_instrumentor = GrpcInstrumentorServer() - grpc_server_instrumentor.instrument() - grpc_server_instrumentor.uninstrument() - server = grpc.server( - futures.ThreadPoolExecutor(max_workers=1), - options=(("grpc.so_reuseport", 0),), - ) - - server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) - - port = server.add_insecure_port("[::]:0") - channel = grpc.insecure_channel("localhost:{:d}".format(port)) - - try: - server.start() - channel.unary_unary("test")(b"test") - finally: - server.stop(None) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 0) - - def test_create_span(self): - """Check that the interceptor wraps calls with spans server-side.""" - - # Intercept gRPC calls... - interceptor = server_interceptor() - - # No-op RPC handler - def handler(request, context): - return b"" - - server = grpc.server( - futures.ThreadPoolExecutor(max_workers=1), - options=(("grpc.so_reuseport", 0),), - interceptors=[interceptor], - ) - - server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) - - port = server.add_insecure_port("[::]:0") - channel = grpc.insecure_channel("localhost:{:d}".format(port)) - - try: - server.start() - channel.unary_unary("")(b"") - finally: - server.stop(None) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - - self.assertEqual(span.name, "") - self.assertIs(span.kind, trace.SpanKind.SERVER) - - # Check version and name in span's instrumentation info - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.grpc - ) - - def test_span_lifetime(self): - """Check that the span is active for the duration of the call.""" - - interceptor = server_interceptor() - - # To capture the current span at the time the handler is called - active_span_in_handler = None - - def handler(request, context): - nonlocal active_span_in_handler - active_span_in_handler = trace.get_current_span() - return b"" - - server = grpc.server( - futures.ThreadPoolExecutor(max_workers=1), - options=(("grpc.so_reuseport", 0),), - interceptors=[interceptor], - ) - server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) - - port = server.add_insecure_port("[::]:0") - channel = grpc.insecure_channel("localhost:{:d}".format(port)) - - active_span_before_call = trace.get_current_span() - try: - server.start() - channel.unary_unary("")(b"") - finally: - server.stop(None) - active_span_after_call = trace.get_current_span() - - self.assertEqual(active_span_before_call, trace.INVALID_SPAN) - self.assertEqual(active_span_after_call, trace.INVALID_SPAN) - self.assertIsInstance(active_span_in_handler, trace_sdk.Span) - self.assertIsNone(active_span_in_handler.parent) - - def test_sequential_server_spans(self): - """Check that sequential RPCs get separate server spans.""" - - interceptor = server_interceptor() - - # Capture the currently active span in each thread - active_spans_in_handler = [] - - def handler(request, context): - active_spans_in_handler.append(trace.get_current_span()) - return b"" - - server = grpc.server( - futures.ThreadPoolExecutor(max_workers=1), - options=(("grpc.so_reuseport", 0),), - interceptors=[interceptor], - ) - server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) - - port = server.add_insecure_port("[::]:0") - channel = grpc.insecure_channel("localhost:{:d}".format(port)) - - try: - server.start() - channel.unary_unary("")(b"") - channel.unary_unary("")(b"") - finally: - server.stop(None) - - self.assertEqual(len(active_spans_in_handler), 2) - # pylint:disable=unbalanced-tuple-unpacking - span1, span2 = active_spans_in_handler - # Spans should belong to separate traces, and each should be a root - # span - self.assertNotEqual(span1.context.span_id, span2.context.span_id) - self.assertNotEqual(span1.context.trace_id, span2.context.trace_id) - self.assertIsNone(span1.parent) - self.assertIsNone(span1.parent) - - def test_concurrent_server_spans(self): - """Check that concurrent RPC calls don't interfere with each other. - - This is the same check as test_sequential_server_spans except that the - RPCs are concurrent. Two handlers are invoked at the same time on two - separate threads. Each one should see a different active span and - context. - """ - - interceptor = server_interceptor() - - # Capture the currently active span in each thread - active_spans_in_handler = [] - latch = get_latch(2) - - def handler(request, context): - latch() - active_spans_in_handler.append(trace.get_current_span()) - return b"" - - server = grpc.server( - futures.ThreadPoolExecutor(max_workers=2), - options=(("grpc.so_reuseport", 0),), - interceptors=[interceptor], - ) - server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) - - port = server.add_insecure_port("[::]:0") - channel = grpc.insecure_channel("localhost:{:d}".format(port)) - - try: - server.start() - # Interleave calls so spans are active on each thread at the same - # time - with futures.ThreadPoolExecutor(max_workers=2) as tpe: - f1 = tpe.submit(channel.unary_unary(""), b"") - f2 = tpe.submit(channel.unary_unary(""), b"") - futures.wait((f1, f2)) - finally: - server.stop(None) - - self.assertEqual(len(active_spans_in_handler), 2) - # pylint:disable=unbalanced-tuple-unpacking - span1, span2 = active_spans_in_handler - # Spans should belong to separate traces, and each should be a root - # span - self.assertNotEqual(span1.context.span_id, span2.context.span_id) - self.assertNotEqual(span1.context.trace_id, span2.context.trace_id) - self.assertIsNone(span1.parent) - self.assertIsNone(span1.parent) - - -def get_latch(num): - """Get a countdown latch function for use in n threads.""" - cv = threading.Condition() - count = 0 - - def countdown_latch(): - """Block until n-1 other threads have called.""" - nonlocal count - cv.acquire() - count += 1 - cv.notify() - cv.release() - cv.acquire() - while count < num: - cv.wait() - cv.release() - - return countdown_latch diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md deleted file mode 100644 index b1f9ba50e1..0000000000 --- a/instrumentation/opentelemetry-instrumentation-jinja2/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-jinja2 - ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - -## 0.7b1 - -Released 2020-05-12 - -- Add jinja2 instrumentation ([#643](https://github.com/open-telemetry/opentelemetry-python/pull/643)) diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/LICENSE b/instrumentation/opentelemetry-instrumentation-jinja2/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-jinja2/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-jinja2/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-jinja2/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/README.rst b/instrumentation/opentelemetry-instrumentation-jinja2/README.rst deleted file mode 100644 index c74faeb32e..0000000000 --- a/instrumentation/opentelemetry-instrumentation-jinja2/README.rst +++ /dev/null @@ -1,21 +0,0 @@ -OpenTelemetry jinja2 integration -================================ - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-jinja2.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-jinja2/ - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-jinja2 - - -References ----------- - -* `OpenTelemetry jinja2 integration `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg b/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg deleted file mode 100644 index 08f46ae105..0000000000 --- a/instrumentation/opentelemetry-instrumentation-jinja2/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-jinja2 -description = OpenTelemetry jinja2 instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-jinja2 -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - jinja2~=2.7 - wrapt >= 1.0.0, < 2.0.0 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - jinja2 = opentelemetry.instrumentation.jinja2:Jinja2Instrumentor diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/setup.py b/instrumentation/opentelemetry-instrumentation-jinja2/setup.py deleted file mode 100644 index e9d484e1eb..0000000000 --- a/instrumentation/opentelemetry-instrumentation-jinja2/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "jinja2", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py deleted file mode 100644 index 63f23ae79b..0000000000 --- a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py +++ /dev/null @@ -1,147 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" - -Usage ------ - -The OpenTelemetry ``jinja2`` integration traces templates loading, compilation -and rendering. - -Usage ------ - -.. code-block:: python - - from jinja2 import Environment, FileSystemLoader - from opentelemetry.instrumentation.jinja2 import Jinja2Instrumentor - from opentelemetry import trace - from opentelemetry.trace import TracerProvider - - trace.set_tracer_provider(TracerProvider()) - - Jinja2Instrumentor().instrument() - - env = Environment(loader=FileSystemLoader("templates")) - template = env.get_template("mytemplate.html") - -API ---- -""" -# pylint: disable=no-value-for-parameter - -import logging - -import jinja2 -from wrapt import ObjectProxy -from wrapt import wrap_function_wrapper as _wrap - -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.jinja2.version import __version__ -from opentelemetry.instrumentation.utils import unwrap -from opentelemetry.trace import SpanKind, get_tracer -from opentelemetry.trace.status import Status, StatusCode - -logger = logging.getLogger(__name__) - -ATTRIBUTE_JINJA2_TEMPLATE_NAME = "jinja2.template_name" -ATTRIBUTE_JINJA2_TEMPLATE_PATH = "jinja2.template_path" -DEFAULT_TEMPLATE_NAME = "" - - -def _with_tracer_wrapper(func): - """Helper for providing tracer for wrapper functions. - """ - - def _with_tracer(tracer): - def wrapper(wrapped, instance, args, kwargs): - return func(tracer, wrapped, instance, args, kwargs) - - return wrapper - - return _with_tracer - - -@_with_tracer_wrapper -def _wrap_render(tracer, wrapped, instance, args, kwargs): - """Wrap `Template.render()` or `Template.generate()` - """ - with tracer.start_as_current_span( - "jinja2.render", kind=SpanKind.INTERNAL, - ) as span: - if span.is_recording(): - template_name = instance.name or DEFAULT_TEMPLATE_NAME - span.set_attribute(ATTRIBUTE_JINJA2_TEMPLATE_NAME, template_name) - return wrapped(*args, **kwargs) - - -@_with_tracer_wrapper -def _wrap_compile(tracer, wrapped, _, args, kwargs): - with tracer.start_as_current_span( - "jinja2.compile", kind=SpanKind.INTERNAL, - ) as span: - if span.is_recording(): - template_name = ( - args[1] - if len(args) > 1 - else kwargs.get("name", DEFAULT_TEMPLATE_NAME) - ) - span.set_attribute(ATTRIBUTE_JINJA2_TEMPLATE_NAME, template_name) - return wrapped(*args, **kwargs) - - -@_with_tracer_wrapper -def _wrap_load_template(tracer, wrapped, _, args, kwargs): - with tracer.start_as_current_span( - "jinja2.load", kind=SpanKind.INTERNAL, - ) as span: - if span.is_recording(): - template_name = kwargs.get("name", args[0]) - span.set_attribute(ATTRIBUTE_JINJA2_TEMPLATE_NAME, template_name) - template = None - try: - template = wrapped(*args, **kwargs) - return template - finally: - if template and span.is_recording(): - span.set_attribute( - ATTRIBUTE_JINJA2_TEMPLATE_PATH, template.filename - ) - - -class Jinja2Instrumentor(BaseInstrumentor): - """An instrumentor for jinja2 - - See `BaseInstrumentor` - """ - - def _instrument(self, **kwargs): - tracer_provider = kwargs.get("tracer_provider") - tracer = get_tracer(__name__, __version__, tracer_provider) - - _wrap(jinja2, "environment.Template.render", _wrap_render(tracer)) - _wrap(jinja2, "environment.Template.generate", _wrap_render(tracer)) - _wrap(jinja2, "environment.Environment.compile", _wrap_compile(tracer)) - _wrap( - jinja2, - "environment.Environment._load_template", - _wrap_load_template(tracer), - ) - - def _uninstrument(self, **kwargs): - unwrap(jinja2.Template, "render") - unwrap(jinja2.Template, "generate") - unwrap(jinja2.Environment, "compile") - unwrap(jinja2.Environment, "_load_template") diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-jinja2/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/tests/templates/base.html b/instrumentation/opentelemetry-instrumentation-jinja2/tests/templates/base.html deleted file mode 100644 index 05490d0c02..0000000000 --- a/instrumentation/opentelemetry-instrumentation-jinja2/tests/templates/base.html +++ /dev/null @@ -1 +0,0 @@ -Message: {% block content %}{% endblock %} diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/tests/templates/template.html b/instrumentation/opentelemetry-instrumentation-jinja2/tests/templates/template.html deleted file mode 100644 index ab28182415..0000000000 --- a/instrumentation/opentelemetry-instrumentation-jinja2/tests/templates/template.html +++ /dev/null @@ -1,2 +0,0 @@ -{% extends 'base.html' %} -{% block content %}Hello {{name}}!{% endblock %} diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py b/instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py deleted file mode 100644 index 5de1d598cb..0000000000 --- a/instrumentation/opentelemetry-instrumentation-jinja2/tests/test_jinja2.py +++ /dev/null @@ -1,210 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -from unittest import mock - -import jinja2 - -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.jinja2 import Jinja2Instrumentor -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace import get_tracer - -TEST_DIR = os.path.dirname(os.path.realpath(__file__)) -TMPL_DIR = os.path.join(TEST_DIR, "templates") - - -class TestJinja2Instrumentor(TestBase): - def setUp(self): - super().setUp() - Jinja2Instrumentor().instrument() - # prevent cache effects when using Template('code...') - # pylint: disable=protected-access - jinja2.environment._spontaneous_environments.clear() - self.tracer = get_tracer(__name__) - - def tearDown(self): - super().tearDown() - Jinja2Instrumentor().uninstrument() - - def test_render_inline_template_with_root(self): - with self.tracer.start_as_current_span("root"): - template = jinja2.environment.Template("Hello {{name}}!") - self.assertEqual(template.render(name="Jinja"), "Hello Jinja!") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 3) - - # pylint:disable=unbalanced-tuple-unpacking - render, template, root = spans[:3] - - self.assertIs(render.parent, root.get_span_context()) - self.assertIs(template.parent, root.get_span_context()) - self.assertIsNone(root.parent) - - def test_render_not_recording(self): - mock_tracer = mock.Mock() - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = mock_span - with mock.patch("opentelemetry.trace.get_tracer") as tracer: - tracer.return_value = mock_tracer - jinja2.environment.Template("Hello {{name}}!") - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_render_inline_template(self): - template = jinja2.environment.Template("Hello {{name}}!") - self.assertEqual(template.render(name="Jinja"), "Hello Jinja!") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 2) - - # pylint:disable=unbalanced-tuple-unpacking - template, render = spans - - self.assertEqual(template.name, "jinja2.compile") - self.assertIs(template.kind, trace_api.SpanKind.INTERNAL) - self.assertEqual( - template.attributes, {"jinja2.template_name": ""}, - ) - - self.assertEqual(render.name, "jinja2.render") - self.assertIs(render.kind, trace_api.SpanKind.INTERNAL) - self.assertEqual( - render.attributes, {"jinja2.template_name": ""}, - ) - - def test_generate_inline_template_with_root(self): - with self.tracer.start_as_current_span("root"): - template = jinja2.environment.Template("Hello {{name}}!") - self.assertEqual( - "".join(template.generate(name="Jinja")), "Hello Jinja!" - ) - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 3) - - # pylint:disable=unbalanced-tuple-unpacking - template, generate, root = spans - - self.assertIs(generate.parent, root.get_span_context()) - self.assertIs(template.parent, root.get_span_context()) - self.assertIsNone(root.parent) - - def test_generate_inline_template(self): - template = jinja2.environment.Template("Hello {{name}}!") - self.assertEqual( - "".join(template.generate(name="Jinja")), "Hello Jinja!" - ) - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 2) - - # pylint:disable=unbalanced-tuple-unpacking - template, generate = spans[:2] - - self.assertEqual(template.name, "jinja2.compile") - self.assertIs(template.kind, trace_api.SpanKind.INTERNAL) - self.assertEqual( - template.attributes, {"jinja2.template_name": ""}, - ) - - self.assertEqual(generate.name, "jinja2.render") - self.assertIs(generate.kind, trace_api.SpanKind.INTERNAL) - self.assertEqual( - generate.attributes, {"jinja2.template_name": ""}, - ) - - def test_file_template_with_root(self): - with self.tracer.start_as_current_span("root"): - loader = jinja2.loaders.FileSystemLoader(TMPL_DIR) - env = jinja2.Environment(loader=loader) - template = env.get_template("template.html") - self.assertEqual( - template.render(name="Jinja"), "Message: Hello Jinja!" - ) - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 6) - - # pylint:disable=unbalanced-tuple-unpacking - compile2, load2, compile1, load1, render, root = spans - - self.assertIs(compile2.parent, load2.get_span_context()) - self.assertIs(load2.parent, root.get_span_context()) - self.assertIs(compile1.parent, load1.get_span_context()) - self.assertIs(load1.parent, render.get_span_context()) - self.assertIs(render.parent, root.get_span_context()) - self.assertIsNone(root.parent) - - def test_file_template(self): - loader = jinja2.loaders.FileSystemLoader(TMPL_DIR) - env = jinja2.Environment(loader=loader) - template = env.get_template("template.html") - self.assertEqual( - template.render(name="Jinja"), "Message: Hello Jinja!" - ) - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 5) - - # pylint:disable=unbalanced-tuple-unpacking - compile2, load2, compile1, load1, render = spans - - self.assertEqual(compile2.name, "jinja2.compile") - self.assertEqual(load2.name, "jinja2.load") - self.assertEqual(compile1.name, "jinja2.compile") - self.assertEqual(load1.name, "jinja2.load") - self.assertEqual(render.name, "jinja2.render") - - self.assertEqual( - compile2.attributes, {"jinja2.template_name": "template.html"}, - ) - self.assertEqual( - load2.attributes, - { - "jinja2.template_name": "template.html", - "jinja2.template_path": os.path.join( - TMPL_DIR, "template.html" - ), - }, - ) - self.assertEqual( - compile1.attributes, {"jinja2.template_name": "base.html"}, - ) - self.assertEqual( - load1.attributes, - { - "jinja2.template_name": "base.html", - "jinja2.template_path": os.path.join(TMPL_DIR, "base.html"), - }, - ) - self.assertEqual( - render.attributes, {"jinja2.template_name": "template.html"}, - ) - - def test_uninstrumented(self): - Jinja2Instrumentor().uninstrument() - - jinja2.environment.Template("Hello {{name}}!") - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 0) - - Jinja2Instrumentor().instrument() diff --git a/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md deleted file mode 100644 index 8356377212..0000000000 --- a/instrumentation/opentelemetry-instrumentation-mysql/CHANGELOG.md +++ /dev/null @@ -1,36 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-mysql - ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - -## Version 0.11b0 - -Released 2020-07-28 - -- bugfix: Fix auto-instrumentation entry point for mysql - ([#858](https://github.com/open-telemetry/opentelemetry-python/pull/858)) - -## 0.7b1 - -Released 2020-05-12 - -- Implement instrumentor interface ([#654](https://github.com/open-telemetry/opentelemetry-python/pull/654)) - -## 0.4a0 - -Released 2020-02-21 - -- Initial release \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-mysql/LICENSE b/instrumentation/opentelemetry-instrumentation-mysql/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-mysql/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-mysql/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-mysql/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-mysql/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-mysql/README.rst b/instrumentation/opentelemetry-instrumentation-mysql/README.rst deleted file mode 100644 index 9558f64bd9..0000000000 --- a/instrumentation/opentelemetry-instrumentation-mysql/README.rst +++ /dev/null @@ -1,25 +0,0 @@ -OpenTelemetry MySQL Instrumentation -=================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-mysql.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-mysql/ - -Instrumentation with MySQL that supports the mysql-connector library and is -specified to trace_integration using 'MySQL'. - - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-mysql - - -References ----------- -* `OpenTelemetry MySQL Instrumentation `_ -* `OpenTelemetry Project `_ - diff --git a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg deleted file mode 100644 index ff9984a0c5..0000000000 --- a/instrumentation/opentelemetry-instrumentation-mysql/setup.cfg +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-mysql -description = OpenTelemetry MySQL instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-mysql -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation-dbapi == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - mysql-connector-python ~= 8.0 - wrapt >= 1.0.0, < 2.0.0 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - mysql = opentelemetry.instrumentation.mysql:MySQLInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-mysql/setup.py b/instrumentation/opentelemetry-instrumentation-mysql/setup.py deleted file mode 100644 index 955f75d71e..0000000000 --- a/instrumentation/opentelemetry-instrumentation-mysql/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "mysql", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py deleted file mode 100644 index 1cbd240cc5..0000000000 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -MySQL instrumentation supporting `mysql-connector`_, it can be enabled by -using ``MySQLInstrumentor``. - -.. _mysql-connector: https://pypi.org/project/mysql-connector/ - -Usage ------ - -.. code:: python - - import mysql.connector - from opentelemetry import trace - from opentelemetry.trace import TracerProvider - from opentelemetry.instrumentation.mysql import MySQLInstrumentor - - trace.set_tracer_provider(TracerProvider()) - - MySQLInstrumentor().instrument() - - cnx = mysql.connector.connect(database="MySQL_Database") - cursor = cnx.cursor() - cursor.execute("INSERT INTO test (testField) VALUES (123)" - cursor.close() - cnx.close() - -API ---- -""" - -import mysql.connector - -from opentelemetry.instrumentation import dbapi -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.mysql.version import __version__ -from opentelemetry.trace import get_tracer - - -class MySQLInstrumentor(BaseInstrumentor): - _CONNECTION_ATTRIBUTES = { - "database": "database", - "port": "server_port", - "host": "server_host", - "user": "user", - } - - _DATABASE_COMPONENT = "mysql" - _DATABASE_TYPE = "sql" - - def _instrument(self, **kwargs): - """Integrate with MySQL Connector/Python library. - https://dev.mysql.com/doc/connector-python/en/ - """ - tracer_provider = kwargs.get("tracer_provider") - - dbapi.wrap_connect( - __name__, - mysql.connector, - "connect", - self._DATABASE_COMPONENT, - self._DATABASE_TYPE, - self._CONNECTION_ATTRIBUTES, - version=__version__, - tracer_provider=tracer_provider, - ) - - def _uninstrument(self, **kwargs): - """"Disable MySQL instrumentation""" - dbapi.unwrap_connect(mysql.connector, "connect") - - # pylint:disable=no-self-use - def instrument_connection(self, connection): - """Enable instrumentation in a MySQL connection. - - Args: - connection: The connection to instrument. - - Returns: - An instrumented connection. - """ - tracer = get_tracer(__name__, __version__) - - return dbapi.instrument_connection( - tracer, - connection, - self._DATABASE_COMPONENT, - self._DATABASE_TYPE, - self._CONNECTION_ATTRIBUTES, - ) - - def uninstrument_connection(self, connection): - """Disable instrumentation in a MySQL connection. - - Args: - connection: The connection to uninstrument. - - Returns: - An uninstrumented connection. - """ - return dbapi.uninstrument_connection(connection) diff --git a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-mysql/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-mysql/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py b/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py deleted file mode 100644 index 6c114d969f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest import mock - -import mysql.connector - -import opentelemetry.instrumentation.mysql -from opentelemetry.instrumentation.mysql import MySQLInstrumentor -from opentelemetry.sdk import resources -from opentelemetry.test.test_base import TestBase - - -class TestMysqlIntegration(TestBase): - def tearDown(self): - super().tearDown() - with self.disable_logging(): - MySQLInstrumentor().uninstrument() - - @mock.patch("mysql.connector.connect") - # pylint: disable=unused-argument - def test_instrumentor(self, mock_connect): - MySQLInstrumentor().instrument() - - cnx = mysql.connector.connect(database="test") - cursor = cnx.cursor() - query = "SELECT * FROM test" - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - - # Check version and name in span's instrumentation info - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.mysql - ) - - # check that no spans are generated after uninstrumen - MySQLInstrumentor().uninstrument() - - cnx = mysql.connector.connect(database="test") - cursor = cnx.cursor() - query = "SELECT * FROM test" - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - @mock.patch("mysql.connector.connect") - # pylint: disable=unused-argument - def test_custom_tracer_provider(self, mock_connect): - resource = resources.Resource.create({}) - result = self.create_tracer_provider(resource=resource) - tracer_provider, exporter = result - - MySQLInstrumentor().instrument(tracer_provider=tracer_provider) - cnx = mysql.connector.connect(database="test") - cursor = cnx.cursor() - query = "SELECT * FROM test" - cursor.execute(query) - - span_list = exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] - - self.assertIs(span.resource, resource) - - @mock.patch("mysql.connector.connect") - # pylint: disable=unused-argument - def test_instrument_connection(self, mock_connect): - cnx = mysql.connector.connect(database="test") - query = "SELECT * FROM test" - cursor = cnx.cursor() - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 0) - - cnx = MySQLInstrumentor().instrument_connection(cnx) - cursor = cnx.cursor() - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - @mock.patch("mysql.connector.connect") - # pylint: disable=unused-argument - def test_uninstrument_connection(self, mock_connect): - MySQLInstrumentor().instrument() - cnx = mysql.connector.connect(database="test") - query = "SELECT * FROM test" - cursor = cnx.cursor() - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - cnx = MySQLInstrumentor().uninstrument_connection(cnx) - cursor = cnx.cursor() - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md deleted file mode 100644 index 2aa477de2d..0000000000 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/CHANGELOG.md +++ /dev/null @@ -1,29 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-psycopg2 - ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - -## 0.8b0 - -Released 2020-05-27 - -- Implement instrumentor interface, enabling auto-instrumentation ([#694]https://github.com/open-telemetry/opentelemetry-python/pull/694) - -## 0.4a0 - -Released 2020-02-21 - -- Initial release \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/LICENSE b/instrumentation/opentelemetry-instrumentation-psycopg2/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-psycopg2/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/README.rst b/instrumentation/opentelemetry-instrumentation-psycopg2/README.rst deleted file mode 100644 index 3ab1025eae..0000000000 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/README.rst +++ /dev/null @@ -1,20 +0,0 @@ -OpenTelemetry Psycopg Instrumentation -===================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-psycopg2.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-psycopg2/ - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-psycopg2 - - -References ----------- -* `OpenTelemetry Psycopg Instrumentation `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg deleted file mode 100644 index db98bf1251..0000000000 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.cfg +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-psycopg2 -description = OpenTelemetry psycopg2 instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-psycopg2 -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation-dbapi == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - psycopg2-binary >= 2.7.3.1 - wrapt >= 1.0.0, < 2.0.0 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - psycopg2 = opentelemetry.instrumentation.psycopg2:Psycopg2Instrumentor diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.py b/instrumentation/opentelemetry-instrumentation-psycopg2/setup.py deleted file mode 100644 index 4bd0e16be5..0000000000 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "psycopg2", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py deleted file mode 100644 index 7782f6fe63..0000000000 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The integration with PostgreSQL supports the `Psycopg`_ library, it can be enabled by -using ``Psycopg2Instrumentor``. - -.. _Psycopg: http://initd.org/psycopg/ - -Usage ------ - -.. code-block:: python - - import psycopg2 - from opentelemetry import trace - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor - - trace.set_tracer_provider(TracerProvider()) - - Psycopg2Instrumentor().instrument() - - cnx = psycopg2.connect(database='Database') - cursor = cnx.cursor() - cursor.execute("INSERT INTO test (testField) VALUES (123)") - cursor.close() - cnx.close() - -API ---- -""" - -import psycopg2 - -from opentelemetry.instrumentation import dbapi -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.psycopg2.version import __version__ -from opentelemetry.trace import get_tracer - - -class Psycopg2Instrumentor(BaseInstrumentor): - _CONNECTION_ATTRIBUTES = { - "database": "info.dbname", - "port": "info.port", - "host": "info.host", - "user": "info.user", - } - - _DATABASE_COMPONENT = "postgresql" - _DATABASE_TYPE = "sql" - - def _instrument(self, **kwargs): - """Integrate with PostgreSQL Psycopg library. - Psycopg: http://initd.org/psycopg/ - """ - - tracer_provider = kwargs.get("tracer_provider") - - dbapi.wrap_connect( - __name__, - psycopg2, - "connect", - self._DATABASE_COMPONENT, - self._DATABASE_TYPE, - self._CONNECTION_ATTRIBUTES, - version=__version__, - tracer_provider=tracer_provider, - ) - - def _uninstrument(self, **kwargs): - """"Disable Psycopg2 instrumentation""" - dbapi.unwrap_connect(psycopg2, "connect") - - # pylint:disable=no-self-use - def instrument_connection(self, connection): - """Enable instrumentation in a Psycopg2 connection. - - Args: - connection: The connection to instrument. - - Returns: - An instrumented connection. - """ - tracer = get_tracer(__name__, __version__) - - return dbapi.instrument_connection( - tracer, - connection, - self._DATABASE_COMPONENT, - self._DATABASE_TYPE, - self._CONNECTION_ATTRIBUTES, - ) - - def uninstrument_connection(self, connection): - """Disable instrumentation in a Psycopg2 connection. - - Args: - connection: The connection to uninstrument. - - Returns: - An uninstrumented connection. - """ - return dbapi.uninstrument_connection(connection) diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py deleted file mode 100644 index cb127c7a5e..0000000000 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest import mock - -import psycopg2 - -import opentelemetry.instrumentation.psycopg2 -from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor -from opentelemetry.sdk import resources -from opentelemetry.test.test_base import TestBase - - -class TestPostgresqlIntegration(TestBase): - def tearDown(self): - super().tearDown() - with self.disable_logging(): - Psycopg2Instrumentor().uninstrument() - - @mock.patch("psycopg2.connect") - # pylint: disable=unused-argument - def test_instrumentor(self, mock_connect): - Psycopg2Instrumentor().instrument() - - cnx = psycopg2.connect(database="test") - - cursor = cnx.cursor() - - query = "SELECT * FROM test" - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - - # Check version and name in span's instrumentation info - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.psycopg2 - ) - - # check that no spans are generated after uninstrument - Psycopg2Instrumentor().uninstrument() - - cnx = psycopg2.connect(database="test") - cursor = cnx.cursor() - query = "SELECT * FROM test" - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - @mock.patch("psycopg2.connect") - # pylint: disable=unused-argument - def test_not_recording(self, mock_connect): - mock_tracer = mock.Mock() - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = True - Psycopg2Instrumentor().instrument() - with mock.patch("opentelemetry.trace.get_tracer") as tracer: - tracer.return_value = mock_tracer - cnx = psycopg2.connect(database="test") - cursor = cnx.cursor() - query = "SELECT * FROM test" - cursor.execute(query) - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - Psycopg2Instrumentor().uninstrument() - - @mock.patch("psycopg2.connect") - # pylint: disable=unused-argument - def test_custom_tracer_provider(self, mock_connect): - resource = resources.Resource.create({}) - result = self.create_tracer_provider(resource=resource) - tracer_provider, exporter = result - - Psycopg2Instrumentor().instrument(tracer_provider=tracer_provider) - - cnx = psycopg2.connect(database="test") - cursor = cnx.cursor() - query = "SELECT * FROM test" - cursor.execute(query) - - spans_list = exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - - self.assertIs(span.resource, resource) - - @mock.patch("psycopg2.connect") - # pylint: disable=unused-argument - def test_instrument_connection(self, mock_connect): - cnx = psycopg2.connect(database="test") - query = "SELECT * FROM test" - cursor = cnx.cursor() - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 0) - - cnx = Psycopg2Instrumentor().instrument_connection(cnx) - cursor = cnx.cursor() - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - @mock.patch("psycopg2.connect") - # pylint: disable=unused-argument - def test_uninstrument_connection(self, mock_connect): - Psycopg2Instrumentor().instrument() - cnx = psycopg2.connect(database="test") - query = "SELECT * FROM test" - cursor = cnx.cursor() - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - cnx = Psycopg2Instrumentor().uninstrument_connection(cnx) - cursor = cnx.cursor() - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md deleted file mode 100644 index 5cff2cb6e6..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-pymemcache - ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - -## Version 0.10b0 - -Released 2020-06-23 - -- Initial release \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/LICENSE b/instrumentation/opentelemetry-instrumentation-pymemcache/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/MANIFEST.IN b/instrumentation/opentelemetry-instrumentation-pymemcache/MANIFEST.IN deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/MANIFEST.IN +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/README.rst b/instrumentation/opentelemetry-instrumentation-pymemcache/README.rst deleted file mode 100644 index f126f4246d..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/README.rst +++ /dev/null @@ -1,20 +0,0 @@ -OpenTelemetry pymemcache Instrumentation -======================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pymemcache.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-pymemcache/ - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-pymemcache - - -References ----------- -* `OpenTelemetry Pymemcache Instrumentation `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg deleted file mode 100644 index c352ba903c..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.cfg +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-pymemcache -description = OpenTelemetry pymemcache instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/instrumentation/opentelemetry-instrumentation-pymemcache -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - pymemcache ~= 1.3 - wrapt >= 1.0.0, < 2.0.0 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - pymemcache = opentelemetry.instrumentation.pymemcache:PymemcacheInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.py b/instrumentation/opentelemetry-instrumentation-pymemcache/setup.py deleted file mode 100644 index 46bf607933..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/setup.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "pymemcache", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py deleted file mode 100644 index a91d3b525a..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" - -Usage ------ - -The OpenTelemetry ``pymemcache`` integration traces pymemcache client operations - -Usage ------ - -.. code-block:: python - - from opentelemetry import trace - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.instrumentation.pymemcache import PymemcacheInstrumentor - trace.set_tracer_provider(TracerProvider()) - PymemcacheInstrumentor().instrument() - from pymemcache.client.base import Client - client = Client(('localhost', 11211)) - client.set('some_key', 'some_value') - -API ---- -""" -# pylint: disable=no-value-for-parameter - -import logging - -import pymemcache -from wrapt import ObjectProxy -from wrapt import wrap_function_wrapper as _wrap - -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.pymemcache.version import __version__ -from opentelemetry.instrumentation.utils import unwrap -from opentelemetry.trace import SpanKind, get_tracer - -logger = logging.getLogger(__name__) - -# Network attribute semantic convention here: -# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/span-general.md#general-network-connection-attributes -_HOST = "net.peer.name" -_PORT = "net.peer.port" -# Database semantic conventions here: -# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/database.md -_DB = "db.type" -_URL = "db.url" - -_DEFAULT_SERVICE = "memcached" -_RAWCMD = "db.statement" -_CMD = "memcached.command" -COMMANDS = [ - "set", - "set_many", - "add", - "replace", - "append", - "prepend", - "cas", - "get", - "get_many", - "gets", - "gets_many", - "delete", - "delete_many", - "incr", - "decr", - "touch", - "stats", - "version", - "flush_all", - "quit", - "set_multi", - "get_multi", -] - - -def _set_connection_attributes(span, instance): - if not span.is_recording(): - return - for key, value in _get_address_attributes(instance).items(): - span.set_attribute(key, value) - - -def _with_tracer_wrapper(func): - """Helper for providing tracer for wrapper functions.""" - - def _with_tracer(tracer, cmd): - def wrapper(wrapped, instance, args, kwargs): - # prevent double wrapping - if hasattr(wrapped, "__wrapped__"): - return wrapped(*args, **kwargs) - - return func(tracer, cmd, wrapped, instance, args, kwargs) - - return wrapper - - return _with_tracer - - -@_with_tracer_wrapper -def _wrap_cmd(tracer, cmd, wrapped, instance, args, kwargs): - with tracer.start_as_current_span( - _CMD, kind=SpanKind.INTERNAL, attributes={} - ) as span: - try: - if span.is_recording(): - if not args: - vals = "" - else: - vals = _get_query_string(args[0]) - - query = "{}{}{}".format(cmd, " " if vals else "", vals) - span.set_attribute(_RAWCMD, query) - - _set_connection_attributes(span, instance) - except Exception as ex: # pylint: disable=broad-except - logger.warning( - "Failed to set attributes for pymemcache span %s", str(ex) - ) - - return wrapped(*args, **kwargs) - - -def _get_query_string(arg): - - """Return the query values given the first argument to a pymemcache command. - - If there are multiple query values, they are joined together - space-separated. - """ - keys = "" - - if isinstance(arg, dict): - arg = list(arg) - - if isinstance(arg, str): - keys = arg - elif isinstance(arg, bytes): - keys = arg.decode() - elif isinstance(arg, list) and len(arg) >= 1: - if isinstance(arg[0], str): - keys = " ".join(arg) - elif isinstance(arg[0], bytes): - keys = b" ".join(arg).decode() - - return keys - - -def _get_address_attributes(instance): - """Attempt to get host and port from Client instance.""" - address_attributes = {} - address_attributes[_DB] = "memcached" - - # client.base.Client contains server attribute which is either a host/port tuple, or unix socket path string - # https://github.com/pinterest/pymemcache/blob/f02ddf73a28c09256589b8afbb3ee50f1171cac7/pymemcache/client/base.py#L228 - if hasattr(instance, "server"): - if isinstance(instance.server, tuple): - host, port = instance.server - address_attributes[_HOST] = host - address_attributes[_PORT] = port - address_attributes[_URL] = "memcached://{}:{}".format(host, port) - elif isinstance(instance.server, str): - address_attributes[_URL] = "memcached://{}".format(instance.server) - - return address_attributes - - -class PymemcacheInstrumentor(BaseInstrumentor): - """An instrumentor for pymemcache See `BaseInstrumentor`""" - - def _instrument(self, **kwargs): - tracer_provider = kwargs.get("tracer_provider") - tracer = get_tracer(__name__, __version__, tracer_provider) - - for cmd in COMMANDS: - _wrap( - "pymemcache.client.base", - "Client.{}".format(cmd), - _wrap_cmd(tracer, cmd), - ) - - def _uninstrument(self, **kwargs): - for command in COMMANDS: - unwrap(pymemcache.client.base.Client, "{}".format(command)) diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py deleted file mode 100644 index 4efb50eb4f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py +++ /dev/null @@ -1,541 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from unittest import mock - -import pymemcache -from pymemcache.exceptions import ( - MemcacheClientError, - MemcacheIllegalInputError, - MemcacheServerError, - MemcacheUnknownCommandError, - MemcacheUnknownError, -) - -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.pymemcache import PymemcacheInstrumentor -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace import get_tracer - -from .utils import MockSocket, _str - -TEST_HOST = "localhost" -TEST_PORT = 117711 - - -class PymemcacheClientTestCase( - TestBase -): # pylint: disable=too-many-public-methods - """ Tests for a patched pymemcache.client.base.Client. """ - - def setUp(self): - super().setUp() - PymemcacheInstrumentor().instrument() - - # pylint: disable=protected-access - self.tracer = get_tracer(__name__) - - def tearDown(self): - super().tearDown() - PymemcacheInstrumentor().uninstrument() - - def make_client(self, mock_socket_values, **kwargs): - # pylint: disable=attribute-defined-outside-init - self.client = pymemcache.client.base.Client( - (TEST_HOST, TEST_PORT), **kwargs - ) - self.client.sock = MockSocket(list(mock_socket_values)) - return self.client - - def check_spans(self, spans, num_expected, queries_expected): - """A helper for validating basic span information.""" - self.assertEqual(num_expected, len(spans)) - - for span, query in zip(spans, queries_expected): - self.assertEqual(span.name, "memcached.command") - self.assertIs(span.kind, trace_api.SpanKind.INTERNAL) - self.assertEqual( - span.attributes["net.peer.name"], "{}".format(TEST_HOST) - ) - self.assertEqual(span.attributes["net.peer.port"], TEST_PORT) - self.assertEqual(span.attributes["db.type"], "memcached") - self.assertEqual( - span.attributes["db.url"], - "memcached://{}:{}".format(TEST_HOST, TEST_PORT), - ) - self.assertEqual(span.attributes["db.statement"], query) - - def test_set_success(self): - client = self.make_client([b"STORED\r\n"]) - result = client.set(b"key", b"value", noreply=False) - self.assertTrue(result) - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["set key"]) - - def test_set_not_recording(self): - mock_tracer = mock.Mock() - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = True - with mock.patch("opentelemetry.trace.get_tracer") as tracer: - tracer.return_value = mock_tracer - client = self.make_client([b"STORED\r\n"]) - result = client.set(b"key", b"value", noreply=False) - self.assertTrue(result) - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_get_many_none_found(self): - client = self.make_client([b"END\r\n"]) - result = client.get_many([b"key1", b"key2"]) - assert result == {} - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["get_many key1 key2"]) - - def test_get_multi_none_found(self): - client = self.make_client([b"END\r\n"]) - # alias for get_many - result = client.get_multi([b"key1", b"key2"]) - assert result == {} - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["get_multi key1 key2"]) - - def test_set_multi_success(self): - client = self.make_client([b"STORED\r\n"]) - # Alias for set_many, a convienance function that calls set for every key - result = client.set_multi({b"key": b"value"}, noreply=False) - self.assertTrue(result) - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 2, ["set key", "set_multi key"]) - - def test_delete_not_found(self): - client = self.make_client([b"NOT_FOUND\r\n"]) - result = client.delete(b"key", noreply=False) - assert result is False - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["delete key"]) - - def test_incr_found(self): - client = self.make_client([b"STORED\r\n", b"1\r\n"]) - client.set(b"key", 0, noreply=False) - result = client.incr(b"key", 1, noreply=False) - assert result == 1 - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 2, ["set key", "incr key"]) - - def test_get_found(self): - client = self.make_client( - [b"STORED\r\n", b"VALUE key 0 5\r\nvalue\r\nEND\r\n"] - ) - result = client.set(b"key", b"value", noreply=False) - result = client.get(b"key") - assert result == b"value" - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 2, ["set key", "get key"]) - - def test_decr_found(self): - client = self.make_client([b"STORED\r\n", b"1\r\n"]) - client.set(b"key", 2, noreply=False) - result = client.decr(b"key", 1, noreply=False) - assert result == 1 - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 2, ["set key", "decr key"]) - - def test_add_stored(self): - client = self.make_client([b"STORED\r", b"\n"]) - result = client.add(b"key", b"value", noreply=False) - self.assertTrue(result) - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["add key"]) - - def test_delete_many_found(self): - client = self.make_client([b"STORED\r", b"\n", b"DELETED\r\n"]) - result = client.add(b"key", b"value", noreply=False) - # a convienance function that calls delete for every key - result = client.delete_many([b"key"], noreply=False) - self.assertTrue(result) - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans( - spans, 3, ["add key", "delete key", "delete_many key"] - ) - - def test_set_many_success(self): - client = self.make_client([b"STORED\r\n"]) - # a convienance function that calls set for every key - result = client.set_many({b"key": b"value"}, noreply=False) - self.assertTrue(result) - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 2, ["set key", "set_many key"]) - - def test_set_get(self): - client = self.make_client( - [b"STORED\r\n", b"VALUE key 0 5\r\nvalue\r\nEND\r\n"] - ) - client.set(b"key", b"value", noreply=False) - result = client.get(b"key") - assert _str(result) == "value" - - spans = self.memory_exporter.get_finished_spans() - - self.assertEqual(len(spans), 2) - self.assertEqual( - spans[0].attributes["db.url"], - "memcached://{}:{}".format(TEST_HOST, TEST_PORT), - ) - - def test_append_stored(self): - client = self.make_client([b"STORED\r\n"]) - result = client.append(b"key", b"value", noreply=False) - self.assertTrue(result) - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["append key"]) - - def test_prepend_stored(self): - client = self.make_client([b"STORED\r\n"]) - result = client.prepend(b"key", b"value", noreply=False) - self.assertTrue(result) - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["prepend key"]) - - def test_cas_stored(self): - client = self.make_client([b"STORED\r\n"]) - result = client.cas(b"key", b"value", b"cas", noreply=False) - self.assertTrue(result) - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["cas key"]) - - def test_cas_exists(self): - client = self.make_client([b"EXISTS\r\n"]) - result = client.cas(b"key", b"value", b"cas", noreply=False) - assert result is False - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["cas key"]) - - def test_cas_not_found(self): - client = self.make_client([b"NOT_FOUND\r\n"]) - result = client.cas(b"key", b"value", b"cas", noreply=False) - assert result is None - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["cas key"]) - - def test_delete_exception(self): - client = self.make_client([Exception("fail")]) - - def _delete(): - client.delete(b"key", noreply=False) - - with self.assertRaises(Exception): - _delete() - - spans = self.memory_exporter.get_finished_spans() - - span = spans[0] - - self.assertFalse(span.status.is_ok) - - self.check_spans(spans, 1, ["delete key"]) - - def test_flush_all(self): - client = self.make_client([b"OK\r\n"]) - result = client.flush_all(noreply=False) - self.assertTrue(result) - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["flush_all"]) - - def test_incr_exception(self): - client = self.make_client([Exception("fail")]) - - def _incr(): - client.incr(b"key", 1) - - with self.assertRaises(Exception): - _incr() - - spans = self.memory_exporter.get_finished_spans() - - span = spans[0] - - self.assertFalse(span.status.is_ok) - - self.check_spans(spans, 1, ["incr key"]) - - def test_get_error(self): - client = self.make_client([b"ERROR\r\n"]) - - def _get(): - client.get(b"key") - - with self.assertRaises(MemcacheUnknownCommandError): - _get() - - spans = self.memory_exporter.get_finished_spans() - - span = spans[0] - - self.assertFalse(span.status.is_ok) - - self.check_spans(spans, 1, ["get key"]) - - def test_get_unknown_error(self): - client = self.make_client([b"foobarbaz\r\n"]) - - def _get(): - client.get(b"key") - - with self.assertRaises(MemcacheUnknownError): - _get() - - spans = self.memory_exporter.get_finished_spans() - - span = spans[0] - - self.assertFalse(span.status.is_ok) - - self.check_spans(spans, 1, ["get key"]) - - def test_gets_found(self): - client = self.make_client([b"VALUE key 0 5 10\r\nvalue\r\nEND\r\n"]) - result = client.gets(b"key") - assert result == (b"value", b"10") - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["gets key"]) - - def test_touch_not_found(self): - client = self.make_client([b"NOT_FOUND\r\n"]) - result = client.touch(b"key", noreply=False) - assert result is False - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["touch key"]) - - def test_set_client_error(self): - client = self.make_client([b"CLIENT_ERROR some message\r\n"]) - - def _set(): - client.set("key", "value", noreply=False) - - with self.assertRaises(MemcacheClientError): - _set() - - spans = self.memory_exporter.get_finished_spans() - - span = spans[0] - - self.assertFalse(span.status.is_ok) - - self.check_spans(spans, 1, ["set key"]) - - def test_set_server_error(self): - client = self.make_client([b"SERVER_ERROR some message\r\n"]) - - def _set(): - client.set(b"key", b"value", noreply=False) - - with self.assertRaises(MemcacheServerError): - _set() - - spans = self.memory_exporter.get_finished_spans() - - span = spans[0] - - self.assertFalse(span.status.is_ok) - - self.check_spans(spans, 1, ["set key"]) - - def test_set_key_with_space(self): - client = self.make_client([b""]) - - def _set(): - client.set(b"key has space", b"value", noreply=False) - - with self.assertRaises(MemcacheIllegalInputError): - _set() - - spans = self.memory_exporter.get_finished_spans() - - span = spans[0] - - self.assertFalse(span.status.is_ok) - - self.check_spans(spans, 1, ["set key has space"]) - - def test_quit(self): - client = self.make_client([]) - assert client.quit() is None - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["quit"]) - - def test_replace_not_stored(self): - client = self.make_client([b"NOT_STORED\r\n"]) - result = client.replace(b"key", b"value", noreply=False) - assert result is False - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["replace key"]) - - def test_version_success(self): - client = self.make_client( - [b"VERSION 1.2.3\r\n"], default_noreply=False - ) - result = client.version() - assert result == b"1.2.3" - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["version"]) - - def test_stats(self): - client = self.make_client([b"STAT fake_stats 1\r\n", b"END\r\n"]) - result = client.stats() - assert client.sock.send_bufs == [b"stats \r\n"] - assert result == {b"fake_stats": 1} - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 1, ["stats"]) - - def test_uninstrumented(self): - PymemcacheInstrumentor().uninstrument() - - client = self.make_client( - [b"STORED\r\n", b"VALUE key 0 5\r\nvalue\r\nEND\r\n"] - ) - client.set(b"key", b"value", noreply=False) - result = client.get(b"key") - assert _str(result) == "value" - - spans = self.memory_exporter.get_finished_spans() - - self.assertEqual(len(spans), 0) - - PymemcacheInstrumentor().instrument() - - -class PymemcacheHashClientTestCase(TestBase): - """ Tests for a patched pymemcache.client.hash.HashClient. """ - - def setUp(self): - super().setUp() - PymemcacheInstrumentor().instrument() - - # pylint: disable=protected-access - self.tracer = get_tracer(__name__) - - def tearDown(self): - super().tearDown() - PymemcacheInstrumentor().uninstrument() - - def make_client_pool( - self, hostname, mock_socket_values, serializer=None, **kwargs - ): # pylint: disable=no-self-use - mock_client = pymemcache.client.base.Client( - hostname, serializer=serializer, **kwargs - ) - mock_client.sock = MockSocket(mock_socket_values) - client = pymemcache.client.base.PooledClient( - hostname, serializer=serializer - ) - client.client_pool = pymemcache.pool.ObjectPool(lambda: mock_client) - return mock_client - - def make_client(self, *mock_socket_values, **kwargs): - current_port = TEST_PORT - - # pylint: disable=import-outside-toplevel - from pymemcache.client.hash import HashClient - - # pylint: disable=attribute-defined-outside-init - self.client = HashClient([], **kwargs) - ip = TEST_HOST - - for vals in mock_socket_values: - url_string = "{}:{}".format(ip, current_port) - clnt_pool = self.make_client_pool( - (ip, current_port), vals, **kwargs - ) - self.client.clients[url_string] = clnt_pool - self.client.hasher.add_node(url_string) - current_port += 1 - return self.client - - def check_spans(self, spans, num_expected, queries_expected): - """A helper for validating basic span information.""" - self.assertEqual(num_expected, len(spans)) - - for span, query in zip(spans, queries_expected): - self.assertEqual(span.name, "memcached.command") - self.assertIs(span.kind, trace_api.SpanKind.INTERNAL) - self.assertEqual( - span.attributes["net.peer.name"], "{}".format(TEST_HOST) - ) - self.assertEqual(span.attributes["net.peer.port"], TEST_PORT) - self.assertEqual(span.attributes["db.type"], "memcached") - self.assertEqual( - span.attributes["db.url"], - "memcached://{}:{}".format(TEST_HOST, TEST_PORT), - ) - self.assertEqual(span.attributes["db.statement"], query) - - def test_delete_many_found(self): - client = self.make_client([b"STORED\r", b"\n", b"DELETED\r\n"]) - result = client.add(b"key", b"value", noreply=False) - result = client.delete_many([b"key"], noreply=False) - self.assertTrue(result) - - spans = self.memory_exporter.get_finished_spans() - - self.check_spans(spans, 2, ["add key", "delete key"]) diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/tests/utils.py b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/utils.py deleted file mode 100644 index 361fb6e68c..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/tests/utils.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import collections -import socket - - -class MockSocket: - def __init__(self, recv_bufs, connect_failure=None): - self.recv_bufs = collections.deque(recv_bufs) - self.send_bufs = [] - self.closed = False - self.timeouts = [] - self.connect_failure = connect_failure - self.connections = [] - self.socket_options = [] - - def sendall(self, value): - self.send_bufs.append(value) - - def close(self): - self.closed = True - - def recv(self, size): # pylint: disable=unused-argument - value = self.recv_bufs.popleft() - if isinstance(value, Exception): - raise value - return value - - def settimeout(self, timeout): - self.timeouts.append(timeout) - - def connect(self, server): - if isinstance(self.connect_failure, Exception): - raise self.connect_failure - self.connections.append(server) - - def setsockopt(self, level, option, value): - self.socket_options.append((level, option, value)) - - -class MockSocketModule: - def __init__(self, connect_failure=None): - self.connect_failure = connect_failure - self.sockets = [] - - def socket(self): # noqa: A002 - soket = MockSocket([], connect_failure=self.connect_failure) - self.sockets.append(soket) - return soket - - def __getattr__(self, name): - return getattr(socket, name) - - -# Compatibility to get a string back from a request -def _str(string_input): - if isinstance(string_input, str): - return string_input - if isinstance(string_input, bytes): - return string_input.decode() - - return str(string_input) diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md deleted file mode 100644 index 30e36e0048..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymongo/CHANGELOG.md +++ /dev/null @@ -1,43 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.14b0 - -Released 2020-10-13 - -- Cast PyMongo commands as strings - ([#1132](https://github.com/open-telemetry/opentelemetry-python/pull/1132)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-pymongo - ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - -## 0.7b1 - -Released 2020-05-12 - -- Implement instrumentor interface ([#612](https://github.com/open-telemetry/opentelemetry-python/pull/612)) - -## 0.4a0 - -Released 2020-02-21 - -- Updating network connection attribute names - ([#350](https://github.com/open-telemetry/opentelemetry-python/pull/350)) - -## 0.3a0 - -Released 2019-12-11 - -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/LICENSE b/instrumentation/opentelemetry-instrumentation-pymongo/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymongo/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-pymongo/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymongo/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/README.rst b/instrumentation/opentelemetry-instrumentation-pymongo/README.rst deleted file mode 100644 index 7791810e97..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymongo/README.rst +++ /dev/null @@ -1,21 +0,0 @@ -OpenTelemetry pymongo Instrumentation -===================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pymongo.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-pymongo/ - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-pymongo - - -References ----------- -* `OpenTelemetry pymongo Instrumentation `_ -* `OpenTelemetry Project `_ - diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg deleted file mode 100644 index 6ccf6f1002..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymongo/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-pymongo -description = OpenTelemetry pymongo instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-pymongo -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - pymongo ~= 3.1 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - pymongo = opentelemetry.instrumentation.pymongo:PymongoInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/setup.py b/instrumentation/opentelemetry-instrumentation-pymongo/setup.py deleted file mode 100644 index 7b862ae2aa..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymongo/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "pymongo", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py deleted file mode 100644 index adc51b1c84..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py +++ /dev/null @@ -1,169 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The integration with MongoDB supports the `pymongo`_ library, it can be -enabled using the ``PymongoInstrumentor``. - -.. _pymongo: https://pypi.org/project/pymongo - -Usage ------ - -.. code:: python - - from pymongo import MongoClient - from opentelemetry import trace - from opentelemetry.trace import TracerProvider - from opentelemetry.instrumentation.pymongo import PymongoInstrumentor - - trace.set_tracer_provider(TracerProvider()) - - PymongoInstrumentor().instrument() - client = MongoClient() - db = client["MongoDB_Database"] - collection = db["MongoDB_Collection"] - collection.find_one() - -API ---- -""" - -from pymongo import monitoring - -from opentelemetry import trace -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.pymongo.version import __version__ -from opentelemetry.trace import SpanKind, get_tracer -from opentelemetry.trace.status import Status, StatusCode - -DATABASE_TYPE = "mongodb" -COMMAND_ATTRIBUTES = ["filter", "sort", "skip", "limit", "pipeline"] - - -class CommandTracer(monitoring.CommandListener): - def __init__(self, tracer): - self._tracer = tracer - self._span_dict = {} - self.is_enabled = True - - def started(self, event: monitoring.CommandStartedEvent): - """ Method to handle a pymongo CommandStartedEvent """ - if not self.is_enabled: - return - command = event.command.get(event.command_name, "") - name = DATABASE_TYPE + "." + event.command_name - statement = event.command_name - if command: - name += "." + str(command) - statement += " " + str(command) - - try: - span = self._tracer.start_span(name, kind=SpanKind.CLIENT) - if span.is_recording(): - span.set_attribute("component", DATABASE_TYPE) - span.set_attribute("db.type", DATABASE_TYPE) - span.set_attribute("db.instance", event.database_name) - span.set_attribute("db.statement", statement) - if event.connection_id is not None: - span.set_attribute("net.peer.name", event.connection_id[0]) - span.set_attribute("net.peer.port", event.connection_id[1]) - - # pymongo specific, not specified by spec - span.set_attribute("db.mongo.operation_id", event.operation_id) - span.set_attribute("db.mongo.request_id", event.request_id) - - for attr in COMMAND_ATTRIBUTES: - _attr = event.command.get(attr) - if _attr is not None: - span.set_attribute("db.mongo." + attr, str(_attr)) - - # Add Span to dictionary - self._span_dict[_get_span_dict_key(event)] = span - except Exception as ex: # noqa pylint: disable=broad-except - if span is not None and span.is_recording(): - span.set_status(Status(StatusCode.ERROR, str(ex))) - span.end() - self._pop_span(event) - - def succeeded(self, event: monitoring.CommandSucceededEvent): - """ Method to handle a pymongo CommandSucceededEvent """ - if not self.is_enabled: - return - span = self._pop_span(event) - if span is None: - return - if span.is_recording(): - span.set_attribute( - "db.mongo.duration_micros", event.duration_micros - ) - span.end() - - def failed(self, event: monitoring.CommandFailedEvent): - """ Method to handle a pymongo CommandFailedEvent """ - if not self.is_enabled: - return - span = self._pop_span(event) - if span is None: - return - if span.is_recording(): - span.set_attribute( - "db.mongo.duration_micros", event.duration_micros - ) - span.set_status(Status(StatusCode.ERROR, event.failure)) - span.end() - - def _pop_span(self, event): - return self._span_dict.pop(_get_span_dict_key(event), None) - - -def _get_span_dict_key(event): - if event.connection_id is not None: - return (event.request_id, event.connection_id) - return event.request_id - - -class PymongoInstrumentor(BaseInstrumentor): - _commandtracer_instance = None # type CommandTracer - # The instrumentation for PyMongo is based on the event listener interface - # https://api.mongodb.com/python/current/api/pymongo/monitoring.html. - # This interface only allows to register listeners and does not provide - # an unregister API. In order to provide a mechanishm to disable - # instrumentation an enabled flag is implemented in CommandTracer, - # it's checked in the different listeners. - - def _instrument(self, **kwargs): - """Integrate with pymongo to trace it using event listener. - https://api.mongodb.com/python/current/api/pymongo/monitoring.html - - Args: - tracer_provider: The `TracerProvider` to use. If none is passed the - current configured one is used. - """ - - tracer_provider = kwargs.get("tracer_provider") - - # Create and register a CommandTracer only the first time - if self._commandtracer_instance is None: - tracer = get_tracer(__name__, __version__, tracer_provider) - - self._commandtracer_instance = CommandTracer(tracer) - monitoring.register(self._commandtracer_instance) - - # If already created, just enable it - self._commandtracer_instance.is_enabled = True - - def _uninstrument(self, **kwargs): - if self._commandtracer_instance is not None: - self._commandtracer_instance.is_enabled = False diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-pymongo/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py b/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py deleted file mode 100644 index a3bb7b2223..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest import mock - -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.pymongo import ( - CommandTracer, - PymongoInstrumentor, -) -from opentelemetry.test.test_base import TestBase - - -class TestPymongo(TestBase): - def setUp(self): - super().setUp() - self.tracer = self.tracer_provider.get_tracer(__name__) - - def test_pymongo_instrumentor(self): - mock_register = mock.Mock() - patch = mock.patch( - "pymongo.monitoring.register", side_effect=mock_register - ) - with patch: - PymongoInstrumentor().instrument() - - self.assertTrue(mock_register.called) - - def test_started(self): - command_attrs = { - "filter": "filter", - "sort": "sort", - "limit": "limit", - "pipeline": "pipeline", - "command_name": "find", - } - command_tracer = CommandTracer(self.tracer) - mock_event = MockEvent( - command_attrs, ("test.com", "1234"), "test_request_id" - ) - command_tracer.started(event=mock_event) - # the memory exporter can't be used here because the span isn't ended - # yet - # pylint: disable=protected-access - span = command_tracer._pop_span(mock_event) - self.assertIs(span.kind, trace_api.SpanKind.CLIENT) - self.assertEqual(span.name, "mongodb.command_name.find") - self.assertEqual(span.attributes["component"], "mongodb") - self.assertEqual(span.attributes["db.type"], "mongodb") - self.assertEqual(span.attributes["db.instance"], "database_name") - self.assertEqual(span.attributes["db.statement"], "command_name find") - self.assertEqual(span.attributes["net.peer.name"], "test.com") - self.assertEqual(span.attributes["net.peer.port"], "1234") - self.assertEqual( - span.attributes["db.mongo.operation_id"], "operation_id" - ) - self.assertEqual( - span.attributes["db.mongo.request_id"], "test_request_id" - ) - - self.assertEqual(span.attributes["db.mongo.filter"], "filter") - self.assertEqual(span.attributes["db.mongo.sort"], "sort") - self.assertEqual(span.attributes["db.mongo.limit"], "limit") - self.assertEqual(span.attributes["db.mongo.pipeline"], "pipeline") - - def test_succeeded(self): - mock_event = MockEvent({}) - command_tracer = CommandTracer(self.tracer) - command_tracer.started(event=mock_event) - command_tracer.succeeded(event=mock_event) - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - self.assertEqual( - span.attributes["db.mongo.duration_micros"], "duration_micros" - ) - self.assertIs( - span.status.status_code, trace_api.status.StatusCode.UNSET - ) - self.assertIsNotNone(span.end_time) - - def test_not_recording(self): - mock_tracer = mock.Mock() - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = True - mock_event = MockEvent({}) - command_tracer = CommandTracer(mock_tracer) - command_tracer.started(event=mock_event) - command_tracer.succeeded(event=mock_event) - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_failed(self): - mock_event = MockEvent({}) - command_tracer = CommandTracer(self.tracer) - command_tracer.started(event=mock_event) - command_tracer.failed(event=mock_event) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - - self.assertEqual( - span.attributes["db.mongo.duration_micros"], "duration_micros" - ) - self.assertIs( - span.status.status_code, trace_api.status.StatusCode.ERROR, - ) - self.assertEqual(span.status.description, "failure") - self.assertIsNotNone(span.end_time) - - def test_multiple_commands(self): - first_mock_event = MockEvent({}, ("firstUrl", "123"), "first") - second_mock_event = MockEvent({}, ("secondUrl", "456"), "second") - command_tracer = CommandTracer(self.tracer) - command_tracer.started(event=first_mock_event) - command_tracer.started(event=second_mock_event) - command_tracer.succeeded(event=first_mock_event) - command_tracer.failed(event=second_mock_event) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 2) - first_span = spans_list[0] - second_span = spans_list[1] - - self.assertEqual(first_span.attributes["db.mongo.request_id"], "first") - self.assertIs( - first_span.status.status_code, trace_api.status.StatusCode.UNSET, - ) - self.assertEqual( - second_span.attributes["db.mongo.request_id"], "second" - ) - self.assertIs( - second_span.status.status_code, trace_api.status.StatusCode.ERROR, - ) - - def test_int_command(self): - command_attrs = { - "command_name": 123, - } - mock_event = MockEvent(command_attrs) - - command_tracer = CommandTracer(self.tracer) - command_tracer.started(event=mock_event) - command_tracer.succeeded(event=mock_event) - - spans_list = self.memory_exporter.get_finished_spans() - - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - - self.assertEqual(span.name, "mongodb.command_name.123") - - -class MockCommand: - def __init__(self, command_attrs): - self.command_attrs = command_attrs - - def get(self, key, default=""): - return self.command_attrs.get(key, default) - - -class MockEvent: - def __init__(self, command_attrs, connection_id=None, request_id=""): - self.command = MockCommand(command_attrs) - self.connection_id = connection_id - self.request_id = request_id - - def __getattr__(self, item): - return item diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md deleted file mode 100644 index 3f78db57b9..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymysql/CHANGELOG.md +++ /dev/null @@ -1,30 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.14b0 - -Released 2020-10-13 - -- Bumped version from 0.9.3 to 0.10.1 - ([#1228](https://github.com/open-telemetry/opentelemetry-python/pull/1228)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-pymysql - ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - -## 0.7b1 - -Released 2020-05-12 - -- Initial release \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/README.rst b/instrumentation/opentelemetry-instrumentation-pymysql/README.rst deleted file mode 100644 index 0b566d2a94..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymysql/README.rst +++ /dev/null @@ -1,20 +0,0 @@ -OpenTelemetry PyMySQL Instrumentation -===================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pymysql.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-pymysql/ - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-pymysql - - -References ----------- -* `OpenTelemetry PyMySQL Instrumentation `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg b/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg deleted file mode 100644 index b84fe20d55..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymysql/setup.cfg +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-pymysql -description = OpenTelemetry PyMySQL instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-pymysql -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation-dbapi == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - PyMySQL ~= 0.10.1 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - pymysql = opentelemetry.instrumentation.pymysql:PyMySQLInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/setup.py b/instrumentation/opentelemetry-instrumentation-pymysql/setup.py deleted file mode 100644 index 74cd948fee..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymysql/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "pymysql", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py deleted file mode 100644 index 5d299505d6..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The integration with PyMySQL supports the `PyMySQL`_ library and can be enabled -by using ``PyMySQLInstrumentor``. - -.. _PyMySQL: https://pypi.org/project/PyMySQL/ - -Usage ------ - -.. code:: python - - import pymysql - from opentelemetry import trace - from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor - from opentelemetry.sdk.trace import TracerProvider - - trace.set_tracer_provider(TracerProvider()) - - PyMySQLInstrumentor().instrument() - - cnx = pymysql.connect(database="MySQL_Database") - cursor = cnx.cursor() - cursor.execute("INSERT INTO test (testField) VALUES (123)" - cnx.commit() - cursor.close() - cnx.close() - -API ---- -""" - -import pymysql - -from opentelemetry.instrumentation import dbapi -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.pymysql.version import __version__ - - -class PyMySQLInstrumentor(BaseInstrumentor): - _CONNECTION_ATTRIBUTES = { - "database": "db", - "port": "port", - "host": "host", - "user": "user", - } - - _DATABASE_COMPONENT = "mysql" - _DATABASE_TYPE = "sql" - - def _instrument(self, **kwargs): - """Integrate with the PyMySQL library. - https://github.com/PyMySQL/PyMySQL/ - """ - tracer_provider = kwargs.get("tracer_provider") - - dbapi.wrap_connect( - __name__, - pymysql, - "connect", - self._DATABASE_COMPONENT, - self._DATABASE_TYPE, - self._CONNECTION_ATTRIBUTES, - version=__version__, - tracer_provider=tracer_provider, - ) - - def _uninstrument(self, **kwargs): - """"Disable PyMySQL instrumentation""" - dbapi.unwrap_connect(pymysql, "connect") - - # pylint:disable=no-self-use - def instrument_connection(self, connection): - """Enable instrumentation in a PyMySQL connection. - - Args: - connection: The connection to instrument. - - Returns: - An instrumented connection. - """ - - return dbapi.instrument_connection( - __name__, - connection, - self._DATABASE_COMPONENT, - self._DATABASE_TYPE, - self._CONNECTION_ATTRIBUTES, - version=__version__, - ) - - def uninstrument_connection(self, connection): - """Disable instrumentation in a PyMySQL connection. - - Args: - connection: The connection to uninstrument. - - Returns: - An uninstrumented connection. - """ - return dbapi.uninstrument_connection(connection) diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-pymysql/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py deleted file mode 100644 index 35c9f4d32b..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest import mock - -import pymysql - -import opentelemetry.instrumentation.pymysql -from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor -from opentelemetry.sdk import resources -from opentelemetry.test.test_base import TestBase - - -class TestPyMysqlIntegration(TestBase): - def tearDown(self): - super().tearDown() - with self.disable_logging(): - PyMySQLInstrumentor().uninstrument() - - @mock.patch("pymysql.connect") - # pylint: disable=unused-argument - def test_instrumentor(self, mock_connect): - PyMySQLInstrumentor().instrument() - - cnx = pymysql.connect(database="test") - cursor = cnx.cursor() - query = "SELECT * FROM test" - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - - # Check version and name in span's instrumentation info - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.pymysql - ) - - # check that no spans are generated after uninstrument - PyMySQLInstrumentor().uninstrument() - - cnx = pymysql.connect(database="test") - cursor = cnx.cursor() - query = "SELECT * FROM test" - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - @mock.patch("pymysql.connect") - # pylint: disable=unused-argument - def test_custom_tracer_provider(self, mock_connect): - resource = resources.Resource.create({}) - result = self.create_tracer_provider(resource=resource) - tracer_provider, exporter = result - - PyMySQLInstrumentor().instrument(tracer_provider=tracer_provider) - - cnx = pymysql.connect(database="test") - cursor = cnx.cursor() - query = "SELECT * FROM test" - cursor.execute(query) - - spans_list = exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - span = spans_list[0] - - self.assertIs(span.resource, resource) - - @mock.patch("pymysql.connect") - # pylint: disable=unused-argument - def test_instrument_connection(self, mock_connect): - cnx = pymysql.connect(database="test") - query = "SELECT * FROM test" - cursor = cnx.cursor() - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 0) - - cnx = PyMySQLInstrumentor().instrument_connection(cnx) - cursor = cnx.cursor() - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - @mock.patch("pymysql.connect") - # pylint: disable=unused-argument - def test_uninstrument_connection(self, mock_connect): - PyMySQLInstrumentor().instrument() - cnx = pymysql.connect(database="test") - query = "SELECT * FROM test" - cursor = cnx.cursor() - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) - - cnx = PyMySQLInstrumentor().uninstrument_connection(cnx) - cursor = cnx.cursor() - cursor.execute(query) - - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md deleted file mode 100644 index 6f78bfc16a..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pyramid/CHANGELOG.md +++ /dev/null @@ -1,27 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-pyramid ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) -- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - -## Version 0.11b0 - -- Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) - -## 0.9b0 - -Released 2020-06-10 - -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/LICENSE b/instrumentation/opentelemetry-instrumentation-pyramid/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pyramid/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-pyramid/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pyramid/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/README.rst b/instrumentation/opentelemetry-instrumentation-pyramid/README.rst deleted file mode 100644 index 931486773a..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pyramid/README.rst +++ /dev/null @@ -1,32 +0,0 @@ -OpenTelemetry Pyramid Instrumentation -===================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pyramid.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-pyramid/ - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-pyramid - -Exclude lists -************* -To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_PYRAMID_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude. - -For example, - -:: - - export OTEL_PYTHON_PYRAMID_EXCLUDED_URLS="client/.*/info,healthcheck" - -will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. - -References ----------- -* `OpenTelemetry Pyramid Instrumentation `_ -* `OpenTelemetry Project `_ - diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg b/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg deleted file mode 100644 index 44db6be961..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pyramid/setup.cfg +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-pyramid -description = OpenTelemetry Pyramid instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-pyramid -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - pyramid >= 1.7 - opentelemetry-instrumentation == 0.16.dev0 - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation-wsgi == 0.16.dev0 - wrapt >= 1.0.0, < 2.0.0 - -[options.extras_require] -test = - werkzeug == 0.16.1 - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - pyramid = opentelemetry.instrumentation.pyramid:PyramidInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/setup.py b/instrumentation/opentelemetry-instrumentation-pyramid/setup.py deleted file mode 100644 index 7141a89813..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pyramid/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "pyramid", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/__init__.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/__init__.py deleted file mode 100644 index 4125453153..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/__init__.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Pyramid instrumentation supporting `pyramid`_, it can be enabled by -using ``PyramidInstrumentor``. - -.. _pyramid: https://docs.pylonsproject.org/projects/pyramid/en/latest/ - -Usage ------ -There are two methods to instrument Pyramid: - -Method 1 (Instrument all Configurators): ----------------------------------------- - -.. code:: python - - from pyramid.config import Configurator - from opentelemetry.instrumentation.pyramid import PyramidInstrumentor - - PyramidInstrumentor().instrument() - - config = Configurator() - - # use your config as normal - config.add_route('index', '/') - -Method 2 (Instrument one Configurator): ---------------------------------------- - -.. code:: python - - from pyramid.config import Configurator - from opentelemetry.instrumentation.pyramid import PyramidInstrumentor - - config = Configurator() - PyramidInstrumentor().instrument_config(config) - - # use your config as normal - config.add_route('index', '/') - -Using ``pyramid.tweens`` setting: ---------------------------------- - -If you use Method 2 and then set tweens for your application with the ``pyramid.tweens`` setting, -you need to add ``opentelemetry.instrumentation.pyramid.trace_tween_factory`` explicity to the list, -*as well as* instrumenting the config as shown above. - -For example: - -.. code:: python - - from pyramid.config import Configurator - from opentelemetry.instrumentation.pyramid import PyramidInstrumentor - - settings = { - 'pyramid.tweens', 'opentelemetry.instrumentation.pyramid.trace_tween_factory\\nyour_tween_no_1\\nyour_tween_no_2', - } - config = Configurator(settings=settings) - PyramidInstrumentor().instrument_config(config) - - # use your config as normal. - config.add_route('index', '/') - -API ---- -""" - -import typing - -from pyramid.config import Configurator -from pyramid.path import caller_package -from pyramid.settings import aslist -from wrapt import ObjectProxy -from wrapt import wrap_function_wrapper as _wrap - -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.pyramid.callbacks import ( - SETTING_TRACE_ENABLED, - TWEEN_NAME, - trace_tween_factory, -) -from opentelemetry.instrumentation.pyramid.version import __version__ -from opentelemetry.instrumentation.utils import unwrap -from opentelemetry.trace import TracerProvider, get_tracer - - -def _traced_init(wrapped, instance, args, kwargs): - settings = kwargs.get("settings", {}) - tweens = aslist(settings.get("pyramid.tweens", [])) - - if tweens and TWEEN_NAME not in settings: - # pyramid.tweens.EXCVIEW is the name of built-in exception view provided by - # pyramid. We need our tween to be before it, otherwise unhandled - # exceptions will be caught before they reach our tween. - tweens = [TWEEN_NAME] + tweens - - settings["pyramid.tweens"] = "\n".join(tweens) - - kwargs["settings"] = settings - - # `caller_package` works by walking a fixed amount of frames up the stack - # to find the calling package. So if we let the original `__init__` - # function call it, our wrapper will mess things up. - if not kwargs.get("package", None): - # Get the package for the third frame up from this one. - # Default is `level=2` which will give us the package from `wrapt` - # instead of the desired package (the caller) - kwargs["package"] = caller_package(level=3) - - wrapped(*args, **kwargs) - instance.include("opentelemetry.instrumentation.pyramid.callbacks") - - -class PyramidInstrumentor(BaseInstrumentor): - def _instrument(self, **kwargs): - """Integrate with Pyramid Python library. - https://docs.pylonsproject.org/projects/pyramid/en/latest/ - """ - _wrap("pyramid.config", "Configurator.__init__", _traced_init) - - def _uninstrument(self, **kwargs): - """"Disable Pyramid instrumentation""" - unwrap(Configurator, "__init__") - - # pylint:disable=no-self-use - def instrument_config(self, config): - """Enable instrumentation in a Pyramid configurator. - - Args: - config: The Configurator to instrument. - """ - config.include("opentelemetry.instrumentation.pyramid.callbacks") - - def uninstrument_config(self, config): - config.add_settings({SETTING_TRACE_ENABLED: False}) diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py deleted file mode 100644 index e7110bd2b5..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py +++ /dev/null @@ -1,165 +0,0 @@ -from logging import getLogger - -from pyramid.events import BeforeTraversal -from pyramid.httpexceptions import HTTPException -from pyramid.settings import asbool -from pyramid.tweens import EXCVIEW - -import opentelemetry.instrumentation.wsgi as otel_wsgi -from opentelemetry import configuration, context, propagators, trace -from opentelemetry.instrumentation.pyramid.version import __version__ -from opentelemetry.util import ExcludeList, time_ns - -TWEEN_NAME = "opentelemetry.instrumentation.pyramid.trace_tween_factory" -SETTING_TRACE_ENABLED = "opentelemetry-pyramid.trace_enabled" - -_ENVIRON_STARTTIME_KEY = "opentelemetry-pyramid.starttime_key" -_ENVIRON_SPAN_KEY = "opentelemetry-pyramid.span_key" -_ENVIRON_ACTIVATION_KEY = "opentelemetry-pyramid.activation_key" -_ENVIRON_ENABLED_KEY = "opentelemetry-pyramid.tracing_enabled_key" -_ENVIRON_TOKEN = "opentelemetry-pyramid.token" - -_logger = getLogger(__name__) - - -def get_excluded_urls(): - urls = configuration.Configuration().PYRAMID_EXCLUDED_URLS or [] - if urls: - urls = str.split(urls, ",") - return ExcludeList(urls) - - -_excluded_urls = get_excluded_urls() - - -def includeme(config): - config.add_settings({SETTING_TRACE_ENABLED: True}) - - config.add_subscriber(_before_traversal, BeforeTraversal) - _insert_tween(config) - - -def _insert_tween(config): - settings = config.get_settings() - tweens = settings.get("pyramid.tweens") - # If the list is empty, pyramid does not consider the tweens have been - # set explicitly. And if our tween is already there, nothing to do - if not tweens or not tweens.strip(): - # Add our tween just before the default exception handler - config.add_tween(TWEEN_NAME, over=EXCVIEW) - - -def _before_traversal(event): - request = event.request - environ = request.environ - span_name = otel_wsgi.get_default_span_name(environ) - - enabled = environ.get(_ENVIRON_ENABLED_KEY) - if enabled is None: - _logger.warning( - "Opentelemetry pyramid tween 'opentelemetry.instrumentation.pyramid.trace_tween_factory'" - "was not called. Make sure that the tween is included in 'pyramid.tweens' if" - "the tween list was created manually" - ) - return - - if not enabled: - # Tracing not enabled, return - return - - start_time = environ.get(_ENVIRON_STARTTIME_KEY) - - token = context.attach( - propagators.extract(otel_wsgi.carrier_getter, environ) - ) - tracer = trace.get_tracer(__name__, __version__) - - if request.matched_route: - span_name = request.matched_route.pattern - else: - span_name = otel_wsgi.get_default_span_name(environ) - - span = tracer.start_span( - span_name, kind=trace.SpanKind.SERVER, start_time=start_time, - ) - - if span.is_recording(): - attributes = otel_wsgi.collect_request_attributes(environ) - if request.matched_route: - attributes["http.route"] = request.matched_route.pattern - for key, value in attributes.items(): - span.set_attribute(key, value) - - activation = tracer.use_span(span, end_on_exit=True) - activation.__enter__() - environ[_ENVIRON_ACTIVATION_KEY] = activation - environ[_ENVIRON_SPAN_KEY] = span - environ[_ENVIRON_TOKEN] = token - - -def trace_tween_factory(handler, registry): - settings = registry.settings - enabled = asbool(settings.get(SETTING_TRACE_ENABLED, True)) - - if not enabled: - # If disabled, make a tween that signals to the - # BeforeTraversal subscriber that tracing is disabled - def disabled_tween(request): - request.environ[_ENVIRON_ENABLED_KEY] = False - return handler(request) - - return disabled_tween - - # make a request tracing function - def trace_tween(request): - if _excluded_urls.url_disabled(request.url): - request.environ[_ENVIRON_ENABLED_KEY] = False - # short-circuit when we don't want to trace anything - return handler(request) - - request.environ[_ENVIRON_ENABLED_KEY] = True - request.environ[_ENVIRON_STARTTIME_KEY] = time_ns() - - try: - response = handler(request) - response_or_exception = response - except HTTPException as exc: - # If the exception is a pyramid HTTPException, - # that's still valuable information that isn't necessarily - # a 500. For instance, HTTPFound is a 302. - # As described in docs, Pyramid exceptions are all valid - # response types - response_or_exception = exc - raise - finally: - span = request.environ.get(_ENVIRON_SPAN_KEY) - enabled = request.environ.get(_ENVIRON_ENABLED_KEY) - if not span and enabled: - _logger.warning( - "Pyramid environ's OpenTelemetry span missing." - "If the OpenTelemetry tween was added manually, make sure" - "PyramidInstrumentor().instrument_config(config) is called" - ) - elif enabled: - otel_wsgi.add_response_attributes( - span, - response_or_exception.status, - response_or_exception.headers, - ) - - activation = request.environ.get(_ENVIRON_ACTIVATION_KEY) - - if isinstance(response_or_exception, HTTPException): - activation.__exit__( - type(response_or_exception), - response_or_exception, - getattr(response_or_exception, "__traceback__", None), - ) - else: - activation.__exit__(None, None, None) - - context.detach(request.environ.get(_ENVIRON_TOKEN)) - - return response - - return trace_tween diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py deleted file mode 100644 index 21a6a1ab95..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pyramid.httpexceptions as exc -from pyramid.response import Response -from werkzeug.test import Client -from werkzeug.wrappers import BaseResponse - -from opentelemetry.configuration import Configuration - - -class InstrumentationTest: - def setUp(self): # pylint: disable=invalid-name - super().setUp() # pylint: disable=no-member - Configuration._reset() # pylint: disable=protected-access - - @staticmethod - def _hello_endpoint(request): - helloid = int(request.matchdict["helloid"]) - if helloid == 500: - raise exc.HTTPInternalServerError() - return Response("Hello: " + str(helloid)) - - def _common_initialization(self, config): - # pylint: disable=unused-argument - def excluded_endpoint(request): - return Response("excluded") - - # pylint: disable=unused-argument - def excluded2_endpoint(request): - return Response("excluded2") - - config.add_route("hello", "/hello/{helloid}") - config.add_view(self._hello_endpoint, route_name="hello") - config.add_route("excluded_arg", "/excluded/{helloid}") - config.add_view(self._hello_endpoint, route_name="excluded_arg") - config.add_route("excluded", "/excluded_noarg") - config.add_view(excluded_endpoint, route_name="excluded") - config.add_route("excluded2", "/excluded_noarg2") - config.add_view(excluded2_endpoint, route_name="excluded2") - - # pylint: disable=attribute-defined-outside-init - self.client = Client(config.make_wsgi_app(), BaseResponse) diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py deleted file mode 100644 index b065e26064..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from pyramid.config import Configurator - -from opentelemetry.instrumentation.pyramid import PyramidInstrumentor -from opentelemetry.test.test_base import TestBase -from opentelemetry.test.wsgitestutil import WsgiTestBase - -# pylint: disable=import-error -from .pyramid_base_test import InstrumentationTest - - -class TestAutomatic(InstrumentationTest, TestBase, WsgiTestBase): - def setUp(self): - super().setUp() - - PyramidInstrumentor().instrument() - - self.config = Configurator() - - self._common_initialization(self.config) - - def tearDown(self): - super().tearDown() - with self.disable_logging(): - PyramidInstrumentor().uninstrument() - - def test_uninstrument(self): - # pylint: disable=access-member-before-definition - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - PyramidInstrumentor().uninstrument() - self.config = Configurator() - - self._common_initialization(self.config) - - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - def test_tween_list(self): - tween_list = "pyramid.tweens.excview_tween_factory" - config = Configurator(settings={"pyramid.tweens": tween_list}) - self._common_initialization(config) - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - PyramidInstrumentor().uninstrument() - - self.config = Configurator() - - self._common_initialization(self.config) - - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py deleted file mode 100644 index 77427b0db7..0000000000 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py +++ /dev/null @@ -1,209 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest.mock import Mock, patch - -from pyramid.config import Configurator - -from opentelemetry import trace -from opentelemetry.instrumentation.pyramid import PyramidInstrumentor -from opentelemetry.test.test_base import TestBase -from opentelemetry.test.wsgitestutil import WsgiTestBase -from opentelemetry.util import ExcludeList - -# pylint: disable=import-error -from .pyramid_base_test import InstrumentationTest - - -def expected_attributes(override_attributes): - default_attributes = { - "component": "http", - "http.method": "GET", - "http.server_name": "localhost", - "http.scheme": "http", - "host.port": 80, - "http.host": "localhost", - "http.target": "/", - "http.flavor": "1.1", - "http.status_text": "OK", - "http.status_code": 200, - } - for key, val in override_attributes.items(): - default_attributes[key] = val - return default_attributes - - -class TestProgrammatic(InstrumentationTest, TestBase, WsgiTestBase): - def setUp(self): - super().setUp() - config = Configurator() - PyramidInstrumentor().instrument_config(config) - - self.config = config - - self._common_initialization(self.config) - - def tearDown(self): - super().tearDown() - with self.disable_logging(): - PyramidInstrumentor().uninstrument_config(self.config) - - def test_uninstrument(self): - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - PyramidInstrumentor().uninstrument_config(self.config) - # Need to remake the WSGI app export - self._common_initialization(self.config) - - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - def test_simple(self): - expected_attrs = expected_attributes( - {"http.target": "/hello/123", "http.route": "/hello/{helloid}"} - ) - self.client.get("/hello/123") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "/hello/{helloid}") - self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) - self.assertEqual(span_list[0].attributes, expected_attrs) - - def test_not_recording(self): - mock_tracer = Mock() - mock_span = Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = True - with patch("opentelemetry.trace.get_tracer"): - self.client.get("/hello/123") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_404(self): - expected_attrs = expected_attributes( - { - "http.method": "POST", - "http.target": "/bye", - "http.status_text": "Not Found", - "http.status_code": 404, - } - ) - - resp = self.client.post("/bye") - self.assertEqual(404, resp.status_code) - resp.close() - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "HTTP POST") - self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) - self.assertEqual(span_list[0].attributes, expected_attrs) - - def test_internal_error(self): - expected_attrs = expected_attributes( - { - "http.target": "/hello/500", - "http.route": "/hello/{helloid}", - "http.status_text": "Internal Server Error", - "http.status_code": 500, - } - ) - resp = self.client.get("/hello/500") - self.assertEqual(500, resp.status_code) - resp.close() - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "/hello/{helloid}") - self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) - self.assertEqual(span_list[0].attributes, expected_attrs) - - def test_tween_list(self): - tween_list = "opentelemetry.instrumentation.pyramid.trace_tween_factory\npyramid.tweens.excview_tween_factory" - config = Configurator(settings={"pyramid.tweens": tween_list}) - PyramidInstrumentor().instrument_config(config) - self._common_initialization(config) - - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - PyramidInstrumentor().uninstrument_config(config) - # Need to remake the WSGI app export - self._common_initialization(config) - - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - @patch("opentelemetry.instrumentation.pyramid.callbacks._logger") - def test_warnings(self, mock_logger): - tween_list = "pyramid.tweens.excview_tween_factory" - config = Configurator(settings={"pyramid.tweens": tween_list}) - PyramidInstrumentor().instrument_config(config) - self._common_initialization(config) - - self.client.get("/hello/123") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) - self.assertEqual(mock_logger.warning.called, True) - - mock_logger.warning.called = False - - tween_list = ( - "opentelemetry.instrumentation.pyramid.trace_tween_factory" - ) - config = Configurator(settings={"pyramid.tweens": tween_list}) - self._common_initialization(config) - - self.client.get("/hello/123") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) - self.assertEqual(mock_logger.warning.called, True) - - @patch( - "opentelemetry.instrumentation.pyramid.callbacks._excluded_urls", - ExcludeList(["http://localhost/excluded_arg/123", "excluded_noarg"]), - ) - def test_exclude_lists(self): - self.client.get("/excluded_arg/123") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) - - self.client.get("/excluded_arg/125") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - self.client.get("/excluded_noarg") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - self.client.get("/excluded_noarg2") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) diff --git a/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md deleted file mode 100644 index 8f2d5f7e84..0000000000 --- a/instrumentation/opentelemetry-instrumentation-redis/CHANGELOG.md +++ /dev/null @@ -1,24 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Update default SpanKind to `SpanKind.CLIENT` ([#965](https://github.com/open-telemetry/opentelemetry-python/pull/965)) -- Change package name to opentelemetry-instrumentation-redis - ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - -## 0.7b1 - -Released 2020-05-12 - -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-redis/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-redis/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-redis/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-redis/README.rst b/instrumentation/opentelemetry-instrumentation-redis/README.rst deleted file mode 100644 index 1a071ad0fe..0000000000 --- a/instrumentation/opentelemetry-instrumentation-redis/README.rst +++ /dev/null @@ -1,23 +0,0 @@ -OpenTelemetry Redis Instrumentation -=================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-redis.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-redis/ - -This library allows tracing requests made by the Redis library. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-redis - - -References ----------- - -* `OpenTelemetry Redis Instrumentation `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg b/instrumentation/opentelemetry-instrumentation-redis/setup.cfg deleted file mode 100644 index 186e167dea..0000000000 --- a/instrumentation/opentelemetry-instrumentation-redis/setup.cfg +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-redis -description = OpenTelemetry Redis instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-redis -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - redis >= 2.6 - wrapt >= 1.12.1 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - opentelemetry-sdk == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - redis = opentelemetry.instrumentation.redis:RedisInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-redis/setup.py b/instrumentation/opentelemetry-instrumentation-redis/setup.py deleted file mode 100644 index df80a8fd1a..0000000000 --- a/instrumentation/opentelemetry-instrumentation-redis/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "redis", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py deleted file mode 100644 index e1c5db1e94..0000000000 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -Instrument `redis`_ to report Redis queries. - -There are two options for instrumenting code. The first option is to use the -``opentelemetry-instrumentation`` executable which will automatically -instrument your Redis client. The second is to programmatically enable -instrumentation via the following code: - -.. _redis: https://pypi.org/project/redis/ - -Usage ------ - -.. code:: python - - from opentelemetry import trace - from opentelemetry.instrumentation.redis import RedisInstrumentor - from opentelemetry.sdk.trace import TracerProvider - import redis - - trace.set_tracer_provider(TracerProvider()) - - # Instrument redis - RedisInstrumentor().instrument(tracer_provider=trace.get_tracer_provider()) - - # This will report a span with the default settings - client = redis.StrictRedis(host="localhost", port=6379) - client.get("my-key") - -API ---- -""" - -import redis -from wrapt import ObjectProxy, wrap_function_wrapper - -from opentelemetry import trace -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.redis.util import ( - _extract_conn_attributes, - _format_command_args, -) -from opentelemetry.instrumentation.redis.version import __version__ -from opentelemetry.instrumentation.utils import unwrap - -_DEFAULT_SERVICE = "redis" -_RAWCMD = "db.statement" -_CMD = "redis.command" - - -def _set_connection_attributes(span, conn): - if not span.is_recording(): - return - for key, value in _extract_conn_attributes( - conn.connection_pool.connection_kwargs - ).items(): - span.set_attribute(key, value) - - -def _traced_execute_command(func, instance, args, kwargs): - tracer = getattr(redis, "_opentelemetry_tracer") - query = _format_command_args(args) - with tracer.start_as_current_span( - _CMD, kind=trace.SpanKind.CLIENT - ) as span: - if span.is_recording(): - span.set_attribute("service", tracer.instrumentation_info.name) - span.set_attribute(_RAWCMD, query) - _set_connection_attributes(span, instance) - span.set_attribute("redis.args_length", len(args)) - return func(*args, **kwargs) - - -def _traced_execute_pipeline(func, instance, args, kwargs): - tracer = getattr(redis, "_opentelemetry_tracer") - - cmds = [_format_command_args(c) for c, _ in instance.command_stack] - resource = "\n".join(cmds) - - with tracer.start_as_current_span( - _CMD, kind=trace.SpanKind.CLIENT - ) as span: - if span.is_recording(): - span.set_attribute("service", tracer.instrumentation_info.name) - span.set_attribute(_RAWCMD, resource) - _set_connection_attributes(span, instance) - span.set_attribute( - "redis.pipeline_length", len(instance.command_stack) - ) - return func(*args, **kwargs) - - -class RedisInstrumentor(BaseInstrumentor): - """An instrumentor for Redis - See `BaseInstrumentor` - """ - - def _instrument(self, **kwargs): - tracer_provider = kwargs.get( - "tracer_provider", trace.get_tracer_provider() - ) - setattr( - redis, - "_opentelemetry_tracer", - tracer_provider.get_tracer(_DEFAULT_SERVICE, __version__), - ) - - if redis.VERSION < (3, 0, 0): - wrap_function_wrapper( - "redis", "StrictRedis.execute_command", _traced_execute_command - ) - wrap_function_wrapper( - "redis.client", - "BasePipeline.execute", - _traced_execute_pipeline, - ) - wrap_function_wrapper( - "redis.client", - "BasePipeline.immediate_execute_command", - _traced_execute_command, - ) - else: - wrap_function_wrapper( - "redis", "Redis.execute_command", _traced_execute_command - ) - wrap_function_wrapper( - "redis.client", "Pipeline.execute", _traced_execute_pipeline - ) - wrap_function_wrapper( - "redis.client", - "Pipeline.immediate_execute_command", - _traced_execute_command, - ) - - def _uninstrument(self, **kwargs): - if redis.VERSION < (3, 0, 0): - unwrap(redis.StrictRedis, "execute_command") - unwrap(redis.StrictRedis, "pipeline") - unwrap(redis.Redis, "pipeline") - unwrap( - redis.client.BasePipeline, # pylint:disable=no-member - "execute", - ) - unwrap( - redis.client.BasePipeline, # pylint:disable=no-member - "immediate_execute_command", - ) - else: - unwrap(redis.Redis, "execute_command") - unwrap(redis.Redis, "pipeline") - unwrap(redis.client.Pipeline, "execute") - unwrap(redis.client.Pipeline, "immediate_execute_command") diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py deleted file mode 100644 index 2895134089..0000000000 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -Some utils used by the redis integration -""" - - -def _extract_conn_attributes(conn_kwargs): - """ Transform redis conn info into dict """ - attributes = { - "db.type": "redis", - "db.instance": conn_kwargs.get("db", 0), - } - try: - attributes["db.url"] = "redis://{}:{}".format( - conn_kwargs["host"], conn_kwargs["port"] - ) - except KeyError: - pass # don't include url attribute - - return attributes - - -def _format_command_args(args): - """Format command arguments and trim them as needed""" - value_max_len = 100 - value_too_long_mark = "..." - cmd_max_len = 1000 - length = 0 - out = [] - for arg in args: - cmd = str(arg) - - if len(cmd) > value_max_len: - cmd = cmd[:value_max_len] + value_too_long_mark - - if length + len(cmd) > cmd_max_len: - prefix = cmd[: cmd_max_len - length] - out.append("%s%s" % (prefix, value_too_long_mark)) - break - - out.append(cmd) - length += len(cmd) - - return " ".join(out) diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-redis/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-redis/tests/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/instrumentation/opentelemetry-instrumentation-redis/tests/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py deleted file mode 100644 index 3e07ac725e..0000000000 --- a/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from unittest import mock - -import redis - -from opentelemetry.instrumentation.redis import RedisInstrumentor -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace import SpanKind - - -class TestRedis(TestBase): - def test_span_properties(self): - redis_client = redis.Redis() - RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) - - with mock.patch.object(redis_client, "connection"): - redis_client.get("key") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertEqual(span.name, "redis.command") - self.assertEqual(span.kind, SpanKind.CLIENT) - - def test_not_recording(self): - redis_client = redis.Redis() - RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) - - mock_tracer = mock.Mock() - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = True - with mock.patch("opentelemetry.trace.get_tracer") as tracer: - with mock.patch.object(redis_client, "connection"): - tracer.return_value = mock_tracer - redis_client.get("key") - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_instrument_uninstrument(self): - redis_client = redis.Redis() - RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) - - with mock.patch.object(redis_client, "connection"): - redis_client.get("key") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - self.memory_exporter.clear() - - # Test uninstrument - RedisInstrumentor().uninstrument() - - with mock.patch.object(redis_client, "connection"): - redis_client.get("key") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 0) - self.memory_exporter.clear() - - # Test instrument again - RedisInstrumentor().instrument() - - with mock.patch.object(redis_client, "connection"): - redis_client.get("key") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) diff --git a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md deleted file mode 100644 index 00d730f4f4..0000000000 --- a/instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md +++ /dev/null @@ -1,59 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.15b0 - -Released 2020-11-02 - -- Add support for tracking http metrics - ([#1230](https://github.com/open-telemetry/opentelemetry-python/pull/1230)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Add support for instrumenting prepared requests - ([#1040](https://github.com/open-telemetry/opentelemetry-python/pull/1040)) -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) -- Add support for http metrics - ([#1116](https://github.com/open-telemetry/opentelemetry-python/pull/1116)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-requests - ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) -- Span name reported updated to follow semantic conventions to reduce - cardinality ([#972](https://github.com/open-telemetry/opentelemetry-python/pull/972)) - -## 0.7b1 - -Released 2020-05-12 - -- Rename package to opentelemetry-ext-requests - ([#619](https://github.com/open-telemetry/opentelemetry-python/pull/619)) -- Implement instrumentor interface, enabling auto-instrumentation - ([#597](https://github.com/open-telemetry/opentelemetry-python/pull/597)) -- Adding disable_session for more granular instrumentation control - ([#573](https://github.com/open-telemetry/opentelemetry-python/pull/573)) -- Add a callback for custom attributes - ([#656](https://github.com/open-telemetry/opentelemetry-python/pull/656)) - -## 0.3a0 - -Released 2019-10-29 - -## 0.2a0 - -Released 2019-10-29 - -- Updates for core library changes - -## 0.1a0 - -Released 2019-09-30 - -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-requests/LICENSE b/instrumentation/opentelemetry-instrumentation-requests/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-requests/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-requests/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-requests/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-requests/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-requests/README.rst b/instrumentation/opentelemetry-instrumentation-requests/README.rst deleted file mode 100644 index d4944d3526..0000000000 --- a/instrumentation/opentelemetry-instrumentation-requests/README.rst +++ /dev/null @@ -1,23 +0,0 @@ -OpenTelemetry Requests Instrumentation -====================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-requests.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-requests/ - -This library allows tracing HTTP requests made by the -`requests `_ library. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-requests - -References ----------- - -* `OpenTelemetry requests Instrumentation `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg b/instrumentation/opentelemetry-instrumentation-requests/setup.cfg deleted file mode 100644 index 8aaec6e84a..0000000000 --- a/instrumentation/opentelemetry-instrumentation-requests/setup.cfg +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-requests -description = OpenTelemetry requests instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-requests -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - requests ~= 2.0 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - httpretty ~= 1.0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - requests = opentelemetry.instrumentation.requests:RequestsInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-requests/setup.py b/instrumentation/opentelemetry-instrumentation-requests/setup.py deleted file mode 100644 index 237fef583b..0000000000 --- a/instrumentation/opentelemetry-instrumentation-requests/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "requests", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py deleted file mode 100644 index b4738647db..0000000000 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ /dev/null @@ -1,251 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This library allows tracing HTTP requests made by the -`requests `_ library. - -Usage ------ - -.. code-block:: python - - import requests - import opentelemetry.instrumentation.requests - - # You can optionally pass a custom TracerProvider to - # RequestInstrumentor.instrument() - opentelemetry.instrumentation.requests.RequestsInstrumentor().instrument() - response = requests.get(url="https://www.example.org/") - -API ---- -""" - -import functools -import types - -from requests import Timeout, URLRequired -from requests.exceptions import InvalidSchema, InvalidURL, MissingSchema -from requests.sessions import Session -from requests.structures import CaseInsensitiveDict - -from opentelemetry import context, propagators -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.metric import ( - HTTPMetricRecorder, - HTTPMetricType, - MetricMixin, -) -from opentelemetry.instrumentation.requests.version import __version__ -from opentelemetry.instrumentation.utils import http_status_to_status_code -from opentelemetry.trace import SpanKind, get_tracer -from opentelemetry.trace.status import ( - EXCEPTION_STATUS_FIELD, - Status, - StatusCode, -) - -# A key to a context variable to avoid creating duplicate spans when instrumenting -# both, Session.request and Session.send, since Session.request calls into Session.send -_SUPPRESS_REQUESTS_INSTRUMENTATION_KEY = "suppress_requests_instrumentation" - - -# pylint: disable=unused-argument -# pylint: disable=R0915 -def _instrument(tracer_provider=None, span_callback=None): - """Enables tracing of all requests calls that go through - :code:`requests.session.Session.request` (this includes - :code:`requests.get`, etc.).""" - - # Since - # https://github.com/psf/requests/commit/d72d1162142d1bf8b1b5711c664fbbd674f349d1 - # (v0.7.0, Oct 23, 2011), get, post, etc are implemented via request which - # again, is implemented via Session.request (`Session` was named `session` - # before v1.0.0, Dec 17, 2012, see - # https://github.com/psf/requests/commit/4e5c4a6ab7bb0195dececdd19bb8505b872fe120) - - wrapped_request = Session.request - wrapped_send = Session.send - - @functools.wraps(wrapped_request) - def instrumented_request(self, method, url, *args, **kwargs): - def get_or_create_headers(): - headers = kwargs.get("headers") - if headers is None: - headers = {} - kwargs["headers"] = headers - - return headers - - def call_wrapped(): - return wrapped_request(self, method, url, *args, **kwargs) - - return _instrumented_requests_call( - method, url, call_wrapped, get_or_create_headers - ) - - @functools.wraps(wrapped_send) - def instrumented_send(self, request, **kwargs): - def get_or_create_headers(): - request.headers = ( - request.headers - if request.headers is not None - else CaseInsensitiveDict() - ) - return request.headers - - def call_wrapped(): - return wrapped_send(self, request, **kwargs) - - return _instrumented_requests_call( - request.method, request.url, call_wrapped, get_or_create_headers - ) - - def _instrumented_requests_call( - method: str, url: str, call_wrapped, get_or_create_headers - ): - if context.get_value("suppress_instrumentation") or context.get_value( - _SUPPRESS_REQUESTS_INSTRUMENTATION_KEY - ): - return call_wrapped() - - # See - # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#http-client - method = method.upper() - span_name = "HTTP {}".format(method) - - recorder = RequestsInstrumentor().metric_recorder - - labels = {} - labels["http.method"] = method - labels["http.url"] = url - - with get_tracer( - __name__, __version__, tracer_provider - ).start_as_current_span(span_name, kind=SpanKind.CLIENT) as span: - exception = None - with recorder.record_client_duration(labels): - if span.is_recording(): - span.set_attribute("component", "http") - span.set_attribute("http.method", method) - span.set_attribute("http.url", url) - - headers = get_or_create_headers() - propagators.inject(type(headers).__setitem__, headers) - - token = context.attach( - context.set_value( - _SUPPRESS_REQUESTS_INSTRUMENTATION_KEY, True - ) - ) - try: - result = call_wrapped() # *** PROCEED - except Exception as exc: # pylint: disable=W0703 - exception = exc - setattr( - exception, EXCEPTION_STATUS_FIELD, StatusCode.ERROR, - ) - result = getattr(exc, "response", None) - finally: - context.detach(token) - - if result is not None: - if span.is_recording(): - span.set_attribute( - "http.status_code", result.status_code - ) - span.set_attribute("http.status_text", result.reason) - span.set_status( - Status( - http_status_to_status_code(result.status_code) - ) - ) - labels["http.status_code"] = str(result.status_code) - if result.raw and result.raw.version: - labels["http.flavor"] = ( - str(result.raw.version)[:1] - + "." - + str(result.raw.version)[:-1] - ) - if span_callback is not None: - span_callback(span, result) - - if exception is not None: - raise exception.with_traceback(exception.__traceback__) - - return result - - instrumented_request.opentelemetry_instrumentation_requests_applied = True - Session.request = instrumented_request - - instrumented_send.opentelemetry_instrumentation_requests_applied = True - Session.send = instrumented_send - - -def _uninstrument(): - """Disables instrumentation of :code:`requests` through this module. - - Note that this only works if no other module also patches requests.""" - _uninstrument_from(Session) - - -def _uninstrument_from(instr_root, restore_as_bound_func=False): - for instr_func_name in ("request", "send"): - instr_func = getattr(instr_root, instr_func_name) - if not getattr( - instr_func, - "opentelemetry_instrumentation_requests_applied", - False, - ): - continue - - original = instr_func.__wrapped__ # pylint:disable=no-member - if restore_as_bound_func: - original = types.MethodType(original, instr_root) - setattr(instr_root, instr_func_name, original) - - -class RequestsInstrumentor(BaseInstrumentor, MetricMixin): - """An instrumentor for requests - See `BaseInstrumentor` - """ - - def _instrument(self, **kwargs): - """Instruments requests module - - Args: - **kwargs: Optional arguments - ``tracer_provider``: a TracerProvider, defaults to global - ``span_callback``: An optional callback invoked before returning the http response. Invoked with Span and requests.Response - """ - _instrument( - tracer_provider=kwargs.get("tracer_provider"), - span_callback=kwargs.get("span_callback"), - ) - self.init_metrics( - __name__, __version__, - ) - # pylint: disable=W0201 - self.metric_recorder = HTTPMetricRecorder( - self.meter, HTTPMetricType.CLIENT - ) - - def _uninstrument(self, **kwargs): - _uninstrument() - - @staticmethod - def uninstrument_session(session): - """Disables instrumentation on the session object.""" - _uninstrument_from(session, restore_as_bound_func=True) diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py deleted file mode 100644 index f5209108e3..0000000000 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py +++ /dev/null @@ -1,380 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import abc -from unittest import mock - -import httpretty -import requests - -import opentelemetry.instrumentation.requests -from opentelemetry import context, propagators, trace -from opentelemetry.instrumentation.requests import RequestsInstrumentor -from opentelemetry.sdk import resources -from opentelemetry.sdk.util import get_dict_as_key -from opentelemetry.test.mock_textmap import MockTextMapPropagator -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace.status import StatusCode - - -class RequestsIntegrationTestBase(abc.ABC): - # pylint: disable=no-member - - URL = "http://httpbin.org/status/200" - - # pylint: disable=invalid-name - def setUp(self): - super().setUp() - RequestsInstrumentor().instrument() - httpretty.enable() - httpretty.register_uri(httpretty.GET, self.URL, body="Hello!") - - # pylint: disable=invalid-name - def tearDown(self): - super().tearDown() - RequestsInstrumentor().uninstrument() - httpretty.disable() - - def assert_span(self, exporter=None, num_spans=1): - if exporter is None: - exporter = self.memory_exporter - span_list = exporter.get_finished_spans() - self.assertEqual(num_spans, len(span_list)) - if num_spans == 0: - return None - if num_spans == 1: - return span_list[0] - return span_list - - @staticmethod - @abc.abstractmethod - def perform_request(url: str, session: requests.Session = None): - pass - - def test_basic(self): - result = self.perform_request(self.URL) - self.assertEqual(result.text, "Hello!") - span = self.assert_span() - - self.assertIs(span.kind, trace.SpanKind.CLIENT) - self.assertEqual(span.name, "HTTP GET") - - self.assertEqual( - span.attributes, - { - "component": "http", - "http.method": "GET", - "http.url": self.URL, - "http.status_code": 200, - "http.status_text": "OK", - }, - ) - - self.assertIs(span.status.status_code, trace.status.StatusCode.UNSET) - - self.check_span_instrumentation_info( - span, opentelemetry.instrumentation.requests - ) - - self.assertIsNotNone(RequestsInstrumentor().meter) - self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1) - recorder = RequestsInstrumentor().meter.metrics.pop() - match_key = get_dict_as_key( - { - "http.flavor": "1.1", - "http.method": "GET", - "http.status_code": "200", - "http.url": "http://httpbin.org/status/200", - } - ) - for key in recorder.bound_instruments.keys(): - self.assertEqual(key, match_key) - # pylint: disable=protected-access - bound = recorder.bound_instruments.get(key) - for view_data in bound.view_datas: - self.assertEqual(view_data.labels, key) - self.assertEqual(view_data.aggregator.current.count, 1) - self.assertGreaterEqual(view_data.aggregator.current.sum, 0) - - def test_not_foundbasic(self): - url_404 = "http://httpbin.org/status/404" - httpretty.register_uri( - httpretty.GET, url_404, status=404, - ) - result = self.perform_request(url_404) - self.assertEqual(result.status_code, 404) - - span = self.assert_span() - - self.assertEqual(span.attributes.get("http.status_code"), 404) - self.assertEqual(span.attributes.get("http.status_text"), "Not Found") - - self.assertIs( - span.status.status_code, trace.status.StatusCode.ERROR, - ) - - def test_uninstrument(self): - RequestsInstrumentor().uninstrument() - result = self.perform_request(self.URL) - self.assertEqual(result.text, "Hello!") - self.assert_span(num_spans=0) - # instrument again to avoid annoying warning message - RequestsInstrumentor().instrument() - - def test_uninstrument_session(self): - session1 = requests.Session() - RequestsInstrumentor().uninstrument_session(session1) - - result = self.perform_request(self.URL, session1) - self.assertEqual(result.text, "Hello!") - self.assert_span(num_spans=0) - - # Test that other sessions as well as global requests is still - # instrumented - session2 = requests.Session() - result = self.perform_request(self.URL, session2) - self.assertEqual(result.text, "Hello!") - self.assert_span() - - self.memory_exporter.clear() - - result = self.perform_request(self.URL) - self.assertEqual(result.text, "Hello!") - self.assert_span() - - def test_suppress_instrumentation(self): - token = context.attach( - context.set_value("suppress_instrumentation", True) - ) - try: - result = self.perform_request(self.URL) - self.assertEqual(result.text, "Hello!") - finally: - context.detach(token) - - self.assert_span(num_spans=0) - - def test_not_recording(self): - with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span: - RequestsInstrumentor().uninstrument() - # original_tracer_provider returns a default tracer provider, which - # in turn will return an INVALID_SPAN, which is always not recording - RequestsInstrumentor().instrument( - tracer_provider=self.original_tracer_provider - ) - mock_span.is_recording.return_value = False - result = self.perform_request(self.URL) - self.assertEqual(result.text, "Hello!") - self.assert_span(None, 0) - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_distributed_context(self): - previous_propagator = propagators.get_global_textmap() - try: - propagators.set_global_textmap(MockTextMapPropagator()) - result = self.perform_request(self.URL) - self.assertEqual(result.text, "Hello!") - - span = self.assert_span() - - headers = dict(httpretty.last_request().headers) - self.assertIn(MockTextMapPropagator.TRACE_ID_KEY, headers) - self.assertEqual( - str(span.get_span_context().trace_id), - headers[MockTextMapPropagator.TRACE_ID_KEY], - ) - self.assertIn(MockTextMapPropagator.SPAN_ID_KEY, headers) - self.assertEqual( - str(span.get_span_context().span_id), - headers[MockTextMapPropagator.SPAN_ID_KEY], - ) - - finally: - propagators.set_global_textmap(previous_propagator) - - def test_span_callback(self): - RequestsInstrumentor().uninstrument() - - def span_callback(span, result: requests.Response): - span.set_attribute( - "http.response.body", result.content.decode("utf-8") - ) - - RequestsInstrumentor().instrument( - tracer_provider=self.tracer_provider, span_callback=span_callback, - ) - - result = self.perform_request(self.URL) - self.assertEqual(result.text, "Hello!") - - span = self.assert_span() - self.assertEqual( - span.attributes, - { - "component": "http", - "http.method": "GET", - "http.url": self.URL, - "http.status_code": 200, - "http.status_text": "OK", - "http.response.body": "Hello!", - }, - ) - - def test_custom_tracer_provider(self): - resource = resources.Resource.create({}) - result = self.create_tracer_provider(resource=resource) - tracer_provider, exporter = result - RequestsInstrumentor().uninstrument() - RequestsInstrumentor().instrument(tracer_provider=tracer_provider) - - result = self.perform_request(self.URL) - self.assertEqual(result.text, "Hello!") - - span = self.assert_span(exporter=exporter) - self.assertIs(span.resource, resource) - - @mock.patch( - "requests.adapters.HTTPAdapter.send", - side_effect=requests.RequestException, - ) - def test_requests_exception_without_response(self, *_, **__): - with self.assertRaises(requests.RequestException): - self.perform_request(self.URL) - - span = self.assert_span() - self.assertEqual( - span.attributes, - {"component": "http", "http.method": "GET", "http.url": self.URL}, - ) - self.assertEqual(span.status.status_code, StatusCode.ERROR) - - self.assertIsNotNone(RequestsInstrumentor().meter) - self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1) - recorder = RequestsInstrumentor().meter.metrics.pop() - match_key = get_dict_as_key( - { - "http.method": "GET", - "http.url": "http://httpbin.org/status/200", - } - ) - for key in recorder.bound_instruments.keys(): - self.assertEqual(key, match_key) - # pylint: disable=protected-access - bound = recorder.bound_instruments.get(key) - for view_data in bound.view_datas: - self.assertEqual(view_data.labels, key) - self.assertEqual(view_data.aggregator.current.count, 1) - - mocked_response = requests.Response() - mocked_response.status_code = 500 - mocked_response.reason = "Internal Server Error" - - @mock.patch( - "requests.adapters.HTTPAdapter.send", - side_effect=requests.RequestException(response=mocked_response), - ) - def test_requests_exception_with_response(self, *_, **__): - with self.assertRaises(requests.RequestException): - self.perform_request(self.URL) - - span = self.assert_span() - self.assertEqual( - span.attributes, - { - "component": "http", - "http.method": "GET", - "http.url": self.URL, - "http.status_code": 500, - "http.status_text": "Internal Server Error", - }, - ) - self.assertEqual(span.status.status_code, StatusCode.ERROR) - self.assertIsNotNone(RequestsInstrumentor().meter) - self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1) - recorder = RequestsInstrumentor().meter.metrics.pop() - match_key = get_dict_as_key( - { - "http.method": "GET", - "http.status_code": "500", - "http.url": "http://httpbin.org/status/200", - } - ) - for key in recorder.bound_instruments.keys(): - self.assertEqual(key, match_key) - # pylint: disable=protected-access - bound = recorder.bound_instruments.get(key) - for view_data in bound.view_datas: - self.assertEqual(view_data.labels, key) - self.assertEqual(view_data.aggregator.current.count, 1) - - @mock.patch("requests.adapters.HTTPAdapter.send", side_effect=Exception) - def test_requests_basic_exception(self, *_, **__): - with self.assertRaises(Exception): - self.perform_request(self.URL) - - span = self.assert_span() - self.assertEqual(span.status.status_code, StatusCode.ERROR) - - @mock.patch( - "requests.adapters.HTTPAdapter.send", side_effect=requests.Timeout - ) - def test_requests_timeout_exception(self, *_, **__): - with self.assertRaises(Exception): - self.perform_request(self.URL) - - span = self.assert_span() - self.assertEqual(span.status.status_code, StatusCode.ERROR) - - -class TestRequestsIntegration(RequestsIntegrationTestBase, TestBase): - @staticmethod - def perform_request(url: str, session: requests.Session = None): - if session is None: - return requests.get(url) - return session.get(url) - - def test_invalid_url(self): - url = "http://[::1/nope" - - with self.assertRaises(ValueError): - requests.post(url) - - span = self.assert_span() - - self.assertEqual(span.name, "HTTP POST") - self.assertEqual( - span.attributes, - {"component": "http", "http.method": "POST", "http.url": url}, - ) - self.assertEqual(span.status.status_code, StatusCode.ERROR) - - def test_if_headers_equals_none(self): - result = requests.get(self.URL, headers=None) - self.assertEqual(result.text, "Hello!") - self.assert_span() - - -class TestRequestsIntegrationPreparedRequest( - RequestsIntegrationTestBase, TestBase -): - @staticmethod - def perform_request(url: str, session: requests.Session = None): - if session is None: - session = requests.Session() - request = requests.Request("GET", url) - prepared_request = session.prepare_request(request) - return session.send(prepared_request) diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md deleted file mode 100644 index 73fa087a11..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-sqlalchemy - ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - -## 0.7b1 - -Released 2020-05-12 - -- Initial release \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/LICENSE b/instrumentation/opentelemetry-instrumentation-sqlalchemy/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-sqlalchemy/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/README.rst b/instrumentation/opentelemetry-instrumentation-sqlalchemy/README.rst deleted file mode 100644 index f29cbe9fff..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/README.rst +++ /dev/null @@ -1,24 +0,0 @@ -OpenTelemetry SQLAlchemy Instrumentation -======================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-sqlalchemy.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-sqlalchemy/ - -This library allows tracing requests made by the SQLAlchemy library. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-sqlalchemy - - -References ----------- - -* `SQLAlchemy Project `_ -* `OpenTelemetry SQLAlchemy Tracing `_ -* `OpenTelemetry Project `_ \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg deleted file mode 100644 index c6b44013ce..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.cfg +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-sqlalchemy -description = OpenTelemetry SQLAlchemy instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-sqlalchemy -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - wrapt >= 1.11.2 - sqlalchemy - -[options.extras_require] -test = - opentelemetry-sdk == 0.16.dev0 - pytest - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - sqlalchemy = opentelemetry.instrumentation.sqlalchemy:SQLAlchemyInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.py deleted file mode 100644 index 26d3ef4fc9..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "sqlalchemy", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py deleted file mode 100644 index aad6dbfc07..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Instrument `sqlalchemy`_ to report SQL queries. - -There are two options for instrumenting code. The first option is to use -the ``opentelemetry-instrument`` executable which will automatically -instrument your SQLAlchemy engine. The second is to programmatically enable -instrumentation via the following code: - -.. _sqlalchemy: https://pypi.org/project/sqlalchemy/ - -Usage ------ -.. code:: python - - from sqlalchemy import create_engine - - from opentelemetry import trace - from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor - from opentelemetry.sdk.trace import TracerProvider - import sqlalchemy - - trace.set_tracer_provider(TracerProvider()) - engine = create_engine("sqlite:///:memory:") - SQLAlchemyInstrumentor().instrument( - engine=engine, - service="service-A", - ) - -API ---- -""" -import sqlalchemy -import wrapt -from wrapt import wrap_function_wrapper as _w - -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.sqlalchemy.engine import ( - EngineTracer, - _get_tracer, - _wrap_create_engine, -) -from opentelemetry.instrumentation.utils import unwrap - - -class SQLAlchemyInstrumentor(BaseInstrumentor): - """An instrumentor for SQLAlchemy - See `BaseInstrumentor` - """ - - def _instrument(self, **kwargs): - """Instruments SQLAlchemy engine creation methods and the engine - if passed as an argument. - - Args: - **kwargs: Optional arguments - ``engine``: a SQLAlchemy engine instance - ``tracer_provider``: a TracerProvider, defaults to global - ``service``: the name of the service to trace. - - Returns: - An instrumented engine if passed in as an argument, None otherwise. - """ - _w("sqlalchemy", "create_engine", _wrap_create_engine) - _w("sqlalchemy.engine", "create_engine", _wrap_create_engine) - if kwargs.get("engine") is not None: - return EngineTracer( - _get_tracer( - kwargs.get("engine"), kwargs.get("tracer_provider") - ), - kwargs.get("service"), - kwargs.get("engine"), - ) - return None - - def _uninstrument(self, **kwargs): - unwrap(sqlalchemy, "create_engine") - unwrap(sqlalchemy.engine, "create_engine") diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py deleted file mode 100644 index 7c97c685da..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from sqlalchemy.event import listen - -from opentelemetry import trace -from opentelemetry.instrumentation.sqlalchemy.version import __version__ -from opentelemetry.trace.status import Status, StatusCode - -# Network attribute semantic convention here: -# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/span-general.md#general-network-connection-attributes -_HOST = "net.peer.name" -_PORT = "net.peer.port" -# Database semantic conventions here: -# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/database.md -_ROWS = "sql.rows" # number of rows returned by a query -_STMT = "db.statement" -_DB = "db.type" -_URL = "db.url" - - -def _normalize_vendor(vendor): - """Return a canonical name for a type of database.""" - if not vendor: - return "db" # should this ever happen? - - if "sqlite" in vendor: - return "sqlite" - - if "postgres" in vendor or vendor == "psycopg2": - return "postgres" - - return vendor - - -def _get_tracer(engine, tracer_provider=None): - if tracer_provider is None: - tracer_provider = trace.get_tracer_provider() - return tracer_provider.get_tracer( - _normalize_vendor(engine.name), __version__ - ) - - -# pylint: disable=unused-argument -def _wrap_create_engine(func, module, args, kwargs): - """Trace the SQLAlchemy engine, creating an `EngineTracer` - object that will listen to SQLAlchemy events. - """ - engine = func(*args, **kwargs) - EngineTracer(_get_tracer(engine), None, engine) - return engine - - -class EngineTracer: - def __init__(self, tracer, service, engine): - self.tracer = tracer - self.engine = engine - self.vendor = _normalize_vendor(engine.name) - self.service = service or self.vendor - self.name = "%s.query" % self.vendor - self.current_span = None - - listen(engine, "before_cursor_execute", self._before_cur_exec) - listen(engine, "after_cursor_execute", self._after_cur_exec) - listen(engine, "handle_error", self._handle_error) - - # pylint: disable=unused-argument - def _before_cur_exec(self, conn, cursor, statement, *args): - self.current_span = self.tracer.start_span(self.name) - with self.tracer.use_span(self.current_span, end_on_exit=False): - if self.current_span.is_recording(): - self.current_span.set_attribute("service", self.vendor) - self.current_span.set_attribute(_STMT, statement) - - if not _set_attributes_from_url( - self.current_span, conn.engine.url - ): - _set_attributes_from_cursor( - self.current_span, self.vendor, cursor - ) - - # pylint: disable=unused-argument - def _after_cur_exec(self, conn, cursor, statement, *args): - if self.current_span is None: - return - - try: - if ( - cursor - and cursor.rowcount >= 0 - and self.current_span.is_recording() - ): - self.current_span.set_attribute(_ROWS, cursor.rowcount) - finally: - self.current_span.end() - - def _handle_error(self, context): - if self.current_span is None: - return - - try: - if self.current_span.is_recording(): - self.current_span.set_status( - Status(StatusCode.ERROR, str(context.original_exception),) - ) - finally: - self.current_span.end() - - -def _set_attributes_from_url(span: trace.Span, url): - """Set connection tags from the url. return true if successful.""" - if span.is_recording(): - if url.host: - span.set_attribute(_HOST, url.host) - if url.port: - span.set_attribute(_PORT, url.port) - if url.database: - span.set_attribute(_DB, url.database) - - return bool(url.host) - - -def _set_attributes_from_cursor(span: trace.Span, vendor, cursor): - """Attempt to set db connection attributes by introspecting the cursor.""" - if not span.is_recording(): - return - if vendor == "postgres": - # pylint: disable=import-outside-toplevel - from psycopg2.extensions import parse_dsn - - if hasattr(cursor, "connection") and hasattr(cursor.connection, "dsn"): - dsn = getattr(cursor.connection, "dsn", None) - if dsn: - data = parse_dsn(dsn) - span.set_attribute(_DB, data.get("dbname")) - span.set_attribute(_HOST, data.get("host")) - span.set_attribute(_PORT, int(data.get("port"))) diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py deleted file mode 100644 index 3b2e3548a5..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from unittest import mock - -from sqlalchemy import create_engine - -from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor -from opentelemetry.test.test_base import TestBase - - -class TestSqlalchemyInstrumentation(TestBase): - def tearDown(self): - super().tearDown() - SQLAlchemyInstrumentor().uninstrument() - - def test_trace_integration(self): - engine = create_engine("sqlite:///:memory:") - SQLAlchemyInstrumentor().instrument( - engine=engine, - tracer_provider=self.tracer_provider, - service="my-database", - ) - cnx = engine.connect() - cnx.execute("SELECT 1 + 1;").fetchall() - spans = self.memory_exporter.get_finished_spans() - - self.assertEqual(len(spans), 1) - self.assertEqual(spans[0].name, "sqlite.query") - - def test_not_recording(self): - mock_tracer = mock.Mock() - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = True - with mock.patch("opentelemetry.trace.get_tracer") as tracer: - tracer.return_value = mock_tracer - engine = create_engine("sqlite:///:memory:") - SQLAlchemyInstrumentor().instrument( - engine=engine, - tracer_provider=self.tracer_provider, - service="my-database", - ) - cnx = engine.connect() - cnx.execute("SELECT 1 + 1;").fetchall() - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_create_engine_wrapper(self): - SQLAlchemyInstrumentor().instrument() - from sqlalchemy import create_engine # pylint: disable-all - - engine = create_engine("sqlite:///:memory:") - cnx = engine.connect() - cnx.execute("SELECT 1 + 1;").fetchall() - spans = self.memory_exporter.get_finished_spans() - - self.assertEqual(len(spans), 1) - self.assertEqual(spans[0].name, "sqlite.query") diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md deleted file mode 100644 index 3954560408..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-sqlite3 - ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - -## 0.8b0 - -Released 2020-05-27 - -- Initial release \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/LICENSE b/instrumentation/opentelemetry-instrumentation-sqlite3/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-sqlite3/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/README.rst b/instrumentation/opentelemetry-instrumentation-sqlite3/README.rst deleted file mode 100644 index 0d2aa2dd99..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/README.rst +++ /dev/null @@ -1,21 +0,0 @@ -OpenTelemetry SQLite3 Instrumentation -===================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-sqlite3.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-sqlite3/ - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-sqlite3 - - -References ----------- -* `OpenTelemetry SQLite3 Instrumentation `_ -* `OpenTelemetry Project `_ - diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg deleted file mode 100644 index d8145be391..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.cfg +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-sqlite3 -description = OpenTelemetry SQLite3 instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-sqlite3 -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation-dbapi == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - wrapt >= 1.0.0, < 2.0.0 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_instrumentor = - sqlite3 = opentelemetry.instrumentation.sqlite3:SQLite3Instrumentor diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.py b/instrumentation/opentelemetry-instrumentation-sqlite3/setup.py deleted file mode 100644 index 67ce350fab..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "sqlite3", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py deleted file mode 100644 index 568e83f0d6..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -SQLite instrumentation supporting `sqlite3`_, it can be enabled by -using ``SQLite3Instrumentor``. - -.. _sqlite3: https://docs.python.org/3/library/sqlite3.html - -Usage ------ - -.. code:: python - - import sqlite3 - from opentelemetry import trace - from opentelemetry.trace import TracerProvider - from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor - - trace.set_tracer_provider(TracerProvider()) - - SQLite3Instrumentor().instrument() - - cnx = sqlite3.connect('example.db') - cursor = cnx.cursor() - cursor.execute("INSERT INTO test (testField) VALUES (123)") - cursor.close() - cnx.close() - -API ---- -""" - -import sqlite3 - -from opentelemetry.instrumentation import dbapi -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.sqlite3.version import __version__ -from opentelemetry.trace import get_tracer - - -class SQLite3Instrumentor(BaseInstrumentor): - # No useful attributes of sqlite3 connection object - _CONNECTION_ATTRIBUTES = {} - - _DATABASE_COMPONENT = "sqlite3" - _DATABASE_TYPE = "sql" - - def _instrument(self, **kwargs): - """Integrate with SQLite3 Python library. - https://docs.python.org/3/library/sqlite3.html - """ - tracer_provider = kwargs.get("tracer_provider") - - dbapi.wrap_connect( - __name__, - sqlite3, - "connect", - self._DATABASE_COMPONENT, - self._DATABASE_TYPE, - self._CONNECTION_ATTRIBUTES, - version=__version__, - tracer_provider=tracer_provider, - ) - - def _uninstrument(self, **kwargs): - """"Disable SQLite3 instrumentation""" - dbapi.unwrap_connect(sqlite3, "connect") - - # pylint:disable=no-self-use - def instrument_connection(self, connection): - """Enable instrumentation in a SQLite connection. - - Args: - connection: The connection to instrument. - - Returns: - An instrumented connection. - """ - tracer = get_tracer(__name__, __version__) - - return dbapi.instrument_connection( - tracer, - connection, - self._DATABASE_COMPONENT, - self._DATABASE_TYPE, - self._CONNECTION_ATTRIBUTES, - ) - - def uninstrument_connection(self, connection): - """Disable instrumentation in a SQLite connection. - - Args: - connection: The connection to uninstrument. - - Returns: - An uninstrumented connection. - """ - return dbapi.uninstrument_connection(connection) diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlite3/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py b/instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py deleted file mode 100644 index 0e385cf3e7..0000000000 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/tests/test_sqlite3.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright 2020, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import sqlite3 - -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor -from opentelemetry.test.test_base import TestBase - - -class TestSQLite3(TestBase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls._connection = None - cls._cursor = None - cls._tracer = cls.tracer_provider.get_tracer(__name__) - SQLite3Instrumentor().instrument(tracer_provider=cls.tracer_provider) - cls._connection = sqlite3.connect(":memory:") - cls._cursor = cls._connection.cursor() - - @classmethod - def tearDownClass(cls): - if cls._cursor: - cls._cursor.close() - if cls._connection: - cls._connection.close() - - def validate_spans(self): - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 2) - for span in spans: - if span.name == "rootSpan": - root_span = span - else: - child_span = span - self.assertIsInstance(span.start_time, int) - self.assertIsInstance(span.end_time, int) - self.assertIsNotNone(root_span) - self.assertIsNotNone(child_span) - self.assertEqual(root_span.name, "rootSpan") - self.assertEqual(child_span.name, "sqlite3") - self.assertIsNotNone(child_span.parent) - self.assertIs(child_span.parent, root_span.get_span_context()) - self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) - - def test_execute(self): - """Should create a child span for execute method - """ - with self._tracer.start_as_current_span("rootSpan"): - self._cursor.execute( - "CREATE TABLE IF NOT EXISTS test (id integer)" - ) - self.validate_spans() - - def test_executemany(self): - """Should create a child span for executemany - """ - with self._tracer.start_as_current_span("rootSpan"): - data = [("1",), ("2",), ("3",)] - stmt = "INSERT INTO test (id) VALUES (?)" - self._cursor.executemany(stmt, data) - self.validate_spans() - - def test_callproc(self): - """Should create a child span for callproc - """ - with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( - Exception - ): - self._cursor.callproc("test", ()) - self.validate_spans() diff --git a/instrumentation/opentelemetry-instrumentation-starlette/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-starlette/CHANGELOG.md deleted file mode 100644 index 1991025f6c..0000000000 --- a/instrumentation/opentelemetry-instrumentation-starlette/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.10b0 - -Released 2020-06-23 - -- Initial release ([#777](https://github.com/open-telemetry/opentelemetry-python/pull/777)) \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-starlette/README.rst b/instrumentation/opentelemetry-instrumentation-starlette/README.rst deleted file mode 100644 index 1d05c0b717..0000000000 --- a/instrumentation/opentelemetry-instrumentation-starlette/README.rst +++ /dev/null @@ -1,45 +0,0 @@ -OpenTelemetry Starlette Instrumentation -======================================= - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-starlette.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-starlette/ - - -This library provides automatic and manual instrumentation of Starlette web frameworks, -instrumenting http requests served by applications utilizing the framework. - -auto-instrumentation using the opentelemetry-instrumentation package is also supported. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-starlette - - -Usage ------ - -.. code-block:: python - - from opentelemetry.instrumentation.starlette import StarletteInstrumentor - from starlette import applications - from starlette.responses import PlainTextResponse - from starlette.routing import Route - - def home(request): - return PlainTextResponse("hi") - - app = applications.Starlette( - routes=[Route("/foobar", home)] - ) - StarletteInstrumentor.instrument_app(app) - - -References ----------- - -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg b/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg deleted file mode 100644 index 04b1f3cbec..0000000000 --- a/instrumentation/opentelemetry-instrumentation-starlette/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-starlette -description = OpenTelemetry Starlette Instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-starlette -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.6 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation-asgi == 0.16.dev0 - -[options.entry_points] -opentelemetry_instrumentor = - starlette = opentelemetry.instrumentation.starlette:StarletteInstrumentor - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - starlette ~= 0.13.0 - requests ~= 2.23.0 # needed for testclient - -[options.packages.find] -where = src diff --git a/instrumentation/opentelemetry-instrumentation-starlette/setup.py b/instrumentation/opentelemetry-instrumentation-starlette/setup.py deleted file mode 100644 index 0232a6f448..0000000000 --- a/instrumentation/opentelemetry-instrumentation-starlette/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "starlette", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py deleted file mode 100644 index f469447f3a..0000000000 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from typing import Optional - -from starlette import applications -from starlette.routing import Match - -from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.starlette.version import __version__ # noqa - - -class StarletteInstrumentor(BaseInstrumentor): - """An instrumentor for starlette - - See `BaseInstrumentor` - """ - - _original_starlette = None - - @staticmethod - def instrument_app(app: applications.Starlette): - """Instrument an uninstrumented Starlette application. - """ - if not getattr(app, "is_instrumented_by_opentelemetry", False): - app.add_middleware( - OpenTelemetryMiddleware, - span_details_callback=_get_route_details, - ) - app.is_instrumented_by_opentelemetry = True - - def _instrument(self, **kwargs): - self._original_starlette = applications.Starlette - applications.Starlette = _InstrumentedStarlette - - def _uninstrument(self, **kwargs): - applications.Starlette = self._original_starlette - - -class _InstrumentedStarlette(applications.Starlette): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.add_middleware( - OpenTelemetryMiddleware, span_details_callback=_get_route_details - ) - - -def _get_route_details(scope): - """Callback to retrieve the starlette route being served. - - TODO: there is currently no way to retrieve http.route from - a starlette application from scope. - - See: https://github.com/encode/starlette/pull/804 - """ - app = scope["app"] - route = None - for starlette_route in app.routes: - match, _ = starlette_route.matches(scope) - if match == Match.FULL: - route = starlette_route.path - break - if match == Match.PARTIAL: - route = starlette_route.path - # method only exists for http, if websocket - # leave it blank. - span_name = route or scope.get("method", "") - attributes = {} - if route: - attributes["http.route"] = route - return span_name, attributes diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-starlette/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-starlette/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py deleted file mode 100644 index a49db07c9a..0000000000 --- a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -from starlette import applications -from starlette.responses import PlainTextResponse -from starlette.routing import Route -from starlette.testclient import TestClient - -import opentelemetry.instrumentation.starlette as otel_starlette -from opentelemetry.test.test_base import TestBase - - -class TestStarletteManualInstrumentation(TestBase): - def _create_app(self): - app = self._create_starlette_app() - self._instrumentor.instrument_app(app) - return app - - def setUp(self): - super().setUp() - self._instrumentor = otel_starlette.StarletteInstrumentor() - self._app = self._create_app() - self._client = TestClient(self._app) - - def test_basic_starlette_call(self): - self._client.get("/foobar") - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 3) - for span in spans: - self.assertIn("/foobar", span.name) - - def test_starlette_route_attribute_added(self): - """Ensure that starlette routes are used as the span name.""" - self._client.get("/user/123") - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 3) - for span in spans: - self.assertIn("/user/{username}", span.name) - self.assertEqual( - spans[-1].attributes["http.route"], "/user/{username}" - ) - # ensure that at least one attribute that is populated by - # the asgi instrumentation is successfully feeding though. - self.assertEqual(spans[-1].attributes["http.flavor"], "1.1") - - @staticmethod - def _create_starlette_app(): - def home(_): - return PlainTextResponse("hi") - - app = applications.Starlette( - routes=[Route("/foobar", home), Route("/user/{username}", home)] - ) - return app - - -class TestAutoInstrumentation(TestStarletteManualInstrumentation): - """Test the auto-instrumented variant - - Extending the manual instrumentation as most test cases apply - to both. - """ - - def _create_app(self): - # instrumentation is handled by the instrument call - self._instrumentor.instrument() - return self._create_starlette_app() - - def tearDown(self): - self._instrumentor.uninstrument() - super().tearDown() - - -class TestAutoInstrumentationLogic(unittest.TestCase): - def test_instrumentation(self): - """Verify that instrumentation methods are instrumenting and - removing as expected. - """ - instrumentor = otel_starlette.StarletteInstrumentor() - original = applications.Starlette - instrumentor.instrument() - try: - instrumented = applications.Starlette - self.assertIsNot(original, instrumented) - finally: - instrumentor.uninstrument() - - should_be_original = applications.Starlette - self.assertIs(original, should_be_original) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md deleted file mode 100644 index 5f6ff0530c..0000000000 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md +++ /dev/null @@ -1,30 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.14b0 - -Released 2020-10-13 - -- Fix issue when specific metrics are not available in certain OS - ([#1207](https://github.com/open-telemetry/opentelemetry-python/pull/1207)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-system-metrics - ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - -## 0.9b0 - -Released 2020-06-10 - -- Initial release (https://github.com/open-telemetry/opentelemetry-python/pull/652) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/LICENSE b/instrumentation/opentelemetry-instrumentation-system-metrics/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-system-metrics/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/README.rst b/instrumentation/opentelemetry-instrumentation-system-metrics/README.rst deleted file mode 100644 index fc984256bf..0000000000 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/README.rst +++ /dev/null @@ -1,24 +0,0 @@ -OpenTelemetry System Metrics Instrumentation -============================================ - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-system-metrics.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-system-metrics/ - -Instrumentation to collect system performance metrics. - - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-system-metrics - - -References ----------- -* `OpenTelemetry System Metrics Instrumentation `_ -* `OpenTelemetry Project `_ - diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg deleted file mode 100644 index f8c27e54d0..0000000000 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-system-metrics -description = OpenTelemetry System Metrics Instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-system-metrics -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-sdk == 0.16.dev0 - psutil ~= 5.7.0 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.py b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.py deleted file mode 100644 index f0bbf9eff0..0000000000 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "system_metrics", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py deleted file mode 100644 index 2b453dbd7d..0000000000 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py +++ /dev/null @@ -1,683 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Instrument to report system (CPU, memory, network) and -process (CPU, memory, garbage collection) metrics. By default, the -following metrics are configured: - -.. code:: python - - { - "system.cpu.time": ["idle", "user", "system", "irq"], - "system.cpu.utilization": ["idle", "user", "system", "irq"], - "system.memory.usage": ["used", "free", "cached"], - "system.memory.utilization": ["used", "free", "cached"], - "system.swap.usage": ["used", "free"], - "system.swap.utilization": ["used", "free"], - "system.disk.io": ["read", "write"], - "system.disk.operations": ["read", "write"], - "system.disk.time": ["read", "write"], - "system.disk.merged": ["read", "write"], - "system.network.dropped.packets": ["transmit", "receive"], - "system.network.packets": ["transmit", "receive"], - "system.network.errors": ["transmit", "receive"], - "system.network.io": ["trasmit", "receive"], - "system.network.connections": ["family", "type"], - "runtime.memory": ["rss", "vms"], - "runtime.cpu.time": ["user", "system"], - } - -Usage ------ - -.. code:: python - - from opentelemetry import metrics - from opentelemetry.instrumentation.system_metrics import SystemMetrics - from opentelemetry.sdk.metrics import MeterProvider, - from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter - - metrics.set_meter_provider(MeterProvider()) - exporter = ConsoleMetricsExporter() - SystemMetrics(exporter) - - # metrics are collected asynchronously - input("...") - - # to configure custom metrics - configuration = { - "system.memory.usage": ["used", "free", "cached"], - "system.cpu.time": ["idle", "user", "system", "irq"], - "system.network.io": ["trasmit", "receive"], - "runtime.memory": ["rss", "vms"], - "runtime.cpu.time": ["user", "system"], - } - SystemMetrics(exporter, config=configuration) - -API ---- -""" - -import gc -import os -import typing -from platform import python_implementation - -import psutil - -from opentelemetry import metrics -from opentelemetry.sdk.metrics.export import MetricsExporter -from opentelemetry.sdk.metrics.export.controller import PushController -from opentelemetry.sdk.util import get_dict_as_key - - -class SystemMetrics: - # pylint: disable=too-many-statements - def __init__( - self, - exporter: MetricsExporter, - interval: int = 30, - labels: typing.Optional[typing.Dict[str, str]] = None, - config: typing.Optional[typing.Dict[str, typing.List[str]]] = None, - ): - self._labels = {} if labels is None else labels - self.meter = metrics.get_meter(__name__) - self.controller = PushController( - meter=self.meter, exporter=exporter, interval=interval - ) - self._python_implementation = python_implementation().lower() - if config is None: - self._config = { - "system.cpu.time": ["idle", "user", "system", "irq"], - "system.cpu.utilization": ["idle", "user", "system", "irq"], - "system.memory.usage": ["used", "free", "cached"], - "system.memory.utilization": ["used", "free", "cached"], - "system.swap.usage": ["used", "free"], - "system.swap.utilization": ["used", "free"], - # system.swap.page.faults: [], - # system.swap.page.operations: [], - "system.disk.io": ["read", "write"], - "system.disk.operations": ["read", "write"], - "system.disk.time": ["read", "write"], - "system.disk.merged": ["read", "write"], - # "system.filesystem.usage": [], - # "system.filesystem.utilization": [], - "system.network.dropped.packets": ["transmit", "receive"], - "system.network.packets": ["transmit", "receive"], - "system.network.errors": ["transmit", "receive"], - "system.network.io": ["trasmit", "receive"], - "system.network.connections": ["family", "type"], - "runtime.memory": ["rss", "vms"], - "runtime.cpu.time": ["user", "system"], - } - else: - self._config = config - - self._proc = psutil.Process(os.getpid()) - - self._system_cpu_time_labels = self._labels.copy() - self._system_cpu_utilization_labels = self._labels.copy() - - self._system_memory_usage_labels = self._labels.copy() - self._system_memory_utilization_labels = self._labels.copy() - - self._system_swap_usage_labels = self._labels.copy() - self._system_swap_utilization_labels = self._labels.copy() - # self._system_swap_page_faults = self._labels.copy() - # self._system_swap_page_operations = self._labels.copy() - - self._system_disk_io_labels = self._labels.copy() - self._system_disk_operations_labels = self._labels.copy() - self._system_disk_time_labels = self._labels.copy() - self._system_disk_merged_labels = self._labels.copy() - - # self._system_filesystem_usage_labels = self._labels.copy() - # self._system_filesystem_utilization_labels = self._labels.copy() - - self._system_network_dropped_packets_labels = self._labels.copy() - self._system_network_packets_labels = self._labels.copy() - self._system_network_errors_labels = self._labels.copy() - self._system_network_io_labels = self._labels.copy() - self._system_network_connections_labels = self._labels.copy() - - self._runtime_memory_labels = self._labels.copy() - self._runtime_cpu_time_labels = self._labels.copy() - self._runtime_gc_count_labels = self._labels.copy() - - self.meter.register_sumobserver( - callback=self._get_system_cpu_time, - name="system.cpu.time", - description="System CPU time", - unit="seconds", - value_type=float, - ) - - self.meter.register_valueobserver( - callback=self._get_system_cpu_utilization, - name="system.cpu.utilization", - description="System CPU utilization", - unit="1", - value_type=float, - ) - - self.meter.register_valueobserver( - callback=self._get_system_memory_usage, - name="system.memory.usage", - description="System memory usage", - unit="bytes", - value_type=int, - ) - - self.meter.register_valueobserver( - callback=self._get_system_memory_utilization, - name="system.memory.utilization", - description="System memory utilization", - unit="1", - value_type=float, - ) - - self.meter.register_valueobserver( - callback=self._get_system_swap_usage, - name="system.swap.usage", - description="System swap usage", - unit="pages", - value_type=int, - ) - - self.meter.register_valueobserver( - callback=self._get_system_swap_utilization, - name="system.swap.utilization", - description="System swap utilization", - unit="1", - value_type=float, - ) - - # self.meter.register_sumobserver( - # callback=self._get_system_swap_page_faults, - # name="system.swap.page_faults", - # description="System swap page faults", - # unit="faults", - # value_type=int, - # ) - - # self.meter.register_sumobserver( - # callback=self._get_system_swap_page_operations, - # name="system.swap.page_operations", - # description="System swap page operations", - # unit="operations", - # value_type=int, - # ) - - self.meter.register_sumobserver( - callback=self._get_system_disk_io, - name="system.disk.io", - description="System disk IO", - unit="bytes", - value_type=int, - ) - - self.meter.register_sumobserver( - callback=self._get_system_disk_operations, - name="system.disk.operations", - description="System disk operations", - unit="operations", - value_type=int, - ) - - self.meter.register_sumobserver( - callback=self._get_system_disk_time, - name="system.disk.time", - description="System disk time", - unit="seconds", - value_type=float, - ) - - self.meter.register_sumobserver( - callback=self._get_system_disk_merged, - name="system.disk.merged", - description="System disk merged", - unit="1", - value_type=int, - ) - - # self.meter.register_valueobserver( - # callback=self._get_system_filesystem_usage, - # name="system.filesystem.usage", - # description="System filesystem usage", - # unit="bytes", - # value_type=int, - # ) - - # self.meter.register_valueobserver( - # callback=self._get_system_filesystem_utilization, - # name="system.filesystem.utilization", - # description="System filesystem utilization", - # unit="1", - # value_type=float, - # ) - - self.meter.register_sumobserver( - callback=self._get_system_network_dropped_packets, - name="system.network.dropped_packets", - description="System network dropped_packets", - unit="packets", - value_type=int, - ) - - self.meter.register_sumobserver( - callback=self._get_system_network_packets, - name="system.network.packets", - description="System network packets", - unit="packets", - value_type=int, - ) - - self.meter.register_sumobserver( - callback=self._get_system_network_errors, - name="system.network.errors", - description="System network errors", - unit="errors", - value_type=int, - ) - - self.meter.register_sumobserver( - callback=self._get_system_network_io, - name="system.network.io", - description="System network io", - unit="bytes", - value_type=int, - ) - - self.meter.register_updownsumobserver( - callback=self._get_system_network_connections, - name="system.network.connections", - description="System network connections", - unit="connections", - value_type=int, - ) - - self.meter.register_sumobserver( - callback=self._get_runtime_memory, - name="runtime.{}.memory".format(self._python_implementation), - description="Runtime {} memory".format( - self._python_implementation - ), - unit="bytes", - value_type=int, - ) - - self.meter.register_sumobserver( - callback=self._get_runtime_cpu_time, - name="runtime.{}.cpu_time".format(self._python_implementation), - description="Runtime {} CPU time".format( - self._python_implementation - ), - unit="seconds", - value_type=float, - ) - - self.meter.register_sumobserver( - callback=self._get_runtime_gc_count, - name="runtime.{}.gc_count".format(self._python_implementation), - description="Runtime {} GC count".format( - self._python_implementation - ), - unit="bytes", - value_type=int, - ) - - def _get_system_cpu_time(self, observer: metrics.ValueObserver) -> None: - """Observer callback for system CPU time - - Args: - observer: the observer to update - """ - for cpu, times in enumerate(psutil.cpu_times(percpu=True)): - for metric in self._config["system.cpu.time"]: - if hasattr(times, metric): - self._system_cpu_time_labels["state"] = metric - self._system_cpu_time_labels["cpu"] = cpu + 1 - observer.observe( - getattr(times, metric), self._system_cpu_time_labels - ) - - def _get_system_cpu_utilization( - self, observer: metrics.ValueObserver - ) -> None: - """Observer callback for system CPU utilization - - Args: - observer: the observer to update - """ - - for cpu, times_percent in enumerate( - psutil.cpu_times_percent(percpu=True) - ): - for metric in self._config["system.cpu.utilization"]: - if hasattr(times_percent, metric): - self._system_cpu_utilization_labels["state"] = metric - self._system_cpu_utilization_labels["cpu"] = cpu + 1 - observer.observe( - getattr(times_percent, metric) / 100, - self._system_cpu_utilization_labels, - ) - - def _get_system_memory_usage( - self, observer: metrics.ValueObserver - ) -> None: - """Observer callback for memory usage - - Args: - observer: the observer to update - """ - virtual_memory = psutil.virtual_memory() - for metric in self._config["system.memory.usage"]: - self._system_memory_usage_labels["state"] = metric - if hasattr(virtual_memory, metric): - observer.observe( - getattr(virtual_memory, metric), - self._system_memory_usage_labels, - ) - - def _get_system_memory_utilization( - self, observer: metrics.ValueObserver - ) -> None: - """Observer callback for memory utilization - - Args: - observer: the observer to update - """ - system_memory = psutil.virtual_memory() - - for metric in self._config["system.memory.utilization"]: - self._system_memory_utilization_labels["state"] = metric - if hasattr(system_memory, metric): - observer.observe( - getattr(system_memory, metric) / system_memory.total, - self._system_memory_utilization_labels, - ) - - def _get_system_swap_usage(self, observer: metrics.ValueObserver) -> None: - """Observer callback for swap usage - - Args: - observer: the observer to update - """ - system_swap = psutil.swap_memory() - - for metric in self._config["system.swap.usage"]: - self._system_swap_usage_labels["state"] = metric - if hasattr(system_swap, metric): - observer.observe( - getattr(system_swap, metric), - self._system_swap_usage_labels, - ) - - def _get_system_swap_utilization( - self, observer: metrics.ValueObserver - ) -> None: - """Observer callback for swap utilization - - Args: - observer: the observer to update - """ - system_swap = psutil.swap_memory() - - for metric in self._config["system.swap.utilization"]: - if hasattr(system_swap, metric): - self._system_swap_utilization_labels["state"] = metric - observer.observe( - getattr(system_swap, metric) / system_swap.total, - self._system_swap_utilization_labels, - ) - - # TODO Add _get_system_swap_page_faults - # TODO Add _get_system_swap_page_operations - - def _get_system_disk_io(self, observer: metrics.SumObserver) -> None: - """Observer callback for disk IO - - Args: - observer: the observer to update - """ - for device, counters in psutil.disk_io_counters(perdisk=True).items(): - for metric in self._config["system.disk.io"]: - if hasattr(counters, "{}_bytes".format(metric)): - self._system_disk_io_labels["device"] = device - self._system_disk_io_labels["direction"] = metric - observer.observe( - getattr(counters, "{}_bytes".format(metric)), - self._system_disk_io_labels, - ) - - def _get_system_disk_operations( - self, observer: metrics.SumObserver - ) -> None: - """Observer callback for disk operations - - Args: - observer: the observer to update - """ - for device, counters in psutil.disk_io_counters(perdisk=True).items(): - for metric in self._config["system.disk.operations"]: - if hasattr(counters, "{}_count".format(metric)): - self._system_disk_operations_labels["device"] = device - self._system_disk_operations_labels["direction"] = metric - observer.observe( - getattr(counters, "{}_count".format(metric)), - self._system_disk_operations_labels, - ) - - def _get_system_disk_time(self, observer: metrics.SumObserver) -> None: - """Observer callback for disk time - - Args: - observer: the observer to update - """ - for device, counters in psutil.disk_io_counters(perdisk=True).items(): - for metric in self._config["system.disk.time"]: - if hasattr(counters, "{}_time".format(metric)): - self._system_disk_time_labels["device"] = device - self._system_disk_time_labels["direction"] = metric - observer.observe( - getattr(counters, "{}_time".format(metric)) / 1000, - self._system_disk_time_labels, - ) - - def _get_system_disk_merged(self, observer: metrics.SumObserver) -> None: - """Observer callback for disk merged operations - - Args: - observer: the observer to update - """ - - # FIXME The units in the spec is 1, it seems like it should be - # operations or the value type should be Double - - for device, counters in psutil.disk_io_counters(perdisk=True).items(): - for metric in self._config["system.disk.time"]: - if hasattr(counters, "{}_merged_count".format(metric)): - self._system_disk_merged_labels["device"] = device - self._system_disk_merged_labels["direction"] = metric - observer.observe( - getattr(counters, "{}_merged_count".format(metric)), - self._system_disk_merged_labels, - ) - - # TODO Add _get_system_filesystem_usage - # TODO Add _get_system_filesystem_utilization - # TODO Filesystem information can be obtained with os.statvfs in Unix-like - # OSs, how to do the same in Windows? - - def _get_system_network_dropped_packets( - self, observer: metrics.SumObserver - ) -> None: - """Observer callback for network dropped packets - - Args: - observer: the observer to update - """ - - for device, counters in psutil.net_io_counters(pernic=True).items(): - for metric in self._config["system.network.dropped.packets"]: - in_out = {"receive": "in", "transmit": "out"}[metric] - if hasattr(counters, "drop{}".format(in_out)): - self._system_network_dropped_packets_labels[ - "device" - ] = device - self._system_network_dropped_packets_labels[ - "direction" - ] = metric - observer.observe( - getattr(counters, "drop{}".format(in_out)), - self._system_network_dropped_packets_labels, - ) - - def _get_system_network_packets( - self, observer: metrics.SumObserver - ) -> None: - """Observer callback for network packets - - Args: - observer: the observer to update - """ - - for device, counters in psutil.net_io_counters(pernic=True).items(): - for metric in self._config["system.network.dropped.packets"]: - recv_sent = {"receive": "recv", "transmit": "sent"}[metric] - if hasattr(counters, "packets_{}".format(recv_sent)): - self._system_network_packets_labels["device"] = device - self._system_network_packets_labels["direction"] = metric - observer.observe( - getattr(counters, "packets_{}".format(recv_sent)), - self._system_network_packets_labels, - ) - - def _get_system_network_errors( - self, observer: metrics.SumObserver - ) -> None: - """Observer callback for network errors - - Args: - observer: the observer to update - """ - for device, counters in psutil.net_io_counters(pernic=True).items(): - for metric in self._config["system.network.errors"]: - in_out = {"receive": "in", "transmit": "out"}[metric] - if hasattr(counters, "err{}".format(in_out)): - self._system_network_errors_labels["device"] = device - self._system_network_errors_labels["direction"] = metric - observer.observe( - getattr(counters, "err{}".format(in_out)), - self._system_network_errors_labels, - ) - - def _get_system_network_io(self, observer: metrics.SumObserver) -> None: - """Observer callback for network IO - - Args: - observer: the observer to update - """ - - for device, counters in psutil.net_io_counters(pernic=True).items(): - for metric in self._config["system.network.dropped.packets"]: - recv_sent = {"receive": "recv", "transmit": "sent"}[metric] - if hasattr(counters, "bytes_{}".format(recv_sent)): - self._system_network_io_labels["device"] = device - self._system_network_io_labels["direction"] = metric - observer.observe( - getattr(counters, "bytes_{}".format(recv_sent)), - self._system_network_io_labels, - ) - - def _get_system_network_connections( - self, observer: metrics.UpDownSumObserver - ) -> None: - """Observer callback for network connections - - Args: - observer: the observer to update - """ - # TODO How to find the device identifier for a particular - # connection? - - connection_counters = {} - - for net_connection in psutil.net_connections(): - for metric in self._config["system.network.connections"]: - self._system_network_connections_labels["protocol"] = { - 1: "tcp", - 2: "udp", - }[net_connection.type.value] - self._system_network_connections_labels[ - "state" - ] = net_connection.status - self._system_network_connections_labels[metric] = getattr( - net_connection, metric - ) - - connection_counters_key = get_dict_as_key( - self._system_network_connections_labels - ) - - if connection_counters_key in connection_counters.keys(): - connection_counters[connection_counters_key]["counter"] += 1 - else: - connection_counters[connection_counters_key] = { - "counter": 1, - "labels": self._system_network_connections_labels.copy(), - } - - for connection_counter in connection_counters.values(): - observer.observe( - connection_counter["counter"], connection_counter["labels"], - ) - - def _get_runtime_memory(self, observer: metrics.SumObserver) -> None: - """Observer callback for runtime memory - - Args: - observer: the observer to update - """ - proc_memory = self._proc.memory_info() - for metric in self._config["runtime.memory"]: - if hasattr(proc_memory, metric): - self._runtime_memory_labels["type"] = metric - observer.observe( - getattr(proc_memory, metric), self._runtime_memory_labels, - ) - - def _get_runtime_cpu_time(self, observer: metrics.SumObserver) -> None: - """Observer callback for runtime CPU time - - Args: - observer: the observer to update - """ - proc_cpu = self._proc.cpu_times() - for metric in self._config["runtime.cpu.time"]: - if hasattr(proc_cpu, metric): - self._runtime_cpu_time_labels["type"] = metric - observer.observe( - getattr(proc_cpu, metric), self._runtime_cpu_time_labels, - ) - - def _get_runtime_gc_count(self, observer: metrics.SumObserver) -> None: - """Observer callback for garbage collection - - Args: - observer: the observer to update - """ - for index, count in enumerate(gc.get_count()): - self._runtime_gc_count_labels["count"] = str(index) - observer.observe(count, self._runtime_gc_count_labels) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py deleted file mode 100644 index 2f155383f4..0000000000 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py +++ /dev/null @@ -1,703 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=protected-access - -from collections import namedtuple -from platform import python_implementation -from unittest import mock - -from opentelemetry import metrics -from opentelemetry.instrumentation.system_metrics import SystemMetrics -from opentelemetry.sdk.metrics.export.aggregate import ValueObserverAggregator -from opentelemetry.test.test_base import TestBase - - -class TestSystemMetrics(TestBase): - def setUp(self): - super().setUp() - self.memory_metrics_exporter.clear() - self.implementation = python_implementation().lower() - - def test_system_metrics_constructor(self): - # ensure the observers have been registered - meter = metrics.get_meter(__name__) - with mock.patch("opentelemetry.metrics.get_meter") as mock_get_meter: - mock_get_meter.return_value = meter - SystemMetrics(self.memory_metrics_exporter) - - self.assertEqual(len(meter.observers), 18) - - observer_names = [ - "system.cpu.time", - "system.cpu.utilization", - "system.memory.usage", - "system.memory.utilization", - "system.swap.usage", - "system.swap.utilization", - "system.disk.io", - "system.disk.operations", - "system.disk.time", - "system.disk.merged", - "system.network.dropped_packets", - "system.network.packets", - "system.network.errors", - "system.network.io", - "system.network.connections", - "runtime.{}.memory".format(self.implementation), - "runtime.{}.cpu_time".format(self.implementation), - "runtime.{}.gc_count".format(self.implementation), - ] - - for observer in meter.observers: - self.assertIn(observer.name, observer_names) - observer_names.remove(observer.name) - - def _assert_metrics(self, observer_name, system_metrics, expected): - system_metrics.controller.tick() - assertions = 0 - for ( - metric - ) in ( - self.memory_metrics_exporter._exported_metrics # pylint: disable=protected-access - ): - if ( - metric.labels in expected - and metric.instrument.name == observer_name - ): - self.assertEqual( - metric.aggregator.checkpoint, expected[metric.labels], - ) - assertions += 1 - self.assertEqual(len(expected), assertions) - - def _test_metrics(self, observer_name, expected): - meter = self.meter_provider.get_meter(__name__) - - with mock.patch("opentelemetry.metrics.get_meter") as mock_get_meter: - mock_get_meter.return_value = meter - system_metrics = SystemMetrics(self.memory_metrics_exporter) - self._assert_metrics(observer_name, system_metrics, expected) - - # When this test case is executed, _get_system_cpu_utilization gets run - # too because of the controller thread which runs all observers. This patch - # is added here to stop a warning that would otherwise be raised. - # pylint: disable=unused-argument - @mock.patch("psutil.cpu_times_percent") - @mock.patch("psutil.cpu_times") - def test_system_cpu_time(self, mock_cpu_times, mock_cpu_times_percent): - CPUTimes = namedtuple("CPUTimes", ["idle", "user", "system", "irq"]) - mock_cpu_times.return_value = [ - CPUTimes(idle=1.2, user=3.4, system=5.6, irq=7.8), - CPUTimes(idle=1.2, user=3.4, system=5.6, irq=7.8), - ] - - expected = { - (("cpu", 1), ("state", "idle"),): 1.2, - (("cpu", 1), ("state", "user"),): 3.4, - (("cpu", 1), ("state", "system"),): 5.6, - (("cpu", 1), ("state", "irq"),): 7.8, - (("cpu", 2), ("state", "idle"),): 1.2, - (("cpu", 2), ("state", "user"),): 3.4, - (("cpu", 2), ("state", "system"),): 5.6, - (("cpu", 2), ("state", "irq"),): 7.8, - } - self._test_metrics("system.cpu.time", expected) - - @mock.patch("psutil.cpu_times_percent") - def test_system_cpu_utilization(self, mock_cpu_times_percent): - CPUTimesPercent = namedtuple( - "CPUTimesPercent", ["idle", "user", "system", "irq"] - ) - mock_cpu_times_percent.return_value = [ - CPUTimesPercent(idle=1.2, user=3.4, system=5.6, irq=7.8), - CPUTimesPercent(idle=1.2, user=3.4, system=5.6, irq=7.8), - ] - - expected = { - (("cpu", 1), ("state", "idle"),): ValueObserverAggregator._TYPE( - min=1.2 / 100, - max=1.2 / 100, - sum=1.2 / 100, - count=1, - last=1.2 / 100, - ), - (("cpu", 1), ("state", "user"),): ValueObserverAggregator._TYPE( - min=3.4 / 100, - max=3.4 / 100, - sum=3.4 / 100, - count=1, - last=3.4 / 100, - ), - (("cpu", 1), ("state", "system"),): ValueObserverAggregator._TYPE( - min=5.6 / 100, - max=5.6 / 100, - sum=5.6 / 100, - count=1, - last=5.6 / 100, - ), - (("cpu", 1), ("state", "irq"),): ValueObserverAggregator._TYPE( - min=7.8 / 100, - max=7.8 / 100, - sum=7.8 / 100, - count=1, - last=7.8 / 100, - ), - (("cpu", 2), ("state", "idle"),): ValueObserverAggregator._TYPE( - min=1.2 / 100, - max=1.2 / 100, - sum=1.2 / 100, - count=1, - last=1.2 / 100, - ), - (("cpu", 2), ("state", "user"),): ValueObserverAggregator._TYPE( - min=3.4 / 100, - max=3.4 / 100, - sum=3.4 / 100, - count=1, - last=3.4 / 100, - ), - (("cpu", 2), ("state", "system"),): ValueObserverAggregator._TYPE( - min=5.6 / 100, - max=5.6 / 100, - sum=5.6 / 100, - count=1, - last=5.6 / 100, - ), - (("cpu", 2), ("state", "irq"),): ValueObserverAggregator._TYPE( - min=7.8 / 100, - max=7.8 / 100, - sum=7.8 / 100, - count=1, - last=7.8 / 100, - ), - } - self._test_metrics("system.cpu.utilization", expected) - - @mock.patch("psutil.virtual_memory") - def test_system_memory_usage(self, mock_virtual_memory): - VirtualMemory = namedtuple( - "VirtualMemory", ["used", "free", "cached", "total"] - ) - mock_virtual_memory.return_value = VirtualMemory( - used=1, free=2, cached=3, total=4 - ) - - expected = { - (("state", "used"),): ValueObserverAggregator._TYPE( - min=1, max=1, sum=1, count=1, last=1 - ), - (("state", "free"),): ValueObserverAggregator._TYPE( - min=2, max=2, sum=2, count=1, last=2 - ), - (("state", "cached"),): ValueObserverAggregator._TYPE( - min=3, max=3, sum=3, count=1, last=3 - ), - } - self._test_metrics("system.memory.usage", expected) - - @mock.patch("psutil.virtual_memory") - def test_system_memory_utilization(self, mock_virtual_memory): - VirtualMemory = namedtuple( - "VirtualMemory", ["used", "free", "cached", "total"] - ) - mock_virtual_memory.return_value = VirtualMemory( - used=1, free=2, cached=3, total=4 - ) - - expected = { - (("state", "used"),): ValueObserverAggregator._TYPE( - min=1 / 4, max=1 / 4, sum=1 / 4, count=1, last=1 / 4 - ), - (("state", "free"),): ValueObserverAggregator._TYPE( - min=2 / 4, max=2 / 4, sum=2 / 4, count=1, last=2 / 4 - ), - (("state", "cached"),): ValueObserverAggregator._TYPE( - min=3 / 4, max=3 / 4, sum=3 / 4, count=1, last=3 / 4 - ), - } - self._test_metrics("system.memory.utilization", expected) - - @mock.patch("psutil.swap_memory") - def test_system_swap_usage(self, mock_swap_memory): - SwapMemory = namedtuple("SwapMemory", ["used", "free", "total"]) - mock_swap_memory.return_value = SwapMemory(used=1, free=2, total=3) - - expected = { - (("state", "used"),): ValueObserverAggregator._TYPE( - min=1, max=1, sum=1, count=1, last=1 - ), - (("state", "free"),): ValueObserverAggregator._TYPE( - min=2, max=2, sum=2, count=1, last=2 - ), - } - self._test_metrics("system.swap.usage", expected) - - @mock.patch("psutil.swap_memory") - def test_system_swap_utilization(self, mock_swap_memory): - SwapMemory = namedtuple("SwapMemory", ["used", "free", "total"]) - mock_swap_memory.return_value = SwapMemory(used=1, free=2, total=3) - - expected = { - (("state", "used"),): ValueObserverAggregator._TYPE( - min=1 / 3, max=1 / 3, sum=1 / 3, count=1, last=1 / 3 - ), - (("state", "free"),): ValueObserverAggregator._TYPE( - min=2 / 3, max=2 / 3, sum=2 / 3, count=1, last=2 / 3 - ), - } - self._test_metrics("system.swap.utilization", expected) - - @mock.patch("psutil.disk_io_counters") - def test_system_disk_io(self, mock_disk_io_counters): - DiskIO = namedtuple( - "DiskIO", - [ - "read_count", - "write_count", - "read_bytes", - "write_bytes", - "read_time", - "write_time", - "read_merged_count", - "write_merged_count", - ], - ) - mock_disk_io_counters.return_value = { - "sda": DiskIO( - read_count=1, - write_count=2, - read_bytes=3, - write_bytes=4, - read_time=5, - write_time=6, - read_merged_count=7, - write_merged_count=8, - ), - "sdb": DiskIO( - read_count=9, - write_count=10, - read_bytes=11, - write_bytes=12, - read_time=13, - write_time=14, - read_merged_count=15, - write_merged_count=16, - ), - } - - expected = { - (("device", "sda"), ("direction", "read"),): 3, - (("device", "sda"), ("direction", "write"),): 4, - (("device", "sdb"), ("direction", "read"),): 11, - (("device", "sdb"), ("direction", "write"),): 12, - } - self._test_metrics("system.disk.io", expected) - - @mock.patch("psutil.disk_io_counters") - def test_system_disk_operations(self, mock_disk_io_counters): - DiskIO = namedtuple( - "DiskIO", - [ - "read_count", - "write_count", - "read_bytes", - "write_bytes", - "read_time", - "write_time", - "read_merged_count", - "write_merged_count", - ], - ) - mock_disk_io_counters.return_value = { - "sda": DiskIO( - read_count=1, - write_count=2, - read_bytes=3, - write_bytes=4, - read_time=5, - write_time=6, - read_merged_count=7, - write_merged_count=8, - ), - "sdb": DiskIO( - read_count=9, - write_count=10, - read_bytes=11, - write_bytes=12, - read_time=13, - write_time=14, - read_merged_count=15, - write_merged_count=16, - ), - } - - expected = { - (("device", "sda"), ("direction", "read"),): 1, - (("device", "sda"), ("direction", "write"),): 2, - (("device", "sdb"), ("direction", "read"),): 9, - (("device", "sdb"), ("direction", "write"),): 10, - } - self._test_metrics("system.disk.operations", expected) - - @mock.patch("psutil.disk_io_counters") - def test_system_disk_time(self, mock_disk_io_counters): - DiskIO = namedtuple( - "DiskIO", - [ - "read_count", - "write_count", - "read_bytes", - "write_bytes", - "read_time", - "write_time", - "read_merged_count", - "write_merged_count", - ], - ) - mock_disk_io_counters.return_value = { - "sda": DiskIO( - read_count=1, - write_count=2, - read_bytes=3, - write_bytes=4, - read_time=5, - write_time=6, - read_merged_count=7, - write_merged_count=8, - ), - "sdb": DiskIO( - read_count=9, - write_count=10, - read_bytes=11, - write_bytes=12, - read_time=13, - write_time=14, - read_merged_count=15, - write_merged_count=16, - ), - } - - expected = { - (("device", "sda"), ("direction", "read"),): 5 / 1000, - (("device", "sda"), ("direction", "write"),): 6 / 1000, - (("device", "sdb"), ("direction", "read"),): 13 / 1000, - (("device", "sdb"), ("direction", "write"),): 14 / 1000, - } - self._test_metrics("system.disk.time", expected) - - @mock.patch("psutil.disk_io_counters") - def test_system_disk_merged(self, mock_disk_io_counters): - DiskIO = namedtuple( - "DiskIO", - [ - "read_count", - "write_count", - "read_bytes", - "write_bytes", - "read_time", - "write_time", - "read_merged_count", - "write_merged_count", - ], - ) - mock_disk_io_counters.return_value = { - "sda": DiskIO( - read_count=1, - write_count=2, - read_bytes=3, - write_bytes=4, - read_time=5, - write_time=6, - read_merged_count=7, - write_merged_count=8, - ), - "sdb": DiskIO( - read_count=9, - write_count=10, - read_bytes=11, - write_bytes=12, - read_time=13, - write_time=14, - read_merged_count=15, - write_merged_count=16, - ), - } - - expected = { - (("device", "sda"), ("direction", "read"),): 7, - (("device", "sda"), ("direction", "write"),): 8, - (("device", "sdb"), ("direction", "read"),): 15, - (("device", "sdb"), ("direction", "write"),): 16, - } - self._test_metrics("system.disk.merged", expected) - - @mock.patch("psutil.net_io_counters") - def test_system_network_dropped_packets(self, mock_net_io_counters): - NetIO = namedtuple( - "NetIO", - [ - "dropin", - "dropout", - "packets_sent", - "packets_recv", - "errin", - "errout", - "bytes_sent", - "bytes_recv", - ], - ) - mock_net_io_counters.return_value = { - "eth0": NetIO( - dropin=1, - dropout=2, - packets_sent=3, - packets_recv=4, - errin=5, - errout=6, - bytes_sent=7, - bytes_recv=8, - ), - "eth1": NetIO( - dropin=9, - dropout=10, - packets_sent=11, - packets_recv=12, - errin=13, - errout=14, - bytes_sent=15, - bytes_recv=16, - ), - } - - expected = { - (("device", "eth0"), ("direction", "receive"),): 1, - (("device", "eth0"), ("direction", "transmit"),): 2, - (("device", "eth1"), ("direction", "receive"),): 9, - (("device", "eth1"), ("direction", "transmit"),): 10, - } - self._test_metrics("system.network.dropped_packets", expected) - - @mock.patch("psutil.net_io_counters") - def test_system_network_packets(self, mock_net_io_counters): - NetIO = namedtuple( - "NetIO", - [ - "dropin", - "dropout", - "packets_sent", - "packets_recv", - "errin", - "errout", - "bytes_sent", - "bytes_recv", - ], - ) - mock_net_io_counters.return_value = { - "eth0": NetIO( - dropin=1, - dropout=2, - packets_sent=3, - packets_recv=4, - errin=5, - errout=6, - bytes_sent=7, - bytes_recv=8, - ), - "eth1": NetIO( - dropin=9, - dropout=10, - packets_sent=11, - packets_recv=12, - errin=13, - errout=14, - bytes_sent=15, - bytes_recv=16, - ), - } - - expected = { - (("device", "eth0"), ("direction", "receive"),): 4, - (("device", "eth0"), ("direction", "transmit"),): 3, - (("device", "eth1"), ("direction", "receive"),): 12, - (("device", "eth1"), ("direction", "transmit"),): 11, - } - self._test_metrics("system.network.packets", expected) - - @mock.patch("psutil.net_io_counters") - def test_system_network_errors(self, mock_net_io_counters): - NetIO = namedtuple( - "NetIO", - [ - "dropin", - "dropout", - "packets_sent", - "packets_recv", - "errin", - "errout", - "bytes_sent", - "bytes_recv", - ], - ) - mock_net_io_counters.return_value = { - "eth0": NetIO( - dropin=1, - dropout=2, - packets_sent=3, - packets_recv=4, - errin=5, - errout=6, - bytes_sent=7, - bytes_recv=8, - ), - "eth1": NetIO( - dropin=9, - dropout=10, - packets_sent=11, - packets_recv=12, - errin=13, - errout=14, - bytes_sent=15, - bytes_recv=16, - ), - } - - expected = { - (("device", "eth0"), ("direction", "receive"),): 5, - (("device", "eth0"), ("direction", "transmit"),): 6, - (("device", "eth1"), ("direction", "receive"),): 13, - (("device", "eth1"), ("direction", "transmit"),): 14, - } - self._test_metrics("system.network.errors", expected) - - @mock.patch("psutil.net_io_counters") - def test_system_network_io(self, mock_net_io_counters): - NetIO = namedtuple( - "NetIO", - [ - "dropin", - "dropout", - "packets_sent", - "packets_recv", - "errin", - "errout", - "bytes_sent", - "bytes_recv", - ], - ) - mock_net_io_counters.return_value = { - "eth0": NetIO( - dropin=1, - dropout=2, - packets_sent=3, - packets_recv=4, - errin=5, - errout=6, - bytes_sent=7, - bytes_recv=8, - ), - "eth1": NetIO( - dropin=9, - dropout=10, - packets_sent=11, - packets_recv=12, - errin=13, - errout=14, - bytes_sent=15, - bytes_recv=16, - ), - } - - expected = { - (("device", "eth0"), ("direction", "receive"),): 8, - (("device", "eth0"), ("direction", "transmit"),): 7, - (("device", "eth1"), ("direction", "receive"),): 16, - (("device", "eth1"), ("direction", "transmit"),): 15, - } - self._test_metrics("system.network.io", expected) - - @mock.patch("psutil.net_connections") - def test_system_network_connections(self, mock_net_connections): - NetConnection = namedtuple( - "NetworkConnection", ["family", "type", "status"] - ) - Type = namedtuple("Type", ["value"]) - mock_net_connections.return_value = [ - NetConnection(family=1, status="ESTABLISHED", type=Type(value=2),), - NetConnection(family=1, status="ESTABLISHED", type=Type(value=1),), - ] - - expected = { - ( - ("family", 1), - ("protocol", "udp"), - ("state", "ESTABLISHED"), - ("type", Type(value=2)), - ): 1, - ( - ("family", 1), - ("protocol", "tcp"), - ("state", "ESTABLISHED"), - ("type", Type(value=1)), - ): 1, - } - self._test_metrics("system.network.connections", expected) - - @mock.patch("psutil.Process.memory_info") - def test_runtime_memory(self, mock_process_memory_info): - - PMem = namedtuple("PMem", ["rss", "vms"]) - - mock_process_memory_info.configure_mock( - **{"return_value": PMem(rss=1, vms=2)} - ) - - expected = { - (("type", "rss"),): 1, - (("type", "vms"),): 2, - } - self._test_metrics( - "runtime.{}.memory".format(self.implementation), expected - ) - - @mock.patch("psutil.Process.cpu_times") - def test_runtime_cpu_time(self, mock_process_cpu_times): - - PCPUTimes = namedtuple("PCPUTimes", ["user", "system"]) - - mock_process_cpu_times.configure_mock( - **{"return_value": PCPUTimes(user=1.1, system=2.2)} - ) - - expected = { - (("type", "user"),): 1.1, - (("type", "system"),): 2.2, - } - self._test_metrics( - "runtime.{}.cpu_time".format(self.implementation), expected - ) - - @mock.patch("gc.get_count") - def test_runtime_get_count(self, mock_gc_get_count): - - mock_gc_get_count.configure_mock(**{"return_value": (1, 2, 3)}) - - expected = { - (("count", "0"),): 1, - (("count", "1"),): 2, - (("count", "2"),): 3, - } - self._test_metrics( - "runtime.{}.gc_count".format(self.implementation), expected - ) diff --git a/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md deleted file mode 100644 index e341a1f5a3..0000000000 --- a/instrumentation/opentelemetry-instrumentation-tornado/CHANGELOG.md +++ /dev/null @@ -1,15 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.14b0 - -Released 2020-10-13 - -- Added support for `OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS` ([#1178](https://github.com/open-telemetry/opentelemetry-python/pull/1178)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Initial release. Supports Tornado 6.x on Python 3.5 and newer. diff --git a/instrumentation/opentelemetry-instrumentation-tornado/LICENSE b/instrumentation/opentelemetry-instrumentation-tornado/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-tornado/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-tornado/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-tornado/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-tornado/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-tornado/README.rst b/instrumentation/opentelemetry-instrumentation-tornado/README.rst deleted file mode 100644 index 088c7f0e85..0000000000 --- a/instrumentation/opentelemetry-instrumentation-tornado/README.rst +++ /dev/null @@ -1,51 +0,0 @@ -OpenTelemetry Tornado Instrumentation -====================================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-tornado.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-tornado/ - -This library builds on the OpenTelemetry WSGI middleware to track web requests -in Tornado applications. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-tornado - -Configuration -------------- - -The following environment variables are supported as configuration options: - -- OTEL_PYTHON_TORNADO_EXCLUDED_URLS - -A comma separated list of paths that should not be automatically traced. For example, if this is set to - -:: - - export OTEL_PYTHON_TORNADO_EXLUDED_URLS='/healthz,/ping' - -Then any requests made to ``/healthz`` and ``/ping`` will not be automatically traced. - -Request attributes -******************** -To extract certain attributes from Tornado's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS`` to a comma -delimited list of request attribute names. - -For example, - -:: - - export OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS='uri,query' - -will extract path_info and content_type attributes from every traced request and add them as span attributes. - -References ----------- - -* `OpenTelemetry Tornado Instrumentation `_ -* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg b/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg deleted file mode 100644 index 889eb0e97c..0000000000 --- a/instrumentation/opentelemetry-instrumentation-tornado/setup.cfg +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-tornado -description = Tornado instrumentation for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-tornado -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - tornado >= 6.0 - opentelemetry-instrumentation == 0.16.dev0 - opentelemetry-api == 0.16.dev0 - -[options.extras_require] -test = - tornado >= 6.0 - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src diff --git a/instrumentation/opentelemetry-instrumentation-tornado/setup.py b/instrumentation/opentelemetry-instrumentation-tornado/setup.py deleted file mode 100644 index bd6814b50e..0000000000 --- a/instrumentation/opentelemetry-instrumentation-tornado/setup.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "tornado", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup( - version=PACKAGE_INFO["__version__"], - entry_points={ - "opentelemetry_instrumentor": [ - "tornado = opentelemetry.instrumentation.tornado:TornadoInstrumentor" - ] - }, -) diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py deleted file mode 100644 index 6bb956ecb5..0000000000 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py +++ /dev/null @@ -1,273 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This library uses OpenTelemetry to track web requests in Tornado applications. - -Usage ------ - -.. code-block:: python - - import tornado.web - from opentelemetry.instrumentation.tornado import TornadoInstrumentor - - # apply tornado instrumentation - TornadoInstrumentor().instrument() - - class Handler(tornado.web.RequestHandler): - def get(self): - self.set_status(200) - - app = tornado.web.Application([(r"/", Handler)]) - app.listen(8080) - tornado.ioloop.IOLoop.current().start() -""" - -import inspect -import typing -from collections import namedtuple -from functools import partial, wraps -from logging import getLogger - -import tornado.web -import wrapt -from tornado.routing import Rule -from wrapt import wrap_function_wrapper - -from opentelemetry import configuration, context, propagators, trace -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.tornado.version import __version__ -from opentelemetry.instrumentation.utils import ( - extract_attributes_from_object, - http_status_to_status_code, - unwrap, -) -from opentelemetry.trace.propagation.textmap import DictGetter -from opentelemetry.trace.status import Status -from opentelemetry.util import ExcludeList, time_ns - -from .client import fetch_async # pylint: disable=E0401 - -_logger = getLogger(__name__) -_TraceContext = namedtuple("TraceContext", ["activation", "span", "token"]) -_HANDLER_CONTEXT_KEY = "_otel_trace_context_key" -_OTEL_PATCHED_KEY = "_otel_patched_key" - - -def get_excluded_urls(): - urls = configuration.Configuration().TORNADO_EXCLUDED_URLS or "" - if urls: - urls = str.split(urls, ",") - return ExcludeList(urls) - - -def get_traced_request_attrs(): - attrs = configuration.Configuration().TORNADO_TRACED_REQUEST_ATTRS or "" - if attrs: - attrs = [attr.strip() for attr in attrs.split(",")] - else: - attrs = [] - return attrs - - -_excluded_urls = get_excluded_urls() -_traced_attrs = get_traced_request_attrs() - -carrier_getter = DictGetter() - - -class TornadoInstrumentor(BaseInstrumentor): - patched_handlers = [] - original_handler_new = None - - def _instrument(self, **kwargs): - """ - _instrument patches tornado.web.RequestHandler and tornado.httpclient.AsyncHTTPClient classes - to automatically instrument requests both received and sent by Tornado. - - We don't patch RequestHandler._execute as it causes some issues with contextvars based context. - Mainly the context fails to detach from within RequestHandler.on_finish() if it is attached inside - RequestHandler._execute. Same issue plagues RequestHandler.initialize. RequestHandler.prepare works - perfectly on the other hand as it executes in the same context as on_finish and log_exection which - are patched to finish a span after a request is served. - - However, we cannot just patch RequestHandler's prepare method because it is supposed to be overwridden - by sub-classes and since the parent prepare method does not do anything special, sub-classes don't - have to call super() when overriding the method. - - In order to work around this, we patch the __init__ method of RequestHandler and then dynamically patch - the prepare, on_finish and log_exception methods of the derived classes _only_ the first time we see them. - Note that the patch does not apply on every single __init__ call, only the first one for the enture - process lifetime. - """ - tracer_provider = kwargs.get("tracer_provider") - tracer = trace.get_tracer(__name__, __version__, tracer_provider) - - def handler_init(init, handler, args, kwargs): - cls = handler.__class__ - if patch_handler_class(tracer, cls): - self.patched_handlers.append(cls) - return init(*args, **kwargs) - - wrap_function_wrapper( - "tornado.web", "RequestHandler.__init__", handler_init - ) - wrap_function_wrapper( - "tornado.httpclient", - "AsyncHTTPClient.fetch", - partial(fetch_async, tracer), - ) - - def _uninstrument(self, **kwargs): - unwrap(tornado.web.RequestHandler, "__init__") - unwrap(tornado.httpclient.AsyncHTTPClient, "fetch") - for handler in self.patched_handlers: - unpatch_handler_class(handler) - self.patched_handlers = [] - - -def patch_handler_class(tracer, cls): - if getattr(cls, _OTEL_PATCHED_KEY, False): - return False - - setattr(cls, _OTEL_PATCHED_KEY, True) - _wrap(cls, "prepare", partial(_prepare, tracer)) - _wrap(cls, "on_finish", partial(_on_finish, tracer)) - _wrap(cls, "log_exception", partial(_log_exception, tracer)) - return True - - -def unpatch_handler_class(cls): - if not getattr(cls, _OTEL_PATCHED_KEY, False): - return - - unwrap(cls, "prepare") - unwrap(cls, "on_finish") - unwrap(cls, "log_exception") - delattr(cls, _OTEL_PATCHED_KEY) - - -def _wrap(cls, method_name, wrapper): - original = getattr(cls, method_name) - wrapper = wrapt.FunctionWrapper(original, wrapper) - wrapt.apply_patch(cls, method_name, wrapper) - - -def _prepare(tracer, func, handler, args, kwargs): - start_time = time_ns() - request = handler.request - if _excluded_urls.url_disabled(request.uri): - return func(*args, **kwargs) - _start_span(tracer, handler, start_time) - return func(*args, **kwargs) - - -def _on_finish(tracer, func, handler, args, kwargs): - _finish_span(tracer, handler) - return func(*args, **kwargs) - - -def _log_exception(tracer, func, handler, args, kwargs): - error = None - if len(args) == 3: - error = args[1] - - _finish_span(tracer, handler, error) - return func(*args, **kwargs) - - -def _get_attributes_from_request(request): - attrs = { - "component": "tornado", - "http.method": request.method, - "http.scheme": request.protocol, - "http.host": request.host, - "http.target": request.path, - } - - if request.host: - attrs["http.host"] = request.host - - if request.remote_ip: - attrs["net.peer.ip"] = request.remote_ip - - return extract_attributes_from_object(request, _traced_attrs, attrs) - - -def _get_operation_name(handler, request): - full_class_name = type(handler).__name__ - class_name = full_class_name.rsplit(".")[-1] - return "{0}.{1}".format(class_name, request.method.lower()) - - -def _start_span(tracer, handler, start_time) -> _TraceContext: - token = context.attach( - propagators.extract(carrier_getter, handler.request.headers,) - ) - - span = tracer.start_span( - _get_operation_name(handler, handler.request), - kind=trace.SpanKind.SERVER, - start_time=start_time, - ) - if span.is_recording(): - attributes = _get_attributes_from_request(handler.request) - for key, value in attributes.items(): - span.set_attribute(key, value) - - activation = tracer.use_span(span, end_on_exit=True) - activation.__enter__() - ctx = _TraceContext(activation, span, token) - setattr(handler, _HANDLER_CONTEXT_KEY, ctx) - return ctx - - -def _finish_span(tracer, handler, error=None): - status_code = handler.get_status() - reason = getattr(handler, "_reason") - finish_args = (None, None, None) - ctx = getattr(handler, _HANDLER_CONTEXT_KEY, None) - - if error: - if isinstance(error, tornado.web.HTTPError): - status_code = error.status_code - if not ctx and status_code == 404: - ctx = _start_span(tracer, handler, time_ns()) - if status_code != 404: - finish_args = ( - type(error), - error, - getattr(error, "__traceback__", None), - ) - status_code = 500 - reason = None - - if not ctx: - return - - if ctx.span.is_recording(): - if reason: - ctx.span.set_attribute("http.status_text", reason) - ctx.span.set_attribute("http.status_code", status_code) - ctx.span.set_status( - Status( - status_code=http_status_to_status_code(status_code), - description=reason, - ) - ) - - ctx.activation.__exit__(*finish_args) - context.detach(ctx.token) - delattr(handler, _HANDLER_CONTEXT_KEY) diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py deleted file mode 100644 index 5ec001bdab..0000000000 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py +++ /dev/null @@ -1,81 +0,0 @@ -import functools - -from tornado.httpclient import HTTPError, HTTPRequest - -from opentelemetry import propagators, trace -from opentelemetry.instrumentation.utils import http_status_to_status_code -from opentelemetry.trace.status import Status -from opentelemetry.util import time_ns - - -def _normalize_request(args, kwargs): - req = args[0] - if not isinstance(req, str): - return (args, kwargs) - - new_kwargs = {} - for param in ("callback", "raise_error"): - if param in kwargs: - new_kwargs[param] = kwargs.pop(param) - - req = HTTPRequest(req, **kwargs) - new_args = [req] - new_args.extend(args[1:]) - return (new_args, new_kwargs) - - -def fetch_async(tracer, func, _, args, kwargs): - start_time = time_ns() - - # Return immediately if no args were provided (error) - # or original_request is set (meaning we are in a redirect step). - if len(args) == 0 or hasattr(args[0], "original_request"): - return func(*args, **kwargs) - - # Force the creation of a HTTPRequest object if needed, - # so we can inject the context into the headers. - args, kwargs = _normalize_request(args, kwargs) - request = args[0] - - span = tracer.start_span( - request.method, kind=trace.SpanKind.CLIENT, start_time=start_time, - ) - - if span.is_recording(): - attributes = { - "component": "tornado", - "http.url": request.url, - "http.method": request.method, - } - for key, value in attributes.items(): - span.set_attribute(key, value) - - with tracer.use_span(span): - propagators.inject(type(request.headers).__setitem__, request.headers) - future = func(*args, **kwargs) - future.add_done_callback( - functools.partial(_finish_tracing_callback, span=span) - ) - return future - - -def _finish_tracing_callback(future, span): - status_code = None - description = None - exc = future.exception() - if span.is_recording() and exc: - if isinstance(exc, HTTPError): - status_code = exc.code - description = "{}: {}".format(type(exc).__name__, exc) - else: - status_code = future.result().code - - if status_code is not None: - span.set_attribute("http.status_code", status_code) - span.set_status( - Status( - status_code=http_status_to_status_code(status_code), - description=description, - ) - ) - span.end() diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-tornado/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py b/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py deleted file mode 100644 index 5b429766ec..0000000000 --- a/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py +++ /dev/null @@ -1,392 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from unittest.mock import Mock, patch - -from tornado.testing import AsyncHTTPTestCase - -from opentelemetry import trace -from opentelemetry.instrumentation.tornado import ( - TornadoInstrumentor, - patch_handler_class, - unpatch_handler_class, -) -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace import SpanKind -from opentelemetry.util import ExcludeList - -from .tornado_test_app import ( - AsyncHandler, - DynamicHandler, - MainHandler, - make_app, -) - - -class TornadoTest(AsyncHTTPTestCase, TestBase): - def get_app(self): - tracer = trace.get_tracer(__name__) - app = make_app(tracer) - return app - - def setUp(self): - TornadoInstrumentor().instrument() - super().setUp() - - def tearDown(self): - TornadoInstrumentor().uninstrument() - super().tearDown() - - -class TestTornadoInstrumentor(TornadoTest): - def test_patch_references(self): - self.assertEqual(len(TornadoInstrumentor().patched_handlers), 0) - - self.fetch("/") - self.fetch("/async") - self.assertEqual( - TornadoInstrumentor().patched_handlers, [MainHandler, AsyncHandler] - ) - - self.fetch("/async") - self.fetch("/") - self.assertEqual( - TornadoInstrumentor().patched_handlers, [MainHandler, AsyncHandler] - ) - - TornadoInstrumentor().uninstrument() - self.assertEqual(TornadoInstrumentor().patched_handlers, []) - - def test_patch_applied_only_once(self): - tracer = trace.get_tracer(__name__) - self.assertTrue(patch_handler_class(tracer, AsyncHandler)) - self.assertFalse(patch_handler_class(tracer, AsyncHandler)) - self.assertFalse(patch_handler_class(tracer, AsyncHandler)) - unpatch_handler_class(AsyncHandler) - - -class TestTornadoInstrumentation(TornadoTest): - def test_http_calls(self): - methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] - for method in methods: - self._test_http_method_call(method) - - def _test_http_method_call(self, method): - body = "" if method in ["POST", "PUT", "PATCH"] else None - response = self.fetch("/", method=method, body=body) - self.assertEqual(response.code, 201) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 3) - - manual, server, client = self.sorted_spans(spans) - - self.assertEqual(manual.name, "manual") - self.assertEqual(manual.parent, server.context) - self.assertEqual(manual.context.trace_id, client.context.trace_id) - - self.assertEqual(server.name, "MainHandler." + method.lower()) - self.assertTrue(server.parent.is_remote) - self.assertNotEqual(server.parent, client.context) - self.assertEqual(server.parent.span_id, client.context.span_id) - self.assertEqual(server.context.trace_id, client.context.trace_id) - self.assertEqual(server.kind, SpanKind.SERVER) - self.assert_span_has_attributes( - server, - { - "component": "tornado", - "http.method": method, - "http.scheme": "http", - "http.host": "127.0.0.1:" + str(self.get_http_port()), - "http.target": "/", - "net.peer.ip": "127.0.0.1", - "http.status_text": "Created", - "http.status_code": 201, - }, - ) - - self.assertEqual(client.name, method) - self.assertFalse(client.context.is_remote) - self.assertIsNone(client.parent) - self.assertEqual(client.kind, SpanKind.CLIENT) - self.assert_span_has_attributes( - client, - { - "component": "tornado", - "http.url": self.get_url("/"), - "http.method": method, - "http.status_code": 201, - }, - ) - - self.memory_exporter.clear() - - def test_not_recording(self): - mock_tracer = Mock() - mock_span = Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = True - with patch("opentelemetry.trace.get_tracer") as tracer: - tracer.return_value = mock_tracer - self.fetch("/") - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_async_handler(self): - self._test_async_handler("/async", "AsyncHandler") - - def test_coroutine_handler(self): - self._test_async_handler("/cor", "CoroutineHandler") - - def _test_async_handler(self, url, handler_name): - response = self.fetch(url) - self.assertEqual(response.code, 201) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 5) - - sub2, sub1, sub_wrapper, server, client = self.sorted_spans(spans) - - self.assertEqual(sub2.name, "sub-task-2") - self.assertEqual(sub2.parent, sub_wrapper.context) - self.assertEqual(sub2.context.trace_id, client.context.trace_id) - - self.assertEqual(sub1.name, "sub-task-1") - self.assertEqual(sub1.parent, sub_wrapper.context) - self.assertEqual(sub1.context.trace_id, client.context.trace_id) - - self.assertEqual(sub_wrapper.name, "sub-task-wrapper") - self.assertEqual(sub_wrapper.parent, server.context) - self.assertEqual(sub_wrapper.context.trace_id, client.context.trace_id) - - self.assertEqual(server.name, handler_name + ".get") - self.assertTrue(server.parent.is_remote) - self.assertNotEqual(server.parent, client.context) - self.assertEqual(server.parent.span_id, client.context.span_id) - self.assertEqual(server.context.trace_id, client.context.trace_id) - self.assertEqual(server.kind, SpanKind.SERVER) - self.assert_span_has_attributes( - server, - { - "component": "tornado", - "http.method": "GET", - "http.scheme": "http", - "http.host": "127.0.0.1:" + str(self.get_http_port()), - "http.target": url, - "net.peer.ip": "127.0.0.1", - "http.status_text": "Created", - "http.status_code": 201, - }, - ) - - self.assertEqual(client.name, "GET") - self.assertFalse(client.context.is_remote) - self.assertIsNone(client.parent) - self.assertEqual(client.kind, SpanKind.CLIENT) - self.assert_span_has_attributes( - client, - { - "component": "tornado", - "http.url": self.get_url(url), - "http.method": "GET", - "http.status_code": 201, - }, - ) - - def test_500(self): - response = self.fetch("/error") - self.assertEqual(response.code, 500) - - spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) - self.assertEqual(len(spans), 2) - server, client = spans - - self.assertEqual(server.name, "BadHandler.get") - self.assertEqual(server.kind, SpanKind.SERVER) - self.assert_span_has_attributes( - server, - { - "component": "tornado", - "http.method": "GET", - "http.scheme": "http", - "http.host": "127.0.0.1:" + str(self.get_http_port()), - "http.target": "/error", - "net.peer.ip": "127.0.0.1", - "http.status_code": 500, - }, - ) - - self.assertEqual(client.name, "GET") - self.assertEqual(client.kind, SpanKind.CLIENT) - self.assert_span_has_attributes( - client, - { - "component": "tornado", - "http.url": self.get_url("/error"), - "http.method": "GET", - "http.status_code": 500, - }, - ) - - def test_404(self): - response = self.fetch("/missing-url") - self.assertEqual(response.code, 404) - - spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) - self.assertEqual(len(spans), 2) - server, client = spans - - self.assertEqual(server.name, "ErrorHandler.get") - self.assertEqual(server.kind, SpanKind.SERVER) - self.assert_span_has_attributes( - server, - { - "component": "tornado", - "http.method": "GET", - "http.scheme": "http", - "http.host": "127.0.0.1:" + str(self.get_http_port()), - "http.target": "/missing-url", - "net.peer.ip": "127.0.0.1", - "http.status_text": "Not Found", - "http.status_code": 404, - }, - ) - - self.assertEqual(client.name, "GET") - self.assertEqual(client.kind, SpanKind.CLIENT) - self.assert_span_has_attributes( - client, - { - "component": "tornado", - "http.url": self.get_url("/missing-url"), - "http.method": "GET", - "http.status_code": 404, - }, - ) - - def test_dynamic_handler(self): - response = self.fetch("/dyna") - self.assertEqual(response.code, 404) - self.memory_exporter.clear() - - self._app.add_handlers(r".+", [(r"/dyna", DynamicHandler)]) - - response = self.fetch("/dyna") - self.assertEqual(response.code, 202) - - spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) - self.assertEqual(len(spans), 2) - server, client = spans - - self.assertEqual(server.name, "DynamicHandler.get") - self.assertTrue(server.parent.is_remote) - self.assertNotEqual(server.parent, client.context) - self.assertEqual(server.parent.span_id, client.context.span_id) - self.assertEqual(server.context.trace_id, client.context.trace_id) - self.assertEqual(server.kind, SpanKind.SERVER) - self.assert_span_has_attributes( - server, - { - "component": "tornado", - "http.method": "GET", - "http.scheme": "http", - "http.host": "127.0.0.1:" + str(self.get_http_port()), - "http.target": "/dyna", - "net.peer.ip": "127.0.0.1", - "http.status_text": "Accepted", - "http.status_code": 202, - }, - ) - - self.assertEqual(client.name, "GET") - self.assertFalse(client.context.is_remote) - self.assertIsNone(client.parent) - self.assertEqual(client.kind, SpanKind.CLIENT) - self.assert_span_has_attributes( - client, - { - "component": "tornado", - "http.url": self.get_url("/dyna"), - "http.method": "GET", - "http.status_code": 202, - }, - ) - - @patch( - "opentelemetry.instrumentation.tornado._excluded_urls", - ExcludeList(["healthz", "ping"]), - ) - def test_exclude_lists(self): - def test_excluded(path): - self.fetch(path) - spans = self.sorted_spans( - self.memory_exporter.get_finished_spans() - ) - self.assertEqual(len(spans), 1) - client = spans[0] - self.assertEqual(client.name, "GET") - self.assertEqual(client.kind, SpanKind.CLIENT) - self.assert_span_has_attributes( - client, - { - "component": "tornado", - "http.url": self.get_url(path), - "http.method": "GET", - "http.status_code": 200, - }, - ) - self.memory_exporter.clear() - - test_excluded("/healthz") - test_excluded("/ping") - - @patch( - "opentelemetry.instrumentation.tornado._traced_attrs", - ["uri", "full_url", "query"], - ) - def test_traced_attrs(self): - self.fetch("/ping?q=abc&b=123") - spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) - self.assertEqual(len(spans), 2) - server_span = spans[0] - self.assertEqual(server_span.kind, SpanKind.SERVER) - self.assert_span_has_attributes( - server_span, {"uri": "/ping?q=abc&b=123", "query": "q=abc&b=123"} - ) - self.memory_exporter.clear() - - -class TestTornadoUninstrument(TornadoTest): - def test_uninstrument(self): - response = self.fetch("/") - self.assertEqual(response.code, 201) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 3) - manual, server, client = self.sorted_spans(spans) - self.assertEqual(manual.name, "manual") - self.assertEqual(server.name, "MainHandler.get") - self.assertEqual(client.name, "GET") - self.memory_exporter.clear() - - TornadoInstrumentor().uninstrument() - - response = self.fetch("/") - self.assertEqual(response.code, 201) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - manual = spans[0] - self.assertEqual(manual.name, "manual") diff --git a/instrumentation/opentelemetry-instrumentation-tornado/tests/tornado_test_app.py b/instrumentation/opentelemetry-instrumentation-tornado/tests/tornado_test_app.py deleted file mode 100644 index 307dc60b76..0000000000 --- a/instrumentation/opentelemetry-instrumentation-tornado/tests/tornado_test_app.py +++ /dev/null @@ -1,99 +0,0 @@ -# pylint: disable=W0223,R0201 - -import tornado.web -from tornado import gen - - -class AsyncHandler(tornado.web.RequestHandler): - async def get(self): - with self.application.tracer.start_as_current_span("sub-task-wrapper"): - await self.do_something1() - await self.do_something2() - self.set_status(201) - self.write("{}") - - async def do_something1(self): - with self.application.tracer.start_as_current_span("sub-task-1"): - tornado.gen.sleep(0.1) - - async def do_something2(self): - with self.application.tracer.start_as_current_span("sub-task-2"): - tornado.gen.sleep(0.1) - - -class CoroutineHandler(tornado.web.RequestHandler): - @gen.coroutine - def get(self): - with self.application.tracer.start_as_current_span("sub-task-wrapper"): - yield self.do_something1() - yield self.do_something2() - self.set_status(201) - self.write("{}") - - @gen.coroutine - def do_something1(self): - with self.application.tracer.start_as_current_span("sub-task-1"): - tornado.gen.sleep(0.1) - - @gen.coroutine - def do_something2(self): - with self.application.tracer.start_as_current_span("sub-task-2"): - tornado.gen.sleep(0.1) - - -class MainHandler(tornado.web.RequestHandler): - def _handler(self): - with self.application.tracer.start_as_current_span("manual"): - self.write("Hello, world") - self.set_status(201) - - def get(self): - return self._handler() - - def post(self): - return self._handler() - - def patch(self): - return self._handler() - - def delete(self): - return self._handler() - - def put(self): - return self._handler() - - def head(self): - return self._handler() - - def options(self): - return self._handler() - - -class BadHandler(tornado.web.RequestHandler): - def get(self): - raise NameError("some random name error") - - -class DynamicHandler(tornado.web.RequestHandler): - def get(self): - self.set_status(202) - - -class HealthCheckHandler(tornado.web.RequestHandler): - def get(self): - self.set_status(200) - - -def make_app(tracer): - app = tornado.web.Application( - [ - (r"/", MainHandler), - (r"/error", BadHandler), - (r"/cor", CoroutineHandler), - (r"/async", AsyncHandler), - (r"/healthz", HealthCheckHandler), - (r"/ping", HealthCheckHandler), - ] - ) - app.tracer = tracer - return app diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md deleted file mode 100644 index 68bd25237c..0000000000 --- a/instrumentation/opentelemetry-instrumentation-wsgi/CHANGELOG.md +++ /dev/null @@ -1,51 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-instrumentation-wsgi - ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) - -## Version 0.11b0 - -Released 2020-07-28 - -- Set span status on wsgi errors - ([#864](https://github.com/open-telemetry/opentelemetry-python/pull/864)) - -## 0.4a0 - -Released 2020-02-21 - -- Updating network connection attribute names - ([#350](https://github.com/open-telemetry/opentelemetry-python/pull/350)) - -## 0.3a0 - -Released 2019-12-11 - -- Support new semantic conventions - ([#299](https://github.com/open-telemetry/opentelemetry-python/pull/299)) -- Updates for core library changes - -## 0.2a0 - -Released 2019-10-29 - -- Updates for core library changes - -## 0.1a0 - -Released 2019-09-30 - -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/LICENSE b/instrumentation/opentelemetry-instrumentation-wsgi/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/instrumentation/opentelemetry-instrumentation-wsgi/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-wsgi/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/instrumentation/opentelemetry-instrumentation-wsgi/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/README.rst b/instrumentation/opentelemetry-instrumentation-wsgi/README.rst deleted file mode 100644 index ac39dac0c7..0000000000 --- a/instrumentation/opentelemetry-instrumentation-wsgi/README.rst +++ /dev/null @@ -1,26 +0,0 @@ -OpenTelemetry WSGI Middleware -============================= - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-wsgi.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-wsgi/ - - -This library provides a WSGI middleware that can be used on any WSGI framework -(such as Django / Flask) to track requests timing through OpenTelemetry. - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation-wsgi - - -References ----------- - -* `OpenTelemetry WSGI Middleware `_ -* `OpenTelemetry Project `_ -* `WSGI `_ diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg b/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg deleted file mode 100644 index 585a513412..0000000000 --- a/instrumentation/opentelemetry-instrumentation-wsgi/setup.cfg +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation-wsgi -description = WSGI Middleware for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-wsgi -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-instrumentation == 0.16.dev0 - -[options.extras_require] -test = - opentelemetry-test == 0.16.dev0 - -[options.packages.find] -where = src diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/setup.py b/instrumentation/opentelemetry-instrumentation-wsgi/setup.py deleted file mode 100644 index fb4ffa7437..0000000000 --- a/instrumentation/opentelemetry-instrumentation-wsgi/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "wsgi", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py deleted file mode 100644 index e1ef92c6ba..0000000000 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ /dev/null @@ -1,248 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This library provides a WSGI middleware that can be used on any WSGI framework -(such as Django / Flask) to track requests timing through OpenTelemetry. - -Usage (Flask) -------------- - -.. code-block:: python - - from flask import Flask - from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware - - app = Flask(__name__) - app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) - - @app.route("/") - def hello(): - return "Hello!" - - if __name__ == "__main__": - app.run(debug=True) - - -Usage (Django) --------------- - -Modify the application's ``wsgi.py`` file as shown below. - -.. code-block:: python - - import os - from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware - from django.core.wsgi import get_wsgi_application - - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') - - application = get_wsgi_application() - application = OpenTelemetryMiddleware(application) - -API ---- -""" - -import functools -import typing -import wsgiref.util as wsgiref_util - -from opentelemetry import context, propagators, trace -from opentelemetry.instrumentation.utils import http_status_to_status_code -from opentelemetry.instrumentation.wsgi.version import __version__ -from opentelemetry.trace.propagation.textmap import DictGetter -from opentelemetry.trace.status import Status, StatusCode - -_HTTP_VERSION_PREFIX = "HTTP/" - - -class CarrierGetter(DictGetter): - def get(self, carrier: dict, key: str) -> typing.List[str]: - """Getter implementation to retrieve a HTTP header value from the - PEP3333-conforming WSGI environ - - Args: - carrier: WSGI environ object - key: header name in environ object - Returns: - A list with a single string with the header value if it exists, - else an empty list. - """ - environ_key = "HTTP_" + key.upper().replace("-", "_") - value = carrier.get(environ_key) - if value is not None: - return [value] - return [] - - def keys(self, carrier): - return [] - - -carrier_getter = CarrierGetter() - - -def setifnotnone(dic, key, value): - if value is not None: - dic[key] = value - - -def collect_request_attributes(environ): - """Collects HTTP request attributes from the PEP3333-conforming - WSGI environ and returns a dictionary to be used as span creation attributes.""" - - result = { - "component": "http", - "http.method": environ.get("REQUEST_METHOD"), - "http.server_name": environ.get("SERVER_NAME"), - "http.scheme": environ.get("wsgi.url_scheme"), - } - - host_port = environ.get("SERVER_PORT") - if host_port is not None: - result.update({"host.port": int(host_port)}) - - setifnotnone(result, "http.host", environ.get("HTTP_HOST")) - target = environ.get("RAW_URI") - if target is None: # Note: `"" or None is None` - target = environ.get("REQUEST_URI") - if target is not None: - result["http.target"] = target - else: - result["http.url"] = wsgiref_util.request_uri(environ) - - remote_addr = environ.get("REMOTE_ADDR") - if remote_addr: - result["net.peer.ip"] = remote_addr - remote_host = environ.get("REMOTE_HOST") - if remote_host and remote_host != remote_addr: - result["net.peer.name"] = remote_host - - user_agent = environ.get("HTTP_USER_AGENT") - if user_agent is not None and len(user_agent) > 0: - result["http.user_agent"] = user_agent - - setifnotnone(result, "net.peer.port", environ.get("REMOTE_PORT")) - flavor = environ.get("SERVER_PROTOCOL", "") - if flavor.upper().startswith(_HTTP_VERSION_PREFIX): - flavor = flavor[len(_HTTP_VERSION_PREFIX) :] - if flavor: - result["http.flavor"] = flavor - - return result - - -def add_response_attributes( - span, start_response_status, response_headers -): # pylint: disable=unused-argument - """Adds HTTP response attributes to span using the arguments - passed to a PEP3333-conforming start_response callable.""" - if not span.is_recording(): - return - status_code, status_text = start_response_status.split(" ", 1) - span.set_attribute("http.status_text", status_text) - - try: - status_code = int(status_code) - except ValueError: - span.set_status( - Status( - StatusCode.ERROR, - "Non-integer HTTP status: " + repr(status_code), - ) - ) - else: - span.set_attribute("http.status_code", status_code) - span.set_status(Status(http_status_to_status_code(status_code))) - - -def get_default_span_name(environ): - """Default implementation for name_callback, returns HTTP {METHOD_NAME}.""" - return "HTTP {}".format(environ.get("REQUEST_METHOD", "")).strip() - - -class OpenTelemetryMiddleware: - """The WSGI application middleware. - - This class is a PEP 3333 conforming WSGI middleware that starts and - annotates spans for any requests it is invoked with. - - Args: - wsgi: The WSGI application callable to forward requests to. - name_callback: Callback which calculates a generic span name for an - incoming HTTP request based on the PEP3333 WSGI environ. - Optional: Defaults to get_default_span_name. - """ - - def __init__(self, wsgi, name_callback=get_default_span_name): - self.wsgi = wsgi - self.tracer = trace.get_tracer(__name__, __version__) - self.name_callback = name_callback - - @staticmethod - def _create_start_response(span, start_response): - @functools.wraps(start_response) - def _start_response(status, response_headers, *args, **kwargs): - add_response_attributes(span, status, response_headers) - return start_response(status, response_headers, *args, **kwargs) - - return _start_response - - def __call__(self, environ, start_response): - """The WSGI application - - Args: - environ: A WSGI environment. - start_response: The WSGI start_response callable. - """ - - token = context.attach(propagators.extract(carrier_getter, environ)) - span_name = self.name_callback(environ) - - span = self.tracer.start_span( - span_name, - kind=trace.SpanKind.SERVER, - attributes=collect_request_attributes(environ), - ) - - try: - with self.tracer.use_span(span): - start_response = self._create_start_response( - span, start_response - ) - iterable = self.wsgi(environ, start_response) - return _end_span_after_iterating( - iterable, span, self.tracer, token - ) - except Exception as ex: - if span.is_recording(): - span.set_status(Status(StatusCode.ERROR, str(ex))) - span.end() - context.detach(token) - raise - - -# Put this in a subfunction to not delay the call to the wrapped -# WSGI application (instrumentation should change the application -# behavior as little as possible). -def _end_span_after_iterating(iterable, span, tracer, token): - try: - with tracer.use_span(span): - for yielded in iterable: - yield yielded - finally: - close = getattr(iterable, "close", None) - if close: - close() - span.end() - context.detach(token) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py deleted file mode 100644 index 144b4cf069..0000000000 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ /dev/null @@ -1,359 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import sys -import unittest -import unittest.mock as mock -import wsgiref.util as wsgiref_util -from urllib.parse import urlsplit - -import opentelemetry.instrumentation.wsgi as otel_wsgi -from opentelemetry import trace as trace_api -from opentelemetry.test.wsgitestutil import WsgiTestBase -from opentelemetry.trace.status import StatusCode - - -class Response: - def __init__(self): - self.iter = iter([b"*"]) - self.close_calls = 0 - - def __iter__(self): - return self - - def __next__(self): - return next(self.iter) - - def close(self): - self.close_calls += 1 - - -def simple_wsgi(environ, start_response): - assert isinstance(environ, dict) - start_response("200 OK", [("Content-Type", "text/plain")]) - return [b"*"] - - -def create_iter_wsgi(response): - def iter_wsgi(environ, start_response): - assert isinstance(environ, dict) - start_response("200 OK", [("Content-Type", "text/plain")]) - return response - - return iter_wsgi - - -def create_gen_wsgi(response): - def gen_wsgi(environ, start_response): - result = create_iter_wsgi(response)(environ, start_response) - yield from result - getattr(result, "close", lambda: None)() - - return gen_wsgi - - -def error_wsgi(environ, start_response): - assert isinstance(environ, dict) - try: - raise ValueError - except ValueError: - exc_info = sys.exc_info() - start_response("200 OK", [("Content-Type", "text/plain")], exc_info) - exc_info = None - return [b"*"] - - -def error_wsgi_unhandled(environ, start_response): - assert isinstance(environ, dict) - raise ValueError - - -class TestWsgiApplication(WsgiTestBase): - def validate_response( - self, response, error=None, span_name="HTTP GET", http_method="GET" - ): - while True: - try: - value = next(response) - self.assertEqual(value, b"*") - except StopIteration: - break - - self.assertEqual(self.status, "200 OK") - self.assertEqual( - self.response_headers, [("Content-Type", "text/plain")] - ) - if error: - self.assertIs(self.exc_info[0], error) - self.assertIsInstance(self.exc_info[1], error) - self.assertIsNotNone(self.exc_info[2]) - else: - self.assertIsNone(self.exc_info) - - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, span_name) - self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) - expected_attributes = { - "component": "http", - "http.server_name": "127.0.0.1", - "http.scheme": "http", - "host.port": 80, - "http.host": "127.0.0.1", - "http.flavor": "1.0", - "http.url": "http://127.0.0.1/", - "http.status_text": "OK", - "http.status_code": 200, - } - if http_method is not None: - expected_attributes["http.method"] = http_method - self.assertEqual(span_list[0].attributes, expected_attributes) - - def test_basic_wsgi_call(self): - app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) - response = app(self.environ, self.start_response) - self.validate_response(response) - - def test_wsgi_not_recording(self): - mock_tracer = mock.Mock() - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - mock_tracer.use_span.return_value.__enter__ = mock_span - mock_tracer.use_span.return_value.__exit__ = mock_span - with mock.patch("opentelemetry.trace.get_tracer") as tracer: - tracer.return_value = mock_tracer - app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) - # pylint: disable=W0612 - response = app(self.environ, self.start_response) # noqa: F841 - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - def test_wsgi_iterable(self): - original_response = Response() - iter_wsgi = create_iter_wsgi(original_response) - app = otel_wsgi.OpenTelemetryMiddleware(iter_wsgi) - response = app(self.environ, self.start_response) - # Verify that start_response has been called - self.assertTrue(self.status) - self.validate_response(response) - - # Verify that close has been called exactly once - self.assertEqual(1, original_response.close_calls) - - def test_wsgi_generator(self): - original_response = Response() - gen_wsgi = create_gen_wsgi(original_response) - app = otel_wsgi.OpenTelemetryMiddleware(gen_wsgi) - response = app(self.environ, self.start_response) - # Verify that start_response has not been called - self.assertIsNone(self.status) - self.validate_response(response) - - # Verify that close has been called exactly once - self.assertEqual(original_response.close_calls, 1) - - def test_wsgi_exc_info(self): - app = otel_wsgi.OpenTelemetryMiddleware(error_wsgi) - response = app(self.environ, self.start_response) - self.validate_response(response, error=ValueError) - - def test_wsgi_internal_error(self): - app = otel_wsgi.OpenTelemetryMiddleware(error_wsgi_unhandled) - self.assertRaises(ValueError, app, self.environ, self.start_response) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual( - span_list[0].status.status_code, StatusCode.ERROR, - ) - - def test_override_span_name(self): - """Test that span_names can be overwritten by our callback function.""" - span_name = "Dymaxion" - - def get_predefined_span_name(scope): - # pylint: disable=unused-argument - return span_name - - app = otel_wsgi.OpenTelemetryMiddleware( - simple_wsgi, name_callback=get_predefined_span_name - ) - response = app(self.environ, self.start_response) - self.validate_response(response, span_name=span_name) - - def test_default_span_name_missing_request_method(self): - """Test that default span_names with missing request method.""" - self.environ.pop("REQUEST_METHOD") - app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) - response = app(self.environ, self.start_response) - self.validate_response(response, span_name="HTTP", http_method=None) - - -class TestWsgiAttributes(unittest.TestCase): - def setUp(self): - self.environ = {} - wsgiref_util.setup_testing_defaults(self.environ) - self.span = mock.create_autospec(trace_api.Span, spec_set=True) - - def test_request_attributes(self): - self.environ["QUERY_STRING"] = "foo=bar" - - attrs = otel_wsgi.collect_request_attributes(self.environ) - self.assertDictEqual( - attrs, - { - "component": "http", - "http.method": "GET", - "http.host": "127.0.0.1", - "http.url": "http://127.0.0.1/?foo=bar", - "host.port": 80, - "http.scheme": "http", - "http.server_name": "127.0.0.1", - "http.flavor": "1.0", - }, - ) - - def validate_url(self, expected_url, raw=False, has_host=True): - parts = urlsplit(expected_url) - expected = { - "http.scheme": parts.scheme, - "host.port": parts.port or (80 if parts.scheme == "http" else 443), - "http.server_name": parts.hostname, # Not true in the general case, but for all tests. - } - if raw: - expected["http.target"] = expected_url.split(parts.netloc, 1)[1] - else: - expected["http.url"] = expected_url - if has_host: - expected["http.host"] = parts.hostname - - attrs = otel_wsgi.collect_request_attributes(self.environ) - self.assertGreaterEqual( - attrs.items(), expected.items(), expected_url + " expected." - ) - - def test_request_attributes_with_partial_raw_uri(self): - self.environ["RAW_URI"] = "/#top" - self.validate_url("http://127.0.0.1/#top", raw=True) - - def test_request_attributes_with_partial_raw_uri_and_nonstandard_port( - self, - ): - self.environ["RAW_URI"] = "/?" - del self.environ["HTTP_HOST"] - self.environ["SERVER_PORT"] = "8080" - self.validate_url("http://127.0.0.1:8080/?", raw=True, has_host=False) - - def test_https_uri_port(self): - del self.environ["HTTP_HOST"] - self.environ["SERVER_PORT"] = "443" - self.environ["wsgi.url_scheme"] = "https" - self.validate_url("https://127.0.0.1/", has_host=False) - - self.environ["SERVER_PORT"] = "8080" - self.validate_url("https://127.0.0.1:8080/", has_host=False) - - self.environ["SERVER_PORT"] = "80" - self.validate_url("https://127.0.0.1:80/", has_host=False) - - def test_http_uri_port(self): - del self.environ["HTTP_HOST"] - self.environ["SERVER_PORT"] = "80" - self.environ["wsgi.url_scheme"] = "http" - self.validate_url("http://127.0.0.1/", has_host=False) - - self.environ["SERVER_PORT"] = "8080" - self.validate_url("http://127.0.0.1:8080/", has_host=False) - - self.environ["SERVER_PORT"] = "443" - self.validate_url("http://127.0.0.1:443/", has_host=False) - - def test_request_attributes_with_nonstandard_port_and_no_host(self): - del self.environ["HTTP_HOST"] - self.environ["SERVER_PORT"] = "8080" - self.validate_url("http://127.0.0.1:8080/", has_host=False) - - self.environ["SERVER_PORT"] = "443" - self.validate_url("http://127.0.0.1:443/", has_host=False) - - def test_request_attributes_with_conflicting_nonstandard_port(self): - self.environ[ - "HTTP_HOST" - ] += ":8080" # Note that we do not correct SERVER_PORT - expected = { - "http.host": "127.0.0.1:8080", - "http.url": "http://127.0.0.1:8080/", - "host.port": 80, - } - self.assertGreaterEqual( - otel_wsgi.collect_request_attributes(self.environ).items(), - expected.items(), - ) - - def test_request_attributes_with_faux_scheme_relative_raw_uri(self): - self.environ["RAW_URI"] = "//127.0.0.1/?" - self.validate_url("http://127.0.0.1//127.0.0.1/?", raw=True) - - def test_request_attributes_pathless(self): - self.environ["RAW_URI"] = "" - expected = {"http.target": ""} - self.assertGreaterEqual( - otel_wsgi.collect_request_attributes(self.environ).items(), - expected.items(), - ) - - def test_request_attributes_with_full_request_uri(self): - self.environ["HTTP_HOST"] = "127.0.0.1:8080" - self.environ["REQUEST_METHOD"] = "CONNECT" - self.environ[ - "REQUEST_URI" - ] = "127.0.0.1:8080" # Might happen in a CONNECT request - expected = { - "http.host": "127.0.0.1:8080", - "http.target": "127.0.0.1:8080", - } - self.assertGreaterEqual( - otel_wsgi.collect_request_attributes(self.environ).items(), - expected.items(), - ) - - def test_http_user_agent_attribute(self): - self.environ["HTTP_USER_AGENT"] = "test-useragent" - expected = {"http.user_agent": "test-useragent"} - self.assertGreaterEqual( - otel_wsgi.collect_request_attributes(self.environ).items(), - expected.items(), - ) - - def test_response_attributes(self): - otel_wsgi.add_response_attributes(self.span, "404 Not Found", {}) - expected = ( - mock.call("http.status_code", 404), - mock.call("http.status_text", "Not Found"), - ) - self.assertEqual(self.span.set_attribute.call_count, len(expected)) - self.span.set_attribute.assert_has_calls(expected, any_order=True) - - def test_response_attributes_invalid_status_code(self): - otel_wsgi.add_response_attributes(self.span, "Invalid Status Code", {}) - self.assertEqual(self.span.set_attribute.call_count, 1) - self.span.set_attribute.assert_called_with( - "http.status_text", "Status Code" - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 724061690d..cfa1764ab3 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -309,6 +309,18 @@ def keyfunc(path): return float("inf") targets.sort(key=keyfunc) + if "ignore" in mcfg: + ignore = getlistcfg(mcfg["ignore"]) + + def filter_func(path): + path = path.relative_to(rootpath) + for pattern in ignore: + if path.match(pattern): + return False + return True + + filtered = filter(filter_func, targets) + targets = list(filtered) subglobs = getlistcfg(mcfg.get("subglob", "")) if subglobs: diff --git a/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py b/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py deleted file mode 100644 index 2e37efe224..0000000000 --- a/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py +++ /dev/null @@ -1,260 +0,0 @@ -import asyncio -import os - -import asyncpg - -from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace.status import StatusCode - -POSTGRES_HOST = os.getenv("POSTGRESQL_HOST ", "localhost") -POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT ", "5432")) -POSTGRES_DB_NAME = os.getenv("POSTGRESQL_DB_NAME ", "opentelemetry-tests") -POSTGRES_PASSWORD = os.getenv("POSTGRESQL_HOST ", "testpassword") -POSTGRES_USER = os.getenv("POSTGRESQL_HOST ", "testuser") - - -def async_call(coro): - loop = asyncio.get_event_loop() - return loop.run_until_complete(coro) - - -class TestFunctionalAsyncPG(TestBase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls._connection = None - cls._cursor = None - cls._tracer = cls.tracer_provider.get_tracer(__name__) - AsyncPGInstrumentor().instrument(tracer_provider=cls.tracer_provider) - cls._connection = async_call( - asyncpg.connect( - database=POSTGRES_DB_NAME, - user=POSTGRES_USER, - password=POSTGRES_PASSWORD, - host=POSTGRES_HOST, - port=POSTGRES_PORT, - ) - ) - - @classmethod - def tearDownClass(cls): - AsyncPGInstrumentor().uninstrument() - - def test_instrumented_execute_method_without_arguments(self, *_, **__): - async_call(self._connection.execute("SELECT 42;")) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - self.assertIs(StatusCode.UNSET, spans[0].status.status_code) - self.assertEqual( - spans[0].attributes, - { - "db.type": "sql", - "db.user": POSTGRES_USER, - "db.instance": POSTGRES_DB_NAME, - "db.statement": "SELECT 42;", - }, - ) - - def test_instrumented_fetch_method_without_arguments(self, *_, **__): - async_call(self._connection.fetch("SELECT 42;")) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - self.assertEqual( - spans[0].attributes, - { - "db.type": "sql", - "db.user": POSTGRES_USER, - "db.instance": POSTGRES_DB_NAME, - "db.statement": "SELECT 42;", - }, - ) - - def test_instrumented_transaction_method(self, *_, **__): - async def _transaction_execute(): - async with self._connection.transaction(): - await self._connection.execute("SELECT 42;") - - async_call(_transaction_execute()) - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(3, len(spans)) - self.assertEqual( - { - "db.instance": POSTGRES_DB_NAME, - "db.user": POSTGRES_USER, - "db.type": "sql", - "db.statement": "BEGIN;", - }, - spans[0].attributes, - ) - self.assertIs(StatusCode.UNSET, spans[0].status.status_code) - self.assertEqual( - { - "db.instance": POSTGRES_DB_NAME, - "db.user": POSTGRES_USER, - "db.type": "sql", - "db.statement": "SELECT 42;", - }, - spans[1].attributes, - ) - self.assertIs(StatusCode.UNSET, spans[1].status.status_code) - self.assertEqual( - { - "db.instance": POSTGRES_DB_NAME, - "db.user": POSTGRES_USER, - "db.type": "sql", - "db.statement": "COMMIT;", - }, - spans[2].attributes, - ) - self.assertIs(StatusCode.UNSET, spans[2].status.status_code) - - def test_instrumented_failed_transaction_method(self, *_, **__): - async def _transaction_execute(): - async with self._connection.transaction(): - await self._connection.execute("SELECT 42::uuid;") - - with self.assertRaises(asyncpg.CannotCoerceError): - async_call(_transaction_execute()) - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(3, len(spans)) - self.assertEqual( - { - "db.instance": POSTGRES_DB_NAME, - "db.user": POSTGRES_USER, - "db.type": "sql", - "db.statement": "BEGIN;", - }, - spans[0].attributes, - ) - self.assertIs(StatusCode.UNSET, spans[0].status.status_code) - self.assertEqual( - { - "db.instance": POSTGRES_DB_NAME, - "db.user": POSTGRES_USER, - "db.type": "sql", - "db.statement": "SELECT 42::uuid;", - }, - spans[1].attributes, - ) - self.assertEqual( - StatusCode.ERROR, spans[1].status.status_code, - ) - self.assertEqual( - { - "db.instance": POSTGRES_DB_NAME, - "db.user": POSTGRES_USER, - "db.type": "sql", - "db.statement": "ROLLBACK;", - }, - spans[2].attributes, - ) - self.assertIs(StatusCode.UNSET, spans[2].status.status_code) - - def test_instrumented_method_doesnt_capture_parameters(self, *_, **__): - async_call(self._connection.execute("SELECT $1;", "1")) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - self.assertIs(StatusCode.UNSET, spans[0].status.status_code) - self.assertEqual( - spans[0].attributes, - { - "db.type": "sql", - "db.user": POSTGRES_USER, - # This shouldn't be set because we don't capture parameters by - # default - # - # "db.statement.parameters": "('1',)", - "db.instance": POSTGRES_DB_NAME, - "db.statement": "SELECT $1;", - }, - ) - - -class TestFunctionalAsyncPG_CaptureParameters(TestBase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls._connection = None - cls._cursor = None - cls._tracer = cls.tracer_provider.get_tracer(__name__) - AsyncPGInstrumentor(capture_parameters=True).instrument( - tracer_provider=cls.tracer_provider - ) - cls._connection = async_call( - asyncpg.connect( - database=POSTGRES_DB_NAME, - user=POSTGRES_USER, - password=POSTGRES_PASSWORD, - host=POSTGRES_HOST, - port=POSTGRES_PORT, - ) - ) - - @classmethod - def tearDownClass(cls): - AsyncPGInstrumentor().uninstrument() - - def test_instrumented_execute_method_with_arguments(self, *_, **__): - async_call(self._connection.execute("SELECT $1;", "1")) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - self.assertIs(StatusCode.UNSET, spans[0].status.status_code) - self.assertEqual( - spans[0].attributes, - { - "db.type": "sql", - "db.user": POSTGRES_USER, - "db.statement.parameters": "('1',)", - "db.instance": POSTGRES_DB_NAME, - "db.statement": "SELECT $1;", - }, - ) - - def test_instrumented_fetch_method_with_arguments(self, *_, **__): - async_call(self._connection.fetch("SELECT $1;", "1")) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - self.assertEqual( - spans[0].attributes, - { - "db.type": "sql", - "db.user": POSTGRES_USER, - "db.statement.parameters": "('1',)", - "db.instance": POSTGRES_DB_NAME, - "db.statement": "SELECT $1;", - }, - ) - - def test_instrumented_executemany_method_with_arguments(self, *_, **__): - async_call(self._connection.executemany("SELECT $1;", [["1"], ["2"]])) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - self.assertEqual( - { - "db.type": "sql", - "db.statement": "SELECT $1;", - "db.statement.parameters": "([['1'], ['2']],)", - "db.user": POSTGRES_USER, - "db.instance": POSTGRES_DB_NAME, - }, - spans[0].attributes, - ) - - def test_instrumented_execute_interface_error_method(self, *_, **__): - with self.assertRaises(asyncpg.InterfaceError): - async_call(self._connection.execute("SELECT 42;", 1, 2, 3)) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - self.assertEqual( - spans[0].attributes, - { - "db.type": "sql", - "db.instance": POSTGRES_DB_NAME, - "db.user": POSTGRES_USER, - "db.statement.parameters": "(1, 2, 3)", - "db.statement": "SELECT 42;", - }, - ) diff --git a/tests/opentelemetry-docker-tests/tests/celery/conftest.py b/tests/opentelemetry-docker-tests/tests/celery/conftest.py deleted file mode 100644 index 085fe3bab1..0000000000 --- a/tests/opentelemetry-docker-tests/tests/celery/conftest.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -from functools import wraps - -import pytest - -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.celery import CeleryInstrumentor -from opentelemetry.sdk.trace import TracerProvider, export -from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, -) - -REDIS_HOST = os.getenv("REDIS_HOST", "localhost") -REDIS_PORT = int(os.getenv("REDIS_PORT ", "6379")) -REDIS_URL = "redis://{host}:{port}".format(host=REDIS_HOST, port=REDIS_PORT) -BROKER_URL = "{redis}/{db}".format(redis=REDIS_URL, db=0) -BACKEND_URL = "{redis}/{db}".format(redis=REDIS_URL, db=1) - - -@pytest.fixture(scope="session") -def celery_config(): - return {"broker_url": BROKER_URL, "result_backend": BACKEND_URL} - - -@pytest.fixture -def celery_worker_parameters(): - return { - # See https://github.com/celery/celery/issues/3642#issuecomment-457773294 - "perform_ping_check": False, - } - - -@pytest.fixture(autouse=True) -def patch_celery_app(celery_app, celery_worker): - """Patch task decorator on app fixture to reload worker""" - # See https://github.com/celery/celery/issues/3642 - def wrap_task(fn): - @wraps(fn) - def wrapper(*args, **kwargs): - result = fn(*args, **kwargs) - celery_worker.reload() - return result - - return wrapper - - celery_app.task = wrap_task(celery_app.task) - - -@pytest.fixture(autouse=True) -def instrument(tracer_provider, memory_exporter): - CeleryInstrumentor().instrument(tracer_provider=tracer_provider) - memory_exporter.clear() - - yield - - CeleryInstrumentor().uninstrument() - - -@pytest.fixture(scope="session") -def tracer_provider(memory_exporter): - original_tracer_provider = trace_api.get_tracer_provider() - - tracer_provider = TracerProvider() - - span_processor = export.SimpleExportSpanProcessor(memory_exporter) - tracer_provider.add_span_processor(span_processor) - - trace_api.set_tracer_provider(tracer_provider) - - yield tracer_provider - - trace_api.set_tracer_provider(original_tracer_provider) - - -@pytest.fixture(scope="session") -def memory_exporter(): - memory_exporter = InMemorySpanExporter() - return memory_exporter diff --git a/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py b/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py deleted file mode 100644 index 7786890a5f..0000000000 --- a/tests/opentelemetry-docker-tests/tests/celery/test_celery_functional.py +++ /dev/null @@ -1,537 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import celery -import pytest -from celery.exceptions import Retry - -import opentelemetry.instrumentation.celery -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.celery import CeleryInstrumentor -from opentelemetry.sdk import resources -from opentelemetry.sdk.trace import TracerProvider, export -from opentelemetry.trace.status import StatusCode - -# set a high timeout for async executions due to issues in CI -ASYNC_GET_TIMEOUT = 120 - - -class MyException(Exception): - pass - - -@pytest.mark.skip(reason="inconsistent test results") -def test_instrumentation_info(celery_app, memory_exporter): - @celery_app.task - def fn_task(): - return 42 - - result = fn_task.apply_async() - assert result.get(timeout=ASYNC_GET_TIMEOUT) == 42 - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 2 - - async_span, run_span = spans - - assert run_span.parent == async_span.context - assert run_span.parent.span_id == async_span.context.span_id - assert run_span.context.trace_id == async_span.context.trace_id - - assert async_span.instrumentation_info.name == "apply_async/{0}".format( - opentelemetry.instrumentation.celery.__name__ - ) - assert async_span.instrumentation_info.version == "apply_async/{0}".format( - opentelemetry.instrumentation.celery.__version__ - ) - assert run_span.instrumentation_info.name == "run/{0}".format( - opentelemetry.instrumentation.celery.__name__ - ) - assert run_span.instrumentation_info.version == "run/{0}".format( - opentelemetry.instrumentation.celery.__version__ - ) - - -def test_fn_task_run(celery_app, memory_exporter): - @celery_app.task - def fn_task(): - return 42 - - t = fn_task.run() - assert t == 42 - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 0 - - -def test_fn_task_call(celery_app, memory_exporter): - @celery_app.task - def fn_task(): - return 42 - - t = fn_task() - assert t == 42 - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 0 - - -def test_fn_task_apply(celery_app, memory_exporter): - @celery_app.task - def fn_task(): - return 42 - - t = fn_task.apply() - assert t.successful() is True - assert t.result == 42 - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 1 - - span = spans[0] - - assert span.status.is_ok is True - assert span.name == "run/test_celery_functional.fn_task" - assert span.attributes.get("messaging.message_id") == t.task_id - assert ( - span.attributes.get("celery.task_name") - == "test_celery_functional.fn_task" - ) - assert span.attributes.get("celery.action") == "run" - assert span.attributes.get("celery.state") == "SUCCESS" - - -def test_fn_task_apply_bind(celery_app, memory_exporter): - @celery_app.task(bind=True) - def fn_task(self): - return self - - t = fn_task.apply() - assert t.successful() is True - assert "fn_task" in t.result.name - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 1 - - span = spans[0] - - assert span.status.is_ok is True - assert span.name == "run/test_celery_functional.fn_task" - assert span.attributes.get("messaging.message_id") == t.task_id - assert ( - span.attributes.get("celery.task_name") - == "test_celery_functional.fn_task" - ) - assert span.attributes.get("celery.action") == "run" - assert span.attributes.get("celery.state") == "SUCCESS" - - -@pytest.mark.skip(reason="inconsistent test results") -def test_fn_task_apply_async(celery_app, memory_exporter): - @celery_app.task - def fn_task_parameters(user, force_logout=False): - return (user, force_logout) - - result = fn_task_parameters.apply_async( - args=["user"], kwargs={"force_logout": True} - ) - assert result.get(timeout=ASYNC_GET_TIMEOUT) == ["user", True] - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 2 - - async_span, run_span = spans - - assert run_span.context.trace_id != async_span.context.trace_id - - assert async_span.status.is_ok is True - assert ( - async_span.name - == "apply_async/test_celery_functional.fn_task_parameters" - ) - assert async_span.attributes.get("celery.action") == "apply_async" - assert async_span.attributes.get("messaging.message_id") == result.task_id - assert ( - async_span.attributes.get("celery.task_name") - == "test_celery_functional.fn_task_parameters" - ) - - assert run_span.status.is_ok is True - assert run_span.name == "test_celery_functional.fn_task_parameters" - assert run_span.attributes.get("celery.action") == "run" - assert run_span.attributes.get("celery.state") == "SUCCESS" - assert run_span.attributes.get("messaging.message_id") == result.task_id - assert ( - run_span.attributes.get("celery.task_name") - == "test_celery_functional.fn_task_parameters" - ) - - -@pytest.mark.skip(reason="inconsistent test results") -def test_concurrent_delays(celery_app, memory_exporter): - @celery_app.task - def fn_task(): - return 42 - - results = [fn_task.delay() for _ in range(100)] - - for result in results: - assert result.get(timeout=ASYNC_GET_TIMEOUT) == 42 - - spans = memory_exporter.get_finished_spans() - - assert len(spans) == 200 - - -@pytest.mark.skip(reason="inconsistent test results") -def test_fn_task_delay(celery_app, memory_exporter): - @celery_app.task - def fn_task_parameters(user, force_logout=False): - return (user, force_logout) - - result = fn_task_parameters.delay("user", force_logout=True) - assert result.get(timeout=ASYNC_GET_TIMEOUT) == ["user", True] - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 2 - - async_span, run_span = spans - - assert run_span.context.trace_id != async_span.context.trace_id - - assert async_span.status.is_ok is True - assert ( - async_span.name - == "apply_async/test_celery_functional.fn_task_parameters" - ) - assert async_span.attributes.get("celery.action") == "apply_async" - assert async_span.attributes.get("messaging.message_id") == result.task_id - assert ( - async_span.attributes.get("celery.task_name") - == "test_celery_functional.fn_task_parameters" - ) - - assert run_span.status.is_ok is True - assert run_span.name == "run/test_celery_functional.fn_task_parameters" - assert run_span.attributes.get("celery.action") == "run" - assert run_span.attributes.get("celery.state") == "SUCCESS" - assert run_span.attributes.get("messaging.message_id") == result.task_id - assert ( - run_span.attributes.get("celery.task_name") - == "test_celery_functional.fn_task_parameters" - ) - - -def test_fn_exception(celery_app, memory_exporter): - @celery_app.task - def fn_exception(): - raise Exception("Task class is failing") - - result = fn_exception.apply() - - assert result.failed() is True - assert "Task class is failing" in result.traceback - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 1 - - span = spans[0] - - assert span.status.is_ok is False - assert span.name == "run/test_celery_functional.fn_exception" - assert span.attributes.get("celery.action") == "run" - assert span.attributes.get("celery.state") == "FAILURE" - assert ( - span.attributes.get("celery.task_name") - == "test_celery_functional.fn_exception" - ) - assert span.status.status_code == StatusCode.ERROR - assert span.attributes.get("messaging.message_id") == result.task_id - assert "Task class is failing" in span.status.description - - -def test_fn_exception_expected(celery_app, memory_exporter): - @celery_app.task(throws=(MyException,)) - def fn_exception(): - raise MyException("Task class is failing") - - result = fn_exception.apply() - - assert result.failed() is True - assert "Task class is failing" in result.traceback - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 1 - - span = spans[0] - - assert span.status.is_ok is True - assert span.status.status_code == StatusCode.UNSET - assert span.name == "run/test_celery_functional.fn_exception" - assert span.attributes.get("celery.action") == "run" - assert span.attributes.get("celery.state") == "FAILURE" - assert ( - span.attributes.get("celery.task_name") - == "test_celery_functional.fn_exception" - ) - assert span.attributes.get("messaging.message_id") == result.task_id - - -def test_fn_retry_exception(celery_app, memory_exporter): - @celery_app.task - def fn_exception(): - raise Retry("Task class is being retried") - - result = fn_exception.apply() - - assert result.failed() is False - assert "Task class is being retried" in result.traceback - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 1 - - span = spans[0] - - assert span.status.is_ok is True - assert span.status.status_code == StatusCode.UNSET - assert span.name == "run/test_celery_functional.fn_exception" - assert span.attributes.get("celery.action") == "run" - assert span.attributes.get("celery.state") == "RETRY" - assert ( - span.attributes.get("celery.task_name") - == "test_celery_functional.fn_exception" - ) - assert span.attributes.get("messaging.message_id") == result.task_id - - -def test_class_task(celery_app, memory_exporter): - class BaseTask(celery_app.Task): - def run(self): - return 42 - - task = BaseTask() - # register the Task class if it's available (required in Celery 4.0+) - register_task = getattr(celery_app, "register_task", None) - if register_task is not None: - register_task(task) - - result = task.apply() - - assert result.successful() is True - assert result.result == 42 - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 1 - - span = spans[0] - - assert span.status.is_ok is True - assert span.name == "run/test_celery_functional.BaseTask" - assert ( - span.attributes.get("celery.task_name") - == "test_celery_functional.BaseTask" - ) - assert span.attributes.get("celery.action") == "run" - assert span.attributes.get("celery.state") == "SUCCESS" - assert span.attributes.get("messaging.message_id") == result.task_id - - -def test_class_task_exception(celery_app, memory_exporter): - class BaseTask(celery_app.Task): - def run(self): - raise Exception("Task class is failing") - - task = BaseTask() - # register the Task class if it's available (required in Celery 4.0+) - register_task = getattr(celery_app, "register_task", None) - if register_task is not None: - register_task(task) - - result = task.apply() - - assert result.failed() is True - assert "Task class is failing" in result.traceback - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 1 - - span = spans[0] - - assert span.status.is_ok is False - assert span.name == "run/test_celery_functional.BaseTask" - assert ( - span.attributes.get("celery.task_name") - == "test_celery_functional.BaseTask" - ) - assert span.attributes.get("celery.action") == "run" - assert span.attributes.get("celery.state") == "FAILURE" - assert span.status.status_code == StatusCode.ERROR - assert span.attributes.get("messaging.message_id") == result.task_id - assert "Task class is failing" in span.status.description - - -def test_class_task_exception_excepted(celery_app, memory_exporter): - class BaseTask(celery_app.Task): - throws = (MyException,) - - def run(self): - raise MyException("Task class is failing") - - task = BaseTask() - # register the Task class if it's available (required in Celery 4.0+) - register_task = getattr(celery_app, "register_task", None) - if register_task is not None: - register_task(task) - - result = task.apply() - - assert result.failed() is True - assert "Task class is failing" in result.traceback - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 1 - - span = spans[0] - - assert span.status.is_ok is True - assert span.status.status_code == StatusCode.UNSET - assert span.name == "run/test_celery_functional.BaseTask" - assert span.attributes.get("celery.action") == "run" - assert span.attributes.get("celery.state") == "FAILURE" - assert span.attributes.get("messaging.message_id") == result.task_id - - -def test_shared_task(celery_app, memory_exporter): - """Ensure Django Shared Task are supported""" - - @celery.shared_task - def add(x, y): - return x + y - - result = add.apply([2, 2]) - assert result.result == 4 - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 1 - - span = spans[0] - - assert span.status.is_ok is True - assert span.name == "run/test_celery_functional.add" - assert ( - span.attributes.get("celery.task_name") == "test_celery_functional.add" - ) - assert span.attributes.get("celery.action") == "run" - assert span.attributes.get("celery.state") == "SUCCESS" - assert span.attributes.get("messaging.message_id") == result.task_id - - -@pytest.mark.skip(reason="inconsistent test results") -def test_apply_async_previous_style_tasks( - celery_app, celery_worker, memory_exporter -): - """Ensures apply_async is properly patched if Celery 1.0 style tasks are - used even in newer versions. This should extend support to previous versions - of Celery.""" - - class CelerySuperClass(celery.task.Task): - abstract = True - - @classmethod - def apply_async(cls, args=None, kwargs=None, **kwargs_): - return super(CelerySuperClass, cls).apply_async( - args=args, kwargs=kwargs, **kwargs_ - ) - - def run(self, *args, **kwargs): - if "stop" in kwargs: - # avoid call loop - return - CelerySubClass.apply_async(args=[], kwargs={"stop": True}).get( - timeout=ASYNC_GET_TIMEOUT - ) - - class CelerySubClass(CelerySuperClass): - pass - - celery_worker.reload() - - task = CelerySubClass() - result = task.apply() - - spans = memory_exporter.get_finished_spans() - assert len(spans) == 3 - - async_span, async_run_span, run_span = spans - - assert run_span.status.is_ok is True - assert run_span.name == "run/test_celery_functional.CelerySubClass" - assert ( - run_span.attributes.get("celery.task_name") - == "test_celery_functional.CelerySubClass" - ) - assert run_span.attributes.get("celery.action") == "run" - assert run_span.attributes.get("celery.state") == "SUCCESS" - assert run_span.attributes.get("messaging.message_id") == result.task_id - - assert async_run_span.status.is_ok is True - assert async_run_span.name == "run/test_celery_functional.CelerySubClass" - assert ( - async_run_span.attributes.get("celery.task_name") - == "test_celery_functional.CelerySubClass" - ) - assert async_run_span.attributes.get("celery.action") == "run" - assert async_run_span.attributes.get("celery.state") == "SUCCESS" - assert ( - async_run_span.attributes.get("messaging.message_id") != result.task_id - ) - - assert async_span.status.is_ok is True - assert ( - async_span.name == "apply_async/test_celery_functional.CelerySubClass" - ) - assert ( - async_span.attributes.get("celery.task_name") - == "test_celery_functional.CelerySubClass" - ) - assert async_span.attributes.get("celery.action") == "apply_async" - assert async_span.attributes.get("messaging.message_id") != result.task_id - assert async_span.attributes.get( - "messaging.message_id" - ) == async_run_span.attributes.get("messaging.message_id") - - -def test_custom_tracer_provider(celery_app, memory_exporter): - @celery_app.task - def fn_task(): - return 42 - - resource = resources.Resource.create({}) - tracer_provider = TracerProvider(resource=resource) - span_processor = export.SimpleExportSpanProcessor(memory_exporter) - tracer_provider.add_span_processor(span_processor) - - trace_api.set_tracer_provider(tracer_provider) - - CeleryInstrumentor().uninstrument() - CeleryInstrumentor().instrument(tracer_provider=tracer_provider) - - fn_task.delay() - - spans_list = memory_exporter.get_finished_spans() - assert len(spans_list) == 1 - - span = spans_list[0] - assert span.resource == resource diff --git a/tests/opentelemetry-docker-tests/tests/check_availability.py b/tests/opentelemetry-docker-tests/tests/check_availability.py deleted file mode 100644 index 3082572193..0000000000 --- a/tests/opentelemetry-docker-tests/tests/check_availability.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import logging -import os -import time - -import mysql.connector -import psycopg2 -import pymongo -import redis - -MONGODB_COLLECTION_NAME = "test" -MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME", "opentelemetry-tests") -MONGODB_HOST = os.getenv("MONGODB_HOST", "localhost") -MONGODB_PORT = int(os.getenv("MONGODB_PORT", "27017")) -MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME ", "opentelemetry-tests") -MYSQL_HOST = os.getenv("MYSQL_HOST ", "localhost") -MYSQL_PORT = int(os.getenv("MYSQL_PORT ", "3306")) -MYSQL_USER = os.getenv("MYSQL_USER ", "testuser") -MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD ", "testpassword") -POSTGRES_DB_NAME = os.getenv("POSTGRESQL_DB_NAME", "opentelemetry-tests") -POSTGRES_HOST = os.getenv("POSTGRESQL_HOST", "localhost") -POSTGRES_PASSWORD = os.getenv("POSTGRESQL_HOST", "testpassword") -POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT", "5432")) -POSTGRES_USER = os.getenv("POSTGRESQL_HOST", "testuser") -REDIS_HOST = os.getenv("REDIS_HOST", "localhost") -REDIS_PORT = int(os.getenv("REDIS_PORT ", "6379")) -RETRY_COUNT = 8 -RETRY_INTERVAL = 5 # Seconds - -logger = logging.getLogger(__name__) - - -def retryable(func): - def wrapper(): - # Try to connect to DB - for i in range(RETRY_COUNT): - try: - func() - return - except Exception as ex: # pylint: disable=broad-except - logger.error( - "waiting for %s, retry %d/%d [%s]", - func.__name__, - i + 1, - RETRY_COUNT, - ex, - ) - time.sleep(RETRY_INTERVAL) - raise Exception("waiting for {} failed".format(func.__name__)) - - return wrapper - - -@retryable -def check_pymongo_connection(): - client = pymongo.MongoClient( - MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000 - ) - db = client[MONGODB_DB_NAME] - collection = db[MONGODB_COLLECTION_NAME] - collection.find_one() - client.close() - - -@retryable -def check_mysql_connection(): - connection = mysql.connector.connect( - user=MYSQL_USER, - password=MYSQL_PASSWORD, - host=MYSQL_HOST, - port=MYSQL_PORT, - database=MYSQL_DB_NAME, - ) - connection.close() - - -@retryable -def check_postgres_connection(): - connection = psycopg2.connect( - dbname=POSTGRES_DB_NAME, - user=POSTGRES_USER, - password=POSTGRES_PASSWORD, - host=POSTGRES_HOST, - port=POSTGRES_PORT, - ) - connection.close() - - -@retryable -def check_redis_connection(): - connection = redis.Redis(host=REDIS_HOST, port=REDIS_PORT) - connection.hgetall("*") - - -def check_docker_services_availability(): - # Check if Docker services accept connections - check_pymongo_connection() - check_mysql_connection() - check_postgres_connection() - check_redis_connection() - - -check_docker_services_availability() diff --git a/tests/opentelemetry-docker-tests/tests/docker-compose.yml b/tests/opentelemetry-docker-tests/tests/docker-compose.yml index bbb005a02e..c731f83da1 100644 --- a/tests/opentelemetry-docker-tests/tests/docker-compose.yml +++ b/tests/opentelemetry-docker-tests/tests/docker-compose.yml @@ -1,44 +1,6 @@ version: '3' services: - otmongo: - ports: - - "27017:27017" - image: mongo:latest - otmysql: - ports: - - "3306:3306" - image: mysql:latest - restart: always - environment: - MYSQL_USER: testuser - MYSQL_PASSWORD: testpassword - MYSQL_ALLOW_EMPTY_PASSWORD: "yes" - MYSQL_DATABASE: opentelemetry-tests - otpostgres: - image: postgres - ports: - - "5432:5432" - environment: - POSTGRES_USER: testuser - POSTGRES_PASSWORD: testpassword - POSTGRES_DB: opentelemetry-tests - otredis: - image: redis:4.0-alpine - ports: - - "127.0.0.1:6379:6379" - otjaeger: - image: jaegertracing/all-in-one:1.8 - environment: - COLLECTOR_ZIPKIN_HTTP_PORT: "9411" - ports: - - "5775:5775/udp" - - "6831:6831/udp" - - "6832:6832/udp" - - "5778:5778" - - "16686:16686" - - "14268:14268" - - "9411:9411" otopencensus: image: omnition/opencensus-collector:0.1.11 command: --logging-exporter DEBUG diff --git a/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py b/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py deleted file mode 100644 index fc237fe12b..0000000000 --- a/tests/opentelemetry-docker-tests/tests/mysql/test_mysql_functional.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import mysql.connector - -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.mysql import MySQLInstrumentor -from opentelemetry.test.test_base import TestBase - -MYSQL_USER = os.getenv("MYSQL_USER ", "testuser") -MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD ", "testpassword") -MYSQL_HOST = os.getenv("MYSQL_HOST ", "localhost") -MYSQL_PORT = int(os.getenv("MYSQL_PORT ", "3306")) -MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME ", "opentelemetry-tests") - - -class TestFunctionalMysql(TestBase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls._connection = None - cls._cursor = None - cls._tracer = cls.tracer_provider.get_tracer(__name__) - MySQLInstrumentor().instrument() - - @classmethod - def tearDownClass(cls): - if cls._connection: - cls._connection.close() - MySQLInstrumentor().uninstrument() - - def setUp(self): - super().setUp() - self._connection = mysql.connector.connect( - user=MYSQL_USER, - password=MYSQL_PASSWORD, - host=MYSQL_HOST, - port=MYSQL_PORT, - database=MYSQL_DB_NAME, - ) - self._cursor = self._connection.cursor() - - def validate_spans(self): - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 2) - for span in spans: - if span.name == "rootSpan": - root_span = span - else: - db_span = span - self.assertIsInstance(span.start_time, int) - self.assertIsInstance(span.end_time, int) - self.assertIsNotNone(root_span) - self.assertIsNotNone(db_span) - self.assertEqual(root_span.name, "rootSpan") - self.assertEqual(db_span.name, "mysql.opentelemetry-tests") - self.assertIsNotNone(db_span.parent) - self.assertIs(db_span.parent, root_span.get_span_context()) - self.assertIs(db_span.kind, trace_api.SpanKind.CLIENT) - self.assertEqual(db_span.attributes["db.instance"], MYSQL_DB_NAME) - self.assertEqual(db_span.attributes["net.peer.name"], MYSQL_HOST) - self.assertEqual(db_span.attributes["net.peer.port"], MYSQL_PORT) - - def test_execute(self): - """Should create a child span for execute""" - with self._tracer.start_as_current_span("rootSpan"): - self._cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") - self.validate_spans() - - def test_execute_with_connection_context_manager(self): - """Should create a child span for execute with connection context""" - with self._tracer.start_as_current_span("rootSpan"): - with self._connection as conn: - cursor = conn.cursor() - cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") - self.validate_spans() - - def test_execute_with_cursor_context_manager(self): - """Should create a child span for execute with cursor context""" - with self._tracer.start_as_current_span("rootSpan"): - with self._connection.cursor() as cursor: - cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") - self.validate_spans() - - def test_executemany(self): - """Should create a child span for executemany""" - with self._tracer.start_as_current_span("rootSpan"): - data = (("1",), ("2",), ("3",)) - stmt = "INSERT INTO test (id) VALUES (%s)" - self._cursor.executemany(stmt, data) - self.validate_spans() - - def test_callproc(self): - """Should create a child span for callproc""" - with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( - Exception - ): - self._cursor.callproc("test", ()) - self.validate_spans() diff --git a/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py b/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py deleted file mode 100644 index d76cd702ee..0000000000 --- a/tests/opentelemetry-docker-tests/tests/postgres/test_aiopg_functional.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright 2020, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import asyncio -import os - -import aiopg -import psycopg2 -import pytest - -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.aiopg import AiopgInstrumentor -from opentelemetry.test.test_base import TestBase - -POSTGRES_HOST = os.getenv("POSTGRESQL_HOST ", "localhost") -POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT ", "5432")) -POSTGRES_DB_NAME = os.getenv("POSTGRESQL_DB_NAME ", "opentelemetry-tests") -POSTGRES_PASSWORD = os.getenv("POSTGRESQL_HOST ", "testpassword") -POSTGRES_USER = os.getenv("POSTGRESQL_HOST ", "testuser") - - -def async_call(coro): - loop = asyncio.get_event_loop() - return loop.run_until_complete(coro) - - -class TestFunctionalAiopgConnect(TestBase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls._connection = None - cls._cursor = None - cls._tracer = cls.tracer_provider.get_tracer(__name__) - AiopgInstrumentor().instrument(tracer_provider=cls.tracer_provider) - cls._connection = async_call( - aiopg.connect( - dbname=POSTGRES_DB_NAME, - user=POSTGRES_USER, - password=POSTGRES_PASSWORD, - host=POSTGRES_HOST, - port=POSTGRES_PORT, - ) - ) - cls._cursor = async_call(cls._connection.cursor()) - - @classmethod - def tearDownClass(cls): - if cls._cursor: - cls._cursor.close() - if cls._connection: - cls._connection.close() - AiopgInstrumentor().uninstrument() - - def validate_spans(self): - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 2) - for span in spans: - if span.name == "rootSpan": - root_span = span - else: - child_span = span - self.assertIsInstance(span.start_time, int) - self.assertIsInstance(span.end_time, int) - self.assertIsNotNone(root_span) - self.assertIsNotNone(child_span) - self.assertEqual(root_span.name, "rootSpan") - self.assertEqual(child_span.name, "postgresql.opentelemetry-tests") - self.assertIsNotNone(child_span.parent) - self.assertIs(child_span.parent, root_span.get_span_context()) - self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) - self.assertEqual( - child_span.attributes["db.instance"], POSTGRES_DB_NAME - ) - self.assertEqual(child_span.attributes["net.peer.name"], POSTGRES_HOST) - self.assertEqual(child_span.attributes["net.peer.port"], POSTGRES_PORT) - - def test_execute(self): - """Should create a child span for execute method""" - with self._tracer.start_as_current_span("rootSpan"): - async_call( - self._cursor.execute( - "CREATE TABLE IF NOT EXISTS test (id integer)" - ) - ) - self.validate_spans() - - def test_executemany(self): - """Should create a child span for executemany""" - with pytest.raises(psycopg2.ProgrammingError): - with self._tracer.start_as_current_span("rootSpan"): - data = (("1",), ("2",), ("3",)) - stmt = "INSERT INTO test (id) VALUES (%s)" - async_call(self._cursor.executemany(stmt, data)) - self.validate_spans() - - def test_callproc(self): - """Should create a child span for callproc""" - with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( - Exception - ): - async_call(self._cursor.callproc("test", ())) - self.validate_spans() - - -class TestFunctionalAiopgCreatePool(TestBase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls._connection = None - cls._cursor = None - cls._tracer = cls.tracer_provider.get_tracer(__name__) - AiopgInstrumentor().instrument(tracer_provider=cls.tracer_provider) - cls._pool = async_call( - aiopg.create_pool( - dbname=POSTGRES_DB_NAME, - user=POSTGRES_USER, - password=POSTGRES_PASSWORD, - host=POSTGRES_HOST, - port=POSTGRES_PORT, - ) - ) - cls._connection = async_call(cls._pool.acquire()) - cls._cursor = async_call(cls._connection.cursor()) - - @classmethod - def tearDownClass(cls): - if cls._cursor: - cls._cursor.close() - if cls._connection: - cls._connection.close() - if cls._pool: - cls._pool.close() - AiopgInstrumentor().uninstrument() - - def validate_spans(self): - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 2) - for span in spans: - if span.name == "rootSpan": - root_span = span - else: - child_span = span - self.assertIsInstance(span.start_time, int) - self.assertIsInstance(span.end_time, int) - self.assertIsNotNone(root_span) - self.assertIsNotNone(child_span) - self.assertEqual(root_span.name, "rootSpan") - self.assertEqual(child_span.name, "postgresql.opentelemetry-tests") - self.assertIsNotNone(child_span.parent) - self.assertIs(child_span.parent, root_span.get_span_context()) - self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) - self.assertEqual( - child_span.attributes["db.instance"], POSTGRES_DB_NAME - ) - self.assertEqual(child_span.attributes["net.peer.name"], POSTGRES_HOST) - self.assertEqual(child_span.attributes["net.peer.port"], POSTGRES_PORT) - - def test_execute(self): - """Should create a child span for execute method""" - with self._tracer.start_as_current_span("rootSpan"): - async_call( - self._cursor.execute( - "CREATE TABLE IF NOT EXISTS test (id integer)" - ) - ) - self.validate_spans() - - def test_executemany(self): - """Should create a child span for executemany""" - with pytest.raises(psycopg2.ProgrammingError): - with self._tracer.start_as_current_span("rootSpan"): - data = (("1",), ("2",), ("3",)) - stmt = "INSERT INTO test (id) VALUES (%s)" - async_call(self._cursor.executemany(stmt, data)) - self.validate_spans() - - def test_callproc(self): - """Should create a child span for callproc""" - with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( - Exception - ): - async_call(self._cursor.callproc("test", ())) - self.validate_spans() diff --git a/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py b/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py deleted file mode 100644 index 28db4c064f..0000000000 --- a/tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_functional.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright 2020, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import psycopg2 - -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor -from opentelemetry.test.test_base import TestBase - -POSTGRES_HOST = os.getenv("POSTGRESQL_HOST ", "localhost") -POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT ", "5432")) -POSTGRES_DB_NAME = os.getenv("POSTGRESQL_DB_NAME ", "opentelemetry-tests") -POSTGRES_PASSWORD = os.getenv("POSTGRESQL_HOST ", "testpassword") -POSTGRES_USER = os.getenv("POSTGRESQL_HOST ", "testuser") - - -class TestFunctionalPsycopg(TestBase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls._connection = None - cls._cursor = None - cls._tracer = cls.tracer_provider.get_tracer(__name__) - Psycopg2Instrumentor().instrument(tracer_provider=cls.tracer_provider) - cls._connection = psycopg2.connect( - dbname=POSTGRES_DB_NAME, - user=POSTGRES_USER, - password=POSTGRES_PASSWORD, - host=POSTGRES_HOST, - port=POSTGRES_PORT, - ) - cls._connection.set_session(autocommit=True) - cls._cursor = cls._connection.cursor() - - @classmethod - def tearDownClass(cls): - if cls._cursor: - cls._cursor.close() - if cls._connection: - cls._connection.close() - Psycopg2Instrumentor().uninstrument() - - def validate_spans(self): - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 2) - for span in spans: - if span.name == "rootSpan": - root_span = span - else: - child_span = span - self.assertIsInstance(span.start_time, int) - self.assertIsInstance(span.end_time, int) - self.assertIsNotNone(root_span) - self.assertIsNotNone(child_span) - self.assertEqual(root_span.name, "rootSpan") - self.assertEqual(child_span.name, "postgresql.opentelemetry-tests") - self.assertIsNotNone(child_span.parent) - self.assertIs(child_span.parent, root_span.get_span_context()) - self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) - self.assertEqual( - child_span.attributes["db.instance"], POSTGRES_DB_NAME - ) - self.assertEqual(child_span.attributes["net.peer.name"], POSTGRES_HOST) - self.assertEqual(child_span.attributes["net.peer.port"], POSTGRES_PORT) - - def test_execute(self): - """Should create a child span for execute method""" - with self._tracer.start_as_current_span("rootSpan"): - self._cursor.execute( - "CREATE TABLE IF NOT EXISTS test (id integer)" - ) - self.validate_spans() - - def test_execute_with_connection_context_manager(self): - """Should create a child span for execute with connection context""" - with self._tracer.start_as_current_span("rootSpan"): - with self._connection as conn: - cursor = conn.cursor() - cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") - self.validate_spans() - - def test_execute_with_cursor_context_manager(self): - """Should create a child span for execute with cursor context""" - with self._tracer.start_as_current_span("rootSpan"): - with self._connection.cursor() as cursor: - cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") - self.validate_spans() - self.assertTrue(cursor.closed) - - def test_executemany(self): - """Should create a child span for executemany""" - with self._tracer.start_as_current_span("rootSpan"): - data = (("1",), ("2",), ("3",)) - stmt = "INSERT INTO test (id) VALUES (%s)" - self._cursor.executemany(stmt, data) - self.validate_spans() - - def test_callproc(self): - """Should create a child span for callproc""" - with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( - Exception - ): - self._cursor.callproc("test", ()) - self.validate_spans() diff --git a/tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py b/tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py deleted file mode 100644 index 577477a2ab..0000000000 --- a/tests/opentelemetry-docker-tests/tests/pymongo/test_pymongo_functional.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from pymongo import MongoClient - -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.pymongo import PymongoInstrumentor -from opentelemetry.test.test_base import TestBase - -MONGODB_HOST = os.getenv("MONGODB_HOST ", "localhost") -MONGODB_PORT = int(os.getenv("MONGODB_PORT ", "27017")) -MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME ", "opentelemetry-tests") -MONGODB_COLLECTION_NAME = "test" - - -class TestFunctionalPymongo(TestBase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls._tracer = cls.tracer_provider.get_tracer(__name__) - PymongoInstrumentor().instrument() - client = MongoClient( - MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000 - ) - db = client[MONGODB_DB_NAME] - cls._collection = db[MONGODB_COLLECTION_NAME] - - def validate_spans(self): - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 2) - for span in spans: - if span.name == "rootSpan": - root_span = span - else: - pymongo_span = span - self.assertIsInstance(span.start_time, int) - self.assertIsInstance(span.end_time, int) - self.assertIsNot(root_span, None) - self.assertIsNot(pymongo_span, None) - self.assertIsNotNone(pymongo_span.parent) - self.assertIs(pymongo_span.parent, root_span.get_span_context()) - self.assertIs(pymongo_span.kind, trace_api.SpanKind.CLIENT) - self.assertEqual( - pymongo_span.attributes["db.instance"], MONGODB_DB_NAME - ) - self.assertEqual( - pymongo_span.attributes["net.peer.name"], MONGODB_HOST - ) - self.assertEqual( - pymongo_span.attributes["net.peer.port"], MONGODB_PORT - ) - - def test_insert(self): - """Should create a child span for insert""" - with self._tracer.start_as_current_span("rootSpan"): - self._collection.insert_one( - {"name": "testName", "value": "testValue"} - ) - self.validate_spans() - - def test_update(self): - """Should create a child span for update""" - with self._tracer.start_as_current_span("rootSpan"): - self._collection.update_one( - {"name": "testName"}, {"$set": {"value": "someOtherValue"}} - ) - self.validate_spans() - - def test_find(self): - """Should create a child span for find""" - with self._tracer.start_as_current_span("rootSpan"): - self._collection.find_one() - self.validate_spans() - - def test_delete(self): - """Should create a child span for delete""" - with self._tracer.start_as_current_span("rootSpan"): - self._collection.delete_one({"name": "testName"}) - self.validate_spans() - - def test_uninstrument(self): - # check that integration is working - self._collection.find_one() - spans = self.memory_exporter.get_finished_spans() - self.memory_exporter.clear() - self.assertEqual(len(spans), 1) - - # uninstrument and check not new spans are created - PymongoInstrumentor().uninstrument() - self._collection.find_one() - spans = self.memory_exporter.get_finished_spans() - self.memory_exporter.clear() - self.assertEqual(len(spans), 0) - - # re-enable and check that it works again - PymongoInstrumentor().instrument() - self._collection.find_one() - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) diff --git a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py deleted file mode 100644 index 7c09025551..0000000000 --- a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import pymysql as pymy - -from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor -from opentelemetry.test.test_base import TestBase - -MYSQL_USER = os.getenv("MYSQL_USER ", "testuser") -MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD ", "testpassword") -MYSQL_HOST = os.getenv("MYSQL_HOST ", "localhost") -MYSQL_PORT = int(os.getenv("MYSQL_PORT ", "3306")) -MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME ", "opentelemetry-tests") - - -class TestFunctionalPyMysql(TestBase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls._connection = None - cls._cursor = None - cls._tracer = cls.tracer_provider.get_tracer(__name__) - PyMySQLInstrumentor().instrument() - cls._connection = pymy.connect( - user=MYSQL_USER, - password=MYSQL_PASSWORD, - host=MYSQL_HOST, - port=MYSQL_PORT, - database=MYSQL_DB_NAME, - ) - cls._cursor = cls._connection.cursor() - - @classmethod - def tearDownClass(cls): - if cls._connection: - cls._connection.close() - PyMySQLInstrumentor().uninstrument() - - def validate_spans(self): - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 2) - for span in spans: - if span.name == "rootSpan": - root_span = span - else: - db_span = span - self.assertIsInstance(span.start_time, int) - self.assertIsInstance(span.end_time, int) - self.assertIsNotNone(root_span) - self.assertIsNotNone(db_span) - self.assertEqual(root_span.name, "rootSpan") - self.assertEqual(db_span.name, "mysql.opentelemetry-tests") - self.assertIsNotNone(db_span.parent) - self.assertIs(db_span.parent, root_span.get_span_context()) - self.assertIs(db_span.kind, trace_api.SpanKind.CLIENT) - self.assertEqual(db_span.attributes["db.instance"], MYSQL_DB_NAME) - self.assertEqual(db_span.attributes["net.peer.name"], MYSQL_HOST) - self.assertEqual(db_span.attributes["net.peer.port"], MYSQL_PORT) - - def test_execute(self): - """Should create a child span for execute""" - with self._tracer.start_as_current_span("rootSpan"): - self._cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") - self.validate_spans() - - def test_execute_with_cursor_context_manager(self): - """Should create a child span for execute with cursor context""" - with self._tracer.start_as_current_span("rootSpan"): - with self._connection.cursor() as cursor: - cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") - self.validate_spans() - - def test_executemany(self): - """Should create a child span for executemany""" - with self._tracer.start_as_current_span("rootSpan"): - data = (("1",), ("2",), ("3",)) - stmt = "INSERT INTO test (id) VALUES (%s)" - self._cursor.executemany(stmt, data) - self.validate_spans() - - def test_callproc(self): - """Should create a child span for callproc""" - with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( - Exception - ): - self._cursor.callproc("test", ()) - self.validate_spans() diff --git a/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py b/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py deleted file mode 100644 index 8bdc120105..0000000000 --- a/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import redis - -from opentelemetry import trace -from opentelemetry.instrumentation.redis import RedisInstrumentor -from opentelemetry.test.test_base import TestBase - - -class TestRedisInstrument(TestBase): - - test_service = "redis" - - def setUp(self): - super().setUp() - self.redis_client = redis.Redis(port=6379) - self.redis_client.flushall() - RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) - - def tearDown(self): - super().tearDown() - RedisInstrumentor().uninstrument() - - def _check_span(self, span): - self.assertEqual(span.attributes["service"], self.test_service) - self.assertEqual(span.name, "redis.command") - self.assertIs(span.status.status_code, trace.status.StatusCode.UNSET) - self.assertEqual(span.attributes.get("db.instance"), 0) - self.assertEqual( - span.attributes.get("db.url"), "redis://localhost:6379" - ) - - def test_long_command(self): - self.redis_client.mget(*range(1000)) - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self._check_span(span) - self.assertTrue( - span.attributes.get("db.statement").startswith("MGET 0 1 2 3") - ) - self.assertTrue(span.attributes.get("db.statement").endswith("...")) - - def test_basics(self): - self.assertIsNone(self.redis_client.get("cheese")) - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self._check_span(span) - self.assertEqual(span.attributes.get("db.statement"), "GET cheese") - self.assertEqual(span.attributes.get("redis.args_length"), 2) - - def test_pipeline_traced(self): - with self.redis_client.pipeline(transaction=False) as pipeline: - pipeline.set("blah", 32) - pipeline.rpush("foo", "éé") - pipeline.hgetall("xxx") - pipeline.execute() - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self._check_span(span) - self.assertEqual( - span.attributes.get("db.statement"), - "SET blah 32\nRPUSH foo éé\nHGETALL xxx", - ) - self.assertEqual(span.attributes.get("redis.pipeline_length"), 3) - - def test_pipeline_immediate(self): - with self.redis_client.pipeline() as pipeline: - pipeline.set("a", 1) - pipeline.immediate_execute_command("SET", "b", 2) - pipeline.execute() - - spans = self.memory_exporter.get_finished_spans() - # expecting two separate spans here, rather than a - # single span for the whole pipeline - self.assertEqual(len(spans), 2) - span = spans[0] - self._check_span(span) - self.assertEqual(span.attributes.get("db.statement"), "SET b 2") - - def test_parent(self): - """Ensure OpenTelemetry works with redis.""" - ot_tracer = trace.get_tracer("redis_svc") - - with ot_tracer.start_as_current_span("redis_get"): - self.assertIsNone(self.redis_client.get("cheese")) - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 2) - child_span, parent_span = spans[0], spans[1] - - # confirm the parenting - self.assertIsNone(parent_span.parent) - self.assertIs(child_span.parent, parent_span.get_span_context()) - - self.assertEqual(parent_span.name, "redis_get") - self.assertEqual(parent_span.instrumentation_info.name, "redis_svc") - - self.assertEqual( - child_span.attributes.get("service"), self.test_service - ) - self.assertEqual(child_span.name, "redis.command") diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/__init__.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py deleted file mode 100644 index 72137f83e8..0000000000 --- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import contextlib - -from sqlalchemy import Column, Integer, String, create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker - -from opentelemetry import trace -from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor -from opentelemetry.instrumentation.sqlalchemy.engine import _DB, _ROWS, _STMT -from opentelemetry.test.test_base import TestBase - -Base = declarative_base() - - -def _create_engine(engine_args): - # create a SQLAlchemy engine - config = dict(engine_args) - url = config.pop("url") - return create_engine(url, **config) - - -class Player(Base): - """Player entity used to test SQLAlchemy ORM""" - - __tablename__ = "players" - - id = Column(Integer, primary_key=True) - name = Column(String(20)) - - -class SQLAlchemyTestMixin(TestBase): - __test__ = False - - """SQLAlchemy test mixin that includes a complete set of tests - that must be executed for different engine. When a new test (or - a regression test) should be added to SQLAlchemy test suite, a new - entry must be appended here so that it will be executed for all - available and supported engines. If the test is specific to only - one engine, that test must be added to the specific `TestCase` - implementation. - - To support a new engine, create a new `TestCase` that inherits from - `SQLAlchemyTestMixin` and `TestCase`. Then you must define the following - static class variables: - * VENDOR: the database vendor name - * SQL_DB: the `db.type` tag that we expect (it's the name of the database available in the `.env` file) - * SERVICE: the service that we expect by default - * ENGINE_ARGS: all arguments required to create the engine - - To check specific tags in each test, you must implement the - `check_meta(self, span)` method. - """ - - VENDOR = None - SQL_DB = None - SERVICE = None - ENGINE_ARGS = None - - @contextlib.contextmanager - def connection(self): - # context manager that provides a connection - # to the underlying database - try: - conn = self.engine.connect() - yield conn - finally: - conn.close() - - def check_meta(self, span): - """function that can be implemented according to the - specific engine implementation - """ - - def setUp(self): - super().setUp() - # create an engine with the given arguments - self.engine = _create_engine(self.ENGINE_ARGS) - - # create the database / entities and prepare a session for the test - Base.metadata.drop_all(bind=self.engine) - Base.metadata.create_all(self.engine, checkfirst=False) - self.session = sessionmaker(bind=self.engine)() - # trace the engine - SQLAlchemyInstrumentor().instrument( - engine=self.engine, tracer_provider=self.tracer_provider - ) - self.memory_exporter.clear() - - def tearDown(self): - # pylint: disable=invalid-name - # clear the database and dispose the engine - self.session.close() - Base.metadata.drop_all(bind=self.engine) - self.engine.dispose() - SQLAlchemyInstrumentor().uninstrument() - super().tearDown() - - def _check_span(self, span): - self.assertEqual(span.name, "{}.query".format(self.VENDOR)) - self.assertEqual(span.attributes.get("service"), self.SERVICE) - self.assertEqual(span.attributes.get(_DB), self.SQL_DB) - self.assertIs(span.status.status_code, trace.status.StatusCode.UNSET) - self.assertGreater((span.end_time - span.start_time), 0) - - def test_orm_insert(self): - # ensures that the ORM session is traced - wayne = Player(id=1, name="wayne") - self.session.add(wayne) - self.session.commit() - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self._check_span(span) - self.assertIn("INSERT INTO players", span.attributes.get(_STMT)) - self.assertEqual(span.attributes.get(_ROWS), 1) - self.check_meta(span) - - def test_session_query(self): - # ensures that the Session queries are traced - out = list(self.session.query(Player).filter_by(name="wayne")) - self.assertEqual(len(out), 0) - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self._check_span(span) - self.assertIn( - "SELECT players.id AS players_id, players.name AS players_name \nFROM players \nWHERE players.name", - span.attributes.get(_STMT), - ) - self.check_meta(span) - - def test_engine_connect_execute(self): - # ensures that engine.connect() is properly traced - with self.connection() as conn: - rows = conn.execute("SELECT * FROM players").fetchall() - self.assertEqual(len(rows), 0) - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self._check_span(span) - self.assertEqual(span.attributes.get(_STMT), "SELECT * FROM players") - self.check_meta(span) - - def test_parent(self): - """Ensure that sqlalchemy works with opentelemetry.""" - tracer = self.tracer_provider.get_tracer("sqlalch_svc") - - with tracer.start_as_current_span("sqlalch_op"): - with self.connection() as conn: - rows = conn.execute("SELECT * FROM players").fetchall() - self.assertEqual(len(rows), 0) - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 2) - child_span, parent_span = spans - - # confirm the parenting - self.assertIsNone(parent_span.parent) - self.assertIs(child_span.parent, parent_span.get_span_context()) - - self.assertEqual(parent_span.name, "sqlalch_op") - self.assertEqual(parent_span.instrumentation_info.name, "sqlalch_svc") - - self.assertEqual(child_span.name, "{}.query".format(self.VENDOR)) - self.assertEqual(child_span.attributes.get("service"), self.SERVICE) diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_instrument.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_instrument.py deleted file mode 100644 index c408c63d94..0000000000 --- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_instrument.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import unittest - -import sqlalchemy - -from opentelemetry import trace -from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor -from opentelemetry.test.test_base import TestBase - -POSTGRES_CONFIG = { - "host": "127.0.0.1", - "port": int(os.getenv("TEST_POSTGRES_PORT", "5432")), - "user": os.getenv("TEST_POSTGRES_USER", "testuser"), - "password": os.getenv("TEST_POSTGRES_PASSWORD", "testpassword"), - "dbname": os.getenv("TEST_POSTGRES_DB", "opentelemetry-tests"), -} - - -class SQLAlchemyInstrumentTestCase(TestBase): - """TestCase that checks if the engine is properly traced - when the `instrument()` method is used. - """ - - def setUp(self): - # create a traced engine with the given arguments - SQLAlchemyInstrumentor().instrument() - dsn = ( - "postgresql://%(user)s:%(password)s@%(host)s:%(port)s/%(dbname)s" - % POSTGRES_CONFIG - ) - self.engine = sqlalchemy.create_engine(dsn) - - # prepare a connection - self.conn = self.engine.connect() - super().setUp() - - def tearDown(self): - # clear the database and dispose the engine - self.conn.close() - self.engine.dispose() - SQLAlchemyInstrumentor().uninstrument() - - def test_engine_traced(self): - # ensures that the engine is traced - rows = self.conn.execute("SELECT 1").fetchall() - self.assertEqual(len(rows), 1) - - traces = self.memory_exporter.get_finished_spans() - # trace composition - self.assertEqual(len(traces), 1) - span = traces[0] - # check subset of span fields - self.assertEqual(span.name, "postgres.query") - self.assertEqual(span.attributes.get("service"), "postgres") - self.assertIs(span.status.status_code, trace.status.StatusCode.UNSET) - self.assertGreater((span.end_time - span.start_time), 0) diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py deleted file mode 100644 index 310cd91f73..0000000000 --- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import unittest - -import pytest -from sqlalchemy.exc import ProgrammingError - -from opentelemetry import trace -from opentelemetry.instrumentation.sqlalchemy.engine import ( - _DB, - _HOST, - _PORT, - _ROWS, - _STMT, -) - -from .mixins import SQLAlchemyTestMixin - -MYSQL_CONFIG = { - "host": "127.0.0.1", - "port": int(os.getenv("TEST_MYSQL_PORT", "3306")), - "user": os.getenv("TEST_MYSQL_USER", "testuser"), - "password": os.getenv("TEST_MYSQL_PASSWORD", "testpassword"), - "database": os.getenv("TEST_MYSQL_DATABASE", "opentelemetry-tests"), -} - - -class MysqlConnectorTestCase(SQLAlchemyTestMixin): - """TestCase for mysql-connector engine""" - - __test__ = True - - VENDOR = "mysql" - SQL_DB = "opentelemetry-tests" - SERVICE = "mysql" - ENGINE_ARGS = { - "url": "mysql+mysqlconnector://%(user)s:%(password)s@%(host)s:%(port)s/%(database)s" - % MYSQL_CONFIG - } - - def check_meta(self, span): - # check database connection tags - self.assertEqual(span.attributes.get(_HOST), MYSQL_CONFIG["host"]) - self.assertEqual(span.attributes.get(_PORT), MYSQL_CONFIG["port"]) - - def test_engine_execute_errors(self): - # ensures that SQL errors are reported - with pytest.raises(ProgrammingError): - with self.connection() as conn: - conn.execute("SELECT * FROM a_wrong_table").fetchall() - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - # span fields - self.assertEqual(span.name, "{}.query".format(self.VENDOR)) - self.assertEqual(span.attributes.get("service"), self.SERVICE) - self.assertEqual( - span.attributes.get(_STMT), "SELECT * FROM a_wrong_table" - ) - self.assertEqual(span.attributes.get(_DB), self.SQL_DB) - self.assertIsNone(span.attributes.get(_ROWS)) - self.check_meta(span) - self.assertTrue(span.end_time - span.start_time > 0) - # check the error - self.assertIs( - span.status.status_code, trace.status.StatusCode.ERROR, - ) - self.assertIn("a_wrong_table", span.status.description) diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py deleted file mode 100644 index 91fb123c97..0000000000 --- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import unittest - -import psycopg2 -import pytest -from sqlalchemy.exc import ProgrammingError - -from opentelemetry import trace -from opentelemetry.instrumentation.sqlalchemy.engine import ( - _DB, - _HOST, - _PORT, - _ROWS, - _STMT, -) - -from .mixins import SQLAlchemyTestMixin - -POSTGRES_CONFIG = { - "host": "127.0.0.1", - "port": int(os.getenv("TEST_POSTGRES_PORT", "5432")), - "user": os.getenv("TEST_POSTGRES_USER", "testuser"), - "password": os.getenv("TEST_POSTGRES_PASSWORD", "testpassword"), - "dbname": os.getenv("TEST_POSTGRES_DB", "opentelemetry-tests"), -} - - -class PostgresTestCase(SQLAlchemyTestMixin): - """TestCase for Postgres Engine""" - - __test__ = True - - VENDOR = "postgres" - SQL_DB = "opentelemetry-tests" - SERVICE = "postgres" - ENGINE_ARGS = { - "url": "postgresql://%(user)s:%(password)s@%(host)s:%(port)s/%(dbname)s" - % POSTGRES_CONFIG - } - - def check_meta(self, span): - # check database connection tags - self.assertEqual(span.attributes.get(_HOST), POSTGRES_CONFIG["host"]) - self.assertEqual(span.attributes.get(_PORT), POSTGRES_CONFIG["port"]) - - def test_engine_execute_errors(self): - # ensures that SQL errors are reported - with pytest.raises(ProgrammingError): - with self.connection() as conn: - conn.execute("SELECT * FROM a_wrong_table").fetchall() - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - # span fields - self.assertEqual(span.name, "{}.query".format(self.VENDOR)) - self.assertEqual(span.attributes.get("service"), self.SERVICE) - self.assertEqual( - span.attributes.get(_STMT), "SELECT * FROM a_wrong_table" - ) - self.assertEqual(span.attributes.get(_DB), self.SQL_DB) - self.assertIsNone(span.attributes.get(_ROWS)) - self.check_meta(span) - self.assertTrue(span.end_time - span.start_time > 0) - # check the error - self.assertIs( - span.status.status_code, trace.status.StatusCode.ERROR, - ) - self.assertIn("a_wrong_table", span.status.description) - - -class PostgresCreatorTestCase(PostgresTestCase): - """TestCase for Postgres Engine that includes the same tests set - of `PostgresTestCase`, but it uses a specific `creator` function. - """ - - VENDOR = "postgres" - SQL_DB = "opentelemetry-tests" - SERVICE = "postgres" - ENGINE_ARGS = { - "url": "postgresql://", - "creator": lambda: psycopg2.connect(**POSTGRES_CONFIG), - } diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py deleted file mode 100644 index 309dd73ccc..0000000000 --- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -import pytest -from sqlalchemy.exc import OperationalError - -from opentelemetry import trace -from opentelemetry.instrumentation.sqlalchemy.engine import _DB, _ROWS, _STMT - -from .mixins import SQLAlchemyTestMixin - - -class SQLiteTestCase(SQLAlchemyTestMixin): - """TestCase for the SQLite engine""" - - __test__ = True - - VENDOR = "sqlite" - SQL_DB = ":memory:" - SERVICE = "sqlite" - ENGINE_ARGS = {"url": "sqlite:///:memory:"} - - def test_engine_execute_errors(self): - # ensures that SQL errors are reported - with pytest.raises(OperationalError): - with self.connection() as conn: - conn.execute("SELECT * FROM a_wrong_table").fetchall() - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - # span fields - self.assertEqual(span.name, "{}.query".format(self.VENDOR)) - self.assertEqual(span.attributes.get("service"), self.SERVICE) - self.assertEqual( - span.attributes.get(_STMT), "SELECT * FROM a_wrong_table" - ) - self.assertEqual(span.attributes.get(_DB), self.SQL_DB) - self.assertIsNone(span.attributes.get(_ROWS)) - self.assertTrue((span.end_time - span.start_time) > 0) - # check the error - self.assertIs( - span.status.status_code, trace.status.StatusCode.ERROR, - ) - self.assertEqual( - span.status.description, "no such table: a_wrong_table" - ) diff --git a/tox.ini b/tox.ini index 806656ced2..ac5e6079f8 100644 --- a/tox.ini +++ b/tox.ini @@ -25,74 +25,13 @@ envlist = pypy3-test-core-getting-started ; opentelemetry-example-app - py3{5,6,7,8}-test-instrumentation-example-app - pypy3-test-instrumentation-example-app - - ; opentelemetry-instrumentation-aiohttp-client - py3{5,6,7,8}-test-instrumentation-aiohttp-client - pypy3-test-instrumentation-aiohttp-client - - ; opentelemetry-instrumentation-aiopg - py3{5,6,7,8}-test-instrumentation-aiopg - ; instrumentation-aiopg intentionally excluded from pypy3 - - ; opentelemetry-instrumentation-botocore - py3{6,7,8}-test-instrumentation-botocore - pypy3-test-instrumentation-botocore - - ; opentelemetry-instrumentation-django - py3{5,6,7,8}-test-instrumentation-django - pypy3-test-instrumentation-django - - ; opentelemetry-instrumentation-dbapi - py3{5,6,7,8}-test-instrumentation-dbapi - pypy3-test-instrumentation-dbapi - - ; opentelemetry-instrumentation-boto - py3{5,6,7,8}-test-instrumentation-boto - pypy3-test-instrumentation-boto - - ; opentelemetry-instrumentation-elasticsearch - py3{5,6,7,8}-test-instrumentation-elasticsearch{2,5,6,7} - pypy3-test-instrumentation-elasticsearch{2,5,6,7} - - ; opentelemetry-instrumentation-falcon - py3{4,5,6,7,8}-test-instrumentation-falcon - pypy3-test-instrumentation-falcon - - ; opentelemetry-instrumentation-fastapi - ; fastapi only supports 3.6 and above. - py3{6,7,8}-test-instrumentation-fastapi - pypy3-test-instrumentation-fastapi - - ; opentelemetry-instrumentation-flask - py3{5,6,7,8}-test-instrumentation-flask - pypy3-test-instrumentation-flask - - ; opentelemetry-instrumentation-requests - py3{5,6,7,8}-test-instrumentation-requests - pypy3-test-instrumentation-requests - - ; opentelemetry-instrumentation-starlette. - ; starlette only supports 3.6 and above. - py3{6,7,8}-test-instrumentation-starlette - pypy3-test-instrumentation-starlette - - ; opentelemetry-instrumentation-jinja2 - py3{5,6,7,8}-test-instrumentation-jinja2 - pypy3-test-instrumentation-jinja2 + py3{5,6,7,8}-test-core-example-app + pypy3-test-core-example-app ; opentelemetry-exporter-jaeger py3{5,6,7,8}-test-exporter-jaeger pypy3-test-exporter-jaeger - ; opentelemetry-exporter-datadog - py3{5,6,7,8}-test-exporter-datadog - - ; opentelemetry-instrumentation-mysql - py3{5,6,7,8}-test-instrumentation-mysql - pypy3-test-instrumentation-mysql - ; opentelemetry-exporter-opencensus py3{5,6,7,8}-test-exporter-opencensus ; exporter-opencensus intentionally excluded from pypy3 @@ -105,42 +44,6 @@ envlist = py3{5,6,7,8}-test-exporter-prometheus pypy3-test-exporter-prometheus - ; opentelemetry-instrumentation-psycopg2 - py3{5,6,7,8}-test-instrumentation-psycopg2 - ; ext-psycopg2 intentionally excluded from pypy3 - - ; opentelemetry-instrumentation-pymemcache - py3{5,6,7,8}-test-instrumentation-pymemcache - pypy3-test-instrumentation-pymemcache - - ; opentelemetry-instrumentation-pymongo - py3{5,6,7,8}-test-instrumentation-pymongo - pypy3-test-instrumentation-pymongo - - ; opentelemetry-instrumentation-pymysql - py3{5,6,7,8}-test-instrumentation-pymysql - pypy3-test-instrumentation-pymysql - - ; opentelemetry-instrumentation-pyramid - py3{5,6,7,8}-test-instrumentation-pyramid - pypy3-test-instrumentation-pyramid - - ; opentelemetry-instrumentation-asgi - py3{5,6,7,8}-test-instrumentation-asgi - pypy3-test-instrumentation-asgi - - ; opentelemetry-instrumentation-asyncpg - py3{5,6,7,8}-test-instrumentation-asyncpg - ; ext-asyncpg intentionally excluded from pypy3 - - ; opentelemetry-instrumentation-sqlite3 - py3{5,6,7,8}-test-instrumentation-sqlite3 - pypy3-test-instrumentation-sqlite3 - - ; opentelemetry-instrumentation-wsgi - py3{5,6,7,8}-test-instrumentation-wsgi - pypy3-test-instrumentation-wsgi - ; opentelemetry-exporter-zipkin py3{5,6,7,8}-test-exporter-zipkin pypy3-test-exporter-zipkin @@ -149,31 +52,6 @@ envlist = py3{5,6,7,8}-test-core-opentracing-shim pypy3-test-core-opentracing-shim - ; opentelemetry-instrumentation-grpc - py3{5,6,7,8}-test-instrumentation-grpc - - ; opentelemetry-instrumentation-sqlalchemy - py3{5,6,7,8}-test-instrumentation-sqlalchemy - pypy3-test-instrumentation-sqlalchemy - - ; opentelemetry-instrumentation-redis - py3{5,6,7,8}-test-instrumentation-redis - pypy3-test-instrumentation-redis - - ; opentelemetry-instrumentation-celery - py3{5,6,7,8}-test-instrumentation-celery - pypy3-test-instrumentation-celery - - ; opentelemetry-instrumentation-system-metrics - py3{5,6,7,8}-test-instrumentation-system-metrics - ; instrumentation-system-metrics intentionally excluded from pypy3 - ; known limitation: gc.get_count won't work under pypy - - ; opentelemetry-instrumentation-tornado - ; instrumentation supports >=6 on Py 3.5 and above. - py3{5,6,7,8}-test-instrumentation-tornado - pypy3-test-instrumentation-tornado - lint py38-tracecontext py38-{mypy,mypyinstalled} @@ -187,14 +65,6 @@ deps = coverage: pytest coverage: pytest-cov mypy,mypyinstalled: mypy - elasticsearch2: elasticsearch-dsl>=2.0,<3.0 - elasticsearch2: elasticsearch>=2.0,<3.0 - elasticsearch5: elasticsearch-dsl>=5.0,<6.0 - elasticsearch5: elasticsearch>=5.0,<6.0 - elasticsearch6: elasticsearch-dsl>=6.0,<7.0 - elasticsearch6: elasticsearch>=6.0,<7.0 - elasticsearch7: elasticsearch-dsl>=7.0,<8.0 - elasticsearch7: elasticsearch>=7.0,<8.0 setenv = mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ @@ -203,42 +73,11 @@ changedir = test-core-sdk: opentelemetry-sdk/tests test-core-proto: opentelemetry-proto/tests test-core-instrumentation: opentelemetry-instrumentation/tests + test-core-example-app: docs/examples/opentelemetry-example-app/tests test-core-getting-started: docs/getting_started/tests test-core-opentracing-shim: instrumentation/opentelemetry-instrumentation-opentracing-shim/tests - test-instrumentation-aiohttp-client: instrumentation/opentelemetry-instrumentation-aiohttp-client/tests - test-instrumentation-aiopg: instrumentation/opentelemetry-instrumentation-aiopg/tests - test-instrumentation-asgi: instrumentation/opentelemetry-instrumentation-asgi/tests - test-instrumentation-asyncpg: instrumentation/opentelemetry-instrumentation-asyncpg/tests - test-instrumentation-boto: instrumentation/opentelemetry-instrumentation-boto/tests - test-instrumentation-botocore: instrumentation/opentelemetry-instrumentation-botocore/tests - test-instrumentation-celery: instrumentation/opentelemetry-instrumentation-celery/tests - test-instrumentation-dbapi: instrumentation/opentelemetry-instrumentation-dbapi/tests - test-instrumentation-django: instrumentation/opentelemetry-instrumentation-django/tests - test-instrumentation-example-app: docs/examples/opentelemetry-example-app/tests - test-instrumentation-elasticsearch{2,5,6,7}: instrumentation/opentelemetry-instrumentation-elasticsearch/tests - test-instrumentation-falcon: instrumentation/opentelemetry-instrumentation-falcon/tests - test-instrumentation-fastapi: instrumentation/opentelemetry-instrumentation-fastapi/tests - test-instrumentation-flask: instrumentation/opentelemetry-instrumentation-flask/tests - test-instrumentation-grpc: instrumentation/opentelemetry-instrumentation-grpc/tests - test-instrumentation-jinja2: instrumentation/opentelemetry-instrumentation-jinja2/tests - test-instrumentation-mysql: instrumentation/opentelemetry-instrumentation-mysql/tests - test-instrumentation-psycopg2: instrumentation/opentelemetry-instrumentation-psycopg2/tests - test-instrumentation-pymemcache: instrumentation/opentelemetry-instrumentation-pymemcache/tests - test-instrumentation-pymongo: instrumentation/opentelemetry-instrumentation-pymongo/tests - test-instrumentation-pymysql: instrumentation/opentelemetry-instrumentation-pymysql/tests - test-instrumentation-pyramid: instrumentation/opentelemetry-instrumentation-pyramid/tests - test-instrumentation-redis: instrumentation/opentelemetry-instrumentation-redis/tests - test-instrumentation-requests: instrumentation/opentelemetry-instrumentation-requests/tests - test-instrumentation-sqlalchemy: instrumentation/opentelemetry-instrumentation-sqlalchemy/tests - test-instrumentation-sqlite3: instrumentation/opentelemetry-instrumentation-sqlite3/tests - test-instrumentation-starlette: instrumentation/opentelemetry-instrumentation-starlette/tests - test-instrumentation-system-metrics: instrumentation/opentelemetry-instrumentation-system-metrics/tests - test-instrumentation-tornado: instrumentation/opentelemetry-instrumentation-tornado/tests - test-instrumentation-wsgi: instrumentation/opentelemetry-instrumentation-wsgi/tests - test-exporter-jaeger: exporter/opentelemetry-exporter-jaeger/tests - test-exporter-datadog: exporter/opentelemetry-exporter-datadog/tests test-exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests test-exporter-otlp: exporter/opentelemetry-exporter-otlp/tests test-exporter-prometheus: exporter/opentelemetry-exporter-prometheus/tests @@ -254,35 +93,9 @@ commands_pre = test-core-proto: pip install {toxinidir}/opentelemetry-proto instrumentation: pip install {toxinidir}/opentelemetry-instrumentation - example-app: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi {toxinidir}/instrumentation/opentelemetry-instrumentation-flask {toxinidir}/docs/examples/opentelemetry-example-app + example-app: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask {toxinidir}/docs/examples/opentelemetry-example-app - getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/instrumentation/opentelemetry-instrumentation-requests -e {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/instrumentation/opentelemetry-instrumentation-flask - - celery: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-celery[test] - - grpc: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc[test] - - wsgi,falcon,flask,django,pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi - asgi,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi - - asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg - - boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test] - boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-boto[test] - - falcon: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon[test] - - flask: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test] - - botocore: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test] - - dbapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi[test] - - django: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-django[test] - - fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi[test] - - mysql: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql[test] + getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus @@ -291,47 +104,13 @@ commands_pre = prometheus: pip install {toxinidir}/exporter/opentelemetry-exporter-prometheus - pymemcache: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache[test] - - pymongo: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo[test] - - psycopg2: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2[test] - - pymysql: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql[test] - - pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid[test] - - sqlite3: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3[test] - - redis: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-redis[test] - - requests: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-requests[test] - - starlette: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette[test] - - tornado: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado - - jinja2: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2[test] - - aiohttp-client: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client - - aiopg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg[test] - jaeger: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk opentracing-shim: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-opentracing-shim - datadog: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/exporter/opentelemetry-exporter-datadog - zipkin: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin - sqlalchemy: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy - - system-metrics: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics[test] - - elasticsearch{2,5,6,7}: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch[test] - ; In order to get a healthy coverage report, ; we have to install packages in editable mode. coverage: python {toxinidir}/scripts/eachdist.py install --editable @@ -356,7 +135,9 @@ commands = basepython: python3.8 recreate = True deps = + -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-grpc -c dev-requirements.txt + asgiref pylint flake8 isort @@ -373,6 +154,12 @@ commands = [testenv:docs] deps = + -e {toxinidir}/opentelemetry-python-contrib/exporter/opentelemetry-exporter-datadog + -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-grpc + -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask + -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-django + -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests + -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -c {toxinidir}/dev-requirements.txt -r {toxinidir}/docs-requirements.txt @@ -398,9 +185,8 @@ commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/opentelemetry-instrumentation \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-requests \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-flask + -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests \ + -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi commands = {toxinidir}/scripts/tracecontext-integration-test.sh @@ -408,16 +194,7 @@ commands = [testenv:docker-tests] deps = pytest - asyncpg==0.20.1 docker-compose >= 1.25.2 - mysql-connector-python ~= 8.0 - pymongo ~= 3.1 - pymysql ~= 0.9.3 - psycopg2-binary ~= 2.8.4 - aiopg >= 0.13.0 - sqlalchemy ~= 1.3.16 - redis ~= 3.3.11 - celery ~= 4.0, != 4.4.4 changedir = tests/opentelemetry-docker-tests/tests @@ -425,24 +202,10 @@ changedir = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-sdk \ - -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/tests/util \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-celery \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2 \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-redis \ - -e {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics \ -e {toxinidir}/exporter/opentelemetry-exporter-opencensus - docker-compose up -d - python check_availability.py + docker-compose up -d commands = pytest {posargs} - commands_post = docker-compose down -v From 2701445bd4c5f2ff5e363d38a34ab635563d4b52 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 9 Nov 2020 00:05:50 +0000 Subject: [PATCH 0656/1517] Document None attribute values as undefined behavior (#1361) --- opentelemetry-api/src/opentelemetry/trace/span.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 5cf4c36a3e..681e9f7ec3 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -38,6 +38,8 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: """Sets an Attribute. Sets a single Attribute with the key and value passed as arguments. + + Note: The behavior of `None` value attributes is undefined, and hence strongly discouraged. """ @abc.abstractmethod From ad01a563c1119e3f888c39f9b2bf9d797a4bc05c Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 10 Nov 2020 18:58:05 +0530 Subject: [PATCH 0657/1517] Fix Jaeger exporter to correctly translate span.kind attribute (#1329) --- .../src/opentelemetry/exporter/jaeger/__init__.py | 11 ++++++++++- .../tests/test_jaeger_exporter.py | 13 ++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index 3271fdd5ba..d95095bd9c 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -70,6 +70,7 @@ from opentelemetry.exporter.jaeger.gen.agent import Agent as agent from opentelemetry.exporter.jaeger.gen.jaeger import Collector as jaeger from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult +from opentelemetry.trace import SpanKind from opentelemetry.trace.status import StatusCode DEFAULT_AGENT_HOST_NAME = "localhost" @@ -77,6 +78,14 @@ UDP_PACKET_MAX_LENGTH = 65000 +OTLP_JAEGER_SPAN_KIND = { + SpanKind.CLIENT: "client", + SpanKind.SERVER: "server", + SpanKind.CONSUMER: "consumer", + SpanKind.PRODUCER: "producer", + SpanKind.INTERNAL: "internal", +} + logger = logging.getLogger(__name__) @@ -226,7 +235,7 @@ def _translate_to_jaeger(spans: Span): [ _get_long_tag("status.code", status.status_code.value), _get_string_tag("status.message", status.description), - _get_string_tag("span.kind", span.kind.name), + _get_string_tag("span.kind", OTLP_JAEGER_SPAN_KIND[span.kind]), ] ) diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py index 23afdefd78..c49a321688 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py @@ -25,6 +25,7 @@ from opentelemetry.sdk import trace from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.trace import SpanKind from opentelemetry.trace.status import Status, StatusCode @@ -151,6 +152,10 @@ def test_nsec_to_usec_round(self): self.assertEqual(nsec_to_usec_round(5499), 5) self.assertEqual(nsec_to_usec_round(5500), 6) + def test_all_otlp_span_kinds_are_mapped(self): + for kind in SpanKind: + self.assertIn(kind, jaeger_exporter.OTLP_JAEGER_SPAN_KIND) + # pylint: disable=too-many-locals def test_translate_to_jaeger(self): # pylint: disable=invalid-name @@ -216,9 +221,7 @@ def test_translate_to_jaeger(self): key="status.message", vType=jaeger.TagType.STRING, vStr=None ), jaeger.Tag( - key="span.kind", - vType=jaeger.TagType.STRING, - vStr=trace_api.SpanKind.INTERNAL.name, + key="span.kind", vType=jaeger.TagType.STRING, vStr="internal", ), ] @@ -315,7 +318,7 @@ def test_translate_to_jaeger(self): jaeger.Tag( key="span.kind", vType=jaeger.TagType.STRING, - vStr=trace_api.SpanKind.CLIENT.name, + vStr="client", ), jaeger.Tag( key="error", vType=jaeger.TagType.BOOL, vBool=True @@ -391,7 +394,7 @@ def test_translate_to_jaeger(self): jaeger.Tag( key="span.kind", vType=jaeger.TagType.STRING, - vStr=trace_api.SpanKind.INTERNAL.name, + vStr="internal", ), jaeger.Tag( key="otel.instrumentation_library.name", From 26bf23f72b990eeb74ae866d867a6101a8062750 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Wed, 11 Nov 2020 06:40:30 -0800 Subject: [PATCH 0658/1517] Set dependencies version for tests util pkg (#1362) --- tests/util/setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index 7ea637d4b0..6d26100963 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -37,8 +37,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api - opentelemetry-sdk + opentelemetry-api == 0.16.dev0 + opentelemetry-sdk == 0.16.dev0 [options.extras_require] test = flask~=1.0 From 4686bef320c330922ce8549e10f2542bb92e3d38 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 13 Nov 2020 11:02:46 -0500 Subject: [PATCH 0659/1517] Align optional parameters for span related to exceptions, add exception.escaped for record_exception (#1365) --- .../src/opentelemetry/trace/__init__.py | 26 ++++--- .../src/opentelemetry/trace/span.py | 2 + opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 75 +++++++++++-------- opentelemetry-sdk/tests/trace/test_trace.py | 25 +++++++ 5 files changed, 90 insertions(+), 40 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index d747734629..2b13b5b056 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -234,6 +234,7 @@ def start_span( attributes: types.Attributes = None, links: typing.Sequence[Link] = (), start_time: typing.Optional[int] = None, + record_exception: bool = True, set_status_on_exception: bool = True, ) -> "Span": """Starts a span. @@ -266,6 +267,8 @@ def start_span( attributes: The span's attributes. links: Links span to other spans start_time: Sets the start time of a span + record_exception: Whether to record any exceptions raised within the + context as error event on the span. set_status_on_exception: Only relevant if the returned span is used in a with/context manager. Defines wether the span status will be automatically set to ERROR when an uncaught exception is @@ -285,7 +288,9 @@ def start_as_current_span( kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: typing.Sequence[Link] = (), + start_time: typing.Optional[int] = None, record_exception: bool = True, + set_status_on_exception: bool = True, ) -> typing.Iterator["Span"]: """Context manager for creating a new span and set it as the current span in this tracer's context. @@ -325,8 +330,14 @@ def start_as_current_span( meaningful even if there is no parent. attributes: The span's attributes. links: Links span to other spans + start_time: Sets the start time of a span record_exception: Whether to record any exceptions raised within the context as error event on the span. + set_status_on_exception: Only relevant if the returned span is used + in a with/context manager. Defines wether the span status will + be automatically set to ERROR when an uncaught exception is + raised in the span with block. The span status won't be set by + this mechanism if it was previously set manually. Yields: The newly-created span. @@ -335,10 +346,7 @@ def start_as_current_span( @contextmanager # type: ignore @abc.abstractmethod def use_span( - self, - span: "Span", - end_on_exit: bool = False, - record_exception: bool = True, + self, span: "Span", end_on_exit: bool = False, ) -> typing.Iterator[None]: """Context manager for setting the passed span as the current span in the context, as well as resetting the @@ -355,8 +363,6 @@ def use_span( span: The span to start and make current. end_on_exit: Whether to end the span automatically when leaving the context manager. - record_exception: Whether to record any exceptions raised within the - context as error event on the span. """ @@ -374,6 +380,7 @@ def start_span( attributes: types.Attributes = None, links: typing.Sequence[Link] = (), start_time: typing.Optional[int] = None, + record_exception: bool = True, set_status_on_exception: bool = True, ) -> "Span": # pylint: disable=unused-argument,no-self-use @@ -387,17 +394,16 @@ def start_as_current_span( kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: typing.Sequence[Link] = (), + start_time: typing.Optional[int] = None, record_exception: bool = True, + set_status_on_exception: bool = True, ) -> typing.Iterator["Span"]: # pylint: disable=unused-argument,no-self-use yield INVALID_SPAN @contextmanager # type: ignore def use_span( - self, - span: "Span", - end_on_exit: bool = False, - record_exception: bool = True, + self, span: "Span", end_on_exit: bool = False, ) -> typing.Iterator[None]: # pylint: disable=unused-argument,no-self-use yield diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 681e9f7ec3..c173cf8b40 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -86,6 +86,7 @@ def record_exception( exception: Exception, attributes: types.Attributes = None, timestamp: typing.Optional[int] = None, + escaped: bool = False, ) -> None: """Records an exception as a span event.""" @@ -275,6 +276,7 @@ def record_exception( exception: Exception, attributes: types.Attributes = None, timestamp: typing.Optional[int] = None, + escaped: bool = False, ) -> None: pass diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index f1790c0a58..2a3ca9b586 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased - Add optional parameter to `record_exception` method ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) +- Update exception handling optional parameters, add escaped attribute to record_exception + ([#1365](https://github.com/open-telemetry/opentelemetry-python/pull/1365)) ## Version 0.15b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 6b99888a46..facef291ac 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -412,6 +412,7 @@ def __new__(cls, *args, **kwargs): raise TypeError("Span must be instantiated via a tracer.") return super().__new__(cls) + # pylint: disable=too-many-locals def __init__( self, name: str, @@ -426,6 +427,7 @@ def __init__( kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, span_processor: SpanProcessor = SpanProcessor(), instrumentation_info: InstrumentationInfo = None, + record_exception: bool = True, set_status_on_exception: bool = True, ) -> None: @@ -436,6 +438,7 @@ def __init__( self.trace_config = trace_config self.resource = resource self.kind = kind + self._record_exception = record_exception self._set_status_on_exception = set_status_on_exception self.span_processor = span_processor @@ -663,20 +666,25 @@ def __exit__( exc_tb: Optional[TracebackType], ) -> None: """Ends context manager and calls `end` on the `Span`.""" - # Records status if span is used as context manager - # i.e. with tracer.start_span() as span: - # TODO: Record exception - if ( - self.status.status_code is StatusCode.UNSET - and self._set_status_on_exception - and exc_val is not None - ): - self.set_status( - Status( - status_code=StatusCode.ERROR, - description="{}: {}".format(exc_type.__name__, exc_val), + if exc_val is not None: + # Record the exception as an event + # pylint:disable=protected-access + if self._record_exception: + self.record_exception(exception=exc_val, escaped=True) + # Records status if span is used as context manager + # i.e. with tracer.start_span() as span: + if ( + self.status.status_code is StatusCode.UNSET + and self._set_status_on_exception + ): + self.set_status( + Status( + status_code=StatusCode.ERROR, + description="{}: {}".format( + exc_type.__name__, exc_val + ), + ) ) - ) super().__exit__(exc_type, exc_val, exc_tb) @@ -685,6 +693,7 @@ def record_exception( exception: Exception, attributes: types.Attributes = None, timestamp: Optional[int] = None, + escaped: bool = False, ) -> None: """Records an exception as a span event.""" try: @@ -698,6 +707,7 @@ def record_exception( "exception.type": exception.__class__.__name__, "exception.message": str(exception), "exception.stacktrace": stacktrace, + "exception.escaped": str(escaped), } if attributes: _attributes.update(attributes) @@ -740,12 +750,21 @@ def start_as_current_span( kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, attributes: types.Attributes = None, links: Sequence[trace_api.Link] = (), + start_time: Optional[int] = None, record_exception: bool = True, + set_status_on_exception: bool = True, ) -> Iterator[trace_api.Span]: - span = self.start_span(name, context, kind, attributes, links) - return self.use_span( - span, end_on_exit=True, record_exception=record_exception + span = self.start_span( + name=name, + context=context, + kind=kind, + attributes=attributes, + links=links, + start_time=start_time, + record_exception=record_exception, + set_status_on_exception=set_status_on_exception, ) + return self.use_span(span, end_on_exit=True) def start_span( # pylint: disable=too-many-locals self, @@ -755,6 +774,7 @@ def start_span( # pylint: disable=too-many-locals attributes: types.Attributes = None, links: Sequence[trace_api.Link] = (), start_time: Optional[int] = None, + record_exception: bool = True, set_status_on_exception: bool = True, ) -> trace_api.Span: @@ -816,6 +836,7 @@ def start_span( # pylint: disable=too-many-locals kind=kind, links=links, instrumentation_info=self.instrumentation_info, + record_exception=record_exception, set_status_on_exception=set_status_on_exception, ) span.start(start_time=start_time, parent_context=context) @@ -825,10 +846,7 @@ def start_span( # pylint: disable=too-many-locals @contextmanager def use_span( - self, - span: trace_api.Span, - end_on_exit: bool = False, - record_exception: bool = True, + self, span: trace_api.Span, end_on_exit: bool = False, ) -> Iterator[trace_api.Span]: try: token = context_api.attach(context_api.set_value(SPAN_KEY, span)) @@ -837,11 +855,12 @@ def use_span( finally: context_api.detach(token) - except Exception as error: # pylint: disable=broad-except - # pylint:disable=protected-access + except Exception as exc: # pylint: disable=broad-except + # Record the exception as an event if isinstance(span, Span): - if record_exception: - span.record_exception(error) + # pylint:disable=protected-access + if span._record_exception: + span.record_exception(exc) # Records status if use_span is used # i.e. with tracer.start_as_current_span() as span: @@ -851,13 +870,9 @@ def use_span( ): span.set_status( Status( - status_code=getattr( - error, - EXCEPTION_STATUS_FIELD, - StatusCode.ERROR, - ), + status_code=StatusCode.ERROR, description="{}: {}".format( - type(error).__name__, error + type(exc).__name__, exc ), ) ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 521bde00c8..8bb84d1d7d 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -889,6 +889,9 @@ def test_record_exception_with_attributes(self): self.assertEqual( "RuntimeError", exception_event.attributes["exception.type"] ) + self.assertEqual( + "False", exception_event.attributes["exception.escaped"] + ) self.assertIn( "RuntimeError: error", exception_event.attributes["exception.stacktrace"], @@ -900,6 +903,28 @@ def test_record_exception_with_attributes(self): True, exception_event.attributes["has_additional_attributes"], ) + def test_record_exception_escaped(self): + span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) + try: + raise RuntimeError("error") + except RuntimeError as err: + span.record_exception(exception=err, escaped=True) + exception_event = span.events[0] + self.assertEqual("exception", exception_event.name) + self.assertEqual( + "error", exception_event.attributes["exception.message"] + ) + self.assertEqual( + "RuntimeError", exception_event.attributes["exception.type"] + ) + self.assertIn( + "RuntimeError: error", + exception_event.attributes["exception.stacktrace"], + ) + self.assertEqual( + "True", exception_event.attributes["exception.escaped"] + ) + def test_record_exception_with_timestamp(self): span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) try: From 1d03c34bc0f7aeee5392da567e4931cdfe0ccb80 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Sun, 15 Nov 2020 21:09:39 -0800 Subject: [PATCH 0660/1517] Add Contrib repo checkout for docs build (#1371) --- .github/workflows/docs.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3c8d31ca4e..fc9a8392ad 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,12 +10,20 @@ on: - 'instrumentation/**' - 'opentelemetry-python/opentelemetry-api/src/opentelemetry/**' - 'opentelemetry-python/opentelemetry-sdk/src/opentelemetry/sdk/**' +env: + CONTRIB_REPO_SHA: master jobs: docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} + uses: actions/checkout@v2 + with: + repository: open-telemetry/opentelemetry-python-contrib + ref: ${{ env.CONTRIB_REPO_SHA }} + path: opentelemetry-python-contrib - name: Set up Python py38 uses: actions/setup-python@v2 with: From f1db112cb0b417af94efba2babac08bb5c60891d Mon Sep 17 00:00:00 2001 From: Ricky Date: Mon, 16 Nov 2020 00:14:31 -0500 Subject: [PATCH 0661/1517] Add __getnewargs__ to SpanContext class (#1380) --- opentelemetry-api/CHANGELOG.md | 1 + .../src/opentelemetry/trace/span.py | 11 ++++++ .../tests/trace/test_span_context.py | 36 +++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 opentelemetry-api/tests/trace/test_span_context.py diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 8054937312..b231b5aed5 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Add optional parameter to `record_exception` method ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) +- Add pickle support to SpanContext class ([#1380](https://github.com/open-telemetry/opentelemetry-python/pull/1380)) ## Version 0.15b0 diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index c173cf8b40..2d0e34996c 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -190,6 +190,17 @@ def __new__( (trace_id, span_id, is_remote, trace_flags, trace_state, is_valid), ) + def __getnewargs__( + self, + ) -> typing.Tuple[int, int, bool, "TraceFlags", "TraceState"]: + return ( + self.trace_id, + self.span_id, + self.is_remote, + self.trace_flags, + self.trace_state, + ) + @property def trace_id(self) -> int: return self[0] # pylint: disable=unsubscriptable-object diff --git a/opentelemetry-api/tests/trace/test_span_context.py b/opentelemetry-api/tests/trace/test_span_context.py new file mode 100644 index 0000000000..c109d006a5 --- /dev/null +++ b/opentelemetry-api/tests/trace/test_span_context.py @@ -0,0 +1,36 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pickle +import unittest + +from opentelemetry import trace + + +class TestSpanContext(unittest.TestCase): + def test_span_context_pickle(self): + """ + SpanContext needs to be pickleable to support multiprocessing + so span can start as parent from the new spawned process + """ + sc = trace.SpanContext( + 1, + 2, + is_remote=False, + trace_flags=trace.DEFAULT_TRACE_OPTIONS, + trace_state=trace.DEFAULT_TRACE_STATE, + ) + pickle_sc = pickle.loads(pickle.dumps(sc)) + self.assertEqual(sc.trace_id, pickle_sc.trace_id) + self.assertEqual(sc.span_id, pickle_sc.span_id) From c4950a3b65e41d89e04c6f8673db933cfe5932a3 Mon Sep 17 00:00:00 2001 From: Shovnik Bhattacharya Date: Mon, 16 Nov 2020 19:09:57 -0500 Subject: [PATCH 0662/1517] Rename MetricRecord to ExportRecord (#1367) --- .../opencensus/metrics_exporter/__init__.py | 46 +++++------ .../test_otcollector_metrics_exporter.py | 12 +-- .../otlp/metrics_exporter/__init__.py | 76 +++++++++---------- .../tests/test_otlp_metric_exporter.py | 12 +-- .../exporter/prometheus/__init__.py | 34 ++++----- .../tests/test_prometheus_exporter.py | 10 +-- opentelemetry-sdk/CHANGELOG.md | 5 +- .../sdk/metrics/export/__init__.py | 18 ++--- .../export/in_memory_metrics_exporter.py | 6 +- .../sdk/metrics/export/processor.py | 16 ++-- .../tests/metrics/export/test_export.py | 4 +- 11 files changed, 119 insertions(+), 120 deletions(-) diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py index de3c8a8b45..979600173a 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py @@ -29,7 +29,7 @@ import opentelemetry.exporter.opencensus.util as utils from opentelemetry.sdk.metrics import Counter, Metric from opentelemetry.sdk.metrics.export import ( - MetricRecord, + ExportRecord, MetricsExporter, MetricsExportResult, ) @@ -79,11 +79,11 @@ def __init__( self.exporter_start_timestamp.GetCurrentTime() def export( - self, metric_records: Sequence[MetricRecord] + self, export_records: Sequence[ExportRecord] ) -> MetricsExportResult: try: responses = self.client.Export( - self.generate_metrics_requests(metric_records) + self.generate_metrics_requests(export_records) ) # Read response @@ -99,7 +99,7 @@ def shutdown(self) -> None: pass def generate_metrics_requests( - self, metrics: Sequence[MetricRecord] + self, metrics: Sequence[ExportRecord] ) -> metrics_service_pb2.ExportMetricsServiceRequest: collector_metrics = translate_to_collector( metrics, self.exporter_start_timestamp @@ -112,15 +112,15 @@ def generate_metrics_requests( # pylint: disable=too-many-branches def translate_to_collector( - metric_records: Sequence[MetricRecord], + export_records: Sequence[ExportRecord], exporter_start_timestamp: Timestamp, ) -> Sequence[metrics_pb2.Metric]: collector_metrics = [] - for metric_record in metric_records: + for export_record in export_records: label_values = [] label_keys = [] - for label_tuple in metric_record.labels: + for label_tuple in export_record.labels: label_keys.append(metrics_pb2.LabelKey(key=label_tuple[0])) label_values.append( metrics_pb2.LabelValue( @@ -130,30 +130,30 @@ def translate_to_collector( ) metric_descriptor = metrics_pb2.MetricDescriptor( - name=metric_record.instrument.name, - description=metric_record.instrument.description, - unit=metric_record.instrument.unit, - type=get_collector_metric_type(metric_record.instrument), + name=export_record.instrument.name, + description=export_record.instrument.description, + unit=export_record.instrument.unit, + type=get_collector_metric_type(export_record.instrument), label_keys=label_keys, ) # If cumulative and stateful, explicitly set the start_timestamp to # exporter start time. - if metric_record.instrument.meter.processor.stateful: + if export_record.instrument.meter.processor.stateful: start_timestamp = exporter_start_timestamp else: start_timestamp = None timeseries = metrics_pb2.TimeSeries( label_values=label_values, - points=[get_collector_point(metric_record)], + points=[get_collector_point(export_record)], start_timestamp=start_timestamp, ) collector_metrics.append( metrics_pb2.Metric( metric_descriptor=metric_descriptor, timeseries=[timeseries], - resource=get_resource(metric_record), + resource=get_resource(export_record), ) ) return collector_metrics @@ -169,29 +169,29 @@ def get_collector_metric_type(metric: Metric) -> metrics_pb2.MetricDescriptor: return metrics_pb2.MetricDescriptor.UNSPECIFIED -def get_collector_point(metric_record: MetricRecord) -> metrics_pb2.Point: +def get_collector_point(export_record: ExportRecord) -> metrics_pb2.Point: # TODO: horrible hack to get original list of keys to then get the bound # instrument point = metrics_pb2.Point( timestamp=utils.proto_timestamp_from_time_ns( - metric_record.aggregator.last_update_timestamp + export_record.aggregator.last_update_timestamp ) ) - if metric_record.instrument.value_type == int: - point.int64_value = metric_record.aggregator.checkpoint - elif metric_record.instrument.value_type == float: - point.double_value = metric_record.aggregator.checkpoint + if export_record.instrument.value_type == int: + point.int64_value = export_record.aggregator.checkpoint + elif export_record.instrument.value_type == float: + point.double_value = export_record.aggregator.checkpoint else: raise TypeError( "Unsupported metric type: {}".format( - metric_record.instrument.value_type + export_record.instrument.value_type ) ) return point -def get_resource(metric_record: MetricRecord) -> resource_pb2.Resource: - resource_attributes = metric_record.resource.attributes +def get_resource(export_record: ExportRecord) -> resource_pb2.Resource: + resource_attributes = export_record.resource.attributes return resource_pb2.Resource( type=infer_oc_resource_type(resource_attributes), labels={k: str(v) for k, v in resource_attributes.items()}, diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py index 441c75b682..b48a4a5a33 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py @@ -28,7 +28,7 @@ get_dict_as_key, ) from opentelemetry.sdk.metrics.export import ( - MetricRecord, + ExportRecord, MetricsExportResult, aggregate, ) @@ -100,7 +100,7 @@ def test_get_collector_point(self): "testName", "testDescription", "unit", float, ) result = metrics_exporter.get_collector_point( - MetricRecord( + ExportRecord( int_counter, self._key_labels, aggregator, @@ -113,7 +113,7 @@ def test_get_collector_point(self): aggregator.update(123.5) aggregator.take_checkpoint() result = metrics_exporter.get_collector_point( - MetricRecord( + ExportRecord( float_counter, self._key_labels, aggregator, @@ -124,7 +124,7 @@ def test_get_collector_point(self): self.assertRaises( TypeError, metrics_exporter.get_collector_point( - MetricRecord( + ExportRecord( valuerecorder, self._key_labels, aggregator, @@ -144,7 +144,7 @@ def test_export(self): test_metric = self._meter.create_counter( "testname", "testdesc", "unit", int, self._labels.keys(), ) - record = MetricRecord( + record = ExportRecord( test_metric, self._key_labels, aggregate.SumAggregator(), @@ -173,7 +173,7 @@ def test_translate_to_collector(self): aggregator = aggregate.SumAggregator() aggregator.update(123) aggregator.take_checkpoint() - record = MetricRecord( + record = ExportRecord( test_metric, self._key_labels, aggregator, diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py index 2078664b43..c65b2a89df 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -54,7 +54,7 @@ ValueRecorder, ) from opentelemetry.sdk.metrics.export import ( - MetricRecord, + ExportRecord, MetricsExporter, MetricsExportResult, ) @@ -71,39 +71,37 @@ def _get_data_points( - sdk_metric_record: MetricRecord, data_point_class: Type[DataPointT] + export_record: ExportRecord, data_point_class: Type[DataPointT] ) -> List[DataPointT]: - if isinstance(sdk_metric_record.aggregator, SumAggregator): - value = sdk_metric_record.aggregator.checkpoint + if isinstance(export_record.aggregator, SumAggregator): + value = export_record.aggregator.checkpoint - elif isinstance(sdk_metric_record.aggregator, MinMaxSumCountAggregator): + elif isinstance(export_record.aggregator, MinMaxSumCountAggregator): # FIXME: How are values to be interpreted from this aggregator? raise Exception("MinMaxSumCount aggregator data not supported") - elif isinstance(sdk_metric_record.aggregator, HistogramAggregator): + elif isinstance(export_record.aggregator, HistogramAggregator): # FIXME: How are values to be interpreted from this aggregator? raise Exception("Histogram aggregator data not supported") - elif isinstance(sdk_metric_record.aggregator, LastValueAggregator): - value = sdk_metric_record.aggregator.checkpoint + elif isinstance(export_record.aggregator, LastValueAggregator): + value = export_record.aggregator.checkpoint - elif isinstance(sdk_metric_record.aggregator, ValueObserverAggregator): - value = sdk_metric_record.aggregator.checkpoint.last + elif isinstance(export_record.aggregator, ValueObserverAggregator): + value = export_record.aggregator.checkpoint.last return [ data_point_class( labels=[ StringKeyValue(key=str(label_key), value=str(label_value)) - for label_key, label_value in sdk_metric_record.labels + for label_key, label_value in export_record.labels ], value=value, start_time_unix_nano=( - sdk_metric_record.aggregator.initial_checkpoint_timestamp - ), - time_unix_nano=( - sdk_metric_record.aggregator.last_update_timestamp + export_record.aggregator.initial_checkpoint_timestamp ), + time_unix_nano=(export_record.aggregator.last_update_timestamp), ) ] @@ -111,7 +109,7 @@ def _get_data_points( class OTLPMetricsExporter( MetricsExporter, OTLPExporterMixin[ - MetricRecord, ExportMetricsServiceRequest, MetricsExportResult + ExportRecord, ExportMetricsServiceRequest, MetricsExportResult ], ): # pylint: disable=unsubscriptable-object @@ -162,14 +160,14 @@ def __init__( # pylint: disable=no-self-use def _translate_data( - self, data: Sequence[MetricRecord] + self, export_records: Sequence[ExportRecord] ) -> ExportMetricsServiceRequest: # pylint: disable=too-many-locals,no-member # pylint: disable=attribute-defined-outside-init sdk_resource_instrumentation_library_metrics = {} - # The criteria to decide how to translate data is based on this table + # The criteria to decide how to translate export_records is based on this table # taken directly from OpenTelemetry Proto v0.5.0: # TODO: Update table after the decision on: @@ -185,13 +183,13 @@ def _translate_data( # SumObserver Sum(aggregation_temporality=cumulative;is_monotonic=true) # UpDownSumObserver Sum(aggregation_temporality=cumulative;is_monotonic=false) # ValueObserver Gauge() - for sdk_metric_record in data: + for export_record in export_records: - if sdk_metric_record.resource not in ( + if export_record.resource not in ( sdk_resource_instrumentation_library_metrics.keys() ): sdk_resource_instrumentation_library_metrics[ - sdk_metric_record.resource + export_record.resource ] = InstrumentationLibraryMetrics() type_class = { @@ -210,16 +208,16 @@ def _translate_data( }, } - value_type = sdk_metric_record.instrument.value_type + value_type = export_record.instrument.value_type sum_class = type_class[value_type]["sum"]["class"] gauge_class = type_class[value_type]["gauge"]["class"] data_point_class = type_class[value_type]["data_point_class"] - if isinstance(sdk_metric_record.instrument, Counter): + if isinstance(export_record.instrument, Counter): otlp_metric_data = sum_class( data_points=_get_data_points( - sdk_metric_record, data_point_class + export_record, data_point_class ), aggregation_temporality=( AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA @@ -228,10 +226,10 @@ def _translate_data( ) argument = type_class[value_type]["sum"]["argument"] - elif isinstance(sdk_metric_record.instrument, UpDownCounter): + elif isinstance(export_record.instrument, UpDownCounter): otlp_metric_data = sum_class( data_points=_get_data_points( - sdk_metric_record, data_point_class + export_record, data_point_class ), aggregation_temporality=( AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA @@ -240,14 +238,14 @@ def _translate_data( ) argument = type_class[value_type]["sum"]["argument"] - elif isinstance(sdk_metric_record.instrument, (ValueRecorder)): + elif isinstance(export_record.instrument, (ValueRecorder)): logger.warning("Skipping exporting of ValueRecorder metric") continue - elif isinstance(sdk_metric_record.instrument, SumObserver): + elif isinstance(export_record.instrument, SumObserver): otlp_metric_data = sum_class( data_points=_get_data_points( - sdk_metric_record, data_point_class + export_record, data_point_class ), aggregation_temporality=( AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE @@ -256,10 +254,10 @@ def _translate_data( ) argument = type_class[value_type]["sum"]["argument"] - elif isinstance(sdk_metric_record.instrument, UpDownSumObserver): + elif isinstance(export_record.instrument, UpDownSumObserver): otlp_metric_data = sum_class( data_points=_get_data_points( - sdk_metric_record, data_point_class + export_record, data_point_class ), aggregation_temporality=( AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE @@ -268,24 +266,22 @@ def _translate_data( ) argument = type_class[value_type]["sum"]["argument"] - elif isinstance(sdk_metric_record.instrument, (ValueObserver)): + elif isinstance(export_record.instrument, (ValueObserver)): otlp_metric_data = gauge_class( data_points=_get_data_points( - sdk_metric_record, data_point_class + export_record, data_point_class ) ) argument = type_class[value_type]["gauge"]["argument"] sdk_resource_instrumentation_library_metrics[ - sdk_metric_record.resource + export_record.resource ].metrics.append( OTLPMetric( **{ - "name": sdk_metric_record.instrument.name, - "description": ( - sdk_metric_record.instrument.description - ), - "unit": sdk_metric_record.instrument.unit, + "name": export_record.instrument.name, + "description": (export_record.instrument.description), + "unit": export_record.instrument.unit, argument: otlp_metric_data, } ) @@ -299,6 +295,6 @@ def _translate_data( ) ) - def export(self, metrics: Sequence[MetricRecord]) -> MetricsExportResult: + def export(self, metrics: Sequence[ExportRecord]) -> MetricsExportResult: # pylint: disable=arguments-differ return self._export(metrics) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index 3034fcdf65..dd03c27d84 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -40,7 +40,7 @@ Resource as OTLPResource, ) from opentelemetry.sdk.metrics import Counter, MeterProvider -from opentelemetry.sdk.metrics.export import MetricRecord +from opentelemetry.sdk.metrics.export import ExportRecord from opentelemetry.sdk.metrics.export.aggregate import SumAggregator from opentelemetry.sdk.resources import Resource as SDKResource @@ -50,7 +50,7 @@ def setUp(self): self.exporter = OTLPMetricsExporter(insecure=True) resource = SDKResource(OrderedDict([("a", 1), ("b", False)])) - self.counter_metric_record = MetricRecord( + self.counter_export_record = ExportRecord( Counter( "c", "d", @@ -97,9 +97,9 @@ def test_translate_metrics(self, mock_time_ns): mock_time_ns.configure_mock(**{"return_value": 1}) - self.counter_metric_record.aggregator.checkpoint = 1 - self.counter_metric_record.aggregator.initial_checkpoint_timestamp = 1 - self.counter_metric_record.aggregator.last_update_timestamp = 1 + self.counter_export_record.aggregator.checkpoint = 1 + self.counter_export_record.aggregator.initial_checkpoint_timestamp = 1 + self.counter_export_record.aggregator.last_update_timestamp = 1 expected = ExportMetricsServiceRequest( resource_metrics=[ @@ -146,6 +146,6 @@ def test_translate_metrics(self, mock_time_ns): ) # pylint: disable=protected-access - actual = self.exporter._translate_data([self.counter_metric_record]) + actual = self.exporter._translate_data([self.counter_export_record]) self.assertEqual(expected, actual) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 8ca89a94d2..1c13903564 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -77,7 +77,7 @@ from opentelemetry.metrics import Counter, ValueRecorder from opentelemetry.sdk.metrics.export import ( - MetricRecord, + ExportRecord, MetricsExporter, MetricsExportResult, ) @@ -99,9 +99,9 @@ def __init__(self, prefix: str = ""): REGISTRY.register(self._collector) def export( - self, metric_records: Sequence[MetricRecord] + self, export_records: Sequence[ExportRecord] ) -> MetricsExportResult: - self._collector.add_metrics_data(metric_records) + self._collector.add_metrics_data(export_records) return MetricsExportResult.SUCCESS def shutdown(self) -> None: @@ -120,8 +120,8 @@ def __init__(self, prefix: str = ""): r"[^\w]", re.UNICODE | re.IGNORECASE ) - def add_metrics_data(self, metric_records: Sequence[MetricRecord]) -> None: - self._metrics_to_export.append(metric_records) + def add_metrics_data(self, export_records: Sequence[ExportRecord]) -> None: + self._metrics_to_export.append(export_records) def collect(self): """Collect fetches the metrics from OpenTelemetry @@ -131,38 +131,38 @@ def collect(self): """ while self._metrics_to_export: - for metric_record in self._metrics_to_export.popleft(): + for export_record in self._metrics_to_export.popleft(): prometheus_metric = self._translate_to_prometheus( - metric_record + export_record ) if prometheus_metric is not None: yield prometheus_metric - def _translate_to_prometheus(self, metric_record: MetricRecord): + def _translate_to_prometheus(self, export_record: ExportRecord): prometheus_metric = None label_values = [] label_keys = [] - for label_tuple in metric_record.labels: + for label_tuple in export_record.labels: label_keys.append(self._sanitize(label_tuple[0])) label_values.append(label_tuple[1]) metric_name = "" if self._prefix != "": metric_name = self._prefix + "_" - metric_name += self._sanitize(metric_record.instrument.name) + metric_name += self._sanitize(export_record.instrument.name) - description = getattr(metric_record.instrument, "description", "") - if isinstance(metric_record.instrument, Counter): + description = getattr(export_record.instrument, "description", "") + if isinstance(export_record.instrument, Counter): prometheus_metric = CounterMetricFamily( name=metric_name, documentation=description, labels=label_keys ) prometheus_metric.add_metric( - labels=label_values, value=metric_record.aggregator.checkpoint + labels=label_values, value=export_record.aggregator.checkpoint ) # TODO: Add support for histograms when supported in OT - elif isinstance(metric_record.instrument, ValueRecorder): - value = metric_record.aggregator.checkpoint - if isinstance(metric_record.aggregator, MinMaxSumCountAggregator): + elif isinstance(export_record.instrument, ValueRecorder): + value = export_record.aggregator.checkpoint + if isinstance(export_record.aggregator, MinMaxSumCountAggregator): prometheus_metric = SummaryMetricFamily( name=metric_name, documentation=description, @@ -183,7 +183,7 @@ def _translate_to_prometheus(self, metric_record: MetricRecord): else: logger.warning( - "Unsupported metric type. %s", type(metric_record.instrument) + "Unsupported metric type. %s", type(export_record.instrument) ) return prometheus_metric diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index c7c0c5b071..5813fba33c 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -24,7 +24,7 @@ ) from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk import metrics -from opentelemetry.sdk.metrics.export import MetricRecord, MetricsExportResult +from opentelemetry.sdk.metrics.export import ExportRecord, MetricsExportResult from opentelemetry.sdk.metrics.export.aggregate import ( MinMaxSumCountAggregator, SumAggregator, @@ -66,7 +66,7 @@ def test_shutdown(self): def test_export(self): with self._registry_register_patch: - record = MetricRecord( + record = ExportRecord( self._test_metric, self._labels_key, SumAggregator(), @@ -89,7 +89,7 @@ def test_min_max_sum_aggregator_to_prometheus(self): aggregator.update(123) aggregator.update(456) aggregator.take_checkpoint() - record = MetricRecord( + record = ExportRecord( metric, key_labels, aggregator, get_meter_provider().resource ) collector = CustomCollector("testprefix") @@ -107,7 +107,7 @@ def test_counter_to_prometheus(self): aggregator = SumAggregator() aggregator.update(123) aggregator.take_checkpoint() - record = MetricRecord( + record = ExportRecord( metric, key_labels, aggregator, get_meter_provider().resource ) collector = CustomCollector("testprefix") @@ -135,7 +135,7 @@ def test_invalid_metric(self): metric = StubMetric("tesname", "testdesc", "unit", int, meter) labels = {"environment": "staging"} key_labels = get_dict_as_key(labels) - record = MetricRecord( + record = ExportRecord( metric, key_labels, None, get_meter_provider().resource ) collector = CustomCollector("testprefix") diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 2a3ca9b586..47ae86852a 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,7 +2,10 @@ ## Unreleased -- Add optional parameter to `record_exception` method ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) +- Rename `MetricRecord` class to `ExportRecord` + ([#1367](https://github.com/open-telemetry/opentelemetry-python/pull/1367)) +- Add optional parameter to `record_exception` method + ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) - Update exception handling optional parameters, add escaped attribute to record_exception ([#1365](https://github.com/open-telemetry/opentelemetry-python/pull/1365)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index e7882217ec..60ae4b9466 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -25,7 +25,7 @@ class MetricsExportResult(Enum): FAILURE = 1 -class MetricRecord: +class ExportRecord: def __init__( self, instrument: metrics_api.InstrumentT, @@ -47,12 +47,12 @@ class MetricsExporter: """ def export( - self, metric_records: Sequence[MetricRecord] + self, export_records: Sequence[ExportRecord] ) -> "MetricsExportResult": """Exports a batch of telemetry data. Args: - metric_records: A sequence of `MetricRecord` s. A `MetricRecord` + export_records: A sequence of `ExportRecord` s. A `ExportRecord` contains the metric to be exported, the labels associated with that metric, as well as the aggregator used to export the current checkpointed value. @@ -76,16 +76,16 @@ class ConsoleMetricsExporter(MetricsExporter): """ def export( - self, metric_records: Sequence[MetricRecord] + self, export_records: Sequence[ExportRecord] ) -> "MetricsExportResult": - for record in metric_records: + for export_record in export_records: print( '{}(data="{}", labels="{}", value={}, resource={})'.format( type(self).__name__, - record.instrument, - record.labels, - record.aggregator.checkpoint, - record.resource.attributes, + export_record.instrument, + export_record.labels, + export_record.aggregator.checkpoint, + export_record.resource.attributes, ) ) return MetricsExportResult.SUCCESS diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py index e75fd350f7..7261a8f228 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py @@ -16,7 +16,7 @@ from typing import Sequence from opentelemetry.sdk.metrics.export import ( - MetricRecord, + ExportRecord, MetricsExporter, MetricsExportResult, ) @@ -41,13 +41,13 @@ def clear(self): self._exported_metrics.clear() def export( - self, metric_records: Sequence[MetricRecord] + self, export_records: Sequence[ExportRecord] ) -> MetricsExportResult: if self._stopped: return MetricsExportResult.FAILURE with self._lock: - self._exported_metrics.extend(metric_records) + self._exported_metrics.extend(export_records) return MetricsExportResult.SUCCESS def get_exported_metrics(self): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py index fa16f5f4ea..765c94e2c5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py @@ -14,7 +14,7 @@ from typing import Sequence -from opentelemetry.sdk.metrics.export import MetricRecord +from opentelemetry.sdk.metrics.export import ExportRecord from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util import get_dict_as_key @@ -36,22 +36,22 @@ def __init__(self, stateful: bool, resource: Resource): self.stateful = stateful self._resource = resource - def checkpoint_set(self) -> Sequence[MetricRecord]: - """Returns a list of MetricRecords used for exporting. + def checkpoint_set(self) -> Sequence[ExportRecord]: + """Returns a list of ExportRecords used for exporting. - The list of MetricRecords is a snapshot created from the current + The list of ExportRecords is a snapshot created from the current data in all of the aggregators in this processor. """ - metric_records = [] + export_records = [] # pylint: disable=W0612 for ( (instrument, aggregator_type, _, labels), aggregator, ) in self._batch_map.items(): - metric_records.append( - MetricRecord(instrument, labels, aggregator, self._resource) + export_records.append( + ExportRecord(instrument, labels, aggregator, self._resource) ) - return metric_records + return export_records def finished_collection(self): """Performs certain post-export logic. diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index a96c8af455..635409770d 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -22,7 +22,7 @@ from opentelemetry.sdk import metrics from opentelemetry.sdk.metrics.export import ( ConsoleMetricsExporter, - MetricRecord, + ExportRecord, ) from opentelemetry.sdk.metrics.export.aggregate import ( LastValueAggregator, @@ -52,7 +52,7 @@ def test_export(self): ) labels = {"environment": "staging"} aggregator = SumAggregator() - record = MetricRecord( + record = ExportRecord( metric, labels, aggregator, meter_provider.resource ) result = '{}(data="{}", labels="{}", value={}, resource={})'.format( From 3923c4d3bdec1de5b2cfd44909b96b6e9c7ca182 Mon Sep 17 00:00:00 2001 From: Azfaar Qureshi Date: Tue, 17 Nov 2020 10:24:22 -0500 Subject: [PATCH 0663/1517] Rename Record class in Metrics SDK to Accumulation to follow spec (#1373) --- opentelemetry-sdk/CHANGELOG.md | 1 + .../src/opentelemetry/sdk/metrics/__init__.py | 10 +++++----- .../tests/metrics/export/test_export.py | 12 ++++++------ 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 47ae86852a..55d4e01d3c 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -8,6 +8,7 @@ ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) - Update exception handling optional parameters, add escaped attribute to record_exception ([#1365](https://github.com/open-telemetry/opentelemetry-python/pull/1365)) +- Rename Record in Metrics SDK to Accumulation ([#1373](https://github.com/open-telemetry/opentelemetry-python/pull/1373)) ## Version 0.15b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 8207aa5755..5e6f9f0f6c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -325,7 +325,7 @@ class ValueObserver(Observer, metrics_api.ValueObserver): """See `opentelemetry.metrics.ValueObserver`.""" -class Record: +class Accumulation: """Container class used for processing in the `Processor`""" def __init__( @@ -382,10 +382,10 @@ def _collect_metrics(self) -> None: bound_instrument, ) in metric.bound_instruments.items(): for view_data in bound_instrument.view_datas: - record = Record( + accumulation = Accumulation( metric, view_data.labels, view_data.aggregator ) - self.processor.process(record) + self.processor.process(accumulation) if bound_instrument.ref_count() == 0: to_remove.append(labels) @@ -404,8 +404,8 @@ def _collect_observers(self) -> None: continue for labels, aggregator in observer.aggregators.items(): - record = Record(observer, labels, aggregator) - self.processor.process(record) + accumulation = Accumulation(observer, labels, aggregator) + self.processor.process(accumulation) def record_batch( self, diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 635409770d..632570a09d 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -139,8 +139,8 @@ def test_processor_process_exists(self): _batch_map[batch_key] = aggregator aggregator2.update(1.0) processor._batch_map = _batch_map - record = metrics.Record(metric, labels, aggregator2) - processor.process(record) + accumulation = metrics.Accumulation(metric, labels, aggregator2) + processor.process(accumulation) self.assertEqual(len(processor._batch_map), 1) self.assertIsNotNone(processor._batch_map.get(batch_key)) self.assertEqual(processor._batch_map.get(batch_key).current, 0) @@ -159,8 +159,8 @@ def test_processor_process_not_exists(self): batch_key = (metric, SumAggregator, tuple(), labels) aggregator.update(1.0) processor._batch_map = _batch_map - record = metrics.Record(metric, labels, aggregator) - processor.process(record) + accumulation = metrics.Accumulation(metric, labels, aggregator) + processor.process(accumulation) self.assertEqual(len(processor._batch_map), 1) self.assertIsNotNone(processor._batch_map.get(batch_key)) self.assertEqual(processor._batch_map.get(batch_key).current, 0) @@ -182,8 +182,8 @@ def test_processor_process_not_stateful(self): batch_key = (metric, SumAggregator, tuple(), labels) aggregator.update(1.0) processor._batch_map = _batch_map - record = metrics.Record(metric, labels, aggregator) - processor.process(record) + accumulation = metrics.Accumulation(metric, labels, aggregator) + processor.process(accumulation) self.assertEqual(len(processor._batch_map), 1) self.assertIsNotNone(processor._batch_map.get(batch_key)) self.assertEqual(processor._batch_map.get(batch_key).current, 0) From d556b90ffa28ab9f2e8a5dc0c8270ba409fbbbea Mon Sep 17 00:00:00 2001 From: Azfaar Qureshi Date: Tue, 17 Nov 2020 10:43:44 -0500 Subject: [PATCH 0664/1517] Renaming Meter to Accumulator in Metrics SDK context (#1372) --- opentelemetry-instrumentation/tests/test_metric.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 5 ++++- .../src/opentelemetry/sdk/metrics/__init__.py | 6 +++--- .../opentelemetry/sdk/metrics/export/controller.py | 12 ++++++------ .../opentelemetry/sdk/metrics/export/processor.py | 2 +- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/opentelemetry-instrumentation/tests/test_metric.py b/opentelemetry-instrumentation/tests/test_metric.py index 14c39d85e0..8e676c737e 100644 --- a/opentelemetry-instrumentation/tests/test_metric.py +++ b/opentelemetry-instrumentation/tests/test_metric.py @@ -41,7 +41,7 @@ def test_init(self): mixin = MetricMixin() mixin.init_metrics("test", 1.0) meter = mixin.meter - self.assertTrue(isinstance(meter, metrics.Meter)) + self.assertTrue(isinstance(meter, metrics.Accumulator)) self.assertEqual(meter.instrumentation_info.name, "test") self.assertEqual(meter.instrumentation_info.version, 1.0) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 55d4e01d3c..851073bcd5 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -8,7 +8,10 @@ ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) - Update exception handling optional parameters, add escaped attribute to record_exception ([#1365](https://github.com/open-telemetry/opentelemetry-python/pull/1365)) -- Rename Record in Metrics SDK to Accumulation ([#1373](https://github.com/open-telemetry/opentelemetry-python/pull/1373)) +- Rename Record in Metrics SDK to Accumulation + ([#1373](https://github.com/open-telemetry/opentelemetry-python/pull/1373)) +- Rename Meter class to Accumulator in Metrics SDK + ([#1372](https://github.com/open-telemetry/opentelemetry-python/pull/1372)) ## Version 0.15b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 5e6f9f0f6c..a2d16719a8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -145,7 +145,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], - meter: "Meter", + meter: "Accumulator", enabled: bool = True, ): self.name = name @@ -339,7 +339,7 @@ def __init__( self.aggregator = aggregator -class Meter(metrics_api.Meter): +class Accumulator(metrics_api.Meter): """See `opentelemetry.metrics.Meter`. Args: @@ -561,7 +561,7 @@ def get_meter( if not instrumenting_module_name: # Reject empty strings too. instrumenting_module_name = "ERROR:MISSING MODULE NAME" logger.error("get_meter called with missing module name.") - return Meter( + return Accumulator( self, InstrumentationInfo( instrumenting_module_name, instrumenting_library_version diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py index e095ebbb72..7c69468e61 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py @@ -26,7 +26,7 @@ class PushController(threading.Thread): exports them and performs some post-processing. Args: - meter: The meter used to collect metrics. + accumulator: The meter used to collect metrics. exporter: The exporter used to export metrics. interval: The collect/export interval in seconds. """ @@ -34,10 +34,10 @@ class PushController(threading.Thread): daemon = True def __init__( - self, meter: Meter, exporter: MetricsExporter, interval: float + self, accumulator: Meter, exporter: MetricsExporter, interval: float ): super().__init__() - self.meter = meter + self.accumulator = accumulator self.exporter = exporter self.interval = interval self.finished = threading.Event() @@ -54,10 +54,10 @@ def shutdown(self): def tick(self): # Collect all of the meter's metrics to be exported - self.meter.collect() + self.accumulator.collect() # Export the collected metrics token = attach(set_value("suppress_instrumentation", True)) - self.exporter.export(self.meter.processor.checkpoint_set()) + self.exporter.export(self.accumulator.processor.checkpoint_set()) detach(token) # Perform post-exporting logic - self.meter.processor.finished_collection() + self.accumulator.processor.finished_collection() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py index 765c94e2c5..5575ce1666 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py @@ -23,7 +23,7 @@ class Processor: """Base class for all processor types. The processor is responsible for storing the aggregators and aggregated - values received from updates from metrics in the meter. The stored values + values received from updates from metrics in the accumulator. The stored values will be sent to an exporter for exporting. """ From a085c106c27a973beb2b09c692013d7dc6688342 Mon Sep 17 00:00:00 2001 From: Abhilash Gnan Date: Wed, 18 Nov 2020 16:15:48 +0100 Subject: [PATCH 0665/1517] OTLP exporter: Handle error case when no credentials supplied (#1366) --- .../opentelemetry-exporter-otlp/CHANGELOG.md | 2 ++ .../opentelemetry/exporter/otlp/__init__.py | 2 +- .../opentelemetry/exporter/otlp/exporter.py | 24 ++++++++++++------- .../tests/test_otlp_metric_exporter.py | 4 ++++ .../tests/test_otlp_trace_exporter.py | 4 ++++ 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 4233ddded0..979d7c1909 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -4,6 +4,8 @@ - Add Gzip compression for exporter ([#1141](https://github.com/open-telemetry/opentelemetry-python/pull/1141)) +- OTLP exporter: Handle error case when no credentials supplied + ([#1366](https://github.com/open-telemetry/opentelemetry-python/pull/1366)) ## Version 0.15b0 Released 2020-11-02 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py index 3aca014eda..d005232d3e 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py @@ -53,7 +53,7 @@ trace.set_tracer_provider(TracerProvider(resource=resource))) tracer = trace.get_tracer(__name__) - otlp_exporter = OTLPSpanExporter(endpoint="localhost:55680") + otlp_exporter = OTLPSpanExporter(endpoint="localhost:55680", insecure=True) span_processor = BatchExportSpanProcessor(otlp_exporter) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index 77c0f0861e..54853f105c 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -201,15 +201,23 @@ def __init__( self._client = self._stub( insecure_channel(endpoint, compression=compression_algorithm) ) - else: - credentials = credentials or _load_credential_from_file( - Configuration().EXPORTER_OTLP_CERTIFICATE - ) - self._client = self._stub( - secure_channel( - endpoint, credentials, compression=compression_algorithm - ) + return + + # secure mode + if ( + credentials is None + and Configuration().EXPORTER_OTLP_CERTIFICATE is None + ): + raise ValueError("No credentials set in secure mode.") + + credentials = credentials or _load_credential_from_file( + Configuration().EXPORTER_OTLP_CERTIFICATE + ) + self._client = self._stub( + secure_channel( + endpoint, credentials, compression=compression_algorithm ) + ) @abstractmethod def _translate_data( diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index dd03c27d84..299406559c 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -91,6 +91,10 @@ def test_env_variables(self, mock_exporter_mixin): self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) + def test_no_credentials_error(self): + with self.assertRaises(ValueError): + OTLPMetricsExporter() + @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") def test_translate_metrics(self, mock_time_ns): # pylint: disable=no-member diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index 9c99056c44..742ab29bc9 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -183,6 +183,10 @@ def test_env_variables(self, mock_exporter_mixin): self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) + def test_no_credentials_error(self): + with self.assertRaises(ValueError): + OTLPSpanExporter() + @patch("opentelemetry.exporter.otlp.exporter.expo") @patch("opentelemetry.exporter.otlp.exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): From bcf7a2f3ee8b53c8ad6d6ccd5d907d53c694a4b7 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 18 Nov 2020 15:41:23 -0600 Subject: [PATCH 0666/1517] Change temporality for Counter and UpDownCounter to CUMULATIVE (#1384) Fixes #1383 --- .../opentelemetry-exporter-otlp/CHANGELOG.md | 2 + .../otlp/metrics_exporter/__init__.py | 76 +++++++++++++------ .../tests/test_otlp_metric_exporter.py | 11 ++- .../sdk/metrics/export/aggregate.py | 1 + 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 979d7c1909..e467d8f78e 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Change temporality for Counter and UpDownCounter + ([#1384](https://github.com/open-telemetry/opentelemetry-python/pull/1384)) - Add Gzip compression for exporter ([#1141](https://github.com/open-telemetry/opentelemetry-python/pull/1141)) - OTLP exporter: Handle error case when no credentials supplied diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py index c65b2a89df..c90dd47db2 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -15,8 +15,7 @@ """OTLP Metrics Exporter""" import logging -import os -from typing import List, Optional, Sequence, Type, TypeVar, Union +from typing import List, Optional, Sequence, Type, TypeVar from grpc import ChannelCredentials @@ -71,7 +70,9 @@ def _get_data_points( - export_record: ExportRecord, data_point_class: Type[DataPointT] + export_record: ExportRecord, + data_point_class: Type[DataPointT], + aggregation_temporality: int, ) -> List[DataPointT]: if isinstance(export_record.aggregator, SumAggregator): @@ -91,6 +92,15 @@ def _get_data_points( elif isinstance(export_record.aggregator, ValueObserverAggregator): value = export_record.aggregator.checkpoint.last + if aggregation_temporality == ( + AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE + ): + start_time_unix_nano = export_record.aggregator.first_timestamp + else: + start_time_unix_nano = ( + export_record.aggregator.initial_checkpoint_timestamp + ) + return [ data_point_class( labels=[ @@ -98,9 +108,7 @@ def _get_data_points( for label_key, label_value in export_record.labels ], value=value, - start_time_unix_nano=( - export_record.aggregator.initial_checkpoint_timestamp - ), + start_time_unix_nano=start_time_unix_nano, time_unix_nano=(export_record.aggregator.last_update_timestamp), ) ] @@ -215,25 +223,35 @@ def _translate_data( data_point_class = type_class[value_type]["data_point_class"] if isinstance(export_record.instrument, Counter): + + aggregation_temporality = ( + AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE + ) + otlp_metric_data = sum_class( data_points=_get_data_points( - export_record, data_point_class - ), - aggregation_temporality=( - AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA + export_record, + data_point_class, + aggregation_temporality, ), + aggregation_temporality=aggregation_temporality, is_monotonic=True, ) argument = type_class[value_type]["sum"]["argument"] elif isinstance(export_record.instrument, UpDownCounter): + + aggregation_temporality = ( + AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE + ) + otlp_metric_data = sum_class( data_points=_get_data_points( - export_record, data_point_class - ), - aggregation_temporality=( - AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA + export_record, + data_point_class, + aggregation_temporality, ), + aggregation_temporality=aggregation_temporality, is_monotonic=False, ) argument = type_class[value_type]["sum"]["argument"] @@ -243,25 +261,35 @@ def _translate_data( continue elif isinstance(export_record.instrument, SumObserver): + + aggregation_temporality = ( + AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE + ) + otlp_metric_data = sum_class( data_points=_get_data_points( - export_record, data_point_class - ), - aggregation_temporality=( - AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE + export_record, + data_point_class, + aggregation_temporality, ), + aggregation_temporality=aggregation_temporality, is_monotonic=True, ) argument = type_class[value_type]["sum"]["argument"] elif isinstance(export_record.instrument, UpDownSumObserver): + + aggregation_temporality = ( + AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE + ) + otlp_metric_data = sum_class( data_points=_get_data_points( - export_record, data_point_class - ), - aggregation_temporality=( - AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE + export_record, + data_point_class, + aggregation_temporality, ), + aggregation_temporality=aggregation_temporality, is_monotonic=False, ) argument = type_class[value_type]["sum"]["argument"] @@ -269,7 +297,9 @@ def _translate_data( elif isinstance(export_record.instrument, (ValueObserver)): otlp_metric_data = gauge_class( data_points=_get_data_points( - export_record, data_point_class + export_record, + data_point_class, + AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA, ) ) argument = type_class[value_type]["gauge"]["argument"] diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index 299406559c..4f121b38fd 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -46,7 +46,9 @@ class TestOTLPMetricExporter(TestCase): - def setUp(self): + @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") + def setUp(self, mock_time_ns): # pylint: disable=arguments-differ + mock_time_ns.configure_mock(**{"return_value": 1}) self.exporter = OTLPMetricsExporter(insecure=True) resource = SDKResource(OrderedDict([("a", 1), ("b", False)])) @@ -95,12 +97,9 @@ def test_no_credentials_error(self): with self.assertRaises(ValueError): OTLPMetricsExporter() - @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") - def test_translate_metrics(self, mock_time_ns): + def test_translate_metrics(self): # pylint: disable=no-member - mock_time_ns.configure_mock(**{"return_value": 1}) - self.counter_export_record.aggregator.checkpoint = 1 self.counter_export_record.aggregator.initial_checkpoint_timestamp = 1 self.counter_export_record.aggregator.last_update_timestamp = 1 @@ -137,7 +136,7 @@ def test_translate_metrics(self, mock_time_ns): ) ], aggregation_temporality=( - AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA + AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE ), is_monotonic=True, ), diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 84ab518a47..3781b9146b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -34,6 +34,7 @@ def __init__(self, config=None): self._lock = threading.Lock() self.last_update_timestamp = 0 self.initial_checkpoint_timestamp = 0 + self.first_timestamp = time_ns() self.checkpointed = True if config is not None: self.config = config From 3b813eb9921e709538dd1b07fa7a5f93600fbec1 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Wed, 18 Nov 2020 15:24:07 -0800 Subject: [PATCH 0667/1517] Workflow for PRs that would affect Contrib (#1382) --- CONTRIBUTING.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39d720d2db..1cc74de395 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,6 +100,24 @@ $ git push fork feature Open a pull request against the main `opentelemetry-python` repo. +Pull requests are also tested for their compatibility with packages distributed +by OpenTelemetry in the [OpenTelemetry Python Contrib Repository](https://github.com/open-telemetry/opentelemetry-python.git). + +If a pull request (PR) introduces a change that would break the compatibility of +these packages with the Core packages in this repo, a separate PR should be +opened in the Contrib repo with changes to make the packages compatible. + +Follow these steps: +1. Open Core repo PR (Contrib Tests will fail) +2. Open Contrib repo PR and modify its `CORE_REPO_SHA` in `.github/workflows/test.yml` +to equal the commit SHA of the Core repo PR to pass tests +3. Modify the Core repo PR `CONTRIB_REPO_SHA` in `.github/workflows/test.yml` to +equal the commit SHA of the Contrib repo PR to pass Contrib repo tests (a sanity +check for the Maintainers & Approvers) +4. Merge the Contrib repo +5. Restore the Core repo PR `CONTRIB_REPO_SHA` to point to `master` +6. Merge the Core repo PR + ### How to Receive Comments * If the PR is not ready for review, please put `[WIP]` in the title, tag it @@ -113,6 +131,8 @@ A PR is considered to be **ready to merge** when: / [Maintainers](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer) (at different companies). * Major feedbacks are resolved. +* All tests are passing, including Contrib Repo tests which may require +updating the GitHub workflow to reference a PR in the Contrib repo * It has been open for review for at least one working day. This gives people reasonable time to review. * Trivial change (typo, cosmetic, doc, etc.) doesn't have to wait for one day. From 9a754ada14a4b50232cf0b5edea0c054117f982c Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Wed, 18 Nov 2020 22:55:51 -0800 Subject: [PATCH 0668/1517] Fix syntax for Docs Build Workflow (#1392) --- .github/workflows/docs.yml | 32 ++++++++++++++++---------------- docs-requirements.txt | 7 +++++++ tox.ini | 10 ---------- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index fc9a8392ad..5dcff55d6a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,24 +17,24 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} + - uses: actions/checkout@v2 + - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@v2 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} path: opentelemetry-python-contrib - - name: Set up Python py38 - uses: actions/setup-python@v2 - with: - python-version: '3.8' - - name: Build docs - run: | - pip install --upgrade tox - tox -e docs - - name: Publish to gh-pages - uses: JamesIves/github-pages-deploy-action@2.0.2 - env: - ACCESS_TOKEN: ${{ secrets.DocsPushToken }} - BRANCH: gh-pages - FOLDER: docs/_build/html/ + - name: Set up Python py38 + uses: actions/setup-python@v2 + with: + python-version: '3.8' + - name: Build docs + run: | + pip install --upgrade tox + tox -e docs + - name: Publish to gh-pages + uses: JamesIves/github-pages-deploy-action@2.0.2 + env: + ACCESS_TOKEN: ${{ secrets.DocsPushToken }} + BRANCH: gh-pages + FOLDER: docs/_build/html/ diff --git a/docs-requirements.txt b/docs-requirements.txt index a316886277..9a17569205 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -6,6 +6,13 @@ sphinx-autodoc-typehints~=1.10.2 # doesn't work for pkg_resources. ./opentelemetry-api ./opentelemetry-sdk +./opentelemetry-python-contrib/exporter/opentelemetry-exporter-datadog +./opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-grpc +./opentelemetry-instrumentation +./opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests +./opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi +./opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-django +./opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask # Required by ext packages asgiref~=3.0 diff --git a/tox.ini b/tox.ini index ac5e6079f8..67790b2be8 100644 --- a/tox.ini +++ b/tox.ini @@ -154,19 +154,9 @@ commands = [testenv:docs] deps = - -e {toxinidir}/opentelemetry-python-contrib/exporter/opentelemetry-exporter-datadog - -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-grpc - -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask - -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-django - -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests - -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -c {toxinidir}/dev-requirements.txt -r {toxinidir}/docs-requirements.txt -commands_pre = - pip install -e {toxinidir}/opentelemetry-api \ - -e {toxinidir}/opentelemetry-sdk - changedir = docs commands = From 2c1ee67ef960181810215bc30bc3e4e13f93df95 Mon Sep 17 00:00:00 2001 From: Tom Tan Date: Wed, 18 Nov 2020 23:00:14 -0800 Subject: [PATCH 0669/1517] Fix typo of TraceProvider in changelog (#1387) --- opentelemetry-api/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index b231b5aed5..a957b5e844 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -147,7 +147,7 @@ Released 2020-03-16 ([#431](https://github.com/open-telemetry/opentelemetry-python/pull/431)) - Renaming TraceOptions to TraceFlags ([#450](https://github.com/open-telemetry/opentelemetry-python/pull/450)) -- Renaming TracerSource to TraceProvider +- Renaming TracerSource to TracerProvider ([#441](https://github.com/open-telemetry/opentelemetry-python/pull/441)) - Adding attach/detach methods as per spec ([#429](https://github.com/open-telemetry/opentelemetry-python/pull/450) From c60aa693be7814571fa332039d77dcf4cc7585ef Mon Sep 17 00:00:00 2001 From: snyder114 <31932630+snyder114@users.noreply.github.com> Date: Thu, 19 Nov 2020 08:55:21 -0800 Subject: [PATCH 0670/1517] Edits to getting started docs (#1368) --- docs/getting-started.rst | 120 +++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 61 deletions(-) diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 8129dfeac6..7e0ab3c312 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -1,11 +1,11 @@ Getting Started with OpenTelemetry Python ========================================= -This guide will walk you through instrumenting a Python application with ``opentelemetry-python``. +This guide walks you through instrumenting a Python application with ``opentelemetry-python``. -For more elaborate examples, see `examples`. +For more elaborate examples, see `examples `_. -Hello world: emitting a trace to your console +Hello world: emit a trace to your console --------------------------------------------- To get started, install both the opentelemetry API and SDK: @@ -18,21 +18,21 @@ To get started, install both the opentelemetry API and SDK: The API package provides the interfaces required by the application owner, as well as some helper logic to load implementations. -The SDK provides an implementation of those interfaces, designed to be generic and extensible enough -that in many situations, the SDK will be sufficient. +The SDK provides an implementation of those interfaces. The implementation is designed to be generic and extensible enough +that in many situations, the SDK is sufficient. -Once installed, we can now utilize the packages to emit spans from your application. A span +Once installed, you can use the packages to emit spans from your application. A span represents an action within your application that you want to instrument, such as an HTTP request -or a database call. Once instrumented, the application owner can extract helpful information such as -how long the action took, or add arbitrary attributes to the span that may provide more insight for debugging. +or a database call. Once instrumented, you can extract helpful information such as +how long the action took. You can also add arbitrary attributes to the span that provide more insight for debugging. -Here's an example of a script that emits a trace containing three named spans: "foo", "bar", and "baz": +The following example script emits a trace containing three named spans: "foo", "bar", and "baz": .. literalinclude:: getting_started/tracing_example.py :language: python :lines: 15- -We can run it, and see the traces print to your console: +When you run the script you can see the traces printed to your console: .. code-block:: sh @@ -94,65 +94,64 @@ We can run it, and see the traces print to your console: Each span typically represents a single operation or unit of work. Spans can be nested, and have a parent-child relationship with other spans. -While a given span is active, newly-created spans will inherit the active span's trace ID, options, and other attributes of its context. -A span without a parent is called the "root span", and a trace is comprised of one root span and its descendants. +While a given span is active, newly-created spans inherit the active span's trace ID, options, and other attributes of its context. +A span without a parent is called the root span, and a trace is comprised of one root span and its descendants. -In the example above, the OpenTelemetry Python library creates one trace containing three spans and prints it to STDOUT. +In this example, the OpenTelemetry Python library creates one trace containing three spans and prints it to STDOUT. Configure exporters to emit spans elsewhere ------------------------------------------- -The example above does emit information about all spans, but the output is a bit hard to read. -In common cases, you would instead *export* this data to an application performance monitoring backend, to be visualized and queried. -It is also common to aggregate span and trace information from multiple services into a single database, so that actions that require multiple services can still all be visualized together. +The previous example does emit information about all spans, but the output is a bit hard to read. +In most cases, you can instead *export* this data to an application performance monitoring backend to be visualized and queried. +It's also common to aggregate span and trace information from multiple services into a single database, so that actions requiring multiple services can still all be visualized together. -This concept is known as distributed tracing. One such distributed tracing backend is known as Jaeger. +This concept of aggregating span and trace information is known as distributed tracing. One such distributed tracing backend is known as Jaeger. The Jaeger project provides an all-in-one Docker container with a UI, database, and consumer. -The Jaeger project provides an all-in-one docker container that provides a UI, database, and consumer. Let's bring -it up now: +Run the following command to start Jaeger: .. code-block:: sh docker run -p 16686:16686 -p 6831:6831/udp jaegertracing/all-in-one -This will start Jaeger on port 16686 locally, and expose Jaeger thrift agent on port 6831. You can visit it at http://localhost:16686. +This command starts Jaeger locally on port 16686 and exposes the Jaeger thrift agent on port 6831. You can visit Jaeger at http://localhost:16686. -With this backend up, your application will now need to export traces to this system. ``opentelemetry-sdk`` does not provide an exporter -for Jaeger, but you can install that as a separate package: +After you spin up the backend, your application needs to export traces to this system. Although ``opentelemetry-sdk`` doesn't provide an exporter +for Jaeger, you can install it as a separate package with the following command: .. code-block:: sh pip install opentelemetry-exporter-jaeger -Once installed, update your code to import the Jaeger exporter, and use that instead: +After you install the exporter, update your code to import the Jaeger exporter and use that instead: .. literalinclude:: getting_started/jaeger_example.py :language: python :lines: 15- -Run the script: +Finally, run the Python script: .. code-block:: python python jaeger_example.py -You can then visit the jaeger UI, see you service under "services", and find your traces! +You can then visit the Jaeger UI, see your service under "services", and find your traces! .. image:: images/jaeger_trace.png -Integrations example with Flask -------------------------------- +Instrumentation example with Flask +------------------------------------ -The above is a great example, but it's very manual. Within the telemetry space, there are common actions that one wants to instrument: +While the example in the previous section is great, it's very manual. The following are common actions you might want to track and include as part of your distributed tracing. * HTTP responses from web services * HTTP requests from clients * Database calls -To help instrument common scenarios, opentelemetry also has the concept of "instrumentations": packages that are designed to interface -with a specific framework or library, such as Flask and psycopg2. A list of the currently curated extension packages can be found `at the Contrib repo `_. +To track these common actions, OpenTelemetry has the concept of instrumentations. Instrumentations are packages designed to interface +with a specific framework or library, such as Flask and psycopg2. You can find a list of the currently curated extension packages in the `Contrib repository `_. -We will now instrument a basic Flask application that uses the requests library to send HTTP requests. First, install the instrumentation packages themselves: +Instrument a basic Flask application that uses the requests library to send HTTP requests. First, install the instrumentation packages themselves: .. code-block:: sh @@ -160,33 +159,32 @@ We will now instrument a basic Flask application that uses the requests library pip install opentelemetry-instrumentation-requests -And let's write a small Flask application that sends an HTTP request, activating each instrumentation during the initialization: +The following small Flask application sends an HTTP request and also activates each instrumentation during its initialization: .. literalinclude:: getting_started/flask_example.py :language: python :lines: 15- -Now run the above script, hit the root url (http://localhost:5000/) a few times, and watch your spans be emitted! +Now run the script, hit the root URL (http://localhost:5000/) a few times, and watch your spans be emitted! .. code-block:: sh python flask_example.py -Configure Your HTTP Propagator (b3, Baggage) +Configure Your HTTP propagator (b3, Baggage) ------------------------------------------------------- A major feature of distributed tracing is the ability to correlate a trace across multiple services. However, those services need to propagate information about a trace from one service to the other. -To enable this, OpenTelemetry has the concept of `propagators `_, -which provide a common method to encode and decode span information from a request and response, -respectively. +To enable this propagation, OpenTelemetry has the concept of `propagators `_, +which provide a common method to encode and decode span information from a request and response, respectively. -By default, opentelemetry-python is configured to use the `W3C Trace Context `_ -HTTP headers for HTTP requests. This can be configured to leverage different propagators. Here's +By default, ``opentelemetry-python`` is configured to use the `W3C Trace Context `_ +HTTP headers for HTTP requests, but you can configure it to leverage different propagators. Here's an example using Zipkin's `b3 propagation `_: .. code-block:: python @@ -197,24 +195,24 @@ an example using Zipkin's `b3 propagation `_. +It's valuable to have a data store for metrics so you can visualize and query the data. A common solution is +`Prometheus `_, which provides a server to scrape and store time series data. -Let's start by bringing up a Prometheus instance ourselves, to scrape our application. Write the following configuration: +Start by bringing up a Prometheus instance to scrape your application. Write the following configuration: .. code-block:: yaml @@ -239,7 +237,7 @@ Let's start by bringing up a Prometheus instance ourselves, to scrape our applic static_configs: - targets: ['localhost:8000'] -And start a docker container for it: +Then start a Docker container for the instance: .. code-block:: sh @@ -247,35 +245,35 @@ And start a docker container for it: docker run --net=host -v /tmp/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus \ --log.level=debug --config.file=/etc/prometheus/prometheus.yml -For our Python application, we will need to install an exporter specific to Prometheus: +Install an exporter specific to Prometheus for your Python application: .. code-block:: sh pip install opentelemetry-exporter-prometheus -And use that instead of the `ConsoleMetricsExporter`: +Use that exporter instead of the `ConsoleMetricsExporter`: .. literalinclude:: getting_started/prometheus_example.py :language: python :lines: 15- -The Prometheus server will run locally on port 8000, and the instrumented code will make metrics available to Prometheus via the `PrometheusMetricsExporter`. +The Prometheus server runs locally on port 8000. The instrumented code makes metrics available to Prometheus via the `PrometheusMetricsExporter`. Visit the Prometheus UI (http://localhost:9090) to view your metrics. -Using the OpenTelemetry Collector for traces and metrics +Use the OpenTelemetry Collector for traces and metrics -------------------------------------------------------- -Although it's possible to directly export your telemetry data to specific backends, you may more complex use cases, including: +Although it's possible to directly export your telemetry data to specific backends, you might have more complex use cases such as the following: -* having a single telemetry sink shared by multiple services, to reduce overhead of switching exporters -* aggregating metrics or traces across multiple services, running on multiple hosts +* A single telemetry sink shared by multiple services, to reduce overhead of switching exporters. +* Aggregaing metrics or traces across multiple services, running on multiple hosts. To enable a broad range of aggregation strategies, OpenTelemetry provides the `opentelemetry-collector `_. The Collector is a flexible application that can consume trace and metric data and export to multiple other backends, including to another instance of the Collector. -To see how this works in practice, let's start the Collector locally. Write the following file: +Start the Collector locally to see how the Collector works in practice. Write the following file: .. code-block:: yaml @@ -299,7 +297,7 @@ To see how this works in practice, let's start the Collector locally. Write the receivers: [opencensus] exporters: [logging] -Start the docker container: +Then start the Docker container: .. code-block:: sh @@ -314,7 +312,7 @@ Install the OpenTelemetry Collector exporter: pip install opentelemetry-exporter-otlp -And execute the following script: +Finally, execute the following script: .. literalinclude:: getting_started/otlpcollector_example.py :language: python From b5c766a6401de3690694aae0d3ca81d17783ec0a Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Thu, 19 Nov 2020 12:49:31 -0800 Subject: [PATCH 0671/1517] Add Contrib Repo Tests workflow on Core PRs (#1357) --- .github/workflows/test.yml | 93 ++++++++++++++++++- CONTRIBUTING.md | 4 + .../opentelemetry-example-app/setup.cfg | 1 + eachdist.ini | 4 - tox.ini | 2 - 5 files changed, 94 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d21961ee5..b54c1841a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Test +name: Core Repo Tests on: push: @@ -6,7 +6,11 @@ on: - 'release/*' pull_request: env: - CONTRIB_REPO_SHA: 5c9e043d6921550d82668788e3758a733fb11cb8 + # Set variable to 'master' if your change will not affect Contrib. + # Otherwise, set variable to the commit of your branch on + # opentelemetry-python-contrib which is compatible with these Core repo + # changes. + CONTRIB_REPO_SHA: master jobs: build: @@ -62,7 +66,7 @@ jobs: uses: actions/cache@v2 with: path: .tox - key: tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }} + key: tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core - name: run tox run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} misc: @@ -92,6 +96,87 @@ jobs: uses: actions/cache@v2 with: path: .tox - key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }} + key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core + - name: run tox + run: tox -e ${{ matrix.tox-environment }} + contrib-build: + env: + # We use these variables to convert between tox and GHA version literals + py35: 3.5 + py36: 3.6 + py37: 3.7 + py38: 3.8 + pypy3: pypy3 + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false # ensures the entire test matrix is run, even if one permutation fails + matrix: + python-version: [ py35, py36, py37, py38, pypy3 ] + package: ["instrumentation", "exporter"] + os: [ ubuntu-latest ] + include: + # py35-instrumentation segfaults on 18.04 so we instead run on 20.04 + - python-version: py35 + package: instrumentation + os: ubuntu-20.04 + exclude: + - os: ubuntu-latest + python-version: py35 + package: instrumentation + steps: + - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} + uses: actions/checkout@v2 + with: + repository: open-telemetry/opentelemetry-python-contrib + ref: ${{ env.CONTRIB_REPO_SHA }} + - name: Checkout Core Repo @ SHA ${{ github.sha }} + uses: actions/checkout@v2 + with: + repository: open-telemetry/opentelemetry-python + path: opentelemetry-python-core + - name: Set up Python ${{ env[matrix.python-version] }} + uses: actions/setup-python@v2 + with: + python-version: ${{ env[matrix.python-version] }} + - name: Install tox + run: pip install -U tox-factor + - name: Cache tox environment + # Preserves .tox directory between runs for faster installs + uses: actions/cache@v2 + with: + path: .tox + key: tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib + - name: run tox + run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} + contrib-misc: + strategy: + fail-fast: false + matrix: + tox-environment: [ "docker-tests"] + name: ${{ matrix.tox-environment }} + runs-on: ubuntu-latest + steps: + - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} + uses: actions/checkout@v2 + with: + repository: open-telemetry/opentelemetry-python-contrib + ref: ${{ env.CONTRIB_REPO_SHA }} + - name: Checkout Core Repo @ SHA ${{ github.sha }} + uses: actions/checkout@v2 + with: + repository: open-telemetry/opentelemetry-python + path: opentelemetry-python-core + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install tox + run: pip install -U tox + - name: Cache tox environment + # Preserves .tox directory between runs for faster installs + uses: actions/cache@v2 + with: + path: .tox + key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib - name: run tox run: tox -e ${{ matrix.tox-environment }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1cc74de395..4981eb5547 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,6 +42,10 @@ To quickly get up and running, you can use the `scripts/eachdist.py` tool that ships with this project. First create a virtualenv and activate it. Then run `python scripts/eachdist.py develop` to install all required packages as well as the project's packages themselves (in `--editable` mode). + +Further, you'll want to clone the Contrib repo locally to resolve paths needed +to run tests. `git clone git@github.com:open-telemetry/opentelemetry-python-contrib.git opentelemetry-python-contrib`. + You can then run `scripts/eachdist.py test` to test everything or `scripts/eachdist.py lint` to lint everything (fixing anything that is auto-fixable). diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg index b8b981d358..87753ea720 100644 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ b/docs/examples/opentelemetry-example-app/setup.cfg @@ -45,6 +45,7 @@ install_requires = opentelemetry-sdk == 0.16.dev0 opentelemetry-instrumentation-requests == 0.15.b0 opentelemetry-instrumentation-flask == 0.15.b0 + opentelemetry-instrumentation-grpc == 0.15.b0 flask requests protobuf>=3.13.0 diff --git a/eachdist.ini b/eachdist.ini index 1b809374ad..9cef826d20 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -10,10 +10,6 @@ sortfirst= opentelemetry-instrumentation opentelemetry-proto tests/util - instrumentation/opentelemetry-instrumentation-wsgi - instrumentation/opentelemetry-instrumentation-dbapi - instrumentation/opentelemetry-instrumentation-asgi - instrumentation/opentelemetry-instrumentation-botocore instrumentation/* exporter/* ext/* diff --git a/tox.ini b/tox.ini index 67790b2be8..a42174f357 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,6 @@ envlist = ; opentelemetry-example-app py3{5,6,7,8}-test-core-example-app - pypy3-test-core-example-app ; opentelemetry-exporter-jaeger py3{5,6,7,8}-test-exporter-jaeger @@ -135,7 +134,6 @@ commands = basepython: python3.8 recreate = True deps = - -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-grpc -c dev-requirements.txt asgiref pylint From 8660deaa22fb08674229a95ffb858cfa48d5bfa7 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Sat, 21 Nov 2020 00:46:29 +0530 Subject: [PATCH 0672/1517] Add true auto-instrumentation support to opentelemetry-instrument (#1036) This commit extends the instrument command so it automatically configures tracing with a provider, span processor and exporter. Most of the component used can be customized with env vars or CLI arguments. Details can be found on opentelemetry-instrumentation's README package. Fixes #663 --- .../opentelemetry-exporter-jaeger/setup.cfg | 4 + .../setup.cfg | 4 + .../opentelemetry-exporter-otlp/setup.cfg | 5 + .../setup.cfg | 4 + .../opentelemetry-exporter-zipkin/setup.cfg | 4 + opentelemetry-instrumentation/CHANGELOG.md | 4 +- opentelemetry-instrumentation/README.rst | 68 ++++++++-- .../auto_instrumentation/__init__.py | 58 ++++++++- .../auto_instrumentation/components.py | 121 ++++++++++++++++++ .../auto_instrumentation/sitecustomize.py | 41 +++++- .../tests/test_auto_tracing.py | 106 +++++++++++++++ .../tests/test_run.py | 38 ++++-- 12 files changed, 429 insertions(+), 28 deletions(-) create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py create mode 100644 opentelemetry-instrumentation/tests/test_auto_tracing.py diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index b13b95653b..66d0d61cbb 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -48,3 +48,7 @@ where = src [options.extras_require] test = + +[options.entry_points] +opentelemetry_exporter = + jaeger = opentelemetry.exporter.jaeger:JaegerSpanExporter \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index a79a6b7e6d..ad470e2b19 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -50,3 +50,7 @@ where = src [options.extras_require] test = + +[options.entry_points] +opentelemetry_exporter = + opencensus = opentelemetry.exporter.opencensus.trace_exporter:OpenCensusSpanExporter \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 54f6fc7772..7ba1b5c1f9 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -52,3 +52,8 @@ test = [options.packages.find] where = src + +[options.entry_points] +opentelemetry_exporter = + otlp_span = opentelemetry.exporter.otlp.trace_exporter:OTLPSpanExporter + otlp_metric = opentelemetry.exporter.otlp.metrics_exporter:OTLPMetricsExporter \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index 182ffec5c3..540f9fcc9e 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -48,3 +48,7 @@ where = src [options.extras_require] test = + +[options.entry_points] +opentelemetry_exporter = + prometheus = opentelemetry.exporter.prometheus:PrometheusMetricsExporter \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index b4b3ee207b..310ddeb6a4 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -48,3 +48,7 @@ where = src [options.extras_require] test = + +[options.entry_points] +opentelemetry_exporter = + zipkin = opentelemetry.exporter.zipkin:ZipkinSpanExporter \ No newline at end of file diff --git a/opentelemetry-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md index da275415d7..d59bc01c75 100644 --- a/opentelemetry-instrumentation/CHANGELOG.md +++ b/opentelemetry-instrumentation/CHANGELOG.md @@ -2,11 +2,13 @@ ## Unreleased +- Added support for `OTEL_EXPORTER` to the `opentelemetry-instrument` command ([#1036](https://github.com/open-telemetry/opentelemetry-python/pull/1036)) + ## Version 0.14b0 Released 2020-10-13 -- Fixed boostrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask +- Fixed bootstrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask. ([#1138](https://github.com/open-telemetry/opentelemetry-python/pull/1138)) ## Version 0.13b0 diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index 6be744251b..22f2228016 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -16,6 +16,21 @@ Installation This package provides a couple of commands that help automatically instruments a program: + +opentelemetry-bootstrap +----------------------- + +:: + + opentelemetry-bootstrap --action=install|requirements + +This commands inspects the active Python site-packages and figures out which +instrumentation packages the user might want to install. By default it prints out +a list of the suggested instrumentation packages which can be added to a requirements.txt +file. It also supports installing the suggested packages when run with :code:`--action=install` +flag. + + opentelemetry-instrument ------------------------ @@ -23,23 +38,60 @@ opentelemetry-instrument opentelemetry-instrument python program.py +The instrument command will try to automatically detect packages used by your python program +and when possible, apply automatic tracing instrumentation on them. This means your program +will get automatic distributed tracing for free without having to make any code changes +at all. This will also configure a global tracer and tracing exporter without you having to +make any code changes. By default, the instrument command will use the OTLP exporter but +this can be overriden when needed. + +The command supports the following configuration options as CLI arguments and environment vars: + + +* ``--exporter`` or ``OTEL_EXPORTER`` + +Used to specify which trace exporter to use. Can be set to one or more +of the well-known exporter names (see below). + + - Defaults to `otlp`. + - Can be set to `none` to disable automatic tracer initialization. + +You can pass multiple values to configure multiple exporters e.g, ``zipkin,prometheus`` + +Well known trace exporter names: + + - jaeger + - opencensus + - otlp + - otlp_span + - otlp_metric + - zipkin + +``otlp`` is an alias for ``otlp_span,otlp_metric``. + +* ``--service-name`` or ``OTEL_SERVICE_NAME`` + +When present the value is passed on to the relevant exporter initializer as ``service_name`` argument. + The code in ``program.py`` needs to use one of the packages for which there is an OpenTelemetry integration. For a list of the available integrations please check `here `_ +Examples +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -opentelemetry-bootstrap ------------------------ +:: + + opentelemetry-instrument -e otlp flask run --port=3000 + +The above command will pass ``-e otlp`` to the instrument command and ``--port=3000`` to ``flask run``. :: - opentelemetry-bootstrap --action=install|requirements + opentelemetry-instrument -e zipkin,otlp celery -A tasks worker --loglevel=info -This commands inspects the active Python site-packages and figures out which -instrumentation packages the user might want to install. By default it prints out -a list of the suggested instrumentation packages which can be added to a requirements.txt -file. It also supports installing the suggested packages when run with :code:`--action=install` -flag. +The above command will configure global trace provider, attach zipkin and otlp exporters to it and then +start celery with the rest of the arguments. References ---------- diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index 893b8939b9..51af6dffd8 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -14,16 +14,67 @@ # See the License for the specific language governing permissions and # limitations under the License. +import argparse from logging import getLogger from os import environ, execl, getcwd from os.path import abspath, dirname, pathsep from shutil import which -from sys import argv logger = getLogger(__file__) +def parse_args(): + parser = argparse.ArgumentParser( + description=""" + opentelemetry-instrument automatically instruments a Python + program and it's dependencies and then runs the program. + """ + ) + + parser.add_argument( + "-e", + "--exporter", + required=False, + help=""" + Uses the specified exporter to export spans or metrics. + Accepts multiple exporters as comma separated values. + + Examples: + + -e=otlp + -e=otlp_span,prometheus + -e=jaeger,otlp_metric + """, + ) + + parser.add_argument( + "-s", + "--service-name", + required=False, + help=""" + The service name that should be passed to a trace exporter. + """, + ) + + parser.add_argument("command", help="Your Python application.") + parser.add_argument( + "command_args", + help="Arguments for your application.", + nargs=argparse.REMAINDER, + ) + return parser.parse_args() + + +def load_config_from_cli_args(args): + if args.exporter: + environ["OTEL_EXPORTER"] = args.exporter + if args.service_name: + environ["OTEL_SERVICE_NAME"] = args.service_name + + def run() -> None: + args = parse_args() + load_config_from_cli_args(args) python_path = environ.get("PYTHONPATH") @@ -49,6 +100,5 @@ def run() -> None: environ["PYTHONPATH"] = pathsep.join(python_path) - executable = which(argv[1]) - - execl(executable, executable, *argv[2:]) + executable = which(args.command) + execl(executable, executable, *args.command_args) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py new file mode 100644 index 0000000000..105a41722c --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py @@ -0,0 +1,121 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from logging import getLogger +from typing import Sequence, Tuple + +from pkg_resources import iter_entry_points + +from opentelemetry import trace +from opentelemetry.configuration import Configuration +from opentelemetry.sdk.metrics.export import MetricsExporter +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + BatchExportSpanProcessor, + SpanExporter, +) + +logger = getLogger(__file__) + +EXPORTER_OTLP = "otlp" +EXPORTER_OTLP_SPAN = "otlp_span" +EXPORTER_OTLP_METRIC = "otlp_metric" +_DEFAULT_EXPORTER = EXPORTER_OTLP + + +def get_service_name() -> str: + return Configuration().SERVICE_NAME or "" + + +def get_exporter_names() -> Sequence[str]: + exporter = Configuration().EXPORTER or _DEFAULT_EXPORTER + if exporter.lower().strip() == "none": + return [] + + names = [] + for exp in exporter.split(","): + name = exp.strip() + if name == EXPORTER_OTLP: + names.append(EXPORTER_OTLP_SPAN) + names.append(EXPORTER_OTLP_METRIC) + else: + names.append(name) + return names + + +def init_tracing(exporters: Sequence[SpanExporter]): + service_name = get_service_name() + provider = TracerProvider( + resource=Resource.create({"service.name": service_name}), + ) + trace.set_tracer_provider(provider) + + for exporter_name, exporter_class in exporters.items(): + exporter_args = {} + if exporter_name not in [ + EXPORTER_OTLP, + EXPORTER_OTLP_SPAN, + ]: + exporter_args["service_name"] = service_name + + provider.add_span_processor( + BatchExportSpanProcessor(exporter_class(**exporter_args)) + ) + + +def init_metrics(exporters: Sequence[MetricsExporter]): + if exporters: + logger.warning("automatic metric initialization is not supported yet.") + + +def import_exporters( + exporter_names: Sequence[str], +) -> Tuple[Sequence[SpanExporter], Sequence[MetricsExporter]]: + trace_exporters, metric_exporters = {}, {} + + exporters = { + ep.name: ep for ep in iter_entry_points("opentelemetry_exporter") + } + + for exporter_name in exporter_names: + entry_point = exporters.get(exporter_name, None) + if not entry_point: + raise RuntimeError( + "Requested exporter not found: {0}".format(exporter_name) + ) + + exporter_impl = entry_point.load() + if issubclass(exporter_impl, SpanExporter): + trace_exporters[exporter_name] = exporter_impl + elif issubclass(exporter_impl, MetricsExporter): + metric_exporters[exporter_name] = exporter_impl + else: + raise RuntimeError( + "{0} is neither a trace exporter nor a metric exporter".format( + exporter_name + ) + ) + return trace_exporters, metric_exporters + + +def initialize_components(): + exporter_names = get_exporter_names() + trace_exporters, metric_exporters = import_exporters(exporter_names) + init_tracing(trace_exporters) + + # We don't support automatic initialization for metric yet but have added + # some boilerplate in order to make sure current implementation does not + # lock us out of supporting metrics later without major surgery. + init_metrics(metric_exporters) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index b070bf5d77..c56d86c7f1 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -12,17 +12,48 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +import sys from logging import getLogger from pkg_resources import iter_entry_points +from opentelemetry.instrumentation.auto_instrumentation.components import ( + initialize_components, +) + logger = getLogger(__file__) -for entry_point in iter_entry_points("opentelemetry_instrumentor"): - try: - entry_point.load()().instrument() # type: ignore - logger.debug("Instrumented %s", entry_point.name) +def auto_instrument(): + for entry_point in iter_entry_points("opentelemetry_instrumentor"): + try: + entry_point.load()().instrument() # type: ignore + logger.debug("Instrumented %s", entry_point.name) + except Exception as exc: # pylint: disable=broad-except + logger.exception("Instrumenting of %s failed", entry_point.name) + raise exc + +def initialize(): + try: + initialize_components() + auto_instrument() except Exception: # pylint: disable=broad-except - logger.exception("Instrumenting of %s failed", entry_point.name) + logger.exception("Failed to auto initialize opentelemetry") + + +if ( + hasattr(sys, "argv") + and sys.argv[0].split(os.path.sep)[-1] == "celery" + and "worker" in sys.argv[1:] +): + from celery.signals import worker_process_init # pylint:disable=E0401 + + @worker_process_init.connect(weak=False) + def init_celery(*args, **kwargs): + initialize() + + +else: + initialize() diff --git a/opentelemetry-instrumentation/tests/test_auto_tracing.py b/opentelemetry-instrumentation/tests/test_auto_tracing.py new file mode 100644 index 0000000000..359dc12fad --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_auto_tracing.py @@ -0,0 +1,106 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from os import environ +from unittest import TestCase +from unittest.mock import patch + +from opentelemetry.configuration import Configuration +from opentelemetry.instrumentation.auto_instrumentation import components +from opentelemetry.sdk.resources import Resource + + +class Provider: + def __init__(self, resource=None): + self.processor = None + self.resource = resource + + def add_span_processor(self, processor): + self.processor = processor + + +class Processor: + def __init__(self, exporter): + self.exporter = exporter + + +class Exporter: + def __init__(self, service_name): + self.service_name = service_name + + def shutdown(self): + pass + + +class OTLPExporter: + pass + + +class TestTraceInit(TestCase): + def setUp(self): + super() + self.get_provider_patcher = patch( + "opentelemetry.instrumentation.auto_instrumentation.components.TracerProvider", + Provider, + ) + self.get_processor_patcher = patch( + "opentelemetry.instrumentation.auto_instrumentation.components.BatchExportSpanProcessor", + Processor, + ) + self.set_provider_patcher = patch( + "opentelemetry.trace.set_tracer_provider" + ) + + self.get_provider_mock = self.get_provider_patcher.start() + self.get_processor_mock = self.get_processor_patcher.start() + self.set_provider_mock = self.set_provider_patcher.start() + + def tearDown(self): + super() + self.get_provider_patcher.stop() + self.get_processor_patcher.stop() + self.set_provider_patcher.stop() + + # pylint: disable=protected-access + def test_trace_init_default(self): + environ["OTEL_SERVICE_NAME"] = "my-test-service" + Configuration._reset() + components.init_tracing({"zipkin": Exporter}) + + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, Provider) + self.assertIsInstance(provider.processor, Processor) + self.assertIsInstance(provider.processor.exporter, Exporter) + self.assertEqual( + provider.processor.exporter.service_name, "my-test-service" + ) + + def test_trace_init_otlp(self): + environ["OTEL_SERVICE_NAME"] = "my-otlp-test-service" + Configuration._reset() + components.init_tracing({"otlp": OTLPExporter}) + + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, Provider) + self.assertIsInstance(provider.processor, Processor) + self.assertIsInstance(provider.processor.exporter, OTLPExporter) + self.assertIsInstance(provider.resource, Resource) + self.assertEqual( + provider.resource.attributes.get("service.name"), + "my-otlp-test-service", + ) + del environ["OTEL_SERVICE_NAME"] diff --git a/opentelemetry-instrumentation/tests/test_run.py b/opentelemetry-instrumentation/tests/test_run.py index 21f53babc6..9bff8514b1 100644 --- a/opentelemetry-instrumentation/tests/test_run.py +++ b/opentelemetry-instrumentation/tests/test_run.py @@ -26,9 +26,6 @@ class TestRun(TestCase): @classmethod def setUpClass(cls): - cls.argv_patcher = patch( - "opentelemetry.instrumentation.auto_instrumentation.argv" - ) cls.execl_patcher = patch( "opentelemetry.instrumentation.auto_instrumentation.execl" ) @@ -36,16 +33,15 @@ def setUpClass(cls): "opentelemetry.instrumentation.auto_instrumentation.which" ) - cls.argv_patcher.start() cls.execl_patcher.start() cls.which_patcher.start() @classmethod def tearDownClass(cls): - cls.argv_patcher.stop() cls.execl_patcher.stop() cls.which_patcher.stop() + @patch("sys.argv", ["instrument", ""]) @patch.dict("os.environ", {"PYTHONPATH": ""}) def test_empty(self): auto_instrumentation.run() @@ -54,6 +50,7 @@ def test_empty(self): pathsep.join([self.auto_instrumentation_path, getcwd()]), ) + @patch("sys.argv", ["instrument", ""]) @patch.dict("os.environ", {"PYTHONPATH": "abc"}) def test_non_empty(self): auto_instrumentation.run() @@ -62,6 +59,7 @@ def test_non_empty(self): pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), ) + @patch("sys.argv", ["instrument", ""]) @patch.dict( "os.environ", {"PYTHONPATH": pathsep.join(["abc", auto_instrumentation_path])}, @@ -73,6 +71,7 @@ def test_after_path(self): pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), ) + @patch("sys.argv", ["instrument", ""]) @patch.dict( "os.environ", { @@ -90,10 +89,7 @@ def test_single_path(self): class TestExecl(TestCase): - @patch( - "opentelemetry.instrumentation.auto_instrumentation.argv", - new=[1, 2, 3], - ) + @patch("sys.argv", ["1", "2", "3"]) @patch("opentelemetry.instrumentation.auto_instrumentation.which") @patch("opentelemetry.instrumentation.auto_instrumentation.execl") def test_execl( @@ -103,4 +99,26 @@ def test_execl( auto_instrumentation.run() - mock_execl.assert_called_with("python", "python", 3) + mock_execl.assert_called_with("python", "python", "3") + + +class TestArgs(TestCase): + @patch("opentelemetry.instrumentation.auto_instrumentation.execl") + def test_exporter(self, _): # pylint: disable=no-self-use + with patch("sys.argv", ["instrument", "2"]): + auto_instrumentation.run() + self.assertIsNone(environ.get("OTEL_EXPORTER")) + + with patch("sys.argv", ["instrument", "-e", "zipkin", "1", "2"]): + auto_instrumentation.run() + self.assertEqual(environ.get("OTEL_EXPORTER"), "zipkin") + + @patch("opentelemetry.instrumentation.auto_instrumentation.execl") + def test_service_name(self, _): # pylint: disable=no-self-use + with patch("sys.argv", ["instrument", "2"]): + auto_instrumentation.run() + self.assertIsNone(environ.get("OTEL_SERVICE_NAME")) + + with patch("sys.argv", ["instrument", "-s", "my-service", "1", "2"]): + auto_instrumentation.run() + self.assertEqual(environ.get("OTEL_SERVICE_NAME"), "my-service") From 03b24809bfecc22b2f000394d412b48c14fd4643 Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Mon, 23 Nov 2020 17:03:11 +0100 Subject: [PATCH 0673/1517] Fix OTLP exporter config tests (#1395) --- .../tests/test_otlp_metric_exporter.py | 6 +++++- .../tests/test_otlp_trace_exporter.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index 4f121b38fd..308d217f21 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from collections import OrderedDict from unittest import TestCase from unittest.mock import patch @@ -44,6 +45,8 @@ from opentelemetry.sdk.metrics.export.aggregate import SumAggregator from opentelemetry.sdk.resources import Resource as SDKResource +THIS_DIR = os.path.dirname(__file__) + class TestOTLPMetricExporter(TestCase): @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") @@ -75,7 +78,8 @@ def tearDown(self): "os.environ", { "OTEL_EXPORTER_OTLP_METRIC_ENDPOINT": "collector:55680", - "OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE": "fixtures/test.cert", + "OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE": THIS_DIR + + "/fixtures/test.cert", "OTEL_EXPORTER_OTLP_METRIC_HEADERS": "key1:value1;key2:value2", "OTEL_EXPORTER_OTLP_METRIC_TIMEOUT": "10", }, diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index 742ab29bc9..4445873aaf 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from collections import OrderedDict from concurrent.futures import ThreadPoolExecutor from unittest import TestCase @@ -53,6 +54,8 @@ ) from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +THIS_DIR = os.path.dirname(__file__) + class TraceServiceServicerUNAVAILABLEDelay(TraceServiceServicer): # pylint: disable=invalid-name,unused-argument,no-self-use @@ -165,7 +168,8 @@ def tearDown(self): "os.environ", { "OTEL_EXPORTER_OTLP_SPAN_ENDPOINT": "collector:55680", - "OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE": "fixtures/test.cert", + "OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE": THIS_DIR + + "/fixtures/test.cert", "OTEL_EXPORTER_OTLP_SPAN_HEADERS": "key1:value1;key2:value2", "OTEL_EXPORTER_OTLP_SPAN_TIMEOUT": "10", }, From 8ca9e46d6ea97d390a3c9531afcf901b61ce78ab Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Mon, 23 Nov 2020 08:17:40 -0800 Subject: [PATCH 0674/1517] Zipkin exporter v2 api support for protobuf format (#1318) --- .flake8 | 1 + .pylintrc | 2 +- .../CHANGELOG.md | 2 + .../opentelemetry-exporter-zipkin/setup.cfg | 1 + .../opentelemetry/exporter/zipkin/__init__.py | 167 ++++++- .../exporter/zipkin/gen/__init__.py | 0 .../exporter/zipkin/gen/zipkin_pb2.py | 458 ++++++++++++++++++ .../exporter/zipkin/gen/zipkin_pb2.pyi | 214 ++++++++ .../tests/test_zipkin_exporter.py | 324 ++++++++++++- pyproject.toml | 1 + 10 files changed, 1141 insertions(+), 29 deletions(-) create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/__init__.py create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/zipkin_pb2.py create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/zipkin_pb2.pyi diff --git a/.flake8 b/.flake8 index 8dabaabc12..7b88ce37eb 100644 --- a/.flake8 +++ b/.flake8 @@ -18,6 +18,7 @@ exclude = __pycache__ exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/ exporter/opentelemetry-exporter-jaeger/build/* + exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ docs/examples/opentelemetry-example-app/build/* opentelemetry-proto/build/* diff --git a/.pylintrc b/.pylintrc index ea54273868..58fe77eef1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -165,7 +165,7 @@ contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. -generated-members= +generated-members=zipkin_pb2.* # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index 60d7fbe2ec..972096a8ef 100644 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Support for v2 api protobuf format ([#1318](https://github.com/open-telemetry/opentelemetry-python/pull/1318)) + ## Version 0.14b0 Released 2020-10-13 diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 310ddeb6a4..1803eac14e 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -39,6 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = + protobuf >= 3.12 requests ~= 2.7 opentelemetry-api == 0.16.dev0 opentelemetry-sdk == 0.16.dev0 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index bd3cdbb26a..51130d60b5 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -26,6 +26,9 @@ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. _Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/sdk-environment-variables.md#zipkin-exporter +.. envvar:: OTEL_EXPORTER_ZIPKIN_ENDPOINT +.. envvar:: OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT + .. code:: python from opentelemetry import trace @@ -55,7 +58,14 @@ with tracer.start_as_current_span("foo"): print("Hello world!") -The exporter supports endpoint configuration via the OTEL_EXPORTER_ZIPKIN_ENDPOINT environment variables as defined in the `Specification`_ +The exporter supports the following environment variables for configuration: + +:envvar:`OTEL_EXPORTER_ZIPKIN_ENDPOINT`: target to which the exporter will +send data. This may include a path (e.g. http://example.com:9411/api/v2/spans). + +:envvar:`OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT`: transport interchange format +to use when sending data. Currently only Zipkin's v2 json and protobuf formats +are supported, with v2 json being the default. API --- @@ -63,21 +73,24 @@ import json import logging -import os -from typing import Optional, Sequence +from typing import Optional, Sequence, Union from urllib.parse import urlparse import requests +from opentelemetry.configuration import Configuration +from opentelemetry.exporter.zipkin.gen import zipkin_pb2 from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace import Span, SpanContext, SpanKind +TRANSPORT_FORMAT_JSON = "json" +TRANSPORT_FORMAT_PROTOBUF = "protobuf" + DEFAULT_RETRY = False DEFAULT_URL = "http://localhost:9411/api/v2/spans" DEFAULT_MAX_TAG_VALUE_LENGTH = 128 -ZIPKIN_HEADERS = {"Content-Type": "application/json"} -SPAN_KIND_MAP = { +SPAN_KIND_MAP_JSON = { SpanKind.INTERNAL: None, SpanKind.SERVER: "SERVER", SpanKind.CLIENT: "CLIENT", @@ -85,6 +98,14 @@ SpanKind.CONSUMER: "CONSUMER", } +SPAN_KIND_MAP_PROTOBUF = { + SpanKind.INTERNAL: zipkin_pb2.Span.Kind.SPAN_KIND_UNSPECIFIED, + SpanKind.SERVER: zipkin_pb2.Span.Kind.SERVER, + SpanKind.CLIENT: zipkin_pb2.Span.Kind.CLIENT, + SpanKind.PRODUCER: zipkin_pb2.Span.Kind.PRODUCER, + SpanKind.CONSUMER: zipkin_pb2.Span.Kind.CONSUMER, +} + SUCCESS_STATUS_CODES = (200, 202) logger = logging.getLogger(__name__) @@ -100,6 +121,7 @@ class ZipkinSpanExporter(SpanExporter): ipv4: Primary IPv4 address associated with this connection. ipv6: Primary IPv6 address associated with this connection. retry: Set to True to configure the exporter to retry on failure. + transport_format: transport interchange format to use """ def __init__( @@ -110,12 +132,13 @@ def __init__( ipv6: Optional[str] = None, retry: Optional[str] = DEFAULT_RETRY, max_tag_value_length: Optional[int] = DEFAULT_MAX_TAG_VALUE_LENGTH, + transport_format: Union[ + TRANSPORT_FORMAT_JSON, TRANSPORT_FORMAT_PROTOBUF, None + ] = None, ): self.service_name = service_name if url is None: - self.url = os.environ.get( - "OTEL_EXPORTER_ZIPKIN_ENDPOINT", DEFAULT_URL - ) + self.url = Configuration().EXPORTER_ZIPKIN_ENDPOINT or DEFAULT_URL else: self.url = url @@ -126,10 +149,27 @@ def __init__( self.retry = retry self.max_tag_value_length = max_tag_value_length + if transport_format is None: + self.transport_format = ( + Configuration().EXPORTER_ZIPKIN_TRANSPORT_FORMAT + or TRANSPORT_FORMAT_JSON + ) + else: + self.transport_format = transport_format + def export(self, spans: Sequence[Span]) -> SpanExportResult: - zipkin_spans = self._translate_to_zipkin(spans) + if self.transport_format == TRANSPORT_FORMAT_JSON: + content_type = "application/json" + elif self.transport_format == TRANSPORT_FORMAT_PROTOBUF: + content_type = "application/x-protobuf" + else: + logger.error("Invalid transport format %s", self.transport_format) + return SpanExportResult.FAILURE + result = requests.post( - url=self.url, data=json.dumps(zipkin_spans), headers=ZIPKIN_HEADERS + url=self.url, + data=self._translate_to_transport_format(spans), + headers={"Content-Type": content_type}, ) if result.status_code not in SUCCESS_STATUS_CODES: @@ -147,8 +187,14 @@ def export(self, spans: Sequence[Span]) -> SpanExportResult: def shutdown(self) -> None: pass - def _translate_to_zipkin(self, spans: Sequence[Span]): + def _translate_to_transport_format(self, spans: Sequence[Span]): + return ( + self._translate_to_json(spans) + if self.transport_format == TRANSPORT_FORMAT_JSON + else self._translate_to_protobuf(spans) + ) + def _translate_to_json(self, spans: Sequence[Span]): local_endpoint = {"serviceName": self.service_name, "port": self.port} if self.ipv4 is not None: @@ -165,8 +211,8 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): # Timestamp in zipkin spans is int of microseconds. # see: https://zipkin.io/pages/instrumenting.html - start_timestamp_mus = _nsec_to_usec_round(span.start_time) - duration_mus = _nsec_to_usec_round(span.end_time - span.start_time) + start_timestamp_mus = nsec_to_usec_round(span.start_time) + duration_mus = nsec_to_usec_round(span.end_time - span.start_time) zipkin_span = { # Ensure left-zero-padding of traceId, spanId, parentId @@ -176,7 +222,7 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): "timestamp": start_timestamp_mus, "duration": duration_mus, "localEndpoint": local_endpoint, - "kind": SPAN_KIND_MAP[span.kind], + "kind": SPAN_KIND_MAP_JSON[span.kind], "tags": self._extract_tags_from_span(span), "annotations": self._extract_annotations_from_events( span.events @@ -211,7 +257,94 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): zipkin_span["parentId"] = format(span.parent.span_id, "016x") zipkin_spans.append(zipkin_span) - return zipkin_spans + + return json.dumps(zipkin_spans) + + def _translate_to_protobuf(self, spans: Sequence[Span]): + + local_endpoint = zipkin_pb2.Endpoint( + service_name=self.service_name, port=self.port + ) + + if self.ipv4 is not None: + local_endpoint.ipv4 = self.ipv4 + + if self.ipv6 is not None: + local_endpoint.ipv6 = self.ipv6 + + pbuf_spans = zipkin_pb2.ListOfSpans() + + for span in spans: + context = span.get_span_context() + trace_id = context.trace_id.to_bytes( + length=16, byteorder="big", signed=False, + ) + span_id = self.format_pbuf_span_id(context.span_id) + + # Timestamp in zipkin spans is int of microseconds. + # see: https://zipkin.io/pages/instrumenting.html + start_timestamp_mus = nsec_to_usec_round(span.start_time) + duration_mus = nsec_to_usec_round(span.end_time - span.start_time) + + # pylint: disable=no-member + pbuf_span = zipkin_pb2.Span( + trace_id=trace_id, + id=span_id, + name=span.name, + timestamp=start_timestamp_mus, + duration=duration_mus, + local_endpoint=local_endpoint, + kind=SPAN_KIND_MAP_PROTOBUF[span.kind], + tags=self._extract_tags_from_span(span), + ) + + annotations = self._extract_annotations_from_events(span.events) + + if annotations is not None: + for annotation in annotations: + pbuf_span.annotations.append( + zipkin_pb2.Annotation( + timestamp=annotation["timestamp"], + value=annotation["value"], + ) + ) + + if span.instrumentation_info is not None: + pbuf_span.tags.update( + { + "otel.instrumentation_library.name": span.instrumentation_info.name, + "otel.instrumentation_library.version": span.instrumentation_info.version, + } + ) + + if span.status is not None: + pbuf_span.tags.update( + {"otel.status_code": str(span.status.status_code.value)} + ) + if span.status.description is not None: + pbuf_span.tags.update( + {"otel.status_description": span.status.description} + ) + + if context.trace_flags.sampled: + pbuf_span.debug = True + + if isinstance(span.parent, Span): + pbuf_span.parent_id = self.format_pbuf_span_id( + span.parent.get_span_context().span_id + ) + elif isinstance(span.parent, SpanContext): + pbuf_span.parent_id = self.format_pbuf_span_id( + span.parent.span_id + ) + + pbuf_spans.spans.append(pbuf_span) + + return pbuf_spans.SerializeToString() + + @staticmethod + def format_pbuf_span_id(span_id: int): + return span_id.to_bytes(length=8, byteorder="big", signed=False) def _extract_tags_from_dict(self, tags_dict): tags = {} @@ -251,13 +384,13 @@ def _extract_annotations_from_events(self, events): annotations.append( { - "timestamp": _nsec_to_usec_round(event.timestamp), + "timestamp": nsec_to_usec_round(event.timestamp), "value": json.dumps({event.name: attrs}), } ) return annotations -def _nsec_to_usec_round(nsec): +def nsec_to_usec_round(nsec): """Round nanoseconds to microseconds""" return (nsec + 500) // 10 ** 3 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/zipkin_pb2.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/zipkin_pb2.py new file mode 100644 index 0000000000..7b578febc1 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/zipkin_pb2.py @@ -0,0 +1,458 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: zipkin.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +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.FileDescriptor( + name='zipkin.proto', + package='zipkin.proto3', + syntax='proto3', + serialized_options=b'\n\016zipkin2.proto3P\001', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x0czipkin.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_KIND = _descriptor.EnumDescriptor( + name='Kind', + full_name='zipkin.proto3.Span.Kind', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='SPAN_KIND_UNSPECIFIED', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CLIENT', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='SERVER', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='PRODUCER', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CONSUMER', index=4, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=448, + serialized_end=533, +) +_sym_db.RegisterEnumDescriptor(_SPAN_KIND) + + +_SPAN_TAGSENTRY = _descriptor.Descriptor( + name='TagsEntry', + full_name='zipkin.proto3.Span.TagsEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='zipkin.proto3.Span.TagsEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='value', full_name='zipkin.proto3.Span.TagsEntry.value', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=b'8\001', + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=403, + serialized_end=446, +) + +_SPAN = _descriptor.Descriptor( + name='Span', + full_name='zipkin.proto3.Span', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='trace_id', full_name='zipkin.proto3.Span.trace_id', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='parent_id', full_name='zipkin.proto3.Span.parent_id', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='id', full_name='zipkin.proto3.Span.id', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='kind', full_name='zipkin.proto3.Span.kind', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='name', full_name='zipkin.proto3.Span.name', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='timestamp', full_name='zipkin.proto3.Span.timestamp', index=5, + number=6, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='duration', full_name='zipkin.proto3.Span.duration', index=6, + number=7, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='local_endpoint', full_name='zipkin.proto3.Span.local_endpoint', index=7, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='remote_endpoint', full_name='zipkin.proto3.Span.remote_endpoint', index=8, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='annotations', full_name='zipkin.proto3.Span.annotations', index=9, + number=10, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='tags', full_name='zipkin.proto3.Span.tags', index=10, + number=11, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='debug', full_name='zipkin.proto3.Span.debug', index=11, + number=12, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='shared', full_name='zipkin.proto3.Span.shared', index=12, + number=13, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_SPAN_TAGSENTRY, ], + enum_types=[ + _SPAN_KIND, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=32, + serialized_end=533, +) + + +_ENDPOINT = _descriptor.Descriptor( + name='Endpoint', + full_name='zipkin.proto3.Endpoint', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='service_name', full_name='zipkin.proto3.Endpoint.service_name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='ipv4', full_name='zipkin.proto3.Endpoint.ipv4', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='ipv6', full_name='zipkin.proto3.Endpoint.ipv6', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='port', full_name='zipkin.proto3.Endpoint.port', index=3, + number=4, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=535, + serialized_end=609, +) + + +_ANNOTATION = _descriptor.Descriptor( + name='Annotation', + full_name='zipkin.proto3.Annotation', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='timestamp', full_name='zipkin.proto3.Annotation.timestamp', index=0, + number=1, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='value', full_name='zipkin.proto3.Annotation.value', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=611, + serialized_end=657, +) + + +_LISTOFSPANS = _descriptor.Descriptor( + name='ListOfSpans', + full_name='zipkin.proto3.ListOfSpans', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='spans', full_name='zipkin.proto3.ListOfSpans.spans', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=659, + serialized_end=708, +) + + +_REPORTRESPONSE = _descriptor.Descriptor( + name='ReportResponse', + full_name='zipkin.proto3.ReportResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=710, + serialized_end=726, +) + +_SPAN_TAGSENTRY.containing_type = _SPAN +_SPAN.fields_by_name['kind'].enum_type = _SPAN_KIND +_SPAN.fields_by_name['local_endpoint'].message_type = _ENDPOINT +_SPAN.fields_by_name['remote_endpoint'].message_type = _ENDPOINT +_SPAN.fields_by_name['annotations'].message_type = _ANNOTATION +_SPAN.fields_by_name['tags'].message_type = _SPAN_TAGSENTRY +_SPAN_KIND.containing_type = _SPAN +_LISTOFSPANS.fields_by_name['spans'].message_type = _SPAN +DESCRIPTOR.message_types_by_name['Span'] = _SPAN +DESCRIPTOR.message_types_by_name['Endpoint'] = _ENDPOINT +DESCRIPTOR.message_types_by_name['Annotation'] = _ANNOTATION +DESCRIPTOR.message_types_by_name['ListOfSpans'] = _LISTOFSPANS +DESCRIPTOR.message_types_by_name['ReportResponse'] = _REPORTRESPONSE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Span = _reflection.GeneratedProtocolMessageType('Span', (_message.Message,), { + + 'TagsEntry' : _reflection.GeneratedProtocolMessageType('TagsEntry', (_message.Message,), { + 'DESCRIPTOR' : _SPAN_TAGSENTRY, + '__module__' : 'zipkin_pb2' + # @@protoc_insertion_point(class_scope:zipkin.proto3.Span.TagsEntry) + }) + , + 'DESCRIPTOR' : _SPAN, + '__module__' : '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__' : 'zipkin_pb2' + # @@protoc_insertion_point(class_scope:zipkin.proto3.Endpoint) + }) +_sym_db.RegisterMessage(Endpoint) + +Annotation = _reflection.GeneratedProtocolMessageType('Annotation', (_message.Message,), { + 'DESCRIPTOR' : _ANNOTATION, + '__module__' : 'zipkin_pb2' + # @@protoc_insertion_point(class_scope:zipkin.proto3.Annotation) + }) +_sym_db.RegisterMessage(Annotation) + +ListOfSpans = _reflection.GeneratedProtocolMessageType('ListOfSpans', (_message.Message,), { + 'DESCRIPTOR' : _LISTOFSPANS, + '__module__' : 'zipkin_pb2' + # @@protoc_insertion_point(class_scope:zipkin.proto3.ListOfSpans) + }) +_sym_db.RegisterMessage(ListOfSpans) + +ReportResponse = _reflection.GeneratedProtocolMessageType('ReportResponse', (_message.Message,), { + 'DESCRIPTOR' : _REPORTRESPONSE, + '__module__' : 'zipkin_pb2' + # @@protoc_insertion_point(class_scope:zipkin.proto3.ReportResponse) + }) +_sym_db.RegisterMessage(ReportResponse) + + +DESCRIPTOR._options = None +_SPAN_TAGSENTRY._options = None + +_SPANSERVICE = _descriptor.ServiceDescriptor( + name='SpanService', + full_name='zipkin.proto3.SpanService', + file=DESCRIPTOR, + index=0, + serialized_options=None, + create_key=_descriptor._internal_create_key, + serialized_start=728, + serialized_end=812, + methods=[ + _descriptor.MethodDescriptor( + name='Report', + full_name='zipkin.proto3.SpanService.Report', + index=0, + containing_service=None, + input_type=_LISTOFSPANS, + output_type=_REPORTRESPONSE, + serialized_options=None, + create_key=_descriptor._internal_create_key, + ), +]) +_sym_db.RegisterServiceDescriptor(_SPANSERVICE) + +DESCRIPTOR.services_by_name['SpanService'] = _SPANSERVICE + +# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/zipkin_pb2.pyi b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/zipkin_pb2.pyi new file mode 100644 index 0000000000..1624d7d595 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/zipkin_pb2.pyi @@ -0,0 +1,214 @@ +# @generated by generate_proto_mypy_stubs.py. Do not edit! +import sys +from google.protobuf.descriptor import ( + Descriptor as google___protobuf___descriptor___Descriptor, + EnumDescriptor as google___protobuf___descriptor___EnumDescriptor, + FileDescriptor as google___protobuf___descriptor___FileDescriptor, +) + +from google.protobuf.internal.containers import ( + RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, +) + +from google.protobuf.message import ( + Message as google___protobuf___message___Message, +) + +from typing import ( + Iterable as typing___Iterable, + List as typing___List, + Mapping as typing___Mapping, + MutableMapping as typing___MutableMapping, + NewType as typing___NewType, + Optional as typing___Optional, + Text as typing___Text, + Tuple as typing___Tuple, + Union as typing___Union, + cast as typing___cast, +) + +from typing_extensions import ( + Literal as typing_extensions___Literal, +) + + +builtin___bool = bool +builtin___bytes = bytes +builtin___float = float +builtin___int = int +builtin___str = str +if sys.version_info < (3,): + builtin___buffer = buffer + builtin___unicode = unicode + + +DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... + +class Span(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + KindValue = typing___NewType('KindValue', builtin___int) + type___KindValue = KindValue + class Kind(object): + DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... + @classmethod + def Name(cls, number: builtin___int) -> builtin___str: ... + @classmethod + def Value(cls, name: builtin___str) -> Span.KindValue: ... + @classmethod + def keys(cls) -> typing___List[builtin___str]: ... + @classmethod + def values(cls) -> typing___List[Span.KindValue]: ... + @classmethod + def items(cls) -> typing___List[typing___Tuple[builtin___str, Span.KindValue]]: ... + SPAN_KIND_UNSPECIFIED = typing___cast(Span.KindValue, 0) + CLIENT = typing___cast(Span.KindValue, 1) + SERVER = typing___cast(Span.KindValue, 2) + PRODUCER = typing___cast(Span.KindValue, 3) + CONSUMER = typing___cast(Span.KindValue, 4) + SPAN_KIND_UNSPECIFIED = typing___cast(Span.KindValue, 0) + CLIENT = typing___cast(Span.KindValue, 1) + SERVER = typing___cast(Span.KindValue, 2) + PRODUCER = typing___cast(Span.KindValue, 3) + CONSUMER = typing___cast(Span.KindValue, 4) + type___Kind = Kind + + class TagsEntry(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + key: typing___Text = ... + value: typing___Text = ... + + def __init__(self, + *, + key : typing___Optional[typing___Text] = None, + value : typing___Optional[typing___Text] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> Span.TagsEntry: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Span.TagsEntry: ... + def ClearField(self, field_name: typing_extensions___Literal[u"key",b"key",u"value",b"value"]) -> None: ... + type___TagsEntry = TagsEntry + + trace_id: builtin___bytes = ... + parent_id: builtin___bytes = ... + id: builtin___bytes = ... + kind: type___Span.KindValue = ... + name: typing___Text = ... + timestamp: builtin___int = ... + duration: builtin___int = ... + debug: builtin___bool = ... + shared: builtin___bool = ... + + @property + def local_endpoint(self) -> type___Endpoint: ... + + @property + def remote_endpoint(self) -> type___Endpoint: ... + + @property + def annotations(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Annotation]: ... + + @property + def tags(self) -> typing___MutableMapping[typing___Text, typing___Text]: ... + + def __init__(self, + *, + trace_id : typing___Optional[builtin___bytes] = None, + parent_id : typing___Optional[builtin___bytes] = None, + id : typing___Optional[builtin___bytes] = None, + kind : typing___Optional[type___Span.KindValue] = None, + name : typing___Optional[typing___Text] = None, + timestamp : typing___Optional[builtin___int] = None, + duration : typing___Optional[builtin___int] = None, + local_endpoint : typing___Optional[type___Endpoint] = None, + remote_endpoint : typing___Optional[type___Endpoint] = None, + annotations : typing___Optional[typing___Iterable[type___Annotation]] = None, + tags : typing___Optional[typing___Mapping[typing___Text, typing___Text]] = None, + debug : typing___Optional[builtin___bool] = None, + shared : typing___Optional[builtin___bool] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> Span: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Span: ... + def HasField(self, field_name: typing_extensions___Literal[u"local_endpoint",b"local_endpoint",u"remote_endpoint",b"remote_endpoint"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"annotations",b"annotations",u"debug",b"debug",u"duration",b"duration",u"id",b"id",u"kind",b"kind",u"local_endpoint",b"local_endpoint",u"name",b"name",u"parent_id",b"parent_id",u"remote_endpoint",b"remote_endpoint",u"shared",b"shared",u"tags",b"tags",u"timestamp",b"timestamp",u"trace_id",b"trace_id"]) -> None: ... +type___Span = Span + +class Endpoint(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + service_name: typing___Text = ... + ipv4: builtin___bytes = ... + ipv6: builtin___bytes = ... + port: builtin___int = ... + + def __init__(self, + *, + service_name : typing___Optional[typing___Text] = None, + ipv4 : typing___Optional[builtin___bytes] = None, + ipv6 : typing___Optional[builtin___bytes] = None, + port : typing___Optional[builtin___int] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> Endpoint: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Endpoint: ... + def ClearField(self, field_name: typing_extensions___Literal[u"ipv4",b"ipv4",u"ipv6",b"ipv6",u"port",b"port",u"service_name",b"service_name"]) -> None: ... +type___Endpoint = Endpoint + +class Annotation(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + timestamp: builtin___int = ... + value: typing___Text = ... + + def __init__(self, + *, + timestamp : typing___Optional[builtin___int] = None, + value : typing___Optional[typing___Text] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> Annotation: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Annotation: ... + def ClearField(self, field_name: typing_extensions___Literal[u"timestamp",b"timestamp",u"value",b"value"]) -> None: ... +type___Annotation = Annotation + +class ListOfSpans(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + @property + def spans(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Span]: ... + + def __init__(self, + *, + spans : typing___Optional[typing___Iterable[type___Span]] = None, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> ListOfSpans: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ListOfSpans: ... + def ClearField(self, field_name: typing_extensions___Literal[u"spans",b"spans"]) -> None: ... +type___ListOfSpans = ListOfSpans + +class ReportResponse(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + + def __init__(self, + ) -> None: ... + if sys.version_info >= (3,): + @classmethod + def FromString(cls, s: builtin___bytes) -> ReportResponse: ... + else: + @classmethod + def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ReportResponse: ... +type___ReportResponse = ReportResponse diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 12d85a0dc4..bbc6ccf7ce 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -18,12 +18,21 @@ from unittest.mock import MagicMock, patch from opentelemetry import trace as trace_api -from opentelemetry.exporter.zipkin import ZipkinSpanExporter +from opentelemetry.configuration import Configuration +from opentelemetry.exporter.zipkin import ( + SPAN_KIND_MAP_JSON, + SPAN_KIND_MAP_PROTOBUF, + TRANSPORT_FORMAT_JSON, + TRANSPORT_FORMAT_PROTOBUF, + ZipkinSpanExporter, + nsec_to_usec_round, +) +from opentelemetry.exporter.zipkin.gen import zipkin_pb2 from opentelemetry.sdk import trace from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -from opentelemetry.trace import TraceFlags +from opentelemetry.trace import SpanKind, TraceFlags from opentelemetry.trace.status import Status, StatusCode @@ -49,11 +58,17 @@ def setUp(self): def tearDown(self): if "OTEL_EXPORTER_ZIPKIN_ENDPOINT" in os.environ: del os.environ["OTEL_EXPORTER_ZIPKIN_ENDPOINT"] + if "OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT" in os.environ: + del os.environ["OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT"] + Configuration()._reset() # pylint: disable=protected-access def test_constructor_env_var(self): """Test the default values assigned by constructor.""" url = "https://foo:9911/path" os.environ["OTEL_EXPORTER_ZIPKIN_ENDPOINT"] = url + os.environ[ + "OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT" + ] = TRANSPORT_FORMAT_PROTOBUF service_name = "my-service-name" port = 9911 exporter = ZipkinSpanExporter(service_name) @@ -65,6 +80,7 @@ def test_constructor_env_var(self): self.assertEqual(exporter.ipv6, ipv6) self.assertEqual(exporter.url, url) self.assertEqual(exporter.port, port) + self.assertEqual(exporter.transport_format, TRANSPORT_FORMAT_PROTOBUF) def test_constructor_default(self): """Test the default values assigned by constructor.""" @@ -74,12 +90,14 @@ def test_constructor_default(self): ipv4 = None ipv6 = None url = "http://localhost:9411/api/v2/spans" + transport_format = TRANSPORT_FORMAT_JSON self.assertEqual(exporter.service_name, service_name) self.assertEqual(exporter.port, port) self.assertEqual(exporter.ipv4, ipv4) self.assertEqual(exporter.ipv6, ipv6) self.assertEqual(exporter.url, url) + self.assertEqual(exporter.transport_format, transport_format) def test_constructor_explicit(self): """Test the constructor passing all the options.""" @@ -88,8 +106,14 @@ def test_constructor_explicit(self): ipv4 = "1.2.3.4" ipv6 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334" url = "https://opentelemetry.io:15875/myapi/traces?format=zipkin" + transport_format = TRANSPORT_FORMAT_PROTOBUF + exporter = ZipkinSpanExporter( - service_name=service_name, url=url, ipv4=ipv4, ipv6=ipv6, + service_name=service_name, + url=url, + ipv4=ipv4, + ipv6=ipv6, + transport_format=transport_format, ) self.assertEqual(exporter.service_name, service_name) @@ -97,9 +121,10 @@ def test_constructor_explicit(self): self.assertEqual(exporter.ipv4, ipv4) self.assertEqual(exporter.ipv6, ipv6) self.assertEqual(exporter.url, url) + self.assertEqual(exporter.transport_format, transport_format) # pylint: disable=too-many-locals,too-many-statements - def test_export(self): + def test_export_json(self): span_names = ("test1", "test2", "test3", "test4") trace_id = 0x6E0C63257DE34C926F9EFCD03927272E span_id = 0x34BF92DEEFC58C92 @@ -205,6 +230,7 @@ def test_export(self): service_name = "test-service" local_endpoint = {"serviceName": service_name, "port": 9411} + span_kind = SPAN_KIND_MAP_JSON[SpanKind.INTERNAL] exporter = ZipkinSpanExporter(service_name) expected_spans = [ @@ -215,7 +241,7 @@ def test_export(self): "timestamp": start_times[0] // 10 ** 3, "duration": durations[0] // 10 ** 3, "localEndpoint": local_endpoint, - "kind": None, + "kind": span_kind, "tags": { "key_bool": "False", "key_string": "hello_world", @@ -245,7 +271,7 @@ def test_export(self): "timestamp": start_times[1] // 10 ** 3, "duration": durations[1] // 10 ** 3, "localEndpoint": local_endpoint, - "kind": None, + "kind": span_kind, "tags": { "key_resource": "some_resource", "otel.status_code": "1", @@ -259,7 +285,7 @@ def test_export(self): "timestamp": start_times[2] // 10 ** 3, "duration": durations[2] // 10 ** 3, "localEndpoint": local_endpoint, - "kind": None, + "kind": span_kind, "tags": { "key_string": "hello_world", "key_resource": "some_resource", @@ -274,7 +300,7 @@ def test_export(self): "timestamp": start_times[3] // 10 ** 3, "duration": durations[3] // 10 ** 3, "localEndpoint": local_endpoint, - "kind": None, + "kind": span_kind, "tags": { "otel.instrumentation_library.name": "name", "otel.instrumentation_library.version": "version", @@ -294,6 +320,7 @@ def test_export(self): kwargs = mock_post.call_args[1] self.assertEqual(kwargs["url"], "http://localhost:9411/api/v2/spans") + self.assertEqual(kwargs["headers"]["Content-Type"], "application/json") actual_spans = sorted( json.loads(kwargs["data"]), key=lambda span: span["timestamp"] ) @@ -307,7 +334,7 @@ def test_export(self): self.assertEqual(expected_annotations, actual_annotations) # pylint: disable=too-many-locals - def test_zero_padding(self): + def test_export_json_zero_padding(self): """test that hex ids starting with 0 are properly padded to 16 or 32 hex chars when exported @@ -355,7 +382,7 @@ def test_zero_padding(self): "timestamp": start_time // 10 ** 3, "duration": duration // 10 ** 3, "localEndpoint": local_endpoint, - "kind": None, + "kind": SPAN_KIND_MAP_JSON[SpanKind.INTERNAL], "tags": {"otel.status_code": "1"}, "annotations": None, "debug": True, @@ -383,7 +410,7 @@ def test_invalid_response(self, mock_post): status = exporter.export(spans) self.assertEqual(SpanExportResult.FAILURE, status) - def test_max_tag_length(self): + def test_export_json_max_tag_length(self): service_name = "test-service" span_context = trace_api.SpanContext( @@ -427,3 +454,278 @@ def test_max_tag_length(self): tags = json.loads(kwargs["data"])[0]["tags"] self.assertEqual(len(tags["k1"]), 2) self.assertEqual(len(tags["k2"]), 2) + + # pylint: disable=too-many-locals,too-many-statements + def test_export_protobuf(self): + span_names = ("test1", "test2", "test3", "test4") + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + span_id = 0x34BF92DEEFC58C92 + parent_id = 0x1111111111111111 + other_id = 0x2222222222222222 + + base_time = 683647322 * 10 ** 9 # in ns + start_times = ( + base_time, + base_time + 150 * 10 ** 6, + base_time + 300 * 10 ** 6, + base_time + 400 * 10 ** 6, + ) + durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6, 300 * 10 ** 6) + end_times = ( + start_times[0] + durations[0], + start_times[1] + durations[1], + start_times[2] + durations[2], + start_times[3] + durations[3], + ) + + span_context = trace_api.SpanContext( + trace_id, + span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + parent_span_context = trace_api.SpanContext( + trace_id, parent_id, is_remote=False + ) + other_context = trace_api.SpanContext( + trace_id, other_id, is_remote=False + ) + + event_attributes = { + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + } + + event_timestamp = base_time + 50 * 10 ** 6 + event = trace.Event( + name="event0", + timestamp=event_timestamp, + attributes=event_attributes, + ) + + link_attributes = {"key_bool": True} + + link = trace_api.Link( + context=other_context, attributes=link_attributes + ) + + otel_spans = [ + trace._Span( + name=span_names[0], + context=span_context, + parent=parent_span_context, + events=(event,), + links=(link,), + ), + trace._Span( + name=span_names[1], context=parent_span_context, parent=None + ), + trace._Span( + name=span_names[2], context=other_context, parent=None + ), + trace._Span( + name=span_names[3], context=other_context, parent=None + ), + ] + + otel_spans[0].start(start_time=start_times[0]) + otel_spans[0].resource = Resource({}) + # added here to preserve order + otel_spans[0].set_attribute("key_bool", False) + otel_spans[0].set_attribute("key_string", "hello_world") + otel_spans[0].set_attribute("key_float", 111.22) + otel_spans[0].set_status( + Status(StatusCode.ERROR, "Example description") + ) + otel_spans[0].end(end_time=end_times[0]) + + otel_spans[1].start(start_time=start_times[1]) + otel_spans[1].resource = Resource( + attributes={"key_resource": "some_resource"} + ) + otel_spans[1].end(end_time=end_times[1]) + + otel_spans[2].start(start_time=start_times[2]) + otel_spans[2].set_attribute("key_string", "hello_world") + otel_spans[2].resource = Resource( + attributes={"key_resource": "some_resource"} + ) + otel_spans[2].end(end_time=end_times[2]) + + otel_spans[3].start(start_time=start_times[3]) + otel_spans[3].resource = Resource({}) + otel_spans[3].end(end_time=end_times[3]) + otel_spans[3].instrumentation_info = InstrumentationInfo( + name="name", version="version" + ) + + service_name = "test-service" + local_endpoint = zipkin_pb2.Endpoint( + service_name=service_name, port=9411 + ) + span_kind = SPAN_KIND_MAP_PROTOBUF[SpanKind.INTERNAL] + + expected_spans = zipkin_pb2.ListOfSpans( + spans=[ + zipkin_pb2.Span( + trace_id=trace_id.to_bytes( + length=16, byteorder="big", signed=False + ), + id=ZipkinSpanExporter.format_pbuf_span_id(span_id), + name=span_names[0], + timestamp=nsec_to_usec_round(start_times[0]), + duration=nsec_to_usec_round(durations[0]), + local_endpoint=local_endpoint, + kind=span_kind, + tags={ + "key_bool": "False", + "key_string": "hello_world", + "key_float": "111.22", + "otel.status_code": "2", + "otel.status_description": "Example description", + }, + debug=True, + parent_id=ZipkinSpanExporter.format_pbuf_span_id( + parent_id + ), + annotations=[ + zipkin_pb2.Annotation( + timestamp=nsec_to_usec_round(event_timestamp), + value=json.dumps( + { + "event0": { + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + } + } + ), + ), + ], + ), + zipkin_pb2.Span( + trace_id=trace_id.to_bytes( + length=16, byteorder="big", signed=False + ), + id=ZipkinSpanExporter.format_pbuf_span_id(parent_id), + name=span_names[1], + timestamp=nsec_to_usec_round(start_times[1]), + duration=nsec_to_usec_round(durations[1]), + local_endpoint=local_endpoint, + kind=span_kind, + tags={ + "key_resource": "some_resource", + "otel.status_code": "1", + }, + ), + zipkin_pb2.Span( + trace_id=trace_id.to_bytes( + length=16, byteorder="big", signed=False + ), + id=ZipkinSpanExporter.format_pbuf_span_id(other_id), + name=span_names[2], + timestamp=nsec_to_usec_round(start_times[2]), + duration=nsec_to_usec_round(durations[2]), + local_endpoint=local_endpoint, + kind=span_kind, + tags={ + "key_string": "hello_world", + "key_resource": "some_resource", + "otel.status_code": "1", + }, + ), + zipkin_pb2.Span( + trace_id=trace_id.to_bytes( + length=16, byteorder="big", signed=False + ), + id=ZipkinSpanExporter.format_pbuf_span_id(other_id), + name=span_names[3], + timestamp=nsec_to_usec_round(start_times[3]), + duration=nsec_to_usec_round(durations[3]), + local_endpoint=local_endpoint, + kind=span_kind, + tags={ + "otel.instrumentation_library.name": "name", + "otel.instrumentation_library.version": "version", + "otel.status_code": "1", + }, + ), + ], + ) + + exporter = ZipkinSpanExporter( + service_name, transport_format=TRANSPORT_FORMAT_PROTOBUF + ) + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export(otel_spans) + self.assertEqual(SpanExportResult.SUCCESS, status) + + # pylint: disable=unsubscriptable-object + kwargs = mock_post.call_args[1] + + self.assertEqual(kwargs["url"], "http://localhost:9411/api/v2/spans") + self.assertEqual( + kwargs["headers"]["Content-Type"], "application/x-protobuf" + ) + self.assertEqual( + zipkin_pb2.ListOfSpans.FromString(kwargs["data"]), expected_spans + ) + + def test_export_protobuf_max_tag_length(self): + service_name = "test-service" + + span_context = trace_api.SpanContext( + 0x0E0C63257DE34C926F9EFCD03927272E, + 0x04BF92DEEFC58C92, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + + span = trace._Span(name="test-span", context=span_context,) + + span.start() + span.resource = Resource({}) + # added here to preserve order + span.set_attribute("k1", "v" * 500) + span.set_attribute("k2", "v" * 50) + span.set_status(Status(StatusCode.ERROR, "Example description")) + span.end() + + exporter = ZipkinSpanExporter( + service_name, transport_format=TRANSPORT_FORMAT_PROTOBUF, + ) + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export([span]) + self.assertEqual(SpanExportResult.SUCCESS, status) + + # pylint: disable=unsubscriptable-object + kwargs = mock_post.call_args[1] + actual_spans = zipkin_pb2.ListOfSpans.FromString(kwargs["data"]) + span_tags = actual_spans.spans[0].tags + + self.assertEqual(len(span_tags["k1"]), 128) + self.assertEqual(len(span_tags["k2"]), 50) + + exporter = ZipkinSpanExporter( + service_name, + transport_format=TRANSPORT_FORMAT_PROTOBUF, + max_tag_value_length=2, + ) + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export([span]) + self.assertEqual(SpanExportResult.SUCCESS, status) + + # pylint: disable=unsubscriptable-object + kwargs = mock_post.call_args[1] + actual_spans = zipkin_pb2.ListOfSpans.FromString(kwargs["data"]) + span_tags = actual_spans.spans[0].tags + + self.assertEqual(len(span_tags["k1"]), 2) + self.assertEqual(len(span_tags["k2"]), 2) diff --git a/pyproject.toml b/pyproject.toml index 0c79406d05..73e41db223 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ exclude = ''' /( # generated files docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen| exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen| + exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen| opentelemetry-proto/src/opentelemetry/proto/collector| opentelemetry-proto/src/opentelemetry/proto/common| opentelemetry-proto/src/opentelemetry/proto/metrics| From 7ec8a022c80262e8bf6506e75bf0e8e358d11421 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 23 Nov 2020 08:20:25 -0800 Subject: [PATCH 0675/1517] remove github action (#1401) --- .github/workflows/docs.yml | 40 -------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 5dcff55d6a..0000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Docs - -on: - push: - branches: - - master - paths: - - 'docs/**' - - 'exporter/**' - - 'instrumentation/**' - - 'opentelemetry-python/opentelemetry-api/src/opentelemetry/**' - - 'opentelemetry-python/opentelemetry-sdk/src/opentelemetry/sdk/**' -env: - CONTRIB_REPO_SHA: master - -jobs: - docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} - uses: actions/checkout@v2 - with: - repository: open-telemetry/opentelemetry-python-contrib - ref: ${{ env.CONTRIB_REPO_SHA }} - path: opentelemetry-python-contrib - - name: Set up Python py38 - uses: actions/setup-python@v2 - with: - python-version: '3.8' - - name: Build docs - run: | - pip install --upgrade tox - tox -e docs - - name: Publish to gh-pages - uses: JamesIves/github-pages-deploy-action@2.0.2 - env: - ACCESS_TOKEN: ${{ secrets.DocsPushToken }} - BRANCH: gh-pages - FOLDER: docs/_build/html/ From 1b61e2050f32535fc2433729dcc3597b7a9d7384 Mon Sep 17 00:00:00 2001 From: snyder114 <31932630+snyder114@users.noreply.github.com> Date: Mon, 23 Nov 2020 11:13:25 -0800 Subject: [PATCH 0676/1517] Update README.rst (#1403) --- docs/examples/auto-instrumentation/README.rst | 80 +++++++++++-------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index a023acaa94..36132daf5e 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -1,29 +1,30 @@ -Autoinstrumentation -=================== +Auto-instrumentation +==================== + +One of the best ways to instrument Python applications is to use OpenTelemetry automatic instrumentation (auto-instrumentation). This approach is simple, easy, and doesn't require many code changes. You only need to install a few Python packages to successfully instrument your application's code. Overview -------- -This example shows how to use auto-instrumentation in OpenTelemetry. -This example is also based on a previous example for OpenTracing that -can be found +This example demonstrates how to use auto-instrumentation in OpenTelemetry. +The example is based on a previous OpenTracing example that +you can find `here `__. -The source files of these examples are available :scm_web:`here `. +The source files for these examples are available `here `__. -This example uses 2 scripts whose main difference is they being -instrumented manually or not: +This example uses two different scripts. The main difference between them is +whether or not they're instrumented manually: -1. ``server_instrumented.py`` which has been instrumented manually -2. ``server_uninstrumented.py`` which has not been instrumented manually +1. ``server_instrumented.py`` - instrumented manually +2. ``server_uninstrumented.py`` - not instrumented manually -The former will be run without the automatic instrumentation agent and -the latter with the automatic instrumentation agent. They should produce -the same result, showing that the automatic instrumentation agent does -the equivalent of what manual instrumentation does. +Run the first script without the automatic instrumentation agent and +the second with the agent. They should both produce the same results, +demonstrating that the automatic instrumentation agent does +exactly the same thing as manual instrumentation. -In order to understand this better, here is the relevant part of both -scripts: +To better understand auto-instrumentation, see the relevant part of both scripts: Manually instrumented server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -43,7 +44,7 @@ Manually instrumented server print(request.args.get("param")) return "served" -Publisher not instrumented manually +Server not instrumented manually ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``server_uninstrumented.py`` @@ -55,10 +56,11 @@ Publisher not instrumented manually print(request.args.get("param")) return "served" -Preparation +Prepare ----------- -This example will be executed in a separate virtual environment: +Execute the following example in a separate virtual environment. +Run the following commands to prepare for auto-instrumentation: .. code:: sh @@ -66,9 +68,13 @@ This example will be executed in a separate virtual environment: $ virtualenv auto_instrumentation $ source auto_instrumentation/bin/activate -Installation +Install ------------ +Run the following commands to install the appropriate packages. The +``opentelemetry-instrumentation`` package provides several +commands that help automatically instruments a program. + .. code:: sh $ pip install opentelemetry-sdk @@ -76,14 +82,18 @@ Installation $ pip install opentelemetry-instrumentation-flask $ pip install requests -Execution +Execute --------- -Execution of the manually instrumented server +This section guides you through the manual process of instrumenting +a server as well as the process of executing an automatically +instrumented server. + +Execute a manually instrumented server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This is done in 2 separate consoles, one to run each of the scripts that -make up this example: +Execute the server in two separate consoles, one to run each of the +scripts that make up this example: .. code:: sh @@ -95,8 +105,8 @@ make up this example: $ source auto_instrumentation/bin/activate $ python client.py testing -The execution of ``server_instrumented.py`` should return an output -similar to: +When you execute ``server_instrumented.py`` it returns a JSON response +similar to the following example: .. code:: sh @@ -130,25 +140,25 @@ similar to: "links": [] } -Execution of an automatically instrumented server +Execute an automatically instrumented server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now, kill the execution of ``server_instrumented.py`` with ``ctrl + c`` -and run this instead: +Stop the execution of ``server_instrumented.py`` with ``ctrl + c`` +and run the following command instead: .. code:: sh $ opentelemetry-instrument python server_uninstrumented.py -In the console where you previously executed ``client.py``, run again -this again: +In the console where you previously executed ``client.py``, run the following +command again: .. code:: sh $ python client.py testing -The execution of ``server_uninstrumented.py`` should return an output -similar to: +When you execute ``server_uninstrumented.py`` it returns a JSON response +similar to the following example: .. code:: sh @@ -185,5 +195,5 @@ similar to: "links": [] } -Both outputs are equivalent since the automatic instrumentation does -what the manual instrumentation does too. +You can see that both outputs are the same because automatic instrumentation does +exactly what manual instrumentation does. From d37ee5dad38e8d39ddb0b3a4b162c80ee0a9c6f2 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 23 Nov 2020 14:03:36 -0800 Subject: [PATCH 0677/1517] removing example app (#1410) --- .../opentelemetry-example-app/README.rst | 12 - .../opentelemetry-example-app/setup.cfg | 61 -- .../opentelemetry-example-app/setup.py | 26 - .../src/opentelemetry_example_app/__init__.py | 0 .../flask_example.py | 52 -- .../grpc/__init__.py | 0 .../grpc/gen/__init__.py | 19 - .../grpc/gen/codegen.py | 32 - .../grpc/gen/helloworld.proto | 35 - .../grpc/gen/helloworld_pb2.py | 131 ---- .../grpc/gen/helloworld_pb2_grpc.py | 46 -- .../grpc/gen/route_guide.proto | 108 ---- .../grpc/gen/route_guide_pb2.py | 328 ---------- .../grpc/gen/route_guide_pb2_grpc.py | 113 ---- .../grpc/hello_world_client.py | 87 --- .../grpc/hello_world_server.py | 87 --- .../grpc/route_guide_client.py | 179 ------ .../grpc/route_guide_db.json | 601 ------------------ .../grpc/route_guide_resources.py | 46 -- .../grpc/route_guide_server.py | 179 ------ .../src/opentelemetry_example_app/version.py | 15 - .../tests/__init__.py | 13 - .../tests/test_flask_example.py | 72 --- pyproject.toml | 1 - scripts/coverage.sh | 1 - tox.ini | 6 - 26 files changed, 2250 deletions(-) delete mode 100644 docs/examples/opentelemetry-example-app/README.rst delete mode 100644 docs/examples/opentelemetry-example-app/setup.cfg delete mode 100644 docs/examples/opentelemetry-example-app/setup.py delete mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/__init__.py delete mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py delete mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/__init__.py delete mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/__init__.py delete mode 100755 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/codegen.py delete mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld.proto delete mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2.py delete mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2_grpc.py delete mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide.proto delete mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2.py delete mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2_grpc.py delete mode 100755 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py delete mode 100755 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py delete mode 100755 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py delete mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_db.json delete mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_resources.py delete mode 100755 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py delete mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py delete mode 100644 docs/examples/opentelemetry-example-app/tests/__init__.py delete mode 100644 docs/examples/opentelemetry-example-app/tests/test_flask_example.py diff --git a/docs/examples/opentelemetry-example-app/README.rst b/docs/examples/opentelemetry-example-app/README.rst deleted file mode 100644 index 681963a526..0000000000 --- a/docs/examples/opentelemetry-example-app/README.rst +++ /dev/null @@ -1,12 +0,0 @@ -OpenTelemetry Example Application -================================= - -This package is a complete example of an application instrumented with OpenTelemetry. - -The purpose is to provide a reference for consumers that demonstrates how to use the OpenTelemetry API and SDK in a variety of use cases and how to set it up. - - -References ----------- - -* `OpenTelemetry Project `_ \ No newline at end of file diff --git a/docs/examples/opentelemetry-example-app/setup.cfg b/docs/examples/opentelemetry-example-app/setup.cfg deleted file mode 100644 index 87753ea720..0000000000 --- a/docs/examples/opentelemetry-example-app/setup.cfg +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-example-app -description = OpenTelemetry Example App -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-example-app -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages = find_namespace: -zip_safe = False -include_package_data = True -install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-sdk == 0.16.dev0 - opentelemetry-instrumentation-requests == 0.15.b0 - opentelemetry-instrumentation-flask == 0.15.b0 - opentelemetry-instrumentation-grpc == 0.15.b0 - flask - requests - protobuf>=3.13.0 - -[options.packages.find] -where = src -include = opentelemetry_example_app - -[options.entry_points] -opentelemetry_meter_provider = - sdk_meter_provider = opentelemetry.sdk.metrics:MeterProvider -opentelemetry_tracer_provider = - sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider diff --git a/docs/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py deleted file mode 100644 index 07b731b24e..0000000000 --- a/docs/examples/opentelemetry-example-app/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry_example_app", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"],) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/__init__.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py deleted file mode 100644 index 9838336504..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -This module serves as an example to integrate with flask, using -the requests library to perform downstream requests -""" -import flask -import requests - -import opentelemetry.instrumentation.requests -from opentelemetry import trace -from opentelemetry.instrumentation.flask import FlaskInstrumentor -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - ConsoleSpanExporter, - SimpleExportSpanProcessor, -) - -# The preferred tracer implementation must be set, as the opentelemetry-api -# defines the interface with a no-op implementation. -# It must be done before instrumenting any library -trace.set_tracer_provider(TracerProvider()) - -opentelemetry.instrumentation.requests.RequestsInstrumentor().instrument() - -trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) -) - -app = flask.Flask(__name__) - -FlaskInstrumentor().instrument_app(app) - - -@app.route("/") -def hello(): - tracer = trace.get_tracer(__name__) - with tracer.start_as_current_span("example-request"): - requests.get("http://www.example.com") - return "hello" diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/__init__.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/__init__.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/__init__.py deleted file mode 100644 index bcedda2270..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import importlib -import sys - -# gRPC-generated modules expect other generated modules to be on the path. -sys.path.extend(importlib.util.find_spec(__name__).submodule_search_locations) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/codegen.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/codegen.py deleted file mode 100755 index 72c24bad39..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/codegen.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from grpc_tools import protoc - - -def main(): - return protoc.main( - [ - "-I.", - "--python_out=.", - "--grpc_python_out=.", - "helloworld.proto", - "route_guide.proto", - ] - ) - - -if __name__ == "__main__": - main() diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld.proto b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld.proto deleted file mode 100644 index 446102166d..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld.proto +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// https://github.com/grpc/grpc/blob/master/examples/protos/helloworld.proto - -syntax = "proto3"; - -package helloworld; - -// The greeting service definition. -service Greeter { - // Sends a greeting - rpc SayHello (HelloRequest) returns (HelloReply) {} -} - -// The request message containing the user's name. -message HelloRequest { - string name = 1; -} - -// The response message containing the greetings -message HelloReply { - string message = 1; -} diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2.py deleted file mode 100644 index 0e37874ebc..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: helloworld.proto - -from google.protobuf import descriptor as _descriptor -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.FileDescriptor( - name='helloworld.proto', - package='helloworld', - syntax='proto3', - serialized_options=None, - serialized_pb=b'\n\x10helloworld.proto\x12\nhelloworld\"\x1c\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x1d\n\nHelloReply\x12\x0f\n\x07message\x18\x01 \x01(\t2I\n\x07Greeter\x12>\n\x08SayHello\x12\x18.helloworld.HelloRequest\x1a\x16.helloworld.HelloReply\"\x00\x62\x06proto3' -) - - - - -_HELLOREQUEST = _descriptor.Descriptor( - name='HelloRequest', - full_name='helloworld.HelloRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='helloworld.HelloRequest.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=32, - serialized_end=60, -) - - -_HELLOREPLY = _descriptor.Descriptor( - name='HelloReply', - full_name='helloworld.HelloReply', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='message', full_name='helloworld.HelloReply.message', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=62, - serialized_end=91, -) - -DESCRIPTOR.message_types_by_name['HelloRequest'] = _HELLOREQUEST -DESCRIPTOR.message_types_by_name['HelloReply'] = _HELLOREPLY -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -HelloRequest = _reflection.GeneratedProtocolMessageType('HelloRequest', (_message.Message,), { - 'DESCRIPTOR' : _HELLOREQUEST, - '__module__' : 'helloworld_pb2' - # @@protoc_insertion_point(class_scope:helloworld.HelloRequest) - }) -_sym_db.RegisterMessage(HelloRequest) - -HelloReply = _reflection.GeneratedProtocolMessageType('HelloReply', (_message.Message,), { - 'DESCRIPTOR' : _HELLOREPLY, - '__module__' : 'helloworld_pb2' - # @@protoc_insertion_point(class_scope:helloworld.HelloReply) - }) -_sym_db.RegisterMessage(HelloReply) - - - -_GREETER = _descriptor.ServiceDescriptor( - name='Greeter', - full_name='helloworld.Greeter', - file=DESCRIPTOR, - index=0, - serialized_options=None, - serialized_start=93, - serialized_end=166, - methods=[ - _descriptor.MethodDescriptor( - name='SayHello', - full_name='helloworld.Greeter.SayHello', - index=0, - containing_service=None, - input_type=_HELLOREQUEST, - output_type=_HELLOREPLY, - serialized_options=None, - ), -]) -_sym_db.RegisterServiceDescriptor(_GREETER) - -DESCRIPTOR.services_by_name['Greeter'] = _GREETER - -# @@protoc_insertion_point(module_scope) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2_grpc.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2_grpc.py deleted file mode 100644 index 18e07d1679..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2_grpc.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -import grpc - -import helloworld_pb2 as helloworld__pb2 - - -class GreeterStub(object): - """The greeting service definition. - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.SayHello = channel.unary_unary( - '/helloworld.Greeter/SayHello', - request_serializer=helloworld__pb2.HelloRequest.SerializeToString, - response_deserializer=helloworld__pb2.HelloReply.FromString, - ) - - -class GreeterServicer(object): - """The greeting service definition. - """ - - def SayHello(self, request, context): - """Sends a greeting - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_GreeterServicer_to_server(servicer, server): - rpc_method_handlers = { - 'SayHello': grpc.unary_unary_rpc_method_handler( - servicer.SayHello, - request_deserializer=helloworld__pb2.HelloRequest.FromString, - response_serializer=helloworld__pb2.HelloReply.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'helloworld.Greeter', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide.proto b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide.proto deleted file mode 100644 index 7017e93498..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide.proto +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// https://github.com/grpc/grpc/blob/master/examples/protos/route_guide.proto - -syntax = "proto3"; - -package routeguide; - -// Interface exported by the server. -service RouteGuide { - // A simple RPC. - // - // Obtains the feature at a given position. - // - // A feature with an empty name is returned if there's no feature at the given - // position. - rpc GetFeature(Point) returns (Feature) {} - - // A server-to-client streaming RPC. - // - // Obtains the Features available within the given Rectangle. Results are - // streamed rather than returned at once (e.g. in a response message with a - // repeated field), as the rectangle may cover a large area and contain a - // huge number of features. - rpc ListFeatures(Rectangle) returns (stream Feature) {} - - // A client-to-server streaming RPC. - // - // Accepts a stream of Points on a route being traversed, returning a - // RouteSummary when traversal is completed. - rpc RecordRoute(stream Point) returns (RouteSummary) {} - - // A Bidirectional streaming RPC. - // - // Accepts a stream of RouteNotes sent while a route is being traversed, - // while receiving other RouteNotes (e.g. from other users). - rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} -} - -// Points are represented as latitude-longitude pairs in the E7 representation -// (degrees multiplied by 10**7 and rounded to the nearest integer). -// Latitudes should be in the range +/- 90 degrees and longitude should be in -// the range +/- 180 degrees (inclusive). -message Point { - int32 latitude = 1; - int32 longitude = 2; -} - -// A latitude-longitude rectangle, represented as two diagonally opposite -// points "lo" and "hi". -message Rectangle { - // One corner of the rectangle. - Point lo = 1; - - // The other corner of the rectangle. - Point hi = 2; -} - -// A feature names something at a given point. -// -// If a feature could not be named, the name is empty. -message Feature { - // The name of the feature. - string name = 1; - - // The point where the feature is detected. - Point location = 2; -} - -// A RouteNote is a message sent while at a given point. -message RouteNote { - // The location from which the message is sent. - Point location = 1; - - // The message to be sent. - string message = 2; -} - -// A RouteSummary is received in response to a RecordRoute rpc. -// -// It contains the number of individual points received, the number of -// detected features, and the total distance covered as the cumulative sum of -// the distance between each point. -message RouteSummary { - // The number of points received. - int32 point_count = 1; - - // The number of known features passed while traversing the route. - int32 feature_count = 2; - - // The distance covered in metres. - int32 distance = 3; - - // The duration of the traversal in seconds. - int32 elapsed_time = 4; -} diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2.py deleted file mode 100644 index 4a4006a2c7..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2.py +++ /dev/null @@ -1,328 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: route_guide.proto - -from google.protobuf import descriptor as _descriptor -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.FileDescriptor( - name='route_guide.proto', - package='routeguide', - syntax='proto3', - serialized_options=None, - serialized_pb=b'\n\x11route_guide.proto\x12\nrouteguide\",\n\x05Point\x12\x10\n\x08latitude\x18\x01 \x01(\x05\x12\x11\n\tlongitude\x18\x02 \x01(\x05\"I\n\tRectangle\x12\x1d\n\x02lo\x18\x01 \x01(\x0b\x32\x11.routeguide.Point\x12\x1d\n\x02hi\x18\x02 \x01(\x0b\x32\x11.routeguide.Point\"<\n\x07\x46\x65\x61ture\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x08location\x18\x02 \x01(\x0b\x32\x11.routeguide.Point\"A\n\tRouteNote\x12#\n\x08location\x18\x01 \x01(\x0b\x32\x11.routeguide.Point\x12\x0f\n\x07message\x18\x02 \x01(\t\"b\n\x0cRouteSummary\x12\x13\n\x0bpoint_count\x18\x01 \x01(\x05\x12\x15\n\rfeature_count\x18\x02 \x01(\x05\x12\x10\n\x08\x64istance\x18\x03 \x01(\x05\x12\x14\n\x0c\x65lapsed_time\x18\x04 \x01(\x05\x32\x85\x02\n\nRouteGuide\x12\x36\n\nGetFeature\x12\x11.routeguide.Point\x1a\x13.routeguide.Feature\"\x00\x12>\n\x0cListFeatures\x12\x15.routeguide.Rectangle\x1a\x13.routeguide.Feature\"\x00\x30\x01\x12>\n\x0bRecordRoute\x12\x11.routeguide.Point\x1a\x18.routeguide.RouteSummary\"\x00(\x01\x12?\n\tRouteChat\x12\x15.routeguide.RouteNote\x1a\x15.routeguide.RouteNote\"\x00(\x01\x30\x01\x62\x06proto3' -) - - - - -_POINT = _descriptor.Descriptor( - name='Point', - full_name='routeguide.Point', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='latitude', full_name='routeguide.Point.latitude', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='longitude', full_name='routeguide.Point.longitude', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=33, - serialized_end=77, -) - - -_RECTANGLE = _descriptor.Descriptor( - name='Rectangle', - full_name='routeguide.Rectangle', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='lo', full_name='routeguide.Rectangle.lo', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='hi', full_name='routeguide.Rectangle.hi', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=79, - serialized_end=152, -) - - -_FEATURE = _descriptor.Descriptor( - name='Feature', - full_name='routeguide.Feature', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='routeguide.Feature.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='location', full_name='routeguide.Feature.location', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=154, - serialized_end=214, -) - - -_ROUTENOTE = _descriptor.Descriptor( - name='RouteNote', - full_name='routeguide.RouteNote', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='location', full_name='routeguide.RouteNote.location', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='message', full_name='routeguide.RouteNote.message', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=216, - serialized_end=281, -) - - -_ROUTESUMMARY = _descriptor.Descriptor( - name='RouteSummary', - full_name='routeguide.RouteSummary', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='point_count', full_name='routeguide.RouteSummary.point_count', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='feature_count', full_name='routeguide.RouteSummary.feature_count', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='distance', full_name='routeguide.RouteSummary.distance', index=2, - number=3, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='elapsed_time', full_name='routeguide.RouteSummary.elapsed_time', index=3, - number=4, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=283, - serialized_end=381, -) - -_RECTANGLE.fields_by_name['lo'].message_type = _POINT -_RECTANGLE.fields_by_name['hi'].message_type = _POINT -_FEATURE.fields_by_name['location'].message_type = _POINT -_ROUTENOTE.fields_by_name['location'].message_type = _POINT -DESCRIPTOR.message_types_by_name['Point'] = _POINT -DESCRIPTOR.message_types_by_name['Rectangle'] = _RECTANGLE -DESCRIPTOR.message_types_by_name['Feature'] = _FEATURE -DESCRIPTOR.message_types_by_name['RouteNote'] = _ROUTENOTE -DESCRIPTOR.message_types_by_name['RouteSummary'] = _ROUTESUMMARY -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -Point = _reflection.GeneratedProtocolMessageType('Point', (_message.Message,), { - 'DESCRIPTOR' : _POINT, - '__module__' : 'route_guide_pb2' - # @@protoc_insertion_point(class_scope:routeguide.Point) - }) -_sym_db.RegisterMessage(Point) - -Rectangle = _reflection.GeneratedProtocolMessageType('Rectangle', (_message.Message,), { - 'DESCRIPTOR' : _RECTANGLE, - '__module__' : 'route_guide_pb2' - # @@protoc_insertion_point(class_scope:routeguide.Rectangle) - }) -_sym_db.RegisterMessage(Rectangle) - -Feature = _reflection.GeneratedProtocolMessageType('Feature', (_message.Message,), { - 'DESCRIPTOR' : _FEATURE, - '__module__' : 'route_guide_pb2' - # @@protoc_insertion_point(class_scope:routeguide.Feature) - }) -_sym_db.RegisterMessage(Feature) - -RouteNote = _reflection.GeneratedProtocolMessageType('RouteNote', (_message.Message,), { - 'DESCRIPTOR' : _ROUTENOTE, - '__module__' : 'route_guide_pb2' - # @@protoc_insertion_point(class_scope:routeguide.RouteNote) - }) -_sym_db.RegisterMessage(RouteNote) - -RouteSummary = _reflection.GeneratedProtocolMessageType('RouteSummary', (_message.Message,), { - 'DESCRIPTOR' : _ROUTESUMMARY, - '__module__' : 'route_guide_pb2' - # @@protoc_insertion_point(class_scope:routeguide.RouteSummary) - }) -_sym_db.RegisterMessage(RouteSummary) - - - -_ROUTEGUIDE = _descriptor.ServiceDescriptor( - name='RouteGuide', - full_name='routeguide.RouteGuide', - file=DESCRIPTOR, - index=0, - serialized_options=None, - serialized_start=384, - serialized_end=645, - methods=[ - _descriptor.MethodDescriptor( - name='GetFeature', - full_name='routeguide.RouteGuide.GetFeature', - index=0, - containing_service=None, - input_type=_POINT, - output_type=_FEATURE, - serialized_options=None, - ), - _descriptor.MethodDescriptor( - name='ListFeatures', - full_name='routeguide.RouteGuide.ListFeatures', - index=1, - containing_service=None, - input_type=_RECTANGLE, - output_type=_FEATURE, - serialized_options=None, - ), - _descriptor.MethodDescriptor( - name='RecordRoute', - full_name='routeguide.RouteGuide.RecordRoute', - index=2, - containing_service=None, - input_type=_POINT, - output_type=_ROUTESUMMARY, - serialized_options=None, - ), - _descriptor.MethodDescriptor( - name='RouteChat', - full_name='routeguide.RouteGuide.RouteChat', - index=3, - containing_service=None, - input_type=_ROUTENOTE, - output_type=_ROUTENOTE, - serialized_options=None, - ), -]) -_sym_db.RegisterServiceDescriptor(_ROUTEGUIDE) - -DESCRIPTOR.services_by_name['RouteGuide'] = _ROUTEGUIDE - -# @@protoc_insertion_point(module_scope) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2_grpc.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2_grpc.py deleted file mode 100644 index 05c1b79312..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2_grpc.py +++ /dev/null @@ -1,113 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -import grpc - -import route_guide_pb2 as route__guide__pb2 - - -class RouteGuideStub(object): - """Interface exported by the server. - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.GetFeature = channel.unary_unary( - '/routeguide.RouteGuide/GetFeature', - request_serializer=route__guide__pb2.Point.SerializeToString, - response_deserializer=route__guide__pb2.Feature.FromString, - ) - self.ListFeatures = channel.unary_stream( - '/routeguide.RouteGuide/ListFeatures', - request_serializer=route__guide__pb2.Rectangle.SerializeToString, - response_deserializer=route__guide__pb2.Feature.FromString, - ) - self.RecordRoute = channel.stream_unary( - '/routeguide.RouteGuide/RecordRoute', - request_serializer=route__guide__pb2.Point.SerializeToString, - response_deserializer=route__guide__pb2.RouteSummary.FromString, - ) - self.RouteChat = channel.stream_stream( - '/routeguide.RouteGuide/RouteChat', - request_serializer=route__guide__pb2.RouteNote.SerializeToString, - response_deserializer=route__guide__pb2.RouteNote.FromString, - ) - - -class RouteGuideServicer(object): - """Interface exported by the server. - """ - - def GetFeature(self, request, context): - """A simple RPC. - - Obtains the feature at a given position. - - A feature with an empty name is returned if there's no feature at the given - position. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ListFeatures(self, request, context): - """A server-to-client streaming RPC. - - Obtains the Features available within the given Rectangle. Results are - streamed rather than returned at once (e.g. in a response message with a - repeated field), as the rectangle may cover a large area and contain a - huge number of features. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def RecordRoute(self, request_iterator, context): - """A client-to-server streaming RPC. - - Accepts a stream of Points on a route being traversed, returning a - RouteSummary when traversal is completed. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def RouteChat(self, request_iterator, context): - """A Bidirectional streaming RPC. - - Accepts a stream of RouteNotes sent while a route is being traversed, - while receiving other RouteNotes (e.g. from other users). - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_RouteGuideServicer_to_server(servicer, server): - rpc_method_handlers = { - 'GetFeature': grpc.unary_unary_rpc_method_handler( - servicer.GetFeature, - request_deserializer=route__guide__pb2.Point.FromString, - response_serializer=route__guide__pb2.Feature.SerializeToString, - ), - 'ListFeatures': grpc.unary_stream_rpc_method_handler( - servicer.ListFeatures, - request_deserializer=route__guide__pb2.Rectangle.FromString, - response_serializer=route__guide__pb2.Feature.SerializeToString, - ), - 'RecordRoute': grpc.stream_unary_rpc_method_handler( - servicer.RecordRoute, - request_deserializer=route__guide__pb2.Point.FromString, - response_serializer=route__guide__pb2.RouteSummary.SerializeToString, - ), - 'RouteChat': grpc.stream_stream_rpc_method_handler( - servicer.RouteChat, - request_deserializer=route__guide__pb2.RouteNote.FromString, - response_serializer=route__guide__pb2.RouteNote.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'routeguide.RouteGuide', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py deleted file mode 100755 index 2d07dec829..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=import-error - -"""The Python implementation of the GRPC helloworld.Greeter client. - -Note that you need ``opentelemetry-instrumentation-grpc`` and ``protobuf`` to be installed -to run these examples. To run this script in the context of the example app, -install ``opentelemetry-example-app``:: - - pip install -e instrumentation/opentelemetry-instrumentation-grpc/ - pip install -e docs/examples/opentelemetry-example-app - -Then run the server in one shell:: - - python -m opentelemetry_example_app.grpc.hello_world_server - -and the client in another:: - - python -m opentelemetry_example_app.grpc.hello_world_client - -See also: -https://github.com/grpc/grpc/blob/master/examples/python/helloworld/greeter_client.py -https://github.com/grpc/grpc/blob/v1.16.x/examples/python/interceptors/default_value/greeter_client.py -""" - -import logging - -import grpc - -from opentelemetry import trace -from opentelemetry.instrumentation.grpc import client_interceptor -from opentelemetry.instrumentation.grpc.grpcext import intercept_channel -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - ConsoleSpanExporter, - SimpleExportSpanProcessor, -) - -try: - # Relative imports should work in the context of the package, e.g.: - # `python -m opentelemetry_example_app.grpc.hello_world_client`. - from .gen import helloworld_pb2, helloworld_pb2_grpc -except ImportError: - # This will fail when running the file as a script, e.g.: - # `./hello_world_client.py` - # fall back to importing from the same directory in this case. - from gen import helloworld_pb2, helloworld_pb2_grpc - -trace.set_tracer_provider(TracerProvider()) -trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) -) - - -def run(): - # NOTE(gRPC Python Team): .close() is possible on a channel and should be - # used in circumstances in which the with statement does not fit the needs - # of the code. - with grpc.insecure_channel("localhost:50051") as channel: - - channel = intercept_channel(channel, client_interceptor()) - - stub = helloworld_pb2_grpc.GreeterStub(channel) - - # stub.SayHello is a _InterceptorUnaryUnaryMultiCallable - response = stub.SayHello(helloworld_pb2.HelloRequest(name="YOU")) - - print("Greeter client received: " + response.message) - - -if __name__ == "__main__": - logging.basicConfig() - run() diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py deleted file mode 100755 index 29035f17aa..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=import-error - -"""The Python implementation of the GRPC helloworld.Greeter server. - -Note that you need ``opentelemetry-instrumentation-grpc`` and ``protobuf`` to be installed -to run these examples. To run this script in the context of the example app, -install ``opentelemetry-example-app``:: - - pip install -e instrumentation/opentelemetry-instrumentation-grpc/ - pip install -e docs/examples/opentelemetry-example-app - -Then run the server in one shell:: - - python -m opentelemetry_example_app.grpc.hello_world_client - -and the client in another:: - - python -m opentelemetry_example_app.grpc.hello_world_server - -See also: -https://github.com/grpc/grpc/blob/master/examples/python/helloworld/greeter_server.py -""" - -import logging -from concurrent import futures - -import grpc - -from opentelemetry import trace -from opentelemetry.instrumentation.grpc import server_interceptor -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - ConsoleSpanExporter, - SimpleExportSpanProcessor, -) - -try: - # Relative imports should work in the context of the package, e.g.: - # `python -m opentelemetry_example_app.grpc.hello_world_server`. - from .gen import helloworld_pb2, helloworld_pb2_grpc -except ImportError: - # This will fail when running the file as a script, e.g.: - # `./hello_world_server.py` - # fall back to importing from the same directory in this case. - from gen import helloworld_pb2, helloworld_pb2_grpc - -trace.set_tracer_provider(TracerProvider()) -trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) -) - - -class Greeter(helloworld_pb2_grpc.GreeterServicer): - def SayHello(self, request, context): - return helloworld_pb2.HelloReply(message="Hello, %s!" % request.name) - - -def serve(): - - server = grpc.server( - futures.ThreadPoolExecutor(), interceptors=[server_interceptor()], - ) - - helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server) - server.add_insecure_port("[::]:50051") - server.start() - server.wait_for_termination() - - -if __name__ == "__main__": - logging.basicConfig() - serve() diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py deleted file mode 100755 index 4e8b13704d..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=import-error - -"""The Python implementation of the gRPC route guide client. - -Note that you need ``opentelemetry-instrumentation-grpc`` and ``protobuf`` to be installed -to run these examples. To run this script in the context of the example app, -install ``opentelemetry-example-app``:: - - pip install -e instrumentation/opentelemetry-instrumentation-grpc/ - pip install -e docs/examples/opentelemetry-example-app - -Then run the server in one shell:: - - python -m opentelemetry_example_app.grpc.route_guide_server - -and the client in another:: - - python -m opentelemetry_example_app.grpc.route_guide_client - -See also: -https://github.com/grpc/grpc/tree/master/examples/python/route_guide -""" - - -import logging -import random - -import grpc - -from opentelemetry import trace -from opentelemetry.instrumentation.grpc import client_interceptor -from opentelemetry.instrumentation.grpc.grpcext import intercept_channel -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - ConsoleSpanExporter, - SimpleExportSpanProcessor, -) - -try: - # Relative imports should work in the context of the package, e.g.: - # `python -m opentelemetry_example_app.grpc.route_guide_client`. - from .gen import route_guide_pb2, route_guide_pb2_grpc - from . import route_guide_resources -except ImportError: - # This will fail when running the file as a script, e.g.: - # `./route_guide_client.py` - # fall back to importing from the same directory in this case. - from gen import route_guide_pb2, route_guide_pb2_grpc - import route_guide_resources - -trace.set_tracer_provider(TracerProvider()) -trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) -) - - -def make_route_note(message, latitude, longitude): - return route_guide_pb2.RouteNote( - message=message, - location=route_guide_pb2.Point(latitude=latitude, longitude=longitude), - ) - - -def guide_get_one_feature(stub, point): - feature = stub.GetFeature(point) - if not feature.location: - print("Server returned incomplete feature") - return - - if feature.name: - print("Feature called %s at %s" % (feature.name, feature.location)) - else: - print("Found no feature at %s" % feature.location) - - -def guide_get_feature(stub): - guide_get_one_feature( - stub, route_guide_pb2.Point(latitude=409146138, longitude=-746188906) - ) - guide_get_one_feature(stub, route_guide_pb2.Point(latitude=0, longitude=0)) - - -def guide_list_features(stub): - rectangle = route_guide_pb2.Rectangle( - lo=route_guide_pb2.Point(latitude=400000000, longitude=-750000000), - hi=route_guide_pb2.Point(latitude=420000000, longitude=-730000000), - ) - print("Looking for features between 40, -75 and 42, -73") - - features = stub.ListFeatures(rectangle) - - for feature in features: - print("Feature called %s at %s" % (feature.name, feature.location)) - - -def generate_route(feature_list): - for _ in range(0, 10): - random_feature = feature_list[random.randint(0, len(feature_list) - 1)] - print("Visiting point %s" % random_feature.location) - yield random_feature.location - - -def guide_record_route(stub): - feature_list = route_guide_resources.read_route_guide_database() - - route_iterator = generate_route(feature_list) - route_summary = stub.RecordRoute(route_iterator) - print("Finished trip with %s points " % route_summary.point_count) - print("Passed %s features " % route_summary.feature_count) - print("Travelled %s meters " % route_summary.distance) - print("It took %s seconds " % route_summary.elapsed_time) - - -def generate_messages(): - messages = [ - make_route_note("First message", 0, 0), - make_route_note("Second message", 0, 1), - make_route_note("Third message", 1, 0), - make_route_note("Fourth message", 0, 0), - make_route_note("Fifth message", 1, 0), - ] - for msg in messages: - print("Sending %s at %s" % (msg.message, msg.location)) - yield msg - - -def guide_route_chat(stub): - responses = stub.RouteChat(generate_messages()) - for response in responses: - print( - "Received message %s at %s" % (response.message, response.location) - ) - - -def run(): - - # NOTE(gRPC Python Team): .close() is possible on a channel and should be - # used in circumstances in which the with statement does not fit the needs - # of the code. - with grpc.insecure_channel("localhost:50051") as channel: - channel = intercept_channel(channel, client_interceptor()) - - stub = route_guide_pb2_grpc.RouteGuideStub(channel) - - # Unary - print("-------------- GetFeature --------------") - guide_get_feature(stub) - - # Server streaming - print("-------------- ListFeatures --------------") - guide_list_features(stub) - - # Client streaming - print("-------------- RecordRoute --------------") - guide_record_route(stub) - - # Bidirectional streaming - print("-------------- RouteChat --------------") - guide_route_chat(stub) - - -if __name__ == "__main__": - logging.basicConfig() - run() diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_db.json b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_db.json deleted file mode 100644 index 9d6a980ab7..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_db.json +++ /dev/null @@ -1,601 +0,0 @@ -[{ - "location": { - "latitude": 407838351, - "longitude": -746143763 - }, - "name": "Patriots Path, Mendham, NJ 07945, USA" -}, { - "location": { - "latitude": 408122808, - "longitude": -743999179 - }, - "name": "101 New Jersey 10, Whippany, NJ 07981, USA" -}, { - "location": { - "latitude": 413628156, - "longitude": -749015468 - }, - "name": "U.S. 6, Shohola, PA 18458, USA" -}, { - "location": { - "latitude": 419999544, - "longitude": -740371136 - }, - "name": "5 Conners Road, Kingston, NY 12401, USA" -}, { - "location": { - "latitude": 414008389, - "longitude": -743951297 - }, - "name": "Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA" -}, { - "location": { - "latitude": 419611318, - "longitude": -746524769 - }, - "name": "287 Flugertown Road, Livingston Manor, NY 12758, USA" -}, { - "location": { - "latitude": 406109563, - "longitude": -742186778 - }, - "name": "4001 Tremley Point Road, Linden, NJ 07036, USA" -}, { - "location": { - "latitude": 416802456, - "longitude": -742370183 - }, - "name": "352 South Mountain Road, Wallkill, NY 12589, USA" -}, { - "location": { - "latitude": 412950425, - "longitude": -741077389 - }, - "name": "Bailey Turn Road, Harriman, NY 10926, USA" -}, { - "location": { - "latitude": 412144655, - "longitude": -743949739 - }, - "name": "193-199 Wawayanda Road, Hewitt, NJ 07421, USA" -}, { - "location": { - "latitude": 415736605, - "longitude": -742847522 - }, - "name": "406-496 Ward Avenue, Pine Bush, NY 12566, USA" -}, { - "location": { - "latitude": 413843930, - "longitude": -740501726 - }, - "name": "162 Merrill Road, Highland Mills, NY 10930, USA" -}, { - "location": { - "latitude": 410873075, - "longitude": -744459023 - }, - "name": "Clinton Road, West Milford, NJ 07480, USA" -}, { - "location": { - "latitude": 412346009, - "longitude": -744026814 - }, - "name": "16 Old Brook Lane, Warwick, NY 10990, USA" -}, { - "location": { - "latitude": 402948455, - "longitude": -747903913 - }, - "name": "3 Drake Lane, Pennington, NJ 08534, USA" -}, { - "location": { - "latitude": 406337092, - "longitude": -740122226 - }, - "name": "6324 8th Avenue, Brooklyn, NY 11220, USA" -}, { - "location": { - "latitude": 406421967, - "longitude": -747727624 - }, - "name": "1 Merck Access Road, Whitehouse Station, NJ 08889, USA" -}, { - "location": { - "latitude": 416318082, - "longitude": -749677716 - }, - "name": "78-98 Schalck Road, Narrowsburg, NY 12764, USA" -}, { - "location": { - "latitude": 415301720, - "longitude": -748416257 - }, - "name": "282 Lakeview Drive Road, Highland Lake, NY 12743, USA" -}, { - "location": { - "latitude": 402647019, - "longitude": -747071791 - }, - "name": "330 Evelyn Avenue, Hamilton Township, NJ 08619, USA" -}, { - "location": { - "latitude": 412567807, - "longitude": -741058078 - }, - "name": "New York State Reference Route 987E, Southfields, NY 10975, USA" -}, { - "location": { - "latitude": 416855156, - "longitude": -744420597 - }, - "name": "103-271 Tempaloni Road, Ellenville, NY 12428, USA" -}, { - "location": { - "latitude": 404663628, - "longitude": -744820157 - }, - "name": "1300 Airport Road, North Brunswick Township, NJ 08902, USA" -}, { - "location": { - "latitude": 407113723, - "longitude": -749746483 - }, - "name": "" -}, { - "location": { - "latitude": 402133926, - "longitude": -743613249 - }, - "name": "" -}, { - "location": { - "latitude": 400273442, - "longitude": -741220915 - }, - "name": "" -}, { - "location": { - "latitude": 411236786, - "longitude": -744070769 - }, - "name": "" -}, { - "location": { - "latitude": 411633782, - "longitude": -746784970 - }, - "name": "211-225 Plains Road, Augusta, NJ 07822, USA" -}, { - "location": { - "latitude": 415830701, - "longitude": -742952812 - }, - "name": "" -}, { - "location": { - "latitude": 413447164, - "longitude": -748712898 - }, - "name": "165 Pedersen Ridge Road, Milford, PA 18337, USA" -}, { - "location": { - "latitude": 405047245, - "longitude": -749800722 - }, - "name": "100-122 Locktown Road, Frenchtown, NJ 08825, USA" -}, { - "location": { - "latitude": 418858923, - "longitude": -746156790 - }, - "name": "" -}, { - "location": { - "latitude": 417951888, - "longitude": -748484944 - }, - "name": "650-652 Willi Hill Road, Swan Lake, NY 12783, USA" -}, { - "location": { - "latitude": 407033786, - "longitude": -743977337 - }, - "name": "26 East 3rd Street, New Providence, NJ 07974, USA" -}, { - "location": { - "latitude": 417548014, - "longitude": -740075041 - }, - "name": "" -}, { - "location": { - "latitude": 410395868, - "longitude": -744972325 - }, - "name": "" -}, { - "location": { - "latitude": 404615353, - "longitude": -745129803 - }, - "name": "" -}, { - "location": { - "latitude": 406589790, - "longitude": -743560121 - }, - "name": "611 Lawrence Avenue, Westfield, NJ 07090, USA" -}, { - "location": { - "latitude": 414653148, - "longitude": -740477477 - }, - "name": "18 Lannis Avenue, New Windsor, NY 12553, USA" -}, { - "location": { - "latitude": 405957808, - "longitude": -743255336 - }, - "name": "82-104 Amherst Avenue, Colonia, NJ 07067, USA" -}, { - "location": { - "latitude": 411733589, - "longitude": -741648093 - }, - "name": "170 Seven Lakes Drive, Sloatsburg, NY 10974, USA" -}, { - "location": { - "latitude": 412676291, - "longitude": -742606606 - }, - "name": "1270 Lakes Road, Monroe, NY 10950, USA" -}, { - "location": { - "latitude": 409224445, - "longitude": -748286738 - }, - "name": "509-535 Alphano Road, Great Meadows, NJ 07838, USA" -}, { - "location": { - "latitude": 406523420, - "longitude": -742135517 - }, - "name": "652 Garden Street, Elizabeth, NJ 07202, USA" -}, { - "location": { - "latitude": 401827388, - "longitude": -740294537 - }, - "name": "349 Sea Spray Court, Neptune City, NJ 07753, USA" -}, { - "location": { - "latitude": 410564152, - "longitude": -743685054 - }, - "name": "13-17 Stanley Street, West Milford, NJ 07480, USA" -}, { - "location": { - "latitude": 408472324, - "longitude": -740726046 - }, - "name": "47 Industrial Avenue, Teterboro, NJ 07608, USA" -}, { - "location": { - "latitude": 412452168, - "longitude": -740214052 - }, - "name": "5 White Oak Lane, Stony Point, NY 10980, USA" -}, { - "location": { - "latitude": 409146138, - "longitude": -746188906 - }, - "name": "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" -}, { - "location": { - "latitude": 404701380, - "longitude": -744781745 - }, - "name": "1007 Jersey Avenue, New Brunswick, NJ 08901, USA" -}, { - "location": { - "latitude": 409642566, - "longitude": -746017679 - }, - "name": "6 East Emerald Isle Drive, Lake Hopatcong, NJ 07849, USA" -}, { - "location": { - "latitude": 408031728, - "longitude": -748645385 - }, - "name": "1358-1474 New Jersey 57, Port Murray, NJ 07865, USA" -}, { - "location": { - "latitude": 413700272, - "longitude": -742135189 - }, - "name": "367 Prospect Road, Chester, NY 10918, USA" -}, { - "location": { - "latitude": 404310607, - "longitude": -740282632 - }, - "name": "10 Simon Lake Drive, Atlantic Highlands, NJ 07716, USA" -}, { - "location": { - "latitude": 409319800, - "longitude": -746201391 - }, - "name": "11 Ward Street, Mount Arlington, NJ 07856, USA" -}, { - "location": { - "latitude": 406685311, - "longitude": -742108603 - }, - "name": "300-398 Jefferson Avenue, Elizabeth, NJ 07201, USA" -}, { - "location": { - "latitude": 419018117, - "longitude": -749142781 - }, - "name": "43 Dreher Road, Roscoe, NY 12776, USA" -}, { - "location": { - "latitude": 412856162, - "longitude": -745148837 - }, - "name": "Swan Street, Pine Island, NY 10969, USA" -}, { - "location": { - "latitude": 416560744, - "longitude": -746721964 - }, - "name": "66 Pleasantview Avenue, Monticello, NY 12701, USA" -}, { - "location": { - "latitude": 405314270, - "longitude": -749836354 - }, - "name": "" -}, { - "location": { - "latitude": 414219548, - "longitude": -743327440 - }, - "name": "" -}, { - "location": { - "latitude": 415534177, - "longitude": -742900616 - }, - "name": "565 Winding Hills Road, Montgomery, NY 12549, USA" -}, { - "location": { - "latitude": 406898530, - "longitude": -749127080 - }, - "name": "231 Rocky Run Road, Glen Gardner, NJ 08826, USA" -}, { - "location": { - "latitude": 407586880, - "longitude": -741670168 - }, - "name": "100 Mount Pleasant Avenue, Newark, NJ 07104, USA" -}, { - "location": { - "latitude": 400106455, - "longitude": -742870190 - }, - "name": "517-521 Huntington Drive, Manchester Township, NJ 08759, USA" -}, { - "location": { - "latitude": 400066188, - "longitude": -746793294 - }, - "name": "" -}, { - "location": { - "latitude": 418803880, - "longitude": -744102673 - }, - "name": "40 Mountain Road, Napanoch, NY 12458, USA" -}, { - "location": { - "latitude": 414204288, - "longitude": -747895140 - }, - "name": "" -}, { - "location": { - "latitude": 414777405, - "longitude": -740615601 - }, - "name": "" -}, { - "location": { - "latitude": 415464475, - "longitude": -747175374 - }, - "name": "48 North Road, Forestburgh, NY 12777, USA" -}, { - "location": { - "latitude": 404062378, - "longitude": -746376177 - }, - "name": "" -}, { - "location": { - "latitude": 405688272, - "longitude": -749285130 - }, - "name": "" -}, { - "location": { - "latitude": 400342070, - "longitude": -748788996 - }, - "name": "" -}, { - "location": { - "latitude": 401809022, - "longitude": -744157964 - }, - "name": "" -}, { - "location": { - "latitude": 404226644, - "longitude": -740517141 - }, - "name": "9 Thompson Avenue, Leonardo, NJ 07737, USA" -}, { - "location": { - "latitude": 410322033, - "longitude": -747871659 - }, - "name": "" -}, { - "location": { - "latitude": 407100674, - "longitude": -747742727 - }, - "name": "" -}, { - "location": { - "latitude": 418811433, - "longitude": -741718005 - }, - "name": "213 Bush Road, Stone Ridge, NY 12484, USA" -}, { - "location": { - "latitude": 415034302, - "longitude": -743850945 - }, - "name": "" -}, { - "location": { - "latitude": 411349992, - "longitude": -743694161 - }, - "name": "" -}, { - "location": { - "latitude": 404839914, - "longitude": -744759616 - }, - "name": "1-17 Bergen Court, New Brunswick, NJ 08901, USA" -}, { - "location": { - "latitude": 414638017, - "longitude": -745957854 - }, - "name": "35 Oakland Valley Road, Cuddebackville, NY 12729, USA" -}, { - "location": { - "latitude": 412127800, - "longitude": -740173578 - }, - "name": "" -}, { - "location": { - "latitude": 401263460, - "longitude": -747964303 - }, - "name": "" -}, { - "location": { - "latitude": 412843391, - "longitude": -749086026 - }, - "name": "" -}, { - "location": { - "latitude": 418512773, - "longitude": -743067823 - }, - "name": "" -}, { - "location": { - "latitude": 404318328, - "longitude": -740835638 - }, - "name": "42-102 Main Street, Belford, NJ 07718, USA" -}, { - "location": { - "latitude": 419020746, - "longitude": -741172328 - }, - "name": "" -}, { - "location": { - "latitude": 404080723, - "longitude": -746119569 - }, - "name": "" -}, { - "location": { - "latitude": 401012643, - "longitude": -744035134 - }, - "name": "" -}, { - "location": { - "latitude": 404306372, - "longitude": -741079661 - }, - "name": "" -}, { - "location": { - "latitude": 403966326, - "longitude": -748519297 - }, - "name": "" -}, { - "location": { - "latitude": 405002031, - "longitude": -748407866 - }, - "name": "" -}, { - "location": { - "latitude": 409532885, - "longitude": -742200683 - }, - "name": "" -}, { - "location": { - "latitude": 416851321, - "longitude": -742674555 - }, - "name": "" -}, { - "location": { - "latitude": 406411633, - "longitude": -741722051 - }, - "name": "3387 Richmond Terrace, Staten Island, NY 10303, USA" -}, { - "location": { - "latitude": 413069058, - "longitude": -744597778 - }, - "name": "261 Van Sickle Road, Goshen, NY 10924, USA" -}, { - "location": { - "latitude": 418465462, - "longitude": -746859398 - }, - "name": "" -}, { - "location": { - "latitude": 411733222, - "longitude": -744228360 - }, - "name": "" -}, { - "location": { - "latitude": 410248224, - "longitude": -747127767 - }, - "name": "3 Hasta Way, Newton, NJ 07860, USA" -}] diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_resources.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_resources.py deleted file mode 100644 index c7977698ba..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_resources.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# https://github.com/grpc/grpc/tree/master/examples/python/route_guide - -# pylint: disable=import-error - -"""Common resources used in the gRPC route guide example.""" - -import json -import os - -import route_guide_pb2 - - -def read_route_guide_database(): - """Reads the route guide database. - - Returns: - The full contents of the route guide database as a sequence of - route_guide_pb2.Features. - """ - feature_list = [] - db_file = os.path.join(os.path.dirname(__file__), "route_guide_db.json") - with open(db_file) as route_guide_db_file: - for item in json.load(route_guide_db_file): - feature = route_guide_pb2.Feature( - name=item["name"], - location=route_guide_pb2.Point( - latitude=item["location"]["latitude"], - longitude=item["location"]["longitude"], - ), - ) - feature_list.append(feature) - return feature_list diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py deleted file mode 100755 index 5e53da07c9..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=import-error -# pylint: disable=invalid-name - -"""The Python implementation of the gRPC route guide server. - -Note that you need ``opentelemetry-instrumentation-grpc`` and ``protobuf`` to be installed -to run these examples. To run this script in the context of the example app, -install ``opentelemetry-example-app``:: - - pip install -e instrumentation/opentelemetry-instrumentation-grpc/ - pip install -e docs/examples/opentelemetry-example-app - -Then run the server in one shell:: - - python -m opentelemetry_example_app.grpc.route_guide_server - -and the client in another:: - - python -m opentelemetry_example_app.grpc.route_guide_client - -See also: -https://github.com/grpc/grpc/tree/master/examples/python/route_guide -""" - -import logging -import math -import time -from concurrent import futures - -import grpc - -from opentelemetry import trace -from opentelemetry.instrumentation.grpc import server_interceptor -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - ConsoleSpanExporter, - SimpleExportSpanProcessor, -) - -try: - # Relative imports should work in the context of the package, e.g.: - # `python -m opentelemetry_example_app.grpc.route_guide_server`. - from .gen import route_guide_pb2, route_guide_pb2_grpc - from . import route_guide_resources -except ImportError: - # This will fail when running the file as a script, e.g.: - # `./route_guide_server.py` - # fall back to importing from the same directory in this case. - from gen import route_guide_pb2, route_guide_pb2_grpc - import route_guide_resources - -trace.set_tracer_provider(TracerProvider()) -trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) -) - - -def get_feature(feature_db, point): - """Returns Feature at given location or None.""" - for feature in feature_db: - if feature.location == point: - return feature - return None - - -def get_distance(start, end): - """Distance between two points.""" - coord_factor = 10000000.0 - lat_1 = start.latitude / coord_factor - lat_2 = end.latitude / coord_factor - lon_1 = start.longitude / coord_factor - lon_2 = end.longitude / coord_factor - lat_rad_1 = math.radians(lat_1) - lat_rad_2 = math.radians(lat_2) - delta_lat_rad = math.radians(lat_2 - lat_1) - delta_lon_rad = math.radians(lon_2 - lon_1) - - # Formula is based on http://mathforum.org/library/drmath/view/51879.html - a = pow(math.sin(delta_lat_rad / 2), 2) + ( - math.cos(lat_rad_1) - * math.cos(lat_rad_2) - * pow(math.sin(delta_lon_rad / 2), 2) - ) - c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) - R = 6371000 - # metres - return R * c - - -class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer): - """Provides methods that implement functionality of route guide server.""" - - def __init__(self): - self.db = route_guide_resources.read_route_guide_database() - - def GetFeature(self, request, context): - feature = get_feature(self.db, request) - if feature is None: - return route_guide_pb2.Feature(name="", location=request) - return feature - - def ListFeatures(self, request, context): - left = min(request.lo.longitude, request.hi.longitude) - right = max(request.lo.longitude, request.hi.longitude) - top = max(request.lo.latitude, request.hi.latitude) - bottom = min(request.lo.latitude, request.hi.latitude) - for feature in self.db: - if ( - feature.location.longitude >= left - and feature.location.longitude <= right - and feature.location.latitude >= bottom - and feature.location.latitude <= top - ): - yield feature - - def RecordRoute(self, request_iterator, context): - point_count = 0 - feature_count = 0 - distance = 0.0 - prev_point = None - - start_time = time.time() - for point in request_iterator: - point_count += 1 - if get_feature(self.db, point): - feature_count += 1 - if prev_point: - distance += get_distance(prev_point, point) - prev_point = point - - elapsed_time = time.time() - start_time - return route_guide_pb2.RouteSummary( - point_count=point_count, - feature_count=feature_count, - distance=int(distance), - elapsed_time=int(elapsed_time), - ) - - def RouteChat(self, request_iterator, context): - prev_notes = [] - for new_note in request_iterator: - for prev_note in prev_notes: - if prev_note.location == new_note.location: - yield prev_note - prev_notes.append(new_note) - - -def serve(): - server = grpc.server( - futures.ThreadPoolExecutor(max_workers=10), - interceptors=[server_interceptor()], - ) - - route_guide_pb2_grpc.add_RouteGuideServicer_to_server( - RouteGuideServicer(), server - ) - server.add_insecure_port("[::]:50051") - server.start() - server.wait_for_termination() - - -if __name__ == "__main__": - logging.basicConfig() - serve() diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py deleted file mode 100644 index 1f98d44fa8..0000000000 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.16.dev0" diff --git a/docs/examples/opentelemetry-example-app/tests/__init__.py b/docs/examples/opentelemetry-example-app/tests/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/docs/examples/opentelemetry-example-app/tests/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/docs/examples/opentelemetry-example-app/tests/test_flask_example.py b/docs/examples/opentelemetry-example-app/tests/test_flask_example.py deleted file mode 100644 index 124a79c0ef..0000000000 --- a/docs/examples/opentelemetry-example-app/tests/test_flask_example.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -from unittest import mock - -import requests -from werkzeug.test import Client -from werkzeug.wrappers import BaseResponse - -import opentelemetry_example_app.flask_example as flask_example -from opentelemetry import trace - - -class TestFlaskExample(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.app = flask_example.app - - def setUp(self): - mocked_response = requests.models.Response() - mocked_response.status_code = 200 - mocked_response.reason = "Roger that!" - self.send_patcher = mock.patch.object( - requests.Session, - "send", - autospec=True, - spec_set=True, - return_value=mocked_response, - ) - self.send = self.send_patcher.start() - - def tearDown(self): - self.send_patcher.stop() - - def test_full_path(self): - ids_generator = trace.RandomIdsGenerator() - trace_id = ids_generator.generate_trace_id() - # We need to use the Werkzeug test app because - # The headers are injected at the wsgi layer. - # The flask test app will not include these, and - # result in the values not propagated. - client = Client(self.app.wsgi_app, BaseResponse) - # emulate b3 headers - client.get( - "/", - headers={ - "traceparent": "00-{:032x}-{:016x}-{:02x}".format( - trace_id, - ids_generator.generate_span_id(), - trace.TraceFlags.SAMPLED, - ) - }, - ) - # assert the http request header was propagated through. - prepared_request = self.send.call_args[0][1] - headers = prepared_request.headers - self.assertRegex( - headers["traceparent"], - r"00-{:032x}-[0-9a-f]{{16}}-01".format(trace_id), - ) diff --git a/pyproject.toml b/pyproject.toml index 73e41db223..5cea9a3445 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,6 @@ line-length = 79 exclude = ''' ( /( # generated files - docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen| exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen| exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen| opentelemetry-proto/src/opentelemetry/proto/collector| diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 7d3daa2af7..2b36a3f863 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -38,7 +38,6 @@ cov exporter/opentelemetry-exporter-jaeger cov instrumentation/opentelemetry-instrumentation-opentracing-shim cov instrumentation/opentelemetry-instrumentation-wsgi cov exporter/opentelemetry-exporter-zipkin -cov docs/examples/opentelemetry-example-app # aiohttp is only supported on Python 3.5+. if [ ${PYTHON_VERSION_INFO[1]} -gt 4 ]; then diff --git a/tox.ini b/tox.ini index a42174f357..b968225645 100644 --- a/tox.ini +++ b/tox.ini @@ -24,9 +24,6 @@ envlist = py3{5,6,7,8}-test-core-getting-started pypy3-test-core-getting-started - ; opentelemetry-example-app - py3{5,6,7,8}-test-core-example-app - ; opentelemetry-exporter-jaeger py3{5,6,7,8}-test-exporter-jaeger pypy3-test-exporter-jaeger @@ -72,7 +69,6 @@ changedir = test-core-sdk: opentelemetry-sdk/tests test-core-proto: opentelemetry-proto/tests test-core-instrumentation: opentelemetry-instrumentation/tests - test-core-example-app: docs/examples/opentelemetry-example-app/tests test-core-getting-started: docs/getting_started/tests test-core-opentracing-shim: instrumentation/opentelemetry-instrumentation-opentracing-shim/tests @@ -92,8 +88,6 @@ commands_pre = test-core-proto: pip install {toxinidir}/opentelemetry-proto instrumentation: pip install {toxinidir}/opentelemetry-instrumentation - example-app: pip install {toxinidir}/opentelemetry-instrumentation {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask {toxinidir}/docs/examples/opentelemetry-example-app - getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus From c1de3874f3a81675bd52dced0a11dbe4f3372667 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 23 Nov 2020 22:42:22 -0500 Subject: [PATCH 0678/1517] recording (#1386) --- opentelemetry-api/src/opentelemetry/trace/status.py | 3 --- .../src/opentelemetry/sdk/trace/__init__.py | 10 +++------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/status.py b/opentelemetry-api/src/opentelemetry/trace/status.py index 7089793579..ebc427db83 100644 --- a/opentelemetry-api/src/opentelemetry/trace/status.py +++ b/opentelemetry-api/src/opentelemetry/trace/status.py @@ -19,9 +19,6 @@ logger = logging.getLogger(__name__) -EXCEPTION_STATUS_FIELD = "_otel_status_code" - - class StatusCode(enum.Enum): """Represents the canonical set of status codes of a finished Span.""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index facef291ac..6328414ab3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -44,11 +44,7 @@ from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanContext from opentelemetry.trace.propagation import SPAN_KEY -from opentelemetry.trace.status import ( - EXCEPTION_STATUS_FIELD, - Status, - StatusCode, -) +from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import time_ns, types logger = logging.getLogger(__name__) @@ -666,7 +662,7 @@ def __exit__( exc_tb: Optional[TracebackType], ) -> None: """Ends context manager and calls `end` on the `Span`.""" - if exc_val is not None: + if exc_val is not None and self.is_recording(): # Record the exception as an event # pylint:disable=protected-access if self._record_exception: @@ -857,7 +853,7 @@ def use_span( except Exception as exc: # pylint: disable=broad-except # Record the exception as an event - if isinstance(span, Span): + if isinstance(span, Span) and span.is_recording(): # pylint:disable=protected-access if span._record_exception: span.record_exception(exc) From 388a491e7987d26a361770bda35ed95cbaad8532 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 24 Nov 2020 06:24:33 -0800 Subject: [PATCH 0679/1517] modify tracestate log was in wrong release (#1412) --- opentelemetry-sdk/CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 851073bcd5..ffc04404ac 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,16 +2,20 @@ ## Unreleased -- Rename `MetricRecord` class to `ExportRecord` - ([#1367](https://github.com/open-telemetry/opentelemetry-python/pull/1367)) +- Allow samplers to modify tracestate + ([#1319](https://github.com/open-telemetry/opentelemetry-python/pull/1319)) - Add optional parameter to `record_exception` method ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) - Update exception handling optional parameters, add escaped attribute to record_exception ([#1365](https://github.com/open-telemetry/opentelemetry-python/pull/1365)) +- Rename `MetricRecord` class to `ExportRecord` + ([#1367](https://github.com/open-telemetry/opentelemetry-python/pull/1367)) - Rename Record in Metrics SDK to Accumulation ([#1373](https://github.com/open-telemetry/opentelemetry-python/pull/1373)) - Rename Meter class to Accumulator in Metrics SDK ([#1372](https://github.com/open-telemetry/opentelemetry-python/pull/1372)) +- Rename Meter class to Accumulator in Metrics SDK + ([#1372](https://github.com/open-telemetry/opentelemetry-python/pull/1372)) ## Version 0.15b0 From 4afa77568344579449d47d5edd5cf098ce2de4f6 Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Tue, 24 Nov 2020 16:13:49 +0100 Subject: [PATCH 0680/1517] Fix b3 format tests (#1396) --- .../tests/trace/propagation/test_b3_format.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index 79c4618aee..8546b0b922 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -66,6 +66,14 @@ def setUpClass(cls): ids_generator.generate_span_id() ) + def setUp(self) -> None: + tracer_provider = trace.TracerProvider() + patcher = unittest.mock.patch.object( + trace_api, "get_tracer_provider", return_value=tracer_provider + ) + patcher.start() + self.addCleanup(patcher.stop) + def test_extract_multi_header(self): """Test the extraction of B3 headers.""" child, parent, new_carrier = get_child_parent_new_carrier( From da7597c357b84fddbff12bc265bdd9035c55b19a Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Tue, 24 Nov 2020 08:46:51 -0800 Subject: [PATCH 0681/1517] Add IDs Generator as Configurable Property of Auto Instrumentation (#1404) --- opentelemetry-api/setup.cfg | 2 + opentelemetry-instrumentation/CHANGELOG.md | 2 + opentelemetry-instrumentation/README.rst | 12 ++++ .../auto_instrumentation/__init__.py | 14 ++++ .../auto_instrumentation/components.py | 70 +++++++++++++++---- .../tests/test_auto_tracing.py | 48 ++++++++++++- 6 files changed, 131 insertions(+), 17 deletions(-) diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index c697e30854..78c3123222 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -57,6 +57,8 @@ opentelemetry_tracer_provider = opentelemetry_propagator = tracecontext = opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator baggage = opentelemetry.baggage.propagation:BaggagePropagator +opentelemetry_ids_generator = + random = opentelemetry.trace.ids_generator:RandomIdsGenerator [options.extras_require] test = diff --git a/opentelemetry-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md index d59bc01c75..34366cb6b1 100644 --- a/opentelemetry-instrumentation/CHANGELOG.md +++ b/opentelemetry-instrumentation/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add IDs Generator as Configurable Property of Auto Instrumentation + ([#1404](https://github.com/open-telemetry/opentelemetry-python/pull/1404)) - Added support for `OTEL_EXPORTER` to the `opentelemetry-instrument` command ([#1036](https://github.com/open-telemetry/opentelemetry-python/pull/1036)) ## Version 0.14b0 diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index 22f2228016..aed9909a04 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -73,6 +73,11 @@ Well known trace exporter names: When present the value is passed on to the relevant exporter initializer as ``service_name`` argument. +* ``--ids-generator`` or ``OTEL_IDS_GENERATOR`` + +Used to specify which IDs Generator to use for the global Tracer Provider. By default, it +will use the random IDs generator. + The code in ``program.py`` needs to use one of the packages for which there is an OpenTelemetry integration. For a list of the available integrations please check `here `_ @@ -93,6 +98,13 @@ The above command will pass ``-e otlp`` to the instrument command and ``--port=3 The above command will configure global trace provider, attach zipkin and otlp exporters to it and then start celery with the rest of the arguments. +:: + + opentelemetry-instrument --ids-generator random flask run --port=3000 + +The above command will configure the global trace provider to use the Random IDs Generator, and then +pass ``--port=3000`` to ``flask run``. + References ---------- diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index 51af6dffd8..959708de96 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -47,6 +47,18 @@ def parse_args(): """, ) + parser.add_argument( + "--ids-generator", + required=False, + help=""" + The IDs Generator to be used with the Tracer Provider. + + Examples: + + --ids-generator=random + """, + ) + parser.add_argument( "-s", "--service-name", @@ -70,6 +82,8 @@ def load_config_from_cli_args(args): environ["OTEL_EXPORTER"] = args.exporter if args.service_name: environ["OTEL_SERVICE_NAME"] = args.service_name + if args.ids_generator: + environ["OTEL_IDS_GENERATOR"] = args.ids_generator def run() -> None: diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py index 105a41722c..8b4ef4f3e8 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py @@ -34,6 +34,13 @@ EXPORTER_OTLP_METRIC = "otlp_metric" _DEFAULT_EXPORTER = EXPORTER_OTLP +RANDOM_IDS_GENERATOR = "random" +_DEFAULT_IDS_GENERATOR = RANDOM_IDS_GENERATOR + + +def get_ids_generator() -> str: + return Configuration().IDS_GENERATOR or _DEFAULT_IDS_GENERATOR + def get_service_name() -> str: return Configuration().SERVICE_NAME or "" @@ -55,10 +62,13 @@ def get_exporter_names() -> Sequence[str]: return names -def init_tracing(exporters: Sequence[SpanExporter]): +def init_tracing( + exporters: Sequence[SpanExporter], ids_generator: trace.IdsGenerator +): service_name = get_service_name() provider = TracerProvider( resource=Resource.create({"service.name": service_name}), + ids_generator=ids_generator(), ) trace.set_tracer_provider(provider) @@ -80,23 +90,39 @@ def init_metrics(exporters: Sequence[MetricsExporter]): logger.warning("automatic metric initialization is not supported yet.") -def import_exporters( - exporter_names: Sequence[str], -) -> Tuple[Sequence[SpanExporter], Sequence[MetricsExporter]]: - trace_exporters, metric_exporters = {}, {} - - exporters = { - ep.name: ep for ep in iter_entry_points("opentelemetry_exporter") +def import_tracer_provider_config_components( + selected_components, entry_point_name +) -> Sequence[Tuple[str, object]]: + component_entry_points = { + ep.name: ep for ep in iter_entry_points(entry_point_name) } - - for exporter_name in exporter_names: - entry_point = exporters.get(exporter_name, None) + component_impls = [] + for selected_component in selected_components: + entry_point = component_entry_points.get(selected_component, None) if not entry_point: raise RuntimeError( - "Requested exporter not found: {0}".format(exporter_name) + "Requested component '{}' not found in entry points for '{}'".format( + selected_component, entry_point_name + ) ) - exporter_impl = entry_point.load() + component_impl = entry_point.load() + component_impls.append((selected_component, component_impl)) + + return component_impls + + +def import_exporters( + exporter_names: Sequence[str], +) -> Tuple[Sequence[SpanExporter], Sequence[MetricsExporter]]: + trace_exporters, metric_exporters = {}, {} + + for ( + exporter_name, + exporter_impl, + ) in import_tracer_provider_config_components( + exporter_names, "opentelemetry_exporter" + ): if issubclass(exporter_impl, SpanExporter): trace_exporters[exporter_name] = exporter_impl elif issubclass(exporter_impl, MetricsExporter): @@ -110,10 +136,26 @@ def import_exporters( return trace_exporters, metric_exporters +def import_ids_generator(ids_generator_name: str) -> trace.IdsGenerator: + # pylint: disable=unbalanced-tuple-unpacking + [ + (ids_generator_name, ids_generator_impl) + ] = import_tracer_provider_config_components( + [ids_generator_name.strip()], "opentelemetry_ids_generator" + ) + + if issubclass(ids_generator_impl, trace.IdsGenerator): + return ids_generator_impl + + raise RuntimeError("{0} is not an IdsGenerator".format(ids_generator_name)) + + def initialize_components(): exporter_names = get_exporter_names() trace_exporters, metric_exporters = import_exporters(exporter_names) - init_tracing(trace_exporters) + ids_generator_name = get_ids_generator() + ids_generator = import_ids_generator(ids_generator_name) + init_tracing(trace_exporters, ids_generator) # We don't support automatic initialization for metric yet but have added # some boilerplate in order to make sure current implementation does not diff --git a/opentelemetry-instrumentation/tests/test_auto_tracing.py b/opentelemetry-instrumentation/tests/test_auto_tracing.py index 359dc12fad..9ffe421a25 100644 --- a/opentelemetry-instrumentation/tests/test_auto_tracing.py +++ b/opentelemetry-instrumentation/tests/test_auto_tracing.py @@ -20,10 +20,12 @@ from opentelemetry.configuration import Configuration from opentelemetry.instrumentation.auto_instrumentation import components from opentelemetry.sdk.resources import Resource +from opentelemetry.trace.ids_generator import RandomIdsGenerator class Provider: - def __init__(self, resource=None): + def __init__(self, resource=None, ids_generator=None): + self.ids_generator = ids_generator self.processor = None self.resource = resource @@ -48,6 +50,23 @@ class OTLPExporter: pass +class IdsGenerator: + pass + + +class CustomIdsGenerator(IdsGenerator): + pass + + +class IterEntryPoint: + def __init__(self, name, class_type): + self.name = name + self.class_type = class_type + + def load(self): + return self.class_type + + class TestTraceInit(TestCase): def setUp(self): super() @@ -77,11 +96,12 @@ def tearDown(self): def test_trace_init_default(self): environ["OTEL_SERVICE_NAME"] = "my-test-service" Configuration._reset() - components.init_tracing({"zipkin": Exporter}) + components.init_tracing({"zipkin": Exporter}, RandomIdsGenerator) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, Provider) + self.assertIsInstance(provider.ids_generator, RandomIdsGenerator) self.assertIsInstance(provider.processor, Processor) self.assertIsInstance(provider.processor.exporter, Exporter) self.assertEqual( @@ -91,11 +111,12 @@ def test_trace_init_default(self): def test_trace_init_otlp(self): environ["OTEL_SERVICE_NAME"] = "my-otlp-test-service" Configuration._reset() - components.init_tracing({"otlp": OTLPExporter}) + components.init_tracing({"otlp": OTLPExporter}, RandomIdsGenerator) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, Provider) + self.assertIsInstance(provider.ids_generator, RandomIdsGenerator) self.assertIsInstance(provider.processor, Processor) self.assertIsInstance(provider.processor.exporter, OTLPExporter) self.assertIsInstance(provider.resource, Resource) @@ -104,3 +125,24 @@ def test_trace_init_otlp(self): "my-otlp-test-service", ) del environ["OTEL_SERVICE_NAME"] + + @patch.dict(environ, {"OTEL_IDS_GENERATOR": "custom_ids_generator"}) + @patch( + "opentelemetry.instrumentation.auto_instrumentation.components.trace.IdsGenerator", + new=IdsGenerator, + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation.components.iter_entry_points" + ) + def test_trace_init_custom_ids_generator(self, mock_iter_entry_points): + mock_iter_entry_points.configure_mock( + return_value=[ + IterEntryPoint("custom_ids_generator", CustomIdsGenerator) + ] + ) + Configuration._reset() + ids_generator_name = components.get_ids_generator() + ids_generator = components.import_ids_generator(ids_generator_name) + components.init_tracing({}, ids_generator) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider.ids_generator, CustomIdsGenerator) From 2425d6647eb5f6bb234212896d20e5512df6bba9 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Wed, 25 Nov 2020 07:26:07 -0800 Subject: [PATCH 0682/1517] Add PR question regarding Contrib Repo Link (#1399) --- .github/pull_request_template.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2281d05e17..48d109f5ef 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,6 +19,25 @@ Please describe the tests that you ran to verify your changes. Provide instructi - [ ] Test A +# Does This PR Require a Contrib Repo Change? + +Answer the following question based on these examples of changes that would require a Contrib Repo Change: +- [The OTel specification](https://github.com/open-telemetry/opentelemetry-specification) has changed which prompted this PR to update the method interfaces of `opentelemetry-api/` or `opentelemetry-sdk/` +- The method interfaces of `opentelemetry-instrumentation/` have changed +- The method interfaces of `test/util` have changed +- Scripts in `scripts/` that were copied over to the Contrib repo have changed +- Configuration files that were copied over to the Contrib repo have changed (when consistency between repositories is applicable) such as in + - `pyproject.toml` + - `isort.cfg` + - `.flake8` +- When a new `.github/CODEOWNER` is added +- Major changes to project information, such as in: + - `README.md` + - `CONTRIBUTING.md` + +- [ ] Yes. - Link to PR: +- [ ] No. + # Checklist: - [ ] Followed the style guidelines of this project From 7b33dd69080ebc38ea2bdd3e287361ec068341cf Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Wed, 25 Nov 2020 18:32:14 +0100 Subject: [PATCH 0683/1517] Fix ParentBased sampler for implicit parent spans (#1394) consider the sample decision of implicit parent spans (when creating a span without explicitly providing a context) instead of forwarding to the delegating sampler. * fix trace_state erasure for dropped spans * samplers did not return the trace_state in the sampling result when a span was dropped. This caused the trace_state extracted from a remote parent span to be erased and further context propagation to break. * fix also TraceIdRatioBased which erased the trace_state independent of the sampling outcome. --- opentelemetry-sdk/CHANGELOG.md | 5 +- .../src/opentelemetry/sdk/trace/sampling.py | 28 +- .../tests/trace/test_sampling.py | 385 ++++++++++-------- 3 files changed, 231 insertions(+), 187 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index ffc04404ac..2a1967c2ee 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -14,8 +14,9 @@ ([#1373](https://github.com/open-telemetry/opentelemetry-python/pull/1373)) - Rename Meter class to Accumulator in Metrics SDK ([#1372](https://github.com/open-telemetry/opentelemetry-python/pull/1372)) -- Rename Meter class to Accumulator in Metrics SDK - ([#1372](https://github.com/open-telemetry/opentelemetry-python/pull/1372)) +- Fix `ParentBased` sampler for implicit parent spans. Fix also `trace_state` + erasure for dropped spans or spans sampled by the `TraceIdRatioBased` sampler. + ([#1394](https://github.com/open-telemetry/opentelemetry-python/pull/1394)) ## Version 0.15b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index ffa51506ff..82d2cebaa5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -151,7 +151,7 @@ def should_sample( trace_state: "TraceState" = None, ) -> "SamplingResult": if self._decision is Decision.DROP: - return SamplingResult(self._decision) + attributes = None return SamplingResult(self._decision, attributes, trace_state) def get_description(self) -> str: @@ -209,8 +209,8 @@ def should_sample( if trace_id & self.TRACE_ID_LIMIT < self.bound: decision = Decision.RECORD_AND_SAMPLE if decision is Decision.DROP: - return SamplingResult(decision) - return SamplingResult(decision, attributes) + attributes = None + return SamplingResult(decision, attributes, trace_state) def get_description(self) -> str: return "TraceIdRatioBased{{{}}}".format(self._rate) @@ -238,18 +238,16 @@ def should_sample( links: Sequence["Link"] = None, trace_state: "TraceState" = None, ) -> "SamplingResult": - if parent_context is not None: - parent_span_context = get_current_span( - parent_context - ).get_span_context() - # only drop if parent exists and is not a root span - if ( - parent_span_context is not None - and parent_span_context.is_valid - and not parent_span_context.trace_flags.sampled - ): - return SamplingResult(Decision.DROP) - return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes) + parent_span_context = get_current_span( + parent_context + ).get_span_context() + # respect the sampling flag of the parent if present + if parent_span_context is not None and parent_span_context.is_valid: + decision = Decision.RECORD_AND_SAMPLE + if not parent_span_context.trace_flags.sampled: + decision = Decision.DROP + attributes = None + return SamplingResult(decision, attributes, trace_state) return self._delegate.should_sample( parent_context=parent_context, diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index d51a59c106..f6d77ab04b 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -12,9 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import contextlib import sys +import typing import unittest +from opentelemetry import context as context_api from opentelemetry import trace from opentelemetry.sdk.trace import sampling @@ -62,161 +65,173 @@ def test_ctr(self): class TestSampler(unittest.TestCase): - def test_always_on(self): - no_record_always_on = sampling.ALWAYS_ON.should_sample( + def _create_parent( + self, trace_flags: trace.TraceFlags, is_remote=False + ) -> typing.Optional[context_api.Context]: + if trace_flags is None: + return None + return trace.set_span_in_context( + self._create_parent_span(trace_flags, is_remote) + ) + + @staticmethod + def _create_parent_span( + trace_flags: trace.TraceFlags, is_remote=False + ) -> trace.DefaultSpan: + return trace.DefaultSpan( trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_DEFAULT - ), - 0xDEADBEF1, - 0xDEADBEF2, - {"unsampled parent": "sampling on"}, - ) - self.assertTrue(no_record_always_on.decision.is_sampled()) - self.assertEqual( - no_record_always_on.attributes, {"unsampled parent": "sampling on"} + 0xDEADBEEF, + 0xDEADBEF0, + is_remote=is_remote, + trace_flags=trace_flags, + ) ) - sampled_always_on = sampling.ALWAYS_ON.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_SAMPLED - ), - 0xDEADBEF1, - 0xDEADBEF2, - {"sampled parent": "sampling on"}, - ) - self.assertTrue(no_record_always_on.decision.is_sampled()) - self.assertEqual( - sampled_always_on.attributes, {"sampled parent": "sampling on"} - ) + def test_always_on(self): + trace_state = trace.TraceState({"key": "value"}) + test_data = (TO_DEFAULT, TO_SAMPLED, None) + + for trace_flags in test_data: + with self.subTest(trace_flags=trace_flags): + context = self._create_parent(trace_flags) + sample_result = sampling.ALWAYS_ON.should_sample( + context, + 0xDEADBEF1, + "sampling on", + attributes={"sampled.expect": "true"}, + trace_state=trace_state, + ) + + self.assertTrue(sample_result.decision.is_sampled()) + self.assertEqual( + sample_result.attributes, {"sampled.expect": "true"} + ) + self.assertEqual(sample_result.trace_state, trace_state) def test_always_off(self): - no_record_always_off = sampling.ALWAYS_OFF.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_DEFAULT - ), - 0xDEADBEF1, - 0xDEADBEF2, - "unsampled parent, sampling off", - ) - self.assertFalse(no_record_always_off.decision.is_sampled()) - self.assertEqual(no_record_always_off.attributes, {}) + trace_state = trace.TraceState({"key": "value"}) + test_data = (TO_DEFAULT, TO_SAMPLED, None) + for trace_flags in test_data: + with self.subTest(trace_flags=trace_flags): + context = self._create_parent(trace_flags) + sample_result = sampling.ALWAYS_OFF.should_sample( + context, + 0xDEADBEF1, + "sampling off", + attributes={"sampled.expect": "false"}, + trace_state=trace_state, + ) + self.assertFalse(sample_result.decision.is_sampled()) + self.assertEqual(sample_result.attributes, {}) + self.assertEqual(sample_result.trace_state, trace_state) - sampled_always_on = sampling.ALWAYS_OFF.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_SAMPLED - ), + def test_default_on(self): + trace_state = trace.TraceState({"key": "value"}) + context = self._create_parent(trace_flags=TO_DEFAULT) + sample_result = sampling.DEFAULT_ON.should_sample( + context, 0xDEADBEF1, - 0xDEADBEF2, - "sampled parent, sampling off", + "unsampled parent, sampling on", + attributes={"sampled.expect": "false"}, + trace_state=trace_state, ) - self.assertFalse(sampled_always_on.decision.is_sampled()) - self.assertEqual(sampled_always_on.attributes, {}) + self.assertFalse(sample_result.decision.is_sampled()) + self.assertEqual(sample_result.attributes, {}) + self.assertEqual(sample_result.trace_state, trace_state) - def test_default_on(self): - context = trace.set_span_in_context( - trace.DefaultSpan( - trace.SpanContext( - 0xDEADBEEF, - 0xDEADBEF0, - is_remote=False, - trace_flags=TO_DEFAULT, - ) - ) - ) - no_record_default_on = sampling.DEFAULT_ON.should_sample( - context, 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling on", - ) - self.assertFalse(no_record_default_on.decision.is_sampled()) - self.assertEqual(no_record_default_on.attributes, {}) - - context = trace.set_span_in_context( - trace.DefaultSpan( - trace.SpanContext( - 0xDEADBEEF, - 0xDEADBEF0, - is_remote=False, - trace_flags=TO_SAMPLED, - ) - ) - ) - sampled_default_on = sampling.DEFAULT_ON.should_sample( - context, 0xDEADBEF1, 0xDEADBEF2, {"sampled parent": "sampling on"}, - ) - self.assertTrue(sampled_default_on.decision.is_sampled()) - self.assertEqual( - sampled_default_on.attributes, {"sampled parent": "sampling on"} + context = self._create_parent(trace_flags=TO_SAMPLED) + sample_result = sampling.DEFAULT_ON.should_sample( + context, + 0xDEADBEF1, + "sampled parent, sampling on", + attributes={"sampled.expect": "true"}, + trace_state=trace_state, ) + self.assertTrue(sample_result.decision.is_sampled()) + self.assertEqual(sample_result.attributes, {"sampled.expect": "true"}) + self.assertEqual(sample_result.trace_state, trace_state) - default_on = sampling.DEFAULT_ON.should_sample( - None, 0xDEADBEF1, 0xDEADBEF2, {"sampled parent": "sampling on"}, - ) - self.assertTrue(default_on.decision.is_sampled()) - self.assertEqual( - default_on.attributes, {"sampled parent": "sampling on"} + sample_result = sampling.DEFAULT_ON.should_sample( + None, + 0xDEADBEF1, + "no parent, sampling on", + attributes={"sampled.expect": "true"}, + trace_state=trace_state, ) + self.assertTrue(sample_result.decision.is_sampled()) + self.assertEqual(sample_result.attributes, {"sampled.expect": "true"}) + self.assertEqual(sample_result.trace_state, trace_state) def test_default_off(self): - context = trace.set_span_in_context( - trace.DefaultSpan( - trace.SpanContext( - 0xDEADBEEF, - 0xDEADBEF0, - is_remote=False, - trace_flags=TO_DEFAULT, - ) - ) - ) - no_record_default_off = sampling.DEFAULT_OFF.should_sample( - context, 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling off", - ) - self.assertFalse(no_record_default_off.decision.is_sampled()) - self.assertEqual(no_record_default_off.attributes, {}) - - context = trace.set_span_in_context( - trace.DefaultSpan( - trace.SpanContext( - 0xDEADBEEF, - 0xDEADBEF0, - is_remote=False, - trace_flags=TO_SAMPLED, - ) - ) - ) - sampled_default_off = sampling.DEFAULT_OFF.should_sample( - context, 0xDEADBEF1, 0xDEADBEF2, {"sampled parent": "sampling on"}, + trace_state = trace.TraceState({"key": "value"}) + context = self._create_parent(trace_flags=TO_DEFAULT) + sample_result = sampling.DEFAULT_OFF.should_sample( + context, + 0xDEADBEF1, + "unsampled parent, sampling off", + attributes={"sampled.expect", "false"}, + trace_state=trace_state, ) - self.assertTrue(sampled_default_off.decision.is_sampled()) - self.assertEqual( - sampled_default_off.attributes, {"sampled parent": "sampling on"} + self.assertFalse(sample_result.decision.is_sampled()) + self.assertEqual(sample_result.attributes, {}) + self.assertEqual(sample_result.trace_state, trace_state) + + context = self._create_parent(trace_flags=TO_SAMPLED) + sample_result = sampling.DEFAULT_OFF.should_sample( + context, + 0xDEADBEF1, + "sampled parent, sampling on", + attributes={"sampled.expect": "true"}, + trace_state=trace_state, ) + self.assertTrue(sample_result.decision.is_sampled()) + self.assertEqual(sample_result.attributes, {"sampled.expect": "true"}) + self.assertEqual(sample_result.trace_state, trace_state) default_off = sampling.DEFAULT_OFF.should_sample( - None, 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling off", + None, + 0xDEADBEF1, + "unsampled parent, sampling off", + attributes={"sampled.expect": "false"}, + trace_state=trace_state, ) self.assertFalse(default_off.decision.is_sampled()) self.assertEqual(default_off.attributes, {}) + self.assertEqual(default_off.trace_state, trace_state) def test_probability_sampler(self): + trace_state = trace.TraceState({"key": "value"}) sampler = sampling.TraceIdRatioBased(0.5) # Check that we sample based on the trace ID if the parent context is # null - self.assertTrue( - sampler.should_sample( - None, 0x7FFFFFFFFFFFFFFF, 0xDEADBEEF, "span name" - ).decision.is_sampled() - ) - self.assertFalse( - sampler.should_sample( - None, 0x8000000000000000, 0xDEADBEEF, "span name" - ).decision.is_sampled() - ) + sampled_result = sampler.should_sample( + None, + 0x7FFFFFFFFFFFFFFF, + "sampled true", + attributes={"sampled.expect": "true"}, + trace_state=trace_state, + ) + self.assertTrue(sampled_result.decision.is_sampled()) + self.assertEqual(sampled_result.attributes, {"sampled.expect": "true"}) + self.assertEqual(sampled_result.trace_state, trace_state) + + not_sampled_result = sampler.should_sample( + None, + 0x8000000000000000, + "sampled false", + attributes={"sampled.expect": "false"}, + trace_state=trace_state, + ) + self.assertFalse(not_sampled_result.decision.is_sampled()) + self.assertEqual(not_sampled_result.attributes, {}) + self.assertEqual(not_sampled_result.trace_state, trace_state) def test_probability_sampler_zero(self): default_off = sampling.TraceIdRatioBased(0.0) self.assertFalse( default_off.should_sample( - None, 0x0, 0xDEADBEEF, "span name" + None, 0x0, "span name" ).decision.is_sampled() ) @@ -224,7 +239,7 @@ def test_probability_sampler_one(self): default_off = sampling.TraceIdRatioBased(1.0) self.assertTrue( default_off.should_sample( - None, 0xFFFFFFFFFFFFFFFF, 0xDEADBEEF, "span name" + None, 0xFFFFFFFFFFFFFFFF, "span name" ).decision.is_sampled() ) @@ -236,12 +251,12 @@ def test_probability_sampler_limits(self): almost_always_off = sampling.TraceIdRatioBased(2 ** -64) self.assertTrue( almost_always_off.should_sample( - None, 0x0, 0xDEADBEEF, "span name" + None, 0x0, "span name" ).decision.is_sampled() ) self.assertFalse( almost_always_off.should_sample( - None, 0x1, 0xDEADBEEF, "span name" + None, 0x1, "span name" ).decision.is_sampled() ) self.assertEqual( @@ -259,7 +274,7 @@ def test_probability_sampler_limits(self): almost_always_on = sampling.TraceIdRatioBased(1 - 2 ** -64) self.assertTrue( almost_always_on.should_sample( - None, 0xFFFFFFFFFFFFFFFE, 0xDEADBEEF, "span name" + None, 0xFFFFFFFFFFFFFFFE, "span name" ).decision.is_sampled() ) @@ -271,9 +286,8 @@ def test_probability_sampler_limits(self): # almost_always_on.should_sample( # None, # 0xFFFFFFFFFFFFFFFF, - # 0xDEADBEEF, # "span name", - # ).sampled + # ).decision.is_sampled() # ) # self.assertEqual( # sampling.TraceIdRatioBased.get_bound_for_rate(1 - 2 ** -64)), @@ -287,7 +301,7 @@ def test_probability_sampler_limits(self): ) self.assertFalse( almost_almost_always_on.should_sample( - None, 0xFFFFFFFFFFFFFFFF, 0xDEADBEEF, "span name" + None, 0xFFFFFFFFFFFFFFFF, "span name" ).decision.is_sampled() ) # Check that the higest effective sampling rate is actually lower than @@ -297,47 +311,78 @@ def test_probability_sampler_limits(self): almost_almost_always_on.bound, 0xFFFFFFFFFFFFFFFF, ) - def test_parent_based(self): + def exec_parent_based(self, parent_sampling_context): + trace_state = trace.TraceState({"key": "value"}) sampler = sampling.ParentBased(sampling.ALWAYS_ON) - context = trace.set_span_in_context( - trace.DefaultSpan( - trace.SpanContext( - 0xDEADBEF0, - 0xDEADBEF1, - is_remote=False, - trace_flags=TO_DEFAULT, - ) + with parent_sampling_context( + self._create_parent_span(trace_flags=TO_DEFAULT) + ) as context: + # Check that the sampling decision matches the parent context if given + not_sampled_result = sampler.should_sample( + context, + 0x7FFFFFFFFFFFFFFF, + "unsampled parent, sampling on", + attributes={"sampled": "false"}, + trace_state=trace_state, ) - ) - # Check that the sampling decision matches the parent context if given - self.assertFalse( - sampler.should_sample( - context, 0x7FFFFFFFFFFFFFFF, 0xDEADBEEF, "span name", - ).decision.is_sampled() - ) - - context = trace.set_span_in_context( - trace.DefaultSpan( - trace.SpanContext( - 0xDEADBEF0, - 0xDEADBEF1, - is_remote=False, - trace_flags=TO_SAMPLED, - ) + self.assertFalse(not_sampled_result.decision.is_sampled()) + self.assertEqual(not_sampled_result.attributes, {}) + self.assertEqual(not_sampled_result.trace_state, trace_state) + + with parent_sampling_context( + self._create_parent_span(trace_flags=TO_SAMPLED) + ) as context: + sampler2 = sampling.ParentBased(sampling.ALWAYS_OFF) + sampled_result = sampler2.should_sample( + context, + 0x8000000000000000, + "sampled parent, sampling off", + attributes={"sampled": "true"}, + trace_state=trace_state, ) - ) - sampler2 = sampling.ParentBased(sampling.ALWAYS_OFF) - self.assertTrue( - sampler2.should_sample( - context, 0x8000000000000000, 0xDEADBEEF, "span name", - ).decision.is_sampled() - ) + self.assertTrue(sampled_result.decision.is_sampled()) + self.assertEqual(sampled_result.attributes, {"sampled": "true"}) + self.assertEqual(sampled_result.trace_state, trace_state) + + # for root span follow decision of delegate sampler + with parent_sampling_context(trace.INVALID_SPAN) as context: + sampler3 = sampling.ParentBased(sampling.ALWAYS_OFF) + not_sampled_result = sampler3.should_sample( + context, + 0x8000000000000000, + "parent, sampling off", + attributes={"sampled": "false"}, + trace_state=trace_state, + ) + self.assertFalse(not_sampled_result.decision.is_sampled()) + self.assertEqual(not_sampled_result.attributes, {}) + self.assertEqual(not_sampled_result.trace_state, trace_state) + + with parent_sampling_context(trace.INVALID_SPAN) as context: + sampler4 = sampling.ParentBased(sampling.ALWAYS_ON) + sampled_result = sampler4.should_sample( + context, + 0x8000000000000000, + "no parent, sampling on", + attributes={"sampled": "true"}, + trace_state=trace_state, + ) + self.assertTrue(sampled_result.decision.is_sampled()) + self.assertEqual(sampled_result.attributes, {"sampled": "true"}) + self.assertEqual(sampled_result.trace_state, trace_state) - # root span always sampled for parentbased - context = trace.set_span_in_context(trace.INVALID_SPAN) - sampler3 = sampling.ParentBased(sampling.ALWAYS_OFF) - self.assertTrue( - sampler3.should_sample( - context, 0x8000000000000000, 0xDEADBEEF, "span name", - ).decision.is_sampled() - ) + def test_parent_based_explicit_parent_context(self): + @contextlib.contextmanager + def explicit_parent_context(span: trace.Span): + yield trace.set_span_in_context(span) + + self.exec_parent_based(explicit_parent_context) + + def test_parent_based_implicit_parent_context(self): + @contextlib.contextmanager + def implicit_parent_context(span: trace.Span): + token = context_api.attach(trace.set_span_in_context(span)) + yield None + context_api.detach(token) + + self.exec_parent_based(implicit_parent_context) From 07b89455f11b00d50d89a884176dc31d1f138228 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Wed, 25 Nov 2020 09:52:01 -0800 Subject: [PATCH 0684/1517] Add missing references to instrumented packages (#1416) --- opentelemetry-instrumentation/CHANGELOG.md | 2 ++ .../instrumentation/bootstrap.py | 20 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/opentelemetry-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md index 34366cb6b1..3ac6bdfa2f 100644 --- a/opentelemetry-instrumentation/CHANGELOG.md +++ b/opentelemetry-instrumentation/CHANGELOG.md @@ -5,6 +5,8 @@ - Add IDs Generator as Configurable Property of Auto Instrumentation ([#1404](https://github.com/open-telemetry/opentelemetry-python/pull/1404)) - Added support for `OTEL_EXPORTER` to the `opentelemetry-instrument` command ([#1036](https://github.com/open-telemetry/opentelemetry-python/pull/1036)) +- Add missing references to instrumented packages + ([#1416](https://github.com/open-telemetry/opentelemetry-python/pull/1416)) ## Version 0.14b0 diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 4b6a677a84..44487a7794 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -23,8 +23,18 @@ logger = getLogger(__file__) -# target library to desired instrumentor path/versioned package name +# A mapping of "target library" to "desired instrumentor path/versioned package +# name". Used as part of the `opentelemetry-bootstrap` command which looks at +# libraries used by the application that is to be instrumented, and handles +# automatically installing the appropriate instrumentations for that app. +# This helps for those who prefer to turn on as much instrumentation as +# possible, and don't want to go through the manual process of combing through +# the libraries their application uses to figure which one can be +# instrumented. +# NOTE: system-metrics is not to be included. instrumentations = { + "aiohttp-client": "opentelemetry-instrumentation-aiohttp-client>=0.15b0", + "aiopg": "opentelemetry-instrumentation-aiopg>=0.15b0", "asgi": "opentelemetry-instrumentation-asgi>=0.11b0", "asyncpg": "opentelemetry-instrumentation-asyncpg>=0.11b0", "boto": "opentelemetry-instrumentation-boto>=0.11b0", @@ -33,8 +43,8 @@ "dbapi": "opentelemetry-instrumentation-dbapi>=0.8b0", "django": "opentelemetry-instrumentation-django>=0.8b0", "elasticsearch": "opentelemetry-instrumentation-elasticsearch>=0.11b0", - "fastapi": "opentelemetry-instrumentation-fastapi>=0.11b0", "falcon": "opentelemetry-instrumentation-falcon>=0.13b0", + "fastapi": "opentelemetry-instrumentation-fastapi>=0.11b0", "flask": "opentelemetry-instrumentation-flask>=0.8b0", "grpc": "opentelemetry-instrumentation-grpc>=0.8b0", "jinja2": "opentelemetry-instrumentation-jinja2>=0.8b0", @@ -46,6 +56,7 @@ "pyramid": "opentelemetry-instrumentation-pyramid>=0.11b0", "redis": "opentelemetry-instrumentation-redis>=0.8b0", "requests": "opentelemetry-instrumentation-requests>=0.8b0", + "sklearn": "opentelemetry-instrumentation-sklearn>=0.15b0", "sqlalchemy": "opentelemetry-instrumentation-sqlalchemy>=0.8b0", "sqlite3": "opentelemetry-instrumentation-sqlite3>=0.11b0", "starlette": "opentelemetry-instrumentation-starlette>=0.11b0", @@ -55,6 +66,8 @@ # relevant instrumentors and tracers to uninstall and check for conflicts for target libraries libraries = { + "aiohttp-client": ("opentelemetry-instrumentation-aiohttp-client",), + "aiopg": ("opentelemetry-instrumentation-aiopg",), "asgi": ("opentelemetry-instrumentation-asgi",), "asyncpg": ("opentelemetry-instrumentation-asyncpg",), "boto": ("opentelemetry-instrumentation-boto",), @@ -63,8 +76,8 @@ "dbapi": ("opentelemetry-instrumentation-dbapi",), "django": ("opentelemetry-instrumentation-django",), "elasticsearch": ("opentelemetry-instrumentation-elasticsearch",), - "fastapi": ("opentelemetry-instrumentation-fastapi",), "falcon": ("opentelemetry-instrumentation-falcon",), + "fastapi": ("opentelemetry-instrumentation-fastapi",), "flask": ("opentelemetry-instrumentation-flask",), "grpc": ("opentelemetry-instrumentation-grpc",), "jinja2": ("opentelemetry-instrumentation-jinja2",), @@ -76,6 +89,7 @@ "pyramid": ("opentelemetry-instrumentation-pyramid",), "redis": ("opentelemetry-instrumentation-redis",), "requests": ("opentelemetry-instrumentation-requests",), + "sklearn": ("opentelemetry-instrumentation-sklearn",), "sqlalchemy": ("opentelemetry-instrumentation-sqlalchemy",), "sqlite3": ("opentelemetry-instrumentation-sqlite3",), "starlette": ("opentelemetry-instrumentation-starlette",), From 47b5b3574fdb6ee6f09dfa4aeb1986365001878c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 25 Nov 2020 16:35:54 -0600 Subject: [PATCH 0685/1517] Add instrumentation library name and version to OTLP exported metrics (#1418) Fixes #1417 --- .../opentelemetry-exporter-otlp/CHANGELOG.md | 2 ++ .../otlp/metrics_exporter/__init__.py | 19 +++++++++++++++++-- .../exporter/otlp/trace_exporter/__init__.py | 1 - .../tests/test_otlp_metric_exporter.py | 8 ++++++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index e467d8f78e..074a30bbc5 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add instrumentation library name and version to OTLP exported metrics + ([#1418](https://github.com/open-telemetry/opentelemetry-python/pull/1418)) - Change temporality for Counter and UpDownCounter ([#1384](https://github.com/open-telemetry/opentelemetry-python/pull/1384)) - Add Gzip compression for exporter diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py index c90dd47db2..559e313477 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -304,9 +304,11 @@ def _translate_data( ) argument = type_class[value_type]["gauge"]["argument"] - sdk_resource_instrumentation_library_metrics[ + instrumentation_library_metrics = sdk_resource_instrumentation_library_metrics[ export_record.resource - ].metrics.append( + ] + + instrumentation_library_metrics.metrics.append( OTLPMetric( **{ "name": export_record.instrument.name, @@ -317,6 +319,19 @@ def _translate_data( ) ) + instrumentation_library_metrics.instrumentation_library.name = ( + export_record.instrument.meter.instrumentation_info.name + ) + + version = ( + export_record.instrument.meter.instrumentation_info.version + ) + + if version: + ( + instrumentation_library_metrics.instrumentation_library.version + ) = version + return ExportMetricsServiceRequest( resource_metrics=_get_resource_data( sdk_resource_instrumentation_library_metrics, diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index 5059e042c3..1af8d26aa9 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -14,7 +14,6 @@ """OTLP Span Exporter""" import logging -import os from typing import Optional, Sequence from grpc import ChannelCredentials diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index 308d217f21..c255d91ec7 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -26,6 +26,7 @@ ) from opentelemetry.proto.common.v1.common_pb2 import ( AnyValue, + InstrumentationLibrary, KeyValue, StringKeyValue, ) @@ -61,7 +62,7 @@ def setUp(self, mock_time_ns): # pylint: disable=arguments-differ "d", "e", int, - MeterProvider(resource=resource,).get_meter(__name__), + MeterProvider(resource=resource,).get_meter("name", "version"), ("f",), ), [("g", "h")], @@ -121,6 +122,9 @@ def test_translate_metrics(self): ), instrumentation_library_metrics=[ InstrumentationLibraryMetrics( + instrumentation_library=InstrumentationLibrary( + name="name", version="version", + ), metrics=[ OTLPMetric( name="c", @@ -145,7 +149,7 @@ def test_translate_metrics(self): is_monotonic=True, ), ) - ] + ], ) ], ) From 895be85424fd5b8fe913929a41230a04c4e307ab Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Wed, 25 Nov 2020 17:50:53 -0800 Subject: [PATCH 0686/1517] Instrumentation Package depends on the OTel SDK (#1405) --- opentelemetry-instrumentation/CHANGELOG.md | 2 ++ opentelemetry-instrumentation/setup.cfg | 1 + 2 files changed, 3 insertions(+) diff --git a/opentelemetry-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md index 3ac6bdfa2f..245f6c3215 100644 --- a/opentelemetry-instrumentation/CHANGELOG.md +++ b/opentelemetry-instrumentation/CHANGELOG.md @@ -7,6 +7,8 @@ - Added support for `OTEL_EXPORTER` to the `opentelemetry-instrument` command ([#1036](https://github.com/open-telemetry/opentelemetry-python/pull/1036)) - Add missing references to instrumented packages ([#1416](https://github.com/open-telemetry/opentelemetry-python/pull/1416)) +- Instrumentation Package depends on the OTel SDK + ([#1405](https://github.com/open-telemetry/opentelemetry-python/pull/1405)) ## Version 0.14b0 diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 6080af2526..0074b3a366 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -42,6 +42,7 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api == 0.16.dev0 + opentelemetry-sdk == 0.16.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] From ffd94883b5cdc2b969e183593f3f8ed6482e1202 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 25 Nov 2020 20:57:30 -0800 Subject: [PATCH 0687/1517] [pre-release] Update changelogs, version [0.16b0] (#1422) --- .github/workflows/test.yml | 2 +- docs/examples/error_hander/error_handler_0/setup.cfg | 2 +- .../error_handler_0/src/error_handler_0/version.py | 2 +- docs/examples/error_hander/error_handler_1/setup.cfg | 2 +- .../error_handler_1/src/error_handler_1/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- .../setup.cfg | 4 ++-- .../instrumentation/opentracing_shim/version.py | 2 +- opentelemetry-api/CHANGELOG.md | 4 ++++ opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-instrumentation/CHANGELOG.md | 4 ++++ opentelemetry-instrumentation/setup.cfg | 4 ++-- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/CHANGELOG.md | 4 ++++ opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 4 ++++ opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- tests/util/setup.cfg | 4 ++-- tests/util/src/opentelemetry/test/version.py | 2 +- 32 files changed, 62 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b54c1841a1..018686ce9e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: master + CONTRIB_REPO_SHA: ab52183a6ad1ba44596457527601d092c9443074 jobs: build: diff --git a/docs/examples/error_hander/error_handler_0/setup.cfg b/docs/examples/error_hander/error_handler_0/setup.cfg index 3ea84d43d8..2d0a816d8b 100644 --- a/docs/examples/error_hander/error_handler_0/setup.cfg +++ b/docs/examples/error_hander/error_handler_0/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.16.dev0 + opentelemetry-sdk == 0.16b0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py index 1f98d44fa8..36c4f8c245 100644 --- a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py +++ b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16.dev0" +__version__ = "0.16b0" diff --git a/docs/examples/error_hander/error_handler_1/setup.cfg b/docs/examples/error_hander/error_handler_1/setup.cfg index 6b9ab21e4b..3e73a13cd4 100644 --- a/docs/examples/error_hander/error_handler_1/setup.cfg +++ b/docs/examples/error_hander/error_handler_1/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.16.dev0 + opentelemetry-sdk == 0.16b0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py index 1f98d44fa8..36c4f8c245 100644 --- a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py +++ b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16.dev0" +__version__ = "0.16b0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 66d0d61cbb..f860236eb2 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.16.dev0 - opentelemetry-sdk == 0.16.dev0 + opentelemetry-api == 0.16b0 + opentelemetry-sdk == 0.16b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 7b04708d09..55018c707e 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16.dev0" +__version__ = "0.16b0" diff --git a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md index 33a182a3fd..33ccae1d45 100644 --- a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.16b0 + +Released 2020-11-25 + - Update protobuf versions ([#1356](https://github.com/open-telemetry/opentelemetry-python/pull/1356)) diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index ad470e2b19..069f588320 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.16.dev0 - opentelemetry-sdk == 0.16.dev0 + opentelemetry-api == 0.16b0 + opentelemetry-sdk == 0.16b0 protobuf >= 3.13.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 1f98d44fa8..36c4f8c245 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16.dev0" +__version__ = "0.16b0" diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 074a30bbc5..6f45b4413c 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.16b0 + +Released 2020-11-25 + - Add instrumentation library name and version to OTLP exported metrics ([#1418](https://github.com/open-telemetry/opentelemetry-python/pull/1418)) - Change temporality for Counter and UpDownCounter diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 7ba1b5c1f9..ae80d0311a 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.16.dev0 - opentelemetry-sdk == 0.16.dev0 - opentelemetry-proto == 0.16.dev0 + opentelemetry-api == 0.16b0 + opentelemetry-sdk == 0.16b0 + opentelemetry-proto == 0.16b0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 1f98d44fa8..36c4f8c245 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16.dev0" +__version__ = "0.16b0" diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index 540f9fcc9e..56d0f7f015 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.16.dev0 - opentelemetry-sdk == 0.16.dev0 + opentelemetry-api == 0.16b0 + opentelemetry-sdk == 0.16b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 1f98d44fa8..36c4f8c245 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16.dev0" +__version__ = "0.16b0" diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index 972096a8ef..bb10a1a629 100644 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.16b0 + +Released 2020-11-25 + - Support for v2 api protobuf format ([#1318](https://github.com/open-telemetry/opentelemetry-python/pull/1318)) ## Version 0.14b0 diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 1803eac14e..35a01719f9 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 - opentelemetry-api == 0.16.dev0 - opentelemetry-sdk == 0.16.dev0 + opentelemetry-api == 0.16b0 + opentelemetry-sdk == 0.16b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 1f98d44fa8..36c4f8c245 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16.dev0" +__version__ = "0.16b0" diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg index 33cad7b935..698b1d83da 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg @@ -41,11 +41,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.16.dev0 + opentelemetry-api == 0.16b0 [options.extras_require] test = - opentelemetry-test == 0.16.dev0 + opentelemetry-test == 0.16b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py index 1f98d44fa8..36c4f8c245 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16.dev0" +__version__ = "0.16b0" diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index a957b5e844..ccf3f53e31 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.16b0 + +Released 2020-11-25 + - Add optional parameter to `record_exception` method ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) - Add pickle support to SpanContext class ([#1380](https://github.com/open-telemetry/opentelemetry-python/pull/1380)) diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 1f98d44fa8..36c4f8c245 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16.dev0" +__version__ = "0.16b0" diff --git a/opentelemetry-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md index 245f6c3215..2a93c09ba5 100644 --- a/opentelemetry-instrumentation/CHANGELOG.md +++ b/opentelemetry-instrumentation/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.16b0 + +Released 2020-11-25 + - Add IDs Generator as Configurable Property of Auto Instrumentation ([#1404](https://github.com/open-telemetry/opentelemetry-python/pull/1404)) - Added support for `OTEL_EXPORTER` to the `opentelemetry-instrument` command ([#1036](https://github.com/open-telemetry/opentelemetry-python/pull/1036)) diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 0074b3a366..c254d4a0ae 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-sdk == 0.16.dev0 + opentelemetry-api == 0.16b0 + opentelemetry-sdk == 0.16b0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 1f98d44fa8..36c4f8c245 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16.dev0" +__version__ = "0.16b0" diff --git a/opentelemetry-proto/CHANGELOG.md b/opentelemetry-proto/CHANGELOG.md index b8e1d48ce5..9906bdee12 100644 --- a/opentelemetry-proto/CHANGELOG.md +++ b/opentelemetry-proto/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.16b0 + +Released 2020-11-25 + - Update protobuf versions ([#1356](https://github.com/open-telemetry/opentelemetry-python/pull/1356)) diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 1f98d44fa8..36c4f8c245 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16.dev0" +__version__ = "0.16b0" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 2a1967c2ee..3b5e72131e 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.16b0 + +Released 2020-11-25 + - Allow samplers to modify tracestate ([#1319](https://github.com/open-telemetry/opentelemetry-python/pull/1319)) - Add optional parameter to `record_exception` method diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index d27a24cf49..4b51b15969 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.16.dev0 + opentelemetry-api == 0.16b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 1f98d44fa8..36c4f8c245 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16.dev0" +__version__ = "0.16b0" diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index 6d26100963..4c48b46302 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -37,8 +37,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.16.dev0 - opentelemetry-sdk == 0.16.dev0 + opentelemetry-api == 0.16b0 + opentelemetry-sdk == 0.16b0 [options.extras_require] test = flask~=1.0 diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 4c4951dcda..39c7b8647d 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.16.dev0" +__version__ = "0.16b0" From bce2aa649c9f3541a57b115ceb6694c5a00ca856 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 25 Nov 2020 21:10:00 -0800 Subject: [PATCH 0688/1517] update version to 0.17.dev0 --- docs/examples/error_hander/error_handler_0/setup.cfg | 2 +- .../error_handler_0/src/error_handler_0/version.py | 2 +- docs/examples/error_hander/error_handler_1/setup.cfg | 2 +- .../error_handler_1/src/error_handler_1/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- .../setup.cfg | 4 ++-- .../instrumentation/opentracing_shim/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 4 ++-- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- tests/util/setup.cfg | 4 ++-- tests/util/src/opentelemetry/test/version.py | 2 +- 24 files changed, 33 insertions(+), 33 deletions(-) diff --git a/docs/examples/error_hander/error_handler_0/setup.cfg b/docs/examples/error_hander/error_handler_0/setup.cfg index 2d0a816d8b..d0a7b6275e 100644 --- a/docs/examples/error_hander/error_handler_0/setup.cfg +++ b/docs/examples/error_hander/error_handler_0/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.16b0 + opentelemetry-sdk == 0.17.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py index 36c4f8c245..86e1dbb17a 100644 --- a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py +++ b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16b0" +__version__ = "0.17.dev0" diff --git a/docs/examples/error_hander/error_handler_1/setup.cfg b/docs/examples/error_hander/error_handler_1/setup.cfg index 3e73a13cd4..304b32fd5e 100644 --- a/docs/examples/error_hander/error_handler_1/setup.cfg +++ b/docs/examples/error_hander/error_handler_1/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.16b0 + opentelemetry-sdk == 0.17.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py index 36c4f8c245..86e1dbb17a 100644 --- a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py +++ b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16b0" +__version__ = "0.17.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index f860236eb2..37d2402d5f 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 0.16b0 - opentelemetry-sdk == 0.16b0 + opentelemetry-api == 0.17.dev0 + opentelemetry-sdk == 0.17.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 55018c707e..203d035bb2 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16b0" +__version__ = "0.17.dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 069f588320..2ff3b2bc2b 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.16b0 - opentelemetry-sdk == 0.16b0 + opentelemetry-api == 0.17.dev0 + opentelemetry-sdk == 0.17.dev0 protobuf >= 3.13.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 36c4f8c245..86e1dbb17a 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16b0" +__version__ = "0.17.dev0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index ae80d0311a..20d724e867 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.16b0 - opentelemetry-sdk == 0.16b0 - opentelemetry-proto == 0.16b0 + opentelemetry-api == 0.17.dev0 + opentelemetry-sdk == 0.17.dev0 + opentelemetry-proto == 0.17.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 36c4f8c245..86e1dbb17a 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16b0" +__version__ = "0.17.dev0" diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index 56d0f7f015..6ab2101f8c 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.16b0 - opentelemetry-sdk == 0.16b0 + opentelemetry-api == 0.17.dev0 + opentelemetry-sdk == 0.17.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 36c4f8c245..86e1dbb17a 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16b0" +__version__ = "0.17.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 35a01719f9..6fda337014 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 - opentelemetry-api == 0.16b0 - opentelemetry-sdk == 0.16b0 + opentelemetry-api == 0.17.dev0 + opentelemetry-sdk == 0.17.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 36c4f8c245..86e1dbb17a 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16b0" +__version__ = "0.17.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg index 698b1d83da..87def81089 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg @@ -41,11 +41,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.16b0 + opentelemetry-api == 0.17.dev0 [options.extras_require] test = - opentelemetry-test == 0.16b0 + opentelemetry-test == 0.17.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py index 36c4f8c245..86e1dbb17a 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16b0" +__version__ = "0.17.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 36c4f8c245..86e1dbb17a 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16b0" +__version__ = "0.17.dev0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index c254d4a0ae..558e6a54e2 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.16b0 - opentelemetry-sdk == 0.16b0 + opentelemetry-api == 0.17.dev0 + opentelemetry-sdk == 0.17.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 36c4f8c245..86e1dbb17a 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16b0" +__version__ = "0.17.dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 36c4f8c245..86e1dbb17a 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16b0" +__version__ = "0.17.dev0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 4b51b15969..4acb6ca1fc 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.16b0 + opentelemetry-api == 0.17.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 36c4f8c245..86e1dbb17a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16b0" +__version__ = "0.17.dev0" diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index 4c48b46302..c788346791 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -37,8 +37,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.16b0 - opentelemetry-sdk == 0.16b0 + opentelemetry-api == 0.17.dev0 + opentelemetry-sdk == 0.17.dev0 [options.extras_require] test = flask~=1.0 diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 39c7b8647d..da85ff96db 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.16b0" +__version__ = "0.17.dev0" From deab7901a2361f22a6b3671ca3864a3d4185f79f Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 25 Nov 2020 21:12:05 -0800 Subject: [PATCH 0689/1517] update contrib SHA --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 018686ce9e..ce3c100d0a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: ab52183a6ad1ba44596457527601d092c9443074 + CONTRIB_REPO_SHA: bcec49cf2eccf8da66c9e63b9836ea8a20516efc jobs: build: From 8cc92600158aba9d9de272f66a29a8422e3e5a66 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 26 Nov 2020 16:47:38 -0600 Subject: [PATCH 0690/1517] Add meter to observers (#1425) Co-authored-by: Leighton Chen --- .../opentelemetry-exporter-otlp/CHANGELOG.md | 3 + .../tests/test_otlp_metric_exporter.py | 249 ++++++++++++++++-- opentelemetry-sdk/CHANGELOG.md | 3 + .../src/opentelemetry/sdk/metrics/__init__.py | 29 +- .../tests/metrics/test_metrics.py | 132 +++++----- opentelemetry-sdk/tests/metrics/test_view.py | 10 +- 6 files changed, 328 insertions(+), 98 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 6f45b4413c..4528c21534 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add meter reference to observers + ([#1425](https://github.com/open-telemetry/opentelemetry-python/pull/1425)) + ## Version 0.16b0 Released 2020-11-25 diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index c255d91ec7..6d45e30d44 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -15,7 +15,7 @@ import os from collections import OrderedDict from unittest import TestCase -from unittest.mock import patch +from unittest.mock import Mock, patch from grpc import ChannelCredentials @@ -41,7 +41,13 @@ from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as OTLPResource, ) -from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics import ( + Counter, + MeterProvider, + SumObserver, + UpDownCounter, + UpDownSumObserver, +) from opentelemetry.sdk.metrics.export import ExportRecord from opentelemetry.sdk.metrics.export.aggregate import SumAggregator from opentelemetry.sdk.resources import Resource as SDKResource @@ -50,26 +56,12 @@ class TestOTLPMetricExporter(TestCase): - @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") - def setUp(self, mock_time_ns): # pylint: disable=arguments-differ - mock_time_ns.configure_mock(**{"return_value": 1}) + def setUp(self): # pylint: disable=arguments-differ self.exporter = OTLPMetricsExporter(insecure=True) - resource = SDKResource(OrderedDict([("a", 1), ("b", False)])) - - self.counter_export_record = ExportRecord( - Counter( - "c", - "d", - "e", - int, - MeterProvider(resource=resource,).get_meter("name", "version"), - ("f",), - ), - [("g", "h")], - SumAggregator(), - resource, + self.resource = SDKResource(OrderedDict([("a", 1), ("b", False)])) + self.meter = MeterProvider(resource=self.resource,).get_meter( + "name", "version" ) - Configuration._reset() # pylint: disable=protected-access def tearDown(self): @@ -102,12 +94,20 @@ def test_no_credentials_error(self): with self.assertRaises(ValueError): OTLPMetricsExporter() - def test_translate_metrics(self): - # pylint: disable=no-member + @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") + def test_translate_counter_export_record(self, mock_time_ns): + mock_time_ns.configure_mock(**{"return_value": 1}) + + counter_export_record = ExportRecord( + Counter("c", "d", "e", int, self.meter, ("f",),), + [("g", "h")], + SumAggregator(), + self.resource, + ) - self.counter_export_record.aggregator.checkpoint = 1 - self.counter_export_record.aggregator.initial_checkpoint_timestamp = 1 - self.counter_export_record.aggregator.last_update_timestamp = 1 + counter_export_record.aggregator.checkpoint = 1 + counter_export_record.aggregator.initial_checkpoint_timestamp = 1 + counter_export_record.aggregator.last_update_timestamp = 1 expected = ExportMetricsServiceRequest( resource_metrics=[ @@ -157,6 +157,203 @@ def test_translate_metrics(self): ) # pylint: disable=protected-access - actual = self.exporter._translate_data([self.counter_export_record]) + actual = self.exporter._translate_data([counter_export_record]) + + self.assertEqual(expected, actual) + + @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") + def test_translate_sum_observer_export_record(self, mock_time_ns): + mock_time_ns.configure_mock(**{"return_value": 1}) + counter_export_record = ExportRecord( + SumObserver(Mock(), "c", "d", "e", int, self.meter, ("f",),), + [("g", "h")], + SumAggregator(), + self.resource, + ) + + counter_export_record.aggregator.checkpoint = 1 + counter_export_record.aggregator.initial_checkpoint_timestamp = 1 + counter_export_record.aggregator.last_update_timestamp = 1 + + expected = ExportMetricsServiceRequest( + resource_metrics=[ + ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_metrics=[ + InstrumentationLibraryMetrics( + instrumentation_library=InstrumentationLibrary( + name="name", version="version", + ), + metrics=[ + OTLPMetric( + name="c", + description="d", + unit="e", + int_sum=IntSum( + data_points=[ + IntDataPoint( + labels=[ + StringKeyValue( + key="g", value="h" + ) + ], + value=1, + time_unix_nano=1, + start_time_unix_nano=1, + ) + ], + aggregation_temporality=( + AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE + ), + is_monotonic=True, + ), + ) + ], + ) + ], + ) + ] + ) + + # pylint: disable=protected-access + actual = self.exporter._translate_data([counter_export_record]) + + self.assertEqual(expected, actual) + + @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") + def test_translate_updowncounter_export_record(self, mock_time_ns): + mock_time_ns.configure_mock(**{"return_value": 1}) + + counter_export_record = ExportRecord( + UpDownCounter("c", "d", "e", int, self.meter), + [("g", "h")], + SumAggregator(), + self.resource, + ) + + counter_export_record.aggregator.checkpoint = 1 + counter_export_record.aggregator.initial_checkpoint_timestamp = 1 + counter_export_record.aggregator.last_update_timestamp = 1 + + expected = ExportMetricsServiceRequest( + resource_metrics=[ + ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_metrics=[ + InstrumentationLibraryMetrics( + instrumentation_library=InstrumentationLibrary( + name="name", version="version", + ), + metrics=[ + OTLPMetric( + name="c", + description="d", + unit="e", + int_sum=IntSum( + data_points=[ + IntDataPoint( + labels=[ + StringKeyValue( + key="g", value="h" + ) + ], + value=1, + time_unix_nano=1, + start_time_unix_nano=1, + ) + ], + aggregation_temporality=( + AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE + ), + ), + ) + ], + ) + ], + ) + ] + ) + + # pylint: disable=protected-access + actual = self.exporter._translate_data([counter_export_record]) + + self.assertEqual(expected, actual) + + @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") + def test_translate_updownsum_observer_export_record(self, mock_time_ns): + mock_time_ns.configure_mock(**{"return_value": 1}) + counter_export_record = ExportRecord( + UpDownSumObserver(Mock(), "c", "d", "e", int, self.meter, ("f",),), + [("g", "h")], + SumAggregator(), + self.resource, + ) + + counter_export_record.aggregator.checkpoint = 1 + counter_export_record.aggregator.initial_checkpoint_timestamp = 1 + counter_export_record.aggregator.last_update_timestamp = 1 + + expected = ExportMetricsServiceRequest( + resource_metrics=[ + ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_metrics=[ + InstrumentationLibraryMetrics( + instrumentation_library=InstrumentationLibrary( + name="name", version="version", + ), + metrics=[ + OTLPMetric( + name="c", + description="d", + unit="e", + int_sum=IntSum( + data_points=[ + IntDataPoint( + labels=[ + StringKeyValue( + key="g", value="h" + ) + ], + value=1, + time_unix_nano=1, + start_time_unix_nano=1, + ) + ], + aggregation_temporality=( + AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE + ), + ), + ) + ], + ) + ], + ) + ] + ) + + # pylint: disable=protected-access + actual = self.exporter._translate_data([counter_export_record]) self.assertEqual(expected, actual) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 3b5e72131e..0e0b0f550f 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add meter reference to observers + ([#1425](https://github.com/open-telemetry/opentelemetry-python/pull/1425)) + ## Version 0.16b0 Released 2020-11-25 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index a2d16719a8..a09047e523 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -241,6 +241,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], + meter: "Accumulator", label_keys: Sequence[str] = (), enabled: bool = True, ): @@ -249,6 +250,7 @@ def __init__( self.description = description self.unit = unit self.value_type = value_type + self.meter = meter self.label_keys = label_keys self.enabled = enabled @@ -477,7 +479,14 @@ def register_sumobserver( enabled: bool = True, ) -> metrics_api.SumObserver: ob = SumObserver( - callback, name, description, unit, value_type, label_keys, enabled + callback, + name, + description, + unit, + value_type, + self, + label_keys, + enabled, ) with self.observers_lock: self.observers.add(ob) @@ -494,7 +503,14 @@ def register_updownsumobserver( enabled: bool = True, ) -> metrics_api.UpDownSumObserver: ob = UpDownSumObserver( - callback, name, description, unit, value_type, label_keys, enabled + callback, + name, + description, + unit, + value_type, + self, + label_keys, + enabled, ) with self.observers_lock: self.observers.add(ob) @@ -511,7 +527,14 @@ def register_valueobserver( enabled: bool = True, ) -> metrics_api.ValueObserver: ob = ValueObserver( - callback, name, description, unit, value_type, label_keys, enabled + callback, + name, + description, + unit, + value_type, + self, + label_keys, + enabled, ) with self.observers_lock: self.observers.add(ob) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 58cc195cdf..1697d8e6c8 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -13,7 +13,7 @@ # limitations under the License. import unittest -from unittest import mock +from unittest.mock import Mock, patch from opentelemetry import metrics as metrics_api from opentelemetry.sdk import metrics, resources @@ -41,7 +41,7 @@ def test_resource_empty(self): self.assertEqual(meter_provider.resource, resources._DEFAULT_RESOURCE) def test_start_pipeline(self): - exporter = mock.Mock() + exporter = Mock() meter_provider = metrics.MeterProvider() meter = meter_provider.get_meter(__name__) # pylint: disable=protected-access @@ -53,8 +53,8 @@ def test_start_pipeline(self): meter_provider.shutdown() def test_shutdown(self): - controller = mock.Mock() - exporter = mock.Mock() + controller = Mock() + exporter = Mock() meter_provider = metrics.MeterProvider() # pylint: disable=protected-access meter_provider._controllers = [controller] @@ -72,7 +72,7 @@ def test_extends_api(self): def test_collect_metrics(self): meter = metrics.MeterProvider().get_meter(__name__) - processor_mock = mock.Mock() + processor_mock = Mock() meter.processor = processor_mock counter = meter.create_counter("name", "desc", "unit", float,) labels = {"key1": "value1"} @@ -83,14 +83,14 @@ def test_collect_metrics(self): def test_collect_no_metrics(self): meter = metrics.MeterProvider().get_meter(__name__) - processor_mock = mock.Mock() + processor_mock = Mock() meter.processor = processor_mock meter.collect() self.assertFalse(processor_mock.process.called) def test_collect_not_registered(self): meter = metrics.MeterProvider().get_meter(__name__) - processor_mock = mock.Mock() + processor_mock = Mock() meter.processor = processor_mock counter = metrics.Counter("name", "desc", "unit", float, meter) labels = {"key1": "value1"} @@ -100,7 +100,7 @@ def test_collect_not_registered(self): def test_collect_disabled_metric(self): meter = metrics.MeterProvider().get_meter(__name__) - processor_mock = mock.Mock() + processor_mock = Mock() meter.processor = processor_mock counter = metrics.Counter("name", "desc", "unit", float, meter, False) labels = {"key1": "value1"} @@ -111,7 +111,7 @@ def test_collect_disabled_metric(self): def test_collect_observers(self): meter = metrics.MeterProvider().get_meter(__name__) - processor_mock = mock.Mock() + processor_mock = Mock() meter.processor = processor_mock def callback(observer): @@ -154,7 +154,7 @@ def test_record_batch(self): ) def test_create_counter(self): - resource = mock.Mock(spec=resources.Resource) + resource = Mock(spec=resources.Resource) meter_provider = metrics.MeterProvider(resource=resource) meter = meter_provider.get_meter(__name__) counter = meter.create_counter("name", "desc", "unit", int,) @@ -186,7 +186,7 @@ def test_create_valuerecorder(self): def test_register_sumobserver(self): meter = metrics.MeterProvider().get_meter(__name__) - callback = mock.Mock() + callback = Mock() observer = meter.register_sumobserver( callback, "name", "desc", "unit", int, @@ -206,7 +206,7 @@ def test_register_sumobserver(self): def test_register_updownsumobserver(self): meter = metrics.MeterProvider().get_meter(__name__) - callback = mock.Mock() + callback = Mock() observer = meter.register_updownsumobserver( callback, "name", "desc", "unit", int, @@ -226,7 +226,7 @@ def test_register_updownsumobserver(self): def test_register_valueobserver(self): meter = metrics.MeterProvider().get_meter(__name__) - callback = mock.Mock() + callback = Mock() observer = meter.register_valueobserver( callback, "name", "desc", "unit", int, @@ -246,7 +246,7 @@ def test_register_valueobserver(self): def test_unregister_observer(self): meter = metrics.MeterProvider().get_meter(__name__) - callback = mock.Mock() + callback = Mock() observer = meter.register_valueobserver( callback, "name", "desc", "unit", int, metrics.ValueObserver @@ -286,7 +286,7 @@ def test_add(self): metric.add(2, labels) self.assertEqual(bound_mock.view_datas.pop().aggregator.current, 5) - @mock.patch("opentelemetry.sdk.metrics.logger") + @patch("opentelemetry.sdk.metrics.logger") def test_add_non_decreasing_int_error(self, logger_mock): meter = metrics.MeterProvider().get_meter(__name__) metric = metrics.Counter("name", "desc", "unit", int, meter) @@ -300,7 +300,7 @@ def test_add_non_decreasing_int_error(self, logger_mock): self.assertEqual(bound_counter.view_datas.pop().aggregator.current, 3) self.assertEqual(logger_mock.warning.call_count, 1) - @mock.patch("opentelemetry.sdk.metrics.logger") + @patch("opentelemetry.sdk.metrics.logger") def test_add_non_decreasing_float_error(self, logger_mock): meter = metrics.MeterProvider().get_meter(__name__) metric = metrics.Counter("name", "desc", "unit", float, meter) @@ -351,7 +351,7 @@ def test_record(self): class TestSumObserver(unittest.TestCase): def test_observe(self): observer = metrics.SumObserver( - None, "name", "desc", "unit", int, ("key",), True + None, "name", "desc", "unit", int, Mock(), ("key",), True ) labels = {"key": "value"} key_labels = metrics.get_dict_as_key(labels) @@ -363,26 +363,26 @@ def test_observe(self): def test_observe_disabled(self): observer = metrics.SumObserver( - None, "name", "desc", "unit", int, ("key",), False + None, "name", "desc", "unit", int, Mock(), ("key",), False ) labels = {"key": "value"} observer.observe(37, labels) self.assertEqual(len(observer.aggregators), 0) - @mock.patch("opentelemetry.sdk.metrics.logger") + @patch("opentelemetry.sdk.metrics.logger") def test_observe_incorrect_type(self, logger_mock): observer = metrics.SumObserver( - None, "name", "desc", "unit", int, ("key",), True + None, "name", "desc", "unit", int, Mock(), ("key",), True ) labels = {"key": "value"} observer.observe(37.0, labels) self.assertEqual(len(observer.aggregators), 0) self.assertTrue(logger_mock.warning.called) - @mock.patch("opentelemetry.sdk.metrics.logger") + @patch("opentelemetry.sdk.metrics.logger") def test_observe_non_decreasing_error(self, logger_mock): observer = metrics.SumObserver( - None, "name", "desc", "unit", int, ("key",), True + None, "name", "desc", "unit", int, Mock(), ("key",), True ) labels = {"key": "value"} observer.observe(37, labels) @@ -391,21 +391,21 @@ def test_observe_non_decreasing_error(self, logger_mock): self.assertTrue(logger_mock.warning.called) def test_run(self): - callback = mock.Mock() + callback = Mock() observer = metrics.SumObserver( - callback, "name", "desc", "unit", int, (), True + callback, "name", "desc", "unit", int, Mock(), (), True ) self.assertTrue(observer.run()) callback.assert_called_once_with(observer) - @mock.patch("opentelemetry.sdk.metrics.logger") + @patch("opentelemetry.sdk.metrics.logger") def test_run_exception(self, logger_mock): - callback = mock.Mock() + callback = Mock() callback.side_effect = Exception("We have a problem!") observer = metrics.SumObserver( - callback, "name", "desc", "unit", int, (), True + callback, "name", "desc", "unit", int, Mock(), (), True ) self.assertFalse(observer.run()) @@ -415,7 +415,7 @@ def test_run_exception(self, logger_mock): class TestUpDownSumObserver(unittest.TestCase): def test_observe(self): observer = metrics.UpDownSumObserver( - None, "name", "desc", "unit", int, ("key",), True + None, "name", "desc", "unit", int, Mock(), ("key",), True ) labels = {"key": "value"} key_labels = metrics.get_dict_as_key(labels) @@ -427,16 +427,16 @@ def test_observe(self): def test_observe_disabled(self): observer = metrics.UpDownSumObserver( - None, "name", "desc", "unit", int, ("key",), False + None, "name", "desc", "unit", int, Mock(), ("key",), False ) labels = {"key": "value"} observer.observe(37, labels) self.assertEqual(len(observer.aggregators), 0) - @mock.patch("opentelemetry.sdk.metrics.logger") + @patch("opentelemetry.sdk.metrics.logger") def test_observe_incorrect_type(self, logger_mock): observer = metrics.UpDownSumObserver( - None, "name", "desc", "unit", int, ("key",), True + None, "name", "desc", "unit", int, Mock(), ("key",), True ) labels = {"key": "value"} observer.observe(37.0, labels) @@ -444,21 +444,21 @@ def test_observe_incorrect_type(self, logger_mock): self.assertTrue(logger_mock.warning.called) def test_run(self): - callback = mock.Mock() + callback = Mock() observer = metrics.UpDownSumObserver( - callback, "name", "desc", "unit", int, (), True + callback, "name", "desc", "unit", int, Mock(), (), True ) self.assertTrue(observer.run()) callback.assert_called_once_with(observer) - @mock.patch("opentelemetry.sdk.metrics.logger") + @patch("opentelemetry.sdk.metrics.logger") def test_run_exception(self, logger_mock): - callback = mock.Mock() + callback = Mock() callback.side_effect = Exception("We have a problem!") observer = metrics.UpDownSumObserver( - callback, "name", "desc", "unit", int, (), True + callback, "name", "desc", "unit", int, Mock(), (), True ) self.assertFalse(observer.run()) @@ -468,7 +468,7 @@ def test_run_exception(self, logger_mock): class TestValueObserver(unittest.TestCase): def test_observe(self): observer = metrics.ValueObserver( - None, "name", "desc", "unit", int, ("key",), True + None, "name", "desc", "unit", int, Mock(), ("key",), True ) labels = {"key": "value"} key_labels = metrics.get_dict_as_key(labels) @@ -484,16 +484,16 @@ def test_observe(self): def test_observe_disabled(self): observer = metrics.ValueObserver( - None, "name", "desc", "unit", int, ("key",), False + None, "name", "desc", "unit", int, Mock(), ("key",), False ) labels = {"key": "value"} observer.observe(37, labels) self.assertEqual(len(observer.aggregators), 0) - @mock.patch("opentelemetry.sdk.metrics.logger") + @patch("opentelemetry.sdk.metrics.logger") def test_observe_incorrect_type(self, logger_mock): observer = metrics.ValueObserver( - None, "name", "desc", "unit", int, ("key",), True + None, "name", "desc", "unit", int, Mock(), ("key",), True ) labels = {"key": "value"} observer.observe(37.0, labels) @@ -501,21 +501,21 @@ def test_observe_incorrect_type(self, logger_mock): self.assertTrue(logger_mock.warning.called) def test_run(self): - callback = mock.Mock() + callback = Mock() observer = metrics.ValueObserver( - callback, "name", "desc", "unit", int, (), True + callback, "name", "desc", "unit", int, Mock(), (), True ) self.assertTrue(observer.run()) callback.assert_called_once_with(observer) - @mock.patch("opentelemetry.sdk.metrics.logger") + @patch("opentelemetry.sdk.metrics.logger") def test_run_exception(self, logger_mock): - callback = mock.Mock() + callback = Mock() callback.side_effect = Exception("We have a problem!") observer = metrics.ValueObserver( - callback, "name", "desc", "unit", int, (), True + callback, "name", "desc", "unit", int, Mock(), (), True ) self.assertFalse(observer.run()) @@ -525,40 +525,40 @@ def test_run_exception(self, logger_mock): # pylint: disable=no-self-use class TestBoundCounter(unittest.TestCase): def test_add(self): - meter_mock = mock.Mock() - metric_mock = mock.Mock() + meter_mock = Mock() + metric_mock = Mock() metric_mock.enabled = True metric_mock.value_type = int metric_mock.meter = meter_mock bound_metric = metrics.BoundCounter((), metric_mock) - view_datas_mock = mock.Mock() + view_datas_mock = Mock() bound_metric.view_datas = [view_datas_mock] bound_metric.add(3) view_datas_mock.record.assert_called_once_with(3) def test_add_disabled(self): - meter_mock = mock.Mock() - metric_mock = mock.Mock() + meter_mock = Mock() + metric_mock = Mock() metric_mock.enabled = False metric_mock.value_type = int metric_mock.meter = meter_mock bound_metric = metrics.BoundCounter((), metric_mock) - view_datas_mock = mock.Mock() + view_datas_mock = Mock() bound_metric.view_datas = [view_datas_mock] bound_metric.add(3) view_datas_mock.record.update_view.assert_not_called() - @mock.patch("opentelemetry.sdk.metrics.logger") + @patch("opentelemetry.sdk.metrics.logger") def test_add_incorrect_type(self, logger_mock): - meter_mock = mock.Mock() - viewm_mock = mock.Mock() + meter_mock = Mock() + viewm_mock = Mock() meter_mock.view_manager = viewm_mock - metric_mock = mock.Mock() + metric_mock = Mock() metric_mock.enabled = True metric_mock.value_type = float metric_mock.meter = meter_mock bound_metric = metrics.BoundCounter((), metric_mock) - view_datas_mock = mock.Mock() + view_datas_mock = Mock() bound_metric.view_datas = [view_datas_mock] bound_metric.add(3) view_datas_mock.record.update_view.assert_not_called() @@ -567,38 +567,38 @@ def test_add_incorrect_type(self, logger_mock): class TestBoundValueRecorder(unittest.TestCase): def test_record(self): - meter_mock = mock.Mock() - metric_mock = mock.Mock() + meter_mock = Mock() + metric_mock = Mock() metric_mock.enabled = True metric_mock.value_type = int metric_mock.meter = meter_mock bound_valuerecorder = metrics.BoundValueRecorder((), metric_mock) - view_datas_mock = mock.Mock() + view_datas_mock = Mock() bound_valuerecorder.view_datas = [view_datas_mock] bound_valuerecorder.record(3) view_datas_mock.record.assert_called_once_with(3) def test_record_disabled(self): - meter_mock = mock.Mock() - metric_mock = mock.Mock() + meter_mock = Mock() + metric_mock = Mock() metric_mock.enabled = False metric_mock.value_type = int metric_mock.meter = meter_mock bound_valuerecorder = metrics.BoundValueRecorder((), metric_mock) - view_datas_mock = mock.Mock() + view_datas_mock = Mock() bound_valuerecorder.view_datas = [view_datas_mock] bound_valuerecorder.record(3) view_datas_mock.record.update_view.assert_not_called() - @mock.patch("opentelemetry.sdk.metrics.logger") + @patch("opentelemetry.sdk.metrics.logger") def test_record_incorrect_type(self, logger_mock): - meter_mock = mock.Mock() - metric_mock = mock.Mock() + meter_mock = Mock() + metric_mock = Mock() metric_mock.enabled = True metric_mock.value_type = float metric_mock.meter = meter_mock bound_valuerecorder = metrics.BoundValueRecorder((), metric_mock) - view_datas_mock = mock.Mock() + view_datas_mock = Mock() bound_valuerecorder.view_datas = [view_datas_mock] bound_valuerecorder.record(3) view_datas_mock.record.update_view.assert_not_called() diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py index 102db6da25..c20d6ed3c4 100644 --- a/opentelemetry-sdk/tests/metrics/test_view.py +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -43,12 +43,14 @@ def test_default_aggregator(self, logger_mock): self.assertEqual( view.get_default_aggregator(ud_counter), aggregate.SumAggregator ) - observer = metrics.SumObserver(lambda: None, "", "", "1", int) + observer = metrics.SumObserver(lambda: None, "", "", "1", int, meter) self.assertEqual( view.get_default_aggregator(observer), aggregate.LastValueAggregator, ) - ud_observer = metrics.SumObserver(lambda: None, "", "", "1", int) + ud_observer = metrics.SumObserver( + lambda: None, "", "", "1", int, meter + ) self.assertEqual( view.get_default_aggregator(ud_observer), aggregate.LastValueAggregator, @@ -58,7 +60,9 @@ def test_default_aggregator(self, logger_mock): view.get_default_aggregator(recorder), aggregate.MinMaxSumCountAggregator, ) - v_observer = metrics.ValueObserver(lambda: None, "", "", "1", int) + v_observer = metrics.ValueObserver( + lambda: None, "", "", "1", int, meter + ) self.assertEqual( view.get_default_aggregator(v_observer), aggregate.ValueObserverAggregator, From d2028958d75b6399af1331c261f024993a4da476 Mon Sep 17 00:00:00 2001 From: Yang Sun Date: Mon, 30 Nov 2020 17:21:02 +0000 Subject: [PATCH 0691/1517] Fixes #1390 - Remove unexpected keyword argument in metrics_example (#1433) Co-authored-by: Leighton Chen --- docs/getting_started/metrics_example.py | 1 - docs/getting_started/tests/test_metrics.py | 29 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 docs/getting_started/tests/test_metrics.py diff --git a/docs/getting_started/metrics_example.py b/docs/getting_started/metrics_example.py index 08751e9864..9efcab6403 100644 --- a/docs/getting_started/metrics_example.py +++ b/docs/getting_started/metrics_example.py @@ -32,7 +32,6 @@ description="number of requests", unit="1", value_type=int, - label_keys=("environment",), ) requests_counter.add(25, staging_labels) diff --git a/docs/getting_started/tests/test_metrics.py b/docs/getting_started/tests/test_metrics.py new file mode 100644 index 0000000000..a9d7dbe45b --- /dev/null +++ b/docs/getting_started/tests/test_metrics.py @@ -0,0 +1,29 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import subprocess +import sys +import unittest + + +class TestBasicMetricsExample(unittest.TestCase): + def test_basic_meter(self): + dirpath = os.path.dirname(os.path.realpath(__file__)) + test_script = "{}/../metrics_example.py".format(dirpath) + output = subprocess.check_output( + (sys.executable, test_script) + ).decode() + + self.assertIn('name="requests"', output) + self.assertIn('description="number of requests"', output) From 50f5c6c93b6fde0292fa9583851ed6fdf0435e5d Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 2 Dec 2020 08:58:12 -0600 Subject: [PATCH 0692/1517] Add fields property (#1374) --- .github/workflows/test.yml | 2 +- opentelemetry-api/CHANGELOG.md | 12 +++++-- .../baggage/propagation/__init__.py | 5 +++ .../opentelemetry/propagators/composite.py | 15 ++++++++ .../trace/propagation/textmap.py | 13 +++++++ .../trace/propagation/tracecontext.py | 9 +++++ .../tests/baggage/test_baggage_propagation.py | 17 ++++++++- .../tests/propagators/test_composite.py | 35 +++++++++++++++++-- .../test_tracecontexthttptextformat.py | 34 +++++++++++++++++- opentelemetry-sdk/CHANGELOG.md | 2 ++ .../sdk/trace/propagation/b3_format.py | 9 +++++ .../tests/trace/propagation/test_b3_format.py | 20 ++++++++++- .../src/opentelemetry/test/mock_textmap.py | 8 +++++ 13 files changed, 172 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce3c100d0a..e082982a03 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: bcec49cf2eccf8da66c9e63b9836ea8a20516efc + CONTRIB_REPO_SHA: e1d4eeb951694afebb1767b4ea8f593753d894e3 jobs: build: diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index ccf3f53e31..84604f4bb2 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,18 +2,24 @@ ## Unreleased +- Add `fields` to propagators + ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) + ## Version 0.16b0 Released 2020-11-25 -- Add optional parameter to `record_exception` method ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) -- Add pickle support to SpanContext class ([#1380](https://github.com/open-telemetry/opentelemetry-python/pull/1380)) +- Add optional parameter to `record_exception` method + ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) +- Add pickle support to SpanContext class + ([#1380](https://github.com/open-telemetry/opentelemetry-python/pull/1380)) ## Version 0.15b0 Released 2020-11-02 -- Updating status codes to adhere to specs ([#1282](https://github.com/open-telemetry/opentelemetry-python/pull/1282)) +- Updating status codes to adhere to specs + ([#1282](https://github.com/open-telemetry/opentelemetry-python/pull/1282)) ## Version 0.14b0 diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index 70e73a3b23..ad47179c3d 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -87,6 +87,11 @@ def inject( baggage_string = _format_baggage(baggage_entries) set_in_carrier(carrier, self._BAGGAGE_HEADER_NAME, baggage_string) + @property + def fields(self) -> typing.Set[str]: + """Returns a set with the fields set in `inject`.""" + return {self._BAGGAGE_HEADER_NAME} + def _format_baggage(baggage_entries: typing.Mapping[str, object]) -> str: return ",".join( diff --git a/opentelemetry-api/src/opentelemetry/propagators/composite.py b/opentelemetry-api/src/opentelemetry/propagators/composite.py index 441098ec1f..fde42d9373 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/composite.py +++ b/opentelemetry-api/src/opentelemetry/propagators/composite.py @@ -65,3 +65,18 @@ def inject( """ for propagator in self._propagators: propagator.inject(set_in_carrier, carrier, context) + + @property + def fields(self) -> typing.Set[str]: + """Returns a set with the fields set in `inject`. + + See + `opentelemetry.trace.propagation.textmap.TextMapPropagator.fields` + """ + composite_fields = set() + + for propagator in self._propagators: + for field in propagator.fields: + composite_fields.add(field) + + return composite_fields diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py b/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py index ec505135be..fb92d37168 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py @@ -129,3 +129,16 @@ def inject( context if not set. """ + + @property + @abc.abstractmethod + def fields(self) -> typing.Set[str]: + """ + Gets the fields set in the carrier by the `inject` method. + + If the carrier is reused, its fields that correspond with the ones + present in this attribute should be deleted before calling `inject`. + + Returns: + A set with the fields set in `inject`. + """ diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index 57933ef9c4..adcdb5e125 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -132,6 +132,15 @@ def inject( carrier, self._TRACESTATE_HEADER_NAME, tracestate_string ) + @property + def fields(self) -> typing.Set[str]: + """Returns a set with the fields set in `inject`. + + See + `opentelemetry.trace.propagation.textmap.TextMapPropagator.fields` + """ + return {self._TRACEPARENT_HEADER_NAME, self._TRACESTATE_HEADER_NAME} + def _parse_tracestate(header_list: typing.List[str]) -> trace.TraceState: """Parse one or more w3c tracestate header into a TraceState. diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py index 4c7b3de215..68723c6a0b 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import typing import unittest +from unittest.mock import Mock, patch from opentelemetry import baggage from opentelemetry.baggage.propagation import BaggagePropagator @@ -142,3 +142,18 @@ def test_inject_non_string_values(self): self.assertIn("key1=True", output) self.assertIn("key2=123", output) self.assertIn("key3=123.567", output) + + @patch("opentelemetry.baggage.propagation.baggage") + @patch("opentelemetry.baggage.propagation._format_baggage") + def test_fields(self, mock_format_baggage, mock_baggage): + + mock_set_in_carrier = Mock() + + self.propagator.inject(mock_set_in_carrier, {}) + + inject_fields = set() + + for mock_call in mock_set_in_carrier.mock_calls: + inject_fields.add(mock_call[1][1]) + + self.assertEqual(inject_fields, self.propagator.fields) diff --git a/opentelemetry-api/tests/propagators/test_composite.py b/opentelemetry-api/tests/propagators/test_composite.py index 8c61b6dc1f..232e177d3d 100644 --- a/opentelemetry-api/tests/propagators/test_composite.py +++ b/opentelemetry-api/tests/propagators/test_composite.py @@ -26,6 +26,8 @@ def get_as_list(dict_object, key): def mock_inject(name, value="data"): def wrapped(setter, carrier=None, context=None): carrier[name] = value + setter({}, "inject_field_{}_0".format(name), None) + setter({}, "inject_field_{}_1".format(name), None) return wrapped @@ -39,18 +41,27 @@ def wrapped(getter, carrier=None, context=None): return wrapped +def mock_fields(name): + return {"inject_field_{}_0".format(name), "inject_field_{}_1".format(name)} + + class TestCompositePropagator(unittest.TestCase): @classmethod def setUpClass(cls): cls.mock_propagator_0 = Mock( - inject=mock_inject("mock-0"), extract=mock_extract("mock-0") + inject=mock_inject("mock-0"), + extract=mock_extract("mock-0"), + fields=mock_fields("mock-0"), ) cls.mock_propagator_1 = Mock( - inject=mock_inject("mock-1"), extract=mock_extract("mock-1") + inject=mock_inject("mock-1"), + extract=mock_extract("mock-1"), + fields=mock_fields("mock-1"), ) cls.mock_propagator_2 = Mock( inject=mock_inject("mock-0", value="data2"), extract=mock_extract("mock-0", value="context2"), + fields=mock_fields("mock-0"), ) def test_no_propagators(self): @@ -105,3 +116,23 @@ def test_multiple_propagators_same_key(self): get_as_list, carrier=new_carrier, context={} ) self.assertEqual(context, {"mock-0": "context2"}) + + def test_fields(self): + propagator = CompositeHTTPPropagator( + [ + self.mock_propagator_0, + self.mock_propagator_1, + self.mock_propagator_2, + ] + ) + + mock_set_in_carrier = Mock() + + propagator.inject(mock_set_in_carrier, {}) + + inject_fields = set() + + for mock_call in mock_set_in_carrier.mock_calls: + inject_fields.add(mock_call[1][1]) + + self.assertEqual(inject_fields, propagator.fields) diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index f012be2a23..1e5c820243 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -14,6 +14,7 @@ import typing import unittest +from unittest.mock import Mock, patch from opentelemetry import trace from opentelemetry.trace.propagation import tracecontext @@ -232,7 +233,8 @@ def test_tracestate_keys(self): carrier_getter, { "traceparent": [ - "00-12345678901234567890123456789012-1234567890123456-00" + "00-12345678901234567890123456789012-" + "1234567890123456-00" ], "tracestate": [tracestate_value], }, @@ -248,3 +250,33 @@ def test_tracestate_keys(self): self.assertEqual( span.get_span_context().trace_state["foo-_*/bar"], "bar4" ) + + @patch("opentelemetry.trace.INVALID_SPAN_CONTEXT") + @patch("opentelemetry.trace.get_current_span") + def test_fields(self, mock_get_current_span, mock_invalid_span_context): + + mock_get_current_span.configure_mock( + return_value=Mock( + **{ + "get_span_context.return_value": Mock( + **{ + "trace_id": 1, + "span_id": 2, + "trace_flags": 3, + "trace_state": {"a": "b"}, + } + ) + } + ) + ) + + mock_set_in_carrier = Mock() + + FORMAT.inject(mock_set_in_carrier, {}) + + inject_fields = set() + + for mock_call in mock_set_in_carrier.mock_calls: + inject_fields.add(mock_call[1][1]) + + self.assertEqual(inject_fields, FORMAT.fields) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 0e0b0f550f..5014e1c19b 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -4,6 +4,8 @@ - Add meter reference to observers ([#1425](https://github.com/open-telemetry/opentelemetry-python/pull/1425)) +- Add `fields` to propagators + ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) ## Version 0.16b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py index c629d107d3..7f59ff417b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py @@ -152,6 +152,15 @@ def inject( ) set_in_carrier(carrier, self.SAMPLED_KEY, "1" if sampled else "0") + @property + def fields(self) -> typing.Set[str]: + return { + self.TRACE_ID_KEY, + self.SPAN_ID_KEY, + self.PARENT_SPAN_ID_KEY, + self.SAMPLED_KEY, + } + def format_trace_id(trace_id: int) -> str: """Format the trace id according to b3 specification.""" diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index 8546b0b922..93d5bbe56e 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -13,7 +13,7 @@ # limitations under the License. import unittest -from unittest.mock import patch +from unittest.mock import Mock, patch import opentelemetry.sdk.trace as trace import opentelemetry.sdk.trace.propagation.b3_format as b3_format @@ -337,3 +337,21 @@ def setter(carrier, key, value): ctx = FORMAT.extract(CarrierGetter(), {}) FORMAT.inject(setter, {}, ctx) + + def test_fields(self): + """Make sure the fields attribute returns the fields used in inject""" + + tracer = trace.TracerProvider().get_tracer("sdk_tracer_provider") + + mock_set_in_carrier = Mock() + + with tracer.start_as_current_span("parent"): + with tracer.start_as_current_span("child"): + FORMAT.inject(mock_set_in_carrier, {}) + + inject_fields = set() + + for call in mock_set_in_carrier.mock_calls: + inject_fields.add(call[1][1]) + + self.assertEqual(FORMAT.fields, inject_fields) diff --git a/tests/util/src/opentelemetry/test/mock_textmap.py b/tests/util/src/opentelemetry/test/mock_textmap.py index 1fee6d2a9c..50e40490b7 100644 --- a/tests/util/src/opentelemetry/test/mock_textmap.py +++ b/tests/util/src/opentelemetry/test/mock_textmap.py @@ -47,6 +47,10 @@ def inject( ) -> None: return None + @property + def fields(self): + return set() + class MockTextMapPropagator(TextMapPropagator): """Mock propagator for testing purposes.""" @@ -89,3 +93,7 @@ def inject( set_in_carrier( carrier, self.SPAN_ID_KEY, str(span.get_span_context().span_id) ) + + @property + def fields(self): + return {self.TRACE_ID_KEY, self.SPAN_ID_KEY} From 00578e334ace3227308034ef16ba623968ee19c2 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 3 Dec 2020 10:12:45 -0800 Subject: [PATCH 0693/1517] OTLP exporter use grpc runtime cert if not set (#1430) --- exporter/opentelemetry-exporter-otlp/CHANGELOG.md | 4 ++++ .../src/opentelemetry/exporter/otlp/exporter.py | 11 ++++++----- .../tests/test_otlp_metric_exporter.py | 14 +++++++++++--- .../tests/test_otlp_trace_exporter.py | 12 +++++++++--- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 4528c21534..e3968d6825 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.16b1 + +Released 2020-11-26 + - Add meter reference to observers ([#1425](https://github.com/open-telemetry/opentelemetry-python/pull/1425)) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index 54853f105c..93a87b8cf7 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -208,11 +208,12 @@ def __init__( credentials is None and Configuration().EXPORTER_OTLP_CERTIFICATE is None ): - raise ValueError("No credentials set in secure mode.") - - credentials = credentials or _load_credential_from_file( - Configuration().EXPORTER_OTLP_CERTIFICATE - ) + # use the default location chosen by gRPC runtime + credentials = ssl_channel_credentials() + else: + credentials = credentials or _load_credential_from_file( + Configuration().EXPORTER_OTLP_CERTIFICATE + ) self._client = self._stub( secure_channel( endpoint, credentials, compression=compression_algorithm diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index 6d45e30d44..c8664f94fa 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -90,9 +90,17 @@ def test_env_variables(self, mock_exporter_mixin): self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) - def test_no_credentials_error(self): - with self.assertRaises(ValueError): - OTLPMetricsExporter() + @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") + @patch("opentelemetry.exporter.otlp.exporter.secure_channel") + @patch( + "opentelemetry.exporter.otlp.metrics_exporter.OTLPMetricsExporter._stub" + ) + # pylint: disable=unused-argument + def test_no_credentials_error( + self, mock_ssl_channel, mock_secure, mock_stub + ): + OTLPMetricsExporter(insecure=False) + self.assertTrue(mock_ssl_channel.called) @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") def test_translate_counter_export_record(self, mock_time_ns): diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index 4445873aaf..b691e85849 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -187,9 +187,15 @@ def test_env_variables(self, mock_exporter_mixin): self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) - def test_no_credentials_error(self): - with self.assertRaises(ValueError): - OTLPSpanExporter() + @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") + @patch("opentelemetry.exporter.otlp.exporter.secure_channel") + @patch("opentelemetry.exporter.otlp.trace_exporter.OTLPSpanExporter._stub") + # pylint: disable=unused-argument + def test_no_credentials_error( + self, mock_ssl_channel, mock_secure, mock_stub + ): + OTLPSpanExporter(insecure=False) + self.assertTrue(mock_ssl_channel.called) @patch("opentelemetry.exporter.otlp.exporter.expo") @patch("opentelemetry.exporter.otlp.exporter.sleep") From 268cfbf42a6b09069e93904c737ca6cef9c5c14d Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 4 Dec 2020 15:12:20 +0000 Subject: [PATCH 0694/1517] Add convenience methods to access common config options in Configuration class (#1426) --- .github/workflows/test.yml | 2 +- .../opentelemetry/configuration/__init__.py | 35 +++++++++++++++++-- .../src/opentelemetry/util/__init__.py | 12 ------- .../tests/configuration/test_configuration.py | 29 +++++++++++++++ .../test_exclude_list.py | 3 +- 5 files changed, 63 insertions(+), 18 deletions(-) rename opentelemetry-api/tests/{util => configuration}/test_exclude_list.py (97%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e082982a03..e71b58f94a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: e1d4eeb951694afebb1767b4ea8f593753d894e3 + CONTRIB_REPO_SHA: fd12b1d624fe44ca17d2c88c0ace39dc80db85df jobs: build: diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index bf641ca836..5e3d3667dc 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -93,14 +93,26 @@ to override this value instead of changing it. """ +import re from os import environ -from re import fullmatch -from typing import ClassVar, Dict, Optional, TypeVar, Union +from typing import ClassVar, Dict, List, Optional, Sequence, TypeVar, Union ConfigValue = Union[str, bool, int, float] _T = TypeVar("_T", ConfigValue, Optional[ConfigValue]) +class ExcludeList: + """Class to exclude certain paths (given as a list of regexes) from tracing requests""" + + def __init__(self, excluded_urls: Sequence[str]): + self._non_empty = len(excluded_urls) > 0 + if self._non_empty: + self._regex = re.compile("|".join(excluded_urls)) + + def url_disabled(self, url: str) -> bool: + return bool(self._non_empty and re.search(self._regex, url)) + + class Configuration: _instance = None # type: ClassVar[Optional[Configuration]] _config_map = {} # type: ClassVar[Dict[str, ConfigValue]] @@ -113,7 +125,7 @@ def __new__(cls) -> "Configuration": instance = super().__new__(cls) for key, value_str in environ.items(): - match = fullmatch(r"OTEL_(PYTHON_)?([A-Za-z_][\w_]*)", key) + match = re.fullmatch(r"OTEL_(PYTHON_)?([A-Za-z_][\w_]*)", key) if match is not None: @@ -167,3 +179,20 @@ def _reset(cls) -> None: if cls._instance: cls._instance._config_map.clear() # pylint: disable=protected-access cls._instance = None + + def _traced_request_attrs(self, instrumentation: str) -> List[str]: + """Returns list of traced request attributes for instrumentation.""" + key = "{}_TRACED_REQUEST_ATTRS".format(instrumentation.upper()) + value = self._config_map.get(key, "") + + request_attrs = ( + [attr.strip() for attr in str.split(value, ",")] if value else [] # type: ignore + ) + return request_attrs + + def _excluded_urls(self, instrumentation: str) -> ExcludeList: + key = "{}_EXCLUDED_URLS".format(instrumentation.upper()) + value = self._config_map.get(key, "") + + urls = str.split(value, ",") if value else [] # type: ignore + return ExcludeList(urls) diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index 5d98ba96bf..58c297269e 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -65,15 +65,3 @@ def _load_meter_provider(provider: str) -> "MeterProvider": def _load_trace_provider(provider: str) -> "TracerProvider": return cast("TracerProvider", _load_provider(provider)) - - -class ExcludeList: - """Class to exclude certain paths (given as a list of regexes) from tracing requests""" - - def __init__(self, excluded_urls: Sequence[str]): - self._non_empty = len(excluded_urls) > 0 - if self._non_empty: - self._regex = re.compile("|".join(excluded_urls)) - - def url_disabled(self, url: str) -> bool: - return bool(self._non_empty and re.search(self._regex, url)) diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index 608a96c96d..de7bb16de6 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -145,3 +145,32 @@ def test_float(self) -> None: self.assertEqual( Configuration().NON_FLOAT, "-12z3.123" ) # pylint: disable=no-member + + @patch.dict( + "os.environ", # type: ignore + { + "OTEL_PYTHON_WEBFRAMEWORK_TRACED_REQUEST_ATTRS": "content_type,keep_alive", + }, + ) + def test_traced_request_attrs(self) -> None: + cfg = Configuration() + request_attrs = cfg._traced_request_attrs("webframework") + self.assertEqual(len(request_attrs), 2) + self.assertIn("content_type", request_attrs) + self.assertIn("keep_alive", request_attrs) + self.assertNotIn("authorization", request_attrs) + + @patch.dict( + "os.environ", # type: ignore + { + "OTEL_PYTHON_WEBFRAMEWORK_EXCLUDED_URLS": "/healthzz,path,/issues/.*/view", + }, + ) + def test_excluded_urls(self) -> None: + cfg = Configuration() + excluded_urls = cfg._excluded_urls("webframework") + self.assertTrue(excluded_urls.url_disabled("/healthzz")) + self.assertTrue(excluded_urls.url_disabled("/path")) + self.assertTrue(excluded_urls.url_disabled("/issues/123/view")) + self.assertFalse(excluded_urls.url_disabled("/issues")) + self.assertFalse(excluded_urls.url_disabled("/hello")) diff --git a/opentelemetry-api/tests/util/test_exclude_list.py b/opentelemetry-api/tests/configuration/test_exclude_list.py similarity index 97% rename from opentelemetry-api/tests/util/test_exclude_list.py rename to opentelemetry-api/tests/configuration/test_exclude_list.py index da51478de3..c7baf842ed 100644 --- a/opentelemetry-api/tests/util/test_exclude_list.py +++ b/opentelemetry-api/tests/configuration/test_exclude_list.py @@ -13,9 +13,8 @@ # limitations under the License. import unittest -from unittest import mock -from opentelemetry.util import ExcludeList +from opentelemetry.configuration import ExcludeList class TestExcludeList(unittest.TestCase): From 0f8eb6cc2d3554797c7d36ba6d75c9d81b93bb3b Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 4 Dec 2020 12:51:40 -0500 Subject: [PATCH 0695/1517] Use a delegate sampler for each possible case in ParentBased Sampler (#1440) --- opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/sampling.py | 76 ++++++++--- .../tests/trace/test_sampling.py | 127 ++++++++++++++++-- 3 files changed, 176 insertions(+), 29 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 5014e1c19b..9f94dff63c 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -4,6 +4,8 @@ - Add meter reference to observers ([#1425](https://github.com/open-telemetry/opentelemetry-python/pull/1425)) +- Add local/remote samplers to parent based sampler + ([#1440](https://github.com/open-telemetry/opentelemetry-python/pull/1440)) - Add `fields` to propagators ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 82d2cebaa5..d1e02b3109 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -27,7 +27,7 @@ A `TraceIdRatioBased` sampler makes a random sampling result based on the sampling probability given. -If the span being sampled has a parent, `ParentBased` will respect the parent span's sampling result. Otherwise, it returns the sampling result from the given delegate sampler. +If the span being sampled has a parent, `ParentBased` will respect the parent delegate sampler. Otherwise, it returns the sampling result from the given root sampler. Currently, sampling results are always made during the creation of the span. However, this might not always be the case in the future (see `OTEP #115 `_). @@ -160,6 +160,13 @@ def get_description(self) -> str: return "AlwaysOnSampler" +ALWAYS_OFF = StaticSampler(Decision.DROP) +"""Sampler that never samples spans, regardless of the parent span's sampling decision.""" + +ALWAYS_ON = StaticSampler(Decision.RECORD_AND_SAMPLE) +"""Sampler that always samples spans, regardless of the parent span's sampling decision.""" + + class TraceIdRatioBased(Sampler): """ Sampler that makes sampling decisions probabalistically based on `rate`, @@ -218,16 +225,33 @@ def get_description(self) -> str: class ParentBased(Sampler): """ - If a parent is set, follows the same sampling decision as the parent. - Otherwise, uses the delegate provided at initialization to make a + If a parent is set, applies the respective delegate sampler. + Otherwise, uses the root provided at initialization to make a decision. Args: - delegate: The delegate sampler to use if parent is not set. + root: Sampler called for spans with no parent (root spans). + remote_parent_sampled: Sampler called for a remote sampled parent. + remote_parent_not_sampled: Sampler called for a remote parent that is + not sampled. + local_parent_sampled: Sampler called for a local sampled parent. + local_parent_not_sampled: Sampler called for a local parent that is + not sampled. """ - def __init__(self, delegate: Sampler): - self._delegate = delegate + def __init__( + self, + root: Sampler, + remote_parent_sampled: Sampler = ALWAYS_ON, + remote_parent_not_sampled: Sampler = ALWAYS_OFF, + local_parent_sampled: Sampler = ALWAYS_ON, + local_parent_not_sampled: Sampler = ALWAYS_OFF, + ): + self._root = root + self._remote_parent_sampled = remote_parent_sampled + self._remote_parent_not_sampled = remote_parent_not_sampled + self._local_parent_sampled = local_parent_sampled + self._local_parent_not_sampled = local_parent_not_sampled def should_sample( self, @@ -241,15 +265,22 @@ def should_sample( parent_span_context = get_current_span( parent_context ).get_span_context() - # respect the sampling flag of the parent if present + # default to the root sampler + sampler = self._root + # respect the sampling and remote flag of the parent if present if parent_span_context is not None and parent_span_context.is_valid: - decision = Decision.RECORD_AND_SAMPLE - if not parent_span_context.trace_flags.sampled: - decision = Decision.DROP - attributes = None - return SamplingResult(decision, attributes, trace_state) - - return self._delegate.should_sample( + if parent_span_context.is_remote: + if parent_span_context.trace_flags.sampled: + sampler = self._remote_parent_sampled + else: + sampler = self._remote_parent_not_sampled + else: + if parent_span_context.trace_flags.sampled: + sampler = self._local_parent_sampled + else: + sampler = self._local_parent_not_sampled + + return sampler.should_sample( parent_context=parent_context, trace_id=trace_id, name=name, @@ -259,14 +290,17 @@ def should_sample( ) def get_description(self): - return "ParentBased{{{}}}".format(self._delegate.get_description()) - - -ALWAYS_OFF = StaticSampler(Decision.DROP) -"""Sampler that never samples spans, regardless of the parent span's sampling decision.""" + return ( + "ParentBased{{root:{},remoteParentSampled:{},remoteParentNotSampled:{}," + "localParentSampled:{},localParentNotSampled:{}}}".format( + self._root.get_description(), + self._remote_parent_sampled.get_description(), + self._remote_parent_not_sampled.get_description(), + self._local_parent_sampled.get_description(), + self._local_parent_not_sampled.get_description(), + ) + ) -ALWAYS_ON = StaticSampler(Decision.RECORD_AND_SAMPLE) -"""Sampler that always samples spans, regardless of the parent span's sampling decision.""" DEFAULT_OFF = ParentBased(ALWAYS_OFF) """Sampler that respects its parent span's sampling decision, but otherwise never samples.""" diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index f6d77ab04b..0d026de01d 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -311,13 +311,15 @@ def test_probability_sampler_limits(self): almost_almost_always_on.bound, 0xFFFFFFFFFFFFFFFF, ) + # pylint:disable=too-many-statements def exec_parent_based(self, parent_sampling_context): trace_state = trace.TraceState({"key": "value"}) sampler = sampling.ParentBased(sampling.ALWAYS_ON) + # Check that the sampling decision matches the parent context if given with parent_sampling_context( self._create_parent_span(trace_flags=TO_DEFAULT) ) as context: - # Check that the sampling decision matches the parent context if given + # local, not sampled not_sampled_result = sampler.should_sample( context, 0x7FFFFFFFFFFFFFFF, @@ -329,11 +331,101 @@ def exec_parent_based(self, parent_sampling_context): self.assertEqual(not_sampled_result.attributes, {}) self.assertEqual(not_sampled_result.trace_state, trace_state) + with parent_sampling_context( + self._create_parent_span(trace_flags=TO_DEFAULT) + ) as context: + sampler = sampling.ParentBased( + root=sampling.ALWAYS_OFF, + local_parent_not_sampled=sampling.ALWAYS_ON, + ) + # local, not sampled -> opposite sampler + sampled_result = sampler.should_sample( + context, + 0x7FFFFFFFFFFFFFFF, + "unsampled parent, sampling on", + attributes={"sampled": "false"}, + trace_state=trace_state, + ) + self.assertTrue(sampled_result.decision.is_sampled()) + self.assertEqual(sampled_result.attributes, {"sampled": "false"}) + self.assertEqual(sampled_result.trace_state, trace_state) + + with parent_sampling_context( + self._create_parent_span(trace_flags=TO_SAMPLED) + ) as context: + sampler = sampling.ParentBased(sampling.ALWAYS_OFF) + # local, sampled + sampled_result = sampler.should_sample( + context, + 0x8000000000000000, + "sampled parent, sampling off", + attributes={"sampled": "true"}, + trace_state=trace_state, + ) + self.assertTrue(sampled_result.decision.is_sampled()) + self.assertEqual(sampled_result.attributes, {"sampled": "true"}) + self.assertEqual(sampled_result.trace_state, trace_state) + with parent_sampling_context( self._create_parent_span(trace_flags=TO_SAMPLED) ) as context: - sampler2 = sampling.ParentBased(sampling.ALWAYS_OFF) - sampled_result = sampler2.should_sample( + sampler = sampling.ParentBased( + root=sampling.ALWAYS_ON, + local_parent_sampled=sampling.ALWAYS_OFF, + ) + # local, sampled -> opposite sampler + not_sampled_result = sampler.should_sample( + context, + 0x7FFFFFFFFFFFFFFF, + "unsampled parent, sampling on", + attributes={"sampled": "false"}, + trace_state=trace_state, + ) + self.assertFalse(not_sampled_result.decision.is_sampled()) + self.assertEqual(not_sampled_result.attributes, {}) + self.assertEqual(not_sampled_result.trace_state, trace_state) + + with parent_sampling_context( + self._create_parent_span(trace_flags=TO_DEFAULT, is_remote=True) + ) as context: + sampler = sampling.ParentBased(sampling.ALWAYS_ON) + # remote, not sampled + not_sampled_result = sampler.should_sample( + context, + 0x7FFFFFFFFFFFFFFF, + "unsampled parent, sampling on", + attributes={"sampled": "false"}, + trace_state=trace_state, + ) + self.assertFalse(not_sampled_result.decision.is_sampled()) + self.assertEqual(not_sampled_result.attributes, {}) + self.assertEqual(not_sampled_result.trace_state, trace_state) + + with parent_sampling_context( + self._create_parent_span(trace_flags=TO_DEFAULT, is_remote=True) + ) as context: + sampler = sampling.ParentBased( + root=sampling.ALWAYS_OFF, + remote_parent_not_sampled=sampling.ALWAYS_ON, + ) + # remote, not sampled -> opposite sampler + sampled_result = sampler.should_sample( + context, + 0x7FFFFFFFFFFFFFFF, + "unsampled parent, sampling on", + attributes={"sampled": "false"}, + trace_state=trace_state, + ) + self.assertTrue(sampled_result.decision.is_sampled()) + self.assertEqual(sampled_result.attributes, {"sampled": "false"}) + self.assertEqual(sampled_result.trace_state, trace_state) + + with parent_sampling_context( + self._create_parent_span(trace_flags=TO_SAMPLED, is_remote=True) + ) as context: + sampler = sampling.ParentBased(sampling.ALWAYS_OFF) + # remote, sampled + sampled_result = sampler.should_sample( context, 0x8000000000000000, "sampled parent, sampling off", @@ -344,10 +436,29 @@ def exec_parent_based(self, parent_sampling_context): self.assertEqual(sampled_result.attributes, {"sampled": "true"}) self.assertEqual(sampled_result.trace_state, trace_state) - # for root span follow decision of delegate sampler + with parent_sampling_context( + self._create_parent_span(trace_flags=TO_SAMPLED, is_remote=True) + ) as context: + sampler = sampling.ParentBased( + root=sampling.ALWAYS_ON, + remote_parent_sampled=sampling.ALWAYS_OFF, + ) + # remote, sampled -> opposite sampler + not_sampled_result = sampler.should_sample( + context, + 0x7FFFFFFFFFFFFFFF, + "unsampled parent, sampling on", + attributes={"sampled": "false"}, + trace_state=trace_state, + ) + self.assertFalse(not_sampled_result.decision.is_sampled()) + self.assertEqual(not_sampled_result.attributes, {}) + self.assertEqual(not_sampled_result.trace_state, trace_state) + + # for root span follow decision of root sampler with parent_sampling_context(trace.INVALID_SPAN) as context: - sampler3 = sampling.ParentBased(sampling.ALWAYS_OFF) - not_sampled_result = sampler3.should_sample( + sampler = sampling.ParentBased(sampling.ALWAYS_OFF) + not_sampled_result = sampler.should_sample( context, 0x8000000000000000, "parent, sampling off", @@ -359,8 +470,8 @@ def exec_parent_based(self, parent_sampling_context): self.assertEqual(not_sampled_result.trace_state, trace_state) with parent_sampling_context(trace.INVALID_SPAN) as context: - sampler4 = sampling.ParentBased(sampling.ALWAYS_ON) - sampled_result = sampler4.should_sample( + sampler = sampling.ParentBased(sampling.ALWAYS_ON) + sampled_result = sampler.should_sample( context, 0x8000000000000000, "no parent, sampling on", From 6411755abd078a3eecabd88931f7beb5876ea2e9 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 4 Dec 2020 18:33:50 +0000 Subject: [PATCH 0696/1517] Update set_status docstring (#1434) --- opentelemetry-api/src/opentelemetry/trace/span.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 2d0e34996c..d41a0c3fbd 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -77,7 +77,7 @@ def is_recording(self) -> bool: @abc.abstractmethod def set_status(self, status: Status) -> None: """Sets the Status of the Span. If used, this will override the default - Span status, which is OK. + Span status. """ @abc.abstractmethod From 8d195e6a5a3c43a035f068dd7937ad47b32cd8e4 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 7 Dec 2020 17:44:11 +0000 Subject: [PATCH 0697/1517] Throw an error when multiple instruments are registered by the same name (#1438) --- .github/workflows/test.yml | 2 +- .gitignore | 2 + .../test_otcollector_metrics_exporter.py | 12 +- .../tests/test_metric.py | 6 +- .../src/opentelemetry/sdk/metrics/__init__.py | 111 +++++++++--------- .../tests/metrics/test_metrics.py | 49 ++++++-- 6 files changed, 113 insertions(+), 69 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e71b58f94a..7a7b9354dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: fd12b1d624fe44ca17d2c88c0ace39dc80db85df + CONTRIB_REPO_SHA: b37945bdeaf49822b240281d493d053995cc2b7b jobs: build: diff --git a/.gitignore b/.gitignore index 5378aadb36..d168765890 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,9 @@ var sdist develop-eggs .installed.cfg +pyvenv.cfg lib +share/ lib64 __pycache__ venv*/ diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py index b48a4a5a33..3b40b8d75a 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py @@ -91,13 +91,13 @@ def test_get_collector_metric_type(self): def test_get_collector_point(self): aggregator = aggregate.SumAggregator() int_counter = self._meter.create_counter( - "testName", "testDescription", "unit", int, + "testNameIntCounter", "testDescription", "unit", int, ) float_counter = self._meter.create_counter( - "testName", "testDescription", "unit", float, + "testNameFloatCounter", "testDescription", "unit", float, ) valuerecorder = self._meter.create_valuerecorder( - "testName", "testDescription", "unit", float, + "testNameValueRecorder", "testDescription", "unit", float, ) result = metrics_exporter.get_collector_point( ExportRecord( @@ -168,7 +168,7 @@ def test_export(self): def test_translate_to_collector(self): test_metric = self._meter.create_counter( - "testname", "testdesc", "unit", int, self._labels.keys() + "testcollector", "testdesc", "unit", int, self._labels.keys() ) aggregator = aggregate.SumAggregator() aggregator.update(123) @@ -185,7 +185,9 @@ def test_translate_to_collector(self): ) self.assertEqual(len(output_metrics), 1) self.assertIsInstance(output_metrics[0], metrics_pb2.Metric) - self.assertEqual(output_metrics[0].metric_descriptor.name, "testname") + self.assertEqual( + output_metrics[0].metric_descriptor.name, "testcollector" + ) self.assertEqual( output_metrics[0].metric_descriptor.description, "testdesc" ) diff --git a/opentelemetry-instrumentation/tests/test_metric.py b/opentelemetry-instrumentation/tests/test_metric.py index 8e676c737e..c0bdcca15a 100644 --- a/opentelemetry-instrumentation/tests/test_metric.py +++ b/opentelemetry-instrumentation/tests/test_metric.py @@ -72,7 +72,7 @@ def test_ctor(self): "measures the duration of the outbound HTTP request", ) - def test_ctor_types(self): + def test_ctor_type_client(self): meter = metrics_api.get_meter(__name__) recorder = HTTPMetricRecorder(meter, HTTPMetricType.CLIENT) self.assertEqual(recorder._http_type, HTTPMetricType.CLIENT) @@ -81,6 +81,8 @@ def test_ctor_types(self): ) self.assertIsNone(recorder._server_duration) + def test_ctor_type_server(self): + meter = metrics_api.get_meter(__name__) recorder = HTTPMetricRecorder(meter, HTTPMetricType.SERVER) self.assertEqual(recorder._http_type, HTTPMetricType.SERVER) self.assertTrue( @@ -88,6 +90,8 @@ def test_ctor_types(self): ) self.assertIsNone(recorder._client_duration) + def test_ctor_type_both(self): + meter = metrics_api.get_meter(__name__) recorder = HTTPMetricRecorder(meter, HTTPMetricType.BOTH) self.assertEqual(recorder._http_type, HTTPMetricType.BOTH) self.assertTrue( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index a09047e523..ca0ec2f967 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -15,7 +15,7 @@ import atexit import logging import threading -from typing import Dict, Sequence, Tuple, Type, TypeVar +from typing import Dict, Sequence, Tuple, Type, TypeVar, Union from opentelemetry import metrics as metrics_api from opentelemetry.sdk.metrics.export import ( @@ -356,12 +356,23 @@ def __init__( ): self.instrumentation_info = instrumentation_info self.processor = Processor(source.stateful, source.resource) - self.metrics = set() - self.observers = set() - self.metrics_lock = threading.Lock() - self.observers_lock = threading.Lock() + self.instruments = {} + self.instruments_lock = threading.Lock() self.view_manager = ViewManager() + def _register_instrument( + self, instrument: Union[metrics_api.Metric, metrics_api.Observer] + ): + name = instrument.name.strip().lower() + with self.instruments_lock: + if name in self.instruments: + raise ValueError( + "Multiple instruments can't be registered by the same name: ({})".format( + name + ) + ) + self.instruments[name] = instrument + def collect(self) -> None: """Collects all the metrics created with this `Meter` for export. @@ -369,46 +380,41 @@ def collect(self) -> None: each aggregator belonging to the metrics that were created with this meter instance. """ - - self._collect_metrics() - self._collect_observers() - - def _collect_metrics(self) -> None: - for metric in self.metrics: - if not metric.enabled: - continue - to_remove = [] - with metric.bound_instruments_lock: - for ( - labels, - bound_instrument, - ) in metric.bound_instruments.items(): - for view_data in bound_instrument.view_datas: + with self.instruments_lock: + for instrument in self.instruments.values(): + if not instrument.enabled: + continue + if isinstance(instrument, metrics_api.Metric): + to_remove = [] + with instrument.bound_instruments_lock: + for ( + labels, + bound_instrument, + ) in instrument.bound_instruments.items(): + for view_data in bound_instrument.view_datas: + accumulation = Accumulation( + instrument, + view_data.labels, + view_data.aggregator, + ) + self.processor.process(accumulation) + + if bound_instrument.ref_count() == 0: + to_remove.append(labels) + + # Remove handles that were released + for labels in to_remove: + del instrument.bound_instruments[labels] + elif isinstance(instrument, metrics_api.Observer): + if not instrument.run(): + continue + + for labels, aggregator in instrument.aggregators.items(): accumulation = Accumulation( - metric, view_data.labels, view_data.aggregator + instrument, labels, aggregator ) self.processor.process(accumulation) - if bound_instrument.ref_count() == 0: - to_remove.append(labels) - - # Remove handles that were released - for labels in to_remove: - del metric.bound_instruments[labels] - - def _collect_observers(self) -> None: - with self.observers_lock: - for observer in self.observers: - if not observer.enabled: - continue - - if not observer.run(): - continue - - for labels, aggregator in observer.aggregators.items(): - accumulation = Accumulation(observer, labels, aggregator) - self.processor.process(accumulation) - def record_batch( self, labels: Dict[str, str], @@ -432,8 +438,7 @@ def create_counter( counter = Counter( name, description, unit, value_type, self, enabled=enabled ) - with self.metrics_lock: - self.metrics.add(counter) + self._register_instrument(counter) return counter def create_updowncounter( @@ -448,8 +453,7 @@ def create_updowncounter( counter = UpDownCounter( name, description, unit, value_type, self, enabled=enabled ) - with self.metrics_lock: - self.metrics.add(counter) + self._register_instrument(counter) return counter def create_valuerecorder( @@ -464,8 +468,7 @@ def create_valuerecorder( recorder = ValueRecorder( name, description, unit, value_type, self, enabled=enabled ) - with self.metrics_lock: - self.metrics.add(recorder) + self._register_instrument(recorder) return recorder def register_sumobserver( @@ -488,8 +491,7 @@ def register_sumobserver( label_keys, enabled, ) - with self.observers_lock: - self.observers.add(ob) + self._register_instrument(ob) return ob def register_updownsumobserver( @@ -512,8 +514,7 @@ def register_updownsumobserver( label_keys, enabled, ) - with self.observers_lock: - self.observers.add(ob) + self._register_instrument(ob) return ob def register_valueobserver( @@ -536,13 +537,13 @@ def register_valueobserver( label_keys, enabled, ) - with self.observers_lock: - self.observers.add(ob) + self._register_instrument(ob) return ob def unregister_observer(self, observer: metrics_api.Observer) -> None: - with self.observers_lock: - self.observers.remove(observer) + name = observer.name.strip().lower() + with self.instruments_lock: + self.instruments.pop(name) def register_view(self, view): self.view_manager.register_view(view) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 1697d8e6c8..32c22c8c6b 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -118,11 +118,9 @@ def callback(observer): self.assertIsInstance(observer, metrics_api.Observer) observer.observe(45, {}) - observer = metrics.ValueObserver( + meter.register_valueobserver( callback, "name", "desc", "unit", int, (), True ) - - meter.observers.add(observer) meter.collect() self.assertTrue(processor_mock.process.called) @@ -164,6 +162,23 @@ def test_create_counter(self): self.assertIs(meter_provider.resource, resource) self.assertEqual(counter.meter, meter) + def test_instrument_same_name_error(self): + resource = Mock(spec=resources.Resource) + meter_provider = metrics.MeterProvider(resource=resource) + meter = meter_provider.get_meter(__name__) + counter = meter.create_counter("name", "desc", "unit", int,) + self.assertIsInstance(counter, metrics.Counter) + self.assertEqual(counter.value_type, int) + self.assertEqual(counter.name, "name") + self.assertIs(meter_provider.resource, resource) + self.assertEqual(counter.meter, meter) + with self.assertRaises(ValueError) as ctx: + _ = meter.create_counter("naME", "desc", "unit", int,) + self.assertTrue( + "Multiple instruments can't be registered by the same name: (name)" + in str(ctx.exception) + ) + def test_create_updowncounter(self): meter = metrics.MeterProvider().get_meter(__name__) updowncounter = meter.create_updowncounter( @@ -193,7 +208,7 @@ def test_register_sumobserver(self): ) self.assertIsInstance(observer, metrics.SumObserver) - self.assertEqual(len(meter.observers), 1) + self.assertEqual(len(meter.instruments), 1) self.assertEqual(observer.callback, callback) self.assertEqual(observer.name, "name") @@ -213,7 +228,7 @@ def test_register_updownsumobserver(self): ) self.assertIsInstance(observer, metrics.UpDownSumObserver) - self.assertEqual(len(meter.observers), 1) + self.assertEqual(len(meter.instruments), 1) self.assertEqual(observer.callback, callback) self.assertEqual(observer.name, "name") @@ -233,7 +248,7 @@ def test_register_valueobserver(self): ) self.assertIsInstance(observer, metrics.ValueObserver) - self.assertEqual(len(meter.observers), 1) + self.assertEqual(len(meter.instruments), 1) self.assertEqual(observer.callback, callback) self.assertEqual(observer.name, "name") @@ -253,7 +268,27 @@ def test_unregister_observer(self): ) meter.unregister_observer(observer) - self.assertEqual(len(meter.observers), 0) + self.assertEqual(len(meter.instruments), 0) + + def test_unregister_and_reregister_observer(self): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = Mock() + + observer = meter.register_valueobserver( + callback, + "nameCaSEinSENsitive", + "desc", + "unit", + int, + metrics.ValueObserver, + ) + + meter.unregister_observer(observer) + self.assertEqual(len(meter.instruments), 0) + observer = meter.register_valueobserver( + callback, "name", "desc", "unit", int, metrics.ValueObserver + ) class TestMetric(unittest.TestCase): From a2cc1ad51bb37ffe46ded144dd49022beb3f73b5 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 8 Dec 2020 09:23:34 -0500 Subject: [PATCH 0698/1517] Explicitly install dependencies in lint env for tox (#1456) --- .github/workflows/test.yml | 2 +- tox.ini | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a7b9354dc..b54c1841a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: b37945bdeaf49822b240281d493d053995cc2b7b + CONTRIB_REPO_SHA: master jobs: build: diff --git a/tox.ini b/tox.ini index b968225645..447613d039 100644 --- a/tox.ini +++ b/tox.ini @@ -139,7 +139,17 @@ deps = httpretty commands_pre = - python scripts/eachdist.py install --editable --with-test-deps + python -m pip install -e {toxinidir}/opentelemetry-api[test] + python -m pip install -e {toxinidir}/opentelemetry-sdk[test] + python -m pip install -e {toxinidir}/opentelemetry-instrumentation[test] + python -m pip install -e {toxinidir}/opentelemetry-proto[test] + python -m pip install -e {toxinidir}/tests/util[test] + python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-opentracing-shim[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-opencensus[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-prometheus[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin[test] commands = python scripts/eachdist.py lint --check-only From 9ff4321439d4c77642be2ee8b25f92c830f6eed7 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 8 Dec 2020 14:39:06 +0000 Subject: [PATCH 0699/1517] Fix typo (#1450) --- .../src/opentelemetry/exporter/otlp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py index d005232d3e..7265003fee 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py @@ -50,7 +50,7 @@ "service.name": "service" }) - trace.set_tracer_provider(TracerProvider(resource=resource))) + trace.set_tracer_provider(TracerProvider(resource=resource)) tracer = trace.get_tracer(__name__) otlp_exporter = OTLPSpanExporter(endpoint="localhost:55680", insecure=True) From 780a7822c930ea629aa2133a3f0ce34b724c2bfe Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 8 Dec 2020 10:02:10 -0500 Subject: [PATCH 0700/1517] Return none for Getter if key does not exist (#1449) --- opentelemetry-api/CHANGELOG.md | 2 + .../baggage/propagation/__init__.py | 2 +- .../trace/propagation/textmap.py | 14 ++++--- .../trace/propagation/tracecontext.py | 5 ++- .../src/opentelemetry/trace/span.py | 4 +- .../tests/trace/propagation/test_textmap.py | 42 +++++++++++++++++++ 6 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 opentelemetry-api/tests/trace/propagation/test_textmap.py diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 84604f4bb2..af02d068dc 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -4,6 +4,8 @@ - Add `fields` to propagators ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) +- Return `None` for `DictGetter` if key not found + ([#1449](https://github.com/open-telemetry/opentelemetry-python/pull/1449)) ## Version 0.16b0 diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index ad47179c3d..75ba25bbe8 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -101,7 +101,7 @@ def _format_baggage(baggage_entries: typing.Mapping[str, object]) -> str: def _extract_first_element( - items: typing.Iterable[textmap.TextMapPropagatorT], + items: typing.Optional[typing.Iterable[textmap.TextMapPropagatorT]], ) -> typing.Optional[textmap.TextMapPropagatorT]: if items is None: return None diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py b/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py index fb92d37168..0612a740f6 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py @@ -29,7 +29,9 @@ class Getter(typing.Generic[TextMapPropagatorT]): """ - def get(self, carrier: TextMapPropagatorT, key: str) -> typing.List[str]: + def get( + self, carrier: TextMapPropagatorT, key: str + ) -> typing.Optional[typing.List[str]]: """Function that can retrieve zero or more values from the carrier. In the case that the value does not exist, returns an empty list. @@ -38,8 +40,8 @@ def get(self, carrier: TextMapPropagatorT, key: str) -> typing.List[str]: carrier: An object which contains values that are used to construct a Context. key: key of a field in carrier. - Returns: first value of the propagation key or an empty list if the - key doesn't exist. + Returns: first value of the propagation key or None if the key doesn't + exist. """ raise NotImplementedError() @@ -58,8 +60,10 @@ def keys(self, carrier: TextMapPropagatorT) -> typing.List[str]: class DictGetter(Getter[typing.Dict[str, CarrierValT]]): def get( self, carrier: typing.Dict[str, CarrierValT], key: str - ) -> typing.List[str]: - val = carrier.get(key, []) + ) -> typing.Optional[typing.List[str]]: + val = carrier.get(key, None) + if val is None: + return None if isinstance(val, typing.Iterable) and not isinstance(val, str): return list(val) return [val] diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index adcdb5e125..146ba47772 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -91,7 +91,10 @@ def extract( return trace.set_span_in_context(trace.INVALID_SPAN, context) tracestate_headers = getter.get(carrier, self._TRACESTATE_HEADER_NAME) - tracestate = _parse_tracestate(tracestate_headers) + if tracestate_headers is None: + tracestate = None + else: + tracestate = _parse_tracestate(tracestate_headers) span_context = trace.SpanContext( trace_id=int(trace_id, 16), diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index d41a0c3fbd..6506f6f949 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -175,8 +175,8 @@ def __new__( trace_id: int, span_id: int, is_remote: bool, - trace_flags: "TraceFlags" = DEFAULT_TRACE_OPTIONS, - trace_state: "TraceState" = DEFAULT_TRACE_STATE, + trace_flags: typing.Optional["TraceFlags"] = DEFAULT_TRACE_OPTIONS, + trace_state: typing.Optional["TraceState"] = DEFAULT_TRACE_STATE, ) -> "SpanContext": if trace_flags is None: trace_flags = DEFAULT_TRACE_OPTIONS diff --git a/opentelemetry-api/tests/trace/propagation/test_textmap.py b/opentelemetry-api/tests/trace/propagation/test_textmap.py new file mode 100644 index 0000000000..830f7ac2c1 --- /dev/null +++ b/opentelemetry-api/tests/trace/propagation/test_textmap.py @@ -0,0 +1,42 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry.trace.propagation.textmap import DictGetter + + +class TestDictGetter(unittest.TestCase): + def test_get_none(self): + getter = DictGetter() + carrier = {} + val = getter.get(carrier, "test") + self.assertIsNone(val) + + def test_get_str(self): + getter = DictGetter() + carrier = {"test": "val"} + val = getter.get(carrier, "test") + self.assertEqual(val, ["val"]) + + def test_get_iter(self): + getter = DictGetter() + carrier = {"test": ["val"]} + val = getter.get(carrier, "test") + self.assertEqual(val, ["val"]) + + def test_keys(self): + getter = DictGetter() + keys = getter.keys({"test": "val"}) + self.assertEqual(keys, ["test"]) From b8ff552870c73a67a32f0c45223d43a5ee55f4ed Mon Sep 17 00:00:00 2001 From: Abtin Date: Tue, 8 Dec 2020 10:25:41 -0500 Subject: [PATCH 0701/1517] fix typo (#1452) --- docs/examples/basic_meter/basic_metrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/basic_meter/basic_metrics.py b/docs/examples/basic_meter/basic_metrics.py index 0d57238c2d..7341568205 100644 --- a/docs/examples/basic_meter/basic_metrics.py +++ b/docs/examples/basic_meter/basic_metrics.py @@ -16,9 +16,9 @@ This module serves as an example for a simple application using metrics. It shows: -- How to configure a meter passing a sateful or stateless. +- How to configure a meter passing a stateful or stateless. - How to configure an exporter and how to create a controller. -- How to create some metrics intruments and how to capture data with them. +- How to create some metrics instruments and how to capture data with them. - How to use views to specify aggregation types for each metric instrument. """ import sys From e6c2f44a1a6ea4e93617400e2a0991a397ba1bda Mon Sep 17 00:00:00 2001 From: snyder114 <31932630+snyder114@users.noreply.github.com> Date: Tue, 8 Dec 2020 07:43:16 -0800 Subject: [PATCH 0702/1517] Update README.md (#1428) --- README.md | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 507ebaf649..8d211cd118 100644 --- a/README.md +++ b/README.md @@ -38,23 +38,22 @@ ## OpenTelemetry Python -The Python [OpenTelemetry](https://opentelemetry.io/) implementation. +This page describes the Python [OpenTelemetry](https://opentelemetry.io/) implementation. OpenTelemetry is an observability framework for cloud-native software. ## Getting started -OpenTelemetry's goal is to provide a single set of APIs to capture distributed traces and metrics from your application and send them to an observability platform. This project allows you to do just that for applications written in Python. +The goal of OpenTelemetry is to provide a single set of APIs to capture distributed traces and metrics from your application and send them to an observability platform. This project lets you do just that for applications written in Python. This repository includes multiple installable packages. The `opentelemetry-api` -package includes abstract classes and no-op implementations that comprise the OpenTelemetry API following -[the -specification](https://github.com/open-telemetry/opentelemetry-specification). +package includes abstract classes and no-op implementations that comprise the OpenTelemetry API following the +[OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification). The `opentelemetry-sdk` package is the reference implementation of the API. Libraries that produce telemetry data should only depend on `opentelemetry-api`, and defer the choice of the SDK to the application developer. Applications may depend on `opentelemetry-sdk` or another package that implements the API. -The API and SDK packages are available on PyPI, and can installed via `pip`: +The API and SDK packages are available on the Python Package Index (PyPI). You can install them via `pip` with the following commands: ```sh pip install opentelemetry-api @@ -63,7 +62,7 @@ pip install opentelemetry-sdk The [`instrumentation/`](https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation) -directory includes OpenTelemetry instrumentation packages, which can be installed separately as: +directory includes OpenTelemetry instrumentation packages. You can install the packages separately with the following command: ```sh pip install opentelemetry-instrumentation-{instrumentation} @@ -71,14 +70,14 @@ pip install opentelemetry-instrumentation-{instrumentation} The [`exporter/`](https://github.com/open-telemetry/opentelemetry-python/tree/master/exporter) -directory includes OpenTelemetry exporter packages, which can be installed separately as: +directory includes OpenTelemetry exporter packages. You can install the packages separately with the following command: ```sh pip install opentelemetry-exporter-{exporter} ``` To install the development versions of these packages instead, clone or fork -this repo and do an [editable +this repository and perform an [editable install](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs): ```sh @@ -87,17 +86,20 @@ pip install -e ./opentelemetry-sdk pip install -e ./instrumentation/opentelemetry-instrumentation-{instrumentation} ``` +For additional exporter and instrumentation packages, see the +[`opentelemetry-python-contrib`](https://github.com/open-telemetry/opentelemetry-python-contrib) repository. + ## Documentation -The online documentation is available at https://opentelemetry-python.readthedocs.io/, -if you want to access the documentation for the latest version use +The online documentation is available at https://opentelemetry-python.readthedocs.io/. +To access the latest version of the documentation, see https://opentelemetry-python.readthedocs.io/en/latest/. ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md). +For information about contributing to OpenTelemetry Python, see [CONTRIBUTING.md](CONTRIBUTING.md). -We meet weekly on Thursday at 9AM PT. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates. +We meet weekly on Thursdays at 9AM PST. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates. Meetings take place via [Zoom video conference](https://zoom.us/j/6729396170). @@ -115,14 +117,14 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Reiley Yang](https://github.com/reyang), Microsoft - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google -*Find more about the approver role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver).* +*For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver).* Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-telemetry/teams/python-maintainers)): - [Alex Boten](https://github.com/codeboten), Lightstep - [Leighton Chen](https://github.com/lzchen), Microsoft -*Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).* +*For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).* ### Thanks to all the people who already contributed! @@ -132,5 +134,9 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t ## Project Status -Project [boards](https://github.com/open-telemetry/opentelemetry-python/projects) and [milestones](https://github.com/open-telemetry/opentelemetry-python/milestones) can be found at the respective links. We try to keep these accurate and should be the best place to go for answers on project status. The dates and features described in issues -and milestones are estimates, and subject to change. +For project boards and milestones, see the following links: +- [Project boards](https://github.com/open-telemetry/opentelemetry-python/projects) +- [Milestones](https://github.com/open-telemetry/opentelemetry-python/milestones) + +We try to keep these links accurate, so they're the best place to go for questions about project status. The dates and features described in the issues +and milestones are estimates and subject to change. From 553bdfcb4972a7e67a42baf8f8b3fa5520785cd9 Mon Sep 17 00:00:00 2001 From: LetzNico Date: Tue, 8 Dec 2020 17:03:11 +0100 Subject: [PATCH 0703/1517] Add support for span collection limit via env vars (#1377) --- .../tests/test_jaeger_exporter.py | 8 ++- opentelemetry-sdk/CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 19 ++++--- opentelemetry-sdk/tests/trace/test_trace.py | 50 +++++++++++++++++++ 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py index c49a321688..75ab622c95 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py @@ -41,6 +41,12 @@ def setUp(self): self._test_span = trace._Span("test_span", context=context) self._test_span.start() self._test_span.end() + # pylint: disable=protected-access + Configuration._reset() + + def tearDown(self): + # pylint: disable=protected-access + Configuration._reset() def test_constructor_default(self): """Test the default values assigned by constructor.""" @@ -142,8 +148,6 @@ def test_constructor_by_environment_variables(self): environ_patcher.stop() - Configuration._reset() - def test_nsec_to_usec_round(self): # pylint: disable=protected-access nsec_to_usec_round = jaeger_exporter._nsec_to_usec_round diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 9f94dff63c..5a71274bfd 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -8,6 +8,8 @@ ([#1440](https://github.com/open-telemetry/opentelemetry-python/pull/1440)) - Add `fields` to propagators ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) +- Add support for OTEL_SPAN_{ATTRIBUTE_COUNT_LIMIT,EVENT_COUNT_LIMIT,LINK_COUNT_LIMIT} + ([#1377](https://github.com/open-telemetry/opentelemetry-python/pull/1377)) ## Version 0.16b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 6328414ab3..cd087a7314 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -37,6 +37,7 @@ from opentelemetry import context as context_api from opentelemetry import trace as trace_api +from opentelemetry.configuration import Configuration from opentelemetry.sdk import util from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import sampling @@ -49,9 +50,11 @@ logger = logging.getLogger(__name__) -MAX_NUM_ATTRIBUTES = 1000 -MAX_NUM_EVENTS = 1000 -MAX_NUM_LINKS = 1000 +SPAN_ATTRIBUTE_COUNT_LIMIT = Configuration().get( + "SPAN_ATTRIBUTE_COUNT_LIMIT", 1000 +) +SPAN_EVENT_COUNT_LIMIT = Configuration().get("SPAN_EVENT_COUNT_LIMIT", 1000) +SPAN_LINK_COUNT_LIMIT = Configuration().get("SPAN_LINK_COUNT_LIMIT", 1000) VALID_ATTR_VALUE_TYPES = (bool, str, int, float) @@ -446,7 +449,7 @@ def __init__( self.attributes = self._new_attributes() else: self.attributes = BoundedDict.from_map( - MAX_NUM_ATTRIBUTES, attributes + SPAN_ATTRIBUTE_COUNT_LIMIT, attributes ) self.events = self._new_events() @@ -462,7 +465,7 @@ def __init__( if links is None: self.links = self._new_links() else: - self.links = BoundedList.from_seq(MAX_NUM_LINKS, links) + self.links = BoundedList.from_seq(SPAN_LINK_COUNT_LIMIT, links) self._end_time = None # type: Optional[int] self._start_time = None # type: Optional[int] @@ -483,15 +486,15 @@ def __repr__(self): @staticmethod def _new_attributes(): - return BoundedDict(MAX_NUM_ATTRIBUTES) + return BoundedDict(SPAN_ATTRIBUTE_COUNT_LIMIT) @staticmethod def _new_events(): - return BoundedList(MAX_NUM_EVENTS) + return BoundedList(SPAN_EVENT_COUNT_LIMIT) @staticmethod def _new_links(): - return BoundedList(MAX_NUM_LINKS) + return BoundedList(SPAN_LINK_COUNT_LIMIT) @staticmethod def _format_context(context): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 8bb84d1d7d..6879b6390d 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -16,11 +16,15 @@ import shutil import subprocess import unittest +from importlib import reload from logging import ERROR, WARNING from typing import Optional from unittest import mock +import pytest + from opentelemetry import trace as trace_api +from opentelemetry.configuration import Configuration from opentelemetry.context import Context from opentelemetry.sdk import resources, trace from opentelemetry.sdk.trace import Resource, sampling @@ -1182,3 +1186,49 @@ def test_attributes_to_json(self): + date_str + '", "attributes": {"key2": "value2"}}], "links": [], "resource": {}}', ) + + +class TestSpanLimits(unittest.TestCase): + def setUp(self): + # reset global state of configuration object + # pylint: disable=protected-access + Configuration._reset() + + def tearDown(self): + # reset global state of configuration object + # pylint: disable=protected-access + Configuration._reset() + + @mock.patch.dict( + "os.environ", + { + "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT": "10", + "OTEL_SPAN_EVENT_COUNT_LIMIT": "20", + "OTEL_SPAN_LINK_COUNT_LIMIT": "30", + }, + ) + def test_span_environment_limits(self): + reload(trace) + tracer = new_tracer() + ids_generator = trace_api.RandomIdsGenerator() + some_links = [ + trace_api.Link( + trace_api.SpanContext( + trace_id=ids_generator.generate_trace_id(), + span_id=ids_generator.generate_span_id(), + is_remote=False, + ) + ) + for _ in range(100) + ] + with pytest.raises(ValueError): + with tracer.start_as_current_span("root", links=some_links): + pass + + with tracer.start_as_current_span("root") as root: + for idx in range(100): + root.set_attribute("my_attribute_{}".format(idx), 0) + root.add_event("my_event_{}".format(idx)) + + self.assertEqual(len(root.attributes), 10) + self.assertEqual(len(root.events), 20) From 5eb0533c66356401214ccc6f3cbdc01d7e1961d6 Mon Sep 17 00:00:00 2001 From: Rashmi K A <39820442+Rashmi-K-A@users.noreply.github.com> Date: Tue, 8 Dec 2020 21:44:18 +0530 Subject: [PATCH 0704/1517] adding support for Jaeger propagator (#1219) --- opentelemetry-sdk/CHANGELOG.md | 6 +- .../sdk/trace/propagation/__init__.py | 26 +++ .../trace/propagation/jaeger_propagator.py | 137 +++++++++++++ .../propagation/test_jaeger_propagator.py | 184 ++++++++++++++++++ 4 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/jaeger_propagator.py create mode 100644 opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 5a71274bfd..547da6ead5 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -8,6 +8,8 @@ ([#1440](https://github.com/open-telemetry/opentelemetry-python/pull/1440)) - Add `fields` to propagators ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) +- Added support for Jaeger propagator + ([#1219](https://github.com/open-telemetry/opentelemetry-python/pull/1219)) - Add support for OTEL_SPAN_{ATTRIBUTE_COUNT_LIMIT,EVENT_COUNT_LIMIT,LINK_COUNT_LIMIT} ([#1377](https://github.com/open-telemetry/opentelemetry-python/pull/1377)) @@ -27,8 +29,8 @@ Released 2020-11-25 ([#1373](https://github.com/open-telemetry/opentelemetry-python/pull/1373)) - Rename Meter class to Accumulator in Metrics SDK ([#1372](https://github.com/open-telemetry/opentelemetry-python/pull/1372)) -- Fix `ParentBased` sampler for implicit parent spans. Fix also `trace_state` - erasure for dropped spans or spans sampled by the `TraceIdRatioBased` sampler. +- Fix `ParentBased` sampler for implicit parent spans. Fix also `trace_state` + erasure for dropped spans or spans sampled by the `TraceIdRatioBased` sampler. ([#1394](https://github.com/open-telemetry/opentelemetry-python/pull/1394)) ## Version 0.15b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py index e69de29bb2..a02a07ea2a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import typing + +from opentelemetry.trace.propagation.textmap import TextMapPropagatorT + + +def extract_first_element( + items: typing.Iterable[TextMapPropagatorT], +) -> typing.Optional[TextMapPropagatorT]: + if items is None: + return None + return next(iter(items), None) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/jaeger_propagator.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/jaeger_propagator.py new file mode 100644 index 0000000000..e4d2b1cb2c --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/jaeger_propagator.py @@ -0,0 +1,137 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing +import urllib.parse + +import opentelemetry.trace as trace +from opentelemetry import baggage +from opentelemetry.context import Context, get_current +from opentelemetry.sdk.trace.propagation import extract_first_element +from opentelemetry.trace.propagation.textmap import ( + Getter, + Setter, + TextMapPropagator, + TextMapPropagatorT, +) + + +class JaegerPropagator(TextMapPropagator): + """Propagator for the Jaeger format. + + See: https://www.jaegertracing.io/docs/1.19/client-libraries/#propagation-format + """ + + TRACE_ID_KEY = "uber-trace-id" + BAGGAGE_PREFIX = "uberctx-" + DEBUG_FLAG = 0x02 + + def extract( + self, + getter: Getter[TextMapPropagatorT], + carrier: TextMapPropagatorT, + context: typing.Optional[Context] = None, + ) -> Context: + + if context is None: + context = get_current() + fields = extract_first_element( + getter.get(carrier, self.TRACE_ID_KEY) + ).split(":") + + context = self._extract_baggage(getter, carrier, context) + if len(fields) != 4: + return trace.set_span_in_context(trace.INVALID_SPAN, context) + + trace_id, span_id, _parent_id, flags = fields + if ( + trace_id == trace.INVALID_TRACE_ID + or span_id == trace.INVALID_SPAN_ID + ): + return trace.set_span_in_context(trace.INVALID_SPAN, context) + + span = trace.DefaultSpan( + trace.SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id, 16), + is_remote=True, + trace_flags=trace.TraceFlags( + int(flags, 16) & trace.TraceFlags.SAMPLED + ), + ) + ) + return trace.set_span_in_context(span, context) + + def inject( + self, + set_in_carrier: Setter[TextMapPropagatorT], + carrier: TextMapPropagatorT, + context: typing.Optional[Context] = None, + ) -> None: + span = trace.get_current_span(context=context) + span_context = span.get_span_context() + if span_context == trace.INVALID_SPAN_CONTEXT: + return + + span_parent_id = span.parent.span_id if span.parent else 0 + trace_flags = span_context.trace_flags + if trace_flags.sampled: + trace_flags |= self.DEBUG_FLAG + + # set span identity + set_in_carrier( + carrier, + self.TRACE_ID_KEY, + _format_uber_trace_id( + span_context.trace_id, + span_context.span_id, + span_parent_id, + trace_flags, + ), + ) + + # set span baggage, if any + baggage_entries = baggage.get_all(context=context) + if not baggage_entries: + return + for key, value in baggage_entries.items(): + baggage_key = self.BAGGAGE_PREFIX + key + set_in_carrier( + carrier, baggage_key, urllib.parse.quote(str(value)) + ) + + @property + def fields(self) -> typing.Set[str]: + return {self.TRACE_ID_KEY} + + def _extract_baggage(self, getter, carrier, context): + baggage_keys = [ + key + for key in getter.keys(carrier) + if key.startswith(self.BAGGAGE_PREFIX) + ] + for key in baggage_keys: + value = extract_first_element(getter.get(carrier, key)) + context = baggage.set_baggage( + key.replace(self.BAGGAGE_PREFIX, ""), + urllib.parse.unquote(value).strip(), + context=context, + ) + return context + + +def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags): + return "{:032x}:{:016x}:{:016x}:{:02x}".format( + trace_id, span_id, parent_span_id, flags + ) diff --git a/opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py b/opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py new file mode 100644 index 0000000000..bd80770368 --- /dev/null +++ b/opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py @@ -0,0 +1,184 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import Mock + +import opentelemetry.sdk.trace as trace +import opentelemetry.sdk.trace.propagation.jaeger_propagator as jaeger +import opentelemetry.trace as trace_api +from opentelemetry import baggage +from opentelemetry.trace.propagation.textmap import DictGetter + +FORMAT = jaeger.JaegerPropagator() + + +carrier_getter = DictGetter() + + +def get_context_new_carrier(old_carrier, carrier_baggage=None): + + ctx = FORMAT.extract(carrier_getter, old_carrier) + if carrier_baggage: + for key, value in carrier_baggage.items(): + ctx = baggage.set_baggage(key, value, ctx) + parent_span_context = trace_api.get_current_span(ctx).get_span_context() + + parent = trace._Span("parent", parent_span_context) + child = trace._Span( + "child", + trace_api.SpanContext( + parent_span_context.trace_id, + trace_api.RandomIdsGenerator().generate_span_id(), + is_remote=False, + trace_flags=parent_span_context.trace_flags, + trace_state=parent_span_context.trace_state, + ), + parent=parent.get_span_context(), + ) + + new_carrier = {} + ctx = trace_api.set_span_in_context(child, ctx) + + FORMAT.inject(dict.__setitem__, new_carrier, context=ctx) + + return ctx, new_carrier + + +def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags): + return "{:032x}:{:016x}:{:016x}:{:02x}".format( + trace_id, span_id, parent_span_id, flags + ) + + +class TestJaegerPropagator(unittest.TestCase): + @classmethod + def setUpClass(cls): + ids_generator = trace_api.RandomIdsGenerator() + cls.trace_id = ids_generator.generate_trace_id() + cls.span_id = ids_generator.generate_span_id() + cls.parent_span_id = ids_generator.generate_span_id() + cls.serialized_uber_trace_id = _format_uber_trace_id( + cls.trace_id, cls.span_id, cls.parent_span_id, 11 + ) + + def test_extract_valid_span(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + ctx = FORMAT.extract(carrier_getter, old_carrier) + span_context = trace_api.get_current_span(ctx).get_span_context() + self.assertEqual(span_context.trace_id, self.trace_id) + self.assertEqual(span_context.span_id, self.span_id) + + def test_trace_id(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + _, new_carrier = get_context_new_carrier(old_carrier) + self.assertEqual( + self.serialized_uber_trace_id.split(":")[0], + new_carrier[FORMAT.TRACE_ID_KEY].split(":")[0], + ) + + def test_parent_span_id(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + _, new_carrier = get_context_new_carrier(old_carrier) + span_id = self.serialized_uber_trace_id.split(":")[1] + parent_span_id = new_carrier[FORMAT.TRACE_ID_KEY].split(":")[2] + self.assertEqual(span_id, parent_span_id) + + def test_sampled_flag_set(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + _, new_carrier = get_context_new_carrier(old_carrier) + sample_flag_value = ( + int(new_carrier[FORMAT.TRACE_ID_KEY].split(":")[3]) & 0x01 + ) + self.assertEqual(1, sample_flag_value) + + def test_debug_flag_set(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + _, new_carrier = get_context_new_carrier(old_carrier) + debug_flag_value = ( + int(new_carrier[FORMAT.TRACE_ID_KEY].split(":")[3]) + & FORMAT.DEBUG_FLAG + ) + self.assertEqual(FORMAT.DEBUG_FLAG, debug_flag_value) + + def test_sample_debug_flags_unset(self): + uber_trace_id = _format_uber_trace_id( + self.trace_id, self.span_id, self.parent_span_id, 0 + ) + old_carrier = {FORMAT.TRACE_ID_KEY: uber_trace_id} + _, new_carrier = get_context_new_carrier(old_carrier) + flags = int(new_carrier[FORMAT.TRACE_ID_KEY].split(":")[3]) + sample_flag_value = flags & 0x01 + debug_flag_value = flags & FORMAT.DEBUG_FLAG + self.assertEqual(0, sample_flag_value) + self.assertEqual(0, debug_flag_value) + + def test_baggage(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + input_baggage = {"key1": "value1"} + _, new_carrier = get_context_new_carrier(old_carrier, input_baggage) + ctx = FORMAT.extract(carrier_getter, new_carrier) + self.assertDictEqual(input_baggage, ctx["baggage"]) + + def test_non_string_baggage(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + input_baggage = {"key1": 1, "key2": True} + formatted_baggage = {"key1": "1", "key2": "True"} + _, new_carrier = get_context_new_carrier(old_carrier, input_baggage) + ctx = FORMAT.extract(carrier_getter, new_carrier) + self.assertDictEqual(formatted_baggage, ctx["baggage"]) + + def test_extract_invalid_uber_trace_id(self): + old_carrier = { + "uber-trace-id": "000000000000000000000000deadbeef:00000000deadbef0:00", + "uberctx-key1": "value1", + } + formatted_baggage = {"key1": "value1"} + context = FORMAT.extract(carrier_getter, old_carrier) + span_context = trace_api.get_current_span(context).get_span_context() + self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) + self.assertDictEqual(formatted_baggage, context["baggage"]) + + def test_extract_invalid_trace_id(self): + old_carrier = { + "uber-trace-id": "00000000000000000000000000000000:00000000deadbef0:00:00", + "uberctx-key1": "value1", + } + formatted_baggage = {"key1": "value1"} + context = FORMAT.extract(carrier_getter, old_carrier) + span_context = trace_api.get_current_span(context).get_span_context() + self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) + self.assertDictEqual(formatted_baggage, context["baggage"]) + + def test_extract_invalid_span_id(self): + old_carrier = { + "uber-trace-id": "000000000000000000000000deadbeef:0000000000000000:00:00", + "uberctx-key1": "value1", + } + formatted_baggage = {"key1": "value1"} + context = FORMAT.extract(carrier_getter, old_carrier) + span_context = trace_api.get_current_span(context).get_span_context() + self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) + self.assertDictEqual(formatted_baggage, context["baggage"]) + + def test_fields(self): + tracer = trace.TracerProvider().get_tracer("sdk_tracer_provider") + mock_set_in_carrier = Mock() + with tracer.start_as_current_span("parent"): + with tracer.start_as_current_span("child"): + FORMAT.inject(mock_set_in_carrier, {}) + inject_fields = set() + for call in mock_set_in_carrier.mock_calls: + inject_fields.add(call[1][1]) + self.assertEqual(FORMAT.fields, inject_fields) From 0803a0fdb1327943cea5a7ae0a8c5bf13b972046 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 8 Dec 2020 08:29:23 -0800 Subject: [PATCH 0705/1517] Combine changelogs (#1447) --- .github/workflows/changelog.yml | 34 ++ CHANGELOG.md | 489 ++++++++++++++++++ .../CHANGELOG.md | 63 --- .../CHANGELOG.md | 39 -- .../opentelemetry-exporter-otlp/CHANGELOG.md | 70 --- .../CHANGELOG.md | 23 - .../CHANGELOG.md | 62 --- .../CHANGELOG.md | 30 -- opentelemetry-api/CHANGELOG.md | 214 -------- opentelemetry-instrumentation/CHANGELOG.md | 61 --- opentelemetry-proto/CHANGELOG.md | 30 -- opentelemetry-sdk/CHANGELOG.md | 290 ----------- 12 files changed, 523 insertions(+), 882 deletions(-) create mode 100644 .github/workflows/changelog.yml create mode 100644 CHANGELOG.md delete mode 100644 exporter/opentelemetry-exporter-jaeger/CHANGELOG.md delete mode 100644 exporter/opentelemetry-exporter-opencensus/CHANGELOG.md delete mode 100644 exporter/opentelemetry-exporter-otlp/CHANGELOG.md delete mode 100644 exporter/opentelemetry-exporter-prometheus/CHANGELOG.md delete mode 100644 exporter/opentelemetry-exporter-zipkin/CHANGELOG.md delete mode 100644 instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md delete mode 100644 opentelemetry-api/CHANGELOG.md delete mode 100644 opentelemetry-instrumentation/CHANGELOG.md delete mode 100644 opentelemetry-proto/CHANGELOG.md delete mode 100644 opentelemetry-sdk/CHANGELOG.md diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000000..7e4b1032a7 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,34 @@ +# This action requires that any PR targeting the master branch should touch at +# least one CHANGELOG file. If a CHANGELOG entry is not required, add the "Skip +# Changelog" label to disable this action. + +name: changelog + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + branches: + - master + +jobs: + changelog: + runs-on: ubuntu-latest + if: "!contains(github.event.pull_request.labels.*.name, 'Skip Changelog')" + + steps: + - uses: actions/checkout@v2 + + - name: Check for CHANGELOG changes + run: | + # Only the latest commit of the feature branch is available + # automatically. To diff with the base branch, we need to + # fetch that too (and we only need its latest commit). + git fetch origin ${{ github.base_ref }} --depth=1 + if [[ $(git diff --name-only FETCH_HEAD | grep CHANGELOG) ]] + then + echo "A CHANGELOG was modified. Looks good!" + else + echo "No CHANGELOG was modified." + echo "Please add a CHANGELOG entry, or add the \"Skip Changelog\" label if not required." + false + fi \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..0702323c5d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,489 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.16b1...HEAD) + +### Added +- Add `fields` to propagators + ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) +- Add local/remote samplers to parent based sampler + ([#1440](https://github.com/open-telemetry/opentelemetry-python/pull/1440)) +- Add support for OTEL_SPAN_{ATTRIBUTE_COUNT_LIMIT,EVENT_COUNT_LIMIT,LINK_COUNT_LIMIT} + ([#1377](https://github.com/open-telemetry/opentelemetry-python/pull/1377)) +- Return `None` for `DictGetter` if key not found + ([#1449](https://github.com/open-telemetry/opentelemetry-python/pull/1449)) +- Added support for Jaeger propagator + ([#1219](https://github.com/open-telemetry/opentelemetry-python/pull/1219)) + +## [0.16b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b1) - 2020-11-26 +### Added +- Add meter reference to observers + ([#1425](https://github.com/open-telemetry/opentelemetry-python/pull/1425)) + +## [0.16b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b0) - 2020-11-25 +### Added +- Add optional parameter to `record_exception` method + ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) +- Add pickle support to SpanContext class + ([#1380](https://github.com/open-telemetry/opentelemetry-python/pull/1380)) +- Add instrumentation library name and version to OTLP exported metrics + ([#1418](https://github.com/open-telemetry/opentelemetry-python/pull/1418)) +- Add Gzip compression for exporter + ([#1141](https://github.com/open-telemetry/opentelemetry-python/pull/1141)) +- Support for v2 api protobuf format + ([#1318](https://github.com/open-telemetry/opentelemetry-python/pull/1318)) +- Add IDs Generator as Configurable Property of Auto Instrumentation + ([#1404](https://github.com/open-telemetry/opentelemetry-python/pull/1404)) +- Added support for `OTEL_EXPORTER` to the `opentelemetry-instrument` command + ([#1036](https://github.com/open-telemetry/opentelemetry-python/pull/1036)) +### Changed +- Change temporality for Counter and UpDownCounter + ([#1384](https://github.com/open-telemetry/opentelemetry-python/pull/1384)) +- OTLP exporter: Handle error case when no credentials supplied + ([#1366](https://github.com/open-telemetry/opentelemetry-python/pull/1366)) +- Update protobuf versions + ([#1356](https://github.com/open-telemetry/opentelemetry-python/pull/1356)) +- Add missing references to instrumented packages + ([#1416](https://github.com/open-telemetry/opentelemetry-python/pull/1416)) +- Instrumentation Package depends on the OTel SDK + ([#1405](https://github.com/open-telemetry/opentelemetry-python/pull/1405)) +- Allow samplers to modify tracestate + ([#1319](https://github.com/open-telemetry/opentelemetry-python/pull/1319)) +- Update exception handling optional parameters, add escaped attribute to record_exception + ([#1365](https://github.com/open-telemetry/opentelemetry-python/pull/1365)) +- Rename `MetricRecord` to `ExportRecord` + ([#1367](https://github.com/open-telemetry/opentelemetry-python/pull/1367)) +- Rename `Record` to `Accumulation` + ([#1373](https://github.com/open-telemetry/opentelemetry-python/pull/1373)) +- Rename `Meter` to `Accumulator` + ([#1372](https://github.com/open-telemetry/opentelemetry-python/pull/1372)) +- Fix `ParentBased` sampler for implicit parent spans. Fix also `trace_state` + erasure for dropped spans or spans sampled by the `TraceIdRatioBased` sampler. + ([#1394](https://github.com/open-telemetry/opentelemetry-python/pull/1394)) + +## [0.15b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.15b0) -2020-11-02 + +### Added +- Add Env variables in OTLP exporter + ([#1101](https://github.com/open-telemetry/opentelemetry-python/pull/1101)) +- Add support for Jaeger Span Exporter configuration by environment variables and
+ change JaegerSpanExporter constructor parameters + ([#1114](https://github.com/open-telemetry/opentelemetry-python/pull/1114)) + +### Changed +- Updating status codes to adhere to specs + ([#1282](https://github.com/open-telemetry/opentelemetry-python/pull/1282)) +- Set initial checkpoint timestamp in aggregators + ([#1237](https://github.com/open-telemetry/opentelemetry-python/pull/1237)) +- Make `SpanProcessor.on_start` accept parent Context + ([#1251](https://github.com/open-telemetry/opentelemetry-python/pull/1251)) +- Fix b3 propagator entrypoint + ([#1265](https://github.com/open-telemetry/opentelemetry-python/pull/1265)) +- Allow None in sequence attributes values + ([#998](https://github.com/open-telemetry/opentelemetry-python/pull/998)) +- Samplers to accept parent Context + ([#1267](https://github.com/open-telemetry/opentelemetry-python/pull/1267)) +- Span.is_recording() returns false after span has ended + ([#1289](https://github.com/open-telemetry/opentelemetry-python/pull/1289)) +- Allow samplers to modify tracestate + ([#1319](https://github.com/open-telemetry/opentelemetry-python/pull/1319)) +- Remove TracerProvider coupling from Tracer init + ([#1295](https://github.com/open-telemetry/opentelemetry-python/pull/1295)) + +## [0.14b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.14b0) - 2020-10-13 + +### Added +- Add optional parameter to `record_exception` method + ([#1242](https://github.com/open-telemetry/opentelemetry-python/pull/1242)) +- Add support for `OTEL_PROPAGATORS` + ([#1123](https://github.com/open-telemetry/opentelemetry-python/pull/1123)) +- Add keys method to TextMap propagator Getter + ([#1196](https://github.com/open-telemetry/opentelemetry-python/issues/1196)) +- Add timestamps to OTLP exporter + ([#1199](https://github.com/open-telemetry/opentelemetry-python/pull/1199)) +- Add Global Error Handler + ([#1080](https://github.com/open-telemetry/opentelemetry-python/pull/1080)) +- Add support for `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_SCHEDULE_DELAY_MILLIS`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` and `OTEL_BSP_EXPORT_TIMEOUT_MILLIS` environment variables + ([#1105](https://github.com/open-telemetry/opentelemetry-python/pull/1120)) +- Adding Resource to MeterRecord + ([#1209](https://github.com/open-telemetry/opentelemetry-python/pull/1209)) +s +### Changed +- Store `int`s as `int`s in the global Configuration object + ([#1118](https://github.com/open-telemetry/opentelemetry-python/pull/1118)) +- Allow for Custom Trace and Span IDs Generation - `IdsGenerator` for TracerProvider + ([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153)) +- Update baggage propagation header + ([#1194](https://github.com/open-telemetry/opentelemetry-python/pull/1194)) +- Make instances of SpanContext immutable + ([#1134](https://github.com/open-telemetry/opentelemetry-python/pull/1134)) +- Parent is now always passed in via Context, intead of Span or SpanContext + ([#1146](https://github.com/open-telemetry/opentelemetry-python/pull/1146)) +- Update OpenTelemetry protos to v0.5.0 + ([#1143](https://github.com/open-telemetry/opentelemetry-python/pull/1143)) +- Zipkin exporter now accepts a ``max_tag_value_length`` attribute to customize the + maximum allowed size a tag value can have. + ([#1151](https://github.com/open-telemetry/opentelemetry-python/pull/1151)) +- Fixed OTLP events to Zipkin annotations translation. + ([#1161](https://github.com/open-telemetry/opentelemetry-python/pull/1161)) +- Fixed bootstrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask. + ([#1138](https://github.com/open-telemetry/opentelemetry-python/pull/1138)) +- Update sampling result names + ([#1128](https://github.com/open-telemetry/opentelemetry-python/pull/1128)) +- Event attributes are now immutable + ([#1195](https://github.com/open-telemetry/opentelemetry-python/pull/1195)) +- Renaming metrics Batcher to Processor + ([#1203](https://github.com/open-telemetry/opentelemetry-python/pull/1203)) +- Protect access to Span implementation + ([#1188](https://github.com/open-telemetry/opentelemetry-python/pull/1188)) +- `start_as_current_span` and `use_span` can now optionally auto-record any exceptions raised inside the context manager. + ([#1162](https://github.com/open-telemetry/opentelemetry-python/pull/1162)) + +## [0.13b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.13b0) - 2020-09-17 + +### Added +- Add instrumentation info to exported spans + ([#1095](https://github.com/open-telemetry/opentelemetry-python/pull/1095)) +- Add metric OTLP exporter + ([#835](https://github.com/open-telemetry/opentelemetry-python/pull/835)) +- Add type hints to OTLP exporter + ([#1121](https://github.com/open-telemetry/opentelemetry-python/pull/1121)) +- Add support for OTEL_EXPORTER_ZIPKIN_ENDPOINT env var. As part of this change, the + configuration of the ZipkinSpanExporter exposes a `url` argument to replace `host_name`, + `port`, `protocol`, `endpoint`. This brings this implementation inline with other + implementations. + ([#1064](https://github.com/open-telemetry/opentelemetry-python/pull/1064)) +- Zipkin exporter report instrumentation info. + ([#1097](https://github.com/open-telemetry/opentelemetry-python/pull/1097)) +- Add status mapping to tags + ([#1111](https://github.com/open-telemetry/opentelemetry-python/issues/1111)) +- Report instrumentation info + ([#1098](https://github.com/open-telemetry/opentelemetry-python/pull/1098)) +- Add support for http metrics + ([#1116](https://github.com/open-telemetry/opentelemetry-python/pull/1116)) +- Populate resource attributes as per semantic conventions + ([#1053](https://github.com/open-telemetry/opentelemetry-python/pull/1053)) + +### Changed +- Refactor `SpanContext.is_valid` from a method to a data attribute + ([#1005](https://github.com/open-telemetry/opentelemetry-python/pull/1005)) +- Moved samplers from API to SDK + ([#1023](https://github.com/open-telemetry/opentelemetry-python/pull/1023)) +- Change return value type of `correlationcontext.get_correlations` to immutable `MappingProxyType` + ([#1024](https://github.com/open-telemetry/opentelemetry-python/pull/1024)) +- Sampling spec changes + ([#1034](https://github.com/open-telemetry/opentelemetry-python/pull/1034)) +- Remove lazy Event and Link API from Span interface + ([#1045](https://github.com/open-telemetry/opentelemetry-python/pull/1045)) +- Rename CorrelationContext to Baggage + ([#1060](https://github.com/open-telemetry/opentelemetry-python/pull/1060)) +- Rename HTTPTextFormat to TextMapPropagator. This change also updates `get_global_httptextformat` and + `set_global_httptextformat` to `get_global_textmap` and `set_global_textmap` + ([#1085](https://github.com/open-telemetry/opentelemetry-python/pull/1085)) +- Fix api/sdk setup.cfg to include missing python files + ([#1091](https://github.com/open-telemetry/opentelemetry-python/pull/1091)) +- Improve BatchExportSpanProcessor + ([#1062](https://github.com/open-telemetry/opentelemetry-python/pull/1062)) +- Rename Resource labels to attributes + ([#1082](https://github.com/open-telemetry/opentelemetry-python/pull/1082)) +- Rename members of `trace.sampling.Decision` enum + ([#1115](https://github.com/open-telemetry/opentelemetry-python/pull/1115)) +- Merge `OTELResourceDetector` result when creating resources + ([#1096](https://github.com/open-telemetry/opentelemetry-python/pull/1096)) + +### Removed +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + +## [0.12b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.12.0) - 2020-08-14 + +### Added +- Implement Views in metrics SDK + ([#596](https://github.com/open-telemetry/opentelemetry-python/pull/596)) + +### Changed +- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` + ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) +- Stop TracerProvider and MeterProvider from being overridden + ([#959](https://github.com/open-telemetry/opentelemetry-python/pull/959)) +- Update default port to 55680 + ([#977](https://github.com/open-telemetry/opentelemetry-python/pull/977)) +- Add proper length zero padding to hex strings of traceId, spanId, parentId sent on the wire, for compatibility with jaeger-collector + ([#908](https://github.com/open-telemetry/opentelemetry-python/pull/908)) +- Send start_timestamp and convert labels to strings + ([#937](https://github.com/open-telemetry/opentelemetry-python/pull/937)) +- Renamed several packages + ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) +- Thrift URL for Jaeger exporter doesn't allow HTTPS (hardcoded to HTTP) + ([#978](https://github.com/open-telemetry/opentelemetry-python/pull/978)) +- Change reference names to opentelemetry-instrumentation-opentracing-shim + ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) +- Changed default Sampler to `ParentOrElse(AlwaysOn)` + ([#960](https://github.com/open-telemetry/opentelemetry-python/pull/960)) +- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` + ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) +- Update environment variable `OTEL_RESOURCE` to `OTEL_RESOURCE_ATTRIBUTES` as per + the specification + +## [0.11b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.11.0) - 2020-07-28 + +### Added +- Add support for resources and resource detector + ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) +### Changed +- Return INVALID_SPAN if no TracerProvider set for get_current_span + ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) +- Rename record_error to record_exception + ([#927](https://github.com/open-telemetry/opentelemetry-python/pull/927)) +- Update span exporter to use OpenTelemetry Proto v0.4.0 + ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/889)) + +## [0.10b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.10.0) - 2020-06-23 + +### Changed +- Regenerate proto code and add pyi stubs + ([#823](https://github.com/open-telemetry/opentelemetry-python/pull/823)) +- Rename CounterAggregator -> SumAggregator + ([#816](https://github.com/open-telemetry/opentelemetry-python/pull/816)) + +## [0.9b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.9.0) - 2020-06-10 + +### Added +- Adding trace.get_current_span, Removing Tracer.get_current_span + ([#552](https://github.com/open-telemetry/opentelemetry-python/pull/552)) +- Add SumObserver, UpDownSumObserver and LastValueAggregator in metrics + ([#789](https://github.com/open-telemetry/opentelemetry-python/pull/789)) +- Add start_pipeline to MeterProvider + ([#791](https://github.com/open-telemetry/opentelemetry-python/pull/791)) +- Initial release of opentelemetry-ext-otlp, opentelemetry-proto +### Changed +- Move stateful & resource from Meter to MeterProvider + ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) +- Rename Measure to ValueRecorder in metrics + ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) +- Rename Observer to ValueObserver + ([#764](https://github.com/open-telemetry/opentelemetry-python/pull/764)) +- Log a warning when replacing the global Tracer/Meter provider + ([#856](https://github.com/open-telemetry/opentelemetry-python/pull/856)) +- bugfix: byte type attributes are decoded before adding to attributes dict + ([#775](https://github.com/open-telemetry/opentelemetry-python/pull/775)) +- Rename opentelemetry-auto-instrumentation to opentelemetry-instrumentation, + and console script `opentelemetry-auto-instrumentation` to `opentelemetry-instrument` + +## [0.8b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.8.0) - 2020-05-27 + +### Added +- Add a new bootstrap command that enables automatic instrument installations. + ([#650](https://github.com/open-telemetry/opentelemetry-python/pull/650)) + +### Changed +- Handle boolean, integer and float values in Configuration + ([#662](https://github.com/open-telemetry/opentelemetry-python/pull/662)) +- bugfix: ensure status is always string + ([#640](https://github.com/open-telemetry/opentelemetry-python/pull/640)) +- Transform resource to tags when exporting + ([#707](https://github.com/open-telemetry/opentelemetry-python/pull/707)) +- Rename otcollector to opencensus + ([#695](https://github.com/open-telemetry/opentelemetry-python/pull/695)) +- Transform resource to tags when exporting + ([#645](https://github.com/open-telemetry/opentelemetry-python/pull/645)) +- `ext/boto`: Could not serialize attribute aws.region to tag when exporting via jaeger + Serialize tuple type values by coercing them into a string, since Jaeger does not + support tuple types. + ([#865](https://github.com/open-telemetry/opentelemetry-python/pull/865)) +- Validate span attribute types in SDK + ([#678](https://github.com/open-telemetry/opentelemetry-python/pull/678)) +- Specify to_json indent from arguments + ([#718](https://github.com/open-telemetry/opentelemetry-python/pull/718)) +- Span.resource will now default to an empty resource + ([#724](https://github.com/open-telemetry/opentelemetry-python/pull/724)) +- bugfix: Fix error message + ([#729](https://github.com/open-telemetry/opentelemetry-python/pull/729)) +- deep copy empty attributes + ([#714](https://github.com/open-telemetry/opentelemetry-python/pull/714)) + +## [0.7b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.7.1) - 2020-05-12 + +### Added +- Add reset for the global configuration object, for testing purposes + ([#636](https://github.com/open-telemetry/opentelemetry-python/pull/636)) +- Add support for programmatic instrumentation + ([#579](https://github.com/open-telemetry/opentelemetry-python/pull/569)) + +### Changed +- tracer.get_tracer now optionally accepts a TracerProvider + ([#602](https://github.com/open-telemetry/opentelemetry-python/pull/602)) +- Configuration object can now be used by any component of opentelemetry, + including 3rd party instrumentations + ([#563](https://github.com/open-telemetry/opentelemetry-python/pull/563)) +- bugfix: configuration object now matches fields in a case-sensitive manner + ([#583](https://github.com/open-telemetry/opentelemetry-python/pull/583)) +- bugfix: configuration object now accepts all valid python variable names + ([#583](https://github.com/open-telemetry/opentelemetry-python/pull/583)) +- bugfix: configuration undefined attributes now return None instead of raising + an AttributeError. + ([#583](https://github.com/open-telemetry/opentelemetry-python/pull/583)) +- bugfix: 'debug' field is now correct + ([#549](https://github.com/open-telemetry/opentelemetry-python/pull/549)) +- bugfix: enable auto-instrumentation command to work for custom entry points + (e.g. flask_run) + ([#567](https://github.com/open-telemetry/opentelemetry-python/pull/567)) +- Exporter API: span parents are now always spancontext + ([#548](https://github.com/open-telemetry/opentelemetry-python/pull/548)) +- Console span exporter now prints prettier, more legible messages + ([#505](https://github.com/open-telemetry/opentelemetry-python/pull/505)) +- bugfix: B3 propagation now retrieves parentSpanId correctly + ([#621](https://github.com/open-telemetry/opentelemetry-python/pull/621)) +- bugfix: a DefaultSpan now longer causes an exception when used with tracer + ([#577](https://github.com/open-telemetry/opentelemetry-python/pull/577)) +- move last_updated_timestamp into aggregators instead of bound metric + instrument + ([#522](https://github.com/open-telemetry/opentelemetry-python/pull/522)) +- bugfix: suppressing instrumentation in metrics to eliminate an infinite loop + of telemetry + ([#529](https://github.com/open-telemetry/opentelemetry-python/pull/529)) +- bugfix: freezing span attribute sequences, reducing potential user errors + ([#529](https://github.com/open-telemetry/opentelemetry-python/pull/529)) + +## [0.6b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.6.0) - 2020-03-30 + +### Added +- Add support for lazy events and links + ([#474](https://github.com/open-telemetry/opentelemetry-python/pull/474)) +- Adding is_remote flag to SpanContext, indicating when a span is remote + ([#516](https://github.com/open-telemetry/opentelemetry-python/pull/516)) +- Adding a solution to release metric handles and observers + ([#435](https://github.com/open-telemetry/opentelemetry-python/pull/435)) +- Initial release: opentelemetry-instrumentation + +### Changed +- Metrics API no longer uses LabelSet + ([#527](https://github.com/open-telemetry/opentelemetry-python/pull/527)) +- Allow digit as first char in vendor specific trace state key + ([#511](https://github.com/open-telemetry/opentelemetry-python/pull/511)) +- Exporting to collector now works + ([#508](https://github.com/open-telemetry/opentelemetry-python/pull/508)) + +## [0.5b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.5.0) - 2020-03-16 + +### Added +- Adding Correlation Context API/SDK and propagator + ([#471](https://github.com/open-telemetry/opentelemetry-python/pull/471)) +- Adding a global configuration module to simplify setting and getting globals + ([#466](https://github.com/open-telemetry/opentelemetry-python/pull/466)) +- Adding named meters, removing batchers + ([#431](https://github.com/open-telemetry/opentelemetry-python/pull/431)) +- Adding attach/detach methods as per spec + ([#429](https://github.com/open-telemetry/opentelemetry-python/pull/429)) +- Adding OT Collector metrics exporter + ([#454](https://github.com/open-telemetry/opentelemetry-python/pull/454)) +- Initial release opentelemetry-ext-otcollector + +### Changed +- Rename metric handle to bound metric instrument + ([#470](https://github.com/open-telemetry/opentelemetry-python/pull/470)) +- Moving resources to sdk + ([#464](https://github.com/open-telemetry/opentelemetry-python/pull/464)) +- Implementing propagators to API to use context + ([#446](https://github.com/open-telemetry/opentelemetry-python/pull/446)) +- Renaming TraceOptions to TraceFlags + ([#450](https://github.com/open-telemetry/opentelemetry-python/pull/450)) +- Renaming TracerSource to TracerProvider + ([#441](https://github.com/open-telemetry/opentelemetry-python/pull/441)) +- Improve validation of attributes + ([#460](https://github.com/open-telemetry/opentelemetry-python/pull/460)) +- Re-raise errors caught in opentelemetry.sdk.trace.Tracer.use_span() + ([#469](https://github.com/open-telemetry/opentelemetry-python/pull/469)) +- Implement observer instrument + ([#425](https://github.com/open-telemetry/opentelemetry-python/pull/425)) + +## [0.4a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.4.0) - 2020-02-21 + +### Added +- Added named Tracers + ([#301](https://github.com/open-telemetry/opentelemetry-python/pull/301)) +- Add int and valid sequenced to AttributeValue type + ([#368](https://github.com/open-telemetry/opentelemetry-python/pull/368)) +- Add ABC for Metric + ([#391](https://github.com/open-telemetry/opentelemetry-python/pull/391)) +- Metrics export pipeline, and stdout exporter + ([#341](https://github.com/open-telemetry/opentelemetry-python/pull/341)) +- Adding Context API Implementation + ([#395](https://github.com/open-telemetry/opentelemetry-python/pull/395)) +- Adding trace.get_tracer function + ([#430](https://github.com/open-telemetry/opentelemetry-python/pull/430)) +- Add runtime validation for set_attribute + ([#348](https://github.com/open-telemetry/opentelemetry-python/pull/348)) +- Add support for B3 ParentSpanID + ([#286](https://github.com/open-telemetry/opentelemetry-python/pull/286)) +- Implement MinMaxSumCount aggregator + ([#422](https://github.com/open-telemetry/opentelemetry-python/pull/422)) +- Initial release opentelemetry-ext-zipkin, opentelemetry-ext-prometheus + +### Changed +- Separate Default classes from interface descriptions + ([#311](https://github.com/open-telemetry/opentelemetry-python/pull/311)) +- Export span status + ([#367](https://github.com/open-telemetry/opentelemetry-python/pull/367)) +- Export span kind + ([#387](https://github.com/open-telemetry/opentelemetry-python/pull/387)) +- Set status for ended spans + ([#297](https://github.com/open-telemetry/opentelemetry-python/pull/297) and + [#358](https://github.com/open-telemetry/opentelemetry-python/pull/358)) +- Use module loggers + ([#351](https://github.com/open-telemetry/opentelemetry-python/pull/351)) +- Protect start_time and end_time from being set manually by the user + ([#363](https://github.com/open-telemetry/opentelemetry-python/pull/363)) +- Set status in start_as_current_span + ([#377](https://github.com/open-telemetry/opentelemetry-python/pull/377)) +- Implement force_flush for span processors + ([#389](https://github.com/open-telemetry/opentelemetry-python/pull/389)) +- Set sampled flag on sampling trace + ([#407](https://github.com/open-telemetry/opentelemetry-python/pull/407)) +- Add io and formatter options to console exporter + ([#412](https://github.com/open-telemetry/opentelemetry-python/pull/412)) +- Clean up ProbabilitySample for 64 bit trace IDs + ([#238](https://github.com/open-telemetry/opentelemetry-python/pull/238)) + +### Removed +- Remove monotonic and absolute metric instruments + ([#410](https://github.com/open-telemetry/opentelemetry-python/pull/410)) + +## [0.3a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.3.0) - 2019-12-11 + +### Added +- Add metrics exporters + ([#192](https://github.com/open-telemetry/opentelemetry-python/pull/192)) +- Implement extract and inject support for HTTP_HEADERS and TEXT_MAP formats + ([#256](https://github.com/open-telemetry/opentelemetry-python/pull/256)) + +### Changed +- Multiple tracing API/SDK changes +- Multiple metrics API/SDK changes + +### Removed +- Remove option to create unstarted spans from API + ([#290](https://github.com/open-telemetry/opentelemetry-python/pull/290)) + +## [0.2a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.2.0) - 2019-10-29 + +### Added +- W3C TraceContext fixes and compliance tests + ([#228](https://github.com/open-telemetry/opentelemetry-python/pull/228)) +- Sampler API/SDK + ([#225](https://github.com/open-telemetry/opentelemetry-python/pull/225)) +- Initial release: opentelemetry-ext-jaeger, opentelemetry-opentracing-shim + +### Changed +- Multiple metrics API/SDK changes +- Multiple tracing API/SDK changes +- Multiple context API changes +- Multiple bugfixes and improvements + +## [0.1a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.1.0) - 2019-09-30 + +### Added +- Initial release api/sdk diff --git a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md b/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md deleted file mode 100644 index 32454b4bca..0000000000 --- a/exporter/opentelemetry-exporter-jaeger/CHANGELOG.md +++ /dev/null @@ -1,63 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.15b0 - -Released 2020-11-02 -- Add support for Jaeger Span Exporter configuration by environment variables and
- change JaegerSpanExporter constructor parameters - ([#1114](https://github.com/open-telemetry/opentelemetry-python/pull/1114)) - -## Version 0.13b0 - -Released 2020-09-17 -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) -- Report instrumentation info - ([#1098](https://github.com/open-telemetry/opentelemetry-python/pull/1098)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-exporter-jaeger - ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) - -- Thrift URL for Jaeger exporter doesn't allow HTTPS (hardcoded to HTTP) - ([#978] (https://github.com/open-telemetry/opentelemetry-python/pull/978)) - -## 0.8b0 - -Released 2020-05-27 - -- Transform resource to tags when exporting - ([#645](https://github.com/open-telemetry/opentelemetry-python/pull/645)) -- ext/boto: Could not serialize attribute aws.region to tag when exporting via jaeger - Serialize tuple type values by coercing them into a string, since Jaeger does not - support tuple types. - ([#865](https://github.com/open-telemetry/opentelemetry-python/pull/865)) - -## 0.6b0 - -Released 2020-03-30 - -- Exporting to collector now works - ([#508](https://github.com/open-telemetry/opentelemetry-python/pull/508)) - -## 0.4a0 - -Released 2020-02-21 - -- Export span status ([#367](https://github.com/open-telemetry/opentelemetry-python/pull/367)) -- Export span kind ([#387](https://github.com/open-telemetry/opentelemetry-python/pull/387)) - -## 0.3a0 - -Released 2019-12-11 - -## 0.2a0 - -Released 2019-10-29 - -- Initial release diff --git a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md b/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md deleted file mode 100644 index 33ccae1d45..0000000000 --- a/exporter/opentelemetry-exporter-opencensus/CHANGELOG.md +++ /dev/null @@ -1,39 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.16b0 - -Released 2020-11-25 - -- Update protobuf versions - ([#1356](https://github.com/open-telemetry/opentelemetry-python/pull/1356)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4' - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-exporter-opencensus - ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) -- Send start_timestamp and convert labels to strings - ([#937](https://github.com/open-telemetry/opentelemetry-python/pull/937)) - -## 0.8b0 - -Released 2020-05-27 - -- Rename otcollector to opencensus - ([#695](https://github.com/open-telemetry/opentelemetry-python/pull/695)) - -## 0.5b0 - -Released 2020-03-16 - -- Initial release. diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md deleted file mode 100644 index e3968d6825..0000000000 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ /dev/null @@ -1,70 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.16b1 - -Released 2020-11-26 - -- Add meter reference to observers - ([#1425](https://github.com/open-telemetry/opentelemetry-python/pull/1425)) - -## Version 0.16b0 - -Released 2020-11-25 - -- Add instrumentation library name and version to OTLP exported metrics - ([#1418](https://github.com/open-telemetry/opentelemetry-python/pull/1418)) -- Change temporality for Counter and UpDownCounter - ([#1384](https://github.com/open-telemetry/opentelemetry-python/pull/1384)) -- Add Gzip compression for exporter - ([#1141](https://github.com/open-telemetry/opentelemetry-python/pull/1141)) -- OTLP exporter: Handle error case when no credentials supplied - ([#1366](https://github.com/open-telemetry/opentelemetry-python/pull/1366)) -## Version 0.15b0 - -Released 2020-11-02 - -- Add Env variables in OTLP exporter - ([#1101](https://github.com/open-telemetry/opentelemetry-python/pull/1101)) -- Do not use bound instruments in OTLP exporter - ([#1237](https://github.com/open-telemetry/opentelemetry-python/pull/1237)) - -## Version 0.14b0 - -Released 2020-10-13 - -- Add timestamps to OTLP exporter - ([#1199](https://github.com/open-telemetry/opentelemetry-python/pull/1199)) -- Update OpenTelemetry protos to v0.5.0 - ([#1143](https://github.com/open-telemetry/opentelemetry-python/pull/1143)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Add instrumentation info to exported spans - ([#1095](https://github.com/open-telemetry/opentelemetry-python/pull/1095)) -- Add metric OTLP exporter - ([#835](https://github.com/open-telemetry/opentelemetry-python/pull/835)) -- Add type hints to OTLP exporter - ([#1121](https://github.com/open-telemetry/opentelemetry-python/pull/1121)) - -## Version 0.12b0 - -- Change package name to opentelemetry-exporter-otlp - ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) -- Update default port to 55680 - ([#977](https://github.com/open-telemetry/opentelemetry-python/pull/977)) - -## Version 0.11b0 - -Released 2020-07-28 - -- Update span exporter to use OpenTelemetry Proto v0.4.0 ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/889)) - -## 0.9b0 - -Released 2020-06-10 - -- Initial release diff --git a/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md b/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md deleted file mode 100644 index 618e2c4146..0000000000 --- a/exporter/opentelemetry-exporter-prometheus/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-exporter-prometheus - ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) - -## 0.4a0 - -Released 2020-02-21 - -- Initial release diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md deleted file mode 100644 index bb10a1a629..0000000000 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ /dev/null @@ -1,62 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.16b0 - -Released 2020-11-25 - -- Support for v2 api protobuf format ([#1318](https://github.com/open-telemetry/opentelemetry-python/pull/1318)) - -## Version 0.14b0 - -Released 2020-10-13 - -- Zipkin exporter now accepts a ``max_tag_value_length`` attribute to customize the - maximum allowed size a tag value can have. ([#1151](https://github.com/open-telemetry/opentelemetry-python/pull/1151)) -- Fixed OTLP events to Zipkin annotations translation. ([#1161](https://github.com/open-telemetry/opentelemetry-python/pull/1161)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Add support for OTEL_EXPORTER_ZIPKIN_ENDPOINT env var. As part of this change, the - configuration of the ZipkinSpanExporter exposes a `url` argument to replace `host_name`, - `port`, `protocol`, `endpoint`. This brings this implementation inline with other - implementations. - ([#1064](https://github.com/open-telemetry/opentelemetry-python/pull/1064)) -- Zipkin exporter report instrumentation info. - ([#1097](https://github.com/open-telemetry/opentelemetry-python/pull/1097)) -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) -- Add status mapping to tags - ([#1111](https://github.com/open-telemetry/opentelemetry-python/issues/1111)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change package name to opentelemetry-exporter-zipkin - ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) -- Add proper length zero padding to hex strings of traceId, spanId, parentId sent on the wire, for compatibility with jaeger-collector - ([#908](https://github.com/open-telemetry/opentelemetry-python/pull/908)) - -## 0.8b0 - -Released 2020-05-27 - -- Transform resource to tags when exporting - ([#707](https://github.com/open-telemetry/opentelemetry-python/pull/707)) - -## 0.7b1 - -Released 2020-05-12 - -- bugfix: 'debug' field is now correct - ([#549](https://github.com/open-telemetry/opentelemetry-python/pull/549)) - -## 0.4a0 - -Released 2020-02-21 - -- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md deleted file mode 100644 index be243d9584..0000000000 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/CHANGELOG.md +++ /dev/null @@ -1,30 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Change reference names to opentelemetry-instrumentation-opentracing-shim - ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - -## 0.3a0 - -Released 2019-12-11 - -- Implement extract and inject support for HTTP_HEADERS and TEXT_MAP formats - ([#256](https://github.com/open-telemetry/opentelemetry-python/pull/256)) - -## 0.2a0 - -Released 2019-10-29 - -- Initial release diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md deleted file mode 100644 index af02d068dc..0000000000 --- a/opentelemetry-api/CHANGELOG.md +++ /dev/null @@ -1,214 +0,0 @@ -# Changelog - -## Unreleased - -- Add `fields` to propagators - ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) -- Return `None` for `DictGetter` if key not found - ([#1449](https://github.com/open-telemetry/opentelemetry-python/pull/1449)) - -## Version 0.16b0 - -Released 2020-11-25 - -- Add optional parameter to `record_exception` method - ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) -- Add pickle support to SpanContext class - ([#1380](https://github.com/open-telemetry/opentelemetry-python/pull/1380)) - -## Version 0.15b0 - -Released 2020-11-02 - -- Updating status codes to adhere to specs - ([#1282](https://github.com/open-telemetry/opentelemetry-python/pull/1282)) - -## Version 0.14b0 - -Released 2020-10-13 -- Add optional parameter to `record_exception` method - ([#1242](https://github.com/open-telemetry/opentelemetry-python/pull/1242)) -- Add support for `OTEL_PROPAGATORS` - ([#1123](https://github.com/open-telemetry/opentelemetry-python/pull/1123)) -- Store `int`s as `int`s in the global Configuration object - ([#1118](https://github.com/open-telemetry/opentelemetry-python/pull/1118)) -- Allow for Custom Trace and Span IDs Generation - `IdsGenerator` for TracerProvider - ([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153)) -- Update baggage propagation header - ([#1194](https://github.com/open-telemetry/opentelemetry-python/pull/1194)) -- Make instances of SpanContext immutable - ([#1134](https://github.com/open-telemetry/opentelemetry-python/pull/1134)) -- Parent is now always passed in via Context, intead of Span or SpanContext - ([#1146](https://github.com/open-telemetry/opentelemetry-python/pull/1146)) -- Add keys method to TextMap propagator Getter - ([#1196](https://github.com/open-telemetry/opentelemetry-python/issues/1196)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Refactor `SpanContext.is_valid` from a method to a data attribute - ([#1005](https://github.com/open-telemetry/opentelemetry-python/pull/1005)) -- Moved samplers from API to SDK - ([#1023](https://github.com/open-telemetry/opentelemetry-python/pull/1023)) -- Change return value type of `correlationcontext.get_correlations` to immutable `MappingProxyType` - ([#1024](https://github.com/open-telemetry/opentelemetry-python/pull/1024)) -- Change is_recording_events to is_recording - ([#1034](https://github.com/open-telemetry/opentelemetry-python/pull/1034)) -- Remove lazy Event and Link API from Span interface - ([#1045](https://github.com/open-telemetry/opentelemetry-python/pull/1045)) -- Rename CorrelationContext to Baggage - ([#1060](https://github.com/open-telemetry/opentelemetry-python/pull/1060)) -- Rename HTTPTextFormat to TextMapPropagator. This change also updates `get_global_httptextformat` and - `set_global_httptextformat` to `get_global_textmap` and `set_global_textmap` - ([#1085](https://github.com/open-telemetry/opentelemetry-python/pull/1085)) -- Fix api/sdk setup.cfg to include missing python files - ([#1091](https://github.com/open-telemetry/opentelemetry-python/pull/1091)) -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` - ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) -- Stop TracerProvider and MeterProvider from being overridden - ([#959](https://github.com/open-telemetry/opentelemetry-python/pull/959)) - -## Version 0.11b0 - -- Return INVALID_SPAN if no TracerProvider set for get_current_span - ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) -- Rename record_error to record_exception - ([#927](https://github.com/open-telemetry/opentelemetry-python/pull/927)) - -## 0.9b0 - -Released 2020-06-10 - -- Move stateful from Meter to MeterProvider - ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) -- Rename Measure to ValueRecorder in metrics - ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) -- Adding trace.get_current_span, Removing Tracer.get_current_span - ([#552](https://github.com/open-telemetry/opentelemetry-python/pull/552)) -- Rename Observer to ValueObserver - ([#764](https://github.com/open-telemetry/opentelemetry-python/pull/764)) -- Add SumObserver and UpDownSumObserver in metrics - ([#789](https://github.com/open-telemetry/opentelemetry-python/pull/789)) -- Log a warning when replacing the global Tracer/Meter provider - ([#856](https://github.com/open-telemetry/opentelemetry-python/pull/856)) - -## 0.8b0 - -Released 2020-05-27 - -- Handle boolean, integer and float values in Configuration - ([#662](https://github.com/open-telemetry/opentelemetry-python/pull/662)) -- bugfix: ensure status is always string - ([#640](https://github.com/open-telemetry/opentelemetry-python/pull/640)) - -## 0.7b1 - -Released 2020-05-12 - -- Add reset for the global configuration object, for testing purposes - ([#636](https://github.com/open-telemetry/opentelemetry-python/pull/636)) -- tracer.get_tracer now optionally accepts a TracerProvider - ([#602](https://github.com/open-telemetry/opentelemetry-python/pull/602)) -- Configuration object can now be used by any component of opentelemetry, - including 3rd party instrumentations - ([#563](https://github.com/open-telemetry/opentelemetry-python/pull/563)) -- bugfix: configuration object now matches fields in a case-sensitive manner - ([#583](https://github.com/open-telemetry/opentelemetry-python/pull/583)) -- bugfix: configuration object now accepts all valid python variable names - ([#583](https://github.com/open-telemetry/opentelemetry-python/pull/583)) -- bugfix: configuration undefined attributes now return None instead of raising - an AttributeError. - ([#583](https://github.com/open-telemetry/opentelemetry-python/pull/583)) - -## 0.6b0 - -Released 2020-03-30 - -- Add support for lazy events and links - ([#474](https://github.com/open-telemetry/opentelemetry-python/pull/474)) -- Metrics API no longer uses LabelSet - ([#527](https://github.com/open-telemetry/opentelemetry-python/pull/527)) -- Adding is_remote flag to SpanContext, indicating when a span is remote - ([#516](https://github.com/open-telemetry/opentelemetry-python/pull/516)) -- Allow digit as first char in vendor specific trace state key - ([#511](https://github.com/open-telemetry/opentelemetry-python/pull/511)) - -## 0.5b0 - -Released 2020-03-16 - -- Adding Correlation Context API and propagator - ([#471](https://github.com/open-telemetry/opentelemetry-python/pull/471)) -- Adding a global configuration module to simplify setting and getting globals - ([#466](https://github.com/open-telemetry/opentelemetry-python/pull/466)) -- Rename metric handle to bound metric instrument - ([#470](https://github.com/open-telemetry/opentelemetry-python/pull/470)) -- Moving resources to sdk - ([#464](https://github.com/open-telemetry/opentelemetry-python/pull/464)) -- Implementing propagators to API to use context - ([#446](https://github.com/open-telemetry/opentelemetry-python/pull/446)) -- Adding named meters, removing batchers - ([#431](https://github.com/open-telemetry/opentelemetry-python/pull/431)) -- Renaming TraceOptions to TraceFlags - ([#450](https://github.com/open-telemetry/opentelemetry-python/pull/450)) -- Renaming TracerSource to TracerProvider - ([#441](https://github.com/open-telemetry/opentelemetry-python/pull/441)) -- Adding attach/detach methods as per spec - ([#429](https://github.com/open-telemetry/opentelemetry-python/pull/450) - -## 0.4a0 - -Released 2020-02-21 - -- Separate Default classes from interface descriptions - ([#311](https://github.com/open-telemetry/opentelemetry-python/pull/311)) -- Added named Tracers - ([#301](https://github.com/open-telemetry/opentelemetry-python/pull/301)) -- Add int and valid sequenced to AttributeValue type - ([#368](https://github.com/open-telemetry/opentelemetry-python/pull/368)) -- Add ABC for Metric - ([#391](https://github.com/open-telemetry/opentelemetry-python/pull/391)) -- Metric classes required for export pipeline - ([#341](https://github.com/open-telemetry/opentelemetry-python/pull/341)) -- Adding Context API Implementation - ([#395](https://github.com/open-telemetry/opentelemetry-python/pull/395)) -- Remove monotonic and absolute metric instruments - ([#410](https://github.com/open-telemetry/opentelemetry-python/pull/410)) -- Adding trace.get_tracer function - ([#430](https://github.com/open-telemetry/opentelemetry-python/pull/430)) - -## 0.3a0 - -Released 2019-12-11 - -- Multiple tracing API changes -- Multiple metrics API changes -- Remove option to create unstarted spans from API - ([#290](https://github.com/open-telemetry/opentelemetry-python/pull/290)) - -## 0.2a0 - -Released 2019-10-29 - -- W3C TraceContext fixes and compliance tests - ([#228](https://github.com/open-telemetry/opentelemetry-python/pull/228)) -- Multiple metrics API changes -- Multiple tracing API changes -- Multiple context API changes -- Sampler API - ([#225](https://github.com/open-telemetry/opentelemetry-python/pull/225)) -- Multiple bugfixes and improvements - -## 0.1a0 - -Released 2019-09-30 - -- Initial release diff --git a/opentelemetry-instrumentation/CHANGELOG.md b/opentelemetry-instrumentation/CHANGELOG.md deleted file mode 100644 index 2a93c09ba5..0000000000 --- a/opentelemetry-instrumentation/CHANGELOG.md +++ /dev/null @@ -1,61 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.16b0 - -Released 2020-11-25 - -- Add IDs Generator as Configurable Property of Auto Instrumentation - ([#1404](https://github.com/open-telemetry/opentelemetry-python/pull/1404)) -- Added support for `OTEL_EXPORTER` to the `opentelemetry-instrument` command ([#1036](https://github.com/open-telemetry/opentelemetry-python/pull/1036)) -- Add missing references to instrumented packages - ([#1416](https://github.com/open-telemetry/opentelemetry-python/pull/1416)) -- Instrumentation Package depends on the OTel SDK - ([#1405](https://github.com/open-telemetry/opentelemetry-python/pull/1405)) - -## Version 0.14b0 - -Released 2020-10-13 - -- Fixed bootstrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask. ([#1138](https://github.com/open-telemetry/opentelemetry-python/pull/1138)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) -- Add support for http metrics - ([#1116](https://github.com/open-telemetry/opentelemetry-python/pull/1116)) - -## 0.9b0 - -Released 2020-06-10 - -- Rename opentelemetry-auto-instrumentation to opentelemetry-instrumentation, - and console script `opentelemetry-auto-instrumentation` to `opentelemetry-instrument` - -## 0.8b0 - -Released 2020-05-27 - -- Add a new bootstrap command that enables automatic instrument installations. - ([#650](https://github.com/open-telemetry/opentelemetry-python/pull/650)) - -## 0.7b1 - -Released 2020-05-12 - -- Add support for programmatic instrumentation - ([#579](https://github.com/open-telemetry/opentelemetry-python/pull/569)) -- bugfix: enable auto-instrumentation command to work for custom entry points - (e.g. flask_run) - ([#567](https://github.com/open-telemetry/opentelemetry-python/pull/567)) - - -## 0.6b0 - -Released 2020-03-30 - -- Initial release. diff --git a/opentelemetry-proto/CHANGELOG.md b/opentelemetry-proto/CHANGELOG.md deleted file mode 100644 index 9906bdee12..0000000000 --- a/opentelemetry-proto/CHANGELOG.md +++ /dev/null @@ -1,30 +0,0 @@ -# Changelog - -## Unreleased - -## Version 0.16b0 - -Released 2020-11-25 - -- Update protobuf versions - ([#1356](https://github.com/open-telemetry/opentelemetry-python/pull/1356)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) - -## Version 0.10b0 - -Released 2020-06-23 - -- Regenerate proto code and add pyi stubs - ([#823](https://github.com/open-telemetry/opentelemetry-python/pull/823)) - -## 0.9b0 - -Released 2020-06-10 - -- Initial release diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md deleted file mode 100644 index 547da6ead5..0000000000 --- a/opentelemetry-sdk/CHANGELOG.md +++ /dev/null @@ -1,290 +0,0 @@ -# Changelog - -## Unreleased - -- Add meter reference to observers - ([#1425](https://github.com/open-telemetry/opentelemetry-python/pull/1425)) -- Add local/remote samplers to parent based sampler - ([#1440](https://github.com/open-telemetry/opentelemetry-python/pull/1440)) -- Add `fields` to propagators - ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) -- Added support for Jaeger propagator - ([#1219](https://github.com/open-telemetry/opentelemetry-python/pull/1219)) -- Add support for OTEL_SPAN_{ATTRIBUTE_COUNT_LIMIT,EVENT_COUNT_LIMIT,LINK_COUNT_LIMIT} - ([#1377](https://github.com/open-telemetry/opentelemetry-python/pull/1377)) - -## Version 0.16b0 - -Released 2020-11-25 - -- Allow samplers to modify tracestate - ([#1319](https://github.com/open-telemetry/opentelemetry-python/pull/1319)) -- Add optional parameter to `record_exception` method - ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) -- Update exception handling optional parameters, add escaped attribute to record_exception - ([#1365](https://github.com/open-telemetry/opentelemetry-python/pull/1365)) -- Rename `MetricRecord` class to `ExportRecord` - ([#1367](https://github.com/open-telemetry/opentelemetry-python/pull/1367)) -- Rename Record in Metrics SDK to Accumulation - ([#1373](https://github.com/open-telemetry/opentelemetry-python/pull/1373)) -- Rename Meter class to Accumulator in Metrics SDK - ([#1372](https://github.com/open-telemetry/opentelemetry-python/pull/1372)) -- Fix `ParentBased` sampler for implicit parent spans. Fix also `trace_state` - erasure for dropped spans or spans sampled by the `TraceIdRatioBased` sampler. - ([#1394](https://github.com/open-telemetry/opentelemetry-python/pull/1394)) - -## Version 0.15b0 - -Released 2020-11-02 - -- Make `SpanProcessor.on_start` accept parent Context - ([#1251](https://github.com/open-telemetry/opentelemetry-python/pull/1251)) -- Fix b3 propagator entrypoint - ([#1265](https://github.com/open-telemetry/opentelemetry-python/pull/1265)) -- Allow None in sequence attributes values - ([#998](https://github.com/open-telemetry/opentelemetry-python/pull/998)) -- Samplers to accept parent Context - ([#1267](https://github.com/open-telemetry/opentelemetry-python/pull/1267)) -- Updating status codes to adhere to spec - ([#1282](https://github.com/open-telemetry/opentelemetry-python/pull/1282)) -- Span.is_recording() returns false after span has ended - ([#1289](https://github.com/open-telemetry/opentelemetry-python/pull/1289)) -- Set initial checkpoint timestamp in aggregators - ([#1237](https://github.com/open-telemetry/opentelemetry-python/pull/1237)) -- Allow samplers to modify tracestate - ([#1319](https://github.com/open-telemetry/opentelemetry-python/pull/1319)) -- Remove TracerProvider coupling from Tracer init - ([#1295](https://github.com/open-telemetry/opentelemetry-python/pull/1295)) - -## Version 0.14b0 - -Released 2020-10-13 -- Add optional parameter to `record_exception` method - ([#1242](https://github.com/open-telemetry/opentelemetry-python/pull/1242)) -- Add timestamps to aggregators - ([#1199](https://github.com/open-telemetry/opentelemetry-python/pull/1199)) -- Add Global Error Handler - ([#1080](https://github.com/open-telemetry/opentelemetry-python/pull/1080)) -- Update sampling result names - ([#1128](https://github.com/open-telemetry/opentelemetry-python/pull/1128)) -- Add support for `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_SCHEDULE_DELAY_MILLIS`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` and `OTEL_BSP_EXPORT_TIMEOUT_MILLIS` environment variables - ([#1105](https://github.com/open-telemetry/opentelemetry-python/pull/1120)) -- Allow for Custom Trace and Span IDs Generation - `IdsGenerator` for TracerProvider - ([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153)) -- Event attributes are now immutable - ([#1195](https://github.com/open-telemetry/opentelemetry-python/pull/1195)) -- Renaming metrics Batcher to Processor - ([#1203](https://github.com/open-telemetry/opentelemetry-python/pull/1203)) -- Protect access to Span implementation - ([#1188](https://github.com/open-telemetry/opentelemetry-python/pull/1188)) -- `start_as_current_span` and `use_span` can now optionally auto-record any exceptions raised inside the context manager. - ([#1162](https://github.com/open-telemetry/opentelemetry-python/pull/1162)) -- Adding Resource to MeterRecord - ([#1209](https://github.com/open-telemetry/opentelemetry-python/pull/1209)) -- Parent is now always passed in via Context, intead of Span or SpanContext - ([#1146](https://github.com/open-telemetry/opentelemetry-python/pull/1146)) -- Add keys method to TextMap propagator Getter - ([#1196](https://github.com/open-telemetry/opentelemetry-python/issues/1196)) - -## Version 0.13b0 - -Released 2020-09-17 - -- Moved samplers from API to SDK - ([#1023](https://github.com/open-telemetry/opentelemetry-python/pull/1023)) -- Sampling spec changes - ([#1034](https://github.com/open-telemetry/opentelemetry-python/pull/1034)) -- Remove lazy Event and Link API from Span interface - ([#1045](https://github.com/open-telemetry/opentelemetry-python/pull/1045)) -- Improve BatchExportSpanProcessor - ([#1062](https://github.com/open-telemetry/opentelemetry-python/pull/1062)) -- Populate resource attributes as per semantic conventions - ([#1053](https://github.com/open-telemetry/opentelemetry-python/pull/1053)) -- Rename Resource labels to attributes - ([#1082](https://github.com/open-telemetry/opentelemetry-python/pull/1082)) -- Fix api/sdk setup.cfg to include missing python files - ([#1091](https://github.com/open-telemetry/opentelemetry-python/pull/1091)) -- Drop support for Python 3.4 - ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) -- Rename members of `trace.sampling.Decision` enum - ([#1115](https://github.com/open-telemetry/opentelemetry-python/pull/1115)) -- Merge `OTELResourceDetector` result when creating resources - ([#1096](https://github.com/open-telemetry/opentelemetry-python/pull/1096)) - -## Version 0.12b0 - -Released 2020-08-14 - -- Changed default Sampler to `ParentOrElse(AlwaysOn)` - ([#960](https://github.com/open-telemetry/opentelemetry-python/pull/960)) -- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` - ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) -- Implement Views in metrics SDK - ([#596](https://github.com/open-telemetry/opentelemetry-python/pull/596)) -- Update environment variable OTEL_RESOURCE to OTEL_RESOURCE_ATTRIBUTES as per - the specification - -## Version 0.11b0 - -- Add support for resources and resource detector - ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) -- Rename record_error to record_exception - ([#927](https://github.com/open-telemetry/opentelemetry-python/pull/927)) - -## Version 0.10b0 - -Released 2020-06-23 - -- Rename CounterAggregator -> SumAggregator - ([#816](https://github.com/open-telemetry/opentelemetry-python/pull/816)) - -## 0.9b0 - -Released 2020-06-10 - -- Move stateful & resource from Meter to MeterProvider - ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) -- Rename Measure to ValueRecorder in metrics - ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) -- Adding trace.get_current_span, Removing Tracer.get_current_span - ([#552](https://github.com/open-telemetry/opentelemetry-python/pull/552)) -- bugfix: byte type attributes are decoded before adding to attributes dict - ([#775](https://github.com/open-telemetry/opentelemetry-python/pull/775)) -- Rename Observer to ValueObserver - ([#764](https://github.com/open-telemetry/opentelemetry-python/pull/764)) -- Add SumObserver, UpDownSumObserver and LastValueAggregator in metrics - ([#789](https://github.com/open-telemetry/opentelemetry-python/pull/789)) -- Add start_pipeline to MeterProvider - ([#791](https://github.com/open-telemetry/opentelemetry-python/pull/791)) - -## 0.8b0 - -Released 2020-05-27 - -- Validate span attribute types in SDK - ([#678](https://github.com/open-telemetry/opentelemetry-python/pull/678)) -- Specify to_json indent from arguments - ([#718](https://github.com/open-telemetry/opentelemetry-python/pull/718)) -- Span.resource will now default to an empty resource - ([#724](https://github.com/open-telemetry/opentelemetry-python/pull/724)) -- bugfix: Fix error message - ([#729](https://github.com/open-telemetry/opentelemetry-python/pull/729)) -- deep copy empty attributes - ([#714](https://github.com/open-telemetry/opentelemetry-python/pull/714)) - -## 0.7b1 - -Released 2020-05-12 - -- Exporter API: span parents are now always spancontext - ([#548](https://github.com/open-telemetry/opentelemetry-python/pull/548)) -- tracer.get_tracer now optionally accepts a TracerProvider - ([#602](https://github.com/open-telemetry/opentelemetry-python/pull/602)) -- Console span exporter now prints prettier, more legible messages - ([#505](https://github.com/open-telemetry/opentelemetry-python/pull/505)) -- bugfix: B3 propagation now retrieves parentSpanId correctly - ([#621](https://github.com/open-telemetry/opentelemetry-python/pull/621)) -- bugfix: a DefaultSpan now longer causes an exception when used with tracer - ([#577](https://github.com/open-telemetry/opentelemetry-python/pull/577)) -- move last_updated_timestamp into aggregators instead of bound metric - instrument - ([#522](https://github.com/open-telemetry/opentelemetry-python/pull/522)) -- bugfix: suppressing instrumentation in metrics to eliminate an infinite loop - of telemetry - ([#529](https://github.com/open-telemetry/opentelemetry-python/pull/529)) -- bugfix: freezing span attribute sequences, reducing potential user errors - ([#529](https://github.com/open-telemetry/opentelemetry-python/pull/529)) - -## 0.6b0 - -Released 2020-03-30 - -- Add support for lazy events and links - ([#474](https://github.com/open-telemetry/opentelemetry-python/pull/474)) -- Metrics API no longer uses LabelSet - ([#527](https://github.com/open-telemetry/opentelemetry-python/pull/527)) -- Adding is_remote flag to SpanContext, indicating when a span is remote - ([#516](https://github.com/open-telemetry/opentelemetry-python/pull/516)) -- Adding a solution to release metric handles and observers - ([#435](https://github.com/open-telemetry/opentelemetry-python/pull/435)) - -## 0.5b0 - -Released 2020-03-16 - -- Adding Correlation Context SDK and propagator - ([#471](https://github.com/open-telemetry/opentelemetry-python/pull/471)) -- Adding OT Collector metrics exporter - ([#454](https://github.com/open-telemetry/opentelemetry-python/pull/454)) -- Improve validation of attributes - ([#460](https://github.com/open-telemetry/opentelemetry-python/pull/460)) -- Moving resources to sdk - ([#464](https://github.com/open-telemetry/opentelemetry-python/pull/464)) -- Re-raise errors caught in opentelemetry.sdk.trace.Tracer.use_span() - ([#469](https://github.com/open-telemetry/opentelemetry-python/pull/469)) -- Implement observer instrument - ([#425](https://github.com/open-telemetry/opentelemetry-python/pull/425)) - -## 0.4a0 - -Released 2020-02-21 - -- Added named Tracers - ([#301](https://github.com/open-telemetry/opentelemetry-python/pull/301)) -- Set status for ended spans - ([#297](https://github.com/open-telemetry/opentelemetry-python/pull/297) and - [#358](https://github.com/open-telemetry/opentelemetry-python/pull/358)) -- Use module loggers - ([#351](https://github.com/open-telemetry/opentelemetry-python/pull/351)) -- Protect start_time and end_time from being set manually by the user - ([#363](https://github.com/open-telemetry/opentelemetry-python/pull/363)) -- Add runtime validation for set_attribute - ([#348](https://github.com/open-telemetry/opentelemetry-python/pull/348)) -- Add support for B3 ParentSpanID - ([#286](https://github.com/open-telemetry/opentelemetry-python/pull/286)) -- Set status in start_as_current_span - ([#377](https://github.com/open-telemetry/opentelemetry-python/pull/377)) -- Implement force_flush for span processors - ([#389](https://github.com/open-telemetry/opentelemetry-python/pull/389)) -- Metrics export pipeline, and stdout exporter - ([#341](https://github.com/open-telemetry/opentelemetry-python/pull/389)) -- Set sampled flag on sampling trace - ([#407](https://github.com/open-telemetry/opentelemetry-python/pull/407)) -- Add io and formatter options to console exporter - ([#412](https://github.com/open-telemetry/opentelemetry-python/pull/412)) -- Clean up ProbabilitySample for 64 bit trace IDs - ([#238](https://github.com/open-telemetry/opentelemetry-python/pull/238)) -- Adding Context API Implementation - ([#395](https://github.com/open-telemetry/opentelemetry-python/pull/395)) -- Remove monotonic and absolute metric instruments - ([#410](https://github.com/open-telemetry/opentelemetry-python/pull/410)) -- Implement MinMaxSumCount aggregator - ([#422](https://github.com/open-telemetry/opentelemetry-python/pull/422)) - -## 0.3a0 - -Released 2019-12-11 - -- Multiple tracing SDK changes -- Multiple metrics SDK changes -- Add metrics exporters - ([#192](https://github.com/open-telemetry/opentelemetry-python/pull/192)) -- Multiple bugfixes and improvements - -## 0.2a0 - -Released 2019-10-29 - -- W3C TraceContext fixes and compliance tests - ([#228](https://github.com/open-telemetry/opentelemetry-python/pull/228)) -- Multiple metrics SDK changes -- Multiple tracing SDK changes -- Sampler SDK - ([#225](https://github.com/open-telemetry/opentelemetry-python/pull/225)) -- Multiple bugfixes and improvements - -## 0.1a0 - -Released 2019-09-30 - -- Initial release From 5450e6decfbc82b9fa1d2b159272efa33b112c97 Mon Sep 17 00:00:00 2001 From: Dilip M <55284676+dmarar@users.noreply.github.com> Date: Tue, 8 Dec 2020 22:10:41 +0530 Subject: [PATCH 0706/1517] Fix to run the Auto instrumentation example in the docs (#1435) * Fix to run the auto-instrumentation example successfully * reverting the changes in ConsoleSpanExporter, setup.cfg Modifying the README.rst accordingly. * changes to include a consolespanexporter and consolemetricsexporter as per review comments. * Change as per review comments to replace **kwargs with service_name * moving the service_name as the first parameter in the ConsoleSpanExporter since some py35-exporter-zipkin test were failing. * changes to add the service_name to the exported span * pass the service name to the exported span fix the unit tests for the above change. * update the setup.cfg to the correct path of ConsoleMetricsExporter * revert the changes to add the service_name to the exported span. Co-authored-by: Leighton Chen --- docs/examples/auto-instrumentation/README.rst | 22 ++++++++++++++----- .../server_instrumented.py | 5 ++--- opentelemetry-sdk/setup.cfg | 3 +++ .../sdk/trace/export/__init__.py | 4 ++++ .../tests/trace/export/test_export.py | 3 ++- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index 36132daf5e..d7e59eedef 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -37,9 +37,8 @@ Manually instrumented server def server_request(): with tracer.start_as_current_span( "server_request", - parent=propagators.extract( - lambda dict_, key: dict_.get(key, []), request.headers - )["current-span"], + context=propagators.extract(DictGetter(), request.headers + ), ): print(request.args.get("param")) return "served" @@ -137,7 +136,12 @@ similar to the following example: "http.flavor": "1.1" }, "events": [], - "links": [] + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "0.16b1" + } } Execute an automatically instrumented server @@ -148,7 +152,7 @@ and run the following command instead: .. code:: sh - $ opentelemetry-instrument python server_uninstrumented.py + $ opentelemetry-instrument -e console_span,console_metrics python server_uninstrumented.py In the console where you previously executed ``client.py``, run the following command again: @@ -192,7 +196,13 @@ similar to the following example: "http.status_code": 200 }, "events": [], - "links": [] + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "0.16b1", + "service.name": "" + } } You can see that both outputs are the same because automatic instrumentation does diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_instrumented.py index c8562828be..6212ec3333 100644 --- a/docs/examples/auto-instrumentation/server_instrumented.py +++ b/docs/examples/auto-instrumentation/server_instrumented.py @@ -21,6 +21,7 @@ ConsoleSpanExporter, SimpleExportSpanProcessor, ) +from opentelemetry.trace.propagation.textmap import DictGetter app = Flask(__name__) @@ -36,9 +37,7 @@ def server_request(): with tracer.start_as_current_span( "server_request", - parent=propagators.extract( - lambda dict_, key: dict_.get(key, []), request.headers - )["current-span"], + context=propagators.extract(DictGetter(), request.headers), kind=trace.SpanKind.SERVER, attributes=collect_request_attributes(request.environ), ): diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 4acb6ca1fc..6da0d4642c 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -53,6 +53,9 @@ opentelemetry_tracer_provider = sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider opentelemetry_propagator = b3 = opentelemetry.sdk.trace.propagation.b3_format:B3Format +opentelemetry_exporter = + console_span = opentelemetry.sdk.trace.export:ConsoleSpanExporter + console_metrics = opentelemetry.sdk.metrics.export:ConsoleMetricsExporter [options.extras_require] test = diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 53f85dcbb2..d8786e6d21 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -13,12 +13,14 @@ # limitations under the License. import collections +import json import logging import os import sys import threading import typing from enum import Enum +from typing import Optional from opentelemetry.configuration import Configuration from opentelemetry.context import Context, attach, detach, set_value @@ -370,12 +372,14 @@ class ConsoleSpanExporter(SpanExporter): def __init__( self, + service_name: Optional[str] = None, out: typing.IO = sys.stdout, formatter: typing.Callable[[Span], str] = lambda span: span.to_json() + os.linesep, ): self.out = out self.formatter = formatter + self.service_name = service_name def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: for span in spans: diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 4ad23dd863..92d4f65d13 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -477,14 +477,15 @@ def test_batch_span_processor_parameters(self): class TestConsoleSpanExporter(unittest.TestCase): def test_export(self): # pylint: disable=no-self-use """Check that the console exporter prints spans.""" - exporter = export.ConsoleSpanExporter() + exporter = export.ConsoleSpanExporter() # Mocking stdout interferes with debugging and test reporting, mock on # the exporter instance instead. span = trace._Span("span name", trace_api.INVALID_SPAN_CONTEXT) with mock.patch.object(exporter, "out") as mock_stdout: exporter.export([span]) mock_stdout.write.assert_called_once_with(span.to_json() + os.linesep) + self.assertEqual(mock_stdout.write.call_count, 1) self.assertEqual(mock_stdout.flush.call_count, 1) From 3bbc54735fd54779c034cfcd49979e963b0ad04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lipt=C3=A1k?= Date: Thu, 10 Dec 2020 23:44:03 -0500 Subject: [PATCH 0707/1517] Add Python 3.9 to build (#1441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gábor Lipták --- .github/workflows/test.yml | 18 ++++++----- CHANGELOG.md | 3 ++ dev-requirements.txt | 2 +- .../error_hander/error_handler_0/setup.cfg | 1 + .../error_hander/error_handler_1/setup.cfg | 1 + .../setup.cfg | 1 + .../opentelemetry-exporter-otlp/setup.cfg | 1 + .../setup.cfg | 1 + .../opentelemetry-exporter-zipkin/setup.cfg | 1 + .../setup.cfg | 1 + opentelemetry-api/setup.cfg | 1 + opentelemetry-instrumentation/setup.cfg | 1 + opentelemetry-proto/setup.cfg | 1 + opentelemetry-sdk/setup.cfg | 1 + tests/util/setup.cfg | 1 + tox.ini | 32 +++++++++---------- 16 files changed, 42 insertions(+), 25 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b54c1841a1..84b2356fe1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,22 +20,23 @@ jobs: py36: 3.6 py37: 3.7 py38: 3.8 + py39: 3.9 pypy3: pypy3 runs-on: ${{ matrix.os }} strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [ py35, py36, py37, py38, pypy3 ] + python-version: [ py35, py36, py37, py38, py39, pypy3 ] package: ["instrumentation", "core", "exporter"] os: [ ubuntu-latest ] include: - - python-version: py38 + - python-version: py39 package: "tracecontext" os: ubuntu-latest - - python-version: py38 + - python-version: py39 package: "mypy" os: ubuntu-latest - - python-version: py38 + - python-version: py39 package: "mypyinstalled" os: ubuntu-latest # py35-instrumentation segfaults on 18.04 so we instead run on 20.04 @@ -85,7 +86,7 @@ jobs: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} path: opentelemetry-python-contrib - - name: Set up Python 3.8 + - name: Set up Python uses: actions/setup-python@v2 with: python-version: 3.8 @@ -106,12 +107,13 @@ jobs: py36: 3.6 py37: 3.7 py38: 3.8 + py39: 3.9 pypy3: pypy3 runs-on: ${{ matrix.os }} strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [ py35, py36, py37, py38, pypy3 ] + python-version: [ py35, py36, py37, py38, py39, pypy3 ] package: ["instrumentation", "exporter"] os: [ ubuntu-latest ] include: @@ -166,10 +168,10 @@ jobs: with: repository: open-telemetry/opentelemetry-python path: opentelemetry-python-core - - name: Set up Python 3.8 + - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install tox run: pip install -U tox - name: Cache tox environment diff --git a/CHANGELOG.md b/CHANGELOG.md index 0702323c5d..73ec8a070c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.16b1...HEAD) +- Add support for Python 3.9 + ([#1441](https://github.com/open-telemetry/opentelemetry-python/pull/1441)) + ### Added - Add `fields` to propagators ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) diff --git a/dev-requirements.txt b/dev-requirements.txt index 489fc33fcb..c60b8640f6 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,7 +2,7 @@ pylint==2.4.4 flake8~=3.7 isort~=4.3 black>=19.3b0,==19.* -mypy==0.770 +mypy==0.790 sphinx~=2.1 sphinx-rtd-theme~=0.4 sphinx-autodoc-typehints~=1.10.2 diff --git a/docs/examples/error_hander/error_handler_0/setup.cfg b/docs/examples/error_hander/error_handler_0/setup.cfg index d0a7b6275e..e3c039c045 100644 --- a/docs/examples/error_hander/error_handler_0/setup.cfg +++ b/docs/examples/error_hander/error_handler_0/setup.cfg @@ -29,6 +29,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >=3.5 diff --git a/docs/examples/error_hander/error_handler_1/setup.cfg b/docs/examples/error_hander/error_handler_1/setup.cfg index 304b32fd5e..749b0d827f 100644 --- a/docs/examples/error_hander/error_handler_1/setup.cfg +++ b/docs/examples/error_hander/error_handler_1/setup.cfg @@ -29,6 +29,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >=3.5 diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 2ff3b2bc2b..c707b86b9f 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >=3.5 diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 20d724e867..cb11e0e041 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >=3.5 diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index 6ab2101f8c..40991ace38 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >=3.5 diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 6fda337014..f8002c583d 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >=3.5 diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg index 87def81089..abe7bc5815 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >=3.5 diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 78c3123222..8bb3e6cc6f 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >=3.5 diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 558e6a54e2..e38e80d1be 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >=3.5 diff --git a/opentelemetry-proto/setup.cfg b/opentelemetry-proto/setup.cfg index 9f8087df66..6b924a7a39 100644 --- a/opentelemetry-proto/setup.cfg +++ b/opentelemetry-proto/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >=3.5 diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 6da0d4642c..94dfbd18bc 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >=3.5 diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index c788346791..a1fdf1a01e 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -30,6 +30,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >=3.5 diff --git a/tox.ini b/tox.ini index 447613d039..4dd1bc6b80 100644 --- a/tox.ini +++ b/tox.ini @@ -5,52 +5,52 @@ envlist = ; Environments are organized by individual package, allowing ; for specifying supported Python versions per package. ; opentelemetry-api - py3{5,6,7,8}-test-core-api + py3{5,6,7,8,9}-test-core-api pypy3-test-core-api ; opentelemetry-proto - py3{5,6,7,8}-test-core-proto + py3{5,6,7,8,9}-test-core-proto pypy3-test-core-proto ; opentelemetry-sdk - py3{5,6,7,8}-test-core-sdk + py3{5,6,7,8,9}-test-core-sdk pypy3-test-core-sdk ; opentelemetry-instrumentation - py3{5,6,7,8}-test-core-instrumentation + py3{5,6,7,8,9}-test-core-instrumentation pypy3-test-core-instrumentation ; docs/getting-started - py3{5,6,7,8}-test-core-getting-started + py3{5,6,7,8,9}-test-core-getting-started pypy3-test-core-getting-started ; opentelemetry-exporter-jaeger - py3{5,6,7,8}-test-exporter-jaeger + py3{5,6,7,8,9}-test-exporter-jaeger pypy3-test-exporter-jaeger ; opentelemetry-exporter-opencensus - py3{5,6,7,8}-test-exporter-opencensus + py3{5,6,7,8,9}-test-exporter-opencensus ; exporter-opencensus intentionally excluded from pypy3 ; opentelemetry-exporter-otlp - py3{5,6,7,8}-test-exporter-otlp + py3{5,6,7,8,9}-test-exporter-otlp ; exporter-otlp intentionally excluded from pypy3 ; opentelemetry-exporter-prometheus - py3{5,6,7,8}-test-exporter-prometheus + py3{5,6,7,8,9}-test-exporter-prometheus pypy3-test-exporter-prometheus ; opentelemetry-exporter-zipkin - py3{5,6,7,8}-test-exporter-zipkin + py3{5,6,7,8,9}-test-exporter-zipkin pypy3-test-exporter-zipkin ; opentelemetry-opentracing-shim - py3{5,6,7,8}-test-core-opentracing-shim + py3{5,6,7,8,9}-test-core-opentracing-shim pypy3-test-core-opentracing-shim lint - py38-tracecontext - py38-{mypy,mypyinstalled} + py39-tracecontext + py39-{mypy,mypyinstalled} docs docker-tests @@ -80,7 +80,7 @@ changedir = commands_pre = ; Install without -e to test the actual installation - py3{5,6,7,8}: python -m pip install -U pip setuptools wheel + py3{5,6,7,8,9}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util @@ -164,8 +164,8 @@ changedir = docs commands = sphinx-build -E -a -W -b html -T . _build/html -[testenv:py38-tracecontext] -basepython: python3.8 +[testenv:py39-tracecontext] +basepython: python3.9 deps = # needed for tracecontext aiohttp~=3.6 From 119812a81bfb45383243362757ffafe0da469c4d Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Fri, 11 Dec 2020 21:51:47 +0530 Subject: [PATCH 0708/1517] Upgrade isort and enable black compat mode (#1467) --- .isort.cfg | 3 ++- .pylintrc | 6 +++++- dev-requirements.txt | 5 +++-- docs/examples/django/pages/views.py | 1 + docs/examples/opentracing/main.py | 3 ++- .../src/opentelemetry/exporter/otlp/exporter.py | 4 ++-- opentelemetry-api/src/opentelemetry/util/__init__.py | 2 +- opentelemetry-api/tests/context/test_contextvars_context.py | 1 + opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py | 6 ++---- opentelemetry-sdk/tests/context/test_asyncio.py | 1 + scripts/eachdist.py | 2 +- 11 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index cbe7a33601..85533188fa 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -3,6 +3,7 @@ include_trailing_comma=True force_grid_wrap=0 use_parentheses=True line_length=79 +profile=black ; 3 stands for Vertical Hanging Indent, e.g. ; from third_party import ( @@ -13,6 +14,6 @@ line_length=79 ; docs: https://github.com/timothycrosley/isort#multi-line-output-modes multi_line_output=3 skip=target -skip_glob=**/gen/*,.venv*/*,venv*/*,**/proto/*,opentelemetry-python-contrib/* +skip_glob=**/gen/*,.venv*/*,venv*/*,**/proto/*,opentelemetry-python-contrib/*,.tox/* known_first_party=opentelemetry,opentelemetry_example_app known_third_party=psutil,pytest,redis,redis_opentracing diff --git a/.pylintrc b/.pylintrc index 58fe77eef1..5daedb0139 100644 --- a/.pylintrc +++ b/.pylintrc @@ -70,7 +70,11 @@ disable=missing-docstring, wrong-import-order, # Leave this up to isort bad-continuation, # Leave this up to black line-too-long, # Leave this up to black - exec-used + exec-used, + super-with-arguments, # temp-pylint-upgrade + isinstance-second-argument-not-valid-type, # temp-pylint-upgrade + raise-missing-from, # temp-pylint-upgrade + unused-argument, # temp-pylint-upgrade # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/dev-requirements.txt b/dev-requirements.txt index c60b8640f6..bd2515b117 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,8 @@ -pylint==2.4.4 +pylint==2.6.0 flake8~=3.7 -isort~=4.3 +isort~=5.6 black>=19.3b0,==19.* +httpretty~=1.0 mypy==0.790 sphinx~=2.1 sphinx-rtd-theme~=0.4 diff --git a/docs/examples/django/pages/views.py b/docs/examples/django/pages/views.py index 9d277e2b7f..4083888e17 100644 --- a/docs/examples/django/pages/views.py +++ b/docs/examples/django/pages/views.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from django.http import HttpResponse + from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( diff --git a/docs/examples/opentracing/main.py b/docs/examples/opentracing/main.py index 0653ca82ec..bff9a7c3da 100755 --- a/docs/examples/opentracing/main.py +++ b/docs/examples/opentracing/main.py @@ -1,11 +1,12 @@ #!/usr/bin/env python +from rediscache import RedisCache + from opentelemetry import trace from opentelemetry.exporter.jaeger import JaegerSpanExporter from opentelemetry.instrumentation import opentracing_shim from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor -from rediscache import RedisCache # Configure the tracer using the default implementation trace.set_tracer_provider(TracerProvider()) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index 93a87b8cf7..915e5f4d3e 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -123,8 +123,8 @@ def _get_resource_data( def _load_credential_from_file(filepath) -> ChannelCredentials: try: - with open(filepath, "rb") as f: - credential = f.read() + with open(filepath, "rb") as creds_file: + credential = creds_file.read() return ssl_channel_credentials(credential) except FileNotFoundError: logger.exception("Failed to read credential file") diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index 58c297269e..c1c5a77f09 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -21,8 +21,8 @@ from opentelemetry.configuration import Configuration if TYPE_CHECKING: - from opentelemetry.trace import TracerProvider from opentelemetry.metrics import MeterProvider + from opentelemetry.trace import TracerProvider Provider = Union["TracerProvider", "MeterProvider"] diff --git a/opentelemetry-api/tests/context/test_contextvars_context.py b/opentelemetry-api/tests/context/test_contextvars_context.py index 7a384f1678..0c2ec1e6c2 100644 --- a/opentelemetry-api/tests/context/test_contextvars_context.py +++ b/opentelemetry-api/tests/context/test_contextvars_context.py @@ -21,6 +21,7 @@ try: import contextvars # pylint: disable=unused-import + from opentelemetry.context.contextvars_context import ( ContextVarsRuntimeContext, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index 09d7283cab..b2d3930bb1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -17,12 +17,10 @@ try: # pylint: disable=ungrouped-imports - from collections.abc import MutableMapping - from collections.abc import Sequence + from collections.abc import MutableMapping, Sequence except ImportError: # pylint: disable=no-name-in-module,ungrouped-imports - from collections import MutableMapping - from collections import Sequence + from collections import MutableMapping, Sequence def ns_to_iso_str(nanoseconds): diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index 452915d86c..c235e71d83 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -25,6 +25,7 @@ try: import contextvars # pylint: disable=unused-import + from opentelemetry.context.contextvars_context import ( ContextVarsRuntimeContext, ) diff --git a/scripts/eachdist.py b/scripts/eachdist.py index cfa1764ab3..4bf1f33370 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -509,7 +509,7 @@ def lint_args(args): ) runsubprocess( args.dry_run, - ("isort", "--recursive", ".") + ("isort", ".") + (("--diff", "--check-only") if args.check_only else ()), cwd=rootdir, check=True, From 6aa09b630de6afdb74a40a2b6ca0fb26a2954e2b Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Fri, 11 Dec 2020 19:05:30 +0000 Subject: [PATCH 0709/1517] Clone the contrib for every docs build requirement (#1464) --- docs-requirements.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index 9a17569205..64fd71c5a9 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -6,13 +6,13 @@ sphinx-autodoc-typehints~=1.10.2 # doesn't work for pkg_resources. ./opentelemetry-api ./opentelemetry-sdk -./opentelemetry-python-contrib/exporter/opentelemetry-exporter-datadog -./opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-grpc +-e "git+https://github.com/open-telemetry/opentelemetry-python-contrib.git#egg=opentelemetry-exporter-datadog&subdirectory=exporter/opentelemetry-exporter-datadog" +-e "git+https://github.com/open-telemetry/opentelemetry-python-contrib.git#egg=opentelemetry-instrumentation-grpc&subdirectory=instrumentation/opentelemetry-instrumentation-grpc" ./opentelemetry-instrumentation -./opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests -./opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -./opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-django -./opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask +-e "git+https://github.com/open-telemetry/opentelemetry-python-contrib.git#egg=opentelemetry-instrumentation-requests&subdirectory=instrumentation/opentelemetry-instrumentation-requests" +-e "git+https://github.com/open-telemetry/opentelemetry-python-contrib.git#egg=opentelemetry-instrumentation-wsgi&subdirectory=instrumentation/opentelemetry-instrumentation-wsgi" +-e "git+https://github.com/open-telemetry/opentelemetry-python-contrib.git#egg=opentelemetry-instrumentation-django&subdirectory=instrumentation/opentelemetry-instrumentation-django" +-e "git+https://github.com/open-telemetry/opentelemetry-python-contrib.git#egg=opentelemetry-instrumentation-flask&subdirectory=instrumentation/opentelemetry-instrumentation-flask" # Required by ext packages asgiref~=3.0 From 496ed8bf875394ae6f3aab734dfa5e45ec7ab94b Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Fri, 11 Dec 2020 20:51:41 +0000 Subject: [PATCH 0710/1517] Add performance testing + test SDK Span Creation (#1443) --- .github/workflows/test.yml | 40 ++++++++++----- CONTRIBUTING.md | 25 +++++++++ .../benchmarks/trace/test_benchmark_trace.py | 51 +++++++++++++++++++ tox.ini | 7 +-- 4 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 opentelemetry-sdk/tests/performance/benchmarks/trace/test_benchmark_trace.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84b2356fe1..856b2f3a84 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: py38: 3.8 py39: 3.9 pypy3: pypy3 + RUN_MATRIX_COMBINATION: ${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails @@ -30,15 +31,6 @@ jobs: package: ["instrumentation", "core", "exporter"] os: [ ubuntu-latest ] include: - - python-version: py39 - package: "tracecontext" - os: ubuntu-latest - - python-version: py39 - package: "mypy" - os: ubuntu-latest - - python-version: py39 - package: "mypyinstalled" - os: ubuntu-latest # py35-instrumentation segfaults on 18.04 so we instead run on 20.04 - python-version: py35 package: instrumentation @@ -67,14 +59,38 @@ jobs: uses: actions/cache@v2 with: path: .tox - key: tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core + key: tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core - name: run tox - run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} + run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json + - name: Find and merge benchmarks + # TODO: Add at least one benchmark to every package type to remove this + if: matrix.package == 'core' + run: >- + jq -s '.[0].benchmarks = ([.[].benchmarks] | add) + | if .[0].benchmarks == null then null else .[0] end' + opentelemetry-*/tests/*${{ matrix.package }}*-benchmark.json > output.json + - name: Report on benchmark results + # TODO: Add at least one benchmark to every package type to remove this + if: matrix.package == 'core' + uses: rhysd/github-action-benchmark@v1 + with: + name: OpenTelemetry Python Benchmarks - Python ${{ env[matrix.python-version ]}} - ${{ matrix.package-group }} + tool: pytest + output-file-path: output.json + github-token: ${{ secrets.GITHUB_TOKEN }} + # Alert with a commit comment on possible performance regression + alert-threshold: 200% + comment-always: true + fail-on-alert: true + # Make a commit on `gh-pages` with benchmarks from previous step + auto-push: ${{ github.ref == 'refs/heads/master' }} + gh-pages-branch: master + benchmark-data-dir-path: benchmarks misc: strategy: fail-fast: false matrix: - tox-environment: [ "docker-tests", "lint", "docs" ] + tox-environment: [ "docker-tests", "lint", "docs", "mypy", "mypyinstalled", "tracecontext" ] name: ${{ matrix.tox-environment }} runs-on: ubuntu-latest steps: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4981eb5547..f04808d16a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,6 +66,31 @@ See [`tox.ini`](https://github.com/open-telemetry/opentelemetry-python/blob/master/tox.ini) for more detail on available tox commands. +### Benchmarks + +Performance progression of benchmarks for packages distributed by OpenTelemetry Python can be viewed as a [graph of throughput vs commit history](https://opentelemetry-python.readthedocs.io/en/latest/benchmarks/index.html). From this page, you can download a JSON file with the performance results. + +Running the `tox` tests also runs the performance tests if any are available. Benchmarking tests are done with `pytest-benchmark` and they output a table with results to the console. + +To write benchmarks, simply use the [pytest benchmark fixture](https://pytest-benchmark.readthedocs.io/en/latest/usage.html#usage) like the following: + +```python +def test_simple_start_span(benchmark): + def benchmark_start_as_current_span(span_name, attribute_num): + span = tracer.start_span( + span_name, + attributes={"count": attribute_num}, + ) + span.end() + + benchmark(benchmark_start_as_current_span, "benchmarkedSpan", 42) +``` + +Make sure the test file is under the `tests/performance/benchmarks/` folder of +the package it is benchmarking and further has a path that corresponds to the +file in the package it is testing. Make sure that the file name begins with +`test_benchmark_`. (e.g. `opentelemetry-sdk/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py`) + ## Pull Requests ### How to Send Pull Requests diff --git a/opentelemetry-sdk/tests/performance/benchmarks/trace/test_benchmark_trace.py b/opentelemetry-sdk/tests/performance/benchmarks/trace/test_benchmark_trace.py new file mode 100644 index 0000000000..a407a341f4 --- /dev/null +++ b/opentelemetry-sdk/tests/performance/benchmarks/trace/test_benchmark_trace.py @@ -0,0 +1,51 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import opentelemetry.sdk.trace as trace +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import sampling + +tracer = trace.TracerProvider( + sampler=sampling.DEFAULT_ON, + resource=Resource( + { + "service.name": "A123456789", + "service.version": "1.34567890", + "service.instance.id": "123ab456-a123-12ab-12ab-12340a1abc12", + } + ), +).get_tracer("sdk_tracer_provider") + + +def test_simple_start_span(benchmark): + def benchmark_start_as_current_span(): + span = tracer.start_span( + "benchmarkedSpan", + attributes={"long.attribute": -10000000001000000000}, + ) + span.add_event("benchmarkEvent") + span.end() + + benchmark(benchmark_start_as_current_span) + + +def test_simple_start_as_current_span(benchmark): + def benchmark_start_as_current_span(): + with tracer.start_as_current_span( + "benchmarkedSpan", + attributes={"long.attribute": -10000000001000000000}, + ) as span: + span.add_event("benchmarkEvent") + + benchmark(benchmark_start_as_current_span) diff --git a/tox.ini b/tox.ini index 4dd1bc6b80..c5044993b8 100644 --- a/tox.ini +++ b/tox.ini @@ -49,8 +49,8 @@ envlist = pypy3-test-core-opentracing-shim lint - py39-tracecontext - py39-{mypy,mypyinstalled} + tracecontext + mypy,mypyinstalled docs docker-tests @@ -58,6 +58,7 @@ envlist = deps = -c dev-requirements.txt test: pytest + test: pytest-benchmark coverage: pytest coverage: pytest-cov mypy,mypyinstalled: mypy @@ -164,7 +165,7 @@ changedir = docs commands = sphinx-build -E -a -W -b html -T . _build/html -[testenv:py39-tracecontext] +[testenv:tracecontext] basepython: python3.9 deps = # needed for tracecontext From 2f1f13f96742284ba3e86f413148e80fe4988686 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Sun, 13 Dec 2020 20:05:16 +0000 Subject: [PATCH 0711/1517] Use gh-pages to save performance benchmarks results (#1469) --- .github/workflows/test.yml | 2 +- CONTRIBUTING.md | 2 +- docs/index.rst | 8 ++++++++ docs/performance/benchmarks.rst | 4 ++++ 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 docs/performance/benchmarks.rst diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 856b2f3a84..4c7dbc0478 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,7 +84,7 @@ jobs: fail-on-alert: true # Make a commit on `gh-pages` with benchmarks from previous step auto-push: ${{ github.ref == 'refs/heads/master' }} - gh-pages-branch: master + gh-pages-branch: gh-pages benchmark-data-dir-path: benchmarks misc: strategy: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f04808d16a..7612cb8516 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,7 +68,7 @@ for more detail on available tox commands. ### Benchmarks -Performance progression of benchmarks for packages distributed by OpenTelemetry Python can be viewed as a [graph of throughput vs commit history](https://opentelemetry-python.readthedocs.io/en/latest/benchmarks/index.html). From this page, you can download a JSON file with the performance results. +Performance progression of benchmarks for packages distributed by OpenTelemetry Python can be viewed as a [graph of throughput vs commit history](https://opentelemetry-python.readthedocs.io/en/latest/performance/benchmarks.html). From the linked page, you can download a JSON file with the performance results. Running the `tox` tests also runs the performance tests if any are available. Benchmarking tests are done with `pytest-benchmark` and they output a table with results to the console. diff --git a/docs/index.rst b/docs/index.rst index e39b969b2c..5a6a3a7715 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -91,6 +91,14 @@ install instrumentation/** +.. toctree:: + :maxdepth: 1 + :caption: OpenTelemetry Python Performance + :name: performance-tests + :glob: + + performance/** + .. toctree:: :maxdepth: 1 :caption: Examples diff --git a/docs/performance/benchmarks.rst b/docs/performance/benchmarks.rst new file mode 100644 index 0000000000..9c406c0323 --- /dev/null +++ b/docs/performance/benchmarks.rst @@ -0,0 +1,4 @@ +Performance Tests - Benchmarks +============================== + +Click `here `_ to view the latest performance benchmarks for packages in this repo. From cd63eb6e52c6f66c9170a2928d2b4dc8918a41de Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Mon, 14 Dec 2020 16:12:51 +0000 Subject: [PATCH 0712/1517] Fix Benchmarks grouping name (#1473) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c7dbc0478..23c78227d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,7 +74,7 @@ jobs: if: matrix.package == 'core' uses: rhysd/github-action-benchmark@v1 with: - name: OpenTelemetry Python Benchmarks - Python ${{ env[matrix.python-version ]}} - ${{ matrix.package-group }} + name: OpenTelemetry Python Benchmarks - Python ${{ env[matrix.python-version ]}} - ${{ matrix.package }} tool: pytest output-file-path: output.json github-token: ${{ secrets.GITHUB_TOKEN }} From 47d6e95057c3a44faaea8562e2875e6d52f3fdab Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Mon, 14 Dec 2020 21:28:12 +0000 Subject: [PATCH 0713/1517] Do not try to comment on PR after benchmarks (#1478) --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 23c78227d6..672e14e1e7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,7 +80,6 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} # Alert with a commit comment on possible performance regression alert-threshold: 200% - comment-always: true fail-on-alert: true # Make a commit on `gh-pages` with benchmarks from previous step auto-push: ${{ github.ref == 'refs/heads/master' }} From 33fa7b99274d1a7d7d7cb52fb273b18cbb5107a0 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Mon, 14 Dec 2020 22:37:44 +0000 Subject: [PATCH 0714/1517] Remove unnecessary contrib pkgs from docs install (#1470) --- docs-requirements.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index 64fd71c5a9..2ce3b654c8 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -6,13 +6,7 @@ sphinx-autodoc-typehints~=1.10.2 # doesn't work for pkg_resources. ./opentelemetry-api ./opentelemetry-sdk --e "git+https://github.com/open-telemetry/opentelemetry-python-contrib.git#egg=opentelemetry-exporter-datadog&subdirectory=exporter/opentelemetry-exporter-datadog" --e "git+https://github.com/open-telemetry/opentelemetry-python-contrib.git#egg=opentelemetry-instrumentation-grpc&subdirectory=instrumentation/opentelemetry-instrumentation-grpc" ./opentelemetry-instrumentation --e "git+https://github.com/open-telemetry/opentelemetry-python-contrib.git#egg=opentelemetry-instrumentation-requests&subdirectory=instrumentation/opentelemetry-instrumentation-requests" --e "git+https://github.com/open-telemetry/opentelemetry-python-contrib.git#egg=opentelemetry-instrumentation-wsgi&subdirectory=instrumentation/opentelemetry-instrumentation-wsgi" --e "git+https://github.com/open-telemetry/opentelemetry-python-contrib.git#egg=opentelemetry-instrumentation-django&subdirectory=instrumentation/opentelemetry-instrumentation-django" --e "git+https://github.com/open-telemetry/opentelemetry-python-contrib.git#egg=opentelemetry-instrumentation-flask&subdirectory=instrumentation/opentelemetry-instrumentation-flask" # Required by ext packages asgiref~=3.0 From 001163739d7cc09a26592b688f9880da02baf208 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 14 Dec 2020 15:03:25 -0800 Subject: [PATCH 0715/1517] Remove SDK dependency from auto-instrumentation (#1420) --- CHANGELOG.md | 4 ++ opentelemetry-instrumentation/setup.cfg | 1 - .../auto_instrumentation/sitecustomize.py | 34 +++++++++--- .../instrumentation/configurator.py | 53 +++++++++++++++++++ opentelemetry-sdk/setup.cfg | 3 ++ .../sdk/configuration/__init__.py | 42 ++++++++------- .../tests/configuration/test_configurator.py | 28 +++++----- tox.ini | 7 +-- 8 files changed, 129 insertions(+), 43 deletions(-) create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py rename opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py => opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py (82%) rename opentelemetry-instrumentation/tests/test_auto_tracing.py => opentelemetry-sdk/tests/configuration/test_configurator.py (83%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73ec8a070c..078f02791f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1449](https://github.com/open-telemetry/opentelemetry-python/pull/1449)) - Added support for Jaeger propagator ([#1219](https://github.com/open-telemetry/opentelemetry-python/pull/1219)) +- Remove dependency on SDK from `opentelemetry-instrumentation` package. The + `opentelemetry-sdk` package now registers an entrypoint `opentelemetry_configurator` + to allow `opentelemetry-instrument` to load the configuration for the SDK + ([#1420](https://github.com/open-telemetry/opentelemetry-python/pull/1420)) ## [0.16b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b1) - 2020-11-26 ### Added diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index e38e80d1be..265cdec80b 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -43,7 +43,6 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api == 0.17.dev0 - opentelemetry-sdk == 0.17.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index c56d86c7f1..2a51d0b803 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -18,14 +18,15 @@ from pkg_resources import iter_entry_points -from opentelemetry.instrumentation.auto_instrumentation.components import ( - initialize_components, -) - logger = getLogger(__file__) -def auto_instrument(): +def _load_distros(): + # will be implemented in a subsequent PR + pass + + +def _load_instrumentors(): for entry_point in iter_entry_points("opentelemetry_instrumentor"): try: entry_point.load()().instrument() # type: ignore @@ -35,10 +36,29 @@ def auto_instrument(): raise exc +def _load_configurators(): + configured = None + for entry_point in iter_entry_points("opentelemetry_configurator"): + if configured is not None: + logger.warning( + "Configuration of %s not loaded, %s already loaded", + entry_point.name, + configured, + ) + continue + try: + entry_point.load()().configure() # type: ignore + configured = entry_point.name + except Exception as exc: # pylint: disable=broad-except + logger.exception("Configuration of %s failed", entry_point.name) + raise exc + + def initialize(): try: - initialize_components() - auto_instrument() + _load_distros() + _load_configurators() + _load_instrumentors() except Exception: # pylint: disable=broad-except logger.exception("Failed to auto initialize opentelemetry") diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py new file mode 100644 index 0000000000..3efa71e89e --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py @@ -0,0 +1,53 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +""" +OpenTelemetry Base Configurator +""" + +from abc import ABC, abstractmethod +from logging import getLogger + +_LOG = getLogger(__name__) + + +class BaseConfigurator(ABC): + """An ABC for configurators + + Configurators are used to configure + SDKs (i.e. TracerProvider, MeterProvider, Processors...) + to reduce the amount of manual configuration required. + """ + + _instance = None + _is_instrumented = False + + def __new__(cls, *args, **kwargs): + + if cls._instance is None: + cls._instance = object.__new__(cls, *args, **kwargs) + + return cls._instance + + @abstractmethod + def _configure(self, **kwargs): + """Configure the SDK""" + + def configure(self, **kwargs): + """Configure the SDK""" + self._configure(**kwargs) + + +__all__ = ["BaseConfigurator"] diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 94dfbd18bc..765d3891ec 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,6 +43,7 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api == 0.17.dev0 + opentelemetry-instrumentation == 0.17.dev0 [options.packages.find] where = src @@ -57,6 +58,8 @@ opentelemetry_propagator = opentelemetry_exporter = console_span = opentelemetry.sdk.trace.export:ConsoleSpanExporter console_metrics = opentelemetry.sdk.metrics.export:ConsoleMetricsExporter +opentelemetry_configurator = + sdk_configurator = opentelemetry.sdk.configuration:Configurator [options.extras_require] test = diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py b/opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py similarity index 82% rename from opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py rename to opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py index 8b4ef4f3e8..477aa5cf62 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/components.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py @@ -19,6 +19,7 @@ from opentelemetry import trace from opentelemetry.configuration import Configuration +from opentelemetry.instrumentation.configurator import BaseConfigurator from opentelemetry.sdk.metrics.export import MetricsExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider @@ -38,15 +39,15 @@ _DEFAULT_IDS_GENERATOR = RANDOM_IDS_GENERATOR -def get_ids_generator() -> str: +def _get_ids_generator() -> str: return Configuration().IDS_GENERATOR or _DEFAULT_IDS_GENERATOR -def get_service_name() -> str: +def _get_service_name() -> str: return Configuration().SERVICE_NAME or "" -def get_exporter_names() -> Sequence[str]: +def _get_exporter_names() -> Sequence[str]: exporter = Configuration().EXPORTER or _DEFAULT_EXPORTER if exporter.lower().strip() == "none": return [] @@ -62,10 +63,10 @@ def get_exporter_names() -> Sequence[str]: return names -def init_tracing( +def _init_tracing( exporters: Sequence[SpanExporter], ids_generator: trace.IdsGenerator ): - service_name = get_service_name() + service_name = _get_service_name() provider = TracerProvider( resource=Resource.create({"service.name": service_name}), ids_generator=ids_generator(), @@ -85,12 +86,12 @@ def init_tracing( ) -def init_metrics(exporters: Sequence[MetricsExporter]): +def _init_metrics(exporters: Sequence[MetricsExporter]): if exporters: logger.warning("automatic metric initialization is not supported yet.") -def import_tracer_provider_config_components( +def _import_tracer_provider_config_components( selected_components, entry_point_name ) -> Sequence[Tuple[str, object]]: component_entry_points = { @@ -112,7 +113,7 @@ def import_tracer_provider_config_components( return component_impls -def import_exporters( +def _import_exporters( exporter_names: Sequence[str], ) -> Tuple[Sequence[SpanExporter], Sequence[MetricsExporter]]: trace_exporters, metric_exporters = {}, {} @@ -120,7 +121,7 @@ def import_exporters( for ( exporter_name, exporter_impl, - ) in import_tracer_provider_config_components( + ) in _import_tracer_provider_config_components( exporter_names, "opentelemetry_exporter" ): if issubclass(exporter_impl, SpanExporter): @@ -136,11 +137,11 @@ def import_exporters( return trace_exporters, metric_exporters -def import_ids_generator(ids_generator_name: str) -> trace.IdsGenerator: +def _import_ids_generator(ids_generator_name: str) -> trace.IdsGenerator: # pylint: disable=unbalanced-tuple-unpacking [ (ids_generator_name, ids_generator_impl) - ] = import_tracer_provider_config_components( + ] = _import_tracer_provider_config_components( [ids_generator_name.strip()], "opentelemetry_ids_generator" ) @@ -150,14 +151,19 @@ def import_ids_generator(ids_generator_name: str) -> trace.IdsGenerator: raise RuntimeError("{0} is not an IdsGenerator".format(ids_generator_name)) -def initialize_components(): - exporter_names = get_exporter_names() - trace_exporters, metric_exporters = import_exporters(exporter_names) - ids_generator_name = get_ids_generator() - ids_generator = import_ids_generator(ids_generator_name) - init_tracing(trace_exporters, ids_generator) +def _initialize_components(): + exporter_names = _get_exporter_names() + trace_exporters, metric_exporters = _import_exporters(exporter_names) + ids_generator_name = _get_ids_generator() + ids_generator = _import_ids_generator(ids_generator_name) + _init_tracing(trace_exporters, ids_generator) # We don't support automatic initialization for metric yet but have added # some boilerplate in order to make sure current implementation does not # lock us out of supporting metrics later without major surgery. - init_metrics(metric_exporters) + _init_metrics(metric_exporters) + + +class Configurator(BaseConfigurator): + def _configure(self, **kwargs): + _initialize_components() diff --git a/opentelemetry-instrumentation/tests/test_auto_tracing.py b/opentelemetry-sdk/tests/configuration/test_configurator.py similarity index 83% rename from opentelemetry-instrumentation/tests/test_auto_tracing.py rename to opentelemetry-sdk/tests/configuration/test_configurator.py index 9ffe421a25..c8851321f1 100644 --- a/opentelemetry-instrumentation/tests/test_auto_tracing.py +++ b/opentelemetry-sdk/tests/configuration/test_configurator.py @@ -18,7 +18,11 @@ from unittest.mock import patch from opentelemetry.configuration import Configuration -from opentelemetry.instrumentation.auto_instrumentation import components +from opentelemetry.sdk.configuration import ( + _get_ids_generator, + _import_ids_generator, + _init_tracing, +) from opentelemetry.sdk.resources import Resource from opentelemetry.trace.ids_generator import RandomIdsGenerator @@ -71,11 +75,10 @@ class TestTraceInit(TestCase): def setUp(self): super() self.get_provider_patcher = patch( - "opentelemetry.instrumentation.auto_instrumentation.components.TracerProvider", - Provider, + "opentelemetry.sdk.configuration.TracerProvider", Provider, ) self.get_processor_patcher = patch( - "opentelemetry.instrumentation.auto_instrumentation.components.BatchExportSpanProcessor", + "opentelemetry.sdk.configuration.BatchExportSpanProcessor", Processor, ) self.set_provider_patcher = patch( @@ -96,7 +99,7 @@ def tearDown(self): def test_trace_init_default(self): environ["OTEL_SERVICE_NAME"] = "my-test-service" Configuration._reset() - components.init_tracing({"zipkin": Exporter}, RandomIdsGenerator) + _init_tracing({"zipkin": Exporter}, RandomIdsGenerator) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] @@ -111,7 +114,7 @@ def test_trace_init_default(self): def test_trace_init_otlp(self): environ["OTEL_SERVICE_NAME"] = "my-otlp-test-service" Configuration._reset() - components.init_tracing({"otlp": OTLPExporter}, RandomIdsGenerator) + _init_tracing({"otlp": OTLPExporter}, RandomIdsGenerator) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] @@ -128,12 +131,9 @@ def test_trace_init_otlp(self): @patch.dict(environ, {"OTEL_IDS_GENERATOR": "custom_ids_generator"}) @patch( - "opentelemetry.instrumentation.auto_instrumentation.components.trace.IdsGenerator", - new=IdsGenerator, - ) - @patch( - "opentelemetry.instrumentation.auto_instrumentation.components.iter_entry_points" + "opentelemetry.sdk.configuration.trace.IdsGenerator", new=IdsGenerator, ) + @patch("opentelemetry.sdk.configuration.iter_entry_points") def test_trace_init_custom_ids_generator(self, mock_iter_entry_points): mock_iter_entry_points.configure_mock( return_value=[ @@ -141,8 +141,8 @@ def test_trace_init_custom_ids_generator(self, mock_iter_entry_points): ] ) Configuration._reset() - ids_generator_name = components.get_ids_generator() - ids_generator = components.import_ids_generator(ids_generator_name) - components.init_tracing({}, ids_generator) + ids_generator_name = _get_ids_generator() + ids_generator = _import_ids_generator(ids_generator_name) + _init_tracing({}, ids_generator) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider.ids_generator, CustomIdsGenerator) diff --git a/tox.ini b/tox.ini index c5044993b8..9a7c8ce3f8 100644 --- a/tox.ini +++ b/tox.ini @@ -84,7 +84,7 @@ commands_pre = py3{5,6,7,8,9}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. - test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util + test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-instrumentation {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util test-core-proto: pip install {toxinidir}/opentelemetry-proto instrumentation: pip install {toxinidir}/opentelemetry-instrumentation @@ -141,8 +141,8 @@ deps = commands_pre = python -m pip install -e {toxinidir}/opentelemetry-api[test] - python -m pip install -e {toxinidir}/opentelemetry-sdk[test] python -m pip install -e {toxinidir}/opentelemetry-instrumentation[test] + python -m pip install -e {toxinidir}/opentelemetry-sdk[test] python -m pip install -e {toxinidir}/opentelemetry-proto[test] python -m pip install -e {toxinidir}/tests/util[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-opentracing-shim[test] @@ -176,8 +176,8 @@ deps = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ - -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/opentelemetry-instrumentation \ + -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests \ -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi @@ -194,6 +194,7 @@ changedir = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ + -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/tests/util \ -e {toxinidir}/exporter/opentelemetry-exporter-opencensus From 9cfe5559ce6c015eea04f66fb9a4ece596f775b3 Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Tue, 15 Dec 2020 20:38:24 -0800 Subject: [PATCH 0716/1517] Adding support for array attributes to Zipkin exporter (#1285) --- CHANGELOG.md | 2 + .../opentelemetry/exporter/zipkin/__init__.py | 47 +++- .../tests/test_zipkin_exporter.py | 205 +++++++++++++++++- 3 files changed, 245 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 078f02791f..dad442d252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `opentelemetry-sdk` package now registers an entrypoint `opentelemetry_configurator` to allow `opentelemetry-instrument` to load the configuration for the SDK ([#1420](https://github.com/open-telemetry/opentelemetry-python/pull/1420)) +- `opentelemetry-exporter-zipkin` Add support for array attributes in Span and Resource exports + ([#1285](https://github.com/open-telemetry/opentelemetry-python/pull/1285)) ## [0.16b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b1) - 2020-11-26 ### Added diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 51130d60b5..c6d3559b90 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -351,10 +351,15 @@ def _extract_tags_from_dict(self, tags_dict): if not tags_dict: return tags for attribute_key, attribute_value in tags_dict.items(): - if isinstance(attribute_value, (int, bool, float)): + if isinstance(attribute_value, (int, bool, float, str)): value = str(attribute_value) - elif isinstance(attribute_value, str): - value = attribute_value + elif isinstance(attribute_value, Sequence): + value = self._extract_tag_value_string_from_sequence( + attribute_value + ) + if not value: + logger.warning("Could not serialize tag %s", attribute_key) + continue else: logger.warning("Could not serialize tag %s", attribute_key) continue @@ -364,6 +369,42 @@ def _extract_tags_from_dict(self, tags_dict): tags[attribute_key] = value return tags + def _extract_tag_value_string_from_sequence(self, sequence: Sequence): + if self.max_tag_value_length == 1: + return None + + tag_value_elements = [] + running_string_length = ( + 2 # accounts for array brackets in output string + ) + defined_max_tag_value_length = self.max_tag_value_length > 0 + + for element in sequence: + if isinstance(element, (int, bool, float, str)): + tag_value_element = str(element) + elif element is None: + tag_value_element = None + else: + continue + + if defined_max_tag_value_length: + if tag_value_element is None: + running_string_length += 4 # null with no quotes + else: + # + 2 accounts for string quotation marks + running_string_length += len(tag_value_element) + 2 + + if tag_value_elements: + # accounts for ',' item separator + running_string_length += 1 + + if running_string_length > self.max_tag_value_length: + break + + tag_value_elements.append(tag_value_element) + + return json.dumps(tag_value_elements, separators=(",", ":")) + def _extract_tags_from_span(self, span: Span): tags = self._extract_tags_from_dict(getattr(span, "attributes", None)) if span.resource: diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index bbc6ccf7ce..a21199659b 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -425,8 +425,25 @@ def test_export_json_max_tag_length(self): span.start() span.resource = Resource({}) # added here to preserve order - span.set_attribute("k1", "v" * 500) - span.set_attribute("k2", "v" * 50) + span.set_attribute("string1", "v" * 500) + span.set_attribute("string2", "v" * 50) + span.set_attribute("list1", ["a"] * 25) + span.set_attribute("list2", ["a"] * 10) + span.set_attribute("list3", [2] * 25) + span.set_attribute("list4", [2] * 10) + span.set_attribute("list5", [True] * 25) + span.set_attribute("list6", [True] * 10) + span.set_attribute("tuple1", ("a",) * 25) + span.set_attribute("tuple2", ("a",) * 10) + span.set_attribute("tuple3", (2,) * 25) + span.set_attribute("tuple4", (2,) * 10) + span.set_attribute("tuple5", (True,) * 25) + span.set_attribute("tuple6", (True,) * 10) + span.set_attribute("range1", range(0, 25)) + span.set_attribute("range2", range(0, 10)) + span.set_attribute("empty_list", []) + span.set_attribute("none_list", ["hello", None, "world"]) + span.set_status(Status(StatusCode.ERROR, "Example description")) span.end() @@ -440,8 +457,66 @@ def test_export_json_max_tag_length(self): _, kwargs = mock_post.call_args # pylint: disable=E0633 tags = json.loads(kwargs["data"])[0]["tags"] - self.assertEqual(len(tags["k1"]), 128) - self.assertEqual(len(tags["k2"]), 50) + + self.assertEqual(len(tags["string1"]), 128) + self.assertEqual(len(tags["string2"]), 50) + self.assertEqual( + tags["list1"], + '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', + ) + self.assertEqual( + tags["list2"], '["a","a","a","a","a","a","a","a","a","a"]', + ) + self.assertEqual( + tags["list3"], + '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', + ) + self.assertEqual( + tags["list4"], '["2","2","2","2","2","2","2","2","2","2"]', + ) + self.assertEqual( + tags["list5"], + '["True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True"]', + ) + self.assertEqual( + tags["list6"], + '["True","True","True","True","True","True","True","True","True","True"]', + ) + self.assertEqual( + tags["tuple1"], + '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', + ) + self.assertEqual( + tags["tuple2"], '["a","a","a","a","a","a","a","a","a","a"]', + ) + self.assertEqual( + tags["tuple3"], + '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', + ) + self.assertEqual( + tags["tuple4"], '["2","2","2","2","2","2","2","2","2","2"]', + ) + self.assertEqual( + tags["tuple5"], + '["True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True"]', + ) + self.assertEqual( + tags["tuple6"], + '["True","True","True","True","True","True","True","True","True","True"]', + ) + self.assertEqual( + tags["range1"], + '["0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24"]', + ) + self.assertEqual( + tags["range2"], '["0","1","2","3","4","5","6","7","8","9"]', + ) + self.assertEqual( + tags["empty_list"], "[]", + ) + self.assertEqual( + tags["none_list"], '["hello",null,"world"]', + ) exporter = ZipkinSpanExporter(service_name, max_tag_value_length=2) mock_post = MagicMock() @@ -452,8 +527,126 @@ def test_export_json_max_tag_length(self): _, kwargs = mock_post.call_args # pylint: disable=E0633 tags = json.loads(kwargs["data"])[0]["tags"] - self.assertEqual(len(tags["k1"]), 2) - self.assertEqual(len(tags["k2"]), 2) + self.assertEqual(len(tags["string1"]), 2) + self.assertEqual(len(tags["string2"]), 2) + self.assertEqual(tags["list1"], "[]") + self.assertEqual(tags["list2"], "[]") + self.assertEqual(tags["list3"], "[]") + self.assertEqual(tags["list4"], "[]") + self.assertEqual(tags["list5"], "[]") + self.assertEqual(tags["list6"], "[]") + self.assertEqual(tags["tuple1"], "[]") + self.assertEqual(tags["tuple2"], "[]") + self.assertEqual(tags["tuple3"], "[]") + self.assertEqual(tags["tuple4"], "[]") + self.assertEqual(tags["tuple5"], "[]") + self.assertEqual(tags["tuple6"], "[]") + self.assertEqual(tags["range1"], "[]") + self.assertEqual(tags["range2"], "[]") + + exporter = ZipkinSpanExporter(service_name, max_tag_value_length=5) + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export([span]) + self.assertEqual(SpanExportResult.SUCCESS, status) + + _, kwargs = mock_post.call_args # pylint: disable=E0633 + tags = json.loads(kwargs["data"])[0]["tags"] + self.assertEqual(len(tags["string1"]), 5) + self.assertEqual(len(tags["string2"]), 5) + self.assertEqual(tags["list1"], '["a"]') + self.assertEqual(tags["list2"], '["a"]') + self.assertEqual(tags["list3"], '["2"]') + self.assertEqual(tags["list4"], '["2"]') + self.assertEqual(tags["list5"], "[]") + self.assertEqual(tags["list6"], "[]") + self.assertEqual(tags["tuple1"], '["a"]') + self.assertEqual(tags["tuple2"], '["a"]') + self.assertEqual(tags["tuple3"], '["2"]') + self.assertEqual(tags["tuple4"], '["2"]') + self.assertEqual(tags["tuple5"], "[]") + self.assertEqual(tags["tuple6"], "[]") + self.assertEqual(tags["range1"], '["0"]') + self.assertEqual(tags["range2"], '["0"]') + + exporter = ZipkinSpanExporter(service_name, max_tag_value_length=9) + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export([span]) + self.assertEqual(SpanExportResult.SUCCESS, status) + + _, kwargs = mock_post.call_args # pylint: disable=E0633 + tags = json.loads(kwargs["data"])[0]["tags"] + self.assertEqual(len(tags["string1"]), 9) + self.assertEqual(len(tags["string2"]), 9) + self.assertEqual(tags["list1"], '["a","a"]') + self.assertEqual(tags["list2"], '["a","a"]') + self.assertEqual(tags["list3"], '["2","2"]') + self.assertEqual(tags["list4"], '["2","2"]') + self.assertEqual(tags["list5"], '["True"]') + self.assertEqual(tags["list6"], '["True"]') + self.assertEqual(tags["tuple1"], '["a","a"]') + self.assertEqual(tags["tuple2"], '["a","a"]') + self.assertEqual(tags["tuple3"], '["2","2"]') + self.assertEqual(tags["tuple4"], '["2","2"]') + self.assertEqual(tags["tuple5"], '["True"]') + self.assertEqual(tags["tuple6"], '["True"]') + self.assertEqual(tags["range1"], '["0","1"]') + self.assertEqual(tags["range2"], '["0","1"]') + + exporter = ZipkinSpanExporter(service_name, max_tag_value_length=10) + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export([span]) + self.assertEqual(SpanExportResult.SUCCESS, status) + + _, kwargs = mock_post.call_args # pylint: disable=E0633 + tags = json.loads(kwargs["data"])[0]["tags"] + self.assertEqual(len(tags["string1"]), 10) + self.assertEqual(len(tags["string2"]), 10) + self.assertEqual(tags["list1"], '["a","a"]') + self.assertEqual(tags["list2"], '["a","a"]') + self.assertEqual(tags["list3"], '["2","2"]') + self.assertEqual(tags["list4"], '["2","2"]') + self.assertEqual(tags["list5"], '["True"]') + self.assertEqual(tags["list6"], '["True"]') + self.assertEqual(tags["tuple1"], '["a","a"]') + self.assertEqual(tags["tuple2"], '["a","a"]') + self.assertEqual(tags["tuple3"], '["2","2"]') + self.assertEqual(tags["tuple4"], '["2","2"]') + self.assertEqual(tags["tuple5"], '["True"]') + self.assertEqual(tags["tuple6"], '["True"]') + self.assertEqual(tags["range1"], '["0","1"]') + self.assertEqual(tags["range2"], '["0","1"]') + + exporter = ZipkinSpanExporter(service_name, max_tag_value_length=11) + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export([span]) + self.assertEqual(SpanExportResult.SUCCESS, status) + + _, kwargs = mock_post.call_args # pylint: disable=E0633 + tags = json.loads(kwargs["data"])[0]["tags"] + self.assertEqual(len(tags["string1"]), 11) + self.assertEqual(len(tags["string2"]), 11) + self.assertEqual(tags["list1"], '["a","a"]') + self.assertEqual(tags["list2"], '["a","a"]') + self.assertEqual(tags["list3"], '["2","2"]') + self.assertEqual(tags["list4"], '["2","2"]') + self.assertEqual(tags["list5"], '["True"]') + self.assertEqual(tags["list6"], '["True"]') + self.assertEqual(tags["tuple1"], '["a","a"]') + self.assertEqual(tags["tuple2"], '["a","a"]') + self.assertEqual(tags["tuple3"], '["2","2"]') + self.assertEqual(tags["tuple4"], '["2","2"]') + self.assertEqual(tags["tuple5"], '["True"]') + self.assertEqual(tags["tuple6"], '["True"]') + self.assertEqual(tags["range1"], '["0","1"]') + self.assertEqual(tags["range2"], '["0","1"]') # pylint: disable=too-many-locals,too-many-statements def test_export_protobuf(self): From d8c3c24c347981e8c9ff9e41485f9579a80bc5bb Mon Sep 17 00:00:00 2001 From: Wanqi Lyu Date: Wed, 16 Dec 2020 23:40:44 +0800 Subject: [PATCH 0717/1517] Update usage for `Meter.create_counter` in tests and examples, clean up unused imports (#1463) --- docs/examples/opencensus-exporter-metrics/collector.py | 3 +-- docs/getting_started/otlpcollector_example.py | 1 - docs/getting_started/prometheus_example.py | 2 -- .../tests/test_otcollector_metrics_exporter.py | 4 ++-- .../src/opentelemetry/exporter/prometheus/__init__.py | 3 +-- 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/docs/examples/opencensus-exporter-metrics/collector.py b/docs/examples/opencensus-exporter-metrics/collector.py index cf162631b6..31527d480a 100644 --- a/docs/examples/opencensus-exporter-metrics/collector.py +++ b/docs/examples/opencensus-exporter-metrics/collector.py @@ -20,7 +20,7 @@ from opentelemetry.exporter.opencensus.metrics_exporter import ( OpenCensusMetricsExporter, ) -from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics import MeterProvider exporter = OpenCensusMetricsExporter( service_name="basic-service", endpoint="localhost:55678" @@ -35,7 +35,6 @@ description="number of requests", unit="1", value_type=int, - label_keys=("environment",), ) staging_labels = {"environment": "staging"} diff --git a/docs/getting_started/otlpcollector_example.py b/docs/getting_started/otlpcollector_example.py index f298f08fb1..57440bda78 100644 --- a/docs/getting_started/otlpcollector_example.py +++ b/docs/getting_started/otlpcollector_example.py @@ -59,7 +59,6 @@ description="number of requests", unit="1", value_type=int, - label_keys=("environment",), ) # Labels are used to identify key-values that are associated with a specific # metric that you want to record. These are useful for pre-aggregation and can diff --git a/docs/getting_started/prometheus_example.py b/docs/getting_started/prometheus_example.py index 8a76984f68..7aba740bd7 100644 --- a/docs/getting_started/prometheus_example.py +++ b/docs/getting_started/prometheus_example.py @@ -21,7 +21,6 @@ from opentelemetry import metrics from opentelemetry.exporter.prometheus import PrometheusMetricsExporter from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController # Start Prometheus client @@ -40,7 +39,6 @@ description="number of requests", unit="1", value_type=int, - label_keys=("environment",), ) requests_counter.add(25, staging_labels) diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py index 3b40b8d75a..d949c6cd33 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py @@ -142,7 +142,7 @@ def test_export(self): client=mock_client, host_name=host_name ) test_metric = self._meter.create_counter( - "testname", "testdesc", "unit", int, self._labels.keys(), + "testname", "testdesc", "unit", int, ) record = ExportRecord( test_metric, @@ -168,7 +168,7 @@ def test_export(self): def test_translate_to_collector(self): test_metric = self._meter.create_counter( - "testcollector", "testdesc", "unit", int, self._labels.keys() + "testcollector", "testdesc", "unit", int, ) aggregator = aggregate.SumAggregator() aggregator.update(123) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 1c13903564..760c54d3c8 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -28,7 +28,7 @@ from opentelemetry import metrics from opentelemetry.exporter.prometheus import PrometheusMetricsExporter - from opentelemetry.sdk.metrics import Counter, Meter + from opentelemetry.sdk.metrics import Meter from prometheus_client import start_http_server # Start Prometheus client @@ -48,7 +48,6 @@ "number of requests", "requests", int, - ("environment",), ) # Labels are used to identify key-values that are associated with a specific From 9d0834d38f8a8eb1e9849d4924357f3d6e569471 Mon Sep 17 00:00:00 2001 From: Dilip M <55284676+dmarar@users.noreply.github.com> Date: Wed, 16 Dec 2020 23:35:26 +0530 Subject: [PATCH 0718/1517] Auto-instrumentation should exclude packages mentioned in OTEL_PYTHON_DISABLED_INSTRUMENTATIONS env variable (#1461) --- CHANGELOG.md | 2 ++ opentelemetry-instrumentation/README.rst | 6 ++++++ .../auto_instrumentation/sitecustomize.py | 13 +++++++++++++ 3 files changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dad442d252..f6bd54ab48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1441](https://github.com/open-telemetry/opentelemetry-python/pull/1441)) ### Added +- Added the ability to disable instrumenting libraries specified by OTEL_PYTHON_DISABLED_INSTRUMENTATIONS env variable, when using opentelemetry-instrument command. + ([#1461](https://github.com/open-telemetry/opentelemetry-python/pull/1461)) - Add `fields` to propagators ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) - Add local/remote samplers to parent based sampler diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index aed9909a04..97ebc5e7c4 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -82,6 +82,12 @@ The code in ``program.py`` needs to use one of the packages for which there is an OpenTelemetry integration. For a list of the available integrations please check `here `_ +* ``OTEL_PYTHON_DISABLED_INSTRUMENTATIONS`` + +If set by the user, opentelemetry-instrument will read this environment variable to disable specific instrumentations. +e.g OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "requests,django" + + Examples ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index 2a51d0b803..3465619ea3 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -18,6 +18,8 @@ from pkg_resources import iter_entry_points +from opentelemetry.configuration import Configuration + logger = getLogger(__file__) @@ -27,8 +29,19 @@ def _load_distros(): def _load_instrumentors(): + package_to_exclude = Configuration().get("DISABLED_INSTRUMENTATIONS", []) + if isinstance(package_to_exclude, str): + package_to_exclude = package_to_exclude.split(",") + # to handle users entering "requests , flask" or "requests, flask" with spaces + package_to_exclude = [x.strip() for x in package_to_exclude] + for entry_point in iter_entry_points("opentelemetry_instrumentor"): try: + if entry_point.name in package_to_exclude: + logger.debug( + "Instrumentation skipped for library %s", entry_point.name + ) + continue entry_point.load()().instrument() # type: ignore logger.debug("Instrumented %s", entry_point.name) except Exception as exc: # pylint: disable=broad-except From 1cc23e00af15fe4b795350c21e08f8a266381db1 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 18 Dec 2020 15:20:15 -0600 Subject: [PATCH 0719/1517] Use uppercase if testing in windows (#1484) Fixes #1419 --- opentelemetry-api/tests/configuration/test_configuration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index de7bb16de6..b36d93412b 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -13,6 +13,7 @@ # limitations under the License. # pylint: disable-all +from sys import platform from unittest import TestCase from unittest.mock import patch @@ -35,7 +36,7 @@ def test_singleton(self) -> None: { "OTEL_PYTHON_METER_PROVIDER": "meter_provider", "OTEL_PYTHON_TRACER_PROVIDER": "tracer_provider", - "OTEL_OThER": "other", + "OTEL_OTHER" if platform == "windows" else "OTEL_OThER": "other", "OTEL_OTHER_7": "other_7", "OPENTELEMETRY_PTHON_TRACEX_PROVIDER": "tracex_provider", }, From 753ec6c272273c5d941d43ce836e304e23c75ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Fri, 18 Dec 2020 22:52:08 +0100 Subject: [PATCH 0720/1517] Add some repr()s to basic trace types. (#1485) --- CHANGELOG.md | 2 ++ opentelemetry-api/src/opentelemetry/trace/span.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6bd54ab48..e2d0b55121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1420](https://github.com/open-telemetry/opentelemetry-python/pull/1420)) - `opentelemetry-exporter-zipkin` Add support for array attributes in Span and Resource exports ([#1285](https://github.com/open-telemetry/opentelemetry-python/pull/1285)) +- Added `__repr__` for `DefaultSpan`, added `trace_flags` to `__repr__` of + `SpanContext` ([#1485](https://github.com/open-telemetry/opentelemetry-python/pull/1485)]) ## [0.16b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b1) - 2020-11-26 ### Added diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 6506f6f949..507b051368 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -237,11 +237,12 @@ def __delattr__(self, *args: str) -> None: def __repr__(self) -> str: return ( - "{}(trace_id={}, span_id={}, trace_state={!r}, is_remote={})" + "{}(trace_id={}, span_id={}, trace_flags=0x{:02x}, trace_state={!r}, is_remote={})" ).format( type(self).__name__, format_trace_id(self.trace_id), format_span_id(self.span_id), + self.trace_flags, self.trace_state, self.is_remote, ) @@ -291,6 +292,9 @@ def record_exception( ) -> None: pass + def __repr__(self) -> str: + return "DefaultSpan({!r})".format(self._context) + INVALID_SPAN_ID = 0x0000000000000000 INVALID_TRACE_ID = 0x00000000000000000000000000000000 From 6f2b5ceefa87f443bf574eac7debe2b0e996e59d Mon Sep 17 00:00:00 2001 From: alrex Date: Sun, 20 Dec 2020 08:47:13 -0800 Subject: [PATCH 0721/1517] Deleting unused file (#1492) --- opentelemetry-api/src/opentelemetry/distributedcontext/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 opentelemetry-api/src/opentelemetry/distributedcontext/py.typed diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/py.typed b/opentelemetry-api/src/opentelemetry/distributedcontext/py.typed deleted file mode 100644 index e69de29bb2..0000000000 From dbe9d386fcd7b7e8ade2f803766b56deab628738 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Mon, 21 Dec 2020 16:09:05 -0800 Subject: [PATCH 0722/1517] Set max number of commits in performance graph (#1498) --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 672e14e1e7..c534fd9caf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,6 +78,7 @@ jobs: tool: pytest output-file-path: output.json github-token: ${{ secrets.GITHUB_TOKEN }} + max-items-in-chart: 100 # Alert with a commit comment on possible performance regression alert-threshold: 200% fail-on-alert: true From 41953604180695852223805efe1e1b0d7372de3d Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 23 Dec 2020 06:23:43 +0530 Subject: [PATCH 0723/1517] Update zipkin exporter status code and error tag (#1486) --- CHANGELOG.md | 3 +++ .../opentelemetry/exporter/zipkin/__init__.py | 25 ++++++++++--------- .../tests/test_zipkin_exporter.py | 22 ++++++---------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2d0b55121..ccf9f81c10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1285](https://github.com/open-telemetry/opentelemetry-python/pull/1285)) - Added `__repr__` for `DefaultSpan`, added `trace_flags` to `__repr__` of `SpanContext` ([#1485](https://github.com/open-telemetry/opentelemetry-python/pull/1485)]) +### Changed +- `opentelemetry-exporter-zipkin` Updated zipkin exporter status code and error tag + ([#1486](https://github.com/open-telemetry/opentelemetry-python/pull/1486)) ## [0.16b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b1) - 2020-11-26 ### Added diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index c6d3559b90..6b376c203c 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -82,6 +82,7 @@ from opentelemetry.exporter.zipkin.gen import zipkin_pb2 from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace import Span, SpanContext, SpanKind +from opentelemetry.trace.status import StatusCode TRANSPORT_FORMAT_JSON = "json" TRANSPORT_FORMAT_PROTOBUF = "protobuf" @@ -237,14 +238,14 @@ def _translate_to_json(self, spans: Sequence[Span]): "otel.instrumentation_library.version" ] = span.instrumentation_info.version - if span.status is not None: - zipkin_span["tags"]["otel.status_code"] = str( - span.status.status_code.value - ) - if span.status.description is not None: - zipkin_span["tags"][ - "otel.status_description" - ] = span.status.description + if span.status.status_code is not StatusCode.UNSET: + zipkin_span["tags"][ + "otel.status_code" + ] = span.status.status_code.name + if span.status.status_code is StatusCode.ERROR: + zipkin_span["tags"]["error"] = ( + span.status.description or "" + ) if context.trace_flags.sampled: zipkin_span["debug"] = True @@ -317,13 +318,13 @@ def _translate_to_protobuf(self, spans: Sequence[Span]): } ) - if span.status is not None: + if span.status.status_code is not StatusCode.UNSET: pbuf_span.tags.update( - {"otel.status_code": str(span.status.status_code.value)} + {"otel.status_code": span.status.status_code.name} ) - if span.status.description is not None: + if span.status.status_code is StatusCode.ERROR: pbuf_span.tags.update( - {"otel.status_description": span.status.description} + {"error": span.status.description or ""} ) if context.trace_flags.sampled: diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index a21199659b..eb3f4894c1 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -246,8 +246,8 @@ def test_export_json(self): "key_bool": "False", "key_string": "hello_world", "key_float": "111.22", - "otel.status_code": "2", - "otel.status_description": "Example description", + "otel.status_code": "ERROR", + "error": "Example description", }, "debug": True, "parentId": format(parent_id, "x"), @@ -272,10 +272,7 @@ def test_export_json(self): "duration": durations[1] // 10 ** 3, "localEndpoint": local_endpoint, "kind": span_kind, - "tags": { - "key_resource": "some_resource", - "otel.status_code": "1", - }, + "tags": {"key_resource": "some_resource"}, "annotations": None, }, { @@ -289,7 +286,6 @@ def test_export_json(self): "tags": { "key_string": "hello_world", "key_resource": "some_resource", - "otel.status_code": "1", }, "annotations": None, }, @@ -304,7 +300,6 @@ def test_export_json(self): "tags": { "otel.instrumentation_library.name": "name", "otel.instrumentation_library.version": "version", - "otel.status_code": "1", }, "annotations": None, }, @@ -383,7 +378,7 @@ def test_export_json_zero_padding(self): "duration": duration // 10 ** 3, "localEndpoint": local_endpoint, "kind": SPAN_KIND_MAP_JSON[SpanKind.INTERNAL], - "tags": {"otel.status_code": "1"}, + "tags": {}, "annotations": None, "debug": True, "parentId": "0aaaaaaaaaaaaaaa", @@ -737,6 +732,7 @@ def test_export_protobuf(self): otel_spans[1].resource = Resource( attributes={"key_resource": "some_resource"} ) + otel_spans[1].set_status(Status(StatusCode.OK)) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) @@ -775,8 +771,8 @@ def test_export_protobuf(self): "key_bool": "False", "key_string": "hello_world", "key_float": "111.22", - "otel.status_code": "2", - "otel.status_description": "Example description", + "otel.status_code": "ERROR", + "error": "Example description", }, debug=True, parent_id=ZipkinSpanExporter.format_pbuf_span_id( @@ -809,7 +805,7 @@ def test_export_protobuf(self): kind=span_kind, tags={ "key_resource": "some_resource", - "otel.status_code": "1", + "otel.status_code": "OK", }, ), zipkin_pb2.Span( @@ -825,7 +821,6 @@ def test_export_protobuf(self): tags={ "key_string": "hello_world", "key_resource": "some_resource", - "otel.status_code": "1", }, ), zipkin_pb2.Span( @@ -841,7 +836,6 @@ def test_export_protobuf(self): tags={ "otel.instrumentation_library.name": "name", "otel.instrumentation_library.version": "version", - "otel.status_code": "1", }, ), ], From 8ebd6c80c0475e1d203671a5a3807961e0eb44cf Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Tue, 22 Dec 2020 17:06:41 -0800 Subject: [PATCH 0724/1517] Add throughput performance tests for OTLP exporter (#1491) --- .github/workflows/test.yml | 9 +-- .../test_benchmark_trace_exporter.py | 80 +++++++++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 exporter/opentelemetry-exporter-otlp/tests/performance/benchmarks/test_benchmark_trace_exporter.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c534fd9caf..d702613f32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,15 +63,14 @@ jobs: - name: run tox run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json - name: Find and merge benchmarks - # TODO: Add at least one benchmark to every package type to remove this - if: matrix.package == 'core' + id: find_and_merge_benchmarks run: >- jq -s '.[0].benchmarks = ([.[].benchmarks] | add) | if .[0].benchmarks == null then null else .[0] end' - opentelemetry-*/tests/*${{ matrix.package }}*-benchmark.json > output.json + $(find . -name '*${{ matrix.package }}*-benchmark.json') > output.json + && echo "::set-output name=json_plaintext::$(cat output.json)" - name: Report on benchmark results - # TODO: Add at least one benchmark to every package type to remove this - if: matrix.package == 'core' + if: steps.find_and_merge_benchmarks.outputs.json_plaintext != 'null' uses: rhysd/github-action-benchmark@v1 with: name: OpenTelemetry Python Benchmarks - Python ${{ env[matrix.python-version ]}} - ${{ matrix.package }} diff --git a/exporter/opentelemetry-exporter-otlp/tests/performance/benchmarks/test_benchmark_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/performance/benchmarks/test_benchmark_trace_exporter.py new file mode 100644 index 0000000000..00c22f0359 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/tests/performance/benchmarks/test_benchmark_trace_exporter.py @@ -0,0 +1,80 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest.mock import patch + +from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.trace import TracerProvider, sampling +from opentelemetry.sdk.trace.export import ( + BatchExportSpanProcessor, + SimpleExportSpanProcessor, +) + + +def get_tracer_with_processor(span_processor_class): + span_processor = span_processor_class(OTLPSpanExporter()) + tracer = TracerProvider( + active_span_processor=span_processor, sampler=sampling.DEFAULT_ON, + ).get_tracer("pipeline_benchmark_tracer") + return tracer + + +class MockTraceServiceStub(object): + def __init__(self, channel): + self.Export = lambda *args, **kwargs: None + + +@patch( + "opentelemetry.exporter.otlp.trace_exporter.OTLPSpanExporter._stub", + new=MockTraceServiceStub, +) +def test_simple_span_processor(benchmark): + tracer = get_tracer_with_processor(SimpleExportSpanProcessor) + + def create_spans_to_be_exported(): + span = tracer.start_span("benchmarkedSpan",) + for i in range(10): + span.set_attribute( + "benchmarkAttribute_{}".format(i), + "benchmarkAttrValue_{}".format(i), + ) + span.end() + + benchmark(create_spans_to_be_exported) + + +@patch( + "opentelemetry.exporter.otlp.trace_exporter.OTLPSpanExporter._stub", + new=MockTraceServiceStub, +) +def test_batch_span_processor(benchmark): + """Runs benchmark tests using BatchExportSpanProcessor. + + One particular call by pytest-benchmark will be much more expensive since + the batch export thread will activate and consume a lot of CPU to process + all the spans. For this reason, focus on the average measurement. Do not + focus on the min/max measurements which will be misleading. + """ + tracer = get_tracer_with_processor(BatchExportSpanProcessor) + + def create_spans_to_be_exported(): + span = tracer.start_span("benchmarkedSpan",) + for i in range(10): + span.set_attribute( + "benchmarkAttribute_{}".format(i), + "benchmarkAttrValue_{}".format(i), + ) + span.end() + + benchmark(create_spans_to_be_exported) From bd8db6e2a461d896c1c0556efd0f4b569106fa96 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 4 Jan 2021 22:43:16 +0530 Subject: [PATCH 0725/1517] Add protobuf via gRPC exporting support for Jaeger (#1471) --- CHANGELOG.md | 3 + docs/conf.py | 1 + docs/exporter/jaeger/jaeger.rst | 10 + .../examples/jaeger_exporter_example.py | 13 + .../proto/api_v2/collector.proto | 66 + .../proto/api_v2/model.proto | 166 ++ .../opentelemetry-exporter-jaeger/setup.cfg | 2 + .../opentelemetry/exporter/jaeger/__init__.py | 400 +--- .../exporter/jaeger/gen/collector_pb2.py | 134 ++ .../exporter/jaeger/gen/collector_pb2_grpc.py | 46 + .../exporter/jaeger/gen/gogoproto/gogo_pb2.py | 794 ++++++++ .../jaeger/gen/google/api/annotations_pb2.py | 46 + .../jaeger/gen/google/api/http_pb2.py | 250 +++ .../exporter/jaeger/gen/model_pb2.py | 686 +++++++ .../options/annotations_pb2.py | 90 + .../options/openapiv2_pb2.py | 1777 +++++++++++++++++ .../exporter/jaeger/send/__init__.py | 0 .../exporter/jaeger/send/thrift.py | 118 ++ .../exporter/jaeger/translate/__init__.py | 87 + .../exporter/jaeger/translate/protobuf.py | 257 +++ .../exporter/jaeger/translate/thrift.py | 194 ++ .../src/opentelemetry/exporter/jaeger/util.py | 51 + .../tests/certs/cred.cert | 0 ...rter.py => test_jaeger_exporter_thrift.py} | 52 +- .../tests/test_jarget_exporter_protobuf.py | 392 ++++ tox.ini | 1 - 26 files changed, 5308 insertions(+), 328 deletions(-) create mode 100644 exporter/opentelemetry-exporter-jaeger/proto/api_v2/collector.proto create mode 100644 exporter/opentelemetry-exporter-jaeger/proto/api_v2/model.proto create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/collector_pb2.py create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/collector_pb2_grpc.py create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/gogoproto/gogo_pb2.py create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/google/api/annotations_pb2.py create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/google/api/http_pb2.py create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/model_pb2.py create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/protoc_gen_swagger/options/annotations_pb2.py create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/protoc_gen_swagger/options/openapiv2_pb2.py create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/__init__.py create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/thrift.py create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py create mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py create mode 100644 exporter/opentelemetry-exporter-jaeger/tests/certs/cred.cert rename exporter/opentelemetry-exporter-jaeger/tests/{test_jaeger_exporter.py => test_jaeger_exporter_thrift.py} (90%) create mode 100644 exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ccf9f81c10..1b1b8fc620 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.16b1...HEAD) +- Add protobuf via gRPC exporting support for Jaeger + ([#1471](https://github.com/open-telemetry/opentelemetry-python/pull/1471)) + - Add support for Python 3.9 ([#1441](https://github.com/open-telemetry/opentelemetry-python/pull/1441)) diff --git a/docs/conf.py b/docs/conf.py index b227cb5181..0d06bffec1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -86,6 +86,7 @@ "aiohttp": ("https://aiohttp.readthedocs.io/en/stable/", None), "wrapt": ("https://wrapt.readthedocs.io/en/latest/", None), "pymongo": ("https://pymongo.readthedocs.io/en/stable/", None), + "grpc": ("https://grpc.github.io/grpc/python/", None), } # http://www.sphinx-doc.org/en/master/config.html#confval-nitpicky diff --git a/docs/exporter/jaeger/jaeger.rst b/docs/exporter/jaeger/jaeger.rst index 6988d8cea9..efbcfbd7d7 100644 --- a/docs/exporter/jaeger/jaeger.rst +++ b/docs/exporter/jaeger/jaeger.rst @@ -14,3 +14,13 @@ Submodules :members: :undoc-members: :show-inheritance: + +.. automodule:: opentelemetry.exporter.jaeger.send.thrift + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: opentelemetry.exporter.jaeger.gen.collector_pb2_grpc + :members: + :undoc-members: + :show-inheritance: diff --git a/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py b/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py index 0552f75183..0d312676b9 100644 --- a/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py +++ b/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py @@ -22,6 +22,19 @@ # password=xxxx, # optional ) +# Create a JaegerSpanExporter to send spans with gRPC +# If there is no encryption or authentication set `insecure` to True +# If server has authentication with SSL/TLS you can set the +# parameter credentials=ChannelCredentials(...) or the environment variable +# `EXPORTER_JAEGER_CERTIFICATE` with file containing creds. + +# jaeger_exporter = jaeger.JaegerSpanExporter( +# service_name="my-helloworld-service", +# collector_endpoint="localhost:14250", +# insecure=True, +# transport_format="protobuf", +# ) + # create a BatchExportSpanProcessor and add the exporter to it span_processor = BatchExportSpanProcessor(jaeger_exporter) diff --git a/exporter/opentelemetry-exporter-jaeger/proto/api_v2/collector.proto b/exporter/opentelemetry-exporter-jaeger/proto/api_v2/collector.proto new file mode 100644 index 0000000000..e897a043cd --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/proto/api_v2/collector.proto @@ -0,0 +1,66 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2018 Uber Technologies, 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. + +syntax="proto3"; + +package jaeger.api_v2; + +import "model.proto"; +import "gogoproto/gogo.proto"; +import "google/api/annotations.proto"; +import "protoc-gen-swagger/options/annotations.proto"; + +option go_package = "api_v2"; +option java_package = "io.jaegertracing.api_v2"; + +// Enable gogoprotobuf extensions (https://github.com/gogo/protobuf/blob/master/extensions.md). +// Enable custom Marshal method. +option (gogoproto.marshaler_all) = true; +// Enable custom Unmarshal method. +option (gogoproto.unmarshaler_all) = true; +// Enable custom Size method (Required by Marshal and Unmarshal). +option (gogoproto.sizer_all) = true; +// Enable registration with golang/protobuf for the grpc-gateway. +option (gogoproto.goproto_registration) = true; + +option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { + info: { + version: "1.0"; + }; + external_docs: { + url: "https://github.com/jaegertracing/jaeger"; + description: "Jaeger API"; + } + schemes: HTTP; + schemes: HTTPS; +}; + +message PostSpansRequest { + Batch batch = 1 [ + (gogoproto.nullable) = false + ]; +} + +message PostSpansResponse { +} + +service CollectorService { + rpc PostSpans(PostSpansRequest) returns (PostSpansResponse) { + option (google.api.http) = { + post: "/api/v2/spans" + body: "*" + }; + } +} diff --git a/exporter/opentelemetry-exporter-jaeger/proto/api_v2/model.proto b/exporter/opentelemetry-exporter-jaeger/proto/api_v2/model.proto new file mode 100644 index 0000000000..3cc15df3b8 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/proto/api_v2/model.proto @@ -0,0 +1,166 @@ +// Copyright (c) 2018 Uber Technologies, 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. + +syntax="proto3"; + +package jaeger.api_v2; + +import "gogoproto/gogo.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; + +// TODO: document all types and fields + +// TODO: once this moves to jaeger-idl repo, we may want to change Go pkg to api_v2 +// and rewrite it to model only in this repo. That should make it easier to generate +// classes in other languages. +option go_package = "model"; +option java_package = "io.jaegertracing.api_v2"; + +// Enable gogoprotobuf extensions (https://github.com/gogo/protobuf/blob/master/extensions.md). +// Enable custom Marshal method. +option (gogoproto.marshaler_all) = true; +// Enable custom Unmarshal method. +option (gogoproto.unmarshaler_all) = true; +// Enable custom Size method (Required by Marshal and Unmarshal). +option (gogoproto.sizer_all) = true; +// Enable registration with golang/protobuf for the grpc-gateway. +option (gogoproto.goproto_registration) = true; + +enum ValueType { + STRING = 0; + BOOL = 1; + INT64 = 2; + FLOAT64 = 3; + BINARY = 4; +}; + +message KeyValue { + option (gogoproto.equal) = true; + option (gogoproto.compare) = true; + + string key = 1; + ValueType v_type = 2; + string v_str = 3; + bool v_bool = 4; + int64 v_int64 = 5; + double v_float64 = 6; + bytes v_binary = 7; +} + +message Log { + google.protobuf.Timestamp timestamp = 1 [ + (gogoproto.stdtime) = true, + (gogoproto.nullable) = false + ]; + repeated KeyValue fields = 2 [ + (gogoproto.nullable) = false + ]; +} + +enum SpanRefType { + CHILD_OF = 0; + FOLLOWS_FROM = 1; +}; + +message SpanRef { + bytes trace_id = 1 [ + (gogoproto.nullable) = false, + (gogoproto.customtype) = "TraceID", + (gogoproto.customname) = "TraceID" + ]; + bytes span_id = 2 [ + (gogoproto.nullable) = false, + (gogoproto.customtype) = "SpanID", + (gogoproto.customname) = "SpanID" + ]; + SpanRefType ref_type = 3; +} + +message Process { + string service_name = 1; + repeated KeyValue tags = 2 [ + (gogoproto.nullable) = false + ]; +} + +message Span { + bytes trace_id = 1 [ + (gogoproto.nullable) = false, + (gogoproto.customtype) = "TraceID", + (gogoproto.customname) = "TraceID" + ]; + bytes span_id = 2 [ + (gogoproto.nullable) = false, + (gogoproto.customtype) = "SpanID", + (gogoproto.customname) = "SpanID" + ]; + string operation_name = 3; + repeated SpanRef references = 4 [ + (gogoproto.nullable) = false + ]; + uint32 flags = 5 [ + (gogoproto.nullable) = false, + (gogoproto.customtype) = "Flags" + ]; + google.protobuf.Timestamp start_time = 6 [ + (gogoproto.stdtime) = true, + (gogoproto.nullable) = false + ]; + google.protobuf.Duration duration = 7 [ + (gogoproto.stdduration) = true, + (gogoproto.nullable) = false + ]; + repeated KeyValue tags = 8 [ + (gogoproto.nullable) = false + ]; + repeated Log logs = 9 [ + (gogoproto.nullable) = false + ]; + Process process = 10; + string process_id = 11 [ + (gogoproto.customname) = "ProcessID" + ]; + repeated string warnings = 12; +} + +message Trace { + message ProcessMapping { + string process_id = 1 [ + (gogoproto.customname) = "ProcessID" + ]; + Process process = 2 [ + (gogoproto.nullable) = false + ]; + } + repeated Span spans = 1; + repeated ProcessMapping process_map = 2 [ + (gogoproto.nullable) = false + ]; + repeated string warnings = 3; +} + +message Batch { + repeated Span spans = 1; + Process process = 2 [ + (gogoproto.nullable) = true + ]; +} + +message DependencyLink { + string parent = 1; + string child = 2; + uint64 call_count = 3; + string source = 4; +} diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 37d2402d5f..7e3d6f50a5 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,6 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = + grpcio >= 1.0.0, < 2.0.0 + googleapis-common-protos ~= 1.52.0 thrift >= 0.10.0 opentelemetry-api == 0.17.dev0 opentelemetry-sdk == 0.17.dev0 diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index d95095bd9c..297c1e2b26 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -15,9 +15,11 @@ """ The **OpenTelemetry Jaeger Exporter** allows to export `OpenTelemetry`_ traces to `Jaeger`_. -This exporter always send traces to the configured agent using Thrift compact protocol over UDP. -An optional collector can be configured, in this case Thrift binary protocol over HTTP is used. -gRPC is still not supported by this implementation. +This exporter always sends traces to the configured agent using the Thrift compact protocol over UDP. +When it is not feasible to deploy Jaeger Agent next to the application, for example, when the +application code is running as Lambda function, a collector can be configured to send spans +using either Thrift over HTTP or Protobuf via gRPC. If both agent and collector are configured, +the exporter sends traces only to the collector to eliminate the duplicate entries. Usage ----- @@ -42,6 +44,9 @@ # collector_endpoint='http://localhost:14268/api/traces?format=jaeger.thrift', # username=xxxx, # optional # password=xxxx, # optional + # insecure=True, # optional + # credentials=xxx # optional channel creds + # transport_format='protobuf' # optional ) # Create a BatchExportSpanProcessor and add the exporter to it @@ -58,33 +63,47 @@ .. _Jaeger: https://www.jaegertracing.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ """ +# pylint: disable=protected-access import base64 import logging import socket - +from typing import Optional, Union + +from grpc import ( + ChannelCredentials, + insecure_channel, + secure_channel, + ssl_channel_credentials, +) from thrift.protocol import TBinaryProtocol, TCompactProtocol from thrift.transport import THttpClient, TTransport from opentelemetry.configuration import Configuration +from opentelemetry.exporter.jaeger import util +from opentelemetry.exporter.jaeger.gen import model_pb2 from opentelemetry.exporter.jaeger.gen.agent import Agent as agent -from opentelemetry.exporter.jaeger.gen.jaeger import Collector as jaeger +from opentelemetry.exporter.jaeger.gen.collector_pb2 import PostSpansRequest +from opentelemetry.exporter.jaeger.gen.collector_pb2_grpc import ( + CollectorServiceStub, +) +from opentelemetry.exporter.jaeger.gen.jaeger import Collector as jaeger_thrift +from opentelemetry.exporter.jaeger.send.thrift import AgentClientUDP, Collector +from opentelemetry.exporter.jaeger.translate import Translate +from opentelemetry.exporter.jaeger.translate.protobuf import ProtobufTranslator +from opentelemetry.exporter.jaeger.translate.thrift import ThriftTranslator from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult from opentelemetry.trace import SpanKind from opentelemetry.trace.status import StatusCode DEFAULT_AGENT_HOST_NAME = "localhost" DEFAULT_AGENT_PORT = 6831 +DEFAULT_GRPC_COLLECTOR_ENDPOINT = "localhost:14250" UDP_PACKET_MAX_LENGTH = 65000 -OTLP_JAEGER_SPAN_KIND = { - SpanKind.CLIENT: "client", - SpanKind.SERVER: "server", - SpanKind.CONSUMER: "consumer", - SpanKind.PRODUCER: "producer", - SpanKind.INTERNAL: "internal", -} +TRANSPORT_FORMAT_THRIFT = "thrift" +TRANSPORT_FORMAT_PROTOBUF = "protobuf" logger = logging.getLogger(__name__) @@ -97,21 +116,28 @@ class JaegerSpanExporter(SpanExporter): when query for spans. agent_host_name: The host name of the Jaeger-Agent. agent_port: The port of the Jaeger-Agent. - collector_endpoint: The endpoint of the Jaeger-Collector HTTP/HTTPS Thrift. + collector_endpoint: The endpoint of the Jaeger collector that uses + Thrift over HTTP/HTTPS or Protobuf via gRPC. username: The user name of the Basic Auth if authentication is required. password: The password of the Basic Auth if authentication is required. + insecure: True if collector has no encryption or authentication + credentials: Credentials for server authentication. + transport_format: Transport format for exporting spans to collector. """ def __init__( self, - service_name, - agent_host_name=None, - agent_port=None, - collector_endpoint=None, - username=None, - password=None, + service_name: str, + agent_host_name: Optional[str] = None, + agent_port: Optional[int] = None, + collector_endpoint: Optional[str] = None, + username: Optional[str] = None, + password: Optional[str] = None, + insecure: Optional[bool] = None, + credentials: Optional[ChannelCredentials] = None, + transport_format: Optional[str] = None, ): self.service_name = service_name self.agent_host_name = _parameter_setter( @@ -124,7 +150,9 @@ def __init__( env_variable=Configuration().EXPORTER_JAEGER_AGENT_PORT, default=DEFAULT_AGENT_PORT, ) - self._agent_client = None + self._agent_client = AgentClientUDP( + host_name=self.agent_host_name, port=self.agent_port + ) self.collector_endpoint = _parameter_setter( param=collector_endpoint, env_variable=Configuration().EXPORTER_JAEGER_ENDPOINT, @@ -141,21 +169,42 @@ def __init__( default=None, ) self._collector = None + self._grpc_client = None + self.insecure = util._get_insecure(insecure) + self.credentials = util._get_credentials(credentials) + self.transport_format = ( + transport_format.lower() + if transport_format + else TRANSPORT_FORMAT_THRIFT + ) @property - def agent_client(self): - if self._agent_client is None: - self._agent_client = AgentClientUDP( - host_name=self.agent_host_name, port=self.agent_port - ) - return self._agent_client + def _collector_grpc_client(self) -> Optional[CollectorServiceStub]: + if self.transport_format != TRANSPORT_FORMAT_PROTOBUF: + return None + + endpoint = self.collector_endpoint or DEFAULT_GRPC_COLLECTOR_ENDPOINT + + if self._grpc_client is None: + if self.insecure: + self._grpc_client = CollectorServiceStub( + insecure_channel(endpoint) + ) + else: + self._grpc_client = CollectorServiceStub( + secure_channel(endpoint, self.credentials) + ) + return self._grpc_client @property - def collector(self): + def _collector_http_client(self) -> Optional[Collector]: if self._collector is not None: return self._collector - if self.collector_endpoint is None: + if ( + self.collector_endpoint is None + or self.transport_format != TRANSPORT_FORMAT_THRIFT + ): return None auth = None @@ -167,18 +216,25 @@ def collector(self): ) return self._collector - def export(self, spans): - jaeger_spans = _translate_to_jaeger(spans) - - batch = jaeger.Batch( - spans=jaeger_spans, - process=jaeger.Process(serviceName=self.service_name), - ) - - if self.collector is not None: - self.collector.submit(batch) + def export(self, spans) -> SpanExportResult: + translator = Translate(spans) + if self.transport_format == TRANSPORT_FORMAT_PROTOBUF: + pb_translator = ProtobufTranslator(self.service_name) + jaeger_spans = translator._translate(pb_translator) + batch = model_pb2.Batch(spans=jaeger_spans) + request = PostSpansRequest(batch=batch) + self._collector_grpc_client.PostSpans(request) else: - self.agent_client.emit(batch) + thrift_translator = ThriftTranslator() + jaeger_spans = translator._translate(thrift_translator) + batch = jaeger_thrift.Batch( + spans=jaeger_spans, + process=jaeger_thrift.Process(serviceName=self.service_name), + ) + if self._collector_http_client is not None: + self._collector_http_client.submit(batch) + else: + self._agent_client.emit(batch) return SpanExportResult.SUCCESS @@ -200,269 +256,3 @@ def _parameter_setter(param, env_variable, default): res = param return res - - -def _nsec_to_usec_round(nsec): - """Round nanoseconds to microseconds""" - return (nsec + 500) // 10 ** 3 - - -def _translate_to_jaeger(spans: Span): - """Translate the spans to Jaeger format. - - Args: - spans: Tuple of spans to convert - """ - - jaeger_spans = [] - - for span in spans: - ctx = span.get_span_context() - trace_id = ctx.trace_id - span_id = ctx.span_id - - start_time_us = _nsec_to_usec_round(span.start_time) - duration_us = _nsec_to_usec_round(span.end_time - span.start_time) - - status = span.status - - parent_id = span.parent.span_id if span.parent else 0 - - tags = _extract_tags(span.attributes) - tags.extend(_extract_tags(span.resource.attributes)) - - tags.extend( - [ - _get_long_tag("status.code", status.status_code.value), - _get_string_tag("status.message", status.description), - _get_string_tag("span.kind", OTLP_JAEGER_SPAN_KIND[span.kind]), - ] - ) - - if span.instrumentation_info is not None: - tags.extend( - [ - _get_string_tag( - "otel.instrumentation_library.name", - span.instrumentation_info.name, - ), - _get_string_tag( - "otel.instrumentation_library.version", - span.instrumentation_info.version, - ), - ] - ) - - # Ensure that if Status.Code is not OK, that we set the "error" tag on the Jaeger span. - if not status.is_ok: - tags.append(_get_bool_tag("error", True)) - - refs = _extract_refs_from_span(span) - logs = _extract_logs_from_span(span) - - flags = int(ctx.trace_flags) - - jaeger_span = jaeger.Span( - traceIdHigh=_get_trace_id_high(trace_id), - traceIdLow=_get_trace_id_low(trace_id), - # generated code expects i64 - spanId=_convert_int_to_i64(span_id), - operationName=span.name, - startTime=start_time_us, - duration=duration_us, - tags=tags, - logs=logs, - references=refs, - flags=flags, - parentSpanId=_convert_int_to_i64(parent_id), - ) - - jaeger_spans.append(jaeger_span) - - return jaeger_spans - - -def _extract_refs_from_span(span): - if not span.links: - return None - - refs = [] - for link in span.links: - trace_id = link.context.trace_id - span_id = link.context.span_id - refs.append( - jaeger.SpanRef( - refType=jaeger.SpanRefType.FOLLOWS_FROM, - traceIdHigh=_get_trace_id_high(trace_id), - traceIdLow=_get_trace_id_low(trace_id), - spanId=_convert_int_to_i64(span_id), - ) - ) - return refs - - -def _convert_int_to_i64(val): - """Convert integer to signed int64 (i64)""" - if val > 0x7FFFFFFFFFFFFFFF: - val -= 0x10000000000000000 - return val - - -def _get_trace_id_low(trace_id): - return _convert_int_to_i64(trace_id & 0xFFFFFFFFFFFFFFFF) - - -def _get_trace_id_high(trace_id): - return _convert_int_to_i64((trace_id >> 64) & 0xFFFFFFFFFFFFFFFF) - - -def _extract_logs_from_span(span): - if not span.events: - return None - - logs = [] - - for event in span.events: - fields = _extract_tags(event.attributes) - - fields.append( - jaeger.Tag( - key="message", vType=jaeger.TagType.STRING, vStr=event.name - ) - ) - - event_timestamp_us = _nsec_to_usec_round(event.timestamp) - logs.append( - jaeger.Log(timestamp=int(event_timestamp_us), fields=fields) - ) - return logs - - -def _extract_tags(attr): - if not attr: - return [] - tags = [] - for attribute_key, attribute_value in attr.items(): - tag = _convert_attribute_to_tag(attribute_key, attribute_value) - if tag is None: - continue - tags.append(tag) - return tags - - -def _convert_attribute_to_tag(key, attr): - """Convert the attributes to jaeger tags.""" - if isinstance(attr, bool): - return jaeger.Tag(key=key, vBool=attr, vType=jaeger.TagType.BOOL) - if isinstance(attr, str): - return jaeger.Tag(key=key, vStr=attr, vType=jaeger.TagType.STRING) - if isinstance(attr, int): - return jaeger.Tag(key=key, vLong=attr, vType=jaeger.TagType.LONG) - if isinstance(attr, float): - return jaeger.Tag(key=key, vDouble=attr, vType=jaeger.TagType.DOUBLE) - if isinstance(attr, tuple): - return jaeger.Tag(key=key, vStr=str(attr), vType=jaeger.TagType.STRING) - logger.warning("Could not serialize attribute %s:%r to tag", key, attr) - return None - - -def _get_long_tag(key, val): - return jaeger.Tag(key=key, vLong=val, vType=jaeger.TagType.LONG) - - -def _get_string_tag(key, val): - return jaeger.Tag(key=key, vStr=val, vType=jaeger.TagType.STRING) - - -def _get_bool_tag(key, val): - return jaeger.Tag(key=key, vBool=val, vType=jaeger.TagType.BOOL) - - -class AgentClientUDP: - """Implement a UDP client to agent. - - Args: - host_name: The host name of the Jaeger server. - port: The port of the Jaeger server. - max_packet_size: Maximum size of UDP packet. - client: Class for creating new client objects for agencies. - """ - - def __init__( - self, - host_name, - port, - max_packet_size=UDP_PACKET_MAX_LENGTH, - client=agent.Client, - ): - self.address = (host_name, port) - self.max_packet_size = max_packet_size - self.buffer = TTransport.TMemoryBuffer() - self.client = client( - iprot=TCompactProtocol.TCompactProtocol(trans=self.buffer) - ) - - def emit(self, batch: jaeger.Batch): - """ - Args: - batch: Object to emit Jaeger spans. - """ - - # pylint: disable=protected-access - self.client._seqid = 0 - # truncate and reset the position of BytesIO object - self.buffer._buffer.truncate(0) - self.buffer._buffer.seek(0) - self.client.emitBatch(batch) - buff = self.buffer.getvalue() - if len(buff) > self.max_packet_size: - logger.warning( - "Data exceeds the max UDP packet size; size %r, max %r", - len(buff), - self.max_packet_size, - ) - return - - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_socket: - udp_socket.sendto(buff, self.address) - - -class Collector: - """Submits collected spans to Thrift HTTP server. - - Args: - thrift_url: URL of the Jaeger HTTP Thrift. - auth: Auth tuple that contains username and password for Basic Auth. - """ - - def __init__(self, thrift_url="", auth=None): - self.thrift_url = thrift_url - self.auth = auth - self.http_transport = THttpClient.THttpClient( - uri_or_host=self.thrift_url - ) - self.protocol = TBinaryProtocol.TBinaryProtocol(self.http_transport) - - # set basic auth header - if auth is not None: - auth_header = "{}:{}".format(*auth) - decoded = base64.b64encode(auth_header.encode()).decode("ascii") - basic_auth = dict(Authorization="Basic {}".format(decoded)) - self.http_transport.setCustomHeaders(basic_auth) - - def submit(self, batch: jaeger.Batch): - """Submits batches to Thrift HTTP Server through Binary Protocol. - - Args: - batch: Object to emit Jaeger spans. - """ - batch.write(self.protocol) - self.http_transport.flush() - code = self.http_transport.code - msg = self.http_transport.message - if code >= 300 or code < 200: - logger.error( - "Traces cannot be uploaded; HTTP status code: %s, message: %s", - code, - msg, - ) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/collector_pb2.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/collector_pb2.py new file mode 100644 index 0000000000..87dffbbc6d --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/collector_pb2.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: collector.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +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() + + +import model_pb2 as model__pb2 +from gogoproto import gogo_pb2 as gogoproto_dot_gogo__pb2 +from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 +from protoc_gen_swagger.options import annotations_pb2 as protoc__gen__swagger_dot_options_dot_annotations__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='collector.proto', + package='jaeger.api_v2', + syntax='proto3', + serialized_options=_b('\n\027io.jaegertracing.api_v2Z\006api_v2\310\342\036\001\320\342\036\001\340\342\036\001\300\343\036\001\222AB\022\0052\0031.0*\002\001\002r5\n\nJaeger API\022\'https://github.com/jaegertracing/jaeger'), + serialized_pb=_b('\n\x0f\x63ollector.proto\x12\rjaeger.api_v2\x1a\x0bmodel.proto\x1a\x14gogoproto/gogo.proto\x1a\x1cgoogle/api/annotations.proto\x1a,protoc-gen-swagger/options/annotations.proto\"=\n\x10PostSpansRequest\x12)\n\x05\x62\x61tch\x18\x01 \x01(\x0b\x32\x14.jaeger.api_v2.BatchB\x04\xc8\xde\x1f\x00\"\x13\n\x11PostSpansResponse2|\n\x10\x43ollectorService\x12h\n\tPostSpans\x12\x1f.jaeger.api_v2.PostSpansRequest\x1a .jaeger.api_v2.PostSpansResponse\"\x18\x82\xd3\xe4\x93\x02\x12\"\r/api/v2/spans:\x01*Bv\n\x17io.jaegertracing.api_v2Z\x06\x61pi_v2\xc8\xe2\x1e\x01\xd0\xe2\x1e\x01\xe0\xe2\x1e\x01\xc0\xe3\x1e\x01\x92\x41\x42\x12\x05\x32\x03\x31.0*\x02\x01\x02r5\n\nJaeger API\x12\'https://github.com/jaegertracing/jaegerb\x06proto3') + , + dependencies=[model__pb2.DESCRIPTOR,gogoproto_dot_gogo__pb2.DESCRIPTOR,google_dot_api_dot_annotations__pb2.DESCRIPTOR,protoc__gen__swagger_dot_options_dot_annotations__pb2.DESCRIPTOR,]) + + + + +_POSTSPANSREQUEST = _descriptor.Descriptor( + name='PostSpansRequest', + full_name='jaeger.api_v2.PostSpansRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='batch', full_name='jaeger.api_v2.PostSpansRequest.batch', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=145, + serialized_end=206, +) + + +_POSTSPANSRESPONSE = _descriptor.Descriptor( + name='PostSpansResponse', + full_name='jaeger.api_v2.PostSpansResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=208, + serialized_end=227, +) + +_POSTSPANSREQUEST.fields_by_name['batch'].message_type = model__pb2._BATCH +DESCRIPTOR.message_types_by_name['PostSpansRequest'] = _POSTSPANSREQUEST +DESCRIPTOR.message_types_by_name['PostSpansResponse'] = _POSTSPANSRESPONSE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +PostSpansRequest = _reflection.GeneratedProtocolMessageType('PostSpansRequest', (_message.Message,), { + 'DESCRIPTOR' : _POSTSPANSREQUEST, + '__module__' : 'collector_pb2' + # @@protoc_insertion_point(class_scope:jaeger.api_v2.PostSpansRequest) + }) +_sym_db.RegisterMessage(PostSpansRequest) + +PostSpansResponse = _reflection.GeneratedProtocolMessageType('PostSpansResponse', (_message.Message,), { + 'DESCRIPTOR' : _POSTSPANSRESPONSE, + '__module__' : 'collector_pb2' + # @@protoc_insertion_point(class_scope:jaeger.api_v2.PostSpansResponse) + }) +_sym_db.RegisterMessage(PostSpansResponse) + + +DESCRIPTOR._options = None +_POSTSPANSREQUEST.fields_by_name['batch']._options = None + +_COLLECTORSERVICE = _descriptor.ServiceDescriptor( + name='CollectorService', + full_name='jaeger.api_v2.CollectorService', + file=DESCRIPTOR, + index=0, + serialized_options=None, + serialized_start=229, + serialized_end=353, + methods=[ + _descriptor.MethodDescriptor( + name='PostSpans', + full_name='jaeger.api_v2.CollectorService.PostSpans', + index=0, + containing_service=None, + input_type=_POSTSPANSREQUEST, + output_type=_POSTSPANSRESPONSE, + serialized_options=_b('\202\323\344\223\002\022\"\r/api/v2/spans:\001*'), + ), +]) +_sym_db.RegisterServiceDescriptor(_COLLECTORSERVICE) + +DESCRIPTOR.services_by_name['CollectorService'] = _COLLECTORSERVICE + +# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/collector_pb2_grpc.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/collector_pb2_grpc.py new file mode 100644 index 0000000000..b6fcc3592a --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/collector_pb2_grpc.py @@ -0,0 +1,46 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +import grpc + +import collector_pb2 as collector__pb2 + + +class CollectorServiceStub(object): + # missing associated documentation comment in .proto file + pass + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.PostSpans = channel.unary_unary( + '/jaeger.api_v2.CollectorService/PostSpans', + request_serializer=collector__pb2.PostSpansRequest.SerializeToString, + response_deserializer=collector__pb2.PostSpansResponse.FromString, + ) + + +class CollectorServiceServicer(object): + # missing associated documentation comment in .proto file + pass + + def PostSpans(self, request, context): + # missing associated documentation comment in .proto file + pass + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_CollectorServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'PostSpans': grpc.unary_unary_rpc_method_handler( + servicer.PostSpans, + request_deserializer=collector__pb2.PostSpansRequest.FromString, + response_serializer=collector__pb2.PostSpansResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'jaeger.api_v2.CollectorService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/gogoproto/gogo_pb2.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/gogoproto/gogo_pb2.py new file mode 100644 index 0000000000..7268c1b693 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/gogoproto/gogo_pb2.py @@ -0,0 +1,794 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: gogoproto/gogo.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +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() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='gogoproto/gogo.proto', + package='gogoproto', + syntax='proto2', + serialized_options=_b('\n\023com.google.protobufB\nGoGoProtosZ\"github.com/gogo/protobuf/gogoproto'), + serialized_pb=_b('\n\x14gogoproto/gogo.proto\x12\tgogoproto\x1a google/protobuf/descriptor.proto:;\n\x13goproto_enum_prefix\x12\x1c.google.protobuf.EnumOptions\x18\xb1\xe4\x03 \x01(\x08:=\n\x15goproto_enum_stringer\x12\x1c.google.protobuf.EnumOptions\x18\xc5\xe4\x03 \x01(\x08:5\n\renum_stringer\x12\x1c.google.protobuf.EnumOptions\x18\xc6\xe4\x03 \x01(\x08:7\n\x0f\x65num_customname\x12\x1c.google.protobuf.EnumOptions\x18\xc7\xe4\x03 \x01(\t:0\n\x08\x65numdecl\x12\x1c.google.protobuf.EnumOptions\x18\xc8\xe4\x03 \x01(\x08:A\n\x14\x65numvalue_customname\x12!.google.protobuf.EnumValueOptions\x18\xd1\x83\x04 \x01(\t:;\n\x13goproto_getters_all\x12\x1c.google.protobuf.FileOptions\x18\x99\xec\x03 \x01(\x08:?\n\x17goproto_enum_prefix_all\x12\x1c.google.protobuf.FileOptions\x18\x9a\xec\x03 \x01(\x08:<\n\x14goproto_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\x9b\xec\x03 \x01(\x08:9\n\x11verbose_equal_all\x12\x1c.google.protobuf.FileOptions\x18\x9c\xec\x03 \x01(\x08:0\n\x08\x66\x61\x63\x65_all\x12\x1c.google.protobuf.FileOptions\x18\x9d\xec\x03 \x01(\x08:4\n\x0cgostring_all\x12\x1c.google.protobuf.FileOptions\x18\x9e\xec\x03 \x01(\x08:4\n\x0cpopulate_all\x12\x1c.google.protobuf.FileOptions\x18\x9f\xec\x03 \x01(\x08:4\n\x0cstringer_all\x12\x1c.google.protobuf.FileOptions\x18\xa0\xec\x03 \x01(\x08:3\n\x0bonlyone_all\x12\x1c.google.protobuf.FileOptions\x18\xa1\xec\x03 \x01(\x08:1\n\tequal_all\x12\x1c.google.protobuf.FileOptions\x18\xa5\xec\x03 \x01(\x08:7\n\x0f\x64\x65scription_all\x12\x1c.google.protobuf.FileOptions\x18\xa6\xec\x03 \x01(\x08:3\n\x0btestgen_all\x12\x1c.google.protobuf.FileOptions\x18\xa7\xec\x03 \x01(\x08:4\n\x0c\x62\x65nchgen_all\x12\x1c.google.protobuf.FileOptions\x18\xa8\xec\x03 \x01(\x08:5\n\rmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xa9\xec\x03 \x01(\x08:7\n\x0funmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xaa\xec\x03 \x01(\x08:<\n\x14stable_marshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xab\xec\x03 \x01(\x08:1\n\tsizer_all\x12\x1c.google.protobuf.FileOptions\x18\xac\xec\x03 \x01(\x08:A\n\x19goproto_enum_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\xad\xec\x03 \x01(\x08:9\n\x11\x65num_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\xae\xec\x03 \x01(\x08:<\n\x14unsafe_marshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xaf\xec\x03 \x01(\x08:>\n\x16unsafe_unmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xb0\xec\x03 \x01(\x08:B\n\x1agoproto_extensions_map_all\x12\x1c.google.protobuf.FileOptions\x18\xb1\xec\x03 \x01(\x08:@\n\x18goproto_unrecognized_all\x12\x1c.google.protobuf.FileOptions\x18\xb2\xec\x03 \x01(\x08:8\n\x10gogoproto_import\x12\x1c.google.protobuf.FileOptions\x18\xb3\xec\x03 \x01(\x08:6\n\x0eprotosizer_all\x12\x1c.google.protobuf.FileOptions\x18\xb4\xec\x03 \x01(\x08:3\n\x0b\x63ompare_all\x12\x1c.google.protobuf.FileOptions\x18\xb5\xec\x03 \x01(\x08:4\n\x0ctypedecl_all\x12\x1c.google.protobuf.FileOptions\x18\xb6\xec\x03 \x01(\x08:4\n\x0c\x65numdecl_all\x12\x1c.google.protobuf.FileOptions\x18\xb7\xec\x03 \x01(\x08:<\n\x14goproto_registration\x12\x1c.google.protobuf.FileOptions\x18\xb8\xec\x03 \x01(\x08:7\n\x0fmessagename_all\x12\x1c.google.protobuf.FileOptions\x18\xb9\xec\x03 \x01(\x08:=\n\x15goproto_sizecache_all\x12\x1c.google.protobuf.FileOptions\x18\xba\xec\x03 \x01(\x08:;\n\x13goproto_unkeyed_all\x12\x1c.google.protobuf.FileOptions\x18\xbb\xec\x03 \x01(\x08::\n\x0fgoproto_getters\x12\x1f.google.protobuf.MessageOptions\x18\x81\xf4\x03 \x01(\x08:;\n\x10goproto_stringer\x12\x1f.google.protobuf.MessageOptions\x18\x83\xf4\x03 \x01(\x08:8\n\rverbose_equal\x12\x1f.google.protobuf.MessageOptions\x18\x84\xf4\x03 \x01(\x08:/\n\x04\x66\x61\x63\x65\x12\x1f.google.protobuf.MessageOptions\x18\x85\xf4\x03 \x01(\x08:3\n\x08gostring\x12\x1f.google.protobuf.MessageOptions\x18\x86\xf4\x03 \x01(\x08:3\n\x08populate\x12\x1f.google.protobuf.MessageOptions\x18\x87\xf4\x03 \x01(\x08:3\n\x08stringer\x12\x1f.google.protobuf.MessageOptions\x18\xc0\x8b\x04 \x01(\x08:2\n\x07onlyone\x12\x1f.google.protobuf.MessageOptions\x18\x89\xf4\x03 \x01(\x08:0\n\x05\x65qual\x12\x1f.google.protobuf.MessageOptions\x18\x8d\xf4\x03 \x01(\x08:6\n\x0b\x64\x65scription\x12\x1f.google.protobuf.MessageOptions\x18\x8e\xf4\x03 \x01(\x08:2\n\x07testgen\x12\x1f.google.protobuf.MessageOptions\x18\x8f\xf4\x03 \x01(\x08:3\n\x08\x62\x65nchgen\x12\x1f.google.protobuf.MessageOptions\x18\x90\xf4\x03 \x01(\x08:4\n\tmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x91\xf4\x03 \x01(\x08:6\n\x0bunmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x92\xf4\x03 \x01(\x08:;\n\x10stable_marshaler\x12\x1f.google.protobuf.MessageOptions\x18\x93\xf4\x03 \x01(\x08:0\n\x05sizer\x12\x1f.google.protobuf.MessageOptions\x18\x94\xf4\x03 \x01(\x08:;\n\x10unsafe_marshaler\x12\x1f.google.protobuf.MessageOptions\x18\x97\xf4\x03 \x01(\x08:=\n\x12unsafe_unmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x98\xf4\x03 \x01(\x08:A\n\x16goproto_extensions_map\x12\x1f.google.protobuf.MessageOptions\x18\x99\xf4\x03 \x01(\x08:?\n\x14goproto_unrecognized\x12\x1f.google.protobuf.MessageOptions\x18\x9a\xf4\x03 \x01(\x08:5\n\nprotosizer\x12\x1f.google.protobuf.MessageOptions\x18\x9c\xf4\x03 \x01(\x08:2\n\x07\x63ompare\x12\x1f.google.protobuf.MessageOptions\x18\x9d\xf4\x03 \x01(\x08:3\n\x08typedecl\x12\x1f.google.protobuf.MessageOptions\x18\x9e\xf4\x03 \x01(\x08:6\n\x0bmessagename\x12\x1f.google.protobuf.MessageOptions\x18\xa1\xf4\x03 \x01(\x08:<\n\x11goproto_sizecache\x12\x1f.google.protobuf.MessageOptions\x18\xa2\xf4\x03 \x01(\x08::\n\x0fgoproto_unkeyed\x12\x1f.google.protobuf.MessageOptions\x18\xa3\xf4\x03 \x01(\x08:1\n\x08nullable\x12\x1d.google.protobuf.FieldOptions\x18\xe9\xfb\x03 \x01(\x08:.\n\x05\x65mbed\x12\x1d.google.protobuf.FieldOptions\x18\xea\xfb\x03 \x01(\x08:3\n\ncustomtype\x12\x1d.google.protobuf.FieldOptions\x18\xeb\xfb\x03 \x01(\t:3\n\ncustomname\x12\x1d.google.protobuf.FieldOptions\x18\xec\xfb\x03 \x01(\t:0\n\x07jsontag\x12\x1d.google.protobuf.FieldOptions\x18\xed\xfb\x03 \x01(\t:1\n\x08moretags\x12\x1d.google.protobuf.FieldOptions\x18\xee\xfb\x03 \x01(\t:1\n\x08\x63\x61sttype\x12\x1d.google.protobuf.FieldOptions\x18\xef\xfb\x03 \x01(\t:0\n\x07\x63\x61stkey\x12\x1d.google.protobuf.FieldOptions\x18\xf0\xfb\x03 \x01(\t:2\n\tcastvalue\x12\x1d.google.protobuf.FieldOptions\x18\xf1\xfb\x03 \x01(\t:0\n\x07stdtime\x12\x1d.google.protobuf.FieldOptions\x18\xf2\xfb\x03 \x01(\x08:4\n\x0bstdduration\x12\x1d.google.protobuf.FieldOptions\x18\xf3\xfb\x03 \x01(\x08:3\n\nwktpointer\x12\x1d.google.protobuf.FieldOptions\x18\xf4\xfb\x03 \x01(\x08\x42\x45\n\x13\x63om.google.protobufB\nGoGoProtosZ\"github.com/gogo/protobuf/gogoproto') + , + dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,]) + + +GOPROTO_ENUM_PREFIX_FIELD_NUMBER = 62001 +goproto_enum_prefix = _descriptor.FieldDescriptor( + name='goproto_enum_prefix', full_name='gogoproto.goproto_enum_prefix', index=0, + number=62001, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_ENUM_STRINGER_FIELD_NUMBER = 62021 +goproto_enum_stringer = _descriptor.FieldDescriptor( + name='goproto_enum_stringer', full_name='gogoproto.goproto_enum_stringer', index=1, + number=62021, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +ENUM_STRINGER_FIELD_NUMBER = 62022 +enum_stringer = _descriptor.FieldDescriptor( + name='enum_stringer', full_name='gogoproto.enum_stringer', index=2, + number=62022, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +ENUM_CUSTOMNAME_FIELD_NUMBER = 62023 +enum_customname = _descriptor.FieldDescriptor( + name='enum_customname', full_name='gogoproto.enum_customname', index=3, + number=62023, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +ENUMDECL_FIELD_NUMBER = 62024 +enumdecl = _descriptor.FieldDescriptor( + name='enumdecl', full_name='gogoproto.enumdecl', index=4, + number=62024, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +ENUMVALUE_CUSTOMNAME_FIELD_NUMBER = 66001 +enumvalue_customname = _descriptor.FieldDescriptor( + name='enumvalue_customname', full_name='gogoproto.enumvalue_customname', index=5, + number=66001, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_GETTERS_ALL_FIELD_NUMBER = 63001 +goproto_getters_all = _descriptor.FieldDescriptor( + name='goproto_getters_all', full_name='gogoproto.goproto_getters_all', index=6, + number=63001, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_ENUM_PREFIX_ALL_FIELD_NUMBER = 63002 +goproto_enum_prefix_all = _descriptor.FieldDescriptor( + name='goproto_enum_prefix_all', full_name='gogoproto.goproto_enum_prefix_all', index=7, + number=63002, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_STRINGER_ALL_FIELD_NUMBER = 63003 +goproto_stringer_all = _descriptor.FieldDescriptor( + name='goproto_stringer_all', full_name='gogoproto.goproto_stringer_all', index=8, + number=63003, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +VERBOSE_EQUAL_ALL_FIELD_NUMBER = 63004 +verbose_equal_all = _descriptor.FieldDescriptor( + name='verbose_equal_all', full_name='gogoproto.verbose_equal_all', index=9, + number=63004, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +FACE_ALL_FIELD_NUMBER = 63005 +face_all = _descriptor.FieldDescriptor( + name='face_all', full_name='gogoproto.face_all', index=10, + number=63005, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOSTRING_ALL_FIELD_NUMBER = 63006 +gostring_all = _descriptor.FieldDescriptor( + name='gostring_all', full_name='gogoproto.gostring_all', index=11, + number=63006, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +POPULATE_ALL_FIELD_NUMBER = 63007 +populate_all = _descriptor.FieldDescriptor( + name='populate_all', full_name='gogoproto.populate_all', index=12, + number=63007, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +STRINGER_ALL_FIELD_NUMBER = 63008 +stringer_all = _descriptor.FieldDescriptor( + name='stringer_all', full_name='gogoproto.stringer_all', index=13, + number=63008, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +ONLYONE_ALL_FIELD_NUMBER = 63009 +onlyone_all = _descriptor.FieldDescriptor( + name='onlyone_all', full_name='gogoproto.onlyone_all', index=14, + number=63009, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +EQUAL_ALL_FIELD_NUMBER = 63013 +equal_all = _descriptor.FieldDescriptor( + name='equal_all', full_name='gogoproto.equal_all', index=15, + number=63013, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +DESCRIPTION_ALL_FIELD_NUMBER = 63014 +description_all = _descriptor.FieldDescriptor( + name='description_all', full_name='gogoproto.description_all', index=16, + number=63014, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +TESTGEN_ALL_FIELD_NUMBER = 63015 +testgen_all = _descriptor.FieldDescriptor( + name='testgen_all', full_name='gogoproto.testgen_all', index=17, + number=63015, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +BENCHGEN_ALL_FIELD_NUMBER = 63016 +benchgen_all = _descriptor.FieldDescriptor( + name='benchgen_all', full_name='gogoproto.benchgen_all', index=18, + number=63016, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +MARSHALER_ALL_FIELD_NUMBER = 63017 +marshaler_all = _descriptor.FieldDescriptor( + name='marshaler_all', full_name='gogoproto.marshaler_all', index=19, + number=63017, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +UNMARSHALER_ALL_FIELD_NUMBER = 63018 +unmarshaler_all = _descriptor.FieldDescriptor( + name='unmarshaler_all', full_name='gogoproto.unmarshaler_all', index=20, + number=63018, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +STABLE_MARSHALER_ALL_FIELD_NUMBER = 63019 +stable_marshaler_all = _descriptor.FieldDescriptor( + name='stable_marshaler_all', full_name='gogoproto.stable_marshaler_all', index=21, + number=63019, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +SIZER_ALL_FIELD_NUMBER = 63020 +sizer_all = _descriptor.FieldDescriptor( + name='sizer_all', full_name='gogoproto.sizer_all', index=22, + number=63020, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_ENUM_STRINGER_ALL_FIELD_NUMBER = 63021 +goproto_enum_stringer_all = _descriptor.FieldDescriptor( + name='goproto_enum_stringer_all', full_name='gogoproto.goproto_enum_stringer_all', index=23, + number=63021, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +ENUM_STRINGER_ALL_FIELD_NUMBER = 63022 +enum_stringer_all = _descriptor.FieldDescriptor( + name='enum_stringer_all', full_name='gogoproto.enum_stringer_all', index=24, + number=63022, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +UNSAFE_MARSHALER_ALL_FIELD_NUMBER = 63023 +unsafe_marshaler_all = _descriptor.FieldDescriptor( + name='unsafe_marshaler_all', full_name='gogoproto.unsafe_marshaler_all', index=25, + number=63023, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +UNSAFE_UNMARSHALER_ALL_FIELD_NUMBER = 63024 +unsafe_unmarshaler_all = _descriptor.FieldDescriptor( + name='unsafe_unmarshaler_all', full_name='gogoproto.unsafe_unmarshaler_all', index=26, + number=63024, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_EXTENSIONS_MAP_ALL_FIELD_NUMBER = 63025 +goproto_extensions_map_all = _descriptor.FieldDescriptor( + name='goproto_extensions_map_all', full_name='gogoproto.goproto_extensions_map_all', index=27, + number=63025, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_UNRECOGNIZED_ALL_FIELD_NUMBER = 63026 +goproto_unrecognized_all = _descriptor.FieldDescriptor( + name='goproto_unrecognized_all', full_name='gogoproto.goproto_unrecognized_all', index=28, + number=63026, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOGOPROTO_IMPORT_FIELD_NUMBER = 63027 +gogoproto_import = _descriptor.FieldDescriptor( + name='gogoproto_import', full_name='gogoproto.gogoproto_import', index=29, + number=63027, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +PROTOSIZER_ALL_FIELD_NUMBER = 63028 +protosizer_all = _descriptor.FieldDescriptor( + name='protosizer_all', full_name='gogoproto.protosizer_all', index=30, + number=63028, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +COMPARE_ALL_FIELD_NUMBER = 63029 +compare_all = _descriptor.FieldDescriptor( + name='compare_all', full_name='gogoproto.compare_all', index=31, + number=63029, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +TYPEDECL_ALL_FIELD_NUMBER = 63030 +typedecl_all = _descriptor.FieldDescriptor( + name='typedecl_all', full_name='gogoproto.typedecl_all', index=32, + number=63030, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +ENUMDECL_ALL_FIELD_NUMBER = 63031 +enumdecl_all = _descriptor.FieldDescriptor( + name='enumdecl_all', full_name='gogoproto.enumdecl_all', index=33, + number=63031, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_REGISTRATION_FIELD_NUMBER = 63032 +goproto_registration = _descriptor.FieldDescriptor( + name='goproto_registration', full_name='gogoproto.goproto_registration', index=34, + number=63032, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +MESSAGENAME_ALL_FIELD_NUMBER = 63033 +messagename_all = _descriptor.FieldDescriptor( + name='messagename_all', full_name='gogoproto.messagename_all', index=35, + number=63033, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_SIZECACHE_ALL_FIELD_NUMBER = 63034 +goproto_sizecache_all = _descriptor.FieldDescriptor( + name='goproto_sizecache_all', full_name='gogoproto.goproto_sizecache_all', index=36, + number=63034, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_UNKEYED_ALL_FIELD_NUMBER = 63035 +goproto_unkeyed_all = _descriptor.FieldDescriptor( + name='goproto_unkeyed_all', full_name='gogoproto.goproto_unkeyed_all', index=37, + number=63035, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_GETTERS_FIELD_NUMBER = 64001 +goproto_getters = _descriptor.FieldDescriptor( + name='goproto_getters', full_name='gogoproto.goproto_getters', index=38, + number=64001, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_STRINGER_FIELD_NUMBER = 64003 +goproto_stringer = _descriptor.FieldDescriptor( + name='goproto_stringer', full_name='gogoproto.goproto_stringer', index=39, + number=64003, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +VERBOSE_EQUAL_FIELD_NUMBER = 64004 +verbose_equal = _descriptor.FieldDescriptor( + name='verbose_equal', full_name='gogoproto.verbose_equal', index=40, + number=64004, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +FACE_FIELD_NUMBER = 64005 +face = _descriptor.FieldDescriptor( + name='face', full_name='gogoproto.face', index=41, + number=64005, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOSTRING_FIELD_NUMBER = 64006 +gostring = _descriptor.FieldDescriptor( + name='gostring', full_name='gogoproto.gostring', index=42, + number=64006, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +POPULATE_FIELD_NUMBER = 64007 +populate = _descriptor.FieldDescriptor( + name='populate', full_name='gogoproto.populate', index=43, + number=64007, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +STRINGER_FIELD_NUMBER = 67008 +stringer = _descriptor.FieldDescriptor( + name='stringer', full_name='gogoproto.stringer', index=44, + number=67008, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +ONLYONE_FIELD_NUMBER = 64009 +onlyone = _descriptor.FieldDescriptor( + name='onlyone', full_name='gogoproto.onlyone', index=45, + number=64009, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +EQUAL_FIELD_NUMBER = 64013 +equal = _descriptor.FieldDescriptor( + name='equal', full_name='gogoproto.equal', index=46, + number=64013, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +DESCRIPTION_FIELD_NUMBER = 64014 +description = _descriptor.FieldDescriptor( + name='description', full_name='gogoproto.description', index=47, + number=64014, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +TESTGEN_FIELD_NUMBER = 64015 +testgen = _descriptor.FieldDescriptor( + name='testgen', full_name='gogoproto.testgen', index=48, + number=64015, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +BENCHGEN_FIELD_NUMBER = 64016 +benchgen = _descriptor.FieldDescriptor( + name='benchgen', full_name='gogoproto.benchgen', index=49, + number=64016, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +MARSHALER_FIELD_NUMBER = 64017 +marshaler = _descriptor.FieldDescriptor( + name='marshaler', full_name='gogoproto.marshaler', index=50, + number=64017, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +UNMARSHALER_FIELD_NUMBER = 64018 +unmarshaler = _descriptor.FieldDescriptor( + name='unmarshaler', full_name='gogoproto.unmarshaler', index=51, + number=64018, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +STABLE_MARSHALER_FIELD_NUMBER = 64019 +stable_marshaler = _descriptor.FieldDescriptor( + name='stable_marshaler', full_name='gogoproto.stable_marshaler', index=52, + number=64019, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +SIZER_FIELD_NUMBER = 64020 +sizer = _descriptor.FieldDescriptor( + name='sizer', full_name='gogoproto.sizer', index=53, + number=64020, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +UNSAFE_MARSHALER_FIELD_NUMBER = 64023 +unsafe_marshaler = _descriptor.FieldDescriptor( + name='unsafe_marshaler', full_name='gogoproto.unsafe_marshaler', index=54, + number=64023, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +UNSAFE_UNMARSHALER_FIELD_NUMBER = 64024 +unsafe_unmarshaler = _descriptor.FieldDescriptor( + name='unsafe_unmarshaler', full_name='gogoproto.unsafe_unmarshaler', index=55, + number=64024, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_EXTENSIONS_MAP_FIELD_NUMBER = 64025 +goproto_extensions_map = _descriptor.FieldDescriptor( + name='goproto_extensions_map', full_name='gogoproto.goproto_extensions_map', index=56, + number=64025, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_UNRECOGNIZED_FIELD_NUMBER = 64026 +goproto_unrecognized = _descriptor.FieldDescriptor( + name='goproto_unrecognized', full_name='gogoproto.goproto_unrecognized', index=57, + number=64026, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +PROTOSIZER_FIELD_NUMBER = 64028 +protosizer = _descriptor.FieldDescriptor( + name='protosizer', full_name='gogoproto.protosizer', index=58, + number=64028, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +COMPARE_FIELD_NUMBER = 64029 +compare = _descriptor.FieldDescriptor( + name='compare', full_name='gogoproto.compare', index=59, + number=64029, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +TYPEDECL_FIELD_NUMBER = 64030 +typedecl = _descriptor.FieldDescriptor( + name='typedecl', full_name='gogoproto.typedecl', index=60, + number=64030, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +MESSAGENAME_FIELD_NUMBER = 64033 +messagename = _descriptor.FieldDescriptor( + name='messagename', full_name='gogoproto.messagename', index=61, + number=64033, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_SIZECACHE_FIELD_NUMBER = 64034 +goproto_sizecache = _descriptor.FieldDescriptor( + name='goproto_sizecache', full_name='gogoproto.goproto_sizecache', index=62, + number=64034, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +GOPROTO_UNKEYED_FIELD_NUMBER = 64035 +goproto_unkeyed = _descriptor.FieldDescriptor( + name='goproto_unkeyed', full_name='gogoproto.goproto_unkeyed', index=63, + number=64035, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +NULLABLE_FIELD_NUMBER = 65001 +nullable = _descriptor.FieldDescriptor( + name='nullable', full_name='gogoproto.nullable', index=64, + number=65001, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +EMBED_FIELD_NUMBER = 65002 +embed = _descriptor.FieldDescriptor( + name='embed', full_name='gogoproto.embed', index=65, + number=65002, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +CUSTOMTYPE_FIELD_NUMBER = 65003 +customtype = _descriptor.FieldDescriptor( + name='customtype', full_name='gogoproto.customtype', index=66, + number=65003, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +CUSTOMNAME_FIELD_NUMBER = 65004 +customname = _descriptor.FieldDescriptor( + name='customname', full_name='gogoproto.customname', index=67, + number=65004, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +JSONTAG_FIELD_NUMBER = 65005 +jsontag = _descriptor.FieldDescriptor( + name='jsontag', full_name='gogoproto.jsontag', index=68, + number=65005, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +MORETAGS_FIELD_NUMBER = 65006 +moretags = _descriptor.FieldDescriptor( + name='moretags', full_name='gogoproto.moretags', index=69, + number=65006, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +CASTTYPE_FIELD_NUMBER = 65007 +casttype = _descriptor.FieldDescriptor( + name='casttype', full_name='gogoproto.casttype', index=70, + number=65007, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +CASTKEY_FIELD_NUMBER = 65008 +castkey = _descriptor.FieldDescriptor( + name='castkey', full_name='gogoproto.castkey', index=71, + number=65008, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +CASTVALUE_FIELD_NUMBER = 65009 +castvalue = _descriptor.FieldDescriptor( + name='castvalue', full_name='gogoproto.castvalue', index=72, + number=65009, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +STDTIME_FIELD_NUMBER = 65010 +stdtime = _descriptor.FieldDescriptor( + name='stdtime', full_name='gogoproto.stdtime', index=73, + number=65010, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +STDDURATION_FIELD_NUMBER = 65011 +stdduration = _descriptor.FieldDescriptor( + name='stdduration', full_name='gogoproto.stdduration', index=74, + number=65011, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +WKTPOINTER_FIELD_NUMBER = 65012 +wktpointer = _descriptor.FieldDescriptor( + name='wktpointer', full_name='gogoproto.wktpointer', index=75, + number=65012, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) + +DESCRIPTOR.extensions_by_name['goproto_enum_prefix'] = goproto_enum_prefix +DESCRIPTOR.extensions_by_name['goproto_enum_stringer'] = goproto_enum_stringer +DESCRIPTOR.extensions_by_name['enum_stringer'] = enum_stringer +DESCRIPTOR.extensions_by_name['enum_customname'] = enum_customname +DESCRIPTOR.extensions_by_name['enumdecl'] = enumdecl +DESCRIPTOR.extensions_by_name['enumvalue_customname'] = enumvalue_customname +DESCRIPTOR.extensions_by_name['goproto_getters_all'] = goproto_getters_all +DESCRIPTOR.extensions_by_name['goproto_enum_prefix_all'] = goproto_enum_prefix_all +DESCRIPTOR.extensions_by_name['goproto_stringer_all'] = goproto_stringer_all +DESCRIPTOR.extensions_by_name['verbose_equal_all'] = verbose_equal_all +DESCRIPTOR.extensions_by_name['face_all'] = face_all +DESCRIPTOR.extensions_by_name['gostring_all'] = gostring_all +DESCRIPTOR.extensions_by_name['populate_all'] = populate_all +DESCRIPTOR.extensions_by_name['stringer_all'] = stringer_all +DESCRIPTOR.extensions_by_name['onlyone_all'] = onlyone_all +DESCRIPTOR.extensions_by_name['equal_all'] = equal_all +DESCRIPTOR.extensions_by_name['description_all'] = description_all +DESCRIPTOR.extensions_by_name['testgen_all'] = testgen_all +DESCRIPTOR.extensions_by_name['benchgen_all'] = benchgen_all +DESCRIPTOR.extensions_by_name['marshaler_all'] = marshaler_all +DESCRIPTOR.extensions_by_name['unmarshaler_all'] = unmarshaler_all +DESCRIPTOR.extensions_by_name['stable_marshaler_all'] = stable_marshaler_all +DESCRIPTOR.extensions_by_name['sizer_all'] = sizer_all +DESCRIPTOR.extensions_by_name['goproto_enum_stringer_all'] = goproto_enum_stringer_all +DESCRIPTOR.extensions_by_name['enum_stringer_all'] = enum_stringer_all +DESCRIPTOR.extensions_by_name['unsafe_marshaler_all'] = unsafe_marshaler_all +DESCRIPTOR.extensions_by_name['unsafe_unmarshaler_all'] = unsafe_unmarshaler_all +DESCRIPTOR.extensions_by_name['goproto_extensions_map_all'] = goproto_extensions_map_all +DESCRIPTOR.extensions_by_name['goproto_unrecognized_all'] = goproto_unrecognized_all +DESCRIPTOR.extensions_by_name['gogoproto_import'] = gogoproto_import +DESCRIPTOR.extensions_by_name['protosizer_all'] = protosizer_all +DESCRIPTOR.extensions_by_name['compare_all'] = compare_all +DESCRIPTOR.extensions_by_name['typedecl_all'] = typedecl_all +DESCRIPTOR.extensions_by_name['enumdecl_all'] = enumdecl_all +DESCRIPTOR.extensions_by_name['goproto_registration'] = goproto_registration +DESCRIPTOR.extensions_by_name['messagename_all'] = messagename_all +DESCRIPTOR.extensions_by_name['goproto_sizecache_all'] = goproto_sizecache_all +DESCRIPTOR.extensions_by_name['goproto_unkeyed_all'] = goproto_unkeyed_all +DESCRIPTOR.extensions_by_name['goproto_getters'] = goproto_getters +DESCRIPTOR.extensions_by_name['goproto_stringer'] = goproto_stringer +DESCRIPTOR.extensions_by_name['verbose_equal'] = verbose_equal +DESCRIPTOR.extensions_by_name['face'] = face +DESCRIPTOR.extensions_by_name['gostring'] = gostring +DESCRIPTOR.extensions_by_name['populate'] = populate +DESCRIPTOR.extensions_by_name['stringer'] = stringer +DESCRIPTOR.extensions_by_name['onlyone'] = onlyone +DESCRIPTOR.extensions_by_name['equal'] = equal +DESCRIPTOR.extensions_by_name['description'] = description +DESCRIPTOR.extensions_by_name['testgen'] = testgen +DESCRIPTOR.extensions_by_name['benchgen'] = benchgen +DESCRIPTOR.extensions_by_name['marshaler'] = marshaler +DESCRIPTOR.extensions_by_name['unmarshaler'] = unmarshaler +DESCRIPTOR.extensions_by_name['stable_marshaler'] = stable_marshaler +DESCRIPTOR.extensions_by_name['sizer'] = sizer +DESCRIPTOR.extensions_by_name['unsafe_marshaler'] = unsafe_marshaler +DESCRIPTOR.extensions_by_name['unsafe_unmarshaler'] = unsafe_unmarshaler +DESCRIPTOR.extensions_by_name['goproto_extensions_map'] = goproto_extensions_map +DESCRIPTOR.extensions_by_name['goproto_unrecognized'] = goproto_unrecognized +DESCRIPTOR.extensions_by_name['protosizer'] = protosizer +DESCRIPTOR.extensions_by_name['compare'] = compare +DESCRIPTOR.extensions_by_name['typedecl'] = typedecl +DESCRIPTOR.extensions_by_name['messagename'] = messagename +DESCRIPTOR.extensions_by_name['goproto_sizecache'] = goproto_sizecache +DESCRIPTOR.extensions_by_name['goproto_unkeyed'] = goproto_unkeyed +DESCRIPTOR.extensions_by_name['nullable'] = nullable +DESCRIPTOR.extensions_by_name['embed'] = embed +DESCRIPTOR.extensions_by_name['customtype'] = customtype +DESCRIPTOR.extensions_by_name['customname'] = customname +DESCRIPTOR.extensions_by_name['jsontag'] = jsontag +DESCRIPTOR.extensions_by_name['moretags'] = moretags +DESCRIPTOR.extensions_by_name['casttype'] = casttype +DESCRIPTOR.extensions_by_name['castkey'] = castkey +DESCRIPTOR.extensions_by_name['castvalue'] = castvalue +DESCRIPTOR.extensions_by_name['stdtime'] = stdtime +DESCRIPTOR.extensions_by_name['stdduration'] = stdduration +DESCRIPTOR.extensions_by_name['wktpointer'] = wktpointer +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension(goproto_enum_prefix) +google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension(goproto_enum_stringer) +google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension(enum_stringer) +google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension(enum_customname) +google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension(enumdecl) +google_dot_protobuf_dot_descriptor__pb2.EnumValueOptions.RegisterExtension(enumvalue_customname) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_getters_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_enum_prefix_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_stringer_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(verbose_equal_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(face_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(gostring_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(populate_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(stringer_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(onlyone_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(equal_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(description_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(testgen_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(benchgen_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(marshaler_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(unmarshaler_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(stable_marshaler_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(sizer_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_enum_stringer_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(enum_stringer_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(unsafe_marshaler_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(unsafe_unmarshaler_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_extensions_map_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_unrecognized_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(gogoproto_import) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(protosizer_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(compare_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(typedecl_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(enumdecl_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_registration) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(messagename_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_sizecache_all) +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_unkeyed_all) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(goproto_getters) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(goproto_stringer) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(verbose_equal) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(face) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(gostring) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(populate) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(stringer) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(onlyone) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(equal) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(description) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(testgen) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(benchgen) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(marshaler) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(unmarshaler) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(stable_marshaler) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(sizer) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(unsafe_marshaler) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(unsafe_unmarshaler) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(goproto_extensions_map) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(goproto_unrecognized) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(protosizer) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(compare) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(typedecl) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(messagename) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(goproto_sizecache) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(goproto_unkeyed) +google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(nullable) +google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(embed) +google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(customtype) +google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(customname) +google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(jsontag) +google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(moretags) +google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(casttype) +google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(castkey) +google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(castvalue) +google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(stdtime) +google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(stdduration) +google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(wktpointer) + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/google/api/annotations_pb2.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/google/api/annotations_pb2.py new file mode 100644 index 0000000000..e72a7f8b74 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/google/api/annotations_pb2.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/api/annotations.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +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() + + +from google.api import http_pb2 as google_dot_api_dot_http__pb2 +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='google/api/annotations.proto', + package='google.api', + syntax='proto3', + serialized_options=_b('\n\016com.google.apiB\020AnnotationsProtoP\001ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\242\002\004GAPI'), + serialized_pb=_b('\n\x1cgoogle/api/annotations.proto\x12\ngoogle.api\x1a\x15google/api/http.proto\x1a google/protobuf/descriptor.proto:E\n\x04http\x12\x1e.google.protobuf.MethodOptions\x18\xb0\xca\xbc\" \x01(\x0b\x32\x14.google.api.HttpRuleBn\n\x0e\x63om.google.apiB\x10\x41nnotationsProtoP\x01ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\xa2\x02\x04GAPIb\x06proto3') + , + dependencies=[google_dot_api_dot_http__pb2.DESCRIPTOR,google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,]) + + +HTTP_FIELD_NUMBER = 72295728 +http = _descriptor.FieldDescriptor( + name='http', full_name='google.api.http', index=0, + number=72295728, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) + +DESCRIPTOR.extensions_by_name['http'] = http +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +http.message_type = google_dot_api_dot_http__pb2._HTTPRULE +google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(http) + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/google/api/http_pb2.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/google/api/http_pb2.py new file mode 100644 index 0000000000..01352aca81 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/google/api/http_pb2.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/api/http.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +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.FileDescriptor( + name='google/api/http.proto', + package='google.api', + syntax='proto3', + serialized_options=_b('\n\016com.google.apiB\tHttpProtoP\001ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\370\001\001\242\002\004GAPI'), + serialized_pb=_b('\n\x15google/api/http.proto\x12\ngoogle.api\"T\n\x04Http\x12#\n\x05rules\x18\x01 \x03(\x0b\x32\x14.google.api.HttpRule\x12\'\n\x1f\x66ully_decode_reserved_expansion\x18\x02 \x01(\x08\"\x81\x02\n\x08HttpRule\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\r\n\x03get\x18\x02 \x01(\tH\x00\x12\r\n\x03put\x18\x03 \x01(\tH\x00\x12\x0e\n\x04post\x18\x04 \x01(\tH\x00\x12\x10\n\x06\x64\x65lete\x18\x05 \x01(\tH\x00\x12\x0f\n\x05patch\x18\x06 \x01(\tH\x00\x12/\n\x06\x63ustom\x18\x08 \x01(\x0b\x32\x1d.google.api.CustomHttpPatternH\x00\x12\x0c\n\x04\x62ody\x18\x07 \x01(\t\x12\x15\n\rresponse_body\x18\x0c \x01(\t\x12\x31\n\x13\x61\x64\x64itional_bindings\x18\x0b \x03(\x0b\x32\x14.google.api.HttpRuleB\t\n\x07pattern\"/\n\x11\x43ustomHttpPattern\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\tBj\n\x0e\x63om.google.apiB\tHttpProtoP\x01ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\xf8\x01\x01\xa2\x02\x04GAPIb\x06proto3') +) + + + + +_HTTP = _descriptor.Descriptor( + name='Http', + full_name='google.api.Http', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='rules', full_name='google.api.Http.rules', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='fully_decode_reserved_expansion', full_name='google.api.Http.fully_decode_reserved_expansion', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=37, + serialized_end=121, +) + + +_HTTPRULE = _descriptor.Descriptor( + name='HttpRule', + full_name='google.api.HttpRule', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='selector', full_name='google.api.HttpRule.selector', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='get', full_name='google.api.HttpRule.get', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='put', full_name='google.api.HttpRule.put', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='post', full_name='google.api.HttpRule.post', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='delete', full_name='google.api.HttpRule.delete', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='patch', full_name='google.api.HttpRule.patch', index=5, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='custom', full_name='google.api.HttpRule.custom', index=6, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='body', full_name='google.api.HttpRule.body', index=7, + number=7, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='response_body', full_name='google.api.HttpRule.response_body', index=8, + number=12, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='additional_bindings', full_name='google.api.HttpRule.additional_bindings', index=9, + number=11, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='pattern', full_name='google.api.HttpRule.pattern', + index=0, containing_type=None, fields=[]), + ], + serialized_start=124, + serialized_end=381, +) + + +_CUSTOMHTTPPATTERN = _descriptor.Descriptor( + name='CustomHttpPattern', + full_name='google.api.CustomHttpPattern', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='kind', full_name='google.api.CustomHttpPattern.kind', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='path', full_name='google.api.CustomHttpPattern.path', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=383, + serialized_end=430, +) + +_HTTP.fields_by_name['rules'].message_type = _HTTPRULE +_HTTPRULE.fields_by_name['custom'].message_type = _CUSTOMHTTPPATTERN +_HTTPRULE.fields_by_name['additional_bindings'].message_type = _HTTPRULE +_HTTPRULE.oneofs_by_name['pattern'].fields.append( + _HTTPRULE.fields_by_name['get']) +_HTTPRULE.fields_by_name['get'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] +_HTTPRULE.oneofs_by_name['pattern'].fields.append( + _HTTPRULE.fields_by_name['put']) +_HTTPRULE.fields_by_name['put'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] +_HTTPRULE.oneofs_by_name['pattern'].fields.append( + _HTTPRULE.fields_by_name['post']) +_HTTPRULE.fields_by_name['post'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] +_HTTPRULE.oneofs_by_name['pattern'].fields.append( + _HTTPRULE.fields_by_name['delete']) +_HTTPRULE.fields_by_name['delete'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] +_HTTPRULE.oneofs_by_name['pattern'].fields.append( + _HTTPRULE.fields_by_name['patch']) +_HTTPRULE.fields_by_name['patch'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] +_HTTPRULE.oneofs_by_name['pattern'].fields.append( + _HTTPRULE.fields_by_name['custom']) +_HTTPRULE.fields_by_name['custom'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] +DESCRIPTOR.message_types_by_name['Http'] = _HTTP +DESCRIPTOR.message_types_by_name['HttpRule'] = _HTTPRULE +DESCRIPTOR.message_types_by_name['CustomHttpPattern'] = _CUSTOMHTTPPATTERN +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Http = _reflection.GeneratedProtocolMessageType('Http', (_message.Message,), { + 'DESCRIPTOR' : _HTTP, + '__module__' : 'google.api.http_pb2' + # @@protoc_insertion_point(class_scope:google.api.Http) + }) +_sym_db.RegisterMessage(Http) + +HttpRule = _reflection.GeneratedProtocolMessageType('HttpRule', (_message.Message,), { + 'DESCRIPTOR' : _HTTPRULE, + '__module__' : 'google.api.http_pb2' + # @@protoc_insertion_point(class_scope:google.api.HttpRule) + }) +_sym_db.RegisterMessage(HttpRule) + +CustomHttpPattern = _reflection.GeneratedProtocolMessageType('CustomHttpPattern', (_message.Message,), { + 'DESCRIPTOR' : _CUSTOMHTTPPATTERN, + '__module__' : 'google.api.http_pb2' + # @@protoc_insertion_point(class_scope:google.api.CustomHttpPattern) + }) +_sym_db.RegisterMessage(CustomHttpPattern) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/model_pb2.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/model_pb2.py new file mode 100644 index 0000000000..8abc302a62 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/model_pb2.py @@ -0,0 +1,686 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: model.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +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() + + +from gogoproto import gogo_pb2 as gogoproto_dot_gogo__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='model.proto', + package='jaeger.api_v2', + syntax='proto3', + serialized_options=_b('\n\027io.jaegertracing.api_v2Z\005model\310\342\036\001\320\342\036\001\340\342\036\001\300\343\036\001'), + serialized_pb=_b('\n\x0bmodel.proto\x12\rjaeger.api_v2\x1a\x14gogoproto/gogo.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\"\xa0\x01\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12(\n\x06v_type\x18\x02 \x01(\x0e\x32\x18.jaeger.api_v2.ValueType\x12\r\n\x05v_str\x18\x03 \x01(\t\x12\x0e\n\x06v_bool\x18\x04 \x01(\x08\x12\x0f\n\x07v_int64\x18\x05 \x01(\x03\x12\x11\n\tv_float64\x18\x06 \x01(\x01\x12\x10\n\x08v_binary\x18\x07 \x01(\x0c:\x08\xe8\xa0\x1f\x01\xe8\xa1\x1f\x01\"m\n\x03Log\x12\x37\n\ttimestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x08\x90\xdf\x1f\x01\xc8\xde\x1f\x00\x12-\n\x06\x66ields\x18\x02 \x03(\x0b\x32\x17.jaeger.api_v2.KeyValueB\x04\xc8\xde\x1f\x00\"\x90\x01\n\x07SpanRef\x12,\n\x08trace_id\x18\x01 \x01(\x0c\x42\x1a\xc8\xde\x1f\x00\xda\xde\x1f\x07TraceID\xe2\xde\x1f\x07TraceID\x12)\n\x07span_id\x18\x02 \x01(\x0c\x42\x18\xc8\xde\x1f\x00\xda\xde\x1f\x06SpanID\xe2\xde\x1f\x06SpanID\x12,\n\x08ref_type\x18\x03 \x01(\x0e\x32\x1a.jaeger.api_v2.SpanRefType\"L\n\x07Process\x12\x14\n\x0cservice_name\x18\x01 \x01(\t\x12+\n\x04tags\x18\x02 \x03(\x0b\x32\x17.jaeger.api_v2.KeyValueB\x04\xc8\xde\x1f\x00\"\xeb\x03\n\x04Span\x12,\n\x08trace_id\x18\x01 \x01(\x0c\x42\x1a\xc8\xde\x1f\x00\xda\xde\x1f\x07TraceID\xe2\xde\x1f\x07TraceID\x12)\n\x07span_id\x18\x02 \x01(\x0c\x42\x18\xc8\xde\x1f\x00\xda\xde\x1f\x06SpanID\xe2\xde\x1f\x06SpanID\x12\x16\n\x0eoperation_name\x18\x03 \x01(\t\x12\x30\n\nreferences\x18\x04 \x03(\x0b\x32\x16.jaeger.api_v2.SpanRefB\x04\xc8\xde\x1f\x00\x12\x1c\n\x05\x66lags\x18\x05 \x01(\rB\r\xc8\xde\x1f\x00\xda\xde\x1f\x05\x46lags\x12\x38\n\nstart_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x08\x90\xdf\x1f\x01\xc8\xde\x1f\x00\x12\x35\n\x08\x64uration\x18\x07 \x01(\x0b\x32\x19.google.protobuf.DurationB\x08\x98\xdf\x1f\x01\xc8\xde\x1f\x00\x12+\n\x04tags\x18\x08 \x03(\x0b\x32\x17.jaeger.api_v2.KeyValueB\x04\xc8\xde\x1f\x00\x12&\n\x04logs\x18\t \x03(\x0b\x32\x12.jaeger.api_v2.LogB\x04\xc8\xde\x1f\x00\x12\'\n\x07process\x18\n \x01(\x0b\x32\x16.jaeger.api_v2.Process\x12!\n\nprocess_id\x18\x0b \x01(\tB\r\xe2\xde\x1f\tProcessID\x12\x10\n\x08warnings\x18\x0c \x03(\t\"\xe1\x01\n\x05Trace\x12\"\n\x05spans\x18\x01 \x03(\x0b\x32\x13.jaeger.api_v2.Span\x12>\n\x0bprocess_map\x18\x02 \x03(\x0b\x32#.jaeger.api_v2.Trace.ProcessMappingB\x04\xc8\xde\x1f\x00\x12\x10\n\x08warnings\x18\x03 \x03(\t\x1a\x62\n\x0eProcessMapping\x12!\n\nprocess_id\x18\x01 \x01(\tB\r\xe2\xde\x1f\tProcessID\x12-\n\x07process\x18\x02 \x01(\x0b\x32\x16.jaeger.api_v2.ProcessB\x04\xc8\xde\x1f\x00\"Z\n\x05\x42\x61tch\x12\"\n\x05spans\x18\x01 \x03(\x0b\x32\x13.jaeger.api_v2.Span\x12-\n\x07process\x18\x02 \x01(\x0b\x32\x16.jaeger.api_v2.ProcessB\x04\xc8\xde\x1f\x01\"S\n\x0e\x44\x65pendencyLink\x12\x0e\n\x06parent\x18\x01 \x01(\t\x12\r\n\x05\x63hild\x18\x02 \x01(\t\x12\x12\n\ncall_count\x18\x03 \x01(\x04\x12\x0e\n\x06source\x18\x04 \x01(\t*E\n\tValueType\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x42OOL\x10\x01\x12\t\n\x05INT64\x10\x02\x12\x0b\n\x07\x46LOAT64\x10\x03\x12\n\n\x06\x42INARY\x10\x04*-\n\x0bSpanRefType\x12\x0c\n\x08\x43HILD_OF\x10\x00\x12\x10\n\x0c\x46OLLOWS_FROM\x10\x01\x42\x30\n\x17io.jaegertracing.api_v2Z\x05model\xc8\xe2\x1e\x01\xd0\xe2\x1e\x01\xe0\xe2\x1e\x01\xc0\xe3\x1e\x01\x62\x06proto3') + , + dependencies=[gogoproto_dot_gogo__pb2.DESCRIPTOR,google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,google_dot_protobuf_dot_duration__pb2.DESCRIPTOR,]) + +_VALUETYPE = _descriptor.EnumDescriptor( + name='ValueType', + full_name='jaeger.api_v2.ValueType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='STRING', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='BOOL', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='INT64', index=2, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='FLOAT64', index=3, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='BINARY', index=4, number=4, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=1515, + serialized_end=1584, +) +_sym_db.RegisterEnumDescriptor(_VALUETYPE) + +ValueType = enum_type_wrapper.EnumTypeWrapper(_VALUETYPE) +_SPANREFTYPE = _descriptor.EnumDescriptor( + name='SpanRefType', + full_name='jaeger.api_v2.SpanRefType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='CHILD_OF', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='FOLLOWS_FROM', index=1, number=1, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=1586, + serialized_end=1631, +) +_sym_db.RegisterEnumDescriptor(_SPANREFTYPE) + +SpanRefType = enum_type_wrapper.EnumTypeWrapper(_SPANREFTYPE) +STRING = 0 +BOOL = 1 +INT64 = 2 +FLOAT64 = 3 +BINARY = 4 +CHILD_OF = 0 +FOLLOWS_FROM = 1 + + + +_KEYVALUE = _descriptor.Descriptor( + name='KeyValue', + full_name='jaeger.api_v2.KeyValue', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='jaeger.api_v2.KeyValue.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='v_type', full_name='jaeger.api_v2.KeyValue.v_type', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='v_str', full_name='jaeger.api_v2.KeyValue.v_str', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='v_bool', full_name='jaeger.api_v2.KeyValue.v_bool', index=3, + number=4, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='v_int64', full_name='jaeger.api_v2.KeyValue.v_int64', index=4, + number=5, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='v_float64', full_name='jaeger.api_v2.KeyValue.v_float64', index=5, + number=6, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='v_binary', full_name='jaeger.api_v2.KeyValue.v_binary', index=6, + number=7, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=_b('\350\240\037\001\350\241\037\001'), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=118, + serialized_end=278, +) + + +_LOG = _descriptor.Descriptor( + name='Log', + full_name='jaeger.api_v2.Log', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='timestamp', full_name='jaeger.api_v2.Log.timestamp', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\220\337\037\001\310\336\037\000'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='fields', full_name='jaeger.api_v2.Log.fields', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=280, + serialized_end=389, +) + + +_SPANREF = _descriptor.Descriptor( + name='SpanRef', + full_name='jaeger.api_v2.SpanRef', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='trace_id', full_name='jaeger.api_v2.SpanRef.trace_id', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\000\332\336\037\007TraceID\342\336\037\007TraceID'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='span_id', full_name='jaeger.api_v2.SpanRef.span_id', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\000\332\336\037\006SpanID\342\336\037\006SpanID'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ref_type', full_name='jaeger.api_v2.SpanRef.ref_type', index=2, + number=3, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=392, + serialized_end=536, +) + + +_PROCESS = _descriptor.Descriptor( + name='Process', + full_name='jaeger.api_v2.Process', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='service_name', full_name='jaeger.api_v2.Process.service_name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='tags', full_name='jaeger.api_v2.Process.tags', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=538, + serialized_end=614, +) + + +_SPAN = _descriptor.Descriptor( + name='Span', + full_name='jaeger.api_v2.Span', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='trace_id', full_name='jaeger.api_v2.Span.trace_id', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\000\332\336\037\007TraceID\342\336\037\007TraceID'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='span_id', full_name='jaeger.api_v2.Span.span_id', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\000\332\336\037\006SpanID\342\336\037\006SpanID'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='operation_name', full_name='jaeger.api_v2.Span.operation_name', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='references', full_name='jaeger.api_v2.Span.references', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='flags', full_name='jaeger.api_v2.Span.flags', index=4, + number=5, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\000\332\336\037\005Flags'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='start_time', full_name='jaeger.api_v2.Span.start_time', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\220\337\037\001\310\336\037\000'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='duration', full_name='jaeger.api_v2.Span.duration', index=6, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\230\337\037\001\310\336\037\000'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='tags', full_name='jaeger.api_v2.Span.tags', index=7, + number=8, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='logs', full_name='jaeger.api_v2.Span.logs', index=8, + number=9, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='process', full_name='jaeger.api_v2.Span.process', index=9, + number=10, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='process_id', full_name='jaeger.api_v2.Span.process_id', index=10, + number=11, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\342\336\037\tProcessID'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='warnings', full_name='jaeger.api_v2.Span.warnings', index=11, + number=12, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=617, + serialized_end=1108, +) + + +_TRACE_PROCESSMAPPING = _descriptor.Descriptor( + name='ProcessMapping', + full_name='jaeger.api_v2.Trace.ProcessMapping', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='process_id', full_name='jaeger.api_v2.Trace.ProcessMapping.process_id', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\342\336\037\tProcessID'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='process', full_name='jaeger.api_v2.Trace.ProcessMapping.process', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1238, + serialized_end=1336, +) + +_TRACE = _descriptor.Descriptor( + name='Trace', + full_name='jaeger.api_v2.Trace', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='spans', full_name='jaeger.api_v2.Trace.spans', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='process_map', full_name='jaeger.api_v2.Trace.process_map', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='warnings', full_name='jaeger.api_v2.Trace.warnings', index=2, + number=3, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_TRACE_PROCESSMAPPING, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1111, + serialized_end=1336, +) + + +_BATCH = _descriptor.Descriptor( + name='Batch', + full_name='jaeger.api_v2.Batch', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='spans', full_name='jaeger.api_v2.Batch.spans', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='process', full_name='jaeger.api_v2.Batch.process', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=_b('\310\336\037\001'), file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1338, + serialized_end=1428, +) + + +_DEPENDENCYLINK = _descriptor.Descriptor( + name='DependencyLink', + full_name='jaeger.api_v2.DependencyLink', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='parent', full_name='jaeger.api_v2.DependencyLink.parent', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='child', full_name='jaeger.api_v2.DependencyLink.child', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='call_count', full_name='jaeger.api_v2.DependencyLink.call_count', index=2, + number=3, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='source', full_name='jaeger.api_v2.DependencyLink.source', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1430, + serialized_end=1513, +) + +_KEYVALUE.fields_by_name['v_type'].enum_type = _VALUETYPE +_LOG.fields_by_name['timestamp'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP +_LOG.fields_by_name['fields'].message_type = _KEYVALUE +_SPANREF.fields_by_name['ref_type'].enum_type = _SPANREFTYPE +_PROCESS.fields_by_name['tags'].message_type = _KEYVALUE +_SPAN.fields_by_name['references'].message_type = _SPANREF +_SPAN.fields_by_name['start_time'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP +_SPAN.fields_by_name['duration'].message_type = google_dot_protobuf_dot_duration__pb2._DURATION +_SPAN.fields_by_name['tags'].message_type = _KEYVALUE +_SPAN.fields_by_name['logs'].message_type = _LOG +_SPAN.fields_by_name['process'].message_type = _PROCESS +_TRACE_PROCESSMAPPING.fields_by_name['process'].message_type = _PROCESS +_TRACE_PROCESSMAPPING.containing_type = _TRACE +_TRACE.fields_by_name['spans'].message_type = _SPAN +_TRACE.fields_by_name['process_map'].message_type = _TRACE_PROCESSMAPPING +_BATCH.fields_by_name['spans'].message_type = _SPAN +_BATCH.fields_by_name['process'].message_type = _PROCESS +DESCRIPTOR.message_types_by_name['KeyValue'] = _KEYVALUE +DESCRIPTOR.message_types_by_name['Log'] = _LOG +DESCRIPTOR.message_types_by_name['SpanRef'] = _SPANREF +DESCRIPTOR.message_types_by_name['Process'] = _PROCESS +DESCRIPTOR.message_types_by_name['Span'] = _SPAN +DESCRIPTOR.message_types_by_name['Trace'] = _TRACE +DESCRIPTOR.message_types_by_name['Batch'] = _BATCH +DESCRIPTOR.message_types_by_name['DependencyLink'] = _DEPENDENCYLINK +DESCRIPTOR.enum_types_by_name['ValueType'] = _VALUETYPE +DESCRIPTOR.enum_types_by_name['SpanRefType'] = _SPANREFTYPE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +KeyValue = _reflection.GeneratedProtocolMessageType('KeyValue', (_message.Message,), { + 'DESCRIPTOR' : _KEYVALUE, + '__module__' : 'model_pb2' + # @@protoc_insertion_point(class_scope:jaeger.api_v2.KeyValue) + }) +_sym_db.RegisterMessage(KeyValue) + +Log = _reflection.GeneratedProtocolMessageType('Log', (_message.Message,), { + 'DESCRIPTOR' : _LOG, + '__module__' : 'model_pb2' + # @@protoc_insertion_point(class_scope:jaeger.api_v2.Log) + }) +_sym_db.RegisterMessage(Log) + +SpanRef = _reflection.GeneratedProtocolMessageType('SpanRef', (_message.Message,), { + 'DESCRIPTOR' : _SPANREF, + '__module__' : 'model_pb2' + # @@protoc_insertion_point(class_scope:jaeger.api_v2.SpanRef) + }) +_sym_db.RegisterMessage(SpanRef) + +Process = _reflection.GeneratedProtocolMessageType('Process', (_message.Message,), { + 'DESCRIPTOR' : _PROCESS, + '__module__' : 'model_pb2' + # @@protoc_insertion_point(class_scope:jaeger.api_v2.Process) + }) +_sym_db.RegisterMessage(Process) + +Span = _reflection.GeneratedProtocolMessageType('Span', (_message.Message,), { + 'DESCRIPTOR' : _SPAN, + '__module__' : 'model_pb2' + # @@protoc_insertion_point(class_scope:jaeger.api_v2.Span) + }) +_sym_db.RegisterMessage(Span) + +Trace = _reflection.GeneratedProtocolMessageType('Trace', (_message.Message,), { + + 'ProcessMapping' : _reflection.GeneratedProtocolMessageType('ProcessMapping', (_message.Message,), { + 'DESCRIPTOR' : _TRACE_PROCESSMAPPING, + '__module__' : 'model_pb2' + # @@protoc_insertion_point(class_scope:jaeger.api_v2.Trace.ProcessMapping) + }) + , + 'DESCRIPTOR' : _TRACE, + '__module__' : 'model_pb2' + # @@protoc_insertion_point(class_scope:jaeger.api_v2.Trace) + }) +_sym_db.RegisterMessage(Trace) +_sym_db.RegisterMessage(Trace.ProcessMapping) + +Batch = _reflection.GeneratedProtocolMessageType('Batch', (_message.Message,), { + 'DESCRIPTOR' : _BATCH, + '__module__' : 'model_pb2' + # @@protoc_insertion_point(class_scope:jaeger.api_v2.Batch) + }) +_sym_db.RegisterMessage(Batch) + +DependencyLink = _reflection.GeneratedProtocolMessageType('DependencyLink', (_message.Message,), { + 'DESCRIPTOR' : _DEPENDENCYLINK, + '__module__' : 'model_pb2' + # @@protoc_insertion_point(class_scope:jaeger.api_v2.DependencyLink) + }) +_sym_db.RegisterMessage(DependencyLink) + + +DESCRIPTOR._options = None +_KEYVALUE._options = None +_LOG.fields_by_name['timestamp']._options = None +_LOG.fields_by_name['fields']._options = None +_SPANREF.fields_by_name['trace_id']._options = None +_SPANREF.fields_by_name['span_id']._options = None +_PROCESS.fields_by_name['tags']._options = None +_SPAN.fields_by_name['trace_id']._options = None +_SPAN.fields_by_name['span_id']._options = None +_SPAN.fields_by_name['references']._options = None +_SPAN.fields_by_name['flags']._options = None +_SPAN.fields_by_name['start_time']._options = None +_SPAN.fields_by_name['duration']._options = None +_SPAN.fields_by_name['tags']._options = None +_SPAN.fields_by_name['logs']._options = None +_SPAN.fields_by_name['process_id']._options = None +_TRACE_PROCESSMAPPING.fields_by_name['process_id']._options = None +_TRACE_PROCESSMAPPING.fields_by_name['process']._options = None +_TRACE.fields_by_name['process_map']._options = None +_BATCH.fields_by_name['process']._options = None +# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/protoc_gen_swagger/options/annotations_pb2.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/protoc_gen_swagger/options/annotations_pb2.py new file mode 100644 index 0000000000..42bc642368 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/protoc_gen_swagger/options/annotations_pb2.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: protoc-gen-swagger/options/annotations.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +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() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 +from protoc_gen_swagger.options import openapiv2_pb2 as protoc__gen__swagger_dot_options_dot_openapiv2__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='protoc-gen-swagger/options/annotations.proto', + package='grpc.gateway.protoc_gen_swagger.options', + syntax='proto3', + serialized_options=_b('ZAgithub.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options'), + serialized_pb=_b('\n,protoc-gen-swagger/options/annotations.proto\x12\'grpc.gateway.protoc_gen_swagger.options\x1a google/protobuf/descriptor.proto\x1a*protoc-gen-swagger/options/openapiv2.proto:j\n\x11openapiv2_swagger\x12\x1c.google.protobuf.FileOptions\x18\x92\x08 \x01(\x0b\x32\x30.grpc.gateway.protoc_gen_swagger.options.Swagger:p\n\x13openapiv2_operation\x12\x1e.google.protobuf.MethodOptions\x18\x92\x08 \x01(\x0b\x32\x32.grpc.gateway.protoc_gen_swagger.options.Operation:k\n\x10openapiv2_schema\x12\x1f.google.protobuf.MessageOptions\x18\x92\x08 \x01(\x0b\x32/.grpc.gateway.protoc_gen_swagger.options.Schema:e\n\ropenapiv2_tag\x12\x1f.google.protobuf.ServiceOptions\x18\x92\x08 \x01(\x0b\x32,.grpc.gateway.protoc_gen_swagger.options.Tag:l\n\x0fopenapiv2_field\x12\x1d.google.protobuf.FieldOptions\x18\x92\x08 \x01(\x0b\x32\x33.grpc.gateway.protoc_gen_swagger.options.JSONSchemaBCZAgithub.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/optionsb\x06proto3') + , + dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,protoc__gen__swagger_dot_options_dot_openapiv2__pb2.DESCRIPTOR,]) + + +OPENAPIV2_SWAGGER_FIELD_NUMBER = 1042 +openapiv2_swagger = _descriptor.FieldDescriptor( + name='openapiv2_swagger', full_name='grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger', index=0, + number=1042, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +OPENAPIV2_OPERATION_FIELD_NUMBER = 1042 +openapiv2_operation = _descriptor.FieldDescriptor( + name='openapiv2_operation', full_name='grpc.gateway.protoc_gen_swagger.options.openapiv2_operation', index=1, + number=1042, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +OPENAPIV2_SCHEMA_FIELD_NUMBER = 1042 +openapiv2_schema = _descriptor.FieldDescriptor( + name='openapiv2_schema', full_name='grpc.gateway.protoc_gen_swagger.options.openapiv2_schema', index=2, + number=1042, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +OPENAPIV2_TAG_FIELD_NUMBER = 1042 +openapiv2_tag = _descriptor.FieldDescriptor( + name='openapiv2_tag', full_name='grpc.gateway.protoc_gen_swagger.options.openapiv2_tag', index=3, + number=1042, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +OPENAPIV2_FIELD_FIELD_NUMBER = 1042 +openapiv2_field = _descriptor.FieldDescriptor( + name='openapiv2_field', full_name='grpc.gateway.protoc_gen_swagger.options.openapiv2_field', index=4, + number=1042, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) + +DESCRIPTOR.extensions_by_name['openapiv2_swagger'] = openapiv2_swagger +DESCRIPTOR.extensions_by_name['openapiv2_operation'] = openapiv2_operation +DESCRIPTOR.extensions_by_name['openapiv2_schema'] = openapiv2_schema +DESCRIPTOR.extensions_by_name['openapiv2_tag'] = openapiv2_tag +DESCRIPTOR.extensions_by_name['openapiv2_field'] = openapiv2_field +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +openapiv2_swagger.message_type = protoc__gen__swagger_dot_options_dot_openapiv2__pb2._SWAGGER +google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(openapiv2_swagger) +openapiv2_operation.message_type = protoc__gen__swagger_dot_options_dot_openapiv2__pb2._OPERATION +google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(openapiv2_operation) +openapiv2_schema.message_type = protoc__gen__swagger_dot_options_dot_openapiv2__pb2._SCHEMA +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(openapiv2_schema) +openapiv2_tag.message_type = protoc__gen__swagger_dot_options_dot_openapiv2__pb2._TAG +google_dot_protobuf_dot_descriptor__pb2.ServiceOptions.RegisterExtension(openapiv2_tag) +openapiv2_field.message_type = protoc__gen__swagger_dot_options_dot_openapiv2__pb2._JSONSCHEMA +google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(openapiv2_field) + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/protoc_gen_swagger/options/openapiv2_pb2.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/protoc_gen_swagger/options/openapiv2_pb2.py new file mode 100644 index 0000000000..c2e7aa0ba1 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/protoc_gen_swagger/options/openapiv2_pb2.py @@ -0,0 +1,1777 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: protoc-gen-swagger/options/openapiv2.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +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() + + +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='protoc-gen-swagger/options/openapiv2.proto', + package='grpc.gateway.protoc_gen_swagger.options', + syntax='proto3', + serialized_options=_b('ZAgithub.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options'), + serialized_pb=_b('\n*protoc-gen-swagger/options/openapiv2.proto\x12\'grpc.gateway.protoc_gen_swagger.options\x1a\x19google/protobuf/any.proto\x1a\x1cgoogle/protobuf/struct.proto\"\xa0\x07\n\x07Swagger\x12\x0f\n\x07swagger\x18\x01 \x01(\t\x12;\n\x04info\x18\x02 \x01(\x0b\x32-.grpc.gateway.protoc_gen_swagger.options.Info\x12\x0c\n\x04host\x18\x03 \x01(\t\x12\x11\n\tbase_path\x18\x04 \x01(\t\x12O\n\x07schemes\x18\x05 \x03(\x0e\x32>.grpc.gateway.protoc_gen_swagger.options.Swagger.SwaggerScheme\x12\x10\n\x08\x63onsumes\x18\x06 \x03(\t\x12\x10\n\x08produces\x18\x07 \x03(\t\x12R\n\tresponses\x18\n \x03(\x0b\x32?.grpc.gateway.protoc_gen_swagger.options.Swagger.ResponsesEntry\x12Z\n\x14security_definitions\x18\x0b \x01(\x0b\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions\x12N\n\x08security\x18\x0c \x03(\x0b\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityRequirement\x12U\n\rexternal_docs\x18\x0e \x01(\x0b\x32>.grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation\x12T\n\nextensions\x18\x0f \x03(\x0b\x32@.grpc.gateway.protoc_gen_swagger.options.Swagger.ExtensionsEntry\x1a\x63\n\x0eResponsesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12@\n\x05value\x18\x02 \x01(\x0b\x32\x31.grpc.gateway.protoc_gen_swagger.options.Response:\x02\x38\x01\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"B\n\rSwaggerScheme\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x08\n\x04HTTP\x10\x01\x12\t\n\x05HTTPS\x10\x02\x12\x06\n\x02WS\x10\x03\x12\x07\n\x03WSS\x10\x04J\x04\x08\x08\x10\tJ\x04\x08\t\x10\nJ\x04\x08\r\x10\x0e\"\xa9\x05\n\tOperation\x12\x0c\n\x04tags\x18\x01 \x03(\t\x12\x0f\n\x07summary\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12U\n\rexternal_docs\x18\x04 \x01(\x0b\x32>.grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation\x12\x14\n\x0coperation_id\x18\x05 \x01(\t\x12\x10\n\x08\x63onsumes\x18\x06 \x03(\t\x12\x10\n\x08produces\x18\x07 \x03(\t\x12T\n\tresponses\x18\t \x03(\x0b\x32\x41.grpc.gateway.protoc_gen_swagger.options.Operation.ResponsesEntry\x12\x0f\n\x07schemes\x18\n \x03(\t\x12\x12\n\ndeprecated\x18\x0b \x01(\x08\x12N\n\x08security\x18\x0c \x03(\x0b\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityRequirement\x12V\n\nextensions\x18\r \x03(\x0b\x32\x42.grpc.gateway.protoc_gen_swagger.options.Operation.ExtensionsEntry\x1a\x63\n\x0eResponsesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12@\n\x05value\x18\x02 \x01(\x0b\x32\x31.grpc.gateway.protoc_gen_swagger.options.Response:\x02\x38\x01\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01J\x04\x08\x08\x10\t\"\x8e\x02\n\x08Response\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12?\n\x06schema\x18\x02 \x01(\x0b\x32/.grpc.gateway.protoc_gen_swagger.options.Schema\x12U\n\nextensions\x18\x05 \x03(\x0b\x32\x41.grpc.gateway.protoc_gen_swagger.options.Response.ExtensionsEntry\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01J\x04\x08\x03\x10\x04J\x04\x08\x04\x10\x05\"\xf9\x02\n\x04Info\x12\r\n\x05title\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x18\n\x10terms_of_service\x18\x03 \x01(\t\x12\x41\n\x07\x63ontact\x18\x04 \x01(\x0b\x32\x30.grpc.gateway.protoc_gen_swagger.options.Contact\x12\x41\n\x07license\x18\x05 \x01(\x0b\x32\x30.grpc.gateway.protoc_gen_swagger.options.License\x12\x0f\n\x07version\x18\x06 \x01(\t\x12Q\n\nextensions\x18\x07 \x03(\x0b\x32=.grpc.gateway.protoc_gen_swagger.options.Info.ExtensionsEntry\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"3\n\x07\x43ontact\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\"$\n\x07License\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\"9\n\x15\x45xternalDocumentation\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\"\x80\x02\n\x06Schema\x12H\n\x0bjson_schema\x18\x01 \x01(\x0b\x32\x33.grpc.gateway.protoc_gen_swagger.options.JSONSchema\x12\x15\n\rdiscriminator\x18\x02 \x01(\t\x12\x11\n\tread_only\x18\x03 \x01(\x08\x12U\n\rexternal_docs\x18\x05 \x01(\x0b\x32>.grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation\x12%\n\x07\x65xample\x18\x06 \x01(\x0b\x32\x14.google.protobuf.AnyJ\x04\x08\x04\x10\x05\"\xba\x05\n\nJSONSchema\x12\x0b\n\x03ref\x18\x03 \x01(\t\x12\r\n\x05title\x18\x05 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12\x0f\n\x07\x64\x65\x66\x61ult\x18\x07 \x01(\t\x12\x11\n\tread_only\x18\x08 \x01(\x08\x12\x13\n\x0bmultiple_of\x18\n \x01(\x01\x12\x0f\n\x07maximum\x18\x0b \x01(\x01\x12\x19\n\x11\x65xclusive_maximum\x18\x0c \x01(\x08\x12\x0f\n\x07minimum\x18\r \x01(\x01\x12\x19\n\x11\x65xclusive_minimum\x18\x0e \x01(\x08\x12\x12\n\nmax_length\x18\x0f \x01(\x04\x12\x12\n\nmin_length\x18\x10 \x01(\x04\x12\x0f\n\x07pattern\x18\x11 \x01(\t\x12\x11\n\tmax_items\x18\x14 \x01(\x04\x12\x11\n\tmin_items\x18\x15 \x01(\x04\x12\x14\n\x0cunique_items\x18\x16 \x01(\x08\x12\x16\n\x0emax_properties\x18\x18 \x01(\x04\x12\x16\n\x0emin_properties\x18\x19 \x01(\x04\x12\x10\n\x08required\x18\x1a \x03(\t\x12\r\n\x05\x61rray\x18\" \x03(\t\x12W\n\x04type\x18# \x03(\x0e\x32I.grpc.gateway.protoc_gen_swagger.options.JSONSchema.JSONSchemaSimpleTypes\"w\n\x15JSONSchemaSimpleTypes\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05\x41RRAY\x10\x01\x12\x0b\n\x07\x42OOLEAN\x10\x02\x12\x0b\n\x07INTEGER\x10\x03\x12\x08\n\x04NULL\x10\x04\x12\n\n\x06NUMBER\x10\x05\x12\n\n\x06OBJECT\x10\x06\x12\n\n\x06STRING\x10\x07J\x04\x08\x01\x10\x02J\x04\x08\x02\x10\x03J\x04\x08\x04\x10\x05J\x04\x08\t\x10\nJ\x04\x08\x12\x10\x13J\x04\x08\x13\x10\x14J\x04\x08\x17\x10\x18J\x04\x08\x1b\x10\x1cJ\x04\x08\x1c\x10\x1dJ\x04\x08\x1d\x10\x1eJ\x04\x08\x1e\x10\"J\x04\x08$\x10*J\x04\x08*\x10+J\x04\x08+\x10.\"w\n\x03Tag\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12U\n\rexternal_docs\x18\x03 \x01(\x0b\x32>.grpc.gateway.protoc_gen_swagger.options.ExternalDocumentationJ\x04\x08\x01\x10\x02\"\xdd\x01\n\x13SecurityDefinitions\x12\\\n\x08security\x18\x01 \x03(\x0b\x32J.grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions.SecurityEntry\x1ah\n\rSecurityEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x46\n\x05value\x18\x02 \x01(\x0b\x32\x37.grpc.gateway.protoc_gen_swagger.options.SecurityScheme:\x02\x38\x01\"\x96\x06\n\x0eSecurityScheme\x12J\n\x04type\x18\x01 \x01(\x0e\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityScheme.Type\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x46\n\x02in\x18\x04 \x01(\x0e\x32:.grpc.gateway.protoc_gen_swagger.options.SecurityScheme.In\x12J\n\x04\x66low\x18\x05 \x01(\x0e\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityScheme.Flow\x12\x19\n\x11\x61uthorization_url\x18\x06 \x01(\t\x12\x11\n\ttoken_url\x18\x07 \x01(\t\x12?\n\x06scopes\x18\x08 \x01(\x0b\x32/.grpc.gateway.protoc_gen_swagger.options.Scopes\x12[\n\nextensions\x18\t \x03(\x0b\x32G.grpc.gateway.protoc_gen_swagger.options.SecurityScheme.ExtensionsEntry\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"K\n\x04Type\x12\x10\n\x0cTYPE_INVALID\x10\x00\x12\x0e\n\nTYPE_BASIC\x10\x01\x12\x10\n\x0cTYPE_API_KEY\x10\x02\x12\x0f\n\x0bTYPE_OAUTH2\x10\x03\"1\n\x02In\x12\x0e\n\nIN_INVALID\x10\x00\x12\x0c\n\x08IN_QUERY\x10\x01\x12\r\n\tIN_HEADER\x10\x02\"j\n\x04\x46low\x12\x10\n\x0c\x46LOW_INVALID\x10\x00\x12\x11\n\rFLOW_IMPLICIT\x10\x01\x12\x11\n\rFLOW_PASSWORD\x10\x02\x12\x14\n\x10\x46LOW_APPLICATION\x10\x03\x12\x14\n\x10\x46LOW_ACCESS_CODE\x10\x04\"\xc9\x02\n\x13SecurityRequirement\x12s\n\x14security_requirement\x18\x01 \x03(\x0b\x32U.grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementEntry\x1a)\n\x18SecurityRequirementValue\x12\r\n\x05scope\x18\x01 \x03(\t\x1a\x91\x01\n\x18SecurityRequirementEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x64\n\x05value\x18\x02 \x01(\x0b\x32U.grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementValue:\x02\x38\x01\"\x81\x01\n\x06Scopes\x12I\n\x05scope\x18\x01 \x03(\x0b\x32:.grpc.gateway.protoc_gen_swagger.options.Scopes.ScopeEntry\x1a,\n\nScopeEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x43ZAgithub.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/optionsb\x06proto3') + , + dependencies=[google_dot_protobuf_dot_any__pb2.DESCRIPTOR,google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,]) + + + +_SWAGGER_SWAGGERSCHEME = _descriptor.EnumDescriptor( + name='SwaggerScheme', + full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.SwaggerScheme', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='UNKNOWN', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='HTTP', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='HTTPS', index=2, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='WS', index=3, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='WSS', index=4, number=4, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=989, + serialized_end=1055, +) +_sym_db.RegisterEnumDescriptor(_SWAGGER_SWAGGERSCHEME) + +_JSONSCHEMA_JSONSCHEMASIMPLETYPES = _descriptor.EnumDescriptor( + name='JSONSchemaSimpleTypes', + full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.JSONSchemaSimpleTypes', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='UNKNOWN', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='ARRAY', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='BOOLEAN', index=2, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='INTEGER', index=3, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='NULL', index=4, number=4, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='NUMBER', index=5, number=5, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='OBJECT', index=6, number=6, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='STRING', index=7, number=7, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=3317, + serialized_end=3436, +) +_sym_db.RegisterEnumDescriptor(_JSONSCHEMA_JSONSCHEMASIMPLETYPES) + +_SECURITYSCHEME_TYPE = _descriptor.EnumDescriptor( + name='Type', + full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.Type', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='TYPE_INVALID', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TYPE_BASIC', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TYPE_API_KEY', index=2, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TYPE_OAUTH2', index=3, number=3, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=4424, + serialized_end=4499, +) +_sym_db.RegisterEnumDescriptor(_SECURITYSCHEME_TYPE) + +_SECURITYSCHEME_IN = _descriptor.EnumDescriptor( + name='In', + full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.In', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='IN_INVALID', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='IN_QUERY', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='IN_HEADER', index=2, number=2, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=4501, + serialized_end=4550, +) +_sym_db.RegisterEnumDescriptor(_SECURITYSCHEME_IN) + +_SECURITYSCHEME_FLOW = _descriptor.EnumDescriptor( + name='Flow', + full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.Flow', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='FLOW_INVALID', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='FLOW_IMPLICIT', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='FLOW_PASSWORD', index=2, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='FLOW_APPLICATION', index=3, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='FLOW_ACCESS_CODE', index=4, number=4, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=4552, + serialized_end=4658, +) +_sym_db.RegisterEnumDescriptor(_SECURITYSCHEME_FLOW) + + +_SWAGGER_RESPONSESENTRY = _descriptor.Descriptor( + name='ResponsesEntry', + full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.ResponsesEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.ResponsesEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.ResponsesEntry.value', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=_b('8\001'), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=813, + serialized_end=912, +) + +_SWAGGER_EXTENSIONSENTRY = _descriptor.Descriptor( + name='ExtensionsEntry', + full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.ExtensionsEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.ExtensionsEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.ExtensionsEntry.value', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=_b('8\001'), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=914, + serialized_end=987, +) + +_SWAGGER = _descriptor.Descriptor( + name='Swagger', + full_name='grpc.gateway.protoc_gen_swagger.options.Swagger', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='swagger', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.swagger', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='info', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.info', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='host', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.host', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='base_path', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.base_path', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='schemes', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.schemes', index=4, + number=5, type=14, cpp_type=8, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='consumes', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.consumes', index=5, + number=6, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='produces', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.produces', index=6, + number=7, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='responses', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.responses', index=7, + number=10, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='security_definitions', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.security_definitions', index=8, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='security', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.security', index=9, + number=12, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='external_docs', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.external_docs', index=10, + number=14, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='extensions', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.extensions', index=11, + number=15, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_SWAGGER_RESPONSESENTRY, _SWAGGER_EXTENSIONSENTRY, ], + enum_types=[ + _SWAGGER_SWAGGERSCHEME, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=145, + serialized_end=1073, +) + + +_OPERATION_RESPONSESENTRY = _descriptor.Descriptor( + name='ResponsesEntry', + full_name='grpc.gateway.protoc_gen_swagger.options.Operation.ResponsesEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.ResponsesEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.ResponsesEntry.value', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=_b('8\001'), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=813, + serialized_end=912, +) + +_OPERATION_EXTENSIONSENTRY = _descriptor.Descriptor( + name='ExtensionsEntry', + full_name='grpc.gateway.protoc_gen_swagger.options.Operation.ExtensionsEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.ExtensionsEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.ExtensionsEntry.value', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=_b('8\001'), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=914, + serialized_end=987, +) + +_OPERATION = _descriptor.Descriptor( + name='Operation', + full_name='grpc.gateway.protoc_gen_swagger.options.Operation', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='tags', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.tags', index=0, + number=1, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='summary', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.summary', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='description', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.description', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='external_docs', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.external_docs', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='operation_id', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.operation_id', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='consumes', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.consumes', index=5, + number=6, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='produces', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.produces', index=6, + number=7, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='responses', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.responses', index=7, + number=9, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='schemes', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.schemes', index=8, + number=10, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='deprecated', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.deprecated', index=9, + number=11, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='security', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.security', index=10, + number=12, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='extensions', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.extensions', index=11, + number=13, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_OPERATION_RESPONSESENTRY, _OPERATION_EXTENSIONSENTRY, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1076, + serialized_end=1757, +) + + +_RESPONSE_EXTENSIONSENTRY = _descriptor.Descriptor( + name='ExtensionsEntry', + full_name='grpc.gateway.protoc_gen_swagger.options.Response.ExtensionsEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Response.ExtensionsEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Response.ExtensionsEntry.value', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=_b('8\001'), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=914, + serialized_end=987, +) + +_RESPONSE = _descriptor.Descriptor( + name='Response', + full_name='grpc.gateway.protoc_gen_swagger.options.Response', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='description', full_name='grpc.gateway.protoc_gen_swagger.options.Response.description', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='schema', full_name='grpc.gateway.protoc_gen_swagger.options.Response.schema', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='extensions', full_name='grpc.gateway.protoc_gen_swagger.options.Response.extensions', index=2, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_RESPONSE_EXTENSIONSENTRY, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1760, + serialized_end=2030, +) + + +_INFO_EXTENSIONSENTRY = _descriptor.Descriptor( + name='ExtensionsEntry', + full_name='grpc.gateway.protoc_gen_swagger.options.Info.ExtensionsEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Info.ExtensionsEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Info.ExtensionsEntry.value', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=_b('8\001'), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=914, + serialized_end=987, +) + +_INFO = _descriptor.Descriptor( + name='Info', + full_name='grpc.gateway.protoc_gen_swagger.options.Info', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='title', full_name='grpc.gateway.protoc_gen_swagger.options.Info.title', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='description', full_name='grpc.gateway.protoc_gen_swagger.options.Info.description', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='terms_of_service', full_name='grpc.gateway.protoc_gen_swagger.options.Info.terms_of_service', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='contact', full_name='grpc.gateway.protoc_gen_swagger.options.Info.contact', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='license', full_name='grpc.gateway.protoc_gen_swagger.options.Info.license', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='version', full_name='grpc.gateway.protoc_gen_swagger.options.Info.version', index=5, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='extensions', full_name='grpc.gateway.protoc_gen_swagger.options.Info.extensions', index=6, + number=7, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_INFO_EXTENSIONSENTRY, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2033, + serialized_end=2410, +) + + +_CONTACT = _descriptor.Descriptor( + name='Contact', + full_name='grpc.gateway.protoc_gen_swagger.options.Contact', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='grpc.gateway.protoc_gen_swagger.options.Contact.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='url', full_name='grpc.gateway.protoc_gen_swagger.options.Contact.url', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='email', full_name='grpc.gateway.protoc_gen_swagger.options.Contact.email', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2412, + serialized_end=2463, +) + + +_LICENSE = _descriptor.Descriptor( + name='License', + full_name='grpc.gateway.protoc_gen_swagger.options.License', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='grpc.gateway.protoc_gen_swagger.options.License.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='url', full_name='grpc.gateway.protoc_gen_swagger.options.License.url', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2465, + serialized_end=2501, +) + + +_EXTERNALDOCUMENTATION = _descriptor.Descriptor( + name='ExternalDocumentation', + full_name='grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='description', full_name='grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation.description', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='url', full_name='grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation.url', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2503, + serialized_end=2560, +) + + +_SCHEMA = _descriptor.Descriptor( + name='Schema', + full_name='grpc.gateway.protoc_gen_swagger.options.Schema', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='json_schema', full_name='grpc.gateway.protoc_gen_swagger.options.Schema.json_schema', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='discriminator', full_name='grpc.gateway.protoc_gen_swagger.options.Schema.discriminator', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='read_only', full_name='grpc.gateway.protoc_gen_swagger.options.Schema.read_only', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='external_docs', full_name='grpc.gateway.protoc_gen_swagger.options.Schema.external_docs', index=3, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='example', full_name='grpc.gateway.protoc_gen_swagger.options.Schema.example', index=4, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2563, + serialized_end=2819, +) + + +_JSONSCHEMA = _descriptor.Descriptor( + name='JSONSchema', + full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='ref', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.ref', index=0, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='title', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.title', index=1, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='description', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.description', index=2, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='default', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.default', index=3, + number=7, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='read_only', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.read_only', index=4, + number=8, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='multiple_of', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.multiple_of', index=5, + number=10, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='maximum', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.maximum', index=6, + number=11, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='exclusive_maximum', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.exclusive_maximum', index=7, + number=12, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='minimum', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.minimum', index=8, + number=13, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='exclusive_minimum', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.exclusive_minimum', index=9, + number=14, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='max_length', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.max_length', index=10, + number=15, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='min_length', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.min_length', index=11, + number=16, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='pattern', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.pattern', index=12, + number=17, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='max_items', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.max_items', index=13, + number=20, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='min_items', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.min_items', index=14, + number=21, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='unique_items', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.unique_items', index=15, + number=22, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='max_properties', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.max_properties', index=16, + number=24, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='min_properties', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.min_properties', index=17, + number=25, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='required', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.required', index=18, + number=26, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='array', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.array', index=19, + number=34, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='type', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.type', index=20, + number=35, type=14, cpp_type=8, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _JSONSCHEMA_JSONSCHEMASIMPLETYPES, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2822, + serialized_end=3520, +) + + +_TAG = _descriptor.Descriptor( + name='Tag', + full_name='grpc.gateway.protoc_gen_swagger.options.Tag', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='description', full_name='grpc.gateway.protoc_gen_swagger.options.Tag.description', index=0, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='external_docs', full_name='grpc.gateway.protoc_gen_swagger.options.Tag.external_docs', index=1, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=3522, + serialized_end=3641, +) + + +_SECURITYDEFINITIONS_SECURITYENTRY = _descriptor.Descriptor( + name='SecurityEntry', + full_name='grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions.SecurityEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions.SecurityEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions.SecurityEntry.value', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=_b('8\001'), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=3761, + serialized_end=3865, +) + +_SECURITYDEFINITIONS = _descriptor.Descriptor( + name='SecurityDefinitions', + full_name='grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='security', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions.security', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_SECURITYDEFINITIONS_SECURITYENTRY, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=3644, + serialized_end=3865, +) + + +_SECURITYSCHEME_EXTENSIONSENTRY = _descriptor.Descriptor( + name='ExtensionsEntry', + full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.ExtensionsEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.ExtensionsEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.ExtensionsEntry.value', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=_b('8\001'), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=914, + serialized_end=987, +) + +_SECURITYSCHEME = _descriptor.Descriptor( + name='SecurityScheme', + full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='type', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.type', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='description', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.description', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='name', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.name', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='in', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.in', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='flow', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.flow', index=4, + number=5, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='authorization_url', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.authorization_url', index=5, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='token_url', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.token_url', index=6, + number=7, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='scopes', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.scopes', index=7, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='extensions', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.extensions', index=8, + number=9, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_SECURITYSCHEME_EXTENSIONSENTRY, ], + enum_types=[ + _SECURITYSCHEME_TYPE, + _SECURITYSCHEME_IN, + _SECURITYSCHEME_FLOW, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=3868, + serialized_end=4658, +) + + +_SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE = _descriptor.Descriptor( + name='SecurityRequirementValue', + full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementValue', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='scope', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementValue.scope', index=0, + number=1, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=4801, + serialized_end=4842, +) + +_SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY = _descriptor.Descriptor( + name='SecurityRequirementEntry', + full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementEntry.value', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=_b('8\001'), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=4845, + serialized_end=4990, +) + +_SECURITYREQUIREMENT = _descriptor.Descriptor( + name='SecurityRequirement', + full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='security_requirement', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.security_requirement', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE, _SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=4661, + serialized_end=4990, +) + + +_SCOPES_SCOPEENTRY = _descriptor.Descriptor( + name='ScopeEntry', + full_name='grpc.gateway.protoc_gen_swagger.options.Scopes.ScopeEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Scopes.ScopeEntry.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Scopes.ScopeEntry.value', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=_b('8\001'), + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=5078, + serialized_end=5122, +) + +_SCOPES = _descriptor.Descriptor( + name='Scopes', + full_name='grpc.gateway.protoc_gen_swagger.options.Scopes', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='scope', full_name='grpc.gateway.protoc_gen_swagger.options.Scopes.scope', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_SCOPES_SCOPEENTRY, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=4993, + serialized_end=5122, +) + +_SWAGGER_RESPONSESENTRY.fields_by_name['value'].message_type = _RESPONSE +_SWAGGER_RESPONSESENTRY.containing_type = _SWAGGER +_SWAGGER_EXTENSIONSENTRY.fields_by_name['value'].message_type = google_dot_protobuf_dot_struct__pb2._VALUE +_SWAGGER_EXTENSIONSENTRY.containing_type = _SWAGGER +_SWAGGER.fields_by_name['info'].message_type = _INFO +_SWAGGER.fields_by_name['schemes'].enum_type = _SWAGGER_SWAGGERSCHEME +_SWAGGER.fields_by_name['responses'].message_type = _SWAGGER_RESPONSESENTRY +_SWAGGER.fields_by_name['security_definitions'].message_type = _SECURITYDEFINITIONS +_SWAGGER.fields_by_name['security'].message_type = _SECURITYREQUIREMENT +_SWAGGER.fields_by_name['external_docs'].message_type = _EXTERNALDOCUMENTATION +_SWAGGER.fields_by_name['extensions'].message_type = _SWAGGER_EXTENSIONSENTRY +_SWAGGER_SWAGGERSCHEME.containing_type = _SWAGGER +_OPERATION_RESPONSESENTRY.fields_by_name['value'].message_type = _RESPONSE +_OPERATION_RESPONSESENTRY.containing_type = _OPERATION +_OPERATION_EXTENSIONSENTRY.fields_by_name['value'].message_type = google_dot_protobuf_dot_struct__pb2._VALUE +_OPERATION_EXTENSIONSENTRY.containing_type = _OPERATION +_OPERATION.fields_by_name['external_docs'].message_type = _EXTERNALDOCUMENTATION +_OPERATION.fields_by_name['responses'].message_type = _OPERATION_RESPONSESENTRY +_OPERATION.fields_by_name['security'].message_type = _SECURITYREQUIREMENT +_OPERATION.fields_by_name['extensions'].message_type = _OPERATION_EXTENSIONSENTRY +_RESPONSE_EXTENSIONSENTRY.fields_by_name['value'].message_type = google_dot_protobuf_dot_struct__pb2._VALUE +_RESPONSE_EXTENSIONSENTRY.containing_type = _RESPONSE +_RESPONSE.fields_by_name['schema'].message_type = _SCHEMA +_RESPONSE.fields_by_name['extensions'].message_type = _RESPONSE_EXTENSIONSENTRY +_INFO_EXTENSIONSENTRY.fields_by_name['value'].message_type = google_dot_protobuf_dot_struct__pb2._VALUE +_INFO_EXTENSIONSENTRY.containing_type = _INFO +_INFO.fields_by_name['contact'].message_type = _CONTACT +_INFO.fields_by_name['license'].message_type = _LICENSE +_INFO.fields_by_name['extensions'].message_type = _INFO_EXTENSIONSENTRY +_SCHEMA.fields_by_name['json_schema'].message_type = _JSONSCHEMA +_SCHEMA.fields_by_name['external_docs'].message_type = _EXTERNALDOCUMENTATION +_SCHEMA.fields_by_name['example'].message_type = google_dot_protobuf_dot_any__pb2._ANY +_JSONSCHEMA.fields_by_name['type'].enum_type = _JSONSCHEMA_JSONSCHEMASIMPLETYPES +_JSONSCHEMA_JSONSCHEMASIMPLETYPES.containing_type = _JSONSCHEMA +_TAG.fields_by_name['external_docs'].message_type = _EXTERNALDOCUMENTATION +_SECURITYDEFINITIONS_SECURITYENTRY.fields_by_name['value'].message_type = _SECURITYSCHEME +_SECURITYDEFINITIONS_SECURITYENTRY.containing_type = _SECURITYDEFINITIONS +_SECURITYDEFINITIONS.fields_by_name['security'].message_type = _SECURITYDEFINITIONS_SECURITYENTRY +_SECURITYSCHEME_EXTENSIONSENTRY.fields_by_name['value'].message_type = google_dot_protobuf_dot_struct__pb2._VALUE +_SECURITYSCHEME_EXTENSIONSENTRY.containing_type = _SECURITYSCHEME +_SECURITYSCHEME.fields_by_name['type'].enum_type = _SECURITYSCHEME_TYPE +_SECURITYSCHEME.fields_by_name['in'].enum_type = _SECURITYSCHEME_IN +_SECURITYSCHEME.fields_by_name['flow'].enum_type = _SECURITYSCHEME_FLOW +_SECURITYSCHEME.fields_by_name['scopes'].message_type = _SCOPES +_SECURITYSCHEME.fields_by_name['extensions'].message_type = _SECURITYSCHEME_EXTENSIONSENTRY +_SECURITYSCHEME_TYPE.containing_type = _SECURITYSCHEME +_SECURITYSCHEME_IN.containing_type = _SECURITYSCHEME +_SECURITYSCHEME_FLOW.containing_type = _SECURITYSCHEME +_SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE.containing_type = _SECURITYREQUIREMENT +_SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY.fields_by_name['value'].message_type = _SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE +_SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY.containing_type = _SECURITYREQUIREMENT +_SECURITYREQUIREMENT.fields_by_name['security_requirement'].message_type = _SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY +_SCOPES_SCOPEENTRY.containing_type = _SCOPES +_SCOPES.fields_by_name['scope'].message_type = _SCOPES_SCOPEENTRY +DESCRIPTOR.message_types_by_name['Swagger'] = _SWAGGER +DESCRIPTOR.message_types_by_name['Operation'] = _OPERATION +DESCRIPTOR.message_types_by_name['Response'] = _RESPONSE +DESCRIPTOR.message_types_by_name['Info'] = _INFO +DESCRIPTOR.message_types_by_name['Contact'] = _CONTACT +DESCRIPTOR.message_types_by_name['License'] = _LICENSE +DESCRIPTOR.message_types_by_name['ExternalDocumentation'] = _EXTERNALDOCUMENTATION +DESCRIPTOR.message_types_by_name['Schema'] = _SCHEMA +DESCRIPTOR.message_types_by_name['JSONSchema'] = _JSONSCHEMA +DESCRIPTOR.message_types_by_name['Tag'] = _TAG +DESCRIPTOR.message_types_by_name['SecurityDefinitions'] = _SECURITYDEFINITIONS +DESCRIPTOR.message_types_by_name['SecurityScheme'] = _SECURITYSCHEME +DESCRIPTOR.message_types_by_name['SecurityRequirement'] = _SECURITYREQUIREMENT +DESCRIPTOR.message_types_by_name['Scopes'] = _SCOPES +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Swagger = _reflection.GeneratedProtocolMessageType('Swagger', (_message.Message,), { + + 'ResponsesEntry' : _reflection.GeneratedProtocolMessageType('ResponsesEntry', (_message.Message,), { + 'DESCRIPTOR' : _SWAGGER_RESPONSESENTRY, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Swagger.ResponsesEntry) + }) + , + + 'ExtensionsEntry' : _reflection.GeneratedProtocolMessageType('ExtensionsEntry', (_message.Message,), { + 'DESCRIPTOR' : _SWAGGER_EXTENSIONSENTRY, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Swagger.ExtensionsEntry) + }) + , + 'DESCRIPTOR' : _SWAGGER, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Swagger) + }) +_sym_db.RegisterMessage(Swagger) +_sym_db.RegisterMessage(Swagger.ResponsesEntry) +_sym_db.RegisterMessage(Swagger.ExtensionsEntry) + +Operation = _reflection.GeneratedProtocolMessageType('Operation', (_message.Message,), { + + 'ResponsesEntry' : _reflection.GeneratedProtocolMessageType('ResponsesEntry', (_message.Message,), { + 'DESCRIPTOR' : _OPERATION_RESPONSESENTRY, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Operation.ResponsesEntry) + }) + , + + 'ExtensionsEntry' : _reflection.GeneratedProtocolMessageType('ExtensionsEntry', (_message.Message,), { + 'DESCRIPTOR' : _OPERATION_EXTENSIONSENTRY, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Operation.ExtensionsEntry) + }) + , + 'DESCRIPTOR' : _OPERATION, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Operation) + }) +_sym_db.RegisterMessage(Operation) +_sym_db.RegisterMessage(Operation.ResponsesEntry) +_sym_db.RegisterMessage(Operation.ExtensionsEntry) + +Response = _reflection.GeneratedProtocolMessageType('Response', (_message.Message,), { + + 'ExtensionsEntry' : _reflection.GeneratedProtocolMessageType('ExtensionsEntry', (_message.Message,), { + 'DESCRIPTOR' : _RESPONSE_EXTENSIONSENTRY, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Response.ExtensionsEntry) + }) + , + 'DESCRIPTOR' : _RESPONSE, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Response) + }) +_sym_db.RegisterMessage(Response) +_sym_db.RegisterMessage(Response.ExtensionsEntry) + +Info = _reflection.GeneratedProtocolMessageType('Info', (_message.Message,), { + + 'ExtensionsEntry' : _reflection.GeneratedProtocolMessageType('ExtensionsEntry', (_message.Message,), { + 'DESCRIPTOR' : _INFO_EXTENSIONSENTRY, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Info.ExtensionsEntry) + }) + , + 'DESCRIPTOR' : _INFO, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Info) + }) +_sym_db.RegisterMessage(Info) +_sym_db.RegisterMessage(Info.ExtensionsEntry) + +Contact = _reflection.GeneratedProtocolMessageType('Contact', (_message.Message,), { + 'DESCRIPTOR' : _CONTACT, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Contact) + }) +_sym_db.RegisterMessage(Contact) + +License = _reflection.GeneratedProtocolMessageType('License', (_message.Message,), { + 'DESCRIPTOR' : _LICENSE, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.License) + }) +_sym_db.RegisterMessage(License) + +ExternalDocumentation = _reflection.GeneratedProtocolMessageType('ExternalDocumentation', (_message.Message,), { + 'DESCRIPTOR' : _EXTERNALDOCUMENTATION, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation) + }) +_sym_db.RegisterMessage(ExternalDocumentation) + +Schema = _reflection.GeneratedProtocolMessageType('Schema', (_message.Message,), { + 'DESCRIPTOR' : _SCHEMA, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Schema) + }) +_sym_db.RegisterMessage(Schema) + +JSONSchema = _reflection.GeneratedProtocolMessageType('JSONSchema', (_message.Message,), { + 'DESCRIPTOR' : _JSONSCHEMA, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.JSONSchema) + }) +_sym_db.RegisterMessage(JSONSchema) + +Tag = _reflection.GeneratedProtocolMessageType('Tag', (_message.Message,), { + 'DESCRIPTOR' : _TAG, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Tag) + }) +_sym_db.RegisterMessage(Tag) + +SecurityDefinitions = _reflection.GeneratedProtocolMessageType('SecurityDefinitions', (_message.Message,), { + + 'SecurityEntry' : _reflection.GeneratedProtocolMessageType('SecurityEntry', (_message.Message,), { + 'DESCRIPTOR' : _SECURITYDEFINITIONS_SECURITYENTRY, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions.SecurityEntry) + }) + , + 'DESCRIPTOR' : _SECURITYDEFINITIONS, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions) + }) +_sym_db.RegisterMessage(SecurityDefinitions) +_sym_db.RegisterMessage(SecurityDefinitions.SecurityEntry) + +SecurityScheme = _reflection.GeneratedProtocolMessageType('SecurityScheme', (_message.Message,), { + + 'ExtensionsEntry' : _reflection.GeneratedProtocolMessageType('ExtensionsEntry', (_message.Message,), { + 'DESCRIPTOR' : _SECURITYSCHEME_EXTENSIONSENTRY, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityScheme.ExtensionsEntry) + }) + , + 'DESCRIPTOR' : _SECURITYSCHEME, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityScheme) + }) +_sym_db.RegisterMessage(SecurityScheme) +_sym_db.RegisterMessage(SecurityScheme.ExtensionsEntry) + +SecurityRequirement = _reflection.GeneratedProtocolMessageType('SecurityRequirement', (_message.Message,), { + + 'SecurityRequirementValue' : _reflection.GeneratedProtocolMessageType('SecurityRequirementValue', (_message.Message,), { + 'DESCRIPTOR' : _SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementValue) + }) + , + + 'SecurityRequirementEntry' : _reflection.GeneratedProtocolMessageType('SecurityRequirementEntry', (_message.Message,), { + 'DESCRIPTOR' : _SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementEntry) + }) + , + 'DESCRIPTOR' : _SECURITYREQUIREMENT, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityRequirement) + }) +_sym_db.RegisterMessage(SecurityRequirement) +_sym_db.RegisterMessage(SecurityRequirement.SecurityRequirementValue) +_sym_db.RegisterMessage(SecurityRequirement.SecurityRequirementEntry) + +Scopes = _reflection.GeneratedProtocolMessageType('Scopes', (_message.Message,), { + + 'ScopeEntry' : _reflection.GeneratedProtocolMessageType('ScopeEntry', (_message.Message,), { + 'DESCRIPTOR' : _SCOPES_SCOPEENTRY, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Scopes.ScopeEntry) + }) + , + 'DESCRIPTOR' : _SCOPES, + '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' + # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Scopes) + }) +_sym_db.RegisterMessage(Scopes) +_sym_db.RegisterMessage(Scopes.ScopeEntry) + + +DESCRIPTOR._options = None +_SWAGGER_RESPONSESENTRY._options = None +_SWAGGER_EXTENSIONSENTRY._options = None +_OPERATION_RESPONSESENTRY._options = None +_OPERATION_EXTENSIONSENTRY._options = None +_RESPONSE_EXTENSIONSENTRY._options = None +_INFO_EXTENSIONSENTRY._options = None +_SECURITYDEFINITIONS_SECURITYENTRY._options = None +_SECURITYSCHEME_EXTENSIONSENTRY._options = None +_SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY._options = None +_SCOPES_SCOPEENTRY._options = None +# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/thrift.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/thrift.py new file mode 100644 index 0000000000..151e017b45 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/thrift.py @@ -0,0 +1,118 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import logging +import socket + +from thrift.protocol import TBinaryProtocol, TCompactProtocol +from thrift.transport import THttpClient, TTransport + +from opentelemetry.exporter.jaeger.gen.agent import Agent as agent +from opentelemetry.exporter.jaeger.gen.jaeger import Collector as jaeger + +UDP_PACKET_MAX_LENGTH = 65000 + + +logger = logging.getLogger(__name__) + + +class AgentClientUDP: + """Implement a UDP client to agent. + + Args: + host_name: The host name of the Jaeger server. + port: The port of the Jaeger server. + max_packet_size: Maximum size of UDP packet. + client: Class for creating new client objects for agencies. + """ + + def __init__( + self, + host_name, + port, + max_packet_size=UDP_PACKET_MAX_LENGTH, + client=agent.Client, + ): + self.address = (host_name, port) + self.max_packet_size = max_packet_size + self.buffer = TTransport.TMemoryBuffer() + self.client = client( + iprot=TCompactProtocol.TCompactProtocol(trans=self.buffer) + ) + + def emit(self, batch: jaeger.Batch): + """ + Args: + batch: Object to emit Jaeger spans. + """ + + # pylint: disable=protected-access + self.client._seqid = 0 + # truncate and reset the position of BytesIO object + self.buffer._buffer.truncate(0) + self.buffer._buffer.seek(0) + self.client.emitBatch(batch) + buff = self.buffer.getvalue() + if len(buff) > self.max_packet_size: + logger.warning( + "Data exceeds the max UDP packet size; size %r, max %r", + len(buff), + self.max_packet_size, + ) + return + + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_socket: + udp_socket.sendto(buff, self.address) + + +class Collector: + """Submits collected spans to Thrift HTTP server. + + Args: + thrift_url: URL of the Jaeger HTTP Thrift. + auth: Auth tuple that contains username and password for Basic Auth. + """ + + def __init__(self, thrift_url="", auth=None): + self.thrift_url = thrift_url + self.auth = auth + self.http_transport = THttpClient.THttpClient( + uri_or_host=self.thrift_url + ) + self.protocol = TBinaryProtocol.TBinaryProtocol(self.http_transport) + + # set basic auth header + if auth is not None: + auth_header = "{}:{}".format(*auth) + decoded = base64.b64encode(auth_header.encode()).decode("ascii") + basic_auth = dict(Authorization="Basic {}".format(decoded)) + self.http_transport.setCustomHeaders(basic_auth) + + def submit(self, batch: jaeger.Batch): + """Submits batches to Thrift HTTP Server through Binary Protocol. + + Args: + batch: Object to emit Jaeger spans. + """ + batch.write(self.protocol) + self.http_transport.flush() + code = self.http_transport.code + msg = self.http_transport.message + if code >= 300 or code < 200: + logger.error( + "Traces cannot be uploaded; HTTP status code: %s, message: %s", + code, + msg, + ) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py new file mode 100644 index 0000000000..b840687fbe --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py @@ -0,0 +1,87 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc + +from opentelemetry.trace import SpanKind + +OTLP_JAEGER_SPAN_KIND = { + SpanKind.CLIENT: "client", + SpanKind.SERVER: "server", + SpanKind.CONSUMER: "consumer", + SpanKind.PRODUCER: "producer", + SpanKind.INTERNAL: "internal", +} + +NAME_KEY = "otel.instrumentation_library.name" +VERSION_KEY = "otel.instrumentation_library.version" + + +def _nsec_to_usec_round(nsec: int) -> int: + """Round nanoseconds to microseconds""" + return (nsec + 500) // 10 ** 3 + + +def _convert_int_to_i64(val): + """Convert integer to signed int64 (i64)""" + if val > 0x7FFFFFFFFFFFFFFF: + val -= 0x10000000000000000 + return val + + +class Translator(abc.ABC): + @abc.abstractmethod + def _translate_span(self, span): + """Translates span to jaeger format. + + Args: + span: span to translate + """ + + @abc.abstractmethod + def _extract_tags(self, span): + """Extracts tags from span and returns list of jaeger Tags. + + Args: + span: span to extract tags + """ + + @abc.abstractmethod + def _extract_refs(self, span): + """Extracts references from span and returns list of jaeger SpanRefs. + + Args: + span: span to extract references + """ + + @abc.abstractmethod + def _extract_logs(self, span): + """Extracts logs from span and returns list of jaeger Logs. + + Args: + span: span to extract logs + """ + + +class Translate: + def __init__(self, spans): + self.spans = spans + + def _translate(self, translator: Translator): + translated_spans = [] + for span in self.spans: + # pylint: disable=protected-access + translated_span = translator._translate_span(span) + translated_spans.append(translated_span) + return translated_spans diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py new file mode 100644 index 0000000000..f43bdfb599 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py @@ -0,0 +1,257 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional, Sequence + +from google.protobuf.duration_pb2 import Duration +from google.protobuf.timestamp_pb2 import Timestamp + +from opentelemetry.exporter.jaeger.gen import model_pb2 +from opentelemetry.exporter.jaeger.translate import ( + NAME_KEY, + OTLP_JAEGER_SPAN_KIND, + VERSION_KEY, + Translator, +) +from opentelemetry.sdk.trace import Span +from opentelemetry.util import types + +# pylint: disable=no-member,too-many-locals,no-self-use + + +def _trace_id_to_bytes(trace_id: int) -> bytes: + """Returns bytes representation of trace id.""" + return trace_id.to_bytes(16, "big") + + +def _span_id_to_bytes(span_id: int) -> bytes: + """Returns bytes representation of span id""" + return span_id.to_bytes(8, "big") + + +def _get_string_key_value(key, value: str) -> model_pb2.KeyValue: + """Returns jaeger string KeyValue.""" + return model_pb2.KeyValue( + key=key, v_str=value, v_type=model_pb2.ValueType.STRING + ) + + +def _get_bool_key_value(key: str, value: bool) -> model_pb2.KeyValue: + """Returns jaeger boolean KeyValue.""" + return model_pb2.KeyValue( + key=key, v_bool=value, v_type=model_pb2.ValueType.BOOL + ) + + +def _get_long_key_value(key: str, value: int) -> model_pb2.KeyValue: + """Returns jaeger long KeyValue.""" + return model_pb2.KeyValue( + key=key, v_int64=value, v_type=model_pb2.ValueType.INT64 + ) + + +def _get_double_key_value(key: str, value: float) -> model_pb2.KeyValue: + """Returns jaeger double KeyValue.""" + return model_pb2.KeyValue( + key=key, v_float64=value, v_type=model_pb2.ValueType.FLOAT64 + ) + + +def _get_binary_key_value(key: str, value: bytes) -> model_pb2.KeyValue: + """Returns jaeger double KeyValue.""" + return model_pb2.KeyValue( + key=key, v_binary=value, v_type=model_pb2.ValueType.BINARY + ) + + +def _translate_attribute( + key: str, value: types.AttributeValue +) -> Optional[model_pb2.KeyValue]: + """Convert the attributes to jaeger keyvalues.""" + translated = None + if isinstance(value, bool): + translated = _get_bool_key_value(key, value) + elif isinstance(value, str): + translated = _get_string_key_value(key, value) + elif isinstance(value, int): + translated = _get_long_key_value(key, value) + elif isinstance(value, float): + translated = _get_double_key_value(key, value) + elif isinstance(value, tuple): + translated = _get_string_key_value(key, str(value)) + return translated + + +def _extract_resource_tags(span: Span) -> Sequence[model_pb2.KeyValue]: + """Extracts resource attributes from span and returns + list of jaeger keyvalues. + + Args: + span: span to extract keyvalues + """ + tags = [] + for key, value in span.resource.attributes.items(): + tag = _translate_attribute(key, value) + if tag: + tags.append(tag) + return tags + + +def _duration_from_two_time_stamps( + start: Timestamp, end: Timestamp +) -> Duration: + """Compute Duration from two Timestamps. + + See https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#duration + """ + duration = Duration( + seconds=end.seconds - start.seconds, nanos=end.nanos - start.nanos, + ) + # pylint: disable=chained-comparison + if duration.seconds < 0 and duration.nanos > 0: + duration.seconds += 1 + duration.nanos -= 1000000000 + elif duration.seconds > 0 and duration.nanos < 0: + duration.seconds -= 1 + duration.nanos += 1000000000 + return duration + + +def _proto_timestamp_from_epoch_nanos(nsec: int) -> Timestamp: + """Create a Timestamp from the number of nanoseconds elapsed from the epoch. + + See https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#timestamp + """ + nsec_time = nsec / 1e9 + seconds = int(nsec_time) + nanos = int((nsec_time - seconds) * 1e9) + return Timestamp(seconds=seconds, nanos=nanos) + + +class ProtobufTranslator(Translator): + def __init__(self, svc_name): + self.svc_name = svc_name + + def _translate_span(self, span: Span) -> model_pb2.Span: + + ctx = span.get_span_context() + # pb2 span expects in byte format + trace_id = _trace_id_to_bytes(ctx.trace_id) + span_id = _span_id_to_bytes(ctx.span_id) + + start_time = _proto_timestamp_from_epoch_nanos(span.start_time) + end_time = _proto_timestamp_from_epoch_nanos(span.end_time) + duration = _duration_from_two_time_stamps(start_time, end_time) + + tags = self._extract_tags(span) + refs = self._extract_refs(span) + logs = self._extract_logs(span) + + flags = int(ctx.trace_flags) + + process = model_pb2.Process( + service_name=self.svc_name, tags=_extract_resource_tags(span) + ) + jaeger_span = model_pb2.Span( + trace_id=trace_id, + span_id=span_id, + operation_name=span.name, + references=refs, + flags=flags, + start_time=start_time, + duration=duration, + tags=tags, + logs=logs, + process=process, + ) + return jaeger_span + + def _extract_tags(self, span: Span) -> Sequence[model_pb2.KeyValue]: + translated = [] + if span.attributes: + for key, value in span.attributes.items(): + key_value = _translate_attribute(key, value) + if key_value is not None: + translated.append(key_value) + if span.resource.attributes: + for key, value in span.resource.attributes.items(): + key_value = _translate_attribute(key, value) + if key_value: + translated.append(key_value) + + code = _get_long_key_value( + "status.code", span.status.status_code.value + ) + message = _get_string_key_value( + "status.message", span.status.description + ) + kind = _get_string_key_value( + "span.kind", OTLP_JAEGER_SPAN_KIND[span.kind] + ) + translated.extend([code, message, kind]) + + # Instrumentation info KeyValues + if span.instrumentation_info: + name = _get_string_key_value( + NAME_KEY, span.instrumentation_info.name + ) + version = _get_string_key_value( + VERSION_KEY, span.instrumentation_info.version + ) + translated.extend([name, version]) + + # Make sure to add "error" tag if span status is not OK + if not span.status.is_ok: + translated.append(_get_bool_key_value("error", True)) + + return translated + + def _extract_refs( + self, span: Span + ) -> Optional[Sequence[model_pb2.SpanRef]]: + if not span.links: + return None + + refs = [] + for link in span.links: + trace_id = link.context.trace_id + span_id = link.context.span_id + refs.append( + model_pb2.SpanRef( + ref_type=model_pb2.SpanRefType.FOLLOWS_FROM, + trace_id=_trace_id_to_bytes(trace_id), + span_id=_span_id_to_bytes(span_id), + ) + ) + return refs + + def _extract_logs(self, span: Span) -> Optional[Sequence[model_pb2.Log]]: + if not span.events: + return None + + logs = [] + for event in span.events: + fields = [] + for key, value in event.attributes.items(): + tag = _translate_attribute(key, value) + if tag: + fields.append(tag) + + fields.append( + _get_string_key_value(key="message", value=event.name,) + ) + event_ts = _proto_timestamp_from_epoch_nanos(event.timestamp) + logs.append(model_pb2.Log(timestamp=event_ts, fields=fields)) + + return logs diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py new file mode 100644 index 0000000000..710ba85a6e --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py @@ -0,0 +1,194 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: disable=no-self-use +from typing import Optional, Sequence + +from opentelemetry.exporter.jaeger.gen.jaeger import Collector as TCollector +from opentelemetry.exporter.jaeger.translate import ( + NAME_KEY, + OTLP_JAEGER_SPAN_KIND, + VERSION_KEY, + Translator, + _convert_int_to_i64, + _nsec_to_usec_round, +) +from opentelemetry.sdk.trace import Span +from opentelemetry.util import types + + +def _get_string_tag(key, value: str) -> TCollector.Tag: + """Returns jaeger string tag.""" + return TCollector.Tag(key=key, vStr=value, vType=TCollector.TagType.STRING) + + +def _get_bool_tag(key: str, value: bool) -> TCollector.Tag: + """Returns jaeger boolean tag.""" + return TCollector.Tag(key=key, vBool=value, vType=TCollector.TagType.BOOL) + + +def _get_long_tag(key: str, value: int) -> TCollector.Tag: + """Returns jaeger long tag.""" + return TCollector.Tag(key=key, vLong=value, vType=TCollector.TagType.LONG) + + +def _get_double_tag(key: str, value: float) -> TCollector.Tag: + """Returns jaeger double tag.""" + return TCollector.Tag( + key=key, vDouble=value, vType=TCollector.TagType.DOUBLE + ) + + +def _get_trace_id_low(trace_id): + return _convert_int_to_i64(trace_id & 0xFFFFFFFFFFFFFFFF) + + +def _get_trace_id_high(trace_id): + return _convert_int_to_i64((trace_id >> 64) & 0xFFFFFFFFFFFFFFFF) + + +def _translate_attribute( + key: str, value: types.AttributeValue +) -> Optional[TCollector.Tag]: + """Convert the attributes to jaeger tags.""" + if isinstance(value, bool): + return _get_bool_tag(key, value) + if isinstance(value, str): + return _get_string_tag(key, value) + if isinstance(value, int): + return _get_long_tag(key, value) + if isinstance(value, float): + return _get_double_tag(key, value) + if isinstance(value, tuple): + return _get_string_tag(key, str(value)) + return None + + +class ThriftTranslator(Translator): + def _translate_span(self, span: Span) -> TCollector.Span: + ctx = span.get_span_context() + trace_id = ctx.trace_id + span_id = ctx.span_id + + start_time_us = _nsec_to_usec_round(span.start_time) + duration_us = _nsec_to_usec_round(span.end_time - span.start_time) + + parent_id = span.parent.span_id if span.parent else 0 + + tags = self._extract_tags(span) + refs = self._extract_refs(span) + logs = self._extract_logs(span) + + flags = int(ctx.trace_flags) + + jaeger_span = TCollector.Span( + traceIdHigh=_get_trace_id_high(trace_id), + traceIdLow=_get_trace_id_low(trace_id), + spanId=_convert_int_to_i64(span_id), + operationName=span.name, + startTime=start_time_us, + duration=duration_us, + tags=tags, + logs=logs, + references=refs, + flags=flags, + parentSpanId=_convert_int_to_i64(parent_id), + ) + return jaeger_span + + def _extract_tags(self, span: Span) -> Sequence[TCollector.Tag]: + + translated = [] + if span.attributes: + for key, value in span.attributes.items(): + tag = _translate_attribute(key, value) + if tag: + translated.append(tag) + if span.resource.attributes: + for key, value in span.resource.attributes.items(): + tag = _translate_attribute(key, value) + if tag: + translated.append(tag) + + code = _get_long_tag("status.code", span.status.status_code.value) + message = _get_string_tag("status.message", span.status.description) + kind = _get_string_tag("span.kind", OTLP_JAEGER_SPAN_KIND[span.kind]) + translated.extend([code, message, kind]) + + # Instrumentation info tags + if span.instrumentation_info: + name = _get_string_tag(NAME_KEY, span.instrumentation_info.name) + version = _get_string_tag( + VERSION_KEY, span.instrumentation_info.version + ) + translated.extend([name, version]) + + # Make sure to add "error" tag if span status is not OK + if not span.status.is_ok: + translated.append(_get_bool_tag("error", True)) + + return translated + + def _extract_refs( + self, span: Span + ) -> Optional[Sequence[TCollector.SpanRef]]: + if not span.links: + return None + + refs = [] + for link in span.links: + trace_id = link.context.trace_id + span_id = link.context.span_id + refs.append( + TCollector.SpanRef( + refType=TCollector.SpanRefType.FOLLOWS_FROM, + traceIdHigh=_get_trace_id_high(trace_id), + traceIdLow=_get_trace_id_low(trace_id), + spanId=_convert_int_to_i64(span_id), + ) + ) + return refs + + def _extract_logs(self, span: Span) -> Optional[Sequence[TCollector.Log]]: + """Returns jaeger logs if events exists, otherwise None. + + Args: + span: span to extract logs + """ + if not span.events: + return None + + logs = [] + for event in span.events: + fields = [] + for key, value in event.attributes.items(): + tag = _translate_attribute(key, value) + if tag: + fields.append(tag) + + fields.append( + TCollector.Tag( + key="message", + vType=TCollector.TagType.STRING, + vStr=event.name, + ) + ) + + event_timestamp_us = _nsec_to_usec_round(event.timestamp) + logs.append( + TCollector.Log( + timestamp=int(event_timestamp_us), fields=fields + ) + ) + + return logs diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py new file mode 100644 index 0000000000..6be9d509ac --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py @@ -0,0 +1,51 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from grpc import ChannelCredentials, ssl_channel_credentials + +from opentelemetry.configuration import Configuration + +logger = logging.getLogger(__name__) + +DEFAULT_INSECURE = False + + +def _get_insecure(param): + if param is not None: + return param + insecure_env = Configuration().get("EXPORTER_JAEGER_INSECURE", None) + if insecure_env is not None: + return insecure_env.lower() == "true" + return DEFAULT_INSECURE + + +def _load_credential_from_file(path) -> ChannelCredentials: + try: + with open(path, "rb") as creds_file: + credential = creds_file.read() + return ssl_channel_credentials(credential) + except FileNotFoundError: + logger.exception("Failed to read credential file") + return None + + +def _get_credentials(param): + if param is not None: + return param + creds_env = Configuration().get("EXPORTER_JAEGER_CERTIFICATE", None) + if creds_env: + return _load_credential_from_file(creds_env) + return ssl_channel_credentials() diff --git a/exporter/opentelemetry-exporter-jaeger/tests/certs/cred.cert b/exporter/opentelemetry-exporter-jaeger/tests/certs/cred.cert new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py similarity index 90% rename from exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py rename to exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py index 75ab622c95..853542c7bb 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py @@ -22,6 +22,8 @@ from opentelemetry import trace as trace_api from opentelemetry.configuration import Configuration from opentelemetry.exporter.jaeger.gen.jaeger import ttypes as jaeger +from opentelemetry.exporter.jaeger.translate import Translate +from opentelemetry.exporter.jaeger.translate.thrift import ThriftTranslator from opentelemetry.sdk import trace from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -49,6 +51,7 @@ def tearDown(self): Configuration._reset() def test_constructor_default(self): + # pylint: disable=protected-access """Test the default values assigned by constructor.""" service_name = "my-service-name" agent_host_name = "localhost" @@ -61,10 +64,11 @@ def test_constructor_default(self): self.assertEqual(exporter.collector_endpoint, None) self.assertEqual(exporter.username, None) self.assertEqual(exporter.password, None) - self.assertTrue(exporter.collector is None) - self.assertTrue(exporter.agent_client is not None) + self.assertTrue(exporter._collector_http_client is None) + self.assertTrue(exporter._agent_client is not None) def test_constructor_explicit(self): + # pylint: disable=protected-access """Test the constructor passing all the options.""" service = "my-opentelemetry-jaeger" collector_endpoint = "https://opentelemetry.io:15875" @@ -88,20 +92,20 @@ def test_constructor_explicit(self): self.assertEqual(exporter.service_name, service) self.assertEqual(exporter.agent_host_name, agent_host_name) self.assertEqual(exporter.agent_port, agent_port) - self.assertTrue(exporter.collector is not None) - self.assertEqual(exporter.collector.auth, auth) + self.assertTrue(exporter._collector_http_client is not None) + self.assertEqual(exporter._collector_http_client.auth, auth) # property should not construct new object - collector = exporter.collector - self.assertEqual(exporter.collector, collector) + collector = exporter._collector_http_client + self.assertEqual(exporter._collector_http_client, collector) # property should construct new object - # pylint: disable=protected-access exporter._collector = None exporter.username = None exporter.password = None - self.assertNotEqual(exporter.collector, collector) - self.assertTrue(exporter.collector.auth is None) + self.assertNotEqual(exporter._collector_http_client, collector) + self.assertTrue(exporter._collector_http_client.auth is None) def test_constructor_by_environment_variables(self): + # pylint: disable=protected-access """Test the constructor using Environment Variables.""" service = "my-opentelemetry-jaeger" @@ -132,25 +136,24 @@ def test_constructor_by_environment_variables(self): self.assertEqual(exporter.service_name, service) self.assertEqual(exporter.agent_host_name, agent_host_name) self.assertEqual(exporter.agent_port, int(agent_port)) - self.assertTrue(exporter.collector is not None) + self.assertTrue(exporter._collector_http_client is not None) self.assertEqual(exporter.collector_endpoint, collector_endpoint) - self.assertEqual(exporter.collector.auth, auth) + self.assertEqual(exporter._collector_http_client.auth, auth) # property should not construct new object - collector = exporter.collector - self.assertEqual(exporter.collector, collector) + collector = exporter._collector_http_client + self.assertEqual(exporter._collector_http_client, collector) # property should construct new object - # pylint: disable=protected-access exporter._collector = None exporter.username = None exporter.password = None - self.assertNotEqual(exporter.collector, collector) - self.assertTrue(exporter.collector.auth is None) + self.assertNotEqual(exporter._collector_http_client, collector) + self.assertTrue(exporter._collector_http_client.auth is None) environ_patcher.stop() def test_nsec_to_usec_round(self): # pylint: disable=protected-access - nsec_to_usec_round = jaeger_exporter._nsec_to_usec_round + nsec_to_usec_round = jaeger_exporter.translate._nsec_to_usec_round self.assertEqual(nsec_to_usec_round(5000), 5) self.assertEqual(nsec_to_usec_round(5499), 5) @@ -158,7 +161,9 @@ def test_nsec_to_usec_round(self): def test_all_otlp_span_kinds_are_mapped(self): for kind in SpanKind: - self.assertIn(kind, jaeger_exporter.OTLP_JAEGER_SPAN_KIND) + self.assertIn( + kind, jaeger_exporter.translate.OTLP_JAEGER_SPAN_KIND + ) # pylint: disable=too-many-locals def test_translate_to_jaeger(self): @@ -272,8 +277,9 @@ def test_translate_to_jaeger(self): name="name", version="version" ) + translate = Translate(otel_spans) # pylint: disable=protected-access - spans = jaeger_exporter._translate_to_jaeger(otel_spans) + spans = translate._translate(ThriftTranslator()) expected_spans = [ jaeger.Span( @@ -453,10 +459,12 @@ def test_agent_client(self): host_name="localhost", port=6354 ) + translate = Translate([self._test_span]) + # pylint: disable=protected-access + spans = translate._translate(ThriftTranslator()) + batch = jaeger.Batch( - # pylint: disable=protected-access - spans=jaeger_exporter._translate_to_jaeger((self._test_span,)), - process=jaeger.Process(serviceName="xxx"), + spans=spans, process=jaeger.Process(serviceName="xxx"), ) agent_client.emit(batch) diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py new file mode 100644 index 0000000000..cf19428ded --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py @@ -0,0 +1,392 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import unittest +from collections import OrderedDict +from unittest.mock import patch + +# pylint:disable=no-name-in-module +# pylint:disable=import-error +import opentelemetry.exporter.jaeger.gen.model_pb2 as model_pb2 +import opentelemetry.exporter.jaeger.translate.protobuf as pb_translator +from opentelemetry import trace as trace_api +from opentelemetry.configuration import Configuration +from opentelemetry.exporter.jaeger import JaegerSpanExporter +from opentelemetry.exporter.jaeger.translate import Translate +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.trace.status import Status, StatusCode + + +# pylint:disable=no-member +class TestJaegerSpanExporter(unittest.TestCase): + def setUp(self): + # create and save span to be used in tests + context = trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + ) + + self._test_span = trace._Span("test_span", context=context) + self._test_span.start() + self._test_span.end() + # pylint: disable=protected-access + Configuration._reset() + + def tearDown(self): + # pylint: disable=protected-access + Configuration._reset() + + def test_constructor_by_environment_variables(self): + """Test using Environment Variables.""" + # pylint: disable=protected-access + Configuration._reset() + service = "my-opentelemetry-jaeger" + + collector_endpoint = "localhost:14250" + + env_patch = patch.dict( + "os.environ", + { + "OTEL_EXPORTER_JAEGER_ENDPOINT": collector_endpoint, + "OTEL_EXPORTER_JAEGER_CERTIFICATE": os.path.dirname(__file__) + + "/certs/cred.cert", + }, + ) + + env_patch.start() + + exporter = JaegerSpanExporter( + service_name=service, transport_format="protobuf" + ) + + self.assertEqual(exporter.service_name, service) + self.assertIsNotNone(exporter._collector_grpc_client) + self.assertEqual(exporter.collector_endpoint, collector_endpoint) + self.assertIsNotNone(exporter.credentials) + + env_patch.stop() + + # pylint: disable=too-many-locals,too-many-statements + def test_translate_to_jaeger(self): + + span_names = ("test1", "test2", "test3") + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + span_id = 0x34BF92DEEFC58C92 + parent_id = 0x1111111111111111 + other_id = 0x2222222222222222 + + base_time = 683647322 * 10 ** 9 # in ns + start_times = ( + base_time, + base_time + 150 * 10 ** 6, + base_time + 300 * 10 ** 6, + ) + durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6) + end_times = ( + start_times[0] + durations[0], + start_times[1] + durations[1], + start_times[2] + durations[2], + ) + + span_context = trace_api.SpanContext( + trace_id, span_id, is_remote=False + ) + parent_span_context = trace_api.SpanContext( + trace_id, parent_id, is_remote=False + ) + other_context = trace_api.SpanContext( + trace_id, other_id, is_remote=False + ) + + event_attributes = OrderedDict( + [ + ("annotation_bool", True), + ("annotation_string", "annotation_test"), + ("key_float", 0.3), + ] + ) + + event_timestamp = base_time + 50 * 10 ** 6 + # pylint:disable=protected-access + event_timestamp_proto = pb_translator._proto_timestamp_from_epoch_nanos( + event_timestamp + ) + + event = trace.Event( + name="event0", + timestamp=event_timestamp, + attributes=event_attributes, + ) + + link_attributes = {"key_bool": True} + + link = trace_api.Link( + context=other_context, attributes=link_attributes + ) + + default_tags = [ + model_pb2.KeyValue( + key="status.code", + v_type=model_pb2.ValueType.INT64, + v_int64=StatusCode.UNSET.value, + ), + model_pb2.KeyValue( + key="status.message", + v_type=model_pb2.ValueType.STRING, + v_str=None, + ), + model_pb2.KeyValue( + key="span.kind", + v_type=model_pb2.ValueType.STRING, + v_str="internal", + ), + ] + + otel_spans = [ + trace._Span( + name=span_names[0], + context=span_context, + parent=parent_span_context, + events=(event,), + links=(link,), + kind=trace_api.SpanKind.CLIENT, + ), + trace._Span( + name=span_names[1], context=parent_span_context, parent=None + ), + trace._Span( + name=span_names[2], context=other_context, parent=None + ), + ] + + otel_spans[0].start(start_time=start_times[0]) + # added here to preserve order + otel_spans[0].set_attribute("key_bool", False) + otel_spans[0].set_attribute("key_string", "hello_world") + otel_spans[0].set_attribute("key_float", 111.22) + otel_spans[0].set_attribute("key_tuple", ("tuple_element",)) + otel_spans[0].resource = Resource( + attributes={"key_resource": "some_resource"} + ) + otel_spans[0].set_status( + Status(StatusCode.ERROR, "Example description") + ) + otel_spans[0].end(end_time=end_times[0]) + + otel_spans[1].start(start_time=start_times[1]) + otel_spans[1].resource = Resource({}) + otel_spans[1].end(end_time=end_times[1]) + + otel_spans[2].start(start_time=start_times[2]) + otel_spans[2].resource = Resource({}) + otel_spans[2].set_status(Status(StatusCode.OK, "Example description")) + otel_spans[2].end(end_time=end_times[2]) + otel_spans[2].instrumentation_info = InstrumentationInfo( + name="name", version="version" + ) + + translate = Translate(otel_spans) + # pylint: disable=protected-access + spans = translate._translate(pb_translator.ProtobufTranslator("svc")) + + span1_start_time = pb_translator._proto_timestamp_from_epoch_nanos( + start_times[0] + ) + span2_start_time = pb_translator._proto_timestamp_from_epoch_nanos( + start_times[1] + ) + span3_start_time = pb_translator._proto_timestamp_from_epoch_nanos( + start_times[2] + ) + + span1_end_time = pb_translator._proto_timestamp_from_epoch_nanos( + end_times[0] + ) + span2_end_time = pb_translator._proto_timestamp_from_epoch_nanos( + end_times[1] + ) + span3_end_time = pb_translator._proto_timestamp_from_epoch_nanos( + end_times[2] + ) + + span1_duration = pb_translator._duration_from_two_time_stamps( + span1_start_time, span1_end_time + ) + span2_duration = pb_translator._duration_from_two_time_stamps( + span2_start_time, span2_end_time + ) + span3_duration = pb_translator._duration_from_two_time_stamps( + span3_start_time, span3_end_time + ) + + expected_spans = [ + model_pb2.Span( + operation_name=span_names[0], + trace_id=pb_translator._trace_id_to_bytes(trace_id), + span_id=pb_translator._span_id_to_bytes(span_id), + start_time=span1_start_time, + duration=span1_duration, + flags=0, + tags=[ + model_pb2.KeyValue( + key="key_bool", + v_type=model_pb2.ValueType.BOOL, + v_bool=False, + ), + model_pb2.KeyValue( + key="key_string", + v_type=model_pb2.ValueType.STRING, + v_str="hello_world", + ), + model_pb2.KeyValue( + key="key_float", + v_type=model_pb2.ValueType.FLOAT64, + v_float64=111.22, + ), + model_pb2.KeyValue( + key="key_tuple", + v_type=model_pb2.ValueType.STRING, + v_str="('tuple_element',)", + ), + model_pb2.KeyValue( + key="key_resource", + v_type=model_pb2.ValueType.STRING, + v_str="some_resource", + ), + model_pb2.KeyValue( + key="status.code", + v_type=model_pb2.ValueType.INT64, + v_int64=StatusCode.ERROR.value, + ), + model_pb2.KeyValue( + key="status.message", + v_type=model_pb2.ValueType.STRING, + v_str="Example description", + ), + model_pb2.KeyValue( + key="span.kind", + v_type=model_pb2.ValueType.STRING, + v_str="client", + ), + model_pb2.KeyValue( + key="error", + v_type=model_pb2.ValueType.BOOL, + v_bool=True, + ), + ], + references=[ + model_pb2.SpanRef( + ref_type=model_pb2.SpanRefType.FOLLOWS_FROM, + trace_id=pb_translator._trace_id_to_bytes(trace_id), + span_id=pb_translator._span_id_to_bytes(other_id), + ) + ], + logs=[ + model_pb2.Log( + timestamp=event_timestamp_proto, + fields=[ + model_pb2.KeyValue( + key="annotation_bool", + v_type=model_pb2.ValueType.BOOL, + v_bool=True, + ), + model_pb2.KeyValue( + key="annotation_string", + v_type=model_pb2.ValueType.STRING, + v_str="annotation_test", + ), + model_pb2.KeyValue( + key="key_float", + v_type=model_pb2.ValueType.FLOAT64, + v_float64=0.3, + ), + model_pb2.KeyValue( + key="message", + v_type=model_pb2.ValueType.STRING, + v_str="event0", + ), + ], + ) + ], + process=model_pb2.Process( + service_name="svc", + tags=[ + model_pb2.KeyValue( + key="key_resource", + v_str="some_resource", + v_type=model_pb2.ValueType.STRING, + ) + ], + ), + ), + model_pb2.Span( + operation_name=span_names[1], + trace_id=pb_translator._trace_id_to_bytes(trace_id), + span_id=pb_translator._span_id_to_bytes(parent_id), + start_time=span2_start_time, + duration=span2_duration, + flags=0, + tags=default_tags, + process=model_pb2.Process(service_name="svc",), + ), + model_pb2.Span( + operation_name=span_names[2], + trace_id=pb_translator._trace_id_to_bytes(trace_id), + span_id=pb_translator._span_id_to_bytes(other_id), + start_time=span3_start_time, + duration=span3_duration, + flags=0, + tags=[ + model_pb2.KeyValue( + key="status.code", + v_type=model_pb2.ValueType.INT64, + v_int64=StatusCode.OK.value, + ), + model_pb2.KeyValue( + key="status.message", + v_type=model_pb2.ValueType.STRING, + v_str="Example description", + ), + model_pb2.KeyValue( + key="span.kind", + v_type=model_pb2.ValueType.STRING, + v_str="internal", + ), + model_pb2.KeyValue( + key="otel.instrumentation_library.name", + v_type=model_pb2.ValueType.STRING, + v_str="name", + ), + model_pb2.KeyValue( + key="otel.instrumentation_library.version", + v_type=model_pb2.ValueType.STRING, + v_str="version", + ), + ], + process=model_pb2.Process(service_name="svc",), + ), + ] + + # events are complicated to compare because order of fields + # (attributes) in otel is not important but in jeager it is + # pylint: disable=no-member + self.assertCountEqual( + spans[0].logs[0].fields, expected_spans[0].logs[0].fields, + ) + + self.assertEqual(spans, expected_spans) diff --git a/tox.ini b/tox.ini index 9a7c8ce3f8..fb5ebee5ab 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,6 @@ envlist = ; opentelemetry-exporter-jaeger py3{5,6,7,8,9}-test-exporter-jaeger - pypy3-test-exporter-jaeger ; opentelemetry-exporter-opencensus py3{5,6,7,8,9}-test-exporter-opencensus From 336af87a095deb46a0de8a9ef39b7f60c87e422c Mon Sep 17 00:00:00 2001 From: Anton Ryzhov Date: Mon, 4 Jan 2021 19:05:15 +0100 Subject: [PATCH 0726/1517] Recreate span on every run of a decorated function (#1451) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/__init__.py | 4 ++- opentelemetry-sdk/tests/trace/test_trace.py | 31 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b1b8fc620..55541fd137 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - `opentelemetry-exporter-zipkin` Updated zipkin exporter status code and error tag ([#1486](https://github.com/open-telemetry/opentelemetry-python/pull/1486)) +- Recreate span on every run of a `start_as_current_span`-decorated function + ([#1451](https://github.com/open-telemetry/opentelemetry-python/pull/1451)) ## [0.16b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b1) - 2020-11-26 ### Added diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index cd087a7314..c8dec7bc62 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -742,6 +742,7 @@ def __init__( self.ids_generator = ids_generator self.instrumentation_info = instrumentation_info + @contextmanager def start_as_current_span( self, name: str, @@ -763,7 +764,8 @@ def start_as_current_span( record_exception=record_exception, set_status_on_exception=set_status_on_exception, ) - return self.use_span(span, end_on_exit=True) + with self.use_span(span, end_on_exit=True) as span_context: + yield span_context def start_span( # pylint: disable=too-many-locals self, diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 6879b6390d..bfefdfcb42 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -414,6 +414,37 @@ def test_start_as_current_span_explicit(self): self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) + def test_start_as_current_span_decorator(self): + tracer = new_tracer() + + self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) + + @tracer.start_as_current_span("root") + def func(): + root = trace_api.get_current_span() + + with tracer.start_as_current_span("child") as child: + self.assertIs(trace_api.get_current_span(), child) + self.assertIs(child.parent, root.get_span_context()) + + # After exiting the child's scope the parent should become the + # current span again. + self.assertIs(trace_api.get_current_span(), root) + self.assertIsNotNone(child.end_time) + + return root + + root1 = func() + + self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) + self.assertIsNotNone(root1.end_time) + + # Second call must create a new span + root2 = func() + self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) + self.assertIsNotNone(root2.end_time) + self.assertIsNot(root1, root2) + def test_explicit_span_resource(self): resource = resources.Resource.create({}) tracer_provider = trace.TracerProvider(resource=resource) From 90db9dc30609bd192976625465a37b0f5724e943 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 5 Jan 2021 02:27:26 +0530 Subject: [PATCH 0727/1517] Code cleanup (#1503) --- .../opentelemetry/instrumentation/utils.py | 25 ++----------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py index 22ba7a2057..f0ebc042b1 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -40,34 +40,13 @@ def http_status_to_status_code( Args: status (int): HTTP status code """ - # pylint:disable=too-many-branches,too-many-return-statements # See: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#status if status < 100: return StatusCode.ERROR if status <= 299: return StatusCode.UNSET - if status <= 399: - if allow_redirect: - return StatusCode.UNSET - return StatusCode.ERROR - if status <= 499: - if status == 401: # HTTPStatus.UNAUTHORIZED: - return StatusCode.ERROR - if status == 403: # HTTPStatus.FORBIDDEN: - return StatusCode.ERROR - if status == 404: # HTTPStatus.NOT_FOUND: - return StatusCode.ERROR - if status == 429: # HTTPStatus.TOO_MANY_REQUESTS: - return StatusCode.ERROR - return StatusCode.ERROR - if status <= 599: - if status == 501: # HTTPStatus.NOT_IMPLEMENTED: - return StatusCode.ERROR - if status == 503: # HTTPStatus.SERVICE_UNAVAILABLE: - return StatusCode.ERROR - if status == 504: # HTTPStatus.GATEWAY_TIMEOUT: - return StatusCode.ERROR - return StatusCode.ERROR + if status <= 399 and allow_redirect: + return StatusCode.UNSET return StatusCode.ERROR From 87d7ced747177b6d840e069f935734a163216108 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 6 Jan 2021 05:11:48 +0530 Subject: [PATCH 0728/1517] Add support for OTLP v0.6.0 (#1472) --- CHANGELOG.md | 3 + .../exporter/otlp/trace_exporter/__init__.py | 12 +- .../tests/test_otlp_trace_exporter.py | 71 ++++++++ .../src/opentelemetry/trace/status.py | 8 +- .../collector/logs/v1/logs_service_pb2.pyi | 16 -- .../metrics/v1/metrics_service_pb2.pyi | 16 -- .../collector/trace/v1/trace_service_pb2.pyi | 16 -- .../proto/common/v1/common_pb2.pyi | 40 ----- .../opentelemetry/proto/logs/v1/logs_pb2.pyi | 84 ++------- .../experimental/configservice_pb2.pyi | 28 --- .../proto/metrics/v1/metrics_pb2.pyi | 114 +----------- .../proto/resource/v1/resource_pb2.pyi | 10 -- .../proto/trace/v1/trace_config_pb2.py | 34 ++-- .../proto/trace/v1/trace_config_pb2.pyi | 68 ++------ .../opentelemetry/proto/trace/v1/trace_pb2.py | 89 +++++++--- .../proto/trace/v1/trace_pb2.pyi | 162 +++++++----------- scripts/proto_codegen.sh | 2 +- 17 files changed, 264 insertions(+), 509 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55541fd137..4b4c8efa86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.16b1...HEAD) +- Add support for OTLP v0.6.0 + ([#1472](https://github.com/open-telemetry/opentelemetry-python/pull/1472)) + - Add protobuf via gRPC exporting support for Jaeger ([#1471](https://github.com/open-telemetry/opentelemetry-python/pull/1471)) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index 1af8d26aa9..0c9ffd79f2 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -197,13 +197,15 @@ def _translate_links(self, sdk_span: SDKSpan) -> None: ) def _translate_status(self, sdk_span: SDKSpan) -> None: + # pylint: disable=no-member if sdk_span.status is not None: - # TODO: Update this when the proto definitions are updated to include UNSET and ERROR - proto_status_code = Status.STATUS_CODE_OK - if sdk_span.status.status_code is StatusCode.ERROR: - proto_status_code = Status.STATUS_CODE_UNKNOWN_ERROR + deprecated_code = Status.DEPRECATED_STATUS_CODE_OK + if sdk_span.status.status_code == StatusCode.ERROR: + deprecated_code = Status.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR self._collector_span_kwargs["status"] = Status( - code=proto_status_code, message=sdk_span.status.description, + deprecated_code=deprecated_code, + code=sdk_span.status.status_code.value, + message=sdk_span.status.description, ) def _translate_data( diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index b691e85849..8afb964edb 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -47,6 +47,8 @@ from opentelemetry.proto.trace.v1.trace_pb2 import Span as OTLPSpan from opentelemetry.proto.trace.v1.trace_pb2 import Status from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.trace import Status as SDKStatus +from opentelemetry.sdk.trace import StatusCode as SDKStatusCode from opentelemetry.sdk.trace import TracerProvider, _Span from opentelemetry.sdk.trace.export import ( SimpleExportSpanProcessor, @@ -343,3 +345,72 @@ def test_translate_spans(self): # pylint: disable=protected-access self.assertEqual(expected, self.exporter._translate_data([self.span])) + + def _check_translated_status( + self, + translated: ExportTraceServiceRequest, + code_expected: Status, + deprecated_code_expected: Status, + ): + status = ( + translated.resource_spans[0] + .instrumentation_library_spans[0] + .spans[0] + .status + ) + + self.assertEqual( + status.code, code_expected, + ) + self.assertEqual( + status.deprecated_code, deprecated_code_expected, + ) + + def test_span_status_translate(self): + # pylint: disable=protected-access,no-member + unset = SDKStatus(status_code=SDKStatusCode.UNSET) + ok = SDKStatus(status_code=SDKStatusCode.OK) + error = SDKStatus(status_code=SDKStatusCode.ERROR) + unset_translated = self.exporter._translate_data( + [_create_span_with_status(unset)] + ) + ok_translated = self.exporter._translate_data( + [_create_span_with_status(ok)] + ) + error_translated = self.exporter._translate_data( + [_create_span_with_status(error)] + ) + self._check_translated_status( + unset_translated, + Status.STATUS_CODE_UNSET, + Status.DEPRECATED_STATUS_CODE_OK, + ) + self._check_translated_status( + ok_translated, + Status.STATUS_CODE_OK, + Status.DEPRECATED_STATUS_CODE_OK, + ) + self._check_translated_status( + error_translated, + Status.STATUS_CODE_ERROR, + Status.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR, + ) + + +def _create_span_with_status(status: SDKStatus): + span = _Span( + "a", + context=Mock( + **{ + "trace_state": OrderedDict([("a", "b"), ("c", "d")]), + "span_id": 10217189687419569865, + "trace_id": 67545097771067222548457157018666467027, + } + ), + parent=Mock(**{"span_id": 12345}), + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), + ) + span.set_status(status) + return span diff --git a/opentelemetry-api/src/opentelemetry/trace/status.py b/opentelemetry-api/src/opentelemetry/trace/status.py index ebc427db83..822d3ca83d 100644 --- a/opentelemetry-api/src/opentelemetry/trace/status.py +++ b/opentelemetry-api/src/opentelemetry/trace/status.py @@ -22,12 +22,12 @@ class StatusCode(enum.Enum): """Represents the canonical set of status codes of a finished Span.""" - OK = 0 - """The operation has been validated by an Application developer or Operator to have completed successfully.""" - - UNSET = 1 + UNSET = 0 """The default status.""" + OK = 1 + """The operation has been validated by an Application developer or Operator to have completed successfully.""" + ERROR = 2 """The operation contains an error.""" diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi index a2082a132f..f93d6312eb 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi @@ -20,7 +20,6 @@ from opentelemetry.proto.logs.v1.logs_pb2 import ( from typing import ( Iterable as typing___Iterable, Optional as typing___Optional, - Union as typing___Union, ) from typing_extensions import ( @@ -32,9 +31,6 @@ builtin___bool = bool builtin___bytes = bytes builtin___float = float builtin___int = int -if sys.version_info < (3,): - builtin___buffer = buffer - builtin___unicode = unicode DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... @@ -49,12 +45,6 @@ class ExportLogsServiceRequest(google___protobuf___message___Message): *, resource_logs : typing___Optional[typing___Iterable[opentelemetry___proto___logs___v1___logs_pb2___ResourceLogs]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> ExportLogsServiceRequest: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ExportLogsServiceRequest: ... def ClearField(self, field_name: typing_extensions___Literal[u"resource_logs",b"resource_logs"]) -> None: ... type___ExportLogsServiceRequest = ExportLogsServiceRequest @@ -63,10 +53,4 @@ class ExportLogsServiceResponse(google___protobuf___message___Message): def __init__(self, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> ExportLogsServiceResponse: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ExportLogsServiceResponse: ... type___ExportLogsServiceResponse = ExportLogsServiceResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi index 7c42c410f4..31efaf83cd 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi @@ -20,7 +20,6 @@ from opentelemetry.proto.metrics.v1.metrics_pb2 import ( from typing import ( Iterable as typing___Iterable, Optional as typing___Optional, - Union as typing___Union, ) from typing_extensions import ( @@ -32,9 +31,6 @@ builtin___bool = bool builtin___bytes = bytes builtin___float = float builtin___int = int -if sys.version_info < (3,): - builtin___buffer = buffer - builtin___unicode = unicode DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... @@ -49,12 +45,6 @@ class ExportMetricsServiceRequest(google___protobuf___message___Message): *, resource_metrics : typing___Optional[typing___Iterable[opentelemetry___proto___metrics___v1___metrics_pb2___ResourceMetrics]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> ExportMetricsServiceRequest: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ExportMetricsServiceRequest: ... def ClearField(self, field_name: typing_extensions___Literal[u"resource_metrics",b"resource_metrics"]) -> None: ... type___ExportMetricsServiceRequest = ExportMetricsServiceRequest @@ -63,10 +53,4 @@ class ExportMetricsServiceResponse(google___protobuf___message___Message): def __init__(self, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> ExportMetricsServiceResponse: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ExportMetricsServiceResponse: ... type___ExportMetricsServiceResponse = ExportMetricsServiceResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi index 540a82ebc5..1f36dfd707 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi @@ -20,7 +20,6 @@ from opentelemetry.proto.trace.v1.trace_pb2 import ( from typing import ( Iterable as typing___Iterable, Optional as typing___Optional, - Union as typing___Union, ) from typing_extensions import ( @@ -32,9 +31,6 @@ builtin___bool = bool builtin___bytes = bytes builtin___float = float builtin___int = int -if sys.version_info < (3,): - builtin___buffer = buffer - builtin___unicode = unicode DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... @@ -49,12 +45,6 @@ class ExportTraceServiceRequest(google___protobuf___message___Message): *, resource_spans : typing___Optional[typing___Iterable[opentelemetry___proto___trace___v1___trace_pb2___ResourceSpans]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> ExportTraceServiceRequest: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ExportTraceServiceRequest: ... def ClearField(self, field_name: typing_extensions___Literal[u"resource_spans",b"resource_spans"]) -> None: ... type___ExportTraceServiceRequest = ExportTraceServiceRequest @@ -63,10 +53,4 @@ class ExportTraceServiceResponse(google___protobuf___message___Message): def __init__(self, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> ExportTraceServiceResponse: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ExportTraceServiceResponse: ... type___ExportTraceServiceResponse = ExportTraceServiceResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi index 12d629436d..4305729cb9 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi @@ -17,7 +17,6 @@ from typing import ( Iterable as typing___Iterable, Optional as typing___Optional, Text as typing___Text, - Union as typing___Union, ) from typing_extensions import ( @@ -29,9 +28,6 @@ builtin___bool = bool builtin___bytes = bytes builtin___float = float builtin___int = int -if sys.version_info < (3,): - builtin___buffer = buffer - builtin___unicode = unicode DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... @@ -58,12 +54,6 @@ class AnyValue(google___protobuf___message___Message): array_value : typing___Optional[type___ArrayValue] = None, kvlist_value : typing___Optional[type___KeyValueList] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> AnyValue: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> AnyValue: ... def HasField(self, field_name: typing_extensions___Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> builtin___bool: ... def ClearField(self, field_name: typing_extensions___Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions___Literal[u"value",b"value"]) -> typing_extensions___Literal["string_value","bool_value","int_value","double_value","array_value","kvlist_value"]: ... @@ -79,12 +69,6 @@ class ArrayValue(google___protobuf___message___Message): *, values : typing___Optional[typing___Iterable[type___AnyValue]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> ArrayValue: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ArrayValue: ... def ClearField(self, field_name: typing_extensions___Literal[u"values",b"values"]) -> None: ... type___ArrayValue = ArrayValue @@ -98,12 +82,6 @@ class KeyValueList(google___protobuf___message___Message): *, values : typing___Optional[typing___Iterable[type___KeyValue]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> KeyValueList: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> KeyValueList: ... def ClearField(self, field_name: typing_extensions___Literal[u"values",b"values"]) -> None: ... type___KeyValueList = KeyValueList @@ -119,12 +97,6 @@ class KeyValue(google___protobuf___message___Message): key : typing___Optional[typing___Text] = None, value : typing___Optional[type___AnyValue] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> KeyValue: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> KeyValue: ... def HasField(self, field_name: typing_extensions___Literal[u"value",b"value"]) -> builtin___bool: ... def ClearField(self, field_name: typing_extensions___Literal[u"key",b"key",u"value",b"value"]) -> None: ... type___KeyValue = KeyValue @@ -139,12 +111,6 @@ class StringKeyValue(google___protobuf___message___Message): key : typing___Optional[typing___Text] = None, value : typing___Optional[typing___Text] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> StringKeyValue: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> StringKeyValue: ... def ClearField(self, field_name: typing_extensions___Literal[u"key",b"key",u"value",b"value"]) -> None: ... type___StringKeyValue = StringKeyValue @@ -158,11 +124,5 @@ class InstrumentationLibrary(google___protobuf___message___Message): name : typing___Optional[typing___Text] = None, version : typing___Optional[typing___Text] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> InstrumentationLibrary: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> InstrumentationLibrary: ... def ClearField(self, field_name: typing_extensions___Literal[u"name",b"name",u"version",b"version"]) -> None: ... type___InstrumentationLibrary = InstrumentationLibrary diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi index 088b238cb4..0f280f6f6c 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi @@ -10,6 +10,10 @@ from google.protobuf.internal.containers import ( RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, ) +from google.protobuf.internal.enum_type_wrapper import ( + _EnumTypeWrapper as google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper, +) + from google.protobuf.message import ( Message as google___protobuf___message___Message, ) @@ -26,12 +30,9 @@ from opentelemetry.proto.resource.v1.resource_pb2 import ( from typing import ( Iterable as typing___Iterable, - List as typing___List, NewType as typing___NewType, Optional as typing___Optional, Text as typing___Text, - Tuple as typing___Tuple, - Union as typing___Union, cast as typing___cast, ) @@ -41,30 +42,19 @@ builtin___bool = bool builtin___bytes = bytes builtin___float = float builtin___int = int -builtin___str = str -if sys.version_info < (3,): - builtin___buffer = buffer - builtin___unicode = unicode DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... SeverityNumberValue = typing___NewType("SeverityNumberValue", builtin___int) type___SeverityNumberValue = SeverityNumberValue +SeverityNumber: _SeverityNumber -class SeverityNumber(object): +class _SeverityNumber( + google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[ + SeverityNumberValue + ] +): DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... - @classmethod - def Name(cls, number: builtin___int) -> builtin___str: ... - @classmethod - def Value(cls, name: builtin___str) -> SeverityNumberValue: ... - @classmethod - def keys(cls) -> typing___List[builtin___str]: ... - @classmethod - def values(cls) -> typing___List[SeverityNumberValue]: ... - @classmethod - def items( - cls, - ) -> typing___List[typing___Tuple[builtin___str, SeverityNumberValue]]: ... SEVERITY_NUMBER_UNSPECIFIED = typing___cast(SeverityNumberValue, 0) SEVERITY_NUMBER_TRACE = typing___cast(SeverityNumberValue, 1) SEVERITY_NUMBER_TRACE2 = typing___cast(SeverityNumberValue, 2) @@ -120,21 +110,14 @@ type___SeverityNumber = SeverityNumber LogRecordFlagsValue = typing___NewType("LogRecordFlagsValue", builtin___int) type___LogRecordFlagsValue = LogRecordFlagsValue +LogRecordFlags: _LogRecordFlags -class LogRecordFlags(object): +class _LogRecordFlags( + google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[ + LogRecordFlagsValue + ] +): DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... - @classmethod - def Name(cls, number: builtin___int) -> builtin___str: ... - @classmethod - def Value(cls, name: builtin___str) -> LogRecordFlagsValue: ... - @classmethod - def keys(cls) -> typing___List[builtin___str]: ... - @classmethod - def values(cls) -> typing___List[LogRecordFlagsValue]: ... - @classmethod - def items( - cls, - ) -> typing___List[typing___Tuple[builtin___str, LogRecordFlagsValue]]: ... LOG_RECORD_FLAG_UNSPECIFIED = typing___cast(LogRecordFlagsValue, 0) LOG_RECORD_FLAG_TRACE_FLAGS_MASK = typing___cast(LogRecordFlagsValue, 255) @@ -164,17 +147,6 @@ class ResourceLogs(google___protobuf___message___Message): typing___Iterable[type___InstrumentationLibraryLogs] ] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> ResourceLogs: ... - else: - @classmethod - def FromString( - cls, - s: typing___Union[ - builtin___bytes, builtin___buffer, builtin___unicode - ], - ) -> ResourceLogs: ... def HasField( self, field_name: typing_extensions___Literal["resource", b"resource"] ) -> builtin___bool: ... @@ -210,19 +182,6 @@ class InstrumentationLibraryLogs(google___protobuf___message___Message): ] = None, logs: typing___Optional[typing___Iterable[type___LogRecord]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString( - cls, s: builtin___bytes - ) -> InstrumentationLibraryLogs: ... - else: - @classmethod - def FromString( - cls, - s: typing___Union[ - builtin___bytes, builtin___buffer, builtin___unicode - ], - ) -> InstrumentationLibraryLogs: ... def HasField( self, field_name: typing_extensions___Literal[ @@ -281,17 +240,6 @@ class LogRecord(google___protobuf___message___Message): trace_id: typing___Optional[builtin___bytes] = None, span_id: typing___Optional[builtin___bytes] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> LogRecord: ... - else: - @classmethod - def FromString( - cls, - s: typing___Union[ - builtin___bytes, builtin___buffer, builtin___unicode - ], - ) -> LogRecord: ... def HasField( self, field_name: typing_extensions___Literal["body", b"body"] ) -> builtin___bool: ... diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi index ffe85a7a8b..eeb17d2f31 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi @@ -21,7 +21,6 @@ from typing import ( Iterable as typing___Iterable, Optional as typing___Optional, Text as typing___Text, - Union as typing___Union, ) from typing_extensions import ( @@ -33,9 +32,6 @@ builtin___bool = bool builtin___bytes = bytes builtin___float = float builtin___int = int -if sys.version_info < (3,): - builtin___buffer = buffer - builtin___unicode = unicode DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... @@ -52,12 +48,6 @@ class MetricConfigRequest(google___protobuf___message___Message): resource : typing___Optional[opentelemetry___proto___resource___v1___resource_pb2___Resource] = None, last_known_fingerprint : typing___Optional[builtin___bytes] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> MetricConfigRequest: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> MetricConfigRequest: ... def HasField(self, field_name: typing_extensions___Literal[u"resource",b"resource"]) -> builtin___bool: ... def ClearField(self, field_name: typing_extensions___Literal[u"last_known_fingerprint",b"last_known_fingerprint",u"resource",b"resource"]) -> None: ... type___MetricConfigRequest = MetricConfigRequest @@ -76,12 +66,6 @@ class MetricConfigResponse(google___protobuf___message___Message): equals : typing___Optional[typing___Text] = None, starts_with : typing___Optional[typing___Text] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> MetricConfigResponse.Schedule.Pattern: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> MetricConfigResponse.Schedule.Pattern: ... def HasField(self, field_name: typing_extensions___Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> builtin___bool: ... def ClearField(self, field_name: typing_extensions___Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions___Literal[u"match",b"match"]) -> typing_extensions___Literal["equals","starts_with"]: ... @@ -101,12 +85,6 @@ class MetricConfigResponse(google___protobuf___message___Message): inclusion_patterns : typing___Optional[typing___Iterable[type___MetricConfigResponse.Schedule.Pattern]] = None, period_sec : typing___Optional[builtin___int] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> MetricConfigResponse.Schedule: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> MetricConfigResponse.Schedule: ... def ClearField(self, field_name: typing_extensions___Literal[u"exclusion_patterns",b"exclusion_patterns",u"inclusion_patterns",b"inclusion_patterns",u"period_sec",b"period_sec"]) -> None: ... type___Schedule = Schedule @@ -122,11 +100,5 @@ class MetricConfigResponse(google___protobuf___message___Message): schedules : typing___Optional[typing___Iterable[type___MetricConfigResponse.Schedule]] = None, suggested_wait_time_sec : typing___Optional[builtin___int] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> MetricConfigResponse: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> MetricConfigResponse: ... def ClearField(self, field_name: typing_extensions___Literal[u"fingerprint",b"fingerprint",u"schedules",b"schedules",u"suggested_wait_time_sec",b"suggested_wait_time_sec"]) -> None: ... type___MetricConfigResponse = MetricConfigResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi index 16a6b3d4b5..5462660f08 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi @@ -11,6 +11,10 @@ from google.protobuf.internal.containers import ( RepeatedScalarFieldContainer as google___protobuf___internal___containers___RepeatedScalarFieldContainer, ) +from google.protobuf.internal.enum_type_wrapper import ( + _EnumTypeWrapper as google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper, +) + from google.protobuf.message import ( Message as google___protobuf___message___Message, ) @@ -26,12 +30,9 @@ from opentelemetry.proto.resource.v1.resource_pb2 import ( from typing import ( Iterable as typing___Iterable, - List as typing___List, NewType as typing___NewType, Optional as typing___Optional, Text as typing___Text, - Tuple as typing___Tuple, - Union as typing___Union, cast as typing___cast, ) @@ -44,28 +45,15 @@ builtin___bool = bool builtin___bytes = bytes builtin___float = float builtin___int = int -builtin___str = str -if sys.version_info < (3,): - builtin___buffer = buffer - builtin___unicode = unicode DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... AggregationTemporalityValue = typing___NewType('AggregationTemporalityValue', builtin___int) type___AggregationTemporalityValue = AggregationTemporalityValue -class AggregationTemporality(object): +AggregationTemporality: _AggregationTemporality +class _AggregationTemporality(google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[AggregationTemporalityValue]): DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... - @classmethod - def Name(cls, number: builtin___int) -> builtin___str: ... - @classmethod - def Value(cls, name: builtin___str) -> AggregationTemporalityValue: ... - @classmethod - def keys(cls) -> typing___List[builtin___str]: ... - @classmethod - def values(cls) -> typing___List[AggregationTemporalityValue]: ... - @classmethod - def items(cls) -> typing___List[typing___Tuple[builtin___str, AggregationTemporalityValue]]: ... AGGREGATION_TEMPORALITY_UNSPECIFIED = typing___cast(AggregationTemporalityValue, 0) AGGREGATION_TEMPORALITY_DELTA = typing___cast(AggregationTemporalityValue, 1) AGGREGATION_TEMPORALITY_CUMULATIVE = typing___cast(AggregationTemporalityValue, 2) @@ -88,12 +76,6 @@ class ResourceMetrics(google___protobuf___message___Message): resource : typing___Optional[opentelemetry___proto___resource___v1___resource_pb2___Resource] = None, instrumentation_library_metrics : typing___Optional[typing___Iterable[type___InstrumentationLibraryMetrics]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> ResourceMetrics: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ResourceMetrics: ... def HasField(self, field_name: typing_extensions___Literal[u"resource",b"resource"]) -> builtin___bool: ... def ClearField(self, field_name: typing_extensions___Literal[u"instrumentation_library_metrics",b"instrumentation_library_metrics",u"resource",b"resource"]) -> None: ... type___ResourceMetrics = ResourceMetrics @@ -112,12 +94,6 @@ class InstrumentationLibraryMetrics(google___protobuf___message___Message): instrumentation_library : typing___Optional[opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary] = None, metrics : typing___Optional[typing___Iterable[type___Metric]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> InstrumentationLibraryMetrics: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> InstrumentationLibraryMetrics: ... def HasField(self, field_name: typing_extensions___Literal[u"instrumentation_library",b"instrumentation_library"]) -> builtin___bool: ... def ClearField(self, field_name: typing_extensions___Literal[u"instrumentation_library",b"instrumentation_library",u"metrics",b"metrics"]) -> None: ... type___InstrumentationLibraryMetrics = InstrumentationLibraryMetrics @@ -158,12 +134,6 @@ class Metric(google___protobuf___message___Message): int_histogram : typing___Optional[type___IntHistogram] = None, double_histogram : typing___Optional[type___DoubleHistogram] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> Metric: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Metric: ... def HasField(self, field_name: typing_extensions___Literal[u"data",b"data",u"double_gauge",b"double_gauge",u"double_histogram",b"double_histogram",u"double_sum",b"double_sum",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum"]) -> builtin___bool: ... def ClearField(self, field_name: typing_extensions___Literal[u"data",b"data",u"description",b"description",u"double_gauge",b"double_gauge",u"double_histogram",b"double_histogram",u"double_sum",b"double_sum",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum",u"name",b"name",u"unit",b"unit"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions___Literal[u"data",b"data"]) -> typing_extensions___Literal["int_gauge","double_gauge","int_sum","double_sum","int_histogram","double_histogram"]: ... @@ -179,12 +149,6 @@ class IntGauge(google___protobuf___message___Message): *, data_points : typing___Optional[typing___Iterable[type___IntDataPoint]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> IntGauge: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> IntGauge: ... def ClearField(self, field_name: typing_extensions___Literal[u"data_points",b"data_points"]) -> None: ... type___IntGauge = IntGauge @@ -198,12 +162,6 @@ class DoubleGauge(google___protobuf___message___Message): *, data_points : typing___Optional[typing___Iterable[type___DoubleDataPoint]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> DoubleGauge: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> DoubleGauge: ... def ClearField(self, field_name: typing_extensions___Literal[u"data_points",b"data_points"]) -> None: ... type___DoubleGauge = DoubleGauge @@ -221,12 +179,6 @@ class IntSum(google___protobuf___message___Message): aggregation_temporality : typing___Optional[type___AggregationTemporalityValue] = None, is_monotonic : typing___Optional[builtin___bool] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> IntSum: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> IntSum: ... def ClearField(self, field_name: typing_extensions___Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points",u"is_monotonic",b"is_monotonic"]) -> None: ... type___IntSum = IntSum @@ -244,12 +196,6 @@ class DoubleSum(google___protobuf___message___Message): aggregation_temporality : typing___Optional[type___AggregationTemporalityValue] = None, is_monotonic : typing___Optional[builtin___bool] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> DoubleSum: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> DoubleSum: ... def ClearField(self, field_name: typing_extensions___Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points",u"is_monotonic",b"is_monotonic"]) -> None: ... type___DoubleSum = DoubleSum @@ -265,12 +211,6 @@ class IntHistogram(google___protobuf___message___Message): data_points : typing___Optional[typing___Iterable[type___IntHistogramDataPoint]] = None, aggregation_temporality : typing___Optional[type___AggregationTemporalityValue] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> IntHistogram: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> IntHistogram: ... def ClearField(self, field_name: typing_extensions___Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points"]) -> None: ... type___IntHistogram = IntHistogram @@ -286,12 +226,6 @@ class DoubleHistogram(google___protobuf___message___Message): data_points : typing___Optional[typing___Iterable[type___DoubleHistogramDataPoint]] = None, aggregation_temporality : typing___Optional[type___AggregationTemporalityValue] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> DoubleHistogram: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> DoubleHistogram: ... def ClearField(self, field_name: typing_extensions___Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points"]) -> None: ... type___DoubleHistogram = DoubleHistogram @@ -315,12 +249,6 @@ class IntDataPoint(google___protobuf___message___Message): value : typing___Optional[builtin___int] = None, exemplars : typing___Optional[typing___Iterable[type___IntExemplar]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> IntDataPoint: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> IntDataPoint: ... def ClearField(self, field_name: typing_extensions___Literal[u"exemplars",b"exemplars",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... type___IntDataPoint = IntDataPoint @@ -344,12 +272,6 @@ class DoubleDataPoint(google___protobuf___message___Message): value : typing___Optional[builtin___float] = None, exemplars : typing___Optional[typing___Iterable[type___DoubleExemplar]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> DoubleDataPoint: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> DoubleDataPoint: ... def ClearField(self, field_name: typing_extensions___Literal[u"exemplars",b"exemplars",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... type___DoubleDataPoint = DoubleDataPoint @@ -379,12 +301,6 @@ class IntHistogramDataPoint(google___protobuf___message___Message): explicit_bounds : typing___Optional[typing___Iterable[builtin___float]] = None, exemplars : typing___Optional[typing___Iterable[type___IntExemplar]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> IntHistogramDataPoint: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> IntHistogramDataPoint: ... def ClearField(self, field_name: typing_extensions___Literal[u"bucket_counts",b"bucket_counts",u"count",b"count",u"exemplars",b"exemplars",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... type___IntHistogramDataPoint = IntHistogramDataPoint @@ -414,12 +330,6 @@ class DoubleHistogramDataPoint(google___protobuf___message___Message): explicit_bounds : typing___Optional[typing___Iterable[builtin___float]] = None, exemplars : typing___Optional[typing___Iterable[type___DoubleExemplar]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> DoubleHistogramDataPoint: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> DoubleHistogramDataPoint: ... def ClearField(self, field_name: typing_extensions___Literal[u"bucket_counts",b"bucket_counts",u"count",b"count",u"exemplars",b"exemplars",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... type___DoubleHistogramDataPoint = DoubleHistogramDataPoint @@ -441,12 +351,6 @@ class IntExemplar(google___protobuf___message___Message): span_id : typing___Optional[builtin___bytes] = None, trace_id : typing___Optional[builtin___bytes] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> IntExemplar: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> IntExemplar: ... def ClearField(self, field_name: typing_extensions___Literal[u"filtered_labels",b"filtered_labels",u"span_id",b"span_id",u"time_unix_nano",b"time_unix_nano",u"trace_id",b"trace_id",u"value",b"value"]) -> None: ... type___IntExemplar = IntExemplar @@ -468,11 +372,5 @@ class DoubleExemplar(google___protobuf___message___Message): span_id : typing___Optional[builtin___bytes] = None, trace_id : typing___Optional[builtin___bytes] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> DoubleExemplar: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> DoubleExemplar: ... def ClearField(self, field_name: typing_extensions___Literal[u"filtered_labels",b"filtered_labels",u"span_id",b"span_id",u"time_unix_nano",b"time_unix_nano",u"trace_id",b"trace_id",u"value",b"value"]) -> None: ... type___DoubleExemplar = DoubleExemplar diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi index 0d65464986..4f6cb1419e 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi @@ -20,7 +20,6 @@ from opentelemetry.proto.common.v1.common_pb2 import ( from typing import ( Iterable as typing___Iterable, Optional as typing___Optional, - Union as typing___Union, ) from typing_extensions import ( @@ -32,9 +31,6 @@ builtin___bool = bool builtin___bytes = bytes builtin___float = float builtin___int = int -if sys.version_info < (3,): - builtin___buffer = buffer - builtin___unicode = unicode DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... @@ -51,11 +47,5 @@ class Resource(google___protobuf___message___Message): attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___KeyValue]] = None, dropped_attributes_count : typing___Optional[builtin___int] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> Resource: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Resource: ... def ClearField(self, field_name: typing_extensions___Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count"]) -> None: ... type___Resource = Resource diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py index de6ca90f5f..a30df48e2c 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py @@ -18,7 +18,7 @@ package='opentelemetry.proto.trace.v1', syntax='proto3', serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\020TraceConfigProtoP\001ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1', - serialized_pb=b'\n/opentelemetry/proto/trace/v1/trace_config.proto\x12\x1copentelemetry.proto.trace.v1\"\xc8\x03\n\x0bTraceConfig\x12I\n\x10\x63onstant_sampler\x18\x01 \x01(\x0b\x32-.opentelemetry.proto.trace.v1.ConstantSamplerH\x00\x12O\n\x13probability_sampler\x18\x02 \x01(\x0b\x32\x30.opentelemetry.proto.trace.v1.ProbabilitySamplerH\x00\x12R\n\x15rate_limiting_sampler\x18\x03 \x01(\x0b\x32\x31.opentelemetry.proto.trace.v1.RateLimitingSamplerH\x00\x12 \n\x18max_number_of_attributes\x18\x04 \x01(\x03\x12\"\n\x1amax_number_of_timed_events\x18\x05 \x01(\x03\x12\x30\n(max_number_of_attributes_per_timed_event\x18\x06 \x01(\x03\x12\x1b\n\x13max_number_of_links\x18\x07 \x01(\x03\x12)\n!max_number_of_attributes_per_link\x18\x08 \x01(\x03\x42\t\n\x07sampler\"\xa9\x01\n\x0f\x43onstantSampler\x12P\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32>.opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision\"D\n\x10\x43onstantDecision\x12\x0e\n\nALWAYS_OFF\x10\x00\x12\r\n\tALWAYS_ON\x10\x01\x12\x11\n\rALWAYS_PARENT\x10\x02\"1\n\x12ProbabilitySampler\x12\x1b\n\x13samplingProbability\x18\x01 \x01(\x01\"\"\n\x13RateLimitingSampler\x12\x0b\n\x03qps\x18\x01 \x01(\x03\x42~\n\x1fio.opentelemetry.proto.trace.v1B\x10TraceConfigProtoP\x01ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1b\x06proto3' + serialized_pb=b'\n/opentelemetry/proto/trace/v1/trace_config.proto\x12\x1copentelemetry.proto.trace.v1\"\xc8\x03\n\x0bTraceConfig\x12I\n\x10\x63onstant_sampler\x18\x01 \x01(\x0b\x32-.opentelemetry.proto.trace.v1.ConstantSamplerH\x00\x12O\n\x14trace_id_ratio_based\x18\x02 \x01(\x0b\x32/.opentelemetry.proto.trace.v1.TraceIdRatioBasedH\x00\x12R\n\x15rate_limiting_sampler\x18\x03 \x01(\x0b\x32\x31.opentelemetry.proto.trace.v1.RateLimitingSamplerH\x00\x12 \n\x18max_number_of_attributes\x18\x04 \x01(\x03\x12\"\n\x1amax_number_of_timed_events\x18\x05 \x01(\x03\x12\x30\n(max_number_of_attributes_per_timed_event\x18\x06 \x01(\x03\x12\x1b\n\x13max_number_of_links\x18\x07 \x01(\x03\x12)\n!max_number_of_attributes_per_link\x18\x08 \x01(\x03\x42\t\n\x07sampler\"\xa9\x01\n\x0f\x43onstantSampler\x12P\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32>.opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision\"D\n\x10\x43onstantDecision\x12\x0e\n\nALWAYS_OFF\x10\x00\x12\r\n\tALWAYS_ON\x10\x01\x12\x11\n\rALWAYS_PARENT\x10\x02\"*\n\x11TraceIdRatioBased\x12\x15\n\rsamplingRatio\x18\x01 \x01(\x01\"\"\n\x13RateLimitingSampler\x12\x0b\n\x03qps\x18\x01 \x01(\x03\x42~\n\x1fio.opentelemetry.proto.trace.v1B\x10TraceConfigProtoP\x01ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1b\x06proto3' ) @@ -65,7 +65,7 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='probability_sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.probability_sampler', index=1, + name='trace_id_ratio_based', full_name='opentelemetry.proto.trace.v1.TraceConfig.trace_id_ratio_based', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, @@ -165,15 +165,15 @@ ) -_PROBABILITYSAMPLER = _descriptor.Descriptor( - name='ProbabilitySampler', - full_name='opentelemetry.proto.trace.v1.ProbabilitySampler', +_TRACEIDRATIOBASED = _descriptor.Descriptor( + name='TraceIdRatioBased', + full_name='opentelemetry.proto.trace.v1.TraceIdRatioBased', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='samplingProbability', full_name='opentelemetry.proto.trace.v1.ProbabilitySampler.samplingProbability', index=0, + name='samplingRatio', full_name='opentelemetry.proto.trace.v1.TraceIdRatioBased.samplingRatio', index=0, number=1, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, @@ -192,7 +192,7 @@ oneofs=[ ], serialized_start=712, - serialized_end=761, + serialized_end=754, ) @@ -222,19 +222,19 @@ extension_ranges=[], oneofs=[ ], - serialized_start=763, - serialized_end=797, + serialized_start=756, + serialized_end=790, ) _TRACECONFIG.fields_by_name['constant_sampler'].message_type = _CONSTANTSAMPLER -_TRACECONFIG.fields_by_name['probability_sampler'].message_type = _PROBABILITYSAMPLER +_TRACECONFIG.fields_by_name['trace_id_ratio_based'].message_type = _TRACEIDRATIOBASED _TRACECONFIG.fields_by_name['rate_limiting_sampler'].message_type = _RATELIMITINGSAMPLER _TRACECONFIG.oneofs_by_name['sampler'].fields.append( _TRACECONFIG.fields_by_name['constant_sampler']) _TRACECONFIG.fields_by_name['constant_sampler'].containing_oneof = _TRACECONFIG.oneofs_by_name['sampler'] _TRACECONFIG.oneofs_by_name['sampler'].fields.append( - _TRACECONFIG.fields_by_name['probability_sampler']) -_TRACECONFIG.fields_by_name['probability_sampler'].containing_oneof = _TRACECONFIG.oneofs_by_name['sampler'] + _TRACECONFIG.fields_by_name['trace_id_ratio_based']) +_TRACECONFIG.fields_by_name['trace_id_ratio_based'].containing_oneof = _TRACECONFIG.oneofs_by_name['sampler'] _TRACECONFIG.oneofs_by_name['sampler'].fields.append( _TRACECONFIG.fields_by_name['rate_limiting_sampler']) _TRACECONFIG.fields_by_name['rate_limiting_sampler'].containing_oneof = _TRACECONFIG.oneofs_by_name['sampler'] @@ -242,7 +242,7 @@ _CONSTANTSAMPLER_CONSTANTDECISION.containing_type = _CONSTANTSAMPLER DESCRIPTOR.message_types_by_name['TraceConfig'] = _TRACECONFIG DESCRIPTOR.message_types_by_name['ConstantSampler'] = _CONSTANTSAMPLER -DESCRIPTOR.message_types_by_name['ProbabilitySampler'] = _PROBABILITYSAMPLER +DESCRIPTOR.message_types_by_name['TraceIdRatioBased'] = _TRACEIDRATIOBASED DESCRIPTOR.message_types_by_name['RateLimitingSampler'] = _RATELIMITINGSAMPLER _sym_db.RegisterFileDescriptor(DESCRIPTOR) @@ -260,12 +260,12 @@ }) _sym_db.RegisterMessage(ConstantSampler) -ProbabilitySampler = _reflection.GeneratedProtocolMessageType('ProbabilitySampler', (_message.Message,), { - 'DESCRIPTOR' : _PROBABILITYSAMPLER, +TraceIdRatioBased = _reflection.GeneratedProtocolMessageType('TraceIdRatioBased', (_message.Message,), { + 'DESCRIPTOR' : _TRACEIDRATIOBASED, '__module__' : 'opentelemetry.proto.trace.v1.trace_config_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.ProbabilitySampler) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.TraceIdRatioBased) }) -_sym_db.RegisterMessage(ProbabilitySampler) +_sym_db.RegisterMessage(TraceIdRatioBased) RateLimitingSampler = _reflection.GeneratedProtocolMessageType('RateLimitingSampler', (_message.Message,), { 'DESCRIPTOR' : _RATELIMITINGSAMPLER, diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi index 9cee74883c..0b0fe87c62 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi @@ -6,16 +6,17 @@ from google.protobuf.descriptor import ( FileDescriptor as google___protobuf___descriptor___FileDescriptor, ) +from google.protobuf.internal.enum_type_wrapper import ( + _EnumTypeWrapper as google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper, +) + from google.protobuf.message import ( Message as google___protobuf___message___Message, ) from typing import ( - List as typing___List, NewType as typing___NewType, Optional as typing___Optional, - Tuple as typing___Tuple, - Union as typing___Union, cast as typing___cast, ) @@ -28,10 +29,6 @@ builtin___bool = bool builtin___bytes = bytes builtin___float = float builtin___int = int -builtin___str = str -if sys.version_info < (3,): - builtin___buffer = buffer - builtin___unicode = unicode DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... @@ -48,7 +45,7 @@ class TraceConfig(google___protobuf___message___Message): def constant_sampler(self) -> type___ConstantSampler: ... @property - def probability_sampler(self) -> type___ProbabilitySampler: ... + def trace_id_ratio_based(self) -> type___TraceIdRatioBased: ... @property def rate_limiting_sampler(self) -> type___RateLimitingSampler: ... @@ -56,7 +53,7 @@ class TraceConfig(google___protobuf___message___Message): def __init__(self, *, constant_sampler : typing___Optional[type___ConstantSampler] = None, - probability_sampler : typing___Optional[type___ProbabilitySampler] = None, + trace_id_ratio_based : typing___Optional[type___TraceIdRatioBased] = None, rate_limiting_sampler : typing___Optional[type___RateLimitingSampler] = None, max_number_of_attributes : typing___Optional[builtin___int] = None, max_number_of_timed_events : typing___Optional[builtin___int] = None, @@ -64,33 +61,18 @@ class TraceConfig(google___protobuf___message___Message): max_number_of_links : typing___Optional[builtin___int] = None, max_number_of_attributes_per_link : typing___Optional[builtin___int] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> TraceConfig: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> TraceConfig: ... - def HasField(self, field_name: typing_extensions___Literal[u"constant_sampler",b"constant_sampler",u"probability_sampler",b"probability_sampler",u"rate_limiting_sampler",b"rate_limiting_sampler",u"sampler",b"sampler"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"constant_sampler",b"constant_sampler",u"max_number_of_attributes",b"max_number_of_attributes",u"max_number_of_attributes_per_link",b"max_number_of_attributes_per_link",u"max_number_of_attributes_per_timed_event",b"max_number_of_attributes_per_timed_event",u"max_number_of_links",b"max_number_of_links",u"max_number_of_timed_events",b"max_number_of_timed_events",u"probability_sampler",b"probability_sampler",u"rate_limiting_sampler",b"rate_limiting_sampler",u"sampler",b"sampler"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions___Literal[u"sampler",b"sampler"]) -> typing_extensions___Literal["constant_sampler","probability_sampler","rate_limiting_sampler"]: ... + def HasField(self, field_name: typing_extensions___Literal[u"constant_sampler",b"constant_sampler",u"rate_limiting_sampler",b"rate_limiting_sampler",u"sampler",b"sampler",u"trace_id_ratio_based",b"trace_id_ratio_based"]) -> builtin___bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"constant_sampler",b"constant_sampler",u"max_number_of_attributes",b"max_number_of_attributes",u"max_number_of_attributes_per_link",b"max_number_of_attributes_per_link",u"max_number_of_attributes_per_timed_event",b"max_number_of_attributes_per_timed_event",u"max_number_of_links",b"max_number_of_links",u"max_number_of_timed_events",b"max_number_of_timed_events",u"rate_limiting_sampler",b"rate_limiting_sampler",u"sampler",b"sampler",u"trace_id_ratio_based",b"trace_id_ratio_based"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions___Literal[u"sampler",b"sampler"]) -> typing_extensions___Literal["constant_sampler","trace_id_ratio_based","rate_limiting_sampler"]: ... type___TraceConfig = TraceConfig class ConstantSampler(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... ConstantDecisionValue = typing___NewType('ConstantDecisionValue', builtin___int) type___ConstantDecisionValue = ConstantDecisionValue - class ConstantDecision(object): + ConstantDecision: _ConstantDecision + class _ConstantDecision(google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[ConstantSampler.ConstantDecisionValue]): DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... - @classmethod - def Name(cls, number: builtin___int) -> builtin___str: ... - @classmethod - def Value(cls, name: builtin___str) -> ConstantSampler.ConstantDecisionValue: ... - @classmethod - def keys(cls) -> typing___List[builtin___str]: ... - @classmethod - def values(cls) -> typing___List[ConstantSampler.ConstantDecisionValue]: ... - @classmethod - def items(cls) -> typing___List[typing___Tuple[builtin___str, ConstantSampler.ConstantDecisionValue]]: ... ALWAYS_OFF = typing___cast(ConstantSampler.ConstantDecisionValue, 0) ALWAYS_ON = typing___cast(ConstantSampler.ConstantDecisionValue, 1) ALWAYS_PARENT = typing___cast(ConstantSampler.ConstantDecisionValue, 2) @@ -105,31 +87,19 @@ class ConstantSampler(google___protobuf___message___Message): *, decision : typing___Optional[type___ConstantSampler.ConstantDecisionValue] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> ConstantSampler: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ConstantSampler: ... def ClearField(self, field_name: typing_extensions___Literal[u"decision",b"decision"]) -> None: ... type___ConstantSampler = ConstantSampler -class ProbabilitySampler(google___protobuf___message___Message): +class TraceIdRatioBased(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - samplingProbability: builtin___float = ... + samplingRatio: builtin___float = ... def __init__(self, *, - samplingProbability : typing___Optional[builtin___float] = None, + samplingRatio : typing___Optional[builtin___float] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> ProbabilitySampler: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ProbabilitySampler: ... - def ClearField(self, field_name: typing_extensions___Literal[u"samplingProbability",b"samplingProbability"]) -> None: ... -type___ProbabilitySampler = ProbabilitySampler + def ClearField(self, field_name: typing_extensions___Literal[u"samplingRatio",b"samplingRatio"]) -> None: ... +type___TraceIdRatioBased = TraceIdRatioBased class RateLimitingSampler(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... @@ -139,11 +109,5 @@ class RateLimitingSampler(google___protobuf___message___Message): *, qps : typing___Optional[builtin___int] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> RateLimitingSampler: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> RateLimitingSampler: ... def ClearField(self, field_name: typing_extensions___Literal[u"qps",b"qps"]) -> None: ... type___RateLimitingSampler = RateLimitingSampler diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py index 7fb57c4bf0..d5937acb50 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py @@ -20,7 +20,7 @@ package='opentelemetry.proto.trace.v1', syntax='proto3', serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1', - serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xae\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\"\xa8\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xf0\x04\n\x06Status\x12=\n\x04\x63ode\x18\x01 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x95\x04\n\nStatusCode\x12\x12\n\x0eSTATUS_CODE_OK\x10\x00\x12\x19\n\x15STATUS_CODE_CANCELLED\x10\x01\x12\x1d\n\x19STATUS_CODE_UNKNOWN_ERROR\x10\x02\x12 \n\x1cSTATUS_CODE_INVALID_ARGUMENT\x10\x03\x12!\n\x1dSTATUS_CODE_DEADLINE_EXCEEDED\x10\x04\x12\x19\n\x15STATUS_CODE_NOT_FOUND\x10\x05\x12\x1e\n\x1aSTATUS_CODE_ALREADY_EXISTS\x10\x06\x12!\n\x1dSTATUS_CODE_PERMISSION_DENIED\x10\x07\x12\"\n\x1eSTATUS_CODE_RESOURCE_EXHAUSTED\x10\x08\x12#\n\x1fSTATUS_CODE_FAILED_PRECONDITION\x10\t\x12\x17\n\x13STATUS_CODE_ABORTED\x10\n\x12\x1c\n\x18STATUS_CODE_OUT_OF_RANGE\x10\x0b\x12\x1d\n\x19STATUS_CODE_UNIMPLEMENTED\x10\x0c\x12\x1e\n\x1aSTATUS_CODE_INTERNAL_ERROR\x10\r\x12\x1b\n\x17STATUS_CODE_UNAVAILABLE\x10\x0e\x12\x19\n\x15STATUS_CODE_DATA_LOSS\x10\x0f\x12\x1f\n\x1bSTATUS_CODE_UNAUTHENTICATED\x10\x10\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' + serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xae\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\"\xa8\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xdd\x07\n\x06Status\x12V\n\x0f\x64\x65precated_code\x18\x01 \x01(\x0e\x32\x39.opentelemetry.proto.trace.v1.Status.DeprecatedStatusCodeB\x02\x18\x01\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"\xda\x05\n\x14\x44\x65precatedStatusCode\x12\x1d\n\x19\x44\x45PRECATED_STATUS_CODE_OK\x10\x00\x12$\n DEPRECATED_STATUS_CODE_CANCELLED\x10\x01\x12(\n$DEPRECATED_STATUS_CODE_UNKNOWN_ERROR\x10\x02\x12+\n\'DEPRECATED_STATUS_CODE_INVALID_ARGUMENT\x10\x03\x12,\n(DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED\x10\x04\x12$\n DEPRECATED_STATUS_CODE_NOT_FOUND\x10\x05\x12)\n%DEPRECATED_STATUS_CODE_ALREADY_EXISTS\x10\x06\x12,\n(DEPRECATED_STATUS_CODE_PERMISSION_DENIED\x10\x07\x12-\n)DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED\x10\x08\x12.\n*DEPRECATED_STATUS_CODE_FAILED_PRECONDITION\x10\t\x12\"\n\x1e\x44\x45PRECATED_STATUS_CODE_ABORTED\x10\n\x12\'\n#DEPRECATED_STATUS_CODE_OUT_OF_RANGE\x10\x0b\x12(\n$DEPRECATED_STATUS_CODE_UNIMPLEMENTED\x10\x0c\x12)\n%DEPRECATED_STATUS_CODE_INTERNAL_ERROR\x10\r\x12&\n\"DEPRECATED_STATUS_CODE_UNAVAILABLE\x10\x0e\x12$\n DEPRECATED_STATUS_CODE_DATA_LOSS\x10\x0f\x12*\n&DEPRECATED_STATUS_CODE_UNAUTHENTICATED\x10\x10\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -64,85 +64,111 @@ ) _sym_db.RegisterEnumDescriptor(_SPAN_SPANKIND) -_STATUS_STATUSCODE = _descriptor.EnumDescriptor( - name='StatusCode', - full_name='opentelemetry.proto.trace.v1.Status.StatusCode', +_STATUS_DEPRECATEDSTATUSCODE = _descriptor.EnumDescriptor( + name='DeprecatedStatusCode', + full_name='opentelemetry.proto.trace.v1.Status.DeprecatedStatusCode', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( - name='STATUS_CODE_OK', index=0, number=0, + name='DEPRECATED_STATUS_CODE_OK', index=0, number=0, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_CANCELLED', index=1, number=1, + name='DEPRECATED_STATUS_CODE_CANCELLED', index=1, number=1, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_UNKNOWN_ERROR', index=2, number=2, + name='DEPRECATED_STATUS_CODE_UNKNOWN_ERROR', index=2, number=2, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_INVALID_ARGUMENT', index=3, number=3, + name='DEPRECATED_STATUS_CODE_INVALID_ARGUMENT', index=3, number=3, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_DEADLINE_EXCEEDED', index=4, number=4, + name='DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED', index=4, number=4, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_NOT_FOUND', index=5, number=5, + name='DEPRECATED_STATUS_CODE_NOT_FOUND', index=5, number=5, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_ALREADY_EXISTS', index=6, number=6, + name='DEPRECATED_STATUS_CODE_ALREADY_EXISTS', index=6, number=6, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_PERMISSION_DENIED', index=7, number=7, + name='DEPRECATED_STATUS_CODE_PERMISSION_DENIED', index=7, number=7, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_RESOURCE_EXHAUSTED', index=8, number=8, + name='DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED', index=8, number=8, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_FAILED_PRECONDITION', index=9, number=9, + name='DEPRECATED_STATUS_CODE_FAILED_PRECONDITION', index=9, number=9, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_ABORTED', index=10, number=10, + name='DEPRECATED_STATUS_CODE_ABORTED', index=10, number=10, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_OUT_OF_RANGE', index=11, number=11, + name='DEPRECATED_STATUS_CODE_OUT_OF_RANGE', index=11, number=11, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_UNIMPLEMENTED', index=12, number=12, + name='DEPRECATED_STATUS_CODE_UNIMPLEMENTED', index=12, number=12, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_INTERNAL_ERROR', index=13, number=13, + name='DEPRECATED_STATUS_CODE_INTERNAL_ERROR', index=13, number=13, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_UNAVAILABLE', index=14, number=14, + name='DEPRECATED_STATUS_CODE_UNAVAILABLE', index=14, number=14, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_DATA_LOSS', index=15, number=15, + name='DEPRECATED_STATUS_CODE_DATA_LOSS', index=15, number=15, serialized_options=None, type=None), _descriptor.EnumValueDescriptor( - name='STATUS_CODE_UNAUTHENTICATED', index=16, number=16, + name='DEPRECATED_STATUS_CODE_UNAUTHENTICATED', index=16, number=16, serialized_options=None, type=None), ], containing_type=None, serialized_options=None, - serialized_start=1607, - serialized_end=2140, + serialized_start=1695, + serialized_end=2425, +) +_sym_db.RegisterEnumDescriptor(_STATUS_DEPRECATEDSTATUSCODE) + +_STATUS_STATUSCODE = _descriptor.EnumDescriptor( + name='StatusCode', + full_name='opentelemetry.proto.trace.v1.Status.StatusCode', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='STATUS_CODE_UNSET', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='STATUS_CODE_OK', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='STATUS_CODE_ERROR', index=2, number=2, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=2427, + serialized_end=2505, ) _sym_db.RegisterEnumDescriptor(_STATUS_STATUSCODE) @@ -470,12 +496,12 @@ containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='code', full_name='opentelemetry.proto.trace.v1.Status.code', index=0, + name='deprecated_code', full_name='opentelemetry.proto.trace.v1.Status.deprecated_code', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=b'\030\001', file=DESCRIPTOR), _descriptor.FieldDescriptor( name='message', full_name='opentelemetry.proto.trace.v1.Status.message', index=1, number=2, type=9, cpp_type=9, label=1, @@ -483,11 +509,19 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='code', full_name='opentelemetry.proto.trace.v1.Status.code', index=2, + number=3, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], nested_types=[], enum_types=[ + _STATUS_DEPRECATEDSTATUSCODE, _STATUS_STATUSCODE, ], serialized_options=None, @@ -497,7 +531,7 @@ oneofs=[ ], serialized_start=1516, - serialized_end=2140, + serialized_end=2505, ) _RESOURCESPANS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE @@ -514,7 +548,9 @@ _SPAN.fields_by_name['links'].message_type = _SPAN_LINK _SPAN.fields_by_name['status'].message_type = _STATUS _SPAN_SPANKIND.containing_type = _SPAN +_STATUS.fields_by_name['deprecated_code'].enum_type = _STATUS_DEPRECATEDSTATUSCODE _STATUS.fields_by_name['code'].enum_type = _STATUS_STATUSCODE +_STATUS_DEPRECATEDSTATUSCODE.containing_type = _STATUS _STATUS_STATUSCODE.containing_type = _STATUS DESCRIPTOR.message_types_by_name['ResourceSpans'] = _RESOURCESPANS DESCRIPTOR.message_types_by_name['InstrumentationLibrarySpans'] = _INSTRUMENTATIONLIBRARYSPANS @@ -568,4 +604,5 @@ DESCRIPTOR._options = None +_STATUS.fields_by_name['deprecated_code']._options = None # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi index ad2c016705..fc1cf87e40 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi @@ -10,6 +10,10 @@ from google.protobuf.internal.containers import ( RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, ) +from google.protobuf.internal.enum_type_wrapper import ( + _EnumTypeWrapper as google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper, +) + from google.protobuf.message import ( Message as google___protobuf___message___Message, ) @@ -25,12 +29,9 @@ from opentelemetry.proto.resource.v1.resource_pb2 import ( from typing import ( Iterable as typing___Iterable, - List as typing___List, NewType as typing___NewType, Optional as typing___Optional, Text as typing___Text, - Tuple as typing___Tuple, - Union as typing___Union, cast as typing___cast, ) @@ -43,10 +44,6 @@ builtin___bool = bool builtin___bytes = bytes builtin___float = float builtin___int = int -builtin___str = str -if sys.version_info < (3,): - builtin___buffer = buffer - builtin___unicode = unicode DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... @@ -65,12 +62,6 @@ class ResourceSpans(google___protobuf___message___Message): resource : typing___Optional[opentelemetry___proto___resource___v1___resource_pb2___Resource] = None, instrumentation_library_spans : typing___Optional[typing___Iterable[type___InstrumentationLibrarySpans]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> ResourceSpans: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ResourceSpans: ... def HasField(self, field_name: typing_extensions___Literal[u"resource",b"resource"]) -> builtin___bool: ... def ClearField(self, field_name: typing_extensions___Literal[u"instrumentation_library_spans",b"instrumentation_library_spans",u"resource",b"resource"]) -> None: ... type___ResourceSpans = ResourceSpans @@ -89,12 +80,6 @@ class InstrumentationLibrarySpans(google___protobuf___message___Message): instrumentation_library : typing___Optional[opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary] = None, spans : typing___Optional[typing___Iterable[type___Span]] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> InstrumentationLibrarySpans: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> InstrumentationLibrarySpans: ... def HasField(self, field_name: typing_extensions___Literal[u"instrumentation_library",b"instrumentation_library"]) -> builtin___bool: ... def ClearField(self, field_name: typing_extensions___Literal[u"instrumentation_library",b"instrumentation_library",u"spans",b"spans"]) -> None: ... type___InstrumentationLibrarySpans = InstrumentationLibrarySpans @@ -103,18 +88,9 @@ class Span(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... SpanKindValue = typing___NewType('SpanKindValue', builtin___int) type___SpanKindValue = SpanKindValue - class SpanKind(object): + SpanKind: _SpanKind + class _SpanKind(google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[Span.SpanKindValue]): DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... - @classmethod - def Name(cls, number: builtin___int) -> builtin___str: ... - @classmethod - def Value(cls, name: builtin___str) -> Span.SpanKindValue: ... - @classmethod - def keys(cls) -> typing___List[builtin___str]: ... - @classmethod - def values(cls) -> typing___List[Span.SpanKindValue]: ... - @classmethod - def items(cls) -> typing___List[typing___Tuple[builtin___str, Span.SpanKindValue]]: ... SPAN_KIND_UNSPECIFIED = typing___cast(Span.SpanKindValue, 0) SPAN_KIND_INTERNAL = typing___cast(Span.SpanKindValue, 1) SPAN_KIND_SERVER = typing___cast(Span.SpanKindValue, 2) @@ -145,12 +121,6 @@ class Span(google___protobuf___message___Message): attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___KeyValue]] = None, dropped_attributes_count : typing___Optional[builtin___int] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> Span.Event: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Span.Event: ... def ClearField(self, field_name: typing_extensions___Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"name",b"name",u"time_unix_nano",b"time_unix_nano"]) -> None: ... type___Event = Event @@ -172,12 +142,6 @@ class Span(google___protobuf___message___Message): attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___KeyValue]] = None, dropped_attributes_count : typing___Optional[builtin___int] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> Span.Link: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Span.Link: ... def ClearField(self, field_name: typing_extensions___Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"span_id",b"span_id",u"trace_id",b"trace_id",u"trace_state",b"trace_state"]) -> None: ... type___Link = Link @@ -223,81 +187,75 @@ class Span(google___protobuf___message___Message): dropped_links_count : typing___Optional[builtin___int] = None, status : typing___Optional[type___Status] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> Span: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Span: ... def HasField(self, field_name: typing_extensions___Literal[u"status",b"status"]) -> builtin___bool: ... def ClearField(self, field_name: typing_extensions___Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"dropped_events_count",b"dropped_events_count",u"dropped_links_count",b"dropped_links_count",u"end_time_unix_nano",b"end_time_unix_nano",u"events",b"events",u"kind",b"kind",u"links",b"links",u"name",b"name",u"parent_span_id",b"parent_span_id",u"span_id",b"span_id",u"start_time_unix_nano",b"start_time_unix_nano",u"status",b"status",u"trace_id",b"trace_id",u"trace_state",b"trace_state"]) -> None: ... type___Span = Span class Status(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + DeprecatedStatusCodeValue = typing___NewType('DeprecatedStatusCodeValue', builtin___int) + type___DeprecatedStatusCodeValue = DeprecatedStatusCodeValue + DeprecatedStatusCode: _DeprecatedStatusCode + class _DeprecatedStatusCode(google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[Status.DeprecatedStatusCodeValue]): + DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... + DEPRECATED_STATUS_CODE_OK = typing___cast(Status.DeprecatedStatusCodeValue, 0) + DEPRECATED_STATUS_CODE_CANCELLED = typing___cast(Status.DeprecatedStatusCodeValue, 1) + DEPRECATED_STATUS_CODE_UNKNOWN_ERROR = typing___cast(Status.DeprecatedStatusCodeValue, 2) + DEPRECATED_STATUS_CODE_INVALID_ARGUMENT = typing___cast(Status.DeprecatedStatusCodeValue, 3) + DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED = typing___cast(Status.DeprecatedStatusCodeValue, 4) + DEPRECATED_STATUS_CODE_NOT_FOUND = typing___cast(Status.DeprecatedStatusCodeValue, 5) + DEPRECATED_STATUS_CODE_ALREADY_EXISTS = typing___cast(Status.DeprecatedStatusCodeValue, 6) + DEPRECATED_STATUS_CODE_PERMISSION_DENIED = typing___cast(Status.DeprecatedStatusCodeValue, 7) + DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED = typing___cast(Status.DeprecatedStatusCodeValue, 8) + DEPRECATED_STATUS_CODE_FAILED_PRECONDITION = typing___cast(Status.DeprecatedStatusCodeValue, 9) + DEPRECATED_STATUS_CODE_ABORTED = typing___cast(Status.DeprecatedStatusCodeValue, 10) + DEPRECATED_STATUS_CODE_OUT_OF_RANGE = typing___cast(Status.DeprecatedStatusCodeValue, 11) + DEPRECATED_STATUS_CODE_UNIMPLEMENTED = typing___cast(Status.DeprecatedStatusCodeValue, 12) + DEPRECATED_STATUS_CODE_INTERNAL_ERROR = typing___cast(Status.DeprecatedStatusCodeValue, 13) + DEPRECATED_STATUS_CODE_UNAVAILABLE = typing___cast(Status.DeprecatedStatusCodeValue, 14) + DEPRECATED_STATUS_CODE_DATA_LOSS = typing___cast(Status.DeprecatedStatusCodeValue, 15) + DEPRECATED_STATUS_CODE_UNAUTHENTICATED = typing___cast(Status.DeprecatedStatusCodeValue, 16) + DEPRECATED_STATUS_CODE_OK = typing___cast(Status.DeprecatedStatusCodeValue, 0) + DEPRECATED_STATUS_CODE_CANCELLED = typing___cast(Status.DeprecatedStatusCodeValue, 1) + DEPRECATED_STATUS_CODE_UNKNOWN_ERROR = typing___cast(Status.DeprecatedStatusCodeValue, 2) + DEPRECATED_STATUS_CODE_INVALID_ARGUMENT = typing___cast(Status.DeprecatedStatusCodeValue, 3) + DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED = typing___cast(Status.DeprecatedStatusCodeValue, 4) + DEPRECATED_STATUS_CODE_NOT_FOUND = typing___cast(Status.DeprecatedStatusCodeValue, 5) + DEPRECATED_STATUS_CODE_ALREADY_EXISTS = typing___cast(Status.DeprecatedStatusCodeValue, 6) + DEPRECATED_STATUS_CODE_PERMISSION_DENIED = typing___cast(Status.DeprecatedStatusCodeValue, 7) + DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED = typing___cast(Status.DeprecatedStatusCodeValue, 8) + DEPRECATED_STATUS_CODE_FAILED_PRECONDITION = typing___cast(Status.DeprecatedStatusCodeValue, 9) + DEPRECATED_STATUS_CODE_ABORTED = typing___cast(Status.DeprecatedStatusCodeValue, 10) + DEPRECATED_STATUS_CODE_OUT_OF_RANGE = typing___cast(Status.DeprecatedStatusCodeValue, 11) + DEPRECATED_STATUS_CODE_UNIMPLEMENTED = typing___cast(Status.DeprecatedStatusCodeValue, 12) + DEPRECATED_STATUS_CODE_INTERNAL_ERROR = typing___cast(Status.DeprecatedStatusCodeValue, 13) + DEPRECATED_STATUS_CODE_UNAVAILABLE = typing___cast(Status.DeprecatedStatusCodeValue, 14) + DEPRECATED_STATUS_CODE_DATA_LOSS = typing___cast(Status.DeprecatedStatusCodeValue, 15) + DEPRECATED_STATUS_CODE_UNAUTHENTICATED = typing___cast(Status.DeprecatedStatusCodeValue, 16) + type___DeprecatedStatusCode = DeprecatedStatusCode + StatusCodeValue = typing___NewType('StatusCodeValue', builtin___int) type___StatusCodeValue = StatusCodeValue - class StatusCode(object): + StatusCode: _StatusCode + class _StatusCode(google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[Status.StatusCodeValue]): DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... - @classmethod - def Name(cls, number: builtin___int) -> builtin___str: ... - @classmethod - def Value(cls, name: builtin___str) -> Status.StatusCodeValue: ... - @classmethod - def keys(cls) -> typing___List[builtin___str]: ... - @classmethod - def values(cls) -> typing___List[Status.StatusCodeValue]: ... - @classmethod - def items(cls) -> typing___List[typing___Tuple[builtin___str, Status.StatusCodeValue]]: ... - STATUS_CODE_OK = typing___cast(Status.StatusCodeValue, 0) - STATUS_CODE_CANCELLED = typing___cast(Status.StatusCodeValue, 1) - STATUS_CODE_UNKNOWN_ERROR = typing___cast(Status.StatusCodeValue, 2) - STATUS_CODE_INVALID_ARGUMENT = typing___cast(Status.StatusCodeValue, 3) - STATUS_CODE_DEADLINE_EXCEEDED = typing___cast(Status.StatusCodeValue, 4) - STATUS_CODE_NOT_FOUND = typing___cast(Status.StatusCodeValue, 5) - STATUS_CODE_ALREADY_EXISTS = typing___cast(Status.StatusCodeValue, 6) - STATUS_CODE_PERMISSION_DENIED = typing___cast(Status.StatusCodeValue, 7) - STATUS_CODE_RESOURCE_EXHAUSTED = typing___cast(Status.StatusCodeValue, 8) - STATUS_CODE_FAILED_PRECONDITION = typing___cast(Status.StatusCodeValue, 9) - STATUS_CODE_ABORTED = typing___cast(Status.StatusCodeValue, 10) - STATUS_CODE_OUT_OF_RANGE = typing___cast(Status.StatusCodeValue, 11) - STATUS_CODE_UNIMPLEMENTED = typing___cast(Status.StatusCodeValue, 12) - STATUS_CODE_INTERNAL_ERROR = typing___cast(Status.StatusCodeValue, 13) - STATUS_CODE_UNAVAILABLE = typing___cast(Status.StatusCodeValue, 14) - STATUS_CODE_DATA_LOSS = typing___cast(Status.StatusCodeValue, 15) - STATUS_CODE_UNAUTHENTICATED = typing___cast(Status.StatusCodeValue, 16) - STATUS_CODE_OK = typing___cast(Status.StatusCodeValue, 0) - STATUS_CODE_CANCELLED = typing___cast(Status.StatusCodeValue, 1) - STATUS_CODE_UNKNOWN_ERROR = typing___cast(Status.StatusCodeValue, 2) - STATUS_CODE_INVALID_ARGUMENT = typing___cast(Status.StatusCodeValue, 3) - STATUS_CODE_DEADLINE_EXCEEDED = typing___cast(Status.StatusCodeValue, 4) - STATUS_CODE_NOT_FOUND = typing___cast(Status.StatusCodeValue, 5) - STATUS_CODE_ALREADY_EXISTS = typing___cast(Status.StatusCodeValue, 6) - STATUS_CODE_PERMISSION_DENIED = typing___cast(Status.StatusCodeValue, 7) - STATUS_CODE_RESOURCE_EXHAUSTED = typing___cast(Status.StatusCodeValue, 8) - STATUS_CODE_FAILED_PRECONDITION = typing___cast(Status.StatusCodeValue, 9) - STATUS_CODE_ABORTED = typing___cast(Status.StatusCodeValue, 10) - STATUS_CODE_OUT_OF_RANGE = typing___cast(Status.StatusCodeValue, 11) - STATUS_CODE_UNIMPLEMENTED = typing___cast(Status.StatusCodeValue, 12) - STATUS_CODE_INTERNAL_ERROR = typing___cast(Status.StatusCodeValue, 13) - STATUS_CODE_UNAVAILABLE = typing___cast(Status.StatusCodeValue, 14) - STATUS_CODE_DATA_LOSS = typing___cast(Status.StatusCodeValue, 15) - STATUS_CODE_UNAUTHENTICATED = typing___cast(Status.StatusCodeValue, 16) + STATUS_CODE_UNSET = typing___cast(Status.StatusCodeValue, 0) + STATUS_CODE_OK = typing___cast(Status.StatusCodeValue, 1) + STATUS_CODE_ERROR = typing___cast(Status.StatusCodeValue, 2) + STATUS_CODE_UNSET = typing___cast(Status.StatusCodeValue, 0) + STATUS_CODE_OK = typing___cast(Status.StatusCodeValue, 1) + STATUS_CODE_ERROR = typing___cast(Status.StatusCodeValue, 2) type___StatusCode = StatusCode - code: type___Status.StatusCodeValue = ... + deprecated_code: type___Status.DeprecatedStatusCodeValue = ... message: typing___Text = ... + code: type___Status.StatusCodeValue = ... def __init__(self, *, - code : typing___Optional[type___Status.StatusCodeValue] = None, + deprecated_code : typing___Optional[type___Status.DeprecatedStatusCodeValue] = None, message : typing___Optional[typing___Text] = None, + code : typing___Optional[type___Status.StatusCodeValue] = None, ) -> None: ... - if sys.version_info >= (3,): - @classmethod - def FromString(cls, s: builtin___bytes) -> Status: ... - else: - @classmethod - def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Status: ... - def ClearField(self, field_name: typing_extensions___Literal[u"code",b"code",u"message",b"message"]) -> None: ... + def ClearField(self, field_name: typing_extensions___Literal[u"code",b"code",u"deprecated_code",b"deprecated_code",u"message",b"message"]) -> None: ... type___Status = Status diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index e06a8a9320..f70286eca4 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -12,7 +12,7 @@ # PROTO_REPO_DIR - the path to an existing checkout of the opentelemetry-proto repo # Pinned commit/branch/tag for the current version used in opentelemetry-proto python package. -PROTO_REPO_BRANCH_OR_COMMIT="v0.5.0" +PROTO_REPO_BRANCH_OR_COMMIT="v0.6.0" set -e From ff1a058f6a4c563716d2e991a0f1e3cccb92c2bd Mon Sep 17 00:00:00 2001 From: Shovnik Bhattacharya Date: Wed, 6 Jan 2021 23:54:40 -0500 Subject: [PATCH 0729/1517] Added CodeQL Analysis workflow (#1505) --- .github/workflows/codeql-analysis.yml | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..b979a3121b --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,35 @@ +name: CodeQL Analysis + +on: + workflow_dispatch: + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # │ │ │ │ │ + # │ │ │ │ │ + # │ │ │ │ │ + # * * * * * + - cron: '30 1 * * *' + +jobs: + CodeQL-Build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: python + + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 \ No newline at end of file From eb7125d7a7fb42dde67088e055c6053ea6c103b2 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 6 Jan 2021 21:14:55 -0800 Subject: [PATCH 0730/1517] Pass OTLP metadata as tuple instead of string (#1507) --- CHANGELOG.md | 3 ++ .../opentelemetry/exporter/otlp/exporter.py | 6 +++- .../otlp/metrics_exporter/__init__.py | 2 +- .../exporter/otlp/trace_exporter/__init__.py | 2 +- .../tests/test_otlp_metric_exporter.py | 33 +++++++++++++++++-- .../tests/test_otlp_trace_exporter.py | 33 +++++++++++++++++-- 6 files changed, 72 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b4c8efa86..040665e91e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1486](https://github.com/open-telemetry/opentelemetry-python/pull/1486)) - Recreate span on every run of a `start_as_current_span`-decorated function ([#1451](https://github.com/open-telemetry/opentelemetry-python/pull/1451)) +- `opentelemetry-exporter-otlp` Headers are now passed in as tuple as metadata, instead of a + string, which was incorrect. + ([#1507](https://github.com/open-telemetry/opentelemetry-python/pull/1507)) ## [0.16b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b1) - 2020-11-26 ### Added diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index 915e5f4d3e..e00f119b00 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -151,7 +151,7 @@ def __init__( endpoint: Optional[str] = None, insecure: Optional[bool] = None, credentials: Optional[ChannelCredentials] = None, - headers: Optional[str] = None, + headers: Optional[Sequence] = None, timeout: Optional[int] = None, compression: str = None, ): @@ -169,6 +169,10 @@ def __init__( insecure = False self._headers = headers or Configuration().EXPORTER_OTLP_HEADERS + if isinstance(self._headers, str): + self._headers = tuple( + tuple(item.split("=")) for item in self._headers.split(",") + ) self._timeout = ( timeout or Configuration().EXPORTER_OTLP_TIMEOUT diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py index 559e313477..c371c177e9 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -139,7 +139,7 @@ def __init__( endpoint: Optional[str] = None, insecure: Optional[bool] = None, credentials: Optional[ChannelCredentials] = None, - headers: Optional[str] = None, + headers: Optional[Sequence] = None, timeout: Optional[int] = None, ): if insecure is None: diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index 0c9ffd79f2..b872b624a1 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -69,7 +69,7 @@ def __init__( endpoint: Optional[str] = None, insecure: Optional[bool] = None, credentials: Optional[ChannelCredentials] = None, - headers: Optional[str] = None, + headers: Optional[Sequence] = None, timeout: Optional[int] = None, ): if insecure is None: diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index c8664f94fa..6ecce722a3 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -73,7 +73,7 @@ def tearDown(self): "OTEL_EXPORTER_OTLP_METRIC_ENDPOINT": "collector:55680", "OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE": THIS_DIR + "/fixtures/test.cert", - "OTEL_EXPORTER_OTLP_METRIC_HEADERS": "key1:value1;key2:value2", + "OTEL_EXPORTER_OTLP_METRIC_HEADERS": "key1=value1,key2=value2", "OTEL_EXPORTER_OTLP_METRIC_TIMEOUT": "10", }, ) @@ -85,7 +85,7 @@ def test_env_variables(self, mock_exporter_mixin): _, kwargs = mock_exporter_mixin.call_args_list[0] self.assertEqual(kwargs["endpoint"], "collector:55680") - self.assertEqual(kwargs["headers"], "key1:value1;key2:value2") + self.assertEqual(kwargs["headers"], "key1=value1,key2=value2") self.assertEqual(kwargs["timeout"], 10) self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) @@ -102,6 +102,35 @@ def test_no_credentials_error( OTLPMetricsExporter(insecure=False) self.assertTrue(mock_ssl_channel.called) + @patch.dict( + "os.environ", + {"OTEL_EXPORTER_OTLP_METRIC_HEADERS": "key1=value1,key2=value2"}, + ) + @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") + @patch("opentelemetry.exporter.otlp.exporter.secure_channel") + # pylint: disable=unused-argument + def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): + exporter = OTLPMetricsExporter() + # pylint: disable=protected-access + self.assertEqual( + exporter._headers, (("key1", "value1"), ("key2", "value2")), + ) + exporter = OTLPMetricsExporter( + headers=(("key3", "value3"), ("key4", "value4")) + ) + # pylint: disable=protected-access + self.assertEqual( + exporter._headers, (("key3", "value3"), ("key4", "value4")), + ) + + @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") + @patch("opentelemetry.exporter.otlp.exporter.secure_channel") + # pylint: disable=unused-argument + def test_otlp_headers(self, mock_ssl_channel, mock_secure): + exporter = OTLPMetricsExporter() + # pylint: disable=protected-access + self.assertIsNone(exporter._headers, None) + @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") def test_translate_counter_export_record(self, mock_time_ns): mock_time_ns.configure_mock(**{"return_value": 1}) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index 8afb964edb..a152d1c69c 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -172,7 +172,7 @@ def tearDown(self): "OTEL_EXPORTER_OTLP_SPAN_ENDPOINT": "collector:55680", "OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE": THIS_DIR + "/fixtures/test.cert", - "OTEL_EXPORTER_OTLP_SPAN_HEADERS": "key1:value1;key2:value2", + "OTEL_EXPORTER_OTLP_SPAN_HEADERS": "key1=value1,key2=value2", "OTEL_EXPORTER_OTLP_SPAN_TIMEOUT": "10", }, ) @@ -184,7 +184,7 @@ def test_env_variables(self, mock_exporter_mixin): _, kwargs = mock_exporter_mixin.call_args_list[0] self.assertEqual(kwargs["endpoint"], "collector:55680") - self.assertEqual(kwargs["headers"], "key1:value1;key2:value2") + self.assertEqual(kwargs["headers"], "key1=value1,key2=value2") self.assertEqual(kwargs["timeout"], 10) self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) @@ -199,6 +199,35 @@ def test_no_credentials_error( OTLPSpanExporter(insecure=False) self.assertTrue(mock_ssl_channel.called) + @patch.dict( + "os.environ", + {"OTEL_EXPORTER_OTLP_SPAN_HEADERS": "key1=value1,key2=value2"}, + ) + @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") + @patch("opentelemetry.exporter.otlp.exporter.secure_channel") + # pylint: disable=unused-argument + def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): + exporter = OTLPSpanExporter() + # pylint: disable=protected-access + self.assertEqual( + exporter._headers, (("key1", "value1"), ("key2", "value2")) + ) + exporter = OTLPSpanExporter( + headers=(("key3", "value3"), ("key4", "value4")) + ) + # pylint: disable=protected-access + self.assertEqual( + exporter._headers, (("key3", "value3"), ("key4", "value4")) + ) + + @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") + @patch("opentelemetry.exporter.otlp.exporter.secure_channel") + # pylint: disable=unused-argument + def test_otlp_headers(self, mock_ssl_channel, mock_secure): + exporter = OTLPSpanExporter() + # pylint: disable=protected-access + self.assertIsNone(exporter._headers, None) + @patch("opentelemetry.exporter.otlp.exporter.expo") @patch("opentelemetry.exporter.otlp.exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): From cd39fc17b21baf68eddc4ddd0acfe2e80df59eae Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 7 Jan 2021 21:35:29 +0530 Subject: [PATCH 0731/1517] Update Jaeger exporter status code (#1488) --- CHANGELOG.md | 2 ++ .../exporter/jaeger/translate/protobuf.py | 28 ++++++++++++------- .../exporter/jaeger/translate/thrift.py | 21 ++++++++++---- .../tests/test_jaeger_exporter_thrift.py | 24 ++++++---------- .../tests/test_jarget_exporter_protobuf.py | 26 ++++++----------- 5 files changed, 52 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 040665e91e..7d77447ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-exporter-otlp` Headers are now passed in as tuple as metadata, instead of a string, which was incorrect. ([#1507](https://github.com/open-telemetry/opentelemetry-python/pull/1507)) +- `opentelemetry-exporter-jaeger` Updated Jaeger exporter status code tag + ([#1488](https://github.com/open-telemetry/opentelemetry-python/pull/1488)) ## [0.16b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b1) - 2020-11-26 ### Added diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py index f43bdfb599..cc7354217a 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py @@ -24,7 +24,7 @@ VERSION_KEY, Translator, ) -from opentelemetry.sdk.trace import Span +from opentelemetry.sdk.trace import Span, StatusCode from opentelemetry.util import types # pylint: disable=no-member,too-many-locals,no-self-use @@ -190,16 +190,24 @@ def _extract_tags(self, span: Span) -> Sequence[model_pb2.KeyValue]: if key_value: translated.append(key_value) - code = _get_long_key_value( - "status.code", span.status.status_code.value - ) - message = _get_string_key_value( - "status.message", span.status.description - ) - kind = _get_string_key_value( - "span.kind", OTLP_JAEGER_SPAN_KIND[span.kind] + status = span.status + if status.status_code is not StatusCode.UNSET: + translated.append( + _get_string_key_value( + "otel.status_code", status.status_code.name + ) + ) + if status.description is not None: + translated.append( + _get_string_key_value( + "otel.status_description", status.description + ) + ) + translated.append( + _get_string_key_value( + "span.kind", OTLP_JAEGER_SPAN_KIND[span.kind] + ) ) - translated.extend([code, message, kind]) # Instrumentation info KeyValues if span.instrumentation_info: diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py index 710ba85a6e..8ca371c1b5 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py @@ -23,7 +23,7 @@ _convert_int_to_i64, _nsec_to_usec_round, ) -from opentelemetry.sdk.trace import Span +from opentelemetry.sdk.trace import Span, StatusCode from opentelemetry.util import types @@ -120,10 +120,21 @@ def _extract_tags(self, span: Span) -> Sequence[TCollector.Tag]: if tag: translated.append(tag) - code = _get_long_tag("status.code", span.status.status_code.value) - message = _get_string_tag("status.message", span.status.description) - kind = _get_string_tag("span.kind", OTLP_JAEGER_SPAN_KIND[span.kind]) - translated.extend([code, message, kind]) + status = span.status + if status.status_code is not StatusCode.UNSET: + translated.append( + _get_string_tag("otel.status_code", status.status_code.name) + ) + if status.description is not None: + translated.append( + _get_string_tag( + "otel.status_description", status.description + ) + ) + + translated.append( + _get_string_tag("span.kind", OTLP_JAEGER_SPAN_KIND[span.kind]) + ) # Instrumentation info tags if span.instrumentation_info: diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py index 853542c7bb..132d92b1e1 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py @@ -221,14 +221,6 @@ def test_translate_to_jaeger(self): ) default_tags = [ - jaeger.Tag( - key="status.code", - vType=jaeger.TagType.LONG, - vLong=StatusCode.UNSET.value, - ), - jaeger.Tag( - key="status.message", vType=jaeger.TagType.STRING, vStr=None - ), jaeger.Tag( key="span.kind", vType=jaeger.TagType.STRING, vStr="internal", ), @@ -316,12 +308,12 @@ def test_translate_to_jaeger(self): vStr="some_resource", ), jaeger.Tag( - key="status.code", - vType=jaeger.TagType.LONG, - vLong=StatusCode.ERROR.value, + key="otel.status_code", + vType=jaeger.TagType.STRING, + vStr="ERROR", ), jaeger.Tag( - key="status.message", + key="otel.status_description", vType=jaeger.TagType.STRING, vStr="Example description", ), @@ -392,12 +384,12 @@ def test_translate_to_jaeger(self): flags=0, tags=[ jaeger.Tag( - key="status.code", - vType=jaeger.TagType.LONG, - vLong=StatusCode.OK.value, + key="otel.status_code", + vType=jaeger.TagType.STRING, + vStr="OK", ), jaeger.Tag( - key="status.message", + key="otel.status_description", vType=jaeger.TagType.STRING, vStr="Example description", ), diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py index cf19428ded..f6f7d99bb5 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py @@ -140,16 +140,6 @@ def test_translate_to_jaeger(self): ) default_tags = [ - model_pb2.KeyValue( - key="status.code", - v_type=model_pb2.ValueType.INT64, - v_int64=StatusCode.UNSET.value, - ), - model_pb2.KeyValue( - key="status.message", - v_type=model_pb2.ValueType.STRING, - v_str=None, - ), model_pb2.KeyValue( key="span.kind", v_type=model_pb2.ValueType.STRING, @@ -269,12 +259,12 @@ def test_translate_to_jaeger(self): v_str="some_resource", ), model_pb2.KeyValue( - key="status.code", - v_type=model_pb2.ValueType.INT64, - v_int64=StatusCode.ERROR.value, + key="otel.status_code", + v_type=model_pb2.ValueType.STRING, + v_str="ERROR", ), model_pb2.KeyValue( - key="status.message", + key="otel.status_description", v_type=model_pb2.ValueType.STRING, v_str="Example description", ), @@ -353,12 +343,12 @@ def test_translate_to_jaeger(self): flags=0, tags=[ model_pb2.KeyValue( - key="status.code", - v_type=model_pb2.ValueType.INT64, - v_int64=StatusCode.OK.value, + key="otel.status_code", + v_type=model_pb2.ValueType.STRING, + v_str="OK", ), model_pb2.KeyValue( - key="status.message", + key="otel.status_description", v_type=model_pb2.ValueType.STRING, v_str="Example description", ), From 09ac955f5301fbb3d4fc521bdf86c0a76b4a3387 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 11 Jan 2021 12:39:23 -0800 Subject: [PATCH 0732/1517] Move idsgenerator from api into sdk package (#1514) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 3 +++ docs/api/trace.ids_generator.rst | 7 ------- docs/api/trace.rst | 1 - docs/sdk/trace.ids_generator.rst | 7 +++++++ docs/sdk/trace.rst | 3 ++- opentelemetry-api/setup.cfg | 2 -- .../src/opentelemetry/trace/__init__.py | 3 --- opentelemetry-sdk/setup.cfg | 2 ++ .../sdk/configuration/__init__.py | 7 ++++--- .../src/opentelemetry/sdk/trace/__init__.py | 10 +++++++--- .../opentelemetry/sdk}/trace/ids_generator.py | 0 .../tests/configuration/test_configurator.py | 18 +++++++++--------- .../tests/trace/propagation/test_b3_format.py | 19 ++++++++++--------- .../propagation/test_jaeger_propagator.py | 11 ++++++----- opentelemetry-sdk/tests/trace/test_trace.py | 5 +++-- 16 files changed, 54 insertions(+), 46 deletions(-) delete mode 100644 docs/api/trace.ids_generator.rst create mode 100644 docs/sdk/trace.ids_generator.rst rename {opentelemetry-api/src/opentelemetry => opentelemetry-sdk/src/opentelemetry/sdk}/trace/ids_generator.py (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d702613f32..16e0799b08 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: master + CONTRIB_REPO_SHA: 32cac7a9ff6c831aa0e9514bb38c430fce819141 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d77447ad3..7128e62bfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1285](https://github.com/open-telemetry/opentelemetry-python/pull/1285)) - Added `__repr__` for `DefaultSpan`, added `trace_flags` to `__repr__` of `SpanContext` ([#1485](https://github.com/open-telemetry/opentelemetry-python/pull/1485)]) + ### Changed - `opentelemetry-exporter-zipkin` Updated zipkin exporter status code and error tag ([#1486](https://github.com/open-telemetry/opentelemetry-python/pull/1486)) @@ -46,6 +47,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1507](https://github.com/open-telemetry/opentelemetry-python/pull/1507)) - `opentelemetry-exporter-jaeger` Updated Jaeger exporter status code tag ([#1488](https://github.com/open-telemetry/opentelemetry-python/pull/1488)) +- `opentelemetry-api` `opentelemety-sdk` Moved `idsgenerator` into sdk + ([#1514](https://github.com/open-telemetry/opentelemetry-python/pull/1514)) ## [0.16b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b1) - 2020-11-26 ### Added diff --git a/docs/api/trace.ids_generator.rst b/docs/api/trace.ids_generator.rst deleted file mode 100644 index 8f516bb3b1..0000000000 --- a/docs/api/trace.ids_generator.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.trace.ids_generator -================================= - -.. automodule:: opentelemetry.trace.ids_generator - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/api/trace.rst b/docs/api/trace.rst index 81f11e3dd0..65d9b4d8c8 100644 --- a/docs/api/trace.rst +++ b/docs/api/trace.rst @@ -8,7 +8,6 @@ Submodules trace.status trace.span - trace.ids_generator Module contents --------------- diff --git a/docs/sdk/trace.ids_generator.rst b/docs/sdk/trace.ids_generator.rst new file mode 100644 index 0000000000..ab5ab14951 --- /dev/null +++ b/docs/sdk/trace.ids_generator.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.trace.ids_generator +===================================== + +.. automodule:: opentelemetry.sdk.trace.ids_generator + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/sdk/trace.rst b/docs/sdk/trace.rst index 0b53444e3b..22e4fcc13f 100644 --- a/docs/sdk/trace.rst +++ b/docs/sdk/trace.rst @@ -1,5 +1,5 @@ opentelemetry.sdk.trace package -========================================== +=============================== Submodules ---------- @@ -7,6 +7,7 @@ Submodules .. toctree:: trace.export + trace.ids_generator trace.sampling util.instrumentation diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 8bb3e6cc6f..ab0fcd01eb 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -58,8 +58,6 @@ opentelemetry_tracer_provider = opentelemetry_propagator = tracecontext = opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator baggage = opentelemetry.baggage.propagation:BaggagePropagator -opentelemetry_ids_generator = - random = opentelemetry.trace.ids_generator:RandomIdsGenerator [options.extras_require] test = diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 2b13b5b056..7371b8d72d 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -81,7 +81,6 @@ from logging import getLogger from opentelemetry.context.context import Context -from opentelemetry.trace.ids_generator import IdsGenerator, RandomIdsGenerator from opentelemetry.trace.propagation import ( get_current_span, set_span_in_context, @@ -459,7 +458,6 @@ def get_tracer_provider() -> TracerProvider: __all__ = [ "DEFAULT_TRACE_OPTIONS", "DEFAULT_TRACE_STATE", - "IdsGenerator", "INVALID_SPAN", "INVALID_SPAN_CONTEXT", "INVALID_SPAN_ID", @@ -469,7 +467,6 @@ def get_tracer_provider() -> TracerProvider: "DefaultTracerProvider", "Link", "LinkBase", - "RandomIdsGenerator", "Span", "SpanContext", "SpanKind", diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 765d3891ec..37d219eece 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -60,6 +60,8 @@ opentelemetry_exporter = console_metrics = opentelemetry.sdk.metrics.export:ConsoleMetricsExporter opentelemetry_configurator = sdk_configurator = opentelemetry.sdk.configuration:Configurator +opentelemetry_ids_generator = + random = opentelemetry.sdk.trace.ids_generator:RandomIdsGenerator [options.extras_require] test = diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py index 477aa5cf62..b8a99bb62c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py @@ -27,6 +27,7 @@ BatchExportSpanProcessor, SpanExporter, ) +from opentelemetry.sdk.trace.ids_generator import IdsGenerator logger = getLogger(__file__) @@ -64,7 +65,7 @@ def _get_exporter_names() -> Sequence[str]: def _init_tracing( - exporters: Sequence[SpanExporter], ids_generator: trace.IdsGenerator + exporters: Sequence[SpanExporter], ids_generator: IdsGenerator ): service_name = _get_service_name() provider = TracerProvider( @@ -137,7 +138,7 @@ def _import_exporters( return trace_exporters, metric_exporters -def _import_ids_generator(ids_generator_name: str) -> trace.IdsGenerator: +def _import_ids_generator(ids_generator_name: str) -> IdsGenerator: # pylint: disable=unbalanced-tuple-unpacking [ (ids_generator_name, ids_generator_impl) @@ -145,7 +146,7 @@ def _import_ids_generator(ids_generator_name: str) -> trace.IdsGenerator: [ids_generator_name.strip()], "opentelemetry_ids_generator" ) - if issubclass(ids_generator_impl, trace.IdsGenerator): + if issubclass(ids_generator_impl, IdsGenerator): return ids_generator_impl raise RuntimeError("{0} is not an IdsGenerator".format(ids_generator_name)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index c8dec7bc62..c5b736902a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -41,6 +41,10 @@ from opentelemetry.sdk import util from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import sampling +from opentelemetry.sdk.trace.ids_generator import ( + IdsGenerator, + RandomIdsGenerator, +) from opentelemetry.sdk.util import BoundedDict, BoundedList from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanContext @@ -733,7 +737,7 @@ def __init__( span_processor: Union[ SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor ], - ids_generator: trace_api.IdsGenerator, + ids_generator: IdsGenerator, instrumentation_info: InstrumentationInfo, ) -> None: self.sampler = sampler @@ -893,13 +897,13 @@ def __init__( active_span_processor: Union[ SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor ] = None, - ids_generator: trace_api.IdsGenerator = None, + ids_generator: IdsGenerator = None, ): self._active_span_processor = ( active_span_processor or SynchronousMultiSpanProcessor() ) if ids_generator is None: - self.ids_generator = trace_api.RandomIdsGenerator() + self.ids_generator = RandomIdsGenerator() else: self.ids_generator = ids_generator self.resource = resource diff --git a/opentelemetry-api/src/opentelemetry/trace/ids_generator.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/ids_generator.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/trace/ids_generator.py rename to opentelemetry-sdk/src/opentelemetry/sdk/trace/ids_generator.py diff --git a/opentelemetry-sdk/tests/configuration/test_configurator.py b/opentelemetry-sdk/tests/configuration/test_configurator.py index c8851321f1..ebd6768444 100644 --- a/opentelemetry-sdk/tests/configuration/test_configurator.py +++ b/opentelemetry-sdk/tests/configuration/test_configurator.py @@ -24,7 +24,10 @@ _init_tracing, ) from opentelemetry.sdk.resources import Resource -from opentelemetry.trace.ids_generator import RandomIdsGenerator +from opentelemetry.sdk.trace.ids_generator import ( + IdsGenerator, + RandomIdsGenerator, +) class Provider: @@ -54,12 +57,12 @@ class OTLPExporter: pass -class IdsGenerator: - pass - - class CustomIdsGenerator(IdsGenerator): - pass + def generate_span_id(self): + pass + + def generate_trace_id(self): + pass class IterEntryPoint: @@ -130,9 +133,6 @@ def test_trace_init_otlp(self): del environ["OTEL_SERVICE_NAME"] @patch.dict(environ, {"OTEL_IDS_GENERATOR": "custom_ids_generator"}) - @patch( - "opentelemetry.sdk.configuration.trace.IdsGenerator", new=IdsGenerator, - ) @patch("opentelemetry.sdk.configuration.iter_entry_points") def test_trace_init_custom_ids_generator(self, mock_iter_entry_points): mock_iter_entry_points.configure_mock( diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index 93d5bbe56e..ede25e4289 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -16,6 +16,7 @@ from unittest.mock import Mock, patch import opentelemetry.sdk.trace as trace +import opentelemetry.sdk.trace.ids_generator as ids_generator import opentelemetry.sdk.trace.propagation.b3_format as b3_format import opentelemetry.trace as trace_api from opentelemetry.context import get_current @@ -37,7 +38,7 @@ def get_child_parent_new_carrier(old_carrier): "child", trace_api.SpanContext( parent_span_context.trace_id, - trace_api.RandomIdsGenerator().generate_span_id(), + ids_generator.RandomIdsGenerator().generate_span_id(), is_remote=False, trace_flags=parent_span_context.trace_flags, trace_state=parent_span_context.trace_state, @@ -55,15 +56,15 @@ def get_child_parent_new_carrier(old_carrier): class TestB3Format(unittest.TestCase): @classmethod def setUpClass(cls): - ids_generator = trace_api.RandomIdsGenerator() + id_generator = ids_generator.RandomIdsGenerator() cls.serialized_trace_id = b3_format.format_trace_id( - ids_generator.generate_trace_id() + id_generator.generate_trace_id() ) cls.serialized_span_id = b3_format.format_span_id( - ids_generator.generate_span_id() + id_generator.generate_span_id() ) cls.serialized_parent_id = b3_format.format_span_id( - ids_generator.generate_span_id() + id_generator.generate_span_id() ) def setUp(self) -> None: @@ -255,10 +256,10 @@ def test_missing_trace_id(self): self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) @patch( - "opentelemetry.sdk.trace.propagation.b3_format.trace.RandomIdsGenerator.generate_trace_id" + "opentelemetry.sdk.trace.ids_generator.RandomIdsGenerator.generate_trace_id" ) @patch( - "opentelemetry.sdk.trace.propagation.b3_format.trace.RandomIdsGenerator.generate_span_id" + "opentelemetry.sdk.trace.ids_generator.RandomIdsGenerator.generate_span_id" ) def test_invalid_trace_id( self, mock_generate_span_id, mock_generate_trace_id @@ -281,10 +282,10 @@ def test_invalid_trace_id( self.assertEqual(span_context.span_id, 2) @patch( - "opentelemetry.sdk.trace.propagation.b3_format.trace.RandomIdsGenerator.generate_trace_id" + "opentelemetry.sdk.trace.ids_generator.RandomIdsGenerator.generate_trace_id" ) @patch( - "opentelemetry.sdk.trace.propagation.b3_format.trace.RandomIdsGenerator.generate_span_id" + "opentelemetry.sdk.trace.ids_generator.RandomIdsGenerator.generate_span_id" ) def test_invalid_span_id( self, mock_generate_span_id, mock_generate_trace_id diff --git a/opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py b/opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py index bd80770368..aff6062bbf 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py @@ -16,6 +16,7 @@ from unittest.mock import Mock import opentelemetry.sdk.trace as trace +import opentelemetry.sdk.trace.ids_generator as ids_generator import opentelemetry.sdk.trace.propagation.jaeger_propagator as jaeger import opentelemetry.trace as trace_api from opentelemetry import baggage @@ -40,7 +41,7 @@ def get_context_new_carrier(old_carrier, carrier_baggage=None): "child", trace_api.SpanContext( parent_span_context.trace_id, - trace_api.RandomIdsGenerator().generate_span_id(), + ids_generator.RandomIdsGenerator().generate_span_id(), is_remote=False, trace_flags=parent_span_context.trace_flags, trace_state=parent_span_context.trace_state, @@ -65,10 +66,10 @@ def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags): class TestJaegerPropagator(unittest.TestCase): @classmethod def setUpClass(cls): - ids_generator = trace_api.RandomIdsGenerator() - cls.trace_id = ids_generator.generate_trace_id() - cls.span_id = ids_generator.generate_span_id() - cls.parent_span_id = ids_generator.generate_span_id() + id_generator = ids_generator.RandomIdsGenerator() + cls.trace_id = id_generator.generate_trace_id() + cls.span_id = id_generator.generate_span_id() + cls.parent_span_id = id_generator.generate_span_id() cls.serialized_uber_trace_id = _format_uber_trace_id( cls.trace_id, cls.span_id, cls.parent_span_id, 11 ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index bfefdfcb42..6e69542f77 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -28,6 +28,7 @@ from opentelemetry.context import Context from opentelemetry.sdk import resources, trace from opentelemetry.sdk.trace import Resource, sampling +from opentelemetry.sdk.trace.ids_generator import RandomIdsGenerator from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace.status import StatusCode @@ -715,7 +716,7 @@ def test_invalid_event_attributes(self): self.assertEqual(root.events[3].attributes, {"attr2": (1, 2)}) def test_links(self): - ids_generator = trace_api.RandomIdsGenerator() + ids_generator = RandomIdsGenerator() other_context1 = trace_api.SpanContext( trace_id=ids_generator.generate_trace_id(), span_id=ids_generator.generate_span_id(), @@ -1241,7 +1242,7 @@ def tearDown(self): def test_span_environment_limits(self): reload(trace) tracer = new_tracer() - ids_generator = trace_api.RandomIdsGenerator() + ids_generator = RandomIdsGenerator() some_links = [ trace_api.Link( trace_api.SpanContext( From b326793803118b49f094e7e830ab3ff36db495f8 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 12 Jan 2021 21:47:01 +0530 Subject: [PATCH 0733/1517] Remove ThreadLocalRuntimeContext since py34 is no longer supported (#1512) --- CHANGELOG.md | 3 + opentelemetry-api/setup.cfg | 1 - .../context/threadlocal_context.py | 60 ------------------- .../tests/context/test_threadlocal_context.py | 33 ---------- 4 files changed, 3 insertions(+), 94 deletions(-) delete mode 100644 opentelemetry-api/src/opentelemetry/context/threadlocal_context.py delete mode 100644 opentelemetry-api/tests/context/test_threadlocal_context.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7128e62bfd..49b292522d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-api` `opentelemety-sdk` Moved `idsgenerator` into sdk ([#1514](https://github.com/open-telemetry/opentelemetry-python/pull/1514)) +### Removed +- `opentelemetry-api` Remove ThreadLocalRuntimeContext since python3.4 is not supported. + ## [0.16b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b1) - 2020-11-26 ### Added - Add meter reference to observers diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index ab0fcd01eb..7deb389f18 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -50,7 +50,6 @@ where = src [options.entry_points] opentelemetry_context = contextvars_context = opentelemetry.context.contextvars_context:ContextVarsRuntimeContext - threadlocal_context = opentelemetry.context.threadlocal_context:ThreadLocalRuntimeContext opentelemetry_meter_provider = default_meter_provider = opentelemetry.metrics:DefaultMeterProvider opentelemetry_tracer_provider = diff --git a/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py b/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py deleted file mode 100644 index 43e9fb7ce9..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading - -from opentelemetry.context.context import Context, RuntimeContext - - -class ThreadLocalRuntimeContext(RuntimeContext): - """An implementation of the RuntimeContext interface - which uses thread-local storage under the hood. This - implementation is available for usage with Python 3.4. - """ - - class Token: - def __init__(self, context: Context) -> None: - self._context = context - - _CONTEXT_KEY = "current_context" - - def __init__(self) -> None: - self._current_context = threading.local() - - def attach(self, context: Context) -> object: - """See `opentelemetry.context.RuntimeContext.attach`.""" - current = self.get_current() - setattr(self._current_context, self._CONTEXT_KEY, context) - return self.Token(current) - - def get_current(self) -> Context: - """See `opentelemetry.context.RuntimeContext.get_current`.""" - if not hasattr(self._current_context, self._CONTEXT_KEY): - setattr( - self._current_context, self._CONTEXT_KEY, Context(), - ) - context = getattr( - self._current_context, self._CONTEXT_KEY - ) # type: Context - return context - - def detach(self, token: object) -> None: - """See `opentelemetry.context.RuntimeContext.detach`.""" - if not isinstance(token, self.Token): - raise ValueError("invalid token") - # pylint: disable=protected-access - setattr(self._current_context, self._CONTEXT_KEY, token._context) - - -__all__ = ["ThreadLocalRuntimeContext"] diff --git a/opentelemetry-api/tests/context/test_threadlocal_context.py b/opentelemetry-api/tests/context/test_threadlocal_context.py deleted file mode 100644 index 02c62ba180..0000000000 --- a/opentelemetry-api/tests/context/test_threadlocal_context.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest.mock import patch - -from opentelemetry import context -from opentelemetry.context.threadlocal_context import ThreadLocalRuntimeContext - -from .base_context import ContextTestCases - - -class TestThreadLocalContext(ContextTestCases.BaseTest): - def setUp(self) -> None: - super(TestThreadLocalContext, self).setUp() - self.mock_runtime = patch.object( - context, "_RUNTIME_CONTEXT", ThreadLocalRuntimeContext(), - ) - self.mock_runtime.start() - - def tearDown(self) -> None: - super(TestThreadLocalContext, self).tearDown() - self.mock_runtime.stop() From 287efdc3fffd7f27be44af84d59ab22bc99c9af4 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 12 Jan 2021 11:24:01 -0800 Subject: [PATCH 0734/1517] Move b3 propagator out of SDK (#1513) --- CHANGELOG.md | 4 +- README.md | 8 + docs/getting-started.rst | 8 +- opentelemetry-sdk/setup.cfg | 2 - .../opentelemetry-propagator-b3/LICENSE | 201 ++++++++++++++++++ .../opentelemetry-propagator-b3/MANIFEST.in | 9 + .../opentelemetry-propagator-b3/README.rst | 23 ++ .../opentelemetry-propagator-b3/setup.cfg | 53 +++++ .../opentelemetry-propagator-b3/setup.py | 26 +++ .../opentelemetry/propagators/b3/__init__.py | 0 .../opentelemetry/propagators/b3/version.py | 15 ++ .../tests/__init__.py | 13 ++ .../tests}/test_b3_format.py | 2 +- scripts/build.sh | 2 +- tox.ini | 9 + 15 files changed, 368 insertions(+), 7 deletions(-) create mode 100644 propagator/opentelemetry-propagator-b3/LICENSE create mode 100644 propagator/opentelemetry-propagator-b3/MANIFEST.in create mode 100644 propagator/opentelemetry-propagator-b3/README.rst create mode 100644 propagator/opentelemetry-propagator-b3/setup.cfg create mode 100644 propagator/opentelemetry-propagator-b3/setup.py rename opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py => propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py (100%) create mode 100644 propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py create mode 100644 propagator/opentelemetry-propagator-b3/tests/__init__.py rename {opentelemetry-sdk/tests/trace/propagation => propagator/opentelemetry-propagator-b3/tests}/test_b3_format.py (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b292522d..3cc65a13c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add support for OTLP v0.6.0 ([#1472](https://github.com/open-telemetry/opentelemetry-python/pull/1472)) - - Add protobuf via gRPC exporting support for Jaeger ([#1471](https://github.com/open-telemetry/opentelemetry-python/pull/1471)) - - Add support for Python 3.9 ([#1441](https://github.com/open-telemetry/opentelemetry-python/pull/1441)) @@ -49,6 +47,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1488](https://github.com/open-telemetry/opentelemetry-python/pull/1488)) - `opentelemetry-api` `opentelemety-sdk` Moved `idsgenerator` into sdk ([#1514](https://github.com/open-telemetry/opentelemetry-python/pull/1514)) +- `opentelemetry-sdk` The B3Format propagator has been moved into its own package: `opentelemetry-propagator-b3` + ([#1513](https://github.com/open-telemetry/opentelemetry-python/pull/1513)) ### Removed - `opentelemetry-api` Remove ThreadLocalRuntimeContext since python3.4 is not supported. diff --git a/README.md b/README.md index 8d211cd118..9043aeace9 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,14 @@ directory includes OpenTelemetry exporter packages. You can install the packages pip install opentelemetry-exporter-{exporter} ``` +The +[`propagator/`](https://github.com/open-telemetry/opentelemetry-python/tree/master/propagator) +directory includes OpenTelemetry propagator packages. You can install the packages separately with the following command: + +```sh +pip install opentelemetry-propagator-{propagator} +``` + To install the development versions of these packages instead, clone or fork this repository and perform an [editable install](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs): diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 7e0ab3c312..228e254df6 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -187,10 +187,16 @@ By default, ``opentelemetry-python`` is configured to use the `W3C Trace Context HTTP headers for HTTP requests, but you can configure it to leverage different propagators. Here's an example using Zipkin's `b3 propagation `_: +.. code-block:: sh + + pip install opentelemetry-propagator-b3 + +Following the installation of the package containing the b3 propagator, configure the propagator as follows: + .. code-block:: python from opentelemetry import propagators - from opentelemetry.sdk.trace.propagation.b3_format import B3Format + from opentelemetry.propagators.b3 import B3Format propagators.set_global_textmap(B3Format()) diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 37d219eece..f7ad42ba99 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -53,8 +53,6 @@ opentelemetry_meter_provider = sdk_meter_provider = opentelemetry.sdk.metrics:MeterProvider opentelemetry_tracer_provider = sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider -opentelemetry_propagator = - b3 = opentelemetry.sdk.trace.propagation.b3_format:B3Format opentelemetry_exporter = console_span = opentelemetry.sdk.trace.export:ConsoleSpanExporter console_metrics = opentelemetry.sdk.metrics.export:ConsoleMetricsExporter diff --git a/propagator/opentelemetry-propagator-b3/LICENSE b/propagator/opentelemetry-propagator-b3/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/propagator/opentelemetry-propagator-b3/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/propagator/opentelemetry-propagator-b3/MANIFEST.in b/propagator/opentelemetry-propagator-b3/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/propagator/opentelemetry-propagator-b3/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/propagator/opentelemetry-propagator-b3/README.rst b/propagator/opentelemetry-propagator-b3/README.rst new file mode 100644 index 0000000000..2ff3f9df11 --- /dev/null +++ b/propagator/opentelemetry-propagator-b3/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry B3 Propagator +=========================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-propagator-b3.svg + :target: https://pypi.org/project/opentelemetry-propagator-b3/ + +This library provides a propagator for the B3 format + +Installation +------------ + +:: + + pip install opentelemetry-propagator-b3 + + +References +---------- + +* `OpenTelemetry `_ +* `B3 format `_ diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg new file mode 100644 index 0000000000..9916bc343a --- /dev/null +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -0,0 +1,53 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-propagator-b3 +description = OpenTelemetry B3 Propagator +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/propagator/opentelemetry-propagator-b3 +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.17.dev0 + +[options.extras_require] +test = + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_propagator = + b3 = opentelemetry.propagators.b3:B3Format \ No newline at end of file diff --git a/propagator/opentelemetry-propagator-b3/setup.py b/propagator/opentelemetry-propagator-b3/setup.py new file mode 100644 index 0000000000..4df5188b1d --- /dev/null +++ b/propagator/opentelemetry-propagator-b3/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "propagators", "b3", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py similarity index 100% rename from opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py rename to propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py new file mode 100644 index 0000000000..86e1dbb17a --- /dev/null +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.17.dev0" diff --git a/propagator/opentelemetry-propagator-b3/tests/__init__.py b/propagator/opentelemetry-propagator-b3/tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/propagator/opentelemetry-propagator-b3/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py similarity index 99% rename from opentelemetry-sdk/tests/trace/propagation/test_b3_format.py rename to propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index ede25e4289..aea06b47fb 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -15,9 +15,9 @@ import unittest from unittest.mock import Mock, patch +import opentelemetry.propagators.b3 as b3_format # pylint: disable=no-name-in-module,import-error import opentelemetry.sdk.trace as trace import opentelemetry.sdk.trace.ids_generator as ids_generator -import opentelemetry.sdk.trace.propagation.b3_format as b3_format import opentelemetry.trace as trace_api from opentelemetry.context import get_current from opentelemetry.trace.propagation.textmap import DictGetter diff --git a/scripts/build.sh b/scripts/build.sh index 0dec4a27ba..853a297003 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ exporter/*/ instrumentation/*/ ; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ exporter/*/ instrumentation/*/ propagator/*/; do ( echo "building $d" cd "$d" diff --git a/tox.ini b/tox.ini index fb5ebee5ab..497d3f3556 100644 --- a/tox.ini +++ b/tox.ini @@ -47,6 +47,10 @@ envlist = py3{5,6,7,8,9}-test-core-opentracing-shim pypy3-test-core-opentracing-shim + ; opentelemetry-propagator-b3 + py3{5,6,7,8,9}-test-propagator-b3 + pypy3-test-propagator-b3 + lint tracecontext mypy,mypyinstalled @@ -77,6 +81,8 @@ changedir = test-exporter-otlp: exporter/opentelemetry-exporter-otlp/tests test-exporter-prometheus: exporter/opentelemetry-exporter-prometheus/tests test-exporter-zipkin: exporter/opentelemetry-exporter-zipkin/tests + + test-propagator-b3: propagator/opentelemetry-propagator-b3/tests commands_pre = ; Install without -e to test the actual installation @@ -104,6 +110,8 @@ commands_pre = zipkin: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin + b3: pip install {toxinidir}/propagator/opentelemetry-propagator-b3 + ; In order to get a healthy coverage report, ; we have to install packages in editable mode. coverage: python {toxinidir}/scripts/eachdist.py install --editable @@ -150,6 +158,7 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-prometheus[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin[test] + python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-b3[test] commands = python scripts/eachdist.py lint --check-only From db74594d93b7e42d26ed2061b7672d1ad823f30c Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 13 Jan 2021 01:44:49 +0530 Subject: [PATCH 0735/1517] Update default port for OTLP exporter to 4317 (#1516) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/exporter/otlp/__init__.py | 2 +- .../src/opentelemetry/exporter/otlp/exporter.py | 2 +- .../tests/test_otlp_metric_exporter.py | 4 ++-- .../tests/test_otlp_trace_exporter.py | 6 +++--- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cc65a13c6..b9ffe83453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1514](https://github.com/open-telemetry/opentelemetry-python/pull/1514)) - `opentelemetry-sdk` The B3Format propagator has been moved into its own package: `opentelemetry-propagator-b3` ([#1513](https://github.com/open-telemetry/opentelemetry-python/pull/1513)) +- Update default port for OTLP exporter from 55680 to 4317 + ([#1516](https://github.com/open-telemetry/opentelemetry-python/pull/1516)) ### Removed - `opentelemetry-api` Remove ThreadLocalRuntimeContext since python3.4 is not supported. diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py index 7265003fee..1094be286d 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py @@ -53,7 +53,7 @@ trace.set_tracer_provider(TracerProvider(resource=resource)) tracer = trace.get_tracer(__name__) - otlp_exporter = OTLPSpanExporter(endpoint="localhost:55680", insecure=True) + otlp_exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True) span_processor = BatchExportSpanProcessor(otlp_exporter) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index e00f119b00..a4ae1edcfb 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -160,7 +160,7 @@ def __init__( endpoint = ( endpoint or Configuration().EXPORTER_OTLP_ENDPOINT - or "localhost:55680" + or "localhost:4317" ) if insecure is None: diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index 6ecce722a3..946a313a44 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -70,7 +70,7 @@ def tearDown(self): @patch.dict( "os.environ", { - "OTEL_EXPORTER_OTLP_METRIC_ENDPOINT": "collector:55680", + "OTEL_EXPORTER_OTLP_METRIC_ENDPOINT": "collector:4317", "OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE": THIS_DIR + "/fixtures/test.cert", "OTEL_EXPORTER_OTLP_METRIC_HEADERS": "key1=value1,key2=value2", @@ -84,7 +84,7 @@ def test_env_variables(self, mock_exporter_mixin): self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) _, kwargs = mock_exporter_mixin.call_args_list[0] - self.assertEqual(kwargs["endpoint"], "collector:55680") + self.assertEqual(kwargs["endpoint"], "collector:4317") self.assertEqual(kwargs["headers"], "key1=value1,key2=value2") self.assertEqual(kwargs["timeout"], 10) self.assertIsNotNone(kwargs["credentials"]) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index a152d1c69c..5dde0c9772 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -116,7 +116,7 @@ def setUp(self): self.server = server(ThreadPoolExecutor(max_workers=10)) - self.server.add_insecure_port("[::]:55680") + self.server.add_insecure_port("[::]:4317") self.server.start() @@ -169,7 +169,7 @@ def tearDown(self): @patch.dict( "os.environ", { - "OTEL_EXPORTER_OTLP_SPAN_ENDPOINT": "collector:55680", + "OTEL_EXPORTER_OTLP_SPAN_ENDPOINT": "collector:4317", "OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE": THIS_DIR + "/fixtures/test.cert", "OTEL_EXPORTER_OTLP_SPAN_HEADERS": "key1=value1,key2=value2", @@ -183,7 +183,7 @@ def test_env_variables(self, mock_exporter_mixin): self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) _, kwargs = mock_exporter_mixin.call_args_list[0] - self.assertEqual(kwargs["endpoint"], "collector:55680") + self.assertEqual(kwargs["endpoint"], "collector:4317") self.assertEqual(kwargs["headers"], "key1=value1,key2=value2") self.assertEqual(kwargs["timeout"], 10) self.assertIsNotNone(kwargs["credentials"]) From baa2fcb605a3d4ac95ccf6a509ee2ed4c06cb22b Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 14 Jan 2021 23:09:02 +0530 Subject: [PATCH 0736/1517] Zipkin: Update span boolean attribute conversion (#1509) --- CHANGELOG.md | 2 ++ .../opentelemetry/exporter/zipkin/__init__.py | 10 ++++-- .../tests/test_zipkin_exporter.py | 36 +++++++++---------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9ffe83453..7b38d998f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1513](https://github.com/open-telemetry/opentelemetry-python/pull/1513)) - Update default port for OTLP exporter from 55680 to 4317 ([#1516](https://github.com/open-telemetry/opentelemetry-python/pull/1516)) +- `opentelemetry-exporter-zipkin` Update boolean attribute value transformation + ([#1509](https://github.com/open-telemetry/opentelemetry-python/pull/1509)) ### Removed - `opentelemetry-api` Remove ThreadLocalRuntimeContext since python3.4 is not supported. diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 6b376c203c..1610e0a3f6 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -352,7 +352,9 @@ def _extract_tags_from_dict(self, tags_dict): if not tags_dict: return tags for attribute_key, attribute_value in tags_dict.items(): - if isinstance(attribute_value, (int, bool, float, str)): + if isinstance(attribute_value, bool): + value = str(attribute_value).lower() + elif isinstance(attribute_value, (int, float, str)): value = str(attribute_value) elif isinstance(attribute_value, Sequence): value = self._extract_tag_value_string_from_sequence( @@ -381,7 +383,9 @@ def _extract_tag_value_string_from_sequence(self, sequence: Sequence): defined_max_tag_value_length = self.max_tag_value_length > 0 for element in sequence: - if isinstance(element, (int, bool, float, str)): + if isinstance(element, bool): + tag_value_element = str(element).lower() + elif isinstance(element, (int, float, str)): tag_value_element = str(element) elif element is None: tag_value_element = None @@ -407,7 +411,7 @@ def _extract_tag_value_string_from_sequence(self, sequence: Sequence): return json.dumps(tag_value_elements, separators=(",", ":")) def _extract_tags_from_span(self, span: Span): - tags = self._extract_tags_from_dict(getattr(span, "attributes", None)) + tags = self._extract_tags_from_dict(span.attributes) if span.resource: tags.update(self._extract_tags_from_dict(span.resource.attributes)) return tags diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index eb3f4894c1..ee6c9f2a09 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -243,7 +243,7 @@ def test_export_json(self): "localEndpoint": local_endpoint, "kind": span_kind, "tags": { - "key_bool": "False", + "key_bool": "false", "key_string": "hello_world", "key_float": "111.22", "otel.status_code": "ERROR", @@ -471,11 +471,11 @@ def test_export_json_max_tag_length(self): ) self.assertEqual( tags["list5"], - '["True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True"]', + '["true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true"]', ) self.assertEqual( tags["list6"], - '["True","True","True","True","True","True","True","True","True","True"]', + '["true","true","true","true","true","true","true","true","true","true"]', ) self.assertEqual( tags["tuple1"], @@ -493,11 +493,11 @@ def test_export_json_max_tag_length(self): ) self.assertEqual( tags["tuple5"], - '["True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True"]', + '["true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true"]', ) self.assertEqual( tags["tuple6"], - '["True","True","True","True","True","True","True","True","True","True"]', + '["true","true","true","true","true","true","true","true","true","true"]', ) self.assertEqual( tags["range1"], @@ -580,14 +580,14 @@ def test_export_json_max_tag_length(self): self.assertEqual(tags["list2"], '["a","a"]') self.assertEqual(tags["list3"], '["2","2"]') self.assertEqual(tags["list4"], '["2","2"]') - self.assertEqual(tags["list5"], '["True"]') - self.assertEqual(tags["list6"], '["True"]') + self.assertEqual(tags["list5"], '["true"]') + self.assertEqual(tags["list6"], '["true"]') self.assertEqual(tags["tuple1"], '["a","a"]') self.assertEqual(tags["tuple2"], '["a","a"]') self.assertEqual(tags["tuple3"], '["2","2"]') self.assertEqual(tags["tuple4"], '["2","2"]') - self.assertEqual(tags["tuple5"], '["True"]') - self.assertEqual(tags["tuple6"], '["True"]') + self.assertEqual(tags["tuple5"], '["true"]') + self.assertEqual(tags["tuple6"], '["true"]') self.assertEqual(tags["range1"], '["0","1"]') self.assertEqual(tags["range2"], '["0","1"]') @@ -606,14 +606,14 @@ def test_export_json_max_tag_length(self): self.assertEqual(tags["list2"], '["a","a"]') self.assertEqual(tags["list3"], '["2","2"]') self.assertEqual(tags["list4"], '["2","2"]') - self.assertEqual(tags["list5"], '["True"]') - self.assertEqual(tags["list6"], '["True"]') + self.assertEqual(tags["list5"], '["true"]') + self.assertEqual(tags["list6"], '["true"]') self.assertEqual(tags["tuple1"], '["a","a"]') self.assertEqual(tags["tuple2"], '["a","a"]') self.assertEqual(tags["tuple3"], '["2","2"]') self.assertEqual(tags["tuple4"], '["2","2"]') - self.assertEqual(tags["tuple5"], '["True"]') - self.assertEqual(tags["tuple6"], '["True"]') + self.assertEqual(tags["tuple5"], '["true"]') + self.assertEqual(tags["tuple6"], '["true"]') self.assertEqual(tags["range1"], '["0","1"]') self.assertEqual(tags["range2"], '["0","1"]') @@ -632,14 +632,14 @@ def test_export_json_max_tag_length(self): self.assertEqual(tags["list2"], '["a","a"]') self.assertEqual(tags["list3"], '["2","2"]') self.assertEqual(tags["list4"], '["2","2"]') - self.assertEqual(tags["list5"], '["True"]') - self.assertEqual(tags["list6"], '["True"]') + self.assertEqual(tags["list5"], '["true"]') + self.assertEqual(tags["list6"], '["true"]') self.assertEqual(tags["tuple1"], '["a","a"]') self.assertEqual(tags["tuple2"], '["a","a"]') self.assertEqual(tags["tuple3"], '["2","2"]') self.assertEqual(tags["tuple4"], '["2","2"]') - self.assertEqual(tags["tuple5"], '["True"]') - self.assertEqual(tags["tuple6"], '["True"]') + self.assertEqual(tags["tuple5"], '["true"]') + self.assertEqual(tags["tuple6"], '["true"]') self.assertEqual(tags["range1"], '["0","1"]') self.assertEqual(tags["range2"], '["0","1"]') @@ -768,7 +768,7 @@ def test_export_protobuf(self): local_endpoint=local_endpoint, kind=span_kind, tags={ - "key_bool": "False", + "key_bool": "false", "key_string": "hello_world", "key_float": "111.22", "otel.status_code": "ERROR", From 3d48ecda850d2a2a72ccc446618d7526db3fd3d2 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 14 Jan 2021 12:15:06 -0800 Subject: [PATCH 0737/1517] adding propagator to GH action (#1530) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 16e0799b08..dafc64d07f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: python-version: [ py35, py36, py37, py38, py39, pypy3 ] - package: ["instrumentation", "core", "exporter"] + package: ["instrumentation", "core", "exporter", "propagator"] os: [ ubuntu-latest ] include: # py35-instrumentation segfaults on 18.04 so we instead run on 20.04 From 76883321c2fb3f5f55c4136236183af7631253d3 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 15 Jan 2021 13:09:06 -0500 Subject: [PATCH 0738/1517] boundlist typing wip (#1385) --- .gitignore | 2 + .../src/opentelemetry/util/types.py | 21 ++++-- .../src/opentelemetry/sdk/util/__init__.py | 9 +-- .../src/opentelemetry/sdk/util/__init__.pyi | 71 +++++++++++++++++++ .../src/opentelemetry/sdk/util/py.typed | 0 5 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.pyi create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/util/py.typed diff --git a/.gitignore b/.gitignore index d168765890..c784acf9f6 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ __pycache__ venv*/ .venv*/ opentelemetry-python-contrib/ +# in case of symlink +opentelemetry-python-contrib # Installer logs pip-log.txt diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index fa411efb56..99bb811a5e 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -13,17 +13,28 @@ # limitations under the License. -from typing import Callable, Mapping, Optional, Sequence, Union +from typing import Callable, Mapping, Optional, Sequence, Tuple, Union AttributeValue = Union[ str, bool, int, float, - Sequence[Union[None, str]], - Sequence[Union[None, bool]], - Sequence[Union[None, int]], - Sequence[Union[None, float]], + Sequence[Optional[str]], + Sequence[Optional[bool]], + Sequence[Optional[int]], + Sequence[Optional[float]], +] +AttributeValueAsKey = Union[ + str, + bool, + int, + float, + Tuple[Optional[str], ...], + Tuple[Optional[bool], ...], + Tuple[Optional[int], ...], + Tuple[Optional[float], ...], ] Attributes = Optional[Mapping[str, AttributeValue]] +AttributesAsKey = Tuple[Tuple[str, AttributeValueAsKey], ...] AttributesFormatter = Callable[[], Attributes] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index b2d3930bb1..1bb8ed264f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -11,16 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import datetime import threading from collections import OrderedDict, deque - -try: - # pylint: disable=ungrouped-imports - from collections.abc import MutableMapping, Sequence -except ImportError: - # pylint: disable=no-name-in-module,ungrouped-imports - from collections import MutableMapping, Sequence +from collections.abc import MutableMapping, Sequence def ns_to_iso_str(nanoseconds): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.pyi b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.pyi new file mode 100644 index 0000000000..282c08b38f --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.pyi @@ -0,0 +1,71 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import ( + Iterable, + Iterator, + Mapping, + MutableMapping, + Sequence, + TypeVar, + overload, +) + +from opentelemetry.util.types import AttributesAsKey, AttributeValue + +_T = TypeVar("_T") +_KT = TypeVar("_KT") +_VT = TypeVar("_VT") + +def ns_to_iso_str(nanoseconds: int) -> str: ... +def get_dict_as_key( + labels: Mapping[str, AttributeValue] +) -> AttributesAsKey: ... + +class BoundedList(Sequence[_T]): + """An append only list with a fixed max size. + + Calls to `append` and `extend` will drop the oldest elements if there is + not enough room. + """ + + def __init__(self, maxlen: int): ... + def insert(self, index: int, value: _T) -> None: ... + @overload + def __getitem__(self, i: int) -> _T: ... + @overload + def __getitem__(self, s: slice) -> Sequence[_T]: ... + def __len__(self) -> int: ... + def append(self, item: _T): ... + def extend(self, seq: Sequence[_T]): ... + @classmethod + def from_seq(cls, maxlen: int, seq: Iterable[_T]) -> BoundedList[_T]: ... + +class BoundedDict(MutableMapping[_KT, _VT]): + """An ordered dict with a fixed max capacity. + + Oldest elements are dropped when the dict is full and a new element is + added. + """ + + def __init__(self, maxlen: int): ... + def __getitem__(self, k: _KT) -> _VT: ... + def __setitem__(self, k: _KT, v: _VT) -> None: ... + def __delitem__(self, v: _KT) -> None: ... + def __iter__(self) -> Iterator[_KT]: ... + def __len__(self) -> int: ... + @classmethod + def from_map( + cls, maxlen: int, mapping: Mapping[_KT, _VT] + ) -> BoundedDict[_KT, _VT]: ... diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/py.typed b/opentelemetry-sdk/src/opentelemetry/sdk/util/py.typed new file mode 100644 index 0000000000..e69de29bb2 From 89fb4e58d0c9d4b3192c91c3371b57007b2d2233 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 18 Jan 2021 14:34:10 -0800 Subject: [PATCH 0739/1517] Move jaeger propagator out of SDK (#1525) --- CHANGELOG.md | 2 + .../opentelemetry-propagator-jaeger/LICENSE | 201 ++++++++++++++++++ .../MANIFEST.in | 9 + .../README.rst | 23 ++ .../opentelemetry-propagator-jaeger/setup.cfg | 53 +++++ .../opentelemetry-propagator-jaeger/setup.py | 26 +++ .../propagators/jaeger/__init__.py | 13 +- .../propagators/jaeger/version.py | 13 +- .../tests/__init__.py | 13 ++ .../tests}/test_jaeger_propagator.py | 4 +- tox.ini | 10 +- 11 files changed, 350 insertions(+), 17 deletions(-) create mode 100644 propagator/opentelemetry-propagator-jaeger/LICENSE create mode 100644 propagator/opentelemetry-propagator-jaeger/MANIFEST.in create mode 100644 propagator/opentelemetry-propagator-jaeger/README.rst create mode 100644 propagator/opentelemetry-propagator-jaeger/setup.cfg create mode 100644 propagator/opentelemetry-propagator-jaeger/setup.py rename opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/jaeger_propagator.py => propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py (93%) rename opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py => propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py (67%) create mode 100644 propagator/opentelemetry-propagator-jaeger/tests/__init__.py rename {opentelemetry-sdk/tests/trace/propagation => propagator/opentelemetry-propagator-jaeger/tests}/test_jaeger_propagator.py (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b38d998f7..2197bd9f54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1516](https://github.com/open-telemetry/opentelemetry-python/pull/1516)) - `opentelemetry-exporter-zipkin` Update boolean attribute value transformation ([#1509](https://github.com/open-telemetry/opentelemetry-python/pull/1509)) +- `opentelemetry-sdk` The JaegerPropagator has been moved into its own package: `opentelemetry-propagator-jaeger` + ([#1525](https://github.com/open-telemetry/opentelemetry-python/pull/1525)) ### Removed - `opentelemetry-api` Remove ThreadLocalRuntimeContext since python3.4 is not supported. diff --git a/propagator/opentelemetry-propagator-jaeger/LICENSE b/propagator/opentelemetry-propagator-jaeger/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/propagator/opentelemetry-propagator-jaeger/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/propagator/opentelemetry-propagator-jaeger/MANIFEST.in b/propagator/opentelemetry-propagator-jaeger/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/propagator/opentelemetry-propagator-jaeger/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/propagator/opentelemetry-propagator-jaeger/README.rst b/propagator/opentelemetry-propagator-jaeger/README.rst new file mode 100644 index 0000000000..970cb189f3 --- /dev/null +++ b/propagator/opentelemetry-propagator-jaeger/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry Jaeger Propagator +=============================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-propagator-jaeger.svg + :target: https://pypi.org/project/opentelemetry-propagator-jaeger/ + +This library provides a propagator for the Jaeger format + +Installation +------------ + +:: + + pip install opentelemetry-propagator-jaeger + + +References +---------- + +* `OpenTelemetry `_ +* `Jaeger format `_ diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg new file mode 100644 index 0000000000..a429ef5529 --- /dev/null +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -0,0 +1,53 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-propagator-jaeger +description = OpenTelemetry Jaeger Propagator +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/propagator/opentelemetry-propagator-jaeger +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.17.dev0 + +[options.extras_require] +test = + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_propagator = + jaeger = opentelemetry.propagators.jaeger:JaegerPropagator \ No newline at end of file diff --git a/propagator/opentelemetry-propagator-jaeger/setup.py b/propagator/opentelemetry-propagator-jaeger/setup.py new file mode 100644 index 0000000000..4fda43e516 --- /dev/null +++ b/propagator/opentelemetry-propagator-jaeger/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "propagators", "jaeger", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py similarity index 93% rename from opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/jaeger_propagator.py rename to propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py index e4d2b1cb2c..f9ad768291 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py @@ -18,7 +18,6 @@ import opentelemetry.trace as trace from opentelemetry import baggage from opentelemetry.context import Context, get_current -from opentelemetry.sdk.trace.propagation import extract_first_element from opentelemetry.trace.propagation.textmap import ( Getter, Setter, @@ -46,7 +45,7 @@ def extract( if context is None: context = get_current() - fields = extract_first_element( + fields = _extract_first_element( getter.get(carrier, self.TRACE_ID_KEY) ).split(":") @@ -122,7 +121,7 @@ def _extract_baggage(self, getter, carrier, context): if key.startswith(self.BAGGAGE_PREFIX) ] for key in baggage_keys: - value = extract_first_element(getter.get(carrier, key)) + value = _extract_first_element(getter.get(carrier, key)) context = baggage.set_baggage( key.replace(self.BAGGAGE_PREFIX, ""), urllib.parse.unquote(value).strip(), @@ -135,3 +134,11 @@ def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags): return "{:032x}:{:016x}:{:016x}:{:02x}".format( trace_id, span_id, parent_span_id, flags ) + + +def _extract_first_element( + items: typing.Iterable[TextMapPropagatorT], +) -> typing.Optional[TextMapPropagatorT]: + if items is None: + return None + return next(iter(items), None) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py similarity index 67% rename from opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py rename to propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index a02a07ea2a..86e1dbb17a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,15 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. - -import typing - -from opentelemetry.trace.propagation.textmap import TextMapPropagatorT - - -def extract_first_element( - items: typing.Iterable[TextMapPropagatorT], -) -> typing.Optional[TextMapPropagatorT]: - if items is None: - return None - return next(iter(items), None) +__version__ = "0.17.dev0" diff --git a/propagator/opentelemetry-propagator-jaeger/tests/__init__.py b/propagator/opentelemetry-propagator-jaeger/tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/propagator/opentelemetry-propagator-jaeger/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py similarity index 98% rename from opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py rename to propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py index aff6062bbf..6e6f548d09 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py @@ -17,9 +17,11 @@ import opentelemetry.sdk.trace as trace import opentelemetry.sdk.trace.ids_generator as ids_generator -import opentelemetry.sdk.trace.propagation.jaeger_propagator as jaeger import opentelemetry.trace as trace_api from opentelemetry import baggage +from opentelemetry.propagators import ( # pylint: disable=no-name-in-module + jaeger, +) from opentelemetry.trace.propagation.textmap import DictGetter FORMAT = jaeger.JaegerPropagator() diff --git a/tox.ini b/tox.ini index 497d3f3556..788c4ef85a 100644 --- a/tox.ini +++ b/tox.ini @@ -51,6 +51,10 @@ envlist = py3{5,6,7,8,9}-test-propagator-b3 pypy3-test-propagator-b3 + ; opentelemetry-propagator-jaeger + py3{5,6,7,8,9}-test-propagator-jaeger + pypy3-test-propagator-jaeger + lint tracecontext mypy,mypyinstalled @@ -83,6 +87,7 @@ changedir = test-exporter-zipkin: exporter/opentelemetry-exporter-zipkin/tests test-propagator-b3: propagator/opentelemetry-propagator-b3/tests + test-propagator-jaeger: propagator/opentelemetry-propagator-jaeger/tests commands_pre = ; Install without -e to test the actual installation @@ -103,7 +108,7 @@ commands_pre = prometheus: pip install {toxinidir}/exporter/opentelemetry-exporter-prometheus - jaeger: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger + exporter-jaeger: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk opentracing-shim: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-opentracing-shim @@ -112,6 +117,8 @@ commands_pre = b3: pip install {toxinidir}/propagator/opentelemetry-propagator-b3 + propagator-jaeger: pip install {toxinidir}/propagator/opentelemetry-propagator-jaeger + ; In order to get a healthy coverage report, ; we have to install packages in editable mode. coverage: python {toxinidir}/scripts/eachdist.py install --editable @@ -159,6 +166,7 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-prometheus[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-b3[test] + python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-jaeger[test] commands = python scripts/eachdist.py lint --check-only From 2fb7dba5e5979386165b978b24271a3530e215e4 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 18 Jan 2021 16:32:54 -0800 Subject: [PATCH 0740/1517] Move opentracing-shim folder out of instrumentation folder (#1533) --- CHANGELOG.md | 2 ++ docs/conf.py | 12 ++++++------ docs/examples/opentracing/README.rst | 6 +++--- docs/examples/opentracing/main.py | 2 +- docs/index.rst | 7 +++---- docs/{instrumentation => shim}/instrumentation.rst | 0 docs/{instrumentation => shim}/instrumentor.rst | 0 docs/{instrumentation => shim}/metric.rst | 0 .../opentracing_shim/opentracing_shim.rst | 2 +- .../opentelemetry-opentracing-shim}/LICENSE | 0 .../opentelemetry-opentracing-shim}/MANIFEST.in | 0 .../opentelemetry-opentracing-shim}/README.rst | 2 +- .../opentelemetry-opentracing-shim}/setup.cfg | 0 .../opentelemetry-opentracing-shim}/setup.py | 7 +------ .../opentelemetry/shim}/opentracing_shim/__init__.py | 6 +++--- .../src/opentelemetry/shim}/opentracing_shim/util.py | 0 .../opentelemetry/shim}/opentracing_shim/version.py | 0 .../tests/__init__.py | 0 .../tests/test_shim.py | 10 +++++----- .../tests/test_util.py | 2 +- .../tests/testbed/README.rst | 0 .../tests/testbed/__init__.py | 0 .../tests/testbed/otel_ot_shim_tracer.py | 2 +- .../testbed/test_active_span_replacement/README.rst | 0 .../testbed/test_active_span_replacement/__init__.py | 0 .../test_active_span_replacement/test_asyncio.py | 0 .../test_active_span_replacement/test_threads.py | 0 .../tests/testbed/test_client_server/README.rst | 0 .../tests/testbed/test_client_server/__init__.py | 0 .../tests/testbed/test_client_server/test_asyncio.py | 0 .../tests/testbed/test_client_server/test_threads.py | 0 .../testbed/test_common_request_handler/README.rst | 0 .../testbed/test_common_request_handler/__init__.py | 0 .../test_common_request_handler/request_handler.py | 0 .../test_common_request_handler/test_asyncio.py | 0 .../test_common_request_handler/test_threads.py | 0 .../tests/testbed/test_late_span_finish/README.rst | 0 .../tests/testbed/test_late_span_finish/__init__.py | 0 .../testbed/test_late_span_finish/test_asyncio.py | 0 .../testbed/test_late_span_finish/test_threads.py | 0 .../testbed/test_listener_per_request/README.rst | 0 .../testbed/test_listener_per_request/__init__.py | 0 .../test_listener_per_request/response_listener.py | 0 .../test_listener_per_request/test_asyncio.py | 0 .../test_listener_per_request/test_threads.py | 0 .../tests/testbed/test_multiple_callbacks/README.rst | 0 .../testbed/test_multiple_callbacks/__init__.py | 0 .../testbed/test_multiple_callbacks/test_asyncio.py | 0 .../testbed/test_multiple_callbacks/test_threads.py | 0 .../tests/testbed/test_nested_callbacks/README.rst | 0 .../tests/testbed/test_nested_callbacks/__init__.py | 0 .../testbed/test_nested_callbacks/test_asyncio.py | 0 .../testbed/test_nested_callbacks/test_threads.py | 0 .../testbed/test_subtask_span_propagation/README.rst | 0 .../test_subtask_span_propagation/__init__.py | 0 .../test_subtask_span_propagation/test_asyncio.py | 0 .../test_subtask_span_propagation/test_threads.py | 0 .../tests/testbed/testcase.py | 0 .../tests/testbed/utils.py | 0 tox.ini | 6 +++--- 60 files changed, 31 insertions(+), 35 deletions(-) rename docs/{instrumentation => shim}/instrumentation.rst (100%) rename docs/{instrumentation => shim}/instrumentor.rst (100%) rename docs/{instrumentation => shim}/metric.rst (100%) rename docs/{instrumentation => shim}/opentracing_shim/opentracing_shim.rst (60%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/LICENSE (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/MANIFEST.in (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/README.rst (83%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/setup.cfg (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/setup.py (88%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation => shim/opentelemetry-opentracing-shim/src/opentelemetry/shim}/opentracing_shim/__init__.py (99%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation => shim/opentelemetry-opentracing-shim/src/opentelemetry/shim}/opentracing_shim/util.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation => shim/opentelemetry-opentracing-shim/src/opentelemetry/shim}/opentracing_shim/version.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/__init__.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/test_shim.py (99%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/test_util.py (97%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/README.rst (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/__init__.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/otel_ot_shim_tracer.py (92%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_active_span_replacement/README.rst (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_active_span_replacement/__init__.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_active_span_replacement/test_asyncio.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_active_span_replacement/test_threads.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_client_server/README.rst (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_client_server/__init__.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_client_server/test_asyncio.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_client_server/test_threads.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_common_request_handler/README.rst (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_common_request_handler/__init__.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_common_request_handler/request_handler.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_common_request_handler/test_asyncio.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_common_request_handler/test_threads.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_late_span_finish/README.rst (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_late_span_finish/__init__.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_late_span_finish/test_asyncio.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_late_span_finish/test_threads.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_listener_per_request/README.rst (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_listener_per_request/__init__.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_listener_per_request/response_listener.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_listener_per_request/test_asyncio.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_listener_per_request/test_threads.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_multiple_callbacks/README.rst (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_multiple_callbacks/__init__.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_multiple_callbacks/test_asyncio.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_multiple_callbacks/test_threads.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_nested_callbacks/README.rst (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_nested_callbacks/__init__.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_nested_callbacks/test_asyncio.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_nested_callbacks/test_threads.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_subtask_span_propagation/README.rst (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_subtask_span_propagation/__init__.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_subtask_span_propagation/test_asyncio.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/test_subtask_span_propagation/test_threads.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/testcase.py (100%) rename {instrumentation/opentelemetry-instrumentation-opentracing-shim => shim/opentelemetry-opentracing-shim}/tests/testbed/utils.py (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2197bd9f54..a4b55712f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1516](https://github.com/open-telemetry/opentelemetry-python/pull/1516)) - `opentelemetry-exporter-zipkin` Update boolean attribute value transformation ([#1509](https://github.com/open-telemetry/opentelemetry-python/pull/1509)) +- Move opentelemetry-opentracing-shim out of instrumentation folder + ([#1533](https://github.com/open-telemetry/opentelemetry-python/pull/1533)) - `opentelemetry-sdk` The JaegerPropagator has been moved into its own package: `opentelemetry-propagator-jaeger` ([#1525](https://github.com/open-telemetry/opentelemetry-python/pull/1525)) diff --git a/docs/conf.py b/docs/conf.py index 0d06bffec1..1da52b4a6d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,14 +35,14 @@ if isdir(join(exp, f)) ] -instr = "../instrumentation" -instr_dirs = [ - os.path.abspath("/".join(["../instrumentation", f, "src"])) - for f in listdir(instr) - if isdir(join(instr, f)) +shim = "../shim" +shim_dirs = [ + os.path.abspath("/".join(["../shim", f, "src"])) + for f in listdir(shim) + if isdir(join(shim, f)) ] -sys.path[:0] = source_dirs + exp_dirs + instr_dirs +sys.path[:0] = source_dirs + exp_dirs + shim_dirs # -- Project information ----------------------------------------------------- diff --git a/docs/examples/opentracing/README.rst b/docs/examples/opentracing/README.rst index f56bd3e5fc..da9c70e43b 100644 --- a/docs/examples/opentracing/README.rst +++ b/docs/examples/opentracing/README.rst @@ -1,8 +1,8 @@ OpenTracing Shim Example ========================== -This example shows how to use the :doc:`opentelemetry-instrumentation-opentracing-shim -package <../../instrumentation/opentracing_shim/opentracing_shim>` +This example shows how to use the :doc:`opentelemetry-opentracing-shim +package <../../shim/opentracing_shim/opentracing_shim>` to interact with libraries instrumented with `opentracing-python `_. @@ -100,6 +100,6 @@ Useful links ------------ - OpenTelemetry_ -- :doc:`../../instrumentation/opentracing_shim/opentracing_shim` +- :doc:`../../shim/opentracing_shim/opentracing_shim` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/docs/examples/opentracing/main.py b/docs/examples/opentracing/main.py index bff9a7c3da..7de4b33730 100755 --- a/docs/examples/opentracing/main.py +++ b/docs/examples/opentracing/main.py @@ -4,9 +4,9 @@ from opentelemetry import trace from opentelemetry.exporter.jaeger import JaegerSpanExporter -from opentelemetry.instrumentation import opentracing_shim from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.shim import opentracing_shim # Configure the tracer using the default implementation trace.set_tracer_provider(TracerProvider()) diff --git a/docs/index.rst b/docs/index.rst index 5a6a3a7715..eb62e6a009 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -73,7 +73,6 @@ install api/api sdk/sdk - instrumentation/instrumentation .. toctree:: :maxdepth: 2 @@ -85,11 +84,11 @@ install .. toctree:: :maxdepth: 2 - :caption: OpenTelemetry Instrumentations - :name: Instrumentations + :caption: OpenTelemetry Shims + :name: Shims :glob: - instrumentation/** + shim/** .. toctree:: :maxdepth: 1 diff --git a/docs/instrumentation/instrumentation.rst b/docs/shim/instrumentation.rst similarity index 100% rename from docs/instrumentation/instrumentation.rst rename to docs/shim/instrumentation.rst diff --git a/docs/instrumentation/instrumentor.rst b/docs/shim/instrumentor.rst similarity index 100% rename from docs/instrumentation/instrumentor.rst rename to docs/shim/instrumentor.rst diff --git a/docs/instrumentation/metric.rst b/docs/shim/metric.rst similarity index 100% rename from docs/instrumentation/metric.rst rename to docs/shim/metric.rst diff --git a/docs/instrumentation/opentracing_shim/opentracing_shim.rst b/docs/shim/opentracing_shim/opentracing_shim.rst similarity index 60% rename from docs/instrumentation/opentracing_shim/opentracing_shim.rst rename to docs/shim/opentracing_shim/opentracing_shim.rst index fad4e04bbe..175a10e860 100644 --- a/docs/instrumentation/opentracing_shim/opentracing_shim.rst +++ b/docs/shim/opentracing_shim/opentracing_shim.rst @@ -1,5 +1,5 @@ OpenTracing Shim for OpenTelemetry ================================== -.. automodule:: opentelemetry.instrumentation.opentracing_shim +.. automodule:: opentelemetry.shim.opentracing_shim :no-show-inheritance: diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/LICENSE b/shim/opentelemetry-opentracing-shim/LICENSE similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/LICENSE rename to shim/opentelemetry-opentracing-shim/LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/MANIFEST.in b/shim/opentelemetry-opentracing-shim/MANIFEST.in similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/MANIFEST.in rename to shim/opentelemetry-opentracing-shim/MANIFEST.in diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/README.rst b/shim/opentelemetry-opentracing-shim/README.rst similarity index 83% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/README.rst rename to shim/opentelemetry-opentracing-shim/README.rst index 7a8413ef59..455634858c 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/README.rst +++ b/shim/opentelemetry-opentracing-shim/README.rst @@ -16,5 +16,5 @@ Installation References ---------- -* `OpenTracing Shim for OpenTelemetry `_ +* `OpenTracing Shim for OpenTelemetry `_ * `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.cfg rename to shim/opentelemetry-opentracing-shim/setup.cfg diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.py b/shim/opentelemetry-opentracing-shim/setup.py similarity index 88% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.py rename to shim/opentelemetry-opentracing-shim/setup.py index f5d71a86b5..b8a9520713 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/setup.py +++ b/shim/opentelemetry-opentracing-shim/setup.py @@ -17,12 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "instrumentation", - "opentracing_shim", - "version.py", + BASE_DIR, "src", "opentelemetry", "shim", "opentracing_shim", "version.py", ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py similarity index 99% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py rename to shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index 65727ed559..74c8952664 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -30,7 +30,7 @@ from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.instrumentation.opentracing_shim import create_tracer + from opentelemetry.shim.opentracing_shim import create_tracer # Define which OpenTelemetry Tracer provider implementation to use. trace.set_tracer_provider(TracerProvider()) @@ -102,8 +102,8 @@ from opentelemetry import propagators from opentelemetry.baggage import get_baggage, set_baggage from opentelemetry.context import Context, attach, detach, get_value, set_value -from opentelemetry.instrumentation.opentracing_shim import util -from opentelemetry.instrumentation.opentracing_shim.version import __version__ +from opentelemetry.shim.opentracing_shim import util +from opentelemetry.shim.opentracing_shim.version import __version__ from opentelemetry.trace import INVALID_SPAN_CONTEXT, DefaultSpan, Link from opentelemetry.trace import SpanContext as OtelSpanContext from opentelemetry.trace import Tracer as OtelTracer diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/util.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/util.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/util.py rename to shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/util.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/src/opentelemetry/instrumentation/opentracing_shim/version.py rename to shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/__init__.py b/shim/opentelemetry-opentracing-shim/tests/__init__.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/__init__.py rename to shim/opentelemetry-opentracing-shim/tests/__init__.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py b/shim/opentelemetry-opentracing-shim/tests/test_shim.py similarity index 99% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py rename to shim/opentelemetry-opentracing-shim/tests/test_shim.py index 151ca07ba9..54f1caa92c 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_shim.py +++ b/shim/opentelemetry-opentracing-shim/tests/test_shim.py @@ -22,13 +22,13 @@ import opentracing from opentelemetry import propagators, trace -from opentelemetry.instrumentation.opentracing_shim import ( +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.shim.opentracing_shim import ( SpanContextShim, SpanShim, create_tracer, util, ) -from opentelemetry.sdk.trace import TracerProvider from opentelemetry.test.mock_textmap import ( MockTextMapPropagator, NOOPTextMapPropagator, @@ -137,7 +137,7 @@ def test_explicit_start_time(self): with self.shim.start_active_span("TestSpan4", start_time=now) as scope: result = util.time_seconds_from_ns(scope.span.unwrap().start_time) # Tolerate inaccuracies of less than a microsecond. See Note: - # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.instrumentation.opentracing_shim.html + # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.shim.opentracing_shim.html # TODO: This seems to work consistently, but we should find out the # biggest possible loss of precision. self.assertAlmostEqual(result, now, places=6) @@ -151,7 +151,7 @@ def test_explicit_end_time(self): end_time = util.time_seconds_from_ns(span.unwrap().end_time) # Tolerate inaccuracies of less than a microsecond. See Note: - # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.instrumentation.opentracing_shim.html + # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.shim.opentracing_shim.html # TODO: This seems to work consistently, but we should find out the # biggest possible loss of precision. self.assertAlmostEqual(end_time, now, places=6) @@ -431,7 +431,7 @@ def test_log_kv(self): ) self.assertEqual(span.unwrap().events[1].attributes["foo"], "bar") # Tolerate inaccuracies of less than a microsecond. See Note: - # https://open-telemetry.github.io/opentelemetry-python/instrumentation/opentracing_shim/opentracing_shim.html + # https://open-telemetry.github.io/opentelemetry-python/shim/opentracing_shim/opentracing_shim.html # TODO: This seems to work consistently, but we should find out the # biggest possible loss of precision. self.assertAlmostEqual(result, now, places=6) diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_util.py b/shim/opentelemetry-opentracing-shim/tests/test_util.py similarity index 97% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_util.py rename to shim/opentelemetry-opentracing-shim/tests/test_util.py index 806a8da609..7e34c1ac05 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/test_util.py +++ b/shim/opentelemetry-opentracing-shim/tests/test_util.py @@ -15,7 +15,7 @@ import time import unittest -from opentelemetry.instrumentation.opentracing_shim import util +from opentelemetry.shim.opentracing_shim import util from opentelemetry.util import time_ns diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/README.rst b/shim/opentelemetry-opentracing-shim/tests/testbed/README.rst similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/README.rst rename to shim/opentelemetry-opentracing-shim/tests/testbed/README.rst diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/__init__.py b/shim/opentelemetry-opentracing-shim/tests/testbed/__init__.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/__init__.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/__init__.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py b/shim/opentelemetry-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py similarity index 92% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py index c12bbfa029..6ed5818e34 100644 --- a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py @@ -1,4 +1,4 @@ -import opentelemetry.instrumentation.opentracing_shim as opentracingshim +import opentelemetry.shim.opentracing_shim as opentracingshim from opentelemetry.sdk import trace from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst b/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/README.rst b/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/README.rst similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/README.rst rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/README.rst diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/__init__.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/__init__.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/__init__.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/__init__.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_threads.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_client_server/test_threads.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_threads.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/README.rst b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/README.rst similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/README.rst rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/README.rst diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/README.rst b/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/README.rst similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/README.rst rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/README.rst diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/README.rst b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/README.rst similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/README.rst rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/README.rst diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/testcase.py b/shim/opentelemetry-opentracing-shim/tests/testbed/testcase.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/testcase.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/testcase.py diff --git a/instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/utils.py b/shim/opentelemetry-opentracing-shim/tests/testbed/utils.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/utils.py rename to shim/opentelemetry-opentracing-shim/tests/testbed/utils.py diff --git a/tox.ini b/tox.ini index 788c4ef85a..3ef615bb7e 100644 --- a/tox.ini +++ b/tox.ini @@ -78,7 +78,7 @@ changedir = test-core-proto: opentelemetry-proto/tests test-core-instrumentation: opentelemetry-instrumentation/tests test-core-getting-started: docs/getting_started/tests - test-core-opentracing-shim: instrumentation/opentelemetry-instrumentation-opentracing-shim/tests + test-core-opentracing-shim: shim/opentelemetry-opentracing-shim/tests test-exporter-jaeger: exporter/opentelemetry-exporter-jaeger/tests test-exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests @@ -111,7 +111,7 @@ commands_pre = exporter-jaeger: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk - opentracing-shim: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-opentracing-shim + opentracing-shim: pip install {toxinidir}/shim/opentelemetry-opentracing-shim zipkin: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin @@ -159,7 +159,7 @@ commands_pre = python -m pip install -e {toxinidir}/opentelemetry-sdk[test] python -m pip install -e {toxinidir}/opentelemetry-proto[test] python -m pip install -e {toxinidir}/tests/util[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-opentracing-shim[test] + python -m pip install -e {toxinidir}/shim/opentelemetry-opentracing-shim[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-opencensus[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp[test] From 5184c517605c298b333ed2e54d48c2c86439a693 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 18 Jan 2021 16:53:54 -0800 Subject: [PATCH 0741/1517] Add rationale document with versioning/releasing details (#1460) --- rationale.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 rationale.md diff --git a/rationale.md b/rationale.md new file mode 100644 index 0000000000..1c99eb0342 --- /dev/null +++ b/rationale.md @@ -0,0 +1,61 @@ +# OpenTelemetry Rationale + +When creating a library, often times designs and decisions are made that get lost over time. This +document tries to collect information on design decisions to answer common questions that may come +up when you explore the SDK. + +## Versioning and Releasing + +The OpenTelemetry Applications and OpenTelemetry Spec itself use semver v2. + +## Goals + +### API Stability + +Once the API for a given signal (spans, logs, metrics, baggage) has been officially released, that API module will function with any SDK that has the same major version, and equal or greater minor or patch version. + +For example, libraries that are instrumented with `opentelemetry-api 1.0.1` will function with SDK library `opentelemetry-sdk 1.11.33` or `opentelemetry-sdk 1.3.4`. + +### SDK Stability: + +Public portions of the SDK (constructors, configuration, end-user interfaces) must remain backwards compatible. +Internal interfaces are allowed to break. + +## Methods + +### Mature Signals +API modules for mature (i.e. released) signals will be included in the `opentelemetry-api` module. + +### Immature or experimental signals +API modules for experimental signals will not be included in the `opentelemetry-api` module, and must be installed manually. API modules will remain at version v0.x.y to make it abundantly clear that depending on them is at your own risk. For example, the `opentelemetry-metrics-api` v0.x.y module will provide experimental access to the unfinished metrics API. NO STABILITY GUARANTEES ARE MADE. + +## Examples + +Purely for illustration purposes, not intended to represent actual releases: + +#### V1.0.0 Release (tracing, baggage, propagators, context) + +- `opentelemetry-api` 1.0.0 + - Contains APIs for tracing, baggage, propagators, context +- `opentelemetry-tracing` 1.0.0 + - Contains the tracing SDK +- `opentelemetry-sdk` 1.0.0 + - Contains SDK components for tracing, baggage, propagators, and context + +##### Contains the following experimental packages + +- `opentelemetry-api-metrics` 0.x.y + - Contains the EXPERIMENTAL API for metrics. There are no stability guarantees. +- `opentelemetry-metrics` 0.x.y + - Contains the EXPERIMENTAL SDK for metrics. There are no stability guarantees. + +#### V1.15.0 Release (with metrics) + +- `opentelemetry-api` 1.15.0 + - Contains APIs for tracing, baggage, propagators, context, and metrics +- `opentelemetry-tracing` 1.15.0 + - Contains tracing SDK +- `opentelemetry-metrics` 1.15.0 + - Contains metrics SDK +- `opentelemetry-sdk` 1.15.0 + - Contains SDK components for tracing, baggage, propagators, context and metrics From 98f7b606bc59ac4666e522dd7a40d61b631cd352 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 19 Jan 2021 09:42:44 +0530 Subject: [PATCH 0742/1517] Update InstrumentationInfo tag keys for Jaeger and Zipkin exporters (#1535) --- CHANGELOG.md | 2 ++ .../exporter/jaeger/translate/__init__.py | 4 ++-- .../tests/test_jaeger_exporter_thrift.py | 4 ++-- .../tests/test_jarget_exporter_protobuf.py | 10 +++++++--- .../src/opentelemetry/exporter/zipkin/__init__.py | 13 +++++++------ .../tests/test_zipkin_exporter.py | 12 ++++-------- 6 files changed, 24 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4b55712f3..c65e1a73bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1533](https://github.com/open-telemetry/opentelemetry-python/pull/1533)) - `opentelemetry-sdk` The JaegerPropagator has been moved into its own package: `opentelemetry-propagator-jaeger` ([#1525](https://github.com/open-telemetry/opentelemetry-python/pull/1525)) +- `opentelemetry-exporter-jaeger`, `opentelemetry-exporter-zipkin` Update InstrumentationInfo tag keys for Jaeger and Zipkin exporters + ([#1535](https://github.com/open-telemetry/opentelemetry-python/pull/1535)) ### Removed - `opentelemetry-api` Remove ThreadLocalRuntimeContext since python3.4 is not supported. diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py index b840687fbe..853da9ac6d 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py @@ -24,8 +24,8 @@ SpanKind.INTERNAL: "internal", } -NAME_KEY = "otel.instrumentation_library.name" -VERSION_KEY = "otel.instrumentation_library.version" +NAME_KEY = "otel.library.name" +VERSION_KEY = "otel.library.version" def _nsec_to_usec_round(nsec: int) -> int: diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py index 132d92b1e1..379dd9a1e7 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py @@ -399,12 +399,12 @@ def test_translate_to_jaeger(self): vStr="internal", ), jaeger.Tag( - key="otel.instrumentation_library.name", + key=jaeger_exporter.translate.NAME_KEY, vType=jaeger.TagType.STRING, vStr="name", ), jaeger.Tag( - key="otel.instrumentation_library.version", + key=jaeger_exporter.translate.VERSION_KEY, vType=jaeger.TagType.STRING, vStr="version", ), diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py index f6f7d99bb5..2af228a6b7 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py @@ -24,7 +24,11 @@ from opentelemetry import trace as trace_api from opentelemetry.configuration import Configuration from opentelemetry.exporter.jaeger import JaegerSpanExporter -from opentelemetry.exporter.jaeger.translate import Translate +from opentelemetry.exporter.jaeger.translate import ( + NAME_KEY, + VERSION_KEY, + Translate, +) from opentelemetry.sdk import trace from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -358,12 +362,12 @@ def test_translate_to_jaeger(self): v_str="internal", ), model_pb2.KeyValue( - key="otel.instrumentation_library.name", + key=NAME_KEY, v_type=model_pb2.ValueType.STRING, v_str="name", ), model_pb2.KeyValue( - key="otel.instrumentation_library.version", + key=VERSION_KEY, v_type=model_pb2.ValueType.STRING, v_str="version", ), diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 1610e0a3f6..387e33d594 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -107,6 +107,9 @@ SpanKind.CONSUMER: zipkin_pb2.Span.Kind.CONSUMER, } +NAME_KEY = "otel.library.name" +VERSION_KEY = "otel.library.version" + SUCCESS_STATUS_CODES = (200, 202) logger = logging.getLogger(__name__) @@ -231,11 +234,9 @@ def _translate_to_json(self, spans: Sequence[Span]): } if span.instrumentation_info is not None: + zipkin_span["tags"][NAME_KEY] = span.instrumentation_info.name zipkin_span["tags"][ - "otel.instrumentation_library.name" - ] = span.instrumentation_info.name - zipkin_span["tags"][ - "otel.instrumentation_library.version" + VERSION_KEY ] = span.instrumentation_info.version if span.status.status_code is not StatusCode.UNSET: @@ -313,8 +314,8 @@ def _translate_to_protobuf(self, spans: Sequence[Span]): if span.instrumentation_info is not None: pbuf_span.tags.update( { - "otel.instrumentation_library.name": span.instrumentation_info.name, - "otel.instrumentation_library.version": span.instrumentation_info.version, + NAME_KEY: span.instrumentation_info.name, + VERSION_KEY: span.instrumentation_info.version, } ) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index ee6c9f2a09..30837106c1 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -20,10 +20,12 @@ from opentelemetry import trace as trace_api from opentelemetry.configuration import Configuration from opentelemetry.exporter.zipkin import ( + NAME_KEY, SPAN_KIND_MAP_JSON, SPAN_KIND_MAP_PROTOBUF, TRANSPORT_FORMAT_JSON, TRANSPORT_FORMAT_PROTOBUF, + VERSION_KEY, ZipkinSpanExporter, nsec_to_usec_round, ) @@ -297,10 +299,7 @@ def test_export_json(self): "duration": durations[3] // 10 ** 3, "localEndpoint": local_endpoint, "kind": span_kind, - "tags": { - "otel.instrumentation_library.name": "name", - "otel.instrumentation_library.version": "version", - }, + "tags": {NAME_KEY: "name", VERSION_KEY: "version"}, "annotations": None, }, ] @@ -833,10 +832,7 @@ def test_export_protobuf(self): duration=nsec_to_usec_round(durations[3]), local_endpoint=local_endpoint, kind=span_kind, - tags={ - "otel.instrumentation_library.name": "name", - "otel.instrumentation_library.version": "version", - }, + tags={NAME_KEY: "name", VERSION_KEY: "version"}, ), ], ) From 9a1f594b751289635906a856684b59b3d4a425a5 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 19 Jan 2021 10:38:45 +0530 Subject: [PATCH 0743/1517] Add support for OTEL_TRACE_SAMPLER and OTEL_TRACE_SAMPLER_ARG env variables (#1496) --- CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 4 +- .../src/opentelemetry/sdk/trace/sampling.py | 79 +++++++++++++++++++ opentelemetry-sdk/tests/trace/test_trace.py | 39 +++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c65e1a73bc..d0424a8b5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1285](https://github.com/open-telemetry/opentelemetry-python/pull/1285)) - Added `__repr__` for `DefaultSpan`, added `trace_flags` to `__repr__` of `SpanContext` ([#1485](https://github.com/open-telemetry/opentelemetry-python/pull/1485)]) +- `opentelemetry-sdk` Add support for OTEL_TRACE_SAMPLER and OTEL_TRACE_SAMPLER_ARG env variables + ([#1496](https://github.com/open-telemetry/opentelemetry-python/pull/1496)) ### Changed - `opentelemetry-exporter-zipkin` Updated zipkin exporter status code and error tag diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index c5b736902a..2e8cb6b9e8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -60,6 +60,8 @@ SPAN_EVENT_COUNT_LIMIT = Configuration().get("SPAN_EVENT_COUNT_LIMIT", 1000) SPAN_LINK_COUNT_LIMIT = Configuration().get("SPAN_LINK_COUNT_LIMIT", 1000) VALID_ATTR_VALUE_TYPES = (bool, str, int, float) +# pylint: disable=protected-access +TRACE_SAMPLER = sampling._get_from_env_or_default() class SpanProcessor: @@ -891,7 +893,7 @@ def use_span( class TracerProvider(trace_api.TracerProvider): def __init__( self, - sampler: sampling.Sampler = sampling.DEFAULT_ON, + sampler: sampling.Sampler = TRACE_SAMPLER, resource: Resource = Resource.create({}), shutdown_on_exit: bool = True, active_span_processor: Union[ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index d1e02b3109..38bba0845e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -59,9 +59,46 @@ # created spans will now be sampled by the TraceIdRatioBased sampler with trace.get_tracer(__name__).start_as_current_span("Test Span"): ... + +The tracer sampler can also be configured via environment variables ``OTEL_TRACE_SAMPLER`` and ``OTEL_TRACE_SAMPLER_ARG`` (only if applicable). +The list of known values for ``OTEL_TRACE_SAMPLER`` are: + + * always_on - Sampler that always samples spans, regardless of the parent span's sampling decision. + * always_off - Sampler that never samples spans, regardless of the parent span's sampling decision. + * traceidratio - Sampler that samples probabalistically based on rate. + * parentbased_always_on - (default) Sampler that respects its parent span's sampling decision, but otherwise always samples. + * parentbased_always_off - Sampler that respects its parent span's sampling decision, but otherwise never samples. + * parentbased_traceidratio - Sampler that respects its parent span's sampling decision, but otherwise samples probabalistically based on rate. + +Sampling probability can be set with ``OTEL_TRACE_SAMPLER_ARG`` if the sampler is traceidratio or parentbased_traceidratio, when not provided rate will be set to 1.0 (maximum rate possible). + + +Prev example but with environment vairables. Please make sure to set the env ``OTEL_TRACE_SAMPLER=traceidratio`` and ``OTEL_TRACE_SAMPLER_ARG=0.001``. + +.. code:: python + + from opentelemetry import trace + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, + ) + + trace.set_tracer_provider(TracerProvider()) + + # set up an exporter for sampled spans + trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) + ) + + # created spans will now be sampled by the TraceIdRatioBased sampler with rate 1/1000. + with trace.get_tracer(__name__).start_as_current_span("Test Span"): + ... """ import abc import enum +import os +from logging import getLogger from types import MappingProxyType from typing import Optional, Sequence @@ -71,6 +108,8 @@ from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes +_logger = getLogger(__name__) + class Decision(enum.Enum): # IsRecording() == false, span will not be recorded and all events and attributes will be dropped. @@ -307,3 +346,43 @@ def get_description(self): DEFAULT_ON = ParentBased(ALWAYS_ON) """Sampler that respects its parent span's sampling decision, but otherwise always samples.""" + + +class ParentBasedTraceIdRatio(ParentBased): + """ + Sampler that respects its parent span's sampling decision, but otherwise + samples probabalistically based on `rate`. + """ + + def __init__(self, rate: float): + root = TraceIdRatioBased(rate=rate) + super().__init__(root=root) + + +_KNOWN_SAMPLERS = { + "always_on": ALWAYS_ON, + "always_off": ALWAYS_OFF, + "parentbased_always_on": DEFAULT_ON, + "parentbased_always_off": DEFAULT_OFF, + "traceidratio": TraceIdRatioBased, + "parentbased_traceidratio": ParentBasedTraceIdRatio, +} + + +def _get_from_env_or_default() -> Sampler: + trace_sampler = os.getenv( + "OTEL_TRACE_SAMPLER", "parentbased_always_on" + ).lower() + if trace_sampler not in _KNOWN_SAMPLERS: + _logger.warning("Couldn't recognize sampler %s.", trace_sampler) + trace_sampler = "parentbased_always_on" + + if trace_sampler in ("traceidratio", "parentbased_traceidratio"): + try: + rate = float(os.getenv("OTEL_TRACE_SAMPLER_ARG")) + except ValueError: + _logger.warning("Could not convert TRACE_SAMPLER_ARG to float.") + rate = 1.0 + return _KNOWN_SAMPLERS[trace_sampler](rate) + + return _KNOWN_SAMPLERS[trace_sampler] diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 6e69542f77..ebe538d56d 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -139,6 +139,9 @@ def test_tracer_provider_accepts_concurrent_multi_span_processor(self): class TestTracerSampling(unittest.TestCase): + def tearDown(self): + reload(trace) + def test_default_sampler(self): tracer = new_tracer() @@ -159,6 +162,12 @@ def test_default_sampler(self): trace_api.TraceFlags.SAMPLED, ) + def test_default_sampler_type(self): + tracer_provider = trace.TracerProvider() + self.assertIsInstance(tracer_provider.sampler, sampling.ParentBased) + # pylint: disable=protected-access + self.assertEqual(tracer_provider.sampler._root, sampling.ALWAYS_ON) + def test_sampler_no_sampling(self): tracer_provider = trace.TracerProvider(sampling.ALWAYS_OFF) tracer = tracer_provider.get_tracer(__name__) @@ -179,6 +188,36 @@ def test_sampler_no_sampling(self): trace_api.TraceFlags.DEFAULT, ) + @mock.patch.dict("os.environ", {"OTEL_TRACE_SAMPLER": "always_off"}) + def test_sampler_with_env(self): + # pylint: disable=protected-access + reload(trace) + tracer_provider = trace.TracerProvider() + self.assertIsInstance(tracer_provider.sampler, sampling.StaticSampler) + self.assertEqual( + tracer_provider.sampler._decision, sampling.Decision.DROP + ) + + tracer = tracer_provider.get_tracer(__name__) + + root_span = tracer.start_span(name="root span", context=None) + # Should be no-op + self.assertIsInstance(root_span, trace_api.DefaultSpan) + + @mock.patch.dict( + "os.environ", + { + "OTEL_TRACE_SAMPLER": "parentbased_traceidratio", + "OTEL_TRACE_SAMPLER_ARG": "0.25", + }, + ) + def test_ratio_sampler_with_env(self): + # pylint: disable=protected-access + reload(trace) + tracer_provider = trace.TracerProvider() + self.assertIsInstance(tracer_provider.sampler, sampling.ParentBased) + self.assertEqual(tracer_provider.sampler._root.rate, 0.25) + class TestSpanCreation(unittest.TestCase): def test_start_span_invalid_spancontext(self): From d994e757cf4dd5a468fa688617f0de213c9c6139 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 19 Jan 2021 08:35:16 -0800 Subject: [PATCH 0744/1517] Adding opentelemetry-distro package and entrypoint (#1482) --- CHANGELOG.md | 3 + eachdist.ini | 1 + opentelemetry-distro/MANIFEST.in | 7 +++ opentelemetry-distro/README.rst | 22 +++++++ opentelemetry-distro/setup.cfg | 59 +++++++++++++++++++ opentelemetry-distro/setup.py | 27 +++++++++ .../src/opentelemetry/distro}/__init__.py | 29 ++++++--- .../src/opentelemetry/distro/version.py | 15 +++++ opentelemetry-distro/tests/__init__.py | 0 .../tests}/test_configurator.py | 10 ++-- opentelemetry-distro/tests/test_distro.py | 35 +++++++++++ .../auto_instrumentation/sitecustomize.py | 11 +++- .../opentelemetry/instrumentation/distro.py | 47 +++++++++++++++ opentelemetry-sdk/setup.cfg | 3 - scripts/build.sh | 2 +- tox.ini | 7 +++ 16 files changed, 258 insertions(+), 20 deletions(-) create mode 100644 opentelemetry-distro/MANIFEST.in create mode 100644 opentelemetry-distro/README.rst create mode 100644 opentelemetry-distro/setup.cfg create mode 100644 opentelemetry-distro/setup.py rename {opentelemetry-sdk/src/opentelemetry/sdk/configuration => opentelemetry-distro/src/opentelemetry/distro}/__init__.py (93%) create mode 100644 opentelemetry-distro/src/opentelemetry/distro/version.py create mode 100644 opentelemetry-distro/tests/__init__.py rename {opentelemetry-sdk/tests/configuration => opentelemetry-distro/tests}/test_configurator.py (94%) create mode 100644 opentelemetry-distro/tests/test_distro.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d0424a8b5c..21a16a8de7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `SpanContext` ([#1485](https://github.com/open-telemetry/opentelemetry-python/pull/1485)]) - `opentelemetry-sdk` Add support for OTEL_TRACE_SAMPLER and OTEL_TRACE_SAMPLER_ARG env variables ([#1496](https://github.com/open-telemetry/opentelemetry-python/pull/1496)) +- Adding `opentelemetry-distro` package to add default configuration for + span exporter to OTLP + ([#1482](https://github.com/open-telemetry/opentelemetry-python/pull/1482)) ### Changed - `opentelemetry-exporter-zipkin` Updated zipkin exporter status code and error tag diff --git a/eachdist.ini b/eachdist.ini index 9cef826d20..ded3a5d8c1 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -9,6 +9,7 @@ sortfirst= opentelemetry-sdk opentelemetry-instrumentation opentelemetry-proto + opentelemetry-distro tests/util instrumentation/* exporter/* diff --git a/opentelemetry-distro/MANIFEST.in b/opentelemetry-distro/MANIFEST.in new file mode 100644 index 0000000000..191b7d1959 --- /dev/null +++ b/opentelemetry-distro/MANIFEST.in @@ -0,0 +1,7 @@ +prune tests +graft src +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include MANIFEST.in +include README.rst diff --git a/opentelemetry-distro/README.rst b/opentelemetry-distro/README.rst new file mode 100644 index 0000000000..4189131fc2 --- /dev/null +++ b/opentelemetry-distro/README.rst @@ -0,0 +1,22 @@ +OpenTelemetry Distro +==================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-distro.svg + :target: https://pypi.org/project/opentelemetry-distro/ + +Installation +------------ + +:: + + pip install opentelemetry-distro + + +This package provides entrypoints to configure OpenTelemetry + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg new file mode 100644 index 0000000000..a9340e4608 --- /dev/null +++ b/opentelemetry-distro/setup.cfg @@ -0,0 +1,59 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-distro +description = OpenTelemetry Python Distro +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-distro +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +zip_safe = False +include_package_data = True +install_requires = + opentelemetry-api == 0.17.dev0 + opentelemetry-sdk == 0.17.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_distro = + distro = opentelemetry.distro:OpenTelemetryDistro +opentelemetry_configurator = + configurator = opentelemetry.distro:Configurator + +[options.extras_require] +test = +otlp = + opentelemetry-exporter-otlp == 0.17.dev0 diff --git a/opentelemetry-distro/setup.py b/opentelemetry-distro/setup.py new file mode 100644 index 0000000000..da8417d50a --- /dev/null +++ b/opentelemetry-distro/setup.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "distro", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"],) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py similarity index 93% rename from opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py rename to opentelemetry-distro/src/opentelemetry/distro/__init__.py index b8a99bb62c..8a48d256c7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -11,7 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +# +import os from logging import getLogger from typing import Sequence, Tuple @@ -20,6 +21,7 @@ from opentelemetry import trace from opentelemetry.configuration import Configuration from opentelemetry.instrumentation.configurator import BaseConfigurator +from opentelemetry.instrumentation.distro import BaseDistro from opentelemetry.sdk.metrics.export import MetricsExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider @@ -31,10 +33,10 @@ logger = getLogger(__file__) + EXPORTER_OTLP = "otlp" EXPORTER_OTLP_SPAN = "otlp_span" EXPORTER_OTLP_METRIC = "otlp_metric" -_DEFAULT_EXPORTER = EXPORTER_OTLP RANDOM_IDS_GENERATOR = "random" _DEFAULT_IDS_GENERATOR = RANDOM_IDS_GENERATOR @@ -49,7 +51,7 @@ def _get_service_name() -> str: def _get_exporter_names() -> Sequence[str]: - exporter = Configuration().EXPORTER or _DEFAULT_EXPORTER + exporter = Configuration().EXPORTER or EXPORTER_OTLP if exporter.lower().strip() == "none": return [] @@ -87,11 +89,6 @@ def _init_tracing( ) -def _init_metrics(exporters: Sequence[MetricsExporter]): - if exporters: - logger.warning("automatic metric initialization is not supported yet.") - - def _import_tracer_provider_config_components( selected_components, entry_point_name ) -> Sequence[Tuple[str, object]]: @@ -158,13 +155,27 @@ def _initialize_components(): ids_generator_name = _get_ids_generator() ids_generator = _import_ids_generator(ids_generator_name) _init_tracing(trace_exporters, ids_generator) - # We don't support automatic initialization for metric yet but have added # some boilerplate in order to make sure current implementation does not # lock us out of supporting metrics later without major surgery. _init_metrics(metric_exporters) +def _init_metrics(exporters: Sequence[MetricsExporter]): + if exporters: + logger.warning("automatic metric initialization is not supported yet.") + + class Configurator(BaseConfigurator): def _configure(self, **kwargs): _initialize_components() + + +class OpenTelemetryDistro(BaseDistro): + """ + The OpenTelemetry provided Distro configures a default set of + configuration out of the box. + """ + + def _configure(self, **kwargs): + os.environ.setdefault("OTEL_EXPORTER", "otlp") diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py new file mode 100644 index 0000000000..86e1dbb17a --- /dev/null +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.17.dev0" diff --git a/opentelemetry-distro/tests/__init__.py b/opentelemetry-distro/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-sdk/tests/configuration/test_configurator.py b/opentelemetry-distro/tests/test_configurator.py similarity index 94% rename from opentelemetry-sdk/tests/configuration/test_configurator.py rename to opentelemetry-distro/tests/test_configurator.py index ebd6768444..90ee7fe931 100644 --- a/opentelemetry-sdk/tests/configuration/test_configurator.py +++ b/opentelemetry-distro/tests/test_configurator.py @@ -18,7 +18,7 @@ from unittest.mock import patch from opentelemetry.configuration import Configuration -from opentelemetry.sdk.configuration import ( +from opentelemetry.distro import ( _get_ids_generator, _import_ids_generator, _init_tracing, @@ -78,11 +78,10 @@ class TestTraceInit(TestCase): def setUp(self): super() self.get_provider_patcher = patch( - "opentelemetry.sdk.configuration.TracerProvider", Provider, + "opentelemetry.distro.TracerProvider", Provider ) self.get_processor_patcher = patch( - "opentelemetry.sdk.configuration.BatchExportSpanProcessor", - Processor, + "opentelemetry.distro.BatchExportSpanProcessor", Processor ) self.set_provider_patcher = patch( "opentelemetry.trace.set_tracer_provider" @@ -133,7 +132,8 @@ def test_trace_init_otlp(self): del environ["OTEL_SERVICE_NAME"] @patch.dict(environ, {"OTEL_IDS_GENERATOR": "custom_ids_generator"}) - @patch("opentelemetry.sdk.configuration.iter_entry_points") + @patch("opentelemetry.distro.IdsGenerator", new=IdsGenerator) + @patch("opentelemetry.distro.iter_entry_points") def test_trace_init_custom_ids_generator(self, mock_iter_entry_points): mock_iter_entry_points.configure_mock( return_value=[ diff --git a/opentelemetry-distro/tests/test_distro.py b/opentelemetry-distro/tests/test_distro.py new file mode 100644 index 0000000000..62d3a7e5e3 --- /dev/null +++ b/opentelemetry-distro/tests/test_distro.py @@ -0,0 +1,35 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +import os +from unittest import TestCase + +from pkg_resources import DistributionNotFound, require + +from opentelemetry.distro import OpenTelemetryDistro + + +class TestDistribution(TestCase): + def test_package_available(self): + try: + require(["opentelemetry-distro"]) + except DistributionNotFound: + self.fail("opentelemetry-distro not installed") + + def test_default_configuration(self): + distro = OpenTelemetryDistro() + self.assertIsNone(os.environ.get("OTEL_EXPORTER")) + distro.configure() + self.assertEqual("otlp", os.environ.get("OTEL_EXPORTER")) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index 3465619ea3..72be01fd18 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -24,8 +24,15 @@ def _load_distros(): - # will be implemented in a subsequent PR - pass + for entry_point in iter_entry_points("opentelemetry_distro"): + try: + entry_point.load()().configure() # type: ignore + logger.debug("Distribution %s configured", entry_point.name) + except Exception as exc: # pylint: disable=broad-except + logger.exception( + "Distribution %s configuration failed", entry_point.name + ) + raise exc def _load_instrumentors(): diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py new file mode 100644 index 0000000000..63cacf1f6d --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py @@ -0,0 +1,47 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +""" +OpenTelemetry Base Distribution (Distro) +""" + +from abc import ABC, abstractmethod +from logging import getLogger + +_LOG = getLogger(__name__) + + +class BaseDistro(ABC): + """An ABC for distro""" + + _instance = None + + def __new__(cls, *args, **kwargs): + + if cls._instance is None: + cls._instance = object.__new__(cls, *args, **kwargs) + + return cls._instance + + @abstractmethod + def _configure(self, **kwargs): + """Configure the distribution""" + + def configure(self, **kwargs): + """Configure the distribution""" + self._configure(**kwargs) + + +__all__ = ["BaseDistro"] diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index f7ad42ba99..b1ecb357d2 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,7 +43,6 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api == 0.17.dev0 - opentelemetry-instrumentation == 0.17.dev0 [options.packages.find] where = src @@ -56,8 +55,6 @@ opentelemetry_tracer_provider = opentelemetry_exporter = console_span = opentelemetry.sdk.trace.export:ConsoleSpanExporter console_metrics = opentelemetry.sdk.metrics.export:ConsoleMetricsExporter -opentelemetry_configurator = - sdk_configurator = opentelemetry.sdk.configuration:Configurator opentelemetry_ids_generator = random = opentelemetry.sdk.trace.ids_generator:RandomIdsGenerator diff --git a/scripts/build.sh b/scripts/build.sh index 853a297003..718d91ef9d 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ exporter/*/ instrumentation/*/ propagator/*/; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ opentelemetry-distro/ exporter/*/ instrumentation/*/ propagator/*/; do ( echo "building $d" cd "$d" diff --git a/tox.ini b/tox.ini index 3ef615bb7e..6f27fa0dcb 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,10 @@ envlist = py3{5,6,7,8,9}-test-core-getting-started pypy3-test-core-getting-started + ; opentelemetry-distro + py3{5,6,7,8,9}-test-core-distro + pypy3-test-core-distro + ; opentelemetry-exporter-jaeger py3{5,6,7,8,9}-test-exporter-jaeger @@ -79,6 +83,7 @@ changedir = test-core-instrumentation: opentelemetry-instrumentation/tests test-core-getting-started: docs/getting_started/tests test-core-opentracing-shim: shim/opentelemetry-opentracing-shim/tests + test-core-distro: opentelemetry-distro/tests test-exporter-jaeger: exporter/opentelemetry-exporter-jaeger/tests test-exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests @@ -97,6 +102,7 @@ commands_pre = test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-instrumentation {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util test-core-proto: pip install {toxinidir}/opentelemetry-proto + distro: pip install {toxinidir}/opentelemetry-distro {toxinidir}/opentelemetry-distro instrumentation: pip install {toxinidir}/opentelemetry-instrumentation getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask @@ -167,6 +173,7 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-b3[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-jaeger[test] + python -m pip install -e {toxinidir}/opentelemetry-distro[test] commands = python scripts/eachdist.py lint --check-only From c750109dd75b03a9f91dacd1b66b2b8ee140ec0d Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 20 Jan 2021 04:59:02 +0530 Subject: [PATCH 0745/1517] Remove rate property setter from TraceIdRatioBasedSampler (#1536) --- CHANGELOG.md | 2 ++ opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py | 5 ----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21a16a8de7..5e8290dbe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1525](https://github.com/open-telemetry/opentelemetry-python/pull/1525)) - `opentelemetry-exporter-jaeger`, `opentelemetry-exporter-zipkin` Update InstrumentationInfo tag keys for Jaeger and Zipkin exporters ([#1535](https://github.com/open-telemetry/opentelemetry-python/pull/1535)) +- `opentelemetry-sdk` Remove rate property setter from TraceIdRatioBasedSampler + ([#1536](https://github.com/open-telemetry/opentelemetry-python/pull/1536)) ### Removed - `opentelemetry-api` Remove ThreadLocalRuntimeContext since python3.4 is not supported. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 38bba0845e..b6475c4d34 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -233,11 +233,6 @@ def get_bound_for_rate(cls, rate: float) -> int: def rate(self) -> float: return self._rate - @rate.setter - def rate(self, new_rate: float) -> None: - self._rate = new_rate - self._bound = self.get_bound_for_rate(self._rate) - @property def bound(self) -> int: return self._bound From 1d39f7f819d652dd32eb066dd04bac5ae53b855d Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 20 Jan 2021 22:52:57 +0530 Subject: [PATCH 0746/1517] Fix TraceState to adhere to specs (#1502) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 + .../tests/test_otcollector_trace_exporter.py | 6 +- .../trace/propagation/tracecontext.py | 84 +------- .../src/opentelemetry/trace/span.py | 186 +++++++++++++++++- .../src/opentelemetry/util/tracestate.py | 73 +++++++ .../test_tracecontexthttptextformat.py | 3 +- .../tests/trace/test_tracestate.py | 98 +++++++++ .../tests/trace/test_sampling.py | 12 +- opentelemetry-sdk/tests/trace/test_trace.py | 6 +- 10 files changed, 376 insertions(+), 96 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/util/tracestate.py create mode 100644 opentelemetry-api/tests/trace/test_tracestate.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dafc64d07f..354b52a512 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 32cac7a9ff6c831aa0e9514bb38c430fce819141 + CONTRIB_REPO_SHA: 1e319dbaf21df7573f15f35773b8272579dd1030 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e8290dbe2..df7e779684 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1535](https://github.com/open-telemetry/opentelemetry-python/pull/1535)) - `opentelemetry-sdk` Remove rate property setter from TraceIdRatioBasedSampler ([#1536](https://github.com/open-telemetry/opentelemetry-python/pull/1536)) +- Fix TraceState to adhere to specs + ([#1502](https://github.com/open-telemetry/opentelemetry-python/pull/1502)) ### Removed - `opentelemetry-api` Remove ThreadLocalRuntimeContext since python3.4 is not supported. diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py index b61cf333cc..97b1a37912 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py @@ -94,7 +94,7 @@ def test_translate_to_collector(self): span_id, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), - trace_state=trace_api.TraceState({"testKey": "testValue"}), + trace_state=trace_api.TraceState([("testkey", "testvalue")]), ) parent_span_context = trace_api.SpanContext( trace_id, parent_id, is_remote=False @@ -200,9 +200,9 @@ def test_translate_to_collector(self): ) self.assertEqual(output_spans[0].status.message, "test description") self.assertEqual(len(output_spans[0].tracestate.entries), 1) - self.assertEqual(output_spans[0].tracestate.entries[0].key, "testKey") + self.assertEqual(output_spans[0].tracestate.entries[0].key, "testkey") self.assertEqual( - output_spans[0].tracestate.entries[0].value, "testValue" + output_spans[0].tracestate.entries[0].value, "testvalue" ) self.assertEqual( diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index 146ba47772..c18cde9ee6 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -18,31 +18,7 @@ import opentelemetry.trace as trace from opentelemetry.context.context import Context from opentelemetry.trace.propagation import textmap - -# Keys and values are strings of up to 256 printable US-ASCII characters. -# Implementations should conform to the `W3C Trace Context - Tracestate`_ -# spec, which describes additional restrictions on valid field values. -# -# .. _W3C Trace Context - Tracestate: -# https://www.w3.org/TR/trace-context/#tracestate-field - -_KEY_WITHOUT_VENDOR_FORMAT = r"[a-z][_0-9a-z\-\*\/]{0,255}" -_KEY_WITH_VENDOR_FORMAT = ( - r"[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}" -) - -_KEY_FORMAT = _KEY_WITHOUT_VENDOR_FORMAT + "|" + _KEY_WITH_VENDOR_FORMAT -_VALUE_FORMAT = ( - r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]" -) - -_DELIMITER_FORMAT = "[ \t]*,[ \t]*" -_MEMBER_FORMAT = "({})(=)({})[ \t]*".format(_KEY_FORMAT, _VALUE_FORMAT) - -_DELIMITER_FORMAT_RE = re.compile(_DELIMITER_FORMAT) -_MEMBER_FORMAT_RE = re.compile(_MEMBER_FORMAT) - -_TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS = 32 +from opentelemetry.trace.span import TraceState class TraceContextTextMapPropagator(textmap.TextMapPropagator): @@ -94,7 +70,7 @@ def extract( if tracestate_headers is None: tracestate = None else: - tracestate = _parse_tracestate(tracestate_headers) + tracestate = TraceState.from_header(tracestate_headers) span_context = trace.SpanContext( trace_id=int(trace_id, 16), @@ -130,7 +106,7 @@ def inject( carrier, self._TRACEPARENT_HEADER_NAME, traceparent_string ) if span_context.trace_state: - tracestate_string = _format_tracestate(span_context.trace_state) + tracestate_string = span_context.trace_state.to_header() set_in_carrier( carrier, self._TRACESTATE_HEADER_NAME, tracestate_string ) @@ -143,57 +119,3 @@ def fields(self) -> typing.Set[str]: `opentelemetry.trace.propagation.textmap.TextMapPropagator.fields` """ return {self._TRACEPARENT_HEADER_NAME, self._TRACESTATE_HEADER_NAME} - - -def _parse_tracestate(header_list: typing.List[str]) -> trace.TraceState: - """Parse one or more w3c tracestate header into a TraceState. - - Args: - string: the value of the tracestate header. - - Returns: - A valid TraceState that contains values extracted from - the tracestate header. - - If the format of one headers is illegal, all values will - be discarded and an empty tracestate will be returned. - - If the number of keys is beyond the maximum, all values - will be discarded and an empty tracestate will be returned. - """ - tracestate = trace.TraceState() - value_count = 0 - for header in header_list: - for member in re.split(_DELIMITER_FORMAT_RE, header): - # empty members are valid, but no need to process further. - if not member: - continue - match = _MEMBER_FORMAT_RE.fullmatch(member) - if not match: - # TODO: log this? - return trace.TraceState() - key, _eq, value = match.groups() - if key in tracestate: # pylint:disable=E1135 - # duplicate keys are not legal in - # the header, so we will remove - return trace.TraceState() - # typing.Dict's update is not recognized by pylint: - # https://github.com/PyCQA/pylint/issues/2420 - tracestate[key] = value # pylint:disable=E1137 - value_count += 1 - if value_count > _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS: - return trace.TraceState() - return tracestate - - -def _format_tracestate(tracestate: trace.TraceState) -> str: - """Parse a w3c tracestate header into a TraceState. - - Args: - tracestate: the tracestate header to write - - Returns: - A string that adheres to the w3c tracestate - header format. - """ - return ",".join(key + "=" + value for key, value in tracestate.items()) diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 507b051368..baeed76202 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -1,10 +1,18 @@ import abc import logging +import re import types as python_types import typing +from collections import OrderedDict from opentelemetry.trace.status import Status from opentelemetry.util import types +from opentelemetry.util.tracestate import ( + _DELIMITER_PATTERN, + _MEMBER_PATTERN, + _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS, + _is_valid_pair, +) _logger = logging.getLogger(__name__) @@ -135,7 +143,7 @@ def sampled(self) -> bool: DEFAULT_TRACE_OPTIONS = TraceFlags.get_default() -class TraceState(typing.Dict[str, str]): +class TraceState(typing.Mapping[str, str]): """A list of key-value pairs representing vendor-specific trace info. Keys and values are strings of up to 256 printable US-ASCII characters. @@ -146,10 +154,186 @@ class TraceState(typing.Dict[str, str]): https://www.w3.org/TR/trace-context/#tracestate-field """ + def __init__( + self, + entries: typing.Optional[ + typing.Sequence[typing.Tuple[str, str]] + ] = None, + ) -> None: + self._dict = OrderedDict() # type: OrderedDict[str, str] + if entries is None: + return + if len(entries) > _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS: + _logger.warning( + "There can't be more than %s key/value pairs.", + _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS, + ) + return + + for key, value in entries: + if _is_valid_pair(key, value): + if key in self._dict: + _logger.warning("Duplicate key: %s found.", key) + continue + self._dict[key] = value + else: + _logger.warning( + "Invalid key/value pair (%s, %s) found.", key, value + ) + + def __getitem__(self, key: str) -> typing.Optional[str]: # type: ignore + return self._dict.get(key) + + def __iter__(self) -> typing.Iterator[str]: + return iter(self._dict) + + def __len__(self) -> int: + return len(self._dict) + + def __repr__(self) -> str: + pairs = [ + "{key=%s, value=%s}" % (key, value) + for key, value in self._dict.items() + ] + return str(pairs) + + def add(self, key: str, value: str) -> "TraceState": + """Adds a key-value pair to tracestate. The provided pair should + adhere to w3c tracestate identifiers format. + + Args: + key: A valid tracestate key to add + value: A valid tracestate value to add + + Returns: + A new TraceState with the modifications applied. + + If the provided key-value pair is invalid or results in tracestate + that violates tracecontext specification, they are discarded and + same tracestate will be returned. + """ + if not _is_valid_pair(key, value): + _logger.warning( + "Invalid key/value pair (%s, %s) found.", key, value + ) + return self + # There can be a maximum of 32 pairs + if len(self) >= _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS: + _logger.warning("There can't be more 32 key/value pairs.") + return self + # Duplicate entries are not allowed + if key in self._dict: + _logger.warning("The provided key %s already exists.", key) + return self + new_state = [(key, value)] + list(self._dict.items()) + return TraceState(new_state) + + def update(self, key: str, value: str) -> "TraceState": + """Updates a key-value pair in tracestate. The provided pair should + adhere to w3c tracestate identifiers format. + + Args: + key: A valid tracestate key to update + value: A valid tracestate value to update for key + + Returns: + A new TraceState with the modifications applied. + + If the provided key-value pair is invalid or results in tracestate + that violates tracecontext specification, they are discarded and + same tracestate will be returned. + """ + if not _is_valid_pair(key, value): + _logger.warning( + "Invalid key/value pair (%s, %s) found.", key, value + ) + return self + prev_state = self._dict.copy() + prev_state[key] = value + prev_state.move_to_end(key, last=False) + new_state = list(prev_state.items()) + return TraceState(new_state) + + def delete(self, key: str) -> "TraceState": + """Deletes a key-value from tracestate. + + Args: + key: A valid tracestate key to remove key-value pair from tracestate + + Returns: + A new TraceState with the modifications applied. + + If the provided key-value pair is invalid or results in tracestate + that violates tracecontext specification, they are discarded and + same tracestate will be returned. + """ + if key not in self._dict: + _logger.warning("The provided key %s doesn't exist.", key) + return self + prev_state = self._dict.copy() + prev_state.pop(key) + new_state = list(prev_state.items()) + return TraceState(new_state) + + def to_header(self) -> str: + """Creates a w3c tracestate header from a TraceState. + + Returns: + A string that adheres to the w3c tracestate + header format. + """ + return ",".join(key + "=" + value for key, value in self._dict.items()) + + @classmethod + def from_header(cls, header_list: typing.List[str]) -> "TraceState": + """Parses one or more w3c tracestate header into a TraceState. + + Args: + header_list: one or more w3c tracestate headers. + + Returns: + A valid TraceState that contains values extracted from + the tracestate header. + + If the format of one headers is illegal, all values will + be discarded and an empty tracestate will be returned. + + If the number of keys is beyond the maximum, all values + will be discarded and an empty tracestate will be returned. + """ + pairs = OrderedDict() + for header in header_list: + for member in re.split(_DELIMITER_PATTERN, header): + # empty members are valid, but no need to process further. + if not member: + continue + match = _MEMBER_PATTERN.fullmatch(member) + if not match: + _logger.warning( + "Member doesn't match the w3c identifiers format %s", + member, + ) + return cls() + key, _eq, value = match.groups() + # duplicate keys are not legal in header + if key in pairs: + return cls() + pairs[key] = value + return cls(list(pairs.items())) + @classmethod def get_default(cls) -> "TraceState": return cls() + def keys(self) -> typing.KeysView[str]: + return self._dict.keys() + + def items(self) -> typing.ItemsView[str, str]: + return self._dict.items() + + def values(self) -> typing.ValuesView[str]: + return self._dict.values() + DEFAULT_TRACE_STATE = TraceState.get_default() diff --git a/opentelemetry-api/src/opentelemetry/util/tracestate.py b/opentelemetry-api/src/opentelemetry/util/tracestate.py new file mode 100644 index 0000000000..c352c8caa2 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/util/tracestate.py @@ -0,0 +1,73 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +from logging import getLogger + +_logger = getLogger(__name__) + +# The key MUST begin with a lowercase letter or a digit, +# and can only contain lowercase letters (a-z), digits (0-9), +# underscores (_), dashes (-), asterisks (*), and forward slashes (/). +# For multi-tenant vendor scenarios, an at sign (@) can be used to +# prefix the vendor name. Vendors SHOULD set the tenant ID +# at the beginning of the key. + +# key = ( lcalpha ) 0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) +# key = ( lcalpha / DIGIT ) 0*240( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) "@" lcalpha 0*13( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) +# lcalpha = %x61-7A ; a-z + +_KEY_WITHOUT_VENDOR_FORMAT = r"[a-z][_0-9a-z\-\*\/]{0,255}" +_KEY_WITH_VENDOR_FORMAT = ( + r"[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}" +) + +_KEY_FORMAT = _KEY_WITHOUT_VENDOR_FORMAT + "|" + _KEY_WITH_VENDOR_FORMAT +_KEY_PATTERN = re.compile(_KEY_FORMAT) + +# The value is an opaque string containing up to 256 printable +# ASCII [RFC0020] characters (i.e., the range 0x20 to 0x7E) +# except comma (,) and (=). +# value = 0*255(chr) nblk-chr +# nblk-chr = %x21-2B / %x2D-3C / %x3E-7E +# chr = %x20 / nblk-chr + +_VALUE_FORMAT = ( + r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]" +) +_VALUE_PATTERN = re.compile(_VALUE_FORMAT) + +_DELIMITER_FORMAT = "[ \t]*,[ \t]*" +_MEMBER_FORMAT = "({})(=)({})[ \t]*".format(_KEY_FORMAT, _VALUE_FORMAT) + +_DELIMITER_PATTERN = re.compile(_DELIMITER_FORMAT) +_MEMBER_PATTERN = re.compile(_MEMBER_FORMAT) + +_TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS = 32 + + +def _is_valid_key(key: str) -> bool: + if not isinstance(key, str): + return False + return _KEY_PATTERN.fullmatch(key) is not None + + +def _is_valid_value(value: str) -> bool: + if not isinstance(value, str): + return False + return _VALUE_PATTERN.fullmatch(value) is not None + + +def _is_valid_pair(key: str, value: str) -> bool: + return _is_valid_key(key) and _is_valid_value(value) diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 1e5c820243..0b20fbff4b 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -19,6 +19,7 @@ from opentelemetry import trace from opentelemetry.trace.propagation import tracecontext from opentelemetry.trace.propagation.textmap import DictGetter +from opentelemetry.trace.span import TraceState FORMAT = tracecontext.TraceContextTextMapPropagator() @@ -263,7 +264,7 @@ def test_fields(self, mock_get_current_span, mock_invalid_span_context): "trace_id": 1, "span_id": 2, "trace_flags": 3, - "trace_state": {"a": "b"}, + "trace_state": TraceState([("a", "b")]), } ) } diff --git a/opentelemetry-api/tests/trace/test_tracestate.py b/opentelemetry-api/tests/trace/test_tracestate.py new file mode 100644 index 0000000000..6665dd612d --- /dev/null +++ b/opentelemetry-api/tests/trace/test_tracestate.py @@ -0,0 +1,98 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: disable=no-member + +import unittest + +from opentelemetry.trace.span import TraceState + + +class TestTraceContextFormat(unittest.TestCase): + def test_empty_tracestate(self): + state = TraceState() + self.assertEqual(len(state), 0) + self.assertEqual(state.to_header(), "") + + def test_tracestate_valid_pairs(self): + pairs = [("1a-2f@foo", "bar1"), ("foo-_*/bar", "bar4")] + state = TraceState(pairs) + self.assertEqual(len(state), 2) + self.assertIsNotNone(state.get("foo-_*/bar")) + self.assertEqual(state.get("foo-_*/bar"), "bar4") + self.assertEqual(state.to_header(), "1a-2f@foo=bar1,foo-_*/bar=bar4") + self.assertIsNone(state.get("random")) + + def test_tracestate_add_valid(self): + state = TraceState() + new_state = state.add("1a-2f@foo", "bar4") + self.assertEqual(len(new_state), 1) + self.assertEqual(new_state.get("1a-2f@foo"), "bar4") + + def test_tracestate_add_invalid(self): + state = TraceState() + new_state = state.add("%%%nsasa", "val") + self.assertEqual(len(new_state), 0) + new_state = new_state.add("key", "====val====") + self.assertEqual(len(new_state), 0) + self.assertEqual(new_state.to_header(), "") + + def test_tracestate_update_valid(self): + state = TraceState([("a", "1")]) + new_state = state.update("a", "2") + self.assertEqual(new_state.get("a"), "2") + new_state = new_state.add("b", "3") + self.assertNotEqual(state, new_state) + + def test_tracestate_update_invalid(self): + state = TraceState([("a", "1")]) + new_state = state.update("a", "2=/") + self.assertNotEqual(new_state.get("a"), "2=/") + new_state = new_state.update("a", ",,2,,f") + self.assertNotEqual(new_state.get("a"), ",,2,,f") + self.assertEqual(new_state.get("a"), "1") + + def test_tracestate_delete_preserved(self): + state = TraceState([("a", "1"), ("b", "2"), ("c", "3")]) + new_state = state.delete("b") + self.assertIsNone(new_state.get("b")) + entries = list(new_state.items()) + a_place = entries.index(("a", "1")) + c_place = entries.index(("c", "3")) + self.assertLessEqual(a_place, c_place) + + def test_tracestate_from_header(self): + entries = [ + "1a-2f@foo=bar1", + "1a-_*/2b@foo=bar2", + "foo=bar3", + "foo-_*/bar=bar4", + ] + header_list = [",".join(entries)] + state = TraceState.from_header(header_list) + self.assertEqual(state.to_header(), ",".join(entries)) + + def test_tracestate_order_changed(self): + entries = [ + "1a-2f@foo=bar1", + "1a-_*/2b@foo=bar2", + "foo=bar3", + "foo-_*/bar=bar4", + ] + header_list = [",".join(entries)] + state = TraceState.from_header(header_list) + new_state = state.update("foo", "bar33") + entries = list(new_state.items()) # type: ignore + foo_place = entries.index(("foo", "bar33")) # type: ignore + prev_first_place = entries.index(("1a-2f@foo", "bar1")) # type: ignore + self.assertLessEqual(foo_place, prev_first_place) diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index 0d026de01d..e2b20a5c88 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -88,7 +88,7 @@ def _create_parent_span( ) def test_always_on(self): - trace_state = trace.TraceState({"key": "value"}) + trace_state = trace.TraceState([("key", "value")]) test_data = (TO_DEFAULT, TO_SAMPLED, None) for trace_flags in test_data: @@ -109,7 +109,7 @@ def test_always_on(self): self.assertEqual(sample_result.trace_state, trace_state) def test_always_off(self): - trace_state = trace.TraceState({"key": "value"}) + trace_state = trace.TraceState([("key", "value")]) test_data = (TO_DEFAULT, TO_SAMPLED, None) for trace_flags in test_data: with self.subTest(trace_flags=trace_flags): @@ -126,7 +126,7 @@ def test_always_off(self): self.assertEqual(sample_result.trace_state, trace_state) def test_default_on(self): - trace_state = trace.TraceState({"key": "value"}) + trace_state = trace.TraceState([("key", "value")]) context = self._create_parent(trace_flags=TO_DEFAULT) sample_result = sampling.DEFAULT_ON.should_sample( context, @@ -163,7 +163,7 @@ def test_default_on(self): self.assertEqual(sample_result.trace_state, trace_state) def test_default_off(self): - trace_state = trace.TraceState({"key": "value"}) + trace_state = trace.TraceState([("key", "value")]) context = self._create_parent(trace_flags=TO_DEFAULT) sample_result = sampling.DEFAULT_OFF.should_sample( context, @@ -200,7 +200,7 @@ def test_default_off(self): self.assertEqual(default_off.trace_state, trace_state) def test_probability_sampler(self): - trace_state = trace.TraceState({"key": "value"}) + trace_state = trace.TraceState([("key", "value")]) sampler = sampling.TraceIdRatioBased(0.5) # Check that we sample based on the trace ID if the parent context is @@ -313,7 +313,7 @@ def test_probability_sampler_limits(self): # pylint:disable=too-many-statements def exec_parent_based(self, parent_sampling_context): - trace_state = trace.TraceState({"key": "value"}) + trace_state = trace.TraceState([("key", "value")]) sampler = sampling.ParentBased(sampling.ALWAYS_ON) # Check that the sampling decision matches the parent context if given with parent_sampling_context( diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index ebe538d56d..df96703d9a 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -1219,7 +1219,7 @@ def test_to_json(self): "context": { "trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", - "trace_state": "{}" + "trace_state": "[]" }, "kind": "SpanKind.INTERNAL", "parent_id": null, @@ -1236,7 +1236,7 @@ def test_to_json(self): ) self.assertEqual( span.to_json(indent=None), - '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "{}"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {}, "events": [], "links": [], "resource": {}}', + '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "[]"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {}, "events": [], "links": [], "resource": {}}', ) def test_attributes_to_json(self): @@ -1253,7 +1253,7 @@ def test_attributes_to_json(self): date_str = ns_to_iso_str(123) self.assertEqual( span.to_json(indent=None), - '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "{}"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {"key": "value"}, "events": [{"name": "event", "timestamp": "' + '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "[]"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {"key": "value"}, "events": [{"name": "event", "timestamp": "' + date_str + '", "attributes": {"key2": "value2"}}], "links": [], "resource": {}}', ) From bbcecaba8597901c7b9614b06ebc1ac4c22682fc Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 20 Jan 2021 15:21:28 -0800 Subject: [PATCH 0747/1517] [pre-release] Update changelogs, version [0.17b0] (#1538) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 7 ++++--- docs/examples/error_hander/error_handler_0/setup.cfg | 2 +- .../error_handler_0/src/error_handler_0/version.py | 2 +- docs/examples/error_hander/error_handler_1/setup.cfg | 2 +- .../error_handler_1/src/error_handler_1/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 6 +++--- opentelemetry-distro/src/opentelemetry/distro/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- propagator/opentelemetry-propagator-b3/setup.cfg | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- propagator/opentelemetry-propagator-jaeger/setup.cfg | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 4 ++-- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/util/setup.cfg | 4 ++-- tests/util/src/opentelemetry/test/version.py | 2 +- 32 files changed, 45 insertions(+), 44 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 354b52a512..04f5ee3193 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 1e319dbaf21df7573f15f35773b8272579dd1030 + CONTRIB_REPO_SHA: 65a33ac4701773f98c1750c2249effc3f91313be jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index df7e779684..bdadff0df8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,16 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.16b1...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.17b0...HEAD) +## [0.17b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.17b0) - 2021-01-20 + +### Added - Add support for OTLP v0.6.0 ([#1472](https://github.com/open-telemetry/opentelemetry-python/pull/1472)) - Add protobuf via gRPC exporting support for Jaeger ([#1471](https://github.com/open-telemetry/opentelemetry-python/pull/1471)) - Add support for Python 3.9 ([#1441](https://github.com/open-telemetry/opentelemetry-python/pull/1441)) - -### Added - Added the ability to disable instrumenting libraries specified by OTEL_PYTHON_DISABLED_INSTRUMENTATIONS env variable, when using opentelemetry-instrument command. ([#1461](https://github.com/open-telemetry/opentelemetry-python/pull/1461)) - Add `fields` to propagators diff --git a/docs/examples/error_hander/error_handler_0/setup.cfg b/docs/examples/error_hander/error_handler_0/setup.cfg index e3c039c045..6145afd2a7 100644 --- a/docs/examples/error_hander/error_handler_0/setup.cfg +++ b/docs/examples/error_hander/error_handler_0/setup.cfg @@ -37,7 +37,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.17.dev0 + opentelemetry-sdk == 0.17b0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py +++ b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/docs/examples/error_hander/error_handler_1/setup.cfg b/docs/examples/error_hander/error_handler_1/setup.cfg index 749b0d827f..81435562a6 100644 --- a/docs/examples/error_hander/error_handler_1/setup.cfg +++ b/docs/examples/error_hander/error_handler_1/setup.cfg @@ -37,7 +37,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.17.dev0 + opentelemetry-sdk == 0.17b0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py +++ b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 7e3d6f50a5..1857822e13 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -42,8 +42,8 @@ install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 thrift >= 0.10.0 - opentelemetry-api == 0.17.dev0 - opentelemetry-sdk == 0.17.dev0 + opentelemetry-api == 0.17b0 + opentelemetry-sdk == 0.17b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 203d035bb2..97835b1f2f 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index c707b86b9f..fb493aab03 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.17.dev0 - opentelemetry-sdk == 0.17.dev0 + opentelemetry-api == 0.17b0 + opentelemetry-sdk == 0.17b0 protobuf >= 3.13.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index cb11e0e041..2f6fcd5058 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -42,9 +42,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.17.dev0 - opentelemetry-sdk == 0.17.dev0 - opentelemetry-proto == 0.17.dev0 + opentelemetry-api == 0.17b0 + opentelemetry-sdk == 0.17b0 + opentelemetry-proto == 0.17b0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index 40991ace38..ec23df2a07 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.17.dev0 - opentelemetry-sdk == 0.17.dev0 + opentelemetry-api == 0.17b0 + opentelemetry-sdk == 0.17b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index f8002c583d..01dafe1603 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 - opentelemetry-api == 0.17.dev0 - opentelemetry-sdk == 0.17.dev0 + opentelemetry-api == 0.17b0 + opentelemetry-sdk == 0.17b0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index a9340e4608..de077e9f3f 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.17.dev0 - opentelemetry-sdk == 0.17.dev0 + opentelemetry-api == 0.17b0 + opentelemetry-sdk == 0.17b0 [options.packages.find] where = src @@ -56,4 +56,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 0.17.dev0 + opentelemetry-exporter-otlp == 0.17b0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 265cdec80b..417bfb5846 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.17.dev0 + opentelemetry-api == 0.17b0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index b1ecb357d2..8effb59fb0 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.17.dev0 + opentelemetry-api == 0.17b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 9916bc343a..08b00793b6 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.17.dev0 + opentelemetry-api == 0.17b0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index a429ef5529..5d2403e5f7 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.17.dev0 + opentelemetry-api == 0.17b0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index abe7bc5815..0ac9135930 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.17.dev0 + opentelemetry-api == 0.17b0 [options.extras_require] test = - opentelemetry-test == 0.17.dev0 + opentelemetry-test == 0.17b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 86e1dbb17a..4f674f19fb 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.dev0" +__version__ = "0.17b0" diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index a1fdf1a01e..1f547d96d3 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.17.dev0 - opentelemetry-sdk == 0.17.dev0 + opentelemetry-api == 0.17b0 + opentelemetry-sdk == 0.17b0 [options.extras_require] test = flask~=1.0 diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index da85ff96db..be45780a72 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.17.dev0" +__version__ = "0.17b0" From db9905a164c1f3604eeb8ce5939243948f5be2a8 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 20 Jan 2021 15:51:31 -0800 Subject: [PATCH 0748/1517] rename instrumentation to shim (#1539) --- scripts/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index 718d91ef9d..53a9fa6eef 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ opentelemetry-distro/ exporter/*/ instrumentation/*/ propagator/*/; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ opentelemetry-distro/ exporter/*/ shim/*/ propagator/*/; do ( echo "building $d" cd "$d" From 33c9269b227df473920efc4976b132566d7d1301 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 21 Jan 2021 09:10:49 -0800 Subject: [PATCH 0749/1517] [post-release] updating version to 0.18.dev0 (#1541) --- .github/workflows/test.yml | 2 +- docs/examples/error_hander/error_handler_0/setup.cfg | 2 +- .../error_handler_0/src/error_handler_0/version.py | 2 +- docs/examples/error_hander/error_handler_1/setup.cfg | 2 +- .../error_handler_1/src/error_handler_1/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 6 +++--- opentelemetry-distro/src/opentelemetry/distro/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- propagator/opentelemetry-propagator-b3/setup.cfg | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- propagator/opentelemetry-propagator-jaeger/setup.cfg | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 4 ++-- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/util/setup.cfg | 4 ++-- tests/util/src/opentelemetry/test/version.py | 2 +- 31 files changed, 41 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 04f5ee3193..a473fef774 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 65a33ac4701773f98c1750c2249effc3f91313be + CONTRIB_REPO_SHA: a67a23d0a0a4dd7c3c06c7050c220fa3b3689a77 jobs: build: diff --git a/docs/examples/error_hander/error_handler_0/setup.cfg b/docs/examples/error_hander/error_handler_0/setup.cfg index 6145afd2a7..b79d972bc1 100644 --- a/docs/examples/error_hander/error_handler_0/setup.cfg +++ b/docs/examples/error_hander/error_handler_0/setup.cfg @@ -37,7 +37,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.17b0 + opentelemetry-sdk == 0.18.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py +++ b/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/docs/examples/error_hander/error_handler_1/setup.cfg b/docs/examples/error_hander/error_handler_1/setup.cfg index 81435562a6..f1ebad60f8 100644 --- a/docs/examples/error_hander/error_handler_1/setup.cfg +++ b/docs/examples/error_hander/error_handler_1/setup.cfg @@ -37,7 +37,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.17b0 + opentelemetry-sdk == 0.18.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py +++ b/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 1857822e13..040c4d5a63 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -42,8 +42,8 @@ install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 thrift >= 0.10.0 - opentelemetry-api == 0.17b0 - opentelemetry-sdk == 0.17b0 + opentelemetry-api == 0.18.dev0 + opentelemetry-sdk == 0.18.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 97835b1f2f..70e291a43a 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index fb493aab03..02b277e17e 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.17b0 - opentelemetry-sdk == 0.17b0 + opentelemetry-api == 0.18.dev0 + opentelemetry-sdk == 0.18.dev0 protobuf >= 3.13.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 2f6fcd5058..9e2be8636f 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -42,9 +42,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.17b0 - opentelemetry-sdk == 0.17b0 - opentelemetry-proto == 0.17b0 + opentelemetry-api == 0.18.dev0 + opentelemetry-sdk == 0.18.dev0 + opentelemetry-proto == 0.18.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index ec23df2a07..3c6720337a 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.17b0 - opentelemetry-sdk == 0.17b0 + opentelemetry-api == 0.18.dev0 + opentelemetry-sdk == 0.18.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 01dafe1603..2b4c642dee 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 - opentelemetry-api == 0.17b0 - opentelemetry-sdk == 0.17b0 + opentelemetry-api == 0.18.dev0 + opentelemetry-sdk == 0.18.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index de077e9f3f..ed6cb82545 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.17b0 - opentelemetry-sdk == 0.17b0 + opentelemetry-api == 0.18.dev0 + opentelemetry-sdk == 0.18.dev0 [options.packages.find] where = src @@ -56,4 +56,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 0.17b0 + opentelemetry-exporter-otlp == 0.18.dev0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 417bfb5846..825b1ad2ad 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.17b0 + opentelemetry-api == 0.18.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 8effb59fb0..4108b3efa8 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.17b0 + opentelemetry-api == 0.18.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 08b00793b6..9da95c16a9 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.17b0 + opentelemetry-api == 0.18.dev0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index 5d2403e5f7..255813b579 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.17b0 + opentelemetry-api == 0.18.dev0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 0ac9135930..8a7b0cfcce 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.17b0 + opentelemetry-api == 0.18.dev0 [options.extras_require] test = - opentelemetry-test == 0.17b0 + opentelemetry-test == 0.18.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 4f674f19fb..ebb75f6c11 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17b0" +__version__ = "0.18.dev0" diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index 1f547d96d3..eea2bc5b60 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.17b0 - opentelemetry-sdk == 0.17b0 + opentelemetry-api == 0.18.dev0 + opentelemetry-sdk == 0.18.dev0 [options.extras_require] test = flask~=1.0 diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index be45780a72..b34d984ac9 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.17b0" +__version__ = "0.18.dev0" From 4083cac3c0ec107df68cdecb8fc52c00e2684b08 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Thu, 21 Jan 2021 09:47:30 -0800 Subject: [PATCH 0750/1517] Add b3 format benchmark tests (#1489) Co-authored-by: alrex --- .../propagation/test_benchmark_b3_format.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py diff --git a/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py new file mode 100644 index 0000000000..843e6e96b0 --- /dev/null +++ b/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py @@ -0,0 +1,45 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import opentelemetry.propagators.b3 as b3_format +import opentelemetry.sdk.trace as trace +from opentelemetry.trace.propagation.textmap import DictGetter + +FORMAT = b3_format.B3Format() + + +def test_extract_single_header(benchmark): + benchmark( + FORMAT.extract, + DictGetter(), + { + FORMAT.SINGLE_HEADER_KEY: "bdb5b63237ed38aea578af665aa5aa60-c32d953d73ad2251-1-11fd79a30b0896cd285b396ae102dd76" + }, + ) + + +def test_inject_empty_context(benchmark): + tracer = trace.TracerProvider().get_tracer("sdk_tracer_provider") + with tracer.start_as_current_span("Root Span"): + with tracer.start_as_current_span("Child Span"): + benchmark( + FORMAT.inject, + dict.__setitem__, + { + FORMAT.TRACE_ID_KEY: "bdb5b63237ed38aea578af665aa5aa60", + FORMAT.SPAN_ID_KEY: "00000000000000000c32d953d73ad225", + FORMAT.PARENT_SPAN_ID_KEY: "11fd79a30b0896cd285b396ae102dd76", + FORMAT.SAMPLED_KEY: "1", + }, + ) From b32365bf71a8a258e668bd81e90f158f2a88a4d7 Mon Sep 17 00:00:00 2001 From: Anton Ryzhov Date: Thu, 21 Jan 2021 19:01:46 +0100 Subject: [PATCH 0751/1517] Allow to `start_as_current_span` with `end_on_exit=False` (#1519) --- CHANGELOG.md | 4 ++++ opentelemetry-api/src/opentelemetry/trace/__init__.py | 2 ++ opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 3 ++- opentelemetry-sdk/tests/trace/test_trace.py | 8 ++++++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdadff0df8..b2927d75f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.17b0...HEAD) +### Added +- Added `end_on_exit` argument to `start_as_current_span` + ([#1519](https://github.com/open-telemetry/opentelemetry-python/pull/1519)]) + ## [0.17b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.17b0) - 2021-01-20 ### Added diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 7371b8d72d..1c80b7a3ae 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -290,6 +290,7 @@ def start_as_current_span( start_time: typing.Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, + end_on_exit: bool = True, ) -> typing.Iterator["Span"]: """Context manager for creating a new span and set it as the current span in this tracer's context. @@ -396,6 +397,7 @@ def start_as_current_span( start_time: typing.Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, + end_on_exit: bool = True, ) -> typing.Iterator["Span"]: # pylint: disable=unused-argument,no-self-use yield INVALID_SPAN diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 2e8cb6b9e8..83c1eff93e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -759,6 +759,7 @@ def start_as_current_span( start_time: Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, + end_on_exit: bool = True, ) -> Iterator[trace_api.Span]: span = self.start_span( name=name, @@ -770,7 +771,7 @@ def start_as_current_span( record_exception=record_exception, set_status_on_exception=set_status_on_exception, ) - with self.use_span(span, end_on_exit=True) as span_context: + with self.use_span(span, end_on_exit=end_on_exit) as span_context: yield span_context def start_span( # pylint: disable=too-many-locals diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index df96703d9a..eb34714fbf 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -485,6 +485,14 @@ def func(): self.assertIsNotNone(root2.end_time) self.assertIsNot(root1, root2) + def test_start_as_current_span_no_end_on_exit(self): + tracer = new_tracer() + + with tracer.start_as_current_span("root", end_on_exit=False) as root: + self.assertIsNone(root.end_time) + + self.assertIsNone(root.end_time) + def test_explicit_span_resource(self): resource = resources.Resource.create({}) tracer_provider = trace.TracerProvider(resource=resource) From 6489bf50a576c9c772d2f7b78d677cf16b4526ac Mon Sep 17 00:00:00 2001 From: Anton Ryzhov Date: Thu, 21 Jan 2021 21:42:01 +0100 Subject: [PATCH 0752/1517] Add `Span.set_attributes` method (#1520) --- CHANGELOG.md | 2 + .../src/opentelemetry/trace/span.py | 16 +++++++ .../src/opentelemetry/sdk/trace/__init__.py | 43 +++++++++++-------- opentelemetry-sdk/tests/trace/test_trace.py | 18 +++++--- 4 files changed, 56 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2927d75f9..be58d61cea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `end_on_exit` argument to `start_as_current_span` ([#1519](https://github.com/open-telemetry/opentelemetry-python/pull/1519)]) +- Add `Span.set_attributes` method to set multiple values with one call + ([#1520](https://github.com/open-telemetry/opentelemetry-python/pull/1520)) ## [0.17b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.17b0) - 2021-01-20 diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index baeed76202..f46aca9c1e 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -41,6 +41,17 @@ def get_span_context(self) -> "SpanContext": A :class:`opentelemetry.trace.SpanContext` with a copy of this span's immutable state. """ + @abc.abstractmethod + def set_attributes( + self, attributes: typing.Dict[str, types.AttributeValue] + ) -> None: + """Sets Attributes. + + Sets Attributes with the key and value passed as arguments dict. + + Note: The behavior of `None` value attributes is undefined, and hence strongly discouraged. + """ + @abc.abstractmethod def set_attribute(self, key: str, value: types.AttributeValue) -> None: """Sets an Attribute. @@ -450,6 +461,11 @@ def is_recording(self) -> bool: def end(self, end_time: typing.Optional[int] = None) -> None: pass + def set_attributes( + self, attributes: typing.Dict[str, types.AttributeValue] + ) -> None: + pass + def set_attribute(self, key: str, value: types.AttributeValue) -> None: pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 83c1eff93e..09292cf4d9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -26,6 +26,7 @@ from typing import ( Any, Callable, + Dict, Iterator, MutableSequence, Optional, @@ -582,29 +583,35 @@ def to_json(self, indent=4): def get_span_context(self): return self.context - def set_attribute(self, key: str, value: types.AttributeValue) -> None: - if not _is_valid_attribute_value(value): - return - - if not key: - logger.warning("invalid key (empty or null)") - return - + def set_attributes( + self, attributes: Dict[str, types.AttributeValue] + ) -> None: with self._lock: if self.end_time is not None: logger.warning("Setting attribute on ended span.") return - # Freeze mutable sequences defensively - if isinstance(value, MutableSequence): - value = tuple(value) - if isinstance(value, bytes): - try: - value = value.decode() - except ValueError: - logger.warning("Byte attribute could not be decoded.") - return - self.attributes[key] = value + for key, value in attributes.items(): + if not _is_valid_attribute_value(value): + continue + + if not key: + logger.warning("invalid key `%s` (empty or null)", key) + continue + + # Freeze mutable sequences defensively + if isinstance(value, MutableSequence): + value = tuple(value) + if isinstance(value, bytes): + try: + value = value.decode() + except ValueError: + logger.warning("Byte attribute could not be decoded.") + return + self.attributes[key] = value + + def set_attribute(self, key: str, value: types.AttributeValue) -> None: + return self.set_attributes({key: value}) @_check_span_ended def _add_event(self, event: EventBase) -> None: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index eb34714fbf..71a1dcb44b 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -531,11 +531,14 @@ def test_basic_span(self): def test_attributes(self): with self.tracer.start_as_current_span("root") as root: - root.set_attribute("component", "http") - root.set_attribute("http.method", "GET") - root.set_attribute( - "http.url", "https://example.com:779/path/12/?q=d#123" + root.set_attributes( + { + "component": "http", + "http.method": "GET", + "http.url": "https://example.com:779/path/12/?q=d#123", + } ) + root.set_attribute("http.status_code", 200) root.set_attribute("http.status_text", "OK") root.set_attribute("misc.pi", 3.14) @@ -593,6 +596,10 @@ def test_attributes(self): def test_invalid_attribute_values(self): with self.tracer.start_as_current_span("root") as root: + root.set_attributes( + {"correct-value": "foo", "non-primitive-data-type": dict()} + ) + root.set_attribute("non-primitive-data-type", dict()) root.set_attribute( "list-of-mixed-data-types-numeric-first", @@ -609,7 +616,8 @@ def test_invalid_attribute_values(self): root.set_attribute("", 123) root.set_attribute(None, 123) - self.assertEqual(len(root.attributes), 0) + self.assertEqual(len(root.attributes), 1) + self.assertEqual(root.attributes["correct-value"], "foo") def test_byte_type_attribute_value(self): with self.tracer.start_as_current_span("root") as root: From 0e0465824bf8804f29b3c57caec48791cf571007 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Mon, 25 Jan 2021 15:30:54 -0800 Subject: [PATCH 0753/1517] Make sure Resources follow semantic conventions (#1480) --- CHANGELOG.md | 3 + .../opentelemetry/sdk/resources/__init__.py | 68 ++++++++++++++++++- .../tests/metrics/test_metrics.py | 48 +++++++++---- .../tests/resources/test_resources.py | 27 +++++++- opentelemetry-sdk/tests/trace/test_trace.py | 42 +++++++----- 5 files changed, 153 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be58d61cea..97c15396cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.17b0...HEAD) +======= ### Added - Added `end_on_exit` argument to `start_as_current_span` ([#1519](https://github.com/open-telemetry/opentelemetry-python/pull/1519)]) - Add `Span.set_attributes` method to set multiple values with one call ([#1520](https://github.com/open-telemetry/opentelemetry-python/pull/1520)) +- Make sure Resources follow semantic conventions + ([#1480](https://github.com/open-telemetry/opentelemetry-python/pull/1480)) ## [0.17b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.17b0) - 2021-01-20 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 086a2fdb5a..29e8ff7d1e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -91,9 +91,62 @@ logger = logging.getLogger(__name__) -TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language" +CLOUD_PROVIDER = "cloud.provider" +CLOUD_ACCOUNT_ID = "cloud.account.id" +CLOUD_REGION = "cloud.region" +CLOUD_ZONE = "cloud.zone" +CONTAINER_NAME = "container.name" +CONTAINER_ID = "container.id" +CONTAINER_IMAGE_NAME = "container.image.name" +CONTAINER_IMAGE_TAG = "container.image.tag" +DEPLOYMENT_ENVIRONMENT = "deployment.environment" +FAAS_NAME = "faas.name" +FAAS_ID = "faas.id" +FAAS_VERSION = "faas.version" +FAAS_INSTANCE = "faas.instance" +HOST_NAME = "host.name" +HOST_TYPE = "host.type" +HOST_IMAGE_NAME = "host.image.name" +HOST_IMAGE_ID = "host.image.id" +HOST_IMAGE_VERSION = "host.image.version" +KUBERNETES_CLUSTER_NAME = "k8s.cluster.name" +KUBERNETES_NAMESPACE_NAME = "k8s.namespace.name" +KUBERNETES_POD_UID = "k8s.pod.uid" +KUBERNETES_POD_NAME = "k8s.pod.name" +KUBERNETES_CONTAINER_NAME = "k8s.container.name" +KUBERNETES_REPLICA_SET_UID = "k8s.replicaset.uid" +KUBERNETES_REPLICA_SET_NAME = "k8s.replicaset.name" +KUBERNETES_DEPLOYMENT_UID = "k8s.deployment.uid" +KUBERNETES_DEPLOYMENT_NAME = "k8s.deployment.name" +KUBERNETES_STATEFUL_SET_UID = "k8s.statefulset.uid" +KUBERNETES_STATEFUL_SET_NAME = "k8s.statefulset.name" +KUBERNETES_DAEMON_SET_UID = "k8s.daemonset.uid" +KUBERNETES_DAEMON_SET_NAME = "k8s.daemonset.name" +KUBERNETES_JOB_UID = "k8s.job.uid" +KUBERNETES_JOB_NAME = "k8s.job.name" +KUBERNETES_CRON_JOB_UID = "k8s.cronjob.uid" +KUBERNETES_CRON_JOB_NAME = "k8s.cronjob.name" +OS_TYPE = "os.type" +OS_DESCRIPTION = "os.description" +PROCESS_PID = "process.pid" +PROCESS_EXECUTABLE_NAME = "process.executable.name" +PROCESS_EXECUTABLE_PATH = "process.executable.path" +PROCESS_COMMAND = "process.command" +PROCESS_COMMAND_LINE = "process.command_line" +PROCESS_COMMAND_ARGS = "process.command_args" +PROCESS_OWNER = "process.owner" +PROCESS_RUNTIME_NAME = "process.runtime.name" +PROCESS_RUNTIME_VERSION = "process.runtime.version" +PROCESS_RUNTIME_DESCRIPTION = "process.runtime.description" +SERVICE_NAME = "service.name" +SERVICE_NAMESPACE = "service.namespace" +SERVICE_INSTANCE_ID = "service.instance.id" +SERVICE_VERSION = "service.version" TELEMETRY_SDK_NAME = "telemetry.sdk.name" TELEMETRY_SDK_VERSION = "telemetry.sdk.version" +TELEMETRY_AUTO_VERSION = "telemetry.auto.version" +TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language" + OPENTELEMETRY_SDK_VERSION = pkg_resources.get_distribution( "opentelemetry-sdk" @@ -111,7 +164,18 @@ def create(attributes: typing.Optional[Attributes] = None) -> "Resource": resource = _DEFAULT_RESOURCE else: resource = _DEFAULT_RESOURCE.merge(Resource(attributes)) - return resource.merge(OTELResourceDetector().detect()) + resource = resource.merge(OTELResourceDetector().detect()) + if not resource.attributes.get(SERVICE_NAME, None): + default_service_name = "unknown_service" + process_executable_name = resource.attributes.get( + PROCESS_EXECUTABLE_NAME, None + ) + if process_executable_name: + default_service_name += ":" + process_executable_name + resource = resource.merge( + Resource({SERVICE_NAME: default_service_name}) + ) + return resource @staticmethod def create_empty() -> "Resource": diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 32c22c8c6b..aa628655c4 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -30,15 +30,37 @@ def test_stateful(self): meter = meter_provider.get_meter(__name__) self.assertIs(meter.processor.stateful, False) - def test_resource(self): - resource = resources.Resource.create({}) + def test_resource_empty(self): + resource = resources.Resource.create_empty() meter_provider = metrics.MeterProvider(resource=resource) self.assertIs(meter_provider.resource, resource) - def test_resource_empty(self): + def test_resources(self): meter_provider = metrics.MeterProvider() # pylint: disable=protected-access - self.assertEqual(meter_provider.resource, resources._DEFAULT_RESOURCE) + self.assertIsInstance(meter_provider.resource, resources.Resource) + self.assertEqual( + meter_provider.resource.attributes.get(resources.SERVICE_NAME), + "unknown_service", + ) + self.assertEqual( + meter_provider.resource.attributes.get( + resources.TELEMETRY_SDK_LANGUAGE + ), + "python", + ) + self.assertEqual( + meter_provider.resource.attributes.get( + resources.TELEMETRY_SDK_NAME + ), + "opentelemetry", + ) + self.assertEqual( + meter_provider.resource.attributes.get( + resources.TELEMETRY_SDK_VERSION + ), + resources.OPENTELEMETRY_SDK_VERSION, + ) def test_start_pipeline(self): exporter = Mock() @@ -74,7 +96,7 @@ def test_collect_metrics(self): meter = metrics.MeterProvider().get_meter(__name__) processor_mock = Mock() meter.processor = processor_mock - counter = meter.create_counter("name", "desc", "unit", float,) + counter = meter.create_counter("name", "desc", "unit", float) labels = {"key1": "value1"} meter.register_view(View(counter, SumAggregator)) counter.add(1.0, labels) @@ -155,7 +177,7 @@ def test_create_counter(self): resource = Mock(spec=resources.Resource) meter_provider = metrics.MeterProvider(resource=resource) meter = meter_provider.get_meter(__name__) - counter = meter.create_counter("name", "desc", "unit", int,) + counter = meter.create_counter("name", "desc", "unit", int) self.assertIsInstance(counter, metrics.Counter) self.assertEqual(counter.value_type, int) self.assertEqual(counter.name, "name") @@ -166,14 +188,14 @@ def test_instrument_same_name_error(self): resource = Mock(spec=resources.Resource) meter_provider = metrics.MeterProvider(resource=resource) meter = meter_provider.get_meter(__name__) - counter = meter.create_counter("name", "desc", "unit", int,) + counter = meter.create_counter("name", "desc", "unit", int) self.assertIsInstance(counter, metrics.Counter) self.assertEqual(counter.value_type, int) self.assertEqual(counter.name, "name") self.assertIs(meter_provider.resource, resource) self.assertEqual(counter.meter, meter) with self.assertRaises(ValueError) as ctx: - _ = meter.create_counter("naME", "desc", "unit", int,) + _ = meter.create_counter("naME", "desc", "unit", int) self.assertTrue( "Multiple instruments can't be registered by the same name: (name)" in str(ctx.exception) @@ -182,7 +204,7 @@ def test_instrument_same_name_error(self): def test_create_updowncounter(self): meter = metrics.MeterProvider().get_meter(__name__) updowncounter = meter.create_updowncounter( - "name", "desc", "unit", float, + "name", "desc", "unit", float ) self.assertIsInstance(updowncounter, metrics.UpDownCounter) self.assertEqual(updowncounter.value_type, float) @@ -191,7 +213,7 @@ def test_create_updowncounter(self): def test_create_valuerecorder(self): meter = metrics.MeterProvider().get_meter(__name__) valuerecorder = meter.create_valuerecorder( - "name", "desc", "unit", float, + "name", "desc", "unit", float ) self.assertIsInstance(valuerecorder, metrics.ValueRecorder) self.assertEqual(valuerecorder.value_type, float) @@ -204,7 +226,7 @@ def test_register_sumobserver(self): callback = Mock() observer = meter.register_sumobserver( - callback, "name", "desc", "unit", int, + callback, "name", "desc", "unit", int ) self.assertIsInstance(observer, metrics.SumObserver) @@ -224,7 +246,7 @@ def test_register_updownsumobserver(self): callback = Mock() observer = meter.register_updownsumobserver( - callback, "name", "desc", "unit", int, + callback, "name", "desc", "unit", int ) self.assertIsInstance(observer, metrics.UpDownSumObserver) @@ -244,7 +266,7 @@ def test_register_valueobserver(self): callback = Mock() observer = meter.register_valueobserver( - callback, "name", "desc", "unit", int, + callback, "name", "desc", "unit", int ) self.assertIsInstance(observer, metrics.ValueObserver) diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 35bffb10b8..1121459fd0 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -44,6 +44,7 @@ def test_create(self): resources.TELEMETRY_SDK_NAME: "opentelemetry", resources.TELEMETRY_SDK_LANGUAGE: "python", resources.TELEMETRY_SDK_VERSION: resources.OPENTELEMETRY_SDK_VERSION, + resources.SERVICE_NAME: "unknown_service", } resource = resources.Resource.create(attributes) @@ -62,10 +63,20 @@ def test_create(self): self.assertEqual(resource, resources._EMPTY_RESOURCE) resource = resources.Resource.create(None) - self.assertEqual(resource, resources._DEFAULT_RESOURCE) + self.assertEqual( + resource, + resources._DEFAULT_RESOURCE.merge( + resources.Resource({resources.SERVICE_NAME: "unknown_service"}) + ), + ) resource = resources.Resource.create({}) - self.assertEqual(resource, resources._DEFAULT_RESOURCE) + self.assertEqual( + resource, + resources._DEFAULT_RESOURCE.merge( + resources.Resource({resources.SERVICE_NAME: "unknown_service"}) + ), + ) def test_resource_merge(self): left = resources.Resource({"service": "ui"}) @@ -103,6 +114,7 @@ def test_immutability(self): resources.TELEMETRY_SDK_NAME: "opentelemetry", resources.TELEMETRY_SDK_LANGUAGE: "python", resources.TELEMETRY_SDK_VERSION: resources.OPENTELEMETRY_SDK_VERSION, + resources.SERVICE_NAME: "unknown_service", } attributes_copy = attributes.copy() @@ -117,6 +129,15 @@ def test_immutability(self): attributes["cost"] = 999.91 self.assertEqual(resource.attributes, attributes_copy) + def test_service_name_using_process_name(self): + resource = resources.Resource.create( + {resources.PROCESS_EXECUTABLE_NAME: "test"} + ) + self.assertEqual( + resource.attributes.get(resources.SERVICE_NAME), + "unknown_service:test", + ) + def test_aggregated_resources_no_detectors(self): aggregated_resources = resources.get_aggregated_resources([]) self.assertEqual( @@ -192,7 +213,7 @@ def test_resource_detector_raise_error(self): resource_detector.detect.side_effect = Exception() resource_detector.raise_on_error = True self.assertRaises( - Exception, resources.get_aggregated_resources, [resource_detector], + Exception, resources.get_aggregated_resources, [resource_detector] ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 71a1dcb44b..d17bd2ef21 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -505,7 +505,23 @@ def test_default_span_resource(self): tracer = tracer_provider.get_tracer(__name__) span = tracer.start_span("root") # pylint: disable=protected-access - self.assertEqual(span.resource, resources._DEFAULT_RESOURCE) + self.assertIsInstance(span.resource, resources.Resource) + self.assertEqual( + span.resource.attributes.get(resources.SERVICE_NAME), + "unknown_service", + ) + self.assertEqual( + span.resource.attributes.get(resources.TELEMETRY_SDK_LANGUAGE), + "python", + ) + self.assertEqual( + span.resource.attributes.get(resources.TELEMETRY_SDK_NAME), + "opentelemetry", + ) + self.assertEqual( + span.resource.attributes.get(resources.TELEMETRY_SDK_VERSION), + resources.OPENTELEMETRY_SDK_VERSION, + ) def test_span_context_remote_flag(self): tracer = new_tracer() @@ -835,7 +851,7 @@ def test_start_span(self): ) span.set_status(new_status) self.assertIs( - span.status.status_code, trace_api.status.StatusCode.ERROR, + span.status.status_code, trace_api.status.StatusCode.ERROR ) self.assertIs(span.status.description, "Test description") @@ -930,7 +946,7 @@ def error_status_test(context): with self.assertRaises(AssertionError): with context as root: root.set_status( - trace_api.status.Status(StatusCode.OK, "OK",) + trace_api.status.Status(StatusCode.OK, "OK") ) raise AssertionError("unknown") @@ -987,11 +1003,9 @@ def test_record_exception_with_attributes(self): "RuntimeError: error", exception_event.attributes["exception.stacktrace"], ) - self.assertIn( - "has_additional_attributes", exception_event.attributes, - ) + self.assertIn("has_additional_attributes", exception_event.attributes) self.assertEqual( - True, exception_event.attributes["has_additional_attributes"], + True, exception_event.attributes["has_additional_attributes"] ) def test_record_exception_escaped(self): @@ -1035,9 +1049,7 @@ def test_record_exception_with_timestamp(self): "RuntimeError: error", exception_event.attributes["exception.stacktrace"], ) - self.assertEqual( - 1604238587112021089, exception_event.timestamp, - ) + self.assertEqual(1604238587112021089, exception_event.timestamp) def test_record_exception_with_attributes_and_timestamp(self): span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) @@ -1059,15 +1071,11 @@ def test_record_exception_with_attributes_and_timestamp(self): "RuntimeError: error", exception_event.attributes["exception.stacktrace"], ) - self.assertIn( - "has_additional_attributes", exception_event.attributes, - ) - self.assertEqual( - True, exception_event.attributes["has_additional_attributes"], - ) + self.assertIn("has_additional_attributes", exception_event.attributes) self.assertEqual( - 1604238587112021089, exception_event.timestamp, + True, exception_event.attributes["has_additional_attributes"] ) + self.assertEqual(1604238587112021089, exception_event.timestamp) def test_record_exception_context_manager(self): try: From 9077ce3c2f3682b7858f701c60a31f31ab692f27 Mon Sep 17 00:00:00 2001 From: Morgan McLean Date: Tue, 26 Jan 2021 08:07:44 -0800 Subject: [PATCH 0754/1517] Add Zoom passcode (#1550) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9043aeace9..c131b62fe1 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ For information about contributing to OpenTelemetry Python, see [CONTRIBUTING.md We meet weekly on Thursdays at 9AM PST. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates. -Meetings take place via [Zoom video conference](https://zoom.us/j/6729396170). +Meetings take place via [Zoom video conference](https://zoom.us/j/6729396170). The passcode is _77777_. Meeting notes are available as a public [Google doc](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit). For edit access, get in touch on [Gitter](https://gitter.im/open-telemetry/opentelemetry-python). From a600657afd2f3a1c66e016a3ae2691edce6fb260 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 27 Jan 2021 04:08:29 +0530 Subject: [PATCH 0755/1517] Update docs (#1548) --- docs/examples/basic_tracer/README.rst | 2 +- docs/examples/basic_tracer/resources.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/examples/basic_tracer/README.rst b/docs/examples/basic_tracer/README.rst index 57d431a387..618263b7a4 100644 --- a/docs/examples/basic_tracer/README.rst +++ b/docs/examples/basic_tracer/README.rst @@ -7,7 +7,7 @@ There are two different examples: * basic_trace: Shows how to configure a SpanProcessor and Exporter, and how to create a tracer and span. -* resources: Shows how to add resource information to a Provider. Note that this must be run on a Google Compute Engine instance. +* resources: Shows how to add resource information to a Provider. The source files of these examples are available :scm_web:`here `. diff --git a/docs/examples/basic_tracer/resources.py b/docs/examples/basic_tracer/resources.py index f37e73531d..a889b8af96 100644 --- a/docs/examples/basic_tracer/resources.py +++ b/docs/examples/basic_tracer/resources.py @@ -1,15 +1,14 @@ from opentelemetry import trace -from opentelemetry.sdk.resources import get_aggregated_resources +from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleExportSpanProcessor, ) -from opentelemetry.tools.resource_detector import GoogleCloudResourceDetector -resources = get_aggregated_resources([GoogleCloudResourceDetector()]) +resource = Resource.create({"service.name": "basic_service"}) -trace.set_tracer_provider(TracerProvider(resource=resources)) +trace.set_tracer_provider(TracerProvider(resource=resource)) trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) From f433bc7bbd7e7484b7a84f1876e320c4c90ed0d6 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 26 Jan 2021 14:59:03 -0800 Subject: [PATCH 0756/1517] adding note to check on the stable docs build (#1552) --- RELEASING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASING.md b/RELEASING.md index 8b9abee354..efee93b39f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -56,6 +56,8 @@ git tag stable git push origin stable ``` +To validate this worked, ensure the stable build has run successfully: https://readthedocs.org/projects/opentelemetry-python/builds/. If the build has not run automatically, it can be manually trigger via the readthedocs interface. + ## Update master Ensure the version and changelog updates have been applied to master. From ad894b19f531706645491869151f63a4138cf768 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 26 Jan 2021 15:55:29 -0800 Subject: [PATCH 0757/1517] Update some docs (#1553) --- docs/examples/basic_tracer/basic_trace.py | 14 ++++++++++++++ docs/examples/basic_tracer/resources.py | 14 ++++++++++++++ .../opentelemetry/baggage/propagation/__init__.py | 3 +++ .../src/opentelemetry/trace/__init__.py | 4 +++- .../src/opentelemetry/trace/propagation/textmap.py | 14 +++++++++++--- opentelemetry-api/src/opentelemetry/trace/span.py | 2 +- .../src/opentelemetry/sdk/resources/__init__.py | 6 +++--- .../src/opentelemetry/sdk/trace/__init__.py | 7 +++++-- .../src/opentelemetry/sdk/trace/export/__init__.py | 4 ++-- 9 files changed, 56 insertions(+), 12 deletions(-) diff --git a/docs/examples/basic_tracer/basic_trace.py b/docs/examples/basic_tracer/basic_trace.py index f7df424da3..5fcfe51d19 100644 --- a/docs/examples/basic_tracer/basic_trace.py +++ b/docs/examples/basic_tracer/basic_trace.py @@ -1,3 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( diff --git a/docs/examples/basic_tracer/resources.py b/docs/examples/basic_tracer/resources.py index a889b8af96..1d1a6cd047 100644 --- a/docs/examples/basic_tracer/resources.py +++ b/docs/examples/basic_tracer/resources.py @@ -1,3 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index 75ba25bbe8..ae5e0c34e1 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -22,6 +22,9 @@ class BaggagePropagator(textmap.TextMapPropagator): + """Extracts and injects Baggage which is used to annotate telemetry. + """ + MAX_HEADER_LENGTH = 8192 MAX_PAIR_LENGTH = 4096 MAX_PAIRS = 180 diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 1c80b7a3ae..b579e5ccf7 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -338,6 +338,8 @@ def start_as_current_span( be automatically set to ERROR when an uncaught exception is raised in the span with block. The span status won't be set by this mechanism if it was previously set manually. + end_on_exit: Whether to end the span automatically when leaving the + context manager. Yields: The newly-created span. @@ -423,7 +425,7 @@ def get_tracer( This function is a convenience wrapper for opentelemetry.trace.TracerProvider.get_tracer. - If tracer_provider is ommited the current configured one is used. + If tracer_provider is omitted the current configured one is used. """ if tracer_provider is None: tracer_provider = get_tracer_provider() diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py b/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py index 0612a740f6..cf93d1d631 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py @@ -25,8 +25,7 @@ class Getter(typing.Generic[TextMapPropagatorT]): """This class implements a Getter that enables extracting propagated - fields from a carrier - + fields from a carrier. """ def get( @@ -34,7 +33,7 @@ def get( ) -> typing.Optional[typing.List[str]]: """Function that can retrieve zero or more values from the carrier. In the case that - the value does not exist, returns an empty list. + the value does not exist, returns None. Args: carrier: An object which contains values that are used to @@ -61,6 +60,14 @@ class DictGetter(Getter[typing.Dict[str, CarrierValT]]): def get( self, carrier: typing.Dict[str, CarrierValT], key: str ) -> typing.Optional[typing.List[str]]: + """Getter implementation to retrieve a value from a dictionary. + + Args: + carrier: dictionary in which header + key: the key used to get the value + Returns: + A list with a single string with the value if it exists, else None. + """ val = carrier.get(key, None) if val is None: return None @@ -69,6 +76,7 @@ def get( return [val] def keys(self, carrier: typing.Dict[str, CarrierValT]) -> typing.List[str]: + """Keys implementation that returns all keys from a dictionary.""" return list(carrier.keys()) diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index f46aca9c1e..3ef2ad91d2 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -360,9 +360,9 @@ class SpanContext( Args: trace_id: The ID of the trace that this span belongs to. span_id: This span's ID. + is_remote: True if propagated from a remote parent. trace_flags: Trace options to propagate. trace_state: Tracing-system-specific info to propagate. - is_remote: True if propagated from a remote parent. """ def __new__( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 29e8ff7d1e..436b4d6c64 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -32,15 +32,15 @@ .. code-block:: python - metrics.set_meter_provider( - MeterProvider( + trace.set_tracer_provider( + TracerProvider( resource=Resource.create({ "service.name": "shoppingcart", "service.instance.id": "instance-12", }), ), ) - print(metrics.get_meter_provider().resource.attributes) + print(trace.get_tracer_provider().resource.attributes) {'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 09292cf4d9..adafedb3da 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -427,8 +427,8 @@ def __init__( sampler: Optional[sampling.Sampler] = None, trace_config: None = None, # TODO resource: Resource = Resource.create({}), - attributes: types.Attributes = None, # TODO - events: Sequence[Event] = None, # TODO + attributes: types.Attributes = None, + events: Sequence[Event] = None, links: Sequence[trace_api.Link] = (), kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, span_processor: SpanProcessor = SpanProcessor(), @@ -899,6 +899,9 @@ def use_span( class TracerProvider(trace_api.TracerProvider): + """See `opentelemetry.trace.TracerProvider`. + """ + def __init__( self, sampler: sampling.Sampler = TRACE_SAMPLER, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index d8786e6d21..ee63c84375 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -38,8 +38,8 @@ class SpanExportResult(Enum): class SpanExporter: """Interface for exporting spans. - Interface to be implemented by services that want to export recorded in - its own format. + Interface to be implemented by services that want to export spans recorded + in their own format. To export data this MUST be registered to the :class`opentelemetry.sdk.trace.Tracer` using a `SimpleExportSpanProcessor` or a `BatchExportSpanProcessor`. From c026c0f6a53adae1af6cf36fb4bb830a31c6ffc9 Mon Sep 17 00:00:00 2001 From: Ewan Higgs Date: Wed, 27 Jan 2021 17:31:32 +0100 Subject: [PATCH 0758/1517] 1542. Allow missing carrier headers to continue without raising AttributeError (#1545) --- CHANGELOG.md | 3 ++- .../src/opentelemetry/propagators/jaeger/__init__.py | 7 ++++--- .../tests/test_jaeger_propagator.py | 7 +++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97c15396cb..9bed862e52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.17b0...HEAD) -======= ### Added - Added `end_on_exit` argument to `start_as_current_span` ([#1519](https://github.com/open-telemetry/opentelemetry-python/pull/1519)]) @@ -14,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1520](https://github.com/open-telemetry/opentelemetry-python/pull/1520)) - Make sure Resources follow semantic conventions ([#1480](https://github.com/open-telemetry/opentelemetry-python/pull/1480)) +- Allow missing carrier headers to continue without raising AttributeError + ([#1545](https://github.com/open-telemetry/opentelemetry-python/pull/1545)) ## [0.17b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.17b0) - 2021-01-20 diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py index f9ad768291..67c7d716c9 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py @@ -45,9 +45,10 @@ def extract( if context is None: context = get_current() - fields = _extract_first_element( - getter.get(carrier, self.TRACE_ID_KEY) - ).split(":") + header = getter.get(carrier, self.TRACE_ID_KEY) + if not header: + return trace.set_span_in_context(trace.INVALID_SPAN, context) + fields = _extract_first_element(header).split(":") context = self._extract_baggage(getter, carrier, context) if len(fields) != 4: diff --git a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py index 6e6f548d09..46c554d4f5 100644 --- a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py @@ -83,6 +83,13 @@ def test_extract_valid_span(self): self.assertEqual(span_context.trace_id, self.trace_id) self.assertEqual(span_context.span_id, self.span_id) + def test_missing_carrier(self): + old_carrier = {} + ctx = FORMAT.extract(carrier_getter, old_carrier) + span_context = trace_api.get_current_span(ctx).get_span_context() + self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) + self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) + def test_trace_id(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} _, new_carrier = get_context_new_carrier(old_carrier) From 18761d78e30a4293abd65a897129b57fbf0c0a15 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 28 Jan 2021 09:20:50 -0800 Subject: [PATCH 0759/1517] Update Resource merge key conflict precedence (#1544) --- CHANGELOG.md | 2 ++ .../opentelemetry/sdk/resources/__init__.py | 35 ++++++++++++++----- .../tests/resources/test_resources.py | 30 ++++++++++++---- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bed862e52..a83dd049e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1536](https://github.com/open-telemetry/opentelemetry-python/pull/1536)) - Fix TraceState to adhere to specs ([#1502](https://github.com/open-telemetry/opentelemetry-python/pull/1502)) +- Update Resource `merge` key conflict precedence + ([#1544](https://github.com/open-telemetry/opentelemetry-python/pull/1544)) ### Removed - `opentelemetry-api` Remove ThreadLocalRuntimeContext since python3.4 is not supported. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 436b4d6c64..2b832cff7d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -155,16 +155,27 @@ class Resource: + """A Resource is an immutable representation of the entity producing telemetry as Attributes. + """ + def __init__(self, attributes: Attributes): self._attributes = attributes.copy() @staticmethod def create(attributes: typing.Optional[Attributes] = None) -> "Resource": + """Creates a new `Resource` from attributes. + + Args: + attributes: Optional zero or more key-value pairs. + + Returns: + The newly-created Resource. + """ if not attributes: - resource = _DEFAULT_RESOURCE - else: - resource = _DEFAULT_RESOURCE.merge(Resource(attributes)) - resource = resource.merge(OTELResourceDetector().detect()) + attributes = {} + resource = _DEFAULT_RESOURCE.merge( + OTELResourceDetector().detect() + ).merge(Resource(attributes)) if not resource.attributes.get(SERVICE_NAME, None): default_service_name = "unknown_service" process_executable_name = resource.attributes.get( @@ -186,11 +197,19 @@ def attributes(self) -> Attributes: return self._attributes.copy() def merge(self, other: "Resource") -> "Resource": + """Merges this resource and an updating resource into a new `Resource`. + + If a key exists on both the old and updating resource, the value of the + updating resource will override the old resource value. + + Args: + other: The other resource to be merged. + + Returns: + The newly-created Resource. + """ merged_attributes = self.attributes - # pylint: disable=protected-access - for key, value in other._attributes.items(): - if key not in merged_attributes or merged_attributes[key] == "": - merged_attributes[key] = value + merged_attributes.update(other.attributes) return Resource(merged_attributes) def __eq__(self, other: object) -> bool: diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 1121459fd0..3effa5d245 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -99,7 +99,7 @@ def test_resource_merge_empty_string(self): ) self.assertEqual( left.merge(right), - resources.Resource({"service": "ui", "host": "service-host"}), + resources.Resource({"service": "not-ui", "host": "service-host"}), ) def test_immutability(self): @@ -154,7 +154,6 @@ def test_aggregated_resources_with_static_resource(self): static_resource, ) - # Static resource values should never be overwritten resource_detector = mock.Mock(spec=resources.ResourceDetector) resource_detector.detect.return_value = resources.Resource( {"static_key": "try_to_overwrite_existing_value", "key": "value"} @@ -163,7 +162,12 @@ def test_aggregated_resources_with_static_resource(self): resources.get_aggregated_resources( [resource_detector], initial_resource=static_resource ), - resources.Resource({"static_key": "static_value", "key": "value"}), + resources.Resource( + { + "static_key": "try_to_overwrite_existing_value", + "key": "value", + } + ), ) def test_aggregated_resources_multiple_detectors(self): @@ -184,7 +188,6 @@ def test_aggregated_resources_multiple_detectors(self): } ) - # New values should not overwrite existing values self.assertEqual( resources.get_aggregated_resources( [resource_detector1, resource_detector2, resource_detector3] @@ -192,8 +195,8 @@ def test_aggregated_resources_multiple_detectors(self): resources.Resource( { "key1": "value1", - "key2": "value2", - "key3": "value3", + "key2": "try_to_overwrite_existing_value", + "key3": "try_to_overwrite_existing_value", "key4": "value4", } ), @@ -216,6 +219,21 @@ def test_resource_detector_raise_error(self): Exception, resources.get_aggregated_resources, [resource_detector] ) + @mock.patch.dict( + os.environ, + {"OTEL_RESOURCE_ATTRIBUTES": "key1=env_value1,key2=env_value2"}, + ) + def test_env_priority(self): + resource_env = resources.Resource.create() + self.assertEqual(resource_env.attributes["key1"], "env_value1") + self.assertEqual(resource_env.attributes["key2"], "env_value2") + + resource_env_override = resources.Resource.create( + {"key1": "value1", "key2": "value2"} + ) + self.assertEqual(resource_env_override.attributes["key1"], "value1") + self.assertEqual(resource_env_override.attributes["key2"], "value2") + class TestOTELResourceDetector(unittest.TestCase): def setUp(self) -> None: From cf8b70e77b65686d6bc40db227e4af63ee336cf5 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 29 Jan 2021 11:47:19 -0800 Subject: [PATCH 0760/1517] updating references to main (#1558) --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/workflows/changelog.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- CONTRIBUTING.md | 18 +++++++++--------- README.md | 14 +++++++------- RELEASING.md | 16 ++++++++-------- docs/conf.py | 2 +- docs/examples/auto-instrumentation/README.rst | 2 +- docs/examples/django/README.rst | 2 +- docs/getting-started.rst | 6 +++--- docs/index.rst | 4 ++-- .../opentelemetry-exporter-jaeger/README.rst | 4 ++-- .../opentelemetry-exporter-jaeger/setup.cfg | 2 +- .../setup.cfg | 2 +- .../opentelemetry-exporter-otlp/README.rst | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 2 +- .../opentelemetry/exporter/otlp/__init__.py | 2 +- .../setup.cfg | 2 +- .../opentelemetry-exporter-zipkin/setup.cfg | 2 +- .../opentelemetry/exporter/zipkin/__init__.py | 2 +- opentelemetry-api/setup.cfg | 2 +- .../src/opentelemetry/metrics/__init__.py | 2 +- .../src/opentelemetry/propagators/__init__.py | 6 +++--- opentelemetry-distro/setup.cfg | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../opentelemetry/instrumentation/metric.py | 2 +- .../src/opentelemetry/instrumentation/utils.py | 2 +- .../tests/test_utils.py | 2 +- opentelemetry-proto/README.rst | 2 +- opentelemetry-proto/setup.cfg | 2 +- opentelemetry-sdk/setup.cfg | 2 +- .../opentelemetry/sdk/resources/__init__.py | 11 +++++------ .../opentelemetry/sdk/trace/ids_generator.py | 2 +- .../src/opentelemetry/sdk/trace/sampling.py | 2 +- .../opentelemetry-propagator-b3/setup.cfg | 2 +- .../opentelemetry-propagator-jaeger/setup.cfg | 2 +- scripts/prepare_release.sh | 6 +++--- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- 38 files changed, 73 insertions(+), 74 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 832b294a94..2597b185d0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,7 @@ about: Create a report to help us improve labels: bug --- -**Describe your environment** Describe any aspect of your environment relevant to the problem, including your Python version, [platform](https://docs.python.org/3/library/platform.html), version numbers of installed dependencies, information about your cloud hosting provider, etc. If you're reporting a problem with a specific version of a library in this repo, please check whether the problem has been fixed on master. +**Describe your environment** Describe any aspect of your environment relevant to the problem, including your Python version, [platform](https://docs.python.org/3/library/platform.html), version numbers of installed dependencies, information about your cloud hosting provider, etc. If you're reporting a problem with a specific version of a library in this repo, please check whether the problem has been fixed on main. **Steps to reproduce** Describe exactly how to reproduce the error. Include a code sample if applicable. diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 7e4b1032a7..11e1a61fc5 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -1,4 +1,4 @@ -# This action requires that any PR targeting the master branch should touch at +# This action requires that any PR targeting the main branch should touch at # least one CHANGELOG file. If a CHANGELOG entry is not required, add the "Skip # Changelog" label to disable this action. @@ -8,7 +8,7 @@ on: pull_request: types: [opened, synchronize, reopened, labeled, unlabeled] branches: - - master + - main jobs: changelog: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a473fef774..a276d60601 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ on: - 'release/*' pull_request: env: - # Set variable to 'master' if your change will not affect Contrib. + # Set variable to 'main' if your change will not affect Contrib. # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. @@ -82,7 +82,7 @@ jobs: alert-threshold: 200% fail-on-alert: true # Make a commit on `gh-pages` with benchmarks from previous step - auto-push: ${{ github.ref == 'refs/heads/master' }} + auto-push: ${{ github.ref == 'refs/heads/main' }} gh-pages-branch: gh-pages benchmark-data-dir-path: benchmarks misc: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7612cb8516..c12c2e13ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,10 +8,10 @@ See the [public meeting notes](https://docs.google.com/document/d/1CIMGoIOZ-c3-i for a summary description of past meetings. To request edit access, join the meeting or get in touch on [Gitter](https://gitter.im/open-telemetry/opentelemetry-python). -See to the [community membership document](https://github.com/open-telemetry/community/blob/master/community-membership.md) -on how to become a [**Member**](https://github.com/open-telemetry/community/blob/master/community-membership.md#member), -[**Approver**](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver) -and [**Maintainer**](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer). +See to the [community membership document](https://github.com/open-telemetry/community/blob/main/community-membership.md) +on how to become a [**Member**](https://github.com/open-telemetry/community/blob/main/community-membership.md#member), +[**Approver**](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver) +and [**Maintainer**](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer). # Find your right repo @@ -63,7 +63,7 @@ You can run: - `tox -e lint` to run lint checks on all code See -[`tox.ini`](https://github.com/open-telemetry/opentelemetry-python/blob/master/tox.ini) +[`tox.ini`](https://github.com/open-telemetry/opentelemetry-python/blob/main/tox.ini) for more detail on available tox commands. ### Benchmarks @@ -144,7 +144,7 @@ to equal the commit SHA of the Core repo PR to pass tests equal the commit SHA of the Contrib repo PR to pass Contrib repo tests (a sanity check for the Maintainers & Approvers) 4. Merge the Contrib repo -5. Restore the Core repo PR `CONTRIB_REPO_SHA` to point to `master` +5. Restore the Core repo PR `CONTRIB_REPO_SHA` to point to `main` 6. Merge the Core repo PR ### How to Receive Comments @@ -156,8 +156,8 @@ check for the Maintainers & Approvers) ### How to Get PRs Merged A PR is considered to be **ready to merge** when: -* It has received two approvals from [Approvers](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver) - / [Maintainers](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer) +* It has received two approvals from [Approvers](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver) + / [Maintainers](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer) (at different companies). * Major feedbacks are resolved. * All tests are passing, including Contrib Repo tests which may require @@ -174,7 +174,7 @@ Any Approver / Maintainer can merge the PR once it is **ready to merge**. As with other OpenTelemetry clients, opentelemetry-python follows the [opentelemetry-specification](https://github.com/open-telemetry/opentelemetry-specification). -It's especially valuable to read through the [library guidelines](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/library-guidelines.md). +It's especially valuable to read through the [library guidelines](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/library-guidelines.md). ### Focus on Capabilities, Not Structure Compliance diff --git a/README.md b/README.md index c131b62fe1..eeb0ea231a 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ GitHub release (latest by date including pre-releases) - + Codecov Status - + license
@@ -61,7 +61,7 @@ pip install opentelemetry-sdk ``` The -[`instrumentation/`](https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation) +[`instrumentation/`](https://github.com/open-telemetry/opentelemetry-python/tree/main/instrumentation) directory includes OpenTelemetry instrumentation packages. You can install the packages separately with the following command: ```sh @@ -69,7 +69,7 @@ pip install opentelemetry-instrumentation-{instrumentation} ``` The -[`exporter/`](https://github.com/open-telemetry/opentelemetry-python/tree/master/exporter) +[`exporter/`](https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter) directory includes OpenTelemetry exporter packages. You can install the packages separately with the following command: ```sh @@ -77,7 +77,7 @@ pip install opentelemetry-exporter-{exporter} ``` The -[`propagator/`](https://github.com/open-telemetry/opentelemetry-python/tree/master/propagator) +[`propagator/`](https://github.com/open-telemetry/opentelemetry-python/tree/main/propagator) directory includes OpenTelemetry propagator packages. You can install the packages separately with the following command: ```sh @@ -125,14 +125,14 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Reiley Yang](https://github.com/reyang), Microsoft - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google -*For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver).* +*For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver).* Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-telemetry/teams/python-maintainers)): - [Alex Boten](https://github.com/codeboten), Lightstep - [Leighton Chen](https://github.com/lzchen), Microsoft -*For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).* +*For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer).* ### Thanks to all the people who already contributed! diff --git a/RELEASING.md b/RELEASING.md index efee93b39f..a16777987e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,7 +7,7 @@ Release Process: * [Open a Pull Request](#open-a-pull-request) * [Create a Release](#Create-a-Release) * [Move stable tag](#Move-stable-tag) -* [Update master](#Update-master) +* [Update main](#Update-main) * [Check PyPI](#Check-PyPI) * [Troubleshooting](#troubleshooting) @@ -16,7 +16,7 @@ Release Process: ## Create a new branch The following script does the following: -- update master locally +- update main locally - creates a new release branch `release/` - updates version and changelog files - commits the change to a new branch `release/-auto` @@ -39,7 +39,7 @@ The PR should be opened from the `release/-auto` branch created as part ## Check PyPI -This should be handled automatically on release by the [publish action](https://github.com/open-telemetry/opentelemetry-python/blob/master/.github/workflows/publish.yml). +This should be handled automatically on release by the [publish action](https://github.com/open-telemetry/opentelemetry-python/blob/main/.github/workflows/publish.yml). - Check the [action logs](https://github.com/open-telemetry/opentelemetry-python/actions?query=workflow%3APublish) to make sure packages have been uploaded to PyPI - Check the release history (e.g. https://pypi.org/project/opentelemetry-api/#history) on PyPI @@ -58,18 +58,18 @@ git push origin stable To validate this worked, ensure the stable build has run successfully: https://readthedocs.org/projects/opentelemetry-python/builds/. If the build has not run automatically, it can be manually trigger via the readthedocs interface. -## Update master +## Update main -Ensure the version and changelog updates have been applied to master. +Ensure the version and changelog updates have been applied to main. ```bash -# checkout a new branch from master -git checkout -b v0.7b0-master-update +# checkout a new branch from main +git checkout -b v0.7b0-main-update # cherry pick the change from the release branch git cherry-pick $(git log -n 1 origin/release/0.7b0 --format="%H") # update the version number, make it a "dev" greater than release number, e.g. 0.8.dev0 perl -i -p -e 's/0.7b0/0.8.dev0/' $(git grep -l "0.7b0" | grep -vi CHANGELOG) -# open a PR targeting master see #331 +# open a PR targeting main see #331 git commit -m ``` diff --git a/docs/conf.py b/docs/conf.py index 1da52b4a6d..42eefa1ce1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -144,7 +144,7 @@ # Support external links to specific versions of the files in the Github repo branch = os.environ.get("READTHEDOCS_VERSION") if branch is None or branch == "latest": - branch = "master" + branch = "main" REPO = "open-telemetry/opentelemetry-python/" scm_raw_web = "https://raw.githubusercontent.com/" + REPO + branch diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index d7e59eedef..f85106b66d 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -11,7 +11,7 @@ The example is based on a previous OpenTracing example that you can find `here `__. -The source files for these examples are available `here `__. +The source files for these examples are available `here `__. This example uses two different scripts. The main difference between them is whether or not they're instrumented manually: diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 4545dd57b3..0e5606c6da 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -111,4 +111,4 @@ References * `Django `_ * `OpenTelemetry Project `_ -* `OpenTelemetry Django extension `_ +* `OpenTelemetry Django extension `_ diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 228e254df6..f2cd75717f 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -3,7 +3,7 @@ Getting Started with OpenTelemetry Python This guide walks you through instrumenting a Python application with ``opentelemetry-python``. -For more elaborate examples, see `examples `_. +For more elaborate examples, see `examples `_. Hello world: emit a trace to your console --------------------------------------------- @@ -149,7 +149,7 @@ While the example in the previous section is great, it's very manual. The follow * Database calls To track these common actions, OpenTelemetry has the concept of instrumentations. Instrumentations are packages designed to interface -with a specific framework or library, such as Flask and psycopg2. You can find a list of the currently curated extension packages in the `Contrib repository `_. +with a specific framework or library, such as Flask and psycopg2. You can find a list of the currently curated extension packages in the `Contrib repository `_. Instrument a basic Flask application that uses the requests library to send HTTP requests. First, install the instrumentation packages themselves: @@ -180,7 +180,7 @@ A major feature of distributed tracing is the ability to correlate a trace acros multiple services. However, those services need to propagate information about a trace from one service to the other. -To enable this propagation, OpenTelemetry has the concept of `propagators `_, +To enable this propagation, OpenTelemetry has the concept of `propagators `_, which provide a common method to encode and decode span information from a request and response, respectively. By default, ``opentelemetry-python`` is configured to use the `W3C Trace Context `_ diff --git a/docs/index.rst b/docs/index.rst index eb62e6a009..66797a3a74 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,8 +32,8 @@ In addition, there are several extension packages which can be installed separat These are for exporter and instrumentation packages respectively. Some packages can be found in :scm_web:`instrumentation ` and :scm_web:`exporter ` directory of the repository. The remaining packages can be found at the -`Contrib repo instrumentation `_ -and `Contrib repo exporter `_ directories. +`Contrib repo instrumentation `_ +and `Contrib repo exporter `_ directories. Extensions ---------- diff --git a/exporter/opentelemetry-exporter-jaeger/README.rst b/exporter/opentelemetry-exporter-jaeger/README.rst index 8a9b6b0819..67798395a3 100644 --- a/exporter/opentelemetry-exporter-jaeger/README.rst +++ b/exporter/opentelemetry-exporter-jaeger/README.rst @@ -23,9 +23,9 @@ Configuration ------------- OpenTelemetry Jaeger Exporter can be configured by setting `JaegerSpanExporter parameters -`_ or by setting -`environment variables `_ References diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 040c4d5a63..c5fa47b2d6 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/exporter/opentelemetry-exporter-jaeger +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger platforms = any license = Apache-2.0 classifiers = diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 02b277e17e..9c8dc747dd 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/exporter/opentelemetry-exporter-opencensus +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-opencensus platforms = any license = Apache-2.0 classifiers = diff --git a/exporter/opentelemetry-exporter-otlp/README.rst b/exporter/opentelemetry-exporter-otlp/README.rst index 8adf657181..bd3b581e1d 100644 --- a/exporter/opentelemetry-exporter-otlp/README.rst +++ b/exporter/opentelemetry-exporter-otlp/README.rst @@ -22,4 +22,4 @@ References * `OpenTelemetry Collector Exporter `_ * `OpenTelemetry Collector `_ * `OpenTelemetry `_ -* `OpenTelemetry Protocol Specification `_ +* `OpenTelemetry Protocol Specification `_ diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 9e2be8636f..23eb724df4 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/exporter/opentelemetry-exporter-otlp +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp platforms = any license = Apache-2.0 classifiers = diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py index 1094be286d..abc4186b71 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py @@ -34,7 +34,7 @@ be in the format of a string "gzip" for gzip compression, and no value specified if no compression is the desired choice. Additional details are available `in the specification -`_. +`_. .. code:: python diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index 3c6720337a..f8f463c257 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/exporter/opentelemetry-exporter-prometheus +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-prometheus platforms = any license = Apache-2.0 classifiers = diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 2b4c642dee..45cdb03436 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/exporter/opentelemetry-exporter-zipkin +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-zipkin platforms = any license = Apache-2.0 classifiers = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 387e33d594..3f1984b8a8 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -24,7 +24,7 @@ .. _Zipkin: https://zipkin.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ -.. _Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/sdk-environment-variables.md#zipkin-exporter +.. _Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#zipkin-exporter .. envvar:: OTEL_EXPORTER_ZIPKIN_ENDPOINT .. envvar:: OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 7deb389f18..20efac052d 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-api +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-api platforms = any license = Apache-2.0 classifiers = diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 227a02bc35..325d055023 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -25,7 +25,7 @@ See the `metrics api`_ spec for terminology and context clarification. .. _metrics api: - https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/metrics/api.md + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md """ import abc from logging import getLogger diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index fb2863ac8a..5c42075e3c 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -65,7 +65,7 @@ def example_route(): .. _Propagation API Specification: - https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/context/api-propagators.md + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md """ import typing @@ -86,7 +86,7 @@ def extract( carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: - """ Uses the configured propagator to extract a Context from the carrier. + """Uses the configured propagator to extract a Context from the carrier. Args: getter: an object which contains a get function that can retrieve zero @@ -107,7 +107,7 @@ def inject( carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> None: - """ Uses the configured propagator to inject a Context into the carrier. + """Uses the configured propagator to inject a Context into the carrier. Args: set_in_carrier: A setter function that can set values diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index ed6cb82545..d2a40b0e04 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-distro +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-distro platforms = any license = Apache-2.0 classifiers = diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 825b1ad2ad..0ebc59d2db 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-instrumentation +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-instrumentation platforms = any license = Apache-2.0 classifiers = diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py index 107d5a49fd..4f8972ac82 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py @@ -75,7 +75,7 @@ def __init__( ) # Conventions for recording duration can be found at: - # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/metrics/semantic_conventions/http-metrics.md + # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md @contextmanager def record_client_duration(self, labels: Dict[str, str]): start_time = time() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py index f0ebc042b1..49386f1ddd 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -40,7 +40,7 @@ def http_status_to_status_code( Args: status (int): HTTP status code """ - # See: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#status + # See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status if status < 100: return StatusCode.ERROR if status <= 299: diff --git a/opentelemetry-instrumentation/tests/test_utils.py b/opentelemetry-instrumentation/tests/test_utils.py index edd2320473..bcbf6eced0 100644 --- a/opentelemetry-instrumentation/tests/test_utils.py +++ b/opentelemetry-instrumentation/tests/test_utils.py @@ -20,7 +20,7 @@ class TestUtils(TestBase): - # See https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#status + # See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status def test_http_status_to_status_code(self): for status_code, expected in ( (HTTPStatus.OK, StatusCode.UNSET), diff --git a/opentelemetry-proto/README.rst b/opentelemetry-proto/README.rst index 6825aa8823..d8cff6e68c 100644 --- a/opentelemetry-proto/README.rst +++ b/opentelemetry-proto/README.rst @@ -32,4 +32,4 @@ References * `OpenTelemetry Project `_ * `OpenTelemetry Proto `_ -* `proto_codegen.sh script `_ +* `proto_codegen.sh script `_ diff --git a/opentelemetry-proto/setup.cfg b/opentelemetry-proto/setup.cfg index 6b924a7a39..9934a7c5ec 100644 --- a/opentelemetry-proto/setup.cfg +++ b/opentelemetry-proto/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-proto +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-proto platforms = any license = Apache-2.0 classifiers = diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 4108b3efa8..719911ffcb 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-sdk +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk platforms = any license = Apache-2.0 classifiers = diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 2b832cff7d..74d57439f3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -14,7 +14,7 @@ """ This package implements `OpenTelemetry Resources -`_: +`_: *A Resource is an immutable representation of the entity producing telemetry. For example, a process producing telemetry that is running in @@ -49,7 +49,7 @@ 'service.instance.id': 'instance-12'} Note that the OpenTelemetry project documents certain `"standard attributes" -`_ +`_ that have prescribed semantic meanings, for example ``service.name`` in the above example. @@ -61,7 +61,7 @@ `Resource.create`, meaning :envvar:`OTEL_RESOURCE_ATTRIBUTES` takes *lower* priority. Attributes should be in the format ``key1=value1,key2=value2``. Additional details are available `in the specification -`_. +`_. .. code-block:: console @@ -155,8 +155,7 @@ class Resource: - """A Resource is an immutable representation of the entity producing telemetry as Attributes. - """ + """A Resource is an immutable representation of the entity producing telemetry as Attributes.""" def __init__(self, attributes: Attributes): self._attributes = attributes.copy() @@ -260,7 +259,7 @@ def get_aggregated_resources( initial_resource: typing.Optional[Resource] = None, timeout=5, ) -> "Resource": - """ Retrieves resources from detectors in the order that they were passed + """Retrieves resources from detectors in the order that they were passed :param detectors: List of resources in order of priority :param initial_resource: Static resource. This has highest priority diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/ids_generator.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/ids_generator.py index 31e1fee078..4e2a81b731 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/ids_generator.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/ids_generator.py @@ -33,7 +33,7 @@ def generate_trace_id(self) -> int: uniformly random. Samplers like the `TraceIdRatioBased` sampler rely on this randomness to make sampling decisions. - See `the specification on TraceIdRatioBased `_. + See `the specification on TraceIdRatioBased `_. Returns: A 128-bit int for use as a trace ID diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index b6475c4d34..bb17423354 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -13,7 +13,7 @@ # limitations under the License. """ -For general information about sampling, see `the specification `_. +For general information about sampling, see `the specification `_. OpenTelemetry provides two types of samplers: diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 9da95c16a9..903117996c 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/propagator/opentelemetry-propagator-b3 +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/propagator/opentelemetry-propagator-b3 platforms = any license = Apache-2.0 classifiers = diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index 255813b579..c94e3e8b49 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/propagator/opentelemetry-propagator-jaeger +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/propagator/opentelemetry-propagator-jaeger platforms = any license = Apache-2.0 classifiers = diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh index e75278eb5e..09b934420d 100755 --- a/scripts/prepare_release.sh +++ b/scripts/prepare_release.sh @@ -22,9 +22,9 @@ if [[ ! "${VERSION}" =~ ^([0-9])(\.*[0-9]{1,5}[a-b]*){1,3}$ ]]; then fi # create the release branch -git fetch origin master -git checkout master -git reset --hard origin/master +git fetch origin main +git checkout main +git reset --hard origin/main git checkout -b release/${VERSION} git push origin release/${VERSION} diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 8a7b0cfcce..667a9e5a4c 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-opentracing-shim +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/instrumentation/opentelemetry-instrumentation-opentracing-shim platforms = any license = Apache-2.0 classifiers = From 05ab4a70e17958d3d75c6c35253216d5afb42085 Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Sat, 30 Jan 2021 00:21:08 +0100 Subject: [PATCH 0761/1517] Cleanup references to semantic convetion 'component' attribute (#1555) --- docs/examples/auto-instrumentation/README.rst | 2 -- docs/examples/django/README.rst | 1 - opentelemetry-sdk/tests/trace/test_trace.py | 6 ++---- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index f85106b66d..0c8a0e929d 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -124,7 +124,6 @@ similar to the following example: "status_code": "OK" }, "attributes": { - "component": "http", "http.method": "GET", "http.server_name": "127.0.0.1", "http.scheme": "http", @@ -181,7 +180,6 @@ similar to the following example: "status_code": "OK" }, "attributes": { - "component": "http", "http.method": "GET", "http.server_name": "127.0.0.1", "http.scheme": "http", diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 0e5606c6da..4398372102 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -80,7 +80,6 @@ output similar to this one: "status_code": "OK" }, "attributes": { - "component": "http", "http.method": "GET", "http.server_name": "localhost", "http.scheme": "http", diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index d17bd2ef21..b507bc5439 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -549,7 +549,6 @@ def test_attributes(self): with self.tracer.start_as_current_span("root") as root: root.set_attributes( { - "component": "http", "http.method": "GET", "http.url": "https://example.com:779/path/12/?q=d#123", } @@ -570,8 +569,7 @@ def test_attributes(self): list_of_numerics = [123, 314, 0] root.set_attribute("list-of-numerics", list_of_numerics) - self.assertEqual(len(root.attributes), 10) - self.assertEqual(root.attributes["component"], "http") + self.assertEqual(len(root.attributes), 9) self.assertEqual(root.attributes["http.method"], "GET") self.assertEqual( root.attributes["http.url"], @@ -900,7 +898,7 @@ def test_ended_span(self): self.assertEqual(end_time0, root.end_time) with self.assertLogs(level=WARNING): - root.set_attribute("component", "http") + root.set_attribute("http.method", "GET") self.assertEqual(len(root.attributes), 0) with self.assertLogs(level=WARNING): From 731f32c4ace615d743b424860741334d76369780 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Sat, 30 Jan 2021 17:19:38 -0600 Subject: [PATCH 0762/1517] Remove `Configuration` (#1523) --- .github/workflows/test.yml | 4 +- CHANGELOG.md | 4 + docs/api/api.rst | 2 +- docs/api/configuration.rst | 10 - docs/api/environment_variables.rst | 7 + .../server_instrumented.py | 2 +- docs/sdk/environment_variables.rst | 12 ++ docs/sdk/sdk.rst | 1 + .../opentelemetry/exporter/jaeger/__init__.py | 45 ++-- .../src/opentelemetry/exporter/jaeger/util.py | 10 +- ...uf.py => test_jaeger_exporter_protobuf.py} | 15 +- .../tests/test_jaeger_exporter_thrift.py | 23 +- .../opentelemetry/exporter/otlp/exporter.py | 23 +- .../otlp/metrics_exporter/__init__.py | 27 ++- .../exporter/otlp/trace_exporter/__init__.py | 27 ++- .../tests/test_otlp_metric_exporter.py | 21 +- .../tests/test_otlp_trace_exporter.py | 20 +- .../opentelemetry/exporter/zipkin/__init__.py | 12 +- .../tests/test_zipkin_exporter.py | 18 +- .../opentelemetry/configuration/__init__.py | 198 ------------------ .../src/opentelemetry/configuration/py.typed | 0 .../src/opentelemetry/context/__init__.py | 3 +- .../environment_variables/__init__.py | 21 ++ .../src/opentelemetry/propagators/__init__.py | 13 +- .../src/opentelemetry/util/__init__.py | 9 +- .../tests/configuration/__init__.py | 0 .../tests/configuration/test_configuration.py | 177 ---------------- .../tests/configuration/test_exclude_list.py | 60 ------ .../tests/propagators/test_propagators.py | 7 +- .../src/opentelemetry/distro/__init__.py | 61 ++++-- .../tests/test_configurator.py | 16 +- opentelemetry-distro/tests/test_distro.py | 10 +- opentelemetry-instrumentation/README.rst | 11 +- .../auto_instrumentation/__init__.py | 39 +++- .../auto_instrumentation/sitecustomize.py | 10 +- .../instrumentation/bootstrap.py | 4 - .../instrumentation/instrumentor.py | 12 +- .../tests/test_run.py | 19 +- .../sdk/environment_variables/__init__.py | 58 +++++ .../opentelemetry/sdk/resources/__init__.py | 3 +- .../src/opentelemetry/sdk/trace/__init__.py | 16 +- .../sdk/trace/export/__init__.py | 26 ++- .../src/opentelemetry/sdk/trace/sampling.py | 8 +- opentelemetry-sdk/tests/conftest.py | 6 +- .../tests/trace/export/test_export.py | 20 +- opentelemetry-sdk/tests/trace/test_trace.py | 30 ++- scripts/coverage.sh | 2 +- tests/w3c_tracecontext_validation_server.py | 2 +- tox.ini | 4 +- 49 files changed, 447 insertions(+), 681 deletions(-) delete mode 100644 docs/api/configuration.rst create mode 100644 docs/api/environment_variables.rst create mode 100644 docs/sdk/environment_variables.rst rename exporter/opentelemetry-exporter-jaeger/tests/{test_jarget_exporter_protobuf.py => test_jaeger_exporter_protobuf.py} (97%) delete mode 100644 opentelemetry-api/src/opentelemetry/configuration/__init__.py delete mode 100644 opentelemetry-api/src/opentelemetry/configuration/py.typed create mode 100644 opentelemetry-api/src/opentelemetry/environment_variables/__init__.py delete mode 100644 opentelemetry-api/tests/configuration/__init__.py delete mode 100644 opentelemetry-api/tests/configuration/test_configuration.py delete mode 100644 opentelemetry-api/tests/configuration/test_exclude_list.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a276d60601..fa9272e9f4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: a67a23d0a0a4dd7c3c06c7050c220fa3b3689a77 + CONTRIB_REPO_SHA: f005d90ed3bc75ee6eb7297f9e3a6b55a55b22aa jobs: build: @@ -109,7 +109,7 @@ jobs: run: pip install -U tox - name: Cache tox environment # Preserves .tox directory between runs for faster installs - uses: actions/cache@v2 + uses: actions/cache@v1 with: path: .tox key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core diff --git a/CHANGELOG.md b/CHANGELOG.md index a83dd049e9..3f05dd7e83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow missing carrier headers to continue without raising AttributeError ([#1545](https://github.com/open-telemetry/opentelemetry-python/pull/1545)) +### Removed +- Remove Configuration + ([#1523](https://github.com/open-telemetry/opentelemetry-python/pull/1523)) + ## [0.17b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.17b0) - 2021-01-20 ### Added diff --git a/docs/api/api.rst b/docs/api/api.rst index ec6d8b03aa..d132b78cf8 100644 --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -7,7 +7,7 @@ OpenTelemetry Python API :maxdepth: 1 baggage - configuration context metrics trace + environment_variables diff --git a/docs/api/configuration.rst b/docs/api/configuration.rst deleted file mode 100644 index 06ae433277..0000000000 --- a/docs/api/configuration.rst +++ /dev/null @@ -1,10 +0,0 @@ -opentelemetry.configuration module -================================== - -Module contents ---------------- - -.. automodule:: opentelemetry.configuration - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/environment_variables.rst b/docs/api/environment_variables.rst new file mode 100644 index 0000000000..284675cf08 --- /dev/null +++ b/docs/api/environment_variables.rst @@ -0,0 +1,7 @@ +opentelemetry.environment_variables package +=========================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.environment_variables diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_instrumented.py index 6212ec3333..bc331de3ca 100644 --- a/docs/examples/auto-instrumentation/server_instrumented.py +++ b/docs/examples/auto-instrumentation/server_instrumented.py @@ -15,13 +15,13 @@ from flask import Flask, request from opentelemetry import propagators, trace -from opentelemetry.instrumentation.wsgi import collect_request_attributes from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleExportSpanProcessor, ) from opentelemetry.trace.propagation.textmap import DictGetter +from opentelemetry.util.http.wsgi import collect_request_attributes app = Flask(__name__) diff --git a/docs/sdk/environment_variables.rst b/docs/sdk/environment_variables.rst new file mode 100644 index 0000000000..084a34b7be --- /dev/null +++ b/docs/sdk/environment_variables.rst @@ -0,0 +1,12 @@ +opentelemetry.sdk.environment_variables +======================================= + +.. TODO: what is the SDK + +.. toctree:: + :maxdepth: 1 + +.. automodule:: opentelemetry.sdk.environment_variables + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sdk/sdk.rst b/docs/sdk/sdk.rst index e777aebac6..1c5653e1b3 100644 --- a/docs/sdk/sdk.rst +++ b/docs/sdk/sdk.rst @@ -10,3 +10,4 @@ OpenTelemetry Python SDK resources trace error_handler + environment_variables diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index 297c1e2b26..20b4bda3d9 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -65,24 +65,14 @@ """ # pylint: disable=protected-access -import base64 import logging -import socket -from typing import Optional, Union - -from grpc import ( - ChannelCredentials, - insecure_channel, - secure_channel, - ssl_channel_credentials, -) -from thrift.protocol import TBinaryProtocol, TCompactProtocol -from thrift.transport import THttpClient, TTransport +from os import environ +from typing import Optional + +from grpc import ChannelCredentials, insecure_channel, secure_channel -from opentelemetry.configuration import Configuration from opentelemetry.exporter.jaeger import util from opentelemetry.exporter.jaeger.gen import model_pb2 -from opentelemetry.exporter.jaeger.gen.agent import Agent as agent from opentelemetry.exporter.jaeger.gen.collector_pb2 import PostSpansRequest from opentelemetry.exporter.jaeger.gen.collector_pb2_grpc import ( CollectorServiceStub, @@ -92,9 +82,14 @@ from opentelemetry.exporter.jaeger.translate import Translate from opentelemetry.exporter.jaeger.translate.protobuf import ProtobufTranslator from opentelemetry.exporter.jaeger.translate.thrift import ThriftTranslator -from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult -from opentelemetry.trace import SpanKind -from opentelemetry.trace.status import StatusCode +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_JAEGER_AGENT_HOST, + OTEL_EXPORTER_JAEGER_AGENT_PORT, + OTEL_EXPORTER_JAEGER_ENDPOINT, + OTEL_EXPORTER_JAEGER_PASSWORD, + OTEL_EXPORTER_JAEGER_USER, +) +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult DEFAULT_AGENT_HOST_NAME = "localhost" DEFAULT_AGENT_PORT = 6831 @@ -142,12 +137,18 @@ def __init__( self.service_name = service_name self.agent_host_name = _parameter_setter( param=agent_host_name, - env_variable=Configuration().EXPORTER_JAEGER_AGENT_HOST, + env_variable=environ.get(OTEL_EXPORTER_JAEGER_AGENT_HOST), default=DEFAULT_AGENT_HOST_NAME, ) + + environ_agent_port = environ.get(OTEL_EXPORTER_JAEGER_AGENT_PORT) + environ_agent_port = ( + int(environ_agent_port) if environ_agent_port is not None else None + ) + self.agent_port = _parameter_setter( param=agent_port, - env_variable=Configuration().EXPORTER_JAEGER_AGENT_PORT, + env_variable=environ_agent_port, default=DEFAULT_AGENT_PORT, ) self._agent_client = AgentClientUDP( @@ -155,17 +156,17 @@ def __init__( ) self.collector_endpoint = _parameter_setter( param=collector_endpoint, - env_variable=Configuration().EXPORTER_JAEGER_ENDPOINT, + env_variable=environ.get(OTEL_EXPORTER_JAEGER_ENDPOINT), default=None, ) self.username = _parameter_setter( param=username, - env_variable=Configuration().EXPORTER_JAEGER_USER, + env_variable=environ.get(OTEL_EXPORTER_JAEGER_USER), default=None, ) self.password = _parameter_setter( param=password, - env_variable=Configuration().EXPORTER_JAEGER_PASSWORD, + env_variable=environ.get(OTEL_EXPORTER_JAEGER_PASSWORD), default=None, ) self._collector = None diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py index 6be9d509ac..4a72c1817c 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py @@ -13,10 +13,14 @@ # limitations under the License. import logging +from os import environ from grpc import ChannelCredentials, ssl_channel_credentials -from opentelemetry.configuration import Configuration +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_JAEGER_CERTIFICATE, + OTEL_EXPORTER_JAEGER_INSECURE, +) logger = logging.getLogger(__name__) @@ -26,7 +30,7 @@ def _get_insecure(param): if param is not None: return param - insecure_env = Configuration().get("EXPORTER_JAEGER_INSECURE", None) + insecure_env = environ.get(OTEL_EXPORTER_JAEGER_INSECURE) if insecure_env is not None: return insecure_env.lower() == "true" return DEFAULT_INSECURE @@ -45,7 +49,7 @@ def _load_credential_from_file(path) -> ChannelCredentials: def _get_credentials(param): if param is not None: return param - creds_env = Configuration().get("EXPORTER_JAEGER_CERTIFICATE", None) + creds_env = environ.get(OTEL_EXPORTER_JAEGER_CERTIFICATE) if creds_env: return _load_credential_from_file(creds_env) return ssl_channel_credentials() diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py similarity index 97% rename from exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py rename to exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py index 2af228a6b7..26cfc41498 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jarget_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py @@ -22,7 +22,6 @@ import opentelemetry.exporter.jaeger.gen.model_pb2 as model_pb2 import opentelemetry.exporter.jaeger.translate.protobuf as pb_translator from opentelemetry import trace as trace_api -from opentelemetry.configuration import Configuration from opentelemetry.exporter.jaeger import JaegerSpanExporter from opentelemetry.exporter.jaeger.translate import ( NAME_KEY, @@ -30,6 +29,10 @@ Translate, ) from opentelemetry.sdk import trace +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_JAEGER_CERTIFICATE, + OTEL_EXPORTER_JAEGER_ENDPOINT, +) from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace.status import Status, StatusCode @@ -49,16 +52,10 @@ def setUp(self): self._test_span.start() self._test_span.end() # pylint: disable=protected-access - Configuration._reset() - - def tearDown(self): - # pylint: disable=protected-access - Configuration._reset() def test_constructor_by_environment_variables(self): """Test using Environment Variables.""" # pylint: disable=protected-access - Configuration._reset() service = "my-opentelemetry-jaeger" collector_endpoint = "localhost:14250" @@ -66,8 +63,8 @@ def test_constructor_by_environment_variables(self): env_patch = patch.dict( "os.environ", { - "OTEL_EXPORTER_JAEGER_ENDPOINT": collector_endpoint, - "OTEL_EXPORTER_JAEGER_CERTIFICATE": os.path.dirname(__file__) + OTEL_EXPORTER_JAEGER_ENDPOINT: collector_endpoint, + OTEL_EXPORTER_JAEGER_CERTIFICATE: os.path.dirname(__file__) + "/certs/cred.cert", }, ) diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py index 379dd9a1e7..947597ab46 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py @@ -20,11 +20,17 @@ # pylint:disable=import-error import opentelemetry.exporter.jaeger as jaeger_exporter from opentelemetry import trace as trace_api -from opentelemetry.configuration import Configuration from opentelemetry.exporter.jaeger.gen.jaeger import ttypes as jaeger from opentelemetry.exporter.jaeger.translate import Translate from opentelemetry.exporter.jaeger.translate.thrift import ThriftTranslator from opentelemetry.sdk import trace +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_JAEGER_AGENT_HOST, + OTEL_EXPORTER_JAEGER_AGENT_PORT, + OTEL_EXPORTER_JAEGER_ENDPOINT, + OTEL_EXPORTER_JAEGER_PASSWORD, + OTEL_EXPORTER_JAEGER_USER, +) from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanKind @@ -44,11 +50,6 @@ def setUp(self): self._test_span.start() self._test_span.end() # pylint: disable=protected-access - Configuration._reset() - - def tearDown(self): - # pylint: disable=protected-access - Configuration._reset() def test_constructor_default(self): # pylint: disable=protected-access @@ -121,11 +122,11 @@ def test_constructor_by_environment_variables(self): environ_patcher = mock.patch.dict( "os.environ", { - "OTEL_EXPORTER_JAEGER_AGENT_HOST": agent_host_name, - "OTEL_EXPORTER_JAEGER_AGENT_PORT": agent_port, - "OTEL_EXPORTER_JAEGER_ENDPOINT": collector_endpoint, - "OTEL_EXPORTER_JAEGER_USER": username, - "OTEL_EXPORTER_JAEGER_PASSWORD": password, + OTEL_EXPORTER_JAEGER_AGENT_HOST: agent_host_name, + OTEL_EXPORTER_JAEGER_AGENT_PORT: agent_port, + OTEL_EXPORTER_JAEGER_ENDPOINT: collector_endpoint, + OTEL_EXPORTER_JAEGER_USER: username, + OTEL_EXPORTER_JAEGER_PASSWORD: password, }, ) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index a4ae1edcfb..6d54970f19 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -18,6 +18,7 @@ import logging from abc import ABC, abstractmethod from collections.abc import Mapping, Sequence +from os import environ from time import sleep from typing import Any, Callable, Dict, Generic, List, Optional from typing import Sequence as TypingSequence @@ -35,9 +36,15 @@ ssl_channel_credentials, ) -from opentelemetry.configuration import Configuration from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue from opentelemetry.proto.resource.v1.resource_pb2 import Resource +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_INSECURE, + OTEL_EXPORTER_OTLP_TIMEOUT, +) from opentelemetry.sdk.resources import Resource as SDKResource logger = logging.getLogger(__name__) @@ -159,23 +166,23 @@ def __init__( endpoint = ( endpoint - or Configuration().EXPORTER_OTLP_ENDPOINT + or environ.get(OTEL_EXPORTER_OTLP_ENDPOINT) or "localhost:4317" ) if insecure is None: - insecure = Configuration().EXPORTER_OTLP_INSECURE + insecure = environ.get(OTEL_EXPORTER_OTLP_INSECURE) if insecure is None: insecure = False - self._headers = headers or Configuration().EXPORTER_OTLP_HEADERS + self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS) if isinstance(self._headers, str): self._headers = tuple( tuple(item.split("=")) for item in self._headers.split(",") ) self._timeout = ( timeout - or Configuration().EXPORTER_OTLP_TIMEOUT + or int(environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 0)) or 10 # default: 10 seconds ) self._collector_span_kwargs = None @@ -188,7 +195,7 @@ def __init__( ): compression_algorithm = Compression.Gzip else: - compression_str = Configuration().EXPORTER_OTLP_INSECURE or None + compression_str = environ.get(OTEL_EXPORTER_OTLP_INSECURE) if compression_str is None: compression_algorithm = Compression.NoCompression elif ( @@ -210,13 +217,13 @@ def __init__( # secure mode if ( credentials is None - and Configuration().EXPORTER_OTLP_CERTIFICATE is None + and environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE) is None ): # use the default location chosen by gRPC runtime credentials = ssl_channel_credentials() else: credentials = credentials or _load_credential_from_file( - Configuration().EXPORTER_OTLP_CERTIFICATE + environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE) ) self._client = self._stub( secure_channel( diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py index c371c177e9..8e388e9f9e 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -15,11 +15,11 @@ """OTLP Metrics Exporter""" import logging +from os import environ from typing import List, Optional, Sequence, Type, TypeVar from grpc import ChannelCredentials -from opentelemetry.configuration import Configuration from opentelemetry.exporter.otlp.exporter import ( OTLPExporterMixin, _get_resource_data, @@ -44,6 +44,13 @@ ) from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric as OTLPMetric from opentelemetry.proto.metrics.v1.metrics_pb2 import ResourceMetrics +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRIC_ENDPOINT, + OTEL_EXPORTER_OTLP_METRIC_HEADERS, + OTEL_EXPORTER_OTLP_METRIC_INSECURE, + OTEL_EXPORTER_OTLP_METRIC_TIMEOUT, +) from opentelemetry.sdk.metrics import ( Counter, SumObserver, @@ -143,26 +150,30 @@ def __init__( timeout: Optional[int] = None, ): if insecure is None: - insecure = Configuration().EXPORTER_OTLP_METRIC_INSECURE + insecure = environ.get(OTEL_EXPORTER_OTLP_METRIC_INSECURE) if ( not insecure - and Configuration().EXPORTER_OTLP_METRIC_CERTIFICATE is not None + and environ.get(OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE) is not None ): credentials = credentials or _load_credential_from_file( - Configuration().EXPORTER_OTLP_METRIC_CERTIFICATE + environ.get(OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE) ) + environ_timeout = environ.get(OTEL_EXPORTER_OTLP_METRIC_TIMEOUT) + environ_timeout = ( + int(environ_timeout) if environ_timeout is not None else None + ) + super().__init__( **{ "endpoint": endpoint - or Configuration().EXPORTER_OTLP_METRIC_ENDPOINT, + or environ.get(OTEL_EXPORTER_OTLP_METRIC_ENDPOINT), "insecure": insecure, "credentials": credentials, "headers": headers - or Configuration().EXPORTER_OTLP_METRIC_HEADERS, - "timeout": timeout - or Configuration().EXPORTER_OTLP_METRIC_TIMEOUT, + or environ.get(OTEL_EXPORTER_OTLP_METRIC_HEADERS), + "timeout": timeout or environ_timeout, } ) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index b872b624a1..96dbbc084b 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -14,11 +14,11 @@ """OTLP Span Exporter""" import logging +from os import environ from typing import Optional, Sequence from grpc import ChannelCredentials -from opentelemetry.configuration import Configuration from opentelemetry.exporter.otlp.exporter import ( OTLPExporterMixin, _get_resource_data, @@ -38,6 +38,13 @@ ) from opentelemetry.proto.trace.v1.trace_pb2 import Span as CollectorSpan from opentelemetry.proto.trace.v1.trace_pb2 import Status +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE, + OTEL_EXPORTER_OTLP_SPAN_ENDPOINT, + OTEL_EXPORTER_OTLP_SPAN_HEADERS, + OTEL_EXPORTER_OTLP_SPAN_INSECURE, + OTEL_EXPORTER_OTLP_SPAN_TIMEOUT, +) from opentelemetry.sdk.trace import Span as SDKSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace.status import StatusCode @@ -73,26 +80,30 @@ def __init__( timeout: Optional[int] = None, ): if insecure is None: - insecure = Configuration().EXPORTER_OTLP_SPAN_INSECURE + insecure = environ.get(OTEL_EXPORTER_OTLP_SPAN_INSECURE) if ( not insecure - and Configuration().EXPORTER_OTLP_SPAN_CERTIFICATE is not None + and environ.get(OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE) is not None ): credentials = credentials or _load_credential_from_file( - Configuration().EXPORTER_OTLP_SPAN_CERTIFICATE + environ.get(OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE) ) + environ_timeout = environ.get(OTEL_EXPORTER_OTLP_SPAN_TIMEOUT) + environ_timeout = ( + int(environ_timeout) if environ_timeout is not None else None + ) + super().__init__( **{ "endpoint": endpoint - or Configuration().EXPORTER_OTLP_SPAN_ENDPOINT, + or environ.get(OTEL_EXPORTER_OTLP_SPAN_ENDPOINT), "insecure": insecure, "credentials": credentials, "headers": headers - or Configuration().EXPORTER_OTLP_SPAN_HEADERS, - "timeout": timeout - or Configuration().EXPORTER_OTLP_SPAN_TIMEOUT, + or environ.get(OTEL_EXPORTER_OTLP_SPAN_HEADERS), + "timeout": timeout or environ_timeout, } ) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py index 946a313a44..841dcff03e 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -19,7 +19,6 @@ from grpc import ChannelCredentials -from opentelemetry.configuration import Configuration from opentelemetry.exporter.otlp.metrics_exporter import OTLPMetricsExporter from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, @@ -41,6 +40,12 @@ from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as OTLPResource, ) +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRIC_ENDPOINT, + OTEL_EXPORTER_OTLP_METRIC_HEADERS, + OTEL_EXPORTER_OTLP_METRIC_TIMEOUT, +) from opentelemetry.sdk.metrics import ( Counter, MeterProvider, @@ -62,19 +67,15 @@ def setUp(self): # pylint: disable=arguments-differ self.meter = MeterProvider(resource=self.resource,).get_meter( "name", "version" ) - Configuration._reset() # pylint: disable=protected-access - - def tearDown(self): - Configuration._reset() # pylint: disable=protected-access @patch.dict( "os.environ", { - "OTEL_EXPORTER_OTLP_METRIC_ENDPOINT": "collector:4317", - "OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE": THIS_DIR + OTEL_EXPORTER_OTLP_METRIC_ENDPOINT: "collector:4317", + OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE: THIS_DIR + "/fixtures/test.cert", - "OTEL_EXPORTER_OTLP_METRIC_HEADERS": "key1=value1,key2=value2", - "OTEL_EXPORTER_OTLP_METRIC_TIMEOUT": "10", + OTEL_EXPORTER_OTLP_METRIC_HEADERS: "key1=value1,key2=value2", + OTEL_EXPORTER_OTLP_METRIC_TIMEOUT: "10", }, ) @patch("opentelemetry.exporter.otlp.exporter.OTLPExporterMixin.__init__") @@ -104,7 +105,7 @@ def test_no_credentials_error( @patch.dict( "os.environ", - {"OTEL_EXPORTER_OTLP_METRIC_HEADERS": "key1=value1,key2=value2"}, + {OTEL_EXPORTER_OTLP_METRIC_HEADERS: "key1=value1,key2=value2"}, ) @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") @patch("opentelemetry.exporter.otlp.exporter.secure_channel") diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index 5dde0c9772..a1dbcfbd63 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -22,7 +22,6 @@ from google.rpc.error_details_pb2 import RetryInfo from grpc import ChannelCredentials, StatusCode, server -from opentelemetry.configuration import Configuration from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest, @@ -46,6 +45,12 @@ ) from opentelemetry.proto.trace.v1.trace_pb2 import Span as OTLPSpan from opentelemetry.proto.trace.v1.trace_pb2 import Status +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE, + OTEL_EXPORTER_OTLP_SPAN_ENDPOINT, + OTEL_EXPORTER_OTLP_SPAN_HEADERS, + OTEL_EXPORTER_OTLP_SPAN_TIMEOUT, +) from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.trace import Status as SDKStatus from opentelemetry.sdk.trace import StatusCode as SDKStatusCode @@ -160,20 +165,17 @@ def setUp(self): self.span.start() self.span.end() - Configuration._reset() # pylint: disable=protected-access - def tearDown(self): self.server.stop(None) - Configuration._reset() # pylint: disable=protected-access @patch.dict( "os.environ", { - "OTEL_EXPORTER_OTLP_SPAN_ENDPOINT": "collector:4317", - "OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE": THIS_DIR + OTEL_EXPORTER_OTLP_SPAN_ENDPOINT: "collector:4317", + OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE: THIS_DIR + "/fixtures/test.cert", - "OTEL_EXPORTER_OTLP_SPAN_HEADERS": "key1=value1,key2=value2", - "OTEL_EXPORTER_OTLP_SPAN_TIMEOUT": "10", + OTEL_EXPORTER_OTLP_SPAN_HEADERS: "key1=value1,key2=value2", + OTEL_EXPORTER_OTLP_SPAN_TIMEOUT: "10", }, ) @patch("opentelemetry.exporter.otlp.exporter.OTLPExporterMixin.__init__") @@ -201,7 +203,7 @@ def test_no_credentials_error( @patch.dict( "os.environ", - {"OTEL_EXPORTER_OTLP_SPAN_HEADERS": "key1=value1,key2=value2"}, + {OTEL_EXPORTER_OTLP_SPAN_HEADERS: "key1=value1,key2=value2"}, ) @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") @patch("opentelemetry.exporter.otlp.exporter.secure_channel") diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 3f1984b8a8..a7ea40180a 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -73,13 +73,17 @@ import json import logging +from os import environ from typing import Optional, Sequence, Union from urllib.parse import urlparse import requests -from opentelemetry.configuration import Configuration from opentelemetry.exporter.zipkin.gen import zipkin_pb2 +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_ZIPKIN_ENDPOINT, + OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT, +) from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace import Span, SpanContext, SpanKind from opentelemetry.trace.status import StatusCode @@ -142,7 +146,9 @@ def __init__( ): self.service_name = service_name if url is None: - self.url = Configuration().EXPORTER_ZIPKIN_ENDPOINT or DEFAULT_URL + self.url = ( + environ.get(OTEL_EXPORTER_ZIPKIN_ENDPOINT) or DEFAULT_URL + ) else: self.url = url @@ -155,7 +161,7 @@ def __init__( if transport_format is None: self.transport_format = ( - Configuration().EXPORTER_ZIPKIN_TRANSPORT_FORMAT + environ.get(OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT) or TRANSPORT_FORMAT_JSON ) else: diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 30837106c1..5c2e0e7e4d 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -18,7 +18,6 @@ from unittest.mock import MagicMock, patch from opentelemetry import trace as trace_api -from opentelemetry.configuration import Configuration from opentelemetry.exporter.zipkin import ( NAME_KEY, SPAN_KIND_MAP_JSON, @@ -31,6 +30,10 @@ ) from opentelemetry.exporter.zipkin.gen import zipkin_pb2 from opentelemetry.sdk import trace +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_ZIPKIN_ENDPOINT, + OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT, +) from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -58,18 +61,17 @@ def setUp(self): self._test_span.end() def tearDown(self): - if "OTEL_EXPORTER_ZIPKIN_ENDPOINT" in os.environ: - del os.environ["OTEL_EXPORTER_ZIPKIN_ENDPOINT"] - if "OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT" in os.environ: - del os.environ["OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT"] - Configuration()._reset() # pylint: disable=protected-access + if OTEL_EXPORTER_ZIPKIN_ENDPOINT in os.environ: + del os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] + if OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT in os.environ: + del os.environ[OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT] def test_constructor_env_var(self): """Test the default values assigned by constructor.""" url = "https://foo:9911/path" - os.environ["OTEL_EXPORTER_ZIPKIN_ENDPOINT"] = url + os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = url os.environ[ - "OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT" + OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT ] = TRANSPORT_FORMAT_PROTOBUF service_name = "my-service-name" port = 9911 diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py deleted file mode 100644 index 5e3d3667dc..0000000000 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Simple configuration manager - -This is a configuration manager for OpenTelemetry. It reads configuration -values from environment variables prefixed with ``OTEL_`` (for environment -variables that apply to any OpenTelemetry implementation) or with -``OTEL_PYTHON_`` (for environment variables that are specific to the Python -implementation of OpenTelemetry) whose characters are only alphanumeric -characters and unserscores, except for the first character after ``OTEL_`` or -``OTEL_PYTHON_`` which must not be a number. - -For example, these environment variables will be read: - -1. ``OTEL_SOMETHING`` -2. ``OTEL_SOMETHING_ELSE_`` -3. ``OTEL_SOMETHING_ELSE_AND__ELSE`` -4. ``OTEL_SOMETHING_ELSE_AND_else`` -5. ``OTEL_SOMETHING_ELSE_AND_else2`` - -These won't: - -1. ``OPENTELEMETRY_PYTH_SOMETHING`` -2. ``OTEL_2_SOMETHING_AND__ELSE`` -3. ``OTEL_SOMETHING_%_ELSE`` - -The values stored in the environment variables can be found in an instance of -``opentelemetry.configuration.Configuration``. This class can be instantiated -freely because instantiating it returns always the same object. - -For example, if the environment variable -``OTEL_PYTHON_METER_PROVIDER`` value is ``my_meter_provider``, then -``Configuration().METER_PROVIDER == "my_meter_provider"`` would be ``True``. - -Non defined attributes will always return ``None``. This is intended to make it -easier to use the ``Configuration`` object in actual code, because it won't be -necessary to check for the attribute to be defined first. - -Environment variables used by OpenTelemetry -------------------------------------------- - -1. OTEL_PYTHON_METER_PROVIDER -2. OTEL_PYTHON_TRACER_PROVIDER - -The value of these environment variables should be the name of the entry point -that points to the class that implements either provider. This OpenTelemetry -API package provides one entry point for each, which can be found in the -setup.py file:: - - entry_points={ - ... - "opentelemetry_meter_provider": [ - "default_meter_provider = " - "opentelemetry.metrics:DefaultMeterProvider" - ], - "opentelemetry_tracer_provider": [ - "default_tracer_provider = " - "opentelemetry.trace:DefaultTracerProvider" - ], - } - -To use the meter provider above, then the -``OTEL_PYTHON_METER_PROVIDER`` should be set to -``"default_meter_provider"`` (this is not actually necessary since the -OpenTelemetry API provided providers are the default ones used if no -configuration is found in the environment variables). - -Configuration values that are exactly ``"True"`` or ``"False"`` will be -converted to its boolean values of ``True`` and ``False`` respectively. - -Configuration values that can be casted to integers or floats will be casted. - -This object can be used by any OpenTelemetry component, native or external. -For that reason, the ``Configuration`` object is designed to be immutable. -If a component would change the value of one of the ``Configuration`` object -attributes then another component that relied on that value may break, leading -to bugs that are very hard to debug. To avoid this situation, the preferred -approach for components that need a different value than the one provided by -the ``Configuration`` object is to implement a mechanism that allows the user -to override this value instead of changing it. -""" - -import re -from os import environ -from typing import ClassVar, Dict, List, Optional, Sequence, TypeVar, Union - -ConfigValue = Union[str, bool, int, float] -_T = TypeVar("_T", ConfigValue, Optional[ConfigValue]) - - -class ExcludeList: - """Class to exclude certain paths (given as a list of regexes) from tracing requests""" - - def __init__(self, excluded_urls: Sequence[str]): - self._non_empty = len(excluded_urls) > 0 - if self._non_empty: - self._regex = re.compile("|".join(excluded_urls)) - - def url_disabled(self, url: str) -> bool: - return bool(self._non_empty and re.search(self._regex, url)) - - -class Configuration: - _instance = None # type: ClassVar[Optional[Configuration]] - _config_map = {} # type: ClassVar[Dict[str, ConfigValue]] - - def __new__(cls) -> "Configuration": - if cls._instance is not None: - instance = cls._instance - else: - - instance = super().__new__(cls) - for key, value_str in environ.items(): - - match = re.fullmatch(r"OTEL_(PYTHON_)?([A-Za-z_][\w_]*)", key) - - if match is not None: - - key = match.group(2) - value = value_str # type: ConfigValue - - if value_str == "True": - value = True - elif value_str == "False": - value = False - else: - try: - value = int(value_str) - except ValueError: - try: - value = float(value_str) - except ValueError: - pass - - instance._config_map[key] = value - - cls._instance = instance - - return instance - - def __getattr__(self, name: str) -> Optional[ConfigValue]: - return self._config_map.get(name) - - def __setattr__(self, name: str, value: ConfigValue) -> None: - if name not in self._config_map.keys(): - self._config_map[name] = value - else: - raise AttributeError(name) - - def get(self, name: str, default: _T) -> _T: - """Use this typed method for dynamic access instead of `getattr` - - :rtype: str or bool or int or float or None - """ - return self._config_map.get(name, default) - - @classmethod - def _reset(cls) -> None: - """ - This method "resets" the global configuration attributes - - It is not intended to be used by production code but by testing code - only. - """ - - if cls._instance: - cls._instance._config_map.clear() # pylint: disable=protected-access - cls._instance = None - - def _traced_request_attrs(self, instrumentation: str) -> List[str]: - """Returns list of traced request attributes for instrumentation.""" - key = "{}_TRACED_REQUEST_ATTRS".format(instrumentation.upper()) - value = self._config_map.get(key, "") - - request_attrs = ( - [attr.strip() for attr in str.split(value, ",")] if value else [] # type: ignore - ) - return request_attrs - - def _excluded_urls(self, instrumentation: str) -> ExcludeList: - key = "{}_EXCLUDED_URLS".format(instrumentation.upper()) - value = self._config_map.get(key, "") - - urls = str.split(value, ",") if value else [] # type: ignore - return ExcludeList(urls) diff --git a/opentelemetry-api/src/opentelemetry/configuration/py.typed b/opentelemetry-api/src/opentelemetry/configuration/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 22441c306b..2f1249d4d0 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -21,6 +21,7 @@ from pkg_resources import iter_entry_points from opentelemetry.context.context import Context, RuntimeContext +from opentelemetry.environment_variables import OTEL_PYTHON_CONTEXT logger = logging.getLogger(__name__) _RUNTIME_CONTEXT = None # type: typing.Optional[RuntimeContext] @@ -50,7 +51,7 @@ def wrapper( default_context = "contextvars_context" configured_context = environ.get( - "OTEL_CONTEXT", default_context + OTEL_PYTHON_CONTEXT, default_context ) # type: str try: _RUNTIME_CONTEXT = next( diff --git a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py new file mode 100644 index 0000000000..1bd5a6f46d --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py @@ -0,0 +1,21 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +OTEL_PROPAGATORS = "OTEL_PROPAGATORS" +OTEL_PYTHON_CONTEXT = "OTEL_PYTHON_CONTEXT" +OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS" +OTEL_PYTHON_IDS_GENERATOR = "OTEL_PYTHON_IDS_GENERATOR" +OTEL_PYTHON_SERVICE_NAME = "OTEL_PYTHON_SERVICE_NAME" +OTEL_TRACE_EXPORTER = "OTEL_TRACE_EXPORTER" +OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER" diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index 5c42075e3c..6050b5f4ff 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -70,11 +70,12 @@ def example_route(): import typing from logging import getLogger +from os import environ from pkg_resources import iter_entry_points -from opentelemetry.configuration import Configuration from opentelemetry.context.context import Context +from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.propagators import composite from opentelemetry.trace.propagation import textmap @@ -125,13 +126,15 @@ def inject( propagators = [] - for propagator in ( # type: ignore - Configuration().get("PROPAGATORS", "tracecontext,baggage").split(",") # type: ignore - ): + # Single use variable here to hack black and make lint pass + environ_propagators = environ.get( + OTEL_PROPAGATORS, "tracecontext,baggage", + ) + for propagator in environ_propagators.split(","): propagators.append( # type: ignore next( # type: ignore - iter_entry_points("opentelemetry_propagator", propagator) # type: ignore + iter_entry_points("opentelemetry_propagator", propagator) ).load()() ) diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index c1c5a77f09..c49ba1b353 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -11,15 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import re + import time from logging import getLogger -from typing import TYPE_CHECKING, Sequence, Union, cast +from os import environ +from typing import TYPE_CHECKING, Union, cast from pkg_resources import iter_entry_points -from opentelemetry.configuration import Configuration - if TYPE_CHECKING: from opentelemetry.metrics import MeterProvider from opentelemetry.trace import TracerProvider @@ -47,7 +46,7 @@ def _load_provider(provider: str) -> Provider: "opentelemetry_{}".format(provider), name=cast( str, - Configuration().get( + environ.get( provider.upper(), "default_{}".format(provider), ), ), diff --git a/opentelemetry-api/tests/configuration/__init__.py b/opentelemetry-api/tests/configuration/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py deleted file mode 100644 index b36d93412b..0000000000 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable-all - -from sys import platform -from unittest import TestCase -from unittest.mock import patch - -from opentelemetry.configuration import Configuration - - -class TestConfiguration(TestCase): - - # These calls reset the attributes of the Configuration class so that each - # test is executed in the same conditions. - def setUp(self) -> None: - Configuration._reset() - - def test_singleton(self) -> None: - self.assertIsInstance(Configuration(), Configuration) - self.assertIs(Configuration(), Configuration()) - - @patch.dict( - "os.environ", # type: ignore - { - "OTEL_PYTHON_METER_PROVIDER": "meter_provider", - "OTEL_PYTHON_TRACER_PROVIDER": "tracer_provider", - "OTEL_OTHER" if platform == "windows" else "OTEL_OThER": "other", - "OTEL_OTHER_7": "other_7", - "OPENTELEMETRY_PTHON_TRACEX_PROVIDER": "tracex_provider", - }, - ) - def test_environment_variables(self) -> None: - self.assertEqual( - Configuration().METER_PROVIDER, "meter_provider" - ) # pylint: disable=no-member - self.assertEqual( - Configuration().TRACER_PROVIDER, "tracer_provider" - ) # pylint: disable=no-member - self.assertEqual( - Configuration().OThER, "other" - ) # pylint: disable=no-member - self.assertEqual( - Configuration().OTHER_7, "other_7" - ) # pylint: disable=no-member - self.assertIsNone(Configuration().TRACEX_PROVIDER) - - @patch.dict( - "os.environ", # type: ignore - {"OTEL_PYTHON_TRACER_PROVIDER": "tracer_provider"}, - ) - def test_property(self) -> None: - with self.assertRaises(AttributeError): - Configuration().TRACER_PROVIDER = "new_tracer_provider" - - def test_set_once(self) -> None: - - Configuration().XYZ = "xyz" - - with self.assertRaises(AttributeError): - Configuration().XYZ = "abc" # pylint: disable=assigning-non-slot - - def test_getattr(self) -> None: - # literal access - self.assertIsNone(Configuration().XYZ) - - # dynamic access - self.assertIsNone(getattr(Configuration(), "XYZ")) - self.assertIsNone(Configuration().get("XYZ", None)) - - def test_reset(self) -> None: - environ_patcher = patch.dict( - "os.environ", {"OTEL_PYTHON_TRACER_PROVIDER": "tracer_provider"}, - ) - - environ_patcher.start() - - self.assertEqual( - Configuration().TRACER_PROVIDER, "tracer_provider" - ) # pylint: disable=no-member - - environ_patcher.stop() - - Configuration._reset() - - self.assertIsNone( - Configuration().TRACER_PROVIDER - ) # pylint: disable=no-member - - @patch.dict( - "os.environ", # type: ignore - {"OTEL_TRUE": "True", "OTEL_FALSE": "False"}, - ) - def test_boolean(self) -> None: - self.assertIsInstance( - Configuration().TRUE, bool - ) # pylint: disable=no-member - self.assertIsInstance( - Configuration().FALSE, bool - ) # pylint: disable=no-member - self.assertTrue(Configuration().TRUE) # pylint: disable=no-member - self.assertFalse(Configuration().FALSE) # pylint: disable=no-member - - @patch.dict( - "os.environ", # type: ignore - { - "OTEL_POSITIVE_INTEGER": "123", - "OTEL_NEGATIVE_INTEGER": "-123", - "OTEL_NON_INTEGER": "-12z3", - }, - ) - def test_integer(self) -> None: - # pylint: disable=no-member - self.assertIsInstance(Configuration().POSITIVE_INTEGER, int) - self.assertEqual(Configuration().POSITIVE_INTEGER, 123) - self.assertIsInstance(Configuration().NEGATIVE_INTEGER, int) - self.assertEqual(Configuration().NEGATIVE_INTEGER, -123) - self.assertEqual(Configuration().NON_INTEGER, "-12z3") - - @patch.dict( - "os.environ", # type: ignore - { - "OTEL_POSITIVE_FLOAT": "123.123", - "OTEL_NEGATIVE_FLOAT": "-123.123", - "OTEL_NON_FLOAT": "-12z3.123", - }, - ) - def test_float(self) -> None: - self.assertEqual( - Configuration().POSITIVE_FLOAT, 123.123 - ) # pylint: disable=no-member - self.assertEqual( - Configuration().NEGATIVE_FLOAT, -123.123 - ) # pylint: disable=no-member - self.assertEqual( - Configuration().NON_FLOAT, "-12z3.123" - ) # pylint: disable=no-member - - @patch.dict( - "os.environ", # type: ignore - { - "OTEL_PYTHON_WEBFRAMEWORK_TRACED_REQUEST_ATTRS": "content_type,keep_alive", - }, - ) - def test_traced_request_attrs(self) -> None: - cfg = Configuration() - request_attrs = cfg._traced_request_attrs("webframework") - self.assertEqual(len(request_attrs), 2) - self.assertIn("content_type", request_attrs) - self.assertIn("keep_alive", request_attrs) - self.assertNotIn("authorization", request_attrs) - - @patch.dict( - "os.environ", # type: ignore - { - "OTEL_PYTHON_WEBFRAMEWORK_EXCLUDED_URLS": "/healthzz,path,/issues/.*/view", - }, - ) - def test_excluded_urls(self) -> None: - cfg = Configuration() - excluded_urls = cfg._excluded_urls("webframework") - self.assertTrue(excluded_urls.url_disabled("/healthzz")) - self.assertTrue(excluded_urls.url_disabled("/path")) - self.assertTrue(excluded_urls.url_disabled("/issues/123/view")) - self.assertFalse(excluded_urls.url_disabled("/issues")) - self.assertFalse(excluded_urls.url_disabled("/hello")) diff --git a/opentelemetry-api/tests/configuration/test_exclude_list.py b/opentelemetry-api/tests/configuration/test_exclude_list.py deleted file mode 100644 index c7baf842ed..0000000000 --- a/opentelemetry-api/tests/configuration/test_exclude_list.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -from opentelemetry.configuration import ExcludeList - - -class TestExcludeList(unittest.TestCase): - def test_basic(self): - regexes = ExcludeList(["path/123", "http://site.com/other_path"]) - self.assertTrue(regexes.url_disabled("http://site.com/path/123")) - self.assertTrue(regexes.url_disabled("http://site.com/path/123/abc")) - self.assertTrue( - regexes.url_disabled("https://site.com/path/123?arg=other") - ) - - self.assertFalse(regexes.url_disabled("https://site.com/path/abc/123")) - self.assertFalse(regexes.url_disabled("https://site.com/path")) - - self.assertTrue(regexes.url_disabled("http://site.com/other_path")) - self.assertTrue(regexes.url_disabled("http://site.com/other_path?abc")) - - self.assertFalse(regexes.url_disabled("https://site.com/other_path")) - self.assertFalse( - regexes.url_disabled("https://site.com/abc/other_path") - ) - - def test_regex(self): - regexes = ExcludeList( - [r"^https?://site\.com/path/123$", r"^http://.*\?arg=foo"] - ) - - self.assertTrue(regexes.url_disabled("http://site.com/path/123")) - self.assertTrue(regexes.url_disabled("https://site.com/path/123")) - - self.assertFalse(regexes.url_disabled("http://site.com/path/123/abc")) - self.assertFalse(regexes.url_disabled("http://site,com/path/123")) - - self.assertTrue( - regexes.url_disabled("http://site.com/path/123?arg=foo") - ) - self.assertTrue( - regexes.url_disabled("http://site.com/path/123?arg=foo,arg2=foo2") - ) - - self.assertFalse( - regexes.url_disabled("https://site.com/path/123?arg=foo") - ) diff --git a/opentelemetry-api/tests/propagators/test_propagators.py b/opentelemetry-api/tests/propagators/test_propagators.py index 70f4999dff..9fceb91a0b 100644 --- a/opentelemetry-api/tests/propagators/test_propagators.py +++ b/opentelemetry-api/tests/propagators/test_propagators.py @@ -18,7 +18,7 @@ from unittest.mock import Mock, patch from opentelemetry.baggage.propagation import BaggagePropagator -from opentelemetry.configuration import Configuration +from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) @@ -44,15 +44,12 @@ def test_propagators(propagators): reload(opentelemetry.propagators) - @patch.dict(environ, {"OTEL_PROPAGATORS": "a,b,c"}) + @patch.dict(environ, {OTEL_PROPAGATORS: "a,b,c"}) @patch("opentelemetry.propagators.composite.CompositeHTTPPropagator") @patch("pkg_resources.iter_entry_points") def test_non_default_propagators( self, mock_iter_entry_points, mock_compositehttppropagator ): - - Configuration._reset() - def iter_entry_points_mock(_, propagator): return iter( [ diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index 8a48d256c7..c4ac8f5e87 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -14,12 +14,18 @@ # import os from logging import getLogger +from os import environ from typing import Sequence, Tuple from pkg_resources import iter_entry_points from opentelemetry import trace -from opentelemetry.configuration import Configuration +from opentelemetry.environment_variables import ( + OTEL_METRICS_EXPORTER, + OTEL_PYTHON_IDS_GENERATOR, + OTEL_PYTHON_SERVICE_NAME, + OTEL_TRACE_EXPORTER, +) from opentelemetry.instrumentation.configurator import BaseConfigurator from opentelemetry.instrumentation.distro import BaseDistro from opentelemetry.sdk.metrics.export import MetricsExporter @@ -43,27 +49,47 @@ def _get_ids_generator() -> str: - return Configuration().IDS_GENERATOR or _DEFAULT_IDS_GENERATOR + return environ.get(OTEL_PYTHON_IDS_GENERATOR, _DEFAULT_IDS_GENERATOR) def _get_service_name() -> str: - return Configuration().SERVICE_NAME or "" + return environ.get(OTEL_PYTHON_SERVICE_NAME, "") def _get_exporter_names() -> Sequence[str]: - exporter = Configuration().EXPORTER or EXPORTER_OTLP - if exporter.lower().strip() == "none": - return [] - - names = [] - for exp in exporter.split(","): - name = exp.strip() - if name == EXPORTER_OTLP: - names.append(EXPORTER_OTLP_SPAN) - names.append(EXPORTER_OTLP_METRIC) - else: - names.append(name) - return names + trace_exporters = environ.get(OTEL_TRACE_EXPORTER) + metrics_exporters = environ.get(OTEL_METRICS_EXPORTER) + + exporters = set() + + if ( + trace_exporters is not None + or trace_exporters.lower().strip() != "none" + ): + exporters.update( + { + trace_exporter.strip() + for trace_exporter in trace_exporters.split(",") + } + ) + + if ( + metrics_exporters is not None + or metrics_exporters.lower().strip() != "none" + ): + exporters.update( + { + metrics_exporter.strip() + for metrics_exporter in metrics_exporters.split(",") + } + ) + + if EXPORTER_OTLP in exporters: + exporters.pop(EXPORTER_OTLP) + exporters.add(EXPORTER_OTLP_SPAN) + exporters.add(EXPORTER_OTLP_METRIC) + + return list(exporters) def _init_tracing( @@ -178,4 +204,5 @@ class OpenTelemetryDistro(BaseDistro): """ def _configure(self, **kwargs): - os.environ.setdefault("OTEL_EXPORTER", "otlp") + os.environ.setdefault(OTEL_TRACE_EXPORTER, "otlp_span") + os.environ.setdefault(OTEL_METRICS_EXPORTER, "otlp_metric") diff --git a/opentelemetry-distro/tests/test_configurator.py b/opentelemetry-distro/tests/test_configurator.py index 90ee7fe931..e1a8cfc8ae 100644 --- a/opentelemetry-distro/tests/test_configurator.py +++ b/opentelemetry-distro/tests/test_configurator.py @@ -17,12 +17,15 @@ from unittest import TestCase from unittest.mock import patch -from opentelemetry.configuration import Configuration from opentelemetry.distro import ( _get_ids_generator, _import_ids_generator, _init_tracing, ) +from opentelemetry.environment_variables import ( + OTEL_PYTHON_IDS_GENERATOR, + OTEL_PYTHON_SERVICE_NAME, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace.ids_generator import ( IdsGenerator, @@ -99,8 +102,7 @@ def tearDown(self): # pylint: disable=protected-access def test_trace_init_default(self): - environ["OTEL_SERVICE_NAME"] = "my-test-service" - Configuration._reset() + environ[OTEL_PYTHON_SERVICE_NAME] = "my-test-service" _init_tracing({"zipkin": Exporter}, RandomIdsGenerator) self.assertEqual(self.set_provider_mock.call_count, 1) @@ -114,8 +116,7 @@ def test_trace_init_default(self): ) def test_trace_init_otlp(self): - environ["OTEL_SERVICE_NAME"] = "my-otlp-test-service" - Configuration._reset() + environ[OTEL_PYTHON_SERVICE_NAME] = "my-otlp-test-service" _init_tracing({"otlp": OTLPExporter}, RandomIdsGenerator) self.assertEqual(self.set_provider_mock.call_count, 1) @@ -129,9 +130,9 @@ def test_trace_init_otlp(self): provider.resource.attributes.get("service.name"), "my-otlp-test-service", ) - del environ["OTEL_SERVICE_NAME"] + del environ[OTEL_PYTHON_SERVICE_NAME] - @patch.dict(environ, {"OTEL_IDS_GENERATOR": "custom_ids_generator"}) + @patch.dict(environ, {OTEL_PYTHON_IDS_GENERATOR: "custom_ids_generator"}) @patch("opentelemetry.distro.IdsGenerator", new=IdsGenerator) @patch("opentelemetry.distro.iter_entry_points") def test_trace_init_custom_ids_generator(self, mock_iter_entry_points): @@ -140,7 +141,6 @@ def test_trace_init_custom_ids_generator(self, mock_iter_entry_points): IterEntryPoint("custom_ids_generator", CustomIdsGenerator) ] ) - Configuration._reset() ids_generator_name = _get_ids_generator() ids_generator = _import_ids_generator(ids_generator_name) _init_tracing({}, ids_generator) diff --git a/opentelemetry-distro/tests/test_distro.py b/opentelemetry-distro/tests/test_distro.py index 62d3a7e5e3..109aead69e 100644 --- a/opentelemetry-distro/tests/test_distro.py +++ b/opentelemetry-distro/tests/test_distro.py @@ -19,6 +19,10 @@ from pkg_resources import DistributionNotFound, require from opentelemetry.distro import OpenTelemetryDistro +from opentelemetry.environment_variables import ( + OTEL_METRICS_EXPORTER, + OTEL_TRACE_EXPORTER, +) class TestDistribution(TestCase): @@ -30,6 +34,8 @@ def test_package_available(self): def test_default_configuration(self): distro = OpenTelemetryDistro() - self.assertIsNone(os.environ.get("OTEL_EXPORTER")) + self.assertIsNone(os.environ.get(OTEL_TRACE_EXPORTER)) + self.assertIsNone(os.environ.get(OTEL_METRICS_EXPORTER)) distro.configure() - self.assertEqual("otlp", os.environ.get("OTEL_EXPORTER")) + self.assertEqual("otlp_span", os.environ.get(OTEL_TRACE_EXPORTER)) + self.assertEqual("otlp_metric", os.environ.get(OTEL_METRICS_EXPORTER)) diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index 97ebc5e7c4..1f6dd269e5 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -48,10 +48,11 @@ this can be overriden when needed. The command supports the following configuration options as CLI arguments and environment vars: -* ``--exporter`` or ``OTEL_EXPORTER`` +* ``--trace-exporter`` or ``OTEL_TRACE_EXPORTER`` +* ``--metrics-exporter`` or ``OTEL_METRICS_EXPORTER`` -Used to specify which trace exporter to use. Can be set to one or more -of the well-known exporter names (see below). +Used to specify which trace or metrics exporter to use. Can be set to one or +more of the well-known exporter names (see below). - Defaults to `otlp`. - Can be set to `none` to disable automatic tracer initialization. @@ -69,11 +70,11 @@ Well known trace exporter names: ``otlp`` is an alias for ``otlp_span,otlp_metric``. -* ``--service-name`` or ``OTEL_SERVICE_NAME`` +* ``--service-name`` or ``OTEL_PYTHON_SERVICE_NAME`` When present the value is passed on to the relevant exporter initializer as ``service_name`` argument. -* ``--ids-generator`` or ``OTEL_IDS_GENERATOR`` +* ``--ids-generator`` or ``OTEL_PYTHON_IDS_GENERATOR`` Used to specify which IDs Generator to use for the global Tracer Provider. By default, it will use the random IDs generator. diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index 959708de96..7d827663cb 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -20,6 +20,13 @@ from os.path import abspath, dirname, pathsep from shutil import which +from opentelemetry.environment_variables import ( + OTEL_METRICS_EXPORTER, + OTEL_PYTHON_IDS_GENERATOR, + OTEL_PYTHON_SERVICE_NAME, + OTEL_TRACE_EXPORTER, +) + logger = getLogger(__file__) @@ -32,18 +39,28 @@ def parse_args(): ) parser.add_argument( - "-e", - "--exporter", + "--metrics-exporter", + required=False, + help=""" + Uses the specified exporter to export metrics. + Accepts multiple exporters as comma separated values. + + Examples: + + --metrics-exporter=otlp_metric + """, + ) + + parser.add_argument( + "--trace-exporter", required=False, help=""" - Uses the specified exporter to export spans or metrics. + Uses the specified exporter to export spans. Accepts multiple exporters as comma separated values. Examples: - -e=otlp - -e=otlp_span,prometheus - -e=jaeger,otlp_metric + --trace-exporter=jaeger """, ) @@ -78,12 +95,14 @@ def parse_args(): def load_config_from_cli_args(args): - if args.exporter: - environ["OTEL_EXPORTER"] = args.exporter + if args.metrics_exporter: + environ[OTEL_METRICS_EXPORTER] = args.metrics_exporter + if args.trace_exporter: + environ[OTEL_TRACE_EXPORTER] = args.trace_exporter if args.service_name: - environ["OTEL_SERVICE_NAME"] = args.service_name + environ[OTEL_PYTHON_SERVICE_NAME] = args.service_name if args.ids_generator: - environ["OTEL_IDS_GENERATOR"] = args.ids_generator + environ[OTEL_PYTHON_IDS_GENERATOR] = args.ids_generator def run() -> None: diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index 72be01fd18..ba84bce585 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -12,13 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import sys from logging import getLogger +from os import environ, path from pkg_resources import iter_entry_points -from opentelemetry.configuration import Configuration +from opentelemetry.environment_variables import ( + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, +) logger = getLogger(__file__) @@ -36,7 +38,7 @@ def _load_distros(): def _load_instrumentors(): - package_to_exclude = Configuration().get("DISABLED_INSTRUMENTATIONS", []) + package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, []) if isinstance(package_to_exclude, str): package_to_exclude = package_to_exclude.split(",") # to handle users entering "requests , flask" or "requests, flask" with spaces @@ -85,7 +87,7 @@ def initialize(): if ( hasattr(sys, "argv") - and sys.argv[0].split(os.path.sep)[-1] == "celery" + and sys.argv[0].split(path.sep)[-1] == "celery" and "worker" in sys.argv[1:] ): from celery.signals import worker_process_init # pylint:disable=E0401 diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 44487a7794..ff2d244aa8 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -35,7 +35,6 @@ instrumentations = { "aiohttp-client": "opentelemetry-instrumentation-aiohttp-client>=0.15b0", "aiopg": "opentelemetry-instrumentation-aiopg>=0.15b0", - "asgi": "opentelemetry-instrumentation-asgi>=0.11b0", "asyncpg": "opentelemetry-instrumentation-asyncpg>=0.11b0", "boto": "opentelemetry-instrumentation-boto>=0.11b0", "botocore": "opentelemetry-instrumentation-botocore>=0.11b0", @@ -61,14 +60,12 @@ "sqlite3": "opentelemetry-instrumentation-sqlite3>=0.11b0", "starlette": "opentelemetry-instrumentation-starlette>=0.11b0", "tornado": "opentelemetry-instrumentation-tornado>=0.13b0", - "wsgi": "opentelemetry-instrumentation-wsgi>=0.8b0", } # relevant instrumentors and tracers to uninstall and check for conflicts for target libraries libraries = { "aiohttp-client": ("opentelemetry-instrumentation-aiohttp-client",), "aiopg": ("opentelemetry-instrumentation-aiopg",), - "asgi": ("opentelemetry-instrumentation-asgi",), "asyncpg": ("opentelemetry-instrumentation-asyncpg",), "boto": ("opentelemetry-instrumentation-boto",), "botocore": ("opentelemetry-instrumentation-botocore",), @@ -94,7 +91,6 @@ "sqlite3": ("opentelemetry-instrumentation-sqlite3",), "starlette": ("opentelemetry-instrumentation-starlette",), "tornado": ("opentelemetry-instrumentation-tornado",), - "wsgi": ("opentelemetry-instrumentation-wsgi",), } diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py index dedeca2003..8cd77a8a5b 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py @@ -58,19 +58,11 @@ def instrument(self, **kwargs): """Instrument the library This method will be called without any optional arguments by the - ``opentelemetry-instrument`` command. The configuration of - the instrumentation when done in this way should be done by previously - setting the configuration (using environment variables or any other - mechanism) that will be used later by the code in the ``instrument`` - implementation via the global ``Configuration`` object. - - The ``instrument`` methods ``kwargs`` should default to values from the - ``Configuration`` object. + ``opentelemetry-instrument`` command. This means that calling this method directly without passing any optional values should do the very same thing that the - ``opentelemetry-instrument`` command does. This approach is - followed because the ``Configuration`` object is immutable. + ``opentelemetry-instrument`` command does. """ if not self._is_instrumented: diff --git a/opentelemetry-instrumentation/tests/test_run.py b/opentelemetry-instrumentation/tests/test_run.py index 9bff8514b1..6b8c097f06 100644 --- a/opentelemetry-instrumentation/tests/test_run.py +++ b/opentelemetry-instrumentation/tests/test_run.py @@ -18,6 +18,11 @@ from unittest import TestCase from unittest.mock import patch +from opentelemetry.environment_variables import ( + OTEL_METRICS_EXPORTER, + OTEL_PYTHON_SERVICE_NAME, + OTEL_TRACE_EXPORTER, +) from opentelemetry.instrumentation import auto_instrumentation @@ -107,18 +112,22 @@ class TestArgs(TestCase): def test_exporter(self, _): # pylint: disable=no-self-use with patch("sys.argv", ["instrument", "2"]): auto_instrumentation.run() - self.assertIsNone(environ.get("OTEL_EXPORTER")) + self.assertIsNone(environ.get(OTEL_METRICS_EXPORTER)) - with patch("sys.argv", ["instrument", "-e", "zipkin", "1", "2"]): + with patch( + "sys.argv", ["instrument", "--trace-exporter", "jaeger", "1", "2"] + ): auto_instrumentation.run() - self.assertEqual(environ.get("OTEL_EXPORTER"), "zipkin") + self.assertEqual(environ.get(OTEL_TRACE_EXPORTER), "jaeger") @patch("opentelemetry.instrumentation.auto_instrumentation.execl") def test_service_name(self, _): # pylint: disable=no-self-use with patch("sys.argv", ["instrument", "2"]): auto_instrumentation.run() - self.assertIsNone(environ.get("OTEL_SERVICE_NAME")) + self.assertIsNone(environ.get(OTEL_PYTHON_SERVICE_NAME)) with patch("sys.argv", ["instrument", "-s", "my-service", "1", "2"]): auto_instrumentation.run() - self.assertEqual(environ.get("OTEL_SERVICE_NAME"), "my-service") + self.assertEqual( + environ.get(OTEL_PYTHON_SERVICE_NAME), "my-service" + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py new file mode 100644 index 0000000000..9ab85031b7 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES" +OTEL_LOG_LEVEL = "OTEL_LOG_LEVEL" +OTEL_TRACE_SAMPLER = "OTEL_TRACE_SAMPLER" +OTEL_TRACE_SAMPLER_ARG = "OTEL_TRACE_SAMPLER_ARG" +OTEL_BSP_SCHEDULE_DELAY = "OTEL_BSP_SCHEDULE_DELAY" +OTEL_BSP_EXPORT_TIMEOUT = "OTEL_BSP_EXPORT_TIMEOUT" +OTEL_BSP_MAX_QUEUE_SIZE = "OTEL_BSP_MAX_QUEUE_SIZE" +OTEL_BSP_MAX_EXPORT_BATCH_SIZE = "OTEL_BSP_MAX_EXPORT_BATCH_SIZE" +OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT" +OTEL_SPAN_EVENT_COUNT_LIMIT = "OTEL_SPAN_EVENT_COUNT_LIMIT" +OTEL_SPAN_LINK_COUNT_LIMIT = "OTEL_SPAN_LINK_COUNT_LIMIT" +OTEL_EXPORTER_JAEGER_AGENT_HOST = "OTEL_EXPORTER_JAEGER_AGENT_HOST" +OTEL_EXPORTER_JAEGER_AGENT_PORT = "OTEL_EXPORTER_JAEGER_AGENT_PORT" +OTEL_EXPORTER_JAEGER_ENDPOINT = "OTEL_EXPORTER_JAEGER_ENDPOINT" +OTEL_EXPORTER_JAEGER_USER = "OTEL_EXPORTER_JAEGER_USER" +OTEL_EXPORTER_JAEGER_PASSWORD = "OTEL_EXPORTER_JAEGER_PASSWORD" +OTEL_EXPORTER_ZIPKIN_ENDPOINT = "OTEL_EXPORTER_ZIPKIN_ENDPOINT" +OTEL_EXPORTER_PROMETHEUS_HOST = "OTEL_EXPORTER_PROMETHEUS_HOST" +OTEL_EXPORTER_PROMETHEUS_PORT = "OTEL_EXPORTER_PROMETHEUS_PORT" +OTEL_EXPORTER_ENDPOINT = "OTEL_EXPORTER_ENDPOINT" +OTEL_EXPORTER_OTLP_PROTOCOL = "OTEL_EXPORTER_OTLP_PROTOCOL" +OTEL_EXPORTER_OTLP_CERTIFICATE = "OTEL_EXPORTER_OTLP_CERTIFICATE" +OTEL_EXPORTER_OTLP_HEADERS = "OTEL_EXPORTER_OTLP_HEADERS" +OTEL_EXPORTER_OTLP_COMPRESSION = "OTEL_EXPORTER_OTLP_COMPRESSION" +OTEL_EXPORTER_OTLP_TIMEOUT = "OTEL_EXPORTER_OTLP_TIMEOUT" +OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT" +OTEL_EXPORTER_OTLP_SPAN_ENDPOINT = "OTEL_EXPORTER_OTLP_SPAN_ENDPOINT" +OTEL_EXPORTER_OTLP_METRIC_ENDPOINT = "OTEL_EXPORTER_OTLP_METRIC_ENDPOINT" +OTEL_EXPORTER_OTLP_SPAN_PROTOCOL = "OTEL_EXPORTER_OTLP_SPAN_PROTOCOL" +OTEL_EXPORTER_OTLP_METRIC_PROTOCOL = "OTEL_EXPORTER_OTLP_METRIC_PROTOCOL" +OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE = "OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE" +OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE = "OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE" +OTEL_EXPORTER_OTLP_SPAN_HEADERS = "OTEL_EXPORTER_OTLP_SPAN_HEADERS" +OTEL_EXPORTER_OTLP_METRIC_HEADERS = "OTEL_EXPORTER_OTLP_METRIC_HEADERS" +OTEL_EXPORTER_OTLP_SPAN_COMPRESSION = "OTEL_EXPORTER_OTLP_SPAN_COMPRESSION" +OTEL_EXPORTER_OTLP_METRIC_COMPRESSION = "OTEL_EXPORTER_OTLP_METRIC_COMPRESSION" +OTEL_EXPORTER_OTLP_SPAN_TIMEOUT = "OTEL_EXPORTER_OTLP_SPAN_TIMEOUT" +OTEL_EXPORTER_OTLP_METRIC_TIMEOUT = "OTEL_EXPORTER_OTLP_METRIC_TIMEOUT" +OTEL_EXPORTER_JAEGER_INSECURE = "OTEL_EXPORTER_JAEGER_INSECURE" +OTEL_EXPORTER_JAEGER_CERTIFICATE = "OTEL_EXPORTER_JAEGER_CERTIFICATE" +OTEL_EXPORTER_OTLP_INSECURE = "OTEL_EXPORTER_OTLP_INSECURE" +OTEL_EXPORTER_OTLP_SPAN_INSECURE = "OTEL_EXPORTER_OTLP_SPAN_INSECURE" +OTEL_EXPORTER_OTLP_METRIC_INSECURE = "OTEL_EXPORTER_OTLP_METRIC_INSECURE" +OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT = "OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 74d57439f3..aae838fe9e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -86,6 +86,8 @@ import pkg_resources +from opentelemetry.sdk.environment_variables import OTEL_RESOURCE_ATTRIBUTES + LabelValue = typing.Union[str, bool, int, float] Attributes = typing.Dict[str, LabelValue] logger = logging.getLogger(__name__) @@ -151,7 +153,6 @@ OPENTELEMETRY_SDK_VERSION = pkg_resources.get_distribution( "opentelemetry-sdk" ).version -OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES" class Resource: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index adafedb3da..2ecb82a0c3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -22,6 +22,7 @@ import traceback from collections import OrderedDict from contextlib import contextmanager +from os import environ from types import MappingProxyType, TracebackType from typing import ( Any, @@ -38,8 +39,12 @@ from opentelemetry import context as context_api from opentelemetry import trace as trace_api -from opentelemetry.configuration import Configuration from opentelemetry.sdk import util +from opentelemetry.sdk.environment_variables import ( + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + OTEL_SPAN_EVENT_COUNT_LIMIT, + OTEL_SPAN_LINK_COUNT_LIMIT, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import sampling from opentelemetry.sdk.trace.ids_generator import ( @@ -55,11 +60,12 @@ logger = logging.getLogger(__name__) -SPAN_ATTRIBUTE_COUNT_LIMIT = Configuration().get( - "SPAN_ATTRIBUTE_COUNT_LIMIT", 1000 +SPAN_ATTRIBUTE_COUNT_LIMIT = int( + environ.get(OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, 1000) ) -SPAN_EVENT_COUNT_LIMIT = Configuration().get("SPAN_EVENT_COUNT_LIMIT", 1000) -SPAN_LINK_COUNT_LIMIT = Configuration().get("SPAN_LINK_COUNT_LIMIT", 1000) + +SPAN_EVENT_COUNT_LIMIT = int(environ.get(OTEL_SPAN_EVENT_COUNT_LIMIT, 1000)) +SPAN_LINK_COUNT_LIMIT = int(environ.get(OTEL_SPAN_LINK_COUNT_LIMIT, 1000)) VALID_ATTR_VALUE_TYPES = (bool, str, int, float) # pylint: disable=protected-access TRACE_SAMPLER = sampling._get_from_env_or_default() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index ee63c84375..4d4cc70224 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -13,17 +13,21 @@ # limitations under the License. import collections -import json import logging -import os import sys import threading import typing from enum import Enum +from os import environ, linesep from typing import Optional -from opentelemetry.configuration import Configuration from opentelemetry.context import Context, attach, detach, set_value +from opentelemetry.sdk.environment_variables import ( + OTEL_BSP_EXPORT_TIMEOUT, + OTEL_BSP_MAX_EXPORT_BATCH_SIZE, + OTEL_BSP_MAX_QUEUE_SIZE, + OTEL_BSP_SCHEDULE_DELAY, +) from opentelemetry.sdk.trace import Span, SpanProcessor from opentelemetry.util import time_ns @@ -123,21 +127,21 @@ def __init__( ): if max_queue_size is None: - max_queue_size = Configuration().get("BSP_MAX_QUEUE_SIZE", 2048) + max_queue_size = int(environ.get(OTEL_BSP_MAX_QUEUE_SIZE, 2048)) if schedule_delay_millis is None: - schedule_delay_millis = Configuration().get( - "BSP_SCHEDULE_DELAY_MILLIS", 5000 + schedule_delay_millis = int( + environ.get(OTEL_BSP_SCHEDULE_DELAY, 5000) ) if max_export_batch_size is None: - max_export_batch_size = Configuration().get( - "BSP_MAX_EXPORT_BATCH_SIZE", 512 + max_export_batch_size = int( + environ.get(OTEL_BSP_MAX_EXPORT_BATCH_SIZE, 512) ) if export_timeout_millis is None: - export_timeout_millis = Configuration().get( - "BSP_EXPORT_TIMEOUT_MILLIS", 30000 + export_timeout_millis = int( + environ.get(OTEL_BSP_EXPORT_TIMEOUT, 30000) ) if max_queue_size <= 0: @@ -375,7 +379,7 @@ def __init__( service_name: Optional[str] = None, out: typing.IO = sys.stdout, formatter: typing.Callable[[Span], str] = lambda span: span.to_json() - + os.linesep, + + linesep, ): self.out = out self.formatter = formatter diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index bb17423354..17e90f78a9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -104,6 +104,10 @@ # pylint: disable=unused-import from opentelemetry.context import Context +from opentelemetry.sdk.environment_variables import ( + OTEL_TRACE_SAMPLER, + OTEL_TRACE_SAMPLER_ARG, +) from opentelemetry.trace import Link, get_current_span from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes @@ -366,7 +370,7 @@ def __init__(self, rate: float): def _get_from_env_or_default() -> Sampler: trace_sampler = os.getenv( - "OTEL_TRACE_SAMPLER", "parentbased_always_on" + OTEL_TRACE_SAMPLER, "parentbased_always_on" ).lower() if trace_sampler not in _KNOWN_SAMPLERS: _logger.warning("Couldn't recognize sampler %s.", trace_sampler) @@ -374,7 +378,7 @@ def _get_from_env_or_default() -> Sampler: if trace_sampler in ("traceidratio", "parentbased_traceidratio"): try: - rate = float(os.getenv("OTEL_TRACE_SAMPLER_ARG")) + rate = float(os.getenv(OTEL_TRACE_SAMPLER_ARG)) except ValueError: _logger.warning("Could not convert TRACE_SAMPLER_ARG to float.") rate = 1.0 diff --git a/opentelemetry-sdk/tests/conftest.py b/opentelemetry-sdk/tests/conftest.py index 5652fda036..92fd7a734d 100644 --- a/opentelemetry-sdk/tests/conftest.py +++ b/opentelemetry-sdk/tests/conftest.py @@ -14,12 +14,14 @@ from os import environ +from opentelemetry.environment_variables import OTEL_PYTHON_CONTEXT + def pytest_sessionstart(session): # pylint: disable=unused-argument - environ["OTEL_CONTEXT"] = "contextvars_context" + environ[OTEL_PYTHON_CONTEXT] = "contextvars_context" def pytest_sessionfinish(session): # pylint: disable=unused-argument - environ.pop("OTEL_CONTEXT") + environ.pop(OTEL_PYTHON_CONTEXT) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 92d4f65d13..86cd990cca 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -21,9 +21,14 @@ from unittest import mock from opentelemetry import trace as trace_api -from opentelemetry.configuration import Configuration from opentelemetry.context import Context from opentelemetry.sdk import trace +from opentelemetry.sdk.environment_variables import ( + OTEL_BSP_EXPORT_TIMEOUT, + OTEL_BSP_MAX_EXPORT_BATCH_SIZE, + OTEL_BSP_MAX_QUEUE_SIZE, + OTEL_BSP_SCHEDULE_DELAY, +) from opentelemetry.sdk.trace import export @@ -155,18 +160,13 @@ def _create_start_and_end_span(name, span_processor): class TestBatchExportSpanProcessor(unittest.TestCase): - def tearDown(self) -> None: - # reset global state of configuration object - # pylint: disable=protected-access - Configuration._reset() - @mock.patch.dict( "os.environ", { - "OTEL_BSP_MAX_QUEUE_SIZE": "10", - "OTEL_BSP_SCHEDULE_DELAY_MILLIS": "2", - "OTEL_BSP_MAX_EXPORT_BATCH_SIZE": "3", - "OTEL_BSP_EXPORT_TIMEOUT_MILLIS": "4", + OTEL_BSP_MAX_QUEUE_SIZE: "10", + OTEL_BSP_SCHEDULE_DELAY: "2", + OTEL_BSP_MAX_EXPORT_BATCH_SIZE: "3", + OTEL_BSP_EXPORT_TIMEOUT: "4", }, ) def test_batch_span_processor_environment_variables(self): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index b507bc5439..66a5a82c30 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -24,9 +24,15 @@ import pytest from opentelemetry import trace as trace_api -from opentelemetry.configuration import Configuration from opentelemetry.context import Context from opentelemetry.sdk import resources, trace +from opentelemetry.sdk.environment_variables import ( + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + OTEL_SPAN_EVENT_COUNT_LIMIT, + OTEL_SPAN_LINK_COUNT_LIMIT, + OTEL_TRACE_SAMPLER, + OTEL_TRACE_SAMPLER_ARG, +) from opentelemetry.sdk.trace import Resource, sampling from opentelemetry.sdk.trace.ids_generator import RandomIdsGenerator from opentelemetry.sdk.util import ns_to_iso_str @@ -188,7 +194,7 @@ def test_sampler_no_sampling(self): trace_api.TraceFlags.DEFAULT, ) - @mock.patch.dict("os.environ", {"OTEL_TRACE_SAMPLER": "always_off"}) + @mock.patch.dict("os.environ", {OTEL_TRACE_SAMPLER: "always_off"}) def test_sampler_with_env(self): # pylint: disable=protected-access reload(trace) @@ -207,8 +213,8 @@ def test_sampler_with_env(self): @mock.patch.dict( "os.environ", { - "OTEL_TRACE_SAMPLER": "parentbased_traceidratio", - "OTEL_TRACE_SAMPLER_ARG": "0.25", + OTEL_TRACE_SAMPLER: "parentbased_traceidratio", + OTEL_TRACE_SAMPLER_ARG: "0.25", }, ) def test_ratio_sampler_with_env(self): @@ -1282,22 +1288,12 @@ def test_attributes_to_json(self): class TestSpanLimits(unittest.TestCase): - def setUp(self): - # reset global state of configuration object - # pylint: disable=protected-access - Configuration._reset() - - def tearDown(self): - # reset global state of configuration object - # pylint: disable=protected-access - Configuration._reset() - @mock.patch.dict( "os.environ", { - "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT": "10", - "OTEL_SPAN_EVENT_COUNT_LIMIT": "20", - "OTEL_SPAN_LINK_COUNT_LIMIT": "30", + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10", + OTEL_SPAN_EVENT_COUNT_LIMIT: "20", + OTEL_SPAN_LINK_COUNT_LIMIT: "30", }, ) def test_span_environment_limits(self): diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 2b36a3f863..00d98a0ca8 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -36,7 +36,7 @@ cov instrumentation/opentelemetry-instrumentation-flask cov instrumentation/opentelemetry-instrumentation-requests cov exporter/opentelemetry-exporter-jaeger cov instrumentation/opentelemetry-instrumentation-opentracing-shim -cov instrumentation/opentelemetry-instrumentation-wsgi +cov util/opentelemetry-util-http cov exporter/opentelemetry-exporter-zipkin # aiohttp is only supported on Python 3.5+. diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py index f2fb14f206..2d8246c7a0 100644 --- a/tests/w3c_tracecontext_validation_server.py +++ b/tests/w3c_tracecontext_validation_server.py @@ -25,12 +25,12 @@ from opentelemetry import trace from opentelemetry.instrumentation.requests import RequestsInstrumentor -from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleExportSpanProcessor, ) +from opentelemetry.util.http.wsgi import OpenTelemetryMiddleware # FIXME This could likely be avoided by integrating this script into the # standard test running mechanisms. diff --git a/tox.ini b/tox.ini index 6f27fa0dcb..ef5f3f5a65 100644 --- a/tox.ini +++ b/tox.ini @@ -105,7 +105,7 @@ commands_pre = distro: pip install {toxinidir}/opentelemetry-distro {toxinidir}/opentelemetry-distro instrumentation: pip install {toxinidir}/opentelemetry-instrumentation - getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask + getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus @@ -202,7 +202,7 @@ commands_pre = -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests \ - -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi + -e {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http commands = {toxinidir}/scripts/tracecontext-integration-test.sh From 5bc23b0b027208e45c80f04ab6d983a9803607b5 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Sun, 31 Jan 2021 10:01:13 -0800 Subject: [PATCH 0763/1517] Add resource usage performance tests for creating a span (#1499) --- README.md | 20 ++++++++ .../profile_resource_usage_batch_export.py | 47 +++++++++++++++++++ .../profile_resource_usage_simple_export.py | 47 +++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py create mode 100644 opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py diff --git a/README.md b/README.md index eeb0ea231a..f09cf58a3f 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,26 @@ pip install -e ./instrumentation/opentelemetry-instrumentation-{instrumentation} For additional exporter and instrumentation packages, see the [`opentelemetry-python-contrib`](https://github.com/open-telemetry/opentelemetry-python-contrib) repository. +## Running Performance Tests + +This section provides details on how to reproduce performance tests results on your own +machine. + +### Resource Usage Tests + +1. Install scalene using the following command + +:: + + pip install -U scalene + +2. Run the `scalene` tests on any of the example Python programs + +:: + + scalene opentelemetry-/tests/performance/resource-usage//profile_resource_usage_.py + + ## Documentation The online documentation is available at https://opentelemetry-python.readthedocs.io/. diff --git a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py new file mode 100644 index 0000000000..08e3d77245 --- /dev/null +++ b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py @@ -0,0 +1,47 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from unittest.mock import patch + +from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.trace import TracerProvider, sampling +from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + +TEST_DURATION_SECONDS = 15 +SPANS_PER_SECOND = 10_000 + + +class MockTraceServiceStub(object): + def __init__(self, channel): + self.Export = lambda *args, **kwargs: None + + +old_stub = OTLPSpanExporter._stub +OTLPSpanExporter._stub = MockTraceServiceStub + +simple_span_processor = BatchExportSpanProcessor(OTLPSpanExporter()) +tracer = TracerProvider( + active_span_processor=simple_span_processor, sampler=sampling.DEFAULT_ON, +).get_tracer("resource_usage_tracer") + +starttime = time.time() +for _ in range(TEST_DURATION_SECONDS): + for _ in range(SPANS_PER_SECOND): + span = tracer.start_span("benchmarkedSpan") + span.end() + time_to_finish_spans = time.time() - starttime + time.sleep(1.0 - time_to_finish_spans if time_to_finish_spans < 1.0 else 0) + +OTLPSpanExporter._stub = old_stub diff --git a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py new file mode 100644 index 0000000000..f9dc460b00 --- /dev/null +++ b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py @@ -0,0 +1,47 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from unittest.mock import patch + +from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.trace import TracerProvider, sampling +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor + +TEST_DURATION_SECONDS = 15 +SPANS_PER_SECOND = 10_000 + + +class MockTraceServiceStub(object): + def __init__(self, channel): + self.Export = lambda *args, **kwargs: None + + +old_stub = OTLPSpanExporter._stub +OTLPSpanExporter._stub = MockTraceServiceStub + +simple_span_processor = SimpleExportSpanProcessor(OTLPSpanExporter()) +tracer = TracerProvider( + active_span_processor=simple_span_processor, sampler=sampling.DEFAULT_ON, +).get_tracer("resource_usage_tracer") + +starttime = time.time() +for _ in range(TEST_DURATION_SECONDS): + for _ in range(SPANS_PER_SECOND): + span = tracer.start_span("benchmarkedSpan") + span.end() + time_to_finish_spans = time.time() - starttime + time.sleep(1.0 - time_to_finish_spans if time_to_finish_spans < 1.0 else 0) + +OTLPSpanExporter._stub = old_stub From 30bb6a7b3e87e25c428f5582db96c4509d293523 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 2 Feb 2021 11:07:09 -0600 Subject: [PATCH 0764/1517] Set actions cache back to V2 (#1565) Fixes #1564 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fa9272e9f4..2747d78935 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -109,7 +109,7 @@ jobs: run: pip install -U tox - name: Cache tox environment # Preserves .tox directory between runs for faster installs - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: .tox key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core From b743ab1817a9d2dd10376ec34017ef6f16053ace Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 4 Feb 2021 10:36:23 -0600 Subject: [PATCH 0765/1517] Use environment variables for tracer and meter providers (#1571) --- CHANGELOG.md | 4 ++++ .../environment_variables/__init__.py | 2 ++ .../src/opentelemetry/util/__init__.py | 21 +++++++++++++++---- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f05dd7e83..b9aa9cc2ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.17b0...HEAD) +### Changed +- Tracer and Meter provider environment variables are now consistent with the rest + ([#1571](https://github.com/open-telemetry/opentelemetry-python/pull/1571)]) + ### Added - Added `end_on_exit` argument to `start_as_current_span` ([#1519](https://github.com/open-telemetry/opentelemetry-python/pull/1519)]) diff --git a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py index 1bd5a6f46d..810b24ca6d 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py @@ -19,3 +19,5 @@ OTEL_PYTHON_SERVICE_NAME = "OTEL_PYTHON_SERVICE_NAME" OTEL_TRACE_EXPORTER = "OTEL_TRACE_EXPORTER" OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER" +OTEL_PYTHON_TRACER_PROVIDER = "OTEL_PYTHON_TRACER_PROVIDER" +OTEL_PYTHON_METER_PROVIDER = "OTEL_PYTHON_METER_PROVIDER" diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index c49ba1b353..bb07b2c515 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -19,6 +19,11 @@ from pkg_resources import iter_entry_points +from opentelemetry.environment_variables import ( + OTEL_PYTHON_METER_PROVIDER, + OTEL_PYTHON_TRACER_PROVIDER, +) + if TYPE_CHECKING: from opentelemetry.metrics import MeterProvider from opentelemetry.trace import TracerProvider @@ -39,7 +44,9 @@ def time_ns() -> int: return int(time.time() * 1e9) -def _load_provider(provider: str) -> Provider: +def _load_provider( + provider_environment_variable: str, provider: str +) -> Provider: try: entry_point = next( iter_entry_points( @@ -47,7 +54,8 @@ def _load_provider(provider: str) -> Provider: name=cast( str, environ.get( - provider.upper(), "default_{}".format(provider), + provider_environment_variable, + "default_{}".format(provider), ), ), ) @@ -59,8 +67,13 @@ def _load_provider(provider: str) -> Provider: def _load_meter_provider(provider: str) -> "MeterProvider": - return cast("MeterProvider", _load_provider(provider)) + return cast( + "MeterProvider", _load_provider(OTEL_PYTHON_METER_PROVIDER, provider), + ) def _load_trace_provider(provider: str) -> "TracerProvider": - return cast("TracerProvider", _load_provider(provider)) + return cast( + "TracerProvider", + _load_provider(OTEL_PYTHON_TRACER_PROVIDER, provider), + ) From 9c81fdf34fb4175d322cf677893944d9dd1a627c Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 5 Feb 2021 08:44:41 -0800 Subject: [PATCH 0766/1517] Fix missing wsgi dependency (#1578) --- .github/workflows/test.yml | 2 +- docs/examples/auto-instrumentation/server_instrumented.py | 2 +- tests/w3c_tracecontext_validation_server.py | 2 +- tox.ini | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2747d78935..91da71663f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: f005d90ed3bc75ee6eb7297f9e3a6b55a55b22aa + CONTRIB_REPO_SHA: b404de2f393aaaeca73694c37fe58fecf423a707 jobs: build: diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_instrumented.py index bc331de3ca..6212ec3333 100644 --- a/docs/examples/auto-instrumentation/server_instrumented.py +++ b/docs/examples/auto-instrumentation/server_instrumented.py @@ -15,13 +15,13 @@ from flask import Flask, request from opentelemetry import propagators, trace +from opentelemetry.instrumentation.wsgi import collect_request_attributes from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleExportSpanProcessor, ) from opentelemetry.trace.propagation.textmap import DictGetter -from opentelemetry.util.http.wsgi import collect_request_attributes app = Flask(__name__) diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py index 2d8246c7a0..f2fb14f206 100644 --- a/tests/w3c_tracecontext_validation_server.py +++ b/tests/w3c_tracecontext_validation_server.py @@ -25,12 +25,12 @@ from opentelemetry import trace from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleExportSpanProcessor, ) -from opentelemetry.util.http.wsgi import OpenTelemetryMiddleware # FIXME This could likely be avoided by integrating this script into the # standard test running mechanisms. diff --git a/tox.ini b/tox.ini index ef5f3f5a65..079f40d164 100644 --- a/tox.ini +++ b/tox.ini @@ -105,7 +105,7 @@ commands_pre = distro: pip install {toxinidir}/opentelemetry-distro {toxinidir}/opentelemetry-distro instrumentation: pip install {toxinidir}/opentelemetry-instrumentation - getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask + getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus @@ -202,6 +202,7 @@ commands_pre = -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests \ + -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi \ -e {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http commands = From 59fc285333041f3dc45a1c82563f531711bf63a5 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 5 Feb 2021 22:38:16 +0530 Subject: [PATCH 0767/1517] Update README.md (#1574) --- README.md | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f09cf58a3f..901fbf9112 100644 --- a/README.md +++ b/README.md @@ -60,14 +60,6 @@ pip install opentelemetry-api pip install opentelemetry-sdk ``` -The -[`instrumentation/`](https://github.com/open-telemetry/opentelemetry-python/tree/main/instrumentation) -directory includes OpenTelemetry instrumentation packages. You can install the packages separately with the following command: - -```sh -pip install opentelemetry-instrumentation-{instrumentation} -``` - The [`exporter/`](https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter) directory includes OpenTelemetry exporter packages. You can install the packages separately with the following command: @@ -106,15 +98,11 @@ machine. 1. Install scalene using the following command -:: - - pip install -U scalene +`pip install scalene` 2. Run the `scalene` tests on any of the example Python programs -:: - - scalene opentelemetry-/tests/performance/resource-usage//profile_resource_usage_.py +`scalene opentelemetry-/tests/performance/resource-usage//profile_resource_usage_.py` ## Documentation From 24edd3d81afb394c3b2d1f0416faca59db931b66 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 8 Feb 2021 09:48:28 -0800 Subject: [PATCH 0768/1517] Remove metrics from main branch (#1568) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 + CONTRIBUTING.md | 4 + docs/api/api.rst | 1 - docs/api/metrics.rst | 7 - docs/examples/auto-instrumentation/README.rst | 2 +- docs/examples/basic_meter/README.rst | 42 - docs/examples/basic_meter/basic_metrics.py | 87 --- .../basic_meter/calling_conventions.py | 66 -- docs/examples/basic_meter/http_metrics.py | 42 - docs/examples/basic_meter/observer.py | 60 -- docs/examples/basic_meter/view.py | 106 --- .../README.rst | 0 .../error_handler_0/README.rst | 0 .../error_handler_0/setup.cfg | 0 .../error_handler_0/setup.py | 0 .../src/error_handler_0/__init__.py | 0 .../src/error_handler_0/version.py | 0 .../error_handler_1/README.rst | 0 .../error_handler_1/setup.cfg | 0 .../error_handler_1/setup.py | 0 .../src/error_handler_1/__init__.py | 0 .../src/error_handler_1/version.py | 0 .../example.py | 0 .../opencensus-exporter-metrics/README.rst | 52 -- .../opencensus-exporter-metrics/collector.py | 44 -- .../docker/collector-config.yaml | 18 - .../docker/docker-compose.yaml | 19 - .../docker/prometheus.yaml | 5 - docs/exporter/prometheus/prometheus.rst | 7 - docs/getting-started.rst | 79 +- docs/getting_started/metrics_example.py | 41 - docs/getting_started/otlpcollector_example.py | 32 +- docs/getting_started/prometheus_example.py | 51 -- docs/getting_started/tests/test_metrics.py | 29 - docs/sdk/metrics.export.aggregate.rst | 7 - docs/sdk/metrics.export.processor.rst | 11 - docs/sdk/metrics.export.rst | 7 - docs/sdk/metrics.rst | 16 - docs/sdk/sdk.rst | 1 - docs/shim/instrumentation.rst | 1 - docs/shim/metric.rst | 7 - .../README.rst | 2 +- .../exporter/opencensus/__init__.py | 3 +- .../opencensus/metrics_exporter/__init__.py | 209 ----- .../test_otcollector_metrics_exporter.py | 311 -------- .../opentelemetry-exporter-otlp/setup.cfg | 1 - .../opentelemetry/exporter/otlp/exporter.py | 2 +- .../otlp/metrics_exporter/__init__.py | 356 --------- .../tests/test_otlp_metric_exporter.py | 397 ---------- .../opentelemetry-exporter-prometheus/LICENSE | 201 ----- .../MANIFEST.in | 9 - .../README.rst | 23 - .../setup.cfg | 55 -- .../setup.py | 26 - .../exporter/prometheus/__init__.py | 193 ----- .../exporter/prometheus/version.py | 15 - .../tests/__init__.py | 13 - .../tests/test_prometheus_exporter.py | 169 ----- opentelemetry-api/setup.cfg | 2 - .../environment_variables/__init__.py | 2 - .../src/opentelemetry/metrics/__init__.py | 610 --------------- .../src/opentelemetry/metrics/py.typed | 0 .../src/opentelemetry/util/__init__.py | 16 +- opentelemetry-api/tests/metrics/__init__.py | 13 - .../tests/metrics/test_metrics.py | 74 -- .../tests/test_implementation.py | 59 +- .../src/opentelemetry/distro/__init__.py | 40 +- opentelemetry-distro/tests/test_distro.py | 7 +- opentelemetry-instrumentation/README.rst | 8 +- .../auto_instrumentation/__init__.py | 16 - .../opentelemetry/instrumentation/metric.py | 107 --- .../tests/test_metric.py | 138 ---- .../tests/test_run.py | 3 +- opentelemetry-sdk/setup.cfg | 3 - .../sdk/environment_variables/__init__.py | 9 - .../src/opentelemetry/sdk/metrics/__init__.py | 621 --------------- .../sdk/metrics/export/__init__.py | 91 --- .../sdk/metrics/export/aggregate.py | 258 ------- .../sdk/metrics/export/controller.py | 63 -- .../export/in_memory_metrics_exporter.py | 63 -- .../sdk/metrics/export/processor.py | 96 --- .../src/opentelemetry/sdk/metrics/view.py | 186 ----- .../opentelemetry/sdk/resources/__init__.py | 7 +- .../opentelemetry/sdk/util/instrumentation.py | 3 +- opentelemetry-sdk/tests/metrics/__init__.py | 13 - .../tests/metrics/export/__init__.py | 13 - .../tests/metrics/export/test_export.py | 717 ------------------ .../tests/metrics/test_globals.py | 23 - .../tests/metrics/test_implementation.py | 33 - .../tests/metrics/test_metrics.py | 662 ---------------- opentelemetry-sdk/tests/metrics/test_view.py | 303 -------- .../util/src/opentelemetry/test/controller.py | 62 -- .../util/src/opentelemetry/test/test_base.py | 30 - tox.ini | 8 - 95 files changed, 34 insertions(+), 7158 deletions(-) delete mode 100644 docs/api/metrics.rst delete mode 100644 docs/examples/basic_meter/README.rst delete mode 100644 docs/examples/basic_meter/basic_metrics.py delete mode 100644 docs/examples/basic_meter/calling_conventions.py delete mode 100644 docs/examples/basic_meter/http_metrics.py delete mode 100644 docs/examples/basic_meter/observer.py delete mode 100644 docs/examples/basic_meter/view.py rename docs/examples/{error_hander => error_handler}/README.rst (100%) rename docs/examples/{error_hander => error_handler}/error_handler_0/README.rst (100%) rename docs/examples/{error_hander => error_handler}/error_handler_0/setup.cfg (100%) rename docs/examples/{error_hander => error_handler}/error_handler_0/setup.py (100%) rename docs/examples/{error_hander => error_handler}/error_handler_0/src/error_handler_0/__init__.py (100%) rename docs/examples/{error_hander => error_handler}/error_handler_0/src/error_handler_0/version.py (100%) rename docs/examples/{error_hander => error_handler}/error_handler_1/README.rst (100%) rename docs/examples/{error_hander => error_handler}/error_handler_1/setup.cfg (100%) rename docs/examples/{error_hander => error_handler}/error_handler_1/setup.py (100%) rename docs/examples/{error_hander => error_handler}/error_handler_1/src/error_handler_1/__init__.py (100%) rename docs/examples/{error_hander => error_handler}/error_handler_1/src/error_handler_1/version.py (100%) rename docs/examples/{error_hander => error_handler}/example.py (100%) delete mode 100644 docs/examples/opencensus-exporter-metrics/README.rst delete mode 100644 docs/examples/opencensus-exporter-metrics/collector.py delete mode 100644 docs/examples/opencensus-exporter-metrics/docker/collector-config.yaml delete mode 100644 docs/examples/opencensus-exporter-metrics/docker/docker-compose.yaml delete mode 100644 docs/examples/opencensus-exporter-metrics/docker/prometheus.yaml delete mode 100644 docs/exporter/prometheus/prometheus.rst delete mode 100644 docs/getting_started/metrics_example.py delete mode 100644 docs/getting_started/prometheus_example.py delete mode 100644 docs/getting_started/tests/test_metrics.py delete mode 100644 docs/sdk/metrics.export.aggregate.rst delete mode 100644 docs/sdk/metrics.export.processor.rst delete mode 100644 docs/sdk/metrics.export.rst delete mode 100644 docs/sdk/metrics.rst delete mode 100644 docs/shim/metric.rst delete mode 100644 exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py delete mode 100644 exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py delete mode 100644 exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py delete mode 100644 exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py delete mode 100644 exporter/opentelemetry-exporter-prometheus/LICENSE delete mode 100644 exporter/opentelemetry-exporter-prometheus/MANIFEST.in delete mode 100644 exporter/opentelemetry-exporter-prometheus/README.rst delete mode 100644 exporter/opentelemetry-exporter-prometheus/setup.cfg delete mode 100644 exporter/opentelemetry-exporter-prometheus/setup.py delete mode 100644 exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py delete mode 100644 exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py delete mode 100644 exporter/opentelemetry-exporter-prometheus/tests/__init__.py delete mode 100644 exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py delete mode 100644 opentelemetry-api/src/opentelemetry/metrics/__init__.py delete mode 100644 opentelemetry-api/src/opentelemetry/metrics/py.typed delete mode 100644 opentelemetry-api/tests/metrics/__init__.py delete mode 100644 opentelemetry-api/tests/metrics/test_metrics.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py delete mode 100644 opentelemetry-instrumentation/tests/test_metric.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/view.py delete mode 100644 opentelemetry-sdk/tests/metrics/__init__.py delete mode 100644 opentelemetry-sdk/tests/metrics/export/__init__.py delete mode 100644 opentelemetry-sdk/tests/metrics/export/test_export.py delete mode 100644 opentelemetry-sdk/tests/metrics/test_globals.py delete mode 100644 opentelemetry-sdk/tests/metrics/test_implementation.py delete mode 100644 opentelemetry-sdk/tests/metrics/test_metrics.py delete mode 100644 opentelemetry-sdk/tests/metrics/test_view.py delete mode 100644 tests/util/src/opentelemetry/test/controller.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 91da71663f..7debd81c9d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: b404de2f393aaaeca73694c37fe58fecf423a707 + CONTRIB_REPO_SHA: 43df76e5ed69f45d993c98ea68daea3c4622ea2d jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index b9aa9cc2ee..e93c12b215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - Remove Configuration ([#1523](https://github.com/open-telemetry/opentelemetry-python/pull/1523)) +- Remove Metrics as part of stable, marked as experimental + ([#1568](https://github.com/open-telemetry/opentelemetry-python/pull/1568)) ## [0.17b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.17b0) - 2021-01-20 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c12c2e13ee..9a01f446d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,10 @@ Please take a look at this list first, your contributions may belong in one of t programmatic instrumentations that are now in the `ext` directory in the main OpenTelemetry repo. Please ask in the Gitter channel (see below) for guidance if you want to contribute with these instrumentations. +# Find the right branch + +The default branch for this repo is `main`. Changes that pertain to components marked as `stable` in the [specifications](https://github.com/open-telemetry/opentelemetry-specification) go into this branch, which currently does not include `metrics`. Changes that pertain to `metrics` go into the `metrics` branch. + ## Find a Buddy and get Started Quickly! If you are looking for someone to help you find a starting point and be a resource for your first contribution, join our diff --git a/docs/api/api.rst b/docs/api/api.rst index d132b78cf8..e531d1419e 100644 --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -8,6 +8,5 @@ OpenTelemetry Python API baggage context - metrics trace environment_variables diff --git a/docs/api/metrics.rst b/docs/api/metrics.rst deleted file mode 100644 index 358d5491a6..0000000000 --- a/docs/api/metrics.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.metrics package -============================= - -Module contents ---------------- - -.. automodule:: opentelemetry.metrics diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index 0c8a0e929d..f21ab8f566 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -151,7 +151,7 @@ and run the following command instead: .. code:: sh - $ opentelemetry-instrument -e console_span,console_metrics python server_uninstrumented.py + $ opentelemetry-instrument -e console_span python server_uninstrumented.py In the console where you previously executed ``client.py``, run the following command again: diff --git a/docs/examples/basic_meter/README.rst b/docs/examples/basic_meter/README.rst deleted file mode 100644 index 46efe36388..0000000000 --- a/docs/examples/basic_meter/README.rst +++ /dev/null @@ -1,42 +0,0 @@ -Basic Meter -=========== - -These examples show how to use OpenTelemetry to capture and report metrics. - -There are three different examples: - -* basic_metrics: Shows how to create a metric instrument, how to configure an - exporter and a controller and also how to capture data by using the direct - calling convention. - -* calling_conventions: Shows how to use the direct, bound and batch calling conventions. - -* observer: Shows how to use the observer instrument. - -The source files of these examples are available :scm_web:`here `. - -Installation ------------- - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - pip install psutil # needed to get ram and cpu usage in the observer example - -Run the Example ---------------- - -.. code-block:: sh - - python .py - -The output will be shown in the console after few seconds. - -Useful links ------------- - -- OpenTelemetry_ -- :doc:`../../api/metrics` - -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/docs/examples/basic_meter/basic_metrics.py b/docs/examples/basic_meter/basic_metrics.py deleted file mode 100644 index 7341568205..0000000000 --- a/docs/examples/basic_meter/basic_metrics.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -This module serves as an example for a simple application using metrics. - -It shows: -- How to configure a meter passing a stateful or stateless. -- How to configure an exporter and how to create a controller. -- How to create some metrics instruments and how to capture data with them. -- How to use views to specify aggregation types for each metric instrument. -""" -import sys -import time - -from opentelemetry import metrics -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter - -print( - "Starting example, values will be printed to the console every 5 seconds." -) - -# Stateful determines whether how metrics are collected: if true, metrics -# accumulate over the process lifetime. If false, metrics are reset at the -# beginning of each collection interval. -stateful = True - -# Sets the global MeterProvider instance -metrics.set_meter_provider(MeterProvider()) - -# The Meter is responsible for creating and recording metrics. Each meter has a -# unique name, which we set as the module's name here. -meter = metrics.get_meter(__name__) - -# Exporter to export metrics to the console -exporter = ConsoleMetricsExporter() - -# start_pipeline will notify the MeterProvider to begin collecting/exporting -# metrics with the given meter, exporter and interval in seconds -metrics.get_meter_provider().start_pipeline(meter, exporter, 5) - -# Metric instruments allow to capture measurements -requests_counter = meter.create_counter( - name="requests", - description="number of requests", - unit="1", - value_type=int, -) - -requests_size = meter.create_valuerecorder( - name="requests_size", - description="size of requests", - unit="1", - value_type=int, -) - -# Labels are used to identify key-values that are associated with a specific -# metric that you want to record. These are useful for pre-aggregation and can -# be used to store custom dimensions pertaining to a metric -staging_labels = {"environment": "staging"} -testing_labels = {"environment": "testing"} - -# Update the metric instruments using the direct calling convention -requests_counter.add(25, staging_labels) -requests_size.record(100, staging_labels) -time.sleep(10) - -requests_counter.add(50, staging_labels) -requests_size.record(5000, staging_labels) -time.sleep(5) - -requests_counter.add(35, testing_labels) -requests_size.record(2, testing_labels) - -input("...\n") diff --git a/docs/examples/basic_meter/calling_conventions.py b/docs/examples/basic_meter/calling_conventions.py deleted file mode 100644 index 58aeed6c6f..0000000000 --- a/docs/examples/basic_meter/calling_conventions.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -This example shows how to use the different modes to capture metrics. -It shows the usage of the direct, bound and batch calling conventions. -""" -import time - -from opentelemetry import metrics -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter - -# Use the meter type provided by the SDK package -metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__) -metrics.get_meter_provider().start_pipeline(meter, ConsoleMetricsExporter(), 5) - -requests_counter = meter.create_counter( - name="requests", - description="number of requests", - unit="1", - value_type=int, -) - -clicks_counter = meter.create_counter( - name="clicks", description="number of clicks", unit="1", value_type=int, -) - -labels = {"environment": "staging"} - -print("Updating using direct calling convention...") -# You can record metrics directly using the metric instrument. You pass in -# labels that you would like to record for. -requests_counter.add(25, labels) -time.sleep(10) - -print("Updating using a bound instrument...") -# You can record metrics with bound metric instruments. Bound metric -# instruments are created by passing in labels. A bound metric instrument -# is essentially metric data that corresponds to a specific set of labels. -# Therefore, getting a bound metric instrument using the same set of labels -# will yield the same bound metric instrument. -bound_requests_counter = requests_counter.bind(labels) -bound_requests_counter.add(100) -time.sleep(5) - -print("Updating using batch calling convention...") -# You can record metrics in a batch by passing in labels and a sequence of -# (metric, value) pairs. The value would be recorded for each metric using the -# specified labels for each. -meter.record_batch(labels, ((requests_counter, 50), (clicks_counter, 70))) -time.sleep(5) - -input("...\n") diff --git a/docs/examples/basic_meter/http_metrics.py b/docs/examples/basic_meter/http_metrics.py deleted file mode 100644 index 8fd6c6294c..0000000000 --- a/docs/examples/basic_meter/http_metrics.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -This module shows how you can enable collection and exporting of http metrics -related to instrumentations. -""" -import requests - -from opentelemetry import metrics -from opentelemetry.instrumentation.requests import RequestsInstrumentor -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter - -# Sets the global MeterProvider instance -metrics.set_meter_provider(MeterProvider()) - -# Exporter to export metrics to the console -exporter = ConsoleMetricsExporter() - -# Instrument the requests library -RequestsInstrumentor().instrument() - -# Indicate to start collecting and exporting requests related metrics -metrics.get_meter_provider().start_pipeline( - RequestsInstrumentor().meter, exporter, 5 -) - -response = requests.get("http://example.com") - -input("...\n") diff --git a/docs/examples/basic_meter/observer.py b/docs/examples/basic_meter/observer.py deleted file mode 100644 index ed0eb5e278..0000000000 --- a/docs/examples/basic_meter/observer.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -This example shows how the Observer metric instrument can be used to capture -asynchronous metrics data. -""" -import psutil - -from opentelemetry import metrics -from opentelemetry.sdk.metrics import MeterProvider, ValueObserver -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter - -metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__) -metrics.get_meter_provider().start_pipeline(meter, ConsoleMetricsExporter(), 5) - - -# Callback to gather cpu usage -def get_cpu_usage_callback(observer): - for (number, percent) in enumerate(psutil.cpu_percent(percpu=True)): - labels = {"cpu_number": str(number)} - observer.observe(percent, labels) - - -meter.register_valueobserver( - callback=get_cpu_usage_callback, - name="cpu_percent", - description="per-cpu usage", - unit="1", - value_type=float, -) - - -# Callback to gather RAM memory usage -def get_ram_usage_callback(observer): - ram_percent = psutil.virtual_memory().percent - observer.observe(ram_percent, {}) - - -meter.register_valueobserver( - callback=get_ram_usage_callback, - name="ram_percent", - description="RAM memory usage", - unit="1", - value_type=float, -) - -input("Metrics will be printed soon. Press a key to finish...\n") diff --git a/docs/examples/basic_meter/view.py b/docs/examples/basic_meter/view.py deleted file mode 100644 index 81850b1452..0000000000 --- a/docs/examples/basic_meter/view.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -This example shows how to use the different modes to capture metrics. -It shows the usage of the direct, bound and batch calling conventions. -""" -from opentelemetry import metrics -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.aggregate import ( - HistogramAggregator, - LastValueAggregator, - MinMaxSumCountAggregator, - SumAggregator, -) -from opentelemetry.sdk.metrics.view import View, ViewConfig - -# Use the meter type provided by the SDK package -metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__) -metrics.get_meter_provider().start_pipeline(meter, ConsoleMetricsExporter(), 5) - -requests_counter = meter.create_counter( - name="requests", - description="number of requests", - unit="1", - value_type=int, -) - -requests_size = meter.create_valuerecorder( - name="requests_size", - description="size of requests", - unit="1", - value_type=int, -) - -# Views are used to define an aggregation type and label keys to aggregate by -# for a given metric - -# Two views with the same metric and aggregation type but different label keys -# With ViewConfig.LABEL_KEYS, all labels but the ones defined in label_keys are -# dropped from the aggregation -counter_view1 = View( - requests_counter, - SumAggregator, - label_keys=["environment"], - view_config=ViewConfig.LABEL_KEYS, -) -counter_view2 = View( - requests_counter, - MinMaxSumCountAggregator, - label_keys=["os_type"], - view_config=ViewConfig.LABEL_KEYS, -) -# This view has ViewConfig set to UNGROUPED, meaning all recorded metrics take -# the labels directly without and consideration for label_keys -counter_view3 = View( - requests_counter, - LastValueAggregator, - label_keys=["environment"], # is not used due to ViewConfig.UNGROUPED - view_config=ViewConfig.UNGROUPED, -) -# This view uses the HistogramAggregator which accepts an option config -# parameter to specify the bucket ranges -size_view = View( - requests_size, - HistogramAggregator, - label_keys=["environment"], # is not used due to ViewConfig.UNGROUPED - aggregator_config={"bounds": [20, 40, 60, 80, 100]}, - view_config=ViewConfig.UNGROUPED, -) - -# Register the views to the view manager to use the views. Views MUST be -# registered before recording metrics. Metrics that are recorded without -# views defined for them will use a default for that type of metric -meter.register_view(counter_view1) -meter.register_view(counter_view2) -meter.register_view(counter_view3) -meter.register_view(size_view) - -# The views will evaluate the labels passed into the record and aggregate upon -# the unique labels that are generated -# view1 labels will evaluate to {"environment": "staging"} -# view2 labels will evaluate to {"os_type": linux} -# view3 labels will evaluate to {"environment": "staging", "os_type": "linux"} -requests_counter.add(100, {"environment": "staging", "os_type": "linux"}) - -# Since this is using the HistogramAggregator, the bucket counts will be reflected -# with each record -requests_size.record(25, {"test": "value"}) -requests_size.record(-3, {"test": "value"}) -requests_size.record(200, {"test": "value"}) - -input("...\n") diff --git a/docs/examples/error_hander/README.rst b/docs/examples/error_handler/README.rst similarity index 100% rename from docs/examples/error_hander/README.rst rename to docs/examples/error_handler/README.rst diff --git a/docs/examples/error_hander/error_handler_0/README.rst b/docs/examples/error_handler/error_handler_0/README.rst similarity index 100% rename from docs/examples/error_hander/error_handler_0/README.rst rename to docs/examples/error_handler/error_handler_0/README.rst diff --git a/docs/examples/error_hander/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg similarity index 100% rename from docs/examples/error_hander/error_handler_0/setup.cfg rename to docs/examples/error_handler/error_handler_0/setup.cfg diff --git a/docs/examples/error_hander/error_handler_0/setup.py b/docs/examples/error_handler/error_handler_0/setup.py similarity index 100% rename from docs/examples/error_hander/error_handler_0/setup.py rename to docs/examples/error_handler/error_handler_0/setup.py diff --git a/docs/examples/error_hander/error_handler_0/src/error_handler_0/__init__.py b/docs/examples/error_handler/error_handler_0/src/error_handler_0/__init__.py similarity index 100% rename from docs/examples/error_hander/error_handler_0/src/error_handler_0/__init__.py rename to docs/examples/error_handler/error_handler_0/src/error_handler_0/__init__.py diff --git a/docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py similarity index 100% rename from docs/examples/error_hander/error_handler_0/src/error_handler_0/version.py rename to docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py diff --git a/docs/examples/error_hander/error_handler_1/README.rst b/docs/examples/error_handler/error_handler_1/README.rst similarity index 100% rename from docs/examples/error_hander/error_handler_1/README.rst rename to docs/examples/error_handler/error_handler_1/README.rst diff --git a/docs/examples/error_hander/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg similarity index 100% rename from docs/examples/error_hander/error_handler_1/setup.cfg rename to docs/examples/error_handler/error_handler_1/setup.cfg diff --git a/docs/examples/error_hander/error_handler_1/setup.py b/docs/examples/error_handler/error_handler_1/setup.py similarity index 100% rename from docs/examples/error_hander/error_handler_1/setup.py rename to docs/examples/error_handler/error_handler_1/setup.py diff --git a/docs/examples/error_hander/error_handler_1/src/error_handler_1/__init__.py b/docs/examples/error_handler/error_handler_1/src/error_handler_1/__init__.py similarity index 100% rename from docs/examples/error_hander/error_handler_1/src/error_handler_1/__init__.py rename to docs/examples/error_handler/error_handler_1/src/error_handler_1/__init__.py diff --git a/docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py similarity index 100% rename from docs/examples/error_hander/error_handler_1/src/error_handler_1/version.py rename to docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py diff --git a/docs/examples/error_hander/example.py b/docs/examples/error_handler/example.py similarity index 100% rename from docs/examples/error_hander/example.py rename to docs/examples/error_handler/example.py diff --git a/docs/examples/opencensus-exporter-metrics/README.rst b/docs/examples/opencensus-exporter-metrics/README.rst deleted file mode 100644 index 0a71685ee9..0000000000 --- a/docs/examples/opencensus-exporter-metrics/README.rst +++ /dev/null @@ -1,52 +0,0 @@ -OpenTelemetry Collector Metrics OpenCensus Exporter Example -=========================================================== - -This example shows how to use the OpenCensus Exporter to export metrics to -the OpenTelemetry collector. - -The source files of this example are available :scm_web:`here `. - -Installation ------------- - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - pip install opentelemetry-exporter-opencensus - -Run the Example ---------------- - -Before running the example, it's necessary to run the OpenTelemetry collector -and Prometheus. The :scm_web:`docker ` -folder contains the a docker-compose template with the configuration of those -services. - -.. code-block:: sh - - pip install docker-compose - cd docker - docker-compose up - - -Now, the example can be executed: - -.. code-block:: sh - - python collector.py - - -The metrics are available in the Prometheus dashboard at http://localhost:9090/graph, -look for the "requests" metric on the list. - -Useful links ------------- - -- OpenTelemetry_ -- `OpenTelemetry Collector`_ -- :doc:`../../api/trace` -- :doc:`../../exporter/opencensus/opencensus` - -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ -.. _OpenTelemetry Collector: https://github.com/open-telemetry/opentelemetry-collector diff --git a/docs/examples/opencensus-exporter-metrics/collector.py b/docs/examples/opencensus-exporter-metrics/collector.py deleted file mode 100644 index 31527d480a..0000000000 --- a/docs/examples/opencensus-exporter-metrics/collector.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -This example shows how to export metrics to the OT collector. -""" - -from opentelemetry import metrics -from opentelemetry.exporter.opencensus.metrics_exporter import ( - OpenCensusMetricsExporter, -) -from opentelemetry.sdk.metrics import MeterProvider - -exporter = OpenCensusMetricsExporter( - service_name="basic-service", endpoint="localhost:55678" -) - -metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__) -metrics.get_meter_provider().start_pipeline(meter, exporter, 5) - -requests_counter = meter.create_counter( - name="requests", - description="number of requests", - unit="1", - value_type=int, -) - -staging_labels = {"environment": "staging"} -requests_counter.add(25, staging_labels) - -print("Metrics are available now at http://localhost:9090/graph") -input("Press any key to exit...") diff --git a/docs/examples/opencensus-exporter-metrics/docker/collector-config.yaml b/docs/examples/opencensus-exporter-metrics/docker/collector-config.yaml deleted file mode 100644 index 78f685d208..0000000000 --- a/docs/examples/opencensus-exporter-metrics/docker/collector-config.yaml +++ /dev/null @@ -1,18 +0,0 @@ -receivers: - opencensus: - endpoint: "0.0.0.0:55678" - -exporters: - prometheus: - endpoint: "0.0.0.0:8889" - logging: {} - -processors: - batch: - queued_retry: - -service: - pipelines: - metrics: - receivers: [opencensus] - exporters: [logging, prometheus] diff --git a/docs/examples/opencensus-exporter-metrics/docker/docker-compose.yaml b/docs/examples/opencensus-exporter-metrics/docker/docker-compose.yaml deleted file mode 100644 index d12a0a4f18..0000000000 --- a/docs/examples/opencensus-exporter-metrics/docker/docker-compose.yaml +++ /dev/null @@ -1,19 +0,0 @@ -version: "2" -services: - - otel-collector: - image: omnition/opentelemetry-collector-contrib:latest - command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"] - volumes: - - ./collector-config.yaml:/conf/collector-config.yaml - ports: - - "8889:8889" # Prometheus exporter metrics - - "55678:55678" # OpenCensus receiver - - prometheus: - container_name: prometheus - image: prom/prometheus:latest - volumes: - - ./prometheus.yaml:/etc/prometheus/prometheus.yml - ports: - - "9090:9090" diff --git a/docs/examples/opencensus-exporter-metrics/docker/prometheus.yaml b/docs/examples/opencensus-exporter-metrics/docker/prometheus.yaml deleted file mode 100644 index 4eb2357231..0000000000 --- a/docs/examples/opencensus-exporter-metrics/docker/prometheus.yaml +++ /dev/null @@ -1,5 +0,0 @@ -scrape_configs: - - job_name: 'otel-collector' - scrape_interval: 5s - static_configs: - - targets: ['otel-collector:8889'] diff --git a/docs/exporter/prometheus/prometheus.rst b/docs/exporter/prometheus/prometheus.rst deleted file mode 100644 index f4ad1a8245..0000000000 --- a/docs/exporter/prometheus/prometheus.rst +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry Prometheus Exporter -================================= - -.. automodule:: opentelemetry.exporter.prometheus - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/getting-started.rst b/docs/getting-started.rst index f2cd75717f..dce074e897 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -200,84 +200,16 @@ Following the installation of the package containing the b3 propagator, configur propagators.set_global_textmap(B3Format()) - -Add metrics --------------- - -Spans are a great way to get detailed information about what your application is doing, but -what about a more aggregated perspective? OpenTelemetry provides support for metrics. Metrics are a time series -of values that express things such as CPU utilization, request count for an HTTP server, or a -business metric such as transactions. - -You can annotate all metrics with labels. Labels are additional qualifiers that describe what -subdivision of the measurements the metric represents. - -The following example emits metrics to your console, similar to the trace example: - -.. literalinclude:: getting_started/metrics_example.py - :language: python - :lines: 15- - -The sleep functions cause the script to take a while, but it eventually yields the following output: - -.. code-block:: sh - - $ python metrics_example.py - ConsoleMetricsExporter(data="Counter(name="requests", description="number of requests")", labels="(('environment', 'staging'),)", value=25) - ConsoleMetricsExporter(data="Counter(name="requests", description="number of requests")", labels="(('environment', 'staging'),)", value=45) - -Use metrics with Prometheus ------------------------------- - -It's valuable to have a data store for metrics so you can visualize and query the data. A common solution is -`Prometheus `_, which provides a server to scrape and store time series data. - -Start by bringing up a Prometheus instance to scrape your application. Write the following configuration: - -.. code-block:: yaml - - # /tmp/prometheus.yml - scrape_configs: - - job_name: 'my-app' - scrape_interval: 5s - static_configs: - - targets: ['localhost:8000'] - -Then start a Docker container for the instance: - -.. code-block:: sh - - # --net=host will not work properly outside of Linux. - docker run --net=host -v /tmp/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus \ - --log.level=debug --config.file=/etc/prometheus/prometheus.yml - -Install an exporter specific to Prometheus for your Python application: - -.. code-block:: sh - - pip install opentelemetry-exporter-prometheus - - -Use that exporter instead of the `ConsoleMetricsExporter`: - -.. literalinclude:: getting_started/prometheus_example.py - :language: python - :lines: 15- - -The Prometheus server runs locally on port 8000. The instrumented code makes metrics available to Prometheus via the `PrometheusMetricsExporter`. -Visit the Prometheus UI (http://localhost:9090) to view your metrics. - - -Use the OpenTelemetry Collector for traces and metrics --------------------------------------------------------- +Use the OpenTelemetry Collector for traces +------------------------------------------ Although it's possible to directly export your telemetry data to specific backends, you might have more complex use cases such as the following: * A single telemetry sink shared by multiple services, to reduce overhead of switching exporters. -* Aggregaing metrics or traces across multiple services, running on multiple hosts. +* Aggregating traces across multiple services, running on multiple hosts. To enable a broad range of aggregation strategies, OpenTelemetry provides the `opentelemetry-collector `_. -The Collector is a flexible application that can consume trace and metric data and export to multiple other backends, including to another instance of the Collector. +The Collector is a flexible application that can consume trace data and export to multiple other backends, including to another instance of the Collector. Start the Collector locally to see how the Collector works in practice. Write the following file: @@ -299,9 +231,6 @@ Start the Collector locally to see how the Collector works in practice. Write th receivers: [opencensus] exporters: [logging] processors: [batch, queued_retry] - metrics: - receivers: [opencensus] - exporters: [logging] Then start the Docker container: diff --git a/docs/getting_started/metrics_example.py b/docs/getting_started/metrics_example.py deleted file mode 100644 index 9efcab6403..0000000000 --- a/docs/getting_started/metrics_example.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# metrics.py -import time - -from opentelemetry import metrics -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.controller import PushController - -metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__, True) -exporter = ConsoleMetricsExporter() -controller = PushController(meter, exporter, 5) - -staging_labels = {"environment": "staging"} - -requests_counter = meter.create_counter( - name="requests", - description="number of requests", - unit="1", - value_type=int, -) - -requests_counter.add(25, staging_labels) -time.sleep(5) - -requests_counter.add(20, staging_labels) -time.sleep(5) diff --git a/docs/getting_started/otlpcollector_example.py b/docs/getting_started/otlpcollector_example.py index 57440bda78..d31fdf0bd8 100644 --- a/docs/getting_started/otlpcollector_example.py +++ b/docs/getting_started/otlpcollector_example.py @@ -15,11 +15,8 @@ # otcollector.py import time -from opentelemetry import metrics, trace -from opentelemetry.exporter.otlp.metrics_exporter import OTLPMetricsExporter +from opentelemetry import trace from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export.controller import PushController from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor @@ -34,35 +31,8 @@ span_processor = BatchExportSpanProcessor(span_exporter) tracer_provider.add_span_processor(span_processor) -metric_exporter = OTLPMetricsExporter( - # optional - # endpoint:="myCollectorURL:55678", - # credentials=ChannelCredentials(credentials), - # headers=(("metadata", "metadata")), -) - -# Meter is responsible for creating and recording metrics -metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__) -# controller collects metrics created from meter and exports it via the -# exporter every interval -controller = PushController(meter, metric_exporter, 5) - # Configure the tracer to use the collector exporter tracer = trace.get_tracer_provider().get_tracer(__name__) with tracer.start_as_current_span("foo"): print("Hello world!") - -requests_counter = meter.create_counter( - name="requests", - description="number of requests", - unit="1", - value_type=int, -) -# Labels are used to identify key-values that are associated with a specific -# metric that you want to record. These are useful for pre-aggregation and can -# be used to store custom dimensions pertaining to a metric -labels = {"environment": "staging"} -requests_counter.add(25, labels) -time.sleep(10) # give push_controller time to push metrics diff --git a/docs/getting_started/prometheus_example.py b/docs/getting_started/prometheus_example.py deleted file mode 100644 index 7aba740bd7..0000000000 --- a/docs/getting_started/prometheus_example.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# prometheus.py -import sys -import time - -from prometheus_client import start_http_server - -from opentelemetry import metrics -from opentelemetry.exporter.prometheus import PrometheusMetricsExporter -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export.controller import PushController - -# Start Prometheus client -start_http_server(port=8000, addr="localhost") - -processor_mode = "stateful" -metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__, processor_mode == "stateful") -exporter = PrometheusMetricsExporter("MyAppPrefix") -controller = PushController(meter, exporter, 5) - -staging_labels = {"environment": "staging"} - -requests_counter = meter.create_counter( - name="requests", - description="number of requests", - unit="1", - value_type=int, -) - -requests_counter.add(25, staging_labels) -time.sleep(5) - -requests_counter.add(20, staging_labels) -time.sleep(5) - -# This line is added to keep the HTTP server up long enough to scrape. -input("Press any key to exit...") diff --git a/docs/getting_started/tests/test_metrics.py b/docs/getting_started/tests/test_metrics.py deleted file mode 100644 index a9d7dbe45b..0000000000 --- a/docs/getting_started/tests/test_metrics.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os -import subprocess -import sys -import unittest - - -class TestBasicMetricsExample(unittest.TestCase): - def test_basic_meter(self): - dirpath = os.path.dirname(os.path.realpath(__file__)) - test_script = "{}/../metrics_example.py".format(dirpath) - output = subprocess.check_output( - (sys.executable, test_script) - ).decode() - - self.assertIn('name="requests"', output) - self.assertIn('description="number of requests"', output) diff --git a/docs/sdk/metrics.export.aggregate.rst b/docs/sdk/metrics.export.aggregate.rst deleted file mode 100644 index 7c9306c684..0000000000 --- a/docs/sdk/metrics.export.aggregate.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.sdk.metrics.export.aggregate -========================================== - -.. automodule:: opentelemetry.sdk.metrics.export.aggregate - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/sdk/metrics.export.processor.rst b/docs/sdk/metrics.export.processor.rst deleted file mode 100644 index cdae8b2fbe..0000000000 --- a/docs/sdk/metrics.export.processor.rst +++ /dev/null @@ -1,11 +0,0 @@ -opentelemetry.sdk.metrics.export.processor -========================================== - -.. toctree:: - - metrics.export - -.. automodule:: opentelemetry.sdk.metrics.export.processor - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/sdk/metrics.export.rst b/docs/sdk/metrics.export.rst deleted file mode 100644 index 1ae51170e4..0000000000 --- a/docs/sdk/metrics.export.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.sdk.metrics.export -========================================== - -.. automodule:: opentelemetry.sdk.metrics.export - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/sdk/metrics.rst b/docs/sdk/metrics.rst deleted file mode 100644 index 8e34be5a4b..0000000000 --- a/docs/sdk/metrics.rst +++ /dev/null @@ -1,16 +0,0 @@ -opentelemetry.sdk.metrics package -========================================== - -Submodules ----------- - -.. toctree:: - - metrics.export.aggregate - metrics.export.processor - util.instrumentation - -.. automodule:: opentelemetry.sdk.metrics - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/sdk/sdk.rst b/docs/sdk/sdk.rst index 1c5653e1b3..333da1820b 100644 --- a/docs/sdk/sdk.rst +++ b/docs/sdk/sdk.rst @@ -6,7 +6,6 @@ OpenTelemetry Python SDK .. toctree:: :maxdepth: 1 - metrics resources trace error_handler diff --git a/docs/shim/instrumentation.rst b/docs/shim/instrumentation.rst index 16b82a2093..9c01b6b6f4 100644 --- a/docs/shim/instrumentation.rst +++ b/docs/shim/instrumentation.rst @@ -13,4 +13,3 @@ Submodules :maxdepth: 1 instrumentor - metric diff --git a/docs/shim/metric.rst b/docs/shim/metric.rst deleted file mode 100644 index 6a69eeeca5..0000000000 --- a/docs/shim/metric.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.instrumentation.metric package -============================================ - -.. automodule:: opentelemetry.instrumentation.metric - :members: - :undoc-members: - :show-inheritance: diff --git a/exporter/opentelemetry-exporter-opencensus/README.rst b/exporter/opentelemetry-exporter-opencensus/README.rst index 7f282d307e..f7b7f4fb2b 100644 --- a/exporter/opentelemetry-exporter-opencensus/README.rst +++ b/exporter/opentelemetry-exporter-opencensus/README.rst @@ -6,7 +6,7 @@ OpenCensus Exporter .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-opencensus.svg :target: https://pypi.org/project/opentelemetry-exporter-opencensus/ -This library allows to export traces and metrics using OpenCensus. +This library allows to export traces using OpenCensus. Installation ------------ diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/__init__.py index 27aae238f4..ff8bb25be6 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/__init__.py @@ -13,6 +13,5 @@ # limitations under the License. """ -The **OpenCensus Exporter** allows to export traces and metrics using -OpenCensus. +The **OpenCensus Exporter** allows to export traces using OpenCensus. """ diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py deleted file mode 100644 index 979600173a..0000000000 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/metrics_exporter/__init__.py +++ /dev/null @@ -1,209 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""OpenCensus Collector Metrics Exporter.""" - -import logging -from typing import Dict, Sequence - -import grpc -from google.protobuf.timestamp_pb2 import Timestamp -from opencensus.proto.agent.metrics.v1 import ( - metrics_service_pb2, - metrics_service_pb2_grpc, -) -from opencensus.proto.metrics.v1 import metrics_pb2 -from opencensus.proto.resource.v1 import resource_pb2 - -import opentelemetry.exporter.opencensus.util as utils -from opentelemetry.sdk.metrics import Counter, Metric -from opentelemetry.sdk.metrics.export import ( - ExportRecord, - MetricsExporter, - MetricsExportResult, -) - -DEFAULT_ENDPOINT = "localhost:55678" - -# In priority order. See collector impl https://bit.ly/2DvJW6y -_OT_LABEL_PRESENCE_TO_RESOURCE_TYPE = ( - ("container.name", "container"), - ("k8s.pod.name", "k8s"), - ("host.name", "host"), - ("cloud.provider", "cloud"), -) - -logger = logging.getLogger(__name__) - - -# pylint: disable=no-member -class OpenCensusMetricsExporter(MetricsExporter): - """OpenCensus metrics exporter. - - Args: - endpoint: OpenCensus Collector receiver endpoint. - service_name: Name of Collector service. - host_name: Host name. - client: MetricsService client stub. - """ - - def __init__( - self, - endpoint: str = DEFAULT_ENDPOINT, - service_name: str = None, - host_name: str = None, - client: metrics_service_pb2_grpc.MetricsServiceStub = None, - ): - self.endpoint = endpoint - if client is None: - channel = grpc.insecure_channel(self.endpoint) - self.client = metrics_service_pb2_grpc.MetricsServiceStub( - channel=channel - ) - else: - self.client = client - - self.node = utils.get_node(service_name, host_name) - self.exporter_start_timestamp = Timestamp() - self.exporter_start_timestamp.GetCurrentTime() - - def export( - self, export_records: Sequence[ExportRecord] - ) -> MetricsExportResult: - try: - responses = self.client.Export( - self.generate_metrics_requests(export_records) - ) - - # Read response - for _ in responses: - pass - - except grpc.RpcError: - return MetricsExportResult.FAILURE - - return MetricsExportResult.SUCCESS - - def shutdown(self) -> None: - pass - - def generate_metrics_requests( - self, metrics: Sequence[ExportRecord] - ) -> metrics_service_pb2.ExportMetricsServiceRequest: - collector_metrics = translate_to_collector( - metrics, self.exporter_start_timestamp - ) - service_request = metrics_service_pb2.ExportMetricsServiceRequest( - node=self.node, metrics=collector_metrics - ) - yield service_request - - -# pylint: disable=too-many-branches -def translate_to_collector( - export_records: Sequence[ExportRecord], - exporter_start_timestamp: Timestamp, -) -> Sequence[metrics_pb2.Metric]: - collector_metrics = [] - for export_record in export_records: - - label_values = [] - label_keys = [] - for label_tuple in export_record.labels: - label_keys.append(metrics_pb2.LabelKey(key=label_tuple[0])) - label_values.append( - metrics_pb2.LabelValue( - has_value=label_tuple[1] is not None, - value=str(label_tuple[1]), - ) - ) - - metric_descriptor = metrics_pb2.MetricDescriptor( - name=export_record.instrument.name, - description=export_record.instrument.description, - unit=export_record.instrument.unit, - type=get_collector_metric_type(export_record.instrument), - label_keys=label_keys, - ) - - # If cumulative and stateful, explicitly set the start_timestamp to - # exporter start time. - if export_record.instrument.meter.processor.stateful: - start_timestamp = exporter_start_timestamp - else: - start_timestamp = None - - timeseries = metrics_pb2.TimeSeries( - label_values=label_values, - points=[get_collector_point(export_record)], - start_timestamp=start_timestamp, - ) - collector_metrics.append( - metrics_pb2.Metric( - metric_descriptor=metric_descriptor, - timeseries=[timeseries], - resource=get_resource(export_record), - ) - ) - return collector_metrics - - -# pylint: disable=no-else-return -def get_collector_metric_type(metric: Metric) -> metrics_pb2.MetricDescriptor: - if isinstance(metric, Counter): - if metric.value_type == int: - return metrics_pb2.MetricDescriptor.CUMULATIVE_INT64 - elif metric.value_type == float: - return metrics_pb2.MetricDescriptor.CUMULATIVE_DOUBLE - return metrics_pb2.MetricDescriptor.UNSPECIFIED - - -def get_collector_point(export_record: ExportRecord) -> metrics_pb2.Point: - # TODO: horrible hack to get original list of keys to then get the bound - # instrument - point = metrics_pb2.Point( - timestamp=utils.proto_timestamp_from_time_ns( - export_record.aggregator.last_update_timestamp - ) - ) - if export_record.instrument.value_type == int: - point.int64_value = export_record.aggregator.checkpoint - elif export_record.instrument.value_type == float: - point.double_value = export_record.aggregator.checkpoint - else: - raise TypeError( - "Unsupported metric type: {}".format( - export_record.instrument.value_type - ) - ) - return point - - -def get_resource(export_record: ExportRecord) -> resource_pb2.Resource: - resource_attributes = export_record.resource.attributes - return resource_pb2.Resource( - type=infer_oc_resource_type(resource_attributes), - labels={k: str(v) for k, v in resource_attributes.items()}, - ) - - -def infer_oc_resource_type(resource_attributes: Dict[str, str]) -> str: - """Convert from OT resource labels to OC resource type""" - for ( - ot_resource_key, - oc_resource_type, - ) in _OT_LABEL_PRESENCE_TO_RESOURCE_TYPE: - if ot_resource_key in resource_attributes: - return oc_resource_type - return "" diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py deleted file mode 100644 index d949c6cd33..0000000000 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_metrics_exporter.py +++ /dev/null @@ -1,311 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -from unittest import mock - -import grpc -from google.protobuf.timestamp_pb2 import Timestamp -from opencensus.proto.metrics.v1 import metrics_pb2 - -from opentelemetry import metrics -from opentelemetry.exporter.opencensus import metrics_exporter -from opentelemetry.sdk.metrics import ( - Counter, - MeterProvider, - ValueRecorder, - get_dict_as_key, -) -from opentelemetry.sdk.metrics.export import ( - ExportRecord, - MetricsExportResult, - aggregate, -) -from opentelemetry.sdk.resources import Resource - - -# pylint: disable=no-member -class TestCollectorMetricsExporter(unittest.TestCase): - @classmethod - def setUpClass(cls): - # pylint: disable=protected-access - cls._resource_labels = { - "key_with_str_value": "some string", - "key_with_int_val": 321, - "key_with_true": True, - } - metrics.set_meter_provider( - MeterProvider(resource=Resource(cls._resource_labels)) - ) - cls._meter = metrics.get_meter(__name__) - cls._labels = {"environment": "staging", "number": 321} - cls._key_labels = get_dict_as_key(cls._labels) - - def test_constructor(self): - mock_get_node = mock.Mock() - patch = mock.patch( - "opentelemetry.exporter.opencensus.util.get_node", - side_effect=mock_get_node, - ) - service_name = "testServiceName" - host_name = "testHostName" - client = grpc.insecure_channel("") - endpoint = "testEndpoint" - with patch: - exporter = metrics_exporter.OpenCensusMetricsExporter( - service_name=service_name, - host_name=host_name, - endpoint=endpoint, - client=client, - ) - - self.assertIs(exporter.client, client) - self.assertEqual(exporter.endpoint, endpoint) - mock_get_node.assert_called_with(service_name, host_name) - - def test_get_collector_metric_type(self): - result = metrics_exporter.get_collector_metric_type( - Counter("testName", "testDescription", "unit", int, None) - ) - self.assertIs(result, metrics_pb2.MetricDescriptor.CUMULATIVE_INT64) - result = metrics_exporter.get_collector_metric_type( - Counter("testName", "testDescription", "unit", float, None) - ) - self.assertIs(result, metrics_pb2.MetricDescriptor.CUMULATIVE_DOUBLE) - result = metrics_exporter.get_collector_metric_type( - ValueRecorder("testName", "testDescription", "unit", None, None) - ) - self.assertIs(result, metrics_pb2.MetricDescriptor.UNSPECIFIED) - - def test_get_collector_point(self): - aggregator = aggregate.SumAggregator() - int_counter = self._meter.create_counter( - "testNameIntCounter", "testDescription", "unit", int, - ) - float_counter = self._meter.create_counter( - "testNameFloatCounter", "testDescription", "unit", float, - ) - valuerecorder = self._meter.create_valuerecorder( - "testNameValueRecorder", "testDescription", "unit", float, - ) - result = metrics_exporter.get_collector_point( - ExportRecord( - int_counter, - self._key_labels, - aggregator, - metrics.get_meter_provider().resource, - ) - ) - self.assertIsInstance(result, metrics_pb2.Point) - self.assertIsInstance(result.timestamp, Timestamp) - self.assertEqual(result.int64_value, 0) - aggregator.update(123.5) - aggregator.take_checkpoint() - result = metrics_exporter.get_collector_point( - ExportRecord( - float_counter, - self._key_labels, - aggregator, - metrics.get_meter_provider().resource, - ) - ) - self.assertEqual(result.double_value, 123.5) - self.assertRaises( - TypeError, - metrics_exporter.get_collector_point( - ExportRecord( - valuerecorder, - self._key_labels, - aggregator, - metrics.get_meter_provider().resource, - ) - ), - ) - - def test_export(self): - mock_client = mock.MagicMock() - mock_export = mock.MagicMock() - mock_client.Export = mock_export - host_name = "testHostName" - collector_exporter = metrics_exporter.OpenCensusMetricsExporter( - client=mock_client, host_name=host_name - ) - test_metric = self._meter.create_counter( - "testname", "testdesc", "unit", int, - ) - record = ExportRecord( - test_metric, - self._key_labels, - aggregate.SumAggregator(), - metrics.get_meter_provider().resource, - ) - - result = collector_exporter.export([record]) - self.assertIs(result, MetricsExportResult.SUCCESS) - # pylint: disable=unsubscriptable-object - export_arg = mock_export.call_args[0] - service_request = next(export_arg[0]) - output_metrics = getattr(service_request, "metrics") - output_node = getattr(service_request, "node") - self.assertEqual(len(output_metrics), 1) - self.assertIsNotNone(getattr(output_node, "library_info")) - self.assertIsNotNone(getattr(output_node, "service_info")) - output_identifier = getattr(output_node, "identifier") - self.assertEqual( - getattr(output_identifier, "host_name"), "testHostName" - ) - - def test_translate_to_collector(self): - test_metric = self._meter.create_counter( - "testcollector", "testdesc", "unit", int, - ) - aggregator = aggregate.SumAggregator() - aggregator.update(123) - aggregator.take_checkpoint() - record = ExportRecord( - test_metric, - self._key_labels, - aggregator, - metrics.get_meter_provider().resource, - ) - start_timestamp = Timestamp() - output_metrics = metrics_exporter.translate_to_collector( - [record], start_timestamp, - ) - self.assertEqual(len(output_metrics), 1) - self.assertIsInstance(output_metrics[0], metrics_pb2.Metric) - self.assertEqual( - output_metrics[0].metric_descriptor.name, "testcollector" - ) - self.assertEqual( - output_metrics[0].metric_descriptor.description, "testdesc" - ) - self.assertEqual(output_metrics[0].metric_descriptor.unit, "unit") - self.assertEqual( - output_metrics[0].metric_descriptor.type, - metrics_pb2.MetricDescriptor.CUMULATIVE_INT64, - ) - self.assertEqual( - len(output_metrics[0].metric_descriptor.label_keys), 2 - ) - self.assertEqual( - output_metrics[0].metric_descriptor.label_keys[0].key, - "environment", - ) - self.assertEqual( - output_metrics[0].metric_descriptor.label_keys[1].key, "number", - ) - - self.assertIsNotNone(output_metrics[0].resource) - self.assertEqual( - output_metrics[0].resource.type, "", - ) - self.assertEqual( - output_metrics[0].resource.labels["key_with_str_value"], - self._resource_labels["key_with_str_value"], - ) - self.assertIsInstance( - output_metrics[0].resource.labels["key_with_int_val"], str, - ) - self.assertEqual( - output_metrics[0].resource.labels["key_with_int_val"], - str(self._resource_labels["key_with_int_val"]), - ) - self.assertIsInstance( - output_metrics[0].resource.labels["key_with_true"], str, - ) - self.assertEqual( - output_metrics[0].resource.labels["key_with_true"], - str(self._resource_labels["key_with_true"]), - ) - - self.assertEqual(len(output_metrics[0].timeseries), 1) - self.assertEqual(len(output_metrics[0].timeseries[0].label_values), 2) - self.assertEqual( - output_metrics[0].timeseries[0].start_timestamp, start_timestamp - ) - self.assertEqual( - output_metrics[0].timeseries[0].label_values[0].has_value, True - ) - self.assertEqual( - output_metrics[0].timeseries[0].label_values[0].value, "staging" - ) - self.assertEqual(len(output_metrics[0].timeseries[0].points), 1) - self.assertEqual( - output_metrics[0].timeseries[0].points[0].timestamp.seconds, - record.aggregator.last_update_timestamp // 1000000000, - ) - self.assertEqual( - output_metrics[0].timeseries[0].points[0].timestamp.nanos, - record.aggregator.last_update_timestamp % 1000000000, - ) - self.assertEqual( - output_metrics[0].timeseries[0].points[0].int64_value, 123 - ) - - def test_infer_ot_resource_type(self): - # empty resource - self.assertEqual(metrics_exporter.infer_oc_resource_type({}), "") - - # container - self.assertEqual( - metrics_exporter.infer_oc_resource_type( - { - "k8s.cluster.name": "cluster1", - "k8s.pod.name": "pod1", - "k8s.namespace.name": "namespace1", - "container.name": "container-name1", - "cloud.account.id": "proj1", - "cloud.zone": "zone1", - } - ), - "container", - ) - - # k8s pod - self.assertEqual( - metrics_exporter.infer_oc_resource_type( - { - "k8s.cluster.name": "cluster1", - "k8s.pod.name": "pod1", - "k8s.namespace.name": "namespace1", - "cloud.zone": "zone1", - } - ), - "k8s", - ) - - # host - self.assertEqual( - metrics_exporter.infer_oc_resource_type( - { - "k8s.cluster.name": "cluster1", - "cloud.zone": "zone1", - "host.name": "node1", - } - ), - "host", - ) - - # cloud - self.assertEqual( - metrics_exporter.infer_oc_resource_type( - { - "cloud.provider": "gcp", - "host.id": "inst1", - "cloud.zone": "zone1", - } - ), - "cloud", - ) diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 23eb724df4..ef8386d944 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -57,4 +57,3 @@ where = src [options.entry_points] opentelemetry_exporter = otlp_span = opentelemetry.exporter.otlp.trace_exporter:OTLPSpanExporter - otlp_metric = opentelemetry.exporter.otlp.metrics_exporter:OTLPMetricsExporter \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index 6d54970f19..06f0c9e6c7 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -142,7 +142,7 @@ def _load_credential_from_file(filepath) -> ChannelCredentials: class OTLPExporterMixin( ABC, Generic[SDKDataT, ExportServiceRequestT, ExportResultT] ): - """OTLP span/metric exporter + """OTLP span exporter Args: endpoint: OpenTelemetry Collector receiver endpoint diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py deleted file mode 100644 index 8e388e9f9e..0000000000 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py +++ /dev/null @@ -1,356 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""OTLP Metrics Exporter""" - -import logging -from os import environ -from typing import List, Optional, Sequence, Type, TypeVar - -from grpc import ChannelCredentials - -from opentelemetry.exporter.otlp.exporter import ( - OTLPExporterMixin, - _get_resource_data, - _load_credential_from_file, -) -from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( - ExportMetricsServiceRequest, -) -from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( - MetricsServiceStub, -) -from opentelemetry.proto.common.v1.common_pb2 import StringKeyValue -from opentelemetry.proto.metrics.v1.metrics_pb2 import ( - AggregationTemporality, - DoubleDataPoint, - DoubleGauge, - DoubleSum, - InstrumentationLibraryMetrics, - IntDataPoint, - IntGauge, - IntSum, -) -from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric as OTLPMetric -from opentelemetry.proto.metrics.v1.metrics_pb2 import ResourceMetrics -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE, - OTEL_EXPORTER_OTLP_METRIC_ENDPOINT, - OTEL_EXPORTER_OTLP_METRIC_HEADERS, - OTEL_EXPORTER_OTLP_METRIC_INSECURE, - OTEL_EXPORTER_OTLP_METRIC_TIMEOUT, -) -from opentelemetry.sdk.metrics import ( - Counter, - SumObserver, - UpDownCounter, - UpDownSumObserver, - ValueObserver, - ValueRecorder, -) -from opentelemetry.sdk.metrics.export import ( - ExportRecord, - MetricsExporter, - MetricsExportResult, -) -from opentelemetry.sdk.metrics.export.aggregate import ( - HistogramAggregator, - LastValueAggregator, - MinMaxSumCountAggregator, - SumAggregator, - ValueObserverAggregator, -) - -logger = logging.getLogger(__name__) -DataPointT = TypeVar("DataPointT", IntDataPoint, DoubleDataPoint) - - -def _get_data_points( - export_record: ExportRecord, - data_point_class: Type[DataPointT], - aggregation_temporality: int, -) -> List[DataPointT]: - - if isinstance(export_record.aggregator, SumAggregator): - value = export_record.aggregator.checkpoint - - elif isinstance(export_record.aggregator, MinMaxSumCountAggregator): - # FIXME: How are values to be interpreted from this aggregator? - raise Exception("MinMaxSumCount aggregator data not supported") - - elif isinstance(export_record.aggregator, HistogramAggregator): - # FIXME: How are values to be interpreted from this aggregator? - raise Exception("Histogram aggregator data not supported") - - elif isinstance(export_record.aggregator, LastValueAggregator): - value = export_record.aggregator.checkpoint - - elif isinstance(export_record.aggregator, ValueObserverAggregator): - value = export_record.aggregator.checkpoint.last - - if aggregation_temporality == ( - AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE - ): - start_time_unix_nano = export_record.aggregator.first_timestamp - else: - start_time_unix_nano = ( - export_record.aggregator.initial_checkpoint_timestamp - ) - - return [ - data_point_class( - labels=[ - StringKeyValue(key=str(label_key), value=str(label_value)) - for label_key, label_value in export_record.labels - ], - value=value, - start_time_unix_nano=start_time_unix_nano, - time_unix_nano=(export_record.aggregator.last_update_timestamp), - ) - ] - - -class OTLPMetricsExporter( - MetricsExporter, - OTLPExporterMixin[ - ExportRecord, ExportMetricsServiceRequest, MetricsExportResult - ], -): - # pylint: disable=unsubscriptable-object - """OTLP metrics exporter - - Args: - endpoint: OpenTelemetry Collector receiver endpoint - insecure: Connection type - credentials: Credentials object for server authentication - headers: Headers to send when exporting - timeout: Backend request timeout in seconds - """ - - _stub = MetricsServiceStub - _result = MetricsExportResult - - def __init__( - self, - endpoint: Optional[str] = None, - insecure: Optional[bool] = None, - credentials: Optional[ChannelCredentials] = None, - headers: Optional[Sequence] = None, - timeout: Optional[int] = None, - ): - if insecure is None: - insecure = environ.get(OTEL_EXPORTER_OTLP_METRIC_INSECURE) - - if ( - not insecure - and environ.get(OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE) is not None - ): - credentials = credentials or _load_credential_from_file( - environ.get(OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE) - ) - - environ_timeout = environ.get(OTEL_EXPORTER_OTLP_METRIC_TIMEOUT) - environ_timeout = ( - int(environ_timeout) if environ_timeout is not None else None - ) - - super().__init__( - **{ - "endpoint": endpoint - or environ.get(OTEL_EXPORTER_OTLP_METRIC_ENDPOINT), - "insecure": insecure, - "credentials": credentials, - "headers": headers - or environ.get(OTEL_EXPORTER_OTLP_METRIC_HEADERS), - "timeout": timeout or environ_timeout, - } - ) - - # pylint: disable=no-self-use - def _translate_data( - self, export_records: Sequence[ExportRecord] - ) -> ExportMetricsServiceRequest: - # pylint: disable=too-many-locals,no-member - # pylint: disable=attribute-defined-outside-init - - sdk_resource_instrumentation_library_metrics = {} - - # The criteria to decide how to translate export_records is based on this table - # taken directly from OpenTelemetry Proto v0.5.0: - - # TODO: Update table after the decision on: - # https://github.com/open-telemetry/opentelemetry-specification/issues/731. - # By default, metrics recording using the OpenTelemetry API are exported as - # (the table does not include MeasurementValueType to avoid extra rows): - # - # Instrument Type - # ---------------------------------------------- - # Counter Sum(aggregation_temporality=delta;is_monotonic=true) - # UpDownCounter Sum(aggregation_temporality=delta;is_monotonic=false) - # ValueRecorder TBD - # SumObserver Sum(aggregation_temporality=cumulative;is_monotonic=true) - # UpDownSumObserver Sum(aggregation_temporality=cumulative;is_monotonic=false) - # ValueObserver Gauge() - for export_record in export_records: - - if export_record.resource not in ( - sdk_resource_instrumentation_library_metrics.keys() - ): - sdk_resource_instrumentation_library_metrics[ - export_record.resource - ] = InstrumentationLibraryMetrics() - - type_class = { - int: { - "sum": {"class": IntSum, "argument": "int_sum"}, - "gauge": {"class": IntGauge, "argument": "int_gauge"}, - "data_point_class": IntDataPoint, - }, - float: { - "sum": {"class": DoubleSum, "argument": "double_sum"}, - "gauge": { - "class": DoubleGauge, - "argument": "double_gauge", - }, - "data_point_class": DoubleDataPoint, - }, - } - - value_type = export_record.instrument.value_type - - sum_class = type_class[value_type]["sum"]["class"] - gauge_class = type_class[value_type]["gauge"]["class"] - data_point_class = type_class[value_type]["data_point_class"] - - if isinstance(export_record.instrument, Counter): - - aggregation_temporality = ( - AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE - ) - - otlp_metric_data = sum_class( - data_points=_get_data_points( - export_record, - data_point_class, - aggregation_temporality, - ), - aggregation_temporality=aggregation_temporality, - is_monotonic=True, - ) - argument = type_class[value_type]["sum"]["argument"] - - elif isinstance(export_record.instrument, UpDownCounter): - - aggregation_temporality = ( - AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE - ) - - otlp_metric_data = sum_class( - data_points=_get_data_points( - export_record, - data_point_class, - aggregation_temporality, - ), - aggregation_temporality=aggregation_temporality, - is_monotonic=False, - ) - argument = type_class[value_type]["sum"]["argument"] - - elif isinstance(export_record.instrument, (ValueRecorder)): - logger.warning("Skipping exporting of ValueRecorder metric") - continue - - elif isinstance(export_record.instrument, SumObserver): - - aggregation_temporality = ( - AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE - ) - - otlp_metric_data = sum_class( - data_points=_get_data_points( - export_record, - data_point_class, - aggregation_temporality, - ), - aggregation_temporality=aggregation_temporality, - is_monotonic=True, - ) - argument = type_class[value_type]["sum"]["argument"] - - elif isinstance(export_record.instrument, UpDownSumObserver): - - aggregation_temporality = ( - AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE - ) - - otlp_metric_data = sum_class( - data_points=_get_data_points( - export_record, - data_point_class, - aggregation_temporality, - ), - aggregation_temporality=aggregation_temporality, - is_monotonic=False, - ) - argument = type_class[value_type]["sum"]["argument"] - - elif isinstance(export_record.instrument, (ValueObserver)): - otlp_metric_data = gauge_class( - data_points=_get_data_points( - export_record, - data_point_class, - AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA, - ) - ) - argument = type_class[value_type]["gauge"]["argument"] - - instrumentation_library_metrics = sdk_resource_instrumentation_library_metrics[ - export_record.resource - ] - - instrumentation_library_metrics.metrics.append( - OTLPMetric( - **{ - "name": export_record.instrument.name, - "description": (export_record.instrument.description), - "unit": export_record.instrument.unit, - argument: otlp_metric_data, - } - ) - ) - - instrumentation_library_metrics.instrumentation_library.name = ( - export_record.instrument.meter.instrumentation_info.name - ) - - version = ( - export_record.instrument.meter.instrumentation_info.version - ) - - if version: - ( - instrumentation_library_metrics.instrumentation_library.version - ) = version - - return ExportMetricsServiceRequest( - resource_metrics=_get_resource_data( - sdk_resource_instrumentation_library_metrics, - ResourceMetrics, - "metrics", - ) - ) - - def export(self, metrics: Sequence[ExportRecord]) -> MetricsExportResult: - # pylint: disable=arguments-differ - return self._export(metrics) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py deleted file mode 100644 index 841dcff03e..0000000000 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py +++ /dev/null @@ -1,397 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -from collections import OrderedDict -from unittest import TestCase -from unittest.mock import Mock, patch - -from grpc import ChannelCredentials - -from opentelemetry.exporter.otlp.metrics_exporter import OTLPMetricsExporter -from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( - ExportMetricsServiceRequest, -) -from opentelemetry.proto.common.v1.common_pb2 import ( - AnyValue, - InstrumentationLibrary, - KeyValue, - StringKeyValue, -) -from opentelemetry.proto.metrics.v1.metrics_pb2 import ( - AggregationTemporality, - InstrumentationLibraryMetrics, - IntDataPoint, - IntSum, -) -from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric as OTLPMetric -from opentelemetry.proto.metrics.v1.metrics_pb2 import ResourceMetrics -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as OTLPResource, -) -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE, - OTEL_EXPORTER_OTLP_METRIC_ENDPOINT, - OTEL_EXPORTER_OTLP_METRIC_HEADERS, - OTEL_EXPORTER_OTLP_METRIC_TIMEOUT, -) -from opentelemetry.sdk.metrics import ( - Counter, - MeterProvider, - SumObserver, - UpDownCounter, - UpDownSumObserver, -) -from opentelemetry.sdk.metrics.export import ExportRecord -from opentelemetry.sdk.metrics.export.aggregate import SumAggregator -from opentelemetry.sdk.resources import Resource as SDKResource - -THIS_DIR = os.path.dirname(__file__) - - -class TestOTLPMetricExporter(TestCase): - def setUp(self): # pylint: disable=arguments-differ - self.exporter = OTLPMetricsExporter(insecure=True) - self.resource = SDKResource(OrderedDict([("a", 1), ("b", False)])) - self.meter = MeterProvider(resource=self.resource,).get_meter( - "name", "version" - ) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_METRIC_ENDPOINT: "collector:4317", - OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE: THIS_DIR - + "/fixtures/test.cert", - OTEL_EXPORTER_OTLP_METRIC_HEADERS: "key1=value1,key2=value2", - OTEL_EXPORTER_OTLP_METRIC_TIMEOUT: "10", - }, - ) - @patch("opentelemetry.exporter.otlp.exporter.OTLPExporterMixin.__init__") - def test_env_variables(self, mock_exporter_mixin): - OTLPMetricsExporter() - - self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) - _, kwargs = mock_exporter_mixin.call_args_list[0] - - self.assertEqual(kwargs["endpoint"], "collector:4317") - self.assertEqual(kwargs["headers"], "key1=value1,key2=value2") - self.assertEqual(kwargs["timeout"], 10) - self.assertIsNotNone(kwargs["credentials"]) - self.assertIsInstance(kwargs["credentials"], ChannelCredentials) - - @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") - @patch("opentelemetry.exporter.otlp.exporter.secure_channel") - @patch( - "opentelemetry.exporter.otlp.metrics_exporter.OTLPMetricsExporter._stub" - ) - # pylint: disable=unused-argument - def test_no_credentials_error( - self, mock_ssl_channel, mock_secure, mock_stub - ): - OTLPMetricsExporter(insecure=False) - self.assertTrue(mock_ssl_channel.called) - - @patch.dict( - "os.environ", - {OTEL_EXPORTER_OTLP_METRIC_HEADERS: "key1=value1,key2=value2"}, - ) - @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") - @patch("opentelemetry.exporter.otlp.exporter.secure_channel") - # pylint: disable=unused-argument - def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): - exporter = OTLPMetricsExporter() - # pylint: disable=protected-access - self.assertEqual( - exporter._headers, (("key1", "value1"), ("key2", "value2")), - ) - exporter = OTLPMetricsExporter( - headers=(("key3", "value3"), ("key4", "value4")) - ) - # pylint: disable=protected-access - self.assertEqual( - exporter._headers, (("key3", "value3"), ("key4", "value4")), - ) - - @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") - @patch("opentelemetry.exporter.otlp.exporter.secure_channel") - # pylint: disable=unused-argument - def test_otlp_headers(self, mock_ssl_channel, mock_secure): - exporter = OTLPMetricsExporter() - # pylint: disable=protected-access - self.assertIsNone(exporter._headers, None) - - @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") - def test_translate_counter_export_record(self, mock_time_ns): - mock_time_ns.configure_mock(**{"return_value": 1}) - - counter_export_record = ExportRecord( - Counter("c", "d", "e", int, self.meter, ("f",),), - [("g", "h")], - SumAggregator(), - self.resource, - ) - - counter_export_record.aggregator.checkpoint = 1 - counter_export_record.aggregator.initial_checkpoint_timestamp = 1 - counter_export_record.aggregator.last_update_timestamp = 1 - - expected = ExportMetricsServiceRequest( - resource_metrics=[ - ResourceMetrics( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - instrumentation_library_metrics=[ - InstrumentationLibraryMetrics( - instrumentation_library=InstrumentationLibrary( - name="name", version="version", - ), - metrics=[ - OTLPMetric( - name="c", - description="d", - unit="e", - int_sum=IntSum( - data_points=[ - IntDataPoint( - labels=[ - StringKeyValue( - key="g", value="h" - ) - ], - value=1, - time_unix_nano=1, - start_time_unix_nano=1, - ) - ], - aggregation_temporality=( - AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE - ), - is_monotonic=True, - ), - ) - ], - ) - ], - ) - ] - ) - - # pylint: disable=protected-access - actual = self.exporter._translate_data([counter_export_record]) - - self.assertEqual(expected, actual) - - @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") - def test_translate_sum_observer_export_record(self, mock_time_ns): - mock_time_ns.configure_mock(**{"return_value": 1}) - counter_export_record = ExportRecord( - SumObserver(Mock(), "c", "d", "e", int, self.meter, ("f",),), - [("g", "h")], - SumAggregator(), - self.resource, - ) - - counter_export_record.aggregator.checkpoint = 1 - counter_export_record.aggregator.initial_checkpoint_timestamp = 1 - counter_export_record.aggregator.last_update_timestamp = 1 - - expected = ExportMetricsServiceRequest( - resource_metrics=[ - ResourceMetrics( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - instrumentation_library_metrics=[ - InstrumentationLibraryMetrics( - instrumentation_library=InstrumentationLibrary( - name="name", version="version", - ), - metrics=[ - OTLPMetric( - name="c", - description="d", - unit="e", - int_sum=IntSum( - data_points=[ - IntDataPoint( - labels=[ - StringKeyValue( - key="g", value="h" - ) - ], - value=1, - time_unix_nano=1, - start_time_unix_nano=1, - ) - ], - aggregation_temporality=( - AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE - ), - is_monotonic=True, - ), - ) - ], - ) - ], - ) - ] - ) - - # pylint: disable=protected-access - actual = self.exporter._translate_data([counter_export_record]) - - self.assertEqual(expected, actual) - - @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") - def test_translate_updowncounter_export_record(self, mock_time_ns): - mock_time_ns.configure_mock(**{"return_value": 1}) - - counter_export_record = ExportRecord( - UpDownCounter("c", "d", "e", int, self.meter), - [("g", "h")], - SumAggregator(), - self.resource, - ) - - counter_export_record.aggregator.checkpoint = 1 - counter_export_record.aggregator.initial_checkpoint_timestamp = 1 - counter_export_record.aggregator.last_update_timestamp = 1 - - expected = ExportMetricsServiceRequest( - resource_metrics=[ - ResourceMetrics( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - instrumentation_library_metrics=[ - InstrumentationLibraryMetrics( - instrumentation_library=InstrumentationLibrary( - name="name", version="version", - ), - metrics=[ - OTLPMetric( - name="c", - description="d", - unit="e", - int_sum=IntSum( - data_points=[ - IntDataPoint( - labels=[ - StringKeyValue( - key="g", value="h" - ) - ], - value=1, - time_unix_nano=1, - start_time_unix_nano=1, - ) - ], - aggregation_temporality=( - AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE - ), - ), - ) - ], - ) - ], - ) - ] - ) - - # pylint: disable=protected-access - actual = self.exporter._translate_data([counter_export_record]) - - self.assertEqual(expected, actual) - - @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") - def test_translate_updownsum_observer_export_record(self, mock_time_ns): - mock_time_ns.configure_mock(**{"return_value": 1}) - counter_export_record = ExportRecord( - UpDownSumObserver(Mock(), "c", "d", "e", int, self.meter, ("f",),), - [("g", "h")], - SumAggregator(), - self.resource, - ) - - counter_export_record.aggregator.checkpoint = 1 - counter_export_record.aggregator.initial_checkpoint_timestamp = 1 - counter_export_record.aggregator.last_update_timestamp = 1 - - expected = ExportMetricsServiceRequest( - resource_metrics=[ - ResourceMetrics( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - instrumentation_library_metrics=[ - InstrumentationLibraryMetrics( - instrumentation_library=InstrumentationLibrary( - name="name", version="version", - ), - metrics=[ - OTLPMetric( - name="c", - description="d", - unit="e", - int_sum=IntSum( - data_points=[ - IntDataPoint( - labels=[ - StringKeyValue( - key="g", value="h" - ) - ], - value=1, - time_unix_nano=1, - start_time_unix_nano=1, - ) - ], - aggregation_temporality=( - AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE - ), - ), - ) - ], - ) - ], - ) - ] - ) - - # pylint: disable=protected-access - actual = self.exporter._translate_data([counter_export_record]) - - self.assertEqual(expected, actual) diff --git a/exporter/opentelemetry-exporter-prometheus/LICENSE b/exporter/opentelemetry-exporter-prometheus/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/exporter/opentelemetry-exporter-prometheus/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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. diff --git a/exporter/opentelemetry-exporter-prometheus/MANIFEST.in b/exporter/opentelemetry-exporter-prometheus/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/exporter/opentelemetry-exporter-prometheus/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/exporter/opentelemetry-exporter-prometheus/README.rst b/exporter/opentelemetry-exporter-prometheus/README.rst deleted file mode 100644 index a3eb920000..0000000000 --- a/exporter/opentelemetry-exporter-prometheus/README.rst +++ /dev/null @@ -1,23 +0,0 @@ -OpenTelemetry Prometheus Exporter -================================= - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-prometheus.svg - :target: https://pypi.org/project/opentelemetry-exporter-prometheus/ - -This library allows to export metrics data to `Prometheus `_. - -Installation ------------- - -:: - - pip install opentelemetry-exporter-prometheus - -References ----------- - -* `OpenTelemetry Prometheus Exporter `_ -* `Prometheus `_ -* `OpenTelemetry Project `_ diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg deleted file mode 100644 index f8f463c257..0000000000 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-prometheus -description = Prometheus Metric Exporter for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-prometheus -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - -[options] -python_requires = >=3.5 -package_dir= - =src -packages=find_namespace: -install_requires = - prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api == 0.18.dev0 - opentelemetry-sdk == 0.18.dev0 - -[options.packages.find] -where = src - -[options.extras_require] -test = - -[options.entry_points] -opentelemetry_exporter = - prometheus = opentelemetry.exporter.prometheus:PrometheusMetricsExporter \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-prometheus/setup.py b/exporter/opentelemetry-exporter-prometheus/setup.py deleted file mode 100644 index 86067a2bd5..0000000000 --- a/exporter/opentelemetry-exporter-prometheus/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "exporter", "prometheus", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py deleted file mode 100644 index 760c54d3c8..0000000000 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This library allows export of metrics data to `Prometheus `_. - -Usage ------ - -The **OpenTelemetry Prometheus Exporter** allows export of `OpenTelemetry`_ metrics to `Prometheus`_. - - -.. _Prometheus: https://prometheus.io/ -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ - -.. code:: python - - from opentelemetry import metrics - from opentelemetry.exporter.prometheus import PrometheusMetricsExporter - from opentelemetry.sdk.metrics import Meter - from prometheus_client import start_http_server - - # Start Prometheus client - start_http_server(port=8000, addr="localhost") - - # Meter is responsible for creating and recording metrics - metrics.set_meter_provider(MeterProvider()) - meter = metrics.get_meter(__name__) - # exporter to export metrics to Prometheus - prefix = "MyAppPrefix" - exporter = PrometheusMetricsExporter(prefix) - # Starts the collect/export pipeline for metrics - metrics.get_meter_provider().start_pipeline(meter, exporter, 5) - - counter = meter.create_counter( - "requests", - "number of requests", - "requests", - int, - ) - - # Labels are used to identify key-values that are associated with a specific - # metric that you want to record. These are useful for pre-aggregation and can - # be used to store custom dimensions pertaining to a metric - labels = {"environment": "staging"} - - counter.add(25, labels) - input("Press any key to exit...") - -API ---- -""" - -import collections -import logging -import re -from typing import Iterable, Optional, Sequence, Union - -from prometheus_client.core import ( - REGISTRY, - CounterMetricFamily, - SummaryMetricFamily, - UnknownMetricFamily, -) - -from opentelemetry.metrics import Counter, ValueRecorder -from opentelemetry.sdk.metrics.export import ( - ExportRecord, - MetricsExporter, - MetricsExportResult, -) -from opentelemetry.sdk.metrics.export.aggregate import MinMaxSumCountAggregator - -logger = logging.getLogger(__name__) - - -class PrometheusMetricsExporter(MetricsExporter): - """Prometheus metric exporter for OpenTelemetry. - - Args: - prefix: single-word application prefix relevant to the domain - the metric belongs to. - """ - - def __init__(self, prefix: str = ""): - self._collector = CustomCollector(prefix) - REGISTRY.register(self._collector) - - def export( - self, export_records: Sequence[ExportRecord] - ) -> MetricsExportResult: - self._collector.add_metrics_data(export_records) - return MetricsExportResult.SUCCESS - - def shutdown(self) -> None: - REGISTRY.unregister(self._collector) - - -class CustomCollector: - """CustomCollector represents the Prometheus Collector object - https://github.com/prometheus/client_python#custom-collectors - """ - - def __init__(self, prefix: str = ""): - self._prefix = prefix - self._metrics_to_export = collections.deque() - self._non_letters_nor_digits_re = re.compile( - r"[^\w]", re.UNICODE | re.IGNORECASE - ) - - def add_metrics_data(self, export_records: Sequence[ExportRecord]) -> None: - self._metrics_to_export.append(export_records) - - def collect(self): - """Collect fetches the metrics from OpenTelemetry - and delivers them as Prometheus Metrics. - Collect is invoked every time a prometheus.Gatherer is run - for example when the HTTP endpoint is invoked by Prometheus. - """ - - while self._metrics_to_export: - for export_record in self._metrics_to_export.popleft(): - prometheus_metric = self._translate_to_prometheus( - export_record - ) - if prometheus_metric is not None: - yield prometheus_metric - - def _translate_to_prometheus(self, export_record: ExportRecord): - prometheus_metric = None - label_values = [] - label_keys = [] - for label_tuple in export_record.labels: - label_keys.append(self._sanitize(label_tuple[0])) - label_values.append(label_tuple[1]) - - metric_name = "" - if self._prefix != "": - metric_name = self._prefix + "_" - metric_name += self._sanitize(export_record.instrument.name) - - description = getattr(export_record.instrument, "description", "") - if isinstance(export_record.instrument, Counter): - prometheus_metric = CounterMetricFamily( - name=metric_name, documentation=description, labels=label_keys - ) - prometheus_metric.add_metric( - labels=label_values, value=export_record.aggregator.checkpoint - ) - # TODO: Add support for histograms when supported in OT - elif isinstance(export_record.instrument, ValueRecorder): - value = export_record.aggregator.checkpoint - if isinstance(export_record.aggregator, MinMaxSumCountAggregator): - prometheus_metric = SummaryMetricFamily( - name=metric_name, - documentation=description, - labels=label_keys, - ) - prometheus_metric.add_metric( - labels=label_values, - count_value=value.count, - sum_value=value.sum, - ) - else: - prometheus_metric = UnknownMetricFamily( - name=metric_name, - documentation=description, - labels=label_keys, - ) - prometheus_metric.add_metric(labels=label_values, value=value) - - else: - logger.warning( - "Unsupported metric type. %s", type(export_record.instrument) - ) - return prometheus_metric - - def _sanitize(self, key: str) -> str: - """sanitize the given metric name or label according to Prometheus rule. - Replace all characters other than [A-Za-z0-9_] with '_'. - """ - return self._non_letters_nor_digits_re.sub("_", key) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py deleted file mode 100644 index ebb75f6c11..0000000000 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.18.dev0" diff --git a/exporter/opentelemetry-exporter-prometheus/tests/__init__.py b/exporter/opentelemetry-exporter-prometheus/tests/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/exporter/opentelemetry-exporter-prometheus/tests/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py deleted file mode 100644 index 5813fba33c..0000000000 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ /dev/null @@ -1,169 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -from unittest import mock - -from prometheus_client import generate_latest -from prometheus_client.core import CounterMetricFamily - -from opentelemetry.exporter.prometheus import ( - CustomCollector, - PrometheusMetricsExporter, -) -from opentelemetry.metrics import get_meter_provider, set_meter_provider -from opentelemetry.sdk import metrics -from opentelemetry.sdk.metrics.export import ExportRecord, MetricsExportResult -from opentelemetry.sdk.metrics.export.aggregate import ( - MinMaxSumCountAggregator, - SumAggregator, -) -from opentelemetry.sdk.util import get_dict_as_key - - -class TestPrometheusMetricExporter(unittest.TestCase): - def setUp(self): - set_meter_provider(metrics.MeterProvider()) - self._meter = get_meter_provider().get_meter(__name__) - self._test_metric = self._meter.create_counter( - "testname", "testdesc", "unit", int, - ) - labels = {"environment": "staging"} - self._labels_key = get_dict_as_key(labels) - - self._mock_registry_register = mock.Mock() - self._registry_register_patch = mock.patch( - "prometheus_client.core.REGISTRY.register", - side_effect=self._mock_registry_register, - ) - - # pylint: disable=protected-access - def test_constructor(self): - """Test the constructor.""" - with self._registry_register_patch: - exporter = PrometheusMetricsExporter("testprefix") - self.assertEqual(exporter._collector._prefix, "testprefix") - self.assertTrue(self._mock_registry_register.called) - - def test_shutdown(self): - with mock.patch( - "prometheus_client.core.REGISTRY.unregister" - ) as registry_unregister_patch: - exporter = PrometheusMetricsExporter() - exporter.shutdown() - self.assertTrue(registry_unregister_patch.called) - - def test_export(self): - with self._registry_register_patch: - record = ExportRecord( - self._test_metric, - self._labels_key, - SumAggregator(), - get_meter_provider().resource, - ) - exporter = PrometheusMetricsExporter() - result = exporter.export([record]) - # pylint: disable=protected-access - self.assertEqual(len(exporter._collector._metrics_to_export), 1) - self.assertIs(result, MetricsExportResult.SUCCESS) - - def test_min_max_sum_aggregator_to_prometheus(self): - meter = get_meter_provider().get_meter(__name__) - metric = meter.create_valuerecorder( - "test@name", "testdesc", "unit", int, [] - ) - labels = {} - key_labels = get_dict_as_key(labels) - aggregator = MinMaxSumCountAggregator() - aggregator.update(123) - aggregator.update(456) - aggregator.take_checkpoint() - record = ExportRecord( - metric, key_labels, aggregator, get_meter_provider().resource - ) - collector = CustomCollector("testprefix") - collector.add_metrics_data([record]) - result_bytes = generate_latest(collector) - result = result_bytes.decode("utf-8") - self.assertIn("testprefix_test_name_count 2.0", result) - self.assertIn("testprefix_test_name_sum 579.0", result) - - def test_counter_to_prometheus(self): - meter = get_meter_provider().get_meter(__name__) - metric = meter.create_counter("test@name", "testdesc", "unit", int,) - labels = {"environment@": "staging", "os": "Windows"} - key_labels = get_dict_as_key(labels) - aggregator = SumAggregator() - aggregator.update(123) - aggregator.take_checkpoint() - record = ExportRecord( - metric, key_labels, aggregator, get_meter_provider().resource - ) - collector = CustomCollector("testprefix") - collector.add_metrics_data([record]) - - for prometheus_metric in collector.collect(): - self.assertEqual(type(prometheus_metric), CounterMetricFamily) - self.assertEqual(prometheus_metric.name, "testprefix_test_name") - self.assertEqual(prometheus_metric.documentation, "testdesc") - self.assertTrue(len(prometheus_metric.samples) == 1) - self.assertEqual(prometheus_metric.samples[0].value, 123) - self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) - self.assertEqual( - prometheus_metric.samples[0].labels["environment_"], "staging" - ) - self.assertEqual( - prometheus_metric.samples[0].labels["os"], "Windows" - ) - - # TODO: Add unit test once GaugeAggregator is available - # TODO: Add unit test once Measure Aggregators are available - - def test_invalid_metric(self): - meter = get_meter_provider().get_meter(__name__) - metric = StubMetric("tesname", "testdesc", "unit", int, meter) - labels = {"environment": "staging"} - key_labels = get_dict_as_key(labels) - record = ExportRecord( - metric, key_labels, None, get_meter_provider().resource - ) - collector = CustomCollector("testprefix") - collector.add_metrics_data([record]) - collector.collect() - self.assertLogs("opentelemetry.exporter.prometheus", level="WARNING") - - def test_sanitize(self): - collector = CustomCollector("testprefix") - self.assertEqual( - collector._sanitize("1!2@3#4$5%6^7&8*9(0)_-"), - "1_2_3_4_5_6_7_8_9_0___", - ) - self.assertEqual(collector._sanitize(",./?;:[]{}"), "__________") - self.assertEqual(collector._sanitize("TestString"), "TestString") - self.assertEqual(collector._sanitize("aAbBcC_12_oi"), "aAbBcC_12_oi") - - -class StubMetric(metrics.Metric): - def __init__( - self, - name: str, - description: str, - unit: str, - value_type, - meter, - enabled: bool = True, - ): - super().__init__( - name, description, unit, value_type, meter, enabled=enabled, - ) diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 20efac052d..d3cbe4ddf2 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -50,8 +50,6 @@ where = src [options.entry_points] opentelemetry_context = contextvars_context = opentelemetry.context.contextvars_context:ContextVarsRuntimeContext -opentelemetry_meter_provider = - default_meter_provider = opentelemetry.metrics:DefaultMeterProvider opentelemetry_tracer_provider = default_tracer_provider = opentelemetry.trace:DefaultTracerProvider opentelemetry_propagator = diff --git a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py index 810b24ca6d..ab006aeb38 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py @@ -18,6 +18,4 @@ OTEL_PYTHON_IDS_GENERATOR = "OTEL_PYTHON_IDS_GENERATOR" OTEL_PYTHON_SERVICE_NAME = "OTEL_PYTHON_SERVICE_NAME" OTEL_TRACE_EXPORTER = "OTEL_TRACE_EXPORTER" -OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER" OTEL_PYTHON_TRACER_PROVIDER = "OTEL_PYTHON_TRACER_PROVIDER" -OTEL_PYTHON_METER_PROVIDER = "OTEL_PYTHON_METER_PROVIDER" diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py deleted file mode 100644 index 325d055023..0000000000 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ /dev/null @@ -1,610 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The OpenTelemetry metrics API describes the classes used to report raw -measurements, as well as metrics with known aggregation and labels. - -The `Meter` class is used to construct `Metric` s to record raw statistics -as well as metrics with predefined aggregation. - -`Meter` s can be obtained via the `MeterProvider`, corresponding to the name -of the instrumenting library and (optionally) a version. - -See the `metrics api`_ spec for terminology and context clarification. - -.. _metrics api: - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md -""" -import abc -from logging import getLogger -from typing import ( - Callable, - Dict, - Optional, - Sequence, - Tuple, - Type, - TypeVar, - Union, -) - -from opentelemetry.util import _load_meter_provider - -logger = getLogger(__name__) -ValueT = TypeVar("ValueT", int, float) - - -class BoundCounter(abc.ABC): - @abc.abstractmethod - def add(self, value: ValueT) -> None: - """Increases the value of the bound counter by ``value``. - - Args: - value: The value to add to the bound counter. Must be positive. - """ - - -class DefaultBoundCounter(BoundCounter): - """The default bound counter instrument. - - Used when no bound counter implementation is available. - """ - - def add(self, value: ValueT) -> None: - pass - - -class BoundUpDownCounter(abc.ABC): - @abc.abstractmethod - def add(self, value: ValueT) -> None: - """Increases the value of the bound updowncounter by ``value``. - - Args: - value: The value to add to the bound updowncounter. Can be positive - or negative. - """ - - -class DefaultBoundUpDownCounter(BoundUpDownCounter): - """The default bound updowncounter instrument. - - Used when no bound updowncounter implementation is available. - """ - - def add(self, value: ValueT) -> None: - pass - - -class BoundValueRecorder(abc.ABC): - @abc.abstractmethod - def record(self, value: ValueT) -> None: - """Records the given ``value`` to this bound valuerecorder. - - Args: - value: The value to record to the bound valuerecorder. - """ - - -class DefaultBoundValueRecorder(BoundValueRecorder): - """The default bound valuerecorder instrument. - - Used when no bound valuerecorder implementation is available. - """ - - def record(self, value: ValueT) -> None: - pass - - -class Metric(abc.ABC): - """Base class for various types of metrics. - - Metric class that inherit from this class are specialized with the type of - bound metric instrument that the metric holds. - """ - - @abc.abstractmethod - def bind(self, labels: Dict[str, str]) -> "object": - """Gets a bound metric instrument. - - Bound metric instruments are useful to reduce the cost of repeatedly - recording a metric with a pre-defined set of label values. - - Args: - labels: Labels to associate with the bound instrument. - """ - - -class Counter(Metric): - """A counter type metric that expresses the computation of a sum.""" - - @abc.abstractmethod - def add(self, value: ValueT, labels: Dict[str, str]) -> None: - """Increases the value of the counter by ``value``. - - Args: - value: The value to add to the counter metric. Should be positive - or zero. For a Counter that can decrease, use - `UpDownCounter`. - labels: Labels to associate with the bound instrument. - """ - - -class DefaultCounter(Counter): - """The default counter instrument. - - Used when no `Counter` implementation is available. - """ - - def bind(self, labels: Dict[str, str]) -> "DefaultBoundCounter": - return DefaultBoundCounter() - - def add(self, value: ValueT, labels: Dict[str, str]) -> None: - pass - - -class UpDownCounter(Metric): - """A counter type metric that expresses the computation of a sum, - allowing negative increments.""" - - @abc.abstractmethod - def add(self, value: ValueT, labels: Dict[str, str]) -> None: - """Increases the value of the counter by ``value``. - - Args: - value: The value to add to the counter metric. Can be positive or - negative. For a Counter that is never decreasing, use - `Counter`. - labels: Labels to associate with the bound instrument. - """ - - -class DefaultUpDownCounter(UpDownCounter): - """The default updowncounter instrument. - - Used when no `UpDownCounter` implementation is available. - """ - - def bind(self, labels: Dict[str, str]) -> "DefaultBoundUpDownCounter": - return DefaultBoundUpDownCounter() - - def add(self, value: ValueT, labels: Dict[str, str]) -> None: - pass - - -class ValueRecorder(Metric): - """A valuerecorder type metric that represent raw stats.""" - - @abc.abstractmethod - def record(self, value: ValueT, labels: Dict[str, str]) -> None: - """Records the ``value`` to the valuerecorder. - - Args: - value: The value to record to this valuerecorder metric. - labels: Labels to associate with the bound instrument. - """ - - -class DefaultValueRecorder(ValueRecorder): - """The default valuerecorder instrument. - - Used when no `ValueRecorder` implementation is available. - """ - - def bind(self, labels: Dict[str, str]) -> "DefaultBoundValueRecorder": - return DefaultBoundValueRecorder() - - def record(self, value: ValueT, labels: Dict[str, str]) -> None: - pass - - -class Observer(abc.ABC): - """An observer type metric instrument used to capture a current set of - values. - - Observer instruments are asynchronous, a callback is invoked with the - observer instrument as argument allowing the user to capture multiple - values per collection interval. - """ - - @abc.abstractmethod - def observe(self, value: ValueT, labels: Dict[str, str]) -> None: - """Captures ``value`` to the observer. - - Args: - value: The value to capture to this observer metric. - labels: Labels associated to ``value``. - """ - - -# pylint: disable=W0223 -class SumObserver(Observer): - """Asynchronous instrument used to capture a monotonic sum.""" - - -class DefaultSumObserver(SumObserver): - """No-op implementation of ``SumObserver``.""" - - def observe(self, value: ValueT, labels: Dict[str, str]) -> None: - pass - - -# pylint: disable=W0223 -class UpDownSumObserver(Observer): - """Asynchronous instrument used to capture a non-monotonic count.""" - - -class DefaultUpDownSumObserver(UpDownSumObserver): - """No-op implementation of ``UpDownSumObserver``.""" - - def observe(self, value: ValueT, labels: Dict[str, str]) -> None: - pass - - -# pylint: disable=W0223 -class ValueObserver(Observer): - """Asynchronous instrument used to capture grouping measurements.""" - - -class DefaultValueObserver(ValueObserver): - """No-op implementation of ``ValueObserver``.""" - - def observe(self, value: ValueT, labels: Dict[str, str]) -> None: - pass - - -class MeterProvider(abc.ABC): - @abc.abstractmethod - def get_meter( - self, - instrumenting_module_name: str, - instrumenting_library_version: str = "", - ) -> "Meter": - """Returns a `Meter` for use by the given instrumentation library. - - This function may return different `Meter` types (e.g. a no-op meter - vs. a functional meter). - - Args: - instrumenting_module_name: The name of the instrumenting module - (usually just ``__name__``). - - This should *not* be the name of the module that is - instrumented but the name of the module doing the instrumentation. - E.g., instead of ``"requests"``, use - ``"opentelemetry.instrumentation.requests"``. - - instrumenting_library_version: Optional. The version string of the - instrumenting library. Usually this should be the same as - ``pkg_resources.get_distribution(instrumenting_library_name).version``. - """ - - -class DefaultMeterProvider(MeterProvider): - """The default MeterProvider, used when no implementation is available. - - All operations are no-op. - """ - - def get_meter( - self, - instrumenting_module_name: str, - instrumenting_library_version: str = "", - ) -> "Meter": - # pylint:disable=no-self-use,unused-argument - return DefaultMeter() - - -InstrumentT = TypeVar("InstrumentT", bound=Union[Metric, Observer]) -ObserverCallbackT = Callable[[Observer], None] - - -# pylint: disable=unused-argument -class Meter(abc.ABC): - """An interface to allow the recording of metrics. - - `Metric` s or metric instruments, are devices used for capturing raw - measurements. Each metric instrument supports a single method, each with - fixed interpretation to capture measurements. - """ - - @abc.abstractmethod - def record_batch( - self, - labels: Dict[str, str], - record_tuples: Sequence[Tuple["Metric", ValueT]], - ) -> None: - """Atomically records a batch of `Metric` and value pairs. - - Allows the functionality of acting upon multiple metrics with a single - API call. Implementations should find bound metric instruments that - match the key-value pairs in the labels. - - Args: - labels: Labels associated with all measurements in the - batch. - record_tuples: A sequence of pairs of `Metric` s and the - corresponding value to record for that metric. - """ - - @abc.abstractmethod - def create_counter( - self, - name: str, - description: str, - unit: str, - value_type: Type[ValueT], - enabled: bool = True, - ) -> "Counter": - """Creates a `Counter` metric with type ``value_type``. - - Args: - name: The name of the metric. - description: Human-readable description of the metric. - unit: Unit of the metric values following the UCUM convention - (https://unitsofmeasure.org/ucum.html). - value_type: The type of values being recorded by the metric. - enabled: Whether to report the metric by default. - """ - - @abc.abstractmethod - def create_updowncounter( - self, - name: str, - description: str, - unit: str, - value_type: Type[ValueT], - enabled: bool = True, - ) -> "UpDownCounter": - """Creates a `UpDownCounter` metric with type ``value_type``. - - Args: - name: The name of the metric. - description: Human-readable description of the metric. - unit: Unit of the metric values following the UCUM convention - (https://unitsofmeasure.org/ucum.html). - value_type: The type of values being recorded by the metric. - enabled: Whether to report the metric by default. - """ - - @abc.abstractmethod - def create_valuerecorder( - self, - name: str, - description: str, - unit: str, - value_type: Type[ValueT], - enabled: bool = True, - ) -> "ValueRecorder": - """Creates a `ValueRecorder` metric with type ``value_type``. - - Args: - name: The name of the metric. - description: Human-readable description of the metric. - unit: Unit of the metric values following the UCUM convention - (https://unitsofmeasure.org/ucum.html). - value_type: The type of values being recorded by the metric. - enabled: Whether to report the metric by default. - """ - - @abc.abstractmethod - def register_sumobserver( - self, - callback: ObserverCallbackT, - name: str, - description: str, - unit: str, - value_type: Type[ValueT], - label_keys: Sequence[str] = (), - enabled: bool = True, - ) -> "SumObserver": - """Registers an ``SumObserver`` metric instrument. - - Args: - callback: Callback invoked each collection interval with the - observer as argument. - name: The name of the metric. - description: Human-readable description of the metric. - unit: Unit of the metric values following the UCUM convention - (https://unitsofmeasure.org/ucum.html). - value_type: The type of values being recorded by the metric. - label_keys: The keys for the labels with dynamic values. - enabled: Whether to report the metric by default. - Returns: A new ``SumObserver`` metric instrument. - """ - - @abc.abstractmethod - def register_updownsumobserver( - self, - callback: ObserverCallbackT, - name: str, - description: str, - unit: str, - value_type: Type[ValueT], - label_keys: Sequence[str] = (), - enabled: bool = True, - ) -> "UpDownSumObserver": - """Registers an ``UpDownSumObserver`` metric instrument. - - Args: - callback: Callback invoked each collection interval with the - observer as argument. - name: The name of the metric. - description: Human-readable description of the metric. - unit: Unit of the metric values following the UCUM convention - (https://unitsofmeasure.org/ucum.html). - value_type: The type of values being recorded by the metric. - label_keys: The keys for the labels with dynamic values. - enabled: Whether to report the metric by default. - Returns: A new ``UpDownSumObserver`` metric instrument. - """ - - @abc.abstractmethod - def register_valueobserver( - self, - callback: ObserverCallbackT, - name: str, - description: str, - unit: str, - value_type: Type[ValueT], - label_keys: Sequence[str] = (), - enabled: bool = True, - ) -> "ValueObserver": - """Registers an ``ValueObserver`` metric instrument. - - Args: - callback: Callback invoked each collection interval with the - observer as argument. - name: The name of the metric. - description: Human-readable description of the metric. - unit: Unit of the metric values following the UCUM convention - (https://unitsofmeasure.org/ucum.html). - value_type: The type of values being recorded by the metric. - label_keys: The keys for the labels with dynamic values. - enabled: Whether to report the metric by default. - Returns: A new ``ValueObserver`` metric instrument. - """ - - @abc.abstractmethod - def unregister_observer(self, observer: "Observer") -> None: - """Unregisters an ``Observer`` metric instrument. - - Args: - observer: The observer to unregister. - """ - - -class DefaultMeter(Meter): - """The default Meter used when no Meter implementation is available.""" - - def record_batch( - self, - labels: Dict[str, str], - record_tuples: Sequence[Tuple["Metric", ValueT]], - ) -> None: - pass - - def create_counter( - self, - name: str, - description: str, - unit: str, - value_type: Type[ValueT], - enabled: bool = True, - ) -> "Counter": - # pylint: disable=no-self-use - return DefaultCounter() - - def create_updowncounter( - self, - name: str, - description: str, - unit: str, - value_type: Type[ValueT], - enabled: bool = True, - ) -> "UpDownCounter": - # pylint: disable=no-self-use - return DefaultUpDownCounter() - - def create_valuerecorder( - self, - name: str, - description: str, - unit: str, - value_type: Type[ValueT], - enabled: bool = True, - ) -> "ValueRecorder": - # pylint: disable=no-self-use - return DefaultValueRecorder() - - def register_sumobserver( - self, - callback: ObserverCallbackT, - name: str, - description: str, - unit: str, - value_type: Type[ValueT], - label_keys: Sequence[str] = (), - enabled: bool = True, - ) -> "DefaultSumObserver": - return DefaultSumObserver() - - def register_updownsumobserver( - self, - callback: ObserverCallbackT, - name: str, - description: str, - unit: str, - value_type: Type[ValueT], - label_keys: Sequence[str] = (), - enabled: bool = True, - ) -> "DefaultUpDownSumObserver": - return DefaultUpDownSumObserver() - - def register_valueobserver( - self, - callback: ObserverCallbackT, - name: str, - description: str, - unit: str, - value_type: Type[ValueT], - label_keys: Sequence[str] = (), - enabled: bool = True, - ) -> "DefaultValueObserver": - return DefaultValueObserver() - - def unregister_observer(self, observer: "Observer") -> None: - pass - - -_METER_PROVIDER = None - - -def get_meter( - instrumenting_module_name: str, - instrumenting_library_version: str = "", - meter_provider: Optional[MeterProvider] = None, -) -> "Meter": - """Returns a `Meter` for use by the given instrumentation library. - This function is a convenience wrapper for - opentelemetry.metrics.get_meter_provider().get_meter - - If meter_provider is omitted the current configured one is used. - """ - if meter_provider is None: - meter_provider = get_meter_provider() - return meter_provider.get_meter( - instrumenting_module_name, instrumenting_library_version - ) - - -def set_meter_provider(meter_provider: MeterProvider) -> None: - """Sets the current global :class:`~.MeterProvider` object.""" - global _METER_PROVIDER # pylint: disable=global-statement - - if _METER_PROVIDER is not None: - logger.warning("Overriding of current MeterProvider is not allowed") - return - - _METER_PROVIDER = meter_provider - - -def get_meter_provider() -> MeterProvider: - """Gets the current global :class:`~.MeterProvider` object.""" - global _METER_PROVIDER # pylint: disable=global-statement - - if _METER_PROVIDER is None: - _METER_PROVIDER = _load_meter_provider("meter_provider") - - return _METER_PROVIDER diff --git a/opentelemetry-api/src/opentelemetry/metrics/py.typed b/opentelemetry-api/src/opentelemetry/metrics/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index bb07b2c515..b8e60afc62 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -19,16 +19,12 @@ from pkg_resources import iter_entry_points -from opentelemetry.environment_variables import ( - OTEL_PYTHON_METER_PROVIDER, - OTEL_PYTHON_TRACER_PROVIDER, -) +from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER if TYPE_CHECKING: - from opentelemetry.metrics import MeterProvider from opentelemetry.trace import TracerProvider -Provider = Union["TracerProvider", "MeterProvider"] +Provider = Union["TracerProvider"] logger = getLogger(__name__) @@ -66,14 +62,8 @@ def _load_provider( raise -def _load_meter_provider(provider: str) -> "MeterProvider": - return cast( - "MeterProvider", _load_provider(OTEL_PYTHON_METER_PROVIDER, provider), - ) - - def _load_trace_provider(provider: str) -> "TracerProvider": - return cast( + return cast( # type: ignore "TracerProvider", _load_provider(OTEL_PYTHON_TRACER_PROVIDER, provider), ) diff --git a/opentelemetry-api/tests/metrics/__init__.py b/opentelemetry-api/tests/metrics/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/opentelemetry-api/tests/metrics/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py deleted file mode 100644 index 12496d0c61..0000000000 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -from opentelemetry import metrics - - -# pylint: disable=no-self-use -class TestMetrics(unittest.TestCase): - def test_default_counter(self): - counter = metrics.DefaultCounter() - bound_counter = counter.bind({}) - self.assertIsInstance(bound_counter, metrics.DefaultBoundCounter) - - def test_default_counter_add(self): - counter = metrics.DefaultCounter() - counter.add(1, {}) - - def test_default_updowncounter(self): - counter = metrics.DefaultUpDownCounter() - bound_counter = counter.bind({}) - self.assertIsInstance(bound_counter, metrics.DefaultBoundUpDownCounter) - - def test_default_updowncounter_add(self): - counter = metrics.DefaultUpDownCounter() - counter.add(1, {}) - counter.add(-1, {}) - - def test_default_valuerecorder(self): - valuerecorder = metrics.DefaultValueRecorder() - bound_valuerecorder = valuerecorder.bind({}) - self.assertIsInstance( - bound_valuerecorder, metrics.DefaultBoundValueRecorder - ) - - def test_default_valuerecorder_record(self): - valuerecorder = metrics.DefaultValueRecorder() - valuerecorder.record(1, {}) - - def test_default_bound_counter(self): - bound_counter = metrics.DefaultBoundCounter() - bound_counter.add(1) - - def test_default_bound_updowncounter(self): - bound_counter = metrics.DefaultBoundUpDownCounter() - bound_counter.add(1) - - def test_default_bound_valuerecorder(self): - bound_valuerecorder = metrics.DefaultBoundValueRecorder() - bound_valuerecorder.record(1) - - def test_default_sum_observer(self): - observer = metrics.DefaultSumObserver() - observer.observe(1, {}) - - def test_default_updown_sum_observer(self): - observer = metrics.DefaultUpDownSumObserver() - observer.observe(1, {}) - - def test_default_value_observer(self): - observer = metrics.DefaultValueObserver() - observer.observe(1, {}) diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index 1293e17435..c41f608e80 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -13,9 +13,8 @@ # limitations under the License. import unittest -from unittest import mock -from opentelemetry import metrics, trace +from opentelemetry import trace class TestAPIOnlyImplementation(unittest.TestCase): @@ -58,59 +57,3 @@ def test_default_span(self): span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) self.assertEqual(span.get_span_context(), trace.INVALID_SPAN_CONTEXT) self.assertIs(span.is_recording(), False) - - # METER - - def test_meter(self): - with self.assertRaises(TypeError): - # pylint: disable=abstract-class-instantiated - metrics.Meter() # type:ignore - - def test_default_meter(self): - meter_provider = metrics.DefaultMeterProvider() - meter = meter_provider.get_meter(__name__) - self.assertIsInstance(meter, metrics.DefaultMeter) - - # pylint: disable=no-self-use - def test_record_batch(self): - meter = metrics.DefaultMeter() - counter = metrics.DefaultCounter() - meter.record_batch({}, ((counter, 1),)) - - def test_create_counter(self): - meter = metrics.DefaultMeter() - metric = meter.create_counter("", "", "", float) - self.assertIsInstance(metric, metrics.DefaultCounter) - - def test_create_updowncounter(self): - meter = metrics.DefaultMeter() - metric = meter.create_updowncounter("", "", "", float) - self.assertIsInstance(metric, metrics.DefaultUpDownCounter) - - def test_create_valuerecorder(self): - meter = metrics.DefaultMeter() - metric = meter.create_valuerecorder("", "", "", float) - self.assertIsInstance(metric, metrics.DefaultValueRecorder) - - def test_register_sumobserver(self): - meter = metrics.DefaultMeter() - callback = mock.Mock() - observer = meter.register_sumobserver(callback, "", "", "", int) - self.assertIsInstance(observer, metrics.DefaultSumObserver) - - def test_register_updownsumobserver(self): - meter = metrics.DefaultMeter() - callback = mock.Mock() - observer = meter.register_updownsumobserver(callback, "", "", "", int) - self.assertIsInstance(observer, metrics.DefaultUpDownSumObserver) - - def test_register_valueobserver(self): - meter = metrics.DefaultMeter() - callback = mock.Mock() - observer = meter.register_valueobserver(callback, "", "", "", int) - self.assertIsInstance(observer, metrics.DefaultValueObserver) - - def test_unregister_observer(self): - meter = metrics.DefaultMeter() - observer = metrics.DefaultSumObserver() - meter.unregister_observer(observer) diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index c4ac8f5e87..2a6c6bd575 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -21,14 +21,12 @@ from opentelemetry import trace from opentelemetry.environment_variables import ( - OTEL_METRICS_EXPORTER, OTEL_PYTHON_IDS_GENERATOR, OTEL_PYTHON_SERVICE_NAME, OTEL_TRACE_EXPORTER, ) from opentelemetry.instrumentation.configurator import BaseConfigurator from opentelemetry.instrumentation.distro import BaseDistro -from opentelemetry.sdk.metrics.export import MetricsExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( @@ -42,7 +40,6 @@ EXPORTER_OTLP = "otlp" EXPORTER_OTLP_SPAN = "otlp_span" -EXPORTER_OTLP_METRIC = "otlp_metric" RANDOM_IDS_GENERATOR = "random" _DEFAULT_IDS_GENERATOR = RANDOM_IDS_GENERATOR @@ -58,7 +55,6 @@ def _get_service_name() -> str: def _get_exporter_names() -> Sequence[str]: trace_exporters = environ.get(OTEL_TRACE_EXPORTER) - metrics_exporters = environ.get(OTEL_METRICS_EXPORTER) exporters = set() @@ -73,21 +69,9 @@ def _get_exporter_names() -> Sequence[str]: } ) - if ( - metrics_exporters is not None - or metrics_exporters.lower().strip() != "none" - ): - exporters.update( - { - metrics_exporter.strip() - for metrics_exporter in metrics_exporters.split(",") - } - ) - if EXPORTER_OTLP in exporters: exporters.pop(EXPORTER_OTLP) exporters.add(EXPORTER_OTLP_SPAN) - exporters.add(EXPORTER_OTLP_METRIC) return list(exporters) @@ -139,8 +123,8 @@ def _import_tracer_provider_config_components( def _import_exporters( exporter_names: Sequence[str], -) -> Tuple[Sequence[SpanExporter], Sequence[MetricsExporter]]: - trace_exporters, metric_exporters = {}, {} +) -> Sequence[SpanExporter]: + trace_exporters = {} for ( exporter_name, @@ -150,15 +134,11 @@ def _import_exporters( ): if issubclass(exporter_impl, SpanExporter): trace_exporters[exporter_name] = exporter_impl - elif issubclass(exporter_impl, MetricsExporter): - metric_exporters[exporter_name] = exporter_impl else: raise RuntimeError( - "{0} is neither a trace exporter nor a metric exporter".format( - exporter_name - ) + "{0} is not a trace exporter".format(exporter_name) ) - return trace_exporters, metric_exporters + return trace_exporters def _import_ids_generator(ids_generator_name: str) -> IdsGenerator: @@ -177,19 +157,10 @@ def _import_ids_generator(ids_generator_name: str) -> IdsGenerator: def _initialize_components(): exporter_names = _get_exporter_names() - trace_exporters, metric_exporters = _import_exporters(exporter_names) + trace_exporters = _import_exporters(exporter_names) ids_generator_name = _get_ids_generator() ids_generator = _import_ids_generator(ids_generator_name) _init_tracing(trace_exporters, ids_generator) - # We don't support automatic initialization for metric yet but have added - # some boilerplate in order to make sure current implementation does not - # lock us out of supporting metrics later without major surgery. - _init_metrics(metric_exporters) - - -def _init_metrics(exporters: Sequence[MetricsExporter]): - if exporters: - logger.warning("automatic metric initialization is not supported yet.") class Configurator(BaseConfigurator): @@ -205,4 +176,3 @@ class OpenTelemetryDistro(BaseDistro): def _configure(self, **kwargs): os.environ.setdefault(OTEL_TRACE_EXPORTER, "otlp_span") - os.environ.setdefault(OTEL_METRICS_EXPORTER, "otlp_metric") diff --git a/opentelemetry-distro/tests/test_distro.py b/opentelemetry-distro/tests/test_distro.py index 109aead69e..ab8afb7a9b 100644 --- a/opentelemetry-distro/tests/test_distro.py +++ b/opentelemetry-distro/tests/test_distro.py @@ -19,10 +19,7 @@ from pkg_resources import DistributionNotFound, require from opentelemetry.distro import OpenTelemetryDistro -from opentelemetry.environment_variables import ( - OTEL_METRICS_EXPORTER, - OTEL_TRACE_EXPORTER, -) +from opentelemetry.environment_variables import OTEL_TRACE_EXPORTER class TestDistribution(TestCase): @@ -35,7 +32,5 @@ def test_package_available(self): def test_default_configuration(self): distro = OpenTelemetryDistro() self.assertIsNone(os.environ.get(OTEL_TRACE_EXPORTER)) - self.assertIsNone(os.environ.get(OTEL_METRICS_EXPORTER)) distro.configure() self.assertEqual("otlp_span", os.environ.get(OTEL_TRACE_EXPORTER)) - self.assertEqual("otlp_metric", os.environ.get(OTEL_METRICS_EXPORTER)) diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index 1f6dd269e5..346ae555b0 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -49,10 +49,9 @@ The command supports the following configuration options as CLI arguments and en * ``--trace-exporter`` or ``OTEL_TRACE_EXPORTER`` -* ``--metrics-exporter`` or ``OTEL_METRICS_EXPORTER`` -Used to specify which trace or metrics exporter to use. Can be set to one or -more of the well-known exporter names (see below). +Used to specify which trace exporter to use. Can be set to one or more of the well-known exporter +names (see below). - Defaults to `otlp`. - Can be set to `none` to disable automatic tracer initialization. @@ -65,10 +64,9 @@ Well known trace exporter names: - opencensus - otlp - otlp_span - - otlp_metric - zipkin -``otlp`` is an alias for ``otlp_span,otlp_metric``. +``otlp`` is an alias for ``otlp_span``. * ``--service-name`` or ``OTEL_PYTHON_SERVICE_NAME`` diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index 7d827663cb..66bf9caddd 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -21,7 +21,6 @@ from shutil import which from opentelemetry.environment_variables import ( - OTEL_METRICS_EXPORTER, OTEL_PYTHON_IDS_GENERATOR, OTEL_PYTHON_SERVICE_NAME, OTEL_TRACE_EXPORTER, @@ -38,19 +37,6 @@ def parse_args(): """ ) - parser.add_argument( - "--metrics-exporter", - required=False, - help=""" - Uses the specified exporter to export metrics. - Accepts multiple exporters as comma separated values. - - Examples: - - --metrics-exporter=otlp_metric - """, - ) - parser.add_argument( "--trace-exporter", required=False, @@ -95,8 +81,6 @@ def parse_args(): def load_config_from_cli_args(args): - if args.metrics_exporter: - environ[OTEL_METRICS_EXPORTER] = args.metrics_exporter if args.trace_exporter: environ[OTEL_TRACE_EXPORTER] = args.trace_exporter if args.service_name: diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py deleted file mode 100644 index 4f8972ac82..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/metric.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -""" -OpenTelemetry Instrumentation Metric mixin -""" -import enum -from contextlib import contextmanager -from time import time -from typing import Dict, Optional - -from opentelemetry import metrics - - -class HTTPMetricType(enum.Enum): - CLIENT = 0 - SERVER = 1 - BOTH = 2 - - -class MetricMixin: - """Used to record metrics related to instrumentations.""" - - def init_metrics(self, name: str, version: str): - self._meter = metrics.get_meter(name, version) - - @property - def meter(self): - return self._meter - - -class MetricRecorder: - """Base class for metric recorders of different types.""" - - def __init__(self, meter: Optional[metrics.Meter] = None): - self._meter = meter - - -class HTTPMetricRecorder(MetricRecorder): - """Metric recorder for http instrumentations. Tracks duration.""" - - def __init__( - self, meter: Optional[metrics.Meter], http_type: HTTPMetricType, - ): - super().__init__(meter) - self._http_type = http_type - self._client_duration = None - self._server_duration = None - if self._meter is not None: - if http_type in (HTTPMetricType.CLIENT, HTTPMetricType.BOTH): - self._client_duration = self._meter.create_valuerecorder( - name="{}.{}.duration".format("http", "client"), - description="measures the duration of the outbound HTTP request", - unit="ms", - value_type=float, - ) - if http_type is not HTTPMetricType.CLIENT: - self._server_duration = self._meter.create_valuerecorder( - name="{}.{}.duration".format("http", "server"), - description="measures the duration of the inbound HTTP request", - unit="ms", - value_type=float, - ) - - # Conventions for recording duration can be found at: - # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md - @contextmanager - def record_client_duration(self, labels: Dict[str, str]): - start_time = time() - try: - yield start_time - finally: - self.record_client_duration_range(start_time, time(), labels) - - def record_client_duration_range( - self, start_time, end_time, labels: Dict[str, str] - ): - if self._client_duration is not None: - elapsed_time = (end_time - start_time) * 1000 - self._client_duration.record(elapsed_time, labels) - - @contextmanager - def record_server_duration(self, labels: Dict[str, str]): - start_time = time() - try: - yield start_time - finally: - self.record_server_duration_range(start_time, time(), labels) - - def record_server_duration_range( - self, start_time, end_time, labels: Dict[str, str] - ): - if self._server_duration is not None: - elapsed_time = (end_time - start_time) * 1000 - self._server_duration.record(elapsed_time, labels) diff --git a/opentelemetry-instrumentation/tests/test_metric.py b/opentelemetry-instrumentation/tests/test_metric.py deleted file mode 100644 index c0bdcca15a..0000000000 --- a/opentelemetry-instrumentation/tests/test_metric.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -from unittest import TestCase, mock - -from opentelemetry import metrics as metrics_api -from opentelemetry.instrumentation.metric import ( - HTTPMetricRecorder, - HTTPMetricType, - MetricMixin, -) -from opentelemetry.metrics import set_meter_provider -from opentelemetry.sdk import metrics -from opentelemetry.sdk.util import get_dict_as_key - - -# pylint: disable=protected-access -class TestMetricMixin(TestCase): - @classmethod - def setUpClass(cls): - metrics_api._METER_PROVIDER = None - set_meter_provider(metrics.MeterProvider()) - - @classmethod - def tearDownClass(cls): - metrics_api._METER_PROVIDER = None - - def test_init(self): - mixin = MetricMixin() - mixin.init_metrics("test", 1.0) - meter = mixin.meter - self.assertTrue(isinstance(meter, metrics.Accumulator)) - self.assertEqual(meter.instrumentation_info.name, "test") - self.assertEqual(meter.instrumentation_info.version, 1.0) - - -class TestHTTPMetricRecorder(TestCase): - @classmethod - def setUpClass(cls): - metrics_api._METER_PROVIDER = None - set_meter_provider(metrics.MeterProvider()) - - @classmethod - def tearDownClass(cls): - metrics_api._METER_PROVIDER = None - - def test_ctor(self): - meter = metrics_api.get_meter(__name__) - recorder = HTTPMetricRecorder(meter, HTTPMetricType.CLIENT) - # pylint: disable=protected-access - self.assertEqual(recorder._http_type, HTTPMetricType.CLIENT) - self.assertTrue( - isinstance(recorder._client_duration, metrics.ValueRecorder) - ) - self.assertEqual( - recorder._client_duration.name, "http.client.duration" - ) - self.assertEqual( - recorder._client_duration.description, - "measures the duration of the outbound HTTP request", - ) - - def test_ctor_type_client(self): - meter = metrics_api.get_meter(__name__) - recorder = HTTPMetricRecorder(meter, HTTPMetricType.CLIENT) - self.assertEqual(recorder._http_type, HTTPMetricType.CLIENT) - self.assertTrue( - isinstance(recorder._client_duration, metrics.ValueRecorder) - ) - self.assertIsNone(recorder._server_duration) - - def test_ctor_type_server(self): - meter = metrics_api.get_meter(__name__) - recorder = HTTPMetricRecorder(meter, HTTPMetricType.SERVER) - self.assertEqual(recorder._http_type, HTTPMetricType.SERVER) - self.assertTrue( - isinstance(recorder._server_duration, metrics.ValueRecorder) - ) - self.assertIsNone(recorder._client_duration) - - def test_ctor_type_both(self): - meter = metrics_api.get_meter(__name__) - recorder = HTTPMetricRecorder(meter, HTTPMetricType.BOTH) - self.assertEqual(recorder._http_type, HTTPMetricType.BOTH) - self.assertTrue( - isinstance(recorder._client_duration, metrics.ValueRecorder) - ) - self.assertTrue( - isinstance(recorder._server_duration, metrics.ValueRecorder) - ) - - def test_record_client_duration(self): - meter = metrics_api.get_meter(__name__) - recorder = HTTPMetricRecorder(meter, HTTPMetricType.CLIENT) - labels = {"test": "asd"} - with mock.patch("time.time") as time_patch: - time_patch.return_value = 5.0 - with recorder.record_client_duration(labels): - labels["test2"] = "asd2" - match_key = get_dict_as_key({"test": "asd", "test2": "asd2"}) - for key in recorder._client_duration.bound_instruments.keys(): - self.assertEqual(key, match_key) - # pylint: disable=protected-access - bound = recorder._client_duration.bound_instruments.get(key) - for view_data in bound.view_datas: - self.assertEqual(view_data.labels, key) - self.assertEqual(view_data.aggregator.current.count, 1) - self.assertGreaterEqual(view_data.aggregator.current.sum, 0) - - def test_record_server_duration(self): - meter = metrics_api.get_meter(__name__) - recorder = HTTPMetricRecorder(meter, HTTPMetricType.SERVER) - labels = {"test": "asd"} - with mock.patch("time.time") as time_patch: - time_patch.return_value = 5.0 - with recorder.record_server_duration(labels): - labels["test2"] = "asd2" - match_key = get_dict_as_key({"test": "asd", "test2": "asd2"}) - for key in recorder._server_duration.bound_instruments.keys(): - self.assertEqual(key, match_key) - # pylint: disable=protected-access - bound = recorder._server_duration.bound_instruments.get(key) - for view_data in bound.view_datas: - self.assertEqual(view_data.labels, key) - self.assertEqual(view_data.aggregator.current.count, 1) - self.assertGreaterEqual(view_data.aggregator.current.sum, 0) diff --git a/opentelemetry-instrumentation/tests/test_run.py b/opentelemetry-instrumentation/tests/test_run.py index 6b8c097f06..9956ad3c25 100644 --- a/opentelemetry-instrumentation/tests/test_run.py +++ b/opentelemetry-instrumentation/tests/test_run.py @@ -19,7 +19,6 @@ from unittest.mock import patch from opentelemetry.environment_variables import ( - OTEL_METRICS_EXPORTER, OTEL_PYTHON_SERVICE_NAME, OTEL_TRACE_EXPORTER, ) @@ -112,7 +111,7 @@ class TestArgs(TestCase): def test_exporter(self, _): # pylint: disable=no-self-use with patch("sys.argv", ["instrument", "2"]): auto_instrumentation.run() - self.assertIsNone(environ.get(OTEL_METRICS_EXPORTER)) + self.assertIsNone(environ.get(OTEL_TRACE_EXPORTER)) with patch( "sys.argv", ["instrument", "--trace-exporter", "jaeger", "1", "2"] diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 719911ffcb..988d571cd5 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -48,13 +48,10 @@ install_requires = where = src [options.entry_points] -opentelemetry_meter_provider = - sdk_meter_provider = opentelemetry.sdk.metrics:MeterProvider opentelemetry_tracer_provider = sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider opentelemetry_exporter = console_span = opentelemetry.sdk.trace.export:ConsoleSpanExporter - console_metrics = opentelemetry.sdk.metrics.export:ConsoleMetricsExporter opentelemetry_ids_generator = random = opentelemetry.sdk.trace.ids_generator:RandomIdsGenerator diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 9ab85031b7..bf2c45a255 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -29,8 +29,6 @@ OTEL_EXPORTER_JAEGER_USER = "OTEL_EXPORTER_JAEGER_USER" OTEL_EXPORTER_JAEGER_PASSWORD = "OTEL_EXPORTER_JAEGER_PASSWORD" OTEL_EXPORTER_ZIPKIN_ENDPOINT = "OTEL_EXPORTER_ZIPKIN_ENDPOINT" -OTEL_EXPORTER_PROMETHEUS_HOST = "OTEL_EXPORTER_PROMETHEUS_HOST" -OTEL_EXPORTER_PROMETHEUS_PORT = "OTEL_EXPORTER_PROMETHEUS_PORT" OTEL_EXPORTER_ENDPOINT = "OTEL_EXPORTER_ENDPOINT" OTEL_EXPORTER_OTLP_PROTOCOL = "OTEL_EXPORTER_OTLP_PROTOCOL" OTEL_EXPORTER_OTLP_CERTIFICATE = "OTEL_EXPORTER_OTLP_CERTIFICATE" @@ -39,20 +37,13 @@ OTEL_EXPORTER_OTLP_TIMEOUT = "OTEL_EXPORTER_OTLP_TIMEOUT" OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT" OTEL_EXPORTER_OTLP_SPAN_ENDPOINT = "OTEL_EXPORTER_OTLP_SPAN_ENDPOINT" -OTEL_EXPORTER_OTLP_METRIC_ENDPOINT = "OTEL_EXPORTER_OTLP_METRIC_ENDPOINT" OTEL_EXPORTER_OTLP_SPAN_PROTOCOL = "OTEL_EXPORTER_OTLP_SPAN_PROTOCOL" -OTEL_EXPORTER_OTLP_METRIC_PROTOCOL = "OTEL_EXPORTER_OTLP_METRIC_PROTOCOL" OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE = "OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE" -OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE = "OTEL_EXPORTER_OTLP_METRIC_CERTIFICATE" OTEL_EXPORTER_OTLP_SPAN_HEADERS = "OTEL_EXPORTER_OTLP_SPAN_HEADERS" -OTEL_EXPORTER_OTLP_METRIC_HEADERS = "OTEL_EXPORTER_OTLP_METRIC_HEADERS" OTEL_EXPORTER_OTLP_SPAN_COMPRESSION = "OTEL_EXPORTER_OTLP_SPAN_COMPRESSION" -OTEL_EXPORTER_OTLP_METRIC_COMPRESSION = "OTEL_EXPORTER_OTLP_METRIC_COMPRESSION" OTEL_EXPORTER_OTLP_SPAN_TIMEOUT = "OTEL_EXPORTER_OTLP_SPAN_TIMEOUT" -OTEL_EXPORTER_OTLP_METRIC_TIMEOUT = "OTEL_EXPORTER_OTLP_METRIC_TIMEOUT" OTEL_EXPORTER_JAEGER_INSECURE = "OTEL_EXPORTER_JAEGER_INSECURE" OTEL_EXPORTER_JAEGER_CERTIFICATE = "OTEL_EXPORTER_JAEGER_CERTIFICATE" OTEL_EXPORTER_OTLP_INSECURE = "OTEL_EXPORTER_OTLP_INSECURE" OTEL_EXPORTER_OTLP_SPAN_INSECURE = "OTEL_EXPORTER_OTLP_SPAN_INSECURE" -OTEL_EXPORTER_OTLP_METRIC_INSECURE = "OTEL_EXPORTER_OTLP_METRIC_INSECURE" OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT = "OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py deleted file mode 100644 index ca0ec2f967..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ /dev/null @@ -1,621 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import atexit -import logging -import threading -from typing import Dict, Sequence, Tuple, Type, TypeVar, Union - -from opentelemetry import metrics as metrics_api -from opentelemetry.sdk.metrics.export import ( - ConsoleMetricsExporter, - MetricsExporter, -) -from opentelemetry.sdk.metrics.export.aggregate import Aggregator -from opentelemetry.sdk.metrics.export.controller import PushController -from opentelemetry.sdk.metrics.export.processor import Processor -from opentelemetry.sdk.metrics.view import ( - ViewData, - ViewManager, - get_default_aggregator, -) -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util import get_dict_as_key -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo - -logger = logging.getLogger(__name__) - - -class BaseBoundInstrument: - """Class containing common behavior for all bound metric instruments. - - Bound metric instruments are responsible for operating on data for metric - instruments for a specific set of labels. - - Args: - labels: A set of labels as keys that bind this metric instrument. - metric: The metric that created this bound instrument. - """ - - def __init__(self, labels: Tuple[Tuple[str, str]], metric: "MetricT"): - self._labels = labels - self._metric = metric - self.view_datas = metric.meter.view_manager.get_view_datas( - metric, labels - ) - self._view_datas_lock = threading.Lock() - self._ref_count = 0 - self._ref_count_lock = threading.Lock() - - def _validate_update(self, value: metrics_api.ValueT) -> bool: - if not self._metric.enabled: - return False - if not isinstance(value, self._metric.value_type): - logger.warning( - "Invalid value passed for %s.", - self._metric.value_type.__name__, - ) - return False - return True - - def update(self, value: metrics_api.ValueT): - with self._view_datas_lock: - # record the value for each view_data belonging to this aggregator - for view_data in self.view_datas: - view_data.record(value) - - def release(self): - self.decrease_ref_count() - - def decrease_ref_count(self): - with self._ref_count_lock: - self._ref_count -= 1 - - def increase_ref_count(self): - with self._ref_count_lock: - self._ref_count += 1 - - def ref_count(self): - with self._ref_count_lock: - return self._ref_count - - -class BoundCounter(metrics_api.BoundCounter, BaseBoundInstrument): - def add(self, value: metrics_api.ValueT) -> None: - """See `opentelemetry.metrics.BoundCounter.add`.""" - if self._validate_update(value): - self.update(value) - - def _validate_update(self, value: metrics_api.ValueT) -> bool: - if not super()._validate_update(value): - return False - if value < 0: - logger.warning( - "Invalid value %s passed to Counter, value must be non-negative. " - "For a Counter that can decrease, use UpDownCounter.", - value, - ) - return False - return True - - -class BoundUpDownCounter(metrics_api.BoundUpDownCounter, BaseBoundInstrument): - def add(self, value: metrics_api.ValueT) -> None: - """See `opentelemetry.metrics.BoundUpDownCounter.add`.""" - if self._validate_update(value): - self.update(value) - - -class BoundValueRecorder(metrics_api.BoundValueRecorder, BaseBoundInstrument): - def record(self, value: metrics_api.ValueT) -> None: - """See `opentelemetry.metrics.BoundValueRecorder.record`.""" - if self._validate_update(value): - self.update(value) - - -class Metric(metrics_api.Metric): - """Base class for all synchronous metric types. - - This is the class that is used to represent a metric that is to be - synchronously recorded and tracked. Synchronous instruments are called - inside a request, meaning they have an associated distributed context - (i.e. Span context, baggage). Multiple metric events may occur - for a synchronous instrument within a give collection interval. - - Each metric has a set of bound metrics that are created from the metric. - See `BaseBoundInstrument` for information on bound metric instruments. - """ - - BOUND_INSTR_TYPE = BaseBoundInstrument - - def __init__( - self, - name: str, - description: str, - unit: str, - value_type: Type[metrics_api.ValueT], - meter: "Accumulator", - enabled: bool = True, - ): - self.name = name - self.description = description - self.unit = unit - self.value_type = value_type - self.meter = meter - self.enabled = enabled - self.bound_instruments = {} - self.bound_instruments_lock = threading.Lock() - - def bind(self, labels: Dict[str, str]) -> BaseBoundInstrument: - """See `opentelemetry.metrics.Metric.bind`.""" - key = get_dict_as_key(labels) - with self.bound_instruments_lock: - bound_instrument = self.bound_instruments.get(key) - if bound_instrument is None: - bound_instrument = self.BOUND_INSTR_TYPE(key, self) - self.bound_instruments[key] = bound_instrument - bound_instrument.increase_ref_count() - return bound_instrument - - def __repr__(self): - return '{}(name="{}", description="{}")'.format( - type(self).__name__, self.name, self.description - ) - - UPDATE_FUNCTION = lambda x, y: None # noqa: E731 - - -class Counter(Metric, metrics_api.Counter): - """See `opentelemetry.metrics.Counter`. - """ - - BOUND_INSTR_TYPE = BoundCounter - - def add(self, value: metrics_api.ValueT, labels: Dict[str, str]) -> None: - """See `opentelemetry.metrics.Counter.add`.""" - bound_intrument = self.bind(labels) - bound_intrument.add(value) - bound_intrument.release() - - UPDATE_FUNCTION = add - - -class UpDownCounter(Metric, metrics_api.UpDownCounter): - """See `opentelemetry.metrics.UpDownCounter`. - """ - - BOUND_INSTR_TYPE = BoundUpDownCounter - - def add(self, value: metrics_api.ValueT, labels: Dict[str, str]) -> None: - """See `opentelemetry.metrics.UpDownCounter.add`.""" - bound_intrument = self.bind(labels) - bound_intrument.add(value) - bound_intrument.release() - - UPDATE_FUNCTION = add - - -class ValueRecorder(Metric, metrics_api.ValueRecorder): - """See `opentelemetry.metrics.ValueRecorder`.""" - - BOUND_INSTR_TYPE = BoundValueRecorder - - def record( - self, value: metrics_api.ValueT, labels: Dict[str, str] - ) -> None: - """See `opentelemetry.metrics.ValueRecorder.record`.""" - bound_intrument = self.bind(labels) - bound_intrument.record(value) - bound_intrument.release() - - UPDATE_FUNCTION = record - - -MetricT = TypeVar("MetricT", Counter, UpDownCounter, ValueRecorder) - - -class Observer(metrics_api.Observer): - """Base class for all asynchronous metric types. - - Also known as Observers, observer metric instruments are asynchronous in - that they are reported by a callback, once per collection interval, and - lack context. They are permitted to report only one value per distinct - label set per period. - """ - - def __init__( - self, - callback: metrics_api.ObserverCallbackT, - name: str, - description: str, - unit: str, - value_type: Type[metrics_api.ValueT], - meter: "Accumulator", - label_keys: Sequence[str] = (), - enabled: bool = True, - ): - self.callback = callback - self.name = name - self.description = description - self.unit = unit - self.value_type = value_type - self.meter = meter - self.label_keys = label_keys - self.enabled = enabled - - self.aggregators = {} - - def observe( - self, value: metrics_api.ValueT, labels: Dict[str, str] - ) -> None: - key = get_dict_as_key(labels) - if not self._validate_observe(value, key): - return - - if key not in self.aggregators: - # TODO: how to cleanup aggregators? - self.aggregators[key] = get_default_aggregator(self)() - aggregator = self.aggregators[key] - aggregator.update(value) - - # pylint: disable=W0613 - def _validate_observe( - self, value: metrics_api.ValueT, key: Tuple[Tuple[str, str]] - ) -> bool: - if not self.enabled: - return False - if not isinstance(value, self.value_type): - logger.warning( - "Invalid value passed for %s.", self.value_type.__name__ - ) - return False - - return True - - def run(self) -> bool: - try: - self.callback(self) - # pylint: disable=broad-except - except Exception as exc: - logger.warning( - "Exception while executing observer callback: %s.", exc - ) - return False - return True - - def __repr__(self): - return '{}(name="{}", description="{}")'.format( - type(self).__name__, self.name, self.description - ) - - -class SumObserver(Observer, metrics_api.SumObserver): - """See `opentelemetry.metrics.SumObserver`.""" - - def _validate_observe( - self, value: metrics_api.ValueT, key: Tuple[Tuple[str, str]] - ) -> bool: - if not super()._validate_observe(value, key): - return False - # Must be non-decreasing because monotonic - if ( - key in self.aggregators - and self.aggregators[key].current is not None - ): - if value < self.aggregators[key].current: - logger.warning("Value passed must be non-decreasing.") - return False - return True - - -class UpDownSumObserver(Observer, metrics_api.UpDownSumObserver): - """See `opentelemetry.metrics.UpDownSumObserver`.""" - - -class ValueObserver(Observer, metrics_api.ValueObserver): - """See `opentelemetry.metrics.ValueObserver`.""" - - -class Accumulation: - """Container class used for processing in the `Processor`""" - - def __init__( - self, - instrument: metrics_api.InstrumentT, - labels: Tuple[Tuple[str, str]], - aggregator: Aggregator, - ): - self.instrument = instrument - self.labels = labels - self.aggregator = aggregator - - -class Accumulator(metrics_api.Meter): - """See `opentelemetry.metrics.Meter`. - - Args: - source: The `MeterProvider` that created this meter. - instrumentation_info: The `InstrumentationInfo` for this meter. - """ - - def __init__( - self, - source: "MeterProvider", - instrumentation_info: "InstrumentationInfo", - ): - self.instrumentation_info = instrumentation_info - self.processor = Processor(source.stateful, source.resource) - self.instruments = {} - self.instruments_lock = threading.Lock() - self.view_manager = ViewManager() - - def _register_instrument( - self, instrument: Union[metrics_api.Metric, metrics_api.Observer] - ): - name = instrument.name.strip().lower() - with self.instruments_lock: - if name in self.instruments: - raise ValueError( - "Multiple instruments can't be registered by the same name: ({})".format( - name - ) - ) - self.instruments[name] = instrument - - def collect(self) -> None: - """Collects all the metrics created with this `Meter` for export. - - Utilizes the processor to create checkpoints of the current values in - each aggregator belonging to the metrics that were created with this - meter instance. - """ - with self.instruments_lock: - for instrument in self.instruments.values(): - if not instrument.enabled: - continue - if isinstance(instrument, metrics_api.Metric): - to_remove = [] - with instrument.bound_instruments_lock: - for ( - labels, - bound_instrument, - ) in instrument.bound_instruments.items(): - for view_data in bound_instrument.view_datas: - accumulation = Accumulation( - instrument, - view_data.labels, - view_data.aggregator, - ) - self.processor.process(accumulation) - - if bound_instrument.ref_count() == 0: - to_remove.append(labels) - - # Remove handles that were released - for labels in to_remove: - del instrument.bound_instruments[labels] - elif isinstance(instrument, metrics_api.Observer): - if not instrument.run(): - continue - - for labels, aggregator in instrument.aggregators.items(): - accumulation = Accumulation( - instrument, labels, aggregator - ) - self.processor.process(accumulation) - - def record_batch( - self, - labels: Dict[str, str], - record_tuples: Sequence[Tuple[metrics_api.Metric, metrics_api.ValueT]], - ) -> None: - """See `opentelemetry.metrics.Meter.record_batch`.""" - # TODO: Avoid enconding the labels for each instrument, encode once - # and reuse. - for metric, value in record_tuples: - metric.UPDATE_FUNCTION(value, labels) - - def create_counter( - self, - name: str, - description: str, - unit: str, - value_type: Type[metrics_api.ValueT], - enabled: bool = True, - ) -> metrics_api.Counter: - """See `opentelemetry.metrics.Meter.create_counter`.""" - counter = Counter( - name, description, unit, value_type, self, enabled=enabled - ) - self._register_instrument(counter) - return counter - - def create_updowncounter( - self, - name: str, - description: str, - unit: str, - value_type: Type[metrics_api.ValueT], - enabled: bool = True, - ) -> metrics_api.UpDownCounter: - """See `opentelemetry.metrics.Meter.create_updowncounter`.""" - counter = UpDownCounter( - name, description, unit, value_type, self, enabled=enabled - ) - self._register_instrument(counter) - return counter - - def create_valuerecorder( - self, - name: str, - description: str, - unit: str, - value_type: Type[metrics_api.ValueT], - enabled: bool = True, - ) -> metrics_api.ValueRecorder: - """See `opentelemetry.metrics.Meter.create_valuerecorder`.""" - recorder = ValueRecorder( - name, description, unit, value_type, self, enabled=enabled - ) - self._register_instrument(recorder) - return recorder - - def register_sumobserver( - self, - callback: metrics_api.ObserverCallbackT, - name: str, - description: str, - unit: str, - value_type: Type[metrics_api.ValueT], - label_keys: Sequence[str] = (), - enabled: bool = True, - ) -> metrics_api.SumObserver: - ob = SumObserver( - callback, - name, - description, - unit, - value_type, - self, - label_keys, - enabled, - ) - self._register_instrument(ob) - return ob - - def register_updownsumobserver( - self, - callback: metrics_api.ObserverCallbackT, - name: str, - description: str, - unit: str, - value_type: Type[metrics_api.ValueT], - label_keys: Sequence[str] = (), - enabled: bool = True, - ) -> metrics_api.UpDownSumObserver: - ob = UpDownSumObserver( - callback, - name, - description, - unit, - value_type, - self, - label_keys, - enabled, - ) - self._register_instrument(ob) - return ob - - def register_valueobserver( - self, - callback: metrics_api.ObserverCallbackT, - name: str, - description: str, - unit: str, - value_type: Type[metrics_api.ValueT], - label_keys: Sequence[str] = (), - enabled: bool = True, - ) -> metrics_api.ValueObserver: - ob = ValueObserver( - callback, - name, - description, - unit, - value_type, - self, - label_keys, - enabled, - ) - self._register_instrument(ob) - return ob - - def unregister_observer(self, observer: metrics_api.Observer) -> None: - name = observer.name.strip().lower() - with self.instruments_lock: - self.instruments.pop(name) - - def register_view(self, view): - self.view_manager.register_view(view) - - def unregister_view(self, view): - self.view_manager.unregister_view(view) - - -class MeterProvider(metrics_api.MeterProvider): - """See `opentelemetry.metrics.MeterProvider`. - - Args: - stateful: Indicates whether meters created are going to be stateful - resource: Resource for this MeterProvider - shutdown_on_exit: Register an atexit hook to shut down when the - application exists - """ - - def __init__( - self, - stateful=True, - resource: Resource = Resource.create({}), - shutdown_on_exit: bool = True, - ): - self.stateful = stateful - self.resource = resource - self._controllers = [] - self._exporters = set() - self._atexit_handler = None - if shutdown_on_exit: - self._atexit_handler = atexit.register(self.shutdown) - - def get_meter( - self, - instrumenting_module_name: str, - instrumenting_library_version: str = "", - ) -> "metrics_api.Meter": - """See `opentelemetry.metrics.MeterProvider`.get_meter.""" - if not instrumenting_module_name: # Reject empty strings too. - instrumenting_module_name = "ERROR:MISSING MODULE NAME" - logger.error("get_meter called with missing module name.") - return Accumulator( - self, - InstrumentationInfo( - instrumenting_module_name, instrumenting_library_version - ), - ) - - def start_pipeline( - self, - meter: metrics_api.Meter, - exporter: MetricsExporter = None, - interval: float = 15.0, - ) -> None: - """Method to begin the collect/export pipeline. - - Args: - meter: The meter to collect metrics from. - exporter: The exporter to export metrics to. - interval: The collect/export interval in seconds. - """ - if not exporter: - exporter = ConsoleMetricsExporter() - self._exporters.add(exporter) - # TODO: Controller type configurable? - self._controllers.append(PushController(meter, exporter, interval)) - - def shutdown(self) -> None: - for controller in self._controllers: - controller.shutdown() - for exporter in self._exporters: - exporter.shutdown() - if self._atexit_handler is not None: - atexit.unregister(self._atexit_handler) - self._atexit_handler = None diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py deleted file mode 100644 index 60ae4b9466..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from enum import Enum -from typing import Sequence, Tuple - -from opentelemetry import metrics as metrics_api -from opentelemetry.sdk.metrics.export.aggregate import Aggregator -from opentelemetry.sdk.resources import Resource - - -class MetricsExportResult(Enum): - SUCCESS = 0 - FAILURE = 1 - - -class ExportRecord: - def __init__( - self, - instrument: metrics_api.InstrumentT, - labels: Tuple[Tuple[str, str]], - aggregator: Aggregator, - resource: Resource, - ): - self.instrument = instrument - self.labels = labels - self.aggregator = aggregator - self.resource = resource - - -class MetricsExporter: - """Interface for exporting metrics. - - Interface to be implemented by services that want to export recorded - metrics in its own format. - """ - - def export( - self, export_records: Sequence[ExportRecord] - ) -> "MetricsExportResult": - """Exports a batch of telemetry data. - - Args: - export_records: A sequence of `ExportRecord` s. A `ExportRecord` - contains the metric to be exported, the labels associated - with that metric, as well as the aggregator used to export the - current checkpointed value. - - Returns: - The result of the export - """ - - def shutdown(self) -> None: - """Shuts down the exporter. - - Called when the SDK is shut down. - """ - - -class ConsoleMetricsExporter(MetricsExporter): - """Implementation of `MetricsExporter` that prints metrics to the console. - - This class can be used for diagnostic purposes. It prints the exported - metrics to the console STDOUT. - """ - - def export( - self, export_records: Sequence[ExportRecord] - ) -> "MetricsExportResult": - for export_record in export_records: - print( - '{}(data="{}", labels="{}", value={}, resource={})'.format( - type(self).__name__, - export_record.instrument, - export_record.labels, - export_record.aggregator.checkpoint, - export_record.resource.attributes, - ) - ) - return MetricsExportResult.SUCCESS diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py deleted file mode 100644 index 3781b9146b..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ /dev/null @@ -1,258 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import abc -import logging -import threading -from collections import OrderedDict, namedtuple -from math import inf - -from opentelemetry.util import time_ns - -logger = logging.getLogger(__name__) - - -class Aggregator(abc.ABC): - """Base class for aggregators. - - Aggregators are responsible for holding aggregated values and taking a - snapshot of these values upon export (checkpoint). - """ - - def __init__(self, config=None): - self._lock = threading.Lock() - self.last_update_timestamp = 0 - self.initial_checkpoint_timestamp = 0 - self.first_timestamp = time_ns() - self.checkpointed = True - if config is not None: - self.config = config - else: - self.config = {} - - @abc.abstractmethod - def update(self, value): - """Updates the current with the new value.""" - if self.checkpointed: - self.initial_checkpoint_timestamp = time_ns() - self.checkpointed = False - self.last_update_timestamp = time_ns() - - @abc.abstractmethod - def take_checkpoint(self): - """Stores a snapshot of the current value.""" - self.checkpointed = True - - @abc.abstractmethod - def merge(self, other): - """Combines two aggregator values.""" - self.last_update_timestamp = max( - self.last_update_timestamp, other.last_update_timestamp - ) - self.initial_checkpoint_timestamp = max( - self.initial_checkpoint_timestamp, - other.initial_checkpoint_timestamp, - ) - - def _verify_type(self, other): - if isinstance(other, self.__class__): - return True - logger.warning( - "Error in merging %s with %s.", - self.__class__.__name__, - other.__class__.__name__, - ) - return False - - -class SumAggregator(Aggregator): - """Aggregator for counter metrics.""" - - def __init__(self, config=None): - super().__init__(config=config) - self.current = 0 - self.checkpoint = 0 - - def update(self, value): - with self._lock: - self.current += value - super().update(value) - - def take_checkpoint(self): - with self._lock: - self.checkpoint = self.current - self.current = 0 - super().take_checkpoint() - - def merge(self, other): - if self._verify_type(other): - with self._lock: - self.checkpoint += other.checkpoint - super().merge(other) - - -class MinMaxSumCountAggregator(Aggregator): - """Aggregator for ValueRecorder metrics that keeps min, max, sum, count.""" - - _TYPE = namedtuple("minmaxsumcount", "min max sum count") - _EMPTY = _TYPE(inf, -inf, 0, 0) - - def __init__(self, config=None): - super().__init__(config=config) - self.current = self._EMPTY - self.checkpoint = self._EMPTY - - def update(self, value): - with self._lock: - self.current = self._TYPE( - min(self.current.min, value), - max(self.current.max, value), - self.current.sum + value, - self.current.count + 1, - ) - super().update(value) - - def take_checkpoint(self): - with self._lock: - self.checkpoint = self.current - self.current = self._EMPTY - super().take_checkpoint() - - def merge(self, other): - if self._verify_type(other): - with self._lock: - self.checkpoint = self._TYPE( - min(self.checkpoint.min, other.checkpoint.min), - max(self.checkpoint.max, other.checkpoint.max), - self.checkpoint.sum + other.checkpoint.sum, - self.checkpoint.count + other.checkpoint.count, - ) - super().merge(other) - - -class HistogramAggregator(Aggregator): - """Aggregator for ValueRecorder metrics that keeps a histogram of values.""" - - def __init__(self, config=None): - super().__init__(config=config) - # no buckets except < 0 and > - bounds = (0,) - config_bounds = self.config.get("bounds") - if config_bounds is not None: - if all( - config_bounds[i] < config_bounds[i + 1] - for i in range(len(config_bounds) - 1) - ): - bounds = config_bounds - else: - logger.warning( - "Bounds must be all different and sorted in increasing" - " order. Using default." - ) - - self.current = OrderedDict([(bb, 0) for bb in bounds]) - self.current[inf] = 0 - self.checkpoint = OrderedDict([(bb, 0) for bb in bounds]) - self.checkpoint[inf] = 0 - - def update(self, value): - with self._lock: - for bb in self.current.keys(): - # find first bucket that value is less than - if value < bb: - self.current[bb] += 1 - break - super().update(value) - - def take_checkpoint(self): - with self._lock: - self.checkpoint = self.current.copy() - for bb in self.current.keys(): - self.current[bb] = 0 - super().take_checkpoint() - - def merge(self, other): - if self._verify_type(other): - with self._lock: - if self.checkpoint.keys() == other.checkpoint.keys(): - for ii, bb in other.checkpoint.items(): - self.checkpoint[ii] += bb - super().merge(other) - else: - logger.warning( - "Cannot merge histograms with different buckets." - ) - - -class LastValueAggregator(Aggregator): - """Aggregator that stores last value results.""" - - def __init__(self, config=None): - super().__init__(config=config) - self.current = None - self.checkpoint = None - - def update(self, value): - with self._lock: - self.current = value - super().update(value) - - def take_checkpoint(self): - with self._lock: - self.checkpoint = self.current - self.current = None - super().take_checkpoint() - - def merge(self, other): - last = self.checkpoint - super().merge(other) - if self.last_update_timestamp == other.last_update_timestamp: - last = other.checkpoint - self.checkpoint = last - - -class ValueObserverAggregator(Aggregator): - """Same as MinMaxSumCount but also with last value.""" - - _TYPE = namedtuple("minmaxsumcountlast", "min max sum count last") - - def __init__(self, config=None): - super().__init__(config=config) - self.mmsc = MinMaxSumCountAggregator() - self.current = None - self.checkpoint = self._TYPE(None, None, None, 0, None) - - def update(self, value): - self.mmsc.update(value) - self.current = value - super().update(value) - - def take_checkpoint(self): - self.mmsc.take_checkpoint() - self.checkpoint = self._TYPE(*(self.mmsc.checkpoint + (self.current,))) - super().take_checkpoint() - - def merge(self, other): - if self._verify_type(other): - self.mmsc.merge(other.mmsc) - last = self.checkpoint.last - - self.last_update_timestamp = max( - self.last_update_timestamp, other.last_update_timestamp - ) - - if self.last_update_timestamp == other.last_update_timestamp: - last = other.checkpoint.last - self.checkpoint = self._TYPE(*(self.mmsc.checkpoint + (last,))) - super().merge(other) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py deleted file mode 100644 index 7c69468e61..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading - -from opentelemetry.context import attach, detach, set_value -from opentelemetry.metrics import Meter -from opentelemetry.sdk.metrics.export import MetricsExporter - - -class PushController(threading.Thread): - """A push based controller, used for collecting and exporting. - - Uses a worker thread that periodically collects metrics for exporting, - exports them and performs some post-processing. - - Args: - accumulator: The meter used to collect metrics. - exporter: The exporter used to export metrics. - interval: The collect/export interval in seconds. - """ - - daemon = True - - def __init__( - self, accumulator: Meter, exporter: MetricsExporter, interval: float - ): - super().__init__() - self.accumulator = accumulator - self.exporter = exporter - self.interval = interval - self.finished = threading.Event() - self.start() - - def run(self): - while not self.finished.wait(self.interval): - self.tick() - - def shutdown(self): - self.finished.set() - # Run one more collection pass to flush metrics batched in the meter - self.tick() - - def tick(self): - # Collect all of the meter's metrics to be exported - self.accumulator.collect() - # Export the collected metrics - token = attach(set_value("suppress_instrumentation", True)) - self.exporter.export(self.accumulator.processor.checkpoint_set()) - detach(token) - # Perform post-exporting logic - self.accumulator.processor.finished_collection() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py deleted file mode 100644 index 7261a8f228..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/in_memory_metrics_exporter.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading -from typing import Sequence - -from opentelemetry.sdk.metrics.export import ( - ExportRecord, - MetricsExporter, - MetricsExportResult, -) - - -class InMemoryMetricsExporter(MetricsExporter): - """Implementation of `MetricsExporter` that stores metrics in memory. - - This class can be used for testing purposes. It stores exported metrics - in a list in memory that can be retrieved using the - :func:`.get_exported_metrics` method. - """ - - def __init__(self): - self._exported_metrics = [] - self._stopped = False - self._lock = threading.Lock() - - def clear(self): - """Clear list of collected metrics.""" - with self._lock: - self._exported_metrics.clear() - - def export( - self, export_records: Sequence[ExportRecord] - ) -> MetricsExportResult: - if self._stopped: - return MetricsExportResult.FAILURE - - with self._lock: - self._exported_metrics.extend(export_records) - return MetricsExportResult.SUCCESS - - def get_exported_metrics(self): - """Get list of collected metrics.""" - with self._lock: - return tuple(self._exported_metrics) - - def shutdown(self) -> None: - """Shuts down the exporter. - - Called when the SDK is shut down. - """ - self._stopped = True diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py deleted file mode 100644 index 5575ce1666..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/processor.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Sequence - -from opentelemetry.sdk.metrics.export import ExportRecord -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util import get_dict_as_key - - -class Processor: - """Base class for all processor types. - - The processor is responsible for storing the aggregators and aggregated - values received from updates from metrics in the accumulator. The stored values - will be sent to an exporter for exporting. - """ - - def __init__(self, stateful: bool, resource: Resource): - self._batch_map = {} - # stateful=True indicates the processor computes checkpoints from over - # the process lifetime. False indicates the processor computes - # checkpoints which describe the updates of a single collection period - # (deltas) - self.stateful = stateful - self._resource = resource - - def checkpoint_set(self) -> Sequence[ExportRecord]: - """Returns a list of ExportRecords used for exporting. - - The list of ExportRecords is a snapshot created from the current - data in all of the aggregators in this processor. - """ - export_records = [] - # pylint: disable=W0612 - for ( - (instrument, aggregator_type, _, labels), - aggregator, - ) in self._batch_map.items(): - export_records.append( - ExportRecord(instrument, labels, aggregator, self._resource) - ) - return export_records - - def finished_collection(self): - """Performs certain post-export logic. - - For processors that are stateless, resets the batch map. - """ - if not self.stateful: - self._batch_map = {} - - def process(self, record) -> None: - """Stores record information to be ready for exporting.""" - # Checkpoints the current aggregator value to be collected for export - aggregator = record.aggregator - aggregator.take_checkpoint() - - # The uniqueness of a batch record is defined by a specific metric - # using an aggregator type with a specific set of labels. - # If two aggregators are the same but with different configs, they are still two valid unique records - # (for example, two histogram views with different buckets) - key = ( - record.instrument, - aggregator.__class__, - get_dict_as_key(aggregator.config), - record.labels, - ) - - batch_value = self._batch_map.get(key) - - if batch_value: - # Update the stored checkpointed value if exists. The call to merge - # here combines only identical records (same key). - batch_value.merge(aggregator) - return - - # create a copy of the aggregator and update - # it with the current checkpointed value for long-term storage - aggregator = record.aggregator.__class__( - config=record.aggregator.config - ) - aggregator.merge(record.aggregator) - - self._batch_map[key] = aggregator diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view.py deleted file mode 100644 index 0dd75c6887..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import threading -from collections import defaultdict -from typing import Optional, Sequence, Tuple - -from opentelemetry.metrics import ( - Counter, - InstrumentT, - SumObserver, - UpDownCounter, - UpDownSumObserver, - ValueObserver, - ValueRecorder, - ValueT, -) -from opentelemetry.sdk.metrics.export.aggregate import ( - Aggregator, - LastValueAggregator, - MinMaxSumCountAggregator, - SumAggregator, - ValueObserverAggregator, -) - -logger = logging.getLogger(__name__) - - -class ViewData: - def __init__(self, labels: Tuple[Tuple[str, str]], aggregator: Aggregator): - self.labels = labels - self.aggregator = aggregator - - def record(self, value: ValueT): - self.aggregator.update(value) - - # Uniqueness is based on labels and aggregator type - def __hash__(self): - return hash((self.labels, self.aggregator.__class__)) - - def __eq__(self, other): - return ( - self.labels == other.labels - and self.aggregator.__class__ == other.aggregator.__class__ - ) - - -class ViewConfig: - - UNGROUPED = 0 - LABEL_KEYS = 1 - DROP_ALL = 2 - - -class View: - def __init__( - self, - metric: InstrumentT, - aggregator: type, - aggregator_config: Optional[dict] = None, - label_keys: Optional[Sequence[str]] = None, - view_config: ViewConfig = ViewConfig.UNGROUPED, - ): - self.metric = metric - self.aggregator = aggregator - if aggregator_config is None: - aggregator_config = {} - self.aggregator_config = aggregator_config - if label_keys is None: - label_keys = [] - self.label_keys = sorted(label_keys) - self.view_config = view_config - self.view_datas = set() - - def get_view_data(self, labels): - """Find an existing ViewData for this set of labels. If that ViewData - does not exist, create a new one to represent the labels - """ - active_labels = [] - if self.view_config == ViewConfig.LABEL_KEYS: - # reduce the set of labels to only labels specified in label_keys - active_labels = { - (lk, lv) for lk, lv in labels if lk in set(self.label_keys) - } - active_labels = tuple(active_labels) - elif self.view_config == ViewConfig.UNGROUPED: - active_labels = labels - - for view_data in self.view_datas: - if view_data.labels == active_labels: - return view_data - new_view_data = ViewData( - active_labels, self.aggregator(self.aggregator_config) - ) - self.view_datas.add(new_view_data) - return new_view_data - - # Uniqueness is based on metric, aggregator type, aggregator config, - # ordered label keys and ViewConfig - def __hash__(self): - return hash( - ( - self.metric, - self.aggregator.__class__, - tuple(self.label_keys), - tuple(self.aggregator_config), - self.view_config, - ) - ) - - def __eq__(self, other): - return ( - self.metric == other.metric - and self.aggregator.__class__ == other.aggregator.__class__ - and self.label_keys == other.label_keys - and self.aggregator_config == other.aggregator_config - and self.view_config == other.view_config - ) - - -class ViewManager: - def __init__(self): - self.views = defaultdict(set) # Map[Metric, Set] - self._view_lock = threading.Lock() - self.view_datas = set() - - def register_view(self, view): - with self._view_lock: - if view not in self.views[view.metric]: - self.views[view.metric].add(view) - else: - logger.warning("View already registered.") - return - - def unregister_view(self, view): - with self._view_lock: - if self.views.get(view.metric) is None: - logger.warning("Metric for view does not exist.") - elif view in self.views.get(view.metric): - self.views.get(view.metric).remove(view) - - def get_view_datas(self, metric, labels): - view_datas = set() - views = self.views.get(metric) - # No views configured, use default aggregations - if views is None: - # make a default view for the metric - default_view = View(metric, get_default_aggregator(metric)) - self.register_view(default_view) - views = [default_view] - - for view in views: - view_datas.add(view.get_view_data(labels)) - - return view_datas - - -def get_default_aggregator(instrument: InstrumentT) -> Aggregator: - """Returns an aggregator based on metric instrument's type. - - Aggregators keep track of and updates values when metrics get updated. - """ - # pylint:disable=R0201 - instrument_type = instrument.__class__ - if issubclass(instrument_type, (Counter, UpDownCounter)): - return SumAggregator - if issubclass(instrument_type, (SumObserver, UpDownSumObserver)): - return LastValueAggregator - if issubclass(instrument_type, ValueRecorder): - return MinMaxSumCountAggregator - if issubclass(instrument_type, ValueObserver): - return ValueObserverAggregator - logger.warning("No default aggregator configured for: %s", instrument_type) - return SumAggregator diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index aae838fe9e..c840e5f298 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -25,10 +25,9 @@ Resource objects are created with `Resource.create`, which accepts attributes (key-values). Resource attributes can also be passed at process invocation in the :envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable. You should -register your resource with the `opentelemetry.sdk.metrics.MeterProvider` and -`opentelemetry.sdk.trace.TracerProvider` by passing them into their -constructors. The `Resource` passed to a provider is available to the -exporter, which can send on this information as it sees fit. +register your resource with the `opentelemetry.sdk.trace.TracerProvider` by +passing them into their constructors. The `Resource` passed to a provider is +available to the exporter, which can send on this information as it sees fit. .. code-block:: python diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index ee8111fdb1..c55f26ec62 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -16,8 +16,7 @@ class InstrumentationInfo: """Immutable information about an instrumentation library module. - See `opentelemetry.trace.TracerProvider.get_tracer` or - `opentelemetry.metrics.MeterProvider.get_meter` for the meaning of these + See `opentelemetry.trace.TracerProvider.get_tracer` for the meaning of these properties. """ diff --git a/opentelemetry-sdk/tests/metrics/__init__.py b/opentelemetry-sdk/tests/metrics/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/opentelemetry-sdk/tests/metrics/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/opentelemetry-sdk/tests/metrics/export/__init__.py b/opentelemetry-sdk/tests/metrics/export/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/opentelemetry-sdk/tests/metrics/export/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py deleted file mode 100644 index 632570a09d..0000000000 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ /dev/null @@ -1,717 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import concurrent.futures -import random -import unittest -from math import inf -from unittest import mock - -from opentelemetry.context import get_value -from opentelemetry.sdk import metrics -from opentelemetry.sdk.metrics.export import ( - ConsoleMetricsExporter, - ExportRecord, -) -from opentelemetry.sdk.metrics.export.aggregate import ( - LastValueAggregator, - MinMaxSumCountAggregator, - SumAggregator, - ValueObserverAggregator, -) -from opentelemetry.sdk.metrics.export.controller import PushController -from opentelemetry.sdk.metrics.export.processor import Processor -from opentelemetry.sdk.resources import Resource - - -# pylint: disable=protected-access -class TestConsoleMetricsExporter(unittest.TestCase): - # pylint: disable=no-self-use - def test_export(self): - meter_provider = metrics.MeterProvider() - meter = meter_provider.get_meter(__name__) - exporter = ConsoleMetricsExporter() - metric = metrics.Counter( - "available memory", - "available memory", - "bytes", - int, - meter, - ("environment",), - ) - labels = {"environment": "staging"} - aggregator = SumAggregator() - record = ExportRecord( - metric, labels, aggregator, meter_provider.resource - ) - result = '{}(data="{}", labels="{}", value={}, resource={})'.format( - ConsoleMetricsExporter.__name__, - metric, - labels, - aggregator.checkpoint, - meter_provider.resource.attributes, - ) - with mock.patch("sys.stdout") as mock_stdout: - exporter.export([record]) - mock_stdout.write.assert_any_call(result) - - -class TestProcessor(unittest.TestCase): - def test_checkpoint_set(self): - meter_provider = metrics.MeterProvider() - meter = meter_provider.get_meter(__name__) - processor = Processor(True, meter_provider.resource) - aggregator = SumAggregator() - metric = metrics.Counter( - "available memory", "available memory", "bytes", int, meter - ) - aggregator.update(1.0) - labels = () - _batch_map = {} - _batch_map[(metric, SumAggregator, tuple(), labels)] = aggregator - processor._batch_map = _batch_map - records = processor.checkpoint_set() - self.assertEqual(len(records), 1) - self.assertEqual(records[0].instrument, metric) - self.assertEqual(records[0].labels, labels) - self.assertEqual(records[0].aggregator, aggregator) - - def test_checkpoint_set_empty(self): - processor = Processor(True, Resource.create_empty()) - records = processor.checkpoint_set() - self.assertEqual(len(records), 0) - - def test_finished_collection_stateless(self): - meter_provider = metrics.MeterProvider() - meter = meter_provider.get_meter(__name__) - processor = Processor(False, meter_provider.resource) - aggregator = SumAggregator() - metric = metrics.Counter( - "available memory", "available memory", "bytes", int, meter - ) - aggregator.update(1.0) - labels = () - _batch_map = {} - _batch_map[(metric, SumAggregator, tuple(), labels)] = aggregator - processor._batch_map = _batch_map - processor.finished_collection() - self.assertEqual(len(processor._batch_map), 0) - - def test_finished_collection_stateful(self): - meter_provider = metrics.MeterProvider() - meter = meter_provider.get_meter(__name__) - processor = Processor(True, meter_provider.resource) - aggregator = SumAggregator() - metric = metrics.Counter( - "available memory", "available memory", "bytes", int, meter - ) - aggregator.update(1.0) - labels = () - _batch_map = {} - _batch_map[(metric, SumAggregator, tuple(), labels)] = aggregator - processor._batch_map = _batch_map - processor.finished_collection() - self.assertEqual(len(processor._batch_map), 1) - - def test_processor_process_exists(self): - meter_provider = metrics.MeterProvider() - meter = meter_provider.get_meter(__name__) - processor = Processor(True, meter_provider.resource) - aggregator = SumAggregator() - aggregator2 = SumAggregator() - metric = metrics.Counter( - "available memory", "available memory", "bytes", int, meter - ) - labels = () - _batch_map = {} - batch_key = (metric, SumAggregator, tuple(), labels) - _batch_map[batch_key] = aggregator - aggregator2.update(1.0) - processor._batch_map = _batch_map - accumulation = metrics.Accumulation(metric, labels, aggregator2) - processor.process(accumulation) - self.assertEqual(len(processor._batch_map), 1) - self.assertIsNotNone(processor._batch_map.get(batch_key)) - self.assertEqual(processor._batch_map.get(batch_key).current, 0) - self.assertEqual(processor._batch_map.get(batch_key).checkpoint, 1.0) - - def test_processor_process_not_exists(self): - meter_provider = metrics.MeterProvider() - meter = meter_provider.get_meter(__name__) - processor = Processor(True, meter_provider.resource) - aggregator = SumAggregator() - metric = metrics.Counter( - "available memory", "available memory", "bytes", int, meter - ) - labels = () - _batch_map = {} - batch_key = (metric, SumAggregator, tuple(), labels) - aggregator.update(1.0) - processor._batch_map = _batch_map - accumulation = metrics.Accumulation(metric, labels, aggregator) - processor.process(accumulation) - self.assertEqual(len(processor._batch_map), 1) - self.assertIsNotNone(processor._batch_map.get(batch_key)) - self.assertEqual(processor._batch_map.get(batch_key).current, 0) - self.assertEqual(processor._batch_map.get(batch_key).checkpoint, 1.0) - - def test_processor_process_not_stateful(self): - meter_provider = metrics.MeterProvider() - processor = Processor(True, meter_provider.resource) - aggregator = SumAggregator() - metric = metrics.Counter( - "available memory", - "available memory", - "bytes", - int, - meter_provider.get_meter(__name__), - ) - labels = () - _batch_map = {} - batch_key = (metric, SumAggregator, tuple(), labels) - aggregator.update(1.0) - processor._batch_map = _batch_map - accumulation = metrics.Accumulation(metric, labels, aggregator) - processor.process(accumulation) - self.assertEqual(len(processor._batch_map), 1) - self.assertIsNotNone(processor._batch_map.get(batch_key)) - self.assertEqual(processor._batch_map.get(batch_key).current, 0) - self.assertEqual(processor._batch_map.get(batch_key).checkpoint, 1.0) - - -class TestSumAggregator(unittest.TestCase): - @staticmethod - def call_update(sum_agg): - update_total = 0 - for _ in range(0, 100000): - val = random.getrandbits(32) - sum_agg.update(val) - update_total += val - return update_total - - @mock.patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") - def test_update(self, time_mock): - time_mock.return_value = 123 - sum_agg = SumAggregator() - sum_agg.update(1.0) - sum_agg.update(2.0) - self.assertEqual(sum_agg.current, 3.0) - self.assertEqual(sum_agg.last_update_timestamp, 123) - - def test_checkpoint(self): - sum_agg = SumAggregator() - sum_agg.update(2.0) - sum_agg.take_checkpoint() - self.assertEqual(sum_agg.current, 0) - self.assertEqual(sum_agg.checkpoint, 2.0) - - def test_merge(self): - sum_agg = SumAggregator() - sum_agg2 = SumAggregator() - sum_agg.checkpoint = 1.0 - sum_agg2.checkpoint = 3.0 - sum_agg2.last_update_timestamp = 123 - sum_agg.merge(sum_agg2) - self.assertEqual(sum_agg.checkpoint, 4.0) - self.assertEqual(sum_agg.last_update_timestamp, 123) - - def test_concurrent_update(self): - sum_agg = SumAggregator() - - with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: - fut1 = executor.submit(self.call_update, sum_agg) - fut2 = executor.submit(self.call_update, sum_agg) - - updapte_total = fut1.result() + fut2.result() - - sum_agg.take_checkpoint() - self.assertEqual(updapte_total, sum_agg.checkpoint) - - def test_concurrent_update_and_checkpoint(self): - sum_agg = SumAggregator() - checkpoint_total = 0 - - with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: - fut = executor.submit(self.call_update, sum_agg) - - while not fut.done(): - sum_agg.take_checkpoint() - checkpoint_total += sum_agg.checkpoint - - sum_agg.take_checkpoint() - checkpoint_total += sum_agg.checkpoint - - self.assertEqual(fut.result(), checkpoint_total) - - -class TestMinMaxSumCountAggregator(unittest.TestCase): - @staticmethod - def call_update(mmsc): - min_ = float("inf") - max_ = float("-inf") - sum_ = 0 - count_ = 0 - for _ in range(0, 100000): - val = random.getrandbits(32) - mmsc.update(val) - if val < min_: - min_ = val - if val > max_: - max_ = val - sum_ += val - count_ += 1 - return MinMaxSumCountAggregator._TYPE(min_, max_, sum_, count_) - - @mock.patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") - def test_update(self, time_mock): - time_mock.return_value = 123 - mmsc = MinMaxSumCountAggregator() - # test current values without any update - self.assertEqual(mmsc.current, MinMaxSumCountAggregator._EMPTY) - - # call update with some values - values = (3, 50, 3, 97) - for val in values: - mmsc.update(val) - - self.assertEqual( - mmsc.current, (min(values), max(values), sum(values), len(values)) - ) - self.assertEqual(mmsc.last_update_timestamp, 123) - - def test_checkpoint(self): - mmsc = MinMaxSumCountAggregator() - - # take checkpoint wihtout any update - mmsc.take_checkpoint() - self.assertEqual(mmsc.checkpoint, MinMaxSumCountAggregator._EMPTY) - - # call update with some values - values = (3, 50, 3, 97) - for val in values: - mmsc.update(val) - - mmsc.take_checkpoint() - self.assertEqual( - mmsc.checkpoint, - (min(values), max(values), sum(values), len(values)), - ) - - self.assertEqual(mmsc.current, MinMaxSumCountAggregator._EMPTY) - - def test_merge(self): - mmsc1 = MinMaxSumCountAggregator() - mmsc2 = MinMaxSumCountAggregator() - - checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) - checkpoint2 = MinMaxSumCountAggregator._TYPE(1, 33, 44, 2) - - mmsc1.checkpoint = checkpoint1 - mmsc2.checkpoint = checkpoint2 - - mmsc1.last_update_timestamp = 100 - mmsc2.last_update_timestamp = 123 - - mmsc1.merge(mmsc2) - - mmsc1_checkpoint = mmsc1.checkpoint - mmsc1.checkpoint = checkpoint1 - mmsc2.checkpoint = checkpoint2 - - mmsc1.merge(mmsc2) - - self.assertEqual(mmsc1_checkpoint, mmsc1.checkpoint) - - self.assertEqual(mmsc1.last_update_timestamp, 123) - - def test_merge_checkpoint(self): - type_ = MinMaxSumCountAggregator._TYPE - empty = MinMaxSumCountAggregator._EMPTY - - mmsc0 = MinMaxSumCountAggregator() - mmsc1 = MinMaxSumCountAggregator() - - mmsc0.checkpoint = empty - mmsc1.checkpoint = empty - - mmsc0.merge(mmsc1) - self.assertEqual(mmsc0.checkpoint, mmsc1.checkpoint) - - mmsc0.checkpoint = empty - mmsc1.checkpoint = type_(0, 0, 0, 0) - - mmsc0.merge(mmsc1) - self.assertEqual(mmsc0.checkpoint, mmsc1.checkpoint) - - mmsc0.checkpoint = type_(0, 0, 0, 0) - mmsc1.checkpoint = empty - - mmsc1.merge(mmsc0) - self.assertEqual(mmsc1.checkpoint, mmsc0.checkpoint) - - mmsc0.checkpoint = type_(0, 0, 0, 0) - mmsc1.checkpoint = type_(0, 0, 0, 0) - - mmsc0.merge(mmsc1) - self.assertEqual(mmsc1.checkpoint, mmsc0.checkpoint) - - mmsc0.checkpoint = type_(44, 23, 55, 86) - mmsc1.checkpoint = empty - - mmsc0.merge(mmsc1) - self.assertEqual(mmsc0.checkpoint, type_(44, 23, 55, 86)) - - mmsc0.checkpoint = type_(3, 150, 101, 3) - mmsc1.checkpoint = type_(1, 33, 44, 2) - - mmsc0.merge(mmsc1) - self.assertEqual(mmsc0.checkpoint, type_(1, 150, 101 + 44, 2 + 3)) - - def test_merge_with_empty(self): - mmsc1 = MinMaxSumCountAggregator() - mmsc2 = MinMaxSumCountAggregator() - - checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) - mmsc1.checkpoint = checkpoint1 - - mmsc1.merge(mmsc2) - - self.assertEqual(mmsc1.checkpoint, checkpoint1) - - def test_concurrent_update(self): - mmsc0 = MinMaxSumCountAggregator() - mmsc1 = MinMaxSumCountAggregator() - - with concurrent.futures.ThreadPoolExecutor(max_workers=2) as ex: - mmsc0.checkpoint = ex.submit(self.call_update, mmsc0).result() - mmsc1.checkpoint = ex.submit(self.call_update, mmsc0).result() - - mmsc0.merge(mmsc1) - - mmsc0_checkpoint = mmsc0.checkpoint - - mmsc0.take_checkpoint() - - self.assertEqual(mmsc0_checkpoint, mmsc0.checkpoint) - self.assertIsNot(mmsc0_checkpoint, mmsc0.checkpoint) - - def test_concurrent_update_and_checkpoint(self): - mmsc0 = MinMaxSumCountAggregator() - mmsc1 = MinMaxSumCountAggregator() - mmsc1.checkpoint = MinMaxSumCountAggregator._TYPE(2 ** 32, 0, 0, 0) - - with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex: - fut = ex.submit(self.call_update, mmsc0) - - while not fut.done(): - mmsc0.take_checkpoint() - mmsc0.merge(mmsc1) - mmsc1.checkpoint = mmsc0.checkpoint - - mmsc0.take_checkpoint() - mmsc0.merge(mmsc1) - - self.assertEqual(mmsc0.checkpoint, fut.result()) - - -class TestValueObserverAggregator(unittest.TestCase): - @mock.patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") - def test_update(self, time_mock): - time_mock.return_value = 123 - observer = ValueObserverAggregator() - # test current values without any update - self.assertEqual(observer.mmsc.current, (inf, -inf, 0, 0)) - self.assertIsNone(observer.current) - - # call update with some values - values = (3, 50, 3, 97, 27) - for val in values: - observer.update(val) - - self.assertEqual( - observer.mmsc.current, - (min(values), max(values), sum(values), len(values)), - ) - self.assertEqual(observer.last_update_timestamp, 123) - - self.assertEqual(observer.current, values[-1]) - - def test_checkpoint(self): - observer = ValueObserverAggregator() - - # take checkpoint wihtout any update - observer.take_checkpoint() - self.assertEqual(observer.checkpoint, (inf, -inf, 0, 0, None)) - - # call update with some values - values = (3, 50, 3, 97) - for val in values: - observer.update(val) - - observer.take_checkpoint() - self.assertEqual( - observer.checkpoint, - (min(values), max(values), sum(values), len(values), values[-1]), - ) - - def test_merge(self): - observer1 = ValueObserverAggregator() - observer2 = ValueObserverAggregator() - - mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) - mmsc_checkpoint2 = MinMaxSumCountAggregator._TYPE(1, 33, 44, 2) - - checkpoint1 = ValueObserverAggregator._TYPE( - *(mmsc_checkpoint1 + (23,)) - ) - - checkpoint2 = ValueObserverAggregator._TYPE( - *(mmsc_checkpoint2 + (27,)) - ) - - observer1.mmsc.checkpoint = mmsc_checkpoint1 - observer2.mmsc.checkpoint = mmsc_checkpoint2 - - observer1.last_update_timestamp = 100 - observer2.last_update_timestamp = 123 - - observer1.checkpoint = checkpoint1 - observer2.checkpoint = checkpoint2 - - observer1.merge(observer2) - - self.assertEqual( - observer1.checkpoint, - ( - min(checkpoint1.min, checkpoint2.min), - max(checkpoint1.max, checkpoint2.max), - checkpoint1.sum + checkpoint2.sum, - checkpoint1.count + checkpoint2.count, - checkpoint2.last, - ), - ) - self.assertEqual(observer1.last_update_timestamp, 123) - - def test_merge_last_updated(self): - observer1 = ValueObserverAggregator() - observer2 = ValueObserverAggregator() - - mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) - mmsc_checkpoint2 = MinMaxSumCountAggregator._TYPE(1, 33, 44, 2) - - checkpoint1 = ValueObserverAggregator._TYPE( - *(mmsc_checkpoint1 + (23,)) - ) - - checkpoint2 = ValueObserverAggregator._TYPE( - *(mmsc_checkpoint2 + (27,)) - ) - - observer1.mmsc.checkpoint = mmsc_checkpoint1 - observer2.mmsc.checkpoint = mmsc_checkpoint2 - - observer1.last_update_timestamp = 123 - observer2.last_update_timestamp = 100 - - observer1.checkpoint = checkpoint1 - observer2.checkpoint = checkpoint2 - - observer1.merge(observer2) - - self.assertEqual( - observer1.checkpoint, - ( - min(checkpoint1.min, checkpoint2.min), - max(checkpoint1.max, checkpoint2.max), - checkpoint1.sum + checkpoint2.sum, - checkpoint1.count + checkpoint2.count, - checkpoint1.last, - ), - ) - self.assertEqual(observer1.last_update_timestamp, 123) - - def test_merge_last_updated_none(self): - observer1 = ValueObserverAggregator() - observer2 = ValueObserverAggregator() - - mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) - mmsc_checkpoint2 = MinMaxSumCountAggregator._TYPE(1, 33, 44, 2) - - checkpoint1 = ValueObserverAggregator._TYPE( - *(mmsc_checkpoint1 + (23,)) - ) - - checkpoint2 = ValueObserverAggregator._TYPE( - *(mmsc_checkpoint2 + (27,)) - ) - - observer1.mmsc.checkpoint = mmsc_checkpoint1 - observer2.mmsc.checkpoint = mmsc_checkpoint2 - - observer1.last_update_timestamp = 0 - observer2.last_update_timestamp = 100 - - observer1.checkpoint = checkpoint1 - observer2.checkpoint = checkpoint2 - - observer1.merge(observer2) - - self.assertEqual( - observer1.checkpoint, - ( - min(checkpoint1.min, checkpoint2.min), - max(checkpoint1.max, checkpoint2.max), - checkpoint1.sum + checkpoint2.sum, - checkpoint1.count + checkpoint2.count, - checkpoint2.last, - ), - ) - self.assertEqual(observer1.last_update_timestamp, 100) - - def test_merge_with_empty(self): - observer1 = ValueObserverAggregator() - observer2 = ValueObserverAggregator() - - mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) - checkpoint1 = ValueObserverAggregator._TYPE( - *(mmsc_checkpoint1 + (23,)) - ) - - observer1.mmsc.checkpoint = mmsc_checkpoint1 - observer1.checkpoint = checkpoint1 - observer1.last_update_timestamp = 100 - - observer1.merge(observer2) - - self.assertEqual(observer1.checkpoint, checkpoint1) - - -class TestLastValueAggregator(unittest.TestCase): - @mock.patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") - def test_update(self, time_mock): - time_mock.return_value = 123 - observer = LastValueAggregator() - # test current values without any update - self.assertIsNone(observer.current) - - # call update with some values - values = (3, 50, 3, 97, 27) - for val in values: - observer.update(val) - - self.assertEqual(observer.last_update_timestamp, 123) - self.assertEqual(observer.current, values[-1]) - - def test_checkpoint(self): - observer = LastValueAggregator() - - # take checkpoint without any update - observer.take_checkpoint() - self.assertEqual(observer.checkpoint, None) - - # call update with some values - values = (3, 50, 3, 97) - for val in values: - observer.update(val) - - observer.take_checkpoint() - self.assertEqual(observer.checkpoint, 97) - - def test_merge(self): - observer1 = LastValueAggregator() - observer2 = LastValueAggregator() - - observer1.checkpoint = 23 - observer2.checkpoint = 47 - - observer1.last_update_timestamp = 100 - observer2.last_update_timestamp = 123 - - observer1.merge(observer2) - - self.assertEqual(observer1.checkpoint, 47) - self.assertEqual(observer1.last_update_timestamp, 123) - - def test_merge_last_updated(self): - observer1 = LastValueAggregator() - observer2 = LastValueAggregator() - - observer1.checkpoint = 23 - observer2.checkpoint = 47 - - observer1.last_update_timestamp = 123 - observer2.last_update_timestamp = 100 - - observer1.merge(observer2) - - self.assertEqual(observer1.checkpoint, 23) - self.assertEqual(observer1.last_update_timestamp, 123) - - def test_merge_last_updated_none(self): - observer1 = LastValueAggregator() - observer2 = LastValueAggregator() - - observer1.checkpoint = 23 - observer2.checkpoint = 47 - - observer1.last_update_timestamp = 0 - observer2.last_update_timestamp = 100 - - observer1.merge(observer2) - - self.assertEqual(observer1.checkpoint, 47) - self.assertEqual(observer1.last_update_timestamp, 100) - - def test_merge_with_empty(self): - observer1 = LastValueAggregator() - observer2 = LastValueAggregator() - - observer1.checkpoint = 23 - observer1.last_update_timestamp = 100 - - observer1.merge(observer2) - - self.assertEqual(observer1.checkpoint, 23) - self.assertEqual(observer1.last_update_timestamp, 100) - - -class TestController(unittest.TestCase): - def test_push_controller(self): - meter = mock.Mock() - exporter = mock.Mock() - controller = PushController(meter, exporter, 5.0) - meter.collect.assert_not_called() - exporter.export.assert_not_called() - - controller.shutdown() - self.assertTrue(controller.finished.isSet()) - - # shutdown should flush the meter - self.assertEqual(meter.collect.call_count, 1) - self.assertEqual(exporter.export.call_count, 1) - - def test_push_controller_suppress_instrumentation(self): - meter = mock.Mock() - exporter = mock.Mock() - exporter.export = lambda x: self.assertIsNotNone( - get_value("suppress_instrumentation") - ) - with mock.patch( - "opentelemetry.context._RUNTIME_CONTEXT" - ) as context_patch: - controller = PushController(meter, exporter, 30.0) - controller.tick() - self.assertEqual(context_patch.attach.called, True) - self.assertEqual(context_patch.detach.called, True) - self.assertEqual(get_value("suppress_instrumentation"), None) diff --git a/opentelemetry-sdk/tests/metrics/test_globals.py b/opentelemetry-sdk/tests/metrics/test_globals.py deleted file mode 100644 index 513dd7dd2d..0000000000 --- a/opentelemetry-sdk/tests/metrics/test_globals.py +++ /dev/null @@ -1,23 +0,0 @@ -# type:ignore -import unittest -from logging import WARNING - -from opentelemetry import metrics -from opentelemetry.sdk.metrics import MeterProvider - - -class TestGlobals(unittest.TestCase): - def test_meter_provider_override_warning(self): - """metrics.set_meter_provider should throw a warning when overridden""" - metrics.set_meter_provider(MeterProvider()) - with self.assertLogs(level=WARNING) as test: - metrics.set_meter_provider(MeterProvider()) - self.assertEqual( - test.output, - [ - ( - "WARNING:opentelemetry.metrics:Overriding of current " - "MeterProvider is not allowed" - ) - ], - ) diff --git a/opentelemetry-sdk/tests/metrics/test_implementation.py b/opentelemetry-sdk/tests/metrics/test_implementation.py deleted file mode 100644 index 0a2fbd5740..0000000000 --- a/opentelemetry-sdk/tests/metrics/test_implementation.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -from opentelemetry.metrics import DefaultCounter, DefaultMeter -from opentelemetry.sdk import metrics - - -class TestMeterImplementation(unittest.TestCase): - """ - This test is in place to ensure the SDK implementation of the API - is returning values that are valid. The same tests have been added - to the API with different expected results. See issue for more details: - https://github.com/open-telemetry/opentelemetry-python/issues/142 - """ - - def test_meter(self): - meter = metrics.MeterProvider().get_meter(__name__) - metric = meter.create_counter("", "", "", float) - self.assertNotIsInstance(meter, DefaultMeter) - self.assertNotIsInstance(metric, DefaultCounter) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py deleted file mode 100644 index aa628655c4..0000000000 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ /dev/null @@ -1,662 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -from unittest.mock import Mock, patch - -from opentelemetry import metrics as metrics_api -from opentelemetry.sdk import metrics, resources -from opentelemetry.sdk.metrics.export.aggregate import ( - MinMaxSumCountAggregator, - SumAggregator, -) -from opentelemetry.sdk.metrics.view import View - - -class TestMeterProvider(unittest.TestCase): - def test_stateful(self): - meter_provider = metrics.MeterProvider(stateful=False) - meter = meter_provider.get_meter(__name__) - self.assertIs(meter.processor.stateful, False) - - def test_resource_empty(self): - resource = resources.Resource.create_empty() - meter_provider = metrics.MeterProvider(resource=resource) - self.assertIs(meter_provider.resource, resource) - - def test_resources(self): - meter_provider = metrics.MeterProvider() - # pylint: disable=protected-access - self.assertIsInstance(meter_provider.resource, resources.Resource) - self.assertEqual( - meter_provider.resource.attributes.get(resources.SERVICE_NAME), - "unknown_service", - ) - self.assertEqual( - meter_provider.resource.attributes.get( - resources.TELEMETRY_SDK_LANGUAGE - ), - "python", - ) - self.assertEqual( - meter_provider.resource.attributes.get( - resources.TELEMETRY_SDK_NAME - ), - "opentelemetry", - ) - self.assertEqual( - meter_provider.resource.attributes.get( - resources.TELEMETRY_SDK_VERSION - ), - resources.OPENTELEMETRY_SDK_VERSION, - ) - - def test_start_pipeline(self): - exporter = Mock() - meter_provider = metrics.MeterProvider() - meter = meter_provider.get_meter(__name__) - # pylint: disable=protected-access - meter_provider.start_pipeline(meter, exporter, 6) - try: - self.assertEqual(len(meter_provider._exporters), 1) - self.assertEqual(len(meter_provider._controllers), 1) - finally: - meter_provider.shutdown() - - def test_shutdown(self): - controller = Mock() - exporter = Mock() - meter_provider = metrics.MeterProvider() - # pylint: disable=protected-access - meter_provider._controllers = [controller] - meter_provider._exporters = [exporter] - meter_provider.shutdown() - self.assertEqual(controller.shutdown.call_count, 1) - self.assertEqual(exporter.shutdown.call_count, 1) - self.assertIsNone(meter_provider._atexit_handler) - - -class TestMeter(unittest.TestCase): - def test_extends_api(self): - meter = metrics.MeterProvider().get_meter(__name__) - self.assertIsInstance(meter, metrics_api.Meter) - - def test_collect_metrics(self): - meter = metrics.MeterProvider().get_meter(__name__) - processor_mock = Mock() - meter.processor = processor_mock - counter = meter.create_counter("name", "desc", "unit", float) - labels = {"key1": "value1"} - meter.register_view(View(counter, SumAggregator)) - counter.add(1.0, labels) - meter.collect() - self.assertTrue(processor_mock.process.called) - - def test_collect_no_metrics(self): - meter = metrics.MeterProvider().get_meter(__name__) - processor_mock = Mock() - meter.processor = processor_mock - meter.collect() - self.assertFalse(processor_mock.process.called) - - def test_collect_not_registered(self): - meter = metrics.MeterProvider().get_meter(__name__) - processor_mock = Mock() - meter.processor = processor_mock - counter = metrics.Counter("name", "desc", "unit", float, meter) - labels = {"key1": "value1"} - counter.add(1.0, labels) - meter.collect() - self.assertFalse(processor_mock.process.called) - - def test_collect_disabled_metric(self): - meter = metrics.MeterProvider().get_meter(__name__) - processor_mock = Mock() - meter.processor = processor_mock - counter = metrics.Counter("name", "desc", "unit", float, meter, False) - labels = {"key1": "value1"} - meter.register_view(View(counter, SumAggregator)) - counter.add(1.0, labels) - meter.collect() - self.assertFalse(processor_mock.process.called) - - def test_collect_observers(self): - meter = metrics.MeterProvider().get_meter(__name__) - processor_mock = Mock() - meter.processor = processor_mock - - def callback(observer): - self.assertIsInstance(observer, metrics_api.Observer) - observer.observe(45, {}) - - meter.register_valueobserver( - callback, "name", "desc", "unit", int, (), True - ) - meter.collect() - self.assertTrue(processor_mock.process.called) - - def test_record_batch(self): - meter = metrics.MeterProvider().get_meter(__name__) - labels = {"key1": "value1", "key2": "value2", "key3": "value3"} - counter = metrics.Counter("name", "desc", "unit", float, meter) - valuerecorder = metrics.ValueRecorder( - "name", "desc", "unit", float, meter - ) - counter_v = View(counter, SumAggregator) - measure_v = View(valuerecorder, MinMaxSumCountAggregator) - meter.register_view(counter_v) - meter.register_view(measure_v) - record_tuples = [(counter, 1.0), (valuerecorder, 3.0)] - meter.record_batch(labels, record_tuples) - labels_key = metrics.get_dict_as_key(labels) - self.assertEqual( - counter.bound_instruments[labels_key] - .view_datas.pop() - .aggregator.current, - 1.0, - ) - self.assertEqual( - valuerecorder.bound_instruments[labels_key] - .view_datas.pop() - .aggregator.current, - (3.0, 3.0, 3.0, 1), - ) - - def test_create_counter(self): - resource = Mock(spec=resources.Resource) - meter_provider = metrics.MeterProvider(resource=resource) - meter = meter_provider.get_meter(__name__) - counter = meter.create_counter("name", "desc", "unit", int) - self.assertIsInstance(counter, metrics.Counter) - self.assertEqual(counter.value_type, int) - self.assertEqual(counter.name, "name") - self.assertIs(meter_provider.resource, resource) - self.assertEqual(counter.meter, meter) - - def test_instrument_same_name_error(self): - resource = Mock(spec=resources.Resource) - meter_provider = metrics.MeterProvider(resource=resource) - meter = meter_provider.get_meter(__name__) - counter = meter.create_counter("name", "desc", "unit", int) - self.assertIsInstance(counter, metrics.Counter) - self.assertEqual(counter.value_type, int) - self.assertEqual(counter.name, "name") - self.assertIs(meter_provider.resource, resource) - self.assertEqual(counter.meter, meter) - with self.assertRaises(ValueError) as ctx: - _ = meter.create_counter("naME", "desc", "unit", int) - self.assertTrue( - "Multiple instruments can't be registered by the same name: (name)" - in str(ctx.exception) - ) - - def test_create_updowncounter(self): - meter = metrics.MeterProvider().get_meter(__name__) - updowncounter = meter.create_updowncounter( - "name", "desc", "unit", float - ) - self.assertIsInstance(updowncounter, metrics.UpDownCounter) - self.assertEqual(updowncounter.value_type, float) - self.assertEqual(updowncounter.name, "name") - - def test_create_valuerecorder(self): - meter = metrics.MeterProvider().get_meter(__name__) - valuerecorder = meter.create_valuerecorder( - "name", "desc", "unit", float - ) - self.assertIsInstance(valuerecorder, metrics.ValueRecorder) - self.assertEqual(valuerecorder.value_type, float) - self.assertEqual(valuerecorder.name, "name") - self.assertEqual(valuerecorder.meter, meter) - - def test_register_sumobserver(self): - meter = metrics.MeterProvider().get_meter(__name__) - - callback = Mock() - - observer = meter.register_sumobserver( - callback, "name", "desc", "unit", int - ) - - self.assertIsInstance(observer, metrics.SumObserver) - self.assertEqual(len(meter.instruments), 1) - - self.assertEqual(observer.callback, callback) - self.assertEqual(observer.name, "name") - self.assertEqual(observer.description, "desc") - self.assertEqual(observer.unit, "unit") - self.assertEqual(observer.value_type, int) - self.assertEqual(observer.label_keys, ()) - self.assertTrue(observer.enabled) - - def test_register_updownsumobserver(self): - meter = metrics.MeterProvider().get_meter(__name__) - - callback = Mock() - - observer = meter.register_updownsumobserver( - callback, "name", "desc", "unit", int - ) - - self.assertIsInstance(observer, metrics.UpDownSumObserver) - self.assertEqual(len(meter.instruments), 1) - - self.assertEqual(observer.callback, callback) - self.assertEqual(observer.name, "name") - self.assertEqual(observer.description, "desc") - self.assertEqual(observer.unit, "unit") - self.assertEqual(observer.value_type, int) - self.assertEqual(observer.label_keys, ()) - self.assertTrue(observer.enabled) - - def test_register_valueobserver(self): - meter = metrics.MeterProvider().get_meter(__name__) - - callback = Mock() - - observer = meter.register_valueobserver( - callback, "name", "desc", "unit", int - ) - - self.assertIsInstance(observer, metrics.ValueObserver) - self.assertEqual(len(meter.instruments), 1) - - self.assertEqual(observer.callback, callback) - self.assertEqual(observer.name, "name") - self.assertEqual(observer.description, "desc") - self.assertEqual(observer.unit, "unit") - self.assertEqual(observer.value_type, int) - self.assertEqual(observer.label_keys, ()) - self.assertTrue(observer.enabled) - - def test_unregister_observer(self): - meter = metrics.MeterProvider().get_meter(__name__) - - callback = Mock() - - observer = meter.register_valueobserver( - callback, "name", "desc", "unit", int, metrics.ValueObserver - ) - - meter.unregister_observer(observer) - self.assertEqual(len(meter.instruments), 0) - - def test_unregister_and_reregister_observer(self): - meter = metrics.MeterProvider().get_meter(__name__) - - callback = Mock() - - observer = meter.register_valueobserver( - callback, - "nameCaSEinSENsitive", - "desc", - "unit", - int, - metrics.ValueObserver, - ) - - meter.unregister_observer(observer) - self.assertEqual(len(meter.instruments), 0) - observer = meter.register_valueobserver( - callback, "name", "desc", "unit", int, metrics.ValueObserver - ) - - -class TestMetric(unittest.TestCase): - def test_bind(self): - meter = metrics.MeterProvider().get_meter(__name__) - metric_types = [ - metrics.Counter, - metrics.UpDownCounter, - metrics.ValueRecorder, - ] - labels = {"key": "value"} - key_labels = metrics.get_dict_as_key(labels) - for _type in metric_types: - metric = _type("name", "desc", "unit", int, meter) - bound_instrument = metric.bind(labels) - self.assertEqual( - metric.bound_instruments.get(key_labels), bound_instrument - ) - - -class TestCounter(unittest.TestCase): - def test_add(self): - meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.Counter("name", "desc", "unit", int, meter) - labels = {"key": "value"} - counter_v = View(metric, SumAggregator) - meter.register_view(counter_v) - bound_mock = metric.bind(labels) - metric.add(3, labels) - metric.add(2, labels) - self.assertEqual(bound_mock.view_datas.pop().aggregator.current, 5) - - @patch("opentelemetry.sdk.metrics.logger") - def test_add_non_decreasing_int_error(self, logger_mock): - meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.Counter("name", "desc", "unit", int, meter) - labels = {"key": "value"} - counter_v = View(metric, SumAggregator) - meter.register_view(counter_v) - bound_counter = metric.bind(labels) - metric.add(3, labels) - metric.add(0, labels) - metric.add(-1, labels) - self.assertEqual(bound_counter.view_datas.pop().aggregator.current, 3) - self.assertEqual(logger_mock.warning.call_count, 1) - - @patch("opentelemetry.sdk.metrics.logger") - def test_add_non_decreasing_float_error(self, logger_mock): - meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.Counter("name", "desc", "unit", float, meter) - labels = {"key": "value"} - counter_v = View(metric, SumAggregator) - meter.register_view(counter_v) - bound_counter = metric.bind(labels) - metric.add(3.3, labels) - metric.add(0.0, labels) - metric.add(0.1, labels) - metric.add(-0.1, labels) - self.assertEqual( - bound_counter.view_datas.pop().aggregator.current, 3.4 - ) - self.assertEqual(logger_mock.warning.call_count, 1) - - -class TestUpDownCounter(unittest.TestCase): - def test_add(self): - meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.UpDownCounter("name", "desc", "unit", int, meter) - labels = {"key": "value"} - bound_counter = metric.bind(labels) - counter_v = View(metric, SumAggregator) - meter.register_view(counter_v) - metric.add(3, labels) - metric.add(2, labels) - self.assertEqual(bound_counter.view_datas.pop().aggregator.current, 5) - - -class TestValueRecorder(unittest.TestCase): - def test_record(self): - meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.ValueRecorder("name", "desc", "unit", int, meter) - labels = {"key": "value"} - measure_v = View(metric, MinMaxSumCountAggregator) - bound_valuerecorder = metric.bind(labels) - meter.register_view(measure_v) - values = (37, 42, 7) - for val in values: - metric.record(val, labels) - self.assertEqual( - bound_valuerecorder.view_datas.pop().aggregator.current, - (min(values), max(values), sum(values), len(values)), - ) - - -class TestSumObserver(unittest.TestCase): - def test_observe(self): - observer = metrics.SumObserver( - None, "name", "desc", "unit", int, Mock(), ("key",), True - ) - labels = {"key": "value"} - key_labels = metrics.get_dict_as_key(labels) - values = (37, 42, 60, 100) - for val in values: - observer.observe(val, labels) - - self.assertEqual(observer.aggregators[key_labels].current, values[-1]) - - def test_observe_disabled(self): - observer = metrics.SumObserver( - None, "name", "desc", "unit", int, Mock(), ("key",), False - ) - labels = {"key": "value"} - observer.observe(37, labels) - self.assertEqual(len(observer.aggregators), 0) - - @patch("opentelemetry.sdk.metrics.logger") - def test_observe_incorrect_type(self, logger_mock): - observer = metrics.SumObserver( - None, "name", "desc", "unit", int, Mock(), ("key",), True - ) - labels = {"key": "value"} - observer.observe(37.0, labels) - self.assertEqual(len(observer.aggregators), 0) - self.assertTrue(logger_mock.warning.called) - - @patch("opentelemetry.sdk.metrics.logger") - def test_observe_non_decreasing_error(self, logger_mock): - observer = metrics.SumObserver( - None, "name", "desc", "unit", int, Mock(), ("key",), True - ) - labels = {"key": "value"} - observer.observe(37, labels) - observer.observe(14, labels) - self.assertEqual(len(observer.aggregators), 1) - self.assertTrue(logger_mock.warning.called) - - def test_run(self): - callback = Mock() - observer = metrics.SumObserver( - callback, "name", "desc", "unit", int, Mock(), (), True - ) - - self.assertTrue(observer.run()) - callback.assert_called_once_with(observer) - - @patch("opentelemetry.sdk.metrics.logger") - def test_run_exception(self, logger_mock): - callback = Mock() - callback.side_effect = Exception("We have a problem!") - - observer = metrics.SumObserver( - callback, "name", "desc", "unit", int, Mock(), (), True - ) - - self.assertFalse(observer.run()) - self.assertTrue(logger_mock.warning.called) - - -class TestUpDownSumObserver(unittest.TestCase): - def test_observe(self): - observer = metrics.UpDownSumObserver( - None, "name", "desc", "unit", int, Mock(), ("key",), True - ) - labels = {"key": "value"} - key_labels = metrics.get_dict_as_key(labels) - values = (37, 42, 14, 30) - for val in values: - observer.observe(val, labels) - - self.assertEqual(observer.aggregators[key_labels].current, values[-1]) - - def test_observe_disabled(self): - observer = metrics.UpDownSumObserver( - None, "name", "desc", "unit", int, Mock(), ("key",), False - ) - labels = {"key": "value"} - observer.observe(37, labels) - self.assertEqual(len(observer.aggregators), 0) - - @patch("opentelemetry.sdk.metrics.logger") - def test_observe_incorrect_type(self, logger_mock): - observer = metrics.UpDownSumObserver( - None, "name", "desc", "unit", int, Mock(), ("key",), True - ) - labels = {"key": "value"} - observer.observe(37.0, labels) - self.assertEqual(len(observer.aggregators), 0) - self.assertTrue(logger_mock.warning.called) - - def test_run(self): - callback = Mock() - observer = metrics.UpDownSumObserver( - callback, "name", "desc", "unit", int, Mock(), (), True - ) - - self.assertTrue(observer.run()) - callback.assert_called_once_with(observer) - - @patch("opentelemetry.sdk.metrics.logger") - def test_run_exception(self, logger_mock): - callback = Mock() - callback.side_effect = Exception("We have a problem!") - - observer = metrics.UpDownSumObserver( - callback, "name", "desc", "unit", int, Mock(), (), True - ) - - self.assertFalse(observer.run()) - self.assertTrue(logger_mock.warning.called) - - -class TestValueObserver(unittest.TestCase): - def test_observe(self): - observer = metrics.ValueObserver( - None, "name", "desc", "unit", int, Mock(), ("key",), True - ) - labels = {"key": "value"} - key_labels = metrics.get_dict_as_key(labels) - values = (37, 42, 7, 21) - for val in values: - observer.observe(val, labels) - self.assertEqual( - observer.aggregators[key_labels].mmsc.current, - (min(values), max(values), sum(values), len(values)), - ) - - self.assertEqual(observer.aggregators[key_labels].current, values[-1]) - - def test_observe_disabled(self): - observer = metrics.ValueObserver( - None, "name", "desc", "unit", int, Mock(), ("key",), False - ) - labels = {"key": "value"} - observer.observe(37, labels) - self.assertEqual(len(observer.aggregators), 0) - - @patch("opentelemetry.sdk.metrics.logger") - def test_observe_incorrect_type(self, logger_mock): - observer = metrics.ValueObserver( - None, "name", "desc", "unit", int, Mock(), ("key",), True - ) - labels = {"key": "value"} - observer.observe(37.0, labels) - self.assertEqual(len(observer.aggregators), 0) - self.assertTrue(logger_mock.warning.called) - - def test_run(self): - callback = Mock() - observer = metrics.ValueObserver( - callback, "name", "desc", "unit", int, Mock(), (), True - ) - - self.assertTrue(observer.run()) - callback.assert_called_once_with(observer) - - @patch("opentelemetry.sdk.metrics.logger") - def test_run_exception(self, logger_mock): - callback = Mock() - callback.side_effect = Exception("We have a problem!") - - observer = metrics.ValueObserver( - callback, "name", "desc", "unit", int, Mock(), (), True - ) - - self.assertFalse(observer.run()) - self.assertTrue(logger_mock.warning.called) - - -# pylint: disable=no-self-use -class TestBoundCounter(unittest.TestCase): - def test_add(self): - meter_mock = Mock() - metric_mock = Mock() - metric_mock.enabled = True - metric_mock.value_type = int - metric_mock.meter = meter_mock - bound_metric = metrics.BoundCounter((), metric_mock) - view_datas_mock = Mock() - bound_metric.view_datas = [view_datas_mock] - bound_metric.add(3) - view_datas_mock.record.assert_called_once_with(3) - - def test_add_disabled(self): - meter_mock = Mock() - metric_mock = Mock() - metric_mock.enabled = False - metric_mock.value_type = int - metric_mock.meter = meter_mock - bound_metric = metrics.BoundCounter((), metric_mock) - view_datas_mock = Mock() - bound_metric.view_datas = [view_datas_mock] - bound_metric.add(3) - view_datas_mock.record.update_view.assert_not_called() - - @patch("opentelemetry.sdk.metrics.logger") - def test_add_incorrect_type(self, logger_mock): - meter_mock = Mock() - viewm_mock = Mock() - meter_mock.view_manager = viewm_mock - metric_mock = Mock() - metric_mock.enabled = True - metric_mock.value_type = float - metric_mock.meter = meter_mock - bound_metric = metrics.BoundCounter((), metric_mock) - view_datas_mock = Mock() - bound_metric.view_datas = [view_datas_mock] - bound_metric.add(3) - view_datas_mock.record.update_view.assert_not_called() - self.assertTrue(logger_mock.warning.called) - - -class TestBoundValueRecorder(unittest.TestCase): - def test_record(self): - meter_mock = Mock() - metric_mock = Mock() - metric_mock.enabled = True - metric_mock.value_type = int - metric_mock.meter = meter_mock - bound_valuerecorder = metrics.BoundValueRecorder((), metric_mock) - view_datas_mock = Mock() - bound_valuerecorder.view_datas = [view_datas_mock] - bound_valuerecorder.record(3) - view_datas_mock.record.assert_called_once_with(3) - - def test_record_disabled(self): - meter_mock = Mock() - metric_mock = Mock() - metric_mock.enabled = False - metric_mock.value_type = int - metric_mock.meter = meter_mock - bound_valuerecorder = metrics.BoundValueRecorder((), metric_mock) - view_datas_mock = Mock() - bound_valuerecorder.view_datas = [view_datas_mock] - bound_valuerecorder.record(3) - view_datas_mock.record.update_view.assert_not_called() - - @patch("opentelemetry.sdk.metrics.logger") - def test_record_incorrect_type(self, logger_mock): - meter_mock = Mock() - metric_mock = Mock() - metric_mock.enabled = True - metric_mock.value_type = float - metric_mock.meter = meter_mock - bound_valuerecorder = metrics.BoundValueRecorder((), metric_mock) - view_datas_mock = Mock() - bound_valuerecorder.view_datas = [view_datas_mock] - bound_valuerecorder.record(3) - view_datas_mock.record.update_view.assert_not_called() - self.assertTrue(logger_mock.warning.called) diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py deleted file mode 100644 index c20d6ed3c4..0000000000 --- a/opentelemetry-sdk/tests/metrics/test_view.py +++ /dev/null @@ -1,303 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -from math import inf -from unittest import mock - -from opentelemetry.sdk import metrics -from opentelemetry.sdk.metrics import view -from opentelemetry.sdk.metrics.export import aggregate -from opentelemetry.sdk.metrics.export.aggregate import ( - HistogramAggregator, - MinMaxSumCountAggregator, - SumAggregator, -) -from opentelemetry.sdk.metrics.export.controller import PushController -from opentelemetry.sdk.metrics.export.in_memory_metrics_exporter import ( - InMemoryMetricsExporter, -) -from opentelemetry.sdk.metrics.view import View, ViewConfig - - -class TestUtil(unittest.TestCase): - @mock.patch("opentelemetry.sdk.metrics.view.logger") - def test_default_aggregator(self, logger_mock): - meter = metrics.MeterProvider().get_meter(__name__) - counter = metrics.Counter("", "", "1", int, meter) - self.assertEqual( - view.get_default_aggregator(counter), aggregate.SumAggregator - ) - ud_counter = metrics.UpDownCounter("", "", "1", int, meter) - self.assertEqual( - view.get_default_aggregator(ud_counter), aggregate.SumAggregator - ) - observer = metrics.SumObserver(lambda: None, "", "", "1", int, meter) - self.assertEqual( - view.get_default_aggregator(observer), - aggregate.LastValueAggregator, - ) - ud_observer = metrics.SumObserver( - lambda: None, "", "", "1", int, meter - ) - self.assertEqual( - view.get_default_aggregator(ud_observer), - aggregate.LastValueAggregator, - ) - recorder = metrics.ValueRecorder("", "", "1", int, meter) - self.assertEqual( - view.get_default_aggregator(recorder), - aggregate.MinMaxSumCountAggregator, - ) - v_observer = metrics.ValueObserver( - lambda: None, "", "", "1", int, meter - ) - self.assertEqual( - view.get_default_aggregator(v_observer), - aggregate.ValueObserverAggregator, - ) - self.assertEqual( - view.get_default_aggregator(DummyMetric()), aggregate.SumAggregator - ) - self.assertEqual(logger_mock.warning.call_count, 1) - - -class TestStateless(unittest.TestCase): - def setUp(self): - self.meter = metrics.MeterProvider(stateful=False).get_meter(__name__) - self.exporter = InMemoryMetricsExporter() - self.controller = PushController(self.meter, self.exporter, 30) - - def tearDown(self): - self.controller.shutdown() - - def test_label_keys(self): - test_counter = self.meter.create_counter( - name="test_counter", - description="description", - unit="By", - value_type=int, - ) - counter_view = View( - test_counter, - SumAggregator, - label_keys=["environment"], - view_config=ViewConfig.LABEL_KEYS, - ) - - self.meter.register_view(counter_view) - test_counter.add(6, {"environment": "production", "customer_id": 123}) - test_counter.add(5, {"environment": "production", "customer_id": 247}) - - self.controller.tick() - - metric_data = self.exporter.get_exported_metrics() - self.assertEqual(len(metric_data), 1) - self.assertEqual( - metric_data[0].labels, (("environment", "production"),) - ) - self.assertEqual(metric_data[0].aggregator.checkpoint, 11) - - def test_ungrouped(self): - test_counter = self.meter.create_counter( - name="test_counter", - description="description", - unit="By", - value_type=int, - ) - counter_view = View( - test_counter, - SumAggregator, - label_keys=["environment"], - view_config=ViewConfig.UNGROUPED, - ) - - self.meter.register_view(counter_view) - test_counter.add(6, {"environment": "production", "customer_id": 123}) - test_counter.add(5, {"environment": "production", "customer_id": 247}) - - self.controller.tick() - - metric_data = self.exporter.get_exported_metrics() - data_set = set() - for data in metric_data: - data_set.add((data.labels, data.aggregator.checkpoint)) - self.assertEqual(len(metric_data), 2) - label1 = (("customer_id", 123), ("environment", "production")) - label2 = (("customer_id", 247), ("environment", "production")) - self.assertTrue((label1, 6) in data_set) - self.assertTrue((label2, 5) in data_set) - - def test_multiple_views(self): - test_counter = self.meter.create_counter( - name="test_counter", - description="description", - unit="By", - value_type=int, - ) - - counter_view = View( - test_counter, - SumAggregator, - label_keys=["environment"], - view_config=ViewConfig.UNGROUPED, - ) - - mmsc_view = View( - test_counter, - MinMaxSumCountAggregator, - label_keys=["environment"], - view_config=ViewConfig.LABEL_KEYS, - ) - - self.meter.register_view(counter_view) - self.meter.register_view(mmsc_view) - test_counter.add(6, {"environment": "production", "customer_id": 123}) - test_counter.add(5, {"environment": "production", "customer_id": 247}) - - self.controller.tick() - - metric_data = self.exporter.get_exported_metrics() - sum_set = set() - mmsc_set = set() - for data in metric_data: - if isinstance(data.aggregator, SumAggregator): - tup = (data.labels, data.aggregator.checkpoint) - sum_set.add(tup) - elif isinstance(data.aggregator, MinMaxSumCountAggregator): - mmsc_set.add(data) - self.assertEqual(data.labels, (("environment", "production"),)) - self.assertEqual(data.aggregator.checkpoint.sum, 11) - # we have to assert this way because order is unknown - self.assertEqual(len(sum_set), 2) - self.assertEqual(len(mmsc_set), 1) - label1 = (("customer_id", 123), ("environment", "production")) - label2 = (("customer_id", 247), ("environment", "production")) - self.assertTrue((label1, 6) in sum_set) - self.assertTrue((label2, 5) in sum_set) - - -class TestHistogramView(unittest.TestCase): - def test_histogram_stateful(self): - meter = metrics.MeterProvider(stateful=True).get_meter(__name__) - exporter = InMemoryMetricsExporter() - controller = PushController(meter, exporter, 30) - - requests_size = meter.create_valuerecorder( - name="requests_size", - description="size of requests", - unit="1", - value_type=int, - ) - - size_view = View( - requests_size, - HistogramAggregator, - aggregator_config={"bounds": [20, 40, 60, 80, 100]}, - label_keys=["environment"], - view_config=ViewConfig.LABEL_KEYS, - ) - - meter.register_view(size_view) - - # Since this is using the HistogramAggregator, the bucket counts will be reflected - # with each record - requests_size.record(25, {"environment": "staging", "test": "value"}) - requests_size.record(1, {"environment": "staging", "test": "value2"}) - requests_size.record(200, {"environment": "staging", "test": "value3"}) - - controller.tick() - - metrics_list = exporter.get_exported_metrics() - self.assertEqual(len(metrics_list), 1) - checkpoint = metrics_list[0].aggregator.checkpoint - self.assertEqual( - tuple(checkpoint.items()), - ((20, 1), (40, 1), (60, 0), (80, 0), (100, 0), (inf, 1)), - ) - exporter.clear() - - requests_size.record(25, {"environment": "staging", "test": "value"}) - requests_size.record(1, {"environment": "staging", "test": "value2"}) - requests_size.record(200, {"environment": "staging", "test": "value3"}) - - controller.tick() - - metrics_list = exporter.get_exported_metrics() - self.assertEqual(len(metrics_list), 1) - checkpoint = metrics_list[0].aggregator.checkpoint - self.assertEqual( - tuple(checkpoint.items()), - ((20, 2), (40, 2), (60, 0), (80, 0), (100, 0), (inf, 2)), - ) - - def test_histogram_stateless(self): - # Use the meter type provided by the SDK package - meter = metrics.MeterProvider(stateful=False).get_meter(__name__) - exporter = InMemoryMetricsExporter() - controller = PushController(meter, exporter, 30) - - requests_size = meter.create_valuerecorder( - name="requests_size", - description="size of requests", - unit="1", - value_type=int, - ) - - size_view = View( - requests_size, - HistogramAggregator, - aggregator_config={"bounds": [20, 40, 60, 80, 100]}, - label_keys=["environment"], - view_config=ViewConfig.LABEL_KEYS, - ) - - meter.register_view(size_view) - - # Since this is using the HistogramAggregator, the bucket counts will be reflected - # with each record - requests_size.record(25, {"environment": "staging", "test": "value"}) - requests_size.record(1, {"environment": "staging", "test": "value2"}) - requests_size.record(200, {"environment": "staging", "test": "value3"}) - - controller.tick() - - metrics_list = exporter.get_exported_metrics() - self.assertEqual(len(metrics_list), 1) - checkpoint = metrics_list[0].aggregator.checkpoint - self.assertEqual( - tuple(checkpoint.items()), - ((20, 1), (40, 1), (60, 0), (80, 0), (100, 0), (inf, 1)), - ) - exporter.clear() - - requests_size.record(25, {"environment": "staging", "test": "value"}) - requests_size.record(1, {"environment": "staging", "test": "value2"}) - requests_size.record(200, {"environment": "staging", "test": "value3"}) - - controller.tick() - - metrics_list = exporter.get_exported_metrics() - self.assertEqual(len(metrics_list), 1) - checkpoint = metrics_list[0].aggregator.checkpoint - self.assertEqual( - tuple(checkpoint.items()), - ((20, 1), (40, 1), (60, 0), (80, 0), (100, 0), (inf, 1)), - ) - - -class DummyMetric(metrics.Metric): - # pylint: disable=W0231 - def __init__(self): - pass diff --git a/tests/util/src/opentelemetry/test/controller.py b/tests/util/src/opentelemetry/test/controller.py deleted file mode 100644 index 754d7bf979..0000000000 --- a/tests/util/src/opentelemetry/test/controller.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from time import sleep - -from opentelemetry.context import attach, detach, set_value -from opentelemetry.metrics import Meter -from opentelemetry.sdk.metrics.export import MetricsExporter - - -class DebugController: - """A debug controller, used to replace Push controller when debugging - - Push controller uses a thread which makes it hard to use the IPython - debugger. This controller does not use a thread, but relies on the user - manually calling its ``run`` method to start the controller. - - Args: - meter: The meter used to collect metrics. - exporter: The exporter used to export metrics. - interval: The collect/export interval in seconds. - """ - - daemon = True - - def __init__( - self, meter: Meter, exporter: MetricsExporter, interval: float - ): - super().__init__() - self.meter = meter - self.exporter = exporter - self.interval = interval - - def run(self): - while True: - self.tick() - sleep(self.interval) - - def shutdown(self): - # Run one more collection pass to flush metrics batched in the meter - self.tick() - - def tick(self): - # Collect all of the meter's metrics to be exported - self.meter.collect() - # Export the collected metrics - token = attach(set_value("suppress_instrumentation", True)) - self.exporter.export(self.meter.processor.checkpoint_set()) - detach(token) - # Perform post-exporting logic - self.meter.processor.finished_collection() diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index e1dbc55e62..c1ec6d2ee4 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -16,12 +16,7 @@ import unittest from contextlib import contextmanager -from opentelemetry import metrics as metrics_api from opentelemetry import trace as trace_api -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export.in_memory_metrics_exporter import ( - InMemoryMetricsExporter, -) from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, @@ -38,13 +33,6 @@ def setUpClass(cls): # current tracer provider. trace_api._TRACER_PROVIDER = None # pylint: disable=protected-access trace_api.set_tracer_provider(cls.tracer_provider) - cls.original_meter_provider = metrics_api.get_meter_provider() - result = cls.create_meter_provider() - cls.meter_provider, cls.memory_metrics_exporter = result - # This is done because set_meter_provider cannot override the - # current meter provider. - metrics_api._METER_PROVIDER = None # pylint: disable=protected-access - metrics_api.set_meter_provider(cls.meter_provider) @classmethod def tearDownClass(cls): @@ -52,10 +40,6 @@ def tearDownClass(cls): # current tracer provider. trace_api._TRACER_PROVIDER = None # pylint: disable=protected-access trace_api.set_tracer_provider(cls.original_tracer_provider) - # This is done because set_meter_provider cannot override the - # current meter provider. - metrics_api._METER_PROVIDER = None # pylint: disable=protected-access - metrics_api.set_meter_provider(cls.original_meter_provider) def setUp(self): self.memory_exporter.clear() @@ -96,20 +80,6 @@ def create_tracer_provider(**kwargs): return tracer_provider, memory_exporter - @staticmethod - def create_meter_provider(**kwargs): - """Helper to create a configured meter provider - - Creates a `MeterProvider` and an `InMemoryMetricsExporter`. - - Returns: - A list with the meter provider in the first element and the - in-memory metrics exporter in the second - """ - meter_provider = MeterProvider(**kwargs) - memory_exporter = InMemoryMetricsExporter() - return meter_provider, memory_exporter - @staticmethod @contextmanager def disable_logging(highest_level=logging.CRITICAL): diff --git a/tox.ini b/tox.ini index 079f40d164..f6e2e18aaa 100644 --- a/tox.ini +++ b/tox.ini @@ -39,10 +39,6 @@ envlist = py3{5,6,7,8,9}-test-exporter-otlp ; exporter-otlp intentionally excluded from pypy3 - ; opentelemetry-exporter-prometheus - py3{5,6,7,8,9}-test-exporter-prometheus - pypy3-test-exporter-prometheus - ; opentelemetry-exporter-zipkin py3{5,6,7,8,9}-test-exporter-zipkin pypy3-test-exporter-zipkin @@ -88,7 +84,6 @@ changedir = test-exporter-jaeger: exporter/opentelemetry-exporter-jaeger/tests test-exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests test-exporter-otlp: exporter/opentelemetry-exporter-otlp/tests - test-exporter-prometheus: exporter/opentelemetry-exporter-prometheus/tests test-exporter-zipkin: exporter/opentelemetry-exporter-zipkin/tests test-propagator-b3: propagator/opentelemetry-propagator-b3/tests @@ -112,8 +107,6 @@ commands_pre = otlp: pip install {toxinidir}/opentelemetry-proto otlp: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp - prometheus: pip install {toxinidir}/exporter/opentelemetry-exporter-prometheus - exporter-jaeger: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk @@ -169,7 +162,6 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-opencensus[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp[test] - python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-prometheus[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-b3[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-jaeger[test] From 2113d4a52befdc3aedb600c83c2d87d89bc3228a Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 9 Feb 2021 08:45:48 -0800 Subject: [PATCH 0769/1517] Update README.md (#1589) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 901fbf9112..810e94a809 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@   •   API Documentation   •   - Getting In Touch (Gitter) + Getting In Touch (GitHub Discussions)

@@ -119,7 +119,7 @@ We meet weekly on Thursdays at 9AM PST. The meeting is subject to change dependi Meetings take place via [Zoom video conference](https://zoom.us/j/6729396170). The passcode is _77777_. -Meeting notes are available as a public [Google doc](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit). For edit access, get in touch on [Gitter](https://gitter.im/open-telemetry/opentelemetry-python). +Meeting notes are available as a public [Google doc](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit). For edit access, get in touch on [GitHub Discussions](https://github.com/open-telemetry/opentelemetry-python/discussions). Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): From 806d4d6c796d326a64dba1ee744e762033581306 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 10 Feb 2021 08:43:06 -0800 Subject: [PATCH 0770/1517] Update versioning doc (#1588) --- CONTRIBUTING.md | 2 +- rationale.md | 54 +++++++++++++++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a01f446d0..e90006a662 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ Please take a look at this list first, your contributions may belong in one of t # Find the right branch -The default branch for this repo is `main`. Changes that pertain to components marked as `stable` in the [specifications](https://github.com/open-telemetry/opentelemetry-specification) go into this branch, which currently does not include `metrics`. Changes that pertain to `metrics` go into the `metrics` branch. +The default branch for this repo is `main`. Changes that pertain to `metrics` go into the `metrics` branch. Any changes that pertain to components marked as `stable` in the [specifications](https://github.com/open-telemetry/opentelemetry-specification) or anything that is not `metrics` related go into this branch. ## Find a Buddy and get Started Quickly! diff --git a/rationale.md b/rationale.md index 1c99eb0342..24cc141643 100644 --- a/rationale.md +++ b/rationale.md @@ -1,12 +1,18 @@ # OpenTelemetry Rationale -When creating a library, often times designs and decisions are made that get lost over time. This -document tries to collect information on design decisions to answer common questions that may come -up when you explore the SDK. +When creating a library, often times designs and decisions are made that get lost over time. This document tries to collect information on design decisions to answer common questions that may come up when you explore the SDK. ## Versioning and Releasing -The OpenTelemetry Applications and OpenTelemetry Spec itself use semver v2. +This document describes the versioning and stability policy of components shipped from this repository, as per the [OpenTelemetry versioning and stability +specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md). + +The OpenTelemetry implementations, the OpenTelemetry Spec itself and this repo follows [SemVer V2](https://semver.org/spec/v2.0.0.html) guidelines. +This means that, for any stable packages released from this repo, all public APIs will remain [backward +compatible](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/breaking-change-rules.md#breaking-change-rules), +unless a major version bump occurs. This applies to the API, SDK, as well as Exporters, Instrumentation etc. shipped from this repo. + +For example, users can take a dependency on 1.0.0 version of any package, with the assurance that all future releases until 2.0.0 will be backward compatible. ## Goals @@ -18,16 +24,23 @@ For example, libraries that are instrumented with `opentelemetry-api 1.0.1` will ### SDK Stability: -Public portions of the SDK (constructors, configuration, end-user interfaces) must remain backwards compatible. -Internal interfaces are allowed to break. +Public portions of the SDK (constructors, configuration, end-user interfaces) must remain backwards compatible. Internal interfaces are allowed to break. + +## Core components + +Core components refer to the set of components which are required as per the spec. This includes API, SDK, propagators (B3 and Jaeger) and exporters which are required by the specification. These exporters are OTLP, Jaeger and Zipkin. + +## Mature or stable Signals -## Methods +Modules for mature (i.e. released) signals will be found in the latest versions of the corresponding packages of the core components. The version numbers of these will have no suffix appended, indicating they are stable. For example, the package `opentelemetry-api` v1.x.y will be considered stable. -### Mature Signals -API modules for mature (i.e. released) signals will be included in the `opentelemetry-api` module. +## Pre-releases + +Pre-release packages are denoted by appending identifiers such as -Alpha, -Beta, -RC etc. There are no API guarantees in pre-releases. Each release can contain breaking changes and functionality could be removed as well. In general, an RC pre-release is more stable than a Beta release, which is more stable than an Alpha release. ### Immature or experimental signals -API modules for experimental signals will not be included in the `opentelemetry-api` module, and must be installed manually. API modules will remain at version v0.x.y to make it abundantly clear that depending on them is at your own risk. For example, the `opentelemetry-metrics-api` v0.x.y module will provide experimental access to the unfinished metrics API. NO STABILITY GUARANTEES ARE MADE. + +Modules for experimental signals will be released in a separate versions that will be marked as pre-releases, and must be installed manually. Modules will remain at version v0.x.y to make it abundantly clear that depending on them is at your own risk. For example, the `opentelemetry-api` v0.x.y-b0 module will provide experimental access to the latest features in development, which will include the experimental metrics signals. Notice the `b0` suffix which indicates that the release is still in beta, and therefore deemed to be in pre-release. NO STABILITY GUARANTEES ARE MADE. ## Examples @@ -37,25 +50,26 @@ Purely for illustration purposes, not intended to represent actual releases: - `opentelemetry-api` 1.0.0 - Contains APIs for tracing, baggage, propagators, context -- `opentelemetry-tracing` 1.0.0 - - Contains the tracing SDK - `opentelemetry-sdk` 1.0.0 - Contains SDK components for tracing, baggage, propagators, and context ##### Contains the following experimental packages -- `opentelemetry-api-metrics` 0.x.y - - Contains the EXPERIMENTAL API for metrics. There are no stability guarantees. -- `opentelemetry-metrics` 0.x.y - - Contains the EXPERIMENTAL SDK for metrics. There are no stability guarantees. +- `opentelemetry-api` 1.x.y-b0 + - Contains the EXPERIMENTAL API for metrics plus other unstable features. There are no stability guarantees. +- `opentelemetry-sdk` 1.x.y-b0 + - Contains the EXPERIMENTAL SDK for metrics plus other unstable features. There are no stability guarantees. #### V1.15.0 Release (with metrics) - `opentelemetry-api` 1.15.0 - Contains APIs for tracing, baggage, propagators, context, and metrics -- `opentelemetry-tracing` 1.15.0 - - Contains tracing SDK -- `opentelemetry-metrics` 1.15.0 - - Contains metrics SDK - `opentelemetry-sdk` 1.15.0 - Contains SDK components for tracing, baggage, propagators, context and metrics + +##### Contains the following experimental packages + +- `opentelemetry-api` 1.x.y-b0 + - Contains the EXPERIMENTAL API for logging plus other unstable features. There are no stability guarantees. +- `opentelemetry-sdk` 1.x.y-b0 + - Contains the EXPERIMENTAL SDK for logging plus other unstable features. There are no stability guarantees. From 78f6949fe29fc544f4d3c7190ebf3254960a0a6c Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 10 Feb 2021 09:19:28 -0800 Subject: [PATCH 0771/1517] adding readable span (#1560) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 5 + .../exporter/jaeger/translate/protobuf.py | 16 +- .../exporter/jaeger/translate/thrift.py | 12 +- .../tests/test_jaeger_exporter_protobuf.py | 24 +- .../tests/test_jaeger_exporter_thrift.py | 24 +- .../opencensus/trace_exporter/__init__.py | 6 +- .../exporter/otlp/trace_exporter/__init__.py | 32 +- .../tests/test_zipkin_exporter.py | 80 ++-- .../src/opentelemetry/trace/__init__.py | 11 +- .../src/opentelemetry/sdk/trace/__init__.py | 354 +++++++++++------- .../sdk/trace/export/__init__.py | 18 +- .../trace/export/in_memory_span_exporter.py | 4 +- .../tests/context/test_asyncio.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 12 +- .../tests/testbed/testcase.py | 8 +- tox.ini | 1 + 17 files changed, 368 insertions(+), 243 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7debd81c9d..dfa5f957bd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 43df76e5ed69f45d993c98ea68daea3c4622ea2d + CONTRIB_REPO_SHA: 263adc5f7f524fae2c84571f656cef0896de0868 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index e93c12b215..b8f8546d45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow missing carrier headers to continue without raising AttributeError ([#1545](https://github.com/open-telemetry/opentelemetry-python/pull/1545)) + +### Changed +- Read-only Span attributes have been moved to ReadableSpan class + ([#1560](https://github.com/open-telemetry/opentelemetry-python/pull/1560)) + ### Removed - Remove Configuration ([#1523](https://github.com/open-telemetry/opentelemetry-python/pull/1523)) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py index cc7354217a..f97977516d 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py @@ -24,7 +24,7 @@ VERSION_KEY, Translator, ) -from opentelemetry.sdk.trace import Span, StatusCode +from opentelemetry.sdk.trace import ReadableSpan, StatusCode from opentelemetry.util import types # pylint: disable=no-member,too-many-locals,no-self-use @@ -93,7 +93,7 @@ def _translate_attribute( return translated -def _extract_resource_tags(span: Span) -> Sequence[model_pb2.KeyValue]: +def _extract_resource_tags(span: ReadableSpan) -> Sequence[model_pb2.KeyValue]: """Extracts resource attributes from span and returns list of jaeger keyvalues. @@ -143,7 +143,7 @@ class ProtobufTranslator(Translator): def __init__(self, svc_name): self.svc_name = svc_name - def _translate_span(self, span: Span) -> model_pb2.Span: + def _translate_span(self, span: ReadableSpan) -> model_pb2.Span: ctx = span.get_span_context() # pb2 span expects in byte format @@ -177,7 +177,9 @@ def _translate_span(self, span: Span) -> model_pb2.Span: ) return jaeger_span - def _extract_tags(self, span: Span) -> Sequence[model_pb2.KeyValue]: + def _extract_tags( + self, span: ReadableSpan + ) -> Sequence[model_pb2.KeyValue]: translated = [] if span.attributes: for key, value in span.attributes.items(): @@ -226,7 +228,7 @@ def _extract_tags(self, span: Span) -> Sequence[model_pb2.KeyValue]: return translated def _extract_refs( - self, span: Span + self, span: ReadableSpan ) -> Optional[Sequence[model_pb2.SpanRef]]: if not span.links: return None @@ -244,7 +246,9 @@ def _extract_refs( ) return refs - def _extract_logs(self, span: Span) -> Optional[Sequence[model_pb2.Log]]: + def _extract_logs( + self, span: ReadableSpan + ) -> Optional[Sequence[model_pb2.Log]]: if not span.events: return None diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py index 8ca371c1b5..9df9c71688 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py @@ -23,7 +23,7 @@ _convert_int_to_i64, _nsec_to_usec_round, ) -from opentelemetry.sdk.trace import Span, StatusCode +from opentelemetry.sdk.trace import ReadableSpan, StatusCode from opentelemetry.util import types @@ -75,7 +75,7 @@ def _translate_attribute( class ThriftTranslator(Translator): - def _translate_span(self, span: Span) -> TCollector.Span: + def _translate_span(self, span: ReadableSpan) -> TCollector.Span: ctx = span.get_span_context() trace_id = ctx.trace_id span_id = ctx.span_id @@ -106,7 +106,7 @@ def _translate_span(self, span: Span) -> TCollector.Span: ) return jaeger_span - def _extract_tags(self, span: Span) -> Sequence[TCollector.Tag]: + def _extract_tags(self, span: ReadableSpan) -> Sequence[TCollector.Tag]: translated = [] if span.attributes: @@ -151,7 +151,7 @@ def _extract_tags(self, span: Span) -> Sequence[TCollector.Tag]: return translated def _extract_refs( - self, span: Span + self, span: ReadableSpan ) -> Optional[Sequence[TCollector.SpanRef]]: if not span.links: return None @@ -170,7 +170,9 @@ def _extract_refs( ) return refs - def _extract_logs(self, span: Span) -> Optional[Sequence[TCollector.Log]]: + def _extract_logs( + self, span: ReadableSpan + ) -> Optional[Sequence[TCollector.Log]]: """Returns jaeger logs if events exists, otherwise None. Args: diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py index 26cfc41498..80cbdfb55d 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py @@ -156,12 +156,24 @@ def test_translate_to_jaeger(self): events=(event,), links=(link,), kind=trace_api.SpanKind.CLIENT, + resource=Resource( + attributes={"key_resource": "some_resource"} + ), ), trace._Span( - name=span_names[1], context=parent_span_context, parent=None + name=span_names[1], + context=parent_span_context, + parent=None, + resource=Resource({}), ), trace._Span( - name=span_names[2], context=other_context, parent=None + name=span_names[2], + context=other_context, + parent=None, + resource=Resource({}), + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), ), ] @@ -171,25 +183,17 @@ def test_translate_to_jaeger(self): otel_spans[0].set_attribute("key_string", "hello_world") otel_spans[0].set_attribute("key_float", 111.22) otel_spans[0].set_attribute("key_tuple", ("tuple_element",)) - otel_spans[0].resource = Resource( - attributes={"key_resource": "some_resource"} - ) otel_spans[0].set_status( Status(StatusCode.ERROR, "Example description") ) otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) - otel_spans[1].resource = Resource({}) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) - otel_spans[2].resource = Resource({}) otel_spans[2].set_status(Status(StatusCode.OK, "Example description")) otel_spans[2].end(end_time=end_times[2]) - otel_spans[2].instrumentation_info = InstrumentationInfo( - name="name", version="version" - ) translate = Translate(otel_spans) # pylint: disable=protected-access diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py index 947597ab46..c0faafc1b6 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py @@ -235,12 +235,24 @@ def test_translate_to_jaeger(self): events=(event,), links=(link,), kind=trace_api.SpanKind.CLIENT, + resource=Resource( + attributes={"key_resource": "some_resource"} + ), ), trace._Span( - name=span_names[1], context=parent_span_context, parent=None + name=span_names[1], + context=parent_span_context, + parent=None, + resource=Resource({}), ), trace._Span( - name=span_names[2], context=other_context, parent=None + name=span_names[2], + context=other_context, + parent=None, + resource=Resource({}), + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), ), ] @@ -250,25 +262,17 @@ def test_translate_to_jaeger(self): otel_spans[0].set_attribute("key_string", "hello_world") otel_spans[0].set_attribute("key_float", 111.22) otel_spans[0].set_attribute("key_tuple", ("tuple_element",)) - otel_spans[0].resource = Resource( - attributes={"key_resource": "some_resource"} - ) otel_spans[0].set_status( Status(StatusCode.ERROR, "Example description") ) otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) - otel_spans[1].resource = Resource({}) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) - otel_spans[2].resource = Resource({}) otel_spans[2].set_status(Status(StatusCode.OK, "Example description")) otel_spans[2].end(end_time=end_times[2]) - otel_spans[2].instrumentation_info = InstrumentationInfo( - name="name", version="version" - ) translate = Translate(otel_spans) # pylint: disable=protected-access diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py index 613ee6482b..6a779c09fe 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py @@ -25,7 +25,7 @@ from opencensus.proto.trace.v1 import trace_pb2 import opentelemetry.exporter.opencensus.util as utils -from opentelemetry.sdk.trace import Span +from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult DEFAULT_ENDPOINT = "localhost:55678" @@ -62,7 +62,7 @@ def __init__( self.node = utils.get_node(service_name, host_name) - def export(self, spans: Sequence[Span]) -> SpanExportResult: + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: try: responses = self.client.Export(self.generate_span_requests(spans)) @@ -87,7 +87,7 @@ def generate_span_requests(self, spans): # pylint: disable=too-many-branches -def translate_to_collector(spans: Sequence[Span]): +def translate_to_collector(spans: Sequence[ReadableSpan]): collector_spans = [] for span in spans: status = None diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index 96dbbc084b..a357734bc1 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -45,7 +45,7 @@ OTEL_EXPORTER_OTLP_SPAN_INSECURE, OTEL_EXPORTER_OTLP_SPAN_TIMEOUT, ) -from opentelemetry.sdk.trace import Span as SDKSpan +from opentelemetry.sdk.trace import Span as ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace.status import StatusCode @@ -55,7 +55,9 @@ # pylint: disable=no-member class OTLPSpanExporter( SpanExporter, - OTLPExporterMixin[SDKSpan, ExportTraceServiceRequest, SpanExportResult], + OTLPExporterMixin[ + ReadableSpan, ExportTraceServiceRequest, SpanExportResult + ], ): # pylint: disable=unsubscriptable-object """OTLP span exporter @@ -107,34 +109,34 @@ def __init__( } ) - def _translate_name(self, sdk_span: SDKSpan) -> None: + def _translate_name(self, sdk_span: ReadableSpan) -> None: self._collector_span_kwargs["name"] = sdk_span.name - def _translate_start_time(self, sdk_span: SDKSpan) -> None: + def _translate_start_time(self, sdk_span: ReadableSpan) -> None: self._collector_span_kwargs[ "start_time_unix_nano" ] = sdk_span.start_time - def _translate_end_time(self, sdk_span: SDKSpan) -> None: + def _translate_end_time(self, sdk_span: ReadableSpan) -> None: self._collector_span_kwargs["end_time_unix_nano"] = sdk_span.end_time - def _translate_span_id(self, sdk_span: SDKSpan) -> None: + def _translate_span_id(self, sdk_span: ReadableSpan) -> None: self._collector_span_kwargs[ "span_id" ] = sdk_span.context.span_id.to_bytes(8, "big") - def _translate_trace_id(self, sdk_span: SDKSpan) -> None: + def _translate_trace_id(self, sdk_span: ReadableSpan) -> None: self._collector_span_kwargs[ "trace_id" ] = sdk_span.context.trace_id.to_bytes(16, "big") - def _translate_parent(self, sdk_span: SDKSpan) -> None: + def _translate_parent(self, sdk_span: ReadableSpan) -> None: if sdk_span.parent is not None: self._collector_span_kwargs[ "parent_span_id" ] = sdk_span.parent.span_id.to_bytes(8, "big") - def _translate_context_trace_state(self, sdk_span: SDKSpan) -> None: + def _translate_context_trace_state(self, sdk_span: ReadableSpan) -> None: if sdk_span.context.trace_state is not None: self._collector_span_kwargs["trace_state"] = ",".join( [ @@ -143,7 +145,7 @@ def _translate_context_trace_state(self, sdk_span: SDKSpan) -> None: ] ) - def _translate_attributes(self, sdk_span: SDKSpan) -> None: + def _translate_attributes(self, sdk_span: ReadableSpan) -> None: if sdk_span.attributes: self._collector_span_kwargs["attributes"] = [] @@ -157,7 +159,7 @@ def _translate_attributes(self, sdk_span: SDKSpan) -> None: except Exception as error: # pylint: disable=broad-except logger.exception(error) - def _translate_events(self, sdk_span: SDKSpan) -> None: + def _translate_events(self, sdk_span: ReadableSpan) -> None: if sdk_span.events: self._collector_span_kwargs["events"] = [] @@ -181,7 +183,7 @@ def _translate_events(self, sdk_span: SDKSpan) -> None: collector_span_event ) - def _translate_links(self, sdk_span: SDKSpan) -> None: + def _translate_links(self, sdk_span: ReadableSpan) -> None: if sdk_span.links: self._collector_span_kwargs["links"] = [] @@ -207,7 +209,7 @@ def _translate_links(self, sdk_span: SDKSpan) -> None: collector_span_link ) - def _translate_status(self, sdk_span: SDKSpan) -> None: + def _translate_status(self, sdk_span: ReadableSpan) -> None: # pylint: disable=no-member if sdk_span.status is not None: deprecated_code = Status.DEPRECATED_STATUS_CODE_OK @@ -220,7 +222,7 @@ def _translate_status(self, sdk_span: SDKSpan) -> None: ) def _translate_data( - self, data: Sequence[SDKSpan] + self, data: Sequence[ReadableSpan] ) -> ExportTraceServiceRequest: # pylint: disable=attribute-defined-outside-init @@ -279,5 +281,5 @@ def _translate_data( ) ) - def export(self, spans: Sequence[SDKSpan]) -> SpanExportResult: + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return self._export(spans) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 5c2e0e7e4d..36320b78c3 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -189,20 +189,36 @@ def test_export_json(self): parent=parent_span_context, events=(event,), links=(link,), + resource=Resource({}), ), trace._Span( - name=span_names[1], context=parent_span_context, parent=None + name=span_names[1], + context=parent_span_context, + parent=None, + resource=Resource( + attributes={"key_resource": "some_resource"} + ), ), trace._Span( - name=span_names[2], context=other_context, parent=None + name=span_names[2], + context=other_context, + parent=None, + resource=Resource( + attributes={"key_resource": "some_resource"} + ), ), trace._Span( - name=span_names[3], context=other_context, parent=None + name=span_names[3], + context=other_context, + parent=None, + resource=Resource({}), + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), ), ] otel_spans[0].start(start_time=start_times[0]) - otel_spans[0].resource = Resource({}) # added here to preserve order otel_spans[0].set_attribute("key_bool", False) otel_spans[0].set_attribute("key_string", "hello_world") @@ -213,24 +229,14 @@ def test_export_json(self): otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) - otel_spans[1].resource = Resource( - attributes={"key_resource": "some_resource"} - ) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) otel_spans[2].set_attribute("key_string", "hello_world") - otel_spans[2].resource = Resource( - attributes={"key_resource": "some_resource"} - ) otel_spans[2].end(end_time=end_times[2]) otel_spans[3].start(start_time=start_times[3]) - otel_spans[3].resource = Resource({}) otel_spans[3].end(end_time=end_times[3]) - otel_spans[3].instrumentation_info = InstrumentationInfo( - name="name", version="version" - ) service_name = "test-service" local_endpoint = {"serviceName": service_name, "port": 9411} @@ -359,10 +365,10 @@ def test_export_json_zero_padding(self): name=span_names[0], context=span_context, parent=parent_span_context, + resource=Resource({}), ) otel_span.start(start_time=start_time) - otel_span.resource = Resource({}) otel_span.end(end_time=end_time) service_name = "test-service" @@ -416,10 +422,11 @@ def test_export_json_max_tag_length(self): trace_flags=TraceFlags(TraceFlags.SAMPLED), ) - span = trace._Span(name="test-span", context=span_context,) + span = trace._Span( + name="test-span", context=span_context, resource=Resource({}) + ) span.start() - span.resource = Resource({}) # added here to preserve order span.set_attribute("string1", "v" * 500) span.set_attribute("string2", "v" * 50) @@ -704,22 +711,38 @@ def test_export_protobuf(self): name=span_names[0], context=span_context, parent=parent_span_context, + resource=Resource({}), events=(event,), links=(link,), ), trace._Span( - name=span_names[1], context=parent_span_context, parent=None + name=span_names[1], + context=parent_span_context, + parent=None, + resource=Resource( + attributes={"key_resource": "some_resource"} + ), ), trace._Span( - name=span_names[2], context=other_context, parent=None + name=span_names[2], + context=other_context, + parent=None, + resource=Resource( + attributes={"key_resource": "some_resource"} + ), ), trace._Span( - name=span_names[3], context=other_context, parent=None + name=span_names[3], + context=other_context, + parent=None, + resource=Resource({}), + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), ), ] otel_spans[0].start(start_time=start_times[0]) - otel_spans[0].resource = Resource({}) # added here to preserve order otel_spans[0].set_attribute("key_bool", False) otel_spans[0].set_attribute("key_string", "hello_world") @@ -730,25 +753,15 @@ def test_export_protobuf(self): otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) - otel_spans[1].resource = Resource( - attributes={"key_resource": "some_resource"} - ) otel_spans[1].set_status(Status(StatusCode.OK)) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) otel_spans[2].set_attribute("key_string", "hello_world") - otel_spans[2].resource = Resource( - attributes={"key_resource": "some_resource"} - ) otel_spans[2].end(end_time=end_times[2]) otel_spans[3].start(start_time=start_times[3]) - otel_spans[3].resource = Resource({}) otel_spans[3].end(end_time=end_times[3]) - otel_spans[3].instrumentation_info = InstrumentationInfo( - name="name", version="version" - ) service_name = "test-service" local_endpoint = zipkin_pb2.Endpoint( @@ -869,10 +882,11 @@ def test_export_protobuf_max_tag_length(self): trace_flags=TraceFlags(TraceFlags.SAMPLED), ) - span = trace._Span(name="test-span", context=span_context,) + span = trace._Span( + name="test-span", context=span_context, resource=Resource({}) + ) span.start() - span.resource = Resource({}) # added here to preserve order span.set_attribute("k1", "v" * 500) span.set_attribute("k2", "v" * 50) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index b579e5ccf7..218c8256f0 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -139,6 +139,9 @@ def attributes(self) -> types.Attributes: return self._attributes +_Links = typing.Optional[typing.Sequence[Link]] + + class SpanKind(enum.Enum): """Specifies additional details on how this span relates to its parent span. @@ -231,7 +234,7 @@ def start_span( context: typing.Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, - links: typing.Sequence[Link] = (), + links: _Links = None, start_time: typing.Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, @@ -286,7 +289,7 @@ def start_as_current_span( context: typing.Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, - links: typing.Sequence[Link] = (), + links: _Links = None, start_time: typing.Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, @@ -380,7 +383,7 @@ def start_span( context: typing.Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, - links: typing.Sequence[Link] = (), + links: _Links = None, start_time: typing.Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, @@ -395,7 +398,7 @@ def start_as_current_span( context: typing.Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, - links: typing.Sequence[Link] = (), + links: _Links = None, start_time: typing.Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 2ecb82a0c3..0a6eefbd84 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. - +# pylint: disable=too-many-lines import abc import atexit import concurrent.futures @@ -95,7 +95,7 @@ def on_start( parent_context: The parent context of the span that just started. """ - def on_end(self, span: "Span") -> None: + def on_end(self, span: "ReadableSpan") -> None: """Called when a :class:`opentelemetry.trace.Span` is ended. This method is called synchronously on the thread that ends the @@ -106,8 +106,7 @@ def on_end(self, span: "Span") -> None: """ def shutdown(self) -> None: - """Called when a :class:`opentelemetry.sdk.trace.Tracer` is shutdown. - """ + """Called when a :class:`opentelemetry.sdk.trace.Tracer` is shutdown.""" def force_flush(self, timeout_millis: int = 30000) -> bool: """Export all ended spans to the configured Exporter that have not yet @@ -149,13 +148,12 @@ def on_start( for sp in self._span_processors: sp.on_start(span, parent_context=parent_context) - def on_end(self, span: "Span") -> None: + def on_end(self, span: "ReadableSpan") -> None: for sp in self._span_processors: sp.on_end(span) def shutdown(self) -> None: - """Sequentially shuts down all underlying span processors. - """ + """Sequentially shuts down all underlying span processors.""" for sp in self._span_processors: sp.shutdown() @@ -234,7 +232,7 @@ def on_start( lambda sp: sp.on_start, span, parent_context=parent_context ) - def on_end(self, span: "Span") -> None: + def on_end(self, span: "ReadableSpan") -> None: self._submit_and_await(lambda sp: sp.on_end, span) def shutdown(self) -> None: @@ -387,7 +385,7 @@ def _check_span_ended(func): def wrapper(self, *args, **kwargs): already_ended = False with self._lock: # pylint: disable=protected-access - if self.end_time is None: + if self._end_time is None: func(self, *args, **kwargs) else: already_ended = True @@ -398,7 +396,167 @@ def wrapper(self, *args, **kwargs): return wrapper -class Span(trace_api.Span): +class ReadableSpan: + """Provides read-only access to span attributes""" + + def __init__( + self, + name: str = None, + context: trace_api.SpanContext = None, + parent: Optional[trace_api.SpanContext] = None, + resource: Resource = Resource.create({}), + attributes: types.Attributes = None, + events: Sequence[Event] = None, + links: Sequence[trace_api.Link] = (), + kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, + instrumentation_info: InstrumentationInfo = None, + status: Status = Status(StatusCode.UNSET), + start_time: Optional[int] = None, + end_time: Optional[int] = None, + ) -> None: + self._name = name + self._context = context + self._kind = kind + self._instrumentation_info = instrumentation_info + self._parent = parent + self._start_time = start_time + self._end_time = end_time + self._attributes = attributes + self._events = events + self._links = links + self._resource = resource + self._status = status + + @property + def name(self) -> str: + return self._name + + def get_span_context(self): + return self._context + + @property + def context(self): + return self._context + + @property + def kind(self) -> trace_api.SpanKind: + return self._kind + + @property + def parent(self) -> Optional[trace_api.SpanContext]: + return self._parent + + @property + def start_time(self) -> Optional[int]: + return self._start_time + + @property + def end_time(self) -> Optional[int]: + return self._end_time + + @property + def status(self) -> trace_api.Status: + return self._status + + @property + def attributes(self) -> types.Attributes: + return MappingProxyType(self._attributes) + + @property + def events(self) -> Sequence[Event]: + return MappingProxyType(self._events) + + @property + def links(self) -> Sequence[trace_api.Link]: + return MappingProxyType(self._links) + + @property + def resource(self) -> Resource: + return self._resource + + @property + def instrumentation_info(self) -> InstrumentationInfo: + return self._instrumentation_info + + def to_json(self, indent=4): + parent_id = None + if self.parent is not None: + if isinstance(self.parent, Span): + ctx = self.parent.context + parent_id = trace_api.format_span_id(ctx.span_id) + elif isinstance(self.parent, SpanContext): + parent_id = trace_api.format_span_id(self.parent.span_id) + + start_time = None + if self._start_time: + start_time = util.ns_to_iso_str(self._start_time) + + end_time = None + if self._end_time: + end_time = util.ns_to_iso_str(self._end_time) + + if self._status is not None: + status = OrderedDict() + status["status_code"] = str(self._status.status_code.name) + if self._status.description: + status["description"] = self._status.description + + f_span = OrderedDict() + + f_span["name"] = self._name + f_span["context"] = self._format_context(self._context) + f_span["kind"] = str(self.kind) + f_span["parent_id"] = parent_id + f_span["start_time"] = start_time + f_span["end_time"] = end_time + if self._status is not None: + f_span["status"] = status + f_span["attributes"] = self._format_attributes(self._attributes) + f_span["events"] = self._format_events(self._events) + f_span["links"] = self._format_links(self._links) + f_span["resource"] = self._resource.attributes + + return json.dumps(f_span, indent=indent) + + @staticmethod + def _format_context(context): + x_ctx = OrderedDict() + x_ctx["trace_id"] = trace_api.format_trace_id(context.trace_id) + x_ctx["span_id"] = trace_api.format_span_id(context.span_id) + x_ctx["trace_state"] = repr(context.trace_state) + return x_ctx + + @staticmethod + def _format_attributes(attributes): + if isinstance(attributes, BoundedDict): + return attributes._dict # pylint: disable=protected-access + if isinstance(attributes, MappingProxyType): + return attributes.copy() + return attributes + + @staticmethod + def _format_events(events): + f_events = [] + for event in events: + f_event = OrderedDict() + f_event["name"] = event.name + f_event["timestamp"] = util.ns_to_iso_str(event.timestamp) + f_event["attributes"] = Span._format_attributes(event.attributes) + f_events.append(f_event) + return f_events + + @staticmethod + def _format_links(links): + f_links = [] + for link in links: + f_link = OrderedDict() + f_link["context"] = Span._format_context(link.context) + f_link["attributes"] = Span._format_attributes(link.attributes) + f_links.append(f_link) + return f_links + + +class Span(trace_api.Span, ReadableSpan): """See `opentelemetry.trace.Span`. Users should create `Span` objects via the `Tracer` instead of this @@ -442,30 +600,30 @@ def __init__( record_exception: bool = True, set_status_on_exception: bool = True, ) -> None: - - self.name = name - self.context = context - self.parent = parent - self.sampler = sampler - self.trace_config = trace_config - self.resource = resource - self.kind = kind + super().__init__( + name=name, + context=context, + parent=parent, + kind=kind, + resource=resource, + instrumentation_info=instrumentation_info, + ) + self._sampler = sampler + self._trace_config = trace_config self._record_exception = record_exception self._set_status_on_exception = set_status_on_exception - - self.span_processor = span_processor - self.status = Status(StatusCode.UNSET) + self._span_processor = span_processor self._lock = threading.Lock() _filter_attribute_values(attributes) if not attributes: - self.attributes = self._new_attributes() + self._attributes = self._new_attributes() else: - self.attributes = BoundedDict.from_map( + self._attributes = BoundedDict.from_map( SPAN_ATTRIBUTE_COUNT_LIMIT, attributes ) - self.events = self._new_events() + self._events = self._new_events() if events: for event in events: _filter_attribute_values(event.attributes) @@ -473,28 +631,16 @@ def __init__( event._attributes = _create_immutable_attributes( event.attributes ) - self.events.append(event) + self._events.append(event) if links is None: - self.links = self._new_links() + self._links = self._new_links() else: - self.links = BoundedList.from_seq(SPAN_LINK_COUNT_LIMIT, links) - - self._end_time = None # type: Optional[int] - self._start_time = None # type: Optional[int] - self.instrumentation_info = instrumentation_info - - @property - def start_time(self): - return self._start_time - - @property - def end_time(self): - return self._end_time + self._links = BoundedList.from_seq(SPAN_LINK_COUNT_LIMIT, links) def __repr__(self): return '{}(name="{}", context={})'.format( - type(self).__name__, self.name, self.context + type(self).__name__, self._name, self._context ) @staticmethod @@ -509,91 +655,14 @@ def _new_events(): def _new_links(): return BoundedList(SPAN_LINK_COUNT_LIMIT) - @staticmethod - def _format_context(context): - x_ctx = OrderedDict() - x_ctx["trace_id"] = trace_api.format_trace_id(context.trace_id) - x_ctx["span_id"] = trace_api.format_span_id(context.span_id) - x_ctx["trace_state"] = repr(context.trace_state) - return x_ctx - - @staticmethod - def _format_attributes(attributes): - if isinstance(attributes, BoundedDict): - return attributes._dict # pylint: disable=protected-access - if isinstance(attributes, MappingProxyType): - return attributes.copy() - return attributes - - @staticmethod - def _format_events(events): - f_events = [] - for event in events: - f_event = OrderedDict() - f_event["name"] = event.name - f_event["timestamp"] = util.ns_to_iso_str(event.timestamp) - f_event["attributes"] = Span._format_attributes(event.attributes) - f_events.append(f_event) - return f_events - - @staticmethod - def _format_links(links): - f_links = [] - for link in links: - f_link = OrderedDict() - f_link["context"] = Span._format_context(link.context) - f_link["attributes"] = Span._format_attributes(link.attributes) - f_links.append(f_link) - return f_links - - def to_json(self, indent=4): - parent_id = None - if self.parent is not None: - if isinstance(self.parent, Span): - ctx = self.parent.context - parent_id = trace_api.format_span_id(ctx.span_id) - elif isinstance(self.parent, SpanContext): - parent_id = trace_api.format_span_id(self.parent.span_id) - - start_time = None - if self.start_time: - start_time = util.ns_to_iso_str(self.start_time) - - end_time = None - if self.end_time: - end_time = util.ns_to_iso_str(self.end_time) - - if self.status is not None: - status = OrderedDict() - status["status_code"] = str(self.status.status_code.name) - if self.status.description: - status["description"] = self.status.description - - f_span = OrderedDict() - - f_span["name"] = self.name - f_span["context"] = self._format_context(self.context) - f_span["kind"] = str(self.kind) - f_span["parent_id"] = parent_id - f_span["start_time"] = start_time - f_span["end_time"] = end_time - if self.status is not None: - f_span["status"] = status - f_span["attributes"] = self._format_attributes(self.attributes) - f_span["events"] = self._format_events(self.events) - f_span["links"] = self._format_links(self.links) - f_span["resource"] = self.resource.attributes - - return json.dumps(f_span, indent=indent) - def get_span_context(self): - return self.context + return self._context def set_attributes( self, attributes: Dict[str, types.AttributeValue] ) -> None: with self._lock: - if self.end_time is not None: + if self._end_time is not None: logger.warning("Setting attribute on ended span.") return @@ -614,14 +683,14 @@ def set_attributes( except ValueError: logger.warning("Byte attribute could not be decoded.") return - self.attributes[key] = value + self._attributes[key] = value def set_attribute(self, key: str, value: types.AttributeValue) -> None: return self.set_attributes({key: value}) @_check_span_ended def _add_event(self, event: EventBase) -> None: - self.events.append(event) + self._events.append(event) def add_event( self, @@ -639,43 +708,59 @@ def add_event( ) ) + def _readable_span(self) -> ReadableSpan: + return ReadableSpan( + name=self._name, + context=self._context, + parent=self._parent, + resource=self._resource, + attributes=self._attributes, + events=self._events, + links=self._links, + kind=self.kind, + status=self._status, + start_time=self._start_time, + end_time=self._end_time, + instrumentation_info=self._instrumentation_info, + ) + def start( self, start_time: Optional[int] = None, parent_context: Optional[context_api.Context] = None, ) -> None: with self._lock: - if self.start_time is not None: + if self._start_time is not None: logger.warning("Calling start() on a started span.") return self._start_time = ( start_time if start_time is not None else time_ns() ) - self.span_processor.on_start(self, parent_context=parent_context) + self._span_processor.on_start(self, parent_context=parent_context) def end(self, end_time: Optional[int] = None) -> None: with self._lock: - if self.start_time is None: + if self._start_time is None: raise RuntimeError("Calling end() on a not started span.") - if self.end_time is not None: + if self._end_time is not None: logger.warning("Calling end() on an ended span.") return self._end_time = end_time if end_time is not None else time_ns() - self.span_processor.on_end(self) + self._span_processor.on_end(self._readable_span()) @_check_span_ended def update_name(self, name: str) -> None: - self.name = name + self._name = name def is_recording(self) -> bool: return self._end_time is None @_check_span_ended def set_status(self, status: trace_api.Status) -> None: - self.status = status + self._status = status def __exit__( self, @@ -692,7 +777,7 @@ def __exit__( # Records status if span is used as context manager # i.e. with tracer.start_span() as span: if ( - self.status.status_code is StatusCode.UNSET + self._status.status_code is StatusCode.UNSET and self._set_status_on_exception ): self.set_status( @@ -737,13 +822,13 @@ def record_exception( class _Span(Span): """Protected implementation of `opentelemetry.trace.Span`. - This constructor should only be used internally. + This constructor exists to prevent the instantiation of the `Span` class + by other mechanisms than through the `Tracer`. """ class Tracer(trace_api.Tracer): - """See `opentelemetry.trace.Tracer`. - """ + """See `opentelemetry.trace.Tracer`.""" def __init__( self, @@ -886,7 +971,7 @@ def use_span( # Records status if use_span is used # i.e. with tracer.start_as_current_span() as span: if ( - span.status.status_code is StatusCode.UNSET + span._status.status_code is StatusCode.UNSET and span._set_status_on_exception ): span.set_status( @@ -905,8 +990,7 @@ def use_span( class TracerProvider(trace_api.TracerProvider): - """See `opentelemetry.trace.TracerProvider`. - """ + """See `opentelemetry.trace.TracerProvider`.""" def __init__( self, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 4d4cc70224..932d05f233 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -28,7 +28,7 @@ OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_SCHEDULE_DELAY, ) -from opentelemetry.sdk.trace import Span, SpanProcessor +from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -49,7 +49,9 @@ class SpanExporter: `SimpleExportSpanProcessor` or a `BatchExportSpanProcessor`. """ - def export(self, spans: typing.Sequence[Span]) -> "SpanExportResult": + def export( + self, spans: typing.Sequence[ReadableSpan] + ) -> "SpanExportResult": """Exports a batch of telemetry data. Args: @@ -81,7 +83,7 @@ def on_start( ) -> None: pass - def on_end(self, span: Span) -> None: + def on_end(self, span: ReadableSpan) -> None: if not span.context.trace_flags.sampled: return token = attach(set_value("suppress_instrumentation", True)) @@ -185,7 +187,7 @@ def on_start( ) -> None: pass - def on_end(self, span: Span) -> None: + def on_end(self, span: ReadableSpan) -> None: if self.done: logger.warning("Already shutdown, dropping span.") return @@ -329,7 +331,7 @@ def _export_batch(self) -> int: return idx def _drain_queue(self): - """"Export all elements until queue is empty. + """Export all elements until queue is empty. Can only be called from the worker thread context because it invokes `export` that is not thread safe. @@ -378,14 +380,16 @@ def __init__( self, service_name: Optional[str] = None, out: typing.IO = sys.stdout, - formatter: typing.Callable[[Span], str] = lambda span: span.to_json() + formatter: typing.Callable[ + [ReadableSpan], str + ] = lambda span: span.to_json() + linesep, ): self.out = out self.formatter = formatter self.service_name = service_name - def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: + def export(self, spans: typing.Sequence[ReadableSpan]) -> SpanExportResult: for span in spans: self.out.write(self.formatter(span)) self.out.flush() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py index 967d29b3a8..e46266b93b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py @@ -15,7 +15,7 @@ import threading import typing -from opentelemetry.sdk.trace import Span +from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult @@ -42,7 +42,7 @@ def get_finished_spans(self): with self._lock: return tuple(self._finished_spans) - def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: + def export(self, spans: typing.Sequence[ReadableSpan]) -> SpanExportResult: """Stores a list of spans in memory.""" if self._stopped: return SpanExportResult.FAILURE diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index c235e71d83..7f9539738c 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -109,4 +109,4 @@ def test_with_asyncio(self): for span in span_list: if span is expected_parent: continue - self.assertEqual(span.parent, expected_parent.get_span_context()) + self.assertEqual(span.parent, expected_parent.context) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 66a5a82c30..6969b65b11 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -289,10 +289,10 @@ def test_span_processor_for_source(self): # pylint:disable=protected-access self.assertIs( - span1.span_processor, tracer_provider._active_span_processor + span1._span_processor, tracer_provider._active_span_processor ) self.assertIs( - span2.span_processor, tracer_provider._active_span_processor + span2._span_processor, tracer_provider._active_span_processor ) def test_start_span_implicit(self): @@ -1132,7 +1132,7 @@ def on_start( ) -> None: self.span_list.append(span_event_start_fmt(self.name, span.name)) - def on_end(self, span: "trace.Span") -> None: + def on_end(self, span: "trace.ReadableSpan") -> None: self.span_list.append(span_event_end_fmt(self.name, span.name)) @@ -1237,8 +1237,7 @@ def test_to_json(self): is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) - span = trace._Span("span-name", context) - span.resource = Resource({}) + span = trace._Span("span-name", context, resource=Resource({})) self.assertEqual( span.to_json(), @@ -1274,8 +1273,7 @@ def test_attributes_to_json(self): is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) - span = trace._Span("span-name", context) - span.resource = Resource({}) + span = trace._Span("span-name", context, resource=Resource({})) span.set_attribute("key", "value") span.add_event("event", {"key2": "value2"}, 123) date_str = ns_to_iso_str(123) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/testcase.py b/shim/opentelemetry-opentracing-shim/tests/testbed/testcase.py index c1ce6ea5ab..3c16682fad 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/testcase.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/testcase.py @@ -18,11 +18,11 @@ def assertIsChildOf(self, spanA, spanB): self.assertIsNotNone(spanA.parent) ctxA = spanA.parent - if isinstance(spanA.parent, trace_api.Span): + if not isinstance(ctxA, trace_api.SpanContext): ctxA = spanA.parent.context ctxB = spanB - if isinstance(ctxB, trace_api.Span): + if not isinstance(ctxB, trace_api.SpanContext): ctxB = spanB.context return self.assertEqual(ctxA.span_id, ctxB.span_id) @@ -33,11 +33,11 @@ def assertIsNotChildOf(self, spanA, spanB): return ctxA = spanA.parent - if isinstance(spanA.parent, trace_api.Span): + if not isinstance(ctxA, trace_api.SpanContext): ctxA = spanA.parent.context ctxB = spanB - if isinstance(ctxB, trace_api.Span): + if not isinstance(ctxB, trace_api.SpanContext): ctxB = spanB.context self.assertNotEqual(ctxA.span_id, ctxB.span_id) diff --git a/tox.ini b/tox.ini index f6e2e18aaa..9f30b40831 100644 --- a/tox.ini +++ b/tox.ini @@ -171,6 +171,7 @@ commands = python scripts/eachdist.py lint --check-only [testenv:docs] +recreate = True deps = -c {toxinidir}/dev-requirements.txt -r {toxinidir}/docs-requirements.txt From 59f5babeb0d2e6bb6f136c295e91077a8cd05a16 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 11 Feb 2021 21:30:37 +0530 Subject: [PATCH 0772/1517] Add urllib to opentelemetry-bootstrap target list (#1584) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/instrumentation/bootstrap.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8f8546d45..55a550dda2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1480](https://github.com/open-telemetry/opentelemetry-python/pull/1480)) - Allow missing carrier headers to continue without raising AttributeError ([#1545](https://github.com/open-telemetry/opentelemetry-python/pull/1545)) +- Add urllib to opentelemetry-bootstrap target list + ([#1584])(https://github.com/open-telemetry/opentelemetry-python/pull/1584) ### Changed diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index ff2d244aa8..5fb0be5ecb 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -60,6 +60,7 @@ "sqlite3": "opentelemetry-instrumentation-sqlite3>=0.11b0", "starlette": "opentelemetry-instrumentation-starlette>=0.11b0", "tornado": "opentelemetry-instrumentation-tornado>=0.13b0", + "urllib": "opentelemetry-instrumentation-urllib>=0.17b0", } # relevant instrumentors and tracers to uninstall and check for conflicts for target libraries @@ -91,6 +92,7 @@ "sqlite3": ("opentelemetry-instrumentation-sqlite3",), "starlette": ("opentelemetry-instrumentation-starlette",), "tornado": ("opentelemetry-instrumentation-tornado",), + "urllib": ("opentelemetry-instrumentation-urllib",), } From 84b1b0cf2e1121d7e468c4dc0da937c54a520b08 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 11 Feb 2021 14:04:12 -0800 Subject: [PATCH 0773/1517] Rename env vars from OTEL_TRACE -> OTEL_TRACES (#1595) --- CHANGELOG.md | 2 ++ .../environment_variables/__init__.py | 2 +- .../src/opentelemetry/distro/__init__.py | 6 +++--- opentelemetry-distro/tests/test_distro.py | 6 +++--- .../auto_instrumentation/__init__.py | 4 ++-- .../tests/test_run.py | 6 +++--- .../sdk/environment_variables/__init__.py | 4 ++-- .../src/opentelemetry/sdk/trace/sampling.py | 18 +++++++++--------- opentelemetry-sdk/tests/trace/test_trace.py | 10 +++++----- 9 files changed, 30 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55a550dda2..0d03dbadd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Tracer and Meter provider environment variables are now consistent with the rest ([#1571](https://github.com/open-telemetry/opentelemetry-python/pull/1571)]) +- Rename `TRACE_` to `TRACES_` for environment variables + ([#1595](https://github.com/open-telemetry/opentelemetry-python/pull/1595)]) ### Added - Added `end_on_exit` argument to `start_as_current_span` diff --git a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py index ab006aeb38..3b1ee8a7e3 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py @@ -17,5 +17,5 @@ OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS" OTEL_PYTHON_IDS_GENERATOR = "OTEL_PYTHON_IDS_GENERATOR" OTEL_PYTHON_SERVICE_NAME = "OTEL_PYTHON_SERVICE_NAME" -OTEL_TRACE_EXPORTER = "OTEL_TRACE_EXPORTER" +OTEL_TRACES_EXPORTER = "OTEL_TRACES_EXPORTER" OTEL_PYTHON_TRACER_PROVIDER = "OTEL_PYTHON_TRACER_PROVIDER" diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index 2a6c6bd575..46b469b0fb 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -23,7 +23,7 @@ from opentelemetry.environment_variables import ( OTEL_PYTHON_IDS_GENERATOR, OTEL_PYTHON_SERVICE_NAME, - OTEL_TRACE_EXPORTER, + OTEL_TRACES_EXPORTER, ) from opentelemetry.instrumentation.configurator import BaseConfigurator from opentelemetry.instrumentation.distro import BaseDistro @@ -54,7 +54,7 @@ def _get_service_name() -> str: def _get_exporter_names() -> Sequence[str]: - trace_exporters = environ.get(OTEL_TRACE_EXPORTER) + trace_exporters = environ.get(OTEL_TRACES_EXPORTER) exporters = set() @@ -175,4 +175,4 @@ class OpenTelemetryDistro(BaseDistro): """ def _configure(self, **kwargs): - os.environ.setdefault(OTEL_TRACE_EXPORTER, "otlp_span") + os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp_span") diff --git a/opentelemetry-distro/tests/test_distro.py b/opentelemetry-distro/tests/test_distro.py index ab8afb7a9b..3e4aa799e2 100644 --- a/opentelemetry-distro/tests/test_distro.py +++ b/opentelemetry-distro/tests/test_distro.py @@ -19,7 +19,7 @@ from pkg_resources import DistributionNotFound, require from opentelemetry.distro import OpenTelemetryDistro -from opentelemetry.environment_variables import OTEL_TRACE_EXPORTER +from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER class TestDistribution(TestCase): @@ -31,6 +31,6 @@ def test_package_available(self): def test_default_configuration(self): distro = OpenTelemetryDistro() - self.assertIsNone(os.environ.get(OTEL_TRACE_EXPORTER)) + self.assertIsNone(os.environ.get(OTEL_TRACES_EXPORTER)) distro.configure() - self.assertEqual("otlp_span", os.environ.get(OTEL_TRACE_EXPORTER)) + self.assertEqual("otlp_span", os.environ.get(OTEL_TRACES_EXPORTER)) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index 66bf9caddd..dfd83a4983 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -23,7 +23,7 @@ from opentelemetry.environment_variables import ( OTEL_PYTHON_IDS_GENERATOR, OTEL_PYTHON_SERVICE_NAME, - OTEL_TRACE_EXPORTER, + OTEL_TRACES_EXPORTER, ) logger = getLogger(__file__) @@ -82,7 +82,7 @@ def parse_args(): def load_config_from_cli_args(args): if args.trace_exporter: - environ[OTEL_TRACE_EXPORTER] = args.trace_exporter + environ[OTEL_TRACES_EXPORTER] = args.trace_exporter if args.service_name: environ[OTEL_PYTHON_SERVICE_NAME] = args.service_name if args.ids_generator: diff --git a/opentelemetry-instrumentation/tests/test_run.py b/opentelemetry-instrumentation/tests/test_run.py index 9956ad3c25..69d4bdecf1 100644 --- a/opentelemetry-instrumentation/tests/test_run.py +++ b/opentelemetry-instrumentation/tests/test_run.py @@ -20,7 +20,7 @@ from opentelemetry.environment_variables import ( OTEL_PYTHON_SERVICE_NAME, - OTEL_TRACE_EXPORTER, + OTEL_TRACES_EXPORTER, ) from opentelemetry.instrumentation import auto_instrumentation @@ -111,13 +111,13 @@ class TestArgs(TestCase): def test_exporter(self, _): # pylint: disable=no-self-use with patch("sys.argv", ["instrument", "2"]): auto_instrumentation.run() - self.assertIsNone(environ.get(OTEL_TRACE_EXPORTER)) + self.assertIsNone(environ.get(OTEL_TRACES_EXPORTER)) with patch( "sys.argv", ["instrument", "--trace-exporter", "jaeger", "1", "2"] ): auto_instrumentation.run() - self.assertEqual(environ.get(OTEL_TRACE_EXPORTER), "jaeger") + self.assertEqual(environ.get(OTEL_TRACES_EXPORTER), "jaeger") @patch("opentelemetry.instrumentation.auto_instrumentation.execl") def test_service_name(self, _): # pylint: disable=no-self-use diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index bf2c45a255..2770b3659c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -14,8 +14,8 @@ OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES" OTEL_LOG_LEVEL = "OTEL_LOG_LEVEL" -OTEL_TRACE_SAMPLER = "OTEL_TRACE_SAMPLER" -OTEL_TRACE_SAMPLER_ARG = "OTEL_TRACE_SAMPLER_ARG" +OTEL_TRACES_SAMPLER = "OTEL_TRACES_SAMPLER" +OTEL_TRACES_SAMPLER_ARG = "OTEL_TRACES_SAMPLER_ARG" OTEL_BSP_SCHEDULE_DELAY = "OTEL_BSP_SCHEDULE_DELAY" OTEL_BSP_EXPORT_TIMEOUT = "OTEL_BSP_EXPORT_TIMEOUT" OTEL_BSP_MAX_QUEUE_SIZE = "OTEL_BSP_MAX_QUEUE_SIZE" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 17e90f78a9..cc860fc7a7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -60,8 +60,8 @@ with trace.get_tracer(__name__).start_as_current_span("Test Span"): ... -The tracer sampler can also be configured via environment variables ``OTEL_TRACE_SAMPLER`` and ``OTEL_TRACE_SAMPLER_ARG`` (only if applicable). -The list of known values for ``OTEL_TRACE_SAMPLER`` are: +The tracer sampler can also be configured via environment variables ``OTEL_TRACES_SAMPLER`` and ``OTEL_TRACES_SAMPLER_ARG`` (only if applicable). +The list of known values for ``OTEL_TRACES_SAMPLER`` are: * always_on - Sampler that always samples spans, regardless of the parent span's sampling decision. * always_off - Sampler that never samples spans, regardless of the parent span's sampling decision. @@ -70,10 +70,10 @@ * parentbased_always_off - Sampler that respects its parent span's sampling decision, but otherwise never samples. * parentbased_traceidratio - Sampler that respects its parent span's sampling decision, but otherwise samples probabalistically based on rate. -Sampling probability can be set with ``OTEL_TRACE_SAMPLER_ARG`` if the sampler is traceidratio or parentbased_traceidratio, when not provided rate will be set to 1.0 (maximum rate possible). +Sampling probability can be set with ``OTEL_TRACES_SAMPLER_ARG`` if the sampler is traceidratio or parentbased_traceidratio, when not provided rate will be set to 1.0 (maximum rate possible). -Prev example but with environment vairables. Please make sure to set the env ``OTEL_TRACE_SAMPLER=traceidratio`` and ``OTEL_TRACE_SAMPLER_ARG=0.001``. +Prev example but with environment vairables. Please make sure to set the env ``OTEL_TRACES_SAMPLER=traceidratio`` and ``OTEL_TRACES_SAMPLER_ARG=0.001``. .. code:: python @@ -105,8 +105,8 @@ # pylint: disable=unused-import from opentelemetry.context import Context from opentelemetry.sdk.environment_variables import ( - OTEL_TRACE_SAMPLER, - OTEL_TRACE_SAMPLER_ARG, + OTEL_TRACES_SAMPLER, + OTEL_TRACES_SAMPLER_ARG, ) from opentelemetry.trace import Link, get_current_span from opentelemetry.trace.span import TraceState @@ -370,7 +370,7 @@ def __init__(self, rate: float): def _get_from_env_or_default() -> Sampler: trace_sampler = os.getenv( - OTEL_TRACE_SAMPLER, "parentbased_always_on" + OTEL_TRACES_SAMPLER, "parentbased_always_on" ).lower() if trace_sampler not in _KNOWN_SAMPLERS: _logger.warning("Couldn't recognize sampler %s.", trace_sampler) @@ -378,9 +378,9 @@ def _get_from_env_or_default() -> Sampler: if trace_sampler in ("traceidratio", "parentbased_traceidratio"): try: - rate = float(os.getenv(OTEL_TRACE_SAMPLER_ARG)) + rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG)) except ValueError: - _logger.warning("Could not convert TRACE_SAMPLER_ARG to float.") + _logger.warning("Could not convert TRACES_SAMPLER_ARG to float.") rate = 1.0 return _KNOWN_SAMPLERS[trace_sampler](rate) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 6969b65b11..f6af8fea35 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -30,8 +30,8 @@ OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT, OTEL_SPAN_LINK_COUNT_LIMIT, - OTEL_TRACE_SAMPLER, - OTEL_TRACE_SAMPLER_ARG, + OTEL_TRACES_SAMPLER, + OTEL_TRACES_SAMPLER_ARG, ) from opentelemetry.sdk.trace import Resource, sampling from opentelemetry.sdk.trace.ids_generator import RandomIdsGenerator @@ -194,7 +194,7 @@ def test_sampler_no_sampling(self): trace_api.TraceFlags.DEFAULT, ) - @mock.patch.dict("os.environ", {OTEL_TRACE_SAMPLER: "always_off"}) + @mock.patch.dict("os.environ", {OTEL_TRACES_SAMPLER: "always_off"}) def test_sampler_with_env(self): # pylint: disable=protected-access reload(trace) @@ -213,8 +213,8 @@ def test_sampler_with_env(self): @mock.patch.dict( "os.environ", { - OTEL_TRACE_SAMPLER: "parentbased_traceidratio", - OTEL_TRACE_SAMPLER_ARG: "0.25", + OTEL_TRACES_SAMPLER: "parentbased_traceidratio", + OTEL_TRACES_SAMPLER_ARG: "0.25", }, ) def test_ratio_sampler_with_env(self): From 06bbb8efaa1cf7b37948d6290d862aa40c9fc54b Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 11 Feb 2021 15:32:35 -0800 Subject: [PATCH 0774/1517] minor fixes to release scripts (#1561) --- scripts/eachdist.py | 32 ++++++++++++++++---------------- scripts/prepare_release.sh | 4 +--- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 4bf1f33370..d1069290e8 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -533,13 +533,13 @@ def update_changelog(path, version, new_entry): try: with open(path) as changelog: text = changelog.read() - if "## Version {}".format(version) in text: + if "## [{}]".format(version) in text: raise AttributeError( "{} already contans version {}".format(path, version) ) with open(path) as changelog: for line in changelog: - if line.startswith("## Unreleased"): + if line.startswith("## [Unreleased]"): unreleased_changes = False elif line.startswith("## "): break @@ -552,26 +552,26 @@ def update_changelog(path, version, new_entry): if unreleased_changes: print("updating: {}".format(path)) - text = re.sub("## Unreleased", new_entry, text) + text = re.sub(r"## \[Unreleased\].*", new_entry, text) with open(path, "w") as changelog: changelog.write(text) -def update_changelogs(targets, version): - print("updating CHANGELOG") +def update_changelogs(version): today = datetime.now().strftime("%Y-%m-%d") - new_entry = "## Unreleased\n\n## Version {}\n\nReleased {}".format( - version, today + new_entry = """## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v{version}...HEAD) + +## [{version}](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v{version}) - {today} + +""".format( + version=version, today=today ) errors = False - for target in targets: - try: - update_changelog( - "{}/CHANGELOG.md".format(target), version, new_entry - ) - except Exception as err: # pylint: disable=broad-except - print(str(err)) - errors = True + try: + update_changelog("./CHANGELOG.md", version, new_entry) + except Exception as err: # pylint: disable=broad-except + print(str(err)) + errors = True if errors: sys.exit(1) @@ -637,7 +637,7 @@ def release_args(args): version = args.version update_dependencies(targets, version) update_version_files(targets, version) - update_changelogs(targets, version) + update_changelogs(version) def test_args(args): diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh index 09b934420d..18fe8e8a17 100755 --- a/scripts/prepare_release.sh +++ b/scripts/prepare_release.sh @@ -28,8 +28,6 @@ git reset --hard origin/main git checkout -b release/${VERSION} git push origin release/${VERSION} -# create a temporary branch to create a PR for updated version and changelogs -git checkout -b release/${VERSION}-auto ./scripts/eachdist.py release --version ${VERSION} rc=$? if [ $rc != 0 ]; then @@ -37,7 +35,7 @@ if [ $rc != 0 ]; then exit 0 fi -git add **/version.py **/setup.cfg **/CHANGELOG.md +git add . git commit -m "updating changelogs and version to ${VERSION}" From 8e34a26427098965a2bb7291f5e8acf40c614d89 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 11 Feb 2021 15:55:39 -0800 Subject: [PATCH 0775/1517] update limits as per specification (#1597) --- CHANGELOG.md | 2 ++ opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d03dbadd3..dfff8d1ae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1571](https://github.com/open-telemetry/opentelemetry-python/pull/1571)]) - Rename `TRACE_` to `TRACES_` for environment variables ([#1595](https://github.com/open-telemetry/opentelemetry-python/pull/1595)]) +- Limits for Span attributes, events and links have been updated to 128 + ([1597](https://github.com/open-telemetry/opentelemetry-python/pull/1597)]) ### Added - Added `end_on_exit` argument to `start_as_current_span` diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0a6eefbd84..b7b00d9ed1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -61,11 +61,11 @@ logger = logging.getLogger(__name__) SPAN_ATTRIBUTE_COUNT_LIMIT = int( - environ.get(OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, 1000) + environ.get(OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, 128) ) -SPAN_EVENT_COUNT_LIMIT = int(environ.get(OTEL_SPAN_EVENT_COUNT_LIMIT, 1000)) -SPAN_LINK_COUNT_LIMIT = int(environ.get(OTEL_SPAN_LINK_COUNT_LIMIT, 1000)) +SPAN_EVENT_COUNT_LIMIT = int(environ.get(OTEL_SPAN_EVENT_COUNT_LIMIT, 128)) +SPAN_LINK_COUNT_LIMIT = int(environ.get(OTEL_SPAN_LINK_COUNT_LIMIT, 128)) VALID_ATTR_VALUE_TYPES = (bool, str, int, float) # pylint: disable=protected-access TRACE_SAMPLER = sampling._get_from_env_or_default() From 5967374f37105d461f27db2013291d8e663e0363 Mon Sep 17 00:00:00 2001 From: Anton Ryzhov Date: Fri, 12 Feb 2021 01:14:42 +0100 Subject: [PATCH 0776/1517] Flush export queue when it reaches `max_export_batch_size` (#1521) --- CHANGELOG.md | 3 ++- .../src/opentelemetry/sdk/trace/export/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfff8d1ae3..af9d9c2f56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,10 +26,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add urllib to opentelemetry-bootstrap target list ([#1584])(https://github.com/open-telemetry/opentelemetry-python/pull/1584) - ### Changed - Read-only Span attributes have been moved to ReadableSpan class ([#1560](https://github.com/open-telemetry/opentelemetry-python/pull/1560)) +- `BatchExportSpanProcessor` flushes export queue when it reaches `max_export_batch_size` + ([#1521])(https://github.com/open-telemetry/opentelemetry-python/pull/1521) ### Removed - Remove Configuration diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 932d05f233..f49f4bddce 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -200,7 +200,7 @@ def on_end(self, span: ReadableSpan) -> None: self.queue.appendleft(span) - if len(self.queue) >= self.max_queue_size // 2: + if len(self.queue) >= self.max_export_batch_size: with self.condition: self.condition.notify() From 0fe1f476a05bfa055a868d8433488a785bb65cdc Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 12 Feb 2021 09:39:24 -0600 Subject: [PATCH 0777/1517] Make util a namespace (#1582) --- .github/workflows/test.yml | 2 +- opentelemetry-api/src/opentelemetry/trace/__init__.py | 3 ++- .../src/opentelemetry/util/{__init__.py => providers.py} | 0 opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 3 ++- .../src/opentelemetry/sdk/trace/export/__init__.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- shim/opentelemetry-opentracing-shim/tests/test_util.py | 2 +- 7 files changed, 8 insertions(+), 6 deletions(-) rename opentelemetry-api/src/opentelemetry/util/{__init__.py => providers.py} (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dfa5f957bd..a31dab6c93 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 263adc5f7f524fae2c84571f656cef0896de0868 + CONTRIB_REPO_SHA: 74af6ab48505cdb148de46fb12cf0802a199f297 jobs: build: diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 218c8256f0..ede12ab442 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -101,7 +101,8 @@ format_trace_id, ) from opentelemetry.trace.status import Status -from opentelemetry.util import _load_trace_provider, types +from opentelemetry.util import types +from opentelemetry.util.providers import _load_trace_provider logger = getLogger(__name__) diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/providers.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/util/__init__.py rename to opentelemetry-api/src/opentelemetry/util/providers.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index b7b00d9ed1..39f34e961e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -56,7 +56,8 @@ from opentelemetry.trace import SpanContext from opentelemetry.trace.propagation import SPAN_KEY from opentelemetry.trace.status import Status, StatusCode -from opentelemetry.util import time_ns, types +from opentelemetry.util import types +from opentelemetry.util.providers import time_ns logger = logging.getLogger(__name__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index f49f4bddce..18f1465473 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -29,7 +29,7 @@ OTEL_BSP_SCHEDULE_DELAY, ) from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor -from opentelemetry.util import time_ns +from opentelemetry.util.providers import time_ns logger = logging.getLogger(__name__) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index f6af8fea35..043c6dc0e3 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -38,7 +38,7 @@ from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace.status import StatusCode -from opentelemetry.util import time_ns +from opentelemetry.util.providers import time_ns def new_tracer() -> trace_api.Tracer: diff --git a/shim/opentelemetry-opentracing-shim/tests/test_util.py b/shim/opentelemetry-opentracing-shim/tests/test_util.py index 7e34c1ac05..086e0bef5f 100644 --- a/shim/opentelemetry-opentracing-shim/tests/test_util.py +++ b/shim/opentelemetry-opentracing-shim/tests/test_util.py @@ -16,7 +16,7 @@ import unittest from opentelemetry.shim.opentracing_shim import util -from opentelemetry.util import time_ns +from opentelemetry.util.providers import time_ns class TestUtil(unittest.TestCase): From 21007cc0bf10c5e76d85ba5b9f72e0777717797d Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 12 Feb 2021 15:23:45 -0600 Subject: [PATCH 0778/1517] Remove sitecustomize path from PYTHONPATH (#1583) --- .../instrumentation/auto_instrumentation/sitecustomize.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index ba84bce585..10c8faf899 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -15,6 +15,8 @@ import sys from logging import getLogger from os import environ, path +from os.path import abspath, dirname, pathsep +from re import sub from pkg_resources import iter_entry_points @@ -83,6 +85,12 @@ def initialize(): _load_instrumentors() except Exception: # pylint: disable=broad-except logger.exception("Failed to auto initialize opentelemetry") + finally: + environ["PYTHONPATH"] = sub( + r"{}{}?".format(dirname(abspath(__file__)), pathsep), + "", + environ["PYTHONPATH"], + ) if ( From a3d23a892d591da00833a2ee8406fe89c0536bc9 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 16 Feb 2021 09:35:08 -0800 Subject: [PATCH 0779/1517] Update main after 1.0.0rc1 release (#1603) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 18 ++++++++---------- .../error_handler/error_handler_0/setup.cfg | 2 +- .../error_handler/error_handler_1/setup.cfg | 2 +- .../opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../opentelemetry/exporter/jaeger/version.py | 2 +- .../setup.cfg | 4 ++-- exporter/opentelemetry-exporter-otlp/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 6 +++--- opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 2 +- .../src/opentelemetry/sdk/version.py | 2 +- .../opentelemetry-propagator-b3/setup.cfg | 2 +- .../opentelemetry/propagators/b3/version.py | 2 +- .../opentelemetry-propagator-jaeger/setup.cfg | 2 +- .../propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- tests/util/setup.cfg | 4 ++-- 23 files changed, 38 insertions(+), 40 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a31dab6c93..b60727d00d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 74af6ab48505cdb148de46fb12cf0802a199f297 + CONTRIB_REPO_SHA: 370afa618c30a9773b0594c2ea469518c8b2c274 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index af9d9c2f56..a655af87e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.17b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.0.0rc1...HEAD) + +## [1.0.0rc1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0rc1) - 2021-02-12 ### Changed -- Tracer and Meter provider environment variables are now consistent with the rest +- Tracer provider environment variables are now consistent with the rest ([#1571](https://github.com/open-telemetry/opentelemetry-python/pull/1571)]) - Rename `TRACE_` to `TRACES_` for environment variables ([#1595](https://github.com/open-telemetry/opentelemetry-python/pull/1595)]) - Limits for Span attributes, events and links have been updated to 128 ([1597](https://github.com/open-telemetry/opentelemetry-python/pull/1597)]) +- Read-only Span attributes have been moved to ReadableSpan class + ([#1560](https://github.com/open-telemetry/opentelemetry-python/pull/1560)) +- `BatchExportSpanProcessor` flushes export queue when it reaches `max_export_batch_size` + ([#1521])(https://github.com/open-telemetry/opentelemetry-python/pull/1521) ### Added - Added `end_on_exit` argument to `start_as_current_span` @@ -23,14 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1480](https://github.com/open-telemetry/opentelemetry-python/pull/1480)) - Allow missing carrier headers to continue without raising AttributeError ([#1545](https://github.com/open-telemetry/opentelemetry-python/pull/1545)) -- Add urllib to opentelemetry-bootstrap target list - ([#1584])(https://github.com/open-telemetry/opentelemetry-python/pull/1584) - -### Changed -- Read-only Span attributes have been moved to ReadableSpan class - ([#1560](https://github.com/open-telemetry/opentelemetry-python/pull/1560)) -- `BatchExportSpanProcessor` flushes export queue when it reaches `max_export_batch_size` - ([#1521])(https://github.com/open-telemetry/opentelemetry-python/pull/1521) ### Removed - Remove Configuration diff --git a/docs/examples/error_handler/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg index b79d972bc1..b26c9d57a1 100644 --- a/docs/examples/error_handler/error_handler_0/setup.cfg +++ b/docs/examples/error_handler/error_handler_0/setup.cfg @@ -37,7 +37,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.18.dev0 + opentelemetry-sdk == 1.0.0.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg index f1ebad60f8..f24ed6bd0d 100644 --- a/docs/examples/error_handler/error_handler_1/setup.cfg +++ b/docs/examples/error_handler/error_handler_1/setup.cfg @@ -37,7 +37,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 0.18.dev0 + opentelemetry-sdk == 1.0.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index c5fa47b2d6..02dbd4597c 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -42,8 +42,8 @@ install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 thrift >= 0.10.0 - opentelemetry-api == 0.18.dev0 - opentelemetry-sdk == 0.18.dev0 + opentelemetry-api == 1.0.0.dev0 + opentelemetry-sdk == 1.0.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 70e291a43a..15eb84e8c0 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "1.0.0.dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 9c8dc747dd..b10fbe2f04 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 0.18.dev0 - opentelemetry-sdk == 0.18.dev0 + opentelemetry-api == 1.0.0.dev0 + opentelemetry-sdk == 1.0.0.dev0 protobuf >= 3.13.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index ef8386d944..612529ff3e 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -42,9 +42,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 0.18.dev0 - opentelemetry-sdk == 0.18.dev0 - opentelemetry-proto == 0.18.dev0 + opentelemetry-api == 1.0.0.dev0 + opentelemetry-sdk == 1.0.0.dev0 + opentelemetry-proto == 1.0.0.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index ebb75f6c11..e4529dffc6 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "1.0.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 45cdb03436..58cad78612 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 - opentelemetry-api == 0.18.dev0 - opentelemetry-sdk == 0.18.dev0 + opentelemetry-api == 1.0.0.dev0 + opentelemetry-sdk == 1.0.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index ebb75f6c11..e4529dffc6 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "1.0.0.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index ebb75f6c11..e4529dffc6 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "1.0.0.dev0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index d2a40b0e04..41eeda5d70 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.18.dev0 - opentelemetry-sdk == 0.18.dev0 + opentelemetry-api == 1.0.0.dev0 + opentelemetry-sdk == 1.0.0.dev0 [options.packages.find] where = src @@ -56,4 +56,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 0.18.dev0 + opentelemetry-exporter-otlp == 1.0.0.dev0 diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 0ebc59d2db..db8c4675be 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.18.dev0 + opentelemetry-api == 1.0.0.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index ebb75f6c11..e4529dffc6 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "1.0.0.dev0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 988d571cd5..231b715980 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 0.18.dev0 + opentelemetry-api == 1.0.0.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index ebb75f6c11..e4529dffc6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "1.0.0.dev0" diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 903117996c..8062d85339 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.18.dev0 + opentelemetry-api == 1.0.0.dev0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index ebb75f6c11..e4529dffc6 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "1.0.0.dev0" diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index c94e3e8b49..b24569100f 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.18.dev0 + opentelemetry-api == 1.0.0.dev0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index ebb75f6c11..e4529dffc6 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "1.0.0.dev0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 667a9e5a4c..f7a06ea35d 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 0.18.dev0 + opentelemetry-api == 1.0.0.dev0 [options.extras_require] test = diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index eea2bc5b60..94333459ab 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.18.dev0 - opentelemetry-sdk == 0.18.dev0 + opentelemetry-api == 1.0.0.dev0 + opentelemetry-sdk == 1.0.0.dev0 [options.extras_require] test = flask~=1.0 From 22cfddd46acbec4136b914fd05d308a7d99c3d1b Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 16 Feb 2021 13:53:41 -0800 Subject: [PATCH 0780/1517] Fix for pylint (#1613) --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 39f34e961e..e3f489706f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -122,6 +122,8 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: """ +# Temporary fix until https://github.com/PyCQA/pylint/issues/4098 is resolved +# pylint:disable=no-member class SynchronousMultiSpanProcessor(SpanProcessor): """Implementation of class:`SpanProcessor` that forwards all received events to a list of span processors sequentially. From ea9e93350d468573ea2007898e1048d0bf9b4605 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 16 Feb 2021 17:14:24 -0600 Subject: [PATCH 0781/1517] Make propagators a namespace (#1591) --- .github/workflows/test.yml | 2 +- .../auto-instrumentation/server_instrumented.py | 5 +++-- docs/examples/datadog_exporter/client.py | 5 +++-- docs/examples/datadog_exporter/server.py | 16 ++++++++-------- docs/examples/django/client.py | 5 +++-- docs/getting-started.rst | 4 ++-- .../{propagators => propagate}/__init__.py | 0 .../propagators/test_global_httptextformat.py | 3 +-- .../tests/propagators/test_propagators.py | 8 ++++---- .../shim/opentracing_shim/__init__.py | 6 +++--- .../tests/test_shim.py | 15 ++++++++------- 11 files changed, 36 insertions(+), 33 deletions(-) rename opentelemetry-api/src/opentelemetry/{propagators => propagate}/__init__.py (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b60727d00d..4a48de8eaf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 370afa618c30a9773b0594c2ea469518c8b2c274 + CONTRIB_REPO_SHA: 8e1dd27be3a2101c9034e480560fd5ceb6712d51 jobs: build: diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_instrumented.py index 6212ec3333..66bb0b584a 100644 --- a/docs/examples/auto-instrumentation/server_instrumented.py +++ b/docs/examples/auto-instrumentation/server_instrumented.py @@ -14,8 +14,9 @@ from flask import Flask, request -from opentelemetry import propagators, trace +from opentelemetry import trace from opentelemetry.instrumentation.wsgi import collect_request_attributes +from opentelemetry.propagate import extract from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -37,7 +38,7 @@ def server_request(): with tracer.start_as_current_span( "server_request", - context=propagators.extract(DictGetter(), request.headers), + context=extract(DictGetter(), request.headers), kind=trace.SpanKind.SERVER, attributes=collect_request_attributes(request.environ), ): diff --git a/docs/examples/datadog_exporter/client.py b/docs/examples/datadog_exporter/client.py index a5ddf2843c..6b4b5d00ec 100644 --- a/docs/examples/datadog_exporter/client.py +++ b/docs/examples/datadog_exporter/client.py @@ -16,11 +16,12 @@ from requests import get -from opentelemetry import propagators, trace +from opentelemetry import trace from opentelemetry.exporter.datadog import ( DatadogExportSpanProcessor, DatadogSpanExporter, ) +from opentelemetry.propagate import inject from opentelemetry.sdk import resources from opentelemetry.sdk.trace import TracerProvider @@ -46,7 +47,7 @@ with tracer.start_as_current_span("client-server"): headers = {} - propagators.inject(dict.__setitem__, headers) + inject(dict.__setitem__, headers) requested = get( "http://localhost:8082/server_request", params={"param": argv[1]}, diff --git a/docs/examples/datadog_exporter/server.py b/docs/examples/datadog_exporter/server.py index 9c83de8bb8..662d3ebe97 100644 --- a/docs/examples/datadog_exporter/server.py +++ b/docs/examples/datadog_exporter/server.py @@ -14,12 +14,14 @@ from flask import Flask, request -from opentelemetry import propagators, trace +from opentelemetry import trace from opentelemetry.exporter.datadog import ( DatadogExportSpanProcessor, DatadogSpanExporter, ) from opentelemetry.exporter.datadog.propagator import DatadogFormat +from opentelemetry.propagate import get_global_textmap, set_global_textmap +from opentelemetry.propagators.composite import CompositeHTTPPropagator from opentelemetry.sdk.trace import TracerProvider app = Flask(__name__) @@ -35,19 +37,17 @@ ) # append Datadog format for propagation to and from Datadog instrumented services -global_textmap = propagators.get_global_textmap() -if isinstance( - global_textmap, propagators.composite.CompositeHTTPPropagator -) and not any( +global_textmap = get_global_textmap() +if isinstance(global_textmap, CompositeHTTPPropagator) and not any( isinstance(p, DatadogFormat) for p in global_textmap._propagators ): - propagators.set_global_textmap( - propagators.composite.CompositeHTTPPropagator( + set_global_textmap( + CompositeHTTPPropagator( global_textmap._propagators + [DatadogFormat()] ) ) else: - propagators.set_global_textmap(DatadogFormat()) + set_global_textmap(DatadogFormat()) tracer = trace.get_tracer(__name__) diff --git a/docs/examples/django/client.py b/docs/examples/django/client.py index e65285c35d..d32be31a85 100644 --- a/docs/examples/django/client.py +++ b/docs/examples/django/client.py @@ -16,7 +16,8 @@ from requests import get -from opentelemetry import propagators, trace +from opentelemetry import trace +from opentelemetry.propagate import inject from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -35,7 +36,7 @@ with tracer.start_as_current_span("client-server"): headers = {} - propagators.inject(dict.__setitem__, headers) + inject(dict.__setitem__, headers) requested = get( "http://localhost:8000", params={"param": argv[1]}, diff --git a/docs/getting-started.rst b/docs/getting-started.rst index dce074e897..6432e02103 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -195,10 +195,10 @@ Following the installation of the package containing the b3 propagator, configur .. code-block:: python - from opentelemetry import propagators + from opentelemetry.propagate import set_global_textmap from opentelemetry.propagators.b3 import B3Format - propagators.set_global_textmap(B3Format()) + set_global_textmap(B3Format()) Use the OpenTelemetry Collector for traces ------------------------------------------ diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/propagators/__init__.py rename to opentelemetry-api/src/opentelemetry/propagate/__init__.py diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index b704207ed5..88b1920468 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -12,11 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import typing import unittest from opentelemetry import baggage, trace -from opentelemetry.propagators import extract, inject +from opentelemetry.propagate import extract, inject from opentelemetry.trace import get_current_span, set_span_in_context from opentelemetry.trace.propagation.textmap import DictGetter diff --git a/opentelemetry-api/tests/propagators/test_propagators.py b/opentelemetry-api/tests/propagators/test_propagators.py index 9fceb91a0b..5e2abaed1a 100644 --- a/opentelemetry-api/tests/propagators/test_propagators.py +++ b/opentelemetry-api/tests/propagators/test_propagators.py @@ -40,9 +40,9 @@ def test_propagators(propagators): **{"side_effect": test_propagators} ) - import opentelemetry.propagators + import opentelemetry.propagate - reload(opentelemetry.propagators) + reload(opentelemetry.propagate) @patch.dict(environ, {OTEL_PROPAGATORS: "a,b,c"}) @patch("opentelemetry.propagators.composite.CompositeHTTPPropagator") @@ -75,6 +75,6 @@ def test_propagators(propagators): **{"side_effect": test_propagators} ) - import opentelemetry.propagators + import opentelemetry.propagate - reload(opentelemetry.propagators) + reload(opentelemetry.propagate) diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index 74c8952664..1083e68263 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -99,9 +99,9 @@ UnsupportedFormatException, ) -from opentelemetry import propagators from opentelemetry.baggage import get_baggage, set_baggage from opentelemetry.context import Context, attach, detach, get_value, set_value +from opentelemetry.propagate import get_global_textmap from opentelemetry.shim.opentracing_shim import util from opentelemetry.shim.opentracing_shim.version import __version__ from opentelemetry.trace import INVALID_SPAN_CONTEXT, DefaultSpan, Link @@ -682,7 +682,7 @@ def inject(self, span_context, format: object, carrier: object): if format not in self._supported_formats: raise UnsupportedFormatException - propagator = propagators.get_global_textmap() + propagator = get_global_textmap() ctx = set_span_in_context(DefaultSpan(span_context.unwrap())) propagator.inject(type(carrier).__setitem__, carrier, context=ctx) @@ -712,7 +712,7 @@ def extract(self, format: object, carrier: object): if format not in self._supported_formats: raise UnsupportedFormatException - propagator = propagators.get_global_textmap() + propagator = get_global_textmap() ctx = propagator.extract(self._carrier_getter, carrier) span = get_current_span(ctx) if span is not None: diff --git a/shim/opentelemetry-opentracing-shim/tests/test_shim.py b/shim/opentelemetry-opentracing-shim/tests/test_shim.py index 54f1caa92c..a27d30de71 100644 --- a/shim/opentelemetry-opentracing-shim/tests/test_shim.py +++ b/shim/opentelemetry-opentracing-shim/tests/test_shim.py @@ -21,7 +21,8 @@ import opentracing -from opentelemetry import propagators, trace +from opentelemetry import trace +from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.sdk.trace import TracerProvider from opentelemetry.shim.opentracing_shim import ( SpanContextShim, @@ -46,15 +47,15 @@ def setUp(self): @classmethod def setUpClass(cls): # Save current propagator to be restored on teardown. - cls._previous_propagator = propagators.get_global_textmap() + cls._previous_propagator = get_global_textmap() # Set mock propagator for testing. - propagators.set_global_textmap(MockTextMapPropagator()) + set_global_textmap(MockTextMapPropagator()) @classmethod def tearDownClass(cls): # Restore previous propagator. - propagators.set_global_textmap(cls._previous_propagator) + set_global_textmap(cls._previous_propagator) def test_shim_type(self): # Verify shim is an OpenTracing tracer. @@ -542,15 +543,15 @@ def test_extract_empty_context_returns_invalid_context(self): """In the case where the propagator cannot extract a SpanContext, extract should return and invalid span context. """ - _old_propagator = propagators.get_global_textmap() - propagators.set_global_textmap(NOOPTextMapPropagator()) + _old_propagator = get_global_textmap() + set_global_textmap(NOOPTextMapPropagator()) try: carrier = {} ctx = self.shim.extract(opentracing.Format.HTTP_HEADERS, carrier) self.assertEqual(ctx.unwrap(), trace.INVALID_SPAN_CONTEXT) finally: - propagators.set_global_textmap(_old_propagator) + set_global_textmap(_old_propagator) def test_extract_text_map(self): """Test `extract()` method for Format.TEXT_MAP.""" From a1a340ee65d1943d7d474ba49f96db23fdbcec67 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 16 Feb 2021 16:24:20 -0800 Subject: [PATCH 0782/1517] [Chore] Update main after 0.18b0 release (#1614) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 8 +++++++- .../error_handler_0/src/error_handler_0/version.py | 2 +- .../error_handler_1/src/error_handler_1/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- opentelemetry-distro/src/opentelemetry/distro/version.py | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 10 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a48de8eaf..d1c0db2b9d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 8e1dd27be3a2101c9034e480560fd5ceb6712d51 + CONTRIB_REPO_SHA: 6e328246c895ff433b14430c9edddfead072287c jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index a655af87e4..bcb8d65d4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.0.0rc1...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.18b0...HEAD) + +## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 + +### Added +- Add urllib to opentelemetry-bootstrap target list + ([#1584])(https://github.com/open-telemetry/opentelemetry-python/pull/1584) ## [1.0.0rc1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0rc1) - 2021-02-12 diff --git a/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py index ebb75f6c11..9194ed4a33 100644 --- a/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py +++ b/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "0.19.dev0" diff --git a/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py index ebb75f6c11..9194ed4a33 100644 --- a/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py +++ b/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "0.19.dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index ebb75f6c11..9194ed4a33 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "0.19.dev0" diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index ebb75f6c11..9194ed4a33 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "0.19.dev0" diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index ebb75f6c11..9194ed4a33 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "0.19.dev0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index f7a06ea35d..8a490470cb 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -46,7 +46,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test == 0.18.dev0 + opentelemetry-test == 0.19.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index ebb75f6c11..9194ed4a33 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.dev0" +__version__ = "0.19.dev0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index b34d984ac9..19edb16431 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.18.dev0" +__version__ = "0.19.dev0" From 03c6b7391d16236ad5d4e48b0cb0bff786c2e583 Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Wed, 17 Feb 2021 12:03:29 -0800 Subject: [PATCH 0783/1517] Zipkin exporter add support for v1 api json format (#1411) --- .flake8 | 2 +- .../zipkin/gen/__init__.py => CHANGELOG.md} | 0 .../opentelemetry/exporter/zipkin/__init__.py | 404 +------ .../exporter/zipkin/encoder/__init__.py | 267 +++++ .../exporter/zipkin/encoder/v1/__init__.py | 41 + .../exporter/zipkin/encoder/v1/json.py | 65 ++ .../exporter/zipkin/encoder/v2/json.py | 67 ++ .../zipkin/encoder/v2/protobuf/__init__.py | 129 +++ .../encoder/v2/protobuf/gen/__init__.py | 0 .../v2/protobuf}/gen/zipkin_pb2.py | 0 .../v2/protobuf}/gen/zipkin_pb2.pyi | 0 .../exporter/zipkin/node_endpoint.py | 85 ++ .../tests/encoder/__init__.py | 0 .../tests/encoder/common_tests.py | 505 +++++++++ .../tests/encoder/test_v1_json.py | 251 +++++ .../tests/encoder/test_v2_json.py | 205 ++++ .../tests/encoder/test_v2_protobuf.py | 234 +++++ .../tests/test_zipkin_exporter.py | 989 ++---------------- .../sdk/environment_variables/__init__.py | 1 - pyproject.toml | 2 +- 20 files changed, 2010 insertions(+), 1237 deletions(-) rename exporter/opentelemetry-exporter-zipkin/{src/opentelemetry/exporter/zipkin/gen/__init__.py => CHANGELOG.md} (100%) create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/__init__.py create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/json.py create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/json.py create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/__init__.py create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/__init__.py rename exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/{ => encoder/v2/protobuf}/gen/zipkin_pb2.py (100%) rename exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/{ => encoder/v2/protobuf}/gen/zipkin_pb2.pyi (100%) create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/node_endpoint.py create mode 100644 exporter/opentelemetry-exporter-zipkin/tests/encoder/__init__.py create mode 100644 exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py create mode 100644 exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py create mode 100644 exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_json.py create mode 100644 exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_protobuf.py diff --git a/.flake8 b/.flake8 index 7b88ce37eb..baa9f02c31 100644 --- a/.flake8 +++ b/.flake8 @@ -18,7 +18,7 @@ exclude = __pycache__ exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/ exporter/opentelemetry-exporter-jaeger/build/* - exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen + exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/ docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ docs/examples/opentelemetry-example-app/build/* opentelemetry-proto/build/* diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/__init__.py b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md similarity index 100% rename from exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/__init__.py rename to exporter/opentelemetry-exporter-zipkin/CHANGELOG.md diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index a7ea40180a..1b2554923f 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -18,16 +18,16 @@ Usage ----- -The **OpenTelemetry Zipkin Exporter** allows to export `OpenTelemetry`_ traces to `Zipkin`_. -This exporter always send traces to the configured Zipkin collector using HTTP. - +The **OpenTelemetry Zipkin Exporter** allows exporting of `OpenTelemetry`_ +traces to `Zipkin`_. This exporter sends traces to the configured Zipkin +collector endpoint using HTTP and supports multiple protocols (v1 json, +v2 json, v2 protobuf). .. _Zipkin: https://zipkin.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. _Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#zipkin-exporter .. envvar:: OTEL_EXPORTER_ZIPKIN_ENDPOINT -.. envvar:: OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT .. code:: python @@ -41,12 +41,13 @@ # create a ZipkinSpanExporter zipkin_exporter = zipkin.ZipkinSpanExporter( - service_name="my-helloworld-service", + # protocol=Protocol.V2_PROTOBUF # optional: - # url="http://localhost:9411/api/v2/spans", - # ipv4="", - # ipv6="", - # retry=False, + # endpoint="http://localhost:9411/api/v2/spans", + # local_node_ipv4="192.168.0.1", + # local_node_ipv6="2001:db8::c001", + # local_node_port=31313, + # max_tag_value_length=256 ) # Create a BatchExportSpanProcessor and add the exporter to it @@ -58,392 +59,81 @@ with tracer.start_as_current_span("foo"): print("Hello world!") -The exporter supports the following environment variables for configuration: - -:envvar:`OTEL_EXPORTER_ZIPKIN_ENDPOINT`: target to which the exporter will -send data. This may include a path (e.g. http://example.com:9411/api/v2/spans). +The exporter supports the following environment variable for configuration: -:envvar:`OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT`: transport interchange format -to use when sending data. Currently only Zipkin's v2 json and protobuf formats -are supported, with v2 json being the default. +:envvar:`OTEL_EXPORTER_ZIPKIN_ENDPOINT`: zipkin collector endpoint to which the +exporter will send data. This may include a path (e.g. +http://example.com:9411/api/v2/spans). API --- """ -import json import logging from os import environ -from typing import Optional, Sequence, Union -from urllib.parse import urlparse +from typing import Optional, Sequence import requests -from opentelemetry.exporter.zipkin.gen import zipkin_pb2 +from opentelemetry.exporter.zipkin.encoder import Encoder, Protocol +from opentelemetry.exporter.zipkin.encoder.v1.json import JsonV1Encoder +from opentelemetry.exporter.zipkin.encoder.v2.json import JsonV2Encoder +from opentelemetry.exporter.zipkin.encoder.v2.protobuf import ProtobufEncoder +from opentelemetry.exporter.zipkin.node_endpoint import IpInput, NodeEndpoint from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_ZIPKIN_ENDPOINT, - OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT, ) from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult -from opentelemetry.trace import Span, SpanContext, SpanKind -from opentelemetry.trace.status import StatusCode - -TRANSPORT_FORMAT_JSON = "json" -TRANSPORT_FORMAT_PROTOBUF = "protobuf" - -DEFAULT_RETRY = False -DEFAULT_URL = "http://localhost:9411/api/v2/spans" -DEFAULT_MAX_TAG_VALUE_LENGTH = 128 - -SPAN_KIND_MAP_JSON = { - SpanKind.INTERNAL: None, - SpanKind.SERVER: "SERVER", - SpanKind.CLIENT: "CLIENT", - SpanKind.PRODUCER: "PRODUCER", - SpanKind.CONSUMER: "CONSUMER", -} +from opentelemetry.trace import Span -SPAN_KIND_MAP_PROTOBUF = { - SpanKind.INTERNAL: zipkin_pb2.Span.Kind.SPAN_KIND_UNSPECIFIED, - SpanKind.SERVER: zipkin_pb2.Span.Kind.SERVER, - SpanKind.CLIENT: zipkin_pb2.Span.Kind.CLIENT, - SpanKind.PRODUCER: zipkin_pb2.Span.Kind.PRODUCER, - SpanKind.CONSUMER: zipkin_pb2.Span.Kind.CONSUMER, -} - -NAME_KEY = "otel.library.name" -VERSION_KEY = "otel.library.version" - -SUCCESS_STATUS_CODES = (200, 202) +DEFAULT_ENDPOINT = "http://localhost:9411/api/v2/spans" +REQUESTS_SUCCESS_STATUS_CODES = (200, 202) logger = logging.getLogger(__name__) class ZipkinSpanExporter(SpanExporter): - """Zipkin span exporter for OpenTelemetry. - - Args: - service_name: Service that logged an annotation in a trace.Classifier - when query for spans. - url: The Zipkin endpoint URL - ipv4: Primary IPv4 address associated with this connection. - ipv6: Primary IPv6 address associated with this connection. - retry: Set to True to configure the exporter to retry on failure. - transport_format: transport interchange format to use - """ - def __init__( self, - service_name: str, - url: str = None, - ipv4: Optional[str] = None, - ipv6: Optional[str] = None, - retry: Optional[str] = DEFAULT_RETRY, - max_tag_value_length: Optional[int] = DEFAULT_MAX_TAG_VALUE_LENGTH, - transport_format: Union[ - TRANSPORT_FORMAT_JSON, TRANSPORT_FORMAT_PROTOBUF, None - ] = None, + protocol: Protocol, + endpoint: Optional[str] = None, + local_node_ipv4: IpInput = None, + local_node_ipv6: IpInput = None, + local_node_port: Optional[int] = None, + max_tag_value_length: Optional[int] = None, ): - self.service_name = service_name - if url is None: - self.url = ( - environ.get(OTEL_EXPORTER_ZIPKIN_ENDPOINT) or DEFAULT_URL - ) - else: - self.url = url - - self.port = urlparse(self.url).port - - self.ipv4 = ipv4 - self.ipv6 = ipv6 - self.retry = retry - self.max_tag_value_length = max_tag_value_length + self.local_node = NodeEndpoint( + local_node_ipv4, local_node_ipv6, local_node_port + ) - if transport_format is None: - self.transport_format = ( - environ.get(OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT) - or TRANSPORT_FORMAT_JSON + if endpoint is None: + endpoint = ( + environ.get(OTEL_EXPORTER_ZIPKIN_ENDPOINT) or DEFAULT_ENDPOINT ) - else: - self.transport_format = transport_format + self.endpoint = endpoint - def export(self, spans: Sequence[Span]) -> SpanExportResult: - if self.transport_format == TRANSPORT_FORMAT_JSON: - content_type = "application/json" - elif self.transport_format == TRANSPORT_FORMAT_PROTOBUF: - content_type = "application/x-protobuf" - else: - logger.error("Invalid transport format %s", self.transport_format) - return SpanExportResult.FAILURE + if protocol == Protocol.V1_JSON: + self.encoder = JsonV1Encoder(max_tag_value_length) + elif protocol == Protocol.V2_JSON: + self.encoder = JsonV2Encoder(max_tag_value_length) + elif protocol == Protocol.V2_PROTOBUF: + self.encoder = ProtobufEncoder(max_tag_value_length) + def export(self, spans: Sequence[Span]) -> SpanExportResult: result = requests.post( - url=self.url, - data=self._translate_to_transport_format(spans), - headers={"Content-Type": content_type}, + url=self.endpoint, + data=self.encoder.serialize(spans, self.local_node), + headers={"Content-Type": self.encoder.content_type()}, ) - if result.status_code not in SUCCESS_STATUS_CODES: + if result.status_code not in REQUESTS_SUCCESS_STATUS_CODES: logger.error( "Traces cannot be uploaded; status code: %s, message %s", result.status_code, result.text, ) - - if self.retry: - return SpanExportResult.FAILURE return SpanExportResult.FAILURE return SpanExportResult.SUCCESS def shutdown(self) -> None: pass - - def _translate_to_transport_format(self, spans: Sequence[Span]): - return ( - self._translate_to_json(spans) - if self.transport_format == TRANSPORT_FORMAT_JSON - else self._translate_to_protobuf(spans) - ) - - def _translate_to_json(self, spans: Sequence[Span]): - local_endpoint = {"serviceName": self.service_name, "port": self.port} - - if self.ipv4 is not None: - local_endpoint["ipv4"] = self.ipv4 - - if self.ipv6 is not None: - local_endpoint["ipv6"] = self.ipv6 - - zipkin_spans = [] - for span in spans: - context = span.get_span_context() - trace_id = context.trace_id - span_id = context.span_id - - # Timestamp in zipkin spans is int of microseconds. - # see: https://zipkin.io/pages/instrumenting.html - start_timestamp_mus = nsec_to_usec_round(span.start_time) - duration_mus = nsec_to_usec_round(span.end_time - span.start_time) - - zipkin_span = { - # Ensure left-zero-padding of traceId, spanId, parentId - "traceId": format(trace_id, "032x"), - "id": format(span_id, "016x"), - "name": span.name, - "timestamp": start_timestamp_mus, - "duration": duration_mus, - "localEndpoint": local_endpoint, - "kind": SPAN_KIND_MAP_JSON[span.kind], - "tags": self._extract_tags_from_span(span), - "annotations": self._extract_annotations_from_events( - span.events - ), - } - - if span.instrumentation_info is not None: - zipkin_span["tags"][NAME_KEY] = span.instrumentation_info.name - zipkin_span["tags"][ - VERSION_KEY - ] = span.instrumentation_info.version - - if span.status.status_code is not StatusCode.UNSET: - zipkin_span["tags"][ - "otel.status_code" - ] = span.status.status_code.name - if span.status.status_code is StatusCode.ERROR: - zipkin_span["tags"]["error"] = ( - span.status.description or "" - ) - - if context.trace_flags.sampled: - zipkin_span["debug"] = True - - if isinstance(span.parent, Span): - zipkin_span["parentId"] = format( - span.parent.get_span_context().span_id, "016x" - ) - elif isinstance(span.parent, SpanContext): - zipkin_span["parentId"] = format(span.parent.span_id, "016x") - - zipkin_spans.append(zipkin_span) - - return json.dumps(zipkin_spans) - - def _translate_to_protobuf(self, spans: Sequence[Span]): - - local_endpoint = zipkin_pb2.Endpoint( - service_name=self.service_name, port=self.port - ) - - if self.ipv4 is not None: - local_endpoint.ipv4 = self.ipv4 - - if self.ipv6 is not None: - local_endpoint.ipv6 = self.ipv6 - - pbuf_spans = zipkin_pb2.ListOfSpans() - - for span in spans: - context = span.get_span_context() - trace_id = context.trace_id.to_bytes( - length=16, byteorder="big", signed=False, - ) - span_id = self.format_pbuf_span_id(context.span_id) - - # Timestamp in zipkin spans is int of microseconds. - # see: https://zipkin.io/pages/instrumenting.html - start_timestamp_mus = nsec_to_usec_round(span.start_time) - duration_mus = nsec_to_usec_round(span.end_time - span.start_time) - - # pylint: disable=no-member - pbuf_span = zipkin_pb2.Span( - trace_id=trace_id, - id=span_id, - name=span.name, - timestamp=start_timestamp_mus, - duration=duration_mus, - local_endpoint=local_endpoint, - kind=SPAN_KIND_MAP_PROTOBUF[span.kind], - tags=self._extract_tags_from_span(span), - ) - - annotations = self._extract_annotations_from_events(span.events) - - if annotations is not None: - for annotation in annotations: - pbuf_span.annotations.append( - zipkin_pb2.Annotation( - timestamp=annotation["timestamp"], - value=annotation["value"], - ) - ) - - if span.instrumentation_info is not None: - pbuf_span.tags.update( - { - NAME_KEY: span.instrumentation_info.name, - VERSION_KEY: span.instrumentation_info.version, - } - ) - - if span.status.status_code is not StatusCode.UNSET: - pbuf_span.tags.update( - {"otel.status_code": span.status.status_code.name} - ) - if span.status.status_code is StatusCode.ERROR: - pbuf_span.tags.update( - {"error": span.status.description or ""} - ) - - if context.trace_flags.sampled: - pbuf_span.debug = True - - if isinstance(span.parent, Span): - pbuf_span.parent_id = self.format_pbuf_span_id( - span.parent.get_span_context().span_id - ) - elif isinstance(span.parent, SpanContext): - pbuf_span.parent_id = self.format_pbuf_span_id( - span.parent.span_id - ) - - pbuf_spans.spans.append(pbuf_span) - - return pbuf_spans.SerializeToString() - - @staticmethod - def format_pbuf_span_id(span_id: int): - return span_id.to_bytes(length=8, byteorder="big", signed=False) - - def _extract_tags_from_dict(self, tags_dict): - tags = {} - if not tags_dict: - return tags - for attribute_key, attribute_value in tags_dict.items(): - if isinstance(attribute_value, bool): - value = str(attribute_value).lower() - elif isinstance(attribute_value, (int, float, str)): - value = str(attribute_value) - elif isinstance(attribute_value, Sequence): - value = self._extract_tag_value_string_from_sequence( - attribute_value - ) - if not value: - logger.warning("Could not serialize tag %s", attribute_key) - continue - else: - logger.warning("Could not serialize tag %s", attribute_key) - continue - - if self.max_tag_value_length > 0: - value = value[: self.max_tag_value_length] - tags[attribute_key] = value - return tags - - def _extract_tag_value_string_from_sequence(self, sequence: Sequence): - if self.max_tag_value_length == 1: - return None - - tag_value_elements = [] - running_string_length = ( - 2 # accounts for array brackets in output string - ) - defined_max_tag_value_length = self.max_tag_value_length > 0 - - for element in sequence: - if isinstance(element, bool): - tag_value_element = str(element).lower() - elif isinstance(element, (int, float, str)): - tag_value_element = str(element) - elif element is None: - tag_value_element = None - else: - continue - - if defined_max_tag_value_length: - if tag_value_element is None: - running_string_length += 4 # null with no quotes - else: - # + 2 accounts for string quotation marks - running_string_length += len(tag_value_element) + 2 - - if tag_value_elements: - # accounts for ',' item separator - running_string_length += 1 - - if running_string_length > self.max_tag_value_length: - break - - tag_value_elements.append(tag_value_element) - - return json.dumps(tag_value_elements, separators=(",", ":")) - - def _extract_tags_from_span(self, span: Span): - tags = self._extract_tags_from_dict(span.attributes) - if span.resource: - tags.update(self._extract_tags_from_dict(span.resource.attributes)) - return tags - - def _extract_annotations_from_events(self, events): - if not events: - return None - - annotations = [] - for event in events: - attrs = {} - for key, value in event.attributes.items(): - if isinstance(value, str): - value = value[: self.max_tag_value_length] - attrs[key] = value - - annotations.append( - { - "timestamp": nsec_to_usec_round(event.timestamp), - "value": json.dumps({event.name: attrs}), - } - ) - return annotations - - -def nsec_to_usec_round(nsec): - """Round nanoseconds to microseconds""" - return (nsec + 500) // 10 ** 3 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py new file mode 100644 index 0000000000..2d4aad6b24 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py @@ -0,0 +1,267 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Zipkin Exporter Transport Encoder + +Base module and abstract class for concrete transport encoders to extend. +""" + +import abc +import json +import logging +from enum import Enum +from typing import Any, Dict, List, Optional, Sequence, TypeVar + +from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint +from opentelemetry.sdk.trace import Event +from opentelemetry.trace import Span, SpanContext +from opentelemetry.trace.status import StatusCode + +EncodedLocalEndpointT = TypeVar("EncodedLocalEndpointT") + +DEFAULT_MAX_TAG_VALUE_LENGTH = 128 +NAME_KEY = "otel.library.name" +VERSION_KEY = "otel.library.version" + +logger = logging.getLogger(__name__) + + +class Protocol(Enum): + """Enum of supported protocol formats. + + Values are human-readable strings so that they can be easily used by the + OS environ var OTEL_EXPORTER_ZIPKIN_PROTOCOL (reserved for future usage). + """ + + V1_JSON = "v1_json" + V2_JSON = "v2_json" + V2_PROTOBUF = "v2_protobuf" + + +# pylint: disable=W0223 +class Encoder(abc.ABC): + """Base class for encoders that are used by the exporter. + + Args: + max_tag_value_length: maximum length of an exported tag value. Values + will be truncated to conform. Since values are serialized to a JSON + list string, max_tag_value_length is honored at the element boundary. + """ + + def __init__( + self, max_tag_value_length: int = DEFAULT_MAX_TAG_VALUE_LENGTH + ): + self.max_tag_value_length = max_tag_value_length + + @staticmethod + @abc.abstractmethod + def content_type() -> str: + pass + + @abc.abstractmethod + def serialize( + self, spans: Sequence[Span], local_endpoint: NodeEndpoint + ) -> str: + pass + + @abc.abstractmethod + def _encode_span( + self, span: Span, encoded_local_endpoint: EncodedLocalEndpointT + ) -> Any: + """ + Per spec Zipkin fields that can be absent SHOULD be omitted from the + payload when they are empty in the OpenTelemetry Span. + + https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk_exporters/zipkin.md#request-payload + """ + + @staticmethod + @abc.abstractmethod + def _encode_local_endpoint( + local_endpoint: NodeEndpoint, + ) -> EncodedLocalEndpointT: + pass + + @staticmethod + def _encode_debug(span_context) -> Any: + return span_context.trace_flags.sampled + + @staticmethod + @abc.abstractmethod + def _encode_span_id(span_id: int) -> Any: + pass + + @staticmethod + @abc.abstractmethod + def _encode_trace_id(trace_id: int) -> Any: + pass + + @staticmethod + def _get_parent_id(span_context) -> Optional[int]: + if isinstance(span_context, Span): + parent_id = span_context.parent.span_id + elif isinstance(span_context, SpanContext): + parent_id = span_context.span_id + else: + parent_id = None + return parent_id + + def _extract_tags_from_dict( + self, tags_dict: Optional[Dict] + ) -> Dict[str, str]: + tags = {} + if not tags_dict: + return tags + for attribute_key, attribute_value in tags_dict.items(): + if isinstance(attribute_value, bool): + value = str(attribute_value).lower() + elif isinstance(attribute_value, (int, float, str)): + value = str(attribute_value) + elif isinstance(attribute_value, Sequence): + value = self._extract_tag_value_string_from_sequence( + attribute_value + ) + if not value: + logger.warning("Could not serialize tag %s", attribute_key) + continue + else: + logger.warning("Could not serialize tag %s", attribute_key) + continue + + if self.max_tag_value_length > 0: + value = value[: self.max_tag_value_length] + tags[attribute_key] = value + return tags + + def _extract_tag_value_string_from_sequence(self, sequence: Sequence): + if self.max_tag_value_length == 1: + return None + + tag_value_elements = [] + running_string_length = ( + 2 # accounts for array brackets in output string + ) + defined_max_tag_value_length = self.max_tag_value_length > 0 + + for element in sequence: + if isinstance(element, bool): + tag_value_element = str(element).lower() + elif isinstance(element, (int, float, str)): + tag_value_element = str(element) + elif element is None: + tag_value_element = None + else: + continue + + if defined_max_tag_value_length: + if tag_value_element is None: + running_string_length += 4 # null with no quotes + else: + # + 2 accounts for string quotation marks + running_string_length += len(tag_value_element) + 2 + + if tag_value_elements: + # accounts for ',' item separator + running_string_length += 1 + + if running_string_length > self.max_tag_value_length: + break + + tag_value_elements.append(tag_value_element) + + return json.dumps(tag_value_elements, separators=(",", ":")) + + def _extract_tags_from_span(self, span: Span) -> Dict[str, str]: + tags = self._extract_tags_from_dict(span.attributes) + if span.resource: + tags.update(self._extract_tags_from_dict(span.resource.attributes)) + if span.instrumentation_info is not None: + tags.update( + { + NAME_KEY: span.instrumentation_info.name, + VERSION_KEY: span.instrumentation_info.version, + } + ) + if span.status.status_code is not StatusCode.UNSET: + tags.update({"otel.status_code": span.status.status_code.name}) + if span.status.status_code is StatusCode.ERROR: + tags.update({"error": span.status.description or ""}) + return tags + + def _extract_annotations_from_events( + self, events: Optional[List[Event]] + ) -> Optional[List[Dict]]: + if not events: + return None + + annotations = [] + for event in events: + attrs = {} + for key, value in event.attributes.items(): + if isinstance(value, str) and self.max_tag_value_length > 0: + value = value[: self.max_tag_value_length] + attrs[key] = value + + annotations.append( + { + "timestamp": self._nsec_to_usec_round(event.timestamp), + "value": json.dumps({event.name: attrs}, sort_keys=True), + } + ) + return annotations + + @staticmethod + def _nsec_to_usec_round(nsec: int) -> int: + """Round nanoseconds to microseconds + + Timestamp in zipkin spans is int of microseconds. + See: https://zipkin.io/pages/instrumenting.html + """ + return (nsec + 500) // 10 ** 3 + + +class JsonEncoder(Encoder): + @staticmethod + def content_type(): + return "application/json" + + def serialize( + self, spans: Sequence[Span], local_endpoint: NodeEndpoint + ) -> str: + encoded_local_endpoint = self._encode_local_endpoint(local_endpoint) + encoded_spans = [] + for span in spans: + encoded_spans.append( + self._encode_span(span, encoded_local_endpoint) + ) + return json.dumps(encoded_spans) + + @staticmethod + def _encode_local_endpoint(local_endpoint: NodeEndpoint) -> Dict: + encoded_local_endpoint = {"serviceName": local_endpoint.service_name} + if local_endpoint.ipv4 is not None: + encoded_local_endpoint["ipv4"] = str(local_endpoint.ipv4) + if local_endpoint.ipv6 is not None: + encoded_local_endpoint["ipv6"] = str(local_endpoint.ipv6) + if local_endpoint.port is not None: + encoded_local_endpoint["port"] = local_endpoint.port + return encoded_local_endpoint + + @staticmethod + def _encode_span_id(span_id: int) -> str: + return format(span_id, "016x") + + @staticmethod + def _encode_trace_id(trace_id: int) -> str: + return format(trace_id, "032x") diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/__init__.py new file mode 100644 index 0000000000..b892a67bef --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/__init__.py @@ -0,0 +1,41 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Zipkin Export Encoders for JSON formats +""" + +import abc +from typing import Dict, List + +from opentelemetry.exporter.zipkin.encoder import Encoder +from opentelemetry.trace import Span, SpanContext, SpanKind + + +# pylint: disable=W0223 +class V1Encoder(Encoder): + def _extract_binary_annotations( + self, span: Span, encoded_local_endpoint: Dict + ) -> List[Dict]: + binary_annotations = [] + for tag_key, tag_value in self._extract_tags_from_span(span).items(): + if isinstance(tag_value, str) and self.max_tag_value_length > 0: + tag_value = tag_value[: self.max_tag_value_length] + binary_annotations.append( + { + "key": tag_key, + "value": tag_value, + "endpoint": encoded_local_endpoint, + } + ) + return binary_annotations diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/json.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/json.py new file mode 100644 index 0000000000..966657311b --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/json.py @@ -0,0 +1,65 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Zipkin Export Encoders for JSON formats +""" +from typing import Dict + +from opentelemetry.exporter.zipkin.encoder import JsonEncoder +from opentelemetry.exporter.zipkin.encoder.v1 import V1Encoder +from opentelemetry.trace import Span + + +class JsonV1Encoder(JsonEncoder, V1Encoder): + """Zipkin Export Encoder for JSON v1 API + + API spec: https://github.com/openzipkin/zipkin-api/blob/master/zipkin-api.yaml + """ + + def _encode_span(self, span: Span, encoded_local_endpoint: Dict) -> Dict: + context = span.get_span_context() + + encoded_span = { + "traceId": self._encode_trace_id(context.trace_id), + "id": self._encode_span_id(context.span_id), + "name": span.name, + "timestamp": self._nsec_to_usec_round(span.start_time), + "duration": self._nsec_to_usec_round( + span.end_time - span.start_time + ), + } + + encoded_annotations = self._extract_annotations_from_events( + span.events + ) + if encoded_annotations is not None: + for annotation in encoded_annotations: + annotation["endpoint"] = encoded_local_endpoint + encoded_span["annotations"] = encoded_annotations + + binary_annotations = self._extract_binary_annotations( + span, encoded_local_endpoint + ) + if binary_annotations: + encoded_span["binaryAnnotations"] = binary_annotations + + debug = self._encode_debug(context) + if debug: + encoded_span["debug"] = debug + + parent_id = self._get_parent_id(span.parent) + if parent_id is not None: + encoded_span["parentId"] = self._encode_span_id(parent_id) + + return encoded_span diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/json.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/json.py new file mode 100644 index 0000000000..ec6e53382b --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/json.py @@ -0,0 +1,67 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Zipkin Export Encoders for JSON formats +""" +from typing import Dict + +from opentelemetry.exporter.zipkin.encoder import JsonEncoder +from opentelemetry.trace import Span, SpanKind + + +class JsonV2Encoder(JsonEncoder): + """Zipkin Export Encoder for JSON v2 API + + API spec: https://github.com/openzipkin/zipkin-api/blob/master/zipkin2-api.yaml + """ + + SPAN_KIND_MAP = { + SpanKind.INTERNAL: None, + SpanKind.SERVER: "SERVER", + SpanKind.CLIENT: "CLIENT", + SpanKind.PRODUCER: "PRODUCER", + SpanKind.CONSUMER: "CONSUMER", + } + + def _encode_span(self, span: Span, encoded_local_endpoint: Dict) -> Dict: + context = span.get_span_context() + encoded_span = { + "traceId": self._encode_trace_id(context.trace_id), + "id": self._encode_span_id(context.span_id), + "name": span.name, + "timestamp": self._nsec_to_usec_round(span.start_time), + "duration": self._nsec_to_usec_round( + span.end_time - span.start_time + ), + "localEndpoint": encoded_local_endpoint, + "kind": self.SPAN_KIND_MAP[span.kind], + } + + tags = self._extract_tags_from_span(span) + if tags: + encoded_span["tags"] = tags + + annotations = self._extract_annotations_from_events(span.events) + if annotations: + encoded_span["annotations"] = annotations + + debug = self._encode_debug(context) + if debug: + encoded_span["debug"] = debug + + parent_id = self._get_parent_id(span.parent) + if parent_id is not None: + encoded_span["parentId"] = self._encode_span_id(parent_id) + + return encoded_span diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/__init__.py new file mode 100644 index 0000000000..f0cdb65b37 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/__init__.py @@ -0,0 +1,129 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Zipkin Export Encoder for Protobuf + +API spec: https://github.com/openzipkin/zipkin-api/blob/master/zipkin.proto +""" +from typing import List, Optional, Sequence + +from opentelemetry.exporter.zipkin.encoder import Encoder +from opentelemetry.exporter.zipkin.encoder.v2.protobuf.gen import zipkin_pb2 +from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint +from opentelemetry.sdk.trace import Event +from opentelemetry.trace import Span, SpanContext, SpanKind + + +class ProtobufEncoder(Encoder): + """Zipkin Export Encoder for Protobuf + + API spec: https://github.com/openzipkin/zipkin-api/blob/master/zipkin.proto + """ + + SPAN_KIND_MAP = { + SpanKind.INTERNAL: zipkin_pb2.Span.Kind.SPAN_KIND_UNSPECIFIED, + SpanKind.SERVER: zipkin_pb2.Span.Kind.SERVER, + SpanKind.CLIENT: zipkin_pb2.Span.Kind.CLIENT, + SpanKind.PRODUCER: zipkin_pb2.Span.Kind.PRODUCER, + SpanKind.CONSUMER: zipkin_pb2.Span.Kind.CONSUMER, + } + + @staticmethod + def content_type(): + return "application/x-protobuf" + + def serialize( + self, spans: Sequence[Span], local_endpoint: NodeEndpoint + ) -> str: + encoded_local_endpoint = self._encode_local_endpoint(local_endpoint) + # pylint: disable=no-member + encoded_spans = zipkin_pb2.ListOfSpans() + for span in spans: + encoded_spans.spans.append( + self._encode_span(span, encoded_local_endpoint) + ) + return encoded_spans.SerializeToString() + + def _encode_span( + self, span: Span, encoded_local_endpoint: zipkin_pb2.Endpoint + ) -> zipkin_pb2.Span: + context = span.get_span_context() + # pylint: disable=no-member + encoded_span = zipkin_pb2.Span( + trace_id=self._encode_trace_id(context.trace_id), + id=self._encode_span_id(context.span_id), + name=span.name, + timestamp=self._nsec_to_usec_round(span.start_time), + duration=self._nsec_to_usec_round(span.end_time - span.start_time), + local_endpoint=encoded_local_endpoint, + kind=self.SPAN_KIND_MAP[span.kind], + ) + + tags = self._extract_tags_from_span(span) + if tags: + encoded_span.tags.update(tags) + + annotations = self._encode_annotations(span.events) + if annotations: + encoded_span.annotations.extend(annotations) + + debug = self._encode_debug(context) + if debug: + encoded_span.debug = debug + + parent_id = self._get_parent_id(span.parent) + if parent_id is not None: + encoded_span.parent_id = self._encode_span_id(parent_id) + + return encoded_span + + def _encode_annotations( + self, span_events: Optional[List[Event]] + ) -> Optional[List]: + annotations = self._extract_annotations_from_events(span_events) + if annotations is None: + encoded_annotations = None + else: + encoded_annotations = [] + for annotation in annotations: + encoded_annotations.append( + zipkin_pb2.Annotation( + timestamp=annotation["timestamp"], + value=annotation["value"], + ) + ) + return encoded_annotations + + @staticmethod + def _encode_local_endpoint( + local_endpoint: NodeEndpoint, + ) -> zipkin_pb2.Endpoint: + encoded_local_endpoint = zipkin_pb2.Endpoint( + service_name=local_endpoint.service_name, + ) + if local_endpoint.ipv4 is not None: + encoded_local_endpoint.ipv4 = local_endpoint.ipv4.packed + if local_endpoint.ipv6 is not None: + encoded_local_endpoint.ipv6 = local_endpoint.ipv6.packed + if local_endpoint.port is not None: + encoded_local_endpoint.port = local_endpoint.port + return encoded_local_endpoint + + @staticmethod + def _encode_span_id(span_id: int) -> bytes: + return span_id.to_bytes(length=8, byteorder="big", signed=False) + + @staticmethod + def _encode_trace_id(trace_id: int) -> bytes: + return trace_id.to_bytes(length=16, byteorder="big", signed=False) diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/zipkin_pb2.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/zipkin_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/zipkin_pb2.py rename to exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/zipkin_pb2.py diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/zipkin_pb2.pyi b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/zipkin_pb2.pyi similarity index 100% rename from exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen/zipkin_pb2.pyi rename to exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/zipkin_pb2.pyi diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/node_endpoint.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/node_endpoint.py new file mode 100644 index 0000000000..ee7cb71f02 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/node_endpoint.py @@ -0,0 +1,85 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Zipkin Exporter Endpoints""" + +import ipaddress +from typing import Optional, Union + +from opentelemetry import trace +from opentelemetry.sdk.resources import SERVICE_NAME, Resource + +IpInput = Union[str, int, None] + + +class NodeEndpoint: + """The network context of a node in the service graph. + + Args: + ipv4: Primary IPv4 address associated with this connection. + ipv6: Primary IPv6 address associated with this connection. + port: Depending on context, this could be a listen port or the + client-side of a socket. None if unknown. + """ + + def __init__( + self, + ipv4: IpInput = None, + ipv6: IpInput = None, + port: Optional[int] = None, + ): + self.ipv4 = ipv4 + self.ipv6 = ipv6 + self.port = port + + tracer_provider = trace.get_tracer_provider() + + if hasattr(tracer_provider, "resource"): + resource = tracer_provider.resource + else: + resource = Resource.create() + + self.service_name = resource.attributes[SERVICE_NAME] + + @property + def ipv4(self) -> Optional[ipaddress.IPv4Address]: + return self._ipv4 + + @ipv4.setter + def ipv4(self, address: IpInput) -> None: + if address is None: + self._ipv4 = None + else: + ipv4_address = ipaddress.ip_address(address) + if not isinstance(ipv4_address, ipaddress.IPv4Address): + raise ValueError( + "%r does not appear to be an IPv4 address" % address + ) + self._ipv4 = ipv4_address + + @property + def ipv6(self) -> Optional[ipaddress.IPv6Address]: + return self._ipv6 + + @ipv6.setter + def ipv6(self, address: IpInput) -> None: + if address is None: + self._ipv6 = None + else: + ipv6_address = ipaddress.ip_address(address) + if not isinstance(ipv6_address, ipaddress.IPv6Address): + raise ValueError( + "%r does not appear to be an IPv6 address" % address + ) + self._ipv6 = ipv6_address diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py new file mode 100644 index 0000000000..a04148b6ea --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py @@ -0,0 +1,505 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import abc +import json +import sys +import unittest +from typing import Dict, List + +from opentelemetry import trace as trace_api +from opentelemetry.exporter.zipkin.encoder import ( + DEFAULT_MAX_TAG_VALUE_LENGTH, + Encoder, +) +from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint +from opentelemetry.sdk import trace +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.trace import TraceFlags +from opentelemetry.trace.status import Status, StatusCode + +TEST_SERVICE_NAME = "test_service" + + +# pylint: disable=protected-access +class CommonEncoderTestCases: + class CommonEncoderTest(unittest.TestCase): + @staticmethod + @abc.abstractmethod + def get_encoder(*args, **kwargs) -> Encoder: + pass + + @classmethod + def get_encoder_default(cls) -> Encoder: + return cls.get_encoder() + + @abc.abstractmethod + def test_encode_trace_id(self): + pass + + @abc.abstractmethod + def test_encode_span_id(self): + pass + + @abc.abstractmethod + def test_encode_local_endpoint_default(self): + pass + + @abc.abstractmethod + def test_encode_local_endpoint_explicits(self): + pass + + @abc.abstractmethod + def _test_encode_max_tag_length(self, max_tag_value_length: int): + pass + + def test_encode_max_tag_length_2(self): + self._test_encode_max_tag_length(2) + + def test_encode_max_tag_length_5(self): + self._test_encode_max_tag_length(5) + + def test_encode_max_tag_length_9(self): + self._test_encode_max_tag_length(9) + + def test_encode_max_tag_length_10(self): + self._test_encode_max_tag_length(10) + + def test_encode_max_tag_length_11(self): + self._test_encode_max_tag_length(11) + + def test_encode_max_tag_length_128(self): + self._test_encode_max_tag_length(128) + + def test_constructor_default(self): + encoder = self.get_encoder() + + self.assertEqual( + DEFAULT_MAX_TAG_VALUE_LENGTH, encoder.max_tag_value_length + ) + + def test_constructor_max_tag_value_length(self): + max_tag_value_length = 123456 + encoder = self.get_encoder(max_tag_value_length) + self.assertEqual( + max_tag_value_length, encoder.max_tag_value_length + ) + + def test_nsec_to_usec_round(self): + base_time_nsec = 683647322 * 10 ** 9 + for nsec in ( + base_time_nsec, + base_time_nsec + 150 * 10 ** 6, + base_time_nsec + 300 * 10 ** 6, + base_time_nsec + 400 * 10 ** 6, + ): + self.assertEqual( + (nsec + 500) // 10 ** 3, + self.get_encoder_default()._nsec_to_usec_round(nsec), + ) + + def test_encode_debug(self): + self.assertFalse( + self.get_encoder_default()._encode_debug( + trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.DEFAULT), + ) + ) + ) + self.assertTrue( + self.get_encoder_default()._encode_debug( + trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + ) + ) + + def test_get_parent_id_from_span(self): + parent_id = 0x00000000DEADBEF0 + self.assertEqual( + parent_id, + self.get_encoder_default()._get_parent_id( + trace._Span( + name="test-span", + context=trace_api.SpanContext( + 0x000000000000000000000000DEADBEEF, + 0x04BF92DEEFC58C92, + is_remote=False, + ), + parent=trace_api.SpanContext( + 0x0000000000000000000000AADEADBEEF, + parent_id, + is_remote=False, + ), + ) + ), + ) + + def test_get_parent_id_from_span_context(self): + parent_id = 0x00000000DEADBEF0 + self.assertEqual( + parent_id, + self.get_encoder_default()._get_parent_id( + trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=parent_id, + is_remote=False, + ), + ), + ) + + @staticmethod + def get_data_for_max_tag_length_test( + max_tag_length: int, + ) -> (trace._Span, Dict): + start_time = 683647322 * 10 ** 9 # in ns + duration = 50 * 10 ** 6 + end_time = start_time + duration + + span = trace._Span( + name=TEST_SERVICE_NAME, + context=trace_api.SpanContext( + 0x0E0C63257DE34C926F9EFCD03927272E, + 0x04BF92DEEFC58C92, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ), + resource=trace.Resource({}), + ) + span.start(start_time=start_time) + span.set_attribute("string1", "v" * 500) + span.set_attribute("string2", "v" * 50) + span.set_attribute("list1", ["a"] * 25) + span.set_attribute("list2", ["a"] * 10) + span.set_attribute("list3", [2] * 25) + span.set_attribute("list4", [2] * 10) + span.set_attribute("list5", [True] * 25) + span.set_attribute("list6", [True] * 10) + span.set_attribute("tuple1", ("a",) * 25) + span.set_attribute("tuple2", ("a",) * 10) + span.set_attribute("tuple3", (2,) * 25) + span.set_attribute("tuple4", (2,) * 10) + span.set_attribute("tuple5", (True,) * 25) + span.set_attribute("tuple6", (True,) * 10) + span.set_attribute("range1", range(0, 25)) + span.set_attribute("range2", range(0, 10)) + span.set_attribute("empty_list", []) + span.set_attribute("none_list", ["hello", None, "world"]) + span.end(end_time=end_time) + + expected_outputs = { + 2: { + "string1": "vv", + "string2": "vv", + "list1": "[]", + "list2": "[]", + "list3": "[]", + "list4": "[]", + "list5": "[]", + "list6": "[]", + "tuple1": "[]", + "tuple2": "[]", + "tuple3": "[]", + "tuple4": "[]", + "tuple5": "[]", + "tuple6": "[]", + "range1": "[]", + "range2": "[]", + "empty_list": "[]", + "none_list": "[]", + }, + 5: { + "string1": "vvvvv", + "string2": "vvvvv", + "list1": '["a"]', + "list2": '["a"]', + "list3": '["2"]', + "list4": '["2"]', + "list5": "[]", + "list6": "[]", + "tuple1": '["a"]', + "tuple2": '["a"]', + "tuple3": '["2"]', + "tuple4": '["2"]', + "tuple5": "[]", + "tuple6": "[]", + "range1": '["0"]', + "range2": '["0"]', + "empty_list": "[]", + "none_list": "[]", + }, + 9: { + "string1": "vvvvvvvvv", + "string2": "vvvvvvvvv", + "list1": '["a","a"]', + "list2": '["a","a"]', + "list3": '["2","2"]', + "list4": '["2","2"]', + "list5": '["true"]', + "list6": '["true"]', + "tuple1": '["a","a"]', + "tuple2": '["a","a"]', + "tuple3": '["2","2"]', + "tuple4": '["2","2"]', + "tuple5": '["true"]', + "tuple6": '["true"]', + "range1": '["0","1"]', + "range2": '["0","1"]', + "empty_list": "[]", + "none_list": '["hello"]', + }, + 10: { + "string1": "vvvvvvvvvv", + "string2": "vvvvvvvvvv", + "list1": '["a","a"]', + "list2": '["a","a"]', + "list3": '["2","2"]', + "list4": '["2","2"]', + "list5": '["true"]', + "list6": '["true"]', + "tuple1": '["a","a"]', + "tuple2": '["a","a"]', + "tuple3": '["2","2"]', + "tuple4": '["2","2"]', + "tuple5": '["true"]', + "tuple6": '["true"]', + "range1": '["0","1"]', + "range2": '["0","1"]', + "empty_list": "[]", + "none_list": '["hello"]', + }, + 11: { + "string1": "vvvvvvvvvvv", + "string2": "vvvvvvvvvvv", + "list1": '["a","a"]', + "list2": '["a","a"]', + "list3": '["2","2"]', + "list4": '["2","2"]', + "list5": '["true"]', + "list6": '["true"]', + "tuple1": '["a","a"]', + "tuple2": '["a","a"]', + "tuple3": '["2","2"]', + "tuple4": '["2","2"]', + "tuple5": '["true"]', + "tuple6": '["true"]', + "range1": '["0","1"]', + "range2": '["0","1"]', + "empty_list": "[]", + "none_list": '["hello"]', + }, + 128: { + "string1": "v" * 128, + "string2": "v" * 50, + "list1": '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', + "list2": '["a","a","a","a","a","a","a","a","a","a"]', + "list3": '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', + "list4": '["2","2","2","2","2","2","2","2","2","2"]', + "list5": '["true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true"]', + "list6": '["true","true","true","true","true","true","true","true","true","true"]', + "tuple1": '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', + "tuple2": '["a","a","a","a","a","a","a","a","a","a"]', + "tuple3": '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', + "tuple4": '["2","2","2","2","2","2","2","2","2","2"]', + "tuple5": '["true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true"]', + "tuple6": '["true","true","true","true","true","true","true","true","true","true"]', + "range1": '["0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24"]', + "range2": '["0","1","2","3","4","5","6","7","8","9"]', + "empty_list": "[]", + "none_list": '["hello",null,"world"]', + }, + } + + return span, expected_outputs[max_tag_length] + + @staticmethod + def get_exhaustive_otel_span_list() -> List[trace._Span]: + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + + base_time = 683647322 * 10 ** 9 # in ns + start_times = ( + base_time, + base_time + 150 * 10 ** 6, + base_time + 300 * 10 ** 6, + base_time + 400 * 10 ** 6, + ) + end_times = ( + start_times[0] + (50 * 10 ** 6), + start_times[1] + (100 * 10 ** 6), + start_times[2] + (200 * 10 ** 6), + start_times[3] + (300 * 10 ** 6), + ) + + parent_span_context = trace_api.SpanContext( + trace_id, 0x1111111111111111, is_remote=False + ) + + other_context = trace_api.SpanContext( + trace_id, 0x2222222222222222, is_remote=False + ) + + span1 = trace._Span( + name="test-span-1", + context=trace_api.SpanContext( + trace_id, + 0x34BF92DEEFC58C92, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ), + parent=parent_span_context, + events=( + trace.Event( + name="event0", + timestamp=base_time + 50 * 10 ** 6, + attributes={ + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + }, + ), + ), + links=( + trace_api.Link( + context=other_context, attributes={"key_bool": True} + ), + ), + resource=trace.Resource({}), + ) + span1.start(start_time=start_times[0]) + span1.set_attribute("key_bool", False) + span1.set_attribute("key_string", "hello_world") + span1.set_attribute("key_float", 111.22) + span1.set_status(Status(StatusCode.OK)) + span1.end(end_time=end_times[0]) + + span2 = trace._Span( + name="test-span-2", + context=parent_span_context, + parent=None, + resource=trace.Resource( + attributes={"key_resource": "some_resource"} + ), + ) + span2.start(start_time=start_times[1]) + span2.set_status(Status(StatusCode.ERROR, "Example description")) + span2.end(end_time=end_times[1]) + + span3 = trace._Span( + name="test-span-3", + context=other_context, + parent=None, + resource=trace.Resource( + attributes={"key_resource": "some_resource"} + ), + ) + span3.start(start_time=start_times[2]) + span3.set_attribute("key_string", "hello_world") + span3.end(end_time=end_times[2]) + + span4 = trace._Span( + name="test-span-3", + context=other_context, + parent=None, + resource=trace.Resource({}), + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), + ) + span4.start(start_time=start_times[3]) + span4.end(end_time=end_times[3]) + + return [span1, span2, span3, span4] + + # pylint: disable=W0223 + class CommonJsonEncoderTest(CommonEncoderTest, abc.ABC): + def test_encode_trace_id(self): + for trace_id in (1, 1024, 2 ** 32, 2 ** 64, 2 ** 65): + self.assertEqual( + format(trace_id, "032x"), + self.get_encoder_default()._encode_trace_id(trace_id), + ) + + def test_encode_span_id(self): + for span_id in (1, 1024, 2 ** 8, 2 ** 16, 2 ** 32, 2 ** 64): + self.assertEqual( + format(span_id, "016x"), + self.get_encoder_default()._encode_span_id(span_id), + ) + + def test_encode_local_endpoint_default(self): + self.assertEqual( + self.get_encoder_default()._encode_local_endpoint( + NodeEndpoint() + ), + {"serviceName": TEST_SERVICE_NAME}, + ) + + def test_encode_local_endpoint_explicits(self): + ipv4 = "192.168.0.1" + ipv6 = "2001:db8::c001" + port = 414120 + self.assertEqual( + self.get_encoder_default()._encode_local_endpoint( + NodeEndpoint(ipv4, ipv6, port) + ), + { + "serviceName": TEST_SERVICE_NAME, + "ipv4": ipv4, + "ipv6": ipv6, + "port": port, + }, + ) + + @staticmethod + def pop_and_sort(source_list, source_index, sort_key): + """ + Convenience method that will pop a specified index from a list, + sort it by a given key and then return it. + """ + popped_item = source_list.pop(source_index, None) + if popped_item is not None: + popped_item = sorted(popped_item, key=lambda x: x[sort_key]) + return popped_item + + def assert_equal_encoded_spans(self, expected_spans, actual_spans): + if sys.version_info.major == 3 and sys.version_info.minor <= 5: + expected_spans = json.loads(expected_spans) + actual_spans = json.loads(actual_spans) + for expected_span, actual_span in zip( + expected_spans, actual_spans + ): + actual_annotations = self.pop_and_sort( + actual_span, "annotations", "timestamp" + ) + expected_annotations = self.pop_and_sort( + expected_span, "annotations", "timestamp" + ) + expected_binary_annotations = self.pop_and_sort( + expected_span, "binaryAnnotations", "key" + ) + actual_binary_annotations = self.pop_and_sort( + actual_span, "binaryAnnotations", "key" + ) + self.assertEqual(actual_span, expected_span) + self.assertEqual(actual_annotations, expected_annotations) + self.assertEqual( + actual_binary_annotations, expected_binary_annotations + ) + else: + self.assertEqual(expected_spans, actual_spans) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py new file mode 100644 index 0000000000..13bc24e6cd --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py @@ -0,0 +1,251 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json + +from opentelemetry import trace as trace_api +from opentelemetry.exporter.zipkin.encoder import NAME_KEY, VERSION_KEY +from opentelemetry.exporter.zipkin.encoder.v1.json import JsonV1Encoder +from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint +from opentelemetry.sdk import trace +from opentelemetry.trace import TraceFlags + +from .common_tests import TEST_SERVICE_NAME, CommonEncoderTestCases + + +# pylint: disable=protected-access +class TestV1JsonEncoder(CommonEncoderTestCases.CommonJsonEncoderTest): + @staticmethod + def get_encoder(*args, **kwargs) -> JsonV1Encoder: + return JsonV1Encoder(*args, **kwargs) + + def test_encode(self): + + local_endpoint = {"serviceName": TEST_SERVICE_NAME} + + otel_spans = self.get_exhaustive_otel_span_list() + trace_id = JsonV1Encoder._encode_trace_id( + otel_spans[0].context.trace_id + ) + + expected_output = [ + { + "traceId": trace_id, + "id": JsonV1Encoder._encode_span_id( + otel_spans[0].context.span_id + ), + "name": otel_spans[0].name, + "timestamp": otel_spans[0].start_time // 10 ** 3, + "duration": (otel_spans[0].end_time // 10 ** 3) + - (otel_spans[0].start_time // 10 ** 3), + "annotations": [ + { + "timestamp": otel_spans[0].events[0].timestamp + // 10 ** 3, + "value": json.dumps( + { + "event0": { + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + } + }, + sort_keys=True, + ), + "endpoint": local_endpoint, + } + ], + "binaryAnnotations": [ + { + "key": "key_bool", + "value": "false", + "endpoint": local_endpoint, + }, + { + "key": "key_string", + "value": "hello_world", + "endpoint": local_endpoint, + }, + { + "key": "key_float", + "value": "111.22", + "endpoint": local_endpoint, + }, + { + "key": "otel.status_code", + "value": "OK", + "endpoint": local_endpoint, + }, + ], + "debug": True, + "parentId": JsonV1Encoder._encode_span_id( + otel_spans[0].parent.span_id + ), + }, + { + "traceId": trace_id, + "id": JsonV1Encoder._encode_span_id( + otel_spans[1].context.span_id + ), + "name": otel_spans[1].name, + "timestamp": otel_spans[1].start_time // 10 ** 3, + "duration": (otel_spans[1].end_time // 10 ** 3) + - (otel_spans[1].start_time // 10 ** 3), + "binaryAnnotations": [ + { + "key": "key_resource", + "value": "some_resource", + "endpoint": local_endpoint, + }, + { + "key": "otel.status_code", + "value": "ERROR", + "endpoint": local_endpoint, + }, + { + "key": "error", + "value": "Example description", + "endpoint": local_endpoint, + }, + ], + }, + { + "traceId": trace_id, + "id": JsonV1Encoder._encode_span_id( + otel_spans[2].context.span_id + ), + "name": otel_spans[2].name, + "timestamp": otel_spans[2].start_time // 10 ** 3, + "duration": (otel_spans[2].end_time // 10 ** 3) + - (otel_spans[2].start_time // 10 ** 3), + "binaryAnnotations": [ + { + "key": "key_string", + "value": "hello_world", + "endpoint": local_endpoint, + }, + { + "key": "key_resource", + "value": "some_resource", + "endpoint": local_endpoint, + }, + ], + }, + { + "traceId": trace_id, + "id": JsonV1Encoder._encode_span_id( + otel_spans[3].context.span_id + ), + "name": otel_spans[3].name, + "timestamp": otel_spans[3].start_time // 10 ** 3, + "duration": (otel_spans[3].end_time // 10 ** 3) + - (otel_spans[3].start_time // 10 ** 3), + "binaryAnnotations": [ + { + "key": NAME_KEY, + "value": "name", + "endpoint": local_endpoint, + }, + { + "key": VERSION_KEY, + "value": "version", + "endpoint": local_endpoint, + }, + ], + }, + ] + + self.assert_equal_encoded_spans( + json.dumps(expected_output), + JsonV1Encoder().serialize(otel_spans, NodeEndpoint()), + ) + + def test_encode_id_zero_padding(self): + trace_id = 0x0E0C63257DE34C926F9EFCD03927272E + span_id = 0x04BF92DEEFC58C92 + parent_id = 0x0AAAAAAAAAAAAAAA + start_time = 683647322 * 10 ** 9 # in ns + duration = 50 * 10 ** 6 + end_time = start_time + duration + + otel_span = trace._Span( + name=TEST_SERVICE_NAME, + context=trace_api.SpanContext( + trace_id, + span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ), + parent=trace_api.SpanContext(trace_id, parent_id, is_remote=False), + resource=trace.Resource({}), + ) + otel_span.start(start_time=start_time) + otel_span.end(end_time=end_time) + + expected_output = [ + { + "traceId": format(trace_id, "032x"), + "id": format(span_id, "016x"), + "name": TEST_SERVICE_NAME, + "timestamp": JsonV1Encoder._nsec_to_usec_round(start_time), + "duration": JsonV1Encoder._nsec_to_usec_round(duration), + "debug": True, + "parentId": format(parent_id, "016x"), + } + ] + + self.assertEqual( + json.dumps(expected_output), + JsonV1Encoder().serialize([otel_span], NodeEndpoint()), + ) + + def _test_encode_max_tag_length(self, max_tag_value_length: int): + otel_span, expected_tag_output = self.get_data_for_max_tag_length_test( + max_tag_value_length + ) + service_name = otel_span.name + + binary_annotations = [] + for tag_key, tag_expected_value in expected_tag_output.items(): + binary_annotations.append( + { + "key": tag_key, + "value": tag_expected_value, + "endpoint": {"serviceName": service_name}, + } + ) + + expected_output = [ + { + "traceId": JsonV1Encoder._encode_trace_id( + otel_span.context.trace_id + ), + "id": JsonV1Encoder._encode_span_id(otel_span.context.span_id), + "name": service_name, + "timestamp": JsonV1Encoder._nsec_to_usec_round( + otel_span.start_time + ), + "duration": JsonV1Encoder._nsec_to_usec_round( + otel_span.end_time - otel_span.start_time + ), + "binaryAnnotations": binary_annotations, + "debug": True, + } + ] + + self.assert_equal_encoded_spans( + json.dumps(expected_output), + JsonV1Encoder(max_tag_value_length).serialize( + [otel_span], NodeEndpoint() + ), + ) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_json.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_json.py new file mode 100644 index 0000000000..369eef6895 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_json.py @@ -0,0 +1,205 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json + +from opentelemetry import trace as trace_api +from opentelemetry.exporter.zipkin.encoder import NAME_KEY, VERSION_KEY +from opentelemetry.exporter.zipkin.encoder.v2.json import JsonV2Encoder +from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint +from opentelemetry.sdk import trace +from opentelemetry.trace import SpanKind, TraceFlags + +from .common_tests import TEST_SERVICE_NAME, CommonEncoderTestCases + + +# pylint: disable=protected-access +class TestV2JsonEncoder(CommonEncoderTestCases.CommonJsonEncoderTest): + @staticmethod + def get_encoder(*args, **kwargs) -> JsonV2Encoder: + return JsonV2Encoder(*args, **kwargs) + + def test_encode(self): + local_endpoint = {"serviceName": TEST_SERVICE_NAME} + span_kind = JsonV2Encoder.SPAN_KIND_MAP[SpanKind.INTERNAL] + + otel_spans = self.get_exhaustive_otel_span_list() + trace_id = JsonV2Encoder._encode_trace_id( + otel_spans[0].context.trace_id + ) + + expected_output = [ + { + "traceId": trace_id, + "id": JsonV2Encoder._encode_span_id( + otel_spans[0].context.span_id + ), + "name": otel_spans[0].name, + "timestamp": otel_spans[0].start_time // 10 ** 3, + "duration": (otel_spans[0].end_time // 10 ** 3) + - (otel_spans[0].start_time // 10 ** 3), + "localEndpoint": local_endpoint, + "kind": span_kind, + "tags": { + "key_bool": "false", + "key_string": "hello_world", + "key_float": "111.22", + "otel.status_code": "OK", + }, + "annotations": [ + { + "timestamp": otel_spans[0].events[0].timestamp + // 10 ** 3, + "value": json.dumps( + { + "event0": { + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + } + }, + sort_keys=True, + ), + } + ], + "debug": True, + "parentId": JsonV2Encoder._encode_span_id( + otel_spans[0].parent.span_id + ), + }, + { + "traceId": trace_id, + "id": JsonV2Encoder._encode_span_id( + otel_spans[1].context.span_id + ), + "name": otel_spans[1].name, + "timestamp": otel_spans[1].start_time // 10 ** 3, + "duration": (otel_spans[1].end_time // 10 ** 3) + - (otel_spans[1].start_time // 10 ** 3), + "localEndpoint": local_endpoint, + "kind": span_kind, + "tags": { + "key_resource": "some_resource", + "otel.status_code": "ERROR", + "error": "Example description", + }, + }, + { + "traceId": trace_id, + "id": JsonV2Encoder._encode_span_id( + otel_spans[2].context.span_id + ), + "name": otel_spans[2].name, + "timestamp": otel_spans[2].start_time // 10 ** 3, + "duration": (otel_spans[2].end_time // 10 ** 3) + - (otel_spans[2].start_time // 10 ** 3), + "localEndpoint": local_endpoint, + "kind": span_kind, + "tags": { + "key_string": "hello_world", + "key_resource": "some_resource", + }, + }, + { + "traceId": trace_id, + "id": JsonV2Encoder._encode_span_id( + otel_spans[3].context.span_id + ), + "name": otel_spans[3].name, + "timestamp": otel_spans[3].start_time // 10 ** 3, + "duration": (otel_spans[3].end_time // 10 ** 3) + - (otel_spans[3].start_time // 10 ** 3), + "localEndpoint": local_endpoint, + "kind": span_kind, + "tags": {NAME_KEY: "name", VERSION_KEY: "version"}, + }, + ] + + self.assert_equal_encoded_spans( + json.dumps(expected_output), + JsonV2Encoder().serialize(otel_spans, NodeEndpoint()), + ) + + def test_encode_id_zero_padding(self): + trace_id = 0x0E0C63257DE34C926F9EFCD03927272E + span_id = 0x04BF92DEEFC58C92 + parent_id = 0x0AAAAAAAAAAAAAAA + start_time = 683647322 * 10 ** 9 # in ns + duration = 50 * 10 ** 6 + end_time = start_time + duration + + otel_span = trace._Span( + name=TEST_SERVICE_NAME, + context=trace_api.SpanContext( + trace_id, + span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ), + parent=trace_api.SpanContext(trace_id, parent_id, is_remote=False), + resource=trace.Resource({}), + ) + otel_span.start(start_time=start_time) + otel_span.end(end_time=end_time) + + expected_output = [ + { + "traceId": format(trace_id, "032x"), + "id": format(span_id, "016x"), + "name": TEST_SERVICE_NAME, + "timestamp": JsonV2Encoder._nsec_to_usec_round(start_time), + "duration": JsonV2Encoder._nsec_to_usec_round(duration), + "localEndpoint": {"serviceName": TEST_SERVICE_NAME}, + "kind": JsonV2Encoder.SPAN_KIND_MAP[SpanKind.INTERNAL], + "debug": True, + "parentId": format(parent_id, "016x"), + } + ] + + self.assert_equal_encoded_spans( + json.dumps(expected_output), + JsonV2Encoder().serialize([otel_span], NodeEndpoint()), + ) + + def _test_encode_max_tag_length(self, max_tag_value_length: int): + otel_span, expected_tag_output = self.get_data_for_max_tag_length_test( + max_tag_value_length + ) + service_name = otel_span.name + + expected_output = [ + { + "traceId": JsonV2Encoder._encode_trace_id( + otel_span.context.trace_id + ), + "id": JsonV2Encoder._encode_span_id(otel_span.context.span_id), + "name": service_name, + "timestamp": JsonV2Encoder._nsec_to_usec_round( + otel_span.start_time + ), + "duration": JsonV2Encoder._nsec_to_usec_round( + otel_span.end_time - otel_span.start_time + ), + "localEndpoint": {"serviceName": service_name}, + "kind": JsonV2Encoder.SPAN_KIND_MAP[SpanKind.INTERNAL], + "tags": expected_tag_output, + "debug": True, + } + ] + + self.assert_equal_encoded_spans( + json.dumps(expected_output), + JsonV2Encoder(max_tag_value_length).serialize( + [otel_span], NodeEndpoint() + ), + ) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_protobuf.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_protobuf.py new file mode 100644 index 0000000000..092a8a303c --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_protobuf.py @@ -0,0 +1,234 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import ipaddress +import json + +from opentelemetry.exporter.zipkin.encoder import NAME_KEY, VERSION_KEY +from opentelemetry.exporter.zipkin.encoder.v2.protobuf import ProtobufEncoder +from opentelemetry.exporter.zipkin.encoder.v2.protobuf.gen import zipkin_pb2 +from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint +from opentelemetry.trace import SpanKind + +from .common_tests import TEST_SERVICE_NAME, CommonEncoderTestCases + + +# pylint: disable=protected-access +class TestProtobufEncoder(CommonEncoderTestCases.CommonEncoderTest): + @staticmethod + def get_encoder(*args, **kwargs) -> ProtobufEncoder: + return ProtobufEncoder(*args, **kwargs) + + def test_encode_trace_id(self): + for trace_id in (1, 1024, 2 ** 32, 2 ** 64, 2 ** 127): + self.assertEqual( + self.get_encoder_default()._encode_trace_id(trace_id), + trace_id.to_bytes(length=16, byteorder="big", signed=False), + ) + + def test_encode_span_id(self): + for span_id in (1, 1024, 2 ** 8, 2 ** 16, 2 ** 32, 2 ** 63): + self.assertEqual( + self.get_encoder_default()._encode_span_id(span_id), + span_id.to_bytes(length=8, byteorder="big", signed=False), + ) + + def test_encode_local_endpoint_default(self): + self.assertEqual( + ProtobufEncoder()._encode_local_endpoint(NodeEndpoint()), + zipkin_pb2.Endpoint(service_name=TEST_SERVICE_NAME), + ) + + def test_encode_local_endpoint_explicits(self): + ipv4 = "192.168.0.1" + ipv6 = "2001:db8::c001" + port = 414120 + self.assertEqual( + ProtobufEncoder()._encode_local_endpoint( + NodeEndpoint(ipv4, ipv6, port) + ), + zipkin_pb2.Endpoint( + service_name=TEST_SERVICE_NAME, + ipv4=ipaddress.ip_address(ipv4).packed, + ipv6=ipaddress.ip_address(ipv6).packed, + port=port, + ), + ) + + def test_encode(self): + local_endpoint = zipkin_pb2.Endpoint(service_name=TEST_SERVICE_NAME) + span_kind = ProtobufEncoder.SPAN_KIND_MAP[SpanKind.INTERNAL] + + otel_spans = self.get_exhaustive_otel_span_list() + trace_id = ProtobufEncoder._encode_trace_id( + otel_spans[0].context.trace_id + ) + expected_output = zipkin_pb2.ListOfSpans( + spans=[ + zipkin_pb2.Span( + trace_id=trace_id, + id=ProtobufEncoder._encode_span_id( + otel_spans[0].context.span_id + ), + name=otel_spans[0].name, + timestamp=ProtobufEncoder._nsec_to_usec_round( + otel_spans[0].start_time + ), + duration=( + ProtobufEncoder._nsec_to_usec_round( + otel_spans[0].end_time - otel_spans[0].start_time + ) + ), + local_endpoint=local_endpoint, + kind=span_kind, + tags={ + "key_bool": "false", + "key_string": "hello_world", + "key_float": "111.22", + "otel.status_code": "OK", + }, + debug=True, + parent_id=ProtobufEncoder._encode_span_id( + otel_spans[0].parent.span_id + ), + annotations=[ + zipkin_pb2.Annotation( + timestamp=ProtobufEncoder._nsec_to_usec_round( + otel_spans[0].events[0].timestamp + ), + value=json.dumps( + { + "event0": { + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + } + }, + sort_keys=True, + ), + ), + ], + ), + zipkin_pb2.Span( + trace_id=trace_id, + id=ProtobufEncoder._encode_span_id( + otel_spans[1].context.span_id + ), + name=otel_spans[1].name, + timestamp=ProtobufEncoder._nsec_to_usec_round( + otel_spans[1].start_time + ), + duration=( + ProtobufEncoder._nsec_to_usec_round( + otel_spans[1].end_time - otel_spans[1].start_time + ) + ), + local_endpoint=local_endpoint, + kind=span_kind, + tags={ + "key_resource": "some_resource", + "otel.status_code": "ERROR", + "error": "Example description", + }, + debug=False, + ), + zipkin_pb2.Span( + trace_id=trace_id, + id=ProtobufEncoder._encode_span_id( + otel_spans[2].context.span_id + ), + name=otel_spans[2].name, + timestamp=ProtobufEncoder._nsec_to_usec_round( + otel_spans[2].start_time + ), + duration=( + ProtobufEncoder._nsec_to_usec_round( + otel_spans[2].end_time - otel_spans[2].start_time + ) + ), + local_endpoint=local_endpoint, + kind=span_kind, + tags={ + "key_string": "hello_world", + "key_resource": "some_resource", + }, + debug=False, + ), + zipkin_pb2.Span( + trace_id=trace_id, + id=ProtobufEncoder._encode_span_id( + otel_spans[3].context.span_id + ), + name=otel_spans[3].name, + timestamp=ProtobufEncoder._nsec_to_usec_round( + otel_spans[3].start_time + ), + duration=( + ProtobufEncoder._nsec_to_usec_round( + otel_spans[3].end_time - otel_spans[3].start_time + ) + ), + local_endpoint=local_endpoint, + kind=span_kind, + tags={NAME_KEY: "name", VERSION_KEY: "version"}, + debug=False, + ), + ], + ) + + actual_output = zipkin_pb2.ListOfSpans.FromString( + ProtobufEncoder().serialize(otel_spans, NodeEndpoint()) + ) + + self.assertEqual(actual_output, expected_output) + + def _test_encode_max_tag_length(self, max_tag_value_length: int): + otel_span, expected_tag_output = self.get_data_for_max_tag_length_test( + max_tag_value_length + ) + service_name = otel_span.name + + expected_output = zipkin_pb2.ListOfSpans( + spans=[ + zipkin_pb2.Span( + trace_id=ProtobufEncoder._encode_trace_id( + otel_span.context.trace_id + ), + id=ProtobufEncoder._encode_span_id( + otel_span.context.span_id + ), + name=service_name, + timestamp=ProtobufEncoder._nsec_to_usec_round( + otel_span.start_time + ), + duration=ProtobufEncoder._nsec_to_usec_round( + otel_span.end_time - otel_span.start_time + ), + local_endpoint=zipkin_pb2.Endpoint( + service_name=service_name + ), + kind=ProtobufEncoder.SPAN_KIND_MAP[SpanKind.INTERNAL], + tags=expected_tag_output, + annotations=None, + debug=True, + ) + ] + ) + + actual_output = zipkin_pb2.ListOfSpans.FromString( + ProtobufEncoder(max_tag_value_length).serialize( + [otel_span], NodeEndpoint() + ) + ) + + self.assertEqual(actual_output, expected_output) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 36320b78c3..43cbd25403 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -12,33 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json +import ipaddress import os import unittest -from unittest.mock import MagicMock, patch +from unittest.mock import patch -from opentelemetry import trace as trace_api -from opentelemetry.exporter.zipkin import ( - NAME_KEY, - SPAN_KIND_MAP_JSON, - SPAN_KIND_MAP_PROTOBUF, - TRANSPORT_FORMAT_JSON, - TRANSPORT_FORMAT_PROTOBUF, - VERSION_KEY, - ZipkinSpanExporter, - nsec_to_usec_round, -) -from opentelemetry.exporter.zipkin.gen import zipkin_pb2 -from opentelemetry.sdk import trace +from opentelemetry import trace +from opentelemetry.exporter.zipkin import DEFAULT_ENDPOINT, ZipkinSpanExporter +from opentelemetry.exporter.zipkin.encoder import Protocol +from opentelemetry.exporter.zipkin.encoder.v2.protobuf import ProtobufEncoder +from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_ZIPKIN_ENDPOINT, - OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT, ) -from opentelemetry.sdk.trace import Resource +from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SpanExportResult -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -from opentelemetry.trace import SpanKind, TraceFlags -from opentelemetry.trace.status import Status, StatusCode + +TEST_SERVICE_NAME = "test_service" class MockResponse: @@ -48,883 +39,127 @@ def __init__(self, status_code): class TestZipkinSpanExporter(unittest.TestCase): - def setUp(self): - # create and save span to be used in tests - context = trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x00000000DEADBEF0, - is_remote=False, + @classmethod + def setUpClass(cls): + trace.set_tracer_provider( + TracerProvider( + resource=Resource({SERVICE_NAME: TEST_SERVICE_NAME}) + ) ) - self._test_span = trace._Span("test_span", context=context) - self._test_span.start() - self._test_span.end() - def tearDown(self): if OTEL_EXPORTER_ZIPKIN_ENDPOINT in os.environ: del os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] - if OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT in os.environ: - del os.environ[OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT] - - def test_constructor_env_var(self): - """Test the default values assigned by constructor.""" - url = "https://foo:9911/path" - os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = url - os.environ[ - OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT - ] = TRANSPORT_FORMAT_PROTOBUF - service_name = "my-service-name" - port = 9911 - exporter = ZipkinSpanExporter(service_name) - ipv4 = None - ipv6 = None - - self.assertEqual(exporter.service_name, service_name) - self.assertEqual(exporter.ipv4, ipv4) - self.assertEqual(exporter.ipv6, ipv6) - self.assertEqual(exporter.url, url) - self.assertEqual(exporter.port, port) - self.assertEqual(exporter.transport_format, TRANSPORT_FORMAT_PROTOBUF) def test_constructor_default(self): - """Test the default values assigned by constructor.""" - service_name = "my-service-name" - port = 9411 - exporter = ZipkinSpanExporter(service_name) - ipv4 = None - ipv6 = None - url = "http://localhost:9411/api/v2/spans" - transport_format = TRANSPORT_FORMAT_JSON - - self.assertEqual(exporter.service_name, service_name) - self.assertEqual(exporter.port, port) - self.assertEqual(exporter.ipv4, ipv4) - self.assertEqual(exporter.ipv6, ipv6) - self.assertEqual(exporter.url, url) - self.assertEqual(exporter.transport_format, transport_format) - - def test_constructor_explicit(self): - """Test the constructor passing all the options.""" - service_name = "my-opentelemetry-zipkin" - port = 15875 - ipv4 = "1.2.3.4" - ipv6 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334" - url = "https://opentelemetry.io:15875/myapi/traces?format=zipkin" - transport_format = TRANSPORT_FORMAT_PROTOBUF - - exporter = ZipkinSpanExporter( - service_name=service_name, - url=url, - ipv4=ipv4, - ipv6=ipv6, - transport_format=transport_format, - ) - - self.assertEqual(exporter.service_name, service_name) - self.assertEqual(exporter.port, port) - self.assertEqual(exporter.ipv4, ipv4) - self.assertEqual(exporter.ipv6, ipv6) - self.assertEqual(exporter.url, url) - self.assertEqual(exporter.transport_format, transport_format) - - # pylint: disable=too-many-locals,too-many-statements - def test_export_json(self): - span_names = ("test1", "test2", "test3", "test4") - trace_id = 0x6E0C63257DE34C926F9EFCD03927272E - span_id = 0x34BF92DEEFC58C92 - parent_id = 0x1111111111111111 - other_id = 0x2222222222222222 - - base_time = 683647322 * 10 ** 9 # in ns - start_times = ( - base_time, - base_time + 150 * 10 ** 6, - base_time + 300 * 10 ** 6, - base_time + 400 * 10 ** 6, - ) - durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6, 300 * 10 ** 6) - end_times = ( - start_times[0] + durations[0], - start_times[1] + durations[1], - start_times[2] + durations[2], - start_times[3] + durations[3], - ) - - span_context = trace_api.SpanContext( - trace_id, - span_id, - is_remote=False, - trace_flags=TraceFlags(TraceFlags.SAMPLED), - ) - parent_span_context = trace_api.SpanContext( - trace_id, parent_id, is_remote=False - ) - other_context = trace_api.SpanContext( - trace_id, other_id, is_remote=False - ) - - event_attributes = { - "annotation_bool": True, - "annotation_string": "annotation_test", - "key_float": 0.3, - } - - event_timestamp = base_time + 50 * 10 ** 6 - event = trace.Event( - name="event0", - timestamp=event_timestamp, - attributes=event_attributes, - ) - - link_attributes = {"key_bool": True} - - link = trace_api.Link( - context=other_context, attributes=link_attributes - ) - - otel_spans = [ - trace._Span( - name=span_names[0], - context=span_context, - parent=parent_span_context, - events=(event,), - links=(link,), - resource=Resource({}), - ), - trace._Span( - name=span_names[1], - context=parent_span_context, - parent=None, - resource=Resource( - attributes={"key_resource": "some_resource"} - ), - ), - trace._Span( - name=span_names[2], - context=other_context, - parent=None, - resource=Resource( - attributes={"key_resource": "some_resource"} - ), - ), - trace._Span( - name=span_names[3], - context=other_context, - parent=None, - resource=Resource({}), - instrumentation_info=InstrumentationInfo( - name="name", version="version" - ), - ), - ] - - otel_spans[0].start(start_time=start_times[0]) - # added here to preserve order - otel_spans[0].set_attribute("key_bool", False) - otel_spans[0].set_attribute("key_string", "hello_world") - otel_spans[0].set_attribute("key_float", 111.22) - otel_spans[0].set_status( - Status(StatusCode.ERROR, "Example description") - ) - otel_spans[0].end(end_time=end_times[0]) - - otel_spans[1].start(start_time=start_times[1]) - otel_spans[1].end(end_time=end_times[1]) - - otel_spans[2].start(start_time=start_times[2]) - otel_spans[2].set_attribute("key_string", "hello_world") - otel_spans[2].end(end_time=end_times[2]) - - otel_spans[3].start(start_time=start_times[3]) - otel_spans[3].end(end_time=end_times[3]) - - service_name = "test-service" - local_endpoint = {"serviceName": service_name, "port": 9411} - span_kind = SPAN_KIND_MAP_JSON[SpanKind.INTERNAL] - - exporter = ZipkinSpanExporter(service_name) - expected_spans = [ - { - "traceId": format(trace_id, "x"), - "id": format(span_id, "x"), - "name": span_names[0], - "timestamp": start_times[0] // 10 ** 3, - "duration": durations[0] // 10 ** 3, - "localEndpoint": local_endpoint, - "kind": span_kind, - "tags": { - "key_bool": "false", - "key_string": "hello_world", - "key_float": "111.22", - "otel.status_code": "ERROR", - "error": "Example description", - }, - "debug": True, - "parentId": format(parent_id, "x"), - "annotations": [ - { - "timestamp": event_timestamp // 10 ** 3, - "value": { - "event0": { - "annotation_bool": True, - "annotation_string": "annotation_test", - "key_float": 0.3, - } - }, - } - ], - }, - { - "traceId": format(trace_id, "x"), - "id": format(parent_id, "x"), - "name": span_names[1], - "timestamp": start_times[1] // 10 ** 3, - "duration": durations[1] // 10 ** 3, - "localEndpoint": local_endpoint, - "kind": span_kind, - "tags": {"key_resource": "some_resource"}, - "annotations": None, - }, - { - "traceId": format(trace_id, "x"), - "id": format(other_id, "x"), - "name": span_names[2], - "timestamp": start_times[2] // 10 ** 3, - "duration": durations[2] // 10 ** 3, - "localEndpoint": local_endpoint, - "kind": span_kind, - "tags": { - "key_string": "hello_world", - "key_resource": "some_resource", - }, - "annotations": None, - }, - { - "traceId": format(trace_id, "x"), - "id": format(other_id, "x"), - "name": span_names[3], - "timestamp": start_times[3] // 10 ** 3, - "duration": durations[3] // 10 ** 3, - "localEndpoint": local_endpoint, - "kind": span_kind, - "tags": {NAME_KEY: "name", VERSION_KEY: "version"}, - "annotations": None, - }, - ] - - mock_post = MagicMock() - with patch("requests.post", mock_post): - mock_post.return_value = MockResponse(200) - status = exporter.export(otel_spans) - self.assertEqual(SpanExportResult.SUCCESS, status) - - # pylint: disable=unsubscriptable-object - kwargs = mock_post.call_args[1] - - self.assertEqual(kwargs["url"], "http://localhost:9411/api/v2/spans") - self.assertEqual(kwargs["headers"]["Content-Type"], "application/json") - actual_spans = sorted( - json.loads(kwargs["data"]), key=lambda span: span["timestamp"] - ) - for expected, actual in zip(expected_spans, actual_spans): - expected_annotations = expected.pop("annotations", None) - actual_annotations = actual.pop("annotations", None) - if actual_annotations: - for annotation in actual_annotations: - annotation["value"] = json.loads(annotation["value"]) - self.assertEqual(expected, actual) - self.assertEqual(expected_annotations, actual_annotations) - - # pylint: disable=too-many-locals - def test_export_json_zero_padding(self): - """test that hex ids starting with 0 - are properly padded to 16 or 32 hex chars - when exported + exporter = ZipkinSpanExporter(Protocol.V2_PROTOBUF) + self.assertIsInstance(exporter.encoder, ProtobufEncoder) + self.assertEqual(exporter.endpoint, DEFAULT_ENDPOINT) + self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) + self.assertEqual(exporter.local_node.ipv4, None) + self.assertEqual(exporter.local_node.ipv6, None) + self.assertEqual(exporter.local_node.port, None) + + def test_constructor_env_vars(self): + os_endpoint = "https://foo:9911/path" + os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint + + exporter = ZipkinSpanExporter(Protocol.V2_PROTOBUF) + + self.assertEqual(exporter.endpoint, os_endpoint) + self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) + self.assertEqual(exporter.local_node.ipv4, None) + self.assertEqual(exporter.local_node.ipv6, None) + self.assertEqual(exporter.local_node.port, None) + + def test_constructor_protocol_endpoint(self): + """Test the constructor for the common usage of providing the + protocol and endpoint arguments.""" + protocol = Protocol.V2_PROTOBUF + endpoint = "https://opentelemetry.io:15875/myapi/traces?format=zipkin" + + exporter = ZipkinSpanExporter(protocol, endpoint) + + self.assertIsInstance(exporter.encoder, ProtobufEncoder) + self.assertEqual(exporter.endpoint, endpoint) + self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) + self.assertEqual(exporter.local_node.ipv4, None) + self.assertEqual(exporter.local_node.ipv6, None) + self.assertEqual(exporter.local_node.port, None) + + def test_constructor_all_params_and_env_vars(self): + """Test the scenario where all params are provided and all OS env + vars are set. Explicit params should take precedence. """ + os_endpoint = "https://os.env.param:9911/path" + os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint - span_names = "testZeroes" - trace_id = 0x0E0C63257DE34C926F9EFCD03927272E - span_id = 0x04BF92DEEFC58C92 - parent_id = 0x0AAAAAAAAAAAAAAA + constructor_param_protocol = Protocol.V2_PROTOBUF + constructor_param_endpoint = "https://constructor.param:9911/path" + local_node_ipv4 = "192.168.0.1" + local_node_ipv6 = "2001:db8::1000" + local_node_port = 30301 + max_tag_value_length = 56 - start_time = 683647322 * 10 ** 9 # in ns - duration = 50 * 10 ** 6 - end_time = start_time + duration - - span_context = trace_api.SpanContext( - trace_id, - span_id, - is_remote=False, - trace_flags=TraceFlags(TraceFlags.SAMPLED), - ) - parent_span_context = trace_api.SpanContext( - trace_id, parent_id, is_remote=False + exporter = ZipkinSpanExporter( + constructor_param_protocol, + constructor_param_endpoint, + local_node_ipv4, + local_node_ipv6, + local_node_port, + max_tag_value_length, ) - otel_span = trace._Span( - name=span_names[0], - context=span_context, - parent=parent_span_context, - resource=Resource({}), + self.assertIsInstance(exporter.encoder, ProtobufEncoder) + self.assertEqual(exporter.endpoint, constructor_param_endpoint) + self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) + self.assertEqual( + exporter.local_node.ipv4, ipaddress.IPv4Address(local_node_ipv4) ) - - otel_span.start(start_time=start_time) - otel_span.end(end_time=end_time) - - service_name = "test-service" - local_endpoint = {"serviceName": service_name, "port": 9411} - - exporter = ZipkinSpanExporter(service_name) - # Check traceId are properly lowercase 16 or 32 hex - expected = [ - { - "traceId": "0e0c63257de34c926f9efcd03927272e", - "id": "04bf92deefc58c92", - "name": span_names[0], - "timestamp": start_time // 10 ** 3, - "duration": duration // 10 ** 3, - "localEndpoint": local_endpoint, - "kind": SPAN_KIND_MAP_JSON[SpanKind.INTERNAL], - "tags": {}, - "annotations": None, - "debug": True, - "parentId": "0aaaaaaaaaaaaaaa", - } - ] - - mock_post = MagicMock() - with patch("requests.post", mock_post): - mock_post.return_value = MockResponse(200) - status = exporter.export([otel_span]) - self.assertEqual(SpanExportResult.SUCCESS, status) - - mock_post.assert_called_with( - url="http://localhost:9411/api/v2/spans", - data=json.dumps(expected), - headers={"Content-Type": "application/json"}, + self.assertEqual( + exporter.local_node.ipv6, ipaddress.IPv6Address(local_node_ipv6) ) + self.assertEqual(exporter.local_node.port, local_node_port) @patch("requests.post") def test_invalid_response(self, mock_post): mock_post.return_value = MockResponse(404) spans = [] - exporter = ZipkinSpanExporter("test-service") + exporter = ZipkinSpanExporter(Protocol.V2_PROTOBUF) status = exporter.export(spans) self.assertEqual(SpanExportResult.FAILURE, status) - def test_export_json_max_tag_length(self): - service_name = "test-service" - - span_context = trace_api.SpanContext( - 0x0E0C63257DE34C926F9EFCD03927272E, - 0x04BF92DEEFC58C92, - is_remote=False, - trace_flags=TraceFlags(TraceFlags.SAMPLED), - ) - - span = trace._Span( - name="test-span", context=span_context, resource=Resource({}) - ) - - span.start() - # added here to preserve order - span.set_attribute("string1", "v" * 500) - span.set_attribute("string2", "v" * 50) - span.set_attribute("list1", ["a"] * 25) - span.set_attribute("list2", ["a"] * 10) - span.set_attribute("list3", [2] * 25) - span.set_attribute("list4", [2] * 10) - span.set_attribute("list5", [True] * 25) - span.set_attribute("list6", [True] * 10) - span.set_attribute("tuple1", ("a",) * 25) - span.set_attribute("tuple2", ("a",) * 10) - span.set_attribute("tuple3", (2,) * 25) - span.set_attribute("tuple4", (2,) * 10) - span.set_attribute("tuple5", (True,) * 25) - span.set_attribute("tuple6", (True,) * 10) - span.set_attribute("range1", range(0, 25)) - span.set_attribute("range2", range(0, 10)) - span.set_attribute("empty_list", []) - span.set_attribute("none_list", ["hello", None, "world"]) - - span.set_status(Status(StatusCode.ERROR, "Example description")) - span.end() - - exporter = ZipkinSpanExporter(service_name) - mock_post = MagicMock() - with patch("requests.post", mock_post): - mock_post.return_value = MockResponse(200) - status = exporter.export([span]) - self.assertEqual(SpanExportResult.SUCCESS, status) - - _, kwargs = mock_post.call_args # pylint: disable=E0633 - - tags = json.loads(kwargs["data"])[0]["tags"] - self.assertEqual(len(tags["string1"]), 128) - self.assertEqual(len(tags["string2"]), 50) - self.assertEqual( - tags["list1"], - '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', - ) - self.assertEqual( - tags["list2"], '["a","a","a","a","a","a","a","a","a","a"]', - ) - self.assertEqual( - tags["list3"], - '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', - ) - self.assertEqual( - tags["list4"], '["2","2","2","2","2","2","2","2","2","2"]', - ) - self.assertEqual( - tags["list5"], - '["true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true"]', - ) - self.assertEqual( - tags["list6"], - '["true","true","true","true","true","true","true","true","true","true"]', - ) - self.assertEqual( - tags["tuple1"], - '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', - ) - self.assertEqual( - tags["tuple2"], '["a","a","a","a","a","a","a","a","a","a"]', - ) - self.assertEqual( - tags["tuple3"], - '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', - ) - self.assertEqual( - tags["tuple4"], '["2","2","2","2","2","2","2","2","2","2"]', - ) - self.assertEqual( - tags["tuple5"], - '["true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true"]', - ) - self.assertEqual( - tags["tuple6"], - '["true","true","true","true","true","true","true","true","true","true"]', - ) - self.assertEqual( - tags["range1"], - '["0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24"]', - ) - self.assertEqual( - tags["range2"], '["0","1","2","3","4","5","6","7","8","9"]', - ) - self.assertEqual( - tags["empty_list"], "[]", - ) - self.assertEqual( - tags["none_list"], '["hello",null,"world"]', - ) - - exporter = ZipkinSpanExporter(service_name, max_tag_value_length=2) - mock_post = MagicMock() - with patch("requests.post", mock_post): - mock_post.return_value = MockResponse(200) - status = exporter.export([span]) - self.assertEqual(SpanExportResult.SUCCESS, status) - - _, kwargs = mock_post.call_args # pylint: disable=E0633 - tags = json.loads(kwargs["data"])[0]["tags"] - self.assertEqual(len(tags["string1"]), 2) - self.assertEqual(len(tags["string2"]), 2) - self.assertEqual(tags["list1"], "[]") - self.assertEqual(tags["list2"], "[]") - self.assertEqual(tags["list3"], "[]") - self.assertEqual(tags["list4"], "[]") - self.assertEqual(tags["list5"], "[]") - self.assertEqual(tags["list6"], "[]") - self.assertEqual(tags["tuple1"], "[]") - self.assertEqual(tags["tuple2"], "[]") - self.assertEqual(tags["tuple3"], "[]") - self.assertEqual(tags["tuple4"], "[]") - self.assertEqual(tags["tuple5"], "[]") - self.assertEqual(tags["tuple6"], "[]") - self.assertEqual(tags["range1"], "[]") - self.assertEqual(tags["range2"], "[]") - - exporter = ZipkinSpanExporter(service_name, max_tag_value_length=5) - mock_post = MagicMock() - with patch("requests.post", mock_post): - mock_post.return_value = MockResponse(200) - status = exporter.export([span]) - self.assertEqual(SpanExportResult.SUCCESS, status) - - _, kwargs = mock_post.call_args # pylint: disable=E0633 - tags = json.loads(kwargs["data"])[0]["tags"] - self.assertEqual(len(tags["string1"]), 5) - self.assertEqual(len(tags["string2"]), 5) - self.assertEqual(tags["list1"], '["a"]') - self.assertEqual(tags["list2"], '["a"]') - self.assertEqual(tags["list3"], '["2"]') - self.assertEqual(tags["list4"], '["2"]') - self.assertEqual(tags["list5"], "[]") - self.assertEqual(tags["list6"], "[]") - self.assertEqual(tags["tuple1"], '["a"]') - self.assertEqual(tags["tuple2"], '["a"]') - self.assertEqual(tags["tuple3"], '["2"]') - self.assertEqual(tags["tuple4"], '["2"]') - self.assertEqual(tags["tuple5"], "[]") - self.assertEqual(tags["tuple6"], "[]") - self.assertEqual(tags["range1"], '["0"]') - self.assertEqual(tags["range2"], '["0"]') - - exporter = ZipkinSpanExporter(service_name, max_tag_value_length=9) - mock_post = MagicMock() - with patch("requests.post", mock_post): - mock_post.return_value = MockResponse(200) - status = exporter.export([span]) - self.assertEqual(SpanExportResult.SUCCESS, status) - - _, kwargs = mock_post.call_args # pylint: disable=E0633 - tags = json.loads(kwargs["data"])[0]["tags"] - self.assertEqual(len(tags["string1"]), 9) - self.assertEqual(len(tags["string2"]), 9) - self.assertEqual(tags["list1"], '["a","a"]') - self.assertEqual(tags["list2"], '["a","a"]') - self.assertEqual(tags["list3"], '["2","2"]') - self.assertEqual(tags["list4"], '["2","2"]') - self.assertEqual(tags["list5"], '["true"]') - self.assertEqual(tags["list6"], '["true"]') - self.assertEqual(tags["tuple1"], '["a","a"]') - self.assertEqual(tags["tuple2"], '["a","a"]') - self.assertEqual(tags["tuple3"], '["2","2"]') - self.assertEqual(tags["tuple4"], '["2","2"]') - self.assertEqual(tags["tuple5"], '["true"]') - self.assertEqual(tags["tuple6"], '["true"]') - self.assertEqual(tags["range1"], '["0","1"]') - self.assertEqual(tags["range2"], '["0","1"]') - - exporter = ZipkinSpanExporter(service_name, max_tag_value_length=10) - mock_post = MagicMock() - with patch("requests.post", mock_post): - mock_post.return_value = MockResponse(200) - status = exporter.export([span]) - self.assertEqual(SpanExportResult.SUCCESS, status) - - _, kwargs = mock_post.call_args # pylint: disable=E0633 - tags = json.loads(kwargs["data"])[0]["tags"] - self.assertEqual(len(tags["string1"]), 10) - self.assertEqual(len(tags["string2"]), 10) - self.assertEqual(tags["list1"], '["a","a"]') - self.assertEqual(tags["list2"], '["a","a"]') - self.assertEqual(tags["list3"], '["2","2"]') - self.assertEqual(tags["list4"], '["2","2"]') - self.assertEqual(tags["list5"], '["true"]') - self.assertEqual(tags["list6"], '["true"]') - self.assertEqual(tags["tuple1"], '["a","a"]') - self.assertEqual(tags["tuple2"], '["a","a"]') - self.assertEqual(tags["tuple3"], '["2","2"]') - self.assertEqual(tags["tuple4"], '["2","2"]') - self.assertEqual(tags["tuple5"], '["true"]') - self.assertEqual(tags["tuple6"], '["true"]') - self.assertEqual(tags["range1"], '["0","1"]') - self.assertEqual(tags["range2"], '["0","1"]') - - exporter = ZipkinSpanExporter(service_name, max_tag_value_length=11) - mock_post = MagicMock() - with patch("requests.post", mock_post): - mock_post.return_value = MockResponse(200) - status = exporter.export([span]) - self.assertEqual(SpanExportResult.SUCCESS, status) - - _, kwargs = mock_post.call_args # pylint: disable=E0633 - tags = json.loads(kwargs["data"])[0]["tags"] - self.assertEqual(len(tags["string1"]), 11) - self.assertEqual(len(tags["string2"]), 11) - self.assertEqual(tags["list1"], '["a","a"]') - self.assertEqual(tags["list2"], '["a","a"]') - self.assertEqual(tags["list3"], '["2","2"]') - self.assertEqual(tags["list4"], '["2","2"]') - self.assertEqual(tags["list5"], '["true"]') - self.assertEqual(tags["list6"], '["true"]') - self.assertEqual(tags["tuple1"], '["a","a"]') - self.assertEqual(tags["tuple2"], '["a","a"]') - self.assertEqual(tags["tuple3"], '["2","2"]') - self.assertEqual(tags["tuple4"], '["2","2"]') - self.assertEqual(tags["tuple5"], '["true"]') - self.assertEqual(tags["tuple6"], '["true"]') - self.assertEqual(tags["range1"], '["0","1"]') - self.assertEqual(tags["range2"], '["0","1"]') - - # pylint: disable=too-many-locals,too-many-statements - def test_export_protobuf(self): - span_names = ("test1", "test2", "test3", "test4") - trace_id = 0x6E0C63257DE34C926F9EFCD03927272E - span_id = 0x34BF92DEEFC58C92 - parent_id = 0x1111111111111111 - other_id = 0x2222222222222222 - - base_time = 683647322 * 10 ** 9 # in ns - start_times = ( - base_time, - base_time + 150 * 10 ** 6, - base_time + 300 * 10 ** 6, - base_time + 400 * 10 ** 6, - ) - durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6, 300 * 10 ** 6) - end_times = ( - start_times[0] + durations[0], - start_times[1] + durations[1], - start_times[2] + durations[2], - start_times[3] + durations[3], - ) - - span_context = trace_api.SpanContext( - trace_id, - span_id, - is_remote=False, - trace_flags=TraceFlags(TraceFlags.SAMPLED), - ) - parent_span_context = trace_api.SpanContext( - trace_id, parent_id, is_remote=False - ) - other_context = trace_api.SpanContext( - trace_id, other_id, is_remote=False - ) - - event_attributes = { - "annotation_bool": True, - "annotation_string": "annotation_test", - "key_float": 0.3, - } - - event_timestamp = base_time + 50 * 10 ** 6 - event = trace.Event( - name="event0", - timestamp=event_timestamp, - attributes=event_attributes, - ) - - link_attributes = {"key_bool": True} - - link = trace_api.Link( - context=other_context, attributes=link_attributes - ) - - otel_spans = [ - trace._Span( - name=span_names[0], - context=span_context, - parent=parent_span_context, - resource=Resource({}), - events=(event,), - links=(link,), - ), - trace._Span( - name=span_names[1], - context=parent_span_context, - parent=None, - resource=Resource( - attributes={"key_resource": "some_resource"} - ), - ), - trace._Span( - name=span_names[2], - context=other_context, - parent=None, - resource=Resource( - attributes={"key_resource": "some_resource"} - ), - ), - trace._Span( - name=span_names[3], - context=other_context, - parent=None, - resource=Resource({}), - instrumentation_info=InstrumentationInfo( - name="name", version="version" - ), - ), - ] - - otel_spans[0].start(start_time=start_times[0]) - # added here to preserve order - otel_spans[0].set_attribute("key_bool", False) - otel_spans[0].set_attribute("key_string", "hello_world") - otel_spans[0].set_attribute("key_float", 111.22) - otel_spans[0].set_status( - Status(StatusCode.ERROR, "Example description") - ) - otel_spans[0].end(end_time=end_times[0]) - - otel_spans[1].start(start_time=start_times[1]) - otel_spans[1].set_status(Status(StatusCode.OK)) - otel_spans[1].end(end_time=end_times[1]) - - otel_spans[2].start(start_time=start_times[2]) - otel_spans[2].set_attribute("key_string", "hello_world") - otel_spans[2].end(end_time=end_times[2]) - - otel_spans[3].start(start_time=start_times[3]) - otel_spans[3].end(end_time=end_times[3]) - - service_name = "test-service" - local_endpoint = zipkin_pb2.Endpoint( - service_name=service_name, port=9411 - ) - span_kind = SPAN_KIND_MAP_PROTOBUF[SpanKind.INTERNAL] - - expected_spans = zipkin_pb2.ListOfSpans( - spans=[ - zipkin_pb2.Span( - trace_id=trace_id.to_bytes( - length=16, byteorder="big", signed=False - ), - id=ZipkinSpanExporter.format_pbuf_span_id(span_id), - name=span_names[0], - timestamp=nsec_to_usec_round(start_times[0]), - duration=nsec_to_usec_round(durations[0]), - local_endpoint=local_endpoint, - kind=span_kind, - tags={ - "key_bool": "false", - "key_string": "hello_world", - "key_float": "111.22", - "otel.status_code": "ERROR", - "error": "Example description", - }, - debug=True, - parent_id=ZipkinSpanExporter.format_pbuf_span_id( - parent_id - ), - annotations=[ - zipkin_pb2.Annotation( - timestamp=nsec_to_usec_round(event_timestamp), - value=json.dumps( - { - "event0": { - "annotation_bool": True, - "annotation_string": "annotation_test", - "key_float": 0.3, - } - } - ), - ), - ], - ), - zipkin_pb2.Span( - trace_id=trace_id.to_bytes( - length=16, byteorder="big", signed=False - ), - id=ZipkinSpanExporter.format_pbuf_span_id(parent_id), - name=span_names[1], - timestamp=nsec_to_usec_round(start_times[1]), - duration=nsec_to_usec_round(durations[1]), - local_endpoint=local_endpoint, - kind=span_kind, - tags={ - "key_resource": "some_resource", - "otel.status_code": "OK", - }, - ), - zipkin_pb2.Span( - trace_id=trace_id.to_bytes( - length=16, byteorder="big", signed=False - ), - id=ZipkinSpanExporter.format_pbuf_span_id(other_id), - name=span_names[2], - timestamp=nsec_to_usec_round(start_times[2]), - duration=nsec_to_usec_round(durations[2]), - local_endpoint=local_endpoint, - kind=span_kind, - tags={ - "key_string": "hello_world", - "key_resource": "some_resource", - }, - ), - zipkin_pb2.Span( - trace_id=trace_id.to_bytes( - length=16, byteorder="big", signed=False - ), - id=ZipkinSpanExporter.format_pbuf_span_id(other_id), - name=span_names[3], - timestamp=nsec_to_usec_round(start_times[3]), - duration=nsec_to_usec_round(durations[3]), - local_endpoint=local_endpoint, - kind=span_kind, - tags={NAME_KEY: "name", VERSION_KEY: "version"}, - ), - ], - ) - - exporter = ZipkinSpanExporter( - service_name, transport_format=TRANSPORT_FORMAT_PROTOBUF - ) - mock_post = MagicMock() - with patch("requests.post", mock_post): - mock_post.return_value = MockResponse(200) - status = exporter.export(otel_spans) - self.assertEqual(SpanExportResult.SUCCESS, status) - - # pylint: disable=unsubscriptable-object - kwargs = mock_post.call_args[1] - - self.assertEqual(kwargs["url"], "http://localhost:9411/api/v2/spans") - self.assertEqual( - kwargs["headers"]["Content-Type"], "application/x-protobuf" - ) - self.assertEqual( - zipkin_pb2.ListOfSpans.FromString(kwargs["data"]), expected_spans - ) - - def test_export_protobuf_max_tag_length(self): - service_name = "test-service" - - span_context = trace_api.SpanContext( - 0x0E0C63257DE34C926F9EFCD03927272E, - 0x04BF92DEEFC58C92, - is_remote=False, - trace_flags=TraceFlags(TraceFlags.SAMPLED), - ) - - span = trace._Span( - name="test-span", context=span_context, resource=Resource({}) - ) - - span.start() - # added here to preserve order - span.set_attribute("k1", "v" * 500) - span.set_attribute("k2", "v" * 50) - span.set_status(Status(StatusCode.ERROR, "Example description")) - span.end() - - exporter = ZipkinSpanExporter( - service_name, transport_format=TRANSPORT_FORMAT_PROTOBUF, - ) - mock_post = MagicMock() - with patch("requests.post", mock_post): - mock_post.return_value = MockResponse(200) - status = exporter.export([span]) - self.assertEqual(SpanExportResult.SUCCESS, status) - - # pylint: disable=unsubscriptable-object - kwargs = mock_post.call_args[1] - actual_spans = zipkin_pb2.ListOfSpans.FromString(kwargs["data"]) - span_tags = actual_spans.spans[0].tags - - self.assertEqual(len(span_tags["k1"]), 128) - self.assertEqual(len(span_tags["k2"]), 50) - - exporter = ZipkinSpanExporter( - service_name, - transport_format=TRANSPORT_FORMAT_PROTOBUF, - max_tag_value_length=2, - ) - mock_post = MagicMock() - with patch("requests.post", mock_post): - mock_post.return_value = MockResponse(200) - status = exporter.export([span]) - self.assertEqual(SpanExportResult.SUCCESS, status) - - # pylint: disable=unsubscriptable-object - kwargs = mock_post.call_args[1] - actual_spans = zipkin_pb2.ListOfSpans.FromString(kwargs["data"]) - span_tags = actual_spans.spans[0].tags - - self.assertEqual(len(span_tags["k1"]), 2) - self.assertEqual(len(span_tags["k2"]), 2) +class TestZipkinNodeEndpoint(unittest.TestCase): + def test_constructor_default(self): + node_endpoint = NodeEndpoint() + self.assertEqual(node_endpoint.ipv4, None) + self.assertEqual(node_endpoint.ipv6, None) + self.assertEqual(node_endpoint.port, None) + self.assertEqual(node_endpoint.service_name, TEST_SERVICE_NAME) + + def test_constructor_explicits(self): + ipv4 = "192.168.0.1" + ipv6 = "2001:db8::c001" + port = 414120 + node_endpoint = NodeEndpoint(ipv4, ipv6, port) + self.assertEqual(node_endpoint.ipv4, ipaddress.IPv4Address(ipv4)) + self.assertEqual(node_endpoint.ipv6, ipaddress.IPv6Address(ipv6)) + self.assertEqual(node_endpoint.port, port) + self.assertEqual(node_endpoint.service_name, TEST_SERVICE_NAME) + + def test_ipv4_invalid_raises_error(self): + with self.assertRaises(ValueError): + NodeEndpoint(ipv4="invalid-ipv4-address") + + def test_ipv4_passed_ipv6_raises_error(self): + with self.assertRaises(ValueError): + NodeEndpoint(ipv4="2001:db8::c001") + + def test_ipv6_invalid_raises_error(self): + with self.assertRaises(ValueError): + NodeEndpoint(ipv6="invalid-ipv6-address") + + def test_ipv6_passed_ipv4_raises_error(self): + with self.assertRaises(ValueError): + NodeEndpoint(ipv6="192.168.0.1") diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 2770b3659c..79d9c29c20 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -46,4 +46,3 @@ OTEL_EXPORTER_JAEGER_CERTIFICATE = "OTEL_EXPORTER_JAEGER_CERTIFICATE" OTEL_EXPORTER_OTLP_INSECURE = "OTEL_EXPORTER_OTLP_INSECURE" OTEL_EXPORTER_OTLP_SPAN_INSECURE = "OTEL_EXPORTER_OTLP_SPAN_INSECURE" -OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT = "OTEL_EXPORTER_ZIPKIN_TRANSPORT_FORMAT" diff --git a/pyproject.toml b/pyproject.toml index 5cea9a3445..5207dd223a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ exclude = ''' ( /( # generated files exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen| - exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/gen| + exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen| opentelemetry-proto/src/opentelemetry/proto/collector| opentelemetry-proto/src/opentelemetry/proto/common| opentelemetry-proto/src/opentelemetry/proto/metrics| From 99128b35d1a9cf2ecb04097c6a87d320a552a3e4 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 23 Feb 2021 17:02:25 -0800 Subject: [PATCH 0784/1517] Add minimum Python requirement (#1626) --- README.md | 3 +++ docs/index.rst | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index 810e94a809..1231b72347 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,9 @@ This page describes the Python [OpenTelemetry](https://opentelemetry.io/) implementation. OpenTelemetry is an observability framework for cloud-native software. +## Requirements +Unless otherwise noted, all published artifacts support Python 3.5 or higher. See CONTRIBUTING.md for additional instructions for building this project for development. + ## Getting started The goal of OpenTelemetry is to provide a single set of APIs to capture distributed traces and metrics from your application and send them to an observability platform. This project lets you do just that for applications written in Python. diff --git a/docs/index.rst b/docs/index.rst index 66797a3a74..c07ef84e4f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,6 +14,10 @@ This documentation describes the :doc:`opentelemetry-api `, **Please note** that this library is currently in _beta_, and shouldn't generally be used in production environments. +Requirement +----------- +OpenTelemetry-Python supports Python 3.5 and higher. + Installation ------------ From 61b63e5606163393567392d5b56342b59e0072e2 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 26 Feb 2021 08:07:42 -0800 Subject: [PATCH 0785/1517] Remove gitter from docs (#1650) Co-authored-by: alrex --- CONTRIBUTING.md | 11 +++++++---- README.md | 4 ---- docs/index.rst | 6 +++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e90006a662..6b6918ec30 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ information on this and other language SIGs. See the [public meeting notes](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit) for a summary description of past meetings. To request edit access, join the -meeting or get in touch on [Gitter](https://gitter.im/open-telemetry/opentelemetry-python). +meeting or get in touch on [Slack](https://cloud-native.slack.com/archives/C01PD4HUVBL). See to the [community membership document](https://github.com/open-telemetry/community/blob/main/community-membership.md) on how to become a [**Member**](https://github.com/open-telemetry/community/blob/main/community-membership.md#member), @@ -20,7 +20,7 @@ Please take a look at this list first, your contributions may belong in one of t 1. [OpenTelemetry Contrib](https://github.com/open-telemetry/opentelemetry-python-contrib): Instrumentations for third-party libraries and frameworks. There is an ongoing effort to migrate into the Opentelemetry Contrib repo some of the existing - programmatic instrumentations that are now in the `ext` directory in the main OpenTelemetry repo. Please ask in the Gitter + programmatic instrumentations that are now in the `ext` directory in the main OpenTelemetry repo. Please ask in the Slack channel (see below) for guidance if you want to contribute with these instrumentations. # Find the right branch @@ -30,12 +30,15 @@ The default branch for this repo is `main`. Changes that pertain to `metrics` go ## Find a Buddy and get Started Quickly! If you are looking for someone to help you find a starting point and be a resource for your first contribution, join our -Gitter and find a buddy! +Slack and find a buddy! -1. Join [Gitter.im](https://gitter.im) and join our [chat room](https://gitter.im/open-telemetry/opentelemetry-python). +1. Join [Slack](https://slack.cncf.io/) and join our [channel](https://cloud-native.slack.com/archives/C01PD4HUVBL). 2. Post in the room with an introduction to yourself, what area you are interested in (check issues marked "Help Wanted"), and say you are looking for a buddy. We will match you with someone who has experience in that area. +The Slack channel will be used for introductions and an entry point for external people to be triaged and redirected. For +discussions, please open up an issue or a Github [Discussion](https://github.com/open-telemetry/opentelemetry-python/discussions). + Your OpenTelemetry buddy is your resource to talk to directly on all aspects of contributing to OpenTelemetry: providing context, reviewing PRs, and helping those get merged. Buddies will not be available 24/7, but is committed to responding during their normal contribution hours. diff --git a/README.md b/README.md index 1231b72347..b848b930be 100644 --- a/README.md +++ b/README.md @@ -127,13 +127,9 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): - [Aaron Abbott](https://github.com/aabmass), Google -- [Carlos Alberto Cortez](https://github.com/carlosalberto), Lightstep -- [Tahir H. Butt](https://github.com/majorgreys) DataDog -- [Chris Kleinknecht](https://github.com/c24t), Google - [Diego Hurtado](https://github.com/ocelotl) - [Hector Hernandez](https://github.com/hectorhdzg), Microsoft - [Owais Lone](https://github.com/owais), Splunk -- [Reiley Yang](https://github.com/reyang), Microsoft - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google *For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver).* diff --git a/docs/index.rst b/docs/index.rst index c07ef84e4f..12fbd7ef44 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,9 +3,9 @@ OpenTelemetry-Python The Python `OpenTelemetry `_ client. -.. image:: https://img.shields.io/gitter/room/opentelemetry/opentelemetry-python - :target: https://gitter.im/open-telemetry/opentelemetry-python - :alt: Gitter Chat +.. image:: https://img.shields.io/badge/slack-chat-green.svg + :target: https://cloud-native.slack.com/archives/C01PD4HUVBL + :alt: Slack Chat This documentation describes the :doc:`opentelemetry-api `, From 290478e3312debd023f179fe403ede025fbbefc7 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 26 Feb 2021 14:56:23 -0800 Subject: [PATCH 0786/1517] Rename IdsGenerator to IdGenerator (#1651) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 4 ++ docs/sdk/trace.id_generator.rst | 7 ++++ docs/sdk/trace.ids_generator.rst | 7 ---- docs/sdk/trace.rst | 2 +- .../environment_variables/__init__.py | 2 +- .../src/opentelemetry/distro/__init__.py | 34 +++++++-------- .../tests/test_configurator.py | 41 +++++++++---------- opentelemetry-instrumentation/README.rst | 2 +- .../auto_instrumentation/__init__.py | 10 ++--- opentelemetry-sdk/setup.cfg | 4 +- .../src/opentelemetry/sdk/trace/__init__.py | 23 +++++------ .../{ids_generator.py => id_generator.py} | 6 +-- opentelemetry-sdk/tests/trace/test_trace.py | 18 ++++---- .../opentelemetry/propagators/b3/__init__.py | 6 +-- .../tests/test_b3_format.py | 20 ++++----- .../tests/test_jaeger_propagator.py | 12 +++--- 17 files changed, 99 insertions(+), 101 deletions(-) create mode 100644 docs/sdk/trace.id_generator.rst delete mode 100644 docs/sdk/trace.ids_generator.rst rename opentelemetry-sdk/src/opentelemetry/sdk/trace/{ids_generator.py => id_generator.py} (91%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d1c0db2b9d..34e11fc67e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 6e328246c895ff433b14430c9edddfead072287c + CONTRIB_REPO_SHA: c86562fcddcf8fd037492de3880790776abe79ce jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index bcb8d65d4b..9ddc4e879b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.18b0...HEAD) +### Changed +- Rename `IdsGenerator` to `IdGenerator` + ([#1651])(https://github.com/open-telemetry/opentelemetry-python/pull/1651) + ## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 ### Added diff --git a/docs/sdk/trace.id_generator.rst b/docs/sdk/trace.id_generator.rst new file mode 100644 index 0000000000..e0b4640e41 --- /dev/null +++ b/docs/sdk/trace.id_generator.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.trace.id_generator +==================================== + +.. automodule:: opentelemetry.sdk.trace.id_generator + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sdk/trace.ids_generator.rst b/docs/sdk/trace.ids_generator.rst deleted file mode 100644 index ab5ab14951..0000000000 --- a/docs/sdk/trace.ids_generator.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.sdk.trace.ids_generator -===================================== - -.. automodule:: opentelemetry.sdk.trace.ids_generator - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/sdk/trace.rst b/docs/sdk/trace.rst index 22e4fcc13f..d163ac11e2 100644 --- a/docs/sdk/trace.rst +++ b/docs/sdk/trace.rst @@ -7,7 +7,7 @@ Submodules .. toctree:: trace.export - trace.ids_generator + trace.id_generator trace.sampling util.instrumentation diff --git a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py index 3b1ee8a7e3..21d84d0f85 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py @@ -15,7 +15,7 @@ OTEL_PROPAGATORS = "OTEL_PROPAGATORS" OTEL_PYTHON_CONTEXT = "OTEL_PYTHON_CONTEXT" OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS" -OTEL_PYTHON_IDS_GENERATOR = "OTEL_PYTHON_IDS_GENERATOR" +OTEL_PYTHON_ID_GENERATOR = "OTEL_PYTHON_ID_GENERATOR" OTEL_PYTHON_SERVICE_NAME = "OTEL_PYTHON_SERVICE_NAME" OTEL_TRACES_EXPORTER = "OTEL_TRACES_EXPORTER" OTEL_PYTHON_TRACER_PROVIDER = "OTEL_PYTHON_TRACER_PROVIDER" diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index 46b469b0fb..51464d51a5 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -21,7 +21,7 @@ from opentelemetry import trace from opentelemetry.environment_variables import ( - OTEL_PYTHON_IDS_GENERATOR, + OTEL_PYTHON_ID_GENERATOR, OTEL_PYTHON_SERVICE_NAME, OTEL_TRACES_EXPORTER, ) @@ -33,7 +33,7 @@ BatchExportSpanProcessor, SpanExporter, ) -from opentelemetry.sdk.trace.ids_generator import IdsGenerator +from opentelemetry.sdk.trace.id_generator import IdGenerator logger = getLogger(__file__) @@ -41,12 +41,12 @@ EXPORTER_OTLP = "otlp" EXPORTER_OTLP_SPAN = "otlp_span" -RANDOM_IDS_GENERATOR = "random" -_DEFAULT_IDS_GENERATOR = RANDOM_IDS_GENERATOR +RANDOM_ID_GENERATOR = "random" +_DEFAULT_ID_GENERATOR = RANDOM_ID_GENERATOR -def _get_ids_generator() -> str: - return environ.get(OTEL_PYTHON_IDS_GENERATOR, _DEFAULT_IDS_GENERATOR) +def _get_id_generator() -> str: + return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR) def _get_service_name() -> str: @@ -77,12 +77,12 @@ def _get_exporter_names() -> Sequence[str]: def _init_tracing( - exporters: Sequence[SpanExporter], ids_generator: IdsGenerator + exporters: Sequence[SpanExporter], id_generator: IdGenerator ): service_name = _get_service_name() provider = TracerProvider( resource=Resource.create({"service.name": service_name}), - ids_generator=ids_generator(), + id_generator=id_generator(), ) trace.set_tracer_provider(provider) @@ -141,26 +141,26 @@ def _import_exporters( return trace_exporters -def _import_ids_generator(ids_generator_name: str) -> IdsGenerator: +def _import_id_generator(id_generator_name: str) -> IdGenerator: # pylint: disable=unbalanced-tuple-unpacking [ - (ids_generator_name, ids_generator_impl) + (id_generator_name, id_generator_impl) ] = _import_tracer_provider_config_components( - [ids_generator_name.strip()], "opentelemetry_ids_generator" + [id_generator_name.strip()], "opentelemetry_id_generator" ) - if issubclass(ids_generator_impl, IdsGenerator): - return ids_generator_impl + if issubclass(id_generator_impl, IdGenerator): + return id_generator_impl - raise RuntimeError("{0} is not an IdsGenerator".format(ids_generator_name)) + raise RuntimeError("{0} is not an IdGenerator".format(id_generator_name)) def _initialize_components(): exporter_names = _get_exporter_names() trace_exporters = _import_exporters(exporter_names) - ids_generator_name = _get_ids_generator() - ids_generator = _import_ids_generator(ids_generator_name) - _init_tracing(trace_exporters, ids_generator) + id_generator_name = _get_id_generator() + id_generator = _import_id_generator(id_generator_name) + _init_tracing(trace_exporters, id_generator) class Configurator(BaseConfigurator): diff --git a/opentelemetry-distro/tests/test_configurator.py b/opentelemetry-distro/tests/test_configurator.py index e1a8cfc8ae..b6b80b2e60 100644 --- a/opentelemetry-distro/tests/test_configurator.py +++ b/opentelemetry-distro/tests/test_configurator.py @@ -18,24 +18,21 @@ from unittest.mock import patch from opentelemetry.distro import ( - _get_ids_generator, - _import_ids_generator, + _get_id_generator, + _import_id_generator, _init_tracing, ) from opentelemetry.environment_variables import ( - OTEL_PYTHON_IDS_GENERATOR, + OTEL_PYTHON_ID_GENERATOR, OTEL_PYTHON_SERVICE_NAME, ) from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.trace.ids_generator import ( - IdsGenerator, - RandomIdsGenerator, -) +from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator class Provider: - def __init__(self, resource=None, ids_generator=None): - self.ids_generator = ids_generator + def __init__(self, resource=None, id_generator=None): + self.id_generator = id_generator self.processor = None self.resource = resource @@ -60,7 +57,7 @@ class OTLPExporter: pass -class CustomIdsGenerator(IdsGenerator): +class CustomIdGenerator(IdGenerator): def generate_span_id(self): pass @@ -103,12 +100,12 @@ def tearDown(self): # pylint: disable=protected-access def test_trace_init_default(self): environ[OTEL_PYTHON_SERVICE_NAME] = "my-test-service" - _init_tracing({"zipkin": Exporter}, RandomIdsGenerator) + _init_tracing({"zipkin": Exporter}, RandomIdGenerator) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, Provider) - self.assertIsInstance(provider.ids_generator, RandomIdsGenerator) + self.assertIsInstance(provider.id_generator, RandomIdGenerator) self.assertIsInstance(provider.processor, Processor) self.assertIsInstance(provider.processor.exporter, Exporter) self.assertEqual( @@ -117,12 +114,12 @@ def test_trace_init_default(self): def test_trace_init_otlp(self): environ[OTEL_PYTHON_SERVICE_NAME] = "my-otlp-test-service" - _init_tracing({"otlp": OTLPExporter}, RandomIdsGenerator) + _init_tracing({"otlp": OTLPExporter}, RandomIdGenerator) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, Provider) - self.assertIsInstance(provider.ids_generator, RandomIdsGenerator) + self.assertIsInstance(provider.id_generator, RandomIdGenerator) self.assertIsInstance(provider.processor, Processor) self.assertIsInstance(provider.processor.exporter, OTLPExporter) self.assertIsInstance(provider.resource, Resource) @@ -132,17 +129,17 @@ def test_trace_init_otlp(self): ) del environ[OTEL_PYTHON_SERVICE_NAME] - @patch.dict(environ, {OTEL_PYTHON_IDS_GENERATOR: "custom_ids_generator"}) - @patch("opentelemetry.distro.IdsGenerator", new=IdsGenerator) + @patch.dict(environ, {OTEL_PYTHON_ID_GENERATOR: "custom_id_generator"}) + @patch("opentelemetry.distro.IdGenerator", new=IdGenerator) @patch("opentelemetry.distro.iter_entry_points") - def test_trace_init_custom_ids_generator(self, mock_iter_entry_points): + def test_trace_init_custom_id_generator(self, mock_iter_entry_points): mock_iter_entry_points.configure_mock( return_value=[ - IterEntryPoint("custom_ids_generator", CustomIdsGenerator) + IterEntryPoint("custom_id_generator", CustomIdGenerator) ] ) - ids_generator_name = _get_ids_generator() - ids_generator = _import_ids_generator(ids_generator_name) - _init_tracing({}, ids_generator) + id_generator_name = _get_id_generator() + id_generator = _import_id_generator(id_generator_name) + _init_tracing({}, id_generator) provider = self.set_provider_mock.call_args[0][0] - self.assertIsInstance(provider.ids_generator, CustomIdsGenerator) + self.assertIsInstance(provider.id_generator, CustomIdGenerator) diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index 346ae555b0..14ba530e83 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -72,7 +72,7 @@ Well known trace exporter names: When present the value is passed on to the relevant exporter initializer as ``service_name`` argument. -* ``--ids-generator`` or ``OTEL_PYTHON_IDS_GENERATOR`` +* ``--id-generator`` or ``OTEL_PYTHON_ID_GENERATOR`` Used to specify which IDs Generator to use for the global Tracer Provider. By default, it will use the random IDs generator. diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index dfd83a4983..b48cc93682 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -21,7 +21,7 @@ from shutil import which from opentelemetry.environment_variables import ( - OTEL_PYTHON_IDS_GENERATOR, + OTEL_PYTHON_ID_GENERATOR, OTEL_PYTHON_SERVICE_NAME, OTEL_TRACES_EXPORTER, ) @@ -51,14 +51,14 @@ def parse_args(): ) parser.add_argument( - "--ids-generator", + "--id-generator", required=False, help=""" The IDs Generator to be used with the Tracer Provider. Examples: - --ids-generator=random + --id-generator=random """, ) @@ -85,8 +85,8 @@ def load_config_from_cli_args(args): environ[OTEL_TRACES_EXPORTER] = args.trace_exporter if args.service_name: environ[OTEL_PYTHON_SERVICE_NAME] = args.service_name - if args.ids_generator: - environ[OTEL_PYTHON_IDS_GENERATOR] = args.ids_generator + if args.id_generator: + environ[OTEL_PYTHON_ID_GENERATOR] = args.id_generator def run() -> None: diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 231b715980..5573fa7127 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -52,8 +52,8 @@ opentelemetry_tracer_provider = sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider opentelemetry_exporter = console_span = opentelemetry.sdk.trace.export:ConsoleSpanExporter -opentelemetry_ids_generator = - random = opentelemetry.sdk.trace.ids_generator:RandomIdsGenerator +opentelemetry_id_generator = + random = opentelemetry.sdk.trace.id_generator:RandomIdGenerator [options.extras_require] test = diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index e3f489706f..ddfc2e2c93 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -47,10 +47,7 @@ ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import sampling -from opentelemetry.sdk.trace.ids_generator import ( - IdsGenerator, - RandomIdsGenerator, -) +from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator from opentelemetry.sdk.util import BoundedDict, BoundedList from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanContext @@ -840,13 +837,13 @@ def __init__( span_processor: Union[ SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor ], - ids_generator: IdsGenerator, + id_generator: IdGenerator, instrumentation_info: InstrumentationInfo, ) -> None: self.sampler = sampler self.resource = resource self.span_processor = span_processor - self.ids_generator = ids_generator + self.id_generator = id_generator self.instrumentation_info = instrumentation_info @contextmanager @@ -901,7 +898,7 @@ def start_span( # pylint: disable=too-many-locals # is_valid determines root span if parent_span_context is None or not parent_span_context.is_valid: parent_span_context = None - trace_id = self.ids_generator.generate_trace_id() + trace_id = self.id_generator.generate_trace_id() trace_flags = None trace_state = None else: @@ -925,7 +922,7 @@ def start_span( # pylint: disable=too-many-locals ) span_context = trace_api.SpanContext( trace_id, - self.ids_generator.generate_span_id(), + self.id_generator.generate_span_id(), is_remote=False, trace_flags=trace_flags, trace_state=sampling_result.trace_state, @@ -1003,15 +1000,15 @@ def __init__( active_span_processor: Union[ SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor ] = None, - ids_generator: IdsGenerator = None, + id_generator: IdGenerator = None, ): self._active_span_processor = ( active_span_processor or SynchronousMultiSpanProcessor() ) - if ids_generator is None: - self.ids_generator = RandomIdsGenerator() + if id_generator is None: + self.id_generator = RandomIdGenerator() else: - self.ids_generator = ids_generator + self.id_generator = id_generator self.resource = resource self.sampler = sampler self._atexit_handler = None @@ -1030,7 +1027,7 @@ def get_tracer( self.sampler, self.resource, self._active_span_processor, - self.ids_generator, + self.id_generator, InstrumentationInfo( instrumenting_module_name, instrumenting_library_version ), diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/ids_generator.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py similarity index 91% rename from opentelemetry-sdk/src/opentelemetry/sdk/trace/ids_generator.py rename to opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py index 4e2a81b731..62b12a9492 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/ids_generator.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py @@ -16,7 +16,7 @@ import random -class IdsGenerator(abc.ABC): +class IdGenerator(abc.ABC): @abc.abstractmethod def generate_span_id(self) -> int: """Get a new span ID. @@ -40,8 +40,8 @@ def generate_trace_id(self) -> int: """ -class RandomIdsGenerator(IdsGenerator): - """The default IDs generator for TracerProvider which randomly generates all +class RandomIdGenerator(IdGenerator): + """The default ID generator for TracerProvider which randomly generates all bits when generating IDs. """ diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 043c6dc0e3..f83d949a85 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -34,7 +34,7 @@ OTEL_TRACES_SAMPLER_ARG, ) from opentelemetry.sdk.trace import Resource, sampling -from opentelemetry.sdk.trace.ids_generator import RandomIdsGenerator +from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace.status import StatusCode @@ -791,15 +791,15 @@ def test_invalid_event_attributes(self): self.assertEqual(root.events[3].attributes, {"attr2": (1, 2)}) def test_links(self): - ids_generator = RandomIdsGenerator() + id_generator = RandomIdGenerator() other_context1 = trace_api.SpanContext( - trace_id=ids_generator.generate_trace_id(), - span_id=ids_generator.generate_span_id(), + trace_id=id_generator.generate_trace_id(), + span_id=id_generator.generate_span_id(), is_remote=False, ) other_context2 = trace_api.SpanContext( - trace_id=ids_generator.generate_trace_id(), - span_id=ids_generator.generate_span_id(), + trace_id=id_generator.generate_trace_id(), + span_id=id_generator.generate_span_id(), is_remote=False, ) @@ -1297,12 +1297,12 @@ class TestSpanLimits(unittest.TestCase): def test_span_environment_limits(self): reload(trace) tracer = new_tracer() - ids_generator = RandomIdsGenerator() + id_generator = RandomIdGenerator() some_links = [ trace_api.Link( trace_api.SpanContext( - trace_id=ids_generator.generate_trace_id(), - span_id=ids_generator.generate_span_id(), + trace_id=id_generator.generate_trace_id(), + span_id=id_generator.generate_span_id(), is_remote=False, ) ) diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py index 7f59ff417b..e872cef9e6 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py @@ -94,9 +94,9 @@ def extract( self._trace_id_regex.fullmatch(trace_id) is None or self._span_id_regex.fullmatch(span_id) is None ): - ids_generator = trace.get_tracer_provider().ids_generator - trace_id = ids_generator.generate_trace_id() - span_id = ids_generator.generate_span_id() + id_generator = trace.get_tracer_provider().id_generator + trace_id = id_generator.generate_trace_id() + span_id = id_generator.generate_span_id() sampled = "0" else: diff --git a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index aea06b47fb..cbf19b02a2 100644 --- a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -17,7 +17,7 @@ import opentelemetry.propagators.b3 as b3_format # pylint: disable=no-name-in-module,import-error import opentelemetry.sdk.trace as trace -import opentelemetry.sdk.trace.ids_generator as ids_generator +import opentelemetry.sdk.trace.id_generator as id_generator import opentelemetry.trace as trace_api from opentelemetry.context import get_current from opentelemetry.trace.propagation.textmap import DictGetter @@ -38,7 +38,7 @@ def get_child_parent_new_carrier(old_carrier): "child", trace_api.SpanContext( parent_span_context.trace_id, - ids_generator.RandomIdsGenerator().generate_span_id(), + id_generator.RandomIdGenerator().generate_span_id(), is_remote=False, trace_flags=parent_span_context.trace_flags, trace_state=parent_span_context.trace_state, @@ -56,15 +56,15 @@ def get_child_parent_new_carrier(old_carrier): class TestB3Format(unittest.TestCase): @classmethod def setUpClass(cls): - id_generator = ids_generator.RandomIdsGenerator() + generator = id_generator.RandomIdGenerator() cls.serialized_trace_id = b3_format.format_trace_id( - id_generator.generate_trace_id() + generator.generate_trace_id() ) cls.serialized_span_id = b3_format.format_span_id( - id_generator.generate_span_id() + generator.generate_span_id() ) cls.serialized_parent_id = b3_format.format_span_id( - id_generator.generate_span_id() + generator.generate_span_id() ) def setUp(self) -> None: @@ -256,10 +256,10 @@ def test_missing_trace_id(self): self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) @patch( - "opentelemetry.sdk.trace.ids_generator.RandomIdsGenerator.generate_trace_id" + "opentelemetry.sdk.trace.id_generator.RandomIdGenerator.generate_trace_id" ) @patch( - "opentelemetry.sdk.trace.ids_generator.RandomIdsGenerator.generate_span_id" + "opentelemetry.sdk.trace.id_generator.RandomIdGenerator.generate_span_id" ) def test_invalid_trace_id( self, mock_generate_span_id, mock_generate_trace_id @@ -282,10 +282,10 @@ def test_invalid_trace_id( self.assertEqual(span_context.span_id, 2) @patch( - "opentelemetry.sdk.trace.ids_generator.RandomIdsGenerator.generate_trace_id" + "opentelemetry.sdk.trace.id_generator.RandomIdGenerator.generate_trace_id" ) @patch( - "opentelemetry.sdk.trace.ids_generator.RandomIdsGenerator.generate_span_id" + "opentelemetry.sdk.trace.id_generator.RandomIdGenerator.generate_span_id" ) def test_invalid_span_id( self, mock_generate_span_id, mock_generate_trace_id diff --git a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py index 46c554d4f5..8512678aee 100644 --- a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py @@ -16,7 +16,7 @@ from unittest.mock import Mock import opentelemetry.sdk.trace as trace -import opentelemetry.sdk.trace.ids_generator as ids_generator +import opentelemetry.sdk.trace.id_generator as id_generator import opentelemetry.trace as trace_api from opentelemetry import baggage from opentelemetry.propagators import ( # pylint: disable=no-name-in-module @@ -43,7 +43,7 @@ def get_context_new_carrier(old_carrier, carrier_baggage=None): "child", trace_api.SpanContext( parent_span_context.trace_id, - ids_generator.RandomIdsGenerator().generate_span_id(), + id_generator.RandomIdGenerator().generate_span_id(), is_remote=False, trace_flags=parent_span_context.trace_flags, trace_state=parent_span_context.trace_state, @@ -68,10 +68,10 @@ def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags): class TestJaegerPropagator(unittest.TestCase): @classmethod def setUpClass(cls): - id_generator = ids_generator.RandomIdsGenerator() - cls.trace_id = id_generator.generate_trace_id() - cls.span_id = id_generator.generate_span_id() - cls.parent_span_id = id_generator.generate_span_id() + generator = id_generator.RandomIdGenerator() + cls.trace_id = generator.generate_trace_id() + cls.span_id = generator.generate_span_id() + cls.parent_span_id = generator.generate_span_id() cls.serialized_uber_trace_id = _format_uber_trace_id( cls.trace_id, cls.span_id, cls.parent_span_id, 11 ) From e503ef36e62d6cb53da3a6e728c31033ee8db83c Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 1 Mar 2021 08:12:00 +0530 Subject: [PATCH 0787/1517] Rename Resource's create_empty to get_empty (#1653) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/resources/__init__.py | 2 +- opentelemetry-sdk/tests/resources/test_resources.py | 10 ++++------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ddc4e879b..339d4b8674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Rename `IdsGenerator` to `IdGenerator` ([#1651])(https://github.com/open-telemetry/opentelemetry-python/pull/1651) +- Rename Resource's `create_empty` to `get_empty` + ([#1653])(https://github.com/open-telemetry/opentelemetry-python/pull/1653) ## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index c840e5f298..a7fa6dd54c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -188,7 +188,7 @@ def create(attributes: typing.Optional[Attributes] = None) -> "Resource": return resource @staticmethod - def create_empty() -> "Resource": + def get_empty() -> "Resource": return _EMPTY_RESOURCE @property diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 3effa5d245..4151c2fbd8 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -59,7 +59,7 @@ def test_create(self): self.assertEqual(resource.attributes, expected_with_envar) os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" - resource = resources.Resource.create_empty() + resource = resources.Resource.get_empty() self.assertEqual(resource, resources._EMPTY_RESOURCE) resource = resources.Resource.create(None) @@ -140,9 +140,7 @@ def test_service_name_using_process_name(self): def test_aggregated_resources_no_detectors(self): aggregated_resources = resources.get_aggregated_resources([]) - self.assertEqual( - aggregated_resources, resources.Resource.create_empty() - ) + self.assertEqual(aggregated_resources, resources.Resource.get_empty()) def test_aggregated_resources_with_static_resource(self): static_resource = resources.Resource({"static_key": "static_value"}) @@ -208,7 +206,7 @@ def test_resource_detector_ignore_error(self): resource_detector.raise_on_error = False self.assertEqual( resources.get_aggregated_resources([resource_detector]), - resources.Resource.create_empty(), + resources.Resource.get_empty(), ) def test_resource_detector_raise_error(self): @@ -245,7 +243,7 @@ def tearDown(self) -> None: def test_empty(self): detector = resources.OTELResourceDetector() os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" - self.assertEqual(detector.detect(), resources.Resource.create_empty()) + self.assertEqual(detector.detect(), resources.Resource.get_empty()) def test_one(self): detector = resources.OTELResourceDetector() From 0c654f25dabf4237fb643ec03169fbed935fea10 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 1 Mar 2021 23:04:57 +0530 Subject: [PATCH 0788/1517] Make TracerProvider's resource private (#1652) --- CHANGELOG.md | 2 ++ opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 339d4b8674..43efa6ad00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Rename `IdsGenerator` to `IdGenerator` ([#1651])(https://github.com/open-telemetry/opentelemetry-python/pull/1651) +- Make TracerProvider's resource attribute private + ([#1652])(https://github.com/open-telemetry/opentelemetry-python/pull/1652) - Rename Resource's `create_empty` to `get_empty` ([#1653])(https://github.com/open-telemetry/opentelemetry-python/pull/1653) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index ddfc2e2c93..9599fbd976 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -1009,12 +1009,16 @@ def __init__( self.id_generator = RandomIdGenerator() else: self.id_generator = id_generator - self.resource = resource + self._resource = resource self.sampler = sampler self._atexit_handler = None if shutdown_on_exit: self._atexit_handler = atexit.register(self.shutdown) + @property + def resource(self) -> Resource: + return self._resource + def get_tracer( self, instrumenting_module_name: str, From 326b7b0d0e9bc1cfc21d70d3d09e58b38c5cc8a3 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Mon, 1 Mar 2021 23:26:10 +0530 Subject: [PATCH 0789/1517] Add `max_tag_value_length` support to Jaeger exporter (#1633) This commit adds ability to specifiy maximum length for string attributes to the Jaeger exporter. This is similar to how Zipkin exporter supports `max_tag_value_length`. --- CHANGELOG.md | 4 ++ .../opentelemetry/exporter/jaeger/__init__.py | 10 +++- .../exporter/jaeger/translate/__init__.py | 4 ++ .../exporter/jaeger/translate/protobuf.py | 35 ++++++++--- .../exporter/jaeger/translate/thrift.py | 21 +++++-- .../tests/test_jaeger_exporter_protobuf.py | 58 +++++++++++++++++++ .../tests/test_jaeger_exporter_thrift.py | 55 ++++++++++++++++++ 7 files changed, 171 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43efa6ad00..df4d319ef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.18b0...HEAD) +### Added +- Add `max_attr_value_length` support to Jaeger exporter + ([#1633])(https://github.com/open-telemetry/opentelemetry-python/pull/1633) + ### Changed - Rename `IdsGenerator` to `IdGenerator` ([#1651])(https://github.com/open-telemetry/opentelemetry-python/pull/1651) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index 20b4bda3d9..69102db1d9 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -47,6 +47,7 @@ # insecure=True, # optional # credentials=xxx # optional channel creds # transport_format='protobuf' # optional + # max_tag_value_length=None # optional ) # Create a BatchExportSpanProcessor and add the exporter to it @@ -120,6 +121,7 @@ class JaegerSpanExporter(SpanExporter): insecure: True if collector has no encryption or authentication credentials: Credentials for server authentication. transport_format: Transport format for exporting spans to collector. + max_tag_value_length: Max length string attribute values can have. Set to None to disable. """ def __init__( @@ -133,8 +135,10 @@ def __init__( insecure: Optional[bool] = None, credentials: Optional[ChannelCredentials] = None, transport_format: Optional[str] = None, + max_tag_value_length: Optional[int] = None, ): self.service_name = service_name + self._max_tag_value_length = max_tag_value_length self.agent_host_name = _parameter_setter( param=agent_host_name, env_variable=environ.get(OTEL_EXPORTER_JAEGER_AGENT_HOST), @@ -220,13 +224,15 @@ def _collector_http_client(self) -> Optional[Collector]: def export(self, spans) -> SpanExportResult: translator = Translate(spans) if self.transport_format == TRANSPORT_FORMAT_PROTOBUF: - pb_translator = ProtobufTranslator(self.service_name) + pb_translator = ProtobufTranslator( + self.service_name, self._max_tag_value_length + ) jaeger_spans = translator._translate(pb_translator) batch = model_pb2.Batch(spans=jaeger_spans) request = PostSpansRequest(batch=batch) self._collector_grpc_client.PostSpans(request) else: - thrift_translator = ThriftTranslator() + thrift_translator = ThriftTranslator(self._max_tag_value_length) jaeger_spans = translator._translate(thrift_translator) batch = jaeger_thrift.Batch( spans=jaeger_spans, diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py index 853da9ac6d..c60820085a 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. import abc +from typing import Optional from opentelemetry.trace import SpanKind @@ -41,6 +42,9 @@ def _convert_int_to_i64(val): class Translator(abc.ABC): + def __init__(self, max_tag_value_length: Optional[int] = None): + self._max_tag_value_length = max_tag_value_length + @abc.abstractmethod def _translate_span(self, span): """Translates span to jaeger format. diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py index f97977516d..011e24f17f 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py @@ -76,24 +76,31 @@ def _get_binary_key_value(key: str, value: bytes) -> model_pb2.KeyValue: def _translate_attribute( - key: str, value: types.AttributeValue + key: str, value: types.AttributeValue, max_length: Optional[int] ) -> Optional[model_pb2.KeyValue]: """Convert the attributes to jaeger keyvalues.""" translated = None if isinstance(value, bool): translated = _get_bool_key_value(key, value) elif isinstance(value, str): + if max_length is not None: + value = value[:max_length] translated = _get_string_key_value(key, value) elif isinstance(value, int): translated = _get_long_key_value(key, value) elif isinstance(value, float): translated = _get_double_key_value(key, value) elif isinstance(value, tuple): - translated = _get_string_key_value(key, str(value)) + value = str(value) + if max_length is not None: + value = value[:max_length] + translated = _get_string_key_value(key, value) return translated -def _extract_resource_tags(span: ReadableSpan) -> Sequence[model_pb2.KeyValue]: +def _extract_resource_tags( + span: ReadableSpan, max_tag_value_length: Optional[int] +) -> Sequence[model_pb2.KeyValue]: """Extracts resource attributes from span and returns list of jaeger keyvalues. @@ -102,7 +109,7 @@ def _extract_resource_tags(span: ReadableSpan) -> Sequence[model_pb2.KeyValue]: """ tags = [] for key, value in span.resource.attributes.items(): - tag = _translate_attribute(key, value) + tag = _translate_attribute(key, value, max_tag_value_length) if tag: tags.append(tag) return tags @@ -140,7 +147,10 @@ def _proto_timestamp_from_epoch_nanos(nsec: int) -> Timestamp: class ProtobufTranslator(Translator): - def __init__(self, svc_name): + def __init__( + self, svc_name: str, max_tag_value_length: Optional[int] = None + ): + super().__init__(max_tag_value_length) self.svc_name = svc_name def _translate_span(self, span: ReadableSpan) -> model_pb2.Span: @@ -161,7 +171,8 @@ def _translate_span(self, span: ReadableSpan) -> model_pb2.Span: flags = int(ctx.trace_flags) process = model_pb2.Process( - service_name=self.svc_name, tags=_extract_resource_tags(span) + service_name=self.svc_name, + tags=_extract_resource_tags(span, self._max_tag_value_length), ) jaeger_span = model_pb2.Span( trace_id=trace_id, @@ -183,12 +194,16 @@ def _extract_tags( translated = [] if span.attributes: for key, value in span.attributes.items(): - key_value = _translate_attribute(key, value) + key_value = _translate_attribute( + key, value, self._max_tag_value_length + ) if key_value is not None: translated.append(key_value) if span.resource.attributes: for key, value in span.resource.attributes.items(): - key_value = _translate_attribute(key, value) + key_value = _translate_attribute( + key, value, self._max_tag_value_length + ) if key_value: translated.append(key_value) @@ -256,7 +271,9 @@ def _extract_logs( for event in span.events: fields = [] for key, value in event.attributes.items(): - tag = _translate_attribute(key, value) + tag = _translate_attribute( + key, value, self._max_tag_value_length + ) if tag: fields.append(tag) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py index 9df9c71688..45129601ac 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py @@ -58,19 +58,24 @@ def _get_trace_id_high(trace_id): def _translate_attribute( - key: str, value: types.AttributeValue + key: str, value: types.AttributeValue, max_length: Optional[int] ) -> Optional[TCollector.Tag]: """Convert the attributes to jaeger tags.""" if isinstance(value, bool): return _get_bool_tag(key, value) if isinstance(value, str): + if max_length is not None: + value = value[:max_length] return _get_string_tag(key, value) if isinstance(value, int): return _get_long_tag(key, value) if isinstance(value, float): return _get_double_tag(key, value) if isinstance(value, tuple): - return _get_string_tag(key, str(value)) + value = str(value) + if max_length is not None: + value = value[:max_length] + return _get_string_tag(key, value) return None @@ -111,12 +116,16 @@ def _extract_tags(self, span: ReadableSpan) -> Sequence[TCollector.Tag]: translated = [] if span.attributes: for key, value in span.attributes.items(): - tag = _translate_attribute(key, value) + tag = _translate_attribute( + key, value, self._max_tag_value_length + ) if tag: translated.append(tag) if span.resource.attributes: for key, value in span.resource.attributes.items(): - tag = _translate_attribute(key, value) + tag = _translate_attribute( + key, value, self._max_tag_value_length + ) if tag: translated.append(tag) @@ -185,7 +194,9 @@ def _extract_logs( for event in span.events: fields = [] for key, value in event.attributes.items(): - tag = _translate_attribute(key, value) + tag = _translate_attribute( + key, value, self._max_tag_value_length + ) if tag: fields.append(tag) diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py index 80cbdfb55d..eea3e54700 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py @@ -385,3 +385,61 @@ def test_translate_to_jaeger(self): ) self.assertEqual(spans, expected_spans) + + def test_max_tag_value_length(self): + span = trace._Span( + name="span", + resource=Resource( + attributes={ + "key_resource": "some_resource some_resource some_more_resource" + } + ), + context=trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + ), + ) + + span.start() + span.set_attribute("key_bool", False) + span.set_attribute("key_string", "hello_world hello_world hello_world") + span.set_attribute("key_float", 111.22) + span.set_attribute("key_int", 1100) + span.set_attribute("key_tuple", ("tuple_element", "tuple_element2")) + span.end() + + translate = Translate([span]) + + # does not truncate by default + # pylint: disable=protected-access + spans = translate._translate(pb_translator.ProtobufTranslator("svc")) + tags_by_keys = { + tag.key: tag.v_str + for tag in spans[0].tags + if tag.v_type == model_pb2.ValueType.STRING + } + self.assertEqual( + "hello_world hello_world hello_world", tags_by_keys["key_string"] + ) + self.assertEqual( + "('tuple_element', 'tuple_element2')", tags_by_keys["key_tuple"] + ) + self.assertEqual( + "some_resource some_resource some_more_resource", + tags_by_keys["key_resource"], + ) + + # truncates when max_tag_value_length is passed + # pylint: disable=protected-access + spans = translate._translate( + pb_translator.ProtobufTranslator("svc", max_tag_value_length=5) + ) + tags_by_keys = { + tag.key: tag.v_str + for tag in spans[0].tags + if tag.v_type == model_pb2.ValueType.STRING + } + self.assertEqual("hello", tags_by_keys["key_string"]) + self.assertEqual("('tup", tags_by_keys["key_tuple"]) + self.assertEqual("some_", tags_by_keys["key_resource"]) diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py index c0faafc1b6..0bee785760 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py @@ -67,6 +67,7 @@ def test_constructor_default(self): self.assertEqual(exporter.password, None) self.assertTrue(exporter._collector_http_client is None) self.assertTrue(exporter._agent_client is not None) + self.assertIsNone(exporter._max_tag_value_length) def test_constructor_explicit(self): # pylint: disable=protected-access @@ -88,6 +89,7 @@ def test_constructor_explicit(self): collector_endpoint=collector_endpoint, username=username, password=password, + max_tag_value_length=42, ) self.assertEqual(exporter.service_name, service) @@ -104,6 +106,7 @@ def test_constructor_explicit(self): exporter.password = None self.assertNotEqual(exporter._collector_http_client, collector) self.assertTrue(exporter._collector_http_client.auth is None) + self.assertEqual(exporter._max_tag_value_length, 42) def test_constructor_by_environment_variables(self): # pylint: disable=protected-access @@ -465,3 +468,55 @@ def test_agent_client(self): ) agent_client.emit(batch) + + def test_max_tag_value_length(self): + span = trace._Span( + name="span", + resource=Resource( + attributes={ + "key_resource": "some_resource some_resource some_more_resource" + } + ), + context=trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + ), + ) + + span.start() + span.set_attribute("key_bool", False) + span.set_attribute("key_string", "hello_world hello_world hello_world") + span.set_attribute("key_float", 111.22) + span.set_attribute("key_int", 1100) + span.set_attribute("key_tuple", ("tuple_element", "tuple_element2")) + span.end() + + translate = Translate([span]) + + # does not truncate by default + # pylint: disable=protected-access + spans = translate._translate(ThriftTranslator()) + tags_by_keys = { + tag.key: tag.vStr for tag in spans[0].tags if tag.vType == 0 + } + self.assertEqual( + "hello_world hello_world hello_world", tags_by_keys["key_string"] + ) + self.assertEqual( + "('tuple_element', 'tuple_element2')", tags_by_keys["key_tuple"] + ) + self.assertEqual( + "some_resource some_resource some_more_resource", + tags_by_keys["key_resource"], + ) + + # truncates when max_tag_value_length is passed + # pylint: disable=protected-access + spans = translate._translate(ThriftTranslator(max_tag_value_length=5)) + tags_by_keys = { + tag.key: tag.vStr for tag in spans[0].tags if tag.vType == 0 + } + self.assertEqual("hello", tags_by_keys["key_string"]) + self.assertEqual("('tup", tags_by_keys["key_tuple"]) + self.assertEqual("some_", tags_by_keys["key_resource"]) From 489a81e5d33690ec10f2d7606c7137168f85c17a Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 1 Mar 2021 13:50:35 -0800 Subject: [PATCH 0790/1517] rename span processor (#1656) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 3 + docs/examples/auto-instrumentation/client.py | 4 +- .../server_instrumented.py | 4 +- .../server_uninstrumented.py | 4 +- docs/examples/basic_tracer/basic_trace.py | 4 +- docs/examples/basic_tracer/resources.py | 4 +- docs/examples/django/client.py | 4 +- docs/examples/django/pages/views.py | 4 +- .../opencensus-exporter-tracer/collector.py | 4 +- docs/examples/opentracing/main.py | 4 +- docs/getting_started/flask_example.py | 4 +- docs/getting_started/jaeger_example.py | 4 +- docs/getting_started/otlpcollector_example.py | 4 +- docs/getting_started/tracing_example.py | 4 +- .../examples/jaeger_exporter_example.py | 6 +- .../opentelemetry/exporter/jaeger/__init__.py | 6 +- .../opentelemetry/exporter/otlp/__init__.py | 4 +- .../test_benchmark_trace_exporter.py | 10 ++-- .../tests/test_otlp_trace_exporter.py | 6 +- .../opentelemetry/exporter/zipkin/__init__.py | 6 +- .../src/opentelemetry/distro/__init__.py | 7 +-- .../tests/test_configurator.py | 2 +- .../sdk/trace/export/__init__.py | 12 ++-- .../src/opentelemetry/sdk/trace/sampling.py | 8 +-- .../tests/context/test_asyncio.py | 2 +- .../profile_resource_usage_batch_export.py | 4 +- .../profile_resource_usage_simple_export.py | 4 +- .../tests/trace/export/test_export.py | 55 +++++++++---------- .../export/test_in_memory_span_exporter.py | 2 +- .../tests/testbed/otel_ot_shim_tracer.py | 4 +- .../test_opencensusexporter_functional.py | 4 +- .../src/opentelemetry/test/spantestutil.py | 2 +- .../util/src/opentelemetry/test/test_base.py | 4 +- tests/w3c_tracecontext_validation_server.py | 4 +- 35 files changed, 101 insertions(+), 108 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34e11fc67e..236cd3fd91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: c86562fcddcf8fd037492de3880790776abe79ce + CONTRIB_REPO_SHA: 63a755348c2c9a2b83da85a5c72487ddc507b8b4 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index df4d319ef0..e09bf17a4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1652])(https://github.com/open-telemetry/opentelemetry-python/pull/1652) - Rename Resource's `create_empty` to `get_empty` ([#1653])(https://github.com/open-telemetry/opentelemetry-python/pull/1653) +- Renamed `BatchExportSpanProcessor` to `BatchSpanProcessor` and `SimpleExportSpanProcessor` to + `SimpleSpanProcessor` + ([#1656])(https://github.com/open-telemetry/opentelemetry-python/pull/1656) ## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 diff --git a/docs/examples/auto-instrumentation/client.py b/docs/examples/auto-instrumentation/client.py index 8cef055b35..fefc1f67b9 100644 --- a/docs/examples/auto-instrumentation/client.py +++ b/docs/examples/auto-instrumentation/client.py @@ -20,14 +20,14 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleExportSpanProcessor, + SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer_provider().get_tracer(__name__) trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) + SimpleSpanProcessor(ConsoleSpanExporter()) ) diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_instrumented.py index 66bb0b584a..5bf5e8dafe 100644 --- a/docs/examples/auto-instrumentation/server_instrumented.py +++ b/docs/examples/auto-instrumentation/server_instrumented.py @@ -20,7 +20,7 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleExportSpanProcessor, + SimpleSpanProcessor, ) from opentelemetry.trace.propagation.textmap import DictGetter @@ -30,7 +30,7 @@ tracer = trace.get_tracer_provider().get_tracer(__name__) trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) + SimpleSpanProcessor(ConsoleSpanExporter()) ) diff --git a/docs/examples/auto-instrumentation/server_uninstrumented.py b/docs/examples/auto-instrumentation/server_uninstrumented.py index b8360341ab..5edcd5cf60 100644 --- a/docs/examples/auto-instrumentation/server_uninstrumented.py +++ b/docs/examples/auto-instrumentation/server_uninstrumented.py @@ -18,7 +18,7 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleExportSpanProcessor, + SimpleSpanProcessor, ) app = Flask(__name__) @@ -26,7 +26,7 @@ trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) + SimpleSpanProcessor(ConsoleSpanExporter()) ) diff --git a/docs/examples/basic_tracer/basic_trace.py b/docs/examples/basic_tracer/basic_trace.py index 5fcfe51d19..54fcb1c3c6 100644 --- a/docs/examples/basic_tracer/basic_trace.py +++ b/docs/examples/basic_tracer/basic_trace.py @@ -16,12 +16,12 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleExportSpanProcessor, + SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) + SimpleSpanProcessor(ConsoleSpanExporter()) ) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("foo"): diff --git a/docs/examples/basic_tracer/resources.py b/docs/examples/basic_tracer/resources.py index 1d1a6cd047..d69b689aa6 100644 --- a/docs/examples/basic_tracer/resources.py +++ b/docs/examples/basic_tracer/resources.py @@ -17,7 +17,7 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleExportSpanProcessor, + SimpleSpanProcessor, ) resource = Resource.create({"service.name": "basic_service"}) @@ -25,7 +25,7 @@ trace.set_tracer_provider(TracerProvider(resource=resource)) trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) + SimpleSpanProcessor(ConsoleSpanExporter()) ) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("foo"): diff --git a/docs/examples/django/client.py b/docs/examples/django/client.py index d32be31a85..bc3606cbe7 100644 --- a/docs/examples/django/client.py +++ b/docs/examples/django/client.py @@ -21,14 +21,14 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleExportSpanProcessor, + SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer_provider().get_tracer(__name__) trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) + SimpleSpanProcessor(ConsoleSpanExporter()) ) diff --git a/docs/examples/django/pages/views.py b/docs/examples/django/pages/views.py index 4083888e17..e69de63f09 100644 --- a/docs/examples/django/pages/views.py +++ b/docs/examples/django/pages/views.py @@ -17,13 +17,13 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleExportSpanProcessor, + SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) + SimpleSpanProcessor(ConsoleSpanExporter()) ) diff --git a/docs/examples/opencensus-exporter-tracer/collector.py b/docs/examples/opencensus-exporter-tracer/collector.py index d404cfbce5..fdc45d7162 100644 --- a/docs/examples/opencensus-exporter-tracer/collector.py +++ b/docs/examples/opencensus-exporter-tracer/collector.py @@ -19,7 +19,7 @@ OpenCensusSpanExporter, ) from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchExportSpanProcessor +from opentelemetry.sdk.trace.export import BatchSpanProcessor exporter = OpenCensusSpanExporter( service_name="basic-service", endpoint="localhost:55678" @@ -27,7 +27,7 @@ trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) -span_processor = BatchExportSpanProcessor(exporter) +span_processor = BatchSpanProcessor(exporter) trace.get_tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): diff --git a/docs/examples/opentracing/main.py b/docs/examples/opentracing/main.py index 7de4b33730..f997801aaa 100755 --- a/docs/examples/opentracing/main.py +++ b/docs/examples/opentracing/main.py @@ -5,7 +5,7 @@ from opentelemetry import trace from opentelemetry.exporter.jaeger import JaegerSpanExporter from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.shim import opentracing_shim # Configure the tracer using the default implementation @@ -18,7 +18,7 @@ agent_host_name="localhost", agent_port=6831, ) -span_processor = SimpleExportSpanProcessor(jaeger_exporter) +span_processor = SimpleSpanProcessor(jaeger_exporter) tracer_provider.add_span_processor(span_processor) # Create an OpenTracing shim. This implements the OpenTracing tracer API, but diff --git a/docs/getting_started/flask_example.py b/docs/getting_started/flask_example.py index c4a172389c..2b60d4def8 100644 --- a/docs/getting_started/flask_example.py +++ b/docs/getting_started/flask_example.py @@ -22,12 +22,12 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleExportSpanProcessor, + SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) + SimpleSpanProcessor(ConsoleSpanExporter()) ) app = flask.Flask(__name__) diff --git a/docs/getting_started/jaeger_example.py b/docs/getting_started/jaeger_example.py index f9c8e5cd05..0494333d2c 100644 --- a/docs/getting_started/jaeger_example.py +++ b/docs/getting_started/jaeger_example.py @@ -16,7 +16,7 @@ from opentelemetry import trace from opentelemetry.exporter import jaeger from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchExportSpanProcessor +from opentelemetry.sdk.trace.export import BatchSpanProcessor trace.set_tracer_provider(TracerProvider()) @@ -27,7 +27,7 @@ ) trace.get_tracer_provider().add_span_processor( - BatchExportSpanProcessor(jaeger_exporter) + BatchSpanProcessor(jaeger_exporter) ) tracer = trace.get_tracer(__name__) diff --git a/docs/getting_started/otlpcollector_example.py b/docs/getting_started/otlpcollector_example.py index d31fdf0bd8..4ca7ade35f 100644 --- a/docs/getting_started/otlpcollector_example.py +++ b/docs/getting_started/otlpcollector_example.py @@ -18,7 +18,7 @@ from opentelemetry import trace from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchExportSpanProcessor +from opentelemetry.sdk.trace.export import BatchSpanProcessor span_exporter = OTLPSpanExporter( # optional @@ -28,7 +28,7 @@ ) tracer_provider = TracerProvider() trace.set_tracer_provider(tracer_provider) -span_processor = BatchExportSpanProcessor(span_exporter) +span_processor = BatchSpanProcessor(span_exporter) tracer_provider.add_span_processor(span_processor) # Configure the tracer to use the collector exporter diff --git a/docs/getting_started/tracing_example.py b/docs/getting_started/tracing_example.py index 5d449458b4..6bb8f18e8a 100644 --- a/docs/getting_started/tracing_example.py +++ b/docs/getting_started/tracing_example.py @@ -17,12 +17,12 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleExportSpanProcessor, + SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) + SimpleSpanProcessor(ConsoleSpanExporter()) ) tracer = trace.get_tracer(__name__) diff --git a/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py b/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py index 0d312676b9..0ebe1c874c 100644 --- a/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py +++ b/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py @@ -3,7 +3,7 @@ from opentelemetry import trace from opentelemetry.exporter import jaeger from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchExportSpanProcessor +from opentelemetry.sdk.trace.export import BatchSpanProcessor trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) @@ -35,8 +35,8 @@ # transport_format="protobuf", # ) -# create a BatchExportSpanProcessor and add the exporter to it -span_processor = BatchExportSpanProcessor(jaeger_exporter) +# create a BatchSpanProcessor and add the exporter to it +span_processor = BatchSpanProcessor(jaeger_exporter) # add to the tracer factory trace.get_tracer_provider().add_span_processor(span_processor) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index 69102db1d9..be81bfaf0b 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -29,7 +29,7 @@ from opentelemetry import trace from opentelemetry.exporter import jaeger from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + from opentelemetry.sdk.trace.export import BatchSpanProcessor trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) @@ -50,8 +50,8 @@ # max_tag_value_length=None # optional ) - # Create a BatchExportSpanProcessor and add the exporter to it - span_processor = BatchExportSpanProcessor(jaeger_exporter) + # Create a BatchSpanProcessor and add the exporter to it + span_processor = BatchSpanProcessor(jaeger_exporter) # add to the tracer trace.get_tracer_provider().add_span_processor(span_processor) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py index abc4186b71..b1db06b295 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py @@ -42,7 +42,7 @@ from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + from opentelemetry.sdk.trace.export import BatchSpanProcessor # Resource can be required for some backends, e.g. Jaeger # If resource wouldn't be set - traces wouldn't appears in Jaeger @@ -55,7 +55,7 @@ otlp_exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True) - span_processor = BatchExportSpanProcessor(otlp_exporter) + span_processor = BatchSpanProcessor(otlp_exporter) trace.get_tracer_provider().add_span_processor(span_processor) diff --git a/exporter/opentelemetry-exporter-otlp/tests/performance/benchmarks/test_benchmark_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/performance/benchmarks/test_benchmark_trace_exporter.py index 00c22f0359..7434a0d8d0 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/performance/benchmarks/test_benchmark_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/performance/benchmarks/test_benchmark_trace_exporter.py @@ -17,8 +17,8 @@ from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace import TracerProvider, sampling from opentelemetry.sdk.trace.export import ( - BatchExportSpanProcessor, - SimpleExportSpanProcessor, + BatchSpanProcessor, + SimpleSpanProcessor, ) @@ -40,7 +40,7 @@ def __init__(self, channel): new=MockTraceServiceStub, ) def test_simple_span_processor(benchmark): - tracer = get_tracer_with_processor(SimpleExportSpanProcessor) + tracer = get_tracer_with_processor(SimpleSpanProcessor) def create_spans_to_be_exported(): span = tracer.start_span("benchmarkedSpan",) @@ -59,14 +59,14 @@ def create_spans_to_be_exported(): new=MockTraceServiceStub, ) def test_batch_span_processor(benchmark): - """Runs benchmark tests using BatchExportSpanProcessor. + """Runs benchmark tests using BatchSpanProcessor. One particular call by pytest-benchmark will be much more expensive since the batch export thread will activate and consume a lot of CPU to process all the spans. For this reason, focus on the average measurement. Do not focus on the min/max measurements which will be misleading. """ - tracer = get_tracer_with_processor(BatchExportSpanProcessor) + tracer = get_tracer_with_processor(BatchSpanProcessor) def create_spans_to_be_exported(): span = tracer.start_span("benchmarkedSpan",) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index a1dbcfbd63..be35f30623 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -56,7 +56,7 @@ from opentelemetry.sdk.trace import StatusCode as SDKStatusCode from opentelemetry.sdk.trace import TracerProvider, _Span from opentelemetry.sdk.trace.export import ( - SimpleExportSpanProcessor, + SimpleSpanProcessor, SpanExportResult, ) from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -114,9 +114,7 @@ class TestOTLPSpanExporter(TestCase): def setUp(self): tracer_provider = TracerProvider() self.exporter = OTLPSpanExporter(insecure=True) - tracer_provider.add_span_processor( - SimpleExportSpanProcessor(self.exporter) - ) + tracer_provider.add_span_processor(SimpleSpanProcessor(self.exporter)) self.tracer = tracer_provider.get_tracer(__name__) self.server = server(ThreadPoolExecutor(max_workers=10)) diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 1b2554923f..3dce5e4ed2 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -34,7 +34,7 @@ from opentelemetry import trace from opentelemetry.exporter import zipkin from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + from opentelemetry.sdk.trace.export import BatchSpanProcessor trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) @@ -50,8 +50,8 @@ # max_tag_value_length=256 ) - # Create a BatchExportSpanProcessor and add the exporter to it - span_processor = BatchExportSpanProcessor(zipkin_exporter) + # Create a BatchSpanProcessor and add the exporter to it + span_processor = BatchSpanProcessor(zipkin_exporter) # add to the tracer trace.get_tracer_provider().add_span_processor(span_processor) diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index 51464d51a5..666ab57bb3 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -29,10 +29,7 @@ from opentelemetry.instrumentation.distro import BaseDistro from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - BatchExportSpanProcessor, - SpanExporter, -) +from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator logger = getLogger(__file__) @@ -95,7 +92,7 @@ def _init_tracing( exporter_args["service_name"] = service_name provider.add_span_processor( - BatchExportSpanProcessor(exporter_class(**exporter_args)) + BatchSpanProcessor(exporter_class(**exporter_args)) ) diff --git a/opentelemetry-distro/tests/test_configurator.py b/opentelemetry-distro/tests/test_configurator.py index b6b80b2e60..91ca82da86 100644 --- a/opentelemetry-distro/tests/test_configurator.py +++ b/opentelemetry-distro/tests/test_configurator.py @@ -81,7 +81,7 @@ def setUp(self): "opentelemetry.distro.TracerProvider", Provider ) self.get_processor_patcher = patch( - "opentelemetry.distro.BatchExportSpanProcessor", Processor + "opentelemetry.distro.BatchSpanProcessor", Processor ) self.set_provider_patcher = patch( "opentelemetry.trace.set_tracer_provider" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 18f1465473..457c7f4d6e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -46,7 +46,7 @@ class SpanExporter: in their own format. To export data this MUST be registered to the :class`opentelemetry.sdk.trace.Tracer` using a - `SimpleExportSpanProcessor` or a `BatchExportSpanProcessor`. + `SimpleSpanProcessor` or a `BatchSpanProcessor`. """ def export( @@ -68,10 +68,10 @@ def shutdown(self) -> None: """ -class SimpleExportSpanProcessor(SpanProcessor): +class SimpleSpanProcessor(SpanProcessor): """Simple SpanProcessor implementation. - SimpleExportSpanProcessor is an implementation of `SpanProcessor` that + SimpleSpanProcessor is an implementation of `SpanProcessor` that passes ended spans directly to the configured `SpanExporter`. """ @@ -103,7 +103,7 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: class _FlushRequest: - """Represents a request for the BatchExportSpanProcessor to flush spans.""" + """Represents a request for the BatchSpanProcessor to flush spans.""" __slots__ = ["event", "num_spans"] @@ -112,10 +112,10 @@ def __init__(self): self.num_spans = 0 -class BatchExportSpanProcessor(SpanProcessor): +class BatchSpanProcessor(SpanProcessor): """Batch span processor implementation. - BatchExportSpanProcessor is an implementation of `SpanProcessor` that + BatchSpanProcessor is an implementation of `SpanProcessor` that batches ended spans and pushes them to the configured `SpanExporter`. """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index cc860fc7a7..81051f4b03 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -41,7 +41,7 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleExportSpanProcessor, + SimpleSpanProcessor, ) from opentelemetry.sdk.trace.sampling import TraceIdRatioBased @@ -53,7 +53,7 @@ # set up an exporter for sampled spans trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) + SimpleSpanProcessor(ConsoleSpanExporter()) ) # created spans will now be sampled by the TraceIdRatioBased sampler @@ -81,14 +81,14 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleExportSpanProcessor, + SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) # set up an exporter for sampled spans trace.get_tracer_provider().add_span_processor( - SimpleExportSpanProcessor(ConsoleSpanExporter()) + SimpleSpanProcessor(ConsoleSpanExporter()) ) # created spans will now be sampled by the TraceIdRatioBased sampler with rate 1/1000. diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index 7f9539738c..a9b32e10fa 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -68,7 +68,7 @@ def setUp(self): self.tracer_provider = trace.TracerProvider() self.tracer = self.tracer_provider.get_tracer(__name__) self.memory_exporter = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) + span_processor = export.SimpleSpanProcessor(self.memory_exporter) self.tracer_provider.add_span_processor(span_processor) self.loop = asyncio.get_event_loop() diff --git a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py index 08e3d77245..570965cd53 100644 --- a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py +++ b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py @@ -17,7 +17,7 @@ from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace import TracerProvider, sampling -from opentelemetry.sdk.trace.export import BatchExportSpanProcessor +from opentelemetry.sdk.trace.export import BatchSpanProcessor TEST_DURATION_SECONDS = 15 SPANS_PER_SECOND = 10_000 @@ -31,7 +31,7 @@ def __init__(self, channel): old_stub = OTLPSpanExporter._stub OTLPSpanExporter._stub = MockTraceServiceStub -simple_span_processor = BatchExportSpanProcessor(OTLPSpanExporter()) +simple_span_processor = BatchSpanProcessor(OTLPSpanExporter()) tracer = TracerProvider( active_span_processor=simple_span_processor, sampler=sampling.DEFAULT_ON, ).get_tracer("resource_usage_tracer") diff --git a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py index f9dc460b00..4636b31d5c 100644 --- a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py +++ b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py @@ -17,7 +17,7 @@ from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace import TracerProvider, sampling -from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.sdk.trace.export import SimpleSpanProcessor TEST_DURATION_SECONDS = 15 SPANS_PER_SECOND = 10_000 @@ -31,7 +31,7 @@ def __init__(self, channel): old_stub = OTLPSpanExporter._stub OTLPSpanExporter._stub = MockTraceServiceStub -simple_span_processor = SimpleExportSpanProcessor(OTLPSpanExporter()) +simple_span_processor = SimpleSpanProcessor(OTLPSpanExporter()) tracer = TracerProvider( active_span_processor=simple_span_processor, sampler=sampling.DEFAULT_ON, ).get_tracer("resource_usage_tracer") diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 86cd990cca..831105b8ff 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -64,7 +64,7 @@ def shutdown(self): self.is_shutdown = True -class TestSimpleExportSpanProcessor(unittest.TestCase): +class TestSimpleSpanProcessor(unittest.TestCase): def test_simple_span_processor(self): tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) @@ -72,7 +72,7 @@ def test_simple_span_processor(self): spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) - span_processor = export.SimpleExportSpanProcessor(my_exporter) + span_processor = export.SimpleSpanProcessor(my_exporter) tracer_provider.add_span_processor(span_processor) with tracer.start_as_current_span("foo"): @@ -97,7 +97,7 @@ def test_simple_span_processor_no_context(self): spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) - span_processor = export.SimpleExportSpanProcessor(my_exporter) + span_processor = export.SimpleSpanProcessor(my_exporter) tracer_provider.add_span_processor(span_processor) with tracer.start_span("foo"): @@ -113,9 +113,7 @@ def test_on_start_accepts_context(self): tracer = tracer_provider.get_tracer(__name__) exporter = MySpanExporter([]) - span_processor = mock.Mock( - wraps=export.SimpleExportSpanProcessor(exporter) - ) + span_processor = mock.Mock(wraps=export.SimpleSpanProcessor(exporter)) tracer_provider.add_span_processor(span_processor) context = Context() @@ -133,7 +131,7 @@ def test_simple_span_processor_not_sampled(self): spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) - span_processor = export.SimpleExportSpanProcessor(my_exporter) + span_processor = export.SimpleSpanProcessor(my_exporter) tracer_provider.add_span_processor(span_processor) with tracer.start_as_current_span("foo"): @@ -159,7 +157,7 @@ def _create_start_and_end_span(name, span_processor): span.end() -class TestBatchExportSpanProcessor(unittest.TestCase): +class TestBatchSpanProcessor(unittest.TestCase): @mock.patch.dict( "os.environ", { @@ -171,7 +169,7 @@ class TestBatchExportSpanProcessor(unittest.TestCase): ) def test_batch_span_processor_environment_variables(self): - batch_span_processor = export.BatchExportSpanProcessor( + batch_span_processor = export.BatchSpanProcessor( MySpanExporter(destination=[]) ) @@ -184,7 +182,7 @@ def test_on_start_accepts_parent_context(self): # pylint: disable=no-self-use my_exporter = MySpanExporter(destination=[]) span_processor = mock.Mock( - wraps=export.BatchExportSpanProcessor(my_exporter) + wraps=export.BatchSpanProcessor(my_exporter) ) tracer_provider = trace.TracerProvider() tracer_provider.add_span_processor(span_processor) @@ -201,7 +199,7 @@ def test_shutdown(self): spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) - span_processor = export.BatchExportSpanProcessor(my_exporter) + span_processor = export.BatchSpanProcessor(my_exporter) span_names = ["xxx", "bar", "foo"] @@ -219,7 +217,7 @@ def test_flush(self): spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) - span_processor = export.BatchExportSpanProcessor(my_exporter) + span_processor = export.BatchSpanProcessor(my_exporter) span_names0 = ["xxx", "bar", "foo"] span_names1 = ["yyy", "baz", "fox"] @@ -243,7 +241,7 @@ def test_flush_empty(self): spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) - span_processor = export.BatchExportSpanProcessor(my_exporter) + span_processor = export.BatchSpanProcessor(my_exporter) self.assertTrue(span_processor.force_flush()) @@ -254,7 +252,7 @@ def test_flush_from_multiple_threads(self): span_list = [] my_exporter = MySpanExporter(destination=span_list) - span_processor = export.BatchExportSpanProcessor( + span_processor = export.BatchSpanProcessor( my_exporter, max_queue_size=512, max_export_batch_size=128 ) @@ -281,7 +279,7 @@ def test_flush_timeout(self): my_exporter = MySpanExporter( destination=spans_names_list, export_timeout_millis=500 ) - span_processor = export.BatchExportSpanProcessor(my_exporter) + span_processor = export.BatchSpanProcessor(my_exporter) _create_start_and_end_span("foo", span_processor) @@ -297,7 +295,7 @@ def test_batch_span_processor_lossless(self): my_exporter = MySpanExporter( destination=spans_names_list, max_export_batch_size=128 ) - span_processor = export.BatchExportSpanProcessor( + span_processor = export.BatchSpanProcessor( my_exporter, max_queue_size=512, max_export_batch_size=128 ) @@ -316,7 +314,7 @@ def test_batch_span_processor_many_spans(self): my_exporter = MySpanExporter( destination=spans_names_list, max_export_batch_size=128 ) - span_processor = export.BatchExportSpanProcessor( + span_processor = export.BatchSpanProcessor( my_exporter, max_queue_size=256, max_export_batch_size=64, @@ -343,7 +341,7 @@ def test_batch_span_processor_not_sampled(self): my_exporter = MySpanExporter( destination=spans_names_list, max_export_batch_size=128 ) - span_processor = export.BatchExportSpanProcessor( + span_processor = export.BatchSpanProcessor( my_exporter, max_queue_size=256, max_export_batch_size=64, @@ -366,7 +364,7 @@ def test_batch_span_processor_scheduled_delay(self): my_exporter = MySpanExporter( destination=spans_names_list, export_event=export_event ) - span_processor = export.BatchExportSpanProcessor( + span_processor = export.BatchSpanProcessor( my_exporter, schedule_delay_millis=50, ) @@ -392,7 +390,7 @@ def test_batch_span_processor_reset_timeout(self): export_timeout_millis=50, ) - span_processor = export.BatchExportSpanProcessor( + span_processor = export.BatchSpanProcessor( my_exporter, schedule_delay_millis=50, ) @@ -421,21 +419,18 @@ def test_batch_span_processor_reset_timeout(self): def test_batch_span_processor_parameters(self): # zero max_queue_size self.assertRaises( - ValueError, export.BatchExportSpanProcessor, None, max_queue_size=0 + ValueError, export.BatchSpanProcessor, None, max_queue_size=0 ) # negative max_queue_size self.assertRaises( - ValueError, - export.BatchExportSpanProcessor, - None, - max_queue_size=-500, + ValueError, export.BatchSpanProcessor, None, max_queue_size=-500, ) # zero schedule_delay_millis self.assertRaises( ValueError, - export.BatchExportSpanProcessor, + export.BatchSpanProcessor, None, schedule_delay_millis=0, ) @@ -443,7 +438,7 @@ def test_batch_span_processor_parameters(self): # negative schedule_delay_millis self.assertRaises( ValueError, - export.BatchExportSpanProcessor, + export.BatchSpanProcessor, None, schedule_delay_millis=-500, ) @@ -451,7 +446,7 @@ def test_batch_span_processor_parameters(self): # zero max_export_batch_size self.assertRaises( ValueError, - export.BatchExportSpanProcessor, + export.BatchSpanProcessor, None, max_export_batch_size=0, ) @@ -459,7 +454,7 @@ def test_batch_span_processor_parameters(self): # negative max_export_batch_size self.assertRaises( ValueError, - export.BatchExportSpanProcessor, + export.BatchSpanProcessor, None, max_export_batch_size=-500, ) @@ -467,7 +462,7 @@ def test_batch_span_processor_parameters(self): # max_export_batch_size > max_queue_size: self.assertRaises( ValueError, - export.BatchExportSpanProcessor, + export.BatchSpanProcessor, None, max_queue_size=256, max_export_batch_size=512, diff --git a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py index e45e8ee8fd..eb366728c0 100644 --- a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py +++ b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py @@ -28,7 +28,7 @@ def setUp(self): self.tracer_provider = trace.TracerProvider() self.tracer = self.tracer_provider.get_tracer(__name__) self.memory_exporter = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) + span_processor = export.SimpleSpanProcessor(self.memory_exporter) self.tracer_provider.add_span_processor(span_processor) self.exec_scenario() diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py b/shim/opentelemetry-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py index 6ed5818e34..fa1f537a99 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py @@ -1,6 +1,6 @@ import opentelemetry.shim.opentracing_shim as opentracingshim from opentelemetry.sdk import trace -from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) @@ -17,7 +17,7 @@ def __init__(self): oteltracer = tracer_provider.get_tracer(__name__) super(MockTracer, self).__init__(oteltracer) exporter = InMemorySpanExporter() - span_processor = SimpleExportSpanProcessor(exporter) + span_processor = SimpleSpanProcessor(exporter) tracer_provider.add_span_processor(span_processor) self.exporter = exporter diff --git a/tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py b/tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py index c794891783..ce2f3a02b4 100644 --- a/tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py +++ b/tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py @@ -18,11 +18,11 @@ OpenCensusSpanExporter, ) from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.test.test_base import TestBase -class ExportStatusSpanProcessor(SimpleExportSpanProcessor): +class ExportStatusSpanProcessor(SimpleSpanProcessor): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.export_status = [] diff --git a/tests/util/src/opentelemetry/test/spantestutil.py b/tests/util/src/opentelemetry/test/spantestutil.py index 4ae4669a04..3e7de79158 100644 --- a/tests/util/src/opentelemetry/test/spantestutil.py +++ b/tests/util/src/opentelemetry/test/spantestutil.py @@ -31,7 +31,7 @@ def setUpClass(cls): trace_api.set_tracer_provider(TracerProvider()) tracer_provider = trace_api.get_tracer_provider() _MEMORY_EXPORTER = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(_MEMORY_EXPORTER) + span_processor = export.SimpleSpanProcessor(_MEMORY_EXPORTER) tracer_provider.add_span_processor(span_processor) @classmethod diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index c1ec6d2ee4..14ef48d40e 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -65,7 +65,7 @@ def create_tracer_provider(**kwargs): """Helper to create a configured tracer provider. Creates and configures a `TracerProvider` with a - `SimpleExportSpanProcessor` and a `InMemorySpanExporter`. + `SimpleSpanProcessor` and a `InMemorySpanExporter`. All the parameters passed are forwarded to the TracerProvider constructor. @@ -75,7 +75,7 @@ def create_tracer_provider(**kwargs): """ tracer_provider = TracerProvider(**kwargs) memory_exporter = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(memory_exporter) + span_processor = export.SimpleSpanProcessor(memory_exporter) tracer_provider.add_span_processor(span_processor) return tracer_provider, memory_exporter diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py index f2fb14f206..d6c468025e 100644 --- a/tests/w3c_tracecontext_validation_server.py +++ b/tests/w3c_tracecontext_validation_server.py @@ -29,7 +29,7 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleExportSpanProcessor, + SimpleSpanProcessor, ) # FIXME This could likely be avoided by integrating this script into the @@ -42,7 +42,7 @@ RequestsInstrumentor().instrument() # SpanExporter receives the spans and send them to the target location. -span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter()) +span_processor = SimpleSpanProcessor(ConsoleSpanExporter()) trace.get_tracer_provider().add_span_processor(span_processor) app = flask.Flask(__name__) From dc59139a3ac82bc6a8d0c738abf0e36676806b5a Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 1 Mar 2021 16:24:56 -0600 Subject: [PATCH 0791/1517] Refactor utils (#1599) --- .github/workflows/test.yml | 2 +- .../src/opentelemetry/trace/__init__.py | 61 ++++++++-------- .../src/opentelemetry/trace/span.py | 53 ++++++++++++-- .../util/{providers.py => _providers.py} | 21 ------ .../src/opentelemetry/util/time.py | 27 +++++++ .../src/opentelemetry/util/tracestate.py | 73 ------------------- .../src/opentelemetry/util/types.py | 30 ++++---- .../src/opentelemetry/sdk/trace/__init__.py | 2 +- .../sdk/trace/export/__init__.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- .../tests/test_util.py | 2 +- 11 files changed, 127 insertions(+), 148 deletions(-) rename opentelemetry-api/src/opentelemetry/util/{providers.py => _providers.py} (74%) create mode 100644 opentelemetry-api/src/opentelemetry/util/time.py delete mode 100644 opentelemetry-api/src/opentelemetry/util/tracestate.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 236cd3fd91..0bab2951fe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 63a755348c2c9a2b83da85a5c72487ddc507b8b4 + CONTRIB_REPO_SHA: 8a367be397a77bd3b09226260f345abf885297b9 jobs: build: diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index ede12ab442..2d2b197c61 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -74,13 +74,14 @@ """ -import abc -import enum -import typing +from abc import ABC, abstractmethod from contextlib import contextmanager +from enum import Enum from logging import getLogger +from typing import Iterator, Optional, Sequence, cast from opentelemetry.context.context import Context +from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER from opentelemetry.trace.propagation import ( get_current_span, set_span_in_context, @@ -102,12 +103,12 @@ ) from opentelemetry.trace.status import Status from opentelemetry.util import types -from opentelemetry.util.providers import _load_trace_provider +from opentelemetry.util._providers import _load_provider logger = getLogger(__name__) -class LinkBase(abc.ABC): +class LinkBase(ABC): def __init__(self, context: "SpanContext") -> None: self._context = context @@ -116,7 +117,7 @@ def context(self) -> "SpanContext": return self._context @property - @abc.abstractmethod + @abstractmethod def attributes(self) -> types.Attributes: pass @@ -140,10 +141,10 @@ def attributes(self) -> types.Attributes: return self._attributes -_Links = typing.Optional[typing.Sequence[Link]] +_Links = Optional[Sequence[Link]] -class SpanKind(enum.Enum): +class SpanKind(Enum): """Specifies additional details on how this span relates to its parent span. Note that this enumeration is experimental and likely to change. See @@ -172,8 +173,8 @@ class SpanKind(enum.Enum): CONSUMER = 4 -class TracerProvider(abc.ABC): - @abc.abstractmethod +class TracerProvider(ABC): + @abstractmethod def get_tracer( self, instrumenting_module_name: str, @@ -217,7 +218,7 @@ def get_tracer( return DefaultTracer() -class Tracer(abc.ABC): +class Tracer(ABC): """Handles span creation and in-process context propagation. This class provides methods for manipulating the context, creating spans, @@ -228,15 +229,15 @@ class Tracer(abc.ABC): # This is the default behavior when creating spans. CURRENT_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) - @abc.abstractmethod + @abstractmethod def start_span( self, name: str, - context: typing.Optional[Context] = None, + context: Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, - start_time: typing.Optional[int] = None, + start_time: Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, ) -> "Span": @@ -283,19 +284,19 @@ def start_span( """ @contextmanager # type: ignore - @abc.abstractmethod + @abstractmethod def start_as_current_span( self, name: str, - context: typing.Optional[Context] = None, + context: Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, - start_time: typing.Optional[int] = None, + start_time: Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, end_on_exit: bool = True, - ) -> typing.Iterator["Span"]: + ) -> Iterator["Span"]: """Context manager for creating a new span and set it as the current span in this tracer's context. @@ -350,10 +351,10 @@ def start_as_current_span( """ @contextmanager # type: ignore - @abc.abstractmethod + @abstractmethod def use_span( self, span: "Span", end_on_exit: bool = False, - ) -> typing.Iterator[None]: + ) -> Iterator[None]: """Context manager for setting the passed span as the current span in the context, as well as resetting the context back upon exiting the context manager. @@ -381,11 +382,11 @@ class DefaultTracer(Tracer): def start_span( self, name: str, - context: typing.Optional[Context] = None, + context: Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, - start_time: typing.Optional[int] = None, + start_time: Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, ) -> "Span": @@ -396,22 +397,22 @@ def start_span( def start_as_current_span( self, name: str, - context: typing.Optional[Context] = None, + context: Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, - start_time: typing.Optional[int] = None, + start_time: Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, end_on_exit: bool = True, - ) -> typing.Iterator["Span"]: + ) -> Iterator["Span"]: # pylint: disable=unused-argument,no-self-use yield INVALID_SPAN @contextmanager # type: ignore def use_span( self, span: "Span", end_on_exit: bool = False, - ) -> typing.Iterator[None]: + ) -> Iterator[None]: # pylint: disable=unused-argument,no-self-use yield @@ -422,7 +423,7 @@ def use_span( def get_tracer( instrumenting_module_name: str, instrumenting_library_version: str = "", - tracer_provider: typing.Optional[TracerProvider] = None, + tracer_provider: Optional[TracerProvider] = None, ) -> "Tracer": """Returns a `Tracer` for use by the given instrumentation library. @@ -458,8 +459,10 @@ def get_tracer_provider() -> TracerProvider: global _TRACER_PROVIDER # pylint: disable=global-statement if _TRACER_PROVIDER is None: - _TRACER_PROVIDER = _load_trace_provider("tracer_provider") - + _TRACER_PROVIDER = cast( # type: ignore + "TracerProvider", + _load_provider(OTEL_PYTHON_TRACER_PROVIDER, "tracer_provider"), + ) return _TRACER_PROVIDER diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 3ef2ad91d2..2205fea54a 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -7,16 +7,55 @@ from opentelemetry.trace.status import Status from opentelemetry.util import types -from opentelemetry.util.tracestate import ( - _DELIMITER_PATTERN, - _MEMBER_PATTERN, - _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS, - _is_valid_pair, + +# The key MUST begin with a lowercase letter or a digit, +# and can only contain lowercase letters (a-z), digits (0-9), +# underscores (_), dashes (-), asterisks (*), and forward slashes (/). +# For multi-tenant vendor scenarios, an at sign (@) can be used to +# prefix the vendor name. Vendors SHOULD set the tenant ID +# at the beginning of the key. + +# key = ( lcalpha ) 0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) +# key = ( lcalpha / DIGIT ) 0*240( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) "@" lcalpha 0*13( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) +# lcalpha = %x61-7A ; a-z + +_KEY_FORMAT = ( + r"[a-z][_0-9a-z\-\*\/]{0,255}|" + r"[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}" +) +_KEY_PATTERN = re.compile(_KEY_FORMAT) + +# The value is an opaque string containing up to 256 printable +# ASCII [RFC0020] characters (i.e., the range 0x20 to 0x7E) +# except comma (,) and (=). +# value = 0*255(chr) nblk-chr +# nblk-chr = %x21-2B / %x2D-3C / %x3E-7E +# chr = %x20 / nblk-chr + +_VALUE_FORMAT = ( + r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]" ) +_VALUE_PATTERN = re.compile(_VALUE_FORMAT) + +_TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS = 32 +_delimiter_pattern = re.compile(r"[ \t]*,[ \t]*") +_member_pattern = re.compile( + "({})(=)({})[ \t]*".format(_KEY_FORMAT, _VALUE_FORMAT) +) _logger = logging.getLogger(__name__) +def _is_valid_pair(key: str, value: str) -> bool: + + return ( + isinstance(key, str) + and _KEY_PATTERN.fullmatch(key) is not None + and isinstance(value, str) + and _VALUE_PATTERN.fullmatch(value) is not None + ) + + class Span(abc.ABC): """A span represents a single operation within a trace.""" @@ -314,11 +353,11 @@ def from_header(cls, header_list: typing.List[str]) -> "TraceState": """ pairs = OrderedDict() for header in header_list: - for member in re.split(_DELIMITER_PATTERN, header): + for member in re.split(_delimiter_pattern, header): # empty members are valid, but no need to process further. if not member: continue - match = _MEMBER_PATTERN.fullmatch(member) + match = _member_pattern.fullmatch(member) if not match: _logger.warning( "Member doesn't match the w3c identifiers format %s", diff --git a/opentelemetry-api/src/opentelemetry/util/providers.py b/opentelemetry-api/src/opentelemetry/util/_providers.py similarity index 74% rename from opentelemetry-api/src/opentelemetry/util/providers.py rename to opentelemetry-api/src/opentelemetry/util/_providers.py index b8e60afc62..8388f6d8e9 100644 --- a/opentelemetry-api/src/opentelemetry/util/providers.py +++ b/opentelemetry-api/src/opentelemetry/util/_providers.py @@ -12,15 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import time from logging import getLogger from os import environ from typing import TYPE_CHECKING, Union, cast from pkg_resources import iter_entry_points -from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER - if TYPE_CHECKING: from opentelemetry.trace import TracerProvider @@ -28,17 +25,6 @@ logger = getLogger(__name__) -# Since we want API users to be able to provide timestamps, -# this needs to be in the API. - -try: - time_ns = time.time_ns -# Python versions < 3.7 -except AttributeError: - - def time_ns() -> int: - return int(time.time() * 1e9) - def _load_provider( provider_environment_variable: str, provider: str @@ -60,10 +46,3 @@ def _load_provider( except Exception: # pylint: disable=broad-except logger.error("Failed to load configured provider %s", provider) raise - - -def _load_trace_provider(provider: str) -> "TracerProvider": - return cast( # type: ignore - "TracerProvider", - _load_provider(OTEL_PYTHON_TRACER_PROVIDER, provider), - ) diff --git a/opentelemetry-api/src/opentelemetry/util/time.py b/opentelemetry-api/src/opentelemetry/util/time.py new file mode 100644 index 0000000000..fa9f901d0d --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/util/time.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sys import version_info + +if version_info.minor < 7: + from time import time + + def time_ns() -> int: + # FIXME this approach can have precision problems as explained here: + # https://github.com/open-telemetry/opentelemetry-python/issues/1594 + return int(time() * 1e9) + + +else: + from time import time_ns # pylint: disable=unused-import diff --git a/opentelemetry-api/src/opentelemetry/util/tracestate.py b/opentelemetry-api/src/opentelemetry/util/tracestate.py deleted file mode 100644 index c352c8caa2..0000000000 --- a/opentelemetry-api/src/opentelemetry/util/tracestate.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re -from logging import getLogger - -_logger = getLogger(__name__) - -# The key MUST begin with a lowercase letter or a digit, -# and can only contain lowercase letters (a-z), digits (0-9), -# underscores (_), dashes (-), asterisks (*), and forward slashes (/). -# For multi-tenant vendor scenarios, an at sign (@) can be used to -# prefix the vendor name. Vendors SHOULD set the tenant ID -# at the beginning of the key. - -# key = ( lcalpha ) 0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) -# key = ( lcalpha / DIGIT ) 0*240( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) "@" lcalpha 0*13( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) -# lcalpha = %x61-7A ; a-z - -_KEY_WITHOUT_VENDOR_FORMAT = r"[a-z][_0-9a-z\-\*\/]{0,255}" -_KEY_WITH_VENDOR_FORMAT = ( - r"[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}" -) - -_KEY_FORMAT = _KEY_WITHOUT_VENDOR_FORMAT + "|" + _KEY_WITH_VENDOR_FORMAT -_KEY_PATTERN = re.compile(_KEY_FORMAT) - -# The value is an opaque string containing up to 256 printable -# ASCII [RFC0020] characters (i.e., the range 0x20 to 0x7E) -# except comma (,) and (=). -# value = 0*255(chr) nblk-chr -# nblk-chr = %x21-2B / %x2D-3C / %x3E-7E -# chr = %x20 / nblk-chr - -_VALUE_FORMAT = ( - r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]" -) -_VALUE_PATTERN = re.compile(_VALUE_FORMAT) - -_DELIMITER_FORMAT = "[ \t]*,[ \t]*" -_MEMBER_FORMAT = "({})(=)({})[ \t]*".format(_KEY_FORMAT, _VALUE_FORMAT) - -_DELIMITER_PATTERN = re.compile(_DELIMITER_FORMAT) -_MEMBER_PATTERN = re.compile(_MEMBER_FORMAT) - -_TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS = 32 - - -def _is_valid_key(key: str) -> bool: - if not isinstance(key, str): - return False - return _KEY_PATTERN.fullmatch(key) is not None - - -def _is_valid_value(value: str) -> bool: - if not isinstance(value, str): - return False - return _VALUE_PATTERN.fullmatch(value) is not None - - -def _is_valid_pair(key: str, value: str) -> bool: - return _is_valid_key(key) and _is_valid_value(value) diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index 99bb811a5e..ec72a28704 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -13,7 +13,7 @@ # limitations under the License. -from typing import Callable, Mapping, Optional, Sequence, Tuple, Union +from typing import Mapping, Optional, Sequence, Tuple, Union AttributeValue = Union[ str, @@ -25,16 +25,20 @@ Sequence[Optional[int]], Sequence[Optional[float]], ] -AttributeValueAsKey = Union[ - str, - bool, - int, - float, - Tuple[Optional[str], ...], - Tuple[Optional[bool], ...], - Tuple[Optional[int], ...], - Tuple[Optional[float], ...], -] Attributes = Optional[Mapping[str, AttributeValue]] -AttributesAsKey = Tuple[Tuple[str, AttributeValueAsKey], ...] -AttributesFormatter = Callable[[], Attributes] +AttributesAsKey = Tuple[ + Tuple[ + str, + Union[ + str, + bool, + int, + float, + Tuple[Optional[str], ...], + Tuple[Optional[bool], ...], + Tuple[Optional[int], ...], + Tuple[Optional[float], ...], + ], + ], + ..., +] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 9599fbd976..625e50cefb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -54,7 +54,7 @@ from opentelemetry.trace.propagation import SPAN_KEY from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types -from opentelemetry.util.providers import time_ns +from opentelemetry.util.time import time_ns logger = logging.getLogger(__name__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 457c7f4d6e..ae24bcd002 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -29,7 +29,7 @@ OTEL_BSP_SCHEDULE_DELAY, ) from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor -from opentelemetry.util.providers import time_ns +from opentelemetry.util.time import time_ns logger = logging.getLogger(__name__) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index f83d949a85..cdbfbe2f5e 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -38,7 +38,7 @@ from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace.status import StatusCode -from opentelemetry.util.providers import time_ns +from opentelemetry.util.time import time_ns def new_tracer() -> trace_api.Tracer: diff --git a/shim/opentelemetry-opentracing-shim/tests/test_util.py b/shim/opentelemetry-opentracing-shim/tests/test_util.py index 086e0bef5f..7ce3c36950 100644 --- a/shim/opentelemetry-opentracing-shim/tests/test_util.py +++ b/shim/opentelemetry-opentracing-shim/tests/test_util.py @@ -16,7 +16,7 @@ import unittest from opentelemetry.shim.opentracing_shim import util -from opentelemetry.util.providers import time_ns +from opentelemetry.util.time import time_ns class TestUtil(unittest.TestCase): From 74e24415f5f2e91f92532bac5fecc7c879cad3fb Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 2 Mar 2021 10:30:44 -0800 Subject: [PATCH 0792/1517] Rename DefaultSpan to NonRecordingSpan (#1661) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ opentelemetry-api/src/opentelemetry/trace/__init__.py | 8 ++++---- .../opentelemetry/trace/propagation/tracecontext.py | 2 +- opentelemetry-api/src/opentelemetry/trace/span.py | 8 ++++---- .../tests/propagators/test_global_httptextformat.py | 2 +- opentelemetry-api/tests/test_implementation.py | 2 +- .../propagation/test_tracecontexthttptextformat.py | 4 ++-- opentelemetry-api/tests/trace/test_defaultspan.py | 4 ++-- opentelemetry-api/tests/trace/test_globals.py | 2 +- opentelemetry-api/tests/trace/test_tracer.py | 2 +- .../src/opentelemetry/sdk/trace/__init__.py | 2 +- opentelemetry-sdk/tests/trace/test_sampling.py | 4 ++-- opentelemetry-sdk/tests/trace/test_span_processor.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 10 ++++++---- .../src/opentelemetry/propagators/b3/__init__.py | 2 +- .../tests/test_b3_format.py | 2 +- .../src/opentelemetry/propagators/jaeger/__init__.py | 2 +- .../opentelemetry/shim/opentracing_shim/__init__.py | 6 +++--- tests/util/src/opentelemetry/test/mock_textmap.py | 2 +- 20 files changed, 37 insertions(+), 33 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0bab2951fe..6a371b9de6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 8a367be397a77bd3b09226260f345abf885297b9 + CONTRIB_REPO_SHA: 3ceaa0ec08801a8efccee328a805c038f9a7648e jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index e09bf17a4b..c7fcb83e20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Renamed `BatchExportSpanProcessor` to `BatchSpanProcessor` and `SimpleExportSpanProcessor` to `SimpleSpanProcessor` ([#1656])(https://github.com/open-telemetry/opentelemetry-python/pull/1656) +- Rename `DefaultSpan` to `NonRecordingSpan` + ([#1661])(https://github.com/open-telemetry/opentelemetry-python/pull/1661) ## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 2d2b197c61..54b5abd87b 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -22,7 +22,7 @@ the operation. This module provides abstract (i.e. unimplemented) classes required for -tracing, and a concrete no-op :class:`.DefaultSpan` that allows applications +tracing, and a concrete no-op :class:`.NonRecordingSpan` that allows applications to use the API package alone without a supporting implementation. To get a tracer, you need to provide the package name from which you are @@ -93,7 +93,7 @@ INVALID_SPAN_CONTEXT, INVALID_SPAN_ID, INVALID_TRACE_ID, - DefaultSpan, + NonRecordingSpan, Span, SpanContext, TraceFlags, @@ -227,7 +227,7 @@ class Tracer(ABC): # Constant used to represent the current span being used as a parent. # This is the default behavior when creating spans. - CURRENT_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) + CURRENT_SPAN = NonRecordingSpan(INVALID_SPAN_CONTEXT) @abstractmethod def start_span( @@ -473,7 +473,7 @@ def get_tracer_provider() -> TracerProvider: "INVALID_SPAN_CONTEXT", "INVALID_SPAN_ID", "INVALID_TRACE_ID", - "DefaultSpan", + "NonRecordingSpan", "DefaultTracer", "DefaultTracerProvider", "Link", diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index c18cde9ee6..cc2a45c30c 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -80,7 +80,7 @@ def extract( trace_state=tracestate, ) return trace.set_span_in_context( - trace.DefaultSpan(span_context), context + trace.NonRecordingSpan(span_context), context ) def inject( diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 2205fea54a..dc4ce88339 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -482,8 +482,8 @@ def __repr__(self) -> str: ) -class DefaultSpan(Span): - """The default Span that is used when no Span implementation is available. +class NonRecordingSpan(Span): + """The Span that is used when no Span implementation is available. All operations are no-op except context propagation. """ @@ -532,7 +532,7 @@ def record_exception( pass def __repr__(self) -> str: - return "DefaultSpan({!r})".format(self._context) + return "NonRecordingSpan({!r})".format(self._context) INVALID_SPAN_ID = 0x0000000000000000 @@ -544,7 +544,7 @@ def __repr__(self) -> str: trace_flags=DEFAULT_TRACE_OPTIONS, trace_state=DEFAULT_TRACE_STATE, ) -INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) +INVALID_SPAN = NonRecordingSpan(INVALID_SPAN_CONTEXT) def format_trace_id(trace_id: int) -> str: diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index 88b1920468..4f54053735 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -48,7 +48,7 @@ def test_propagation(self): self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) - span = trace.DefaultSpan(span_context) + span = trace.NonRecordingSpan(span_context) ctx = baggage.set_baggage("key3", "val3") ctx = baggage.set_baggage("key4", "val4", context=ctx) ctx = set_span_in_context(span, context=ctx) diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index c41f608e80..3cb3689961 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -54,6 +54,6 @@ def test_span(self): trace.Span() # type:ignore def test_default_span(self): - span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) + span = trace.NonRecordingSpan(trace.INVALID_SPAN_CONTEXT) self.assertEqual(span.get_span_context(), trace.INVALID_SPAN_CONTEXT) self.assertIs(span.is_recording(), False) diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 0b20fbff4b..77fe8e8e1a 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -68,7 +68,7 @@ def test_headers_with_tracestate(self): ) self.assertTrue(span_context.is_remote) output = {} # type:typing.Dict[str, str] - span = trace.DefaultSpan(span_context) + span = trace.NonRecordingSpan(span_context) ctx = trace.set_span_in_context(span) FORMAT.inject(dict.__setitem__, output, ctx) @@ -149,7 +149,7 @@ def test_no_send_empty_tracestate(self): empty tracestate headers but SHOULD avoid sending them. """ output = {} # type:typing.Dict[str, str] - span = trace.DefaultSpan( + span = trace.NonRecordingSpan( trace.SpanContext(self.TRACE_ID, self.SPAN_ID, is_remote=False) ) ctx = trace.set_span_in_context(span) diff --git a/opentelemetry-api/tests/trace/test_defaultspan.py b/opentelemetry-api/tests/trace/test_defaultspan.py index 73f068825b..fbd3c00774 100644 --- a/opentelemetry-api/tests/trace/test_defaultspan.py +++ b/opentelemetry-api/tests/trace/test_defaultspan.py @@ -17,7 +17,7 @@ from opentelemetry import trace -class TestDefaultSpan(unittest.TestCase): +class TestNonRecordingSpan(unittest.TestCase): def test_ctor(self): context = trace.SpanContext( 1, @@ -26,7 +26,7 @@ def test_ctor(self): trace_flags=trace.DEFAULT_TRACE_OPTIONS, trace_state=trace.DEFAULT_TRACE_STATE, ) - span = trace.DefaultSpan(context) + span = trace.NonRecordingSpan(context) self.assertEqual(context, span.get_span_context()) def test_invalid_span(self): diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 5d1a4c5c38..55b040f2bc 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -30,7 +30,7 @@ def test_get_current_span(self): be retrievable via get_current_span """ self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN) - span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) + span = trace.NonRecordingSpan(trace.INVALID_SPAN_CONTEXT) ctx = trace.set_span_in_context(span) token = context.attach(ctx) try: diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py index 73909fd8dd..0ae3a0505b 100644 --- a/opentelemetry-api/tests/trace/test_tracer.py +++ b/opentelemetry-api/tests/trace/test_tracer.py @@ -30,7 +30,7 @@ def test_start_as_current_span(self): self.assertIsInstance(span, trace.Span) def test_use_span(self): - span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) + span = trace.NonRecordingSpan(trace.INVALID_SPAN_CONTEXT) with self.tracer.use_span(span): pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 625e50cefb..f7b3ecd566 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -947,7 +947,7 @@ def start_span( # pylint: disable=too-many-locals ) span.start(start_time=start_time, parent_context=context) else: - span = trace_api.DefaultSpan(context=span_context) + span = trace_api.NonRecordingSpan(context=span_context) return span @contextmanager diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index e2b20a5c88..0abaa55b71 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -77,8 +77,8 @@ def _create_parent( @staticmethod def _create_parent_span( trace_flags: trace.TraceFlags, is_remote=False - ) -> trace.DefaultSpan: - return trace.DefaultSpan( + ) -> trace.NonRecordingSpan: + return trace.NonRecordingSpan( trace.SpanContext( 0xDEADBEEF, 0xDEADBEF0, diff --git a/opentelemetry-sdk/tests/trace/test_span_processor.py b/opentelemetry-sdk/tests/trace/test_span_processor.py index fff3509110..b8568fc7a1 100644 --- a/opentelemetry-sdk/tests/trace/test_span_processor.py +++ b/opentelemetry-sdk/tests/trace/test_span_processor.py @@ -154,7 +154,7 @@ def create_multi_span_processor( @staticmethod def create_default_span() -> trace_api.Span: span_context = trace_api.SpanContext(37, 73, is_remote=False) - return trace_api.DefaultSpan(span_context) + return trace_api.NonRecordingSpan(span_context) def test_on_start(self): multi_processor = self.create_multi_span_processor() diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index cdbfbe2f5e..e2acad09b7 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -126,7 +126,9 @@ def test_use_span_exception(self): class TestUseSpanException(Exception): pass - default_span = trace_api.DefaultSpan(trace_api.INVALID_SPAN_CONTEXT) + default_span = trace_api.NonRecordingSpan( + trace_api.INVALID_SPAN_CONTEXT + ) tracer = new_tracer() with self.assertRaises(TestUseSpanException): with tracer.use_span(default_span): @@ -182,9 +184,9 @@ def test_sampler_no_sampling(self): # decides not to sampler root_span = tracer.start_span(name="root span", context=None) ctx = trace_api.set_span_in_context(root_span) - self.assertIsInstance(root_span, trace_api.DefaultSpan) + self.assertIsInstance(root_span, trace_api.NonRecordingSpan) child_span = tracer.start_span(name="child span", context=ctx) - self.assertIsInstance(child_span, trace_api.DefaultSpan) + self.assertIsInstance(child_span, trace_api.NonRecordingSpan) self.assertEqual( root_span.get_span_context().trace_flags, trace_api.TraceFlags.DEFAULT, @@ -208,7 +210,7 @@ def test_sampler_with_env(self): root_span = tracer.start_span(name="root span", context=None) # Should be no-op - self.assertIsInstance(root_span, trace_api.DefaultSpan) + self.assertIsInstance(root_span, trace_api.NonRecordingSpan) @mock.patch.dict( "os.environ", diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py index e872cef9e6..35ed8ec13e 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py @@ -112,7 +112,7 @@ def extract( options |= trace.TraceFlags.SAMPLED return trace.set_span_in_context( - trace.DefaultSpan( + trace.NonRecordingSpan( trace.SpanContext( # trace an span ids are encoded in hex, so must be converted trace_id=trace_id, diff --git a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index cbf19b02a2..b0f1138693 100644 --- a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -327,7 +327,7 @@ def test_inject_empty_context(): @staticmethod def test_default_span(): - """Make sure propagator does not crash when working with DefaultSpan""" + """Make sure propagator does not crash when working with NonRecordingSpan""" class CarrierGetter(DictGetter): def get(self, carrier, key): diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py index 67c7d716c9..5272fe6761 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py @@ -61,7 +61,7 @@ def extract( ): return trace.set_span_in_context(trace.INVALID_SPAN, context) - span = trace.DefaultSpan( + span = trace.NonRecordingSpan( trace.SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index 1083e68263..e0ba424745 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -104,7 +104,7 @@ from opentelemetry.propagate import get_global_textmap from opentelemetry.shim.opentracing_shim import util from opentelemetry.shim.opentracing_shim.version import __version__ -from opentelemetry.trace import INVALID_SPAN_CONTEXT, DefaultSpan, Link +from opentelemetry.trace import INVALID_SPAN_CONTEXT, Link, NonRecordingSpan from opentelemetry.trace import SpanContext as OtelSpanContext from opentelemetry.trace import Tracer as OtelTracer from opentelemetry.trace import ( @@ -633,7 +633,7 @@ def start_span( # use a `None` parent, which triggers the creation of a new trace. parent = child_of.unwrap() if child_of else None if isinstance(parent, OtelSpanContext): - parent = DefaultSpan(parent) + parent = NonRecordingSpan(parent) parent_span_context = set_span_in_context(parent) @@ -684,7 +684,7 @@ def inject(self, span_context, format: object, carrier: object): propagator = get_global_textmap() - ctx = set_span_in_context(DefaultSpan(span_context.unwrap())) + ctx = set_span_in_context(NonRecordingSpan(span_context.unwrap())) propagator.inject(type(carrier).__setitem__, carrier, context=ctx) def extract(self, format: object, carrier: object): diff --git a/tests/util/src/opentelemetry/test/mock_textmap.py b/tests/util/src/opentelemetry/test/mock_textmap.py index 50e40490b7..12e22a1f50 100644 --- a/tests/util/src/opentelemetry/test/mock_textmap.py +++ b/tests/util/src/opentelemetry/test/mock_textmap.py @@ -71,7 +71,7 @@ def extract( return trace.set_span_in_context(trace.INVALID_SPAN) return trace.set_span_in_context( - trace.DefaultSpan( + trace.NonRecordingSpan( trace.SpanContext( trace_id=int(trace_id_list[0]), span_id=int(span_id_list[0]), From a046518d21153070da2933d1e987713df0d4a6c3 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 2 Mar 2021 16:35:40 -0800 Subject: [PATCH 0793/1517] move textmap propagators to opentelemetry.propagators (#1662) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 3 +++ docs/conf.py | 14 ++++---------- .../auto-instrumentation/server_instrumented.py | 2 +- .../opentelemetry/baggage/propagation/__init__.py | 9 ++++----- .../src/opentelemetry/propagate/__init__.py | 3 +-- .../src/opentelemetry/propagators/composite.py | 14 +++++++------- .../{trace/propagation => propagators}/textmap.py | 0 .../trace/propagation/tracecontext.py | 11 +++++------ .../tests/baggage/test_baggage_propagation.py | 2 +- .../propagators/test_global_httptextformat.py | 2 +- .../tests/trace/propagation/test_textmap.py | 2 +- .../propagation/test_tracecontexthttptextformat.py | 11 ++++------- .../src/opentelemetry/propagators/b3/__init__.py | 2 +- .../trace/propagation/test_benchmark_b3_format.py | 2 +- .../tests/test_b3_format.py | 2 +- .../opentelemetry/propagators/jaeger/__init__.py | 2 +- .../tests/test_jaeger_propagator.py | 2 +- .../shim/opentracing_shim/__init__.py | 2 +- tests/util/src/opentelemetry/test/mock_textmap.py | 2 +- 20 files changed, 40 insertions(+), 49 deletions(-) rename opentelemetry-api/src/opentelemetry/{trace/propagation => propagators}/textmap.py (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6a371b9de6..73baecb785 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 3ceaa0ec08801a8efccee328a805c038f9a7648e + CONTRIB_REPO_SHA: 16ae58b341b960df52c1cf4541695164821b3638 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index c7fcb83e20..cfea0a571c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1656])(https://github.com/open-telemetry/opentelemetry-python/pull/1656) - Rename `DefaultSpan` to `NonRecordingSpan` ([#1661])(https://github.com/open-telemetry/opentelemetry-python/pull/1661) +- Moving `Getter`, `Setter` and `TextMapPropagator` out of `opentelemetry.trace.propagation` and + into `opentelemetry.propagators` + ([#1662])(https://github.com/open-telemetry/opentelemetry-python/pull/1662) ## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 diff --git a/docs/conf.py b/docs/conf.py index 42eefa1ce1..a8c659b2d4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -102,16 +102,10 @@ # with "class reference target not found: ObjectProxy". ("py:class", "ObjectProxy"), # TODO: Understand why sphinx is not able to find this local class - ("py:class", "opentelemetry.trace.propagation.textmap.TextMapPropagator",), - ("py:class", "opentelemetry.trace.propagation.textmap.DictGetter",), - ( - "any", - "opentelemetry.trace.propagation.textmap.TextMapPropagator.extract", - ), - ( - "any", - "opentelemetry.trace.propagation.textmap.TextMapPropagator.inject", - ), + ("py:class", "opentelemetry.propagators.textmap.TextMapPropagator",), + ("py:class", "opentelemetry.propagators.textmap.DictGetter",), + ("any", "opentelemetry.propagators.textmap.TextMapPropagator.extract",), + ("any", "opentelemetry.propagators.textmap.TextMapPropagator.inject",), ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_instrumented.py index 5bf5e8dafe..1ac1bd6b71 100644 --- a/docs/examples/auto-instrumentation/server_instrumented.py +++ b/docs/examples/auto-instrumentation/server_instrumented.py @@ -17,12 +17,12 @@ from opentelemetry import trace from opentelemetry.instrumentation.wsgi import collect_request_attributes from opentelemetry.propagate import extract +from opentelemetry.propagators.textmap import DictGetter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleSpanProcessor, ) -from opentelemetry.trace.propagation.textmap import DictGetter app = Flask(__name__) diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index ae5e0c34e1..9c68357a7c 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -18,12 +18,11 @@ from opentelemetry import baggage from opentelemetry.context import get_current from opentelemetry.context.context import Context -from opentelemetry.trace.propagation import textmap +from opentelemetry.propagators import textmap class BaggagePropagator(textmap.TextMapPropagator): - """Extracts and injects Baggage which is used to annotate telemetry. - """ + """Extracts and injects Baggage which is used to annotate telemetry.""" MAX_HEADER_LENGTH = 8192 MAX_PAIR_LENGTH = 4096 @@ -39,7 +38,7 @@ def extract( """Extract Baggage from the carrier. See - `opentelemetry.trace.propagation.textmap.TextMapPropagator.extract` + `opentelemetry.propagators.textmap.TextMapPropagator.extract` """ if context is None: @@ -81,7 +80,7 @@ def inject( """Injects Baggage into the carrier. See - `opentelemetry.trace.propagation.textmap.TextMapPropagator.inject` + `opentelemetry.propagators.textmap.TextMapPropagator.inject` """ baggage_entries = baggage.get_all(context=context) if not baggage_entries: diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index 6050b5f4ff..77d09b9a9f 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -76,8 +76,7 @@ def example_route(): from opentelemetry.context.context import Context from opentelemetry.environment_variables import OTEL_PROPAGATORS -from opentelemetry.propagators import composite -from opentelemetry.trace.propagation import textmap +from opentelemetry.propagators import composite, textmap logger = getLogger(__name__) diff --git a/opentelemetry-api/src/opentelemetry/propagators/composite.py b/opentelemetry-api/src/opentelemetry/propagators/composite.py index fde42d9373..92dc6b8a38 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/composite.py +++ b/opentelemetry-api/src/opentelemetry/propagators/composite.py @@ -15,13 +15,13 @@ import typing from opentelemetry.context.context import Context -from opentelemetry.trace.propagation import textmap +from opentelemetry.propagators import textmap logger = logging.getLogger(__name__) class CompositeHTTPPropagator(textmap.TextMapPropagator): - """ CompositeHTTPPropagator provides a mechanism for combining multiple + """CompositeHTTPPropagator provides a mechanism for combining multiple propagators into a single one. Args: @@ -39,12 +39,12 @@ def extract( carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: - """ Run each of the configured propagators with the given context and carrier. + """Run each of the configured propagators with the given context and carrier. Propagators are run in the order they are configured, if multiple propagators write the same context key, the propagator later in the list will override previous propagators. - See `opentelemetry.trace.propagation.textmap.TextMapPropagator.extract` + See `opentelemetry.propagators.textmap.TextMapPropagator.extract` """ for propagator in self._propagators: context = propagator.extract(getter, carrier, context) @@ -56,12 +56,12 @@ def inject( carrier: textmap.TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> None: - """ Run each of the configured propagators with the given context and carrier. + """Run each of the configured propagators with the given context and carrier. Propagators are run in the order they are configured, if multiple propagators write the same carrier key, the propagator later in the list will override previous propagators. - See `opentelemetry.trace.propagation.textmap.TextMapPropagator.inject` + See `opentelemetry.propagators.textmap.TextMapPropagator.inject` """ for propagator in self._propagators: propagator.inject(set_in_carrier, carrier, context) @@ -71,7 +71,7 @@ def fields(self) -> typing.Set[str]: """Returns a set with the fields set in `inject`. See - `opentelemetry.trace.propagation.textmap.TextMapPropagator.fields` + `opentelemetry.propagators.textmap.TextMapPropagator.fields` """ composite_fields = set() diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py b/opentelemetry-api/src/opentelemetry/propagators/textmap.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/trace/propagation/textmap.py rename to opentelemetry-api/src/opentelemetry/propagators/textmap.py diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index cc2a45c30c..e648338e2c 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -17,13 +17,12 @@ import opentelemetry.trace as trace from opentelemetry.context.context import Context -from opentelemetry.trace.propagation import textmap +from opentelemetry.propagators import textmap from opentelemetry.trace.span import TraceState class TraceContextTextMapPropagator(textmap.TextMapPropagator): - """Extracts and injects using w3c TraceContext's headers. - """ + """Extracts and injects using w3c TraceContext's headers.""" _TRACEPARENT_HEADER_NAME = "traceparent" _TRACESTATE_HEADER_NAME = "tracestate" @@ -41,7 +40,7 @@ def extract( ) -> Context: """Extracts SpanContext from the carrier. - See `opentelemetry.trace.propagation.textmap.TextMapPropagator.extract` + See `opentelemetry.propagators.textmap.TextMapPropagator.extract` """ header = getter.get(carrier, self._TRACEPARENT_HEADER_NAME) @@ -91,7 +90,7 @@ def inject( ) -> None: """Injects SpanContext into the carrier. - See `opentelemetry.trace.propagation.textmap.TextMapPropagator.inject` + See `opentelemetry.propagators.textmap.TextMapPropagator.inject` """ span = trace.get_current_span(context) span_context = span.get_span_context() @@ -116,6 +115,6 @@ def fields(self) -> typing.Set[str]: """Returns a set with the fields set in `inject`. See - `opentelemetry.trace.propagation.textmap.TextMapPropagator.fields` + `opentelemetry.propagators.textmap.TextMapPropagator.fields` """ return {self._TRACEPARENT_HEADER_NAME, self._TRACESTATE_HEADER_NAME} diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py index 68723c6a0b..719e8d91e3 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py @@ -18,7 +18,7 @@ from opentelemetry import baggage from opentelemetry.baggage.propagation import BaggagePropagator from opentelemetry.context import get_current -from opentelemetry.trace.propagation.textmap import DictGetter +from opentelemetry.propagators.textmap import DictGetter carrier_getter = DictGetter() diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index 4f54053735..0b69f6f286 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -16,8 +16,8 @@ from opentelemetry import baggage, trace from opentelemetry.propagate import extract, inject +from opentelemetry.propagators.textmap import DictGetter from opentelemetry.trace import get_current_span, set_span_in_context -from opentelemetry.trace.propagation.textmap import DictGetter carrier_getter = DictGetter() diff --git a/opentelemetry-api/tests/trace/propagation/test_textmap.py b/opentelemetry-api/tests/trace/propagation/test_textmap.py index 830f7ac2c1..12e851de34 100644 --- a/opentelemetry-api/tests/trace/propagation/test_textmap.py +++ b/opentelemetry-api/tests/trace/propagation/test_textmap.py @@ -14,7 +14,7 @@ import unittest -from opentelemetry.trace.propagation.textmap import DictGetter +from opentelemetry.propagators.textmap import DictGetter class TestDictGetter(unittest.TestCase): diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 77fe8e8e1a..cff30b7c9b 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -17,8 +17,8 @@ from unittest.mock import Mock, patch from opentelemetry import trace +from opentelemetry.propagators.textmap import DictGetter from opentelemetry.trace.propagation import tracecontext -from opentelemetry.trace.propagation.textmap import DictGetter from opentelemetry.trace.span import TraceState FORMAT = tracecontext.TraceContextTextMapPropagator() @@ -187,8 +187,7 @@ def test_propagate_invalid_context(self): self.assertFalse("traceparent" in output) def test_tracestate_empty_header(self): - """Test tracestate with an additional empty header (should be ignored) - """ + """Test tracestate with an additional empty header (should be ignored)""" span = trace.get_current_span( FORMAT.extract( carrier_getter, @@ -203,8 +202,7 @@ def test_tracestate_empty_header(self): self.assertEqual(span.get_span_context().trace_state["foo"], "1") def test_tracestate_header_with_trailing_comma(self): - """Do not propagate invalid trace context. - """ + """Do not propagate invalid trace context.""" span = trace.get_current_span( FORMAT.extract( carrier_getter, @@ -219,8 +217,7 @@ def test_tracestate_header_with_trailing_comma(self): self.assertEqual(span.get_span_context().trace_state["foo"], "1") def test_tracestate_keys(self): - """Test for valid key patterns in the tracestate - """ + """Test for valid key patterns in the tracestate""" tracestate_value = ",".join( [ "1a-2f@foo=bar1", diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py index 35ed8ec13e..448e59517b 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py @@ -17,7 +17,7 @@ import opentelemetry.trace as trace from opentelemetry.context import Context -from opentelemetry.trace.propagation.textmap import ( +from opentelemetry.propagators.textmap import ( Getter, Setter, TextMapPropagator, diff --git a/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py index 843e6e96b0..5048f495f0 100644 --- a/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py @@ -14,7 +14,7 @@ import opentelemetry.propagators.b3 as b3_format import opentelemetry.sdk.trace as trace -from opentelemetry.trace.propagation.textmap import DictGetter +from opentelemetry.propagators.textmap import DictGetter FORMAT = b3_format.B3Format() diff --git a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index b0f1138693..f9d3bce1ad 100644 --- a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -20,7 +20,7 @@ import opentelemetry.sdk.trace.id_generator as id_generator import opentelemetry.trace as trace_api from opentelemetry.context import get_current -from opentelemetry.trace.propagation.textmap import DictGetter +from opentelemetry.propagators.textmap import DictGetter FORMAT = b3_format.B3Format() diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py index 5272fe6761..e10d464be6 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py @@ -18,7 +18,7 @@ import opentelemetry.trace as trace from opentelemetry import baggage from opentelemetry.context import Context, get_current -from opentelemetry.trace.propagation.textmap import ( +from opentelemetry.propagators.textmap import ( Getter, Setter, TextMapPropagator, diff --git a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py index 8512678aee..8e7190d1cc 100644 --- a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py @@ -22,7 +22,7 @@ from opentelemetry.propagators import ( # pylint: disable=no-name-in-module jaeger, ) -from opentelemetry.trace.propagation.textmap import DictGetter +from opentelemetry.propagators.textmap import DictGetter FORMAT = jaeger.JaegerPropagator() diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index e0ba424745..c1dffbc1f9 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -102,6 +102,7 @@ from opentelemetry.baggage import get_baggage, set_baggage from opentelemetry.context import Context, attach, detach, get_value, set_value from opentelemetry.propagate import get_global_textmap +from opentelemetry.propagators.textmap import DictGetter from opentelemetry.shim.opentracing_shim import util from opentelemetry.shim.opentracing_shim.version import __version__ from opentelemetry.trace import INVALID_SPAN_CONTEXT, Link, NonRecordingSpan @@ -112,7 +113,6 @@ get_current_span, set_span_in_context, ) -from opentelemetry.trace.propagation.textmap import DictGetter from opentelemetry.util.types import Attributes ValueT = TypeVar("ValueT", int, float, bool, str) diff --git a/tests/util/src/opentelemetry/test/mock_textmap.py b/tests/util/src/opentelemetry/test/mock_textmap.py index 12e22a1f50..1edd079042 100644 --- a/tests/util/src/opentelemetry/test/mock_textmap.py +++ b/tests/util/src/opentelemetry/test/mock_textmap.py @@ -16,7 +16,7 @@ from opentelemetry import trace from opentelemetry.context import Context, get_current -from opentelemetry.trace.propagation.textmap import ( +from opentelemetry.propagators.textmap import ( Getter, Setter, TextMapPropagator, From 9bf28fb451a85fd9e9a4f2276c3eebd484e55d02 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 2 Mar 2021 17:05:32 -0800 Subject: [PATCH 0794/1517] rename BaggagePropagator to W3CBaggagePropagator (#1663) Co-authored-by: Leighton Chen --- CHANGELOG.md | 2 ++ opentelemetry-api/setup.cfg | 2 +- .../opentelemetry/baggage/propagation/__init__.py | 2 +- .../src/opentelemetry/propagate/__init__.py | 2 +- .../tests/baggage/test_baggage_propagation.py | 12 ++++++------ .../tests/propagators/test_propagators.py | 5 +++-- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfea0a571c..f257ced33a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Moving `Getter`, `Setter` and `TextMapPropagator` out of `opentelemetry.trace.propagation` and into `opentelemetry.propagators` ([#1662])(https://github.com/open-telemetry/opentelemetry-python/pull/1662) +- Rename `BaggagePropagator` to `W3CBaggagePropagator` + ([#1663])(https://github.com/open-telemetry/opentelemetry-python/pull/1663) ## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index d3cbe4ddf2..332069041c 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -54,7 +54,7 @@ opentelemetry_tracer_provider = default_tracer_provider = opentelemetry.trace:DefaultTracerProvider opentelemetry_propagator = tracecontext = opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator - baggage = opentelemetry.baggage.propagation:BaggagePropagator + baggage = opentelemetry.baggage.propagation:W3CBaggagePropagator [options.extras_require] test = diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index 9c68357a7c..e59df9d285 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -21,7 +21,7 @@ from opentelemetry.propagators import textmap -class BaggagePropagator(textmap.TextMapPropagator): +class W3CBaggagePropagator(textmap.TextMapPropagator): """Extracts and injects Baggage which is used to annotate telemetry.""" MAX_HEADER_LENGTH = 8192 diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index 77d09b9a9f..44f9897a53 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -25,7 +25,7 @@ ``opentelemetry.propagators.composite.CompositeHTTPPropagator`` with 2 propagators, one of type ``opentelemetry.trace.propagation.tracecontext.TraceContextTextMapPropagator`` -and other of type ``opentelemetry.baggage.propagation.BaggagePropagator``. +and other of type ``opentelemetry.baggage.propagation.W3CBaggagePropagator``. Notice that these propagator classes are defined as ``opentelemetry_propagator`` entry points in the ``setup.cfg`` file of ``opentelemetry``. diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py index 719e8d91e3..954bc1aba7 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py @@ -16,7 +16,7 @@ from unittest.mock import Mock, patch from opentelemetry import baggage -from opentelemetry.baggage.propagation import BaggagePropagator +from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.context import get_current from opentelemetry.propagators.textmap import DictGetter @@ -25,7 +25,7 @@ class TestBaggagePropagation(unittest.TestCase): def setUp(self): - self.propagator = BaggagePropagator() + self.propagator = W3CBaggagePropagator() def _extract(self, header_value): """Test helper""" @@ -87,7 +87,7 @@ def test_invalid_header(self): self.assertEqual(self._extract(header), expected) def test_header_too_long(self): - long_value = "s" * (BaggagePropagator.MAX_HEADER_LENGTH + 1) + long_value = "s" * (W3CBaggagePropagator.MAX_HEADER_LENGTH + 1) header = "key1={}".format(long_value) expected = {} self.assertEqual(self._extract(header), expected) @@ -96,15 +96,15 @@ def test_header_contains_too_many_entries(self): header = ",".join( [ "key{}=val".format(k) - for k in range(BaggagePropagator.MAX_PAIRS + 1) + for k in range(W3CBaggagePropagator.MAX_PAIRS + 1) ] ) self.assertEqual( - len(self._extract(header)), BaggagePropagator.MAX_PAIRS + len(self._extract(header)), W3CBaggagePropagator.MAX_PAIRS ) def test_header_contains_pair_too_long(self): - long_value = "s" * (BaggagePropagator.MAX_PAIR_LENGTH + 1) + long_value = "s" * (W3CBaggagePropagator.MAX_PAIR_LENGTH + 1) header = "key1=value1,key2={},key3=value3".format(long_value) expected = {"key1": "value1", "key3": "value3"} self.assertEqual(self._extract(header), expected) diff --git a/opentelemetry-api/tests/propagators/test_propagators.py b/opentelemetry-api/tests/propagators/test_propagators.py index 5e2abaed1a..1ea80146ef 100644 --- a/opentelemetry-api/tests/propagators/test_propagators.py +++ b/opentelemetry-api/tests/propagators/test_propagators.py @@ -17,7 +17,7 @@ from unittest import TestCase from unittest.mock import Mock, patch -from opentelemetry.baggage.propagation import BaggagePropagator +from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, @@ -33,7 +33,8 @@ def test_propagators(propagators): self.assertEqual(len(propagators), 2) self.assertEqual( - propagators, {TraceContextTextMapPropagator, BaggagePropagator} + propagators, + {TraceContextTextMapPropagator, W3CBaggagePropagator}, ) mock_compositehttppropagator.configure_mock( From 1af9f877794d69d37fa27791baa7bac5bfc51088 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 4 Mar 2021 18:26:30 -0800 Subject: [PATCH 0795/1517] Remove 'span' for jaeger and zipkin exporters (#1664) --- CHANGELOG.md | 2 ++ docs/examples/opentracing/main.py | 4 ++-- docs/getting_started/jaeger_example.py | 2 +- exporter/opentelemetry-exporter-jaeger/README.rst | 2 +- .../examples/jaeger_exporter_example.py | 8 ++++---- exporter/opentelemetry-exporter-jaeger/setup.cfg | 2 +- .../src/opentelemetry/exporter/jaeger/__init__.py | 6 +++--- .../tests/test_jaeger_exporter_protobuf.py | 6 +++--- .../tests/test_jaeger_exporter_thrift.py | 10 +++++----- exporter/opentelemetry-exporter-zipkin/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/__init__.py | 6 +++--- .../tests/test_zipkin_exporter.py | 14 +++++++------- 12 files changed, 33 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f257ced33a..81c56db379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1662])(https://github.com/open-telemetry/opentelemetry-python/pull/1662) - Rename `BaggagePropagator` to `W3CBaggagePropagator` ([#1663])(https://github.com/open-telemetry/opentelemetry-python/pull/1663) +- Rename `JaegerSpanExporter` to `JaegerExporter` and rename `ZipkinSpanExporter` to `ZipkinExporter` + ([#1664])(https://github.com/open-telemetry/opentelemetry-python/pull/1664) ## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 diff --git a/docs/examples/opentracing/main.py b/docs/examples/opentracing/main.py index f997801aaa..df5626b043 100755 --- a/docs/examples/opentracing/main.py +++ b/docs/examples/opentracing/main.py @@ -3,7 +3,7 @@ from rediscache import RedisCache from opentelemetry import trace -from opentelemetry.exporter.jaeger import JaegerSpanExporter +from opentelemetry.exporter.jaeger import JaegerExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.shim import opentracing_shim @@ -13,7 +13,7 @@ tracer_provider = trace.get_tracer_provider() # Configure the tracer to export traces to Jaeger -jaeger_exporter = JaegerSpanExporter( +jaeger_exporter = JaegerExporter( service_name="OpenTracing Shim Example", agent_host_name="localhost", agent_port=6831, diff --git a/docs/getting_started/jaeger_example.py b/docs/getting_started/jaeger_example.py index 0494333d2c..79b22c5e77 100644 --- a/docs/getting_started/jaeger_example.py +++ b/docs/getting_started/jaeger_example.py @@ -20,7 +20,7 @@ trace.set_tracer_provider(TracerProvider()) -jaeger_exporter = jaeger.JaegerSpanExporter( +jaeger_exporter = jaeger.JaegerExporter( service_name="my-helloworld-service", agent_host_name="localhost", agent_port=6831, diff --git a/exporter/opentelemetry-exporter-jaeger/README.rst b/exporter/opentelemetry-exporter-jaeger/README.rst index 67798395a3..2176b2a71d 100644 --- a/exporter/opentelemetry-exporter-jaeger/README.rst +++ b/exporter/opentelemetry-exporter-jaeger/README.rst @@ -22,7 +22,7 @@ Installation Configuration ------------- -OpenTelemetry Jaeger Exporter can be configured by setting `JaegerSpanExporter parameters +OpenTelemetry Jaeger Exporter can be configured by setting `JaegerExporter parameters `_ or by setting `environment variables Date: Sat, 6 Mar 2021 18:45:06 +0200 Subject: [PATCH 0796/1517] distro: fix auto instrumentation with otlp OTEL_TRACES_EXPORTER env var (#1657) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/distro/__init__.py | 2 +- opentelemetry-distro/tests/test_configurator.py | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c56db379..47db772665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1656])(https://github.com/open-telemetry/opentelemetry-python/pull/1656) - Rename `DefaultSpan` to `NonRecordingSpan` ([#1661])(https://github.com/open-telemetry/opentelemetry-python/pull/1661) +- Fixed distro configuration with `OTEL_TRACES_EXPORTER` env var set to `otlp` + ([#1657])(https://github.com/open-telemetry/opentelemetry-python/pull/1657) - Moving `Getter`, `Setter` and `TextMapPropagator` out of `opentelemetry.trace.propagation` and into `opentelemetry.propagators` ([#1662])(https://github.com/open-telemetry/opentelemetry-python/pull/1662) diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index 666ab57bb3..6496be16c5 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -67,7 +67,7 @@ def _get_exporter_names() -> Sequence[str]: ) if EXPORTER_OTLP in exporters: - exporters.pop(EXPORTER_OTLP) + exporters.remove(EXPORTER_OTLP) exporters.add(EXPORTER_OTLP_SPAN) return list(exporters) diff --git a/opentelemetry-distro/tests/test_configurator.py b/opentelemetry-distro/tests/test_configurator.py index 91ca82da86..899e019113 100644 --- a/opentelemetry-distro/tests/test_configurator.py +++ b/opentelemetry-distro/tests/test_configurator.py @@ -18,6 +18,9 @@ from unittest.mock import patch from opentelemetry.distro import ( + EXPORTER_OTLP, + EXPORTER_OTLP_SPAN, + _get_exporter_names, _get_id_generator, _import_id_generator, _init_tracing, @@ -25,6 +28,7 @@ from opentelemetry.environment_variables import ( OTEL_PYTHON_ID_GENERATOR, OTEL_PYTHON_SERVICE_NAME, + OTEL_TRACES_EXPORTER, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator @@ -143,3 +147,14 @@ def test_trace_init_custom_id_generator(self, mock_iter_entry_points): _init_tracing({}, id_generator) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider.id_generator, CustomIdGenerator) + + +class TestExporterNames(TestCase): + def test_otlp_exporter_overwrite(self): + for exporter in [EXPORTER_OTLP, EXPORTER_OTLP_SPAN]: + with patch.dict(environ, {OTEL_TRACES_EXPORTER: exporter}): + self.assertEqual(_get_exporter_names(), [EXPORTER_OTLP_SPAN]) + + @patch.dict(environ, {OTEL_TRACES_EXPORTER: "jaeger,zipkin"}) + def test_multiple_exporters(self): + self.assertEqual(sorted(_get_exporter_names()), ["jaeger", "zipkin"]) From 9254c170d644c853f7824e27e733e0930b4ee8f2 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Mon, 8 Mar 2021 23:04:01 +0530 Subject: [PATCH 0797/1517] Move `use_span()` method from Tracer to `opentelemetry.trace.use_span()` (#1668) Fixes: #1630 --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 5 ++ docs/examples/basic_context/async_context.py | 2 +- .../src/opentelemetry/trace/__init__.py | 85 ++++++++++++------- opentelemetry-api/tests/trace/test_globals.py | 71 ++++++++++++++++ opentelemetry-api/tests/trace/test_tracer.py | 5 -- .../src/opentelemetry/sdk/trace/__init__.py | 50 ++--------- opentelemetry-sdk/tests/trace/test_trace.py | 24 ++---- .../shim/opentracing_shim/__init__.py | 11 ++- 9 files changed, 151 insertions(+), 104 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 73baecb785..12f49c1491 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 16ae58b341b960df52c1cf4541695164821b3638 + CONTRIB_REPO_SHA: 8783e0ff97ad123006ff1ff2c2cf3f52161a406f jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 47db772665..cd126d8a52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `max_attr_value_length` support to Jaeger exporter ([#1633])(https://github.com/open-telemetry/opentelemetry-python/pull/1633) +- Moved `use_span` from Tracer to `opentelemetry.trace.use_span`. + ([#1668](https://github.com/open-telemetry/opentelemetry-python/pull/1668)) +- `opentelemetry.trace.use_span()` will now overwrite previously set status on span in case an + exception is raised inside the context manager and `set_status_on_exception` is set to `True`. + ([#1668](https://github.com/open-telemetry/opentelemetry-python/pull/1668)) ### Changed - Rename `IdsGenerator` to `IdGenerator` diff --git a/docs/examples/basic_context/async_context.py b/docs/examples/basic_context/async_context.py index 41e049e2a6..d80ccb31e0 100644 --- a/docs/examples/basic_context/async_context.py +++ b/docs/examples/basic_context/async_context.py @@ -24,7 +24,7 @@ async def async_span(span): - with tracer.use_span(span): + with trace.use_span(span): ctx = baggage.set_baggage("foo", "bar") return ctx diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 54b5abd87b..6e32fb352d 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -80,9 +80,11 @@ from logging import getLogger from typing import Iterator, Optional, Sequence, cast +from opentelemetry import context as context_api from opentelemetry.context.context import Context from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER from opentelemetry.trace.propagation import ( + SPAN_KEY, get_current_span, set_span_in_context, ) @@ -101,7 +103,7 @@ format_span_id, format_trace_id, ) -from opentelemetry.trace.status import Status +from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types from opentelemetry.util._providers import _load_provider @@ -324,7 +326,7 @@ def start_as_current_span( is equivalent to:: span = tracer.start_span(name) - with tracer.use_span(span, end_on_exit=True): + with opentelemetry.trace.use_span(span, end_on_exit=True): do_work() Args: @@ -350,28 +352,6 @@ def start_as_current_span( The newly-created span. """ - @contextmanager # type: ignore - @abstractmethod - def use_span( - self, span: "Span", end_on_exit: bool = False, - ) -> Iterator[None]: - """Context manager for setting the passed span as the - current span in the context, as well as resetting the - context back upon exiting the context manager. - - Set the given span as the current span in this tracer's context. - - On exiting the context manager set the span that was previously active - as the current span (this is usually but not necessarily the parent of - the given span). If ``end_on_exit`` is ``True``, then the span is also - ended when exiting the context manager. - - Args: - span: The span to start and make current. - end_on_exit: Whether to end the span automatically when leaving the - context manager. - """ - class DefaultTracer(Tracer): """The default Tracer, used when no Tracer implementation is available. @@ -409,13 +389,6 @@ def start_as_current_span( # pylint: disable=unused-argument,no-self-use yield INVALID_SPAN - @contextmanager # type: ignore - def use_span( - self, span: "Span", end_on_exit: bool = False, - ) -> Iterator[None]: - # pylint: disable=unused-argument,no-self-use - yield - _TRACER_PROVIDER = None @@ -466,6 +439,55 @@ def get_tracer_provider() -> TracerProvider: return _TRACER_PROVIDER +@contextmanager # type: ignore +def use_span( + span: Span, + end_on_exit: bool = False, + record_exception: bool = True, + set_status_on_exception: bool = True, +) -> Iterator[Span]: + """Takes a non-active span and activates it in the current context. + + Args: + span: The span that should be activated in the current context. + end_on_exit: Whether to end the span automatically when leaving the + context manager scope. + record_exception: Whether to record any exceptions raised within the + context as error event on the span. + set_status_on_exception: Only relevant if the returned span is used + in a with/context manager. Defines wether the span status will + be automatically set to ERROR when an uncaught exception is + raised in the span with block. The span status won't be set by + this mechanism if it was previously set manually. + """ + try: + token = context_api.attach(context_api.set_value(SPAN_KEY, span)) + try: + yield span + finally: + context_api.detach(token) + + except Exception as exc: # pylint: disable=broad-except + if isinstance(span, Span) and span.is_recording(): + # Record the exception as an event + if record_exception: + span.record_exception(exc) + + # Set status in case exception was raised + if set_status_on_exception: + span.set_status( + Status( + status_code=StatusCode.ERROR, + description="{}: {}".format(type(exc).__name__, exc), + ) + ) + raise + + finally: + if end_on_exit: + span.end() + + __all__ = [ "DEFAULT_TRACE_OPTIONS", "DEFAULT_TRACE_STATE", @@ -492,5 +514,6 @@ def get_tracer_provider() -> TracerProvider: "get_tracer_provider", "set_tracer_provider", "set_span_in_context", + "use_span", "Status", ] diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 55b040f2bc..a6cfb9eb2c 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -2,6 +2,27 @@ from unittest.mock import patch from opentelemetry import context, trace +from opentelemetry.trace.status import Status, StatusCode + + +class TestSpan(trace.NonRecordingSpan): + has_ended = False + recorded_exception = None + recorded_status = Status(status_code=StatusCode.UNSET) + + def set_status(self, status): + self.recorded_status = status + + def end(self, end_time=None): + self.has_ended = True + + def is_recording(self): + return not self.has_ended + + def record_exception( + self, exception, attributes=None, timestamp=None, escaped=False + ): + self.recorded_exception = exception class TestGlobals(unittest.TestCase): @@ -38,3 +59,53 @@ def test_get_current_span(self): finally: context.detach(token) self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN) + + +class TestUseTracer(unittest.TestCase): + def test_use_span(self): + self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN) + span = trace.NonRecordingSpan(trace.INVALID_SPAN_CONTEXT) + with trace.use_span(span): + self.assertIs(trace.get_current_span(), span) + self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN) + + def test_use_span_end_on_exit(self): + + test_span = TestSpan(trace.INVALID_SPAN_CONTEXT) + + with trace.use_span(test_span): + pass + self.assertFalse(test_span.has_ended) + + with trace.use_span(test_span, end_on_exit=True): + pass + self.assertTrue(test_span.has_ended) + + def test_use_span_exception(self): + class TestUseSpanException(Exception): + pass + + test_span = TestSpan(trace.INVALID_SPAN_CONTEXT) + exception = TestUseSpanException("test exception") + with self.assertRaises(TestUseSpanException): + with trace.use_span(test_span): + raise exception + + self.assertEqual(test_span.recorded_exception, exception) + + def test_use_span_set_status(self): + class TestUseSpanException(Exception): + pass + + test_span = TestSpan(trace.INVALID_SPAN_CONTEXT) + with self.assertRaises(TestUseSpanException): + with trace.use_span(test_span): + raise TestUseSpanException("test error") + + self.assertEqual( + test_span.recorded_status.status_code, StatusCode.ERROR + ) + self.assertEqual( + test_span.recorded_status.description, + "TestUseSpanException: test error", + ) diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py index 0ae3a0505b..d0c451e1f2 100644 --- a/opentelemetry-api/tests/trace/test_tracer.py +++ b/opentelemetry-api/tests/trace/test_tracer.py @@ -29,11 +29,6 @@ def test_start_as_current_span(self): with self.tracer.start_as_current_span("") as span: self.assertIsInstance(span, trace.Span) - def test_use_span(self): - span = trace.NonRecordingSpan(trace.INVALID_SPAN_CONTEXT) - with self.tracer.use_span(span): - pass - def test_get_current_span(self): with self.tracer.start_as_current_span("test") as span: trace.get_current_span().set_attribute("test", "test") diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index f7b3ecd566..6fc2fcddd3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -776,10 +776,7 @@ def __exit__( self.record_exception(exception=exc_val, escaped=True) # Records status if span is used as context manager # i.e. with tracer.start_span() as span: - if ( - self._status.status_code is StatusCode.UNSET - and self._set_status_on_exception - ): + if self._set_status_on_exception: self.set_status( Status( status_code=StatusCode.ERROR, @@ -869,7 +866,12 @@ def start_as_current_span( record_exception=record_exception, set_status_on_exception=set_status_on_exception, ) - with self.use_span(span, end_on_exit=end_on_exit) as span_context: + with trace_api.use_span( + span, + end_on_exit=end_on_exit, + record_exception=record_exception, + set_status_on_exception=set_status_on_exception, + ) as span_context: yield span_context def start_span( # pylint: disable=too-many-locals @@ -950,44 +952,6 @@ def start_span( # pylint: disable=too-many-locals span = trace_api.NonRecordingSpan(context=span_context) return span - @contextmanager - def use_span( - self, span: trace_api.Span, end_on_exit: bool = False, - ) -> Iterator[trace_api.Span]: - try: - token = context_api.attach(context_api.set_value(SPAN_KEY, span)) - try: - yield span - finally: - context_api.detach(token) - - except Exception as exc: # pylint: disable=broad-except - # Record the exception as an event - if isinstance(span, Span) and span.is_recording(): - # pylint:disable=protected-access - if span._record_exception: - span.record_exception(exc) - - # Records status if use_span is used - # i.e. with tracer.start_as_current_span() as span: - if ( - span._status.status_code is StatusCode.UNSET - and span._set_status_on_exception - ): - span.set_status( - Status( - status_code=StatusCode.ERROR, - description="{}: {}".format( - type(exc).__name__, exc - ), - ) - ) - raise - - finally: - if end_on_exit: - span.end() - class TracerProvider(trace_api.TracerProvider): """See `opentelemetry.trace.TracerProvider`.""" diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index e2acad09b7..2bcc1bdd19 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -122,18 +122,6 @@ def run_general_code(shutdown_on_exit, explicit_shutdown): out = run_general_code(False, False) self.assertTrue(out.startswith(b"0")) - def test_use_span_exception(self): - class TestUseSpanException(Exception): - pass - - default_span = trace_api.NonRecordingSpan( - trace_api.INVALID_SPAN_CONTEXT - ) - tracer = new_tracer() - with self.assertRaises(TestUseSpanException): - with tracer.use_span(default_span): - raise TestUseSpanException() - def test_tracer_provider_accepts_concurrent_multi_span_processor(self): span_processor = trace.ConcurrentMultiSpanProcessor(2) tracer_provider = trace.TracerProvider( @@ -307,7 +295,7 @@ def test_start_span_implicit(self): self.assertIsNone(root.end_time) self.assertEqual(root.kind, trace_api.SpanKind.INTERNAL) - with tracer.use_span(root, True): + with trace_api.use_span(root, True): self.assertIs(trace_api.get_current_span(), root) with tracer.start_span( @@ -364,7 +352,7 @@ def test_start_span_explicit(self): self.assertIsNone(root.end_time) # Test with the implicit root span - with tracer.use_span(root, True): + with trace_api.use_span(root, True): self.assertIs(trace_api.get_current_span(), root) with tracer.start_span("stepchild", other_parent_context) as child: @@ -947,7 +935,7 @@ def error_status_test(context): .start_as_current_span("root") ) - def test_override_error_status(self): + def test_last_status_wins(self): def error_status_test(context): with self.assertRaises(AssertionError): with context as root: @@ -956,8 +944,10 @@ def error_status_test(context): ) raise AssertionError("unknown") - self.assertIs(root.status.status_code, StatusCode.OK) - self.assertEqual(root.status.description, "OK") + self.assertIs(root.status.status_code, StatusCode.ERROR) + self.assertEqual( + root.status.description, "AssertionError: unknown" + ) error_status_test( trace.TracerProvider().get_tracer(__name__).start_span("root") diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index c1dffbc1f9..b7a365302f 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -112,6 +112,7 @@ TracerProvider, get_current_span, set_span_in_context, + use_span, ) from opentelemetry.util.types import Attributes @@ -322,7 +323,7 @@ class ScopeShim(Scope): It is necessary to have both ways for constructing `ScopeShim` objects because in some cases we need to create the object from an OpenTelemetry `opentelemetry.trace.Span` context manager (as returned by - :meth:`opentelemetry.trace.Tracer.use_span`), in which case our only way of + :meth:`opentelemetry.trace.use_span`), in which case our only way of retrieving a `opentelemetry.trace.Span` object is by calling the ``__enter__()`` method on the context manager, which makes the span active in the OpenTelemetry tracer; whereas in other cases we need to accept a @@ -365,7 +366,7 @@ def from_context_manager(cls, manager: "ScopeManagerShim", span_cm): Example usage:: span = otel_tracer.start_span("TestSpan") - span_cm = otel_tracer.use_span(span) + span_cm = opentelemetry.trace.use_span(span) scope_shim = ScopeShim.from_context_manager( scope_manager_shim, span_cm=span_cm, @@ -375,7 +376,7 @@ def from_context_manager(cls, manager: "ScopeManagerShim", span_cm): manager: The :class:`ScopeManagerShim` that created this :class:`ScopeShim`. span_cm: A context manager as returned by - :meth:`opentelemetry.trace.Tracer.use_span`. + :meth:`opentelemetry.trace.use_span`. """ otel_span = span_cm.__enter__() @@ -452,9 +453,7 @@ def activate(self, span: SpanShim, finish_on_close: bool) -> "ScopeShim": A :class:`ScopeShim` representing the activated span. """ - span_cm = self._tracer.unwrap().use_span( - span.unwrap(), end_on_exit=finish_on_close - ) + span_cm = use_span(span.unwrap(), end_on_exit=finish_on_close) return ScopeShim.from_context_manager(self, span_cm=span_cm) @property From 6f42def1d3bed5fd1812316f0e8e0e62d02ff76c Mon Sep 17 00:00:00 2001 From: Jan Losinski Date: Mon, 8 Mar 2021 19:57:06 +0100 Subject: [PATCH 0798/1517] Add possibility to split oversized udp batches (#1500) --- CHANGELOG.md | 2 + .../opentelemetry/exporter/jaeger/__init__.py | 14 ++++- .../exporter/jaeger/send/thrift.py | 28 ++++++++-- .../tests/test_jaeger_exporter_thrift.py | 55 +++++++++++++++++++ .../sdk/environment_variables/__init__.py | 3 + 5 files changed, 96 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd126d8a52..d7268cde0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry.trace.use_span()` will now overwrite previously set status on span in case an exception is raised inside the context manager and `set_status_on_exception` is set to `True`. ([#1668](https://github.com/open-telemetry/opentelemetry-python/pull/1668)) +- Add `udp_split_oversized_batches` support to jaeger exporter + ([#1500](https://github.com/open-telemetry/opentelemetry-python/pull/1500)) ### Changed - Rename `IdsGenerator` to `IdGenerator` diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index a98dca33ed..80c5f04317 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -86,6 +86,7 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_JAEGER_AGENT_HOST, OTEL_EXPORTER_JAEGER_AGENT_PORT, + OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES, OTEL_EXPORTER_JAEGER_ENDPOINT, OTEL_EXPORTER_JAEGER_PASSWORD, OTEL_EXPORTER_JAEGER_USER, @@ -122,6 +123,7 @@ class JaegerExporter(SpanExporter): credentials: Credentials for server authentication. transport_format: Transport format for exporting spans to collector. max_tag_value_length: Max length string attribute values can have. Set to None to disable. + udp_split_oversized_batches: Re-emit oversized batches in smaller chunks. """ def __init__( @@ -136,6 +138,7 @@ def __init__( credentials: Optional[ChannelCredentials] = None, transport_format: Optional[str] = None, max_tag_value_length: Optional[int] = None, + udp_split_oversized_batches: bool = None, ): self.service_name = service_name self._max_tag_value_length = max_tag_value_length @@ -155,8 +158,17 @@ def __init__( env_variable=environ_agent_port, default=DEFAULT_AGENT_PORT, ) + self.udp_split_oversized_batches = _parameter_setter( + param=udp_split_oversized_batches, + env_variable=environ.get( + OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES + ), + default=False, + ) self._agent_client = AgentClientUDP( - host_name=self.agent_host_name, port=self.agent_port + host_name=self.agent_host_name, + port=self.agent_port, + split_oversized_batches=self.udp_split_oversized_batches, ) self.collector_endpoint = _parameter_setter( param=collector_endpoint, diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/thrift.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/thrift.py index 151e017b45..fe4f463319 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/thrift.py @@ -14,6 +14,7 @@ import base64 import logging +import math import socket from thrift.protocol import TBinaryProtocol, TCompactProtocol @@ -36,6 +37,7 @@ class AgentClientUDP: port: The port of the Jaeger server. max_packet_size: Maximum size of UDP packet. client: Class for creating new client objects for agencies. + split_oversized_batches: Re-emit oversized batches in smaller chunks. """ def __init__( @@ -44,6 +46,7 @@ def __init__( port, max_packet_size=UDP_PACKET_MAX_LENGTH, client=agent.Client, + split_oversized_batches=False, ): self.address = (host_name, port) self.max_packet_size = max_packet_size @@ -51,6 +54,7 @@ def __init__( self.client = client( iprot=TCompactProtocol.TCompactProtocol(trans=self.buffer) ) + self.split_oversized_batches = split_oversized_batches def emit(self, batch: jaeger.Batch): """ @@ -66,11 +70,25 @@ def emit(self, batch: jaeger.Batch): self.client.emitBatch(batch) buff = self.buffer.getvalue() if len(buff) > self.max_packet_size: - logger.warning( - "Data exceeds the max UDP packet size; size %r, max %r", - len(buff), - self.max_packet_size, - ) + if self.split_oversized_batches and len(batch.spans) > 1: + packets = math.ceil(len(buff) / self.max_packet_size) + div = math.ceil(len(batch.spans) / packets) + for packet in range(packets): + start = packet * div + end = (packet + 1) * div + if start < len(batch.spans): + self.emit( + jaeger.Batch( + process=batch.process, + spans=batch.spans[start:end], + ) + ) + else: + logger.warning( + "Data exceeds the max UDP packet size; size %r, max %r", + len(buff), + self.max_packet_size, + ) return with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_socket: diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py index 5b611093ae..750b050fe2 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py @@ -520,3 +520,58 @@ def test_max_tag_value_length(self): self.assertEqual("hello", tags_by_keys["key_string"]) self.assertEqual("('tup", tags_by_keys["key_tuple"]) self.assertEqual("some_", tags_by_keys["key_resource"]) + + def test_agent_client_split(self): + agent_client = jaeger_exporter.AgentClientUDP( + host_name="localhost", + port=6354, + max_packet_size=250, + split_oversized_batches=True, + ) + + translator = jaeger_exporter.Translate((self._test_span,)) + small_batch = jaeger.Batch( + # pylint: disable=protected-access + spans=translator._translate(ThriftTranslator()), + process=jaeger.Process(serviceName="xxx"), + ) + + with unittest.mock.patch( + "socket.socket.sendto", autospec=True + ) as fake_sendto: + agent_client.emit(small_batch) + self.assertEqual(fake_sendto.call_count, 1) + + translator = jaeger_exporter.Translate([self._test_span] * 2) + large_batch = jaeger.Batch( + # pylint: disable=protected-access + spans=translator._translate(ThriftTranslator()), + process=jaeger.Process(serviceName="xxx"), + ) + + with unittest.mock.patch( + "socket.socket.sendto", autospec=True + ) as fake_sendto: + agent_client.emit(large_batch) + self.assertEqual(fake_sendto.call_count, 2) + + def test_agent_client_dont_send_empty_spans(self): + agent_client = jaeger_exporter.AgentClientUDP( + host_name="localhost", + port=6354, + max_packet_size=415, + split_oversized_batches=True, + ) + + translator = jaeger_exporter.Translate([self._test_span] * 4) + large_batch = jaeger.Batch( + # pylint: disable=protected-access + spans=translator._translate(ThriftTranslator()), + process=jaeger.Process(serviceName="xxx"), + ) + + with unittest.mock.patch( + "socket.socket.sendto", autospec=True + ) as fake_sendto: + agent_client.emit(large_batch) + self.assertEqual(fake_sendto.call_count, 4) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 79d9c29c20..283aaae470 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -46,3 +46,6 @@ OTEL_EXPORTER_JAEGER_CERTIFICATE = "OTEL_EXPORTER_JAEGER_CERTIFICATE" OTEL_EXPORTER_OTLP_INSECURE = "OTEL_EXPORTER_OTLP_INSECURE" OTEL_EXPORTER_OTLP_SPAN_INSECURE = "OTEL_EXPORTER_OTLP_SPAN_INSECURE" +OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES = ( + "OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES" +) From c387aa382cc31e2e2afb777fa93e579ca60776ad Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 9 Mar 2021 01:25:13 +0530 Subject: [PATCH 0799/1517] Document how to work with fork process web server models(Gunicorn, uWSGI etc...) (#1609) --- CHANGELOG.md | 2 + README.md | 8 +- docs/examples/fork-process-model/README.rst | 66 ++++++++++++++++ .../flask-gunicorn/README.rst | 11 +++ .../fork-process-model/flask-gunicorn/app.py | 58 ++++++++++++++ .../flask-gunicorn/gunicorn.config.py | 50 ++++++++++++ .../flask-gunicorn/requirements.txt | 20 +++++ .../fork-process-model/flask-uwsgi/README.rst | 12 +++ .../fork-process-model/flask-uwsgi/app.py | 76 +++++++++++++++++++ .../flask-uwsgi/requirements.txt | 20 +++++ 10 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 docs/examples/fork-process-model/README.rst create mode 100644 docs/examples/fork-process-model/flask-gunicorn/README.rst create mode 100644 docs/examples/fork-process-model/flask-gunicorn/app.py create mode 100644 docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py create mode 100644 docs/examples/fork-process-model/flask-gunicorn/requirements.txt create mode 100644 docs/examples/fork-process-model/flask-uwsgi/README.rst create mode 100644 docs/examples/fork-process-model/flask-uwsgi/app.py create mode 100644 docs/examples/fork-process-model/flask-uwsgi/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index d7268cde0a..35216b2100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.18b0...HEAD) ### Added +- Document how to work with fork process web server models(Gunicorn, uWSGI etc...) + ([#1609](https://github.com/open-telemetry/opentelemetry-python/pull/1609)) - Add `max_attr_value_length` support to Jaeger exporter ([#1633])(https://github.com/open-telemetry/opentelemetry-python/pull/1633) - Moved `use_span` from Tracer to `opentelemetry.trace.use_span`. diff --git a/README.md b/README.md index b848b930be..3ab7bd25ec 100644 --- a/README.md +++ b/README.md @@ -101,11 +101,15 @@ machine. 1. Install scalene using the following command -`pip install scalene` +```sh +pip install scalene +``` 2. Run the `scalene` tests on any of the example Python programs -`scalene opentelemetry-/tests/performance/resource-usage//profile_resource_usage_.py` +```sh +scalene opentelemetry-/tests/performance/resource-usage//profile_resource_usage_.py +``` ## Documentation diff --git a/docs/examples/fork-process-model/README.rst b/docs/examples/fork-process-model/README.rst new file mode 100644 index 0000000000..ba09530f93 --- /dev/null +++ b/docs/examples/fork-process-model/README.rst @@ -0,0 +1,66 @@ +Working With Fork Process Models +================================ + +The `BatchSpanProcessor` is not fork-safe and doesn't work well with application servers +(Gunicorn, uWSGI) which are based on the pre-fork web server model. The `BatchSpanProcessor` +spawns a thread to run in the background to export spans to the telemetry backend. During the fork, the child +process inherits the lock which is held by the parent process and deadlock occurs. We can use fork hooks to +get around this limitation of the span processor. + +Please see http://bugs.python.org/issue6721 for the problems about Python locks in (multi)threaded +context with fork. + +Gunicorn post_fork hook +----------------------- + +.. code-block:: python + + from opentelemetry import trace + from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter + from opentelemetry.sdk.resources import Resource + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + + + def post_fork(server, worker): + server.log.info("Worker spawned (pid: %s)", worker.pid) + + resource = Resource.create(attributes={ + "service.name": "api-service" + }) + + trace.set_tracer_provider(TracerProvider(resource=resource)) + span_processor = BatchSpanProcessor( + OTLPSpanExporter(endpoint="localhost:4317") + ) + trace.get_tracer_provider().add_span_processor(span_processor) + + +uWSGI postfork decorator +------------------------ + +.. code-block:: python + + from uwsgidecorators import postfork + + from opentelemetry import trace + from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter + from opentelemetry.sdk.resources import Resource + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + + + @postfork + def init_tracing(): + resource = Resource.create(attributes={ + "service.name": "api-service" + }) + + trace.set_tracer_provider(TracerProvider(resource=resource)) + span_processor = BatchSpanProcessor( + OTLPSpanExporter(endpoint="localhost:4317") + ) + trace.get_tracer_provider().add_span_processor(span_processor) + + +The source code for the examples with Flask app are available :scm_web:`here `. diff --git a/docs/examples/fork-process-model/flask-gunicorn/README.rst b/docs/examples/fork-process-model/flask-gunicorn/README.rst new file mode 100644 index 0000000000..ec7a2e9e60 --- /dev/null +++ b/docs/examples/fork-process-model/flask-gunicorn/README.rst @@ -0,0 +1,11 @@ +Installation +------------ +.. code-block:: sh + + pip install -rrequirements.txt + +Run application +--------------- +.. code-block:: sh + + gunicorn app -c gunicorn.config.py diff --git a/docs/examples/fork-process-model/flask-gunicorn/app.py b/docs/examples/fork-process-model/flask-gunicorn/app.py new file mode 100644 index 0000000000..e8ea6a1dea --- /dev/null +++ b/docs/examples/fork-process-model/flask-gunicorn/app.py @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import flask +from flask import request + +from opentelemetry import trace +from opentelemetry.instrumentation.flask import FlaskInstrumentor + +application = flask.Flask(__name__) + +FlaskInstrumentor().instrument_app(application) + + +def fib_slow(n): + if n <= 1: + return n + return fib_slow(n - 1) + fib_fast(n - 2) + + +def fib_fast(n): + nth_fib = [0] * (n + 2) + nth_fib[1] = 1 + for i in range(2, n + 1): + nth_fib[i] = nth_fib[i - 1] + nth_fib[i - 2] + return nth_fib[n] + + +@application.route("/fibonacci") +def fibonacci(): + tracer = trace.get_tracer(__name__) + n = int(request.args.get("n", 1)) + with tracer.start_as_current_span("root"): + with tracer.start_as_current_span("fib_slow") as slow_span: + ans = fib_slow(n) + slow_span.set_attribute("n", n) + slow_span.set_attribute("nth_fibonacci", ans) + with tracer.start_as_current_span("fib_fast") as fast_span: + ans = fib_fast(n) + fast_span.set_attribute("n", n) + fast_span.set_attribute("nth_fibonacci", ans) + + return "F({}) is: ({})".format(n, ans) + + +if __name__ == "__main__": + application.run() diff --git a/docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py b/docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py new file mode 100644 index 0000000000..237f5244b3 --- /dev/null +++ b/docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py @@ -0,0 +1,50 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry import trace +from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +bind = "127.0.0.1:8000" + +# Sample Worker processes +workers = 4 +worker_class = "sync" +worker_connections = 1000 +timeout = 30 +keepalive = 2 + +# Sample logging +errorlog = "-" +loglevel = "info" +accesslog = "-" +access_log_format = ( + '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' +) + + +def post_fork(server, worker): + server.log.info("Worker spawned (pid: %s)", worker.pid) + + resource = Resource.create(attributes={"service.name": "api-service"}) + + trace.set_tracer_provider(TracerProvider(resource=resource)) + # This uses insecure connection for the purpose of example. Please see the + # OTLP Exporter documentation for other options. + span_processor = BatchSpanProcessor( + OTLPSpanExporter(endpoint="localhost:4317", insecure=True) + ) + trace.get_tracer_provider().add_span_processor(span_processor) diff --git a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt new file mode 100644 index 0000000000..e36515841e --- /dev/null +++ b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt @@ -0,0 +1,20 @@ +click==7.1.2 +Flask==1.1.2 +googleapis-common-protos==1.52.0 +grpcio==1.35.0 +gunicorn==20.0.4 +itsdangerous==1.1.0 +Jinja2==2.11.3 +MarkupSafe==1.1.1 +opentelemetry-api==0.18b0 +opentelemetry-exporter-otlp==0.18b0 +opentelemetry-instrumentation==0.18b0 +opentelemetry-instrumentation-flask==0.18b1 +opentelemetry-instrumentation-wsgi==0.18b1 +opentelemetry-sdk==0.18b0 +protobuf==3.14.0 +six==1.15.0 +thrift==0.13.0 +uWSGI==2.0.19.1 +Werkzeug==1.0.1 +wrapt==1.12.1 diff --git a/docs/examples/fork-process-model/flask-uwsgi/README.rst b/docs/examples/fork-process-model/flask-uwsgi/README.rst new file mode 100644 index 0000000000..d9310e03f4 --- /dev/null +++ b/docs/examples/fork-process-model/flask-uwsgi/README.rst @@ -0,0 +1,12 @@ +Installation +------------ +.. code-block:: sh + + pip install -rrequirements.txt + +Run application +--------------- + +.. code-block:: sh + + uwsgi --http :8000 --wsgi-file app.py --callable application --master --enable-threads diff --git a/docs/examples/fork-process-model/flask-uwsgi/app.py b/docs/examples/fork-process-model/flask-uwsgi/app.py new file mode 100644 index 0000000000..d7683e5916 --- /dev/null +++ b/docs/examples/fork-process-model/flask-uwsgi/app.py @@ -0,0 +1,76 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import flask +from flask import request +from uwsgidecorators import postfork + +from opentelemetry import trace +from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.instrumentation.flask import FlaskInstrumentor +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +application = flask.Flask(__name__) + +FlaskInstrumentor().instrument_app(application) + + +@postfork +def init_tracing(): + resource = Resource.create(attributes={"service.name": "api-service"}) + + trace.set_tracer_provider(TracerProvider(resource=resource)) + # This uses insecure connection for the purpose of example. Please see the + # OTLP Exporter documentation for other options. + span_processor = BatchSpanProcessor( + OTLPSpanExporter(endpoint="localhost:4317", insecure=True) + ) + trace.get_tracer_provider().add_span_processor(span_processor) + + +def fib_slow(n): + if n <= 1: + return n + return fib_slow(n - 1) + fib_fast(n - 2) + + +def fib_fast(n): + nth_fib = [0] * (n + 2) + nth_fib[1] = 1 + for i in range(2, n + 1): + nth_fib[i] = nth_fib[i - 1] + nth_fib[i - 2] + return nth_fib[n] + + +@application.route("/fibonacci") +def fibonacci(): + tracer = trace.get_tracer(__name__) + n = int(request.args.get("n", 1)) + with tracer.start_as_current_span("root"): + with tracer.start_as_current_span("fib_slow") as slow_span: + ans = fib_slow(n) + slow_span.set_attribute("n", n) + slow_span.set_attribute("nth_fibonacci", ans) + with tracer.start_as_current_span("fib_fast") as fast_span: + ans = fib_fast(n) + fast_span.set_attribute("n", n) + fast_span.set_attribute("nth_fibonacci", ans) + + return "F({}) is: ({})".format(n, ans) + + +if __name__ == "__main__": + application.run() diff --git a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt new file mode 100644 index 0000000000..e36515841e --- /dev/null +++ b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt @@ -0,0 +1,20 @@ +click==7.1.2 +Flask==1.1.2 +googleapis-common-protos==1.52.0 +grpcio==1.35.0 +gunicorn==20.0.4 +itsdangerous==1.1.0 +Jinja2==2.11.3 +MarkupSafe==1.1.1 +opentelemetry-api==0.18b0 +opentelemetry-exporter-otlp==0.18b0 +opentelemetry-instrumentation==0.18b0 +opentelemetry-instrumentation-flask==0.18b1 +opentelemetry-instrumentation-wsgi==0.18b1 +opentelemetry-sdk==0.18b0 +protobuf==3.14.0 +six==1.15.0 +thrift==0.13.0 +uWSGI==2.0.19.1 +Werkzeug==1.0.1 +wrapt==1.12.1 From a3a5e55721823f0c6148d62de82beb7f460cd8d5 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 8 Mar 2021 15:33:20 -0500 Subject: [PATCH 0800/1517] fixed changelog formatting issues (#1679) --- CHANGELOG.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35216b2100..c8c8c763a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Document how to work with fork process web server models(Gunicorn, uWSGI etc...) ([#1609](https://github.com/open-telemetry/opentelemetry-python/pull/1609)) - Add `max_attr_value_length` support to Jaeger exporter - ([#1633])(https://github.com/open-telemetry/opentelemetry-python/pull/1633) + ([#1633](https://github.com/open-telemetry/opentelemetry-python/pull/1633)) - Moved `use_span` from Tracer to `opentelemetry.trace.use_span`. ([#1668](https://github.com/open-telemetry/opentelemetry-python/pull/1668)) - `opentelemetry.trace.use_span()` will now overwrite previously set status on span in case an @@ -21,49 +21,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Rename `IdsGenerator` to `IdGenerator` - ([#1651])(https://github.com/open-telemetry/opentelemetry-python/pull/1651) + ([#1651](https://github.com/open-telemetry/opentelemetry-python/pull/1651)) - Make TracerProvider's resource attribute private - ([#1652])(https://github.com/open-telemetry/opentelemetry-python/pull/1652) + ([#1652](https://github.com/open-telemetry/opentelemetry-python/pull/1652)) - Rename Resource's `create_empty` to `get_empty` - ([#1653])(https://github.com/open-telemetry/opentelemetry-python/pull/1653) + ([#1653](https://github.com/open-telemetry/opentelemetry-python/pull/1653)) - Renamed `BatchExportSpanProcessor` to `BatchSpanProcessor` and `SimpleExportSpanProcessor` to `SimpleSpanProcessor` - ([#1656])(https://github.com/open-telemetry/opentelemetry-python/pull/1656) + ([#1656](https://github.com/open-telemetry/opentelemetry-python/pull/1656)) - Rename `DefaultSpan` to `NonRecordingSpan` - ([#1661])(https://github.com/open-telemetry/opentelemetry-python/pull/1661) + ([#1661](https://github.com/open-telemetry/opentelemetry-python/pull/1661)) - Fixed distro configuration with `OTEL_TRACES_EXPORTER` env var set to `otlp` - ([#1657])(https://github.com/open-telemetry/opentelemetry-python/pull/1657) + ([#1657](https://github.com/open-telemetry/opentelemetry-python/pull/1657)) - Moving `Getter`, `Setter` and `TextMapPropagator` out of `opentelemetry.trace.propagation` and into `opentelemetry.propagators` - ([#1662])(https://github.com/open-telemetry/opentelemetry-python/pull/1662) + ([#1662](https://github.com/open-telemetry/opentelemetry-python/pull/1662)) - Rename `BaggagePropagator` to `W3CBaggagePropagator` - ([#1663])(https://github.com/open-telemetry/opentelemetry-python/pull/1663) + ([#1663](https://github.com/open-telemetry/opentelemetry-python/pull/1663)) - Rename `JaegerSpanExporter` to `JaegerExporter` and rename `ZipkinSpanExporter` to `ZipkinExporter` - ([#1664])(https://github.com/open-telemetry/opentelemetry-python/pull/1664) + ([#1664](https://github.com/open-telemetry/opentelemetry-python/pull/1664)) ## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 ### Added - Add urllib to opentelemetry-bootstrap target list - ([#1584])(https://github.com/open-telemetry/opentelemetry-python/pull/1584) + ([#1584](https://github.com/open-telemetry/opentelemetry-python/pull/1584)) ## [1.0.0rc1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0rc1) - 2021-02-12 ### Changed - Tracer provider environment variables are now consistent with the rest - ([#1571](https://github.com/open-telemetry/opentelemetry-python/pull/1571)]) + ([#1571](https://github.com/open-telemetry/opentelemetry-python/pull/1571)) - Rename `TRACE_` to `TRACES_` for environment variables - ([#1595](https://github.com/open-telemetry/opentelemetry-python/pull/1595)]) + ([#1595](https://github.com/open-telemetry/opentelemetry-python/pull/1595)) - Limits for Span attributes, events and links have been updated to 128 - ([1597](https://github.com/open-telemetry/opentelemetry-python/pull/1597)]) + ([1597](https://github.com/open-telemetry/opentelemetry-python/pull/1597)) - Read-only Span attributes have been moved to ReadableSpan class ([#1560](https://github.com/open-telemetry/opentelemetry-python/pull/1560)) - `BatchExportSpanProcessor` flushes export queue when it reaches `max_export_batch_size` - ([#1521])(https://github.com/open-telemetry/opentelemetry-python/pull/1521) + ([#1521](https://github.com/open-telemetry/opentelemetry-python/pull/1521)) ### Added - Added `end_on_exit` argument to `start_as_current_span` - ([#1519](https://github.com/open-telemetry/opentelemetry-python/pull/1519)]) + ([#1519](https://github.com/open-telemetry/opentelemetry-python/pull/1519)) - Add `Span.set_attributes` method to set multiple values with one call ([#1520](https://github.com/open-telemetry/opentelemetry-python/pull/1520)) - Make sure Resources follow semantic conventions @@ -105,7 +105,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-exporter-zipkin` Add support for array attributes in Span and Resource exports ([#1285](https://github.com/open-telemetry/opentelemetry-python/pull/1285)) - Added `__repr__` for `DefaultSpan`, added `trace_flags` to `__repr__` of - `SpanContext` ([#1485](https://github.com/open-telemetry/opentelemetry-python/pull/1485)]) + `SpanContext` ([#1485](https://github.com/open-telemetry/opentelemetry-python/pull/1485)) - `opentelemetry-sdk` Add support for OTEL_TRACE_SAMPLER and OTEL_TRACE_SAMPLER_ARG env variables ([#1496](https://github.com/open-telemetry/opentelemetry-python/pull/1496)) - Adding `opentelemetry-distro` package to add default configuration for From 8aaf3f60b52367c2bd14c8f2834a47599ec07d54 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 8 Mar 2021 12:57:57 -0800 Subject: [PATCH 0801/1517] cleaning up trace/span id format methods (#1675) --- CHANGELOG.md | 4 +++ .../exporter/zipkin/encoder/__init__.py | 11 +++++--- .../tests/encoder/test_v1_json.py | 8 +++--- .../trace/propagation/tracecontext.py | 7 ++--- .../src/opentelemetry/trace/span.py | 26 ++++++++++++------- .../propagators/test_global_httptextformat.py | 5 ++-- .../src/opentelemetry/sdk/trace/__init__.py | 8 ++++-- .../opentelemetry/propagators/b3/__init__.py | 11 +------- .../propagators/jaeger/__init__.py | 8 ++++-- .../tests/test_jaeger_propagator.py | 10 ++----- 10 files changed, 54 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8c8c763a8..bfac59d9cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Rename `JaegerSpanExporter` to `JaegerExporter` and rename `ZipkinSpanExporter` to `ZipkinExporter` ([#1664](https://github.com/open-telemetry/opentelemetry-python/pull/1664)) +### Removed +- Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. + ([#1675])(https://github.com/open-telemetry/opentelemetry-python/pull/1675) + ## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 ### Added diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py index 2d4aad6b24..2b9e53af6e 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py @@ -25,7 +25,12 @@ from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk.trace import Event -from opentelemetry.trace import Span, SpanContext +from opentelemetry.trace import ( + Span, + SpanContext, + format_span_id, + format_trace_id, +) from opentelemetry.trace.status import StatusCode EncodedLocalEndpointT = TypeVar("EncodedLocalEndpointT") @@ -260,8 +265,8 @@ def _encode_local_endpoint(local_endpoint: NodeEndpoint) -> Dict: @staticmethod def _encode_span_id(span_id: int) -> str: - return format(span_id, "016x") + return format_span_id(span_id) @staticmethod def _encode_trace_id(trace_id: int) -> str: - return format(trace_id, "032x") + return format_trace_id(trace_id) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py index 13bc24e6cd..c8cdb0edac 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py @@ -18,7 +18,7 @@ from opentelemetry.exporter.zipkin.encoder.v1.json import JsonV1Encoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace -from opentelemetry.trace import TraceFlags +from opentelemetry.trace import TraceFlags, format_span_id, format_trace_id from .common_tests import TEST_SERVICE_NAME, CommonEncoderTestCases @@ -194,13 +194,13 @@ def test_encode_id_zero_padding(self): expected_output = [ { - "traceId": format(trace_id, "032x"), - "id": format(span_id, "016x"), + "traceId": format_trace_id(trace_id), + "id": format_span_id(span_id), "name": TEST_SERVICE_NAME, "timestamp": JsonV1Encoder._nsec_to_usec_round(start_time), "duration": JsonV1Encoder._nsec_to_usec_round(duration), "debug": True, - "parentId": format(parent_id, "016x"), + "parentId": format_span_id(parent_id), } ] diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index e648338e2c..480e716bf7 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -18,6 +18,7 @@ import opentelemetry.trace as trace from opentelemetry.context.context import Context from opentelemetry.propagators import textmap +from opentelemetry.trace import format_span_id, format_trace_id from opentelemetry.trace.span import TraceState @@ -96,10 +97,10 @@ def inject( span_context = span.get_span_context() if span_context == trace.INVALID_SPAN_CONTEXT: return - traceparent_string = "00-{:032x}-{:016x}-{:02x}".format( - span_context.trace_id, - span_context.span_id, + traceparent_string = "00-{trace_id}-{span_id}-{:02x}".format( span_context.trace_flags, + trace_id=format_trace_id(span_context.trace_id), + span_id=format_span_id(span_context.span_id), ) set_in_carrier( carrier, self._TRACEPARENT_HEADER_NAME, traceparent_string diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index dc4ce88339..d04cdfa49d 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -471,7 +471,7 @@ def __delattr__(self, *args: str) -> None: def __repr__(self) -> str: return ( - "{}(trace_id={}, span_id={}, trace_flags=0x{:02x}, trace_state={!r}, is_remote={})" + "{}(trace_id=0x{}, span_id=0x{}, trace_flags=0x{:02x}, trace_state={!r}, is_remote={})" ).format( type(self).__name__, format_trace_id(self.trace_id), @@ -548,16 +548,22 @@ def __repr__(self) -> str: def format_trace_id(trace_id: int) -> str: - return "0x{:032x}".format(trace_id) - - -def format_span_id(span_id: int) -> str: - return "0x{:016x}".format(span_id) + """Convenience trace ID formatting method + Args: + trace_id: Trace ID int + Returns: + The trace ID as 32-byte hexadecimal string + """ + return format(trace_id, "032x") -def get_hexadecimal_trace_id(trace_id: int) -> str: - return "{:032x}".format(trace_id) +def format_span_id(span_id: int) -> str: + """Convenience span ID formatting method + Args: + span_id: Span ID int -def get_hexadecimal_span_id(span_id: int) -> str: - return "{:016x}".format(span_id) + Returns: + The span ID as 16-byte hexadecimal string + """ + return format(span_id, "016x") diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index 0b69f6f286..faa4023d5d 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -18,6 +18,7 @@ from opentelemetry.propagate import extract, inject from opentelemetry.propagators.textmap import DictGetter from opentelemetry.trace import get_current_span, set_span_in_context +from opentelemetry.trace.span import format_span_id, format_trace_id carrier_getter = DictGetter() @@ -30,8 +31,8 @@ class TestDefaultGlobalPropagator(unittest.TestCase): def test_propagation(self): traceparent_value = "00-{trace_id}-{span_id}-00".format( - trace_id=format(self.TRACE_ID, "032x"), - span_id=format(self.SPAN_ID, "016x"), + trace_id=format_trace_id(self.TRACE_ID), + span_id=format_span_id(self.SPAN_ID), ) tracestate_value = "foo=1,bar=2,baz=3" headers = { diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 6fc2fcddd3..944d4002d6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -521,8 +521,12 @@ def to_json(self, indent=4): @staticmethod def _format_context(context): x_ctx = OrderedDict() - x_ctx["trace_id"] = trace_api.format_trace_id(context.trace_id) - x_ctx["span_id"] = trace_api.format_span_id(context.span_id) + x_ctx["trace_id"] = "0x{}".format( + trace_api.format_trace_id(context.trace_id) + ) + x_ctx["span_id"] = "0x{}".format( + trace_api.format_span_id(context.span_id) + ) x_ctx["trace_state"] = repr(context.trace_state) return x_ctx diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py index 448e59517b..01abcc7c87 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py @@ -23,6 +23,7 @@ TextMapPropagator, TextMapPropagatorT, ) +from opentelemetry.trace import format_span_id, format_trace_id class B3Format(TextMapPropagator): @@ -162,16 +163,6 @@ def fields(self) -> typing.Set[str]: } -def format_trace_id(trace_id: int) -> str: - """Format the trace id according to b3 specification.""" - return format(trace_id, "032x") - - -def format_span_id(span_id: int) -> str: - """Format the span id according to b3 specification.""" - return format(span_id, "016x") - - def _extract_first_element( items: typing.Iterable[TextMapPropagatorT], ) -> typing.Optional[TextMapPropagatorT]: diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py index e10d464be6..8e7fe5f69f 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py @@ -24,6 +24,7 @@ TextMapPropagator, TextMapPropagatorT, ) +from opentelemetry.trace import format_span_id, format_trace_id class JaegerPropagator(TextMapPropagator): @@ -132,8 +133,11 @@ def _extract_baggage(self, getter, carrier, context): def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags): - return "{:032x}:{:016x}:{:016x}:{:02x}".format( - trace_id, span_id, parent_span_id, flags + return "{trace_id}:{span_id}:{parent_id}:{:02x}".format( + flags, + trace_id=format_trace_id(trace_id), + span_id=format_span_id(span_id), + parent_id=format_span_id(parent_span_id), ) diff --git a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py index 8e7190d1cc..da8a855edb 100644 --- a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py @@ -59,12 +59,6 @@ def get_context_new_carrier(old_carrier, carrier_baggage=None): return ctx, new_carrier -def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags): - return "{:032x}:{:016x}:{:016x}:{:02x}".format( - trace_id, span_id, parent_span_id, flags - ) - - class TestJaegerPropagator(unittest.TestCase): @classmethod def setUpClass(cls): @@ -72,7 +66,7 @@ def setUpClass(cls): cls.trace_id = generator.generate_trace_id() cls.span_id = generator.generate_span_id() cls.parent_span_id = generator.generate_span_id() - cls.serialized_uber_trace_id = _format_uber_trace_id( + cls.serialized_uber_trace_id = jaeger._format_uber_trace_id( # pylint: disable=protected-access cls.trace_id, cls.span_id, cls.parent_span_id, 11 ) @@ -123,7 +117,7 @@ def test_debug_flag_set(self): self.assertEqual(FORMAT.DEBUG_FLAG, debug_flag_value) def test_sample_debug_flags_unset(self): - uber_trace_id = _format_uber_trace_id( + uber_trace_id = jaeger._format_uber_trace_id( # pylint: disable=protected-access self.trace_id, self.span_id, self.parent_span_id, 0 ) old_carrier = {FORMAT.TRACE_ID_KEY: uber_trace_id} From 50d0bc04400e9cbbd35b2185da4b1f24515d99f9 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 9 Mar 2021 03:15:04 +0530 Subject: [PATCH 0802/1517] Use opentelemetry-proto 0.7.0 (#1674) --- CHANGELOG.md | 2 + .../collector/logs/v1/logs_service_pb2.pyi | 70 +-- .../metrics/v1/metrics_service_pb2.pyi | 70 +-- .../collector/trace/v1/trace_service_pb2.pyi | 70 +-- .../proto/common/v1/common_pb2.pyi | 171 +++-- .../opentelemetry/proto/logs/v1/logs_pb2.pyi | 347 +++++----- .../experimental/configservice_pb2.pyi | 140 ++--- .../proto/metrics/v1/metrics_pb2.py | 229 ++++++- .../proto/metrics/v1/metrics_pb2.pyi | 595 ++++++++++-------- .../proto/resource/v1/resource_pb2.pyi | 69 +- .../proto/trace/v1/trace_config_pb2.pyi | 165 +++-- .../proto/trace/v1/trace_pb2.pyi | 421 ++++++------- scripts/proto_codegen.sh | 2 +- 13 files changed, 1231 insertions(+), 1120 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfac59d9cd..0ecb4cc30e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.18b0...HEAD) +- Update OTLP exporter to use OTLP proto `0.7.0` + ([#1674](https://github.com/open-telemetry/opentelemetry-python/pull/1674)) ### Added - Document how to work with fork process web server models(Gunicorn, uWSGI etc...) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi index f93d6312eb..da5d542e05 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi @@ -1,56 +1,34 @@ -# @generated by generate_proto_mypy_stubs.py. Do not edit! -import sys -from google.protobuf.descriptor import ( - Descriptor as google___protobuf___descriptor___Descriptor, - FileDescriptor as google___protobuf___descriptor___FileDescriptor, -) - -from google.protobuf.internal.containers import ( - RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, -) - -from google.protobuf.message import ( - Message as google___protobuf___message___Message, -) - -from opentelemetry.proto.logs.v1.logs_pb2 import ( - ResourceLogs as opentelemetry___proto___logs___v1___logs_pb2___ResourceLogs, -) - -from typing import ( - Iterable as typing___Iterable, - Optional as typing___Optional, -) - -from typing_extensions import ( - Literal as typing_extensions___Literal, -) - - -builtin___bool = bool -builtin___bytes = bytes -builtin___float = float -builtin___int = int - - -DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... - -class ExportLogsServiceRequest(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import opentelemetry.proto.logs.v1.logs_pb2 +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... + +class ExportLogsServiceRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + RESOURCE_LOGS_FIELD_NUMBER: builtins.int @property - def resource_logs(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___logs___v1___logs_pb2___ResourceLogs]: ... + def resource_logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.logs.v1.logs_pb2.ResourceLogs]: ... def __init__(self, *, - resource_logs : typing___Optional[typing___Iterable[opentelemetry___proto___logs___v1___logs_pb2___ResourceLogs]] = None, + resource_logs : typing.Optional[typing.Iterable[opentelemetry.proto.logs.v1.logs_pb2.ResourceLogs]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"resource_logs",b"resource_logs"]) -> None: ... -type___ExportLogsServiceRequest = ExportLogsServiceRequest + def ClearField(self, field_name: typing_extensions.Literal[u"resource_logs",b"resource_logs"]) -> None: ... +global___ExportLogsServiceRequest = ExportLogsServiceRequest -class ExportLogsServiceResponse(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +class ExportLogsServiceResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... def __init__(self, ) -> None: ... -type___ExportLogsServiceResponse = ExportLogsServiceResponse +global___ExportLogsServiceResponse = ExportLogsServiceResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi index 31efaf83cd..be50774a30 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi @@ -1,56 +1,34 @@ -# @generated by generate_proto_mypy_stubs.py. Do not edit! -import sys -from google.protobuf.descriptor import ( - Descriptor as google___protobuf___descriptor___Descriptor, - FileDescriptor as google___protobuf___descriptor___FileDescriptor, -) - -from google.protobuf.internal.containers import ( - RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, -) - -from google.protobuf.message import ( - Message as google___protobuf___message___Message, -) - -from opentelemetry.proto.metrics.v1.metrics_pb2 import ( - ResourceMetrics as opentelemetry___proto___metrics___v1___metrics_pb2___ResourceMetrics, -) - -from typing import ( - Iterable as typing___Iterable, - Optional as typing___Optional, -) - -from typing_extensions import ( - Literal as typing_extensions___Literal, -) - - -builtin___bool = bool -builtin___bytes = bytes -builtin___float = float -builtin___int = int - - -DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... - -class ExportMetricsServiceRequest(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import opentelemetry.proto.metrics.v1.metrics_pb2 +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... + +class ExportMetricsServiceRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + RESOURCE_METRICS_FIELD_NUMBER: builtins.int @property - def resource_metrics(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___metrics___v1___metrics_pb2___ResourceMetrics]: ... + def resource_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.metrics.v1.metrics_pb2.ResourceMetrics]: ... def __init__(self, *, - resource_metrics : typing___Optional[typing___Iterable[opentelemetry___proto___metrics___v1___metrics_pb2___ResourceMetrics]] = None, + resource_metrics : typing.Optional[typing.Iterable[opentelemetry.proto.metrics.v1.metrics_pb2.ResourceMetrics]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"resource_metrics",b"resource_metrics"]) -> None: ... -type___ExportMetricsServiceRequest = ExportMetricsServiceRequest + def ClearField(self, field_name: typing_extensions.Literal[u"resource_metrics",b"resource_metrics"]) -> None: ... +global___ExportMetricsServiceRequest = ExportMetricsServiceRequest -class ExportMetricsServiceResponse(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +class ExportMetricsServiceResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... def __init__(self, ) -> None: ... -type___ExportMetricsServiceResponse = ExportMetricsServiceResponse +global___ExportMetricsServiceResponse = ExportMetricsServiceResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi index 1f36dfd707..f40bd9a179 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi @@ -1,56 +1,34 @@ -# @generated by generate_proto_mypy_stubs.py. Do not edit! -import sys -from google.protobuf.descriptor import ( - Descriptor as google___protobuf___descriptor___Descriptor, - FileDescriptor as google___protobuf___descriptor___FileDescriptor, -) - -from google.protobuf.internal.containers import ( - RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, -) - -from google.protobuf.message import ( - Message as google___protobuf___message___Message, -) - -from opentelemetry.proto.trace.v1.trace_pb2 import ( - ResourceSpans as opentelemetry___proto___trace___v1___trace_pb2___ResourceSpans, -) - -from typing import ( - Iterable as typing___Iterable, - Optional as typing___Optional, -) - -from typing_extensions import ( - Literal as typing_extensions___Literal, -) - - -builtin___bool = bool -builtin___bytes = bytes -builtin___float = float -builtin___int = int - - -DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... - -class ExportTraceServiceRequest(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import opentelemetry.proto.trace.v1.trace_pb2 +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... + +class ExportTraceServiceRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + RESOURCE_SPANS_FIELD_NUMBER: builtins.int @property - def resource_spans(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___trace___v1___trace_pb2___ResourceSpans]: ... + def resource_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.trace.v1.trace_pb2.ResourceSpans]: ... def __init__(self, *, - resource_spans : typing___Optional[typing___Iterable[opentelemetry___proto___trace___v1___trace_pb2___ResourceSpans]] = None, + resource_spans : typing.Optional[typing.Iterable[opentelemetry.proto.trace.v1.trace_pb2.ResourceSpans]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"resource_spans",b"resource_spans"]) -> None: ... -type___ExportTraceServiceRequest = ExportTraceServiceRequest + def ClearField(self, field_name: typing_extensions.Literal[u"resource_spans",b"resource_spans"]) -> None: ... +global___ExportTraceServiceRequest = ExportTraceServiceRequest -class ExportTraceServiceResponse(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +class ExportTraceServiceResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... def __init__(self, ) -> None: ... -type___ExportTraceServiceResponse = ExportTraceServiceResponse +global___ExportTraceServiceResponse = ExportTraceServiceResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi index 4305729cb9..7afb08ab9a 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi @@ -1,128 +1,121 @@ -# @generated by generate_proto_mypy_stubs.py. Do not edit! -import sys -from google.protobuf.descriptor import ( - Descriptor as google___protobuf___descriptor___Descriptor, - FileDescriptor as google___protobuf___descriptor___FileDescriptor, -) - -from google.protobuf.internal.containers import ( - RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, -) - -from google.protobuf.message import ( - Message as google___protobuf___message___Message, -) - -from typing import ( - Iterable as typing___Iterable, - Optional as typing___Optional, - Text as typing___Text, -) - -from typing_extensions import ( - Literal as typing_extensions___Literal, -) - - -builtin___bool = bool -builtin___bytes = bytes -builtin___float = float -builtin___int = int - - -DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... - -class AnyValue(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - string_value: typing___Text = ... - bool_value: builtin___bool = ... - int_value: builtin___int = ... - double_value: builtin___float = ... +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... + +class AnyValue(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + STRING_VALUE_FIELD_NUMBER: builtins.int + BOOL_VALUE_FIELD_NUMBER: builtins.int + INT_VALUE_FIELD_NUMBER: builtins.int + DOUBLE_VALUE_FIELD_NUMBER: builtins.int + ARRAY_VALUE_FIELD_NUMBER: builtins.int + KVLIST_VALUE_FIELD_NUMBER: builtins.int + string_value: typing.Text = ... + bool_value: builtins.bool = ... + int_value: builtins.int = ... + double_value: builtins.float = ... @property - def array_value(self) -> type___ArrayValue: ... + def array_value(self) -> global___ArrayValue: ... @property - def kvlist_value(self) -> type___KeyValueList: ... + def kvlist_value(self) -> global___KeyValueList: ... def __init__(self, *, - string_value : typing___Optional[typing___Text] = None, - bool_value : typing___Optional[builtin___bool] = None, - int_value : typing___Optional[builtin___int] = None, - double_value : typing___Optional[builtin___float] = None, - array_value : typing___Optional[type___ArrayValue] = None, - kvlist_value : typing___Optional[type___KeyValueList] = None, + string_value : typing.Text = ..., + bool_value : builtins.bool = ..., + int_value : builtins.int = ..., + double_value : builtins.float = ..., + array_value : typing.Optional[global___ArrayValue] = ..., + kvlist_value : typing.Optional[global___KeyValueList] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions___Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions___Literal[u"value",b"value"]) -> typing_extensions___Literal["string_value","bool_value","int_value","double_value","array_value","kvlist_value"]: ... -type___AnyValue = AnyValue + def HasField(self, field_name: typing_extensions.Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal[u"value",b"value"]) -> typing_extensions.Literal["string_value","bool_value","int_value","double_value","array_value","kvlist_value"]: ... +global___AnyValue = AnyValue -class ArrayValue(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +class ArrayValue(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + VALUES_FIELD_NUMBER: builtins.int @property - def values(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___AnyValue]: ... + def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___AnyValue]: ... def __init__(self, *, - values : typing___Optional[typing___Iterable[type___AnyValue]] = None, + values : typing.Optional[typing.Iterable[global___AnyValue]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"values",b"values"]) -> None: ... -type___ArrayValue = ArrayValue + def ClearField(self, field_name: typing_extensions.Literal[u"values",b"values"]) -> None: ... +global___ArrayValue = ArrayValue -class KeyValueList(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +class KeyValueList(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + VALUES_FIELD_NUMBER: builtins.int @property - def values(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___KeyValue]: ... + def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___KeyValue]: ... def __init__(self, *, - values : typing___Optional[typing___Iterable[type___KeyValue]] = None, + values : typing.Optional[typing.Iterable[global___KeyValue]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"values",b"values"]) -> None: ... -type___KeyValueList = KeyValueList + def ClearField(self, field_name: typing_extensions.Literal[u"values",b"values"]) -> None: ... +global___KeyValueList = KeyValueList -class KeyValue(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - key: typing___Text = ... +class KeyValue(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: typing.Text = ... @property - def value(self) -> type___AnyValue: ... + def value(self) -> global___AnyValue: ... def __init__(self, *, - key : typing___Optional[typing___Text] = None, - value : typing___Optional[type___AnyValue] = None, + key : typing.Text = ..., + value : typing.Optional[global___AnyValue] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions___Literal[u"value",b"value"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"key",b"key",u"value",b"value"]) -> None: ... -type___KeyValue = KeyValue + def HasField(self, field_name: typing_extensions.Literal[u"value",b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"key",b"key",u"value",b"value"]) -> None: ... +global___KeyValue = KeyValue -class StringKeyValue(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - key: typing___Text = ... - value: typing___Text = ... +class StringKeyValue(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: typing.Text = ... + value: typing.Text = ... def __init__(self, *, - key : typing___Optional[typing___Text] = None, - value : typing___Optional[typing___Text] = None, + key : typing.Text = ..., + value : typing.Text = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"key",b"key",u"value",b"value"]) -> None: ... -type___StringKeyValue = StringKeyValue + def ClearField(self, field_name: typing_extensions.Literal[u"key",b"key",u"value",b"value"]) -> None: ... +global___StringKeyValue = StringKeyValue -class InstrumentationLibrary(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - name: typing___Text = ... - version: typing___Text = ... +class InstrumentationLibrary(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + NAME_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + name: typing.Text = ... + version: typing.Text = ... def __init__(self, *, - name : typing___Optional[typing___Text] = None, - version : typing___Optional[typing___Text] = None, + name : typing.Text = ..., + version : typing.Text = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"name",b"name",u"version",b"version"]) -> None: ... -type___InstrumentationLibrary = InstrumentationLibrary + def ClearField(self, field_name: typing_extensions.Literal[u"name",b"name",u"version",b"version"]) -> None: ... +global___InstrumentationLibrary = InstrumentationLibrary diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi index 0f280f6f6c..a8d51507aa 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi @@ -1,158 +1,131 @@ -# @generated by generate_proto_mypy_stubs.py. Do not edit! -import sys -from google.protobuf.descriptor import ( - Descriptor as google___protobuf___descriptor___Descriptor, - EnumDescriptor as google___protobuf___descriptor___EnumDescriptor, - FileDescriptor as google___protobuf___descriptor___FileDescriptor, -) - -from google.protobuf.internal.containers import ( - RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, -) - -from google.protobuf.internal.enum_type_wrapper import ( - _EnumTypeWrapper as google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper, -) - -from google.protobuf.message import ( - Message as google___protobuf___message___Message, -) - -from opentelemetry.proto.common.v1.common_pb2 import ( - AnyValue as opentelemetry___proto___common___v1___common_pb2___AnyValue, - InstrumentationLibrary as opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary, - KeyValue as opentelemetry___proto___common___v1___common_pb2___KeyValue, -) - -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as opentelemetry___proto___resource___v1___resource_pb2___Resource, -) - -from typing import ( - Iterable as typing___Iterable, - NewType as typing___NewType, - Optional as typing___Optional, - Text as typing___Text, - cast as typing___cast, -) - -from typing_extensions import Literal as typing_extensions___Literal - -builtin___bool = bool -builtin___bytes = bytes -builtin___float = float -builtin___int = int - -DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... - -SeverityNumberValue = typing___NewType("SeverityNumberValue", builtin___int) -type___SeverityNumberValue = SeverityNumberValue -SeverityNumber: _SeverityNumber +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import opentelemetry.proto.common.v1.common_pb2 +import opentelemetry.proto.resource.v1.resource_pb2 +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... + +global___SeverityNumber = SeverityNumber class _SeverityNumber( - google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[ - SeverityNumberValue - ] + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + SeverityNumber.V + ], + builtins.type, ): - DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... - SEVERITY_NUMBER_UNSPECIFIED = typing___cast(SeverityNumberValue, 0) - SEVERITY_NUMBER_TRACE = typing___cast(SeverityNumberValue, 1) - SEVERITY_NUMBER_TRACE2 = typing___cast(SeverityNumberValue, 2) - SEVERITY_NUMBER_TRACE3 = typing___cast(SeverityNumberValue, 3) - SEVERITY_NUMBER_TRACE4 = typing___cast(SeverityNumberValue, 4) - SEVERITY_NUMBER_DEBUG = typing___cast(SeverityNumberValue, 5) - SEVERITY_NUMBER_DEBUG2 = typing___cast(SeverityNumberValue, 6) - SEVERITY_NUMBER_DEBUG3 = typing___cast(SeverityNumberValue, 7) - SEVERITY_NUMBER_DEBUG4 = typing___cast(SeverityNumberValue, 8) - SEVERITY_NUMBER_INFO = typing___cast(SeverityNumberValue, 9) - SEVERITY_NUMBER_INFO2 = typing___cast(SeverityNumberValue, 10) - SEVERITY_NUMBER_INFO3 = typing___cast(SeverityNumberValue, 11) - SEVERITY_NUMBER_INFO4 = typing___cast(SeverityNumberValue, 12) - SEVERITY_NUMBER_WARN = typing___cast(SeverityNumberValue, 13) - SEVERITY_NUMBER_WARN2 = typing___cast(SeverityNumberValue, 14) - SEVERITY_NUMBER_WARN3 = typing___cast(SeverityNumberValue, 15) - SEVERITY_NUMBER_WARN4 = typing___cast(SeverityNumberValue, 16) - SEVERITY_NUMBER_ERROR = typing___cast(SeverityNumberValue, 17) - SEVERITY_NUMBER_ERROR2 = typing___cast(SeverityNumberValue, 18) - SEVERITY_NUMBER_ERROR3 = typing___cast(SeverityNumberValue, 19) - SEVERITY_NUMBER_ERROR4 = typing___cast(SeverityNumberValue, 20) - SEVERITY_NUMBER_FATAL = typing___cast(SeverityNumberValue, 21) - SEVERITY_NUMBER_FATAL2 = typing___cast(SeverityNumberValue, 22) - SEVERITY_NUMBER_FATAL3 = typing___cast(SeverityNumberValue, 23) - SEVERITY_NUMBER_FATAL4 = typing___cast(SeverityNumberValue, 24) - -SEVERITY_NUMBER_UNSPECIFIED = typing___cast(SeverityNumberValue, 0) -SEVERITY_NUMBER_TRACE = typing___cast(SeverityNumberValue, 1) -SEVERITY_NUMBER_TRACE2 = typing___cast(SeverityNumberValue, 2) -SEVERITY_NUMBER_TRACE3 = typing___cast(SeverityNumberValue, 3) -SEVERITY_NUMBER_TRACE4 = typing___cast(SeverityNumberValue, 4) -SEVERITY_NUMBER_DEBUG = typing___cast(SeverityNumberValue, 5) -SEVERITY_NUMBER_DEBUG2 = typing___cast(SeverityNumberValue, 6) -SEVERITY_NUMBER_DEBUG3 = typing___cast(SeverityNumberValue, 7) -SEVERITY_NUMBER_DEBUG4 = typing___cast(SeverityNumberValue, 8) -SEVERITY_NUMBER_INFO = typing___cast(SeverityNumberValue, 9) -SEVERITY_NUMBER_INFO2 = typing___cast(SeverityNumberValue, 10) -SEVERITY_NUMBER_INFO3 = typing___cast(SeverityNumberValue, 11) -SEVERITY_NUMBER_INFO4 = typing___cast(SeverityNumberValue, 12) -SEVERITY_NUMBER_WARN = typing___cast(SeverityNumberValue, 13) -SEVERITY_NUMBER_WARN2 = typing___cast(SeverityNumberValue, 14) -SEVERITY_NUMBER_WARN3 = typing___cast(SeverityNumberValue, 15) -SEVERITY_NUMBER_WARN4 = typing___cast(SeverityNumberValue, 16) -SEVERITY_NUMBER_ERROR = typing___cast(SeverityNumberValue, 17) -SEVERITY_NUMBER_ERROR2 = typing___cast(SeverityNumberValue, 18) -SEVERITY_NUMBER_ERROR3 = typing___cast(SeverityNumberValue, 19) -SEVERITY_NUMBER_ERROR4 = typing___cast(SeverityNumberValue, 20) -SEVERITY_NUMBER_FATAL = typing___cast(SeverityNumberValue, 21) -SEVERITY_NUMBER_FATAL2 = typing___cast(SeverityNumberValue, 22) -SEVERITY_NUMBER_FATAL3 = typing___cast(SeverityNumberValue, 23) -SEVERITY_NUMBER_FATAL4 = typing___cast(SeverityNumberValue, 24) -type___SeverityNumber = SeverityNumber - -LogRecordFlagsValue = typing___NewType("LogRecordFlagsValue", builtin___int) -type___LogRecordFlagsValue = LogRecordFlagsValue -LogRecordFlags: _LogRecordFlags + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... + SEVERITY_NUMBER_UNSPECIFIED = SeverityNumber.V(0) + SEVERITY_NUMBER_TRACE = SeverityNumber.V(1) + SEVERITY_NUMBER_TRACE2 = SeverityNumber.V(2) + SEVERITY_NUMBER_TRACE3 = SeverityNumber.V(3) + SEVERITY_NUMBER_TRACE4 = SeverityNumber.V(4) + SEVERITY_NUMBER_DEBUG = SeverityNumber.V(5) + SEVERITY_NUMBER_DEBUG2 = SeverityNumber.V(6) + SEVERITY_NUMBER_DEBUG3 = SeverityNumber.V(7) + SEVERITY_NUMBER_DEBUG4 = SeverityNumber.V(8) + SEVERITY_NUMBER_INFO = SeverityNumber.V(9) + SEVERITY_NUMBER_INFO2 = SeverityNumber.V(10) + SEVERITY_NUMBER_INFO3 = SeverityNumber.V(11) + SEVERITY_NUMBER_INFO4 = SeverityNumber.V(12) + SEVERITY_NUMBER_WARN = SeverityNumber.V(13) + SEVERITY_NUMBER_WARN2 = SeverityNumber.V(14) + SEVERITY_NUMBER_WARN3 = SeverityNumber.V(15) + SEVERITY_NUMBER_WARN4 = SeverityNumber.V(16) + SEVERITY_NUMBER_ERROR = SeverityNumber.V(17) + SEVERITY_NUMBER_ERROR2 = SeverityNumber.V(18) + SEVERITY_NUMBER_ERROR3 = SeverityNumber.V(19) + SEVERITY_NUMBER_ERROR4 = SeverityNumber.V(20) + SEVERITY_NUMBER_FATAL = SeverityNumber.V(21) + SEVERITY_NUMBER_FATAL2 = SeverityNumber.V(22) + SEVERITY_NUMBER_FATAL3 = SeverityNumber.V(23) + SEVERITY_NUMBER_FATAL4 = SeverityNumber.V(24) + +class SeverityNumber(metaclass=_SeverityNumber): + V = typing.NewType("V", builtins.int) + +SEVERITY_NUMBER_UNSPECIFIED = SeverityNumber.V(0) +SEVERITY_NUMBER_TRACE = SeverityNumber.V(1) +SEVERITY_NUMBER_TRACE2 = SeverityNumber.V(2) +SEVERITY_NUMBER_TRACE3 = SeverityNumber.V(3) +SEVERITY_NUMBER_TRACE4 = SeverityNumber.V(4) +SEVERITY_NUMBER_DEBUG = SeverityNumber.V(5) +SEVERITY_NUMBER_DEBUG2 = SeverityNumber.V(6) +SEVERITY_NUMBER_DEBUG3 = SeverityNumber.V(7) +SEVERITY_NUMBER_DEBUG4 = SeverityNumber.V(8) +SEVERITY_NUMBER_INFO = SeverityNumber.V(9) +SEVERITY_NUMBER_INFO2 = SeverityNumber.V(10) +SEVERITY_NUMBER_INFO3 = SeverityNumber.V(11) +SEVERITY_NUMBER_INFO4 = SeverityNumber.V(12) +SEVERITY_NUMBER_WARN = SeverityNumber.V(13) +SEVERITY_NUMBER_WARN2 = SeverityNumber.V(14) +SEVERITY_NUMBER_WARN3 = SeverityNumber.V(15) +SEVERITY_NUMBER_WARN4 = SeverityNumber.V(16) +SEVERITY_NUMBER_ERROR = SeverityNumber.V(17) +SEVERITY_NUMBER_ERROR2 = SeverityNumber.V(18) +SEVERITY_NUMBER_ERROR3 = SeverityNumber.V(19) +SEVERITY_NUMBER_ERROR4 = SeverityNumber.V(20) +SEVERITY_NUMBER_FATAL = SeverityNumber.V(21) +SEVERITY_NUMBER_FATAL2 = SeverityNumber.V(22) +SEVERITY_NUMBER_FATAL3 = SeverityNumber.V(23) +SEVERITY_NUMBER_FATAL4 = SeverityNumber.V(24) + +global___LogRecordFlags = LogRecordFlags class _LogRecordFlags( - google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[ - LogRecordFlagsValue - ] + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + LogRecordFlags.V + ], + builtins.type, ): - DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... - LOG_RECORD_FLAG_UNSPECIFIED = typing___cast(LogRecordFlagsValue, 0) - LOG_RECORD_FLAG_TRACE_FLAGS_MASK = typing___cast(LogRecordFlagsValue, 255) + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... + LOG_RECORD_FLAG_UNSPECIFIED = LogRecordFlags.V(0) + LOG_RECORD_FLAG_TRACE_FLAGS_MASK = LogRecordFlags.V(255) -LOG_RECORD_FLAG_UNSPECIFIED = typing___cast(LogRecordFlagsValue, 0) -LOG_RECORD_FLAG_TRACE_FLAGS_MASK = typing___cast(LogRecordFlagsValue, 255) -type___LogRecordFlags = LogRecordFlags +class LogRecordFlags(metaclass=_LogRecordFlags): + V = typing.NewType("V", builtins.int) -class ResourceLogs(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +LOG_RECORD_FLAG_UNSPECIFIED = LogRecordFlags.V(0) +LOG_RECORD_FLAG_TRACE_FLAGS_MASK = LogRecordFlags.V(255) + +class ResourceLogs(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + RESOURCE_FIELD_NUMBER: builtins.int + INSTRUMENTATION_LIBRARY_LOGS_FIELD_NUMBER: builtins.int @property def resource( self, - ) -> opentelemetry___proto___resource___v1___resource_pb2___Resource: ... + ) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: ... @property def instrumentation_library_logs( self, - ) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[ - type___InstrumentationLibraryLogs + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___InstrumentationLibraryLogs ]: ... def __init__( self, *, - resource: typing___Optional[ - opentelemetry___proto___resource___v1___resource_pb2___Resource - ] = None, - instrumentation_library_logs: typing___Optional[ - typing___Iterable[type___InstrumentationLibraryLogs] - ] = None, + resource: typing.Optional[ + opentelemetry.proto.resource.v1.resource_pb2.Resource + ] = ..., + instrumentation_library_logs: typing.Optional[ + typing.Iterable[global___InstrumentationLibraryLogs] + ] = ..., ) -> None: ... def HasField( - self, field_name: typing_extensions___Literal["resource", b"resource"] - ) -> builtin___bool: ... + self, field_name: typing_extensions.Literal["resource", b"resource"] + ) -> builtins.bool: ... def ClearField( self, - field_name: typing_extensions___Literal[ + field_name: typing_extensions.Literal[ "instrumentation_library_logs", b"instrumentation_library_logs", "resource", @@ -160,37 +133,39 @@ class ResourceLogs(google___protobuf___message___Message): ], ) -> None: ... -type___ResourceLogs = ResourceLogs +global___ResourceLogs = ResourceLogs -class InstrumentationLibraryLogs(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +class InstrumentationLibraryLogs(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int + LOGS_FIELD_NUMBER: builtins.int @property def instrumentation_library( self, - ) -> opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary: ... + ) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: ... @property def logs( self, - ) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[ - type___LogRecord + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___LogRecord ]: ... def __init__( self, *, - instrumentation_library: typing___Optional[ - opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary - ] = None, - logs: typing___Optional[typing___Iterable[type___LogRecord]] = None, + instrumentation_library: typing.Optional[ + opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary + ] = ..., + logs: typing.Optional[typing.Iterable[global___LogRecord]] = ..., ) -> None: ... def HasField( self, - field_name: typing_extensions___Literal[ + field_name: typing_extensions.Literal[ "instrumentation_library", b"instrumentation_library" ], - ) -> builtin___bool: ... + ) -> builtins.bool: ... def ClearField( self, - field_name: typing_extensions___Literal[ + field_name: typing_extensions.Literal[ "instrumentation_library", b"instrumentation_library", "logs", @@ -198,54 +173,60 @@ class InstrumentationLibraryLogs(google___protobuf___message___Message): ], ) -> None: ... -type___InstrumentationLibraryLogs = InstrumentationLibraryLogs - -class LogRecord(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - time_unix_nano: builtin___int = ... - severity_number: type___SeverityNumberValue = ... - severity_text: typing___Text = ... - name: typing___Text = ... - dropped_attributes_count: builtin___int = ... - flags: builtin___int = ... - trace_id: builtin___bytes = ... - span_id: builtin___bytes = ... +global___InstrumentationLibraryLogs = InstrumentationLibraryLogs + +class LogRecord(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + SEVERITY_NUMBER_FIELD_NUMBER: builtins.int + SEVERITY_TEXT_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + BODY_FIELD_NUMBER: builtins.int + ATTRIBUTES_FIELD_NUMBER: builtins.int + DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int + FLAGS_FIELD_NUMBER: builtins.int + TRACE_ID_FIELD_NUMBER: builtins.int + SPAN_ID_FIELD_NUMBER: builtins.int + time_unix_nano: builtins.int = ... + severity_number: global___SeverityNumber.V = ... + severity_text: typing.Text = ... + name: typing.Text = ... + dropped_attributes_count: builtins.int = ... + flags: builtins.int = ... + trace_id: builtins.bytes = ... + span_id: builtins.bytes = ... @property - def body( - self, - ) -> opentelemetry___proto___common___v1___common_pb2___AnyValue: ... + def body(self) -> opentelemetry.proto.common.v1.common_pb2.AnyValue: ... @property def attributes( self, - ) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[ - opentelemetry___proto___common___v1___common_pb2___KeyValue + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + opentelemetry.proto.common.v1.common_pb2.KeyValue ]: ... def __init__( self, *, - time_unix_nano: typing___Optional[builtin___int] = None, - severity_number: typing___Optional[type___SeverityNumberValue] = None, - severity_text: typing___Optional[typing___Text] = None, - name: typing___Optional[typing___Text] = None, - body: typing___Optional[ - opentelemetry___proto___common___v1___common_pb2___AnyValue - ] = None, - attributes: typing___Optional[ - typing___Iterable[ - opentelemetry___proto___common___v1___common_pb2___KeyValue - ] - ] = None, - dropped_attributes_count: typing___Optional[builtin___int] = None, - flags: typing___Optional[builtin___int] = None, - trace_id: typing___Optional[builtin___bytes] = None, - span_id: typing___Optional[builtin___bytes] = None, + time_unix_nano: builtins.int = ..., + severity_number: global___SeverityNumber.V = ..., + severity_text: typing.Text = ..., + name: typing.Text = ..., + body: typing.Optional[ + opentelemetry.proto.common.v1.common_pb2.AnyValue + ] = ..., + attributes: typing.Optional[ + typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] + ] = ..., + dropped_attributes_count: builtins.int = ..., + flags: builtins.int = ..., + trace_id: builtins.bytes = ..., + span_id: builtins.bytes = ..., ) -> None: ... def HasField( - self, field_name: typing_extensions___Literal["body", b"body"] - ) -> builtin___bool: ... + self, field_name: typing_extensions.Literal["body", b"body"] + ) -> builtins.bool: ... def ClearField( self, - field_name: typing_extensions___Literal[ + field_name: typing_extensions.Literal[ "attributes", b"attributes", "body", @@ -269,4 +250,4 @@ class LogRecord(google___protobuf___message___Message): ], ) -> None: ... -type___LogRecord = LogRecord +global___LogRecord = LogRecord diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi index eeb17d2f31..7218e03264 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi @@ -1,104 +1,88 @@ -# @generated by generate_proto_mypy_stubs.py. Do not edit! -import sys -from google.protobuf.descriptor import ( - Descriptor as google___protobuf___descriptor___Descriptor, - FileDescriptor as google___protobuf___descriptor___FileDescriptor, -) - -from google.protobuf.internal.containers import ( - RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, -) - -from google.protobuf.message import ( - Message as google___protobuf___message___Message, -) - -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as opentelemetry___proto___resource___v1___resource_pb2___Resource, -) - -from typing import ( - Iterable as typing___Iterable, - Optional as typing___Optional, - Text as typing___Text, -) - -from typing_extensions import ( - Literal as typing_extensions___Literal, -) - - -builtin___bool = bool -builtin___bytes = bytes -builtin___float = float -builtin___int = int - - -DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... - -class MetricConfigRequest(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - last_known_fingerprint: builtin___bytes = ... +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import opentelemetry.proto.resource.v1.resource_pb2 +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... + +class MetricConfigRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + RESOURCE_FIELD_NUMBER: builtins.int + LAST_KNOWN_FINGERPRINT_FIELD_NUMBER: builtins.int + last_known_fingerprint: builtins.bytes = ... @property - def resource(self) -> opentelemetry___proto___resource___v1___resource_pb2___Resource: ... + def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: ... def __init__(self, *, - resource : typing___Optional[opentelemetry___proto___resource___v1___resource_pb2___Resource] = None, - last_known_fingerprint : typing___Optional[builtin___bytes] = None, + resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., + last_known_fingerprint : builtins.bytes = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions___Literal[u"resource",b"resource"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"last_known_fingerprint",b"last_known_fingerprint",u"resource",b"resource"]) -> None: ... -type___MetricConfigRequest = MetricConfigRequest - -class MetricConfigResponse(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - class Schedule(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - class Pattern(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - equals: typing___Text = ... - starts_with: typing___Text = ... + def HasField(self, field_name: typing_extensions.Literal[u"resource",b"resource"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"last_known_fingerprint",b"last_known_fingerprint",u"resource",b"resource"]) -> None: ... +global___MetricConfigRequest = MetricConfigRequest + +class MetricConfigResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + class Schedule(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + class Pattern(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + EQUALS_FIELD_NUMBER: builtins.int + STARTS_WITH_FIELD_NUMBER: builtins.int + equals: typing.Text = ... + starts_with: typing.Text = ... def __init__(self, *, - equals : typing___Optional[typing___Text] = None, - starts_with : typing___Optional[typing___Text] = None, + equals : typing.Text = ..., + starts_with : typing.Text = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions___Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions___Literal[u"match",b"match"]) -> typing_extensions___Literal["equals","starts_with"]: ... - type___Pattern = Pattern + def HasField(self, field_name: typing_extensions.Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal[u"match",b"match"]) -> typing_extensions.Literal["equals","starts_with"]: ... - period_sec: builtin___int = ... + EXCLUSION_PATTERNS_FIELD_NUMBER: builtins.int + INCLUSION_PATTERNS_FIELD_NUMBER: builtins.int + PERIOD_SEC_FIELD_NUMBER: builtins.int + period_sec: builtins.int = ... @property - def exclusion_patterns(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___MetricConfigResponse.Schedule.Pattern]: ... + def exclusion_patterns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule.Pattern]: ... @property - def inclusion_patterns(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___MetricConfigResponse.Schedule.Pattern]: ... + def inclusion_patterns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule.Pattern]: ... def __init__(self, *, - exclusion_patterns : typing___Optional[typing___Iterable[type___MetricConfigResponse.Schedule.Pattern]] = None, - inclusion_patterns : typing___Optional[typing___Iterable[type___MetricConfigResponse.Schedule.Pattern]] = None, - period_sec : typing___Optional[builtin___int] = None, + exclusion_patterns : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule.Pattern]] = ..., + inclusion_patterns : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule.Pattern]] = ..., + period_sec : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"exclusion_patterns",b"exclusion_patterns",u"inclusion_patterns",b"inclusion_patterns",u"period_sec",b"period_sec"]) -> None: ... - type___Schedule = Schedule + def ClearField(self, field_name: typing_extensions.Literal[u"exclusion_patterns",b"exclusion_patterns",u"inclusion_patterns",b"inclusion_patterns",u"period_sec",b"period_sec"]) -> None: ... - fingerprint: builtin___bytes = ... - suggested_wait_time_sec: builtin___int = ... + FINGERPRINT_FIELD_NUMBER: builtins.int + SCHEDULES_FIELD_NUMBER: builtins.int + SUGGESTED_WAIT_TIME_SEC_FIELD_NUMBER: builtins.int + fingerprint: builtins.bytes = ... + suggested_wait_time_sec: builtins.int = ... @property - def schedules(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___MetricConfigResponse.Schedule]: ... + def schedules(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule]: ... def __init__(self, *, - fingerprint : typing___Optional[builtin___bytes] = None, - schedules : typing___Optional[typing___Iterable[type___MetricConfigResponse.Schedule]] = None, - suggested_wait_time_sec : typing___Optional[builtin___int] = None, + fingerprint : builtins.bytes = ..., + schedules : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule]] = ..., + suggested_wait_time_sec : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"fingerprint",b"fingerprint",u"schedules",b"schedules",u"suggested_wait_time_sec",b"suggested_wait_time_sec"]) -> None: ... -type___MetricConfigResponse = MetricConfigResponse + def ClearField(self, field_name: typing_extensions.Literal[u"fingerprint",b"fingerprint",u"schedules",b"schedules",u"suggested_wait_time_sec",b"suggested_wait_time_sec"]) -> None: ... +global___MetricConfigResponse = MetricConfigResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py index 62956103e8..4e98c560fd 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py @@ -21,7 +21,7 @@ package='opentelemetry.proto.metrics.v1', syntax='proto3', serialized_options=b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1', - serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xb6\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x66\n\x1finstrumentation_library_metrics\x18\x02 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics\"\xb0\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\"\xd5\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12=\n\tint_gauge\x18\x04 \x01(\x0b\x32(.opentelemetry.proto.metrics.v1.IntGaugeH\x00\x12\x43\n\x0c\x64ouble_gauge\x18\x05 \x01(\x0b\x32+.opentelemetry.proto.metrics.v1.DoubleGaugeH\x00\x12\x39\n\x07int_sum\x18\x06 \x01(\x0b\x32&.opentelemetry.proto.metrics.v1.IntSumH\x00\x12?\n\ndouble_sum\x18\x07 \x01(\x0b\x32).opentelemetry.proto.metrics.v1.DoubleSumH\x00\x12\x45\n\rint_histogram\x18\x08 \x01(\x0b\x32,.opentelemetry.proto.metrics.v1.IntHistogramH\x00\x12K\n\x10\x64ouble_histogram\x18\t \x01(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleHistogramH\x00\x42\x06\n\x04\x64\x61ta\"M\n\x08IntGauge\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint\"S\n\x0b\x44oubleGauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleDataPoint\"\xba\x01\n\x06IntSum\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xc0\x01\n\tDoubleSum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xb3\x01\n\x0cIntHistogram\x12J\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x35.opentelemetry.proto.metrics.v1.IntHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xb9\x01\n\x0f\x44oubleHistogram\x12M\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x38.opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xd2\x01\n\x0cIntDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x10\x12>\n\texemplars\x18\x05 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar\"\xd8\x01\n\x0f\x44oubleDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x01\x12\x41\n\texemplars\x18\x05 \x03(\x0b\x32..opentelemetry.proto.metrics.v1.DoubleExemplar\"\x98\x02\n\x15IntHistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x10\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12>\n\texemplars\x18\x08 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar\"\x9e\x02\n\x18\x44oubleHistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12\x41\n\texemplars\x18\x08 \x03(\x0b\x32..opentelemetry.proto.metrics.v1.DoubleExemplar\"\x9f\x01\n\x0bIntExemplar\x12\x46\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\r\n\x05value\x18\x03 \x01(\x10\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\"\xa2\x01\n\x0e\x44oubleExemplar\x12\x46\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\r\n\x05value\x18\x03 \x01(\x01\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02\x42t\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1b\x06proto3' + serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xb6\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x66\n\x1finstrumentation_library_metrics\x18\x02 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics\"\xb0\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\"\x9e\x04\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12=\n\tint_gauge\x18\x04 \x01(\x0b\x32(.opentelemetry.proto.metrics.v1.IntGaugeH\x00\x12\x43\n\x0c\x64ouble_gauge\x18\x05 \x01(\x0b\x32+.opentelemetry.proto.metrics.v1.DoubleGaugeH\x00\x12\x39\n\x07int_sum\x18\x06 \x01(\x0b\x32&.opentelemetry.proto.metrics.v1.IntSumH\x00\x12?\n\ndouble_sum\x18\x07 \x01(\x0b\x32).opentelemetry.proto.metrics.v1.DoubleSumH\x00\x12\x45\n\rint_histogram\x18\x08 \x01(\x0b\x32,.opentelemetry.proto.metrics.v1.IntHistogramH\x00\x12K\n\x10\x64ouble_histogram\x18\t \x01(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleHistogramH\x00\x12G\n\x0e\x64ouble_summary\x18\x0b \x01(\x0b\x32-.opentelemetry.proto.metrics.v1.DoubleSummaryH\x00\x42\x06\n\x04\x64\x61ta\"M\n\x08IntGauge\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint\"S\n\x0b\x44oubleGauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleDataPoint\"\xba\x01\n\x06IntSum\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xc0\x01\n\tDoubleSum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xb3\x01\n\x0cIntHistogram\x12J\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x35.opentelemetry.proto.metrics.v1.IntHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xb9\x01\n\x0f\x44oubleHistogram\x12M\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x38.opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\\\n\rDoubleSummary\x12K\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x36.opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint\"\xd2\x01\n\x0cIntDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x10\x12>\n\texemplars\x18\x05 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar\"\xd8\x01\n\x0f\x44oubleDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x01\x12\x41\n\texemplars\x18\x05 \x03(\x0b\x32..opentelemetry.proto.metrics.v1.DoubleExemplar\"\x98\x02\n\x15IntHistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x10\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12>\n\texemplars\x18\x08 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar\"\x9e\x02\n\x18\x44oubleHistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12\x41\n\texemplars\x18\x08 \x03(\x0b\x32..opentelemetry.proto.metrics.v1.DoubleExemplar\"\xbe\x02\n\x16\x44oubleSummaryDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12_\n\x0fquantile_values\x18\x06 \x03(\x0b\x32\x46.opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.ValueAtQuantile\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01\"\x9f\x01\n\x0bIntExemplar\x12\x46\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\r\n\x05value\x18\x03 \x01(\x10\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\"\xa2\x01\n\x0e\x44oubleExemplar\x12\x46\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\r\n\x05value\x18\x03 \x01(\x01\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02\x42t\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -46,8 +46,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=3258, - serialized_end=3398, + serialized_start=3746, + serialized_end=3886, ) _sym_db.RegisterEnumDescriptor(_AGGREGATIONTEMPORALITY) @@ -204,6 +204,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='double_summary', full_name='opentelemetry.proto.metrics.v1.Metric.double_summary', index=9, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -220,7 +227,7 @@ index=0, containing_type=None, fields=[]), ], serialized_start=537, - serialized_end=1006, + serialized_end=1079, ) @@ -250,8 +257,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1008, - serialized_end=1085, + serialized_start=1081, + serialized_end=1158, ) @@ -281,8 +288,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1087, - serialized_end=1170, + serialized_start=1160, + serialized_end=1243, ) @@ -326,8 +333,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1173, - serialized_end=1359, + serialized_start=1246, + serialized_end=1432, ) @@ -371,8 +378,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1362, - serialized_end=1554, + serialized_start=1435, + serialized_end=1627, ) @@ -409,8 +416,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1557, - serialized_end=1736, + serialized_start=1630, + serialized_end=1809, ) @@ -447,8 +454,39 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1739, - serialized_end=1924, + serialized_start=1812, + serialized_end=1997, +) + + +_DOUBLESUMMARY = _descriptor.Descriptor( + name='DoubleSummary', + full_name='opentelemetry.proto.metrics.v1.DoubleSummary', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='data_points', full_name='opentelemetry.proto.metrics.v1.DoubleSummary.data_points', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1999, + serialized_end=2091, ) @@ -506,8 +544,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1927, - serialized_end=2137, + serialized_start=2094, + serialized_end=2304, ) @@ -565,8 +603,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2140, - serialized_end=2356, + serialized_start=2307, + serialized_end=2523, ) @@ -645,8 +683,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2359, - serialized_end=2639, + serialized_start=2526, + serialized_end=2806, ) @@ -725,8 +763,111 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2642, - serialized_end=2928, + serialized_start=2809, + serialized_end=3095, +) + + +_DOUBLESUMMARYDATAPOINT_VALUEATQUANTILE = _descriptor.Descriptor( + name='ValueAtQuantile', + full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.ValueAtQuantile', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='quantile', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.ValueAtQuantile.quantile', index=0, + number=1, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='value', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.ValueAtQuantile.value', index=1, + number=2, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=3366, + serialized_end=3416, +) + +_DOUBLESUMMARYDATAPOINT = _descriptor.Descriptor( + name='DoubleSummaryDataPoint', + full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='labels', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.labels', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.start_time_unix_nano', index=1, + number=2, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.time_unix_nano', index=2, + number=3, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='count', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.count', index=3, + number=4, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sum', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.sum', index=4, + number=5, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='quantile_values', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.quantile_values', index=5, + number=6, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_DOUBLESUMMARYDATAPOINT_VALUEATQUANTILE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=3098, + serialized_end=3416, ) @@ -784,8 +925,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2931, - serialized_end=3090, + serialized_start=3419, + serialized_end=3578, ) @@ -843,8 +984,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3093, - serialized_end=3255, + serialized_start=3581, + serialized_end=3743, ) _RESOURCEMETRICS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE @@ -857,6 +998,7 @@ _METRIC.fields_by_name['double_sum'].message_type = _DOUBLESUM _METRIC.fields_by_name['int_histogram'].message_type = _INTHISTOGRAM _METRIC.fields_by_name['double_histogram'].message_type = _DOUBLEHISTOGRAM +_METRIC.fields_by_name['double_summary'].message_type = _DOUBLESUMMARY _METRIC.oneofs_by_name['data'].fields.append( _METRIC.fields_by_name['int_gauge']) _METRIC.fields_by_name['int_gauge'].containing_oneof = _METRIC.oneofs_by_name['data'] @@ -875,6 +1017,9 @@ _METRIC.oneofs_by_name['data'].fields.append( _METRIC.fields_by_name['double_histogram']) _METRIC.fields_by_name['double_histogram'].containing_oneof = _METRIC.oneofs_by_name['data'] +_METRIC.oneofs_by_name['data'].fields.append( + _METRIC.fields_by_name['double_summary']) +_METRIC.fields_by_name['double_summary'].containing_oneof = _METRIC.oneofs_by_name['data'] _INTGAUGE.fields_by_name['data_points'].message_type = _INTDATAPOINT _DOUBLEGAUGE.fields_by_name['data_points'].message_type = _DOUBLEDATAPOINT _INTSUM.fields_by_name['data_points'].message_type = _INTDATAPOINT @@ -885,6 +1030,7 @@ _INTHISTOGRAM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY _DOUBLEHISTOGRAM.fields_by_name['data_points'].message_type = _DOUBLEHISTOGRAMDATAPOINT _DOUBLEHISTOGRAM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY +_DOUBLESUMMARY.fields_by_name['data_points'].message_type = _DOUBLESUMMARYDATAPOINT _INTDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE _INTDATAPOINT.fields_by_name['exemplars'].message_type = _INTEXEMPLAR _DOUBLEDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE @@ -893,6 +1039,9 @@ _INTHISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _INTEXEMPLAR _DOUBLEHISTOGRAMDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE _DOUBLEHISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _DOUBLEEXEMPLAR +_DOUBLESUMMARYDATAPOINT_VALUEATQUANTILE.containing_type = _DOUBLESUMMARYDATAPOINT +_DOUBLESUMMARYDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_DOUBLESUMMARYDATAPOINT.fields_by_name['quantile_values'].message_type = _DOUBLESUMMARYDATAPOINT_VALUEATQUANTILE _INTEXEMPLAR.fields_by_name['filtered_labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE _DOUBLEEXEMPLAR.fields_by_name['filtered_labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE DESCRIPTOR.message_types_by_name['ResourceMetrics'] = _RESOURCEMETRICS @@ -904,10 +1053,12 @@ DESCRIPTOR.message_types_by_name['DoubleSum'] = _DOUBLESUM DESCRIPTOR.message_types_by_name['IntHistogram'] = _INTHISTOGRAM DESCRIPTOR.message_types_by_name['DoubleHistogram'] = _DOUBLEHISTOGRAM +DESCRIPTOR.message_types_by_name['DoubleSummary'] = _DOUBLESUMMARY DESCRIPTOR.message_types_by_name['IntDataPoint'] = _INTDATAPOINT DESCRIPTOR.message_types_by_name['DoubleDataPoint'] = _DOUBLEDATAPOINT DESCRIPTOR.message_types_by_name['IntHistogramDataPoint'] = _INTHISTOGRAMDATAPOINT DESCRIPTOR.message_types_by_name['DoubleHistogramDataPoint'] = _DOUBLEHISTOGRAMDATAPOINT +DESCRIPTOR.message_types_by_name['DoubleSummaryDataPoint'] = _DOUBLESUMMARYDATAPOINT DESCRIPTOR.message_types_by_name['IntExemplar'] = _INTEXEMPLAR DESCRIPTOR.message_types_by_name['DoubleExemplar'] = _DOUBLEEXEMPLAR DESCRIPTOR.enum_types_by_name['AggregationTemporality'] = _AGGREGATIONTEMPORALITY @@ -976,6 +1127,13 @@ }) _sym_db.RegisterMessage(DoubleHistogram) +DoubleSummary = _reflection.GeneratedProtocolMessageType('DoubleSummary', (_message.Message,), { + 'DESCRIPTOR' : _DOUBLESUMMARY, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleSummary) + }) +_sym_db.RegisterMessage(DoubleSummary) + IntDataPoint = _reflection.GeneratedProtocolMessageType('IntDataPoint', (_message.Message,), { 'DESCRIPTOR' : _INTDATAPOINT, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' @@ -1004,6 +1162,21 @@ }) _sym_db.RegisterMessage(DoubleHistogramDataPoint) +DoubleSummaryDataPoint = _reflection.GeneratedProtocolMessageType('DoubleSummaryDataPoint', (_message.Message,), { + + 'ValueAtQuantile' : _reflection.GeneratedProtocolMessageType('ValueAtQuantile', (_message.Message,), { + 'DESCRIPTOR' : _DOUBLESUMMARYDATAPOINT_VALUEATQUANTILE, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.ValueAtQuantile) + }) + , + 'DESCRIPTOR' : _DOUBLESUMMARYDATAPOINT, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint) + }) +_sym_db.RegisterMessage(DoubleSummaryDataPoint) +_sym_db.RegisterMessage(DoubleSummaryDataPoint.ValueAtQuantile) + IntExemplar = _reflection.GeneratedProtocolMessageType('IntExemplar', (_message.Message,), { 'DESCRIPTOR' : _INTEXEMPLAR, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi index 5462660f08..d4e03f3a39 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi @@ -1,376 +1,465 @@ -# @generated by generate_proto_mypy_stubs.py. Do not edit! -import sys -from google.protobuf.descriptor import ( - Descriptor as google___protobuf___descriptor___Descriptor, - EnumDescriptor as google___protobuf___descriptor___EnumDescriptor, - FileDescriptor as google___protobuf___descriptor___FileDescriptor, -) - -from google.protobuf.internal.containers import ( - RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, - RepeatedScalarFieldContainer as google___protobuf___internal___containers___RepeatedScalarFieldContainer, -) - -from google.protobuf.internal.enum_type_wrapper import ( - _EnumTypeWrapper as google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper, -) - -from google.protobuf.message import ( - Message as google___protobuf___message___Message, -) - -from opentelemetry.proto.common.v1.common_pb2 import ( - InstrumentationLibrary as opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary, - StringKeyValue as opentelemetry___proto___common___v1___common_pb2___StringKeyValue, -) - -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as opentelemetry___proto___resource___v1___resource_pb2___Resource, -) - -from typing import ( - Iterable as typing___Iterable, - NewType as typing___NewType, - Optional as typing___Optional, - Text as typing___Text, - cast as typing___cast, -) - -from typing_extensions import ( - Literal as typing_extensions___Literal, -) - - -builtin___bool = bool -builtin___bytes = bytes -builtin___float = float -builtin___int = int - - -DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... - -AggregationTemporalityValue = typing___NewType('AggregationTemporalityValue', builtin___int) -type___AggregationTemporalityValue = AggregationTemporalityValue -AggregationTemporality: _AggregationTemporality -class _AggregationTemporality(google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[AggregationTemporalityValue]): - DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... - AGGREGATION_TEMPORALITY_UNSPECIFIED = typing___cast(AggregationTemporalityValue, 0) - AGGREGATION_TEMPORALITY_DELTA = typing___cast(AggregationTemporalityValue, 1) - AGGREGATION_TEMPORALITY_CUMULATIVE = typing___cast(AggregationTemporalityValue, 2) -AGGREGATION_TEMPORALITY_UNSPECIFIED = typing___cast(AggregationTemporalityValue, 0) -AGGREGATION_TEMPORALITY_DELTA = typing___cast(AggregationTemporalityValue, 1) -AGGREGATION_TEMPORALITY_CUMULATIVE = typing___cast(AggregationTemporalityValue, 2) -type___AggregationTemporality = AggregationTemporality - -class ResourceMetrics(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import opentelemetry.proto.common.v1.common_pb2 +import opentelemetry.proto.resource.v1.resource_pb2 +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... + +global___AggregationTemporality = AggregationTemporality +class _AggregationTemporality(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[AggregationTemporality.V], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... + AGGREGATION_TEMPORALITY_UNSPECIFIED = AggregationTemporality.V(0) + AGGREGATION_TEMPORALITY_DELTA = AggregationTemporality.V(1) + AGGREGATION_TEMPORALITY_CUMULATIVE = AggregationTemporality.V(2) +class AggregationTemporality(metaclass=_AggregationTemporality): + V = typing.NewType('V', builtins.int) +AGGREGATION_TEMPORALITY_UNSPECIFIED = AggregationTemporality.V(0) +AGGREGATION_TEMPORALITY_DELTA = AggregationTemporality.V(1) +AGGREGATION_TEMPORALITY_CUMULATIVE = AggregationTemporality.V(2) + +class ResourceMetrics(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + RESOURCE_FIELD_NUMBER: builtins.int + INSTRUMENTATION_LIBRARY_METRICS_FIELD_NUMBER: builtins.int @property - def resource(self) -> opentelemetry___proto___resource___v1___resource_pb2___Resource: ... + def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: ... @property - def instrumentation_library_metrics(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___InstrumentationLibraryMetrics]: ... + def instrumentation_library_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibraryMetrics]: ... def __init__(self, *, - resource : typing___Optional[opentelemetry___proto___resource___v1___resource_pb2___Resource] = None, - instrumentation_library_metrics : typing___Optional[typing___Iterable[type___InstrumentationLibraryMetrics]] = None, + resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., + instrumentation_library_metrics : typing.Optional[typing.Iterable[global___InstrumentationLibraryMetrics]] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions___Literal[u"resource",b"resource"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"instrumentation_library_metrics",b"instrumentation_library_metrics",u"resource",b"resource"]) -> None: ... -type___ResourceMetrics = ResourceMetrics + def HasField(self, field_name: typing_extensions.Literal[u"resource",b"resource"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library_metrics",b"instrumentation_library_metrics",u"resource",b"resource"]) -> None: ... +global___ResourceMetrics = ResourceMetrics -class InstrumentationLibraryMetrics(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +class InstrumentationLibraryMetrics(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int + METRICS_FIELD_NUMBER: builtins.int @property - def instrumentation_library(self) -> opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary: ... + def instrumentation_library(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: ... @property - def metrics(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Metric]: ... + def metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Metric]: ... def __init__(self, *, - instrumentation_library : typing___Optional[opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary] = None, - metrics : typing___Optional[typing___Iterable[type___Metric]] = None, + instrumentation_library : typing.Optional[opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary] = ..., + metrics : typing.Optional[typing.Iterable[global___Metric]] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions___Literal[u"instrumentation_library",b"instrumentation_library"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"instrumentation_library",b"instrumentation_library",u"metrics",b"metrics"]) -> None: ... -type___InstrumentationLibraryMetrics = InstrumentationLibraryMetrics + def HasField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library",u"metrics",b"metrics"]) -> None: ... +global___InstrumentationLibraryMetrics = InstrumentationLibraryMetrics + +class Metric(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + NAME_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + UNIT_FIELD_NUMBER: builtins.int + INT_GAUGE_FIELD_NUMBER: builtins.int + DOUBLE_GAUGE_FIELD_NUMBER: builtins.int + INT_SUM_FIELD_NUMBER: builtins.int + DOUBLE_SUM_FIELD_NUMBER: builtins.int + INT_HISTOGRAM_FIELD_NUMBER: builtins.int + DOUBLE_HISTOGRAM_FIELD_NUMBER: builtins.int + DOUBLE_SUMMARY_FIELD_NUMBER: builtins.int + name: typing.Text = ... + description: typing.Text = ... + unit: typing.Text = ... -class Metric(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - name: typing___Text = ... - description: typing___Text = ... - unit: typing___Text = ... + @property + def int_gauge(self) -> global___IntGauge: ... @property - def int_gauge(self) -> type___IntGauge: ... + def double_gauge(self) -> global___DoubleGauge: ... @property - def double_gauge(self) -> type___DoubleGauge: ... + def int_sum(self) -> global___IntSum: ... @property - def int_sum(self) -> type___IntSum: ... + def double_sum(self) -> global___DoubleSum: ... @property - def double_sum(self) -> type___DoubleSum: ... + def int_histogram(self) -> global___IntHistogram: ... @property - def int_histogram(self) -> type___IntHistogram: ... + def double_histogram(self) -> global___DoubleHistogram: ... @property - def double_histogram(self) -> type___DoubleHistogram: ... + def double_summary(self) -> global___DoubleSummary: ... def __init__(self, *, - name : typing___Optional[typing___Text] = None, - description : typing___Optional[typing___Text] = None, - unit : typing___Optional[typing___Text] = None, - int_gauge : typing___Optional[type___IntGauge] = None, - double_gauge : typing___Optional[type___DoubleGauge] = None, - int_sum : typing___Optional[type___IntSum] = None, - double_sum : typing___Optional[type___DoubleSum] = None, - int_histogram : typing___Optional[type___IntHistogram] = None, - double_histogram : typing___Optional[type___DoubleHistogram] = None, + name : typing.Text = ..., + description : typing.Text = ..., + unit : typing.Text = ..., + int_gauge : typing.Optional[global___IntGauge] = ..., + double_gauge : typing.Optional[global___DoubleGauge] = ..., + int_sum : typing.Optional[global___IntSum] = ..., + double_sum : typing.Optional[global___DoubleSum] = ..., + int_histogram : typing.Optional[global___IntHistogram] = ..., + double_histogram : typing.Optional[global___DoubleHistogram] = ..., + double_summary : typing.Optional[global___DoubleSummary] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions___Literal[u"data",b"data",u"double_gauge",b"double_gauge",u"double_histogram",b"double_histogram",u"double_sum",b"double_sum",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"data",b"data",u"description",b"description",u"double_gauge",b"double_gauge",u"double_histogram",b"double_histogram",u"double_sum",b"double_sum",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum",u"name",b"name",u"unit",b"unit"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions___Literal[u"data",b"data"]) -> typing_extensions___Literal["int_gauge","double_gauge","int_sum","double_sum","int_histogram","double_histogram"]: ... -type___Metric = Metric + def HasField(self, field_name: typing_extensions.Literal[u"data",b"data",u"double_gauge",b"double_gauge",u"double_histogram",b"double_histogram",u"double_sum",b"double_sum",u"double_summary",b"double_summary",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"data",b"data",u"description",b"description",u"double_gauge",b"double_gauge",u"double_histogram",b"double_histogram",u"double_sum",b"double_sum",u"double_summary",b"double_summary",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum",u"name",b"name",u"unit",b"unit"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal[u"data",b"data"]) -> typing_extensions.Literal["int_gauge","double_gauge","int_sum","double_sum","int_histogram","double_histogram","double_summary"]: ... +global___Metric = Metric -class IntGauge(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +class IntGauge(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + DATA_POINTS_FIELD_NUMBER: builtins.int @property - def data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___IntDataPoint]: ... + def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntDataPoint]: ... def __init__(self, *, - data_points : typing___Optional[typing___Iterable[type___IntDataPoint]] = None, + data_points : typing.Optional[typing.Iterable[global___IntDataPoint]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"data_points",b"data_points"]) -> None: ... -type___IntGauge = IntGauge + def ClearField(self, field_name: typing_extensions.Literal[u"data_points",b"data_points"]) -> None: ... +global___IntGauge = IntGauge -class DoubleGauge(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +class DoubleGauge(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + DATA_POINTS_FIELD_NUMBER: builtins.int @property - def data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___DoubleDataPoint]: ... + def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleDataPoint]: ... def __init__(self, *, - data_points : typing___Optional[typing___Iterable[type___DoubleDataPoint]] = None, + data_points : typing.Optional[typing.Iterable[global___DoubleDataPoint]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"data_points",b"data_points"]) -> None: ... -type___DoubleGauge = DoubleGauge + def ClearField(self, field_name: typing_extensions.Literal[u"data_points",b"data_points"]) -> None: ... +global___DoubleGauge = DoubleGauge -class IntSum(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - aggregation_temporality: type___AggregationTemporalityValue = ... - is_monotonic: builtin___bool = ... +class IntSum(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + DATA_POINTS_FIELD_NUMBER: builtins.int + AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int + IS_MONOTONIC_FIELD_NUMBER: builtins.int + aggregation_temporality: global___AggregationTemporality.V = ... + is_monotonic: builtins.bool = ... @property - def data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___IntDataPoint]: ... + def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntDataPoint]: ... def __init__(self, *, - data_points : typing___Optional[typing___Iterable[type___IntDataPoint]] = None, - aggregation_temporality : typing___Optional[type___AggregationTemporalityValue] = None, - is_monotonic : typing___Optional[builtin___bool] = None, + data_points : typing.Optional[typing.Iterable[global___IntDataPoint]] = ..., + aggregation_temporality : global___AggregationTemporality.V = ..., + is_monotonic : builtins.bool = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points",u"is_monotonic",b"is_monotonic"]) -> None: ... -type___IntSum = IntSum + def ClearField(self, field_name: typing_extensions.Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points",u"is_monotonic",b"is_monotonic"]) -> None: ... +global___IntSum = IntSum -class DoubleSum(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - aggregation_temporality: type___AggregationTemporalityValue = ... - is_monotonic: builtin___bool = ... +class DoubleSum(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + DATA_POINTS_FIELD_NUMBER: builtins.int + AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int + IS_MONOTONIC_FIELD_NUMBER: builtins.int + aggregation_temporality: global___AggregationTemporality.V = ... + is_monotonic: builtins.bool = ... @property - def data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___DoubleDataPoint]: ... + def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleDataPoint]: ... def __init__(self, *, - data_points : typing___Optional[typing___Iterable[type___DoubleDataPoint]] = None, - aggregation_temporality : typing___Optional[type___AggregationTemporalityValue] = None, - is_monotonic : typing___Optional[builtin___bool] = None, + data_points : typing.Optional[typing.Iterable[global___DoubleDataPoint]] = ..., + aggregation_temporality : global___AggregationTemporality.V = ..., + is_monotonic : builtins.bool = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points",u"is_monotonic",b"is_monotonic"]) -> None: ... -type___DoubleSum = DoubleSum + def ClearField(self, field_name: typing_extensions.Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points",u"is_monotonic",b"is_monotonic"]) -> None: ... +global___DoubleSum = DoubleSum -class IntHistogram(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - aggregation_temporality: type___AggregationTemporalityValue = ... +class IntHistogram(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + DATA_POINTS_FIELD_NUMBER: builtins.int + AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int + aggregation_temporality: global___AggregationTemporality.V = ... @property - def data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___IntHistogramDataPoint]: ... + def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntHistogramDataPoint]: ... def __init__(self, *, - data_points : typing___Optional[typing___Iterable[type___IntHistogramDataPoint]] = None, - aggregation_temporality : typing___Optional[type___AggregationTemporalityValue] = None, + data_points : typing.Optional[typing.Iterable[global___IntHistogramDataPoint]] = ..., + aggregation_temporality : global___AggregationTemporality.V = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points"]) -> None: ... -type___IntHistogram = IntHistogram + def ClearField(self, field_name: typing_extensions.Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points"]) -> None: ... +global___IntHistogram = IntHistogram -class DoubleHistogram(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - aggregation_temporality: type___AggregationTemporalityValue = ... +class DoubleHistogram(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + DATA_POINTS_FIELD_NUMBER: builtins.int + AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int + aggregation_temporality: global___AggregationTemporality.V = ... @property - def data_points(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___DoubleHistogramDataPoint]: ... + def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleHistogramDataPoint]: ... def __init__(self, *, - data_points : typing___Optional[typing___Iterable[type___DoubleHistogramDataPoint]] = None, - aggregation_temporality : typing___Optional[type___AggregationTemporalityValue] = None, + data_points : typing.Optional[typing.Iterable[global___DoubleHistogramDataPoint]] = ..., + aggregation_temporality : global___AggregationTemporality.V = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points"]) -> None: ... -type___DoubleHistogram = DoubleHistogram + def ClearField(self, field_name: typing_extensions.Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points"]) -> None: ... +global___DoubleHistogram = DoubleHistogram + +class DoubleSummary(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + DATA_POINTS_FIELD_NUMBER: builtins.int + + @property + def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleSummaryDataPoint]: ... -class IntDataPoint(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - start_time_unix_nano: builtin___int = ... - time_unix_nano: builtin___int = ... - value: builtin___int = ... + def __init__(self, + *, + data_points : typing.Optional[typing.Iterable[global___DoubleSummaryDataPoint]] = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal[u"data_points",b"data_points"]) -> None: ... +global___DoubleSummary = DoubleSummary + +class IntDataPoint(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + LABELS_FIELD_NUMBER: builtins.int + START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + EXEMPLARS_FIELD_NUMBER: builtins.int + start_time_unix_nano: builtins.int = ... + time_unix_nano: builtins.int = ... + value: builtins.int = ... @property - def labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... @property - def exemplars(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___IntExemplar]: ... + def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntExemplar]: ... def __init__(self, *, - labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, - start_time_unix_nano : typing___Optional[builtin___int] = None, - time_unix_nano : typing___Optional[builtin___int] = None, - value : typing___Optional[builtin___int] = None, - exemplars : typing___Optional[typing___Iterable[type___IntExemplar]] = None, + labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., + start_time_unix_nano : builtins.int = ..., + time_unix_nano : builtins.int = ..., + value : builtins.int = ..., + exemplars : typing.Optional[typing.Iterable[global___IntExemplar]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"exemplars",b"exemplars",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... -type___IntDataPoint = IntDataPoint - -class DoubleDataPoint(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - start_time_unix_nano: builtin___int = ... - time_unix_nano: builtin___int = ... - value: builtin___float = ... + def ClearField(self, field_name: typing_extensions.Literal[u"exemplars",b"exemplars",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... +global___IntDataPoint = IntDataPoint + +class DoubleDataPoint(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + LABELS_FIELD_NUMBER: builtins.int + START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + EXEMPLARS_FIELD_NUMBER: builtins.int + start_time_unix_nano: builtins.int = ... + time_unix_nano: builtins.int = ... + value: builtins.float = ... @property - def labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... @property - def exemplars(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___DoubleExemplar]: ... + def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleExemplar]: ... def __init__(self, *, - labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, - start_time_unix_nano : typing___Optional[builtin___int] = None, - time_unix_nano : typing___Optional[builtin___int] = None, - value : typing___Optional[builtin___float] = None, - exemplars : typing___Optional[typing___Iterable[type___DoubleExemplar]] = None, + labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., + start_time_unix_nano : builtins.int = ..., + time_unix_nano : builtins.int = ..., + value : builtins.float = ..., + exemplars : typing.Optional[typing.Iterable[global___DoubleExemplar]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"exemplars",b"exemplars",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... -type___DoubleDataPoint = DoubleDataPoint - -class IntHistogramDataPoint(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - start_time_unix_nano: builtin___int = ... - time_unix_nano: builtin___int = ... - count: builtin___int = ... - sum: builtin___int = ... - bucket_counts: google___protobuf___internal___containers___RepeatedScalarFieldContainer[builtin___int] = ... - explicit_bounds: google___protobuf___internal___containers___RepeatedScalarFieldContainer[builtin___float] = ... + def ClearField(self, field_name: typing_extensions.Literal[u"exemplars",b"exemplars",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... +global___DoubleDataPoint = DoubleDataPoint + +class IntHistogramDataPoint(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + LABELS_FIELD_NUMBER: builtins.int + START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + COUNT_FIELD_NUMBER: builtins.int + SUM_FIELD_NUMBER: builtins.int + BUCKET_COUNTS_FIELD_NUMBER: builtins.int + EXPLICIT_BOUNDS_FIELD_NUMBER: builtins.int + EXEMPLARS_FIELD_NUMBER: builtins.int + start_time_unix_nano: builtins.int = ... + time_unix_nano: builtins.int = ... + count: builtins.int = ... + sum: builtins.int = ... + bucket_counts: google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int] = ... + explicit_bounds: google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float] = ... @property - def labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... @property - def exemplars(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___IntExemplar]: ... + def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntExemplar]: ... def __init__(self, *, - labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, - start_time_unix_nano : typing___Optional[builtin___int] = None, - time_unix_nano : typing___Optional[builtin___int] = None, - count : typing___Optional[builtin___int] = None, - sum : typing___Optional[builtin___int] = None, - bucket_counts : typing___Optional[typing___Iterable[builtin___int]] = None, - explicit_bounds : typing___Optional[typing___Iterable[builtin___float]] = None, - exemplars : typing___Optional[typing___Iterable[type___IntExemplar]] = None, + labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., + start_time_unix_nano : builtins.int = ..., + time_unix_nano : builtins.int = ..., + count : builtins.int = ..., + sum : builtins.int = ..., + bucket_counts : typing.Optional[typing.Iterable[builtins.int]] = ..., + explicit_bounds : typing.Optional[typing.Iterable[builtins.float]] = ..., + exemplars : typing.Optional[typing.Iterable[global___IntExemplar]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"bucket_counts",b"bucket_counts",u"count",b"count",u"exemplars",b"exemplars",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... -type___IntHistogramDataPoint = IntHistogramDataPoint - -class DoubleHistogramDataPoint(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - start_time_unix_nano: builtin___int = ... - time_unix_nano: builtin___int = ... - count: builtin___int = ... - sum: builtin___float = ... - bucket_counts: google___protobuf___internal___containers___RepeatedScalarFieldContainer[builtin___int] = ... - explicit_bounds: google___protobuf___internal___containers___RepeatedScalarFieldContainer[builtin___float] = ... + def ClearField(self, field_name: typing_extensions.Literal[u"bucket_counts",b"bucket_counts",u"count",b"count",u"exemplars",b"exemplars",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... +global___IntHistogramDataPoint = IntHistogramDataPoint + +class DoubleHistogramDataPoint(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + LABELS_FIELD_NUMBER: builtins.int + START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + COUNT_FIELD_NUMBER: builtins.int + SUM_FIELD_NUMBER: builtins.int + BUCKET_COUNTS_FIELD_NUMBER: builtins.int + EXPLICIT_BOUNDS_FIELD_NUMBER: builtins.int + EXEMPLARS_FIELD_NUMBER: builtins.int + start_time_unix_nano: builtins.int = ... + time_unix_nano: builtins.int = ... + count: builtins.int = ... + sum: builtins.float = ... + bucket_counts: google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int] = ... + explicit_bounds: google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float] = ... @property - def labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... @property - def exemplars(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___DoubleExemplar]: ... + def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleExemplar]: ... def __init__(self, *, - labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, - start_time_unix_nano : typing___Optional[builtin___int] = None, - time_unix_nano : typing___Optional[builtin___int] = None, - count : typing___Optional[builtin___int] = None, - sum : typing___Optional[builtin___float] = None, - bucket_counts : typing___Optional[typing___Iterable[builtin___int]] = None, - explicit_bounds : typing___Optional[typing___Iterable[builtin___float]] = None, - exemplars : typing___Optional[typing___Iterable[type___DoubleExemplar]] = None, + labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., + start_time_unix_nano : builtins.int = ..., + time_unix_nano : builtins.int = ..., + count : builtins.int = ..., + sum : builtins.float = ..., + bucket_counts : typing.Optional[typing.Iterable[builtins.int]] = ..., + explicit_bounds : typing.Optional[typing.Iterable[builtins.float]] = ..., + exemplars : typing.Optional[typing.Iterable[global___DoubleExemplar]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"bucket_counts",b"bucket_counts",u"count",b"count",u"exemplars",b"exemplars",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... -type___DoubleHistogramDataPoint = DoubleHistogramDataPoint + def ClearField(self, field_name: typing_extensions.Literal[u"bucket_counts",b"bucket_counts",u"count",b"count",u"exemplars",b"exemplars",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... +global___DoubleHistogramDataPoint = DoubleHistogramDataPoint + +class DoubleSummaryDataPoint(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + class ValueAtQuantile(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + QUANTILE_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + quantile: builtins.float = ... + value: builtins.float = ... + + def __init__(self, + *, + quantile : builtins.float = ..., + value : builtins.float = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal[u"quantile",b"quantile",u"value",b"value"]) -> None: ... + + LABELS_FIELD_NUMBER: builtins.int + START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + COUNT_FIELD_NUMBER: builtins.int + SUM_FIELD_NUMBER: builtins.int + QUANTILE_VALUES_FIELD_NUMBER: builtins.int + start_time_unix_nano: builtins.int = ... + time_unix_nano: builtins.int = ... + count: builtins.int = ... + sum: builtins.float = ... -class IntExemplar(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - time_unix_nano: builtin___int = ... - value: builtin___int = ... - span_id: builtin___bytes = ... - trace_id: builtin___bytes = ... + @property + def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... @property - def filtered_labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + def quantile_values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleSummaryDataPoint.ValueAtQuantile]: ... def __init__(self, *, - filtered_labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, - time_unix_nano : typing___Optional[builtin___int] = None, - value : typing___Optional[builtin___int] = None, - span_id : typing___Optional[builtin___bytes] = None, - trace_id : typing___Optional[builtin___bytes] = None, + labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., + start_time_unix_nano : builtins.int = ..., + time_unix_nano : builtins.int = ..., + count : builtins.int = ..., + sum : builtins.float = ..., + quantile_values : typing.Optional[typing.Iterable[global___DoubleSummaryDataPoint.ValueAtQuantile]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"filtered_labels",b"filtered_labels",u"span_id",b"span_id",u"time_unix_nano",b"time_unix_nano",u"trace_id",b"trace_id",u"value",b"value"]) -> None: ... -type___IntExemplar = IntExemplar + def ClearField(self, field_name: typing_extensions.Literal[u"count",b"count",u"labels",b"labels",u"quantile_values",b"quantile_values",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... +global___DoubleSummaryDataPoint = DoubleSummaryDataPoint + +class IntExemplar(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + FILTERED_LABELS_FIELD_NUMBER: builtins.int + TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + SPAN_ID_FIELD_NUMBER: builtins.int + TRACE_ID_FIELD_NUMBER: builtins.int + time_unix_nano: builtins.int = ... + value: builtins.int = ... + span_id: builtins.bytes = ... + trace_id: builtins.bytes = ... + + @property + def filtered_labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... -class DoubleExemplar(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - time_unix_nano: builtin___int = ... - value: builtin___float = ... - span_id: builtin___bytes = ... - trace_id: builtin___bytes = ... + def __init__(self, + *, + filtered_labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., + time_unix_nano : builtins.int = ..., + value : builtins.int = ..., + span_id : builtins.bytes = ..., + trace_id : builtins.bytes = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal[u"filtered_labels",b"filtered_labels",u"span_id",b"span_id",u"time_unix_nano",b"time_unix_nano",u"trace_id",b"trace_id",u"value",b"value"]) -> None: ... +global___IntExemplar = IntExemplar + +class DoubleExemplar(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + FILTERED_LABELS_FIELD_NUMBER: builtins.int + TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + SPAN_ID_FIELD_NUMBER: builtins.int + TRACE_ID_FIELD_NUMBER: builtins.int + time_unix_nano: builtins.int = ... + value: builtins.float = ... + span_id: builtins.bytes = ... + trace_id: builtins.bytes = ... @property - def filtered_labels(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]: ... + def filtered_labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... def __init__(self, *, - filtered_labels : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___StringKeyValue]] = None, - time_unix_nano : typing___Optional[builtin___int] = None, - value : typing___Optional[builtin___float] = None, - span_id : typing___Optional[builtin___bytes] = None, - trace_id : typing___Optional[builtin___bytes] = None, + filtered_labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., + time_unix_nano : builtins.int = ..., + value : builtins.float = ..., + span_id : builtins.bytes = ..., + trace_id : builtins.bytes = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"filtered_labels",b"filtered_labels",u"span_id",b"span_id",u"time_unix_nano",b"time_unix_nano",u"trace_id",b"trace_id",u"value",b"value"]) -> None: ... -type___DoubleExemplar = DoubleExemplar + def ClearField(self, field_name: typing_extensions.Literal[u"filtered_labels",b"filtered_labels",u"span_id",b"span_id",u"time_unix_nano",b"time_unix_nano",u"trace_id",b"trace_id",u"value",b"value"]) -> None: ... +global___DoubleExemplar = DoubleExemplar diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi index 4f6cb1419e..c2c5881753 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi @@ -1,51 +1,30 @@ -# @generated by generate_proto_mypy_stubs.py. Do not edit! -import sys -from google.protobuf.descriptor import ( - Descriptor as google___protobuf___descriptor___Descriptor, - FileDescriptor as google___protobuf___descriptor___FileDescriptor, -) - -from google.protobuf.internal.containers import ( - RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, -) - -from google.protobuf.message import ( - Message as google___protobuf___message___Message, -) - -from opentelemetry.proto.common.v1.common_pb2 import ( - KeyValue as opentelemetry___proto___common___v1___common_pb2___KeyValue, -) - -from typing import ( - Iterable as typing___Iterable, - Optional as typing___Optional, -) - -from typing_extensions import ( - Literal as typing_extensions___Literal, -) - - -builtin___bool = bool -builtin___bytes = bytes -builtin___float = float -builtin___int = int - - -DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... - -class Resource(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - dropped_attributes_count: builtin___int = ... +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import opentelemetry.proto.common.v1.common_pb2 +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... + +class Resource(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + ATTRIBUTES_FIELD_NUMBER: builtins.int + DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int + dropped_attributes_count: builtins.int = ... @property - def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___KeyValue]: ... + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... def __init__(self, *, - attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___KeyValue]] = None, - dropped_attributes_count : typing___Optional[builtin___int] = None, + attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., + dropped_attributes_count : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count"]) -> None: ... -type___Resource = Resource + def ClearField(self, field_name: typing_extensions.Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count"]) -> None: ... +global___Resource = Resource diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi index 0b0fe87c62..e02bf3d5ba 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi @@ -1,113 +1,100 @@ -# @generated by generate_proto_mypy_stubs.py. Do not edit! -import sys -from google.protobuf.descriptor import ( - Descriptor as google___protobuf___descriptor___Descriptor, - EnumDescriptor as google___protobuf___descriptor___EnumDescriptor, - FileDescriptor as google___protobuf___descriptor___FileDescriptor, -) - -from google.protobuf.internal.enum_type_wrapper import ( - _EnumTypeWrapper as google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper, -) - -from google.protobuf.message import ( - Message as google___protobuf___message___Message, -) - -from typing import ( - NewType as typing___NewType, - Optional as typing___Optional, - cast as typing___cast, -) - -from typing_extensions import ( - Literal as typing_extensions___Literal, -) - - -builtin___bool = bool -builtin___bytes = bytes -builtin___float = float -builtin___int = int - - -DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... - -class TraceConfig(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - max_number_of_attributes: builtin___int = ... - max_number_of_timed_events: builtin___int = ... - max_number_of_attributes_per_timed_event: builtin___int = ... - max_number_of_links: builtin___int = ... - max_number_of_attributes_per_link: builtin___int = ... +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... + +class TraceConfig(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + CONSTANT_SAMPLER_FIELD_NUMBER: builtins.int + TRACE_ID_RATIO_BASED_FIELD_NUMBER: builtins.int + RATE_LIMITING_SAMPLER_FIELD_NUMBER: builtins.int + MAX_NUMBER_OF_ATTRIBUTES_FIELD_NUMBER: builtins.int + MAX_NUMBER_OF_TIMED_EVENTS_FIELD_NUMBER: builtins.int + MAX_NUMBER_OF_ATTRIBUTES_PER_TIMED_EVENT_FIELD_NUMBER: builtins.int + MAX_NUMBER_OF_LINKS_FIELD_NUMBER: builtins.int + MAX_NUMBER_OF_ATTRIBUTES_PER_LINK_FIELD_NUMBER: builtins.int + max_number_of_attributes: builtins.int = ... + max_number_of_timed_events: builtins.int = ... + max_number_of_attributes_per_timed_event: builtins.int = ... + max_number_of_links: builtins.int = ... + max_number_of_attributes_per_link: builtins.int = ... @property - def constant_sampler(self) -> type___ConstantSampler: ... + def constant_sampler(self) -> global___ConstantSampler: ... @property - def trace_id_ratio_based(self) -> type___TraceIdRatioBased: ... + def trace_id_ratio_based(self) -> global___TraceIdRatioBased: ... @property - def rate_limiting_sampler(self) -> type___RateLimitingSampler: ... + def rate_limiting_sampler(self) -> global___RateLimitingSampler: ... def __init__(self, *, - constant_sampler : typing___Optional[type___ConstantSampler] = None, - trace_id_ratio_based : typing___Optional[type___TraceIdRatioBased] = None, - rate_limiting_sampler : typing___Optional[type___RateLimitingSampler] = None, - max_number_of_attributes : typing___Optional[builtin___int] = None, - max_number_of_timed_events : typing___Optional[builtin___int] = None, - max_number_of_attributes_per_timed_event : typing___Optional[builtin___int] = None, - max_number_of_links : typing___Optional[builtin___int] = None, - max_number_of_attributes_per_link : typing___Optional[builtin___int] = None, + constant_sampler : typing.Optional[global___ConstantSampler] = ..., + trace_id_ratio_based : typing.Optional[global___TraceIdRatioBased] = ..., + rate_limiting_sampler : typing.Optional[global___RateLimitingSampler] = ..., + max_number_of_attributes : builtins.int = ..., + max_number_of_timed_events : builtins.int = ..., + max_number_of_attributes_per_timed_event : builtins.int = ..., + max_number_of_links : builtins.int = ..., + max_number_of_attributes_per_link : builtins.int = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions___Literal[u"constant_sampler",b"constant_sampler",u"rate_limiting_sampler",b"rate_limiting_sampler",u"sampler",b"sampler",u"trace_id_ratio_based",b"trace_id_ratio_based"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"constant_sampler",b"constant_sampler",u"max_number_of_attributes",b"max_number_of_attributes",u"max_number_of_attributes_per_link",b"max_number_of_attributes_per_link",u"max_number_of_attributes_per_timed_event",b"max_number_of_attributes_per_timed_event",u"max_number_of_links",b"max_number_of_links",u"max_number_of_timed_events",b"max_number_of_timed_events",u"rate_limiting_sampler",b"rate_limiting_sampler",u"sampler",b"sampler",u"trace_id_ratio_based",b"trace_id_ratio_based"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions___Literal[u"sampler",b"sampler"]) -> typing_extensions___Literal["constant_sampler","trace_id_ratio_based","rate_limiting_sampler"]: ... -type___TraceConfig = TraceConfig - -class ConstantSampler(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - ConstantDecisionValue = typing___NewType('ConstantDecisionValue', builtin___int) - type___ConstantDecisionValue = ConstantDecisionValue - ConstantDecision: _ConstantDecision - class _ConstantDecision(google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[ConstantSampler.ConstantDecisionValue]): - DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... - ALWAYS_OFF = typing___cast(ConstantSampler.ConstantDecisionValue, 0) - ALWAYS_ON = typing___cast(ConstantSampler.ConstantDecisionValue, 1) - ALWAYS_PARENT = typing___cast(ConstantSampler.ConstantDecisionValue, 2) - ALWAYS_OFF = typing___cast(ConstantSampler.ConstantDecisionValue, 0) - ALWAYS_ON = typing___cast(ConstantSampler.ConstantDecisionValue, 1) - ALWAYS_PARENT = typing___cast(ConstantSampler.ConstantDecisionValue, 2) - type___ConstantDecision = ConstantDecision - - decision: type___ConstantSampler.ConstantDecisionValue = ... + def HasField(self, field_name: typing_extensions.Literal[u"constant_sampler",b"constant_sampler",u"rate_limiting_sampler",b"rate_limiting_sampler",u"sampler",b"sampler",u"trace_id_ratio_based",b"trace_id_ratio_based"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"constant_sampler",b"constant_sampler",u"max_number_of_attributes",b"max_number_of_attributes",u"max_number_of_attributes_per_link",b"max_number_of_attributes_per_link",u"max_number_of_attributes_per_timed_event",b"max_number_of_attributes_per_timed_event",u"max_number_of_links",b"max_number_of_links",u"max_number_of_timed_events",b"max_number_of_timed_events",u"rate_limiting_sampler",b"rate_limiting_sampler",u"sampler",b"sampler",u"trace_id_ratio_based",b"trace_id_ratio_based"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal[u"sampler",b"sampler"]) -> typing_extensions.Literal["constant_sampler","trace_id_ratio_based","rate_limiting_sampler"]: ... +global___TraceConfig = TraceConfig + +class ConstantSampler(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + class _ConstantDecision(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ConstantDecision.V], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... + ALWAYS_OFF = ConstantSampler.ConstantDecision.V(0) + ALWAYS_ON = ConstantSampler.ConstantDecision.V(1) + ALWAYS_PARENT = ConstantSampler.ConstantDecision.V(2) + class ConstantDecision(metaclass=_ConstantDecision): + V = typing.NewType('V', builtins.int) + ALWAYS_OFF = ConstantSampler.ConstantDecision.V(0) + ALWAYS_ON = ConstantSampler.ConstantDecision.V(1) + ALWAYS_PARENT = ConstantSampler.ConstantDecision.V(2) + + DECISION_FIELD_NUMBER: builtins.int + decision: global___ConstantSampler.ConstantDecision.V = ... def __init__(self, *, - decision : typing___Optional[type___ConstantSampler.ConstantDecisionValue] = None, + decision : global___ConstantSampler.ConstantDecision.V = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"decision",b"decision"]) -> None: ... -type___ConstantSampler = ConstantSampler + def ClearField(self, field_name: typing_extensions.Literal[u"decision",b"decision"]) -> None: ... +global___ConstantSampler = ConstantSampler -class TraceIdRatioBased(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - samplingRatio: builtin___float = ... +class TraceIdRatioBased(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + SAMPLINGRATIO_FIELD_NUMBER: builtins.int + samplingRatio: builtins.float = ... def __init__(self, *, - samplingRatio : typing___Optional[builtin___float] = None, + samplingRatio : builtins.float = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"samplingRatio",b"samplingRatio"]) -> None: ... -type___TraceIdRatioBased = TraceIdRatioBased + def ClearField(self, field_name: typing_extensions.Literal[u"samplingRatio",b"samplingRatio"]) -> None: ... +global___TraceIdRatioBased = TraceIdRatioBased -class RateLimitingSampler(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - qps: builtin___int = ... +class RateLimitingSampler(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + QPS_FIELD_NUMBER: builtins.int + qps: builtins.int = ... def __init__(self, *, - qps : typing___Optional[builtin___int] = None, + qps : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"qps",b"qps"]) -> None: ... -type___RateLimitingSampler = RateLimitingSampler + def ClearField(self, field_name: typing_extensions.Literal[u"qps",b"qps"]) -> None: ... +global___RateLimitingSampler = RateLimitingSampler diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi index fc1cf87e40..71e323b99e 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi @@ -1,261 +1,250 @@ -# @generated by generate_proto_mypy_stubs.py. Do not edit! -import sys -from google.protobuf.descriptor import ( - Descriptor as google___protobuf___descriptor___Descriptor, - EnumDescriptor as google___protobuf___descriptor___EnumDescriptor, - FileDescriptor as google___protobuf___descriptor___FileDescriptor, -) - -from google.protobuf.internal.containers import ( - RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, -) - -from google.protobuf.internal.enum_type_wrapper import ( - _EnumTypeWrapper as google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper, -) - -from google.protobuf.message import ( - Message as google___protobuf___message___Message, -) - -from opentelemetry.proto.common.v1.common_pb2 import ( - InstrumentationLibrary as opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary, - KeyValue as opentelemetry___proto___common___v1___common_pb2___KeyValue, -) - -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as opentelemetry___proto___resource___v1___resource_pb2___Resource, -) - -from typing import ( - Iterable as typing___Iterable, - NewType as typing___NewType, - Optional as typing___Optional, - Text as typing___Text, - cast as typing___cast, -) - -from typing_extensions import ( - Literal as typing_extensions___Literal, -) - - -builtin___bool = bool -builtin___bytes = bytes -builtin___float = float -builtin___int = int - - -DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... - -class ResourceSpans(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import opentelemetry.proto.common.v1.common_pb2 +import opentelemetry.proto.resource.v1.resource_pb2 +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... + +class ResourceSpans(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + RESOURCE_FIELD_NUMBER: builtins.int + INSTRUMENTATION_LIBRARY_SPANS_FIELD_NUMBER: builtins.int @property - def resource(self) -> opentelemetry___proto___resource___v1___resource_pb2___Resource: ... + def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: ... @property - def instrumentation_library_spans(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___InstrumentationLibrarySpans]: ... + def instrumentation_library_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibrarySpans]: ... def __init__(self, *, - resource : typing___Optional[opentelemetry___proto___resource___v1___resource_pb2___Resource] = None, - instrumentation_library_spans : typing___Optional[typing___Iterable[type___InstrumentationLibrarySpans]] = None, + resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., + instrumentation_library_spans : typing.Optional[typing.Iterable[global___InstrumentationLibrarySpans]] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions___Literal[u"resource",b"resource"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"instrumentation_library_spans",b"instrumentation_library_spans",u"resource",b"resource"]) -> None: ... -type___ResourceSpans = ResourceSpans + def HasField(self, field_name: typing_extensions.Literal[u"resource",b"resource"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library_spans",b"instrumentation_library_spans",u"resource",b"resource"]) -> None: ... +global___ResourceSpans = ResourceSpans -class InstrumentationLibrarySpans(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... +class InstrumentationLibrarySpans(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int + SPANS_FIELD_NUMBER: builtins.int @property - def instrumentation_library(self) -> opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary: ... + def instrumentation_library(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: ... @property - def spans(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Span]: ... + def spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span]: ... def __init__(self, *, - instrumentation_library : typing___Optional[opentelemetry___proto___common___v1___common_pb2___InstrumentationLibrary] = None, - spans : typing___Optional[typing___Iterable[type___Span]] = None, + instrumentation_library : typing.Optional[opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary] = ..., + spans : typing.Optional[typing.Iterable[global___Span]] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions___Literal[u"instrumentation_library",b"instrumentation_library"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"instrumentation_library",b"instrumentation_library",u"spans",b"spans"]) -> None: ... -type___InstrumentationLibrarySpans = InstrumentationLibrarySpans - -class Span(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - SpanKindValue = typing___NewType('SpanKindValue', builtin___int) - type___SpanKindValue = SpanKindValue - SpanKind: _SpanKind - class _SpanKind(google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[Span.SpanKindValue]): - DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... - SPAN_KIND_UNSPECIFIED = typing___cast(Span.SpanKindValue, 0) - SPAN_KIND_INTERNAL = typing___cast(Span.SpanKindValue, 1) - SPAN_KIND_SERVER = typing___cast(Span.SpanKindValue, 2) - SPAN_KIND_CLIENT = typing___cast(Span.SpanKindValue, 3) - SPAN_KIND_PRODUCER = typing___cast(Span.SpanKindValue, 4) - SPAN_KIND_CONSUMER = typing___cast(Span.SpanKindValue, 5) - SPAN_KIND_UNSPECIFIED = typing___cast(Span.SpanKindValue, 0) - SPAN_KIND_INTERNAL = typing___cast(Span.SpanKindValue, 1) - SPAN_KIND_SERVER = typing___cast(Span.SpanKindValue, 2) - SPAN_KIND_CLIENT = typing___cast(Span.SpanKindValue, 3) - SPAN_KIND_PRODUCER = typing___cast(Span.SpanKindValue, 4) - SPAN_KIND_CONSUMER = typing___cast(Span.SpanKindValue, 5) - type___SpanKind = SpanKind - - class Event(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - time_unix_nano: builtin___int = ... - name: typing___Text = ... - dropped_attributes_count: builtin___int = ... + def HasField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library",u"spans",b"spans"]) -> None: ... +global___InstrumentationLibrarySpans = InstrumentationLibrarySpans + +class Span(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + class _SpanKind(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SpanKind.V], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... + SPAN_KIND_UNSPECIFIED = Span.SpanKind.V(0) + SPAN_KIND_INTERNAL = Span.SpanKind.V(1) + SPAN_KIND_SERVER = Span.SpanKind.V(2) + SPAN_KIND_CLIENT = Span.SpanKind.V(3) + SPAN_KIND_PRODUCER = Span.SpanKind.V(4) + SPAN_KIND_CONSUMER = Span.SpanKind.V(5) + class SpanKind(metaclass=_SpanKind): + V = typing.NewType('V', builtins.int) + SPAN_KIND_UNSPECIFIED = Span.SpanKind.V(0) + SPAN_KIND_INTERNAL = Span.SpanKind.V(1) + SPAN_KIND_SERVER = Span.SpanKind.V(2) + SPAN_KIND_CLIENT = Span.SpanKind.V(3) + SPAN_KIND_PRODUCER = Span.SpanKind.V(4) + SPAN_KIND_CONSUMER = Span.SpanKind.V(5) + + class Event(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + ATTRIBUTES_FIELD_NUMBER: builtins.int + DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int + time_unix_nano: builtins.int = ... + name: typing.Text = ... + dropped_attributes_count: builtins.int = ... @property - def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___KeyValue]: ... + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... def __init__(self, *, - time_unix_nano : typing___Optional[builtin___int] = None, - name : typing___Optional[typing___Text] = None, - attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___KeyValue]] = None, - dropped_attributes_count : typing___Optional[builtin___int] = None, + time_unix_nano : builtins.int = ..., + name : typing.Text = ..., + attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., + dropped_attributes_count : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"name",b"name",u"time_unix_nano",b"time_unix_nano"]) -> None: ... - type___Event = Event - - class Link(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - trace_id: builtin___bytes = ... - span_id: builtin___bytes = ... - trace_state: typing___Text = ... - dropped_attributes_count: builtin___int = ... + def ClearField(self, field_name: typing_extensions.Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"name",b"name",u"time_unix_nano",b"time_unix_nano"]) -> None: ... + + class Link(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + TRACE_ID_FIELD_NUMBER: builtins.int + SPAN_ID_FIELD_NUMBER: builtins.int + TRACE_STATE_FIELD_NUMBER: builtins.int + ATTRIBUTES_FIELD_NUMBER: builtins.int + DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int + trace_id: builtins.bytes = ... + span_id: builtins.bytes = ... + trace_state: typing.Text = ... + dropped_attributes_count: builtins.int = ... @property - def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___KeyValue]: ... + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... def __init__(self, *, - trace_id : typing___Optional[builtin___bytes] = None, - span_id : typing___Optional[builtin___bytes] = None, - trace_state : typing___Optional[typing___Text] = None, - attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___KeyValue]] = None, - dropped_attributes_count : typing___Optional[builtin___int] = None, + trace_id : builtins.bytes = ..., + span_id : builtins.bytes = ..., + trace_state : typing.Text = ..., + attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., + dropped_attributes_count : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"span_id",b"span_id",u"trace_id",b"trace_id",u"trace_state",b"trace_state"]) -> None: ... - type___Link = Link - - trace_id: builtin___bytes = ... - span_id: builtin___bytes = ... - trace_state: typing___Text = ... - parent_span_id: builtin___bytes = ... - name: typing___Text = ... - kind: type___Span.SpanKindValue = ... - start_time_unix_nano: builtin___int = ... - end_time_unix_nano: builtin___int = ... - dropped_attributes_count: builtin___int = ... - dropped_events_count: builtin___int = ... - dropped_links_count: builtin___int = ... + def ClearField(self, field_name: typing_extensions.Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"span_id",b"span_id",u"trace_id",b"trace_id",u"trace_state",b"trace_state"]) -> None: ... + + TRACE_ID_FIELD_NUMBER: builtins.int + SPAN_ID_FIELD_NUMBER: builtins.int + TRACE_STATE_FIELD_NUMBER: builtins.int + PARENT_SPAN_ID_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + KIND_FIELD_NUMBER: builtins.int + START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + END_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + ATTRIBUTES_FIELD_NUMBER: builtins.int + DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int + EVENTS_FIELD_NUMBER: builtins.int + DROPPED_EVENTS_COUNT_FIELD_NUMBER: builtins.int + LINKS_FIELD_NUMBER: builtins.int + DROPPED_LINKS_COUNT_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + trace_id: builtins.bytes = ... + span_id: builtins.bytes = ... + trace_state: typing.Text = ... + parent_span_id: builtins.bytes = ... + name: typing.Text = ... + kind: global___Span.SpanKind.V = ... + start_time_unix_nano: builtins.int = ... + end_time_unix_nano: builtins.int = ... + dropped_attributes_count: builtins.int = ... + dropped_events_count: builtins.int = ... + dropped_links_count: builtins.int = ... @property - def attributes(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[opentelemetry___proto___common___v1___common_pb2___KeyValue]: ... + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... @property - def events(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Span.Event]: ... + def events(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span.Event]: ... @property - def links(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Span.Link]: ... + def links(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span.Link]: ... @property - def status(self) -> type___Status: ... + def status(self) -> global___Status: ... def __init__(self, *, - trace_id : typing___Optional[builtin___bytes] = None, - span_id : typing___Optional[builtin___bytes] = None, - trace_state : typing___Optional[typing___Text] = None, - parent_span_id : typing___Optional[builtin___bytes] = None, - name : typing___Optional[typing___Text] = None, - kind : typing___Optional[type___Span.SpanKindValue] = None, - start_time_unix_nano : typing___Optional[builtin___int] = None, - end_time_unix_nano : typing___Optional[builtin___int] = None, - attributes : typing___Optional[typing___Iterable[opentelemetry___proto___common___v1___common_pb2___KeyValue]] = None, - dropped_attributes_count : typing___Optional[builtin___int] = None, - events : typing___Optional[typing___Iterable[type___Span.Event]] = None, - dropped_events_count : typing___Optional[builtin___int] = None, - links : typing___Optional[typing___Iterable[type___Span.Link]] = None, - dropped_links_count : typing___Optional[builtin___int] = None, - status : typing___Optional[type___Status] = None, + trace_id : builtins.bytes = ..., + span_id : builtins.bytes = ..., + trace_state : typing.Text = ..., + parent_span_id : builtins.bytes = ..., + name : typing.Text = ..., + kind : global___Span.SpanKind.V = ..., + start_time_unix_nano : builtins.int = ..., + end_time_unix_nano : builtins.int = ..., + attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., + dropped_attributes_count : builtins.int = ..., + events : typing.Optional[typing.Iterable[global___Span.Event]] = ..., + dropped_events_count : builtins.int = ..., + links : typing.Optional[typing.Iterable[global___Span.Link]] = ..., + dropped_links_count : builtins.int = ..., + status : typing.Optional[global___Status] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions___Literal[u"status",b"status"]) -> builtin___bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"dropped_events_count",b"dropped_events_count",u"dropped_links_count",b"dropped_links_count",u"end_time_unix_nano",b"end_time_unix_nano",u"events",b"events",u"kind",b"kind",u"links",b"links",u"name",b"name",u"parent_span_id",b"parent_span_id",u"span_id",b"span_id",u"start_time_unix_nano",b"start_time_unix_nano",u"status",b"status",u"trace_id",b"trace_id",u"trace_state",b"trace_state"]) -> None: ... -type___Span = Span - -class Status(google___protobuf___message___Message): - DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... - DeprecatedStatusCodeValue = typing___NewType('DeprecatedStatusCodeValue', builtin___int) - type___DeprecatedStatusCodeValue = DeprecatedStatusCodeValue - DeprecatedStatusCode: _DeprecatedStatusCode - class _DeprecatedStatusCode(google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[Status.DeprecatedStatusCodeValue]): - DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... - DEPRECATED_STATUS_CODE_OK = typing___cast(Status.DeprecatedStatusCodeValue, 0) - DEPRECATED_STATUS_CODE_CANCELLED = typing___cast(Status.DeprecatedStatusCodeValue, 1) - DEPRECATED_STATUS_CODE_UNKNOWN_ERROR = typing___cast(Status.DeprecatedStatusCodeValue, 2) - DEPRECATED_STATUS_CODE_INVALID_ARGUMENT = typing___cast(Status.DeprecatedStatusCodeValue, 3) - DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED = typing___cast(Status.DeprecatedStatusCodeValue, 4) - DEPRECATED_STATUS_CODE_NOT_FOUND = typing___cast(Status.DeprecatedStatusCodeValue, 5) - DEPRECATED_STATUS_CODE_ALREADY_EXISTS = typing___cast(Status.DeprecatedStatusCodeValue, 6) - DEPRECATED_STATUS_CODE_PERMISSION_DENIED = typing___cast(Status.DeprecatedStatusCodeValue, 7) - DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED = typing___cast(Status.DeprecatedStatusCodeValue, 8) - DEPRECATED_STATUS_CODE_FAILED_PRECONDITION = typing___cast(Status.DeprecatedStatusCodeValue, 9) - DEPRECATED_STATUS_CODE_ABORTED = typing___cast(Status.DeprecatedStatusCodeValue, 10) - DEPRECATED_STATUS_CODE_OUT_OF_RANGE = typing___cast(Status.DeprecatedStatusCodeValue, 11) - DEPRECATED_STATUS_CODE_UNIMPLEMENTED = typing___cast(Status.DeprecatedStatusCodeValue, 12) - DEPRECATED_STATUS_CODE_INTERNAL_ERROR = typing___cast(Status.DeprecatedStatusCodeValue, 13) - DEPRECATED_STATUS_CODE_UNAVAILABLE = typing___cast(Status.DeprecatedStatusCodeValue, 14) - DEPRECATED_STATUS_CODE_DATA_LOSS = typing___cast(Status.DeprecatedStatusCodeValue, 15) - DEPRECATED_STATUS_CODE_UNAUTHENTICATED = typing___cast(Status.DeprecatedStatusCodeValue, 16) - DEPRECATED_STATUS_CODE_OK = typing___cast(Status.DeprecatedStatusCodeValue, 0) - DEPRECATED_STATUS_CODE_CANCELLED = typing___cast(Status.DeprecatedStatusCodeValue, 1) - DEPRECATED_STATUS_CODE_UNKNOWN_ERROR = typing___cast(Status.DeprecatedStatusCodeValue, 2) - DEPRECATED_STATUS_CODE_INVALID_ARGUMENT = typing___cast(Status.DeprecatedStatusCodeValue, 3) - DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED = typing___cast(Status.DeprecatedStatusCodeValue, 4) - DEPRECATED_STATUS_CODE_NOT_FOUND = typing___cast(Status.DeprecatedStatusCodeValue, 5) - DEPRECATED_STATUS_CODE_ALREADY_EXISTS = typing___cast(Status.DeprecatedStatusCodeValue, 6) - DEPRECATED_STATUS_CODE_PERMISSION_DENIED = typing___cast(Status.DeprecatedStatusCodeValue, 7) - DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED = typing___cast(Status.DeprecatedStatusCodeValue, 8) - DEPRECATED_STATUS_CODE_FAILED_PRECONDITION = typing___cast(Status.DeprecatedStatusCodeValue, 9) - DEPRECATED_STATUS_CODE_ABORTED = typing___cast(Status.DeprecatedStatusCodeValue, 10) - DEPRECATED_STATUS_CODE_OUT_OF_RANGE = typing___cast(Status.DeprecatedStatusCodeValue, 11) - DEPRECATED_STATUS_CODE_UNIMPLEMENTED = typing___cast(Status.DeprecatedStatusCodeValue, 12) - DEPRECATED_STATUS_CODE_INTERNAL_ERROR = typing___cast(Status.DeprecatedStatusCodeValue, 13) - DEPRECATED_STATUS_CODE_UNAVAILABLE = typing___cast(Status.DeprecatedStatusCodeValue, 14) - DEPRECATED_STATUS_CODE_DATA_LOSS = typing___cast(Status.DeprecatedStatusCodeValue, 15) - DEPRECATED_STATUS_CODE_UNAUTHENTICATED = typing___cast(Status.DeprecatedStatusCodeValue, 16) - type___DeprecatedStatusCode = DeprecatedStatusCode - - StatusCodeValue = typing___NewType('StatusCodeValue', builtin___int) - type___StatusCodeValue = StatusCodeValue - StatusCode: _StatusCode - class _StatusCode(google___protobuf___internal___enum_type_wrapper____EnumTypeWrapper[Status.StatusCodeValue]): - DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... - STATUS_CODE_UNSET = typing___cast(Status.StatusCodeValue, 0) - STATUS_CODE_OK = typing___cast(Status.StatusCodeValue, 1) - STATUS_CODE_ERROR = typing___cast(Status.StatusCodeValue, 2) - STATUS_CODE_UNSET = typing___cast(Status.StatusCodeValue, 0) - STATUS_CODE_OK = typing___cast(Status.StatusCodeValue, 1) - STATUS_CODE_ERROR = typing___cast(Status.StatusCodeValue, 2) - type___StatusCode = StatusCode - - deprecated_code: type___Status.DeprecatedStatusCodeValue = ... - message: typing___Text = ... - code: type___Status.StatusCodeValue = ... + def HasField(self, field_name: typing_extensions.Literal[u"status",b"status"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"dropped_events_count",b"dropped_events_count",u"dropped_links_count",b"dropped_links_count",u"end_time_unix_nano",b"end_time_unix_nano",u"events",b"events",u"kind",b"kind",u"links",b"links",u"name",b"name",u"parent_span_id",b"parent_span_id",u"span_id",b"span_id",u"start_time_unix_nano",b"start_time_unix_nano",u"status",b"status",u"trace_id",b"trace_id",u"trace_state",b"trace_state"]) -> None: ... +global___Span = Span + +class Status(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + class _DeprecatedStatusCode(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[DeprecatedStatusCode.V], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... + DEPRECATED_STATUS_CODE_OK = Status.DeprecatedStatusCode.V(0) + DEPRECATED_STATUS_CODE_CANCELLED = Status.DeprecatedStatusCode.V(1) + DEPRECATED_STATUS_CODE_UNKNOWN_ERROR = Status.DeprecatedStatusCode.V(2) + DEPRECATED_STATUS_CODE_INVALID_ARGUMENT = Status.DeprecatedStatusCode.V(3) + DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED = Status.DeprecatedStatusCode.V(4) + DEPRECATED_STATUS_CODE_NOT_FOUND = Status.DeprecatedStatusCode.V(5) + DEPRECATED_STATUS_CODE_ALREADY_EXISTS = Status.DeprecatedStatusCode.V(6) + DEPRECATED_STATUS_CODE_PERMISSION_DENIED = Status.DeprecatedStatusCode.V(7) + DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED = Status.DeprecatedStatusCode.V(8) + DEPRECATED_STATUS_CODE_FAILED_PRECONDITION = Status.DeprecatedStatusCode.V(9) + DEPRECATED_STATUS_CODE_ABORTED = Status.DeprecatedStatusCode.V(10) + DEPRECATED_STATUS_CODE_OUT_OF_RANGE = Status.DeprecatedStatusCode.V(11) + DEPRECATED_STATUS_CODE_UNIMPLEMENTED = Status.DeprecatedStatusCode.V(12) + DEPRECATED_STATUS_CODE_INTERNAL_ERROR = Status.DeprecatedStatusCode.V(13) + DEPRECATED_STATUS_CODE_UNAVAILABLE = Status.DeprecatedStatusCode.V(14) + DEPRECATED_STATUS_CODE_DATA_LOSS = Status.DeprecatedStatusCode.V(15) + DEPRECATED_STATUS_CODE_UNAUTHENTICATED = Status.DeprecatedStatusCode.V(16) + class DeprecatedStatusCode(metaclass=_DeprecatedStatusCode): + V = typing.NewType('V', builtins.int) + DEPRECATED_STATUS_CODE_OK = Status.DeprecatedStatusCode.V(0) + DEPRECATED_STATUS_CODE_CANCELLED = Status.DeprecatedStatusCode.V(1) + DEPRECATED_STATUS_CODE_UNKNOWN_ERROR = Status.DeprecatedStatusCode.V(2) + DEPRECATED_STATUS_CODE_INVALID_ARGUMENT = Status.DeprecatedStatusCode.V(3) + DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED = Status.DeprecatedStatusCode.V(4) + DEPRECATED_STATUS_CODE_NOT_FOUND = Status.DeprecatedStatusCode.V(5) + DEPRECATED_STATUS_CODE_ALREADY_EXISTS = Status.DeprecatedStatusCode.V(6) + DEPRECATED_STATUS_CODE_PERMISSION_DENIED = Status.DeprecatedStatusCode.V(7) + DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED = Status.DeprecatedStatusCode.V(8) + DEPRECATED_STATUS_CODE_FAILED_PRECONDITION = Status.DeprecatedStatusCode.V(9) + DEPRECATED_STATUS_CODE_ABORTED = Status.DeprecatedStatusCode.V(10) + DEPRECATED_STATUS_CODE_OUT_OF_RANGE = Status.DeprecatedStatusCode.V(11) + DEPRECATED_STATUS_CODE_UNIMPLEMENTED = Status.DeprecatedStatusCode.V(12) + DEPRECATED_STATUS_CODE_INTERNAL_ERROR = Status.DeprecatedStatusCode.V(13) + DEPRECATED_STATUS_CODE_UNAVAILABLE = Status.DeprecatedStatusCode.V(14) + DEPRECATED_STATUS_CODE_DATA_LOSS = Status.DeprecatedStatusCode.V(15) + DEPRECATED_STATUS_CODE_UNAUTHENTICATED = Status.DeprecatedStatusCode.V(16) + + class _StatusCode(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[StatusCode.V], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... + STATUS_CODE_UNSET = Status.StatusCode.V(0) + STATUS_CODE_OK = Status.StatusCode.V(1) + STATUS_CODE_ERROR = Status.StatusCode.V(2) + class StatusCode(metaclass=_StatusCode): + V = typing.NewType('V', builtins.int) + STATUS_CODE_UNSET = Status.StatusCode.V(0) + STATUS_CODE_OK = Status.StatusCode.V(1) + STATUS_CODE_ERROR = Status.StatusCode.V(2) + + DEPRECATED_CODE_FIELD_NUMBER: builtins.int + MESSAGE_FIELD_NUMBER: builtins.int + CODE_FIELD_NUMBER: builtins.int + deprecated_code: global___Status.DeprecatedStatusCode.V = ... + message: typing.Text = ... + code: global___Status.StatusCode.V = ... def __init__(self, *, - deprecated_code : typing___Optional[type___Status.DeprecatedStatusCodeValue] = None, - message : typing___Optional[typing___Text] = None, - code : typing___Optional[type___Status.StatusCodeValue] = None, + deprecated_code : global___Status.DeprecatedStatusCode.V = ..., + message : typing.Text = ..., + code : global___Status.StatusCode.V = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions___Literal[u"code",b"code",u"deprecated_code",b"deprecated_code",u"message",b"message"]) -> None: ... -type___Status = Status + def ClearField(self, field_name: typing_extensions.Literal[u"code",b"code",u"deprecated_code",b"deprecated_code",u"message",b"message"]) -> None: ... +global___Status = Status diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index f70286eca4..d3082b0599 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -12,7 +12,7 @@ # PROTO_REPO_DIR - the path to an existing checkout of the opentelemetry-proto repo # Pinned commit/branch/tag for the current version used in opentelemetry-proto python package. -PROTO_REPO_BRANCH_OR_COMMIT="v0.6.0" +PROTO_REPO_BRANCH_OR_COMMIT="v0.7.0" set -e From 6087aea507e5acd974e965e2b16884df02612e57 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 8 Mar 2021 14:15:58 -0800 Subject: [PATCH 0803/1517] expose StatusCode from opentelemetry.trace (#1681) --- CHANGELOG.md | 2 ++ .../tests/test_otcollector_trace_exporter.py | 13 ++++--------- .../exporter/otlp/trace_exporter/__init__.py | 2 +- .../exporter/zipkin/encoder/__init__.py | 2 +- .../src/opentelemetry/trace/__init__.py | 1 + .../src/opentelemetry/instrumentation/utils.py | 2 +- .../tests/test_utils.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 18 ++++++------------ 8 files changed, 17 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ecb4cc30e..1812c0b9d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1663](https://github.com/open-telemetry/opentelemetry-python/pull/1663)) - Rename `JaegerSpanExporter` to `JaegerExporter` and rename `ZipkinSpanExporter` to `ZipkinExporter` ([#1664](https://github.com/open-telemetry/opentelemetry-python/pull/1664)) +- Expose `StatusCode` from the `opentelemetry.trace` module + ([#](https://github.com/open-telemetry/opentelemetry-python/pull/)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py index 97b1a37912..1d9442dcef 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py @@ -147,16 +147,12 @@ def test_translate_to_collector(self): otel_spans[0].set_attribute("key_float", 111.22) otel_spans[0].set_attribute("key_int", 333) otel_spans[0].set_status( - trace_api.Status( - trace_api.status.StatusCode.OK, "test description", - ) + trace_api.Status(trace_api.StatusCode.OK, "test description",) ) otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) otel_spans[1].set_status( - trace_api.Status( - trace_api.status.StatusCode.ERROR, {"test", "val"}, - ) + trace_api.Status(trace_api.StatusCode.ERROR, {"test", "val"},) ) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) @@ -196,7 +192,7 @@ def test_translate_to_collector(self): output_spans[2].parent_span_id, b"\x11\x11\x11\x11\x11\x11\x11\x11" ) self.assertEqual( - output_spans[0].status.code, trace_api.status.StatusCode.OK.value, + output_spans[0].status.code, trace_api.StatusCode.OK.value, ) self.assertEqual(output_spans[0].status.message, "test description") self.assertEqual(len(output_spans[0].tracestate.entries), 1) @@ -267,8 +263,7 @@ def test_translate_to_collector(self): trace_pb2.Span.Link.Type.TYPE_UNSPECIFIED, ) self.assertEqual( - output_spans[1].status.code, - trace_api.status.StatusCode.ERROR.value, + output_spans[1].status.code, trace_api.StatusCode.ERROR.value, ) self.assertEqual( output_spans[2].links.link[0].type, diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index a357734bc1..44da4288cb 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -47,7 +47,7 @@ ) from opentelemetry.sdk.trace import Span as ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult -from opentelemetry.trace.status import StatusCode +from opentelemetry.trace import StatusCode logger = logging.getLogger(__name__) diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py index 2b9e53af6e..f1c30410ce 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py @@ -28,10 +28,10 @@ from opentelemetry.trace import ( Span, SpanContext, + StatusCode, format_span_id, format_trace_id, ) -from opentelemetry.trace.status import StatusCode EncodedLocalEndpointT = TypeVar("EncodedLocalEndpointT") diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 6e32fb352d..9ca199cf95 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -516,4 +516,5 @@ def use_span( "set_span_in_context", "use_span", "Status", + "StatusCode", ] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py index 49386f1ddd..dec070570f 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -16,7 +16,7 @@ from wrapt import ObjectProxy -from opentelemetry.trace.status import StatusCode +from opentelemetry.trace import StatusCode def extract_attributes_from_object( diff --git a/opentelemetry-instrumentation/tests/test_utils.py b/opentelemetry-instrumentation/tests/test_utils.py index bcbf6eced0..273c6f085c 100644 --- a/opentelemetry-instrumentation/tests/test_utils.py +++ b/opentelemetry-instrumentation/tests/test_utils.py @@ -16,7 +16,7 @@ from opentelemetry.instrumentation.utils import http_status_to_status_code from opentelemetry.test.test_base import TestBase -from opentelemetry.trace.status import StatusCode +from opentelemetry.trace import StatusCode class TestUtils(TestBase): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 2bcc1bdd19..d4732cf02b 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -37,7 +37,7 @@ from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -from opentelemetry.trace.status import StatusCode +from opentelemetry.trace import StatusCode from opentelemetry.util.time import time_ns @@ -835,18 +835,14 @@ def test_start_span(self): self.assertEqual(start_time, span.start_time) self.assertIsNotNone(span.status) - self.assertIs( - span.status.status_code, trace_api.status.StatusCode.UNSET - ) + self.assertIs(span.status.status_code, trace_api.StatusCode.UNSET) # status new_status = trace_api.status.Status( - trace_api.status.StatusCode.ERROR, "Test description" + trace_api.StatusCode.ERROR, "Test description" ) span.set_status(new_status) - self.assertIs( - span.status.status_code, trace_api.status.StatusCode.ERROR - ) + self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) self.assertIs(span.status.description, "Test description") def test_start_accepts_context(self): @@ -906,14 +902,12 @@ def test_ended_span(self): self.assertEqual(root.name, "root") new_status = trace_api.status.Status( - trace_api.status.StatusCode.ERROR, "Test description" + trace_api.StatusCode.ERROR, "Test description" ) with self.assertLogs(level=WARNING): root.set_status(new_status) - self.assertEqual( - root.status.status_code, trace_api.status.StatusCode.UNSET - ) + self.assertEqual(root.status.status_code, trace_api.StatusCode.UNSET) def test_error_status(self): def error_status_test(context): From fb1ae06026d648d867461396a6d1b5c37cbb3a31 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 10 Mar 2021 01:20:57 +0530 Subject: [PATCH 0804/1517] Span Status: only set description for ERROR status code. (#1673) Co-authored-by: alrex --- CHANGELOG.md | 8 +++-- .../tests/test_jaeger_exporter_protobuf.py | 7 +--- .../tests/test_jaeger_exporter_thrift.py | 7 +--- .../tests/test_otcollector_trace_exporter.py | 5 +-- .../src/opentelemetry/context/context.py | 4 +-- .../src/opentelemetry/trace/status.py | 12 +++++-- opentelemetry-api/tests/trace/test_status.py | 35 ++++++++++++++++++- opentelemetry-sdk/tests/trace/test_trace.py | 4 +-- 8 files changed, 55 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1812c0b9d6..0998593955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.18b0...HEAD) -- Update OTLP exporter to use OTLP proto `0.7.0` - ([#1674](https://github.com/open-telemetry/opentelemetry-python/pull/1674)) ### Added - Document how to work with fork process web server models(Gunicorn, uWSGI etc...) @@ -43,7 +41,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Rename `JaegerSpanExporter` to `JaegerExporter` and rename `ZipkinSpanExporter` to `ZipkinExporter` ([#1664](https://github.com/open-telemetry/opentelemetry-python/pull/1664)) - Expose `StatusCode` from the `opentelemetry.trace` module - ([#](https://github.com/open-telemetry/opentelemetry-python/pull/)) + ([#1681](https://github.com/open-telemetry/opentelemetry-python/pull/1681)) +- Status now only sets `description` when `status_code` is set to `StatusCode.ERROR` + ([#1673](https://github.com/open-telemetry/opentelemetry-python/pull/1673)) +- Update OTLP exporter to use OTLP proto `0.7.0` + ([#1674](https://github.com/open-telemetry/opentelemetry-python/pull/1674)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py index 11603a8f9e..b127b7d4d4 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py @@ -192,7 +192,7 @@ def test_translate_to_jaeger(self): otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) - otel_spans[2].set_status(Status(StatusCode.OK, "Example description")) + otel_spans[2].set_status(Status(StatusCode.OK)) otel_spans[2].end(end_time=end_times[2]) translate = Translate(otel_spans) @@ -352,11 +352,6 @@ def test_translate_to_jaeger(self): v_type=model_pb2.ValueType.STRING, v_str="OK", ), - model_pb2.KeyValue( - key="otel.status_description", - v_type=model_pb2.ValueType.STRING, - v_str="Example description", - ), model_pb2.KeyValue( key="span.kind", v_type=model_pb2.ValueType.STRING, diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py index 750b050fe2..561e30f92c 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py @@ -274,7 +274,7 @@ def test_translate_to_jaeger(self): otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) - otel_spans[2].set_status(Status(StatusCode.OK, "Example description")) + otel_spans[2].set_status(Status(StatusCode.OK)) otel_spans[2].end(end_time=end_times[2]) translate = Translate(otel_spans) @@ -396,11 +396,6 @@ def test_translate_to_jaeger(self): vType=jaeger.TagType.STRING, vStr="OK", ), - jaeger.Tag( - key="otel.status_description", - vType=jaeger.TagType.STRING, - vStr="Example description", - ), jaeger.Tag( key="span.kind", vType=jaeger.TagType.STRING, diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py index 1d9442dcef..8fd2460de0 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py @@ -146,9 +146,7 @@ def test_translate_to_collector(self): otel_spans[0].set_attribute("key_string", "hello_world") otel_spans[0].set_attribute("key_float", 111.22) otel_spans[0].set_attribute("key_int", 333) - otel_spans[0].set_status( - trace_api.Status(trace_api.StatusCode.OK, "test description",) - ) + otel_spans[0].set_status(trace_api.Status(trace_api.StatusCode.OK)) otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) otel_spans[1].set_status( @@ -194,7 +192,6 @@ def test_translate_to_collector(self): self.assertEqual( output_spans[0].status.code, trace_api.StatusCode.OK.value, ) - self.assertEqual(output_spans[0].status.message, "test description") self.assertEqual(len(output_spans[0].tracestate.entries), 1) self.assertEqual(output_spans[0].tracestate.entries[0].key, "testkey") self.assertEqual( diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index c7508603a5..b4867611ef 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -30,7 +30,7 @@ class RuntimeContext(ABC): @abstractmethod def attach(self, context: Context) -> object: - """ Sets the current `Context` object. Returns a + """Sets the current `Context` object. Returns a token that can be used to reset to the previous `Context`. Args: @@ -43,7 +43,7 @@ def get_current(self) -> Context: @abstractmethod def detach(self, token: object) -> None: - """ Resets Context to a previous value + """Resets Context to a previous value Args: token: A reference to a previous Context. diff --git a/opentelemetry-api/src/opentelemetry/trace/status.py b/opentelemetry-api/src/opentelemetry/trace/status.py index 822d3ca83d..5e0469d71a 100644 --- a/opentelemetry-api/src/opentelemetry/trace/status.py +++ b/opentelemetry-api/src/opentelemetry/trace/status.py @@ -48,10 +48,18 @@ def __init__( ): self._status_code = status_code self._description = None + if description is not None and not isinstance(description, str): logger.warning("Invalid status description type, expected str") - else: - self._description = description + return + + if status_code is not StatusCode.ERROR: + logger.warning( + "description should only be set when status_code is set to StatusCode.ERROR" + ) + return + + self._description = description @property def status_code(self) -> StatusCode: diff --git a/opentelemetry-api/tests/trace/test_status.py b/opentelemetry-api/tests/trace/test_status.py index cd0f678d13..fdfcd4e83e 100644 --- a/opentelemetry-api/tests/trace/test_status.py +++ b/opentelemetry-api/tests/trace/test_status.py @@ -29,7 +29,40 @@ def test_constructor(self): self.assertEqual(status.description, "unavailable") def test_invalid_description(self): - with self.assertLogs(level=WARNING): + with self.assertLogs(level=WARNING) as warning: status = Status(description={"test": "val"}) # type: ignore self.assertIs(status.status_code, StatusCode.UNSET) self.assertEqual(status.description, None) + self.assertIn( + "Invalid status description type, expected str", + warning.output[0], + ) + + def test_description_and_non_error_status(self): + with self.assertLogs(level=WARNING) as warning: + status = Status( + status_code=StatusCode.OK, description="status description" + ) + self.assertIs(status.status_code, StatusCode.OK) + self.assertEqual(status.description, None) + self.assertIn( + "description should only be set when status_code is set to StatusCode.ERROR", + warning.output[0], + ) + + with self.assertLogs(level=WARNING) as warning: + status = Status( + status_code=StatusCode.UNSET, description="status description" + ) + self.assertIs(status.status_code, StatusCode.UNSET) + self.assertEqual(status.description, None) + self.assertIn( + "description should only be set when status_code is set to StatusCode.ERROR", + warning.output[0], + ) + + status = Status( + status_code=StatusCode.ERROR, description="status description" + ) + self.assertIs(status.status_code, StatusCode.ERROR) + self.assertEqual(status.description, "status description") diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index d4732cf02b..49bff1bc20 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -933,9 +933,7 @@ def test_last_status_wins(self): def error_status_test(context): with self.assertRaises(AssertionError): with context as root: - root.set_status( - trace_api.status.Status(StatusCode.OK, "OK") - ) + root.set_status(trace_api.status.Status(StatusCode.OK)) raise AssertionError("unknown") self.assertIs(root.status.status_code, StatusCode.ERROR) From 5cc7ec5763d3567cd46f65cc296c3cd3a7b5a261 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 10 Mar 2021 10:21:50 -0600 Subject: [PATCH 0805/1517] Remove time_ns from API (#1602) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 + .../opentelemetry/util/{time.py => _time.py} | 16 ++++++-- .../src/opentelemetry/sdk/trace/__init__.py | 14 +++---- .../sdk/trace/export/__init__.py | 6 +-- opentelemetry-sdk/tests/trace/test_trace.py | 4 +- .../tests/test_util.py | 41 +++++++++++-------- 7 files changed, 50 insertions(+), 35 deletions(-) rename opentelemetry-api/src/opentelemetry/util/{time.py => _time.py} (57%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 12f49c1491..5016576346 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 8783e0ff97ad123006ff1ff2c2cf3f52161a406f + CONTRIB_REPO_SHA: 5bc0fa1611502be47a1f4eb550fe255e4b707ba1 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 0998593955..d46f9f2b27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1673](https://github.com/open-telemetry/opentelemetry-python/pull/1673)) - Update OTLP exporter to use OTLP proto `0.7.0` ([#1674](https://github.com/open-telemetry/opentelemetry-python/pull/1674)) +- Remove time_ns from API and add a warning for older versions of Python + ([#1602](https://github.com/open-telemetry/opentelemetry-python/pull/1602)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. diff --git a/opentelemetry-api/src/opentelemetry/util/time.py b/opentelemetry-api/src/opentelemetry/util/_time.py similarity index 57% rename from opentelemetry-api/src/opentelemetry/util/time.py rename to opentelemetry-api/src/opentelemetry/util/_time.py index fa9f901d0d..10a2aee33e 100644 --- a/opentelemetry-api/src/opentelemetry/util/time.py +++ b/opentelemetry-api/src/opentelemetry/util/_time.py @@ -12,16 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. +from logging import getLogger from sys import version_info if version_info.minor < 7: + getLogger(__name__).warning( # pylint: disable=logging-not-lazy + "You are using Python 3.%s. This version does not support timestamps " + "with nanosecond precision and the Opentelemetry SDK will use " + "millisecond precision instead. Please refer to PEP 546 for more " + "information. Please upgrade to Python 3.7 or newer to use nanosecond " + "precision." % version_info.minor + ) from time import time - def time_ns() -> int: - # FIXME this approach can have precision problems as explained here: - # https://github.com/open-telemetry/opentelemetry-python/issues/1594 + def _time_ns(): return int(time() * 1e9) else: - from time import time_ns # pylint: disable=unused-import + from time import time_ns + + _time_ns = time_ns diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 944d4002d6..2ce5c73cc1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -54,7 +54,7 @@ from opentelemetry.trace.propagation import SPAN_KEY from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types -from opentelemetry.util.time import time_ns +from opentelemetry.util._time import _time_ns logger = logging.getLogger(__name__) @@ -171,9 +171,9 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: True if all span processors flushed their spans within the given timeout, False otherwise. """ - deadline_ns = time_ns() + timeout_millis * 1000000 + deadline_ns = _time_ns() + timeout_millis * 1000000 for sp in self._span_processors: - current_time_ns = time_ns() + current_time_ns = _time_ns() if current_time_ns >= deadline_ns: return False @@ -273,7 +273,7 @@ class EventBase(abc.ABC): def __init__(self, name: str, timestamp: Optional[int] = None) -> None: self._name = name if timestamp is None: - self._timestamp = time_ns() + self._timestamp = _time_ns() else: self._timestamp = timestamp @@ -708,7 +708,7 @@ def add_event( Event( name=name, attributes=attributes, - timestamp=time_ns() if timestamp is None else timestamp, + timestamp=_time_ns() if timestamp is None else timestamp, ) ) @@ -738,7 +738,7 @@ def start( logger.warning("Calling start() on a started span.") return self._start_time = ( - start_time if start_time is not None else time_ns() + start_time if start_time is not None else _time_ns() ) self._span_processor.on_start(self, parent_context=parent_context) @@ -751,7 +751,7 @@ def end(self, end_time: Optional[int] = None) -> None: logger.warning("Calling end() on an ended span.") return - self._end_time = end_time if end_time is not None else time_ns() + self._end_time = end_time if end_time is not None else _time_ns() self._span_processor.on_end(self._readable_span()) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index ae24bcd002..b63a30c16b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -29,7 +29,7 @@ OTEL_BSP_SCHEDULE_DELAY, ) from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor -from opentelemetry.util.time import time_ns +from opentelemetry.util._time import _time_ns logger = logging.getLogger(__name__) @@ -231,9 +231,9 @@ def worker(self): break # subtract the duration of this export call to the next timeout - start = time_ns() + start = _time_ns() self._export(flush_request) - end = time_ns() + end = _time_ns() duration = (end - start) / 1e9 timeout = self.schedule_delay_millis / 1e3 - duration diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 49bff1bc20..7e33f869a3 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -38,7 +38,7 @@ from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import StatusCode -from opentelemetry.util.time import time_ns +from opentelemetry.util._time import _time_ns def new_tracer() -> trace_api.Tracer: @@ -709,7 +709,7 @@ def test_events(self): ) # event name, attributes and timestamp - now = time_ns() + now = _time_ns() root.add_event("event2", {"name": ["birthday"]}, now) mutable_list = ["original_contents"] diff --git a/shim/opentelemetry-opentracing-shim/tests/test_util.py b/shim/opentelemetry-opentracing-shim/tests/test_util.py index 7ce3c36950..e9a57a9d1f 100644 --- a/shim/opentelemetry-opentracing-shim/tests/test_util.py +++ b/shim/opentelemetry-opentracing-shim/tests/test_util.py @@ -12,41 +12,46 @@ # See the License for the specific language governing permissions and # limitations under the License. -import time -import unittest +from time import time +from unittest import TestCase -from opentelemetry.shim.opentracing_shim import util -from opentelemetry.util.time import time_ns +from opentelemetry.shim.opentracing_shim.util import ( + DEFAULT_EVENT_NAME, + event_name_from_kv, + time_seconds_from_ns, + time_seconds_to_ns, +) +from opentelemetry.util._time import _time_ns -class TestUtil(unittest.TestCase): +class TestUtil(TestCase): def test_event_name_from_kv(self): # Test basic behavior. event_name = "send HTTP request" - res = util.event_name_from_kv({"event": event_name, "foo": "bar"}) + res = event_name_from_kv({"event": event_name, "foo": "bar"}) self.assertEqual(res, event_name) # Test None. - res = util.event_name_from_kv(None) - self.assertEqual(res, util.DEFAULT_EVENT_NAME) + res = event_name_from_kv(None) + self.assertEqual(res, DEFAULT_EVENT_NAME) # Test empty dict. - res = util.event_name_from_kv({}) - self.assertEqual(res, util.DEFAULT_EVENT_NAME) + res = event_name_from_kv({}) + self.assertEqual(res, DEFAULT_EVENT_NAME) # Test missing `event` field. - res = util.event_name_from_kv({"foo": "bar"}) - self.assertEqual(res, util.DEFAULT_EVENT_NAME) + res = event_name_from_kv({"foo": "bar"}) + self.assertEqual(res, DEFAULT_EVENT_NAME) def test_time_seconds_to_ns(self): - time_seconds = time.time() - result = util.time_seconds_to_ns(time_seconds) + time_seconds = time() + result = time_seconds_to_ns(time_seconds) self.assertEqual(result, int(time_seconds * 1e9)) def test_time_seconds_from_ns(self): - time_nanoseconds = time_ns() - result = util.time_seconds_from_ns(time_nanoseconds) + time_nanoseconds = _time_ns() + result = time_seconds_from_ns(time_nanoseconds) self.assertEqual(result, time_nanoseconds / 1e9) @@ -56,8 +61,8 @@ def test_time_conversion_precision(self): """ time_seconds = 1570484241.9501917 - time_nanoseconds = util.time_seconds_to_ns(time_seconds) - result = util.time_seconds_from_ns(time_nanoseconds) + time_nanoseconds = time_seconds_to_ns(time_seconds) + result = time_seconds_from_ns(time_nanoseconds) # Tolerate inaccuracies of less than a microsecond. # TODO: Put a link to an explanation in the docs. From 0cd037fdd396f68a8080f84b3a71ecea49c1a393 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 10 Mar 2021 09:10:37 -0800 Subject: [PATCH 0806/1517] docs: update documentation to reflect default (#1676) --- docs/getting-started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 6432e02103..b39f8c68cd 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -184,7 +184,7 @@ To enable this propagation, OpenTelemetry has the concept of `propagators `_ -HTTP headers for HTTP requests, but you can configure it to leverage different propagators. Here's +and `W3C Baggage `_ HTTP headers for HTTP requests, but you can configure it to leverage different propagators. Here's an example using Zipkin's `b3 propagation `_: .. code-block:: sh From c81fd5e19ee91ecbed2e3d09804cc1137704d419 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 10 Mar 2021 10:32:30 -0800 Subject: [PATCH 0807/1517] Hide certain implementation specific classes/variables (#1684) --- CHANGELOG.md | 2 ++ docs/conf.py | 1 + opentelemetry-api/setup.cfg | 2 +- .../baggage/propagation/__init__.py | 12 +++++----- .../src/opentelemetry/context/__init__.py | 4 ++-- .../src/opentelemetry/context/context.py | 6 ++--- .../context/contextvars_context.py | 19 +++++++++++---- .../src/opentelemetry/trace/__init__.py | 17 ++++--------- .../tests/baggage/test_baggage_propagation.py | 8 +++---- .../tests/test_implementation.py | 3 ++- opentelemetry-api/tests/trace/test_globals.py | 5 ++-- opentelemetry-api/tests/trace/test_tracer.py | 3 ++- .../sdk/error_handler/__init__.py | 4 ++-- .../opentelemetry/sdk/resources/__init__.py | 4 ++-- .../src/opentelemetry/sdk/trace/__init__.py | 24 +++++++++---------- .../tests/resources/test_resources.py | 4 ++-- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- 17 files changed, 64 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d46f9f2b27..df3a6e5e06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1674](https://github.com/open-telemetry/opentelemetry-python/pull/1674)) - Remove time_ns from API and add a warning for older versions of Python ([#1602](https://github.com/open-telemetry/opentelemetry-python/pull/1602)) +- Hide implementation classes/variables in api/sdk + ([#1684](https://github.com/open-telemetry/opentelemetry-python/pull/1684)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. diff --git a/docs/conf.py b/docs/conf.py index a8c659b2d4..d23cebfe96 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -101,6 +101,7 @@ # Even if wrapt is added to intersphinx_mapping, sphinx keeps failing # with "class reference target not found: ObjectProxy". ("py:class", "ObjectProxy"), + ("py:class", "opentelemetry.trace._LinkBase",), # TODO: Understand why sphinx is not able to find this local class ("py:class", "opentelemetry.propagators.textmap.TextMapPropagator",), ("py:class", "opentelemetry.propagators.textmap.DictGetter",), diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 332069041c..428eb5a8e8 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -51,7 +51,7 @@ where = src opentelemetry_context = contextvars_context = opentelemetry.context.contextvars_context:ContextVarsRuntimeContext opentelemetry_tracer_provider = - default_tracer_provider = opentelemetry.trace:DefaultTracerProvider + default_tracer_provider = opentelemetry.trace:_DefaultTracerProvider opentelemetry_propagator = tracecontext = opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator baggage = opentelemetry.baggage.propagation:W3CBaggagePropagator diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index e59df9d285..e6d1c4207b 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -24,9 +24,9 @@ class W3CBaggagePropagator(textmap.TextMapPropagator): """Extracts and injects Baggage which is used to annotate telemetry.""" - MAX_HEADER_LENGTH = 8192 - MAX_PAIR_LENGTH = 4096 - MAX_PAIRS = 180 + _MAX_HEADER_LENGTH = 8192 + _MAX_PAIR_LENGTH = 4096 + _MAX_PAIRS = 180 _BAGGAGE_HEADER_NAME = "baggage" def extract( @@ -48,16 +48,16 @@ def extract( getter.get(carrier, self._BAGGAGE_HEADER_NAME) ) - if not header or len(header) > self.MAX_HEADER_LENGTH: + if not header or len(header) > self._MAX_HEADER_LENGTH: return context baggage_entries = header.split(",") - total_baggage_entries = self.MAX_PAIRS + total_baggage_entries = self._MAX_PAIRS for entry in baggage_entries: if total_baggage_entries <= 0: return context total_baggage_entries -= 1 - if len(entry) > self.MAX_PAIR_LENGTH: + if len(entry) > self._MAX_PAIR_LENGTH: continue try: name, value = entry.split("=", 1) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 2f1249d4d0..3328d62772 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -20,11 +20,11 @@ from pkg_resources import iter_entry_points -from opentelemetry.context.context import Context, RuntimeContext +from opentelemetry.context.context import Context, _RuntimeContext from opentelemetry.environment_variables import OTEL_PYTHON_CONTEXT logger = logging.getLogger(__name__) -_RUNTIME_CONTEXT = None # type: typing.Optional[RuntimeContext] +_RUNTIME_CONTEXT = None # type: typing.Optional[_RuntimeContext] _RUNTIME_CONTEXT_LOCK = threading.Lock() _F = typing.TypeVar("_F", bound=typing.Callable[..., typing.Any]) diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index b4867611ef..48ead2205f 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -21,7 +21,7 @@ def __setitem__(self, key: str, value: object) -> None: raise ValueError -class RuntimeContext(ABC): +class _RuntimeContext(ABC): """The RuntimeContext interface provides a wrapper for the different mechanisms that are used to propagate context in Python. Implementations can be made available via entry_points and @@ -39,7 +39,7 @@ def attach(self, context: Context) -> object: @abstractmethod def get_current(self) -> Context: - """ Returns the current `Context` object. """ + """Returns the current `Context` object. """ @abstractmethod def detach(self, token: object) -> None: @@ -50,4 +50,4 @@ def detach(self, token: object) -> None: """ -__all__ = ["Context", "RuntimeContext"] +__all__ = ["Context"] diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index 429fd10c2c..fcb10fba7e 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -14,7 +14,7 @@ from contextvars import ContextVar from sys import version_info -from opentelemetry.context.context import Context, RuntimeContext +from opentelemetry.context.context import Context, _RuntimeContext if (3, 5, 3) <= version_info < (3, 7): import aiocontextvars # type: ignore # pylint:disable=unused-import,import-error @@ -23,7 +23,7 @@ import opentelemetry.context.aiocontextvarsfix # pylint:disable=unused-import -class ContextVarsRuntimeContext(RuntimeContext): +class ContextVarsRuntimeContext(_RuntimeContext): """An implementation of the RuntimeContext interface which wraps ContextVar under the hood. This is the prefered implementation for usage with Python 3.5+ """ @@ -36,15 +36,24 @@ def __init__(self) -> None: ) def attach(self, context: Context) -> object: - """See `opentelemetry.context.RuntimeContext.attach`.""" + """Sets the current `Context` object. Returns a + token that can be used to reset to the previous `Context`. + + Args: + context: The Context to set. + """ return self._current_context.set(context) def get_current(self) -> Context: - """See `opentelemetry.context.RuntimeContext.get_current`.""" + """Returns the current `Context` object. """ return self._current_context.get() def detach(self, token: object) -> None: - """See `opentelemetry.context.RuntimeContext.detach`.""" + """Resets Context to a previous value + + Args: + token: A reference to a previous Context. + """ self._current_context.reset(token) # type: ignore diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 9ca199cf95..59ae217304 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -110,7 +110,7 @@ logger = getLogger(__name__) -class LinkBase(ABC): +class _LinkBase(ABC): def __init__(self, context: "SpanContext") -> None: self._context = context @@ -124,7 +124,7 @@ def attributes(self) -> types.Attributes: pass -class Link(LinkBase): +class Link(_LinkBase): """A link to a `Span`. Args: @@ -205,7 +205,7 @@ def get_tracer( """ -class DefaultTracerProvider(TracerProvider): +class _DefaultTracerProvider(TracerProvider): """The default TracerProvider, used when no implementation is available. All operations are no-op. @@ -217,7 +217,7 @@ def get_tracer( instrumenting_library_version: str = "", ) -> "Tracer": # pylint:disable=no-self-use,unused-argument - return DefaultTracer() + return _DefaultTracer() class Tracer(ABC): @@ -227,10 +227,6 @@ class Tracer(ABC): and controlling spans' lifecycles. """ - # Constant used to represent the current span being used as a parent. - # This is the default behavior when creating spans. - CURRENT_SPAN = NonRecordingSpan(INVALID_SPAN_CONTEXT) - @abstractmethod def start_span( self, @@ -353,7 +349,7 @@ def start_as_current_span( """ -class DefaultTracer(Tracer): +class _DefaultTracer(Tracer): """The default Tracer, used when no Tracer implementation is available. All operations are no-op. @@ -496,10 +492,7 @@ def use_span( "INVALID_SPAN_ID", "INVALID_TRACE_ID", "NonRecordingSpan", - "DefaultTracer", - "DefaultTracerProvider", "Link", - "LinkBase", "Span", "SpanContext", "SpanKind", diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py index 954bc1aba7..a928a2fc8c 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py @@ -87,7 +87,7 @@ def test_invalid_header(self): self.assertEqual(self._extract(header), expected) def test_header_too_long(self): - long_value = "s" * (W3CBaggagePropagator.MAX_HEADER_LENGTH + 1) + long_value = "s" * (W3CBaggagePropagator._MAX_HEADER_LENGTH + 1) header = "key1={}".format(long_value) expected = {} self.assertEqual(self._extract(header), expected) @@ -96,15 +96,15 @@ def test_header_contains_too_many_entries(self): header = ",".join( [ "key{}=val".format(k) - for k in range(W3CBaggagePropagator.MAX_PAIRS + 1) + for k in range(W3CBaggagePropagator._MAX_PAIRS + 1) ] ) self.assertEqual( - len(self._extract(header)), W3CBaggagePropagator.MAX_PAIRS + len(self._extract(header)), W3CBaggagePropagator._MAX_PAIRS ) def test_header_contains_pair_too_long(self): - long_value = "s" * (W3CBaggagePropagator.MAX_PAIR_LENGTH + 1) + long_value = "s" * (W3CBaggagePropagator._MAX_PAIR_LENGTH + 1) header = "key1=value1,key2={},key3=value3".format(long_value) expected = {"key1": "value1", "key3": "value3"} self.assertEqual(self._extract(header), expected) diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index 3cb3689961..1871ea951e 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -33,7 +33,8 @@ def test_tracer(self): trace.TracerProvider() # type:ignore def test_default_tracer(self): - tracer_provider = trace.DefaultTracerProvider() + # pylint: disable=protected-access + tracer_provider = trace._DefaultTracerProvider() tracer = tracer_provider.get_tracer(__name__) with tracer.start_span("test") as span: self.assertEqual( diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index a6cfb9eb2c..a297d054ef 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -44,10 +44,11 @@ def test_get_tracer(self): class TestTracer(unittest.TestCase): def setUp(self): - self.tracer = trace.DefaultTracer() + # pylint: disable=protected-access + self.tracer = trace._DefaultTracer() def test_get_current_span(self): - """DefaultTracer's start_span will also + """_DefaultTracer's start_span will also be retrievable via get_current_span """ self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN) diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py index d0c451e1f2..382aeec743 100644 --- a/opentelemetry-api/tests/trace/test_tracer.py +++ b/opentelemetry-api/tests/trace/test_tracer.py @@ -19,7 +19,8 @@ class TestTracer(unittest.TestCase): def setUp(self): - self.tracer = trace.DefaultTracer() + # pylint: disable=protected-access + self.tracer = trace._DefaultTracer() def test_start_span(self): with self.tracer.start_span("") as span: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/error_handler/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/error_handler/__init__.py index 6afbd7c2f3..781f42e41a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/error_handler/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/error_handler/__init__.py @@ -75,7 +75,7 @@ def _handle(self, error: Exception, *args, **kwargs): """ -class DefaultErrorHandler(ErrorHandler): +class _DefaultErrorHandler(ErrorHandler): """ Default error handler @@ -144,6 +144,6 @@ def __exit__(self, exc_type, exc_value, traceback): if not plugin_handled: - DefaultErrorHandler()._handle(exc_value) + _DefaultErrorHandler()._handle(exc_value) return True diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index a7fa6dd54c..637e71166b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -149,7 +149,7 @@ TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language" -OPENTELEMETRY_SDK_VERSION = pkg_resources.get_distribution( +_OPENTELEMETRY_SDK_VERSION = pkg_resources.get_distribution( "opentelemetry-sdk" ).version @@ -225,7 +225,7 @@ def __hash__(self): { TELEMETRY_SDK_LANGUAGE: "python", TELEMETRY_SDK_NAME: "opentelemetry", - TELEMETRY_SDK_VERSION: OPENTELEMETRY_SDK_VERSION, + TELEMETRY_SDK_VERSION: _OPENTELEMETRY_SDK_VERSION, } ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 2ce5c73cc1..ced0925a3b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -62,11 +62,11 @@ environ.get(OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, 128) ) -SPAN_EVENT_COUNT_LIMIT = int(environ.get(OTEL_SPAN_EVENT_COUNT_LIMIT, 128)) -SPAN_LINK_COUNT_LIMIT = int(environ.get(OTEL_SPAN_LINK_COUNT_LIMIT, 128)) -VALID_ATTR_VALUE_TYPES = (bool, str, int, float) +_SPAN_EVENT_COUNT_LIMIT = int(environ.get(OTEL_SPAN_EVENT_COUNT_LIMIT, 128)) +_SPAN_LINK_COUNT_LIMIT = int(environ.get(OTEL_SPAN_LINK_COUNT_LIMIT, 128)) +_VALID_ATTR_VALUE_TYPES = (bool, str, int, float) # pylint: disable=protected-access -TRACE_SAMPLER = sampling._get_from_env_or_default() +_TRACE_SAMPLER = sampling._get_from_env_or_default() class SpanProcessor: @@ -333,14 +333,14 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool: if element is None: continue element_type = type(element) - if element_type not in VALID_ATTR_VALUE_TYPES: + if element_type not in _VALID_ATTR_VALUE_TYPES: logger.warning( "Invalid type %s in attribute value sequence. Expected one of " "%s or None", element_type.__name__, [ valid_type.__name__ - for valid_type in VALID_ATTR_VALUE_TYPES + for valid_type in _VALID_ATTR_VALUE_TYPES ], ) return False @@ -356,12 +356,12 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool: ) return False - elif not isinstance(value, VALID_ATTR_VALUE_TYPES): + elif not isinstance(value, _VALID_ATTR_VALUE_TYPES): logger.warning( "Invalid type %s for attribute value. Expected one of %s or a " "sequence of those types", type(value).__name__, - [valid_type.__name__ for valid_type in VALID_ATTR_VALUE_TYPES], + [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES], ) return False return True @@ -640,7 +640,7 @@ def __init__( if links is None: self._links = self._new_links() else: - self._links = BoundedList.from_seq(SPAN_LINK_COUNT_LIMIT, links) + self._links = BoundedList.from_seq(_SPAN_LINK_COUNT_LIMIT, links) def __repr__(self): return '{}(name="{}", context={})'.format( @@ -653,11 +653,11 @@ def _new_attributes(): @staticmethod def _new_events(): - return BoundedList(SPAN_EVENT_COUNT_LIMIT) + return BoundedList(_SPAN_EVENT_COUNT_LIMIT) @staticmethod def _new_links(): - return BoundedList(SPAN_LINK_COUNT_LIMIT) + return BoundedList(_SPAN_LINK_COUNT_LIMIT) def get_span_context(self): return self._context @@ -962,7 +962,7 @@ class TracerProvider(trace_api.TracerProvider): def __init__( self, - sampler: sampling.Sampler = TRACE_SAMPLER, + sampler: sampling.Sampler = _TRACE_SAMPLER, resource: Resource = Resource.create({}), shutdown_on_exit: bool = True, active_span_processor: Union[ diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 4151c2fbd8..4b8fd71da2 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -43,7 +43,7 @@ def test_create(self): "cost": 112.12, resources.TELEMETRY_SDK_NAME: "opentelemetry", resources.TELEMETRY_SDK_LANGUAGE: "python", - resources.TELEMETRY_SDK_VERSION: resources.OPENTELEMETRY_SDK_VERSION, + resources.TELEMETRY_SDK_VERSION: resources._OPENTELEMETRY_SDK_VERSION, resources.SERVICE_NAME: "unknown_service", } @@ -113,7 +113,7 @@ def test_immutability(self): default_attributes = { resources.TELEMETRY_SDK_NAME: "opentelemetry", resources.TELEMETRY_SDK_LANGUAGE: "python", - resources.TELEMETRY_SDK_VERSION: resources.OPENTELEMETRY_SDK_VERSION, + resources.TELEMETRY_SDK_VERSION: resources._OPENTELEMETRY_SDK_VERSION, resources.SERVICE_NAME: "unknown_service", } diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 7e33f869a3..df2454a521 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -516,7 +516,7 @@ def test_default_span_resource(self): ) self.assertEqual( span.resource.attributes.get(resources.TELEMETRY_SDK_VERSION), - resources.OPENTELEMETRY_SDK_VERSION, + resources._OPENTELEMETRY_SDK_VERSION, ) def test_span_context_remote_flag(self): From e3c8215ff629822d64c0615aa8cde0db70566a27 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 11 Mar 2021 13:51:34 -0800 Subject: [PATCH 0808/1517] Update README.md (#1687) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3ab7bd25ec..2eafcc6d12 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,7 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Diego Hurtado](https://github.com/ocelotl) - [Hector Hernandez](https://github.com/hectorhdzg), Microsoft - [Owais Lone](https://github.com/owais), Splunk +- [Srikanth Chekuri](https://github.com/lonewolf3739) - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google *For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver).* From 58a8e8c3a61474b20eb31722f93b14a32b5236de Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 11 Mar 2021 20:15:44 -0500 Subject: [PATCH 0809/1517] Cleanup OTLP exporter compression options, add tests (#1671) --- CHANGELOG.md | 2 + .../opentelemetry/exporter/otlp/exporter.py | 64 ++++++++-------- .../exporter/otlp/trace_exporter/__init__.py | 39 ++++++---- .../tests/test_otlp_exporter_mixin.py | 47 ++++++++++++ .../tests/test_otlp_trace_exporter.py | 74 ++++++++++++++++--- .../sdk/environment_variables/__init__.py | 14 ++-- 6 files changed, 179 insertions(+), 61 deletions(-) create mode 100644 exporter/opentelemetry-exporter-otlp/tests/test_otlp_exporter_mixin.py diff --git a/CHANGELOG.md b/CHANGELOG.md index df3a6e5e06..d0d6cbbaae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1602](https://github.com/open-telemetry/opentelemetry-python/pull/1602)) - Hide implementation classes/variables in api/sdk ([#1684](https://github.com/open-telemetry/opentelemetry-python/pull/1684)) +- Cleanup OTLP exporter compression options, add tests + ([#1671](https://github.com/open-telemetry/opentelemetry-python/pull/1671)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index 06f0c9e6c7..f5da0609f3 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -14,7 +14,6 @@ """OTLP Exporter""" -import enum import logging from abc import ABC, abstractmethod from collections.abc import Mapping, Sequence @@ -40,6 +39,7 @@ from opentelemetry.proto.resource.v1.resource_pb2 import Resource from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_INSECURE, @@ -54,9 +54,30 @@ ExportServiceRequestT = TypeVar("ExportServiceRequestT") ExportResultT = TypeVar("ExportResultT") +_ENVIRON_TO_COMPRESSION = { + None: None, + "gzip": Compression.Gzip, +} -class OTLPCompression(enum.Enum): - gzip = "gzip" + +class InvalidCompressionValueException(Exception): + def __init__(self, environ_key: str, environ_value: str): + super().__init__( + 'Invalid value "{}" for compression envvar {}'.format( + environ_value, environ_key + ) + ) + + +def environ_to_compression(environ_key: str) -> Optional[Compression]: + environ_value = ( + environ[environ_key].lower().strip() + if environ_key in environ + else None + ) + if environ_value not in _ENVIRON_TO_COMPRESSION: + raise InvalidCompressionValueException(environ_key, environ_value) + return _ENVIRON_TO_COMPRESSION[environ_value] def _translate_key_values(key: Text, value: Any) -> KeyValue: @@ -87,7 +108,7 @@ def _translate_key_values(key: Text, value: Any) -> KeyValue: return KeyValue(key=key, value=any_value) -def _get_resource_data( +def get_resource_data( sdk_resource_instrumentation_library_data: Dict[ SDKResource, ResourceDataT ], @@ -149,8 +170,8 @@ class OTLPExporterMixin( insecure: Connection type credentials: ChannelCredentials object for server authentication headers: Headers to send when exporting - compression: Compression algorithm to be used in channel timeout: Backend request timeout in seconds + compression: gRPC compression method to use """ def __init__( @@ -160,7 +181,7 @@ def __init__( credentials: Optional[ChannelCredentials] = None, headers: Optional[Sequence] = None, timeout: Optional[int] = None, - compression: str = None, + compression: Optional[Compression] = None, ): super().__init__() @@ -187,30 +208,15 @@ def __init__( ) self._collector_span_kwargs = None - if compression is None: - compression_algorithm = Compression.NoCompression - elif ( - compression in OTLPCompression._value2member_map_ - and OTLPCompression(compression) is OTLPCompression.gzip - ): - compression_algorithm = Compression.Gzip - else: - compression_str = environ.get(OTEL_EXPORTER_OTLP_INSECURE) - if compression_str is None: - compression_algorithm = Compression.NoCompression - elif ( - compression_str in OTLPCompression._value2member_map_ - and OTLPCompression(compression_str) is OTLPCompression.gzip - ): - compression_algorithm = Compression.Gzip - else: - raise ValueError( - "OTEL_EXPORTER_OTLP_COMPRESSION environment variable does not match gzip." - ) + compression = ( + environ_to_compression(OTEL_EXPORTER_OTLP_COMPRESSION) + if compression is None + else compression + ) or Compression.NoCompression if insecure: self._client = self._stub( - insecure_channel(endpoint, compression=compression_algorithm) + insecure_channel(endpoint, compression=compression) ) return @@ -226,9 +232,7 @@ def __init__( environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE) ) self._client = self._stub( - secure_channel( - endpoint, credentials, compression=compression_algorithm - ) + secure_channel(endpoint, credentials, compression=compression) ) @abstractmethod diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index 44da4288cb..bbc43956cb 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -17,13 +17,14 @@ from os import environ from typing import Optional, Sequence -from grpc import ChannelCredentials +from grpc import ChannelCredentials, Compression from opentelemetry.exporter.otlp.exporter import ( OTLPExporterMixin, - _get_resource_data, _load_credential_from_file, _translate_key_values, + environ_to_compression, + get_resource_data, ) from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest, @@ -39,11 +40,12 @@ from opentelemetry.proto.trace.v1.trace_pb2 import Span as CollectorSpan from opentelemetry.proto.trace.v1.trace_pb2 import Status from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE, - OTEL_EXPORTER_OTLP_SPAN_ENDPOINT, - OTEL_EXPORTER_OTLP_SPAN_HEADERS, - OTEL_EXPORTER_OTLP_SPAN_INSECURE, - OTEL_EXPORTER_OTLP_SPAN_TIMEOUT, + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_INSECURE, + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) from opentelemetry.sdk.trace import Span as ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult @@ -68,6 +70,7 @@ class OTLPSpanExporter( credentials: Credentials object for server authentication headers: Headers to send when exporting timeout: Backend request timeout in seconds + compression: gRPC compression method to use """ _result = SpanExportResult @@ -80,32 +83,40 @@ def __init__( credentials: Optional[ChannelCredentials] = None, headers: Optional[Sequence] = None, timeout: Optional[int] = None, + compression: Optional[Compression] = None, ): if insecure is None: - insecure = environ.get(OTEL_EXPORTER_OTLP_SPAN_INSECURE) + insecure = environ.get(OTEL_EXPORTER_OTLP_TRACES_INSECURE) if ( not insecure - and environ.get(OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE) is not None + and environ.get(OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE) is not None ): credentials = credentials or _load_credential_from_file( - environ.get(OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE) + environ.get(OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE) ) - environ_timeout = environ.get(OTEL_EXPORTER_OTLP_SPAN_TIMEOUT) + environ_timeout = environ.get(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT) environ_timeout = ( int(environ_timeout) if environ_timeout is not None else None ) + compression = ( + environ_to_compression(OTEL_EXPORTER_OTLP_TRACES_COMPRESSION) + if compression is None + else compression + ) + super().__init__( **{ "endpoint": endpoint - or environ.get(OTEL_EXPORTER_OTLP_SPAN_ENDPOINT), + or environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT), "insecure": insecure, "credentials": credentials, "headers": headers - or environ.get(OTEL_EXPORTER_OTLP_SPAN_HEADERS), + or environ.get(OTEL_EXPORTER_OTLP_TRACES_HEADERS), "timeout": timeout or environ_timeout, + "compression": compression, } ) @@ -274,7 +285,7 @@ def _translate_data( ].spans.append(CollectorSpan(**self._collector_span_kwargs)) return ExportTraceServiceRequest( - resource_spans=_get_resource_data( + resource_spans=get_resource_data( sdk_resource_instrumentation_library_spans, ResourceSpans, "spans", diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_exporter_mixin.py new file mode 100644 index 0000000000..e9c9e3f9d2 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_exporter_mixin.py @@ -0,0 +1,47 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase +from unittest.mock import patch + +from grpc import Compression + +from opentelemetry.exporter.otlp.exporter import ( + InvalidCompressionValueException, + environ_to_compression, +) + + +class TestOTLPExporterMixin(TestCase): + def test_environ_to_compression(self): + with patch.dict( + "os.environ", + { + "test_gzip": "gzip", + "test_gzip_caseinsensitive_with_whitespace": " GzIp ", + "test_invalid": "some invalid compression", + }, + ): + self.assertEqual( + environ_to_compression("test_gzip"), Compression.Gzip + ) + self.assertEqual( + environ_to_compression( + "test_gzip_caseinsensitive_with_whitespace" + ), + Compression.Gzip, + ) + self.assertIsNone(environ_to_compression("missing_key"),) + with self.assertRaises(InvalidCompressionValueException): + environ_to_compression("test_invalid") diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index be35f30623..254f7dc315 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -20,7 +20,7 @@ from google.protobuf.duration_pb2 import Duration from google.rpc.error_details_pb2 import RetryInfo -from grpc import ChannelCredentials, StatusCode, server +from grpc import ChannelCredentials, Compression, StatusCode, server from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( @@ -46,10 +46,12 @@ from opentelemetry.proto.trace.v1.trace_pb2 import Span as OTLPSpan from opentelemetry.proto.trace.v1.trace_pb2 import Status from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE, - OTEL_EXPORTER_OTLP_SPAN_ENDPOINT, - OTEL_EXPORTER_OTLP_SPAN_HEADERS, - OTEL_EXPORTER_OTLP_SPAN_TIMEOUT, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.trace import Status as SDKStatus @@ -169,11 +171,12 @@ def tearDown(self): @patch.dict( "os.environ", { - OTEL_EXPORTER_OTLP_SPAN_ENDPOINT: "collector:4317", - OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE: THIS_DIR + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "collector:4317", + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: THIS_DIR + "/fixtures/test.cert", - OTEL_EXPORTER_OTLP_SPAN_HEADERS: "key1=value1,key2=value2", - OTEL_EXPORTER_OTLP_SPAN_TIMEOUT: "10", + OTEL_EXPORTER_OTLP_TRACES_HEADERS: "key1=value1,key2=value2", + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "10", + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip", }, ) @patch("opentelemetry.exporter.otlp.exporter.OTLPExporterMixin.__init__") @@ -186,6 +189,7 @@ def test_env_variables(self, mock_exporter_mixin): self.assertEqual(kwargs["endpoint"], "collector:4317") self.assertEqual(kwargs["headers"], "key1=value1,key2=value2") self.assertEqual(kwargs["timeout"], 10) + self.assertEqual(kwargs["compression"], Compression.Gzip) self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) @@ -201,7 +205,7 @@ def test_no_credentials_error( @patch.dict( "os.environ", - {OTEL_EXPORTER_OTLP_SPAN_HEADERS: "key1=value1,key2=value2"}, + {OTEL_EXPORTER_OTLP_TRACES_HEADERS: "key1=value1,key2=value2"}, ) @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") @patch("opentelemetry.exporter.otlp.exporter.secure_channel") @@ -220,6 +224,56 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): exporter._headers, (("key3", "value3"), ("key4", "value4")) ) + # pylint: disable=no-self-use + @patch("opentelemetry.exporter.otlp.exporter.insecure_channel") + @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) + def test_otlp_exporter_otlp_compression_envvar( + self, mock_insecure_channel + ): + """Just OTEL_EXPORTER_OTLP_COMPRESSION should work""" + OTLPSpanExporter(insecure=True) + mock_insecure_channel.assert_called_once_with( + "localhost:4317", compression=Compression.Gzip + ) + + # pylint: disable=no-self-use + @patch("opentelemetry.exporter.otlp.exporter.insecure_channel") + @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) + def test_otlp_exporter_otlp_compression_kwarg(self, mock_insecure_channel): + """Specifying kwarg should take precedence over env""" + OTLPSpanExporter(insecure=True, compression=Compression.NoCompression) + mock_insecure_channel.assert_called_once_with( + "localhost:4317", compression=Compression.NoCompression + ) + + # pylint: disable=no-self-use + @patch("opentelemetry.exporter.otlp.exporter.insecure_channel") + @patch.dict("os.environ", {}) + def test_otlp_exporter_otlp_compression_unspecified( + self, mock_insecure_channel + ): + """No env or kwarg should be NoCompression""" + OTLPSpanExporter(insecure=True) + mock_insecure_channel.assert_called_once_with( + "localhost:4317", compression=Compression.NoCompression + ) + + # pylint: disable=no-self-use + @patch("opentelemetry.exporter.otlp.exporter.insecure_channel") + @patch.dict( + "os.environ", {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip"}, + ) + def test_otlp_exporter_otlp_compression_precendence( + self, mock_insecure_channel + ): + """OTEL_EXPORTER_OTLP_TRACES_COMPRESSION as higher priority than + OTEL_EXPORTER_OTLP_COMPRESSION + """ + OTLPSpanExporter(insecure=True) + mock_insecure_channel.assert_called_once_with( + "localhost:4317", compression=Compression.Gzip + ) + @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") @patch("opentelemetry.exporter.otlp.exporter.secure_channel") # pylint: disable=unused-argument diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 283aaae470..7dbf032830 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -36,16 +36,16 @@ OTEL_EXPORTER_OTLP_COMPRESSION = "OTEL_EXPORTER_OTLP_COMPRESSION" OTEL_EXPORTER_OTLP_TIMEOUT = "OTEL_EXPORTER_OTLP_TIMEOUT" OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT" -OTEL_EXPORTER_OTLP_SPAN_ENDPOINT = "OTEL_EXPORTER_OTLP_SPAN_ENDPOINT" -OTEL_EXPORTER_OTLP_SPAN_PROTOCOL = "OTEL_EXPORTER_OTLP_SPAN_PROTOCOL" -OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE = "OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE" -OTEL_EXPORTER_OTLP_SPAN_HEADERS = "OTEL_EXPORTER_OTLP_SPAN_HEADERS" -OTEL_EXPORTER_OTLP_SPAN_COMPRESSION = "OTEL_EXPORTER_OTLP_SPAN_COMPRESSION" -OTEL_EXPORTER_OTLP_SPAN_TIMEOUT = "OTEL_EXPORTER_OTLP_SPAN_TIMEOUT" +OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" +OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" +OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE = "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE" +OTEL_EXPORTER_OTLP_TRACES_HEADERS = "OTEL_EXPORTER_OTLP_TRACES_HEADERS" +OTEL_EXPORTER_OTLP_TRACES_COMPRESSION = "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION" +OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT" OTEL_EXPORTER_JAEGER_INSECURE = "OTEL_EXPORTER_JAEGER_INSECURE" OTEL_EXPORTER_JAEGER_CERTIFICATE = "OTEL_EXPORTER_JAEGER_CERTIFICATE" OTEL_EXPORTER_OTLP_INSECURE = "OTEL_EXPORTER_OTLP_INSECURE" -OTEL_EXPORTER_OTLP_SPAN_INSECURE = "OTEL_EXPORTER_OTLP_SPAN_INSECURE" +OTEL_EXPORTER_OTLP_TRACES_INSECURE = "OTEL_EXPORTER_OTLP_TRACES_INSECURE" OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES = ( "OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES" ) From a46b4c2039275b4c480b3ef3a3ca530fd443c0a2 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 11 Mar 2021 18:55:08 -0800 Subject: [PATCH 0810/1517] Update docs to specify to use Resource APIs to work with Resource (#1686) --- docs/examples/basic_tracer/resources.py | 1 + .../src/opentelemetry/sdk/resources/__init__.py | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/examples/basic_tracer/resources.py b/docs/examples/basic_tracer/resources.py index d69b689aa6..0f07c42b3e 100644 --- a/docs/examples/basic_tracer/resources.py +++ b/docs/examples/basic_tracer/resources.py @@ -20,6 +20,7 @@ SimpleSpanProcessor, ) +# Use Resource.create() instead of constructor directly resource = Resource.create({"service.name": "basic_service"}) trace.set_tracer_provider(TracerProvider(resource=resource)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 637e71166b..5cb17e2f1f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -23,11 +23,13 @@ these attributes can be included in the Resource.* Resource objects are created with `Resource.create`, which accepts attributes -(key-values). Resource attributes can also be passed at process invocation in -the :envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable. You should -register your resource with the `opentelemetry.sdk.trace.TracerProvider` by -passing them into their constructors. The `Resource` passed to a provider is -available to the exporter, which can send on this information as it sees fit. +(key-values). Resources should NOT be created via constructor, and working with +`Resource` objects should only be done via the Resource API methods. Resource +attributes can also be passed at process invocation in the +:envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable. You should register +your resource with the `opentelemetry.sdk.trace.TracerProvider` by passing +them into their constructors. The `Resource` passed to a provider is available +to the exporter, which can send on this information as it sees fit. .. code-block:: python From 826a79055753c81bbb8dc4b42492d00a3ae8e6e1 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 15 Mar 2021 09:22:09 -0700 Subject: [PATCH 0811/1517] add otlp grpc exporter test (#1692) --- .../tests/docker-compose.yml | 4 ++ .../test_otlp_grpc_exporter_functional.py | 56 +++++++++++++++++++ tox.ini | 4 +- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py diff --git a/tests/opentelemetry-docker-tests/tests/docker-compose.yml b/tests/opentelemetry-docker-tests/tests/docker-compose.yml index c731f83da1..c0566a6f00 100644 --- a/tests/opentelemetry-docker-tests/tests/docker-compose.yml +++ b/tests/opentelemetry-docker-tests/tests/docker-compose.yml @@ -7,3 +7,7 @@ services: ports: - "8888:8888" - "55678:55678" + otcollector: + image: otel/opentelemetry-collector:0.22.0 + ports: + - "4317:4317" \ No newline at end of file diff --git a/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py new file mode 100644 index 0000000000..b0992f42d8 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py @@ -0,0 +1,56 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry import trace +from opentelemetry.context import attach, detach, set_value +from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.test.test_base import TestBase + + +class ExportStatusSpanProcessor(SimpleSpanProcessor): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.export_status = [] + + def on_end(self, span): + token = attach(set_value("suppress_instrumentation", True)) + self.export_status.append(self.span_exporter.export((span,))) + detach(token) + + +class TestOTLPExporter(TestBase): + def setUp(self): + super().setUp() + + trace.set_tracer_provider(TracerProvider()) + self.tracer = trace.get_tracer(__name__) + self.span_processor = ExportStatusSpanProcessor( + OTLPSpanExporter(insecure=True, timeout=1) + ) + + trace.get_tracer_provider().add_span_processor(self.span_processor) + + def test_export(self): + with self.tracer.start_as_current_span("foo"): + with self.tracer.start_as_current_span("bar"): + with self.tracer.start_as_current_span("baz"): + pass + + self.assertTrue(len(self.span_processor.export_status), 3) + + for export_status in self.span_processor.export_status: + self.assertEqual(export_status.name, "SUCCESS") + self.assertEqual(export_status.value, 0) diff --git a/tox.ini b/tox.ini index 9f30b40831..c1f7e7aca2 100644 --- a/tox.ini +++ b/tox.ini @@ -214,7 +214,9 @@ commands_pre = -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/tests/util \ - -e {toxinidir}/exporter/opentelemetry-exporter-opencensus + -e {toxinidir}/exporter/opentelemetry-exporter-opencensus \ + -e {toxinidir}/opentelemetry-proto \ + -e {toxinidir}/exporter/opentelemetry-exporter-otlp docker-compose up -d commands = pytest {posargs} From 7662d83f3b8b64e208909f8b1acb97b45b14ce00 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 15 Mar 2021 13:53:01 -0400 Subject: [PATCH 0812/1517] Improve documentation around environment variables (#1680) --- CHANGELOG.md | 2 + .../opentelemetry/exporter/jaeger/__init__.py | 10 + .../opentelemetry/exporter/otlp/__init__.py | 26 ++- .../opentelemetry/exporter/zipkin/__init__.py | 6 +- .../environment_variables/__init__.py | 27 +++ .../sdk/environment_variables/__init__.py | 182 +++++++++++++++++- .../opentelemetry/sdk/resources/__init__.py | 23 --- .../sdk/trace/export/__init__.py | 10 +- 8 files changed, 244 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0d6cbbaae..d0bc8feaca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1684](https://github.com/open-telemetry/opentelemetry-python/pull/1684)) - Cleanup OTLP exporter compression options, add tests ([#1671](https://github.com/open-telemetry/opentelemetry-python/pull/1671)) +- Initial documentation for environment variables + ([#1680](https://github.com/open-telemetry/opentelemetry-python/pull/1680)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index 80c5f04317..5730c7054a 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -59,6 +59,16 @@ with tracer.start_as_current_span('foo'): print('Hello world!') +You can configure the exporter with the following environment variables: + +- :envvar:`OTEL_EXPORTER_JAEGER_USER` +- :envvar:`OTEL_EXPORTER_JAEGER_PASSWORD` +- :envvar:`OTEL_EXPORTER_JAEGER_INSECURE` +- :envvar:`OTEL_EXPORTER_JAEGER_ENDPOINT` +- :envvar:`OTEL_EXPORTER_JAEGER_CERTIFICATE` +- :envvar:`OTEL_EXPORTER_JAEGER_AGENT_PORT` +- :envvar:`OTEL_EXPORTER_JAEGER_AGENT_HOST` + API --- .. _Jaeger: https://www.jaegertracing.io/ diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py index b1db06b295..d7de7a979a 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py @@ -22,20 +22,26 @@ The **OTLP Span Exporter** allows to export `OpenTelemetry`_ traces to the `OTLP`_ collector. +You can configure the exporter with the following environment variables: + +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_INSECURE` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_HEADERS` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_COMPRESSION` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE` +- :envvar:`OTEL_EXPORTER_OTLP_TIMEOUT` +- :envvar:`OTEL_EXPORTER_OTLP_PROTOCOL` +- :envvar:`OTEL_EXPORTER_OTLP_INSECURE` +- :envvar:`OTEL_EXPORTER_OTLP_HEADERS` +- :envvar:`OTEL_EXPORTER_OTLP_ENDPOINT` +- :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` +- :envvar:`OTEL_EXPORTER_OTLP_CERTIFICATE` .. _OTLP: https://github.com/open-telemetry/opentelemetry-collector/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ -.. envvar:: OTEL_EXPORTER_OTLP_COMPRESSION - -The :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` environment variable allows a -compression algorithm to be passed to the OTLP exporter. The compression -algorithms that are supported include gzip and no compression. The value should -be in the format of a string "gzip" for gzip compression, and no value specified -if no compression is the desired choice. -Additional details are available `in the specification -`_. - .. code:: python from opentelemetry import trace diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 6c8fea5ea0..e9c7a693d9 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -27,8 +27,6 @@ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. _Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#zipkin-exporter -.. envvar:: OTEL_EXPORTER_ZIPKIN_ENDPOINT - .. code:: python from opentelemetry import trace @@ -61,9 +59,7 @@ The exporter supports the following environment variable for configuration: -:envvar:`OTEL_EXPORTER_ZIPKIN_ENDPOINT`: zipkin collector endpoint to which the -exporter will send data. This may include a path (e.g. -http://example.com:9411/api/v2/spans). +- :envvar:`OTEL_EXPORTER_ZIPKIN_ENDPOINT` API --- diff --git a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py index 21d84d0f85..299232fb77 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py @@ -13,9 +13,36 @@ # limitations under the License. OTEL_PROPAGATORS = "OTEL_PROPAGATORS" +""" +.. envvar:: OTEL_PROPAGATORS +""" + OTEL_PYTHON_CONTEXT = "OTEL_PYTHON_CONTEXT" +""" +.. envvar:: OTEL_PYTHON_CONTEXT +""" + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS" +""" +.. envvar:: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS +""" + OTEL_PYTHON_ID_GENERATOR = "OTEL_PYTHON_ID_GENERATOR" +""" +.. envvar:: OTEL_PYTHON_ID_GENERATOR +""" + OTEL_PYTHON_SERVICE_NAME = "OTEL_PYTHON_SERVICE_NAME" +""" +.. envvar:: OTEL_PYTHON_SERVICE_NAME +""" + OTEL_TRACES_EXPORTER = "OTEL_TRACES_EXPORTER" +""" +.. envvar:: OTEL_TRACES_EXPORTER +""" + OTEL_PYTHON_TRACER_PROVIDER = "OTEL_PYTHON_TRACER_PROVIDER" +""" +.. envvar:: OTEL_PYTHON_TRACER_PROVIDER +""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 7dbf032830..6ff1dee222 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -13,39 +13,215 @@ # limitations under the License. OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES" +""" +.. envvar:: OTEL_RESOURCE_ATTRIBUTES + +The :envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable allows resource +attributes to be passed to the SDK at process invocation. The attributes from +:envvar:`OTEL_RESOURCE_ATTRIBUTES` are merged with those passed to +`Resource.create`, meaning :envvar:`OTEL_RESOURCE_ATTRIBUTES` takes *lower* +priority. Attributes should be in the format ``key1=value1,key2=value2``. +Additional details are available `in the specification +`__. + +.. code-block:: console + + $ OTEL_RESOURCE_ATTRIBUTES="service.name=shoppingcard,will_be_overridden=foo" python - <`__. +""" + OTEL_EXPORTER_OTLP_TIMEOUT = "OTEL_EXPORTER_OTLP_TIMEOUT" +""" +.. envvar:: OTEL_EXPORTER_OTLP_TIMEOUT +""" + OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT" +""" +.. envvar:: OTEL_EXPORTER_OTLP_ENDPOINT +""" + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" +""" +.. envvar:: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT +""" + OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" +""" +.. envvar:: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL +""" + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE = "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE" +""" +.. envvar:: OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE +""" + OTEL_EXPORTER_OTLP_TRACES_HEADERS = "OTEL_EXPORTER_OTLP_TRACES_HEADERS" +""" +.. envvar:: OTEL_EXPORTER_OTLP_TRACES_HEADERS +""" + + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION = "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION" +""" +.. envvar:: OTEL_EXPORTER_OTLP_TRACES_COMPRESSION + +Same as :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` but only for the span +exporter. If both are present, this takes higher precendence. +""" + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT" +""" +.. envvar:: OTEL_EXPORTER_OTLP_TRACES_TIMEOUT +""" + OTEL_EXPORTER_JAEGER_INSECURE = "OTEL_EXPORTER_JAEGER_INSECURE" +""" +.. envvar:: OTEL_EXPORTER_JAEGER_INSECURE +""" + OTEL_EXPORTER_JAEGER_CERTIFICATE = "OTEL_EXPORTER_JAEGER_CERTIFICATE" -OTEL_EXPORTER_OTLP_INSECURE = "OTEL_EXPORTER_OTLP_INSECURE" -OTEL_EXPORTER_OTLP_TRACES_INSECURE = "OTEL_EXPORTER_OTLP_TRACES_INSECURE" +""" +.. envvar:: OTEL_EXPORTER_JAEGER_CERTIFICATE +""" + OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES = ( "OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES" ) +""" +.. envvar:: OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES +""" + +OTEL_EXPORTER_OTLP_INSECURE = "OTEL_EXPORTER_OTLP_INSECURE" +""" +.. envvar:: OTEL_EXPORTER_OTLP_INSECURE +""" + +OTEL_EXPORTER_OTLP_TRACES_INSECURE = "OTEL_EXPORTER_OTLP_TRACES_INSECURE" +""" +.. envvar:: OTEL_EXPORTER_OTLP_TRACES_INSECURE +""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 5cb17e2f1f..6234891782 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -53,29 +53,6 @@ `_ that have prescribed semantic meanings, for example ``service.name`` in the above example. - -.. envvar:: OTEL_RESOURCE_ATTRIBUTES - -The :envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable allows resource -attributes to be passed to the SDK at process invocation. The attributes from -:envvar:`OTEL_RESOURCE_ATTRIBUTES` are merged with those passed to -`Resource.create`, meaning :envvar:`OTEL_RESOURCE_ATTRIBUTES` takes *lower* -priority. Attributes should be in the format ``key1=value1,key2=value2``. -Additional details are available `in the specification -`_. - -.. code-block:: console - - $ OTEL_RESOURCE_ATTRIBUTES="service.name=shoppingcard,will_be_overridden=foo" python - < Date: Tue, 16 Mar 2021 08:50:36 -0700 Subject: [PATCH 0813/1517] don't copy baggage for each call to get (#1697) --- opentelemetry-api/src/opentelemetry/baggage/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/baggage/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/__init__.py index cdb2196f74..da78d92417 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/__init__.py @@ -34,7 +34,7 @@ def get_all( """ baggage = get_value(_BAGGAGE_KEY, context=context) if isinstance(baggage, dict): - return MappingProxyType(baggage.copy()) + return MappingProxyType(baggage) return MappingProxyType({}) From 3e2bf0e4fdef04ab870f18513931ff9a2000ea79 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 16 Mar 2021 22:15:12 +0530 Subject: [PATCH 0814/1517] Remove insecure param from exporters constructor and OTEL_EXPORTER_*_ INSECURE env var (#1682) --- CHANGELOG.md | 4 +- .../opentelemetry/exporter/jaeger/__init__.py | 3 +- .../src/opentelemetry/exporter/jaeger/util.py | 12 ----- .../opentelemetry/exporter/otlp/__init__.py | 2 - .../opentelemetry/exporter/otlp/exporter.py | 46 ++++++++----------- .../exporter/otlp/trace_exporter/__init__.py | 10 ++-- .../sdk/environment_variables/__init__.py | 15 ------ 7 files changed, 25 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0bc8feaca..15bae8031f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,7 +57,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. - ([#1675])(https://github.com/open-telemetry/opentelemetry-python/pull/1675) + ([#1675](https://github.com/open-telemetry/opentelemetry-python/pull/1675)) +- Remove `OTEL_EXPORTER_*_ INSECURE` env var + ([#1682](https://github.com/open-telemetry/opentelemetry-python/pull/1682)) ## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index 5730c7054a..84b2f7502c 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -63,7 +63,6 @@ - :envvar:`OTEL_EXPORTER_JAEGER_USER` - :envvar:`OTEL_EXPORTER_JAEGER_PASSWORD` -- :envvar:`OTEL_EXPORTER_JAEGER_INSECURE` - :envvar:`OTEL_EXPORTER_JAEGER_ENDPOINT` - :envvar:`OTEL_EXPORTER_JAEGER_CERTIFICATE` - :envvar:`OTEL_EXPORTER_JAEGER_AGENT_PORT` @@ -197,7 +196,7 @@ def __init__( ) self._collector = None self._grpc_client = None - self.insecure = util._get_insecure(insecure) + self.insecure = insecure self.credentials = util._get_credentials(credentials) self.transport_format = ( transport_format.lower() diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py index 4a72c1817c..4f943a0e4c 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py @@ -19,22 +19,10 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_JAEGER_CERTIFICATE, - OTEL_EXPORTER_JAEGER_INSECURE, ) logger = logging.getLogger(__name__) -DEFAULT_INSECURE = False - - -def _get_insecure(param): - if param is not None: - return param - insecure_env = environ.get(OTEL_EXPORTER_JAEGER_INSECURE) - if insecure_env is not None: - return insecure_env.lower() == "true" - return DEFAULT_INSECURE - def _load_credential_from_file(path) -> ChannelCredentials: try: diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py index d7de7a979a..705f14d7c2 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py @@ -26,14 +26,12 @@ - :envvar:`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` -- :envvar:`OTEL_EXPORTER_OTLP_TRACES_INSECURE` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_HEADERS` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_COMPRESSION` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE` - :envvar:`OTEL_EXPORTER_OTLP_TIMEOUT` - :envvar:`OTEL_EXPORTER_OTLP_PROTOCOL` -- :envvar:`OTEL_EXPORTER_OTLP_INSECURE` - :envvar:`OTEL_EXPORTER_OTLP_HEADERS` - :envvar:`OTEL_EXPORTER_OTLP_ENDPOINT` - :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py index f5da0609f3..e3a42a24c5 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -42,7 +42,6 @@ OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, - OTEL_EXPORTER_OTLP_INSECURE, OTEL_EXPORTER_OTLP_TIMEOUT, ) from opentelemetry.sdk.resources import Resource as SDKResource @@ -159,6 +158,15 @@ def _load_credential_from_file(filepath) -> ChannelCredentials: return None +def _get_credentials(creds, environ_key): + if creds is not None: + return creds + creds_env = environ.get(environ_key) + if creds_env: + return _load_credential_from_file(creds_env) + return ssl_channel_credentials() + + # pylint: disable=no-member class OTLPExporterMixin( ABC, Generic[SDKDataT, ExportServiceRequestT, ExportResultT] @@ -185,26 +193,17 @@ def __init__( ): super().__init__() - endpoint = ( - endpoint - or environ.get(OTEL_EXPORTER_OTLP_ENDPOINT) - or "localhost:4317" + endpoint = endpoint or environ.get( + OTEL_EXPORTER_OTLP_ENDPOINT, "localhost:4317" ) - if insecure is None: - insecure = environ.get(OTEL_EXPORTER_OTLP_INSECURE) - if insecure is None: - insecure = False - self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS) if isinstance(self._headers, str): self._headers = tuple( tuple(item.split("=")) for item in self._headers.split(",") ) - self._timeout = ( - timeout - or int(environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 0)) - or 10 # default: 10 seconds + self._timeout = timeout or int( + environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 10) ) self._collector_span_kwargs = None @@ -218,22 +217,13 @@ def __init__( self._client = self._stub( insecure_channel(endpoint, compression=compression) ) - return - - # secure mode - if ( - credentials is None - and environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE) is None - ): - # use the default location chosen by gRPC runtime - credentials = ssl_channel_credentials() else: - credentials = credentials or _load_credential_from_file( - environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE) + credentials = _get_credentials( + credentials, OTEL_EXPORTER_OTLP_CERTIFICATE + ) + self._client = self._stub( + secure_channel(endpoint, credentials, compression=compression) ) - self._client = self._stub( - secure_channel(endpoint, credentials, compression=compression) - ) @abstractmethod def _translate_data( diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index bbc43956cb..a8b3cc79b4 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -21,7 +21,7 @@ from opentelemetry.exporter.otlp.exporter import ( OTLPExporterMixin, - _load_credential_from_file, + _get_credentials, _translate_key_values, environ_to_compression, get_resource_data, @@ -44,7 +44,6 @@ OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, - OTEL_EXPORTER_OTLP_TRACES_INSECURE, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) from opentelemetry.sdk.trace import Span as ReadableSpan @@ -85,15 +84,12 @@ def __init__( timeout: Optional[int] = None, compression: Optional[Compression] = None, ): - if insecure is None: - insecure = environ.get(OTEL_EXPORTER_OTLP_TRACES_INSECURE) - if ( not insecure and environ.get(OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE) is not None ): - credentials = credentials or _load_credential_from_file( - environ.get(OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE) + credentials = _get_credentials( + credentials, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE ) environ_timeout = environ.get(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 6ff1dee222..c3329caf76 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -199,11 +199,6 @@ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_TIMEOUT """ -OTEL_EXPORTER_JAEGER_INSECURE = "OTEL_EXPORTER_JAEGER_INSECURE" -""" -.. envvar:: OTEL_EXPORTER_JAEGER_INSECURE -""" - OTEL_EXPORTER_JAEGER_CERTIFICATE = "OTEL_EXPORTER_JAEGER_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_JAEGER_CERTIFICATE @@ -215,13 +210,3 @@ """ .. envvar:: OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES """ - -OTEL_EXPORTER_OTLP_INSECURE = "OTEL_EXPORTER_OTLP_INSECURE" -""" -.. envvar:: OTEL_EXPORTER_OTLP_INSECURE -""" - -OTEL_EXPORTER_OTLP_TRACES_INSECURE = "OTEL_EXPORTER_OTLP_TRACES_INSECURE" -""" -.. envvar:: OTEL_EXPORTER_OTLP_TRACES_INSECURE -""" From 75c7371c5870bc322b4e473feca97754821ad2c1 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 16 Mar 2021 10:49:49 -0700 Subject: [PATCH 0815/1517] Zipkin exporter to get service_name from span instead of global tracer_provider (#1696) --- CHANGELOG.md | 2 ++ .../opentelemetry/exporter/zipkin/__init__.py | 16 ++++++++++- .../exporter/zipkin/encoder/__init__.py | 18 +++++++++--- .../tests/test_zipkin_exporter.py | 28 +++++++++++++++++-- 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15bae8031f..153bc2576d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1671](https://github.com/open-telemetry/opentelemetry-python/pull/1671)) - Initial documentation for environment variables ([#1680](https://github.com/open-telemetry/opentelemetry-python/pull/1680)) +- Change Zipkin exporter to obtain service.name from span + ([#1696](https://github.com/open-telemetry/opentelemetry-python/pull/1696)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index e9c7a693d9..afb29b239f 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -71,7 +71,11 @@ import requests -from opentelemetry.exporter.zipkin.encoder import Encoder, Protocol +from opentelemetry.exporter.zipkin.encoder import ( + DEFAULT_MAX_TAG_VALUE_LENGTH, + Encoder, + Protocol, +) from opentelemetry.exporter.zipkin.encoder.v1.json import JsonV1Encoder from opentelemetry.exporter.zipkin.encoder.v2.json import JsonV2Encoder from opentelemetry.exporter.zipkin.encoder.v2.protobuf import ProtobufEncoder @@ -79,6 +83,7 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_ZIPKIN_ENDPOINT, ) +from opentelemetry.sdk.resources import SERVICE_NAME from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace import Span @@ -116,6 +121,15 @@ def __init__( self.encoder = ProtobufEncoder(max_tag_value_length) def export(self, spans: Sequence[Span]) -> SpanExportResult: + # Populate service_name from first span + # We restrict any SpanProcessor to be only associated with a single + # TracerProvider, so it is safe to assume that all Spans in a single + # batch all originate from one TracerProvider (and in turn have all + # the same service.name) + if spans: + service_name = spans[0].resource.attributes.get(SERVICE_NAME) + if service_name: + self.local_node.service_name = service_name result = requests.post( url=self.endpoint, data=self.encoder.serialize(spans, self.local_node), diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py index f1c30410ce..8705a7bc37 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py @@ -144,20 +144,26 @@ def _extract_tags_from_dict( logger.warning("Could not serialize tag %s", attribute_key) continue - if self.max_tag_value_length > 0: + if ( + self.max_tag_value_length is not None + and self.max_tag_value_length > 0 + ): value = value[: self.max_tag_value_length] tags[attribute_key] = value return tags def _extract_tag_value_string_from_sequence(self, sequence: Sequence): - if self.max_tag_value_length == 1: + if self.max_tag_value_length and self.max_tag_value_length == 1: return None tag_value_elements = [] running_string_length = ( 2 # accounts for array brackets in output string ) - defined_max_tag_value_length = self.max_tag_value_length > 0 + defined_max_tag_value_length = ( + self.max_tag_value_length is not None + and self.max_tag_value_length > 0 + ) for element in sequence: if isinstance(element, bool): @@ -214,7 +220,11 @@ def _extract_annotations_from_events( for event in events: attrs = {} for key, value in event.attributes.items(): - if isinstance(value, str) and self.max_tag_value_length > 0: + if ( + isinstance(value, str) + and self.max_tag_value_length is not None + and self.max_tag_value_length > 0 + ): value = value[: self.max_tag_value_length] attrs[key] = value diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 93f4af8e45..23da474730 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -26,7 +26,7 @@ OTEL_EXPORTER_ZIPKIN_ENDPOINT, ) from opentelemetry.sdk.resources import SERVICE_NAME, Resource -from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace import TracerProvider, _Span from opentelemetry.sdk.trace.export import SpanExportResult TEST_SERVICE_NAME = "test_service" @@ -122,13 +122,37 @@ def test_constructor_all_params_and_env_vars(self): self.assertEqual(exporter.local_node.port, local_node_port) @patch("requests.post") - def test_invalid_response(self, mock_post): + def test_export_success(self, mock_post): + mock_post.return_value = MockResponse(200) + spans = [] + exporter = ZipkinExporter(Protocol.V2_PROTOBUF) + status = exporter.export(spans) + self.assertEqual(SpanExportResult.SUCCESS, status) + + @patch("requests.post") + def test_export_invalid_response(self, mock_post): mock_post.return_value = MockResponse(404) spans = [] exporter = ZipkinExporter(Protocol.V2_PROTOBUF) status = exporter.export(spans) self.assertEqual(SpanExportResult.FAILURE, status) + @patch("requests.post") + def test_export_span_service_name(self, mock_post): + mock_post.return_value = MockResponse(200) + resource = Resource.create({SERVICE_NAME: "test"}) + context = trace.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + ) + span = _Span("test_span", context=context, resource=resource) + span.start() + span.end() + exporter = ZipkinExporter(Protocol.V2_PROTOBUF) + exporter.export([span]) + self.assertEqual(exporter.local_node.service_name, "test") + class TestZipkinNodeEndpoint(unittest.TestCase): def test_constructor_default(self): From c61a7126445f57748af8f7ffd1b7fc0cdadd5476 Mon Sep 17 00:00:00 2001 From: Dilip M <55284676+dmarar@users.noreply.github.com> Date: Wed, 17 Mar 2021 00:45:48 +0530 Subject: [PATCH 0816/1517] Remove service name (#1669) --- CHANGELOG.md | 3 + .../opencensus-exporter-tracer/collector.py | 4 +- docs/getting_started/jaeger_example.py | 11 ++-- .../examples/jaeger_exporter_example.py | 2 - .../opentelemetry/exporter/jaeger/__init__.py | 14 +++-- .../tests/test_jaeger_exporter_protobuf.py | 14 ++--- .../tests/test_jaeger_exporter_thrift.py | 58 ++++++++++++++++--- .../opencensus/trace_exporter/__init__.py | 15 +++-- .../tests/test_otcollector_trace_exporter.py | 16 +++-- .../environment_variables/__init__.py | 5 -- .../src/opentelemetry/distro/__init__.py | 23 ++------ .../tests/test_configurator.py | 25 +++++--- opentelemetry-instrumentation/README.rst | 4 -- .../auto_instrumentation/__init__.py | 3 - .../tests/test_run.py | 17 +----- .../test_opencensusexporter_functional.py | 4 +- 16 files changed, 119 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 153bc2576d..b87debc484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1500](https://github.com/open-telemetry/opentelemetry-python/pull/1500)) ### Changed +- remove `service_name` from constructor of jaeger and opencensus exporters and + use of env variable `OTEL_PYTHON_SERVICE_NAME` + ([#1669])(https://github.com/open-telemetry/opentelemetry-python/pull/1669) - Rename `IdsGenerator` to `IdGenerator` ([#1651](https://github.com/open-telemetry/opentelemetry-python/pull/1651)) - Make TracerProvider's resource attribute private diff --git a/docs/examples/opencensus-exporter-tracer/collector.py b/docs/examples/opencensus-exporter-tracer/collector.py index fdc45d7162..5c98cc4ce9 100644 --- a/docs/examples/opencensus-exporter-tracer/collector.py +++ b/docs/examples/opencensus-exporter-tracer/collector.py @@ -21,9 +21,7 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor -exporter = OpenCensusSpanExporter( - service_name="basic-service", endpoint="localhost:55678" -) +exporter = OpenCensusSpanExporter(endpoint="localhost:55678") trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) diff --git a/docs/getting_started/jaeger_example.py b/docs/getting_started/jaeger_example.py index 79b22c5e77..6733741483 100644 --- a/docs/getting_started/jaeger_example.py +++ b/docs/getting_started/jaeger_example.py @@ -15,15 +15,18 @@ # jaeger_example.py from opentelemetry import trace from opentelemetry.exporter import jaeger +from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor -trace.set_tracer_provider(TracerProvider()) +trace.set_tracer_provider( + TracerProvider( + resource=Resource.create({SERVICE_NAME: "my-helloworld-service"}) + ) +) jaeger_exporter = jaeger.JaegerExporter( - service_name="my-helloworld-service", - agent_host_name="localhost", - agent_port=6831, + agent_host_name="localhost", agent_port=6831, ) trace.get_tracer_provider().add_span_processor( diff --git a/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py b/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py index 8123963ce7..35e83f18f3 100644 --- a/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py +++ b/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py @@ -10,7 +10,6 @@ # create a JaegerExporter jaeger_exporter = jaeger.JaegerExporter( - service_name="my-helloworld-service", # configure agent agent_host_name="localhost", agent_port=6831, @@ -29,7 +28,6 @@ # `EXPORTER_JAEGER_CERTIFICATE` with file containing creds. # jaeger_exporter = jaeger.JaegerExporter( -# service_name="my-helloworld-service", # collector_endpoint="localhost:14250", # insecure=True, # transport_format="protobuf", diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index 84b2f7502c..6634fcc6e8 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -36,7 +36,6 @@ # create a JaegerExporter jaeger_exporter = jaeger.JaegerExporter( - service_name='my-helloworld-service', # configure agent agent_host_name='localhost', agent_port=6831, @@ -81,6 +80,7 @@ from grpc import ChannelCredentials, insecure_channel, secure_channel +from opentelemetry import trace from opentelemetry.exporter.jaeger import util from opentelemetry.exporter.jaeger.gen import model_pb2 from opentelemetry.exporter.jaeger.gen.collector_pb2 import PostSpansRequest @@ -100,6 +100,7 @@ OTEL_EXPORTER_JAEGER_PASSWORD, OTEL_EXPORTER_JAEGER_USER, ) +from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult DEFAULT_AGENT_HOST_NAME = "localhost" @@ -118,8 +119,6 @@ class JaegerExporter(SpanExporter): """Jaeger span exporter for OpenTelemetry. Args: - service_name: Service that logged an annotation in a trace.Classifier - when query for spans. agent_host_name: The host name of the Jaeger-Agent. agent_port: The port of the Jaeger-Agent. collector_endpoint: The endpoint of the Jaeger collector that uses @@ -137,7 +136,6 @@ class JaegerExporter(SpanExporter): def __init__( self, - service_name: str, agent_host_name: Optional[str] = None, agent_port: Optional[int] = None, collector_endpoint: Optional[str] = None, @@ -149,7 +147,6 @@ def __init__( max_tag_value_length: Optional[int] = None, udp_split_oversized_batches: bool = None, ): - self.service_name = service_name self._max_tag_value_length = max_tag_value_length self.agent_host_name = _parameter_setter( param=agent_host_name, @@ -203,6 +200,12 @@ def __init__( if transport_format else TRANSPORT_FORMAT_THRIFT ) + tracer_provider = trace.get_tracer_provider() + self.service_name = ( + tracer_provider.resource.attributes[SERVICE_NAME] + if getattr(tracer_provider, "resource", None) + else Resource.create().attributes.get(SERVICE_NAME) + ) @property def _collector_grpc_client(self) -> Optional[CollectorServiceStub]: @@ -243,6 +246,7 @@ def _collector_http_client(self) -> Optional[Collector]: return self._collector def export(self, spans) -> SpanExportResult: + translator = Translate(spans) if self.transport_format == TRANSPORT_FORMAT_PROTOBUF: pb_translator = ProtobufTranslator( diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py index b127b7d4d4..2c2bef7b4f 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py @@ -32,8 +32,9 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_JAEGER_CERTIFICATE, OTEL_EXPORTER_JAEGER_ENDPOINT, + OTEL_RESOURCE_ATTRIBUTES, ) -from opentelemetry.sdk.trace import Resource +from opentelemetry.sdk.trace import Resource, TracerProvider from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace.status import Status, StatusCode @@ -51,6 +52,7 @@ def setUp(self): self._test_span = trace._Span("test_span", context=context) self._test_span.start() self._test_span.end() + # pylint: disable=protected-access def test_constructor_by_environment_variables(self): @@ -66,20 +68,18 @@ def test_constructor_by_environment_variables(self): OTEL_EXPORTER_JAEGER_ENDPOINT: collector_endpoint, OTEL_EXPORTER_JAEGER_CERTIFICATE: os.path.dirname(__file__) + "/certs/cred.cert", + OTEL_RESOURCE_ATTRIBUTES: "service.name=my-opentelemetry-jaeger", }, ) env_patch.start() - - exporter = JaegerExporter( - service_name=service, transport_format="protobuf" - ) - + provider = TracerProvider(resource=Resource.create({})) + trace_api.set_tracer_provider(provider) + exporter = JaegerExporter(transport_format="protobuf") self.assertEqual(exporter.service_name, service) self.assertIsNotNone(exporter._collector_grpc_client) self.assertEqual(exporter.collector_endpoint, collector_endpoint) self.assertIsNotNone(exporter.credentials) - env_patch.stop() # pylint: disable=too-many-locals,too-many-statements diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py index 561e30f92c..d777615739 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py @@ -15,6 +15,7 @@ import unittest from unittest import mock +from unittest.mock import patch # pylint:disable=no-name-in-module # pylint:disable=import-error @@ -31,7 +32,8 @@ OTEL_EXPORTER_JAEGER_PASSWORD, OTEL_EXPORTER_JAEGER_USER, ) -from opentelemetry.sdk.trace import Resource +from opentelemetry.sdk.resources import SERVICE_NAME +from opentelemetry.sdk.trace import Resource, TracerProvider from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanKind from opentelemetry.trace.status import Status, StatusCode @@ -51,14 +53,21 @@ def setUp(self): self._test_span.end() # pylint: disable=protected-access + @patch("opentelemetry.exporter.jaeger.trace._TRACER_PROVIDER", None) def test_constructor_default(self): # pylint: disable=protected-access """Test the default values assigned by constructor.""" service_name = "my-service-name" agent_host_name = "localhost" agent_port = 6831 - exporter = jaeger_exporter.JaegerExporter(service_name) + trace_api.set_tracer_provider( + TracerProvider( + resource=Resource.create({SERVICE_NAME: "my-service-name"}) + ) + ) + + exporter = jaeger_exporter.JaegerExporter() self.assertEqual(exporter.service_name, service_name) self.assertEqual(exporter.agent_host_name, agent_host_name) self.assertEqual(exporter.agent_port, agent_port) @@ -69,6 +78,7 @@ def test_constructor_default(self): self.assertTrue(exporter._agent_client is not None) self.assertIsNone(exporter._max_tag_value_length) + @patch("opentelemetry.exporter.jaeger.trace._TRACER_PROVIDER", None) def test_constructor_explicit(self): # pylint: disable=protected-access """Test the constructor passing all the options.""" @@ -81,9 +91,15 @@ def test_constructor_explicit(self): username = "username" password = "password" auth = (username, password) + trace_api.set_tracer_provider( + TracerProvider( + resource=Resource.create( + {SERVICE_NAME: "my-opentelemetry-jaeger"} + ) + ) + ) exporter = jaeger_exporter.JaegerExporter( - service_name=service, agent_host_name=agent_host_name, agent_port=agent_port, collector_endpoint=collector_endpoint, @@ -91,7 +107,6 @@ def test_constructor_explicit(self): password=password, max_tag_value_length=42, ) - self.assertEqual(exporter.service_name, service) self.assertEqual(exporter.agent_host_name, agent_host_name) self.assertEqual(exporter.agent_port, agent_port) @@ -108,6 +123,7 @@ def test_constructor_explicit(self): self.assertTrue(exporter._collector_http_client.auth is None) self.assertEqual(exporter._max_tag_value_length, 42) + @patch("opentelemetry.exporter.jaeger.trace._TRACER_PROVIDER", None) def test_constructor_by_environment_variables(self): # pylint: disable=protected-access """Test the constructor using Environment Variables.""" @@ -133,10 +149,16 @@ def test_constructor_by_environment_variables(self): }, ) - environ_patcher.start() - - exporter = jaeger_exporter.JaegerExporter(service_name=service) + trace_api.set_tracer_provider( + TracerProvider( + resource=Resource.create( + {SERVICE_NAME: "my-opentelemetry-jaeger"} + ) + ) + ) + environ_patcher.start() + exporter = jaeger_exporter.JaegerExporter() self.assertEqual(exporter.service_name, service) self.assertEqual(exporter.agent_host_name, agent_host_name) self.assertEqual(exporter.agent_port, int(agent_port)) @@ -152,9 +174,17 @@ def test_constructor_by_environment_variables(self): exporter.password = None self.assertNotEqual(exporter._collector_http_client, collector) self.assertTrue(exporter._collector_http_client.auth is None) - environ_patcher.stop() + @patch("opentelemetry.exporter.jaeger.trace._TRACER_PROVIDER", None) + def test_constructor_with_no_traceprovider_resource(self): + + """Test the constructor when there is no resource attached to trace_provider""" + + exporter = jaeger_exporter.JaegerExporter() + + self.assertEqual(exporter.service_name, "unknown_service") + def test_nsec_to_usec_round(self): # pylint: disable=protected-access nsec_to_usec_round = jaeger_exporter.translate._nsec_to_usec_round @@ -426,10 +456,19 @@ def test_translate_to_jaeger(self): self.assertEqual(spans, expected_spans) + @patch("opentelemetry.exporter.jaeger.trace._TRACER_PROVIDER", None) def test_export(self): + """Test that agent and/or collector are invoked""" + + trace_api.set_tracer_provider( + TracerProvider( + resource=Resource.create({SERVICE_NAME: "text_export"}) + ) + ) + exporter = jaeger_exporter.JaegerExporter( - "test_export", agent_host_name="localhost", agent_port=6318 + agent_host_name="localhost", agent_port=6318 ) # just agent is configured now @@ -448,6 +487,7 @@ def test_export(self): exporter.export((self._test_span,)) self.assertEqual(agent_client_mock.emit.call_count, 1) self.assertEqual(collector_mock.submit.call_count, 1) + # trace_api._TRACER_PROVIDER = None def test_agent_client(self): agent_client = jaeger_exporter.AgentClientUDP( diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py index 6a779c09fe..4bea1af156 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py @@ -25,6 +25,8 @@ from opencensus.proto.trace.v1 import trace_pb2 import opentelemetry.exporter.opencensus.util as utils +from opentelemetry import trace +from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult @@ -39,18 +41,19 @@ class OpenCensusSpanExporter(SpanExporter): Args: endpoint: OpenCensus Collector receiver endpoint. - service_name: Name of Collector service. host_name: Host name. client: TraceService client stub. """ def __init__( - self, - endpoint=DEFAULT_ENDPOINT, - service_name=None, - host_name=None, - client=None, + self, endpoint=DEFAULT_ENDPOINT, host_name=None, client=None, ): + tracer_provider = trace.get_tracer_provider() + service_name = ( + tracer_provider.resource.attributes[SERVICE_NAME] + if getattr(tracer_provider, "resource", None) + else Resource.create().attributes.get(SERVICE_NAME) + ) self.endpoint = endpoint if client is None: self.channel = grpc.insecure_channel(self.endpoint) diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py index 8fd2460de0..ab8a3a2eb6 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py @@ -26,6 +26,8 @@ translate_to_collector, ) from opentelemetry.sdk import trace +from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.trace import TraceFlags @@ -38,21 +40,23 @@ def test_constructor(self): "opentelemetry.exporter.opencensus.util.get_node", side_effect=mock_get_node, ) - service_name = "testServiceName" + trace_api.set_tracer_provider( + TracerProvider( + resource=Resource.create({SERVICE_NAME: "testServiceName"}) + ) + ) + host_name = "testHostName" client = grpc.insecure_channel("") endpoint = "testEndpoint" with patch: exporter = OpenCensusSpanExporter( - service_name=service_name, - host_name=host_name, - endpoint=endpoint, - client=client, + host_name=host_name, endpoint=endpoint, client=client, ) self.assertIs(exporter.client, client) self.assertEqual(exporter.endpoint, endpoint) - mock_get_node.assert_called_with(service_name, host_name) + mock_get_node.assert_called_with("testServiceName", host_name) def test_get_collector_span_kind(self): result = utils.get_collector_span_kind(trace_api.SpanKind.SERVER) diff --git a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py index 299232fb77..6077115b01 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py @@ -32,11 +32,6 @@ .. envvar:: OTEL_PYTHON_ID_GENERATOR """ -OTEL_PYTHON_SERVICE_NAME = "OTEL_PYTHON_SERVICE_NAME" -""" -.. envvar:: OTEL_PYTHON_SERVICE_NAME -""" - OTEL_TRACES_EXPORTER = "OTEL_TRACES_EXPORTER" """ .. envvar:: OTEL_TRACES_EXPORTER diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index 6496be16c5..1adfe56ac7 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -22,12 +22,11 @@ from opentelemetry import trace from opentelemetry.environment_variables import ( OTEL_PYTHON_ID_GENERATOR, - OTEL_PYTHON_SERVICE_NAME, OTEL_TRACES_EXPORTER, ) from opentelemetry.instrumentation.configurator import BaseConfigurator from opentelemetry.instrumentation.distro import BaseDistro -from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator @@ -46,10 +45,6 @@ def _get_id_generator() -> str: return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR) -def _get_service_name() -> str: - return environ.get(OTEL_PYTHON_SERVICE_NAME, "") - - def _get_exporter_names() -> Sequence[str]: trace_exporters = environ.get(OTEL_TRACES_EXPORTER) @@ -76,21 +71,13 @@ def _get_exporter_names() -> Sequence[str]: def _init_tracing( exporters: Sequence[SpanExporter], id_generator: IdGenerator ): - service_name = _get_service_name() - provider = TracerProvider( - resource=Resource.create({"service.name": service_name}), - id_generator=id_generator(), - ) + # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name + # from the env variable else defaults to "unknown_service" + provider = TracerProvider(id_generator=id_generator(),) trace.set_tracer_provider(provider) - for exporter_name, exporter_class in exporters.items(): + for _, exporter_class in exporters.items(): exporter_args = {} - if exporter_name not in [ - EXPORTER_OTLP, - EXPORTER_OTLP_SPAN, - ]: - exporter_args["service_name"] = service_name - provider.add_span_processor( BatchSpanProcessor(exporter_class(**exporter_args)) ) diff --git a/opentelemetry-distro/tests/test_configurator.py b/opentelemetry-distro/tests/test_configurator.py index 899e019113..fa33744dea 100644 --- a/opentelemetry-distro/tests/test_configurator.py +++ b/opentelemetry-distro/tests/test_configurator.py @@ -17,6 +17,7 @@ from unittest import TestCase from unittest.mock import patch +from opentelemetry import trace from opentelemetry.distro import ( EXPORTER_OTLP, EXPORTER_OTLP_SPAN, @@ -27,10 +28,9 @@ ) from opentelemetry.environment_variables import ( OTEL_PYTHON_ID_GENERATOR, - OTEL_PYTHON_SERVICE_NAME, OTEL_TRACES_EXPORTER, ) -from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator @@ -38,7 +38,7 @@ class Provider: def __init__(self, resource=None, id_generator=None): self.id_generator = id_generator self.processor = None - self.resource = resource + self.resource = resource or Resource.create({}) def add_span_processor(self, processor): self.processor = processor @@ -50,8 +50,13 @@ def __init__(self, exporter): class Exporter: - def __init__(self, service_name): - self.service_name = service_name + def __init__(self): + tracer_provider = trace.get_tracer_provider() + self.service_name = ( + tracer_provider.resource.attributes[SERVICE_NAME] + if getattr(tracer_provider, "resource", None) + else Resource.create().attributes.get(SERVICE_NAME) + ) def shutdown(self): pass @@ -102,8 +107,10 @@ def tearDown(self): self.set_provider_patcher.stop() # pylint: disable=protected-access + @patch.dict( + environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=my-test-service"} + ) def test_trace_init_default(self): - environ[OTEL_PYTHON_SERVICE_NAME] = "my-test-service" _init_tracing({"zipkin": Exporter}, RandomIdGenerator) self.assertEqual(self.set_provider_mock.call_count, 1) @@ -116,8 +123,11 @@ def test_trace_init_default(self): provider.processor.exporter.service_name, "my-test-service" ) + @patch.dict( + environ, + {"OTEL_RESOURCE_ATTRIBUTES": "service.name=my-otlp-test-service"}, + ) def test_trace_init_otlp(self): - environ[OTEL_PYTHON_SERVICE_NAME] = "my-otlp-test-service" _init_tracing({"otlp": OTLPExporter}, RandomIdGenerator) self.assertEqual(self.set_provider_mock.call_count, 1) @@ -131,7 +141,6 @@ def test_trace_init_otlp(self): provider.resource.attributes.get("service.name"), "my-otlp-test-service", ) - del environ[OTEL_PYTHON_SERVICE_NAME] @patch.dict(environ, {OTEL_PYTHON_ID_GENERATOR: "custom_id_generator"}) @patch("opentelemetry.distro.IdGenerator", new=IdGenerator) diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index 14ba530e83..7261c177f8 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -68,10 +68,6 @@ Well known trace exporter names: ``otlp`` is an alias for ``otlp_span``. -* ``--service-name`` or ``OTEL_PYTHON_SERVICE_NAME`` - -When present the value is passed on to the relevant exporter initializer as ``service_name`` argument. - * ``--id-generator`` or ``OTEL_PYTHON_ID_GENERATOR`` Used to specify which IDs Generator to use for the global Tracer Provider. By default, it diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index b48cc93682..f08b0b144e 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -22,7 +22,6 @@ from opentelemetry.environment_variables import ( OTEL_PYTHON_ID_GENERATOR, - OTEL_PYTHON_SERVICE_NAME, OTEL_TRACES_EXPORTER, ) @@ -83,8 +82,6 @@ def parse_args(): def load_config_from_cli_args(args): if args.trace_exporter: environ[OTEL_TRACES_EXPORTER] = args.trace_exporter - if args.service_name: - environ[OTEL_PYTHON_SERVICE_NAME] = args.service_name if args.id_generator: environ[OTEL_PYTHON_ID_GENERATOR] = args.id_generator diff --git a/opentelemetry-instrumentation/tests/test_run.py b/opentelemetry-instrumentation/tests/test_run.py index 69d4bdecf1..01bd86ed32 100644 --- a/opentelemetry-instrumentation/tests/test_run.py +++ b/opentelemetry-instrumentation/tests/test_run.py @@ -18,10 +18,7 @@ from unittest import TestCase from unittest.mock import patch -from opentelemetry.environment_variables import ( - OTEL_PYTHON_SERVICE_NAME, - OTEL_TRACES_EXPORTER, -) +from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER from opentelemetry.instrumentation import auto_instrumentation @@ -118,15 +115,3 @@ def test_exporter(self, _): # pylint: disable=no-self-use ): auto_instrumentation.run() self.assertEqual(environ.get(OTEL_TRACES_EXPORTER), "jaeger") - - @patch("opentelemetry.instrumentation.auto_instrumentation.execl") - def test_service_name(self, _): # pylint: disable=no-self-use - with patch("sys.argv", ["instrument", "2"]): - auto_instrumentation.run() - self.assertIsNone(environ.get(OTEL_PYTHON_SERVICE_NAME)) - - with patch("sys.argv", ["instrument", "-s", "my-service", "1", "2"]): - auto_instrumentation.run() - self.assertEqual( - environ.get(OTEL_PYTHON_SERVICE_NAME), "my-service" - ) diff --git a/tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py b/tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py index ce2f3a02b4..a3c1ee2030 100644 --- a/tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py +++ b/tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py @@ -40,9 +40,7 @@ def setUp(self): trace.set_tracer_provider(TracerProvider()) self.tracer = trace.get_tracer(__name__) self.span_processor = ExportStatusSpanProcessor( - OpenCensusSpanExporter( - service_name="basic-service", endpoint="localhost:55678" - ) + OpenCensusSpanExporter(endpoint="localhost:55678") ) trace.get_tracer_provider().add_span_processor(self.span_processor) From f92431e90d258ad6f0f4f496b2e9b778bcb1f627 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 17 Mar 2021 08:03:18 -0700 Subject: [PATCH 0817/1517] split jaeger exporter for proto/thrift (#1694) --- .flake8 | 6 +- CHANGELOG.md | 3 + docs/examples/opentracing/main.py | 8 +- docs/exporter/jaeger/jaeger.rst | 19 +- docs/getting_started/jaeger_example.py | 6 +- .../LICENSE | 201 ++++++++++++++++++ .../MANIFEST.in | 9 + .../README.rst | 36 ++++ .../examples/jaeger_exporter_example.py | 23 +- .../proto/api_v2/collector.proto | 0 .../proto/api_v2/model.proto | 0 .../setup.cfg | 56 +++++ .../setup.py | 32 +++ .../exporter/jaeger/proto/__init__.py | 176 +++++++++++++++ .../exporter/jaeger/proto}/gen/__init__.py | 0 .../jaeger/proto}/gen/collector_pb2.py | 0 .../jaeger/proto}/gen/collector_pb2_grpc.py | 0 .../jaeger/proto}/gen/gogoproto/gogo_pb2.py | 0 .../proto}/gen/google/api/annotations_pb2.py | 0 .../jaeger/proto}/gen/google/api/http_pb2.py | 0 .../exporter/jaeger/proto}/gen/model_pb2.py | 0 .../options/annotations_pb2.py | 0 .../options/openapiv2_pb2.py | 0 .../exporter/jaeger/proto}/send/__init__.py | 0 .../jaeger/proto/translate/__init__.py} | 86 +++++++- .../exporter/jaeger/proto}/util.py | 0 .../exporter/jaeger/proto/version.py | 16 ++ .../tests/__init__.py | 0 .../tests/certs/cred.cert | 0 .../tests/test_jaeger_exporter_protobuf.py | 10 +- .../LICENSE | 201 ++++++++++++++++++ .../MANIFEST.in | 9 + .../README.rst | 36 ++++ .../examples/jaeger_exporter_example.py | 45 ++++ .../setup.cfg | 55 +++++ .../setup.py | 32 +++ .../exporter/jaeger/thrift}/__init__.py | 110 +++------- .../exporter/jaeger/thrift/gen/__init__.py | 4 + .../jaeger/thrift}/gen/agent/Agent-remote | 0 .../jaeger/thrift}/gen/agent/Agent.py | 0 .../jaeger/thrift}/gen/agent/__init__.py | 0 .../jaeger/thrift}/gen/agent/constants.py | 0 .../jaeger/thrift}/gen/agent/ttypes.py | 0 .../thrift}/gen/jaeger/Collector-remote | 0 .../jaeger/thrift}/gen/jaeger/Collector.py | 0 .../jaeger/thrift}/gen/jaeger/__init__.py | 0 .../jaeger/thrift}/gen/jaeger/constants.py | 0 .../jaeger/thrift}/gen/jaeger/ttypes.py | 0 .../gen/zipkincore/ZipkinCollector-remote | 0 .../thrift}/gen/zipkincore/ZipkinCollector.py | 0 .../jaeger/thrift}/gen/zipkincore/__init__.py | 0 .../thrift}/gen/zipkincore/constants.py | 0 .../jaeger/thrift}/gen/zipkincore/ttypes.py | 0 .../exporter/jaeger/thrift/send.py} | 4 +- .../jaeger/thrift/translate/__init__.py} | 87 +++++++- .../exporter/jaeger/thrift/version.py | 16 ++ .../tests/__init__.py | 0 .../tests/certs/cred.cert | 0 .../tests/test_jaeger_exporter_thrift.py | 20 +- .../thrift/agent.thrift | 0 .../thrift/jaeger.thrift | 0 .../thrift/zipkincore.thrift | 0 .../opentelemetry-exporter-jaeger/setup.cfg | 20 +- .../exporter/jaeger/translate/__init__.py | 91 -------- .../tests/test_jaeger.py | 30 +++ pyproject.toml | 3 +- scripts/coverage.sh | 3 +- tox.ini | 18 +- 68 files changed, 1210 insertions(+), 261 deletions(-) create mode 100644 exporter/opentelemetry-exporter-jaeger-proto/LICENSE create mode 100644 exporter/opentelemetry-exporter-jaeger-proto/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-jaeger-proto/README.rst rename exporter/{opentelemetry-exporter-jaeger => opentelemetry-exporter-jaeger-proto}/examples/jaeger_exporter_example.py (69%) rename exporter/{opentelemetry-exporter-jaeger => opentelemetry-exporter-jaeger-proto}/proto/api_v2/collector.proto (100%) rename exporter/{opentelemetry-exporter-jaeger => opentelemetry-exporter-jaeger-proto}/proto/api_v2/model.proto (100%) create mode 100644 exporter/opentelemetry-exporter-jaeger-proto/setup.cfg create mode 100644 exporter/opentelemetry-exporter-jaeger-proto/setup.py create mode 100644 exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/__init__.py rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto}/gen/__init__.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto}/gen/collector_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto}/gen/collector_pb2_grpc.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto}/gen/gogoproto/gogo_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto}/gen/google/api/annotations_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto}/gen/google/api/http_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto}/gen/model_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto}/gen/protoc_gen_swagger/options/annotations_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto}/gen/protoc_gen_swagger/options/openapiv2_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto}/send/__init__.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py => opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/translate/__init__.py} (82%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto}/util.py (100%) create mode 100644 exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/version.py rename exporter/{opentelemetry-exporter-jaeger => opentelemetry-exporter-jaeger-proto}/tests/__init__.py (100%) rename exporter/{opentelemetry-exporter-jaeger => opentelemetry-exporter-jaeger-proto}/tests/certs/cred.cert (100%) rename exporter/{opentelemetry-exporter-jaeger => opentelemetry-exporter-jaeger-proto}/tests/test_jaeger_exporter_protobuf.py (98%) create mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/LICENSE create mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/README.rst create mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/examples/jaeger_exporter_example.py create mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg create mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/setup.py rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/__init__.py (64%) create mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/__init__.py rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/agent/Agent-remote (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/agent/Agent.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/agent/__init__.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/agent/constants.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/agent/ttypes.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/jaeger/Collector-remote (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/jaeger/Collector.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/jaeger/__init__.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/jaeger/constants.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/jaeger/ttypes.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/zipkincore/ZipkinCollector-remote (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/zipkincore/ZipkinCollector.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/zipkincore/__init__.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/zipkincore/constants.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/gen/zipkincore/ttypes.py (100%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/thrift.py => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py} (96%) rename exporter/{opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py => opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py} (77%) create mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py create mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/tests/__init__.py create mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/tests/certs/cred.cert rename exporter/{opentelemetry-exporter-jaeger => opentelemetry-exporter-jaeger-thrift}/tests/test_jaeger_exporter_thrift.py (97%) rename exporter/{opentelemetry-exporter-jaeger => opentelemetry-exporter-jaeger-thrift}/thrift/agent.thrift (100%) rename exporter/{opentelemetry-exporter-jaeger => opentelemetry-exporter-jaeger-thrift}/thrift/jaeger.thrift (100%) rename exporter/{opentelemetry-exporter-jaeger => opentelemetry-exporter-jaeger-thrift}/thrift/zipkincore.thrift (100%) delete mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py create mode 100644 exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py diff --git a/.flake8 b/.flake8 index baa9f02c31..8431f87079 100644 --- a/.flake8 +++ b/.flake8 @@ -16,8 +16,10 @@ exclude = venv*/ target __pycache__ - exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/ - exporter/opentelemetry-exporter-jaeger/build/* + exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/ + exporter/opentelemetry-exporter-jaeger-proto/build/* + exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/ + exporter/opentelemetry-exporter-jaeger-thrift/build/* exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/ docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ docs/examples/opentelemetry-example-app/build/* diff --git a/CHANGELOG.md b/CHANGELOG.md index b87debc484..30aeb1209e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1680](https://github.com/open-telemetry/opentelemetry-python/pull/1680)) - Change Zipkin exporter to obtain service.name from span ([#1696](https://github.com/open-telemetry/opentelemetry-python/pull/1696)) +- Split up `opentelemetry-exporter-jaeger` package into `opentelemetry-exporter-jaeger-proto` and + `opentelemetry-exporter-jaeger-thrift` packages to reduce dependencies for each one. + ([#1694](https://github.com/open-telemetry/opentelemetry-python/pull/1694)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. diff --git a/docs/examples/opentracing/main.py b/docs/examples/opentracing/main.py index df5626b043..09ff51465c 100755 --- a/docs/examples/opentracing/main.py +++ b/docs/examples/opentracing/main.py @@ -3,7 +3,7 @@ from rediscache import RedisCache from opentelemetry import trace -from opentelemetry.exporter.jaeger import JaegerExporter +from opentelemetry.exporter.jaeger.thrift import JaegerExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.shim import opentracing_shim @@ -13,11 +13,7 @@ tracer_provider = trace.get_tracer_provider() # Configure the tracer to export traces to Jaeger -jaeger_exporter = JaegerExporter( - service_name="OpenTracing Shim Example", - agent_host_name="localhost", - agent_port=6831, -) +jaeger_exporter = JaegerExporter(agent_host_name="localhost", agent_port=6831,) span_processor = SimpleSpanProcessor(jaeger_exporter) tracer_provider.add_span_processor(span_processor) diff --git a/docs/exporter/jaeger/jaeger.rst b/docs/exporter/jaeger/jaeger.rst index efbcfbd7d7..ea2e427cbe 100644 --- a/docs/exporter/jaeger/jaeger.rst +++ b/docs/exporter/jaeger/jaeger.rst @@ -1,26 +1,35 @@ -Opentelemetry Jaeger Exporter -============================= +Opentelemetry Jaeger Exporters +============================== .. automodule:: opentelemetry.exporter.jaeger :members: :undoc-members: :show-inheritance: +.. automodule:: opentelemetry.exporter.jaeger.thrift + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: opentelemetry.exporter.jaeger.proto + :members: + :undoc-members: + :show-inheritance: Submodules ---------- -.. automodule:: opentelemetry.exporter.jaeger.gen.jaeger.ttypes +.. automodule:: opentelemetry.exporter.jaeger.thrift.gen.jaeger.ttypes :members: :undoc-members: :show-inheritance: -.. automodule:: opentelemetry.exporter.jaeger.send.thrift +.. automodule:: opentelemetry.exporter.jaeger.thrift.send :members: :undoc-members: :show-inheritance: -.. automodule:: opentelemetry.exporter.jaeger.gen.collector_pb2_grpc +.. automodule:: opentelemetry.exporter.jaeger.proto.gen.collector_pb2_grpc :members: :undoc-members: :show-inheritance: diff --git a/docs/getting_started/jaeger_example.py b/docs/getting_started/jaeger_example.py index 6733741483..77fea2585d 100644 --- a/docs/getting_started/jaeger_example.py +++ b/docs/getting_started/jaeger_example.py @@ -14,7 +14,7 @@ # jaeger_example.py from opentelemetry import trace -from opentelemetry.exporter import jaeger +from opentelemetry.exporter.jaeger.thrift import JaegerExporter from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor @@ -25,9 +25,7 @@ ) ) -jaeger_exporter = jaeger.JaegerExporter( - agent_host_name="localhost", agent_port=6831, -) +jaeger_exporter = JaegerExporter(agent_host_name="localhost", agent_port=6831,) trace.get_tracer_provider().add_span_processor( BatchSpanProcessor(jaeger_exporter) diff --git a/exporter/opentelemetry-exporter-jaeger-proto/LICENSE b/exporter/opentelemetry-exporter-jaeger-proto/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-proto/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/exporter/opentelemetry-exporter-jaeger-proto/MANIFEST.in b/exporter/opentelemetry-exporter-jaeger-proto/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-proto/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/exporter/opentelemetry-exporter-jaeger-proto/README.rst b/exporter/opentelemetry-exporter-jaeger-proto/README.rst new file mode 100644 index 0000000000..bd1a0a64f0 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-proto/README.rst @@ -0,0 +1,36 @@ +OpenTelemetry Jaeger Protobuf Exporter +====================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger-proto.svg + :target: https://pypi.org/project/opentelemetry-exporter-jaeger-proto/ + +This library allows to export tracing data to `Jaeger `_. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-jaeger-proto + + +.. _Jaeger: https://www.jaegertracing.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + +Configuration +------------- + +OpenTelemetry Jaeger Exporter can be configured by setting `JaegerExporter parameters +`_ or by setting +`environment variables `_ + +References +---------- + +* `OpenTelemetry Jaeger Exporter `_ +* `Jaeger `_ +* `OpenTelemetry Project `_ diff --git a/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py b/exporter/opentelemetry-exporter-jaeger-proto/examples/jaeger_exporter_example.py similarity index 69% rename from exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py rename to exporter/opentelemetry-exporter-jaeger-proto/examples/jaeger_exporter_example.py index 35e83f18f3..9eb7389735 100644 --- a/exporter/opentelemetry-exporter-jaeger/examples/jaeger_exporter_example.py +++ b/exporter/opentelemetry-exporter-jaeger-proto/examples/jaeger_exporter_example.py @@ -1,37 +1,22 @@ import time from opentelemetry import trace -from opentelemetry.exporter import jaeger +from opentelemetry.exporter.jaeger import proto from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) -# create a JaegerExporter -jaeger_exporter = jaeger.JaegerExporter( - # configure agent - agent_host_name="localhost", - agent_port=6831, - # optional: configure also collector - # collector_host_name="localhost", - # collector_port=14268, - # collector_endpoint="/api/traces?format=jaeger.thrift", - # username=xxxx, # optional - # password=xxxx, # optional -) - # Create a JaegerExporter to send spans with gRPC # If there is no encryption or authentication set `insecure` to True # If server has authentication with SSL/TLS you can set the # parameter credentials=ChannelCredentials(...) or the environment variable # `EXPORTER_JAEGER_CERTIFICATE` with file containing creds. -# jaeger_exporter = jaeger.JaegerExporter( -# collector_endpoint="localhost:14250", -# insecure=True, -# transport_format="protobuf", -# ) +jaeger_exporter = proto.JaegerExporter( + collector_endpoint="localhost:14250", insecure=True, +) # create a BatchSpanProcessor and add the exporter to it span_processor = BatchSpanProcessor(jaeger_exporter) diff --git a/exporter/opentelemetry-exporter-jaeger/proto/api_v2/collector.proto b/exporter/opentelemetry-exporter-jaeger-proto/proto/api_v2/collector.proto similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/proto/api_v2/collector.proto rename to exporter/opentelemetry-exporter-jaeger-proto/proto/api_v2/collector.proto diff --git a/exporter/opentelemetry-exporter-jaeger/proto/api_v2/model.proto b/exporter/opentelemetry-exporter-jaeger-proto/proto/api_v2/model.proto similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/proto/api_v2/model.proto rename to exporter/opentelemetry-exporter-jaeger-proto/proto/api_v2/model.proto diff --git a/exporter/opentelemetry-exporter-jaeger-proto/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto/setup.cfg new file mode 100644 index 0000000000..3122904f61 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-proto/setup.cfg @@ -0,0 +1,56 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-exporter-jaeger-proto +description = Jaeger Protobuf Exporter for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger-proto +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + grpcio >= 1.0.0, < 2.0.0 + googleapis-common-protos ~= 1.52.0 + opentelemetry-api == 1.0.0.dev0 + opentelemetry-sdk == 1.0.0.dev0 + +[options.packages.find] +where = src + +[options.extras_require] +test = + +[options.entry_points] +opentelemetry_exporter = + jaeger_proto = opentelemetry.exporter.jaeger.proto:JaegerExporter \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-jaeger-proto/setup.py b/exporter/opentelemetry-exporter-jaeger-proto/setup.py new file mode 100644 index 0000000000..2cc98f9b0b --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-proto/setup.py @@ -0,0 +1,32 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "exporter", + "jaeger", + "proto", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/__init__.py new file mode 100644 index 0000000000..77a29fd09a --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/__init__.py @@ -0,0 +1,176 @@ +# Copyright 2018, OpenCensus Authors +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" + +OpenTelemetry Jaeger Protobuf Exporter +-------------------------------------- + +The **OpenTelemetry Jaeger Protobuf Exporter** allows to export `OpenTelemetry`_ traces to `Jaeger`_. +This exporter always sends traces to the configured agent using Protobuf via gRPC. + +Usage +----- + +.. code:: python + + from opentelemetry import trace + from opentelemetry.exporter.jaeger.proto import JaegerExporter + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + + trace.set_tracer_provider(TracerProvider()) + tracer = trace.get_tracer(__name__) + + # create a JaegerExporter + jaeger_exporter = JaegerExporter( + # optional: configure collector + # collector_endpoint='localhost:14250', + # insecure=True, # optional + # credentials=xxx # optional channel creds + # max_tag_value_length=None # optional + ) + + # Create a BatchSpanProcessor and add the exporter to it + span_processor = BatchSpanProcessor(jaeger_exporter) + + # add to the tracer + trace.get_tracer_provider().add_span_processor(span_processor) + + with tracer.start_as_current_span('foo'): + print('Hello world!') + +You can configure the exporter with the following environment variables: + +- :envvar:`OTEL_EXPORTER_JAEGER_ENDPOINT` +- :envvar:`OTEL_EXPORTER_JAEGER_CERTIFICATE` + +API +--- +.. _Jaeger: https://www.jaegertracing.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +""" +# pylint: disable=protected-access + +import logging +from os import environ +from typing import Optional + +from grpc import ChannelCredentials, insecure_channel, secure_channel + +from opentelemetry import trace +from opentelemetry.exporter.jaeger.proto import util +from opentelemetry.exporter.jaeger.proto.gen import model_pb2 +from opentelemetry.exporter.jaeger.proto.gen.collector_pb2 import ( + PostSpansRequest, +) +from opentelemetry.exporter.jaeger.proto.gen.collector_pb2_grpc import ( + CollectorServiceStub, +) +from opentelemetry.exporter.jaeger.proto.translate import ( + ProtobufTranslator, + Translate, +) +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_JAEGER_ENDPOINT, +) +from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult + +DEFAULT_GRPC_COLLECTOR_ENDPOINT = "localhost:14250" + +logger = logging.getLogger(__name__) + + +class JaegerExporter(SpanExporter): + """Jaeger span exporter for OpenTelemetry. + + Args: + collector_endpoint: The endpoint of the Jaeger collector that uses + Protobuf via gRPC. + insecure: True if collector has no encryption or authentication + credentials: Credentials for server authentication. + max_tag_value_length: Max length string attribute values can have. Set to None to disable. + """ + + def __init__( + self, + collector_endpoint: Optional[str] = None, + insecure: Optional[bool] = None, + credentials: Optional[ChannelCredentials] = None, + max_tag_value_length: Optional[int] = None, + ): + self._max_tag_value_length = max_tag_value_length + + self.collector_endpoint = _parameter_setter( + param=collector_endpoint, + env_variable=environ.get(OTEL_EXPORTER_JAEGER_ENDPOINT), + default=None, + ) + self._grpc_client = None + self.insecure = insecure + self.credentials = util._get_credentials(credentials) + tracer_provider = trace.get_tracer_provider() + self.service_name = ( + tracer_provider.resource.attributes[SERVICE_NAME] + if getattr(tracer_provider, "resource", None) + else Resource.create().attributes.get(SERVICE_NAME) + ) + + @property + def _collector_grpc_client(self) -> Optional[CollectorServiceStub]: + endpoint = self.collector_endpoint or DEFAULT_GRPC_COLLECTOR_ENDPOINT + + if self._grpc_client is None: + if self.insecure: + self._grpc_client = CollectorServiceStub( + insecure_channel(endpoint) + ) + else: + self._grpc_client = CollectorServiceStub( + secure_channel(endpoint, self.credentials) + ) + return self._grpc_client + + def export(self, spans) -> SpanExportResult: + translator = Translate(spans) + pb_translator = ProtobufTranslator( + self.service_name, self._max_tag_value_length + ) + jaeger_spans = translator._translate(pb_translator) + batch = model_pb2.Batch(spans=jaeger_spans) + request = PostSpansRequest(batch=batch) + self._collector_grpc_client.PostSpans(request) + + return SpanExportResult.SUCCESS + + def shutdown(self): + pass + + +def _parameter_setter(param, env_variable, default): + """Returns value according to the provided data. + + Args: + param: Constructor parameter value + env_variable: Environment variable related to the parameter + default: Constructor parameter default value + """ + if param is None: + res = env_variable or default + else: + res = param + + return res diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/__init__.py rename to exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/__init__.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/collector_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/collector_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/collector_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/collector_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/collector_pb2_grpc.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/collector_pb2_grpc.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/collector_pb2_grpc.py rename to exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/collector_pb2_grpc.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/gogoproto/gogo_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/gogoproto/gogo_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/gogoproto/gogo_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/gogoproto/gogo_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/google/api/annotations_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/google/api/annotations_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/google/api/annotations_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/google/api/annotations_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/google/api/http_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/google/api/http_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/google/api/http_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/google/api/http_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/model_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/model_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/model_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/model_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/protoc_gen_swagger/options/annotations_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/protoc_gen_swagger/options/annotations_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/protoc_gen_swagger/options/annotations_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/protoc_gen_swagger/options/annotations_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/protoc_gen_swagger/options/openapiv2_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/protoc_gen_swagger/options/openapiv2_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/protoc_gen_swagger/options/openapiv2_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/protoc_gen_swagger/options/openapiv2_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/send/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/__init__.py rename to exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/send/__init__.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/translate/__init__.py similarity index 82% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py rename to exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/translate/__init__.py index 011e24f17f..6a346000b4 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/translate/__init__.py @@ -12,21 +12,93 @@ # See the License for the specific language governing permissions and # limitations under the License. +import abc from typing import Optional, Sequence from google.protobuf.duration_pb2 import Duration from google.protobuf.timestamp_pb2 import Timestamp -from opentelemetry.exporter.jaeger.gen import model_pb2 -from opentelemetry.exporter.jaeger.translate import ( - NAME_KEY, - OTLP_JAEGER_SPAN_KIND, - VERSION_KEY, - Translator, -) +from opentelemetry.exporter.jaeger.proto.gen import model_pb2 + from opentelemetry.sdk.trace import ReadableSpan, StatusCode +from opentelemetry.trace import SpanKind from opentelemetry.util import types + +OTLP_JAEGER_SPAN_KIND = { + SpanKind.CLIENT: "client", + SpanKind.SERVER: "server", + SpanKind.CONSUMER: "consumer", + SpanKind.PRODUCER: "producer", + SpanKind.INTERNAL: "internal", +} + +NAME_KEY = "otel.library.name" +VERSION_KEY = "otel.library.version" + + +def _nsec_to_usec_round(nsec: int) -> int: + """Round nanoseconds to microseconds""" + return (nsec + 500) // 10 ** 3 + + +def _convert_int_to_i64(val): + """Convert integer to signed int64 (i64)""" + if val > 0x7FFFFFFFFFFFFFFF: + val -= 0x10000000000000000 + return val + + +class Translator(abc.ABC): + def __init__(self, max_tag_value_length: Optional[int] = None): + self._max_tag_value_length = max_tag_value_length + + @abc.abstractmethod + def _translate_span(self, span): + """Translates span to jaeger format. + + Args: + span: span to translate + """ + + @abc.abstractmethod + def _extract_tags(self, span): + """Extracts tags from span and returns list of jaeger Tags. + + Args: + span: span to extract tags + """ + + @abc.abstractmethod + def _extract_refs(self, span): + """Extracts references from span and returns list of jaeger SpanRefs. + + Args: + span: span to extract references + """ + + @abc.abstractmethod + def _extract_logs(self, span): + """Extracts logs from span and returns list of jaeger Logs. + + Args: + span: span to extract logs + """ + + +class Translate: + def __init__(self, spans): + self.spans = spans + + def _translate(self, translator: Translator): + translated_spans = [] + for span in self.spans: + # pylint: disable=protected-access + translated_span = translator._translate_span(span) + translated_spans.append(translated_span) + return translated_spans + + # pylint: disable=no-member,too-many-locals,no-self-use diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/util.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/util.py rename to exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/util.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/version.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/version.py new file mode 100644 index 0000000000..15eb84e8c0 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/version.py @@ -0,0 +1,16 @@ +# Copyright 2019, OpenCensus Authors +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "1.0.0.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger/tests/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto/tests/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/tests/__init__.py rename to exporter/opentelemetry-exporter-jaeger-proto/tests/__init__.py diff --git a/exporter/opentelemetry-exporter-jaeger/tests/certs/cred.cert b/exporter/opentelemetry-exporter-jaeger-proto/tests/certs/cred.cert similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/tests/certs/cred.cert rename to exporter/opentelemetry-exporter-jaeger-proto/tests/certs/cred.cert diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger-proto/tests/test_jaeger_exporter_protobuf.py similarity index 98% rename from exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py rename to exporter/opentelemetry-exporter-jaeger-proto/tests/test_jaeger_exporter_protobuf.py index 2c2bef7b4f..237ba54b2f 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger-proto/tests/test_jaeger_exporter_protobuf.py @@ -19,11 +19,11 @@ # pylint:disable=no-name-in-module # pylint:disable=import-error -import opentelemetry.exporter.jaeger.gen.model_pb2 as model_pb2 -import opentelemetry.exporter.jaeger.translate.protobuf as pb_translator +import opentelemetry.exporter.jaeger.proto.gen.model_pb2 as model_pb2 +import opentelemetry.exporter.jaeger.proto.translate as pb_translator from opentelemetry import trace as trace_api -from opentelemetry.exporter.jaeger import JaegerExporter -from opentelemetry.exporter.jaeger.translate import ( +from opentelemetry.exporter.jaeger.proto import JaegerExporter +from opentelemetry.exporter.jaeger.proto.translate import ( NAME_KEY, VERSION_KEY, Translate, @@ -75,7 +75,7 @@ def test_constructor_by_environment_variables(self): env_patch.start() provider = TracerProvider(resource=Resource.create({})) trace_api.set_tracer_provider(provider) - exporter = JaegerExporter(transport_format="protobuf") + exporter = JaegerExporter() self.assertEqual(exporter.service_name, service) self.assertIsNotNone(exporter._collector_grpc_client) self.assertEqual(exporter.collector_endpoint, collector_endpoint) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/LICENSE b/exporter/opentelemetry-exporter-jaeger-thrift/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-thrift/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/MANIFEST.in b/exporter/opentelemetry-exporter-jaeger-thrift/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-thrift/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/README.rst b/exporter/opentelemetry-exporter-jaeger-thrift/README.rst new file mode 100644 index 0000000000..ed6bb1d792 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-thrift/README.rst @@ -0,0 +1,36 @@ +OpenTelemetry Jaeger Thrift Exporter +==================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger-thrift.svg + :target: https://pypi.org/project/opentelemetry-exporter-jaeger-thrift/ + +This library allows to export tracing data to `Jaeger `_ using Thrift. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-jaeger-thrift + + +.. _Jaeger: https://www.jaegertracing.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + +Configuration +------------- + +OpenTelemetry Jaeger Exporter can be configured by setting `JaegerExporter parameters +`_ or by setting +`environment variables `_ + +References +---------- + +* `OpenTelemetry Jaeger Exporter `_ +* `Jaeger `_ +* `OpenTelemetry Project `_ diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/examples/jaeger_exporter_example.py b/exporter/opentelemetry-exporter-jaeger-thrift/examples/jaeger_exporter_example.py new file mode 100644 index 0000000000..1dc138c1da --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-thrift/examples/jaeger_exporter_example.py @@ -0,0 +1,45 @@ +import time + +from opentelemetry import trace +from opentelemetry.exporter.jaeger import thrift +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer(__name__) + +# create a JaegerExporter +jaeger_exporter = thrift.JaegerExporter( + # configure agent + agent_host_name="localhost", + agent_port=6831, + # optional: configure also collector + # collector_endpoint="http://localhost:14268/api/traces?format=jaeger.thrift", + # username=xxxx, # optional + # password=xxxx, # optional +) + +# create a BatchSpanProcessor and add the exporter to it +span_processor = BatchSpanProcessor(jaeger_exporter) + +# add to the tracer factory +trace.get_tracer_provider().add_span_processor(span_processor) + +# create some spans for testing +with tracer.start_as_current_span("foo") as foo: + time.sleep(0.1) + foo.set_attribute("my_atribbute", True) + foo.add_event("event in foo", {"name": "foo1"}) + with tracer.start_as_current_span( + "bar", links=[trace.Link(foo.get_span_context())] + ) as bar: + time.sleep(0.2) + bar.set_attribute("speed", 100.0) + + with tracer.start_as_current_span("baz") as baz: + time.sleep(0.3) + baz.set_attribute("name", "mauricio") + + time.sleep(0.2) + + time.sleep(0.1) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg new file mode 100644 index 0000000000..5ab20acdf2 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-exporter-jaeger-thrift +description = Jaeger Thrift Exporter for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger-thrift +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + thrift >= 0.10.0 + opentelemetry-api == 1.0.0.dev0 + opentelemetry-sdk == 1.0.0.dev0 + +[options.packages.find] +where = src + +[options.extras_require] +test = + +[options.entry_points] +opentelemetry_exporter = + jaeger_thrift = opentelemetry.exporter.jaeger.thrift:JaegerExporter \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.py b/exporter/opentelemetry-exporter-jaeger-thrift/setup.py new file mode 100644 index 0000000000..e9200b0c7a --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.py @@ -0,0 +1,32 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "exporter", + "jaeger", + "thrift", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py similarity index 64% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py index 6634fcc6e8..674b9d2bf6 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py @@ -14,12 +14,16 @@ # limitations under the License. """ -The **OpenTelemetry Jaeger Exporter** allows to export `OpenTelemetry`_ traces to `Jaeger`_. + +OpenTelemetry Jaeger Thrift Exporter +------------------------------------ + +The **OpenTelemetry Jaeger Thrift Exporter** allows to export `OpenTelemetry`_ traces to `Jaeger`_. This exporter always sends traces to the configured agent using the Thrift compact protocol over UDP. When it is not feasible to deploy Jaeger Agent next to the application, for example, when the application code is running as Lambda function, a collector can be configured to send spans -using either Thrift over HTTP or Protobuf via gRPC. If both agent and collector are configured, -the exporter sends traces only to the collector to eliminate the duplicate entries. +using Thrift over HTTP. If both agent and collector are configured, the exporter sends traces +only to the collector to eliminate the duplicate entries. Usage ----- @@ -27,7 +31,7 @@ .. code:: python from opentelemetry import trace - from opentelemetry.exporter import jaeger + from opentelemetry.exporter.jaeger.thrift import JaegerExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor @@ -35,7 +39,7 @@ tracer = trace.get_tracer(__name__) # create a JaegerExporter - jaeger_exporter = jaeger.JaegerExporter( + jaeger_exporter = JaegerExporter( # configure agent agent_host_name='localhost', agent_port=6831, @@ -43,9 +47,6 @@ # collector_endpoint='http://localhost:14268/api/traces?format=jaeger.thrift', # username=xxxx, # optional # password=xxxx, # optional - # insecure=True, # optional - # credentials=xxx # optional channel creds - # transport_format='protobuf' # optional # max_tag_value_length=None # optional ) @@ -63,9 +64,9 @@ - :envvar:`OTEL_EXPORTER_JAEGER_USER` - :envvar:`OTEL_EXPORTER_JAEGER_PASSWORD` - :envvar:`OTEL_EXPORTER_JAEGER_ENDPOINT` -- :envvar:`OTEL_EXPORTER_JAEGER_CERTIFICATE` - :envvar:`OTEL_EXPORTER_JAEGER_AGENT_PORT` - :envvar:`OTEL_EXPORTER_JAEGER_AGENT_HOST` +- :envvar:`OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES` API --- @@ -78,20 +79,15 @@ from os import environ from typing import Optional -from grpc import ChannelCredentials, insecure_channel, secure_channel - from opentelemetry import trace -from opentelemetry.exporter.jaeger import util -from opentelemetry.exporter.jaeger.gen import model_pb2 -from opentelemetry.exporter.jaeger.gen.collector_pb2 import PostSpansRequest -from opentelemetry.exporter.jaeger.gen.collector_pb2_grpc import ( - CollectorServiceStub, +from opentelemetry.exporter.jaeger.thrift.gen.jaeger import ( + Collector as jaeger_thrift, +) +from opentelemetry.exporter.jaeger.thrift.send import AgentClientUDP, Collector +from opentelemetry.exporter.jaeger.thrift.translate import ( + ThriftTranslator, + Translate, ) -from opentelemetry.exporter.jaeger.gen.jaeger import Collector as jaeger_thrift -from opentelemetry.exporter.jaeger.send.thrift import AgentClientUDP, Collector -from opentelemetry.exporter.jaeger.translate import Translate -from opentelemetry.exporter.jaeger.translate.protobuf import ProtobufTranslator -from opentelemetry.exporter.jaeger.translate.thrift import ThriftTranslator from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_JAEGER_AGENT_HOST, OTEL_EXPORTER_JAEGER_AGENT_PORT, @@ -105,12 +101,6 @@ DEFAULT_AGENT_HOST_NAME = "localhost" DEFAULT_AGENT_PORT = 6831 -DEFAULT_GRPC_COLLECTOR_ENDPOINT = "localhost:14250" - -UDP_PACKET_MAX_LENGTH = 65000 - -TRANSPORT_FORMAT_THRIFT = "thrift" -TRANSPORT_FORMAT_PROTOBUF = "protobuf" logger = logging.getLogger(__name__) @@ -122,14 +112,11 @@ class JaegerExporter(SpanExporter): agent_host_name: The host name of the Jaeger-Agent. agent_port: The port of the Jaeger-Agent. collector_endpoint: The endpoint of the Jaeger collector that uses - Thrift over HTTP/HTTPS or Protobuf via gRPC. + Thrift over HTTP/HTTPS. username: The user name of the Basic Auth if authentication is required. password: The password of the Basic Auth if authentication is required. - insecure: True if collector has no encryption or authentication - credentials: Credentials for server authentication. - transport_format: Transport format for exporting spans to collector. max_tag_value_length: Max length string attribute values can have. Set to None to disable. udp_split_oversized_batches: Re-emit oversized batches in smaller chunks. """ @@ -141,9 +128,6 @@ def __init__( collector_endpoint: Optional[str] = None, username: Optional[str] = None, password: Optional[str] = None, - insecure: Optional[bool] = None, - credentials: Optional[ChannelCredentials] = None, - transport_format: Optional[str] = None, max_tag_value_length: Optional[int] = None, udp_split_oversized_batches: bool = None, ): @@ -192,14 +176,6 @@ def __init__( default=None, ) self._collector = None - self._grpc_client = None - self.insecure = insecure - self.credentials = util._get_credentials(credentials) - self.transport_format = ( - transport_format.lower() - if transport_format - else TRANSPORT_FORMAT_THRIFT - ) tracer_provider = trace.get_tracer_provider() self.service_name = ( tracer_provider.resource.attributes[SERVICE_NAME] @@ -207,33 +183,12 @@ def __init__( else Resource.create().attributes.get(SERVICE_NAME) ) - @property - def _collector_grpc_client(self) -> Optional[CollectorServiceStub]: - if self.transport_format != TRANSPORT_FORMAT_PROTOBUF: - return None - - endpoint = self.collector_endpoint or DEFAULT_GRPC_COLLECTOR_ENDPOINT - - if self._grpc_client is None: - if self.insecure: - self._grpc_client = CollectorServiceStub( - insecure_channel(endpoint) - ) - else: - self._grpc_client = CollectorServiceStub( - secure_channel(endpoint, self.credentials) - ) - return self._grpc_client - @property def _collector_http_client(self) -> Optional[Collector]: if self._collector is not None: return self._collector - if ( - self.collector_endpoint is None - or self.transport_format != TRANSPORT_FORMAT_THRIFT - ): + if self.collector_endpoint is None: return None auth = None @@ -248,25 +203,16 @@ def _collector_http_client(self) -> Optional[Collector]: def export(self, spans) -> SpanExportResult: translator = Translate(spans) - if self.transport_format == TRANSPORT_FORMAT_PROTOBUF: - pb_translator = ProtobufTranslator( - self.service_name, self._max_tag_value_length - ) - jaeger_spans = translator._translate(pb_translator) - batch = model_pb2.Batch(spans=jaeger_spans) - request = PostSpansRequest(batch=batch) - self._collector_grpc_client.PostSpans(request) + thrift_translator = ThriftTranslator(self._max_tag_value_length) + jaeger_spans = translator._translate(thrift_translator) + batch = jaeger_thrift.Batch( + spans=jaeger_spans, + process=jaeger_thrift.Process(serviceName=self.service_name), + ) + if self._collector_http_client is not None: + self._collector_http_client.submit(batch) else: - thrift_translator = ThriftTranslator(self._max_tag_value_length) - jaeger_spans = translator._translate(thrift_translator) - batch = jaeger_thrift.Batch( - spans=jaeger_spans, - process=jaeger_thrift.Process(serviceName=self.service_name), - ) - if self._collector_http_client is not None: - self._collector_http_client.submit(batch) - else: - self._agent_client.emit(batch) + self._agent_client.emit(batch) return SpanExportResult.SUCCESS diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/__init__.py new file mode 100644 index 0000000000..52b3cfb3e9 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/__init__.py @@ -0,0 +1,4 @@ + +import sys +from os.path import dirname +sys.path.append(dirname(__file__)) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/Agent-remote b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/Agent-remote similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/Agent-remote rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/Agent-remote diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/Agent.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/Agent.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/Agent.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/Agent.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/__init__.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/__init__.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/constants.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/constants.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/constants.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/constants.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/ttypes.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/ttypes.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/agent/ttypes.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/ttypes.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/Collector-remote b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/Collector-remote similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/Collector-remote rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/Collector-remote diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/Collector.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/Collector.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/Collector.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/Collector.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/__init__.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/__init__.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/constants.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/constants.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/constants.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/constants.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/ttypes.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/ttypes.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/jaeger/ttypes.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/ttypes.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/ZipkinCollector-remote b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ZipkinCollector-remote similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/ZipkinCollector-remote rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ZipkinCollector-remote diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/ZipkinCollector.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ZipkinCollector.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/ZipkinCollector.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ZipkinCollector.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/__init__.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/__init__.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/constants.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/constants.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/constants.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/constants.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/ttypes.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ttypes.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/zipkincore/ttypes.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ttypes.py diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/thrift.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py similarity index 96% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/thrift.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py index fe4f463319..a04b09c4ac 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/send/thrift.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py @@ -20,8 +20,8 @@ from thrift.protocol import TBinaryProtocol, TCompactProtocol from thrift.transport import THttpClient, TTransport -from opentelemetry.exporter.jaeger.gen.agent import Agent as agent -from opentelemetry.exporter.jaeger.gen.jaeger import Collector as jaeger +from opentelemetry.exporter.jaeger.thrift.gen.agent import Agent as agent +from opentelemetry.exporter.jaeger.thrift.gen.jaeger import Collector as jaeger UDP_PACKET_MAX_LENGTH = 65000 diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py similarity index 77% rename from exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py index 45129601ac..c48c8bde8f 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py @@ -11,21 +11,90 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=no-self-use + +import abc from typing import Optional, Sequence -from opentelemetry.exporter.jaeger.gen.jaeger import Collector as TCollector -from opentelemetry.exporter.jaeger.translate import ( - NAME_KEY, - OTLP_JAEGER_SPAN_KIND, - VERSION_KEY, - Translator, - _convert_int_to_i64, - _nsec_to_usec_round, +from opentelemetry.exporter.jaeger.thrift.gen.jaeger import ( + Collector as TCollector, ) from opentelemetry.sdk.trace import ReadableSpan, StatusCode +from opentelemetry.trace import SpanKind from opentelemetry.util import types +OTLP_JAEGER_SPAN_KIND = { + SpanKind.CLIENT: "client", + SpanKind.SERVER: "server", + SpanKind.CONSUMER: "consumer", + SpanKind.PRODUCER: "producer", + SpanKind.INTERNAL: "internal", +} + +NAME_KEY = "otel.library.name" +VERSION_KEY = "otel.library.version" + + +def _nsec_to_usec_round(nsec: int) -> int: + """Round nanoseconds to microseconds""" + return (nsec + 500) // 10 ** 3 + + +def _convert_int_to_i64(val): + """Convert integer to signed int64 (i64)""" + if val > 0x7FFFFFFFFFFFFFFF: + val -= 0x10000000000000000 + return val + + +class Translator(abc.ABC): + def __init__(self, max_tag_value_length: Optional[int] = None): + self._max_tag_value_length = max_tag_value_length + + @abc.abstractmethod + def _translate_span(self, span): + """Translates span to jaeger format. + + Args: + span: span to translate + """ + + @abc.abstractmethod + def _extract_tags(self, span): + """Extracts tags from span and returns list of jaeger Tags. + + Args: + span: span to extract tags + """ + + @abc.abstractmethod + def _extract_refs(self, span): + """Extracts references from span and returns list of jaeger SpanRefs. + + Args: + span: span to extract references + """ + + @abc.abstractmethod + def _extract_logs(self, span): + """Extracts logs from span and returns list of jaeger Logs. + + Args: + span: span to extract logs + """ + + +class Translate: + def __init__(self, spans): + self.spans = spans + + def _translate(self, translator: Translator): + translated_spans = [] + for span in self.spans: + # pylint: disable=protected-access + translated_span = translator._translate_span(span) + translated_spans.append(translated_span) + return translated_spans + def _get_string_tag(key, value: str) -> TCollector.Tag: """Returns jaeger string tag.""" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py new file mode 100644 index 0000000000..15eb84e8c0 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -0,0 +1,16 @@ +# Copyright 2019, OpenCensus Authors +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "1.0.0.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/tests/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/tests/certs/cred.cert b/exporter/opentelemetry-exporter-jaeger-thrift/tests/certs/cred.cert new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py similarity index 97% rename from exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py rename to exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py index d777615739..9fcd69b817 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py @@ -19,11 +19,13 @@ # pylint:disable=no-name-in-module # pylint:disable=import-error -import opentelemetry.exporter.jaeger as jaeger_exporter +import opentelemetry.exporter.jaeger.thrift as jaeger_exporter from opentelemetry import trace as trace_api -from opentelemetry.exporter.jaeger.gen.jaeger import ttypes as jaeger -from opentelemetry.exporter.jaeger.translate import Translate -from opentelemetry.exporter.jaeger.translate.thrift import ThriftTranslator +from opentelemetry.exporter.jaeger.thrift.gen.jaeger import ttypes as jaeger +from opentelemetry.exporter.jaeger.thrift.translate import ( + ThriftTranslator, + Translate, +) from opentelemetry.sdk import trace from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_JAEGER_AGENT_HOST, @@ -53,7 +55,7 @@ def setUp(self): self._test_span.end() # pylint: disable=protected-access - @patch("opentelemetry.exporter.jaeger.trace._TRACER_PROVIDER", None) + @patch("opentelemetry.exporter.jaeger.thrift.trace._TRACER_PROVIDER", None) def test_constructor_default(self): # pylint: disable=protected-access """Test the default values assigned by constructor.""" @@ -78,7 +80,7 @@ def test_constructor_default(self): self.assertTrue(exporter._agent_client is not None) self.assertIsNone(exporter._max_tag_value_length) - @patch("opentelemetry.exporter.jaeger.trace._TRACER_PROVIDER", None) + @patch("opentelemetry.exporter.jaeger.thrift.trace._TRACER_PROVIDER", None) def test_constructor_explicit(self): # pylint: disable=protected-access """Test the constructor passing all the options.""" @@ -123,7 +125,7 @@ def test_constructor_explicit(self): self.assertTrue(exporter._collector_http_client.auth is None) self.assertEqual(exporter._max_tag_value_length, 42) - @patch("opentelemetry.exporter.jaeger.trace._TRACER_PROVIDER", None) + @patch("opentelemetry.exporter.jaeger.thrift.trace._TRACER_PROVIDER", None) def test_constructor_by_environment_variables(self): # pylint: disable=protected-access """Test the constructor using Environment Variables.""" @@ -176,7 +178,7 @@ def test_constructor_by_environment_variables(self): self.assertTrue(exporter._collector_http_client.auth is None) environ_patcher.stop() - @patch("opentelemetry.exporter.jaeger.trace._TRACER_PROVIDER", None) + @patch("opentelemetry.exporter.jaeger.thrift.trace._TRACER_PROVIDER", None) def test_constructor_with_no_traceprovider_resource(self): """Test the constructor when there is no resource attached to trace_provider""" @@ -456,7 +458,7 @@ def test_translate_to_jaeger(self): self.assertEqual(spans, expected_spans) - @patch("opentelemetry.exporter.jaeger.trace._TRACER_PROVIDER", None) + @patch("opentelemetry.exporter.jaeger.thrift.trace._TRACER_PROVIDER", None) def test_export(self): """Test that agent and/or collector are invoked""" diff --git a/exporter/opentelemetry-exporter-jaeger/thrift/agent.thrift b/exporter/opentelemetry-exporter-jaeger-thrift/thrift/agent.thrift similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/thrift/agent.thrift rename to exporter/opentelemetry-exporter-jaeger-thrift/thrift/agent.thrift diff --git a/exporter/opentelemetry-exporter-jaeger/thrift/jaeger.thrift b/exporter/opentelemetry-exporter-jaeger-thrift/thrift/jaeger.thrift similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/thrift/jaeger.thrift rename to exporter/opentelemetry-exporter-jaeger-thrift/thrift/jaeger.thrift diff --git a/exporter/opentelemetry-exporter-jaeger/thrift/zipkincore.thrift b/exporter/opentelemetry-exporter-jaeger-thrift/thrift/zipkincore.thrift similarity index 100% rename from exporter/opentelemetry-exporter-jaeger/thrift/zipkincore.thrift rename to exporter/opentelemetry-exporter-jaeger-thrift/thrift/zipkincore.thrift diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index d2a446a05f..e904eb7a2d 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -14,7 +14,7 @@ # [metadata] name = opentelemetry-exporter-jaeger -description = Jaeger Exporter for OpenTelemetry +description = Jaeger Exporters for OpenTelemetry long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors @@ -32,25 +32,15 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >=3.5 -package_dir= - =src + packages=find_namespace: install_requires = - grpcio >= 1.0.0, < 2.0.0 - googleapis-common-protos ~= 1.52.0 - thrift >= 0.10.0 - opentelemetry-api == 1.0.0.dev0 - opentelemetry-sdk == 1.0.0.dev0 - -[options.packages.find] -where = src + opentelemetry-exporter-jaeger-proto == 1.0.0.dev0 + opentelemetry-exporter-jaeger-thrift == 1.0.0.dev0 [options.extras_require] test = - -[options.entry_points] -opentelemetry_exporter = - jaeger = opentelemetry.exporter.jaeger:JaegerExporter \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py deleted file mode 100644 index c60820085a..0000000000 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import abc -from typing import Optional - -from opentelemetry.trace import SpanKind - -OTLP_JAEGER_SPAN_KIND = { - SpanKind.CLIENT: "client", - SpanKind.SERVER: "server", - SpanKind.CONSUMER: "consumer", - SpanKind.PRODUCER: "producer", - SpanKind.INTERNAL: "internal", -} - -NAME_KEY = "otel.library.name" -VERSION_KEY = "otel.library.version" - - -def _nsec_to_usec_round(nsec: int) -> int: - """Round nanoseconds to microseconds""" - return (nsec + 500) // 10 ** 3 - - -def _convert_int_to_i64(val): - """Convert integer to signed int64 (i64)""" - if val > 0x7FFFFFFFFFFFFFFF: - val -= 0x10000000000000000 - return val - - -class Translator(abc.ABC): - def __init__(self, max_tag_value_length: Optional[int] = None): - self._max_tag_value_length = max_tag_value_length - - @abc.abstractmethod - def _translate_span(self, span): - """Translates span to jaeger format. - - Args: - span: span to translate - """ - - @abc.abstractmethod - def _extract_tags(self, span): - """Extracts tags from span and returns list of jaeger Tags. - - Args: - span: span to extract tags - """ - - @abc.abstractmethod - def _extract_refs(self, span): - """Extracts references from span and returns list of jaeger SpanRefs. - - Args: - span: span to extract references - """ - - @abc.abstractmethod - def _extract_logs(self, span): - """Extracts logs from span and returns list of jaeger Logs. - - Args: - span: span to extract logs - """ - - -class Translate: - def __init__(self, spans): - self.spans = spans - - def _translate(self, translator: Translator): - translated_spans = [] - for span in self.spans: - # pylint: disable=protected-access - translated_span = translator._translate_span(span) - translated_spans.append(translated_span) - return translated_spans diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py new file mode 100644 index 0000000000..9f199348ba --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +# pylint:disable=no-name-in-module +# pylint:disable=import-error +from opentelemetry.exporter.jaeger import proto, thrift + + +# pylint:disable=no-member +class TestJaegerExporter(unittest.TestCase): + def test_constructors(self): + """ Test ensures both exporters can co-exist""" + try: + proto.JaegerExporter() + thrift.JaegerExporter() + except Exception as exc: # pylint: disable=broad-except + self.assertIsNone(exc) diff --git a/pyproject.toml b/pyproject.toml index 5207dd223a..d45615a1e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,8 @@ line-length = 79 exclude = ''' ( /( # generated files - exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen| + exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen| + exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen| exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen| opentelemetry-proto/src/opentelemetry/proto/collector| opentelemetry-proto/src/opentelemetry/proto/common| diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 00d98a0ca8..aedfbd2966 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -34,7 +34,8 @@ cov opentelemetry-sdk cov exporter/opentelemetry-exporter-datadog cov instrumentation/opentelemetry-instrumentation-flask cov instrumentation/opentelemetry-instrumentation-requests -cov exporter/opentelemetry-exporter-jaeger +cov exporter/opentelemetry-exporter-jaeger-proto +cov exporter/opentelemetry-exporter-jaeger-thrift cov instrumentation/opentelemetry-instrumentation-opentracing-shim cov util/opentelemetry-util-http cov exporter/opentelemetry-exporter-zipkin diff --git a/tox.ini b/tox.ini index c1f7e7aca2..556430bfae 100644 --- a/tox.ini +++ b/tox.ini @@ -29,7 +29,13 @@ envlist = pypy3-test-core-distro ; opentelemetry-exporter-jaeger - py3{5,6,7,8,9}-test-exporter-jaeger + py3{5,6,7,8,9}-test-exporter-jaeger-combined + + ; opentelemetry-exporter-jaeger-proto + py3{5,6,7,8,9}-test-exporter-jaeger-proto + + ; opentelemetry-exporter-jaeger-thrift + py3{5,6,7,8,9}-test-exporter-jaeger-thrift ; opentelemetry-exporter-opencensus py3{5,6,7,8,9}-test-exporter-opencensus @@ -81,7 +87,9 @@ changedir = test-core-opentracing-shim: shim/opentelemetry-opentracing-shim/tests test-core-distro: opentelemetry-distro/tests - test-exporter-jaeger: exporter/opentelemetry-exporter-jaeger/tests + test-exporter-jaeger-combined: exporter/opentelemetry-exporter-jaeger/tests + test-exporter-jaeger-proto: exporter/opentelemetry-exporter-jaeger-proto/tests + test-exporter-jaeger-thrift: exporter/opentelemetry-exporter-jaeger-thrift/tests test-exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests test-exporter-otlp: exporter/opentelemetry-exporter-otlp/tests test-exporter-zipkin: exporter/opentelemetry-exporter-zipkin/tests @@ -107,7 +115,9 @@ commands_pre = otlp: pip install {toxinidir}/opentelemetry-proto otlp: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp - exporter-jaeger: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger + exporter-jaeger-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift {toxinidir}/exporter/opentelemetry-exporter-jaeger + exporter-jaeger-proto: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto + exporter-jaeger-thrift: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift opentracing-shim: pip install {toxinidir}/opentelemetry-sdk opentracing-shim: pip install {toxinidir}/shim/opentelemetry-opentracing-shim @@ -159,6 +169,8 @@ commands_pre = python -m pip install -e {toxinidir}/opentelemetry-proto[test] python -m pip install -e {toxinidir}/tests/util[test] python -m pip install -e {toxinidir}/shim/opentelemetry-opentracing-shim[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-opencensus[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp[test] From 94180137e9e01fe281896e0200e58156bcfb30cc Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 17 Mar 2021 08:58:16 -0700 Subject: [PATCH 0818/1517] fix wrong argument (#1702) --- docs/examples/auto-instrumentation/README.rst | 2 +- opentelemetry-instrumentation/README.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index f21ab8f566..607aa1b44b 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -151,7 +151,7 @@ and run the following command instead: .. code:: sh - $ opentelemetry-instrument -e console_span python server_uninstrumented.py + $ opentelemetry-instrument --trace-exporter console_span python server_uninstrumented.py In the console where you previously executed ``client.py``, run the following command again: diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index 7261c177f8..78b6d79087 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -88,13 +88,13 @@ Examples :: - opentelemetry-instrument -e otlp flask run --port=3000 + opentelemetry-instrument --trace-exporter otlp flask run --port=3000 -The above command will pass ``-e otlp`` to the instrument command and ``--port=3000`` to ``flask run``. +The above command will pass ``--trace-exporter otlp`` to the instrument command and ``--port=3000`` to ``flask run``. :: - opentelemetry-instrument -e zipkin,otlp celery -A tasks worker --loglevel=info + opentelemetry-instrument --trace-exporter zipkin,otlp celery -A tasks worker --loglevel=info The above command will configure global trace provider, attach zipkin and otlp exporters to it and then start celery with the rest of the arguments. From 77ff0702c405805cb732d621109364e79ba944fa Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 17 Mar 2021 10:02:51 -0700 Subject: [PATCH 0819/1517] split otlp exporter (#1695) --- CHANGELOG.md | 4 + docs/examples/fork-process-model/README.rst | 4 +- .../flask-gunicorn/gunicorn.config.py | 4 +- .../fork-process-model/flask-uwsgi/app.py | 4 +- docs/exporter/otlp/otlp.rst | 9 +- docs/getting_started/otlpcollector_example.py | 4 +- .../LICENSE | 201 ++++++++++++++++++ .../MANIFEST.in | 9 + .../README.rst | 25 +++ .../setup.cfg | 59 +++++ .../setup.py | 33 +++ .../exporter/otlp/proto/grpc}/__init__.py | 2 +- .../exporter/otlp/proto/grpc}/exporter.py | 0 .../proto/grpc}/trace_exporter/__init__.py | 2 +- .../exporter/otlp/proto/grpc/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/fixtures/test.cert | 0 .../test_benchmark_trace_exporter.py | 8 +- .../tests/test_otlp_exporter_mixin.py | 2 +- .../tests/test_otlp_trace_exporter.py | 46 ++-- .../opentelemetry-exporter-otlp/README.rst | 14 +- .../opentelemetry-exporter-otlp/setup.cfg | 22 +- .../tests/test_otlp.py | 29 +++ .../src/opentelemetry/distro/__init__.py | 4 +- opentelemetry-distro/tests/test_distro.py | 4 +- opentelemetry-instrumentation/README.rst | 4 +- .../profile_resource_usage_batch_export.py | 4 +- .../profile_resource_usage_simple_export.py | 4 +- .../test_otlp_grpc_exporter_functional.py | 4 +- tox.ini | 23 +- 30 files changed, 476 insertions(+), 67 deletions(-) create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/LICENSE create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/README.rst create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/setup.py rename exporter/{opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp => opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc}/__init__.py (96%) rename exporter/{opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp => opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc}/exporter.py (100%) rename exporter/{opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp => opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc}/trace_exporter/__init__.py (99%) create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py rename exporter/{opentelemetry-exporter-otlp => opentelemetry-exporter-otlp-proto-grpc}/tests/__init__.py (100%) rename exporter/{opentelemetry-exporter-otlp => opentelemetry-exporter-otlp-proto-grpc}/tests/fixtures/test.cert (100%) rename exporter/{opentelemetry-exporter-otlp => opentelemetry-exporter-otlp-proto-grpc}/tests/performance/benchmarks/test_benchmark_trace_exporter.py (90%) rename exporter/{opentelemetry-exporter-otlp => opentelemetry-exporter-otlp-proto-grpc}/tests/test_otlp_exporter_mixin.py (96%) rename exporter/{opentelemetry-exporter-otlp => opentelemetry-exporter-otlp-proto-grpc}/tests/test_otlp_trace_exporter.py (92%) create mode 100644 exporter/opentelemetry-exporter-otlp/tests/test_otlp.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 30aeb1209e..918c5e502a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Split up `opentelemetry-exporter-jaeger` package into `opentelemetry-exporter-jaeger-proto` and `opentelemetry-exporter-jaeger-thrift` packages to reduce dependencies for each one. ([#1694](https://github.com/open-telemetry/opentelemetry-python/pull/1694)) +- Added `opentelemetry-exporter-otlp-proto-grpc` and changed `opentelemetry-exporter-otlp` to + install it as a dependency. This will allow for the next package/protocol to also be in + its own package. + ([#1695](https://github.com/open-telemetry/opentelemetry-python/pull/1695)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. diff --git a/docs/examples/fork-process-model/README.rst b/docs/examples/fork-process-model/README.rst index ba09530f93..2bd0c9ecbc 100644 --- a/docs/examples/fork-process-model/README.rst +++ b/docs/examples/fork-process-model/README.rst @@ -16,7 +16,7 @@ Gunicorn post_fork hook .. code-block:: python from opentelemetry import trace - from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor @@ -44,7 +44,7 @@ uWSGI postfork decorator from uwsgidecorators import postfork from opentelemetry import trace - from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor diff --git a/docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py b/docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py index 237f5244b3..eaf6777392 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py +++ b/docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py @@ -13,7 +13,9 @@ # limitations under the License. from opentelemetry import trace -from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor diff --git a/docs/examples/fork-process-model/flask-uwsgi/app.py b/docs/examples/fork-process-model/flask-uwsgi/app.py index d7683e5916..adcf52691f 100644 --- a/docs/examples/fork-process-model/flask-uwsgi/app.py +++ b/docs/examples/fork-process-model/flask-uwsgi/app.py @@ -17,7 +17,9 @@ from uwsgidecorators import postfork from opentelemetry import trace -from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider diff --git a/docs/exporter/otlp/otlp.rst b/docs/exporter/otlp/otlp.rst index 7663ec9489..e4bfdd07a1 100644 --- a/docs/exporter/otlp/otlp.rst +++ b/docs/exporter/otlp/otlp.rst @@ -1,7 +1,12 @@ -Opentelemetry OTLP Exporter -=========================== +Opentelemetry OTLP Exporters +============================ .. automodule:: opentelemetry.exporter.otlp :members: :undoc-members: :show-inheritance: + +.. automodule:: opentelemetry.exporter.otlp.proto.grpc + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/getting_started/otlpcollector_example.py b/docs/getting_started/otlpcollector_example.py index 4ca7ade35f..f4ada80f19 100644 --- a/docs/getting_started/otlpcollector_example.py +++ b/docs/getting_started/otlpcollector_example.py @@ -16,7 +16,9 @@ import time from opentelemetry import trace -from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/LICENSE b/exporter/opentelemetry-exporter-otlp-proto-grpc/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/MANIFEST.in b/exporter/opentelemetry-exporter-otlp-proto-grpc/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/README.rst b/exporter/opentelemetry-exporter-otlp-proto-grpc/README.rst new file mode 100644 index 0000000000..279e1aed21 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/README.rst @@ -0,0 +1,25 @@ +OpenTelemetry Collector Protobuf over gRPC Exporter +=================================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp-proto-grpc.svg + :target: https://pypi.org/project/opentelemetry-exporter-otlp-proto-grpc/ + +This library allows to export data to the OpenTelemetry Collector using the OpenTelemetry Protocol using Protobuf over gRPC. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-otlp-proto-grpc + + +References +---------- + +* `OpenTelemetry Collector Exporter `_ +* `OpenTelemetry Collector `_ +* `OpenTelemetry `_ +* `OpenTelemetry Protocol Specification `_ diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg new file mode 100644 index 0000000000..cfb450d897 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -0,0 +1,59 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-exporter-otlp-proto-grpc +description = OpenTelemetry Collector Protobuf over gRPC Exporter +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-grpc +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + grpcio >= 1.0.0, < 2.0.0 + googleapis-common-protos ~= 1.52.0 + opentelemetry-api == 1.0.0.dev0 + opentelemetry-sdk == 1.0.0.dev0 + opentelemetry-proto == 1.0.0.dev0 + backoff ~= 1.10.0 + +[options.extras_require] +test = + pytest-grpc + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_exporter = + otlp_proto_grpc_span = opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.py new file mode 100644 index 0000000000..616edde878 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.py @@ -0,0 +1,33 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "exporter", + "otlp", + "proto", + "grpc", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py similarity index 96% rename from exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py rename to exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py index 705f14d7c2..c4c96b76c0 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py @@ -43,7 +43,7 @@ .. code:: python from opentelemetry import trace - from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py rename to exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py similarity index 99% rename from exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py rename to exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index a8b3cc79b4..518fc5551f 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -19,7 +19,7 @@ from grpc import ChannelCredentials, Compression -from opentelemetry.exporter.otlp.exporter import ( +from opentelemetry.exporter.otlp.proto.grpc.exporter import ( OTLPExporterMixin, _get_credentials, _translate_key_values, diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py new file mode 100644 index 0000000000..e4529dffc6 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "1.0.0.dev0" diff --git a/exporter/opentelemetry-exporter-otlp/tests/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp/tests/__init__.py rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/__init__.py diff --git a/exporter/opentelemetry-exporter-otlp/tests/fixtures/test.cert b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixtures/test.cert similarity index 100% rename from exporter/opentelemetry-exporter-otlp/tests/fixtures/test.cert rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixtures/test.cert diff --git a/exporter/opentelemetry-exporter-otlp/tests/performance/benchmarks/test_benchmark_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/benchmarks/test_benchmark_trace_exporter.py similarity index 90% rename from exporter/opentelemetry-exporter-otlp/tests/performance/benchmarks/test_benchmark_trace_exporter.py rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/benchmarks/test_benchmark_trace_exporter.py index 7434a0d8d0..201a29fe7c 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/performance/benchmarks/test_benchmark_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/benchmarks/test_benchmark_trace_exporter.py @@ -14,7 +14,9 @@ from unittest.mock import patch -from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) from opentelemetry.sdk.trace import TracerProvider, sampling from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, @@ -36,7 +38,7 @@ def __init__(self, channel): @patch( - "opentelemetry.exporter.otlp.trace_exporter.OTLPSpanExporter._stub", + "opentelemetry.exporter.otlp.proto.grpc.trace_exporter.OTLPSpanExporter._stub", new=MockTraceServiceStub, ) def test_simple_span_processor(benchmark): @@ -55,7 +57,7 @@ def create_spans_to_be_exported(): @patch( - "opentelemetry.exporter.otlp.trace_exporter.OTLPSpanExporter._stub", + "opentelemetry.exporter.otlp.proto.grpc.trace_exporter.OTLPSpanExporter._stub", new=MockTraceServiceStub, ) def test_batch_span_processor(benchmark): diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py similarity index 96% rename from exporter/opentelemetry-exporter-otlp/tests/test_otlp_exporter_mixin.py rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index e9c9e3f9d2..7ea53ddc06 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -17,7 +17,7 @@ from grpc import Compression -from opentelemetry.exporter.otlp.exporter import ( +from opentelemetry.exporter.otlp.proto.grpc.exporter import ( InvalidCompressionValueException, environ_to_compression, ) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py similarity index 92% rename from exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 254f7dc315..ed22cbdbea 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -22,7 +22,9 @@ from google.rpc.error_details_pb2 import RetryInfo from grpc import ChannelCredentials, Compression, StatusCode, server -from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest, ExportTraceServiceResponse, @@ -179,7 +181,9 @@ def tearDown(self): OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip", }, ) - @patch("opentelemetry.exporter.otlp.exporter.OTLPExporterMixin.__init__") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__" + ) def test_env_variables(self, mock_exporter_mixin): OTLPSpanExporter() @@ -193,9 +197,13 @@ def test_env_variables(self, mock_exporter_mixin): self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) - @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") - @patch("opentelemetry.exporter.otlp.exporter.secure_channel") - @patch("opentelemetry.exporter.otlp.trace_exporter.OTLPSpanExporter._stub") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" + ) + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.trace_exporter.OTLPSpanExporter._stub" + ) # pylint: disable=unused-argument def test_no_credentials_error( self, mock_ssl_channel, mock_secure, mock_stub @@ -207,8 +215,10 @@ def test_no_credentials_error( "os.environ", {OTEL_EXPORTER_OTLP_TRACES_HEADERS: "key1=value1,key2=value2"}, ) - @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") - @patch("opentelemetry.exporter.otlp.exporter.secure_channel") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" + ) + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") # pylint: disable=unused-argument def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): exporter = OTLPSpanExporter() @@ -225,7 +235,7 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): ) # pylint: disable=no-self-use - @patch("opentelemetry.exporter.otlp.exporter.insecure_channel") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) def test_otlp_exporter_otlp_compression_envvar( self, mock_insecure_channel @@ -237,7 +247,7 @@ def test_otlp_exporter_otlp_compression_envvar( ) # pylint: disable=no-self-use - @patch("opentelemetry.exporter.otlp.exporter.insecure_channel") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) def test_otlp_exporter_otlp_compression_kwarg(self, mock_insecure_channel): """Specifying kwarg should take precedence over env""" @@ -247,7 +257,7 @@ def test_otlp_exporter_otlp_compression_kwarg(self, mock_insecure_channel): ) # pylint: disable=no-self-use - @patch("opentelemetry.exporter.otlp.exporter.insecure_channel") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch.dict("os.environ", {}) def test_otlp_exporter_otlp_compression_unspecified( self, mock_insecure_channel @@ -259,7 +269,7 @@ def test_otlp_exporter_otlp_compression_unspecified( ) # pylint: disable=no-self-use - @patch("opentelemetry.exporter.otlp.exporter.insecure_channel") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip"}, ) @@ -274,16 +284,18 @@ def test_otlp_exporter_otlp_compression_precendence( "localhost:4317", compression=Compression.Gzip ) - @patch("opentelemetry.exporter.otlp.exporter.ssl_channel_credentials") - @patch("opentelemetry.exporter.otlp.exporter.secure_channel") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" + ) + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") # pylint: disable=unused-argument def test_otlp_headers(self, mock_ssl_channel, mock_secure): exporter = OTLPSpanExporter() # pylint: disable=protected-access self.assertIsNone(exporter._headers, None) - @patch("opentelemetry.exporter.otlp.exporter.expo") - @patch("opentelemetry.exporter.otlp.exporter.sleep") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): mock_expo.configure_mock(**{"return_value": [1]}) @@ -296,8 +308,8 @@ def test_unavailable(self, mock_sleep, mock_expo): ) mock_sleep.assert_called_with(1) - @patch("opentelemetry.exporter.otlp.exporter.expo") - @patch("opentelemetry.exporter.otlp.exporter.sleep") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable_delay(self, mock_sleep, mock_expo): mock_expo.configure_mock(**{"return_value": [1]}) diff --git a/exporter/opentelemetry-exporter-otlp/README.rst b/exporter/opentelemetry-exporter-otlp/README.rst index bd3b581e1d..06f45540de 100644 --- a/exporter/opentelemetry-exporter-otlp/README.rst +++ b/exporter/opentelemetry-exporter-otlp/README.rst @@ -1,12 +1,20 @@ -OpenTelemetry Collector Exporter -================================ +OpenTelemetry Collector Exporters +================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp.svg :target: https://pypi.org/project/opentelemetry-exporter-otlp/ -This library allows to export data to the OpenTelemetry Collector using the OpenTelemetry Protocol. +This library is provided as a convenience to install all supported OpenTelemetry Collector Exporters. Currently it installs: +* opentelemetry-exporter-otlp-proto-grpc + +In the future, additional packages will be available: +* opentelemetry-exporter-otlp-proto-http +* opentelemetry-exporter-otlp-json-http + +To avoid unnecessary dependencies, users should install the specific package once they've determined their +preferred serialization and protocol method. Installation ------------ diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 612529ff3e..64d94a91a4 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -14,7 +14,7 @@ # [metadata] name = opentelemetry-exporter-otlp -description = OpenTelemetry Collector Exporter +description = OpenTelemetry Collector Exporters long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors @@ -36,24 +36,6 @@ classifiers = [options] python_requires = >=3.5 -package_dir= - =src packages=find_namespace: install_requires = - grpcio >= 1.0.0, < 2.0.0 - googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 1.0.0.dev0 - opentelemetry-sdk == 1.0.0.dev0 - opentelemetry-proto == 1.0.0.dev0 - backoff ~= 1.10.0 - -[options.extras_require] -test = - pytest-grpc - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_exporter = - otlp_span = opentelemetry.exporter.otlp.trace_exporter:OTLPSpanExporter + opentelemetry-exporter-otlp-proto-grpc == 1.0.0.dev0 diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py new file mode 100644 index 0000000000..ab3173928c --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py @@ -0,0 +1,29 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) + + +class TestOTLPExporters(unittest.TestCase): + def test_constructors(self): + try: + OTLPSpanExporter() + except Exception: # pylint: disable=broad-except + self.fail( + "Unexpected exception raised when instantiating OTLPSpanExporter" + ) diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index 1adfe56ac7..576f44a506 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -35,7 +35,7 @@ EXPORTER_OTLP = "otlp" -EXPORTER_OTLP_SPAN = "otlp_span" +EXPORTER_OTLP_SPAN = "otlp_proto_grpc_span" RANDOM_ID_GENERATOR = "random" _DEFAULT_ID_GENERATOR = RANDOM_ID_GENERATOR @@ -159,4 +159,4 @@ class OpenTelemetryDistro(BaseDistro): """ def _configure(self, **kwargs): - os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp_span") + os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp_proto_grpc_span") diff --git a/opentelemetry-distro/tests/test_distro.py b/opentelemetry-distro/tests/test_distro.py index 3e4aa799e2..2e42ed904a 100644 --- a/opentelemetry-distro/tests/test_distro.py +++ b/opentelemetry-distro/tests/test_distro.py @@ -33,4 +33,6 @@ def test_default_configuration(self): distro = OpenTelemetryDistro() self.assertIsNone(os.environ.get(OTEL_TRACES_EXPORTER)) distro.configure() - self.assertEqual("otlp_span", os.environ.get(OTEL_TRACES_EXPORTER)) + self.assertEqual( + "otlp_proto_grpc_span", os.environ.get(OTEL_TRACES_EXPORTER) + ) diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index 78b6d79087..6f74d2232f 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -63,10 +63,10 @@ Well known trace exporter names: - jaeger - opencensus - otlp - - otlp_span + - otlp_proto_grpc_span - zipkin -``otlp`` is an alias for ``otlp_span``. +``otlp`` is an alias for ``otlp_proto_grpc_span``. * ``--id-generator`` or ``OTEL_PYTHON_ID_GENERATOR`` diff --git a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py index 570965cd53..c4b16dd8de 100644 --- a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py +++ b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py @@ -15,7 +15,9 @@ import time from unittest.mock import patch -from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) from opentelemetry.sdk.trace import TracerProvider, sampling from opentelemetry.sdk.trace.export import BatchSpanProcessor diff --git a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py index 4636b31d5c..efad1a8407 100644 --- a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py +++ b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py @@ -15,7 +15,9 @@ import time from unittest.mock import patch -from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) from opentelemetry.sdk.trace import TracerProvider, sampling from opentelemetry.sdk.trace.export import SimpleSpanProcessor diff --git a/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py index b0992f42d8..789dcc7d10 100644 --- a/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py +++ b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py @@ -14,7 +14,9 @@ from opentelemetry import trace from opentelemetry.context import attach, detach, set_value -from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.test.test_base import TestBase diff --git a/tox.ini b/tox.ini index 556430bfae..07126f90fc 100644 --- a/tox.ini +++ b/tox.ini @@ -41,9 +41,13 @@ envlist = py3{5,6,7,8,9}-test-exporter-opencensus ; exporter-opencensus intentionally excluded from pypy3 - ; opentelemetry-exporter-otlp - py3{5,6,7,8,9}-test-exporter-otlp - ; exporter-otlp intentionally excluded from pypy3 + ; opentelemetry-exporter-otlp-combined + py3{5,6,7,8,9}-test-exporter-otlp-combined + ; intentionally excluded from pypy3 + + ; opentelemetry-exporter-otlp-proto-grpc + py3{5,6,7,8,9}-test-exporter-otlp-proto-grpc + ; intentionally excluded from pypy3 ; opentelemetry-exporter-zipkin py3{5,6,7,8,9}-test-exporter-zipkin @@ -91,7 +95,8 @@ changedir = test-exporter-jaeger-proto: exporter/opentelemetry-exporter-jaeger-proto/tests test-exporter-jaeger-thrift: exporter/opentelemetry-exporter-jaeger-thrift/tests test-exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests - test-exporter-otlp: exporter/opentelemetry-exporter-otlp/tests + test-exporter-otlp-combined: exporter/opentelemetry-exporter-otlp/tests + test-exporter-otlp-proto-grpc: exporter/opentelemetry-exporter-otlp-proto-grpc/tests test-exporter-zipkin: exporter/opentelemetry-exporter-zipkin/tests test-propagator-b3: propagator/opentelemetry-propagator-b3/tests @@ -112,8 +117,12 @@ commands_pre = opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus - otlp: pip install {toxinidir}/opentelemetry-proto - otlp: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp + exporter-otlp-combined: pip install {toxinidir}/opentelemetry-proto + exporter-otlp-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc + exporter-otlp-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp + + exporter-otlp-proto-grpc: pip install {toxinidir}/opentelemetry-proto + exporter-otlp-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc exporter-jaeger-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift {toxinidir}/exporter/opentelemetry-exporter-jaeger exporter-jaeger-proto: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto @@ -173,6 +182,7 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-opencensus[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-b3[test] @@ -228,6 +238,7 @@ commands_pre = -e {toxinidir}/tests/util \ -e {toxinidir}/exporter/opentelemetry-exporter-opencensus \ -e {toxinidir}/opentelemetry-proto \ + -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc \ -e {toxinidir}/exporter/opentelemetry-exporter-otlp docker-compose up -d commands = From 724c7920e71c7cce77cc331ee24659c30d42526c Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 17 Mar 2021 12:09:50 -0700 Subject: [PATCH 0820/1517] Populate service_name from exporting span in JaegerExporters (#1703) --- CHANGELOG.md | 2 + .../exporter/jaeger/proto/__init__.py | 9 +++++ .../tests/test_jaeger_exporter_protobuf.py | 39 ++++++++++++++++--- .../exporter/jaeger/thrift/__init__.py | 10 ++++- .../tests/test_jaeger_exporter_thrift.py | 25 +++++++++++- .../opentelemetry-exporter-jaeger/README.rst | 19 +++------ 6 files changed, 83 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 918c5e502a..050b822d09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 install it as a dependency. This will allow for the next package/protocol to also be in its own package. ([#1695](https://github.com/open-telemetry/opentelemetry-python/pull/1695)) +- Change Jaeger exporters to obtain service.name from span + ([#1703](https://github.com/open-telemetry/opentelemetry-python/pull/1703)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/__init__.py index 77a29fd09a..040fc99640 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/__init__.py @@ -145,6 +145,15 @@ def _collector_grpc_client(self) -> Optional[CollectorServiceStub]: return self._grpc_client def export(self, spans) -> SpanExportResult: + # Populate service_name from first span + # We restrict any SpanProcessor to be only associated with a single + # TracerProvider, so it is safe to assume that all Spans in a single + # batch all originate from one TracerProvider (and in turn have all + # the same service.name) + if spans: + service_name = spans[0].resource.attributes.get(SERVICE_NAME) + if service_name: + self.service_name = service_name translator = Translate(spans) pb_translator = ProtobufTranslator( self.service_name, self._max_tag_value_length diff --git a/exporter/opentelemetry-exporter-jaeger-proto/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger-proto/tests/test_jaeger_exporter_protobuf.py index 237ba54b2f..09abc3012b 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger-proto/tests/test_jaeger_exporter_protobuf.py @@ -15,7 +15,7 @@ import os import unittest from collections import OrderedDict -from unittest.mock import patch +from unittest import mock # pylint:disable=no-name-in-module # pylint:disable=import-error @@ -34,7 +34,9 @@ OTEL_EXPORTER_JAEGER_ENDPOINT, OTEL_RESOURCE_ATTRIBUTES, ) -from opentelemetry.sdk.trace import Resource, TracerProvider +from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace.status import Status, StatusCode @@ -43,13 +45,13 @@ class TestJaegerExporter(unittest.TestCase): def setUp(self): # create and save span to be used in tests - context = trace_api.SpanContext( + self.context = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, ) - self._test_span = trace._Span("test_span", context=context) + self._test_span = trace._Span("test_span", context=self.context) self._test_span.start() self._test_span.end() @@ -62,7 +64,7 @@ def test_constructor_by_environment_variables(self): collector_endpoint = "localhost:14250" - env_patch = patch.dict( + env_patch = mock.patch.dict( "os.environ", { OTEL_EXPORTER_JAEGER_ENDPOINT: collector_endpoint, @@ -438,3 +440,30 @@ def test_max_tag_value_length(self): self.assertEqual("hello", tags_by_keys["key_string"]) self.assertEqual("('tup", tags_by_keys["key_tuple"]) self.assertEqual("some_", tags_by_keys["key_resource"]) + + def test_export(self): + client_mock = mock.Mock() + spans = [] + exporter = JaegerExporter() + exporter._grpc_client = client_mock + status = exporter.export(spans) + self.assertEqual(SpanExportResult.SUCCESS, status) + + def test_export_span_service_name(self): + resource = Resource.create({SERVICE_NAME: "test"}) + span = trace._Span( + "test_span", context=self.context, resource=resource + ) + span.start() + span.end() + client_mock = mock.Mock() + exporter = JaegerExporter() + exporter._grpc_client = client_mock + exporter.export([span]) + self.assertEqual(exporter.service_name, "test") + + +class MockResponse: + def __init__(self, status_code): + self.status_code = status_code + self.text = status_code diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py index 674b9d2bf6..482fbdbf4e 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py @@ -201,7 +201,15 @@ def _collector_http_client(self) -> Optional[Collector]: return self._collector def export(self, spans) -> SpanExportResult: - + # Populate service_name from first span + # We restrict any SpanProcessor to be only associated with a single + # TracerProvider, so it is safe to assume that all Spans in a single + # batch all originate from one TracerProvider (and in turn have all + # the same service.name) + if spans: + service_name = spans[0].resource.attributes.get(SERVICE_NAME) + if service_name: + self.service_name = service_name translator = Translate(spans) thrift_translator = ThriftTranslator(self._max_tag_value_length) jaeger_spans = translator._translate(thrift_translator) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py index 9fcd69b817..4cf06b04a5 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py @@ -44,13 +44,13 @@ class TestJaegerExporter(unittest.TestCase): def setUp(self): # create and save span to be used in tests - context = trace_api.SpanContext( + self.context = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, ) - self._test_span = trace._Span("test_span", context=context) + self._test_span = trace._Span("test_span", context=self.context) self._test_span.start() self._test_span.end() # pylint: disable=protected-access @@ -491,6 +491,27 @@ def test_export(self): self.assertEqual(collector_mock.submit.call_count, 1) # trace_api._TRACER_PROVIDER = None + @patch("opentelemetry.exporter.jaeger.thrift.trace._TRACER_PROVIDER", None) + def test_export_span_service_name(self): + trace_api.set_tracer_provider( + TracerProvider( + resource=Resource.create({SERVICE_NAME: "text_export"}) + ) + ) + exporter = jaeger_exporter.JaegerExporter( + agent_host_name="localhost", agent_port=6318 + ) + agent_client_mock = mock.Mock(spec=jaeger_exporter.AgentClientUDP) + exporter._agent_client = agent_client_mock + resource = Resource.create({SERVICE_NAME: "test"}) + span = trace._Span( + "test_span", context=self.context, resource=resource + ) + span.start() + span.end() + exporter.export([span]) + self.assertEqual(exporter.service_name, "test") + def test_agent_client(self): agent_client = jaeger_exporter.AgentClientUDP( host_name="localhost", port=6354 diff --git a/exporter/opentelemetry-exporter-jaeger/README.rst b/exporter/opentelemetry-exporter-jaeger/README.rst index 2176b2a71d..a75c55c6b4 100644 --- a/exporter/opentelemetry-exporter-jaeger/README.rst +++ b/exporter/opentelemetry-exporter-jaeger/README.rst @@ -6,7 +6,12 @@ OpenTelemetry Jaeger Exporter .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger.svg :target: https://pypi.org/project/opentelemetry-exporter-jaeger/ -This library allows to export tracing data to `Jaeger `_. +This library is provided as a convenience to install all supported Jaeger Exporters. Currently it installs: +* opentelemetry-exporter-jaeger-proto +* opentelemetry-exporter-jaeger-thrift + +To avoid unnecessary dependencies, users should install the specific package once they've determined their +preferred serialization method. Installation ------------ @@ -16,18 +21,6 @@ Installation pip install opentelemetry-exporter-jaeger -.. _Jaeger: https://www.jaegertracing.io/ -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ - -Configuration -------------- - -OpenTelemetry Jaeger Exporter can be configured by setting `JaegerExporter parameters -`_ or by setting -`environment variables `_ - References ---------- From d21793d1249ee6e41180dc9a33755c6dcc0e94a1 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 18 Mar 2021 08:15:22 -0700 Subject: [PATCH 0821/1517] remove python 3.5 support (#1706) --- .github/workflows/test.yml | 24 ++----------- CHANGELOG.md | 2 ++ README.md | 2 +- .../error_handler/error_handler_0/setup.cfg | 3 +- .../error_handler/error_handler_1/setup.cfg | 3 +- docs/index.rst | 2 +- .../setup.cfg | 3 +- .../setup.cfg | 3 +- .../opentelemetry-exporter-jaeger/setup.cfg | 3 +- .../setup.cfg | 3 +- .../setup.cfg | 3 +- .../opentelemetry-exporter-otlp/setup.cfg | 3 +- .../opentelemetry-exporter-zipkin/setup.cfg | 3 +- opentelemetry-api/setup.cfg | 3 +- opentelemetry-distro/setup.cfg | 3 +- opentelemetry-instrumentation/setup.cfg | 3 +- opentelemetry-proto/setup.cfg | 3 +- opentelemetry-sdk/setup.cfg | 3 +- .../opentelemetry-propagator-b3/setup.cfg | 3 +- .../opentelemetry-propagator-jaeger/setup.cfg | 3 +- scripts/coverage.sh | 10 ++---- scripts/eachdist.py | 13 +------ shim/opentelemetry-opentracing-shim/setup.cfg | 3 +- tests/util/setup.cfg | 3 +- tox.ini | 34 +++++++++---------- 25 files changed, 45 insertions(+), 96 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5016576346..908cf71caa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,6 @@ jobs: build: env: # We use these variables to convert between tox and GHA version literals - py35: 3.5 py36: 3.6 py37: 3.7 py38: 3.8 @@ -27,18 +26,9 @@ jobs: strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [ py35, py36, py37, py38, py39, pypy3 ] + python-version: [ py36, py37, py38, py39, pypy3 ] package: ["instrumentation", "core", "exporter", "propagator"] os: [ ubuntu-latest ] - include: - # py35-instrumentation segfaults on 18.04 so we instead run on 20.04 - - python-version: py35 - package: instrumentation - os: ubuntu-20.04 - exclude: - - os: ubuntu-latest - python-version: py35 - package: instrumentation steps: - name: Checkout Core Repo @ SHA - ${{ github.sha }} uses: actions/checkout@v2 @@ -118,7 +108,6 @@ jobs: contrib-build: env: # We use these variables to convert between tox and GHA version literals - py35: 3.5 py36: 3.6 py37: 3.7 py38: 3.8 @@ -128,18 +117,9 @@ jobs: strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [ py35, py36, py37, py38, py39, pypy3 ] + python-version: [ py36, py37, py38, py39, pypy3 ] package: ["instrumentation", "exporter"] os: [ ubuntu-latest ] - include: - # py35-instrumentation segfaults on 18.04 so we instead run on 20.04 - - python-version: py35 - package: instrumentation - os: ubuntu-20.04 - exclude: - - os: ubuntu-latest - python-version: py35 - package: instrumentation steps: - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 050b822d09..a2ee4a62e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1675](https://github.com/open-telemetry/opentelemetry-python/pull/1675)) - Remove `OTEL_EXPORTER_*_ INSECURE` env var ([#1682](https://github.com/open-telemetry/opentelemetry-python/pull/1682)) +- Removing support for Python 3.5 + ([#1706](https://github.com/open-telemetry/opentelemetry-python/pull/1706)) ## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 diff --git a/README.md b/README.md index 2eafcc6d12..f56b790f32 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ This page describes the Python [OpenTelemetry](https://opentelemetry.io/) implementation. OpenTelemetry is an observability framework for cloud-native software. ## Requirements -Unless otherwise noted, all published artifacts support Python 3.5 or higher. See CONTRIBUTING.md for additional instructions for building this project for development. +Unless otherwise noted, all published artifacts support Python 3.6 or higher. See CONTRIBUTING.md for additional instructions for building this project for development. ## Getting started diff --git a/docs/examples/error_handler/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg index b26c9d57a1..481d15ab8d 100644 --- a/docs/examples/error_handler/error_handler_0/setup.cfg +++ b/docs/examples/error_handler/error_handler_0/setup.cfg @@ -25,14 +25,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/docs/examples/error_handler/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg index f24ed6bd0d..05388009e3 100644 --- a/docs/examples/error_handler/error_handler_1/setup.cfg +++ b/docs/examples/error_handler/error_handler_1/setup.cfg @@ -25,14 +25,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/docs/index.rst b/docs/index.rst index 12fbd7ef44..42969c058f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,7 +16,7 @@ generally be used in production environments. Requirement ----------- -OpenTelemetry-Python supports Python 3.5 and higher. +OpenTelemetry-Python supports Python 3.6 and higher. Installation ------------ diff --git a/exporter/opentelemetry-exporter-jaeger-proto/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto/setup.cfg index 3122904f61..cbcae8262e 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index 5ab20acdf2..02d9cd4c9f 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index e904eb7a2d..b1444538a4 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 packages=find_namespace: install_requires = diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index b10fbe2f04..41edc45e3e 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index cfb450d897..7cf46dabaf 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 64d94a91a4..24ad1517f5 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 packages=find_namespace: install_requires = opentelemetry-exporter-otlp-proto-grpc == 1.0.0.dev0 diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index f1a809be37..11cc221589 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 428eb5a8e8..1f4c7aab1e 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 41eeda5d70..0ee4ea93a0 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -28,13 +28,12 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index db8c4675be..563eebfce0 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-proto/setup.cfg b/opentelemetry-proto/setup.cfg index 9934a7c5ec..96777d2fdf 100644 --- a/opentelemetry-proto/setup.cfg +++ b/opentelemetry-proto/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 5573fa7127..063e8a052c 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 8062d85339..62ac8b7a7b 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index b24569100f..38646d203f 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/scripts/coverage.sh b/scripts/coverage.sh index aedfbd2966..437c633c33 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -40,13 +40,9 @@ cov instrumentation/opentelemetry-instrumentation-opentracing-shim cov util/opentelemetry-util-http cov exporter/opentelemetry-exporter-zipkin -# aiohttp is only supported on Python 3.5+. -if [ ${PYTHON_VERSION_INFO[1]} -gt 4 ]; then - cov instrumentation/opentelemetry-instrumentation-aiohttp-client -# ext-asgi is only supported on Python 3.5+. -if [ ${PYTHON_VERSION_INFO[1]} -gt 4 ]; then - cov instrumentation/opentelemetry-instrumentation-asgi -fi + +cov instrumentation/opentelemetry-instrumentation-aiohttp-client +cov instrumentation/opentelemetry-instrumentation-asgi coverage report --show-missing coverage xml diff --git a/scripts/eachdist.py b/scripts/eachdist.py index d1069290e8..a939f0d93d 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -7,7 +7,6 @@ import shutil import subprocess import sys -from collections import namedtuple from configparser import ConfigParser from datetime import datetime from inspect import cleandoc @@ -26,17 +25,7 @@ def unique(elems): seen.add(elem) -try: - subprocess_run = subprocess.run -except AttributeError: # Py < 3.5 compat - CompletedProcess = namedtuple("CompletedProcess", "returncode") - - def subprocess_run(*args, **kwargs): - check = kwargs.pop("check", False) - if check: - subprocess.check_call(*args, **kwargs) - return CompletedProcess(returncode=0) - return CompletedProcess(returncode=subprocess.call(*args, **kwargs)) +subprocess_run = subprocess.run def extraargs_help(calledcmd): diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 8a490470cb..3e305863b6 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index 94333459ab..6ea81897a7 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -26,14 +26,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/tox.ini b/tox.ini index 07126f90fc..4865be2427 100644 --- a/tox.ini +++ b/tox.ini @@ -5,64 +5,64 @@ envlist = ; Environments are organized by individual package, allowing ; for specifying supported Python versions per package. ; opentelemetry-api - py3{5,6,7,8,9}-test-core-api + py3{6,7,8,9}-test-core-api pypy3-test-core-api ; opentelemetry-proto - py3{5,6,7,8,9}-test-core-proto + py3{6,7,8,9}-test-core-proto pypy3-test-core-proto ; opentelemetry-sdk - py3{5,6,7,8,9}-test-core-sdk + py3{6,7,8,9}-test-core-sdk pypy3-test-core-sdk ; opentelemetry-instrumentation - py3{5,6,7,8,9}-test-core-instrumentation + py3{6,7,8,9}-test-core-instrumentation pypy3-test-core-instrumentation ; docs/getting-started - py3{5,6,7,8,9}-test-core-getting-started + py3{6,7,8,9}-test-core-getting-started pypy3-test-core-getting-started ; opentelemetry-distro - py3{5,6,7,8,9}-test-core-distro + py3{6,7,8,9}-test-core-distro pypy3-test-core-distro ; opentelemetry-exporter-jaeger - py3{5,6,7,8,9}-test-exporter-jaeger-combined + py3{6,7,8,9}-test-exporter-jaeger-combined ; opentelemetry-exporter-jaeger-proto - py3{5,6,7,8,9}-test-exporter-jaeger-proto + py3{6,7,8,9}-test-exporter-jaeger-proto ; opentelemetry-exporter-jaeger-thrift - py3{5,6,7,8,9}-test-exporter-jaeger-thrift + py3{6,7,8,9}-test-exporter-jaeger-thrift ; opentelemetry-exporter-opencensus - py3{5,6,7,8,9}-test-exporter-opencensus + py3{6,7,8,9}-test-exporter-opencensus ; exporter-opencensus intentionally excluded from pypy3 ; opentelemetry-exporter-otlp-combined - py3{5,6,7,8,9}-test-exporter-otlp-combined + py3{6,7,8,9}-test-exporter-otlp-combined ; intentionally excluded from pypy3 ; opentelemetry-exporter-otlp-proto-grpc - py3{5,6,7,8,9}-test-exporter-otlp-proto-grpc + py3{6,7,8,9}-test-exporter-otlp-proto-grpc ; intentionally excluded from pypy3 ; opentelemetry-exporter-zipkin - py3{5,6,7,8,9}-test-exporter-zipkin + py3{6,7,8,9}-test-exporter-zipkin pypy3-test-exporter-zipkin ; opentelemetry-opentracing-shim - py3{5,6,7,8,9}-test-core-opentracing-shim + py3{6,7,8,9}-test-core-opentracing-shim pypy3-test-core-opentracing-shim ; opentelemetry-propagator-b3 - py3{5,6,7,8,9}-test-propagator-b3 + py3{6,7,8,9}-test-propagator-b3 pypy3-test-propagator-b3 ; opentelemetry-propagator-jaeger - py3{5,6,7,8,9}-test-propagator-jaeger + py3{6,7,8,9}-test-propagator-jaeger pypy3-test-propagator-jaeger lint @@ -104,7 +104,7 @@ changedir = commands_pre = ; Install without -e to test the actual installation - py3{5,6,7,8,9}: python -m pip install -U pip setuptools wheel + py3{6,7,8,9}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-instrumentation {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util From f5d10d456ba0e9deaf9d2b25bb9c9b1fdcd93bda Mon Sep 17 00:00:00 2001 From: Dominika Molenda <73838995+dmolenda-sumo@users.noreply.github.com> Date: Thu, 18 Mar 2021 16:56:28 +0100 Subject: [PATCH 0822/1517] Upgrade Mypy to version 0.812 (#1705) --- dev-requirements.txt | 2 +- opentelemetry-api/src/opentelemetry/__init__.pyi | 0 opentelemetry-api/src/opentelemetry/context/__init__.py | 8 ++++---- opentelemetry-api/src/opentelemetry/util/_time.py | 2 +- opentelemetry-api/tests/baggage/test_baggage.py | 2 ++ .../tests/baggage/test_baggage_propagation.py | 2 ++ opentelemetry-api/tests/propagators/test_composite.py | 2 ++ .../tests/propagators/test_global_httptextformat.py | 2 ++ opentelemetry-api/tests/propagators/test_propagators.py | 2 ++ opentelemetry-api/tests/trace/propagation/test_textmap.py | 2 ++ .../trace/propagation/test_tracecontexthttptextformat.py | 2 ++ tox.ini | 2 +- 12 files changed, 21 insertions(+), 7 deletions(-) delete mode 100644 opentelemetry-api/src/opentelemetry/__init__.pyi diff --git a/dev-requirements.txt b/dev-requirements.txt index bd2515b117..87dc7adc59 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,7 +3,7 @@ flake8~=3.7 isort~=5.6 black>=19.3b0,==19.* httpretty~=1.0 -mypy==0.790 +mypy==0.812 sphinx~=2.1 sphinx-rtd-theme~=0.4 sphinx-autodoc-typehints~=1.10.2 diff --git a/opentelemetry-api/src/opentelemetry/__init__.pyi b/opentelemetry-api/src/opentelemetry/__init__.pyi deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 3328d62772..0f60d86a0c 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -37,8 +37,8 @@ def _load_runtime_context(func: _F) -> _F: A wrapper of the decorated method. """ - @wraps(func) # type: ignore - def wrapper( + @wraps(func) # type: ignore[misc] + def wrapper( # type: ignore[misc] *args: typing.Tuple[typing.Any, typing.Any], **kwargs: typing.Dict[typing.Any, typing.Any] ) -> typing.Optional[typing.Any]: @@ -63,9 +63,9 @@ def wrapper( logger.error( "Failed to load context: %s", configured_context ) - return func(*args, **kwargs) # type: ignore + return func(*args, **kwargs) # type: ignore[misc] - return wrapper # type:ignore + return typing.cast(_F, wrapper) # type: ignore[misc] def get_value(key: str, context: typing.Optional[Context] = None) -> "object": diff --git a/opentelemetry-api/src/opentelemetry/util/_time.py b/opentelemetry-api/src/opentelemetry/util/_time.py index 10a2aee33e..aa61bc02aa 100644 --- a/opentelemetry-api/src/opentelemetry/util/_time.py +++ b/opentelemetry-api/src/opentelemetry/util/_time.py @@ -25,7 +25,7 @@ ) from time import time - def _time_ns(): + def _time_ns() -> int: return int(time() * 1e9) diff --git a/opentelemetry-api/tests/baggage/test_baggage.py b/opentelemetry-api/tests/baggage/test_baggage.py index 276d2bc8b0..62f3fb77d5 100644 --- a/opentelemetry-api/tests/baggage/test_baggage.py +++ b/opentelemetry-api/tests/baggage/test_baggage.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# type: ignore + import unittest from opentelemetry import baggage, context diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py index a928a2fc8c..3047ddbbe4 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# type: ignore + import unittest from unittest.mock import Mock, patch diff --git a/opentelemetry-api/tests/propagators/test_composite.py b/opentelemetry-api/tests/propagators/test_composite.py index 232e177d3d..e33649bbdd 100644 --- a/opentelemetry-api/tests/propagators/test_composite.py +++ b/opentelemetry-api/tests/propagators/test_composite.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# type: ignore + import unittest from unittest.mock import Mock diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index faa4023d5d..6ba32e4618 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# type: ignore + import unittest from opentelemetry import baggage, trace diff --git a/opentelemetry-api/tests/propagators/test_propagators.py b/opentelemetry-api/tests/propagators/test_propagators.py index 1ea80146ef..80299d3490 100644 --- a/opentelemetry-api/tests/propagators/test_propagators.py +++ b/opentelemetry-api/tests/propagators/test_propagators.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# type: ignore + from importlib import reload from os import environ from unittest import TestCase diff --git a/opentelemetry-api/tests/trace/propagation/test_textmap.py b/opentelemetry-api/tests/trace/propagation/test_textmap.py index 12e851de34..e47a0d22cb 100644 --- a/opentelemetry-api/tests/trace/propagation/test_textmap.py +++ b/opentelemetry-api/tests/trace/propagation/test_textmap.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# type: ignore + import unittest from opentelemetry.propagators.textmap import DictGetter diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index cff30b7c9b..79683d43d9 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# type: ignore + import typing import unittest from unittest.mock import Mock, patch diff --git a/tox.ini b/tox.ini index 4865be2427..c07bbd9e7f 100644 --- a/tox.ini +++ b/tox.ini @@ -149,7 +149,7 @@ commands = test: pytest {posargs} coverage: {toxinidir}/scripts/coverage.sh - mypy: mypy --namespace-packages opentelemetry-api/src/opentelemetry/ + mypy: mypy --namespace-packages --explicit-package-bases opentelemetry-api/src/opentelemetry/ ; For test code, we don't want to enforce the full mypy strictness mypy: mypy --namespace-packages --config-file=mypy-relaxed.ini opentelemetry-api/tests/ From 320d69f4f0f28b6df91047f31f810e0aa5731038 Mon Sep 17 00:00:00 2001 From: Daisuke Taniwaki Date: Tue, 23 Mar 2021 00:49:36 +0900 Subject: [PATCH 0823/1517] Fix get exporter names condition (#1707) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/distro/__init__.py | 5 +---- opentelemetry-distro/tests/test_configurator.py | 12 ++++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2ee4a62e4..25741bf98b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1695](https://github.com/open-telemetry/opentelemetry-python/pull/1695)) - Change Jaeger exporters to obtain service.name from span ([#1703](https://github.com/open-telemetry/opentelemetry-python/pull/1703)) +- Fixed an unset `OTEL_TRACES_EXPORTER` resulting in an error + ([#1707](https://github.com/open-telemetry/opentelemetry-python/pull/1707)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index 576f44a506..258bdaddc5 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -50,10 +50,7 @@ def _get_exporter_names() -> Sequence[str]: exporters = set() - if ( - trace_exporters is not None - or trace_exporters.lower().strip() != "none" - ): + if trace_exporters and trace_exporters.lower().strip() != "none": exporters.update( { trace_exporter.strip() diff --git a/opentelemetry-distro/tests/test_configurator.py b/opentelemetry-distro/tests/test_configurator.py index fa33744dea..b2d2cb46d4 100644 --- a/opentelemetry-distro/tests/test_configurator.py +++ b/opentelemetry-distro/tests/test_configurator.py @@ -167,3 +167,15 @@ def test_otlp_exporter_overwrite(self): @patch.dict(environ, {OTEL_TRACES_EXPORTER: "jaeger,zipkin"}) def test_multiple_exporters(self): self.assertEqual(sorted(_get_exporter_names()), ["jaeger", "zipkin"]) + + @patch.dict(environ, {OTEL_TRACES_EXPORTER: "none"}) + def test_none_exporters(self): + self.assertEqual(sorted(_get_exporter_names()), []) + + @patch.dict(environ, {}, clear=True) + def test_no_exporters(self): + self.assertEqual(sorted(_get_exporter_names()), []) + + @patch.dict(environ, {OTEL_TRACES_EXPORTER: ""}) + def test_empty_exporters(self): + self.assertEqual(sorted(_get_exporter_names()), []) From 4fb02197bcd22afd808599ccacbe85d2e45a9683 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 22 Mar 2021 09:50:09 -0700 Subject: [PATCH 0824/1517] rename package to opentelemetry-exporter-jaeger-proto-grpc (#1709) --- .flake8 | 4 ++-- CHANGELOG.md | 2 +- docs/exporter/jaeger/jaeger.rst | 4 ++-- .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 8 ++++---- .../examples/jaeger_exporter_example.py | 4 ++-- .../proto/api_v2/collector.proto | 0 .../proto/api_v2/model.proto | 0 .../setup.cfg | 6 +++--- .../setup.py | 1 + .../exporter/jaeger/proto/grpc}/__init__.py | 12 ++++++------ .../exporter/jaeger/proto/grpc}/gen/__init__.py | 0 .../exporter/jaeger/proto/grpc}/gen/collector_pb2.py | 0 .../jaeger/proto/grpc}/gen/collector_pb2_grpc.py | 0 .../jaeger/proto/grpc}/gen/gogoproto/gogo_pb2.py | 0 .../proto/grpc}/gen/google/api/annotations_pb2.py | 0 .../jaeger/proto/grpc}/gen/google/api/http_pb2.py | 0 .../exporter/jaeger/proto/grpc}/gen/model_pb2.py | 0 .../protoc_gen_swagger/options/annotations_pb2.py | 0 .../gen/protoc_gen_swagger/options/openapiv2_pb2.py | 0 .../exporter/jaeger/proto/grpc}/send/__init__.py | 0 .../jaeger/proto/grpc}/translate/__init__.py | 2 +- .../exporter/jaeger/proto/grpc}/util.py | 0 .../exporter/jaeger/proto/grpc}/version.py | 0 .../tests/__init__.py | 0 .../tests/certs/cred.cert | 0 .../tests/test_jaeger_exporter_protobuf.py | 8 ++++---- exporter/opentelemetry-exporter-jaeger/README.rst | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 2 +- .../tests/test_jaeger.py | 5 +++-- pyproject.toml | 2 +- scripts/coverage.sh | 2 +- tox.ini | 12 ++++++------ 34 files changed, 39 insertions(+), 37 deletions(-) rename exporter/{opentelemetry-exporter-jaeger-proto => opentelemetry-exporter-jaeger-proto-grpc}/LICENSE (100%) rename exporter/{opentelemetry-exporter-jaeger-proto => opentelemetry-exporter-jaeger-proto-grpc}/MANIFEST.in (100%) rename exporter/{opentelemetry-exporter-jaeger-proto => opentelemetry-exporter-jaeger-proto-grpc}/README.rst (88%) rename exporter/{opentelemetry-exporter-jaeger-proto => opentelemetry-exporter-jaeger-proto-grpc}/examples/jaeger_exporter_example.py (93%) rename exporter/{opentelemetry-exporter-jaeger-proto => opentelemetry-exporter-jaeger-proto-grpc}/proto/api_v2/collector.proto (100%) rename exporter/{opentelemetry-exporter-jaeger-proto => opentelemetry-exporter-jaeger-proto-grpc}/proto/api_v2/model.proto (100%) rename exporter/{opentelemetry-exporter-jaeger-proto => opentelemetry-exporter-jaeger-proto-grpc}/setup.cfg (90%) rename exporter/{opentelemetry-exporter-jaeger-proto => opentelemetry-exporter-jaeger-proto-grpc}/setup.py (98%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/__init__.py (93%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/gen/__init__.py (100%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/gen/collector_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/gen/collector_pb2_grpc.py (100%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/gen/gogoproto/gogo_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/gen/google/api/annotations_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/gen/google/api/http_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/gen/model_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/gen/protoc_gen_swagger/options/annotations_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/gen/protoc_gen_swagger/options/openapiv2_pb2.py (100%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/send/__init__.py (100%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/translate/__init__.py (99%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/util.py (100%) rename exporter/{opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto => opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/version.py (100%) rename exporter/{opentelemetry-exporter-jaeger-proto => opentelemetry-exporter-jaeger-proto-grpc}/tests/__init__.py (100%) rename exporter/{opentelemetry-exporter-jaeger-proto => opentelemetry-exporter-jaeger-proto-grpc}/tests/certs/cred.cert (100%) rename exporter/{opentelemetry-exporter-jaeger-proto => opentelemetry-exporter-jaeger-proto-grpc}/tests/test_jaeger_exporter_protobuf.py (98%) diff --git a/.flake8 b/.flake8 index 8431f87079..33658400ce 100644 --- a/.flake8 +++ b/.flake8 @@ -16,8 +16,8 @@ exclude = venv*/ target __pycache__ - exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/ - exporter/opentelemetry-exporter-jaeger-proto/build/* + exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/ + exporter/opentelemetry-exporter-jaeger-proto-grpc/build/* exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/ exporter/opentelemetry-exporter-jaeger-thrift/build/* exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 25741bf98b..9f43369ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,7 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1680](https://github.com/open-telemetry/opentelemetry-python/pull/1680)) - Change Zipkin exporter to obtain service.name from span ([#1696](https://github.com/open-telemetry/opentelemetry-python/pull/1696)) -- Split up `opentelemetry-exporter-jaeger` package into `opentelemetry-exporter-jaeger-proto` and +- Split up `opentelemetry-exporter-jaeger` package into `opentelemetry-exporter-jaeger-proto-grpc` and `opentelemetry-exporter-jaeger-thrift` packages to reduce dependencies for each one. ([#1694](https://github.com/open-telemetry/opentelemetry-python/pull/1694)) - Added `opentelemetry-exporter-otlp-proto-grpc` and changed `opentelemetry-exporter-otlp` to diff --git a/docs/exporter/jaeger/jaeger.rst b/docs/exporter/jaeger/jaeger.rst index ea2e427cbe..5a49f72b5c 100644 --- a/docs/exporter/jaeger/jaeger.rst +++ b/docs/exporter/jaeger/jaeger.rst @@ -11,7 +11,7 @@ Opentelemetry Jaeger Exporters :undoc-members: :show-inheritance: -.. automodule:: opentelemetry.exporter.jaeger.proto +.. automodule:: opentelemetry.exporter.jaeger.proto.grpc :members: :undoc-members: :show-inheritance: @@ -29,7 +29,7 @@ Submodules :undoc-members: :show-inheritance: -.. automodule:: opentelemetry.exporter.jaeger.proto.gen.collector_pb2_grpc +.. automodule:: opentelemetry.exporter.jaeger.proto.grpc.gen.collector_pb2_grpc :members: :undoc-members: :show-inheritance: diff --git a/exporter/opentelemetry-exporter-jaeger-proto/LICENSE b/exporter/opentelemetry-exporter-jaeger-proto-grpc/LICENSE similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/LICENSE rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/LICENSE diff --git a/exporter/opentelemetry-exporter-jaeger-proto/MANIFEST.in b/exporter/opentelemetry-exporter-jaeger-proto-grpc/MANIFEST.in similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/MANIFEST.in rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/MANIFEST.in diff --git a/exporter/opentelemetry-exporter-jaeger-proto/README.rst b/exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst similarity index 88% rename from exporter/opentelemetry-exporter-jaeger-proto/README.rst rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst index bd1a0a64f0..471e92085b 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto/README.rst +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst @@ -3,8 +3,8 @@ OpenTelemetry Jaeger Protobuf Exporter |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger-proto.svg - :target: https://pypi.org/project/opentelemetry-exporter-jaeger-proto/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger-proto-grpc.svg + :target: https://pypi.org/project/opentelemetry-exporter-jaeger-proto-grpc/ This library allows to export tracing data to `Jaeger `_. @@ -13,7 +13,7 @@ Installation :: - pip install opentelemetry-exporter-jaeger-proto + pip install opentelemetry-exporter-jaeger-proto-grpc .. _Jaeger: https://www.jaegertracing.io/ @@ -23,7 +23,7 @@ Configuration ------------- OpenTelemetry Jaeger Exporter can be configured by setting `JaegerExporter parameters -`_ or by setting `environment variables `_ diff --git a/exporter/opentelemetry-exporter-jaeger-proto/examples/jaeger_exporter_example.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/examples/jaeger_exporter_example.py similarity index 93% rename from exporter/opentelemetry-exporter-jaeger-proto/examples/jaeger_exporter_example.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/examples/jaeger_exporter_example.py index 9eb7389735..d8c017e5b0 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto/examples/jaeger_exporter_example.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/examples/jaeger_exporter_example.py @@ -1,7 +1,7 @@ import time from opentelemetry import trace -from opentelemetry.exporter.jaeger import proto +from opentelemetry.exporter.jaeger.proto import grpc from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor @@ -14,7 +14,7 @@ # parameter credentials=ChannelCredentials(...) or the environment variable # `EXPORTER_JAEGER_CERTIFICATE` with file containing creds. -jaeger_exporter = proto.JaegerExporter( +jaeger_exporter = grpc.JaegerExporter( collector_endpoint="localhost:14250", insecure=True, ) diff --git a/exporter/opentelemetry-exporter-jaeger-proto/proto/api_v2/collector.proto b/exporter/opentelemetry-exporter-jaeger-proto-grpc/proto/api_v2/collector.proto similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/proto/api_v2/collector.proto rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/proto/api_v2/collector.proto diff --git a/exporter/opentelemetry-exporter-jaeger-proto/proto/api_v2/model.proto b/exporter/opentelemetry-exporter-jaeger-proto-grpc/proto/api_v2/model.proto similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/proto/api_v2/model.proto rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/proto/api_v2/model.proto diff --git a/exporter/opentelemetry-exporter-jaeger-proto/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg similarity index 90% rename from exporter/opentelemetry-exporter-jaeger-proto/setup.cfg rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index cbcae8262e..5c37d00b0a 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-exporter-jaeger-proto +name = opentelemetry-exporter-jaeger-proto-grpc description = Jaeger Protobuf Exporter for OpenTelemetry long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger-proto +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger-proto-grpc platforms = any license = Apache-2.0 classifiers = @@ -52,4 +52,4 @@ test = [options.entry_points] opentelemetry_exporter = - jaeger_proto = opentelemetry.exporter.jaeger.proto:JaegerExporter \ No newline at end of file + jaeger_proto = opentelemetry.exporter.jaeger.proto.grpc:JaegerExporter \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-jaeger-proto/setup.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.py similarity index 98% rename from exporter/opentelemetry-exporter-jaeger-proto/setup.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.py index 2cc98f9b0b..642093080e 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto/setup.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.py @@ -23,6 +23,7 @@ "exporter", "jaeger", "proto", + "grpc", "version.py", ) PACKAGE_INFO = {} diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py similarity index 93% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/__init__.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py index 040fc99640..d287d0663b 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py @@ -27,7 +27,7 @@ .. code:: python from opentelemetry import trace - from opentelemetry.exporter.jaeger.proto import JaegerExporter + from opentelemetry.exporter.jaeger.proto.grpc import JaegerExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor @@ -71,15 +71,15 @@ from grpc import ChannelCredentials, insecure_channel, secure_channel from opentelemetry import trace -from opentelemetry.exporter.jaeger.proto import util -from opentelemetry.exporter.jaeger.proto.gen import model_pb2 -from opentelemetry.exporter.jaeger.proto.gen.collector_pb2 import ( +from opentelemetry.exporter.jaeger.proto.grpc import util +from opentelemetry.exporter.jaeger.proto.grpc.gen import model_pb2 +from opentelemetry.exporter.jaeger.proto.grpc.gen.collector_pb2 import ( PostSpansRequest, ) -from opentelemetry.exporter.jaeger.proto.gen.collector_pb2_grpc import ( +from opentelemetry.exporter.jaeger.proto.grpc.gen.collector_pb2_grpc import ( CollectorServiceStub, ) -from opentelemetry.exporter.jaeger.proto.translate import ( +from opentelemetry.exporter.jaeger.proto.grpc.translate import ( ProtobufTranslator, Translate, ) diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/__init__.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/__init__.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/collector_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/collector_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/collector_pb2_grpc.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2_grpc.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/collector_pb2_grpc.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2_grpc.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/gogoproto/gogo_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/gogoproto/gogo_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/gogoproto/gogo_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/gogoproto/gogo_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/google/api/annotations_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/google/api/annotations_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/google/api/annotations_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/google/api/annotations_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/google/api/http_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/google/api/http_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/google/api/http_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/google/api/http_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/model_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/model_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/model_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/model_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/protoc_gen_swagger/options/annotations_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/protoc_gen_swagger/options/annotations_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/protoc_gen_swagger/options/annotations_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/protoc_gen_swagger/options/annotations_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/protoc_gen_swagger/options/openapiv2_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/protoc_gen_swagger/options/openapiv2_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen/protoc_gen_swagger/options/openapiv2_pb2.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/protoc_gen_swagger/options/openapiv2_pb2.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/send/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/send/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/send/__init__.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/send/__init__.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py similarity index 99% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/translate/__init__.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py index 6a346000b4..5d11e14f60 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/translate/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py @@ -18,7 +18,7 @@ from google.protobuf.duration_pb2 import Duration from google.protobuf.timestamp_pb2 import Timestamp -from opentelemetry.exporter.jaeger.proto.gen import model_pb2 +from opentelemetry.exporter.jaeger.proto.grpc.gen import model_pb2 from opentelemetry.sdk.trace import ReadableSpan, StatusCode from opentelemetry.trace import SpanKind diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/util.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/util.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/util.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/util.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/version.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/tests/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/tests/__init__.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/__init__.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto/tests/certs/cred.cert b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/certs/cred.cert similarity index 100% rename from exporter/opentelemetry-exporter-jaeger-proto/tests/certs/cred.cert rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/certs/cred.cert diff --git a/exporter/opentelemetry-exporter-jaeger-proto/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py similarity index 98% rename from exporter/opentelemetry-exporter-jaeger-proto/tests/test_jaeger_exporter_protobuf.py rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py index 09abc3012b..96bccaef4a 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py @@ -19,11 +19,11 @@ # pylint:disable=no-name-in-module # pylint:disable=import-error -import opentelemetry.exporter.jaeger.proto.gen.model_pb2 as model_pb2 -import opentelemetry.exporter.jaeger.proto.translate as pb_translator +import opentelemetry.exporter.jaeger.proto.grpc.gen.model_pb2 as model_pb2 +import opentelemetry.exporter.jaeger.proto.grpc.translate as pb_translator from opentelemetry import trace as trace_api -from opentelemetry.exporter.jaeger.proto import JaegerExporter -from opentelemetry.exporter.jaeger.proto.translate import ( +from opentelemetry.exporter.jaeger.proto.grpc import JaegerExporter +from opentelemetry.exporter.jaeger.proto.grpc.translate import ( NAME_KEY, VERSION_KEY, Translate, diff --git a/exporter/opentelemetry-exporter-jaeger/README.rst b/exporter/opentelemetry-exporter-jaeger/README.rst index a75c55c6b4..00154142ec 100644 --- a/exporter/opentelemetry-exporter-jaeger/README.rst +++ b/exporter/opentelemetry-exporter-jaeger/README.rst @@ -7,7 +7,7 @@ OpenTelemetry Jaeger Exporter :target: https://pypi.org/project/opentelemetry-exporter-jaeger/ This library is provided as a convenience to install all supported Jaeger Exporters. Currently it installs: -* opentelemetry-exporter-jaeger-proto +* opentelemetry-exporter-jaeger-proto-grpc * opentelemetry-exporter-jaeger-thrift To avoid unnecessary dependencies, users should install the specific package once they've determined their diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index b1444538a4..36ee39e28d 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -38,7 +38,7 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto == 1.0.0.dev0 + opentelemetry-exporter-jaeger-proto-grpc == 1.0.0.dev0 opentelemetry-exporter-jaeger-thrift == 1.0.0.dev0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py index 9f199348ba..4ce87cceac 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py @@ -16,7 +16,8 @@ # pylint:disable=no-name-in-module # pylint:disable=import-error -from opentelemetry.exporter.jaeger import proto, thrift +from opentelemetry.exporter.jaeger import thrift +from opentelemetry.exporter.jaeger.proto import grpc # pylint:disable=no-member @@ -24,7 +25,7 @@ class TestJaegerExporter(unittest.TestCase): def test_constructors(self): """ Test ensures both exporters can co-exist""" try: - proto.JaegerExporter() + grpc.JaegerExporter() thrift.JaegerExporter() except Exception as exc: # pylint: disable=broad-except self.assertIsNone(exc) diff --git a/pyproject.toml b/pyproject.toml index d45615a1e8..f94565946a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 79 exclude = ''' ( /( # generated files - exporter/opentelemetry-exporter-jaeger-proto/src/opentelemetry/exporter/jaeger/proto/gen| + exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen| exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen| exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen| opentelemetry-proto/src/opentelemetry/proto/collector| diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 437c633c33..72b7adec74 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -34,7 +34,7 @@ cov opentelemetry-sdk cov exporter/opentelemetry-exporter-datadog cov instrumentation/opentelemetry-instrumentation-flask cov instrumentation/opentelemetry-instrumentation-requests -cov exporter/opentelemetry-exporter-jaeger-proto +cov exporter/opentelemetry-exporter-jaeger-proto-grpc cov exporter/opentelemetry-exporter-jaeger-thrift cov instrumentation/opentelemetry-instrumentation-opentracing-shim cov util/opentelemetry-util-http diff --git a/tox.ini b/tox.ini index c07bbd9e7f..2be66c6014 100644 --- a/tox.ini +++ b/tox.ini @@ -31,8 +31,8 @@ envlist = ; opentelemetry-exporter-jaeger py3{6,7,8,9}-test-exporter-jaeger-combined - ; opentelemetry-exporter-jaeger-proto - py3{6,7,8,9}-test-exporter-jaeger-proto + ; opentelemetry-exporter-jaeger-proto-grpc + py3{6,7,8,9}-test-exporter-jaeger-proto-grpc ; opentelemetry-exporter-jaeger-thrift py3{6,7,8,9}-test-exporter-jaeger-thrift @@ -92,7 +92,7 @@ changedir = test-core-distro: opentelemetry-distro/tests test-exporter-jaeger-combined: exporter/opentelemetry-exporter-jaeger/tests - test-exporter-jaeger-proto: exporter/opentelemetry-exporter-jaeger-proto/tests + test-exporter-jaeger-proto-grpc: exporter/opentelemetry-exporter-jaeger-proto-grpc/tests test-exporter-jaeger-thrift: exporter/opentelemetry-exporter-jaeger-thrift/tests test-exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests test-exporter-otlp-combined: exporter/opentelemetry-exporter-otlp/tests @@ -124,8 +124,8 @@ commands_pre = exporter-otlp-proto-grpc: pip install {toxinidir}/opentelemetry-proto exporter-otlp-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc - exporter-jaeger-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift {toxinidir}/exporter/opentelemetry-exporter-jaeger - exporter-jaeger-proto: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto + exporter-jaeger-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift {toxinidir}/exporter/opentelemetry-exporter-jaeger + exporter-jaeger-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc exporter-jaeger-thrift: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift opentracing-shim: pip install {toxinidir}/opentelemetry-sdk @@ -178,7 +178,7 @@ commands_pre = python -m pip install -e {toxinidir}/opentelemetry-proto[test] python -m pip install -e {toxinidir}/tests/util[test] python -m pip install -e {toxinidir}/shim/opentelemetry-opentracing-shim[test] - python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-opencensus[test] From 5dc009306fd3372c3246fdffd7be7f011781f794 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 22 Mar 2021 12:36:28 -0700 Subject: [PATCH 0825/1517] split zipkin exporter into json/proto packages (#1699) --- .flake8 | 2 +- CHANGELOG.md | 4 + docs/exporter/zipkin/zipkin.rst | 14 +- .../opentelemetry/exporter/opencensus/util.py | 6 +- .../CHANGELOG.md | 0 .../LICENSE | 201 +++++++ .../MANIFEST.in | 9 + .../README.rst | 25 + .../setup.cfg | 55 ++ .../setup.py | 32 ++ .../exporter/zipkin/encoder/__init__.py | 5 +- .../exporter/zipkin/json}/__init__.py | 27 +- .../exporter/zipkin/json/v1/__init__.py} | 26 +- .../exporter/zipkin/json/v2/__init__.py} | 0 .../exporter/zipkin/json/version.py | 15 + .../exporter/zipkin/node_endpoint.py | 0 .../tests/__init__.py | 0 .../tests/encoder}/__init__.py | 0 .../tests/encoder/common_tests.py | 0 .../tests/encoder/test_v1_json.py | 2 +- .../tests/encoder/test_v2_json.py | 2 +- .../tests/test_zipkin_exporter.py | 188 +++++++ .../CHANGELOG.md} | 0 .../LICENSE | 201 +++++++ .../MANIFEST.in | 9 + .../README.rst | 25 + .../setup.cfg | 57 ++ .../setup.py | 33 ++ .../exporter/zipkin/proto/http/__init__.py | 142 +++++ .../zipkin/proto/http/v2}/__init__.py | 2 +- .../zipkin/proto/http/v2/gen/__init__.py | 0 .../zipkin/proto/http/v2}/gen/zipkin_pb2.py | 0 .../zipkin/proto/http/v2}/gen/zipkin_pb2.pyi | 0 .../exporter/zipkin/proto/http/version.py | 15 + .../tests/__init__.py | 13 + .../tests/encoder/__init__.py | 0 .../tests/encoder/common_tests.py | 505 ++++++++++++++++++ .../tests/encoder/test_v2_protobuf.py | 4 +- .../tests/test_zipkin_exporter.py | 23 +- .../opentelemetry-exporter-zipkin/README.rst | 10 +- .../opentelemetry-exporter-zipkin/setup.cfg | 17 +- .../exporter/zipkin/encoder/v1/__init__.py | 41 -- .../tests/test_zipkin.py | 27 + pyproject.toml | 2 +- tox.ini | 27 +- 45 files changed, 1662 insertions(+), 104 deletions(-) rename exporter/{opentelemetry-exporter-zipkin => opentelemetry-exporter-zipkin-json}/CHANGELOG.md (100%) create mode 100644 exporter/opentelemetry-exporter-zipkin-json/LICENSE create mode 100644 exporter/opentelemetry-exporter-zipkin-json/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-zipkin-json/README.rst create mode 100644 exporter/opentelemetry-exporter-zipkin-json/setup.cfg create mode 100644 exporter/opentelemetry-exporter-zipkin-json/setup.py rename exporter/{opentelemetry-exporter-zipkin => opentelemetry-exporter-zipkin-json}/src/opentelemetry/exporter/zipkin/encoder/__init__.py (99%) rename exporter/{opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin => opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json}/__init__.py (85%) rename exporter/{opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/json.py => opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/v1/__init__.py} (73%) rename exporter/{opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/json.py => opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/v2/__init__.py} (100%) create mode 100644 exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py rename exporter/{opentelemetry-exporter-zipkin => opentelemetry-exporter-zipkin-json}/src/opentelemetry/exporter/zipkin/node_endpoint.py (100%) rename exporter/{opentelemetry-exporter-zipkin => opentelemetry-exporter-zipkin-json}/tests/__init__.py (100%) rename exporter/{opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen => opentelemetry-exporter-zipkin-json/tests/encoder}/__init__.py (100%) rename exporter/{opentelemetry-exporter-zipkin => opentelemetry-exporter-zipkin-json}/tests/encoder/common_tests.py (100%) rename exporter/{opentelemetry-exporter-zipkin => opentelemetry-exporter-zipkin-json}/tests/encoder/test_v1_json.py (99%) rename exporter/{opentelemetry-exporter-zipkin => opentelemetry-exporter-zipkin-json}/tests/encoder/test_v2_json.py (99%) create mode 100644 exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py rename exporter/{opentelemetry-exporter-zipkin/tests/encoder/__init__.py => opentelemetry-exporter-zipkin-proto-http/CHANGELOG.md} (100%) create mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/LICENSE create mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/README.rst create mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg create mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/setup.py create mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py rename exporter/{opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf => opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2}/__init__.py (98%) create mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen/__init__.py rename exporter/{opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf => opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2}/gen/zipkin_pb2.py (100%) rename exporter/{opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf => opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2}/gen/zipkin_pb2.pyi (100%) create mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py create mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/tests/__init__.py create mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/__init__.py create mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py rename exporter/{opentelemetry-exporter-zipkin => opentelemetry-exporter-zipkin-proto-http}/tests/encoder/test_v2_protobuf.py (98%) rename exporter/{opentelemetry-exporter-zipkin => opentelemetry-exporter-zipkin-proto-http}/tests/test_zipkin_exporter.py (90%) delete mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/__init__.py create mode 100644 exporter/opentelemetry-exporter-zipkin/tests/test_zipkin.py diff --git a/.flake8 b/.flake8 index 33658400ce..c66d869076 100644 --- a/.flake8 +++ b/.flake8 @@ -20,7 +20,7 @@ exclude = exporter/opentelemetry-exporter-jaeger-proto-grpc/build/* exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/ exporter/opentelemetry-exporter-jaeger-thrift/build/* - exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/ + exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen/ docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ docs/examples/opentelemetry-example-app/build/* opentelemetry-proto/build/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f43369ea9..7a8e24366d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1703](https://github.com/open-telemetry/opentelemetry-python/pull/1703)) - Fixed an unset `OTEL_TRACES_EXPORTER` resulting in an error ([#1707](https://github.com/open-telemetry/opentelemetry-python/pull/1707)) +- Split Zipkin exporter into `opentelemetry-exporter-zipkin-json` and + `opentelemetry-exporter-zipkin-proto-http` packages to reduce dependencies. The + `opentelemetry-exporter-zipkin` installs both. + ([#1699](https://github.com/open-telemetry/opentelemetry-python/pull/1699)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. diff --git a/docs/exporter/zipkin/zipkin.rst b/docs/exporter/zipkin/zipkin.rst index 18042022a4..a33b7f5de1 100644 --- a/docs/exporter/zipkin/zipkin.rst +++ b/docs/exporter/zipkin/zipkin.rst @@ -1,7 +1,17 @@ -Opentelemetry Zipkin Exporter -============================= +OpenTelemetry Zipkin Exporters +============================== .. automodule:: opentelemetry.exporter.zipkin :members: :undoc-members: :show-inheritance: + +.. automodule:: opentelemetry.exporter.zipkin.json + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: opentelemetry.exporter.zipkin.proto.http + :members: + :undoc-members: + :show-inheritance: diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/util.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/util.py index 9b9e720190..e08b884c3f 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/util.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/util.py @@ -82,9 +82,9 @@ def add_proto_attribute_value(pb_attributes, key, value): def get_node(service_name, host_name): """Generates Node message from params and system information. - Args: - service_name: Name of Collector service. - host_name: Host name. + Args: + service_name: Name of Collector service. + host_name: Host name. """ return common_pb2.Node( identifier=common_pb2.ProcessIdentifier( diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin-json/CHANGELOG.md similarity index 100% rename from exporter/opentelemetry-exporter-zipkin/CHANGELOG.md rename to exporter/opentelemetry-exporter-zipkin-json/CHANGELOG.md diff --git a/exporter/opentelemetry-exporter-zipkin-json/LICENSE b/exporter/opentelemetry-exporter-zipkin-json/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-json/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/exporter/opentelemetry-exporter-zipkin-json/MANIFEST.in b/exporter/opentelemetry-exporter-zipkin-json/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-json/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/exporter/opentelemetry-exporter-zipkin-json/README.rst b/exporter/opentelemetry-exporter-zipkin-json/README.rst new file mode 100644 index 0000000000..cfb7b1fa53 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-json/README.rst @@ -0,0 +1,25 @@ +OpenTelemetry Zipkin JSON Exporter +================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-zipkin-json.svg + :target: https://pypi.org/project/opentelemetry-exporter-zipkin-json/ + +This library allows export of tracing data to `Zipkin `_ using JSON +for serialization. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-zipkin-json + + +References +---------- + +* `OpenTelemetry Zipkin Exporter `_ +* `Zipkin `_ +* `OpenTelemetry Project `_ diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg new file mode 100644 index 0000000000..0f8a7c900a --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-exporter-zipkin-json +description = Zipkin Span JSON Exporter for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-zipkin-json +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + requests ~= 2.7 + opentelemetry-api == 1.0.0.dev0 + opentelemetry-sdk == 1.0.0.dev0 + +[options.packages.find] +where = src + +[options.extras_require] +test = + +[options.entry_points] +opentelemetry_exporter = + zipkin_json = opentelemetry.exporter.zipkin.json:ZipkinExporter \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.py b/exporter/opentelemetry-exporter-zipkin-json/setup.py new file mode 100644 index 0000000000..1e8b53811c --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.py @@ -0,0 +1,32 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "exporter", + "zipkin", + "json", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py similarity index 99% rename from exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py rename to exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py index 8705a7bc37..3390c19a46 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py @@ -49,9 +49,8 @@ class Protocol(Enum): OS environ var OTEL_EXPORTER_ZIPKIN_PROTOCOL (reserved for future usage). """ - V1_JSON = "v1_json" - V2_JSON = "v2_json" - V2_PROTOBUF = "v2_protobuf" + V1 = "v1" + V2 = "v2" # pylint: disable=W0223 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py similarity index 85% rename from exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py rename to exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py index afb29b239f..55e3a565ec 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py @@ -13,15 +13,17 @@ # limitations under the License. """ +OpenTelemetry Zipkin JSON Exporter +---------------------------------- + This library allows to export tracing data to `Zipkin `_. Usage ----- -The **OpenTelemetry Zipkin Exporter** allows exporting of `OpenTelemetry`_ +The **OpenTelemetry Zipkin JSON Exporter** allows exporting of `OpenTelemetry`_ traces to `Zipkin`_. This exporter sends traces to the configured Zipkin -collector endpoint using HTTP and supports multiple protocols (v1 json, -v2 json, v2 protobuf). +collector endpoint using JSON over HTTP and supports multiple versions (v1, v2). .. _Zipkin: https://zipkin.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ @@ -30,7 +32,7 @@ .. code:: python from opentelemetry import trace - from opentelemetry.exporter import zipkin + from opentelemetry.exporter.zipkin.json import ZipkinExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor @@ -38,8 +40,8 @@ tracer = trace.get_tracer(__name__) # create a ZipkinExporter - zipkin_exporter = zipkin.ZipkinExporter( - # protocol=Protocol.V2_PROTOBUF + zipkin_exporter = ZipkinExporter( + # version=Protocol.V2 # optional: # endpoint="http://localhost:9411/api/v2/spans", # local_node_ipv4="192.168.0.1", @@ -76,9 +78,8 @@ Encoder, Protocol, ) -from opentelemetry.exporter.zipkin.encoder.v1.json import JsonV1Encoder -from opentelemetry.exporter.zipkin.encoder.v2.json import JsonV2Encoder -from opentelemetry.exporter.zipkin.encoder.v2.protobuf import ProtobufEncoder +from opentelemetry.exporter.zipkin.json.v1 import JsonV1Encoder +from opentelemetry.exporter.zipkin.json.v2 import JsonV2Encoder from opentelemetry.exporter.zipkin.node_endpoint import IpInput, NodeEndpoint from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_ZIPKIN_ENDPOINT, @@ -96,7 +97,7 @@ class ZipkinExporter(SpanExporter): def __init__( self, - protocol: Protocol, + version: Protocol = Protocol.V2, endpoint: Optional[str] = None, local_node_ipv4: IpInput = None, local_node_ipv6: IpInput = None, @@ -113,12 +114,10 @@ def __init__( ) self.endpoint = endpoint - if protocol == Protocol.V1_JSON: + if version == Protocol.V1: self.encoder = JsonV1Encoder(max_tag_value_length) - elif protocol == Protocol.V2_JSON: + elif version == Protocol.V2: self.encoder = JsonV2Encoder(max_tag_value_length) - elif protocol == Protocol.V2_PROTOBUF: - self.encoder = ProtobufEncoder(max_tag_value_length) def export(self, spans: Sequence[Span]) -> SpanExportResult: # Populate service_name from first span diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/json.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/v1/__init__.py similarity index 73% rename from exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/json.py rename to exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/v1/__init__.py index 966657311b..2fcb4ab168 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/json.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/v1/__init__.py @@ -14,13 +14,33 @@ """Zipkin Export Encoders for JSON formats """ -from typing import Dict -from opentelemetry.exporter.zipkin.encoder import JsonEncoder -from opentelemetry.exporter.zipkin.encoder.v1 import V1Encoder +import abc +from typing import Dict, List + +from opentelemetry.exporter.zipkin.encoder import Encoder, JsonEncoder from opentelemetry.trace import Span +# pylint: disable=W0223 +class V1Encoder(Encoder): + def _extract_binary_annotations( + self, span: Span, encoded_local_endpoint: Dict + ) -> List[Dict]: + binary_annotations = [] + for tag_key, tag_value in self._extract_tags_from_span(span).items(): + if isinstance(tag_value, str) and self.max_tag_value_length > 0: + tag_value = tag_value[: self.max_tag_value_length] + binary_annotations.append( + { + "key": tag_key, + "value": tag_value, + "endpoint": encoded_local_endpoint, + } + ) + return binary_annotations + + class JsonV1Encoder(JsonEncoder, V1Encoder): """Zipkin Export Encoder for JSON v1 API diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/json.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/v2/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/json.py rename to exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/v2/__init__.py diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py new file mode 100644 index 0000000000..e4529dffc6 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "1.0.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/node_endpoint.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/node_endpoint.py similarity index 100% rename from exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/node_endpoint.py rename to exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/node_endpoint.py diff --git a/exporter/opentelemetry-exporter-zipkin/tests/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/tests/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-zipkin/tests/__init__.py rename to exporter/opentelemetry-exporter-zipkin-json/tests/__init__.py diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/__init__.py rename to exporter/opentelemetry-exporter-zipkin-json/tests/encoder/__init__.py diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py similarity index 100% rename from exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py rename to exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py similarity index 99% rename from exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py rename to exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py index c8cdb0edac..c1956500bc 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py @@ -15,7 +15,7 @@ from opentelemetry import trace as trace_api from opentelemetry.exporter.zipkin.encoder import NAME_KEY, VERSION_KEY -from opentelemetry.exporter.zipkin.encoder.v1.json import JsonV1Encoder +from opentelemetry.exporter.zipkin.json.v1 import JsonV1Encoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace from opentelemetry.trace import TraceFlags, format_span_id, format_trace_id diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_json.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py similarity index 99% rename from exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_json.py rename to exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py index 369eef6895..eb0ad6000a 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_json.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py @@ -15,7 +15,7 @@ from opentelemetry import trace as trace_api from opentelemetry.exporter.zipkin.encoder import NAME_KEY, VERSION_KEY -from opentelemetry.exporter.zipkin.encoder.v2.json import JsonV2Encoder +from opentelemetry.exporter.zipkin.json.v2 import JsonV2Encoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace from opentelemetry.trace import SpanKind, TraceFlags diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py new file mode 100644 index 0000000000..aa2ad9e625 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py @@ -0,0 +1,188 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import ipaddress +import os +import unittest +from unittest.mock import patch + +from opentelemetry import trace +from opentelemetry.exporter.zipkin.encoder import Protocol +from opentelemetry.exporter.zipkin.json import DEFAULT_ENDPOINT, ZipkinExporter +from opentelemetry.exporter.zipkin.json.v2 import JsonV2Encoder +from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_ZIPKIN_ENDPOINT, +) +from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace import TracerProvider, _Span +from opentelemetry.sdk.trace.export import SpanExportResult + +TEST_SERVICE_NAME = "test_service" + + +class MockResponse: + def __init__(self, status_code): + self.status_code = status_code + self.text = status_code + + +class TestZipkinExporter(unittest.TestCase): + @classmethod + def setUpClass(cls): + trace.set_tracer_provider( + TracerProvider( + resource=Resource({SERVICE_NAME: TEST_SERVICE_NAME}) + ) + ) + + def tearDown(self): + if OTEL_EXPORTER_ZIPKIN_ENDPOINT in os.environ: + del os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] + + def test_constructor_default(self): + exporter = ZipkinExporter() + self.assertIsInstance(exporter.encoder, JsonV2Encoder) + self.assertEqual(exporter.endpoint, DEFAULT_ENDPOINT) + self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) + self.assertEqual(exporter.local_node.ipv4, None) + self.assertEqual(exporter.local_node.ipv6, None) + self.assertEqual(exporter.local_node.port, None) + + def test_constructor_env_vars(self): + os_endpoint = "https://foo:9911/path" + os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint + + exporter = ZipkinExporter() + + self.assertEqual(exporter.endpoint, os_endpoint) + self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) + self.assertEqual(exporter.local_node.ipv4, None) + self.assertEqual(exporter.local_node.ipv6, None) + self.assertEqual(exporter.local_node.port, None) + + def test_constructor_protocol_endpoint(self): + """Test the constructor for the common usage of providing the + protocol and endpoint arguments.""" + endpoint = "https://opentelemetry.io:15875/myapi/traces?format=zipkin" + + exporter = ZipkinExporter(endpoint=endpoint) + + self.assertIsInstance(exporter.encoder, JsonV2Encoder) + self.assertEqual(exporter.endpoint, endpoint) + self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) + self.assertEqual(exporter.local_node.ipv4, None) + self.assertEqual(exporter.local_node.ipv6, None) + self.assertEqual(exporter.local_node.port, None) + + def test_constructor_all_params_and_env_vars(self): + """Test the scenario where all params are provided and all OS env + vars are set. Explicit params should take precedence. + """ + os_endpoint = "https://os.env.param:9911/path" + os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint + + constructor_param_version = Protocol.V2 + constructor_param_endpoint = "https://constructor.param:9911/path" + local_node_ipv4 = "192.168.0.1" + local_node_ipv6 = "2001:db8::1000" + local_node_port = 30301 + max_tag_value_length = 56 + + exporter = ZipkinExporter( + constructor_param_version, + constructor_param_endpoint, + local_node_ipv4, + local_node_ipv6, + local_node_port, + max_tag_value_length, + ) + + self.assertIsInstance(exporter.encoder, JsonV2Encoder) + self.assertEqual(exporter.endpoint, constructor_param_endpoint) + self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) + self.assertEqual( + exporter.local_node.ipv4, ipaddress.IPv4Address(local_node_ipv4) + ) + self.assertEqual( + exporter.local_node.ipv6, ipaddress.IPv6Address(local_node_ipv6) + ) + self.assertEqual(exporter.local_node.port, local_node_port) + + @patch("requests.post") + def test_export_success(self, mock_post): + mock_post.return_value = MockResponse(200) + spans = [] + exporter = ZipkinExporter() + status = exporter.export(spans) + self.assertEqual(SpanExportResult.SUCCESS, status) + + @patch("requests.post") + def test_export_invalid_response(self, mock_post): + mock_post.return_value = MockResponse(404) + spans = [] + exporter = ZipkinExporter() + status = exporter.export(spans) + self.assertEqual(SpanExportResult.FAILURE, status) + + @patch("requests.post") + def test_export_span_service_name(self, mock_post): + mock_post.return_value = MockResponse(200) + resource = Resource.create({SERVICE_NAME: "test"}) + context = trace.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + ) + span = _Span("test_span", context=context, resource=resource) + span.start() + span.end() + exporter = ZipkinExporter() + exporter.export([span]) + self.assertEqual(exporter.local_node.service_name, "test") + + +class TestZipkinNodeEndpoint(unittest.TestCase): + def test_constructor_default(self): + node_endpoint = NodeEndpoint() + self.assertEqual(node_endpoint.ipv4, None) + self.assertEqual(node_endpoint.ipv6, None) + self.assertEqual(node_endpoint.port, None) + self.assertEqual(node_endpoint.service_name, TEST_SERVICE_NAME) + + def test_constructor_explicits(self): + ipv4 = "192.168.0.1" + ipv6 = "2001:db8::c001" + port = 414120 + node_endpoint = NodeEndpoint(ipv4, ipv6, port) + self.assertEqual(node_endpoint.ipv4, ipaddress.IPv4Address(ipv4)) + self.assertEqual(node_endpoint.ipv6, ipaddress.IPv6Address(ipv6)) + self.assertEqual(node_endpoint.port, port) + self.assertEqual(node_endpoint.service_name, TEST_SERVICE_NAME) + + def test_ipv4_invalid_raises_error(self): + with self.assertRaises(ValueError): + NodeEndpoint(ipv4="invalid-ipv4-address") + + def test_ipv4_passed_ipv6_raises_error(self): + with self.assertRaises(ValueError): + NodeEndpoint(ipv4="2001:db8::c001") + + def test_ipv6_invalid_raises_error(self): + with self.assertRaises(ValueError): + NodeEndpoint(ipv6="invalid-ipv6-address") + + def test_ipv6_passed_ipv4_raises_error(self): + with self.assertRaises(ValueError): + NodeEndpoint(ipv6="192.168.0.1") diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/CHANGELOG.md similarity index 100% rename from exporter/opentelemetry-exporter-zipkin/tests/encoder/__init__.py rename to exporter/opentelemetry-exporter-zipkin-proto-http/CHANGELOG.md diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/LICENSE b/exporter/opentelemetry-exporter-zipkin-proto-http/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/MANIFEST.in b/exporter/opentelemetry-exporter-zipkin-proto-http/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/README.rst b/exporter/opentelemetry-exporter-zipkin-proto-http/README.rst new file mode 100644 index 0000000000..12801dbf37 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/README.rst @@ -0,0 +1,25 @@ +OpenTelemetry Zipkin Protobuf Exporter +====================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-zipkin-proto-http.svg + :target: https://pypi.org/project/opentelemetry-exporter-zipkin-proto-http/ + +This library allows export of tracing data to `Zipkin `_ using Protobuf +for serialization. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-zipkin-proto-http + + +References +---------- + +* `OpenTelemetry Zipkin Exporter `_ +* `Zipkin `_ +* `OpenTelemetry Project `_ diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg new file mode 100644 index 0000000000..96e67653a7 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -0,0 +1,57 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-exporter-zipkin-proto-http +description = Zipkin Span Protobuf Exporter for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-zipkin-proto-http +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + protobuf >= 3.12 + requests ~= 2.7 + opentelemetry-api == 1.0.0.dev0 + opentelemetry-sdk == 1.0.0.dev0 + opentelemetry-exporter-zipkin-json == 1.0.0.dev0 + +[options.packages.find] +where = src + +[options.extras_require] +test = + +[options.entry_points] +opentelemetry_exporter = + zipkin_proto = opentelemetry.exporter.zipkin.proto.http:ZipkinExporter \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.py b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.py new file mode 100644 index 0000000000..707e6c1a12 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.py @@ -0,0 +1,33 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "exporter", + "zipkin", + "proto", + "http", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py new file mode 100644 index 0000000000..fbc8cc03cf --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py @@ -0,0 +1,142 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +OpenTelemetry Zipkin Protobuf Exporter +-------------------------------------- + +This library allows to export tracing data to `Zipkin `_. + +Usage +----- + +The **OpenTelemetry Zipkin Exporter** allows exporting of `OpenTelemetry`_ +traces to `Zipkin`_. This exporter sends traces to the configured Zipkin +collector endpoint using HTTP and supports v2 protobuf. + +.. _Zipkin: https://zipkin.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +.. _Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#zipkin-exporter + +.. code:: python + + from opentelemetry import trace + from opentelemetry.exporter.zipkin.proto.http import ZipkinExporter + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + + trace.set_tracer_provider(TracerProvider()) + tracer = trace.get_tracer(__name__) + + # create a ZipkinExporter + zipkin_exporter = ZipkinExporter( + # optional: + # endpoint="http://localhost:9411/api/v2/spans", + # local_node_ipv4="192.168.0.1", + # local_node_ipv6="2001:db8::c001", + # local_node_port=31313, + # max_tag_value_length=256 + ) + + # Create a BatchSpanProcessor and add the exporter to it + span_processor = BatchSpanProcessor(zipkin_exporter) + + # add to the tracer + trace.get_tracer_provider().add_span_processor(span_processor) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + +The exporter supports the following environment variable for configuration: + +- :envvar:`OTEL_EXPORTER_ZIPKIN_ENDPOINT` + +API +--- +""" + +import logging +from os import environ +from typing import Optional, Sequence + +import requests + +from opentelemetry.exporter.zipkin.encoder import ( + DEFAULT_MAX_TAG_VALUE_LENGTH, + Encoder, + Protocol, +) +from opentelemetry.exporter.zipkin.proto.http.v2 import ProtobufEncoder +from opentelemetry.exporter.zipkin.node_endpoint import IpInput, NodeEndpoint +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_ZIPKIN_ENDPOINT, +) +from opentelemetry.sdk.resources import SERVICE_NAME +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.trace import Span + +DEFAULT_ENDPOINT = "http://localhost:9411/api/v2/spans" +REQUESTS_SUCCESS_STATUS_CODES = (200, 202) + +logger = logging.getLogger(__name__) + + +class ZipkinExporter(SpanExporter): + def __init__( + self, + endpoint: Optional[str] = None, + local_node_ipv4: IpInput = None, + local_node_ipv6: IpInput = None, + local_node_port: Optional[int] = None, + max_tag_value_length: Optional[int] = None, + ): + self.local_node = NodeEndpoint( + local_node_ipv4, local_node_ipv6, local_node_port + ) + + if endpoint is None: + endpoint = ( + environ.get(OTEL_EXPORTER_ZIPKIN_ENDPOINT) or DEFAULT_ENDPOINT + ) + self.endpoint = endpoint + + self.encoder = ProtobufEncoder(max_tag_value_length) + + def export(self, spans: Sequence[Span]) -> SpanExportResult: + # Populate service_name from first span + # We restrict any SpanProcessor to be only associated with a single + # TracerProvider, so it is safe to assume that all Spans in a single + # batch all originate from one TracerProvider (and in turn have all + # the same service.name) + if spans: + service_name = spans[0].resource.attributes.get(SERVICE_NAME) + if service_name: + self.local_node.service_name = service_name + result = requests.post( + url=self.endpoint, + data=self.encoder.serialize(spans, self.local_node), + headers={"Content-Type": self.encoder.content_type()}, + ) + + if result.status_code not in REQUESTS_SUCCESS_STATUS_CODES: + logger.error( + "Traces cannot be uploaded; status code: %s, message %s", + result.status_code, + result.text, + ) + return SpanExportResult.FAILURE + return SpanExportResult.SUCCESS + + def shutdown(self) -> None: + pass diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/__init__.py similarity index 98% rename from exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/__init__.py rename to exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/__init__.py index f0cdb65b37..b54761e166 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/__init__.py @@ -19,7 +19,7 @@ from typing import List, Optional, Sequence from opentelemetry.exporter.zipkin.encoder import Encoder -from opentelemetry.exporter.zipkin.encoder.v2.protobuf.gen import zipkin_pb2 +from opentelemetry.exporter.zipkin.proto.http.v2.gen import zipkin_pb2 from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk.trace import Event from opentelemetry.trace import Span, SpanContext, SpanKind diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/zipkin_pb2.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen/zipkin_pb2.py similarity index 100% rename from exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/zipkin_pb2.py rename to exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen/zipkin_pb2.py diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/zipkin_pb2.pyi b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen/zipkin_pb2.pyi similarity index 100% rename from exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/zipkin_pb2.pyi rename to exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen/zipkin_pb2.pyi diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py new file mode 100644 index 0000000000..e4529dffc6 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "1.0.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py new file mode 100644 index 0000000000..a04148b6ea --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py @@ -0,0 +1,505 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import abc +import json +import sys +import unittest +from typing import Dict, List + +from opentelemetry import trace as trace_api +from opentelemetry.exporter.zipkin.encoder import ( + DEFAULT_MAX_TAG_VALUE_LENGTH, + Encoder, +) +from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint +from opentelemetry.sdk import trace +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.trace import TraceFlags +from opentelemetry.trace.status import Status, StatusCode + +TEST_SERVICE_NAME = "test_service" + + +# pylint: disable=protected-access +class CommonEncoderTestCases: + class CommonEncoderTest(unittest.TestCase): + @staticmethod + @abc.abstractmethod + def get_encoder(*args, **kwargs) -> Encoder: + pass + + @classmethod + def get_encoder_default(cls) -> Encoder: + return cls.get_encoder() + + @abc.abstractmethod + def test_encode_trace_id(self): + pass + + @abc.abstractmethod + def test_encode_span_id(self): + pass + + @abc.abstractmethod + def test_encode_local_endpoint_default(self): + pass + + @abc.abstractmethod + def test_encode_local_endpoint_explicits(self): + pass + + @abc.abstractmethod + def _test_encode_max_tag_length(self, max_tag_value_length: int): + pass + + def test_encode_max_tag_length_2(self): + self._test_encode_max_tag_length(2) + + def test_encode_max_tag_length_5(self): + self._test_encode_max_tag_length(5) + + def test_encode_max_tag_length_9(self): + self._test_encode_max_tag_length(9) + + def test_encode_max_tag_length_10(self): + self._test_encode_max_tag_length(10) + + def test_encode_max_tag_length_11(self): + self._test_encode_max_tag_length(11) + + def test_encode_max_tag_length_128(self): + self._test_encode_max_tag_length(128) + + def test_constructor_default(self): + encoder = self.get_encoder() + + self.assertEqual( + DEFAULT_MAX_TAG_VALUE_LENGTH, encoder.max_tag_value_length + ) + + def test_constructor_max_tag_value_length(self): + max_tag_value_length = 123456 + encoder = self.get_encoder(max_tag_value_length) + self.assertEqual( + max_tag_value_length, encoder.max_tag_value_length + ) + + def test_nsec_to_usec_round(self): + base_time_nsec = 683647322 * 10 ** 9 + for nsec in ( + base_time_nsec, + base_time_nsec + 150 * 10 ** 6, + base_time_nsec + 300 * 10 ** 6, + base_time_nsec + 400 * 10 ** 6, + ): + self.assertEqual( + (nsec + 500) // 10 ** 3, + self.get_encoder_default()._nsec_to_usec_round(nsec), + ) + + def test_encode_debug(self): + self.assertFalse( + self.get_encoder_default()._encode_debug( + trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.DEFAULT), + ) + ) + ) + self.assertTrue( + self.get_encoder_default()._encode_debug( + trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + ) + ) + + def test_get_parent_id_from_span(self): + parent_id = 0x00000000DEADBEF0 + self.assertEqual( + parent_id, + self.get_encoder_default()._get_parent_id( + trace._Span( + name="test-span", + context=trace_api.SpanContext( + 0x000000000000000000000000DEADBEEF, + 0x04BF92DEEFC58C92, + is_remote=False, + ), + parent=trace_api.SpanContext( + 0x0000000000000000000000AADEADBEEF, + parent_id, + is_remote=False, + ), + ) + ), + ) + + def test_get_parent_id_from_span_context(self): + parent_id = 0x00000000DEADBEF0 + self.assertEqual( + parent_id, + self.get_encoder_default()._get_parent_id( + trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=parent_id, + is_remote=False, + ), + ), + ) + + @staticmethod + def get_data_for_max_tag_length_test( + max_tag_length: int, + ) -> (trace._Span, Dict): + start_time = 683647322 * 10 ** 9 # in ns + duration = 50 * 10 ** 6 + end_time = start_time + duration + + span = trace._Span( + name=TEST_SERVICE_NAME, + context=trace_api.SpanContext( + 0x0E0C63257DE34C926F9EFCD03927272E, + 0x04BF92DEEFC58C92, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ), + resource=trace.Resource({}), + ) + span.start(start_time=start_time) + span.set_attribute("string1", "v" * 500) + span.set_attribute("string2", "v" * 50) + span.set_attribute("list1", ["a"] * 25) + span.set_attribute("list2", ["a"] * 10) + span.set_attribute("list3", [2] * 25) + span.set_attribute("list4", [2] * 10) + span.set_attribute("list5", [True] * 25) + span.set_attribute("list6", [True] * 10) + span.set_attribute("tuple1", ("a",) * 25) + span.set_attribute("tuple2", ("a",) * 10) + span.set_attribute("tuple3", (2,) * 25) + span.set_attribute("tuple4", (2,) * 10) + span.set_attribute("tuple5", (True,) * 25) + span.set_attribute("tuple6", (True,) * 10) + span.set_attribute("range1", range(0, 25)) + span.set_attribute("range2", range(0, 10)) + span.set_attribute("empty_list", []) + span.set_attribute("none_list", ["hello", None, "world"]) + span.end(end_time=end_time) + + expected_outputs = { + 2: { + "string1": "vv", + "string2": "vv", + "list1": "[]", + "list2": "[]", + "list3": "[]", + "list4": "[]", + "list5": "[]", + "list6": "[]", + "tuple1": "[]", + "tuple2": "[]", + "tuple3": "[]", + "tuple4": "[]", + "tuple5": "[]", + "tuple6": "[]", + "range1": "[]", + "range2": "[]", + "empty_list": "[]", + "none_list": "[]", + }, + 5: { + "string1": "vvvvv", + "string2": "vvvvv", + "list1": '["a"]', + "list2": '["a"]', + "list3": '["2"]', + "list4": '["2"]', + "list5": "[]", + "list6": "[]", + "tuple1": '["a"]', + "tuple2": '["a"]', + "tuple3": '["2"]', + "tuple4": '["2"]', + "tuple5": "[]", + "tuple6": "[]", + "range1": '["0"]', + "range2": '["0"]', + "empty_list": "[]", + "none_list": "[]", + }, + 9: { + "string1": "vvvvvvvvv", + "string2": "vvvvvvvvv", + "list1": '["a","a"]', + "list2": '["a","a"]', + "list3": '["2","2"]', + "list4": '["2","2"]', + "list5": '["true"]', + "list6": '["true"]', + "tuple1": '["a","a"]', + "tuple2": '["a","a"]', + "tuple3": '["2","2"]', + "tuple4": '["2","2"]', + "tuple5": '["true"]', + "tuple6": '["true"]', + "range1": '["0","1"]', + "range2": '["0","1"]', + "empty_list": "[]", + "none_list": '["hello"]', + }, + 10: { + "string1": "vvvvvvvvvv", + "string2": "vvvvvvvvvv", + "list1": '["a","a"]', + "list2": '["a","a"]', + "list3": '["2","2"]', + "list4": '["2","2"]', + "list5": '["true"]', + "list6": '["true"]', + "tuple1": '["a","a"]', + "tuple2": '["a","a"]', + "tuple3": '["2","2"]', + "tuple4": '["2","2"]', + "tuple5": '["true"]', + "tuple6": '["true"]', + "range1": '["0","1"]', + "range2": '["0","1"]', + "empty_list": "[]", + "none_list": '["hello"]', + }, + 11: { + "string1": "vvvvvvvvvvv", + "string2": "vvvvvvvvvvv", + "list1": '["a","a"]', + "list2": '["a","a"]', + "list3": '["2","2"]', + "list4": '["2","2"]', + "list5": '["true"]', + "list6": '["true"]', + "tuple1": '["a","a"]', + "tuple2": '["a","a"]', + "tuple3": '["2","2"]', + "tuple4": '["2","2"]', + "tuple5": '["true"]', + "tuple6": '["true"]', + "range1": '["0","1"]', + "range2": '["0","1"]', + "empty_list": "[]", + "none_list": '["hello"]', + }, + 128: { + "string1": "v" * 128, + "string2": "v" * 50, + "list1": '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', + "list2": '["a","a","a","a","a","a","a","a","a","a"]', + "list3": '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', + "list4": '["2","2","2","2","2","2","2","2","2","2"]', + "list5": '["true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true"]', + "list6": '["true","true","true","true","true","true","true","true","true","true"]', + "tuple1": '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', + "tuple2": '["a","a","a","a","a","a","a","a","a","a"]', + "tuple3": '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', + "tuple4": '["2","2","2","2","2","2","2","2","2","2"]', + "tuple5": '["true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true"]', + "tuple6": '["true","true","true","true","true","true","true","true","true","true"]', + "range1": '["0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24"]', + "range2": '["0","1","2","3","4","5","6","7","8","9"]', + "empty_list": "[]", + "none_list": '["hello",null,"world"]', + }, + } + + return span, expected_outputs[max_tag_length] + + @staticmethod + def get_exhaustive_otel_span_list() -> List[trace._Span]: + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + + base_time = 683647322 * 10 ** 9 # in ns + start_times = ( + base_time, + base_time + 150 * 10 ** 6, + base_time + 300 * 10 ** 6, + base_time + 400 * 10 ** 6, + ) + end_times = ( + start_times[0] + (50 * 10 ** 6), + start_times[1] + (100 * 10 ** 6), + start_times[2] + (200 * 10 ** 6), + start_times[3] + (300 * 10 ** 6), + ) + + parent_span_context = trace_api.SpanContext( + trace_id, 0x1111111111111111, is_remote=False + ) + + other_context = trace_api.SpanContext( + trace_id, 0x2222222222222222, is_remote=False + ) + + span1 = trace._Span( + name="test-span-1", + context=trace_api.SpanContext( + trace_id, + 0x34BF92DEEFC58C92, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ), + parent=parent_span_context, + events=( + trace.Event( + name="event0", + timestamp=base_time + 50 * 10 ** 6, + attributes={ + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + }, + ), + ), + links=( + trace_api.Link( + context=other_context, attributes={"key_bool": True} + ), + ), + resource=trace.Resource({}), + ) + span1.start(start_time=start_times[0]) + span1.set_attribute("key_bool", False) + span1.set_attribute("key_string", "hello_world") + span1.set_attribute("key_float", 111.22) + span1.set_status(Status(StatusCode.OK)) + span1.end(end_time=end_times[0]) + + span2 = trace._Span( + name="test-span-2", + context=parent_span_context, + parent=None, + resource=trace.Resource( + attributes={"key_resource": "some_resource"} + ), + ) + span2.start(start_time=start_times[1]) + span2.set_status(Status(StatusCode.ERROR, "Example description")) + span2.end(end_time=end_times[1]) + + span3 = trace._Span( + name="test-span-3", + context=other_context, + parent=None, + resource=trace.Resource( + attributes={"key_resource": "some_resource"} + ), + ) + span3.start(start_time=start_times[2]) + span3.set_attribute("key_string", "hello_world") + span3.end(end_time=end_times[2]) + + span4 = trace._Span( + name="test-span-3", + context=other_context, + parent=None, + resource=trace.Resource({}), + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), + ) + span4.start(start_time=start_times[3]) + span4.end(end_time=end_times[3]) + + return [span1, span2, span3, span4] + + # pylint: disable=W0223 + class CommonJsonEncoderTest(CommonEncoderTest, abc.ABC): + def test_encode_trace_id(self): + for trace_id in (1, 1024, 2 ** 32, 2 ** 64, 2 ** 65): + self.assertEqual( + format(trace_id, "032x"), + self.get_encoder_default()._encode_trace_id(trace_id), + ) + + def test_encode_span_id(self): + for span_id in (1, 1024, 2 ** 8, 2 ** 16, 2 ** 32, 2 ** 64): + self.assertEqual( + format(span_id, "016x"), + self.get_encoder_default()._encode_span_id(span_id), + ) + + def test_encode_local_endpoint_default(self): + self.assertEqual( + self.get_encoder_default()._encode_local_endpoint( + NodeEndpoint() + ), + {"serviceName": TEST_SERVICE_NAME}, + ) + + def test_encode_local_endpoint_explicits(self): + ipv4 = "192.168.0.1" + ipv6 = "2001:db8::c001" + port = 414120 + self.assertEqual( + self.get_encoder_default()._encode_local_endpoint( + NodeEndpoint(ipv4, ipv6, port) + ), + { + "serviceName": TEST_SERVICE_NAME, + "ipv4": ipv4, + "ipv6": ipv6, + "port": port, + }, + ) + + @staticmethod + def pop_and_sort(source_list, source_index, sort_key): + """ + Convenience method that will pop a specified index from a list, + sort it by a given key and then return it. + """ + popped_item = source_list.pop(source_index, None) + if popped_item is not None: + popped_item = sorted(popped_item, key=lambda x: x[sort_key]) + return popped_item + + def assert_equal_encoded_spans(self, expected_spans, actual_spans): + if sys.version_info.major == 3 and sys.version_info.minor <= 5: + expected_spans = json.loads(expected_spans) + actual_spans = json.loads(actual_spans) + for expected_span, actual_span in zip( + expected_spans, actual_spans + ): + actual_annotations = self.pop_and_sort( + actual_span, "annotations", "timestamp" + ) + expected_annotations = self.pop_and_sort( + expected_span, "annotations", "timestamp" + ) + expected_binary_annotations = self.pop_and_sort( + expected_span, "binaryAnnotations", "key" + ) + actual_binary_annotations = self.pop_and_sort( + actual_span, "binaryAnnotations", "key" + ) + self.assertEqual(actual_span, expected_span) + self.assertEqual(actual_annotations, expected_annotations) + self.assertEqual( + actual_binary_annotations, expected_binary_annotations + ) + else: + self.assertEqual(expected_spans, actual_spans) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_protobuf.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py similarity index 98% rename from exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_protobuf.py rename to exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py index 092a8a303c..8cf9f6abf5 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_protobuf.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py @@ -15,9 +15,9 @@ import json from opentelemetry.exporter.zipkin.encoder import NAME_KEY, VERSION_KEY -from opentelemetry.exporter.zipkin.encoder.v2.protobuf import ProtobufEncoder -from opentelemetry.exporter.zipkin.encoder.v2.protobuf.gen import zipkin_pb2 from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint +from opentelemetry.exporter.zipkin.proto.http.v2 import ProtobufEncoder +from opentelemetry.exporter.zipkin.proto.http.v2.gen import zipkin_pb2 from opentelemetry.trace import SpanKind from .common_tests import TEST_SERVICE_NAME, CommonEncoderTestCases diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py similarity index 90% rename from exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py rename to exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py index 23da474730..5537ec8d1a 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py @@ -18,10 +18,12 @@ from unittest.mock import patch from opentelemetry import trace -from opentelemetry.exporter.zipkin import DEFAULT_ENDPOINT, ZipkinExporter -from opentelemetry.exporter.zipkin.encoder import Protocol -from opentelemetry.exporter.zipkin.encoder.v2.protobuf import ProtobufEncoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint +from opentelemetry.exporter.zipkin.proto.http import ( + DEFAULT_ENDPOINT, + ZipkinExporter, +) +from opentelemetry.exporter.zipkin.proto.http.v2 import ProtobufEncoder from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_ZIPKIN_ENDPOINT, ) @@ -52,7 +54,7 @@ def tearDown(self): del os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] def test_constructor_default(self): - exporter = ZipkinExporter(Protocol.V2_PROTOBUF) + exporter = ZipkinExporter() self.assertIsInstance(exporter.encoder, ProtobufEncoder) self.assertEqual(exporter.endpoint, DEFAULT_ENDPOINT) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) @@ -64,7 +66,7 @@ def test_constructor_env_vars(self): os_endpoint = "https://foo:9911/path" os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint - exporter = ZipkinExporter(Protocol.V2_PROTOBUF) + exporter = ZipkinExporter() self.assertEqual(exporter.endpoint, os_endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) @@ -75,10 +77,9 @@ def test_constructor_env_vars(self): def test_constructor_protocol_endpoint(self): """Test the constructor for the common usage of providing the protocol and endpoint arguments.""" - protocol = Protocol.V2_PROTOBUF endpoint = "https://opentelemetry.io:15875/myapi/traces?format=zipkin" - exporter = ZipkinExporter(protocol, endpoint) + exporter = ZipkinExporter(endpoint) self.assertIsInstance(exporter.encoder, ProtobufEncoder) self.assertEqual(exporter.endpoint, endpoint) @@ -94,7 +95,6 @@ def test_constructor_all_params_and_env_vars(self): os_endpoint = "https://os.env.param:9911/path" os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint - constructor_param_protocol = Protocol.V2_PROTOBUF constructor_param_endpoint = "https://constructor.param:9911/path" local_node_ipv4 = "192.168.0.1" local_node_ipv6 = "2001:db8::1000" @@ -102,7 +102,6 @@ def test_constructor_all_params_and_env_vars(self): max_tag_value_length = 56 exporter = ZipkinExporter( - constructor_param_protocol, constructor_param_endpoint, local_node_ipv4, local_node_ipv6, @@ -125,7 +124,7 @@ def test_constructor_all_params_and_env_vars(self): def test_export_success(self, mock_post): mock_post.return_value = MockResponse(200) spans = [] - exporter = ZipkinExporter(Protocol.V2_PROTOBUF) + exporter = ZipkinExporter() status = exporter.export(spans) self.assertEqual(SpanExportResult.SUCCESS, status) @@ -133,7 +132,7 @@ def test_export_success(self, mock_post): def test_export_invalid_response(self, mock_post): mock_post.return_value = MockResponse(404) spans = [] - exporter = ZipkinExporter(Protocol.V2_PROTOBUF) + exporter = ZipkinExporter() status = exporter.export(spans) self.assertEqual(SpanExportResult.FAILURE, status) @@ -149,7 +148,7 @@ def test_export_span_service_name(self, mock_post): span = _Span("test_span", context=context, resource=resource) span.start() span.end() - exporter = ZipkinExporter(Protocol.V2_PROTOBUF) + exporter = ZipkinExporter() exporter.export([span]) self.assertEqual(exporter.local_node.service_name, "test") diff --git a/exporter/opentelemetry-exporter-zipkin/README.rst b/exporter/opentelemetry-exporter-zipkin/README.rst index a23df5eeec..2445ca879b 100644 --- a/exporter/opentelemetry-exporter-zipkin/README.rst +++ b/exporter/opentelemetry-exporter-zipkin/README.rst @@ -6,7 +6,15 @@ OpenTelemetry Zipkin Exporter .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-zipkin.svg :target: https://pypi.org/project/opentelemetry-exporter-zipkin/ -This library allows to export tracing data to `Zipkin `_. +This library is provided as a convenience to install all supported OpenTelemetry Zipkin Exporters. Currently it installs: +* opentelemetry-exporter-zipkin-json +* opentelemetry-exporter-zipkin-proto-http + +In the future, additional packages may be available: +* opentelemetry-exporter-zipkin-thrift + +To avoid unnecessary dependencies, users should install the specific package once they've determined their +preferred serialization method. Installation ------------ diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 11cc221589..2e636b13b3 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -14,7 +14,7 @@ # [metadata] name = opentelemetry-exporter-zipkin -description = Zipkin Span Exporter for OpenTelemetry +description = Zipkin Span Exporters for OpenTelemetry long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors @@ -35,21 +35,10 @@ classifiers = [options] python_requires = >=3.6 -package_dir= - =src packages=find_namespace: install_requires = - protobuf >= 3.12 - requests ~= 2.7 - opentelemetry-api == 1.0.0.dev0 - opentelemetry-sdk == 1.0.0.dev0 - -[options.packages.find] -where = src + opentelemetry-exporter-zipkin-json == 1.0.0.dev0 + opentelemetry-exporter-zipkin-proto-http == 1.0.0.dev0 [options.extras_require] test = - -[options.entry_points] -opentelemetry_exporter = - zipkin = opentelemetry.exporter.zipkin:ZipkinExporter \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/__init__.py deleted file mode 100644 index b892a67bef..0000000000 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Zipkin Export Encoders for JSON formats -""" - -import abc -from typing import Dict, List - -from opentelemetry.exporter.zipkin.encoder import Encoder -from opentelemetry.trace import Span, SpanContext, SpanKind - - -# pylint: disable=W0223 -class V1Encoder(Encoder): - def _extract_binary_annotations( - self, span: Span, encoded_local_endpoint: Dict - ) -> List[Dict]: - binary_annotations = [] - for tag_key, tag_value in self._extract_tags_from_span(span).items(): - if isinstance(tag_value, str) and self.max_tag_value_length > 0: - tag_value = tag_value[: self.max_tag_value_length] - binary_annotations.append( - { - "key": tag_key, - "value": tag_value, - "endpoint": encoded_local_endpoint, - } - ) - return binary_annotations diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin.py new file mode 100644 index 0000000000..fa9c3ecf48 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry.exporter.zipkin import json +from opentelemetry.exporter.zipkin.proto import http + + +class TestZipkinExporter(unittest.TestCase): + def test_constructors(self): + try: + json.ZipkinExporter() + http.ZipkinExporter() + except Exception as exc: # pylint: disable=broad-except + self.assertIsNone(exc) diff --git a/pyproject.toml b/pyproject.toml index f94565946a..e59980b2cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ exclude = ''' /( # generated files exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen| exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen| - exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen| + exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen| opentelemetry-proto/src/opentelemetry/proto/collector| opentelemetry-proto/src/opentelemetry/proto/common| opentelemetry-proto/src/opentelemetry/proto/metrics| diff --git a/tox.ini b/tox.ini index 2be66c6014..af932e009e 100644 --- a/tox.ini +++ b/tox.ini @@ -50,8 +50,16 @@ envlist = ; intentionally excluded from pypy3 ; opentelemetry-exporter-zipkin - py3{6,7,8,9}-test-exporter-zipkin - pypy3-test-exporter-zipkin + py3{6,7,8,9}-test-exporter-zipkin-combined + pypy3-test-exporter-zipkin-combined + + ; opentelemetry-exporter-zipkin-proto-http + py3{6,7,8,9}-test-exporter-zipkin-proto-http + pypy3-test-exporter-zipkin-proto-http + + ; opentelemetry-exporter-zipkin-json + py3{6,7,8,9}-test-exporter-zipkin-json + pypy3-test-exporter-zipkin-json ; opentelemetry-opentracing-shim py3{6,7,8,9}-test-core-opentracing-shim @@ -97,7 +105,9 @@ changedir = test-exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests test-exporter-otlp-combined: exporter/opentelemetry-exporter-otlp/tests test-exporter-otlp-proto-grpc: exporter/opentelemetry-exporter-otlp-proto-grpc/tests - test-exporter-zipkin: exporter/opentelemetry-exporter-zipkin/tests + test-exporter-zipkin-combined: exporter/opentelemetry-exporter-zipkin/tests + test-exporter-zipkin-proto-http: exporter/opentelemetry-exporter-zipkin-proto-http/tests + test-exporter-zipkin-json: exporter/opentelemetry-exporter-zipkin-json/tests test-propagator-b3: propagator/opentelemetry-propagator-b3/tests test-propagator-jaeger: propagator/opentelemetry-propagator-jaeger/tests @@ -131,7 +141,14 @@ commands_pre = opentracing-shim: pip install {toxinidir}/opentelemetry-sdk opentracing-shim: pip install {toxinidir}/shim/opentelemetry-opentracing-shim - zipkin: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin + exporter-zipkin-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin-json + exporter-zipkin-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin-proto-http + exporter-zipkin-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin + + exporter-zipkin-proto-http: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin-json + exporter-zipkin-proto-http: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin-proto-http + + exporter-zipkin-json: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin-json b3: pip install {toxinidir}/propagator/opentelemetry-propagator-b3 @@ -184,6 +201,8 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-opencensus[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin-json[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin-proto-http[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-b3[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-jaeger[test] From c533ac111f6f551cf18679c954e8addac5f47e32 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 25 Mar 2021 20:22:51 -0700 Subject: [PATCH 0826/1517] Fix link in rationale.md (#1717) --- rationale.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rationale.md b/rationale.md index 24cc141643..8ff92e902c 100644 --- a/rationale.md +++ b/rationale.md @@ -9,7 +9,7 @@ specification](https://github.com/open-telemetry/opentelemetry-specification/blo The OpenTelemetry implementations, the OpenTelemetry Spec itself and this repo follows [SemVer V2](https://semver.org/spec/v2.0.0.html) guidelines. This means that, for any stable packages released from this repo, all public APIs will remain [backward -compatible](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/breaking-change-rules.md#breaking-change-rules), +compatible](https://www.python.org/dev/peps/pep-0387/), unless a major version bump occurs. This applies to the API, SDK, as well as Exporters, Instrumentation etc. shipped from this repo. For example, users can take a dependency on 1.0.0 version of any package, with the assurance that all future releases until 2.0.0 will be backward compatible. From 25edfefd70f2d2fd8f6c6a90293d598a358fbb96 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 26 Mar 2021 11:20:20 -0600 Subject: [PATCH 0827/1517] Make setters and getters optional (#1690) Co-authored-by: alrex --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 + docs/conf.py | 2 +- docs/examples/auto-instrumentation/README.rst | 3 +- docs/examples/auto-instrumentation/client.py | 2 +- .../server_instrumented.py | 3 +- docs/examples/datadog_exporter/client.py | 2 +- docs/examples/django/client.py | 2 +- .../baggage/propagation/__init__.py | 14 ++-- .../src/opentelemetry/propagate/__init__.py | 20 ++--- .../opentelemetry/propagators/composite.py | 12 +-- .../src/opentelemetry/propagators/textmap.py | 82 ++++++++++++++----- .../trace/propagation/tracecontext.py | 14 ++-- .../tests/baggage/test_baggage_propagation.py | 17 ++-- .../tests/propagators/test_composite.py | 30 +++---- .../propagators/test_global_httptextformat.py | 7 +- .../tests/trace/propagation/test_textmap.py | 12 +-- .../test_tracecontexthttptextformat.py | 24 ++---- .../opentelemetry/propagators/b3/__init__.py | 24 +++--- .../propagation/test_benchmark_b3_format.py | 3 - .../tests/test_b3_format.py | 36 ++++---- .../propagators/jaeger/__init__.py | 22 ++--- .../tests/test_jaeger_propagator.py | 28 +++---- .../shim/opentracing_shim/__init__.py | 6 +- .../src/opentelemetry/test/mock_textmap.py | 24 +++--- 25 files changed, 201 insertions(+), 192 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 908cf71caa..34d58b2cdb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 5bc0fa1611502be47a1f4eb550fe255e4b707ba1 + CONTRIB_REPO_SHA: 0d12fa39523212e268ef435825af2039a876fd75 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a8e24366d..4e7cbc0325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.18b0...HEAD) +- Make setters and getters optional + ([#1690](https://github.com/open-telemetry/opentelemetry-python/pull/1690)) ### Added - Document how to work with fork process web server models(Gunicorn, uWSGI etc...) diff --git a/docs/conf.py b/docs/conf.py index d23cebfe96..61dc1a8254 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -104,7 +104,7 @@ ("py:class", "opentelemetry.trace._LinkBase",), # TODO: Understand why sphinx is not able to find this local class ("py:class", "opentelemetry.propagators.textmap.TextMapPropagator",), - ("py:class", "opentelemetry.propagators.textmap.DictGetter",), + ("py:class", "opentelemetry.propagators.textmap.DefaultGetter",), ("any", "opentelemetry.propagators.textmap.TextMapPropagator.extract",), ("any", "opentelemetry.propagators.textmap.TextMapPropagator.inject",), ] diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index 607aa1b44b..48c59951b4 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -37,8 +37,7 @@ Manually instrumented server def server_request(): with tracer.start_as_current_span( "server_request", - context=propagators.extract(DictGetter(), request.headers - ), + context=propagators.extract(request.headers), ): print(request.args.get("param")) return "served" diff --git a/docs/examples/auto-instrumentation/client.py b/docs/examples/auto-instrumentation/client.py index fefc1f67b9..cc948cc54b 100644 --- a/docs/examples/auto-instrumentation/client.py +++ b/docs/examples/auto-instrumentation/client.py @@ -37,7 +37,7 @@ with tracer.start_as_current_span("client-server"): headers = {} - propagators.inject(dict.__setitem__, headers) + propagators.inject(headers) requested = get( "http://localhost:8082/server_request", params={"param": argv[1]}, diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_instrumented.py index 1ac1bd6b71..652358e3a2 100644 --- a/docs/examples/auto-instrumentation/server_instrumented.py +++ b/docs/examples/auto-instrumentation/server_instrumented.py @@ -17,7 +17,6 @@ from opentelemetry import trace from opentelemetry.instrumentation.wsgi import collect_request_attributes from opentelemetry.propagate import extract -from opentelemetry.propagators.textmap import DictGetter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -38,7 +37,7 @@ def server_request(): with tracer.start_as_current_span( "server_request", - context=extract(DictGetter(), request.headers), + context=extract(request.headers), kind=trace.SpanKind.SERVER, attributes=collect_request_attributes(request.environ), ): diff --git a/docs/examples/datadog_exporter/client.py b/docs/examples/datadog_exporter/client.py index 6b4b5d00ec..7c6196ad4a 100644 --- a/docs/examples/datadog_exporter/client.py +++ b/docs/examples/datadog_exporter/client.py @@ -47,7 +47,7 @@ with tracer.start_as_current_span("client-server"): headers = {} - inject(dict.__setitem__, headers) + inject(headers) requested = get( "http://localhost:8082/server_request", params={"param": argv[1]}, diff --git a/docs/examples/django/client.py b/docs/examples/django/client.py index bc3606cbe7..3ae0cb6e1c 100644 --- a/docs/examples/django/client.py +++ b/docs/examples/django/client.py @@ -36,7 +36,7 @@ with tracer.start_as_current_span("client-server"): headers = {} - inject(dict.__setitem__, headers) + inject(headers) requested = get( "http://localhost:8000", params={"param": argv[1]}, diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index e6d1c4207b..04d896baa3 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -31,9 +31,9 @@ class W3CBaggagePropagator(textmap.TextMapPropagator): def extract( self, - getter: textmap.Getter[textmap.TextMapPropagatorT], - carrier: textmap.TextMapPropagatorT, + carrier: textmap.CarrierT, context: typing.Optional[Context] = None, + getter: textmap.Getter = textmap.default_getter, ) -> Context: """Extract Baggage from the carrier. @@ -73,9 +73,9 @@ def extract( def inject( self, - set_in_carrier: textmap.Setter[textmap.TextMapPropagatorT], - carrier: textmap.TextMapPropagatorT, + carrier: textmap.CarrierT, context: typing.Optional[Context] = None, + setter: textmap.Setter = textmap.default_setter, ) -> None: """Injects Baggage into the carrier. @@ -87,7 +87,7 @@ def inject( return baggage_string = _format_baggage(baggage_entries) - set_in_carrier(carrier, self._BAGGAGE_HEADER_NAME, baggage_string) + setter.set(carrier, self._BAGGAGE_HEADER_NAME, baggage_string) @property def fields(self) -> typing.Set[str]: @@ -103,8 +103,8 @@ def _format_baggage(baggage_entries: typing.Mapping[str, object]) -> str: def _extract_first_element( - items: typing.Optional[typing.Iterable[textmap.TextMapPropagatorT]], -) -> typing.Optional[textmap.TextMapPropagatorT]: + items: typing.Optional[typing.Iterable[textmap.CarrierT]], +) -> typing.Optional[textmap.CarrierT]: if items is None: return None return next(iter(items), None) diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index 44f9897a53..091b5b8d44 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -82,9 +82,9 @@ def example_route(): def extract( - getter: textmap.Getter[textmap.TextMapPropagatorT], - carrier: textmap.TextMapPropagatorT, + carrier: textmap.CarrierT, context: typing.Optional[Context] = None, + getter: textmap.Getter = textmap.default_getter, ) -> Context: """Uses the configured propagator to extract a Context from the carrier. @@ -99,26 +99,26 @@ def extract( context: an optional Context to use. Defaults to current context if not set. """ - return get_global_textmap().extract(getter, carrier, context) + return get_global_textmap().extract(carrier, context, getter=getter) def inject( - set_in_carrier: textmap.Setter[textmap.TextMapPropagatorT], - carrier: textmap.TextMapPropagatorT, + carrier: textmap.CarrierT, context: typing.Optional[Context] = None, + setter: textmap.Setter = textmap.default_setter, ) -> None: """Uses the configured propagator to inject a Context into the carrier. Args: - set_in_carrier: A setter function that can set values - on the carrier. carrier: An object that contains a representation of HTTP - headers. Should be paired with set_in_carrier, which + headers. Should be paired with setter, which should know how to set header values on the carrier. - context: an optional Context to use. Defaults to current + context: An optional Context to use. Defaults to current context if not set. + setter: An optional `Setter` object that can set values + on the carrier. """ - get_global_textmap().inject(set_in_carrier, carrier, context) + get_global_textmap().inject(carrier, context=context, setter=setter) try: diff --git a/opentelemetry-api/src/opentelemetry/propagators/composite.py b/opentelemetry-api/src/opentelemetry/propagators/composite.py index 92dc6b8a38..c027f638dc 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/composite.py +++ b/opentelemetry-api/src/opentelemetry/propagators/composite.py @@ -35,9 +35,9 @@ def __init__( def extract( self, - getter: textmap.Getter[textmap.TextMapPropagatorT], - carrier: textmap.TextMapPropagatorT, + carrier: textmap.CarrierT, context: typing.Optional[Context] = None, + getter: textmap.Getter = textmap.default_getter, ) -> Context: """Run each of the configured propagators with the given context and carrier. Propagators are run in the order they are configured, if multiple @@ -47,14 +47,14 @@ def extract( See `opentelemetry.propagators.textmap.TextMapPropagator.extract` """ for propagator in self._propagators: - context = propagator.extract(getter, carrier, context) + context = propagator.extract(carrier, context, getter=getter) return context # type: ignore def inject( self, - set_in_carrier: textmap.Setter[textmap.TextMapPropagatorT], - carrier: textmap.TextMapPropagatorT, + carrier: textmap.CarrierT, context: typing.Optional[Context] = None, + setter: textmap.Setter = textmap.default_setter, ) -> None: """Run each of the configured propagators with the given context and carrier. Propagators are run in the order they are configured, if multiple @@ -64,7 +64,7 @@ def inject( See `opentelemetry.propagators.textmap.TextMapPropagator.inject` """ for propagator in self._propagators: - propagator.inject(set_in_carrier, carrier, context) + propagator.inject(carrier, context, setter=setter) @property def fields(self) -> typing.Set[str]: diff --git a/opentelemetry-api/src/opentelemetry/propagators/textmap.py b/opentelemetry-api/src/opentelemetry/propagators/textmap.py index cf93d1d631..45c2308f66 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/textmap.py +++ b/opentelemetry-api/src/opentelemetry/propagators/textmap.py @@ -17,19 +17,18 @@ from opentelemetry.context.context import Context -TextMapPropagatorT = typing.TypeVar("TextMapPropagatorT") +CarrierT = typing.TypeVar("CarrierT") CarrierValT = typing.Union[typing.List[str], str] -Setter = typing.Callable[[TextMapPropagatorT, str, str], None] - -class Getter(typing.Generic[TextMapPropagatorT]): +class Getter(abc.ABC): """This class implements a Getter that enables extracting propagated fields from a carrier. """ + @abc.abstractmethod def get( - self, carrier: TextMapPropagatorT, key: str + self, carrier: CarrierT, key: str ) -> typing.Optional[typing.List[str]]: """Function that can retrieve zero or more values from the carrier. In the case that @@ -42,9 +41,9 @@ def get( Returns: first value of the propagation key or None if the key doesn't exist. """ - raise NotImplementedError() - def keys(self, carrier: TextMapPropagatorT) -> typing.List[str]: + @abc.abstractmethod + def keys(self, carrier: CarrierT) -> typing.List[str]: """Function that can retrieve all the keys in a carrier object. Args: @@ -53,17 +52,33 @@ def keys(self, carrier: TextMapPropagatorT) -> typing.List[str]: Returns: list of keys from the carrier. """ - raise NotImplementedError() -class DictGetter(Getter[typing.Dict[str, CarrierValT]]): - def get( - self, carrier: typing.Dict[str, CarrierValT], key: str +class Setter(abc.ABC): + """This class implements a Setter that enables injecting propagated + fields into a carrier. + """ + + @abc.abstractmethod + def set(self, carrier: CarrierT, key: str, value: str) -> None: + """Function that can set a value into a carrier"" + + Args: + carrier: An object which contains values that are used to + construct a Context. + key: key of a field in carrier. + value: value for a field in carrier. + """ + + +class DefaultGetter(Getter): + def get( # type: ignore + self, carrier: typing.Mapping[str, CarrierValT], key: str ) -> typing.Optional[typing.List[str]]: """Getter implementation to retrieve a value from a dictionary. Args: - carrier: dictionary in which header + carrier: dictionary in which to get value key: the key used to get the value Returns: A list with a single string with the value if it exists, else None. @@ -75,11 +90,36 @@ def get( return list(val) return [val] - def keys(self, carrier: typing.Dict[str, CarrierValT]) -> typing.List[str]: + def keys( # type: ignore + self, carrier: typing.Dict[str, CarrierValT] + ) -> typing.List[str]: """Keys implementation that returns all keys from a dictionary.""" return list(carrier.keys()) +default_getter = DefaultGetter() + + +class DefaultSetter(Setter): + def set( # type: ignore + self, + carrier: typing.MutableMapping[str, CarrierValT], + key: str, + value: CarrierValT, + ) -> None: + """Setter implementation to set a value into a dictionary. + + Args: + carrier: dictionary in which to set value + key: the key used to set the value + value: the value to set + """ + carrier[key] = value + + +default_setter = DefaultSetter() + + class TextMapPropagator(abc.ABC): """This class provides an interface that enables extracting and injecting context into headers of HTTP requests. HTTP frameworks and clients @@ -92,9 +132,9 @@ class TextMapPropagator(abc.ABC): @abc.abstractmethod def extract( self, - getter: Getter[TextMapPropagatorT], - carrier: TextMapPropagatorT, + carrier: CarrierT, context: typing.Optional[Context] = None, + getter: Getter = default_getter, ) -> Context: """Create a Context from values in the carrier. @@ -120,25 +160,25 @@ def extract( @abc.abstractmethod def inject( self, - set_in_carrier: Setter[TextMapPropagatorT], - carrier: TextMapPropagatorT, + carrier: CarrierT, context: typing.Optional[Context] = None, + setter: Setter = default_setter, ) -> None: """Inject values from a Context into a carrier. inject enables the propagation of values into HTTP clients or other objects which perform an HTTP request. Implementations - should use the set_in_carrier method to set values on the + should use the `Setter` 's set method to set values on the carrier. Args: - set_in_carrier: A setter function that can set values - on the carrier. carrier: An object that a place to define HTTP headers. - Should be paired with set_in_carrier, which should + Should be paired with setter, which should know how to set header values on the carrier. context: an optional Context to use. Defaults to current context if not set. + setter: An optional `Setter` object that can set values + on the carrier. """ diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index 480e716bf7..9fc5cfed24 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -35,9 +35,9 @@ class TraceContextTextMapPropagator(textmap.TextMapPropagator): def extract( self, - getter: textmap.Getter[textmap.TextMapPropagatorT], - carrier: textmap.TextMapPropagatorT, + carrier: textmap.CarrierT, context: typing.Optional[Context] = None, + getter: textmap.Getter = textmap.default_getter, ) -> Context: """Extracts SpanContext from the carrier. @@ -85,9 +85,9 @@ def extract( def inject( self, - set_in_carrier: textmap.Setter[textmap.TextMapPropagatorT], - carrier: textmap.TextMapPropagatorT, + carrier: textmap.CarrierT, context: typing.Optional[Context] = None, + setter: textmap.Setter = textmap.default_setter, ) -> None: """Injects SpanContext into the carrier. @@ -102,12 +102,10 @@ def inject( trace_id=format_trace_id(span_context.trace_id), span_id=format_span_id(span_context.span_id), ) - set_in_carrier( - carrier, self._TRACEPARENT_HEADER_NAME, traceparent_string - ) + setter.set(carrier, self._TRACEPARENT_HEADER_NAME, traceparent_string) if span_context.trace_state: tracestate_string = span_context.trace_state.to_header() - set_in_carrier( + setter.set( carrier, self._TRACESTATE_HEADER_NAME, tracestate_string ) diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py index 3047ddbbe4..9084bb778e 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py @@ -20,9 +20,6 @@ from opentelemetry import baggage from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.context import get_current -from opentelemetry.propagators.textmap import DictGetter - -carrier_getter = DictGetter() class TestBaggagePropagation(unittest.TestCase): @@ -32,7 +29,7 @@ def setUp(self): def _extract(self, header_value): """Test helper""" header = {"baggage": [header_value]} - return baggage.get_all(self.propagator.extract(carrier_getter, header)) + return baggage.get_all(self.propagator.extract(header)) def _inject(self, values): """Test helper""" @@ -40,13 +37,11 @@ def _inject(self, values): for k, v in values.items(): ctx = baggage.set_baggage(k, v, context=ctx) output = {} - self.propagator.inject(dict.__setitem__, output, context=ctx) + self.propagator.inject(output, context=ctx) return output.get("baggage") def test_no_context_header(self): - baggage_entries = baggage.get_all( - self.propagator.extract(carrier_getter, {}) - ) + baggage_entries = baggage.get_all(self.propagator.extract({})) self.assertEqual(baggage_entries, {}) def test_empty_context_header(self): @@ -149,13 +144,13 @@ def test_inject_non_string_values(self): @patch("opentelemetry.baggage.propagation._format_baggage") def test_fields(self, mock_format_baggage, mock_baggage): - mock_set_in_carrier = Mock() + mock_setter = Mock() - self.propagator.inject(mock_set_in_carrier, {}) + self.propagator.inject({}, setter=mock_setter) inject_fields = set() - for mock_call in mock_set_in_carrier.mock_calls: + for mock_call in mock_setter.mock_calls: inject_fields.add(mock_call[1][1]) self.assertEqual(inject_fields, self.propagator.fields) diff --git a/opentelemetry-api/tests/propagators/test_composite.py b/opentelemetry-api/tests/propagators/test_composite.py index e33649bbdd..ef9fae2a1a 100644 --- a/opentelemetry-api/tests/propagators/test_composite.py +++ b/opentelemetry-api/tests/propagators/test_composite.py @@ -26,16 +26,16 @@ def get_as_list(dict_object, key): def mock_inject(name, value="data"): - def wrapped(setter, carrier=None, context=None): + def wrapped(carrier=None, context=None, setter=None): carrier[name] = value - setter({}, "inject_field_{}_0".format(name), None) - setter({}, "inject_field_{}_1".format(name), None) + setter.set({}, "inject_field_{}_0".format(name), None) + setter.set({}, "inject_field_{}_1".format(name), None) return wrapped def mock_extract(name, value="context"): - def wrapped(getter, carrier=None, context=None): + def wrapped(carrier=None, context=None, getter=None): new_context = context.copy() new_context[name] = value return new_context @@ -69,11 +69,11 @@ def setUpClass(cls): def test_no_propagators(self): propagator = CompositeHTTPPropagator([]) new_carrier = {} - propagator.inject(dict.__setitem__, carrier=new_carrier) + propagator.inject(new_carrier) self.assertEqual(new_carrier, {}) context = propagator.extract( - get_as_list, carrier=new_carrier, context={} + carrier=new_carrier, context={}, getter=get_as_list ) self.assertEqual(context, {}) @@ -81,11 +81,11 @@ def test_single_propagator(self): propagator = CompositeHTTPPropagator([self.mock_propagator_0]) new_carrier = {} - propagator.inject(dict.__setitem__, carrier=new_carrier) + propagator.inject(new_carrier) self.assertEqual(new_carrier, {"mock-0": "data"}) context = propagator.extract( - get_as_list, carrier=new_carrier, context={} + carrier=new_carrier, context={}, getter=get_as_list ) self.assertEqual(context, {"mock-0": "context"}) @@ -95,11 +95,11 @@ def test_multiple_propagators(self): ) new_carrier = {} - propagator.inject(dict.__setitem__, carrier=new_carrier) + propagator.inject(new_carrier) self.assertEqual(new_carrier, {"mock-0": "data", "mock-1": "data"}) context = propagator.extract( - get_as_list, carrier=new_carrier, context={} + carrier=new_carrier, context={}, getter=get_as_list ) self.assertEqual(context, {"mock-0": "context", "mock-1": "context"}) @@ -111,11 +111,11 @@ def test_multiple_propagators_same_key(self): ) new_carrier = {} - propagator.inject(dict.__setitem__, carrier=new_carrier) + propagator.inject(new_carrier) self.assertEqual(new_carrier, {"mock-0": "data2"}) context = propagator.extract( - get_as_list, carrier=new_carrier, context={} + carrier=new_carrier, context={}, getter=get_as_list ) self.assertEqual(context, {"mock-0": "context2"}) @@ -128,13 +128,13 @@ def test_fields(self): ] ) - mock_set_in_carrier = Mock() + mock_setter = Mock() - propagator.inject(mock_set_in_carrier, {}) + propagator.inject({}, setter=mock_setter) inject_fields = set() - for mock_call in mock_set_in_carrier.mock_calls: + for mock_call in mock_setter.mock_calls: inject_fields.add(mock_call[1][1]) self.assertEqual(inject_fields, propagator.fields) diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index 6ba32e4618..466ce6895f 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -18,12 +18,9 @@ from opentelemetry import baggage, trace from opentelemetry.propagate import extract, inject -from opentelemetry.propagators.textmap import DictGetter from opentelemetry.trace import get_current_span, set_span_in_context from opentelemetry.trace.span import format_span_id, format_trace_id -carrier_getter = DictGetter() - class TestDefaultGlobalPropagator(unittest.TestCase): """Test ensures the default global composite propagator works as intended""" @@ -42,7 +39,7 @@ def test_propagation(self): "traceparent": [traceparent_value], "tracestate": [tracestate_value], } - ctx = extract(carrier_getter, headers) + ctx = extract(headers) baggage_entries = baggage.get_all(context=ctx) expected = {"key1": "val1", "key2": "val2"} self.assertEqual(baggage_entries, expected) @@ -56,7 +53,7 @@ def test_propagation(self): ctx = baggage.set_baggage("key4", "val4", context=ctx) ctx = set_span_in_context(span, context=ctx) output = {} - inject(dict.__setitem__, output, context=ctx) + inject(output, context=ctx) self.assertEqual(traceparent_value, output["traceparent"]) self.assertIn("key3=val3", output["baggage"]) self.assertIn("key4=val4", output["baggage"]) diff --git a/opentelemetry-api/tests/trace/propagation/test_textmap.py b/opentelemetry-api/tests/trace/propagation/test_textmap.py index e47a0d22cb..6b22d46f88 100644 --- a/opentelemetry-api/tests/trace/propagation/test_textmap.py +++ b/opentelemetry-api/tests/trace/propagation/test_textmap.py @@ -16,29 +16,29 @@ import unittest -from opentelemetry.propagators.textmap import DictGetter +from opentelemetry.propagators.textmap import DefaultGetter -class TestDictGetter(unittest.TestCase): +class TestDefaultGetter(unittest.TestCase): def test_get_none(self): - getter = DictGetter() + getter = DefaultGetter() carrier = {} val = getter.get(carrier, "test") self.assertIsNone(val) def test_get_str(self): - getter = DictGetter() + getter = DefaultGetter() carrier = {"test": "val"} val = getter.get(carrier, "test") self.assertEqual(val, ["val"]) def test_get_iter(self): - getter = DictGetter() + getter = DefaultGetter() carrier = {"test": ["val"]} val = getter.get(carrier, "test") self.assertEqual(val, ["val"]) def test_keys(self): - getter = DictGetter() + getter = DefaultGetter() keys = getter.keys({"test": "val"}) self.assertEqual(keys, ["test"]) diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 79683d43d9..98ca50610b 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -19,14 +19,11 @@ from unittest.mock import Mock, patch from opentelemetry import trace -from opentelemetry.propagators.textmap import DictGetter from opentelemetry.trace.propagation import tracecontext from opentelemetry.trace.span import TraceState FORMAT = tracecontext.TraceContextTextMapPropagator() -carrier_getter = DictGetter() - class TestTraceContextFormat(unittest.TestCase): TRACE_ID = int("12345678901234567890123456789012", 16) # type:int @@ -42,7 +39,7 @@ def test_no_traceparent_header(self): trace-id and parent-id that represents the current request. """ output = {} # type:typing.Dict[str, typing.List[str]] - span = trace.get_current_span(FORMAT.extract(carrier_getter, output)) + span = trace.get_current_span(FORMAT.extract(output)) self.assertIsInstance(span.get_span_context(), trace.SpanContext) def test_headers_with_tracestate(self): @@ -56,7 +53,6 @@ def test_headers_with_tracestate(self): tracestate_value = "foo=1,bar=2,baz=3" span_context = trace.get_current_span( FORMAT.extract( - carrier_getter, { "traceparent": [traceparent_value], "tracestate": [tracestate_value], @@ -73,7 +69,7 @@ def test_headers_with_tracestate(self): span = trace.NonRecordingSpan(span_context) ctx = trace.set_span_in_context(span) - FORMAT.inject(dict.__setitem__, output, ctx) + FORMAT.inject(output, context=ctx) self.assertEqual(output["traceparent"], traceparent_value) for pair in ["foo=1", "bar=2", "baz=3"]: self.assertIn(pair, output["tracestate"]) @@ -100,7 +96,6 @@ def test_invalid_trace_id(self): """ span = trace.get_current_span( FORMAT.extract( - carrier_getter, { "traceparent": [ "00-00000000000000000000000000000000-1234567890123456-00" @@ -131,7 +126,6 @@ def test_invalid_parent_id(self): """ span = trace.get_current_span( FORMAT.extract( - carrier_getter, { "traceparent": [ "00-00000000000000000000000000000000-0000000000000000-00" @@ -155,7 +149,7 @@ def test_no_send_empty_tracestate(self): trace.SpanContext(self.TRACE_ID, self.SPAN_ID, is_remote=False) ) ctx = trace.set_span_in_context(span) - FORMAT.inject(dict.__setitem__, output, ctx) + FORMAT.inject(output, context=ctx) self.assertTrue("traceparent" in output) self.assertFalse("tracestate" in output) @@ -169,7 +163,6 @@ def test_format_not_supported(self): """ span = trace.get_current_span( FORMAT.extract( - carrier_getter, { "traceparent": [ "00-12345678901234567890123456789012-" @@ -185,14 +178,13 @@ def test_propagate_invalid_context(self): """Do not propagate invalid trace context.""" output = {} # type:typing.Dict[str, str] ctx = trace.set_span_in_context(trace.INVALID_SPAN) - FORMAT.inject(dict.__setitem__, output, context=ctx) + FORMAT.inject(output, context=ctx) self.assertFalse("traceparent" in output) def test_tracestate_empty_header(self): """Test tracestate with an additional empty header (should be ignored)""" span = trace.get_current_span( FORMAT.extract( - carrier_getter, { "traceparent": [ "00-12345678901234567890123456789012-1234567890123456-00" @@ -207,7 +199,6 @@ def test_tracestate_header_with_trailing_comma(self): """Do not propagate invalid trace context.""" span = trace.get_current_span( FORMAT.extract( - carrier_getter, { "traceparent": [ "00-12345678901234567890123456789012-1234567890123456-00" @@ -230,7 +221,6 @@ def test_tracestate_keys(self): ) span = trace.get_current_span( FORMAT.extract( - carrier_getter, { "traceparent": [ "00-12345678901234567890123456789012-" @@ -270,13 +260,13 @@ def test_fields(self, mock_get_current_span, mock_invalid_span_context): ) ) - mock_set_in_carrier = Mock() + mock_setter = Mock() - FORMAT.inject(mock_set_in_carrier, {}) + FORMAT.inject({}, setter=mock_setter) inject_fields = set() - for mock_call in mock_set_in_carrier.mock_calls: + for mock_call in mock_setter.mock_calls: inject_fields.add(mock_call[1][1]) self.assertEqual(inject_fields, FORMAT.fields) diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py index 01abcc7c87..be478b05ec 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py @@ -18,10 +18,12 @@ import opentelemetry.trace as trace from opentelemetry.context import Context from opentelemetry.propagators.textmap import ( + CarrierT, Getter, Setter, TextMapPropagator, - TextMapPropagatorT, + default_getter, + default_setter, ) from opentelemetry.trace import format_span_id, format_trace_id @@ -44,9 +46,9 @@ class B3Format(TextMapPropagator): def extract( self, - getter: Getter[TextMapPropagatorT], - carrier: TextMapPropagatorT, + carrier: CarrierT, context: typing.Optional[Context] = None, + getter: Getter = default_getter, ) -> Context: trace_id = format_trace_id(trace.INVALID_TRACE_ID) span_id = format_span_id(trace.INVALID_SPAN_ID) @@ -127,9 +129,9 @@ def extract( def inject( self, - set_in_carrier: Setter[TextMapPropagatorT], - carrier: TextMapPropagatorT, + carrier: CarrierT, context: typing.Optional[Context] = None, + setter: Setter = default_setter, ) -> None: span = trace.get_current_span(context=context) @@ -138,20 +140,20 @@ def inject( return sampled = (trace.TraceFlags.SAMPLED & span_context.trace_flags) != 0 - set_in_carrier( + setter.set( carrier, self.TRACE_ID_KEY, format_trace_id(span_context.trace_id), ) - set_in_carrier( + setter.set( carrier, self.SPAN_ID_KEY, format_span_id(span_context.span_id) ) span_parent = getattr(span, "parent", None) if span_parent is not None: - set_in_carrier( + setter.set( carrier, self.PARENT_SPAN_ID_KEY, format_span_id(span_parent.span_id), ) - set_in_carrier(carrier, self.SAMPLED_KEY, "1" if sampled else "0") + setter.set(carrier, self.SAMPLED_KEY, "1" if sampled else "0") @property def fields(self) -> typing.Set[str]: @@ -164,8 +166,8 @@ def fields(self) -> typing.Set[str]: def _extract_first_element( - items: typing.Iterable[TextMapPropagatorT], -) -> typing.Optional[TextMapPropagatorT]: + items: typing.Iterable[CarrierT], +) -> typing.Optional[CarrierT]: if items is None: return None return next(iter(items), None) diff --git a/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py index 5048f495f0..3a7a251ad8 100644 --- a/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py @@ -14,7 +14,6 @@ import opentelemetry.propagators.b3 as b3_format import opentelemetry.sdk.trace as trace -from opentelemetry.propagators.textmap import DictGetter FORMAT = b3_format.B3Format() @@ -22,7 +21,6 @@ def test_extract_single_header(benchmark): benchmark( FORMAT.extract, - DictGetter(), { FORMAT.SINGLE_HEADER_KEY: "bdb5b63237ed38aea578af665aa5aa60-c32d953d73ad2251-1-11fd79a30b0896cd285b396ae102dd76" }, @@ -35,7 +33,6 @@ def test_inject_empty_context(benchmark): with tracer.start_as_current_span("Child Span"): benchmark( FORMAT.inject, - dict.__setitem__, { FORMAT.TRACE_ID_KEY: "bdb5b63237ed38aea578af665aa5aa60", FORMAT.SPAN_ID_KEY: "00000000000000000c32d953d73ad225", diff --git a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index f9d3bce1ad..d1d96a269f 100644 --- a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -20,17 +20,14 @@ import opentelemetry.sdk.trace.id_generator as id_generator import opentelemetry.trace as trace_api from opentelemetry.context import get_current -from opentelemetry.propagators.textmap import DictGetter +from opentelemetry.propagators.textmap import DefaultGetter FORMAT = b3_format.B3Format() -carrier_getter = DictGetter() - - def get_child_parent_new_carrier(old_carrier): - ctx = FORMAT.extract(carrier_getter, old_carrier) + ctx = FORMAT.extract(old_carrier) parent_span_context = trace_api.get_current_span(ctx).get_span_context() parent = trace._Span("parent", parent_span_context) @@ -48,7 +45,7 @@ def get_child_parent_new_carrier(old_carrier): new_carrier = {} ctx = trace_api.set_span_in_context(child) - FORMAT.inject(dict.__setitem__, new_carrier, context=ctx) + FORMAT.inject(new_carrier, context=ctx) return child, parent, new_carrier @@ -239,7 +236,7 @@ def test_invalid_single_header(self): invalid SpanContext. """ carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} - ctx = FORMAT.extract(carrier_getter, carrier) + ctx = FORMAT.extract(carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) @@ -251,7 +248,7 @@ def test_missing_trace_id(self): FORMAT.FLAGS_KEY: "1", } - ctx = FORMAT.extract(carrier_getter, carrier) + ctx = FORMAT.extract(carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) @@ -275,7 +272,7 @@ def test_invalid_trace_id( FORMAT.FLAGS_KEY: "1", } - ctx = FORMAT.extract(carrier_getter, carrier) + ctx = FORMAT.extract(carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, 1) @@ -301,7 +298,7 @@ def test_invalid_span_id( FORMAT.FLAGS_KEY: "1", } - ctx = FORMAT.extract(carrier_getter, carrier) + ctx = FORMAT.extract(carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, 1) @@ -314,7 +311,7 @@ def test_missing_span_id(self): FORMAT.FLAGS_KEY: "1", } - ctx = FORMAT.extract(carrier_getter, carrier) + ctx = FORMAT.extract(carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) @@ -322,37 +319,34 @@ def test_missing_span_id(self): def test_inject_empty_context(): """If the current context has no span, don't add headers""" new_carrier = {} - FORMAT.inject(dict.__setitem__, new_carrier, get_current()) + FORMAT.inject(new_carrier, get_current()) assert len(new_carrier) == 0 @staticmethod def test_default_span(): """Make sure propagator does not crash when working with NonRecordingSpan""" - class CarrierGetter(DictGetter): + class CarrierGetter(DefaultGetter): def get(self, carrier, key): return carrier.get(key, None) - def setter(carrier, key, value): - carrier[key] = value - - ctx = FORMAT.extract(CarrierGetter(), {}) - FORMAT.inject(setter, {}, ctx) + ctx = FORMAT.extract({}, getter=CarrierGetter()) + FORMAT.inject({}, context=ctx) def test_fields(self): """Make sure the fields attribute returns the fields used in inject""" tracer = trace.TracerProvider().get_tracer("sdk_tracer_provider") - mock_set_in_carrier = Mock() + mock_setter = Mock() with tracer.start_as_current_span("parent"): with tracer.start_as_current_span("child"): - FORMAT.inject(mock_set_in_carrier, {}) + FORMAT.inject({}, setter=mock_setter) inject_fields = set() - for call in mock_set_in_carrier.mock_calls: + for call in mock_setter.mock_calls: inject_fields.add(call[1][1]) self.assertEqual(FORMAT.fields, inject_fields) diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py index 8e7fe5f69f..47f438531f 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py @@ -19,10 +19,12 @@ from opentelemetry import baggage from opentelemetry.context import Context, get_current from opentelemetry.propagators.textmap import ( + CarrierT, Getter, Setter, TextMapPropagator, - TextMapPropagatorT, + default_getter, + default_setter, ) from opentelemetry.trace import format_span_id, format_trace_id @@ -39,9 +41,9 @@ class JaegerPropagator(TextMapPropagator): def extract( self, - getter: Getter[TextMapPropagatorT], - carrier: TextMapPropagatorT, + carrier: CarrierT, context: typing.Optional[Context] = None, + getter: Getter = default_getter, ) -> Context: if context is None: @@ -76,9 +78,9 @@ def extract( def inject( self, - set_in_carrier: Setter[TextMapPropagatorT], - carrier: TextMapPropagatorT, + carrier: CarrierT, context: typing.Optional[Context] = None, + setter: Setter = default_setter, ) -> None: span = trace.get_current_span(context=context) span_context = span.get_span_context() @@ -91,7 +93,7 @@ def inject( trace_flags |= self.DEBUG_FLAG # set span identity - set_in_carrier( + setter.set( carrier, self.TRACE_ID_KEY, _format_uber_trace_id( @@ -108,9 +110,7 @@ def inject( return for key, value in baggage_entries.items(): baggage_key = self.BAGGAGE_PREFIX + key - set_in_carrier( - carrier, baggage_key, urllib.parse.quote(str(value)) - ) + setter.set(carrier, baggage_key, urllib.parse.quote(str(value))) @property def fields(self) -> typing.Set[str]: @@ -142,8 +142,8 @@ def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags): def _extract_first_element( - items: typing.Iterable[TextMapPropagatorT], -) -> typing.Optional[TextMapPropagatorT]: + items: typing.Iterable[CarrierT], +) -> typing.Optional[CarrierT]: if items is None: return None return next(iter(items), None) diff --git a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py index da8a855edb..003bbfeb49 100644 --- a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py @@ -22,17 +22,13 @@ from opentelemetry.propagators import ( # pylint: disable=no-name-in-module jaeger, ) -from opentelemetry.propagators.textmap import DictGetter FORMAT = jaeger.JaegerPropagator() -carrier_getter = DictGetter() - - def get_context_new_carrier(old_carrier, carrier_baggage=None): - ctx = FORMAT.extract(carrier_getter, old_carrier) + ctx = FORMAT.extract(old_carrier) if carrier_baggage: for key, value in carrier_baggage.items(): ctx = baggage.set_baggage(key, value, ctx) @@ -54,7 +50,7 @@ def get_context_new_carrier(old_carrier, carrier_baggage=None): new_carrier = {} ctx = trace_api.set_span_in_context(child, ctx) - FORMAT.inject(dict.__setitem__, new_carrier, context=ctx) + FORMAT.inject(new_carrier, context=ctx) return ctx, new_carrier @@ -72,14 +68,14 @@ def setUpClass(cls): def test_extract_valid_span(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} - ctx = FORMAT.extract(carrier_getter, old_carrier) + ctx = FORMAT.extract(old_carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, self.trace_id) self.assertEqual(span_context.span_id, self.span_id) def test_missing_carrier(self): old_carrier = {} - ctx = FORMAT.extract(carrier_getter, old_carrier) + ctx = FORMAT.extract(old_carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) @@ -132,7 +128,7 @@ def test_baggage(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} input_baggage = {"key1": "value1"} _, new_carrier = get_context_new_carrier(old_carrier, input_baggage) - ctx = FORMAT.extract(carrier_getter, new_carrier) + ctx = FORMAT.extract(new_carrier) self.assertDictEqual(input_baggage, ctx["baggage"]) def test_non_string_baggage(self): @@ -140,7 +136,7 @@ def test_non_string_baggage(self): input_baggage = {"key1": 1, "key2": True} formatted_baggage = {"key1": "1", "key2": "True"} _, new_carrier = get_context_new_carrier(old_carrier, input_baggage) - ctx = FORMAT.extract(carrier_getter, new_carrier) + ctx = FORMAT.extract(new_carrier) self.assertDictEqual(formatted_baggage, ctx["baggage"]) def test_extract_invalid_uber_trace_id(self): @@ -149,7 +145,7 @@ def test_extract_invalid_uber_trace_id(self): "uberctx-key1": "value1", } formatted_baggage = {"key1": "value1"} - context = FORMAT.extract(carrier_getter, old_carrier) + context = FORMAT.extract(old_carrier) span_context = trace_api.get_current_span(context).get_span_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) self.assertDictEqual(formatted_baggage, context["baggage"]) @@ -160,7 +156,7 @@ def test_extract_invalid_trace_id(self): "uberctx-key1": "value1", } formatted_baggage = {"key1": "value1"} - context = FORMAT.extract(carrier_getter, old_carrier) + context = FORMAT.extract(old_carrier) span_context = trace_api.get_current_span(context).get_span_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) self.assertDictEqual(formatted_baggage, context["baggage"]) @@ -171,18 +167,18 @@ def test_extract_invalid_span_id(self): "uberctx-key1": "value1", } formatted_baggage = {"key1": "value1"} - context = FORMAT.extract(carrier_getter, old_carrier) + context = FORMAT.extract(old_carrier) span_context = trace_api.get_current_span(context).get_span_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) self.assertDictEqual(formatted_baggage, context["baggage"]) def test_fields(self): tracer = trace.TracerProvider().get_tracer("sdk_tracer_provider") - mock_set_in_carrier = Mock() + mock_setter = Mock() with tracer.start_as_current_span("parent"): with tracer.start_as_current_span("child"): - FORMAT.inject(mock_set_in_carrier, {}) + FORMAT.inject({}, setter=mock_setter) inject_fields = set() - for call in mock_set_in_carrier.mock_calls: + for call in mock_setter.mock_calls: inject_fields.add(call[1][1]) self.assertEqual(FORMAT.fields, inject_fields) diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index b7a365302f..2327abbfae 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -102,7 +102,6 @@ from opentelemetry.baggage import get_baggage, set_baggage from opentelemetry.context import Context, attach, detach, get_value, set_value from opentelemetry.propagate import get_global_textmap -from opentelemetry.propagators.textmap import DictGetter from opentelemetry.shim.opentracing_shim import util from opentelemetry.shim.opentracing_shim.version import __version__ from opentelemetry.trace import INVALID_SPAN_CONTEXT, Link, NonRecordingSpan @@ -527,7 +526,6 @@ def __init__(self, tracer: OtelTracer): Format.TEXT_MAP, Format.HTTP_HEADERS, ) - self._carrier_getter = DictGetter() def unwrap(self): """Returns the :class:`opentelemetry.trace.Tracer` object that is @@ -684,7 +682,7 @@ def inject(self, span_context, format: object, carrier: object): propagator = get_global_textmap() ctx = set_span_in_context(NonRecordingSpan(span_context.unwrap())) - propagator.inject(type(carrier).__setitem__, carrier, context=ctx) + propagator.inject(carrier, context=ctx) def extract(self, format: object, carrier: object): """Returns an ``opentracing.SpanContext`` instance extracted from a @@ -712,7 +710,7 @@ def extract(self, format: object, carrier: object): raise UnsupportedFormatException propagator = get_global_textmap() - ctx = propagator.extract(self._carrier_getter, carrier) + ctx = propagator.extract(carrier) span = get_current_span(ctx) if span is not None: otel_context = span.get_span_context() diff --git a/tests/util/src/opentelemetry/test/mock_textmap.py b/tests/util/src/opentelemetry/test/mock_textmap.py index 1edd079042..4cdef447d6 100644 --- a/tests/util/src/opentelemetry/test/mock_textmap.py +++ b/tests/util/src/opentelemetry/test/mock_textmap.py @@ -17,10 +17,12 @@ from opentelemetry import trace from opentelemetry.context import Context, get_current from opentelemetry.propagators.textmap import ( + CarrierT, Getter, Setter, TextMapPropagator, - TextMapPropagatorT, + default_getter, + default_setter, ) @@ -33,17 +35,17 @@ class NOOPTextMapPropagator(TextMapPropagator): def extract( self, - getter: Getter[TextMapPropagatorT], - carrier: TextMapPropagatorT, + carrier: CarrierT, context: typing.Optional[Context] = None, + getter: Getter = default_getter, ) -> Context: return get_current() def inject( self, - set_in_carrier: Setter[TextMapPropagatorT], - carrier: TextMapPropagatorT, + carrier: CarrierT, context: typing.Optional[Context] = None, + setter: Setter = default_setter, ) -> None: return None @@ -60,9 +62,9 @@ class MockTextMapPropagator(TextMapPropagator): def extract( self, - getter: Getter[TextMapPropagatorT], - carrier: TextMapPropagatorT, + carrier: CarrierT, context: typing.Optional[Context] = None, + getter: Getter = default_getter, ) -> Context: trace_id_list = getter.get(carrier, self.TRACE_ID_KEY) span_id_list = getter.get(carrier, self.SPAN_ID_KEY) @@ -82,15 +84,15 @@ def extract( def inject( self, - set_in_carrier: Setter[TextMapPropagatorT], - carrier: TextMapPropagatorT, + carrier: CarrierT, context: typing.Optional[Context] = None, + setter: Setter = default_setter, ) -> None: span = trace.get_current_span(context) - set_in_carrier( + setter.set( carrier, self.TRACE_ID_KEY, str(span.get_span_context().trace_id) ) - set_in_carrier( + setter.set( carrier, self.SPAN_ID_KEY, str(span.get_span_context().span_id) ) From b9c0b6f8827895e23eeff1f253f4c502154d1873 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 26 Mar 2021 15:54:32 -0700 Subject: [PATCH 0828/1517] preparing 1.0.0/0.19b0 release (#1718) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 21 ++++++++++++++++--- .../error_handler/error_handler_0/setup.cfg | 2 +- .../src/error_handler_0/version.py | 2 +- .../error_handler/error_handler_1/setup.cfg | 2 +- .../src/error_handler_1/version.py | 2 +- .../setup.cfg | 6 +++--- .../exporter/jaeger/proto/grpc/version.py | 2 +- .../setup.cfg | 6 +++--- .../exporter/jaeger/thrift/version.py | 2 +- .../opentelemetry-exporter-jaeger/setup.cfg | 6 +++--- .../opentelemetry/exporter/jaeger/version.py | 2 +- .../setup.cfg | 4 ++-- .../exporter/opencensus/version.py | 2 +- .../setup.cfg | 8 +++---- .../exporter/otlp/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../opentelemetry/exporter/otlp/version.py | 2 +- .../setup.cfg | 6 +++--- .../exporter/zipkin/json/version.py | 2 +- .../setup.cfg | 8 +++---- .../exporter/zipkin/proto/http/version.py | 2 +- .../opentelemetry-exporter-zipkin/setup.cfg | 6 +++--- .../opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/setup.cfg | 2 +- .../src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 6 +++--- .../src/opentelemetry/distro/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/setup.cfg | 2 +- .../src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- .../src/opentelemetry/sdk/version.py | 2 +- .../opentelemetry-propagator-b3/setup.cfg | 4 ++-- .../opentelemetry/propagators/b3/version.py | 2 +- .../opentelemetry-propagator-jaeger/setup.cfg | 4 ++-- .../propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 4 ++-- .../shim/opentracing_shim/version.py | 2 +- tests/util/setup.cfg | 4 ++-- tests/util/src/opentelemetry/test/version.py | 2 +- 42 files changed, 84 insertions(+), 69 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34d58b2cdb..1605ed5c5b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 0d12fa39523212e268ef435825af2039a876fd75 + CONTRIB_REPO_SHA: 4fc64a01c118c640e273f56d2afb1b4597b82f0f jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e7cbc0325..2eed844b5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.18b0...HEAD) -- Make setters and getters optional - ([#1690](https://github.com/open-telemetry/opentelemetry-python/pull/1690)) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.0.0...HEAD) + +## [1.0.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0) - 2021-03-26 ### Added - Document how to work with fork process web server models(Gunicorn, uWSGI etc...) ([#1609](https://github.com/open-telemetry/opentelemetry-python/pull/1609)) @@ -76,6 +76,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `opentelemetry-exporter-zipkin-proto-http` packages to reduce dependencies. The `opentelemetry-exporter-zipkin` installs both. ([#1699](https://github.com/open-telemetry/opentelemetry-python/pull/1699)) +- Make setters and getters optional + ([#1690](https://github.com/open-telemetry/opentelemetry-python/pull/1690)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. @@ -85,6 +87,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removing support for Python 3.5 ([#1706](https://github.com/open-telemetry/opentelemetry-python/pull/1706)) +## [0.19b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.19b0) - 2021-03-26 + +### Changed +- remove `service_name` from constructor of jaeger and opencensus exporters and + use of env variable `OTEL_PYTHON_SERVICE_NAME` + ([#1669])(https://github.com/open-telemetry/opentelemetry-python/pull/1669) +- Rename `IdsGenerator` to `IdGenerator` + ([#1651](https://github.com/open-telemetry/opentelemetry-python/pull/1651)) + +### Removed +- Removing support for Python 3.5 + ([#1706](https://github.com/open-telemetry/opentelemetry-python/pull/1706)) + ## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 ### Added diff --git a/docs/examples/error_handler/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg index 481d15ab8d..230b119661 100644 --- a/docs/examples/error_handler/error_handler_0/setup.cfg +++ b/docs/examples/error_handler/error_handler_0/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.0.0.dev0 + opentelemetry-sdk == 1.0.0 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py index 9194ed4a33..c7a4430aeb 100644 --- a/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py +++ b/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19.dev0" +__version__ = "0.19b0" diff --git a/docs/examples/error_handler/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg index 05388009e3..5cec7742d5 100644 --- a/docs/examples/error_handler/error_handler_1/setup.cfg +++ b/docs/examples/error_handler/error_handler_1/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.0.0.dev0 + opentelemetry-sdk == 1.0.0 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py index 9194ed4a33..c7a4430aeb 100644 --- a/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py +++ b/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19.dev0" +__version__ = "0.19b0" diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index 5c37d00b0a..d063ff3387 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/ platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 1.0.0.dev0 - opentelemetry-sdk == 1.0.0.dev0 + opentelemetry-api == 1.0.0 + opentelemetry-sdk == 1.0.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 15eb84e8c0..2406910318 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0.dev0" +__version__ = "1.0.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index 02d9cd4c9f..a1d53d7ca4 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/ platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 1.0.0.dev0 - opentelemetry-sdk == 1.0.0.dev0 + opentelemetry-api == 1.0.0 + opentelemetry-sdk == 1.0.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 15eb84e8c0..2406910318 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0.dev0" +__version__ = "1.0.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 36ee39e28d..81e2ae9f2b 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/ platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -38,8 +38,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.0.0.dev0 - opentelemetry-exporter-jaeger-thrift == 1.0.0.dev0 + opentelemetry-exporter-jaeger-proto-grpc == 1.0.0 + opentelemetry-exporter-jaeger-thrift == 1.0.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 15eb84e8c0..2406910318 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0.dev0" +__version__ = "1.0.0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 41edc45e3e..5154026d68 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 1.0.0.dev0 - opentelemetry-sdk == 1.0.0.dev0 + opentelemetry-api == 1.0.0 + opentelemetry-sdk == 1.0.0 protobuf >= 3.13.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 9194ed4a33..c7a4430aeb 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19.dev0" +__version__ = "0.19b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 7cf46dabaf..368286baa8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/ platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 1.0.0.dev0 - opentelemetry-sdk == 1.0.0.dev0 - opentelemetry-proto == 1.0.0.dev0 + opentelemetry-api == 1.0.0 + opentelemetry-sdk == 1.0.0 + opentelemetry-proto == 1.0.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index e4529dffc6..7b82da2b85 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0.dev0" +__version__ = "1.0.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 24ad1517f5..71055965c2 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/ platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -37,4 +37,4 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.0.0.dev0 + opentelemetry-exporter-otlp-proto-grpc == 1.0.0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index e4529dffc6..7b82da2b85 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0.dev0" +__version__ = "1.0.0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index 0f8a7c900a..6edf83421b 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/ platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 1.0.0.dev0 - opentelemetry-sdk == 1.0.0.dev0 + opentelemetry-api == 1.0.0 + opentelemetry-sdk == 1.0.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index e4529dffc6..7b82da2b85 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0.dev0" +__version__ = "1.0.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 96e67653a7..fc002c1d0a 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/ platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -42,9 +42,9 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 - opentelemetry-api == 1.0.0.dev0 - opentelemetry-sdk == 1.0.0.dev0 - opentelemetry-exporter-zipkin-json == 1.0.0.dev0 + opentelemetry-api == 1.0.0 + opentelemetry-sdk == 1.0.0 + opentelemetry-exporter-zipkin-json == 1.0.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index e4529dffc6..7b82da2b85 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0.dev0" +__version__ = "1.0.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 2e636b13b3..328fa87d75 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/ platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -37,8 +37,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.0.0.dev0 - opentelemetry-exporter-zipkin-proto-http == 1.0.0.dev0 + opentelemetry-exporter-zipkin-json == 1.0.0 + opentelemetry-exporter-zipkin-proto-http == 1.0.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index e4529dffc6..7b82da2b85 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0.dev0" +__version__ = "1.0.0" diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 1f4c7aab1e..63a0ee4c66 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelem platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index e4529dffc6..7b82da2b85 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0.dev0" +__version__ = "1.0.0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 0ee4ea93a0..f945a29f9f 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -40,8 +40,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.0.0.dev0 - opentelemetry-sdk == 1.0.0.dev0 + opentelemetry-api == 1.0.0 + opentelemetry-sdk == 1.0.0 [options.packages.find] where = src @@ -55,4 +55,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.0.0.dev0 + opentelemetry-exporter-otlp == 1.0.0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index 9194ed4a33..c7a4430aeb 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19.dev0" +__version__ = "0.19b0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 563eebfce0..8b565988fb 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.0.0.dev0 + opentelemetry-api == 1.0.0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 9194ed4a33..c7a4430aeb 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19.dev0" +__version__ = "0.19b0" diff --git a/opentelemetry-proto/setup.cfg b/opentelemetry-proto/setup.cfg index 96777d2fdf..0eaef7cfc5 100644 --- a/opentelemetry-proto/setup.cfg +++ b/opentelemetry-proto/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelem platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index e4529dffc6..7b82da2b85 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0.dev0" +__version__ = "1.0.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 063e8a052c..32fead520a 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelem platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.0.0.dev0 + opentelemetry-api == 1.0.0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index e4529dffc6..7b82da2b85 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0.dev0" +__version__ = "1.0.0" diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 62ac8b7a7b..861394dd5f 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/propagato platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.0.0.dev0 + opentelemetry-api == 1.0.0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index e4529dffc6..7b82da2b85 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0.dev0" +__version__ = "1.0.0" diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index 38646d203f..3cb1259f81 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/propagato platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.0.0.dev0 + opentelemetry-api == 1.0.0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index e4529dffc6..7b82da2b85 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0.dev0" +__version__ = "1.0.0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 3e305863b6..e8d4dc1fa5 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -41,11 +41,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 1.0.0.dev0 + opentelemetry-api == 1.0.0 [options.extras_require] test = - opentelemetry-test == 0.19.dev0 + opentelemetry-test == 0.19b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 9194ed4a33..c7a4430aeb 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19.dev0" +__version__ = "0.19b0" diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index 6ea81897a7..196a763d66 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -37,8 +37,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.0.0.dev0 - opentelemetry-sdk == 1.0.0.dev0 + opentelemetry-api == 1.0.0 + opentelemetry-sdk == 1.0.0 [options.extras_require] test = flask~=1.0 diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 19edb16431..0f82e1cdd2 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.19.dev0" +__version__ = "0.19b0" From d06ba7b332b7e18a5147e1875bddc19d7834ac8c Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 30 Mar 2021 20:34:52 +0530 Subject: [PATCH 0829/1517] Added a py.typed file every package. (#1720) --- CHANGELOG.md | 4 ++++ .../src/opentelemetry/exporter/jaeger/proto/grpc}/py.typed | 0 .../src/opentelemetry/exporter/jaeger/thrift}/py.typed | 0 .../src/opentelemetry/exporter/jaeger}/py.typed | 0 .../src/opentelemetry/exporter/opencensus}/py.typed | 0 .../src/opentelemetry/exporter/otlp/proto/grpc/py.typed | 0 .../src/opentelemetry/exporter/otlp/py.typed | 0 .../src/opentelemetry/exporter/zipkin/py.typed | 0 .../src/opentelemetry/exporter/zipkin/proto/http/py.typed | 0 .../src/opentelemetry/exporter/zipkin/py.typed | 0 opentelemetry-api/src/opentelemetry/py.typed | 0 opentelemetry-distro/src/opentelemetry/distro/py.typed | 0 .../src/opentelemetry/instrumentation/py.typed | 0 opentelemetry-sdk/src/opentelemetry/sdk/py.typed | 0 .../src/opentelemetry/propagators/b3/py.typed | 0 .../src/opentelemetry/propagators/jaeger/py.typed | 0 .../src/opentelemetry/shim/opentracing_shim/py.typed | 0 17 files changed, 4 insertions(+) rename {opentelemetry-api/src/opentelemetry/context => exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc}/py.typed (100%) rename {opentelemetry-api/src/opentelemetry/trace => exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift}/py.typed (100%) rename {opentelemetry-api/src/opentelemetry/util => exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger}/py.typed (100%) rename {opentelemetry-sdk/src/opentelemetry/sdk/util => exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus}/py.typed (100%) create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/py.typed create mode 100644 exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/py.typed create mode 100644 exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/py.typed create mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/py.typed create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/py.typed create mode 100644 opentelemetry-api/src/opentelemetry/py.typed create mode 100644 opentelemetry-distro/src/opentelemetry/distro/py.typed create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/py.typed create mode 100644 propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/py.typed create mode 100644 propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/py.typed create mode 100644 shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/py.typed diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eed844b5a..17eec0a6b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.0.0...HEAD) +- Added `py.typed` file to every package. This should resolve a bunch of mypy + errors for users. + ([#1720](https://github.com/open-telemetry/opentelemetry-python/pull/1720)) + ## [1.0.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0) - 2021-03-26 ### Added diff --git a/opentelemetry-api/src/opentelemetry/context/py.typed b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/py.typed similarity index 100% rename from opentelemetry-api/src/opentelemetry/context/py.typed rename to exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/py.typed diff --git a/opentelemetry-api/src/opentelemetry/trace/py.typed b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/py.typed similarity index 100% rename from opentelemetry-api/src/opentelemetry/trace/py.typed rename to exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/py.typed diff --git a/opentelemetry-api/src/opentelemetry/util/py.typed b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/py.typed similarity index 100% rename from opentelemetry-api/src/opentelemetry/util/py.typed rename to exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/py.typed diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/py.typed b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/py.typed similarity index 100% rename from opentelemetry-sdk/src/opentelemetry/sdk/util/py.typed rename to exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/py.typed diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/py.typed b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/py.typed b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/py.typed b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/py.typed b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/py.typed b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-api/src/opentelemetry/py.typed b/opentelemetry-api/src/opentelemetry/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-distro/src/opentelemetry/distro/py.typed b/opentelemetry-distro/src/opentelemetry/distro/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/py.typed b/opentelemetry-sdk/src/opentelemetry/sdk/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/py.typed b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/py.typed b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/py.typed b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/py.typed new file mode 100644 index 0000000000..e69de29bb2 From 442af037621919f777087e1fbb58841d940c8269 Mon Sep 17 00:00:00 2001 From: Marcin Zaremba Date: Wed, 31 Mar 2021 19:49:39 +0200 Subject: [PATCH 0830/1517] Make B3 propagator spec compliant (#1728) --- CHANGELOG.md | 6 ++ .../opentelemetry/propagators/b3/__init__.py | 21 ++--- .../tests/test_b3_format.py | 92 ++++++++----------- 3 files changed, 52 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17eec0a6b2..4c0a919e38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.0.0...HEAD) +### Added - Added `py.typed` file to every package. This should resolve a bunch of mypy errors for users. ([#1720](https://github.com/open-telemetry/opentelemetry-python/pull/1720)) +### Changed +- Adjust `B3Format` propagator to be spec compliant by not modifying context + when propagation headers are not present/invalid/empty + ([#1728](https://github.com/open-telemetry/opentelemetry-python/pull/1728)) + ## [1.0.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0) - 2021-03-26 ### Added diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py index be478b05ec..df8803d617 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py @@ -50,8 +50,8 @@ def extract( context: typing.Optional[Context] = None, getter: Getter = default_getter, ) -> Context: - trace_id = format_trace_id(trace.INVALID_TRACE_ID) - span_id = format_span_id(trace.INVALID_SPAN_ID) + trace_id = trace.INVALID_TRACE_ID + span_id = trace.INVALID_SPAN_ID sampled = "0" flags = None @@ -73,8 +73,6 @@ def extract( trace_id, span_id, sampled = fields elif len(fields) == 4: trace_id, span_id, sampled, _ = fields - else: - return trace.set_span_in_context(trace.INVALID_SPAN) else: trace_id = ( _extract_first_element(getter.get(carrier, self.TRACE_ID_KEY)) @@ -94,18 +92,15 @@ def extract( ) if ( - self._trace_id_regex.fullmatch(trace_id) is None + trace_id == trace.INVALID_TRACE_ID + or span_id == trace.INVALID_SPAN_ID + or self._trace_id_regex.fullmatch(trace_id) is None or self._span_id_regex.fullmatch(span_id) is None ): - id_generator = trace.get_tracer_provider().id_generator - trace_id = id_generator.generate_trace_id() - span_id = id_generator.generate_span_id() - sampled = "0" - - else: - trace_id = int(trace_id, 16) - span_id = int(span_id, 16) + return context + trace_id = int(trace_id, 16) + span_id = int(span_id, 16) options = 0 # The b3 spec provides no defined behavior for both sample and # flag values set. Since the setting of at least one implies diff --git a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index d1d96a269f..fd0a9a4029 100644 --- a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -13,7 +13,7 @@ # limitations under the License. import unittest -from unittest.mock import Mock, patch +from unittest.mock import Mock import opentelemetry.propagators.b3 as b3_format # pylint: disable=no-name-in-module,import-error import opentelemetry.sdk.trace as trace @@ -231,89 +231,73 @@ def test_64bit_trace_id(self): new_carrier[FORMAT.TRACE_ID_KEY], "0" * 16 + trace_id_64_bit ) - def test_invalid_single_header(self): - """If an invalid single header is passed, return an - invalid SpanContext. - """ + def test_extract_invalid_single_header(self): + """Given unparsable header, do not modify context""" + old_ctx = {} + carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} - ctx = FORMAT.extract(carrier) - span_context = trace_api.get_current_span(ctx).get_span_context() - self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) - self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) + new_ctx = FORMAT.extract(carrier, old_ctx) + + self.assertDictEqual(new_ctx, old_ctx) + + def test_extract_missing_trace_id(self): + """Given no trace ID, do not modify context""" + old_ctx = {} - def test_missing_trace_id(self): - """If a trace id is missing, populate an invalid trace id.""" carrier = { FORMAT.SPAN_ID_KEY: self.serialized_span_id, FORMAT.FLAGS_KEY: "1", } + new_ctx = FORMAT.extract(carrier, old_ctx) - ctx = FORMAT.extract(carrier) - span_context = trace_api.get_current_span(ctx).get_span_context() - self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) - - @patch( - "opentelemetry.sdk.trace.id_generator.RandomIdGenerator.generate_trace_id" - ) - @patch( - "opentelemetry.sdk.trace.id_generator.RandomIdGenerator.generate_span_id" - ) - def test_invalid_trace_id( - self, mock_generate_span_id, mock_generate_trace_id - ): - """If a trace id is invalid, generate a trace id.""" + self.assertDictEqual(new_ctx, old_ctx) - mock_generate_trace_id.configure_mock(return_value=1) - mock_generate_span_id.configure_mock(return_value=2) + def test_extract_invalid_trace_id(self): + """Given invalid trace ID, do not modify context""" + old_ctx = {} carrier = { FORMAT.TRACE_ID_KEY: "abc123", FORMAT.SPAN_ID_KEY: self.serialized_span_id, FORMAT.FLAGS_KEY: "1", } + new_ctx = FORMAT.extract(carrier, old_ctx) - ctx = FORMAT.extract(carrier) - span_context = trace_api.get_current_span(ctx).get_span_context() + self.assertDictEqual(new_ctx, old_ctx) - self.assertEqual(span_context.trace_id, 1) - self.assertEqual(span_context.span_id, 2) - - @patch( - "opentelemetry.sdk.trace.id_generator.RandomIdGenerator.generate_trace_id" - ) - @patch( - "opentelemetry.sdk.trace.id_generator.RandomIdGenerator.generate_span_id" - ) - def test_invalid_span_id( - self, mock_generate_span_id, mock_generate_trace_id - ): - """If a span id is invalid, generate a trace id.""" - - mock_generate_trace_id.configure_mock(return_value=1) - mock_generate_span_id.configure_mock(return_value=2) + def test_extract_invalid_span_id(self): + """Given invalid span ID, do not modify context""" + old_ctx = {} carrier = { FORMAT.TRACE_ID_KEY: self.serialized_trace_id, FORMAT.SPAN_ID_KEY: "abc123", FORMAT.FLAGS_KEY: "1", } + new_ctx = FORMAT.extract(carrier, old_ctx) - ctx = FORMAT.extract(carrier) - span_context = trace_api.get_current_span(ctx).get_span_context() + self.assertDictEqual(new_ctx, old_ctx) - self.assertEqual(span_context.trace_id, 1) - self.assertEqual(span_context.span_id, 2) + def test_extract_missing_span_id(self): + """Given no span ID, do not modify context""" + old_ctx = {} - def test_missing_span_id(self): - """If a trace id is missing, populate an invalid trace id.""" carrier = { FORMAT.TRACE_ID_KEY: self.serialized_trace_id, FORMAT.FLAGS_KEY: "1", } + new_ctx = FORMAT.extract(carrier, old_ctx) + + self.assertDictEqual(new_ctx, old_ctx) + + def test_extract_empty_carrier(self): + """Given no headers at all, do not modify context""" + old_ctx = {} + + carrier = {} + new_ctx = FORMAT.extract(carrier, old_ctx) - ctx = FORMAT.extract(carrier) - span_context = trace_api.get_current_span(ctx).get_span_context() - self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) + self.assertDictEqual(new_ctx, old_ctx) @staticmethod def test_inject_empty_context(): From 4027dae7b7418b02630c82ecfc2cfe154f6560d2 Mon Sep 17 00:00:00 2001 From: Manoj Pandey Date: Thu, 1 Apr 2021 17:19:46 +0200 Subject: [PATCH 0831/1517] Add type stub for dropped in BoundedList/BoundedDict (#1725) --- opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.pyi b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.pyi index 282c08b38f..d42e0f018f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.pyi +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.pyi @@ -40,6 +40,7 @@ class BoundedList(Sequence[_T]): not enough room. """ + dropped: int def __init__(self, maxlen: int): ... def insert(self, index: int, value: _T) -> None: ... @overload @@ -59,6 +60,7 @@ class BoundedDict(MutableMapping[_KT, _VT]): added. """ + dropped: int def __init__(self, maxlen: int): ... def __getitem__(self, k: _KT) -> _VT: ... def __setitem__(self, k: _KT, v: _VT) -> None: ... From 58521be7cd294bd30e9d0a9892ac52a3ce526c48 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Fri, 2 Apr 2021 04:23:15 +0530 Subject: [PATCH 0832/1517] Update bootstrap cmd to use exact version when installing (#1722) --- CHANGELOG.md | 2 + .../instrumentation/bootstrap.py | 71 +++++++++++-------- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c0a919e38..ec84c7a5a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adjust `B3Format` propagator to be spec compliant by not modifying context when propagation headers are not present/invalid/empty ([#1728](https://github.com/open-telemetry/opentelemetry-python/pull/1728)) +- Update bootstrap cmd to use exact version when installing instrumentation packages. + ([#1722](https://github.com/open-telemetry/opentelemetry-python/pull/1722)) ## [1.0.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0) - 2021-03-26 diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 5fb0be5ecb..46d05a7c8f 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -20,6 +20,8 @@ import sys from logging import getLogger +from opentelemetry.instrumentation.version import __version__ as version + logger = getLogger(__file__) @@ -32,36 +34,45 @@ # the libraries their application uses to figure which one can be # instrumented. # NOTE: system-metrics is not to be included. -instrumentations = { - "aiohttp-client": "opentelemetry-instrumentation-aiohttp-client>=0.15b0", - "aiopg": "opentelemetry-instrumentation-aiopg>=0.15b0", - "asyncpg": "opentelemetry-instrumentation-asyncpg>=0.11b0", - "boto": "opentelemetry-instrumentation-boto>=0.11b0", - "botocore": "opentelemetry-instrumentation-botocore>=0.11b0", - "celery": "opentelemetry-instrumentation-celery>=0.11b0", - "dbapi": "opentelemetry-instrumentation-dbapi>=0.8b0", - "django": "opentelemetry-instrumentation-django>=0.8b0", - "elasticsearch": "opentelemetry-instrumentation-elasticsearch>=0.11b0", - "falcon": "opentelemetry-instrumentation-falcon>=0.13b0", - "fastapi": "opentelemetry-instrumentation-fastapi>=0.11b0", - "flask": "opentelemetry-instrumentation-flask>=0.8b0", - "grpc": "opentelemetry-instrumentation-grpc>=0.8b0", - "jinja2": "opentelemetry-instrumentation-jinja2>=0.8b0", - "mysql": "opentelemetry-instrumentation-mysql>=0.8b0", - "psycopg2": "opentelemetry-instrumentation-psycopg2>=0.8b0", - "pymemcache": "opentelemetry-instrumentation-pymemcache>=0.11b0", - "pymongo": "opentelemetry-instrumentation-pymongo>=0.8b0", - "pymysql": "opentelemetry-instrumentation-pymysql>=0.8b0", - "pyramid": "opentelemetry-instrumentation-pyramid>=0.11b0", - "redis": "opentelemetry-instrumentation-redis>=0.8b0", - "requests": "opentelemetry-instrumentation-requests>=0.8b0", - "sklearn": "opentelemetry-instrumentation-sklearn>=0.15b0", - "sqlalchemy": "opentelemetry-instrumentation-sqlalchemy>=0.8b0", - "sqlite3": "opentelemetry-instrumentation-sqlite3>=0.11b0", - "starlette": "opentelemetry-instrumentation-starlette>=0.11b0", - "tornado": "opentelemetry-instrumentation-tornado>=0.13b0", - "urllib": "opentelemetry-instrumentation-urllib>=0.17b0", -} +def all_instrumentations(): + pkg_instrumentation_map = { + "aiohttp-client": "opentelemetry-instrumentation-aiohttp-client", + "aiopg": "opentelemetry-instrumentation-aiopg", + "asyncpg": "opentelemetry-instrumentation-asyncpg", + "boto": "opentelemetry-instrumentation-boto", + "botocore": "opentelemetry-instrumentation-botocore", + "celery": "opentelemetry-instrumentation-celery", + "dbapi": "opentelemetry-instrumentation-dbapi", + "django": "opentelemetry-instrumentation-django", + "elasticsearch": "opentelemetry-instrumentation-elasticsearch", + "falcon": "opentelemetry-instrumentation-falcon", + "fastapi": "opentelemetry-instrumentation-fastapi", + "flask": "opentelemetry-instrumentation-flask", + "grpc": "opentelemetry-instrumentation-grpc", + "jinja2": "opentelemetry-instrumentation-jinja2", + "mysql": "opentelemetry-instrumentation-mysql", + "psycopg2": "opentelemetry-instrumentation-psycopg2", + "pymemcache": "opentelemetry-instrumentation-pymemcache", + "pymongo": "opentelemetry-instrumentation-pymongo", + "pymysql": "opentelemetry-instrumentation-pymysql", + "pyramid": "opentelemetry-instrumentation-pyramid", + "redis": "opentelemetry-instrumentation-redis", + "requests": "opentelemetry-instrumentation-requests", + "sklearn": "opentelemetry-instrumentation-sklearn", + "sqlalchemy": "opentelemetry-instrumentation-sqlalchemy", + "sqlite3": "opentelemetry-instrumentation-sqlite3", + "starlette": "opentelemetry-instrumentation-starlette", + "tornado": "opentelemetry-instrumentation-tornado", + "urllib": "opentelemetry-instrumentation-urllib", + } + for pkg, instrumentation in pkg_instrumentation_map.items(): + pkg_instrumentation_map[pkg] = "{0}=={1}".format( + instrumentation, version + ) + return pkg_instrumentation_map + + +instrumentations = all_instrumentations() # relevant instrumentors and tracers to uninstall and check for conflicts for target libraries libraries = { From 0c0466e4cd2cfda986e526f833ce1c4f5310d960 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Fri, 2 Apr 2021 20:47:00 +0530 Subject: [PATCH 0833/1517] Remove unnecessary warning when (not) setting status description (#1721) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/trace/status.py | 18 +++++++++--------- opentelemetry-api/tests/trace/test_status.py | 4 ++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec84c7a5a2..b22da9c9b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adjust `B3Format` propagator to be spec compliant by not modifying context when propagation headers are not present/invalid/empty ([#1728](https://github.com/open-telemetry/opentelemetry-python/pull/1728)) +- Silence unnecessary warning when creating a new Status object without description. + ([#1721](https://github.com/open-telemetry/opentelemetry-python/pull/1721)) - Update bootstrap cmd to use exact version when installing instrumentation packages. ([#1722](https://github.com/open-telemetry/opentelemetry-python/pull/1722)) diff --git a/opentelemetry-api/src/opentelemetry/trace/status.py b/opentelemetry-api/src/opentelemetry/trace/status.py index 5e0469d71a..ada7fa1ebd 100644 --- a/opentelemetry-api/src/opentelemetry/trace/status.py +++ b/opentelemetry-api/src/opentelemetry/trace/status.py @@ -49,15 +49,15 @@ def __init__( self._status_code = status_code self._description = None - if description is not None and not isinstance(description, str): - logger.warning("Invalid status description type, expected str") - return - - if status_code is not StatusCode.ERROR: - logger.warning( - "description should only be set when status_code is set to StatusCode.ERROR" - ) - return + if description: + if not isinstance(description, str): + logger.warning("Invalid status description type, expected str") + return + if status_code is not StatusCode.ERROR: + logger.warning( + "description should only be set when status_code is set to StatusCode.ERROR" + ) + return self._description = description diff --git a/opentelemetry-api/tests/trace/test_status.py b/opentelemetry-api/tests/trace/test_status.py index fdfcd4e83e..74da78d6c7 100644 --- a/opentelemetry-api/tests/trace/test_status.py +++ b/opentelemetry-api/tests/trace/test_status.py @@ -30,8 +30,8 @@ def test_constructor(self): def test_invalid_description(self): with self.assertLogs(level=WARNING) as warning: - status = Status(description={"test": "val"}) # type: ignore - self.assertIs(status.status_code, StatusCode.UNSET) + status = Status(status_code=StatusCode.ERROR, description={"test": "val"}) # type: ignore + self.assertIs(status.status_code, StatusCode.ERROR) self.assertEqual(status.description, None) self.assertIn( "Invalid status description type, expected str", From 7054b534404fc69199e368cbe62a4e7570cc46ea Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Fri, 2 Apr 2021 13:17:07 -0700 Subject: [PATCH 0834/1517] Include new `service_name` workflow (#1739) Changes from https://github.com/open-telemetry/opentelemetry-python/pull/1669/ have not been reflected in the docs --- .../examples/jaeger_exporter_example.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/examples/jaeger_exporter_example.py b/exporter/opentelemetry-exporter-jaeger-thrift/examples/jaeger_exporter_example.py index 1dc138c1da..5678e3346d 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/examples/jaeger_exporter_example.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/examples/jaeger_exporter_example.py @@ -2,10 +2,15 @@ from opentelemetry import trace from opentelemetry.exporter.jaeger import thrift +from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor -trace.set_tracer_provider(TracerProvider()) +trace.set_tracer_provider( + TracerProvider( + resource=Resource.create({SERVICE_NAME: "my-helloworld-service"}) + ) +) tracer = trace.get_tracer(__name__) # create a JaegerExporter From b1939da2fdd6c5ec58609ca63f0c20a82a64e593 Mon Sep 17 00:00:00 2001 From: Austin Parker Date: Mon, 5 Apr 2021 11:26:26 -0400 Subject: [PATCH 0835/1517] Add opentelemetry.io docs (#1724) --- website_docs/_index.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 website_docs/_index.md diff --git a/website_docs/_index.md b/website_docs/_index.md new file mode 100644 index 0000000000..b056b02b56 --- /dev/null +++ b/website_docs/_index.md @@ -0,0 +1,30 @@ +--- +title: "Python" +weight: 22 +description: > + + A language-specific implementation of OpenTelemetry in Python. +--- + +This is the OpenTelemetry for Python documentation. OpenTelemetry is an +observability framework -- an API, SDK, and tools that are designed to aid in +the generation and collection of application telemetry data such as metrics, +logs, and traces. This documentation is designed to help you understand how to +get started using OpenTelemetry for Python. + +## Status and Releases + +The current status of the major functional components for OpenTelemetry Python +is as follows: + +| Tracing | Metrics | Logging | +| ------- | ------- | ------- | +| Stable | Alpha | Not Yet Implemented | + +The current release can be found [here](https://github.com/open-telemetry/opentelemetry-python/releases) + +## Further Reading + +- [Read the Docs](https://opentelemetry-python.readthedocs.io/en/stable/) +- [Examples](https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples) +- [Contrib Repository](https://github.com/open-telemetry/opentelemetry-python-contrib) From bcc3361e9219e8ebf554f753a4a1970887bcee40 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 5 Apr 2021 08:52:50 -0700 Subject: [PATCH 0836/1517] remove toumorokoshi from python-approvers (#1744) --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f56b790f32..2e8d6c887a 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,6 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Hector Hernandez](https://github.com/hectorhdzg), Microsoft - [Owais Lone](https://github.com/owais), Splunk - [Srikanth Chekuri](https://github.com/lonewolf3739) -- [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google *For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver).* From 39da28f8e721b6c2937d22dd8fe96742b13d409f Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 5 Apr 2021 09:14:05 -0700 Subject: [PATCH 0837/1517] Update dev version to 1.0.1.dev0 & 0.20.dev0 (#1740) --- .github/workflows/test.yml | 2 +- docs/examples/error_handler/error_handler_0/setup.cfg | 2 +- .../error_handler_0/src/error_handler_0/version.py | 2 +- docs/examples/error_handler/error_handler_1/setup.cfg | 2 +- .../error_handler_1/src/error_handler_1/version.py | 2 +- exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-json/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 6 +++--- opentelemetry-distro/src/opentelemetry/distro/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- propagator/opentelemetry-propagator-b3/setup.cfg | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- propagator/opentelemetry-propagator-jaeger/setup.cfg | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 4 ++-- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/util/setup.cfg | 4 ++-- tests/util/src/opentelemetry/test/version.py | 2 +- 39 files changed, 53 insertions(+), 53 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1605ed5c5b..5443bc16db 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 4fc64a01c118c640e273f56d2afb1b4597b82f0f + CONTRIB_REPO_SHA: fbd7e07d0b3f9be37ddfdbc8b12b35f0a8d71ede jobs: build: diff --git a/docs/examples/error_handler/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg index 230b119661..44656dabc8 100644 --- a/docs/examples/error_handler/error_handler_0/setup.cfg +++ b/docs/examples/error_handler/error_handler_0/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.0.0 + opentelemetry-sdk == 1.0.1.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py index c7a4430aeb..3b16be5d22 100644 --- a/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py +++ b/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19b0" +__version__ = "0.20.dev0" diff --git a/docs/examples/error_handler/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg index 5cec7742d5..b01d478664 100644 --- a/docs/examples/error_handler/error_handler_1/setup.cfg +++ b/docs/examples/error_handler/error_handler_1/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.0.0 + opentelemetry-sdk == 1.0.1.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py index c7a4430aeb..3b16be5d22 100644 --- a/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py +++ b/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19b0" +__version__ = "0.20.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index d063ff3387..a6e26c29e6 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 1.0.0 - opentelemetry-sdk == 1.0.0 + opentelemetry-api == 1.0.1.dev0 + opentelemetry-sdk == 1.0.1.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 2406910318..99c41b956f 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.0.1.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index a1d53d7ca4..4a5b7b4a70 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 1.0.0 - opentelemetry-sdk == 1.0.0 + opentelemetry-api == 1.0.1.dev0 + opentelemetry-sdk == 1.0.1.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 2406910318..99c41b956f 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.0.1.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 81e2ae9f2b..ff346fd8d4 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -38,8 +38,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.0.0 - opentelemetry-exporter-jaeger-thrift == 1.0.0 + opentelemetry-exporter-jaeger-proto-grpc == 1.0.1.dev0 + opentelemetry-exporter-jaeger-thrift == 1.0.1.dev0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 2406910318..99c41b956f 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.0.1.dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 5154026d68..67615351eb 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 1.0.0 - opentelemetry-sdk == 1.0.0 + opentelemetry-api == 1.0.1.dev0 + opentelemetry-sdk == 1.0.1.dev0 protobuf >= 3.13.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index c7a4430aeb..3b16be5d22 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19b0" +__version__ = "0.20.dev0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 368286baa8..a2d6eacce3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52.0 - opentelemetry-api == 1.0.0 - opentelemetry-sdk == 1.0.0 - opentelemetry-proto == 1.0.0 + opentelemetry-api == 1.0.1.dev0 + opentelemetry-sdk == 1.0.1.dev0 + opentelemetry-proto == 1.0.1.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 7b82da2b85..bfd8ccb84f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.0.1.dev0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 71055965c2..6776518fd8 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -37,4 +37,4 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.0.0 + opentelemetry-exporter-otlp-proto-grpc == 1.0.1.dev0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 7b82da2b85..bfd8ccb84f 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.0.1.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index 6edf83421b..cc860b0310 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 1.0.0 - opentelemetry-sdk == 1.0.0 + opentelemetry-api == 1.0.1.dev0 + opentelemetry-sdk == 1.0.1.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 7b82da2b85..bfd8ccb84f 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.0.1.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index fc002c1d0a..52f5ec30e0 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -42,9 +42,9 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 - opentelemetry-api == 1.0.0 - opentelemetry-sdk == 1.0.0 - opentelemetry-exporter-zipkin-json == 1.0.0 + opentelemetry-api == 1.0.1.dev0 + opentelemetry-sdk == 1.0.1.dev0 + opentelemetry-exporter-zipkin-json == 1.0.1.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 7b82da2b85..bfd8ccb84f 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.0.1.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 328fa87d75..388f47e779 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -37,8 +37,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.0.0 - opentelemetry-exporter-zipkin-proto-http == 1.0.0 + opentelemetry-exporter-zipkin-json == 1.0.1.dev0 + opentelemetry-exporter-zipkin-proto-http == 1.0.1.dev0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 7b82da2b85..bfd8ccb84f 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.0.1.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 7b82da2b85..bfd8ccb84f 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.0.1.dev0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index f945a29f9f..ba6eff4b03 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -40,8 +40,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.0.0 - opentelemetry-sdk == 1.0.0 + opentelemetry-api == 1.0.1.dev0 + opentelemetry-sdk == 1.0.1.dev0 [options.packages.find] where = src @@ -55,4 +55,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.0.0 + opentelemetry-exporter-otlp == 1.0.1.dev0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index c7a4430aeb..3b16be5d22 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19b0" +__version__ = "0.20.dev0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 8b565988fb..d2614f57f9 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.0.0 + opentelemetry-api == 1.0.1.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index c7a4430aeb..3b16be5d22 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19b0" +__version__ = "0.20.dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 7b82da2b85..bfd8ccb84f 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.0.1.dev0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 32fead520a..e3b2de4895 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.0.0 + opentelemetry-api == 1.0.1.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 7b82da2b85..bfd8ccb84f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.0.1.dev0" diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 861394dd5f..ce54e96f35 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.0.0 + opentelemetry-api == 1.0.1.dev0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 7b82da2b85..bfd8ccb84f 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.0.1.dev0" diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index 3cb1259f81..0af4b6e947 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.0.0 + opentelemetry-api == 1.0.1.dev0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 7b82da2b85..bfd8ccb84f 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.0.1.dev0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index e8d4dc1fa5..e340c78272 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -41,11 +41,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 1.0.0 + opentelemetry-api == 1.0.1.dev0 [options.extras_require] test = - opentelemetry-test == 0.19b0 + opentelemetry-test == 0.20.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index c7a4430aeb..3b16be5d22 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19b0" +__version__ = "0.20.dev0" diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index 196a763d66..897d04b0c2 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -37,8 +37,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.0.0 - opentelemetry-sdk == 1.0.0 + opentelemetry-api == 1.0.1.dev0 + opentelemetry-sdk == 1.0.1.dev0 [options.extras_require] test = flask~=1.0 diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 0f82e1cdd2..266e55076e 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.19b0" +__version__ = "0.20.dev0" From cad261e5dae1fe986c87e6965664b45cc9ab73c3 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 5 Apr 2021 09:56:40 -0700 Subject: [PATCH 0838/1517] Update RELEASING.md (#1746) --- RELEASING.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASING.md b/RELEASING.md index a16777987e..a3239d3053 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -8,6 +8,7 @@ Release Process: * [Create a Release](#Create-a-Release) * [Move stable tag](#Move-stable-tag) * [Update main](#Update-main) +* [Update docs](#Update-docs) * [Check PyPI](#Check-PyPI) * [Troubleshooting](#troubleshooting) @@ -73,6 +74,11 @@ perl -i -p -e 's/0.7b0/0.8.dev0/' $(git grep -l "0.7b0" | grep -vi CHANGELOG) git commit -m ``` +## Update docs + +If the docs for the Opentelemetry [website](https://opentelemetry.io/docs/python/) was updated in this release in this [folder](https://github.com/open-telemetry/opentelemetry-python/tree/main/website_docs), submit a [PR](https://github.com/open-telemetry/opentelemetry.io/tree/main/content/en/docs/python) to update the docs on the website accordingly. + + ## Troubleshooting ### Publish failed From 96fd84f024adb745e8380626168184b3ac37f27d Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 7 Apr 2021 03:38:21 +0530 Subject: [PATCH 0839/1517] Fixed B3 Propagator (#1750) --- CHANGELOG.md | 2 ++ .../opentelemetry/propagators/b3/__init__.py | 5 ++- .../tests/test_b3_format.py | 36 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b22da9c9b9..6dda7ba415 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1721](https://github.com/open-telemetry/opentelemetry-python/pull/1721)) - Update bootstrap cmd to use exact version when installing instrumentation packages. ([#1722](https://github.com/open-telemetry/opentelemetry-python/pull/1722)) +- Fix B3 propagator to never return None. + ([#1750](https://github.com/open-telemetry/opentelemetry-python/pull/1750)) ## [1.0.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0) - 2021-03-26 diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py index df8803d617..2d50ea88dc 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py @@ -97,6 +97,8 @@ def extract( or self._trace_id_regex.fullmatch(trace_id) is None or self._span_id_regex.fullmatch(span_id) is None ): + if context is None: + return trace.set_span_in_context(trace.INVALID_SPAN, context) return context trace_id = int(trace_id, 16) @@ -119,7 +121,8 @@ def extract( trace_flags=trace.TraceFlags(options), trace_state=trace.TraceState(), ) - ) + ), + context, ) def inject( diff --git a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index fd0a9a4029..6ee0be2ce1 100644 --- a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -51,6 +51,8 @@ def get_child_parent_new_carrier(old_carrier): class TestB3Format(unittest.TestCase): + # pylint: disable=too-many-public-methods + @classmethod def setUpClass(cls): generator = id_generator.RandomIdGenerator() @@ -215,6 +217,31 @@ def test_flags_and_sampling(self): self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + def test_derived_ctx_is_returned_for_success(self): + """Ensure returned context is derived from the given context.""" + old_ctx = {"k1": "v1"} + new_ctx = FORMAT.extract( + { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1", + }, + old_ctx, + ) + self.assertIn("current-span", new_ctx) + for key, value in old_ctx.items(): + self.assertIn(key, new_ctx) + self.assertEqual(new_ctx[key], value) + + def test_derived_ctx_is_returned_for_failure(self): + """Ensure returned context is derived from the given context.""" + old_ctx = {"k2": "v2"} + new_ctx = FORMAT.extract({}, old_ctx) + self.assertNotIn("current-span", new_ctx) + for key, value in old_ctx.items(): + self.assertIn(key, new_ctx) + self.assertEqual(new_ctx[key], value) + def test_64bit_trace_id(self): """64 bit trace ids should be padded to 128 bit trace ids.""" trace_id_64_bit = self.serialized_trace_id[:16] @@ -334,3 +361,12 @@ def test_fields(self): inject_fields.add(call[1][1]) self.assertEqual(FORMAT.fields, inject_fields) + + def test_extract_none_context(self): + """Given no trace ID, do not modify context""" + old_ctx = None + + carrier = {} + new_ctx = FORMAT.extract(carrier, old_ctx) + self.assertIsNotNone(new_ctx) + self.assertEqual(new_ctx["current-span"], trace_api.INVALID_SPAN) From 87742769baba1c874405a9da8749ae0d02beafaf Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 7 Apr 2021 20:35:34 +0530 Subject: [PATCH 0840/1517] Prototype proxy tracer/provider to enable lazy setup of tracing pipeline (#1726) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 3 + .../src/opentelemetry/trace/__init__.py | 62 +++++++++++++++- opentelemetry-api/tests/trace/test_proxy.py | 72 +++++++++++++++++++ 4 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 opentelemetry-api/tests/trace/test_proxy.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5443bc16db..b0821906d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: fbd7e07d0b3f9be37ddfdbc8b12b35f0a8d71ede + CONTRIB_REPO_SHA: 1064da4bf2afaa0fb84fa10f573b211efeba72bc jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dda7ba415..dc3b62b611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1722](https://github.com/open-telemetry/opentelemetry-python/pull/1722)) - Fix B3 propagator to never return None. ([#1750](https://github.com/open-telemetry/opentelemetry-python/pull/1750)) +- Added ProxyTracerProvider and ProxyTracer implementations to allow fetching provider + and tracer instances before a global provider is set up. + ([#1726](https://github.com/open-telemetry/opentelemetry-python/pull/1726)) ## [1.0.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0) - 2021-03-26 diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 59ae217304..d06a09c20a 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -74,6 +74,7 @@ """ +import os from abc import ABC, abstractmethod from contextlib import contextmanager from enum import Enum @@ -220,6 +221,21 @@ def get_tracer( return _DefaultTracer() +class ProxyTracerProvider(TracerProvider): + def get_tracer( + self, + instrumenting_module_name: str, + instrumenting_library_version: str = "", + ) -> "Tracer": + if _TRACER_PROVIDER: + return _TRACER_PROVIDER.get_tracer( + instrumenting_module_name, instrumenting_library_version + ) + return ProxyTracer( + instrumenting_module_name, instrumenting_library_version + ) + + class Tracer(ABC): """Handles span creation and in-process context propagation. @@ -349,6 +365,40 @@ def start_as_current_span( """ +class ProxyTracer(Tracer): + # pylint: disable=W0222,signature-differs + def __init__( + self, + instrumenting_module_name: str, + instrumenting_library_version: str, + ): + self._instrumenting_module_name = instrumenting_module_name + self._instrumenting_library_version = instrumenting_library_version + self._real_tracer: Optional[Tracer] = None + self._noop_tracer = _DefaultTracer() + + @property + def _tracer(self) -> Tracer: + if self._real_tracer: + return self._real_tracer + + if _TRACER_PROVIDER: + self._real_tracer = _TRACER_PROVIDER.get_tracer( + self._instrumenting_module_name, + self._instrumenting_library_version, + ) + return self._real_tracer + return self._noop_tracer + + def start_span(self, *args, **kwargs) -> Span: # type: ignore + return self._tracer.start_span(*args, **kwargs) # type: ignore + + def start_as_current_span( # type: ignore + self, *args, **kwargs + ) -> Span: + return self._tracer.start_as_current_span(*args, **kwargs) # type: ignore + + class _DefaultTracer(Tracer): """The default Tracer, used when no Tracer implementation is available. @@ -387,6 +437,7 @@ def start_as_current_span( _TRACER_PROVIDER = None +_PROXY_TRACER_PROVIDER = None def get_tracer( @@ -425,9 +476,18 @@ def set_tracer_provider(tracer_provider: TracerProvider) -> None: def get_tracer_provider() -> TracerProvider: """Gets the current global :class:`~.TracerProvider` object.""" - global _TRACER_PROVIDER # pylint: disable=global-statement + # pylint: disable=global-statement + global _TRACER_PROVIDER + global _PROXY_TRACER_PROVIDER if _TRACER_PROVIDER is None: + # if a global tracer provider has not been set either via code or env + # vars, return a proxy tracer provider + if OTEL_PYTHON_TRACER_PROVIDER not in os.environ: + if not _PROXY_TRACER_PROVIDER: + _PROXY_TRACER_PROVIDER = ProxyTracerProvider() + return _PROXY_TRACER_PROVIDER + _TRACER_PROVIDER = cast( # type: ignore "TracerProvider", _load_provider(OTEL_PYTHON_TRACER_PROVIDER, "tracer_provider"), diff --git a/opentelemetry-api/tests/trace/test_proxy.py b/opentelemetry-api/tests/trace/test_proxy.py new file mode 100644 index 0000000000..57759676fe --- /dev/null +++ b/opentelemetry-api/tests/trace/test_proxy.py @@ -0,0 +1,72 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=W0212,W0222,W0221 + +import unittest + +from opentelemetry import trace +from opentelemetry.trace.span import INVALID_SPAN_CONTEXT, NonRecordingSpan + + +class TestProvider(trace._DefaultTracerProvider): + def get_tracer( + self, instrumentation_module_name, instrumentaiton_library_version=None + ): + return TestTracer() + + +class TestTracer(trace._DefaultTracer): + def start_span(self, *args, **kwargs): + return TestSpan(INVALID_SPAN_CONTEXT) + + +class TestSpan(NonRecordingSpan): + pass + + +class TestProxy(unittest.TestCase): + def test_proxy_tracer(self): + original_provider = trace._TRACER_PROVIDER + + provider = trace.get_tracer_provider() + # proxy provider + self.assertIsInstance(provider, trace.ProxyTracerProvider) + + # provider returns proxy tracer + tracer = provider.get_tracer("proxy-test") + self.assertIsInstance(tracer, trace.ProxyTracer) + + with tracer.start_span("span1") as span: + self.assertIsInstance(span, trace.NonRecordingSpan) + + with tracer.start_as_current_span("span2") as span: + self.assertIsInstance(span, trace.NonRecordingSpan) + + # set a real provider + trace.set_tracer_provider(TestProvider()) + + # tracer provider now returns real instance + self.assertIsInstance(trace.get_tracer_provider(), TestProvider) + + # references to the old provider still work but return real tracer now + real_tracer = provider.get_tracer("proxy-test") + self.assertIsInstance(real_tracer, TestTracer) + + # reference to old proxy tracer now delegates to a real tracer and + # creates real spans + with tracer.start_span("") as span: + self.assertIsInstance(span, TestSpan) + + trace._TRACER_PROVIDER = original_provider From 59d00ce0be7e3888a102507916a7844b50c84aa5 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 7 Apr 2021 12:31:42 -0600 Subject: [PATCH 0841/1517] Unpin micro version (#1753) --- exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg | 4 ++-- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index a6e26c29e6..29dc6fd2d6 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -40,7 +40,7 @@ package_dir= packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 - googleapis-common-protos ~= 1.52.0 + googleapis-common-protos ~= 1.52 opentelemetry-api == 1.0.1.dev0 opentelemetry-sdk == 1.0.1.dev0 @@ -52,4 +52,4 @@ test = [options.entry_points] opentelemetry_exporter = - jaeger_proto = opentelemetry.exporter.jaeger.proto.grpc:JaegerExporter \ No newline at end of file + jaeger_proto = opentelemetry.exporter.jaeger.proto.grpc:JaegerExporter diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index a2d6eacce3..9bf9f2c988 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -40,7 +40,7 @@ package_dir= packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 - googleapis-common-protos ~= 1.52.0 + googleapis-common-protos ~= 1.52 opentelemetry-api == 1.0.1.dev0 opentelemetry-sdk == 1.0.1.dev0 opentelemetry-proto == 1.0.1.dev0 From 88157a4489f734a0fa633ebea4ad31c0fb394c8e Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 8 Apr 2021 09:41:15 -0700 Subject: [PATCH 0842/1517] add automation to close stale issues (#1733) --- .github/workflows/close-stale-issues.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/close-stale-issues.yml diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml new file mode 100644 index 0000000000..943cec5df4 --- /dev/null +++ b/.github/workflows/close-stale-issues.yml @@ -0,0 +1,18 @@ +name: "Close stale issues" +on: + schedule: + - cron: "12 3 * * *" # arbitrary time not to DDOS GitHub + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue was marked stale due to lack of activity. It will be closed in 30 days.' + close-issue-message: 'Closed as inactive. Feel free to reopen if this issue needs resolving.' + exempt-issue-labels: 'feature-request,triaged' + stale-issue-label: 'backlog' + days-before-stale: 30 + days-before-close: 60 From 6f8c077fa5349d445358b879fc83c139a0c22cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Fri, 9 Apr 2021 20:23:24 +0200 Subject: [PATCH 0843/1517] Fix eachdist.py not printing invoked commands. (#1758) I observed the following pattern: ``` >>> cmd1 >>> cmd2 ``` The `>>> cmd` header should come immediately before the command output to facilitate debugging & progress reporting. --- scripts/eachdist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/eachdist.py b/scripts/eachdist.py index a939f0d93d..7a305a34e0 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -343,7 +343,7 @@ def runsubprocess(dry_run, params, *args, **kwargs): check = kwargs.pop("check") # Enforce specifying check - print(">>>", cmdstr, file=sys.stderr) + print(">>>", cmdstr, file=sys.stderr, flush=True) # This is a workaround for subprocess.run(['python']) leaving the virtualenv on Win32. # The cause for this is that when running the python.exe in a virtualenv, @@ -356,7 +356,7 @@ def runsubprocess(dry_run, params, *args, **kwargs): # Only this would find the "correct" python.exe. params = list(params) - executable = shutil.which(params[0]) # On Win32, pytho + executable = shutil.which(params[0]) if executable: params[0] = executable try: From b73d8009904fc1693b2ba3a1e0656af376f859cf Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 13 Apr 2021 15:24:03 -0700 Subject: [PATCH 0844/1517] no longer pin sphinx (#1766) --- docs-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index 2ce3b654c8..5a5e91059f 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -1,6 +1,6 @@ sphinx~=2.4 sphinx-rtd-theme~=0.4 -sphinx-autodoc-typehints~=1.10.2 +sphinx-autodoc-typehints # Need to install the api/sdk in the venv for autodoc. Modifying sys.path # doesn't work for pkg_resources. From 71e3a7a192c0fc8a7503fac967ada36a74b79e58 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 15 Apr 2021 08:00:08 -0700 Subject: [PATCH 0845/1517] Change `should_sample` parameters to be spec compliant (#1764) --- CHANGELOG.md | 3 + .../src/opentelemetry/sdk/trace/__init__.py | 7 +- .../src/opentelemetry/sdk/trace/sampling.py | 28 ++++- .../tests/trace/test_sampling.py | 119 +++++++++++------- 4 files changed, 106 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc3b62b611..e7f76a9797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `py.typed` file to every package. This should resolve a bunch of mypy errors for users. ([#1720](https://github.com/open-telemetry/opentelemetry-python/pull/1720)) +- Added `SpanKind` to `should_sample` parameters, suggest using parent span context's tracestate + instead of manually passed in tracestate in `should_sample` + ([#1764](https://github.com/open-telemetry/opentelemetry-python/pull/1764)) ### Changed - Adjust `B3Format` propagator to be spec compliant by not modifying context diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index ced0925a3b..625b8519ef 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -905,20 +905,17 @@ def start_span( # pylint: disable=too-many-locals if parent_span_context is None or not parent_span_context.is_valid: parent_span_context = None trace_id = self.id_generator.generate_trace_id() - trace_flags = None - trace_state = None else: trace_id = parent_span_context.trace_id - trace_flags = parent_span_context.trace_flags - trace_state = parent_span_context.trace_state # The sampler decides whether to create a real or no-op span at the # time of span creation. No-op spans do not record events, and are not # exported. # The sampler may also add attributes to the newly-created span, e.g. # to include information about the sampling result. + # The sampler may also modify the parent span context's tracestate sampling_result = self.sampler.should_sample( - context, trace_id, name, attributes, links, trace_state + context, trace_id, name, kind, attributes, links ) trace_flags = ( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 81051f4b03..ce71569e72 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -33,6 +33,9 @@ Custom samplers can be created by subclassing `Sampler` and implementing `Sampler.should_sample` as well as `Sampler.get_description`. +Samplers are able to modify the `opentelemetry.trace.span.TraceState` of the parent of the span being created. For custom samplers, it is suggested to implement `Sampler.should_sample` to utilize the +parent span context's `opentelemetry.trace.span.TraceState` and pass into the `SamplingResult` instead of the explicit trace_state field passed into the parameter of `Sampler.should_sample`. + To use a sampler, pass it into the tracer provider constructor. For example: .. code:: python @@ -108,7 +111,7 @@ OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) -from opentelemetry.trace import Link, get_current_span +from opentelemetry.trace import Link, SpanKind, get_current_span from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes @@ -167,6 +170,7 @@ def should_sample( parent_context: Optional["Context"], trace_id: int, name: str, + kind: SpanKind = None, attributes: Attributes = None, links: Sequence["Link"] = None, trace_state: "TraceState" = None, @@ -189,13 +193,18 @@ def should_sample( parent_context: Optional["Context"], trace_id: int, name: str, + kind: SpanKind = None, attributes: Attributes = None, links: Sequence["Link"] = None, trace_state: "TraceState" = None, ) -> "SamplingResult": if self._decision is Decision.DROP: attributes = None - return SamplingResult(self._decision, attributes, trace_state) + return SamplingResult( + self._decision, + attributes, + _get_parent_trace_state(parent_context), + ) def get_description(self) -> str: if self._decision is Decision.DROP: @@ -246,6 +255,7 @@ def should_sample( parent_context: Optional["Context"], trace_id: int, name: str, + kind: SpanKind = None, attributes: Attributes = None, links: Sequence["Link"] = None, trace_state: "TraceState" = None, @@ -255,7 +265,9 @@ def should_sample( decision = Decision.RECORD_AND_SAMPLE if decision is Decision.DROP: attributes = None - return SamplingResult(decision, attributes, trace_state) + return SamplingResult( + decision, attributes, _get_parent_trace_state(parent_context), + ) def get_description(self) -> str: return "TraceIdRatioBased{{{}}}".format(self._rate) @@ -296,6 +308,7 @@ def should_sample( parent_context: Optional["Context"], trace_id: int, name: str, + kind: SpanKind = None, attributes: Attributes = None, links: Sequence["Link"] = None, trace_state: "TraceState" = None, @@ -322,9 +335,9 @@ def should_sample( parent_context=parent_context, trace_id=trace_id, name=name, + kind=kind, attributes=attributes, links=links, - trace_state=trace_state, ) def get_description(self): @@ -385,3 +398,10 @@ def _get_from_env_or_default() -> Sampler: return _KNOWN_SAMPLERS[trace_sampler](rate) return _KNOWN_SAMPLERS[trace_sampler] + + +def _get_parent_trace_state(parent_context) -> Optional["TraceState"]: + parent_span_context = get_current_span(parent_context).get_span_context() + if parent_span_context is None or not parent_span_context.is_valid: + return None + return parent_span_context.trace_state diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index 0abaa55b71..3e2a66bb90 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -66,17 +66,17 @@ def test_ctr(self): class TestSampler(unittest.TestCase): def _create_parent( - self, trace_flags: trace.TraceFlags, is_remote=False + self, trace_flags: trace.TraceFlags, is_remote=False, trace_state=None ) -> typing.Optional[context_api.Context]: if trace_flags is None: return None return trace.set_span_in_context( - self._create_parent_span(trace_flags, is_remote) + self._create_parent_span(trace_flags, is_remote, trace_state) ) @staticmethod def _create_parent_span( - trace_flags: trace.TraceFlags, is_remote=False + trace_flags: trace.TraceFlags, is_remote=False, trace_state=None ) -> trace.NonRecordingSpan: return trace.NonRecordingSpan( trace.SpanContext( @@ -84,6 +84,7 @@ def _create_parent_span( 0xDEADBEF0, is_remote=is_remote, trace_flags=trace_flags, + trace_state=trace_state, ) ) @@ -93,59 +94,65 @@ def test_always_on(self): for trace_flags in test_data: with self.subTest(trace_flags=trace_flags): - context = self._create_parent(trace_flags) + context = self._create_parent(trace_flags, False, trace_state) sample_result = sampling.ALWAYS_ON.should_sample( context, 0xDEADBEF1, "sampling on", + trace.SpanKind.INTERNAL, attributes={"sampled.expect": "true"}, - trace_state=trace_state, ) self.assertTrue(sample_result.decision.is_sampled()) self.assertEqual( sample_result.attributes, {"sampled.expect": "true"} ) - self.assertEqual(sample_result.trace_state, trace_state) + if context is not None: + self.assertEqual(sample_result.trace_state, trace_state) + else: + self.assertIsNone(sample_result.trace_state) def test_always_off(self): trace_state = trace.TraceState([("key", "value")]) test_data = (TO_DEFAULT, TO_SAMPLED, None) for trace_flags in test_data: with self.subTest(trace_flags=trace_flags): - context = self._create_parent(trace_flags) + context = self._create_parent(trace_flags, False, trace_state) sample_result = sampling.ALWAYS_OFF.should_sample( context, 0xDEADBEF1, "sampling off", + trace.SpanKind.INTERNAL, attributes={"sampled.expect": "false"}, - trace_state=trace_state, ) self.assertFalse(sample_result.decision.is_sampled()) self.assertEqual(sample_result.attributes, {}) - self.assertEqual(sample_result.trace_state, trace_state) + if context is not None: + self.assertEqual(sample_result.trace_state, trace_state) + else: + self.assertIsNone(sample_result.trace_state) def test_default_on(self): trace_state = trace.TraceState([("key", "value")]) - context = self._create_parent(trace_flags=TO_DEFAULT) + context = self._create_parent(TO_DEFAULT, False, trace_state) sample_result = sampling.DEFAULT_ON.should_sample( context, 0xDEADBEF1, "unsampled parent, sampling on", + trace.SpanKind.INTERNAL, attributes={"sampled.expect": "false"}, - trace_state=trace_state, ) self.assertFalse(sample_result.decision.is_sampled()) self.assertEqual(sample_result.attributes, {}) self.assertEqual(sample_result.trace_state, trace_state) - context = self._create_parent(trace_flags=TO_SAMPLED) + context = self._create_parent(TO_SAMPLED, False, trace_state) sample_result = sampling.DEFAULT_ON.should_sample( context, 0xDEADBEF1, "sampled parent, sampling on", + trace.SpanKind.INTERNAL, attributes={"sampled.expect": "true"}, - trace_state=trace_state, ) self.assertTrue(sample_result.decision.is_sampled()) self.assertEqual(sample_result.attributes, {"sampled.expect": "true"}) @@ -155,34 +162,34 @@ def test_default_on(self): None, 0xDEADBEF1, "no parent, sampling on", + trace.SpanKind.INTERNAL, attributes={"sampled.expect": "true"}, - trace_state=trace_state, ) self.assertTrue(sample_result.decision.is_sampled()) self.assertEqual(sample_result.attributes, {"sampled.expect": "true"}) - self.assertEqual(sample_result.trace_state, trace_state) + self.assertIsNone(sample_result.trace_state) def test_default_off(self): trace_state = trace.TraceState([("key", "value")]) - context = self._create_parent(trace_flags=TO_DEFAULT) + context = self._create_parent(TO_DEFAULT, False, trace_state) sample_result = sampling.DEFAULT_OFF.should_sample( context, 0xDEADBEF1, "unsampled parent, sampling off", + trace.SpanKind.INTERNAL, attributes={"sampled.expect", "false"}, - trace_state=trace_state, ) self.assertFalse(sample_result.decision.is_sampled()) self.assertEqual(sample_result.attributes, {}) self.assertEqual(sample_result.trace_state, trace_state) - context = self._create_parent(trace_flags=TO_SAMPLED) + context = self._create_parent(TO_SAMPLED, False, trace_state) sample_result = sampling.DEFAULT_OFF.should_sample( context, 0xDEADBEF1, "sampled parent, sampling on", + trace.SpanKind.INTERNAL, attributes={"sampled.expect": "true"}, - trace_state=trace_state, ) self.assertTrue(sample_result.decision.is_sampled()) self.assertEqual(sample_result.attributes, {"sampled.expect": "true"}) @@ -192,40 +199,40 @@ def test_default_off(self): None, 0xDEADBEF1, "unsampled parent, sampling off", + trace.SpanKind.INTERNAL, attributes={"sampled.expect": "false"}, - trace_state=trace_state, ) self.assertFalse(default_off.decision.is_sampled()) self.assertEqual(default_off.attributes, {}) - self.assertEqual(default_off.trace_state, trace_state) + self.assertIsNone(default_off.trace_state) def test_probability_sampler(self): - trace_state = trace.TraceState([("key", "value")]) sampler = sampling.TraceIdRatioBased(0.5) # Check that we sample based on the trace ID if the parent context is # null + # trace_state should also be empty since it is based off of parent sampled_result = sampler.should_sample( None, 0x7FFFFFFFFFFFFFFF, "sampled true", + trace.SpanKind.INTERNAL, attributes={"sampled.expect": "true"}, - trace_state=trace_state, ) self.assertTrue(sampled_result.decision.is_sampled()) self.assertEqual(sampled_result.attributes, {"sampled.expect": "true"}) - self.assertEqual(sampled_result.trace_state, trace_state) + self.assertIsNone(sampled_result.trace_state) not_sampled_result = sampler.should_sample( None, 0x8000000000000000, "sampled false", + trace.SpanKind.INTERNAL, attributes={"sampled.expect": "false"}, - trace_state=trace_state, ) self.assertFalse(not_sampled_result.decision.is_sampled()) self.assertEqual(not_sampled_result.attributes, {}) - self.assertEqual(not_sampled_result.trace_state, trace_state) + self.assertIsNone(sampled_result.trace_state) def test_probability_sampler_zero(self): default_off = sampling.TraceIdRatioBased(0.0) @@ -317,22 +324,26 @@ def exec_parent_based(self, parent_sampling_context): sampler = sampling.ParentBased(sampling.ALWAYS_ON) # Check that the sampling decision matches the parent context if given with parent_sampling_context( - self._create_parent_span(trace_flags=TO_DEFAULT) + self._create_parent_span( + trace_flags=TO_DEFAULT, trace_state=trace_state, + ) ) as context: # local, not sampled not_sampled_result = sampler.should_sample( context, 0x7FFFFFFFFFFFFFFF, "unsampled parent, sampling on", + trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, - trace_state=trace_state, ) self.assertFalse(not_sampled_result.decision.is_sampled()) self.assertEqual(not_sampled_result.attributes, {}) self.assertEqual(not_sampled_result.trace_state, trace_state) with parent_sampling_context( - self._create_parent_span(trace_flags=TO_DEFAULT) + self._create_parent_span( + trace_flags=TO_DEFAULT, trace_state=trace_state, + ) ) as context: sampler = sampling.ParentBased( root=sampling.ALWAYS_OFF, @@ -343,15 +354,17 @@ def exec_parent_based(self, parent_sampling_context): context, 0x7FFFFFFFFFFFFFFF, "unsampled parent, sampling on", + trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, - trace_state=trace_state, ) self.assertTrue(sampled_result.decision.is_sampled()) self.assertEqual(sampled_result.attributes, {"sampled": "false"}) self.assertEqual(sampled_result.trace_state, trace_state) with parent_sampling_context( - self._create_parent_span(trace_flags=TO_SAMPLED) + self._create_parent_span( + trace_flags=TO_SAMPLED, trace_state=trace_state, + ) ) as context: sampler = sampling.ParentBased(sampling.ALWAYS_OFF) # local, sampled @@ -359,6 +372,7 @@ def exec_parent_based(self, parent_sampling_context): context, 0x8000000000000000, "sampled parent, sampling off", + trace.SpanKind.INTERNAL, attributes={"sampled": "true"}, trace_state=trace_state, ) @@ -367,7 +381,9 @@ def exec_parent_based(self, parent_sampling_context): self.assertEqual(sampled_result.trace_state, trace_state) with parent_sampling_context( - self._create_parent_span(trace_flags=TO_SAMPLED) + self._create_parent_span( + trace_flags=TO_SAMPLED, trace_state=trace_state, + ) ) as context: sampler = sampling.ParentBased( root=sampling.ALWAYS_ON, @@ -378,6 +394,7 @@ def exec_parent_based(self, parent_sampling_context): context, 0x7FFFFFFFFFFFFFFF, "unsampled parent, sampling on", + trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, trace_state=trace_state, ) @@ -386,7 +403,11 @@ def exec_parent_based(self, parent_sampling_context): self.assertEqual(not_sampled_result.trace_state, trace_state) with parent_sampling_context( - self._create_parent_span(trace_flags=TO_DEFAULT, is_remote=True) + self._create_parent_span( + trace_flags=TO_DEFAULT, + is_remote=True, + trace_state=trace_state, + ) ) as context: sampler = sampling.ParentBased(sampling.ALWAYS_ON) # remote, not sampled @@ -394,6 +415,7 @@ def exec_parent_based(self, parent_sampling_context): context, 0x7FFFFFFFFFFFFFFF, "unsampled parent, sampling on", + trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, trace_state=trace_state, ) @@ -402,7 +424,11 @@ def exec_parent_based(self, parent_sampling_context): self.assertEqual(not_sampled_result.trace_state, trace_state) with parent_sampling_context( - self._create_parent_span(trace_flags=TO_DEFAULT, is_remote=True) + self._create_parent_span( + trace_flags=TO_DEFAULT, + is_remote=True, + trace_state=trace_state, + ) ) as context: sampler = sampling.ParentBased( root=sampling.ALWAYS_OFF, @@ -413,15 +439,19 @@ def exec_parent_based(self, parent_sampling_context): context, 0x7FFFFFFFFFFFFFFF, "unsampled parent, sampling on", + trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, - trace_state=trace_state, ) self.assertTrue(sampled_result.decision.is_sampled()) self.assertEqual(sampled_result.attributes, {"sampled": "false"}) self.assertEqual(sampled_result.trace_state, trace_state) with parent_sampling_context( - self._create_parent_span(trace_flags=TO_SAMPLED, is_remote=True) + self._create_parent_span( + trace_flags=TO_SAMPLED, + is_remote=True, + trace_state=trace_state, + ) ) as context: sampler = sampling.ParentBased(sampling.ALWAYS_OFF) # remote, sampled @@ -429,15 +459,19 @@ def exec_parent_based(self, parent_sampling_context): context, 0x8000000000000000, "sampled parent, sampling off", + trace.SpanKind.INTERNAL, attributes={"sampled": "true"}, - trace_state=trace_state, ) self.assertTrue(sampled_result.decision.is_sampled()) self.assertEqual(sampled_result.attributes, {"sampled": "true"}) self.assertEqual(sampled_result.trace_state, trace_state) with parent_sampling_context( - self._create_parent_span(trace_flags=TO_SAMPLED, is_remote=True) + self._create_parent_span( + trace_flags=TO_SAMPLED, + is_remote=True, + trace_state=trace_state, + ) ) as context: sampler = sampling.ParentBased( root=sampling.ALWAYS_ON, @@ -448,8 +482,8 @@ def exec_parent_based(self, parent_sampling_context): context, 0x7FFFFFFFFFFFFFFF, "unsampled parent, sampling on", + trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, - trace_state=trace_state, ) self.assertFalse(not_sampled_result.decision.is_sampled()) self.assertEqual(not_sampled_result.attributes, {}) @@ -462,12 +496,12 @@ def exec_parent_based(self, parent_sampling_context): context, 0x8000000000000000, "parent, sampling off", + trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, - trace_state=trace_state, ) self.assertFalse(not_sampled_result.decision.is_sampled()) self.assertEqual(not_sampled_result.attributes, {}) - self.assertEqual(not_sampled_result.trace_state, trace_state) + self.assertIsNone(not_sampled_result.trace_state) with parent_sampling_context(trace.INVALID_SPAN) as context: sampler = sampling.ParentBased(sampling.ALWAYS_ON) @@ -475,12 +509,13 @@ def exec_parent_based(self, parent_sampling_context): context, 0x8000000000000000, "no parent, sampling on", + trace.SpanKind.INTERNAL, attributes={"sampled": "true"}, trace_state=trace_state, ) self.assertTrue(sampled_result.decision.is_sampled()) self.assertEqual(sampled_result.attributes, {"sampled": "true"}) - self.assertEqual(sampled_result.trace_state, trace_state) + self.assertIsNone(sampled_result.trace_state) def test_parent_based_explicit_parent_context(self): @contextlib.contextmanager From 371eaa8e2d90fe418dbef163b65181736c8cfce3 Mon Sep 17 00:00:00 2001 From: Michael Stella Date: Fri, 16 Apr 2021 11:29:03 -0400 Subject: [PATCH 0846/1517] Add __contains__ to TraceState (#1773) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/trace/span.py | 7 +++++-- opentelemetry-api/tests/trace/test_tracestate.py | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f76a9797..62d9649844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added ProxyTracerProvider and ProxyTracer implementations to allow fetching provider and tracer instances before a global provider is set up. ([#1726](https://github.com/open-telemetry/opentelemetry-python/pull/1726)) +- Added `__contains__` to `opentelementry.trace.span.TraceState`. + ([#1773](https://github.com/open-telemetry/opentelemetry-python/pull/1773)) ## [1.0.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0) - 2021-03-26 diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index d04cdfa49d..c4c713cf3e 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -231,8 +231,11 @@ def __init__( "Invalid key/value pair (%s, %s) found.", key, value ) - def __getitem__(self, key: str) -> typing.Optional[str]: # type: ignore - return self._dict.get(key) + def __contains__(self, item: object) -> bool: + return item in self._dict + + def __getitem__(self, key: str) -> str: + return self._dict[key] def __iter__(self) -> typing.Iterator[str]: return iter(self._dict) diff --git a/opentelemetry-api/tests/trace/test_tracestate.py b/opentelemetry-api/tests/trace/test_tracestate.py index 6665dd612d..625b260d54 100644 --- a/opentelemetry-api/tests/trace/test_tracestate.py +++ b/opentelemetry-api/tests/trace/test_tracestate.py @@ -96,3 +96,19 @@ def test_tracestate_order_changed(self): foo_place = entries.index(("foo", "bar33")) # type: ignore prev_first_place = entries.index(("1a-2f@foo", "bar1")) # type: ignore self.assertLessEqual(foo_place, prev_first_place) + + def test_trace_contains(self): + entries = [ + "1a-2f@foo=bar1", + "1a-_*/2b@foo=bar2", + "foo=bar3", + "foo-_*/bar=bar4", + ] + header_list = [",".join(entries)] + state = TraceState.from_header(header_list) + + self.assertTrue("foo" in state) + self.assertFalse("bar" in state) + self.assertIsNone(state.get("bar")) + with self.assertRaises(KeyError): + state["bar"] # pylint:disable=W0104 From 77c9867dcb0f3179c1b395729aae6aec2c3133a0 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Fri, 16 Apr 2021 21:19:02 +0530 Subject: [PATCH 0847/1517] Upgraded black and isort (#1777) --- dev-requirements.txt | 4 +-- docs/conf.py | 25 +++++++++++++++---- docs/examples/opentracing/main.py | 5 +++- docs/getting_started/jaeger_example.py | 5 +++- docs/getting_started/tests/test_flask.py | 3 ++- .../examples/jaeger_exporter_example.py | 3 ++- .../jaeger/proto/grpc/translate/__init__.py | 8 ++++-- .../tests/test_jaeger_exporter_protobuf.py | 15 +++++++---- .../tests/test_jaeger_exporter_thrift.py | 7 ++++-- .../opencensus/trace_exporter/__init__.py | 5 +++- .../tests/test_otcollector_trace_exporter.py | 15 ++++++++--- .../proto/grpc/trace_exporter/__init__.py | 10 +++++--- .../test_benchmark_trace_exporter.py | 11 +++++--- .../tests/test_otlp_exporter_mixin.py | 4 ++- .../tests/test_otlp_trace_exporter.py | 9 ++++--- opentelemetry-api/setup.py | 4 ++- .../src/opentelemetry/propagate/__init__.py | 7 ++++-- .../src/opentelemetry/trace/__init__.py | 8 +++--- .../src/opentelemetry/util/_providers.py | 5 +++- .../tests/baggage/test_baggage.py | 3 ++- .../tests/context/test_contextvars_context.py | 4 ++- opentelemetry-distro/setup.py | 4 ++- .../src/opentelemetry/distro/__init__.py | 4 ++- opentelemetry-instrumentation/setup.py | 4 ++- .../tests/test_bootstrap.py | 5 +++- .../tests/test_utils.py | 20 ++++++++++++--- opentelemetry-proto/setup.py | 4 ++- opentelemetry-sdk/setup.py | 4 ++- .../sdk/trace/export/__init__.py | 8 +++--- .../src/opentelemetry/sdk/trace/sampling.py | 19 +++++++------- .../profile_resource_usage_batch_export.py | 3 ++- .../profile_resource_usage_simple_export.py | 3 ++- .../tests/trace/export/test_export.py | 11 +++++--- .../tests/trace/test_sampling.py | 15 +++++++---- .../opentelemetry/propagators/b3/__init__.py | 4 ++- .../tests/test_jaeger_propagator.py | 12 ++++++--- scripts/eachdist.py | 9 +++++-- shim/opentelemetry-opentracing-shim/setup.py | 7 +++++- .../tests/test_shim.py | 6 +++-- .../test_asyncio.py | 3 ++- 40 files changed, 215 insertions(+), 90 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 87dc7adc59..bcbc90148c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,7 @@ pylint==2.6.0 flake8~=3.7 -isort~=5.6 -black>=19.3b0,==19.* +isort~=5.8 +black~=20.8b1 httpretty~=1.0 mypy==0.812 sphinx~=2.1 diff --git a/docs/conf.py b/docs/conf.py index 61dc1a8254..0f1a69503f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -101,12 +101,27 @@ # Even if wrapt is added to intersphinx_mapping, sphinx keeps failing # with "class reference target not found: ObjectProxy". ("py:class", "ObjectProxy"), - ("py:class", "opentelemetry.trace._LinkBase",), + ( + "py:class", + "opentelemetry.trace._LinkBase", + ), # TODO: Understand why sphinx is not able to find this local class - ("py:class", "opentelemetry.propagators.textmap.TextMapPropagator",), - ("py:class", "opentelemetry.propagators.textmap.DefaultGetter",), - ("any", "opentelemetry.propagators.textmap.TextMapPropagator.extract",), - ("any", "opentelemetry.propagators.textmap.TextMapPropagator.inject",), + ( + "py:class", + "opentelemetry.propagators.textmap.TextMapPropagator", + ), + ( + "py:class", + "opentelemetry.propagators.textmap.DefaultGetter", + ), + ( + "any", + "opentelemetry.propagators.textmap.TextMapPropagator.extract", + ), + ( + "any", + "opentelemetry.propagators.textmap.TextMapPropagator.inject", + ), ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/examples/opentracing/main.py b/docs/examples/opentracing/main.py index 09ff51465c..9c6252ff8c 100755 --- a/docs/examples/opentracing/main.py +++ b/docs/examples/opentracing/main.py @@ -13,7 +13,10 @@ tracer_provider = trace.get_tracer_provider() # Configure the tracer to export traces to Jaeger -jaeger_exporter = JaegerExporter(agent_host_name="localhost", agent_port=6831,) +jaeger_exporter = JaegerExporter( + agent_host_name="localhost", + agent_port=6831, +) span_processor = SimpleSpanProcessor(jaeger_exporter) tracer_provider.add_span_processor(span_processor) diff --git a/docs/getting_started/jaeger_example.py b/docs/getting_started/jaeger_example.py index 77fea2585d..3d31b18fb9 100644 --- a/docs/getting_started/jaeger_example.py +++ b/docs/getting_started/jaeger_example.py @@ -25,7 +25,10 @@ ) ) -jaeger_exporter = JaegerExporter(agent_host_name="localhost", agent_port=6831,) +jaeger_exporter = JaegerExporter( + agent_host_name="localhost", + agent_port=6831, +) trace.get_tracer_provider().add_span_processor( BatchSpanProcessor(jaeger_exporter) diff --git a/docs/getting_started/tests/test_flask.py b/docs/getting_started/tests/test_flask.py index fdd5cb330a..d1475133f9 100644 --- a/docs/getting_started/tests/test_flask.py +++ b/docs/getting_started/tests/test_flask.py @@ -27,7 +27,8 @@ def test_flask(self): dirpath = os.path.dirname(os.path.realpath(__file__)) server_script = "{}/../flask_example.py".format(dirpath) server = subprocess.Popen( - [sys.executable, server_script], stdout=subprocess.PIPE, + [sys.executable, server_script], + stdout=subprocess.PIPE, ) retry_strategy = Retry(total=10, backoff_factor=1) adapter = HTTPAdapter(max_retries=retry_strategy) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/examples/jaeger_exporter_example.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/examples/jaeger_exporter_example.py index d8c017e5b0..dc3c539e75 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/examples/jaeger_exporter_example.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/examples/jaeger_exporter_example.py @@ -15,7 +15,8 @@ # `EXPORTER_JAEGER_CERTIFICATE` with file containing creds. jaeger_exporter = grpc.JaegerExporter( - collector_endpoint="localhost:14250", insecure=True, + collector_endpoint="localhost:14250", + insecure=True, ) # create a BatchSpanProcessor and add the exporter to it diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py index 5d11e14f60..778296a917 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py @@ -195,7 +195,8 @@ def _duration_from_two_time_stamps( See https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#duration """ duration = Duration( - seconds=end.seconds - start.seconds, nanos=end.nanos - start.nanos, + seconds=end.seconds - start.seconds, + nanos=end.nanos - start.nanos, ) # pylint: disable=chained-comparison if duration.seconds < 0 and duration.nanos > 0: @@ -350,7 +351,10 @@ def _extract_logs( fields.append(tag) fields.append( - _get_string_key_value(key="message", value=event.name,) + _get_string_key_value( + key="message", + value=event.name, + ) ) event_ts = _proto_timestamp_from_epoch_nanos(event.timestamp) logs.append(model_pb2.Log(timestamp=event_ts, fields=fields)) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py index 96bccaef4a..99b3f093cb 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py @@ -126,8 +126,8 @@ def test_translate_to_jaeger(self): event_timestamp = base_time + 50 * 10 ** 6 # pylint:disable=protected-access - event_timestamp_proto = pb_translator._proto_timestamp_from_epoch_nanos( - event_timestamp + event_timestamp_proto = ( + pb_translator._proto_timestamp_from_epoch_nanos(event_timestamp) ) event = trace.Event( @@ -339,7 +339,9 @@ def test_translate_to_jaeger(self): duration=span2_duration, flags=0, tags=default_tags, - process=model_pb2.Process(service_name="svc",), + process=model_pb2.Process( + service_name="svc", + ), ), model_pb2.Span( operation_name=span_names[2], @@ -370,7 +372,9 @@ def test_translate_to_jaeger(self): v_str="version", ), ], - process=model_pb2.Process(service_name="svc",), + process=model_pb2.Process( + service_name="svc", + ), ), ] @@ -378,7 +382,8 @@ def test_translate_to_jaeger(self): # (attributes) in otel is not important but in jeager it is # pylint: disable=no-member self.assertCountEqual( - spans[0].logs[0].fields, expected_spans[0].logs[0].fields, + spans[0].logs[0].fields, + expected_spans[0].logs[0].fields, ) self.assertEqual(spans, expected_spans) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py index 4cf06b04a5..9c1773d4fa 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py @@ -258,7 +258,9 @@ def test_translate_to_jaeger(self): default_tags = [ jaeger.Tag( - key="span.kind", vType=jaeger.TagType.STRING, vStr="internal", + key="span.kind", + vType=jaeger.TagType.STRING, + vStr="internal", ), ] @@ -522,7 +524,8 @@ def test_agent_client(self): spans = translate._translate(ThriftTranslator()) batch = jaeger.Batch( - spans=spans, process=jaeger.Process(serviceName="xxx"), + spans=spans, + process=jaeger.Process(serviceName="xxx"), ) agent_client.emit(batch) diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py index 4bea1af156..b20084888f 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py @@ -46,7 +46,10 @@ class OpenCensusSpanExporter(SpanExporter): """ def __init__( - self, endpoint=DEFAULT_ENDPOINT, host_name=None, client=None, + self, + endpoint=DEFAULT_ENDPOINT, + host_name=None, + client=None, ): tracer_provider = trace.get_tracer_provider() service_name = ( diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py index ab8a3a2eb6..222a94d60b 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py @@ -51,7 +51,9 @@ def test_constructor(self): endpoint = "testEndpoint" with patch: exporter = OpenCensusSpanExporter( - host_name=host_name, endpoint=endpoint, client=client, + host_name=host_name, + endpoint=endpoint, + client=client, ) self.assertIs(exporter.client, client) @@ -154,7 +156,10 @@ def test_translate_to_collector(self): otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) otel_spans[1].set_status( - trace_api.Status(trace_api.StatusCode.ERROR, {"test", "val"},) + trace_api.Status( + trace_api.StatusCode.ERROR, + {"test", "val"}, + ) ) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) @@ -194,7 +199,8 @@ def test_translate_to_collector(self): output_spans[2].parent_span_id, b"\x11\x11\x11\x11\x11\x11\x11\x11" ) self.assertEqual( - output_spans[0].status.code, trace_api.StatusCode.OK.value, + output_spans[0].status.code, + trace_api.StatusCode.OK.value, ) self.assertEqual(len(output_spans[0].tracestate.entries), 1) self.assertEqual(output_spans[0].tracestate.entries[0].key, "testkey") @@ -264,7 +270,8 @@ def test_translate_to_collector(self): trace_pb2.Span.Link.Type.TYPE_UNSPECIFIED, ) self.assertEqual( - output_spans[1].status.code, trace_api.StatusCode.ERROR.value, + output_spans[1].status.code, + trace_api.StatusCode.ERROR.value, ) self.assertEqual( output_spans[2].links.link[0].type, diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 518fc5551f..e7f54c1ac3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -241,10 +241,12 @@ def _translate_data( sdk_resource_instrumentation_library_spans.keys() ): if sdk_span.instrumentation_info is not None: - instrumentation_library_spans = InstrumentationLibrarySpans( - instrumentation_library=InstrumentationLibrary( - name=sdk_span.instrumentation_info.name, - version=sdk_span.instrumentation_info.version, + instrumentation_library_spans = ( + InstrumentationLibrarySpans( + instrumentation_library=InstrumentationLibrary( + name=sdk_span.instrumentation_info.name, + version=sdk_span.instrumentation_info.version, + ) ) ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/benchmarks/test_benchmark_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/benchmarks/test_benchmark_trace_exporter.py index 201a29fe7c..da292d02ad 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/benchmarks/test_benchmark_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/benchmarks/test_benchmark_trace_exporter.py @@ -27,7 +27,8 @@ def get_tracer_with_processor(span_processor_class): span_processor = span_processor_class(OTLPSpanExporter()) tracer = TracerProvider( - active_span_processor=span_processor, sampler=sampling.DEFAULT_ON, + active_span_processor=span_processor, + sampler=sampling.DEFAULT_ON, ).get_tracer("pipeline_benchmark_tracer") return tracer @@ -45,7 +46,9 @@ def test_simple_span_processor(benchmark): tracer = get_tracer_with_processor(SimpleSpanProcessor) def create_spans_to_be_exported(): - span = tracer.start_span("benchmarkedSpan",) + span = tracer.start_span( + "benchmarkedSpan", + ) for i in range(10): span.set_attribute( "benchmarkAttribute_{}".format(i), @@ -71,7 +74,9 @@ def test_batch_span_processor(benchmark): tracer = get_tracer_with_processor(BatchSpanProcessor) def create_spans_to_be_exported(): - span = tracer.start_span("benchmarkedSpan",) + span = tracer.start_span( + "benchmarkedSpan", + ) for i in range(10): span.set_attribute( "benchmarkAttribute_{}".format(i), diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index 7ea53ddc06..a7627b237c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -42,6 +42,8 @@ def test_environ_to_compression(self): ), Compression.Gzip, ) - self.assertIsNone(environ_to_compression("missing_key"),) + self.assertIsNone( + environ_to_compression("missing_key"), + ) with self.assertRaises(InvalidCompressionValueException): environ_to_compression("test_invalid") diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index ed22cbdbea..94fe01767d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -271,7 +271,8 @@ def test_otlp_exporter_otlp_compression_unspecified( # pylint: disable=no-self-use @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch.dict( - "os.environ", {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip"}, + "os.environ", + {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip"}, ) def test_otlp_exporter_otlp_compression_precendence( self, mock_insecure_channel @@ -455,10 +456,12 @@ def _check_translated_status( ) self.assertEqual( - status.code, code_expected, + status.code, + code_expected, ) self.assertEqual( - status.deprecated_code, deprecated_code_expected, + status.deprecated_code, + deprecated_code_expected, ) def test_span_status_translate(self): diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index 75d213ae4e..1bd85cb410 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -22,4 +22,6 @@ with open(VERSION_FILENAME) as f: exec(f.read(), PACKAGE_INFO) -setuptools.setup(version=PACKAGE_INFO["__version__"],) +setuptools.setup( + version=PACKAGE_INFO["__version__"], +) diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index 091b5b8d44..6c63edec3c 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -127,7 +127,8 @@ def inject( # Single use variable here to hack black and make lint pass environ_propagators = environ.get( - OTEL_PROPAGATORS, "tracecontext,baggage", + OTEL_PROPAGATORS, + "tracecontext,baggage", ) for propagator in environ_propagators.split(","): @@ -148,6 +149,8 @@ def get_global_textmap() -> textmap.TextMapPropagator: return _HTTP_TEXT_FORMAT -def set_global_textmap(http_text_format: textmap.TextMapPropagator,) -> None: +def set_global_textmap( + http_text_format: textmap.TextMapPropagator, +) -> None: global _HTTP_TEXT_FORMAT # pylint:disable=global-statement _HTTP_TEXT_FORMAT = http_text_format # type: ignore diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index d06a09c20a..cae50a5b47 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -134,7 +134,9 @@ class Link(_LinkBase): """ def __init__( - self, context: "SpanContext", attributes: types.Attributes = None, + self, + context: "SpanContext", + attributes: types.Attributes = None, ) -> None: super().__init__(context) self._attributes = attributes @@ -393,9 +395,7 @@ def _tracer(self) -> Tracer: def start_span(self, *args, **kwargs) -> Span: # type: ignore return self._tracer.start_span(*args, **kwargs) # type: ignore - def start_as_current_span( # type: ignore - self, *args, **kwargs - ) -> Span: + def start_as_current_span(self, *args, **kwargs) -> Span: # type: ignore return self._tracer.start_as_current_span(*args, **kwargs) # type: ignore diff --git a/opentelemetry-api/src/opentelemetry/util/_providers.py b/opentelemetry-api/src/opentelemetry/util/_providers.py index 8388f6d8e9..0e31f702ba 100644 --- a/opentelemetry-api/src/opentelemetry/util/_providers.py +++ b/opentelemetry-api/src/opentelemetry/util/_providers.py @@ -42,7 +42,10 @@ def _load_provider( ), ) ) - return cast(Provider, entry_point.load()(),) + return cast( + Provider, + entry_point.load()(), + ) except Exception: # pylint: disable=broad-except logger.error("Failed to load configured provider %s", provider) raise diff --git a/opentelemetry-api/tests/baggage/test_baggage.py b/opentelemetry-api/tests/baggage/test_baggage.py index 62f3fb77d5..223b9b2ff2 100644 --- a/opentelemetry-api/tests/baggage/test_baggage.py +++ b/opentelemetry-api/tests/baggage/test_baggage.py @@ -41,7 +41,8 @@ def test_set_multiple_baggage_entries(self): self.assertEqual(baggage.get_baggage("test", context=ctx), "value") self.assertEqual(baggage.get_baggage("test2", context=ctx), "value2") self.assertEqual( - baggage.get_all(context=ctx), {"test": "value", "test2": "value2"}, + baggage.get_all(context=ctx), + {"test": "value", "test2": "value2"}, ) def test_modifying_baggage(self): diff --git a/opentelemetry-api/tests/context/test_contextvars_context.py b/opentelemetry-api/tests/context/test_contextvars_context.py index 0c2ec1e6c2..2ac517abb2 100644 --- a/opentelemetry-api/tests/context/test_contextvars_context.py +++ b/opentelemetry-api/tests/context/test_contextvars_context.py @@ -33,7 +33,9 @@ class TestContextVarsContext(ContextTestCases.BaseTest): def setUp(self) -> None: super(TestContextVarsContext, self).setUp() self.mock_runtime = patch.object( - context, "_RUNTIME_CONTEXT", ContextVarsRuntimeContext(), + context, + "_RUNTIME_CONTEXT", + ContextVarsRuntimeContext(), ) self.mock_runtime.start() diff --git a/opentelemetry-distro/setup.py b/opentelemetry-distro/setup.py index da8417d50a..95af2c4840 100644 --- a/opentelemetry-distro/setup.py +++ b/opentelemetry-distro/setup.py @@ -24,4 +24,6 @@ with open(VERSION_FILENAME) as f: exec(f.read(), PACKAGE_INFO) -setuptools.setup(version=PACKAGE_INFO["__version__"],) +setuptools.setup( + version=PACKAGE_INFO["__version__"], +) diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index 258bdaddc5..d60bbd208e 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -70,7 +70,9 @@ def _init_tracing( ): # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name # from the env variable else defaults to "unknown_service" - provider = TracerProvider(id_generator=id_generator(),) + provider = TracerProvider( + id_generator=id_generator(), + ) trace.set_tracer_provider(provider) for _, exporter_class in exporters.items(): diff --git a/opentelemetry-instrumentation/setup.py b/opentelemetry-instrumentation/setup.py index fb3c8ff9f1..d4f84f738b 100644 --- a/opentelemetry-instrumentation/setup.py +++ b/opentelemetry-instrumentation/setup.py @@ -24,4 +24,6 @@ with open(VERSION_FILENAME) as f: exec(f.read(), PACKAGE_INFO) -setuptools.setup(version=PACKAGE_INFO["__version__"],) +setuptools.setup( + version=PACKAGE_INFO["__version__"], +) diff --git a/opentelemetry-instrumentation/tests/test_bootstrap.py b/opentelemetry-instrumentation/tests/test_bootstrap.py index e5a1a86dda..de978eb6d5 100644 --- a/opentelemetry-instrumentation/tests/test_bootstrap.py +++ b/opentelemetry-instrumentation/tests/test_bootstrap.py @@ -23,7 +23,10 @@ def sample_packages(packages, rate): - sampled = sample(list(packages), int(len(packages) * rate),) + sampled = sample( + list(packages), + int(len(packages) * rate), + ) return {k: v for k, v in packages.items() if k in sampled} diff --git a/opentelemetry-instrumentation/tests/test_utils.py b/opentelemetry-instrumentation/tests/test_utils.py index 273c6f085c..e5246335c9 100644 --- a/opentelemetry-instrumentation/tests/test_utils.py +++ b/opentelemetry-instrumentation/tests/test_utils.py @@ -31,12 +31,24 @@ def test_http_status_to_status_code(self): (HTTPStatus.UNAUTHORIZED, StatusCode.ERROR), (HTTPStatus.FORBIDDEN, StatusCode.ERROR), (HTTPStatus.NOT_FOUND, StatusCode.ERROR), - (HTTPStatus.UNPROCESSABLE_ENTITY, StatusCode.ERROR,), - (HTTPStatus.TOO_MANY_REQUESTS, StatusCode.ERROR,), + ( + HTTPStatus.UNPROCESSABLE_ENTITY, + StatusCode.ERROR, + ), + ( + HTTPStatus.TOO_MANY_REQUESTS, + StatusCode.ERROR, + ), (HTTPStatus.NOT_IMPLEMENTED, StatusCode.ERROR), (HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR), - (HTTPStatus.GATEWAY_TIMEOUT, StatusCode.ERROR,), - (HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, StatusCode.ERROR,), + ( + HTTPStatus.GATEWAY_TIMEOUT, + StatusCode.ERROR, + ), + ( + HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, + StatusCode.ERROR, + ), (600, StatusCode.ERROR), (99, StatusCode.ERROR), ): diff --git a/opentelemetry-proto/setup.py b/opentelemetry-proto/setup.py index 0bb2e6012e..8daf422ffa 100644 --- a/opentelemetry-proto/setup.py +++ b/opentelemetry-proto/setup.py @@ -24,4 +24,6 @@ with open(VERSION_FILENAME) as f: exec(f.read(), PACKAGE_INFO) -setuptools.setup(version=PACKAGE_INFO["__version__"],) +setuptools.setup( + version=PACKAGE_INFO["__version__"], +) diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index 3ba02ba245..cf910e40f1 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -24,4 +24,6 @@ with open(VERSION_FILENAME) as f: exec(f.read(), PACKAGE_INFO) -setuptools.setup(version=PACKAGE_INFO["__version__"],) +setuptools.setup( + version=PACKAGE_INFO["__version__"], +) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 2e7ab83d61..457a1ef56c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -258,7 +258,9 @@ def worker(self): self._notify_flush_request_finished(flush_request) self._notify_flush_request_finished(shutdown_flush_request) - def _get_and_unset_flush_request(self,) -> typing.Optional[_FlushRequest]: + def _get_and_unset_flush_request( + self, + ) -> typing.Optional[_FlushRequest]: """Returns the current flush request and makes it invisible to the worker thread for subsequent calls. """ @@ -316,8 +318,8 @@ def _export(self, flush_request: typing.Optional[_FlushRequest]): def _export_batch(self) -> int: """Exports at most max_export_batch_size spans and returns the number of - exported spans. - """ + exported spans. + """ idx = 0 # currently only a single thread acts as consumer, so queue.pop() will # not raise an exception diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index ce71569e72..833fbc76aa 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -266,7 +266,9 @@ def should_sample( if decision is Decision.DROP: attributes = None return SamplingResult( - decision, attributes, _get_parent_trace_state(parent_context), + decision, + attributes, + _get_parent_trace_state(parent_context), ) def get_description(self) -> str: @@ -341,15 +343,12 @@ def should_sample( ) def get_description(self): - return ( - "ParentBased{{root:{},remoteParentSampled:{},remoteParentNotSampled:{}," - "localParentSampled:{},localParentNotSampled:{}}}".format( - self._root.get_description(), - self._remote_parent_sampled.get_description(), - self._remote_parent_not_sampled.get_description(), - self._local_parent_sampled.get_description(), - self._local_parent_not_sampled.get_description(), - ) + return "ParentBased{{root:{},remoteParentSampled:{},remoteParentNotSampled:{}," "localParentSampled:{},localParentNotSampled:{}}}".format( + self._root.get_description(), + self._remote_parent_sampled.get_description(), + self._remote_parent_not_sampled.get_description(), + self._local_parent_sampled.get_description(), + self._local_parent_not_sampled.get_description(), ) diff --git a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py index c4b16dd8de..178d65b114 100644 --- a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py +++ b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py @@ -35,7 +35,8 @@ def __init__(self, channel): simple_span_processor = BatchSpanProcessor(OTLPSpanExporter()) tracer = TracerProvider( - active_span_processor=simple_span_processor, sampler=sampling.DEFAULT_ON, + active_span_processor=simple_span_processor, + sampler=sampling.DEFAULT_ON, ).get_tracer("resource_usage_tracer") starttime = time.time() diff --git a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py index efad1a8407..b82f133ec4 100644 --- a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py +++ b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py @@ -35,7 +35,8 @@ def __init__(self, channel): simple_span_processor = SimpleSpanProcessor(OTLPSpanExporter()) tracer = TracerProvider( - active_span_processor=simple_span_processor, sampler=sampling.DEFAULT_ON, + active_span_processor=simple_span_processor, + sampler=sampling.DEFAULT_ON, ).get_tracer("resource_usage_tracer") starttime = time.time() diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 831105b8ff..23f9308e08 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -365,7 +365,8 @@ def test_batch_span_processor_scheduled_delay(self): destination=spans_names_list, export_event=export_event ) span_processor = export.BatchSpanProcessor( - my_exporter, schedule_delay_millis=50, + my_exporter, + schedule_delay_millis=50, ) # create single span @@ -391,7 +392,8 @@ def test_batch_span_processor_reset_timeout(self): ) span_processor = export.BatchSpanProcessor( - my_exporter, schedule_delay_millis=50, + my_exporter, + schedule_delay_millis=50, ) with mock.patch.object(span_processor.condition, "wait") as mock_wait: @@ -424,7 +426,10 @@ def test_batch_span_processor_parameters(self): # negative max_queue_size self.assertRaises( - ValueError, export.BatchSpanProcessor, None, max_queue_size=-500, + ValueError, + export.BatchSpanProcessor, + None, + max_queue_size=-500, ) # zero schedule_delay_millis diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index 3e2a66bb90..808c64499d 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -315,7 +315,8 @@ def test_probability_sampler_limits(self): # the highest theoretical sampling rate. If this test fails the test # above is wrong. self.assertLess( - almost_almost_always_on.bound, 0xFFFFFFFFFFFFFFFF, + almost_almost_always_on.bound, + 0xFFFFFFFFFFFFFFFF, ) # pylint:disable=too-many-statements @@ -325,7 +326,8 @@ def exec_parent_based(self, parent_sampling_context): # Check that the sampling decision matches the parent context if given with parent_sampling_context( self._create_parent_span( - trace_flags=TO_DEFAULT, trace_state=trace_state, + trace_flags=TO_DEFAULT, + trace_state=trace_state, ) ) as context: # local, not sampled @@ -342,7 +344,8 @@ def exec_parent_based(self, parent_sampling_context): with parent_sampling_context( self._create_parent_span( - trace_flags=TO_DEFAULT, trace_state=trace_state, + trace_flags=TO_DEFAULT, + trace_state=trace_state, ) ) as context: sampler = sampling.ParentBased( @@ -363,7 +366,8 @@ def exec_parent_based(self, parent_sampling_context): with parent_sampling_context( self._create_parent_span( - trace_flags=TO_SAMPLED, trace_state=trace_state, + trace_flags=TO_SAMPLED, + trace_state=trace_state, ) ) as context: sampler = sampling.ParentBased(sampling.ALWAYS_OFF) @@ -382,7 +386,8 @@ def exec_parent_based(self, parent_sampling_context): with parent_sampling_context( self._create_parent_span( - trace_flags=TO_SAMPLED, trace_state=trace_state, + trace_flags=TO_SAMPLED, + trace_state=trace_state, ) ) as context: sampler = sampling.ParentBased( diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py index 2d50ea88dc..6977bc32c6 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py @@ -139,7 +139,9 @@ def inject( sampled = (trace.TraceFlags.SAMPLED & span_context.trace_flags) != 0 setter.set( - carrier, self.TRACE_ID_KEY, format_trace_id(span_context.trace_id), + carrier, + self.TRACE_ID_KEY, + format_trace_id(span_context.trace_id), ) setter.set( carrier, self.SPAN_ID_KEY, format_span_id(span_context.span_id) diff --git a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py index 003bbfeb49..12a0a028dd 100644 --- a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py @@ -62,8 +62,10 @@ def setUpClass(cls): cls.trace_id = generator.generate_trace_id() cls.span_id = generator.generate_span_id() cls.parent_span_id = generator.generate_span_id() - cls.serialized_uber_trace_id = jaeger._format_uber_trace_id( # pylint: disable=protected-access - cls.trace_id, cls.span_id, cls.parent_span_id, 11 + cls.serialized_uber_trace_id = ( + jaeger._format_uber_trace_id( # pylint: disable=protected-access + cls.trace_id, cls.span_id, cls.parent_span_id, 11 + ) ) def test_extract_valid_span(self): @@ -113,8 +115,10 @@ def test_debug_flag_set(self): self.assertEqual(FORMAT.DEBUG_FLAG, debug_flag_value) def test_sample_debug_flags_unset(self): - uber_trace_id = jaeger._format_uber_trace_id( # pylint: disable=protected-access - self.trace_id, self.span_id, self.parent_span_id, 0 + uber_trace_id = ( + jaeger._format_uber_trace_id( # pylint: disable=protected-access + self.trace_id, self.span_id, self.parent_span_id, 0 + ) ) old_carrier = {FORMAT.TRACE_ID_KEY: uber_trace_id} _, new_carrier = get_context_new_carrier(old_carrier) diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 7a305a34e0..3a9b2b3b9e 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -229,7 +229,8 @@ def setup_instparser(instparser): ) releaseparser = subparsers.add_parser( - "release", help="Prepares release, used by maintainers and CI", + "release", + help="Prepares release, used by maintainers and CI", ) releaseparser.set_defaults(func=release_args) releaseparser.add_argument("--version", required=True) @@ -512,7 +513,11 @@ def lint_args(args): execute_args( parse_subargs( args, - ("exec", "python scripts/check_for_valid_readme.py {}", "--all",), + ( + "exec", + "python scripts/check_for_valid_readme.py {}", + "--all", + ), ) ) diff --git a/shim/opentelemetry-opentracing-shim/setup.py b/shim/opentelemetry-opentracing-shim/setup.py index b8a9520713..304a5e661e 100644 --- a/shim/opentelemetry-opentracing-shim/setup.py +++ b/shim/opentelemetry-opentracing-shim/setup.py @@ -17,7 +17,12 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "shim", "opentracing_shim", "version.py", + BASE_DIR, + "src", + "opentelemetry", + "shim", + "opentracing_shim", + "version.py", ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/shim/opentelemetry-opentracing-shim/tests/test_shim.py b/shim/opentelemetry-opentracing-shim/tests/test_shim.py index a27d30de71..6feeedff06 100644 --- a/shim/opentelemetry-opentracing-shim/tests/test_shim.py +++ b/shim/opentelemetry-opentracing-shim/tests/test_shim.py @@ -623,7 +623,8 @@ def test_mixed_mode(self): ) as opentelemetry_span: self.assertIs( - span_shim.unwrap().context, opentelemetry_span.parent, + span_shim.unwrap().context, + opentelemetry_span.parent, ) with ( @@ -633,5 +634,6 @@ def test_mixed_mode(self): with self.shim.start_active_span("TestSpan17") as scope: self.assertIs( - scope.span.unwrap().parent, opentelemetry_span.context, + scope.span.unwrap().parent, + opentelemetry_span.context, ) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py index b0216dd756..b4619b4ed8 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py @@ -111,7 +111,8 @@ async def do_task(): # Set ignore_active_span to False indicating that the # framework will do it for us. req_handler = RequestHandler( - self.tracer, ignore_active_span=False, + self.tracer, + ignore_active_span=False, ) client = Client(req_handler, self.loop) response = await client.send_task("correct_parent") From 49c5c2fe1291bf7ab8c8733bc660b71de0821b6a Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 16 Apr 2021 10:44:46 -0700 Subject: [PATCH 0848/1517] fix issue in opentracing shim causing span to be wrapped unnecessarily (#1776) --- CHANGELOG.md | 4 +++- .../src/opentelemetry/shim/opentracing_shim/__init__.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d9649844..74d64e5911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1726](https://github.com/open-telemetry/opentelemetry-python/pull/1726)) - Added `__contains__` to `opentelementry.trace.span.TraceState`. ([#1773](https://github.com/open-telemetry/opentelemetry-python/pull/1773)) - +- `opentelemetry-opentracing-shim` Fix an issue in the shim where a Span was being wrapped + in a NonRecordingSpan when it wasn't necessary. + ([#1776](https://github.com/open-telemetry/opentelemetry-python/pull/1776)) ## [1.0.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0) - 2021-03-26 ### Added diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index 2327abbfae..4cddf2a57a 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -681,7 +681,11 @@ def inject(self, span_context, format: object, carrier: object): propagator = get_global_textmap() - ctx = set_span_in_context(NonRecordingSpan(span_context.unwrap())) + span = span_context.unwrap() if span_context else None + if isinstance(span, OtelSpanContext): + span = NonRecordingSpan(span) + + ctx = set_span_in_context(span) propagator.inject(carrier, context=ctx) def extract(self, format: object, carrier: object): From e2a5b0b7439acfc16d474fdd0adb3fca41308e80 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 16 Apr 2021 12:38:10 -0700 Subject: [PATCH 0849/1517] OTLP exporter uses scheme from endpoint configuration (#1771) --- CHANGELOG.md | 3 ++ docs/examples/fork-process-model/README.rst | 4 +- .../flask-gunicorn/gunicorn.config.py | 2 +- .../fork-process-model/flask-uwsgi/app.py | 2 +- .../exporter/otlp/proto/grpc/__init__.py | 2 +- .../exporter/otlp/proto/grpc/exporter.py | 14 +++++- .../tests/test_otlp_trace_exporter.py | 43 +++++++++++++++++++ 7 files changed, 64 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74d64e5911..ebfb94a195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-opentracing-shim` Fix an issue in the shim where a Span was being wrapped in a NonRecordingSpan when it wasn't necessary. ([#1776](https://github.com/open-telemetry/opentelemetry-python/pull/1776)) +- OTLP Exporter now uses the scheme in the endpoint to determine whether to establish + a secure connection or not. + ([#1771](https://github.com/open-telemetry/opentelemetry-python/pull/1771)) ## [1.0.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0) - 2021-03-26 ### Added diff --git a/docs/examples/fork-process-model/README.rst b/docs/examples/fork-process-model/README.rst index 2bd0c9ecbc..2f33bcf500 100644 --- a/docs/examples/fork-process-model/README.rst +++ b/docs/examples/fork-process-model/README.rst @@ -31,7 +31,7 @@ Gunicorn post_fork hook trace.set_tracer_provider(TracerProvider(resource=resource)) span_processor = BatchSpanProcessor( - OTLPSpanExporter(endpoint="localhost:4317") + OTLPSpanExporter(endpoint="http://localhost:4317") ) trace.get_tracer_provider().add_span_processor(span_processor) @@ -58,7 +58,7 @@ uWSGI postfork decorator trace.set_tracer_provider(TracerProvider(resource=resource)) span_processor = BatchSpanProcessor( - OTLPSpanExporter(endpoint="localhost:4317") + OTLPSpanExporter(endpoint="http://localhost:4317") ) trace.get_tracer_provider().add_span_processor(span_processor) diff --git a/docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py b/docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py index eaf6777392..902a0133e3 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py +++ b/docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py @@ -47,6 +47,6 @@ def post_fork(server, worker): # This uses insecure connection for the purpose of example. Please see the # OTLP Exporter documentation for other options. span_processor = BatchSpanProcessor( - OTLPSpanExporter(endpoint="localhost:4317", insecure=True) + OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True) ) trace.get_tracer_provider().add_span_processor(span_processor) diff --git a/docs/examples/fork-process-model/flask-uwsgi/app.py b/docs/examples/fork-process-model/flask-uwsgi/app.py index adcf52691f..fb56037c06 100644 --- a/docs/examples/fork-process-model/flask-uwsgi/app.py +++ b/docs/examples/fork-process-model/flask-uwsgi/app.py @@ -38,7 +38,7 @@ def init_tracing(): # This uses insecure connection for the purpose of example. Please see the # OTLP Exporter documentation for other options. span_processor = BatchSpanProcessor( - OTLPSpanExporter(endpoint="localhost:4317", insecure=True) + OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True) ) trace.get_tracer_provider().add_span_processor(span_processor) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py index c4c96b76c0..0a33b6325a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py @@ -57,7 +57,7 @@ trace.set_tracer_provider(TracerProvider(resource=resource)) tracer = trace.get_tracer(__name__) - otlp_exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True) + otlp_exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True) span_processor = BatchSpanProcessor(otlp_exporter) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index e3a42a24c5..03b17cf2e4 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -22,6 +22,8 @@ from typing import Any, Callable, Dict, Generic, List, Optional from typing import Sequence as TypingSequence from typing import Text, TypeVar +from urllib import parse +from urllib.parse import urlparse from backoff import expo from google.rpc.error_details_pb2 import RetryInfo @@ -194,9 +196,19 @@ def __init__( super().__init__() endpoint = endpoint or environ.get( - OTEL_EXPORTER_OTLP_ENDPOINT, "localhost:4317" + OTEL_EXPORTER_OTLP_ENDPOINT, "http://localhost:4317" ) + parsed_url = urlparse(endpoint) + + if insecure is None: + if parsed_url.scheme == "https": + insecure = False + else: + insecure = True + + endpoint = parsed_url.netloc + self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS) if isinstance(self._headers, str): self._headers = tuple( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 94fe01767d..5e0e9dd37b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -234,6 +234,49 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): exporter._headers, (("key3", "value3"), ("key4", "value4")) ) + # pylint: disable=no-self-use + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") + def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): + """Just OTEL_EXPORTER_OTLP_COMPRESSION should work""" + endpoints = [ + ( + "http://localhost:4317", + None, + mock_insecure, + ), + ( + "localhost:4317", + None, + mock_insecure, + ), + ( + "localhost:4317", + False, + mock_secure, + ), + ( + "https://localhost:4317", + None, + mock_secure, + ), + ( + "https://localhost:4317", + True, + mock_insecure, + ), + ] + for endpoint, insecure, mock_method in endpoints: + OTLPSpanExporter(endpoint=endpoint, insecure=insecure) + self.assertEqual( + 1, + mock_method.call_count, + "expected {} to be called for {} {}".format( + mock_method, endpoint, insecure + ), + ) + mock_method.reset_mock() + # pylint: disable=no-self-use @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) From 6bd163f6d670319eba6693b8465a068a1828f484 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Mon, 19 Apr 2021 22:52:37 +0530 Subject: [PATCH 0850/1517] Added experimental HTTP backpropagators (#1762) --- CHANGELOG.md | 2 + .../instrumentation/propagators.py | 128 ++++++++++++++++++ .../tests/test_propagators.py | 80 +++++++++++ 3 files changed, 210 insertions(+) create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py create mode 100644 opentelemetry-instrumentation/tests/test_propagators.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ebfb94a195..0ce882b471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `SpanKind` to `should_sample` parameters, suggest using parent span context's tracestate instead of manually passed in tracestate in `should_sample` ([#1764](https://github.com/open-telemetry/opentelemetry-python/pull/1764)) +- Added experimental HTTP back propagators. + ([#1762](https://github.com/open-telemetry/opentelemetry-python/pull/1762)) ### Changed - Adjust `B3Format` propagator to be spec compliant by not modifying context diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py new file mode 100644 index 0000000000..2fb246c9d9 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py @@ -0,0 +1,128 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module implements experimental propagators to inject trace context +into response carriers. This is useful for server side frameworks that start traces +when server requests and want to share the trace context with the client so the +client can add it's spans to the same trace. + +This is part of an upcoming W3C spec and will eventually make it to the Otel spec. + +https://w3c.github.io/trace-context/#trace-context-http-response-headers-format +""" + +import typing +from abc import ABC, abstractmethod + +import opentelemetry.trace as trace +from opentelemetry.context.context import Context +from opentelemetry.propagators import textmap +from opentelemetry.trace import format_span_id, format_trace_id + +_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers" +_RESPONSE_PROPAGATOR = None + + +def get_global_response_propagator(): + return _RESPONSE_PROPAGATOR + + +def set_global_response_propagator(propagator): + global _RESPONSE_PROPAGATOR # pylint:disable=global-statement + _RESPONSE_PROPAGATOR = propagator + + +class Setter(ABC): + @abstractmethod + def set(self, carrier, key, value): + """Inject the provided key value pair in carrier.""" + + +class DictHeaderSetter(Setter): + def set(self, carrier, key, value): # pylint: disable=no-self-use + old_value = carrier.get(key, "") + if old_value: + value = "{0}, {1}".format(old_value, value) + carrier[key] = value + + +class FuncSetter(Setter): + """FuncSetter coverts a function into a valid Setter. Any function that can + set values in a carrier can be converted into a Setter by using FuncSetter. + This is useful when injecting trace context into non-dict objects such + HTTP Response objects for different framework. + + For example, it can be used to create a setter for Falcon response object as: + + setter = FuncSetter(falcon.api.Response.append_header) + + and then used with the propagator as: + + propagator.inject(falcon_response, setter=setter) + + This would essentially make the propagator call `falcon_response.append_header(key, value)` + """ + + def __init__(self, func): + self._func = func + + def set(self, carrier, key, value): + self._func(carrier, key, value) + + +default_setter = DictHeaderSetter() + + +class ResponsePropagator(ABC): + @abstractmethod + def inject( + self, + carrier: textmap.CarrierT, + context: typing.Optional[Context] = None, + setter: textmap.Setter = default_setter, + ) -> None: + """Injects SpanContext into the HTTP response carrier.""" + + +class TraceResponsePropagator(ResponsePropagator): + """Experimental propagator that injects tracecontext into HTTP responses.""" + + def inject( + self, + carrier: textmap.CarrierT, + context: typing.Optional[Context] = None, + setter: textmap.Setter = default_setter, + ) -> None: + """Injects SpanContext into the HTTP response carrier.""" + span = trace.get_current_span(context) + span_context = span.get_span_context() + if span_context == trace.INVALID_SPAN_CONTEXT: + return + + header_name = "traceresponse" + setter.set( + carrier, + header_name, + "00-{trace_id}-{span_id}-{:02x}".format( + span_context.trace_flags, + trace_id=format_trace_id(span_context.trace_id), + span_id=format_span_id(span_context.span_id), + ), + ) + setter.set( + carrier, + _HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, + header_name, + ) diff --git a/opentelemetry-instrumentation/tests/test_propagators.py b/opentelemetry-instrumentation/tests/test_propagators.py new file mode 100644 index 0000000000..62461aafa9 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_propagators.py @@ -0,0 +1,80 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access + +from opentelemetry import trace +from opentelemetry.instrumentation import propagators +from opentelemetry.instrumentation.propagators import ( + DictHeaderSetter, + TraceResponsePropagator, + get_global_response_propagator, + set_global_response_propagator, +) +from opentelemetry.test.test_base import TestBase + + +class TestGlobals(TestBase): + def test_get_set(self): + original = propagators._RESPONSE_PROPAGATOR + + propagators._RESPONSE_PROPAGATOR = None + self.assertIsNone(get_global_response_propagator()) + + prop = TraceResponsePropagator() + set_global_response_propagator(prop) + self.assertIs(prop, get_global_response_propagator()) + + propagators._RESPONSE_PROPAGATOR = original + + +class TestDictHeaderSetter(TestBase): + def test_simple(self): + setter = DictHeaderSetter() + carrier = {} + setter.set(carrier, "kk", "vv") + self.assertIn("kk", carrier) + self.assertEqual(carrier["kk"], "vv") + + def test_append(self): + setter = DictHeaderSetter() + carrier = {"kk": "old"} + setter.set(carrier, "kk", "vv") + self.assertIn("kk", carrier) + self.assertEqual(carrier["kk"], "old, vv") + + +class TestTraceResponsePropagator(TestBase): + def test_inject(self): + span = trace.NonRecordingSpan( + trace.SpanContext( + trace_id=1, + span_id=2, + is_remote=False, + trace_flags=trace.DEFAULT_TRACE_OPTIONS, + trace_state=trace.DEFAULT_TRACE_STATE, + ), + ) + + ctx = trace.set_span_in_context(span) + prop = TraceResponsePropagator() + carrier = {} + prop.inject(carrier, ctx) + self.assertEqual( + carrier["Access-Control-Expose-Headers"], "traceresponse" + ) + self.assertEqual( + carrier["traceresponse"], + "00-00000000000000000000000000000001-0000000000000002-00", + ) From c85c409cb4541d2faacb26d882ba0592ea56cd75 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 20 Apr 2021 12:06:34 +0530 Subject: [PATCH 0851/1517] Autogenerate semantic convention constants (#1759) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 + docs-requirements.txt | 1 + opentelemetry-sdk/setup.cfg | 1 + .../opentelemetry/sdk/resources/__init__.py | 111 +-- opentelemetry-semantic-conventions/LICENSE | 201 +++++ .../MANIFEST.in | 9 + opentelemetry-semantic-conventions/README.rst | 36 + opentelemetry-semantic-conventions/setup.cfg | 48 + opentelemetry-semantic-conventions/setup.py | 29 + .../src/opentelemetry/semconv/__init__.py | 0 .../semconv/resource/__init__.py | 543 ++++++++++++ .../opentelemetry/semconv/trace/__init__.py | 822 ++++++++++++++++++ .../src/opentelemetry/semconv/version.py | 15 + .../tests/__init__.py | 0 .../tests/test_semconv.py | 27 + scripts/build.sh | 2 +- scripts/semconv/.gitignore | 1 + scripts/semconv/generate.sh | 41 + .../semconv/templates/semantic_attributes.j2 | 51 ++ tox.ini | 9 +- 21 files changed, 1893 insertions(+), 58 deletions(-) create mode 100644 opentelemetry-semantic-conventions/LICENSE create mode 100644 opentelemetry-semantic-conventions/MANIFEST.in create mode 100644 opentelemetry-semantic-conventions/README.rst create mode 100644 opentelemetry-semantic-conventions/setup.cfg create mode 100644 opentelemetry-semantic-conventions/setup.py create mode 100644 opentelemetry-semantic-conventions/src/opentelemetry/semconv/__init__.py create mode 100644 opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py create mode 100644 opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py create mode 100644 opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py create mode 100644 opentelemetry-semantic-conventions/tests/__init__.py create mode 100644 opentelemetry-semantic-conventions/tests/test_semconv.py create mode 100644 scripts/semconv/.gitignore create mode 100644 scripts/semconv/generate.sh create mode 100644 scripts/semconv/templates/semantic_attributes.j2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b0821906d1..eec2994f75 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 1064da4bf2afaa0fb84fa10f573b211efeba72bc + CONTRIB_REPO_SHA: 2cfa00e1f9277fcefdb64a8f47e7a99681ab65fa jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce882b471..50e4ca8765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `py.typed` file to every package. This should resolve a bunch of mypy errors for users. ([#1720](https://github.com/open-telemetry/opentelemetry-python/pull/1720)) +- Add auto generated trace and resource attributes semantic conventions + ([#1759](https://github.com/open-telemetry/opentelemetry-python/pull/1759)) - Added `SpanKind` to `should_sample` parameters, suggest using parent span context's tracestate instead of manually passed in tracestate in `should_sample` ([#1764](https://github.com/open-telemetry/opentelemetry-python/pull/1764)) diff --git a/docs-requirements.txt b/docs-requirements.txt index 5a5e91059f..bda3028040 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -5,6 +5,7 @@ sphinx-autodoc-typehints # Need to install the api/sdk in the venv for autodoc. Modifying sys.path # doesn't work for pkg_resources. ./opentelemetry-api +./opentelemetry-semantic-conventions ./opentelemetry-sdk ./opentelemetry-instrumentation diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index e3b2de4895..fc4b749853 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,6 +42,7 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api == 1.0.1.dev0 + opentelemetry-semantic-conventions == 0.20.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 6234891782..6cffff4263 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -65,67 +65,68 @@ import pkg_resources from opentelemetry.sdk.environment_variables import OTEL_RESOURCE_ATTRIBUTES +from opentelemetry.semconv.resource import ResourceAttributes LabelValue = typing.Union[str, bool, int, float] Attributes = typing.Dict[str, LabelValue] logger = logging.getLogger(__name__) -CLOUD_PROVIDER = "cloud.provider" -CLOUD_ACCOUNT_ID = "cloud.account.id" -CLOUD_REGION = "cloud.region" -CLOUD_ZONE = "cloud.zone" -CONTAINER_NAME = "container.name" -CONTAINER_ID = "container.id" -CONTAINER_IMAGE_NAME = "container.image.name" -CONTAINER_IMAGE_TAG = "container.image.tag" -DEPLOYMENT_ENVIRONMENT = "deployment.environment" -FAAS_NAME = "faas.name" -FAAS_ID = "faas.id" -FAAS_VERSION = "faas.version" -FAAS_INSTANCE = "faas.instance" -HOST_NAME = "host.name" -HOST_TYPE = "host.type" -HOST_IMAGE_NAME = "host.image.name" -HOST_IMAGE_ID = "host.image.id" -HOST_IMAGE_VERSION = "host.image.version" -KUBERNETES_CLUSTER_NAME = "k8s.cluster.name" -KUBERNETES_NAMESPACE_NAME = "k8s.namespace.name" -KUBERNETES_POD_UID = "k8s.pod.uid" -KUBERNETES_POD_NAME = "k8s.pod.name" -KUBERNETES_CONTAINER_NAME = "k8s.container.name" -KUBERNETES_REPLICA_SET_UID = "k8s.replicaset.uid" -KUBERNETES_REPLICA_SET_NAME = "k8s.replicaset.name" -KUBERNETES_DEPLOYMENT_UID = "k8s.deployment.uid" -KUBERNETES_DEPLOYMENT_NAME = "k8s.deployment.name" -KUBERNETES_STATEFUL_SET_UID = "k8s.statefulset.uid" -KUBERNETES_STATEFUL_SET_NAME = "k8s.statefulset.name" -KUBERNETES_DAEMON_SET_UID = "k8s.daemonset.uid" -KUBERNETES_DAEMON_SET_NAME = "k8s.daemonset.name" -KUBERNETES_JOB_UID = "k8s.job.uid" -KUBERNETES_JOB_NAME = "k8s.job.name" -KUBERNETES_CRON_JOB_UID = "k8s.cronjob.uid" -KUBERNETES_CRON_JOB_NAME = "k8s.cronjob.name" -OS_TYPE = "os.type" -OS_DESCRIPTION = "os.description" -PROCESS_PID = "process.pid" -PROCESS_EXECUTABLE_NAME = "process.executable.name" -PROCESS_EXECUTABLE_PATH = "process.executable.path" -PROCESS_COMMAND = "process.command" -PROCESS_COMMAND_LINE = "process.command_line" -PROCESS_COMMAND_ARGS = "process.command_args" -PROCESS_OWNER = "process.owner" -PROCESS_RUNTIME_NAME = "process.runtime.name" -PROCESS_RUNTIME_VERSION = "process.runtime.version" -PROCESS_RUNTIME_DESCRIPTION = "process.runtime.description" -SERVICE_NAME = "service.name" -SERVICE_NAMESPACE = "service.namespace" -SERVICE_INSTANCE_ID = "service.instance.id" -SERVICE_VERSION = "service.version" -TELEMETRY_SDK_NAME = "telemetry.sdk.name" -TELEMETRY_SDK_VERSION = "telemetry.sdk.version" -TELEMETRY_AUTO_VERSION = "telemetry.auto.version" -TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language" +CLOUD_PROVIDER = ResourceAttributes.CLOUD_PROVIDER +CLOUD_ACCOUNT_ID = ResourceAttributes.CLOUD_ACCOUNT_ID +CLOUD_REGION = ResourceAttributes.CLOUD_REGION +CLOUD_AVAILABILITY_ZONE = ResourceAttributes.CLOUD_AVAILABILITY_ZONE +CONTAINER_NAME = ResourceAttributes.CONTAINER_NAME +CONTAINER_ID = ResourceAttributes.CONTAINER_ID +CONTAINER_IMAGE_NAME = ResourceAttributes.CONTAINER_IMAGE_NAME +CONTAINER_IMAGE_TAG = ResourceAttributes.CONTAINER_IMAGE_TAG +DEPLOYMENT_ENVIRONMENT = ResourceAttributes.DEPLOYMENT_ENVIRONMENT +FAAS_NAME = ResourceAttributes.FAAS_NAME +FAAS_ID = ResourceAttributes.FAAS_ID +FAAS_VERSION = ResourceAttributes.FAAS_VERSION +FAAS_INSTANCE = ResourceAttributes.FAAS_INSTANCE +HOST_NAME = ResourceAttributes.HOST_NAME +HOST_TYPE = ResourceAttributes.HOST_TYPE +HOST_IMAGE_NAME = ResourceAttributes.HOST_IMAGE_NAME +HOST_IMAGE_ID = ResourceAttributes.HOST_IMAGE_ID +HOST_IMAGE_VERSION = ResourceAttributes.HOST_IMAGE_VERSION +KUBERNETES_CLUSTER_NAME = ResourceAttributes.K8S_CLUSTER_NAME +KUBERNETES_NAMESPACE_NAME = ResourceAttributes.K8S_NAMESPACE_NAME +KUBERNETES_POD_UID = ResourceAttributes.K8S_POD_UID +KUBERNETES_POD_NAME = ResourceAttributes.K8S_POD_NAME +KUBERNETES_CONTAINER_NAME = ResourceAttributes.K8S_CONTAINER_NAME +KUBERNETES_REPLICA_SET_UID = ResourceAttributes.K8S_REPLICASET_UID +KUBERNETES_REPLICA_SET_NAME = ResourceAttributes.K8S_REPLICASET_NAME +KUBERNETES_DEPLOYMENT_UID = ResourceAttributes.K8S_DEPLOYMENT_UID +KUBERNETES_DEPLOYMENT_NAME = ResourceAttributes.K8S_DEPLOYMENT_NAME +KUBERNETES_STATEFUL_SET_UID = ResourceAttributes.K8S_STATEFULSET_UID +KUBERNETES_STATEFUL_SET_NAME = ResourceAttributes.K8S_STATEFULSET_NAME +KUBERNETES_DAEMON_SET_UID = ResourceAttributes.K8S_DAEMONSET_UID +KUBERNETES_DAEMON_SET_NAME = ResourceAttributes.K8S_DAEMONSET_NAME +KUBERNETES_JOB_UID = ResourceAttributes.K8S_JOB_UID +KUBERNETES_JOB_NAME = ResourceAttributes.K8S_JOB_NAME +KUBERNETES_CRON_JOB_UID = ResourceAttributes.K8S_CRONJOB_UID +KUBERNETES_CRON_JOB_NAME = ResourceAttributes.K8S_CRONJOB_NAME +OS_TYPE = ResourceAttributes.OS_TYPE +OS_DESCRIPTION = ResourceAttributes.OS_DESCRIPTION +PROCESS_PID = ResourceAttributes.PROCESS_PID +PROCESS_EXECUTABLE_NAME = ResourceAttributes.PROCESS_EXECUTABLE_NAME +PROCESS_EXECUTABLE_PATH = ResourceAttributes.PROCESS_EXECUTABLE_PATH +PROCESS_COMMAND = ResourceAttributes.PROCESS_COMMAND +PROCESS_COMMAND_LINE = ResourceAttributes.PROCESS_COMMAND_LINE +PROCESS_COMMAND_ARGS = ResourceAttributes.PROCESS_COMMAND_ARGS +PROCESS_OWNER = ResourceAttributes.PROCESS_OWNER +PROCESS_RUNTIME_NAME = ResourceAttributes.PROCESS_RUNTIME_NAME +PROCESS_RUNTIME_VERSION = ResourceAttributes.PROCESS_RUNTIME_VERSION +PROCESS_RUNTIME_DESCRIPTION = ResourceAttributes.PROCESS_RUNTIME_DESCRIPTION +SERVICE_NAME = ResourceAttributes.SERVICE_NAME +SERVICE_NAMESPACE = ResourceAttributes.SERVICE_NAMESPACE +SERVICE_INSTANCE_ID = ResourceAttributes.SERVICE_INSTANCE_ID +SERVICE_VERSION = ResourceAttributes.SERVICE_VERSION +TELEMETRY_SDK_NAME = ResourceAttributes.TELEMETRY_SDK_NAME +TELEMETRY_SDK_VERSION = ResourceAttributes.TELEMETRY_SDK_VERSION +TELEMETRY_AUTO_VERSION = ResourceAttributes.TELEMETRY_AUTO_VERSION +TELEMETRY_SDK_LANGUAGE = ResourceAttributes.TELEMETRY_SDK_LANGUAGE _OPENTELEMETRY_SDK_VERSION = pkg_resources.get_distribution( diff --git a/opentelemetry-semantic-conventions/LICENSE b/opentelemetry-semantic-conventions/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/opentelemetry-semantic-conventions/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/opentelemetry-semantic-conventions/MANIFEST.in b/opentelemetry-semantic-conventions/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/opentelemetry-semantic-conventions/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/opentelemetry-semantic-conventions/README.rst b/opentelemetry-semantic-conventions/README.rst new file mode 100644 index 0000000000..3e1a322cdf --- /dev/null +++ b/opentelemetry-semantic-conventions/README.rst @@ -0,0 +1,36 @@ +OpenTelemetry Semantic Conventions +================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-semantic-conventions.svg + :target: https://pypi.org/project/opentelemetry-semantic-conventions/ + +This library contains generated code for the semantic conventions defined by the OpenTelemetry specification. + +Installation +------------ + +:: + + pip install opentelemetry-semantic-conventions + +Code Generation +--------------- + +These files were generated automatically from code in opentelemetry-semantic-conventions_. +To regenerate the code, run ``../scripts/semconv/generate.sh``. + +To build against a new release or specific commit of opentelemetry-semantic-conventions_, +update the ``SPEC_VERSION`` variable in +``../scripts/semconv/generate.sh``. Then run the script and commit the changes. + +.. _opentelemetry-semantic-conventions: https://github.com/open-telemetry/opentelemetry-semantic-conventions + + +References +---------- + +* `OpenTelemetry Project `_ +* `OpenTelemetry Semantic Conventions YAML Definitions `_ +* `generate.sh script `_ diff --git a/opentelemetry-semantic-conventions/setup.cfg b/opentelemetry-semantic-conventions/setup.cfg new file mode 100644 index 0000000000..81565ec00b --- /dev/null +++ b/opentelemetry-semantic-conventions/setup.cfg @@ -0,0 +1,48 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-semantic-conventions +description = OpenTelemetry Semantic Conventions +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-semantic-conventions +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.6 +package_dir= + =src +packages=find_namespace: +zip_safe = False +include_package_data = True + +[options.packages.find] +where = src + +[options.extras_require] +test = \ No newline at end of file diff --git a/opentelemetry-semantic-conventions/setup.py b/opentelemetry-semantic-conventions/setup.py new file mode 100644 index 0000000000..677d43cabb --- /dev/null +++ b/opentelemetry-semantic-conventions/setup.py @@ -0,0 +1,29 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "semconv", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup( + version=PACKAGE_INFO["__version__"], +) diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py new file mode 100644 index 0000000000..b1e76a77b6 --- /dev/null +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py @@ -0,0 +1,543 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum + + +class ResourceAttributes: + CLOUD_PROVIDER = "cloud.provider" + """ + Name of the cloud provider. + """ + + CLOUD_ACCOUNT_ID = "cloud.account.id" + """ + The cloud account ID the resource is assigned to. + """ + + CLOUD_REGION = "cloud.region" + """ + The geographical region the resource is running. Refer to your provider's docs to see the available regions, for example [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/en-us/global-infrastructure/geographies/), or [Google Cloud regions](https://cloud.google.com/about/locations). + """ + + CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone" + """ + Cloud regions often have multiple, isolated locations known as zones to increase availability. Availability zone represents the zone where the resource is running. + Note: Availability zones are called "zones" on Google Cloud. + """ + + CLOUD_INFRASTRUCTURE_SERVICE = "cloud.infrastructure_service" + """ + The cloud infrastructure resource in use. + Note: The prefix of the service SHOULD match the one specified in `cloud.provider`. + """ + + AWS_ECS_CONTAINER_ARN = "aws.ecs.container.arn" + """ + The Amazon Resource Name (ARN) of an [ECS container instance](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_instances.html). + """ + + AWS_ECS_CLUSTER_ARN = "aws.ecs.cluster.arn" + """ + The ARN of an [ECS cluster](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/clusters.html). + """ + + AWS_ECS_LAUNCHTYPE = "aws.ecs.launchtype" + """ + The [launch type](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html) for an ECS task. + """ + + AWS_ECS_TASK_ARN = "aws.ecs.task.arn" + """ + The ARN of an [ECS task definition](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html). + """ + + AWS_ECS_TASK_FAMILY = "aws.ecs.task.family" + """ + The task definition family this task definition is a member of. + """ + + AWS_EKS_CLUSTER_ARN = "aws.eks.cluster.arn" + """ + The ARN of an EKS cluster. + """ + + AWS_LOG_GROUP_NAMES = "aws.log.group.names" + """ + The name(s) of the AWS log group(s) an application is writing to. + Note: Multiple log groups must be supported for cases like multi-container applications, where a single application has sidecar containers, and each write to their own log group. + """ + + AWS_LOG_GROUP_ARNS = "aws.log.group.arns" + """ + The Amazon Resource Name(s) (ARN) of the AWS log group(s). + Note: See the [log group ARN format documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html#CWL_ARN_Format). + """ + + AWS_LOG_STREAM_NAMES = "aws.log.stream.names" + """ + The name(s) of the AWS log stream(s) an application is writing to. + """ + + AWS_LOG_STREAM_ARNS = "aws.log.stream.arns" + """ + The ARN(s) of the AWS log stream(s). + Note: See the [log stream ARN format documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html#CWL_ARN_Format). One log group can contain several log streams, so these ARNs necessarily identify both a log group and a log stream. + """ + + CONTAINER_NAME = "container.name" + """ + Container name. + """ + + CONTAINER_ID = "container.id" + """ + Container ID. Usually a UUID, as for example used to [identify Docker containers](https://docs.docker.com/engine/reference/run/#container-identification). The UUID might be abbreviated. + """ + + CONTAINER_RUNTIME = "container.runtime" + """ + The container runtime managing this container. + """ + + CONTAINER_IMAGE_NAME = "container.image.name" + """ + Name of the image the container was built on. + """ + + CONTAINER_IMAGE_TAG = "container.image.tag" + """ + Container image tag. + """ + + DEPLOYMENT_ENVIRONMENT = "deployment.environment" + """ + Name of the [deployment environment](https://en.wikipedia.org/wiki/Deployment_environment) (aka deployment tier). + """ + + FAAS_NAME = "faas.name" + """ + The name of the function being executed. + """ + + FAAS_ID = "faas.id" + """ + The unique ID of the function being executed. + Note: For example, in AWS Lambda this field corresponds to the [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) value, in GCP to the URI of the resource, and in Azure to the [FunctionDirectory](https://github.com/Azure/azure-functions-host/wiki/Retrieving-information-about-the-currently-running-function) field. + """ + + FAAS_VERSION = "faas.version" + """ + The version string of the function being executed as defined in [Version Attributes](../../resource/semantic_conventions/README.md#version-attributes). + """ + + FAAS_INSTANCE = "faas.instance" + """ + The execution environment ID as a string. + """ + + FAAS_MAX_MEMORY = "faas.max_memory" + """ + The amount of memory available to the serverless function in MiB. + Note: It's recommended to set this attribute since e.g. too little memory can easily stop a Java AWS Lambda function from working correctly. On AWS Lambda, the environment variable `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` provides this information. + """ + + HOST_ID = "host.id" + """ + Unique host ID. For Cloud, this must be the instance_id assigned by the cloud provider. + """ + + HOST_NAME = "host.name" + """ + Name of the host. On Unix systems, it may contain what the hostname command returns, or the fully qualified hostname, or another name specified by the user. + """ + + HOST_TYPE = "host.type" + """ + Type of host. For Cloud, this must be the machine type. + """ + + HOST_ARCH = "host.arch" + """ + The CPU architecture the host system is running on. + """ + + HOST_IMAGE_NAME = "host.image.name" + """ + Name of the VM image or OS install the host was instantiated from. + """ + + HOST_IMAGE_ID = "host.image.id" + """ + VM image ID. For Cloud, this value is from the provider. + """ + + HOST_IMAGE_VERSION = "host.image.version" + """ + The version string of the VM image as defined in [Version Attributes](README.md#version-attributes). + """ + + K8S_CLUSTER_NAME = "k8s.cluster.name" + """ + The name of the cluster. + """ + + K8S_NODE_NAME = "k8s.node.name" + """ + The name of the Node. + """ + + K8S_NODE_UID = "k8s.node.uid" + """ + The UID of the Node. + """ + + K8S_NAMESPACE_NAME = "k8s.namespace.name" + """ + The name of the namespace that the pod is running in. + """ + + K8S_POD_UID = "k8s.pod.uid" + """ + The UID of the Pod. + """ + + K8S_POD_NAME = "k8s.pod.name" + """ + The name of the Pod. + """ + + K8S_CONTAINER_NAME = "k8s.container.name" + """ + The name of the Container in a Pod template. + """ + + K8S_REPLICASET_UID = "k8s.replicaset.uid" + """ + The UID of the ReplicaSet. + """ + + K8S_REPLICASET_NAME = "k8s.replicaset.name" + """ + The name of the ReplicaSet. + """ + + K8S_DEPLOYMENT_UID = "k8s.deployment.uid" + """ + The UID of the Deployment. + """ + + K8S_DEPLOYMENT_NAME = "k8s.deployment.name" + """ + The name of the Deployment. + """ + + K8S_STATEFULSET_UID = "k8s.statefulset.uid" + """ + The UID of the StatefulSet. + """ + + K8S_STATEFULSET_NAME = "k8s.statefulset.name" + """ + The name of the StatefulSet. + """ + + K8S_DAEMONSET_UID = "k8s.daemonset.uid" + """ + The UID of the DaemonSet. + """ + + K8S_DAEMONSET_NAME = "k8s.daemonset.name" + """ + The name of the DaemonSet. + """ + + K8S_JOB_UID = "k8s.job.uid" + """ + The UID of the Job. + """ + + K8S_JOB_NAME = "k8s.job.name" + """ + The name of the Job. + """ + + K8S_CRONJOB_UID = "k8s.cronjob.uid" + """ + The UID of the CronJob. + """ + + K8S_CRONJOB_NAME = "k8s.cronjob.name" + """ + The name of the CronJob. + """ + + OS_TYPE = "os.type" + """ + The operating system type. + """ + + OS_DESCRIPTION = "os.description" + """ + Human readable (not intended to be parsed) OS version information, like e.g. reported by `ver` or `lsb_release -a` commands. + """ + + PROCESS_PID = "process.pid" + """ + Process identifier (PID). + """ + + PROCESS_EXECUTABLE_NAME = "process.executable.name" + """ + The name of the process executable. On Linux based systems, can be set to the `Name` in `proc/[pid]/status`. On Windows, can be set to the base name of `GetProcessImageFileNameW`. + """ + + PROCESS_EXECUTABLE_PATH = "process.executable.path" + """ + The full path to the process executable. On Linux based systems, can be set to the target of `proc/[pid]/exe`. On Windows, can be set to the result of `GetProcessImageFileNameW`. + """ + + PROCESS_COMMAND = "process.command" + """ + The command used to launch the process (i.e. the command name). On Linux based systems, can be set to the zeroth string in `proc/[pid]/cmdline`. On Windows, can be set to the first parameter extracted from `GetCommandLineW`. + """ + + PROCESS_COMMAND_LINE = "process.command_line" + """ + The full command used to launch the process as a single string representing the full command. On Windows, can be set to the result of `GetCommandLineW`. Do not set this if you have to assemble it just for monitoring; use `process.command_args` instead. + """ + + PROCESS_COMMAND_ARGS = "process.command_args" + """ + All the command arguments (including the command/executable itself) as received by the process. On Linux-based systems (and some other Unixoid systems supporting procfs), can be set according to the list of null-delimited strings extracted from `proc/[pid]/cmdline`. For libc-based executables, this would be the full argv vector passed to `main`. + """ + + PROCESS_OWNER = "process.owner" + """ + The username of the user that owns the process. + """ + + PROCESS_RUNTIME_NAME = "process.runtime.name" + """ + The name of the runtime of this process. For compiled native binaries, this SHOULD be the name of the compiler. + """ + + PROCESS_RUNTIME_VERSION = "process.runtime.version" + """ + The version of the runtime of this process, as returned by the runtime without modification. + """ + + PROCESS_RUNTIME_DESCRIPTION = "process.runtime.description" + """ + An additional description about the runtime of the process, for example a specific vendor customization of the runtime environment. + """ + + SERVICE_NAME = "service.name" + """ + Logical name of the service. + Note: MUST be the same for all instances of horizontally scaled services. If the value was not specified, SDKs MUST fallback to `unknown_service:` concatenated with [`process.executable.name`](process.md#process), e.g. `unknown_service:bash`. If `process.executable.name` is not available, the value MUST be set to `unknown_service`. + """ + + SERVICE_NAMESPACE = "service.namespace" + """ + A namespace for `service.name`. + Note: A string value having a meaning that helps to distinguish a group of services, for example the team name that owns a group of services. `service.name` is expected to be unique within the same namespace. If `service.namespace` is not specified in the Resource then `service.name` is expected to be unique for all services that have no explicit namespace defined (so the empty/unspecified namespace is simply one more valid namespace). Zero-length namespace string is assumed equal to unspecified namespace. + """ + + SERVICE_INSTANCE_ID = "service.instance.id" + """ + The string ID of the service instance. + Note: MUST be unique for each instance of the same `service.namespace,service.name` pair (in other words `service.namespace,service.name,service.instance.id` triplet MUST be globally unique). The ID helps to distinguish instances of the same service that exist at the same time (e.g. instances of a horizontally scaled service). It is preferable for the ID to be persistent and stay the same for the lifetime of the service instance, however it is acceptable that the ID is ephemeral and changes during important lifetime events for the service (e.g. service restarts). If the service has no inherent unique ID that can be used as the value of this attribute it is recommended to generate a random Version 1 or Version 4 RFC 4122 UUID (services aiming for reproducible UUIDs may also use Version 5, see RFC 4122 for more recommendations). + """ + + SERVICE_VERSION = "service.version" + """ + The version string of the service API or implementation. + """ + + TELEMETRY_SDK_NAME = "telemetry.sdk.name" + """ + The name of the telemetry SDK as defined above. + """ + + TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language" + """ + The language of the telemetry SDK. + """ + + TELEMETRY_SDK_VERSION = "telemetry.sdk.version" + """ + The version string of the telemetry SDK. + """ + + TELEMETRY_AUTO_VERSION = "telemetry.auto.version" + """ + The version string of the auto instrumentation agent, if used. + """ + + +class CloudProviderValues(Enum): + AWS = "aws" + """Amazon Web Services.""" + + AZURE = "azure" + """Microsoft Azure.""" + + GCP = "gcp" + """Google Cloud Platform.""" + + +class CloudInfrastructureServiceValues(Enum): + AWS_EC2 = "aws_ec2" + """AWS Elastic Compute Cloud.""" + + AWS_ECS = "aws_ecs" + """AWS Elastic Container Service.""" + + AWS_EKS = "aws_eks" + """AWS Elastic Kubernetes Service.""" + + AWS_LAMBDA = "aws_lambda" + """AWS Lambda.""" + + AWS_ELASTICBEANSTALK = "aws_elastic_beanstalk" + """AWS Elastic Beanstalk.""" + + AZURE_VM = "azure_vm" + """Azure Virtual Machines.""" + + AZURE_CONTAINERINSTANCES = "azure_container_instances" + """Azure Container Instances.""" + + AZURE_AKS = "azure_aks" + """Azure Kubernetes Service.""" + + AZURE_FUNCTIONS = "azure_functions" + """Azure Functions.""" + + AZURE_APPSERVICE = "azure_app_service" + """Azure App Service.""" + + GCP_COMPUTEENGINE = "gcp_compute_engine" + """Google Cloud Compute Engine (GCE).""" + + GCP_CLOUDRUN = "gcp_cloud_run" + """Google Cloud Run.""" + + GCP_KUBERNETESENGINE = "gcp_kubernetes_engine" + """Google Cloud Kubernetes Engine (GKE).""" + + GCP_CLOUDFUNCTIONS = "gcp_cloud_functions" + """Google Cloud Functions (GCF).""" + + GCP_APPENGINE = "gcp_app_engine" + """Google Cloud App Engine (GAE).""" + + +class AwsEcsLaunchtypeValues(Enum): + EC2 = "ec2" + """ec2.""" + + FARGATE = "fargate" + """fargate.""" + + +class HostArchValues(Enum): + AMD64 = "amd64" + """AMD64.""" + + ARM32 = "arm32" + """ARM32.""" + + ARM64 = "arm64" + """ARM64.""" + + IA64 = "ia64" + """Itanium.""" + + PPC32 = "ppc32" + """32-bit PowerPC.""" + + PPC64 = "ppc64" + """64-bit PowerPC.""" + + X86 = "x86" + """32-bit x86.""" + + +class OsTypeValues(Enum): + WINDOWS = "WINDOWS" + """Microsoft Windows.""" + + LINUX = "LINUX" + """Linux.""" + + DARWIN = "DARWIN" + """Apple Darwin.""" + + FREEBSD = "FREEBSD" + """FreeBSD.""" + + NETBSD = "NETBSD" + """NetBSD.""" + + OPENBSD = "OPENBSD" + """OpenBSD.""" + + DRAGONFLYBSD = "DRAGONFLYBSD" + """DragonFly BSD.""" + + HPUX = "HPUX" + """HP-UX (Hewlett Packard Unix).""" + + AIX = "AIX" + """AIX (Advanced Interactive eXecutive).""" + + SOLARIS = "SOLARIS" + """Oracle Solaris.""" + + ZOS = "ZOS" + """IBM z/OS.""" + + +class TelemetrySdkLanguageValues(Enum): + CPP = "cpp" + """cpp.""" + + DOTNET = "dotnet" + """dotnet.""" + + ERLANG = "erlang" + """erlang.""" + + GO = "go" + """go.""" + + JAVA = "java" + """java.""" + + NODEJS = "nodejs" + """nodejs.""" + + PHP = "php" + """php.""" + + PYTHON = "python" + """python.""" + + RUBY = "ruby" + """ruby.""" + + WEBJS = "webjs" + """webjs.""" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py new file mode 100644 index 0000000000..02b8768f16 --- /dev/null +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py @@ -0,0 +1,822 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum + + +class SpanAttributes: + DB_SYSTEM = "db.system" + """ + An identifier for the database management system (DBMS) product being used. See below for a list of well-known identifiers. + """ + + DB_CONNECTION_STRING = "db.connection_string" + """ + The connection string used to connect to the database. It is recommended to remove embedded credentials. + """ + + DB_USER = "db.user" + """ + Username for accessing the database. + """ + + DB_JDBC_DRIVER_CLASSNAME = "db.jdbc.driver_classname" + """ + The fully-qualified class name of the [Java Database Connectivity (JDBC)](https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/) driver used to connect. + """ + + DB_NAME = "db.name" + """ + If no [tech-specific attribute](#call-level-attributes-for-specific-technologies) is defined, this attribute is used to report the name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). + Note: In some SQL databases, the database name to be used is called "schema name". + """ + + DB_STATEMENT = "db.statement" + """ + The database statement being executed. + Note: The value may be sanitized to exclude sensitive information. + """ + + DB_OPERATION = "db.operation" + """ + The name of the operation being executed, e.g. the [MongoDB command name](https://docs.mongodb.com/manual/reference/command/#database-operations) such as `findAndModify`, or the SQL keyword. + Note: When setting this to an SQL keyword, it is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if the operation name is provided by the library being instrumented. If the SQL statement has an ambiguous operation, or performs more than one operation, this value may be omitted. + """ + + NET_PEER_NAME = "net.peer.name" + """ + Remote hostname or similar, see note below. + """ + + NET_PEER_IP = "net.peer.ip" + """ + Remote address of the peer (dotted decimal for IPv4 or [RFC5952](https://tools.ietf.org/html/rfc5952) for IPv6). + """ + + NET_PEER_PORT = "net.peer.port" + """ + Remote port number. + """ + + NET_TRANSPORT = "net.transport" + """ + Transport protocol used. See note below. + """ + + DB_MSSQL_INSTANCE_NAME = "db.mssql.instance_name" + """ + The Microsoft SQL Server [instance name](https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15) connecting to. This name is used to determine the port of a named instance. + Note: If setting a `db.mssql.instance_name`, `net.peer.port` is no longer required (but still recommended if non-standard). + """ + + DB_CASSANDRA_KEYSPACE = "db.cassandra.keyspace" + """ + The name of the keyspace being accessed. To be used instead of the generic `db.name` attribute. + """ + + DB_CASSANDRA_PAGE_SIZE = "db.cassandra.page_size" + """ + The fetch size used for paging, i.e. how many rows will be returned at once. + """ + + DB_CASSANDRA_CONSISTENCY_LEVEL = "db.cassandra.consistency_level" + """ + The consistency level of the query. Based on consistency values from [CQL](https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/dml/dmlConfigConsistency.html). + """ + + DB_CASSANDRA_TABLE = "db.cassandra.table" + """ + The name of the primary table that the operation is acting upon, including the schema name (if applicable). + Note: This mirrors the db.sql.table attribute but references cassandra rather than sql. It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. + """ + + DB_CASSANDRA_IDEMPOTENCE = "db.cassandra.idempotence" + """ + Whether or not the query is idempotent. + """ + + DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT = ( + "db.cassandra.speculative_execution_count" + ) + """ + The number of times a query was speculatively executed. Not set or `0` if the query was not executed speculatively. + """ + + DB_CASSANDRA_COORDINATOR_ID = "db.cassandra.coordinator.id" + """ + The ID of the coordinating node for a query. + """ + + DB_CASSANDRA_COORDINATOR_DC = "db.cassandra.coordinator.dc" + """ + The data center of the coordinating node for a query. + """ + + DB_HBASE_NAMESPACE = "db.hbase.namespace" + """ + The [HBase namespace](https://hbase.apache.org/book.html#_namespace) being accessed. To be used instead of the generic `db.name` attribute. + """ + + DB_REDIS_DATABASE_INDEX = "db.redis.database_index" + """ + The index of the database being accessed as used in the [`SELECT` command](https://redis.io/commands/select), provided as an integer. To be used instead of the generic `db.name` attribute. + """ + + DB_MONGODB_COLLECTION = "db.mongodb.collection" + """ + The collection being accessed within the database stated in `db.name`. + """ + + DB_SQL_TABLE = "db.sql.table" + """ + The name of the primary table that the operation is acting upon, including the schema name (if applicable). + Note: It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. + """ + + EXCEPTION_TYPE = "exception.type" + """ + The type of the exception (its fully-qualified class name, if applicable). The dynamic type of the exception should be preferred over the static type in languages that support it. + """ + + EXCEPTION_MESSAGE = "exception.message" + """ + The exception message. + """ + + EXCEPTION_STACKTRACE = "exception.stacktrace" + """ + A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG. + """ + + EXCEPTION_ESCAPED = "exception.escaped" + """ + SHOULD be set to true if the exception event is recorded at a point where it is known that the exception is escaping the scope of the span. + Note: An exception is considered to have escaped (or left) the scope of a span, +if that span is ended while the exception is still logically "in flight". +This may be actually "in flight" in some languages (e.g. if the exception +is passed to a Context manager's `__exit__` method in Python) but will +usually be caught at the point of recording the exception in most languages. + +It is usually not possible to determine at the point where an exception is thrown +whether it will escape the scope of a span. +However, it is trivial to know that an exception +will escape, if one checks for an active exception just before ending the span, +as done in the [example above](#exception-end-example). + +It follows that an exception may still escape the scope of the span +even if the `exception.escaped` attribute was not set or set to false, +since the event might have been recorded at a time where it was not +clear whether the exception will escape. + """ + + FAAS_TRIGGER = "faas.trigger" + """ + Type of the trigger on which the function is executed. + """ + + FAAS_EXECUTION = "faas.execution" + """ + The execution ID of the current function execution. + """ + + FAAS_DOCUMENT_COLLECTION = "faas.document.collection" + """ + The name of the source on which the triggering operation was performed. For example, in Cloud Storage or S3 corresponds to the bucket name, and in Cosmos DB to the database name. + """ + + FAAS_DOCUMENT_OPERATION = "faas.document.operation" + """ + Describes the type of the operation that was performed on the data. + """ + + FAAS_DOCUMENT_TIME = "faas.document.time" + """ + A string containing the time when the data was accessed in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). + """ + + FAAS_DOCUMENT_NAME = "faas.document.name" + """ + The document name/table subjected to the operation. For example, in Cloud Storage or S3 is the name of the file, and in Cosmos DB the table name. + """ + + HTTP_METHOD = "http.method" + """ + HTTP request method. + """ + + HTTP_URL = "http.url" + """ + Full HTTP request URL in the form `scheme://host[:port]/path?query[#fragment]`. Usually the fragment is not transmitted over HTTP, but if it is known, it should be included nevertheless. + Note: `http.url` MUST NOT contain credentials passed via URL in form of `https://username:password@www.example.com/`. In such case the attribute's value should be `https://www.example.com/`. + """ + + HTTP_TARGET = "http.target" + """ + The full request target as passed in a HTTP request line or equivalent. + """ + + HTTP_HOST = "http.host" + """ + The value of the [HTTP host header](https://tools.ietf.org/html/rfc7230#section-5.4). When the header is empty or not present, this attribute should be the same. + """ + + HTTP_SCHEME = "http.scheme" + """ + The URI scheme identifying the used protocol. + """ + + HTTP_STATUS_CODE = "http.status_code" + """ + [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). + """ + + HTTP_FLAVOR = "http.flavor" + """ + Kind of HTTP protocol used. + Note: If `net.transport` is not specified, it can be assumed to be `IP.TCP` except if `http.flavor` is `QUIC`, in which case `IP.UDP` is assumed. + """ + + HTTP_USER_AGENT = "http.user_agent" + """ + Value of the [HTTP User-Agent](https://tools.ietf.org/html/rfc7231#section-5.5.3) header sent by the client. + """ + + HTTP_REQUEST_CONTENT_LENGTH = "http.request_content_length" + """ + The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://tools.ietf.org/html/rfc7230#section-3.3.2) header. For requests using transport encoding, this should be the compressed size. + """ + + HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED = ( + "http.request_content_length_uncompressed" + ) + """ + The size of the uncompressed request payload body after transport decoding. Not set if transport encoding not used. + """ + + HTTP_RESPONSE_CONTENT_LENGTH = "http.response_content_length" + """ + The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://tools.ietf.org/html/rfc7230#section-3.3.2) header. For requests using transport encoding, this should be the compressed size. + """ + + HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED = ( + "http.response_content_length_uncompressed" + ) + """ + The size of the uncompressed response payload body after transport decoding. Not set if transport encoding not used. + """ + + HTTP_SERVER_NAME = "http.server_name" + """ + The primary server name of the matched virtual host. This should be obtained via configuration. If no such configuration can be obtained, this attribute MUST NOT be set ( `net.host.name` should be used instead). + Note: `http.url` is usually not readily available on the server side but would have to be assembled in a cumbersome and sometimes lossy process from other information (see e.g. open-telemetry/opentelemetry-python/pull/148). It is thus preferred to supply the raw data that is available. + """ + + HTTP_ROUTE = "http.route" + """ + The matched route (path template). + """ + + HTTP_CLIENT_IP = "http.client_ip" + """ + The IP address of the original client behind all proxies, if known (e.g. from [X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)). + Note: This is not necessarily the same as `net.peer.ip`, which would identify the network-level peer, which may be a proxy. + """ + + NET_HOST_IP = "net.host.ip" + """ + Like `net.peer.ip` but for the host IP. Useful in case of a multi-IP host. + """ + + NET_HOST_PORT = "net.host.port" + """ + Like `net.peer.port` but for the host port. + """ + + NET_HOST_NAME = "net.host.name" + """ + Local hostname or similar, see note below. + """ + + MESSAGING_SYSTEM = "messaging.system" + """ + A string identifying the messaging system. + """ + + MESSAGING_DESTINATION = "messaging.destination" + """ + The message destination name. This might be equal to the span name but is required nevertheless. + """ + + MESSAGING_DESTINATION_KIND = "messaging.destination_kind" + """ + The kind of message destination. + """ + + MESSAGING_TEMP_DESTINATION = "messaging.temp_destination" + """ + A boolean that is true if the message destination is temporary. + """ + + MESSAGING_PROTOCOL = "messaging.protocol" + """ + The name of the transport protocol. + """ + + MESSAGING_PROTOCOL_VERSION = "messaging.protocol_version" + """ + The version of the transport protocol. + """ + + MESSAGING_URL = "messaging.url" + """ + Connection string. + """ + + MESSAGING_MESSAGE_ID = "messaging.message_id" + """ + A value used by the messaging system as an identifier for the message, represented as a string. + """ + + MESSAGING_CONVERSATION_ID = "messaging.conversation_id" + """ + The [conversation ID](#conversations) identifying the conversation to which the message belongs, represented as a string. Sometimes called "Correlation ID". + """ + + MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES = ( + "messaging.message_payload_size_bytes" + ) + """ + The (uncompressed) size of the message payload in bytes. Also use this attribute if it is unknown whether the compressed or uncompressed payload size is reported. + """ + + MESSAGING_MESSAGE_PAYLOAD_COMPRESSED_SIZE_BYTES = ( + "messaging.message_payload_compressed_size_bytes" + ) + """ + The compressed size of the message payload in bytes. + """ + + FAAS_TIME = "faas.time" + """ + A string containing the function invocation time in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). + """ + + FAAS_CRON = "faas.cron" + """ + A string containing the schedule period as [Cron Expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm). + """ + + FAAS_COLDSTART = "faas.coldstart" + """ + A boolean that is true if the serverless function is executed for the first time (aka cold-start). + """ + + FAAS_INVOKED_NAME = "faas.invoked_name" + """ + The name of the invoked function. + Note: SHOULD be equal to the `faas.name` resource attribute of the invoked function. + """ + + FAAS_INVOKED_PROVIDER = "faas.invoked_provider" + """ + The cloud provider of the invoked function. + Note: SHOULD be equal to the `cloud.provider` resource attribute of the invoked function. + """ + + FAAS_INVOKED_REGION = "faas.invoked_region" + """ + The cloud region of the invoked function. + Note: SHOULD be equal to the `cloud.region` resource attribute of the invoked function. + """ + + PEER_SERVICE = "peer.service" + """ + The [`service.name`](../../resource/semantic_conventions/README.md#service) of the remote service. SHOULD be equal to the actual `service.name` resource attribute of the remote service if any. + """ + + ENDUSER_ID = "enduser.id" + """ + Username or client_id extracted from the access token or [Authorization](https://tools.ietf.org/html/rfc7235#section-4.2) header in the inbound request from outside the system. + """ + + ENDUSER_ROLE = "enduser.role" + """ + Actual/assumed role the client is making the request under extracted from token or application security context. + """ + + ENDUSER_SCOPE = "enduser.scope" + """ + Scopes or granted authorities the client currently possesses extracted from token or application security context. The value would come from the scope associated with an [OAuth 2.0 Access Token](https://tools.ietf.org/html/rfc6749#section-3.3) or an attribute value in a [SAML 2.0 Assertion](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html). + """ + + THREAD_ID = "thread.id" + """ + Current "managed" thread ID (as opposed to OS thread ID). + """ + + THREAD_NAME = "thread.name" + """ + Current thread name. + """ + + CODE_FUNCTION = "code.function" + """ + The method or function name, or equivalent (usually rightmost part of the code unit's name). + """ + + CODE_NAMESPACE = "code.namespace" + """ + The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. + """ + + CODE_FILEPATH = "code.filepath" + """ + The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). + """ + + CODE_LINENO = "code.lineno" + """ + The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. + """ + + MESSAGING_OPERATION = "messaging.operation" + """ + A string identifying the kind of message consumption as defined in the [Operation names](#operation-names) section above. If the operation is "send", this attribute MUST NOT be set, since the operation can be inferred from the span kind in that case. + """ + + MESSAGING_KAFKA_MESSAGE_KEY = "messaging.kafka.message_key" + """ + Message keys in Kafka are used for grouping alike messages to ensure they're processed on the same partition. They differ from `messaging.message_id` in that they're not unique. If the key is `null`, the attribute MUST NOT be set. + Note: If the key type is not string, it's string representation has to be supplied for the attribute. If the key has no unambiguous, canonical string form, don't include its value. + """ + + MESSAGING_KAFKA_CONSUMER_GROUP = "messaging.kafka.consumer_group" + """ + Name of the Kafka Consumer Group that is handling the message. Only applies to consumers, not producers. + """ + + MESSAGING_KAFKA_CLIENT_ID = "messaging.kafka.client_id" + """ + Client Id for the Consumer or Producer that is handling the message. + """ + + MESSAGING_KAFKA_PARTITION = "messaging.kafka.partition" + """ + Partition the message is sent to. + """ + + MESSAGING_KAFKA_TOMBSTONE = "messaging.kafka.tombstone" + """ + A boolean that is true if the message is a tombstone. + """ + + RPC_SYSTEM = "rpc.system" + """ + A string identifying the remoting system. + """ + + RPC_SERVICE = "rpc.service" + """ + The full name of the service being called, including its package name, if applicable. + """ + + RPC_METHOD = "rpc.method" + """ + The name of the method being called, must be equal to the $method part in the span name. + """ + + RPC_GRPC_STATUS_CODE = "rpc.grpc.status_code" + """ + The [numeric status code](https://github.com/grpc/grpc/blob/v1.33.2/doc/statuscodes.md) of the gRPC request. + """ + + +class DbSystemValues(Enum): + OTHER_SQL = "other_sql" + """Some other SQL database. Fallback only. See notes.""" + + MSSQL = "mssql" + """Microsoft SQL Server.""" + + MYSQL = "mysql" + """MySQL.""" + + ORACLE = "oracle" + """Oracle Database.""" + + DB2 = "db2" + """IBM Db2.""" + + POSTGRESQL = "postgresql" + """PostgreSQL.""" + + REDSHIFT = "redshift" + """Amazon Redshift.""" + + HIVE = "hive" + """Apache Hive.""" + + CLOUDSCAPE = "cloudscape" + """Cloudscape.""" + + HSQLDB = "hsqldb" + """HyperSQL DataBase.""" + + PROGRESS = "progress" + """Progress Database.""" + + MAXDB = "maxdb" + """SAP MaxDB.""" + + HANADB = "hanadb" + """SAP HANA.""" + + INGRES = "ingres" + """Ingres.""" + + FIRSTSQL = "firstsql" + """FirstSQL.""" + + EDB = "edb" + """EnterpriseDB.""" + + CACHE = "cache" + """InterSystems Caché.""" + + ADABAS = "adabas" + """Adabas (Adaptable Database System).""" + + FIREBIRD = "firebird" + """Firebird.""" + + DERBY = "derby" + """Apache Derby.""" + + FILEMAKER = "filemaker" + """FileMaker.""" + + INFORMIX = "informix" + """Informix.""" + + INSTANTDB = "instantdb" + """InstantDB.""" + + INTERBASE = "interbase" + """InterBase.""" + + MARIADB = "mariadb" + """MariaDB.""" + + NETEZZA = "netezza" + """Netezza.""" + + PERVASIVE = "pervasive" + """Pervasive PSQL.""" + + POINTBASE = "pointbase" + """PointBase.""" + + SQLITE = "sqlite" + """SQLite.""" + + SYBASE = "sybase" + """Sybase.""" + + TERADATA = "teradata" + """Teradata.""" + + VERTICA = "vertica" + """Vertica.""" + + H2 = "h2" + """H2.""" + + COLDFUSION = "coldfusion" + """ColdFusion IMQ.""" + + CASSANDRA = "cassandra" + """Apache Cassandra.""" + + HBASE = "hbase" + """Apache HBase.""" + + MONGODB = "mongodb" + """MongoDB.""" + + REDIS = "redis" + """Redis.""" + + COUCHBASE = "couchbase" + """Couchbase.""" + + COUCHDB = "couchdb" + """CouchDB.""" + + COSMOSDB = "cosmosdb" + """Microsoft Azure Cosmos DB.""" + + DYNAMODB = "dynamodb" + """Amazon DynamoDB.""" + + NEO4J = "neo4j" + """Neo4j.""" + + GEODE = "geode" + """Apache Geode.""" + + ELASTICSEARCH = "elasticsearch" + """Elasticsearch.""" + + +class NetTransportValues(Enum): + IP_TCP = "IP.TCP" + """IP.TCP.""" + + IP_UDP = "IP.UDP" + """IP.UDP.""" + + IP = "IP" + """Another IP-based protocol.""" + + UNIX = "Unix" + """Unix Domain socket. See below.""" + + PIPE = "pipe" + """Named or anonymous pipe. See note below.""" + + INPROC = "inproc" + """In-process communication.""" + + OTHER = "other" + """Something else (non IP-based).""" + + +class DbCassandraConsistencyLevelValues(Enum): + ALL = "ALL" + """ALL.""" + + EACH_QUORUM = "EACH_QUORUM" + """EACH_QUORUM.""" + + QUORUM = "QUORUM" + """QUORUM.""" + + LOCAL_QUORUM = "LOCAL_QUORUM" + """LOCAL_QUORUM.""" + + ONE = "ONE" + """ONE.""" + + TWO = "TWO" + """TWO.""" + + THREE = "THREE" + """THREE.""" + + LOCAL_ONE = "LOCAL_ONE" + """LOCAL_ONE.""" + + ANY = "ANY" + """ANY.""" + + SERIAL = "SERIAL" + """SERIAL.""" + + LOCAL_SERIAL = "LOCAL_SERIAL" + """LOCAL_SERIAL.""" + + +class FaasTriggerValues(Enum): + DATASOURCE = "datasource" + """A response to some data source operation such as a database or filesystem read/write.""" + + HTTP = "http" + """To provide an answer to an inbound HTTP request.""" + + PUBSUB = "pubsub" + """A function is set to be executed when messages are sent to a messaging system.""" + + TIMER = "timer" + """A function is scheduled to be executed regularly.""" + + OTHER = "other" + """If none of the others apply.""" + + +class FaasDocumentOperationValues(Enum): + INSERT = "insert" + """When a new object is created.""" + + EDIT = "edit" + """When an object is modified.""" + + DELETE = "delete" + """When an object is deleted.""" + + +class HttpFlavorValues(Enum): + HTTP_1_0 = "1.0" + """HTTP 1.0.""" + + HTTP_1_1 = "1.1" + """HTTP 1.1.""" + + HTTP_2_0 = "2.0" + """HTTP 2.""" + + SPDY = "SPDY" + """SPDY protocol.""" + + QUIC = "QUIC" + """QUIC protocol.""" + + +class MessagingDestinationKindValues(Enum): + QUEUE = "queue" + """A message sent to a queue.""" + + TOPIC = "topic" + """A message sent to a topic.""" + + +class FaasInvokedProviderValues(Enum): + AWS = "aws" + """Amazon Web Services.""" + + AZURE = "azure" + """Amazon Web Services.""" + + GCP = "gcp" + """Google Cloud Platform.""" + + +class MessagingOperationValues(Enum): + RECEIVE = "receive" + """receive.""" + + PROCESS = "process" + """process.""" + + +class RpcGrpcStatusCodeValues(Enum): + OK = 0 + """OK.""" + + CANCELLED = 1 + """CANCELLED.""" + + UNKNOWN = 2 + """UNKNOWN.""" + + INVALID_ARGUMENT = 3 + """INVALID_ARGUMENT.""" + + DEADLINE_EXCEEDED = 4 + """DEADLINE_EXCEEDED.""" + + NOT_FOUND = 5 + """NOT_FOUND.""" + + ALREADY_EXISTS = 6 + """ALREADY_EXISTS.""" + + PERMISSION_DENIED = 7 + """PERMISSION_DENIED.""" + + RESOURCE_EXHAUSTED = 8 + """RESOURCE_EXHAUSTED.""" + + FAILED_PRECONDITION = 9 + """FAILED_PRECONDITION.""" + + ABORTED = 10 + """ABORTED.""" + + OUT_OF_RANGE = 11 + """OUT_OF_RANGE.""" + + UNIMPLEMENTED = 12 + """UNIMPLEMENTED.""" + + INTERNAL = 13 + """INTERNAL.""" + + UNAVAILABLE = 14 + """UNAVAILABLE.""" + + DATA_LOSS = 15 + """DATA_LOSS.""" + + UNAUTHENTICATED = 16 + """UNAUTHENTICATED.""" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py new file mode 100644 index 0000000000..3b16be5d22 --- /dev/null +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.20.dev0" diff --git a/opentelemetry-semantic-conventions/tests/__init__.py b/opentelemetry-semantic-conventions/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-semantic-conventions/tests/test_semconv.py b/opentelemetry-semantic-conventions/tests/test_semconv.py new file mode 100644 index 0000000000..f8e827145a --- /dev/null +++ b/opentelemetry-semantic-conventions/tests/test_semconv.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from unittest import TestCase + +from pkg_resources import DistributionNotFound, require + + +class TestSemanticConventions(TestCase): + def test_semantic_conventions(self): + + try: + require(["opentelemetry-semantic-conventions"]) + except DistributionNotFound: + self.fail("opentelemetry-semantic-conventions not installed") diff --git a/scripts/build.sh b/scripts/build.sh index 53a9fa6eef..2f40f1a003 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ opentelemetry-distro/ exporter/*/ shim/*/ propagator/*/; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ opentelemetry-distro/ opentelemetry-semantic-conventions/ exporter/*/ shim/*/ propagator/*/; do ( echo "building $d" cd "$d" diff --git a/scripts/semconv/.gitignore b/scripts/semconv/.gitignore new file mode 100644 index 0000000000..ed7b836bb6 --- /dev/null +++ b/scripts/semconv/.gitignore @@ -0,0 +1 @@ +opentelemetry-specification \ No newline at end of file diff --git a/scripts/semconv/generate.sh b/scripts/semconv/generate.sh new file mode 100644 index 0000000000..62c28ede5c --- /dev/null +++ b/scripts/semconv/generate.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +ROOT_DIR="${SCRIPT_DIR}/../../" + +# freeze the spec version to make SemanticAttributes generation reproducible +SPEC_VERSION=v1.1.0 + +cd ${SCRIPT_DIR} + +rm -rf opentelemetry-specification || true +mkdir opentelemetry-specification +cd opentelemetry-specification + +git init +git remote add origin https://github.com/open-telemetry/opentelemetry-specification.git +git fetch origin "$SPEC_VERSION" +git reset --hard FETCH_HEAD +cd ${SCRIPT_DIR} + +docker run --rm \ + -v ${SCRIPT_DIR}/opentelemetry-specification/semantic_conventions/trace:/source \ + -v ${SCRIPT_DIR}/templates:/templates \ + -v ${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/:/output \ + otel/semconvgen:0.2.1 \ + -f /source code \ + --template /templates/semantic_attributes.j2 \ + --output /output/__init__.py \ + -Dclass=SpanAttributes + +docker run --rm \ + -v ${SCRIPT_DIR}/opentelemetry-specification/semantic_conventions/resource:/source \ + -v ${SCRIPT_DIR}/templates:/templates \ + -v ${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resources/:/output \ + otel/semconvgen:0.2.1 \ + -f /source code \ + --template /templates/semantic_attributes.j2 \ + --output /output/__init__.py \ + -Dclass=ResourceAttributes + +cd "$ROOT_DIR" diff --git a/scripts/semconv/templates/semantic_attributes.j2 b/scripts/semconv/templates/semantic_attributes.j2 new file mode 100644 index 0000000000..32d3f18a1b --- /dev/null +++ b/scripts/semconv/templates/semantic_attributes.j2 @@ -0,0 +1,51 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + + +{%- macro print_value(type, value) -%} + {{ "\"" if type == "string"}}{{value}}{{ "\"" if type == "string"}} +{%- endmacro %} + +from enum import Enum + +class {{class}}: + {%- for attribute in attributes | unique(attribute="fqn") %} + {{attribute.fqn | to_const_name}} = "{{attribute.fqn}}" + """ + {{attribute.brief | to_doc_brief}}. + + {%- if attribute.note %} + Note: {{attribute.note | to_doc_brief}}. + {%- endif %} + + {%- if attribute.deprecated %} + Deprecated: {{attribute.deprecated | to_doc_brief}}. + {%- endif %} + """ +{# Extra line #} + {%- endfor %} + +{%- for attribute in attributes | unique(attribute="fqn") %} +{%- if attribute.is_enum %} +{%- set class_name = attribute.fqn | to_camelcase(True) ~ "Values" %} +{%- set type = attribute.attr_type.enum_type %} +class {{class_name}}(Enum): + {%- for member in attribute.attr_type.members %} + {{ member.member_id | to_const_name }} = {{ print_value(type, member.value) }} + """{% filter escape %}{{member.brief | to_doc_brief}}.{% endfilter %}""" +{# Extra line #} + {%- endfor %} +{% endif %} +{%- endfor %} diff --git a/tox.ini b/tox.ini index af932e009e..96d756c45e 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,10 @@ envlist = py3{6,7,8,9}-test-core-instrumentation pypy3-test-core-instrumentation + ; opentelemetry-semantic-conventions + py3{6,7,8,9}-test-semantic-conventions + pypy3-test-semantic-conventions + ; docs/getting-started py3{6,7,8,9}-test-core-getting-started pypy3-test-core-getting-started @@ -94,6 +98,7 @@ changedir = test-core-api: opentelemetry-api/tests test-core-sdk: opentelemetry-sdk/tests test-core-proto: opentelemetry-proto/tests + test-semantic-conventions: opentelemetry-semantic-conventions/tests test-core-instrumentation: opentelemetry-instrumentation/tests test-core-getting-started: docs/getting_started/tests test-core-opentracing-shim: shim/opentelemetry-opentracing-shim/tests @@ -117,7 +122,7 @@ commands_pre = py3{6,7,8,9}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. - test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-instrumentation {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util + test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-instrumentation {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util test-core-proto: pip install {toxinidir}/opentelemetry-proto distro: pip install {toxinidir}/opentelemetry-distro {toxinidir}/opentelemetry-distro @@ -190,6 +195,7 @@ deps = commands_pre = python -m pip install -e {toxinidir}/opentelemetry-api[test] + python -m pip install -e {toxinidir}/opentelemetry-semantic-conventions[test] python -m pip install -e {toxinidir}/opentelemetry-instrumentation[test] python -m pip install -e {toxinidir}/opentelemetry-sdk[test] python -m pip install -e {toxinidir}/opentelemetry-proto[test] @@ -252,6 +258,7 @@ changedir = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ + -e {toxinidir}/opentelemetry-semantic-conventions \ -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/tests/util \ From 0fb4fd1d7c63d72c9be56d884f428938b522232f Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 21 Apr 2021 07:52:32 -0700 Subject: [PATCH 0852/1517] [chore] Update main after 1.1.0 release (#1786) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 5 ++++- docs/examples/error_handler/error_handler_0/setup.cfg | 2 +- .../error_handler_0/src/error_handler_0/version.py | 2 +- docs/examples/error_handler/error_handler_1/setup.cfg | 2 +- .../error_handler_1/src/error_handler_1/version.py | 2 +- exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-json/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 6 +++--- opentelemetry-distro/src/opentelemetry/distro/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- propagator/opentelemetry-propagator-b3/setup.cfg | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- propagator/opentelemetry-propagator-jaeger/setup.cfg | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 4 ++-- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/util/setup.cfg | 4 ++-- tests/util/src/opentelemetry/test/version.py | 2 +- 41 files changed, 59 insertions(+), 56 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eec2994f75..99ea5b1827 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 2cfa00e1f9277fcefdb64a8f47e7a99681ab65fa + CONTRIB_REPO_SHA: 793ddb4ed638231f387eef2e11207ffb18c6682a jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 50e4ca8765..56f1751856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.0.0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.1.0...HEAD) + +## [1.1.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.1.0) - 2021-04-20 ### Added - Added `py.typed` file to every package. This should resolve a bunch of mypy @@ -41,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1771](https://github.com/open-telemetry/opentelemetry-python/pull/1771)) ## [1.0.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0) - 2021-03-26 + ### Added - Document how to work with fork process web server models(Gunicorn, uWSGI etc...) ([#1609](https://github.com/open-telemetry/opentelemetry-python/pull/1609)) diff --git a/docs/examples/error_handler/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg index 44656dabc8..6b9410dd73 100644 --- a/docs/examples/error_handler/error_handler_0/setup.cfg +++ b/docs/examples/error_handler/error_handler_0/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.0.1.dev0 + opentelemetry-sdk == 1.2.0.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py index 3b16be5d22..2b08175266 100644 --- a/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py +++ b/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.20.dev0" +__version__ = "0.21.dev0" diff --git a/docs/examples/error_handler/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg index b01d478664..2802576928 100644 --- a/docs/examples/error_handler/error_handler_1/setup.cfg +++ b/docs/examples/error_handler/error_handler_1/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.0.1.dev0 + opentelemetry-sdk == 1.2.0.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py index 3b16be5d22..2b08175266 100644 --- a/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py +++ b/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.20.dev0" +__version__ = "0.21.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index 29dc6fd2d6..4ff1ba9514 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 - opentelemetry-api == 1.0.1.dev0 - opentelemetry-sdk == 1.0.1.dev0 + opentelemetry-api == 1.2.0.dev0 + opentelemetry-sdk == 1.2.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 99c41b956f..fa725c3d71 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.1.dev0" +__version__ = "1.2.0.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index 4a5b7b4a70..83e17a61ac 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -40,8 +40,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 1.0.1.dev0 - opentelemetry-sdk == 1.0.1.dev0 + opentelemetry-api == 1.2.0.dev0 + opentelemetry-sdk == 1.2.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 99c41b956f..fa725c3d71 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.1.dev0" +__version__ = "1.2.0.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index ff346fd8d4..1300813323 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -38,8 +38,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.0.1.dev0 - opentelemetry-exporter-jaeger-thrift == 1.0.1.dev0 + opentelemetry-exporter-jaeger-proto-grpc == 1.2.0.dev0 + opentelemetry-exporter-jaeger-thrift == 1.2.0.dev0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 99c41b956f..fa725c3d71 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.1.dev0" +__version__ = "1.2.0.dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 67615351eb..d9814fa7bb 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 1.0.1.dev0 - opentelemetry-sdk == 1.0.1.dev0 + opentelemetry-api == 1.2.0.dev0 + opentelemetry-sdk == 1.2.0.dev0 protobuf >= 3.13.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 3b16be5d22..2b08175266 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.20.dev0" +__version__ = "0.21.dev0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 9bf9f2c988..8d7b9bf96c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 - opentelemetry-api == 1.0.1.dev0 - opentelemetry-sdk == 1.0.1.dev0 - opentelemetry-proto == 1.0.1.dev0 + opentelemetry-api == 1.2.0.dev0 + opentelemetry-sdk == 1.2.0.dev0 + opentelemetry-proto == 1.2.0.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index bfd8ccb84f..2f082c8e9a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.1.dev0" +__version__ = "1.2.0.dev0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 6776518fd8..b57ef21051 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -37,4 +37,4 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.0.1.dev0 + opentelemetry-exporter-otlp-proto-grpc == 1.2.0.dev0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index bfd8ccb84f..2f082c8e9a 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.1.dev0" +__version__ = "1.2.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index cc860b0310..6a4583f038 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 1.0.1.dev0 - opentelemetry-sdk == 1.0.1.dev0 + opentelemetry-api == 1.2.0.dev0 + opentelemetry-sdk == 1.2.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index bfd8ccb84f..2f082c8e9a 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.1.dev0" +__version__ = "1.2.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 52f5ec30e0..f84d61060e 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -42,9 +42,9 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 - opentelemetry-api == 1.0.1.dev0 - opentelemetry-sdk == 1.0.1.dev0 - opentelemetry-exporter-zipkin-json == 1.0.1.dev0 + opentelemetry-api == 1.2.0.dev0 + opentelemetry-sdk == 1.2.0.dev0 + opentelemetry-exporter-zipkin-json == 1.2.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index bfd8ccb84f..2f082c8e9a 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.1.dev0" +__version__ = "1.2.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 388f47e779..da6b3faa92 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -37,8 +37,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.0.1.dev0 - opentelemetry-exporter-zipkin-proto-http == 1.0.1.dev0 + opentelemetry-exporter-zipkin-json == 1.2.0.dev0 + opentelemetry-exporter-zipkin-proto-http == 1.2.0.dev0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index bfd8ccb84f..2f082c8e9a 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.1.dev0" +__version__ = "1.2.0.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index bfd8ccb84f..2f082c8e9a 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.1.dev0" +__version__ = "1.2.0.dev0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index ba6eff4b03..112838fc9f 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -40,8 +40,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.0.1.dev0 - opentelemetry-sdk == 1.0.1.dev0 + opentelemetry-api == 1.2.0.dev0 + opentelemetry-sdk == 1.2.0.dev0 [options.packages.find] where = src @@ -55,4 +55,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.0.1.dev0 + opentelemetry-exporter-otlp == 1.2.0.dev0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index 3b16be5d22..2b08175266 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.20.dev0" +__version__ = "0.21.dev0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index d2614f57f9..8178cc8a13 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.0.1.dev0 + opentelemetry-api == 1.2.0.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 3b16be5d22..2b08175266 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.20.dev0" +__version__ = "0.21.dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index bfd8ccb84f..2f082c8e9a 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.1.dev0" +__version__ = "1.2.0.dev0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index fc4b749853..8dd211d244 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.0.1.dev0 - opentelemetry-semantic-conventions == 0.20.dev0 + opentelemetry-api == 1.2.0.dev0 + opentelemetry-semantic-conventions == 0.21.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index bfd8ccb84f..2f082c8e9a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.1.dev0" +__version__ = "1.2.0.dev0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 3b16be5d22..2b08175266 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.20.dev0" +__version__ = "0.21.dev0" diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index ce54e96f35..605aff4eda 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.0.1.dev0 + opentelemetry-api == 1.2.0.dev0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index bfd8ccb84f..2f082c8e9a 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.1.dev0" +__version__ = "1.2.0.dev0" diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index 0af4b6e947..34de83fac7 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.0.1.dev0 + opentelemetry-api == 1.2.0.dev0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index bfd8ccb84f..2f082c8e9a 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.1.dev0" +__version__ = "1.2.0.dev0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index e340c78272..30f33afba7 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -41,11 +41,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 1.0.1.dev0 + opentelemetry-api == 1.2.0.dev0 [options.extras_require] test = - opentelemetry-test == 0.20.dev0 + opentelemetry-test == 0.21.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 3b16be5d22..2b08175266 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.20.dev0" +__version__ = "0.21.dev0" diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index 897d04b0c2..399ae8df1c 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -37,8 +37,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.0.1.dev0 - opentelemetry-sdk == 1.0.1.dev0 + opentelemetry-api == 1.2.0.dev0 + opentelemetry-sdk == 1.2.0.dev0 [options.extras_require] test = flask~=1.0 diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 266e55076e..0c0996a295 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.20.dev0" +__version__ = "0.21.dev0" From 4b23a162f16dd7b267d3e41ab82ff14b23cb51bd Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 23 Apr 2021 09:01:31 -0600 Subject: [PATCH 0853/1517] Fix examples and documentation (#1793) Fixes #1792 --- docs/examples/auto-instrumentation/README.rst | 4 +++- docs/examples/auto-instrumentation/client.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index 48c59951b4..248c461f57 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -37,7 +37,9 @@ Manually instrumented server def server_request(): with tracer.start_as_current_span( "server_request", - context=propagators.extract(request.headers), + context=extract(request.headers), + kind=trace.SpanKind.SERVER, + attributes=collect_request_attributes(request.environ), ): print(request.args.get("param")) return "served" diff --git a/docs/examples/auto-instrumentation/client.py b/docs/examples/auto-instrumentation/client.py index cc948cc54b..f69fa1c6d2 100644 --- a/docs/examples/auto-instrumentation/client.py +++ b/docs/examples/auto-instrumentation/client.py @@ -16,7 +16,8 @@ from requests import get -from opentelemetry import propagators, trace +from opentelemetry import trace +from opentelemetry.propagate import inject from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -37,7 +38,7 @@ with tracer.start_as_current_span("client-server"): headers = {} - propagators.inject(headers) + inject(headers) requested = get( "http://localhost:8082/server_request", params={"param": argv[1]}, From d8ddb9bf83834e566ef2e17d097741c17f7bc774 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 23 Apr 2021 13:48:05 -0700 Subject: [PATCH 0854/1517] adding getting started to website docs (#1788) --- docs/getting-started.rst | 9 +- docs/getting_started/otlpcollector_example.py | 2 +- docs/getting_started/tracing_example.py | 9 +- website_docs/_index.md | 4 +- website_docs/getting-started.md | 359 ++++++++++++++++++ 5 files changed, 371 insertions(+), 12 deletions(-) create mode 100644 website_docs/getting-started.md diff --git a/docs/getting-started.rst b/docs/getting-started.rst index b39f8c68cd..deede3b75c 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -217,8 +217,7 @@ Start the Collector locally to see how the Collector works in practice. Write th # /tmp/otel-collector-config.yaml receivers: - opencensus: - endpoint: 0.0.0.0:55678 + otlp: exporters: logging: loglevel: debug @@ -228,7 +227,7 @@ Start the Collector locally to see how the Collector works in practice. Write th service: pipelines: traces: - receivers: [opencensus] + receivers: [otlp] exporters: [logging] processors: [batch, queued_retry] @@ -236,9 +235,9 @@ Then start the Docker container: .. code-block:: sh - docker run -p 55678:55678 \ + docker run -p 4317:4317 \ -v /tmp/otel-collector-config.yaml:/etc/otel-collector-config.yaml \ - omnition/opentelemetry-collector-contrib:latest \ + otel/opentelemetry-collector:latest \ --config=/etc/otel-collector-config.yaml Install the OpenTelemetry Collector exporter: diff --git a/docs/getting_started/otlpcollector_example.py b/docs/getting_started/otlpcollector_example.py index f4ada80f19..48c0d32a59 100644 --- a/docs/getting_started/otlpcollector_example.py +++ b/docs/getting_started/otlpcollector_example.py @@ -24,7 +24,7 @@ span_exporter = OTLPSpanExporter( # optional - # endpoint:="myCollectorURL:55678", + # endpoint:="myCollectorURL:4317", # credentials=ChannelCredentials(credentials), # headers=(("metadata", "metadata")), ) diff --git a/docs/getting_started/tracing_example.py b/docs/getting_started/tracing_example.py index 6bb8f18e8a..30e57178fc 100644 --- a/docs/getting_started/tracing_example.py +++ b/docs/getting_started/tracing_example.py @@ -20,10 +20,11 @@ SimpleSpanProcessor, ) -trace.set_tracer_provider(TracerProvider()) -trace.get_tracer_provider().add_span_processor( - SimpleSpanProcessor(ConsoleSpanExporter()) -) +provider = TracerProvider() +processor = SimpleSpanProcessor(ConsoleSpanExporter()) +provider.add_span_processor(processor) +trace.set_tracer_provider(provider) + tracer = trace.get_tracer(__name__) diff --git a/website_docs/_index.md b/website_docs/_index.md index b056b02b56..216af049c7 100644 --- a/website_docs/_index.md +++ b/website_docs/_index.md @@ -17,8 +17,8 @@ get started using OpenTelemetry for Python. The current status of the major functional components for OpenTelemetry Python is as follows: -| Tracing | Metrics | Logging | -| ------- | ------- | ------- | +| Tracing | Metrics | Logging | +| ------- | ------- | ------------------- | | Stable | Alpha | Not Yet Implemented | The current release can be found [here](https://github.com/open-telemetry/opentelemetry-python/releases) diff --git a/website_docs/getting-started.md b/website_docs/getting-started.md new file mode 100644 index 0000000000..9f9cedd402 --- /dev/null +++ b/website_docs/getting-started.md @@ -0,0 +1,359 @@ +--- +title: "Getting Started" +weight: 22 +--- + +This guide walks you through instrumenting a Python application with `opentelemetry-python`. + +For more elaborate examples, see [examples](https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples/). + +## Hello world: emit a trace to your console + +To get started, install both the opentelemetry API and SDK: + +``` +pip install opentelemetry-api +pip install opentelemetry-sdk +``` + +The API package provides the interfaces required by the application owner, as well +as some helper logic to load implementations. + +The SDK provides an implementation of those interfaces. The implementation is designed to be generic and extensible enough +that in many situations, the SDK is sufficient. + +Once installed, you can use the packages to emit spans from your application. A span +represents an action within your application that you want to instrument, such as an HTTP request +or a database call. Once instrumented, you can extract helpful information such as +how long the action took. You can also add arbitrary attributes to the span that provide more insight for debugging. + +The following example script emits a trace containing three named spans: “foo”, “bar”, and “baz”: + +``` +# tracing.py +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleSpanProcessor, +) + +provider = TracerProvider() +processor = SimpleSpanProcessor(ConsoleSpanExporter()) +provider.add_span_processor(processor) +trace.set_tracer_provider(provider) + + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): + print("Hello world from OpenTelemetry Python!") +``` + +When you run the script you can see the traces printed to your console: + +``` +$ python tracing_example.py +{ + "name": "baz", + "context": { + "trace_id": "0xb51058883c02f880111c959f3aa786a2", + "span_id": "0xb2fa4c39f5f35e13", + "trace_state": "{}" + }, + "kind": "SpanKind.INTERNAL", + "parent_id": "0x77e577e6a8813bf4", + "start_time": "2020-05-07T14:39:52.906272Z", + "end_time": "2020-05-07T14:39:52.906343Z", + "status": { + "status_code": "OK" + }, + "attributes": {}, + "events": [], + "links": [] +} +{ + "name": "bar", + "context": { + "trace_id": "0xb51058883c02f880111c959f3aa786a2", + "span_id": "0x77e577e6a8813bf4", + "trace_state": "{}" + }, + "kind": "SpanKind.INTERNAL", + "parent_id": "0x3791d950cc5140c5", + "start_time": "2020-05-07T14:39:52.906230Z", + "end_time": "2020-05-07T14:39:52.906601Z", + "status": { + "status_code": "OK" + }, + "attributes": {}, + "events": [], + "links": [] +} +{ + "name": "foo", + "context": { + "trace_id": "0xb51058883c02f880111c959f3aa786a2", + "span_id": "0x3791d950cc5140c5", + "trace_state": "{}" + }, + "kind": "SpanKind.INTERNAL", + "parent_id": null, + "start_time": "2020-05-07T14:39:52.906157Z", + "end_time": "2020-05-07T14:39:52.906743Z", + "status": { + "status_code": "OK" + }, + "attributes": {}, + "events": [], + "links": [] +} +``` + +Each span typically represents a single operation or unit of work. +Spans can be nested, and have a parent-child relationship with other spans. +While a given span is active, newly-created spans inherit the active span’s trace ID, options, and other attributes of its context. +A span without a parent is called the root span, and a trace is comprised of one root span and its descendants. + +In this example, the OpenTelemetry Python library creates one trace containing three spans and prints it to STDOUT. + +## Configure exporters to emit spans elsewhere + +The previous example does emit information about all spans, but the output is a bit hard to read. +In most cases, you can instead *export* this data to an application performance monitoring backend to be visualized and queried. +It’s also common to aggregate span and trace information from multiple services into a single database, so that actions requiring multiple services can still all be visualized together. + +This concept of aggregating span and trace information is known as distributed tracing. One such distributed tracing backend is known as Jaeger. The Jaeger project provides an all-in-one Docker container with a UI, database, and consumer. + +Run the following command to start Jaeger: + +``` +docker run -p 16686:16686 -p 6831:6831/udp jaegertracing/all-in-one +``` + +This command starts Jaeger locally on port 16686 and exposes the Jaeger thrift agent on port 6831. You can visit Jaeger at [http://localhost:16686](http://localhost:16686). + +After you spin up the backend, your application needs to export traces to this system. Although `opentelemetry-sdk` doesn’t provide an exporter +for Jaeger, you can install it as a separate package with the following command: + +``` +pip install opentelemetry-exporter-jaeger +``` + +After you install the exporter, update your code to import the Jaeger exporter and use that instead: + +``` +# jaeger_example.py +from opentelemetry import trace +from opentelemetry.exporter.jaeger.thrift import JaegerExporter +from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +trace.set_tracer_provider( + TracerProvider( + resource=Resource.create({SERVICE_NAME: "my-helloworld-service"}) + ) +) + +jaeger_exporter = JaegerExporter( + agent_host_name="localhost", + agent_port=6831, +) + +trace.get_tracer_provider().add_span_processor( + BatchSpanProcessor(jaeger_exporter) +) + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): + print("Hello world from OpenTelemetry Python!") +``` + +Finally, run the Python script: + +``` +python jaeger_example.py +``` + +You can then visit the Jaeger UI, see your service under “services”, and find your traces! + + + +![image](images/jaeger_trace.png) + +## Instrumentation example with Flask + +While the example in the previous section is great, it’s very manual. The following are common actions you might want to track and include as part of your distributed tracing. + + +* HTTP responses from web services + + +* HTTP requests from clients + + +* Database calls + +To track these common actions, OpenTelemetry has the concept of instrumentations. Instrumentations are packages designed to interface +with a specific framework or library, such as Flask and psycopg2. You can find a list of the currently curated extension packages in the [Contrib repository](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation). + +Instrument a basic Flask application that uses the requests library to send HTTP requests. First, install the instrumentation packages themselves: + +``` +pip install opentelemetry-instrumentation-flask +pip install opentelemetry-instrumentation-requests +``` + +The following small Flask application sends an HTTP request and also activates each instrumentation during its initialization: + +``` +# flask_example.py +import flask +import requests + +from opentelemetry import trace +from opentelemetry.instrumentation.flask import FlaskInstrumentor +from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleSpanProcessor, +) + +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor( + SimpleSpanProcessor(ConsoleSpanExporter()) +) + +app = flask.Flask(__name__) +FlaskInstrumentor().instrument_app(app) +RequestsInstrumentor().instrument() + + +@app.route("/") +def hello(): + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span("example-request"): + requests.get("http://www.example.com") + return "hello" + + +app.run(debug=True, port=5000) +``` + +Now run the script, hit the root URL ([http://localhost:5000/](http://localhost:5000/)) a few times, and watch your spans be emitted! + +``` +python flask_example.py +``` + +## Configure Your HTTP propagator (b3, Baggage) + +A major feature of distributed tracing is the ability to correlate a trace across +multiple services. However, those services need to propagate information about a +trace from one service to the other. + +To enable this propagation, OpenTelemetry has the concept of [propagators](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md), +which provide a common method to encode and decode span information from a request and response, respectively. + +By default, `opentelemetry-python` is configured to use the [W3C Trace Context](https://www.w3.org/TR/trace-context/) +and [W3C Baggage](https://www.w3.org/TR/baggage/) HTTP headers for HTTP requests, but you can configure it to leverage different propagators. Here’s +an example using Zipkin’s [b3 propagation](https://github.com/openzipkin/b3-propagation): + +``` +pip install opentelemetry-propagator-b3 +``` + +Following the installation of the package containing the b3 propagator, configure the propagator as follows: + +``` +from opentelemetry.propagate import set_global_textmap +from opentelemetry.propagators.b3 import B3Format + +set_global_textmap(B3Format()) +``` + +## Use the OpenTelemetry Collector for traces + +Although it’s possible to directly export your telemetry data to specific backends, you might have more complex use cases such as the following: + + +* A single telemetry sink shared by multiple services, to reduce overhead of switching exporters. + + +* Aggregating traces across multiple services, running on multiple hosts. + +To enable a broad range of aggregation strategies, OpenTelemetry provides the [opentelemetry-collector](https://github.com/open-telemetry/opentelemetry-collector). +The Collector is a flexible application that can consume trace data and export to multiple other backends, including to another instance of the Collector. + +Start the Collector locally to see how the Collector works in practice. Write the following file: + +``` +# /tmp/otel-collector-config.yaml +receivers: + otlp: +exporters: + logging: + loglevel: debug +processors: + batch: + queued_retry: +service: + pipelines: + traces: + receivers: [otlp] + exporters: [logging] + processors: [batch, queued_retry] +``` + +Then start the Docker container: + +``` +docker run -p 4317:4317 \ + -v /tmp/otel-collector-config.yaml:/etc/otel-collector-config.yaml \ + otel/opentelemetry-collector:latest \ + --config=/etc/otel-collector-config.yaml +``` + +Install the OpenTelemetry Collector exporter: + +``` +pip install opentelemetry-exporter-otlp +``` + +Finally, execute the following script: + +``` +# otcollector.py +import time + +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +span_exporter = OTLPSpanExporter( + # optional + # endpoint:="myCollectorURL:4317", + # credentials=ChannelCredentials(credentials), + # headers=(("metadata", "metadata")), +) +tracer_provider = TracerProvider() +trace.set_tracer_provider(tracer_provider) +span_processor = BatchSpanProcessor(span_exporter) +tracer_provider.add_span_processor(span_processor) + +# Configure the tracer to use the collector exporter +tracer = trace.get_tracer_provider().get_tracer(__name__) + +with tracer.start_as_current_span("foo"): + print("Hello world!") +``` From 0dda25fafdd512136c22aa3bb68423cd4237d048 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 26 Apr 2021 09:11:52 -0700 Subject: [PATCH 0855/1517] adding workflow to update website (#1789) --- .github/workflows/docs-update.yml | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/docs-update.yml diff --git a/.github/workflows/docs-update.yml b/.github/workflows/docs-update.yml new file mode 100644 index 0000000000..7892717776 --- /dev/null +++ b/.github/workflows/docs-update.yml @@ -0,0 +1,35 @@ +name: Update OpenTelemetry Website Docs + +on: + # triggers only on a manual dispatch + workflow_dispatch: + +jobs: + update-docs: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v2 + - name: make-pr + env: + API_TOKEN_GITHUB: ${{secrets.DOC_UPDATE_TOKEN}} + # Destination repo should always be 'open-telemetry/opentelemetry.io' + DESTINATION_REPO: open-telemetry/opentelemetry.io + # Destination path should be the absolute path to your language's friendly name in the docs tree (i.e, 'content/en/docs/python') + DESTINATION_PATH: content/en/docs/python + # Source path should be 'website_docs', all files and folders are copied from here to dest + SOURCE_PATH: website_docs + run: | + TARGET_DIR=$(mktemp -d) + export GITHUB_TOKEN=$API_TOKEN_GITHUB + git config --global user.name austinlparker + git config --global user.email austin@lightstep.com + git clone "https://$API_TOKEN_GITHUB@github.com/$DESTINATION_REPO.git" "$TARGET_DIR" + rsync -av --delete "$SOURCE_PATH/" "$TARGET_DIR/$DESTINATION_PATH/" + cd "$TARGET_DIR" + git checkout -b docs-$GITHUB_REPOSITORY-$GITHUB_SHA + git add . + git commit -m "Docs update from $GITHUB_REPOSITORY" + git push -u origin HEAD:docs-$GITHUB_REPOSITORY-$GITHUB_SHA + gh pr create -t "Docs Update from $GITHUB_REPOSITORY" -b "This is an automated pull request." -B main -H docs-$GITHUB_REPOSITORY-$GITHUB_SHA + echo "done" From b3455cd1164f9c5f336cc26a52fb351cb422b0b2 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 26 Apr 2021 09:35:23 -0700 Subject: [PATCH 0856/1517] Move opentelemetry-instrumentation to contrib (#1797) --- .github/pull_request_template.md | 1 - .github/workflows/test.yml | 2 +- CHANGELOG.md | 4 + docs-requirements.txt | 3 +- docs/conf.py | 7 +- eachdist.ini | 3 - opentelemetry-distro/setup.cfg | 1 + .../src/opentelemetry/distro/__init__.py | 3 + opentelemetry-instrumentation/MANIFEST.in | 7 - opentelemetry-instrumentation/README.rst | 112 -------- opentelemetry-instrumentation/setup.cfg | 56 ---- opentelemetry-instrumentation/setup.py | 29 -- .../auto_instrumentation/__init__.py | 118 -------- .../auto_instrumentation/sitecustomize.py | 109 -------- .../instrumentation/bootstrap.py | 252 ------------------ .../instrumentation/configurator.py | 53 ---- .../opentelemetry/instrumentation/distro.py | 47 ---- .../instrumentation/instrumentor.py | 94 ------- .../instrumentation/propagators.py | 128 --------- .../opentelemetry/instrumentation/py.typed | 0 .../opentelemetry/instrumentation/utils.py | 62 ----- .../opentelemetry/instrumentation/version.py | 15 -- .../tests/__init__.py | 0 .../tests/test_bootstrap.py | 128 --------- .../tests/test_instrumentor.py | 47 ---- .../tests/test_propagators.py | 80 ------ .../tests/test_run.py | 117 -------- .../tests/test_utils.py | 57 ---- scripts/build.sh | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- tox.ini | 21 +- 31 files changed, 22 insertions(+), 1538 deletions(-) delete mode 100644 opentelemetry-instrumentation/MANIFEST.in delete mode 100644 opentelemetry-instrumentation/README.rst delete mode 100644 opentelemetry-instrumentation/setup.cfg delete mode 100644 opentelemetry-instrumentation/setup.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py delete mode 100644 opentelemetry-instrumentation/tests/__init__.py delete mode 100644 opentelemetry-instrumentation/tests/test_bootstrap.py delete mode 100644 opentelemetry-instrumentation/tests/test_instrumentor.py delete mode 100644 opentelemetry-instrumentation/tests/test_propagators.py delete mode 100644 opentelemetry-instrumentation/tests/test_run.py delete mode 100644 opentelemetry-instrumentation/tests/test_utils.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 48d109f5ef..d564f69c67 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -23,7 +23,6 @@ Please describe the tests that you ran to verify your changes. Provide instructi Answer the following question based on these examples of changes that would require a Contrib Repo Change: - [The OTel specification](https://github.com/open-telemetry/opentelemetry-specification) has changed which prompted this PR to update the method interfaces of `opentelemetry-api/` or `opentelemetry-sdk/` -- The method interfaces of `opentelemetry-instrumentation/` have changed - The method interfaces of `test/util` have changed - Scripts in `scripts/` that were copied over to the Contrib repo have changed - Configuration files that were copied over to the Contrib repo have changed (when consistency between repositories is applicable) such as in diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99ea5b1827..732d78aeb5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 793ddb4ed638231f387eef2e11207ffb18c6682a + CONTRIB_REPO_SHA: bcd337f74db582684e29fd5e0a9a2de591566c9b jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 56f1751856..87a1781ad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.1.0...HEAD) +### Removed +- Moved `opentelemetry-instrumentation` to contrib repository + ([#1797](https://github.com/open-telemetry/opentelemetry-python/pull/1797)) + ## [1.1.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.1.0) - 2021-04-20 ### Added diff --git a/docs-requirements.txt b/docs-requirements.txt index bda3028040..f802674afd 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -7,9 +7,8 @@ sphinx-autodoc-typehints ./opentelemetry-api ./opentelemetry-semantic-conventions ./opentelemetry-sdk -./opentelemetry-instrumentation -# Required by ext packages +# Required by instrumentation and exporter packages asgiref~=3.0 asyncpg>=0.12.0 ddtrace>=0.34.0 diff --git a/docs/conf.py b/docs/conf.py index 0f1a69503f..38a26e323f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,11 +23,6 @@ settings.configure() - -source_dirs = [ - os.path.abspath("../opentelemetry-instrumentation/src/"), -] - exp = "../exporter" exp_dirs = [ os.path.abspath("/".join(["../exporter", f, "src"])) @@ -42,7 +37,7 @@ if isdir(join(shim, f)) ] -sys.path[:0] = source_dirs + exp_dirs + shim_dirs +sys.path[:0] = exp_dirs + shim_dirs # -- Project information ----------------------------------------------------- diff --git a/eachdist.ini b/eachdist.ini index ded3a5d8c1..d2093491b9 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -7,13 +7,10 @@ ignore= sortfirst= opentelemetry-api opentelemetry-sdk - opentelemetry-instrumentation opentelemetry-proto opentelemetry-distro tests/util - instrumentation/* exporter/* - ext/* [lintroots] extraroots=examples/*,scripts/ diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 112838fc9f..2249765ade 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -41,6 +41,7 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api == 1.2.0.dev0 + opentelemetry-instrumentation == 0.21.dev0 opentelemetry-sdk == 1.2.0.dev0 [options.packages.find] diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index d60bbd208e..ba86e0e247 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -147,6 +147,8 @@ def _initialize_components(): class Configurator(BaseConfigurator): + + # pylint: disable=no-self-use def _configure(self, **kwargs): _initialize_components() @@ -157,5 +159,6 @@ class OpenTelemetryDistro(BaseDistro): configuration out of the box. """ + # pylint: disable=no-self-use def _configure(self, **kwargs): os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp_proto_grpc_span") diff --git a/opentelemetry-instrumentation/MANIFEST.in b/opentelemetry-instrumentation/MANIFEST.in deleted file mode 100644 index 191b7d1959..0000000000 --- a/opentelemetry-instrumentation/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -prune tests -graft src -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include MANIFEST.in -include README.rst diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst deleted file mode 100644 index 6f74d2232f..0000000000 --- a/opentelemetry-instrumentation/README.rst +++ /dev/null @@ -1,112 +0,0 @@ -OpenTelemetry Instrumentation -============================= - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation.svg - :target: https://pypi.org/project/opentelemetry-instrumentation/ - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation - - -This package provides a couple of commands that help automatically instruments a program: - - -opentelemetry-bootstrap ------------------------ - -:: - - opentelemetry-bootstrap --action=install|requirements - -This commands inspects the active Python site-packages and figures out which -instrumentation packages the user might want to install. By default it prints out -a list of the suggested instrumentation packages which can be added to a requirements.txt -file. It also supports installing the suggested packages when run with :code:`--action=install` -flag. - - -opentelemetry-instrument ------------------------- - -:: - - opentelemetry-instrument python program.py - -The instrument command will try to automatically detect packages used by your python program -and when possible, apply automatic tracing instrumentation on them. This means your program -will get automatic distributed tracing for free without having to make any code changes -at all. This will also configure a global tracer and tracing exporter without you having to -make any code changes. By default, the instrument command will use the OTLP exporter but -this can be overriden when needed. - -The command supports the following configuration options as CLI arguments and environment vars: - - -* ``--trace-exporter`` or ``OTEL_TRACE_EXPORTER`` - -Used to specify which trace exporter to use. Can be set to one or more of the well-known exporter -names (see below). - - - Defaults to `otlp`. - - Can be set to `none` to disable automatic tracer initialization. - -You can pass multiple values to configure multiple exporters e.g, ``zipkin,prometheus`` - -Well known trace exporter names: - - - jaeger - - opencensus - - otlp - - otlp_proto_grpc_span - - zipkin - -``otlp`` is an alias for ``otlp_proto_grpc_span``. - -* ``--id-generator`` or ``OTEL_PYTHON_ID_GENERATOR`` - -Used to specify which IDs Generator to use for the global Tracer Provider. By default, it -will use the random IDs generator. - -The code in ``program.py`` needs to use one of the packages for which there is -an OpenTelemetry integration. For a list of the available integrations please -check `here `_ - -* ``OTEL_PYTHON_DISABLED_INSTRUMENTATIONS`` - -If set by the user, opentelemetry-instrument will read this environment variable to disable specific instrumentations. -e.g OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "requests,django" - - -Examples -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:: - - opentelemetry-instrument --trace-exporter otlp flask run --port=3000 - -The above command will pass ``--trace-exporter otlp`` to the instrument command and ``--port=3000`` to ``flask run``. - -:: - - opentelemetry-instrument --trace-exporter zipkin,otlp celery -A tasks worker --loglevel=info - -The above command will configure global trace provider, attach zipkin and otlp exporters to it and then -start celery with the rest of the arguments. - -:: - - opentelemetry-instrument --ids-generator random flask run --port=3000 - -The above command will configure the global trace provider to use the Random IDs Generator, and then -pass ``--port=3000`` to ``flask run``. - -References ----------- - -* `OpenTelemetry Project `_ diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg deleted file mode 100644 index 8178cc8a13..0000000000 --- a/opentelemetry-instrumentation/setup.cfg +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation -description = Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-instrumentation -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - -[options] -python_requires = >=3.6 -package_dir= - =src -packages=find_namespace: -zip_safe = False -include_package_data = True -install_requires = - opentelemetry-api == 1.2.0.dev0 - wrapt >= 1.0.0, < 2.0.0 - -[options.packages.find] -where = src - -[options.entry_points] -console_scripts = - opentelemetry-instrument = opentelemetry.instrumentation.auto_instrumentation:run - opentelemetry-bootstrap = opentelemetry.instrumentation.bootstrap:run - -[options.extras_require] -test = diff --git a/opentelemetry-instrumentation/setup.py b/opentelemetry-instrumentation/setup.py deleted file mode 100644 index d4f84f738b..0000000000 --- a/opentelemetry-instrumentation/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup( - version=PACKAGE_INFO["__version__"], -) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py deleted file mode 100644 index f08b0b144e..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -from logging import getLogger -from os import environ, execl, getcwd -from os.path import abspath, dirname, pathsep -from shutil import which - -from opentelemetry.environment_variables import ( - OTEL_PYTHON_ID_GENERATOR, - OTEL_TRACES_EXPORTER, -) - -logger = getLogger(__file__) - - -def parse_args(): - parser = argparse.ArgumentParser( - description=""" - opentelemetry-instrument automatically instruments a Python - program and it's dependencies and then runs the program. - """ - ) - - parser.add_argument( - "--trace-exporter", - required=False, - help=""" - Uses the specified exporter to export spans. - Accepts multiple exporters as comma separated values. - - Examples: - - --trace-exporter=jaeger - """, - ) - - parser.add_argument( - "--id-generator", - required=False, - help=""" - The IDs Generator to be used with the Tracer Provider. - - Examples: - - --id-generator=random - """, - ) - - parser.add_argument( - "-s", - "--service-name", - required=False, - help=""" - The service name that should be passed to a trace exporter. - """, - ) - - parser.add_argument("command", help="Your Python application.") - parser.add_argument( - "command_args", - help="Arguments for your application.", - nargs=argparse.REMAINDER, - ) - return parser.parse_args() - - -def load_config_from_cli_args(args): - if args.trace_exporter: - environ[OTEL_TRACES_EXPORTER] = args.trace_exporter - if args.id_generator: - environ[OTEL_PYTHON_ID_GENERATOR] = args.id_generator - - -def run() -> None: - args = parse_args() - load_config_from_cli_args(args) - - python_path = environ.get("PYTHONPATH") - - if not python_path: - python_path = [] - - else: - python_path = python_path.split(pathsep) - - cwd_path = getcwd() - - # This is being added to support applications that are being run from their - # own executable, like Django. - # FIXME investigate if there is another way to achieve this - if cwd_path not in python_path: - python_path.insert(0, cwd_path) - - filedir_path = dirname(abspath(__file__)) - - python_path = [path for path in python_path if path != filedir_path] - - python_path.insert(0, filedir_path) - - environ["PYTHONPATH"] = pathsep.join(python_path) - - executable = which(args.command) - execl(executable, executable, *args.command_args) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py deleted file mode 100644 index 10c8faf899..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import sys -from logging import getLogger -from os import environ, path -from os.path import abspath, dirname, pathsep -from re import sub - -from pkg_resources import iter_entry_points - -from opentelemetry.environment_variables import ( - OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, -) - -logger = getLogger(__file__) - - -def _load_distros(): - for entry_point in iter_entry_points("opentelemetry_distro"): - try: - entry_point.load()().configure() # type: ignore - logger.debug("Distribution %s configured", entry_point.name) - except Exception as exc: # pylint: disable=broad-except - logger.exception( - "Distribution %s configuration failed", entry_point.name - ) - raise exc - - -def _load_instrumentors(): - package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, []) - if isinstance(package_to_exclude, str): - package_to_exclude = package_to_exclude.split(",") - # to handle users entering "requests , flask" or "requests, flask" with spaces - package_to_exclude = [x.strip() for x in package_to_exclude] - - for entry_point in iter_entry_points("opentelemetry_instrumentor"): - try: - if entry_point.name in package_to_exclude: - logger.debug( - "Instrumentation skipped for library %s", entry_point.name - ) - continue - entry_point.load()().instrument() # type: ignore - logger.debug("Instrumented %s", entry_point.name) - except Exception as exc: # pylint: disable=broad-except - logger.exception("Instrumenting of %s failed", entry_point.name) - raise exc - - -def _load_configurators(): - configured = None - for entry_point in iter_entry_points("opentelemetry_configurator"): - if configured is not None: - logger.warning( - "Configuration of %s not loaded, %s already loaded", - entry_point.name, - configured, - ) - continue - try: - entry_point.load()().configure() # type: ignore - configured = entry_point.name - except Exception as exc: # pylint: disable=broad-except - logger.exception("Configuration of %s failed", entry_point.name) - raise exc - - -def initialize(): - try: - _load_distros() - _load_configurators() - _load_instrumentors() - except Exception: # pylint: disable=broad-except - logger.exception("Failed to auto initialize opentelemetry") - finally: - environ["PYTHONPATH"] = sub( - r"{}{}?".format(dirname(abspath(__file__)), pathsep), - "", - environ["PYTHONPATH"], - ) - - -if ( - hasattr(sys, "argv") - and sys.argv[0].split(path.sep)[-1] == "celery" - and "worker" in sys.argv[1:] -): - from celery.signals import worker_process_init # pylint:disable=E0401 - - @worker_process_init.connect(weak=False) - def init_celery(*args, **kwargs): - initialize() - - -else: - initialize() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py deleted file mode 100644 index 46d05a7c8f..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ /dev/null @@ -1,252 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import pkgutil -import subprocess -import sys -from logging import getLogger - -from opentelemetry.instrumentation.version import __version__ as version - -logger = getLogger(__file__) - - -# A mapping of "target library" to "desired instrumentor path/versioned package -# name". Used as part of the `opentelemetry-bootstrap` command which looks at -# libraries used by the application that is to be instrumented, and handles -# automatically installing the appropriate instrumentations for that app. -# This helps for those who prefer to turn on as much instrumentation as -# possible, and don't want to go through the manual process of combing through -# the libraries their application uses to figure which one can be -# instrumented. -# NOTE: system-metrics is not to be included. -def all_instrumentations(): - pkg_instrumentation_map = { - "aiohttp-client": "opentelemetry-instrumentation-aiohttp-client", - "aiopg": "opentelemetry-instrumentation-aiopg", - "asyncpg": "opentelemetry-instrumentation-asyncpg", - "boto": "opentelemetry-instrumentation-boto", - "botocore": "opentelemetry-instrumentation-botocore", - "celery": "opentelemetry-instrumentation-celery", - "dbapi": "opentelemetry-instrumentation-dbapi", - "django": "opentelemetry-instrumentation-django", - "elasticsearch": "opentelemetry-instrumentation-elasticsearch", - "falcon": "opentelemetry-instrumentation-falcon", - "fastapi": "opentelemetry-instrumentation-fastapi", - "flask": "opentelemetry-instrumentation-flask", - "grpc": "opentelemetry-instrumentation-grpc", - "jinja2": "opentelemetry-instrumentation-jinja2", - "mysql": "opentelemetry-instrumentation-mysql", - "psycopg2": "opentelemetry-instrumentation-psycopg2", - "pymemcache": "opentelemetry-instrumentation-pymemcache", - "pymongo": "opentelemetry-instrumentation-pymongo", - "pymysql": "opentelemetry-instrumentation-pymysql", - "pyramid": "opentelemetry-instrumentation-pyramid", - "redis": "opentelemetry-instrumentation-redis", - "requests": "opentelemetry-instrumentation-requests", - "sklearn": "opentelemetry-instrumentation-sklearn", - "sqlalchemy": "opentelemetry-instrumentation-sqlalchemy", - "sqlite3": "opentelemetry-instrumentation-sqlite3", - "starlette": "opentelemetry-instrumentation-starlette", - "tornado": "opentelemetry-instrumentation-tornado", - "urllib": "opentelemetry-instrumentation-urllib", - } - for pkg, instrumentation in pkg_instrumentation_map.items(): - pkg_instrumentation_map[pkg] = "{0}=={1}".format( - instrumentation, version - ) - return pkg_instrumentation_map - - -instrumentations = all_instrumentations() - -# relevant instrumentors and tracers to uninstall and check for conflicts for target libraries -libraries = { - "aiohttp-client": ("opentelemetry-instrumentation-aiohttp-client",), - "aiopg": ("opentelemetry-instrumentation-aiopg",), - "asyncpg": ("opentelemetry-instrumentation-asyncpg",), - "boto": ("opentelemetry-instrumentation-boto",), - "botocore": ("opentelemetry-instrumentation-botocore",), - "celery": ("opentelemetry-instrumentation-celery",), - "dbapi": ("opentelemetry-instrumentation-dbapi",), - "django": ("opentelemetry-instrumentation-django",), - "elasticsearch": ("opentelemetry-instrumentation-elasticsearch",), - "falcon": ("opentelemetry-instrumentation-falcon",), - "fastapi": ("opentelemetry-instrumentation-fastapi",), - "flask": ("opentelemetry-instrumentation-flask",), - "grpc": ("opentelemetry-instrumentation-grpc",), - "jinja2": ("opentelemetry-instrumentation-jinja2",), - "mysql": ("opentelemetry-instrumentation-mysql",), - "psycopg2": ("opentelemetry-instrumentation-psycopg2",), - "pymemcache": ("opentelemetry-instrumentation-pymemcache",), - "pymongo": ("opentelemetry-instrumentation-pymongo",), - "pymysql": ("opentelemetry-instrumentation-pymysql",), - "pyramid": ("opentelemetry-instrumentation-pyramid",), - "redis": ("opentelemetry-instrumentation-redis",), - "requests": ("opentelemetry-instrumentation-requests",), - "sklearn": ("opentelemetry-instrumentation-sklearn",), - "sqlalchemy": ("opentelemetry-instrumentation-sqlalchemy",), - "sqlite3": ("opentelemetry-instrumentation-sqlite3",), - "starlette": ("opentelemetry-instrumentation-starlette",), - "tornado": ("opentelemetry-instrumentation-tornado",), - "urllib": ("opentelemetry-instrumentation-urllib",), -} - - -def _install_package(library, instrumentation): - """ - Ensures that desired version is installed w/o upgrading its dependencies - by uninstalling where necessary (if `target` is not provided). - - - OpenTelemetry auto-instrumentation packages often have traced libraries - as instrumentation dependency (e.g. flask for - opentelemetry-instrumentation-flask), so using -I on library could cause - likely undesired Flask upgrade.Using --no-dependencies alone would leave - potential for nonfunctional installations. - """ - pip_list = _sys_pip_freeze() - for package in libraries[library]: - if "{}==".format(package).lower() in pip_list: - logger.info( - "Existing %s installation detected. Uninstalling.", package - ) - _sys_pip_uninstall(package) - _sys_pip_install(instrumentation) - - -def _syscall(func): - def wrapper(package=None): - try: - if package: - return func(package) - return func() - except subprocess.SubprocessError as exp: - cmd = getattr(exp, "cmd", None) - if cmd: - msg = 'Error calling system command "{0}"'.format( - " ".join(cmd) - ) - if package: - msg = '{0} for package "{1}"'.format(msg, package) - raise RuntimeError(msg) - - return wrapper - - -@_syscall -def _sys_pip_freeze(): - return ( - subprocess.check_output([sys.executable, "-m", "pip", "freeze"]) - .decode() - .lower() - ) - - -@_syscall -def _sys_pip_install(package): - # explicit upgrade strategy to override potential pip config - subprocess.check_call( - [ - sys.executable, - "-m", - "pip", - "install", - "-U", - "--upgrade-strategy", - "only-if-needed", - package, - ] - ) - - -@_syscall -def _sys_pip_uninstall(package): - subprocess.check_call( - [sys.executable, "-m", "pip", "uninstall", "-y", package] - ) - - -def _pip_check(): - """Ensures none of the instrumentations have dependency conflicts. - Clean check reported as: - 'No broken requirements found.' - Dependency conflicts are reported as: - 'opentelemetry-instrumentation-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' - To not be too restrictive, we'll only check for relevant packages. - """ - check_pipe = subprocess.Popen( - [sys.executable, "-m", "pip", "check"], stdout=subprocess.PIPE - ) - pip_check = check_pipe.communicate()[0].decode() - pip_check_lower = pip_check.lower() - for package_tup in libraries.values(): - for package in package_tup: - if package.lower() in pip_check_lower: - raise RuntimeError( - "Dependency conflict found: {}".format(pip_check) - ) - - -def _is_installed(library): - return library in sys.modules or pkgutil.find_loader(library) is not None - - -def _find_installed_libraries(): - return {k: v for k, v in instrumentations.items() if _is_installed(k)} - - -def _run_requirements(packages): - print("\n".join(packages.values()), end="") - - -def _run_install(packages): - for pkg, inst in packages.items(): - _install_package(pkg, inst) - - _pip_check() - - -def run() -> None: - action_install = "install" - action_requirements = "requirements" - - parser = argparse.ArgumentParser( - description=""" - opentelemetry-bootstrap detects installed libraries and automatically - installs the relevant instrumentation packages for them. - """ - ) - parser.add_argument( - "-a", - "--action", - choices=[action_install, action_requirements], - default=action_requirements, - help=""" - install - uses pip to install the new requirements using to the - currently active site-package. - requirements - prints out the new requirements to stdout. Action can - be piped and appended to a requirements.txt file. - """, - ) - args = parser.parse_args() - - cmd = { - action_install: _run_install, - action_requirements: _run_requirements, - }[args.action] - cmd(_find_installed_libraries()) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py deleted file mode 100644 index 3efa71e89e..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -""" -OpenTelemetry Base Configurator -""" - -from abc import ABC, abstractmethod -from logging import getLogger - -_LOG = getLogger(__name__) - - -class BaseConfigurator(ABC): - """An ABC for configurators - - Configurators are used to configure - SDKs (i.e. TracerProvider, MeterProvider, Processors...) - to reduce the amount of manual configuration required. - """ - - _instance = None - _is_instrumented = False - - def __new__(cls, *args, **kwargs): - - if cls._instance is None: - cls._instance = object.__new__(cls, *args, **kwargs) - - return cls._instance - - @abstractmethod - def _configure(self, **kwargs): - """Configure the SDK""" - - def configure(self, **kwargs): - """Configure the SDK""" - self._configure(**kwargs) - - -__all__ = ["BaseConfigurator"] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py deleted file mode 100644 index 63cacf1f6d..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -""" -OpenTelemetry Base Distribution (Distro) -""" - -from abc import ABC, abstractmethod -from logging import getLogger - -_LOG = getLogger(__name__) - - -class BaseDistro(ABC): - """An ABC for distro""" - - _instance = None - - def __new__(cls, *args, **kwargs): - - if cls._instance is None: - cls._instance = object.__new__(cls, *args, **kwargs) - - return cls._instance - - @abstractmethod - def _configure(self, **kwargs): - """Configure the distribution""" - - def configure(self, **kwargs): - """Configure the distribution""" - self._configure(**kwargs) - - -__all__ = ["BaseDistro"] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py deleted file mode 100644 index 8cd77a8a5b..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -""" -OpenTelemetry Base Instrumentor -""" - -from abc import ABC, abstractmethod -from logging import getLogger - -_LOG = getLogger(__name__) - - -class BaseInstrumentor(ABC): - """An ABC for instrumentors - - Child classes of this ABC should instrument specific third - party libraries or frameworks either by using the - ``opentelemetry-instrument`` command or by calling their methods - directly. - - Since every third party library or framework is different and has different - instrumentation needs, more methods can be added to the child classes as - needed to provide practical instrumentation to the end user. - """ - - _instance = None - _is_instrumented = False - - def __new__(cls, *args, **kwargs): - - if cls._instance is None: - cls._instance = object.__new__(cls, *args, **kwargs) - - return cls._instance - - @abstractmethod - def _instrument(self, **kwargs): - """Instrument the library""" - - @abstractmethod - def _uninstrument(self, **kwargs): - """Uninstrument the library""" - - def instrument(self, **kwargs): - """Instrument the library - - This method will be called without any optional arguments by the - ``opentelemetry-instrument`` command. - - This means that calling this method directly without passing any - optional values should do the very same thing that the - ``opentelemetry-instrument`` command does. - """ - - if not self._is_instrumented: - result = self._instrument(**kwargs) - self._is_instrumented = True - return result - - _LOG.warning("Attempting to instrument while already instrumented") - - return None - - def uninstrument(self, **kwargs): - """Uninstrument the library - - See ``BaseInstrumentor.instrument`` for more information regarding the - usage of ``kwargs``. - """ - - if self._is_instrumented: - result = self._uninstrument(**kwargs) - self._is_instrumented = False - return result - - _LOG.warning("Attempting to uninstrument while already uninstrumented") - - return None - - -__all__ = ["BaseInstrumentor"] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py deleted file mode 100644 index 2fb246c9d9..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This module implements experimental propagators to inject trace context -into response carriers. This is useful for server side frameworks that start traces -when server requests and want to share the trace context with the client so the -client can add it's spans to the same trace. - -This is part of an upcoming W3C spec and will eventually make it to the Otel spec. - -https://w3c.github.io/trace-context/#trace-context-http-response-headers-format -""" - -import typing -from abc import ABC, abstractmethod - -import opentelemetry.trace as trace -from opentelemetry.context.context import Context -from opentelemetry.propagators import textmap -from opentelemetry.trace import format_span_id, format_trace_id - -_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers" -_RESPONSE_PROPAGATOR = None - - -def get_global_response_propagator(): - return _RESPONSE_PROPAGATOR - - -def set_global_response_propagator(propagator): - global _RESPONSE_PROPAGATOR # pylint:disable=global-statement - _RESPONSE_PROPAGATOR = propagator - - -class Setter(ABC): - @abstractmethod - def set(self, carrier, key, value): - """Inject the provided key value pair in carrier.""" - - -class DictHeaderSetter(Setter): - def set(self, carrier, key, value): # pylint: disable=no-self-use - old_value = carrier.get(key, "") - if old_value: - value = "{0}, {1}".format(old_value, value) - carrier[key] = value - - -class FuncSetter(Setter): - """FuncSetter coverts a function into a valid Setter. Any function that can - set values in a carrier can be converted into a Setter by using FuncSetter. - This is useful when injecting trace context into non-dict objects such - HTTP Response objects for different framework. - - For example, it can be used to create a setter for Falcon response object as: - - setter = FuncSetter(falcon.api.Response.append_header) - - and then used with the propagator as: - - propagator.inject(falcon_response, setter=setter) - - This would essentially make the propagator call `falcon_response.append_header(key, value)` - """ - - def __init__(self, func): - self._func = func - - def set(self, carrier, key, value): - self._func(carrier, key, value) - - -default_setter = DictHeaderSetter() - - -class ResponsePropagator(ABC): - @abstractmethod - def inject( - self, - carrier: textmap.CarrierT, - context: typing.Optional[Context] = None, - setter: textmap.Setter = default_setter, - ) -> None: - """Injects SpanContext into the HTTP response carrier.""" - - -class TraceResponsePropagator(ResponsePropagator): - """Experimental propagator that injects tracecontext into HTTP responses.""" - - def inject( - self, - carrier: textmap.CarrierT, - context: typing.Optional[Context] = None, - setter: textmap.Setter = default_setter, - ) -> None: - """Injects SpanContext into the HTTP response carrier.""" - span = trace.get_current_span(context) - span_context = span.get_span_context() - if span_context == trace.INVALID_SPAN_CONTEXT: - return - - header_name = "traceresponse" - setter.set( - carrier, - header_name, - "00-{trace_id}-{span_id}-{:02x}".format( - span_context.trace_flags, - trace_id=format_trace_id(span_context.trace_id), - span_id=format_span_id(span_context.span_id), - ), - ) - setter.set( - carrier, - _HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, - header_name, - ) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py deleted file mode 100644 index dec070570f..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Dict, Sequence - -from wrapt import ObjectProxy - -from opentelemetry.trace import StatusCode - - -def extract_attributes_from_object( - obj: any, attributes: Sequence[str], existing: Dict[str, str] = None -) -> Dict[str, str]: - extracted = {} - if existing: - extracted.update(existing) - for attr in attributes: - value = getattr(obj, attr, None) - if value is not None: - extracted[attr] = str(value) - return extracted - - -def http_status_to_status_code( - status: int, allow_redirect: bool = True -) -> StatusCode: - """Converts an HTTP status code to an OpenTelemetry canonical status code - - Args: - status (int): HTTP status code - """ - # See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status - if status < 100: - return StatusCode.ERROR - if status <= 299: - return StatusCode.UNSET - if status <= 399 and allow_redirect: - return StatusCode.UNSET - return StatusCode.ERROR - - -def unwrap(obj, attr: str): - """Given a function that was wrapped by wrapt.wrap_function_wrapper, unwrap it - - Args: - obj: Object that holds a reference to the wrapped function - attr (str): Name of the wrapped function - """ - func = getattr(obj, attr, None) - if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"): - setattr(obj, attr, func.__wrapped__) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py deleted file mode 100644 index 2b08175266..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.21.dev0" diff --git a/opentelemetry-instrumentation/tests/__init__.py b/opentelemetry-instrumentation/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry-instrumentation/tests/test_bootstrap.py b/opentelemetry-instrumentation/tests/test_bootstrap.py deleted file mode 100644 index de978eb6d5..0000000000 --- a/opentelemetry-instrumentation/tests/test_bootstrap.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -from functools import reduce -from io import StringIO -from random import sample -from unittest import TestCase -from unittest.mock import call, patch - -from opentelemetry.instrumentation import bootstrap - - -def sample_packages(packages, rate): - sampled = sample( - list(packages), - int(len(packages) * rate), - ) - return {k: v for k, v in packages.items() if k in sampled} - - -class TestBootstrap(TestCase): - - installed_libraries = {} - installed_instrumentations = {} - - @classmethod - def setUpClass(cls): - # select random 60% of instrumentations - cls.installed_libraries = sample_packages( - bootstrap.instrumentations, 0.6 - ) - - # treat 50% of sampled packages as pre-installed - cls.installed_instrumentations = sample_packages( - cls.installed_libraries, 0.5 - ) - - cls.pkg_patcher = patch( - "opentelemetry.instrumentation.bootstrap._find_installed_libraries", - return_value=cls.installed_libraries, - ) - - pip_freeze_output = [] - for inst in cls.installed_instrumentations.values(): - inst = inst.replace(">=", "==") - if "==" not in inst: - inst = "{}==x.y".format(inst) - pip_freeze_output.append(inst) - - cls.pip_freeze_patcher = patch( - "opentelemetry.instrumentation.bootstrap._sys_pip_freeze", - return_value="\n".join(pip_freeze_output), - ) - cls.pip_install_patcher = patch( - "opentelemetry.instrumentation.bootstrap._sys_pip_install", - ) - cls.pip_uninstall_patcher = patch( - "opentelemetry.instrumentation.bootstrap._sys_pip_uninstall", - ) - cls.pip_check_patcher = patch( - "opentelemetry.instrumentation.bootstrap._pip_check", - ) - - cls.pkg_patcher.start() - cls.mock_pip_freeze = cls.pip_freeze_patcher.start() - cls.mock_pip_install = cls.pip_install_patcher.start() - cls.mock_pip_uninstall = cls.pip_uninstall_patcher.start() - cls.mock_pip_check = cls.pip_check_patcher.start() - - @classmethod - def tearDownClass(cls): - cls.pip_check_patcher.start() - cls.pip_uninstall_patcher.start() - cls.pip_install_patcher.start() - cls.pip_freeze_patcher.start() - cls.pkg_patcher.stop() - - @patch("sys.argv", ["bootstrap", "-a", "pipenv"]) - def test_run_unknown_cmd(self): - with self.assertRaises(SystemExit): - bootstrap.run() - - @patch("sys.argv", ["bootstrap", "-a", "requirements"]) - def test_run_cmd_print(self): - with patch("sys.stdout", new=StringIO()) as fake_out: - bootstrap.run() - self.assertEqual( - fake_out.getvalue(), - "\n".join(self.installed_libraries.values()), - ) - - @patch("sys.argv", ["bootstrap", "-a", "install"]) - def test_run_cmd_install(self): - bootstrap.run() - - self.assertEqual( - self.mock_pip_freeze.call_count, len(self.installed_libraries) - ) - - to_uninstall = reduce( - lambda x, y: x + y, - [ - pkgs - for lib, pkgs in bootstrap.libraries.items() - if lib in self.installed_instrumentations - ], - ) - self.mock_pip_uninstall.assert_has_calls( - [call(i) for i in to_uninstall], any_order=True - ) - - self.mock_pip_install.assert_has_calls( - [call(i) for i in self.installed_libraries.values()], - any_order=True, - ) - self.assertEqual(self.mock_pip_check.call_count, 1) diff --git a/opentelemetry-instrumentation/tests/test_instrumentor.py b/opentelemetry-instrumentation/tests/test_instrumentor.py deleted file mode 100644 index 19104a3246..0000000000 --- a/opentelemetry-instrumentation/tests/test_instrumentor.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -from logging import WARNING -from unittest import TestCase - -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor - - -class TestInstrumentor(TestCase): - class Instrumentor(BaseInstrumentor): - def _instrument(self, **kwargs): - return "instrumented" - - def _uninstrument(self, **kwargs): - return "uninstrumented" - - def test_protect(self): - instrumentor = self.Instrumentor() - - with self.assertLogs(level=WARNING): - self.assertIs(instrumentor.uninstrument(), None) - - self.assertEqual(instrumentor.instrument(), "instrumented") - - with self.assertLogs(level=WARNING): - self.assertIs(instrumentor.instrument(), None) - - self.assertEqual(instrumentor.uninstrument(), "uninstrumented") - - with self.assertLogs(level=WARNING): - self.assertIs(instrumentor.uninstrument(), None) - - def test_singleton(self): - self.assertIs(self.Instrumentor(), self.Instrumentor()) diff --git a/opentelemetry-instrumentation/tests/test_propagators.py b/opentelemetry-instrumentation/tests/test_propagators.py deleted file mode 100644 index 62461aafa9..0000000000 --- a/opentelemetry-instrumentation/tests/test_propagators.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=protected-access - -from opentelemetry import trace -from opentelemetry.instrumentation import propagators -from opentelemetry.instrumentation.propagators import ( - DictHeaderSetter, - TraceResponsePropagator, - get_global_response_propagator, - set_global_response_propagator, -) -from opentelemetry.test.test_base import TestBase - - -class TestGlobals(TestBase): - def test_get_set(self): - original = propagators._RESPONSE_PROPAGATOR - - propagators._RESPONSE_PROPAGATOR = None - self.assertIsNone(get_global_response_propagator()) - - prop = TraceResponsePropagator() - set_global_response_propagator(prop) - self.assertIs(prop, get_global_response_propagator()) - - propagators._RESPONSE_PROPAGATOR = original - - -class TestDictHeaderSetter(TestBase): - def test_simple(self): - setter = DictHeaderSetter() - carrier = {} - setter.set(carrier, "kk", "vv") - self.assertIn("kk", carrier) - self.assertEqual(carrier["kk"], "vv") - - def test_append(self): - setter = DictHeaderSetter() - carrier = {"kk": "old"} - setter.set(carrier, "kk", "vv") - self.assertIn("kk", carrier) - self.assertEqual(carrier["kk"], "old, vv") - - -class TestTraceResponsePropagator(TestBase): - def test_inject(self): - span = trace.NonRecordingSpan( - trace.SpanContext( - trace_id=1, - span_id=2, - is_remote=False, - trace_flags=trace.DEFAULT_TRACE_OPTIONS, - trace_state=trace.DEFAULT_TRACE_STATE, - ), - ) - - ctx = trace.set_span_in_context(span) - prop = TraceResponsePropagator() - carrier = {} - prop.inject(carrier, ctx) - self.assertEqual( - carrier["Access-Control-Expose-Headers"], "traceresponse" - ) - self.assertEqual( - carrier["traceresponse"], - "00-00000000000000000000000000000001-0000000000000002-00", - ) diff --git a/opentelemetry-instrumentation/tests/test_run.py b/opentelemetry-instrumentation/tests/test_run.py deleted file mode 100644 index 01bd86ed32..0000000000 --- a/opentelemetry-instrumentation/tests/test_run.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -from os import environ, getcwd -from os.path import abspath, dirname, pathsep -from unittest import TestCase -from unittest.mock import patch - -from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER -from opentelemetry.instrumentation import auto_instrumentation - - -class TestRun(TestCase): - auto_instrumentation_path = dirname(abspath(auto_instrumentation.__file__)) - - @classmethod - def setUpClass(cls): - cls.execl_patcher = patch( - "opentelemetry.instrumentation.auto_instrumentation.execl" - ) - cls.which_patcher = patch( - "opentelemetry.instrumentation.auto_instrumentation.which" - ) - - cls.execl_patcher.start() - cls.which_patcher.start() - - @classmethod - def tearDownClass(cls): - cls.execl_patcher.stop() - cls.which_patcher.stop() - - @patch("sys.argv", ["instrument", ""]) - @patch.dict("os.environ", {"PYTHONPATH": ""}) - def test_empty(self): - auto_instrumentation.run() - self.assertEqual( - environ["PYTHONPATH"], - pathsep.join([self.auto_instrumentation_path, getcwd()]), - ) - - @patch("sys.argv", ["instrument", ""]) - @patch.dict("os.environ", {"PYTHONPATH": "abc"}) - def test_non_empty(self): - auto_instrumentation.run() - self.assertEqual( - environ["PYTHONPATH"], - pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), - ) - - @patch("sys.argv", ["instrument", ""]) - @patch.dict( - "os.environ", - {"PYTHONPATH": pathsep.join(["abc", auto_instrumentation_path])}, - ) - def test_after_path(self): - auto_instrumentation.run() - self.assertEqual( - environ["PYTHONPATH"], - pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), - ) - - @patch("sys.argv", ["instrument", ""]) - @patch.dict( - "os.environ", - { - "PYTHONPATH": pathsep.join( - [auto_instrumentation_path, "abc", auto_instrumentation_path] - ) - }, - ) - def test_single_path(self): - auto_instrumentation.run() - self.assertEqual( - environ["PYTHONPATH"], - pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), - ) - - -class TestExecl(TestCase): - @patch("sys.argv", ["1", "2", "3"]) - @patch("opentelemetry.instrumentation.auto_instrumentation.which") - @patch("opentelemetry.instrumentation.auto_instrumentation.execl") - def test_execl( - self, mock_execl, mock_which - ): # pylint: disable=no-self-use - mock_which.configure_mock(**{"return_value": "python"}) - - auto_instrumentation.run() - - mock_execl.assert_called_with("python", "python", "3") - - -class TestArgs(TestCase): - @patch("opentelemetry.instrumentation.auto_instrumentation.execl") - def test_exporter(self, _): # pylint: disable=no-self-use - with patch("sys.argv", ["instrument", "2"]): - auto_instrumentation.run() - self.assertIsNone(environ.get(OTEL_TRACES_EXPORTER)) - - with patch( - "sys.argv", ["instrument", "--trace-exporter", "jaeger", "1", "2"] - ): - auto_instrumentation.run() - self.assertEqual(environ.get(OTEL_TRACES_EXPORTER), "jaeger") diff --git a/opentelemetry-instrumentation/tests/test_utils.py b/opentelemetry-instrumentation/tests/test_utils.py deleted file mode 100644 index e5246335c9..0000000000 --- a/opentelemetry-instrumentation/tests/test_utils.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from http import HTTPStatus - -from opentelemetry.instrumentation.utils import http_status_to_status_code -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace import StatusCode - - -class TestUtils(TestBase): - # See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status - def test_http_status_to_status_code(self): - for status_code, expected in ( - (HTTPStatus.OK, StatusCode.UNSET), - (HTTPStatus.ACCEPTED, StatusCode.UNSET), - (HTTPStatus.IM_USED, StatusCode.UNSET), - (HTTPStatus.MULTIPLE_CHOICES, StatusCode.UNSET), - (HTTPStatus.BAD_REQUEST, StatusCode.ERROR), - (HTTPStatus.UNAUTHORIZED, StatusCode.ERROR), - (HTTPStatus.FORBIDDEN, StatusCode.ERROR), - (HTTPStatus.NOT_FOUND, StatusCode.ERROR), - ( - HTTPStatus.UNPROCESSABLE_ENTITY, - StatusCode.ERROR, - ), - ( - HTTPStatus.TOO_MANY_REQUESTS, - StatusCode.ERROR, - ), - (HTTPStatus.NOT_IMPLEMENTED, StatusCode.ERROR), - (HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR), - ( - HTTPStatus.GATEWAY_TIMEOUT, - StatusCode.ERROR, - ), - ( - HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, - StatusCode.ERROR, - ), - (600, StatusCode.ERROR), - (99, StatusCode.ERROR), - ): - with self.subTest(status_code=status_code): - actual = http_status_to_status_code(int(status_code)) - self.assertEqual(actual, expected, status_code) diff --git a/scripts/build.sh b/scripts/build.sh index 2f40f1a003..8134769775 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ opentelemetry-distro/ opentelemetry-semantic-conventions/ exporter/*/ shim/*/ propagator/*/; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-proto/ opentelemetry-distro/ opentelemetry-semantic-conventions/ exporter/*/ shim/*/ propagator/*/; do ( echo "building $d" cd "$d" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 30f33afba7..22b1b7c109 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/instrumentation/opentelemetry-instrumentation-opentracing-shim +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/shim/opentelemetry-opentracing-shim platforms = any license = Apache-2.0 classifiers = diff --git a/tox.ini b/tox.ini index 96d756c45e..f0ee49e971 100644 --- a/tox.ini +++ b/tox.ini @@ -16,10 +16,6 @@ envlist = py3{6,7,8,9}-test-core-sdk pypy3-test-core-sdk - ; opentelemetry-instrumentation - py3{6,7,8,9}-test-core-instrumentation - pypy3-test-core-instrumentation - ; opentelemetry-semantic-conventions py3{6,7,8,9}-test-semantic-conventions pypy3-test-semantic-conventions @@ -99,7 +95,6 @@ changedir = test-core-sdk: opentelemetry-sdk/tests test-core-proto: opentelemetry-proto/tests test-semantic-conventions: opentelemetry-semantic-conventions/tests - test-core-instrumentation: opentelemetry-instrumentation/tests test-core-getting-started: docs/getting_started/tests test-core-opentracing-shim: shim/opentelemetry-opentracing-shim/tests test-core-distro: opentelemetry-distro/tests @@ -122,13 +117,12 @@ commands_pre = py3{6,7,8,9}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. - test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-instrumentation {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util + test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util test-core-proto: pip install {toxinidir}/opentelemetry-proto - distro: pip install {toxinidir}/opentelemetry-distro {toxinidir}/opentelemetry-distro - instrumentation: pip install {toxinidir}/opentelemetry-instrumentation + distro: pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation {toxinidir}/opentelemetry-distro - getting-started: pip install -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask + getting-started: pip install -e {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus @@ -196,7 +190,6 @@ deps = commands_pre = python -m pip install -e {toxinidir}/opentelemetry-api[test] python -m pip install -e {toxinidir}/opentelemetry-semantic-conventions[test] - python -m pip install -e {toxinidir}/opentelemetry-instrumentation[test] python -m pip install -e {toxinidir}/opentelemetry-sdk[test] python -m pip install -e {toxinidir}/opentelemetry-proto[test] python -m pip install -e {toxinidir}/tests/util[test] @@ -212,6 +205,7 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-b3[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-jaeger[test] + python -m pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation python -m pip install -e {toxinidir}/opentelemetry-distro[test] commands = @@ -225,6 +219,9 @@ deps = changedir = docs +commands_pre = + python -m pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation + commands = sphinx-build -E -a -W -b html -T . _build/html @@ -239,8 +236,8 @@ deps = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ - -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ + -e {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests \ -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi \ -e {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http @@ -259,7 +256,7 @@ changedir = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-semantic-conventions \ - -e {toxinidir}/opentelemetry-instrumentation \ + -e {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/tests/util \ -e {toxinidir}/exporter/opentelemetry-exporter-opencensus \ From 5b33a8c9e50882d5889b82f3ef3da7cd6b615c15 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 26 Apr 2021 16:52:08 -0700 Subject: [PATCH 0857/1517] Remove extra docs (#1802) --- docs/shim/instrumentation.rst | 15 --------------- docs/shim/instrumentor.rst | 7 ------- 2 files changed, 22 deletions(-) delete mode 100644 docs/shim/instrumentation.rst delete mode 100644 docs/shim/instrumentor.rst diff --git a/docs/shim/instrumentation.rst b/docs/shim/instrumentation.rst deleted file mode 100644 index 9c01b6b6f4..0000000000 --- a/docs/shim/instrumentation.rst +++ /dev/null @@ -1,15 +0,0 @@ -OpenTelemetry Python Instrumentor -================================= - -.. automodule:: opentelemetry.instrumentation - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 1 - - instrumentor diff --git a/docs/shim/instrumentor.rst b/docs/shim/instrumentor.rst deleted file mode 100644 index 5c0c010ff6..0000000000 --- a/docs/shim/instrumentor.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.instrumentation.instrumentor package -================================================== - -.. automodule:: opentelemetry.instrumentation.instrumentor - :members: - :undoc-members: - :show-inheritance: From 46c6eb58b5ae25ca5798073c62e5d167811924c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20=C3=85mdal?= Date: Thu, 29 Apr 2021 01:02:01 +0200 Subject: [PATCH 0858/1517] Add "Typing :: Typed" tags to all packages with py.types (#1798) Pypi defines a typed classifer (https://pypi.org/classifiers/) that we can use for all packages that contain types and the py.types file. --- exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg | 1 + exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg | 1 + exporter/opentelemetry-exporter-jaeger/setup.cfg | 1 + exporter/opentelemetry-exporter-opencensus/setup.cfg | 1 + exporter/opentelemetry-exporter-otlp/setup.cfg | 1 + exporter/opentelemetry-exporter-zipkin-json/setup.cfg | 1 + exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 1 + exporter/opentelemetry-exporter-zipkin/setup.cfg | 1 + opentelemetry-api/setup.cfg | 1 + opentelemetry-distro/setup.cfg | 1 + opentelemetry-sdk/setup.cfg | 1 + propagator/opentelemetry-propagator-b3/setup.cfg | 1 + propagator/opentelemetry-propagator-jaeger/setup.cfg | 1 + shim/opentelemetry-opentracing-shim/setup.cfg | 1 + 14 files changed, 14 insertions(+) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index 4ff1ba9514..7dfada5a7d 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Typing :: Typed [options] python_requires = >=3.6 diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index 83e17a61ac..56f310d074 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Typing :: Typed [options] python_requires = >=3.6 diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 1300813323..802525fb4b 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Typing :: Typed [options] python_requires = >=3.6 diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index d9814fa7bb..31d6cc353d 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Typing :: Typed [options] python_requires = >=3.6 diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index b57ef21051..d8bb421e4e 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Typing :: Typed [options] python_requires = >=3.6 diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index 6a4583f038..e477eddfa2 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -33,6 +33,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Typing :: Typed [options] python_requires = >=3.5 diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index f84d61060e..636a545df7 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -33,6 +33,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Typing :: Typed [options] python_requires = >=3.5 diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index da6b3faa92..862be54288 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Typing :: Typed [options] python_requires = >=3.6 diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 63a0ee4c66..e712564260 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Typing :: Typed [options] python_requires = >=3.6 diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 2249765ade..6bcf8b0cff 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -31,6 +31,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Typing :: Typed [options] python_requires = >=3.6 diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 8dd211d244..00f2deee8c 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Typing :: Typed [options] python_requires = >=3.6 diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 605aff4eda..4739f9ff7b 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Typing :: Typed [options] python_requires = >=3.6 diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index 34de83fac7..f93f5a3c0a 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Typing :: Typed [options] python_requires = >=3.6 diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 22b1b7c109..536cf04451 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Typing :: Typed [options] python_requires = >=3.6 From a4a5b0a7e8c2a404a85b8ad40c4e60d8c1dfde3b Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 29 Apr 2021 21:08:01 +0530 Subject: [PATCH 0859/1517] Zipkin exporter: Add timeout support and implement shutdown (#1799) Co-authored-by: alrex --- CHANGELOG.md | 2 + .../exporter/zipkin/json/__init__.py | 45 +++++++++++++++++-- .../tests/test_zipkin_exporter.py | 43 +++++++++++++++--- .../exporter/zipkin/proto/http/__init__.py | 44 ++++++++++++++++-- .../tests/test_zipkin_exporter.py | 43 +++++++++++++++--- .../sdk/environment_variables/__init__.py | 8 ++++ 6 files changed, 169 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a1781ad0..5466dad65a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1764](https://github.com/open-telemetry/opentelemetry-python/pull/1764)) - Added experimental HTTP back propagators. ([#1762](https://github.com/open-telemetry/opentelemetry-python/pull/1762)) +- Zipkin exporter: Add support for timeout and implement shutdown + ([#1799](https://github.com/open-telemetry/opentelemetry-python/pull/1799)) ### Changed - Adjust `B3Format` propagator to be spec compliant by not modifying context diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py index 55e3a565ec..2fc9df1538 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py @@ -48,6 +48,7 @@ # local_node_ipv6="2001:db8::c001", # local_node_port=31313, # max_tag_value_length=256 + # timeout=5 (in seconds) ) # Create a BatchSpanProcessor and add the exporter to it @@ -62,6 +63,7 @@ The exporter supports the following environment variable for configuration: - :envvar:`OTEL_EXPORTER_ZIPKIN_ENDPOINT` +- :envvar:`OTEL_EXPORTER_ZIPKIN_TIMEOUT` API --- @@ -83,6 +85,7 @@ from opentelemetry.exporter.zipkin.node_endpoint import IpInput, NodeEndpoint from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_ZIPKIN_ENDPOINT, + OTEL_EXPORTER_ZIPKIN_TIMEOUT, ) from opentelemetry.sdk.resources import SERVICE_NAME from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult @@ -103,7 +106,24 @@ def __init__( local_node_ipv6: IpInput = None, local_node_port: Optional[int] = None, max_tag_value_length: Optional[int] = None, + timeout: Optional[int] = None, ): + """Zipkin exporter. + + Args: + version: The protocol version to be used. + endpoint: The endpoint of the Zipkin collector. + local_node_ipv4: Primary IPv4 address associated with this connection. + local_node_ipv6: Primary IPv6 address associated with this connection. + local_node_port: Depending on context, this could be a listen port or the + client-side of a socket. + max_tag_value_length: Max length string attribute values can have. + timeout: Maximum time the Zipkin exporter will wait for each batch export. + The default value is 10s. + + The tuple (local_node_ipv4, local_node_ipv6, local_node_port) is used to represent + the network context of a node in the service graph. + """ self.local_node = NodeEndpoint( local_node_ipv4, local_node_ipv6, local_node_port ) @@ -119,7 +139,22 @@ def __init__( elif version == Protocol.V2: self.encoder = JsonV2Encoder(max_tag_value_length) + self.session = requests.Session() + self.session.headers.update( + {"Content-Type": self.encoder.content_type()} + ) + self._closed = False + self.timeout = timeout or int( + environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10) + ) + def export(self, spans: Sequence[Span]) -> SpanExportResult: + # After the call to Shutdown subsequent calls to Export are + # not allowed and should return a Failure result + if self._closed: + logger.warning("Exporter already shutdown, ignoring batch") + return SpanExportResult.FAILURE + # Populate service_name from first span # We restrict any SpanProcessor to be only associated with a single # TracerProvider, so it is safe to assume that all Spans in a single @@ -129,10 +164,10 @@ def export(self, spans: Sequence[Span]) -> SpanExportResult: service_name = spans[0].resource.attributes.get(SERVICE_NAME) if service_name: self.local_node.service_name = service_name - result = requests.post( + result = self.session.post( url=self.endpoint, data=self.encoder.serialize(spans, self.local_node), - headers={"Content-Type": self.encoder.content_type()}, + timeout=self.timeout, ) if result.status_code not in REQUESTS_SUCCESS_STATUS_CODES: @@ -145,4 +180,8 @@ def export(self, spans: Sequence[Span]) -> SpanExportResult: return SpanExportResult.SUCCESS def shutdown(self) -> None: - pass + if self._closed: + logger.warning("Exporter already shutdown, ignoring call") + return + self.session.close() + self._closed = True diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py index aa2ad9e625..5c2aa0cbe6 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py @@ -24,6 +24,7 @@ from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_ZIPKIN_ENDPOINT, + OTEL_EXPORTER_ZIPKIN_TIMEOUT, ) from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider, _Span @@ -48,8 +49,8 @@ def setUpClass(cls): ) def tearDown(self): - if OTEL_EXPORTER_ZIPKIN_ENDPOINT in os.environ: - del os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] + os.environ.pop(OTEL_EXPORTER_ZIPKIN_ENDPOINT, None) + os.environ.pop(OTEL_EXPORTER_ZIPKIN_TIMEOUT, None) def test_constructor_default(self): exporter = ZipkinExporter() @@ -63,6 +64,7 @@ def test_constructor_default(self): def test_constructor_env_vars(self): os_endpoint = "https://foo:9911/path" os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint + os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "15" exporter = ZipkinExporter() @@ -71,6 +73,7 @@ def test_constructor_env_vars(self): self.assertEqual(exporter.local_node.ipv4, None) self.assertEqual(exporter.local_node.ipv6, None) self.assertEqual(exporter.local_node.port, None) + self.assertEqual(exporter.timeout, 15) def test_constructor_protocol_endpoint(self): """Test the constructor for the common usage of providing the @@ -92,6 +95,7 @@ def test_constructor_all_params_and_env_vars(self): """ os_endpoint = "https://os.env.param:9911/path" os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint + os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "15" constructor_param_version = Protocol.V2 constructor_param_endpoint = "https://constructor.param:9911/path" @@ -99,6 +103,7 @@ def test_constructor_all_params_and_env_vars(self): local_node_ipv6 = "2001:db8::1000" local_node_port = 30301 max_tag_value_length = 56 + timeout_param = 20 exporter = ZipkinExporter( constructor_param_version, @@ -107,6 +112,7 @@ def test_constructor_all_params_and_env_vars(self): local_node_ipv6, local_node_port, max_tag_value_length, + timeout_param, ) self.assertIsInstance(exporter.encoder, JsonV2Encoder) @@ -119,8 +125,11 @@ def test_constructor_all_params_and_env_vars(self): exporter.local_node.ipv6, ipaddress.IPv6Address(local_node_ipv6) ) self.assertEqual(exporter.local_node.port, local_node_port) + # Assert timeout passed in constructor is prioritized over env + # when both are set. + self.assertEqual(exporter.timeout, 20) - @patch("requests.post") + @patch("requests.Session.post") def test_export_success(self, mock_post): mock_post.return_value = MockResponse(200) spans = [] @@ -128,7 +137,7 @@ def test_export_success(self, mock_post): status = exporter.export(spans) self.assertEqual(SpanExportResult.SUCCESS, status) - @patch("requests.post") + @patch("requests.Session.post") def test_export_invalid_response(self, mock_post): mock_post.return_value = MockResponse(404) spans = [] @@ -136,7 +145,7 @@ def test_export_invalid_response(self, mock_post): status = exporter.export(spans) self.assertEqual(SpanExportResult.FAILURE, status) - @patch("requests.post") + @patch("requests.Session.post") def test_export_span_service_name(self, mock_post): mock_post.return_value = MockResponse(200) resource = Resource.create({SERVICE_NAME: "test"}) @@ -152,6 +161,30 @@ def test_export_span_service_name(self, mock_post): exporter.export([span]) self.assertEqual(exporter.local_node.service_name, "test") + @patch("requests.Session.post") + def test_export_shutdown(self, mock_post): + mock_post.return_value = MockResponse(200) + spans = [] + exporter = ZipkinExporter() + status = exporter.export(spans) + self.assertEqual(SpanExportResult.SUCCESS, status) + + exporter.shutdown() + # Any call to .export() post shutdown should return failure + status = exporter.export(spans) + self.assertEqual(SpanExportResult.FAILURE, status) + + @patch("requests.Session.post") + def test_export_timeout(self, mock_post): + mock_post.return_value = MockResponse(200) + spans = [] + exporter = ZipkinExporter(timeout=2) + status = exporter.export(spans) + self.assertEqual(SpanExportResult.SUCCESS, status) + mock_post.assert_called_with( + url="http://localhost:9411/api/v2/spans", data="[]", timeout=2 + ) + class TestZipkinNodeEndpoint(unittest.TestCase): def test_constructor_default(self): diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py index fbc8cc03cf..42a007d172 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py @@ -47,6 +47,7 @@ # local_node_ipv6="2001:db8::c001", # local_node_port=31313, # max_tag_value_length=256 + # timeout=5 (in seconds) ) # Create a BatchSpanProcessor and add the exporter to it @@ -61,6 +62,7 @@ The exporter supports the following environment variable for configuration: - :envvar:`OTEL_EXPORTER_ZIPKIN_ENDPOINT` +- :envvar:`OTEL_EXPORTER_ZIPKIN_TIMEOUT` API --- @@ -81,6 +83,7 @@ from opentelemetry.exporter.zipkin.node_endpoint import IpInput, NodeEndpoint from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_ZIPKIN_ENDPOINT, + OTEL_EXPORTER_ZIPKIN_TIMEOUT, ) from opentelemetry.sdk.resources import SERVICE_NAME from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult @@ -100,7 +103,24 @@ def __init__( local_node_ipv6: IpInput = None, local_node_port: Optional[int] = None, max_tag_value_length: Optional[int] = None, + timeout: Optional[int] = None, ): + """Zipkin exporter. + + Args: + version: The protocol version to be used. + endpoint: The endpoint of the Zipkin collector. + local_node_ipv4: Primary IPv4 address associated with this connection. + local_node_ipv6: Primary IPv6 address associated with this connection. + local_node_port: Depending on context, this could be a listen port or the + client-side of a socket. + max_tag_value_length: Max length string attribute values can have. + timeout: Maximum time the Zipkin exporter will wait for each batch export. + The default value is 10s. + + The tuple (local_node_ipv4, local_node_ipv6, local_node_port) is used to represent + the network context of a node in the service graph. + """ self.local_node = NodeEndpoint( local_node_ipv4, local_node_ipv6, local_node_port ) @@ -113,7 +133,21 @@ def __init__( self.encoder = ProtobufEncoder(max_tag_value_length) + self.session = requests.Session() + self.session.headers.update( + {"Content-Type": self.encoder.content_type()} + ) + self._closed = False + self.timeout = timeout or int( + environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10) + ) + def export(self, spans: Sequence[Span]) -> SpanExportResult: + # After the call to Shutdown subsequent calls to Export are + # not allowed and should return a Failure result + if self._closed: + logger.warning("Exporter already shutdown, ignoring batch") + return SpanExportResult.FAILURE # Populate service_name from first span # We restrict any SpanProcessor to be only associated with a single # TracerProvider, so it is safe to assume that all Spans in a single @@ -123,10 +157,10 @@ def export(self, spans: Sequence[Span]) -> SpanExportResult: service_name = spans[0].resource.attributes.get(SERVICE_NAME) if service_name: self.local_node.service_name = service_name - result = requests.post( + result = self.session.post( url=self.endpoint, data=self.encoder.serialize(spans, self.local_node), - headers={"Content-Type": self.encoder.content_type()}, + timeout=self.timeout, ) if result.status_code not in REQUESTS_SUCCESS_STATUS_CODES: @@ -139,4 +173,8 @@ def export(self, spans: Sequence[Span]) -> SpanExportResult: return SpanExportResult.SUCCESS def shutdown(self) -> None: - pass + if self._closed: + logger.warning("Exporter already shutdown, ignoring call") + return + self.session.close() + self._closed = True diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py index 5537ec8d1a..8b8b01438e 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py @@ -26,6 +26,7 @@ from opentelemetry.exporter.zipkin.proto.http.v2 import ProtobufEncoder from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_ZIPKIN_ENDPOINT, + OTEL_EXPORTER_ZIPKIN_TIMEOUT, ) from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider, _Span @@ -50,8 +51,8 @@ def setUpClass(cls): ) def tearDown(self): - if OTEL_EXPORTER_ZIPKIN_ENDPOINT in os.environ: - del os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] + os.environ.pop(OTEL_EXPORTER_ZIPKIN_ENDPOINT, None) + os.environ.pop(OTEL_EXPORTER_ZIPKIN_TIMEOUT, None) def test_constructor_default(self): exporter = ZipkinExporter() @@ -65,6 +66,7 @@ def test_constructor_default(self): def test_constructor_env_vars(self): os_endpoint = "https://foo:9911/path" os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint + os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "15" exporter = ZipkinExporter() @@ -73,6 +75,7 @@ def test_constructor_env_vars(self): self.assertEqual(exporter.local_node.ipv4, None) self.assertEqual(exporter.local_node.ipv6, None) self.assertEqual(exporter.local_node.port, None) + self.assertEqual(exporter.timeout, 15) def test_constructor_protocol_endpoint(self): """Test the constructor for the common usage of providing the @@ -94,12 +97,14 @@ def test_constructor_all_params_and_env_vars(self): """ os_endpoint = "https://os.env.param:9911/path" os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint + os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "15" constructor_param_endpoint = "https://constructor.param:9911/path" local_node_ipv4 = "192.168.0.1" local_node_ipv6 = "2001:db8::1000" local_node_port = 30301 max_tag_value_length = 56 + timeout_param = 20 exporter = ZipkinExporter( constructor_param_endpoint, @@ -107,6 +112,7 @@ def test_constructor_all_params_and_env_vars(self): local_node_ipv6, local_node_port, max_tag_value_length, + timeout_param, ) self.assertIsInstance(exporter.encoder, ProtobufEncoder) @@ -119,8 +125,11 @@ def test_constructor_all_params_and_env_vars(self): exporter.local_node.ipv6, ipaddress.IPv6Address(local_node_ipv6) ) self.assertEqual(exporter.local_node.port, local_node_port) + # Assert timeout passed in constructor is prioritized over env + # when both are set. + self.assertEqual(exporter.timeout, 20) - @patch("requests.post") + @patch("requests.Session.post") def test_export_success(self, mock_post): mock_post.return_value = MockResponse(200) spans = [] @@ -128,7 +137,7 @@ def test_export_success(self, mock_post): status = exporter.export(spans) self.assertEqual(SpanExportResult.SUCCESS, status) - @patch("requests.post") + @patch("requests.Session.post") def test_export_invalid_response(self, mock_post): mock_post.return_value = MockResponse(404) spans = [] @@ -136,7 +145,7 @@ def test_export_invalid_response(self, mock_post): status = exporter.export(spans) self.assertEqual(SpanExportResult.FAILURE, status) - @patch("requests.post") + @patch("requests.Session.post") def test_export_span_service_name(self, mock_post): mock_post.return_value = MockResponse(200) resource = Resource.create({SERVICE_NAME: "test"}) @@ -152,6 +161,30 @@ def test_export_span_service_name(self, mock_post): exporter.export([span]) self.assertEqual(exporter.local_node.service_name, "test") + @patch("requests.Session.post") + def test_export_shutdown(self, mock_post): + mock_post.return_value = MockResponse(200) + spans = [] + exporter = ZipkinExporter() + status = exporter.export(spans) + self.assertEqual(SpanExportResult.SUCCESS, status) + + exporter.shutdown() + # Any call to .export() post shutdown should return failure + status = exporter.export(spans) + self.assertEqual(SpanExportResult.FAILURE, status) + + @patch("requests.Session.post") + def test_export_timeout(self, mock_post): + mock_post.return_value = MockResponse(200) + spans = [] + exporter = ZipkinExporter(timeout=2) + status = exporter.export(spans) + self.assertEqual(SpanExportResult.SUCCESS, status) + mock_post.assert_called_with( + url="http://localhost:9411/api/v2/spans", data=b"", timeout=2 + ) + class TestZipkinNodeEndpoint(unittest.TestCase): def test_constructor_default(self): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index c3329caf76..9289d1c44a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -122,6 +122,14 @@ include a path (e.g. ``http://example.com:9411/api/v2/spans``). """ +OTEL_EXPORTER_ZIPKIN_TIMEOUT = "OTEL_EXPORTER_ZIPKIN_TIMEOUT" +""" +.. envvar:: OTEL_EXPORTER_ZIPKIN_TIMEOUT + +Maximum time the Zipkin exporter will wait for each batch export, the default +timeout is 10s. +""" + OTEL_EXPORTER_OTLP_PROTOCOL = "OTEL_EXPORTER_OTLP_PROTOCOL" """ .. envvar:: OTEL_EXPORTER_OTLP_PROTOCOL From d598ab434be3e69207dd359fc6ee5115252df0ee Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 30 Apr 2021 11:04:15 -0600 Subject: [PATCH 0860/1517] Explain usage of --noreload (#1795) --- docs/examples/auto-instrumentation/README.rst | 20 +++++++++++++++++++ docs/examples/django/README.rst | 14 ++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index 248c461f57..9298c9bef2 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -206,3 +206,23 @@ similar to the following example: You can see that both outputs are the same because automatic instrumentation does exactly what manual instrumentation does. + +Instrumentation while debugging +=============================== + +The debug mode can be enabled in the Flask app like this: + + +.. code:: python + + if __name__ == "__main__": + app.run(port=8082, debug=True) + +The debug mode can break instrumentation from happening because it enables a +reloader. To run instrumentation while the debug mode is enabled, set the +``use_reloader`` option to ``False``: + +.. code:: python + + if __name__ == "__main__": + app.run(port=8082, debug=True, use_reloader=False) diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 4398372102..0f59d56eb1 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -34,6 +34,9 @@ Execution Execution of the Django app ........................... +This example uses Django features intended for development environment. +The ``runserver`` option should not be used for production environments. + Set these environment variables first: #. ``export DJANGO_SETTINGS_MODULE=instrumentation_example.settings`` @@ -46,7 +49,8 @@ Clone the ``opentelemetry-python`` repository and go to ``opentelemetry-python/d Once there, open the ``manage.py`` file. The call to ``DjangoInstrumentor().instrument()`` in ``main`` is all that is needed to make the app be instrumented. -Run the Django app with ``python manage.py runserver``. +Run the Django app with ``python manage.py runserver --noreload``. +The ``--noreload`` flag is needed to avoid Django from running ``main`` twice. Execution of the client ....................... @@ -105,6 +109,14 @@ Django's instrumentation can be disabled by setting the following environment va #. ``export OTEL_PYTHON_DJANGO_INSTRUMENT=False`` +Auto Instrumentation +-------------------- + +This same example can be run using auto instrumentation. Comment out the call +to ``DjangoInstrumento().instrument()`` in ``main``, then Run the django app +with ``opentelemetry-instrumentation python manage.py runserver --noreload``. +Repeat the steps with the client, the result should be the same. + References ---------- From de8a47405ed21e2f5921f13dfe05bc8ede90790b Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 30 Apr 2021 17:14:30 -0600 Subject: [PATCH 0861/1517] Add example for Django with auto instrumentation (#1803) --- CHANGELOG.md | 4 ++++ docs/examples/django/README.rst | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5466dad65a..ae8eb424a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.1.0...HEAD) +### Added +- Added example for running Django with auto instrumentation + ([#1803](https://github.com/open-telemetry/opentelemetry-python/pull/1803)) + ### Removed - Moved `opentelemetry-instrumentation` to contrib repository ([#1797](https://github.com/open-telemetry/opentelemetry-python/pull/1797)) diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 0f59d56eb1..42316d01ee 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -117,6 +117,26 @@ to ``DjangoInstrumento().instrument()`` in ``main``, then Run the django app with ``opentelemetry-instrumentation python manage.py runserver --noreload``. Repeat the steps with the client, the result should be the same. +Usage with Auto Instrumentation and uWSGI +----------------------------------------- + +uWSGI and Django can be used together with auto instrumentation. To do so, +first install uWSGI in the previous virtual environment: + +``` +pip install uwsgi +``` +Once that is done, run the server with ``uwsgi`` from the directory that +contains ``instrumentation_example``: + +``` +opentelemetry-instrument uwsgi --http :8000 --module instrumentation_example.wsgi +``` + +This should start one uWSGI worker in your console. Open up a browser and point +it to ``localhost:8000``. This request should display a span exported in the +server console. + References ---------- From 300ce1bc601440e5560523296b426592f54bf7c5 Mon Sep 17 00:00:00 2001 From: Marcus Way Date: Sun, 2 May 2021 18:28:39 -0400 Subject: [PATCH 0862/1517] Update example collector-config.yaml (#1805) It seems that there have been some changes to the open-telemetry collector image that cause issues in running this example. I updated the config file to avoid these, but an alternative would be to pin the image to a specific version rather than using the `latest` tag. Anyway, the issues I ran into running the image with the given yaml file were: 1. Empty configuration for the OLTP receiver ```Error: cannot load configuration: error reading receivers configuration for otlp: empty config for OTLP receiver```, which is fixed by adding the ```protocols: grpc: http: ``` bit. 2. `unknown processors type "queued_retry" for queued_retry`. It looks like this processor was [removed](https://github.com/open-telemetry/opentelemetry-operator/issues/18) from the collector, so I just dropped the references to it. --- docs/getting-started.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/getting-started.rst b/docs/getting-started.rst index deede3b75c..6291c13e32 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -218,18 +218,20 @@ Start the Collector locally to see how the Collector works in practice. Write th # /tmp/otel-collector-config.yaml receivers: otlp: + protocols: + grpc: + http: exporters: logging: loglevel: debug processors: batch: - queued_retry: service: pipelines: traces: receivers: [otlp] exporters: [logging] - processors: [batch, queued_retry] + processors: [batch] Then start the Docker container: From cc519287703e861f030874b22a3074bb2a596a9f Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 4 May 2021 11:28:33 -0700 Subject: [PATCH 0863/1517] fix otlp grpc exporter bug when no scheme is passed in (#1806) --- CHANGELOG.md | 8 ++++++-- .../opentelemetry/exporter/otlp/proto/grpc/exporter.py | 3 ++- .../tests/test_otlp_trace_exporter.py | 8 ++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae8eb424a6..164a02b669 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.1.0...HEAD) ### Added -- Added example for running Django with auto instrumentation +- Added example for running Django with auto instrumentation. ([#1803](https://github.com/open-telemetry/opentelemetry-python/pull/1803)) +### Changed +- Fixed OTLP gRPC exporter silently failing if scheme is not specified in endpoint. + ([#1806](https://github.com/open-telemetry/opentelemetry-python/pull/1806)) + ### Removed -- Moved `opentelemetry-instrumentation` to contrib repository +- Moved `opentelemetry-instrumentation` to contrib repository. ([#1797](https://github.com/open-telemetry/opentelemetry-python/pull/1797)) ## [1.1.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.1.0) - 2021-04-20 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 03b17cf2e4..b561a5e84b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -207,7 +207,8 @@ def __init__( else: insecure = True - endpoint = parsed_url.netloc + if parsed_url.netloc: + endpoint = parsed_url.netloc self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS) if isinstance(self._headers, str): diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 5e0e9dd37b..51cbff9fe8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -239,6 +239,7 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): """Just OTEL_EXPORTER_OTLP_COMPRESSION should work""" + expected_endpoint = "localhost:4317" endpoints = [ ( "http://localhost:4317", @@ -275,6 +276,13 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): mock_method, endpoint, insecure ), ) + self.assertEqual( + expected_endpoint, + mock_method.call_args[0][0], + "expected {} got {} {}".format( + expected_endpoint, mock_method.call_args[0][0], endpoint + ), + ) mock_method.reset_mock() # pylint: disable=no-self-use From 4fd46819bd7550d201a2642b962ccc38018ec123 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Thu, 6 May 2021 04:06:45 +0530 Subject: [PATCH 0864/1517] Added `eachdist.py format` command (#1812) --- scripts/eachdist.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 3a9b2b3b9e..1cb8033d62 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -238,6 +238,17 @@ def setup_instparser(instparser): "releaseargs", nargs=argparse.REMAINDER, help=extraargs_help("pytest") ) + fmtparser = subparsers.add_parser( + "format", + help="Formats all source code with black and isort.", + ) + fmtparser.set_defaults(func=format_args) + fmtparser.add_argument( + "--path", + required=False, + help="Format only this path instead of entire repository", + ) + return parser.parse_args(args) @@ -649,6 +660,25 @@ def test_args(args): ) +def format_args(args): + format_dir = str(find_projectroot()) + if args.path: + format_dir = os.path.join(format_dir, args.path) + + runsubprocess( + args.dry_run, + ("black", "."), + cwd=format_dir, + check=True, + ) + runsubprocess( + args.dry_run, + ("isort", "--profile", "black", "."), + cwd=format_dir, + check=True, + ) + + def main(): args = parse_args() args.func(args) From 81d80aab5d4fd23d0d75b223d482d491ac86f006 Mon Sep 17 00:00:00 2001 From: Lucas Bickel Date: Thu, 6 May 2021 03:51:26 +0200 Subject: [PATCH 0865/1517] docs: fix typo in URL (#1819) --- docs/examples/django/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 42316d01ee..bd0b36940a 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -142,4 +142,4 @@ References * `Django `_ * `OpenTelemetry Project `_ -* `OpenTelemetry Django extension `_ +* `OpenTelemetry Django extension `_ From 3a0d38f0bc2af7467d4bec8d5e501d7a172a3154 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 7 May 2021 08:48:36 -0700 Subject: [PATCH 0866/1517] adding documentation for using opentelemetry-distro (#1813) --- docs/examples/auto-instrumentation/README.rst | 20 +++- docs/examples/distro/README.rst | 104 ++++++++++++++++++ docs/getting_started/otlpcollector_example.py | 2 +- opentelemetry-distro/README.rst | 3 +- 4 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 docs/examples/distro/README.rst diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index 9298c9bef2..23fb47b396 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -45,7 +45,7 @@ Manually instrumented server return "served" Server not instrumented manually -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``server_uninstrumented.py`` @@ -57,7 +57,7 @@ Server not instrumented manually return "served" Prepare ------------ +------- Execute the following example in a separate virtual environment. Run the following commands to prepare for auto-instrumentation: @@ -69,7 +69,7 @@ Run the following commands to prepare for auto-instrumentation: $ source auto_instrumentation/bin/activate Install ------------- +------- Run the following commands to install the appropriate packages. The ``opentelemetry-instrumentation`` package provides several @@ -90,7 +90,7 @@ a server as well as the process of executing an automatically instrumented server. Execute a manually instrumented server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Execute the server in two separate consoles, one to run each of the scripts that make up this example: @@ -145,7 +145,7 @@ similar to the following example: } Execute an automatically instrumented server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Stop the execution of ``server_instrumented.py`` with ``ctrl + c`` and run the following command instead: @@ -208,7 +208,7 @@ You can see that both outputs are the same because automatic instrumentation doe exactly what manual instrumentation does. Instrumentation while debugging -=============================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The debug mode can be enabled in the Flask app like this: @@ -226,3 +226,11 @@ reloader. To run instrumentation while the debug mode is enabled, set the if __name__ == "__main__": app.run(port=8082, debug=True, use_reloader=False) + + +Additional resources +~~~~~~~~~~~~~~~~~~~~ + +In order to send telemetry to an OpenTelemetry Collector without doing any +additional configuration, read about the `OpenTelemetry Distro <../distro/README.html>`_ +package. diff --git a/docs/examples/distro/README.rst b/docs/examples/distro/README.rst new file mode 100644 index 0000000000..f58680609a --- /dev/null +++ b/docs/examples/distro/README.rst @@ -0,0 +1,104 @@ +OpenTelemetry Distro +==================== + +In order to make using OpenTelemetry and auto-instrumentation as quick as possible without sacrificing flexibility, +OpenTelemetry distros provide a mechanism to automatically configure some of the more common options for users. By +harnessing their power, users of OpenTelemetry can configure the components as they need. The ``opentelemetry-distro`` +package provides some defaults to users looking to get started, it configures: + +- the SDK TracerProvider +- a BatchSpanProcessor +- the OTLP ``SpanExporter`` to send data to an OpenTelemetry collector + +The package also provides a starting point for anyone interested in producing an alternative distro. The +interfaces implemented by the package are loaded by the auto-instrumentation via the ``opentelemetry_distro`` +and ``opentelemetry_configurator`` entry points to configure the application before any other code is +executed. + +In order to automatically export data from OpenTelemetry to the OpenTelemetry collector, installing the +package will setup all the required entry points. + +.. code:: sh + + $ pip install opentelemetry-distro[otlp] opentelemetry-instrumentation + +Start the Collector locally to see data being exported. Write the following file: + +.. code-block:: yaml + + # /tmp/otel-collector-config.yaml + receivers: + otlp: + protocols: + grpc: + http: + exporters: + logging: + loglevel: debug + processors: + batch: + service: + pipelines: + traces: + receivers: [otlp] + exporters: [logging] + processors: [batch] + +Then start the Docker container: + +.. code-block:: sh + + docker run -p 4317:4317 \ + -v /tmp/otel-collector-config.yaml:/etc/otel-collector-config.yaml \ + otel/opentelemetry-collector:latest \ + --config=/etc/otel-collector-config.yaml + +The following code will create a span with no configuration. + +.. code:: python + + # no_configuration.py + from opentelemetry import trace + + with trace.get_tracer(__name__).start_as_current_span("foo"): + with trace.get_tracer(__name__).start_as_current_span("bar"): + print("baz") + +Lastly, run the ``no_configuration.py`` with the auto-instrumentation: + +.. code-block:: sh + + $ opentelemetry-instrument python no_configuration.py + +The resulting span will appear in the output from the collector and look similar to this: + +.. code-block:: sh + + Resource labels: + -> telemetry.sdk.language: STRING(python) + -> telemetry.sdk.name: STRING(opentelemetry) + -> telemetry.sdk.version: STRING(1.1.0) + -> service.name: STRING(unknown_service) + InstrumentationLibrarySpans #0 + InstrumentationLibrary __main__ + Span #0 + Trace ID : db3c99e5bfc50ef8be1773c3765e8845 + Parent ID : 0677126a4d110cb8 + ID : 3163b3022808ed1b + Name : bar + Kind : SPAN_KIND_INTERNAL + Start time : 2021-05-06 22:54:51.23063 +0000 UTC + End time : 2021-05-06 22:54:51.230684 +0000 UTC + Status code : STATUS_CODE_UNSET + Status message : + Span #1 + Trace ID : db3c99e5bfc50ef8be1773c3765e8845 + Parent ID : + ID : 0677126a4d110cb8 + Name : foo + Kind : SPAN_KIND_INTERNAL + Start time : 2021-05-06 22:54:51.230549 +0000 UTC + End time : 2021-05-06 22:54:51.230706 +0000 UTC + Status code : STATUS_CODE_UNSET + Status message : + diff --git a/docs/getting_started/otlpcollector_example.py b/docs/getting_started/otlpcollector_example.py index 48c0d32a59..71f9ed9754 100644 --- a/docs/getting_started/otlpcollector_example.py +++ b/docs/getting_started/otlpcollector_example.py @@ -24,7 +24,7 @@ span_exporter = OTLPSpanExporter( # optional - # endpoint:="myCollectorURL:4317", + # endpoint="myCollectorURL:4317", # credentials=ChannelCredentials(credentials), # headers=(("metadata", "metadata")), ) diff --git a/opentelemetry-distro/README.rst b/opentelemetry-distro/README.rst index 4189131fc2..8095283910 100644 --- a/opentelemetry-distro/README.rst +++ b/opentelemetry-distro/README.rst @@ -14,9 +14,10 @@ Installation pip install opentelemetry-distro -This package provides entrypoints to configure OpenTelemetry +This package provides entrypoints to configure OpenTelemetry. References ---------- * `OpenTelemetry Project `_ +* `Example using opentelemetry-distro `_ From 64d0426f3b810f6a6f253b2bc646cd4b16e16f9a Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 7 May 2021 14:10:44 -0700 Subject: [PATCH 0867/1517] various fixes to the documentation (#1828) --- CONTRIBUTING.md | 2 +- dev-requirements.txt | 6 +++--- docs-requirements.txt | 6 ++++-- docs/conf.py | 10 +++++++++- docs/examples/basic_context/README.rst | 7 ++----- docs/examples/basic_tracer/README.rst | 5 +---- docs/examples/datadog_exporter/README.rst | 4 ++-- docs/examples/django/README.rst | 4 ++-- docs/examples/opencensus-exporter-tracer/README.rst | 4 ++-- docs/examples/opentracing/README.rst | 4 ++-- docs/exporter/jaeger/jaeger.rst | 2 +- docs/exporter/otlp/otlp.rst | 2 +- docs/index.rst | 8 ++++---- opentelemetry-api/src/opentelemetry/util/_time.py | 2 +- 14 files changed, 35 insertions(+), 31 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b6918ec30..9c6e72244d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ This is the main repo for OpenTelemetry Python. Nevertheless, there are other re Please take a look at this list first, your contributions may belong in one of these repos better: 1. [OpenTelemetry Contrib](https://github.com/open-telemetry/opentelemetry-python-contrib): Instrumentations for third-party - libraries and frameworks. There is an ongoing effort to migrate into the Opentelemetry Contrib repo some of the existing + libraries and frameworks. There is an ongoing effort to migrate into the OpenTelemetry Contrib repo some of the existing programmatic instrumentations that are now in the `ext` directory in the main OpenTelemetry repo. Please ask in the Slack channel (see below) for guidance if you want to contribute with these instrumentations. diff --git a/dev-requirements.txt b/dev-requirements.txt index bcbc90148c..2fb15652b4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,9 +4,9 @@ isort~=5.8 black~=20.8b1 httpretty~=1.0 mypy==0.812 -sphinx~=2.1 -sphinx-rtd-theme~=0.4 -sphinx-autodoc-typehints~=1.10.2 +sphinx~=3.5.4 +sphinx-rtd-theme~=0.5 +sphinx-autodoc-typehints pytest>=6.0 pytest-cov>=2.8 readme-renderer~=24.0 diff --git a/docs-requirements.txt b/docs-requirements.txt index f802674afd..f60325f00b 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -1,6 +1,8 @@ -sphinx~=2.4 -sphinx-rtd-theme~=0.4 +sphinx~=3.5.4 +sphinx-rtd-theme~=0.5 sphinx-autodoc-typehints +# used to generate docs for the website +sphinx-jekyll-builder # Need to install the api/sdk in the venv for autodoc. Modifying sys.path # doesn't work for pkg_resources. diff --git a/docs/conf.py b/docs/conf.py index 38a26e323f..c1476acf17 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -125,7 +125,15 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +exclude_patterns = [ + "_build", + "Thumbs.db", + ".DS_Store", + "examples/fork-process-model/flask-gunicorn", + "examples/fork-process-model/flask-uwsgi", + "examples/error_handler/error_handler_0", + "examples/error_handler/error_handler_1", +] autodoc_default_options = { "members": True, diff --git a/docs/examples/basic_context/README.rst b/docs/examples/basic_context/README.rst index 361192b96f..1499a4bf8e 100644 --- a/docs/examples/basic_context/README.rst +++ b/docs/examples/basic_context/README.rst @@ -1,14 +1,11 @@ Basic Context ============= -These examples show how context is propagated through Spans in OpenTelemetry. - -There are three different examples: +These examples show how context is propagated through Spans in OpenTelemetry. There are three different +examples: * implicit_context: Shows how starting a span implicitly creates context. - * child_context: Shows how context is propagated through child spans. - * async_context: Shows how context can be shared in another coroutine. The source files of these examples are available :scm_web:`here `. diff --git a/docs/examples/basic_tracer/README.rst b/docs/examples/basic_tracer/README.rst index 618263b7a4..572b4dc870 100644 --- a/docs/examples/basic_tracer/README.rst +++ b/docs/examples/basic_tracer/README.rst @@ -1,12 +1,9 @@ Basic Trace =========== -These examples show how to use OpenTelemetry to create and export Spans. - -There are two different examples: +These examples show how to use OpenTelemetry to create and export Spans. There are two different examples: * basic_trace: Shows how to configure a SpanProcessor and Exporter, and how to create a tracer and span. - * resources: Shows how to add resource information to a Provider. The source files of these examples are available :scm_web:`here `. diff --git a/docs/examples/datadog_exporter/README.rst b/docs/examples/datadog_exporter/README.rst index 03f485da15..39e0798da3 100644 --- a/docs/examples/datadog_exporter/README.rst +++ b/docs/examples/datadog_exporter/README.rst @@ -1,5 +1,5 @@ -Datadog Exporter Example -======================== +Datadog Exporter +================ These examples show how to use OpenTelemetry to send tracing data to Datadog. diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index bd0b36940a..656a07b6da 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -1,5 +1,5 @@ -OpenTelemetry Django Instrumentation Example -============================================ +Django Instrumentation +====================== This shows how to use ``opentelemetry-instrumentation-django`` to automatically instrument a Django app. diff --git a/docs/examples/opencensus-exporter-tracer/README.rst b/docs/examples/opencensus-exporter-tracer/README.rst index d147f008d4..3047987c2c 100644 --- a/docs/examples/opencensus-exporter-tracer/README.rst +++ b/docs/examples/opencensus-exporter-tracer/README.rst @@ -1,5 +1,5 @@ -OpenTelemetry Collector Tracer OpenCensus Exporter Example -========================================================== +OpenCensus Exporter +=================== This example shows how to use the OpenCensus Exporter to export traces to the OpenTelemetry collector. diff --git a/docs/examples/opentracing/README.rst b/docs/examples/opentracing/README.rst index da9c70e43b..0bf5f8dca3 100644 --- a/docs/examples/opentracing/README.rst +++ b/docs/examples/opentracing/README.rst @@ -1,5 +1,5 @@ -OpenTracing Shim Example -========================== +OpenTracing Shim +================ This example shows how to use the :doc:`opentelemetry-opentracing-shim package <../../shim/opentracing_shim/opentracing_shim>` diff --git a/docs/exporter/jaeger/jaeger.rst b/docs/exporter/jaeger/jaeger.rst index 5a49f72b5c..1fc02948d8 100644 --- a/docs/exporter/jaeger/jaeger.rst +++ b/docs/exporter/jaeger/jaeger.rst @@ -1,4 +1,4 @@ -Opentelemetry Jaeger Exporters +OpenTelemetry Jaeger Exporters ============================== .. automodule:: opentelemetry.exporter.jaeger diff --git a/docs/exporter/otlp/otlp.rst b/docs/exporter/otlp/otlp.rst index e4bfdd07a1..471f2935fb 100644 --- a/docs/exporter/otlp/otlp.rst +++ b/docs/exporter/otlp/otlp.rst @@ -1,4 +1,4 @@ -Opentelemetry OTLP Exporters +OpenTelemetry OTLP Exporters ============================ .. automodule:: opentelemetry.exporter.otlp diff --git a/docs/index.rst b/docs/index.rst index 42969c058f..fe1b2a8538 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -72,7 +72,7 @@ install .. toctree:: :maxdepth: 1 - :caption: OpenTelemetry Python Packages + :caption: Core Packages :name: packages api/api @@ -80,7 +80,7 @@ install .. toctree:: :maxdepth: 2 - :caption: OpenTelemetry Exporters + :caption: Exporters :name: exporters :glob: @@ -88,7 +88,7 @@ install .. toctree:: :maxdepth: 2 - :caption: OpenTelemetry Shims + :caption: Shims :name: Shims :glob: @@ -96,7 +96,7 @@ install .. toctree:: :maxdepth: 1 - :caption: OpenTelemetry Python Performance + :caption: Performance :name: performance-tests :glob: diff --git a/opentelemetry-api/src/opentelemetry/util/_time.py b/opentelemetry-api/src/opentelemetry/util/_time.py index aa61bc02aa..0099d3dbb5 100644 --- a/opentelemetry-api/src/opentelemetry/util/_time.py +++ b/opentelemetry-api/src/opentelemetry/util/_time.py @@ -18,7 +18,7 @@ if version_info.minor < 7: getLogger(__name__).warning( # pylint: disable=logging-not-lazy "You are using Python 3.%s. This version does not support timestamps " - "with nanosecond precision and the Opentelemetry SDK will use " + "with nanosecond precision and the OpenTelemetry SDK will use " "millisecond precision instead. Please refer to PEP 546 for more " "information. Please upgrade to Python 3.7 or newer to use nanosecond " "precision." % version_info.minor From db0a0c8113eab472487b34a6c786f2b49126d0d9 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 7 May 2021 14:53:27 -0700 Subject: [PATCH 0868/1517] remove hectorhdzg from approvers (#1820) Thanks for all the contributions @hectorhdzg! --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 2e8d6c887a..e6a174e884 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,6 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Aaron Abbott](https://github.com/aabmass), Google - [Diego Hurtado](https://github.com/ocelotl) -- [Hector Hernandez](https://github.com/hectorhdzg), Microsoft - [Owais Lone](https://github.com/owais), Splunk - [Srikanth Chekuri](https://github.com/lonewolf3739) From 2c7689048a150dd51eec7ce722301cb7ce72447c Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 7 May 2021 15:18:25 -0700 Subject: [PATCH 0869/1517] rename CompositeHTTPPropagator, add deprecation notice (#1807) --- CHANGELOG.md | 2 ++ docs/examples/datadog_exporter/server.py | 8 +++----- opentelemetry-api/setup.cfg | 1 + .../src/opentelemetry/propagate/__init__.py | 6 +++--- .../src/opentelemetry/propagators/composite.py | 13 +++++++++++-- .../tests/propagators/test_composite.py | 12 ++++++------ .../tests/propagators/test_propagators.py | 4 ++-- 7 files changed, 28 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 164a02b669..7b152c2ae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Fixed OTLP gRPC exporter silently failing if scheme is not specified in endpoint. ([#1806](https://github.com/open-telemetry/opentelemetry-python/pull/1806)) +- Rename CompositeHTTPPropagator to CompositePropagator as per specification. + ([#1807](https://github.com/open-telemetry/opentelemetry-python/pull/1807)) ### Removed - Moved `opentelemetry-instrumentation` to contrib repository. diff --git a/docs/examples/datadog_exporter/server.py b/docs/examples/datadog_exporter/server.py index 662d3ebe97..f032887a12 100644 --- a/docs/examples/datadog_exporter/server.py +++ b/docs/examples/datadog_exporter/server.py @@ -21,7 +21,7 @@ ) from opentelemetry.exporter.datadog.propagator import DatadogFormat from opentelemetry.propagate import get_global_textmap, set_global_textmap -from opentelemetry.propagators.composite import CompositeHTTPPropagator +from opentelemetry.propagators.composite import CompositePropagator from opentelemetry.sdk.trace import TracerProvider app = Flask(__name__) @@ -38,13 +38,11 @@ # append Datadog format for propagation to and from Datadog instrumented services global_textmap = get_global_textmap() -if isinstance(global_textmap, CompositeHTTPPropagator) and not any( +if isinstance(global_textmap, CompositePropagator) and not any( isinstance(p, DatadogFormat) for p in global_textmap._propagators ): set_global_textmap( - CompositeHTTPPropagator( - global_textmap._propagators + [DatadogFormat()] - ) + CompositePropagator(global_textmap._propagators + [DatadogFormat()]) ) else: set_global_textmap(DatadogFormat()) diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index e712564260..99d52838fb 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -42,6 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = + Deprecated >= 1.2.6 aiocontextvars; python_version<'3.7' [options.packages.find] diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index 6c63edec3c..7095d0eb98 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -16,13 +16,13 @@ API for propagation of context. The propagators for the -``opentelemetry.propagators.composite.CompositeHTTPPropagator`` can be defined +``opentelemetry.propagators.composite.CompositePropagator`` can be defined via configuration in the ``OTEL_PROPAGATORS`` environment variable. This variable should be set to a comma-separated string of names of values for the ``opentelemetry_propagator`` entry point. For example, setting ``OTEL_PROPAGATORS`` to ``tracecontext,baggage`` (which is the default value) would instantiate -``opentelemetry.propagators.composite.CompositeHTTPPropagator`` with 2 +``opentelemetry.propagators.composite.CompositePropagator`` with 2 propagators, one of type ``opentelemetry.trace.propagation.tracecontext.TraceContextTextMapPropagator`` and other of type ``opentelemetry.baggage.propagation.W3CBaggagePropagator``. @@ -142,7 +142,7 @@ def inject( logger.exception("Failed to load configured propagators") raise -_HTTP_TEXT_FORMAT = composite.CompositeHTTPPropagator(propagators) # type: ignore +_HTTP_TEXT_FORMAT = composite.CompositePropagator(propagators) # type: ignore def get_global_textmap() -> textmap.TextMapPropagator: diff --git a/opentelemetry-api/src/opentelemetry/propagators/composite.py b/opentelemetry-api/src/opentelemetry/propagators/composite.py index c027f638dc..b06e385b58 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/composite.py +++ b/opentelemetry-api/src/opentelemetry/propagators/composite.py @@ -14,14 +14,16 @@ import logging import typing +from deprecated import deprecated + from opentelemetry.context.context import Context from opentelemetry.propagators import textmap logger = logging.getLogger(__name__) -class CompositeHTTPPropagator(textmap.TextMapPropagator): - """CompositeHTTPPropagator provides a mechanism for combining multiple +class CompositePropagator(textmap.TextMapPropagator): + """CompositePropagator provides a mechanism for combining multiple propagators into a single one. Args: @@ -80,3 +82,10 @@ def fields(self) -> typing.Set[str]: composite_fields.add(field) return composite_fields + + +@deprecated(version="1.2.0", reason="You should use CompositePropagator") # type: ignore +class CompositeHTTPPropagator(CompositePropagator): + """CompositeHTTPPropagator provides a mechanism for combining multiple + propagators into a single one. + """ diff --git a/opentelemetry-api/tests/propagators/test_composite.py b/opentelemetry-api/tests/propagators/test_composite.py index ef9fae2a1a..d6bf6960cd 100644 --- a/opentelemetry-api/tests/propagators/test_composite.py +++ b/opentelemetry-api/tests/propagators/test_composite.py @@ -17,7 +17,7 @@ import unittest from unittest.mock import Mock -from opentelemetry.propagators.composite import CompositeHTTPPropagator +from opentelemetry.propagators.composite import CompositePropagator def get_as_list(dict_object, key): @@ -67,7 +67,7 @@ def setUpClass(cls): ) def test_no_propagators(self): - propagator = CompositeHTTPPropagator([]) + propagator = CompositePropagator([]) new_carrier = {} propagator.inject(new_carrier) self.assertEqual(new_carrier, {}) @@ -78,7 +78,7 @@ def test_no_propagators(self): self.assertEqual(context, {}) def test_single_propagator(self): - propagator = CompositeHTTPPropagator([self.mock_propagator_0]) + propagator = CompositePropagator([self.mock_propagator_0]) new_carrier = {} propagator.inject(new_carrier) @@ -90,7 +90,7 @@ def test_single_propagator(self): self.assertEqual(context, {"mock-0": "context"}) def test_multiple_propagators(self): - propagator = CompositeHTTPPropagator( + propagator = CompositePropagator( [self.mock_propagator_0, self.mock_propagator_1] ) @@ -106,7 +106,7 @@ def test_multiple_propagators(self): def test_multiple_propagators_same_key(self): # test that when multiple propagators extract/inject the same # key, the later propagator values are extracted/injected - propagator = CompositeHTTPPropagator( + propagator = CompositePropagator( [self.mock_propagator_0, self.mock_propagator_2] ) @@ -120,7 +120,7 @@ def test_multiple_propagators_same_key(self): self.assertEqual(context, {"mock-0": "context2"}) def test_fields(self): - propagator = CompositeHTTPPropagator( + propagator = CompositePropagator( [ self.mock_propagator_0, self.mock_propagator_1, diff --git a/opentelemetry-api/tests/propagators/test_propagators.py b/opentelemetry-api/tests/propagators/test_propagators.py index 80299d3490..bcc0109465 100644 --- a/opentelemetry-api/tests/propagators/test_propagators.py +++ b/opentelemetry-api/tests/propagators/test_propagators.py @@ -27,7 +27,7 @@ class TestPropagators(TestCase): - @patch("opentelemetry.propagators.composite.CompositeHTTPPropagator") + @patch("opentelemetry.propagators.composite.CompositePropagator") def test_default_composite_propagators(self, mock_compositehttppropagator): def test_propagators(propagators): @@ -48,7 +48,7 @@ def test_propagators(propagators): reload(opentelemetry.propagate) @patch.dict(environ, {OTEL_PROPAGATORS: "a,b,c"}) - @patch("opentelemetry.propagators.composite.CompositeHTTPPropagator") + @patch("opentelemetry.propagators.composite.CompositePropagator") @patch("pkg_resources.iter_entry_points") def test_non_default_propagators( self, mock_iter_entry_points, mock_compositehttppropagator From 8a1cbf7cea2abe35bb68b8d02786a42ab19621d6 Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Sat, 8 May 2021 02:50:59 +0200 Subject: [PATCH 0870/1517] Make propagators conform to spec (#1811) * do not modify / set an invalid span in the passed context in case a propagator did not manage to extract * in case no context is passed to propagator.extract assume the root context as default so that a new trace is started instead of continung the current active trace in case extraction fails * fix also jaeger propagator which compared int with str trace/span ids when checking for validity in extract --- CHANGELOG.md | 3 + .../src/opentelemetry/propagate/__init__.py | 2 +- .../src/opentelemetry/propagators/textmap.py | 2 +- .../trace/propagation/tracecontext.py | 13 +-- .../test_tracecontexthttptextformat.py | 49 +++++++++++ .../opentelemetry/propagators/b3/__init__.py | 4 +- .../tests/test_b3_format.py | 87 +++++++++++++++---- .../propagators/jaeger/__init__.py | 51 ++++++++--- .../tests/test_jaeger_propagator.py | 43 +++++++++ .../src/opentelemetry/test/mock_textmap.py | 11 ++- 10 files changed, 221 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b152c2ae7..6e889bccc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1806](https://github.com/open-telemetry/opentelemetry-python/pull/1806)) - Rename CompositeHTTPPropagator to CompositePropagator as per specification. ([#1807](https://github.com/open-telemetry/opentelemetry-python/pull/1807)) +- Propagators use the root context as default for `extract` and do not modify + the context if extracting from carrier does not work. + ([#1811](https://github.com/open-telemetry/opentelemetry-python/pull/1811)) ### Removed - Moved `opentelemetry-instrumentation` to contrib repository. diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index 7095d0eb98..3ad1ad544a 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -96,7 +96,7 @@ def extract( used to construct a Context. This object must be paired with an appropriate getter which understands how to extract a value from it. - context: an optional Context to use. Defaults to current + context: an optional Context to use. Defaults to root context if not set. """ return get_global_textmap().extract(carrier, context, getter=getter) diff --git a/opentelemetry-api/src/opentelemetry/propagators/textmap.py b/opentelemetry-api/src/opentelemetry/propagators/textmap.py index 45c2308f66..0011315cf2 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/textmap.py +++ b/opentelemetry-api/src/opentelemetry/propagators/textmap.py @@ -150,7 +150,7 @@ def extract( used to construct a Context. This object must be paired with an appropriate getter which understands how to extract a value from it. - context: an optional Context to use. Defaults to current + context: an optional Context to use. Defaults to root context if not set. Returns: A Context with configuration found in the carrier. diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index 9fc5cfed24..001db5c729 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -43,14 +43,17 @@ def extract( See `opentelemetry.propagators.textmap.TextMapPropagator.extract` """ + if context is None: + context = Context() + header = getter.get(carrier, self._TRACEPARENT_HEADER_NAME) if not header: - return trace.set_span_in_context(trace.INVALID_SPAN, context) + return context match = re.search(self._TRACEPARENT_HEADER_FORMAT_RE, header[0]) if not match: - return trace.set_span_in_context(trace.INVALID_SPAN, context) + return context version = match.group(1) trace_id = match.group(2) @@ -58,13 +61,13 @@ def extract( trace_flags = match.group(4) if trace_id == "0" * 32 or span_id == "0" * 16: - return trace.set_span_in_context(trace.INVALID_SPAN, context) + return context if version == "00": if match.group(5): - return trace.set_span_in_context(trace.INVALID_SPAN, context) + return context if version == "ff": - return trace.set_span_in_context(trace.INVALID_SPAN, context) + return context tracestate_headers = getter.get(carrier, self._TRACESTATE_HEADER_NAME) if tracestate_headers is None: diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 98ca50610b..9d9561a4a5 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -19,6 +19,7 @@ from unittest.mock import Mock, patch from opentelemetry import trace +from opentelemetry.context import Context from opentelemetry.trace.propagation import tracecontext from opentelemetry.trace.span import TraceState @@ -270,3 +271,51 @@ def test_fields(self, mock_get_current_span, mock_invalid_span_context): inject_fields.add(mock_call[1][1]) self.assertEqual(inject_fields, FORMAT.fields) + + def test_extract_no_trace_parent_to_explicit_ctx(self): + carrier = {"tracestate": ["foo=1"]} + orig_ctx = Context({"k1": "v1"}) + + ctx = FORMAT.extract(carrier, orig_ctx) + self.assertDictEqual(orig_ctx, ctx) + + def test_extract_no_trace_parent_to_implicit_ctx(self): + carrier = {"tracestate": ["foo=1"]} + + ctx = FORMAT.extract(carrier) + self.assertDictEqual(Context(), ctx) + + def test_extract_invalid_trace_parent_to_explicit_ctx(self): + trace_parent_headers = [ + "invalid", + "00-00000000000000000000000000000000-1234567890123456-00", + "00-12345678901234567890123456789012-0000000000000000-00", + "00-12345678901234567890123456789012-1234567890123456-00-residue", + ] + for trace_parent in trace_parent_headers: + with self.subTest(trace_parent=trace_parent): + carrier = { + "traceparent": [trace_parent], + "tracestate": ["foo=1"], + } + orig_ctx = Context({"k1": "v1"}) + + ctx = FORMAT.extract(carrier, orig_ctx) + self.assertDictEqual(orig_ctx, ctx) + + def test_extract_invalid_trace_parent_to_implicit_ctx(self): + trace_parent_headers = [ + "invalid", + "00-00000000000000000000000000000000-1234567890123456-00", + "00-12345678901234567890123456789012-0000000000000000-00", + "00-12345678901234567890123456789012-1234567890123456-00-residue", + ] + for trace_parent in trace_parent_headers: + with self.subTest(trace_parent=trace_parent): + carrier = { + "traceparent": [trace_parent], + "tracestate": ["foo=1"], + } + + ctx = FORMAT.extract(carrier) + self.assertDictEqual(Context(), ctx) diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py index 6977bc32c6..d0beec401a 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py @@ -50,6 +50,8 @@ def extract( context: typing.Optional[Context] = None, getter: Getter = default_getter, ) -> Context: + if context is None: + context = Context() trace_id = trace.INVALID_TRACE_ID span_id = trace.INVALID_SPAN_ID sampled = "0" @@ -97,8 +99,6 @@ def extract( or self._trace_id_regex.fullmatch(trace_id) is None or self._span_id_regex.fullmatch(span_id) is None ): - if context is None: - return trace.set_span_in_context(trace.INVALID_SPAN, context) return context trace_id = int(trace_id, 16) diff --git a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index 6ee0be2ce1..29d8a472ea 100644 --- a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -19,7 +19,7 @@ import opentelemetry.sdk.trace as trace import opentelemetry.sdk.trace.id_generator as id_generator import opentelemetry.trace as trace_api -from opentelemetry.context import get_current +from opentelemetry.context import Context, get_current from opentelemetry.propagators.textmap import DefaultGetter FORMAT = b3_format.B3Format() @@ -219,7 +219,7 @@ def test_flags_and_sampling(self): def test_derived_ctx_is_returned_for_success(self): """Ensure returned context is derived from the given context.""" - old_ctx = {"k1": "v1"} + old_ctx = Context({"k1": "v1"}) new_ctx = FORMAT.extract( { FORMAT.TRACE_ID_KEY: self.serialized_trace_id, @@ -229,17 +229,19 @@ def test_derived_ctx_is_returned_for_success(self): old_ctx, ) self.assertIn("current-span", new_ctx) - for key, value in old_ctx.items(): + for key, value in old_ctx.items(): # pylint:disable=no-member self.assertIn(key, new_ctx) + # pylint:disable=unsubscriptable-object self.assertEqual(new_ctx[key], value) def test_derived_ctx_is_returned_for_failure(self): """Ensure returned context is derived from the given context.""" - old_ctx = {"k2": "v2"} + old_ctx = Context({"k2": "v2"}) new_ctx = FORMAT.extract({}, old_ctx) self.assertNotIn("current-span", new_ctx) - for key, value in old_ctx.items(): + for key, value in old_ctx.items(): # pylint:disable=no-member self.assertIn(key, new_ctx) + # pylint:disable=unsubscriptable-object self.assertEqual(new_ctx[key], value) def test_64bit_trace_id(self): @@ -258,18 +260,24 @@ def test_64bit_trace_id(self): new_carrier[FORMAT.TRACE_ID_KEY], "0" * 16 + trace_id_64_bit ) - def test_extract_invalid_single_header(self): + def test_extract_invalid_single_header_to_explicit_ctx(self): """Given unparsable header, do not modify context""" - old_ctx = {} + old_ctx = Context({"k1": "v1"}) carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} new_ctx = FORMAT.extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) - def test_extract_missing_trace_id(self): + def test_extract_invalid_single_header_to_implicit_ctx(self): + carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} + new_ctx = FORMAT.extract(carrier) + + self.assertDictEqual(Context(), new_ctx) + + def test_extract_missing_trace_id_to_explicit_ctx(self): """Given no trace ID, do not modify context""" - old_ctx = {} + old_ctx = Context({"k1": "v1"}) carrier = { FORMAT.SPAN_ID_KEY: self.serialized_span_id, @@ -279,9 +287,18 @@ def test_extract_missing_trace_id(self): self.assertDictEqual(new_ctx, old_ctx) - def test_extract_invalid_trace_id(self): + def test_extract_missing_trace_id_to_implicit_ctx(self): + carrier = { + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1", + } + new_ctx = FORMAT.extract(carrier) + + self.assertDictEqual(Context(), new_ctx) + + def test_extract_invalid_trace_id_to_explicit_ctx(self): """Given invalid trace ID, do not modify context""" - old_ctx = {} + old_ctx = Context({"k1": "v1"}) carrier = { FORMAT.TRACE_ID_KEY: "abc123", @@ -292,9 +309,19 @@ def test_extract_invalid_trace_id(self): self.assertDictEqual(new_ctx, old_ctx) - def test_extract_invalid_span_id(self): + def test_extract_invalid_trace_id_to_implicit_ctx(self): + carrier = { + FORMAT.TRACE_ID_KEY: "abc123", + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1", + } + new_ctx = FORMAT.extract(carrier) + + self.assertDictEqual(Context(), new_ctx) + + def test_extract_invalid_span_id_to_explicit_ctx(self): """Given invalid span ID, do not modify context""" - old_ctx = {} + old_ctx = Context({"k1": "v1"}) carrier = { FORMAT.TRACE_ID_KEY: self.serialized_trace_id, @@ -305,9 +332,19 @@ def test_extract_invalid_span_id(self): self.assertDictEqual(new_ctx, old_ctx) - def test_extract_missing_span_id(self): + def test_extract_invalid_span_id_to_implicit_ctx(self): + carrier = { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: "abc123", + FORMAT.FLAGS_KEY: "1", + } + new_ctx = FORMAT.extract(carrier) + + self.assertDictEqual(Context(), new_ctx) + + def test_extract_missing_span_id_to_explicit_ctx(self): """Given no span ID, do not modify context""" - old_ctx = {} + old_ctx = Context({"k1": "v1"}) carrier = { FORMAT.TRACE_ID_KEY: self.serialized_trace_id, @@ -317,15 +354,28 @@ def test_extract_missing_span_id(self): self.assertDictEqual(new_ctx, old_ctx) - def test_extract_empty_carrier(self): + def test_extract_missing_span_id_to_implicit_ctx(self): + carrier = { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.FLAGS_KEY: "1", + } + new_ctx = FORMAT.extract(carrier) + + self.assertDictEqual(Context(), new_ctx) + + def test_extract_empty_carrier_to_explicit_ctx(self): """Given no headers at all, do not modify context""" - old_ctx = {} + old_ctx = Context({"k1": "v1"}) carrier = {} new_ctx = FORMAT.extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) + def test_extract_empty_carrier_to_implicit_ctx(self): + new_ctx = FORMAT.extract({}) + self.assertDictEqual(Context(), new_ctx) + @staticmethod def test_inject_empty_context(): """If the current context has no span, don't add headers""" @@ -368,5 +418,4 @@ def test_extract_none_context(self): carrier = {} new_ctx = FORMAT.extract(carrier, old_ctx) - self.assertIsNotNone(new_ctx) - self.assertEqual(new_ctx["current-span"], trace_api.INVALID_SPAN) + self.assertDictEqual(Context(), new_ctx) diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py index 47f438531f..974b9143a5 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py @@ -47,31 +47,26 @@ def extract( ) -> Context: if context is None: - context = get_current() + context = Context() header = getter.get(carrier, self.TRACE_ID_KEY) if not header: - return trace.set_span_in_context(trace.INVALID_SPAN, context) - fields = _extract_first_element(header).split(":") + return context context = self._extract_baggage(getter, carrier, context) - if len(fields) != 4: - return trace.set_span_in_context(trace.INVALID_SPAN, context) - trace_id, span_id, _parent_id, flags = fields + trace_id, span_id, flags = _parse_trace_id_header(header) if ( trace_id == trace.INVALID_TRACE_ID or span_id == trace.INVALID_SPAN_ID ): - return trace.set_span_in_context(trace.INVALID_SPAN, context) + return context span = trace.NonRecordingSpan( trace.SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), + trace_id=trace_id, + span_id=span_id, is_remote=True, - trace_flags=trace.TraceFlags( - int(flags, 16) & trace.TraceFlags.SAMPLED - ), + trace_flags=trace.TraceFlags(flags & trace.TraceFlags.SAMPLED), ) ) return trace.set_span_in_context(span, context) @@ -147,3 +142,35 @@ def _extract_first_element( if items is None: return None return next(iter(items), None) + + +def _parse_trace_id_header( + items: typing.Iterable[CarrierT], +) -> typing.Tuple[int]: + invalid_header_result = (trace.INVALID_TRACE_ID, trace.INVALID_SPAN_ID, 0) + + header = _extract_first_element(items) + if header is None: + return invalid_header_result + + fields = header.split(":") + if len(fields) != 4: + return invalid_header_result + + trace_id_str, span_id_str, _parent_id_str, flags_str = fields + flags = _int_from_hex_str(flags_str, None) + if flags is None: + return invalid_header_result + + trace_id = _int_from_hex_str(trace_id_str, trace.INVALID_TRACE_ID) + span_id = _int_from_hex_str(span_id_str, trace.INVALID_SPAN_ID) + return trace_id, span_id, flags + + +def _int_from_hex_str( + identifier: str, default: typing.Optional[int] +) -> typing.Optional[int]: + try: + return int(identifier, 16) + except ValueError: + return default diff --git a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py index 12a0a028dd..55e096b095 100644 --- a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py @@ -19,6 +19,7 @@ import opentelemetry.sdk.trace.id_generator as id_generator import opentelemetry.trace as trace_api from opentelemetry import baggage +from opentelemetry.context import Context from opentelemetry.propagators import ( # pylint: disable=no-name-in-module jaeger, ) @@ -186,3 +187,45 @@ def test_fields(self): for call in mock_setter.mock_calls: inject_fields.add(call[1][1]) self.assertEqual(FORMAT.fields, inject_fields) + + def test_extract_no_trace_id_to_explicit_ctx(self): + carrier = {} + orig_ctx = Context({"k1": "v1"}) + + ctx = FORMAT.extract(carrier, orig_ctx) + self.assertDictEqual(orig_ctx, ctx) + + def test_extract_no_trace_id_to_implicit_ctx(self): + carrier = {} + + ctx = FORMAT.extract(carrier) + self.assertDictEqual(Context(), ctx) + + def test_extract_invalid_uber_trace_id_header_to_explicit_ctx(self): + trace_id_headers = [ + "000000000000000000000000deadbeef:00000000deadbef0:00", + "00000000000000000000000000000000:00000000deadbef0:00:00", + "000000000000000000000000deadbeef:0000000000000000:00:00", + "000000000000000000000000deadbeef:0000000000000000:00:xyz", + ] + for trace_id_header in trace_id_headers: + with self.subTest(trace_id_header=trace_id_header): + carrier = {"uber-trace-id": trace_id_header} + orig_ctx = Context({"k1": "v1"}) + + ctx = FORMAT.extract(carrier, orig_ctx) + self.assertDictEqual(orig_ctx, ctx) + + def test_extract_invalid_uber_trace_id_header_to_implicit_ctx(self): + trace_id_headers = [ + "000000000000000000000000deadbeef:00000000deadbef0:00", + "00000000000000000000000000000000:00000000deadbef0:00:00", + "000000000000000000000000deadbeef:0000000000000000:00:00", + "000000000000000000000000deadbeef:0000000000000000:00:xyz", + ] + for trace_id_header in trace_id_headers: + with self.subTest(trace_id_header=trace_id_header): + carrier = {"uber-trace-id": trace_id_header} + + ctx = FORMAT.extract(carrier) + self.assertDictEqual(Context(), ctx) diff --git a/tests/util/src/opentelemetry/test/mock_textmap.py b/tests/util/src/opentelemetry/test/mock_textmap.py index 4cdef447d6..c3e901ee28 100644 --- a/tests/util/src/opentelemetry/test/mock_textmap.py +++ b/tests/util/src/opentelemetry/test/mock_textmap.py @@ -15,7 +15,7 @@ import typing from opentelemetry import trace -from opentelemetry.context import Context, get_current +from opentelemetry.context import Context from opentelemetry.propagators.textmap import ( CarrierT, Getter, @@ -39,7 +39,7 @@ def extract( context: typing.Optional[Context] = None, getter: Getter = default_getter, ) -> Context: - return get_current() + return Context() def inject( self, @@ -66,11 +66,13 @@ def extract( context: typing.Optional[Context] = None, getter: Getter = default_getter, ) -> Context: + if context is None: + context = Context() trace_id_list = getter.get(carrier, self.TRACE_ID_KEY) span_id_list = getter.get(carrier, self.SPAN_ID_KEY) if not trace_id_list or not span_id_list: - return trace.set_span_in_context(trace.INVALID_SPAN) + return context return trace.set_span_in_context( trace.NonRecordingSpan( @@ -79,7 +81,8 @@ def extract( span_id=int(span_id_list[0]), is_remote=True, ) - ) + ), + context, ) def inject( From 684f57d64f50d98d65926ff417bf275208b62caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Ch=C3=A1vez?= Date: Mon, 10 May 2021 18:11:51 +0200 Subject: [PATCH 0871/1517] chore: improves logs for warning (#1810) Co-authored-by: alrex --- CHANGELOG.md | 2 ++ opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e889bccc1..1c82de004a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Propagators use the root context as default for `extract` and do not modify the context if extracting from carrier does not work. ([#1811](https://github.com/open-telemetry/opentelemetry-python/pull/1811)) +- Improve warning when failing to decode byte attribute + ([#1810](https://github.com/open-telemetry/opentelemetry-python/pull/1810)) ### Removed - Moved `opentelemetry-instrumentation` to contrib repository. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 625b8519ef..882a7ad4a0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -685,7 +685,10 @@ def set_attributes( try: value = value.decode() except ValueError: - logger.warning("Byte attribute could not be decoded.") + logger.warning( + "Byte attribute could not be decoded for key `%s`.", + key, + ) return self._attributes[key] = value From 6f30160ef93101953644d4636233df5f3f7dd998 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 10 May 2021 12:12:48 -0700 Subject: [PATCH 0872/1517] fix parent_id formatting inconsistency (#1833) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/__init__.py | 8 ++++++-- opentelemetry-sdk/tests/trace/test_trace.py | 9 ++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c82de004a..f0d9bf3e92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1811](https://github.com/open-telemetry/opentelemetry-python/pull/1811)) - Improve warning when failing to decode byte attribute ([#1810](https://github.com/open-telemetry/opentelemetry-python/pull/1810)) +- Fixed inconsistency in parent_id formatting from the ConsoleSpanExporter + ([#1833](https://github.com/open-telemetry/opentelemetry-python/pull/1833)) ### Removed - Moved `opentelemetry-instrumentation` to contrib repository. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 882a7ad4a0..ece7da6d22 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -483,9 +483,13 @@ def to_json(self, indent=4): if self.parent is not None: if isinstance(self.parent, Span): ctx = self.parent.context - parent_id = trace_api.format_span_id(ctx.span_id) + parent_id = "0x{}".format( + trace_api.format_span_id(ctx.span_id) + ) elif isinstance(self.parent, SpanContext): - parent_id = trace_api.format_span_id(self.parent.span_id) + parent_id = "0x{}".format( + trace_api.format_span_id(self.parent.span_id) + ) start_time = None if self._start_time: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index df2454a521..f12dc7c75c 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -1221,7 +1221,10 @@ def test_to_json(self): is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) - span = trace._Span("span-name", context, resource=Resource({})) + parent = trace._Span("parent-name", context, resource=Resource({})) + span = trace._Span( + "span-name", context, resource=Resource({}), parent=parent + ) self.assertEqual( span.to_json(), @@ -1233,7 +1236,7 @@ def test_to_json(self): "trace_state": "[]" }, "kind": "SpanKind.INTERNAL", - "parent_id": null, + "parent_id": "0x00000000deadbef0", "start_time": null, "end_time": null, "status": { @@ -1247,7 +1250,7 @@ def test_to_json(self): ) self.assertEqual( span.to_json(indent=None), - '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "[]"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {}, "events": [], "links": [], "resource": {}}', + '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "[]"}, "kind": "SpanKind.INTERNAL", "parent_id": "0x00000000deadbef0", "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {}, "events": [], "links": [], "resource": {}}', ) def test_attributes_to_json(self): From f7358e981916f35fbb8ca6a740ce94fcdd7068b6 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 11 May 2021 01:45:02 +0530 Subject: [PATCH 0873/1517] Add support for OTEL_SERVICE_NAME env var. (#1829) Spec PR: https://github.com/open-telemetry/opentelemetry-specification/pull/1677 --- CHANGELOG.md | 2 + .../sdk/environment_variables/__init__.py | 17 ++++++++ .../opentelemetry/sdk/resources/__init__.py | 8 +++- .../tests/resources/test_resources.py | 39 +++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0d9bf3e92..99d640ddbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added example for running Django with auto instrumentation. ([#1803](https://github.com/open-telemetry/opentelemetry-python/pull/1803)) +- Added support for OTEL_SERVICE_NAME. + ([#1829](https://github.com/open-telemetry/opentelemetry-python/pull/1829)) ### Changed - Fixed OTLP gRPC exporter silently failing if scheme is not specified in endpoint. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 9289d1c44a..0aa5aa515d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -218,3 +218,20 @@ """ .. envvar:: OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES """ + +OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME" +""" +.. envvar:: OTEL_SERVICE_NAME + +Convenience environment variable for setting the service name resource attribute. +The following two environment variables have the same effect + +.. code-block:: console + + OTEL_SERVICE_NAME=my-python-service + + OTEL_RESOURCE_ATTRIBUTES=service.name=my-python-service + + +If both are set, :envvar:`OTEL_SERVICE_NAME` takes precedence. +""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 6cffff4263..77240b8ef8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -64,7 +64,10 @@ import pkg_resources -from opentelemetry.sdk.environment_variables import OTEL_RESOURCE_ATTRIBUTES +from opentelemetry.sdk.environment_variables import ( + OTEL_RESOURCE_ATTRIBUTES, + OTEL_SERVICE_NAME, +) from opentelemetry.semconv.resource import ResourceAttributes LabelValue = typing.Union[str, bool, int, float] @@ -231,6 +234,9 @@ def detect(self) -> "Resource": item.split("=") for item in env_resources_items.split(",") ) } + service_name = os.environ.get(OTEL_SERVICE_NAME) + if service_name: + env_resource_map[SERVICE_NAME] = service_name return Resource(env_resource_map) diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 4b8fd71da2..211187f1eb 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -232,6 +232,20 @@ def test_env_priority(self): self.assertEqual(resource_env_override.attributes["key1"], "value1") self.assertEqual(resource_env_override.attributes["key2"], "value2") + @mock.patch.dict( + os.environ, + { + resources.OTEL_SERVICE_NAME: "test-srv-name", + resources.OTEL_RESOURCE_ATTRIBUTES: "service.name=svc-name-from-resource", + }, + ) + def test_service_name_env(self): + resource = resources.Resource.create() + self.assertEqual(resource.attributes["service.name"], "test-srv-name") + + resource = resources.Resource.create({"service.name": "from-code"}) + self.assertEqual(resource.attributes["service.name"], "from-code") + class TestOTELResourceDetector(unittest.TestCase): def setUp(self) -> None: @@ -270,3 +284,28 @@ def test_multiple_with_whitespace(self): self.assertEqual( detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) ) + + @mock.patch.dict( + os.environ, + {resources.OTEL_SERVICE_NAME: "test-srv-name"}, + ) + def test_service_name_env(self): + detector = resources.OTELResourceDetector() + self.assertEqual( + detector.detect(), + resources.Resource({"service.name": "test-srv-name"}), + ) + + @mock.patch.dict( + os.environ, + { + resources.OTEL_SERVICE_NAME: "from-service-name", + resources.OTEL_RESOURCE_ATTRIBUTES: "service.name=from-resource-attrs", + }, + ) + def test_service_name_env_precedence(self): + detector = resources.OTELResourceDetector() + self.assertEqual( + detector.detect(), + resources.Resource({"service.name": "from-service-name"}), + ) From cc18b730f9bb7c3ddfa396e06bf91c32a354b405 Mon Sep 17 00:00:00 2001 From: Kristian Larsson Date: Mon, 10 May 2021 23:05:19 +0200 Subject: [PATCH 0874/1517] Include parent span in Jaeger gRPC export (#1809) This extracts the parent span and adds it as a CHILD_OF reference in the gRPC export, so that we get the expected hierarchy of spans. Test case is updated to cover this case. --- CHANGELOG.md | 4 ++++ .../exporter/jaeger/proto/grpc/translate/__init__.py | 12 ++++++++++-- .../tests/test_jaeger_exporter_protobuf.py | 7 ++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99d640ddbe..a2b6899f4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.1.0...HEAD) +### Changed +- Include span parent in Jaeger gRPC export as `CHILD_OF` reference + ([#1809])(https://github.com/open-telemetry/opentelemetry-python/pull/1809) + ### Added - Added example for running Django with auto instrumentation. ([#1803](https://github.com/open-telemetry/opentelemetry-python/pull/1803)) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py index 778296a917..e644e21360 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py @@ -318,10 +318,18 @@ def _extract_tags( def _extract_refs( self, span: ReadableSpan ) -> Optional[Sequence[model_pb2.SpanRef]]: - if not span.links: - return None refs = [] + if span.parent: + ctx = span.get_span_context() + parent_id = span.parent.span_id + parent_ref = model_pb2.SpanRef( + ref_type=model_pb2.SpanRefType.CHILD_OF, + trace_id=_trace_id_to_bytes(ctx.trace_id), + span_id=_span_id_to_bytes(parent_id), + ) + refs.append(parent_ref) + for link in span.links: trace_id = link.context.trace_id span_id = link.context.span_id diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py index 99b3f093cb..383e33d0f1 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py @@ -287,11 +287,16 @@ def test_translate_to_jaeger(self): ), ], references=[ + model_pb2.SpanRef( + ref_type=model_pb2.SpanRefType.CHILD_OF, + trace_id=pb_translator._trace_id_to_bytes(trace_id), + span_id=pb_translator._span_id_to_bytes(parent_id), + ), model_pb2.SpanRef( ref_type=model_pb2.SpanRefType.FOLLOWS_FROM, trace_id=pb_translator._trace_id_to_bytes(trace_id), span_id=pb_translator._span_id_to_bytes(other_id), - ) + ), ], logs=[ model_pb2.Log( From 8e75cfc04224caa84425bec81112abb4a4468415 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 11 May 2021 08:04:49 -0700 Subject: [PATCH 0875/1517] adding script to automatically generate otel website docs (#1837) --- RELEASING.md | 9 +++++++-- docs/getting-started.rst | 2 -- docs/images/jaeger_trace.png | Bin 156601 -> 0 bytes scripts/generate_website_docs.sh | 11 +++++++++++ website_docs/getting-started.md | 16 +++++++++------- 5 files changed, 27 insertions(+), 11 deletions(-) delete mode 100644 docs/images/jaeger_trace.png create mode 100755 scripts/generate_website_docs.sh diff --git a/RELEASING.md b/RELEASING.md index a3239d3053..945c947d89 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -74,10 +74,15 @@ perl -i -p -e 's/0.7b0/0.8.dev0/' $(git grep -l "0.7b0" | grep -vi CHANGELOG) git commit -m ``` -## Update docs +## Update website docs -If the docs for the Opentelemetry [website](https://opentelemetry.io/docs/python/) was updated in this release in this [folder](https://github.com/open-telemetry/opentelemetry-python/tree/main/website_docs), submit a [PR](https://github.com/open-telemetry/opentelemetry.io/tree/main/content/en/docs/python) to update the docs on the website accordingly. +If the docs for the Opentelemetry [website](https://opentelemetry.io/docs/python/) was updated in this release in this [folder](https://github.com/open-telemetry/opentelemetry-python/tree/main/website_docs), submit a [PR](https://github.com/open-telemetry/opentelemetry.io/tree/main/content/en/docs/python) to update the docs on the website accordingly. To check if the new version requires updating, run the following script and compare the diff: +```bash +./scripts/generate_website_docs.sh +``` + +If the diff includes significant changes, create a pull request to commit the changes and once the changes are merged, click the "Run workflow" button for the Update [OpenTelemetry Website Docs](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/docs-update.yml) GitHub Action. ## Troubleshooting diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 6291c13e32..93b688eada 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -137,8 +137,6 @@ Finally, run the Python script: You can then visit the Jaeger UI, see your service under "services", and find your traces! -.. image:: images/jaeger_trace.png - Instrumentation example with Flask ------------------------------------ diff --git a/docs/images/jaeger_trace.png b/docs/images/jaeger_trace.png deleted file mode 100644 index 83afbe1293b8c1d50611eb1d8f4686da601881c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156601 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 diff --git a/scripts/generate_website_docs.sh b/scripts/generate_website_docs.sh new file mode 100755 index 0000000000..aabd6f3e0c --- /dev/null +++ b/scripts/generate_website_docs.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# this script generates the documentation required for +# opentelemetry.io + +pip install -r docs-requirements.txt + +TMP_DIR=/tmp/website_docs +rm -Rf ${TMP_DIR} + +sphinx-build -M jekyll ./docs ${TMP_DIR} +cp ${TMP_DIR}/jekyll/getting-started.md ./website_docs/ \ No newline at end of file diff --git a/website_docs/getting-started.md b/website_docs/getting-started.md index 9f9cedd402..d8850191ca 100644 --- a/website_docs/getting-started.md +++ b/website_docs/getting-started.md @@ -1,4 +1,8 @@ --- +date: '2021-05-07T21:49:47.106Z' +docname: getting-started +images: {} +path: /getting-started title: "Getting Started" weight: 22 --- @@ -183,10 +187,6 @@ python jaeger_example.py You can then visit the Jaeger UI, see your service under “services”, and find your traces! - - -![image](images/jaeger_trace.png) - ## Instrumentation example with Flask While the example in the previous section is great, it’s very manual. The following are common actions you might want to track and include as part of your distributed tracing. @@ -298,18 +298,20 @@ Start the Collector locally to see how the Collector works in practice. Write th # /tmp/otel-collector-config.yaml receivers: otlp: + protocols: + grpc: + http: exporters: logging: loglevel: debug processors: batch: - queued_retry: service: pipelines: traces: receivers: [otlp] exporters: [logging] - processors: [batch, queued_retry] + processors: [batch] ``` Then start the Docker container: @@ -342,7 +344,7 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor span_exporter = OTLPSpanExporter( # optional - # endpoint:="myCollectorURL:4317", + # endpoint="myCollectorURL:4317", # credentials=ChannelCredentials(credentials), # headers=(("metadata", "metadata")), ) From 271f01548a726521d7250fe95f216add62222502 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 11 May 2021 08:40:17 -0700 Subject: [PATCH 0876/1517] Fix otlp exporter translating sequence types (#1818) --- CHANGELOG.md | 4 ++ .../exporter/otlp/proto/grpc/exporter.py | 29 ++++++++-- .../tests/test_otlp_trace_exporter.py | 56 +++++++++++++++++++ 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2b6899f4a..eb6b0b5b93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Moved `opentelemetry-instrumentation` to contrib repository. ([#1797](https://github.com/open-telemetry/opentelemetry-python/pull/1797)) +### Changed +- Fixed sequence values in OTLP exporter not translating + ([#1818](https://github.com/open-telemetry/opentelemetry-python/pull/1818)) + ## [1.1.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.1.0) - 2021-04-20 ### Added diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index b561a5e84b..f48a473fd0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -37,7 +37,11 @@ ssl_channel_credentials, ) -from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue, + ArrayValue, + KeyValue, +) from opentelemetry.proto.resource.v1.resource_pb2 import Resource from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, @@ -81,7 +85,7 @@ def environ_to_compression(environ_key: str) -> Optional[Compression]: return _ENVIRON_TO_COMPRESSION[environ_value] -def _translate_key_values(key: Text, value: Any) -> KeyValue: +def _translate_value(value: Any) -> KeyValue: if isinstance(value, bool): any_value = AnyValue(bool_value=value) @@ -96,17 +100,30 @@ def _translate_key_values(key: Text, value: Any) -> KeyValue: any_value = AnyValue(double_value=value) elif isinstance(value, Sequence): - any_value = AnyValue(array_value=value) + any_value = AnyValue( + array_value=ArrayValue(values=[_translate_value(v) for v in value]) + ) - elif isinstance(value, Mapping): - any_value = AnyValue(kvlist_value=value) + # Tracing specs currently does not support Mapping type attributes + # elif isinstance(value, Mapping): + # any_value = AnyValue( + # kvlist_value=KeyValueList( + # values=[ + # _translate_key_values(str(k), v) for k, v in value.items() + # ] + # ) + # ) else: raise Exception( "Invalid type {} of value {}".format(type(value), value) ) - return KeyValue(key=key, value=any_value) + return any_value + + +def _translate_key_values(key: Text, value: Any) -> KeyValue: + return KeyValue(key=key, value=_translate_value(value)) def get_resource_data( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 51cbff9fe8..3f43f1a8cc 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -22,6 +22,9 @@ from google.rpc.error_details_pb2 import RetryInfo from grpc import ChannelCredentials, Compression, StatusCode, server +from opentelemetry.exporter.otlp.proto.grpc.exporter import ( + _translate_key_values, +) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) @@ -35,6 +38,7 @@ ) from opentelemetry.proto.common.v1.common_pb2 import ( AnyValue, + ArrayValue, InstrumentationLibrary, KeyValue, ) @@ -545,6 +549,58 @@ def test_span_status_translate(self): Status.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR, ) + # pylint:disable=no-member + def test_translate_key_values(self): + bool_value = _translate_key_values("bool_type", False) + self.assertTrue(isinstance(bool_value, KeyValue)) + self.assertEqual(bool_value.key, "bool_type") + self.assertTrue(isinstance(bool_value.value, AnyValue)) + self.assertFalse(bool_value.value.bool_value) + + str_value = _translate_key_values("str_type", "str") + self.assertTrue(isinstance(str_value, KeyValue)) + self.assertEqual(str_value.key, "str_type") + self.assertTrue(isinstance(str_value.value, AnyValue)) + self.assertEqual(str_value.value.string_value, "str") + + int_value = _translate_key_values("int_type", 2) + self.assertTrue(isinstance(int_value, KeyValue)) + self.assertEqual(int_value.key, "int_type") + self.assertTrue(isinstance(int_value.value, AnyValue)) + self.assertEqual(int_value.value.int_value, 2) + + double_value = _translate_key_values("double_type", 3.2) + self.assertTrue(isinstance(double_value, KeyValue)) + self.assertEqual(double_value.key, "double_type") + self.assertTrue(isinstance(double_value.value, AnyValue)) + self.assertEqual(double_value.value.double_value, 3.2) + + seq_value = _translate_key_values("seq_type", ["asd", "123"]) + self.assertTrue(isinstance(seq_value, KeyValue)) + self.assertEqual(seq_value.key, "seq_type") + self.assertTrue(isinstance(seq_value.value, AnyValue)) + self.assertTrue(isinstance(seq_value.value.array_value, ArrayValue)) + + arr_value = seq_value.value.array_value + self.assertTrue(isinstance(arr_value.values[0], AnyValue)) + self.assertEqual(arr_value.values[0].string_value, "asd") + self.assertTrue(isinstance(arr_value.values[1], AnyValue)) + self.assertEqual(arr_value.values[1].string_value, "123") + + # Tracing specs currently does not support Mapping type attributes + # map_value = _translate_key_values( + # "map_type", {"asd": "123", "def": "456"} + # ) + # self.assertTrue(isinstance(map_value, KeyValue)) + # self.assertEqual(map_value.key, "map_type") + # self.assertTrue(isinstance(map_value.value, AnyValue)) + # self.assertTrue(isinstance(map_value.value.kvlist_value, KeyValueList)) + + # kvlist_value = map_value.value.kvlist_value + # self.assertTrue(isinstance(kvlist_value.values[0], KeyValue)) + # self.assertEqual(kvlist_value.values[0].key, "asd") + # self.assertEqual(kvlist_value.values[0].value.string_value, "123") + def _create_span_with_status(status: SDKStatus): span = _Span( From d75fb17c1a1e77b61410e5e43f49f8026adfc78f Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 11 May 2021 21:52:41 +0530 Subject: [PATCH 0877/1517] Added B3 single-header propagator (#1823) --- CHANGELOG.md | 6 + .../opentelemetry-propagator-b3/setup.cfg | 4 +- .../opentelemetry/propagators/b3/__init__.py | 54 ++- .../tests/test_b3_format.py | 324 +++++++++++------- 4 files changed, 256 insertions(+), 132 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb6b0b5b93..935da02ad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added example for running Django with auto instrumentation. ([#1803](https://github.com/open-telemetry/opentelemetry-python/pull/1803)) +- Added `B3SingleFormat` and `B3MultiFormat` propagators to the `opentelemetry-propagator-b3` package. + ([#1823](https://github.com/open-telemetry/opentelemetry-python/pull/1823)) - Added support for OTEL_SERVICE_NAME. ([#1829](https://github.com/open-telemetry/opentelemetry-python/pull/1829)) @@ -24,6 +26,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Propagators use the root context as default for `extract` and do not modify the context if extracting from carrier does not work. ([#1811](https://github.com/open-telemetry/opentelemetry-python/pull/1811)) +- Fixed `b3` propagator entrypoint to point to `B3SingleFormat` propagator. + ([#1823](https://github.com/open-telemetry/opentelemetry-python/pull/1823)) +- Added `b3multi` propagator entrypoint to point to `B3MultiFormat` propagator. + ([#1823](https://github.com/open-telemetry/opentelemetry-python/pull/1823)) - Improve warning when failing to decode byte attribute ([#1810](https://github.com/open-telemetry/opentelemetry-python/pull/1810)) - Fixed inconsistency in parent_id formatting from the ConsoleSpanExporter diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 4739f9ff7b..86962a8c81 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -41,6 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 1.2.0.dev0 + deprecated >= 1.2.6 [options.extras_require] test = @@ -50,4 +51,5 @@ where = src [options.entry_points] opentelemetry_propagator = - b3 = opentelemetry.propagators.b3:B3Format \ No newline at end of file + b3 = opentelemetry.propagators.b3:B3SingleFormat + b3multi = opentelemetry.propagators.b3:B3MultiFormat \ No newline at end of file diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py index d0beec401a..c75d8e9a54 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py @@ -15,6 +15,8 @@ import typing from re import compile as re_compile +from deprecated import deprecated + import opentelemetry.trace as trace from opentelemetry.context import Context from opentelemetry.propagators.textmap import ( @@ -28,10 +30,11 @@ from opentelemetry.trace import format_span_id, format_trace_id -class B3Format(TextMapPropagator): - """Propagator for the B3 HTTP header format. +class B3MultiFormat(TextMapPropagator): + """Propagator for the B3 HTTP multi-header format. See: https://github.com/openzipkin/b3-propagation + https://github.com/openzipkin/b3-propagation#multiple-headers """ SINGLE_HEADER_KEY = "b3" @@ -165,6 +168,53 @@ def fields(self) -> typing.Set[str]: } +class B3SingleFormat(B3MultiFormat): + """Propagator for the B3 HTTP single-header format. + + See: https://github.com/openzipkin/b3-propagation + https://github.com/openzipkin/b3-propagation#single-header + """ + + def inject( + self, + carrier: CarrierT, + context: typing.Optional[Context] = None, + setter: Setter = default_setter, + ) -> None: + span = trace.get_current_span(context=context) + + span_context = span.get_span_context() + if span_context == trace.INVALID_SPAN_CONTEXT: + return + + sampled = (trace.TraceFlags.SAMPLED & span_context.trace_flags) != 0 + + fields = [ + format_trace_id(span_context.trace_id), + format_span_id(span_context.span_id), + "1" if sampled else "0", + ] + + span_parent = getattr(span, "parent", None) + if span_parent: + fields.append(format_span_id(span_parent.span_id)) + + setter.set(carrier, self.SINGLE_HEADER_KEY, "-".join(fields)) + + @property + def fields(self) -> typing.Set[str]: + return {self.SINGLE_HEADER_KEY} + + +class B3Format(B3MultiFormat): + @deprecated( + version="1.2.0", + reason="B3Format is deprecated in favor of B3MultiFormat", + ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def _extract_first_element( items: typing.Iterable[CarrierT], ) -> typing.Optional[CarrierT]: diff --git a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index 29d8a472ea..87a059f57e 100644 --- a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -13,21 +13,23 @@ # limitations under the License. import unittest +from abc import abstractclassmethod from unittest.mock import Mock -import opentelemetry.propagators.b3 as b3_format # pylint: disable=no-name-in-module,import-error import opentelemetry.sdk.trace as trace import opentelemetry.sdk.trace.id_generator as id_generator import opentelemetry.trace as trace_api from opentelemetry.context import Context, get_current +from opentelemetry.propagators.b3 import ( # pylint: disable=no-name-in-module,import-error + B3MultiFormat, + B3SingleFormat, +) from opentelemetry.propagators.textmap import DefaultGetter -FORMAT = b3_format.B3Format() +def get_child_parent_new_carrier(old_carrier, propagator): -def get_child_parent_new_carrier(old_carrier): - - ctx = FORMAT.extract(old_carrier) + ctx = propagator.extract(old_carrier) parent_span_context = trace_api.get_current_span(ctx).get_span_context() parent = trace._Span("parent", parent_span_context) @@ -45,24 +47,24 @@ def get_child_parent_new_carrier(old_carrier): new_carrier = {} ctx = trace_api.set_span_in_context(child) - FORMAT.inject(new_carrier, context=ctx) + propagator.inject(new_carrier, context=ctx) return child, parent, new_carrier -class TestB3Format(unittest.TestCase): - # pylint: disable=too-many-public-methods +class AbstractB3FormatTestCase: + # pylint: disable=too-many-public-methods,no-member,invalid-name @classmethod def setUpClass(cls): generator = id_generator.RandomIdGenerator() - cls.serialized_trace_id = b3_format.format_trace_id( + cls.serialized_trace_id = trace_api.format_trace_id( generator.generate_trace_id() ) - cls.serialized_span_id = b3_format.format_span_id( + cls.serialized_span_id = trace_api.format_span_id( generator.generate_span_id() ) - cls.serialized_parent_id = b3_format.format_span_id( + cls.serialized_parent_id = trace_api.format_span_id( generator.generate_span_id() ) @@ -74,56 +76,72 @@ def setUp(self) -> None: patcher.start() self.addCleanup(patcher.stop) + @classmethod + def get_child_parent_new_carrier(cls, old_carrier): + return get_child_parent_new_carrier(old_carrier, cls.get_propagator()) + + @abstractclassmethod + def get_propagator(cls): + pass + + @abstractclassmethod + def get_trace_id(cls, carrier): + pass + + def assertSampled(self, carrier): + pass + + def assertNotSampled(self, carrier): + pass + def test_extract_multi_header(self): """Test the extraction of B3 headers.""" - child, parent, new_carrier = get_child_parent_new_carrier( - { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.PARENT_SPAN_ID_KEY: self.serialized_parent_id, - FORMAT.SAMPLED_KEY: "1", - } - ) + propagator = self.get_propagator() + context = { + propagator.TRACE_ID_KEY: self.serialized_trace_id, + propagator.SPAN_ID_KEY: self.serialized_span_id, + propagator.PARENT_SPAN_ID_KEY: self.serialized_parent_id, + propagator.SAMPLED_KEY: "1", + } + child, parent, _ = self.get_child_parent_new_carrier(context) self.assertEqual( - new_carrier[FORMAT.TRACE_ID_KEY], - b3_format.format_trace_id(child.context.trace_id), - ) - self.assertEqual( - new_carrier[FORMAT.SPAN_ID_KEY], - b3_format.format_span_id(child.context.span_id), + context[propagator.TRACE_ID_KEY], + trace_api.format_trace_id(child.context.trace_id), ) + self.assertEqual( - new_carrier[FORMAT.PARENT_SPAN_ID_KEY], - b3_format.format_span_id(parent.context.span_id), + context[propagator.SPAN_ID_KEY], + trace_api.format_span_id(child.parent.span_id), ) self.assertTrue(parent.context.is_remote) - self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + self.assertTrue(parent.context.trace_flags.sampled) def test_extract_single_header(self): """Test the extraction from a single b3 header.""" - child, parent, new_carrier = get_child_parent_new_carrier( + propagator = self.get_propagator() + child, parent, _ = self.get_child_parent_new_carrier( { - FORMAT.SINGLE_HEADER_KEY: "{}-{}".format( + propagator.SINGLE_HEADER_KEY: "{}-{}".format( self.serialized_trace_id, self.serialized_span_id ) } ) self.assertEqual( - new_carrier[FORMAT.TRACE_ID_KEY], - b3_format.format_trace_id(child.context.trace_id), + self.serialized_trace_id, + trace_api.format_trace_id(child.context.trace_id), ) self.assertEqual( - new_carrier[FORMAT.SPAN_ID_KEY], - b3_format.format_span_id(child.context.span_id), + self.serialized_span_id, + trace_api.format_span_id(child.parent.span_id), ) - self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") self.assertTrue(parent.context.is_remote) + self.assertTrue(parent.context.trace_flags.sampled) - child, parent, new_carrier = get_child_parent_new_carrier( + child, parent, _ = self.get_child_parent_new_carrier( { - FORMAT.SINGLE_HEADER_KEY: "{}-{}-1-{}".format( + propagator.SINGLE_HEADER_KEY: "{}-{}-1-{}".format( self.serialized_trace_id, self.serialized_span_id, self.serialized_parent_id, @@ -132,99 +150,100 @@ def test_extract_single_header(self): ) self.assertEqual( - new_carrier[FORMAT.TRACE_ID_KEY], - b3_format.format_trace_id(child.context.trace_id), - ) - self.assertEqual( - new_carrier[FORMAT.SPAN_ID_KEY], - b3_format.format_span_id(child.context.span_id), + self.serialized_trace_id, + trace_api.format_trace_id(child.context.trace_id), ) self.assertEqual( - new_carrier[FORMAT.PARENT_SPAN_ID_KEY], - b3_format.format_span_id(parent.context.span_id), + self.serialized_span_id, + trace_api.format_span_id(child.parent.span_id), ) + self.assertTrue(parent.context.is_remote) - self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + self.assertTrue(parent.context.trace_flags.sampled) def test_extract_header_precedence(self): """A single b3 header should take precedence over multiple headers. """ + propagator = self.get_propagator() single_header_trace_id = self.serialized_trace_id[:-3] + "123" - _, _, new_carrier = get_child_parent_new_carrier( + _, _, new_carrier = self.get_child_parent_new_carrier( { - FORMAT.SINGLE_HEADER_KEY: "{}-{}".format( + propagator.SINGLE_HEADER_KEY: "{}-{}".format( single_header_trace_id, self.serialized_span_id ), - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.SAMPLED_KEY: "1", + propagator.TRACE_ID_KEY: self.serialized_trace_id, + propagator.SPAN_ID_KEY: self.serialized_span_id, + propagator.SAMPLED_KEY: "1", } ) self.assertEqual( - new_carrier[FORMAT.TRACE_ID_KEY], single_header_trace_id + self.get_trace_id(new_carrier), single_header_trace_id ) def test_enabled_sampling(self): """Test b3 sample key variants that turn on sampling.""" + propagator = self.get_propagator() for variant in ["1", "True", "true", "d"]: - _, _, new_carrier = get_child_parent_new_carrier( + _, _, new_carrier = self.get_child_parent_new_carrier( { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.SAMPLED_KEY: variant, + propagator.TRACE_ID_KEY: self.serialized_trace_id, + propagator.SPAN_ID_KEY: self.serialized_span_id, + propagator.SAMPLED_KEY: variant, } ) - - self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + self.assertSampled(new_carrier) def test_disabled_sampling(self): """Test b3 sample key variants that turn off sampling.""" + propagator = self.get_propagator() for variant in ["0", "False", "false", None]: - _, _, new_carrier = get_child_parent_new_carrier( + _, _, new_carrier = self.get_child_parent_new_carrier( { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.SAMPLED_KEY: variant, + propagator.TRACE_ID_KEY: self.serialized_trace_id, + propagator.SPAN_ID_KEY: self.serialized_span_id, + propagator.SAMPLED_KEY: variant, } ) - - self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "0") + self.assertNotSampled(new_carrier) def test_flags(self): """x-b3-flags set to "1" should result in propagation.""" - _, _, new_carrier = get_child_parent_new_carrier( + propagator = self.get_propagator() + _, _, new_carrier = self.get_child_parent_new_carrier( { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", + propagator.TRACE_ID_KEY: self.serialized_trace_id, + propagator.SPAN_ID_KEY: self.serialized_span_id, + propagator.FLAGS_KEY: "1", } ) - self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + self.assertSampled(new_carrier) def test_flags_and_sampling(self): """Propagate if b3 flags and sampling are set.""" - _, _, new_carrier = get_child_parent_new_carrier( + propagator = self.get_propagator() + _, _, new_carrier = self.get_child_parent_new_carrier( { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", + propagator.TRACE_ID_KEY: self.serialized_trace_id, + propagator.SPAN_ID_KEY: self.serialized_span_id, + propagator.FLAGS_KEY: "1", } ) - self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + self.assertSampled(new_carrier) def test_derived_ctx_is_returned_for_success(self): """Ensure returned context is derived from the given context.""" old_ctx = Context({"k1": "v1"}) - new_ctx = FORMAT.extract( + propagator = self.get_propagator() + new_ctx = propagator.extract( { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", + propagator.TRACE_ID_KEY: self.serialized_trace_id, + propagator.SPAN_ID_KEY: self.serialized_span_id, + propagator.FLAGS_KEY: "1", }, old_ctx, ) @@ -237,7 +256,7 @@ def test_derived_ctx_is_returned_for_success(self): def test_derived_ctx_is_returned_for_failure(self): """Ensure returned context is derived from the given context.""" old_ctx = Context({"k2": "v2"}) - new_ctx = FORMAT.extract({}, old_ctx) + new_ctx = self.get_propagator().extract({}, old_ctx) self.assertNotIn("current-span", new_ctx) for key, value in old_ctx.items(): # pylint:disable=no-member self.assertIn(key, new_ctx) @@ -246,120 +265,131 @@ def test_derived_ctx_is_returned_for_failure(self): def test_64bit_trace_id(self): """64 bit trace ids should be padded to 128 bit trace ids.""" + propagator = self.get_propagator() trace_id_64_bit = self.serialized_trace_id[:16] - _, _, new_carrier = get_child_parent_new_carrier( + _, _, new_carrier = self.get_child_parent_new_carrier( { - FORMAT.TRACE_ID_KEY: trace_id_64_bit, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", - } + propagator.TRACE_ID_KEY: trace_id_64_bit, + propagator.SPAN_ID_KEY: self.serialized_span_id, + propagator.FLAGS_KEY: "1", + }, ) self.assertEqual( - new_carrier[FORMAT.TRACE_ID_KEY], "0" * 16 + trace_id_64_bit + self.get_trace_id(new_carrier), "0" * 16 + trace_id_64_bit ) def test_extract_invalid_single_header_to_explicit_ctx(self): """Given unparsable header, do not modify context""" old_ctx = Context({"k1": "v1"}) + propagator = self.get_propagator() - carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} - new_ctx = FORMAT.extract(carrier, old_ctx) + carrier = {propagator.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} + new_ctx = propagator.extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) def test_extract_invalid_single_header_to_implicit_ctx(self): - carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} - new_ctx = FORMAT.extract(carrier) + propagator = self.get_propagator() + carrier = {propagator.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} + new_ctx = propagator.extract(carrier) self.assertDictEqual(Context(), new_ctx) def test_extract_missing_trace_id_to_explicit_ctx(self): """Given no trace ID, do not modify context""" old_ctx = Context({"k1": "v1"}) + propagator = self.get_propagator() carrier = { - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", + propagator.SPAN_ID_KEY: self.serialized_span_id, + propagator.FLAGS_KEY: "1", } - new_ctx = FORMAT.extract(carrier, old_ctx) + new_ctx = propagator.extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) def test_extract_missing_trace_id_to_implicit_ctx(self): + propagator = self.get_propagator() carrier = { - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", + propagator.SPAN_ID_KEY: self.serialized_span_id, + propagator.FLAGS_KEY: "1", } - new_ctx = FORMAT.extract(carrier) + new_ctx = propagator.extract(carrier) self.assertDictEqual(Context(), new_ctx) def test_extract_invalid_trace_id_to_explicit_ctx(self): """Given invalid trace ID, do not modify context""" old_ctx = Context({"k1": "v1"}) + propagator = self.get_propagator() carrier = { - FORMAT.TRACE_ID_KEY: "abc123", - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", + propagator.TRACE_ID_KEY: "abc123", + propagator.SPAN_ID_KEY: self.serialized_span_id, + propagator.FLAGS_KEY: "1", } - new_ctx = FORMAT.extract(carrier, old_ctx) + new_ctx = propagator.extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) def test_extract_invalid_trace_id_to_implicit_ctx(self): + propagator = self.get_propagator() carrier = { - FORMAT.TRACE_ID_KEY: "abc123", - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", + propagator.TRACE_ID_KEY: "abc123", + propagator.SPAN_ID_KEY: self.serialized_span_id, + propagator.FLAGS_KEY: "1", } - new_ctx = FORMAT.extract(carrier) + new_ctx = propagator.extract(carrier) self.assertDictEqual(Context(), new_ctx) def test_extract_invalid_span_id_to_explicit_ctx(self): """Given invalid span ID, do not modify context""" old_ctx = Context({"k1": "v1"}) + propagator = self.get_propagator() carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: "abc123", - FORMAT.FLAGS_KEY: "1", + propagator.TRACE_ID_KEY: self.serialized_trace_id, + propagator.SPAN_ID_KEY: "abc123", + propagator.FLAGS_KEY: "1", } - new_ctx = FORMAT.extract(carrier, old_ctx) + new_ctx = propagator.extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) def test_extract_invalid_span_id_to_implicit_ctx(self): + propagator = self.get_propagator() carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: "abc123", - FORMAT.FLAGS_KEY: "1", + propagator.TRACE_ID_KEY: self.serialized_trace_id, + propagator.SPAN_ID_KEY: "abc123", + propagator.FLAGS_KEY: "1", } - new_ctx = FORMAT.extract(carrier) + new_ctx = propagator.extract(carrier) self.assertDictEqual(Context(), new_ctx) def test_extract_missing_span_id_to_explicit_ctx(self): """Given no span ID, do not modify context""" old_ctx = Context({"k1": "v1"}) + propagator = self.get_propagator() carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.FLAGS_KEY: "1", + propagator.TRACE_ID_KEY: self.serialized_trace_id, + propagator.FLAGS_KEY: "1", } - new_ctx = FORMAT.extract(carrier, old_ctx) + new_ctx = propagator.extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) def test_extract_missing_span_id_to_implicit_ctx(self): + propagator = self.get_propagator() carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.FLAGS_KEY: "1", + propagator.TRACE_ID_KEY: self.serialized_trace_id, + propagator.FLAGS_KEY: "1", } - new_ctx = FORMAT.extract(carrier) + new_ctx = propagator.extract(carrier) self.assertDictEqual(Context(), new_ctx) @@ -368,54 +398,90 @@ def test_extract_empty_carrier_to_explicit_ctx(self): old_ctx = Context({"k1": "v1"}) carrier = {} - new_ctx = FORMAT.extract(carrier, old_ctx) + new_ctx = self.get_propagator().extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) def test_extract_empty_carrier_to_implicit_ctx(self): - new_ctx = FORMAT.extract({}) + new_ctx = self.get_propagator().extract({}) self.assertDictEqual(Context(), new_ctx) - @staticmethod - def test_inject_empty_context(): + def test_inject_empty_context(self): """If the current context has no span, don't add headers""" new_carrier = {} - FORMAT.inject(new_carrier, get_current()) + self.get_propagator().inject(new_carrier, get_current()) assert len(new_carrier) == 0 - @staticmethod - def test_default_span(): + def test_default_span(self): """Make sure propagator does not crash when working with NonRecordingSpan""" class CarrierGetter(DefaultGetter): def get(self, carrier, key): return carrier.get(key, None) - ctx = FORMAT.extract({}, getter=CarrierGetter()) - FORMAT.inject({}, context=ctx) + propagator = self.get_propagator() + ctx = propagator.extract({}, getter=CarrierGetter()) + propagator.inject({}, context=ctx) def test_fields(self): """Make sure the fields attribute returns the fields used in inject""" + propagator = self.get_propagator() tracer = trace.TracerProvider().get_tracer("sdk_tracer_provider") mock_setter = Mock() with tracer.start_as_current_span("parent"): with tracer.start_as_current_span("child"): - FORMAT.inject({}, setter=mock_setter) + propagator.inject({}, setter=mock_setter) inject_fields = set() for call in mock_setter.mock_calls: inject_fields.add(call[1][1]) - self.assertEqual(FORMAT.fields, inject_fields) + self.assertEqual(propagator.fields, inject_fields) def test_extract_none_context(self): """Given no trace ID, do not modify context""" old_ctx = None carrier = {} - new_ctx = FORMAT.extract(carrier, old_ctx) + new_ctx = self.get_propagator().extract(carrier, old_ctx) self.assertDictEqual(Context(), new_ctx) + + +class TestB3MultiFormat(AbstractB3FormatTestCase, unittest.TestCase): + @classmethod + def get_propagator(cls): + return B3MultiFormat() + + @classmethod + def get_trace_id(cls, carrier): + return carrier[cls.get_propagator().TRACE_ID_KEY] + + def assertSampled(self, carrier): + self.assertEqual(carrier[self.get_propagator().SAMPLED_KEY], "1") + + def assertNotSampled(self, carrier): + self.assertEqual(carrier[self.get_propagator().SAMPLED_KEY], "0") + + +class TestB3SingleFormat(AbstractB3FormatTestCase, unittest.TestCase): + @classmethod + def get_propagator(cls): + return B3SingleFormat() + + @classmethod + def get_trace_id(cls, carrier): + return carrier[cls.get_propagator().SINGLE_HEADER_KEY].split("-")[0] + + def assertSampled(self, carrier): + self.assertEqual( + carrier[self.get_propagator().SINGLE_HEADER_KEY].split("-")[2], "1" + ) + + def assertNotSampled(self, carrier): + self.assertEqual( + carrier[self.get_propagator().SINGLE_HEADER_KEY].split("-")[2], "0" + ) From f5f3f2a03e92df4a3d5b474e5a395edb6fa523f5 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 11 May 2021 14:16:47 -0700 Subject: [PATCH 0878/1517] improving the release process for maintainers (#1840) --- RELEASING.md | 31 +++++++------- eachdist.ini | 40 ++++++++++++++++++ scripts/eachdist.py | 83 ++++++++++++++++++++++++++++---------- scripts/prepare_release.sh | 13 +----- 4 files changed, 119 insertions(+), 48 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 945c947d89..904f8cb4a1 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -3,6 +3,7 @@ This document explains how to publish all OT modules at version x.y.z. Ensure th Release Process: * [Checkout a clean repo](#checkout-a-clean-repo) +* [Update versions](#update-versions) * [Create a new branch](#create-a-new-branch) * [Open a Pull Request](#open-a-pull-request) * [Create a Release](#Create-a-Release) @@ -13,28 +14,32 @@ Release Process: * [Troubleshooting](#troubleshooting) ## Checkout a clean repo -- To avoid pushing untracked changes, check out the repo in a new dir +To avoid pushing untracked changes, check out the repo in a new dir + +## Update versions +The update of the version information relies on the information in eachdist.ini to identify which packages are stable, prerelease or +experimental. Update the desired version there to begin the release process. ## Create a new branch The following script does the following: - update main locally - creates a new release branch `release/` - updates version and changelog files -- commits the change to a new branch `release/-auto` +- commits the change *NOTE: This script was run by a GitHub Action but required the Action bot to be excluded from the CLA check, which it currently is not.* ```bash -./scripts/prepare_release.sh 0.7b0 +./scripts/prepare_release.sh ``` ## Open a Pull Request -The PR should be opened from the `release/-auto` branch created as part of running `prepare_release.sh` in the steps above. +The PR should be opened from the `release/` branch created as part of running `prepare_release.sh` in the steps above. ## Create a Release -- Create the GH release from the release branch, using a new tag for this micro version, e.g. `v0.7.0` +- Create the GH release from the main branch, using a new tag for this micro version, e.g. `v0.7.0` - Copy the changelogs from all packages that changed into the release notes (and reformat to remove hard line wraps) @@ -42,7 +47,7 @@ The PR should be opened from the `release/-auto` branch created as part This should be handled automatically on release by the [publish action](https://github.com/open-telemetry/opentelemetry-python/blob/main/.github/workflows/publish.yml). - - Check the [action logs](https://github.com/open-telemetry/opentelemetry-python/actions?query=workflow%3APublish) to make sure packages have been uploaded to PyPI +- Check the [action logs](https://github.com/open-telemetry/opentelemetry-python/actions?query=workflow%3APublish) to make sure packages have been uploaded to PyPI - Check the release history (e.g. https://pypi.org/project/opentelemetry-api/#history) on PyPI If for some reason the action failed, see [Publish failed](#publish-failed) below @@ -61,19 +66,13 @@ To validate this worked, ensure the stable build has run successfully: https://r ## Update main -Ensure the version and changelog updates have been applied to main. - +Ensure the version and changelog updates have been applied to main. Update the versions in eachdist.ini once again this time to include the `.dev0` tag and +run eachdist once again: ```bash -# checkout a new branch from main -git checkout -b v0.7b0-main-update -# cherry pick the change from the release branch -git cherry-pick $(git log -n 1 origin/release/0.7b0 --format="%H") -# update the version number, make it a "dev" greater than release number, e.g. 0.8.dev0 -perl -i -p -e 's/0.7b0/0.8.dev0/' $(git grep -l "0.7b0" | grep -vi CHANGELOG) -# open a PR targeting main see #331 -git commit -m +./scripts/eachdist.py update_versions --versions stable,prerelease ``` + ## Update website docs If the docs for the Opentelemetry [website](https://opentelemetry.io/docs/python/) was updated in this release in this [folder](https://github.com/open-telemetry/opentelemetry-python/tree/main/website_docs), submit a [PR](https://github.com/open-telemetry/opentelemetry.io/tree/main/content/en/docs/python) to update the docs on the website accordingly. To check if the new version requires updating, run the following script and compare the diff: diff --git a/eachdist.ini b/eachdist.ini index d2093491b9..62d89b5ff5 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -12,6 +12,46 @@ sortfirst= tests/util exporter/* +[stable] +version=1.2.0.dev0 + +packages= + opentelemetry-sdk + opentelemetry-proto + propagator/opentelemetry-propagator-jaeger + propagator/opentelemetry-propagator-b3 + exporter/opentelemetry-exporter-zipkin-proto-http + exporter/opentelemetry-exporter-zipkin-json + exporter/opentelemetry-exporter-zipkin + exporter/opentelemetry-exporter-otlp-proto-grpc + exporter/opentelemetry-exporter-otlp + exporter/opentelemetry-exporter-jaeger-thrift + exporter/opentelemetry-exporter-jaeger-proto-grpc + exporter/opentelemetry-exporter-jaeger + opentelemetry-api + +[prerelease] +version=0.21.dev0 + +packages= + opentelemetry-opentracing-shim + exporter/opentelemetry-exporter-opencensus + opentelemetry-distro + opentelemetry-semantic-conventions + opentelemetry-test + opentelemetry-instrumentation + +[experimental] +version=1.10a0 + +packages= + exporter/opentelemetry-exporter-prometheus-remote-write + exporter/opentelemetry-exporter-prometheus + opentelemetry-api + opentelemetry-sdk + exporter/opentelemetry-exporter-otlp-proto-grpc + exporter/opentelemetry-exporter-otlp + [lintroots] extraroots=examples/*,scripts/ subglob=*.py,tests/,test/,src/*,examples/* diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 1cb8033d62..db1ce77d85 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -229,11 +229,11 @@ def setup_instparser(instparser): ) releaseparser = subparsers.add_parser( - "release", - help="Prepares release, used by maintainers and CI", + "update_versions", + help="Updates version numbers, used by maintainers and CI", ) releaseparser.set_defaults(func=release_args) - releaseparser.add_argument("--version", required=True) + releaseparser.add_argument("--versions", required=True) releaseparser.add_argument( "releaseargs", nargs=argparse.REMAINDER, help=extraargs_help("pytest") ) @@ -249,6 +249,21 @@ def setup_instparser(instparser): help="Format only this path instead of entire repository", ) + versionparser = subparsers.add_parser( + "version", + help="Get the version for a release", + ) + versionparser.set_defaults(func=version_args) + versionparser.add_argument( + "--mode", + "-m", + default="DEFAULT", + help=cleandoc( + """Section of config file to use for target selection configuration. + See description of exec for available options.""" + ), + ) + return parser.parse_args(args) @@ -589,29 +604,40 @@ def find(name, path): return None -def update_version_files(targets, version): +def filter_packages(targets, packages): + filtered_packages = [] + for target in targets: + for pkg in packages: + if pkg in str(target): + filtered_packages.append(target) + break + return filtered_packages + + +def update_version_files(targets, version, packages): print("updating version.py files") + targets = filter_packages(targets, packages) update_files( targets, - version, "version.py", "__version__ .*", '__version__ = "{}"'.format(version), ) -def update_dependencies(targets, version): +def update_dependencies(targets, version, packages): print("updating dependencies") - update_files( - targets, - version, - "setup.cfg", - r"(opentelemetry-.*)==(.*)", - r"\1== " + version, - ) + for pkg in packages: + package_name = pkg.split("/")[-1] + update_files( + targets, + "setup.cfg", + r"({}.*)==(.*)".format(package_name), + r"\1== " + version, + ) -def update_files(targets, version, filename, search, replace): +def update_files(targets, filename, search, replace): errors = False for target in targets: curr_file = find(filename, target) @@ -622,9 +648,8 @@ def update_files(targets, version, filename, search, replace): with open(curr_file) as _file: text = _file.read() - if version in text: - print("{} already contans version {}".format(curr_file, version)) - errors = True + if replace in text: + print("{} already contains {}".format(curr_file, replace)) continue with open(curr_file, "w") as _file: @@ -639,10 +664,20 @@ def release_args(args): rootpath = find_projectroot() targets = list(find_targets_unordered(rootpath)) - version = args.version - update_dependencies(targets, version) - update_version_files(targets, version) - update_changelogs(version) + cfg = ConfigParser() + cfg.read(str(find_projectroot() / "eachdist.ini")) + versions = args.versions + updated_versions = [] + for group in versions.split(","): + mcfg = cfg[group] + version = mcfg["version"] + updated_versions.append(version) + packages = mcfg["packages"].split() + print("update {} packages to {}".format(group, version)) + update_dependencies(targets, version, packages) + update_version_files(targets, version, packages) + + update_changelogs("-".join(updated_versions)) def test_args(args): @@ -679,6 +714,12 @@ def format_args(args): ) +def version_args(args): + cfg = ConfigParser() + cfg.read(str(find_projectroot() / "eachdist.ini")) + print(cfg[args.mode]["version"]) + + def main(): args = parse_args() args.func(args) diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh index 18fe8e8a17..607b6189bb 100755 --- a/scripts/prepare_release.sh +++ b/scripts/prepare_release.sh @@ -11,24 +11,15 @@ # triggering unnecessary pull requests # -VERSION=`echo $1 | awk -F "/" '{print $NF}'` +VERSION=$(./scripts/eachdist.py version --mode stable)-$(./scripts/eachdist.py version --mode prerelease) echo "Using version ${VERSION}" -# check the version matches expected versioning e.g -# 0.6, 0.6b, 0.6b0, 0.6.0 -if [[ ! "${VERSION}" =~ ^([0-9])(\.*[0-9]{1,5}[a-b]*){1,3}$ ]]; then - echo "Version number invalid: $VERSION" - exit 1 -fi # create the release branch -git fetch origin main -git checkout main -git reset --hard origin/main git checkout -b release/${VERSION} git push origin release/${VERSION} -./scripts/eachdist.py release --version ${VERSION} +./scripts/eachdist.py update_versions --versions stable,prerelease rc=$? if [ $rc != 0 ]; then echo "::set-output name=version_updated::0" From dad4b2ba42a7094f285b6eabf6d86d3ecad3afc5 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 11 May 2021 20:58:24 -0700 Subject: [PATCH 0879/1517] updating changelogs and version to 1.2.0, 0.21b0 (#1841) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 14 ++++++-------- .../error_handler/error_handler_0/setup.cfg | 2 +- .../error_handler/error_handler_1/setup.cfg | 2 +- eachdist.ini | 5 +++-- .../setup.cfg | 4 ++-- .../exporter/jaeger/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-jaeger-thrift/setup.cfg | 4 ++-- .../exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../opentelemetry/exporter/opencensus/version.py | 2 +- .../setup.cfg | 6 +++--- .../exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../opentelemetry-exporter-zipkin-json/setup.cfg | 4 ++-- .../opentelemetry/exporter/zipkin/json/version.py | 2 +- .../setup.cfg | 6 +++--- .../exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 8 ++++---- .../src/opentelemetry/distro/version.py | 2 +- .../src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- propagator/opentelemetry-propagator-b3/setup.cfg | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../opentelemetry-propagator-jaeger/setup.cfg | 2 +- .../opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 4 ++-- .../opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/util/setup.cfg | 4 ++-- tests/util/src/opentelemetry/test/version.py | 2 +- 38 files changed, 61 insertions(+), 62 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 732d78aeb5..3190da5260 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: bcd337f74db582684e29fd5e0a9a2de591566c9b + CONTRIB_REPO_SHA: 6613312c1a5a8cf30511d4f4dd899417059aa1d2 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 935da02ad0..16610f70b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.1.0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.2.0-0.21b0...HEAD) -### Changed -- Include span parent in Jaeger gRPC export as `CHILD_OF` reference - ([#1809])(https://github.com/open-telemetry/opentelemetry-python/pull/1809) +## [1.2.0, 0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0-0.21b0) - 2021-05-11 ### Added - Added example for running Django with auto instrumentation. @@ -34,15 +32,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1810](https://github.com/open-telemetry/opentelemetry-python/pull/1810)) - Fixed inconsistency in parent_id formatting from the ConsoleSpanExporter ([#1833](https://github.com/open-telemetry/opentelemetry-python/pull/1833)) +- Include span parent in Jaeger gRPC export as `CHILD_OF` reference + ([#1809])(https://github.com/open-telemetry/opentelemetry-python/pull/1809) +- Fixed sequence values in OTLP exporter not translating + ([#1818](https://github.com/open-telemetry/opentelemetry-python/pull/1818)) ### Removed - Moved `opentelemetry-instrumentation` to contrib repository. ([#1797](https://github.com/open-telemetry/opentelemetry-python/pull/1797)) -### Changed -- Fixed sequence values in OTLP exporter not translating - ([#1818](https://github.com/open-telemetry/opentelemetry-python/pull/1818)) - ## [1.1.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.1.0) - 2021-04-20 ### Added diff --git a/docs/examples/error_handler/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg index 6b9410dd73..31670ca2b5 100644 --- a/docs/examples/error_handler/error_handler_0/setup.cfg +++ b/docs/examples/error_handler/error_handler_0/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.2.0.dev0 + opentelemetry-sdk == 1.2.0 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg index 2802576928..7c64b892a7 100644 --- a/docs/examples/error_handler/error_handler_1/setup.cfg +++ b/docs/examples/error_handler/error_handler_1/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.2.0.dev0 + opentelemetry-sdk == 1.2.0 [options.packages.find] where = src diff --git a/eachdist.ini b/eachdist.ini index 62d89b5ff5..6892d884ff 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -13,7 +13,7 @@ sortfirst= exporter/* [stable] -version=1.2.0.dev0 +version=1.2.0 packages= opentelemetry-sdk @@ -31,7 +31,7 @@ packages= opentelemetry-api [prerelease] -version=0.21.dev0 +version=0.21b0 packages= opentelemetry-opentracing-shim @@ -40,6 +40,7 @@ packages= opentelemetry-semantic-conventions opentelemetry-test opentelemetry-instrumentation + tests [experimental] version=1.10a0 diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index 7dfada5a7d..2a3ce93f4b 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 - opentelemetry-api == 1.2.0.dev0 - opentelemetry-sdk == 1.2.0.dev0 + opentelemetry-api == 1.2.0 + opentelemetry-sdk == 1.2.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index fa725c3d71..9749d4fcf5 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0.dev0" +__version__ = "1.2.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index 56f310d074..a03430f7af 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 1.2.0.dev0 - opentelemetry-sdk == 1.2.0.dev0 + opentelemetry-api == 1.2.0 + opentelemetry-sdk == 1.2.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index fa725c3d71..9749d4fcf5 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0.dev0" +__version__ = "1.2.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 802525fb4b..d716a9a96b 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,8 +39,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.2.0.dev0 - opentelemetry-exporter-jaeger-thrift == 1.2.0.dev0 + opentelemetry-exporter-jaeger-proto-grpc == 1.2.0 + opentelemetry-exporter-jaeger-thrift == 1.2.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index fa725c3d71..9749d4fcf5 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0.dev0" +__version__ = "1.2.0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 31d6cc353d..8242327123 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 1.2.0.dev0 - opentelemetry-sdk == 1.2.0.dev0 + opentelemetry-api == 1.2.0 + opentelemetry-sdk == 1.2.0 protobuf >= 3.13.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 2b08175266..df84e5962e 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.21.dev0" +__version__ = "0.21b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 8d7b9bf96c..8d30692ab3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 - opentelemetry-api == 1.2.0.dev0 - opentelemetry-sdk == 1.2.0.dev0 - opentelemetry-proto == 1.2.0.dev0 + opentelemetry-api == 1.2.0 + opentelemetry-sdk == 1.2.0 + opentelemetry-proto == 1.2.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 2f082c8e9a..a1383bb691 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0.dev0" +__version__ = "1.2.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index d8bb421e4e..8ea891dda4 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -38,4 +38,4 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.2.0.dev0 + opentelemetry-exporter-otlp-proto-grpc == 1.2.0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 2f082c8e9a..a1383bb691 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0.dev0" +__version__ = "1.2.0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index e477eddfa2..205c7287be 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -42,8 +42,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 1.2.0.dev0 - opentelemetry-sdk == 1.2.0.dev0 + opentelemetry-api == 1.2.0 + opentelemetry-sdk == 1.2.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 2f082c8e9a..a1383bb691 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0.dev0" +__version__ = "1.2.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 636a545df7..bd286f15a8 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -43,9 +43,9 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 - opentelemetry-api == 1.2.0.dev0 - opentelemetry-sdk == 1.2.0.dev0 - opentelemetry-exporter-zipkin-json == 1.2.0.dev0 + opentelemetry-api == 1.2.0 + opentelemetry-sdk == 1.2.0 + opentelemetry-exporter-zipkin-json == 1.2.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 2f082c8e9a..a1383bb691 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0.dev0" +__version__ = "1.2.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 862be54288..a277a9d1a3 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.2.0.dev0 - opentelemetry-exporter-zipkin-proto-http == 1.2.0.dev0 + opentelemetry-exporter-zipkin-json == 1.2.0 + opentelemetry-exporter-zipkin-proto-http == 1.2.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 2f082c8e9a..a1383bb691 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0.dev0" +__version__ = "1.2.0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 2f082c8e9a..a1383bb691 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0.dev0" +__version__ = "1.2.0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 6bcf8b0cff..92c5cddc79 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.2.0.dev0 - opentelemetry-instrumentation == 0.21.dev0 - opentelemetry-sdk == 1.2.0.dev0 + opentelemetry-api == 1.2.0 + opentelemetry-instrumentation == 0.21b0 + opentelemetry-sdk == 1.2.0 [options.packages.find] where = src @@ -57,4 +57,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.2.0.dev0 + opentelemetry-exporter-otlp == 1.2.0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index 2b08175266..df84e5962e 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.21.dev0" +__version__ = "0.21b0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 2f082c8e9a..a1383bb691 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0.dev0" +__version__ = "1.2.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 00f2deee8c..55a752b246 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.2.0.dev0 - opentelemetry-semantic-conventions == 0.21.dev0 + opentelemetry-api == 1.2.0 + opentelemetry-semantic-conventions == 0.21b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 2f082c8e9a..a1383bb691 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0.dev0" +__version__ = "1.2.0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 2b08175266..df84e5962e 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.21.dev0" +__version__ = "0.21b0" diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 86962a8c81..307d3df67f 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.2.0.dev0 + opentelemetry-api == 1.2.0 deprecated >= 1.2.6 [options.extras_require] diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 2f082c8e9a..a1383bb691 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0.dev0" +__version__ = "1.2.0" diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index f93f5a3c0a..d8722ae945 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.2.0.dev0 + opentelemetry-api == 1.2.0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 2f082c8e9a..a1383bb691 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0.dev0" +__version__ = "1.2.0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 536cf04451..f6a0435690 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 1.2.0.dev0 + opentelemetry-api == 1.2.0 [options.extras_require] test = - opentelemetry-test == 0.21.dev0 + opentelemetry-test == 0.21b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 2b08175266..df84e5962e 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.21.dev0" +__version__ = "0.21b0" diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index 399ae8df1c..106bbffca8 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -37,8 +37,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.2.0.dev0 - opentelemetry-sdk == 1.2.0.dev0 + opentelemetry-api == 1.2.0 + opentelemetry-sdk == 1.2.0 [options.extras_require] test = flask~=1.0 diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 0c0996a295..12d411757c 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.21.dev0" +__version__ = "0.21b0" From 42213c41e98b2c058b53ba1b3ff3525e2e86a7ff Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 12 May 2021 11:30:09 -0700 Subject: [PATCH 0880/1517] [chore] bump 1.3.0.dev0 & 0.22.dev0 versions (#1843) --- .github/workflows/test.yml | 2 +- docs/examples/error_handler/error_handler_0/setup.cfg | 2 +- docs/examples/error_handler/error_handler_1/setup.cfg | 2 +- eachdist.ini | 4 ++-- .../opentelemetry-exporter-jaeger-proto-grpc/setup.cfg | 4 ++-- .../opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-json/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/setup.cfg | 6 +++--- .../opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 8 ++++---- opentelemetry-distro/src/opentelemetry/distro/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- propagator/opentelemetry-propagator-b3/setup.cfg | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- propagator/opentelemetry-propagator-jaeger/setup.cfg | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 4 ++-- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/util/setup.cfg | 4 ++-- tests/util/src/opentelemetry/test/version.py | 2 +- 37 files changed, 54 insertions(+), 54 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3190da5260..33413f3213 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 6613312c1a5a8cf30511d4f4dd899417059aa1d2 + CONTRIB_REPO_SHA: c46b39e4e94af5ae8f97e684960f1aec0595c33e jobs: build: diff --git a/docs/examples/error_handler/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg index 31670ca2b5..f80c814cb5 100644 --- a/docs/examples/error_handler/error_handler_0/setup.cfg +++ b/docs/examples/error_handler/error_handler_0/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.2.0 + opentelemetry-sdk == 1.3.0.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg index 7c64b892a7..5fb21fdcaf 100644 --- a/docs/examples/error_handler/error_handler_1/setup.cfg +++ b/docs/examples/error_handler/error_handler_1/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.2.0 + opentelemetry-sdk == 1.3.0.dev0 [options.packages.find] where = src diff --git a/eachdist.ini b/eachdist.ini index 6892d884ff..121ae93dab 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -13,7 +13,7 @@ sortfirst= exporter/* [stable] -version=1.2.0 +version=1.3.0.dev0 packages= opentelemetry-sdk @@ -31,7 +31,7 @@ packages= opentelemetry-api [prerelease] -version=0.21b0 +version=0.22.dev0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index 2a3ce93f4b..d6c5e2bf1c 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 - opentelemetry-api == 1.2.0 - opentelemetry-sdk == 1.2.0 + opentelemetry-api == 1.3.0.dev0 + opentelemetry-sdk == 1.3.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 9749d4fcf5..7369c05815 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index a03430f7af..2adcbf0805 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 1.2.0 - opentelemetry-sdk == 1.2.0 + opentelemetry-api == 1.3.0.dev0 + opentelemetry-sdk == 1.3.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 9749d4fcf5..7369c05815 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index d716a9a96b..02bcc2e243 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,8 +39,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.2.0 - opentelemetry-exporter-jaeger-thrift == 1.2.0 + opentelemetry-exporter-jaeger-proto-grpc == 1.3.0.dev0 + opentelemetry-exporter-jaeger-thrift == 1.3.0.dev0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 9749d4fcf5..7369c05815 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0.dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 8242327123..7294615212 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 1.2.0 - opentelemetry-sdk == 1.2.0 + opentelemetry-api == 1.3.0.dev0 + opentelemetry-sdk == 1.3.0.dev0 protobuf >= 3.13.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index df84e5962e..ba922d2bed 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.21b0" +__version__ = "0.22.dev0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 8d30692ab3..09349faaba 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 - opentelemetry-api == 1.2.0 - opentelemetry-sdk == 1.2.0 - opentelemetry-proto == 1.2.0 + opentelemetry-api == 1.3.0.dev0 + opentelemetry-sdk == 1.3.0.dev0 + opentelemetry-proto == 1.3.0.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index a1383bb691..f94a1f8a6c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0.dev0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 8ea891dda4..5502be5f8e 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -38,4 +38,4 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.2.0 + opentelemetry-exporter-otlp-proto-grpc == 1.3.0.dev0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index a1383bb691..f94a1f8a6c 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index 205c7287be..db2586c6da 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -42,8 +42,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 1.2.0 - opentelemetry-sdk == 1.2.0 + opentelemetry-api == 1.3.0.dev0 + opentelemetry-sdk == 1.3.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index a1383bb691..f94a1f8a6c 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index bd286f15a8..202c37df0e 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -43,9 +43,9 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 - opentelemetry-api == 1.2.0 - opentelemetry-sdk == 1.2.0 - opentelemetry-exporter-zipkin-json == 1.2.0 + opentelemetry-api == 1.3.0.dev0 + opentelemetry-sdk == 1.3.0.dev0 + opentelemetry-exporter-zipkin-json == 1.3.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index a1383bb691..f94a1f8a6c 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index a277a9d1a3..2574034923 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.2.0 - opentelemetry-exporter-zipkin-proto-http == 1.2.0 + opentelemetry-exporter-zipkin-json == 1.3.0.dev0 + opentelemetry-exporter-zipkin-proto-http == 1.3.0.dev0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index a1383bb691..f94a1f8a6c 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index a1383bb691..f94a1f8a6c 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0.dev0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 92c5cddc79..02b69aa7c1 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.2.0 - opentelemetry-instrumentation == 0.21b0 - opentelemetry-sdk == 1.2.0 + opentelemetry-api == 1.3.0.dev0 + opentelemetry-instrumentation == 0.22.dev0 + opentelemetry-sdk == 1.3.0.dev0 [options.packages.find] where = src @@ -57,4 +57,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.2.0 + opentelemetry-exporter-otlp == 1.3.0.dev0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index df84e5962e..ba922d2bed 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.21b0" +__version__ = "0.22.dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index a1383bb691..f94a1f8a6c 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0.dev0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 55a752b246..14863f633a 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.2.0 - opentelemetry-semantic-conventions == 0.21b0 + opentelemetry-api == 1.3.0.dev0 + opentelemetry-semantic-conventions == 0.22.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index a1383bb691..f94a1f8a6c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0.dev0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index df84e5962e..ba922d2bed 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.21b0" +__version__ = "0.22.dev0" diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 307d3df67f..bedcdab8f2 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.2.0 + opentelemetry-api == 1.3.0.dev0 deprecated >= 1.2.6 [options.extras_require] diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index a1383bb691..f94a1f8a6c 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0.dev0" diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index d8722ae945..75c61b77ac 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.2.0 + opentelemetry-api == 1.3.0.dev0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index a1383bb691..f94a1f8a6c 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0.dev0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index f6a0435690..24528123f3 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 1.2.0 + opentelemetry-api == 1.3.0.dev0 [options.extras_require] test = - opentelemetry-test == 0.21b0 + opentelemetry-test == 0.22.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index df84e5962e..ba922d2bed 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.21b0" +__version__ = "0.22.dev0" diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index 106bbffca8..e94c832a5c 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -37,8 +37,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.2.0 - opentelemetry-sdk == 1.2.0 + opentelemetry-api == 1.3.0.dev0 + opentelemetry-sdk == 1.3.0.dev0 [options.extras_require] test = flask~=1.0 diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 12d411757c..7f955c0090 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.21b0" +__version__ = "0.22.dev0" From f816287e5ff0e2d18a1001e11fd0ad7e070e3b2e Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 14 May 2021 22:08:38 +0530 Subject: [PATCH 0881/1517] Update transient errors retry timeout and retryable status codes (#1842) --- CHANGELOG.md | 2 ++ .../opentelemetry/exporter/otlp/proto/grpc/exporter.py | 9 ++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16610f70b1..e4fb4c5b20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1809])(https://github.com/open-telemetry/opentelemetry-python/pull/1809) - Fixed sequence values in OTLP exporter not translating ([#1818](https://github.com/open-telemetry/opentelemetry-python/pull/1818)) +- Update transient errors retry timeout and retryable status codes + ([#1842](https://github.com/open-telemetry/opentelemetry-python/pull/1842)) ### Removed - Moved `opentelemetry-instrumentation` to contrib repository. diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index f48a473fd0..be58cd8258 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -262,14 +262,11 @@ def _translate_data( pass def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: + + max_value = 64 # expo returns a generator that yields delay values which grow # exponentially. Once delay is greater than max_value, the yielded # value will remain constant. - # max_value is set to 900 (900 seconds is 15 minutes) to use the same - # value as used in the Go implementation. - - max_value = 900 - for delay in expo(max_value=max_value): if delay == max_value: @@ -289,8 +286,6 @@ def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: if error.code() in [ StatusCode.CANCELLED, StatusCode.DEADLINE_EXCEEDED, - StatusCode.PERMISSION_DENIED, - StatusCode.UNAUTHENTICATED, StatusCode.RESOURCE_EXHAUSTED, StatusCode.ABORTED, StatusCode.OUT_OF_RANGE, From 7e1f2473b123ae105f590320fb8d0f0b09b77062 Mon Sep 17 00:00:00 2001 From: Eddy Lin Date: Thu, 20 May 2021 10:55:29 -0400 Subject: [PATCH 0882/1517] Update gettracer (#1854) --- CHANGELOG.md | 4 ++++ .../src/opentelemetry/sdk/trace/__init__.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4fb4c5b20..485b6453ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.2.0-0.21b0...HEAD) +### Changed +- Updated get_tracer to return an empty string when passed an invalid name + ([#1854](https://github.com/open-telemetry/opentelemetry-python/pull/1854)) + ## [1.2.0, 0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0-0.21b0) - 2021-05-11 ### Added diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index ece7da6d22..ad1d8284cd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -997,7 +997,7 @@ def get_tracer( instrumenting_library_version: str = "", ) -> "trace_api.Tracer": if not instrumenting_module_name: # Reject empty strings too. - instrumenting_module_name = "ERROR:MISSING MODULE NAME" + instrumenting_module_name = "" logger.error("get_tracer called with missing module name.") return Tracer( self.sampler, diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index f12dc7c75c..50d8d2ae85 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -257,17 +257,25 @@ def test_invalid_instrumentation_info(self): tracer1 = tracer_provider.get_tracer("") with self.assertLogs(level=ERROR): tracer2 = tracer_provider.get_tracer(None) - self.assertEqual( - tracer1.instrumentation_info, tracer2.instrumentation_info - ) + self.assertIsInstance( tracer1.instrumentation_info, InstrumentationInfo ) span1 = tracer1.start_span("foo") self.assertTrue(span1.is_recording()) self.assertEqual(tracer1.instrumentation_info.version, "") + self.assertEqual(tracer1.instrumentation_info.name, "") + + self.assertIsInstance( + tracer2.instrumentation_info, InstrumentationInfo + ) + span2 = tracer2.start_span("bar") + self.assertTrue(span2.is_recording()) + self.assertEqual(tracer2.instrumentation_info.version, "") + self.assertEqual(tracer2.instrumentation_info.name, "") + self.assertEqual( - tracer1.instrumentation_info.name, "ERROR:MISSING MODULE NAME" + tracer1.instrumentation_info, tracer2.instrumentation_info ) def test_span_processor_for_source(self): From 2098c5644588cd4e766eb8e31a1ca26c4e00ea46 Mon Sep 17 00:00:00 2001 From: Eddy Lin Date: Fri, 21 May 2021 16:15:12 -0400 Subject: [PATCH 0883/1517] Change AttributeValue sequences from optional to nonoptional (#1855) --- CHANGELOG.md | 2 ++ opentelemetry-api/src/opentelemetry/util/types.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 485b6453ca..9b9022d5f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Updated get_tracer to return an empty string when passed an invalid name ([#1854](https://github.com/open-telemetry/opentelemetry-python/pull/1854)) +- Changed AttributeValue sequences to warn mypy users on adding None values to array + ([#1855](https://github.com/open-telemetry/opentelemetry-python/pull/1855)) ## [1.2.0, 0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0-0.21b0) - 2021-05-11 diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index ec72a28704..be171ef0ea 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -20,10 +20,10 @@ bool, int, float, - Sequence[Optional[str]], - Sequence[Optional[bool]], - Sequence[Optional[int]], - Sequence[Optional[float]], + Sequence[str], + Sequence[bool], + Sequence[int], + Sequence[float], ] Attributes = Optional[Mapping[str, AttributeValue]] AttributesAsKey = Tuple[ From ef7a415204963b89cdb98974d9a46b53bc762f5b Mon Sep 17 00:00:00 2001 From: Christian Kruse <33990804+c-kruse@users.noreply.github.com> Date: Mon, 24 May 2021 09:29:51 -0700 Subject: [PATCH 0884/1517] Trace SDK does not raise when Span attribute and link limits are exceeded (#1856) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/util/__init__.py | 11 ++---- opentelemetry-sdk/tests/test_util.py | 11 +++--- opentelemetry-sdk/tests/trace/test_trace.py | 34 +++++++++++++++---- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b9022d5f7..c5a8de2443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1818](https://github.com/open-telemetry/opentelemetry-python/pull/1818)) - Update transient errors retry timeout and retryable status codes ([#1842](https://github.com/open-telemetry/opentelemetry-python/pull/1842)) +- Fix start span behavior when excess links and attributes are included + ([#1856](https://github.com/open-telemetry/opentelemetry-python/pull/1856)) ### Removed - Moved `opentelemetry-instrumentation` to contrib repository. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index 1bb8ed264f..981368049f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -81,11 +81,8 @@ def extend(self, seq): @classmethod def from_seq(cls, maxlen, seq): seq = tuple(seq) - if len(seq) > maxlen: - raise ValueError bounded_list = cls(maxlen) - # pylint: disable=protected-access - bounded_list._dq = deque(seq, maxlen=maxlen) + bounded_list.extend(seq) return bounded_list @@ -140,9 +137,7 @@ def __len__(self): @classmethod def from_map(cls, maxlen, mapping): mapping = OrderedDict(mapping) - if len(mapping) > maxlen: - raise ValueError bounded_dict = cls(maxlen) - # pylint: disable=protected-access - bounded_dict._dict = mapping + for key, value in mapping.items(): + bounded_dict[key] = value return bounded_dict diff --git a/opentelemetry-sdk/tests/test_util.py b/opentelemetry-sdk/tests/test_util.py index cacc77dbd9..e003e70789 100644 --- a/opentelemetry-sdk/tests/test_util.py +++ b/opentelemetry-sdk/tests/test_util.py @@ -61,8 +61,9 @@ def test_from_seq(self): self.assertEqual(len(tuple(blist)), list_len) # sequence too big - with self.assertRaises(ValueError): - BoundedList.from_seq(list_len / 2, self.base) + blist = BoundedList.from_seq(list_len // 2, base_copy) + self.assertEqual(len(blist), list_len // 2) + self.assertEqual(blist.dropped, list_len - (list_len // 2)) def test_append_no_drop(self): """Append max capacity elements to the list without dropping elements.""" @@ -167,8 +168,10 @@ def test_from_map(self): self.assertEqual(len(tuple(bdict)), dic_len) # map too big - with self.assertRaises(ValueError): - BoundedDict.from_map(dic_len / 2, self.base) + half_len = dic_len // 2 + bdict = BoundedDict.from_map(half_len, self.base) + self.assertEqual(len(tuple(bdict)), half_len) + self.assertEqual(bdict.dropped, dic_len - half_len) def test_bounded_dict(self): # create empty dict diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 50d8d2ae85..5a713c4cfa 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -21,8 +21,6 @@ from typing import Optional from unittest import mock -import pytest - from opentelemetry import trace as trace_api from opentelemetry.context import Context from opentelemetry.sdk import resources, trace @@ -538,6 +536,26 @@ def test_disallow_direct_span_creation(self): # pylint: disable=abstract-class-instantiated trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + def test_surplus_span_links(self): + # pylint: disable=protected-access + max_links = trace._SPAN_LINK_COUNT_LIMIT + links = [ + trace_api.Link(trace_api.SpanContext(0x1, idx, is_remote=False)) + for idx in range(0, 16 + max_links) + ] + tracer = new_tracer() + with tracer.start_as_current_span("span", links=links) as root: + self.assertEqual(len(root.links), max_links) + + def test_surplus_span_attributes(self): + max_attrs = trace.SPAN_ATTRIBUTE_COUNT_LIMIT + attributes = {str(idx): idx for idx in range(0, 16 + max_attrs)} + tracer = new_tracer() + with tracer.start_as_current_span( + "span", attributes=attributes + ) as root: + self.assertEqual(len(root.attributes), max_attrs) + class TestSpan(unittest.TestCase): # pylint: disable=too-many-public-methods @@ -1303,11 +1321,15 @@ def test_span_environment_limits(self): ) for _ in range(100) ] - with pytest.raises(ValueError): - with tracer.start_as_current_span("root", links=some_links): - pass - with tracer.start_as_current_span("root") as root: + some_attrs = { + "init_attribute_{}".format(idx): idx for idx in range(100) + } + with tracer.start_as_current_span( + "root", links=some_links, attributes=some_attrs + ) as root: + self.assertEqual(len(root.links), 30) + self.assertEqual(len(root.attributes), 10) for idx in range(100): root.set_attribute("my_attribute_{}".format(idx), 0) root.add_event("my_event_{}".format(idx)) From eda57f7705333fa58c0e0422b809e1b683d0a2cf Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 25 May 2021 03:52:50 +0530 Subject: [PATCH 0885/1517] Apply validation of attributes to `Resource`, move attribute related logic to util package (#1834) --- CHANGELOG.md | 2 + .../src/opentelemetry/attributes/__init__.py | 110 ++++++++++++++++++ .../tests/attributes/test_attributes.py | 87 ++++++++++++++ .../opentelemetry/sdk/resources/__init__.py | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 78 ++----------- .../tests/resources/test_resources.py | 20 ++++ opentelemetry-sdk/tests/trace/test_trace.py | 29 ----- 7 files changed, 229 insertions(+), 99 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/attributes/__init__.py create mode 100644 opentelemetry-api/tests/attributes/test_attributes.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c5a8de2443..ed90e63bc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1818](https://github.com/open-telemetry/opentelemetry-python/pull/1818)) - Update transient errors retry timeout and retryable status codes ([#1842](https://github.com/open-telemetry/opentelemetry-python/pull/1842)) +- Apply validation of attributes to `Resource`, move attribute related logic to separate package. + ([#1834](https://github.com/open-telemetry/opentelemetry-python/pull/1834)) - Fix start span behavior when excess links and attributes are included ([#1856](https://github.com/open-telemetry/opentelemetry-python/pull/1856)) diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py new file mode 100644 index 0000000000..6875f56631 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -0,0 +1,110 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +import logging +from types import MappingProxyType +from typing import MutableSequence, Sequence + +from opentelemetry.util import types + +_VALID_ATTR_VALUE_TYPES = (bool, str, int, float) + + +_logger = logging.getLogger(__name__) + + +def _is_valid_attribute_value(value: types.AttributeValue) -> bool: + """Checks if attribute value is valid. + + An attribute value is valid if it is either: + - A primitive type: string, boolean, double precision floating + point (IEEE 754-1985) or integer. + - An array of primitive type values. The array MUST be homogeneous, + i.e. it MUST NOT contain values of different types. + """ + + if isinstance(value, Sequence): + if len(value) == 0: + return True + + sequence_first_valid_type = None + for element in value: + if element is None: + continue + element_type = type(element) + if element_type not in _VALID_ATTR_VALUE_TYPES: + _logger.warning( + "Invalid type %s in attribute value sequence. Expected one of " + "%s or None", + element_type.__name__, + [ + valid_type.__name__ + for valid_type in _VALID_ATTR_VALUE_TYPES + ], + ) + return False + # The type of the sequence must be homogeneous. The first non-None + # element determines the type of the sequence + if sequence_first_valid_type is None: + sequence_first_valid_type = element_type + elif not isinstance(element, sequence_first_valid_type): + _logger.warning( + "Mixed types %s and %s in attribute value sequence", + sequence_first_valid_type.__name__, + type(element).__name__, + ) + return False + + elif not isinstance(value, _VALID_ATTR_VALUE_TYPES): + _logger.warning( + "Invalid type %s for attribute value. Expected one of %s or a " + "sequence of those types", + type(value).__name__, + [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES], + ) + return False + return True + + +def _filter_attributes(attributes: types.Attributes) -> None: + """Applies attribute validation rules and drops (key, value) pairs + that doesn't adhere to attributes specification. + + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/common.md#attributes. + """ + if attributes: + for attr_key, attr_value in list(attributes.items()): + if not attr_key: + _logger.warning("invalid key `%s` (empty or null)", attr_key) + attributes.pop(attr_key) + continue + + if _is_valid_attribute_value(attr_value): + if isinstance(attr_value, MutableSequence): + attributes[attr_key] = tuple(attr_value) + if isinstance(attr_value, bytes): + try: + attributes[attr_key] = attr_value.decode() + except ValueError: + attributes.pop(attr_key) + _logger.warning("Byte attribute could not be decoded.") + else: + attributes.pop(attr_key) + + +def _create_immutable_attributes( + attributes: types.Attributes, +) -> types.Attributes: + return MappingProxyType(attributes.copy() if attributes else {}) diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py new file mode 100644 index 0000000000..2a391f78af --- /dev/null +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -0,0 +1,87 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# type: ignore + +import unittest + +from opentelemetry.attributes import ( + _create_immutable_attributes, + _filter_attributes, + _is_valid_attribute_value, +) + + +class TestAttributes(unittest.TestCase): + def test_is_valid_attribute_value(self): + self.assertFalse(_is_valid_attribute_value([1, 2, 3.4, "ss", 4])) + self.assertFalse(_is_valid_attribute_value([dict(), 1, 2, 3.4, 4])) + self.assertFalse(_is_valid_attribute_value(["sw", "lf", 3.4, "ss"])) + self.assertFalse(_is_valid_attribute_value([1, 2, 3.4, 5])) + self.assertFalse(_is_valid_attribute_value(dict())) + self.assertTrue(_is_valid_attribute_value(True)) + self.assertTrue(_is_valid_attribute_value("hi")) + self.assertTrue(_is_valid_attribute_value(3.4)) + self.assertTrue(_is_valid_attribute_value(15)) + self.assertTrue(_is_valid_attribute_value([1, 2, 3, 5])) + self.assertTrue(_is_valid_attribute_value([1.2, 2.3, 3.4, 4.5])) + self.assertTrue(_is_valid_attribute_value([True, False])) + self.assertTrue(_is_valid_attribute_value(["ss", "dw", "fw"])) + self.assertTrue(_is_valid_attribute_value([])) + # None in sequences are valid + self.assertTrue(_is_valid_attribute_value(["A", None, None])) + self.assertTrue(_is_valid_attribute_value(["A", None, None, "B"])) + self.assertTrue(_is_valid_attribute_value([None, None])) + self.assertFalse(_is_valid_attribute_value(["A", None, 1])) + self.assertFalse(_is_valid_attribute_value([None, "A", None, 1])) + + def test_filter_attributes(self): + attrs_with_invalid_keys = { + "": "empty-key", + None: "None-value", + "attr-key": "attr-value", + } + _filter_attributes(attrs_with_invalid_keys) + self.assertTrue(len(attrs_with_invalid_keys), 1) + self.assertEqual(attrs_with_invalid_keys, {"attr-key": "attr-value"}) + + attrs_with_invalid_values = { + "nonhomogeneous": [1, 2, 3.4, "ss", 4], + "nonprimitive": dict(), + "mixed": [1, 2.4, "st", dict()], + "validkey1": "validvalue1", + "intkey": 5, + "floatkey": 3.14, + "boolkey": True, + "valid-byte-string": b"hello-otel", + } + _filter_attributes(attrs_with_invalid_values) + self.assertEqual(len(attrs_with_invalid_values), 5) + self.assertEqual( + attrs_with_invalid_values, + { + "validkey1": "validvalue1", + "intkey": 5, + "floatkey": 3.14, + "boolkey": True, + "valid-byte-string": "hello-otel", + }, + ) + + def test_create_immutable_attributes(self): + attrs = {"key": "value", "pi": 3.14} + immutable = _create_immutable_attributes(attrs) + # TypeError: 'mappingproxy' object does not support item assignment + with self.assertRaises(TypeError): + immutable["pi"] = 1.34 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 77240b8ef8..8755b2d1f9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -64,6 +64,7 @@ import pkg_resources +from opentelemetry.attributes import _filter_attributes from opentelemetry.sdk.environment_variables import ( OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, @@ -141,6 +142,7 @@ class Resource: """A Resource is an immutable representation of the entity producing telemetry as Attributes.""" def __init__(self, attributes: Attributes): + _filter_attributes(attributes) self._attributes = attributes.copy() @staticmethod diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index ad1d8284cd..cc0114a22d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -39,6 +39,11 @@ from opentelemetry import context as context_api from opentelemetry import trace as trace_api +from opentelemetry.attributes import ( + _create_immutable_attributes, + _filter_attributes, + _is_valid_attribute_value, +) from opentelemetry.sdk import util from opentelemetry.sdk.environment_variables import ( OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, @@ -64,7 +69,6 @@ _SPAN_EVENT_COUNT_LIMIT = int(environ.get(OTEL_SPAN_EVENT_COUNT_LIMIT, 128)) _SPAN_LINK_COUNT_LIMIT = int(environ.get(OTEL_SPAN_LINK_COUNT_LIMIT, 128)) -_VALID_ATTR_VALUE_TYPES = (bool, str, int, float) # pylint: disable=protected-access _TRACE_SAMPLER = sampling._get_from_env_or_default() @@ -315,72 +319,6 @@ def attributes(self) -> types.Attributes: return self._attributes -def _is_valid_attribute_value(value: types.AttributeValue) -> bool: - """Checks if attribute value is valid. - - An attribute value is valid if it is one of the valid types. - If the value is a sequence, it is only valid if all items in the sequence: - - are of the same valid type or None - - are not a sequence - """ - - if isinstance(value, Sequence): - if len(value) == 0: - return True - - sequence_first_valid_type = None - for element in value: - if element is None: - continue - element_type = type(element) - if element_type not in _VALID_ATTR_VALUE_TYPES: - logger.warning( - "Invalid type %s in attribute value sequence. Expected one of " - "%s or None", - element_type.__name__, - [ - valid_type.__name__ - for valid_type in _VALID_ATTR_VALUE_TYPES - ], - ) - return False - # The type of the sequence must be homogeneous. The first non-None - # element determines the type of the sequence - if sequence_first_valid_type is None: - sequence_first_valid_type = element_type - elif not isinstance(element, sequence_first_valid_type): - logger.warning( - "Mixed types %s and %s in attribute value sequence", - sequence_first_valid_type.__name__, - type(element).__name__, - ) - return False - - elif not isinstance(value, _VALID_ATTR_VALUE_TYPES): - logger.warning( - "Invalid type %s for attribute value. Expected one of %s or a " - "sequence of those types", - type(value).__name__, - [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES], - ) - return False - return True - - -def _filter_attribute_values(attributes: types.Attributes): - if attributes: - for attr_key, attr_value in list(attributes.items()): - if _is_valid_attribute_value(attr_value): - if isinstance(attr_value, MutableSequence): - attributes[attr_key] = tuple(attr_value) - else: - attributes.pop(attr_key) - - -def _create_immutable_attributes(attributes): - return MappingProxyType(attributes.copy() if attributes else {}) - - def _check_span_ended(func): def wrapper(self, *args, **kwargs): already_ended = False @@ -623,7 +561,7 @@ def __init__( self._span_processor = span_processor self._lock = threading.Lock() - _filter_attribute_values(attributes) + _filter_attributes(attributes) if not attributes: self._attributes = self._new_attributes() else: @@ -634,7 +572,7 @@ def __init__( self._events = self._new_events() if events: for event in events: - _filter_attribute_values(event.attributes) + _filter_attributes(event.attributes) # pylint: disable=protected-access event._attributes = _create_immutable_attributes( event.attributes @@ -709,7 +647,7 @@ def add_event( attributes: types.Attributes = None, timestamp: Optional[int] = None, ) -> None: - _filter_attribute_values(attributes) + _filter_attributes(attributes) attributes = _create_immutable_attributes(attributes) self._add_event( Event( diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 211187f1eb..87b4006ce7 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -16,6 +16,7 @@ import os import unittest +import uuid from unittest import mock from opentelemetry.sdk import resources @@ -138,6 +139,25 @@ def test_service_name_using_process_name(self): "unknown_service:test", ) + def test_invalid_resource_attribute_values(self): + resource = resources.Resource( + { + resources.SERVICE_NAME: "test", + "non-primitive-data-type": dict(), + "invalid-byte-type-attribute": b"\xd8\xe1\xb7\xeb\xa8\xe5 \xd2\xb7\xe1", + "": "empty-key-value", + None: "null-key-value", + "another-non-primitive": uuid.uuid4(), + } + ) + self.assertEqual( + resource.attributes, + { + resources.SERVICE_NAME: "test", + }, + ) + self.assertEqual(len(resource.attributes), 1) + def test_aggregated_resources_no_detectors(self): aggregated_resources = resources.get_aggregated_resources([]) self.assertEqual(aggregated_resources, resources.Resource.get_empty()) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 5a713c4cfa..a8b8330fe8 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -671,35 +671,6 @@ def test_byte_type_attribute_value(self): isinstance(root.attributes["valid-byte-type-attribute"], str) ) - def test_check_attribute_helper(self): - # pylint: disable=protected-access - self.assertFalse(trace._is_valid_attribute_value([1, 2, 3.4, "ss", 4])) - self.assertFalse( - trace._is_valid_attribute_value([dict(), 1, 2, 3.4, 4]) - ) - self.assertFalse( - trace._is_valid_attribute_value(["sw", "lf", 3.4, "ss"]) - ) - self.assertFalse(trace._is_valid_attribute_value([1, 2, 3.4, 5])) - self.assertTrue(trace._is_valid_attribute_value([1, 2, 3, 5])) - self.assertTrue(trace._is_valid_attribute_value([1.2, 2.3, 3.4, 4.5])) - self.assertTrue(trace._is_valid_attribute_value([True, False])) - self.assertTrue(trace._is_valid_attribute_value(["ss", "dw", "fw"])) - self.assertTrue(trace._is_valid_attribute_value([])) - self.assertFalse(trace._is_valid_attribute_value(dict())) - self.assertTrue(trace._is_valid_attribute_value(True)) - self.assertTrue(trace._is_valid_attribute_value("hi")) - self.assertTrue(trace._is_valid_attribute_value(3.4)) - self.assertTrue(trace._is_valid_attribute_value(15)) - # None in sequences are valid - self.assertTrue(trace._is_valid_attribute_value(["A", None, None])) - self.assertTrue( - trace._is_valid_attribute_value(["A", None, None, "B"]) - ) - self.assertTrue(trace._is_valid_attribute_value([None, None])) - self.assertFalse(trace._is_valid_attribute_value(["A", None, 1])) - self.assertFalse(trace._is_valid_attribute_value([None, "A", None, 1])) - def test_sampling_attributes(self): sampling_attributes = { "sampler-attr": "sample-val", From 3dbbd1b7b2574a4c51254b6d7abb76f8097869d8 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 25 May 2021 09:29:59 +0530 Subject: [PATCH 0886/1517] Allow users to "unset" SDK limits and evaluate default limits lazily instead of on import (#1839) --- CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 120 +++++++++++++++--- .../src/opentelemetry/sdk/util/__init__.py | 30 +++-- opentelemetry-sdk/tests/test_util.py | 16 +++ opentelemetry-sdk/tests/trace/test_trace.py | 95 +++++++++++++- 5 files changed, 229 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed90e63bc8..5477ae7940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1823](https://github.com/open-telemetry/opentelemetry-python/pull/1823)) - Added support for OTEL_SERVICE_NAME. ([#1829](https://github.com/open-telemetry/opentelemetry-python/pull/1829)) +- Lazily read/configure limits and allow limits to be unset. + ([#1839](https://github.com/open-telemetry/opentelemetry-python/pull/1839)) ### Changed - Fixed OTLP gRPC exporter silently failing if scheme is not specified in endpoint. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index cc0114a22d..c202f5cbea 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -56,19 +56,16 @@ from opentelemetry.sdk.util import BoundedDict, BoundedList from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanContext -from opentelemetry.trace.propagation import SPAN_KEY from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types from opentelemetry.util._time import _time_ns logger = logging.getLogger(__name__) -SPAN_ATTRIBUTE_COUNT_LIMIT = int( - environ.get(OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, 128) -) +_DEFAULT_SPAN_EVENTS_LIMIT = 128 +_DEFAULT_SPAN_LINKS_LIMIT = 128 +_DEFAULT_SPAN_ATTRIBUTES_LIMIT = 128 -_SPAN_EVENT_COUNT_LIMIT = int(environ.get(OTEL_SPAN_EVENT_COUNT_LIMIT, 128)) -_SPAN_LINK_COUNT_LIMIT = int(environ.get(OTEL_SPAN_LINK_COUNT_LIMIT, 128)) # pylint: disable=protected-access _TRACE_SAMPLER = sampling._get_from_env_or_default() @@ -502,6 +499,87 @@ def _format_links(links): return f_links +class _Limits: + """The limits that should be enforce on recorded data such as events, links, attributes etc. + + This class does not enforce any limits itself. It only provides an a way read limits from env, + default values and in future from user provided arguments. + + All limit must be either a non-negative integer or ``None``. + Setting a limit to ``None`` will not set any limits for that field/type. + + Args: + max_events: Maximum number of events that can be added to a Span. + max_links: Maximum number of links that can be added to a Span. + max_attributes: Maximum number of attributes that can be added to a Span. + """ + + UNSET = -1 + + max_attributes: int + max_events: int + max_links: int + + def __init__( + self, + max_attributes: Optional[int] = None, + max_events: Optional[int] = None, + max_links: Optional[int] = None, + ): + self.max_attributes = self._from_env_if_absent( + max_attributes, + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + _DEFAULT_SPAN_ATTRIBUTES_LIMIT, + ) + self.max_events = self._from_env_if_absent( + max_events, OTEL_SPAN_EVENT_COUNT_LIMIT, _DEFAULT_SPAN_EVENTS_LIMIT + ) + self.max_links = self._from_env_if_absent( + max_links, OTEL_SPAN_LINK_COUNT_LIMIT, _DEFAULT_SPAN_LINKS_LIMIT + ) + + def __repr__(self): + return "max_attributes={}, max_events={}, max_links={}".format( + self.max_attributes, self.max_events, self.max_links + ) + + @classmethod + def _from_env_if_absent( + cls, value: Optional[int], env_var: str, default: Optional[int] + ) -> Optional[int]: + if value is cls.UNSET: + return None + + err_msg = "{0} must be a non-negative integer but got {}" + + if value is None: + str_value = environ.get(env_var, "").strip().lower() + if not str_value: + return default + if str_value == "unset": + return None + + try: + value = int(str_value) + except ValueError: + raise ValueError(err_msg.format(env_var, str_value)) + + if value < 0: + raise ValueError(err_msg.format(env_var, value)) + return value + + +_UnsetLimits = _Limits( + max_attributes=_Limits.UNSET, + max_events=_Limits.UNSET, + max_links=_Limits.UNSET, +) + +SPAN_ATTRIBUTE_COUNT_LIMIT = _Limits._from_env_if_absent( + None, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_SPAN_ATTRIBUTES_LIMIT +) + + class Span(trace_api.Span, ReadableSpan): """See `opentelemetry.trace.Span`. @@ -566,7 +644,7 @@ def __init__( self._attributes = self._new_attributes() else: self._attributes = BoundedDict.from_map( - SPAN_ATTRIBUTE_COUNT_LIMIT, attributes + self._limits.max_attributes, attributes ) self._events = self._new_events() @@ -582,24 +660,21 @@ def __init__( if links is None: self._links = self._new_links() else: - self._links = BoundedList.from_seq(_SPAN_LINK_COUNT_LIMIT, links) + self._links = BoundedList.from_seq(self._limits.max_links, links) def __repr__(self): return '{}(name="{}", context={})'.format( type(self).__name__, self._name, self._context ) - @staticmethod - def _new_attributes(): - return BoundedDict(SPAN_ATTRIBUTE_COUNT_LIMIT) + def _new_attributes(self): + return BoundedDict(self._limits.max_attributes) - @staticmethod - def _new_events(): - return BoundedList(_SPAN_EVENT_COUNT_LIMIT) + def _new_events(self): + return BoundedList(self._limits.max_events) - @staticmethod - def _new_links(): - return BoundedList(_SPAN_LINK_COUNT_LIMIT) + def _new_links(self): + return BoundedList(self._limits.max_links) def get_span_context(self): return self._context @@ -772,6 +847,10 @@ class _Span(Span): by other mechanisms than through the `Tracer`. """ + def __init__(self, *args, limits=_UnsetLimits, **kwargs): + self._limits = limits + super().__init__(*args, **kwargs) + class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`.""" @@ -791,6 +870,7 @@ def __init__( self.span_processor = span_processor self.id_generator = id_generator self.instrumentation_info = instrumentation_info + self._limits = None @contextmanager def start_as_current_span( @@ -892,6 +972,7 @@ def start_span( # pylint: disable=too-many-locals instrumentation_info=self.instrumentation_info, record_exception=record_exception, set_status_on_exception=set_status_on_exception, + limits=self._limits, ) span.start(start_time=start_time, parent_context=context) else: @@ -921,6 +1002,7 @@ def __init__( self.id_generator = id_generator self._resource = resource self.sampler = sampler + self._limits = _Limits() self._atexit_handler = None if shutdown_on_exit: self._atexit_handler = atexit.register(self.shutdown) @@ -937,7 +1019,7 @@ def get_tracer( if not instrumenting_module_name: # Reject empty strings too. instrumenting_module_name = "" logger.error("get_tracer called with missing module name.") - return Tracer( + tracer = Tracer( self.sampler, self.resource, self._active_span_processor, @@ -946,6 +1028,8 @@ def get_tracer( instrumenting_module_name, instrumenting_library_version ), ) + tracer._limits = self._limits + return tracer def add_span_processor(self, span_processor: SpanProcessor) -> None: """Registers a new :class:`SpanProcessor` for this `TracerProvider`. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index 981368049f..746f100277 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -16,6 +16,7 @@ import threading from collections import OrderedDict, deque from collections.abc import MutableMapping, Sequence +from typing import Optional def ns_to_iso_str(nanoseconds): @@ -45,7 +46,7 @@ class BoundedList(Sequence): not enough room. """ - def __init__(self, maxlen): + def __init__(self, maxlen: Optional[int]): self.dropped = 0 self._dq = deque(maxlen=maxlen) # type: deque self._lock = threading.Lock() @@ -67,15 +68,19 @@ def __iter__(self): def append(self, item): with self._lock: - if len(self._dq) == self._dq.maxlen: + if ( + self._dq.maxlen is not None + and len(self._dq) == self._dq.maxlen + ): self.dropped += 1 self._dq.append(item) def extend(self, seq): with self._lock: - to_drop = len(seq) + len(self._dq) - self._dq.maxlen - if to_drop > 0: - self.dropped += to_drop + if self._dq.maxlen is not None: + to_drop = len(seq) + len(self._dq) - self._dq.maxlen + if to_drop > 0: + self.dropped += to_drop self._dq.extend(seq) @classmethod @@ -93,11 +98,12 @@ class BoundedDict(MutableMapping): added. """ - def __init__(self, maxlen): - if not isinstance(maxlen, int): - raise ValueError - if maxlen < 0: - raise ValueError + def __init__(self, maxlen: Optional[int]): + if maxlen is not None: + if not isinstance(maxlen, int): + raise ValueError + if maxlen < 0: + raise ValueError self.maxlen = maxlen self.dropped = 0 self._dict = OrderedDict() # type: OrderedDict @@ -113,13 +119,13 @@ def __getitem__(self, key): def __setitem__(self, key, value): with self._lock: - if self.maxlen == 0: + if self.maxlen is not None and self.maxlen == 0: self.dropped += 1 return if key in self._dict: del self._dict[key] - elif len(self._dict) == self.maxlen: + elif self.maxlen is not None and len(self._dict) == self.maxlen: del self._dict[next(iter(self._dict.keys()))] self.dropped += 1 self._dict[key] = value diff --git a/opentelemetry-sdk/tests/test_util.py b/opentelemetry-sdk/tests/test_util.py index e003e70789..f90576afe7 100644 --- a/opentelemetry-sdk/tests/test_util.py +++ b/opentelemetry-sdk/tests/test_util.py @@ -135,6 +135,14 @@ def test_extend_drop(self): self.assertEqual(len(blist), list_len) self.assertEqual(blist.dropped, len(other_list)) + def test_no_limit(self): + blist = BoundedList(maxlen=None) + for num in range(100): + blist.append(num) + + for num in range(100): + self.assertEqual(blist[num], num) + class TestBoundedDict(unittest.TestCase): base = collections.OrderedDict( @@ -214,3 +222,11 @@ def test_bounded_dict(self): with self.assertRaises(KeyError): _ = bdict["new-name"] + + def test_no_limit_code(self): + bdict = BoundedDict(maxlen=None) + for num in range(100): + bdict[num] = num + + for num in range(100): + self.assertEqual(bdict[num], num) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index a8b8330fe8..c9c523c3fa 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -18,6 +18,7 @@ import unittest from importlib import reload from logging import ERROR, WARNING +from random import randint from typing import Optional from unittest import mock @@ -538,7 +539,7 @@ def test_disallow_direct_span_creation(self): def test_surplus_span_links(self): # pylint: disable=protected-access - max_links = trace._SPAN_LINK_COUNT_LIMIT + max_links = trace._Limits().max_links links = [ trace_api.Link(trace_api.SpanContext(0x1, idx, is_remote=False)) for idx in range(0, 16 + max_links) @@ -548,7 +549,8 @@ def test_surplus_span_links(self): self.assertEqual(len(root.links), max_links) def test_surplus_span_attributes(self): - max_attrs = trace.SPAN_ATTRIBUTE_COUNT_LIMIT + # pylint: disable=protected-access + max_attrs = trace._Limits().max_attributes attributes = {str(idx): idx for idx in range(0, 16 + max_attrs)} tracer = new_tracer() with tracer.start_as_current_span( @@ -1270,6 +1272,50 @@ def test_attributes_to_json(self): class TestSpanLimits(unittest.TestCase): + # pylint: disable=protected-access + + def test_limits_defaults(self): + limits = trace._Limits() + self.assertEqual( + limits.max_attributes, trace._DEFAULT_SPAN_ATTRIBUTES_LIMIT + ) + self.assertEqual(limits.max_events, trace._DEFAULT_SPAN_EVENTS_LIMIT) + self.assertEqual(limits.max_links, trace._DEFAULT_SPAN_LINKS_LIMIT) + + def test_limits_values_code(self): + max_attributes, max_events, max_links = ( + randint(0, 10000), + randint(0, 10000), + randint(0, 10000), + ) + limits = trace._Limits( + max_attributes=max_attributes, + max_events=max_events, + max_links=max_links, + ) + self.assertEqual(limits.max_attributes, max_attributes) + self.assertEqual(limits.max_events, max_events) + self.assertEqual(limits.max_links, max_links) + + def test_limits_values_env(self): + max_attributes, max_events, max_links = ( + randint(0, 10000), + randint(0, 10000), + randint(0, 10000), + ) + with mock.patch.dict( + "os.environ", + { + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: str(max_attributes), + OTEL_SPAN_EVENT_COUNT_LIMIT: str(max_events), + OTEL_SPAN_LINK_COUNT_LIMIT: str(max_links), + }, + ): + limits = trace._Limits() + self.assertEqual(limits.max_attributes, max_attributes) + self.assertEqual(limits.max_events, max_events) + self.assertEqual(limits.max_links, max_links) + @mock.patch.dict( "os.environ", { @@ -1278,8 +1324,7 @@ class TestSpanLimits(unittest.TestCase): OTEL_SPAN_LINK_COUNT_LIMIT: "30", }, ) - def test_span_environment_limits(self): - reload(trace) + def test_span_limits_env(self): tracer = new_tracer() id_generator = RandomIdGenerator() some_links = [ @@ -1307,3 +1352,45 @@ def test_span_environment_limits(self): self.assertEqual(len(root.attributes), 10) self.assertEqual(len(root.events), 20) + + @mock.patch.dict( + "os.environ", + { + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "unset", + OTEL_SPAN_EVENT_COUNT_LIMIT: "unset", + OTEL_SPAN_LINK_COUNT_LIMIT: "unset", + }, + ) + def test_span_no_limits_env(self): + num_links = int(trace._DEFAULT_SPAN_LINKS_LIMIT) + randint(1, 100) + + tracer = new_tracer() + id_generator = RandomIdGenerator() + some_links = [ + trace_api.Link( + trace_api.SpanContext( + trace_id=id_generator.generate_trace_id(), + span_id=id_generator.generate_span_id(), + is_remote=False, + ) + ) + for _ in range(num_links) + ] + with tracer.start_as_current_span("root", links=some_links) as root: + self.assertEqual(len(root.links), num_links) + + num_events = int(trace._DEFAULT_SPAN_EVENTS_LIMIT) + randint(1, 100) + with tracer.start_as_current_span("root") as root: + for idx in range(num_events): + root.add_event("my_event_{}".format(idx)) + + self.assertEqual(len(root.events), num_events) + + num_attributes = int(trace._DEFAULT_SPAN_ATTRIBUTES_LIMIT) + randint( + 1, 100 + ) + with tracer.start_as_current_span("root") as root: + for idx in range(num_attributes): + root.set_attribute("my_attribute_{}".format(idx), 0) + + self.assertEqual(len(root.attributes), num_attributes) From 1470a8c1ad8b4f7138ab98f7d366cf0dc155e6b0 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 25 May 2021 14:15:27 -0600 Subject: [PATCH 0887/1517] Add public symbols checker script (#1816) --- .github/workflows/public-api-check.yml | 40 +++++++++++ .pylintrc | 5 +- CONTRIBUTING.md | 8 +++ scripts/public_symbols_checker.py | 94 ++++++++++++++++++++++++++ tox.ini | 12 +++- 5 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/public-api-check.yml create mode 100644 scripts/public_symbols_checker.py diff --git a/.github/workflows/public-api-check.yml b/.github/workflows/public-api-check.yml new file mode 100644 index 0000000000..e151c40cf0 --- /dev/null +++ b/.github/workflows/public-api-check.yml @@ -0,0 +1,40 @@ +name: Public API check + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + branches: + - main + +jobs: + publicAPICheck: + + runs-on: ubuntu-latest + + if: "!contains(github.event.pull_request.labels.*.name, 'Skip Public API check')" + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Checkout main + run: git checkout main + + - name: Pull origin + run: git pull --rebase=false origin main + + - name: Checkout pull request + run: git checkout ${{ github.event.pull_request.head.sha }} + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Install tox + run: pip install -U tox-factor + + - name: Public API Check + run: tox -e public-symbols-check diff --git a/.pylintrc b/.pylintrc index 5daedb0139..cf61fde78d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -73,8 +73,9 @@ disable=missing-docstring, exec-used, super-with-arguments, # temp-pylint-upgrade isinstance-second-argument-not-valid-type, # temp-pylint-upgrade - raise-missing-from, # temp-pylint-upgrade - unused-argument, # temp-pylint-upgrade + raise-missing-from, # temp-pylint-upgrade + unused-argument, # temp-pylint-upgrade + redefined-builtin, # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c6e72244d..e74979761a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,6 +69,14 @@ You can run: Python version - `tox -e lint` to run lint checks on all code +We try to keep the amount of _public symbols_ in our code minimal. A public symbol is any Python identifier that does not start with an underscore. +Every public symbol is something that has to be kept in order to maintain backwards compatibility, so we try to have as few as possible. + +To check if your PR is adding public symbols, run `tox -e public-symbols-check`. This will always fail if public symbols are being added. The idea +behind this is that every PR that adds public symbols fails in CI, forcing reviewers to check the symbols to make sure they are strictly necessary. +If after checking them, it is considered that they are indeed necessary, the PR will be labeled with `Skip Public API check` so that this check is not +run. + See [`tox.ini`](https://github.com/open-telemetry/opentelemetry-python/blob/main/tox.ini) for more detail on available tox commands. diff --git a/scripts/public_symbols_checker.py b/scripts/public_symbols_checker.py new file mode 100644 index 0000000000..bdc530e95d --- /dev/null +++ b/scripts/public_symbols_checker.py @@ -0,0 +1,94 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from difflib import unified_diff +from pathlib import Path +from re import match +from sys import exit + +from git import Repo +from git.db import GitDB + +repo = Repo(__file__, odbt=GitDB, search_parent_directories=True) + + +file_path_symbols = {} + + +def get_symbols(change_type, diff_lines_getter, prefix): + for diff_lines in ( + repo.commit("main") + .diff(repo.head.commit) + .iter_change_type(change_type) + ): + + b_file_path = diff_lines.b_blob.path + + if ( + Path(b_file_path).suffix != ".py" + or "opentelemetry" not in b_file_path + ): + continue + + for diff_line in diff_lines_getter(diff_lines): + matching_line = match( + r"{prefix}({symbol_re})\s=\s.+|" + r"{prefix}def\s({symbol_re})|" + r"{prefix}class\s({symbol_re})".format( + symbol_re=r"[a-zA-Z][_\w]+", prefix=prefix + ), + diff_line, + ) + + if matching_line is not None: + if b_file_path not in file_path_symbols.keys(): + file_path_symbols[b_file_path] = [] + + file_path_symbols[b_file_path].append( + next(filter(bool, matching_line.groups())) + ) + + +def a_diff_lines_getter(diff_lines): + return diff_lines.b_blob.data_stream.read().decode("utf-8").split("\n") + + +def m_diff_lines_getter(diff_lines): + return unified_diff( + diff_lines.a_blob.data_stream.read().decode("utf-8").split("\n"), + diff_lines.b_blob.data_stream.read().decode("utf-8").split("\n"), + ) + + +get_symbols("A", a_diff_lines_getter, r"") +get_symbols("M", m_diff_lines_getter, r"\+") + +if file_path_symbols: + print("The code in this branch adds the following public symbols:") + print() + for file_path, symbols in file_path_symbols.items(): + print("- {}".format(file_path)) + for symbol in symbols: + print("\t{}".format(symbol)) + print() + + print( + "Please make sure that all of them are strictly necessary, if not, " + "please consider prefixing them with an underscore to make them " + 'private. After that, please label this PR with "Skip Public API ' + 'check".' + ) + exit(1) +else: + print("The code in this branch will not add any public symbols") diff --git a/tox.ini b/tox.ini index f0ee49e971..10d1f4925b 100644 --- a/tox.ini +++ b/tox.ini @@ -33,7 +33,7 @@ envlist = ; opentelemetry-exporter-jaeger-proto-grpc py3{6,7,8,9}-test-exporter-jaeger-proto-grpc - + ; opentelemetry-exporter-jaeger-thrift py3{6,7,8,9}-test-exporter-jaeger-thrift @@ -78,6 +78,7 @@ envlist = mypy,mypyinstalled docs docker-tests + public-symbols-check [testenv] deps = @@ -186,6 +187,7 @@ deps = psutil readme_renderer httpretty + GitPython commands_pre = python -m pip install -e {toxinidir}/opentelemetry-api[test] @@ -268,3 +270,11 @@ commands = pytest {posargs} commands_post = docker-compose down -v + +[testenv:public-symbols-check] +basepython: python3.8 +recreate = True +deps = + GitPython +commands = + python {toxinidir}/scripts/public_symbols_checker.py From 038bd2499f48671ba293ab16be8b0bb0dc33464d Mon Sep 17 00:00:00 2001 From: Eddy Lin Date: Wed, 26 May 2021 12:56:53 -0400 Subject: [PATCH 0888/1517] Update otel-exporter OTLP headers parsing to match format specs (#1869) --- CHANGELOG.md | 2 ++ .../exporter/otlp/proto/grpc/exporter.py | 17 ++++++++++++++--- .../tests/test_otlp_trace_exporter.py | 8 ++++---- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5477ae7940..587ca0a61b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1854](https://github.com/open-telemetry/opentelemetry-python/pull/1854)) - Changed AttributeValue sequences to warn mypy users on adding None values to array ([#1855](https://github.com/open-telemetry/opentelemetry-python/pull/1855)) +- Fixed exporter OTLP header parsing to match baggage header formatting. + ([#1869](https://github.com/open-telemetry/opentelemetry-python/pull/1869)) ## [1.2.0, 0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0-0.21b0) - 2021-05-11 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index be58cd8258..7b9227fd07 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -229,9 +229,20 @@ def __init__( self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS) if isinstance(self._headers, str): - self._headers = tuple( - tuple(item.split("=")) for item in self._headers.split(",") - ) + temp_headers = [] + for header_pair in self._headers.split(","): + key, value = header_pair.split("=", maxsplit=1) + key = key.strip().lower() + value = value.strip() + temp_headers.append( + ( + key, + value, + ) + ) + + self._headers = tuple(temp_headers) + self._timeout = timeout or int( environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 10) ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 3f43f1a8cc..521c9e6e82 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -180,7 +180,7 @@ def tearDown(self): OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "collector:4317", OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: THIS_DIR + "/fixtures/test.cert", - OTEL_EXPORTER_OTLP_TRACES_HEADERS: "key1=value1,key2=value2", + OTEL_EXPORTER_OTLP_TRACES_HEADERS: " key1=value1,KEY2 = value=2", OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "10", OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip", }, @@ -195,7 +195,7 @@ def test_env_variables(self, mock_exporter_mixin): _, kwargs = mock_exporter_mixin.call_args_list[0] self.assertEqual(kwargs["endpoint"], "collector:4317") - self.assertEqual(kwargs["headers"], "key1=value1,key2=value2") + self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") self.assertEqual(kwargs["timeout"], 10) self.assertEqual(kwargs["compression"], Compression.Gzip) self.assertIsNotNone(kwargs["credentials"]) @@ -217,7 +217,7 @@ def test_no_credentials_error( @patch.dict( "os.environ", - {OTEL_EXPORTER_OTLP_TRACES_HEADERS: "key1=value1,key2=value2"}, + {OTEL_EXPORTER_OTLP_TRACES_HEADERS: " key1=value1,KEY2 = VALUE=2 "}, ) @patch( "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" @@ -228,7 +228,7 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): exporter = OTLPSpanExporter() # pylint: disable=protected-access self.assertEqual( - exporter._headers, (("key1", "value1"), ("key2", "value2")) + exporter._headers, (("key1", "value1"), ("key2", "VALUE=2")) ) exporter = OTLPSpanExporter( headers=(("key3", "value3"), ("key4", "value4")) From cec5c077f96c76c3768d9e2210e6289e4cefc060 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 27 May 2021 15:19:55 -0700 Subject: [PATCH 0889/1517] update base version of python to 3.9 (#1874) --- .github/workflows/test.yml | 2 +- dev-requirements.txt | 2 +- tox.ini | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33413f3213..b715ff520c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -94,7 +94,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install tox run: pip install -U tox - name: Cache tox environment diff --git a/dev-requirements.txt b/dev-requirements.txt index 2fb15652b4..9c4dea3993 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,4 @@ -pylint==2.6.0 +pylint==2.7.1 flake8~=3.7 isort~=5.8 black~=20.8b1 diff --git a/tox.ini b/tox.ini index 10d1f4925b..635c3c2df9 100644 --- a/tox.ini +++ b/tox.ini @@ -175,7 +175,7 @@ commands = mypyinstalled: mypy --namespace-packages opentelemetry-api/tests/mypysmoke.py --strict [testenv:lint] -basepython: python3.8 +basepython: python3.9 recreate = True deps = -c dev-requirements.txt @@ -238,6 +238,7 @@ deps = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ + -e {toxinidir}/opentelemetry-semantic-conventions \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests \ @@ -272,7 +273,7 @@ commands_post = docker-compose down -v [testenv:public-symbols-check] -basepython: python3.8 +basepython: python3.9 recreate = True deps = GitPython From 7f18221a40f1daaf27c0a88584bae53baa2f828a Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 27 May 2021 15:42:34 -0700 Subject: [PATCH 0890/1517] use correct label in github action (#1876) The label is "Approve Public API check" not "Skip Public API check". --- .github/workflows/public-api-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/public-api-check.yml b/.github/workflows/public-api-check.yml index e151c40cf0..5d85ef3f89 100644 --- a/.github/workflows/public-api-check.yml +++ b/.github/workflows/public-api-check.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest - if: "!contains(github.event.pull_request.labels.*.name, 'Skip Public API check')" + if: "!contains(github.event.pull_request.labels.*.name, 'Approve Public API check')" steps: - name: Checkout the repo From 0ecfeb70893845c5083dfe0ba434c110a3df9d6c Mon Sep 17 00:00:00 2001 From: "Marcos F. Lobo" <5293167+marcosflobo@users.noreply.github.com> Date: Fri, 28 May 2021 01:00:02 +0200 Subject: [PATCH 0891/1517] Fix link to instrumentation (#1826) Co-authored-by: Leighton Chen Co-authored-by: alrex --- docs/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index fe1b2a8538..0da22fba40 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -34,8 +34,8 @@ In addition, there are several extension packages which can be installed separat pip install opentelemetry-instrumentation-{instrumentation} These are for exporter and instrumentation packages respectively. -Some packages can be found in :scm_web:`instrumentation ` and :scm_web:`exporter ` -directory of the repository. The remaining packages can be found at the +The Jaeger, Zipkin, OTLP and OpenCensus Exporters can be found in the :scm_web:`exporter ` +directory of the repository. Instrumentations and additional exporters can be found in the `Contrib repo instrumentation `_ and `Contrib repo exporter `_ directories. From c8ebda956160b658a2569a109248cbcdb54c995e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 28 May 2021 10:17:54 -0600 Subject: [PATCH 0892/1517] Fix error in license files (#1881) Fixes #1880 --- LICENSE | 2 +- exporter/opentelemetry-exporter-jaeger-proto-grpc/LICENSE | 2 +- exporter/opentelemetry-exporter-jaeger-thrift/LICENSE | 2 +- exporter/opentelemetry-exporter-jaeger/LICENSE | 2 +- exporter/opentelemetry-exporter-opencensus/LICENSE | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/LICENSE | 2 +- exporter/opentelemetry-exporter-otlp/LICENSE | 2 +- exporter/opentelemetry-exporter-zipkin-json/LICENSE | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/LICENSE | 2 +- exporter/opentelemetry-exporter-zipkin/LICENSE | 2 +- opentelemetry-api/LICENSE | 2 +- opentelemetry-proto/LICENSE | 2 +- opentelemetry-sdk/LICENSE | 2 +- opentelemetry-semantic-conventions/LICENSE | 2 +- propagator/opentelemetry-propagator-b3/LICENSE | 2 +- propagator/opentelemetry-propagator-jaeger/LICENSE | 2 +- shim/opentelemetry-opentracing-shim/LICENSE | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/LICENSE b/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/LICENSE b/exporter/opentelemetry-exporter-jaeger-proto-grpc/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/LICENSE +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/LICENSE b/exporter/opentelemetry-exporter-jaeger-thrift/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/LICENSE +++ b/exporter/opentelemetry-exporter-jaeger-thrift/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/exporter/opentelemetry-exporter-jaeger/LICENSE b/exporter/opentelemetry-exporter-jaeger/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/exporter/opentelemetry-exporter-jaeger/LICENSE +++ b/exporter/opentelemetry-exporter-jaeger/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/exporter/opentelemetry-exporter-opencensus/LICENSE b/exporter/opentelemetry-exporter-opencensus/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/exporter/opentelemetry-exporter-opencensus/LICENSE +++ b/exporter/opentelemetry-exporter-opencensus/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/LICENSE b/exporter/opentelemetry-exporter-otlp-proto-grpc/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/LICENSE +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/exporter/opentelemetry-exporter-otlp/LICENSE b/exporter/opentelemetry-exporter-otlp/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/exporter/opentelemetry-exporter-otlp/LICENSE +++ b/exporter/opentelemetry-exporter-otlp/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/exporter/opentelemetry-exporter-zipkin-json/LICENSE b/exporter/opentelemetry-exporter-zipkin-json/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/LICENSE +++ b/exporter/opentelemetry-exporter-zipkin-json/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/LICENSE b/exporter/opentelemetry-exporter-zipkin-proto-http/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/LICENSE +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/exporter/opentelemetry-exporter-zipkin/LICENSE b/exporter/opentelemetry-exporter-zipkin/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/exporter/opentelemetry-exporter-zipkin/LICENSE +++ b/exporter/opentelemetry-exporter-zipkin/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/LICENSE b/opentelemetry-api/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/opentelemetry-api/LICENSE +++ b/opentelemetry-api/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/opentelemetry-proto/LICENSE b/opentelemetry-proto/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/opentelemetry-proto/LICENSE +++ b/opentelemetry-proto/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/LICENSE b/opentelemetry-sdk/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/opentelemetry-sdk/LICENSE +++ b/opentelemetry-sdk/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/opentelemetry-semantic-conventions/LICENSE b/opentelemetry-semantic-conventions/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/opentelemetry-semantic-conventions/LICENSE +++ b/opentelemetry-semantic-conventions/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/propagator/opentelemetry-propagator-b3/LICENSE b/propagator/opentelemetry-propagator-b3/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/propagator/opentelemetry-propagator-b3/LICENSE +++ b/propagator/opentelemetry-propagator-b3/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/propagator/opentelemetry-propagator-jaeger/LICENSE b/propagator/opentelemetry-propagator-jaeger/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/propagator/opentelemetry-propagator-jaeger/LICENSE +++ b/propagator/opentelemetry-propagator-jaeger/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/shim/opentelemetry-opentracing-shim/LICENSE b/shim/opentelemetry-opentracing-shim/LICENSE index 261eeb9e9f..1ef7dad2c5 100644 --- a/shim/opentelemetry-opentracing-shim/LICENSE +++ b/shim/opentelemetry-opentracing-shim/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright The OpenTelemetry Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 56495ede2d59e6eb4f3cbced8ea2fd7c29918e5b Mon Sep 17 00:00:00 2001 From: Daniel Getu Date: Tue, 1 Jun 2021 08:29:53 -0700 Subject: [PATCH 0893/1517] Add schema_url to Resource (#1871) --- CHANGELOG.md | 2 + .../opentelemetry/sdk/resources/__init__.py | 52 ++++++- .../tests/resources/test_resources.py | 143 +++++++++++++++++- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- 4 files changed, 189 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 587ca0a61b..b0741653da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1855](https://github.com/open-telemetry/opentelemetry-python/pull/1855)) - Fixed exporter OTLP header parsing to match baggage header formatting. ([#1869](https://github.com/open-telemetry/opentelemetry-python/pull/1869)) +- Added optional `schema_url` field to `Resource` class + ([#1871](https://github.com/open-telemetry/opentelemetry-python/pull/1871)) ## [1.2.0, 0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0-0.21b0) - 2021-05-11 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 8755b2d1f9..24e5321ce9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -141,16 +141,25 @@ class Resource: """A Resource is an immutable representation of the entity producing telemetry as Attributes.""" - def __init__(self, attributes: Attributes): + def __init__( + self, attributes: Attributes, schema_url: typing.Optional[str] = None + ): _filter_attributes(attributes) self._attributes = attributes.copy() + if schema_url is None: + schema_url = "" + self._schema_url = schema_url @staticmethod - def create(attributes: typing.Optional[Attributes] = None) -> "Resource": + def create( + attributes: typing.Optional[Attributes] = None, + schema_url: typing.Optional[str] = None, + ) -> "Resource": """Creates a new `Resource` from attributes. Args: attributes: Optional zero or more key-value pairs. + schema_url: Optional URL pointing to the schema Returns: The newly-created Resource. @@ -159,7 +168,7 @@ def create(attributes: typing.Optional[Attributes] = None) -> "Resource": attributes = {} resource = _DEFAULT_RESOURCE.merge( OTELResourceDetector().detect() - ).merge(Resource(attributes)) + ).merge(Resource(attributes, schema_url)) if not resource.attributes.get(SERVICE_NAME, None): default_service_name = "unknown_service" process_executable_name = resource.attributes.get( @@ -168,7 +177,7 @@ def create(attributes: typing.Optional[Attributes] = None) -> "Resource": if process_executable_name: default_service_name += ":" + process_executable_name resource = resource.merge( - Resource({SERVICE_NAME: default_service_name}) + Resource({SERVICE_NAME: default_service_name}, schema_url) ) return resource @@ -180,12 +189,21 @@ def get_empty() -> "Resource": def attributes(self) -> Attributes: return self._attributes.copy() + @property + def schema_url(self) -> str: + return self._schema_url + def merge(self, other: "Resource") -> "Resource": """Merges this resource and an updating resource into a new `Resource`. If a key exists on both the old and updating resource, the value of the updating resource will override the old resource value. + The updating resource's `schema_url` will be used only if the old + `schema_url` is empty. Attempting to merge two resources with + different, non-empty values for `schema_url` will result in an error + and return the old resource. + Args: other: The other resource to be merged. @@ -194,15 +212,35 @@ def merge(self, other: "Resource") -> "Resource": """ merged_attributes = self.attributes merged_attributes.update(other.attributes) - return Resource(merged_attributes) + + if self.schema_url == "": + schema_url = other.schema_url + elif other.schema_url == "": + schema_url = self.schema_url + elif self.schema_url == other.schema_url: + schema_url = other.schema_url + else: + logger.error( + "Failed to merge resources: The two schemas %s and %s are incompatible", + self.schema_url, + other.schema_url, + ) + return self + + return Resource(merged_attributes, schema_url) def __eq__(self, other: object) -> bool: if not isinstance(other, Resource): return False - return self._attributes == other._attributes + return ( + self._attributes == other._attributes + and self._schema_url == other._schema_url + ) def __hash__(self): - return hash(dumps(self._attributes, sort_keys=True)) + return hash( + f"{dumps(self._attributes, sort_keys=True)}|{self._schema_url}" + ) _EMPTY_RESOURCE = Resource({}) diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 87b4006ce7..36eb099cd2 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -17,6 +17,7 @@ import os import unittest import uuid +from logging import ERROR from unittest import mock from opentelemetry.sdk import resources @@ -51,6 +52,14 @@ def test_create(self): resource = resources.Resource.create(attributes) self.assertIsInstance(resource, resources.Resource) self.assertEqual(resource.attributes, expected_attributes) + self.assertEqual(resource.schema_url, "") + + schema_url = "https://opentelemetry.io/schemas/1.3.0" + + resource = resources.Resource.create(attributes, schema_url) + self.assertIsInstance(resource, resources.Resource) + self.assertEqual(resource.attributes, expected_attributes) + self.assertEqual(resource.schema_url, schema_url) os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "key=value" resource = resources.Resource.create(attributes) @@ -67,17 +76,45 @@ def test_create(self): self.assertEqual( resource, resources._DEFAULT_RESOURCE.merge( - resources.Resource({resources.SERVICE_NAME: "unknown_service"}) + resources.Resource( + {resources.SERVICE_NAME: "unknown_service"}, "" + ) ), ) + self.assertEqual(resource.schema_url, "") + + resource = resources.Resource.create(None, None) + self.assertEqual( + resource, + resources._DEFAULT_RESOURCE.merge( + resources.Resource( + {resources.SERVICE_NAME: "unknown_service"}, "" + ) + ), + ) + self.assertEqual(resource.schema_url, "") resource = resources.Resource.create({}) self.assertEqual( resource, resources._DEFAULT_RESOURCE.merge( - resources.Resource({resources.SERVICE_NAME: "unknown_service"}) + resources.Resource( + {resources.SERVICE_NAME: "unknown_service"}, "" + ) + ), + ) + self.assertEqual(resource.schema_url, "") + + resource = resources.Resource.create({}, None) + self.assertEqual( + resource, + resources._DEFAULT_RESOURCE.merge( + resources.Resource( + {resources.SERVICE_NAME: "unknown_service"}, "" + ) ), ) + self.assertEqual(resource.schema_url, "") def test_resource_merge(self): left = resources.Resource({"service": "ui"}) @@ -86,6 +123,33 @@ def test_resource_merge(self): left.merge(right), resources.Resource({"service": "ui", "host": "service-host"}), ) + schema_urls = ( + "https://opentelemetry.io/schemas/1.2.0", + "https://opentelemetry.io/schemas/1.3.0", + ) + + left = resources.Resource.create({}, None) + right = resources.Resource.create({}, None) + self.assertEqual(left.merge(right).schema_url, "") + + left = resources.Resource.create({}, None) + right = resources.Resource.create({}, schema_urls[0]) + self.assertEqual(left.merge(right).schema_url, schema_urls[0]) + + left = resources.Resource.create({}, schema_urls[0]) + right = resources.Resource.create({}, None) + self.assertEqual(left.merge(right).schema_url, schema_urls[0]) + + left = resources.Resource.create({}, schema_urls[0]) + right = resources.Resource.create({}, schema_urls[0]) + self.assertEqual(left.merge(right).schema_url, schema_urls[0]) + + left = resources.Resource.create({}, schema_urls[0]) + right = resources.Resource.create({}, schema_urls[1]) + with self.assertLogs(level=ERROR) as log_entry: + self.assertEqual(left.merge(right), left) + self.assertIn(schema_urls[0], log_entry.output[0]) + self.assertIn(schema_urls[1], log_entry.output[0]) def test_resource_merge_empty_string(self): """Verify Resource.merge behavior with the empty string. @@ -130,6 +194,11 @@ def test_immutability(self): attributes["cost"] = 999.91 self.assertEqual(resource.attributes, attributes_copy) + with self.assertRaises(AttributeError): + resource.schema_url = "bug" + + self.assertEqual(resource.schema_url, "") + def test_service_name_using_process_name(self): resource = resources.Resource.create( {resources.PROCESS_EXECUTABLE_NAME: "test"} @@ -220,6 +289,76 @@ def test_aggregated_resources_multiple_detectors(self): ), ) + def test_aggregated_resources_different_schema_urls(self): + resource_detector1 = mock.Mock(spec=resources.ResourceDetector) + resource_detector1.detect.return_value = resources.Resource( + {"key1": "value1"}, "" + ) + resource_detector2 = mock.Mock(spec=resources.ResourceDetector) + resource_detector2.detect.return_value = resources.Resource( + {"key2": "value2", "key3": "value3"}, "url1" + ) + resource_detector3 = mock.Mock(spec=resources.ResourceDetector) + resource_detector3.detect.return_value = resources.Resource( + { + "key2": "try_to_overwrite_existing_value", + "key3": "try_to_overwrite_existing_value", + "key4": "value4", + }, + "url2", + ) + resource_detector4 = mock.Mock(spec=resources.ResourceDetector) + resource_detector4.detect.return_value = resources.Resource( + { + "key2": "try_to_overwrite_existing_value", + "key3": "try_to_overwrite_existing_value", + "key4": "value4", + }, + "url1", + ) + self.assertEqual( + resources.get_aggregated_resources( + [resource_detector1, resource_detector2] + ), + resources.Resource( + {"key1": "value1", "key2": "value2", "key3": "value3"}, + "url1", + ), + ) + with self.assertLogs(level=ERROR) as log_entry: + self.assertEqual( + resources.get_aggregated_resources( + [resource_detector2, resource_detector3] + ), + resources.Resource( + {"key2": "value2", "key3": "value3"}, "url1" + ), + ) + self.assertIn("url1", log_entry.output[0]) + self.assertIn("url2", log_entry.output[0]) + with self.assertLogs(level=ERROR): + self.assertEqual( + resources.get_aggregated_resources( + [ + resource_detector2, + resource_detector3, + resource_detector4, + resource_detector1, + ] + ), + resources.Resource( + { + "key1": "value1", + "key2": "try_to_overwrite_existing_value", + "key3": "try_to_overwrite_existing_value", + "key4": "value4", + }, + "url1", + ), + ) + self.assertIn("url1", log_entry.output[0]) + self.assertIn("url2", log_entry.output[0]) + def test_resource_detector_ignore_error(self): resource_detector = mock.Mock(spec=resources.ResourceDetector) resource_detector.detect.side_effect = Exception() diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index c9c523c3fa..a5113ffe09 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -869,7 +869,7 @@ def test_span_override_start_and_end_time(self): self.assertEqual(end_time, span.end_time) def test_ended_span(self): - """"Events, attributes are not allowed after span is ended""" + """Events, attributes are not allowed after span is ended""" root = self.tracer.start_span("root") From 0c1fc964f6a9b04e7059f3b6434a518a7d706a08 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 1 Jun 2021 21:31:28 +0530 Subject: [PATCH 0894/1517] Introduce SpanLimits class to tracing SDK (#1877) --- CHANGELOG.md | 4 + .../src/opentelemetry/sdk/trace/__init__.py | 75 +++++++---- opentelemetry-sdk/tests/trace/test_trace.py | 120 +++++++++++++----- 3 files changed, 136 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0741653da..2215bebc54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.2.0-0.21b0...HEAD) +### Added +- Allow span limits to be set programatically via TracerProvider. + ([#1877](https://github.com/open-telemetry/opentelemetry-python/pull/1877)) + ### Changed - Updated get_tracer to return an empty string when passed an invalid name ([#1854](https://github.com/open-telemetry/opentelemetry-python/pull/1854)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index c202f5cbea..66b4b38390 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -62,9 +62,12 @@ logger = logging.getLogger(__name__) -_DEFAULT_SPAN_EVENTS_LIMIT = 128 -_DEFAULT_SPAN_LINKS_LIMIT = 128 -_DEFAULT_SPAN_ATTRIBUTES_LIMIT = 128 +_DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = 128 +_DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT = 128 +_DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT = 128 + + +_ENV_VALUE_UNSET = "unset" # pylint: disable=protected-access _TRACE_SAMPLER = sampling._get_from_env_or_default() @@ -499,19 +502,29 @@ def _format_links(links): return f_links -class _Limits: +class SpanLimits: """The limits that should be enforce on recorded data such as events, links, attributes etc. This class does not enforce any limits itself. It only provides an a way read limits from env, - default values and in future from user provided arguments. + default values and from user provided arguments. + + All limit arguments must be either a non-negative integer, ``None`` or ``SpanLimits.UNSET``. - All limit must be either a non-negative integer or ``None``. - Setting a limit to ``None`` will not set any limits for that field/type. + - All limit arguments are optional. + - If a limit argument is not set, the class will try to read it's value from the corresponding + environment variable. + - If the environment variable is not set, the default value for the limit is used. Args: + max_attributes: Maximum number of attributes that can be added to a Span. + Environment variable: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT + Default: {_DEFAULT_SPAN_ATTRIBUTE_COUNT_LIMIT} max_events: Maximum number of events that can be added to a Span. + Environment variable: OTEL_SPAN_EVENT_COUNT_LIMIT + Default: {_DEFAULT_SPAN_EVENT_COUNT_LIMIT} max_links: Maximum number of links that can be added to a Span. - max_attributes: Maximum number of attributes that can be added to a Span. + Environment variable: OTEL_SPAN_LINK_COUNT_LIMIT + Default: {_DEFAULT_SPAN_LINK_COUNT_LIMIT} """ UNSET = -1 @@ -529,13 +542,17 @@ def __init__( self.max_attributes = self._from_env_if_absent( max_attributes, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, - _DEFAULT_SPAN_ATTRIBUTES_LIMIT, + _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, ) self.max_events = self._from_env_if_absent( - max_events, OTEL_SPAN_EVENT_COUNT_LIMIT, _DEFAULT_SPAN_EVENTS_LIMIT + max_events, + OTEL_SPAN_EVENT_COUNT_LIMIT, + _DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT, ) self.max_links = self._from_env_if_absent( - max_links, OTEL_SPAN_LINK_COUNT_LIMIT, _DEFAULT_SPAN_LINKS_LIMIT + max_links, + OTEL_SPAN_LINK_COUNT_LIMIT, + _DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT, ) def __repr__(self): @@ -556,7 +573,7 @@ def _from_env_if_absent( str_value = environ.get(env_var, "").strip().lower() if not str_value: return default - if str_value == "unset": + if str_value == _ENV_VALUE_UNSET: return None try: @@ -569,14 +586,16 @@ def _from_env_if_absent( return value -_UnsetLimits = _Limits( - max_attributes=_Limits.UNSET, - max_events=_Limits.UNSET, - max_links=_Limits.UNSET, +_UnsetLimits = SpanLimits( + max_attributes=SpanLimits.UNSET, + max_events=SpanLimits.UNSET, + max_links=SpanLimits.UNSET, ) -SPAN_ATTRIBUTE_COUNT_LIMIT = _Limits._from_env_if_absent( - None, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_SPAN_ATTRIBUTES_LIMIT +SPAN_ATTRIBUTE_COUNT_LIMIT = SpanLimits._from_env_if_absent( + None, + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, ) @@ -599,6 +618,7 @@ class Span(trace_api.Span, ReadableSpan): links: Links to other spans to be exported span_processor: `SpanProcessor` to invoke when starting and ending this `Span`. + limits: `SpanLimits` instance that was passed to the `TracerProvider` """ def __new__(cls, *args, **kwargs): @@ -623,6 +643,7 @@ def __init__( instrumentation_info: InstrumentationInfo = None, record_exception: bool = True, set_status_on_exception: bool = True, + limits=_UnsetLimits, ) -> None: super().__init__( name=name, @@ -637,6 +658,7 @@ def __init__( self._record_exception = record_exception self._set_status_on_exception = set_status_on_exception self._span_processor = span_processor + self._limits = limits self._lock = threading.Lock() _filter_attributes(attributes) @@ -847,10 +869,6 @@ class _Span(Span): by other mechanisms than through the `Tracer`. """ - def __init__(self, *args, limits=_UnsetLimits, **kwargs): - self._limits = limits - super().__init__(*args, **kwargs) - class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`.""" @@ -864,13 +882,14 @@ def __init__( ], id_generator: IdGenerator, instrumentation_info: InstrumentationInfo, + span_limits: SpanLimits, ) -> None: self.sampler = sampler self.resource = resource self.span_processor = span_processor self.id_generator = id_generator self.instrumentation_info = instrumentation_info - self._limits = None + self._span_limits = span_limits @contextmanager def start_as_current_span( @@ -972,7 +991,7 @@ def start_span( # pylint: disable=too-many-locals instrumentation_info=self.instrumentation_info, record_exception=record_exception, set_status_on_exception=set_status_on_exception, - limits=self._limits, + limits=self._span_limits, ) span.start(start_time=start_time, parent_context=context) else: @@ -992,6 +1011,7 @@ def __init__( SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor ] = None, id_generator: IdGenerator = None, + span_limits: SpanLimits = None, ): self._active_span_processor = ( active_span_processor or SynchronousMultiSpanProcessor() @@ -1002,7 +1022,7 @@ def __init__( self.id_generator = id_generator self._resource = resource self.sampler = sampler - self._limits = _Limits() + self._span_limits = span_limits or SpanLimits() self._atexit_handler = None if shutdown_on_exit: self._atexit_handler = atexit.register(self.shutdown) @@ -1019,7 +1039,7 @@ def get_tracer( if not instrumenting_module_name: # Reject empty strings too. instrumenting_module_name = "" logger.error("get_tracer called with missing module name.") - tracer = Tracer( + return Tracer( self.sampler, self.resource, self._active_span_processor, @@ -1027,9 +1047,8 @@ def get_tracer( InstrumentationInfo( instrumenting_module_name, instrumenting_library_version ), + self._span_limits, ) - tracer._limits = self._limits - return tracer def add_span_processor(self, span_processor: SpanProcessor) -> None: """Registers a new :class:`SpanProcessor` for this `TracerProvider`. diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index a5113ffe09..0781042a33 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -40,8 +40,8 @@ from opentelemetry.util._time import _time_ns -def new_tracer() -> trace_api.Tracer: - return trace.TracerProvider().get_tracer(__name__) +def new_tracer(span_limits=None) -> trace_api.Tracer: + return trace.TracerProvider(span_limits=span_limits).get_tracer(__name__) class TestTracer(unittest.TestCase): @@ -539,7 +539,7 @@ def test_disallow_direct_span_creation(self): def test_surplus_span_links(self): # pylint: disable=protected-access - max_links = trace._Limits().max_links + max_links = trace.SpanLimits().max_links links = [ trace_api.Link(trace_api.SpanContext(0x1, idx, is_remote=False)) for idx in range(0, 16 + max_links) @@ -550,7 +550,7 @@ def test_surplus_span_links(self): def test_surplus_span_attributes(self): # pylint: disable=protected-access - max_attrs = trace._Limits().max_attributes + max_attrs = trace.SpanLimits().max_attributes attributes = {str(idx): idx for idx in range(0, 16 + max_attrs)} tracer = new_tracer() with tracer.start_as_current_span( @@ -1275,12 +1275,17 @@ class TestSpanLimits(unittest.TestCase): # pylint: disable=protected-access def test_limits_defaults(self): - limits = trace._Limits() + limits = trace.SpanLimits() self.assertEqual( - limits.max_attributes, trace._DEFAULT_SPAN_ATTRIBUTES_LIMIT + limits.max_attributes, + trace._DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + ) + self.assertEqual( + limits.max_events, trace._DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT + ) + self.assertEqual( + limits.max_links, trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT ) - self.assertEqual(limits.max_events, trace._DEFAULT_SPAN_EVENTS_LIMIT) - self.assertEqual(limits.max_links, trace._DEFAULT_SPAN_LINKS_LIMIT) def test_limits_values_code(self): max_attributes, max_events, max_links = ( @@ -1288,7 +1293,7 @@ def test_limits_values_code(self): randint(0, 10000), randint(0, 10000), ) - limits = trace._Limits( + limits = trace.SpanLimits( max_attributes=max_attributes, max_events=max_events, max_links=max_links, @@ -1311,21 +1316,12 @@ def test_limits_values_env(self): OTEL_SPAN_LINK_COUNT_LIMIT: str(max_links), }, ): - limits = trace._Limits() + limits = trace.SpanLimits() self.assertEqual(limits.max_attributes, max_attributes) self.assertEqual(limits.max_events, max_events) self.assertEqual(limits.max_links, max_links) - @mock.patch.dict( - "os.environ", - { - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10", - OTEL_SPAN_EVENT_COUNT_LIMIT: "20", - OTEL_SPAN_LINK_COUNT_LIMIT: "30", - }, - ) - def test_span_limits_env(self): - tracer = new_tracer() + def _test_span_limits(self, tracer): id_generator = RandomIdGenerator() some_links = [ trace_api.Link( @@ -1353,18 +1349,11 @@ def test_span_limits_env(self): self.assertEqual(len(root.attributes), 10) self.assertEqual(len(root.events), 20) - @mock.patch.dict( - "os.environ", - { - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "unset", - OTEL_SPAN_EVENT_COUNT_LIMIT: "unset", - OTEL_SPAN_LINK_COUNT_LIMIT: "unset", - }, - ) - def test_span_no_limits_env(self): - num_links = int(trace._DEFAULT_SPAN_LINKS_LIMIT) + randint(1, 100) + def _test_span_no_limits(self, tracer): + num_links = int(trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT) + randint( + 1, 100 + ) - tracer = new_tracer() id_generator = RandomIdGenerator() some_links = [ trace_api.Link( @@ -1379,18 +1368,79 @@ def test_span_no_limits_env(self): with tracer.start_as_current_span("root", links=some_links) as root: self.assertEqual(len(root.links), num_links) - num_events = int(trace._DEFAULT_SPAN_EVENTS_LIMIT) + randint(1, 100) + num_events = int(trace._DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT) + randint( + 1, 100 + ) with tracer.start_as_current_span("root") as root: for idx in range(num_events): root.add_event("my_event_{}".format(idx)) self.assertEqual(len(root.events), num_events) - num_attributes = int(trace._DEFAULT_SPAN_ATTRIBUTES_LIMIT) + randint( - 1, 100 - ) + num_attributes = int( + trace._DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT + ) + randint(1, 100) with tracer.start_as_current_span("root") as root: for idx in range(num_attributes): root.set_attribute("my_attribute_{}".format(idx), 0) self.assertEqual(len(root.attributes), num_attributes) + + @mock.patch.dict( + "os.environ", + { + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10", + OTEL_SPAN_EVENT_COUNT_LIMIT: "20", + OTEL_SPAN_LINK_COUNT_LIMIT: "30", + }, + ) + def test_span_limits_env(self): + self._test_span_limits(new_tracer()) + + @mock.patch.dict( + "os.environ", + { + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10", + OTEL_SPAN_EVENT_COUNT_LIMIT: "20", + OTEL_SPAN_LINK_COUNT_LIMIT: "30", + }, + ) + def test_span_limits_default_to_env(self): + self._test_span_limits( + new_tracer( + span_limits=trace.SpanLimits( + max_attributes=None, max_events=None, max_links=None + ) + ) + ) + + def test_span_limits_code(self): + self._test_span_limits( + new_tracer( + span_limits=trace.SpanLimits( + max_attributes=10, max_events=20, max_links=30 + ) + ) + ) + + @mock.patch.dict( + "os.environ", + { + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "unset", + OTEL_SPAN_EVENT_COUNT_LIMIT: "unset", + OTEL_SPAN_LINK_COUNT_LIMIT: "unset", + }, + ) + def test_span_no_limits_env(self): + self._test_span_no_limits(new_tracer()) + + def test_span_no_limits_code(self): + self._test_span_no_limits( + new_tracer( + span_limits=trace.SpanLimits( + max_attributes=trace.SpanLimits.UNSET, + max_links=trace.SpanLimits.UNSET, + max_events=trace.SpanLimits.UNSET, + ) + ) + ) From 82b2e87de2c41b43248a2bccdc75c0066ec2ef49 Mon Sep 17 00:00:00 2001 From: Joseph Lisee Date: Tue, 1 Jun 2021 12:43:47 -0400 Subject: [PATCH 0895/1517] Correct SDK PEP 564 nanosecond time reference (#1867) --- opentelemetry-api/src/opentelemetry/util/_time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/util/_time.py b/opentelemetry-api/src/opentelemetry/util/_time.py index 0099d3dbb5..dd1371d177 100644 --- a/opentelemetry-api/src/opentelemetry/util/_time.py +++ b/opentelemetry-api/src/opentelemetry/util/_time.py @@ -19,7 +19,7 @@ getLogger(__name__).warning( # pylint: disable=logging-not-lazy "You are using Python 3.%s. This version does not support timestamps " "with nanosecond precision and the OpenTelemetry SDK will use " - "millisecond precision instead. Please refer to PEP 546 for more " + "millisecond precision instead. Please refer to PEP 564 for more " "information. Please upgrade to Python 3.7 or newer to use nanosecond " "precision." % version_info.minor ) From ae01f709f8cd5f678d176c2c28adbbcaa0445e0c Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 1 Jun 2021 10:32:04 -0700 Subject: [PATCH 0896/1517] bump proto to 0.9.0 (#1873) --- CHANGELOG.md | 2 + .../trace/v1/trace_service_pb2_grpc.py | 6 +- .../proto/common/v1/common_pb2.py | 37 +- .../proto/common/v1/common_pb2.pyi | 9 +- .../opentelemetry/proto/logs/v1/logs_pb2.py | 56 ++- .../opentelemetry/proto/logs/v1/logs_pb2.pyi | 10 + .../metrics_config_service_pb2.py | 267 +++++++++++ .../metrics_config_service_pb2.pyi | 88 ++++ .../metrics_config_service_pb2_grpc.py | 82 ++++ .../proto/metrics/v1/metrics_pb2.py | 451 +++++++++++------- .../proto/metrics/v1/metrics_pb2.pyi | 150 +++--- .../opentelemetry/proto/trace/v1/trace_pb2.py | 50 +- .../proto/trace/v1/trace_pb2.pyi | 10 +- scripts/proto_codegen.sh | 2 +- 14 files changed, 933 insertions(+), 287 deletions(-) create mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.py create mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.pyi create mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2_grpc.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2215bebc54..ab7c822691 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1869](https://github.com/open-telemetry/opentelemetry-python/pull/1869)) - Added optional `schema_url` field to `Resource` class ([#1871](https://github.com/open-telemetry/opentelemetry-python/pull/1871)) +- Update protos to latest version release 0.9.0 + ([#1873](https://github.com/open-telemetry/opentelemetry-python/pull/1873)) ## [1.2.0, 0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0-0.21b0) - 2021-05-11 diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py index 1d86433946..3b9efef6d6 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py @@ -6,7 +6,7 @@ class TraceServiceStub(object): """Service that can be used to push spans between one Application instrumented with - OpenTelemetry and an collector, or between an collector and a central collector (in this + OpenTelemetry and a collector, or between a collector and a central collector (in this case spans are sent/received to/from multiple Applications). """ @@ -25,7 +25,7 @@ def __init__(self, channel): class TraceServiceServicer(object): """Service that can be used to push spans between one Application instrumented with - OpenTelemetry and an collector, or between an collector and a central collector (in this + OpenTelemetry and a collector, or between a collector and a central collector (in this case spans are sent/received to/from multiple Applications). """ @@ -54,7 +54,7 @@ def add_TraceServiceServicer_to_server(servicer, server): # This class is part of an EXPERIMENTAL API. class TraceService(object): """Service that can be used to push spans between one Application instrumented with - OpenTelemetry and an collector, or between an collector and a central collector (in this + OpenTelemetry and a collector, or between a collector and a central collector (in this case spans are sent/received to/from multiple Applications). """ diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py index 21501a30c0..5371e9facf 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py @@ -18,7 +18,7 @@ package='opentelemetry.proto.common.v1', syntax='proto3', serialized_options=b'\n io.opentelemetry.proto.common.v1B\013CommonProtoP\001Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1', - serialized_pb=b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\xf5\x01\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"O\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\",\n\x0eStringKeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"7\n\x16InstrumentationLibrary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\tBq\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1b\x06proto3' + serialized_pb=b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\x8c\x02\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"O\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"0\n\x0eStringKeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x18\x01\"7\n\x16InstrumentationLibrary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\tBq\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1b\x06proto3' ) @@ -73,6 +73,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='bytes_value', full_name='opentelemetry.proto.common.v1.AnyValue.bytes_value', index=6, + number=7, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -89,7 +96,7 @@ index=0, containing_type=None, fields=[]), ], serialized_start=78, - serialized_end=323, + serialized_end=346, ) @@ -119,8 +126,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=325, - serialized_end=394, + serialized_start=348, + serialized_end=417, ) @@ -150,8 +157,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=396, - serialized_end=467, + serialized_start=419, + serialized_end=490, ) @@ -188,8 +195,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=469, - serialized_end=548, + serialized_start=492, + serialized_end=571, ) @@ -220,14 +227,14 @@ nested_types=[], enum_types=[ ], - serialized_options=None, + serialized_options=b'\030\001', is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], - serialized_start=550, - serialized_end=594, + serialized_start=573, + serialized_end=621, ) @@ -264,8 +271,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=596, - serialized_end=651, + serialized_start=623, + serialized_end=678, ) _ANYVALUE.fields_by_name['array_value'].message_type = _ARRAYVALUE @@ -288,6 +295,9 @@ _ANYVALUE.oneofs_by_name['value'].fields.append( _ANYVALUE.fields_by_name['kvlist_value']) _ANYVALUE.fields_by_name['kvlist_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] +_ANYVALUE.oneofs_by_name['value'].fields.append( + _ANYVALUE.fields_by_name['bytes_value']) +_ANYVALUE.fields_by_name['bytes_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] _ARRAYVALUE.fields_by_name['values'].message_type = _ANYVALUE _KEYVALUELIST.fields_by_name['values'].message_type = _KEYVALUE _KEYVALUE.fields_by_name['value'].message_type = _ANYVALUE @@ -343,4 +353,5 @@ DESCRIPTOR._options = None +_STRINGKEYVALUE._options = None # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi index 7afb08ab9a..144df2d4a0 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi @@ -19,10 +19,12 @@ class AnyValue(google.protobuf.message.Message): DOUBLE_VALUE_FIELD_NUMBER: builtins.int ARRAY_VALUE_FIELD_NUMBER: builtins.int KVLIST_VALUE_FIELD_NUMBER: builtins.int + BYTES_VALUE_FIELD_NUMBER: builtins.int string_value: typing.Text = ... bool_value: builtins.bool = ... int_value: builtins.int = ... double_value: builtins.float = ... + bytes_value: builtins.bytes = ... @property def array_value(self) -> global___ArrayValue: ... @@ -38,10 +40,11 @@ class AnyValue(google.protobuf.message.Message): double_value : builtins.float = ..., array_value : typing.Optional[global___ArrayValue] = ..., kvlist_value : typing.Optional[global___KeyValueList] = ..., + bytes_value : builtins.bytes = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal[u"value",b"value"]) -> typing_extensions.Literal["string_value","bool_value","int_value","double_value","array_value","kvlist_value"]: ... + def HasField(self, field_name: typing_extensions.Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"bytes_value",b"bytes_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"bytes_value",b"bytes_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal[u"value",b"value"]) -> typing_extensions.Literal["string_value","bool_value","int_value","double_value","array_value","kvlist_value","bytes_value"]: ... global___AnyValue = AnyValue class ArrayValue(google.protobuf.message.Message): diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py index 184f6ae8b2..b9a8468d6c 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py @@ -26,7 +26,7 @@ package="opentelemetry.proto.logs.v1", syntax="proto3", serialized_options=b"\n\036io.opentelemetry.proto.logs.v1B\tLogsProtoP\001Z None: ... def HasField( self, field_name: typing_extensions.Literal["resource", b"resource"] @@ -130,6 +133,8 @@ class ResourceLogs(google.protobuf.message.Message): b"instrumentation_library_logs", "resource", b"resource", + "schema_url", + b"schema_url", ], ) -> None: ... @@ -139,6 +144,8 @@ class InstrumentationLibraryLogs(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int LOGS_FIELD_NUMBER: builtins.int + SCHEMA_URL_FIELD_NUMBER: builtins.int + schema_url: typing.Text = ... @property def instrumentation_library( self, @@ -156,6 +163,7 @@ class InstrumentationLibraryLogs(google.protobuf.message.Message): opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary ] = ..., logs: typing.Optional[typing.Iterable[global___LogRecord]] = ..., + schema_url: typing.Text = ..., ) -> None: ... def HasField( self, @@ -170,6 +178,8 @@ class InstrumentationLibraryLogs(google.protobuf.message.Message): b"instrumentation_library", "logs", b"logs", + "schema_url", + b"schema_url", ], ) -> None: ... diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.py new file mode 100644 index 0000000000..8f1b43c3d2 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/metrics/experimental/metrics_config_service.proto + +from google.protobuf import descriptor as _descriptor +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() + + +from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='opentelemetry/proto/metrics/experimental/metrics_config_service.proto', + package='opentelemetry.proto.metrics.experimental', + syntax='proto3', + serialized_options=b'\n+io.opentelemetry.proto.metrics.experimentalB\030MetricConfigServiceProtoP\001ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/metrics/experimental', + serialized_pb=b'\nEopentelemetry/proto/metrics/experimental/metrics_config_service.proto\x12(opentelemetry.proto.metrics.experimental\x1a.opentelemetry/proto/resource/v1/resource.proto\"r\n\x13MetricConfigRequest\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x1e\n\x16last_known_fingerprint\x18\x02 \x01(\x0c\"\xe0\x03\n\x14MetricConfigResponse\x12\x13\n\x0b\x66ingerprint\x18\x01 \x01(\x0c\x12Z\n\tschedules\x18\x02 \x03(\x0b\x32G.opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule\x12\x1f\n\x17suggested_wait_time_sec\x18\x03 \x01(\x05\x1a\xb5\x02\n\x08Schedule\x12k\n\x12\x65xclusion_patterns\x18\x01 \x03(\x0b\x32O.opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern\x12k\n\x12inclusion_patterns\x18\x02 \x03(\x0b\x32O.opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern\x12\x12\n\nperiod_sec\x18\x03 \x01(\x05\x1a;\n\x07Pattern\x12\x10\n\x06\x65quals\x18\x01 \x01(\tH\x00\x12\x15\n\x0bstarts_with\x18\x02 \x01(\tH\x00\x42\x07\n\x05match2\xa1\x01\n\x0cMetricConfig\x12\x90\x01\n\x0fGetMetricConfig\x12=.opentelemetry.proto.metrics.experimental.MetricConfigRequest\x1a>.opentelemetry.proto.metrics.experimental.MetricConfigResponseB\x94\x01\n+io.opentelemetry.proto.metrics.experimentalB\x18MetricConfigServiceProtoP\x01ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/metrics/experimentalb\x06proto3' + , + dependencies=[opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) + + + + +_METRICCONFIGREQUEST = _descriptor.Descriptor( + name='MetricConfigRequest', + full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='resource', full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest.resource', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='last_known_fingerprint', full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest.last_known_fingerprint', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=163, + serialized_end=277, +) + + +_METRICCONFIGRESPONSE_SCHEDULE_PATTERN = _descriptor.Descriptor( + name='Pattern', + full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='equals', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.equals', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='starts_with', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.starts_with', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='match', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.match', + index=0, containing_type=None, fields=[]), + ], + serialized_start=701, + serialized_end=760, +) + +_METRICCONFIGRESPONSE_SCHEDULE = _descriptor.Descriptor( + name='Schedule', + full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='exclusion_patterns', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.exclusion_patterns', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='inclusion_patterns', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.inclusion_patterns', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='period_sec', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.period_sec', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_METRICCONFIGRESPONSE_SCHEDULE_PATTERN, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=451, + serialized_end=760, +) + +_METRICCONFIGRESPONSE = _descriptor.Descriptor( + name='MetricConfigResponse', + full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='fingerprint', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.fingerprint', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='schedules', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.schedules', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='suggested_wait_time_sec', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.suggested_wait_time_sec', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_METRICCONFIGRESPONSE_SCHEDULE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=280, + serialized_end=760, +) + +_METRICCONFIGREQUEST.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE +_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.containing_type = _METRICCONFIGRESPONSE_SCHEDULE +_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'].fields.append( + _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['equals']) +_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['equals'].containing_oneof = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'] +_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'].fields.append( + _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['starts_with']) +_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['starts_with'].containing_oneof = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'] +_METRICCONFIGRESPONSE_SCHEDULE.fields_by_name['exclusion_patterns'].message_type = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN +_METRICCONFIGRESPONSE_SCHEDULE.fields_by_name['inclusion_patterns'].message_type = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN +_METRICCONFIGRESPONSE_SCHEDULE.containing_type = _METRICCONFIGRESPONSE +_METRICCONFIGRESPONSE.fields_by_name['schedules'].message_type = _METRICCONFIGRESPONSE_SCHEDULE +DESCRIPTOR.message_types_by_name['MetricConfigRequest'] = _METRICCONFIGREQUEST +DESCRIPTOR.message_types_by_name['MetricConfigResponse'] = _METRICCONFIGRESPONSE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +MetricConfigRequest = _reflection.GeneratedProtocolMessageType('MetricConfigRequest', (_message.Message,), { + 'DESCRIPTOR' : _METRICCONFIGREQUEST, + '__module__' : 'opentelemetry.proto.metrics.experimental.metrics_config_service_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigRequest) + }) +_sym_db.RegisterMessage(MetricConfigRequest) + +MetricConfigResponse = _reflection.GeneratedProtocolMessageType('MetricConfigResponse', (_message.Message,), { + + 'Schedule' : _reflection.GeneratedProtocolMessageType('Schedule', (_message.Message,), { + + 'Pattern' : _reflection.GeneratedProtocolMessageType('Pattern', (_message.Message,), { + 'DESCRIPTOR' : _METRICCONFIGRESPONSE_SCHEDULE_PATTERN, + '__module__' : 'opentelemetry.proto.metrics.experimental.metrics_config_service_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern) + }) + , + 'DESCRIPTOR' : _METRICCONFIGRESPONSE_SCHEDULE, + '__module__' : 'opentelemetry.proto.metrics.experimental.metrics_config_service_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule) + }) + , + 'DESCRIPTOR' : _METRICCONFIGRESPONSE, + '__module__' : 'opentelemetry.proto.metrics.experimental.metrics_config_service_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigResponse) + }) +_sym_db.RegisterMessage(MetricConfigResponse) +_sym_db.RegisterMessage(MetricConfigResponse.Schedule) +_sym_db.RegisterMessage(MetricConfigResponse.Schedule.Pattern) + + +DESCRIPTOR._options = None + +_METRICCONFIG = _descriptor.ServiceDescriptor( + name='MetricConfig', + full_name='opentelemetry.proto.metrics.experimental.MetricConfig', + file=DESCRIPTOR, + index=0, + serialized_options=None, + serialized_start=763, + serialized_end=924, + methods=[ + _descriptor.MethodDescriptor( + name='GetMetricConfig', + full_name='opentelemetry.proto.metrics.experimental.MetricConfig.GetMetricConfig', + index=0, + containing_service=None, + input_type=_METRICCONFIGREQUEST, + output_type=_METRICCONFIGRESPONSE, + serialized_options=None, + ), +]) +_sym_db.RegisterServiceDescriptor(_METRICCONFIG) + +DESCRIPTOR.services_by_name['MetricConfig'] = _METRICCONFIG + +# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.pyi new file mode 100644 index 0000000000..7218e03264 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.pyi @@ -0,0 +1,88 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import opentelemetry.proto.resource.v1.resource_pb2 +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... + +class MetricConfigRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + RESOURCE_FIELD_NUMBER: builtins.int + LAST_KNOWN_FINGERPRINT_FIELD_NUMBER: builtins.int + last_known_fingerprint: builtins.bytes = ... + + @property + def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: ... + + def __init__(self, + *, + resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., + last_known_fingerprint : builtins.bytes = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal[u"resource",b"resource"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"last_known_fingerprint",b"last_known_fingerprint",u"resource",b"resource"]) -> None: ... +global___MetricConfigRequest = MetricConfigRequest + +class MetricConfigResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + class Schedule(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + class Pattern(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + EQUALS_FIELD_NUMBER: builtins.int + STARTS_WITH_FIELD_NUMBER: builtins.int + equals: typing.Text = ... + starts_with: typing.Text = ... + + def __init__(self, + *, + equals : typing.Text = ..., + starts_with : typing.Text = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal[u"match",b"match"]) -> typing_extensions.Literal["equals","starts_with"]: ... + + EXCLUSION_PATTERNS_FIELD_NUMBER: builtins.int + INCLUSION_PATTERNS_FIELD_NUMBER: builtins.int + PERIOD_SEC_FIELD_NUMBER: builtins.int + period_sec: builtins.int = ... + + @property + def exclusion_patterns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule.Pattern]: ... + + @property + def inclusion_patterns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule.Pattern]: ... + + def __init__(self, + *, + exclusion_patterns : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule.Pattern]] = ..., + inclusion_patterns : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule.Pattern]] = ..., + period_sec : builtins.int = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal[u"exclusion_patterns",b"exclusion_patterns",u"inclusion_patterns",b"inclusion_patterns",u"period_sec",b"period_sec"]) -> None: ... + + FINGERPRINT_FIELD_NUMBER: builtins.int + SCHEDULES_FIELD_NUMBER: builtins.int + SUGGESTED_WAIT_TIME_SEC_FIELD_NUMBER: builtins.int + fingerprint: builtins.bytes = ... + suggested_wait_time_sec: builtins.int = ... + + @property + def schedules(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule]: ... + + def __init__(self, + *, + fingerprint : builtins.bytes = ..., + schedules : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule]] = ..., + suggested_wait_time_sec : builtins.int = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal[u"fingerprint",b"fingerprint",u"schedules",b"schedules",u"suggested_wait_time_sec",b"suggested_wait_time_sec"]) -> None: ... +global___MetricConfigResponse = MetricConfigResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2_grpc.py new file mode 100644 index 0000000000..829bf58712 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2_grpc.py @@ -0,0 +1,82 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +import grpc + +from opentelemetry.proto.metrics.experimental import metrics_config_service_pb2 as opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2 + + +class MetricConfigStub(object): + """MetricConfig is a service that enables updating metric schedules, trace + parameters, and other configurations on the SDK without having to restart the + instrumented application. The collector can also serve as the configuration + service, acting as a bridge between third-party configuration services and + the SDK, piping updated configs from a third-party source to an instrumented + application. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetMetricConfig = channel.unary_unary( + '/opentelemetry.proto.metrics.experimental.MetricConfig/GetMetricConfig', + request_serializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigRequest.SerializeToString, + response_deserializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigResponse.FromString, + ) + + +class MetricConfigServicer(object): + """MetricConfig is a service that enables updating metric schedules, trace + parameters, and other configurations on the SDK without having to restart the + instrumented application. The collector can also serve as the configuration + service, acting as a bridge between third-party configuration services and + the SDK, piping updated configs from a third-party source to an instrumented + application. + """ + + def GetMetricConfig(self, request, context): + """Missing associated documentation comment in .proto file""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_MetricConfigServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetMetricConfig': grpc.unary_unary_rpc_method_handler( + servicer.GetMetricConfig, + request_deserializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigRequest.FromString, + response_serializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'opentelemetry.proto.metrics.experimental.MetricConfig', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class MetricConfig(object): + """MetricConfig is a service that enables updating metric schedules, trace + parameters, and other configurations on the SDK without having to restart the + instrumented application. The collector can also serve as the configuration + service, acting as a bridge between third-party configuration services and + the SDK, piping updated configs from a third-party source to an instrumented + application. + """ + + @staticmethod + def GetMetricConfig(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/opentelemetry.proto.metrics.experimental.MetricConfig/GetMetricConfig', + opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigRequest.SerializeToString, + opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigResponse.FromString, + options, channel_credentials, + call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py index 4e98c560fd..77ad3c5c91 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py @@ -21,7 +21,7 @@ package='opentelemetry.proto.metrics.v1', syntax='proto3', serialized_options=b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1', - serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xb6\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x66\n\x1finstrumentation_library_metrics\x18\x02 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics\"\xb0\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\"\x9e\x04\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12=\n\tint_gauge\x18\x04 \x01(\x0b\x32(.opentelemetry.proto.metrics.v1.IntGaugeH\x00\x12\x43\n\x0c\x64ouble_gauge\x18\x05 \x01(\x0b\x32+.opentelemetry.proto.metrics.v1.DoubleGaugeH\x00\x12\x39\n\x07int_sum\x18\x06 \x01(\x0b\x32&.opentelemetry.proto.metrics.v1.IntSumH\x00\x12?\n\ndouble_sum\x18\x07 \x01(\x0b\x32).opentelemetry.proto.metrics.v1.DoubleSumH\x00\x12\x45\n\rint_histogram\x18\x08 \x01(\x0b\x32,.opentelemetry.proto.metrics.v1.IntHistogramH\x00\x12K\n\x10\x64ouble_histogram\x18\t \x01(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleHistogramH\x00\x12G\n\x0e\x64ouble_summary\x18\x0b \x01(\x0b\x32-.opentelemetry.proto.metrics.v1.DoubleSummaryH\x00\x42\x06\n\x04\x64\x61ta\"M\n\x08IntGauge\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint\"S\n\x0b\x44oubleGauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleDataPoint\"\xba\x01\n\x06IntSum\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xc0\x01\n\tDoubleSum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.DoubleDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xb3\x01\n\x0cIntHistogram\x12J\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x35.opentelemetry.proto.metrics.v1.IntHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xb9\x01\n\x0f\x44oubleHistogram\x12M\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x38.opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\\\n\rDoubleSummary\x12K\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x36.opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint\"\xd2\x01\n\x0cIntDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x10\x12>\n\texemplars\x18\x05 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar\"\xd8\x01\n\x0f\x44oubleDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x01\x12\x41\n\texemplars\x18\x05 \x03(\x0b\x32..opentelemetry.proto.metrics.v1.DoubleExemplar\"\x98\x02\n\x15IntHistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x10\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12>\n\texemplars\x18\x08 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar\"\x9e\x02\n\x18\x44oubleHistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12\x41\n\texemplars\x18\x08 \x03(\x0b\x32..opentelemetry.proto.metrics.v1.DoubleExemplar\"\xbe\x02\n\x16\x44oubleSummaryDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12_\n\x0fquantile_values\x18\x06 \x03(\x0b\x32\x46.opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.ValueAtQuantile\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01\"\x9f\x01\n\x0bIntExemplar\x12\x46\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\r\n\x05value\x18\x03 \x01(\x10\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\"\xa2\x01\n\x0e\x44oubleExemplar\x12\x46\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\r\n\x05value\x18\x03 \x01(\x01\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02\x42t\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1b\x06proto3' + serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xca\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x66\n\x1finstrumentation_library_metrics\x18\x02 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc4\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xf6\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x41\n\tint_gauge\x18\x04 \x01(\x0b\x32(.opentelemetry.proto.metrics.v1.IntGaugeB\x02\x18\x01H\x00\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12=\n\x07int_sum\x18\x06 \x01(\x0b\x32&.opentelemetry.proto.metrics.v1.IntSumB\x02\x18\x01H\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12I\n\rint_histogram\x18\x08 \x01(\x0b\x32,.opentelemetry.proto.metrics.v1.IntHistogramB\x02\x18\x01H\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61ta\"Q\n\x08IntGauge\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint:\x02\x18\x01\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xbe\x01\n\x06IntSum\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08:\x02\x18\x01\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xb7\x01\n\x0cIntHistogram\x12J\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x35.opentelemetry.proto.metrics.v1.IntHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality:\x02\x18\x01\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\xd6\x01\n\x0cIntDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x10\x12>\n\texemplars\x18\x05 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar:\x02\x18\x01\"\xb4\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x41\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValueB\x02\x18\x01\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.ExemplarB\x07\n\x05value\"\x9c\x02\n\x15IntHistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x10\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12>\n\texemplars\x18\x08 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar:\x02\x18\x01\"\xd3\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x41\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValueB\x02\x18\x01\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\"\xf3\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x41\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValueB\x02\x18\x01\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01\"\xa3\x01\n\x0bIntExemplar\x12\x46\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\r\n\x05value\x18\x03 \x01(\x10\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c:\x02\x18\x01\"\x87\x02\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12J\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValueB\x02\x18\x01\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05value*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02\x42t\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -46,8 +46,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=3746, - serialized_end=3886, + serialized_start=4033, + serialized_end=4173, ) _sym_db.RegisterEnumDescriptor(_AGGREGATIONTEMPORALITY) @@ -79,6 +79,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='schema_url', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.schema_url', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -92,7 +99,7 @@ oneofs=[ ], serialized_start=173, - serialized_end=355, + serialized_end=375, ) @@ -117,6 +124,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='schema_url', full_name='opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics.schema_url', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -129,8 +143,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=358, - serialized_end=534, + serialized_start=378, + serialized_end=574, ) @@ -168,9 +182,9 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=b'\030\001', file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='double_gauge', full_name='opentelemetry.proto.metrics.v1.Metric.double_gauge', index=4, + name='gauge', full_name='opentelemetry.proto.metrics.v1.Metric.gauge', index=4, number=5, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, @@ -182,9 +196,9 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=b'\030\001', file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='double_sum', full_name='opentelemetry.proto.metrics.v1.Metric.double_sum', index=6, + name='sum', full_name='opentelemetry.proto.metrics.v1.Metric.sum', index=6, number=7, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, @@ -196,16 +210,16 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=b'\030\001', file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='double_histogram', full_name='opentelemetry.proto.metrics.v1.Metric.double_histogram', index=8, + name='histogram', full_name='opentelemetry.proto.metrics.v1.Metric.histogram', index=8, number=9, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='double_summary', full_name='opentelemetry.proto.metrics.v1.Metric.double_summary', index=9, + name='summary', full_name='opentelemetry.proto.metrics.v1.Metric.summary', index=9, number=11, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, @@ -226,7 +240,7 @@ name='data', full_name='opentelemetry.proto.metrics.v1.Metric.data', index=0, containing_type=None, fields=[]), ], - serialized_start=537, + serialized_start=577, serialized_end=1079, ) @@ -251,26 +265,26 @@ nested_types=[], enum_types=[ ], - serialized_options=None, + serialized_options=b'\030\001', is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], serialized_start=1081, - serialized_end=1158, + serialized_end=1162, ) -_DOUBLEGAUGE = _descriptor.Descriptor( - name='DoubleGauge', - full_name='opentelemetry.proto.metrics.v1.DoubleGauge', +_GAUGE = _descriptor.Descriptor( + name='Gauge', + full_name='opentelemetry.proto.metrics.v1.Gauge', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='data_points', full_name='opentelemetry.proto.metrics.v1.DoubleGauge.data_points', index=0, + name='data_points', full_name='opentelemetry.proto.metrics.v1.Gauge.data_points', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, @@ -288,8 +302,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1160, - serialized_end=1243, + serialized_start=1164, + serialized_end=1241, ) @@ -327,40 +341,40 @@ nested_types=[], enum_types=[ ], - serialized_options=None, + serialized_options=b'\030\001', is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], - serialized_start=1246, - serialized_end=1432, + serialized_start=1244, + serialized_end=1434, ) -_DOUBLESUM = _descriptor.Descriptor( - name='DoubleSum', - full_name='opentelemetry.proto.metrics.v1.DoubleSum', +_SUM = _descriptor.Descriptor( + name='Sum', + full_name='opentelemetry.proto.metrics.v1.Sum', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='data_points', full_name='opentelemetry.proto.metrics.v1.DoubleSum.data_points', index=0, + name='data_points', full_name='opentelemetry.proto.metrics.v1.Sum.data_points', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.DoubleSum.aggregation_temporality', index=1, + name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.Sum.aggregation_temporality', index=1, number=2, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='is_monotonic', full_name='opentelemetry.proto.metrics.v1.DoubleSum.is_monotonic', index=2, + name='is_monotonic', full_name='opentelemetry.proto.metrics.v1.Sum.is_monotonic', index=2, number=3, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, @@ -378,8 +392,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1435, - serialized_end=1627, + serialized_start=1437, + serialized_end=1623, ) @@ -410,33 +424,33 @@ nested_types=[], enum_types=[ ], - serialized_options=None, + serialized_options=b'\030\001', is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], - serialized_start=1630, + serialized_start=1626, serialized_end=1809, ) -_DOUBLEHISTOGRAM = _descriptor.Descriptor( - name='DoubleHistogram', - full_name='opentelemetry.proto.metrics.v1.DoubleHistogram', +_HISTOGRAM = _descriptor.Descriptor( + name='Histogram', + full_name='opentelemetry.proto.metrics.v1.Histogram', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='data_points', full_name='opentelemetry.proto.metrics.v1.DoubleHistogram.data_points', index=0, + name='data_points', full_name='opentelemetry.proto.metrics.v1.Histogram.data_points', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.DoubleHistogram.aggregation_temporality', index=1, + name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.Histogram.aggregation_temporality', index=1, number=2, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, @@ -455,19 +469,19 @@ oneofs=[ ], serialized_start=1812, - serialized_end=1997, + serialized_end=1985, ) -_DOUBLESUMMARY = _descriptor.Descriptor( - name='DoubleSummary', - full_name='opentelemetry.proto.metrics.v1.DoubleSummary', +_SUMMARY = _descriptor.Descriptor( + name='Summary', + full_name='opentelemetry.proto.metrics.v1.Summary', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='data_points', full_name='opentelemetry.proto.metrics.v1.DoubleSummary.data_points', index=0, + name='data_points', full_name='opentelemetry.proto.metrics.v1.Summary.data_points', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, @@ -485,8 +499,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1999, - serialized_end=2091, + serialized_start=1987, + serialized_end=2067, ) @@ -538,54 +552,68 @@ nested_types=[], enum_types=[ ], - serialized_options=None, + serialized_options=b'\030\001', is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], - serialized_start=2094, - serialized_end=2304, + serialized_start=2070, + serialized_end=2284, ) -_DOUBLEDATAPOINT = _descriptor.Descriptor( - name='DoubleDataPoint', - full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint', +_NUMBERDATAPOINT = _descriptor.Descriptor( + name='NumberDataPoint', + full_name='opentelemetry.proto.metrics.v1.NumberDataPoint', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='labels', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.labels', index=0, - number=1, type=11, cpp_type=10, label=3, + name='attributes', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.attributes', index=0, + number=7, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.start_time_unix_nano', index=1, + name='labels', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.labels', index=1, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\030\001', file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.start_time_unix_nano', index=2, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.time_unix_nano', index=2, + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.time_unix_nano', index=3, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='value', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.value', index=3, + name='as_double', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.as_double', index=4, number=4, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='exemplars', full_name='opentelemetry.proto.metrics.v1.DoubleDataPoint.exemplars', index=4, + name='as_int', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.as_int', index=5, + number=6, type=16, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='exemplars', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.exemplars', index=6, number=5, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, @@ -602,9 +630,12 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='value', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.value', + index=0, containing_type=None, fields=[]), ], - serialized_start=2307, - serialized_end=2523, + serialized_start=2287, + serialized_end=2595, ) @@ -677,75 +708,82 @@ nested_types=[], enum_types=[ ], - serialized_options=None, + serialized_options=b'\030\001', is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], - serialized_start=2526, - serialized_end=2806, + serialized_start=2598, + serialized_end=2882, ) -_DOUBLEHISTOGRAMDATAPOINT = _descriptor.Descriptor( - name='DoubleHistogramDataPoint', - full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint', +_HISTOGRAMDATAPOINT = _descriptor.Descriptor( + name='HistogramDataPoint', + full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='labels', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.labels', index=0, - number=1, type=11, cpp_type=10, label=3, + name='attributes', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.attributes', index=0, + number=9, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.start_time_unix_nano', index=1, + name='labels', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.labels', index=1, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\030\001', file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.start_time_unix_nano', index=2, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.time_unix_nano', index=2, + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.time_unix_nano', index=3, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='count', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.count', index=3, + name='count', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.count', index=4, number=4, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='sum', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.sum', index=4, + name='sum', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.sum', index=5, number=5, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='bucket_counts', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.bucket_counts', index=5, + name='bucket_counts', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.bucket_counts', index=6, number=6, type=6, cpp_type=4, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='explicit_bounds', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.explicit_bounds', index=6, + name='explicit_bounds', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.explicit_bounds', index=7, number=7, type=1, cpp_type=5, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='exemplars', full_name='opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint.exemplars', index=7, + name='exemplars', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.exemplars', index=8, number=8, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, @@ -763,27 +801,27 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2809, - serialized_end=3095, + serialized_start=2885, + serialized_end=3224, ) -_DOUBLESUMMARYDATAPOINT_VALUEATQUANTILE = _descriptor.Descriptor( +_SUMMARYDATAPOINT_VALUEATQUANTILE = _descriptor.Descriptor( name='ValueAtQuantile', - full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.ValueAtQuantile', + full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='quantile', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.ValueAtQuantile.quantile', index=0, + name='quantile', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile.quantile', index=0, number=1, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='value', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.ValueAtQuantile.value', index=1, + name='value', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile.value', index=1, number=2, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, @@ -801,54 +839,61 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3366, - serialized_end=3416, + serialized_start=3548, + serialized_end=3598, ) -_DOUBLESUMMARYDATAPOINT = _descriptor.Descriptor( - name='DoubleSummaryDataPoint', - full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint', +_SUMMARYDATAPOINT = _descriptor.Descriptor( + name='SummaryDataPoint', + full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='labels', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.labels', index=0, - number=1, type=11, cpp_type=10, label=3, + name='attributes', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.attributes', index=0, + number=7, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.start_time_unix_nano', index=1, + name='labels', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.labels', index=1, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\030\001', file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.start_time_unix_nano', index=2, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.time_unix_nano', index=2, + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.time_unix_nano', index=3, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='count', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.count', index=3, + name='count', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.count', index=4, number=4, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='sum', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.sum', index=4, + name='sum', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.sum', index=5, number=5, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='quantile_values', full_name='opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.quantile_values', index=5, + name='quantile_values', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.quantile_values', index=6, number=6, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, @@ -857,7 +902,7 @@ ], extensions=[ ], - nested_types=[_DOUBLESUMMARYDATAPOINT_VALUEATQUANTILE, ], + nested_types=[_SUMMARYDATAPOINT_VALUEATQUANTILE, ], enum_types=[ ], serialized_options=None, @@ -866,8 +911,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3098, - serialized_end=3416, + serialized_start=3227, + serialized_end=3598, ) @@ -919,54 +964,68 @@ nested_types=[], enum_types=[ ], - serialized_options=None, + serialized_options=b'\030\001', is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], - serialized_start=3419, - serialized_end=3578, + serialized_start=3601, + serialized_end=3764, ) -_DOUBLEEXEMPLAR = _descriptor.Descriptor( - name='DoubleExemplar', - full_name='opentelemetry.proto.metrics.v1.DoubleExemplar', +_EXEMPLAR = _descriptor.Descriptor( + name='Exemplar', + full_name='opentelemetry.proto.metrics.v1.Exemplar', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( - name='filtered_labels', full_name='opentelemetry.proto.metrics.v1.DoubleExemplar.filtered_labels', index=0, - number=1, type=11, cpp_type=10, label=3, + name='filtered_attributes', full_name='opentelemetry.proto.metrics.v1.Exemplar.filtered_attributes', index=0, + number=7, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.DoubleExemplar.time_unix_nano', index=1, + name='filtered_labels', full_name='opentelemetry.proto.metrics.v1.Exemplar.filtered_labels', index=1, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\030\001', file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.Exemplar.time_unix_nano', index=2, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='value', full_name='opentelemetry.proto.metrics.v1.DoubleExemplar.value', index=2, + name='as_double', full_name='opentelemetry.proto.metrics.v1.Exemplar.as_double', index=3, number=3, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='span_id', full_name='opentelemetry.proto.metrics.v1.DoubleExemplar.span_id', index=3, + name='as_int', full_name='opentelemetry.proto.metrics.v1.Exemplar.as_int', index=4, + number=6, type=16, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='span_id', full_name='opentelemetry.proto.metrics.v1.Exemplar.span_id', index=5, number=4, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='trace_id', full_name='opentelemetry.proto.metrics.v1.DoubleExemplar.trace_id', index=4, + name='trace_id', full_name='opentelemetry.proto.metrics.v1.Exemplar.trace_id', index=6, number=5, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, @@ -983,9 +1042,12 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='value', full_name='opentelemetry.proto.metrics.v1.Exemplar.value', + index=0, containing_type=None, fields=[]), ], - serialized_start=3581, - serialized_end=3743, + serialized_start=3767, + serialized_end=4030, ) _RESOURCEMETRICS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE @@ -993,74 +1055,90 @@ _INSTRUMENTATIONLIBRARYMETRICS.fields_by_name['instrumentation_library'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONLIBRARY _INSTRUMENTATIONLIBRARYMETRICS.fields_by_name['metrics'].message_type = _METRIC _METRIC.fields_by_name['int_gauge'].message_type = _INTGAUGE -_METRIC.fields_by_name['double_gauge'].message_type = _DOUBLEGAUGE +_METRIC.fields_by_name['gauge'].message_type = _GAUGE _METRIC.fields_by_name['int_sum'].message_type = _INTSUM -_METRIC.fields_by_name['double_sum'].message_type = _DOUBLESUM +_METRIC.fields_by_name['sum'].message_type = _SUM _METRIC.fields_by_name['int_histogram'].message_type = _INTHISTOGRAM -_METRIC.fields_by_name['double_histogram'].message_type = _DOUBLEHISTOGRAM -_METRIC.fields_by_name['double_summary'].message_type = _DOUBLESUMMARY +_METRIC.fields_by_name['histogram'].message_type = _HISTOGRAM +_METRIC.fields_by_name['summary'].message_type = _SUMMARY _METRIC.oneofs_by_name['data'].fields.append( _METRIC.fields_by_name['int_gauge']) _METRIC.fields_by_name['int_gauge'].containing_oneof = _METRIC.oneofs_by_name['data'] _METRIC.oneofs_by_name['data'].fields.append( - _METRIC.fields_by_name['double_gauge']) -_METRIC.fields_by_name['double_gauge'].containing_oneof = _METRIC.oneofs_by_name['data'] + _METRIC.fields_by_name['gauge']) +_METRIC.fields_by_name['gauge'].containing_oneof = _METRIC.oneofs_by_name['data'] _METRIC.oneofs_by_name['data'].fields.append( _METRIC.fields_by_name['int_sum']) _METRIC.fields_by_name['int_sum'].containing_oneof = _METRIC.oneofs_by_name['data'] _METRIC.oneofs_by_name['data'].fields.append( - _METRIC.fields_by_name['double_sum']) -_METRIC.fields_by_name['double_sum'].containing_oneof = _METRIC.oneofs_by_name['data'] + _METRIC.fields_by_name['sum']) +_METRIC.fields_by_name['sum'].containing_oneof = _METRIC.oneofs_by_name['data'] _METRIC.oneofs_by_name['data'].fields.append( _METRIC.fields_by_name['int_histogram']) _METRIC.fields_by_name['int_histogram'].containing_oneof = _METRIC.oneofs_by_name['data'] _METRIC.oneofs_by_name['data'].fields.append( - _METRIC.fields_by_name['double_histogram']) -_METRIC.fields_by_name['double_histogram'].containing_oneof = _METRIC.oneofs_by_name['data'] + _METRIC.fields_by_name['histogram']) +_METRIC.fields_by_name['histogram'].containing_oneof = _METRIC.oneofs_by_name['data'] _METRIC.oneofs_by_name['data'].fields.append( - _METRIC.fields_by_name['double_summary']) -_METRIC.fields_by_name['double_summary'].containing_oneof = _METRIC.oneofs_by_name['data'] + _METRIC.fields_by_name['summary']) +_METRIC.fields_by_name['summary'].containing_oneof = _METRIC.oneofs_by_name['data'] _INTGAUGE.fields_by_name['data_points'].message_type = _INTDATAPOINT -_DOUBLEGAUGE.fields_by_name['data_points'].message_type = _DOUBLEDATAPOINT +_GAUGE.fields_by_name['data_points'].message_type = _NUMBERDATAPOINT _INTSUM.fields_by_name['data_points'].message_type = _INTDATAPOINT _INTSUM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY -_DOUBLESUM.fields_by_name['data_points'].message_type = _DOUBLEDATAPOINT -_DOUBLESUM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY +_SUM.fields_by_name['data_points'].message_type = _NUMBERDATAPOINT +_SUM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY _INTHISTOGRAM.fields_by_name['data_points'].message_type = _INTHISTOGRAMDATAPOINT _INTHISTOGRAM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY -_DOUBLEHISTOGRAM.fields_by_name['data_points'].message_type = _DOUBLEHISTOGRAMDATAPOINT -_DOUBLEHISTOGRAM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY -_DOUBLESUMMARY.fields_by_name['data_points'].message_type = _DOUBLESUMMARYDATAPOINT +_HISTOGRAM.fields_by_name['data_points'].message_type = _HISTOGRAMDATAPOINT +_HISTOGRAM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY +_SUMMARY.fields_by_name['data_points'].message_type = _SUMMARYDATAPOINT _INTDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE _INTDATAPOINT.fields_by_name['exemplars'].message_type = _INTEXEMPLAR -_DOUBLEDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE -_DOUBLEDATAPOINT.fields_by_name['exemplars'].message_type = _DOUBLEEXEMPLAR +_NUMBERDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE +_NUMBERDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_NUMBERDATAPOINT.fields_by_name['exemplars'].message_type = _EXEMPLAR +_NUMBERDATAPOINT.oneofs_by_name['value'].fields.append( + _NUMBERDATAPOINT.fields_by_name['as_double']) +_NUMBERDATAPOINT.fields_by_name['as_double'].containing_oneof = _NUMBERDATAPOINT.oneofs_by_name['value'] +_NUMBERDATAPOINT.oneofs_by_name['value'].fields.append( + _NUMBERDATAPOINT.fields_by_name['as_int']) +_NUMBERDATAPOINT.fields_by_name['as_int'].containing_oneof = _NUMBERDATAPOINT.oneofs_by_name['value'] _INTHISTOGRAMDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE _INTHISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _INTEXEMPLAR -_DOUBLEHISTOGRAMDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE -_DOUBLEHISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _DOUBLEEXEMPLAR -_DOUBLESUMMARYDATAPOINT_VALUEATQUANTILE.containing_type = _DOUBLESUMMARYDATAPOINT -_DOUBLESUMMARYDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE -_DOUBLESUMMARYDATAPOINT.fields_by_name['quantile_values'].message_type = _DOUBLESUMMARYDATAPOINT_VALUEATQUANTILE +_HISTOGRAMDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE +_HISTOGRAMDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_HISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _EXEMPLAR +_SUMMARYDATAPOINT_VALUEATQUANTILE.containing_type = _SUMMARYDATAPOINT +_SUMMARYDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE +_SUMMARYDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_SUMMARYDATAPOINT.fields_by_name['quantile_values'].message_type = _SUMMARYDATAPOINT_VALUEATQUANTILE _INTEXEMPLAR.fields_by_name['filtered_labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE -_DOUBLEEXEMPLAR.fields_by_name['filtered_labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_EXEMPLAR.fields_by_name['filtered_attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE +_EXEMPLAR.fields_by_name['filtered_labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE +_EXEMPLAR.oneofs_by_name['value'].fields.append( + _EXEMPLAR.fields_by_name['as_double']) +_EXEMPLAR.fields_by_name['as_double'].containing_oneof = _EXEMPLAR.oneofs_by_name['value'] +_EXEMPLAR.oneofs_by_name['value'].fields.append( + _EXEMPLAR.fields_by_name['as_int']) +_EXEMPLAR.fields_by_name['as_int'].containing_oneof = _EXEMPLAR.oneofs_by_name['value'] DESCRIPTOR.message_types_by_name['ResourceMetrics'] = _RESOURCEMETRICS DESCRIPTOR.message_types_by_name['InstrumentationLibraryMetrics'] = _INSTRUMENTATIONLIBRARYMETRICS DESCRIPTOR.message_types_by_name['Metric'] = _METRIC DESCRIPTOR.message_types_by_name['IntGauge'] = _INTGAUGE -DESCRIPTOR.message_types_by_name['DoubleGauge'] = _DOUBLEGAUGE +DESCRIPTOR.message_types_by_name['Gauge'] = _GAUGE DESCRIPTOR.message_types_by_name['IntSum'] = _INTSUM -DESCRIPTOR.message_types_by_name['DoubleSum'] = _DOUBLESUM +DESCRIPTOR.message_types_by_name['Sum'] = _SUM DESCRIPTOR.message_types_by_name['IntHistogram'] = _INTHISTOGRAM -DESCRIPTOR.message_types_by_name['DoubleHistogram'] = _DOUBLEHISTOGRAM -DESCRIPTOR.message_types_by_name['DoubleSummary'] = _DOUBLESUMMARY +DESCRIPTOR.message_types_by_name['Histogram'] = _HISTOGRAM +DESCRIPTOR.message_types_by_name['Summary'] = _SUMMARY DESCRIPTOR.message_types_by_name['IntDataPoint'] = _INTDATAPOINT -DESCRIPTOR.message_types_by_name['DoubleDataPoint'] = _DOUBLEDATAPOINT +DESCRIPTOR.message_types_by_name['NumberDataPoint'] = _NUMBERDATAPOINT DESCRIPTOR.message_types_by_name['IntHistogramDataPoint'] = _INTHISTOGRAMDATAPOINT -DESCRIPTOR.message_types_by_name['DoubleHistogramDataPoint'] = _DOUBLEHISTOGRAMDATAPOINT -DESCRIPTOR.message_types_by_name['DoubleSummaryDataPoint'] = _DOUBLESUMMARYDATAPOINT +DESCRIPTOR.message_types_by_name['HistogramDataPoint'] = _HISTOGRAMDATAPOINT +DESCRIPTOR.message_types_by_name['SummaryDataPoint'] = _SUMMARYDATAPOINT DESCRIPTOR.message_types_by_name['IntExemplar'] = _INTEXEMPLAR -DESCRIPTOR.message_types_by_name['DoubleExemplar'] = _DOUBLEEXEMPLAR +DESCRIPTOR.message_types_by_name['Exemplar'] = _EXEMPLAR DESCRIPTOR.enum_types_by_name['AggregationTemporality'] = _AGGREGATIONTEMPORALITY _sym_db.RegisterFileDescriptor(DESCRIPTOR) @@ -1092,12 +1170,12 @@ }) _sym_db.RegisterMessage(IntGauge) -DoubleGauge = _reflection.GeneratedProtocolMessageType('DoubleGauge', (_message.Message,), { - 'DESCRIPTOR' : _DOUBLEGAUGE, +Gauge = _reflection.GeneratedProtocolMessageType('Gauge', (_message.Message,), { + 'DESCRIPTOR' : _GAUGE, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleGauge) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.Gauge) }) -_sym_db.RegisterMessage(DoubleGauge) +_sym_db.RegisterMessage(Gauge) IntSum = _reflection.GeneratedProtocolMessageType('IntSum', (_message.Message,), { 'DESCRIPTOR' : _INTSUM, @@ -1106,12 +1184,12 @@ }) _sym_db.RegisterMessage(IntSum) -DoubleSum = _reflection.GeneratedProtocolMessageType('DoubleSum', (_message.Message,), { - 'DESCRIPTOR' : _DOUBLESUM, +Sum = _reflection.GeneratedProtocolMessageType('Sum', (_message.Message,), { + 'DESCRIPTOR' : _SUM, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleSum) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.Sum) }) -_sym_db.RegisterMessage(DoubleSum) +_sym_db.RegisterMessage(Sum) IntHistogram = _reflection.GeneratedProtocolMessageType('IntHistogram', (_message.Message,), { 'DESCRIPTOR' : _INTHISTOGRAM, @@ -1120,19 +1198,19 @@ }) _sym_db.RegisterMessage(IntHistogram) -DoubleHistogram = _reflection.GeneratedProtocolMessageType('DoubleHistogram', (_message.Message,), { - 'DESCRIPTOR' : _DOUBLEHISTOGRAM, +Histogram = _reflection.GeneratedProtocolMessageType('Histogram', (_message.Message,), { + 'DESCRIPTOR' : _HISTOGRAM, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleHistogram) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.Histogram) }) -_sym_db.RegisterMessage(DoubleHistogram) +_sym_db.RegisterMessage(Histogram) -DoubleSummary = _reflection.GeneratedProtocolMessageType('DoubleSummary', (_message.Message,), { - 'DESCRIPTOR' : _DOUBLESUMMARY, +Summary = _reflection.GeneratedProtocolMessageType('Summary', (_message.Message,), { + 'DESCRIPTOR' : _SUMMARY, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleSummary) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.Summary) }) -_sym_db.RegisterMessage(DoubleSummary) +_sym_db.RegisterMessage(Summary) IntDataPoint = _reflection.GeneratedProtocolMessageType('IntDataPoint', (_message.Message,), { 'DESCRIPTOR' : _INTDATAPOINT, @@ -1141,12 +1219,12 @@ }) _sym_db.RegisterMessage(IntDataPoint) -DoubleDataPoint = _reflection.GeneratedProtocolMessageType('DoubleDataPoint', (_message.Message,), { - 'DESCRIPTOR' : _DOUBLEDATAPOINT, +NumberDataPoint = _reflection.GeneratedProtocolMessageType('NumberDataPoint', (_message.Message,), { + 'DESCRIPTOR' : _NUMBERDATAPOINT, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleDataPoint) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.NumberDataPoint) }) -_sym_db.RegisterMessage(DoubleDataPoint) +_sym_db.RegisterMessage(NumberDataPoint) IntHistogramDataPoint = _reflection.GeneratedProtocolMessageType('IntHistogramDataPoint', (_message.Message,), { 'DESCRIPTOR' : _INTHISTOGRAMDATAPOINT, @@ -1155,27 +1233,27 @@ }) _sym_db.RegisterMessage(IntHistogramDataPoint) -DoubleHistogramDataPoint = _reflection.GeneratedProtocolMessageType('DoubleHistogramDataPoint', (_message.Message,), { - 'DESCRIPTOR' : _DOUBLEHISTOGRAMDATAPOINT, +HistogramDataPoint = _reflection.GeneratedProtocolMessageType('HistogramDataPoint', (_message.Message,), { + 'DESCRIPTOR' : _HISTOGRAMDATAPOINT, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleHistogramDataPoint) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.HistogramDataPoint) }) -_sym_db.RegisterMessage(DoubleHistogramDataPoint) +_sym_db.RegisterMessage(HistogramDataPoint) -DoubleSummaryDataPoint = _reflection.GeneratedProtocolMessageType('DoubleSummaryDataPoint', (_message.Message,), { +SummaryDataPoint = _reflection.GeneratedProtocolMessageType('SummaryDataPoint', (_message.Message,), { 'ValueAtQuantile' : _reflection.GeneratedProtocolMessageType('ValueAtQuantile', (_message.Message,), { - 'DESCRIPTOR' : _DOUBLESUMMARYDATAPOINT_VALUEATQUANTILE, + 'DESCRIPTOR' : _SUMMARYDATAPOINT_VALUEATQUANTILE, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint.ValueAtQuantile) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile) }) , - 'DESCRIPTOR' : _DOUBLESUMMARYDATAPOINT, + 'DESCRIPTOR' : _SUMMARYDATAPOINT, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleSummaryDataPoint) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.SummaryDataPoint) }) -_sym_db.RegisterMessage(DoubleSummaryDataPoint) -_sym_db.RegisterMessage(DoubleSummaryDataPoint.ValueAtQuantile) +_sym_db.RegisterMessage(SummaryDataPoint) +_sym_db.RegisterMessage(SummaryDataPoint.ValueAtQuantile) IntExemplar = _reflection.GeneratedProtocolMessageType('IntExemplar', (_message.Message,), { 'DESCRIPTOR' : _INTEXEMPLAR, @@ -1184,13 +1262,26 @@ }) _sym_db.RegisterMessage(IntExemplar) -DoubleExemplar = _reflection.GeneratedProtocolMessageType('DoubleExemplar', (_message.Message,), { - 'DESCRIPTOR' : _DOUBLEEXEMPLAR, +Exemplar = _reflection.GeneratedProtocolMessageType('Exemplar', (_message.Message,), { + 'DESCRIPTOR' : _EXEMPLAR, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.DoubleExemplar) + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.Exemplar) }) -_sym_db.RegisterMessage(DoubleExemplar) +_sym_db.RegisterMessage(Exemplar) DESCRIPTOR._options = None +_METRIC.fields_by_name['int_gauge']._options = None +_METRIC.fields_by_name['int_sum']._options = None +_METRIC.fields_by_name['int_histogram']._options = None +_INTGAUGE._options = None +_INTSUM._options = None +_INTHISTOGRAM._options = None +_INTDATAPOINT._options = None +_NUMBERDATAPOINT.fields_by_name['labels']._options = None +_INTHISTOGRAMDATAPOINT._options = None +_HISTOGRAMDATAPOINT.fields_by_name['labels']._options = None +_SUMMARYDATAPOINT.fields_by_name['labels']._options = None +_INTEXEMPLAR._options = None +_EXEMPLAR.fields_by_name['filtered_labels']._options = None # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi index d4e03f3a39..fb181488cd 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi @@ -30,6 +30,8 @@ class ResourceMetrics(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... RESOURCE_FIELD_NUMBER: builtins.int INSTRUMENTATION_LIBRARY_METRICS_FIELD_NUMBER: builtins.int + SCHEMA_URL_FIELD_NUMBER: builtins.int + schema_url: typing.Text = ... @property def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: ... @@ -41,15 +43,18 @@ class ResourceMetrics(google.protobuf.message.Message): *, resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., instrumentation_library_metrics : typing.Optional[typing.Iterable[global___InstrumentationLibraryMetrics]] = ..., + schema_url : typing.Text = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal[u"resource",b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library_metrics",b"instrumentation_library_metrics",u"resource",b"resource"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library_metrics",b"instrumentation_library_metrics",u"resource",b"resource",u"schema_url",b"schema_url"]) -> None: ... global___ResourceMetrics = ResourceMetrics class InstrumentationLibraryMetrics(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int METRICS_FIELD_NUMBER: builtins.int + SCHEMA_URL_FIELD_NUMBER: builtins.int + schema_url: typing.Text = ... @property def instrumentation_library(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: ... @@ -61,9 +66,10 @@ class InstrumentationLibraryMetrics(google.protobuf.message.Message): *, instrumentation_library : typing.Optional[opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary] = ..., metrics : typing.Optional[typing.Iterable[global___Metric]] = ..., + schema_url : typing.Text = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library",u"metrics",b"metrics"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library",u"metrics",b"metrics",u"schema_url",b"schema_url"]) -> None: ... global___InstrumentationLibraryMetrics = InstrumentationLibraryMetrics class Metric(google.protobuf.message.Message): @@ -72,12 +78,12 @@ class Metric(google.protobuf.message.Message): DESCRIPTION_FIELD_NUMBER: builtins.int UNIT_FIELD_NUMBER: builtins.int INT_GAUGE_FIELD_NUMBER: builtins.int - DOUBLE_GAUGE_FIELD_NUMBER: builtins.int + GAUGE_FIELD_NUMBER: builtins.int INT_SUM_FIELD_NUMBER: builtins.int - DOUBLE_SUM_FIELD_NUMBER: builtins.int + SUM_FIELD_NUMBER: builtins.int INT_HISTOGRAM_FIELD_NUMBER: builtins.int - DOUBLE_HISTOGRAM_FIELD_NUMBER: builtins.int - DOUBLE_SUMMARY_FIELD_NUMBER: builtins.int + HISTOGRAM_FIELD_NUMBER: builtins.int + SUMMARY_FIELD_NUMBER: builtins.int name: typing.Text = ... description: typing.Text = ... unit: typing.Text = ... @@ -86,22 +92,22 @@ class Metric(google.protobuf.message.Message): def int_gauge(self) -> global___IntGauge: ... @property - def double_gauge(self) -> global___DoubleGauge: ... + def gauge(self) -> global___Gauge: ... @property def int_sum(self) -> global___IntSum: ... @property - def double_sum(self) -> global___DoubleSum: ... + def sum(self) -> global___Sum: ... @property def int_histogram(self) -> global___IntHistogram: ... @property - def double_histogram(self) -> global___DoubleHistogram: ... + def histogram(self) -> global___Histogram: ... @property - def double_summary(self) -> global___DoubleSummary: ... + def summary(self) -> global___Summary: ... def __init__(self, *, @@ -109,16 +115,16 @@ class Metric(google.protobuf.message.Message): description : typing.Text = ..., unit : typing.Text = ..., int_gauge : typing.Optional[global___IntGauge] = ..., - double_gauge : typing.Optional[global___DoubleGauge] = ..., + gauge : typing.Optional[global___Gauge] = ..., int_sum : typing.Optional[global___IntSum] = ..., - double_sum : typing.Optional[global___DoubleSum] = ..., + sum : typing.Optional[global___Sum] = ..., int_histogram : typing.Optional[global___IntHistogram] = ..., - double_histogram : typing.Optional[global___DoubleHistogram] = ..., - double_summary : typing.Optional[global___DoubleSummary] = ..., + histogram : typing.Optional[global___Histogram] = ..., + summary : typing.Optional[global___Summary] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"data",b"data",u"double_gauge",b"double_gauge",u"double_histogram",b"double_histogram",u"double_sum",b"double_sum",u"double_summary",b"double_summary",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"data",b"data",u"description",b"description",u"double_gauge",b"double_gauge",u"double_histogram",b"double_histogram",u"double_sum",b"double_sum",u"double_summary",b"double_summary",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum",u"name",b"name",u"unit",b"unit"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal[u"data",b"data"]) -> typing_extensions.Literal["int_gauge","double_gauge","int_sum","double_sum","int_histogram","double_histogram","double_summary"]: ... + def HasField(self, field_name: typing_extensions.Literal[u"data",b"data",u"gauge",b"gauge",u"histogram",b"histogram",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum",u"sum",b"sum",u"summary",b"summary"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"data",b"data",u"description",b"description",u"gauge",b"gauge",u"histogram",b"histogram",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum",u"name",b"name",u"sum",b"sum",u"summary",b"summary",u"unit",b"unit"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal[u"data",b"data"]) -> typing_extensions.Literal["int_gauge","gauge","int_sum","sum","int_histogram","histogram","summary"]: ... global___Metric = Metric class IntGauge(google.protobuf.message.Message): @@ -135,19 +141,19 @@ class IntGauge(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal[u"data_points",b"data_points"]) -> None: ... global___IntGauge = IntGauge -class DoubleGauge(google.protobuf.message.Message): +class Gauge(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int @property - def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleDataPoint]: ... + def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NumberDataPoint]: ... def __init__(self, *, - data_points : typing.Optional[typing.Iterable[global___DoubleDataPoint]] = ..., + data_points : typing.Optional[typing.Iterable[global___NumberDataPoint]] = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal[u"data_points",b"data_points"]) -> None: ... -global___DoubleGauge = DoubleGauge +global___Gauge = Gauge class IntSum(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... @@ -169,7 +175,7 @@ class IntSum(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points",u"is_monotonic",b"is_monotonic"]) -> None: ... global___IntSum = IntSum -class DoubleSum(google.protobuf.message.Message): +class Sum(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int @@ -178,16 +184,16 @@ class DoubleSum(google.protobuf.message.Message): is_monotonic: builtins.bool = ... @property - def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleDataPoint]: ... + def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NumberDataPoint]: ... def __init__(self, *, - data_points : typing.Optional[typing.Iterable[global___DoubleDataPoint]] = ..., + data_points : typing.Optional[typing.Iterable[global___NumberDataPoint]] = ..., aggregation_temporality : global___AggregationTemporality.V = ..., is_monotonic : builtins.bool = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points",u"is_monotonic",b"is_monotonic"]) -> None: ... -global___DoubleSum = DoubleSum +global___Sum = Sum class IntHistogram(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... @@ -206,36 +212,36 @@ class IntHistogram(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points"]) -> None: ... global___IntHistogram = IntHistogram -class DoubleHistogram(google.protobuf.message.Message): +class Histogram(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int aggregation_temporality: global___AggregationTemporality.V = ... @property - def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleHistogramDataPoint]: ... + def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___HistogramDataPoint]: ... def __init__(self, *, - data_points : typing.Optional[typing.Iterable[global___DoubleHistogramDataPoint]] = ..., + data_points : typing.Optional[typing.Iterable[global___HistogramDataPoint]] = ..., aggregation_temporality : global___AggregationTemporality.V = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points"]) -> None: ... -global___DoubleHistogram = DoubleHistogram +global___Histogram = Histogram -class DoubleSummary(google.protobuf.message.Message): +class Summary(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int @property - def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleSummaryDataPoint]: ... + def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___SummaryDataPoint]: ... def __init__(self, *, - data_points : typing.Optional[typing.Iterable[global___DoubleSummaryDataPoint]] = ..., + data_points : typing.Optional[typing.Iterable[global___SummaryDataPoint]] = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal[u"data_points",b"data_points"]) -> None: ... -global___DoubleSummary = DoubleSummary +global___Summary = Summary class IntDataPoint(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... @@ -265,33 +271,43 @@ class IntDataPoint(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal[u"exemplars",b"exemplars",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... global___IntDataPoint = IntDataPoint -class DoubleDataPoint(google.protobuf.message.Message): +class NumberDataPoint(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + ATTRIBUTES_FIELD_NUMBER: builtins.int LABELS_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int + AS_DOUBLE_FIELD_NUMBER: builtins.int + AS_INT_FIELD_NUMBER: builtins.int EXEMPLARS_FIELD_NUMBER: builtins.int start_time_unix_nano: builtins.int = ... time_unix_nano: builtins.int = ... - value: builtins.float = ... + as_double: builtins.float = ... + as_int: builtins.int = ... + + @property + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... @property def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... @property - def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleExemplar]: ... + def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: ... def __init__(self, *, + attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., start_time_unix_nano : builtins.int = ..., time_unix_nano : builtins.int = ..., - value : builtins.float = ..., - exemplars : typing.Optional[typing.Iterable[global___DoubleExemplar]] = ..., + as_double : builtins.float = ..., + as_int : builtins.int = ..., + exemplars : typing.Optional[typing.Iterable[global___Exemplar]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"exemplars",b"exemplars",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... -global___DoubleDataPoint = DoubleDataPoint + def HasField(self, field_name: typing_extensions.Literal[u"as_double",b"as_double",u"as_int",b"as_int",u"value",b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"as_double",b"as_double",u"as_int",b"as_int",u"attributes",b"attributes",u"exemplars",b"exemplars",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal[u"value",b"value"]) -> typing_extensions.Literal["as_double","as_int"]: ... +global___NumberDataPoint = NumberDataPoint class IntHistogramDataPoint(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... @@ -330,8 +346,9 @@ class IntHistogramDataPoint(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal[u"bucket_counts",b"bucket_counts",u"count",b"count",u"exemplars",b"exemplars",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... global___IntHistogramDataPoint = IntHistogramDataPoint -class DoubleHistogramDataPoint(google.protobuf.message.Message): +class HistogramDataPoint(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + ATTRIBUTES_FIELD_NUMBER: builtins.int LABELS_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int @@ -347,14 +364,18 @@ class DoubleHistogramDataPoint(google.protobuf.message.Message): bucket_counts: google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int] = ... explicit_bounds: google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float] = ... + @property + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... + @property def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... @property - def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleExemplar]: ... + def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: ... def __init__(self, *, + attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., start_time_unix_nano : builtins.int = ..., time_unix_nano : builtins.int = ..., @@ -362,12 +383,12 @@ class DoubleHistogramDataPoint(google.protobuf.message.Message): sum : builtins.float = ..., bucket_counts : typing.Optional[typing.Iterable[builtins.int]] = ..., explicit_bounds : typing.Optional[typing.Iterable[builtins.float]] = ..., - exemplars : typing.Optional[typing.Iterable[global___DoubleExemplar]] = ..., + exemplars : typing.Optional[typing.Iterable[global___Exemplar]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"bucket_counts",b"bucket_counts",u"count",b"count",u"exemplars",b"exemplars",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... -global___DoubleHistogramDataPoint = DoubleHistogramDataPoint + def ClearField(self, field_name: typing_extensions.Literal[u"attributes",b"attributes",u"bucket_counts",b"bucket_counts",u"count",b"count",u"exemplars",b"exemplars",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... +global___HistogramDataPoint = HistogramDataPoint -class DoubleSummaryDataPoint(google.protobuf.message.Message): +class SummaryDataPoint(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... class ValueAtQuantile(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... @@ -383,6 +404,7 @@ class DoubleSummaryDataPoint(google.protobuf.message.Message): ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal[u"quantile",b"quantile",u"value",b"value"]) -> None: ... + ATTRIBUTES_FIELD_NUMBER: builtins.int LABELS_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int @@ -394,23 +416,27 @@ class DoubleSummaryDataPoint(google.protobuf.message.Message): count: builtins.int = ... sum: builtins.float = ... + @property + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... + @property def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... @property - def quantile_values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DoubleSummaryDataPoint.ValueAtQuantile]: ... + def quantile_values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___SummaryDataPoint.ValueAtQuantile]: ... def __init__(self, *, + attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., start_time_unix_nano : builtins.int = ..., time_unix_nano : builtins.int = ..., count : builtins.int = ..., sum : builtins.float = ..., - quantile_values : typing.Optional[typing.Iterable[global___DoubleSummaryDataPoint.ValueAtQuantile]] = ..., + quantile_values : typing.Optional[typing.Iterable[global___SummaryDataPoint.ValueAtQuantile]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"count",b"count",u"labels",b"labels",u"quantile_values",b"quantile_values",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... -global___DoubleSummaryDataPoint = DoubleSummaryDataPoint + def ClearField(self, field_name: typing_extensions.Literal[u"attributes",b"attributes",u"count",b"count",u"labels",b"labels",u"quantile_values",b"quantile_values",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... +global___SummaryDataPoint = SummaryDataPoint class IntExemplar(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... @@ -438,28 +464,38 @@ class IntExemplar(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal[u"filtered_labels",b"filtered_labels",u"span_id",b"span_id",u"time_unix_nano",b"time_unix_nano",u"trace_id",b"trace_id",u"value",b"value"]) -> None: ... global___IntExemplar = IntExemplar -class DoubleExemplar(google.protobuf.message.Message): +class Exemplar(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + FILTERED_ATTRIBUTES_FIELD_NUMBER: builtins.int FILTERED_LABELS_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int + AS_DOUBLE_FIELD_NUMBER: builtins.int + AS_INT_FIELD_NUMBER: builtins.int SPAN_ID_FIELD_NUMBER: builtins.int TRACE_ID_FIELD_NUMBER: builtins.int time_unix_nano: builtins.int = ... - value: builtins.float = ... + as_double: builtins.float = ... + as_int: builtins.int = ... span_id: builtins.bytes = ... trace_id: builtins.bytes = ... + @property + def filtered_attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... + @property def filtered_labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... def __init__(self, *, + filtered_attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., filtered_labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., time_unix_nano : builtins.int = ..., - value : builtins.float = ..., + as_double : builtins.float = ..., + as_int : builtins.int = ..., span_id : builtins.bytes = ..., trace_id : builtins.bytes = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"filtered_labels",b"filtered_labels",u"span_id",b"span_id",u"time_unix_nano",b"time_unix_nano",u"trace_id",b"trace_id",u"value",b"value"]) -> None: ... -global___DoubleExemplar = DoubleExemplar + def HasField(self, field_name: typing_extensions.Literal[u"as_double",b"as_double",u"as_int",b"as_int",u"value",b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal[u"as_double",b"as_double",u"as_int",b"as_int",u"filtered_attributes",b"filtered_attributes",u"filtered_labels",b"filtered_labels",u"span_id",b"span_id",u"time_unix_nano",b"time_unix_nano",u"trace_id",b"trace_id",u"value",b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal[u"value",b"value"]) -> typing_extensions.Literal["as_double","as_int"]: ... +global___Exemplar = Exemplar diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py index d5937acb50..b5ae44795c 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py @@ -20,7 +20,7 @@ package='opentelemetry.proto.trace.v1', syntax='proto3', serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1', - serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xae\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\"\xa8\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xdd\x07\n\x06Status\x12V\n\x0f\x64\x65precated_code\x18\x01 \x01(\x0e\x32\x39.opentelemetry.proto.trace.v1.Status.DeprecatedStatusCodeB\x02\x18\x01\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"\xda\x05\n\x14\x44\x65precatedStatusCode\x12\x1d\n\x19\x44\x45PRECATED_STATUS_CODE_OK\x10\x00\x12$\n DEPRECATED_STATUS_CODE_CANCELLED\x10\x01\x12(\n$DEPRECATED_STATUS_CODE_UNKNOWN_ERROR\x10\x02\x12+\n\'DEPRECATED_STATUS_CODE_INVALID_ARGUMENT\x10\x03\x12,\n(DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED\x10\x04\x12$\n DEPRECATED_STATUS_CODE_NOT_FOUND\x10\x05\x12)\n%DEPRECATED_STATUS_CODE_ALREADY_EXISTS\x10\x06\x12,\n(DEPRECATED_STATUS_CODE_PERMISSION_DENIED\x10\x07\x12-\n)DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED\x10\x08\x12.\n*DEPRECATED_STATUS_CODE_FAILED_PRECONDITION\x10\t\x12\"\n\x1e\x44\x45PRECATED_STATUS_CODE_ABORTED\x10\n\x12\'\n#DEPRECATED_STATUS_CODE_OUT_OF_RANGE\x10\x0b\x12(\n$DEPRECATED_STATUS_CODE_UNIMPLEMENTED\x10\x0c\x12)\n%DEPRECATED_STATUS_CODE_INTERNAL_ERROR\x10\r\x12&\n\"DEPRECATED_STATUS_CODE_UNAVAILABLE\x10\x0e\x12$\n DEPRECATED_STATUS_CODE_DATA_LOSS\x10\x0f\x12*\n&DEPRECATED_STATUS_CODE_UNAUTHENTICATED\x10\x10\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' + serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xc2\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xbc\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xdd\x07\n\x06Status\x12V\n\x0f\x64\x65precated_code\x18\x01 \x01(\x0e\x32\x39.opentelemetry.proto.trace.v1.Status.DeprecatedStatusCodeB\x02\x18\x01\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"\xda\x05\n\x14\x44\x65precatedStatusCode\x12\x1d\n\x19\x44\x45PRECATED_STATUS_CODE_OK\x10\x00\x12$\n DEPRECATED_STATUS_CODE_CANCELLED\x10\x01\x12(\n$DEPRECATED_STATUS_CODE_UNKNOWN_ERROR\x10\x02\x12+\n\'DEPRECATED_STATUS_CODE_INVALID_ARGUMENT\x10\x03\x12,\n(DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED\x10\x04\x12$\n DEPRECATED_STATUS_CODE_NOT_FOUND\x10\x05\x12)\n%DEPRECATED_STATUS_CODE_ALREADY_EXISTS\x10\x06\x12,\n(DEPRECATED_STATUS_CODE_PERMISSION_DENIED\x10\x07\x12-\n)DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED\x10\x08\x12.\n*DEPRECATED_STATUS_CODE_FAILED_PRECONDITION\x10\t\x12\"\n\x1e\x44\x45PRECATED_STATUS_CODE_ABORTED\x10\n\x12\'\n#DEPRECATED_STATUS_CODE_OUT_OF_RANGE\x10\x0b\x12(\n$DEPRECATED_STATUS_CODE_UNIMPLEMENTED\x10\x0c\x12)\n%DEPRECATED_STATUS_CODE_INTERNAL_ERROR\x10\r\x12&\n\"DEPRECATED_STATUS_CODE_UNAVAILABLE\x10\x0e\x12$\n DEPRECATED_STATUS_CODE_DATA_LOSS\x10\x0f\x12*\n&DEPRECATED_STATUS_CODE_UNAUTHENTICATED\x10\x10\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -59,8 +59,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1360, - serialized_end=1513, + serialized_start=1400, + serialized_end=1553, ) _sym_db.RegisterEnumDescriptor(_SPAN_SPANKIND) @@ -141,8 +141,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1695, - serialized_end=2425, + serialized_start=1735, + serialized_end=2465, ) _sym_db.RegisterEnumDescriptor(_STATUS_DEPRECATEDSTATUSCODE) @@ -167,8 +167,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=2427, - serialized_end=2505, + serialized_start=2467, + serialized_end=2545, ) _sym_db.RegisterEnumDescriptor(_STATUS_STATUSCODE) @@ -194,6 +194,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='schema_url', full_name='opentelemetry.proto.trace.v1.ResourceSpans.schema_url', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -207,7 +214,7 @@ oneofs=[ ], serialized_start=167, - serialized_end=341, + serialized_end=361, ) @@ -232,6 +239,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='schema_url', full_name='opentelemetry.proto.trace.v1.InstrumentationLibrarySpans.schema_url', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -244,8 +258,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=344, - serialized_end=512, + serialized_start=364, + serialized_end=552, ) @@ -296,8 +310,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1057, - serialized_end=1197, + serialized_start=1097, + serialized_end=1237, ) _SPAN_LINK = _descriptor.Descriptor( @@ -354,8 +368,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1200, - serialized_end=1357, + serialized_start=1240, + serialized_end=1397, ) _SPAN = _descriptor.Descriptor( @@ -483,8 +497,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=515, - serialized_end=1513, + serialized_start=555, + serialized_end=1553, ) @@ -530,8 +544,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1516, - serialized_end=2505, + serialized_start=1556, + serialized_end=2545, ) _RESOURCESPANS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi index 71e323b99e..46b37dc5d7 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi @@ -18,6 +18,8 @@ class ResourceSpans(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... RESOURCE_FIELD_NUMBER: builtins.int INSTRUMENTATION_LIBRARY_SPANS_FIELD_NUMBER: builtins.int + SCHEMA_URL_FIELD_NUMBER: builtins.int + schema_url: typing.Text = ... @property def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: ... @@ -29,15 +31,18 @@ class ResourceSpans(google.protobuf.message.Message): *, resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., instrumentation_library_spans : typing.Optional[typing.Iterable[global___InstrumentationLibrarySpans]] = ..., + schema_url : typing.Text = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal[u"resource",b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library_spans",b"instrumentation_library_spans",u"resource",b"resource"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library_spans",b"instrumentation_library_spans",u"resource",b"resource",u"schema_url",b"schema_url"]) -> None: ... global___ResourceSpans = ResourceSpans class InstrumentationLibrarySpans(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int SPANS_FIELD_NUMBER: builtins.int + SCHEMA_URL_FIELD_NUMBER: builtins.int + schema_url: typing.Text = ... @property def instrumentation_library(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: ... @@ -49,9 +54,10 @@ class InstrumentationLibrarySpans(google.protobuf.message.Message): *, instrumentation_library : typing.Optional[opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary] = ..., spans : typing.Optional[typing.Iterable[global___Span]] = ..., + schema_url : typing.Text = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library",u"spans",b"spans"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library",u"schema_url",b"schema_url",u"spans",b"spans"]) -> None: ... global___InstrumentationLibrarySpans = InstrumentationLibrarySpans class Span(google.protobuf.message.Message): diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index d3082b0599..6833c85c4e 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -12,7 +12,7 @@ # PROTO_REPO_DIR - the path to an existing checkout of the opentelemetry-proto repo # Pinned commit/branch/tag for the current version used in opentelemetry-proto python package. -PROTO_REPO_BRANCH_OR_COMMIT="v0.7.0" +PROTO_REPO_BRANCH_OR_COMMIT="v0.9.0" set -e From 70e55290d0e068a941541ece76188726fe489bf1 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 1 Jun 2021 18:03:58 -0600 Subject: [PATCH 0897/1517] Add employer for Diego (#1885) Fixes #1884 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e6a174e884..fc1fac6a32 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): - [Aaron Abbott](https://github.com/aabmass), Google -- [Diego Hurtado](https://github.com/ocelotl) +- [Diego Hurtado](https://github.com/ocelotl), Lightstep - [Owais Lone](https://github.com/owais), Splunk - [Srikanth Chekuri](https://github.com/lonewolf3739) From 2b7592dbf948df712594137a3383fab5ab418bbb Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 1 Jun 2021 18:23:13 -0700 Subject: [PATCH 0898/1517] Release for V1.3.0 and 0.22b0, update release script (#1883) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 6 +++- .../error_handler/error_handler_0/setup.cfg | 2 +- .../src/error_handler_0/version.py | 2 +- .../error_handler/error_handler_1/setup.cfg | 2 +- .../src/error_handler_1/version.py | 2 +- eachdist.ini | 34 +++++++++---------- .../setup.cfg | 4 +-- .../exporter/jaeger/proto/grpc/version.py | 2 +- .../setup.cfg | 4 +-- .../exporter/jaeger/thrift/version.py | 2 +- .../opentelemetry-exporter-jaeger/setup.cfg | 4 +-- .../opentelemetry/exporter/jaeger/version.py | 2 +- .../setup.cfg | 4 +-- .../exporter/opencensus/version.py | 2 +- .../setup.cfg | 6 ++-- .../exporter/otlp/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-otlp/setup.cfg | 2 +- .../opentelemetry/exporter/otlp/version.py | 2 +- .../setup.cfg | 4 +-- .../exporter/zipkin/json/version.py | 2 +- .../setup.cfg | 6 ++-- .../exporter/zipkin/proto/http/version.py | 2 +- .../opentelemetry-exporter-zipkin/setup.cfg | 4 +-- .../opentelemetry/exporter/zipkin/version.py | 2 +- .../src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 8 ++--- .../src/opentelemetry/distro/version.py | 2 +- .../src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 +-- .../src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../opentelemetry-propagator-b3/setup.cfg | 2 +- .../opentelemetry/propagators/b3/version.py | 2 +- .../opentelemetry-propagator-jaeger/setup.cfg | 2 +- .../propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 4 +-- .../shim/opentracing_shim/version.py | 2 +- tests/util/setup.cfg | 4 +-- tests/util/src/opentelemetry/test/version.py | 2 +- 40 files changed, 76 insertions(+), 72 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b715ff520c..492fd2cd3d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: c46b39e4e94af5ae8f97e684960f1aec0595c33e + CONTRIB_REPO_SHA: bfc767f40e50643adf36c07952a07b8f1e380b2f jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index ab7c822691..914e7b975a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.2.0-0.21b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.3.0-0.22b0...HEAD) + +## [1.3.0-0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01 + + ### Added - Allow span limits to be set programatically via TracerProvider. diff --git a/docs/examples/error_handler/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg index f80c814cb5..15b685970c 100644 --- a/docs/examples/error_handler/error_handler_0/setup.cfg +++ b/docs/examples/error_handler/error_handler_0/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.3.0.dev0 + opentelemetry-sdk == 1.3.0 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py index 2b08175266..0011c3d6b2 100644 --- a/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py +++ b/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.21.dev0" +__version__ = "0.22b0" diff --git a/docs/examples/error_handler/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg index 5fb21fdcaf..ba752a472c 100644 --- a/docs/examples/error_handler/error_handler_1/setup.cfg +++ b/docs/examples/error_handler/error_handler_1/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.3.0.dev0 + opentelemetry-sdk == 1.3.0 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py index 2b08175266..0011c3d6b2 100644 --- a/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py +++ b/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.21.dev0" +__version__ = "0.22b0" diff --git a/eachdist.ini b/eachdist.ini index 121ae93dab..276cb74e90 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -13,29 +13,29 @@ sortfirst= exporter/* [stable] -version=1.3.0.dev0 +version=1.3.0 packages= opentelemetry-sdk opentelemetry-proto - propagator/opentelemetry-propagator-jaeger - propagator/opentelemetry-propagator-b3 - exporter/opentelemetry-exporter-zipkin-proto-http - exporter/opentelemetry-exporter-zipkin-json - exporter/opentelemetry-exporter-zipkin - exporter/opentelemetry-exporter-otlp-proto-grpc - exporter/opentelemetry-exporter-otlp - exporter/opentelemetry-exporter-jaeger-thrift - exporter/opentelemetry-exporter-jaeger-proto-grpc - exporter/opentelemetry-exporter-jaeger + opentelemetry-propagator-jaeger + opentelemetry-propagator-b3 + opentelemetry-exporter-zipkin-proto-http + opentelemetry-exporter-zipkin-json + opentelemetry-exporter-zipkin + opentelemetry-exporter-otlp-proto-grpc + opentelemetry-exporter-otlp + opentelemetry-exporter-jaeger-thrift + opentelemetry-exporter-jaeger-proto-grpc + opentelemetry-exporter-jaeger opentelemetry-api [prerelease] -version=0.22.dev0 +version=0.22b0 packages= opentelemetry-opentracing-shim - exporter/opentelemetry-exporter-opencensus + opentelemetry-exporter-opencensus opentelemetry-distro opentelemetry-semantic-conventions opentelemetry-test @@ -46,12 +46,12 @@ packages= version=1.10a0 packages= - exporter/opentelemetry-exporter-prometheus-remote-write - exporter/opentelemetry-exporter-prometheus + opentelemetry-exporter-prometheus-remote-write + opentelemetry-exporter-prometheus opentelemetry-api opentelemetry-sdk - exporter/opentelemetry-exporter-otlp-proto-grpc - exporter/opentelemetry-exporter-otlp + opentelemetry-exporter-otlp-proto-grpc + opentelemetry-exporter-otlp [lintroots] extraroots=examples/*,scripts/ diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index d6c5e2bf1c..7740ce4f34 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 - opentelemetry-api == 1.3.0.dev0 - opentelemetry-sdk == 1.3.0.dev0 + opentelemetry-api == 1.3.0 + opentelemetry-sdk == 1.3.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 7369c05815..d068934c69 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0.dev0" +__version__ = "1.3.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index 2adcbf0805..10b359e3e6 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 1.3.0.dev0 - opentelemetry-sdk == 1.3.0.dev0 + opentelemetry-api == 1.3.0 + opentelemetry-sdk == 1.3.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 7369c05815..d068934c69 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0.dev0" +__version__ = "1.3.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 02bcc2e243..e6b6915395 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,8 +39,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.3.0.dev0 - opentelemetry-exporter-jaeger-thrift == 1.3.0.dev0 + opentelemetry-exporter-jaeger-proto-grpc == 1.3.0 + opentelemetry-exporter-jaeger-thrift == 1.3.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 7369c05815..d068934c69 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0.dev0" +__version__ = "1.3.0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 7294615212..e3b56f9092 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 1.3.0.dev0 - opentelemetry-sdk == 1.3.0.dev0 + opentelemetry-api == 1.3.0 + opentelemetry-sdk == 1.3.0 protobuf >= 3.13.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index ba922d2bed..0011c3d6b2 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.22.dev0" +__version__ = "0.22b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 09349faaba..717267f57f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 - opentelemetry-api == 1.3.0.dev0 - opentelemetry-sdk == 1.3.0.dev0 - opentelemetry-proto == 1.3.0.dev0 + opentelemetry-api == 1.3.0 + opentelemetry-sdk == 1.3.0 + opentelemetry-proto == 1.3.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index f94a1f8a6c..01d2fea7b3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0.dev0" +__version__ = "1.3.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 5502be5f8e..229cbdd64e 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -38,4 +38,4 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.3.0.dev0 + opentelemetry-exporter-otlp-proto-grpc == 1.3.0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index f94a1f8a6c..01d2fea7b3 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0.dev0" +__version__ = "1.3.0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index db2586c6da..0c254964fa 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -42,8 +42,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 1.3.0.dev0 - opentelemetry-sdk == 1.3.0.dev0 + opentelemetry-api == 1.3.0 + opentelemetry-sdk == 1.3.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index f94a1f8a6c..01d2fea7b3 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0.dev0" +__version__ = "1.3.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 202c37df0e..cdc36afad1 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -43,9 +43,9 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 - opentelemetry-api == 1.3.0.dev0 - opentelemetry-sdk == 1.3.0.dev0 - opentelemetry-exporter-zipkin-json == 1.3.0.dev0 + opentelemetry-api == 1.3.0 + opentelemetry-sdk == 1.3.0 + opentelemetry-exporter-zipkin-json == 1.3.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index f94a1f8a6c..01d2fea7b3 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0.dev0" +__version__ = "1.3.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 2574034923..fc76583042 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.3.0.dev0 - opentelemetry-exporter-zipkin-proto-http == 1.3.0.dev0 + opentelemetry-exporter-zipkin-json == 1.3.0 + opentelemetry-exporter-zipkin-proto-http == 1.3.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index f94a1f8a6c..01d2fea7b3 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0.dev0" +__version__ = "1.3.0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index f94a1f8a6c..01d2fea7b3 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0.dev0" +__version__ = "1.3.0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 02b69aa7c1..f4e84e6cff 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.3.0.dev0 - opentelemetry-instrumentation == 0.22.dev0 - opentelemetry-sdk == 1.3.0.dev0 + opentelemetry-api == 1.3.0 + opentelemetry-instrumentation == 0.22b0 + opentelemetry-sdk == 1.3.0 [options.packages.find] where = src @@ -57,4 +57,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.3.0.dev0 + opentelemetry-exporter-otlp == 1.3.0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index ba922d2bed..0011c3d6b2 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.22.dev0" +__version__ = "0.22b0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index f94a1f8a6c..01d2fea7b3 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0.dev0" +__version__ = "1.3.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 14863f633a..e08b62aa93 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.3.0.dev0 - opentelemetry-semantic-conventions == 0.22.dev0 + opentelemetry-api == 1.3.0 + opentelemetry-semantic-conventions == 0.22b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index f94a1f8a6c..01d2fea7b3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0.dev0" +__version__ = "1.3.0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index ba922d2bed..0011c3d6b2 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.22.dev0" +__version__ = "0.22b0" diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index bedcdab8f2..640b2b9853 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.3.0.dev0 + opentelemetry-api == 1.3.0 deprecated >= 1.2.6 [options.extras_require] diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index f94a1f8a6c..01d2fea7b3 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0.dev0" +__version__ = "1.3.0" diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index 75c61b77ac..a90c8a22f0 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.3.0.dev0 + opentelemetry-api == 1.3.0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index f94a1f8a6c..01d2fea7b3 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0.dev0" +__version__ = "1.3.0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 24528123f3..1577cb3171 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 1.3.0.dev0 + opentelemetry-api == 1.3.0 [options.extras_require] test = - opentelemetry-test == 0.22.dev0 + opentelemetry-test == 0.22b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index ba922d2bed..0011c3d6b2 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.22.dev0" +__version__ = "0.22b0" diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index e94c832a5c..9b478a051e 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -37,8 +37,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.3.0.dev0 - opentelemetry-sdk == 1.3.0.dev0 + opentelemetry-api == 1.3.0 + opentelemetry-sdk == 1.3.0 [options.extras_require] test = flask~=1.0 diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 7f955c0090..d544cffe1c 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.22.dev0" +__version__ = "0.22b0" From ea4cdfe42435dda7c39a264612e89dae09eac620 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 2 Jun 2021 09:16:22 -0700 Subject: [PATCH 0899/1517] [chore] update main after 1.3.0 release (#1886) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 -- RELEASING.md | 1 + docs/examples/error_handler/error_handler_0/setup.cfg | 2 +- .../error_handler_0/src/error_handler_0/version.py | 2 +- docs/examples/error_handler/error_handler_1/setup.cfg | 2 +- .../error_handler_1/src/error_handler_1/version.py | 2 +- eachdist.ini | 4 ++-- .../opentelemetry-exporter-jaeger-proto-grpc/setup.cfg | 4 ++-- .../opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-json/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/setup.cfg | 6 +++--- .../opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 8 ++++---- opentelemetry-distro/src/opentelemetry/distro/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- propagator/opentelemetry-propagator-b3/setup.cfg | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- propagator/opentelemetry-propagator-jaeger/setup.cfg | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 4 ++-- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/util/setup.cfg | 4 ++-- tests/util/src/opentelemetry/test/version.py | 2 +- 41 files changed, 57 insertions(+), 58 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 492fd2cd3d..1c8693ed0d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: bfc767f40e50643adf36c07952a07b8f1e380b2f + CONTRIB_REPO_SHA: 01bb63fb8cc429a17f1fb0c93f7ac72a3fc7b4fc jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 914e7b975a..d2808ecf80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.3.0-0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01 - - ### Added - Allow span limits to be set programatically via TracerProvider. ([#1877](https://github.com/open-telemetry/opentelemetry-python/pull/1877)) diff --git a/RELEASING.md b/RELEASING.md index 904f8cb4a1..ab1e955074 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -59,6 +59,7 @@ This will ensure the docs are pointing at the stable release. ```bash git tag -d stable git tag stable +git push --delete origin tagname git push origin stable ``` diff --git a/docs/examples/error_handler/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg index 15b685970c..adcc0614b8 100644 --- a/docs/examples/error_handler/error_handler_0/setup.cfg +++ b/docs/examples/error_handler/error_handler_0/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.3.0 + opentelemetry-sdk == 1.4.0.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py b/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py index 0011c3d6b2..c829b95757 100644 --- a/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py +++ b/docs/examples/error_handler/error_handler_0/src/error_handler_0/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.22b0" +__version__ = "0.23.dev0" diff --git a/docs/examples/error_handler/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg index ba752a472c..b32809866c 100644 --- a/docs/examples/error_handler/error_handler_1/setup.cfg +++ b/docs/examples/error_handler/error_handler_1/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.3.0 + opentelemetry-sdk == 1.4.0.dev0 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py b/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py index 0011c3d6b2..c829b95757 100644 --- a/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py +++ b/docs/examples/error_handler/error_handler_1/src/error_handler_1/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.22b0" +__version__ = "0.23.dev0" diff --git a/eachdist.ini b/eachdist.ini index 276cb74e90..ae202e7232 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -13,7 +13,7 @@ sortfirst= exporter/* [stable] -version=1.3.0 +version=1.4.0.dev0 packages= opentelemetry-sdk @@ -31,7 +31,7 @@ packages= opentelemetry-api [prerelease] -version=0.22b0 +version=0.23.dev0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index 7740ce4f34..8fbb0cdc8e 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 - opentelemetry-api == 1.3.0 - opentelemetry-sdk == 1.3.0 + opentelemetry-api == 1.4.0.dev0 + opentelemetry-sdk == 1.4.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index d068934c69..c173c28d8d 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index 10b359e3e6..f1780aac64 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 1.3.0 - opentelemetry-sdk == 1.3.0 + opentelemetry-api == 1.4.0.dev0 + opentelemetry-sdk == 1.4.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index d068934c69..c173c28d8d 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index e6b6915395..faa9d40a3a 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,8 +39,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.3.0 - opentelemetry-exporter-jaeger-thrift == 1.3.0 + opentelemetry-exporter-jaeger-proto-grpc == 1.4.0.dev0 + opentelemetry-exporter-jaeger-thrift == 1.4.0.dev0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index d068934c69..c173c28d8d 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index e3b56f9092..7eb081d23c 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 1.3.0 - opentelemetry-sdk == 1.3.0 + opentelemetry-api == 1.4.0.dev0 + opentelemetry-sdk == 1.4.0.dev0 protobuf >= 3.13.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 0011c3d6b2..c829b95757 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.22b0" +__version__ = "0.23.dev0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 717267f57f..c58cc708bd 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 - opentelemetry-api == 1.3.0 - opentelemetry-sdk == 1.3.0 - opentelemetry-proto == 1.3.0 + opentelemetry-api == 1.4.0.dev0 + opentelemetry-sdk == 1.4.0.dev0 + opentelemetry-proto == 1.4.0.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 01d2fea7b3..48c4fb840e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 229cbdd64e..d4a891e173 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -38,4 +38,4 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.3.0 + opentelemetry-exporter-otlp-proto-grpc == 1.4.0.dev0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 01d2fea7b3..48c4fb840e 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index 0c254964fa..1a0aa03c38 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -42,8 +42,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 1.3.0 - opentelemetry-sdk == 1.3.0 + opentelemetry-api == 1.4.0.dev0 + opentelemetry-sdk == 1.4.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 01d2fea7b3..48c4fb840e 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index cdc36afad1..baabea2973 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -43,9 +43,9 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 - opentelemetry-api == 1.3.0 - opentelemetry-sdk == 1.3.0 - opentelemetry-exporter-zipkin-json == 1.3.0 + opentelemetry-api == 1.4.0.dev0 + opentelemetry-sdk == 1.4.0.dev0 + opentelemetry-exporter-zipkin-json == 1.4.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 01d2fea7b3..48c4fb840e 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index fc76583042..0b1a832ed7 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.3.0 - opentelemetry-exporter-zipkin-proto-http == 1.3.0 + opentelemetry-exporter-zipkin-json == 1.4.0.dev0 + opentelemetry-exporter-zipkin-proto-http == 1.4.0.dev0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 01d2fea7b3..48c4fb840e 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 01d2fea7b3..48c4fb840e 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0.dev0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index f4e84e6cff..65abe4b947 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -41,9 +41,9 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.3.0 - opentelemetry-instrumentation == 0.22b0 - opentelemetry-sdk == 1.3.0 + opentelemetry-api == 1.4.0.dev0 + opentelemetry-instrumentation == 0.23.dev0 + opentelemetry-sdk == 1.4.0.dev0 [options.packages.find] where = src @@ -57,4 +57,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.3.0 + opentelemetry-exporter-otlp == 1.4.0.dev0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index 0011c3d6b2..c829b95757 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.22b0" +__version__ = "0.23.dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 01d2fea7b3..48c4fb840e 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0.dev0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index e08b62aa93..6c258c9796 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.3.0 - opentelemetry-semantic-conventions == 0.22b0 + opentelemetry-api == 1.4.0.dev0 + opentelemetry-semantic-conventions == 0.23.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 01d2fea7b3..48c4fb840e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0.dev0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 0011c3d6b2..c829b95757 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.22b0" +__version__ = "0.23.dev0" diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 640b2b9853..83b7cc8158 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.3.0 + opentelemetry-api == 1.4.0.dev0 deprecated >= 1.2.6 [options.extras_require] diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 01d2fea7b3..48c4fb840e 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0.dev0" diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index a90c8a22f0..1c28fc7940 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.3.0 + opentelemetry-api == 1.4.0.dev0 [options.extras_require] test = diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 01d2fea7b3..48c4fb840e 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0.dev0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 1577cb3171..fc87a77a23 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -42,11 +42,11 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 1.3.0 + opentelemetry-api == 1.4.0.dev0 [options.extras_require] test = - opentelemetry-test == 0.22b0 + opentelemetry-test == 0.23.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 0011c3d6b2..c829b95757 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.22b0" +__version__ = "0.23.dev0" diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index 9b478a051e..3b4b47ba34 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -37,8 +37,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.3.0 - opentelemetry-sdk == 1.3.0 + opentelemetry-api == 1.4.0.dev0 + opentelemetry-sdk == 1.4.0.dev0 [options.extras_require] test = flask~=1.0 diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index d544cffe1c..12f7794562 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.22b0" +__version__ = "0.23.dev0" From 7f7151572045281ba186ac3ccf8de3e0dd0d36bd Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 4 Jun 2021 23:36:57 +0530 Subject: [PATCH 0900/1517] Add support for OTEL_EXPORTER_JAEGER_TIMEOUT (#1863) --- CHANGELOG.md | 2 + .../exporter/jaeger/proto/grpc/__init__.py | 50 ++++++------- .../tests/test_jaeger_exporter_protobuf.py | 3 + .../exporter/jaeger/thrift/__init__.py | 71 ++++++------------- .../exporter/jaeger/thrift/send.py | 16 ++++- .../tests/test_jaeger_exporter_thrift.py | 3 + .../sdk/environment_variables/__init__.py | 7 ++ 7 files changed, 74 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2808ecf80..ad423b7f47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1829](https://github.com/open-telemetry/opentelemetry-python/pull/1829)) - Lazily read/configure limits and allow limits to be unset. ([#1839](https://github.com/open-telemetry/opentelemetry-python/pull/1839)) +- Added support for OTEL_EXPORTER_JAEGER_TIMEOUT + ([#1863](https://github.com/open-telemetry/opentelemetry-python/pull/1863)) ### Changed - Fixed OTLP gRPC exporter silently failing if scheme is not specified in endpoint. diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py index d287d0663b..ab751b0fb9 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py @@ -56,6 +56,7 @@ - :envvar:`OTEL_EXPORTER_JAEGER_ENDPOINT` - :envvar:`OTEL_EXPORTER_JAEGER_CERTIFICATE` +- :envvar:`OTEL_EXPORTER_JAEGER_TIMEOUT` API --- @@ -68,7 +69,7 @@ from os import environ from typing import Optional -from grpc import ChannelCredentials, insecure_channel, secure_channel +from grpc import ChannelCredentials, RpcError, insecure_channel, secure_channel from opentelemetry import trace from opentelemetry.exporter.jaeger.proto.grpc import util @@ -85,11 +86,13 @@ ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_JAEGER_ENDPOINT, + OTEL_EXPORTER_JAEGER_TIMEOUT, ) from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult DEFAULT_GRPC_COLLECTOR_ENDPOINT = "localhost:14250" +DEFAULT_EXPORT_TIMEOUT = 10 logger = logging.getLogger(__name__) @@ -103,6 +106,7 @@ class JaegerExporter(SpanExporter): insecure: True if collector has no encryption or authentication credentials: Credentials for server authentication. max_tag_value_length: Max length string attribute values can have. Set to None to disable. + timeout: Maximum time the Jaeger exporter should wait for each batch export. """ def __init__( @@ -111,13 +115,15 @@ def __init__( insecure: Optional[bool] = None, credentials: Optional[ChannelCredentials] = None, max_tag_value_length: Optional[int] = None, + timeout: Optional[int] = None, ): self._max_tag_value_length = max_tag_value_length - self.collector_endpoint = _parameter_setter( - param=collector_endpoint, - env_variable=environ.get(OTEL_EXPORTER_JAEGER_ENDPOINT), - default=None, + self.collector_endpoint = collector_endpoint or environ.get( + OTEL_EXPORTER_JAEGER_ENDPOINT, DEFAULT_GRPC_COLLECTOR_ENDPOINT + ) + self._timeout = timeout or int( + environ.get(OTEL_EXPORTER_JAEGER_TIMEOUT, DEFAULT_EXPORT_TIMEOUT) ) self._grpc_client = None self.insecure = insecure @@ -131,16 +137,15 @@ def __init__( @property def _collector_grpc_client(self) -> Optional[CollectorServiceStub]: - endpoint = self.collector_endpoint or DEFAULT_GRPC_COLLECTOR_ENDPOINT if self._grpc_client is None: if self.insecure: self._grpc_client = CollectorServiceStub( - insecure_channel(endpoint) + insecure_channel(self.collector_endpoint) ) else: self._grpc_client = CollectorServiceStub( - secure_channel(endpoint, self.credentials) + secure_channel(self.collector_endpoint, self.credentials) ) return self._grpc_client @@ -161,25 +166,16 @@ def export(self, spans) -> SpanExportResult: jaeger_spans = translator._translate(pb_translator) batch = model_pb2.Batch(spans=jaeger_spans) request = PostSpansRequest(batch=batch) - self._collector_grpc_client.PostSpans(request) - - return SpanExportResult.SUCCESS + try: + self._collector_grpc_client.PostSpans( + request, timeout=self._timeout + ) + return SpanExportResult.SUCCESS + except RpcError as error: + logger.warning( + "Failed to export batch. Status code: %s", error.code() + ) + return SpanExportResult.FAILURE def shutdown(self): pass - - -def _parameter_setter(param, env_variable, default): - """Returns value according to the provided data. - - Args: - param: Constructor parameter value - env_variable: Environment variable related to the parameter - default: Constructor parameter default value - """ - if param is None: - res = env_variable or default - else: - res = param - - return res diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py index 383e33d0f1..3896cdf05a 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py @@ -32,6 +32,7 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_JAEGER_CERTIFICATE, OTEL_EXPORTER_JAEGER_ENDPOINT, + OTEL_EXPORTER_JAEGER_TIMEOUT, OTEL_RESOURCE_ATTRIBUTES, ) from opentelemetry.sdk.resources import SERVICE_NAME, Resource @@ -71,6 +72,7 @@ def test_constructor_by_environment_variables(self): OTEL_EXPORTER_JAEGER_CERTIFICATE: os.path.dirname(__file__) + "/certs/cred.cert", OTEL_RESOURCE_ATTRIBUTES: "service.name=my-opentelemetry-jaeger", + OTEL_EXPORTER_JAEGER_TIMEOUT: "5", }, ) @@ -81,6 +83,7 @@ def test_constructor_by_environment_variables(self): self.assertEqual(exporter.service_name, service) self.assertIsNotNone(exporter._collector_grpc_client) self.assertEqual(exporter.collector_endpoint, collector_endpoint) + self.assertEqual(exporter._timeout, 5) self.assertIsNotNone(exporter.credentials) env_patch.stop() diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py index 482fbdbf4e..954e5a0691 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py @@ -67,6 +67,7 @@ - :envvar:`OTEL_EXPORTER_JAEGER_AGENT_PORT` - :envvar:`OTEL_EXPORTER_JAEGER_AGENT_HOST` - :envvar:`OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES` +- :envvar:`OTEL_EXPORTER_JAEGER_TIMEOUT` API --- @@ -94,6 +95,7 @@ OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES, OTEL_EXPORTER_JAEGER_ENDPOINT, OTEL_EXPORTER_JAEGER_PASSWORD, + OTEL_EXPORTER_JAEGER_TIMEOUT, OTEL_EXPORTER_JAEGER_USER, ) from opentelemetry.sdk.resources import SERVICE_NAME, Resource @@ -101,6 +103,7 @@ DEFAULT_AGENT_HOST_NAME = "localhost" DEFAULT_AGENT_PORT = 6831 +DEFAULT_EXPORT_TIMEOUT = 10 logger = logging.getLogger(__name__) @@ -119,6 +122,7 @@ class JaegerExporter(SpanExporter): required. max_tag_value_length: Max length string attribute values can have. Set to None to disable. udp_split_oversized_batches: Re-emit oversized batches in smaller chunks. + timeout: Maximum time the Jaeger exporter should wait for each batch export. """ def __init__( @@ -130,51 +134,34 @@ def __init__( password: Optional[str] = None, max_tag_value_length: Optional[int] = None, udp_split_oversized_batches: bool = None, + timeout: Optional[int] = None, ): self._max_tag_value_length = max_tag_value_length - self.agent_host_name = _parameter_setter( - param=agent_host_name, - env_variable=environ.get(OTEL_EXPORTER_JAEGER_AGENT_HOST), - default=DEFAULT_AGENT_HOST_NAME, + self.agent_host_name = agent_host_name or environ.get( + OTEL_EXPORTER_JAEGER_AGENT_HOST, DEFAULT_AGENT_HOST_NAME ) - environ_agent_port = environ.get(OTEL_EXPORTER_JAEGER_AGENT_PORT) - environ_agent_port = ( - int(environ_agent_port) if environ_agent_port is not None else None + self.agent_port = agent_port or int( + environ.get(OTEL_EXPORTER_JAEGER_AGENT_PORT, DEFAULT_AGENT_PORT) ) - self.agent_port = _parameter_setter( - param=agent_port, - env_variable=environ_agent_port, - default=DEFAULT_AGENT_PORT, + self._timeout = timeout or int( + environ.get(OTEL_EXPORTER_JAEGER_TIMEOUT, DEFAULT_EXPORT_TIMEOUT) ) - self.udp_split_oversized_batches = _parameter_setter( - param=udp_split_oversized_batches, - env_variable=environ.get( - OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES - ), - default=False, + + self.udp_split_oversized_batches = udp_split_oversized_batches or bool( + environ.get(OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES) ) self._agent_client = AgentClientUDP( host_name=self.agent_host_name, port=self.agent_port, split_oversized_batches=self.udp_split_oversized_batches, ) - self.collector_endpoint = _parameter_setter( - param=collector_endpoint, - env_variable=environ.get(OTEL_EXPORTER_JAEGER_ENDPOINT), - default=None, - ) - self.username = _parameter_setter( - param=username, - env_variable=environ.get(OTEL_EXPORTER_JAEGER_USER), - default=None, - ) - self.password = _parameter_setter( - param=password, - env_variable=environ.get(OTEL_EXPORTER_JAEGER_PASSWORD), - default=None, + self.collector_endpoint = collector_endpoint or environ.get( + OTEL_EXPORTER_JAEGER_ENDPOINT ) + self.username = username or environ.get(OTEL_EXPORTER_JAEGER_USER) + self.password = password or environ.get(OTEL_EXPORTER_JAEGER_PASSWORD) self._collector = None tracer_provider = trace.get_tracer_provider() self.service_name = ( @@ -195,8 +182,12 @@ def _collector_http_client(self) -> Optional[Collector]: if self.username is not None and self.password is not None: auth = (self.username, self.password) + # Thrift HTTP Client expects timeout in millis + timeout_in_millis = self._timeout * 1000.0 self._collector = Collector( - thrift_url=self.collector_endpoint, auth=auth + thrift_url=self.collector_endpoint, + auth=auth, + timeout_in_millis=timeout_in_millis, ) return self._collector @@ -226,19 +217,3 @@ def export(self, spans) -> SpanExportResult: def shutdown(self): pass - - -def _parameter_setter(param, env_variable, default): - """Returns value according to the provided data. - - Args: - param: Constructor parameter value - env_variable: Environment variable related to the parameter - default: Constructor parameter default value - """ - if param is None: - res = env_variable or default - else: - res = param - - return res diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py index a04b09c4ac..c7827038b1 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py @@ -96,19 +96,29 @@ def emit(self, batch: jaeger.Batch): class Collector: - """Submits collected spans to Thrift HTTP server. + """Submits collected spans to Jaeger collector in jaeger.thrift + format over binary thrift protocol. This is recommend option in cases where + it is not feasible to deploy Jaeger Agent next to the application, + for example, when the application code is running as AWS Lambda function. + In these scenarios the Jaeger Clients can be configured to submit spans directly + to the Collectors over HTTP/HTTPS. Args: - thrift_url: URL of the Jaeger HTTP Thrift. + thrift_url: Endpoint used to send spans + directly to Collector the over HTTP. auth: Auth tuple that contains username and password for Basic Auth. + timeout_in_millis: timeout for THttpClient. """ - def __init__(self, thrift_url="", auth=None): + def __init__(self, thrift_url="", auth=None, timeout_in_millis=None): self.thrift_url = thrift_url self.auth = auth self.http_transport = THttpClient.THttpClient( uri_or_host=self.thrift_url ) + if timeout_in_millis is not None: + self.http_transport.setTimeout(timeout_in_millis) + self.protocol = TBinaryProtocol.TBinaryProtocol(self.http_transport) # set basic auth header diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py index 9c1773d4fa..679494fe3b 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py @@ -32,6 +32,7 @@ OTEL_EXPORTER_JAEGER_AGENT_PORT, OTEL_EXPORTER_JAEGER_ENDPOINT, OTEL_EXPORTER_JAEGER_PASSWORD, + OTEL_EXPORTER_JAEGER_TIMEOUT, OTEL_EXPORTER_JAEGER_USER, ) from opentelemetry.sdk.resources import SERVICE_NAME @@ -148,6 +149,7 @@ def test_constructor_by_environment_variables(self): OTEL_EXPORTER_JAEGER_ENDPOINT: collector_endpoint, OTEL_EXPORTER_JAEGER_USER: username, OTEL_EXPORTER_JAEGER_PASSWORD: password, + OTEL_EXPORTER_JAEGER_TIMEOUT: "20", }, ) @@ -164,6 +166,7 @@ def test_constructor_by_environment_variables(self): self.assertEqual(exporter.service_name, service) self.assertEqual(exporter.agent_host_name, agent_host_name) self.assertEqual(exporter.agent_port, int(agent_port)) + self.assertEqual(exporter._timeout, 20) self.assertTrue(exporter._collector_http_client is not None) self.assertEqual(exporter.collector_endpoint, collector_endpoint) self.assertEqual(exporter._collector_http_client.auth, auth) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 0aa5aa515d..a59ca0e5c1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -113,6 +113,13 @@ .. envvar:: OTEL_EXPORTER_JAEGER_PASSWORD """ +OTEL_EXPORTER_JAEGER_TIMEOUT = "OTEL_EXPORTER_JAEGER_TIMEOUT" +""" +.. envvar:: OTEL_EXPORTER_JAEGER_TIMEOUT + +Maximum time the Jaeger exporter will wait for each batch export, the default +timeout is 10s. +""" OTEL_EXPORTER_ZIPKIN_ENDPOINT = "OTEL_EXPORTER_ZIPKIN_ENDPOINT" """ From 3ae6c2eb42af7a2d0209a433cae3e6503ebfa1ed Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 8 Jun 2021 00:01:30 +0530 Subject: [PATCH 0901/1517] Fix pipeline failure (#1896) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 635c3c2df9..19c69be525 100644 --- a/tox.ini +++ b/tox.ini @@ -123,7 +123,7 @@ commands_pre = test-core-proto: pip install {toxinidir}/opentelemetry-proto distro: pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation {toxinidir}/opentelemetry-distro - getting-started: pip install -e {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask + getting-started: pip install requests flask -e {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus From d26699da37431b6a77035fd389a2c5e4255b7aee Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 8 Jun 2021 08:43:17 -0700 Subject: [PATCH 0902/1517] Opencensus exporters should populate service_name from Span Resource (#1897) --- CHANGELOG.md | 4 ++ .../opencensus/trace_exporter/__init__.py | 10 ++++ .../tests/test_otcollector_trace_exporter.py | 47 +++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad423b7f47..d4cf4a1ccd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.3.0-0.22b0...HEAD) +### Changed +- Updated `opentelemetry-opencensus-exporter` to use `service_name` of spans instead of resource + ([#1897](https://github.com/open-telemetry/opentelemetry-python/pull/1897)) + ## [1.3.0-0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01 ### Added diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py index b20084888f..f1987ffb98 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py @@ -66,9 +66,19 @@ def __init__( else: self.client = client + self.host_name = host_name self.node = utils.get_node(service_name, host_name) def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + # Populate service_name from first span + # We restrict any SpanProcessor to be only associated with a single + # TracerProvider, so it is safe to assume that all Spans in a single + # batch all originate from one TracerProvider (and in turn have all + # the same service_name) + if spans: + service_name = spans[0].resource.attributes.get(SERVICE_NAME) + if service_name: + self.node = utils.get_node(service_name, self.host_name) try: responses = self.client.Export(self.generate_span_requests(spans)) diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py index 222a94d60b..43d9bcd430 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py @@ -34,6 +34,10 @@ # pylint: disable=no-member class TestCollectorSpanExporter(unittest.TestCase): + @mock.patch( + "opentelemetry.exporter.opencensus.trace_exporter.trace._TRACER_PROVIDER", + None, + ) def test_constructor(self): mock_get_node = mock.Mock() patch = mock.patch( @@ -324,3 +328,46 @@ def test_export(self): self.assertEqual( getattr(output_identifier, "host_name"), "testHostName" ) + + @mock.patch( + "opentelemetry.exporter.opencensus.trace_exporter.trace._TRACER_PROVIDER", + None, + ) + def test_export_service_name(self): + trace_api.set_tracer_provider( + TracerProvider( + resource=Resource.create({SERVICE_NAME: "testServiceName"}) + ) + ) + mock_client = mock.MagicMock() + mock_export = mock.MagicMock() + mock_client.Export = mock_export + host_name = "testHostName" + collector_exporter = OpenCensusSpanExporter( + client=mock_client, host_name=host_name + ) + self.assertEqual( + collector_exporter.node.service_info.name, "testServiceName" + ) + + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + span_id = 0x34BF92DEEFC58C92 + span_context = trace_api.SpanContext( + trace_id, + span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + resource = Resource.create({SERVICE_NAME: "test"}) + otel_spans = [ + trace._Span( + name="test1", + context=span_context, + kind=trace_api.SpanKind.CLIENT, + resource=resource, + ) + ] + + result_status = collector_exporter.export(otel_spans) + self.assertEqual(SpanExportResult.SUCCESS, result_status) + self.assertEqual(collector_exporter.node.service_info.name, "test") From a47a202d67d7f58cbc67efc9a3fba00d387a67c9 Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Wed, 9 Jun 2021 17:31:44 +0100 Subject: [PATCH 0903/1517] Fix #1848 opentracing shim exception reporting (#1878) --- CHANGELOG.md | 4 +++ .../shim/opentracing_shim/__init__.py | 25 +++++++++++++------ .../tests/test_shim.py | 25 ++++++++++++++++--- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4cf4a1ccd..de7868b500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update protos to latest version release 0.9.0 ([#1873](https://github.com/open-telemetry/opentelemetry-python/pull/1873)) +### Fixed +- Updated `opentelementry-opentracing-shim` `ScopeShim` to report exceptions in + opentelemetry specification format, rather than opentracing spec format. + ## [1.2.0, 0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0-0.21b0) - 2021-05-11 ### Added diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index 4cddf2a57a..e696f04690 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -86,7 +86,8 @@ # pylint:disable=no-member import logging -from typing import Optional, TypeVar, Union +from types import TracebackType +from typing import Optional, Type, TypeVar, Union from deprecated import deprecated from opentracing import ( @@ -401,16 +402,24 @@ def close(self): ends the associated span**, regardless of the value passed in *finish_on_close* when activating the span. """ + self._end_span_scope(None, None, None) - detach(self._token) + def __exit__(self, exc_type, exc_val, exc_tb): + """ + Override the __exit__ method of `opentracing.scope.Scope` so we can report + exceptions correctly in opentelemetry specification format. + """ + self._end_span_scope(exc_type, exc_val, exc_tb) + def _end_span_scope( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + detach(self._token) if self._span_cm is not None: - # We don't have error information to pass to `__exit__()` so we - # pass `None` in all arguments. If the OpenTelemetry tracer - # implementation requires this information, the `__exit__()` method - # on `opentracing.Scope` should be overridden and modified to pass - # the relevant values to this `close()` method. - self._span_cm.__exit__(None, None, None) + self._span_cm.__exit__(exc_type, exc_val, exc_tb) else: self._span.unwrap().end() diff --git a/shim/opentelemetry-opentracing-shim/tests/test_shim.py b/shim/opentelemetry-opentracing-shim/tests/test_shim.py index 6feeedff06..828097270f 100644 --- a/shim/opentelemetry-opentracing-shim/tests/test_shim.py +++ b/shim/opentelemetry-opentracing-shim/tests/test_shim.py @@ -16,6 +16,7 @@ # pylint:disable=no-member import time +import traceback from unittest import TestCase from unittest.mock import Mock @@ -475,12 +476,30 @@ def test_span_on_error(self): """ # Raise an exception while a span is active. - with self.assertRaises(Exception): + with self.assertRaises(Exception) as exc_ctx: with self.shim.start_active_span("TestName") as scope: - raise Exception + raise Exception("bad thing") + ex = exc_ctx.exception + expected_stack = "".join( + traceback.format_exception( + etype=type(ex), value=ex, tb=ex.__traceback__ + ) + ) # Verify exception details have been added to span. - self.assertEqual(scope.span.unwrap().attributes["error"], True) + exc_event = scope.span.unwrap().events[0] + + self.assertEqual(exc_event.name, "exception") + self.assertEqual( + exc_event.attributes["exception.message"], "bad thing" + ) + self.assertEqual( + exc_event.attributes["exception.type"], Exception.__name__ + ) + # cannot get the whole stacktrace so just assert exception part is contained + self.assertIn( + expected_stack, exc_event.attributes["exception.stacktrace"] + ) def test_inject_http_headers(self): """Test `inject()` method for Format.HTTP_HEADERS.""" From fcde9114324289768f47cda0639543b40a9792ca Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 10 Jun 2021 11:29:28 -0700 Subject: [PATCH 0904/1517] fix dependabot alert about fastAPI (#1908) --- docs-requirements.txt | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index f60325f00b..a93eb8aa70 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -11,30 +11,11 @@ sphinx-jekyll-builder ./opentelemetry-sdk # Required by instrumentation and exporter packages -asgiref~=3.0 -asyncpg>=0.12.0 ddtrace>=0.34.0 -aiohttp~= 3.0 -aiopg>=0.13.0 grpcio~=1.27 Deprecated>=1.2.6 django>=2.2 -PyMySQL~=0.9.3 flask~=1.0 -mysql-connector-python~=8.0 opentracing~=2.2.0 -prometheus_client>=0.5.0,<1.0.0 -psycopg2-binary>=2.7.3.1 -pymemcache~=1.3 -pymongo~=3.1 -pyramid>=1.7 -redis>=2.6 -sqlalchemy>=1.0 thrift>=0.10.0 wrapt>=1.0.0,<2.0.0 -celery>=4.0 -psutil~=5.7.0 -boto~=2.0 -botocore~=1.0 -starlette~=0.13 -fastapi~=0.58.1 From 3bb6bdc63939b652ad69072c8a112534d1be4820 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 11 Jun 2021 09:25:18 -0700 Subject: [PATCH 0905/1517] Span.set_status priority (#1902) --- CHANGELOG.md | 3 ++ .../src/opentelemetry/sdk/trace/__init__.py | 8 ++++ opentelemetry-sdk/tests/trace/test_trace.py | 38 ++++++++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de7868b500..6bb68e0531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Updated `opentelemetry-opencensus-exporter` to use `service_name` of spans instead of resource ([#1897](https://github.com/open-telemetry/opentelemetry-python/pull/1897)) +- Ignore calls to `Span.set_status` with `StatusCode.UNSET` and also if previous status already + had `StatusCode.OK`. + ([#1902](https://github.com/open-telemetry/opentelemetry-python/pull/1902)) ## [1.3.0-0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 66b4b38390..bbb56e4b6e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -806,6 +806,14 @@ def is_recording(self) -> bool: @_check_span_ended def set_status(self, status: trace_api.Status) -> None: + # Ignore future calls if status is already set to OK + # Ignore calls to set to StatusCode.UNSET + if ( + self._status + and self._status.status_code is StatusCode.OK + or status.status_code is StatusCode.UNSET + ): + return self._status = status def __exit__( diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 0781042a33..dc5bbb3d69 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -913,7 +913,6 @@ def error_status_test(context): with self.assertRaises(AssertionError): with context as root: raise AssertionError("unknown") - self.assertIs(root.status.status_code, StatusCode.ERROR) self.assertEqual( root.status.description, "AssertionError: unknown" @@ -928,18 +927,53 @@ def error_status_test(context): .start_as_current_span("root") ) - def test_last_status_wins(self): + def test_status_cannot_override_ok(self): def error_status_test(context): with self.assertRaises(AssertionError): with context as root: root.set_status(trace_api.status.Status(StatusCode.OK)) raise AssertionError("unknown") + self.assertIs(root.status.status_code, StatusCode.OK) + self.assertIsNone(root.status.description) + + error_status_test( + trace.TracerProvider().get_tracer(__name__).start_span("root") + ) + error_status_test( + trace.TracerProvider() + .get_tracer(__name__) + .start_as_current_span("root") + ) + def test_status_cannot_set_unset(self): + def unset_status_test(context): + with self.assertRaises(AssertionError): + with context as root: + raise AssertionError("unknown") + root.set_status(trace_api.status.Status(StatusCode.UNSET)) self.assertIs(root.status.status_code, StatusCode.ERROR) self.assertEqual( root.status.description, "AssertionError: unknown" ) + unset_status_test( + trace.TracerProvider().get_tracer(__name__).start_span("root") + ) + unset_status_test( + trace.TracerProvider() + .get_tracer(__name__) + .start_as_current_span("root") + ) + + def test_last_status_wins(self): + def error_status_test(context): + with self.assertRaises(AssertionError): + with context as root: + raise AssertionError("unknown") + root.set_status(trace_api.status.Status(StatusCode.OK)) + self.assertIs(root.status.status_code, StatusCode.OK) + self.assertIsNone(root.status.description) + error_status_test( trace.TracerProvider().get_tracer(__name__).start_span("root") ) From 540b8a5ed0fd79e77eced30eb514605bec97fe87 Mon Sep 17 00:00:00 2001 From: Eunice Kim Date: Fri, 11 Jun 2021 09:49:59 -0700 Subject: [PATCH 0906/1517] Implement CreateKey Functionality (#1853) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ docs-requirements.txt | 1 + .../src/opentelemetry/baggage/__init__.py | 4 +-- .../src/opentelemetry/context/__init__.py | 13 ++++++++ .../src/opentelemetry/trace/__init__.py | 4 +-- .../trace/propagation/__init__.py | 7 ++-- .../tests/context/test_context.py | 32 +++++++++++++------ opentelemetry-sdk/setup.cfg | 1 + .../sdk/trace/export/__init__.py | 5 +-- .../tests/test_b3_format.py | 5 +-- .../tests/test_jaeger_propagator.py | 11 ++++--- .../shim/opentracing_shim/__init__.py | 14 ++++++-- tox.ini | 11 +++++-- 14 files changed, 79 insertions(+), 33 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c8693ed0d..c88b731332 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 01bb63fb8cc429a17f1fb0c93f7ac72a3fc7b4fc + CONTRIB_REPO_SHA: 82c4f600872a72fedaebad758f0d5588dfb53a00 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bb68e0531..d959116335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Allow span limits to be set programatically via TracerProvider. ([#1877](https://github.com/open-telemetry/opentelemetry-python/pull/1877)) +- Added support for CreateKey functionality. + ([#1853](https://github.com/open-telemetry/opentelemetry-python/pull/1853)) ### Changed - Updated get_tracer to return an empty string when passed an invalid name diff --git a/docs-requirements.txt b/docs-requirements.txt index a93eb8aa70..f9a7776cb0 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -8,6 +8,7 @@ sphinx-jekyll-builder # doesn't work for pkg_resources. ./opentelemetry-api ./opentelemetry-semantic-conventions +./opentelemetry-python-contrib/opentelemetry-instrumentation ./opentelemetry-sdk # Required by instrumentation and exporter packages diff --git a/opentelemetry-api/src/opentelemetry/baggage/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/__init__.py index da78d92417..2368c5a325 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/__init__.py @@ -15,10 +15,10 @@ import typing from types import MappingProxyType -from opentelemetry.context import get_value, set_value +from opentelemetry.context import create_key, get_value, set_value from opentelemetry.context.context import Context -_BAGGAGE_KEY = "baggage" +_BAGGAGE_KEY = create_key("baggage") def get_all( diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 0f60d86a0c..a8dd39c53a 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -15,6 +15,7 @@ import logging import threading import typing +import uuid from functools import wraps from os import environ @@ -68,6 +69,18 @@ def wrapper( # type: ignore[misc] return typing.cast(_F, wrapper) # type: ignore[misc] +def create_key(keyname: str) -> str: + """To allow cross-cutting concern to control access to their local state, + the RuntimeContext API provides a function which takes a keyname as input, + and returns a unique key. + Args: + keyname: The key name is for debugging purposes and is not required to be unique. + Returns: + A unique string representing the newly created key. + """ + return keyname + "-" + str(uuid.uuid4()) + + def get_value(key: str, context: typing.Optional[Context] = None) -> "object": """To access the local state of a concern, the RuntimeContext API provides a function which takes a context and a key as input, diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index cae50a5b47..e0aa9e32d7 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -85,7 +85,7 @@ from opentelemetry.context.context import Context from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER from opentelemetry.trace.propagation import ( - SPAN_KEY, + _SPAN_KEY, get_current_span, set_span_in_context, ) @@ -517,7 +517,7 @@ def use_span( this mechanism if it was previously set manually. """ try: - token = context_api.attach(context_api.set_value(SPAN_KEY, span)) + token = context_api.attach(context_api.set_value(_SPAN_KEY, span)) try: yield span finally: diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 51146adeb3..d3529e1779 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -13,11 +13,12 @@ # limitations under the License. from typing import Optional -from opentelemetry.context import get_value, set_value +from opentelemetry.context import create_key, get_value, set_value from opentelemetry.context.context import Context from opentelemetry.trace.span import INVALID_SPAN, Span SPAN_KEY = "current-span" +_SPAN_KEY = create_key("current-span") def set_span_in_context( @@ -30,7 +31,7 @@ def set_span_in_context( context: a Context object. if one is not passed, the default current context is used instead. """ - ctx = set_value(SPAN_KEY, span, context=context) + ctx = set_value(_SPAN_KEY, span, context=context) return ctx @@ -44,7 +45,7 @@ def get_current_span(context: Optional[Context] = None) -> Span: Returns: The Span set in the context if it exists. INVALID_SPAN otherwise. """ - span = get_value(SPAN_KEY, context=context) + span = get_value(_SPAN_KEY, context=context) if span is None or not isinstance(span, Span): return INVALID_SPAN return span diff --git a/opentelemetry-api/tests/context/test_context.py b/opentelemetry-api/tests/context/test_context.py index 7dad1191c6..b90c011b99 100644 --- a/opentelemetry-api/tests/context/test_context.py +++ b/opentelemetry-api/tests/context/test_context.py @@ -18,27 +18,39 @@ from opentelemetry.context.context import Context -def do_work() -> None: - context.attach(context.set_value("say", "bar")) +def _do_work() -> str: + key = context.create_key("say") + context.attach(context.set_value(key, "bar")) + return key class TestContext(unittest.TestCase): def setUp(self): context.attach(Context()) + def test_context_key(self): + key1 = context.create_key("say") + key2 = context.create_key("say") + self.assertNotEqual(key1, key2) + first = context.set_value(key1, "foo") + second = context.set_value(key2, "bar") + self.assertEqual(context.get_value(key1, context=first), "foo") + self.assertEqual(context.get_value(key2, context=second), "bar") + def test_context(self): - self.assertIsNone(context.get_value("say")) + key1 = context.create_key("say") + self.assertIsNone(context.get_value(key1)) empty = context.get_current() - second = context.set_value("say", "foo") - self.assertEqual(context.get_value("say", context=second), "foo") + second = context.set_value(key1, "foo") + self.assertEqual(context.get_value(key1, context=second), "foo") - do_work() - self.assertEqual(context.get_value("say"), "bar") + key2 = _do_work() + self.assertEqual(context.get_value(key2), "bar") third = context.get_current() - self.assertIsNone(context.get_value("say", context=empty)) - self.assertEqual(context.get_value("say", context=second), "foo") - self.assertEqual(context.get_value("say", context=third), "bar") + self.assertIsNone(context.get_value(key1, context=empty)) + self.assertEqual(context.get_value(key1, context=second), "foo") + self.assertEqual(context.get_value(key2, context=third), "bar") def test_set_value(self): first = context.set_value("a", "yyy") diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 6c258c9796..3df5e31ed4 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -44,6 +44,7 @@ include_package_data = True install_requires = opentelemetry-api == 1.4.0.dev0 opentelemetry-semantic-conventions == 0.23.dev0 + opentelemetry-instrumentation == 0.23.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 457a1ef56c..d5d84df045 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -22,6 +22,7 @@ from typing import Optional from opentelemetry.context import Context, attach, detach, set_value +from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY from opentelemetry.sdk.environment_variables import ( OTEL_BSP_EXPORT_TIMEOUT, OTEL_BSP_MAX_EXPORT_BATCH_SIZE, @@ -86,7 +87,7 @@ def on_start( def on_end(self, span: ReadableSpan) -> None: if not span.context.trace_flags.sampled: return - token = attach(set_value("suppress_instrumentation", True)) + token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: self.span_exporter.export((span,)) # pylint: disable=broad-except @@ -326,7 +327,7 @@ def _export_batch(self) -> int: while idx < self.max_export_batch_size and self.queue: self.spans_list[idx] = self.queue.pop() idx += 1 - token = attach(set_value("suppress_instrumentation", True)) + token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: # Ignore type b/c the Optional[None]+slicing is too "clever" # for mypy diff --git a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index 87a059f57e..ebefebaf4f 100644 --- a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -25,6 +25,7 @@ B3SingleFormat, ) from opentelemetry.propagators.textmap import DefaultGetter +from opentelemetry.trace.propagation import _SPAN_KEY def get_child_parent_new_carrier(old_carrier, propagator): @@ -247,7 +248,7 @@ def test_derived_ctx_is_returned_for_success(self): }, old_ctx, ) - self.assertIn("current-span", new_ctx) + self.assertIn(_SPAN_KEY, new_ctx) for key, value in old_ctx.items(): # pylint:disable=no-member self.assertIn(key, new_ctx) # pylint:disable=unsubscriptable-object @@ -257,7 +258,7 @@ def test_derived_ctx_is_returned_for_failure(self): """Ensure returned context is derived from the given context.""" old_ctx = Context({"k2": "v2"}) new_ctx = self.get_propagator().extract({}, old_ctx) - self.assertNotIn("current-span", new_ctx) + self.assertNotIn(_SPAN_KEY, new_ctx) for key, value in old_ctx.items(): # pylint:disable=no-member self.assertIn(key, new_ctx) # pylint:disable=unsubscriptable-object diff --git a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py index 55e096b095..9026558470 100644 --- a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py @@ -19,6 +19,7 @@ import opentelemetry.sdk.trace.id_generator as id_generator import opentelemetry.trace as trace_api from opentelemetry import baggage +from opentelemetry.baggage import _BAGGAGE_KEY from opentelemetry.context import Context from opentelemetry.propagators import ( # pylint: disable=no-name-in-module jaeger, @@ -134,7 +135,7 @@ def test_baggage(self): input_baggage = {"key1": "value1"} _, new_carrier = get_context_new_carrier(old_carrier, input_baggage) ctx = FORMAT.extract(new_carrier) - self.assertDictEqual(input_baggage, ctx["baggage"]) + self.assertDictEqual(input_baggage, ctx[_BAGGAGE_KEY]) def test_non_string_baggage(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} @@ -142,7 +143,7 @@ def test_non_string_baggage(self): formatted_baggage = {"key1": "1", "key2": "True"} _, new_carrier = get_context_new_carrier(old_carrier, input_baggage) ctx = FORMAT.extract(new_carrier) - self.assertDictEqual(formatted_baggage, ctx["baggage"]) + self.assertDictEqual(formatted_baggage, ctx[_BAGGAGE_KEY]) def test_extract_invalid_uber_trace_id(self): old_carrier = { @@ -153,7 +154,7 @@ def test_extract_invalid_uber_trace_id(self): context = FORMAT.extract(old_carrier) span_context = trace_api.get_current_span(context).get_span_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) - self.assertDictEqual(formatted_baggage, context["baggage"]) + self.assertDictEqual(formatted_baggage, context[_BAGGAGE_KEY]) def test_extract_invalid_trace_id(self): old_carrier = { @@ -164,7 +165,7 @@ def test_extract_invalid_trace_id(self): context = FORMAT.extract(old_carrier) span_context = trace_api.get_current_span(context).get_span_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) - self.assertDictEqual(formatted_baggage, context["baggage"]) + self.assertDictEqual(formatted_baggage, context[_BAGGAGE_KEY]) def test_extract_invalid_span_id(self): old_carrier = { @@ -175,7 +176,7 @@ def test_extract_invalid_span_id(self): context = FORMAT.extract(old_carrier) span_context = trace_api.get_current_span(context).get_span_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) - self.assertDictEqual(formatted_baggage, context["baggage"]) + self.assertDictEqual(formatted_baggage, context[_BAGGAGE_KEY]) def test_fields(self): tracer = trace.TracerProvider().get_tracer("sdk_tracer_provider") diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index e696f04690..5777bd0edc 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -101,7 +101,14 @@ ) from opentelemetry.baggage import get_baggage, set_baggage -from opentelemetry.context import Context, attach, detach, get_value, set_value +from opentelemetry.context import ( + Context, + attach, + create_key, + detach, + get_value, + set_value, +) from opentelemetry.propagate import get_global_textmap from opentelemetry.shim.opentracing_shim import util from opentelemetry.shim.opentracing_shim.version import __version__ @@ -118,6 +125,7 @@ ValueT = TypeVar("ValueT", int, float, bool, str) logger = logging.getLogger(__name__) +_SHIM_KEY = create_key("scope_shim") def create_tracer(otel_tracer_provider: TracerProvider) -> "TracerShim": @@ -349,7 +357,7 @@ def __init__( ): super().__init__(manager, span) self._span_cm = span_cm - self._token = attach(set_value("scope_shim", self)) + self._token = attach(set_value(_SHIM_KEY, self)) # TODO: Change type of `manager` argument to `opentracing.ScopeManager`? We # need to get rid of `manager.tracer` for this. @@ -486,7 +494,7 @@ def active(self) -> "ScopeShim": return None try: - return get_value("scope_shim") + return get_value(_SHIM_KEY) except KeyError: span_context = SpanContextShim(span.get_span_context()) wrapped_span = SpanShim(self._tracer, span_context, span) diff --git a/tox.ini b/tox.ini index 19c69be525..49b62e1e27 100644 --- a/tox.ini +++ b/tox.ini @@ -118,7 +118,10 @@ commands_pre = py3{6,7,8,9}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. - test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util + test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util + + test-core-sdk: pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation + test-core-opentracing-shim: pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation test-core-proto: pip install {toxinidir}/opentelemetry-proto distro: pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation {toxinidir}/opentelemetry-distro @@ -135,7 +138,9 @@ commands_pre = exporter-otlp-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc exporter-jaeger-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift {toxinidir}/exporter/opentelemetry-exporter-jaeger + exporter-jaeger-combined: pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation exporter-jaeger-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc + exporter-jaeger-proto-grpc: pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation exporter-jaeger-thrift: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift opentracing-shim: pip install {toxinidir}/opentelemetry-sdk @@ -192,6 +197,7 @@ deps = commands_pre = python -m pip install -e {toxinidir}/opentelemetry-api[test] python -m pip install -e {toxinidir}/opentelemetry-semantic-conventions[test] + python -m pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation python -m pip install -e {toxinidir}/opentelemetry-sdk[test] python -m pip install -e {toxinidir}/opentelemetry-proto[test] python -m pip install -e {toxinidir}/tests/util[test] @@ -207,7 +213,6 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-b3[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-jaeger[test] - python -m pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation python -m pip install -e {toxinidir}/opentelemetry-distro[test] commands = @@ -239,8 +244,8 @@ deps = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-semantic-conventions \ - -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation \ + -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests \ -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi \ -e {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http From bab5011115b0d9fa72636efd7a647bde69876e1f Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 11 Jun 2021 23:04:18 +0530 Subject: [PATCH 0907/1517] Add some how-to examples (#1905) --- docs/faq-and-cookbook.rst | 66 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/docs/faq-and-cookbook.rst b/docs/faq-and-cookbook.rst index 4b70f2a92f..0f077e5324 100644 --- a/docs/faq-and-cookbook.rst +++ b/docs/faq-and-cookbook.rst @@ -43,3 +43,69 @@ Capturing baggage at different contexts print(baggage.get_baggage("context", parent_ctx)) print(baggage.get_baggage("context", child_ctx)) + +Manually setting span context +----------------------------- + +.. code-block:: python + + from opentelemetry import trace + from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor + from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + + trace.set_tracer_provider(TracerProvider()) + trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) + + tracer = trace.get_tracer(__name__) + + # Extracting from carrier header + carrier = {'traceparent': '00-a9c3b99a95cc045e573e163c3ac80a77-d99d251a8caecd06-01'} + ctx = TraceContextTextMapPropagator().extract(carrier=carrier) + + with tracer.start_as_current_span('child', context=ctx) as span: + span.set_attribute('primes', [2, 3, 5, 7]) + + # Or if you have a SpanContext object already. + span_context = SpanContext( + trace_id=2604504634922341076776623263868986797, + span_id=5213367945872657620, + is_remote=True, + trace_flags=TraceFlags(0x01) + ) + ctx = trace.set_span_in_context(NonRecordingSpan(span_context)) + + with tracer.start_as_current_span("child", context=ctx) as span: + span.set_attribute('evens', [2, 4, 6, 8]) + +Using multiple tracer providers with different Resource +------------------------------------------------------- + +.. code-block:: python + + from opentelemetry import trace + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.resources import Resource + from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor + + # Global tracer provider which can be set only once + trace.set_tracer_provider( + TracerProvider(resource=Resource.create({"service.name": "service1"})) + ) + trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) + + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span("some-name") as span: + span.set_attribute("key", "value") + + + + another_tracer_provider = TracerProvider( + resource=Resource.create({"service.name": "service2"}) + ) + another_tracer_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) + + another_tracer = trace.get_tracer(__name__, tracer_provider=another_tracer_provider) + with another_tracer.start_as_current_span("name-here") as span: + span.set_attribute("another-key", "another-value") From 32b83a03a707d11e0b3385e207bc572f9d5425e8 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 14 Jun 2021 14:41:57 -0700 Subject: [PATCH 0908/1517] make immutable attributes consistent (#1909) --- CHANGELOG.md | 3 +++ .../src/opentelemetry/trace/__init__.py | 9 +++++++-- .../src/opentelemetry/sdk/resources/__init__.py | 13 ++++++++----- .../src/opentelemetry/sdk/trace/__init__.py | 5 +++-- opentelemetry-sdk/tests/resources/test_resources.py | 3 ++- opentelemetry-sdk/tests/trace/test_trace.py | 5 ++++- 6 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d959116335..8a3872dcf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ignore calls to `Span.set_status` with `StatusCode.UNSET` and also if previous status already had `StatusCode.OK`. ([#1902](https://github.com/open-telemetry/opentelemetry-python/pull/1902)) +- Attributes for `Link` and `Resource` are immutable as they are for `Event`, which means + any attempt to modify attributes directly will result in a `TypeError` exception. + ([#1909](https://github.com/open-telemetry/opentelemetry-python/pull/1909)) ## [1.3.0-0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01 diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index e0aa9e32d7..37d19a0950 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -82,6 +82,9 @@ from typing import Iterator, Optional, Sequence, cast from opentelemetry import context as context_api +from opentelemetry.attributes import ( # type: ignore + _create_immutable_attributes, +) from opentelemetry.context.context import Context from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER from opentelemetry.trace.propagation import ( @@ -126,7 +129,7 @@ def attributes(self) -> types.Attributes: class Link(_LinkBase): - """A link to a `Span`. + """A link to a `Span`. The attributes of a Link are immutable. Args: context: `SpanContext` of the `Span` to link to. @@ -139,7 +142,9 @@ def __init__( attributes: types.Attributes = None, ) -> None: super().__init__(context) - self._attributes = attributes + self._attributes = _create_immutable_attributes( + attributes + ) # type: types.Attributes @property def attributes(self) -> types.Attributes: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 24e5321ce9..a4efe8d7a5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -64,7 +64,10 @@ import pkg_resources -from opentelemetry.attributes import _filter_attributes +from opentelemetry.attributes import ( + _create_immutable_attributes, + _filter_attributes, +) from opentelemetry.sdk.environment_variables import ( OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, @@ -145,7 +148,7 @@ def __init__( self, attributes: Attributes, schema_url: typing.Optional[str] = None ): _filter_attributes(attributes) - self._attributes = attributes.copy() + self._attributes = _create_immutable_attributes(attributes) if schema_url is None: schema_url = "" self._schema_url = schema_url @@ -187,7 +190,7 @@ def get_empty() -> "Resource": @property def attributes(self) -> Attributes: - return self._attributes.copy() + return self._attributes @property def schema_url(self) -> str: @@ -210,7 +213,7 @@ def merge(self, other: "Resource") -> "Resource": Returns: The newly-created Resource. """ - merged_attributes = self.attributes + merged_attributes = self.attributes.copy() merged_attributes.update(other.attributes) if self.schema_url == "": @@ -239,7 +242,7 @@ def __eq__(self, other: object) -> bool: def __hash__(self): return hash( - f"{dumps(self._attributes, sort_keys=True)}|{self._schema_url}" + f"{dumps(self._attributes.copy(), sort_keys=True)}|{self._schema_url}" ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index bbb56e4b6e..aa172a6a83 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -296,7 +296,8 @@ def attributes(self) -> types.Attributes: class Event(EventBase): - """A text annotation with a set of attributes. + """A text annotation with a set of attributes. The attributes of an event + are immutable. Args: name: Name of the event. @@ -456,7 +457,7 @@ def to_json(self, indent=4): f_span["attributes"] = self._format_attributes(self._attributes) f_span["events"] = self._format_events(self._events) f_span["links"] = self._format_links(self._links) - f_span["resource"] = self._resource.attributes + f_span["resource"] = self._format_attributes(self._resource.attributes) return json.dumps(f_span, indent=indent) diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 36eb099cd2..1f2a094f94 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -188,7 +188,8 @@ def test_immutability(self): resource = resources.Resource.create(attributes) self.assertEqual(resource.attributes, attributes_copy) - resource.attributes["has_bugs"] = False + with self.assertRaises(TypeError): + resource.attributes["has_bugs"] = False self.assertEqual(resource.attributes, attributes_copy) attributes["cost"] = 999.91 diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index dc5bbb3d69..2ec2c0b7a3 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -805,7 +805,7 @@ def test_links(self): self.assertEqual( root.links[0].context.span_id, other_context1.span_id ) - self.assertEqual(root.links[0].attributes, None) + self.assertEqual(0, len(root.links[0].attributes)) self.assertEqual( root.links[1].context.trace_id, other_context2.trace_id ) @@ -814,6 +814,9 @@ def test_links(self): ) self.assertEqual(root.links[1].attributes, {"name": "neighbor"}) + with self.assertRaises(TypeError): + root.links[1].attributes["name"] = "new_neighbour" + def test_update_name(self): with self.tracer.start_as_current_span("root") as root: # name From e3ad7e6ae00a9ca3fd7b0bcb45b02efb7bc771cf Mon Sep 17 00:00:00 2001 From: Jishnu Mohan Date: Tue, 15 Jun 2021 06:28:50 +0800 Subject: [PATCH 0909/1517] Fix unordered list formatting (#1911) --- exporter/opentelemetry-exporter-otlp/README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exporter/opentelemetry-exporter-otlp/README.rst b/exporter/opentelemetry-exporter-otlp/README.rst index 06f45540de..4bbef08f64 100644 --- a/exporter/opentelemetry-exporter-otlp/README.rst +++ b/exporter/opentelemetry-exporter-otlp/README.rst @@ -7,9 +7,11 @@ OpenTelemetry Collector Exporters :target: https://pypi.org/project/opentelemetry-exporter-otlp/ This library is provided as a convenience to install all supported OpenTelemetry Collector Exporters. Currently it installs: + * opentelemetry-exporter-otlp-proto-grpc In the future, additional packages will be available: + * opentelemetry-exporter-otlp-proto-http * opentelemetry-exporter-otlp-json-http From f765b487cb6c2b125e3995098eb4d13dc0a88aff Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Wed, 16 Jun 2021 14:05:07 -0700 Subject: [PATCH 0910/1517] Typo fixes (#1914) --- .../tests/testbed/test_nested_callbacks/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst index c191431ccc..cc3ce0185b 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst @@ -7,7 +7,7 @@ This example shows a ``Span`` for a top-level operation, and how it can be passe Implementation details: -* For ``threading``, the ``Span`` is manually activatted it in each corotuine/task. +* For ``threading``, the ``Span`` is manually activated in each coroutine/task. * For ``asyncio``, the active ``Span`` is not activated down the chain as the ``Context`` automatically propagates it. ``threading`` implementation: From a83b6f1d40eb6db45b76d98cfd02742729d17c96 Mon Sep 17 00:00:00 2001 From: Raj Nishtala Date: Mon, 21 Jun 2021 11:26:20 -0400 Subject: [PATCH 0911/1517] Issue-1147 Adding description to the environment variables (#1898) --- CHANGELOG.md | 26 +++--- .../sdk/environment_variables/__init__.py | 93 ++++++++++++++++++- 2 files changed, 103 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a3872dcf1..acc817a9d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Updated `opentelemetry-opencensus-exporter` to use `service_name` of spans instead of resource ([#1897](https://github.com/open-telemetry/opentelemetry-python/pull/1897)) +- Added descriptions to the env variables mentioned in the opentelemetry-specification + ([#1898](https://github.com/open-telemetry/opentelemetry-python/pull/1898)) - Ignore calls to `Span.set_status` with `StatusCode.UNSET` and also if previous status already had `StatusCode.OK`. ([#1902](https://github.com/open-telemetry/opentelemetry-python/pull/1902)) @@ -18,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.3.0-0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01 -### Added +### Added - Allow span limits to be set programatically via TracerProvider. ([#1877](https://github.com/open-telemetry/opentelemetry-python/pull/1877)) - Added support for CreateKey functionality. @@ -107,7 +109,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1728](https://github.com/open-telemetry/opentelemetry-python/pull/1728)) - Silence unnecessary warning when creating a new Status object without description. ([#1721](https://github.com/open-telemetry/opentelemetry-python/pull/1721)) -- Update bootstrap cmd to use exact version when installing instrumentation packages. +- Update bootstrap cmd to use exact version when installing instrumentation packages. ([#1722](https://github.com/open-telemetry/opentelemetry-python/pull/1722)) - Fix B3 propagator to never return None. ([#1750](https://github.com/open-telemetry/opentelemetry-python/pull/1750)) @@ -139,7 +141,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1500](https://github.com/open-telemetry/opentelemetry-python/pull/1500)) ### Changed -- remove `service_name` from constructor of jaeger and opencensus exporters and +- remove `service_name` from constructor of jaeger and opencensus exporters and use of env variable `OTEL_PYTHON_SERVICE_NAME` ([#1669])(https://github.com/open-telemetry/opentelemetry-python/pull/1669) - Rename `IdsGenerator` to `IdGenerator` @@ -207,7 +209,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.19b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.19b0) - 2021-03-26 ### Changed -- remove `service_name` from constructor of jaeger and opencensus exporters and +- remove `service_name` from constructor of jaeger and opencensus exporters and use of env variable `OTEL_PYTHON_SERVICE_NAME` ([#1669])(https://github.com/open-telemetry/opentelemetry-python/pull/1669) - Rename `IdsGenerator` to `IdGenerator` @@ -364,8 +366,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1373](https://github.com/open-telemetry/opentelemetry-python/pull/1373)) - Rename `Meter` to `Accumulator` ([#1372](https://github.com/open-telemetry/opentelemetry-python/pull/1372)) -- Fix `ParentBased` sampler for implicit parent spans. Fix also `trace_state` - erasure for dropped spans or spans sampled by the `TraceIdRatioBased` sampler. +- Fix `ParentBased` sampler for implicit parent spans. Fix also `trace_state` + erasure for dropped spans or spans sampled by the `TraceIdRatioBased` sampler. ([#1394](https://github.com/open-telemetry/opentelemetry-python/pull/1394)) ## [0.15b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.15b0) -2020-11-02 @@ -430,10 +432,10 @@ s ([#1143](https://github.com/open-telemetry/opentelemetry-python/pull/1143)) - Zipkin exporter now accepts a ``max_tag_value_length`` attribute to customize the maximum allowed size a tag value can have. - ([#1151](https://github.com/open-telemetry/opentelemetry-python/pull/1151)) + ([#1151](https://github.com/open-telemetry/opentelemetry-python/pull/1151)) - Fixed OTLP events to Zipkin annotations translation. ([#1161](https://github.com/open-telemetry/opentelemetry-python/pull/1161)) -- Fixed bootstrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask. +- Fixed bootstrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask. ([#1138](https://github.com/open-telemetry/opentelemetry-python/pull/1138)) - Update sampling result names ([#1128](https://github.com/open-telemetry/opentelemetry-python/pull/1128)) @@ -455,12 +457,12 @@ s ([#835](https://github.com/open-telemetry/opentelemetry-python/pull/835)) - Add type hints to OTLP exporter ([#1121](https://github.com/open-telemetry/opentelemetry-python/pull/1121)) -- Add support for OTEL_EXPORTER_ZIPKIN_ENDPOINT env var. As part of this change, the +- Add support for OTEL_EXPORTER_ZIPKIN_ENDPOINT env var. As part of this change, the configuration of the ZipkinSpanExporter exposes a `url` argument to replace `host_name`, `port`, `protocol`, `endpoint`. This brings this implementation inline with other - implementations. + implementations. ([#1064](https://github.com/open-telemetry/opentelemetry-python/pull/1064)) -- Zipkin exporter report instrumentation info. +- Zipkin exporter report instrumentation info. ([#1097](https://github.com/open-telemetry/opentelemetry-python/pull/1097)) - Add status mapping to tags ([#1111](https://github.com/open-telemetry/opentelemetry-python/issues/1111)) @@ -513,7 +515,7 @@ s ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - Stop TracerProvider and MeterProvider from being overridden ([#959](https://github.com/open-telemetry/opentelemetry-python/pull/959)) -- Update default port to 55680 +- Update default port to 55680 ([#977](https://github.com/open-telemetry/opentelemetry-python/pull/977)) - Add proper length zero padding to hex strings of traceId, spanId, parentId sent on the wire, for compatibility with jaeger-collector ([#908](https://github.com/open-telemetry/opentelemetry-python/pull/908)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index a59ca0e5c1..1a98d1f831 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -41,84 +41,131 @@ OTEL_LOG_LEVEL = "OTEL_LOG_LEVEL" """ .. envvar:: OTEL_LOG_LEVEL + +The :envvar:`OTEL_LOG_LEVEL` environment variable sets the log level used by the SDK logger +Default: "info" """ OTEL_TRACES_SAMPLER = "OTEL_TRACES_SAMPLER" """ .. envvar:: OTEL_TRACES_SAMPLER + +The :envvar:`OTEL_TRACES_SAMPLER` environment variable sets the sampler to be used for traces. +Sampling is a mechanism to control the noise introduced by OpenTelemetry by reducing the number +of traces collected and sent to the backend +Default: "parentbased_always_on" """ OTEL_TRACES_SAMPLER_ARG = "OTEL_TRACES_SAMPLER_ARG" """ .. envvar:: OTEL_TRACES_SAMPLER_ARG + +The :envvar:`OTEL_TRACES_SAMPLER_ARG` environment variable will only be used if OTEL_TRACES_SAMPLER is set. +Each Sampler type defines its own expected input, if any. +Invalid or unrecognized input is ignored, +i.e. the SDK behaves as if OTEL_TRACES_SAMPLER_ARG is not set. """ OTEL_BSP_SCHEDULE_DELAY = "OTEL_BSP_SCHEDULE_DELAY" """ .. envvar:: OTEL_BSP_SCHEDULE_DELAY + +The :envvar:`OTEL_BSP_SCHEDULE_DELAY` represents the delay interval between two consecutive exports. +Default: 5000 """ OTEL_BSP_EXPORT_TIMEOUT = "OTEL_BSP_EXPORT_TIMEOUT" """ .. envvar:: OTEL_BSP_EXPORT_TIMEOUT + +The :envvar:`OTEL_BSP_EXPORT_TIMEOUT` represents the maximum allowed time to export data. +Default: 30000 """ OTEL_BSP_MAX_QUEUE_SIZE = "OTEL_BSP_MAX_QUEUE_SIZE" """ .. envvar:: OTEL_BSP_MAX_QUEUE_SIZE + +The :envvar:`OTEL_BSP_MAX_QUEUE_SIZE` represents the maximum queue size for the data export. +Default: 2048 """ OTEL_BSP_MAX_EXPORT_BATCH_SIZE = "OTEL_BSP_MAX_EXPORT_BATCH_SIZE" """ .. envvar:: OTEL_BSP_MAX_EXPORT_BATCH_SIZE + +The :envvar:`OTEL_BSP_MAX_EXPORT_BATCH_SIZE` represents the maximum batch size for the data export. +Default: 512 """ OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT" """ .. envvar:: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT + +The :envvar:`OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed span attribute count. +Default: 128 """ OTEL_SPAN_EVENT_COUNT_LIMIT = "OTEL_SPAN_EVENT_COUNT_LIMIT" """ .. envvar:: OTEL_SPAN_EVENT_COUNT_LIMIT + +The :envvar:`OTEL_SPAN_EVENT_COUNT_LIMIT` represents the maximum allowed span event count. +Default: 128 """ OTEL_SPAN_LINK_COUNT_LIMIT = "OTEL_SPAN_LINK_COUNT_LIMIT" """ .. envvar:: OTEL_SPAN_LINK_COUNT_LIMIT + +The :envvar:`OTEL_SPAN_LINK_COUNT_LIMIT` represents the maximum allowed span link count. +Default: 128 """ OTEL_EXPORTER_JAEGER_AGENT_HOST = "OTEL_EXPORTER_JAEGER_AGENT_HOST" """ .. envvar:: OTEL_EXPORTER_JAEGER_AGENT_HOST + +The :envvar:`OTEL_EXPORTER_JAEGER_AGENT_HOST` represents the hostname for the Jaeger agent. +Default: "localhost" """ OTEL_EXPORTER_JAEGER_AGENT_PORT = "OTEL_EXPORTER_JAEGER_AGENT_PORT" """ .. envvar:: OTEL_EXPORTER_JAEGER_AGENT_PORT + +The :envvar:`OTEL_EXPORTER_JAEGER_AGENT_PORT` represents the port for the Jaeger agent. +Default: 6831 """ OTEL_EXPORTER_JAEGER_ENDPOINT = "OTEL_EXPORTER_JAEGER_ENDPOINT" """ .. envvar:: OTEL_EXPORTER_JAEGER_ENDPOINT + +The :envvar:`OTEL_EXPORTER_JAEGER_ENDPOINT` represents the HTTP endpoint for Jaeger traces. +Default: "http://localhost:14250" """ OTEL_EXPORTER_JAEGER_USER = "OTEL_EXPORTER_JAEGER_USER" """ .. envvar:: OTEL_EXPORTER_JAEGER_USER + +The :envvar:`OTEL_EXPORTER_JAEGER_USER` represents the username to be used for HTTP basic authentication. """ OTEL_EXPORTER_JAEGER_PASSWORD = "OTEL_EXPORTER_JAEGER_PASSWORD" """ .. envvar:: OTEL_EXPORTER_JAEGER_PASSWORD + +The :envvar:`OTEL_EXPORTER_JAEGER_PASSWORD` represents the password to be used for HTTP basic authentication. """ OTEL_EXPORTER_JAEGER_TIMEOUT = "OTEL_EXPORTER_JAEGER_TIMEOUT" """ .. envvar:: OTEL_EXPORTER_JAEGER_TIMEOUT -Maximum time the Jaeger exporter will wait for each batch export, the default -timeout is 10s. +Maximum time the Jaeger exporter will wait for each batch export. +Default: 10 """ OTEL_EXPORTER_ZIPKIN_ENDPOINT = "OTEL_EXPORTER_ZIPKIN_ENDPOINT" @@ -133,23 +180,32 @@ """ .. envvar:: OTEL_EXPORTER_ZIPKIN_TIMEOUT -Maximum time the Zipkin exporter will wait for each batch export, the default -timeout is 10s. +Maximum time (in seconds) the Zipkin exporter will wait for each batch export. +Default: 10 """ OTEL_EXPORTER_OTLP_PROTOCOL = "OTEL_EXPORTER_OTLP_PROTOCOL" """ .. envvar:: OTEL_EXPORTER_OTLP_PROTOCOL + +The :envvar:`OTEL_EXPORTER_OTLP_PROTOCOL` represents the the transport protocol for the +OTLP exporter. """ OTEL_EXPORTER_OTLP_CERTIFICATE = "OTEL_EXPORTER_OTLP_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_CERTIFICATE + +The :envvar:`OTEL_EXPORTER_OTLP_CERTIFICATE` stores the path to the certificate file for +TLS credentials of gRPC client. Should only be used for a secure connection. """ OTEL_EXPORTER_OTLP_HEADERS = "OTEL_EXPORTER_OTLP_HEADERS" """ .. envvar:: OTEL_EXPORTER_OTLP_HEADERS + +The :envvar:`OTEL_EXPORTER_OTLP_HEADERS` contains the key-value pairs to be used as headers +associated with gRPC or HTTP requests. """ @@ -173,31 +229,51 @@ OTEL_EXPORTER_OTLP_TIMEOUT = "OTEL_EXPORTER_OTLP_TIMEOUT" """ .. envvar:: OTEL_EXPORTER_OTLP_TIMEOUT + +The :envvar:`OTEL_EXPORTER_OTLP_TIMEOUT` is the maximum time the OTLP exporter will wait for each batch export. +Default: 10 """ OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT" """ .. envvar:: OTEL_EXPORTER_OTLP_ENDPOINT + +The :envvar:`OTEL_EXPORTER_OTLP_ENDPOINT` target to which the exporter is going to send spans or metrics. +The endpoint MUST be a valid URL with scheme (http or https) and host, and MAY contain a port and path. +A scheme of https indicates a secure connection. +Default: "https://localhost:4317" """ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT + +The :envvar:`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` target to which the span exporter is going to send spans. +The endpoint MUST be a valid URL with scheme (http or https) and host, and MAY contain a port and path. +A scheme of https indicates a secure connection. """ OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL + +The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for spans. """ OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE = "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE + +The :envvar:`OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE` stores the path to the certificate file for +TLS credentials of gRPC client for traces. Should only be used for a secure connection for tracing. """ OTEL_EXPORTER_OTLP_TRACES_HEADERS = "OTEL_EXPORTER_OTLP_TRACES_HEADERS" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_HEADERS + +The :envvar:`OTEL_EXPORTER_OTLP_TRACES_HEADERS` contains the key-value pairs to be used as headers for spans +associated with gRPC or HTTP requests. """ @@ -212,11 +288,17 @@ OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_TIMEOUT + +The :envvar:`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` is the maximum time the OTLP exporter will +wait for each batch export for spans. """ OTEL_EXPORTER_JAEGER_CERTIFICATE = "OTEL_EXPORTER_JAEGER_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_JAEGER_CERTIFICATE + +The :envvar:`OTEL_EXPORTER_JAEGER_CERTIFICATE` stores the path to the certificate file for +TLS credentials of gRPC client for Jaeger. Should only be used for a secure connection with Jaeger. """ OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES = ( @@ -224,6 +306,9 @@ ) """ .. envvar:: OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES + +The :envvar:`OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES` is a boolean flag to determine whether +to split a large span batch to admire the udp packet size limit. """ OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME" From 1e600f385a81e340a2a4bbb7d10075f113149a21 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Tue, 22 Jun 2021 17:06:26 -0700 Subject: [PATCH 0912/1517] Update Zoom meeting link to match calendar (#1920) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fc1fac6a32..b8457a001a 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ For information about contributing to OpenTelemetry Python, see [CONTRIBUTING.md We meet weekly on Thursdays at 9AM PST. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates. -Meetings take place via [Zoom video conference](https://zoom.us/j/6729396170). The passcode is _77777_. +Meetings take place via [Zoom video conference](https://zoom.us/j/8287234601?pwd=YjN2MURycXc4cEZlYTRtYjJaM0grZz09). The passcode is _77777_. Meeting notes are available as a public [Google doc](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit). For edit access, get in touch on [GitHub Discussions](https://github.com/open-telemetry/opentelemetry-python/discussions). From 01c6954c556149fa7881414c5f50050b9322a138 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 25 Jun 2021 09:21:11 -0700 Subject: [PATCH 0913/1517] use BoundedAttributes for attributes in link, event, resource, spans (#1915) --- CHANGELOG.md | 3 + .../src/opentelemetry/attributes/__init__.py | 78 ++++++++++++++- .../src/opentelemetry/trace/__init__.py | 8 +- .../tests/attributes/test_attributes.py | 99 +++++++++++++++++-- .../opentelemetry/sdk/resources/__init__.py | 8 +- .../src/opentelemetry/sdk/trace/__init__.py | 68 +++++++------ .../src/opentelemetry/sdk/util/__init__.py | 3 + opentelemetry-sdk/tests/test_util.py | 91 +---------------- 8 files changed, 217 insertions(+), 141 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acc817a9d5..306004537d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Attributes for `Link` and `Resource` are immutable as they are for `Event`, which means any attempt to modify attributes directly will result in a `TypeError` exception. ([#1909](https://github.com/open-telemetry/opentelemetry-python/pull/1909)) +- Added `BoundedAttributes` to the API to make it available for `Link` which is defined in the + API. Marked `BoundedDict` in the SDK as deprecated as a result. + ([#1915](https://github.com/open-telemetry/opentelemetry-python/pull/1915)) ## [1.3.0-0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01 diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 6875f56631..5236627d03 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -14,8 +14,11 @@ # type: ignore import logging +import threading +from collections import OrderedDict +from collections.abc import MutableMapping from types import MappingProxyType -from typing import MutableSequence, Sequence +from typing import MutableSequence, Optional, Sequence from opentelemetry.util import types @@ -104,7 +107,72 @@ def _filter_attributes(attributes: types.Attributes) -> None: attributes.pop(attr_key) -def _create_immutable_attributes( - attributes: types.Attributes, -) -> types.Attributes: - return MappingProxyType(attributes.copy() if attributes else {}) +_DEFAULT_LIMIT = 128 + + +class BoundedAttributes(MutableMapping): + """An ordered dict with a fixed max capacity. + + Oldest elements are dropped when the dict is full and a new element is + added. + """ + + def __init__( + self, + maxlen: Optional[int] = _DEFAULT_LIMIT, + attributes: types.Attributes = None, + immutable: bool = True, + ): + if maxlen is not None: + if not isinstance(maxlen, int) or maxlen < 0: + raise ValueError( + "maxlen must be valid int greater or equal to 0" + ) + self.maxlen = maxlen + self.dropped = 0 + self._dict = OrderedDict() # type: OrderedDict + self._lock = threading.Lock() # type: threading.Lock + if attributes: + _filter_attributes(attributes) + for key, value in attributes.items(): + self[key] = value + self._immutable = immutable + + def __repr__(self): + return "{}({}, maxlen={})".format( + type(self).__name__, dict(self._dict), self.maxlen + ) + + def __getitem__(self, key): + return self._dict[key] + + def __setitem__(self, key, value): + if getattr(self, "_immutable", False): + raise TypeError + with self._lock: + if self.maxlen is not None and self.maxlen == 0: + self.dropped += 1 + return + + if key in self._dict: + del self._dict[key] + elif self.maxlen is not None and len(self._dict) == self.maxlen: + del self._dict[next(iter(self._dict.keys()))] + self.dropped += 1 + self._dict[key] = value + + def __delitem__(self, key): + if getattr(self, "_immutable", False): + raise TypeError + with self._lock: + del self._dict[key] + + def __iter__(self): + with self._lock: + return iter(self._dict.copy()) + + def __len__(self): + return len(self._dict) + + def copy(self): + return self._dict.copy() diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 37d19a0950..487592f60d 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -82,9 +82,7 @@ from typing import Iterator, Optional, Sequence, cast from opentelemetry import context as context_api -from opentelemetry.attributes import ( # type: ignore - _create_immutable_attributes, -) +from opentelemetry.attributes import BoundedAttributes # type: ignore from opentelemetry.context.context import Context from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER from opentelemetry.trace.propagation import ( @@ -142,8 +140,8 @@ def __init__( attributes: types.Attributes = None, ) -> None: super().__init__(context) - self._attributes = _create_immutable_attributes( - attributes + self._attributes = BoundedAttributes( + attributes=attributes ) # type: types.Attributes @property diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index 2a391f78af..c1151bf4d4 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -14,10 +14,11 @@ # type: ignore +import collections import unittest from opentelemetry.attributes import ( - _create_immutable_attributes, + BoundedAttributes, _filter_attributes, _is_valid_attribute_value, ) @@ -79,9 +80,95 @@ def test_filter_attributes(self): }, ) - def test_create_immutable_attributes(self): - attrs = {"key": "value", "pi": 3.14} - immutable = _create_immutable_attributes(attrs) - # TypeError: 'mappingproxy' object does not support item assignment + +class TestBoundedAttributes(unittest.TestCase): + base = collections.OrderedDict( + [ + ("name", "Firulais"), + ("age", 7), + ("weight", 13), + ("vaccinated", True), + ] + ) + + def test_negative_maxlen(self): + with self.assertRaises(ValueError): + BoundedAttributes(-1) + + def test_from_map(self): + dic_len = len(self.base) + base_copy = collections.OrderedDict(self.base) + bdict = BoundedAttributes(dic_len, base_copy) + + self.assertEqual(len(bdict), dic_len) + + # modify base_copy and test that bdict is not changed + base_copy["name"] = "Bruno" + base_copy["age"] = 3 + + for key in self.base: + self.assertEqual(bdict[key], self.base[key]) + + # test that iter yields the correct number of elements + self.assertEqual(len(tuple(bdict)), dic_len) + + # map too big + half_len = dic_len // 2 + bdict = BoundedAttributes(half_len, self.base) + self.assertEqual(len(tuple(bdict)), half_len) + self.assertEqual(bdict.dropped, dic_len - half_len) + + def test_bounded_dict(self): + # create empty dict + dic_len = len(self.base) + bdict = BoundedAttributes(dic_len, immutable=False) + self.assertEqual(len(bdict), 0) + + # fill dict + for key in self.base: + bdict[key] = self.base[key] + + self.assertEqual(len(bdict), dic_len) + self.assertEqual(bdict.dropped, 0) + + for key in self.base: + self.assertEqual(bdict[key], self.base[key]) + + # test __iter__ in BoundedAttributes + for key in bdict: + self.assertEqual(bdict[key], self.base[key]) + + # updating an existing element should not drop + bdict["name"] = "Bruno" + self.assertEqual(bdict.dropped, 0) + + # try to append more elements + for key in self.base: + bdict["new-" + key] = self.base[key] + + self.assertEqual(len(bdict), dic_len) + self.assertEqual(bdict.dropped, dic_len) + + # test that elements in the dict are the new ones + for key in self.base: + self.assertEqual(bdict["new-" + key], self.base[key]) + + # delete an element + del bdict["new-name"] + self.assertEqual(len(bdict), dic_len - 1) + + with self.assertRaises(KeyError): + _ = bdict["new-name"] + + def test_no_limit_code(self): + bdict = BoundedAttributes(maxlen=None, immutable=False) + for num in range(100): + bdict[num] = num + + for num in range(100): + self.assertEqual(bdict[num], num) + + def test_immutable(self): + bdict = BoundedAttributes() with self.assertRaises(TypeError): - immutable["pi"] = 1.34 + bdict["should-not-work"] = "dict immutable" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index a4efe8d7a5..3ad01b0aec 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -64,10 +64,7 @@ import pkg_resources -from opentelemetry.attributes import ( - _create_immutable_attributes, - _filter_attributes, -) +from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk.environment_variables import ( OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, @@ -147,8 +144,7 @@ class Resource: def __init__( self, attributes: Attributes, schema_url: typing.Optional[str] = None ): - _filter_attributes(attributes) - self._attributes = _create_immutable_attributes(attributes) + self._attributes = BoundedAttributes(attributes=attributes) if schema_url is None: schema_url = "" self._schema_url = schema_url diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index aa172a6a83..26e12c12b9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -40,8 +40,7 @@ from opentelemetry import context as context_api from opentelemetry import trace as trace_api from opentelemetry.attributes import ( - _create_immutable_attributes, - _filter_attributes, + BoundedAttributes, _is_valid_attribute_value, ) from opentelemetry.sdk import util @@ -53,7 +52,7 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import sampling from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator -from opentelemetry.sdk.util import BoundedDict, BoundedList +from opentelemetry.sdk.util import BoundedList from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanContext from opentelemetry.trace.status import Status, StatusCode @@ -65,6 +64,8 @@ _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = 128 _DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT = 128 _DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT = 128 +_DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT = 128 +_DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT = 128 _ENV_VALUE_UNSET = "unset" @@ -475,7 +476,7 @@ def _format_context(context): @staticmethod def _format_attributes(attributes): - if isinstance(attributes, BoundedDict): + if isinstance(attributes, BoundedAttributes): return attributes._dict # pylint: disable=protected-access if isinstance(attributes, MappingProxyType): return attributes.copy() @@ -526,19 +527,21 @@ class SpanLimits: max_links: Maximum number of links that can be added to a Span. Environment variable: OTEL_SPAN_LINK_COUNT_LIMIT Default: {_DEFAULT_SPAN_LINK_COUNT_LIMIT} + max_event_attributes: Maximum number of attributes that can be added to an Event. + Default: {_DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT} + max_link_attributes: Maximum number of attributes that can be added to a Link. + Default: {_DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT} """ UNSET = -1 - max_attributes: int - max_events: int - max_links: int - def __init__( self, max_attributes: Optional[int] = None, max_events: Optional[int] = None, max_links: Optional[int] = None, + max_event_attributes: Optional[int] = None, + max_link_attributes: Optional[int] = None, ): self.max_attributes = self._from_env_if_absent( max_attributes, @@ -555,10 +558,25 @@ def __init__( OTEL_SPAN_LINK_COUNT_LIMIT, _DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT, ) + self.max_event_attributes = self._from_env_if_absent( + max_event_attributes, + OTEL_SPAN_LINK_COUNT_LIMIT, + _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, + ) + self.max_link_attributes = self._from_env_if_absent( + max_link_attributes, + OTEL_SPAN_LINK_COUNT_LIMIT, + _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, + ) def __repr__(self): - return "max_attributes={}, max_events={}, max_links={}".format( - self.max_attributes, self.max_events, self.max_links + return "{}(max_attributes={}, max_events={}, max_links={}, max_event_attributes={}, max_link_attributes={})".format( + type(self).__name__, + self.max_attributes, + self.max_events, + self.max_links, + self.max_event_attributes, + self.max_link_attributes, ) @classmethod @@ -591,6 +609,8 @@ def _from_env_if_absent( max_attributes=SpanLimits.UNSET, max_events=SpanLimits.UNSET, max_links=SpanLimits.UNSET, + max_event_attributes=SpanLimits.UNSET, + max_link_attributes=SpanLimits.UNSET, ) SPAN_ATTRIBUTE_COUNT_LIMIT = SpanLimits._from_env_if_absent( @@ -661,22 +681,14 @@ def __init__( self._span_processor = span_processor self._limits = limits self._lock = threading.Lock() - - _filter_attributes(attributes) - if not attributes: - self._attributes = self._new_attributes() - else: - self._attributes = BoundedDict.from_map( - self._limits.max_attributes, attributes - ) - + self._attributes = BoundedAttributes( + self._limits.max_attributes, attributes, immutable=False + ) self._events = self._new_events() if events: for event in events: - _filter_attributes(event.attributes) - # pylint: disable=protected-access - event._attributes = _create_immutable_attributes( - event.attributes + event._attributes = BoundedAttributes( + self._limits.max_event_attributes, event.attributes ) self._events.append(event) @@ -690,9 +702,6 @@ def __repr__(self): type(self).__name__, self._name, self._context ) - def _new_attributes(self): - return BoundedDict(self._limits.max_attributes) - def _new_events(self): return BoundedList(self._limits.max_events) @@ -745,13 +754,14 @@ def add_event( attributes: types.Attributes = None, timestamp: Optional[int] = None, ) -> None: - _filter_attributes(attributes) - attributes = _create_immutable_attributes(attributes) + attributes = BoundedAttributes( + self._limits.max_event_attributes, attributes + ) self._add_event( Event( name=name, attributes=attributes, - timestamp=_time_ns() if timestamp is None else timestamp, + timestamp=timestamp, ) ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index 746f100277..a94fe647b7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -18,6 +18,8 @@ from collections.abc import MutableMapping, Sequence from typing import Optional +from deprecated import deprecated + def ns_to_iso_str(nanoseconds): """Get an ISO 8601 string from time_ns value.""" @@ -91,6 +93,7 @@ def from_seq(cls, maxlen, seq): return bounded_list +@deprecated(version="1.4.0") # type: ignore class BoundedDict(MutableMapping): """An ordered dict with a fixed max capacity. diff --git a/opentelemetry-sdk/tests/test_util.py b/opentelemetry-sdk/tests/test_util.py index f90576afe7..00099090cd 100644 --- a/opentelemetry-sdk/tests/test_util.py +++ b/opentelemetry-sdk/tests/test_util.py @@ -12,10 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import collections import unittest -from opentelemetry.sdk.util import BoundedDict, BoundedList +from opentelemetry.sdk.util import BoundedList class TestBoundedList(unittest.TestCase): @@ -142,91 +141,3 @@ def test_no_limit(self): for num in range(100): self.assertEqual(blist[num], num) - - -class TestBoundedDict(unittest.TestCase): - base = collections.OrderedDict( - [ - ("name", "Firulais"), - ("age", 7), - ("weight", 13), - ("vaccinated", True), - ] - ) - - def test_negative_maxlen(self): - with self.assertRaises(ValueError): - BoundedDict(-1) - - def test_from_map(self): - dic_len = len(self.base) - base_copy = collections.OrderedDict(self.base) - bdict = BoundedDict.from_map(dic_len, base_copy) - - self.assertEqual(len(bdict), dic_len) - - # modify base_copy and test that bdict is not changed - base_copy["name"] = "Bruno" - base_copy["age"] = 3 - - for key in self.base: - self.assertEqual(bdict[key], self.base[key]) - - # test that iter yields the correct number of elements - self.assertEqual(len(tuple(bdict)), dic_len) - - # map too big - half_len = dic_len // 2 - bdict = BoundedDict.from_map(half_len, self.base) - self.assertEqual(len(tuple(bdict)), half_len) - self.assertEqual(bdict.dropped, dic_len - half_len) - - def test_bounded_dict(self): - # create empty dict - dic_len = len(self.base) - bdict = BoundedDict(dic_len) - self.assertEqual(len(bdict), 0) - - # fill dict - for key in self.base: - bdict[key] = self.base[key] - - self.assertEqual(len(bdict), dic_len) - self.assertEqual(bdict.dropped, 0) - - for key in self.base: - self.assertEqual(bdict[key], self.base[key]) - - # test __iter__ in BoundedDict - for key in bdict: - self.assertEqual(bdict[key], self.base[key]) - - # updating an existing element should not drop - bdict["name"] = "Bruno" - self.assertEqual(bdict.dropped, 0) - - # try to append more elements - for key in self.base: - bdict["new-" + key] = self.base[key] - - self.assertEqual(len(bdict), dic_len) - self.assertEqual(bdict.dropped, dic_len) - - # test that elements in the dict are the new ones - for key in self.base: - self.assertEqual(bdict["new-" + key], self.base[key]) - - # delete an element - del bdict["new-name"] - self.assertEqual(len(bdict), dic_len - 1) - - with self.assertRaises(KeyError): - _ = bdict["new-name"] - - def test_no_limit_code(self): - bdict = BoundedDict(maxlen=None) - for num in range(100): - bdict[num] = num - - for num in range(100): - self.assertEqual(bdict[num], num) From d724573ef91abb880f0e1bd3c4431eb2a5ab0313 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 30 Jun 2021 15:46:07 -0700 Subject: [PATCH 0914/1517] Report dropped attributes/events/links for otlp/jaeger/zipkin exporters (#1893) --- CHANGELOG.md | 6 ++ .../jaeger/proto/grpc/translate/__init__.py | 27 +++++++++ .../tests/test_jaeger_exporter_protobuf.py | 36 ++++++++++-- .../jaeger/thrift/translate/__init__.py | 25 ++++++++- .../tests/test_jaeger_exporter_thrift.py | 43 +++++++++++++- .../proto/grpc/trace_exporter/__init__.py | 14 +++++ .../tests/test_otlp_trace_exporter.py | 56 ++++++++++++++++++- .../exporter/zipkin/encoder/__init__.py | 14 +++++ .../tests/encoder/test_v1_json.py | 16 ++++++ .../tests/encoder/test_v2_json.py | 11 ++++ .../tests/encoder/test_v2_protobuf.py | 16 ++++++ .../src/opentelemetry/sdk/trace/__init__.py | 19 +++++++ opentelemetry-sdk/tests/trace/test_trace.py | 12 ++++ .../src/opentelemetry/test/spantestutil.py | 41 +++++++++++++- 14 files changed, 324 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 306004537d..2f71ff6b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.3.0-0.22b0...HEAD) +### Added +- Dropped attributes/events/links count available exposed on ReadableSpans. + ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) +- Added dropped count to otlp, jaeger and zipkin exporters. + ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) + ### Changed - Updated `opentelemetry-opencensus-exporter` to use `service_name` of spans instead of resource ([#1897](https://github.com/open-telemetry/opentelemetry-python/pull/1897)) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py index e644e21360..65eef76d0f 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py @@ -313,6 +313,24 @@ def _extract_tags( if not span.status.is_ok: translated.append(_get_bool_key_value("error", True)) + if span.dropped_attributes: + translated.append( + _get_long_key_value( + "otel.dropped_attributes_count", span.dropped_attributes + ) + ) + if span.dropped_events: + translated.append( + _get_long_key_value( + "otel.dropped_events_count", span.dropped_events + ) + ) + if span.dropped_links: + translated.append( + _get_long_key_value( + "otel.dropped_links_count", span.dropped_links + ) + ) return translated def _extract_refs( @@ -358,6 +376,15 @@ def _extract_logs( if tag: fields.append(tag) + if event.attributes.dropped: + fields.append( + _translate_attribute( + "otel.dropped_attributes_count", + event.attributes.dropped, + self._max_tag_value_length, + ) + ) + fields.append( _get_string_key_value( key="message", diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py index 3896cdf05a..1123f9b28f 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py @@ -39,9 +39,20 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.test.spantestutil import ( + get_span_with_dropped_attributes_events_links, +) from opentelemetry.trace.status import Status, StatusCode +def _translate_spans_with_dropped_attributes(): + span = get_span_with_dropped_attributes_events_links() + translate = Translate([span]) + + # pylint: disable=protected-access + return translate._translate(pb_translator.ProtobufTranslator("svc")) + + # pylint:disable=no-member class TestJaegerExporter(unittest.TestCase): def setUp(self): @@ -475,8 +486,23 @@ def test_export_span_service_name(self): exporter.export([span]) self.assertEqual(exporter.service_name, "test") - -class MockResponse: - def __init__(self, status_code): - self.status_code = status_code - self.text = status_code + def test_dropped_span_attributes(self): + spans = _translate_spans_with_dropped_attributes() + tags_by_keys = { + tag.key: tag.v_str or tag.v_int64 for tag in spans[0].tags + } + self.assertEqual(1, tags_by_keys["otel.dropped_links_count"]) + self.assertEqual(2, tags_by_keys["otel.dropped_attributes_count"]) + self.assertEqual(3, tags_by_keys["otel.dropped_events_count"]) + + def test_dropped_event_attributes(self): + spans = _translate_spans_with_dropped_attributes() + fields_by_keys = { + tag.key: tag.v_str or tag.v_int64 + for tag in spans[0].logs[0].fields + } + # get events + self.assertEqual( + 2, + fields_by_keys["otel.dropped_attributes_count"], + ) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py index c48c8bde8f..f21ede6f8b 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py @@ -46,6 +46,11 @@ def _convert_int_to_i64(val): return val +def _append_dropped(tags, key, val): + if val: + tags.append(_get_long_tag(key, val)) + + class Translator(abc.ABC): def __init__(self, max_tag_value_length: Optional[int] = None): self._max_tag_value_length = max_tag_value_length @@ -181,7 +186,7 @@ def _translate_span(self, span: ReadableSpan) -> TCollector.Span: return jaeger_span def _extract_tags(self, span: ReadableSpan) -> Sequence[TCollector.Tag]: - + # pylint: disable=too-many-branches translated = [] if span.attributes: for key, value in span.attributes.items(): @@ -226,6 +231,18 @@ def _extract_tags(self, span: ReadableSpan) -> Sequence[TCollector.Tag]: if not span.status.is_ok: translated.append(_get_bool_tag("error", True)) + _append_dropped( + translated, + "otel.dropped_attributes_count", + span.dropped_attributes, + ) + _append_dropped( + translated, "otel.dropped_events_count", span.dropped_events + ) + _append_dropped( + translated, "otel.dropped_links_count", span.dropped_links + ) + return translated def _extract_refs( @@ -269,6 +286,12 @@ def _extract_logs( if tag: fields.append(tag) + _append_dropped( + fields, + "otel.dropped_attributes_count", + event.attributes.dropped, + ) + fields.append( TCollector.Tag( key="message", diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py index 679494fe3b..0da65d0477 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py @@ -38,10 +38,21 @@ from opentelemetry.sdk.resources import SERVICE_NAME from opentelemetry.sdk.trace import Resource, TracerProvider from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.test.spantestutil import ( + get_span_with_dropped_attributes_events_links, +) from opentelemetry.trace import SpanKind from opentelemetry.trace.status import Status, StatusCode +def _translate_spans_with_dropped_attributes(): + span = get_span_with_dropped_attributes_events_links() + translate = Translate([span]) + + # pylint: disable=protected-access + return translate._translate(ThriftTranslator(max_tag_value_length=5)) + + class TestJaegerExporter(unittest.TestCase): def setUp(self): # create and save span to be used in tests @@ -562,7 +573,9 @@ def test_max_tag_value_length(self): # pylint: disable=protected-access spans = translate._translate(ThriftTranslator()) tags_by_keys = { - tag.key: tag.vStr for tag in spans[0].tags if tag.vType == 0 + tag.key: tag.vStr + for tag in spans[0].tags + if tag.vType == jaeger.TagType.STRING } self.assertEqual( "hello_world hello_world hello_world", tags_by_keys["key_string"] @@ -579,12 +592,38 @@ def test_max_tag_value_length(self): # pylint: disable=protected-access spans = translate._translate(ThriftTranslator(max_tag_value_length=5)) tags_by_keys = { - tag.key: tag.vStr for tag in spans[0].tags if tag.vType == 0 + tag.key: tag.vStr + for tag in spans[0].tags + if tag.vType == jaeger.TagType.STRING } self.assertEqual("hello", tags_by_keys["key_string"]) self.assertEqual("('tup", tags_by_keys["key_tuple"]) self.assertEqual("some_", tags_by_keys["key_resource"]) + def test_dropped_span_attributes(self): + spans = _translate_spans_with_dropped_attributes() + tags_by_keys = { + tag.key: tag.vLong + for tag in spans[0].tags + if tag.vType == jaeger.TagType.LONG + } + + self.assertEqual(1, tags_by_keys["otel.dropped_links_count"]) + self.assertEqual(2, tags_by_keys["otel.dropped_attributes_count"]) + self.assertEqual(3, tags_by_keys["otel.dropped_events_count"]) + + def test_dropped_event_attributes(self): + spans = _translate_spans_with_dropped_attributes() + tags_by_keys = { + tag.key: tag.vLong + for tag in spans[0].logs[0].fields + if tag.vType == jaeger.TagType.LONG + } + self.assertEqual( + 2, + tags_by_keys["otel.dropped_attributes_count"], + ) + def test_agent_client_split(self): agent_client = jaeger_exporter.AgentClientUDP( host_name="localhost", diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index e7f54c1ac3..cbaec907e5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -175,6 +175,7 @@ def _translate_events(self, sdk_span: ReadableSpan) -> None: collector_span_event = CollectorSpan.Event( name=sdk_span_event.name, time_unix_nano=sdk_span_event.timestamp, + dropped_attributes_count=sdk_span_event.attributes.dropped, ) for key, value in sdk_span_event.attributes.items(): @@ -201,6 +202,7 @@ def _translate_links(self, sdk_span: ReadableSpan) -> None: sdk_span_link.context.trace_id.to_bytes(16, "big") ), span_id=(sdk_span_link.context.span_id.to_bytes(8, "big")), + dropped_attributes_count=sdk_span_link.attributes.dropped, ) for key, value in sdk_span_link.attributes.items(): @@ -272,6 +274,18 @@ def _translate_data( self._translate_events(sdk_span) self._translate_links(sdk_span) self._translate_status(sdk_span) + if sdk_span.dropped_attributes: + self._collector_span_kwargs[ + "dropped_attributes_count" + ] = sdk_span.dropped_attributes + if sdk_span.dropped_events: + self._collector_span_kwargs[ + "dropped_events_count" + ] = sdk_span.dropped_events + if sdk_span.dropped_links: + self._collector_span_kwargs[ + "dropped_links_count" + ] = sdk_span.dropped_links self._collector_span_kwargs["kind"] = getattr( CollectorSpan.SpanKind, diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 521c9e6e82..09dbda1682 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -22,6 +22,7 @@ from google.rpc.error_details_pb2 import RetryInfo from grpc import ChannelCredentials, Compression, StatusCode, server +from opentelemetry.attributes import BoundedAttributes from opentelemetry.exporter.otlp.proto.grpc.exporter import ( _translate_key_values, ) @@ -68,6 +69,9 @@ SpanExportResult, ) from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.test.spantestutil import ( + get_span_with_dropped_attributes_events_links, +) THIS_DIR = os.path.dirname(__file__) @@ -134,7 +138,9 @@ def setUp(self): event_mock = Mock( **{ "timestamp": 1591240820506462784, - "attributes": OrderedDict([("a", 1), ("b", False)]), + "attributes": BoundedAttributes( + attributes={"a": 1, "b": False} + ), } ) @@ -151,14 +157,16 @@ def setUp(self): ), resource=SDKResource(OrderedDict([("a", 1), ("b", False)])), parent=Mock(**{"span_id": 12345}), - attributes=OrderedDict([("a", 1), ("b", True)]), + attributes=BoundedAttributes(attributes={"a": 1, "b": True}), events=[event_mock], links=[ Mock( **{ "context.trace_id": 1, "context.span_id": 2, - "attributes": OrderedDict([("a", 1), ("b", False)]), + "attributes": BoundedAttributes( + attributes={"a": 1, "b": False} + ), "kind": OTLPSpan.SpanKind.SPAN_KIND_INTERNAL, # pylint: disable=no-member } ) @@ -601,6 +609,48 @@ def test_translate_key_values(self): # self.assertEqual(kvlist_value.values[0].key, "asd") # self.assertEqual(kvlist_value.values[0].value.string_value, "123") + def test_dropped_values(self): + span = get_span_with_dropped_attributes_events_links() + # pylint:disable=protected-access + translated = self.exporter._translate_data([span]) + self.assertEqual( + 1, + translated.resource_spans[0] + .instrumentation_library_spans[0] + .spans[0] + .dropped_links_count, + ) + self.assertEqual( + 2, + translated.resource_spans[0] + .instrumentation_library_spans[0] + .spans[0] + .dropped_attributes_count, + ) + self.assertEqual( + 3, + translated.resource_spans[0] + .instrumentation_library_spans[0] + .spans[0] + .dropped_events_count, + ) + self.assertEqual( + 2, + translated.resource_spans[0] + .instrumentation_library_spans[0] + .spans[0] + .links[0] + .dropped_attributes_count, + ) + self.assertEqual( + 2, + translated.resource_spans[0] + .instrumentation_library_spans[0] + .spans[0] + .events[0] + .dropped_attributes_count, + ) + def _create_span_with_status(status: SDKStatus): span = _Span( diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py index 3390c19a46..07ad735ece 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py @@ -207,6 +207,20 @@ def _extract_tags_from_span(self, span: Span) -> Dict[str, str]: tags.update({"otel.status_code": span.status.status_code.name}) if span.status.status_code is StatusCode.ERROR: tags.update({"error": span.status.description or ""}) + + if span.dropped_attributes: + tags.update( + {"otel.dropped_attributes_count": str(span.dropped_attributes)} + ) + + if span.dropped_events: + tags.update( + {"otel.dropped_events_count": str(span.dropped_events)} + ) + + if span.dropped_links: + tags.update({"otel.dropped_links_count": str(span.dropped_links)}) + return tags def _extract_annotations_from_events( diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py index c1956500bc..a7e0480f0e 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py @@ -18,6 +18,9 @@ from opentelemetry.exporter.zipkin.json.v1 import JsonV1Encoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace +from opentelemetry.test.spantestutil import ( + get_span_with_dropped_attributes_events_links, +) from opentelemetry.trace import TraceFlags, format_span_id, format_trace_id from .common_tests import TEST_SERVICE_NAME, CommonEncoderTestCases @@ -249,3 +252,16 @@ def _test_encode_max_tag_length(self, max_tag_value_length: int): [otel_span], NodeEndpoint() ), ) + + def test_dropped_span_attributes(self): + otel_span = get_span_with_dropped_attributes_events_links() + annotations = JsonV1Encoder()._encode_span(otel_span, "test")[ + "binaryAnnotations" + ] + annotations = { + annotation["key"]: annotation["value"] + for annotation in annotations + } + self.assertEqual("1", annotations["otel.dropped_links_count"]) + self.assertEqual("2", annotations["otel.dropped_attributes_count"]) + self.assertEqual("3", annotations["otel.dropped_events_count"]) diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py index eb0ad6000a..2898604a25 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py @@ -18,6 +18,9 @@ from opentelemetry.exporter.zipkin.json.v2 import JsonV2Encoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace +from opentelemetry.test.spantestutil import ( + get_span_with_dropped_attributes_events_links, +) from opentelemetry.trace import SpanKind, TraceFlags from .common_tests import TEST_SERVICE_NAME, CommonEncoderTestCases @@ -203,3 +206,11 @@ def _test_encode_max_tag_length(self, max_tag_value_length: int): [otel_span], NodeEndpoint() ), ) + + def test_dropped_span_attributes(self): + otel_span = get_span_with_dropped_attributes_events_links() + tags = JsonV2Encoder()._encode_span(otel_span, "test")["tags"] + + self.assertEqual("1", tags["otel.dropped_links_count"]) + self.assertEqual("2", tags["otel.dropped_attributes_count"]) + self.assertEqual("3", tags["otel.dropped_events_count"]) diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py index 8cf9f6abf5..787566e710 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py @@ -18,6 +18,9 @@ from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.exporter.zipkin.proto.http.v2 import ProtobufEncoder from opentelemetry.exporter.zipkin.proto.http.v2.gen import zipkin_pb2 +from opentelemetry.test.spantestutil import ( + get_span_with_dropped_attributes_events_links, +) from opentelemetry.trace import SpanKind from .common_tests import TEST_SERVICE_NAME, CommonEncoderTestCases @@ -232,3 +235,16 @@ def _test_encode_max_tag_length(self, max_tag_value_length: int): ) self.assertEqual(actual_output, expected_output) + + def test_dropped_span_attributes(self): + otel_span = get_span_with_dropped_attributes_events_links() + # pylint: disable=no-member + tags = ( + ProtobufEncoder() + ._encode_span(otel_span, zipkin_pb2.Endpoint()) + .tags + ) + + self.assertEqual("1", tags["otel.dropped_links_count"]) + self.assertEqual("2", tags["otel.dropped_attributes_count"]) + self.assertEqual("3", tags["otel.dropped_events_count"]) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 26e12c12b9..19531a75a5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -312,6 +312,7 @@ def __init__( name: str, attributes: types.Attributes = None, timestamp: Optional[int] = None, + limit: Optional[int] = _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, ) -> None: super().__init__(name, timestamp) self._attributes = attributes @@ -367,6 +368,24 @@ def __init__( self._resource = resource self._status = status + @property + def dropped_attributes(self) -> int: + if self._attributes: + return self._attributes.dropped + return 0 + + @property + def dropped_events(self) -> int: + if self._events: + return self._events.dropped + return 0 + + @property + def dropped_links(self) -> int: + if self._links: + return self._links.dropped + return 0 + @property def name(self) -> str: return self._name diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 2ec2c0b7a3..e331642e44 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -36,6 +36,9 @@ from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.test.spantestutil import ( + get_span_with_dropped_attributes_events_links, +) from opentelemetry.trace import StatusCode from opentelemetry.util._time import _time_ns @@ -1481,3 +1484,12 @@ def test_span_no_limits_code(self): ) ) ) + + def test_dropped_attributes(self): + span = get_span_with_dropped_attributes_events_links() + self.assertEqual(1, span.dropped_links) + self.assertEqual(2, span.dropped_attributes) + self.assertEqual(3, span.dropped_events) + self.assertEqual(2, span.events[0].attributes.dropped) + self.assertEqual(2, span.links[0].attributes.dropped) + self.assertEqual(2, span.resource.attributes.dropped) diff --git a/tests/util/src/opentelemetry/test/spantestutil.py b/tests/util/src/opentelemetry/test/spantestutil.py index 3e7de79158..faf135f2ae 100644 --- a/tests/util/src/opentelemetry/test/spantestutil.py +++ b/tests/util/src/opentelemetry/test/spantestutil.py @@ -16,7 +16,8 @@ from importlib import reload from opentelemetry import trace as trace_api -from opentelemetry.sdk.trace import TracerProvider, export +from opentelemetry.sdk import trace as trace_sdk +from opentelemetry.sdk.trace import Resource, TracerProvider, export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) @@ -41,3 +42,41 @@ def tearDownClass(cls): def setUp(self): self.memory_exporter = _MEMORY_EXPORTER self.memory_exporter.clear() + + +def get_span_with_dropped_attributes_events_links(): + attributes = {} + for index in range(130): + attributes["key{}".format(index)] = ["value{}".format(index)] + links = [] + for index in range(129): + links.append( + trace_api.Link( + trace_sdk._Span( + name="span{}".format(index), + context=trace_api.INVALID_SPAN_CONTEXT, + attributes=attributes, + ).get_span_context(), + attributes=attributes, + ) + ) + span = trace_sdk._Span( + limits=trace_sdk.SpanLimits(), + name="span", + resource=Resource( + attributes=attributes, + ), + context=trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + ), + links=links, + attributes=attributes, + ) + + span.start() + for index in range(131): + span.add_event("event{}".format(index), attributes=attributes) + span.end() + return span From a10faaf37e32e51959796747805444d655ab61c3 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 1 Jul 2021 14:27:55 -0700 Subject: [PATCH 0915/1517] Add excerpt for hotfixes (#1925) --- .github/workflows/test.yml | 2 +- RELEASING.md | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c88b731332..7e2acbe3a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 82c4f600872a72fedaebad758f0d5588dfb53a00 + CONTRIB_REPO_SHA: c100b21fa40727709bb31cf4e2b67b737a09ea2c jobs: build: diff --git a/RELEASING.md b/RELEASING.md index ab1e955074..b57632048f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -73,7 +73,6 @@ run eachdist once again: ./scripts/eachdist.py update_versions --versions stable,prerelease ``` - ## Update website docs If the docs for the Opentelemetry [website](https://opentelemetry.io/docs/python/) was updated in this release in this [folder](https://github.com/open-telemetry/opentelemetry-python/tree/main/website_docs), submit a [PR](https://github.com/open-telemetry/opentelemetry.io/tree/main/content/en/docs/python) to update the docs on the website accordingly. To check if the new version requires updating, run the following script and compare the diff: @@ -84,6 +83,21 @@ If the docs for the Opentelemetry [website](https://opentelemetry.io/docs/python If the diff includes significant changes, create a pull request to commit the changes and once the changes are merged, click the "Run workflow" button for the Update [OpenTelemetry Website Docs](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/docs-update.yml) GitHub Action. +## Hotfix procedure + +A `hotfix` is defined as a small change developed to correct a bug that should be released as quickly as possible. Due to the nature of hotfixes, they usually will only affect one or a few packages. Therefore, it usually is not necessary to go through the entire release process outlined above for hotfixes. Follow the below steps how to release a hotfix: + +1. Identify the packages that are affected by the bug. Make the changes to those packages, merging to `main`, as quickly as possible. +2. On your local machine, remove the `dev0` tags from the version number and increment the patch version number. +3. On your local machine, update `CHANGELOG.md` with the date of the hotfix change. +4. With administrator priviledges for PyPi, manually publish the affected packages. + a. Install [twine](https://pypi.org/project/twine/) + b. Navigate to where the `setup.py` file exists for the package you want to publish. + c. Run `python setup.py sdist bdist_wheel`. You may have to install [wheel](https://pypi.org/project/wheel/) as well. + d. Validate your built distributions by running `twine check dist/*`. + e. Upload distibutions to PyPi by running `twine upload dist/*`. +5. Note that since hotfixes are manually published, the build scripts for publish after creating a release are not run. + ## Troubleshooting ### Publish failed From 0e126544fba25c0f937a09e7c8cfbc7b6e6e518a Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 2 Jul 2021 04:23:17 +0530 Subject: [PATCH 0916/1517] Use BatchSpanProcessor in examples (#1928) --- docs/examples/auto-instrumentation/client.py | 4 ++-- .../auto-instrumentation/server_instrumented.py | 4 ++-- .../auto-instrumentation/server_uninstrumented.py | 4 ++-- docs/examples/basic_tracer/basic_trace.py | 4 ++-- docs/examples/basic_tracer/resources.py | 4 ++-- docs/examples/django/client.py | 4 ++-- docs/examples/django/pages/views.py | 4 ++-- docs/examples/fork-process-model/flask-gunicorn/app.py | 3 ++- docs/examples/fork-process-model/flask-uwsgi/app.py | 3 ++- docs/examples/opentracing/main.py | 4 ++-- docs/faq-and-cookbook.rst | 10 +++++----- docs/getting_started/flask_example.py | 7 ++++--- docs/getting_started/tests/test_flask.py | 2 +- docs/getting_started/tracing_example.py | 4 ++-- 14 files changed, 32 insertions(+), 29 deletions(-) diff --git a/docs/examples/auto-instrumentation/client.py b/docs/examples/auto-instrumentation/client.py index f69fa1c6d2..4f70e2b933 100644 --- a/docs/examples/auto-instrumentation/client.py +++ b/docs/examples/auto-instrumentation/client.py @@ -20,15 +20,15 @@ from opentelemetry.propagate import inject from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, ConsoleSpanExporter, - SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer_provider().get_tracer(__name__) trace.get_tracer_provider().add_span_processor( - SimpleSpanProcessor(ConsoleSpanExporter()) + BatchSpanProcessor(ConsoleSpanExporter()) ) diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_instrumented.py index 652358e3a2..18db356af6 100644 --- a/docs/examples/auto-instrumentation/server_instrumented.py +++ b/docs/examples/auto-instrumentation/server_instrumented.py @@ -19,8 +19,8 @@ from opentelemetry.propagate import extract from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, ConsoleSpanExporter, - SimpleSpanProcessor, ) app = Flask(__name__) @@ -29,7 +29,7 @@ tracer = trace.get_tracer_provider().get_tracer(__name__) trace.get_tracer_provider().add_span_processor( - SimpleSpanProcessor(ConsoleSpanExporter()) + BatchSpanProcessor(ConsoleSpanExporter()) ) diff --git a/docs/examples/auto-instrumentation/server_uninstrumented.py b/docs/examples/auto-instrumentation/server_uninstrumented.py index 5edcd5cf60..e6fa75f1b5 100644 --- a/docs/examples/auto-instrumentation/server_uninstrumented.py +++ b/docs/examples/auto-instrumentation/server_uninstrumented.py @@ -17,8 +17,8 @@ from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, ConsoleSpanExporter, - SimpleSpanProcessor, ) app = Flask(__name__) @@ -26,7 +26,7 @@ trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( - SimpleSpanProcessor(ConsoleSpanExporter()) + BatchSpanProcessor(ConsoleSpanExporter()) ) diff --git a/docs/examples/basic_tracer/basic_trace.py b/docs/examples/basic_tracer/basic_trace.py index 54fcb1c3c6..bb1e341a61 100644 --- a/docs/examples/basic_tracer/basic_trace.py +++ b/docs/examples/basic_tracer/basic_trace.py @@ -15,13 +15,13 @@ from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, ConsoleSpanExporter, - SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( - SimpleSpanProcessor(ConsoleSpanExporter()) + BatchSpanProcessor(ConsoleSpanExporter()) ) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("foo"): diff --git a/docs/examples/basic_tracer/resources.py b/docs/examples/basic_tracer/resources.py index 0f07c42b3e..87853a8f66 100644 --- a/docs/examples/basic_tracer/resources.py +++ b/docs/examples/basic_tracer/resources.py @@ -16,8 +16,8 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, ConsoleSpanExporter, - SimpleSpanProcessor, ) # Use Resource.create() instead of constructor directly @@ -26,7 +26,7 @@ trace.set_tracer_provider(TracerProvider(resource=resource)) trace.get_tracer_provider().add_span_processor( - SimpleSpanProcessor(ConsoleSpanExporter()) + BatchSpanProcessor(ConsoleSpanExporter()) ) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("foo"): diff --git a/docs/examples/django/client.py b/docs/examples/django/client.py index 3ae0cb6e1c..859fe4a9da 100644 --- a/docs/examples/django/client.py +++ b/docs/examples/django/client.py @@ -20,15 +20,15 @@ from opentelemetry.propagate import inject from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, ConsoleSpanExporter, - SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer_provider().get_tracer(__name__) trace.get_tracer_provider().add_span_processor( - SimpleSpanProcessor(ConsoleSpanExporter()) + BatchSpanProcessor(ConsoleSpanExporter()) ) diff --git a/docs/examples/django/pages/views.py b/docs/examples/django/pages/views.py index e69de63f09..e805f43186 100644 --- a/docs/examples/django/pages/views.py +++ b/docs/examples/django/pages/views.py @@ -16,14 +16,14 @@ from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, ConsoleSpanExporter, - SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( - SimpleSpanProcessor(ConsoleSpanExporter()) + BatchSpanProcessor(ConsoleSpanExporter()) ) diff --git a/docs/examples/fork-process-model/flask-gunicorn/app.py b/docs/examples/fork-process-model/flask-gunicorn/app.py index e8ea6a1dea..4aa3b9f4c5 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/app.py +++ b/docs/examples/fork-process-model/flask-gunicorn/app.py @@ -22,6 +22,8 @@ FlaskInstrumentor().instrument_app(application) +tracer = trace.get_tracer(__name__) + def fib_slow(n): if n <= 1: @@ -39,7 +41,6 @@ def fib_fast(n): @application.route("/fibonacci") def fibonacci(): - tracer = trace.get_tracer(__name__) n = int(request.args.get("n", 1)) with tracer.start_as_current_span("root"): with tracer.start_as_current_span("fib_slow") as slow_span: diff --git a/docs/examples/fork-process-model/flask-uwsgi/app.py b/docs/examples/fork-process-model/flask-uwsgi/app.py index fb56037c06..f5b7bfdab0 100644 --- a/docs/examples/fork-process-model/flask-uwsgi/app.py +++ b/docs/examples/fork-process-model/flask-uwsgi/app.py @@ -29,6 +29,8 @@ FlaskInstrumentor().instrument_app(application) +tracer = trace.get_tracer(__name__) + @postfork def init_tracing(): @@ -59,7 +61,6 @@ def fib_fast(n): @application.route("/fibonacci") def fibonacci(): - tracer = trace.get_tracer(__name__) n = int(request.args.get("n", 1)) with tracer.start_as_current_span("root"): with tracer.start_as_current_span("fib_slow") as slow_span: diff --git a/docs/examples/opentracing/main.py b/docs/examples/opentracing/main.py index 9c6252ff8c..8258b672e1 100755 --- a/docs/examples/opentracing/main.py +++ b/docs/examples/opentracing/main.py @@ -5,7 +5,7 @@ from opentelemetry import trace from opentelemetry.exporter.jaeger.thrift import JaegerExporter from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.shim import opentracing_shim # Configure the tracer using the default implementation @@ -17,7 +17,7 @@ agent_host_name="localhost", agent_port=6831, ) -span_processor = SimpleSpanProcessor(jaeger_exporter) +span_processor = BatchSpanProcessor(jaeger_exporter) tracer_provider.add_span_processor(span_processor) # Create an OpenTracing shim. This implements the OpenTracing tracer API, but diff --git a/docs/faq-and-cookbook.rst b/docs/faq-and-cookbook.rst index 0f077e5324..05bb58f324 100644 --- a/docs/faq-and-cookbook.rst +++ b/docs/faq-and-cookbook.rst @@ -52,11 +52,11 @@ Manually setting span context from opentelemetry import trace from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor + from opentelemetry.sdk.trace.export import ConsoleSpanExporter, BatchSpanProcessor from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator trace.set_tracer_provider(TracerProvider()) - trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) + trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) tracer = trace.get_tracer(__name__) @@ -87,13 +87,13 @@ Using multiple tracer providers with different Resource from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.resources import Resource - from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor + from opentelemetry.sdk.trace.export import ConsoleSpanExporter, BatchSpanProcessor # Global tracer provider which can be set only once trace.set_tracer_provider( TracerProvider(resource=Resource.create({"service.name": "service1"})) ) - trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) + trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("some-name") as span: @@ -104,7 +104,7 @@ Using multiple tracer providers with different Resource another_tracer_provider = TracerProvider( resource=Resource.create({"service.name": "service2"}) ) - another_tracer_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) + another_tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) another_tracer = trace.get_tracer(__name__, tracer_provider=another_tracer_provider) with another_tracer.start_as_current_span("name-here") as span: diff --git a/docs/getting_started/flask_example.py b/docs/getting_started/flask_example.py index 2b60d4def8..ddde3aa839 100644 --- a/docs/getting_started/flask_example.py +++ b/docs/getting_started/flask_example.py @@ -21,23 +21,24 @@ from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, ConsoleSpanExporter, - SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( - SimpleSpanProcessor(ConsoleSpanExporter()) + BatchSpanProcessor(ConsoleSpanExporter()) ) app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) RequestsInstrumentor().instrument() +tracer = trace.get_tracer(__name__) + @app.route("/") def hello(): - tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("example-request"): requests.get("http://www.example.com") return "hello" diff --git a/docs/getting_started/tests/test_flask.py b/docs/getting_started/tests/test_flask.py index d1475133f9..d617cfd652 100644 --- a/docs/getting_started/tests/test_flask.py +++ b/docs/getting_started/tests/test_flask.py @@ -39,7 +39,7 @@ def test_flask(self): result = http.get("http://localhost:5000") self.assertEqual(result.status_code, 200) - sleep(0.1) + sleep(5) finally: server.terminate() diff --git a/docs/getting_started/tracing_example.py b/docs/getting_started/tracing_example.py index 30e57178fc..519e45f360 100644 --- a/docs/getting_started/tracing_example.py +++ b/docs/getting_started/tracing_example.py @@ -16,12 +16,12 @@ from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, ConsoleSpanExporter, - SimpleSpanProcessor, ) provider = TracerProvider() -processor = SimpleSpanProcessor(ConsoleSpanExporter()) +processor = BatchSpanProcessor(ConsoleSpanExporter()) provider.add_span_processor(processor) trace.set_tracer_provider(provider) From b572e1124ecad47a08bee028b05eb12cad03f147 Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Tue, 6 Jul 2021 23:37:18 +0100 Subject: [PATCH 0917/1517] move fix to correct section in changelog (#1932) Co-authored-by: alrex --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f71ff6b41..750e9cc11a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 API. Marked `BoundedDict` in the SDK as deprecated as a result. ([#1915](https://github.com/open-telemetry/opentelemetry-python/pull/1915)) +### Fixed +- Updated `opentelementry-opentracing-shim` `ScopeShim` to report exceptions in + opentelemetry specification format, rather than opentracing spec format. + ([#1878](https://github.com/open-telemetry/opentelemetry-python/pull/1878)) + ## [1.3.0-0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01 ### Added @@ -47,10 +52,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update protos to latest version release 0.9.0 ([#1873](https://github.com/open-telemetry/opentelemetry-python/pull/1873)) -### Fixed -- Updated `opentelementry-opentracing-shim` `ScopeShim` to report exceptions in - opentelemetry specification format, rather than opentracing spec format. - ## [1.2.0, 0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0-0.21b0) - 2021-05-11 ### Added From f11ed2f3bacb11d53a7a2b4837cf6308fa34cc71 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 8 Jul 2021 09:18:48 -0700 Subject: [PATCH 0918/1517] Fix OTLP exporter not grouping exported spans properly (#1927) --- CHANGELOG.md | 2 + .../exporter/otlp/proto/grpc/exporter.py | 6 +- .../proto/grpc/trace_exporter/__init__.py | 57 +++-- .../tests/test_otlp_trace_exporter.py | 211 ++++++++++++++++++ 4 files changed, 251 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 750e9cc11a..f6e0aba903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `BoundedAttributes` to the API to make it available for `Link` which is defined in the API. Marked `BoundedDict` in the SDK as deprecated as a result. ([#1915](https://github.com/open-telemetry/opentelemetry-python/pull/1915)) +- Fix OTLP SpanExporter to distinguish spans based off Resource and InstrumentationInfo + ([#1927](https://github.com/open-telemetry/opentelemetry-python/pull/1927)) ### Fixed - Updated `opentelementry-opentracing-shim` `ScopeShim` to report exceptions in diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 7b9227fd07..ba88dc1d7d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -157,9 +157,9 @@ def get_resource_data( resource_class( **{ "resource": collector_resource, - "instrumentation_library_{}".format(name): [ - instrumentation_library_data - ], + "instrumentation_library_{}".format( + name + ): instrumentation_library_data.values(), } ) ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index cbaec907e5..8fa98feb36 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -238,29 +238,42 @@ def _translate_data( sdk_resource_instrumentation_library_spans = {} for sdk_span in data: - - if sdk_span.resource not in ( - sdk_resource_instrumentation_library_spans.keys() - ): + instrumentation_library_spans_map = ( + sdk_resource_instrumentation_library_spans.get( + sdk_span.resource, {} + ) + ) + # If we haven't seen the Resource yet, add it to the map + if not instrumentation_library_spans_map: + sdk_resource_instrumentation_library_spans[ + sdk_span.resource + ] = instrumentation_library_spans_map + instrumentation_library_spans = ( + instrumentation_library_spans_map.get( + sdk_span.instrumentation_info + ) + ) + # If we haven't seen the InstrumentationInfo for this Resource yet, add it to the map + if not instrumentation_library_spans: if sdk_span.instrumentation_info is not None: - instrumentation_library_spans = ( - InstrumentationLibrarySpans( - instrumentation_library=InstrumentationLibrary( - name=sdk_span.instrumentation_info.name, - version=sdk_span.instrumentation_info.version, - ) + instrumentation_library_spans_map[ + sdk_span.instrumentation_info + ] = InstrumentationLibrarySpans( + instrumentation_library=InstrumentationLibrary( + name=sdk_span.instrumentation_info.name, + version=sdk_span.instrumentation_info.version, ) ) - else: - instrumentation_library_spans = ( - InstrumentationLibrarySpans() - ) - - sdk_resource_instrumentation_library_spans[ - sdk_span.resource - ] = instrumentation_library_spans - + # If no InstrumentationInfo, store in None key + instrumentation_library_spans_map[ + sdk_span.instrumentation_info + ] = InstrumentationLibrarySpans() + instrumentation_library_spans = ( + instrumentation_library_spans_map.get( + sdk_span.instrumentation_info + ) + ) self._collector_span_kwargs = {} self._translate_name(sdk_span) @@ -292,9 +305,9 @@ def _translate_data( "SPAN_KIND_{}".format(sdk_span.kind.name), ) - sdk_resource_instrumentation_library_spans[ - sdk_span.resource - ].spans.append(CollectorSpan(**self._collector_span_kwargs)) + instrumentation_library_spans.spans.append( + CollectorSpan(**self._collector_span_kwargs) + ) return ExportTraceServiceRequest( resource_spans=get_resource_data( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 09dbda1682..1a244b0067 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -176,8 +176,44 @@ def setUp(self): ), ) + self.span2 = _Span( + "b", + context=Mock( + **{ + "trace_state": OrderedDict([("a", "b"), ("c", "d")]), + "span_id": 10217189687419569865, + "trace_id": 67545097771067222548457157018666467027, + } + ), + resource=SDKResource(OrderedDict([("a", 2), ("b", False)])), + parent=Mock(**{"span_id": 12345}), + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), + ) + + self.span3 = _Span( + "c", + context=Mock( + **{ + "trace_state": OrderedDict([("a", "b"), ("c", "d")]), + "span_id": 10217189687419569865, + "trace_id": 67545097771067222548457157018666467027, + } + ), + resource=SDKResource(OrderedDict([("a", 1), ("b", False)])), + parent=Mock(**{"span_id": 12345}), + instrumentation_info=InstrumentationInfo( + name="name2", version="version2" + ), + ) + self.span.start() self.span.end() + self.span2.start() + self.span2.end() + self.span3.start() + self.span3.end() def tearDown(self): self.server.stop(None) @@ -505,6 +541,181 @@ def test_translate_spans(self): # pylint: disable=protected-access self.assertEqual(expected, self.exporter._translate_data([self.span])) + def test_translate_spans_multi(self): + expected = ExportTraceServiceRequest( + resource_spans=[ + ResourceSpans( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_spans=[ + InstrumentationLibrarySpans( + instrumentation_library=InstrumentationLibrary( + name="name", version="version" + ), + spans=[ + OTLPSpan( + # pylint: disable=no-member + name="a", + start_time_unix_nano=self.span.start_time, + end_time_unix_nano=self.span.end_time, + trace_state="a=b,c=d", + span_id=int.to_bytes( + 10217189687419569865, 8, "big" + ), + trace_id=int.to_bytes( + 67545097771067222548457157018666467027, + 16, + "big", + ), + parent_span_id=( + b"\000\000\000\000\000\00009" + ), + kind=( + OTLPSpan.SpanKind.SPAN_KIND_INTERNAL + ), + attributes=[ + KeyValue( + key="a", + value=AnyValue(int_value=1), + ), + KeyValue( + key="b", + value=AnyValue(bool_value=True), + ), + ], + events=[ + OTLPSpan.Event( + name="a", + time_unix_nano=1591240820506462784, + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=False + ), + ), + ], + ) + ], + status=Status(code=0, message=""), + links=[ + OTLPSpan.Link( + trace_id=int.to_bytes( + 1, 16, "big" + ), + span_id=int.to_bytes(2, 8, "big"), + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=False + ), + ), + ], + ) + ], + ) + ], + ), + InstrumentationLibrarySpans( + instrumentation_library=InstrumentationLibrary( + name="name2", version="version2" + ), + spans=[ + OTLPSpan( + # pylint: disable=no-member + name="c", + start_time_unix_nano=self.span3.start_time, + end_time_unix_nano=self.span3.end_time, + trace_state="a=b,c=d", + span_id=int.to_bytes( + 10217189687419569865, 8, "big" + ), + trace_id=int.to_bytes( + 67545097771067222548457157018666467027, + 16, + "big", + ), + parent_span_id=( + b"\000\000\000\000\000\00009" + ), + kind=( + OTLPSpan.SpanKind.SPAN_KIND_INTERNAL + ), + status=Status(code=0, message=""), + ) + ], + ), + ], + ), + ResourceSpans( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=2)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_spans=[ + InstrumentationLibrarySpans( + instrumentation_library=InstrumentationLibrary( + name="name", version="version" + ), + spans=[ + OTLPSpan( + # pylint: disable=no-member + name="b", + start_time_unix_nano=self.span2.start_time, + end_time_unix_nano=self.span2.end_time, + trace_state="a=b,c=d", + span_id=int.to_bytes( + 10217189687419569865, 8, "big" + ), + trace_id=int.to_bytes( + 67545097771067222548457157018666467027, + 16, + "big", + ), + parent_span_id=( + b"\000\000\000\000\000\00009" + ), + kind=( + OTLPSpan.SpanKind.SPAN_KIND_INTERNAL + ), + status=Status(code=0, message=""), + ) + ], + ) + ], + ), + ] + ) + + # pylint: disable=protected-access + self.assertEqual( + expected, + self.exporter._translate_data([self.span, self.span2, self.span3]), + ) + def _check_translated_status( self, translated: ExportTraceServiceRequest, From da16df07ccbab1a628f97eb80742a3cfe61cc58e Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 12 Jul 2021 07:56:13 -0700 Subject: [PATCH 0919/1517] require dependency for api/sdk to match major version (#1933) --- CHANGELOG.md | 3 +++ docs/examples/error_handler/error_handler_0/setup.cfg | 2 +- docs/examples/error_handler/error_handler_1/setup.cfg | 2 +- exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg | 4 ++-- exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg | 4 ++-- exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 4 ++-- exporter/opentelemetry-exporter-zipkin-json/setup.cfg | 4 ++-- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 4 ++-- opentelemetry-distro/setup.cfg | 2 +- propagator/opentelemetry-propagator-b3/setup.cfg | 2 +- propagator/opentelemetry-propagator-jaeger/setup.cfg | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- tests/util/setup.cfg | 4 ++-- 14 files changed, 23 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e0aba903..041d7aaf59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1915](https://github.com/open-telemetry/opentelemetry-python/pull/1915)) - Fix OTLP SpanExporter to distinguish spans based off Resource and InstrumentationInfo ([#1927](https://github.com/open-telemetry/opentelemetry-python/pull/1927)) +- Updating dependency for opentelemetry api/sdk packages to support major version instead of + pinning to specific versions. + ([#1933](https://github.com/open-telemetry/opentelemetry-python/pull/1933)) ### Fixed - Updated `opentelementry-opentracing-shim` `ScopeShim` to report exceptions in diff --git a/docs/examples/error_handler/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg index adcc0614b8..919ece78d6 100644 --- a/docs/examples/error_handler/error_handler_0/setup.cfg +++ b/docs/examples/error_handler/error_handler_0/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.4.0.dev0 + opentelemetry-sdk ~= 1.3 [options.packages.find] where = src diff --git a/docs/examples/error_handler/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg index b32809866c..2012fd81b1 100644 --- a/docs/examples/error_handler/error_handler_1/setup.cfg +++ b/docs/examples/error_handler/error_handler_1/setup.cfg @@ -36,7 +36,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-sdk == 1.4.0.dev0 + opentelemetry-sdk ~= 1.3 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index 8fbb0cdc8e..bd79242989 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 - opentelemetry-api == 1.4.0.dev0 - opentelemetry-sdk == 1.4.0.dev0 + opentelemetry-api ~= 1.3 + opentelemetry-sdk ~= 1.3 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index f1780aac64..945150d687 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api == 1.4.0.dev0 - opentelemetry-sdk == 1.4.0.dev0 + opentelemetry-api ~= 1.3 + opentelemetry-sdk ~= 1.3 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 7eb081d23c..03e82b70f3 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api == 1.4.0.dev0 - opentelemetry-sdk == 1.4.0.dev0 + opentelemetry-api ~= 1.3 + opentelemetry-sdk ~= 1.3 protobuf >= 3.13.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index c58cc708bd..a9b5c5b558 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 - opentelemetry-api == 1.4.0.dev0 - opentelemetry-sdk == 1.4.0.dev0 + opentelemetry-api ~= 1.3 + opentelemetry-sdk ~= 1.3 opentelemetry-proto == 1.4.0.dev0 backoff ~= 1.10.0 diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index 1a0aa03c38..81fcbfa261 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -42,8 +42,8 @@ package_dir= packages=find_namespace: install_requires = requests ~= 2.7 - opentelemetry-api == 1.4.0.dev0 - opentelemetry-sdk == 1.4.0.dev0 + opentelemetry-api ~= 1.3 + opentelemetry-sdk ~= 1.3 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index baabea2973..bee166fd53 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -43,8 +43,8 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 - opentelemetry-api == 1.4.0.dev0 - opentelemetry-sdk == 1.4.0.dev0 + opentelemetry-api ~= 1.3 + opentelemetry-sdk ~= 1.3 opentelemetry-exporter-zipkin-json == 1.4.0.dev0 [options.packages.find] diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 65abe4b947..74b3436f1d 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.4.0.dev0 + opentelemetry-api ~= 1.3 opentelemetry-instrumentation == 0.23.dev0 opentelemetry-sdk == 1.4.0.dev0 diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 83b7cc8158..1c879987fd 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.4.0.dev0 + opentelemetry-api ~= 1.3 deprecated >= 1.2.6 [options.extras_require] diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index 1c28fc7940..639a07d3d9 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.4.0.dev0 + opentelemetry-api ~= 1.3 [options.extras_require] test = diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index fc87a77a23..2b7db534bf 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: install_requires = Deprecated >= 1.2.6 opentracing ~= 2.0 - opentelemetry-api == 1.4.0.dev0 + opentelemetry-api ~= 1.3 [options.extras_require] test = diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index 3b4b47ba34..5bc0f69948 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -37,8 +37,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.4.0.dev0 - opentelemetry-sdk == 1.4.0.dev0 + opentelemetry-api ~= 1.3 + opentelemetry-sdk ~= 1.3 [options.extras_require] test = flask~=1.0 From 12fe4c9d56b9b43b9e6d12c1e7665737ad782bbe Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 12 Jul 2021 13:55:23 -0600 Subject: [PATCH 0920/1517] Do not check every string character (#1942) --- .../src/opentelemetry/attributes/__init__.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 5236627d03..e0b2a48ae2 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -17,7 +17,6 @@ import threading from collections import OrderedDict from collections.abc import MutableMapping -from types import MappingProxyType from typing import MutableSequence, Optional, Sequence from opentelemetry.util import types @@ -38,9 +37,10 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool: i.e. it MUST NOT contain values of different types. """ + if isinstance(value, _VALID_ATTR_VALUE_TYPES): + return True + if isinstance(value, Sequence): - if len(value) == 0: - return True sequence_first_valid_type = None for element in value: @@ -69,16 +69,15 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool: type(element).__name__, ) return False - - elif not isinstance(value, _VALID_ATTR_VALUE_TYPES): - _logger.warning( - "Invalid type %s for attribute value. Expected one of %s or a " - "sequence of those types", - type(value).__name__, - [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES], - ) - return False - return True + return True + + _logger.warning( + "Invalid type %s for attribute value. Expected one of %s or a " + "sequence of those types", + type(value).__name__, + [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES], + ) + return False def _filter_attributes(attributes: types.Attributes) -> None: From 0f6699d966677c86fc7ad658c60e6a497bdc3434 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Tue, 13 Jul 2021 08:37:47 -0700 Subject: [PATCH 0921/1517] Generate semconv constants update for OTel Spec 1.5.0 (#1946) --- CHANGELOG.md | 2 + .../semconv/resource/__init__.py | 126 ++++++++-- .../opentelemetry/semconv/trace/__init__.py | 235 +++++++++++++++--- scripts/semconv/generate.sh | 9 +- 4 files changed, 304 insertions(+), 68 deletions(-) mode change 100644 => 100755 scripts/semconv/generate.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 041d7aaf59..3a311bbdae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.3.0-0.22b0...HEAD) +- `opentelemetry-semantic-conventions` Generate semconv constants update for OTel Spec 1.5.0 + ([#1946](https://github.com/open-telemetry/opentelemetry-python/pull/1946)) ### Added - Dropped attributes/events/links count available exposed on ReadableSpans. diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py index b1e76a77b6..c8e27683ee 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py @@ -37,9 +37,9 @@ class ResourceAttributes: Note: Availability zones are called "zones" on Google Cloud. """ - CLOUD_INFRASTRUCTURE_SERVICE = "cloud.infrastructure_service" + CLOUD_PLATFORM = "cloud.platform" """ - The cloud infrastructure resource in use. + The cloud platform in use. Note: The prefix of the service SHOULD match the one specified in `cloud.provider`. """ @@ -68,6 +68,11 @@ class ResourceAttributes: The task definition family this task definition is a member of. """ + AWS_ECS_TASK_REVISION = "aws.ecs.task.revision" + """ + The revision for this task definition. + """ + AWS_EKS_CLUSTER_ARN = "aws.eks.cluster.arn" """ The ARN of an EKS cluster. @@ -126,25 +131,67 @@ class ResourceAttributes: Name of the [deployment environment](https://en.wikipedia.org/wiki/Deployment_environment) (aka deployment tier). """ + DEVICE_ID = "device.id" + """ + A unique identifier representing the device. + Note: The device identifier MUST only be defined using the values outlined below. This value is not an advertising identifier and MUST NOT be used as such. On iOS (Swift or Objective-C), this value MUST be equal to the [vendor identifier](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor). On Android (Java or Kotlin), this value MUST be equal to the Firebase Installation ID or a globally unique UUID which is persisted across sessions in your application. More information can be found [here](https://developer.android.com/training/articles/user-data-ids) on best practices and exact implementation details. Caution should be taken when storing personal data or anything which can identify a user. GDPR and data protection laws may apply, ensure you do your own due diligence. + """ + + DEVICE_MODEL_IDENTIFIER = "device.model.identifier" + """ + The model identifier for the device. + Note: It's recommended this value represents a machine readable version of the model identifier rather than the market or consumer-friendly name of the device. + """ + + DEVICE_MODEL_NAME = "device.model.name" + """ + The marketing name for the device model. + Note: It's recommended this value represents a human readable version of the device model rather than a machine readable alternative. + """ + FAAS_NAME = "faas.name" """ - The name of the function being executed. + The name of the single function that this runtime instance executes. + Note: This is the name of the function as configured/deployed on the FaaS platform and is usually different from the name of the callback function (which may be stored in the [`code.namespace`/`code.function`](../../trace/semantic_conventions/span-general.md#source-code-attributes) span attributes). """ FAAS_ID = "faas.id" """ - The unique ID of the function being executed. - Note: For example, in AWS Lambda this field corresponds to the [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) value, in GCP to the URI of the resource, and in Azure to the [FunctionDirectory](https://github.com/Azure/azure-functions-host/wiki/Retrieving-information-about-the-currently-running-function) field. + The unique ID of the single function that this runtime instance executes. + Note: Depending on the cloud provider, use: + +* **AWS Lambda:** The function [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html). +Take care not to use the "invoked ARN" directly but replace any +[alias suffix](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html) with the resolved function version, as the same runtime instance may be invokable with multiple +different aliases. +* **GCP:** The [URI of the resource](https://cloud.google.com/iam/docs/full-resource-names) +* **Azure:** The [Fully Qualified Resource ID](https://docs.microsoft.com/en-us/rest/api/resources/resources/get-by-id). + +On some providers, it may not be possible to determine the full ID at startup, +which is why this field cannot be made required. For example, on AWS the account ID +part of the ARN is not available without calling another AWS API +which may be deemed too slow for a short-running lambda function. +As an alternative, consider setting `faas.id` as a span attribute instead. """ FAAS_VERSION = "faas.version" """ - The version string of the function being executed as defined in [Version Attributes](../../resource/semantic_conventions/README.md#version-attributes). + The immutable version of the function being executed. + Note: Depending on the cloud provider and platform, use: + +* **AWS Lambda:** The [function version](https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html) + (an integer represented as a decimal string). +* **Google Cloud Run:** The [revision](https://cloud.google.com/run/docs/managing/revisions) + (i.e., the function name plus the revision suffix). +* **Google Cloud Functions:** The value of the + [`K_REVISION` environment variable](https://cloud.google.com/functions/docs/env-var#runtime_environment_variables_set_automatically). +* **Azure Functions:** Not applicable. Do not set this attribute. """ FAAS_INSTANCE = "faas.instance" """ - The execution environment ID as a string. + The execution environment ID as a string, that will be potentially reused for other invocations to the same function/function version. + Note: * **AWS Lambda:** Use the (full) log stream name. """ FAAS_MAX_MEMORY = "faas.max_memory" @@ -293,6 +340,16 @@ class ResourceAttributes: Human readable (not intended to be parsed) OS version information, like e.g. reported by `ver` or `lsb_release -a` commands. """ + OS_NAME = "os.name" + """ + Human readable operating system name. + """ + + OS_VERSION = "os.version" + """ + The version string of the operating system as defined in [Version Attributes](../../resource/semantic_conventions/README.md#version-attributes). + """ + PROCESS_PID = "process.pid" """ Process identifier (PID). @@ -386,6 +443,21 @@ class ResourceAttributes: The version string of the auto instrumentation agent, if used. """ + WEBENGINE_NAME = "webengine.name" + """ + The name of the web engine. + """ + + WEBENGINE_VERSION = "webengine.version" + """ + The version of the web engine. + """ + + WEBENGINE_DESCRIPTION = "webengine.description" + """ + Additional description of the web engine (e.g. detailed version and edition information). + """ + class CloudProviderValues(Enum): AWS = "aws" @@ -398,7 +470,7 @@ class CloudProviderValues(Enum): """Google Cloud Platform.""" -class CloudInfrastructureServiceValues(Enum): +class CloudPlatformValues(Enum): AWS_EC2 = "aws_ec2" """AWS Elastic Compute Cloud.""" @@ -411,13 +483,13 @@ class CloudInfrastructureServiceValues(Enum): AWS_LAMBDA = "aws_lambda" """AWS Lambda.""" - AWS_ELASTICBEANSTALK = "aws_elastic_beanstalk" + AWS_ELASTIC_BEANSTALK = "aws_elastic_beanstalk" """AWS Elastic Beanstalk.""" AZURE_VM = "azure_vm" """Azure Virtual Machines.""" - AZURE_CONTAINERINSTANCES = "azure_container_instances" + AZURE_CONTAINER_INSTANCES = "azure_container_instances" """Azure Container Instances.""" AZURE_AKS = "azure_aks" @@ -426,22 +498,22 @@ class CloudInfrastructureServiceValues(Enum): AZURE_FUNCTIONS = "azure_functions" """Azure Functions.""" - AZURE_APPSERVICE = "azure_app_service" + AZURE_APP_SERVICE = "azure_app_service" """Azure App Service.""" - GCP_COMPUTEENGINE = "gcp_compute_engine" + GCP_COMPUTE_ENGINE = "gcp_compute_engine" """Google Cloud Compute Engine (GCE).""" - GCP_CLOUDRUN = "gcp_cloud_run" + GCP_CLOUD_RUN = "gcp_cloud_run" """Google Cloud Run.""" - GCP_KUBERNETESENGINE = "gcp_kubernetes_engine" + GCP_KUBERNETES_ENGINE = "gcp_kubernetes_engine" """Google Cloud Kubernetes Engine (GKE).""" - GCP_CLOUDFUNCTIONS = "gcp_cloud_functions" + GCP_CLOUD_FUNCTIONS = "gcp_cloud_functions" """Google Cloud Functions (GCF).""" - GCP_APPENGINE = "gcp_app_engine" + GCP_APP_ENGINE = "gcp_app_engine" """Google Cloud App Engine (GAE).""" @@ -477,37 +549,37 @@ class HostArchValues(Enum): class OsTypeValues(Enum): - WINDOWS = "WINDOWS" + WINDOWS = "windows" """Microsoft Windows.""" - LINUX = "LINUX" + LINUX = "linux" """Linux.""" - DARWIN = "DARWIN" + DARWIN = "darwin" """Apple Darwin.""" - FREEBSD = "FREEBSD" + FREEBSD = "freebsd" """FreeBSD.""" - NETBSD = "NETBSD" + NETBSD = "netbsd" """NetBSD.""" - OPENBSD = "OPENBSD" + OPENBSD = "openbsd" """OpenBSD.""" - DRAGONFLYBSD = "DRAGONFLYBSD" + DRAGONFLYBSD = "dragonflybsd" """DragonFly BSD.""" - HPUX = "HPUX" + HPUX = "hpux" """HP-UX (Hewlett Packard Unix).""" - AIX = "AIX" + AIX = "aix" """AIX (Advanced Interactive eXecutive).""" - SOLARIS = "SOLARIS" + SOLARIS = "solaris" """Oracle Solaris.""" - ZOS = "ZOS" + Z_OS = "z_os" """IBM z/OS.""" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py index 02b8768f16..36184ca955 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py @@ -16,6 +16,12 @@ class SpanAttributes: + AWS_LAMBDA_INVOKED_ARN = "aws.lambda.invoked_arn" + """ + The full invoked ARN as provided on the `Context` passed to the function (`Lambda-Runtime-Invoked-Function-Arn` header on the `/runtime/invocation/next` applicable). + Note: This may be different from `faas.id` if an alias is involved. + """ + DB_SYSTEM = "db.system" """ An identifier for the database management system (DBMS) product being used. See below for a list of well-known identifiers. @@ -450,11 +456,155 @@ class SpanAttributes: The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. """ + RPC_SYSTEM = "rpc.system" + """ + The value `aws-api`. + """ + + RPC_SERVICE = "rpc.service" + """ + The name of the service to which a request is made, as returned by the AWS SDK. + Note: This is the logical name of the service from the RPC interface perspective, which can be different from the name of any implementing class. The `code.namespace` attribute may be used to store the latter (despite the attribute name, it may include a class name; e.g., class with method actually executing the call on the server side, RPC client stub class on the client side). + """ + + RPC_METHOD = "rpc.method" + """ + The name of the operation corresponding to the request, as returned by the AWS SDK. + Note: This is the logical name of the method from the RPC interface perspective, which can be different from the name of any implementing method/function. The `code.function` attribute may be used to store the latter (e.g., method actually executing the call on the server side, RPC client stub method on the client side). + """ + + AWS_DYNAMODB_TABLE_NAMES = "aws.dynamodb.table_names" + """ + The keys in the `RequestItems` object field. + """ + + AWS_DYNAMODB_CONSUMED_CAPACITY = "aws.dynamodb.consumed_capacity" + """ + The JSON-serialized value of each item in the `ConsumedCapacity` response field. + """ + + AWS_DYNAMODB_ITEM_COLLECTION_METRICS = ( + "aws.dynamodb.item_collection_metrics" + ) + """ + The JSON-serialized value of the `ItemCollectionMetrics` response field. + """ + + AWS_DYNAMODB_PROVISIONED_READ_CAPACITY = ( + "aws.dynamodb.provisioned_read_capacity" + ) + """ + The value of the `ProvisionedThroughput.ReadCapacityUnits` request parameter. + """ + + AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY = ( + "aws.dynamodb.provisioned_write_capacity" + ) + """ + The value of the `ProvisionedThroughput.WriteCapacityUnits` request parameter. + """ + + AWS_DYNAMODB_CONSISTENT_READ = "aws.dynamodb.consistent_read" + """ + The value of the `ConsistentRead` request parameter. + """ + + AWS_DYNAMODB_PROJECTION = "aws.dynamodb.projection" + """ + The value of the `ProjectionExpression` request parameter. + """ + + AWS_DYNAMODB_LIMIT = "aws.dynamodb.limit" + """ + The value of the `Limit` request parameter. + """ + + AWS_DYNAMODB_ATTRIBUTES_TO_GET = "aws.dynamodb.attributes_to_get" + """ + The value of the `AttributesToGet` request parameter. + """ + + AWS_DYNAMODB_INDEX_NAME = "aws.dynamodb.index_name" + """ + The value of the `IndexName` request parameter. + """ + + AWS_DYNAMODB_SELECT = "aws.dynamodb.select" + """ + The value of the `Select` request parameter. + """ + + AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES = ( + "aws.dynamodb.global_secondary_indexes" + ) + """ + The JSON-serialized value of each item of the `GlobalSecondaryIndexes` request field. + """ + + AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES = ( + "aws.dynamodb.local_secondary_indexes" + ) + """ + The JSON-serialized value of each item of the `LocalSecondaryIndexes` request field. + """ + + AWS_DYNAMODB_EXCLUSIVE_START_TABLE = "aws.dynamodb.exclusive_start_table" + """ + The value of the `ExclusiveStartTableName` request parameter. + """ + + AWS_DYNAMODB_TABLE_COUNT = "aws.dynamodb.table_count" + """ + The the number of items in the `TableNames` response parameter. + """ + + AWS_DYNAMODB_SCAN_FORWARD = "aws.dynamodb.scan_forward" + """ + The value of the `ScanIndexForward` request parameter. + """ + + AWS_DYNAMODB_SEGMENT = "aws.dynamodb.segment" + """ + The value of the `Segment` request parameter. + """ + + AWS_DYNAMODB_TOTAL_SEGMENTS = "aws.dynamodb.total_segments" + """ + The value of the `TotalSegments` request parameter. + """ + + AWS_DYNAMODB_COUNT = "aws.dynamodb.count" + """ + The value of the `Count` response parameter. + """ + + AWS_DYNAMODB_SCANNED_COUNT = "aws.dynamodb.scanned_count" + """ + The value of the `ScannedCount` response parameter. + """ + + AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS = "aws.dynamodb.attribute_definitions" + """ + The JSON-serialized value of each item in the `AttributeDefinitions` request field. + """ + + AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES = ( + "aws.dynamodb.global_secondary_index_updates" + ) + """ + The JSON-serialized value of each item in the the `GlobalSecondaryIndexUpdates` request field. + """ + MESSAGING_OPERATION = "messaging.operation" """ A string identifying the kind of message consumption as defined in the [Operation names](#operation-names) section above. If the operation is "send", this attribute MUST NOT be set, since the operation can be inferred from the span kind in that case. """ + MESSAGING_RABBITMQ_ROUTING_KEY = "messaging.rabbitmq.routing_key" + """ + RabbitMQ message routing key. + """ + MESSAGING_KAFKA_MESSAGE_KEY = "messaging.kafka.message_key" """ Message keys in Kafka are used for grouping alike messages to ensure they're processed on the same partition. They differ from `messaging.message_id` in that they're not unique. If the key is `null`, the attribute MUST NOT be set. @@ -481,24 +631,29 @@ class SpanAttributes: A boolean that is true if the message is a tombstone. """ - RPC_SYSTEM = "rpc.system" + RPC_GRPC_STATUS_CODE = "rpc.grpc.status_code" """ - A string identifying the remoting system. + The [numeric status code](https://github.com/grpc/grpc/blob/v1.33.2/doc/statuscodes.md) of the gRPC request. """ - RPC_SERVICE = "rpc.service" + RPC_JSONRPC_VERSION = "rpc.jsonrpc.version" """ - The full name of the service being called, including its package name, if applicable. + Protocol version as in `jsonrpc` property of request/response. Since JSON-RPC 1.0 does not specify this, the value can be omitted. """ - RPC_METHOD = "rpc.method" + RPC_JSONRPC_REQUEST_ID = "rpc.jsonrpc.request_id" """ - The name of the method being called, must be equal to the $method part in the span name. + `id` property of request or response. Since protocol allows id to be int, string, `null` or missing (for notifications), value is expected to be cast to string for simplicity. Use empty string in case of `null` value. Omit entirely if this is a notification. """ - RPC_GRPC_STATUS_CODE = "rpc.grpc.status_code" + RPC_JSONRPC_ERROR_CODE = "rpc.jsonrpc.error_code" """ - The [numeric status code](https://github.com/grpc/grpc/blob/v1.33.2/doc/statuscodes.md) of the gRPC request. + `error.code` property of response if it is an error response. + """ + + RPC_JSONRPC_ERROR_MESSAGE = "rpc.jsonrpc.error_message" + """ + `error.message` property of response if it is an error response. """ @@ -638,18 +793,24 @@ class DbSystemValues(Enum): ELASTICSEARCH = "elasticsearch" """Elasticsearch.""" + MEMCACHED = "memcached" + """Memcached.""" + + COCKROACHDB = "cockroachdb" + """CockroachDB.""" + class NetTransportValues(Enum): - IP_TCP = "IP.TCP" - """IP.TCP.""" + IP_TCP = "ip_tcp" + """ip_tcp.""" - IP_UDP = "IP.UDP" - """IP.UDP.""" + IP_UDP = "ip_udp" + """ip_udp.""" - IP = "IP" + IP = "ip" """Another IP-based protocol.""" - UNIX = "Unix" + UNIX = "unix" """Unix Domain socket. See below.""" PIPE = "pipe" @@ -663,38 +824,38 @@ class NetTransportValues(Enum): class DbCassandraConsistencyLevelValues(Enum): - ALL = "ALL" - """ALL.""" + ALL = "all" + """all.""" - EACH_QUORUM = "EACH_QUORUM" - """EACH_QUORUM.""" + EACH_QUORUM = "each_quorum" + """each_quorum.""" - QUORUM = "QUORUM" - """QUORUM.""" + QUORUM = "quorum" + """quorum.""" - LOCAL_QUORUM = "LOCAL_QUORUM" - """LOCAL_QUORUM.""" + LOCAL_QUORUM = "local_quorum" + """local_quorum.""" - ONE = "ONE" - """ONE.""" + ONE = "one" + """one.""" - TWO = "TWO" - """TWO.""" + TWO = "two" + """two.""" - THREE = "THREE" - """THREE.""" + THREE = "three" + """three.""" - LOCAL_ONE = "LOCAL_ONE" - """LOCAL_ONE.""" + LOCAL_ONE = "local_one" + """local_one.""" - ANY = "ANY" - """ANY.""" + ANY = "any" + """any.""" - SERIAL = "SERIAL" - """SERIAL.""" + SERIAL = "serial" + """serial.""" - LOCAL_SERIAL = "LOCAL_SERIAL" - """LOCAL_SERIAL.""" + LOCAL_SERIAL = "local_serial" + """local_serial.""" class FaasTriggerValues(Enum): @@ -755,7 +916,7 @@ class FaasInvokedProviderValues(Enum): """Amazon Web Services.""" AZURE = "azure" - """Amazon Web Services.""" + """Microsoft Azure.""" GCP = "gcp" """Google Cloud Platform.""" diff --git a/scripts/semconv/generate.sh b/scripts/semconv/generate.sh old mode 100644 new mode 100755 index 62c28ede5c..ccf75a6364 --- a/scripts/semconv/generate.sh +++ b/scripts/semconv/generate.sh @@ -4,7 +4,8 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR="${SCRIPT_DIR}/../../" # freeze the spec version to make SemanticAttributes generation reproducible -SPEC_VERSION=v1.1.0 +SPEC_VERSION=v1.5.0 +OTEL_SEMCONV_GEN_IMG_VERSION=0.4.1 cd ${SCRIPT_DIR} @@ -22,7 +23,7 @@ docker run --rm \ -v ${SCRIPT_DIR}/opentelemetry-specification/semantic_conventions/trace:/source \ -v ${SCRIPT_DIR}/templates:/templates \ -v ${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/:/output \ - otel/semconvgen:0.2.1 \ + otel/semconvgen:$OTEL_SEMCONV_GEN_IMG_VERSION \ -f /source code \ --template /templates/semantic_attributes.j2 \ --output /output/__init__.py \ @@ -31,8 +32,8 @@ docker run --rm \ docker run --rm \ -v ${SCRIPT_DIR}/opentelemetry-specification/semantic_conventions/resource:/source \ -v ${SCRIPT_DIR}/templates:/templates \ - -v ${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resources/:/output \ - otel/semconvgen:0.2.1 \ + -v ${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/:/output \ + otel/semconvgen:$OTEL_SEMCONV_GEN_IMG_VERSION \ -f /source code \ --template /templates/semantic_attributes.j2 \ --output /output/__init__.py \ From eed804e28a64f4829ede1bfeede1772713941d67 Mon Sep 17 00:00:00 2001 From: kuerbis-martin <60111015+kuerbis-martin@users.noreply.github.com> Date: Tue, 13 Jul 2021 13:59:27 -0700 Subject: [PATCH 0922/1517] Fix formatting errors in Django instrumentation example docs (#1940) --- docs/examples/django/README.rst | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 656a07b6da..09ddd2638b 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -105,16 +105,16 @@ Instrumentation package. Disabling Django Instrumentation -------------------------------- -Django's instrumentation can be disabled by setting the following environment variable. +Django's instrumentation can be disabled by setting the following environment variable: -#. ``export OTEL_PYTHON_DJANGO_INSTRUMENT=False`` +``export OTEL_PYTHON_DJANGO_INSTRUMENT=False`` Auto Instrumentation -------------------- This same example can be run using auto instrumentation. Comment out the call to ``DjangoInstrumento().instrument()`` in ``main``, then Run the django app -with ``opentelemetry-instrumentation python manage.py runserver --noreload``. +with ``opentelemetry-instrument python manage.py runserver --noreload``. Repeat the steps with the client, the result should be the same. Usage with Auto Instrumentation and uWSGI @@ -123,15 +123,12 @@ Usage with Auto Instrumentation and uWSGI uWSGI and Django can be used together with auto instrumentation. To do so, first install uWSGI in the previous virtual environment: -``` -pip install uwsgi -``` +``pip install uwsgi`` + Once that is done, run the server with ``uwsgi`` from the directory that contains ``instrumentation_example``: -``` -opentelemetry-instrument uwsgi --http :8000 --module instrumentation_example.wsgi -``` +``opentelemetry-instrument uwsgi --http :8000 --module instrumentation_example.wsgi`` This should start one uWSGI worker in your console. Open up a browser and point it to ``localhost:8000``. This request should display a span exported in the From fdf4b3e3b02332e1172546f565586afadbdfc4fc Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 14 Jul 2021 09:58:07 -0600 Subject: [PATCH 0923/1517] Use basename to get file name (#1955) Fixes #1954 --- scripts/eachdist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/eachdist.py b/scripts/eachdist.py index db1ce77d85..f9c27eb835 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -11,6 +11,7 @@ from datetime import datetime from inspect import cleandoc from itertools import chain +from os.path import basename from pathlib import Path, PurePath DEFAULT_ALLSEP = " " @@ -628,11 +629,10 @@ def update_version_files(targets, version, packages): def update_dependencies(targets, version, packages): print("updating dependencies") for pkg in packages: - package_name = pkg.split("/")[-1] update_files( targets, "setup.cfg", - r"({}.*)==(.*)".format(package_name), + r"({}.*)==(.*)".format(basename(pkg)), r"\1== " + version, ) From bb41b81093d4010dadb233f146e8ba04973ec5a3 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 14 Jul 2021 14:38:32 -0600 Subject: [PATCH 0924/1517] updating changelogs and version to 1.4.0-0.23b0 (#1956) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 9 ++++++--- eachdist.ini | 4 ++-- .../opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 6 +++--- opentelemetry-distro/src/opentelemetry/distro/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 6 +++--- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 29 files changed, 41 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e2acbe3a6..04004e76e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: c100b21fa40727709bb31cf4e2b67b737a09ea2c + CONTRIB_REPO_SHA: 7e9964d788c2f91697a682d5f0d01bcfeedf9524 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a311bbdae..bd7c46fa7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.3.0-0.22b0...HEAD) -- `opentelemetry-semantic-conventions` Generate semconv constants update for OTel Spec 1.5.0 - ([#1946](https://github.com/open-telemetry/opentelemetry-python/pull/1946)) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.4.0-0.23b0...HEAD) + +## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-14 + ### Added - Dropped attributes/events/links count available exposed on ReadableSpans. @@ -15,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) ### Changed +- `opentelemetry-semantic-conventions` Generate semconv constants update for OTel Spec 1.5.0 + ([#1946](https://github.com/open-telemetry/opentelemetry-python/pull/1946)) - Updated `opentelemetry-opencensus-exporter` to use `service_name` of spans instead of resource ([#1897](https://github.com/open-telemetry/opentelemetry-python/pull/1897)) - Added descriptions to the env variables mentioned in the opentelemetry-specification diff --git a/eachdist.ini b/eachdist.ini index ae202e7232..f574d5f830 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -13,7 +13,7 @@ sortfirst= exporter/* [stable] -version=1.4.0.dev0 +version=1.4.0 packages= opentelemetry-sdk @@ -31,7 +31,7 @@ packages= opentelemetry-api [prerelease] -version=0.23.dev0 +version=0.23b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index c173c28d8d..f8cc727b10 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index c173c28d8d..f8cc727b10 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index faa9d40a3a..3dc9b0c5ee 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,8 +39,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.4.0.dev0 - opentelemetry-exporter-jaeger-thrift == 1.4.0.dev0 + opentelemetry-exporter-jaeger-proto-grpc == 1.4.0 + opentelemetry-exporter-jaeger-thrift == 1.4.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index c173c28d8d..f8cc727b10 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index c829b95757..7318f8dca8 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23.dev0" +__version__ = "0.23b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index a9b5c5b558..147b4869fd 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.4.0.dev0 + opentelemetry-proto == 1.4.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 48c4fb840e..e0982da0fa 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index d4a891e173..4cd76408ec 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -38,4 +38,4 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.4.0.dev0 + opentelemetry-exporter-otlp-proto-grpc == 1.4.0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 48c4fb840e..e0982da0fa 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 48c4fb840e..e0982da0fa 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index bee166fd53..507693ef4f 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.4.0.dev0 + opentelemetry-exporter-zipkin-json == 1.4.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 48c4fb840e..e0982da0fa 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 0b1a832ed7..529a0e5bc0 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.4.0.dev0 - opentelemetry-exporter-zipkin-proto-http == 1.4.0.dev0 + opentelemetry-exporter-zipkin-json == 1.4.0 + opentelemetry-exporter-zipkin-proto-http == 1.4.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 48c4fb840e..e0982da0fa 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 48c4fb840e..e0982da0fa 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 74b3436f1d..e616635e7f 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -42,8 +42,8 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api ~= 1.3 - opentelemetry-instrumentation == 0.23.dev0 - opentelemetry-sdk == 1.4.0.dev0 + opentelemetry-instrumentation == 0.23b0 + opentelemetry-sdk == 1.4.0 [options.packages.find] where = src @@ -57,4 +57,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.4.0.dev0 + opentelemetry-exporter-otlp == 1.4.0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index c829b95757..7318f8dca8 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23.dev0" +__version__ = "0.23b0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 48c4fb840e..e0982da0fa 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 3df5e31ed4..62c7155f1a 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,9 +42,9 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.4.0.dev0 - opentelemetry-semantic-conventions == 0.23.dev0 - opentelemetry-instrumentation == 0.23.dev0 + opentelemetry-api == 1.4.0 + opentelemetry-semantic-conventions == 0.23b0 + opentelemetry-instrumentation == 0.23b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 48c4fb840e..e0982da0fa 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index c829b95757..7318f8dca8 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23.dev0" +__version__ = "0.23b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 48c4fb840e..e0982da0fa 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 48c4fb840e..e0982da0fa 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 2b7db534bf..54d50f7b73 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -46,7 +46,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test == 0.23.dev0 + opentelemetry-test == 0.23b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index c829b95757..7318f8dca8 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23.dev0" +__version__ = "0.23b0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 12f7794562..08a78866dc 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.23.dev0" +__version__ = "0.23b0" From dfd298064b20ed83622201abde1a738e658509a2 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 16 Jul 2021 10:30:48 -0600 Subject: [PATCH 0925/1517] Revert "updating changelogs and version to 1.4.0-0.23b0 (#1956)" (#1962) This reverts commit bb41b81093d4010dadb233f146e8ba04973ec5a3. --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 9 +++------ eachdist.ini | 4 ++-- .../opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 6 +++--- opentelemetry-distro/src/opentelemetry/distro/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 6 +++--- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 29 files changed, 38 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 04004e76e2..7e2acbe3a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 7e9964d788c2f91697a682d5f0d01bcfeedf9524 + CONTRIB_REPO_SHA: c100b21fa40727709bb31cf4e2b67b737a09ea2c jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index bd7c46fa7a..3a311bbdae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.4.0-0.23b0...HEAD) - -## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-14 - +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.3.0-0.22b0...HEAD) +- `opentelemetry-semantic-conventions` Generate semconv constants update for OTel Spec 1.5.0 + ([#1946](https://github.com/open-telemetry/opentelemetry-python/pull/1946)) ### Added - Dropped attributes/events/links count available exposed on ReadableSpans. @@ -16,8 +15,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) ### Changed -- `opentelemetry-semantic-conventions` Generate semconv constants update for OTel Spec 1.5.0 - ([#1946](https://github.com/open-telemetry/opentelemetry-python/pull/1946)) - Updated `opentelemetry-opencensus-exporter` to use `service_name` of spans instead of resource ([#1897](https://github.com/open-telemetry/opentelemetry-python/pull/1897)) - Added descriptions to the env variables mentioned in the opentelemetry-specification diff --git a/eachdist.ini b/eachdist.ini index f574d5f830..ae202e7232 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -13,7 +13,7 @@ sortfirst= exporter/* [stable] -version=1.4.0 +version=1.4.0.dev0 packages= opentelemetry-sdk @@ -31,7 +31,7 @@ packages= opentelemetry-api [prerelease] -version=0.23b0 +version=0.23.dev0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index f8cc727b10..c173c28d8d 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index f8cc727b10..c173c28d8d 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 3dc9b0c5ee..faa9d40a3a 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,8 +39,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.4.0 - opentelemetry-exporter-jaeger-thrift == 1.4.0 + opentelemetry-exporter-jaeger-proto-grpc == 1.4.0.dev0 + opentelemetry-exporter-jaeger-thrift == 1.4.0.dev0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index f8cc727b10..c173c28d8d 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 7318f8dca8..c829b95757 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23b0" +__version__ = "0.23.dev0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 147b4869fd..a9b5c5b558 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.4.0 + opentelemetry-proto == 1.4.0.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index e0982da0fa..48c4fb840e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 4cd76408ec..d4a891e173 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -38,4 +38,4 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.4.0 + opentelemetry-exporter-otlp-proto-grpc == 1.4.0.dev0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index e0982da0fa..48c4fb840e 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index e0982da0fa..48c4fb840e 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 507693ef4f..bee166fd53 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.4.0 + opentelemetry-exporter-zipkin-json == 1.4.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index e0982da0fa..48c4fb840e 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.4.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 529a0e5bc0..0b1a832ed7 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.4.0 - opentelemetry-exporter-zipkin-proto-http == 1.4.0 + opentelemetry-exporter-zipkin-json == 1.4.0.dev0 + opentelemetry-exporter-zipkin-proto-http == 1.4.0.dev0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index e0982da0fa..48c4fb840e 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.4.0.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index e0982da0fa..48c4fb840e 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.4.0.dev0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index e616635e7f..74b3436f1d 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -42,8 +42,8 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api ~= 1.3 - opentelemetry-instrumentation == 0.23b0 - opentelemetry-sdk == 1.4.0 + opentelemetry-instrumentation == 0.23.dev0 + opentelemetry-sdk == 1.4.0.dev0 [options.packages.find] where = src @@ -57,4 +57,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.4.0 + opentelemetry-exporter-otlp == 1.4.0.dev0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index 7318f8dca8..c829b95757 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23b0" +__version__ = "0.23.dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index e0982da0fa..48c4fb840e 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.4.0.dev0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 62c7155f1a..3df5e31ed4 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,9 +42,9 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.4.0 - opentelemetry-semantic-conventions == 0.23b0 - opentelemetry-instrumentation == 0.23b0 + opentelemetry-api == 1.4.0.dev0 + opentelemetry-semantic-conventions == 0.23.dev0 + opentelemetry-instrumentation == 0.23.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index e0982da0fa..48c4fb840e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.4.0.dev0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 7318f8dca8..c829b95757 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23b0" +__version__ = "0.23.dev0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index e0982da0fa..48c4fb840e 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.4.0.dev0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index e0982da0fa..48c4fb840e 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.4.0.dev0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 54d50f7b73..2b7db534bf 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -46,7 +46,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test == 0.23b0 + opentelemetry-test == 0.23.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 7318f8dca8..c829b95757 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23b0" +__version__ = "0.23.dev0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 08a78866dc..12f7794562 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.23b0" +__version__ = "0.23.dev0" From e78c9c460c1f1f062032a31f46245d7b1f9cdcd0 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 16 Jul 2021 12:13:14 -0600 Subject: [PATCH 0926/1517] Add opentelemetry-instrumentation (#1959) --- .github/pull_request_template.md | 1 + CHANGELOG.md | 2 + docs-requirements.txt | 3 +- docs/conf.py | 7 +- eachdist.ini | 1 + opentelemetry-instrumentation/MANIFEST.in | 7 + opentelemetry-instrumentation/README.rst | 120 +++++++++++++ opentelemetry-instrumentation/setup.cfg | 56 ++++++ opentelemetry-instrumentation/setup.py | 29 ++++ .../auto_instrumentation/__init__.py | 109 ++++++++++++ .../auto_instrumentation/sitecustomize.py | 135 +++++++++++++++ .../instrumentation/bootstrap.py | 159 ++++++++++++++++++ .../instrumentation/bootstrap_gen.py | 138 +++++++++++++++ .../instrumentation/configurator.py | 53 ++++++ .../instrumentation/dependencies.py | 64 +++++++ .../opentelemetry/instrumentation/distro.py | 71 ++++++++ .../instrumentation/instrumentor.py | 132 +++++++++++++++ .../instrumentation/propagators.py | 128 ++++++++++++++ .../opentelemetry/instrumentation/py.typed | 0 .../opentelemetry/instrumentation/utils.py | 68 ++++++++ .../opentelemetry/instrumentation/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/test_bootstrap.py | 91 ++++++++++ .../tests/test_dependencies.py | 79 +++++++++ .../tests/test_distro.py | 58 +++++++ .../tests/test_instrumentor.py | 50 ++++++ .../tests/test_propagators.py | 80 +++++++++ .../tests/test_run.py | 117 +++++++++++++ .../tests/test_utils.py | 57 +++++++ scripts/build.sh | 2 +- tox.ini | 30 ++-- 31 files changed, 1847 insertions(+), 15 deletions(-) create mode 100644 opentelemetry-instrumentation/MANIFEST.in create mode 100644 opentelemetry-instrumentation/README.rst create mode 100644 opentelemetry-instrumentation/setup.cfg create mode 100644 opentelemetry-instrumentation/setup.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py create mode 100644 opentelemetry-instrumentation/tests/__init__.py create mode 100644 opentelemetry-instrumentation/tests/test_bootstrap.py create mode 100644 opentelemetry-instrumentation/tests/test_dependencies.py create mode 100644 opentelemetry-instrumentation/tests/test_distro.py create mode 100644 opentelemetry-instrumentation/tests/test_instrumentor.py create mode 100644 opentelemetry-instrumentation/tests/test_propagators.py create mode 100644 opentelemetry-instrumentation/tests/test_run.py create mode 100644 opentelemetry-instrumentation/tests/test_utils.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d564f69c67..48d109f5ef 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -23,6 +23,7 @@ Please describe the tests that you ran to verify your changes. Provide instructi Answer the following question based on these examples of changes that would require a Contrib Repo Change: - [The OTel specification](https://github.com/open-telemetry/opentelemetry-specification) has changed which prompted this PR to update the method interfaces of `opentelemetry-api/` or `opentelemetry-sdk/` +- The method interfaces of `opentelemetry-instrumentation/` have changed - The method interfaces of `test/util` have changed - Scripts in `scripts/` that were copied over to the Contrib repo have changed - Configuration files that were copied over to the Contrib repo have changed (when consistency between repositories is applicable) such as in diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a311bbdae..b531ebb4d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1946](https://github.com/open-telemetry/opentelemetry-python/pull/1946)) ### Added +- Moved `opentelemetry-instrumentation` to core repository. + ([#1959](https://github.com/open-telemetry/opentelemetry-python/pull/1959)) - Dropped attributes/events/links count available exposed on ReadableSpans. ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) - Added dropped count to otlp, jaeger and zipkin exporters. diff --git a/docs-requirements.txt b/docs-requirements.txt index f9a7776cb0..b670676fbb 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -8,8 +8,9 @@ sphinx-jekyll-builder # doesn't work for pkg_resources. ./opentelemetry-api ./opentelemetry-semantic-conventions -./opentelemetry-python-contrib/opentelemetry-instrumentation +./opentelemetry-instrumentation ./opentelemetry-sdk +./opentelemetry-instrumentation # Required by instrumentation and exporter packages ddtrace>=0.34.0 diff --git a/docs/conf.py b/docs/conf.py index c1476acf17..6559298b8e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,6 +23,11 @@ settings.configure() + +source_dirs = [ + os.path.abspath("../opentelemetry-instrumentation/src/"), +] + exp = "../exporter" exp_dirs = [ os.path.abspath("/".join(["../exporter", f, "src"])) @@ -37,7 +42,7 @@ if isdir(join(shim, f)) ] -sys.path[:0] = exp_dirs + shim_dirs +sys.path[:0] = source_dirs + exp_dirs + shim_dirs # -- Project information ----------------------------------------------------- diff --git a/eachdist.ini b/eachdist.ini index ae202e7232..23afcb1abc 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -7,6 +7,7 @@ ignore= sortfirst= opentelemetry-api opentelemetry-sdk + opentelemetry-instrumentation opentelemetry-proto opentelemetry-distro tests/util diff --git a/opentelemetry-instrumentation/MANIFEST.in b/opentelemetry-instrumentation/MANIFEST.in new file mode 100644 index 0000000000..191b7d1959 --- /dev/null +++ b/opentelemetry-instrumentation/MANIFEST.in @@ -0,0 +1,7 @@ +prune tests +graft src +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include MANIFEST.in +include README.rst diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst new file mode 100644 index 0000000000..18b31f428f --- /dev/null +++ b/opentelemetry-instrumentation/README.rst @@ -0,0 +1,120 @@ +OpenTelemetry Instrumentation +============================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation.svg + :target: https://pypi.org/project/opentelemetry-instrumentation/ + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation + + +This package provides a couple of commands that help automatically instruments a program: + +.. note:: + You need to install a distro package to get auto instrumentation working. The ``opentelemetry-distro`` + package contains the default distro and automatically configures some of the common options for users. + For more info about ``opentelemetry-distro`` check `here `__ + :: + + pip install opentelemetry-distro[otlp] + + +opentelemetry-bootstrap +----------------------- + +:: + + opentelemetry-bootstrap --action=install|requirements + +This commands inspects the active Python site-packages and figures out which +instrumentation packages the user might want to install. By default it prints out +a list of the suggested instrumentation packages which can be added to a requirements.txt +file. It also supports installing the suggested packages when run with :code:`--action=install` +flag. + + +opentelemetry-instrument +------------------------ + +:: + + opentelemetry-instrument python program.py + +The instrument command will try to automatically detect packages used by your python program +and when possible, apply automatic tracing instrumentation on them. This means your program +will get automatic distributed tracing for free without having to make any code changes +at all. This will also configure a global tracer and tracing exporter without you having to +make any code changes. By default, the instrument command will use the OTLP exporter but +this can be overriden when needed. + +The command supports the following configuration options as CLI arguments and environment vars: + + +* ``--trace-exporter`` or ``OTEL_TRACE_EXPORTER`` + +Used to specify which trace exporter to use. Can be set to one or more of the well-known exporter +names (see below). + + - Defaults to `otlp`. + - Can be set to `none` to disable automatic tracer initialization. + +You can pass multiple values to configure multiple exporters e.g, ``zipkin,prometheus`` + +Well known trace exporter names: + + - jaeger + - opencensus + - otlp + - otlp_proto_grpc_span + - zipkin + +``otlp`` is an alias for ``otlp_proto_grpc_span``. + +* ``--id-generator`` or ``OTEL_PYTHON_ID_GENERATOR`` + +Used to specify which IDs Generator to use for the global Tracer Provider. By default, it +will use the random IDs generator. + +The code in ``program.py`` needs to use one of the packages for which there is +an OpenTelemetry integration. For a list of the available integrations please +check `here `_ + +* ``OTEL_PYTHON_DISABLED_INSTRUMENTATIONS`` + +If set by the user, opentelemetry-instrument will read this environment variable to disable specific instrumentations. +e.g OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "requests,django" + + +Examples +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + opentelemetry-instrument --trace-exporter otlp flask run --port=3000 + +The above command will pass ``--trace-exporter otlp`` to the instrument command and ``--port=3000`` to ``flask run``. + +:: + + opentelemetry-instrument --trace-exporter zipkin,otlp celery -A tasks worker --loglevel=info + +The above command will configure global trace provider, attach zipkin and otlp exporters to it and then +start celery with the rest of the arguments. + +:: + + opentelemetry-instrument --ids-generator random flask run --port=3000 + +The above command will configure the global trace provider to use the Random IDs Generator, and then +pass ``--port=3000`` to ``flask run``. + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg new file mode 100644 index 0000000000..a00673cca8 --- /dev/null +++ b/opentelemetry-instrumentation/setup.cfg @@ -0,0 +1,56 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-instrumentation +description = Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opentelemetry-instrumentation +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.6 +package_dir= + =src +packages=find_namespace: +zip_safe = False +include_package_data = True +install_requires = + opentelemetry-api == 1.4.0.dev0 + wrapt >= 1.0.0, < 2.0.0 + +[options.packages.find] +where = src + +[options.entry_points] +console_scripts = + opentelemetry-instrument = opentelemetry.instrumentation.auto_instrumentation:run + opentelemetry-bootstrap = opentelemetry.instrumentation.bootstrap:run + +[options.extras_require] +test = diff --git a/opentelemetry-instrumentation/setup.py b/opentelemetry-instrumentation/setup.py new file mode 100644 index 0000000000..d4f84f738b --- /dev/null +++ b/opentelemetry-instrumentation/setup.py @@ -0,0 +1,29 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "instrumentation", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup( + version=PACKAGE_INFO["__version__"], +) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py new file mode 100644 index 0000000000..45a1f2a221 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +from logging import getLogger +from os import environ, execl, getcwd +from os.path import abspath, dirname, pathsep +from shutil import which + +from opentelemetry.environment_variables import ( + OTEL_PYTHON_ID_GENERATOR, + OTEL_TRACES_EXPORTER, +) + +logger = getLogger(__file__) + + +def parse_args(): + parser = argparse.ArgumentParser( + description=""" + opentelemetry-instrument automatically instruments a Python + program and it's dependencies and then runs the program. + """ + ) + + parser.add_argument( + "--trace-exporter", + required=False, + help=""" + Uses the specified exporter to export spans. + Accepts multiple exporters as comma separated values. + + Examples: + + --trace-exporter=jaeger + """, + ) + + parser.add_argument( + "--id-generator", + required=False, + help=""" + The IDs Generator to be used with the Tracer Provider. + + Examples: + + --id-generator=random + """, + ) + + parser.add_argument("command", help="Your Python application.") + parser.add_argument( + "command_args", + help="Arguments for your application.", + nargs=argparse.REMAINDER, + ) + return parser.parse_args() + + +def load_config_from_cli_args(args): + if args.trace_exporter: + environ[OTEL_TRACES_EXPORTER] = args.trace_exporter + if args.id_generator: + environ[OTEL_PYTHON_ID_GENERATOR] = args.id_generator + + +def run() -> None: + args = parse_args() + load_config_from_cli_args(args) + + python_path = environ.get("PYTHONPATH") + + if not python_path: + python_path = [] + + else: + python_path = python_path.split(pathsep) + + cwd_path = getcwd() + + # This is being added to support applications that are being run from their + # own executable, like Django. + # FIXME investigate if there is another way to achieve this + if cwd_path not in python_path: + python_path.insert(0, cwd_path) + + filedir_path = dirname(abspath(__file__)) + + python_path = [path for path in python_path if path != filedir_path] + + python_path.insert(0, filedir_path) + + environ["PYTHONPATH"] = pathsep.join(python_path) + + executable = which(args.command) + execl(executable, executable, *args.command_args) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py new file mode 100644 index 0000000000..d89b60ec56 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -0,0 +1,135 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +from logging import getLogger +from os import environ, path +from os.path import abspath, dirname, pathsep +from re import sub + +from pkg_resources import iter_entry_points + +from opentelemetry.environment_variables import ( + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, +) +from opentelemetry.instrumentation.dependencies import ( + get_dist_dependency_conflicts, +) +from opentelemetry.instrumentation.distro import BaseDistro, DefaultDistro + +logger = getLogger(__file__) + + +def _load_distros() -> BaseDistro: + for entry_point in iter_entry_points("opentelemetry_distro"): + try: + distro = entry_point.load()() + if not isinstance(distro, BaseDistro): + logger.debug( + "%s is not an OpenTelemetry Distro. Skipping", + entry_point.name, + ) + continue + logger.debug( + "Distribution %s will be configured", entry_point.name + ) + return distro + except Exception as exc: # pylint: disable=broad-except + logger.exception( + "Distribution %s configuration failed", entry_point.name + ) + raise exc + return DefaultDistro() + + +def _load_instrumentors(distro): + package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, []) + if isinstance(package_to_exclude, str): + package_to_exclude = package_to_exclude.split(",") + # to handle users entering "requests , flask" or "requests, flask" with spaces + package_to_exclude = [x.strip() for x in package_to_exclude] + + for entry_point in iter_entry_points("opentelemetry_instrumentor"): + if entry_point.name in package_to_exclude: + logger.debug( + "Instrumentation skipped for library %s", entry_point.name + ) + continue + + try: + conflict = get_dist_dependency_conflicts(entry_point.dist) + if conflict: + logger.debug( + "Skipping instrumentation %s: %s", + entry_point.name, + conflict, + ) + continue + + # tell instrumentation to not run dep checks again as we already did it above + distro.load_instrumentor(entry_point, skip_dep_check=True) + logger.debug("Instrumented %s", entry_point.name) + except Exception as exc: # pylint: disable=broad-except + logger.exception("Instrumenting of %s failed", entry_point.name) + raise exc + + +def _load_configurators(): + configured = None + for entry_point in iter_entry_points("opentelemetry_configurator"): + if configured is not None: + logger.warning( + "Configuration of %s not loaded, %s already loaded", + entry_point.name, + configured, + ) + continue + try: + entry_point.load()().configure() # type: ignore + configured = entry_point.name + except Exception as exc: # pylint: disable=broad-except + logger.exception("Configuration of %s failed", entry_point.name) + raise exc + + +def initialize(): + try: + distro = _load_distros() + distro.configure() + _load_configurators() + _load_instrumentors(distro) + except Exception: # pylint: disable=broad-except + logger.exception("Failed to auto initialize opentelemetry") + finally: + environ["PYTHONPATH"] = sub( + r"{}{}?".format(dirname(abspath(__file__)), pathsep), + "", + environ["PYTHONPATH"], + ) + + +if ( + hasattr(sys, "argv") + and sys.argv[0].split(path.sep)[-1] == "celery" + and "worker" in sys.argv[1:] +): + from celery.signals import worker_process_init # pylint:disable=E0401 + + @worker_process_init.connect(weak=False) + def init_celery(*args, **kwargs): + initialize() + + +else: + initialize() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py new file mode 100644 index 0000000000..a29117f972 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 + +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import logging +import subprocess +import sys + +import pkg_resources + +from opentelemetry.instrumentation.bootstrap_gen import ( + default_instrumentations, + libraries, +) +from opentelemetry.instrumentation.version import __version__ as version + +logger = logging.getLogger(__file__) + + +def _syscall(func): + def wrapper(package=None): + try: + if package: + return func(package) + return func() + except subprocess.SubprocessError as exp: + cmd = getattr(exp, "cmd", None) + if cmd: + msg = 'Error calling system command "{0}"'.format( + " ".join(cmd) + ) + if package: + msg = '{0} for package "{1}"'.format(msg, package) + raise RuntimeError(msg) + + return wrapper + + +@_syscall +def _sys_pip_install(package): + # explicit upgrade strategy to override potential pip config + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "-U", + "--upgrade-strategy", + "only-if-needed", + package, + ] + ) + + +def _pip_check(): + """Ensures none of the instrumentations have dependency conflicts. + Clean check reported as: + 'No broken requirements found.' + Dependency conflicts are reported as: + 'opentelemetry-instrumentation-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' + To not be too restrictive, we'll only check for relevant packages. + """ + check_pipe = subprocess.Popen( + [sys.executable, "-m", "pip", "check"], stdout=subprocess.PIPE + ) + pip_check = check_pipe.communicate()[0].decode() + pip_check_lower = pip_check.lower() + for package_tup in libraries.values(): + for package in package_tup: + if package.lower() in pip_check_lower: + raise RuntimeError( + "Dependency conflict found: {}".format(pip_check) + ) + + +def _is_installed(req): + if req in sys.modules: + return True + + try: + pkg_resources.get_distribution(req) + except pkg_resources.DistributionNotFound: + return False + except pkg_resources.VersionConflict as exc: + logger.warning( + "instrumentation for package %s is available but version %s is installed. Skipping.", + exc.req, + exc.dist.as_requirement(), # pylint: disable=no-member + ) + return False + return True + + +def _find_installed_libraries(): + libs = default_instrumentations[:] + libs.extend( + [ + v["instrumentation"] + for _, v in libraries.items() + if _is_installed(v["library"]) + ] + ) + return libs + + +def _run_requirements(): + logger.setLevel(logging.ERROR) + print("\n".join(_find_installed_libraries()), end="") + + +def _run_install(): + for lib in _find_installed_libraries(): + _sys_pip_install(lib) + _pip_check() + + +def run() -> None: + action_install = "install" + action_requirements = "requirements" + + parser = argparse.ArgumentParser( + description=""" + opentelemetry-bootstrap detects installed libraries and automatically + installs the relevant instrumentation packages for them. + """ + ) + parser.add_argument( + "-a", + "--action", + choices=[action_install, action_requirements], + default=action_requirements, + help=""" + install - uses pip to install the new requirements using to the + currently active site-package. + requirements - prints out the new requirements to stdout. Action can + be piped and appended to a requirements.txt file. + """, + ) + args = parser.parse_args() + + cmd = { + action_install: _run_install, + action_requirements: _run_requirements, + }[args.action] + cmd() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py new file mode 100644 index 0000000000..b49f40905f --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -0,0 +1,138 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/generate_instrumentation_bootstrap.py` TO REGENERATE. + +libraries = { + "aiohttp": { + "library": "aiohttp ~= 3.0", + "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.23.dev0", + }, + "aiopg": { + "library": "aiopg >= 0.13.0, < 1.3.0", + "instrumentation": "opentelemetry-instrumentation-aiopg==0.23.dev0", + }, + "asgiref": { + "library": "asgiref ~= 3.0", + "instrumentation": "opentelemetry-instrumentation-asgi==0.23.dev0", + }, + "asyncpg": { + "library": "asyncpg >= 0.12.0", + "instrumentation": "opentelemetry-instrumentation-asyncpg==0.23.dev0", + }, + "boto": { + "library": "boto~=2.0", + "instrumentation": "opentelemetry-instrumentation-boto==0.23.dev0", + }, + "botocore": { + "library": "botocore ~= 1.0", + "instrumentation": "opentelemetry-instrumentation-botocore==0.23.dev0", + }, + "celery": { + "library": "celery >= 4.0, < 6.0", + "instrumentation": "opentelemetry-instrumentation-celery==0.23.dev0", + }, + "django": { + "library": "django >= 1.10", + "instrumentation": "opentelemetry-instrumentation-django==0.23.dev0", + }, + "elasticsearch": { + "library": "elasticsearch >= 2.0", + "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.23.dev0", + }, + "falcon": { + "library": "falcon ~= 2.0", + "instrumentation": "opentelemetry-instrumentation-falcon==0.23.dev0", + }, + "fastapi": { + "library": "fastapi ~= 0.58.1", + "instrumentation": "opentelemetry-instrumentation-fastapi==0.23.dev0", + }, + "flask": { + "library": "flask >= 1.0, < 3.0", + "instrumentation": "opentelemetry-instrumentation-flask==0.23.dev0", + }, + "grpcio": { + "library": "grpcio ~= 1.27", + "instrumentation": "opentelemetry-instrumentation-grpc==0.23.dev0", + }, + "httpx": { + "library": "httpx >= 0.18.0, < 0.19.0", + "instrumentation": "opentelemetry-instrumentation-httpx==0.23.dev0", + }, + "jinja2": { + "library": "jinja2~=2.7", + "instrumentation": "opentelemetry-instrumentation-jinja2==0.23.dev0", + }, + "mysql-connector-python": { + "library": "mysql-connector-python ~= 8.0", + "instrumentation": "opentelemetry-instrumentation-mysql==0.23.dev0", + }, + "psycopg2": { + "library": "psycopg2 >= 2.7.3.1", + "instrumentation": "opentelemetry-instrumentation-psycopg2==0.23.dev0", + }, + "pymemcache": { + "library": "pymemcache ~= 1.3", + "instrumentation": "opentelemetry-instrumentation-pymemcache==0.23.dev0", + }, + "pymongo": { + "library": "pymongo ~= 3.1", + "instrumentation": "opentelemetry-instrumentation-pymongo==0.23.dev0", + }, + "PyMySQL": { + "library": "PyMySQL ~= 0.10.1", + "instrumentation": "opentelemetry-instrumentation-pymysql==0.23.dev0", + }, + "pyramid": { + "library": "pyramid >= 1.7", + "instrumentation": "opentelemetry-instrumentation-pyramid==0.23.dev0", + }, + "redis": { + "library": "redis >= 2.6", + "instrumentation": "opentelemetry-instrumentation-redis==0.23.dev0", + }, + "requests": { + "library": "requests ~= 2.0", + "instrumentation": "opentelemetry-instrumentation-requests==0.23.dev0", + }, + "scikit-learn": { + "library": "scikit-learn ~= 0.24.0", + "instrumentation": "opentelemetry-instrumentation-sklearn==0.23.dev0", + }, + "sqlalchemy": { + "library": "sqlalchemy", + "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.23.dev0", + }, + "starlette": { + "library": "starlette ~= 0.13.0", + "instrumentation": "opentelemetry-instrumentation-starlette==0.23.dev0", + }, + "tornado": { + "library": "tornado >= 6.0", + "instrumentation": "opentelemetry-instrumentation-tornado==0.23.dev0", + }, + "urllib3": { + "library": "urllib3 >= 1.0.0, < 2.0.0", + "instrumentation": "opentelemetry-instrumentation-urllib3==0.23.dev0", + }, +} +default_instrumentations = [ + "opentelemetry-instrumentation-dbapi==0.23.dev0", + "opentelemetry-instrumentation-logging==0.23.dev0", + "opentelemetry-instrumentation-sqlite3==0.23.dev0", + "opentelemetry-instrumentation-urllib==0.23.dev0", + "opentelemetry-instrumentation-wsgi==0.23.dev0", +] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py new file mode 100644 index 0000000000..3efa71e89e --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py @@ -0,0 +1,53 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +""" +OpenTelemetry Base Configurator +""" + +from abc import ABC, abstractmethod +from logging import getLogger + +_LOG = getLogger(__name__) + + +class BaseConfigurator(ABC): + """An ABC for configurators + + Configurators are used to configure + SDKs (i.e. TracerProvider, MeterProvider, Processors...) + to reduce the amount of manual configuration required. + """ + + _instance = None + _is_instrumented = False + + def __new__(cls, *args, **kwargs): + + if cls._instance is None: + cls._instance = object.__new__(cls, *args, **kwargs) + + return cls._instance + + @abstractmethod + def _configure(self, **kwargs): + """Configure the SDK""" + + def configure(self, **kwargs): + """Configure the SDK""" + self._configure(**kwargs) + + +__all__ = ["BaseConfigurator"] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py new file mode 100644 index 0000000000..0cec55769c --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py @@ -0,0 +1,64 @@ +from logging import getLogger +from typing import Collection, Optional + +from pkg_resources import ( + Distribution, + DistributionNotFound, + RequirementParseError, + VersionConflict, + get_distribution, +) + +logger = getLogger(__file__) + + +class DependencyConflict: + required: str = None + found: Optional[str] = None + + def __init__(self, required, found=None): + self.required = required + self.found = found + + def __str__(self): + return 'DependencyConflict: requested: "{0}" but found: "{1}"'.format( + self.required, self.found + ) + + +def get_dist_dependency_conflicts( + dist: Distribution, +) -> Optional[DependencyConflict]: + main_deps = dist.requires() + instrumentation_deps = [] + for dep in dist.requires(("instruments",)): + if dep not in main_deps: + # we set marker to none so string representation of the dependency looks like + # requests ~= 1.0 + # instead of + # requests ~= 1.0; extra = "instruments" + # which does not work with `get_distribution()` + dep.marker = None + instrumentation_deps.append(str(dep)) + + return get_dependency_conflicts(instrumentation_deps) + + +def get_dependency_conflicts( + deps: Collection[str], +) -> Optional[DependencyConflict]: + for dep in deps: + try: + get_distribution(dep) + except VersionConflict as exc: + return DependencyConflict(dep, exc.dist) + except DistributionNotFound: + return DependencyConflict(dep) + except RequirementParseError as exc: + logger.warning( + 'error parsing dependency, reporting as a conflict: "%s" - %s', + dep, + exc, + ) + return DependencyConflict(dep) + return None diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py new file mode 100644 index 0000000000..cc1c99c1e0 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py @@ -0,0 +1,71 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +""" +OpenTelemetry Base Distribution (Distro) +""" + +from abc import ABC, abstractmethod +from logging import getLogger + +from pkg_resources import EntryPoint + +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + +_LOG = getLogger(__name__) + + +class BaseDistro(ABC): + """An ABC for distro""" + + _instance = None + + def __new__(cls, *args, **kwargs): + + if cls._instance is None: + cls._instance = object.__new__(cls, *args, **kwargs) + + return cls._instance + + @abstractmethod + def _configure(self, **kwargs): + """Configure the distribution""" + + def configure(self, **kwargs): + """Configure the distribution""" + self._configure(**kwargs) + + def load_instrumentor( # pylint: disable=no-self-use + self, entry_point: EntryPoint, **kwargs + ): + """Takes a collection of instrumentation entry points + and activates them by instantiating and calling instrument() + on each one. + + Distros can override this method to customize the behavior by + inspecting each entry point and configuring them in special ways, + passing additional arguments, load a replacement/fork instead, + skip loading entirely, etc. + """ + instrumentor: BaseInstrumentor = entry_point.load() + instrumentor().instrument(**kwargs) + + +class DefaultDistro(BaseDistro): + def _configure(self, **kwargs): + pass + + +__all__ = ["BaseDistro", "DefaultDistro"] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py new file mode 100644 index 0000000000..74ebe86746 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py @@ -0,0 +1,132 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +""" +OpenTelemetry Base Instrumentor +""" + +from abc import ABC, abstractmethod +from logging import getLogger +from typing import Collection, Optional + +from opentelemetry.instrumentation.dependencies import ( + DependencyConflict, + get_dependency_conflicts, +) + +_LOG = getLogger(__name__) + + +class BaseInstrumentor(ABC): + """An ABC for instrumentors + + Child classes of this ABC should instrument specific third + party libraries or frameworks either by using the + ``opentelemetry-instrument`` command or by calling their methods + directly. + + Since every third party library or framework is different and has different + instrumentation needs, more methods can be added to the child classes as + needed to provide practical instrumentation to the end user. + """ + + _instance = None + _is_instrumented_by_opentelemetry = False + + def __new__(cls, *args, **kwargs): + + if cls._instance is None: + cls._instance = object.__new__(cls, *args, **kwargs) + + return cls._instance + + @property + def is_instrumented_by_opentelemetry(self): + return self._is_instrumented_by_opentelemetry + + @abstractmethod + def instrumentation_dependencies(self) -> Collection[str]: + """Return a list of python packages with versions that the will be instrumented. + + The format should be the same as used in requirements.txt or setup.py. + + For example, if an instrumentation instruments requests 1.x, this method should look + like: + + def instrumentation_dependencies(self) -> Collection[str]: + return ['requests ~= 1.0'] + + This will ensure that the instrumentation will only be used when the specified library + is present in the environment. + """ + + def _instrument(self, **kwargs): + """Instrument the library""" + + @abstractmethod + def _uninstrument(self, **kwargs): + """Uninstrument the library""" + + def _check_dependency_conflicts(self) -> Optional[DependencyConflict]: + dependencies = self.instrumentation_dependencies() + return get_dependency_conflicts(dependencies) + + def instrument(self, **kwargs): + """Instrument the library + + This method will be called without any optional arguments by the + ``opentelemetry-instrument`` command. + + This means that calling this method directly without passing any + optional values should do the very same thing that the + ``opentelemetry-instrument`` command does. + """ + + if self._is_instrumented_by_opentelemetry: + _LOG.warning("Attempting to instrument while already instrumented") + return None + + # check if instrumentor has any missing or conflicting dependencies + skip_dep_check = kwargs.pop("skip_dep_check", False) + if not skip_dep_check: + conflict = self._check_dependency_conflicts() + if conflict: + _LOG.error(conflict) + return None + + result = self._instrument( # pylint: disable=assignment-from-no-return + **kwargs + ) + self._is_instrumented_by_opentelemetry = True + return result + + def uninstrument(self, **kwargs): + """Uninstrument the library + + See ``BaseInstrumentor.instrument`` for more information regarding the + usage of ``kwargs``. + """ + + if self._is_instrumented_by_opentelemetry: + result = self._uninstrument(**kwargs) + self._is_instrumented_by_opentelemetry = False + return result + + _LOG.warning("Attempting to uninstrument while already uninstrumented") + + return None + + +__all__ = ["BaseInstrumentor"] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py new file mode 100644 index 0000000000..96a771d719 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py @@ -0,0 +1,128 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module implements experimental propagators to inject trace context +into response carriers. This is useful for server side frameworks that start traces +when server requests and want to share the trace context with the client so the +client can add it's spans to the same trace. + +This is part of an upcoming W3C spec and will eventually make it to the Otel spec. + +https://w3c.github.io/trace-context/#trace-context-http-response-headers-format +""" + +import typing +from abc import ABC, abstractmethod + +from opentelemetry import trace +from opentelemetry.context.context import Context +from opentelemetry.propagators import textmap +from opentelemetry.trace import format_span_id, format_trace_id + +_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers" +_RESPONSE_PROPAGATOR = None + + +def get_global_response_propagator(): + return _RESPONSE_PROPAGATOR + + +def set_global_response_propagator(propagator): + global _RESPONSE_PROPAGATOR # pylint:disable=global-statement + _RESPONSE_PROPAGATOR = propagator + + +class Setter(ABC): + @abstractmethod + def set(self, carrier, key, value): + """Inject the provided key value pair in carrier.""" + + +class DictHeaderSetter(Setter): + def set(self, carrier, key, value): # pylint: disable=no-self-use + old_value = carrier.get(key, "") + if old_value: + value = "{0}, {1}".format(old_value, value) + carrier[key] = value + + +class FuncSetter(Setter): + """FuncSetter coverts a function into a valid Setter. Any function that can + set values in a carrier can be converted into a Setter by using FuncSetter. + This is useful when injecting trace context into non-dict objects such + HTTP Response objects for different framework. + + For example, it can be used to create a setter for Falcon response object as: + + setter = FuncSetter(falcon.api.Response.append_header) + + and then used with the propagator as: + + propagator.inject(falcon_response, setter=setter) + + This would essentially make the propagator call `falcon_response.append_header(key, value)` + """ + + def __init__(self, func): + self._func = func + + def set(self, carrier, key, value): + self._func(carrier, key, value) + + +default_setter = DictHeaderSetter() + + +class ResponsePropagator(ABC): + @abstractmethod + def inject( + self, + carrier: textmap.CarrierT, + context: typing.Optional[Context] = None, + setter: textmap.Setter = default_setter, + ) -> None: + """Injects SpanContext into the HTTP response carrier.""" + + +class TraceResponsePropagator(ResponsePropagator): + """Experimental propagator that injects tracecontext into HTTP responses.""" + + def inject( + self, + carrier: textmap.CarrierT, + context: typing.Optional[Context] = None, + setter: textmap.Setter = default_setter, + ) -> None: + """Injects SpanContext into the HTTP response carrier.""" + span = trace.get_current_span(context) + span_context = span.get_span_context() + if span_context == trace.INVALID_SPAN_CONTEXT: + return + + header_name = "traceresponse" + setter.set( + carrier, + header_name, + "00-{trace_id}-{span_id}-{:02x}".format( + span_context.trace_flags, + trace_id=format_trace_id(span_context.trace_id), + span_id=format_span_id(span_context.span_id), + ), + ) + setter.set( + carrier, + _HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, + header_name, + ) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py new file mode 100644 index 0000000000..16f75aae6c --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -0,0 +1,68 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict, Sequence + +from wrapt import ObjectProxy + +from opentelemetry.context import create_key +from opentelemetry.trace import StatusCode + +# FIXME This is a temporary location for the suppress instrumentation key. +# Once the decision around how to suppress instrumentation is made in the +# spec, this key should be moved accordingly. +_SUPPRESS_INSTRUMENTATION_KEY = create_key("suppress_instrumentation") + + +def extract_attributes_from_object( + obj: any, attributes: Sequence[str], existing: Dict[str, str] = None +) -> Dict[str, str]: + extracted = {} + if existing: + extracted.update(existing) + for attr in attributes: + value = getattr(obj, attr, None) + if value is not None: + extracted[attr] = str(value) + return extracted + + +def http_status_to_status_code( + status: int, allow_redirect: bool = True +) -> StatusCode: + """Converts an HTTP status code to an OpenTelemetry canonical status code + + Args: + status (int): HTTP status code + """ + # See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status + if status < 100: + return StatusCode.ERROR + if status <= 299: + return StatusCode.UNSET + if status <= 399 and allow_redirect: + return StatusCode.UNSET + return StatusCode.ERROR + + +def unwrap(obj, attr: str): + """Given a function that was wrapped by wrapt.wrap_function_wrapper, unwrap it + + Args: + obj: Object that holds a reference to the wrapped function + attr (str): Name of the wrapped function + """ + func = getattr(obj, attr, None) + if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"): + setattr(obj, attr, func.__wrapped__) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py new file mode 100644 index 0000000000..c829b95757 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.23.dev0" diff --git a/opentelemetry-instrumentation/tests/__init__.py b/opentelemetry-instrumentation/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-instrumentation/tests/test_bootstrap.py b/opentelemetry-instrumentation/tests/test_bootstrap.py new file mode 100644 index 0000000000..a266bf8a43 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_bootstrap.py @@ -0,0 +1,91 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from io import StringIO +from random import sample +from unittest import TestCase +from unittest.mock import call, patch + +from opentelemetry.instrumentation import bootstrap +from opentelemetry.instrumentation.bootstrap_gen import libraries + + +def sample_packages(packages, rate): + return sample( + list(packages), + int(len(packages) * rate), + ) + + +class TestBootstrap(TestCase): + + installed_libraries = {} + installed_instrumentations = {} + + @classmethod + def setUpClass(cls): + cls.installed_libraries = sample_packages( + [lib["instrumentation"] for lib in libraries.values()], 0.6 + ) + + # treat 50% of sampled packages as pre-installed + cls.installed_instrumentations = sample_packages( + cls.installed_libraries, 0.5 + ) + + cls.pkg_patcher = patch( + "opentelemetry.instrumentation.bootstrap._find_installed_libraries", + return_value=cls.installed_libraries, + ) + + cls.pip_install_patcher = patch( + "opentelemetry.instrumentation.bootstrap._sys_pip_install", + ) + cls.pip_check_patcher = patch( + "opentelemetry.instrumentation.bootstrap._pip_check", + ) + + cls.pkg_patcher.start() + cls.mock_pip_install = cls.pip_install_patcher.start() + cls.mock_pip_check = cls.pip_check_patcher.start() + + @classmethod + def tearDownClass(cls): + cls.pip_check_patcher.start() + cls.pip_install_patcher.start() + cls.pkg_patcher.stop() + + @patch("sys.argv", ["bootstrap", "-a", "pipenv"]) + def test_run_unknown_cmd(self): + with self.assertRaises(SystemExit): + bootstrap.run() + + @patch("sys.argv", ["bootstrap", "-a", "requirements"]) + def test_run_cmd_print(self): + with patch("sys.stdout", new=StringIO()) as fake_out: + bootstrap.run() + self.assertEqual( + fake_out.getvalue(), + "\n".join(self.installed_libraries), + ) + + @patch("sys.argv", ["bootstrap", "-a", "install"]) + def test_run_cmd_install(self): + bootstrap.run() + self.mock_pip_install.assert_has_calls( + [call(i) for i in self.installed_libraries], + any_order=True, + ) + self.assertEqual(self.mock_pip_check.call_count, 1) diff --git a/opentelemetry-instrumentation/tests/test_dependencies.py b/opentelemetry-instrumentation/tests/test_dependencies.py new file mode 100644 index 0000000000..8b2f2e9b39 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_dependencies.py @@ -0,0 +1,79 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access + +import pkg_resources +import pytest + +from opentelemetry.instrumentation.dependencies import ( + DependencyConflict, + get_dependency_conflicts, + get_dist_dependency_conflicts, +) +from opentelemetry.test.test_base import TestBase + + +class TestDependencyConflicts(TestBase): + def setUp(self): + pass + + def test_get_dependency_conflicts_empty(self): + self.assertIsNone(get_dependency_conflicts([])) + + def test_get_dependency_conflicts_no_conflict(self): + self.assertIsNone(get_dependency_conflicts(["pytest"])) + + def test_get_dependency_conflicts_not_installed(self): + conflict = get_dependency_conflicts(["this-package-does-not-exist"]) + self.assertTrue(conflict is not None) + self.assertTrue(isinstance(conflict, DependencyConflict)) + self.assertEqual( + str(conflict), + 'DependencyConflict: requested: "this-package-does-not-exist" but found: "None"', + ) + + def test_get_dependency_conflicts_mismatched_version(self): + conflict = get_dependency_conflicts(["pytest == 5000"]) + self.assertTrue(conflict is not None) + self.assertTrue(isinstance(conflict, DependencyConflict)) + self.assertEqual( + str(conflict), + 'DependencyConflict: requested: "pytest == 5000" but found: "pytest {0}"'.format( + pytest.__version__ + ), + ) + + def test_get_dist_dependency_conflicts(self): + def mock_requires(extras=()): + if "instruments" in extras: + return [ + pkg_resources.Requirement( + 'test-pkg ~= 1.0; extra == "instruments"' + ) + ] + return [] + + dist = pkg_resources.Distribution( + project_name="test-instrumentation", version="1.0" + ) + dist.requires = mock_requires + + conflict = get_dist_dependency_conflicts(dist) + self.assertTrue(conflict is not None) + self.assertTrue(isinstance(conflict, DependencyConflict)) + self.assertEqual( + str(conflict), + 'DependencyConflict: requested: "test-pkg~=1.0" but found: "None"', + ) diff --git a/opentelemetry-instrumentation/tests/test_distro.py b/opentelemetry-instrumentation/tests/test_distro.py new file mode 100644 index 0000000000..399b3f8a65 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_distro.py @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from unittest import TestCase + +from pkg_resources import EntryPoint + +from opentelemetry.instrumentation.distro import BaseDistro +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + + +class MockInstrumetor(BaseInstrumentor): + def instrumentation_dependencies(self): + return [] + + def _instrument(self, **kwargs): + pass + + def _uninstrument(self, **kwargs): + pass + + +class MockEntryPoint(EntryPoint): + def __init__(self, obj): # pylint: disable=super-init-not-called + self._obj = obj + + def load(self, *args, **kwargs): # pylint: disable=signature-differs + return self._obj + + +class MockDistro(BaseDistro): + def _configure(self, **kwargs): + pass + + +class TestDistro(TestCase): + def test_load_instrumentor(self): + # pylint: disable=protected-access + distro = MockDistro() + + instrumentor = MockInstrumetor() + entry_point = MockEntryPoint(MockInstrumetor) + + self.assertFalse(instrumentor._is_instrumented_by_opentelemetry) + distro.load_instrumentor(entry_point) + self.assertTrue(instrumentor._is_instrumented_by_opentelemetry) diff --git a/opentelemetry-instrumentation/tests/test_instrumentor.py b/opentelemetry-instrumentation/tests/test_instrumentor.py new file mode 100644 index 0000000000..dee32c34e4 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_instrumentor.py @@ -0,0 +1,50 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from logging import WARNING +from unittest import TestCase + +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + + +class TestInstrumentor(TestCase): + class Instrumentor(BaseInstrumentor): + def _instrument(self, **kwargs): + return "instrumented" + + def _uninstrument(self, **kwargs): + return "uninstrumented" + + def instrumentation_dependencies(self): + return [] + + def test_protect(self): + instrumentor = self.Instrumentor() + + with self.assertLogs(level=WARNING): + self.assertIs(instrumentor.uninstrument(), None) + + self.assertEqual(instrumentor.instrument(), "instrumented") + + with self.assertLogs(level=WARNING): + self.assertIs(instrumentor.instrument(), None) + + self.assertEqual(instrumentor.uninstrument(), "uninstrumented") + + with self.assertLogs(level=WARNING): + self.assertIs(instrumentor.uninstrument(), None) + + def test_singleton(self): + self.assertIs(self.Instrumentor(), self.Instrumentor()) diff --git a/opentelemetry-instrumentation/tests/test_propagators.py b/opentelemetry-instrumentation/tests/test_propagators.py new file mode 100644 index 0000000000..62461aafa9 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_propagators.py @@ -0,0 +1,80 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access + +from opentelemetry import trace +from opentelemetry.instrumentation import propagators +from opentelemetry.instrumentation.propagators import ( + DictHeaderSetter, + TraceResponsePropagator, + get_global_response_propagator, + set_global_response_propagator, +) +from opentelemetry.test.test_base import TestBase + + +class TestGlobals(TestBase): + def test_get_set(self): + original = propagators._RESPONSE_PROPAGATOR + + propagators._RESPONSE_PROPAGATOR = None + self.assertIsNone(get_global_response_propagator()) + + prop = TraceResponsePropagator() + set_global_response_propagator(prop) + self.assertIs(prop, get_global_response_propagator()) + + propagators._RESPONSE_PROPAGATOR = original + + +class TestDictHeaderSetter(TestBase): + def test_simple(self): + setter = DictHeaderSetter() + carrier = {} + setter.set(carrier, "kk", "vv") + self.assertIn("kk", carrier) + self.assertEqual(carrier["kk"], "vv") + + def test_append(self): + setter = DictHeaderSetter() + carrier = {"kk": "old"} + setter.set(carrier, "kk", "vv") + self.assertIn("kk", carrier) + self.assertEqual(carrier["kk"], "old, vv") + + +class TestTraceResponsePropagator(TestBase): + def test_inject(self): + span = trace.NonRecordingSpan( + trace.SpanContext( + trace_id=1, + span_id=2, + is_remote=False, + trace_flags=trace.DEFAULT_TRACE_OPTIONS, + trace_state=trace.DEFAULT_TRACE_STATE, + ), + ) + + ctx = trace.set_span_in_context(span) + prop = TraceResponsePropagator() + carrier = {} + prop.inject(carrier, ctx) + self.assertEqual( + carrier["Access-Control-Expose-Headers"], "traceresponse" + ) + self.assertEqual( + carrier["traceresponse"], + "00-00000000000000000000000000000001-0000000000000002-00", + ) diff --git a/opentelemetry-instrumentation/tests/test_run.py b/opentelemetry-instrumentation/tests/test_run.py new file mode 100644 index 0000000000..01bd86ed32 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_run.py @@ -0,0 +1,117 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from os import environ, getcwd +from os.path import abspath, dirname, pathsep +from unittest import TestCase +from unittest.mock import patch + +from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER +from opentelemetry.instrumentation import auto_instrumentation + + +class TestRun(TestCase): + auto_instrumentation_path = dirname(abspath(auto_instrumentation.__file__)) + + @classmethod + def setUpClass(cls): + cls.execl_patcher = patch( + "opentelemetry.instrumentation.auto_instrumentation.execl" + ) + cls.which_patcher = patch( + "opentelemetry.instrumentation.auto_instrumentation.which" + ) + + cls.execl_patcher.start() + cls.which_patcher.start() + + @classmethod + def tearDownClass(cls): + cls.execl_patcher.stop() + cls.which_patcher.stop() + + @patch("sys.argv", ["instrument", ""]) + @patch.dict("os.environ", {"PYTHONPATH": ""}) + def test_empty(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd()]), + ) + + @patch("sys.argv", ["instrument", ""]) + @patch.dict("os.environ", {"PYTHONPATH": "abc"}) + def test_non_empty(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), + ) + + @patch("sys.argv", ["instrument", ""]) + @patch.dict( + "os.environ", + {"PYTHONPATH": pathsep.join(["abc", auto_instrumentation_path])}, + ) + def test_after_path(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), + ) + + @patch("sys.argv", ["instrument", ""]) + @patch.dict( + "os.environ", + { + "PYTHONPATH": pathsep.join( + [auto_instrumentation_path, "abc", auto_instrumentation_path] + ) + }, + ) + def test_single_path(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), + ) + + +class TestExecl(TestCase): + @patch("sys.argv", ["1", "2", "3"]) + @patch("opentelemetry.instrumentation.auto_instrumentation.which") + @patch("opentelemetry.instrumentation.auto_instrumentation.execl") + def test_execl( + self, mock_execl, mock_which + ): # pylint: disable=no-self-use + mock_which.configure_mock(**{"return_value": "python"}) + + auto_instrumentation.run() + + mock_execl.assert_called_with("python", "python", "3") + + +class TestArgs(TestCase): + @patch("opentelemetry.instrumentation.auto_instrumentation.execl") + def test_exporter(self, _): # pylint: disable=no-self-use + with patch("sys.argv", ["instrument", "2"]): + auto_instrumentation.run() + self.assertIsNone(environ.get(OTEL_TRACES_EXPORTER)) + + with patch( + "sys.argv", ["instrument", "--trace-exporter", "jaeger", "1", "2"] + ): + auto_instrumentation.run() + self.assertEqual(environ.get(OTEL_TRACES_EXPORTER), "jaeger") diff --git a/opentelemetry-instrumentation/tests/test_utils.py b/opentelemetry-instrumentation/tests/test_utils.py new file mode 100644 index 0000000000..e5246335c9 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_utils.py @@ -0,0 +1,57 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from http import HTTPStatus + +from opentelemetry.instrumentation.utils import http_status_to_status_code +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import StatusCode + + +class TestUtils(TestBase): + # See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status + def test_http_status_to_status_code(self): + for status_code, expected in ( + (HTTPStatus.OK, StatusCode.UNSET), + (HTTPStatus.ACCEPTED, StatusCode.UNSET), + (HTTPStatus.IM_USED, StatusCode.UNSET), + (HTTPStatus.MULTIPLE_CHOICES, StatusCode.UNSET), + (HTTPStatus.BAD_REQUEST, StatusCode.ERROR), + (HTTPStatus.UNAUTHORIZED, StatusCode.ERROR), + (HTTPStatus.FORBIDDEN, StatusCode.ERROR), + (HTTPStatus.NOT_FOUND, StatusCode.ERROR), + ( + HTTPStatus.UNPROCESSABLE_ENTITY, + StatusCode.ERROR, + ), + ( + HTTPStatus.TOO_MANY_REQUESTS, + StatusCode.ERROR, + ), + (HTTPStatus.NOT_IMPLEMENTED, StatusCode.ERROR), + (HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR), + ( + HTTPStatus.GATEWAY_TIMEOUT, + StatusCode.ERROR, + ), + ( + HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, + StatusCode.ERROR, + ), + (600, StatusCode.ERROR), + (99, StatusCode.ERROR), + ): + with self.subTest(status_code=status_code): + actual = http_status_to_status_code(int(status_code)) + self.assertEqual(actual, expected, status_code) diff --git a/scripts/build.sh b/scripts/build.sh index 8134769775..2f40f1a003 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-proto/ opentelemetry-distro/ opentelemetry-semantic-conventions/ exporter/*/ shim/*/ propagator/*/; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ opentelemetry-distro/ opentelemetry-semantic-conventions/ exporter/*/ shim/*/ propagator/*/; do ( echo "building $d" cd "$d" diff --git a/tox.ini b/tox.ini index 49b62e1e27..04d683489c 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,10 @@ envlist = py3{6,7,8,9}-test-core-sdk pypy3-test-core-sdk + ; opentelemetry-instrumentation + py3{6,7,8,9}-test-core-instrumentation + pypy3-test-core-instrumentation + ; opentelemetry-semantic-conventions py3{6,7,8,9}-test-semantic-conventions pypy3-test-semantic-conventions @@ -96,6 +100,7 @@ changedir = test-core-sdk: opentelemetry-sdk/tests test-core-proto: opentelemetry-proto/tests test-semantic-conventions: opentelemetry-semantic-conventions/tests + test-core-instrumentation: opentelemetry-instrumentation/tests test-core-getting-started: docs/getting_started/tests test-core-opentracing-shim: shim/opentelemetry-opentracing-shim/tests test-core-distro: opentelemetry-distro/tests @@ -118,15 +123,16 @@ commands_pre = py3{6,7,8,9}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. - test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util + test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-instrumentation {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util - test-core-sdk: pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation - test-core-opentracing-shim: pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation + test-core-sdk: pip install {toxinidir}/opentelemetry-instrumentation + test-core-opentracing-shim: pip install {toxinidir}/opentelemetry-instrumentation test-core-proto: pip install {toxinidir}/opentelemetry-proto - distro: pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation {toxinidir}/opentelemetry-distro + distro: pip install {toxinidir}/opentelemetry-distro + instrumentation: pip install {toxinidir}/opentelemetry-instrumentation - getting-started: pip install requests flask -e {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask + getting-started: pip install requests flask -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus @@ -138,9 +144,9 @@ commands_pre = exporter-otlp-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc exporter-jaeger-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift {toxinidir}/exporter/opentelemetry-exporter-jaeger - exporter-jaeger-combined: pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation + exporter-jaeger-combined: pip install {toxinidir}/opentelemetry-instrumentation exporter-jaeger-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc - exporter-jaeger-proto-grpc: pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation + exporter-jaeger-proto-grpc: pip install {toxinidir}/opentelemetry-instrumentation exporter-jaeger-thrift: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift opentracing-shim: pip install {toxinidir}/opentelemetry-sdk @@ -197,7 +203,7 @@ deps = commands_pre = python -m pip install -e {toxinidir}/opentelemetry-api[test] python -m pip install -e {toxinidir}/opentelemetry-semantic-conventions[test] - python -m pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation + python -m pip install -e {toxinidir}/opentelemetry-instrumentation[test] python -m pip install -e {toxinidir}/opentelemetry-sdk[test] python -m pip install -e {toxinidir}/opentelemetry-proto[test] python -m pip install -e {toxinidir}/tests/util[test] @@ -226,8 +232,8 @@ deps = changedir = docs -commands_pre = - python -m pip install {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation +commands-pre = + python -m pip install {toxinidir}/opentelemetry-instrumentation commands = sphinx-build -E -a -W -b html -T . _build/html @@ -244,7 +250,7 @@ deps = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-semantic-conventions \ - -e {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation \ + -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests \ -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi \ @@ -264,7 +270,7 @@ changedir = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-semantic-conventions \ - -e {toxinidir}/opentelemetry-python-contrib/opentelemetry-instrumentation \ + -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/tests/util \ -e {toxinidir}/exporter/opentelemetry-exporter-opencensus \ From 67bbad45917baa354a3a6d0d4361da48ff13fd31 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 19 Jul 2021 10:31:03 -0600 Subject: [PATCH 0927/1517] updating changelogs and version to 1.4.0-0.23b0 (#1965) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 6 +++++- eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 6 +++--- opentelemetry-distro/src/opentelemetry/distro/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 6 +++--- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 31 files changed, 42 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e2acbe3a6..24486c65f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: c100b21fa40727709bb31cf4e2b67b737a09ea2c + CONTRIB_REPO_SHA: 79cac1d0395f1d5cc920bfeaeb8cf04ee2a39872 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index b531ebb4d4..2b155caeea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.3.0-0.22b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.4.0-0.23b0...HEAD) + +## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-18 + + - `opentelemetry-semantic-conventions` Generate semconv constants update for OTel Spec 1.5.0 ([#1946](https://github.com/open-telemetry/opentelemetry-python/pull/1946)) diff --git a/eachdist.ini b/eachdist.ini index 23afcb1abc..546dc1e444 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -14,7 +14,7 @@ sortfirst= exporter/* [stable] -version=1.4.0.dev0 +version=1.4.0 packages= opentelemetry-sdk @@ -32,7 +32,7 @@ packages= opentelemetry-api [prerelease] -version=0.23.dev0 +version=0.23b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index c173c28d8d..f8cc727b10 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index c173c28d8d..f8cc727b10 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index faa9d40a3a..3dc9b0c5ee 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,8 +39,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.4.0.dev0 - opentelemetry-exporter-jaeger-thrift == 1.4.0.dev0 + opentelemetry-exporter-jaeger-proto-grpc == 1.4.0 + opentelemetry-exporter-jaeger-thrift == 1.4.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index c173c28d8d..f8cc727b10 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index c829b95757..7318f8dca8 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23.dev0" +__version__ = "0.23b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index a9b5c5b558..147b4869fd 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.4.0.dev0 + opentelemetry-proto == 1.4.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 48c4fb840e..e0982da0fa 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index d4a891e173..4cd76408ec 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -38,4 +38,4 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.4.0.dev0 + opentelemetry-exporter-otlp-proto-grpc == 1.4.0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 48c4fb840e..e0982da0fa 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 48c4fb840e..e0982da0fa 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index bee166fd53..507693ef4f 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.4.0.dev0 + opentelemetry-exporter-zipkin-json == 1.4.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 48c4fb840e..e0982da0fa 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 0b1a832ed7..529a0e5bc0 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.4.0.dev0 - opentelemetry-exporter-zipkin-proto-http == 1.4.0.dev0 + opentelemetry-exporter-zipkin-json == 1.4.0 + opentelemetry-exporter-zipkin-proto-http == 1.4.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 48c4fb840e..e0982da0fa 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 48c4fb840e..e0982da0fa 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 74b3436f1d..e616635e7f 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -42,8 +42,8 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api ~= 1.3 - opentelemetry-instrumentation == 0.23.dev0 - opentelemetry-sdk == 1.4.0.dev0 + opentelemetry-instrumentation == 0.23b0 + opentelemetry-sdk == 1.4.0 [options.packages.find] where = src @@ -57,4 +57,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.4.0.dev0 + opentelemetry-exporter-otlp == 1.4.0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index c829b95757..7318f8dca8 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23.dev0" +__version__ = "0.23b0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index a00673cca8..c37075bf45 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.4.0.dev0 + opentelemetry-api == 1.4.0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index c829b95757..7318f8dca8 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23.dev0" +__version__ = "0.23b0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 48c4fb840e..e0982da0fa 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 3df5e31ed4..62c7155f1a 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,9 +42,9 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.4.0.dev0 - opentelemetry-semantic-conventions == 0.23.dev0 - opentelemetry-instrumentation == 0.23.dev0 + opentelemetry-api == 1.4.0 + opentelemetry-semantic-conventions == 0.23b0 + opentelemetry-instrumentation == 0.23b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 48c4fb840e..e0982da0fa 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index c829b95757..7318f8dca8 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23.dev0" +__version__ = "0.23b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 48c4fb840e..e0982da0fa 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 48c4fb840e..e0982da0fa 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0.dev0" +__version__ = "1.4.0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 2b7db534bf..54d50f7b73 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -46,7 +46,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test == 0.23.dev0 + opentelemetry-test == 0.23b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index c829b95757..7318f8dca8 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23.dev0" +__version__ = "0.23b0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 12f7794562..08a78866dc 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.23.dev0" +__version__ = "0.23b0" From c49e6fd6417282934b2ec4320b62b0e3adbe082d Mon Sep 17 00:00:00 2001 From: Major Hayden Date: Tue, 20 Jul 2021 12:36:18 -0500 Subject: [PATCH 0928/1517] instrumentation: Add LICENSE + tests (#1974) --- opentelemetry-instrumentation/LICENSE | 201 ++++++++++++++++++++++ opentelemetry-instrumentation/MANIFEST.in | 3 +- 2 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 opentelemetry-instrumentation/LICENSE diff --git a/opentelemetry-instrumentation/LICENSE b/opentelemetry-instrumentation/LICENSE new file mode 100644 index 0000000000..1ef7dad2c5 --- /dev/null +++ b/opentelemetry-instrumentation/LICENSE @@ -0,0 +1,201 @@ + 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 The OpenTelemetry Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/opentelemetry-instrumentation/MANIFEST.in b/opentelemetry-instrumentation/MANIFEST.in index 191b7d1959..faee277146 100644 --- a/opentelemetry-instrumentation/MANIFEST.in +++ b/opentelemetry-instrumentation/MANIFEST.in @@ -1,7 +1,8 @@ -prune tests graft src +graft tests global-exclude *.pyc global-exclude *.pyo global-exclude __pycache__/* include MANIFEST.in include README.rst +include LICENSE From 38f179cf76f00f60934633cda6c79fcfd77e2dbd Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 21 Jul 2021 09:59:35 -0700 Subject: [PATCH 0929/1517] Listen for publish events (#1977) --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 66ae00849b..0921357cb3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,7 +2,7 @@ name: Publish on: release: - types: [created] + types: [published] jobs: publish: From 5779ef158809ad25772f34f4b6737c221fc9c8ec Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 21 Jul 2021 14:45:15 -0700 Subject: [PATCH 0930/1517] update changelog date (#1976) --- CHANGELOG.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b155caeea..b329633833 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.4.0-0.23b0...HEAD) -## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-18 - - -- `opentelemetry-semantic-conventions` Generate semconv constants update for OTel Spec 1.5.0 - ([#1946](https://github.com/open-telemetry/opentelemetry-python/pull/1946)) +## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-21 ### Added - Moved `opentelemetry-instrumentation` to core repository. @@ -39,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updating dependency for opentelemetry api/sdk packages to support major version instead of pinning to specific versions. ([#1933](https://github.com/open-telemetry/opentelemetry-python/pull/1933)) +- `opentelemetry-semantic-conventions` Generate semconv constants update for OTel Spec 1.5.0 + ([#1946](https://github.com/open-telemetry/opentelemetry-python/pull/1946)) ### Fixed - Updated `opentelementry-opentracing-shim` `ScopeShim` to report exceptions in From 566df33f37d28de6f84e73e56975ee4f0731e7be Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 22 Jul 2021 09:29:58 -0700 Subject: [PATCH 0931/1517] add ocelotl to maintainers list (#1980) Fixes #1979 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b8457a001a..082e770eff 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,6 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): - [Aaron Abbott](https://github.com/aabmass), Google -- [Diego Hurtado](https://github.com/ocelotl), Lightstep - [Owais Lone](https://github.com/owais), Splunk - [Srikanth Chekuri](https://github.com/lonewolf3739) @@ -140,6 +139,7 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-telemetry/teams/python-maintainers)): - [Alex Boten](https://github.com/codeboten), Lightstep +- [Diego Hurtado](https://github.com/ocelotl), Lightstep - [Leighton Chen](https://github.com/lzchen), Microsoft *For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer).* From 191a55ce0478822cb054c48495a4ed227a4579d8 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Fri, 23 Jul 2021 10:40:08 -0700 Subject: [PATCH 0932/1517] Default configurators do more for distros (#1937) --- CHANGELOG.md | 3 + opentelemetry-distro/setup.cfg | 2 +- .../src/opentelemetry/distro/__init__.py | 138 +-------------- .../sdk/_configuration/__init__.py | 160 ++++++++++++++++++ .../tests/test_configurator.py | 26 +-- 5 files changed, 181 insertions(+), 148 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py rename {opentelemetry-distro => opentelemetry-sdk}/tests/test_configurator.py (91%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b329633833..0acb5edda9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.4.0-0.23b0...HEAD) +- `opentelemetry-distro` & `opentelemetry-sdk` Moved Auto Instrumentation Configurator code to SDK + to let distros use its default implementation + ([#1937](https://github.com/open-telemetry/opentelemetry-python/pull/1937)) ## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-21 diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index e616635e7f..98644ec854 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -52,7 +52,7 @@ where = src opentelemetry_distro = distro = opentelemetry.distro:OpenTelemetryDistro opentelemetry_configurator = - configurator = opentelemetry.distro:Configurator + configurator = opentelemetry.distro:OpenTelemetryConfigurator [options.extras_require] test = diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index ba86e0e247..e70cb67335 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -13,144 +13,14 @@ # limitations under the License. # import os -from logging import getLogger -from os import environ -from typing import Sequence, Tuple -from pkg_resources import iter_entry_points - -from opentelemetry import trace -from opentelemetry.environment_variables import ( - OTEL_PYTHON_ID_GENERATOR, - OTEL_TRACES_EXPORTER, -) -from opentelemetry.instrumentation.configurator import BaseConfigurator +from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER from opentelemetry.instrumentation.distro import BaseDistro -from opentelemetry.sdk.resources import SERVICE_NAME, Resource -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter -from opentelemetry.sdk.trace.id_generator import IdGenerator - -logger = getLogger(__file__) - - -EXPORTER_OTLP = "otlp" -EXPORTER_OTLP_SPAN = "otlp_proto_grpc_span" - -RANDOM_ID_GENERATOR = "random" -_DEFAULT_ID_GENERATOR = RANDOM_ID_GENERATOR - - -def _get_id_generator() -> str: - return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR) - - -def _get_exporter_names() -> Sequence[str]: - trace_exporters = environ.get(OTEL_TRACES_EXPORTER) - - exporters = set() - - if trace_exporters and trace_exporters.lower().strip() != "none": - exporters.update( - { - trace_exporter.strip() - for trace_exporter in trace_exporters.split(",") - } - ) - - if EXPORTER_OTLP in exporters: - exporters.remove(EXPORTER_OTLP) - exporters.add(EXPORTER_OTLP_SPAN) - - return list(exporters) - - -def _init_tracing( - exporters: Sequence[SpanExporter], id_generator: IdGenerator -): - # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name - # from the env variable else defaults to "unknown_service" - provider = TracerProvider( - id_generator=id_generator(), - ) - trace.set_tracer_provider(provider) - - for _, exporter_class in exporters.items(): - exporter_args = {} - provider.add_span_processor( - BatchSpanProcessor(exporter_class(**exporter_args)) - ) +from opentelemetry.sdk._configuration import _OTelSDKConfigurator -def _import_tracer_provider_config_components( - selected_components, entry_point_name -) -> Sequence[Tuple[str, object]]: - component_entry_points = { - ep.name: ep for ep in iter_entry_points(entry_point_name) - } - component_impls = [] - for selected_component in selected_components: - entry_point = component_entry_points.get(selected_component, None) - if not entry_point: - raise RuntimeError( - "Requested component '{}' not found in entry points for '{}'".format( - selected_component, entry_point_name - ) - ) - - component_impl = entry_point.load() - component_impls.append((selected_component, component_impl)) - - return component_impls - - -def _import_exporters( - exporter_names: Sequence[str], -) -> Sequence[SpanExporter]: - trace_exporters = {} - - for ( - exporter_name, - exporter_impl, - ) in _import_tracer_provider_config_components( - exporter_names, "opentelemetry_exporter" - ): - if issubclass(exporter_impl, SpanExporter): - trace_exporters[exporter_name] = exporter_impl - else: - raise RuntimeError( - "{0} is not a trace exporter".format(exporter_name) - ) - return trace_exporters - - -def _import_id_generator(id_generator_name: str) -> IdGenerator: - # pylint: disable=unbalanced-tuple-unpacking - [ - (id_generator_name, id_generator_impl) - ] = _import_tracer_provider_config_components( - [id_generator_name.strip()], "opentelemetry_id_generator" - ) - - if issubclass(id_generator_impl, IdGenerator): - return id_generator_impl - - raise RuntimeError("{0} is not an IdGenerator".format(id_generator_name)) - - -def _initialize_components(): - exporter_names = _get_exporter_names() - trace_exporters = _import_exporters(exporter_names) - id_generator_name = _get_id_generator() - id_generator = _import_id_generator(id_generator_name) - _init_tracing(trace_exporters, id_generator) - - -class Configurator(BaseConfigurator): - - # pylint: disable=no-self-use - def _configure(self, **kwargs): - _initialize_components() +class OpenTelemetryConfigurator(_OTelSDKConfigurator): + pass class OpenTelemetryDistro(BaseDistro): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py new file mode 100644 index 0000000000..65bb92eb7a --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -0,0 +1,160 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +""" +OpenTelemetry SDK Configurator for Easy Instrumentation with Distros +""" + +from os import environ +from typing import Sequence, Tuple + +from pkg_resources import iter_entry_points + +from opentelemetry import trace +from opentelemetry.environment_variables import ( + OTEL_PYTHON_ID_GENERATOR, + OTEL_TRACES_EXPORTER, +) +from opentelemetry.instrumentation.configurator import BaseConfigurator +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter +from opentelemetry.sdk.trace.id_generator import IdGenerator + +_EXPORTER_OTLP = "otlp" +_EXPORTER_OTLP_SPAN = "otlp_proto_grpc_span" + +_RANDOM_ID_GENERATOR = "random" +_DEFAULT_ID_GENERATOR = _RANDOM_ID_GENERATOR + + +def _get_id_generator() -> str: + return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR) + + +def _get_exporter_names() -> Sequence[str]: + trace_exporters = environ.get(OTEL_TRACES_EXPORTER) + + exporters = set() + + if trace_exporters and trace_exporters.lower().strip() != "none": + exporters.update( + { + trace_exporter.strip() + for trace_exporter in trace_exporters.split(",") + } + ) + + if _EXPORTER_OTLP in exporters: + exporters.remove(_EXPORTER_OTLP) + exporters.add(_EXPORTER_OTLP_SPAN) + + return list(exporters) + + +def _init_tracing( + exporters: Sequence[SpanExporter], id_generator: IdGenerator +): + # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name + # from the env variable else defaults to "unknown_service" + provider = TracerProvider( + id_generator=id_generator(), + ) + trace.set_tracer_provider(provider) + + for _, exporter_class in exporters.items(): + exporter_args = {} + provider.add_span_processor( + BatchSpanProcessor(exporter_class(**exporter_args)) + ) + + +def _import_tracer_provider_config_components( + selected_components, entry_point_name +) -> Sequence[Tuple[str, object]]: + component_entry_points = { + ep.name: ep for ep in iter_entry_points(entry_point_name) + } + component_impls = [] + for selected_component in selected_components: + entry_point = component_entry_points.get(selected_component, None) + if not entry_point: + raise RuntimeError( + "Requested component '{}' not found in entry points for '{}'".format( + selected_component, entry_point_name + ) + ) + + component_impl = entry_point.load() + component_impls.append((selected_component, component_impl)) + + return component_impls + + +def _import_exporters( + exporter_names: Sequence[str], +) -> Sequence[SpanExporter]: + trace_exporters = {} + + for ( + exporter_name, + exporter_impl, + ) in _import_tracer_provider_config_components( + exporter_names, "opentelemetry_exporter" + ): + if issubclass(exporter_impl, SpanExporter): + trace_exporters[exporter_name] = exporter_impl + else: + raise RuntimeError( + "{0} is not a trace exporter".format(exporter_name) + ) + return trace_exporters + + +def _import_id_generator(id_generator_name: str) -> IdGenerator: + # pylint: disable=unbalanced-tuple-unpacking + [ + (id_generator_name, id_generator_impl) + ] = _import_tracer_provider_config_components( + [id_generator_name.strip()], "opentelemetry_id_generator" + ) + + if issubclass(id_generator_impl, IdGenerator): + return id_generator_impl + + raise RuntimeError("{0} is not an IdGenerator".format(id_generator_name)) + + +def _initialize_components(): + exporter_names = _get_exporter_names() + trace_exporters = _import_exporters(exporter_names) + id_generator_name = _get_id_generator() + id_generator = _import_id_generator(id_generator_name) + _init_tracing(trace_exporters, id_generator) + + +class _OTelSDKConfigurator(BaseConfigurator): + """A basic Configurator by OTel Python for initalizing OTel SDK components + + Initializes several crucial OTel SDK components (i.e. TracerProvider, + MeterProvider, Processors...) according to a default implementation. Other + Configurators can subclass and slightly alter this initialization. + + NOTE: This class should not be instantiated nor should it become an entry + point on the `opentelemetry-sdk` package. Instead, distros should subclass + this Configurator and enchance it as needed. + """ + + def _configure(self, **kwargs): + _initialize_components() diff --git a/opentelemetry-distro/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py similarity index 91% rename from opentelemetry-distro/tests/test_configurator.py rename to opentelemetry-sdk/tests/test_configurator.py index b2d2cb46d4..e7619d64dc 100644 --- a/opentelemetry-distro/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -18,18 +18,18 @@ from unittest.mock import patch from opentelemetry import trace -from opentelemetry.distro import ( - EXPORTER_OTLP, - EXPORTER_OTLP_SPAN, +from opentelemetry.environment_variables import ( + OTEL_PYTHON_ID_GENERATOR, + OTEL_TRACES_EXPORTER, +) +from opentelemetry.sdk._configuration import ( + _EXPORTER_OTLP, + _EXPORTER_OTLP_SPAN, _get_exporter_names, _get_id_generator, _import_id_generator, _init_tracing, ) -from opentelemetry.environment_variables import ( - OTEL_PYTHON_ID_GENERATOR, - OTEL_TRACES_EXPORTER, -) from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator @@ -87,10 +87,10 @@ class TestTraceInit(TestCase): def setUp(self): super() self.get_provider_patcher = patch( - "opentelemetry.distro.TracerProvider", Provider + "opentelemetry.sdk._configuration.TracerProvider", Provider ) self.get_processor_patcher = patch( - "opentelemetry.distro.BatchSpanProcessor", Processor + "opentelemetry.sdk._configuration.BatchSpanProcessor", Processor ) self.set_provider_patcher = patch( "opentelemetry.trace.set_tracer_provider" @@ -143,8 +143,8 @@ def test_trace_init_otlp(self): ) @patch.dict(environ, {OTEL_PYTHON_ID_GENERATOR: "custom_id_generator"}) - @patch("opentelemetry.distro.IdGenerator", new=IdGenerator) - @patch("opentelemetry.distro.iter_entry_points") + @patch("opentelemetry.sdk._configuration.IdGenerator", new=IdGenerator) + @patch("opentelemetry.sdk._configuration.iter_entry_points") def test_trace_init_custom_id_generator(self, mock_iter_entry_points): mock_iter_entry_points.configure_mock( return_value=[ @@ -160,9 +160,9 @@ def test_trace_init_custom_id_generator(self, mock_iter_entry_points): class TestExporterNames(TestCase): def test_otlp_exporter_overwrite(self): - for exporter in [EXPORTER_OTLP, EXPORTER_OTLP_SPAN]: + for exporter in [_EXPORTER_OTLP, _EXPORTER_OTLP_SPAN]: with patch.dict(environ, {OTEL_TRACES_EXPORTER: exporter}): - self.assertEqual(_get_exporter_names(), [EXPORTER_OTLP_SPAN]) + self.assertEqual(_get_exporter_names(), [_EXPORTER_OTLP_SPAN]) @patch.dict(environ, {OTEL_TRACES_EXPORTER: "jaeger,zipkin"}) def test_multiple_exporters(self): From c6a0e3eeac3adb852910e2da4b48f8d177eb532d Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 26 Jul 2021 14:48:36 -0700 Subject: [PATCH 0933/1517] fix opentelemetry-bootstrap bug (#1987) * fix opentelemetry-bootstrap bug * Update CHANGELOG.md --- CHANGELOG.md | 6 ++ .../instrumentation/bootstrap_gen.py | 66 +++++++++---------- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0acb5edda9..360d8860c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 to let distros use its default implementation ([#1937](https://github.com/open-telemetry/opentelemetry-python/pull/1937)) +## [0.23.1](https://github.com/open-telemetry/opentelemetry-python/pull/1987) - 2021-07-26 + +### Changed +- Fix opentelemetry-bootstrap dependency script. + ([#1987](https://github.com/open-telemetry/opentelemetry-python/pull/1987)) + ## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-21 ### Added diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index b49f40905f..c9ea34297a 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -18,121 +18,121 @@ libraries = { "aiohttp": { "library": "aiohttp ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.23b0", }, "aiopg": { "library": "aiopg >= 0.13.0, < 1.3.0", - "instrumentation": "opentelemetry-instrumentation-aiopg==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-aiopg==0.23b0", }, "asgiref": { "library": "asgiref ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-asgi==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-asgi==0.23b0", }, "asyncpg": { "library": "asyncpg >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-asyncpg==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-asyncpg==0.23b0", }, "boto": { "library": "boto~=2.0", - "instrumentation": "opentelemetry-instrumentation-boto==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-boto==0.23b0", }, "botocore": { "library": "botocore ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-botocore==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-botocore==0.23b0", }, "celery": { "library": "celery >= 4.0, < 6.0", - "instrumentation": "opentelemetry-instrumentation-celery==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-celery==0.23b0", }, "django": { "library": "django >= 1.10", - "instrumentation": "opentelemetry-instrumentation-django==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-django==0.23b0", }, "elasticsearch": { "library": "elasticsearch >= 2.0", - "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.23b0", }, "falcon": { "library": "falcon ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-falcon==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-falcon==0.23b0", }, "fastapi": { "library": "fastapi ~= 0.58.1", - "instrumentation": "opentelemetry-instrumentation-fastapi==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-fastapi==0.23b0", }, "flask": { "library": "flask >= 1.0, < 3.0", - "instrumentation": "opentelemetry-instrumentation-flask==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-flask==0.23b0", }, "grpcio": { "library": "grpcio ~= 1.27", - "instrumentation": "opentelemetry-instrumentation-grpc==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-grpc==0.23b0", }, "httpx": { "library": "httpx >= 0.18.0, < 0.19.0", - "instrumentation": "opentelemetry-instrumentation-httpx==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-httpx==0.23b0", }, "jinja2": { "library": "jinja2~=2.7", - "instrumentation": "opentelemetry-instrumentation-jinja2==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-jinja2==0.23b0", }, "mysql-connector-python": { "library": "mysql-connector-python ~= 8.0", - "instrumentation": "opentelemetry-instrumentation-mysql==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-mysql==0.23b0", }, "psycopg2": { "library": "psycopg2 >= 2.7.3.1", - "instrumentation": "opentelemetry-instrumentation-psycopg2==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-psycopg2==0.23b0", }, "pymemcache": { "library": "pymemcache ~= 1.3", - "instrumentation": "opentelemetry-instrumentation-pymemcache==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-pymemcache==0.23b0", }, "pymongo": { "library": "pymongo ~= 3.1", - "instrumentation": "opentelemetry-instrumentation-pymongo==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-pymongo==0.23b0", }, "PyMySQL": { "library": "PyMySQL ~= 0.10.1", - "instrumentation": "opentelemetry-instrumentation-pymysql==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-pymysql==0.23b0", }, "pyramid": { "library": "pyramid >= 1.7", - "instrumentation": "opentelemetry-instrumentation-pyramid==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-pyramid==0.23b0", }, "redis": { "library": "redis >= 2.6", - "instrumentation": "opentelemetry-instrumentation-redis==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-redis==0.23b0", }, "requests": { "library": "requests ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-requests==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-requests==0.23b0", }, "scikit-learn": { "library": "scikit-learn ~= 0.24.0", - "instrumentation": "opentelemetry-instrumentation-sklearn==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-sklearn==0.23b0", }, "sqlalchemy": { "library": "sqlalchemy", - "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.23b0", }, "starlette": { "library": "starlette ~= 0.13.0", - "instrumentation": "opentelemetry-instrumentation-starlette==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-starlette==0.23b0", }, "tornado": { "library": "tornado >= 6.0", - "instrumentation": "opentelemetry-instrumentation-tornado==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-tornado==0.23b0", }, "urllib3": { "library": "urllib3 >= 1.0.0, < 2.0.0", - "instrumentation": "opentelemetry-instrumentation-urllib3==0.23.dev0", + "instrumentation": "opentelemetry-instrumentation-urllib3==0.23b0", }, } default_instrumentations = [ - "opentelemetry-instrumentation-dbapi==0.23.dev0", - "opentelemetry-instrumentation-logging==0.23.dev0", - "opentelemetry-instrumentation-sqlite3==0.23.dev0", - "opentelemetry-instrumentation-urllib==0.23.dev0", - "opentelemetry-instrumentation-wsgi==0.23.dev0", + "opentelemetry-instrumentation-dbapi==0.23b0", + "opentelemetry-instrumentation-logging==0.23b0", + "opentelemetry-instrumentation-sqlite3==0.23b0", + "opentelemetry-instrumentation-urllib==0.23b0", + "opentelemetry-instrumentation-wsgi==0.23b0", ] From 39927787cc911af8567731728a37afc24f935d1e Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 27 Jul 2021 22:56:40 +0530 Subject: [PATCH 0934/1517] Back to develop after 1.4.0 release (#1985) * Back to develop after 1.4.0 release * update * Update SHA * Update missed files * Update distro version --- .github/workflows/test.yml | 2 +- eachdist.ini | 4 +- .../exporter/jaeger/proto/grpc/version.py | 2 +- .../exporter/jaeger/thrift/version.py | 2 +- .../opentelemetry-exporter-jaeger/setup.cfg | 4 +- .../opentelemetry/exporter/jaeger/version.py | 2 +- .../exporter/opencensus/version.py | 2 +- .../setup.cfg | 2 +- .../exporter/otlp/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-otlp/setup.cfg | 2 +- .../opentelemetry/exporter/otlp/version.py | 2 +- .../exporter/zipkin/json/version.py | 2 +- .../setup.cfg | 2 +- .../exporter/zipkin/proto/http/version.py | 2 +- .../opentelemetry-exporter-zipkin/setup.cfg | 4 +- .../opentelemetry/exporter/zipkin/version.py | 2 +- .../src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 6 +- .../src/opentelemetry/distro/version.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 +- .../instrumentation/bootstrap_gen.py | 66 +++++++++---------- .../opentelemetry/instrumentation/version.py | 2 +- .../src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 6 +- .../src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../opentelemetry/propagators/b3/version.py | 2 +- .../propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../shim/opentracing_shim/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 31 files changed, 70 insertions(+), 70 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 24486c65f3..ebb36ee8a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 79cac1d0395f1d5cc920bfeaeb8cf04ee2a39872 + CONTRIB_REPO_SHA: 64996996451f9fab16548febe4707883935428d1 jobs: build: diff --git a/eachdist.ini b/eachdist.ini index 546dc1e444..788c1c48c2 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -14,7 +14,7 @@ sortfirst= exporter/* [stable] -version=1.4.0 +version=1.5.0.dev0 packages= opentelemetry-sdk @@ -32,7 +32,7 @@ packages= opentelemetry-api [prerelease] -version=0.23b0 +version=0.24.dev0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index f8cc727b10..38b21f37cb 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index f8cc727b10..38b21f37cb 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0.dev0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 3dc9b0c5ee..b01b3a8d9e 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,8 +39,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.4.0 - opentelemetry-exporter-jaeger-thrift == 1.4.0 + opentelemetry-exporter-jaeger-proto-grpc == 1.5.0.dev0 + opentelemetry-exporter-jaeger-thrift == 1.5.0.dev0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index f8cc727b10..38b21f37cb 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0.dev0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 7318f8dca8..5aeaec6556 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23b0" +__version__ = "0.24.dev0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 147b4869fd..46ed5650e3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.4.0 + opentelemetry-proto == 1.5.0.dev0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index e0982da0fa..36cd1d2223 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0.dev0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 4cd76408ec..bd5c2aa2f5 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -38,4 +38,4 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.4.0 + opentelemetry-exporter-otlp-proto-grpc == 1.5.0.dev0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index e0982da0fa..36cd1d2223 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index e0982da0fa..36cd1d2223 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 507693ef4f..c7828ae496 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.4.0 + opentelemetry-exporter-zipkin-json == 1.5.0.dev0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index e0982da0fa..36cd1d2223 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0.dev0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 529a0e5bc0..6d2f97404a 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.4.0 - opentelemetry-exporter-zipkin-proto-http == 1.4.0 + opentelemetry-exporter-zipkin-json == 1.5.0.dev0 + opentelemetry-exporter-zipkin-proto-http == 1.5.0.dev0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index e0982da0fa..36cd1d2223 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0.dev0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index e0982da0fa..36cd1d2223 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0.dev0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 98644ec854..4819b09730 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -42,8 +42,8 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api ~= 1.3 - opentelemetry-instrumentation == 0.23b0 - opentelemetry-sdk == 1.4.0 + opentelemetry-instrumentation == 0.24.dev0 + opentelemetry-sdk == 1.5.0.dev0 [options.packages.find] where = src @@ -57,4 +57,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.4.0 + opentelemetry-exporter-otlp == 1.5.0.dev0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index 7318f8dca8..5aeaec6556 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23b0" +__version__ = "0.24.dev0" diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index c37075bf45..2ceb43c727 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.4.0 + opentelemetry-api == 1.5.0.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index c9ea34297a..799c5b865c 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -18,121 +18,121 @@ libraries = { "aiohttp": { "library": "aiohttp ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.23b0", + "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.24.dev0", }, "aiopg": { "library": "aiopg >= 0.13.0, < 1.3.0", - "instrumentation": "opentelemetry-instrumentation-aiopg==0.23b0", + "instrumentation": "opentelemetry-instrumentation-aiopg==0.24.dev0", }, "asgiref": { "library": "asgiref ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-asgi==0.23b0", + "instrumentation": "opentelemetry-instrumentation-asgi==0.24.dev0", }, "asyncpg": { "library": "asyncpg >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-asyncpg==0.23b0", + "instrumentation": "opentelemetry-instrumentation-asyncpg==0.24.dev0", }, "boto": { "library": "boto~=2.0", - "instrumentation": "opentelemetry-instrumentation-boto==0.23b0", + "instrumentation": "opentelemetry-instrumentation-boto==0.24.dev0", }, "botocore": { "library": "botocore ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-botocore==0.23b0", + "instrumentation": "opentelemetry-instrumentation-botocore==0.24.dev0", }, "celery": { "library": "celery >= 4.0, < 6.0", - "instrumentation": "opentelemetry-instrumentation-celery==0.23b0", + "instrumentation": "opentelemetry-instrumentation-celery==0.24.dev0", }, "django": { "library": "django >= 1.10", - "instrumentation": "opentelemetry-instrumentation-django==0.23b0", + "instrumentation": "opentelemetry-instrumentation-django==0.24.dev0", }, "elasticsearch": { "library": "elasticsearch >= 2.0", - "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.23b0", + "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.24.dev0", }, "falcon": { "library": "falcon ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-falcon==0.23b0", + "instrumentation": "opentelemetry-instrumentation-falcon==0.24.dev0", }, "fastapi": { "library": "fastapi ~= 0.58.1", - "instrumentation": "opentelemetry-instrumentation-fastapi==0.23b0", + "instrumentation": "opentelemetry-instrumentation-fastapi==0.24.dev0", }, "flask": { "library": "flask >= 1.0, < 3.0", - "instrumentation": "opentelemetry-instrumentation-flask==0.23b0", + "instrumentation": "opentelemetry-instrumentation-flask==0.24.dev0", }, "grpcio": { "library": "grpcio ~= 1.27", - "instrumentation": "opentelemetry-instrumentation-grpc==0.23b0", + "instrumentation": "opentelemetry-instrumentation-grpc==0.24.dev0", }, "httpx": { "library": "httpx >= 0.18.0, < 0.19.0", - "instrumentation": "opentelemetry-instrumentation-httpx==0.23b0", + "instrumentation": "opentelemetry-instrumentation-httpx==0.24.dev0", }, "jinja2": { "library": "jinja2~=2.7", - "instrumentation": "opentelemetry-instrumentation-jinja2==0.23b0", + "instrumentation": "opentelemetry-instrumentation-jinja2==0.24.dev0", }, "mysql-connector-python": { "library": "mysql-connector-python ~= 8.0", - "instrumentation": "opentelemetry-instrumentation-mysql==0.23b0", + "instrumentation": "opentelemetry-instrumentation-mysql==0.24.dev0", }, "psycopg2": { "library": "psycopg2 >= 2.7.3.1", - "instrumentation": "opentelemetry-instrumentation-psycopg2==0.23b0", + "instrumentation": "opentelemetry-instrumentation-psycopg2==0.24.dev0", }, "pymemcache": { "library": "pymemcache ~= 1.3", - "instrumentation": "opentelemetry-instrumentation-pymemcache==0.23b0", + "instrumentation": "opentelemetry-instrumentation-pymemcache==0.24.dev0", }, "pymongo": { "library": "pymongo ~= 3.1", - "instrumentation": "opentelemetry-instrumentation-pymongo==0.23b0", + "instrumentation": "opentelemetry-instrumentation-pymongo==0.24.dev0", }, "PyMySQL": { "library": "PyMySQL ~= 0.10.1", - "instrumentation": "opentelemetry-instrumentation-pymysql==0.23b0", + "instrumentation": "opentelemetry-instrumentation-pymysql==0.24.dev0", }, "pyramid": { "library": "pyramid >= 1.7", - "instrumentation": "opentelemetry-instrumentation-pyramid==0.23b0", + "instrumentation": "opentelemetry-instrumentation-pyramid==0.24.dev0", }, "redis": { "library": "redis >= 2.6", - "instrumentation": "opentelemetry-instrumentation-redis==0.23b0", + "instrumentation": "opentelemetry-instrumentation-redis==0.24.dev0", }, "requests": { "library": "requests ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-requests==0.23b0", + "instrumentation": "opentelemetry-instrumentation-requests==0.24.dev0", }, "scikit-learn": { "library": "scikit-learn ~= 0.24.0", - "instrumentation": "opentelemetry-instrumentation-sklearn==0.23b0", + "instrumentation": "opentelemetry-instrumentation-sklearn==0.24.dev0", }, "sqlalchemy": { "library": "sqlalchemy", - "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.23b0", + "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.24.dev0", }, "starlette": { "library": "starlette ~= 0.13.0", - "instrumentation": "opentelemetry-instrumentation-starlette==0.23b0", + "instrumentation": "opentelemetry-instrumentation-starlette==0.24.dev0", }, "tornado": { "library": "tornado >= 6.0", - "instrumentation": "opentelemetry-instrumentation-tornado==0.23b0", + "instrumentation": "opentelemetry-instrumentation-tornado==0.24.dev0", }, "urllib3": { "library": "urllib3 >= 1.0.0, < 2.0.0", - "instrumentation": "opentelemetry-instrumentation-urllib3==0.23b0", + "instrumentation": "opentelemetry-instrumentation-urllib3==0.24.dev0", }, } default_instrumentations = [ - "opentelemetry-instrumentation-dbapi==0.23b0", - "opentelemetry-instrumentation-logging==0.23b0", - "opentelemetry-instrumentation-sqlite3==0.23b0", - "opentelemetry-instrumentation-urllib==0.23b0", - "opentelemetry-instrumentation-wsgi==0.23b0", + "opentelemetry-instrumentation-dbapi==0.24.dev0", + "opentelemetry-instrumentation-logging==0.24.dev0", + "opentelemetry-instrumentation-sqlite3==0.24.dev0", + "opentelemetry-instrumentation-urllib==0.24.dev0", + "opentelemetry-instrumentation-wsgi==0.24.dev0", ] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 7318f8dca8..5aeaec6556 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23b0" +__version__ = "0.24.dev0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index e0982da0fa..36cd1d2223 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0.dev0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 62c7155f1a..e8cee4d6b7 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,9 +42,9 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.4.0 - opentelemetry-semantic-conventions == 0.23b0 - opentelemetry-instrumentation == 0.23b0 + opentelemetry-api == 1.5.0.dev0 + opentelemetry-semantic-conventions == 0.24.dev0 + opentelemetry-instrumentation == 0.24.dev0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index e0982da0fa..36cd1d2223 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0.dev0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 7318f8dca8..5aeaec6556 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23b0" +__version__ = "0.24.dev0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index e0982da0fa..36cd1d2223 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0.dev0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index e0982da0fa..36cd1d2223 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0.dev0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 54d50f7b73..b172898600 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -46,7 +46,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test == 0.23b0 + opentelemetry-test == 0.24.dev0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 7318f8dca8..5aeaec6556 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23b0" +__version__ = "0.24.dev0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 08a78866dc..668994320e 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.23b0" +__version__ = "0.24.dev0" From 9d55e64bd2bbfccb99e4ad478e36ad68964dacc3 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 29 Jul 2021 23:04:45 +0530 Subject: [PATCH 0935/1517] Add support for OTLP Exporter Protobuf over HTTP (#1868) --- CHANGELOG.md | 2 + eachdist.ini | 2 + .../LICENSE | 201 +++++++++ .../MANIFEST.in | 9 + .../README.rst | 25 ++ .../setup.cfg | 57 +++ .../setup.py | 33 ++ .../exporter/otlp/proto/http/__init__.py | 78 ++++ .../exporter/otlp/proto/http/py.typed | 0 .../proto/http/trace_exporter/__init__.py | 185 ++++++++ .../http/trace_exporter/encoder/__init__.py | 302 +++++++++++++ .../exporter/otlp/proto/http/version.py | 15 + .../tests/test_proto_span_exporter.py | 147 +++++++ .../tests/test_protobuf_encoder.py | 396 ++++++++++++++++++ .../opentelemetry-exporter-otlp/README.rst | 3 +- .../tests/docker-compose.yml | 3 +- .../tests/otlpexporter/__init__.py | 48 +++ .../test_otlp_grpc_exporter_functional.py | 35 +- .../test_otlp_http_exporter_functional.py | 37 ++ tox.ini | 11 + 20 files changed, 1559 insertions(+), 30 deletions(-) create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/LICENSE create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/README.rst create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/setup.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/py.typed create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py create mode 100644 tests/opentelemetry-docker-tests/tests/otlpexporter/__init__.py create mode 100644 tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_http_exporter_functional.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 360d8860c9..359ad7d787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Moved `opentelemetry-instrumentation` to core repository. ([#1959](https://github.com/open-telemetry/opentelemetry-python/pull/1959)) +- Add support for OTLP Exporter Protobuf over HTTP + ([#1868](https://github.com/open-telemetry/opentelemetry-python/pull/1868)) - Dropped attributes/events/links count available exposed on ReadableSpans. ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) - Added dropped count to otlp, jaeger and zipkin exporters. diff --git a/eachdist.ini b/eachdist.ini index 788c1c48c2..bb62be66c8 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -25,6 +25,7 @@ packages= opentelemetry-exporter-zipkin-json opentelemetry-exporter-zipkin opentelemetry-exporter-otlp-proto-grpc + opentelemetry-exporter-otlp-proto-http opentelemetry-exporter-otlp opentelemetry-exporter-jaeger-thrift opentelemetry-exporter-jaeger-proto-grpc @@ -52,6 +53,7 @@ packages= opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc + opentelemetry-exporter-otlp-proto-http opentelemetry-exporter-otlp [lintroots] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/LICENSE b/exporter/opentelemetry-exporter-otlp-proto-http/LICENSE new file mode 100644 index 0000000000..1ef7dad2c5 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/LICENSE @@ -0,0 +1,201 @@ + 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 The OpenTelemetry Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/MANIFEST.in b/exporter/opentelemetry-exporter-otlp-proto-http/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/README.rst b/exporter/opentelemetry-exporter-otlp-proto-http/README.rst new file mode 100644 index 0000000000..394b4cf5e5 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/README.rst @@ -0,0 +1,25 @@ +OpenTelemetry Collector Protobuf over HTTP Exporter +=================================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp-proto-http.svg + :target: https://pypi.org/project/opentelemetry-exporter-otlp-proto-http/ + +This library allows to export data to the OpenTelemetry Collector using the OpenTelemetry Protocol using Protobuf over HTTP. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-otlp-proto-http + + +References +---------- + +* `OpenTelemetry Collector Exporter `_ +* `OpenTelemetry Collector `_ +* `OpenTelemetry `_ +* `OpenTelemetry Protocol Specification `_ diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg new file mode 100644 index 0000000000..c99b806e98 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -0,0 +1,57 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-exporter-otlp-proto-http +description = OpenTelemetry Collector Protobuf over HTTP Exporter +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-http +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.6 +package_dir= + =src +packages=find_namespace: +install_requires = + requests ~= 2.7 + googleapis-common-protos ~= 1.52 + opentelemetry-api ~= 1.3 + opentelemetry-sdk ~= 1.3 + opentelemetry-proto == 1.5.0.dev0 + backoff ~= 1.10.0 + +[options.extras_require] +test = + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_exporter = + otlp_proto_http_span = opentelemetry.exporter.otlp.proto.http.trace_exporter:OTLPSpanExporter diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.py b/exporter/opentelemetry-exporter-otlp-proto-http/setup.py new file mode 100644 index 0000000000..510eceba6c --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.py @@ -0,0 +1,33 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "exporter", + "otlp", + "proto", + "http", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py new file mode 100644 index 0000000000..08b0725835 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py @@ -0,0 +1,78 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +""" +This library allows to export tracing data to an OTLP collector. + +Usage +----- + +The **OTLP Span Exporter** allows to export `OpenTelemetry`_ traces to the +`OTLP`_ collector. + +You can configure the exporter with the following environment variables: + +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_HEADERS` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_COMPRESSION` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE` +- :envvar:`OTEL_EXPORTER_OTLP_TIMEOUT` +- :envvar:`OTEL_EXPORTER_OTLP_PROTOCOL` +- :envvar:`OTEL_EXPORTER_OTLP_HEADERS` +- :envvar:`OTEL_EXPORTER_OTLP_ENDPOINT` +- :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` +- :envvar:`OTEL_EXPORTER_OTLP_CERTIFICATE` + +.. _OTLP: https://github.com/open-telemetry/opentelemetry-collector/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + +.. code:: python + + from opentelemetry import trace + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + from opentelemetry.sdk.resources import Resource + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + + # Resource can be required for some backends, e.g. Jaeger + # If resource wouldn't be set - traces wouldn't appears in Jaeger + resource = Resource(attributes={ + "service.name": "service" + }) + + trace.set_tracer_provider(TracerProvider(resource=resource)) + tracer = trace.get_tracer(__name__) + + otlp_exporter = OTLPSpanExporter() + + span_processor = BatchSpanProcessor(otlp_exporter) + + trace.get_tracer_provider().add_span_processor(span_processor) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + +API +--- +""" +import enum + + +class Compression(enum.Enum): + NoCompression = "none" + Deflate = "deflate" + Gzip = "gzip" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/py.typed b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py new file mode 100644 index 0000000000..061484d0e3 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -0,0 +1,185 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import gzip +import logging +import zlib +from io import BytesIO +from os import environ +from typing import Dict, Optional +from time import sleep + +import requests +from backoff import expo + +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT, +) +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import ( + _ProtobufEncoder, +) + + +_logger = logging.getLogger(__name__) + + +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:55681/v1/traces" +DEFAULT_TIMEOUT = 10 # in seconds + + +class OTLPSpanExporter(SpanExporter): + + _MAX_RETRY_TIMEOUT = 64 + + def __init__( + self, + endpoint: Optional[str] = None, + certificate_file: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + timeout: Optional[int] = None, + compression: Optional[Compression] = None, + ): + self._endpoint = endpoint or environ.get( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT), + ) + self._certificate_file = certificate_file or environ.get( + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), + ) + self._headers = headers or _headers_from_env() + self._timeout = timeout or int( + environ.get( + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), + ) + ) + self._compression = compression or _compression_from_env() + self._session = requests.Session() + self._session.headers.update(self._headers) + self._session.headers.update( + {"Content-Type": _ProtobufEncoder._CONTENT_TYPE} + ) + if self._compression is not Compression.NoCompression: + self._session.headers.update( + {"Content-Encoding": self._compression.value} + ) + self._shutdown = False + + def _export(self, serialized_data: str): + data = serialized_data + if self._compression == Compression.Gzip: + gzip_data = BytesIO() + with gzip.GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: + gzip_stream.write(serialized_data) + data = gzip_data.getvalue() + elif self._compression == Compression.Deflate: + data = zlib.compress(bytes(serialized_data)) + + return self._session.post( + url=self._endpoint, + data=data, + verify=self._certificate_file, + timeout=self._timeout, + ) + + @staticmethod + def _retryable(resp: requests.Response) -> bool: + if resp.status_code == 408: + return True + if resp.status_code >= 500 and resp.status_code <= 599: + return True + return False + + def export(self, spans) -> SpanExportResult: + # After the call to Shutdown subsequent calls to Export are + # not allowed and should return a Failure result. + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring batch") + return SpanExportResult.FAILURE + + serialized_data = _ProtobufEncoder.serialize(spans) + + for delay in expo(max_value=self._MAX_RETRY_TIMEOUT): + + if delay == self._MAX_RETRY_TIMEOUT: + return SpanExportResult.FAILURE + + resp = self._export(serialized_data) + # pylint: disable=no-else-return + if resp.status_code in (200, 202): + return SpanExportResult.SUCCESS + elif self._retryable(resp): + _logger.debug( + "Waiting %ss before retrying export of span", delay + ) + sleep(delay) + continue + else: + _logger.warning( + "Failed to export batch code: %s, reason: %s", + resp.status_code, + resp.text, + ) + return SpanExportResult.FAILURE + return SpanExportResult.FAILURE + + def shutdown(self): + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring call") + return + self._session.close() + self._shutdown = True + + +def _headers_from_env() -> Optional[Dict[str, str]]: + headers_str = environ.get( + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + environ.get(OTEL_EXPORTER_OTLP_HEADERS), + ) + headers = {} + if headers_str: + for header in headers_str.split(","): + try: + header_name, header_value = header.split("=") + headers[header_name.strip()] = header_value.strip() + except ValueError: + _logger.warning( + "Skipped invalid OTLP exporter header: %r", header + ) + return headers + + +def _compression_from_env() -> Compression: + compression = ( + environ.get( + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), + ) + .lower() + .strip() + ) + return Compression(compression) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py new file mode 100644 index 0000000000..7a5adad7b3 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py @@ -0,0 +1,302 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from collections import abc +from typing import Any, List, Optional, Sequence, Text + +from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( + ExportTraceServiceRequest as PB2ExportTraceServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue +from opentelemetry.proto.common.v1.common_pb2 import ( + ArrayValue as PB2ArrayValue, +) +from opentelemetry.proto.common.v1.common_pb2 import ( + InstrumentationLibrary as PB2InstrumentationLibrary, +) +from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as PB2Resource, +) +from opentelemetry.proto.trace.v1.trace_pb2 import ( + InstrumentationLibrarySpans as PB2InstrumentationLibrarySpans, +) +from opentelemetry.proto.trace.v1.trace_pb2 import ( + ResourceSpans as PB2ResourceSpans, +) +from opentelemetry.proto.trace.v1.trace_pb2 import Span as PB2SPan +from opentelemetry.proto.trace.v1.trace_pb2 import Status as PB2Status +from opentelemetry.sdk.trace import Event +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.trace import Resource +from opentelemetry.sdk.trace import Span as SDKSpan +from opentelemetry.trace import Link +from opentelemetry.trace import SpanKind +from opentelemetry.trace.span import SpanContext, TraceState, Status +from opentelemetry.trace.status import StatusCode +from opentelemetry.util.types import Attributes + +# pylint: disable=E1101 +_SPAN_KIND_MAP = { + SpanKind.INTERNAL: PB2SPan.SpanKind.SPAN_KIND_INTERNAL, + SpanKind.SERVER: PB2SPan.SpanKind.SPAN_KIND_SERVER, + SpanKind.CLIENT: PB2SPan.SpanKind.SPAN_KIND_CLIENT, + SpanKind.PRODUCER: PB2SPan.SpanKind.SPAN_KIND_PRODUCER, + SpanKind.CONSUMER: PB2SPan.SpanKind.SPAN_KIND_CONSUMER, +} + +_logger = logging.getLogger(__name__) + + +class _ProtobufEncoder: + _CONTENT_TYPE = "application/x-protobuf" + + @classmethod + def serialize(cls, sdk_spans: Sequence[SDKSpan]) -> str: + return cls.encode(sdk_spans).SerializeToString() + + @staticmethod + def encode(sdk_spans: Sequence[SDKSpan]) -> PB2ExportTraceServiceRequest: + return PB2ExportTraceServiceRequest( + resource_spans=_encode_resource_spans(sdk_spans) + ) + + +def _encode_resource_spans( + sdk_spans: Sequence[SDKSpan], +) -> List[PB2ResourceSpans]: + # We need to inspect the spans and group + structure them as: + # + # Resource + # Instrumentation Library + # Spans + # + # First loop organizes the SDK spans in this structure. Protobuf messages + # are not hashable so we stick with SDK data in this phase. + # + # Second loop encodes the data into Protobuf format. + # + sdk_resource_spans = {} + + for sdk_span in sdk_spans: + sdk_resource = sdk_span.resource + sdk_instrumentation = sdk_span.instrumentation_info or None + pb2_span = _encode_span(sdk_span) + + if sdk_resource not in sdk_resource_spans.keys(): + sdk_resource_spans[sdk_resource] = { + sdk_instrumentation: [pb2_span] + } + elif ( + sdk_instrumentation not in sdk_resource_spans[sdk_resource].keys() + ): + sdk_resource_spans[sdk_resource][sdk_instrumentation] = [pb2_span] + else: + sdk_resource_spans[sdk_resource][sdk_instrumentation].append( + pb2_span + ) + + pb2_resource_spans = [] + + for sdk_resource, sdk_instrumentations in sdk_resource_spans.items(): + instrumentation_library_spans = [] + for sdk_instrumentation, pb2_spans in sdk_instrumentations.items(): + instrumentation_library_spans.append( + PB2InstrumentationLibrarySpans( + instrumentation_library=( + _encode_instrumentation_library(sdk_instrumentation) + ), + spans=pb2_spans, + ) + ) + pb2_resource_spans.append( + PB2ResourceSpans( + resource=_encode_resource(sdk_resource), + instrumentation_library_spans=instrumentation_library_spans, + ) + ) + + return pb2_resource_spans + + +def _encode_span(sdk_span: SDKSpan) -> PB2SPan: + span_context = sdk_span.get_span_context() + return PB2SPan( + trace_id=_encode_trace_id(span_context.trace_id), + span_id=_encode_span_id(span_context.span_id), + trace_state=_encode_trace_state(span_context.trace_state), + parent_span_id=_encode_parent_id(sdk_span.parent), + name=sdk_span.name, + kind=_SPAN_KIND_MAP[sdk_span.kind], + start_time_unix_nano=sdk_span.start_time, + end_time_unix_nano=sdk_span.end_time, + attributes=_encode_attributes(sdk_span.attributes), + events=_encode_events(sdk_span.events), + links=_encode_links(sdk_span.links), + status=_encode_status(sdk_span.status), + ) + + +def _encode_events( + events: Sequence[Event], +) -> Optional[List[PB2SPan.Event]]: + pb2_events = None + if events: + pb2_events = [] + for event in events: + encoded_event = PB2SPan.Event( + name=event.name, + time_unix_nano=event.timestamp, + ) + for key, value in event.attributes.items(): + try: + encoded_event.attributes.append( + _encode_key_value(key, value) + ) + # pylint: disable=broad-except + except Exception as error: + _logger.exception(error) + pb2_events.append(encoded_event) + return pb2_events + + +def _encode_links(links: List[Link]) -> List[PB2SPan.Link]: + pb2_links = None + if links: + pb2_links = [] + for link in links: + encoded_link = PB2SPan.Link( + trace_id=_encode_trace_id(link.context.trace_id), + span_id=_encode_span_id(link.context.span_id), + ) + for key, value in link.attributes.items(): + try: + encoded_link.attributes.append( + _encode_key_value(key, value) + ) + # pylint: disable=broad-except + except Exception as error: + _logger.exception(error) + pb2_links.append(encoded_link) + return pb2_links + + +def _encode_status(status: Status) -> Optional[PB2Status]: + pb2_status = None + if status is not None: + deprecated_code = PB2Status.DEPRECATED_STATUS_CODE_OK + if status.status_code is StatusCode.ERROR: + deprecated_code = PB2Status.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR + pb2_status = PB2Status( + deprecated_code=deprecated_code, + code=status.status_code.value, + message=status.description, + ) + return pb2_status + + +def _encode_trace_state(trace_state: TraceState) -> Optional[str]: + pb2_trace_state = None + if trace_state is not None: + pb2_trace_state = ",".join( + [ + "{}={}".format(key, value) + for key, value in (trace_state.items()) + ] + ) + return pb2_trace_state + + +def _encode_parent_id(context: Optional[SpanContext]) -> Optional[bytes]: + if isinstance(context, SpanContext): + encoded_parent_id = _encode_span_id(context.span_id) + else: + encoded_parent_id = None + return encoded_parent_id + + +def _encode_attributes( + attributes: Attributes, +) -> Optional[List[PB2KeyValue]]: + if attributes: + pb2_attributes = [] + for key, value in attributes.items(): + try: + pb2_attributes.append(_encode_key_value(key, value)) + except Exception as error: # pylint: disable=broad-except + _logger.exception(error) + else: + pb2_attributes = None + return pb2_attributes + + +def _encode_resource(resource: Resource) -> PB2Resource: + pb2_resource = PB2Resource() + for key, value in resource.attributes.items(): + try: + # pylint: disable=no-member + pb2_resource.attributes.append(_encode_key_value(key, value)) + except Exception as error: # pylint: disable=broad-except + _logger.exception(error) + return pb2_resource + + +def _encode_instrumentation_library( + instrumentation_info: InstrumentationInfo, +) -> PB2InstrumentationLibrary: + if instrumentation_info is None: + pb2_instrumentation_library = PB2InstrumentationLibrary() + else: + pb2_instrumentation_library = PB2InstrumentationLibrary( + name=instrumentation_info.name, + version=instrumentation_info.version, + ) + return pb2_instrumentation_library + + +def _encode_value(value: Any) -> PB2AnyValue: + if isinstance(value, bool): + any_value = PB2AnyValue(bool_value=value) + elif isinstance(value, str): + any_value = PB2AnyValue(string_value=value) + elif isinstance(value, int): + any_value = PB2AnyValue(int_value=value) + elif isinstance(value, float): + any_value = PB2AnyValue(double_value=value) + elif isinstance(value, abc.Sequence): + any_value = PB2AnyValue( + array_value=PB2ArrayValue(values=[_encode_value(v) for v in value]) + ) + # tracing specs currently does not support Mapping type attributes. + # elif isinstance(value, abc.Mapping): + # pass + else: + raise Exception( + "Invalid type {} of value {}".format(type(value), value) + ) + return any_value + + +def _encode_key_value(key: Text, value: Any) -> PB2KeyValue: + any_value = _encode_value(value) + return PB2KeyValue(key=key, value=any_value) + + +def _encode_span_id(span_id: int) -> bytes: + return span_id.to_bytes(length=8, byteorder="big", signed=False) + + +def _encode_trace_id(trace_id: int) -> bytes: + return trace_id.to_bytes(length=16, byteorder="big", signed=False) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py new file mode 100644 index 0000000000..5aeaec6556 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.24.dev0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py new file mode 100644 index 0000000000..9b4bb53ad4 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -0,0 +1,147 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import patch + +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, + OTLPSpanExporter, +) +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, +) + +OS_ENV_ENDPOINT = "os.env.base" +OS_ENV_CERTIFICATE = "os/env/base.crt" +OS_ENV_HEADERS = "envHeader1=val1,envHeader2=val2" +OS_ENV_TIMEOUT = "30" + + +# pylint: disable=protected-access +class TestOTLPSpanExporter(unittest.TestCase): + def test_constructor_default(self): + + exporter = OTLPSpanExporter() + + self.assertEqual(exporter._endpoint, DEFAULT_ENDPOINT) + self.assertEqual(exporter._certificate_file, True) + self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) + self.assertIs(exporter._compression, DEFAULT_COMPRESSION) + self.assertEqual(exporter._headers, {}) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "traces/certificate.env", + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: Compression.Deflate.value, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://traces.endpoint.env", + OTEL_EXPORTER_OTLP_TRACES_HEADERS: "tracesEnv1=val1,tracesEnv2=val2", + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "40", + }, + ) + def test_exporter_traces_env_take_priority(self): + exporter = OTLPSpanExporter() + + self.assertEqual(exporter._endpoint, "https://traces.endpoint.env") + self.assertEqual(exporter._certificate_file, "traces/certificate.env") + self.assertEqual(exporter._timeout, 40) + self.assertIs(exporter._compression, Compression.Deflate) + self.assertEqual( + exporter._headers, + {"tracesEnv1": "val1", "tracesEnv2": "val2"}, + ) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, + }, + ) + def test_exporter_constructor_take_priority(self): + exporter = OTLPSpanExporter( + endpoint="example.com/1234", + certificate_file="path/to/service.crt", + headers={"testHeader1": "value1", "testHeader2": "value2"}, + timeout=20, + compression=Compression.NoCompression, + ) + + self.assertEqual(exporter._endpoint, "example.com/1234") + self.assertEqual(exporter._certificate_file, "path/to/service.crt") + self.assertEqual(exporter._timeout, 20) + self.assertIs(exporter._compression, Compression.NoCompression) + self.assertEqual( + exporter._headers, + {"testHeader1": "value1", "testHeader2": "value2"}, + ) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, + }, + ) + def test_exporter_env(self): + + exporter = OTLPSpanExporter() + + self.assertEqual(exporter._endpoint, OS_ENV_ENDPOINT) + self.assertEqual(exporter._certificate_file, OS_ENV_CERTIFICATE) + self.assertEqual(exporter._timeout, int(OS_ENV_TIMEOUT)) + self.assertIs(exporter._compression, Compression.Gzip) + self.assertEqual( + exporter._headers, {"envHeader1": "val1", "envHeader2": "val2"} + ) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_HEADERS: "envHeader1=val1,envHeader2=val2,missingValue" + }, + ) + def test_headers_parse_from_env(self): + + with self.assertLogs(level="WARNING") as cm: + _ = OTLPSpanExporter() + + self.assertEqual( + cm.records[0].message, + "Skipped invalid OTLP exporter header: 'missingValue'", + ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py new file mode 100644 index 0000000000..676b5f7b7a --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py @@ -0,0 +1,396 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access + +import unittest +from typing import List, Tuple + +from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import ( + _SPAN_KIND_MAP, + _encode_span_id, + _encode_status, + _encode_trace_id, + _ProtobufEncoder, +) +from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( + ExportTraceServiceRequest as PB2ExportTraceServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue +from opentelemetry.proto.common.v1.common_pb2 import ( + InstrumentationLibrary as PB2InstrumentationLibrary, +) +from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as PB2Resource, +) +from opentelemetry.proto.trace.v1.trace_pb2 import ( + InstrumentationLibrarySpans as PB2InstrumentationLibrarySpans, +) +from opentelemetry.proto.trace.v1.trace_pb2 import ( + ResourceSpans as PB2ResourceSpans, +) +from opentelemetry.proto.trace.v1.trace_pb2 import Span as PB2SPan +from opentelemetry.proto.trace.v1.trace_pb2 import Status as PB2Status +from opentelemetry.sdk.trace import Event as SDKEvent +from opentelemetry.sdk.trace import Resource as SDKResource +from opentelemetry.sdk.trace import SpanContext as SDKSpanContext +from opentelemetry.sdk.trace import _Span as SDKSpan +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationInfo as SDKInstrumentationInfo, +) +from opentelemetry.trace import Link as SDKLink +from opentelemetry.trace import SpanKind as SDKSpanKind +from opentelemetry.trace import TraceFlags as SDKTraceFlags +from opentelemetry.trace.status import Status as SDKStatus +from opentelemetry.trace.status import StatusCode as SDKStatusCode + + +class TestProtobufEncoder(unittest.TestCase): + def test_encode(self): + otel_spans, expected_encoding = self.get_exhaustive_test_spans() + self.assertEqual( + _ProtobufEncoder().encode(otel_spans), expected_encoding + ) + + def test_serialize(self): + otel_spans, expected_encoding = self.get_exhaustive_test_spans() + self.assertEqual( + _ProtobufEncoder().serialize(otel_spans), + expected_encoding.SerializeToString(), + ) + + def test_content_type(self): + self.assertEqual( + _ProtobufEncoder._CONTENT_TYPE, "application/x-protobuf" + ) + + @staticmethod + def get_exhaustive_otel_span_list() -> List[SDKSpan]: + trace_id = 0x3E0C63257DE34C926F9EFCD03927272E + + base_time = 683647322 * 10 ** 9 # in ns + start_times = ( + base_time, + base_time + 150 * 10 ** 6, + base_time + 300 * 10 ** 6, + base_time + 400 * 10 ** 6, + ) + end_times = ( + start_times[0] + (50 * 10 ** 6), + start_times[1] + (100 * 10 ** 6), + start_times[2] + (200 * 10 ** 6), + start_times[3] + (300 * 10 ** 6), + ) + + parent_span_context = SDKSpanContext( + trace_id, 0x1111111111111111, is_remote=False + ) + + other_context = SDKSpanContext( + trace_id, 0x2222222222222222, is_remote=False + ) + + span1 = SDKSpan( + name="test-span-1", + context=SDKSpanContext( + trace_id, + 0x34BF92DEEFC58C92, + is_remote=False, + trace_flags=SDKTraceFlags(SDKTraceFlags.SAMPLED), + ), + parent=parent_span_context, + events=( + SDKEvent( + name="event0", + timestamp=base_time + 50 * 10 ** 6, + attributes={ + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + }, + ), + ), + links=( + SDKLink(context=other_context, attributes={"key_bool": True}), + ), + resource=SDKResource({}), + ) + span1.start(start_time=start_times[0]) + span1.set_attribute("key_bool", False) + span1.set_attribute("key_string", "hello_world") + span1.set_attribute("key_float", 111.22) + span1.set_status(SDKStatus(SDKStatusCode.ERROR, "Example description")) + span1.end(end_time=end_times[0]) + + span2 = SDKSpan( + name="test-span-2", + context=parent_span_context, + parent=None, + resource=SDKResource(attributes={"key_resource": "some_resource"}), + ) + span2.start(start_time=start_times[1]) + span2.end(end_time=end_times[1]) + + span3 = SDKSpan( + name="test-span-3", + context=other_context, + parent=None, + resource=SDKResource(attributes={"key_resource": "some_resource"}), + ) + span3.start(start_time=start_times[2]) + span3.set_attribute("key_string", "hello_world") + span3.end(end_time=end_times[2]) + + span4 = SDKSpan( + name="test-span-4", + context=other_context, + parent=None, + resource=SDKResource({}), + instrumentation_info=SDKInstrumentationInfo( + name="name", version="version" + ), + ) + span4.start(start_time=start_times[3]) + span4.end(end_time=end_times[3]) + + return [span1, span2, span3, span4] + + def get_exhaustive_test_spans( + self, + ) -> Tuple[List[SDKSpan], PB2ExportTraceServiceRequest]: + otel_spans = self.get_exhaustive_otel_span_list() + trace_id = _encode_trace_id(otel_spans[0].context.trace_id) + span_kind = _SPAN_KIND_MAP[SDKSpanKind.INTERNAL] + + pb2_service_request = PB2ExportTraceServiceRequest( + resource_spans=[ + PB2ResourceSpans( + resource=PB2Resource(), + instrumentation_library_spans=[ + PB2InstrumentationLibrarySpans( + instrumentation_library=PB2InstrumentationLibrary(), + spans=[ + PB2SPan( + trace_id=trace_id, + span_id=_encode_span_id( + otel_spans[0].context.span_id + ), + trace_state=None, + parent_span_id=_encode_span_id( + otel_spans[0].parent.span_id + ), + name=otel_spans[0].name, + kind=span_kind, + start_time_unix_nano=otel_spans[ + 0 + ].start_time, + end_time_unix_nano=otel_spans[0].end_time, + attributes=[ + PB2KeyValue( + key="key_bool", + value=PB2AnyValue( + bool_value=False + ), + ), + PB2KeyValue( + key="key_string", + value=PB2AnyValue( + string_value="hello_world" + ), + ), + PB2KeyValue( + key="key_float", + value=PB2AnyValue( + double_value=111.22 + ), + ), + ], + events=[ + PB2SPan.Event( + name="event0", + time_unix_nano=otel_spans[0] + .events[0] + .timestamp, + attributes=[ + PB2KeyValue( + key="annotation_bool", + value=PB2AnyValue( + bool_value=True + ), + ), + PB2KeyValue( + key="annotation_string", + value=PB2AnyValue( + string_value="annotation_test" + ), + ), + PB2KeyValue( + key="key_float", + value=PB2AnyValue( + double_value=0.3 + ), + ), + ], + ) + ], + links=[ + PB2SPan.Link( + trace_id=_encode_trace_id( + otel_spans[0] + .links[0] + .context.trace_id + ), + span_id=_encode_span_id( + otel_spans[0] + .links[0] + .context.span_id + ), + attributes=[ + PB2KeyValue( + key="key_bool", + value=PB2AnyValue( + bool_value=True + ), + ), + ], + ) + ], + status=PB2Status( + deprecated_code=PB2Status.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR, # pylint: disable=no-member + code=SDKStatusCode.ERROR.value, + message="Example description", + ), + ) + ], + ), + PB2InstrumentationLibrarySpans( + instrumentation_library=PB2InstrumentationLibrary( + name="name", + version="version", + ), + spans=[ + PB2SPan( + trace_id=trace_id, + span_id=_encode_span_id( + otel_spans[3].context.span_id + ), + trace_state=None, + parent_span_id=None, + name=otel_spans[3].name, + kind=span_kind, + start_time_unix_nano=otel_spans[ + 3 + ].start_time, + end_time_unix_nano=otel_spans[3].end_time, + attributes=None, + events=None, + links=None, + status={}, + ) + ], + ), + ], + ), + PB2ResourceSpans( + resource=PB2Resource( + attributes=[ + PB2KeyValue( + key="key_resource", + value=PB2AnyValue( + string_value="some_resource" + ), + ) + ] + ), + instrumentation_library_spans=[ + PB2InstrumentationLibrarySpans( + instrumentation_library=PB2InstrumentationLibrary(), + spans=[ + PB2SPan( + trace_id=trace_id, + span_id=_encode_span_id( + otel_spans[1].context.span_id + ), + trace_state=None, + parent_span_id=None, + name=otel_spans[1].name, + kind=span_kind, + start_time_unix_nano=otel_spans[ + 1 + ].start_time, + end_time_unix_nano=otel_spans[1].end_time, + attributes=None, + events=None, + links=None, + status={}, + ), + PB2SPan( + trace_id=trace_id, + span_id=_encode_span_id( + otel_spans[2].context.span_id + ), + trace_state=None, + parent_span_id=None, + name=otel_spans[2].name, + kind=span_kind, + start_time_unix_nano=otel_spans[ + 2 + ].start_time, + end_time_unix_nano=otel_spans[2].end_time, + attributes=[ + PB2KeyValue( + key="key_string", + value=PB2AnyValue( + string_value="hello_world" + ), + ), + ], + events=None, + links=None, + status={}, + ), + ], + ) + ], + ), + ] + ) + + return otel_spans, pb2_service_request + + def test_encode_status_code_translations(self): + self.assertEqual( + _encode_status(SDKStatus(status_code=SDKStatusCode.UNSET)), + PB2Status( + deprecated_code=PB2Status.DEPRECATED_STATUS_CODE_OK, # pylint: disable=no-member + code=SDKStatusCode.UNSET.value, + ), + ) + + self.assertEqual( + _encode_status(SDKStatus(status_code=SDKStatusCode.OK)), + PB2Status( + deprecated_code=PB2Status.DEPRECATED_STATUS_CODE_OK, # pylint: disable=no-member + code=SDKStatusCode.OK.value, + ), + ) + + self.assertEqual( + _encode_status(SDKStatus(status_code=SDKStatusCode.ERROR)), + PB2Status( + deprecated_code=PB2Status.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR, # pylint: disable=no-member + code=SDKStatusCode.ERROR.value, + ), + ) diff --git a/exporter/opentelemetry-exporter-otlp/README.rst b/exporter/opentelemetry-exporter-otlp/README.rst index 4bbef08f64..7d6d15ad20 100644 --- a/exporter/opentelemetry-exporter-otlp/README.rst +++ b/exporter/opentelemetry-exporter-otlp/README.rst @@ -9,10 +9,9 @@ OpenTelemetry Collector Exporters This library is provided as a convenience to install all supported OpenTelemetry Collector Exporters. Currently it installs: * opentelemetry-exporter-otlp-proto-grpc +* opentelemetry-exporter-otlp-proto-http In the future, additional packages will be available: - -* opentelemetry-exporter-otlp-proto-http * opentelemetry-exporter-otlp-json-http To avoid unnecessary dependencies, users should install the specific package once they've determined their diff --git a/tests/opentelemetry-docker-tests/tests/docker-compose.yml b/tests/opentelemetry-docker-tests/tests/docker-compose.yml index c0566a6f00..b1cd2d6e1f 100644 --- a/tests/opentelemetry-docker-tests/tests/docker-compose.yml +++ b/tests/opentelemetry-docker-tests/tests/docker-compose.yml @@ -10,4 +10,5 @@ services: otcollector: image: otel/opentelemetry-collector:0.22.0 ports: - - "4317:4317" \ No newline at end of file + - "4317:4317" + - "55681:55681" diff --git a/tests/opentelemetry-docker-tests/tests/otlpexporter/__init__.py b/tests/opentelemetry-docker-tests/tests/otlpexporter/__init__.py new file mode 100644 index 0000000000..d4340fb910 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/otlpexporter/__init__.py @@ -0,0 +1,48 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod + +from opentelemetry.context import attach, detach, set_value +from opentelemetry.sdk.trace.export import SimpleSpanProcessor + + +class ExportStatusSpanProcessor(SimpleSpanProcessor): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.export_status = [] + + def on_end(self, span): + token = attach(set_value("suppress_instrumentation", True)) + self.export_status.append(self.span_exporter.export((span,))) + detach(token) + + +class BaseTestOTLPExporter(ABC): + @abstractmethod + def get_span_processor(self): + pass + + # pylint: disable=no-member + def test_export(self): + with self.tracer.start_as_current_span("foo"): + with self.tracer.start_as_current_span("bar"): + with self.tracer.start_as_current_span("baz"): + pass + + self.assertTrue(len(self.span_processor.export_status), 3) + + for export_status in self.span_processor.export_status: + self.assertEqual(export_status.name, "SUCCESS") + self.assertEqual(export_status.value, 0) diff --git a/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py index 789dcc7d10..d48b305396 100644 --- a/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py +++ b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py @@ -13,46 +13,27 @@ # limitations under the License. from opentelemetry import trace -from opentelemetry.context import attach, detach, set_value from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.test.test_base import TestBase +from . import BaseTestOTLPExporter, ExportStatusSpanProcessor -class ExportStatusSpanProcessor(SimpleSpanProcessor): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.export_status = [] - - def on_end(self, span): - token = attach(set_value("suppress_instrumentation", True)) - self.export_status.append(self.span_exporter.export((span,))) - detach(token) +class TestOTLPGRPCExporter(BaseTestOTLPExporter, TestBase): + # pylint: disable=no-self-use + def get_span_processor(self): + return ExportStatusSpanProcessor( + OTLPSpanExporter(insecure=True, timeout=1) + ) -class TestOTLPExporter(TestBase): def setUp(self): super().setUp() trace.set_tracer_provider(TracerProvider()) self.tracer = trace.get_tracer(__name__) - self.span_processor = ExportStatusSpanProcessor( - OTLPSpanExporter(insecure=True, timeout=1) - ) + self.span_processor = self.get_span_processor() trace.get_tracer_provider().add_span_processor(self.span_processor) - - def test_export(self): - with self.tracer.start_as_current_span("foo"): - with self.tracer.start_as_current_span("bar"): - with self.tracer.start_as_current_span("baz"): - pass - - self.assertTrue(len(self.span_processor.export_status), 3) - - for export_status in self.span_processor.export_status: - self.assertEqual(export_status.name, "SUCCESS") - self.assertEqual(export_status.value, 0) diff --git a/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_http_exporter_functional.py b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_http_exporter_functional.py new file mode 100644 index 0000000000..59a333dec6 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_http_exporter_functional.py @@ -0,0 +1,37 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + OTLPSpanExporter, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.test.test_base import TestBase + +from . import BaseTestOTLPExporter, ExportStatusSpanProcessor + + +class TestOTLPHTTPExporter(BaseTestOTLPExporter, TestBase): + # pylint: disable=no-self-use + def get_span_processor(self): + return ExportStatusSpanProcessor(OTLPSpanExporter()) + + def setUp(self): + super().setUp() + + trace.set_tracer_provider(TracerProvider()) + self.tracer = trace.get_tracer(__name__) + self.span_processor = self.get_span_processor() + + trace.get_tracer_provider().add_span_processor(self.span_processor) diff --git a/tox.ini b/tox.ini index 04d683489c..42155157e4 100644 --- a/tox.ini +++ b/tox.ini @@ -53,6 +53,10 @@ envlist = py3{6,7,8,9}-test-exporter-otlp-proto-grpc ; intentionally excluded from pypy3 + ; opentelemetry-exporter-otlp-proto-http + py3{6,7,8,9}-test-exporter-otlp-proto-http + pypy3-test-exporter-otlp-proto-http + ; opentelemetry-exporter-zipkin py3{6,7,8,9}-test-exporter-zipkin-combined pypy3-test-exporter-zipkin-combined @@ -111,6 +115,7 @@ changedir = test-exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests test-exporter-otlp-combined: exporter/opentelemetry-exporter-otlp/tests test-exporter-otlp-proto-grpc: exporter/opentelemetry-exporter-otlp-proto-grpc/tests + test-exporter-otlp-proto-http: exporter/opentelemetry-exporter-otlp-proto-http/tests test-exporter-zipkin-combined: exporter/opentelemetry-exporter-zipkin/tests test-exporter-zipkin-proto-http: exporter/opentelemetry-exporter-zipkin-proto-http/tests test-exporter-zipkin-json: exporter/opentelemetry-exporter-zipkin-json/tests @@ -138,11 +143,15 @@ commands_pre = exporter-otlp-combined: pip install {toxinidir}/opentelemetry-proto exporter-otlp-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc + exporter-otlp-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http exporter-otlp-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp exporter-otlp-proto-grpc: pip install {toxinidir}/opentelemetry-proto exporter-otlp-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc + exporter-otlp-proto-http: pip install {toxinidir}/opentelemetry-proto + exporter-otlp-proto-http: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http + exporter-jaeger-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift {toxinidir}/exporter/opentelemetry-exporter-jaeger exporter-jaeger-combined: pip install {toxinidir}/opentelemetry-instrumentation exporter-jaeger-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc @@ -213,6 +222,7 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-opencensus[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin-json[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin-proto-http[test] @@ -276,6 +286,7 @@ commands_pre = -e {toxinidir}/exporter/opentelemetry-exporter-opencensus \ -e {toxinidir}/opentelemetry-proto \ -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc \ + -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http \ -e {toxinidir}/exporter/opentelemetry-exporter-otlp docker-compose up -d commands = From d4746bdc4379f5d4c4e845e628d752b2cd128c77 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 29 Jul 2021 16:46:02 -0600 Subject: [PATCH 0936/1517] Remove deprecated @asyncio.coroutine (#2002) Fixes #2001 --- opentelemetry-sdk/tests/context/test_asyncio.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index a9b32e10fa..0c25e54e37 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -55,8 +55,7 @@ def stop_loop_when(loop, cond_func, timeout=5.0): class TestAsyncio(unittest.TestCase): - @asyncio.coroutine - def task(self, name): + async def task(self, name): with self.tracer.start_as_current_span(name): context.set_value("say", "bar") From 13f09db172f41d5f92b52a0fc4e602175a2474af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Fri, 30 Jul 2021 18:00:28 +0200 Subject: [PATCH 0937/1517] Make test deterministic. (#1984) --- .../tests/test_jaeger_exporter_thrift.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py index 0da65d0477..c72cd579ff 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py @@ -62,9 +62,15 @@ def setUp(self): is_remote=False, ) - self._test_span = trace._Span("test_span", context=self.context) - self._test_span.start() - self._test_span.end() + self._test_span = trace._Span( + "test_span", + context=self.context, + # Use a fixed version because a longer/shorter version number + # might break tests that care about packet size. + resource=Resource.create({"telemetry.sdk.version": "0.0.0.dev0"}), + ) + self._test_span.start(start_time=1) + self._test_span.end(end_time=3) # pylint: disable=protected-access @patch("opentelemetry.exporter.jaeger.thrift.trace._TRACER_PROVIDER", None) From 65670cfcf4c2ce23fb9b655fa5b7ad0f50a18f4e Mon Sep 17 00:00:00 2001 From: Yoshi Yamaguchi <145104+ymotongpoo@users.noreply.github.com> Date: Tue, 3 Aug 2021 01:42:13 +0900 Subject: [PATCH 0938/1517] Add validation for Trace ID (#1992) --- CHANGELOG.md | 1 + opentelemetry-api/src/opentelemetry/trace/span.py | 7 ++++++- opentelemetry-api/tests/trace/test_span_context.py | 9 +++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 359ad7d787..fae7b82386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-distro` & `opentelemetry-sdk` Moved Auto Instrumentation Configurator code to SDK to let distros use its default implementation ([#1937](https://github.com/open-telemetry/opentelemetry-python/pull/1937)) +- Add Trace ID validation to meet [TraceID spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#spancontext) ([#1992](https://github.com/open-telemetry/opentelemetry-python/pull/1992)) ## [0.23.1](https://github.com/open-telemetry/opentelemetry-python/pull/1987) - 2021-07-26 diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index c4c713cf3e..832b8b62f6 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -389,6 +389,7 @@ def values(self) -> typing.ValuesView[str]: DEFAULT_TRACE_STATE = TraceState.get_default() +_TRACE_ID_HEX_LENGTH = 2 ** 128 - 1 class SpanContext( @@ -420,7 +421,11 @@ def __new__( if trace_state is None: trace_state = DEFAULT_TRACE_STATE - is_valid = trace_id != INVALID_TRACE_ID and span_id != INVALID_SPAN_ID + is_valid = ( + trace_id != INVALID_TRACE_ID + and span_id != INVALID_SPAN_ID + and trace_id < _TRACE_ID_HEX_LENGTH + ) return tuple.__new__( cls, diff --git a/opentelemetry-api/tests/trace/test_span_context.py b/opentelemetry-api/tests/trace/test_span_context.py index c109d006a5..1ec32253c3 100644 --- a/opentelemetry-api/tests/trace/test_span_context.py +++ b/opentelemetry-api/tests/trace/test_span_context.py @@ -34,3 +34,12 @@ def test_span_context_pickle(self): pickle_sc = pickle.loads(pickle.dumps(sc)) self.assertEqual(sc.trace_id, pickle_sc.trace_id) self.assertEqual(sc.span_id, pickle_sc.span_id) + + invalid_sc = trace.SpanContext( + 9999999999999999999999999999999999999999999999999999999999999999999999999999, + 9, + is_remote=False, + trace_flags=trace.DEFAULT_TRACE_OPTIONS, + trace_state=trace.DEFAULT_TRACE_STATE, + ) + self.assertFalse(invalid_sc.is_valid) From e2eb73c3cbfca9180def85eb7639f22d3a7a77b7 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 4 Aug 2021 09:13:42 -0600 Subject: [PATCH 0939/1517] Remove unused imports (#2007) Fixes #2006 Co-authored-by: alrex --- .flake8 | 1 - docs/getting_started/otlpcollector_example.py | 1 - .../exporter/otlp/proto/grpc/exporter.py | 3 +-- .../opentelemetry/exporter/zipkin/json/__init__.py | 6 +----- .../exporter/zipkin/json/v1/__init__.py | 1 - .../exporter/zipkin/proto/http/__init__.py | 5 ----- .../exporter/zipkin/proto/http/v2/__init__.py | 2 +- .../opentelemetry/context/contextvars_context.py | 8 ++++++-- .../tests/context/test_contextvars_context.py | 13 ++++++------- .../src/opentelemetry/instrumentation/bootstrap.py | 1 - opentelemetry-sdk/tests/context/test_asyncio.py | 12 +++++------- .../trace/profile_resource_usage_batch_export.py | 1 - .../trace/profile_resource_usage_simple_export.py | 1 - .../opentelemetry/propagators/jaeger/__init__.py | 2 +- 14 files changed, 21 insertions(+), 36 deletions(-) diff --git a/.flake8 b/.flake8 index c66d869076..9d0b0c101d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,6 @@ [flake8] ignore = E501 # line too long, defer to black - F401 # unused import, defer to pylint W503 # allow line breaks before binary ops W504 # allow line breaks after binary ops E203 # allow whitespace before ':' (https://github.com/psf/black#slices) diff --git a/docs/getting_started/otlpcollector_example.py b/docs/getting_started/otlpcollector_example.py index 71f9ed9754..11b3b12d4b 100644 --- a/docs/getting_started/otlpcollector_example.py +++ b/docs/getting_started/otlpcollector_example.py @@ -13,7 +13,6 @@ # limitations under the License. # otcollector.py -import time from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index ba88dc1d7d..661d7a96ea 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -16,13 +16,12 @@ import logging from abc import ABC, abstractmethod -from collections.abc import Mapping, Sequence +from collections.abc import Sequence from os import environ from time import sleep from typing import Any, Callable, Dict, Generic, List, Optional from typing import Sequence as TypingSequence from typing import Text, TypeVar -from urllib import parse from urllib.parse import urlparse from backoff import expo diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py index 2fc9df1538..0e0642d0be 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py @@ -75,11 +75,7 @@ import requests -from opentelemetry.exporter.zipkin.encoder import ( - DEFAULT_MAX_TAG_VALUE_LENGTH, - Encoder, - Protocol, -) +from opentelemetry.exporter.zipkin.encoder import Protocol from opentelemetry.exporter.zipkin.json.v1 import JsonV1Encoder from opentelemetry.exporter.zipkin.json.v2 import JsonV2Encoder from opentelemetry.exporter.zipkin.node_endpoint import IpInput, NodeEndpoint diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/v1/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/v1/__init__.py index 2fcb4ab168..5272173f31 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/v1/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/v1/__init__.py @@ -15,7 +15,6 @@ """Zipkin Export Encoders for JSON formats """ -import abc from typing import Dict, List from opentelemetry.exporter.zipkin.encoder import Encoder, JsonEncoder diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py index 42a007d172..bd98a1ff06 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py @@ -74,11 +74,6 @@ import requests -from opentelemetry.exporter.zipkin.encoder import ( - DEFAULT_MAX_TAG_VALUE_LENGTH, - Encoder, - Protocol, -) from opentelemetry.exporter.zipkin.proto.http.v2 import ProtobufEncoder from opentelemetry.exporter.zipkin.node_endpoint import IpInput, NodeEndpoint from opentelemetry.sdk.environment_variables import ( diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/__init__.py index b54761e166..676c2496f7 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/__init__.py @@ -22,7 +22,7 @@ from opentelemetry.exporter.zipkin.proto.http.v2.gen import zipkin_pb2 from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk.trace import Event -from opentelemetry.trace import Span, SpanContext, SpanKind +from opentelemetry.trace import Span, SpanKind class ProtobufEncoder(Encoder): diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index fcb10fba7e..2f8417ac01 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -17,10 +17,14 @@ from opentelemetry.context.context import Context, _RuntimeContext if (3, 5, 3) <= version_info < (3, 7): - import aiocontextvars # type: ignore # pylint:disable=unused-import,import-error + import aiocontextvars # type: ignore # pylint:disable=import-error + + aiocontextvars # pylint:disable=pointless-statement elif (3, 4) < version_info <= (3, 5, 2): - import opentelemetry.context.aiocontextvarsfix # pylint:disable=unused-import + import opentelemetry.context.aiocontextvarsfix # pylint:disable=wrong-import-position + + opentelemetry.context.aiocontextvarsfix # pylint:disable=pointless-statement class ContextVarsRuntimeContext(_RuntimeContext): diff --git a/opentelemetry-api/tests/context/test_contextvars_context.py b/opentelemetry-api/tests/context/test_contextvars_context.py index 2ac517abb2..3aeaebcc29 100644 --- a/opentelemetry-api/tests/context/test_contextvars_context.py +++ b/opentelemetry-api/tests/context/test_contextvars_context.py @@ -13,21 +13,20 @@ # limitations under the License. import unittest +from sys import version_info from unittest.mock import patch from opentelemetry import context from .base_context import ContextTestCases -try: - import contextvars # pylint: disable=unused-import - - from opentelemetry.context.contextvars_context import ( - ContextVarsRuntimeContext, - ) -except ImportError: +if version_info.minor < 7: raise unittest.SkipTest("contextvars not available") +from opentelemetry.context.contextvars_context import ( # pylint:disable=wrong-import-position + ContextVarsRuntimeContext, +) + class TestContextVarsContext(ContextTestCases.BaseTest): def setUp(self) -> None: diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index a29117f972..abeb23df3d 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -25,7 +25,6 @@ default_instrumentations, libraries, ) -from opentelemetry.instrumentation.version import __version__ as version logger = logging.getLogger(__file__) diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index 0c25e54e37..b6012a0d7b 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -14,6 +14,7 @@ import asyncio import unittest +from sys import version_info from unittest.mock import patch from opentelemetry import context @@ -23,15 +24,12 @@ InMemorySpanExporter, ) -try: - import contextvars # pylint: disable=unused-import - - from opentelemetry.context.contextvars_context import ( - ContextVarsRuntimeContext, - ) -except ImportError: +if version_info.minor < 7: raise unittest.SkipTest("contextvars not available") +from opentelemetry.context.contextvars_context import ( # pylint:disable=wrong-import-position + ContextVarsRuntimeContext, +) _SPAN_NAMES = [ "test_span1", diff --git a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py index 178d65b114..825091331b 100644 --- a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py +++ b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py @@ -13,7 +13,6 @@ # limitations under the License. import time -from unittest.mock import patch from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, diff --git a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py index b82f133ec4..95b56c3cea 100644 --- a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py +++ b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py @@ -13,7 +13,6 @@ # limitations under the License. import time -from unittest.mock import patch from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py index 974b9143a5..65dc43487e 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py @@ -17,7 +17,7 @@ import opentelemetry.trace as trace from opentelemetry import baggage -from opentelemetry.context import Context, get_current +from opentelemetry.context import Context from opentelemetry.propagators.textmap import ( CarrierT, Getter, From 93aad21a097a7b8740c9e8345f4fcf5d2e55a0e2 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 10 Aug 2021 11:29:30 -0600 Subject: [PATCH 0940/1517] Add Nathaniel to approvers list (#2028) Co-authored-by: alrex Co-authored-by: (Eliseo) Nathaniel Ruiz Nowell --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 082e770eff..c613efd716 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Aaron Abbott](https://github.com/aabmass), Google - [Owais Lone](https://github.com/owais), Splunk - [Srikanth Chekuri](https://github.com/lonewolf3739) +- [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), AWS *For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver).* From 3429ad3e367735438bc0270687d547c5b4d0b7da Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 10 Aug 2021 11:35:24 -0600 Subject: [PATCH 0941/1517] Add Owais to maintainers list (#2025) Fixes #2003 Co-authored-by: alrex --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c613efd716..340e553ee1 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,6 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): - [Aaron Abbott](https://github.com/aabmass), Google -- [Owais Lone](https://github.com/owais), Splunk - [Srikanth Chekuri](https://github.com/lonewolf3739) - [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), AWS @@ -142,6 +141,7 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t - [Alex Boten](https://github.com/codeboten), Lightstep - [Diego Hurtado](https://github.com/ocelotl), Lightstep - [Leighton Chen](https://github.com/lzchen), Microsoft +- [Owais Lone](https://github.com/owais), Splunk *For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer).* From dff57aa9e44acbc8ef88132533dbd8b4fa0340cf Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 10 Aug 2021 10:42:54 -0700 Subject: [PATCH 0942/1517] update codeboten from maintainer -> approver (#2026) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 340e553ee1..9c6e9745b1 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): - [Aaron Abbott](https://github.com/aabmass), Google +- [Alex Boten](https://github.com/codeboten), Lightstep - [Srikanth Chekuri](https://github.com/lonewolf3739) - [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), AWS @@ -138,7 +139,6 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-telemetry/teams/python-maintainers)): -- [Alex Boten](https://github.com/codeboten), Lightstep - [Diego Hurtado](https://github.com/ocelotl), Lightstep - [Leighton Chen](https://github.com/lzchen), Microsoft - [Owais Lone](https://github.com/owais), Splunk From 0cf819a775a0bf41a940cebcc37dfbee1224dae4 Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 12 Aug 2021 10:45:08 -0700 Subject: [PATCH 0943/1517] update contrib SHA (#2036) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ebb36ee8a2..a20e434ef8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 64996996451f9fab16548febe4707883935428d1 + CONTRIB_REPO_SHA: dd65a29b5805397e3b511d6546179e7c03442f82 jobs: build: From 820c89e962613ced683d425ecbda86007c76b84e Mon Sep 17 00:00:00 2001 From: Emmanuel Courreges Date: Fri, 13 Aug 2021 04:34:59 +0200 Subject: [PATCH 0944/1517] fix: documentation on "Well known exporters" zipkin -> zipkin_json, etc. (#2023) fix: documentation OTEL_TRACE_EXPORTER -> OTEL_TRACES_EXPORTER Co-authored-by: alrex --- CHANGELOG.md | 1 + opentelemetry-instrumentation/README.rst | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fae7b82386..a4d89e055a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.4.0-0.23b0...HEAD) +- Fix documentation on well known exporters and variable OTEL_TRACES_EXPORTER which were misnamed [#2023](https://github.com/open-telemetry/opentelemetry-python/pull/2023) - `opentelemetry-distro` & `opentelemetry-sdk` Moved Auto Instrumentation Configurator code to SDK to let distros use its default implementation ([#1937](https://github.com/open-telemetry/opentelemetry-python/pull/1937)) diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index 18b31f428f..cae4e3ab5f 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -56,7 +56,7 @@ this can be overriden when needed. The command supports the following configuration options as CLI arguments and environment vars: -* ``--trace-exporter`` or ``OTEL_TRACE_EXPORTER`` +* ``--trace-exporter`` or ``OTEL_TRACES_EXPORTER`` Used to specify which trace exporter to use. Can be set to one or more of the well-known exporter names (see below). @@ -68,11 +68,14 @@ You can pass multiple values to configure multiple exporters e.g, ``zipkin,prome Well known trace exporter names: - - jaeger + - jaeger_proto + - jaeger_thrift - opencensus - otlp - otlp_proto_grpc_span - - zipkin + - otlp_proto_http_span + - zipkin_json + - zipkin_proto ``otlp`` is an alias for ``otlp_proto_grpc_span``. @@ -102,7 +105,7 @@ The above command will pass ``--trace-exporter otlp`` to the instrument command :: - opentelemetry-instrument --trace-exporter zipkin,otlp celery -A tasks worker --loglevel=info + opentelemetry-instrument --trace-exporter zipkin_json,otlp celery -A tasks worker --loglevel=info The above command will configure global trace provider, attach zipkin and otlp exporters to it and then start celery with the rest of the arguments. From c68adab327bd6cf0fdf8964c3c4ea4f5d1160ce8 Mon Sep 17 00:00:00 2001 From: Antonio Garcia Date: Fri, 13 Aug 2021 14:35:42 -0500 Subject: [PATCH 0945/1517] Update __init__.py (#2029) --- .../src/opentelemetry/exporter/jaeger/thrift/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py index 954e5a0691..1071085777 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py @@ -32,10 +32,15 @@ from opentelemetry import trace from opentelemetry.exporter.jaeger.thrift import JaegerExporter + from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor - trace.set_tracer_provider(TracerProvider()) + trace.set_tracer_provider( + TracerProvider( + resource=Resource.create({SERVICE_NAME: "my-helloworld-service"}) + ) + ) tracer = trace.get_tracer(__name__) # create a JaegerExporter From 240ee760a6f448fd5e836b004caebe3217cfe3b6 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 13 Aug 2021 15:57:01 -0400 Subject: [PATCH 0946/1517] unpin opentelemetry-api dependency in instrumentation package (#2012) Co-authored-by: alrex Co-authored-by: Owais Lone Co-authored-by: Leighton Chen --- opentelemetry-instrumentation/setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 2ceb43c727..72714c581b 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -41,7 +41,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.5.0.dev0 + opentelemetry-api ~= 1.4 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] From 3cdb54f6085ce7170aea05e2c99c480a6e919f34 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Fri, 13 Aug 2021 14:14:13 -0700 Subject: [PATCH 0947/1517] Add default and service name to get_aggregated_resource (#2013) * Add default & service name to resource aggregator * Aggregator calls Resource.create() to set default attributes * Allow get aggregated resources to remove defaults --- CHANGELOG.md | 6 +- .../opentelemetry/sdk/resources/__init__.py | 14 ++-- .../tests/resources/test_resources.py | 83 ++++++++++++++----- 3 files changed, 73 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4d89e055a..735e546a7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.4.0-0.23b0...HEAD) -- Fix documentation on well known exporters and variable OTEL_TRACES_EXPORTER which were misnamed [#2023](https://github.com/open-telemetry/opentelemetry-python/pull/2023) +- Fix documentation on well known exporters and variable OTEL_TRACES_EXPORTER which were misnamed + ([#2023](https://github.com/open-telemetry/opentelemetry-python/pull/2023)) +- `opentelemetry-sdk` `get_aggregated_resource()` returns default resource and service name + whenever called + ([#2013](https://github.com/open-telemetry/opentelemetry-python/pull/2013)) - `opentelemetry-distro` & `opentelemetry-sdk` Moved Auto Instrumentation Configurator code to SDK to let distros use its default implementation ([#1937](https://github.com/open-telemetry/opentelemetry-python/pull/1937)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 3ad01b0aec..5878f375d7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -291,15 +291,14 @@ def get_aggregated_resources( :param timeout: Number of seconds to wait for each detector to return :return: """ - final_resource = initial_resource or _EMPTY_RESOURCE - detectors = [OTELResourceDetector()] + detectors + detectors_merged_resource = initial_resource or Resource.create() with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: futures = [executor.submit(detector.detect) for detector in detectors] for detector_ind, future in enumerate(futures): detector = detectors[detector_ind] try: - detected_resources = future.result(timeout=timeout) + detected_resource = future.result(timeout=timeout) # pylint: disable=broad-except except Exception as ex: if detector.raise_on_error: @@ -307,7 +306,10 @@ def get_aggregated_resources( logger.warning( "Exception %s in detector %s, ignoring", ex, detector ) - detected_resources = _EMPTY_RESOURCE + detected_resource = _EMPTY_RESOURCE finally: - final_resource = final_resource.merge(detected_resources) - return final_resource + detectors_merged_resource = detectors_merged_resource.merge( + detected_resource + ) + + return detectors_merged_resource diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 1f2a094f94..25e8ddb867 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -230,9 +230,18 @@ def test_invalid_resource_attribute_values(self): def test_aggregated_resources_no_detectors(self): aggregated_resources = resources.get_aggregated_resources([]) - self.assertEqual(aggregated_resources, resources.Resource.get_empty()) + self.assertEqual( + aggregated_resources, + resources._DEFAULT_RESOURCE.merge( + resources.Resource( + {resources.SERVICE_NAME: "unknown_service"}, "" + ) + ), + ) - def test_aggregated_resources_with_static_resource(self): + def test_aggregated_resources_with_default_destroying_static_resource( + self, + ): static_resource = resources.Resource({"static_key": "static_value"}) self.assertEqual( @@ -280,13 +289,19 @@ def test_aggregated_resources_multiple_detectors(self): resources.get_aggregated_resources( [resource_detector1, resource_detector2, resource_detector3] ), - resources.Resource( - { - "key1": "value1", - "key2": "try_to_overwrite_existing_value", - "key3": "try_to_overwrite_existing_value", - "key4": "value4", - } + resources._DEFAULT_RESOURCE.merge( + resources.Resource( + {resources.SERVICE_NAME: "unknown_service"}, "" + ) + ).merge( + resources.Resource( + { + "key1": "value1", + "key2": "try_to_overwrite_existing_value", + "key3": "try_to_overwrite_existing_value", + "key4": "value4", + } + ) ), ) @@ -321,9 +336,15 @@ def test_aggregated_resources_different_schema_urls(self): resources.get_aggregated_resources( [resource_detector1, resource_detector2] ), - resources.Resource( - {"key1": "value1", "key2": "value2", "key3": "value3"}, - "url1", + resources._DEFAULT_RESOURCE.merge( + resources.Resource( + {resources.SERVICE_NAME: "unknown_service"}, "" + ) + ).merge( + resources.Resource( + {"key1": "value1", "key2": "value2", "key3": "value3"}, + "url1", + ) ), ) with self.assertLogs(level=ERROR) as log_entry: @@ -331,8 +352,14 @@ def test_aggregated_resources_different_schema_urls(self): resources.get_aggregated_resources( [resource_detector2, resource_detector3] ), - resources.Resource( - {"key2": "value2", "key3": "value3"}, "url1" + resources._DEFAULT_RESOURCE.merge( + resources.Resource( + {resources.SERVICE_NAME: "unknown_service"}, "" + ) + ).merge( + resources.Resource( + {"key2": "value2", "key3": "value3"}, "url1" + ) ), ) self.assertIn("url1", log_entry.output[0]) @@ -347,14 +374,20 @@ def test_aggregated_resources_different_schema_urls(self): resource_detector1, ] ), - resources.Resource( - { - "key1": "value1", - "key2": "try_to_overwrite_existing_value", - "key3": "try_to_overwrite_existing_value", - "key4": "value4", - }, - "url1", + resources._DEFAULT_RESOURCE.merge( + resources.Resource( + {resources.SERVICE_NAME: "unknown_service"}, "" + ) + ).merge( + resources.Resource( + { + "key1": "value1", + "key2": "try_to_overwrite_existing_value", + "key3": "try_to_overwrite_existing_value", + "key4": "value4", + }, + "url1", + ) ), ) self.assertIn("url1", log_entry.output[0]) @@ -366,7 +399,11 @@ def test_resource_detector_ignore_error(self): resource_detector.raise_on_error = False self.assertEqual( resources.get_aggregated_resources([resource_detector]), - resources.Resource.get_empty(), + resources._DEFAULT_RESOURCE.merge( + resources.Resource( + {resources.SERVICE_NAME: "unknown_service"}, "" + ) + ), ) def test_resource_detector_raise_error(self): From 10483915a1d7c10d52bcb94f9668da150a3c0664 Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Mon, 16 Aug 2021 13:45:48 -0400 Subject: [PATCH 0948/1517] Handle traceback.format_exception() API change in Python 3.10 (#2018) --- CHANGELOG.md | 2 ++ shim/opentelemetry-opentracing-shim/tests/test_shim.py | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 735e546a7a..c6bd176ae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 to let distros use its default implementation ([#1937](https://github.com/open-telemetry/opentelemetry-python/pull/1937)) - Add Trace ID validation to meet [TraceID spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#spancontext) ([#1992](https://github.com/open-telemetry/opentelemetry-python/pull/1992)) +- Fixed Python 3.10 incompatibility in `opentelemetry-opentracing-shim` tests + ([#2018](https://github.com/open-telemetry/opentelemetry-python/pull/2018)) ## [0.23.1](https://github.com/open-telemetry/opentelemetry-python/pull/1987) - 2021-07-26 diff --git a/shim/opentelemetry-opentracing-shim/tests/test_shim.py b/shim/opentelemetry-opentracing-shim/tests/test_shim.py index 828097270f..e27f779734 100644 --- a/shim/opentelemetry-opentracing-shim/tests/test_shim.py +++ b/shim/opentelemetry-opentracing-shim/tests/test_shim.py @@ -482,9 +482,7 @@ def test_span_on_error(self): ex = exc_ctx.exception expected_stack = "".join( - traceback.format_exception( - etype=type(ex), value=ex, tb=ex.__traceback__ - ) + traceback.format_exception(type(ex), value=ex, tb=ex.__traceback__) ) # Verify exception details have been added to span. exc_event = scope.span.unwrap().events[0] From 5e0a465965f7a9a711394a1fc949d408bdaee624 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 16 Aug 2021 11:49:56 -0700 Subject: [PATCH 0949/1517] Fix sampling comment (#2040) --- opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 833fbc76aa..878c5d816c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -221,8 +221,7 @@ def get_description(self) -> str: class TraceIdRatioBased(Sampler): """ - Sampler that makes sampling decisions probabalistically based on `rate`, - while also respecting the parent span sampling decision. + Sampler that makes sampling decisions probabilistically based on `rate`. Args: rate: Probability (between 0 and 1) that a span will be sampled From 93fa54da8ae9deb144808efe04d08f7dea391b56 Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Mon, 16 Aug 2021 15:13:04 -0400 Subject: [PATCH 0950/1517] =?UTF-8?q?Fix=20typos=20of=20=E2=80=9Cit's?= =?UTF-8?q?=E2=80=9D=20where=20=E2=80=9Cits=E2=80=9D=20is=20meant=20(#2039?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opentelemetry-api/src/opentelemetry/trace/__init__.py | 2 +- .../instrumentation/auto_instrumentation/__init__.py | 2 +- .../src/opentelemetry/instrumentation/propagators.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 2 +- .../src/opentelemetry/semconv/trace/__init__.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 487592f60d..58d75bbea8 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -320,7 +320,7 @@ def start_as_current_span( as the current span in this tracer's context. Exiting the context manager will call the span's end method, - as well as return the current span to it's previous value by + as well as return the current span to its previous value by returning to the previous context. Example:: diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index 45a1f2a221..9f076b340e 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -32,7 +32,7 @@ def parse_args(): parser = argparse.ArgumentParser( description=""" opentelemetry-instrument automatically instruments a Python - program and it's dependencies and then runs the program. + program and its dependencies and then runs the program. """ ) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py index 96a771d719..4d6ce39eae 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py @@ -16,7 +16,7 @@ This module implements experimental propagators to inject trace context into response carriers. This is useful for server side frameworks that start traces when server requests and want to share the trace context with the client so the -client can add it's spans to the same trace. +client can add its spans to the same trace. This is part of an upcoming W3C spec and will eventually make it to the Otel spec. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 19531a75a5..ca22f39ee8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -532,7 +532,7 @@ class SpanLimits: All limit arguments must be either a non-negative integer, ``None`` or ``SpanLimits.UNSET``. - All limit arguments are optional. - - If a limit argument is not set, the class will try to read it's value from the corresponding + - If a limit argument is not set, the class will try to read its value from the corresponding environment variable. - If the environment variable is not set, the default value for the limit is used. diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py index 36184ca955..5ecfd2946b 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py @@ -608,7 +608,7 @@ class SpanAttributes: MESSAGING_KAFKA_MESSAGE_KEY = "messaging.kafka.message_key" """ Message keys in Kafka are used for grouping alike messages to ensure they're processed on the same partition. They differ from `messaging.message_id` in that they're not unique. If the key is `null`, the attribute MUST NOT be set. - Note: If the key type is not string, it's string representation has to be supplied for the attribute. If the key has no unambiguous, canonical string form, don't include its value. + Note: If the key type is not string, its string representation has to be supplied for the attribute. If the key has no unambiguous, canonical string form, don't include its value. """ MESSAGING_KAFKA_CONSUMER_GROUP = "messaging.kafka.consumer_group" From 4e0642b08967537e6c80fa8992615d4a93492671 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 16 Aug 2021 17:06:39 -0600 Subject: [PATCH 0951/1517] Remove contextvars workaround (#2009) --- .../context/aiocontextvarsfix.py | 86 ------------------- .../context/contextvars_context.py | 11 +-- .../tests/context/test_contextvars_context.py | 10 +-- 3 files changed, 4 insertions(+), 103 deletions(-) delete mode 100644 opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py diff --git a/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py b/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py deleted file mode 100644 index bd8100041c..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py +++ /dev/null @@ -1,86 +0,0 @@ -# type: ignore -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# This module is a patch to allow aiocontextvars to work for older versions -# of Python 3.5. It is copied and pasted from: -# https://github.com/fantix/aiocontextvars/issues/88#issuecomment-522276290 - -import asyncio -import asyncio.coroutines -import asyncio.futures -import concurrent.futures - -if not hasattr(asyncio, "_get_running_loop"): - # noinspection PyCompatibility - # pylint:disable=protected-access - import asyncio.events - from threading import local as threading_local - - if not hasattr(asyncio.events, "_get_running_loop"): - - class _RunningLoop(threading_local): - _loop = None - - _running_loop = _RunningLoop() - - def _get_running_loop(): - return _running_loop._loop - - def set_running_loop(loop): # noqa: F811 - _running_loop._loop = loop - - def _get_event_loop(): - current_loop = _get_running_loop() - if current_loop is not None: - return current_loop - return asyncio.events.get_event_loop_policy().get_event_loop() - - asyncio.events.get_event_loop = _get_event_loop - asyncio.events._get_running_loop = _get_running_loop - asyncio.events._set_running_loop = set_running_loop - - asyncio._get_running_loop = asyncio.events._get_running_loop - asyncio._set_running_loop = asyncio.events._set_running_loop - -# noinspection PyUnresolvedReferences -import aiocontextvars # pylint: disable=import-error,unused-import,wrong-import-position # noqa # isort:skip - - -def _run_coroutine_threadsafe(coro, loop): - """ - Patch to create task in the same thread instead of in the callback. - This ensures that contextvars get copied. Python 3.7 copies contextvars - without this. - """ - if not asyncio.coroutines.iscoroutine(coro): - raise TypeError("A coroutine object is required") - future = concurrent.futures.Future() - task = asyncio.ensure_future(coro, loop=loop) - - def callback() -> None: - try: - # noinspection PyProtectedMember,PyUnresolvedReferences - # pylint:disable=protected-access - asyncio.futures._chain_future(task, future) - except Exception as exc: - if future.set_running_or_notify_cancel(): - future.set_exception(exc) - raise - - loop.call_soon_threadsafe(callback) - return future - - -asyncio.run_coroutine_threadsafe = _run_coroutine_threadsafe diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index 2f8417ac01..589ed58c82 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -16,15 +16,10 @@ from opentelemetry.context.context import Context, _RuntimeContext -if (3, 5, 3) <= version_info < (3, 7): - import aiocontextvars # type: ignore # pylint:disable=import-error +if version_info < (3, 7): + import aiocontextvars # type: ignore # pylint: disable=import-error - aiocontextvars # pylint:disable=pointless-statement - -elif (3, 4) < version_info <= (3, 5, 2): - import opentelemetry.context.aiocontextvarsfix # pylint:disable=wrong-import-position - - opentelemetry.context.aiocontextvarsfix # pylint:disable=pointless-statement + aiocontextvars # pylint: disable=pointless-statement class ContextVarsRuntimeContext(_RuntimeContext): diff --git a/opentelemetry-api/tests/context/test_contextvars_context.py b/opentelemetry-api/tests/context/test_contextvars_context.py index 3aeaebcc29..a805602159 100644 --- a/opentelemetry-api/tests/context/test_contextvars_context.py +++ b/opentelemetry-api/tests/context/test_contextvars_context.py @@ -12,21 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest -from sys import version_info from unittest.mock import patch from opentelemetry import context +from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext from .base_context import ContextTestCases -if version_info.minor < 7: - raise unittest.SkipTest("contextvars not available") - -from opentelemetry.context.contextvars_context import ( # pylint:disable=wrong-import-position - ContextVarsRuntimeContext, -) - class TestContextVarsContext(ContextTestCases.BaseTest): def setUp(self) -> None: From 2a726e41efddf722662064ec219cfb352e6e1ca7 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Thu, 19 Aug 2021 19:26:24 +0530 Subject: [PATCH 0952/1517] Added support for `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` (#2044) Fixes #2045 Fixes #2043 Fixes #2042 Fixes #2041 --- CHANGELOG.md | 4 + .../src/opentelemetry/attributes/__init__.py | 100 +++++++++------ .../tests/attributes/test_attributes.py | 95 ++++++-------- .../sdk/environment_variables/__init__.py | 25 ++++ .../src/opentelemetry/sdk/trace/__init__.py | 71 ++++++----- opentelemetry-sdk/tests/trace/test_trace.py | 119 ++++++++++++++---- .../src/opentelemetry/test/spantestutil.py | 37 +++--- 7 files changed, 279 insertions(+), 172 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6bd176ae9..a6a47407be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add Trace ID validation to meet [TraceID spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#spancontext) ([#1992](https://github.com/open-telemetry/opentelemetry-python/pull/1992)) - Fixed Python 3.10 incompatibility in `opentelemetry-opentracing-shim` tests ([#2018](https://github.com/open-telemetry/opentelemetry-python/pull/2018)) +- `opentelemetry-sdk` added support for `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` + ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) +- `opentelemetry-sdk` Fixed bugs (#2041, #2042 & #2045) in Span Limits + ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) ## [0.23.1](https://github.com/open-telemetry/opentelemetry-python/pull/1987) - 2021-07-26 diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index e0b2a48ae2..877f98e8be 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -17,36 +17,59 @@ import threading from collections import OrderedDict from collections.abc import MutableMapping -from typing import MutableSequence, Optional, Sequence +from typing import Optional, Sequence, Union from opentelemetry.util import types -_VALID_ATTR_VALUE_TYPES = (bool, str, int, float) +# bytes are accepted as a user supplied value for attributes but +# decoded to strings internally. +_VALID_ATTR_VALUE_TYPES = (bool, str, bytes, int, float) _logger = logging.getLogger(__name__) -def _is_valid_attribute_value(value: types.AttributeValue) -> bool: - """Checks if attribute value is valid. +def _clean_attribute( + key: str, value: types.AttributeValue, max_len: Optional[int] +) -> Optional[types.AttributeValue]: + """Checks if attribute value is valid and cleans it if required. + + The function returns the cleaned value or None if the value is not valid. An attribute value is valid if it is either: - A primitive type: string, boolean, double precision floating point (IEEE 754-1985) or integer. - An array of primitive type values. The array MUST be homogeneous, i.e. it MUST NOT contain values of different types. + + An attribute needs cleansing if: + - Its length is greater than the maximum allowed length. + - It needs to be encoded/decoded e.g, bytes to strings. """ + if key is None or key == "": + _logger.warning("invalid key `%s` (empty or null)", key) + return None + if isinstance(value, _VALID_ATTR_VALUE_TYPES): - return True + return _clean_attribute_value(value, max_len) if isinstance(value, Sequence): - sequence_first_valid_type = None + cleaned_seq = [] + for element in value: + # None is considered valid in any sequence + if element is None: + cleaned_seq.append(element) + + element = _clean_attribute_value(element, max_len) + # reject invalid elements if element is None: continue + element_type = type(element) + # Reject attribute value if sequence contains a value with an incompatible type. if element_type not in _VALID_ATTR_VALUE_TYPES: _logger.warning( "Invalid type %s in attribute value sequence. Expected one of " @@ -57,19 +80,25 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool: for valid_type in _VALID_ATTR_VALUE_TYPES ], ) - return False + return None + # The type of the sequence must be homogeneous. The first non-None # element determines the type of the sequence if sequence_first_valid_type is None: sequence_first_valid_type = element_type - elif not isinstance(element, sequence_first_valid_type): + # use equality instead of isinstance as isinstance(True, int) evaluates to True + elif element_type != sequence_first_valid_type: _logger.warning( "Mixed types %s and %s in attribute value sequence", sequence_first_valid_type.__name__, type(element).__name__, ) - return False - return True + return None + + cleaned_seq.append(element) + + # Freeze mutable sequences defensively + return tuple(cleaned_seq) _logger.warning( "Invalid type %s for attribute value. Expected one of %s or a " @@ -77,36 +106,25 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool: type(value).__name__, [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES], ) - return False - + return None -def _filter_attributes(attributes: types.Attributes) -> None: - """Applies attribute validation rules and drops (key, value) pairs - that doesn't adhere to attributes specification. - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/common.md#attributes. - """ - if attributes: - for attr_key, attr_value in list(attributes.items()): - if not attr_key: - _logger.warning("invalid key `%s` (empty or null)", attr_key) - attributes.pop(attr_key) - continue +def _clean_attribute_value( + value: types.AttributeValue, limit: Optional[int] +) -> Union[types.AttributeValue, None]: + if value is None: + return None - if _is_valid_attribute_value(attr_value): - if isinstance(attr_value, MutableSequence): - attributes[attr_key] = tuple(attr_value) - if isinstance(attr_value, bytes): - try: - attributes[attr_key] = attr_value.decode() - except ValueError: - attributes.pop(attr_key) - _logger.warning("Byte attribute could not be decoded.") - else: - attributes.pop(attr_key) + if isinstance(value, bytes): + try: + value = value.decode() + except ValueError: + _logger.warning("Byte attribute could not be decoded.") + return None - -_DEFAULT_LIMIT = 128 + if limit is not None and isinstance(value, str): + value = value[:limit] + return value class BoundedAttributes(MutableMapping): @@ -118,9 +136,10 @@ class BoundedAttributes(MutableMapping): def __init__( self, - maxlen: Optional[int] = _DEFAULT_LIMIT, + maxlen: Optional[int] = None, attributes: types.Attributes = None, immutable: bool = True, + max_value_len: Optional[int] = None, ): if maxlen is not None: if not isinstance(maxlen, int) or maxlen < 0: @@ -129,10 +148,10 @@ def __init__( ) self.maxlen = maxlen self.dropped = 0 + self.max_value_len = max_value_len self._dict = OrderedDict() # type: OrderedDict self._lock = threading.Lock() # type: threading.Lock if attributes: - _filter_attributes(attributes) for key, value in attributes.items(): self[key] = value self._immutable = immutable @@ -158,7 +177,10 @@ def __setitem__(self, key, value): elif self.maxlen is not None and len(self._dict) == self.maxlen: del self._dict[next(iter(self._dict.keys()))] self.dropped += 1 - self._dict[key] = value + + value = _clean_attribute(key, value, self.max_value_len) + if value is not None: + self._dict[key] = value def __delitem__(self, key): if getattr(self, "_immutable", False): diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index c1151bf4d4..045c1f7125 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -16,69 +16,48 @@ import collections import unittest +from typing import MutableSequence -from opentelemetry.attributes import ( - BoundedAttributes, - _filter_attributes, - _is_valid_attribute_value, -) +from opentelemetry.attributes import BoundedAttributes, _clean_attribute class TestAttributes(unittest.TestCase): - def test_is_valid_attribute_value(self): - self.assertFalse(_is_valid_attribute_value([1, 2, 3.4, "ss", 4])) - self.assertFalse(_is_valid_attribute_value([dict(), 1, 2, 3.4, 4])) - self.assertFalse(_is_valid_attribute_value(["sw", "lf", 3.4, "ss"])) - self.assertFalse(_is_valid_attribute_value([1, 2, 3.4, 5])) - self.assertFalse(_is_valid_attribute_value(dict())) - self.assertTrue(_is_valid_attribute_value(True)) - self.assertTrue(_is_valid_attribute_value("hi")) - self.assertTrue(_is_valid_attribute_value(3.4)) - self.assertTrue(_is_valid_attribute_value(15)) - self.assertTrue(_is_valid_attribute_value([1, 2, 3, 5])) - self.assertTrue(_is_valid_attribute_value([1.2, 2.3, 3.4, 4.5])) - self.assertTrue(_is_valid_attribute_value([True, False])) - self.assertTrue(_is_valid_attribute_value(["ss", "dw", "fw"])) - self.assertTrue(_is_valid_attribute_value([])) + def assertValid(self, value, key="k"): + expected = value + if isinstance(value, MutableSequence): + expected = tuple(value) + self.assertEqual(_clean_attribute(key, value, None), expected) + + def assertInvalid(self, value, key="k"): + self.assertIsNone(_clean_attribute(key, value, None)) + + def test_clean_attribute(self): + self.assertInvalid([1, 2, 3.4, "ss", 4]) + self.assertInvalid([dict(), 1, 2, 3.4, 4]) + self.assertInvalid(["sw", "lf", 3.4, "ss"]) + self.assertInvalid([1, 2, 3.4, 5]) + self.assertInvalid(dict()) + self.assertInvalid([1, True]) + self.assertValid(True) + self.assertValid("hi") + self.assertValid(3.4) + self.assertValid(15) + self.assertValid([1, 2, 3, 5]) + self.assertValid([1.2, 2.3, 3.4, 4.5]) + self.assertValid([True, False]) + self.assertValid(["ss", "dw", "fw"]) + self.assertValid([]) # None in sequences are valid - self.assertTrue(_is_valid_attribute_value(["A", None, None])) - self.assertTrue(_is_valid_attribute_value(["A", None, None, "B"])) - self.assertTrue(_is_valid_attribute_value([None, None])) - self.assertFalse(_is_valid_attribute_value(["A", None, 1])) - self.assertFalse(_is_valid_attribute_value([None, "A", None, 1])) - - def test_filter_attributes(self): - attrs_with_invalid_keys = { - "": "empty-key", - None: "None-value", - "attr-key": "attr-value", - } - _filter_attributes(attrs_with_invalid_keys) - self.assertTrue(len(attrs_with_invalid_keys), 1) - self.assertEqual(attrs_with_invalid_keys, {"attr-key": "attr-value"}) - - attrs_with_invalid_values = { - "nonhomogeneous": [1, 2, 3.4, "ss", 4], - "nonprimitive": dict(), - "mixed": [1, 2.4, "st", dict()], - "validkey1": "validvalue1", - "intkey": 5, - "floatkey": 3.14, - "boolkey": True, - "valid-byte-string": b"hello-otel", - } - _filter_attributes(attrs_with_invalid_values) - self.assertEqual(len(attrs_with_invalid_values), 5) - self.assertEqual( - attrs_with_invalid_values, - { - "validkey1": "validvalue1", - "intkey": 5, - "floatkey": 3.14, - "boolkey": True, - "valid-byte-string": "hello-otel", - }, - ) + self.assertValid(["A", None, None]) + self.assertValid(["A", None, None, "B"]) + self.assertValid([None, None]) + self.assertInvalid(["A", None, 1]) + self.assertInvalid([None, "A", None, 1]) + + # test keys + self.assertValid("value", "key") + self.assertInvalid("value", "") + self.assertInvalid("value", None) class TestBoundedAttributes(unittest.TestCase): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 1a98d1f831..86ead4ae2d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -98,6 +98,22 @@ Default: 512 """ +OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT = "OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT" +""" +.. envvar:: OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT + +The :envvar:`OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed event attribute count. +Default: 128 +""" + +OTEL_LINK_ATTRIBUTE_COUNT_LIMIT = "OTEL_LINK_ATTRIBUTE_COUNT_LIMIT" +""" +.. envvar:: OTEL_LINK_ATTRIBUTE_COUNT_LIMIT + +The :envvar:`OTEL_LINK_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed link attribute count. +Default: 128 +""" + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT" """ .. envvar:: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT @@ -106,6 +122,15 @@ Default: 128 """ +OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT = ( + "OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT" +) +""" +.. envvar:: OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT + +The :envvar:`OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` represents the maximum allowed length attribute values can have. +""" + OTEL_SPAN_EVENT_COUNT_LIMIT = "OTEL_SPAN_EVENT_COUNT_LIMIT" """ .. envvar:: OTEL_SPAN_EVENT_COUNT_LIMIT diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index ca22f39ee8..60319e1bfa 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -29,7 +29,6 @@ Callable, Dict, Iterator, - MutableSequence, Optional, Sequence, Tuple, @@ -39,13 +38,13 @@ from opentelemetry import context as context_api from opentelemetry import trace as trace_api -from opentelemetry.attributes import ( - BoundedAttributes, - _is_valid_attribute_value, -) +from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk import util from opentelemetry.sdk.environment_variables import ( + OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, + OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT, OTEL_SPAN_LINK_COUNT_LIMIT, ) @@ -550,6 +549,8 @@ class SpanLimits: Default: {_DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT} max_link_attributes: Maximum number of attributes that can be added to a Link. Default: {_DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT} + max_attribute_length: Maximum length an attribute value can have. Values longer than + the specified length will be truncated. """ UNSET = -1 @@ -561,6 +562,7 @@ def __init__( max_links: Optional[int] = None, max_event_attributes: Optional[int] = None, max_link_attributes: Optional[int] = None, + max_attribute_length: Optional[int] = None, ): self.max_attributes = self._from_env_if_absent( max_attributes, @@ -579,28 +581,33 @@ def __init__( ) self.max_event_attributes = self._from_env_if_absent( max_event_attributes, - OTEL_SPAN_LINK_COUNT_LIMIT, + OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, ) self.max_link_attributes = self._from_env_if_absent( max_link_attributes, - OTEL_SPAN_LINK_COUNT_LIMIT, + OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, ) + self.max_attribute_length = self._from_env_if_absent( + max_attribute_length, + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, + ) def __repr__(self): - return "{}(max_attributes={}, max_events={}, max_links={}, max_event_attributes={}, max_link_attributes={})".format( + return "{}(max_attributes={}, max_events={}, max_links={}, max_event_attributes={}, max_link_attributes={}, max_attribute_length={})".format( type(self).__name__, self.max_attributes, self.max_events, self.max_links, self.max_event_attributes, self.max_link_attributes, + self.max_attribute_length, ) @classmethod def _from_env_if_absent( - cls, value: Optional[int], env_var: str, default: Optional[int] + cls, value: Optional[int], env_var: str, default: Optional[int] = None ) -> Optional[int]: if value is cls.UNSET: return None @@ -630,8 +637,10 @@ def _from_env_if_absent( max_links=SpanLimits.UNSET, max_event_attributes=SpanLimits.UNSET, max_link_attributes=SpanLimits.UNSET, + max_attribute_length=SpanLimits.UNSET, ) +# not remove for backward compat. please use SpanLimits instead. SPAN_ATTRIBUTE_COUNT_LIMIT = SpanLimits._from_env_if_absent( None, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, @@ -701,19 +710,30 @@ def __init__( self._limits = limits self._lock = threading.Lock() self._attributes = BoundedAttributes( - self._limits.max_attributes, attributes, immutable=False + self._limits.max_attributes, + attributes, + immutable=False, + max_value_len=self._limits.max_attribute_length, ) self._events = self._new_events() if events: for event in events: event._attributes = BoundedAttributes( - self._limits.max_event_attributes, event.attributes + self._limits.max_event_attributes, + event.attributes, + max_value_len=self._limits.max_attribute_length, ) self._events.append(event) if links is None: self._links = self._new_links() else: + for link in links: + link._attributes = BoundedAttributes( + self._limits.max_link_attributes, + link.attributes, + max_value_len=self._limits.max_attribute_length, + ) self._links = BoundedList.from_seq(self._limits.max_links, links) def __repr__(self): @@ -739,25 +759,6 @@ def set_attributes( return for key, value in attributes.items(): - if not _is_valid_attribute_value(value): - continue - - if not key: - logger.warning("invalid key `%s` (empty or null)", key) - continue - - # Freeze mutable sequences defensively - if isinstance(value, MutableSequence): - value = tuple(value) - if isinstance(value, bytes): - try: - value = value.decode() - except ValueError: - logger.warning( - "Byte attribute could not be decoded for key `%s`.", - key, - ) - return self._attributes[key] = value def set_attribute(self, key: str, value: types.AttributeValue) -> None: @@ -774,7 +775,9 @@ def add_event( timestamp: Optional[int] = None, ) -> None: attributes = BoundedAttributes( - self._limits.max_event_attributes, attributes + self._limits.max_event_attributes, + attributes, + max_value_len=self._limits.max_attribute_length, ) self._add_event( Event( @@ -1062,6 +1065,12 @@ def __init__( self.sampler = sampler self._span_limits = span_limits or SpanLimits() self._atexit_handler = None + + self._resource._attributes = BoundedAttributes( + self._span_limits.max_attributes, + self._resource._attributes, + max_value_len=self._span_limits.max_attribute_length, + ) if shutdown_on_exit: self._atexit_handler = atexit.register(self.shutdown) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index e331642e44..29609184d2 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -27,6 +27,7 @@ from opentelemetry.sdk import resources, trace from opentelemetry.sdk.environment_variables import ( OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT, OTEL_SPAN_LINK_COUNT_LIMIT, OTEL_TRACES_SAMPLER, @@ -38,15 +39,12 @@ from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, + new_tracer, ) from opentelemetry.trace import StatusCode from opentelemetry.util._time import _time_ns -def new_tracer(span_limits=None) -> trace_api.Tracer: - return trace.TracerProvider(span_limits=span_limits).get_tracer(__name__) - - class TestTracer(unittest.TestCase): def test_extends_api(self): tracer = new_tracer() @@ -653,6 +651,7 @@ def test_invalid_attribute_values(self): root.set_attribute( "list-with-non-primitive-data-type", [dict(), 123] ) + root.set_attribute("list-with-numeric-and-bool", [1, True]) root.set_attribute("", 123) root.set_attribute(None, 123) @@ -1314,6 +1313,15 @@ def test_attributes_to_json(self): class TestSpanLimits(unittest.TestCase): # pylint: disable=protected-access + long_val = "v" * 1000 + + def _assert_attr_length(self, attr_val, max_len): + if isinstance(attr_val, str): + expected = self.long_val + if max_len is not None: + expected = expected[:max_len] + self.assertEqual(attr_val, expected) + def test_limits_defaults(self): limits = trace.SpanLimits() self.assertEqual( @@ -1326,9 +1334,11 @@ def test_limits_defaults(self): self.assertEqual( limits.max_links, trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT ) + self.assertIsNone(limits.max_attribute_length) def test_limits_values_code(self): - max_attributes, max_events, max_links = ( + max_attributes, max_events, max_links, max_attr_length = ( + randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), @@ -1337,13 +1347,16 @@ def test_limits_values_code(self): max_attributes=max_attributes, max_events=max_events, max_links=max_links, + max_attribute_length=max_attr_length, ) self.assertEqual(limits.max_attributes, max_attributes) self.assertEqual(limits.max_events, max_events) self.assertEqual(limits.max_links, max_links) + self.assertEqual(limits.max_attribute_length, max_attr_length) def test_limits_values_env(self): - max_attributes, max_events, max_links = ( + max_attributes, max_events, max_links, max_attr_length = ( + randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), @@ -1354,6 +1367,7 @@ def test_limits_values_env(self): OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: str(max_attributes), OTEL_SPAN_EVENT_COUNT_LIMIT: str(max_events), OTEL_SPAN_LINK_COUNT_LIMIT: str(max_links), + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: str(max_attr_length), }, ): limits = trace.SpanLimits() @@ -1361,7 +1375,9 @@ def test_limits_values_env(self): self.assertEqual(limits.max_events, max_events) self.assertEqual(limits.max_links, max_links) - def _test_span_limits(self, tracer): + def _test_span_limits( + self, tracer, max_attrs, max_events, max_links, max_attr_len + ): id_generator = RandomIdGenerator() some_links = [ trace_api.Link( @@ -1369,25 +1385,48 @@ def _test_span_limits(self, tracer): trace_id=id_generator.generate_trace_id(), span_id=id_generator.generate_span_id(), is_remote=False, - ) + ), + attributes={"k": self.long_val}, ) for _ in range(100) ] some_attrs = { - "init_attribute_{}".format(idx): idx for idx in range(100) + "init_attribute_{}".format(idx): self.long_val + for idx in range(100) } with tracer.start_as_current_span( "root", links=some_links, attributes=some_attrs ) as root: - self.assertEqual(len(root.links), 30) - self.assertEqual(len(root.attributes), 10) + self.assertEqual(len(root.links), max_links) + self.assertEqual(len(root.attributes), max_attrs) for idx in range(100): - root.set_attribute("my_attribute_{}".format(idx), 0) - root.add_event("my_event_{}".format(idx)) + root.set_attribute( + "my_str_attribute_{}".format(idx), self.long_val + ) + root.set_attribute( + "my_byte_attribute_{}".format(idx), self.long_val.encode() + ) + root.set_attribute( + "my_int_attribute_{}".format(idx), self.long_val.encode() + ) + root.add_event( + "my_event_{}".format(idx), attributes={"k": self.long_val} + ) - self.assertEqual(len(root.attributes), 10) - self.assertEqual(len(root.events), 20) + self.assertEqual(len(root.attributes), max_attrs) + self.assertEqual(len(root.events), max_events) + + for link in root.links: + for attr_val in link.attributes.values(): + self._assert_attr_length(attr_val, max_attr_len) + + for event in root.events: + for attr_val in event.attributes.values(): + self._assert_attr_length(attr_val, max_attr_len) + + for attr_val in root.attributes.values(): + self._assert_attr_length(attr_val, max_attr_len) def _test_span_no_limits(self, tracer): num_links = int(trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT) + randint( @@ -1413,7 +1452,9 @@ def _test_span_no_limits(self, tracer): ) with tracer.start_as_current_span("root") as root: for idx in range(num_events): - root.add_event("my_event_{}".format(idx)) + root.add_event( + "my_event_{}".format(idx), attributes={"k": self.long_val} + ) self.assertEqual(len(root.events), num_events) @@ -1422,20 +1463,31 @@ def _test_span_no_limits(self, tracer): ) + randint(1, 100) with tracer.start_as_current_span("root") as root: for idx in range(num_attributes): - root.set_attribute("my_attribute_{}".format(idx), 0) + root.set_attribute( + "my_attribute_{}".format(idx), self.long_val + ) self.assertEqual(len(root.attributes), num_attributes) + for attr_val in root.attributes.values(): + self.assertEqual(attr_val, self.long_val) @mock.patch.dict( "os.environ", { - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10", - OTEL_SPAN_EVENT_COUNT_LIMIT: "20", - OTEL_SPAN_LINK_COUNT_LIMIT: "30", + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "13", + OTEL_SPAN_EVENT_COUNT_LIMIT: "7", + OTEL_SPAN_LINK_COUNT_LIMIT: "4", + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "11", }, ) def test_span_limits_env(self): - self._test_span_limits(new_tracer()) + self._test_span_limits( + new_tracer(), + max_attrs=13, + max_events=7, + max_links=4, + max_attr_len=11, + ) @mock.patch.dict( "os.environ", @@ -1443,24 +1495,39 @@ def test_span_limits_env(self): OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10", OTEL_SPAN_EVENT_COUNT_LIMIT: "20", OTEL_SPAN_LINK_COUNT_LIMIT: "30", + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "40", }, ) def test_span_limits_default_to_env(self): self._test_span_limits( new_tracer( span_limits=trace.SpanLimits( - max_attributes=None, max_events=None, max_links=None + max_attributes=None, + max_events=None, + max_links=None, + max_attribute_length=None, ) - ) + ), + max_attrs=10, + max_events=20, + max_links=30, + max_attr_len=40, ) def test_span_limits_code(self): self._test_span_limits( new_tracer( span_limits=trace.SpanLimits( - max_attributes=10, max_events=20, max_links=30 + max_attributes=11, + max_events=15, + max_links=13, + max_attribute_length=9, ) - ) + ), + max_attrs=11, + max_events=15, + max_links=13, + max_attr_len=9, ) @mock.patch.dict( @@ -1469,6 +1536,7 @@ def test_span_limits_code(self): OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "unset", OTEL_SPAN_EVENT_COUNT_LIMIT: "unset", OTEL_SPAN_LINK_COUNT_LIMIT: "unset", + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "unset", }, ) def test_span_no_limits_env(self): @@ -1481,6 +1549,7 @@ def test_span_no_limits_code(self): max_attributes=trace.SpanLimits.UNSET, max_links=trace.SpanLimits.UNSET, max_events=trace.SpanLimits.UNSET, + max_attribute_length=trace.SpanLimits.UNSET, ) ) ) diff --git a/tests/util/src/opentelemetry/test/spantestutil.py b/tests/util/src/opentelemetry/test/spantestutil.py index faf135f2ae..408f4c4947 100644 --- a/tests/util/src/opentelemetry/test/spantestutil.py +++ b/tests/util/src/opentelemetry/test/spantestutil.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest +from functools import partial from importlib import reload from opentelemetry import trace as trace_api @@ -25,6 +26,13 @@ _MEMORY_EXPORTER = None +def new_tracer(span_limits=None, resource=None) -> trace_api.Tracer: + provider_factory = trace_sdk.TracerProvider + if resource is not None: + provider_factory = partial(provider_factory, resource=resource) + return provider_factory(span_limits=span_limits).get_tracer(__name__) + + class SpanTestBase(unittest.TestCase): @classmethod def setUpClass(cls): @@ -60,23 +68,14 @@ def get_span_with_dropped_attributes_events_links(): attributes=attributes, ) ) - span = trace_sdk._Span( - limits=trace_sdk.SpanLimits(), - name="span", - resource=Resource( - attributes=attributes, - ), - context=trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x00000000DEADBEF0, - is_remote=False, - ), - links=links, - attributes=attributes, - ) - span.start() - for index in range(131): - span.add_event("event{}".format(index), attributes=attributes) - span.end() - return span + tracer = new_tracer( + span_limits=trace_sdk.SpanLimits(), + resource=Resource(attributes=attributes), + ) + with tracer.start_as_current_span( + "span", links=links, attributes=attributes + ) as span: + for index in range(131): + span.add_event("event{}".format(index), attributes=attributes) + return span From 653207dd2181db1a766a4a703dcda78fd7703bb2 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Thu, 19 Aug 2021 23:55:27 +0530 Subject: [PATCH 0953/1517] Restrict attribute keys to non-empty strings (#2057) According to the spec: > The attribute key, which MUST be a non-null and non-empty string. https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/common.md --- CHANGELOG.md | 2 ++ .../src/opentelemetry/attributes/__init__.py | 6 +++--- .../tests/attributes/test_attributes.py | 14 ++++++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6a47407be..dcc8a12632 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) - `opentelemetry-sdk` Fixed bugs (#2041, #2042 & #2045) in Span Limits ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) +- `opentelemetry-api` Attribute keys must be non-empty strings. + ([#2057](https://github.com/open-telemetry/opentelemetry-python/pull/2057)) ## [0.23.1](https://github.com/open-telemetry/opentelemetry-python/pull/1987) - 2021-07-26 diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 877f98e8be..a266839326 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -47,8 +47,8 @@ def _clean_attribute( - It needs to be encoded/decoded e.g, bytes to strings. """ - if key is None or key == "": - _logger.warning("invalid key `%s` (empty or null)", key) + if not (key and isinstance(key, str)): + _logger.warning("invalid key `%s`. must be non-empty string.", key) return None if isinstance(value, _VALID_ATTR_VALUE_TYPES): @@ -118,7 +118,7 @@ def _clean_attribute_value( if isinstance(value, bytes): try: value = value.decode() - except ValueError: + except UnicodeDecodeError: _logger.warning("Byte attribute could not be decoded.") return None diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index 045c1f7125..fa0606b347 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -31,6 +31,16 @@ def assertValid(self, value, key="k"): def assertInvalid(self, value, key="k"): self.assertIsNone(_clean_attribute(key, value, None)) + def test_attribute_key_validation(self): + # only non-empty strings are valid keys + self.assertInvalid(1, "") + self.assertInvalid(1, 1) + self.assertInvalid(1, {}) + self.assertInvalid(1, []) + self.assertInvalid(1, b"1") + self.assertValid(1, "k") + self.assertValid(1, "1") + def test_clean_attribute(self): self.assertInvalid([1, 2, 3.4, "ss", 4]) self.assertInvalid([dict(), 1, 2, 3.4, 4]) @@ -142,10 +152,10 @@ def test_bounded_dict(self): def test_no_limit_code(self): bdict = BoundedAttributes(maxlen=None, immutable=False) for num in range(100): - bdict[num] = num + bdict[str(num)] = num for num in range(100): - self.assertEqual(bdict[num], num) + self.assertEqual(bdict[str(num)], num) def test_immutable(self): bdict = BoundedAttributes() From 1bb44392d61fde496b6c2caa030f85b532792418 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 20 Aug 2021 00:43:03 +0530 Subject: [PATCH 0954/1517] Update getting-started guide (#2053) --- website_docs/getting-started.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/website_docs/getting-started.md b/website_docs/getting-started.md index d8850191ca..8a569fc210 100644 --- a/website_docs/getting-started.md +++ b/website_docs/getting-started.md @@ -39,11 +39,11 @@ from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleSpanProcessor, + BatchSpanProcessor, ) provider = TracerProvider() -processor = SimpleSpanProcessor(ConsoleSpanExporter()) +processor = BatchSpanProcessor(ConsoleSpanExporter()) provider.add_span_processor(processor) trace.set_tracer_provider(provider) @@ -54,6 +54,9 @@ with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): print("Hello world from OpenTelemetry Python!") + +# Flush all ended spans that are yet to be written to stdout before process exit. +provider.force_flush() ``` When you run the script you can see the traces printed to your console: @@ -223,12 +226,12 @@ from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, - SimpleSpanProcessor, + BatchSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( - SimpleSpanProcessor(ConsoleSpanExporter()) + BatchSpanProcessor(ConsoleSpanExporter()) ) app = flask.Flask(__name__) @@ -244,7 +247,7 @@ def hello(): return "hello" -app.run(debug=True, port=5000) +app.run(port=5000) ``` Now run the script, hit the root URL ([http://localhost:5000/](http://localhost:5000/)) a few times, and watch your spans be emitted! From aa1a2acea99e508f54b03cccf2b28060f52c758e Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Sun, 22 Aug 2021 22:31:32 +0300 Subject: [PATCH 0955/1517] docs(auto-instrument): add missing pip install in README (#2061) --- docs/examples/auto-instrumentation/README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index 23fb47b396..dd33a5ea6f 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -80,6 +80,7 @@ commands that help automatically instruments a program. $ pip install opentelemetry-sdk $ pip install opentelemetry-instrumentation $ pip install opentelemetry-instrumentation-flask + $ pip install flask $ pip install requests Execute From b5704d58a7ce6fb75e6bc2c052d9c80565f33247 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 24 Aug 2021 19:18:04 +0530 Subject: [PATCH 0956/1517] Fix limit env vars unset/unlimited values (#2054) * Added support for `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` Fixes #2045 Fixes #2043 Fixes #2042 Fixes #2041 * SpanLimit: Treat empty value env vars as unset Fixes #2052 * Update CHANGELOG.md Co-authored-by: Leighton Chen Co-authored-by: Leighton Chen --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/__init__.py | 13 ++++++++----- opentelemetry-sdk/tests/trace/test_trace.py | 8 ++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcc8a12632..3d024b7a7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) - `opentelemetry-sdk` Fixed bugs (#2041, #2042 & #2045) in Span Limits ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) +- `opentelemetry-sdk` Treat limit even vars set to empty values as unset/unlimited. + ([#2054](https://github.com/open-telemetry/opentelemetry-python/pull/2054)) - `opentelemetry-api` Attribute keys must be non-empty strings. ([#2057](https://github.com/open-telemetry/opentelemetry-python/pull/2057)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 60319e1bfa..f3c4a54283 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -67,7 +67,7 @@ _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT = 128 -_ENV_VALUE_UNSET = "unset" +_ENV_VALUE_UNSET = "" # pylint: disable=protected-access _TRACE_SAMPLER = sampling._get_from_env_or_default() @@ -533,7 +533,7 @@ class SpanLimits: - All limit arguments are optional. - If a limit argument is not set, the class will try to read its value from the corresponding environment variable. - - If the environment variable is not set, the default value for the limit is used. + - If the environment variable is not set, the default value, if any, will be used. Args: max_attributes: Maximum number of attributes that can be added to a Span. @@ -609,15 +609,18 @@ def __repr__(self): def _from_env_if_absent( cls, value: Optional[int], env_var: str, default: Optional[int] = None ) -> Optional[int]: - if value is cls.UNSET: + if value == cls.UNSET: return None err_msg = "{0} must be a non-negative integer but got {}" + # if no value is provided for the limit, try to load it from env if value is None: - str_value = environ.get(env_var, "").strip().lower() - if not str_value: + # return default value if env var is not set + if env_var not in environ: return default + + str_value = environ.get(env_var, "").strip().lower() if str_value == _ENV_VALUE_UNSET: return None diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 29609184d2..0d486302ab 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -1533,10 +1533,10 @@ def test_span_limits_code(self): @mock.patch.dict( "os.environ", { - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "unset", - OTEL_SPAN_EVENT_COUNT_LIMIT: "unset", - OTEL_SPAN_LINK_COUNT_LIMIT: "unset", - OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "unset", + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "", + OTEL_SPAN_EVENT_COUNT_LIMIT: "", + OTEL_SPAN_LINK_COUNT_LIMIT: "", + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "", }, ) def test_span_no_limits_env(self): From 4250078e43ddb24c88e19270c7af01ae63336fb9 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 24 Aug 2021 20:24:39 +0530 Subject: [PATCH 0957/1517] Add support for `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var (#2056) * Added support for `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` Fixes #2045 Fixes #2043 Fixes #2042 Fixes #2041 --- CHANGELOG.md | 2 + .../sdk/environment_variables/__init__.py | 10 +- .../src/opentelemetry/sdk/trace/__init__.py | 27 ++- opentelemetry-sdk/tests/trace/test_trace.py | 221 ++++++++++-------- 4 files changed, 161 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d024b7a7b..6c4e686b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) - `opentelemetry-sdk` Fixed bugs (#2041, #2042 & #2045) in Span Limits ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) +- `opentelemetry-sdk` Add support for `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var + ([#2056](https://github.com/open-telemetry/opentelemetry-python/pull/2056)) - `opentelemetry-sdk` Treat limit even vars set to empty values as unset/unlimited. ([#2054](https://github.com/open-telemetry/opentelemetry-python/pull/2054)) - `opentelemetry-api` Attribute keys must be non-empty strings. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 86ead4ae2d..11fc5af8cf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -98,6 +98,13 @@ Default: 512 """ +OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT = "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT" +""" +.. envvar:: OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT + +The :envvar:`OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` represents the maximum allowed attribute length. +""" + OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT = "OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT" """ .. envvar:: OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT @@ -128,7 +135,8 @@ """ .. envvar:: OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT -The :envvar:`OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` represents the maximum allowed length attribute values can have. +The :envvar:`OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` represents the maximum allowed length +span attribute values can have. This takes precedence over :envvar:`OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT`. """ OTEL_SPAN_EVENT_COUNT_LIMIT = "OTEL_SPAN_EVENT_COUNT_LIMIT" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index f3c4a54283..135546b362 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -41,6 +41,7 @@ from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk import util from opentelemetry.sdk.environment_variables import ( + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, @@ -535,6 +536,12 @@ class SpanLimits: environment variable. - If the environment variable is not set, the default value, if any, will be used. + Limit precedence: + + - If a model specific limit is set, it will be used. + - Else if the model specific limit has a default value, the default value will be used. + - Else if model specific limit has a corresponding global limit, the global limit will be used. + Args: max_attributes: Maximum number of attributes that can be added to a Span. Environment variable: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT @@ -551,6 +558,8 @@ class SpanLimits: Default: {_DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT} max_attribute_length: Maximum length an attribute value can have. Values longer than the specified length will be truncated. + max_span_attribute_length: Maximum length a span attribute value can have. Values longer than + the specified length will be truncated. """ UNSET = -1 @@ -563,6 +572,7 @@ def __init__( max_event_attributes: Optional[int] = None, max_link_attributes: Optional[int] = None, max_attribute_length: Optional[int] = None, + max_span_attribute_length: Optional[int] = None, ): self.max_attributes = self._from_env_if_absent( max_attributes, @@ -589,19 +599,27 @@ def __init__( OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, ) + self.max_attribute_length = self._from_env_if_absent( max_attribute_length, + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, + ) + self.max_span_attribute_length = self._from_env_if_absent( + max_span_attribute_length, OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, + # use global attribute length limit as default + self.max_attribute_length, ) def __repr__(self): - return "{}(max_attributes={}, max_events={}, max_links={}, max_event_attributes={}, max_link_attributes={}, max_attribute_length={})".format( + return "{}(max_span_attributes={}, max_events_attributes={}, max_link_attributes={}, max_attributes={}, max_events={}, max_links={}, max_attribute_length={})".format( type(self).__name__, + self.max_span_attribute_length, + self.max_event_attributes, + self.max_link_attributes, self.max_attributes, self.max_events, self.max_links, - self.max_event_attributes, - self.max_link_attributes, self.max_attribute_length, ) @@ -641,6 +659,7 @@ def _from_env_if_absent( max_event_attributes=SpanLimits.UNSET, max_link_attributes=SpanLimits.UNSET, max_attribute_length=SpanLimits.UNSET, + max_span_attribute_length=SpanLimits.UNSET, ) # not remove for backward compat. please use SpanLimits instead. @@ -716,7 +735,7 @@ def __init__( self._limits.max_attributes, attributes, immutable=False, - max_value_len=self._limits.max_attribute_length, + max_value_len=self._limits.max_span_attribute_length, ) self._events = self._new_events() if events: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 0d486302ab..3db5bcef9a 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -26,6 +26,7 @@ from opentelemetry.context import Context from opentelemetry.sdk import resources, trace from opentelemetry.sdk.environment_variables import ( + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT, @@ -1335,6 +1336,25 @@ def test_limits_defaults(self): limits.max_links, trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT ) self.assertIsNone(limits.max_attribute_length) + self.assertIsNone(limits.max_span_attribute_length) + + def test_limits_attribute_length_limits_code(self): + # global limit unset while span limit is set + limits = trace.SpanLimits(max_span_attribute_length=22) + self.assertIsNone(limits.max_attribute_length) + self.assertEqual(limits.max_span_attribute_length, 22) + + # span limit falls back to global limit when no value is provided + limits = trace.SpanLimits(max_attribute_length=22) + self.assertEqual(limits.max_attribute_length, 22) + self.assertEqual(limits.max_span_attribute_length, 22) + + # global and span limits set to different values + limits = trace.SpanLimits( + max_attribute_length=22, max_span_attribute_length=33 + ) + self.assertEqual(limits.max_attribute_length, 22) + self.assertEqual(limits.max_span_attribute_length, 33) def test_limits_values_code(self): max_attributes, max_events, max_links, max_attr_length = ( @@ -1375,8 +1395,113 @@ def test_limits_values_env(self): self.assertEqual(limits.max_events, max_events) self.assertEqual(limits.max_links, max_links) + @mock.patch.dict( + "os.environ", + { + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "13", + OTEL_SPAN_EVENT_COUNT_LIMIT: "7", + OTEL_SPAN_LINK_COUNT_LIMIT: "4", + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: "11", + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "15", + }, + ) + def test_span_limits_env(self): + self._test_span_limits( + new_tracer(), + max_attrs=13, + max_events=7, + max_links=4, + max_attr_len=11, + max_span_attr_len=15, + ) + + @mock.patch.dict( + "os.environ", + { + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10", + OTEL_SPAN_EVENT_COUNT_LIMIT: "20", + OTEL_SPAN_LINK_COUNT_LIMIT: "30", + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: "40", + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "50", + }, + ) + def test_span_limits_default_to_env(self): + self._test_span_limits( + new_tracer( + span_limits=trace.SpanLimits( + max_attributes=None, + max_events=None, + max_links=None, + max_attribute_length=None, + max_span_attribute_length=None, + ) + ), + max_attrs=10, + max_events=20, + max_links=30, + max_attr_len=40, + max_span_attr_len=50, + ) + + def test_span_limits_code(self): + self._test_span_limits( + new_tracer( + span_limits=trace.SpanLimits( + max_attributes=11, + max_events=15, + max_links=13, + max_attribute_length=9, + max_span_attribute_length=25, + ) + ), + max_attrs=11, + max_events=15, + max_links=13, + max_attr_len=9, + max_span_attr_len=25, + ) + + @mock.patch.dict( + "os.environ", + { + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "", + OTEL_SPAN_EVENT_COUNT_LIMIT: "", + OTEL_SPAN_LINK_COUNT_LIMIT: "", + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "", + }, + ) + def test_span_no_limits_env(self): + self._test_span_no_limits(new_tracer()) + + def test_span_no_limits_code(self): + self._test_span_no_limits( + new_tracer( + span_limits=trace.SpanLimits( + max_attributes=trace.SpanLimits.UNSET, + max_links=trace.SpanLimits.UNSET, + max_events=trace.SpanLimits.UNSET, + max_attribute_length=trace.SpanLimits.UNSET, + ) + ) + ) + + def test_dropped_attributes(self): + span = get_span_with_dropped_attributes_events_links() + self.assertEqual(1, span.dropped_links) + self.assertEqual(2, span.dropped_attributes) + self.assertEqual(3, span.dropped_events) + self.assertEqual(2, span.events[0].attributes.dropped) + self.assertEqual(2, span.links[0].attributes.dropped) + self.assertEqual(2, span.resource.attributes.dropped) + def _test_span_limits( - self, tracer, max_attrs, max_events, max_links, max_attr_len + self, + tracer, + max_attrs, + max_events, + max_links, + max_attr_len, + max_span_attr_len, ): id_generator = RandomIdGenerator() some_links = [ @@ -1426,7 +1551,7 @@ def _test_span_limits( self._assert_attr_length(attr_val, max_attr_len) for attr_val in root.attributes.values(): - self._assert_attr_length(attr_val, max_attr_len) + self._assert_attr_length(attr_val, max_span_attr_len) def _test_span_no_limits(self, tracer): num_links = int(trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT) + randint( @@ -1470,95 +1595,3 @@ def _test_span_no_limits(self, tracer): self.assertEqual(len(root.attributes), num_attributes) for attr_val in root.attributes.values(): self.assertEqual(attr_val, self.long_val) - - @mock.patch.dict( - "os.environ", - { - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "13", - OTEL_SPAN_EVENT_COUNT_LIMIT: "7", - OTEL_SPAN_LINK_COUNT_LIMIT: "4", - OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "11", - }, - ) - def test_span_limits_env(self): - self._test_span_limits( - new_tracer(), - max_attrs=13, - max_events=7, - max_links=4, - max_attr_len=11, - ) - - @mock.patch.dict( - "os.environ", - { - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10", - OTEL_SPAN_EVENT_COUNT_LIMIT: "20", - OTEL_SPAN_LINK_COUNT_LIMIT: "30", - OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "40", - }, - ) - def test_span_limits_default_to_env(self): - self._test_span_limits( - new_tracer( - span_limits=trace.SpanLimits( - max_attributes=None, - max_events=None, - max_links=None, - max_attribute_length=None, - ) - ), - max_attrs=10, - max_events=20, - max_links=30, - max_attr_len=40, - ) - - def test_span_limits_code(self): - self._test_span_limits( - new_tracer( - span_limits=trace.SpanLimits( - max_attributes=11, - max_events=15, - max_links=13, - max_attribute_length=9, - ) - ), - max_attrs=11, - max_events=15, - max_links=13, - max_attr_len=9, - ) - - @mock.patch.dict( - "os.environ", - { - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "", - OTEL_SPAN_EVENT_COUNT_LIMIT: "", - OTEL_SPAN_LINK_COUNT_LIMIT: "", - OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "", - }, - ) - def test_span_no_limits_env(self): - self._test_span_no_limits(new_tracer()) - - def test_span_no_limits_code(self): - self._test_span_no_limits( - new_tracer( - span_limits=trace.SpanLimits( - max_attributes=trace.SpanLimits.UNSET, - max_links=trace.SpanLimits.UNSET, - max_events=trace.SpanLimits.UNSET, - max_attribute_length=trace.SpanLimits.UNSET, - ) - ) - ) - - def test_dropped_attributes(self): - span = get_span_with_dropped_attributes_events_links() - self.assertEqual(1, span.dropped_links) - self.assertEqual(2, span.dropped_attributes) - self.assertEqual(3, span.dropped_events) - self.assertEqual(2, span.events[0].attributes.dropped) - self.assertEqual(2, span.links[0].attributes.dropped) - self.assertEqual(2, span.resource.attributes.dropped) From 3b190f5af766294c5064d2787ab6c8681be1619d Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Thu, 26 Aug 2021 14:54:39 +0530 Subject: [PATCH 0958/1517] updating changelogs and version to 1.5.0-0.24b0 (#2069) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 6 +- eachdist.ini | 4 +- .../exporter/jaeger/proto/grpc/version.py | 2 +- .../exporter/jaeger/thrift/version.py | 2 +- .../opentelemetry-exporter-jaeger/setup.cfg | 4 +- .../opentelemetry/exporter/jaeger/version.py | 2 +- .../exporter/opencensus/version.py | 2 +- .../setup.cfg | 2 +- .../exporter/otlp/proto/grpc/version.py | 2 +- .../setup.cfg | 2 +- .../exporter/otlp/proto/http/version.py | 2 +- .../opentelemetry-exporter-otlp/setup.cfg | 2 +- .../opentelemetry/exporter/otlp/version.py | 2 +- .../exporter/zipkin/json/version.py | 2 +- .../setup.cfg | 2 +- .../exporter/zipkin/proto/http/version.py | 2 +- .../opentelemetry-exporter-zipkin/setup.cfg | 4 +- .../opentelemetry/exporter/zipkin/version.py | 2 +- .../src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 6 +- .../src/opentelemetry/distro/version.py | 2 +- .../instrumentation/bootstrap_gen.py | 68 +++++++++---------- .../opentelemetry/instrumentation/version.py | 2 +- .../src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 6 +- .../src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../opentelemetry/propagators/b3/version.py | 2 +- .../propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../shim/opentracing_shim/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 33 files changed, 77 insertions(+), 73 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a20e434ef8..02cdd49356 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: dd65a29b5805397e3b511d6546179e7c03442f82 + CONTRIB_REPO_SHA: 3ad534cbba41c2b92618f5f03c4c92cee4a72df6 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c4e686b61..67585bde86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.4.0-0.23b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.5.0-0.24b0...HEAD) + +## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 + + - Fix documentation on well known exporters and variable OTEL_TRACES_EXPORTER which were misnamed ([#2023](https://github.com/open-telemetry/opentelemetry-python/pull/2023)) - `opentelemetry-sdk` `get_aggregated_resource()` returns default resource and service name diff --git a/eachdist.ini b/eachdist.ini index bb62be66c8..746f5a787a 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -14,7 +14,7 @@ sortfirst= exporter/* [stable] -version=1.5.0.dev0 +version=1.5.0 packages= opentelemetry-sdk @@ -33,7 +33,7 @@ packages= opentelemetry-api [prerelease] -version=0.24.dev0 +version=0.24b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 38b21f37cb..14cffddd57 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 38b21f37cb..14cffddd57 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index b01b3a8d9e..06e68fded5 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,8 +39,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.5.0.dev0 - opentelemetry-exporter-jaeger-thrift == 1.5.0.dev0 + opentelemetry-exporter-jaeger-proto-grpc == 1.5.0 + opentelemetry-exporter-jaeger-thrift == 1.5.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 38b21f37cb..14cffddd57 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 5aeaec6556..d33bd87ce4 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24.dev0" +__version__ = "0.24b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 46ed5650e3..7013425c95 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.5.0.dev0 + opentelemetry-proto == 1.5.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 36cd1d2223..c8902adc49 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index c99b806e98..5f6d102d12 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.5.0.dev0 + opentelemetry-proto == 1.5.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 5aeaec6556..c8902adc49 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index bd5c2aa2f5..8d24fca242 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -38,4 +38,4 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.5.0.dev0 + opentelemetry-exporter-otlp-proto-grpc == 1.5.0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 36cd1d2223..c8902adc49 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 36cd1d2223..c8902adc49 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index c7828ae496..874ce5a368 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.5.0.dev0 + opentelemetry-exporter-zipkin-json == 1.5.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 36cd1d2223..c8902adc49 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 6d2f97404a..bea3c1043d 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.5.0.dev0 - opentelemetry-exporter-zipkin-proto-http == 1.5.0.dev0 + opentelemetry-exporter-zipkin-json == 1.5.0 + opentelemetry-exporter-zipkin-proto-http == 1.5.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 36cd1d2223..c8902adc49 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 36cd1d2223..c8902adc49 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 4819b09730..36779604d0 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -42,8 +42,8 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api ~= 1.3 - opentelemetry-instrumentation == 0.24.dev0 - opentelemetry-sdk == 1.5.0.dev0 + opentelemetry-instrumentation == 0.24b0 + opentelemetry-sdk == 1.5.0 [options.packages.find] where = src @@ -57,4 +57,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.5.0.dev0 + opentelemetry-exporter-otlp == 1.5.0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index 5aeaec6556..d33bd87ce4 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24.dev0" +__version__ = "0.24b0" diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 799c5b865c..e400a3417b 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -18,121 +18,121 @@ libraries = { "aiohttp": { "library": "aiohttp ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.24b0", }, "aiopg": { "library": "aiopg >= 0.13.0, < 1.3.0", - "instrumentation": "opentelemetry-instrumentation-aiopg==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-aiopg==0.24b0", }, "asgiref": { "library": "asgiref ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-asgi==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-asgi==0.24b0", }, "asyncpg": { "library": "asyncpg >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-asyncpg==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-asyncpg==0.24b0", }, "boto": { "library": "boto~=2.0", - "instrumentation": "opentelemetry-instrumentation-boto==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-boto==0.24b0", }, "botocore": { "library": "botocore ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-botocore==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-botocore==0.24b0", }, "celery": { "library": "celery >= 4.0, < 6.0", - "instrumentation": "opentelemetry-instrumentation-celery==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-celery==0.24b0", }, "django": { "library": "django >= 1.10", - "instrumentation": "opentelemetry-instrumentation-django==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-django==0.24b0", }, "elasticsearch": { "library": "elasticsearch >= 2.0", - "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.24b0", }, "falcon": { "library": "falcon ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-falcon==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-falcon==0.24b0", }, "fastapi": { - "library": "fastapi ~= 0.58.1", - "instrumentation": "opentelemetry-instrumentation-fastapi==0.24.dev0", + "library": "fastapi ~= 0.58", + "instrumentation": "opentelemetry-instrumentation-fastapi==0.24b0", }, "flask": { "library": "flask >= 1.0, < 3.0", - "instrumentation": "opentelemetry-instrumentation-flask==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-flask==0.24b0", }, "grpcio": { "library": "grpcio ~= 1.27", - "instrumentation": "opentelemetry-instrumentation-grpc==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-grpc==0.24b0", }, "httpx": { "library": "httpx >= 0.18.0, < 0.19.0", - "instrumentation": "opentelemetry-instrumentation-httpx==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-httpx==0.24b0", }, "jinja2": { "library": "jinja2~=2.7", - "instrumentation": "opentelemetry-instrumentation-jinja2==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-jinja2==0.24b0", }, "mysql-connector-python": { "library": "mysql-connector-python ~= 8.0", - "instrumentation": "opentelemetry-instrumentation-mysql==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-mysql==0.24b0", }, "psycopg2": { "library": "psycopg2 >= 2.7.3.1", - "instrumentation": "opentelemetry-instrumentation-psycopg2==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-psycopg2==0.24b0", }, "pymemcache": { "library": "pymemcache ~= 1.3", - "instrumentation": "opentelemetry-instrumentation-pymemcache==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-pymemcache==0.24b0", }, "pymongo": { "library": "pymongo ~= 3.1", - "instrumentation": "opentelemetry-instrumentation-pymongo==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-pymongo==0.24b0", }, "PyMySQL": { "library": "PyMySQL ~= 0.10.1", - "instrumentation": "opentelemetry-instrumentation-pymysql==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-pymysql==0.24b0", }, "pyramid": { "library": "pyramid >= 1.7", - "instrumentation": "opentelemetry-instrumentation-pyramid==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-pyramid==0.24b0", }, "redis": { "library": "redis >= 2.6", - "instrumentation": "opentelemetry-instrumentation-redis==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-redis==0.24b0", }, "requests": { "library": "requests ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-requests==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-requests==0.24b0", }, "scikit-learn": { "library": "scikit-learn ~= 0.24.0", - "instrumentation": "opentelemetry-instrumentation-sklearn==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-sklearn==0.24b0", }, "sqlalchemy": { "library": "sqlalchemy", - "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.24b0", }, "starlette": { "library": "starlette ~= 0.13.0", - "instrumentation": "opentelemetry-instrumentation-starlette==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-starlette==0.24b0", }, "tornado": { "library": "tornado >= 6.0", - "instrumentation": "opentelemetry-instrumentation-tornado==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-tornado==0.24b0", }, "urllib3": { "library": "urllib3 >= 1.0.0, < 2.0.0", - "instrumentation": "opentelemetry-instrumentation-urllib3==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-urllib3==0.24b0", }, } default_instrumentations = [ - "opentelemetry-instrumentation-dbapi==0.24.dev0", - "opentelemetry-instrumentation-logging==0.24.dev0", - "opentelemetry-instrumentation-sqlite3==0.24.dev0", - "opentelemetry-instrumentation-urllib==0.24.dev0", - "opentelemetry-instrumentation-wsgi==0.24.dev0", + "opentelemetry-instrumentation-dbapi==0.24b0", + "opentelemetry-instrumentation-logging==0.24b0", + "opentelemetry-instrumentation-sqlite3==0.24b0", + "opentelemetry-instrumentation-urllib==0.24b0", + "opentelemetry-instrumentation-wsgi==0.24b0", ] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 5aeaec6556..d33bd87ce4 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24.dev0" +__version__ = "0.24b0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 36cd1d2223..c8902adc49 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index e8cee4d6b7..160fde86e5 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,9 +42,9 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.5.0.dev0 - opentelemetry-semantic-conventions == 0.24.dev0 - opentelemetry-instrumentation == 0.24.dev0 + opentelemetry-api == 1.5.0 + opentelemetry-semantic-conventions == 0.24b0 + opentelemetry-instrumentation == 0.24b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 36cd1d2223..c8902adc49 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 5aeaec6556..d33bd87ce4 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24.dev0" +__version__ = "0.24b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 36cd1d2223..c8902adc49 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 36cd1d2223..c8902adc49 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index b172898600..41f96654a0 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -46,7 +46,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test == 0.24.dev0 + opentelemetry-test == 0.24b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 5aeaec6556..d33bd87ce4 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24.dev0" +__version__ = "0.24b0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 668994320e..536b2ec853 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.24.dev0" +__version__ = "0.24b0" From 4a9567aa9d88bab7e852b8e61723c16d38d67098 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 27 Aug 2021 19:23:10 +0200 Subject: [PATCH 0959/1517] Fix lint (#2076) --- dev-requirements.txt | 2 +- exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py | 2 +- opentelemetry-api/src/opentelemetry/context/context.py | 2 +- .../src/opentelemetry/context/contextvars_context.py | 2 +- pyproject.toml | 2 ++ 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 9c4dea3993..ec40008979 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,7 @@ pylint==2.7.1 flake8~=3.7 isort~=5.8 -black~=20.8b1 +black~=21.7b0 httpretty~=1.0 mypy==0.812 sphinx~=3.5.4 diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py index 4ce87cceac..935a297607 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py @@ -23,7 +23,7 @@ # pylint:disable=no-member class TestJaegerExporter(unittest.TestCase): def test_constructors(self): - """ Test ensures both exporters can co-exist""" + """Test ensures both exporters can co-exist""" try: grpc.JaegerExporter() thrift.JaegerExporter() diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 48ead2205f..518f09f2b8 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -39,7 +39,7 @@ def attach(self, context: Context) -> object: @abstractmethod def get_current(self) -> Context: - """Returns the current `Context` object. """ + """Returns the current `Context` object.""" @abstractmethod def detach(self, token: object) -> None: diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index 589ed58c82..5daee59a4d 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -44,7 +44,7 @@ def attach(self, context: Context) -> object: return self._current_context.set(context) def get_current(self) -> Context: - """Returns the current `Context` object. """ + """Returns the current `Context` object.""" return self._current_context.get() def detach(self, token: object) -> None: diff --git a/pyproject.toml b/pyproject.toml index e59980b2cf..c0b70e2dc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,8 @@ line-length = 79 exclude = ''' ( /( # generated files + .tox| + opentelemetry-python-contrib| exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen| exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen| exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen| From c3dfa360e643d8e8039e430c468522b5ab93b1c7 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Fri, 27 Aug 2021 23:13:15 +0530 Subject: [PATCH 0960/1517] Use exact dep versions in getting started docs tests (#2070) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 42155157e4..0cde56105a 100644 --- a/tox.ini +++ b/tox.ini @@ -137,7 +137,7 @@ commands_pre = distro: pip install {toxinidir}/opentelemetry-distro instrumentation: pip install {toxinidir}/opentelemetry-instrumentation - getting-started: pip install requests flask -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask + getting-started: pip install requests==2.26.0 flask==2.0.1 -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus From 04f74eeafeb7aca9c0ce7a72bd6a984ccef579c2 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 30 Aug 2021 22:50:41 +0530 Subject: [PATCH 0961/1517] Update to semantic conventions v1.6.1 (#2077) * Update to semantic conventions v1.6.1 * Add CHANGELOG entry * Fix lint --- .gitignore | 1 + CHANGELOG.md | 3 + opentelemetry-semantic-conventions/README.rst | 7 +- .../semconv/resource/__init__.py | 13 +- .../opentelemetry/semconv/trace/__init__.py | 119 +++++++++++++++++- scripts/semconv/generate.sh | 4 +- 6 files changed, 139 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index c784acf9f6..e2538e67ee 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ build eggs parts bin +include var sdist develop-eggs diff --git a/CHANGELOG.md b/CHANGELOG.md index 67585bde86..db3b4eb83f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.5.0-0.24b0...HEAD) +- `opentelemetry-semantic-conventions` Update to semantic conventions v1.6.1 + ([#2077](https://github.com/open-telemetry/opentelemetry-python/pull/2077)) + ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 diff --git a/opentelemetry-semantic-conventions/README.rst b/opentelemetry-semantic-conventions/README.rst index 3e1a322cdf..f84cf523d0 100644 --- a/opentelemetry-semantic-conventions/README.rst +++ b/opentelemetry-semantic-conventions/README.rst @@ -18,14 +18,15 @@ Installation Code Generation --------------- -These files were generated automatically from code in opentelemetry-semantic-conventions_. +These files were generated automatically from code in semconv_. To regenerate the code, run ``../scripts/semconv/generate.sh``. -To build against a new release or specific commit of opentelemetry-semantic-conventions_, +To build against a new release or specific commit of opentelemetry-specification_, update the ``SPEC_VERSION`` variable in ``../scripts/semconv/generate.sh``. Then run the script and commit the changes. -.. _opentelemetry-semantic-conventions: https://github.com/open-telemetry/opentelemetry-semantic-conventions +.. _opentelemetry-specification: https://github.com/open-telemetry/opentelemetry-specification +.. _semconv: https://github.com/open-telemetry/opentelemetry-python/tree/main/scripts/semconv References diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py index c8e27683ee..af10a64c9a 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py @@ -28,13 +28,13 @@ class ResourceAttributes: CLOUD_REGION = "cloud.region" """ - The geographical region the resource is running. Refer to your provider's docs to see the available regions, for example [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/en-us/global-infrastructure/geographies/), or [Google Cloud regions](https://cloud.google.com/about/locations). + The geographical region the resource is running. Refer to your provider's docs to see the available regions, for example [Alibaba Cloud regions](https://www.alibabacloud.com/help/doc-detail/40654.htm), [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/en-us/global-infrastructure/geographies/), or [Google Cloud regions](https://cloud.google.com/about/locations). """ CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone" """ Cloud regions often have multiple, isolated locations known as zones to increase availability. Availability zone represents the zone where the resource is running. - Note: Availability zones are called "zones" on Google Cloud. + Note: Availability zones are called "zones" on Alibaba Cloud and Google Cloud. """ CLOUD_PLATFORM = "cloud.platform" @@ -460,6 +460,9 @@ class ResourceAttributes: class CloudProviderValues(Enum): + ALIBABA_CLOUD = "alibaba_cloud" + """Alibaba Cloud.""" + AWS = "aws" """Amazon Web Services.""" @@ -471,6 +474,12 @@ class CloudProviderValues(Enum): class CloudPlatformValues(Enum): + ALIBABA_CLOUD_ECS = "alibaba_cloud_ecs" + """Alibaba Cloud Elastic Compute Service.""" + + ALIBABA_CLOUD_FC = "alibaba_cloud_fc" + """Alibaba Cloud Function Compute.""" + AWS_EC2 = "aws_ec2" """AWS Elastic Compute Cloud.""" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py index 5ecfd2946b..5d32bbef99 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines + from enum import Enum @@ -314,6 +316,36 @@ class SpanAttributes: Local hostname or similar, see note below. """ + NET_HOST_CONNECTION_TYPE = "net.host.connection.type" + """ + The internet connection type currently being used by the host. + """ + + NET_HOST_CONNECTION_SUBTYPE = "net.host.connection.subtype" + """ + This describes more details regarding the connection.type. It may be the type of cell technology connection, but it could be used for describing details about a wifi connection. + """ + + NET_HOST_CARRIER_NAME = "net.host.carrier.name" + """ + The name of the mobile carrier. + """ + + NET_HOST_CARRIER_MCC = "net.host.carrier.mcc" + """ + The mobile carrier country code. + """ + + NET_HOST_CARRIER_MNC = "net.host.carrier.mnc" + """ + The mobile carrier network code. + """ + + NET_HOST_CARRIER_ICC = "net.host.carrier.icc" + """ + The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network. + """ + MESSAGING_SYSTEM = "messaging.system" """ A string identifying the messaging system. @@ -608,7 +640,7 @@ class SpanAttributes: MESSAGING_KAFKA_MESSAGE_KEY = "messaging.kafka.message_key" """ Message keys in Kafka are used for grouping alike messages to ensure they're processed on the same partition. They differ from `messaging.message_id` in that they're not unique. If the key is `null`, the attribute MUST NOT be set. - Note: If the key type is not string, its string representation has to be supplied for the attribute. If the key has no unambiguous, canonical string form, don't include its value. + Note: If the key type is not string, it's string representation has to be supplied for the attribute. If the key has no unambiguous, canonical string form, don't include its value. """ MESSAGING_KAFKA_CONSUMER_GROUP = "messaging.kafka.consumer_group" @@ -903,6 +935,88 @@ class HttpFlavorValues(Enum): """QUIC protocol.""" +class NetHostConnectionTypeValues(Enum): + WIFI = "wifi" + """wifi.""" + + WIRED = "wired" + """wired.""" + + CELL = "cell" + """cell.""" + + UNAVAILABLE = "unavailable" + """unavailable.""" + + UNKNOWN = "unknown" + """unknown.""" + + +class NetHostConnectionSubtypeValues(Enum): + GPRS = "gprs" + """GPRS.""" + + EDGE = "edge" + """EDGE.""" + + UMTS = "umts" + """UMTS.""" + + CDMA = "cdma" + """CDMA.""" + + EVDO_0 = "evdo_0" + """EVDO Rel. 0.""" + + EVDO_A = "evdo_a" + """EVDO Rev. A.""" + + CDMA2000_1XRTT = "cdma2000_1xrtt" + """CDMA2000 1XRTT.""" + + HSDPA = "hsdpa" + """HSDPA.""" + + HSUPA = "hsupa" + """HSUPA.""" + + HSPA = "hspa" + """HSPA.""" + + IDEN = "iden" + """IDEN.""" + + EVDO_B = "evdo_b" + """EVDO Rev. B.""" + + LTE = "lte" + """LTE.""" + + EHRPD = "ehrpd" + """EHRPD.""" + + HSPAP = "hspap" + """HSPAP.""" + + GSM = "gsm" + """GSM.""" + + TD_SCDMA = "td_scdma" + """TD-SCDMA.""" + + IWLAN = "iwlan" + """IWLAN.""" + + NR = "nr" + """5G NR (New Radio).""" + + NRNSA = "nrnsa" + """5G NRNSA (New Radio Non-Standalone).""" + + LTE_CA = "lte_ca" + """LTE CA.""" + + class MessagingDestinationKindValues(Enum): QUEUE = "queue" """A message sent to a queue.""" @@ -912,6 +1026,9 @@ class MessagingDestinationKindValues(Enum): class FaasInvokedProviderValues(Enum): + ALIBABA_CLOUD = "alibaba_cloud" + """Alibaba Cloud.""" + AWS = "aws" """Amazon Web Services.""" diff --git a/scripts/semconv/generate.sh b/scripts/semconv/generate.sh index ccf75a6364..d793bf5809 100755 --- a/scripts/semconv/generate.sh +++ b/scripts/semconv/generate.sh @@ -4,8 +4,8 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR="${SCRIPT_DIR}/../../" # freeze the spec version to make SemanticAttributes generation reproducible -SPEC_VERSION=v1.5.0 -OTEL_SEMCONV_GEN_IMG_VERSION=0.4.1 +SPEC_VERSION=v1.6.1 +OTEL_SEMCONV_GEN_IMG_VERSION=0.5.0 cd ${SCRIPT_DIR} From b4ebef938f7da8cc789f4642cda3c3ab3d3bb7d1 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 31 Aug 2021 02:07:01 +0530 Subject: [PATCH 0962/1517] Rename base test assertion methods for consistency (#2084) * Rename base test assertion methods for consistency * Renamed assertion method * Updated contrib SHA * Updated contrib SHA --- .github/workflows/test.yml | 2 +- tests/util/src/opentelemetry/test/test_base.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 02cdd49356..f66ad7dbbb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 3ad534cbba41c2b92618f5f03c4c92cee4a72df6 + CONTRIB_REPO_SHA: 2a92e255f7595024242e45c0050c8f3de7140b6b jobs: build: diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index 14ef48d40e..ad124bf445 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -24,6 +24,8 @@ class TestBase(unittest.TestCase): + # pylint: disable=C0103 + @classmethod def setUpClass(cls): cls.original_tracer_provider = trace_api.get_tracer_provider() @@ -44,11 +46,11 @@ def tearDownClass(cls): def setUp(self): self.memory_exporter.clear() - def check_span_instrumentation_info(self, span, module): + def assertEqualSpanInstrumentationInfo(self, span, module): self.assertEqual(span.instrumentation_info.name, module.__name__) self.assertEqual(span.instrumentation_info.version, module.__version__) - def assert_span_has_attributes(self, span, attributes): + def assertSpanHasAttributes(self, span, attributes): for key, val in attributes.items(): self.assertIn(key, span.attributes) self.assertEqual(val, span.attributes[key]) From a36a6152691c37eb2a9bf5d5db182188e2b5017f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 31 Aug 2021 14:07:45 +0200 Subject: [PATCH 0963/1517] Escape reserved characters in baggage keys (#2080) * Escape reserved characters in baggage keys Fixes #2072 * Add extract tests * Fix lint --- .../baggage/propagation/__init__.py | 14 +++++------ .../tests/baggage/test_baggage_propagation.py | 25 +++++++++++++++++-- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index 04d896baa3..b484d9b2e8 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -13,9 +13,9 @@ # limitations under the License. # import typing -import urllib.parse +from urllib.parse import quote_plus, unquote_plus -from opentelemetry import baggage +from opentelemetry.baggage import get_all, set_baggage from opentelemetry.context import get_current from opentelemetry.context.context import Context from opentelemetry.propagators import textmap @@ -63,9 +63,9 @@ def extract( name, value = entry.split("=", 1) except Exception: # pylint: disable=broad-except continue - context = baggage.set_baggage( - urllib.parse.unquote(name).strip(), - urllib.parse.unquote(value).strip(), + context = set_baggage( + unquote_plus(name).strip(), + unquote_plus(value).strip(), context=context, ) @@ -82,7 +82,7 @@ def inject( See `opentelemetry.propagators.textmap.TextMapPropagator.inject` """ - baggage_entries = baggage.get_all(context=context) + baggage_entries = get_all(context=context) if not baggage_entries: return @@ -97,7 +97,7 @@ def fields(self) -> typing.Set[str]: def _format_baggage(baggage_entries: typing.Mapping[str, object]) -> str: return ",".join( - key + "=" + urllib.parse.quote_plus(str(value)) + quote_plus(str(key)) + "=" + quote_plus(str(value)) for key, value in baggage_entries.items() ) diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py index 9084bb778e..009598db9f 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py @@ -18,7 +18,10 @@ from unittest.mock import Mock, patch from opentelemetry import baggage -from opentelemetry.baggage.propagation import W3CBaggagePropagator +from opentelemetry.baggage.propagation import ( + W3CBaggagePropagator, + _format_baggage, +) from opentelemetry.context import get_current @@ -106,6 +109,15 @@ def test_header_contains_pair_too_long(self): expected = {"key1": "value1", "key3": "value3"} self.assertEqual(self._extract(header), expected) + def test_extract_unquote_plus(self): + self.assertEqual( + self._extract("key+key=value+value"), {"key key": "value value"} + ) + self.assertEqual( + self._extract("key%2Fkey=value%2Fvalue"), + {"key/key": "value/value"}, + ) + def test_inject_no_baggage_entries(self): values = {} output = self._inject(values) @@ -140,7 +152,7 @@ def test_inject_non_string_values(self): self.assertIn("key2=123", output) self.assertIn("key3=123.567", output) - @patch("opentelemetry.baggage.propagation.baggage") + @patch("opentelemetry.baggage.propagation.get_all") @patch("opentelemetry.baggage.propagation._format_baggage") def test_fields(self, mock_format_baggage, mock_baggage): @@ -154,3 +166,12 @@ def test_fields(self, mock_format_baggage, mock_baggage): inject_fields.add(mock_call[1][1]) self.assertEqual(inject_fields, self.propagator.fields) + + def test__format_baggage(self): + self.assertEqual( + _format_baggage({"key key": "value value"}), "key+key=value+value" + ) + self.assertEqual( + _format_baggage({"key/key": "value/value"}), + "key%2Fkey=value%2Fvalue", + ) From 91d190017e7944b2a9fa73eee373653beea4615c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 31 Aug 2021 19:00:39 +0200 Subject: [PATCH 0964/1517] Do not count skipped baggage entries (#2079) --- CHANGELOG.md | 2 + .../baggage/propagation/__init__.py | 6 +-- .../tests/baggage/test_baggage_propagation.py | 40 +++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db3b4eb83f..8bee8bd3cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-semantic-conventions` Update to semantic conventions v1.6.1 ([#2077](https://github.com/open-telemetry/opentelemetry-python/pull/2077)) +- Fix propagation bug caused by counting skipped entries + ([#2071](https://github.com/open-telemetry/opentelemetry-python/pull/2071)) ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index b484d9b2e8..9d170aae9a 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -54,9 +54,6 @@ def extract( baggage_entries = header.split(",") total_baggage_entries = self._MAX_PAIRS for entry in baggage_entries: - if total_baggage_entries <= 0: - return context - total_baggage_entries -= 1 if len(entry) > self._MAX_PAIR_LENGTH: continue try: @@ -68,6 +65,9 @@ def extract( unquote_plus(value).strip(), context=context, ) + total_baggage_entries -= 1 + if total_baggage_entries == 0: + break return context diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py index 009598db9f..caecc4615e 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py @@ -118,6 +118,46 @@ def test_extract_unquote_plus(self): {"key/key": "value/value"}, ) + def test_header_max_entries_skip_invalid_entry(self): + + self.assertEqual( + self._extract( + ",".join( + [ + f"key{index}=value{index}" + if index != 2 + else ( + f"key{index}=" + f"value{'s' * (W3CBaggagePropagator._MAX_PAIR_LENGTH + 1)}" + ) + for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) + ] + ) + ), + { + f"key{index}": f"value{index}" + for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) + if index != 2 + }, + ) + self.assertEqual( + self._extract( + ",".join( + [ + f"key{index}=value{index}" + if index != 2 + else f"key{index}xvalue{index}" + for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) + ] + ) + ), + { + f"key{index}": f"value{index}" + for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) + if index != 2 + }, + ) + def test_inject_no_baggage_entries(self): values = {} output = self._inject(values) From b76683eb5b93c713e83320389b8b5482c2fb8256 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 1 Sep 2021 03:37:34 -0700 Subject: [PATCH 0965/1517] Update index.rst (#2085) --- docs/index.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 0da22fba40..e4f1cfc4df 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,8 +11,9 @@ The Python `OpenTelemetry `_ client. This documentation describes the :doc:`opentelemetry-api `, :doc:`opentelemetry-sdk `, and several `integration packages <#integrations>`_. -**Please note** that this library is currently in _beta_, and shouldn't -generally be used in production environments. +The library is currently stable for tracing. Support for `metrics `_ +and `logging `_ is currently under development and is considered +experimental. Requirement ----------- From 3fdfa6d6fbbb26e866ab8d40dc62ef1b4295ffc4 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 1 Sep 2021 17:04:48 +0530 Subject: [PATCH 0966/1517] Updated website docs (#2082) * Updated website docs * Update docs/getting-started.rst Co-authored-by: Leighton Chen Co-authored-by: Leighton Chen --- docs/getting-started.rst | 4 ++-- website_docs/getting-started.md | 20 +++++++++----------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 93b688eada..c4db11c24a 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -1,5 +1,5 @@ -Getting Started with OpenTelemetry Python -========================================= +Getting Started +=============== This guide walks you through instrumenting a Python application with ``opentelemetry-python``. diff --git a/website_docs/getting-started.md b/website_docs/getting-started.md index 8a569fc210..def6372c62 100644 --- a/website_docs/getting-started.md +++ b/website_docs/getting-started.md @@ -1,12 +1,13 @@ --- -date: '2021-05-07T21:49:47.106Z' +date: '2021-08-30T16:49:17.700Z' docname: getting-started images: {} path: /getting-started -title: "Getting Started" -weight: 22 +title: Getting Started --- +# Getting Started + This guide walks you through instrumenting a Python application with `opentelemetry-python`. For more elaborate examples, see [examples](https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples/). @@ -38,8 +39,8 @@ The following example script emits a trace containing three named spans: “foo from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( - ConsoleSpanExporter, BatchSpanProcessor, + ConsoleSpanExporter, ) provider = TracerProvider() @@ -54,9 +55,6 @@ with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): print("Hello world from OpenTelemetry Python!") - -# Flush all ended spans that are yet to be written to stdout before process exit. -provider.force_flush() ``` When you run the script you can see the traces printed to your console: @@ -225,8 +223,8 @@ from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( - ConsoleSpanExporter, BatchSpanProcessor, + ConsoleSpanExporter, ) trace.set_tracer_provider(TracerProvider()) @@ -238,16 +236,17 @@ app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) RequestsInstrumentor().instrument() +tracer = trace.get_tracer(__name__) + @app.route("/") def hello(): - tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("example-request"): requests.get("http://www.example.com") return "hello" -app.run(port=5000) +app.run(debug=True, port=5000) ``` Now run the script, hit the root URL ([http://localhost:5000/](http://localhost:5000/)) a few times, and watch your spans be emitted! @@ -336,7 +335,6 @@ Finally, execute the following script: ``` # otcollector.py -import time from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( From c1e8a51b752f8d71e3910f122f355b49bfc7dbfa Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Thu, 2 Sep 2021 01:48:25 +0530 Subject: [PATCH 0967/1517] let's do this (run tests on windows) (#1889) Run tests on Windows in Github CI --- .github/workflows/test.yml | 45 +++++++++++++------ docs/getting_started/flask_example.py | 2 +- .../test_asyncio.py | 6 +-- .../test_threads.py | 6 ++- .../util/src/opentelemetry/test/test_base.py | 31 +++++++++++++ 5 files changed, 70 insertions(+), 20 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f66ad7dbbb..de0c43cd8c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 2a92e255f7595024242e45c0050c8f3de7140b6b + CONTRIB_REPO_SHA: dde62cebffe519c35875af6d06fae053b3be65ec jobs: build: @@ -20,7 +20,7 @@ jobs: py37: 3.7 py38: 3.8 py39: 3.9 - pypy3: pypy3 + pypy3: pypy-3.7 RUN_MATRIX_COMBINATION: ${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: @@ -28,7 +28,7 @@ jobs: matrix: python-version: [ py36, py37, py38, py39, pypy3 ] package: ["instrumentation", "core", "exporter", "propagator"] - os: [ ubuntu-latest ] + os: [ ubuntu-20.04, windows-2019 ] steps: - name: Checkout Core Repo @ SHA - ${{ github.sha }} uses: actions/checkout@v2 @@ -42,14 +42,22 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ env[matrix.python-version] }} + architecture: 'x64' - name: Install tox run: pip install -U tox-factor - name: Cache tox environment # Preserves .tox directory between runs for faster installs uses: actions/cache@v2 with: - path: .tox - key: tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core + path: | + .tox + ~/.cache/pip + key: v2-tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core + # tox fails on windows and Python3.6 when tox dir is reused between builds so we remove it + - name: fix for windows + py3.6 + if: ${{ matrix.os == 'windows-2019' && matrix.python-version == 'py36' }} + shell: pwsh + run: Remove-Item .\.tox\ -Force -Recurse -ErrorAction Ignore - name: run tox run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json - name: Find and merge benchmarks @@ -81,7 +89,7 @@ jobs: matrix: tox-environment: [ "docker-tests", "lint", "docs", "mypy", "mypyinstalled", "tracecontext" ] name: ${{ matrix.tox-environment }} - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout Core Repo @ SHA - ${{ github.sha }} uses: actions/checkout@v2 @@ -95,14 +103,17 @@ jobs: uses: actions/setup-python@v2 with: python-version: 3.9 + architecture: 'x64' - name: Install tox run: pip install -U tox - name: Cache tox environment # Preserves .tox directory between runs for faster installs uses: actions/cache@v2 with: - path: .tox - key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core + path: | + .tox + ~/.cache/pip + key: v2-tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core - name: run tox run: tox -e ${{ matrix.tox-environment }} contrib-build: @@ -119,7 +130,7 @@ jobs: matrix: python-version: [ py36, py37, py38, py39, pypy3 ] package: ["instrumentation", "exporter"] - os: [ ubuntu-latest ] + os: [ ubuntu-20.04] steps: - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@v2 @@ -135,14 +146,17 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ env[matrix.python-version] }} + architecture: 'x64' - name: Install tox run: pip install -U tox-factor - name: Cache tox environment # Preserves .tox directory between runs for faster installs uses: actions/cache@v2 with: - path: .tox - key: tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib + path: | + .tox + ~/.cache/pip + key: v2-tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib - name: run tox run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} contrib-misc: @@ -151,7 +165,7 @@ jobs: matrix: tox-environment: [ "docker-tests"] name: ${{ matrix.tox-environment }} - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@v2 @@ -167,13 +181,16 @@ jobs: uses: actions/setup-python@v2 with: python-version: 3.9 + architecture: 'x64' - name: Install tox run: pip install -U tox - name: Cache tox environment # Preserves .tox directory between runs for faster installs uses: actions/cache@v2 with: - path: .tox - key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib + path: | + .tox + ~/.cache/pip + key: v2-tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib - name: run tox run: tox -e ${{ matrix.tox-environment }} diff --git a/docs/getting_started/flask_example.py b/docs/getting_started/flask_example.py index ddde3aa839..64ed606c7f 100644 --- a/docs/getting_started/flask_example.py +++ b/docs/getting_started/flask_example.py @@ -44,4 +44,4 @@ def hello(): return "hello" -app.run(debug=True, port=5000) +app.run(port=5000) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py index b4619b4ed8..64cfea933c 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py @@ -129,9 +129,9 @@ async def do_task(): spans = self.tracer.finished_spans() self.assertEqual(len(spans), 3) - spans = sorted(spans, key=lambda x: x.start_time) parent_span = get_one_by_operation_name(spans, "parent") self.assertIsNotNone(parent_span) - self.assertIsChildOf(spans[1], parent_span) - self.assertIsNotChildOf(spans[2], parent_span) + spans = [span for span in spans if span != parent_span] + self.assertIsChildOf(spans[0], parent_span) + self.assertIsNotChildOf(spans[1], parent_span) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py index 4ab8b2a075..36c0a8b841 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py @@ -115,5 +115,7 @@ def test_bad_solution_to_set_parent(self): parent_span = get_one_by_operation_name(spans, "parent") self.assertIsNotNone(parent_span) - self.assertIsChildOf(spans[1], parent_span) - self.assertIsChildOf(spans[2], parent_span) + spans = [s for s in spans if s != parent_span] + self.assertEqual(len(spans), 2) + for span in spans: + self.assertIsChildOf(span, parent_span) diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index ad124bf445..4945d1cb32 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -46,6 +46,11 @@ def tearDownClass(cls): def setUp(self): self.memory_exporter.clear() + def get_finished_spans(self): + return FinishedTestSpans( + self, self.memory_exporter.get_finished_spans() + ) + def assertEqualSpanInstrumentationInfo(self, span, module): self.assertEqual(span.instrumentation_info.name, module.__name__) self.assertEqual(span.instrumentation_info.version, module.__version__) @@ -56,6 +61,12 @@ def assertSpanHasAttributes(self, span, attributes): self.assertEqual(val, span.attributes[key]) def sorted_spans(self, spans): # pylint: disable=R0201 + """ + Sorts spans by span creation time. + + Note: This method should not be used to sort spans in a deterministic way as the + order depends on timing precision provided by the platform. + """ return sorted( spans, key=lambda s: s._start_time, # pylint: disable=W0212 @@ -91,3 +102,23 @@ def disable_logging(highest_level=logging.CRITICAL): yield finally: logging.disable(logging.NOTSET) + + +class FinishedTestSpans(list): + def __init__(self, test, spans): + super().__init__(spans) + self.test = test + + def by_name(self, name): + for span in self: + if span.name == name: + return span + self.test.fail("Did not find span with name {}".format(name)) + return None + + def by_attr(self, key, value): + for span in self: + if span.attributes.get(key) == value: + return span + self.test.fail("Did not find span with attrs {}={}".format(key, value)) + return None From 08392c80b80db5c5893eee08170e85aec314e44f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 9 Sep 2021 19:16:29 +0200 Subject: [PATCH 0968/1517] Add pre and post instrument entrypoints (#1983) * Add pre and post instrument entry points Fixes #1982 Co-authored-by: Owais Lone --- CHANGELOG.md | 2 ++ .../instrumentation/auto_instrumentation/sitecustomize.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bee8bd3cc..a24ca0245e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 +- Add pre and post instrumentation entry points + ([#1983](https://github.com/open-telemetry/opentelemetry-python/pull/1983)) - Fix documentation on well known exporters and variable OTEL_TRACES_EXPORTER which were misnamed ([#2023](https://github.com/open-telemetry/opentelemetry-python/pull/2023)) - `opentelemetry-sdk` `get_aggregated_resource()` returns default resource and service name diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index d89b60ec56..5bbf0685ff 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -60,6 +60,9 @@ def _load_instrumentors(distro): # to handle users entering "requests , flask" or "requests, flask" with spaces package_to_exclude = [x.strip() for x in package_to_exclude] + for entry_point in iter_entry_points("opentelemetry_pre_instrument"): + entry_point.load()() + for entry_point in iter_entry_points("opentelemetry_instrumentor"): if entry_point.name in package_to_exclude: logger.debug( @@ -84,6 +87,9 @@ def _load_instrumentors(distro): logger.exception("Instrumenting of %s failed", entry_point.name) raise exc + for entry_point in iter_entry_points("opentelemetry_post_instrument"): + entry_point.load()() + def _load_configurators(): configured = None From df76b62389ec585e2722ac6d4ef48a407b424e44 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 10 Sep 2021 00:20:35 +0530 Subject: [PATCH 0969/1517] Do not count invalid attributes for dropped (#2096) Do not count invalid attributes for dropped Co-authored-by: Owais Lone --- CHANGELOG.md | 2 ++ .../src/opentelemetry/attributes/__init__.py | 14 ++++++++------ .../tests/attributes/test_attributes.py | 3 +++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a24ca0245e..e1627ba1c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-semantic-conventions` Update to semantic conventions v1.6.1 ([#2077](https://github.com/open-telemetry/opentelemetry-python/pull/2077)) +- Do not count invalid attributes for dropped + ([#2096](https://github.com/open-telemetry/opentelemetry-python/pull/2096)) - Fix propagation bug caused by counting skipped entries ([#2071](https://github.com/open-telemetry/opentelemetry-python/pull/2071)) diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index a266839326..79abcd4d4a 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -172,14 +172,16 @@ def __setitem__(self, key, value): self.dropped += 1 return - if key in self._dict: - del self._dict[key] - elif self.maxlen is not None and len(self._dict) == self.maxlen: - del self._dict[next(iter(self._dict.keys()))] - self.dropped += 1 - value = _clean_attribute(key, value, self.max_value_len) if value is not None: + if key in self._dict: + del self._dict[key] + elif ( + self.maxlen is not None and len(self._dict) == self.maxlen + ): + self._dict.popitem(last=False) + self.dropped += 1 + self._dict[key] = value def __delitem__(self, key): diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index fa0606b347..2af2a0ff5b 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -137,6 +137,9 @@ def test_bounded_dict(self): self.assertEqual(len(bdict), dic_len) self.assertEqual(bdict.dropped, dic_len) + # Invalid values shouldn't be considered for `dropped` + bdict["invalid-seq"] = [None, 1, "2"] + self.assertEqual(bdict.dropped, dic_len) # test that elements in the dict are the new ones for key in self.base: From df1f84243178127f8fbced191f1ed7cb99c214f6 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 10 Sep 2021 09:10:49 +0530 Subject: [PATCH 0970/1517] Do not skip sequence attribute on decode error (#2097) Do not skip sequence attribute on decode error Co-authored-by: Owais Lone --- CHANGELOG.md | 2 ++ .../src/opentelemetry/attributes/__init__.py | 6 +----- .../tests/attributes/test_attributes.py | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1627ba1c3..e6e35414c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2096](https://github.com/open-telemetry/opentelemetry-python/pull/2096)) - Fix propagation bug caused by counting skipped entries ([#2071](https://github.com/open-telemetry/opentelemetry-python/pull/2071)) +- Do not skip sequence attribute on decode error + ([#2097](https://github.com/open-telemetry/opentelemetry-python/pull/2097)) ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 79abcd4d4a..7fd185f317 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -59,13 +59,9 @@ def _clean_attribute( cleaned_seq = [] for element in value: - # None is considered valid in any sequence - if element is None: - cleaned_seq.append(element) - element = _clean_attribute_value(element, max_len) - # reject invalid elements if element is None: + cleaned_seq.append(element) continue element_type = type(element) diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index 2af2a0ff5b..939aa8abd2 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -69,6 +69,25 @@ def test_clean_attribute(self): self.assertInvalid("value", "") self.assertInvalid("value", None) + def test_sequence_attr_decode(self): + seq = [ + None, + b"Content-Disposition", + b"Content-Type", + b"\x81", + b"Keep-Alive", + ] + expected = [ + None, + "Content-Disposition", + "Content-Type", + None, + "Keep-Alive", + ] + self.assertEqual( + _clean_attribute("headers", seq, None), tuple(expected) + ) + class TestBoundedAttributes(unittest.TestCase): base = collections.OrderedDict( From 600243ac1e46d239274953a178a2d01c951f64a1 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 13 Sep 2021 09:29:04 -0700 Subject: [PATCH 0971/1517] rename test-core env to opentelemetry (#2105) Improve test suite - rename test-core env to opentelemetry - for now separate what used to be 'core' packages in CI - use protobuf instead of proto --- .github/workflows/test.yml | 2 +- CONTRIBUTING.md | 4 +- tox.ini | 148 +++++++++++++++++-------------------- 3 files changed, 69 insertions(+), 85 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de0c43cd8c..947dd6abe1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: python-version: [ py36, py37, py38, py39, pypy3 ] - package: ["instrumentation", "core", "exporter", "propagator"] + package: ["api", "sdk", "instrumentation", "semantic", "getting", "distro" , "shim", "exporter", "protobuf", "propagator"] os: [ ubuntu-20.04, windows-2019 ] steps: - name: Checkout Core Repo @ SHA - ${{ github.sha }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e74979761a..a172efae7d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,8 +64,8 @@ You can run: - `tox` to run all existing tox commands, including unit tests for all packages under multiple Python versions - `tox -e docs` to regenerate the API docs -- `tox -e test-core-api` and `tox -e test-core-sdk` to run the API and SDK unit tests -- `tox -e py37-test-core-api` to e.g. run the API unit tests under a specific +- `tox -e opentelemetry-api` and `tox -e opentelemetry-sdk` to run the API and SDK unit tests +- `tox -e py37-opentelemetry-api` to e.g. run the API unit tests under a specific Python version - `tox -e lint` to run lint checks on all code diff --git a/tox.ini b/tox.ini index 0cde56105a..d893b6df6e 100644 --- a/tox.ini +++ b/tox.ini @@ -4,82 +4,65 @@ skip_missing_interpreters = True envlist = ; Environments are organized by individual package, allowing ; for specifying supported Python versions per package. - ; opentelemetry-api - py3{6,7,8,9}-test-core-api - pypy3-test-core-api + py3{6,7,8,9}-opentelemetry-api + pypy3-opentelemetry-api - ; opentelemetry-proto - py3{6,7,8,9}-test-core-proto - pypy3-test-core-proto + py3{6,7,8,9}-opentelemetry-protobuf + pypy3-opentelemetry-protobuf - ; opentelemetry-sdk - py3{6,7,8,9}-test-core-sdk - pypy3-test-core-sdk + py3{6,7,8,9}-opentelemetry-sdk + pypy3-opentelemetry-sdk - ; opentelemetry-instrumentation - py3{6,7,8,9}-test-core-instrumentation - pypy3-test-core-instrumentation + py3{6,7,8,9}-opentelemetry-instrumentation + pypy3-opentelemetry-instrumentation - ; opentelemetry-semantic-conventions - py3{6,7,8,9}-test-semantic-conventions - pypy3-test-semantic-conventions + py3{6,7,8,9}-opentelemetry-semantic-conventions + pypy3-opentelemetry-semantic-conventions ; docs/getting-started - py3{6,7,8,9}-test-core-getting-started - pypy3-test-core-getting-started + py3{6,7,8,9}-opentelemetry-getting-started + pypy3-opentelemetry-getting-started - ; opentelemetry-distro - py3{6,7,8,9}-test-core-distro - pypy3-test-core-distro + py3{6,7,8,9}-opentelemetry-distro + pypy3-opentelemetry-distro - ; opentelemetry-exporter-jaeger - py3{6,7,8,9}-test-exporter-jaeger-combined + py3{6,7,8,9}-opentelemetry-opentracing-shim + pypy3-opentelemetry-opentracing-shim - ; opentelemetry-exporter-jaeger-proto-grpc - py3{6,7,8,9}-test-exporter-jaeger-proto-grpc + py3{6,7,8,9}-opentelemetry-exporter-jaeger-combined - ; opentelemetry-exporter-jaeger-thrift - py3{6,7,8,9}-test-exporter-jaeger-thrift + py3{6,7,8,9}-opentelemetry-exporter-jaeger-proto-grpc - ; opentelemetry-exporter-opencensus - py3{6,7,8,9}-test-exporter-opencensus + py3{6,7,8,9}-opentelemetry-exporter-jaeger-thrift + + py3{6,7,8,9}-opentelemetry-exporter-opencensus ; exporter-opencensus intentionally excluded from pypy3 - ; opentelemetry-exporter-otlp-combined - py3{6,7,8,9}-test-exporter-otlp-combined + ; opentelemetry-exporter-otlp + py3{6,7,8,9}-opentelemetry-exporter-otlp-combined ; intentionally excluded from pypy3 - ; opentelemetry-exporter-otlp-proto-grpc - py3{6,7,8,9}-test-exporter-otlp-proto-grpc + py3{6,7,8,9}-opentelemetry-exporter-otlp-proto-grpc ; intentionally excluded from pypy3 - ; opentelemetry-exporter-otlp-proto-http - py3{6,7,8,9}-test-exporter-otlp-proto-http - pypy3-test-exporter-otlp-proto-http + py3{6,7,8,9}-opentelemetry-exporter-otlp-proto-http + pypy3-opentelemetry-exporter-otlp-proto-http ; opentelemetry-exporter-zipkin - py3{6,7,8,9}-test-exporter-zipkin-combined - pypy3-test-exporter-zipkin-combined - - ; opentelemetry-exporter-zipkin-proto-http - py3{6,7,8,9}-test-exporter-zipkin-proto-http - pypy3-test-exporter-zipkin-proto-http + py3{6,7,8,9}-opentelemetry-exporter-zipkin-combined + pypy3-opentelemetry-exporter-zipkin-combined - ; opentelemetry-exporter-zipkin-json - py3{6,7,8,9}-test-exporter-zipkin-json - pypy3-test-exporter-zipkin-json + py3{6,7,8,9}-opentelemetry-exporter-zipkin-proto-http + pypy3-opentelemetry-exporter-zipkin-proto-http - ; opentelemetry-opentracing-shim - py3{6,7,8,9}-test-core-opentracing-shim - pypy3-test-core-opentracing-shim + py3{6,7,8,9}-opentelemetry-exporter-zipkin-json + pypy3-opentelemetry-exporter-zipkin-json - ; opentelemetry-propagator-b3 - py3{6,7,8,9}-test-propagator-b3 - pypy3-test-propagator-b3 + py3{6,7,8,9}-opentelemetry-propagator-b3 + pypy3-opentelemetry-propagator-b3 - ; opentelemetry-propagator-jaeger - py3{6,7,8,9}-test-propagator-jaeger - pypy3-test-propagator-jaeger + py3{6,7,8,9}-opentelemetry-propagator-jaeger + pypy3-opentelemetry-propagator-jaeger lint tracecontext @@ -91,49 +74,50 @@ envlist = [testenv] deps = -c dev-requirements.txt - test: pytest - test: pytest-benchmark + opentelemetry: pytest + opentelemetry: pytest-benchmark coverage: pytest coverage: pytest-cov mypy,mypyinstalled: mypy -setenv = mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ +setenv = + mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ changedir = - test-core-api: opentelemetry-api/tests - test-core-sdk: opentelemetry-sdk/tests - test-core-proto: opentelemetry-proto/tests - test-semantic-conventions: opentelemetry-semantic-conventions/tests - test-core-instrumentation: opentelemetry-instrumentation/tests - test-core-getting-started: docs/getting_started/tests - test-core-opentracing-shim: shim/opentelemetry-opentracing-shim/tests - test-core-distro: opentelemetry-distro/tests - - test-exporter-jaeger-combined: exporter/opentelemetry-exporter-jaeger/tests - test-exporter-jaeger-proto-grpc: exporter/opentelemetry-exporter-jaeger-proto-grpc/tests - test-exporter-jaeger-thrift: exporter/opentelemetry-exporter-jaeger-thrift/tests - test-exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests - test-exporter-otlp-combined: exporter/opentelemetry-exporter-otlp/tests - test-exporter-otlp-proto-grpc: exporter/opentelemetry-exporter-otlp-proto-grpc/tests - test-exporter-otlp-proto-http: exporter/opentelemetry-exporter-otlp-proto-http/tests - test-exporter-zipkin-combined: exporter/opentelemetry-exporter-zipkin/tests - test-exporter-zipkin-proto-http: exporter/opentelemetry-exporter-zipkin-proto-http/tests - test-exporter-zipkin-json: exporter/opentelemetry-exporter-zipkin-json/tests + api: opentelemetry-api/tests + sdk: opentelemetry-sdk/tests + protobuf: opentelemetry-proto/tests + semantic-conventions: opentelemetry-semantic-conventions/tests + instrumentation: opentelemetry-instrumentation/tests + getting-started: docs/getting_started/tests + opentracing-shim: shim/opentelemetry-opentracing-shim/tests + distro: opentelemetry-distro/tests + + exporter-jaeger-combined: exporter/opentelemetry-exporter-jaeger/tests + exporter-jaeger-proto-grpc: exporter/opentelemetry-exporter-jaeger-proto-grpc/tests + exporter-jaeger-thrift: exporter/opentelemetry-exporter-jaeger-thrift/tests + exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests + exporter-otlp-combined: exporter/opentelemetry-exporter-otlp/tests + exporter-otlp-proto-grpc: exporter/opentelemetry-exporter-otlp-proto-grpc/tests + exporter-otlp-proto-http: exporter/opentelemetry-exporter-otlp-proto-http/tests + exporter-zipkin-combined: exporter/opentelemetry-exporter-zipkin/tests + exporter-zipkin-proto-http: exporter/opentelemetry-exporter-zipkin-proto-http/tests + exporter-zipkin-json: exporter/opentelemetry-exporter-zipkin-json/tests - test-propagator-b3: propagator/opentelemetry-propagator-b3/tests - test-propagator-jaeger: propagator/opentelemetry-propagator-jaeger/tests + propagator-b3: propagator/opentelemetry-propagator-b3/tests + propagator-jaeger: propagator/opentelemetry-propagator-jaeger/tests commands_pre = ; Install without -e to test the actual installation py3{6,7,8,9}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. - test: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-instrumentation {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util + opentelemetry: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-instrumentation {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util - test-core-sdk: pip install {toxinidir}/opentelemetry-instrumentation - test-core-opentracing-shim: pip install {toxinidir}/opentelemetry-instrumentation + sdk: pip install {toxinidir}/opentelemetry-instrumentation + opentracing-shim: pip install {toxinidir}/opentelemetry-instrumentation - test-core-proto: pip install {toxinidir}/opentelemetry-proto + protobuf: pip install {toxinidir}/opentelemetry-proto distro: pip install {toxinidir}/opentelemetry-distro instrumentation: pip install {toxinidir}/opentelemetry-instrumentation @@ -183,7 +167,7 @@ commands_pre = mypyinstalled: pip install file://{toxinidir}/opentelemetry-api/ commands = - test: pytest {posargs} + opentelemetry: pytest {posargs} coverage: {toxinidir}/scripts/coverage.sh mypy: mypy --namespace-packages --explicit-package-bases opentelemetry-api/src/opentelemetry/ From 9020b0baaeb41b7137badca988bb5c2d562cddee Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 13 Sep 2021 11:33:44 -0700 Subject: [PATCH 0972/1517] fix opentelemetry-instrumentation setup.cfg (#2109) Co-authored-by: Leighton Chen --- opentelemetry-instrumentation/setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 72714c581b..6ecd9d75e6 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opentelemetry-instrumentation +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-instrumentation platforms = any license = Apache-2.0 classifiers = From c40f10dedf44861d97186bb78e173699bbefcb12 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 14 Sep 2021 11:07:13 -0700 Subject: [PATCH 0973/1517] remove dependency on contrib repo to run tox (#2108) --- .flake8 | 1 - .github/workflows/test.yml | 12 ------------ .gitignore | 3 --- .isort.cfg | 2 +- CONTRIBUTING.md | 16 +++++++++++++--- eachdist.ini | 2 -- pyproject.toml | 1 - tox.ini | 16 ++++++++++++---- 8 files changed, 26 insertions(+), 27 deletions(-) diff --git a/.flake8 b/.flake8 index 9d0b0c101d..9c1f4b2d41 100644 --- a/.flake8 +++ b/.flake8 @@ -24,4 +24,3 @@ exclude = docs/examples/opentelemetry-example-app/build/* opentelemetry-proto/build/* opentelemetry-proto/src/opentelemetry/proto/ - opentelemetry-python-contrib/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 947dd6abe1..0377126522 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,12 +32,6 @@ jobs: steps: - name: Checkout Core Repo @ SHA - ${{ github.sha }} uses: actions/checkout@v2 - - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} - uses: actions/checkout@v2 - with: - repository: open-telemetry/opentelemetry-python-contrib - ref: ${{ env.CONTRIB_REPO_SHA }} - path: opentelemetry-python-contrib - name: Set up Python ${{ env[matrix.python-version] }} uses: actions/setup-python@v2 with: @@ -93,12 +87,6 @@ jobs: steps: - name: Checkout Core Repo @ SHA - ${{ github.sha }} uses: actions/checkout@v2 - - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} - uses: actions/checkout@v2 - with: - repository: open-telemetry/opentelemetry-python-contrib - ref: ${{ env.CONTRIB_REPO_SHA }} - path: opentelemetry-python-contrib - name: Set up Python uses: actions/setup-python@v2 with: diff --git a/.gitignore b/.gitignore index e2538e67ee..9b3ce8568f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,9 +24,6 @@ lib64 __pycache__ venv*/ .venv*/ -opentelemetry-python-contrib/ -# in case of symlink -opentelemetry-python-contrib # Installer logs pip-log.txt diff --git a/.isort.cfg b/.isort.cfg index 85533188fa..ab1ab74ee5 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -14,6 +14,6 @@ profile=black ; docs: https://github.com/timothycrosley/isort#multi-line-output-modes multi_line_output=3 skip=target -skip_glob=**/gen/*,.venv*/*,venv*/*,**/proto/*,opentelemetry-python-contrib/*,.tox/* +skip_glob=**/gen/*,.venv*/*,venv*/*,**/proto/*,.tox/* known_first_party=opentelemetry,opentelemetry_example_app known_third_party=psutil,pytest,redis,redis_opentracing diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a172efae7d..7084a8fbca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,9 +50,6 @@ ships with this project. First create a virtualenv and activate it. Then run `python scripts/eachdist.py develop` to install all required packages as well as the project's packages themselves (in `--editable` mode). -Further, you'll want to clone the Contrib repo locally to resolve paths needed -to run tests. `git clone git@github.com:open-telemetry/opentelemetry-python-contrib.git opentelemetry-python-contrib`. - You can then run `scripts/eachdist.py test` to test everything or `scripts/eachdist.py lint` to lint everything (fixing anything that is auto-fixable). @@ -81,6 +78,19 @@ See [`tox.ini`](https://github.com/open-telemetry/opentelemetry-python/blob/main/tox.ini) for more detail on available tox commands. +#### Contrib repo + +Some of the `tox` targets install packages from the [OpenTelemetry Python Contrib Repository](https://github.com/open-telemetry/opentelemetry-python.git) via +pip. The version of the packages installed defaults to the `main` branch in that repository when `tox` is run locally. It is possible to install packages tagged +with a specific git commit hash by setting an environment variable before running tox as per the following example: + +``` +CONTRIB_REPO_SHA=dde62cebffe519c35875af6d06fae053b3be65ec tox +``` + +The continuation integration overrides that environment variable with as per the configuration +[here](https://github.com/open-telemetry/opentelemetry-python/blob/9020b0baaeb41b7137badca988bb5c2d562cddee/.github/workflows/test.yml#L13). + ### Benchmarks Performance progression of benchmarks for packages distributed by OpenTelemetry Python can be viewed as a [graph of throughput vs commit history](https://opentelemetry-python.readthedocs.io/en/latest/performance/benchmarks.html). From the linked page, you can download a JSON file with the performance results. diff --git a/eachdist.ini b/eachdist.ini index 746f5a787a..065c543e94 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -1,8 +1,6 @@ # These will be sorted first in that order. # All packages that are depended upon by others should be listed here. [DEFAULT] -ignore= - opentelemetry-python-contrib sortfirst= opentelemetry-api diff --git a/pyproject.toml b/pyproject.toml index c0b70e2dc0..b43fb8b66e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,6 @@ exclude = ''' ( /( # generated files .tox| - opentelemetry-python-contrib| exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen| exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen| exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen| diff --git a/tox.ini b/tox.ini index d893b6df6e..724a7f2018 100644 --- a/tox.ini +++ b/tox.ini @@ -81,6 +81,10 @@ deps = mypy,mypyinstalled: mypy setenv = + ; override CONTRIB_REPO_SHA via env variable when testing other branches/commits than main + ; i.e: CONTRIB_REPO_SHA=dde62cebffe519c35875af6d06fae053b3be65ec tox -e + CONTRIB_REPO_SHA={env:CONTRIB_REPO_SHA:"main"} + CONTRIB_REPO="git+https://github.com/open-telemetry/opentelemetry-python-contrib.git@{env:CONTRIB_REPO_SHA}" mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ changedir = @@ -121,7 +125,11 @@ commands_pre = distro: pip install {toxinidir}/opentelemetry-distro instrumentation: pip install {toxinidir}/opentelemetry-instrumentation - getting-started: pip install requests==2.26.0 flask==2.0.1 -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask + getting-started: pip install requests==2.26.0 flask==2.0.1 -e {toxinidir}/opentelemetry-instrumentation + getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-requests&subdirectory=instrumentation/opentelemetry-instrumentation-requests" + getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-wsgi&subdirectory=instrumentation/opentelemetry-instrumentation-wsgi" + getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http" + getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-flask&subdirectory=instrumentation/opentelemetry-instrumentation-flask" opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus @@ -246,9 +254,9 @@ commands_pre = -e {toxinidir}/opentelemetry-semantic-conventions \ -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ - -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests \ - -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi \ - -e {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http + -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-requests&subdirectory=instrumentation/opentelemetry-instrumentation-requests" \ + -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-wsgi&subdirectory=instrumentation/opentelemetry-instrumentation-wsgi" \ + -e "{env:CONTRIB_REPO}#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http" commands = {toxinidir}/scripts/tracecontext-integration-test.sh From a2c7c20f694531865520e147dd314229f8d52b4c Mon Sep 17 00:00:00 2001 From: alrex Date: Sun, 19 Sep 2021 16:36:59 -0700 Subject: [PATCH 0974/1517] use {} instead of dict() (#2133) * use {} instead of dict() * fix lint --- .../tests/attributes/test_attributes.py | 4 ++-- opentelemetry-sdk/tests/resources/test_resources.py | 2 +- opentelemetry-sdk/tests/trace/test_sampling.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 12 +++++------- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index 939aa8abd2..121dec3d25 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -43,10 +43,10 @@ def test_attribute_key_validation(self): def test_clean_attribute(self): self.assertInvalid([1, 2, 3.4, "ss", 4]) - self.assertInvalid([dict(), 1, 2, 3.4, 4]) + self.assertInvalid([{}, 1, 2, 3.4, 4]) self.assertInvalid(["sw", "lf", 3.4, "ss"]) self.assertInvalid([1, 2, 3.4, 5]) - self.assertInvalid(dict()) + self.assertInvalid({}) self.assertInvalid([1, True]) self.assertValid(True) self.assertValid("hi") diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 25e8ddb867..ea3de80ed1 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -213,7 +213,7 @@ def test_invalid_resource_attribute_values(self): resource = resources.Resource( { resources.SERVICE_NAME: "test", - "non-primitive-data-type": dict(), + "non-primitive-data-type": {}, "invalid-byte-type-attribute": b"\xd8\xe1\xb7\xeb\xa8\xe5 \xd2\xb7\xe1", "": "empty-key-value", None: "null-key-value", diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index 808c64499d..ba6c5f5636 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -50,7 +50,7 @@ def test_is_sampled(self): class TestSamplingResult(unittest.TestCase): def test_ctr(self): attributes = {"asd": "test"} - trace_state = dict() + trace_state = {} # pylint: disable=E1137 trace_state["test"] = "123" result = sampling.SamplingResult( diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 3db5bcef9a..f4b75d106b 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -637,10 +637,10 @@ def test_attributes(self): def test_invalid_attribute_values(self): with self.tracer.start_as_current_span("root") as root: root.set_attributes( - {"correct-value": "foo", "non-primitive-data-type": dict()} + {"correct-value": "foo", "non-primitive-data-type": {}} ) - root.set_attribute("non-primitive-data-type", dict()) + root.set_attribute("non-primitive-data-type", {}) root.set_attribute( "list-of-mixed-data-types-numeric-first", [123, False, "string"], @@ -649,9 +649,7 @@ def test_invalid_attribute_values(self): "list-of-mixed-data-types-non-numeric-first", [False, 123, "string"], ) - root.set_attribute( - "list-with-non-primitive-data-type", [dict(), 123] - ) + root.set_attribute("list-with-non-primitive-data-type", [{}, 123]) root.set_attribute("list-with-numeric-and-bool", [1, True]) root.set_attribute("", 123) @@ -772,9 +770,9 @@ def test_invalid_event_attributes(self): with self.tracer.start_as_current_span("root") as root: root.add_event("event0", {"attr1": True, "attr2": ["hi", False]}) - root.add_event("event0", {"attr1": dict()}) + root.add_event("event0", {"attr1": {}}) root.add_event("event0", {"attr1": [[True]]}) - root.add_event("event0", {"attr1": [dict()], "attr2": [1, 2]}) + root.add_event("event0", {"attr1": [{}], "attr2": [1, 2]}) self.assertEqual(len(root.events), 4) self.assertEqual(root.events[0].attributes, {"attr1": True}) From 664ae4aee91daec662dc70cddf839cc219face4a Mon Sep 17 00:00:00 2001 From: alrex Date: Sun, 19 Sep 2021 17:06:16 -0700 Subject: [PATCH 0975/1517] update open calls to pass encoding (#2127) * update open calls to pass encoding * don't use encoding for binary files Co-authored-by: Owais Lone --- docs/examples/error_handler/error_handler_0/setup.py | 2 +- docs/examples/error_handler/error_handler_1/setup.py | 2 +- .../opentelemetry-exporter-jaeger-proto-grpc/setup.py | 2 +- exporter/opentelemetry-exporter-jaeger-thrift/setup.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.py | 2 +- exporter/opentelemetry-exporter-opencensus/setup.py | 2 +- .../opentelemetry-exporter-otlp-proto-grpc/setup.py | 2 +- .../opentelemetry-exporter-otlp-proto-http/setup.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.py | 2 +- exporter/opentelemetry-exporter-zipkin-json/setup.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/setup.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.py | 2 +- opentelemetry-api/setup.py | 2 +- opentelemetry-distro/setup.py | 2 +- opentelemetry-instrumentation/setup.py | 2 +- opentelemetry-proto/setup.py | 2 +- opentelemetry-sdk/setup.py | 2 +- opentelemetry-semantic-conventions/setup.py | 2 +- propagator/opentelemetry-propagator-b3/setup.py | 2 +- propagator/opentelemetry-propagator-jaeger/setup.py | 2 +- scripts/check_for_valid_readme.py | 2 +- scripts/eachdist.py | 10 +++++----- shim/opentelemetry-opentracing-shim/setup.py | 2 +- tests/util/setup.py | 2 +- 24 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/examples/error_handler/error_handler_0/setup.py b/docs/examples/error_handler/error_handler_0/setup.py index 9e174aa7bb..327b1ce600 100644 --- a/docs/examples/error_handler/error_handler_0/setup.py +++ b/docs/examples/error_handler/error_handler_0/setup.py @@ -20,7 +20,7 @@ BASE_DIR, "src", "error_handler_0", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/docs/examples/error_handler/error_handler_1/setup.py b/docs/examples/error_handler/error_handler_1/setup.py index ccb282dbb2..705d2a2be4 100644 --- a/docs/examples/error_handler/error_handler_1/setup.py +++ b/docs/examples/error_handler/error_handler_1/setup.py @@ -20,7 +20,7 @@ BASE_DIR, "src", "error_handler_1", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.py index 642093080e..93d478eb97 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.py @@ -27,7 +27,7 @@ "version.py", ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.py b/exporter/opentelemetry-exporter-jaeger-thrift/setup.py index e9200b0c7a..9a2312e312 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.py @@ -26,7 +26,7 @@ "version.py", ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-jaeger/setup.py b/exporter/opentelemetry-exporter-jaeger/setup.py index 1bd3970329..2303890631 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.py +++ b/exporter/opentelemetry-exporter-jaeger/setup.py @@ -20,7 +20,7 @@ BASE_DIR, "src", "opentelemetry", "exporter", "jaeger", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-opencensus/setup.py b/exporter/opentelemetry-exporter-opencensus/setup.py index 65c91aba9f..ecf4915d53 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.py +++ b/exporter/opentelemetry-exporter-opencensus/setup.py @@ -20,7 +20,7 @@ BASE_DIR, "src", "opentelemetry", "exporter", "opencensus", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.py index 616edde878..6b30ebfbfe 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.py @@ -27,7 +27,7 @@ "version.py", ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.py b/exporter/opentelemetry-exporter-otlp-proto-http/setup.py index 510eceba6c..5d3210b183 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.py @@ -27,7 +27,7 @@ "version.py", ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-otlp/setup.py b/exporter/opentelemetry-exporter-otlp/setup.py index c04c30fca4..ec0269160e 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.py +++ b/exporter/opentelemetry-exporter-otlp/setup.py @@ -20,7 +20,7 @@ BASE_DIR, "src", "opentelemetry", "exporter", "otlp", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.py b/exporter/opentelemetry-exporter-zipkin-json/setup.py index 1e8b53811c..b4a98ab648 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.py +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.py @@ -26,7 +26,7 @@ "version.py", ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.py b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.py index 707e6c1a12..62cf7e5ffd 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.py @@ -27,7 +27,7 @@ "version.py", ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-zipkin/setup.py b/exporter/opentelemetry-exporter-zipkin/setup.py index 9822805d9d..c763810cf5 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.py +++ b/exporter/opentelemetry-exporter-zipkin/setup.py @@ -20,7 +20,7 @@ BASE_DIR, "src", "opentelemetry", "exporter", "zipkin", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index 1bd85cb410..aac1579996 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -19,7 +19,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join(BASE_DIR, "src", "opentelemetry", "version.py") PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup( diff --git a/opentelemetry-distro/setup.py b/opentelemetry-distro/setup.py index 95af2c4840..4783772d06 100644 --- a/opentelemetry-distro/setup.py +++ b/opentelemetry-distro/setup.py @@ -21,7 +21,7 @@ BASE_DIR, "src", "opentelemetry", "distro", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup( diff --git a/opentelemetry-instrumentation/setup.py b/opentelemetry-instrumentation/setup.py index d4f84f738b..9d1d5b7b06 100644 --- a/opentelemetry-instrumentation/setup.py +++ b/opentelemetry-instrumentation/setup.py @@ -21,7 +21,7 @@ BASE_DIR, "src", "opentelemetry", "instrumentation", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup( diff --git a/opentelemetry-proto/setup.py b/opentelemetry-proto/setup.py index 8daf422ffa..6f80c6c0d4 100644 --- a/opentelemetry-proto/setup.py +++ b/opentelemetry-proto/setup.py @@ -21,7 +21,7 @@ BASE_DIR, "src", "opentelemetry", "proto", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup( diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index cf910e40f1..61fc07cbbf 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -21,7 +21,7 @@ BASE_DIR, "src", "opentelemetry", "sdk", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup( diff --git a/opentelemetry-semantic-conventions/setup.py b/opentelemetry-semantic-conventions/setup.py index 677d43cabb..3ec350247d 100644 --- a/opentelemetry-semantic-conventions/setup.py +++ b/opentelemetry-semantic-conventions/setup.py @@ -21,7 +21,7 @@ BASE_DIR, "src", "opentelemetry", "semconv", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup( diff --git a/propagator/opentelemetry-propagator-b3/setup.py b/propagator/opentelemetry-propagator-b3/setup.py index 4df5188b1d..ca5569293e 100644 --- a/propagator/opentelemetry-propagator-b3/setup.py +++ b/propagator/opentelemetry-propagator-b3/setup.py @@ -20,7 +20,7 @@ BASE_DIR, "src", "opentelemetry", "propagators", "b3", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/propagator/opentelemetry-propagator-jaeger/setup.py b/propagator/opentelemetry-propagator-jaeger/setup.py index 4fda43e516..dc0eaf6906 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.py +++ b/propagator/opentelemetry-propagator-jaeger/setup.py @@ -20,7 +20,7 @@ BASE_DIR, "src", "opentelemetry", "propagators", "jaeger", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/scripts/check_for_valid_readme.py b/scripts/check_for_valid_readme.py index 7eff0ae582..c65097ea56 100644 --- a/scripts/check_for_valid_readme.py +++ b/scripts/check_for_valid_readme.py @@ -8,7 +8,7 @@ def is_valid_rst(path): """Checks if RST can be rendered on PyPI.""" - with open(path) as readme_file: + with open(path, encoding="utf-8") as readme_file: markup = readme_file.read() return readme_renderer.rst.render(markup, stream=sys.stderr) is not None diff --git a/scripts/eachdist.py b/scripts/eachdist.py index f9c27eb835..51d3872d72 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -552,13 +552,13 @@ def lint_args(args): def update_changelog(path, version, new_entry): unreleased_changes = False try: - with open(path) as changelog: + with open(path, encoding="utf-8") as changelog: text = changelog.read() if "## [{}]".format(version) in text: raise AttributeError( "{} already contans version {}".format(path, version) ) - with open(path) as changelog: + with open(path, encoding="utf-8") as changelog: for line in changelog: if line.startswith("## [Unreleased]"): unreleased_changes = False @@ -574,7 +574,7 @@ def update_changelog(path, version, new_entry): if unreleased_changes: print("updating: {}".format(path)) text = re.sub(r"## \[Unreleased\].*", new_entry, text) - with open(path, "w") as changelog: + with open(path, "w", encoding="utf-8") as changelog: changelog.write(text) @@ -645,14 +645,14 @@ def update_files(targets, filename, search, replace): print("file missing: {}/{}".format(target, filename)) continue - with open(curr_file) as _file: + with open(curr_file, encoding="utf-8") as _file: text = _file.read() if replace in text: print("{} already contains {}".format(curr_file, replace)) continue - with open(curr_file, "w") as _file: + with open(curr_file, "w", encoding="utf-8") as _file: _file.write(re.sub(search, replace, text)) if errors: diff --git a/shim/opentelemetry-opentracing-shim/setup.py b/shim/opentelemetry-opentracing-shim/setup.py index 304a5e661e..35cdc05f4c 100644 --- a/shim/opentelemetry-opentracing-shim/setup.py +++ b/shim/opentelemetry-opentracing-shim/setup.py @@ -25,7 +25,7 @@ "version.py", ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/tests/util/setup.py b/tests/util/setup.py index 1a42a2c55d..fa0cf101e7 100644 --- a/tests/util/setup.py +++ b/tests/util/setup.py @@ -20,7 +20,7 @@ BASE_DIR, "src", "opentelemetry", "test", "version.py" ) PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: +with open(VERSION_FILENAME, encoding="utf-8") as f: exec(f.read(), PACKAGE_INFO) setuptools.setup(version=PACKAGE_INFO["__version__"]) From daef2a55f75e642957a2b8089208be5254c93280 Mon Sep 17 00:00:00 2001 From: alrex Date: Sun, 19 Sep 2021 17:34:25 -0700 Subject: [PATCH 0976/1517] don't use deprecated abstractclassmethod (#2135) Co-authored-by: Owais Lone --- .../opentelemetry-propagator-b3/tests/test_b3_format.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index ebefebaf4f..b1d0a9a6bd 100644 --- a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -13,7 +13,7 @@ # limitations under the License. import unittest -from abc import abstractclassmethod +from abc import abstractmethod from unittest.mock import Mock import opentelemetry.sdk.trace as trace @@ -81,11 +81,13 @@ def setUp(self) -> None: def get_child_parent_new_carrier(cls, old_carrier): return get_child_parent_new_carrier(old_carrier, cls.get_propagator()) - @abstractclassmethod + @classmethod + @abstractmethod def get_propagator(cls): pass - @abstractclassmethod + @classmethod + @abstractmethod def get_trace_id(cls, carrier): pass From 61c2d6e1508c75cce4b50ff7b28712c944b94128 Mon Sep 17 00:00:00 2001 From: alrex Date: Sun, 19 Sep 2021 19:04:23 -0700 Subject: [PATCH 0977/1517] fix imports based on pylint recommendations (#2124) Co-authored-by: Owais Lone --- .../tests/test_jaeger_exporter_protobuf.py | 7 ++++--- .../src/opentelemetry/trace/propagation/tracecontext.py | 2 +- .../src/opentelemetry/propagators/b3/__init__.py | 2 +- .../src/opentelemetry/propagators/jaeger/__init__.py | 3 +-- .../tests/test_jaeger_propagator.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py index 1123f9b28f..4448b2c838 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py @@ -17,12 +17,13 @@ from collections import OrderedDict from unittest import mock -# pylint:disable=no-name-in-module -# pylint:disable=import-error -import opentelemetry.exporter.jaeger.proto.grpc.gen.model_pb2 as model_pb2 import opentelemetry.exporter.jaeger.proto.grpc.translate as pb_translator from opentelemetry import trace as trace_api from opentelemetry.exporter.jaeger.proto.grpc import JaegerExporter + +# pylint:disable=no-name-in-module +# pylint:disable=import-error +from opentelemetry.exporter.jaeger.proto.grpc.gen import model_pb2 from opentelemetry.exporter.jaeger.proto.grpc.translate import ( NAME_KEY, VERSION_KEY, diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index 001db5c729..3fc1f2bd66 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -15,7 +15,7 @@ import re import typing -import opentelemetry.trace as trace +from opentelemetry import trace from opentelemetry.context.context import Context from opentelemetry.propagators import textmap from opentelemetry.trace import format_span_id, format_trace_id diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py index c75d8e9a54..81c8167d74 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py @@ -17,7 +17,7 @@ from deprecated import deprecated -import opentelemetry.trace as trace +from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.propagators.textmap import ( CarrierT, diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py index 65dc43487e..fe16c26081 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py @@ -15,8 +15,7 @@ import typing import urllib.parse -import opentelemetry.trace as trace -from opentelemetry import baggage +from opentelemetry import baggage, trace from opentelemetry.context import Context from opentelemetry.propagators.textmap import ( CarrierT, diff --git a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py index 9026558470..f01e0e53da 100644 --- a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py @@ -15,8 +15,6 @@ import unittest from unittest.mock import Mock -import opentelemetry.sdk.trace as trace -import opentelemetry.sdk.trace.id_generator as id_generator import opentelemetry.trace as trace_api from opentelemetry import baggage from opentelemetry.baggage import _BAGGAGE_KEY @@ -24,6 +22,8 @@ from opentelemetry.propagators import ( # pylint: disable=no-name-in-module jaeger, ) +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace import id_generator FORMAT = jaeger.JaegerPropagator() From 45a2b350e54da9f99a718148d257ee4e28b1d8e4 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 20 Sep 2021 08:32:05 -0700 Subject: [PATCH 0978/1517] use f-strings (#2131) Co-authored-by: Owais Lone --- .../exporter/jaeger/thrift/send.py | 4 +- .../tests/test_otlp_trace_exporter.py | 8 +--- .../exporter/zipkin/node_endpoint.py | 4 +- .../src/opentelemetry/attributes/__init__.py | 4 +- .../src/opentelemetry/trace/__init__.py | 2 +- .../trace/propagation/tracecontext.py | 6 +-- .../src/opentelemetry/trace/span.py | 19 ++-------- .../src/opentelemetry/util/_providers.py | 4 +- .../auto_instrumentation/sitecustomize.py | 2 +- .../instrumentation/bootstrap.py | 10 ++--- .../instrumentation/dependencies.py | 4 +- .../instrumentation/propagators.py | 8 +--- .../tests/test_dependencies.py | 4 +- .../sdk/_configuration/__init__.py | 10 ++--- .../src/opentelemetry/sdk/trace/__init__.py | 37 +++++-------------- .../src/opentelemetry/sdk/trace/sampling.py | 14 ++----- .../src/opentelemetry/sdk/util/__init__.py | 8 ++-- .../opentelemetry/sdk/util/instrumentation.py | 4 +- .../tests/trace/export/test_export.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 19 ++++------ .../tests/test_b3_format.py | 14 ++----- .../propagators/jaeger/__init__.py | 7 +--- scripts/eachdist.py | 32 ++++++++-------- scripts/public_symbols_checker.py | 4 +- .../test_asyncio.py | 2 +- .../test_threads.py | 2 +- .../test_listener_per_request/test_asyncio.py | 2 +- .../test_listener_per_request/test_threads.py | 2 +- .../test_nested_callbacks/test_asyncio.py | 2 +- .../test_nested_callbacks/test_threads.py | 2 +- .../test_asyncio.py | 2 +- .../test_threads.py | 2 +- .../src/opentelemetry/test/spantestutil.py | 6 +-- .../util/src/opentelemetry/test/test_base.py | 4 +- 34 files changed, 85 insertions(+), 171 deletions(-) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py index c7827038b1..9ddc6d7b27 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py @@ -123,9 +123,9 @@ def __init__(self, thrift_url="", auth=None, timeout_in_millis=None): # set basic auth header if auth is not None: - auth_header = "{}:{}".format(*auth) + auth_header = f"{auth[0]}:{auth[1]}" decoded = base64.b64encode(auth_header.encode()).decode("ascii") - basic_auth = dict(Authorization="Basic {}".format(decoded)) + basic_auth = dict(Authorization=f"Basic {decoded}") self.http_transport.setCustomHeaders(basic_auth) def submit(self, batch: jaeger.Batch): diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 1a244b0067..198ff44271 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -320,16 +320,12 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): self.assertEqual( 1, mock_method.call_count, - "expected {} to be called for {} {}".format( - mock_method, endpoint, insecure - ), + f"expected {mock_method} to be called for {endpoint} {insecure}", ) self.assertEqual( expected_endpoint, mock_method.call_args[0][0], - "expected {} got {} {}".format( - expected_endpoint, mock_method.call_args[0][0], endpoint - ), + f"expected {expected_endpoint} got {mock_method.call_args[0][0]} {endpoint}", ) mock_method.reset_mock() diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/node_endpoint.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/node_endpoint.py index ee7cb71f02..67f5d0ad12 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/node_endpoint.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/node_endpoint.py @@ -64,7 +64,7 @@ def ipv4(self, address: IpInput) -> None: ipv4_address = ipaddress.ip_address(address) if not isinstance(ipv4_address, ipaddress.IPv4Address): raise ValueError( - "%r does not appear to be an IPv4 address" % address + f"{address!r} does not appear to be an IPv4 address" ) self._ipv4 = ipv4_address @@ -80,6 +80,6 @@ def ipv6(self, address: IpInput) -> None: ipv6_address = ipaddress.ip_address(address) if not isinstance(ipv6_address, ipaddress.IPv6Address): raise ValueError( - "%r does not appear to be an IPv6 address" % address + f"{address!r} does not appear to be an IPv6 address" ) self._ipv6 = ipv6_address diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 7fd185f317..20d9ae91e1 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -153,8 +153,8 @@ def __init__( self._immutable = immutable def __repr__(self): - return "{}({}, maxlen={})".format( - type(self).__name__, dict(self._dict), self.maxlen + return ( + f"{type(self).__name__}({dict(self._dict)}, maxlen={self.maxlen})" ) def __getitem__(self, key): diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 58d75bbea8..482d43a044 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -537,7 +537,7 @@ def use_span( span.set_status( Status( status_code=StatusCode.ERROR, - description="{}: {}".format(type(exc).__name__, exc), + description=f"{type(exc).__name__}: {exc}", ) ) raise diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index 3fc1f2bd66..14b68876b3 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -100,11 +100,7 @@ def inject( span_context = span.get_span_context() if span_context == trace.INVALID_SPAN_CONTEXT: return - traceparent_string = "00-{trace_id}-{span_id}-{:02x}".format( - span_context.trace_flags, - trace_id=format_trace_id(span_context.trace_id), - span_id=format_span_id(span_context.span_id), - ) + traceparent_string = f"00-{format_trace_id(span_context.trace_id)}-{format_span_id(span_context.span_id)}-{span_context.trace_flags:02x}" setter.set(carrier, self._TRACEPARENT_HEADER_NAME, traceparent_string) if span_context.trace_state: tracestate_string = span_context.trace_state.to_header() diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 832b8b62f6..239fbfd3af 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -40,9 +40,7 @@ _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS = 32 _delimiter_pattern = re.compile(r"[ \t]*,[ \t]*") -_member_pattern = re.compile( - "({})(=)({})[ \t]*".format(_KEY_FORMAT, _VALUE_FORMAT) -) +_member_pattern = re.compile(f"({_KEY_FORMAT})(=)({_VALUE_FORMAT})[ \t]*") _logger = logging.getLogger(__name__) @@ -245,7 +243,7 @@ def __len__(self) -> int: def __repr__(self) -> str: pairs = [ - "{key=%s, value=%s}" % (key, value) + f"{{key={key}, value={value}}}" for key, value in self._dict.items() ] return str(pairs) @@ -478,16 +476,7 @@ def __delattr__(self, *args: str) -> None: ) def __repr__(self) -> str: - return ( - "{}(trace_id=0x{}, span_id=0x{}, trace_flags=0x{:02x}, trace_state={!r}, is_remote={})" - ).format( - type(self).__name__, - format_trace_id(self.trace_id), - format_span_id(self.span_id), - self.trace_flags, - self.trace_state, - self.is_remote, - ) + return f"{type(self).__name__}(trace_id=0x{format_trace_id(self.trace_id)}, span_id=0x{format_span_id(self.span_id)}, trace_flags=0x{self.trace_flags:02x}, trace_state={self.trace_state!r}, is_remote={self.is_remote})" class NonRecordingSpan(Span): @@ -540,7 +529,7 @@ def record_exception( pass def __repr__(self) -> str: - return "NonRecordingSpan({!r})".format(self._context) + return f"NonRecordingSpan({self._context!r})" INVALID_SPAN_ID = 0x0000000000000000 diff --git a/opentelemetry-api/src/opentelemetry/util/_providers.py b/opentelemetry-api/src/opentelemetry/util/_providers.py index 0e31f702ba..98c75ed06b 100644 --- a/opentelemetry-api/src/opentelemetry/util/_providers.py +++ b/opentelemetry-api/src/opentelemetry/util/_providers.py @@ -32,12 +32,12 @@ def _load_provider( try: entry_point = next( iter_entry_points( - "opentelemetry_{}".format(provider), + f"opentelemetry_{provider}", name=cast( str, environ.get( provider_environment_variable, - "default_{}".format(provider), + f"default_{provider}", ), ), ) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index 5bbf0685ff..a4a5dc8d5a 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -119,7 +119,7 @@ def initialize(): logger.exception("Failed to auto initialize opentelemetry") finally: environ["PYTHONPATH"] = sub( - r"{}{}?".format(dirname(abspath(__file__)), pathsep), + fr"{dirname(abspath(__file__))}{pathsep}?", "", environ["PYTHONPATH"], ) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index abeb23df3d..7fe8de0ef0 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -38,11 +38,9 @@ def wrapper(package=None): except subprocess.SubprocessError as exp: cmd = getattr(exp, "cmd", None) if cmd: - msg = 'Error calling system command "{0}"'.format( - " ".join(cmd) - ) + msg = f'Error calling system command "{" ".join(cmd)}"' if package: - msg = '{0} for package "{1}"'.format(msg, package) + msg = f'{msg} for package "{package}"' raise RuntimeError(msg) return wrapper @@ -81,9 +79,7 @@ def _pip_check(): for package_tup in libraries.values(): for package in package_tup: if package.lower() in pip_check_lower: - raise RuntimeError( - "Dependency conflict found: {}".format(pip_check) - ) + raise RuntimeError(f"Dependency conflict found: {pip_check}") def _is_installed(req): diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py index 0cec55769c..6c65d6677e 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py @@ -21,9 +21,7 @@ def __init__(self, required, found=None): self.found = found def __str__(self): - return 'DependencyConflict: requested: "{0}" but found: "{1}"'.format( - self.required, self.found - ) + return f'DependencyConflict: requested: "{self.required}" but found: "{self.found}"' def get_dist_dependency_conflicts( diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py index 4d6ce39eae..bc40f7742c 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py @@ -54,7 +54,7 @@ class DictHeaderSetter(Setter): def set(self, carrier, key, value): # pylint: disable=no-self-use old_value = carrier.get(key, "") if old_value: - value = "{0}, {1}".format(old_value, value) + value = f"{old_value}, {value}" carrier[key] = value @@ -115,11 +115,7 @@ def inject( setter.set( carrier, header_name, - "00-{trace_id}-{span_id}-{:02x}".format( - span_context.trace_flags, - trace_id=format_trace_id(span_context.trace_id), - span_id=format_span_id(span_context.span_id), - ), + f"00-{format_trace_id(span_context.trace_id)}-{format_span_id(span_context.span_id)}-{span_context.trace_flags:02x}", ) setter.set( carrier, diff --git a/opentelemetry-instrumentation/tests/test_dependencies.py b/opentelemetry-instrumentation/tests/test_dependencies.py index 8b2f2e9b39..a8acac62f4 100644 --- a/opentelemetry-instrumentation/tests/test_dependencies.py +++ b/opentelemetry-instrumentation/tests/test_dependencies.py @@ -50,9 +50,7 @@ def test_get_dependency_conflicts_mismatched_version(self): self.assertTrue(isinstance(conflict, DependencyConflict)) self.assertEqual( str(conflict), - 'DependencyConflict: requested: "pytest == 5000" but found: "pytest {0}"'.format( - pytest.__version__ - ), + f'DependencyConflict: requested: "pytest == 5000" but found: "pytest {pytest.__version__}"', ) def test_get_dist_dependency_conflicts(self): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 65bb92eb7a..78bb137780 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -91,9 +91,7 @@ def _import_tracer_provider_config_components( entry_point = component_entry_points.get(selected_component, None) if not entry_point: raise RuntimeError( - "Requested component '{}' not found in entry points for '{}'".format( - selected_component, entry_point_name - ) + f"Requested component '{selected_component}' not found in entry points for '{entry_point_name}'" ) component_impl = entry_point.load() @@ -116,9 +114,7 @@ def _import_exporters( if issubclass(exporter_impl, SpanExporter): trace_exporters[exporter_name] = exporter_impl else: - raise RuntimeError( - "{0} is not a trace exporter".format(exporter_name) - ) + raise RuntimeError(f"{exporter_name} is not a trace exporter") return trace_exporters @@ -133,7 +129,7 @@ def _import_id_generator(id_generator_name: str) -> IdGenerator: if issubclass(id_generator_impl, IdGenerator): return id_generator_impl - raise RuntimeError("{0} is not an IdGenerator".format(id_generator_name)) + raise RuntimeError(f"{id_generator_name} is not an IdGenerator") def _initialize_components(): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 135546b362..ab6df18fdd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -219,7 +219,7 @@ def _submit_and_await( self, func: Callable[[SpanProcessor], Callable[..., None]], *args: Any, - **kwargs: Any + **kwargs: Any, ): futures = [] for sp in self._span_processors: @@ -442,12 +442,10 @@ def to_json(self, indent=4): if self.parent is not None: if isinstance(self.parent, Span): ctx = self.parent.context - parent_id = "0x{}".format( - trace_api.format_span_id(ctx.span_id) - ) + parent_id = f"0x{trace_api.format_span_id(ctx.span_id)}" elif isinstance(self.parent, SpanContext): - parent_id = "0x{}".format( - trace_api.format_span_id(self.parent.span_id) + parent_id = ( + f"0x{trace_api.format_span_id(self.parent.span_id)}" ) start_time = None @@ -484,12 +482,8 @@ def to_json(self, indent=4): @staticmethod def _format_context(context): x_ctx = OrderedDict() - x_ctx["trace_id"] = "0x{}".format( - trace_api.format_trace_id(context.trace_id) - ) - x_ctx["span_id"] = "0x{}".format( - trace_api.format_span_id(context.span_id) - ) + x_ctx["trace_id"] = f"0x{trace_api.format_trace_id(context.trace_id)}" + x_ctx["span_id"] = f"0x{trace_api.format_span_id(context.span_id)}" x_ctx["trace_state"] = repr(context.trace_state) return x_ctx @@ -612,16 +606,7 @@ def __init__( ) def __repr__(self): - return "{}(max_span_attributes={}, max_events_attributes={}, max_link_attributes={}, max_attributes={}, max_events={}, max_links={}, max_attribute_length={})".format( - type(self).__name__, - self.max_span_attribute_length, - self.max_event_attributes, - self.max_link_attributes, - self.max_attributes, - self.max_events, - self.max_links, - self.max_attribute_length, - ) + return f"{type(self).__name__}(max_span_attributes={self.max_span_attribute_length}, max_events_attributes={self.max_event_attributes}, max_link_attributes={self.max_link_attributes}, max_attributes={self.max_attributes}, max_events={self.max_events}, max_links={self.max_links}, max_attribute_length={self.max_attribute_length})" @classmethod def _from_env_if_absent( @@ -759,9 +744,7 @@ def __init__( self._links = BoundedList.from_seq(self._limits.max_links, links) def __repr__(self): - return '{}(name="{}", context={})'.format( - type(self).__name__, self._name, self._context - ) + return f'{type(self).__name__}(name="{self._name}", context={self._context})' def _new_events(self): return BoundedList(self._limits.max_events) @@ -889,9 +872,7 @@ def __exit__( self.set_status( Status( status_code=StatusCode.ERROR, - description="{}: {}".format( - exc_type.__name__, exc_val - ), + description=f"{exc_type.__name__}: {exc_val}", ) ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 878c5d816c..4aac5f6f88 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -145,9 +145,7 @@ class SamplingResult: """ def __repr__(self) -> str: - return "{}({}, attributes={})".format( - type(self).__name__, str(self.decision), str(self.attributes) - ) + return f"{type(self).__name__}({str(self.decision)}, attributes={str(self.attributes)})" def __init__( self, @@ -271,7 +269,7 @@ def should_sample( ) def get_description(self) -> str: - return "TraceIdRatioBased{{{}}}".format(self._rate) + return f"TraceIdRatioBased{{{self._rate}}}" class ParentBased(Sampler): @@ -342,13 +340,7 @@ def should_sample( ) def get_description(self): - return "ParentBased{{root:{},remoteParentSampled:{},remoteParentNotSampled:{}," "localParentSampled:{},localParentNotSampled:{}}}".format( - self._root.get_description(), - self._remote_parent_sampled.get_description(), - self._remote_parent_not_sampled.get_description(), - self._local_parent_sampled.get_description(), - self._local_parent_not_sampled.get_description(), - ) + return f"ParentBased{{root:{self._root.get_description()},remoteParentSampled:{self._remote_parent_sampled.get_description()},remoteParentNotSampled:{self._remote_parent_not_sampled.get_description()},localParentSampled:{self._local_parent_sampled.get_description()},localParentNotSampled:{self._local_parent_not_sampled.get_description()}}}" DEFAULT_OFF = ParentBased(ALWAYS_OFF) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index a94fe647b7..e1857d8e62 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -54,9 +54,7 @@ def __init__(self, maxlen: Optional[int]): self._lock = threading.Lock() def __repr__(self): - return "{}({}, maxlen={})".format( - type(self).__name__, list(self._dq), self._dq.maxlen - ) + return f"{type(self).__name__}({list(self._dq)}, maxlen={self._dq.maxlen})" def __getitem__(self, index): return self._dq[index] @@ -113,8 +111,8 @@ def __init__(self, maxlen: Optional[int]): self._lock = threading.Lock() # type: threading.Lock def __repr__(self): - return "{}({}, maxlen={})".format( - type(self).__name__, dict(self._dict), self.maxlen + return ( + f"{type(self).__name__}({dict(self._dict)}, maxlen={self.maxlen})" ) def __getitem__(self, key): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index c55f26ec62..2c3dba4923 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -27,9 +27,7 @@ def __init__(self, name: str, version: str): self._version = version def __repr__(self): - return "{}({}, {})".format( - type(self).__name__, self._name, self._version - ) + return f"{type(self).__name__}({self._name}, {self._version})" def __hash__(self): return hash((self._name, self._version)) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 23f9308e08..c698f4d6e6 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -259,7 +259,7 @@ def test_flush_from_multiple_threads(self): def create_spans_and_flush(tno: int): for span_idx in range(num_spans): _create_start_and_end_span( - "Span {}-{}".format(tno, span_idx), span_processor + f"Span {tno}-{span_idx}", span_processor ) self.assertTrue(span_processor.force_flush()) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index f4b75d106b..09fcec6a9b 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -1515,8 +1515,7 @@ def _test_span_limits( ] some_attrs = { - "init_attribute_{}".format(idx): self.long_val - for idx in range(100) + f"init_attribute_{idx}": self.long_val for idx in range(100) } with tracer.start_as_current_span( "root", links=some_links, attributes=some_attrs @@ -1524,17 +1523,15 @@ def _test_span_limits( self.assertEqual(len(root.links), max_links) self.assertEqual(len(root.attributes), max_attrs) for idx in range(100): + root.set_attribute(f"my_str_attribute_{idx}", self.long_val) root.set_attribute( - "my_str_attribute_{}".format(idx), self.long_val + f"my_byte_attribute_{idx}", self.long_val.encode() ) root.set_attribute( - "my_byte_attribute_{}".format(idx), self.long_val.encode() - ) - root.set_attribute( - "my_int_attribute_{}".format(idx), self.long_val.encode() + f"my_int_attribute_{idx}", self.long_val.encode() ) root.add_event( - "my_event_{}".format(idx), attributes={"k": self.long_val} + f"my_event_{idx}", attributes={"k": self.long_val} ) self.assertEqual(len(root.attributes), max_attrs) @@ -1576,7 +1573,7 @@ def _test_span_no_limits(self, tracer): with tracer.start_as_current_span("root") as root: for idx in range(num_events): root.add_event( - "my_event_{}".format(idx), attributes={"k": self.long_val} + f"my_event_{idx}", attributes={"k": self.long_val} ) self.assertEqual(len(root.events), num_events) @@ -1586,9 +1583,7 @@ def _test_span_no_limits(self, tracer): ) + randint(1, 100) with tracer.start_as_current_span("root") as root: for idx in range(num_attributes): - root.set_attribute( - "my_attribute_{}".format(idx), self.long_val - ) + root.set_attribute(f"my_attribute_{idx}", self.long_val) self.assertEqual(len(root.attributes), num_attributes) for attr_val in root.attributes.values(): diff --git a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index b1d0a9a6bd..4068e2060a 100644 --- a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -125,9 +125,7 @@ def test_extract_single_header(self): propagator = self.get_propagator() child, parent, _ = self.get_child_parent_new_carrier( { - propagator.SINGLE_HEADER_KEY: "{}-{}".format( - self.serialized_trace_id, self.serialized_span_id - ) + propagator.SINGLE_HEADER_KEY: f"{self.serialized_trace_id}-{self.serialized_span_id}" } ) @@ -144,11 +142,7 @@ def test_extract_single_header(self): child, parent, _ = self.get_child_parent_new_carrier( { - propagator.SINGLE_HEADER_KEY: "{}-{}-1-{}".format( - self.serialized_trace_id, - self.serialized_span_id, - self.serialized_parent_id, - ) + propagator.SINGLE_HEADER_KEY: f"{self.serialized_trace_id}-{self.serialized_span_id}-1-{self.serialized_parent_id}" } ) @@ -173,9 +167,7 @@ def test_extract_header_precedence(self): _, _, new_carrier = self.get_child_parent_new_carrier( { - propagator.SINGLE_HEADER_KEY: "{}-{}".format( - single_header_trace_id, self.serialized_span_id - ), + propagator.SINGLE_HEADER_KEY: f"{single_header_trace_id}-{self.serialized_span_id}", propagator.TRACE_ID_KEY: self.serialized_trace_id, propagator.SPAN_ID_KEY: self.serialized_span_id, propagator.SAMPLED_KEY: "1", diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py index fe16c26081..9589f97619 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py @@ -127,12 +127,7 @@ def _extract_baggage(self, getter, carrier, context): def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags): - return "{trace_id}:{span_id}:{parent_id}:{:02x}".format( - flags, - trace_id=format_trace_id(trace_id), - span_id=format_span_id(span_id), - parent_id=format_span_id(parent_span_id), - ) + return f"{format_trace_id(trace_id)}:{format_span_id(span_id)}:{format_span_id(parent_span_id)}:{flags:02x}" def _extract_first_element( diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 51d3872d72..58064a7400 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -31,14 +31,12 @@ def unique(elems): def extraargs_help(calledcmd): return cleandoc( - """ - Additional arguments to pass on to {}. + f""" + Additional arguments to pass on to {calledcmd}. This is collected from any trailing arguments passed to `%(prog)s`. Use an initial `--` to separate them from regular arguments. - """.format( - calledcmd - ) + """ ) @@ -405,7 +403,7 @@ def execute_args(args): rootpath = find_projectroot() targets = find_targets(args.mode, rootpath) if not targets: - sys.exit("Error: No targets selected (root: {})".format(rootpath)) + sys.exit(f"Error: No targets selected (root: {rootpath})") def fmt_for_path(fmt, path): return fmt.format( @@ -421,7 +419,7 @@ def _runcmd(cmd): ) if result is not None and result.returncode not in args.allowexitcode: print( - "'{}' failed with code {}".format(cmd, result.returncode), + f"'{cmd}' failed with code {result.returncode}", file=sys.stderr, ) sys.exit(result.returncode) @@ -476,7 +474,7 @@ def install_args(args): if args.with_test_deps: extras.append("test") if extras: - allfmt += "[{}]".format(",".join(extras)) + allfmt += f"[{','.join(extras)}]" # note the trailing single quote, to close the quote opened above. allfmt += "'" @@ -554,9 +552,9 @@ def update_changelog(path, version, new_entry): try: with open(path, encoding="utf-8") as changelog: text = changelog.read() - if "## [{}]".format(version) in text: + if f"## [{version}]" in text: raise AttributeError( - "{} already contans version {}".format(path, version) + f"{path} already contans version {version}" ) with open(path, encoding="utf-8") as changelog: for line in changelog: @@ -568,11 +566,11 @@ def update_changelog(path, version, new_entry): unreleased_changes = True except FileNotFoundError: - print("file missing: {}".format(path)) + print(f"file missing: {path}") return if unreleased_changes: - print("updating: {}".format(path)) + print(f"updating: {path}") text = re.sub(r"## \[Unreleased\].*", new_entry, text) with open(path, "w", encoding="utf-8") as changelog: changelog.write(text) @@ -622,7 +620,7 @@ def update_version_files(targets, version, packages): targets, "version.py", "__version__ .*", - '__version__ = "{}"'.format(version), + f'__version__ = "{version}"', ) @@ -632,7 +630,7 @@ def update_dependencies(targets, version, packages): update_files( targets, "setup.cfg", - r"({}.*)==(.*)".format(basename(pkg)), + fr"({basename(pkg)}.*)==(.*)", r"\1== " + version, ) @@ -642,14 +640,14 @@ def update_files(targets, filename, search, replace): for target in targets: curr_file = find(filename, target) if curr_file is None: - print("file missing: {}/{}".format(target, filename)) + print(f"file missing: {target}/{filename}") continue with open(curr_file, encoding="utf-8") as _file: text = _file.read() if replace in text: - print("{} already contains {}".format(curr_file, replace)) + print(f"{curr_file} already contains {replace}") continue with open(curr_file, "w", encoding="utf-8") as _file: @@ -673,7 +671,7 @@ def release_args(args): version = mcfg["version"] updated_versions.append(version) packages = mcfg["packages"].split() - print("update {} packages to {}".format(group, version)) + print(f"update {group} packages to {version}") update_dependencies(targets, version, packages) update_version_files(targets, version, packages) diff --git a/scripts/public_symbols_checker.py b/scripts/public_symbols_checker.py index bdc530e95d..3be1c3ee8b 100644 --- a/scripts/public_symbols_checker.py +++ b/scripts/public_symbols_checker.py @@ -78,9 +78,9 @@ def m_diff_lines_getter(diff_lines): print("The code in this branch adds the following public symbols:") print() for file_path, symbols in file_path_symbols.items(): - print("- {}".format(file_path)) + print(f"- {file_path}") for symbol in symbols: - print("\t{}".format(symbol)) + print(f"\t{symbol}") print() print( diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py index 64cfea933c..e94a643c29 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py @@ -29,7 +29,7 @@ async def after_handler(): await before_handler() await after_handler() - return "%s::response" % message + return f"{message}::response" def send(self, message): return self.send_task(message) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py index 36c0a8b841..99693461e3 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py @@ -29,7 +29,7 @@ def after_handler(): self.executor.submit(before_handler).result() self.executor.submit(after_handler).result() - return "%s::response" % message + return f"{message}::response" def send(self, message): return self.executor.submit(self.send_task, message) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py index 085c0ea813..35153cb627 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py @@ -16,7 +16,7 @@ def __init__(self, tracer, loop): self.loop = loop async def task(self, message, listener): - res = "%s::response" % message + res = f"{message}::response" listener.on_response(res) return res diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py index 8f82e1fb15..dc40373912 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py @@ -17,7 +17,7 @@ def __init__(self, tracer): def _task(self, message, listener): # pylint: disable=no-self-use - res = "%s::response" % message + res = f"{message}::response" listener.on_response(res) return res diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py index 12eb436277..1e6d2a3604 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py @@ -34,7 +34,7 @@ async def task(): for idx in range(1, 4): self.assertEqual( - spans[0].attributes.get("key%s" % idx, None), str(idx) + spans[0].attributes.get(f"key{idx}", None), str(idx) ) def submit(self): diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py index a1d35c35d8..5bb63ab9e7 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py @@ -32,7 +32,7 @@ def test_main(self): for idx in range(1, 4): self.assertEqual( - spans[0].attributes.get("key%s" % idx, None), str(idx) + spans[0].attributes.get(f"key{idx}", None), str(idx) ) def submit(self): diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py index 6e54456070..2bf8b3cbd8 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py @@ -29,4 +29,4 @@ async def parent_task(self, message): # noqa async def child_task(self, message): # No need to pass/activate the parent Span, as it stays in the context. with self.tracer.start_active_span("child"): - return "%s::response" % message + return f"{message}::response" diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py index 1ba5f697ca..09eddf3448 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py @@ -30,4 +30,4 @@ def parent_task(self, message): def child_task(self, message, span): with self.tracer.scope_manager.activate(span, False): with self.tracer.start_active_span("child"): - return "%s::response" % message + return f"{message}::response" diff --git a/tests/util/src/opentelemetry/test/spantestutil.py b/tests/util/src/opentelemetry/test/spantestutil.py index 408f4c4947..ea83b90b8d 100644 --- a/tests/util/src/opentelemetry/test/spantestutil.py +++ b/tests/util/src/opentelemetry/test/spantestutil.py @@ -55,13 +55,13 @@ def setUp(self): def get_span_with_dropped_attributes_events_links(): attributes = {} for index in range(130): - attributes["key{}".format(index)] = ["value{}".format(index)] + attributes[f"key{index}"] = [f"value{index}"] links = [] for index in range(129): links.append( trace_api.Link( trace_sdk._Span( - name="span{}".format(index), + name=f"span{index}", context=trace_api.INVALID_SPAN_CONTEXT, attributes=attributes, ).get_span_context(), @@ -77,5 +77,5 @@ def get_span_with_dropped_attributes_events_links(): "span", links=links, attributes=attributes ) as span: for index in range(131): - span.add_event("event{}".format(index), attributes=attributes) + span.add_event(f"event{index}", attributes=attributes) return span diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index 4945d1cb32..9762a08010 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -113,12 +113,12 @@ def by_name(self, name): for span in self: if span.name == name: return span - self.test.fail("Did not find span with name {}".format(name)) + self.test.fail(f"Did not find span with name {name}") return None def by_attr(self, key, value): for span in self: if span.attributes.get(key) == value: return span - self.test.fail("Did not find span with attrs {}={}".format(key, value)) + self.test.fail(f"Did not find span with attrs {key}={value}") return None From d9c22a87b6bfc5ec332588c764f82c32f068b2c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 20 Sep 2021 18:36:45 +0200 Subject: [PATCH 0979/1517] Add test base class with local HTTP server. (#2101) --- CHANGELOG.md | 2 + tests/util/src/opentelemetry/test/httptest.py | 68 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tests/util/src/opentelemetry/test/httptest.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e6e35414c8..00b3640cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2071](https://github.com/open-telemetry/opentelemetry-python/pull/2071)) - Do not skip sequence attribute on decode error ([#2097](https://github.com/open-telemetry/opentelemetry-python/pull/2097)) +- `opentelemetry-test`: Add `HttpTestBase` to allow tests with actual TCP sockets + ([#2101](https://github.com/open-telemetry/opentelemetry-python/pull/2101)) ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 diff --git a/tests/util/src/opentelemetry/test/httptest.py b/tests/util/src/opentelemetry/test/httptest.py new file mode 100644 index 0000000000..94964ea9f1 --- /dev/null +++ b/tests/util/src/opentelemetry/test/httptest.py @@ -0,0 +1,68 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +import unittest +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, HTTPServer +from threading import Thread + + +class HttpTestBase(unittest.TestCase): + DEFAULT_RESPONSE = b"Hello!" + + class Handler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" # Support keep-alive. + # timeout = 3 # No timeout -- if shutdown hangs, make sure to close your connection + + STATUS_RE = re.compile(r"/status/(\d+)") + + def do_GET(self): # pylint:disable=invalid-name + status_match = self.STATUS_RE.fullmatch(self.path) + status = 200 + if status_match: + status = int(status_match.group(1)) + if status == 200: + body = HttpTestBase.DEFAULT_RESPONSE + self.send_response(HTTPStatus.OK) + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + else: + self.send_error(status) + + @classmethod + def create_server(cls): + server_address = ("127.0.0.1", 0) # Only bind to localhost. + return HTTPServer(server_address, cls.Handler) + + @classmethod + def run_server(cls): + httpd = cls.create_server() + worker = Thread( + target=httpd.serve_forever, daemon=True, name="Test server worker" + ) + worker.start() + return worker, httpd + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.server_thread, cls.server = cls.run_server() + + @classmethod + def tearDownClass(cls): + cls.server.shutdown() + cls.server_thread.join() + super().tearDownClass() From 796e070e795701b2cd06efcaa81c453d7046b9c0 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 20 Sep 2021 23:12:41 +0530 Subject: [PATCH 0980/1517] Fix incorrect headers parsing via environment variables (#2103) --- CHANGELOG.md | 2 + .../exporter/otlp/proto/grpc/exporter.py | 16 +---- .../proto/http/trace_exporter/__init__.py | 25 ++----- .../tests/test_proto_span_exporter.py | 12 ++-- .../src/opentelemetry/util/re.py | 56 ++++++++++++++++ opentelemetry-api/tests/util/test_re.py | 66 +++++++++++++++++++ 6 files changed, 141 insertions(+), 36 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/util/re.py create mode 100644 opentelemetry-api/tests/util/test_re.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 00b3640cf1..241c8252d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2097](https://github.com/open-telemetry/opentelemetry-python/pull/2097)) - `opentelemetry-test`: Add `HttpTestBase` to allow tests with actual TCP sockets ([#2101](https://github.com/open-telemetry/opentelemetry-python/pull/2101)) +- Fix incorrect headers parsing via environment variables + ([#2103](https://github.com/open-telemetry/opentelemetry-python/pull/2103)) ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 661d7a96ea..15596ebc9f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -50,6 +50,7 @@ OTEL_EXPORTER_OTLP_TIMEOUT, ) from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.util.re import parse_headers logger = logging.getLogger(__name__) SDKDataT = TypeVar("SDKDataT") @@ -228,19 +229,8 @@ def __init__( self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS) if isinstance(self._headers, str): - temp_headers = [] - for header_pair in self._headers.split(","): - key, value = header_pair.split("=", maxsplit=1) - key = key.strip().lower() - value = value.strip() - temp_headers.append( - ( - key, - value, - ) - ) - - self._headers = tuple(temp_headers) + temp_headers = parse_headers(self._headers) + self._headers = tuple(temp_headers.items()) self._timeout = timeout or int( environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 10) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 061484d0e3..211d037999 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -40,6 +40,7 @@ from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import ( _ProtobufEncoder, ) +from opentelemetry.util.re import parse_headers _logger = logging.getLogger(__name__) @@ -70,7 +71,11 @@ def __init__( OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), ) - self._headers = headers or _headers_from_env() + headers_string = environ.get( + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), + ) + self._headers = headers or parse_headers(headers_string) self._timeout = timeout or int( environ.get( OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, @@ -155,24 +160,6 @@ def shutdown(self): self._shutdown = True -def _headers_from_env() -> Optional[Dict[str, str]]: - headers_str = environ.get( - OTEL_EXPORTER_OTLP_TRACES_HEADERS, - environ.get(OTEL_EXPORTER_OTLP_HEADERS), - ) - headers = {} - if headers_str: - for header in headers_str.split(","): - try: - header_name, header_value = header.split("=") - headers[header_name.strip()] = header_value.strip() - except ValueError: - _logger.warning( - "Skipped invalid OTLP exporter header: %r", header - ) - return headers - - def _compression_from_env() -> Compression: compression = ( environ.get( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index 9b4bb53ad4..7f91e05984 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -64,7 +64,7 @@ def test_constructor_default(self): OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "traces/certificate.env", OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: Compression.Deflate.value, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://traces.endpoint.env", - OTEL_EXPORTER_OTLP_TRACES_HEADERS: "tracesEnv1=val1,tracesEnv2=val2", + OTEL_EXPORTER_OTLP_TRACES_HEADERS: "tracesEnv1=val1,tracesEnv2=val2,traceEnv3===val3==", OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "40", }, ) @@ -77,7 +77,11 @@ def test_exporter_traces_env_take_priority(self): self.assertIs(exporter._compression, Compression.Deflate) self.assertEqual( exporter._headers, - {"tracesEnv1": "val1", "tracesEnv2": "val2"}, + { + "tracesenv1": "val1", + "tracesenv2": "val2", + "traceenv3": "==val3==", + }, ) @patch.dict( @@ -127,7 +131,7 @@ def test_exporter_env(self): self.assertEqual(exporter._timeout, int(OS_ENV_TIMEOUT)) self.assertIs(exporter._compression, Compression.Gzip) self.assertEqual( - exporter._headers, {"envHeader1": "val1", "envHeader2": "val2"} + exporter._headers, {"envheader1": "val1", "envheader2": "val2"} ) @patch.dict( @@ -143,5 +147,5 @@ def test_headers_parse_from_env(self): self.assertEqual( cm.records[0].message, - "Skipped invalid OTLP exporter header: 'missingValue'", + "Header doesn't match the format: missingValue.", ) diff --git a/opentelemetry-api/src/opentelemetry/util/re.py b/opentelemetry-api/src/opentelemetry/util/re.py new file mode 100644 index 0000000000..b503c4893a --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/util/re.py @@ -0,0 +1,56 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from re import compile, split +from typing import Mapping + +_logger = logging.getLogger(__name__) + + +# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables +_OWS = r"[ \t]*" +# A key contains one or more US-ASCII character except CTLs or separators. +_KEY_FORMAT = ( + r"[\x21\x23-\x27\x2a\x2b\x2d\x2e\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+" +) +# A value contains a URL encoded UTF-8 string. +_VALUE_FORMAT = r"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*" +_HEADER_FORMAT = _KEY_FORMAT + _OWS + r"=" + _OWS + _VALUE_FORMAT +_HEADER_PATTERN = compile(_HEADER_FORMAT) +_DELIMITER_PATTERN = compile(r"[ \t]*,[ \t]*") + + +# pylint: disable=invalid-name +def parse_headers(s: str) -> Mapping[str, str]: + """ + Parse ``s`` (a ``str`` instance containing HTTP headers). Uses W3C Baggage + HTTP header format https://www.w3.org/TR/baggage/#baggage-http-header-format, except that + additional semi-colon delimited metadata is not supported. + """ + headers = {} + for header in split(_DELIMITER_PATTERN, s): + if not header: # empty string + continue + match = _HEADER_PATTERN.fullmatch(header.strip()) + if not match: + _logger.warning("Header doesn't match the format: %s.", header) + continue + # value may contain any number of `=` + name, value = match.string.split("=", 1) + name = name.strip().lower() + value = value.strip() + headers[name] = value + + return headers diff --git a/opentelemetry-api/tests/util/test_re.py b/opentelemetry-api/tests/util/test_re.py new file mode 100644 index 0000000000..9c726a7e57 --- /dev/null +++ b/opentelemetry-api/tests/util/test_re.py @@ -0,0 +1,66 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# type: ignore + +import unittest + +from opentelemetry.util.re import parse_headers + + +class TestParseHeaders(unittest.TestCase): + def test_parse_headers(self): + inp = [ + # invalid header name + ("=value", [], True), + ("}key=value", [], True), + ("@key()=value", [], True), + ("/key=value", [], True), + # invalid header value + ("name=\\", [], True), + ('name=value"', [], True), + ("name=;value", [], True), + # different header values + ("name=", [("name", "")], False), + ("name===value=", [("name", "==value=")], False), + # mix of valid and invalid headers + ( + "name1=value1,invalidName, name2 = value2 , name3=value3==", + [ + ( + "name1", + "value1", + ), + ("name2", "value2"), + ("name3", "value3=="), + ], + True, + ), + ( + "=name=valu3; key1; key2, content = application, red=\tvelvet; cake", + [("content", "application")], + True, + ), + ] + for case in inp: + s, expected, warn = case + if warn: + with self.assertLogs(level="WARNING") as cm: + self.assertEqual(parse_headers(s), dict(expected)) + self.assertTrue( + "Header doesn't match the format:" + in cm.records[0].message, + ) + else: + self.assertEqual(parse_headers(s), dict(expected)) From eacff3e84b9452357b7a163fc937b491c089c63f Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 21 Sep 2021 10:29:54 -0700 Subject: [PATCH 0981/1517] removing workflow to automatically close issues (#2136) --- .github/workflows/close-stale-issues.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .github/workflows/close-stale-issues.yml diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml deleted file mode 100644 index 943cec5df4..0000000000 --- a/.github/workflows/close-stale-issues.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: "Close stale issues" -on: - schedule: - - cron: "12 3 * * *" # arbitrary time not to DDOS GitHub - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'This issue was marked stale due to lack of activity. It will be closed in 30 days.' - close-issue-message: 'Closed as inactive. Feel free to reopen if this issue needs resolving.' - exempt-issue-labels: 'feature-request,triaged' - stale-issue-label: 'backlog' - days-before-stale: 30 - days-before-close: 60 From 8b5a967c836ba5654e7a537b4b4ec23458699d5d Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 21 Sep 2021 15:55:49 -0400 Subject: [PATCH 0982/1517] Fixing broken link in README (#2115) Co-authored-by: Srikanth Chekuri --- docs/examples/auto-instrumentation/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index dd33a5ea6f..aeb1e0be8e 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -233,5 +233,5 @@ Additional resources ~~~~~~~~~~~~~~~~~~~~ In order to send telemetry to an OpenTelemetry Collector without doing any -additional configuration, read about the `OpenTelemetry Distro <../distro/README.html>`_ +additional configuration, read about the `OpenTelemetry Distro <../distro>`_ package. From 8b92d57aa8483a55260a9f1c964d67e411f71691 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 23 Sep 2021 22:24:35 +0530 Subject: [PATCH 0983/1517] Add entry point for exporters with default protocol (#2093) --- CHANGELOG.md | 2 ++ exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg | 2 +- exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg | 4 ++-- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++++ exporter/opentelemetry-exporter-opencensus/setup.cfg | 4 ++-- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 4 ++-- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 4 ++-- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++++ exporter/opentelemetry-exporter-zipkin-json/setup.cfg | 4 ++-- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 4 ++-- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++++ opentelemetry-sdk/setup.cfg | 4 ++-- .../src/opentelemetry/sdk/_configuration/__init__.py | 2 +- 13 files changed, 30 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 241c8252d5..4e07232d06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2096](https://github.com/open-telemetry/opentelemetry-python/pull/2096)) - Fix propagation bug caused by counting skipped entries ([#2071](https://github.com/open-telemetry/opentelemetry-python/pull/2071)) +- Add entry point for exporters with default protocol + ([#2093](https://github.com/open-telemetry/opentelemetry-python/pull/2093)) - Do not skip sequence attribute on decode error ([#2097](https://github.com/open-telemetry/opentelemetry-python/pull/2097)) - `opentelemetry-test`: Add `HttpTestBase` to allow tests with actual TCP sockets diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index bd79242989..4f4228f9c7 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -52,5 +52,5 @@ where = src test = [options.entry_points] -opentelemetry_exporter = +opentelemetry_traces_exporter = jaeger_proto = opentelemetry.exporter.jaeger.proto.grpc:JaegerExporter diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index 945150d687..e2c98a4809 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -51,5 +51,5 @@ where = src test = [options.entry_points] -opentelemetry_exporter = - jaeger_thrift = opentelemetry.exporter.jaeger.thrift:JaegerExporter \ No newline at end of file +opentelemetry_traces_exporter = + jaeger_thrift = opentelemetry.exporter.jaeger.thrift:JaegerExporter diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 06e68fded5..e882a65f97 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -44,3 +44,7 @@ install_requires = [options.extras_require] test = + +[options.entry_points] +opentelemetry_traces_exporter = + jaeger = opentelemetry.exporter.jaeger.proto.grpc:JaegerExporter diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 03e82b70f3..1e10bc7b96 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -53,5 +53,5 @@ where = src test = [options.entry_points] -opentelemetry_exporter = - opencensus = opentelemetry.exporter.opencensus.trace_exporter:OpenCensusSpanExporter \ No newline at end of file +opentelemetry_traces_exporter = + opencensus = opentelemetry.exporter.opencensus.trace_exporter:OpenCensusSpanExporter diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 7013425c95..5d144190d3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -54,5 +54,5 @@ test = where = src [options.entry_points] -opentelemetry_exporter = - otlp_proto_grpc_span = opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter +opentelemetry_traces_exporter = + otlp_proto_grpc = opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index 5f6d102d12..38b82f4265 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -53,5 +53,5 @@ test = where = src [options.entry_points] -opentelemetry_exporter = - otlp_proto_http_span = opentelemetry.exporter.otlp.proto.http.trace_exporter:OTLPSpanExporter +opentelemetry_traces_exporter = + otlp_proto_http = opentelemetry.exporter.otlp.proto.http.trace_exporter:OTLPSpanExporter diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 8d24fca242..b971015497 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -39,3 +39,7 @@ python_requires = >=3.6 packages=find_namespace: install_requires = opentelemetry-exporter-otlp-proto-grpc == 1.5.0 + +[options.entry_points] +opentelemetry_traces_exporter = + otlp = opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index 81fcbfa261..2517b52326 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -52,5 +52,5 @@ where = src test = [options.entry_points] -opentelemetry_exporter = - zipkin_json = opentelemetry.exporter.zipkin.json:ZipkinExporter \ No newline at end of file +opentelemetry_traces_exporter = + zipkin_json = opentelemetry.exporter.zipkin.json:ZipkinExporter diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 874ce5a368..a6cdd5b5f8 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -54,5 +54,5 @@ where = src test = [options.entry_points] -opentelemetry_exporter = - zipkin_proto = opentelemetry.exporter.zipkin.proto.http:ZipkinExporter \ No newline at end of file +opentelemetry_traces_exporter = + zipkin_proto = opentelemetry.exporter.zipkin.proto.http:ZipkinExporter diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index bea3c1043d..70e850bfe7 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -43,3 +43,7 @@ install_requires = [options.extras_require] test = + +[options.entry_points] +opentelemetry_traces_exporter = + zipkin = opentelemetry.exporter.zipkin.proto.http:ZipkinExporter diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 160fde86e5..0c699262c6 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -52,8 +52,8 @@ where = src [options.entry_points] opentelemetry_tracer_provider = sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider -opentelemetry_exporter = - console_span = opentelemetry.sdk.trace.export:ConsoleSpanExporter +opentelemetry_traces_exporter = + console = opentelemetry.sdk.trace.export:ConsoleSpanExporter opentelemetry_id_generator = random = opentelemetry.sdk.trace.id_generator:RandomIdGenerator diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 78bb137780..79445a7a35 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -109,7 +109,7 @@ def _import_exporters( exporter_name, exporter_impl, ) in _import_tracer_provider_config_components( - exporter_names, "opentelemetry_exporter" + exporter_names, "opentelemetry_traces_exporter" ): if issubclass(exporter_impl, SpanExporter): trace_exporters[exporter_name] = exporter_impl From e569e0ab6676f8944599418ca0b89d7a2264b6b7 Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Fri, 24 Sep 2021 15:49:55 +0200 Subject: [PATCH 0984/1517] Add OTLP HTTP exporter as dependency to OTLP exporter package (#2147) * Add OTLP HTTP exporter as dependency to OTLP exporter package * changelog --- CHANGELOG.md | 2 ++ exporter/opentelemetry-exporter-otlp/setup.cfg | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e07232d06..0906057435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2101](https://github.com/open-telemetry/opentelemetry-python/pull/2101)) - Fix incorrect headers parsing via environment variables ([#2103](https://github.com/open-telemetry/opentelemetry-python/pull/2103)) +- `opentelemetry-exporter-otlp`: Add `opentelemetry-otlp-proto-http` as dependency +- ([#2147](https://github.com/open-telemetry/opentelemetry-python/pull/2147)) ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index b971015497..88daac2533 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -39,6 +39,7 @@ python_requires = >=3.6 packages=find_namespace: install_requires = opentelemetry-exporter-otlp-proto-grpc == 1.5.0 + opentelemetry-exporter-otlp-proto-http == 1.5.0 [options.entry_points] opentelemetry_traces_exporter = From 3cee4efcc5d0c19ecc0e31ae712ace87528b5808 Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Fri, 24 Sep 2021 16:08:11 +0200 Subject: [PATCH 0985/1517] Fix ReadableSpan import in OTLP grpc exporter (#2146) --- .../exporter/otlp/proto/grpc/trace_exporter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 8fa98feb36..e2c9e6ad89 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -46,7 +46,7 @@ OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) -from opentelemetry.sdk.trace import Span as ReadableSpan +from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace import StatusCode From b2d5ab3175ad7412419b157b0eb351ddcbedf042 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Sat, 25 Sep 2021 06:26:10 +0530 Subject: [PATCH 0986/1517] Exempt resource attributes from span limits (#2138) --- CHANGELOG.md | 4 +++- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 5 ----- opentelemetry-sdk/tests/trace/test_trace.py | 1 - 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0906057435..b2cd6bdc71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2101](https://github.com/open-telemetry/opentelemetry-python/pull/2101)) - Fix incorrect headers parsing via environment variables ([#2103](https://github.com/open-telemetry/opentelemetry-python/pull/2103)) +- Attribute limits no longer apply to Resource attributes + ([#2138](https://github.com/open-telemetry/opentelemetry-python/pull/2138)) - `opentelemetry-exporter-otlp`: Add `opentelemetry-otlp-proto-http` as dependency -- ([#2147](https://github.com/open-telemetry/opentelemetry-python/pull/2147)) + ([#2147](https://github.com/open-telemetry/opentelemetry-python/pull/2147)) ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index ab6df18fdd..e8c1db383d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -1069,11 +1069,6 @@ def __init__( self._span_limits = span_limits or SpanLimits() self._atexit_handler = None - self._resource._attributes = BoundedAttributes( - self._span_limits.max_attributes, - self._resource._attributes, - max_value_len=self._span_limits.max_attribute_length, - ) if shutdown_on_exit: self._atexit_handler = atexit.register(self.shutdown) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 09fcec6a9b..09989a89f8 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -1490,7 +1490,6 @@ def test_dropped_attributes(self): self.assertEqual(3, span.dropped_events) self.assertEqual(2, span.events[0].attributes.dropped) self.assertEqual(2, span.links[0].attributes.dropped) - self.assertEqual(2, span.resource.attributes.dropped) def _test_span_limits( self, From e6d4b9c253db09b0a26e234cedd9f9eb16e2d383 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Sun, 26 Sep 2021 04:51:55 +0530 Subject: [PATCH 0987/1517] Add support for OTEL_ATTRIBUTE_COUNT_LIMIT (#2139) * Add support for OTEL_ATTRIBUTE_COUNT_LIMIT Fixes #2055 Fixes #2111 * Update opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py Co-authored-by: Srikanth Chekuri Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 + .../sdk/environment_variables/__init__.py | 9 ++ .../src/opentelemetry/sdk/trace/__init__.py | 52 ++++++--- opentelemetry-sdk/tests/trace/test_trace.py | 101 ++++++++++++++++-- pyproject.toml | 1 + 5 files changed, 140 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2cd6bdc71..faf23c59dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2101](https://github.com/open-telemetry/opentelemetry-python/pull/2101)) - Fix incorrect headers parsing via environment variables ([#2103](https://github.com/open-telemetry/opentelemetry-python/pull/2103)) +- Add support for OTEL_ATTRIBUTE_COUNT_LIMIT + ([#2139](https://github.com/open-telemetry/opentelemetry-python/pull/2139)) - Attribute limits no longer apply to Resource attributes ([#2138](https://github.com/open-telemetry/opentelemetry-python/pull/2138)) - `opentelemetry-exporter-otlp`: Add `opentelemetry-otlp-proto-http` as dependency diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 11fc5af8cf..8b3d4abbf8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -98,6 +98,15 @@ Default: 512 """ +OTEL_ATTRIBUTE_COUNT_LIMIT = "OTEL_ATTRIBUTE_COUNT_LIMIT" +""" +.. envvar:: OTEL_ATTRIBUTE_COUNT_LIMIT + +The :envvar:`OTEL_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed attribute count for spans, events and links. +This limit is overriden by model specific limits such as OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT. +Default: 128 +""" + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT = "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT" """ .. envvar:: OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index e8c1db383d..431c74be6b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -41,6 +41,7 @@ from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk import util from opentelemetry.sdk.environment_variables import ( + OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, @@ -61,11 +62,12 @@ logger = logging.getLogger(__name__) +_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128 _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = 128 -_DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT = 128 -_DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT = 128 _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT = 128 _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT = 128 +_DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT = 128 +_DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT = 128 _ENV_VALUE_UNSET = "" @@ -533,19 +535,23 @@ class SpanLimits: Limit precedence: - If a model specific limit is set, it will be used. + - Else if the corresponding global limit is set, it will be used. - Else if the model specific limit has a default value, the default value will be used. - - Else if model specific limit has a corresponding global limit, the global limit will be used. + - Else if the global limit has a default value, the default value will be used. Args: - max_attributes: Maximum number of attributes that can be added to a Span. - Environment variable: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT - Default: {_DEFAULT_SPAN_ATTRIBUTE_COUNT_LIMIT} + max_attributes: Maximum number of attributes that can be added to a span, event, and link. + Environment variable: OTEL_ATTRIBUTE_COUNT_LIMIT + Default: {_DEFAULT_ATTRIBUTE_COUNT_LIMIT} max_events: Maximum number of events that can be added to a Span. Environment variable: OTEL_SPAN_EVENT_COUNT_LIMIT Default: {_DEFAULT_SPAN_EVENT_COUNT_LIMIT} max_links: Maximum number of links that can be added to a Span. Environment variable: OTEL_SPAN_LINK_COUNT_LIMIT Default: {_DEFAULT_SPAN_LINK_COUNT_LIMIT} + max_span_attributes: Maximum number of attributes that can be added to a Span. + Environment variable: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT + Default: {_DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT} max_event_attributes: Maximum number of attributes that can be added to an Event. Default: {_DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT} max_link_attributes: Maximum number of attributes that can be added to a Link. @@ -563,16 +569,14 @@ def __init__( max_attributes: Optional[int] = None, max_events: Optional[int] = None, max_links: Optional[int] = None, + max_span_attributes: Optional[int] = None, max_event_attributes: Optional[int] = None, max_link_attributes: Optional[int] = None, max_attribute_length: Optional[int] = None, max_span_attribute_length: Optional[int] = None, ): - self.max_attributes = self._from_env_if_absent( - max_attributes, - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, - _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, - ) + + # span events and links count self.max_events = self._from_env_if_absent( max_events, OTEL_SPAN_EVENT_COUNT_LIMIT, @@ -583,17 +587,32 @@ def __init__( OTEL_SPAN_LINK_COUNT_LIMIT, _DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT, ) + + # attribute count + global_max_attributes = self._from_env_if_absent( + max_attributes, OTEL_ATTRIBUTE_COUNT_LIMIT + ) + self.max_attributes = ( + global_max_attributes or _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT + ) + + self.max_span_attributes = self._from_env_if_absent( + max_span_attributes, + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + global_max_attributes or _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + ) self.max_event_attributes = self._from_env_if_absent( max_event_attributes, OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, - _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, + global_max_attributes or _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, ) self.max_link_attributes = self._from_env_if_absent( max_link_attributes, OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, - _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, + global_max_attributes or _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, ) + # attribute length self.max_attribute_length = self._from_env_if_absent( max_attribute_length, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, @@ -606,7 +625,7 @@ def __init__( ) def __repr__(self): - return f"{type(self).__name__}(max_span_attributes={self.max_span_attribute_length}, max_events_attributes={self.max_event_attributes}, max_link_attributes={self.max_link_attributes}, max_attributes={self.max_attributes}, max_events={self.max_events}, max_links={self.max_links}, max_attribute_length={self.max_attribute_length})" + return f"{type(self).__name__}(max_span_attributes={self.max_span_attributes}, max_events_attributes={self.max_event_attributes}, max_link_attributes={self.max_link_attributes}, max_attributes={self.max_attributes}, max_events={self.max_events}, max_links={self.max_links}, max_attribute_length={self.max_attribute_length})" @classmethod def _from_env_if_absent( @@ -641,13 +660,14 @@ def _from_env_if_absent( max_attributes=SpanLimits.UNSET, max_events=SpanLimits.UNSET, max_links=SpanLimits.UNSET, + max_span_attributes=SpanLimits.UNSET, max_event_attributes=SpanLimits.UNSET, max_link_attributes=SpanLimits.UNSET, max_attribute_length=SpanLimits.UNSET, max_span_attribute_length=SpanLimits.UNSET, ) -# not remove for backward compat. please use SpanLimits instead. +# not removed for backward compat. please use SpanLimits instead. SPAN_ATTRIBUTE_COUNT_LIMIT = SpanLimits._from_env_if_absent( None, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, @@ -717,7 +737,7 @@ def __init__( self._limits = limits self._lock = threading.Lock() self._attributes = BoundedAttributes( - self._limits.max_attributes, + self._limits.max_span_attributes, attributes, immutable=False, max_value_len=self._limits.max_span_attribute_length, diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 09989a89f8..b09550f1df 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -26,7 +26,10 @@ from opentelemetry.context import Context from opentelemetry.sdk import resources, trace from opentelemetry.sdk.environment_variables import ( + OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, + OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, + OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT, @@ -552,7 +555,7 @@ def test_surplus_span_links(self): def test_surplus_span_attributes(self): # pylint: disable=protected-access - max_attrs = trace.SpanLimits().max_attributes + max_attrs = trace.SpanLimits().max_span_attributes attributes = {str(idx): idx for idx in range(0, 16 + max_attrs)} tracer = new_tracer() with tracer.start_as_current_span( @@ -1325,8 +1328,20 @@ def test_limits_defaults(self): limits = trace.SpanLimits() self.assertEqual( limits.max_attributes, + trace._DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT, + ) + self.assertEqual( + limits.max_span_attributes, trace._DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, ) + self.assertEqual( + limits.max_event_attributes, + trace._DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, + ) + self.assertEqual( + limits.max_link_attributes, + trace._DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, + ) self.assertEqual( limits.max_events, trace._DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT ) @@ -1355,25 +1370,61 @@ def test_limits_attribute_length_limits_code(self): self.assertEqual(limits.max_span_attribute_length, 33) def test_limits_values_code(self): - max_attributes, max_events, max_links, max_attr_length = ( + ( + max_attributes, + max_span_attributes, + max_link_attributes, + max_event_attributes, + max_events, + max_links, + max_attr_length, + max_span_attr_length, + ) = ( + randint(0, 10000), + randint(0, 10000), + randint(0, 10000), + randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), ) limits = trace.SpanLimits( - max_attributes=max_attributes, max_events=max_events, max_links=max_links, + max_attributes=max_attributes, + max_span_attributes=max_span_attributes, + max_event_attributes=max_event_attributes, + max_link_attributes=max_link_attributes, max_attribute_length=max_attr_length, + max_span_attribute_length=max_span_attr_length, ) - self.assertEqual(limits.max_attributes, max_attributes) self.assertEqual(limits.max_events, max_events) self.assertEqual(limits.max_links, max_links) + self.assertEqual(limits.max_attributes, max_attributes) + self.assertEqual(limits.max_span_attributes, max_span_attributes) + self.assertEqual(limits.max_event_attributes, max_event_attributes) + self.assertEqual(limits.max_link_attributes, max_link_attributes) self.assertEqual(limits.max_attribute_length, max_attr_length) + self.assertEqual( + limits.max_span_attribute_length, max_span_attr_length + ) def test_limits_values_env(self): - max_attributes, max_events, max_links, max_attr_length = ( + ( + max_attributes, + max_span_attributes, + max_link_attributes, + max_event_attributes, + max_events, + max_links, + max_attr_length, + max_span_attr_length, + ) = ( + randint(0, 10000), + randint(0, 10000), + randint(0, 10000), + randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), @@ -1382,16 +1433,29 @@ def test_limits_values_env(self): with mock.patch.dict( "os.environ", { - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: str(max_attributes), + OTEL_ATTRIBUTE_COUNT_LIMIT: str(max_attributes), + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: str(max_span_attributes), + OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT: str(max_event_attributes), + OTEL_LINK_ATTRIBUTE_COUNT_LIMIT: str(max_link_attributes), OTEL_SPAN_EVENT_COUNT_LIMIT: str(max_events), OTEL_SPAN_LINK_COUNT_LIMIT: str(max_links), - OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: str(max_attr_length), + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: str(max_attr_length), + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: str( + max_span_attr_length + ), }, ): limits = trace.SpanLimits() - self.assertEqual(limits.max_attributes, max_attributes) self.assertEqual(limits.max_events, max_events) self.assertEqual(limits.max_links, max_links) + self.assertEqual(limits.max_attributes, max_attributes) + self.assertEqual(limits.max_span_attributes, max_span_attributes) + self.assertEqual(limits.max_event_attributes, max_event_attributes) + self.assertEqual(limits.max_link_attributes, max_link_attributes) + self.assertEqual(limits.max_attribute_length, max_attr_length) + self.assertEqual( + limits.max_span_attribute_length, max_span_attr_length + ) @mock.patch.dict( "os.environ", @@ -1413,6 +1477,25 @@ def test_span_limits_env(self): max_span_attr_len=15, ) + @mock.patch.dict( + "os.environ", + { + OTEL_ATTRIBUTE_COUNT_LIMIT: "13", + OTEL_SPAN_EVENT_COUNT_LIMIT: "7", + OTEL_SPAN_LINK_COUNT_LIMIT: "4", + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: "11", + }, + ) + def test_span_limits_global_env(self): + self._test_span_limits( + new_tracer(), + max_attrs=13, + max_events=7, + max_links=4, + max_attr_len=11, + max_span_attr_len=11, + ) + @mock.patch.dict( "os.environ", { @@ -1475,7 +1558,7 @@ def test_span_no_limits_code(self): self._test_span_no_limits( new_tracer( span_limits=trace.SpanLimits( - max_attributes=trace.SpanLimits.UNSET, + max_span_attributes=trace.SpanLimits.UNSET, max_links=trace.SpanLimits.UNSET, max_events=trace.SpanLimits.UNSET, max_attribute_length=trace.SpanLimits.UNSET, diff --git a/pyproject.toml b/pyproject.toml index b43fb8b66e..eec7dacdcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ exclude = ''' ( /( # generated files .tox| + venv| exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen| exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen| exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen| From b772c0ffeaaf649b553ae22ec3b16019ad0e7df0 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Mon, 27 Sep 2021 21:19:57 +0530 Subject: [PATCH 0988/1517] Fix slow batch processor test for pypy + windows (#2155) * wip * test --- opentelemetry-sdk/tests/trace/export/test_export.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index c698f4d6e6..2e4672af26 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -364,19 +364,19 @@ def test_batch_span_processor_scheduled_delay(self): my_exporter = MySpanExporter( destination=spans_names_list, export_event=export_event ) + start_time = time.time() span_processor = export.BatchSpanProcessor( my_exporter, - schedule_delay_millis=50, + schedule_delay_millis=500, ) # create single span - start_time = time.time() _create_start_and_end_span("foo", span_processor) self.assertTrue(export_event.wait(2)) export_time = time.time() self.assertEqual(len(spans_names_list), 1) - self.assertGreaterEqual((export_time - start_time) * 1e3, 50) + self.assertGreaterEqual((export_time - start_time) * 1e3, 500) span_processor.shutdown() From a462f554bb7bc2d20a3780ce663f2c4904480a36 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 27 Sep 2021 09:09:22 -0700 Subject: [PATCH 0989/1517] update to pylint 2.11.0 (#2119) * test pylint 2.11.0 * address last few pylint issues * fix mypy Co-authored-by: Srikanth Chekuri --- dev-requirements.txt | 2 +- opentelemetry-api/src/opentelemetry/util/_time.py | 2 +- opentelemetry-api/tests/trace/test_proxy.py | 6 ++++-- .../src/opentelemetry/instrumentation/bootstrap.py | 8 ++++---- .../tests/test_b3_format.py | 4 ++-- .../test_listener_per_request/test_asyncio.py | 13 +++++++------ 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index ec40008979..bd369fac76 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,4 @@ -pylint==2.7.1 +pylint==2.11.0 flake8~=3.7 isort~=5.8 black~=21.7b0 diff --git a/opentelemetry-api/src/opentelemetry/util/_time.py b/opentelemetry-api/src/opentelemetry/util/_time.py index dd1371d177..ceaca22e8d 100644 --- a/opentelemetry-api/src/opentelemetry/util/_time.py +++ b/opentelemetry-api/src/opentelemetry/util/_time.py @@ -17,7 +17,7 @@ if version_info.minor < 7: getLogger(__name__).warning( # pylint: disable=logging-not-lazy - "You are using Python 3.%s. This version does not support timestamps " + "You are using Python 3.%s. This version does not support timestamps " # pylint: disable=C0209 "with nanosecond precision and the OpenTelemetry SDK will use " "millisecond precision instead. Please refer to PEP 564 for more " "information. Please upgrade to Python 3.7 or newer to use nanosecond " diff --git a/opentelemetry-api/tests/trace/test_proxy.py b/opentelemetry-api/tests/trace/test_proxy.py index 57759676fe..3e72eadf1a 100644 --- a/opentelemetry-api/tests/trace/test_proxy.py +++ b/opentelemetry-api/tests/trace/test_proxy.py @@ -22,8 +22,10 @@ class TestProvider(trace._DefaultTracerProvider): def get_tracer( - self, instrumentation_module_name, instrumentaiton_library_version=None - ): + self, + instrumenting_module_name: str, + instrumenting_library_version: str = "", + ) -> trace.Tracer: return TestTracer() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 7fe8de0ef0..f1c8181bae 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -71,11 +71,11 @@ def _pip_check(): 'opentelemetry-instrumentation-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' To not be too restrictive, we'll only check for relevant packages. """ - check_pipe = subprocess.Popen( + with subprocess.Popen( [sys.executable, "-m", "pip", "check"], stdout=subprocess.PIPE - ) - pip_check = check_pipe.communicate()[0].decode() - pip_check_lower = pip_check.lower() + ) as check_pipe: + pip_check = check_pipe.communicate()[0].decode() + pip_check_lower = pip_check.lower() for package_tup in libraries.values(): for package in package_tup: if package.lower() in pip_check_lower: diff --git a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index 4068e2060a..f8c9b0e882 100644 --- a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -16,8 +16,6 @@ from abc import abstractmethod from unittest.mock import Mock -import opentelemetry.sdk.trace as trace -import opentelemetry.sdk.trace.id_generator as id_generator import opentelemetry.trace as trace_api from opentelemetry.context import Context, get_current from opentelemetry.propagators.b3 import ( # pylint: disable=no-name-in-module,import-error @@ -25,6 +23,8 @@ B3SingleFormat, ) from opentelemetry.propagators.textmap import DefaultGetter +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace import id_generator from opentelemetry.trace.propagation import _SPAN_KEY diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py index 35153cb627..ea8690a72f 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py @@ -10,22 +10,23 @@ from .response_listener import ResponseListener +async def task(message, listener): + res = f"{message}::response" + listener.on_response(res) + return res + + class Client: def __init__(self, tracer, loop): self.tracer = tracer self.loop = loop - async def task(self, message, listener): - res = f"{message}::response" - listener.on_response(res) - return res - def send_sync(self, message): span = self.tracer.start_span("send") span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) listener = ResponseListener(span) - return self.loop.run_until_complete(self.task(message, listener)) + return self.loop.run_until_complete(task(message, listener)) class TestAsyncio(OpenTelemetryTestCase): From b59e91472cb178a7fa94e243f3f0952aa720feee Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Wed, 29 Sep 2021 08:24:22 +0200 Subject: [PATCH 0990/1517] Fix validity calculation for trace/span ID (#2145) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 + .../src/opentelemetry/trace/span.py | 8 ++-- .../tests/trace/test_span_context.py | 44 +++++++++++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0377126522..795a288f2e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: dde62cebffe519c35875af6d06fae053b3be65ec + CONTRIB_REPO_SHA: d2984f5242ed2250ad1c11b6164e2e8e11e2a804 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index faf23c59dc..b47cfdeb65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2138](https://github.com/open-telemetry/opentelemetry-python/pull/2138)) - `opentelemetry-exporter-otlp`: Add `opentelemetry-otlp-proto-http` as dependency ([#2147](https://github.com/open-telemetry/opentelemetry-python/pull/2147)) +- Fix validity calculation for trace and span IDs + ([#2145](https://github.com/open-telemetry/opentelemetry-python/pull/2145)) ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 239fbfd3af..23eac7337e 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -387,7 +387,8 @@ def values(self) -> typing.ValuesView[str]: DEFAULT_TRACE_STATE = TraceState.get_default() -_TRACE_ID_HEX_LENGTH = 2 ** 128 - 1 +_TRACE_ID_MAX_VALUE = 2 ** 128 - 1 +_SPAN_ID_MAX_VALUE = 2 ** 64 - 1 class SpanContext( @@ -420,9 +421,8 @@ def __new__( trace_state = DEFAULT_TRACE_STATE is_valid = ( - trace_id != INVALID_TRACE_ID - and span_id != INVALID_SPAN_ID - and trace_id < _TRACE_ID_HEX_LENGTH + INVALID_TRACE_ID < trace_id <= _TRACE_ID_MAX_VALUE + and INVALID_SPAN_ID < span_id <= _SPAN_ID_MAX_VALUE ) return tuple.__new__( diff --git a/opentelemetry-api/tests/trace/test_span_context.py b/opentelemetry-api/tests/trace/test_span_context.py index 1ec32253c3..55abb0f559 100644 --- a/opentelemetry-api/tests/trace/test_span_context.py +++ b/opentelemetry-api/tests/trace/test_span_context.py @@ -43,3 +43,47 @@ def test_span_context_pickle(self): trace_state=trace.DEFAULT_TRACE_STATE, ) self.assertFalse(invalid_sc.is_valid) + + def test_trace_id_validity(self): + trace_id_max_value = int("f" * 32, 16) + span_id = 1 + + # valid trace IDs + sc = trace.SpanContext(trace_id_max_value, span_id, is_remote=False) + self.assertTrue(sc.is_valid) + + sc = trace.SpanContext(1, span_id, is_remote=False) + self.assertTrue(sc.is_valid) + + # invalid trace IDs + sc = trace.SpanContext(0, span_id, is_remote=False) + self.assertFalse(sc.is_valid) + + sc = trace.SpanContext(-1, span_id, is_remote=False) + self.assertFalse(sc.is_valid) + + sc = trace.SpanContext( + trace_id_max_value + 1, span_id, is_remote=False + ) + self.assertFalse(sc.is_valid) + + def test_span_id_validity(self): + span_id_max = int("f" * 16, 16) + trace_id = 1 + + # valid span IDs + sc = trace.SpanContext(trace_id, span_id_max, is_remote=False) + self.assertTrue(sc.is_valid) + + sc = trace.SpanContext(trace_id, 1, is_remote=False) + self.assertTrue(sc.is_valid) + + # invalid span IDs + sc = trace.SpanContext(trace_id, 0, is_remote=False) + self.assertFalse(sc.is_valid) + + sc = trace.SpanContext(trace_id, -1, is_remote=False) + self.assertFalse(sc.is_valid) + + sc = trace.SpanContext(trace_id, span_id_max + 1, is_remote=False) + self.assertFalse(sc.is_valid) From 82a5be2e294ebcf6490b67d631e4070d65d7b66d Mon Sep 17 00:00:00 2001 From: James <45812677+JamesJHPark@users.noreply.github.com> Date: Wed, 29 Sep 2021 07:10:43 -0700 Subject: [PATCH 0991/1517] add `schema_url` to `TracerProvider.get_tracer` (#2154) * add schema_url to TracerProvider.get_tracer * update CHANGELOG * fix CHANGELOG * modify tests and optional parameter in InstrumentationInfo * fixed syntax * update instrumentation_library_version typed Optional * resolve syntax error * fix lint failures * resolve lint failure --- CHANGELOG.md | 2 + .../src/opentelemetry/trace/__init__.py | 30 +++++++++++---- opentelemetry-api/tests/trace/test_globals.py | 6 ++- opentelemetry-api/tests/trace/test_proxy.py | 5 ++- .../src/opentelemetry/sdk/trace/__init__.py | 10 ++++- .../opentelemetry/sdk/util/instrumentation.py | 38 ++++++++++++++----- opentelemetry-sdk/tests/trace/test_trace.py | 9 ++++- 7 files changed, 75 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b47cfdeb65..2d9a92b2e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2147](https://github.com/open-telemetry/opentelemetry-python/pull/2147)) - Fix validity calculation for trace and span IDs ([#2145](https://github.com/open-telemetry/opentelemetry-python/pull/2145)) +- Add `schema_url` to `TracerProvider.get_tracer` + ([#2154](https://github.com/open-telemetry/opentelemetry-python/pull/2154)) ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 482d43a044..24c42b04c6 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -75,6 +75,7 @@ import os +import typing from abc import ABC, abstractmethod from contextlib import contextmanager from enum import Enum @@ -186,7 +187,8 @@ class TracerProvider(ABC): def get_tracer( self, instrumenting_module_name: str, - instrumenting_library_version: str = "", + instrumenting_library_version: typing.Optional[str] = None, + schema_url: typing.Optional[str] = None, ) -> "Tracer": """Returns a `Tracer` for use by the given instrumentation library. @@ -208,6 +210,8 @@ def get_tracer( instrumenting_library_version: Optional. The version string of the instrumenting library. Usually this should be the same as ``pkg_resources.get_distribution(instrumenting_library_name).version``. + + schema_url: Optional. Specifies the Schema URL of the emitted telemetry. """ @@ -220,7 +224,8 @@ class _DefaultTracerProvider(TracerProvider): def get_tracer( self, instrumenting_module_name: str, - instrumenting_library_version: str = "", + instrumenting_library_version: typing.Optional[str] = None, + schema_url: typing.Optional[str] = None, ) -> "Tracer": # pylint:disable=no-self-use,unused-argument return _DefaultTracer() @@ -230,14 +235,19 @@ class ProxyTracerProvider(TracerProvider): def get_tracer( self, instrumenting_module_name: str, - instrumenting_library_version: str = "", + instrumenting_library_version: typing.Optional[str] = None, + schema_url: typing.Optional[str] = None, ) -> "Tracer": if _TRACER_PROVIDER: return _TRACER_PROVIDER.get_tracer( - instrumenting_module_name, instrumenting_library_version + instrumenting_module_name, + instrumenting_library_version, + schema_url, ) return ProxyTracer( - instrumenting_module_name, instrumenting_library_version + instrumenting_module_name, + instrumenting_library_version, + schema_url, ) @@ -375,10 +385,12 @@ class ProxyTracer(Tracer): def __init__( self, instrumenting_module_name: str, - instrumenting_library_version: str, + instrumenting_library_version: typing.Optional[str] = None, + schema_url: typing.Optional[str] = None, ): self._instrumenting_module_name = instrumenting_module_name self._instrumenting_library_version = instrumenting_library_version + self._schema_url = schema_url self._real_tracer: Optional[Tracer] = None self._noop_tracer = _DefaultTracer() @@ -391,6 +403,7 @@ def _tracer(self) -> Tracer: self._real_tracer = _TRACER_PROVIDER.get_tracer( self._instrumenting_module_name, self._instrumenting_library_version, + self._schema_url, ) return self._real_tracer return self._noop_tracer @@ -445,8 +458,9 @@ def start_as_current_span( def get_tracer( instrumenting_module_name: str, - instrumenting_library_version: str = "", + instrumenting_library_version: typing.Optional[str] = None, tracer_provider: Optional[TracerProvider] = None, + schema_url: typing.Optional[str] = None, ) -> "Tracer": """Returns a `Tracer` for use by the given instrumentation library. @@ -458,7 +472,7 @@ def get_tracer( if tracer_provider is None: tracer_provider = get_tracer_provider() return tracer_provider.get_tracer( - instrumenting_module_name, instrumenting_library_version + instrumenting_module_name, instrumenting_library_version, schema_url ) diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index a297d054ef..034a97e4de 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -36,10 +36,12 @@ def tearDown(self) -> None: def test_get_tracer(self): """trace.get_tracer should proxy to the global tracer provider.""" trace.get_tracer("foo", "var") - self._mock_tracer_provider.get_tracer.assert_called_with("foo", "var") + self._mock_tracer_provider.get_tracer.assert_called_with( + "foo", "var", None + ) mock_provider = unittest.mock.Mock() trace.get_tracer("foo", "var", mock_provider) - mock_provider.get_tracer.assert_called_with("foo", "var") + mock_provider.get_tracer.assert_called_with("foo", "var", None) class TestTracer(unittest.TestCase): diff --git a/opentelemetry-api/tests/trace/test_proxy.py b/opentelemetry-api/tests/trace/test_proxy.py index 3e72eadf1a..42a31b4132 100644 --- a/opentelemetry-api/tests/trace/test_proxy.py +++ b/opentelemetry-api/tests/trace/test_proxy.py @@ -13,7 +13,7 @@ # limitations under the License. # pylint: disable=W0212,W0222,W0221 - +import typing import unittest from opentelemetry import trace @@ -24,7 +24,8 @@ class TestProvider(trace._DefaultTracerProvider): def get_tracer( self, instrumenting_module_name: str, - instrumenting_library_version: str = "", + instrumenting_library_version: typing.Optional[str] = None, + schema_url: typing.Optional[str] = None, ) -> trace.Tracer: return TestTracer() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 431c74be6b..38ecf1c316 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -20,6 +20,7 @@ import logging import threading import traceback +import typing from collections import OrderedDict from contextlib import contextmanager from os import environ @@ -1099,18 +1100,23 @@ def resource(self) -> Resource: def get_tracer( self, instrumenting_module_name: str, - instrumenting_library_version: str = "", + instrumenting_library_version: typing.Optional[str] = None, + schema_url: typing.Optional[str] = None, ) -> "trace_api.Tracer": if not instrumenting_module_name: # Reject empty strings too. instrumenting_module_name = "" logger.error("get_tracer called with missing module name.") + if instrumenting_library_version is None: + instrumenting_library_version = "" return Tracer( self.sampler, self.resource, self._active_span_processor, self.id_generator, InstrumentationInfo( - instrumenting_module_name, instrumenting_library_version + instrumenting_module_name, + instrumenting_library_version, + schema_url, ), self._span_limits, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index 2c3dba4923..7f565ba8ea 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import typing class InstrumentationInfo: @@ -20,31 +21,50 @@ class InstrumentationInfo: properties. """ - __slots__ = ("_name", "_version") + __slots__ = ("_name", "_version", "_schema_url") - def __init__(self, name: str, version: str): + def __init__( + self, + name: str, + version: typing.Optional[str] = None, + schema_url: typing.Optional[str] = None, + ): self._name = name self._version = version + self._schema_url = schema_url def __repr__(self): - return f"{type(self).__name__}({self._name}, {self._version})" + return f"{type(self).__name__}({self._name}, {self._version}, {self._schema_url})" def __hash__(self): - return hash((self._name, self._version)) + return hash((self._name, self._version, self._schema_url)) def __eq__(self, value): - return type(value) is type(self) and (self._name, self._version) == ( - value._name, - value._version, + return ( + type(value) is type(self) + and ( + self._name, + self._version, + self._schema_url, + ) + == (value._name, value._version, value._schema_url) ) def __lt__(self, value): if type(value) is not type(self): return NotImplemented - return (self._name, self._version) < (value._name, value._version) + return (self._name, self._version, self._schema_url) < ( + value._name, + value._version, + value._schema_url, + ) + + @property + def schema_url(self) -> typing.Optional[str]: + return self._schema_url @property - def version(self) -> str: + def version(self) -> typing.Optional[str]: return self._version @property diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index b09550f1df..0a5c2d349c 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -237,17 +237,20 @@ def test_start_span_invalid_spancontext(self): def test_instrumentation_info(self): tracer_provider = trace.TracerProvider() + schema_url = "https://opentelemetry.io/schemas/1.3.0" tracer1 = tracer_provider.get_tracer("instr1") - tracer2 = tracer_provider.get_tracer("instr2", "1.3b3") + tracer2 = tracer_provider.get_tracer("instr2", "1.3b3", schema_url) span1 = tracer1.start_span("s1") span2 = tracer2.start_span("s2") self.assertEqual( span1.instrumentation_info, InstrumentationInfo("instr1", "") ) self.assertEqual( - span2.instrumentation_info, InstrumentationInfo("instr2", "1.3b3") + span2.instrumentation_info, + InstrumentationInfo("instr2", "1.3b3", schema_url), ) + self.assertEqual(span2.instrumentation_info.schema_url, schema_url) self.assertEqual(span2.instrumentation_info.version, "1.3b3") self.assertEqual(span2.instrumentation_info.name, "instr2") @@ -267,6 +270,7 @@ def test_invalid_instrumentation_info(self): ) span1 = tracer1.start_span("foo") self.assertTrue(span1.is_recording()) + self.assertEqual(tracer1.instrumentation_info.schema_url, None) self.assertEqual(tracer1.instrumentation_info.version, "") self.assertEqual(tracer1.instrumentation_info.name, "") @@ -275,6 +279,7 @@ def test_invalid_instrumentation_info(self): ) span2 = tracer2.start_span("bar") self.assertTrue(span2.is_recording()) + self.assertEqual(tracer2.instrumentation_info.schema_url, None) self.assertEqual(tracer2.instrumentation_info.version, "") self.assertEqual(tracer2.instrumentation_info.name, "") From 93e53993e73834772c90a85d3963eb11021886cc Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 29 Sep 2021 11:30:47 -0700 Subject: [PATCH 0992/1517] add assertTraceResponseHeaderMatchesSpan method (#2159) * add assertTraceResponseHeaderMatchesSpan method * fix lint Co-authored-by: Diego Hurtado --- .../util/src/opentelemetry/test/wsgitestutil.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/util/src/opentelemetry/test/wsgitestutil.py b/tests/util/src/opentelemetry/test/wsgitestutil.py index 349db8f694..aa2379a2b7 100644 --- a/tests/util/src/opentelemetry/test/wsgitestutil.py +++ b/tests/util/src/opentelemetry/test/wsgitestutil.py @@ -15,6 +15,7 @@ import io import wsgiref.util as wsgiref_util +from opentelemetry import trace from opentelemetry.test.spantestutil import SpanTestBase @@ -37,3 +38,19 @@ def start_response(self, status, response_headers, exc_info=None): self.response_headers = response_headers self.exc_info = exc_info return self.write + + def assertTraceResponseHeaderMatchesSpan( + self, headers, span + ): # pylint: disable=invalid-name + self.assertIn("traceresponse", headers) + self.assertEqual( + headers["access-control-expose-headers"], + "traceresponse", + ) + + trace_id = trace.format_trace_id(span.get_span_context().trace_id) + span_id = trace.format_span_id(span.get_span_context().span_id) + self.assertEqual( + f"00-{trace_id}-{span_id}-01", + headers["traceresponse"], + ) From b20a6c5095717981ffba11d8244aa10a8f22890a Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 29 Sep 2021 12:39:03 -0700 Subject: [PATCH 0993/1517] updating bootstrap_gen (#2158) Co-authored-by: Diego Hurtado --- .../src/opentelemetry/instrumentation/bootstrap_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index e400a3417b..06bad91a22 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -53,7 +53,7 @@ "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.24b0", }, "falcon": { - "library": "falcon ~= 2.0", + "library": "falcon >= 2.0.0, < 4.0.0", "instrumentation": "opentelemetry-instrumentation-falcon==0.24b0", }, "fastapi": { From 18b5cb046bebab0c70841c662268bb7980c2fd5e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 29 Sep 2021 23:28:56 +0200 Subject: [PATCH 0994/1517] Load environment variables as options for opentelemetry-instrument (#1969) --- CHANGELOG.md | 3 +- CONTRIBUTING.md | 10 +++ opentelemetry-api/setup.cfg | 2 + .../__init__.py => environment_variables.py} | 5 -- .../src/opentelemetry/distro/__init__.py | 2 +- opentelemetry-instrumentation/setup.cfg | 2 + .../auto_instrumentation/__init__.py | 80 ++++++++++--------- .../auto_instrumentation/sitecustomize.py | 6 +- .../instrumentation/environment_variables.py | 18 +++++ .../tests/test_run.py | 3 +- opentelemetry-sdk/setup.cfg | 2 + .../__init__.py => environment_variables.py} | 0 12 files changed, 83 insertions(+), 50 deletions(-) rename opentelemetry-api/src/opentelemetry/{environment_variables/__init__.py => environment_variables.py} (88%) create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/environment_variables.py rename opentelemetry-sdk/src/opentelemetry/sdk/{environment_variables/__init__.py => environment_variables.py} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d9a92b2e4..835601d58c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.5.0-0.24b0...HEAD) - +- Automatically load OTEL environment variables as options for `opentelemetry-instrument` + ([#1969](https://github.com/open-telemetry/opentelemetry-python/pull/1969)) - `opentelemetry-semantic-conventions` Update to semantic conventions v1.6.1 ([#2077](https://github.com/open-telemetry/opentelemetry-python/pull/2077)) - Do not count invalid attributes for dropped diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7084a8fbca..ac76ccc70d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -214,6 +214,16 @@ rather than conform to specific API names or argument patterns in the spec. For a deeper discussion, see: https://github.com/open-telemetry/opentelemetry-specification/issues/165 +### Environment Variables + +If you are adding a component that introduces new OpenTelemetry environment variables, put them all in a module, +as it is done in `opentelemetry.environment_variables` or in `opentelemetry.sdk.environment_variables`. + +Keep in mind that any new environment variable must be declared in all caps and must start with `OTEL_PYTHON_`. + +Register this module with the `opentelemetry_environment_variables` entry point to make your environment variables +automatically load as options for the `opentelemetry-instrument` command. + ## Style Guide * docstrings should adhere to the [Google Python Style diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 99d52838fb..91a38ac927 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -56,6 +56,8 @@ opentelemetry_tracer_provider = opentelemetry_propagator = tracecontext = opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator baggage = opentelemetry.baggage.propagation:W3CBaggagePropagator +opentelemetry_environment_variables = + api = opentelemetry.environment_variables [options.extras_require] test = diff --git a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py b/opentelemetry-api/src/opentelemetry/environment_variables.py similarity index 88% rename from opentelemetry-api/src/opentelemetry/environment_variables/__init__.py rename to opentelemetry-api/src/opentelemetry/environment_variables.py index 6077115b01..83ec67149d 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables.py @@ -22,11 +22,6 @@ .. envvar:: OTEL_PYTHON_CONTEXT """ -OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS" -""" -.. envvar:: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS -""" - OTEL_PYTHON_ID_GENERATOR = "OTEL_PYTHON_ID_GENERATOR" """ .. envvar:: OTEL_PYTHON_ID_GENERATOR diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index e70cb67335..97e3e2fcc9 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# + import os from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg index 6ecd9d75e6..042f0edb19 100644 --- a/opentelemetry-instrumentation/setup.cfg +++ b/opentelemetry-instrumentation/setup.cfg @@ -51,6 +51,8 @@ where = src console_scripts = opentelemetry-instrument = opentelemetry.instrumentation.auto_instrumentation:run opentelemetry-bootstrap = opentelemetry.instrumentation.bootstrap:run +opentelemetry_environment_variables = + instrumentation = opentelemetry.instrumentation.environment_variables [options.extras_require] test = diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index 9f076b340e..29b09a0c34 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -14,72 +14,74 @@ # See the License for the specific language governing permissions and # limitations under the License. -import argparse +from argparse import REMAINDER, ArgumentParser from logging import getLogger from os import environ, execl, getcwd from os.path import abspath, dirname, pathsep +from re import sub from shutil import which -from opentelemetry.environment_variables import ( - OTEL_PYTHON_ID_GENERATOR, - OTEL_TRACES_EXPORTER, -) +from pkg_resources import iter_entry_points -logger = getLogger(__file__) +_logger = getLogger(__file__) -def parse_args(): - parser = argparse.ArgumentParser( +def run() -> None: + + parser = ArgumentParser( description=""" opentelemetry-instrument automatically instruments a Python program and its dependencies and then runs the program. - """ + """, + epilog=""" + Optional arguments (except for --help) for opentelemetry-instrument + directly correspond with OpenTelemetry environment variables. The + corresponding optional argument is formed by removing the OTEL_ or + OTEL_PYTHON_ prefix from the environment variable and lower casing the + rest. For example, the optional argument --attribute_value_length_limit + corresponds with the environment variable + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT. + + These optional arguments will override the current value of the + corresponding environment variable during the execution of the command. + """, ) - parser.add_argument( - "--trace-exporter", - required=False, - help=""" - Uses the specified exporter to export spans. - Accepts multiple exporters as comma separated values. + argument_otel_environment_variable = {} - Examples: + for entry_point in iter_entry_points( + "opentelemetry_environment_variables" + ): + environment_variable_module = entry_point.load() - --trace-exporter=jaeger - """, - ) + for attribute in dir(environment_variable_module): - parser.add_argument( - "--id-generator", - required=False, - help=""" - The IDs Generator to be used with the Tracer Provider. + if attribute.startswith("OTEL_"): - Examples: + argument = sub(r"OTEL_(PYTHON_)?", "", attribute).lower() - --id-generator=random - """, - ) + parser.add_argument( + f"--{argument}", + required=False, + ) + argument_otel_environment_variable[argument] = attribute parser.add_argument("command", help="Your Python application.") parser.add_argument( "command_args", help="Arguments for your application.", - nargs=argparse.REMAINDER, + nargs=REMAINDER, ) - return parser.parse_args() - -def load_config_from_cli_args(args): - if args.trace_exporter: - environ[OTEL_TRACES_EXPORTER] = args.trace_exporter - if args.id_generator: - environ[OTEL_PYTHON_ID_GENERATOR] = args.id_generator + args = parser.parse_args() + for argument, otel_environment_variable in ( + argument_otel_environment_variable + ).items(): + value = getattr(args, argument) + if value is not None: -def run() -> None: - args = parse_args() - load_config_from_cli_args(args) + environ[otel_environment_variable] = value python_path = environ.get("PYTHONPATH") diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index a4a5dc8d5a..f7a6412ff6 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -20,13 +20,13 @@ from pkg_resources import iter_entry_points -from opentelemetry.environment_variables import ( - OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, -) from opentelemetry.instrumentation.dependencies import ( get_dist_dependency_conflicts, ) from opentelemetry.instrumentation.distro import BaseDistro, DefaultDistro +from opentelemetry.instrumentation.environment_variables import ( + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, +) logger = getLogger(__file__) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/environment_variables.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/environment_variables.py new file mode 100644 index 0000000000..ad28f06859 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/environment_variables.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS" +""" +.. envvar:: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS +""" diff --git a/opentelemetry-instrumentation/tests/test_run.py b/opentelemetry-instrumentation/tests/test_run.py index 01bd86ed32..9fd3a21711 100644 --- a/opentelemetry-instrumentation/tests/test_run.py +++ b/opentelemetry-instrumentation/tests/test_run.py @@ -111,7 +111,8 @@ def test_exporter(self, _): # pylint: disable=no-self-use self.assertIsNone(environ.get(OTEL_TRACES_EXPORTER)) with patch( - "sys.argv", ["instrument", "--trace-exporter", "jaeger", "1", "2"] + "sys.argv", + ["instrument", "--traces_exporter", "jaeger", "1", "2"], ): auto_instrumentation.run() self.assertEqual(environ.get(OTEL_TRACES_EXPORTER), "jaeger") diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 0c699262c6..56390868ed 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -56,6 +56,8 @@ opentelemetry_traces_exporter = console = opentelemetry.sdk.trace.export:ConsoleSpanExporter opentelemetry_id_generator = random = opentelemetry.sdk.trace.id_generator:RandomIdGenerator +opentelemetry_environment_variables = + sdk = opentelemetry.sdk.environment_variables [options.extras_require] test = diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py similarity index 100% rename from opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py rename to opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py From 9554bbc4c8e60c7bba16a7f07b2f9fadc9e75d36 Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Mon, 4 Oct 2021 15:51:55 -0400 Subject: [PATCH 0995/1517] Delete docs-update.yml (#2171) --- .github/workflows/docs-update.yml | 35 ------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 .github/workflows/docs-update.yml diff --git a/.github/workflows/docs-update.yml b/.github/workflows/docs-update.yml deleted file mode 100644 index 7892717776..0000000000 --- a/.github/workflows/docs-update.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Update OpenTelemetry Website Docs - -on: - # triggers only on a manual dispatch - workflow_dispatch: - -jobs: - update-docs: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v2 - - name: make-pr - env: - API_TOKEN_GITHUB: ${{secrets.DOC_UPDATE_TOKEN}} - # Destination repo should always be 'open-telemetry/opentelemetry.io' - DESTINATION_REPO: open-telemetry/opentelemetry.io - # Destination path should be the absolute path to your language's friendly name in the docs tree (i.e, 'content/en/docs/python') - DESTINATION_PATH: content/en/docs/python - # Source path should be 'website_docs', all files and folders are copied from here to dest - SOURCE_PATH: website_docs - run: | - TARGET_DIR=$(mktemp -d) - export GITHUB_TOKEN=$API_TOKEN_GITHUB - git config --global user.name austinlparker - git config --global user.email austin@lightstep.com - git clone "https://$API_TOKEN_GITHUB@github.com/$DESTINATION_REPO.git" "$TARGET_DIR" - rsync -av --delete "$SOURCE_PATH/" "$TARGET_DIR/$DESTINATION_PATH/" - cd "$TARGET_DIR" - git checkout -b docs-$GITHUB_REPOSITORY-$GITHUB_SHA - git add . - git commit -m "Docs update from $GITHUB_REPOSITORY" - git push -u origin HEAD:docs-$GITHUB_REPOSITORY-$GITHUB_SHA - gh pr create -t "Docs Update from $GITHUB_REPOSITORY" -b "This is an automated pull request." -B main -H docs-$GITHUB_REPOSITORY-$GITHUB_SHA - echo "done" From c9b18c66229da61065846ba9ace25a5f5c2106b8 Mon Sep 17 00:00:00 2001 From: Nikolay Sokolik <81902191+oxeye-nikolay@users.noreply.github.com> Date: Tue, 5 Oct 2021 14:55:44 +0300 Subject: [PATCH 0996/1517] Updated bootstrap_gen.py for pika instrumentation in the contrib (#2160) Co-authored-by: Owais Lone Co-authored-by: Diego Hurtado --- .../src/opentelemetry/instrumentation/bootstrap_gen.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 06bad91a22..851c03d5f3 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -80,6 +80,10 @@ "library": "mysql-connector-python ~= 8.0", "instrumentation": "opentelemetry-instrumentation-mysql==0.24b0", }, + "pika": { + "library": "pika >= 1.1.0", + "instrumentation": "opentelemetry-instrumentation-pika==0.24b0", + }, "psycopg2": { "library": "psycopg2 >= 2.7.3.1", "instrumentation": "opentelemetry-instrumentation-psycopg2==0.24b0", From a0d1267b3aec027b72c39296ea350fa5210332fc Mon Sep 17 00:00:00 2001 From: andrew-matteson Date: Wed, 6 Oct 2021 11:19:10 -0400 Subject: [PATCH 0997/1517] Update bootstrap_gen.py for jinja2 versions (#2183) --- .../src/opentelemetry/instrumentation/bootstrap_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 851c03d5f3..efd37e5266 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -73,7 +73,7 @@ "instrumentation": "opentelemetry-instrumentation-httpx==0.24b0", }, "jinja2": { - "library": "jinja2~=2.7", + "library": "jinja2 >= 2.7, < 4.0", "instrumentation": "opentelemetry-instrumentation-jinja2==0.24b0", }, "mysql-connector-python": { From 65528f7534f1f5f2e8adc7520b6e696a84569c7d Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 6 Oct 2021 14:13:03 -0700 Subject: [PATCH 0998/1517] update OTLP/HTTP example & test port to 4318 (#2016) * update OTLP/HTTP port to 4318 * map 4318->55681 in the collector for now * update development status Co-authored-by: Diego Hurtado --- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- .../exporter/otlp/proto/http/trace_exporter/__init__.py | 2 +- tests/opentelemetry-docker-tests/tests/docker-compose.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index 38b82f4265..9ea3711965 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/ platforms = any license = Apache-2.0 classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 211d037999..0bcb0a7368 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -47,7 +47,7 @@ DEFAULT_COMPRESSION = Compression.NoCompression -DEFAULT_ENDPOINT = "http://localhost:55681/v1/traces" +DEFAULT_ENDPOINT = "http://localhost:4318/v1/traces" DEFAULT_TIMEOUT = 10 # in seconds diff --git a/tests/opentelemetry-docker-tests/tests/docker-compose.yml b/tests/opentelemetry-docker-tests/tests/docker-compose.yml index b1cd2d6e1f..3f74827156 100644 --- a/tests/opentelemetry-docker-tests/tests/docker-compose.yml +++ b/tests/opentelemetry-docker-tests/tests/docker-compose.yml @@ -8,7 +8,7 @@ services: - "8888:8888" - "55678:55678" otcollector: - image: otel/opentelemetry-collector:0.22.0 + image: otel/opentelemetry-collector:0.31.0 ports: - "4317:4317" - - "55681:55681" + - "4318:55681" From 2a94c3d556f456160c4734be4bbd11fb34693d7c Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 12 Oct 2021 09:38:36 -0700 Subject: [PATCH 0999/1517] Update RELEASING.md (#2177) --- RELEASING.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index b57632048f..cc02d74b59 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -73,16 +73,6 @@ run eachdist once again: ./scripts/eachdist.py update_versions --versions stable,prerelease ``` -## Update website docs - -If the docs for the Opentelemetry [website](https://opentelemetry.io/docs/python/) was updated in this release in this [folder](https://github.com/open-telemetry/opentelemetry-python/tree/main/website_docs), submit a [PR](https://github.com/open-telemetry/opentelemetry.io/tree/main/content/en/docs/python) to update the docs on the website accordingly. To check if the new version requires updating, run the following script and compare the diff: - -```bash -./scripts/generate_website_docs.sh -``` - -If the diff includes significant changes, create a pull request to commit the changes and once the changes are merged, click the "Run workflow" button for the Update [OpenTelemetry Website Docs](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/docs-update.yml) GitHub Action. - ## Hotfix procedure A `hotfix` is defined as a small change developed to correct a bug that should be released as quickly as possible. Due to the nature of hotfixes, they usually will only affect one or a few packages. Therefore, it usually is not necessary to go through the entire release process outlined above for hotfixes. Follow the below steps how to release a hotfix: From 0770fcd72f3dce435904e17e99a3795af5d64a08 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 12 Oct 2021 23:58:27 +0530 Subject: [PATCH 1000/1517] Update contrib SHA for Github Actions workflow (#2191) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 795a288f2e..14b8db0a5a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: d2984f5242ed2250ad1c11b6164e2e8e11e2a804 + CONTRIB_REPO_SHA: 36275f3cbf00c2021caa1d922bd98215c50b9af3 jobs: build: From 78672025f7fb49737356e5c50f7cb33992a3da55 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Tue, 12 Oct 2021 16:41:35 -0400 Subject: [PATCH 1001/1517] Fix race in `set_tracer_provider()` (#2182) * Fix race in set_tracer_provider * refactor _reset_globals to a test util * get rid of "Mixin" name and simplify code a bit * add some comments to concurrency_test.py * actually respect log option Co-authored-by: Diego Hurtado Co-authored-by: Leighton Chen Co-authored-by: Owais Lone --- CHANGELOG.md | 2 + .../tests/test_jaeger_exporter_thrift.py | 11 +-- .../tests/test_otcollector_trace_exporter.py | 11 +-- .../src/opentelemetry/trace/__init__.py | 40 +++++---- .../src/opentelemetry/util/_once.py | 47 ++++++++++ opentelemetry-api/tests/trace/test_globals.py | 65 +++++++++++--- opentelemetry-api/tests/trace/test_proxy.py | 10 +-- opentelemetry-api/tests/util/test_once.py | 48 ++++++++++ .../opentelemetry/test/concurrency_test.py | 90 +++++++++++++++++++ .../src/opentelemetry/test/globals_test.py | 41 +++++++++ .../util/src/opentelemetry/test/test_base.py | 7 +- tox.ini | 2 +- 12 files changed, 313 insertions(+), 61 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/util/_once.py create mode 100644 opentelemetry-api/tests/util/test_once.py create mode 100644 tests/util/src/opentelemetry/test/concurrency_test.py create mode 100644 tests/util/src/opentelemetry/test/globals_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 835601d58c..2523b5834d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.5.0-0.24b0...HEAD) +- Fix race in `set_tracer_provider()` + ([#2182](https://github.com/open-telemetry/opentelemetry-python/pull/2182)) - Automatically load OTEL environment variables as options for `opentelemetry-instrument` ([#1969](https://github.com/open-telemetry/opentelemetry-python/pull/1969)) - `opentelemetry-semantic-conventions` Update to semantic conventions v1.6.1 diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py index c72cd579ff..8a30527eeb 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py @@ -15,7 +15,6 @@ import unittest from unittest import mock -from unittest.mock import patch # pylint:disable=no-name-in-module # pylint:disable=import-error @@ -38,6 +37,7 @@ from opentelemetry.sdk.resources import SERVICE_NAME from opentelemetry.sdk.trace import Resource, TracerProvider from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.test.globals_test import TraceGlobalsTest from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, ) @@ -53,7 +53,7 @@ def _translate_spans_with_dropped_attributes(): return translate._translate(ThriftTranslator(max_tag_value_length=5)) -class TestJaegerExporter(unittest.TestCase): +class TestJaegerExporter(TraceGlobalsTest, unittest.TestCase): def setUp(self): # create and save span to be used in tests self.context = trace_api.SpanContext( @@ -73,7 +73,6 @@ def setUp(self): self._test_span.end(end_time=3) # pylint: disable=protected-access - @patch("opentelemetry.exporter.jaeger.thrift.trace._TRACER_PROVIDER", None) def test_constructor_default(self): # pylint: disable=protected-access """Test the default values assigned by constructor.""" @@ -98,7 +97,6 @@ def test_constructor_default(self): self.assertTrue(exporter._agent_client is not None) self.assertIsNone(exporter._max_tag_value_length) - @patch("opentelemetry.exporter.jaeger.thrift.trace._TRACER_PROVIDER", None) def test_constructor_explicit(self): # pylint: disable=protected-access """Test the constructor passing all the options.""" @@ -143,7 +141,6 @@ def test_constructor_explicit(self): self.assertTrue(exporter._collector_http_client.auth is None) self.assertEqual(exporter._max_tag_value_length, 42) - @patch("opentelemetry.exporter.jaeger.thrift.trace._TRACER_PROVIDER", None) def test_constructor_by_environment_variables(self): # pylint: disable=protected-access """Test the constructor using Environment Variables.""" @@ -198,7 +195,6 @@ def test_constructor_by_environment_variables(self): self.assertTrue(exporter._collector_http_client.auth is None) environ_patcher.stop() - @patch("opentelemetry.exporter.jaeger.thrift.trace._TRACER_PROVIDER", None) def test_constructor_with_no_traceprovider_resource(self): """Test the constructor when there is no resource attached to trace_provider""" @@ -480,7 +476,6 @@ def test_translate_to_jaeger(self): self.assertEqual(spans, expected_spans) - @patch("opentelemetry.exporter.jaeger.thrift.trace._TRACER_PROVIDER", None) def test_export(self): """Test that agent and/or collector are invoked""" @@ -511,9 +506,7 @@ def test_export(self): exporter.export((self._test_span,)) self.assertEqual(agent_client_mock.emit.call_count, 1) self.assertEqual(collector_mock.submit.call_count, 1) - # trace_api._TRACER_PROVIDER = None - @patch("opentelemetry.exporter.jaeger.thrift.trace._TRACER_PROVIDER", None) def test_export_span_service_name(self): trace_api.set_tracer_provider( TracerProvider( diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py index 43d9bcd430..cd4dcb1a08 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py @@ -29,15 +29,12 @@ from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SpanExportResult +from opentelemetry.test.globals_test import TraceGlobalsTest from opentelemetry.trace import TraceFlags # pylint: disable=no-member -class TestCollectorSpanExporter(unittest.TestCase): - @mock.patch( - "opentelemetry.exporter.opencensus.trace_exporter.trace._TRACER_PROVIDER", - None, - ) +class TestCollectorSpanExporter(TraceGlobalsTest, unittest.TestCase): def test_constructor(self): mock_get_node = mock.Mock() patch = mock.patch( @@ -329,10 +326,6 @@ def test_export(self): getattr(output_identifier, "host_name"), "testHostName" ) - @mock.patch( - "opentelemetry.exporter.opencensus.trace_exporter.trace._TRACER_PROVIDER", - None, - ) def test_export_service_name(self): trace_api.set_tracer_provider( TracerProvider( diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 24c42b04c6..26df821cbc 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -108,6 +108,7 @@ ) from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types +from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider logger = getLogger(__name__) @@ -452,8 +453,9 @@ def start_as_current_span( yield INVALID_SPAN -_TRACER_PROVIDER = None -_PROXY_TRACER_PROVIDER = None +_TRACER_PROVIDER_SET_ONCE = Once() +_TRACER_PROVIDER: Optional[TracerProvider] = None +_PROXY_TRACER_PROVIDER = ProxyTracerProvider() def get_tracer( @@ -476,40 +478,40 @@ def get_tracer( ) +def _set_tracer_provider(tracer_provider: TracerProvider, log: bool) -> None: + def set_tp() -> None: + global _TRACER_PROVIDER # pylint: disable=global-statement + _TRACER_PROVIDER = tracer_provider + + did_set = _TRACER_PROVIDER_SET_ONCE.do_once(set_tp) + + if log and not did_set: + logger.warning("Overriding of current TracerProvider is not allowed") + + def set_tracer_provider(tracer_provider: TracerProvider) -> None: """Sets the current global :class:`~.TracerProvider` object. This can only be done once, a warning will be logged if any furter attempt is made. """ - global _TRACER_PROVIDER # pylint: disable=global-statement - - if _TRACER_PROVIDER is not None: - logger.warning("Overriding of current TracerProvider is not allowed") - return - - _TRACER_PROVIDER = tracer_provider + _set_tracer_provider(tracer_provider, log=True) def get_tracer_provider() -> TracerProvider: """Gets the current global :class:`~.TracerProvider` object.""" - # pylint: disable=global-statement - global _TRACER_PROVIDER - global _PROXY_TRACER_PROVIDER - if _TRACER_PROVIDER is None: # if a global tracer provider has not been set either via code or env # vars, return a proxy tracer provider if OTEL_PYTHON_TRACER_PROVIDER not in os.environ: - if not _PROXY_TRACER_PROVIDER: - _PROXY_TRACER_PROVIDER = ProxyTracerProvider() return _PROXY_TRACER_PROVIDER - _TRACER_PROVIDER = cast( # type: ignore - "TracerProvider", - _load_provider(OTEL_PYTHON_TRACER_PROVIDER, "tracer_provider"), + tracer_provider: TracerProvider = _load_provider( + OTEL_PYTHON_TRACER_PROVIDER, "tracer_provider" ) - return _TRACER_PROVIDER + _set_tracer_provider(tracer_provider, log=False) + # _TRACER_PROVIDER will have been set by one thread + return cast("TracerProvider", _TRACER_PROVIDER) @contextmanager # type: ignore diff --git a/opentelemetry-api/src/opentelemetry/util/_once.py b/opentelemetry-api/src/opentelemetry/util/_once.py new file mode 100644 index 0000000000..c0cee43a17 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/util/_once.py @@ -0,0 +1,47 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from threading import Lock +from typing import Callable + + +class Once: + """Execute a function exactly once and block all callers until the function returns + + Same as golang's `sync.Once `_ + """ + + def __init__(self) -> None: + self._lock = Lock() + self._done = False + + def do_once(self, func: Callable[[], None]) -> bool: + """Execute ``func`` if it hasn't been executed or return. + + Will block until ``func`` has been called by one thread. + + Returns: + Whether or not ``func`` was executed in this call + """ + + # fast path, try to avoid locking + if self._done: + return False + + with self._lock: + if not self._done: + func() + self._done = True + return True + return False diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 034a97e4de..421b72d65f 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -1,7 +1,9 @@ import unittest -from unittest.mock import patch +from unittest.mock import Mock, patch from opentelemetry import context, trace +from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc +from opentelemetry.test.globals_test import TraceGlobalsTest from opentelemetry.trace.status import Status, StatusCode @@ -25,25 +27,60 @@ def record_exception( self.recorded_exception = exception -class TestGlobals(unittest.TestCase): - def setUp(self): - self._patcher = patch("opentelemetry.trace._TRACER_PROVIDER") - self._mock_tracer_provider = self._patcher.start() - - def tearDown(self) -> None: - self._patcher.stop() - - def test_get_tracer(self): +class TestGlobals(TraceGlobalsTest, unittest.TestCase): + @staticmethod + @patch("opentelemetry.trace._TRACER_PROVIDER") + def test_get_tracer(mock_tracer_provider): # type: ignore """trace.get_tracer should proxy to the global tracer provider.""" trace.get_tracer("foo", "var") - self._mock_tracer_provider.get_tracer.assert_called_with( - "foo", "var", None - ) - mock_provider = unittest.mock.Mock() + mock_tracer_provider.get_tracer.assert_called_with("foo", "var", None) + mock_provider = Mock() trace.get_tracer("foo", "var", mock_provider) mock_provider.get_tracer.assert_called_with("foo", "var", None) +class TestGlobalsConcurrency(TraceGlobalsTest, ConcurrencyTestBase): + @patch("opentelemetry.trace.logger") + def test_set_tracer_provider_many_threads(self, mock_logger) -> None: # type: ignore + mock_logger.warning = MockFunc() + + def do_concurrently() -> Mock: + # first get a proxy tracer + proxy_tracer = trace.ProxyTracerProvider().get_tracer("foo") + + # try to set the global tracer provider + mock_tracer_provider = Mock(get_tracer=MockFunc()) + trace.set_tracer_provider(mock_tracer_provider) + + # start a span through the proxy which will call through to the mock provider + proxy_tracer.start_span("foo") + + return mock_tracer_provider + + num_threads = 100 + mock_tracer_providers = self.run_with_many_threads( + do_concurrently, + num_threads=num_threads, + ) + + # despite trying to set tracer provider many times, only one of the + # mock_tracer_providers should have stuck and been called from + # proxy_tracer.start_span() + mock_tps_with_any_call = [ + mock + for mock in mock_tracer_providers + if mock.get_tracer.call_count > 0 + ] + + self.assertEqual(len(mock_tps_with_any_call), 1) + self.assertEqual( + mock_tps_with_any_call[0].get_tracer.call_count, num_threads + ) + + # should have warned everytime except for the successful set + self.assertEqual(mock_logger.warning.call_count, num_threads - 1) + + class TestTracer(unittest.TestCase): def setUp(self): # pylint: disable=protected-access diff --git a/opentelemetry-api/tests/trace/test_proxy.py b/opentelemetry-api/tests/trace/test_proxy.py index 42a31b4132..da1d60c74e 100644 --- a/opentelemetry-api/tests/trace/test_proxy.py +++ b/opentelemetry-api/tests/trace/test_proxy.py @@ -17,6 +17,7 @@ import unittest from opentelemetry import trace +from opentelemetry.test.globals_test import TraceGlobalsTest from opentelemetry.trace.span import INVALID_SPAN_CONTEXT, NonRecordingSpan @@ -39,10 +40,8 @@ class TestSpan(NonRecordingSpan): pass -class TestProxy(unittest.TestCase): +class TestProxy(TraceGlobalsTest, unittest.TestCase): def test_proxy_tracer(self): - original_provider = trace._TRACER_PROVIDER - provider = trace.get_tracer_provider() # proxy provider self.assertIsInstance(provider, trace.ProxyTracerProvider) @@ -60,6 +59,9 @@ def test_proxy_tracer(self): # set a real provider trace.set_tracer_provider(TestProvider()) + # get_tracer_provider() now returns the real provider + self.assertIsInstance(trace.get_tracer_provider(), TestProvider) + # tracer provider now returns real instance self.assertIsInstance(trace.get_tracer_provider(), TestProvider) @@ -71,5 +73,3 @@ def test_proxy_tracer(self): # creates real spans with tracer.start_span("") as span: self.assertIsInstance(span, TestSpan) - - trace._TRACER_PROVIDER = original_provider diff --git a/opentelemetry-api/tests/util/test_once.py b/opentelemetry-api/tests/util/test_once.py new file mode 100644 index 0000000000..ee94318d22 --- /dev/null +++ b/opentelemetry-api/tests/util/test_once.py @@ -0,0 +1,48 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc +from opentelemetry.util._once import Once + + +class TestOnce(ConcurrencyTestBase): + def test_once_single_thread(self): + once_func = MockFunc() + once = Once() + + self.assertEqual(once_func.call_count, 0) + + # first call should run + called = once.do_once(once_func) + self.assertTrue(called) + self.assertEqual(once_func.call_count, 1) + + # subsequent calls do nothing + called = once.do_once(once_func) + self.assertFalse(called) + self.assertEqual(once_func.call_count, 1) + + def test_once_many_threads(self): + once_func = MockFunc() + once = Once() + + def run_concurrently() -> bool: + return once.do_once(once_func) + + results = self.run_with_many_threads(run_concurrently, num_threads=100) + + self.assertEqual(once_func.call_count, 1) + + # check that only one of the threads got True + self.assertEqual(results.count(True), 1) diff --git a/tests/util/src/opentelemetry/test/concurrency_test.py b/tests/util/src/opentelemetry/test/concurrency_test.py new file mode 100644 index 0000000000..5d178e24ff --- /dev/null +++ b/tests/util/src/opentelemetry/test/concurrency_test.py @@ -0,0 +1,90 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import threading +import unittest +from functools import partial +from typing import Callable, List, Optional, TypeVar +from unittest.mock import Mock + +ReturnT = TypeVar("ReturnT") + + +class MockFunc: + """A thread safe mock function + + Use this as part of your mock if you want to count calls across multiple + threads. + """ + + def __init__(self) -> None: + self.lock = threading.Lock() + self.call_count = 0 + self.mock = Mock() + + def __call__(self, *args, **kwargs): + with self.lock: + self.call_count += 1 + return self.mock + + +class ConcurrencyTestBase(unittest.TestCase): + """Test base class/mixin for tests of concurrent code + + This test class calls ``sys.setswitchinterval(1e-12)`` to try to create more + contention while running tests that use many threads. It also provides + ``run_with_many_threads`` to run some test code in many threads + concurrently. + """ + + orig_switch_interval = sys.getswitchinterval() + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + # switch threads more often to increase chance of contention + sys.setswitchinterval(1e-12) + + @classmethod + def tearDownClass(cls) -> None: + super().tearDownClass() + sys.setswitchinterval(cls.orig_switch_interval) + + @staticmethod + def run_with_many_threads( + func_to_test: Callable[[], ReturnT], + num_threads: int = 100, + ) -> List[ReturnT]: + """Util to run ``func_to_test`` in ``num_threads`` concurrently""" + + barrier = threading.Barrier(num_threads) + results: List[Optional[ReturnT]] = [None] * num_threads + + def thread_start(idx: int) -> None: + nonlocal results + # Get all threads here before releasing them to create contention + barrier.wait() + results[idx] = func_to_test() + + threads = [ + threading.Thread(target=partial(thread_start, i)) + for i in range(num_threads) + ] + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + return results # type: ignore diff --git a/tests/util/src/opentelemetry/test/globals_test.py b/tests/util/src/opentelemetry/test/globals_test.py new file mode 100644 index 0000000000..bb2cad6a0a --- /dev/null +++ b/tests/util/src/opentelemetry/test/globals_test.py @@ -0,0 +1,41 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry import trace as trace_api +from opentelemetry.util._once import Once + + +# pylint: disable=protected-access +def reset_trace_globals() -> None: + """WARNING: only use this for tests.""" + trace_api._TRACER_PROVIDER_SET_ONCE = Once() + trace_api._TRACER_PROVIDER = None + trace_api._PROXY_TRACER_PROVIDER = trace_api.ProxyTracerProvider() + + +class TraceGlobalsTest(unittest.TestCase): + """Resets trace API globals in setUp/tearDown + + Use as a base class or mixin for your test that modifies trace API globals. + """ + + def setUp(self) -> None: + super().setUp() + reset_trace_globals() + + def tearDown(self) -> None: + super().tearDown() + reset_trace_globals() diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index 9762a08010..f176238add 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -21,6 +21,7 @@ from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) +from opentelemetry.test.globals_test import reset_trace_globals class TestBase(unittest.TestCase): @@ -28,20 +29,18 @@ class TestBase(unittest.TestCase): @classmethod def setUpClass(cls): - cls.original_tracer_provider = trace_api.get_tracer_provider() result = cls.create_tracer_provider() cls.tracer_provider, cls.memory_exporter = result # This is done because set_tracer_provider cannot override the # current tracer provider. - trace_api._TRACER_PROVIDER = None # pylint: disable=protected-access + reset_trace_globals() trace_api.set_tracer_provider(cls.tracer_provider) @classmethod def tearDownClass(cls): # This is done because set_tracer_provider cannot override the # current tracer provider. - trace_api._TRACER_PROVIDER = None # pylint: disable=protected-access - trace_api.set_tracer_provider(cls.original_tracer_provider) + reset_trace_globals() def setUp(self): self.memory_exporter.clear() diff --git a/tox.ini b/tox.ini index 724a7f2018..0210e7e6e9 100644 --- a/tox.ini +++ b/tox.ini @@ -85,7 +85,7 @@ setenv = ; i.e: CONTRIB_REPO_SHA=dde62cebffe519c35875af6d06fae053b3be65ec tox -e CONTRIB_REPO_SHA={env:CONTRIB_REPO_SHA:"main"} CONTRIB_REPO="git+https://github.com/open-telemetry/opentelemetry-python-contrib.git@{env:CONTRIB_REPO_SHA}" - mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ + mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/:{toxinidir}/tests/util/src/ changedir = api: opentelemetry-api/tests From ed3d657e489da0152e3604f58cb217a561b0c515 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 13 Oct 2021 23:16:02 +0530 Subject: [PATCH 1002/1517] Make baggage implementation w3c spec complaint (#2167) * Add regexes to check keys and values Fixes #2010 * Fix tests * Add changelog * Fix test * Add checks to set_baggage * Fix lint * Add changelog entry * Fix mypy * Fix mypy * Cast to string * WIP * Key value format * Mostly done * Remove old changelog entry * fomat * Correct typing * Fix lint * Fix issues * Add CHANGELOG entry * Make changes as discussed in SIG meeting * Update opentelemetry-api/src/opentelemetry/baggage/__init__.py Co-authored-by: Leighton Chen Co-authored-by: Diego Hurtado Co-authored-by: Leighton Chen --- CHANGELOG.md | 2 + .../src/opentelemetry/baggage/__init__.py | 60 ++++++-- .../baggage/propagation/__init__.py | 53 +++++-- .../src/opentelemetry/util/re.py | 6 +- .../tests/baggage/test_baggage_propagation.py | 136 +++++++++++------- 5 files changed, 180 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2523b5834d..274eb7c5dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2145](https://github.com/open-telemetry/opentelemetry-python/pull/2145)) - Add `schema_url` to `TracerProvider.get_tracer` ([#2154](https://github.com/open-telemetry/opentelemetry-python/pull/2154)) +- Make baggage implementation w3c spec complaint + ([#2167](https://github.com/open-telemetry/opentelemetry-python/pull/2167)) ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 diff --git a/opentelemetry-api/src/opentelemetry/baggage/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/__init__.py index 2368c5a325..8dea6dbfb9 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/__init__.py @@ -12,18 +12,30 @@ # See the License for the specific language governing permissions and # limitations under the License. -import typing +from logging import getLogger +from re import compile from types import MappingProxyType +from typing import Mapping, Optional from opentelemetry.context import create_key, get_value, set_value from opentelemetry.context.context import Context +from opentelemetry.util.re import ( + _BAGGAGE_PROPERTY_FORMAT, + _KEY_FORMAT, + _VALUE_FORMAT, +) _BAGGAGE_KEY = create_key("baggage") +_logger = getLogger(__name__) + +_KEY_PATTERN = compile(_KEY_FORMAT) +_VALUE_PATTERN = compile(_VALUE_FORMAT) +_PROPERT_PATTERN = compile(_BAGGAGE_PROPERTY_FORMAT) def get_all( - context: typing.Optional[Context] = None, -) -> typing.Mapping[str, object]: + context: Optional[Context] = None, +) -> Mapping[str, object]: """Returns the name/value pairs in the Baggage Args: @@ -39,8 +51,8 @@ def get_all( def get_baggage( - name: str, context: typing.Optional[Context] = None -) -> typing.Optional[object]: + name: str, context: Optional[Context] = None +) -> Optional[object]: """Provides access to the value for a name/value pair in the Baggage @@ -56,7 +68,7 @@ def get_baggage( def set_baggage( - name: str, value: object, context: typing.Optional[Context] = None + name: str, value: object, context: Optional[Context] = None ) -> Context: """Sets a value in the Baggage @@ -69,13 +81,20 @@ def set_baggage( A Context with the value updated """ baggage = dict(get_all(context=context)) - baggage[name] = value + if not _is_valid_key(name): + _logger.warning( + "Baggage key `%s` does not match format, ignoring", name + ) + elif not _is_valid_value(str(value)): + _logger.warning( + "Baggage value `%s` does not match format, ignoring", value + ) + else: + baggage[name] = value return set_value(_BAGGAGE_KEY, baggage, context=context) -def remove_baggage( - name: str, context: typing.Optional[Context] = None -) -> Context: +def remove_baggage(name: str, context: Optional[Context] = None) -> Context: """Removes a value from the Baggage Args: @@ -91,7 +110,7 @@ def remove_baggage( return set_value(_BAGGAGE_KEY, baggage, context=context) -def clear(context: typing.Optional[Context] = None) -> Context: +def clear(context: Optional[Context] = None) -> Context: """Removes all values from the Baggage Args: @@ -101,3 +120,22 @@ def clear(context: typing.Optional[Context] = None) -> Context: A Context with all baggage entries removed """ return set_value(_BAGGAGE_KEY, {}, context=context) + + +def _is_valid_key(name: str) -> bool: + return _KEY_PATTERN.fullmatch(str(name)) is not None + + +def _is_valid_value(value: object) -> bool: + parts = str(value).split(";") + is_valid_value = _VALUE_PATTERN.fullmatch(parts[0]) is not None + if len(parts) > 1: # one or more properties metadata + for property in parts[1:]: + if _PROPERT_PATTERN.fullmatch(property) is None: + is_valid_value = False + break + return is_valid_value + + +def _is_valid_pair(key: str, value: str) -> bool: + return _is_valid_key(key) and _is_valid_value(value) diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index 9d170aae9a..8ba28357c3 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -12,13 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import typing +from logging import getLogger +from re import split +from typing import Iterable, Mapping, Optional, Set from urllib.parse import quote_plus, unquote_plus -from opentelemetry.baggage import get_all, set_baggage +from opentelemetry.baggage import _is_valid_pair, get_all, set_baggage from opentelemetry.context import get_current from opentelemetry.context.context import Context from opentelemetry.propagators import textmap +from opentelemetry.util.re import _DELIMITER_PATTERN + +_logger = getLogger(__name__) class W3CBaggagePropagator(textmap.TextMapPropagator): @@ -32,7 +37,7 @@ class W3CBaggagePropagator(textmap.TextMapPropagator): def extract( self, carrier: textmap.CarrierT, - context: typing.Optional[Context] = None, + context: Optional[Context] = None, getter: textmap.Getter = textmap.default_getter, ) -> Context: """Extract Baggage from the carrier. @@ -49,20 +54,46 @@ def extract( ) if not header or len(header) > self._MAX_HEADER_LENGTH: + _logger.warning( + "Baggage header `%s` exceeded the maximum number of bytes per baggage-string", + header, + ) return context - baggage_entries = header.split(",") + baggage_entries = split(_DELIMITER_PATTERN, header) total_baggage_entries = self._MAX_PAIRS + + if len(baggage_entries) > self._MAX_PAIRS: + _logger.warning( + "Baggage header `%s` exceeded the maximum number of list-members", + header, + ) + for entry in baggage_entries: if len(entry) > self._MAX_PAIR_LENGTH: + _logger.warning( + "Baggage entry `%s` exceeded the maximum number of bytes per list-member", + entry, + ) + continue + if not entry: # empty string continue try: name, value = entry.split("=", 1) except Exception: # pylint: disable=broad-except + _logger.warning( + "Baggage list-member `%s` doesn't match the format", entry + ) continue + name = unquote_plus(name).strip().lower() + value = unquote_plus(value).strip() + if not _is_valid_pair(name, value): + _logger.warning("Invalid baggage entry: `%s`", entry) + continue + context = set_baggage( - unquote_plus(name).strip(), - unquote_plus(value).strip(), + name, + value, context=context, ) total_baggage_entries -= 1 @@ -74,7 +105,7 @@ def extract( def inject( self, carrier: textmap.CarrierT, - context: typing.Optional[Context] = None, + context: Optional[Context] = None, setter: textmap.Setter = textmap.default_setter, ) -> None: """Injects Baggage into the carrier. @@ -90,12 +121,12 @@ def inject( setter.set(carrier, self._BAGGAGE_HEADER_NAME, baggage_string) @property - def fields(self) -> typing.Set[str]: + def fields(self) -> Set[str]: """Returns a set with the fields set in `inject`.""" return {self._BAGGAGE_HEADER_NAME} -def _format_baggage(baggage_entries: typing.Mapping[str, object]) -> str: +def _format_baggage(baggage_entries: Mapping[str, object]) -> str: return ",".join( quote_plus(str(key)) + "=" + quote_plus(str(value)) for key, value in baggage_entries.items() @@ -103,8 +134,8 @@ def _format_baggage(baggage_entries: typing.Mapping[str, object]) -> str: def _extract_first_element( - items: typing.Optional[typing.Iterable[textmap.CarrierT]], -) -> typing.Optional[textmap.CarrierT]: + items: Optional[Iterable[textmap.CarrierT]], +) -> Optional[textmap.CarrierT]: if items is None: return None return next(iter(items), None) diff --git a/opentelemetry-api/src/opentelemetry/util/re.py b/opentelemetry-api/src/opentelemetry/util/re.py index b503c4893a..32c3e3ffb4 100644 --- a/opentelemetry-api/src/opentelemetry/util/re.py +++ b/opentelemetry-api/src/opentelemetry/util/re.py @@ -27,10 +27,12 @@ ) # A value contains a URL encoded UTF-8 string. _VALUE_FORMAT = r"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*" -_HEADER_FORMAT = _KEY_FORMAT + _OWS + r"=" + _OWS + _VALUE_FORMAT -_HEADER_PATTERN = compile(_HEADER_FORMAT) +_KEY_VALUE_FORMAT = rf"{_OWS}{_KEY_FORMAT}{_OWS}={_OWS}{_VALUE_FORMAT}{_OWS}" +_HEADER_PATTERN = compile(_KEY_VALUE_FORMAT) _DELIMITER_PATTERN = compile(r"[ \t]*,[ \t]*") +_BAGGAGE_PROPERTY_FORMAT = rf"{_KEY_VALUE_FORMAT}|{_OWS}{_KEY_FORMAT}{_OWS}" + # pylint: disable=invalid-name def parse_headers(s: str) -> Mapping[str, str]: diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py index caecc4615e..69a1039a49 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py @@ -15,6 +15,7 @@ # type: ignore import unittest +from logging import WARNING from unittest.mock import Mock, patch from opentelemetry import baggage @@ -62,19 +63,27 @@ def test_valid_header_with_space(self): self.assertEqual(self._extract(header), expected) def test_valid_header_with_properties(self): - header = "key1=val1,key2=val2;prop=1" - expected = {"key1": "val1", "key2": "val2;prop=1"} + header = "key1=val1,key2=val2;prop=1;prop2;prop3=2" + expected = {"key1": "val1", "key2": "val2;prop=1;prop2;prop3=2"} self.assertEqual(self._extract(header), expected) - def test_valid_header_with_url_escaped_comma(self): - header = "key%2C1=val1,key2=val2%2Cval3" - expected = {"key,1": "val1", "key2": "val2,val3"} + def test_valid_header_with_url_escaped_values(self): + header = "key1=val1,key2=val2%3Aval3,key3=val4%40%23%24val5" + expected = { + "key1": "val1", + "key2": "val2:val3", + "key3": "val4@#$val5", + } self.assertEqual(self._extract(header), expected) - def test_valid_header_with_invalid_value(self): + def test_header_with_invalid_value(self): header = "key1=val1,key2=val2,a,val3" - expected = {"key1": "val1", "key2": "val2"} - self.assertEqual(self._extract(header), expected) + with self.assertLogs(level=WARNING) as warning: + self._extract(header) + self.assertIn( + "Baggage list-member `a` doesn't match the format", + warning.output[0], + ) def test_valid_header_with_empty_value(self): header = "key1=,key2=val2" @@ -82,9 +91,8 @@ def test_valid_header_with_empty_value(self): self.assertEqual(self._extract(header), expected) def test_invalid_header(self): - header = "header1" - expected = {} - self.assertEqual(self._extract(header), expected) + self.assertEqual(self._extract("header1"), {}) + self.assertEqual(self._extract(" = "), {}) def test_header_too_long(self): long_value = "s" * (W3CBaggagePropagator._MAX_HEADER_LENGTH + 1) @@ -107,62 +115,85 @@ def test_header_contains_pair_too_long(self): long_value = "s" * (W3CBaggagePropagator._MAX_PAIR_LENGTH + 1) header = "key1=value1,key2={},key3=value3".format(long_value) expected = {"key1": "value1", "key3": "value3"} - self.assertEqual(self._extract(header), expected) + with self.assertLogs(level=WARNING) as warning: + self.assertEqual(self._extract(header), expected) + self.assertIn( + "exceeded the maximum number of bytes per list-member", + warning.output[0], + ) def test_extract_unquote_plus(self): self.assertEqual( - self._extract("key+key=value+value"), {"key key": "value value"} + self._extract("keykey=value%5Evalue"), {"keykey": "value^value"} ) self.assertEqual( - self._extract("key%2Fkey=value%2Fvalue"), - {"key/key": "value/value"}, + self._extract("key%23key=value%23value"), + {"key#key": "value#value"}, ) def test_header_max_entries_skip_invalid_entry(self): - self.assertEqual( - self._extract( - ",".join( - [ - f"key{index}=value{index}" - if index != 2 - else ( - f"key{index}=" - f"value{'s' * (W3CBaggagePropagator._MAX_PAIR_LENGTH + 1)}" - ) - for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) - ] - ) - ), - { - f"key{index}": f"value{index}" - for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) - if index != 2 - }, - ) - self.assertEqual( - self._extract( - ",".join( - [ - f"key{index}=value{index}" - if index != 2 - else f"key{index}xvalue{index}" - for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) - ] - ) - ), - { - f"key{index}": f"value{index}" - for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) - if index != 2 - }, - ) + with self.assertLogs(level=WARNING) as warning: + self.assertEqual( + self._extract( + ",".join( + [ + f"key{index}=value{index}" + if index != 2 + else ( + f"key{index}=" + f"value{'s' * (W3CBaggagePropagator._MAX_PAIR_LENGTH + 1)}" + ) + for index in range( + W3CBaggagePropagator._MAX_PAIRS + 1 + ) + ] + ) + ), + { + f"key{index}": f"value{index}" + for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) + if index != 2 + }, + ) + self.assertIn( + "exceeded the maximum number of list-members", + warning.output[0], + ) + + with self.assertLogs(level=WARNING) as warning: + self.assertEqual( + self._extract( + ",".join( + [ + f"key{index}=value{index}" + if index != 2 + else f"key{index}xvalue{index}" + for index in range( + W3CBaggagePropagator._MAX_PAIRS + 1 + ) + ] + ) + ), + { + f"key{index}": f"value{index}" + for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) + if index != 2 + }, + ) + self.assertIn( + "exceeded the maximum number of list-members", + warning.output[0], + ) def test_inject_no_baggage_entries(self): values = {} output = self._inject(values) self.assertEqual(None, output) + def test_inject_invalid_entries(self): + self.assertEqual(None, self._inject({"key": "val ue"})) + def test_inject(self): values = { "key1": "val1", @@ -178,7 +209,6 @@ def test_inject_escaped_values(self): "key2": "val3=4", } output = self._inject(values) - self.assertIn("key1=val1%2Cval2", output) self.assertIn("key2=val3%3D4", output) def test_inject_non_string_values(self): From e74e1f630118fad8e98e900854d4c51fdadd2bf0 Mon Sep 17 00:00:00 2001 From: Ben Campbell Date: Wed, 13 Oct 2021 14:59:06 -0700 Subject: [PATCH 1003/1517] Adding BatchSpanProcessor thread name (#2186) * Adding thread name * Updating CHANGELOG --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/export/__init__.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 274eb7c5dc..d13b3d435a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2154](https://github.com/open-telemetry/opentelemetry-python/pull/2154)) - Make baggage implementation w3c spec complaint ([#2167](https://github.com/open-telemetry/opentelemetry-python/pull/2167)) +- Add name to `BatchSpanProcessor` worker thread + ([#2186](https://github.com/open-telemetry/opentelemetry-python/pull/2186)) ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index d5d84df045..369821239b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -175,7 +175,9 @@ def __init__( self.queue = collections.deque( [], max_queue_size ) # type: typing.Deque[Span] - self.worker_thread = threading.Thread(target=self.worker, daemon=True) + self.worker_thread = threading.Thread( + name="OtelBatchSpanProcessor", target=self.worker, daemon=True + ) self.condition = threading.Condition(threading.Lock()) self._flush_request = None # type: typing.Optional[_FlushRequest] self.schedule_delay_millis = schedule_delay_millis From e1c4a5b49e30268eba090763e39f6195885b395a Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 13 Oct 2021 16:11:59 -0700 Subject: [PATCH 1004/1517] updating changelogs and version to 1.6.0-0.25b0 (#2193) --- .github/workflows/test.yml | 9 ++- CHANGELOG.md | 6 +- eachdist.ini | 4 +- .../exporter/jaeger/proto/grpc/version.py | 2 +- .../exporter/jaeger/thrift/version.py | 2 +- .../opentelemetry-exporter-jaeger/setup.cfg | 4 +- .../opentelemetry/exporter/jaeger/version.py | 2 +- .../exporter/opencensus/version.py | 2 +- .../setup.cfg | 2 +- .../exporter/otlp/proto/grpc/version.py | 2 +- .../setup.cfg | 2 +- .../exporter/otlp/proto/http/version.py | 2 +- .../opentelemetry-exporter-otlp/setup.cfg | 4 +- .../opentelemetry/exporter/otlp/version.py | 2 +- .../exporter/zipkin/json/version.py | 2 +- .../setup.cfg | 2 +- .../exporter/zipkin/proto/http/version.py | 2 +- .../opentelemetry-exporter-zipkin/setup.cfg | 4 +- .../opentelemetry/exporter/zipkin/version.py | 2 +- .../src/opentelemetry/version.py | 2 +- opentelemetry-distro/setup.cfg | 6 +- .../src/opentelemetry/distro/version.py | 2 +- .../instrumentation/bootstrap_gen.py | 68 +++++++++---------- .../opentelemetry/instrumentation/version.py | 2 +- .../src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 6 +- .../src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../opentelemetry/propagators/b3/version.py | 2 +- .../propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../shim/opentracing_shim/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- tox.ini | 6 +- 34 files changed, 88 insertions(+), 77 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 14b8db0a5a..62b31dd150 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,14 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 36275f3cbf00c2021caa1d922bd98215c50b9af3 + CONTRIB_REPO_SHA: 4a4d889b1876323d7f70507b5e4d079f454fe0d6 + # This is needed because we do not clone the core repo in contrib builds anymore. + # When running contrib builds as part of core builds, we use actions/checkout@v2 which + # does not set an environment variable (simply just runs tox), which is different when + # contrib builds are run directly from contrib (since test.yml is executed, which sets CORE_REPO_SHA) + # The solution is to include CORE_REPO_SHA as part of THIS environment so it can be accessed + # from within the contrib build. + CORE_REPO_SHA: ${{ github.sha }} jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index d13b3d435a..7878136a0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.5.0-0.24b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.6.0-0.25b0...HEAD) + +## [1.6.0-0.25b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.0-0.25b0) - 2021-10-13 + + - Fix race in `set_tracer_provider()` ([#2182](https://github.com/open-telemetry/opentelemetry-python/pull/2182)) - Automatically load OTEL environment variables as options for `opentelemetry-instrument` diff --git a/eachdist.ini b/eachdist.ini index 065c543e94..548d6d36e0 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -12,7 +12,7 @@ sortfirst= exporter/* [stable] -version=1.5.0 +version=1.6.0 packages= opentelemetry-sdk @@ -31,7 +31,7 @@ packages= opentelemetry-api [prerelease] -version=0.24b0 +version=0.25b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 14cffddd57..7373c3ac09 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 14cffddd57..7373c3ac09 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index e882a65f97..91f4c8ba9b 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,8 +39,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.5.0 - opentelemetry-exporter-jaeger-thrift == 1.5.0 + opentelemetry-exporter-jaeger-proto-grpc == 1.6.0 + opentelemetry-exporter-jaeger-thrift == 1.6.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 14cffddd57..7373c3ac09 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index d33bd87ce4..2a05c9b361 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24b0" +__version__ = "0.25b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 5d144190d3..9d0fd68d04 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.5.0 + opentelemetry-proto == 1.6.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index c8902adc49..e97f1318ec 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index 9ea3711965..ad9311713a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.5.0 + opentelemetry-proto == 1.6.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index c8902adc49..e97f1318ec 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 88daac2533..53ebc02975 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.5.0 - opentelemetry-exporter-otlp-proto-http == 1.5.0 + opentelemetry-exporter-otlp-proto-grpc == 1.6.0 + opentelemetry-exporter-otlp-proto-http == 1.6.0 [options.entry_points] opentelemetry_traces_exporter = diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index c8902adc49..e97f1318ec 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index c8902adc49..e97f1318ec 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index a6cdd5b5f8..90d69c9478 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.5.0 + opentelemetry-exporter-zipkin-json == 1.6.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index c8902adc49..e97f1318ec 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 70e850bfe7..f4f5c5e083 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.5.0 - opentelemetry-exporter-zipkin-proto-http == 1.5.0 + opentelemetry-exporter-zipkin-json == 1.6.0 + opentelemetry-exporter-zipkin-proto-http == 1.6.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index c8902adc49..e97f1318ec 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index c8902adc49..e97f1318ec 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 36779604d0..bf41c9010a 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -42,8 +42,8 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api ~= 1.3 - opentelemetry-instrumentation == 0.24b0 - opentelemetry-sdk == 1.5.0 + opentelemetry-instrumentation == 0.25b0 + opentelemetry-sdk == 1.6.0 [options.packages.find] where = src @@ -57,4 +57,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.5.0 + opentelemetry-exporter-otlp == 1.6.0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index d33bd87ce4..2a05c9b361 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24b0" +__version__ = "0.25b0" diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index efd37e5266..282d4491ca 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -18,125 +18,125 @@ libraries = { "aiohttp": { "library": "aiohttp ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.24b0", + "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.25b0", }, "aiopg": { "library": "aiopg >= 0.13.0, < 1.3.0", - "instrumentation": "opentelemetry-instrumentation-aiopg==0.24b0", + "instrumentation": "opentelemetry-instrumentation-aiopg==0.25b0", }, "asgiref": { "library": "asgiref ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-asgi==0.24b0", + "instrumentation": "opentelemetry-instrumentation-asgi==0.25b0", }, "asyncpg": { "library": "asyncpg >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-asyncpg==0.24b0", + "instrumentation": "opentelemetry-instrumentation-asyncpg==0.25b0", }, "boto": { "library": "boto~=2.0", - "instrumentation": "opentelemetry-instrumentation-boto==0.24b0", + "instrumentation": "opentelemetry-instrumentation-boto==0.25b0", }, "botocore": { "library": "botocore ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-botocore==0.24b0", + "instrumentation": "opentelemetry-instrumentation-botocore==0.25b0", }, "celery": { "library": "celery >= 4.0, < 6.0", - "instrumentation": "opentelemetry-instrumentation-celery==0.24b0", + "instrumentation": "opentelemetry-instrumentation-celery==0.25b0", }, "django": { "library": "django >= 1.10", - "instrumentation": "opentelemetry-instrumentation-django==0.24b0", + "instrumentation": "opentelemetry-instrumentation-django==0.25b0", }, "elasticsearch": { "library": "elasticsearch >= 2.0", - "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.24b0", + "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.25b0", }, "falcon": { "library": "falcon >= 2.0.0, < 4.0.0", - "instrumentation": "opentelemetry-instrumentation-falcon==0.24b0", + "instrumentation": "opentelemetry-instrumentation-falcon==0.25b0", }, "fastapi": { "library": "fastapi ~= 0.58", - "instrumentation": "opentelemetry-instrumentation-fastapi==0.24b0", + "instrumentation": "opentelemetry-instrumentation-fastapi==0.25b0", }, "flask": { "library": "flask >= 1.0, < 3.0", - "instrumentation": "opentelemetry-instrumentation-flask==0.24b0", + "instrumentation": "opentelemetry-instrumentation-flask==0.25b0", }, "grpcio": { "library": "grpcio ~= 1.27", - "instrumentation": "opentelemetry-instrumentation-grpc==0.24b0", + "instrumentation": "opentelemetry-instrumentation-grpc==0.25b0", }, "httpx": { "library": "httpx >= 0.18.0, < 0.19.0", - "instrumentation": "opentelemetry-instrumentation-httpx==0.24b0", + "instrumentation": "opentelemetry-instrumentation-httpx==0.25b0", }, "jinja2": { "library": "jinja2 >= 2.7, < 4.0", - "instrumentation": "opentelemetry-instrumentation-jinja2==0.24b0", + "instrumentation": "opentelemetry-instrumentation-jinja2==0.25b0", }, "mysql-connector-python": { "library": "mysql-connector-python ~= 8.0", - "instrumentation": "opentelemetry-instrumentation-mysql==0.24b0", + "instrumentation": "opentelemetry-instrumentation-mysql==0.25b0", }, "pika": { "library": "pika >= 1.1.0", - "instrumentation": "opentelemetry-instrumentation-pika==0.24b0", + "instrumentation": "opentelemetry-instrumentation-pika==0.25b0", }, "psycopg2": { "library": "psycopg2 >= 2.7.3.1", - "instrumentation": "opentelemetry-instrumentation-psycopg2==0.24b0", + "instrumentation": "opentelemetry-instrumentation-psycopg2==0.25b0", }, "pymemcache": { "library": "pymemcache ~= 1.3", - "instrumentation": "opentelemetry-instrumentation-pymemcache==0.24b0", + "instrumentation": "opentelemetry-instrumentation-pymemcache==0.25b0", }, "pymongo": { "library": "pymongo ~= 3.1", - "instrumentation": "opentelemetry-instrumentation-pymongo==0.24b0", + "instrumentation": "opentelemetry-instrumentation-pymongo==0.25b0", }, "PyMySQL": { "library": "PyMySQL ~= 0.10.1", - "instrumentation": "opentelemetry-instrumentation-pymysql==0.24b0", + "instrumentation": "opentelemetry-instrumentation-pymysql==0.25b0", }, "pyramid": { "library": "pyramid >= 1.7", - "instrumentation": "opentelemetry-instrumentation-pyramid==0.24b0", + "instrumentation": "opentelemetry-instrumentation-pyramid==0.25b0", }, "redis": { "library": "redis >= 2.6", - "instrumentation": "opentelemetry-instrumentation-redis==0.24b0", + "instrumentation": "opentelemetry-instrumentation-redis==0.25b0", }, "requests": { "library": "requests ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-requests==0.24b0", + "instrumentation": "opentelemetry-instrumentation-requests==0.25b0", }, "scikit-learn": { "library": "scikit-learn ~= 0.24.0", - "instrumentation": "opentelemetry-instrumentation-sklearn==0.24b0", + "instrumentation": "opentelemetry-instrumentation-sklearn==0.25b0", }, "sqlalchemy": { "library": "sqlalchemy", - "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.24b0", + "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.25b0", }, "starlette": { "library": "starlette ~= 0.13.0", - "instrumentation": "opentelemetry-instrumentation-starlette==0.24b0", + "instrumentation": "opentelemetry-instrumentation-starlette==0.25b0", }, "tornado": { "library": "tornado >= 6.0", - "instrumentation": "opentelemetry-instrumentation-tornado==0.24b0", + "instrumentation": "opentelemetry-instrumentation-tornado==0.25b0", }, "urllib3": { "library": "urllib3 >= 1.0.0, < 2.0.0", - "instrumentation": "opentelemetry-instrumentation-urllib3==0.24b0", + "instrumentation": "opentelemetry-instrumentation-urllib3==0.25b0", }, } default_instrumentations = [ - "opentelemetry-instrumentation-dbapi==0.24b0", - "opentelemetry-instrumentation-logging==0.24b0", - "opentelemetry-instrumentation-sqlite3==0.24b0", - "opentelemetry-instrumentation-urllib==0.24b0", - "opentelemetry-instrumentation-wsgi==0.24b0", + "opentelemetry-instrumentation-dbapi==0.25b0", + "opentelemetry-instrumentation-logging==0.25b0", + "opentelemetry-instrumentation-sqlite3==0.25b0", + "opentelemetry-instrumentation-urllib==0.25b0", + "opentelemetry-instrumentation-wsgi==0.25b0", ] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index d33bd87ce4..2a05c9b361 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24b0" +__version__ = "0.25b0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index c8902adc49..e97f1318ec 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 56390868ed..c84e79f5d8 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,9 +42,9 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.5.0 - opentelemetry-semantic-conventions == 0.24b0 - opentelemetry-instrumentation == 0.24b0 + opentelemetry-api == 1.6.0 + opentelemetry-semantic-conventions == 0.25b0 + opentelemetry-instrumentation == 0.25b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index c8902adc49..e97f1318ec 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index d33bd87ce4..2a05c9b361 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24b0" +__version__ = "0.25b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index c8902adc49..e97f1318ec 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index c8902adc49..e97f1318ec 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 41f96654a0..3e7277782d 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -46,7 +46,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test == 0.24b0 + opentelemetry-test == 0.25b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index d33bd87ce4..2a05c9b361 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24b0" +__version__ = "0.25b0" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 536b2ec853..0d5c9e9785 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.24b0" +__version__ = "0.25b0" diff --git a/tox.ini b/tox.ini index 0210e7e6e9..0259cc9bf8 100644 --- a/tox.ini +++ b/tox.ini @@ -126,9 +126,9 @@ commands_pre = instrumentation: pip install {toxinidir}/opentelemetry-instrumentation getting-started: pip install requests==2.26.0 flask==2.0.1 -e {toxinidir}/opentelemetry-instrumentation + getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http" getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-requests&subdirectory=instrumentation/opentelemetry-instrumentation-requests" getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-wsgi&subdirectory=instrumentation/opentelemetry-instrumentation-wsgi" - getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http" getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-flask&subdirectory=instrumentation/opentelemetry-instrumentation-flask" opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus @@ -254,9 +254,9 @@ commands_pre = -e {toxinidir}/opentelemetry-semantic-conventions \ -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ + -e "{env:CONTRIB_REPO}#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http" \ -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-requests&subdirectory=instrumentation/opentelemetry-instrumentation-requests" \ - -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-wsgi&subdirectory=instrumentation/opentelemetry-instrumentation-wsgi" \ - -e "{env:CONTRIB_REPO}#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http" + -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-wsgi&subdirectory=instrumentation/opentelemetry-instrumentation-wsgi" commands = {toxinidir}/scripts/tracecontext-integration-test.sh From ade44b17da64f00624d9a3bd3f161f68280d6653 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Thu, 14 Oct 2021 06:25:44 +0530 Subject: [PATCH 1005/1517] Move BaseConfigurator to SDK (#2188) * Move _SUPPRESS_INTRUMENTATION key from instrumentation to api This will continue to be part of the internal API but live in the API context package. * Move BaseConfigurator from isntrumentation to SDK This allows SDK to not depend on instrumentation package anymore. --- .../src/opentelemetry/context/__init__.py | 6 +++ .../instrumentation/configurator.py | 53 ------------------- .../opentelemetry/instrumentation/utils.py | 8 +-- opentelemetry-sdk/setup.cfg | 1 - .../sdk/_configuration/__init__.py | 31 ++++++++++- .../sdk/trace/export/__init__.py | 9 +++- 6 files changed, 44 insertions(+), 64 deletions(-) delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index a8dd39c53a..7f56cdb216 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -157,3 +157,9 @@ def detach(token: object) -> None: _RUNTIME_CONTEXT.detach(token) # type: ignore except Exception: # pylint: disable=broad-except logger.error("Failed to detach context") + + +# FIXME This is a temporary location for the suppress instrumentation key. +# Once the decision around how to suppress instrumentation is made in the +# spec, this key should be moved accordingly. +_SUPPRESS_INSTRUMENTATION_KEY = create_key("suppress_instrumentation") diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py deleted file mode 100644 index 3efa71e89e..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/configurator.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -""" -OpenTelemetry Base Configurator -""" - -from abc import ABC, abstractmethod -from logging import getLogger - -_LOG = getLogger(__name__) - - -class BaseConfigurator(ABC): - """An ABC for configurators - - Configurators are used to configure - SDKs (i.e. TracerProvider, MeterProvider, Processors...) - to reduce the amount of manual configuration required. - """ - - _instance = None - _is_instrumented = False - - def __new__(cls, *args, **kwargs): - - if cls._instance is None: - cls._instance = object.__new__(cls, *args, **kwargs) - - return cls._instance - - @abstractmethod - def _configure(self, **kwargs): - """Configure the SDK""" - - def configure(self, **kwargs): - """Configure the SDK""" - self._configure(**kwargs) - - -__all__ = ["BaseConfigurator"] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py index 16f75aae6c..5f63b4af5e 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -16,14 +16,10 @@ from wrapt import ObjectProxy -from opentelemetry.context import create_key +# pylint: disable=unused-import +from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401 from opentelemetry.trace import StatusCode -# FIXME This is a temporary location for the suppress instrumentation key. -# Once the decision around how to suppress instrumentation is made in the -# spec, this key should be moved accordingly. -_SUPPRESS_INSTRUMENTATION_KEY = create_key("suppress_instrumentation") - def extract_attributes_from_object( obj: any, attributes: Sequence[str], existing: Dict[str, str] = None diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index c84e79f5d8..bf063a237f 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -44,7 +44,6 @@ include_package_data = True install_requires = opentelemetry-api == 1.6.0 opentelemetry-semantic-conventions == 0.25b0 - opentelemetry-instrumentation == 0.25b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 79445a7a35..f2ebd31749 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -17,6 +17,7 @@ OpenTelemetry SDK Configurator for Easy Instrumentation with Distros """ +from abc import ABC, abstractmethod from os import environ from typing import Sequence, Tuple @@ -27,7 +28,6 @@ OTEL_PYTHON_ID_GENERATOR, OTEL_TRACES_EXPORTER, ) -from opentelemetry.instrumentation.configurator import BaseConfigurator from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator @@ -140,7 +140,34 @@ def _initialize_components(): _init_tracing(trace_exporters, id_generator) -class _OTelSDKConfigurator(BaseConfigurator): +class _BaseConfigurator(ABC): + """An ABC for configurators + + Configurators are used to configure + SDKs (i.e. TracerProvider, MeterProvider, Processors...) + to reduce the amount of manual configuration required. + """ + + _instance = None + _is_instrumented = False + + def __new__(cls, *args, **kwargs): + + if cls._instance is None: + cls._instance = object.__new__(cls, *args, **kwargs) + + return cls._instance + + @abstractmethod + def _configure(self, **kwargs): + """Configure the SDK""" + + def configure(self, **kwargs): + """Configure the SDK""" + self._configure(**kwargs) + + +class _OTelSDKConfigurator(_BaseConfigurator): """A basic Configurator by OTel Python for initalizing OTel SDK components Initializes several crucial OTel SDK components (i.e. TracerProvider, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 369821239b..4f0cc817c9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -21,8 +21,13 @@ from os import environ, linesep from typing import Optional -from opentelemetry.context import Context, attach, detach, set_value -from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.context import ( + _SUPPRESS_INSTRUMENTATION_KEY, + Context, + attach, + detach, + set_value, +) from opentelemetry.sdk.environment_variables import ( OTEL_BSP_EXPORT_TIMEOUT, OTEL_BSP_MAX_EXPORT_BATCH_SIZE, From 171569ad72f3b62604bf563062a6f17c21d38858 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Thu, 14 Oct 2021 07:07:22 +0530 Subject: [PATCH 1006/1517] Only run Python3.6 contrib tests (#2192) --- .github/workflows/test.yml | 45 +++++--------------------------------- 1 file changed, 5 insertions(+), 40 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 62b31dd150..404b1cb485 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -111,19 +111,19 @@ jobs: key: v2-tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core - name: run tox run: tox -e ${{ matrix.tox-environment }} + + # Contrib unit test suite in order to ensure changes in core do not break anything in contrib. + # We only run contrib unit tests on the oldest supported Python version (3.6) as running the same tests + # on all versions is somewhat redundant. contrib-build: env: # We use these variables to convert between tox and GHA version literals py36: 3.6 - py37: 3.7 - py38: 3.8 - py39: 3.9 - pypy3: pypy3 runs-on: ${{ matrix.os }} strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [ py36, py37, py38, py39, pypy3 ] + python-version: [ py36 ] package: ["instrumentation", "exporter"] os: [ ubuntu-20.04] steps: @@ -154,38 +154,3 @@ jobs: key: v2-tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib - name: run tox run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} - contrib-misc: - strategy: - fail-fast: false - matrix: - tox-environment: [ "docker-tests"] - name: ${{ matrix.tox-environment }} - runs-on: ubuntu-20.04 - steps: - - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} - uses: actions/checkout@v2 - with: - repository: open-telemetry/opentelemetry-python-contrib - ref: ${{ env.CONTRIB_REPO_SHA }} - - name: Checkout Core Repo @ SHA ${{ github.sha }} - uses: actions/checkout@v2 - with: - repository: open-telemetry/opentelemetry-python - path: opentelemetry-python-core - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - architecture: 'x64' - - name: Install tox - run: pip install -U tox - - name: Cache tox environment - # Preserves .tox directory between runs for faster installs - uses: actions/cache@v2 - with: - path: | - .tox - ~/.cache/pip - key: v2-tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib - - name: run tox - run: tox -e ${{ matrix.tox-environment }} From a746d577ebc98b1b0b4e97c3d1b3df6826091e93 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Thu, 14 Oct 2021 07:47:28 +0530 Subject: [PATCH 1007/1517] Move _SUPPRESS_INTRUMENTATION key from instrumentation to api (#2187) This will continue to be part of the internal API but live in the API context package. From b0a9ee3d5a95c17547e79d62450cffca17fe6f71 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Thu, 14 Oct 2021 07:13:33 -0700 Subject: [PATCH 1008/1517] Add AWS Lambda Instrumentation to boostrap_gen.py (#2197) --- .../src/opentelemetry/instrumentation/bootstrap_gen.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 282d4491ca..671cb2ba80 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -134,6 +134,7 @@ }, } default_instrumentations = [ + "opentelemetry-instrumentation-aws_lambda==0.25b0", "opentelemetry-instrumentation-dbapi==0.25b0", "opentelemetry-instrumentation-logging==0.25b0", "opentelemetry-instrumentation-sqlite3==0.25b0", From 3bf379da512fe6645c8d6b0391448da014f4c0ca Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Thu, 14 Oct 2021 14:17:58 -0400 Subject: [PATCH 1009/1517] Remove redundant title from Getting started page (#2204) --- website_docs/getting-started.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/website_docs/getting-started.md b/website_docs/getting-started.md index def6372c62..4453c06eb8 100644 --- a/website_docs/getting-started.md +++ b/website_docs/getting-started.md @@ -1,13 +1,11 @@ --- -date: '2021-08-30T16:49:17.700Z' +date: '2021-10-05T20:20:20.000Z' docname: getting-started images: {} path: /getting-started title: Getting Started --- -# Getting Started - This guide walks you through instrumenting a Python application with `opentelemetry-python`. For more elaborate examples, see [examples](https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples/). From 3ef7cb50c8cb0117b39078192824f007efe7b932 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Fri, 15 Oct 2021 02:14:54 +0530 Subject: [PATCH 1010/1517] Move instrumentation and distro to contrib (#2196) Now that SDK does not depend on opentelemetry-instrumentation anymore and opentelemetry-instrumentation has actual build time dependencies on the contrib repo, it makes maintanence a lot easier if we move opentelemetry-instrumentation to contrib repo. opentelemetry-distro depends on opentelemetry-instrumentation and is being moved as well. Neither of the two packages are really part of "core" Otel python anyway. --- .github/pull_request_template.md | 1 - .github/workflows/test.yml | 4 +- docs-requirements.txt | 2 - eachdist.ini | 2 - opentelemetry-distro/MANIFEST.in | 7 - opentelemetry-distro/README.rst | 23 -- opentelemetry-distro/setup.cfg | 60 ------ opentelemetry-distro/setup.py | 29 --- .../src/opentelemetry/distro/__init__.py | 34 --- .../src/opentelemetry/distro/py.typed | 0 .../src/opentelemetry/distro/version.py | 15 -- opentelemetry-distro/tests/__init__.py | 0 opentelemetry-distro/tests/test_distro.py | 38 ---- opentelemetry-instrumentation/LICENSE | 201 ------------------ opentelemetry-instrumentation/MANIFEST.in | 8 - opentelemetry-instrumentation/README.rst | 123 ----------- opentelemetry-instrumentation/setup.cfg | 58 ----- opentelemetry-instrumentation/setup.py | 29 --- .../auto_instrumentation/__init__.py | 111 ---------- .../auto_instrumentation/sitecustomize.py | 141 ------------ .../instrumentation/bootstrap.py | 154 -------------- .../instrumentation/dependencies.py | 62 ------ .../opentelemetry/instrumentation/distro.py | 71 ------- .../instrumentation/environment_variables.py | 18 -- .../instrumentation/instrumentor.py | 132 ------------ .../instrumentation/propagators.py | 124 ----------- .../opentelemetry/instrumentation/py.typed | 0 .../opentelemetry/instrumentation/utils.py | 64 ------ .../opentelemetry/instrumentation/version.py | 15 -- .../tests/__init__.py | 0 .../tests/test_bootstrap.py | 91 -------- .../tests/test_dependencies.py | 77 ------- .../tests/test_distro.py | 58 ----- .../tests/test_instrumentor.py | 50 ----- .../tests/test_propagators.py | 80 ------- .../tests/test_run.py | 118 ---------- .../tests/test_utils.py | 57 ----- scripts/build.sh | 2 +- tox.ini | 28 +-- 39 files changed, 7 insertions(+), 2080 deletions(-) delete mode 100644 opentelemetry-distro/MANIFEST.in delete mode 100644 opentelemetry-distro/README.rst delete mode 100644 opentelemetry-distro/setup.cfg delete mode 100644 opentelemetry-distro/setup.py delete mode 100644 opentelemetry-distro/src/opentelemetry/distro/__init__.py delete mode 100644 opentelemetry-distro/src/opentelemetry/distro/py.typed delete mode 100644 opentelemetry-distro/src/opentelemetry/distro/version.py delete mode 100644 opentelemetry-distro/tests/__init__.py delete mode 100644 opentelemetry-distro/tests/test_distro.py delete mode 100644 opentelemetry-instrumentation/LICENSE delete mode 100644 opentelemetry-instrumentation/MANIFEST.in delete mode 100644 opentelemetry-instrumentation/README.rst delete mode 100644 opentelemetry-instrumentation/setup.cfg delete mode 100644 opentelemetry-instrumentation/setup.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/environment_variables.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py delete mode 100644 opentelemetry-instrumentation/tests/__init__.py delete mode 100644 opentelemetry-instrumentation/tests/test_bootstrap.py delete mode 100644 opentelemetry-instrumentation/tests/test_dependencies.py delete mode 100644 opentelemetry-instrumentation/tests/test_distro.py delete mode 100644 opentelemetry-instrumentation/tests/test_instrumentor.py delete mode 100644 opentelemetry-instrumentation/tests/test_propagators.py delete mode 100644 opentelemetry-instrumentation/tests/test_run.py delete mode 100644 opentelemetry-instrumentation/tests/test_utils.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 48d109f5ef..d564f69c67 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -23,7 +23,6 @@ Please describe the tests that you ran to verify your changes. Provide instructi Answer the following question based on these examples of changes that would require a Contrib Repo Change: - [The OTel specification](https://github.com/open-telemetry/opentelemetry-specification) has changed which prompted this PR to update the method interfaces of `opentelemetry-api/` or `opentelemetry-sdk/` -- The method interfaces of `opentelemetry-instrumentation/` have changed - The method interfaces of `test/util` have changed - Scripts in `scripts/` that were copied over to the Contrib repo have changed - Configuration files that were copied over to the Contrib repo have changed (when consistency between repositories is applicable) such as in diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 404b1cb485..192a11d9ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 4a4d889b1876323d7f70507b5e4d079f454fe0d6 + CONTRIB_REPO_SHA: c2e674983a265e54c5eb14e376459a992498aae6 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when @@ -34,7 +34,7 @@ jobs: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: python-version: [ py36, py37, py38, py39, pypy3 ] - package: ["api", "sdk", "instrumentation", "semantic", "getting", "distro" , "shim", "exporter", "protobuf", "propagator"] + package: ["api", "sdk", "semantic", "getting", "shim", "exporter", "protobuf", "propagator"] os: [ ubuntu-20.04, windows-2019 ] steps: - name: Checkout Core Repo @ SHA - ${{ github.sha }} diff --git a/docs-requirements.txt b/docs-requirements.txt index b670676fbb..a93eb8aa70 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -8,9 +8,7 @@ sphinx-jekyll-builder # doesn't work for pkg_resources. ./opentelemetry-api ./opentelemetry-semantic-conventions -./opentelemetry-instrumentation ./opentelemetry-sdk -./opentelemetry-instrumentation # Required by instrumentation and exporter packages ddtrace>=0.34.0 diff --git a/eachdist.ini b/eachdist.ini index 548d6d36e0..52d5be769f 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -5,7 +5,6 @@ sortfirst= opentelemetry-api opentelemetry-sdk - opentelemetry-instrumentation opentelemetry-proto opentelemetry-distro tests/util @@ -39,7 +38,6 @@ packages= opentelemetry-distro opentelemetry-semantic-conventions opentelemetry-test - opentelemetry-instrumentation tests [experimental] diff --git a/opentelemetry-distro/MANIFEST.in b/opentelemetry-distro/MANIFEST.in deleted file mode 100644 index 191b7d1959..0000000000 --- a/opentelemetry-distro/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -prune tests -graft src -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include MANIFEST.in -include README.rst diff --git a/opentelemetry-distro/README.rst b/opentelemetry-distro/README.rst deleted file mode 100644 index 8095283910..0000000000 --- a/opentelemetry-distro/README.rst +++ /dev/null @@ -1,23 +0,0 @@ -OpenTelemetry Distro -==================== - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-distro.svg - :target: https://pypi.org/project/opentelemetry-distro/ - -Installation ------------- - -:: - - pip install opentelemetry-distro - - -This package provides entrypoints to configure OpenTelemetry. - -References ----------- - -* `OpenTelemetry Project `_ -* `Example using opentelemetry-distro `_ diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg deleted file mode 100644 index bf41c9010a..0000000000 --- a/opentelemetry-distro/setup.cfg +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-distro -description = OpenTelemetry Python Distro -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-distro -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Typing :: Typed - -[options] -python_requires = >=3.6 -package_dir= - =src -packages=find_namespace: -zip_safe = False -include_package_data = True -install_requires = - opentelemetry-api ~= 1.3 - opentelemetry-instrumentation == 0.25b0 - opentelemetry-sdk == 1.6.0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_distro = - distro = opentelemetry.distro:OpenTelemetryDistro -opentelemetry_configurator = - configurator = opentelemetry.distro:OpenTelemetryConfigurator - -[options.extras_require] -test = -otlp = - opentelemetry-exporter-otlp == 1.6.0 diff --git a/opentelemetry-distro/setup.py b/opentelemetry-distro/setup.py deleted file mode 100644 index 4783772d06..0000000000 --- a/opentelemetry-distro/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "distro", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup( - version=PACKAGE_INFO["__version__"], -) diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py deleted file mode 100644 index 97e3e2fcc9..0000000000 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER -from opentelemetry.instrumentation.distro import BaseDistro -from opentelemetry.sdk._configuration import _OTelSDKConfigurator - - -class OpenTelemetryConfigurator(_OTelSDKConfigurator): - pass - - -class OpenTelemetryDistro(BaseDistro): - """ - The OpenTelemetry provided Distro configures a default set of - configuration out of the box. - """ - - # pylint: disable=no-self-use - def _configure(self, **kwargs): - os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp_proto_grpc_span") diff --git a/opentelemetry-distro/src/opentelemetry/distro/py.typed b/opentelemetry-distro/src/opentelemetry/distro/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py deleted file mode 100644 index 2a05c9b361..0000000000 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.25b0" diff --git a/opentelemetry-distro/tests/__init__.py b/opentelemetry-distro/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry-distro/tests/test_distro.py b/opentelemetry-distro/tests/test_distro.py deleted file mode 100644 index 2e42ed904a..0000000000 --- a/opentelemetry-distro/tests/test_distro.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -import os -from unittest import TestCase - -from pkg_resources import DistributionNotFound, require - -from opentelemetry.distro import OpenTelemetryDistro -from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER - - -class TestDistribution(TestCase): - def test_package_available(self): - try: - require(["opentelemetry-distro"]) - except DistributionNotFound: - self.fail("opentelemetry-distro not installed") - - def test_default_configuration(self): - distro = OpenTelemetryDistro() - self.assertIsNone(os.environ.get(OTEL_TRACES_EXPORTER)) - distro.configure() - self.assertEqual( - "otlp_proto_grpc_span", os.environ.get(OTEL_TRACES_EXPORTER) - ) diff --git a/opentelemetry-instrumentation/LICENSE b/opentelemetry-instrumentation/LICENSE deleted file mode 100644 index 1ef7dad2c5..0000000000 --- a/opentelemetry-instrumentation/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 The OpenTelemetry Authors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/opentelemetry-instrumentation/MANIFEST.in b/opentelemetry-instrumentation/MANIFEST.in deleted file mode 100644 index faee277146..0000000000 --- a/opentelemetry-instrumentation/MANIFEST.in +++ /dev/null @@ -1,8 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include MANIFEST.in -include README.rst -include LICENSE diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst deleted file mode 100644 index cae4e3ab5f..0000000000 --- a/opentelemetry-instrumentation/README.rst +++ /dev/null @@ -1,123 +0,0 @@ -OpenTelemetry Instrumentation -============================= - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation.svg - :target: https://pypi.org/project/opentelemetry-instrumentation/ - -Installation ------------- - -:: - - pip install opentelemetry-instrumentation - - -This package provides a couple of commands that help automatically instruments a program: - -.. note:: - You need to install a distro package to get auto instrumentation working. The ``opentelemetry-distro`` - package contains the default distro and automatically configures some of the common options for users. - For more info about ``opentelemetry-distro`` check `here `__ - :: - - pip install opentelemetry-distro[otlp] - - -opentelemetry-bootstrap ------------------------ - -:: - - opentelemetry-bootstrap --action=install|requirements - -This commands inspects the active Python site-packages and figures out which -instrumentation packages the user might want to install. By default it prints out -a list of the suggested instrumentation packages which can be added to a requirements.txt -file. It also supports installing the suggested packages when run with :code:`--action=install` -flag. - - -opentelemetry-instrument ------------------------- - -:: - - opentelemetry-instrument python program.py - -The instrument command will try to automatically detect packages used by your python program -and when possible, apply automatic tracing instrumentation on them. This means your program -will get automatic distributed tracing for free without having to make any code changes -at all. This will also configure a global tracer and tracing exporter without you having to -make any code changes. By default, the instrument command will use the OTLP exporter but -this can be overriden when needed. - -The command supports the following configuration options as CLI arguments and environment vars: - - -* ``--trace-exporter`` or ``OTEL_TRACES_EXPORTER`` - -Used to specify which trace exporter to use. Can be set to one or more of the well-known exporter -names (see below). - - - Defaults to `otlp`. - - Can be set to `none` to disable automatic tracer initialization. - -You can pass multiple values to configure multiple exporters e.g, ``zipkin,prometheus`` - -Well known trace exporter names: - - - jaeger_proto - - jaeger_thrift - - opencensus - - otlp - - otlp_proto_grpc_span - - otlp_proto_http_span - - zipkin_json - - zipkin_proto - -``otlp`` is an alias for ``otlp_proto_grpc_span``. - -* ``--id-generator`` or ``OTEL_PYTHON_ID_GENERATOR`` - -Used to specify which IDs Generator to use for the global Tracer Provider. By default, it -will use the random IDs generator. - -The code in ``program.py`` needs to use one of the packages for which there is -an OpenTelemetry integration. For a list of the available integrations please -check `here `_ - -* ``OTEL_PYTHON_DISABLED_INSTRUMENTATIONS`` - -If set by the user, opentelemetry-instrument will read this environment variable to disable specific instrumentations. -e.g OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "requests,django" - - -Examples -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:: - - opentelemetry-instrument --trace-exporter otlp flask run --port=3000 - -The above command will pass ``--trace-exporter otlp`` to the instrument command and ``--port=3000`` to ``flask run``. - -:: - - opentelemetry-instrument --trace-exporter zipkin_json,otlp celery -A tasks worker --loglevel=info - -The above command will configure global trace provider, attach zipkin and otlp exporters to it and then -start celery with the rest of the arguments. - -:: - - opentelemetry-instrument --ids-generator random flask run --port=3000 - -The above command will configure the global trace provider to use the Random IDs Generator, and then -pass ``--port=3000`` to ``flask run``. - -References ----------- - -* `OpenTelemetry Project `_ diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg deleted file mode 100644 index 042f0edb19..0000000000 --- a/opentelemetry-instrumentation/setup.cfg +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-instrumentation -description = Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-instrumentation -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - -[options] -python_requires = >=3.6 -package_dir= - =src -packages=find_namespace: -zip_safe = False -include_package_data = True -install_requires = - opentelemetry-api ~= 1.4 - wrapt >= 1.0.0, < 2.0.0 - -[options.packages.find] -where = src - -[options.entry_points] -console_scripts = - opentelemetry-instrument = opentelemetry.instrumentation.auto_instrumentation:run - opentelemetry-bootstrap = opentelemetry.instrumentation.bootstrap:run -opentelemetry_environment_variables = - instrumentation = opentelemetry.instrumentation.environment_variables - -[options.extras_require] -test = diff --git a/opentelemetry-instrumentation/setup.py b/opentelemetry-instrumentation/setup.py deleted file mode 100644 index 9d1d5b7b06..0000000000 --- a/opentelemetry-instrumentation/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup( - version=PACKAGE_INFO["__version__"], -) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py deleted file mode 100644 index 29b09a0c34..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from argparse import REMAINDER, ArgumentParser -from logging import getLogger -from os import environ, execl, getcwd -from os.path import abspath, dirname, pathsep -from re import sub -from shutil import which - -from pkg_resources import iter_entry_points - -_logger = getLogger(__file__) - - -def run() -> None: - - parser = ArgumentParser( - description=""" - opentelemetry-instrument automatically instruments a Python - program and its dependencies and then runs the program. - """, - epilog=""" - Optional arguments (except for --help) for opentelemetry-instrument - directly correspond with OpenTelemetry environment variables. The - corresponding optional argument is formed by removing the OTEL_ or - OTEL_PYTHON_ prefix from the environment variable and lower casing the - rest. For example, the optional argument --attribute_value_length_limit - corresponds with the environment variable - OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT. - - These optional arguments will override the current value of the - corresponding environment variable during the execution of the command. - """, - ) - - argument_otel_environment_variable = {} - - for entry_point in iter_entry_points( - "opentelemetry_environment_variables" - ): - environment_variable_module = entry_point.load() - - for attribute in dir(environment_variable_module): - - if attribute.startswith("OTEL_"): - - argument = sub(r"OTEL_(PYTHON_)?", "", attribute).lower() - - parser.add_argument( - f"--{argument}", - required=False, - ) - argument_otel_environment_variable[argument] = attribute - - parser.add_argument("command", help="Your Python application.") - parser.add_argument( - "command_args", - help="Arguments for your application.", - nargs=REMAINDER, - ) - - args = parser.parse_args() - - for argument, otel_environment_variable in ( - argument_otel_environment_variable - ).items(): - value = getattr(args, argument) - if value is not None: - - environ[otel_environment_variable] = value - - python_path = environ.get("PYTHONPATH") - - if not python_path: - python_path = [] - - else: - python_path = python_path.split(pathsep) - - cwd_path = getcwd() - - # This is being added to support applications that are being run from their - # own executable, like Django. - # FIXME investigate if there is another way to achieve this - if cwd_path not in python_path: - python_path.insert(0, cwd_path) - - filedir_path = dirname(abspath(__file__)) - - python_path = [path for path in python_path if path != filedir_path] - - python_path.insert(0, filedir_path) - - environ["PYTHONPATH"] = pathsep.join(python_path) - - executable = which(args.command) - execl(executable, executable, *args.command_args) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py deleted file mode 100644 index f7a6412ff6..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import sys -from logging import getLogger -from os import environ, path -from os.path import abspath, dirname, pathsep -from re import sub - -from pkg_resources import iter_entry_points - -from opentelemetry.instrumentation.dependencies import ( - get_dist_dependency_conflicts, -) -from opentelemetry.instrumentation.distro import BaseDistro, DefaultDistro -from opentelemetry.instrumentation.environment_variables import ( - OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, -) - -logger = getLogger(__file__) - - -def _load_distros() -> BaseDistro: - for entry_point in iter_entry_points("opentelemetry_distro"): - try: - distro = entry_point.load()() - if not isinstance(distro, BaseDistro): - logger.debug( - "%s is not an OpenTelemetry Distro. Skipping", - entry_point.name, - ) - continue - logger.debug( - "Distribution %s will be configured", entry_point.name - ) - return distro - except Exception as exc: # pylint: disable=broad-except - logger.exception( - "Distribution %s configuration failed", entry_point.name - ) - raise exc - return DefaultDistro() - - -def _load_instrumentors(distro): - package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, []) - if isinstance(package_to_exclude, str): - package_to_exclude = package_to_exclude.split(",") - # to handle users entering "requests , flask" or "requests, flask" with spaces - package_to_exclude = [x.strip() for x in package_to_exclude] - - for entry_point in iter_entry_points("opentelemetry_pre_instrument"): - entry_point.load()() - - for entry_point in iter_entry_points("opentelemetry_instrumentor"): - if entry_point.name in package_to_exclude: - logger.debug( - "Instrumentation skipped for library %s", entry_point.name - ) - continue - - try: - conflict = get_dist_dependency_conflicts(entry_point.dist) - if conflict: - logger.debug( - "Skipping instrumentation %s: %s", - entry_point.name, - conflict, - ) - continue - - # tell instrumentation to not run dep checks again as we already did it above - distro.load_instrumentor(entry_point, skip_dep_check=True) - logger.debug("Instrumented %s", entry_point.name) - except Exception as exc: # pylint: disable=broad-except - logger.exception("Instrumenting of %s failed", entry_point.name) - raise exc - - for entry_point in iter_entry_points("opentelemetry_post_instrument"): - entry_point.load()() - - -def _load_configurators(): - configured = None - for entry_point in iter_entry_points("opentelemetry_configurator"): - if configured is not None: - logger.warning( - "Configuration of %s not loaded, %s already loaded", - entry_point.name, - configured, - ) - continue - try: - entry_point.load()().configure() # type: ignore - configured = entry_point.name - except Exception as exc: # pylint: disable=broad-except - logger.exception("Configuration of %s failed", entry_point.name) - raise exc - - -def initialize(): - try: - distro = _load_distros() - distro.configure() - _load_configurators() - _load_instrumentors(distro) - except Exception: # pylint: disable=broad-except - logger.exception("Failed to auto initialize opentelemetry") - finally: - environ["PYTHONPATH"] = sub( - fr"{dirname(abspath(__file__))}{pathsep}?", - "", - environ["PYTHONPATH"], - ) - - -if ( - hasattr(sys, "argv") - and sys.argv[0].split(path.sep)[-1] == "celery" - and "worker" in sys.argv[1:] -): - from celery.signals import worker_process_init # pylint:disable=E0401 - - @worker_process_init.connect(weak=False) - def init_celery(*args, **kwargs): - initialize() - - -else: - initialize() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py deleted file mode 100644 index f1c8181bae..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import logging -import subprocess -import sys - -import pkg_resources - -from opentelemetry.instrumentation.bootstrap_gen import ( - default_instrumentations, - libraries, -) - -logger = logging.getLogger(__file__) - - -def _syscall(func): - def wrapper(package=None): - try: - if package: - return func(package) - return func() - except subprocess.SubprocessError as exp: - cmd = getattr(exp, "cmd", None) - if cmd: - msg = f'Error calling system command "{" ".join(cmd)}"' - if package: - msg = f'{msg} for package "{package}"' - raise RuntimeError(msg) - - return wrapper - - -@_syscall -def _sys_pip_install(package): - # explicit upgrade strategy to override potential pip config - subprocess.check_call( - [ - sys.executable, - "-m", - "pip", - "install", - "-U", - "--upgrade-strategy", - "only-if-needed", - package, - ] - ) - - -def _pip_check(): - """Ensures none of the instrumentations have dependency conflicts. - Clean check reported as: - 'No broken requirements found.' - Dependency conflicts are reported as: - 'opentelemetry-instrumentation-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' - To not be too restrictive, we'll only check for relevant packages. - """ - with subprocess.Popen( - [sys.executable, "-m", "pip", "check"], stdout=subprocess.PIPE - ) as check_pipe: - pip_check = check_pipe.communicate()[0].decode() - pip_check_lower = pip_check.lower() - for package_tup in libraries.values(): - for package in package_tup: - if package.lower() in pip_check_lower: - raise RuntimeError(f"Dependency conflict found: {pip_check}") - - -def _is_installed(req): - if req in sys.modules: - return True - - try: - pkg_resources.get_distribution(req) - except pkg_resources.DistributionNotFound: - return False - except pkg_resources.VersionConflict as exc: - logger.warning( - "instrumentation for package %s is available but version %s is installed. Skipping.", - exc.req, - exc.dist.as_requirement(), # pylint: disable=no-member - ) - return False - return True - - -def _find_installed_libraries(): - libs = default_instrumentations[:] - libs.extend( - [ - v["instrumentation"] - for _, v in libraries.items() - if _is_installed(v["library"]) - ] - ) - return libs - - -def _run_requirements(): - logger.setLevel(logging.ERROR) - print("\n".join(_find_installed_libraries()), end="") - - -def _run_install(): - for lib in _find_installed_libraries(): - _sys_pip_install(lib) - _pip_check() - - -def run() -> None: - action_install = "install" - action_requirements = "requirements" - - parser = argparse.ArgumentParser( - description=""" - opentelemetry-bootstrap detects installed libraries and automatically - installs the relevant instrumentation packages for them. - """ - ) - parser.add_argument( - "-a", - "--action", - choices=[action_install, action_requirements], - default=action_requirements, - help=""" - install - uses pip to install the new requirements using to the - currently active site-package. - requirements - prints out the new requirements to stdout. Action can - be piped and appended to a requirements.txt file. - """, - ) - args = parser.parse_args() - - cmd = { - action_install: _run_install, - action_requirements: _run_requirements, - }[args.action] - cmd() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py deleted file mode 100644 index 6c65d6677e..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py +++ /dev/null @@ -1,62 +0,0 @@ -from logging import getLogger -from typing import Collection, Optional - -from pkg_resources import ( - Distribution, - DistributionNotFound, - RequirementParseError, - VersionConflict, - get_distribution, -) - -logger = getLogger(__file__) - - -class DependencyConflict: - required: str = None - found: Optional[str] = None - - def __init__(self, required, found=None): - self.required = required - self.found = found - - def __str__(self): - return f'DependencyConflict: requested: "{self.required}" but found: "{self.found}"' - - -def get_dist_dependency_conflicts( - dist: Distribution, -) -> Optional[DependencyConflict]: - main_deps = dist.requires() - instrumentation_deps = [] - for dep in dist.requires(("instruments",)): - if dep not in main_deps: - # we set marker to none so string representation of the dependency looks like - # requests ~= 1.0 - # instead of - # requests ~= 1.0; extra = "instruments" - # which does not work with `get_distribution()` - dep.marker = None - instrumentation_deps.append(str(dep)) - - return get_dependency_conflicts(instrumentation_deps) - - -def get_dependency_conflicts( - deps: Collection[str], -) -> Optional[DependencyConflict]: - for dep in deps: - try: - get_distribution(dep) - except VersionConflict as exc: - return DependencyConflict(dep, exc.dist) - except DistributionNotFound: - return DependencyConflict(dep) - except RequirementParseError as exc: - logger.warning( - 'error parsing dependency, reporting as a conflict: "%s" - %s', - dep, - exc, - ) - return DependencyConflict(dep) - return None diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py deleted file mode 100644 index cc1c99c1e0..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -""" -OpenTelemetry Base Distribution (Distro) -""" - -from abc import ABC, abstractmethod -from logging import getLogger - -from pkg_resources import EntryPoint - -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor - -_LOG = getLogger(__name__) - - -class BaseDistro(ABC): - """An ABC for distro""" - - _instance = None - - def __new__(cls, *args, **kwargs): - - if cls._instance is None: - cls._instance = object.__new__(cls, *args, **kwargs) - - return cls._instance - - @abstractmethod - def _configure(self, **kwargs): - """Configure the distribution""" - - def configure(self, **kwargs): - """Configure the distribution""" - self._configure(**kwargs) - - def load_instrumentor( # pylint: disable=no-self-use - self, entry_point: EntryPoint, **kwargs - ): - """Takes a collection of instrumentation entry points - and activates them by instantiating and calling instrument() - on each one. - - Distros can override this method to customize the behavior by - inspecting each entry point and configuring them in special ways, - passing additional arguments, load a replacement/fork instead, - skip loading entirely, etc. - """ - instrumentor: BaseInstrumentor = entry_point.load() - instrumentor().instrument(**kwargs) - - -class DefaultDistro(BaseDistro): - def _configure(self, **kwargs): - pass - - -__all__ = ["BaseDistro", "DefaultDistro"] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/environment_variables.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/environment_variables.py deleted file mode 100644 index ad28f06859..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/environment_variables.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS" -""" -.. envvar:: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS -""" diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py deleted file mode 100644 index 74ebe86746..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -""" -OpenTelemetry Base Instrumentor -""" - -from abc import ABC, abstractmethod -from logging import getLogger -from typing import Collection, Optional - -from opentelemetry.instrumentation.dependencies import ( - DependencyConflict, - get_dependency_conflicts, -) - -_LOG = getLogger(__name__) - - -class BaseInstrumentor(ABC): - """An ABC for instrumentors - - Child classes of this ABC should instrument specific third - party libraries or frameworks either by using the - ``opentelemetry-instrument`` command or by calling their methods - directly. - - Since every third party library or framework is different and has different - instrumentation needs, more methods can be added to the child classes as - needed to provide practical instrumentation to the end user. - """ - - _instance = None - _is_instrumented_by_opentelemetry = False - - def __new__(cls, *args, **kwargs): - - if cls._instance is None: - cls._instance = object.__new__(cls, *args, **kwargs) - - return cls._instance - - @property - def is_instrumented_by_opentelemetry(self): - return self._is_instrumented_by_opentelemetry - - @abstractmethod - def instrumentation_dependencies(self) -> Collection[str]: - """Return a list of python packages with versions that the will be instrumented. - - The format should be the same as used in requirements.txt or setup.py. - - For example, if an instrumentation instruments requests 1.x, this method should look - like: - - def instrumentation_dependencies(self) -> Collection[str]: - return ['requests ~= 1.0'] - - This will ensure that the instrumentation will only be used when the specified library - is present in the environment. - """ - - def _instrument(self, **kwargs): - """Instrument the library""" - - @abstractmethod - def _uninstrument(self, **kwargs): - """Uninstrument the library""" - - def _check_dependency_conflicts(self) -> Optional[DependencyConflict]: - dependencies = self.instrumentation_dependencies() - return get_dependency_conflicts(dependencies) - - def instrument(self, **kwargs): - """Instrument the library - - This method will be called without any optional arguments by the - ``opentelemetry-instrument`` command. - - This means that calling this method directly without passing any - optional values should do the very same thing that the - ``opentelemetry-instrument`` command does. - """ - - if self._is_instrumented_by_opentelemetry: - _LOG.warning("Attempting to instrument while already instrumented") - return None - - # check if instrumentor has any missing or conflicting dependencies - skip_dep_check = kwargs.pop("skip_dep_check", False) - if not skip_dep_check: - conflict = self._check_dependency_conflicts() - if conflict: - _LOG.error(conflict) - return None - - result = self._instrument( # pylint: disable=assignment-from-no-return - **kwargs - ) - self._is_instrumented_by_opentelemetry = True - return result - - def uninstrument(self, **kwargs): - """Uninstrument the library - - See ``BaseInstrumentor.instrument`` for more information regarding the - usage of ``kwargs``. - """ - - if self._is_instrumented_by_opentelemetry: - result = self._uninstrument(**kwargs) - self._is_instrumented_by_opentelemetry = False - return result - - _LOG.warning("Attempting to uninstrument while already uninstrumented") - - return None - - -__all__ = ["BaseInstrumentor"] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py deleted file mode 100644 index bc40f7742c..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This module implements experimental propagators to inject trace context -into response carriers. This is useful for server side frameworks that start traces -when server requests and want to share the trace context with the client so the -client can add its spans to the same trace. - -This is part of an upcoming W3C spec and will eventually make it to the Otel spec. - -https://w3c.github.io/trace-context/#trace-context-http-response-headers-format -""" - -import typing -from abc import ABC, abstractmethod - -from opentelemetry import trace -from opentelemetry.context.context import Context -from opentelemetry.propagators import textmap -from opentelemetry.trace import format_span_id, format_trace_id - -_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers" -_RESPONSE_PROPAGATOR = None - - -def get_global_response_propagator(): - return _RESPONSE_PROPAGATOR - - -def set_global_response_propagator(propagator): - global _RESPONSE_PROPAGATOR # pylint:disable=global-statement - _RESPONSE_PROPAGATOR = propagator - - -class Setter(ABC): - @abstractmethod - def set(self, carrier, key, value): - """Inject the provided key value pair in carrier.""" - - -class DictHeaderSetter(Setter): - def set(self, carrier, key, value): # pylint: disable=no-self-use - old_value = carrier.get(key, "") - if old_value: - value = f"{old_value}, {value}" - carrier[key] = value - - -class FuncSetter(Setter): - """FuncSetter coverts a function into a valid Setter. Any function that can - set values in a carrier can be converted into a Setter by using FuncSetter. - This is useful when injecting trace context into non-dict objects such - HTTP Response objects for different framework. - - For example, it can be used to create a setter for Falcon response object as: - - setter = FuncSetter(falcon.api.Response.append_header) - - and then used with the propagator as: - - propagator.inject(falcon_response, setter=setter) - - This would essentially make the propagator call `falcon_response.append_header(key, value)` - """ - - def __init__(self, func): - self._func = func - - def set(self, carrier, key, value): - self._func(carrier, key, value) - - -default_setter = DictHeaderSetter() - - -class ResponsePropagator(ABC): - @abstractmethod - def inject( - self, - carrier: textmap.CarrierT, - context: typing.Optional[Context] = None, - setter: textmap.Setter = default_setter, - ) -> None: - """Injects SpanContext into the HTTP response carrier.""" - - -class TraceResponsePropagator(ResponsePropagator): - """Experimental propagator that injects tracecontext into HTTP responses.""" - - def inject( - self, - carrier: textmap.CarrierT, - context: typing.Optional[Context] = None, - setter: textmap.Setter = default_setter, - ) -> None: - """Injects SpanContext into the HTTP response carrier.""" - span = trace.get_current_span(context) - span_context = span.get_span_context() - if span_context == trace.INVALID_SPAN_CONTEXT: - return - - header_name = "traceresponse" - setter.set( - carrier, - header_name, - f"00-{format_trace_id(span_context.trace_id)}-{format_span_id(span_context.span_id)}-{span_context.trace_flags:02x}", - ) - setter.set( - carrier, - _HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, - header_name, - ) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py deleted file mode 100644 index 5f63b4af5e..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Dict, Sequence - -from wrapt import ObjectProxy - -# pylint: disable=unused-import -from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401 -from opentelemetry.trace import StatusCode - - -def extract_attributes_from_object( - obj: any, attributes: Sequence[str], existing: Dict[str, str] = None -) -> Dict[str, str]: - extracted = {} - if existing: - extracted.update(existing) - for attr in attributes: - value = getattr(obj, attr, None) - if value is not None: - extracted[attr] = str(value) - return extracted - - -def http_status_to_status_code( - status: int, allow_redirect: bool = True -) -> StatusCode: - """Converts an HTTP status code to an OpenTelemetry canonical status code - - Args: - status (int): HTTP status code - """ - # See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status - if status < 100: - return StatusCode.ERROR - if status <= 299: - return StatusCode.UNSET - if status <= 399 and allow_redirect: - return StatusCode.UNSET - return StatusCode.ERROR - - -def unwrap(obj, attr: str): - """Given a function that was wrapped by wrapt.wrap_function_wrapper, unwrap it - - Args: - obj: Object that holds a reference to the wrapped function - attr (str): Name of the wrapped function - """ - func = getattr(obj, attr, None) - if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"): - setattr(obj, attr, func.__wrapped__) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py deleted file mode 100644 index 2a05c9b361..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.25b0" diff --git a/opentelemetry-instrumentation/tests/__init__.py b/opentelemetry-instrumentation/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry-instrumentation/tests/test_bootstrap.py b/opentelemetry-instrumentation/tests/test_bootstrap.py deleted file mode 100644 index a266bf8a43..0000000000 --- a/opentelemetry-instrumentation/tests/test_bootstrap.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -from io import StringIO -from random import sample -from unittest import TestCase -from unittest.mock import call, patch - -from opentelemetry.instrumentation import bootstrap -from opentelemetry.instrumentation.bootstrap_gen import libraries - - -def sample_packages(packages, rate): - return sample( - list(packages), - int(len(packages) * rate), - ) - - -class TestBootstrap(TestCase): - - installed_libraries = {} - installed_instrumentations = {} - - @classmethod - def setUpClass(cls): - cls.installed_libraries = sample_packages( - [lib["instrumentation"] for lib in libraries.values()], 0.6 - ) - - # treat 50% of sampled packages as pre-installed - cls.installed_instrumentations = sample_packages( - cls.installed_libraries, 0.5 - ) - - cls.pkg_patcher = patch( - "opentelemetry.instrumentation.bootstrap._find_installed_libraries", - return_value=cls.installed_libraries, - ) - - cls.pip_install_patcher = patch( - "opentelemetry.instrumentation.bootstrap._sys_pip_install", - ) - cls.pip_check_patcher = patch( - "opentelemetry.instrumentation.bootstrap._pip_check", - ) - - cls.pkg_patcher.start() - cls.mock_pip_install = cls.pip_install_patcher.start() - cls.mock_pip_check = cls.pip_check_patcher.start() - - @classmethod - def tearDownClass(cls): - cls.pip_check_patcher.start() - cls.pip_install_patcher.start() - cls.pkg_patcher.stop() - - @patch("sys.argv", ["bootstrap", "-a", "pipenv"]) - def test_run_unknown_cmd(self): - with self.assertRaises(SystemExit): - bootstrap.run() - - @patch("sys.argv", ["bootstrap", "-a", "requirements"]) - def test_run_cmd_print(self): - with patch("sys.stdout", new=StringIO()) as fake_out: - bootstrap.run() - self.assertEqual( - fake_out.getvalue(), - "\n".join(self.installed_libraries), - ) - - @patch("sys.argv", ["bootstrap", "-a", "install"]) - def test_run_cmd_install(self): - bootstrap.run() - self.mock_pip_install.assert_has_calls( - [call(i) for i in self.installed_libraries], - any_order=True, - ) - self.assertEqual(self.mock_pip_check.call_count, 1) diff --git a/opentelemetry-instrumentation/tests/test_dependencies.py b/opentelemetry-instrumentation/tests/test_dependencies.py deleted file mode 100644 index a8acac62f4..0000000000 --- a/opentelemetry-instrumentation/tests/test_dependencies.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=protected-access - -import pkg_resources -import pytest - -from opentelemetry.instrumentation.dependencies import ( - DependencyConflict, - get_dependency_conflicts, - get_dist_dependency_conflicts, -) -from opentelemetry.test.test_base import TestBase - - -class TestDependencyConflicts(TestBase): - def setUp(self): - pass - - def test_get_dependency_conflicts_empty(self): - self.assertIsNone(get_dependency_conflicts([])) - - def test_get_dependency_conflicts_no_conflict(self): - self.assertIsNone(get_dependency_conflicts(["pytest"])) - - def test_get_dependency_conflicts_not_installed(self): - conflict = get_dependency_conflicts(["this-package-does-not-exist"]) - self.assertTrue(conflict is not None) - self.assertTrue(isinstance(conflict, DependencyConflict)) - self.assertEqual( - str(conflict), - 'DependencyConflict: requested: "this-package-does-not-exist" but found: "None"', - ) - - def test_get_dependency_conflicts_mismatched_version(self): - conflict = get_dependency_conflicts(["pytest == 5000"]) - self.assertTrue(conflict is not None) - self.assertTrue(isinstance(conflict, DependencyConflict)) - self.assertEqual( - str(conflict), - f'DependencyConflict: requested: "pytest == 5000" but found: "pytest {pytest.__version__}"', - ) - - def test_get_dist_dependency_conflicts(self): - def mock_requires(extras=()): - if "instruments" in extras: - return [ - pkg_resources.Requirement( - 'test-pkg ~= 1.0; extra == "instruments"' - ) - ] - return [] - - dist = pkg_resources.Distribution( - project_name="test-instrumentation", version="1.0" - ) - dist.requires = mock_requires - - conflict = get_dist_dependency_conflicts(dist) - self.assertTrue(conflict is not None) - self.assertTrue(isinstance(conflict, DependencyConflict)) - self.assertEqual( - str(conflict), - 'DependencyConflict: requested: "test-pkg~=1.0" but found: "None"', - ) diff --git a/opentelemetry-instrumentation/tests/test_distro.py b/opentelemetry-instrumentation/tests/test_distro.py deleted file mode 100644 index 399b3f8a65..0000000000 --- a/opentelemetry-instrumentation/tests/test_distro.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -from unittest import TestCase - -from pkg_resources import EntryPoint - -from opentelemetry.instrumentation.distro import BaseDistro -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor - - -class MockInstrumetor(BaseInstrumentor): - def instrumentation_dependencies(self): - return [] - - def _instrument(self, **kwargs): - pass - - def _uninstrument(self, **kwargs): - pass - - -class MockEntryPoint(EntryPoint): - def __init__(self, obj): # pylint: disable=super-init-not-called - self._obj = obj - - def load(self, *args, **kwargs): # pylint: disable=signature-differs - return self._obj - - -class MockDistro(BaseDistro): - def _configure(self, **kwargs): - pass - - -class TestDistro(TestCase): - def test_load_instrumentor(self): - # pylint: disable=protected-access - distro = MockDistro() - - instrumentor = MockInstrumetor() - entry_point = MockEntryPoint(MockInstrumetor) - - self.assertFalse(instrumentor._is_instrumented_by_opentelemetry) - distro.load_instrumentor(entry_point) - self.assertTrue(instrumentor._is_instrumented_by_opentelemetry) diff --git a/opentelemetry-instrumentation/tests/test_instrumentor.py b/opentelemetry-instrumentation/tests/test_instrumentor.py deleted file mode 100644 index dee32c34e4..0000000000 --- a/opentelemetry-instrumentation/tests/test_instrumentor.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -from logging import WARNING -from unittest import TestCase - -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor - - -class TestInstrumentor(TestCase): - class Instrumentor(BaseInstrumentor): - def _instrument(self, **kwargs): - return "instrumented" - - def _uninstrument(self, **kwargs): - return "uninstrumented" - - def instrumentation_dependencies(self): - return [] - - def test_protect(self): - instrumentor = self.Instrumentor() - - with self.assertLogs(level=WARNING): - self.assertIs(instrumentor.uninstrument(), None) - - self.assertEqual(instrumentor.instrument(), "instrumented") - - with self.assertLogs(level=WARNING): - self.assertIs(instrumentor.instrument(), None) - - self.assertEqual(instrumentor.uninstrument(), "uninstrumented") - - with self.assertLogs(level=WARNING): - self.assertIs(instrumentor.uninstrument(), None) - - def test_singleton(self): - self.assertIs(self.Instrumentor(), self.Instrumentor()) diff --git a/opentelemetry-instrumentation/tests/test_propagators.py b/opentelemetry-instrumentation/tests/test_propagators.py deleted file mode 100644 index 62461aafa9..0000000000 --- a/opentelemetry-instrumentation/tests/test_propagators.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=protected-access - -from opentelemetry import trace -from opentelemetry.instrumentation import propagators -from opentelemetry.instrumentation.propagators import ( - DictHeaderSetter, - TraceResponsePropagator, - get_global_response_propagator, - set_global_response_propagator, -) -from opentelemetry.test.test_base import TestBase - - -class TestGlobals(TestBase): - def test_get_set(self): - original = propagators._RESPONSE_PROPAGATOR - - propagators._RESPONSE_PROPAGATOR = None - self.assertIsNone(get_global_response_propagator()) - - prop = TraceResponsePropagator() - set_global_response_propagator(prop) - self.assertIs(prop, get_global_response_propagator()) - - propagators._RESPONSE_PROPAGATOR = original - - -class TestDictHeaderSetter(TestBase): - def test_simple(self): - setter = DictHeaderSetter() - carrier = {} - setter.set(carrier, "kk", "vv") - self.assertIn("kk", carrier) - self.assertEqual(carrier["kk"], "vv") - - def test_append(self): - setter = DictHeaderSetter() - carrier = {"kk": "old"} - setter.set(carrier, "kk", "vv") - self.assertIn("kk", carrier) - self.assertEqual(carrier["kk"], "old, vv") - - -class TestTraceResponsePropagator(TestBase): - def test_inject(self): - span = trace.NonRecordingSpan( - trace.SpanContext( - trace_id=1, - span_id=2, - is_remote=False, - trace_flags=trace.DEFAULT_TRACE_OPTIONS, - trace_state=trace.DEFAULT_TRACE_STATE, - ), - ) - - ctx = trace.set_span_in_context(span) - prop = TraceResponsePropagator() - carrier = {} - prop.inject(carrier, ctx) - self.assertEqual( - carrier["Access-Control-Expose-Headers"], "traceresponse" - ) - self.assertEqual( - carrier["traceresponse"], - "00-00000000000000000000000000000001-0000000000000002-00", - ) diff --git a/opentelemetry-instrumentation/tests/test_run.py b/opentelemetry-instrumentation/tests/test_run.py deleted file mode 100644 index 9fd3a21711..0000000000 --- a/opentelemetry-instrumentation/tests/test_run.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# type: ignore - -from os import environ, getcwd -from os.path import abspath, dirname, pathsep -from unittest import TestCase -from unittest.mock import patch - -from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER -from opentelemetry.instrumentation import auto_instrumentation - - -class TestRun(TestCase): - auto_instrumentation_path = dirname(abspath(auto_instrumentation.__file__)) - - @classmethod - def setUpClass(cls): - cls.execl_patcher = patch( - "opentelemetry.instrumentation.auto_instrumentation.execl" - ) - cls.which_patcher = patch( - "opentelemetry.instrumentation.auto_instrumentation.which" - ) - - cls.execl_patcher.start() - cls.which_patcher.start() - - @classmethod - def tearDownClass(cls): - cls.execl_patcher.stop() - cls.which_patcher.stop() - - @patch("sys.argv", ["instrument", ""]) - @patch.dict("os.environ", {"PYTHONPATH": ""}) - def test_empty(self): - auto_instrumentation.run() - self.assertEqual( - environ["PYTHONPATH"], - pathsep.join([self.auto_instrumentation_path, getcwd()]), - ) - - @patch("sys.argv", ["instrument", ""]) - @patch.dict("os.environ", {"PYTHONPATH": "abc"}) - def test_non_empty(self): - auto_instrumentation.run() - self.assertEqual( - environ["PYTHONPATH"], - pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), - ) - - @patch("sys.argv", ["instrument", ""]) - @patch.dict( - "os.environ", - {"PYTHONPATH": pathsep.join(["abc", auto_instrumentation_path])}, - ) - def test_after_path(self): - auto_instrumentation.run() - self.assertEqual( - environ["PYTHONPATH"], - pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), - ) - - @patch("sys.argv", ["instrument", ""]) - @patch.dict( - "os.environ", - { - "PYTHONPATH": pathsep.join( - [auto_instrumentation_path, "abc", auto_instrumentation_path] - ) - }, - ) - def test_single_path(self): - auto_instrumentation.run() - self.assertEqual( - environ["PYTHONPATH"], - pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), - ) - - -class TestExecl(TestCase): - @patch("sys.argv", ["1", "2", "3"]) - @patch("opentelemetry.instrumentation.auto_instrumentation.which") - @patch("opentelemetry.instrumentation.auto_instrumentation.execl") - def test_execl( - self, mock_execl, mock_which - ): # pylint: disable=no-self-use - mock_which.configure_mock(**{"return_value": "python"}) - - auto_instrumentation.run() - - mock_execl.assert_called_with("python", "python", "3") - - -class TestArgs(TestCase): - @patch("opentelemetry.instrumentation.auto_instrumentation.execl") - def test_exporter(self, _): # pylint: disable=no-self-use - with patch("sys.argv", ["instrument", "2"]): - auto_instrumentation.run() - self.assertIsNone(environ.get(OTEL_TRACES_EXPORTER)) - - with patch( - "sys.argv", - ["instrument", "--traces_exporter", "jaeger", "1", "2"], - ): - auto_instrumentation.run() - self.assertEqual(environ.get(OTEL_TRACES_EXPORTER), "jaeger") diff --git a/opentelemetry-instrumentation/tests/test_utils.py b/opentelemetry-instrumentation/tests/test_utils.py deleted file mode 100644 index e5246335c9..0000000000 --- a/opentelemetry-instrumentation/tests/test_utils.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from http import HTTPStatus - -from opentelemetry.instrumentation.utils import http_status_to_status_code -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace import StatusCode - - -class TestUtils(TestBase): - # See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status - def test_http_status_to_status_code(self): - for status_code, expected in ( - (HTTPStatus.OK, StatusCode.UNSET), - (HTTPStatus.ACCEPTED, StatusCode.UNSET), - (HTTPStatus.IM_USED, StatusCode.UNSET), - (HTTPStatus.MULTIPLE_CHOICES, StatusCode.UNSET), - (HTTPStatus.BAD_REQUEST, StatusCode.ERROR), - (HTTPStatus.UNAUTHORIZED, StatusCode.ERROR), - (HTTPStatus.FORBIDDEN, StatusCode.ERROR), - (HTTPStatus.NOT_FOUND, StatusCode.ERROR), - ( - HTTPStatus.UNPROCESSABLE_ENTITY, - StatusCode.ERROR, - ), - ( - HTTPStatus.TOO_MANY_REQUESTS, - StatusCode.ERROR, - ), - (HTTPStatus.NOT_IMPLEMENTED, StatusCode.ERROR), - (HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR), - ( - HTTPStatus.GATEWAY_TIMEOUT, - StatusCode.ERROR, - ), - ( - HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, - StatusCode.ERROR, - ), - (600, StatusCode.ERROR), - (99, StatusCode.ERROR), - ): - with self.subTest(status_code=status_code): - actual = http_status_to_status_code(int(status_code)) - self.assertEqual(actual, expected, status_code) diff --git a/scripts/build.sh b/scripts/build.sh index 2f40f1a003..63faa001bc 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-instrumentation/ opentelemetry-proto/ opentelemetry-distro/ opentelemetry-semantic-conventions/ exporter/*/ shim/*/ propagator/*/; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-proto/ opentelemetry-semantic-conventions/ exporter/*/ shim/*/ propagator/*/; do ( echo "building $d" cd "$d" diff --git a/tox.ini b/tox.ini index 0259cc9bf8..5398bdd80e 100644 --- a/tox.ini +++ b/tox.ini @@ -13,9 +13,6 @@ envlist = py3{6,7,8,9}-opentelemetry-sdk pypy3-opentelemetry-sdk - py3{6,7,8,9}-opentelemetry-instrumentation - pypy3-opentelemetry-instrumentation - py3{6,7,8,9}-opentelemetry-semantic-conventions pypy3-opentelemetry-semantic-conventions @@ -23,9 +20,6 @@ envlist = py3{6,7,8,9}-opentelemetry-getting-started pypy3-opentelemetry-getting-started - py3{6,7,8,9}-opentelemetry-distro - pypy3-opentelemetry-distro - py3{6,7,8,9}-opentelemetry-opentracing-shim pypy3-opentelemetry-opentracing-shim @@ -92,10 +86,8 @@ changedir = sdk: opentelemetry-sdk/tests protobuf: opentelemetry-proto/tests semantic-conventions: opentelemetry-semantic-conventions/tests - instrumentation: opentelemetry-instrumentation/tests getting-started: docs/getting_started/tests opentracing-shim: shim/opentelemetry-opentracing-shim/tests - distro: opentelemetry-distro/tests exporter-jaeger-combined: exporter/opentelemetry-exporter-jaeger/tests exporter-jaeger-proto-grpc: exporter/opentelemetry-exporter-jaeger-proto-grpc/tests @@ -116,17 +108,13 @@ commands_pre = py3{6,7,8,9}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. - opentelemetry: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-instrumentation {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util - - sdk: pip install {toxinidir}/opentelemetry-instrumentation - opentracing-shim: pip install {toxinidir}/opentelemetry-instrumentation + opentelemetry: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util protobuf: pip install {toxinidir}/opentelemetry-proto - distro: pip install {toxinidir}/opentelemetry-distro - instrumentation: pip install {toxinidir}/opentelemetry-instrumentation - getting-started: pip install requests==2.26.0 flask==2.0.1 -e {toxinidir}/opentelemetry-instrumentation + getting-started: pip install requests==2.26.0 flask==2.0.1 getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http" + getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation" getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-requests&subdirectory=instrumentation/opentelemetry-instrumentation-requests" getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-wsgi&subdirectory=instrumentation/opentelemetry-instrumentation-wsgi" getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-flask&subdirectory=instrumentation/opentelemetry-instrumentation-flask" @@ -145,9 +133,7 @@ commands_pre = exporter-otlp-proto-http: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http exporter-jaeger-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift {toxinidir}/exporter/opentelemetry-exporter-jaeger - exporter-jaeger-combined: pip install {toxinidir}/opentelemetry-instrumentation exporter-jaeger-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc - exporter-jaeger-proto-grpc: pip install {toxinidir}/opentelemetry-instrumentation exporter-jaeger-thrift: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift opentracing-shim: pip install {toxinidir}/opentelemetry-sdk @@ -204,7 +190,6 @@ deps = commands_pre = python -m pip install -e {toxinidir}/opentelemetry-api[test] python -m pip install -e {toxinidir}/opentelemetry-semantic-conventions[test] - python -m pip install -e {toxinidir}/opentelemetry-instrumentation[test] python -m pip install -e {toxinidir}/opentelemetry-sdk[test] python -m pip install -e {toxinidir}/opentelemetry-proto[test] python -m pip install -e {toxinidir}/tests/util[test] @@ -221,7 +206,6 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-b3[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-jaeger[test] - python -m pip install -e {toxinidir}/opentelemetry-distro[test] commands = python scripts/eachdist.py lint --check-only @@ -234,9 +218,6 @@ deps = changedir = docs -commands-pre = - python -m pip install {toxinidir}/opentelemetry-instrumentation - commands = sphinx-build -E -a -W -b html -T . _build/html @@ -252,9 +233,9 @@ deps = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-semantic-conventions \ - -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ -e "{env:CONTRIB_REPO}#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http" \ + -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation" \ -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-requests&subdirectory=instrumentation/opentelemetry-instrumentation-requests" \ -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-wsgi&subdirectory=instrumentation/opentelemetry-instrumentation-wsgi" @@ -272,7 +253,6 @@ changedir = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-semantic-conventions \ - -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/tests/util \ -e {toxinidir}/exporter/opentelemetry-exporter-opencensus \ From 39fe4dbc7a5b7f125fa9988ecdb7b26dc638cc9f Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 14 Oct 2021 22:03:13 -0400 Subject: [PATCH 1011/1517] Upgrade GRPC/protobuf related dependency, regenerate otlp protobufs (#2201) Co-authored-by: Owais Lone --- CHANGELOG.md | 3 + dev-requirements.txt | 8 +- .../collector/logs/v1/logs_service_pb2.py | 9 +- .../collector/logs/v1/logs_service_pb2.pyi | 14 +- .../logs/v1/logs_service_pb2_grpc.py | 4 +- .../metrics/v1/metrics_service_pb2.py | 9 +- .../metrics/v1/metrics_service_pb2.pyi | 14 +- .../metrics/v1/metrics_service_pb2_grpc.py | 4 +- .../collector/trace/v1/trace_service_pb2.py | 9 +- .../collector/trace/v1/trace_service_pb2.pyi | 14 +- .../trace/v1/trace_service_pb2_grpc.py | 4 +- .../proto/common/v1/common_pb2.py | 43 +- .../proto/common/v1/common_pb2.pyi | 65 +- .../opentelemetry/proto/logs/v1/logs_pb2.py | 996 +++++++----------- .../opentelemetry/proto/logs/v1/logs_pb2.pyi | 270 +++-- .../metrics/experimental/configservice_pb2.py | 267 ----- .../experimental/configservice_pb2.pyi | 88 -- .../experimental/configservice_pb2_grpc.py | 82 -- .../metrics_config_service_pb2.py | 33 +- .../metrics_config_service_pb2.pyi | 80 +- .../metrics_config_service_pb2_grpc.py | 6 +- .../proto/metrics/v1/metrics_pb2.py | 201 ++-- .../proto/metrics/v1/metrics_pb2.pyi | 842 +++++++++++++-- .../proto/resource/v1/resource_pb2.py | 8 +- .../proto/resource/v1/resource_pb2.pyi | 13 +- .../proto/trace/v1/trace_config_pb2.py | 43 +- .../proto/trace/v1/trace_config_pb2.pyi | 59 +- .../opentelemetry/proto/trace/v1/trace_pb2.py | 156 +-- .../proto/trace/v1/trace_pb2.pyi | 362 ++++++- pyproject.toml | 6 +- 30 files changed, 2070 insertions(+), 1642 deletions(-) delete mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.py delete mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi delete mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2_grpc.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7878136a0d..7335be2180 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.6.0-0.25b0...HEAD) +- Upgrade GRPC/protobuf related dependency and regenerate otlp protobufs + ([#2201](https://github.com/open-telemetry/opentelemetry-python/pull/2201)) + ## [1.6.0-0.25b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.0-0.25b0) - 2021-10-13 diff --git a/dev-requirements.txt b/dev-requirements.txt index bd369fac76..22595d5af2 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -6,10 +6,10 @@ httpretty~=1.0 mypy==0.812 sphinx~=3.5.4 sphinx-rtd-theme~=0.5 -sphinx-autodoc-typehints +sphinx-autodoc-typehints~=1.12.0 pytest>=6.0 pytest-cov>=2.8 readme-renderer~=24.0 -grpcio-tools==1.29.0 -mypy-protobuf>=1.23 -protobuf>=3.13.0 +grpcio-tools~=1.41.0 +mypy-protobuf~=3.0.0 +protobuf~=3.18.1 diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py index ade8e516c9..e315109d13 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/collector/logs/v1/logs_service.proto - +"""Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection @@ -19,6 +19,7 @@ package='opentelemetry.proto.collector.logs.v1', syntax='proto3', serialized_options=b'\n(io.opentelemetry.proto.collector.logs.v1B\020LogsServiceProtoP\001ZFgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/logs/v1', + create_key=_descriptor._internal_create_key, serialized_pb=b'\n8opentelemetry/proto/collector/logs/v1/logs_service.proto\x12%opentelemetry.proto.collector.logs.v1\x1a&opentelemetry/proto/logs/v1/logs.proto\"\\\n\x18\x45xportLogsServiceRequest\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\x1b\n\x19\x45xportLogsServiceResponse2\x9d\x01\n\x0bLogsService\x12\x8d\x01\n\x06\x45xport\x12?.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest\x1a@.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse\"\x00\x42\x86\x01\n(io.opentelemetry.proto.collector.logs.v1B\x10LogsServiceProtoP\x01ZFgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/logs/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_logs_dot_v1_dot_logs__pb2.DESCRIPTOR,]) @@ -32,6 +33,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='resource_logs', full_name='opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest.resource_logs', index=0, @@ -39,7 +41,7 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -63,6 +65,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ ], extensions=[ @@ -108,6 +111,7 @@ file=DESCRIPTOR, index=0, serialized_options=None, + create_key=_descriptor._internal_create_key, serialized_start=263, serialized_end=420, methods=[ @@ -119,6 +123,7 @@ input_type=_EXPORTLOGSSERVICEREQUEST, output_type=_EXPORTLOGSSERVICERESPONSE, serialized_options=None, + create_key=_descriptor._internal_create_key, ), ]) _sym_db.RegisterServiceDescriptor(_LOGSSERVICE) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi index da5d542e05..5940c192b2 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi @@ -15,20 +15,24 @@ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... class ExportLogsServiceRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... RESOURCE_LOGS_FIELD_NUMBER: builtins.int - @property - def resource_logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.logs.v1.logs_pb2.ResourceLogs]: ... - + def resource_logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.logs.v1.logs_pb2.ResourceLogs]: + """An array of ResourceLogs. + For data coming from a single resource this array will typically contain one + element. Intermediary nodes (such as OpenTelemetry Collector) that receive + data from multiple origins typically batch the data before forwarding further and + in that case this array will contain multiple elements. + """ + pass def __init__(self, *, resource_logs : typing.Optional[typing.Iterable[opentelemetry.proto.logs.v1.logs_pb2.ResourceLogs]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"resource_logs",b"resource_logs"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["resource_logs",b"resource_logs"]) -> None: ... global___ExportLogsServiceRequest = ExportLogsServiceRequest class ExportLogsServiceResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - def __init__(self, ) -> None: ... global___ExportLogsServiceResponse = ExportLogsServiceResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py index c929cdc84c..4d55e57778 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py @@ -1,4 +1,5 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" import grpc from opentelemetry.proto.collector.logs.v1 import logs_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2 @@ -64,6 +65,7 @@ def Export(request, options=(), channel_credentials=None, call_credentials=None, + insecure=False, compression=None, wait_for_ready=None, timeout=None, @@ -72,4 +74,4 @@ def Export(request, opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceRequest.SerializeToString, opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceResponse.FromString, options, channel_credentials, - call_credentials, compression, wait_for_ready, timeout, metadata) + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py index 96bb34bc8f..ba3c7902ff 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/collector/metrics/v1/metrics_service.proto - +"""Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection @@ -19,6 +19,7 @@ package='opentelemetry.proto.collector.metrics.v1', syntax='proto3', serialized_options=b'\n+io.opentelemetry.proto.collector.metrics.v1B\023MetricsServiceProtoP\001ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/metrics/v1', + create_key=_descriptor._internal_create_key, serialized_pb=b'\n>opentelemetry/proto/collector/metrics/v1/metrics_service.proto\x12(opentelemetry.proto.collector.metrics.v1\x1a,opentelemetry/proto/metrics/v1/metrics.proto\"h\n\x1b\x45xportMetricsServiceRequest\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\x1e\n\x1c\x45xportMetricsServiceResponse2\xac\x01\n\x0eMetricsService\x12\x99\x01\n\x06\x45xport\x12\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest\x1a\x46.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse\"\x00\x42\x8f\x01\n+io.opentelemetry.proto.collector.metrics.v1B\x13MetricsServiceProtoP\x01ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/metrics/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_metrics_dot_v1_dot_metrics__pb2.DESCRIPTOR,]) @@ -32,6 +33,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='resource_metrics', full_name='opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest.resource_metrics', index=0, @@ -39,7 +41,7 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -63,6 +65,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ ], extensions=[ @@ -108,6 +111,7 @@ file=DESCRIPTOR, index=0, serialized_options=None, + create_key=_descriptor._internal_create_key, serialized_start=293, serialized_end=465, methods=[ @@ -119,6 +123,7 @@ input_type=_EXPORTMETRICSSERVICEREQUEST, output_type=_EXPORTMETRICSSERVICERESPONSE, serialized_options=None, + create_key=_descriptor._internal_create_key, ), ]) _sym_db.RegisterServiceDescriptor(_METRICSSERVICE) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi index be50774a30..1acc1de3f3 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi @@ -15,20 +15,24 @@ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... class ExportMetricsServiceRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... RESOURCE_METRICS_FIELD_NUMBER: builtins.int - @property - def resource_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.metrics.v1.metrics_pb2.ResourceMetrics]: ... - + def resource_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.metrics.v1.metrics_pb2.ResourceMetrics]: + """An array of ResourceMetrics. + For data coming from a single resource this array will typically contain one + element. Intermediary nodes (such as OpenTelemetry Collector) that receive + data from multiple origins typically batch the data before forwarding further and + in that case this array will contain multiple elements. + """ + pass def __init__(self, *, resource_metrics : typing.Optional[typing.Iterable[opentelemetry.proto.metrics.v1.metrics_pb2.ResourceMetrics]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"resource_metrics",b"resource_metrics"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["resource_metrics",b"resource_metrics"]) -> None: ... global___ExportMetricsServiceRequest = ExportMetricsServiceRequest class ExportMetricsServiceResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - def __init__(self, ) -> None: ... global___ExportMetricsServiceResponse = ExportMetricsServiceResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py index c58f717616..c181c44641 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py @@ -1,4 +1,5 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" import grpc from opentelemetry.proto.collector.metrics.v1 import metrics_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2 @@ -64,6 +65,7 @@ def Export(request, options=(), channel_credentials=None, call_credentials=None, + insecure=False, compression=None, wait_for_ready=None, timeout=None, @@ -72,4 +74,4 @@ def Export(request, opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceRequest.SerializeToString, opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceResponse.FromString, options, channel_credentials, - call_credentials, compression, wait_for_ready, timeout, metadata) + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py index ccdfb345d9..4648414b79 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/collector/trace/v1/trace_service.proto - +"""Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection @@ -19,6 +19,7 @@ package='opentelemetry.proto.collector.trace.v1', syntax='proto3', serialized_options=b'\n)io.opentelemetry.proto.collector.trace.v1B\021TraceServiceProtoP\001ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1', + create_key=_descriptor._internal_create_key, serialized_pb=b'\n:opentelemetry/proto/collector/trace/v1/trace_service.proto\x12&opentelemetry.proto.collector.trace.v1\x1a(opentelemetry/proto/trace/v1/trace.proto\"`\n\x19\x45xportTraceServiceRequest\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\x1c\n\x1a\x45xportTraceServiceResponse2\xa2\x01\n\x0cTraceService\x12\x91\x01\n\x06\x45xport\x12\x41.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\x1a\x42.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse\"\x00\x42\x89\x01\n)io.opentelemetry.proto.collector.trace.v1B\x11TraceServiceProtoP\x01ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_trace_dot_v1_dot_trace__pb2.DESCRIPTOR,]) @@ -32,6 +33,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='resource_spans', full_name='opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest.resource_spans', index=0, @@ -39,7 +41,7 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -63,6 +65,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ ], extensions=[ @@ -108,6 +111,7 @@ file=DESCRIPTOR, index=0, serialized_options=None, + create_key=_descriptor._internal_create_key, serialized_start=273, serialized_end=435, methods=[ @@ -119,6 +123,7 @@ input_type=_EXPORTTRACESERVICEREQUEST, output_type=_EXPORTTRACESERVICERESPONSE, serialized_options=None, + create_key=_descriptor._internal_create_key, ), ]) _sym_db.RegisterServiceDescriptor(_TRACESERVICE) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi index f40bd9a179..7ed93e76de 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi @@ -15,20 +15,24 @@ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... class ExportTraceServiceRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... RESOURCE_SPANS_FIELD_NUMBER: builtins.int - @property - def resource_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.trace.v1.trace_pb2.ResourceSpans]: ... - + def resource_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.trace.v1.trace_pb2.ResourceSpans]: + """An array of ResourceSpans. + For data coming from a single resource this array will typically contain one + element. Intermediary nodes (such as OpenTelemetry Collector) that receive + data from multiple origins typically batch the data before forwarding further and + in that case this array will contain multiple elements. + """ + pass def __init__(self, *, resource_spans : typing.Optional[typing.Iterable[opentelemetry.proto.trace.v1.trace_pb2.ResourceSpans]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"resource_spans",b"resource_spans"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["resource_spans",b"resource_spans"]) -> None: ... global___ExportTraceServiceRequest = ExportTraceServiceRequest class ExportTraceServiceResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - def __init__(self, ) -> None: ... global___ExportTraceServiceResponse = ExportTraceServiceResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py index 3b9efef6d6..81dbbe59f3 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py @@ -1,4 +1,5 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" import grpc from opentelemetry.proto.collector.trace.v1 import trace_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2 @@ -64,6 +65,7 @@ def Export(request, options=(), channel_credentials=None, call_credentials=None, + insecure=False, compression=None, wait_for_ready=None, timeout=None, @@ -72,4 +74,4 @@ def Export(request, opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceRequest.SerializeToString, opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceResponse.FromString, options, channel_credentials, - call_credentials, compression, wait_for_ready, timeout, metadata) + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py index 5371e9facf..4578f9409f 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/common/v1/common.proto - +"""Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection @@ -18,6 +18,7 @@ package='opentelemetry.proto.common.v1', syntax='proto3', serialized_options=b'\n io.opentelemetry.proto.common.v1B\013CommonProtoP\001Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1', + create_key=_descriptor._internal_create_key, serialized_pb=b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\x8c\x02\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"O\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"0\n\x0eStringKeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x18\x01\"7\n\x16InstrumentationLibrary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\tBq\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1b\x06proto3' ) @@ -30,6 +31,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='string_value', full_name='opentelemetry.proto.common.v1.AnyValue.string_value', index=0, @@ -37,49 +39,49 @@ has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='bool_value', full_name='opentelemetry.proto.common.v1.AnyValue.bool_value', index=1, number=2, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='int_value', full_name='opentelemetry.proto.common.v1.AnyValue.int_value', index=2, number=3, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='double_value', full_name='opentelemetry.proto.common.v1.AnyValue.double_value', index=3, number=4, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='array_value', full_name='opentelemetry.proto.common.v1.AnyValue.array_value', index=4, number=5, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='kvlist_value', full_name='opentelemetry.proto.common.v1.AnyValue.kvlist_value', index=5, number=6, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='bytes_value', full_name='opentelemetry.proto.common.v1.AnyValue.bytes_value', index=6, number=7, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -93,7 +95,9 @@ oneofs=[ _descriptor.OneofDescriptor( name='value', full_name='opentelemetry.proto.common.v1.AnyValue.value', - index=0, containing_type=None, fields=[]), + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], serialized_start=78, serialized_end=346, @@ -106,6 +110,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='values', full_name='opentelemetry.proto.common.v1.ArrayValue.values', index=0, @@ -113,7 +118,7 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -137,6 +142,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='values', full_name='opentelemetry.proto.common.v1.KeyValueList.values', index=0, @@ -144,7 +150,7 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -168,6 +174,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='key', full_name='opentelemetry.proto.common.v1.KeyValue.key', index=0, @@ -175,14 +182,14 @@ has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='value', full_name='opentelemetry.proto.common.v1.KeyValue.value', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -206,6 +213,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='key', full_name='opentelemetry.proto.common.v1.StringKeyValue.key', index=0, @@ -213,14 +221,14 @@ has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='value', full_name='opentelemetry.proto.common.v1.StringKeyValue.value', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -244,6 +252,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='opentelemetry.proto.common.v1.InstrumentationLibrary.name', index=0, @@ -251,14 +260,14 @@ has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='version', full_name='opentelemetry.proto.common.v1.InstrumentationLibrary.version', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi index 144df2d4a0..54789ef893 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi @@ -12,6 +12,10 @@ import typing_extensions DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... class AnyValue(google.protobuf.message.Message): + """AnyValue is used to represent any type of attribute value. AnyValue may contain a + primitive value such as a string or integer or it may contain an arbitrary nested + object containing arrays, key-value lists and primitives. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... STRING_VALUE_FIELD_NUMBER: builtins.int BOOL_VALUE_FIELD_NUMBER: builtins.int @@ -24,14 +28,11 @@ class AnyValue(google.protobuf.message.Message): bool_value: builtins.bool = ... int_value: builtins.int = ... double_value: builtins.float = ... - bytes_value: builtins.bytes = ... - @property def array_value(self) -> global___ArrayValue: ... - @property def kvlist_value(self) -> global___KeyValueList: ... - + bytes_value: builtins.bytes = ... def __init__(self, *, string_value : typing.Text = ..., @@ -42,83 +43,101 @@ class AnyValue(google.protobuf.message.Message): kvlist_value : typing.Optional[global___KeyValueList] = ..., bytes_value : builtins.bytes = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"bytes_value",b"bytes_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"array_value",b"array_value",u"bool_value",b"bool_value",u"bytes_value",b"bytes_value",u"double_value",b"double_value",u"int_value",b"int_value",u"kvlist_value",b"kvlist_value",u"string_value",b"string_value",u"value",b"value"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal[u"value",b"value"]) -> typing_extensions.Literal["string_value","bool_value","int_value","double_value","array_value","kvlist_value","bytes_value"]: ... + def HasField(self, field_name: typing_extensions.Literal["array_value",b"array_value","bool_value",b"bool_value","bytes_value",b"bytes_value","double_value",b"double_value","int_value",b"int_value","kvlist_value",b"kvlist_value","string_value",b"string_value","value",b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["array_value",b"array_value","bool_value",b"bool_value","bytes_value",b"bytes_value","double_value",b"double_value","int_value",b"int_value","kvlist_value",b"kvlist_value","string_value",b"string_value","value",b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["value",b"value"]) -> typing.Optional[typing_extensions.Literal["string_value","bool_value","int_value","double_value","array_value","kvlist_value","bytes_value"]]: ... global___AnyValue = AnyValue class ArrayValue(google.protobuf.message.Message): + """ArrayValue is a list of AnyValue messages. We need ArrayValue as a message + since oneof in AnyValue does not allow repeated fields. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... VALUES_FIELD_NUMBER: builtins.int - @property - def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___AnyValue]: ... - + def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___AnyValue]: + """Array of values. The array may be empty (contain 0 elements).""" + pass def __init__(self, *, values : typing.Optional[typing.Iterable[global___AnyValue]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"values",b"values"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["values",b"values"]) -> None: ... global___ArrayValue = ArrayValue class KeyValueList(google.protobuf.message.Message): + """KeyValueList is a list of KeyValue messages. We need KeyValueList as a message + since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need + a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to + avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches + are semantically equivalent. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... VALUES_FIELD_NUMBER: builtins.int - @property - def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___KeyValue]: ... - + def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___KeyValue]: + """A collection of key/value pairs of key-value pairs. The list may be empty (may + contain 0 elements). + """ + pass def __init__(self, *, values : typing.Optional[typing.Iterable[global___KeyValue]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"values",b"values"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["values",b"values"]) -> None: ... global___KeyValueList = KeyValueList class KeyValue(google.protobuf.message.Message): + """KeyValue is a key-value pair that is used to store Span attributes, Link + attributes, etc. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int key: typing.Text = ... - @property def value(self) -> global___AnyValue: ... - def __init__(self, *, key : typing.Text = ..., value : typing.Optional[global___AnyValue] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"value",b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"key",b"key",u"value",b"value"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value",b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key",b"key","value",b"value"]) -> None: ... global___KeyValue = KeyValue class StringKeyValue(google.protobuf.message.Message): + """StringKeyValue is a pair of key/value strings. This is the simpler (and faster) version + of KeyValue that only supports string values. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int key: typing.Text = ... value: typing.Text = ... - def __init__(self, *, key : typing.Text = ..., value : typing.Text = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"key",b"key",u"value",b"value"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key",b"key","value",b"value"]) -> None: ... global___StringKeyValue = StringKeyValue class InstrumentationLibrary(google.protobuf.message.Message): + """InstrumentationLibrary is a message representing the instrumentation library information + such as the fully qualified name and version. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... NAME_FIELD_NUMBER: builtins.int VERSION_FIELD_NUMBER: builtins.int name: typing.Text = ... - version: typing.Text = ... + """An empty instrumentation library name means the name is unknown.""" + version: typing.Text = ... def __init__(self, *, name : typing.Text = ..., version : typing.Text = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"name",b"name",u"version",b"version"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["name",b"name","version",b"version"]) -> None: ... global___InstrumentationLibrary = InstrumentationLibrary diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py index b9a8468d6c..31adc70f13 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py @@ -1,253 +1,194 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/logs/v1/logs.proto - +"""Generated protocol buffer code.""" from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor 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() -from opentelemetry.proto.common.v1 import ( - common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2, -) -from opentelemetry.proto.resource.v1 import ( - resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2, -) +from opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 +from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 DESCRIPTOR = _descriptor.FileDescriptor( - name="opentelemetry/proto/logs/v1/logs.proto", - package="opentelemetry.proto.logs.v1", - syntax="proto3", - serialized_options=b"\n\036io.opentelemetry.proto.logs.v1B\tLogsProtoP\001Z opentelemetry.proto.resource.v1.resource_pb2.Resource: ... + def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: + """The resource for the logs in this message. + If this field is not set then resource info is unknown. + """ + pass @property - def instrumentation_library_logs( - self, - ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ - global___InstrumentationLibraryLogs - ]: ... - def __init__( - self, - *, - resource: typing.Optional[ - opentelemetry.proto.resource.v1.resource_pb2.Resource - ] = ..., - instrumentation_library_logs: typing.Optional[ - typing.Iterable[global___InstrumentationLibraryLogs] - ] = ..., - schema_url: typing.Text = ..., - ) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["resource", b"resource"] - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "instrumentation_library_logs", - b"instrumentation_library_logs", - "resource", - b"resource", - "schema_url", - b"schema_url", - ], - ) -> None: ... + def instrumentation_library_logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibraryLogs]: + """A list of InstrumentationLibraryLogs that originate from a resource.""" + pass + schema_url: typing.Text = ... + """This schema_url applies to the data in the "resource" field. It does not apply + to the data in the "instrumentation_library_logs" field which have their own + schema_url field. + """ + def __init__(self, + *, + resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., + instrumentation_library_logs : typing.Optional[typing.Iterable[global___InstrumentationLibraryLogs]] = ..., + schema_url : typing.Text = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["resource",b"resource"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library_logs",b"instrumentation_library_logs","resource",b"resource","schema_url",b"schema_url"]) -> None: ... global___ResourceLogs = ResourceLogs class InstrumentationLibraryLogs(google.protobuf.message.Message): + """A collection of Logs produced by an InstrumentationLibrary.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int LOGS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int - schema_url: typing.Text = ... @property - def instrumentation_library( - self, - ) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: ... + def instrumentation_library(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: + """The instrumentation library information for the logs in this message. + Semantically when InstrumentationLibrary isn't set, it is equivalent with + an empty instrumentation library name (unknown). + """ + pass @property - def logs( - self, - ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ - global___LogRecord - ]: ... - def __init__( - self, - *, - instrumentation_library: typing.Optional[ - opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary - ] = ..., - logs: typing.Optional[typing.Iterable[global___LogRecord]] = ..., - schema_url: typing.Text = ..., - ) -> None: ... - def HasField( - self, - field_name: typing_extensions.Literal[ - "instrumentation_library", b"instrumentation_library" - ], - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "instrumentation_library", - b"instrumentation_library", - "logs", - b"logs", - "schema_url", - b"schema_url", - ], - ) -> None: ... + def logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___LogRecord]: + """A list of log records.""" + pass + schema_url: typing.Text = ... + """This schema_url applies to all logs in the "logs" field.""" + def __init__(self, + *, + instrumentation_library : typing.Optional[opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary] = ..., + logs : typing.Optional[typing.Iterable[global___LogRecord]] = ..., + schema_url : typing.Text = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library","logs",b"logs","schema_url",b"schema_url"]) -> None: ... global___InstrumentationLibraryLogs = InstrumentationLibraryLogs class LogRecord(google.protobuf.message.Message): + """A log record according to OpenTelemetry Log Data Model: + https://github.com/open-telemetry/oteps/blob/main/text/logs/0097-log-data-model.md + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... TIME_UNIX_NANO_FIELD_NUMBER: builtins.int SEVERITY_NUMBER_FIELD_NUMBER: builtins.int @@ -198,66 +172,74 @@ class LogRecord(google.protobuf.message.Message): TRACE_ID_FIELD_NUMBER: builtins.int SPAN_ID_FIELD_NUMBER: builtins.int time_unix_nano: builtins.int = ... + """time_unix_nano is the time when the event occurred. + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + Value of 0 indicates unknown or missing timestamp. + """ + severity_number: global___SeverityNumber.V = ... + """Numerical value of the severity, normalized to values described in Log Data Model. + [Optional]. + """ + severity_text: typing.Text = ... + """The severity text (also known as log level). The original string representation as + it is known at the source. [Optional]. + """ + name: typing.Text = ... + """Short event identifier that does not contain varying parts. Name describes + what happened (e.g. "ProcessStarted"). Recommended to be no longer than 50 + characters. Not guaranteed to be unique in any way. [Optional]. + """ + + @property + def body(self) -> opentelemetry.proto.common.v1.common_pb2.AnyValue: + """A value containing the body of the log record. Can be for example a human-readable + string message (including multi-line) describing the event in a free form or it can + be a structured data composed of arrays and maps of other values. [Optional]. + """ + pass + @property + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: + """Additional attributes that describe the specific event occurrence. [Optional].""" + pass dropped_attributes_count: builtins.int = ... flags: builtins.int = ... + """Flags, a bit field. 8 least significant bits are the trace flags as + defined in W3C Trace Context specification. 24 most significant bits are reserved + and must be set to 0. Readers must not assume that 24 most significant bits + will be zero and must correctly mask the bits when reading 8-bit trace flag (use + flags & TRACE_FLAGS_MASK). [Optional]. + """ + trace_id: builtins.bytes = ... + """A unique identifier for a trace. All logs from the same trace share + the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes + is considered invalid. Can be set for logs that are part of request processing + and have an assigned trace id. [Optional]. + """ + span_id: builtins.bytes = ... - @property - def body(self) -> opentelemetry.proto.common.v1.common_pb2.AnyValue: ... - @property - def attributes( - self, - ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ - opentelemetry.proto.common.v1.common_pb2.KeyValue - ]: ... - def __init__( - self, - *, - time_unix_nano: builtins.int = ..., - severity_number: global___SeverityNumber.V = ..., - severity_text: typing.Text = ..., - name: typing.Text = ..., - body: typing.Optional[ - opentelemetry.proto.common.v1.common_pb2.AnyValue - ] = ..., - attributes: typing.Optional[ - typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] - ] = ..., - dropped_attributes_count: builtins.int = ..., - flags: builtins.int = ..., - trace_id: builtins.bytes = ..., - span_id: builtins.bytes = ..., - ) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["body", b"body"] - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "attributes", - b"attributes", - "body", - b"body", - "dropped_attributes_count", - b"dropped_attributes_count", - "flags", - b"flags", - "name", - b"name", - "severity_number", - b"severity_number", - "severity_text", - b"severity_text", - "span_id", - b"span_id", - "time_unix_nano", - b"time_unix_nano", - "trace_id", - b"trace_id", - ], - ) -> None: ... + """A unique identifier for a span within a trace, assigned when the span + is created. The ID is an 8-byte array. An ID with all zeroes is considered + invalid. Can be set for logs that are part of a particular processing span. + If span_id is present trace_id SHOULD be also present. [Optional]. + """ + def __init__(self, + *, + time_unix_nano : builtins.int = ..., + severity_number : global___SeverityNumber.V = ..., + severity_text : typing.Text = ..., + name : typing.Text = ..., + body : typing.Optional[opentelemetry.proto.common.v1.common_pb2.AnyValue] = ..., + attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., + dropped_attributes_count : builtins.int = ..., + flags : builtins.int = ..., + trace_id : builtins.bytes = ..., + span_id : builtins.bytes = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["body",b"body"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","body",b"body","dropped_attributes_count",b"dropped_attributes_count","flags",b"flags","name",b"name","severity_number",b"severity_number","severity_text",b"severity_text","span_id",b"span_id","time_unix_nano",b"time_unix_nano","trace_id",b"trace_id"]) -> None: ... global___LogRecord = LogRecord diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.py deleted file mode 100644 index c2a8deb607..0000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.py +++ /dev/null @@ -1,267 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: opentelemetry/proto/metrics/experimental/configservice.proto - -from google.protobuf import descriptor as _descriptor -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() - - -from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='opentelemetry/proto/metrics/experimental/configservice.proto', - package='opentelemetry.proto.metrics.experimental', - syntax='proto3', - serialized_options=b'\n+io.opentelemetry.proto.metrics.experimentalB\030MetricConfigServiceProtoP\001ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/metrics/experimental', - serialized_pb=b'\n.opentelemetry.proto.metrics.experimental.MetricConfigResponseB\x94\x01\n+io.opentelemetry.proto.metrics.experimentalB\x18MetricConfigServiceProtoP\x01ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/metrics/experimentalb\x06proto3' - , - dependencies=[opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) - - - - -_METRICCONFIGREQUEST = _descriptor.Descriptor( - name='MetricConfigRequest', - full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='resource', full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest.resource', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='last_known_fingerprint', full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest.last_known_fingerprint', index=1, - number=2, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=154, - serialized_end=268, -) - - -_METRICCONFIGRESPONSE_SCHEDULE_PATTERN = _descriptor.Descriptor( - name='Pattern', - full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='equals', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.equals', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='starts_with', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.starts_with', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='match', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.match', - index=0, containing_type=None, fields=[]), - ], - serialized_start=692, - serialized_end=751, -) - -_METRICCONFIGRESPONSE_SCHEDULE = _descriptor.Descriptor( - name='Schedule', - full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='exclusion_patterns', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.exclusion_patterns', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='inclusion_patterns', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.inclusion_patterns', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='period_sec', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.period_sec', index=2, - number=3, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_METRICCONFIGRESPONSE_SCHEDULE_PATTERN, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=442, - serialized_end=751, -) - -_METRICCONFIGRESPONSE = _descriptor.Descriptor( - name='MetricConfigResponse', - full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='fingerprint', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.fingerprint', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='schedules', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.schedules', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='suggested_wait_time_sec', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.suggested_wait_time_sec', index=2, - number=3, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_METRICCONFIGRESPONSE_SCHEDULE, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=271, - serialized_end=751, -) - -_METRICCONFIGREQUEST.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE -_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.containing_type = _METRICCONFIGRESPONSE_SCHEDULE -_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'].fields.append( - _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['equals']) -_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['equals'].containing_oneof = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'] -_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'].fields.append( - _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['starts_with']) -_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['starts_with'].containing_oneof = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'] -_METRICCONFIGRESPONSE_SCHEDULE.fields_by_name['exclusion_patterns'].message_type = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN -_METRICCONFIGRESPONSE_SCHEDULE.fields_by_name['inclusion_patterns'].message_type = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN -_METRICCONFIGRESPONSE_SCHEDULE.containing_type = _METRICCONFIGRESPONSE -_METRICCONFIGRESPONSE.fields_by_name['schedules'].message_type = _METRICCONFIGRESPONSE_SCHEDULE -DESCRIPTOR.message_types_by_name['MetricConfigRequest'] = _METRICCONFIGREQUEST -DESCRIPTOR.message_types_by_name['MetricConfigResponse'] = _METRICCONFIGRESPONSE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -MetricConfigRequest = _reflection.GeneratedProtocolMessageType('MetricConfigRequest', (_message.Message,), { - 'DESCRIPTOR' : _METRICCONFIGREQUEST, - '__module__' : 'opentelemetry.proto.metrics.experimental.configservice_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigRequest) - }) -_sym_db.RegisterMessage(MetricConfigRequest) - -MetricConfigResponse = _reflection.GeneratedProtocolMessageType('MetricConfigResponse', (_message.Message,), { - - 'Schedule' : _reflection.GeneratedProtocolMessageType('Schedule', (_message.Message,), { - - 'Pattern' : _reflection.GeneratedProtocolMessageType('Pattern', (_message.Message,), { - 'DESCRIPTOR' : _METRICCONFIGRESPONSE_SCHEDULE_PATTERN, - '__module__' : 'opentelemetry.proto.metrics.experimental.configservice_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern) - }) - , - 'DESCRIPTOR' : _METRICCONFIGRESPONSE_SCHEDULE, - '__module__' : 'opentelemetry.proto.metrics.experimental.configservice_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule) - }) - , - 'DESCRIPTOR' : _METRICCONFIGRESPONSE, - '__module__' : 'opentelemetry.proto.metrics.experimental.configservice_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigResponse) - }) -_sym_db.RegisterMessage(MetricConfigResponse) -_sym_db.RegisterMessage(MetricConfigResponse.Schedule) -_sym_db.RegisterMessage(MetricConfigResponse.Schedule.Pattern) - - -DESCRIPTOR._options = None - -_METRICCONFIG = _descriptor.ServiceDescriptor( - name='MetricConfig', - full_name='opentelemetry.proto.metrics.experimental.MetricConfig', - file=DESCRIPTOR, - index=0, - serialized_options=None, - serialized_start=754, - serialized_end=915, - methods=[ - _descriptor.MethodDescriptor( - name='GetMetricConfig', - full_name='opentelemetry.proto.metrics.experimental.MetricConfig.GetMetricConfig', - index=0, - containing_service=None, - input_type=_METRICCONFIGREQUEST, - output_type=_METRICCONFIGRESPONSE, - serialized_options=None, - ), -]) -_sym_db.RegisterServiceDescriptor(_METRICCONFIG) - -DESCRIPTOR.services_by_name['MetricConfig'] = _METRICCONFIG - -# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi deleted file mode 100644 index 7218e03264..0000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2.pyi +++ /dev/null @@ -1,88 +0,0 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -""" -import builtins -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.message -import opentelemetry.proto.resource.v1.resource_pb2 -import typing -import typing_extensions - -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... - -class MetricConfigRequest(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - RESOURCE_FIELD_NUMBER: builtins.int - LAST_KNOWN_FINGERPRINT_FIELD_NUMBER: builtins.int - last_known_fingerprint: builtins.bytes = ... - - @property - def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: ... - - def __init__(self, - *, - resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., - last_known_fingerprint : builtins.bytes = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"resource",b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"last_known_fingerprint",b"last_known_fingerprint",u"resource",b"resource"]) -> None: ... -global___MetricConfigRequest = MetricConfigRequest - -class MetricConfigResponse(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - class Schedule(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - class Pattern(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - EQUALS_FIELD_NUMBER: builtins.int - STARTS_WITH_FIELD_NUMBER: builtins.int - equals: typing.Text = ... - starts_with: typing.Text = ... - - def __init__(self, - *, - equals : typing.Text = ..., - starts_with : typing.Text = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal[u"match",b"match"]) -> typing_extensions.Literal["equals","starts_with"]: ... - - EXCLUSION_PATTERNS_FIELD_NUMBER: builtins.int - INCLUSION_PATTERNS_FIELD_NUMBER: builtins.int - PERIOD_SEC_FIELD_NUMBER: builtins.int - period_sec: builtins.int = ... - - @property - def exclusion_patterns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule.Pattern]: ... - - @property - def inclusion_patterns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule.Pattern]: ... - - def __init__(self, - *, - exclusion_patterns : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule.Pattern]] = ..., - inclusion_patterns : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule.Pattern]] = ..., - period_sec : builtins.int = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"exclusion_patterns",b"exclusion_patterns",u"inclusion_patterns",b"inclusion_patterns",u"period_sec",b"period_sec"]) -> None: ... - - FINGERPRINT_FIELD_NUMBER: builtins.int - SCHEDULES_FIELD_NUMBER: builtins.int - SUGGESTED_WAIT_TIME_SEC_FIELD_NUMBER: builtins.int - fingerprint: builtins.bytes = ... - suggested_wait_time_sec: builtins.int = ... - - @property - def schedules(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule]: ... - - def __init__(self, - *, - fingerprint : builtins.bytes = ..., - schedules : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule]] = ..., - suggested_wait_time_sec : builtins.int = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"fingerprint",b"fingerprint",u"schedules",b"schedules",u"suggested_wait_time_sec",b"suggested_wait_time_sec"]) -> None: ... -global___MetricConfigResponse = MetricConfigResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2_grpc.py deleted file mode 100644 index a2b2408446..0000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/configservice_pb2_grpc.py +++ /dev/null @@ -1,82 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -import grpc - -from opentelemetry.proto.metrics.experimental import configservice_pb2 as opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2 - - -class MetricConfigStub(object): - """MetricConfig is a service that enables updating metric schedules, trace - parameters, and other configurations on the SDK without having to restart the - instrumented application. The collector can also serve as the configuration - service, acting as a bridge between third-party configuration services and - the SDK, piping updated configs from a third-party source to an instrumented - application. - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.GetMetricConfig = channel.unary_unary( - '/opentelemetry.proto.metrics.experimental.MetricConfig/GetMetricConfig', - request_serializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2.MetricConfigRequest.SerializeToString, - response_deserializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2.MetricConfigResponse.FromString, - ) - - -class MetricConfigServicer(object): - """MetricConfig is a service that enables updating metric schedules, trace - parameters, and other configurations on the SDK without having to restart the - instrumented application. The collector can also serve as the configuration - service, acting as a bridge between third-party configuration services and - the SDK, piping updated configs from a third-party source to an instrumented - application. - """ - - def GetMetricConfig(self, request, context): - """Missing associated documentation comment in .proto file""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_MetricConfigServicer_to_server(servicer, server): - rpc_method_handlers = { - 'GetMetricConfig': grpc.unary_unary_rpc_method_handler( - servicer.GetMetricConfig, - request_deserializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2.MetricConfigRequest.FromString, - response_serializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2.MetricConfigResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'opentelemetry.proto.metrics.experimental.MetricConfig', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - - - # This class is part of an EXPERIMENTAL API. -class MetricConfig(object): - """MetricConfig is a service that enables updating metric schedules, trace - parameters, and other configurations on the SDK without having to restart the - instrumented application. The collector can also serve as the configuration - service, acting as a bridge between third-party configuration services and - the SDK, piping updated configs from a third-party source to an instrumented - application. - """ - - @staticmethod - def GetMetricConfig(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary(request, target, '/opentelemetry.proto.metrics.experimental.MetricConfig/GetMetricConfig', - opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2.MetricConfigRequest.SerializeToString, - opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_configservice__pb2.MetricConfigResponse.FromString, - options, channel_credentials, - call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.py index 8f1b43c3d2..212840ca03 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/metrics/experimental/metrics_config_service.proto - +"""Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection @@ -19,6 +19,7 @@ package='opentelemetry.proto.metrics.experimental', syntax='proto3', serialized_options=b'\n+io.opentelemetry.proto.metrics.experimentalB\030MetricConfigServiceProtoP\001ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/metrics/experimental', + create_key=_descriptor._internal_create_key, serialized_pb=b'\nEopentelemetry/proto/metrics/experimental/metrics_config_service.proto\x12(opentelemetry.proto.metrics.experimental\x1a.opentelemetry/proto/resource/v1/resource.proto\"r\n\x13MetricConfigRequest\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x1e\n\x16last_known_fingerprint\x18\x02 \x01(\x0c\"\xe0\x03\n\x14MetricConfigResponse\x12\x13\n\x0b\x66ingerprint\x18\x01 \x01(\x0c\x12Z\n\tschedules\x18\x02 \x03(\x0b\x32G.opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule\x12\x1f\n\x17suggested_wait_time_sec\x18\x03 \x01(\x05\x1a\xb5\x02\n\x08Schedule\x12k\n\x12\x65xclusion_patterns\x18\x01 \x03(\x0b\x32O.opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern\x12k\n\x12inclusion_patterns\x18\x02 \x03(\x0b\x32O.opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern\x12\x12\n\nperiod_sec\x18\x03 \x01(\x05\x1a;\n\x07Pattern\x12\x10\n\x06\x65quals\x18\x01 \x01(\tH\x00\x12\x15\n\x0bstarts_with\x18\x02 \x01(\tH\x00\x42\x07\n\x05match2\xa1\x01\n\x0cMetricConfig\x12\x90\x01\n\x0fGetMetricConfig\x12=.opentelemetry.proto.metrics.experimental.MetricConfigRequest\x1a>.opentelemetry.proto.metrics.experimental.MetricConfigResponseB\x94\x01\n+io.opentelemetry.proto.metrics.experimentalB\x18MetricConfigServiceProtoP\x01ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/metrics/experimentalb\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -32,6 +33,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='resource', full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest.resource', index=0, @@ -39,14 +41,14 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='last_known_fingerprint', full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest.last_known_fingerprint', index=1, number=2, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -70,6 +72,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='equals', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.equals', index=0, @@ -77,14 +80,14 @@ has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='starts_with', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.starts_with', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -98,7 +101,9 @@ oneofs=[ _descriptor.OneofDescriptor( name='match', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.match', - index=0, containing_type=None, fields=[]), + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], serialized_start=701, serialized_end=760, @@ -110,6 +115,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='exclusion_patterns', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.exclusion_patterns', index=0, @@ -117,21 +123,21 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='inclusion_patterns', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.inclusion_patterns', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='period_sec', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.period_sec', index=2, number=3, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -154,6 +160,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='fingerprint', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.fingerprint', index=0, @@ -161,21 +168,21 @@ has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='schedules', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.schedules', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='suggested_wait_time_sec', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.suggested_wait_time_sec', index=2, number=3, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -247,6 +254,7 @@ file=DESCRIPTOR, index=0, serialized_options=None, + create_key=_descriptor._internal_create_key, serialized_start=763, serialized_end=924, methods=[ @@ -258,6 +266,7 @@ input_type=_METRICCONFIGREQUEST, output_type=_METRICCONFIGRESPONSE, serialized_options=None, + create_key=_descriptor._internal_create_key, ), ]) _sym_db.RegisterServiceDescriptor(_METRICCONFIG) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.pyi index 7218e03264..ee8050802b 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.pyi @@ -16,50 +16,72 @@ class MetricConfigRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... RESOURCE_FIELD_NUMBER: builtins.int LAST_KNOWN_FINGERPRINT_FIELD_NUMBER: builtins.int - last_known_fingerprint: builtins.bytes = ... - @property - def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: ... + def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: + """Required. The resource for which configuration should be returned.""" + pass + last_known_fingerprint: builtins.bytes = ... + """Optional. The value of MetricConfigResponse.fingerprint for the last + configuration that the caller received and successfully applied. + """ def __init__(self, *, resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., last_known_fingerprint : builtins.bytes = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"resource",b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"last_known_fingerprint",b"last_known_fingerprint",u"resource",b"resource"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["resource",b"resource"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["last_known_fingerprint",b"last_known_fingerprint","resource",b"resource"]) -> None: ... global___MetricConfigRequest = MetricConfigRequest class MetricConfigResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... class Schedule(google.protobuf.message.Message): + """A Schedule is used to apply a particular scheduling configuration to + a metric. If a metric name matches a schedule's patterns, then the metric + adopts the configuration specified by the schedule. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... class Pattern(google.protobuf.message.Message): + """A light-weight pattern that can match 1 or more + metrics, for which this schedule will apply. The string is used to + match against metric names. It should not exceed 100k characters. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... EQUALS_FIELD_NUMBER: builtins.int STARTS_WITH_FIELD_NUMBER: builtins.int equals: typing.Text = ... + """matches the metric name exactly""" + starts_with: typing.Text = ... + """prefix-matches the metric name""" def __init__(self, *, equals : typing.Text = ..., starts_with : typing.Text = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"equals",b"equals",u"match",b"match",u"starts_with",b"starts_with"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal[u"match",b"match"]) -> typing_extensions.Literal["equals","starts_with"]: ... + def HasField(self, field_name: typing_extensions.Literal["equals",b"equals","match",b"match","starts_with",b"starts_with"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["equals",b"equals","match",b"match","starts_with",b"starts_with"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["match",b"match"]) -> typing.Optional[typing_extensions.Literal["equals","starts_with"]]: ... EXCLUSION_PATTERNS_FIELD_NUMBER: builtins.int INCLUSION_PATTERNS_FIELD_NUMBER: builtins.int PERIOD_SEC_FIELD_NUMBER: builtins.int - period_sec: builtins.int = ... - @property - def exclusion_patterns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule.Pattern]: ... - + def exclusion_patterns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule.Pattern]: + """Metrics with names that match a rule in the inclusion_patterns are + targeted by this schedule. Metrics that match the exclusion_patterns + are not targeted for this schedule, even if they match an inclusion + pattern. + """ + pass @property def inclusion_patterns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule.Pattern]: ... + period_sec: builtins.int = ... + """Describes the collection period for each metric in seconds. + A period of 0 means to not export. + """ def __init__(self, *, @@ -67,16 +89,42 @@ class MetricConfigResponse(google.protobuf.message.Message): inclusion_patterns : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule.Pattern]] = ..., period_sec : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"exclusion_patterns",b"exclusion_patterns",u"inclusion_patterns",b"inclusion_patterns",u"period_sec",b"period_sec"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["exclusion_patterns",b"exclusion_patterns","inclusion_patterns",b"inclusion_patterns","period_sec",b"period_sec"]) -> None: ... FINGERPRINT_FIELD_NUMBER: builtins.int SCHEDULES_FIELD_NUMBER: builtins.int SUGGESTED_WAIT_TIME_SEC_FIELD_NUMBER: builtins.int fingerprint: builtins.bytes = ... - suggested_wait_time_sec: builtins.int = ... + """Optional. The fingerprint associated with this MetricConfigResponse. Each + change in configs yields a different fingerprint. The resource SHOULD copy + this value to MetricConfigRequest.last_known_fingerprint for the next + configuration request. If there are no changes between fingerprint and + MetricConfigRequest.last_known_fingerprint, then all other fields besides + fingerprint in the response are optional, or the same as the last update if + present. + + The exact mechanics of generating the fingerprint is up to the + implementation. However, a fingerprint must be deterministically determined + by the configurations -- the same configuration will generate the same + fingerprint on any instance of an implementation. Hence using a timestamp is + unacceptable, but a deterministic hash is fine. + """ @property - def schedules(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule]: ... + def schedules(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule]: + """A single metric may match multiple schedules. In such cases, the schedule + that specifies the smallest period is applied. + + Note, for optimization purposes, it is recommended to use as few schedules + as possible to capture all required metric updates. Where you can be + conservative, do take full advantage of the inclusion/exclusion patterns to + capture as much of your targeted metrics. + """ + pass + suggested_wait_time_sec: builtins.int = ... + """Optional. The client is suggested to wait this long (in seconds) before + pinging the configuration service again. + """ def __init__(self, *, @@ -84,5 +132,5 @@ class MetricConfigResponse(google.protobuf.message.Message): schedules : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule]] = ..., suggested_wait_time_sec : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"fingerprint",b"fingerprint",u"schedules",b"schedules",u"suggested_wait_time_sec",b"suggested_wait_time_sec"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["fingerprint",b"fingerprint","schedules",b"schedules","suggested_wait_time_sec",b"suggested_wait_time_sec"]) -> None: ... global___MetricConfigResponse = MetricConfigResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2_grpc.py index 829bf58712..409ddfa261 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2_grpc.py +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2_grpc.py @@ -1,4 +1,5 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" import grpc from opentelemetry.proto.metrics.experimental import metrics_config_service_pb2 as opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2 @@ -36,7 +37,7 @@ class MetricConfigServicer(object): """ def GetMetricConfig(self, request, context): - """Missing associated documentation comment in .proto file""" + """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') @@ -71,6 +72,7 @@ def GetMetricConfig(request, options=(), channel_credentials=None, call_credentials=None, + insecure=False, compression=None, wait_for_ready=None, timeout=None, @@ -79,4 +81,4 @@ def GetMetricConfig(request, opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigRequest.SerializeToString, opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigResponse.FromString, options, channel_credentials, - call_credentials, compression, wait_for_ready, timeout, metadata) + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py index 77ad3c5c91..c28faca33d 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/metrics/v1/metrics.proto - +"""Generated protocol buffer code.""" from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message @@ -21,6 +21,7 @@ package='opentelemetry.proto.metrics.v1', syntax='proto3', serialized_options=b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1', + create_key=_descriptor._internal_create_key, serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xca\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x66\n\x1finstrumentation_library_metrics\x18\x02 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc4\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xf6\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x41\n\tint_gauge\x18\x04 \x01(\x0b\x32(.opentelemetry.proto.metrics.v1.IntGaugeB\x02\x18\x01H\x00\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12=\n\x07int_sum\x18\x06 \x01(\x0b\x32&.opentelemetry.proto.metrics.v1.IntSumB\x02\x18\x01H\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12I\n\rint_histogram\x18\x08 \x01(\x0b\x32,.opentelemetry.proto.metrics.v1.IntHistogramB\x02\x18\x01H\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61ta\"Q\n\x08IntGauge\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint:\x02\x18\x01\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xbe\x01\n\x06IntSum\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08:\x02\x18\x01\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xb7\x01\n\x0cIntHistogram\x12J\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x35.opentelemetry.proto.metrics.v1.IntHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality:\x02\x18\x01\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\xd6\x01\n\x0cIntDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x10\x12>\n\texemplars\x18\x05 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar:\x02\x18\x01\"\xb4\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x41\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValueB\x02\x18\x01\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.ExemplarB\x07\n\x05value\"\x9c\x02\n\x15IntHistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x10\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12>\n\texemplars\x18\x08 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar:\x02\x18\x01\"\xd3\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x41\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValueB\x02\x18\x01\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\"\xf3\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x41\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValueB\x02\x18\x01\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01\"\xa3\x01\n\x0bIntExemplar\x12\x46\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\r\n\x05value\x18\x03 \x01(\x10\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c:\x02\x18\x01\"\x87\x02\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12J\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValueB\x02\x18\x01\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05value*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02\x42t\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -30,19 +31,23 @@ full_name='opentelemetry.proto.metrics.v1.AggregationTemporality', filename=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='AGGREGATION_TEMPORALITY_UNSPECIFIED', index=0, number=0, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='AGGREGATION_TEMPORALITY_DELTA', index=1, number=1, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='AGGREGATION_TEMPORALITY_CUMULATIVE', index=2, number=2, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, @@ -64,6 +69,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='resource', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.resource', index=0, @@ -71,21 +77,21 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='instrumentation_library_metrics', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.instrumentation_library_metrics', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='schema_url', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.schema_url', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -109,6 +115,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='instrumentation_library', full_name='opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics.instrumentation_library', index=0, @@ -116,21 +123,21 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='metrics', full_name='opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics.metrics', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='schema_url', full_name='opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics.schema_url', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -154,6 +161,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='opentelemetry.proto.metrics.v1.Metric.name', index=0, @@ -161,70 +169,70 @@ has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='description', full_name='opentelemetry.proto.metrics.v1.Metric.description', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='unit', full_name='opentelemetry.proto.metrics.v1.Metric.unit', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='int_gauge', full_name='opentelemetry.proto.metrics.v1.Metric.int_gauge', index=3, number=4, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR), + serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='gauge', full_name='opentelemetry.proto.metrics.v1.Metric.gauge', index=4, number=5, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='int_sum', full_name='opentelemetry.proto.metrics.v1.Metric.int_sum', index=5, number=6, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR), + serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='sum', full_name='opentelemetry.proto.metrics.v1.Metric.sum', index=6, number=7, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='int_histogram', full_name='opentelemetry.proto.metrics.v1.Metric.int_histogram', index=7, number=8, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR), + serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='histogram', full_name='opentelemetry.proto.metrics.v1.Metric.histogram', index=8, number=9, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='summary', full_name='opentelemetry.proto.metrics.v1.Metric.summary', index=9, number=11, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -238,7 +246,9 @@ oneofs=[ _descriptor.OneofDescriptor( name='data', full_name='opentelemetry.proto.metrics.v1.Metric.data', - index=0, containing_type=None, fields=[]), + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], serialized_start=577, serialized_end=1079, @@ -251,6 +261,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='data_points', full_name='opentelemetry.proto.metrics.v1.IntGauge.data_points', index=0, @@ -258,7 +269,7 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -282,6 +293,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='data_points', full_name='opentelemetry.proto.metrics.v1.Gauge.data_points', index=0, @@ -289,7 +301,7 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -313,6 +325,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='data_points', full_name='opentelemetry.proto.metrics.v1.IntSum.data_points', index=0, @@ -320,21 +333,21 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.IntSum.aggregation_temporality', index=1, number=2, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='is_monotonic', full_name='opentelemetry.proto.metrics.v1.IntSum.is_monotonic', index=2, number=3, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -358,6 +371,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='data_points', full_name='opentelemetry.proto.metrics.v1.Sum.data_points', index=0, @@ -365,21 +379,21 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.Sum.aggregation_temporality', index=1, number=2, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='is_monotonic', full_name='opentelemetry.proto.metrics.v1.Sum.is_monotonic', index=2, number=3, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -403,6 +417,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='data_points', full_name='opentelemetry.proto.metrics.v1.IntHistogram.data_points', index=0, @@ -410,14 +425,14 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.IntHistogram.aggregation_temporality', index=1, number=2, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -441,6 +456,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='data_points', full_name='opentelemetry.proto.metrics.v1.Histogram.data_points', index=0, @@ -448,14 +464,14 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.Histogram.aggregation_temporality', index=1, number=2, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -479,6 +495,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='data_points', full_name='opentelemetry.proto.metrics.v1.Summary.data_points', index=0, @@ -486,7 +503,7 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -510,6 +527,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='labels', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.labels', index=0, @@ -517,35 +535,35 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.start_time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.time_unix_nano', index=2, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='value', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.value', index=3, number=4, type=16, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='exemplars', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.exemplars', index=4, number=5, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -569,6 +587,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='attributes', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.attributes', index=0, @@ -576,49 +595,49 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='labels', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.labels', index=1, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR), + serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.start_time_unix_nano', index=2, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.time_unix_nano', index=3, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='as_double', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.as_double', index=4, number=4, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='as_int', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.as_int', index=5, number=6, type=16, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='exemplars', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.exemplars', index=6, number=5, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -632,7 +651,9 @@ oneofs=[ _descriptor.OneofDescriptor( name='value', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.value', - index=0, containing_type=None, fields=[]), + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], serialized_start=2287, serialized_end=2595, @@ -645,6 +666,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='labels', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.labels', index=0, @@ -652,56 +674,56 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.start_time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.time_unix_nano', index=2, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='count', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.count', index=3, number=4, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='sum', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.sum', index=4, number=5, type=16, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='bucket_counts', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.bucket_counts', index=5, number=6, type=6, cpp_type=4, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='explicit_bounds', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.explicit_bounds', index=6, number=7, type=1, cpp_type=5, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='exemplars', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.exemplars', index=7, number=8, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -725,6 +747,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='attributes', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.attributes', index=0, @@ -732,63 +755,63 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='labels', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.labels', index=1, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR), + serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.start_time_unix_nano', index=2, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.time_unix_nano', index=3, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='count', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.count', index=4, number=4, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='sum', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.sum', index=5, number=5, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='bucket_counts', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.bucket_counts', index=6, number=6, type=6, cpp_type=4, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='explicit_bounds', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.explicit_bounds', index=7, number=7, type=1, cpp_type=5, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='exemplars', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.exemplars', index=8, number=8, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -812,6 +835,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='quantile', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile.quantile', index=0, @@ -819,14 +843,14 @@ has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='value', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile.value', index=1, number=2, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -849,6 +873,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='attributes', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.attributes', index=0, @@ -856,49 +881,49 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='labels', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.labels', index=1, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR), + serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.start_time_unix_nano', index=2, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.time_unix_nano', index=3, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='count', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.count', index=4, number=4, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='sum', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.sum', index=5, number=5, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='quantile_values', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.quantile_values', index=6, number=6, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -922,6 +947,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='filtered_labels', full_name='opentelemetry.proto.metrics.v1.IntExemplar.filtered_labels', index=0, @@ -929,35 +955,35 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntExemplar.time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='value', full_name='opentelemetry.proto.metrics.v1.IntExemplar.value', index=2, number=3, type=16, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='span_id', full_name='opentelemetry.proto.metrics.v1.IntExemplar.span_id', index=3, number=4, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='trace_id', full_name='opentelemetry.proto.metrics.v1.IntExemplar.trace_id', index=4, number=5, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -981,6 +1007,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='filtered_attributes', full_name='opentelemetry.proto.metrics.v1.Exemplar.filtered_attributes', index=0, @@ -988,49 +1015,49 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='filtered_labels', full_name='opentelemetry.proto.metrics.v1.Exemplar.filtered_labels', index=1, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR), + serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.Exemplar.time_unix_nano', index=2, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='as_double', full_name='opentelemetry.proto.metrics.v1.Exemplar.as_double', index=3, number=3, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='as_int', full_name='opentelemetry.proto.metrics.v1.Exemplar.as_int', index=4, number=6, type=16, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='span_id', full_name='opentelemetry.proto.metrics.v1.Exemplar.span_id', index=5, number=4, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='trace_id', full_name='opentelemetry.proto.metrics.v1.Exemplar.trace_id', index=6, number=5, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -1044,7 +1071,9 @@ oneofs=[ _descriptor.OneofDescriptor( name='value', full_name='opentelemetry.proto.metrics.v1.Exemplar.value', - index=0, containing_type=None, fields=[]), + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], serialized_start=3767, serialized_end=4030, diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi index fb181488cd..4fb9c9ff93 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi @@ -14,30 +14,177 @@ import typing_extensions DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... -global___AggregationTemporality = AggregationTemporality -class _AggregationTemporality(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[AggregationTemporality.V], builtins.type): +class AggregationTemporality(_AggregationTemporality, metaclass=_AggregationTemporalityEnumTypeWrapper): + """AggregationTemporality defines how a metric aggregator reports aggregated + values. It describes how those values relate to the time interval over + which they are aggregated. + """ + pass +class _AggregationTemporality: + V = typing.NewType('V', builtins.int) +class _AggregationTemporalityEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_AggregationTemporality.V], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... AGGREGATION_TEMPORALITY_UNSPECIFIED = AggregationTemporality.V(0) + """UNSPECIFIED is the default AggregationTemporality, it MUST not be used.""" + AGGREGATION_TEMPORALITY_DELTA = AggregationTemporality.V(1) + """DELTA is an AggregationTemporality for a metric aggregator which reports + changes since last report time. Successive metrics contain aggregation of + values from continuous and non-overlapping intervals. + + The values for a DELTA metric are based only on the time interval + associated with one measurement cycle. There is no dependency on + previous measurements like is the case for CUMULATIVE metrics. + + For example, consider a system measuring the number of requests that + it receives and reports the sum of these requests every second as a + DELTA metric: + + 1. The system starts receiving at time=t_0. + 2. A request is received, the system measures 1 request. + 3. A request is received, the system measures 1 request. + 4. A request is received, the system measures 1 request. + 5. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_0 to + t_0+1 with a value of 3. + 6. A request is received, the system measures 1 request. + 7. A request is received, the system measures 1 request. + 8. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_0+1 to + t_0+2 with a value of 2. + """ + AGGREGATION_TEMPORALITY_CUMULATIVE = AggregationTemporality.V(2) -class AggregationTemporality(metaclass=_AggregationTemporality): - V = typing.NewType('V', builtins.int) + """CUMULATIVE is an AggregationTemporality for a metric aggregator which + reports changes since a fixed start time. This means that current values + of a CUMULATIVE metric depend on all previous measurements since the + start time. Because of this, the sender is required to retain this state + in some form. If this state is lost or invalidated, the CUMULATIVE metric + values MUST be reset and a new fixed start time following the last + reported measurement time sent MUST be used. + + For example, consider a system measuring the number of requests that + it receives and reports the sum of these requests every second as a + CUMULATIVE metric: + + 1. The system starts receiving at time=t_0. + 2. A request is received, the system measures 1 request. + 3. A request is received, the system measures 1 request. + 4. A request is received, the system measures 1 request. + 5. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_0 to + t_0+1 with a value of 3. + 6. A request is received, the system measures 1 request. + 7. A request is received, the system measures 1 request. + 8. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_0 to + t_0+2 with a value of 5. + 9. The system experiences a fault and loses state. + 10. The system recovers and resumes receiving at time=t_1. + 11. A request is received, the system measures 1 request. + 12. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_1 to + t_0+1 with a value of 1. + + Note: Even though, when reporting changes since last report time, using + CUMULATIVE is valid, it is not recommended. This may cause problems for + systems that do not use start_time to determine when the aggregation + value was reset (e.g. Prometheus). + """ + + AGGREGATION_TEMPORALITY_UNSPECIFIED = AggregationTemporality.V(0) +"""UNSPECIFIED is the default AggregationTemporality, it MUST not be used.""" + AGGREGATION_TEMPORALITY_DELTA = AggregationTemporality.V(1) +"""DELTA is an AggregationTemporality for a metric aggregator which reports +changes since last report time. Successive metrics contain aggregation of +values from continuous and non-overlapping intervals. + +The values for a DELTA metric are based only on the time interval +associated with one measurement cycle. There is no dependency on +previous measurements like is the case for CUMULATIVE metrics. + +For example, consider a system measuring the number of requests that +it receives and reports the sum of these requests every second as a +DELTA metric: + + 1. The system starts receiving at time=t_0. + 2. A request is received, the system measures 1 request. + 3. A request is received, the system measures 1 request. + 4. A request is received, the system measures 1 request. + 5. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_0 to + t_0+1 with a value of 3. + 6. A request is received, the system measures 1 request. + 7. A request is received, the system measures 1 request. + 8. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_0+1 to + t_0+2 with a value of 2. +""" + AGGREGATION_TEMPORALITY_CUMULATIVE = AggregationTemporality.V(2) +"""CUMULATIVE is an AggregationTemporality for a metric aggregator which +reports changes since a fixed start time. This means that current values +of a CUMULATIVE metric depend on all previous measurements since the +start time. Because of this, the sender is required to retain this state +in some form. If this state is lost or invalidated, the CUMULATIVE metric +values MUST be reset and a new fixed start time following the last +reported measurement time sent MUST be used. + +For example, consider a system measuring the number of requests that +it receives and reports the sum of these requests every second as a +CUMULATIVE metric: + + 1. The system starts receiving at time=t_0. + 2. A request is received, the system measures 1 request. + 3. A request is received, the system measures 1 request. + 4. A request is received, the system measures 1 request. + 5. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_0 to + t_0+1 with a value of 3. + 6. A request is received, the system measures 1 request. + 7. A request is received, the system measures 1 request. + 8. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_0 to + t_0+2 with a value of 5. + 9. The system experiences a fault and loses state. + 10. The system recovers and resumes receiving at time=t_1. + 11. A request is received, the system measures 1 request. + 12. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_1 to + t_0+1 with a value of 1. + +Note: Even though, when reporting changes since last report time, using +CUMULATIVE is valid, it is not recommended. This may cause problems for +systems that do not use start_time to determine when the aggregation +value was reset (e.g. Prometheus). +""" + +global___AggregationTemporality = AggregationTemporality + class ResourceMetrics(google.protobuf.message.Message): + """A collection of InstrumentationLibraryMetrics from a Resource.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... RESOURCE_FIELD_NUMBER: builtins.int INSTRUMENTATION_LIBRARY_METRICS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int - schema_url: typing.Text = ... - @property - def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: ... - + def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: + """The resource for the metrics in this message. + If this field is not set then no resource info is known. + """ + pass @property - def instrumentation_library_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibraryMetrics]: ... + def instrumentation_library_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibraryMetrics]: + """A list of metrics that originate from a resource.""" + pass + schema_url: typing.Text = ... + """This schema_url applies to the data in the "resource" field. It does not apply + to the data in the "instrumentation_library_metrics" field which have their own + schema_url field. + """ def __init__(self, *, @@ -45,22 +192,29 @@ class ResourceMetrics(google.protobuf.message.Message): instrumentation_library_metrics : typing.Optional[typing.Iterable[global___InstrumentationLibraryMetrics]] = ..., schema_url : typing.Text = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"resource",b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library_metrics",b"instrumentation_library_metrics",u"resource",b"resource",u"schema_url",b"schema_url"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["resource",b"resource"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library_metrics",b"instrumentation_library_metrics","resource",b"resource","schema_url",b"schema_url"]) -> None: ... global___ResourceMetrics = ResourceMetrics class InstrumentationLibraryMetrics(google.protobuf.message.Message): + """A collection of Metrics produced by an InstrumentationLibrary.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int METRICS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int - schema_url: typing.Text = ... - @property - def instrumentation_library(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: ... - + def instrumentation_library(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: + """The instrumentation library information for the metrics in this message. + Semantically when InstrumentationLibrary isn't set, it is equivalent with + an empty instrumentation library name (unknown). + """ + pass @property - def metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Metric]: ... + def metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Metric]: + """A list of metrics that originate from an instrumentation library.""" + pass + schema_url: typing.Text = ... + """This schema_url applies to all metrics in the "metrics" field.""" def __init__(self, *, @@ -68,11 +222,97 @@ class InstrumentationLibraryMetrics(google.protobuf.message.Message): metrics : typing.Optional[typing.Iterable[global___Metric]] = ..., schema_url : typing.Text = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library",u"metrics",b"metrics",u"schema_url",b"schema_url"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library","metrics",b"metrics","schema_url",b"schema_url"]) -> None: ... global___InstrumentationLibraryMetrics = InstrumentationLibraryMetrics class Metric(google.protobuf.message.Message): + """Defines a Metric which has one or more timeseries. The following is a + brief summary of the Metric data model. For more details, see: + + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md + + + The data model and relation between entities is shown in the + diagram below. Here, "DataPoint" is the term used to refer to any + one of the specific data point value types, and "points" is the term used + to refer to any one of the lists of points contained in the Metric. + + - Metric is composed of a metadata and data. + - Metadata part contains a name, description, unit. + - Data is one of the possible types (Sum, Gauge, Histogram, Summary). + - DataPoint contains timestamps, attributes, and one of the possible value type + fields. + + Metric + +------------+ + |name | + |description | + |unit | +------------------------------------+ + |data |---> |Gauge, Sum, Histogram, Summary, ... | + +------------+ +------------------------------------+ + + Data [One of Gauge, Sum, Histogram, Summary, ...] + +-----------+ + |... | // Metadata about the Data. + |points |--+ + +-----------+ | + | +---------------------------+ + | |DataPoint 1 | + v |+------+------+ +------+ | + +-----+ ||label |label |...|label | | + | 1 |-->||value1|value2|...|valueN| | + +-----+ |+------+------+ +------+ | + | . | |+-----+ | + | . | ||value| | + | . | |+-----+ | + | . | +---------------------------+ + | . | . + | . | . + | . | . + | . | +---------------------------+ + | . | |DataPoint M | + +-----+ |+------+------+ +------+ | + | M |-->||label |label |...|label | | + +-----+ ||value1|value2|...|valueN| | + |+------+------+ +------+ | + |+-----+ | + ||value| | + |+-----+ | + +---------------------------+ + + Each distinct type of DataPoint represents the output of a specific + aggregation function, the result of applying the DataPoint's + associated function of to one or more measurements. + + All DataPoint types have three common fields: + - Attributes includes key-value pairs associated with the data point + - TimeUnixNano is required, set to the end time of the aggregation + - StartTimeUnixNano is optional, but strongly encouraged for DataPoints + having an AggregationTemporality field, as discussed below. + + Both TimeUnixNano and StartTimeUnixNano values are expressed as + UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + + # TimeUnixNano + + This field is required, having consistent interpretation across + DataPoint types. TimeUnixNano is the moment corresponding to when + the data point's aggregate value was captured. + + Data points with the 0 value for TimeUnixNano SHOULD be rejected + by consumers. + + # StartTimeUnixNano + + StartTimeUnixNano in general allows detecting when a sequence of + observations is unbroken. This field indicates to consumers the + start time for points with cumulative and delta + AggregationTemporality, and it should be included whenever possible + to support correct rate calculation. Although it may be omitted + when the start time is truly unknown, setting StartTimeUnixNano is + strongly encouraged. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... NAME_FIELD_NUMBER: builtins.int DESCRIPTION_FIELD_NUMBER: builtins.int @@ -85,30 +325,51 @@ class Metric(google.protobuf.message.Message): HISTOGRAM_FIELD_NUMBER: builtins.int SUMMARY_FIELD_NUMBER: builtins.int name: typing.Text = ... + """name of the metric, including its DNS name prefix. It must be unique.""" + description: typing.Text = ... + """description of the metric, which can be used in documentation.""" + unit: typing.Text = ... + """unit in which the metric value is reported. Follows the format + described by http://unitsofmeasure.org/ucum.html. + """ @property - def int_gauge(self) -> global___IntGauge: ... - + def int_gauge(self) -> global___IntGauge: + """IntGauge and IntSum are deprecated and will be removed soon. + 1. Old senders and receivers that are not aware of this change will + continue using the `int_gauge` and `int_sum` fields. + 2. New senders, which are aware of this change MUST send only `gauge` + and `sum` fields. + 3. New receivers, which are aware of this change MUST convert these into + `gauge` and `sum` by using the provided as_int field in the oneof values. + This field will be removed in ~3 months, on July 1, 2021. + """ + pass @property def gauge(self) -> global___Gauge: ... - @property - def int_sum(self) -> global___IntSum: ... - + def int_sum(self) -> global___IntSum: + """This field will be removed in ~3 months, on July 1, 2021.""" + pass @property def sum(self) -> global___Sum: ... - @property - def int_histogram(self) -> global___IntHistogram: ... - + def int_histogram(self) -> global___IntHistogram: + """IntHistogram is deprecated and will be removed soon. + 1. Old senders and receivers that are not aware of this change will + continue using the `int_histogram` field. + 2. New senders, which are aware of this change MUST send only `histogram`. + 3. New receivers, which are aware of this change MUST convert this into + `histogram` by simply converting all int64 values into float. + This field will be removed in ~3 months, on July 1, 2021. + """ + pass @property def histogram(self) -> global___Histogram: ... - @property def summary(self) -> global___Summary: ... - def __init__(self, *, name : typing.Text = ..., @@ -122,49 +383,76 @@ class Metric(google.protobuf.message.Message): histogram : typing.Optional[global___Histogram] = ..., summary : typing.Optional[global___Summary] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"data",b"data",u"gauge",b"gauge",u"histogram",b"histogram",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum",u"sum",b"sum",u"summary",b"summary"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"data",b"data",u"description",b"description",u"gauge",b"gauge",u"histogram",b"histogram",u"int_gauge",b"int_gauge",u"int_histogram",b"int_histogram",u"int_sum",b"int_sum",u"name",b"name",u"sum",b"sum",u"summary",b"summary",u"unit",b"unit"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal[u"data",b"data"]) -> typing_extensions.Literal["int_gauge","gauge","int_sum","sum","int_histogram","histogram","summary"]: ... + def HasField(self, field_name: typing_extensions.Literal["data",b"data","gauge",b"gauge","histogram",b"histogram","int_gauge",b"int_gauge","int_histogram",b"int_histogram","int_sum",b"int_sum","sum",b"sum","summary",b"summary"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["data",b"data","description",b"description","gauge",b"gauge","histogram",b"histogram","int_gauge",b"int_gauge","int_histogram",b"int_histogram","int_sum",b"int_sum","name",b"name","sum",b"sum","summary",b"summary","unit",b"unit"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["data",b"data"]) -> typing.Optional[typing_extensions.Literal["int_gauge","gauge","int_sum","sum","int_histogram","histogram","summary"]]: ... global___Metric = Metric class IntGauge(google.protobuf.message.Message): + """IntGauge is deprecated. Use Gauge with an integer value in NumberDataPoint. + + IntGauge represents the type of a int scalar metric that always exports the + "current value" for every data point. It should be used for an "unknown" + aggregation. + + A Gauge does not support different aggregation temporalities. Given the + aggregation is unknown, points cannot be combined using the same + aggregation, regardless of aggregation temporalities. Therefore, + AggregationTemporality is not included. Consequently, this also means + "StartTimeUnixNano" is ignored for all data points. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int - @property def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntDataPoint]: ... - def __init__(self, *, data_points : typing.Optional[typing.Iterable[global___IntDataPoint]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"data_points",b"data_points"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["data_points",b"data_points"]) -> None: ... global___IntGauge = IntGauge class Gauge(google.protobuf.message.Message): + """Gauge represents the type of a double scalar metric that always exports the + "current value" for every data point. It should be used for an "unknown" + aggregation. + + A Gauge does not support different aggregation temporalities. Given the + aggregation is unknown, points cannot be combined using the same + aggregation, regardless of aggregation temporalities. Therefore, + AggregationTemporality is not included. Consequently, this also means + "StartTimeUnixNano" is ignored for all data points. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int - @property def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NumberDataPoint]: ... - def __init__(self, *, data_points : typing.Optional[typing.Iterable[global___NumberDataPoint]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"data_points",b"data_points"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["data_points",b"data_points"]) -> None: ... global___Gauge = Gauge class IntSum(google.protobuf.message.Message): + """IntSum is deprecated. Use Sum with an integer value in NumberDataPoint. + + IntSum represents the type of a numeric int scalar metric that is calculated as + a sum of all reported measurements over a time interval. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int IS_MONOTONIC_FIELD_NUMBER: builtins.int - aggregation_temporality: global___AggregationTemporality.V = ... - is_monotonic: builtins.bool = ... - @property def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntDataPoint]: ... + aggregation_temporality: global___AggregationTemporality.V = ... + """aggregation_temporality describes if the aggregator reports delta changes + since last report time, or cumulative changes since a fixed start time. + """ + + is_monotonic: builtins.bool = ... + """If "true" means that the sum is monotonic.""" def __init__(self, *, @@ -172,19 +460,26 @@ class IntSum(google.protobuf.message.Message): aggregation_temporality : global___AggregationTemporality.V = ..., is_monotonic : builtins.bool = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points",u"is_monotonic",b"is_monotonic"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality",b"aggregation_temporality","data_points",b"data_points","is_monotonic",b"is_monotonic"]) -> None: ... global___IntSum = IntSum class Sum(google.protobuf.message.Message): + """Sum represents the type of a numeric double scalar metric that is calculated + as a sum of all reported measurements over a time interval. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int IS_MONOTONIC_FIELD_NUMBER: builtins.int - aggregation_temporality: global___AggregationTemporality.V = ... - is_monotonic: builtins.bool = ... - @property def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NumberDataPoint]: ... + aggregation_temporality: global___AggregationTemporality.V = ... + """aggregation_temporality describes if the aggregator reports delta changes + since last report time, or cumulative changes since a fixed start time. + """ + + is_monotonic: builtins.bool = ... + """If "true" means that the sum is monotonic.""" def __init__(self, *, @@ -192,74 +487,113 @@ class Sum(google.protobuf.message.Message): aggregation_temporality : global___AggregationTemporality.V = ..., is_monotonic : builtins.bool = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points",u"is_monotonic",b"is_monotonic"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality",b"aggregation_temporality","data_points",b"data_points","is_monotonic",b"is_monotonic"]) -> None: ... global___Sum = Sum class IntHistogram(google.protobuf.message.Message): + """IntHistogram is deprecated, replaced by Histogram points using double- + valued exemplars. + + This represents the type of a metric that is calculated by aggregating as a + Histogram of all reported int measurements over a time interval. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int - aggregation_temporality: global___AggregationTemporality.V = ... - @property def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntHistogramDataPoint]: ... + aggregation_temporality: global___AggregationTemporality.V = ... + """aggregation_temporality describes if the aggregator reports delta changes + since last report time, or cumulative changes since a fixed start time. + """ def __init__(self, *, data_points : typing.Optional[typing.Iterable[global___IntHistogramDataPoint]] = ..., aggregation_temporality : global___AggregationTemporality.V = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality",b"aggregation_temporality","data_points",b"data_points"]) -> None: ... global___IntHistogram = IntHistogram class Histogram(google.protobuf.message.Message): + """Histogram represents the type of a metric that is calculated by aggregating + as a Histogram of all reported double measurements over a time interval. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int - aggregation_temporality: global___AggregationTemporality.V = ... - @property def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___HistogramDataPoint]: ... + aggregation_temporality: global___AggregationTemporality.V = ... + """aggregation_temporality describes if the aggregator reports delta changes + since last report time, or cumulative changes since a fixed start time. + """ def __init__(self, *, data_points : typing.Optional[typing.Iterable[global___HistogramDataPoint]] = ..., aggregation_temporality : global___AggregationTemporality.V = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"aggregation_temporality",b"aggregation_temporality",u"data_points",b"data_points"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality",b"aggregation_temporality","data_points",b"data_points"]) -> None: ... global___Histogram = Histogram class Summary(google.protobuf.message.Message): + """Summary metric data are used to convey quantile summaries, + a Prometheus (see: https://prometheus.io/docs/concepts/metric_types/#summary) + and OpenMetrics (see: https://github.com/OpenObservability/OpenMetrics/blob/4dbf6075567ab43296eed941037c12951faafb92/protos/prometheus.proto#L45) + data type. These data points cannot always be merged in a meaningful way. + While they can be useful in some applications, histogram data points are + recommended for new applications. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int - @property def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___SummaryDataPoint]: ... - def __init__(self, *, data_points : typing.Optional[typing.Iterable[global___SummaryDataPoint]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"data_points",b"data_points"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["data_points",b"data_points"]) -> None: ... global___Summary = Summary class IntDataPoint(google.protobuf.message.Message): + """IntDataPoint is a single data point in a timeseries that describes the + time-varying values of a int64 metric. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... LABELS_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int EXEMPLARS_FIELD_NUMBER: builtins.int + @property + def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: + """The set of labels that uniquely identify this timeseries.""" + pass start_time_unix_nano: builtins.int = ... + """StartTimeUnixNano is optional but strongly encouraged, see the + the detiled comments above Metric. + + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + 1970. + """ + time_unix_nano: builtins.int = ... - value: builtins.int = ... + """TimeUnixNano is required, see the detailed comments above Metric. - @property - def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + 1970. + """ - @property - def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntExemplar]: ... + value: builtins.int = ... + """value itself.""" + @property + def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntExemplar]: + """(Optional) List of exemplars collected from + measurements that were used to form the data point + """ + pass def __init__(self, *, labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., @@ -268,10 +602,13 @@ class IntDataPoint(google.protobuf.message.Message): value : builtins.int = ..., exemplars : typing.Optional[typing.Iterable[global___IntExemplar]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"exemplars",b"exemplars",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["exemplars",b"exemplars","labels",b"labels","start_time_unix_nano",b"start_time_unix_nano","time_unix_nano",b"time_unix_nano","value",b"value"]) -> None: ... global___IntDataPoint = IntDataPoint class NumberDataPoint(google.protobuf.message.Message): + """NumberDataPoint is a single data point in a timeseries that describes the + time-varying value of a double metric. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... ATTRIBUTES_FIELD_NUMBER: builtins.int LABELS_FIELD_NUMBER: builtins.int @@ -280,20 +617,47 @@ class NumberDataPoint(google.protobuf.message.Message): AS_DOUBLE_FIELD_NUMBER: builtins.int AS_INT_FIELD_NUMBER: builtins.int EXEMPLARS_FIELD_NUMBER: builtins.int + @property + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: + """The set of key/value pairs that uniquely identify the timeseries from + where this point belongs. The list may be empty (may contain 0 elements). + """ + pass + @property + def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: + """Labels is deprecated and will be removed soon. + 1. Old senders and receivers that are not aware of this change will + continue using the `labels` field. + 2. New senders, which are aware of this change MUST send only `attributes`. + 3. New receivers, which are aware of this change MUST convert this into + `labels` by simply converting all int64 values into float. + + This field will be removed in ~3 months, on July 1, 2021. + """ + pass start_time_unix_nano: builtins.int = ... - time_unix_nano: builtins.int = ... - as_double: builtins.float = ... - as_int: builtins.int = ... + """StartTimeUnixNano is optional but strongly encouraged, see the + the detiled comments above Metric. - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + 1970. + """ - @property - def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... + time_unix_nano: builtins.int = ... + """TimeUnixNano is required, see the detailed comments above Metric. - @property - def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: ... + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + 1970. + """ + as_double: builtins.float = ... + as_int: builtins.int = ... + @property + def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: + """(Optional) List of exemplars collected from + measurements that were used to form the data point + """ + pass def __init__(self, *, attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., @@ -304,12 +668,25 @@ class NumberDataPoint(google.protobuf.message.Message): as_int : builtins.int = ..., exemplars : typing.Optional[typing.Iterable[global___Exemplar]] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"as_double",b"as_double",u"as_int",b"as_int",u"value",b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"as_double",b"as_double",u"as_int",b"as_int",u"attributes",b"attributes",u"exemplars",b"exemplars",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"time_unix_nano",b"time_unix_nano",u"value",b"value"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal[u"value",b"value"]) -> typing_extensions.Literal["as_double","as_int"]: ... + def HasField(self, field_name: typing_extensions.Literal["as_double",b"as_double","as_int",b"as_int","value",b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["as_double",b"as_double","as_int",b"as_int","attributes",b"attributes","exemplars",b"exemplars","labels",b"labels","start_time_unix_nano",b"start_time_unix_nano","time_unix_nano",b"time_unix_nano","value",b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["value",b"value"]) -> typing.Optional[typing_extensions.Literal["as_double","as_int"]]: ... global___NumberDataPoint = NumberDataPoint class IntHistogramDataPoint(google.protobuf.message.Message): + """IntHistogramDataPoint is deprecated; use HistogramDataPoint. + + This is a single data point in a timeseries that describes + the time-varying values of a Histogram of int values. A Histogram contains + summary statistics for a population of values, it may optionally contain + the distribution of those values across a set of buckets. + + If the histogram contains the distribution of values, then both + "explicit_bounds" and "bucket counts" fields must be defined. + If the histogram does not contain the distribution of values, then both + "explicit_bounds" and "bucket_counts" must be omitted and only "count" and + "sum" are known. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... LABELS_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int @@ -319,19 +696,72 @@ class IntHistogramDataPoint(google.protobuf.message.Message): BUCKET_COUNTS_FIELD_NUMBER: builtins.int EXPLICIT_BOUNDS_FIELD_NUMBER: builtins.int EXEMPLARS_FIELD_NUMBER: builtins.int + @property + def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: + """The set of labels that uniquely identify this timeseries.""" + pass start_time_unix_nano: builtins.int = ... + """StartTimeUnixNano is optional but strongly encouraged, see the + the detiled comments above Metric. + + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + 1970. + """ + time_unix_nano: builtins.int = ... + """TimeUnixNano is required, see the detailed comments above Metric. + + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + 1970. + """ + count: builtins.int = ... + """count is the number of values in the population. Must be non-negative. This + value must be equal to the sum of the "count" fields in buckets if a + histogram is provided. + """ + sum: builtins.int = ... - bucket_counts: google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int] = ... - explicit_bounds: google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float] = ... + """sum of the values in the population. If count is zero then this field + must be zero. This value must be equal to the sum of the "sum" fields in + buckets if a histogram is provided. + """ @property - def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... + def bucket_counts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: + """bucket_counts is an optional field contains the count values of histogram + for each bucket. + The sum of the bucket_counts must equal the value in the count field. + + The number of elements in bucket_counts array must be by one greater than + the number of elements in explicit_bounds array. + """ + pass @property - def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntExemplar]: ... + def explicit_bounds(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]: + """explicit_bounds specifies buckets with explicitly defined bounds for values. + + This defines size(explicit_bounds) + 1 (= N) buckets. The boundaries for + bucket at index i are: + + (-infinity, explicit_bounds[i]] for i == 0 + (explicit_bounds[i-1], explicit_bounds[i]] for 0 < i < N-1 + (explicit_bounds[i], +infinity) for i == N-1 + + The values in the explicit_bounds array must be strictly increasing. + Histogram buckets are inclusive of their upper boundary, except the last + bucket where the boundary is at infinity. This format is intentionally + compatible with the OpenMetrics histogram definition. + """ + pass + @property + def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntExemplar]: + """(Optional) List of exemplars collected from + measurements that were used to form the data point + """ + pass def __init__(self, *, labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., @@ -343,10 +773,21 @@ class IntHistogramDataPoint(google.protobuf.message.Message): explicit_bounds : typing.Optional[typing.Iterable[builtins.float]] = ..., exemplars : typing.Optional[typing.Iterable[global___IntExemplar]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"bucket_counts",b"bucket_counts",u"count",b"count",u"exemplars",b"exemplars",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["bucket_counts",b"bucket_counts","count",b"count","exemplars",b"exemplars","explicit_bounds",b"explicit_bounds","labels",b"labels","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano"]) -> None: ... global___IntHistogramDataPoint = IntHistogramDataPoint class HistogramDataPoint(google.protobuf.message.Message): + """HistogramDataPoint is a single data point in a timeseries that describes the + time-varying values of a Histogram of double values. A Histogram contains + summary statistics for a population of values, it may optionally contain the + distribution of those values across a set of buckets. + + If the histogram contains the distribution of values, then both + "explicit_bounds" and "bucket counts" fields must be defined. + If the histogram does not contain the distribution of values, then both + "explicit_bounds" and "bucket_counts" must be omitted and only "count" and + "sum" are known. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... ATTRIBUTES_FIELD_NUMBER: builtins.int LABELS_FIELD_NUMBER: builtins.int @@ -357,22 +798,92 @@ class HistogramDataPoint(google.protobuf.message.Message): BUCKET_COUNTS_FIELD_NUMBER: builtins.int EXPLICIT_BOUNDS_FIELD_NUMBER: builtins.int EXEMPLARS_FIELD_NUMBER: builtins.int + @property + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: + """The set of key/value pairs that uniquely identify the timeseries from + where this point belongs. The list may be empty (may contain 0 elements). + """ + pass + @property + def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: + """Labels is deprecated and will be removed soon. + 1. Old senders and receivers that are not aware of this change will + continue using the `labels` field. + 2. New senders, which are aware of this change MUST send only `attributes`. + 3. New receivers, which are aware of this change MUST convert this into + `labels` by simply converting all int64 values into float. + + This field will be removed in ~3 months, on July 1, 2021. + """ + pass start_time_unix_nano: builtins.int = ... + """StartTimeUnixNano is optional but strongly encouraged, see the + the detiled comments above Metric. + + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + 1970. + """ + time_unix_nano: builtins.int = ... + """TimeUnixNano is required, see the detailed comments above Metric. + + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + 1970. + """ + count: builtins.int = ... + """count is the number of values in the population. Must be non-negative. This + value must be equal to the sum of the "count" fields in buckets if a + histogram is provided. + """ + sum: builtins.float = ... - bucket_counts: google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int] = ... - explicit_bounds: google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float] = ... + """sum of the values in the population. If count is zero then this field + must be zero. This value must be equal to the sum of the "sum" fields in + buckets if a histogram is provided. - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... + Note: Sum should only be filled out when measuring non-negative discrete + events, and is assumed to be monotonic over the values of these events. + Negative events *can* be recorded, but sum should not be filled out when + doing so. This is specifically to enforce compatibility w/ OpenMetrics, + see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#histogram + """ @property - def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... + def bucket_counts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: + """bucket_counts is an optional field contains the count values of histogram + for each bucket. + + The sum of the bucket_counts must equal the value in the count field. + The number of elements in bucket_counts array must be by one greater than + the number of elements in explicit_bounds array. + """ + pass @property - def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: ... + def explicit_bounds(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]: + """explicit_bounds specifies buckets with explicitly defined bounds for values. + + This defines size(explicit_bounds) + 1 (= N) buckets. The boundaries for + bucket at index i are: + + (-infinity, explicit_bounds[i]] for i == 0 + (explicit_bounds[i-1], explicit_bounds[i]] for 0 < i < N-1 + (explicit_bounds[i], +infinity) for i == N-1 + The values in the explicit_bounds array must be strictly increasing. + + Histogram buckets are inclusive of their upper boundary, except the last + bucket where the boundary is at infinity. This format is intentionally + compatible with the OpenMetrics histogram definition. + """ + pass + @property + def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: + """(Optional) List of exemplars collected from + measurements that were used to form the data point + """ + pass def __init__(self, *, attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., @@ -385,24 +896,44 @@ class HistogramDataPoint(google.protobuf.message.Message): explicit_bounds : typing.Optional[typing.Iterable[builtins.float]] = ..., exemplars : typing.Optional[typing.Iterable[global___Exemplar]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"attributes",b"attributes",u"bucket_counts",b"bucket_counts",u"count",b"count",u"exemplars",b"exemplars",u"explicit_bounds",b"explicit_bounds",u"labels",b"labels",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","bucket_counts",b"bucket_counts","count",b"count","exemplars",b"exemplars","explicit_bounds",b"explicit_bounds","labels",b"labels","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano"]) -> None: ... global___HistogramDataPoint = HistogramDataPoint class SummaryDataPoint(google.protobuf.message.Message): + """SummaryDataPoint is a single data point in a timeseries that describes the + time-varying values of a Summary metric. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... class ValueAtQuantile(google.protobuf.message.Message): + """Represents the value at a given quantile of a distribution. + + To record Min and Max values following conventions are used: + - The 1.0 quantile is equivalent to the maximum value observed. + - The 0.0 quantile is equivalent to the minimum value observed. + + See the following issue for more context: + https://github.com/open-telemetry/opentelemetry-proto/issues/125 + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... QUANTILE_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int quantile: builtins.float = ... + """The quantile of a distribution. Must be in the interval + [0.0, 1.0]. + """ + value: builtins.float = ... + """The value at the given quantile of a distribution. + + Quantile values must NOT be negative. + """ def __init__(self, *, quantile : builtins.float = ..., value : builtins.float = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"quantile",b"quantile",u"value",b"value"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["quantile",b"quantile","value",b"value"]) -> None: ... ATTRIBUTES_FIELD_NUMBER: builtins.int LABELS_FIELD_NUMBER: builtins.int @@ -411,20 +942,59 @@ class SummaryDataPoint(google.protobuf.message.Message): COUNT_FIELD_NUMBER: builtins.int SUM_FIELD_NUMBER: builtins.int QUANTILE_VALUES_FIELD_NUMBER: builtins.int + @property + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: + """The set of key/value pairs that uniquely identify the timeseries from + where this point belongs. The list may be empty (may contain 0 elements). + """ + pass + @property + def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: + """Labels is deprecated and will be removed soon. + 1. Old senders and receivers that are not aware of this change will + continue using the `labels` field. + 2. New senders, which are aware of this change MUST send only `attributes`. + 3. New receivers, which are aware of this change MUST convert this into + `labels` by simply converting all int64 values into float. + + This field will be removed in ~3 months, on July 1, 2021. + """ + pass start_time_unix_nano: builtins.int = ... + """StartTimeUnixNano is optional but strongly encouraged, see the + the detiled comments above Metric. + + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + 1970. + """ + time_unix_nano: builtins.int = ... + """TimeUnixNano is required, see the detailed comments above Metric. + + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + 1970. + """ + count: builtins.int = ... - sum: builtins.float = ... + """count is the number of values in the population. Must be non-negative.""" - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... + sum: builtins.float = ... + """sum of the values in the population. If count is zero then this field + must be zero. - @property - def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... + Note: Sum should only be filled out when measuring non-negative discrete + events, and is assumed to be monotonic over the values of these events. + Negative events *can* be recorded, but sum should not be filled out when + doing so. This is specifically to enforce compatibility w/ OpenMetrics, + see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#summary + """ @property - def quantile_values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___SummaryDataPoint.ValueAtQuantile]: ... - + def quantile_values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___SummaryDataPoint.ValueAtQuantile]: + """(Optional) list of values at different quantiles of the distribution calculated + from the current snapshot. The quantiles must be strictly increasing. + """ + pass def __init__(self, *, attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., @@ -435,23 +1005,49 @@ class SummaryDataPoint(google.protobuf.message.Message): sum : builtins.float = ..., quantile_values : typing.Optional[typing.Iterable[global___SummaryDataPoint.ValueAtQuantile]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"attributes",b"attributes",u"count",b"count",u"labels",b"labels",u"quantile_values",b"quantile_values",u"start_time_unix_nano",b"start_time_unix_nano",u"sum",b"sum",u"time_unix_nano",b"time_unix_nano"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","count",b"count","labels",b"labels","quantile_values",b"quantile_values","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano"]) -> None: ... global___SummaryDataPoint = SummaryDataPoint class IntExemplar(google.protobuf.message.Message): + """A representation of an exemplar, which is a sample input int measurement. + Exemplars also hold information about the environment when the measurement + was recorded, for example the span and trace ID of the active span when the + exemplar was recorded. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... FILTERED_LABELS_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int SPAN_ID_FIELD_NUMBER: builtins.int TRACE_ID_FIELD_NUMBER: builtins.int + @property + def filtered_labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: + """The set of labels that were filtered out by the aggregator, but recorded + alongside the original measurement. Only labels that were filtered out + by the aggregator should be included + """ + pass time_unix_nano: builtins.int = ... + """time_unix_nano is the exact time when this exemplar was recorded + + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + 1970. + """ + value: builtins.int = ... + """Numerical int value of the measurement that was recorded.""" + span_id: builtins.bytes = ... - trace_id: builtins.bytes = ... + """(Optional) Span ID of the exemplar trace. + span_id may be missing if the measurement is not recorded inside a trace + or if the trace is not sampled. + """ - @property - def filtered_labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... + trace_id: builtins.bytes = ... + """(Optional) Trace ID of the exemplar trace. + trace_id may be missing if the measurement is not recorded inside a trace + or if the trace is not sampled. + """ def __init__(self, *, @@ -461,10 +1057,15 @@ class IntExemplar(google.protobuf.message.Message): span_id : builtins.bytes = ..., trace_id : builtins.bytes = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"filtered_labels",b"filtered_labels",u"span_id",b"span_id",u"time_unix_nano",b"time_unix_nano",u"trace_id",b"trace_id",u"value",b"value"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["filtered_labels",b"filtered_labels","span_id",b"span_id","time_unix_nano",b"time_unix_nano","trace_id",b"trace_id","value",b"value"]) -> None: ... global___IntExemplar = IntExemplar class Exemplar(google.protobuf.message.Message): + """A representation of an exemplar, which is a sample input measurement. + Exemplars also hold information about the environment when the measurement + was recorded, for example the span and trace ID of the active span when the + exemplar was recorded. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... FILTERED_ATTRIBUTES_FIELD_NUMBER: builtins.int FILTERED_LABELS_FIELD_NUMBER: builtins.int @@ -473,17 +1074,46 @@ class Exemplar(google.protobuf.message.Message): AS_INT_FIELD_NUMBER: builtins.int SPAN_ID_FIELD_NUMBER: builtins.int TRACE_ID_FIELD_NUMBER: builtins.int + @property + def filtered_attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: + """The set of key/value pairs that were filtered out by the aggregator, but + recorded alongside the original measurement. Only key/value pairs that were + filtered out by the aggregator should be included + """ + pass + @property + def filtered_labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: + """Labels is deprecated and will be removed soon. + 1. Old senders and receivers that are not aware of this change will + continue using the `filtered_labels` field. + 2. New senders, which are aware of this change MUST send only + `filtered_attributes`. + 3. New receivers, which are aware of this change MUST convert this into + `filtered_labels` by simply converting all int64 values into float. + + This field will be removed in ~3 months, on July 1, 2021. + """ + pass time_unix_nano: builtins.int = ... + """time_unix_nano is the exact time when this exemplar was recorded + + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + 1970. + """ + as_double: builtins.float = ... as_int: builtins.int = ... span_id: builtins.bytes = ... - trace_id: builtins.bytes = ... + """(Optional) Span ID of the exemplar trace. + span_id may be missing if the measurement is not recorded inside a trace + or if the trace is not sampled. + """ - @property - def filtered_attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... - - @property - def filtered_labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: ... + trace_id: builtins.bytes = ... + """(Optional) Trace ID of the exemplar trace. + trace_id may be missing if the measurement is not recorded inside a trace + or if the trace is not sampled. + """ def __init__(self, *, @@ -495,7 +1125,7 @@ class Exemplar(google.protobuf.message.Message): span_id : builtins.bytes = ..., trace_id : builtins.bytes = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"as_double",b"as_double",u"as_int",b"as_int",u"value",b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"as_double",b"as_double",u"as_int",b"as_int",u"filtered_attributes",b"filtered_attributes",u"filtered_labels",b"filtered_labels",u"span_id",b"span_id",u"time_unix_nano",b"time_unix_nano",u"trace_id",b"trace_id",u"value",b"value"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal[u"value",b"value"]) -> typing_extensions.Literal["as_double","as_int"]: ... + def HasField(self, field_name: typing_extensions.Literal["as_double",b"as_double","as_int",b"as_int","value",b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["as_double",b"as_double","as_int",b"as_int","filtered_attributes",b"filtered_attributes","filtered_labels",b"filtered_labels","span_id",b"span_id","time_unix_nano",b"time_unix_nano","trace_id",b"trace_id","value",b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["value",b"value"]) -> typing.Optional[typing_extensions.Literal["as_double","as_int"]]: ... global___Exemplar = Exemplar diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py index cf7af9fbb8..f64160f602 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/resource/v1/resource.proto - +"""Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection @@ -19,6 +19,7 @@ package='opentelemetry.proto.resource.v1', syntax='proto3', serialized_options=b'\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\001Z@github.com/open-telemetry/opentelemetry-proto/gen/go/resource/v1', + create_key=_descriptor._internal_create_key, serialized_pb=b'\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"i\n\x08Resource\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\rBw\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z@github.com/open-telemetry/opentelemetry-proto/gen/go/resource/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,]) @@ -32,6 +33,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='attributes', full_name='opentelemetry.proto.resource.v1.Resource.attributes', index=0, @@ -39,14 +41,14 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='dropped_attributes_count', full_name='opentelemetry.proto.resource.v1.Resource.dropped_attributes_count', index=1, number=2, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi index c2c5881753..957a7b6d1b 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi @@ -13,18 +13,23 @@ import typing_extensions DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... class Resource(google.protobuf.message.Message): + """Resource information.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... ATTRIBUTES_FIELD_NUMBER: builtins.int DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int - dropped_attributes_count: builtins.int = ... - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: + """Set of labels that describe the resource.""" + pass + dropped_attributes_count: builtins.int = ... + """dropped_attributes_count is the number of dropped attributes. If the value is 0, then + no attributes were dropped. + """ def __init__(self, *, attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., dropped_attributes_count : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","dropped_attributes_count",b"dropped_attributes_count"]) -> None: ... global___Resource = Resource diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py index a30df48e2c..99428b3c89 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/trace/v1/trace_config.proto - +"""Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection @@ -18,6 +18,7 @@ package='opentelemetry.proto.trace.v1', syntax='proto3', serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\020TraceConfigProtoP\001ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1', + create_key=_descriptor._internal_create_key, serialized_pb=b'\n/opentelemetry/proto/trace/v1/trace_config.proto\x12\x1copentelemetry.proto.trace.v1\"\xc8\x03\n\x0bTraceConfig\x12I\n\x10\x63onstant_sampler\x18\x01 \x01(\x0b\x32-.opentelemetry.proto.trace.v1.ConstantSamplerH\x00\x12O\n\x14trace_id_ratio_based\x18\x02 \x01(\x0b\x32/.opentelemetry.proto.trace.v1.TraceIdRatioBasedH\x00\x12R\n\x15rate_limiting_sampler\x18\x03 \x01(\x0b\x32\x31.opentelemetry.proto.trace.v1.RateLimitingSamplerH\x00\x12 \n\x18max_number_of_attributes\x18\x04 \x01(\x03\x12\"\n\x1amax_number_of_timed_events\x18\x05 \x01(\x03\x12\x30\n(max_number_of_attributes_per_timed_event\x18\x06 \x01(\x03\x12\x1b\n\x13max_number_of_links\x18\x07 \x01(\x03\x12)\n!max_number_of_attributes_per_link\x18\x08 \x01(\x03\x42\t\n\x07sampler\"\xa9\x01\n\x0f\x43onstantSampler\x12P\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32>.opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision\"D\n\x10\x43onstantDecision\x12\x0e\n\nALWAYS_OFF\x10\x00\x12\r\n\tALWAYS_ON\x10\x01\x12\x11\n\rALWAYS_PARENT\x10\x02\"*\n\x11TraceIdRatioBased\x12\x15\n\rsamplingRatio\x18\x01 \x01(\x01\"\"\n\x13RateLimitingSampler\x12\x0b\n\x03qps\x18\x01 \x01(\x03\x42~\n\x1fio.opentelemetry.proto.trace.v1B\x10TraceConfigProtoP\x01ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1b\x06proto3' ) @@ -28,19 +29,23 @@ full_name='opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision', filename=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='ALWAYS_OFF', index=0, number=0, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='ALWAYS_ON', index=1, number=1, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='ALWAYS_PARENT', index=2, number=2, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, @@ -56,6 +61,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='constant_sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.constant_sampler', index=0, @@ -63,56 +69,56 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='trace_id_ratio_based', full_name='opentelemetry.proto.trace.v1.TraceConfig.trace_id_ratio_based', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='rate_limiting_sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.rate_limiting_sampler', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='max_number_of_attributes', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_attributes', index=3, number=4, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='max_number_of_timed_events', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_timed_events', index=4, number=5, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='max_number_of_attributes_per_timed_event', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_attributes_per_timed_event', index=5, number=6, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='max_number_of_links', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_links', index=6, number=7, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='max_number_of_attributes_per_link', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_attributes_per_link', index=7, number=8, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -126,7 +132,9 @@ oneofs=[ _descriptor.OneofDescriptor( name='sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.sampler', - index=0, containing_type=None, fields=[]), + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], serialized_start=82, serialized_end=538, @@ -139,6 +147,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='decision', full_name='opentelemetry.proto.trace.v1.ConstantSampler.decision', index=0, @@ -146,7 +155,7 @@ has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -171,6 +180,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='samplingRatio', full_name='opentelemetry.proto.trace.v1.TraceIdRatioBased.samplingRatio', index=0, @@ -178,7 +188,7 @@ has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -202,6 +212,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='qps', full_name='opentelemetry.proto.trace.v1.RateLimitingSampler.qps', index=0, @@ -209,7 +220,7 @@ has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi index e02bf3d5ba..8290baf58e 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi @@ -12,6 +12,9 @@ import typing_extensions DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... class TraceConfig(google.protobuf.message.Message): + """Global configuration of the trace service. All fields must be specified, or + the default (zero) values will be used for each type. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... CONSTANT_SAMPLER_FIELD_NUMBER: builtins.int TRACE_ID_RATIO_BASED_FIELD_NUMBER: builtins.int @@ -21,20 +24,26 @@ class TraceConfig(google.protobuf.message.Message): MAX_NUMBER_OF_ATTRIBUTES_PER_TIMED_EVENT_FIELD_NUMBER: builtins.int MAX_NUMBER_OF_LINKS_FIELD_NUMBER: builtins.int MAX_NUMBER_OF_ATTRIBUTES_PER_LINK_FIELD_NUMBER: builtins.int - max_number_of_attributes: builtins.int = ... - max_number_of_timed_events: builtins.int = ... - max_number_of_attributes_per_timed_event: builtins.int = ... - max_number_of_links: builtins.int = ... - max_number_of_attributes_per_link: builtins.int = ... - @property def constant_sampler(self) -> global___ConstantSampler: ... - @property def trace_id_ratio_based(self) -> global___TraceIdRatioBased: ... - @property def rate_limiting_sampler(self) -> global___RateLimitingSampler: ... + max_number_of_attributes: builtins.int = ... + """The global default max number of attributes per span.""" + + max_number_of_timed_events: builtins.int = ... + """The global default max number of annotation events per span.""" + + max_number_of_attributes_per_timed_event: builtins.int = ... + """The global default max number of attributes per timed event.""" + + max_number_of_links: builtins.int = ... + """The global default max number of link entries per span.""" + + max_number_of_attributes_per_link: builtins.int = ... + """The global default max number of attributes per span.""" def __init__(self, *, @@ -47,54 +56,68 @@ class TraceConfig(google.protobuf.message.Message): max_number_of_links : builtins.int = ..., max_number_of_attributes_per_link : builtins.int = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"constant_sampler",b"constant_sampler",u"rate_limiting_sampler",b"rate_limiting_sampler",u"sampler",b"sampler",u"trace_id_ratio_based",b"trace_id_ratio_based"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"constant_sampler",b"constant_sampler",u"max_number_of_attributes",b"max_number_of_attributes",u"max_number_of_attributes_per_link",b"max_number_of_attributes_per_link",u"max_number_of_attributes_per_timed_event",b"max_number_of_attributes_per_timed_event",u"max_number_of_links",b"max_number_of_links",u"max_number_of_timed_events",b"max_number_of_timed_events",u"rate_limiting_sampler",b"rate_limiting_sampler",u"sampler",b"sampler",u"trace_id_ratio_based",b"trace_id_ratio_based"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal[u"sampler",b"sampler"]) -> typing_extensions.Literal["constant_sampler","trace_id_ratio_based","rate_limiting_sampler"]: ... + def HasField(self, field_name: typing_extensions.Literal["constant_sampler",b"constant_sampler","rate_limiting_sampler",b"rate_limiting_sampler","sampler",b"sampler","trace_id_ratio_based",b"trace_id_ratio_based"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["constant_sampler",b"constant_sampler","max_number_of_attributes",b"max_number_of_attributes","max_number_of_attributes_per_link",b"max_number_of_attributes_per_link","max_number_of_attributes_per_timed_event",b"max_number_of_attributes_per_timed_event","max_number_of_links",b"max_number_of_links","max_number_of_timed_events",b"max_number_of_timed_events","rate_limiting_sampler",b"rate_limiting_sampler","sampler",b"sampler","trace_id_ratio_based",b"trace_id_ratio_based"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["sampler",b"sampler"]) -> typing.Optional[typing_extensions.Literal["constant_sampler","trace_id_ratio_based","rate_limiting_sampler"]]: ... global___TraceConfig = TraceConfig class ConstantSampler(google.protobuf.message.Message): + """Sampler that always makes a constant decision on span sampling.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - class _ConstantDecision(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ConstantDecision.V], builtins.type): + class ConstantDecision(_ConstantDecision, metaclass=_ConstantDecisionEnumTypeWrapper): + """How spans should be sampled: + - Always off + - Always on + - Always follow the parent Span's decision (off if no parent). + """ + pass + class _ConstantDecision: + V = typing.NewType('V', builtins.int) + class _ConstantDecisionEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_ConstantDecision.V], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... ALWAYS_OFF = ConstantSampler.ConstantDecision.V(0) ALWAYS_ON = ConstantSampler.ConstantDecision.V(1) ALWAYS_PARENT = ConstantSampler.ConstantDecision.V(2) - class ConstantDecision(metaclass=_ConstantDecision): - V = typing.NewType('V', builtins.int) + ALWAYS_OFF = ConstantSampler.ConstantDecision.V(0) ALWAYS_ON = ConstantSampler.ConstantDecision.V(1) ALWAYS_PARENT = ConstantSampler.ConstantDecision.V(2) DECISION_FIELD_NUMBER: builtins.int decision: global___ConstantSampler.ConstantDecision.V = ... - def __init__(self, *, decision : global___ConstantSampler.ConstantDecision.V = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"decision",b"decision"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["decision",b"decision"]) -> None: ... global___ConstantSampler = ConstantSampler class TraceIdRatioBased(google.protobuf.message.Message): + """Sampler that tries to uniformly sample traces with a given ratio. + The ratio of sampling a trace is equal to that of the specified ratio. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... SAMPLINGRATIO_FIELD_NUMBER: builtins.int samplingRatio: builtins.float = ... + """The desired ratio of sampling. Must be within [0.0, 1.0].""" def __init__(self, *, samplingRatio : builtins.float = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"samplingRatio",b"samplingRatio"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["samplingRatio",b"samplingRatio"]) -> None: ... global___TraceIdRatioBased = TraceIdRatioBased class RateLimitingSampler(google.protobuf.message.Message): + """Sampler that tries to sample with a rate per time window.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... QPS_FIELD_NUMBER: builtins.int qps: builtins.int = ... + """Rate per second.""" def __init__(self, *, qps : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"qps",b"qps"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["qps",b"qps"]) -> None: ... global___RateLimitingSampler = RateLimitingSampler diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py index b5ae44795c..d46196bcf5 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/trace/v1/trace.proto - +"""Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection @@ -20,6 +20,7 @@ package='opentelemetry.proto.trace.v1', syntax='proto3', serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1', + create_key=_descriptor._internal_create_key, serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xc2\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xbc\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xdd\x07\n\x06Status\x12V\n\x0f\x64\x65precated_code\x18\x01 \x01(\x0e\x32\x39.opentelemetry.proto.trace.v1.Status.DeprecatedStatusCodeB\x02\x18\x01\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"\xda\x05\n\x14\x44\x65precatedStatusCode\x12\x1d\n\x19\x44\x45PRECATED_STATUS_CODE_OK\x10\x00\x12$\n DEPRECATED_STATUS_CODE_CANCELLED\x10\x01\x12(\n$DEPRECATED_STATUS_CODE_UNKNOWN_ERROR\x10\x02\x12+\n\'DEPRECATED_STATUS_CODE_INVALID_ARGUMENT\x10\x03\x12,\n(DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED\x10\x04\x12$\n DEPRECATED_STATUS_CODE_NOT_FOUND\x10\x05\x12)\n%DEPRECATED_STATUS_CODE_ALREADY_EXISTS\x10\x06\x12,\n(DEPRECATED_STATUS_CODE_PERMISSION_DENIED\x10\x07\x12-\n)DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED\x10\x08\x12.\n*DEPRECATED_STATUS_CODE_FAILED_PRECONDITION\x10\t\x12\"\n\x1e\x44\x45PRECATED_STATUS_CODE_ABORTED\x10\n\x12\'\n#DEPRECATED_STATUS_CODE_OUT_OF_RANGE\x10\x0b\x12(\n$DEPRECATED_STATUS_CODE_UNIMPLEMENTED\x10\x0c\x12)\n%DEPRECATED_STATUS_CODE_INTERNAL_ERROR\x10\r\x12&\n\"DEPRECATED_STATUS_CODE_UNAVAILABLE\x10\x0e\x12$\n DEPRECATED_STATUS_CODE_DATA_LOSS\x10\x0f\x12*\n&DEPRECATED_STATUS_CODE_UNAUTHENTICATED\x10\x10\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -31,31 +32,38 @@ full_name='opentelemetry.proto.trace.v1.Span.SpanKind', filename=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='SPAN_KIND_UNSPECIFIED', index=0, number=0, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='SPAN_KIND_INTERNAL', index=1, number=1, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='SPAN_KIND_SERVER', index=2, number=2, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='SPAN_KIND_CLIENT', index=3, number=3, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='SPAN_KIND_PRODUCER', index=4, number=4, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='SPAN_KIND_CONSUMER', index=5, number=5, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, @@ -69,75 +77,93 @@ full_name='opentelemetry.proto.trace.v1.Status.DeprecatedStatusCode', filename=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_OK', index=0, number=0, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_CANCELLED', index=1, number=1, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_UNKNOWN_ERROR', index=2, number=2, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_INVALID_ARGUMENT', index=3, number=3, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED', index=4, number=4, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_NOT_FOUND', index=5, number=5, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_ALREADY_EXISTS', index=6, number=6, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_PERMISSION_DENIED', index=7, number=7, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED', index=8, number=8, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_FAILED_PRECONDITION', index=9, number=9, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_ABORTED', index=10, number=10, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_OUT_OF_RANGE', index=11, number=11, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_UNIMPLEMENTED', index=12, number=12, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_INTERNAL_ERROR', index=13, number=13, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_UNAVAILABLE', index=14, number=14, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_DATA_LOSS', index=15, number=15, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='DEPRECATED_STATUS_CODE_UNAUTHENTICATED', index=16, number=16, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, @@ -151,19 +177,23 @@ full_name='opentelemetry.proto.trace.v1.Status.StatusCode', filename=None, file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='STATUS_CODE_UNSET', index=0, number=0, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='STATUS_CODE_OK', index=1, number=1, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='STATUS_CODE_ERROR', index=2, number=2, serialized_options=None, - type=None), + type=None, + create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, @@ -179,6 +209,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='resource', full_name='opentelemetry.proto.trace.v1.ResourceSpans.resource', index=0, @@ -186,21 +217,21 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='instrumentation_library_spans', full_name='opentelemetry.proto.trace.v1.ResourceSpans.instrumentation_library_spans', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='schema_url', full_name='opentelemetry.proto.trace.v1.ResourceSpans.schema_url', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -224,6 +255,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='instrumentation_library', full_name='opentelemetry.proto.trace.v1.InstrumentationLibrarySpans.instrumentation_library', index=0, @@ -231,21 +263,21 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='spans', full_name='opentelemetry.proto.trace.v1.InstrumentationLibrarySpans.spans', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='schema_url', full_name='opentelemetry.proto.trace.v1.InstrumentationLibrarySpans.schema_url', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -269,6 +301,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='time_unix_nano', full_name='opentelemetry.proto.trace.v1.Span.Event.time_unix_nano', index=0, @@ -276,28 +309,28 @@ has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='name', full_name='opentelemetry.proto.trace.v1.Span.Event.name', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='attributes', full_name='opentelemetry.proto.trace.v1.Span.Event.attributes', index=2, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='dropped_attributes_count', full_name='opentelemetry.proto.trace.v1.Span.Event.dropped_attributes_count', index=3, number=4, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -320,6 +353,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='trace_id', full_name='opentelemetry.proto.trace.v1.Span.Link.trace_id', index=0, @@ -327,35 +361,35 @@ has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='span_id', full_name='opentelemetry.proto.trace.v1.Span.Link.span_id', index=1, number=2, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='trace_state', full_name='opentelemetry.proto.trace.v1.Span.Link.trace_state', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='attributes', full_name='opentelemetry.proto.trace.v1.Span.Link.attributes', index=3, number=4, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='dropped_attributes_count', full_name='opentelemetry.proto.trace.v1.Span.Link.dropped_attributes_count', index=4, number=5, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -378,6 +412,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='trace_id', full_name='opentelemetry.proto.trace.v1.Span.trace_id', index=0, @@ -385,105 +420,105 @@ has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='span_id', full_name='opentelemetry.proto.trace.v1.Span.span_id', index=1, number=2, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='trace_state', full_name='opentelemetry.proto.trace.v1.Span.trace_state', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='parent_span_id', full_name='opentelemetry.proto.trace.v1.Span.parent_span_id', index=3, number=4, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='name', full_name='opentelemetry.proto.trace.v1.Span.name', index=4, number=5, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='kind', full_name='opentelemetry.proto.trace.v1.Span.kind', index=5, number=6, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='start_time_unix_nano', full_name='opentelemetry.proto.trace.v1.Span.start_time_unix_nano', index=6, number=7, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='end_time_unix_nano', full_name='opentelemetry.proto.trace.v1.Span.end_time_unix_nano', index=7, number=8, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='attributes', full_name='opentelemetry.proto.trace.v1.Span.attributes', index=8, number=9, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='dropped_attributes_count', full_name='opentelemetry.proto.trace.v1.Span.dropped_attributes_count', index=9, number=10, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='events', full_name='opentelemetry.proto.trace.v1.Span.events', index=10, number=11, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='dropped_events_count', full_name='opentelemetry.proto.trace.v1.Span.dropped_events_count', index=11, number=12, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='links', full_name='opentelemetry.proto.trace.v1.Span.links', index=12, number=13, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='dropped_links_count', full_name='opentelemetry.proto.trace.v1.Span.dropped_links_count', index=13, number=14, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='status', full_name='opentelemetry.proto.trace.v1.Span.status', index=14, number=15, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -508,6 +543,7 @@ filename=None, file=DESCRIPTOR, containing_type=None, + create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='deprecated_code', full_name='opentelemetry.proto.trace.v1.Status.deprecated_code', index=0, @@ -515,21 +551,21 @@ has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR), + serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='message', full_name='opentelemetry.proto.trace.v1.Status.message', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='code', full_name='opentelemetry.proto.trace.v1.Status.code', index=2, number=3, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi index 46b37dc5d7..e187f03d5e 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi @@ -15,17 +15,26 @@ import typing_extensions DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... class ResourceSpans(google.protobuf.message.Message): + """A collection of InstrumentationLibrarySpans from a Resource.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... RESOURCE_FIELD_NUMBER: builtins.int INSTRUMENTATION_LIBRARY_SPANS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int - schema_url: typing.Text = ... - @property - def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: ... - + def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: + """The resource for the spans in this message. + If this field is not set then no resource info is known. + """ + pass @property - def instrumentation_library_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibrarySpans]: ... + def instrumentation_library_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibrarySpans]: + """A list of InstrumentationLibrarySpans that originate from a resource.""" + pass + schema_url: typing.Text = ... + """This schema_url applies to the data in the "resource" field. It does not apply + to the data in the "instrumentation_library_spans" field which have their own + schema_url field. + """ def __init__(self, *, @@ -33,22 +42,29 @@ class ResourceSpans(google.protobuf.message.Message): instrumentation_library_spans : typing.Optional[typing.Iterable[global___InstrumentationLibrarySpans]] = ..., schema_url : typing.Text = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"resource",b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library_spans",b"instrumentation_library_spans",u"resource",b"resource",u"schema_url",b"schema_url"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["resource",b"resource"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library_spans",b"instrumentation_library_spans","resource",b"resource","schema_url",b"schema_url"]) -> None: ... global___ResourceSpans = ResourceSpans class InstrumentationLibrarySpans(google.protobuf.message.Message): + """A collection of Spans produced by an InstrumentationLibrary.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int SPANS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int - schema_url: typing.Text = ... - @property - def instrumentation_library(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: ... - + def instrumentation_library(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: + """The instrumentation library information for the spans in this message. + Semantically when InstrumentationLibrary isn't set, it is equivalent with + an empty instrumentation library name (unknown). + """ + pass @property - def spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span]: ... + def spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span]: + """A list of Spans that originate from an instrumentation library.""" + pass + schema_url: typing.Text = ... + """This schema_url applies to all spans and span events in the "spans" field.""" def __init__(self, *, @@ -56,41 +72,120 @@ class InstrumentationLibrarySpans(google.protobuf.message.Message): spans : typing.Optional[typing.Iterable[global___Span]] = ..., schema_url : typing.Text = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"instrumentation_library",b"instrumentation_library",u"schema_url",b"schema_url",u"spans",b"spans"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library","schema_url",b"schema_url","spans",b"spans"]) -> None: ... global___InstrumentationLibrarySpans = InstrumentationLibrarySpans class Span(google.protobuf.message.Message): + """Span represents a single operation within a trace. Spans can be + nested to form a trace tree. Spans may also be linked to other spans + from the same or different trace and form graphs. Often, a trace + contains a root span that describes the end-to-end latency, and one + or more subspans for its sub-operations. A trace can also contain + multiple root spans, or none at all. Spans do not need to be + contiguous - there may be gaps or overlaps between spans in a trace. + + The next available field id is 17. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - class _SpanKind(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SpanKind.V], builtins.type): + class SpanKind(_SpanKind, metaclass=_SpanKindEnumTypeWrapper): + """SpanKind is the type of span. Can be used to specify additional relationships between spans + in addition to a parent/child relationship. + """ + pass + class _SpanKind: + V = typing.NewType('V', builtins.int) + class _SpanKindEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_SpanKind.V], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... SPAN_KIND_UNSPECIFIED = Span.SpanKind.V(0) + """Unspecified. Do NOT use as default. + Implementations MAY assume SpanKind to be INTERNAL when receiving UNSPECIFIED. + """ + SPAN_KIND_INTERNAL = Span.SpanKind.V(1) + """Indicates that the span represents an internal operation within an application, + as opposed to an operation happening at the boundaries. Default value. + """ + SPAN_KIND_SERVER = Span.SpanKind.V(2) + """Indicates that the span covers server-side handling of an RPC or other + remote network request. + """ + SPAN_KIND_CLIENT = Span.SpanKind.V(3) + """Indicates that the span describes a request to some remote service.""" + SPAN_KIND_PRODUCER = Span.SpanKind.V(4) + """Indicates that the span describes a producer sending a message to a broker. + Unlike CLIENT and SERVER, there is often no direct critical path latency relationship + between producer and consumer spans. A PRODUCER span ends when the message was accepted + by the broker while the logical processing of the message might span a much longer time. + """ + SPAN_KIND_CONSUMER = Span.SpanKind.V(5) - class SpanKind(metaclass=_SpanKind): - V = typing.NewType('V', builtins.int) + """Indicates that the span describes consumer receiving a message from a broker. + Like the PRODUCER kind, there is often no direct critical path latency relationship + between producer and consumer spans. + """ + + SPAN_KIND_UNSPECIFIED = Span.SpanKind.V(0) + """Unspecified. Do NOT use as default. + Implementations MAY assume SpanKind to be INTERNAL when receiving UNSPECIFIED. + """ + SPAN_KIND_INTERNAL = Span.SpanKind.V(1) + """Indicates that the span represents an internal operation within an application, + as opposed to an operation happening at the boundaries. Default value. + """ + SPAN_KIND_SERVER = Span.SpanKind.V(2) + """Indicates that the span covers server-side handling of an RPC or other + remote network request. + """ + SPAN_KIND_CLIENT = Span.SpanKind.V(3) + """Indicates that the span describes a request to some remote service.""" + SPAN_KIND_PRODUCER = Span.SpanKind.V(4) + """Indicates that the span describes a producer sending a message to a broker. + Unlike CLIENT and SERVER, there is often no direct critical path latency relationship + between producer and consumer spans. A PRODUCER span ends when the message was accepted + by the broker while the logical processing of the message might span a much longer time. + """ + SPAN_KIND_CONSUMER = Span.SpanKind.V(5) + """Indicates that the span describes consumer receiving a message from a broker. + Like the PRODUCER kind, there is often no direct critical path latency relationship + between producer and consumer spans. + """ + class Event(google.protobuf.message.Message): + """Event is a time-stamped annotation of the span, consisting of user-supplied + text description and key-value pairs. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... TIME_UNIX_NANO_FIELD_NUMBER: builtins.int NAME_FIELD_NUMBER: builtins.int ATTRIBUTES_FIELD_NUMBER: builtins.int DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int time_unix_nano: builtins.int = ... + """time_unix_nano is the time the event occurred.""" + name: typing.Text = ... - dropped_attributes_count: builtins.int = ... + """name of the event. + This field is semantically required to be set to non-empty string. + """ @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: + """attributes is a collection of attribute key/value pairs on the event.""" + pass + dropped_attributes_count: builtins.int = ... + """dropped_attributes_count is the number of dropped attributes. If the value is 0, + then no attributes were dropped. + """ def __init__(self, *, @@ -99,9 +194,14 @@ class Span(google.protobuf.message.Message): attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., dropped_attributes_count : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"name",b"name",u"time_unix_nano",b"time_unix_nano"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","dropped_attributes_count",b"dropped_attributes_count","name",b"name","time_unix_nano",b"time_unix_nano"]) -> None: ... class Link(google.protobuf.message.Message): + """A pointer from the current span to another span in the same trace or in a + different trace. For example, this can be used in batching operations, + where a single batch handler processes multiple requests from different + traces or when the handler receives a request from a different project. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... TRACE_ID_FIELD_NUMBER: builtins.int SPAN_ID_FIELD_NUMBER: builtins.int @@ -109,12 +209,24 @@ class Span(google.protobuf.message.Message): ATTRIBUTES_FIELD_NUMBER: builtins.int DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int trace_id: builtins.bytes = ... + """A unique identifier of a trace that this linked span is part of. The ID is a + 16-byte array. + """ + span_id: builtins.bytes = ... + """A unique identifier for the linked span. The ID is an 8-byte array.""" + trace_state: typing.Text = ... - dropped_attributes_count: builtins.int = ... + """The trace_state associated with the link.""" @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: + """attributes is a collection of attribute key/value pairs on the link.""" + pass + dropped_attributes_count: builtins.int = ... + """dropped_attributes_count is the number of dropped attributes. If the value is 0, + then no attributes were dropped. + """ def __init__(self, *, @@ -124,7 +236,7 @@ class Span(google.protobuf.message.Message): attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., dropped_attributes_count : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"span_id",b"span_id",u"trace_id",b"trace_id",u"trace_state",b"trace_state"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","dropped_attributes_count",b"dropped_attributes_count","span_id",b"span_id","trace_id",b"trace_id","trace_state",b"trace_state"]) -> None: ... TRACE_ID_FIELD_NUMBER: builtins.int SPAN_ID_FIELD_NUMBER: builtins.int @@ -142,29 +254,122 @@ class Span(google.protobuf.message.Message): DROPPED_LINKS_COUNT_FIELD_NUMBER: builtins.int STATUS_FIELD_NUMBER: builtins.int trace_id: builtins.bytes = ... + """A unique identifier for a trace. All spans from the same trace share + the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes + is considered invalid. + + This field is semantically required. Receiver should generate new + random trace_id if empty or invalid trace_id was received. + + This field is required. + """ + span_id: builtins.bytes = ... + """A unique identifier for a span within a trace, assigned when the span + is created. The ID is an 8-byte array. An ID with all zeroes is considered + invalid. + + This field is semantically required. Receiver should generate new + random span_id if empty or invalid span_id was received. + + This field is required. + """ + trace_state: typing.Text = ... + """trace_state conveys information about request position in multiple distributed tracing graphs. + It is a trace_state in w3c-trace-context format: https://www.w3.org/TR/trace-context/#tracestate-header + See also https://github.com/w3c/distributed-tracing for more details about this field. + """ + parent_span_id: builtins.bytes = ... + """The `span_id` of this span's parent span. If this is a root span, then this + field must be empty. The ID is an 8-byte array. + """ + name: typing.Text = ... + """A description of the span's operation. + + For example, the name can be a qualified method name or a file name + and a line number where the operation is called. A best practice is to use + the same display name at the same call point in an application. + This makes it easier to correlate spans in different traces. + + This field is semantically required to be set to non-empty string. + When null or empty string received - receiver may use string "name" + as a replacement. There might be smarted algorithms implemented by + receiver to fix the empty span name. + + This field is required. + """ + kind: global___Span.SpanKind.V = ... + """Distinguishes between spans generated in a particular context. For example, + two spans with the same name may be distinguished using `CLIENT` (caller) + and `SERVER` (callee) to identify queueing latency associated with the span. + """ + start_time_unix_nano: builtins.int = ... + """start_time_unix_nano is the start time of the span. On the client side, this is the time + kept by the local machine where the span execution starts. On the server side, this + is the time when the server's application handler starts running. + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + + This field is semantically required and it is expected that end_time >= start_time. + """ + end_time_unix_nano: builtins.int = ... - dropped_attributes_count: builtins.int = ... - dropped_events_count: builtins.int = ... - dropped_links_count: builtins.int = ... + """end_time_unix_nano is the end time of the span. On the client side, this is the time + kept by the local machine where the span execution ends. On the server side, this + is the time when the server application handler stops running. + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: ... + This field is semantically required and it is expected that end_time >= start_time. + """ @property - def events(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span.Event]: ... + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: + """attributes is a collection of key/value pairs. The value can be a string, + an integer, a double or the Boolean values `true` or `false`. Note, global attributes + like server name can be set using the resource API. Examples of attributes: + + "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" + "/http/server_latency": 300 + "abc.com/myattribute": true + "abc.com/score": 10.239 + """ + pass + dropped_attributes_count: builtins.int = ... + """dropped_attributes_count is the number of attributes that were discarded. Attributes + can be discarded because their keys are too long or because there are too many + attributes. If this value is 0, then no attributes were dropped. + """ @property - def links(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span.Link]: ... + def events(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span.Event]: + """events is a collection of Event items.""" + pass + dropped_events_count: builtins.int = ... + """dropped_events_count is the number of dropped events. If the value is 0, then no + events were dropped. + """ @property - def status(self) -> global___Status: ... + def links(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span.Link]: + """links is a collection of Links, which are references from this span to a span + in the same or different trace. + """ + pass + dropped_links_count: builtins.int = ... + """dropped_links_count is the number of dropped links after the maximum size was + enforced. If this value is 0, then no links were dropped. + """ + @property + def status(self) -> global___Status: + """An optional final status for this span. Semantically when Status isn't set, it means + span's status code is unset, i.e. assume STATUS_CODE_UNSET (code = 0). + """ + pass def __init__(self, *, trace_id : builtins.bytes = ..., @@ -183,13 +388,58 @@ class Span(google.protobuf.message.Message): dropped_links_count : builtins.int = ..., status : typing.Optional[global___Status] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal[u"status",b"status"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal[u"attributes",b"attributes",u"dropped_attributes_count",b"dropped_attributes_count",u"dropped_events_count",b"dropped_events_count",u"dropped_links_count",b"dropped_links_count",u"end_time_unix_nano",b"end_time_unix_nano",u"events",b"events",u"kind",b"kind",u"links",b"links",u"name",b"name",u"parent_span_id",b"parent_span_id",u"span_id",b"span_id",u"start_time_unix_nano",b"start_time_unix_nano",u"status",b"status",u"trace_id",b"trace_id",u"trace_state",b"trace_state"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["status",b"status"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","dropped_attributes_count",b"dropped_attributes_count","dropped_events_count",b"dropped_events_count","dropped_links_count",b"dropped_links_count","end_time_unix_nano",b"end_time_unix_nano","events",b"events","kind",b"kind","links",b"links","name",b"name","parent_span_id",b"parent_span_id","span_id",b"span_id","start_time_unix_nano",b"start_time_unix_nano","status",b"status","trace_id",b"trace_id","trace_state",b"trace_state"]) -> None: ... global___Span = Span class Status(google.protobuf.message.Message): + """The Status type defines a logical error model that is suitable for different + programming environments, including REST APIs and RPC APIs. + IMPORTANT: Backward compatibility notes: + + To ensure any pair of senders and receivers continues to correctly signal and + interpret erroneous situations, the senders and receivers MUST follow these rules: + + 1. Old senders and receivers that are not aware of `code` field will continue using + the `deprecated_code` field to signal and interpret erroneous situation. + + 2. New senders, which are aware of the `code` field MUST set both the + `deprecated_code` and `code` fields according to the following rules: + + if code==STATUS_CODE_UNSET then `deprecated_code` MUST be + set to DEPRECATED_STATUS_CODE_OK. + + if code==STATUS_CODE_OK then `deprecated_code` MUST be + set to DEPRECATED_STATUS_CODE_OK. + + if code==STATUS_CODE_ERROR then `deprecated_code` MUST be + set to DEPRECATED_STATUS_CODE_UNKNOWN_ERROR. + + These rules allow old receivers to correctly interpret data received from new senders. + + 3. New receivers MUST look at both the `code` and `deprecated_code` fields in order + to interpret the overall status: + + If code==STATUS_CODE_UNSET then the value of `deprecated_code` is the + carrier of the overall status according to these rules: + + if deprecated_code==DEPRECATED_STATUS_CODE_OK then the receiver MUST interpret + the overall status to be STATUS_CODE_UNSET. + + if deprecated_code!=DEPRECATED_STATUS_CODE_OK then the receiver MUST interpret + the overall status to be STATUS_CODE_ERROR. + + If code!=STATUS_CODE_UNSET then the value of `deprecated_code` MUST be + ignored, the `code` field is the sole carrier of the status. + + These rules allow new receivers to correctly interpret data received from old senders. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - class _DeprecatedStatusCode(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[DeprecatedStatusCode.V], builtins.type): + class DeprecatedStatusCode(_DeprecatedStatusCode, metaclass=_DeprecatedStatusCodeEnumTypeWrapper): + pass + class _DeprecatedStatusCode: + V = typing.NewType('V', builtins.int) + class _DeprecatedStatusCodeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_DeprecatedStatusCode.V], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... DEPRECATED_STATUS_CODE_OK = Status.DeprecatedStatusCode.V(0) DEPRECATED_STATUS_CODE_CANCELLED = Status.DeprecatedStatusCode.V(1) @@ -208,8 +458,7 @@ class Status(google.protobuf.message.Message): DEPRECATED_STATUS_CODE_UNAVAILABLE = Status.DeprecatedStatusCode.V(14) DEPRECATED_STATUS_CODE_DATA_LOSS = Status.DeprecatedStatusCode.V(15) DEPRECATED_STATUS_CODE_UNAUTHENTICATED = Status.DeprecatedStatusCode.V(16) - class DeprecatedStatusCode(metaclass=_DeprecatedStatusCode): - V = typing.NewType('V', builtins.int) + DEPRECATED_STATUS_CODE_OK = Status.DeprecatedStatusCode.V(0) DEPRECATED_STATUS_CODE_CANCELLED = Status.DeprecatedStatusCode.V(1) DEPRECATED_STATUS_CODE_UNKNOWN_ERROR = Status.DeprecatedStatusCode.V(2) @@ -228,23 +477,56 @@ class Status(google.protobuf.message.Message): DEPRECATED_STATUS_CODE_DATA_LOSS = Status.DeprecatedStatusCode.V(15) DEPRECATED_STATUS_CODE_UNAUTHENTICATED = Status.DeprecatedStatusCode.V(16) - class _StatusCode(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[StatusCode.V], builtins.type): + class StatusCode(_StatusCode, metaclass=_StatusCodeEnumTypeWrapper): + """For the semantics of status codes see + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-status + """ + pass + class _StatusCode: + V = typing.NewType('V', builtins.int) + class _StatusCodeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_StatusCode.V], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... STATUS_CODE_UNSET = Status.StatusCode.V(0) + """The default status.""" + STATUS_CODE_OK = Status.StatusCode.V(1) + """The Span has been validated by an Application developers or Operator to have + completed successfully. + """ + STATUS_CODE_ERROR = Status.StatusCode.V(2) - class StatusCode(metaclass=_StatusCode): - V = typing.NewType('V', builtins.int) + """The Span contains an error.""" + + STATUS_CODE_UNSET = Status.StatusCode.V(0) + """The default status.""" + STATUS_CODE_OK = Status.StatusCode.V(1) + """The Span has been validated by an Application developers or Operator to have + completed successfully. + """ + STATUS_CODE_ERROR = Status.StatusCode.V(2) + """The Span contains an error.""" + DEPRECATED_CODE_FIELD_NUMBER: builtins.int MESSAGE_FIELD_NUMBER: builtins.int CODE_FIELD_NUMBER: builtins.int deprecated_code: global___Status.DeprecatedStatusCode.V = ... + """The deprecated status code. This is an optional field. + + This field is deprecated and is replaced by the `code` field below. See backward + compatibility notes below. According to our stability guarantees this field + will be removed in 12 months, on Oct 22, 2021. All usage of old senders and + receivers that do not understand the `code` field MUST be phased out by then. + """ + message: typing.Text = ... + """A developer-facing human readable error message.""" + code: global___Status.StatusCode.V = ... + """The status code.""" def __init__(self, *, @@ -252,5 +534,5 @@ class Status(google.protobuf.message.Message): message : typing.Text = ..., code : global___Status.StatusCode.V = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal[u"code",b"code",u"deprecated_code",b"deprecated_code",u"message",b"message"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["code",b"code","deprecated_code",b"deprecated_code","message",b"message"]) -> None: ... global___Status = Status diff --git a/pyproject.toml b/pyproject.toml index eec7dacdcf..15e2fed2e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,11 +8,7 @@ exclude = ''' exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen| exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen| exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen| - opentelemetry-proto/src/opentelemetry/proto/collector| - opentelemetry-proto/src/opentelemetry/proto/common| - opentelemetry-proto/src/opentelemetry/proto/metrics| - opentelemetry-proto/src/opentelemetry/proto/resource| - opentelemetry-proto/src/opentelemetry/proto/trace + opentelemetry-proto/src/opentelemetry/proto/.*/.* )/ ) ''' From 73230b66c5497eef834d36dd7f15fa029316b5ce Mon Sep 17 00:00:00 2001 From: Nicolas Marier Date: Fri, 15 Oct 2021 23:02:07 -0400 Subject: [PATCH 1012/1517] fix(baggage): oversized header warn only with a header (#2212) This commit makes sure that warnings about the baggage header length are only emitted when the header is actually present, since it does not make sense to warn about a missing header's length. --- CHANGELOG.md | 2 ++ .../src/opentelemetry/baggage/propagation/__init__.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7335be2180..e1f3b7737e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgrade GRPC/protobuf related dependency and regenerate otlp protobufs ([#2201](https://github.com/open-telemetry/opentelemetry-python/pull/2201)) +- Propagation: only warn about oversized baggage headers when headers exist + ([#2212](https://github.com/open-telemetry/opentelemetry-python/pull/2212)) ## [1.6.0-0.25b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.0-0.25b0) - 2021-10-13 diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index 8ba28357c3..8430dc2301 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -53,7 +53,10 @@ def extract( getter.get(carrier, self._BAGGAGE_HEADER_NAME) ) - if not header or len(header) > self._MAX_HEADER_LENGTH: + if not header: + return context + + if len(header) > self._MAX_HEADER_LENGTH: _logger.warning( "Baggage header `%s` exceeded the maximum number of bytes per baggage-string", header, From 1e001868e3eafade5974f0fe6d67986cbf4e2d2a Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Sun, 17 Oct 2021 04:07:20 +0530 Subject: [PATCH 1013/1517] Remove left-over bootstrap_gen.py file (#2213) Co-authored-by: Srikanth Chekuri --- .../instrumentation/bootstrap_gen.py | 143 ------------------ 1 file changed, 143 deletions(-) delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py deleted file mode 100644 index 671cb2ba80..0000000000 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. -# RUN `python scripts/generate_instrumentation_bootstrap.py` TO REGENERATE. - -libraries = { - "aiohttp": { - "library": "aiohttp ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.25b0", - }, - "aiopg": { - "library": "aiopg >= 0.13.0, < 1.3.0", - "instrumentation": "opentelemetry-instrumentation-aiopg==0.25b0", - }, - "asgiref": { - "library": "asgiref ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-asgi==0.25b0", - }, - "asyncpg": { - "library": "asyncpg >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-asyncpg==0.25b0", - }, - "boto": { - "library": "boto~=2.0", - "instrumentation": "opentelemetry-instrumentation-boto==0.25b0", - }, - "botocore": { - "library": "botocore ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-botocore==0.25b0", - }, - "celery": { - "library": "celery >= 4.0, < 6.0", - "instrumentation": "opentelemetry-instrumentation-celery==0.25b0", - }, - "django": { - "library": "django >= 1.10", - "instrumentation": "opentelemetry-instrumentation-django==0.25b0", - }, - "elasticsearch": { - "library": "elasticsearch >= 2.0", - "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.25b0", - }, - "falcon": { - "library": "falcon >= 2.0.0, < 4.0.0", - "instrumentation": "opentelemetry-instrumentation-falcon==0.25b0", - }, - "fastapi": { - "library": "fastapi ~= 0.58", - "instrumentation": "opentelemetry-instrumentation-fastapi==0.25b0", - }, - "flask": { - "library": "flask >= 1.0, < 3.0", - "instrumentation": "opentelemetry-instrumentation-flask==0.25b0", - }, - "grpcio": { - "library": "grpcio ~= 1.27", - "instrumentation": "opentelemetry-instrumentation-grpc==0.25b0", - }, - "httpx": { - "library": "httpx >= 0.18.0, < 0.19.0", - "instrumentation": "opentelemetry-instrumentation-httpx==0.25b0", - }, - "jinja2": { - "library": "jinja2 >= 2.7, < 4.0", - "instrumentation": "opentelemetry-instrumentation-jinja2==0.25b0", - }, - "mysql-connector-python": { - "library": "mysql-connector-python ~= 8.0", - "instrumentation": "opentelemetry-instrumentation-mysql==0.25b0", - }, - "pika": { - "library": "pika >= 1.1.0", - "instrumentation": "opentelemetry-instrumentation-pika==0.25b0", - }, - "psycopg2": { - "library": "psycopg2 >= 2.7.3.1", - "instrumentation": "opentelemetry-instrumentation-psycopg2==0.25b0", - }, - "pymemcache": { - "library": "pymemcache ~= 1.3", - "instrumentation": "opentelemetry-instrumentation-pymemcache==0.25b0", - }, - "pymongo": { - "library": "pymongo ~= 3.1", - "instrumentation": "opentelemetry-instrumentation-pymongo==0.25b0", - }, - "PyMySQL": { - "library": "PyMySQL ~= 0.10.1", - "instrumentation": "opentelemetry-instrumentation-pymysql==0.25b0", - }, - "pyramid": { - "library": "pyramid >= 1.7", - "instrumentation": "opentelemetry-instrumentation-pyramid==0.25b0", - }, - "redis": { - "library": "redis >= 2.6", - "instrumentation": "opentelemetry-instrumentation-redis==0.25b0", - }, - "requests": { - "library": "requests ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-requests==0.25b0", - }, - "scikit-learn": { - "library": "scikit-learn ~= 0.24.0", - "instrumentation": "opentelemetry-instrumentation-sklearn==0.25b0", - }, - "sqlalchemy": { - "library": "sqlalchemy", - "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.25b0", - }, - "starlette": { - "library": "starlette ~= 0.13.0", - "instrumentation": "opentelemetry-instrumentation-starlette==0.25b0", - }, - "tornado": { - "library": "tornado >= 6.0", - "instrumentation": "opentelemetry-instrumentation-tornado==0.25b0", - }, - "urllib3": { - "library": "urllib3 >= 1.0.0, < 2.0.0", - "instrumentation": "opentelemetry-instrumentation-urllib3==0.25b0", - }, -} -default_instrumentations = [ - "opentelemetry-instrumentation-aws_lambda==0.25b0", - "opentelemetry-instrumentation-dbapi==0.25b0", - "opentelemetry-instrumentation-logging==0.25b0", - "opentelemetry-instrumentation-sqlite3==0.25b0", - "opentelemetry-instrumentation-urllib==0.25b0", - "opentelemetry-instrumentation-wsgi==0.25b0", -] From a1feaf4709a456b108b45b1cb23acfe9f927a445 Mon Sep 17 00:00:00 2001 From: Thanos Diacakis Date: Sun, 17 Oct 2021 11:27:39 -0700 Subject: [PATCH 1014/1517] Fixing typo (#2216) --- docs/examples/django/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 09ddd2638b..2e071127ff 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -113,7 +113,7 @@ Auto Instrumentation -------------------- This same example can be run using auto instrumentation. Comment out the call -to ``DjangoInstrumento().instrument()`` in ``main``, then Run the django app +to ``DjangoInstrumentor().instrument()`` in ``main``, then Run the django app with ``opentelemetry-instrument python manage.py runserver --noreload``. Repeat the steps with the client, the result should be the same. From ea00608996efa8c23dd9ec9cb9c5a2045192c547 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 18 Oct 2021 12:44:24 -0700 Subject: [PATCH 1015/1517] updating changelogs and version to 1.6.1-0.25b1 (#2223) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 6 +++++- eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 29 files changed, 38 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 192a11d9ca..f87613669c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: c2e674983a265e54c5eb14e376459a992498aae6 + CONTRIB_REPO_SHA: 95187df38312671ea171b68de8247838d6be65c1 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index e1f3b7737e..c5541542e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.6.0-0.25b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.6.1-0.25b1...HEAD) + +## [1.6.1-0.25b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.1-0.25b1) - 2021-10-18 + + - Upgrade GRPC/protobuf related dependency and regenerate otlp protobufs ([#2201](https://github.com/open-telemetry/opentelemetry-python/pull/2201)) diff --git a/eachdist.ini b/eachdist.ini index 52d5be769f..93df7637b3 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.6.0 +version=1.6.1 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.25b0 +version=0.25b1 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 7373c3ac09..632ab2dbf4 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 7373c3ac09..632ab2dbf4 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 91f4c8ba9b..b88933fda8 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,8 +39,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.6.0 - opentelemetry-exporter-jaeger-thrift == 1.6.0 + opentelemetry-exporter-jaeger-proto-grpc == 1.6.1 + opentelemetry-exporter-jaeger-thrift == 1.6.1 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 7373c3ac09..632ab2dbf4 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 2a05c9b361..45e421300d 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.25b0" +__version__ = "0.25b1" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 9d0fd68d04..9fae85360d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.6.0 + opentelemetry-proto == 1.6.1 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index e97f1318ec..2c32b963d2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index ad9311713a..ff590043e0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.6.0 + opentelemetry-proto == 1.6.1 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index e97f1318ec..2c32b963d2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 53ebc02975..091019e06f 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.6.0 - opentelemetry-exporter-otlp-proto-http == 1.6.0 + opentelemetry-exporter-otlp-proto-grpc == 1.6.1 + opentelemetry-exporter-otlp-proto-http == 1.6.1 [options.entry_points] opentelemetry_traces_exporter = diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index e97f1318ec..2c32b963d2 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index e97f1318ec..2c32b963d2 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 90d69c9478..a0942b8161 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.6.0 + opentelemetry-exporter-zipkin-json == 1.6.1 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index e97f1318ec..2c32b963d2 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index f4f5c5e083..a43f4ace89 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.6.0 - opentelemetry-exporter-zipkin-proto-http == 1.6.0 + opentelemetry-exporter-zipkin-json == 1.6.1 + opentelemetry-exporter-zipkin-proto-http == 1.6.1 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index e97f1318ec..2c32b963d2 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index e97f1318ec..2c32b963d2 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index e97f1318ec..2c32b963d2 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index bf063a237f..e4d60a76bb 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.6.0 - opentelemetry-semantic-conventions == 0.25b0 + opentelemetry-api == 1.6.1 + opentelemetry-semantic-conventions == 0.25b1 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index e97f1318ec..2c32b963d2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 2a05c9b361..45e421300d 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.25b0" +__version__ = "0.25b1" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index e97f1318ec..2c32b963d2 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index e97f1318ec..2c32b963d2 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 3e7277782d..070289986e 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -46,7 +46,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test == 0.25b0 + opentelemetry-test == 0.25b1 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 2a05c9b361..45e421300d 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.25b0" +__version__ = "0.25b1" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 0d5c9e9785..0a930e8419 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.25b0" +__version__ = "0.25b1" From 063681d2659c370db6d767a44d004deb4f4585e9 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 19 Oct 2021 00:16:41 -0700 Subject: [PATCH 1016/1517] default value is invalid, was renamed (#2226) --- .../src/opentelemetry/sdk/_configuration/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index f2ebd31749..b49e4d8e59 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -33,7 +33,7 @@ from opentelemetry.sdk.trace.id_generator import IdGenerator _EXPORTER_OTLP = "otlp" -_EXPORTER_OTLP_SPAN = "otlp_proto_grpc_span" +_EXPORTER_OTLP_SPAN = "otlp_proto_grpc" _RANDOM_ID_GENERATOR = "random" _DEFAULT_ID_GENERATOR = _RANDOM_ID_GENERATOR From 6b386896ededae6e5db81b78e1d25594a4b859f3 Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Tue, 19 Oct 2021 13:33:59 -0400 Subject: [PATCH 1017/1517] Website: support GH page links to canonical src, and aliases (#2224) --- website_docs/_index.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/website_docs/_index.md b/website_docs/_index.md index 216af049c7..30d11e274b 100644 --- a/website_docs/_index.md +++ b/website_docs/_index.md @@ -1,9 +1,15 @@ --- -title: "Python" -weight: 22 -description: > +title: Python +description: >- A language-specific implementation of OpenTelemetry in Python. +aliases: [/python, /python/metrics, /python/tracing] +cascade: + github_repo: &repo https://github.com/open-telemetry/opentelemetry-python + github_subdir: website_docs + path_base_for_github_subdir: content/en/docs/python/ + github_project_repo: *repo +weight: 22 --- This is the OpenTelemetry for Python documentation. OpenTelemetry is an From 9b25d7422432d4e14a164440a46d83b8f37afc13 Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Tue, 19 Oct 2021 18:48:40 +0100 Subject: [PATCH 1018/1517] Fix opentracing shim references (#2180) --- CHANGELOG.md | 5 +++++ .../shim/opentracing_shim/__init__.py | 19 +++++++++++++------ .../tests/test_shim.py | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5541542e0..e51bee4b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Propagation: only warn about oversized baggage headers when headers exist ([#2212](https://github.com/open-telemetry/opentelemetry-python/pull/2212)) +- Fix parental trace relationship for opentracing `follows_from` reference + ([#2180](https://github.com/open-telemetry/opentelemetry-python/pull/2180)) + + ## [1.6.0-0.25b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.0-0.25b0) - 2021-10-13 @@ -51,6 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add name to `BatchSpanProcessor` worker thread ([#2186](https://github.com/open-telemetry/opentelemetry-python/pull/2186)) + ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index 5777bd0edc..9bc9ee89f1 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -592,7 +592,10 @@ def start_active_span( current_span = get_current_span() - if child_of is None and current_span is not INVALID_SPAN_CONTEXT: + if ( + child_of is None + and current_span.get_span_context() is not INVALID_SPAN_CONTEXT + ): child_of = SpanShim(None, None, current_span) span = self.start_span( @@ -649,12 +652,16 @@ def start_span( if isinstance(parent, OtelSpanContext): parent = NonRecordingSpan(parent) - parent_span_context = set_span_in_context(parent) - - links = [] + valid_links = [] if references: for ref in references: - links.append(Link(ref.referenced_context.unwrap())) + if ref.referenced_context.unwrap() is not INVALID_SPAN_CONTEXT: + valid_links.append(Link(ref.referenced_context.unwrap())) + + if valid_links and parent is None: + parent = NonRecordingSpan(valid_links[0].context) + + parent_span_context = set_span_in_context(parent) # The OpenTracing API expects time values to be `float` values which # represent the number of seconds since the epoch. OpenTelemetry @@ -666,7 +673,7 @@ def start_span( span = self._otel_tracer.start_span( operation_name, context=parent_span_context, - links=links, + links=valid_links, attributes=tags, start_time=start_time_ns, ) diff --git a/shim/opentelemetry-opentracing-shim/tests/test_shim.py b/shim/opentelemetry-opentracing-shim/tests/test_shim.py index e27f779734..99394ad216 100644 --- a/shim/opentelemetry-opentracing-shim/tests/test_shim.py +++ b/shim/opentelemetry-opentracing-shim/tests/test_shim.py @@ -389,6 +389,24 @@ def test_references(self): parent.context.unwrap(), ) + def test_follows_from_references(self): + """Test span creation using the `references` argument with a follows from relationship.""" + + with self.shim.start_span("ParentSpan") as parent: + ref = opentracing.follows_from(parent.context) + + with self.shim.start_active_span( + "FollowingSpan", references=[ref] + ) as child: + self.assertEqual( + child.span.unwrap().links[0].context, + parent.context.unwrap(), + ) + self.assertEqual( + child.span.unwrap().parent, + parent.context.unwrap(), + ) + def test_set_operation_name(self): """Test `set_operation_name()` method.""" From 6f8ccf35a0ca0426d0f03d2e1d717807ce0d072c Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 20 Oct 2021 00:00:14 +0530 Subject: [PATCH 1019/1517] Prepare release v1.6.2 and 0.25b2 (#2230) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 4 +++- eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/util/src/opentelemetry/test/version.py | 2 +- 29 files changed, 36 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f87613669c..fe3dc01699 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 95187df38312671ea171b68de8247838d6be65c1 + CONTRIB_REPO_SHA: a7c054b257225948c68a9dccb3f2973537d9b4ec # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index e51bee4b57..9c3a1f592e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.6.1-0.25b1...HEAD) -## [1.6.1-0.25b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.1-0.25b1) - 2021-10-18 +## [1.6.2-0.25b2](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.2-0.25b2) - 2021-10-19 + +## [1.6.1-0.25b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.1-0.25b1) - 2021-10-18 - Upgrade GRPC/protobuf related dependency and regenerate otlp protobufs diff --git a/eachdist.ini b/eachdist.ini index 93df7637b3..e239102e27 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.6.1 +version=1.6.2 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.25b1 +version=0.25b2 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 632ab2dbf4..2078ab46b5 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 632ab2dbf4..2078ab46b5 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index b88933fda8..82b795d60e 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,8 +39,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.6.1 - opentelemetry-exporter-jaeger-thrift == 1.6.1 + opentelemetry-exporter-jaeger-proto-grpc == 1.6.2 + opentelemetry-exporter-jaeger-thrift == 1.6.2 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 632ab2dbf4..2078ab46b5 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 45e421300d..b3fbda5fcb 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.25b1" +__version__ = "0.25b2" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 9fae85360d..1be2ddf693 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.6.1 + opentelemetry-proto == 1.6.2 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 2c32b963d2..ce2cf11c1f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index ff590043e0..6d62341ff8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.6.1 + opentelemetry-proto == 1.6.2 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 2c32b963d2..ce2cf11c1f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 091019e06f..3e0887e2da 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.6.1 - opentelemetry-exporter-otlp-proto-http == 1.6.1 + opentelemetry-exporter-otlp-proto-grpc == 1.6.2 + opentelemetry-exporter-otlp-proto-http == 1.6.2 [options.entry_points] opentelemetry_traces_exporter = diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 2c32b963d2..ce2cf11c1f 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 2c32b963d2..ce2cf11c1f 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index a0942b8161..0d5c7b8a8c 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.6.1 + opentelemetry-exporter-zipkin-json == 1.6.2 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 2c32b963d2..ce2cf11c1f 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index a43f4ace89..d5bdf2110a 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.6.1 - opentelemetry-exporter-zipkin-proto-http == 1.6.1 + opentelemetry-exporter-zipkin-json == 1.6.2 + opentelemetry-exporter-zipkin-proto-http == 1.6.2 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 2c32b963d2..ce2cf11c1f 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 2c32b963d2..ce2cf11c1f 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 2c32b963d2..ce2cf11c1f 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index e4d60a76bb..b66f1e4604 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.6.1 - opentelemetry-semantic-conventions == 0.25b1 + opentelemetry-api == 1.6.2 + opentelemetry-semantic-conventions == 0.25b2 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 2c32b963d2..ce2cf11c1f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 45e421300d..b3fbda5fcb 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.25b1" +__version__ = "0.25b2" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 2c32b963d2..ce2cf11c1f 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 2c32b963d2..ce2cf11c1f 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 070289986e..45a622f799 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -46,7 +46,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test == 0.25b1 + opentelemetry-test == 0.25b2 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 45e421300d..b3fbda5fcb 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.25b1" +__version__ = "0.25b2" diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 0a930e8419..c5894b3aa5 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.25b1" +__version__ = "0.25b2" From f6e793aed73b7dc97cf372ee82c8851444bb2fbf Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 19 Oct 2021 11:44:20 -0700 Subject: [PATCH 1020/1517] update changelog to note the rename of entrypoints in 1.6.0 (#2228) --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c3a1f592e..471e1acbd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.6.1-0.25b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.1-0.25b1) - 2021-10-18 - - Upgrade GRPC/protobuf related dependency and regenerate otlp protobufs ([#2201](https://github.com/open-telemetry/opentelemetry-python/pull/2201)) - Propagation: only warn about oversized baggage headers when headers exist @@ -23,7 +22,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.6.0-0.25b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.0-0.25b0) - 2021-10-13 - - Fix race in `set_tracer_provider()` ([#2182](https://github.com/open-telemetry/opentelemetry-python/pull/2182)) - Automatically load OTEL environment variables as options for `opentelemetry-instrument` @@ -36,6 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2071](https://github.com/open-telemetry/opentelemetry-python/pull/2071)) - Add entry point for exporters with default protocol ([#2093](https://github.com/open-telemetry/opentelemetry-python/pull/2093)) +- Renamed entrypoints `otlp_proto_http_span`, `otlp_proto_grpc_span`, `console_span` to remove + redundant `_span` suffix. + ([#2093](https://github.com/open-telemetry/opentelemetry-python/pull/2093)) - Do not skip sequence attribute on decode error ([#2097](https://github.com/open-telemetry/opentelemetry-python/pull/2097)) - `opentelemetry-test`: Add `HttpTestBase` to allow tests with actual TCP sockets From 67d65ba924f57171c65ae271d8bd8526744aa1de Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 21 Oct 2021 07:05:17 -0700 Subject: [PATCH 1021/1517] add note about proto version to README.rst (#2232) --- opentelemetry-proto/README.rst | 5 +++++ scripts/proto_codegen.sh | 2 ++ 2 files changed, 7 insertions(+) diff --git a/opentelemetry-proto/README.rst b/opentelemetry-proto/README.rst index d8cff6e68c..03c53dbb67 100644 --- a/opentelemetry-proto/README.rst +++ b/opentelemetry-proto/README.rst @@ -6,6 +6,11 @@ OpenTelemetry Python Proto .. |pypi| image:: https://badge.fury.io/py/opentelemetry-proto.svg :target: https://pypi.org/project/opentelemetry-proto/ +This library contains the generated code for OpenTelemetry protobuf data model. The code in the current +package was generated using the v0.9.0 release_ of opentelemetry-proto. + +.. _release: https://github.com/open-telemetry/opentelemetry-proto/releases/tag/v0.9.0 + Installation ------------ diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index 6833c85c4e..9fd2dbe60e 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -70,3 +70,5 @@ python -m grpc_tools.protoc \ --mypy_out=. \ --grpc_python_out=. \ $service_protos + +echo "Please update ./opentelemetry-proto/README.rst to include the updated version." From a002f7aedcf48c5329645182bbcf82efc037a3fa Mon Sep 17 00:00:00 2001 From: Ted Kern Date: Fri, 22 Oct 2021 11:04:21 -0700 Subject: [PATCH 1022/1517] ReadableSpan events and links now return a tuple (#2215) * ReadableSpan events and links now return a tuple Removed MappingProxy since events and links are not mappings Signed-off-by: Ted Kern * fix lint Signed-off-by: Ted Kern * fix lint * fix lint Co-authored-by: Diego Hurtado Co-authored-by: Leighton Chen --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/__init__.py | 6 ++--- opentelemetry-sdk/tests/trace/test_trace.py | 23 +++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 471e1acbd5..b18bf50216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.6.1-0.25b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.1-0.25b1) - 2021-10-18 +- Fix ReadableSpan property types attempting to create a mapping from a list + ([#2215](https://github.com/open-telemetry/opentelemetry-python/pull/2215)) - Upgrade GRPC/protobuf related dependency and regenerate otlp protobufs ([#2201](https://github.com/open-telemetry/opentelemetry-python/pull/2201)) - Propagation: only warn about oversized baggage headers when headers exist diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 38ecf1c316..35fefc3e3f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -350,7 +350,7 @@ def __init__( parent: Optional[trace_api.SpanContext] = None, resource: Resource = Resource.create({}), attributes: types.Attributes = None, - events: Sequence[Event] = None, + events: Sequence[Event] = (), links: Sequence[trace_api.Link] = (), kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, instrumentation_info: InstrumentationInfo = None, @@ -426,11 +426,11 @@ def attributes(self) -> types.Attributes: @property def events(self) -> Sequence[Event]: - return MappingProxyType(self._events) + return tuple(event for event in self._events) @property def links(self) -> Sequence[trace_api.Link]: - return MappingProxyType(self._links) + return tuple(link for link in self._links) @property def resource(self) -> Resource: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 0a5c2d349c..487c537c18 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -569,6 +569,29 @@ def test_surplus_span_attributes(self): self.assertEqual(len(root.attributes), max_attrs) +class TestReadableSpan(unittest.TestCase): + def test_links(self): + span = trace.ReadableSpan() + self.assertEqual(span.links, ()) + + span = trace.ReadableSpan( + links=[trace_api.Link(context=trace_api.INVALID_SPAN_CONTEXT)] * 2, + ) + self.assertEqual(len(span.links), 2) + for link in span.links: + self.assertFalse(link.context.is_valid) + + def test_events(self): + span = trace.ReadableSpan() + self.assertEqual(span.events, ()) + events = [ + trace.Event("foo1", {"bar1": "baz1"}), + trace.Event("foo2", {"bar2": "baz2"}), + ] + span = trace.ReadableSpan(events=events) + self.assertEqual(span.events, tuple(events)) + + class TestSpan(unittest.TestCase): # pylint: disable=too-many-public-methods From 10231d127f5bef6d1e9bc88a7cbd455c2e560c0d Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 25 Oct 2021 11:17:33 -0700 Subject: [PATCH 1023/1517] adding CI testing for python 3.10 (#2207) --- .github/workflows/test.yml | 3 +- CHANGELOG.md | 5 +- .../error_handler/error_handler_0/setup.cfg | 1 + .../error_handler/error_handler_1/setup.cfg | 1 + .../setup.cfg | 1 + .../jaeger/proto/grpc/gen/collector_pb2.py | 2 +- .../setup.cfg | 1 + .../opentelemetry-exporter-jaeger/setup.cfg | 1 + .../setup.cfg | 1 + .../setup.cfg | 1 + .../setup.cfg | 1 + .../opentelemetry-exporter-otlp/setup.cfg | 1 + .../setup.cfg | 1 + .../setup.cfg | 1 + .../opentelemetry-exporter-zipkin/setup.cfg | 1 + opentelemetry-api/setup.cfg | 1 + opentelemetry-distro/setup.cfg | 62 +++++++++++++++++++ opentelemetry-proto/setup.cfg | 1 + opentelemetry-sdk/setup.cfg | 1 + opentelemetry-semantic-conventions/setup.cfg | 1 + .../opentelemetry-propagator-b3/setup.cfg | 1 + .../opentelemetry-propagator-jaeger/setup.cfg | 1 + shim/opentelemetry-opentracing-shim/setup.cfg | 1 + tests/util/setup.cfg | 1 + tox.ini | 38 ++++++------ 25 files changed, 107 insertions(+), 23 deletions(-) create mode 100644 opentelemetry-distro/setup.cfg diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe3dc01699..2abba7c24a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,13 +27,14 @@ jobs: py37: 3.7 py38: 3.8 py39: 3.9 + py310: "3.10" pypy3: pypy-3.7 RUN_MATRIX_COMBINATION: ${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [ py36, py37, py38, py39, pypy3 ] + python-version: [ py36, py37, py38, py39, py310, pypy3 ] package: ["api", "sdk", "semantic", "getting", "shim", "exporter", "protobuf", "propagator"] os: [ ubuntu-20.04, windows-2019 ] steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index b18bf50216..6e67c36b29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.6.1-0.25b1...HEAD) -## [1.6.2-0.25b2](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.2-0.25b2) - 2021-10-19 +- Add support for Python 3.10 + ([#2207](https://github.com/open-telemetry/opentelemetry-python/pull/2207)) +## [1.6.2-0.25b2](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.2-0.25b2) - 2021-10-19 ## [1.6.1-0.25b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.1-0.25b1) - 2021-10-18 @@ -21,7 +23,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix parental trace relationship for opentracing `follows_from` reference ([#2180](https://github.com/open-telemetry/opentelemetry-python/pull/2180)) - ## [1.6.0-0.25b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.0-0.25b0) - 2021-10-13 - Fix race in `set_tracer_provider()` diff --git a/docs/examples/error_handler/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg index 919ece78d6..9eced88d7c 100644 --- a/docs/examples/error_handler/error_handler_0/setup.cfg +++ b/docs/examples/error_handler/error_handler_0/setup.cfg @@ -29,6 +29,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 [options] python_requires = >=3.6 diff --git a/docs/examples/error_handler/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg index 2012fd81b1..8d09f423db 100644 --- a/docs/examples/error_handler/error_handler_1/setup.cfg +++ b/docs/examples/error_handler/error_handler_1/setup.cfg @@ -29,6 +29,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 [options] python_requires = >=3.6 diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index 4f4228f9c7..dc4ff16c24 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Typing :: Typed [options] diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2.py index 87dffbbc6d..cdb8562a94 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2.py @@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default() -import model_pb2 as model__pb2 +from opentelemetry.exporter.jaeger.proto.grpc.gen import model_pb2 as model__pb2 from gogoproto import gogo_pb2 as gogoproto_dot_gogo__pb2 from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 from protoc_gen_swagger.options import annotations_pb2 as protoc__gen__swagger_dot_options_dot_annotations__pb2 diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index e2c98a4809..7885c053a9 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Typing :: Typed [options] diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 82b795d60e..2cb1b10a3b 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Typing :: Typed [options] diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index 1e10bc7b96..b9dcdbc820 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Typing :: Typed [options] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 1be2ddf693..004be68f68 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 [options] python_requires = >=3.6 diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index 6d62341ff8..5470386391 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 [options] python_requires = >=3.6 diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 3e0887e2da..746ebc96d4 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Typing :: Typed [options] diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index 2517b52326..99d5e3327a 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -33,6 +33,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Typing :: Typed [options] diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 0d5c7b8a8c..4198f69b10 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -33,6 +33,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Typing :: Typed [options] diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index d5bdf2110a..f448f6c8d6 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Typing :: Typed [options] diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 91a38ac927..21ae69460b 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Typing :: Typed [options] diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg new file mode 100644 index 0000000000..22dbe1c525 --- /dev/null +++ b/opentelemetry-distro/setup.cfg @@ -0,0 +1,62 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-distro +description = OpenTelemetry Python Distro +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-distro +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Typing :: Typed + +[options] +python_requires = >=3.6 +package_dir= + =src +packages=find_namespace: +zip_safe = False +include_package_data = True +install_requires = + opentelemetry-api ~= 1.3 + opentelemetry-instrumentation == 0.24b0 + opentelemetry-sdk == 1.5.0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_distro = + distro = opentelemetry.distro:OpenTelemetryDistro +opentelemetry_configurator = + configurator = opentelemetry.distro:OpenTelemetryConfigurator + +[options.extras_require] +test = +otlp = + opentelemetry-exporter-otlp == 1.5.0 diff --git a/opentelemetry-proto/setup.cfg b/opentelemetry-proto/setup.cfg index 0eaef7cfc5..6f19ae8fd3 100644 --- a/opentelemetry-proto/setup.cfg +++ b/opentelemetry-proto/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 [options] python_requires = >=3.6 diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index b66f1e4604..1996249573 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Typing :: Typed [options] diff --git a/opentelemetry-semantic-conventions/setup.cfg b/opentelemetry-semantic-conventions/setup.cfg index 81565ec00b..377bb6ffdf 100644 --- a/opentelemetry-semantic-conventions/setup.cfg +++ b/opentelemetry-semantic-conventions/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 [options] python_requires = >=3.6 diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index 1c879987fd..ee9dbc4d15 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Typing :: Typed [options] diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index 639a07d3d9..0f6c8fe249 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Typing :: Typed [options] diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 45a622f799..5d857a571c 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Typing :: Typed [options] diff --git a/tests/util/setup.cfg b/tests/util/setup.cfg index 5bc0f69948..5274a2d876 100644 --- a/tests/util/setup.cfg +++ b/tests/util/setup.cfg @@ -30,6 +30,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 [options] python_requires = >=3.6 diff --git a/tox.ini b/tox.ini index 5398bdd80e..75772516d7 100644 --- a/tox.ini +++ b/tox.ini @@ -4,58 +4,58 @@ skip_missing_interpreters = True envlist = ; Environments are organized by individual package, allowing ; for specifying supported Python versions per package. - py3{6,7,8,9}-opentelemetry-api + py3{6,7,8,9,10}-opentelemetry-api pypy3-opentelemetry-api - py3{6,7,8,9}-opentelemetry-protobuf + py3{6,7,8,9,10}-opentelemetry-protobuf pypy3-opentelemetry-protobuf - py3{6,7,8,9}-opentelemetry-sdk + py3{6,7,8,9,10}-opentelemetry-sdk pypy3-opentelemetry-sdk - py3{6,7,8,9}-opentelemetry-semantic-conventions + py3{6,7,8,9,10}-opentelemetry-semantic-conventions pypy3-opentelemetry-semantic-conventions ; docs/getting-started - py3{6,7,8,9}-opentelemetry-getting-started + py3{6,7,8,9,10}-opentelemetry-getting-started pypy3-opentelemetry-getting-started - py3{6,7,8,9}-opentelemetry-opentracing-shim + py3{6,7,8,9,10}-opentelemetry-opentracing-shim pypy3-opentelemetry-opentracing-shim - py3{6,7,8,9}-opentelemetry-exporter-jaeger-combined + py3{6,7,8,9,10}-opentelemetry-exporter-jaeger-combined - py3{6,7,8,9}-opentelemetry-exporter-jaeger-proto-grpc + py3{6,7,8,9,10}-opentelemetry-exporter-jaeger-proto-grpc - py3{6,7,8,9}-opentelemetry-exporter-jaeger-thrift + py3{6,7,8,9,10}-opentelemetry-exporter-jaeger-thrift - py3{6,7,8,9}-opentelemetry-exporter-opencensus + py3{6,7,8,9,10}-opentelemetry-exporter-opencensus ; exporter-opencensus intentionally excluded from pypy3 ; opentelemetry-exporter-otlp - py3{6,7,8,9}-opentelemetry-exporter-otlp-combined + py3{6,7,8,9,10}-opentelemetry-exporter-otlp-combined ; intentionally excluded from pypy3 - py3{6,7,8,9}-opentelemetry-exporter-otlp-proto-grpc + py3{6,7,8,9,10}-opentelemetry-exporter-otlp-proto-grpc ; intentionally excluded from pypy3 - py3{6,7,8,9}-opentelemetry-exporter-otlp-proto-http + py3{6,7,8,9,10}-opentelemetry-exporter-otlp-proto-http pypy3-opentelemetry-exporter-otlp-proto-http ; opentelemetry-exporter-zipkin - py3{6,7,8,9}-opentelemetry-exporter-zipkin-combined + py3{6,7,8,9,10}-opentelemetry-exporter-zipkin-combined pypy3-opentelemetry-exporter-zipkin-combined - py3{6,7,8,9}-opentelemetry-exporter-zipkin-proto-http + py3{6,7,8,9,10}-opentelemetry-exporter-zipkin-proto-http pypy3-opentelemetry-exporter-zipkin-proto-http - py3{6,7,8,9}-opentelemetry-exporter-zipkin-json + py3{6,7,8,9,10}-opentelemetry-exporter-zipkin-json pypy3-opentelemetry-exporter-zipkin-json - py3{6,7,8,9}-opentelemetry-propagator-b3 + py3{6,7,8,9,10}-opentelemetry-propagator-b3 pypy3-opentelemetry-propagator-b3 - py3{6,7,8,9}-opentelemetry-propagator-jaeger + py3{6,7,8,9,10}-opentelemetry-propagator-jaeger pypy3-opentelemetry-propagator-jaeger lint @@ -105,7 +105,7 @@ changedir = commands_pre = ; Install without -e to test the actual installation - py3{6,7,8,9}: python -m pip install -U pip setuptools wheel + py3{6,7,8,9,10}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. opentelemetry: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util From 210963395fa67b3b10de98dbff43c6bc80173652 Mon Sep 17 00:00:00 2001 From: James <45812677+JamesJHPark@users.noreply.github.com> Date: Tue, 26 Oct 2021 08:28:32 -0700 Subject: [PATCH 1024/1517] remove `X-B3-ParentSpanId` for B3 propagator as per OpenTelemetry Specification (#2237) * remove X-B3-ParentSpanId for B3 propagator as per OpenTelemetry specification * revert changes - run tests * re-commit changes * add entry to CHANGELOG * remove ParentSpanId in B3SingleFormat --- CHANGELOG.md | 2 ++ .../src/opentelemetry/propagators/b3/__init__.py | 13 ------------- .../trace/propagation/test_benchmark_b3_format.py | 3 +-- .../tests/test_b3_format.py | 6 +----- 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e67c36b29..85ce4ddeee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add support for Python 3.10 ([#2207](https://github.com/open-telemetry/opentelemetry-python/pull/2207)) +- remove `X-B3-ParentSpanId` for B3 propagator as per OpenTelemetry specification + ([#2237](https://github.com/open-telemetry/opentelemetry-python/pull/2237)) ## [1.6.2-0.25b2](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.2-0.25b2) - 2021-10-19 diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py index 81c8167d74..1254bfeac0 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py @@ -40,7 +40,6 @@ class B3MultiFormat(TextMapPropagator): SINGLE_HEADER_KEY = "b3" TRACE_ID_KEY = "x-b3-traceid" SPAN_ID_KEY = "x-b3-spanid" - PARENT_SPAN_ID_KEY = "x-b3-parentspanid" SAMPLED_KEY = "x-b3-sampled" FLAGS_KEY = "x-b3-flags" _SAMPLE_PROPAGATE_VALUES = set(["1", "True", "true", "d"]) @@ -149,13 +148,6 @@ def inject( setter.set( carrier, self.SPAN_ID_KEY, format_span_id(span_context.span_id) ) - span_parent = getattr(span, "parent", None) - if span_parent is not None: - setter.set( - carrier, - self.PARENT_SPAN_ID_KEY, - format_span_id(span_parent.span_id), - ) setter.set(carrier, self.SAMPLED_KEY, "1" if sampled else "0") @property @@ -163,7 +155,6 @@ def fields(self) -> typing.Set[str]: return { self.TRACE_ID_KEY, self.SPAN_ID_KEY, - self.PARENT_SPAN_ID_KEY, self.SAMPLED_KEY, } @@ -195,10 +186,6 @@ def inject( "1" if sampled else "0", ] - span_parent = getattr(span, "parent", None) - if span_parent: - fields.append(format_span_id(span_parent.span_id)) - setter.set(carrier, self.SINGLE_HEADER_KEY, "-".join(fields)) @property diff --git a/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py index 3a7a251ad8..23cbf773ed 100644 --- a/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/performance/benchmarks/trace/propagation/test_benchmark_b3_format.py @@ -22,7 +22,7 @@ def test_extract_single_header(benchmark): benchmark( FORMAT.extract, { - FORMAT.SINGLE_HEADER_KEY: "bdb5b63237ed38aea578af665aa5aa60-c32d953d73ad2251-1-11fd79a30b0896cd285b396ae102dd76" + FORMAT.SINGLE_HEADER_KEY: "bdb5b63237ed38aea578af665aa5aa60-c32d953d73ad2251-1" }, ) @@ -36,7 +36,6 @@ def test_inject_empty_context(benchmark): { FORMAT.TRACE_ID_KEY: "bdb5b63237ed38aea578af665aa5aa60", FORMAT.SPAN_ID_KEY: "00000000000000000c32d953d73ad225", - FORMAT.PARENT_SPAN_ID_KEY: "11fd79a30b0896cd285b396ae102dd76", FORMAT.SAMPLED_KEY: "1", }, ) diff --git a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py index f8c9b0e882..a4c51b90c1 100644 --- a/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py +++ b/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py @@ -65,9 +65,6 @@ def setUpClass(cls): cls.serialized_span_id = trace_api.format_span_id( generator.generate_span_id() ) - cls.serialized_parent_id = trace_api.format_span_id( - generator.generate_span_id() - ) def setUp(self) -> None: tracer_provider = trace.TracerProvider() @@ -103,7 +100,6 @@ def test_extract_multi_header(self): context = { propagator.TRACE_ID_KEY: self.serialized_trace_id, propagator.SPAN_ID_KEY: self.serialized_span_id, - propagator.PARENT_SPAN_ID_KEY: self.serialized_parent_id, propagator.SAMPLED_KEY: "1", } child, parent, _ = self.get_child_parent_new_carrier(context) @@ -142,7 +138,7 @@ def test_extract_single_header(self): child, parent, _ = self.get_child_parent_new_carrier( { - propagator.SINGLE_HEADER_KEY: f"{self.serialized_trace_id}-{self.serialized_span_id}-1-{self.serialized_parent_id}" + propagator.SINGLE_HEADER_KEY: f"{self.serialized_trace_id}-{self.serialized_span_id}-1" } ) From 05e9fe171fdfd6a4d7d2336f6ac41bab69c8a71f Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Wed, 27 Oct 2021 16:27:12 -0400 Subject: [PATCH 1025/1517] Remove lingering distro pkg files (#2241) --- opentelemetry-distro/setup.cfg | 62 ---------------------------------- 1 file changed, 62 deletions(-) delete mode 100644 opentelemetry-distro/setup.cfg diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg deleted file mode 100644 index 22dbe1c525..0000000000 --- a/opentelemetry-distro/setup.cfg +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-distro -description = OpenTelemetry Python Distro -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-distro -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.6 -package_dir= - =src -packages=find_namespace: -zip_safe = False -include_package_data = True -install_requires = - opentelemetry-api ~= 1.3 - opentelemetry-instrumentation == 0.24b0 - opentelemetry-sdk == 1.5.0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_distro = - distro = opentelemetry.distro:OpenTelemetryDistro -opentelemetry_configurator = - configurator = opentelemetry.distro:OpenTelemetryConfigurator - -[options.extras_require] -test = -otlp = - opentelemetry-exporter-otlp == 1.5.0 From 6592f1d3670de522659698d42f227c074de2f3b6 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 24 Sep 2021 18:17:11 +0200 Subject: [PATCH 1026/1517] Adds metrics API (#1887) * Adds metric prototype Fixes #1835 * Fix docs * Add API metrics doc * Add missing docs * Add files * Adding docs * Refactor to _initialize * Refactor initialize * Add more documentation * Add exporter test * Add process * Fix tests * Try to add aggregator_class argument Tests are failing here * Fix instrument parent classes * Test default aggregator * WIP * Add prototype test * Tests passing again * Use right counters * All tests passing * Rearrange instrument storage * Fix tests * Add HTTP server test * WIP * WIP * Add prototype * WIP * Fail the test * WIP * WIP * WIP * WIP * Add views * Discard instruments via views * Fix tests * WIP * WIP * Fix lint * WIP * Fix test * Fix lint * Fix method * Fix lint * Mypy workaround * Skip if 3.6 * Fix lint * Add reason * Fix 3.6 * Fix run * Fix lint * Remove SDK metrics * Remove SDK docs * Remove metrics * Remove assertnotraises mixin * Revert sdk docs conf * Remove SDK env var changes * Fix unit checking * Define positional-only arguments * Add Metrics plans * Add API tests * WIP * WIP test * WIP * WIP * WIP * Set provider test passing * Use a fixture * Add test for get_provider * Rename tests * WIP * WIP * WIP * WIP * Remove non specific requirement * Add meter requirements * Put all meter provider tests in one file * Add meter tests * Make attributes be passed as a dictionary * Make some interfaces private * Log an error instead * Remove ASCII flag * Add CHANGELOG entry * Add instrument tests * All tests passing * Add test * Add name tests * Add unit tests * Add description tests * Add counter tests * Add more tests * Add Histogram tests * Add observable gauge tests * Add updowncounter tests * Add observableupdowncounter tests * Fix lint * Fix docs * Fix lint * Ignore mypy * Remove useless pylint skip * Remove useless pylint skip * Remove useless pylint skip * Remove useless pylint skip * Remove useless pylint skip * Add locks to meter and meterprovider * Add lock to instruments * Fix fixmes * Fix lint * Add documentation placeholder * Remove blank line as requested. * Do not override Rlock * Remove unecessary super calls * Add missing super calls * Remove plan files * Add missing parameters * Rename observe to callback * Fix lint * Rename to secure_instrument_name * Remove locks * Fix lint * Remove args and kwargs * Remove implementation that gives meters access to meter provider * Allow creating async instruments with either a callback function or generator * add additional test with callback form of observable counter * add a test/example that reads measurements from proc stat * implement cpu time integration test with generator too Co-authored-by: Aaron Abbott --- CHANGELOG.md | 2 + docs/api/api.rst | 1 + docs/api/metrics.instrument.rst | 8 + docs/api/metrics.measurement.rst | 7 + docs/api/metrics.rst | 15 + .../opentelemetry/environment_variables.py | 11 + .../src/opentelemetry/metrics/__init__.py | 384 +++++++++ .../src/opentelemetry/metrics/instrument.py | 234 +++++ .../src/opentelemetry/metrics/measurement.py | 39 + .../metrics/integration_test/test_cpu_time.py | 194 +++++ .../tests/metrics/test_instruments.py | 804 ++++++++++++++++++ opentelemetry-api/tests/metrics/test_meter.py | 179 ++++ .../tests/metrics/test_meter_provider.py | 260 ++++++ tests/util/src/opentelemetry/test/__init__.py | 0 tox.ini | 2 +- 15 files changed, 2139 insertions(+), 1 deletion(-) create mode 100644 docs/api/metrics.instrument.rst create mode 100644 docs/api/metrics.measurement.rst create mode 100644 docs/api/metrics.rst create mode 100644 opentelemetry-api/src/opentelemetry/metrics/__init__.py create mode 100644 opentelemetry-api/src/opentelemetry/metrics/instrument.py create mode 100644 opentelemetry-api/src/opentelemetry/metrics/measurement.py create mode 100644 opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py create mode 100644 opentelemetry-api/tests/metrics/test_instruments.py create mode 100644 opentelemetry-api/tests/metrics/test_meter.py create mode 100644 opentelemetry-api/tests/metrics/test_meter_provider.py delete mode 100644 tests/util/src/opentelemetry/test/__init__.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 85ce4ddeee..37c0b3b9b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2207](https://github.com/open-telemetry/opentelemetry-python/pull/2207)) - remove `X-B3-ParentSpanId` for B3 propagator as per OpenTelemetry specification ([#2237](https://github.com/open-telemetry/opentelemetry-python/pull/2237)) +- Add metrics API + ([#1887](https://github.com/open-telemetry/opentelemetry-python/pull/1887)) ## [1.6.2-0.25b2](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.2-0.25b2) - 2021-10-19 diff --git a/docs/api/api.rst b/docs/api/api.rst index e531d1419e..a13c9e698b 100644 --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -9,4 +9,5 @@ OpenTelemetry Python API baggage context trace + metrics environment_variables diff --git a/docs/api/metrics.instrument.rst b/docs/api/metrics.instrument.rst new file mode 100644 index 0000000000..efceaf74c6 --- /dev/null +++ b/docs/api/metrics.instrument.rst @@ -0,0 +1,8 @@ +opentelemetry.metrics.instrument +================================ + +.. automodule:: opentelemetry.metrics.instrument + :members: + :private-members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/metrics.measurement.rst b/docs/api/metrics.measurement.rst new file mode 100644 index 0000000000..4674169c13 --- /dev/null +++ b/docs/api/metrics.measurement.rst @@ -0,0 +1,7 @@ +opentelemetry.metrics.measurement +================================= + +.. automodule:: opentelemetry.metrics.measurement + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/metrics.rst b/docs/api/metrics.rst new file mode 100644 index 0000000000..e445a5015e --- /dev/null +++ b/docs/api/metrics.rst @@ -0,0 +1,15 @@ +opentelemetry.metrics package +============================= + +Submodules +---------- + +.. toctree:: + + metrics.instrument + metrics.measurement + +Module contents +--------------- + +.. automodule:: opentelemetry.metrics diff --git a/opentelemetry-api/src/opentelemetry/environment_variables.py b/opentelemetry-api/src/opentelemetry/environment_variables.py index 83ec67149d..1e2b8f90d3 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables.py @@ -36,3 +36,14 @@ """ .. envvar:: OTEL_PYTHON_TRACER_PROVIDER """ + +OTEL_PYTHON_METER_PROVIDER = "OTEL_PYTHON_METER_PROVIDER" +""" +.. envvar:: OTEL_PYTHON_METER_PROVIDER +""" + +OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER" +""" +.. envvar:: OTEL_METRICS_EXPORTER + +""" diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py new file mode 100644 index 0000000000..83d210e063 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -0,0 +1,384 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=too-many-ancestors +# type: ignore + +# FIXME enhance the documentation of this module +""" +This module provides abstract and concrete (but noop) classes that can be used +to generate metrics. +""" + + +from abc import ABC, abstractmethod +from logging import getLogger +from os import environ +from typing import Optional, cast + +from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER +from opentelemetry.metrics.instrument import ( + Counter, + DefaultCounter, + DefaultHistogram, + DefaultObservableCounter, + DefaultObservableGauge, + DefaultObservableUpDownCounter, + DefaultUpDownCounter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.util._providers import _load_provider + +_logger = getLogger(__name__) + + +class MeterProvider(ABC): + @abstractmethod + def get_meter( + self, + name, + version=None, + schema_url=None, + ) -> "Meter": + if name is None or name == "": + _logger.warning("Invalid name: %s", name) + + +class _DefaultMeterProvider(MeterProvider): + def get_meter( + self, + name, + version=None, + schema_url=None, + ) -> "Meter": + super().get_meter(name, version=version, schema_url=schema_url) + return _DefaultMeter(name, version=version, schema_url=schema_url) + + +class ProxyMeterProvider(MeterProvider): + def get_meter( + self, + name, + version=None, + schema_url=None, + ) -> "Meter": + if _METER_PROVIDER: + return _METER_PROVIDER.get_meter( + name, version=version, schema_url=schema_url + ) + return ProxyMeter(name, version=version, schema_url=schema_url) + + +class Meter(ABC): + def __init__(self, name, version=None, schema_url=None): + super().__init__() + self._name = name + self._version = version + self._schema_url = schema_url + self._instrument_names = set() + + @property + def name(self): + return self._name + + @property + def version(self): + return self._version + + @property + def schema_url(self): + return self._schema_url + + def _secure_instrument_name(self, name): + name = name.lower() + + if name in self._instrument_names: + _logger.error("Instrument name %s has been used already", name) + + else: + self._instrument_names.add(name) + + @abstractmethod + def create_counter(self, name, unit="", description="") -> Counter: + self._secure_instrument_name(name) + + @abstractmethod + def create_up_down_counter( + self, name, unit="", description="" + ) -> UpDownCounter: + self._secure_instrument_name(name) + + @abstractmethod + def create_observable_counter( + self, name, callback, unit="", description="" + ) -> ObservableCounter: + """Creates an observable counter instrument + + An observable counter observes a monotonically increasing count by + calling a provided callback which returns multiple + :class:`~opentelemetry.metrics.measurement.Measurement`. + + For example, an observable counter could be used to report system CPU + time periodically. Here is a basic implementation:: + + def cpu_time_callback() -> Iterable[Measurement]: + measurements = [] + with open("/proc/stat") as procstat: + procstat.readline() # skip the first line + for line in procstat: + if not line.startswith("cpu"): break + cpu, *states = line.split() + measurements.append(Measurement(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) + measurements.append(Measurement(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) + measurements.append(Measurement(int(states[2]) // 100, {"cpu": cpu, "state": "system"})) + # ... other states + return measurements + + meter.create_observable_counter( + "system.cpu.time", + callback=cpu_time_callback, + unit="s", + description="CPU time" + ) + + To reduce memory usage, you can use generator callbacks instead of + building the full list:: + + def cpu_time_callback() -> Iterable[Measurement]: + with open("/proc/stat") as procstat: + procstat.readline() # skip the first line + for line in procstat: + if not line.startswith("cpu"): break + cpu, *states = line.split() + yield Measurement(int(states[0]) // 100, {"cpu": cpu, "state": "user"}) + yield Measurement(int(states[1]) // 100, {"cpu": cpu, "state": "nice"}) + # ... other states + + Alternatively, you can pass a generator directly instead of a callback, + which should return iterables of + :class:`~opentelemetry.metrics.measurement.Measurement`:: + + def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Measurement]]: + while True: + measurements = [] + with open("/proc/stat") as procstat: + procstat.readline() # skip the first line + for line in procstat: + if not line.startswith("cpu"): break + cpu, *states = line.split() + if "user" in states_to_include: + measurements.append(Measurement(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) + if "nice" in states_to_include: + measurements.append(Measurement(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) + # ... other states + yield measurements + + meter.create_observable_counter( + "system.cpu.time", + callback=cpu_time_callback({"user", "system"}), + unit="s", + description="CPU time" + ) + + Args: + name: The name of the instrument to be created + callback: A callback that returns an iterable of + :class:`~opentelemetry.metrics.measurement.Measurement`. + Alternatively, can be a generator that yields iterables of + :class:`~opentelemetry.metrics.measurement.Measurement`. + unit: The unit for measurements this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + + self._secure_instrument_name(name) + + @abstractmethod + def create_histogram(self, name, unit="", description="") -> Histogram: + self._secure_instrument_name(name) + + @abstractmethod + def create_observable_gauge( + self, name, callback, unit="", description="" + ) -> ObservableGauge: + self._secure_instrument_name(name) + + @abstractmethod + def create_observable_up_down_counter( + self, name, callback, unit="", description="" + ) -> ObservableUpDownCounter: + self._secure_instrument_name(name) + + +class ProxyMeter(Meter): + def __init__( + self, + name, + version=None, + schema_url=None, + ): + super().__init__(name, version=version, schema_url=schema_url) + self._real_meter: Optional[Meter] = None + self._noop_meter = _DefaultMeter( + name, version=version, schema_url=schema_url + ) + + @property + def _meter(self) -> Meter: + if self._real_meter is not None: + return self._real_meter + + if _METER_PROVIDER: + self._real_meter = _METER_PROVIDER.get_meter( + self._name, + self._version, + ) + return self._real_meter + return self._noop_meter + + def create_counter(self, *args, **kwargs) -> Counter: + return self._meter.create_counter(*args, **kwargs) + + def create_up_down_counter(self, *args, **kwargs) -> UpDownCounter: + return self._meter.create_up_down_counter(*args, **kwargs) + + def create_observable_counter(self, *args, **kwargs) -> ObservableCounter: + return self._meter.create_observable_counter(*args, **kwargs) + + def create_histogram(self, *args, **kwargs) -> Histogram: + return self._meter.create_histogram(*args, **kwargs) + + def create_observable_gauge(self, *args, **kwargs) -> ObservableGauge: + return self._meter.create_observable_gauge(*args, **kwargs) + + def create_observable_up_down_counter( + self, *args, **kwargs + ) -> ObservableUpDownCounter: + return self._meter.create_observable_up_down_counter(*args, **kwargs) + + +class _DefaultMeter(Meter): + def create_counter(self, name, unit="", description="") -> Counter: + super().create_counter(name, unit=unit, description=description) + return DefaultCounter(name, unit=unit, description=description) + + def create_up_down_counter( + self, name, unit="", description="" + ) -> UpDownCounter: + super().create_up_down_counter( + name, unit=unit, description=description + ) + return DefaultUpDownCounter(name, unit=unit, description=description) + + def create_observable_counter( + self, name, callback, unit="", description="" + ) -> ObservableCounter: + super().create_observable_counter( + name, callback, unit=unit, description=description + ) + return DefaultObservableCounter( + name, + callback, + unit=unit, + description=description, + ) + + def create_histogram(self, name, unit="", description="") -> Histogram: + super().create_histogram(name, unit=unit, description=description) + return DefaultHistogram(name, unit=unit, description=description) + + def create_observable_gauge( + self, name, callback, unit="", description="" + ) -> ObservableGauge: + super().create_observable_gauge( + name, callback, unit=unit, description=description + ) + return DefaultObservableGauge( + name, + callback, + unit=unit, + description=description, + ) + + def create_observable_up_down_counter( + self, name, callback, unit="", description="" + ) -> ObservableUpDownCounter: + super().create_observable_up_down_counter( + name, callback, unit=unit, description=description + ) + return DefaultObservableUpDownCounter( + name, + callback, + unit=unit, + description=description, + ) + + +_METER_PROVIDER = None +_PROXY_METER_PROVIDER = None + + +def get_meter( + name: str, + version: str = "", + meter_provider: Optional[MeterProvider] = None, +) -> "Meter": + """Returns a `Meter` for use by the given instrumentation library. + + This function is a convenience wrapper for + opentelemetry.trace.MeterProvider.get_meter. + + If meter_provider is omitted the current configured one is used. + """ + if meter_provider is None: + meter_provider = get_meter_provider() + return meter_provider.get_meter(name, version) + + +def set_meter_provider(meter_provider: MeterProvider) -> None: + """Sets the current global :class:`~.MeterProvider` object. + + This can only be done once, a warning will be logged if any furter attempt + is made. + """ + global _METER_PROVIDER # pylint: disable=global-statement + + if _METER_PROVIDER is not None: + _logger.warning("Overriding of current MeterProvider is not allowed") + return + + _METER_PROVIDER = meter_provider + + +def get_meter_provider() -> MeterProvider: + """Gets the current global :class:`~.MeterProvider` object.""" + # pylint: disable=global-statement + global _METER_PROVIDER + global _PROXY_METER_PROVIDER + + if _METER_PROVIDER is None: + if OTEL_PYTHON_METER_PROVIDER not in environ.keys(): + if _PROXY_METER_PROVIDER is None: + _PROXY_METER_PROVIDER = ProxyMeterProvider() + return _PROXY_METER_PROVIDER + + _METER_PROVIDER = cast( + "MeterProvider", + _load_provider(OTEL_PYTHON_METER_PROVIDER, "meter_provider"), + ) + return _METER_PROVIDER diff --git a/opentelemetry-api/src/opentelemetry/metrics/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/instrument.py new file mode 100644 index 0000000000..5d38205640 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/metrics/instrument.py @@ -0,0 +1,234 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=too-many-ancestors + +# type: ignore + +from abc import ABC, abstractmethod +from collections import abc as collections_abc +from logging import getLogger +from re import compile as compile_ +from typing import Callable, Generator, Iterable, Union + +from opentelemetry.metrics.measurement import Measurement + +_TInstrumentCallback = Callable[[], Iterable[Measurement]] +_TInstrumentCallbackGenerator = Generator[Iterable[Measurement], None, None] +TCallback = Union[_TInstrumentCallback, _TInstrumentCallbackGenerator] + + +_logger = getLogger(__name__) + + +class Instrument(ABC): + + _name_regex = compile_(r"[a-zA-Z][-.\w]{0,62}") + + @property + def name(self): + return self._name + + @property + def unit(self): + return self._unit + + @property + def description(self): + return self._description + + @abstractmethod + def __init__(self, name, unit="", description=""): + + if name is None or self._name_regex.fullmatch(name) is None: + _logger.error("Invalid instrument name %s", name) + + else: + self._name = name + + if unit is None: + self._unit = "" + elif len(unit) > 63: + _logger.error("unit must be 63 characters or shorter") + + elif any(ord(character) > 127 for character in unit): + _logger.error("unit must only contain ASCII characters") + else: + self._unit = unit + + if description is None: + description = "" + + self._description = description + + +class Synchronous(Instrument): + pass + + +class Asynchronous(Instrument): + @abstractmethod + def __init__( + self, + name, + callback: TCallback, + *args, + unit="", + description="", + **kwargs + ): + super().__init__( + name, *args, unit=unit, description=description, **kwargs + ) + + if isinstance(callback, collections_abc.Callable): + self._callback = callback + elif isinstance(callback, collections_abc.Generator): + self._callback = self._wrap_generator_callback(callback) + else: + _logger.error("callback must be a callable or generator") + + def _wrap_generator_callback( + self, + generator_callback: _TInstrumentCallbackGenerator, + ) -> _TInstrumentCallback: + """Wraps a generator style callback into a callable one""" + has_items = True + + def inner() -> Iterable[Measurement]: + nonlocal has_items + if not has_items: + return [] + + try: + return next(generator_callback) + except StopIteration: + has_items = False + _logger.error( + "callback generator for instrument %s ran out of measurements", + self._name, + ) + return [] + + return inner + + def callback(self): + measurements = self._callback() + if not isinstance(measurements, collections_abc.Iterable): + _logger.error( + "Callback must return an iterable of Measurement, got %s", + type(measurements), + ) + return + for measurement in measurements: + if not isinstance(measurement, Measurement): + _logger.error( + "Callback must return an iterable of Measurement, " + "iterable contained type %s", + type(measurement), + ) + yield measurement + + +class _Adding(Instrument): + pass + + +class _Grouping(Instrument): + pass + + +class _Monotonic(_Adding): + pass + + +class _NonMonotonic(_Adding): + pass + + +class Counter(_Monotonic, Synchronous): + @abstractmethod + def add(self, amount, attributes=None): + if amount < 0: + _logger.error("Amount must be non-negative") + + +class DefaultCounter(Counter): + def __init__(self, name, unit="", description=""): + super().__init__(name, unit=unit, description=description) + + def add(self, amount, attributes=None): + return super().add(amount, attributes=attributes) + + +class UpDownCounter(_NonMonotonic, Synchronous): + @abstractmethod + def add(self, amount, attributes=None): + pass + + +class DefaultUpDownCounter(UpDownCounter): + def __init__(self, name, unit="", description=""): + super().__init__(name, unit=unit, description=description) + + def add(self, amount, attributes=None): + return super().add(amount, attributes=attributes) + + +class ObservableCounter(_Monotonic, Asynchronous): + def callback(self): + measurements = super().callback() + + for measurement in measurements: + if measurement.value < 0: + _logger.error("Amount must be non-negative") + yield measurement + + +class DefaultObservableCounter(ObservableCounter): + def __init__(self, name, callback, unit="", description=""): + super().__init__(name, callback, unit=unit, description=description) + + +class ObservableUpDownCounter(_NonMonotonic, Asynchronous): + + pass + + +class DefaultObservableUpDownCounter(ObservableUpDownCounter): + def __init__(self, name, callback, unit="", description=""): + super().__init__(name, callback, unit=unit, description=description) + + +class Histogram(_Grouping, Synchronous): + @abstractmethod + def record(self, amount, attributes=None): + pass + + +class DefaultHistogram(Histogram): + def __init__(self, name, unit="", description=""): + super().__init__(name, unit=unit, description=description) + + def record(self, amount, attributes=None): + return super().record(amount, attributes=attributes) + + +class ObservableGauge(_Grouping, Asynchronous): + pass + + +class DefaultObservableGauge(ObservableGauge): + def __init__(self, name, callback, unit="", description=""): + super().__init__(name, callback, unit=unit, description=description) diff --git a/opentelemetry-api/src/opentelemetry/metrics/measurement.py b/opentelemetry-api/src/opentelemetry/metrics/measurement.py new file mode 100644 index 0000000000..6b5b081c26 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/metrics/measurement.py @@ -0,0 +1,39 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=too-many-ancestors +# type:ignore + + +from abc import ABC, abstractmethod + + +class Measurement(ABC): + @property + def value(self): + return self._value + + @property + def attributes(self): + return self._attributes + + @abstractmethod + def __init__(self, value, attributes=None): + self._value = value + self._attributes = attributes + + +class DefaultMeasurement(Measurement): + def __init__(self, value, attributes=None): + super().__init__(value, attributes=attributes) diff --git a/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py b/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py new file mode 100644 index 0000000000..347f6c4dc4 --- /dev/null +++ b/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py @@ -0,0 +1,194 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +import io +from typing import Generator, Iterable +from unittest import TestCase + +from opentelemetry.metrics import _DefaultMeter +from opentelemetry.metrics.measurement import Measurement + +# FIXME Test that the instrument methods can be called concurrently safely. + + +class ChildMeasurement(Measurement): + def __init__(self, value, attributes=None): + super().__init__(value, attributes=attributes) + + def __eq__(self, o: Measurement) -> bool: + return self.value == o.value and self.attributes == o.attributes + + +class TestCpuTimeIntegration(TestCase): + """Integration test of scraping CPU time from proc stat with an observable + counter""" + + procstat_str = """\ +cpu 8549517 4919096 9165935 1430260740 1641349 0 1646147 623279 0 0 +cpu0 615029 317746 594601 89126459 129629 0 834346 42137 0 0 +cpu1 588232 349185 640492 89156411 124485 0 241004 41862 0 0 +intr 4370168813 38 9 0 0 1639 0 0 0 0 0 2865202 0 152 0 0 0 0 0 0 0 0 0 0 0 0 7236812 5966240 4501046 6467792 7289114 6048205 5299600 5178254 4642580 6826812 6880917 6230308 6307699 4699637 6119330 4905094 5644039 4700633 10539029 5365438 6086908 2227906 5094323 9685701 10137610 7739951 7143508 8123281 4968458 5683103 9890878 4466603 0 0 0 8929628 0 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +ctxt 6877594077 +btime 1631501040 +processes 2557351 +procs_running 2 +procs_blocked 0 +softirq 1644603067 0 166540056 208 309152755 8936439 0 1354908 935642970 13 222975718\n""" + + measurements_expected = [ + ChildMeasurement(6150, {"cpu": "cpu0", "state": "user"}), + ChildMeasurement(3177, {"cpu": "cpu0", "state": "nice"}), + ChildMeasurement(5946, {"cpu": "cpu0", "state": "system"}), + ChildMeasurement(891264, {"cpu": "cpu0", "state": "idle"}), + ChildMeasurement(1296, {"cpu": "cpu0", "state": "iowait"}), + ChildMeasurement(0, {"cpu": "cpu0", "state": "irq"}), + ChildMeasurement(8343, {"cpu": "cpu0", "state": "softirq"}), + ChildMeasurement(421, {"cpu": "cpu0", "state": "guest"}), + ChildMeasurement(0, {"cpu": "cpu0", "state": "guest_nice"}), + ChildMeasurement(5882, {"cpu": "cpu1", "state": "user"}), + ChildMeasurement(3491, {"cpu": "cpu1", "state": "nice"}), + ChildMeasurement(6404, {"cpu": "cpu1", "state": "system"}), + ChildMeasurement(891564, {"cpu": "cpu1", "state": "idle"}), + ChildMeasurement(1244, {"cpu": "cpu1", "state": "iowait"}), + ChildMeasurement(0, {"cpu": "cpu1", "state": "irq"}), + ChildMeasurement(2410, {"cpu": "cpu1", "state": "softirq"}), + ChildMeasurement(418, {"cpu": "cpu1", "state": "guest"}), + ChildMeasurement(0, {"cpu": "cpu1", "state": "guest_nice"}), + ] + + def test_cpu_time_callback(self): + meter = _DefaultMeter("foo") + + def cpu_time_callback() -> Iterable[Measurement]: + procstat = io.StringIO(self.procstat_str) + procstat.readline() # skip the first line + for line in procstat: + if not line.startswith("cpu"): + break + cpu, *states = line.split() + yield ChildMeasurement( + int(states[0]) // 100, {"cpu": cpu, "state": "user"} + ) + yield ChildMeasurement( + int(states[1]) // 100, {"cpu": cpu, "state": "nice"} + ) + yield ChildMeasurement( + int(states[2]) // 100, {"cpu": cpu, "state": "system"} + ) + yield ChildMeasurement( + int(states[3]) // 100, {"cpu": cpu, "state": "idle"} + ) + yield ChildMeasurement( + int(states[4]) // 100, {"cpu": cpu, "state": "iowait"} + ) + yield ChildMeasurement( + int(states[5]) // 100, {"cpu": cpu, "state": "irq"} + ) + yield ChildMeasurement( + int(states[6]) // 100, {"cpu": cpu, "state": "softirq"} + ) + yield ChildMeasurement( + int(states[7]) // 100, {"cpu": cpu, "state": "guest"} + ) + yield ChildMeasurement( + int(states[8]) // 100, {"cpu": cpu, "state": "guest_nice"} + ) + + observable_counter = meter.create_observable_counter( + "system.cpu.time", + callback=cpu_time_callback, + unit="s", + description="CPU time", + ) + measurements = list(observable_counter.callback()) + self.assertEqual(measurements, self.measurements_expected) + + def test_cpu_time_generator(self): + meter = _DefaultMeter("foo") + + def cpu_time_generator() -> Generator[ + Iterable[Measurement], None, None + ]: + while True: + measurements = [] + procstat = io.StringIO(self.procstat_str) + procstat.readline() # skip the first line + for line in procstat: + if not line.startswith("cpu"): + break + cpu, *states = line.split() + measurements.append( + ChildMeasurement( + int(states[0]) // 100, + {"cpu": cpu, "state": "user"}, + ) + ) + measurements.append( + ChildMeasurement( + int(states[1]) // 100, + {"cpu": cpu, "state": "nice"}, + ) + ) + measurements.append( + ChildMeasurement( + int(states[2]) // 100, + {"cpu": cpu, "state": "system"}, + ) + ) + measurements.append( + ChildMeasurement( + int(states[3]) // 100, + {"cpu": cpu, "state": "idle"}, + ) + ) + measurements.append( + ChildMeasurement( + int(states[4]) // 100, + {"cpu": cpu, "state": "iowait"}, + ) + ) + measurements.append( + ChildMeasurement( + int(states[5]) // 100, {"cpu": cpu, "state": "irq"} + ) + ) + measurements.append( + ChildMeasurement( + int(states[6]) // 100, + {"cpu": cpu, "state": "softirq"}, + ) + ) + measurements.append( + ChildMeasurement( + int(states[7]) // 100, + {"cpu": cpu, "state": "guest"}, + ) + ) + measurements.append( + ChildMeasurement( + int(states[8]) // 100, + {"cpu": cpu, "state": "guest_nice"}, + ) + ) + yield measurements + + observable_counter = meter.create_observable_counter( + "system.cpu.time", + callback=cpu_time_generator(), + unit="s", + description="CPU time", + ) + measurements = list(observable_counter.callback()) + self.assertEqual(measurements, self.measurements_expected) diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py new file mode 100644 index 0000000000..2dd100c9ed --- /dev/null +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -0,0 +1,804 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from inspect import Signature, isabstract, signature +from logging import ERROR +from unittest import TestCase + +from opentelemetry.metrics import Meter, _DefaultMeter +from opentelemetry.metrics.instrument import ( + Counter, + DefaultCounter, + DefaultHistogram, + DefaultObservableCounter, + DefaultObservableGauge, + DefaultObservableUpDownCounter, + DefaultUpDownCounter, + Histogram, + Instrument, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.metrics.measurement import Measurement + +# FIXME Test that the instrument methods can be called concurrently safely. + + +class ChildInstrument(Instrument): + def __init__(self, name, *args, unit="", description="", **kwargs): + super().__init__( + name, *args, unit=unit, description=description, **kwargs + ) + + +class ChildMeasurement(Measurement): + def __init__(self, value, attributes=None): + super().__init__(value, attributes=attributes) + + +class TestInstrument(TestCase): + def test_instrument_has_name(self): + """ + Test that the instrument has name. + """ + + init_signature = signature(Instrument.__init__) + self.assertIn("name", init_signature.parameters.keys()) + self.assertIs( + init_signature.parameters["name"].default, Signature.empty + ) + + self.assertTrue(hasattr(Instrument, "name")) + + def test_instrument_has_unit(self): + """ + Test that the instrument has unit. + """ + + init_signature = signature(Instrument.__init__) + self.assertIn("unit", init_signature.parameters.keys()) + self.assertIs(init_signature.parameters["unit"].default, "") + + self.assertTrue(hasattr(Instrument, "unit")) + + def test_instrument_has_description(self): + """ + Test that the instrument has description. + """ + + init_signature = signature(Instrument.__init__) + self.assertIn("description", init_signature.parameters.keys()) + self.assertIs(init_signature.parameters["description"].default, "") + + self.assertTrue(hasattr(Instrument, "description")) + + def test_instrument_name_syntax(self): + """ + Test that instrument names conform to the specified syntax. + """ + + with self.assertLogs(level=ERROR): + ChildInstrument("") + + with self.assertLogs(level=ERROR): + ChildInstrument(None) + + with self.assertLogs(level=ERROR): + ChildInstrument("1a") + + with self.assertLogs(level=ERROR): + ChildInstrument("_a") + + with self.assertLogs(level=ERROR): + ChildInstrument("!a ") + + with self.assertLogs(level=ERROR): + ChildInstrument("a ") + + with self.assertLogs(level=ERROR): + ChildInstrument("a%") + + with self.assertLogs(level=ERROR): + ChildInstrument("a" * 64) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + ChildInstrument("abc_def_ghi") + + def test_instrument_unit_syntax(self): + """ + Test that instrument unit conform to the specified syntax. + """ + + with self.assertLogs(level=ERROR): + ChildInstrument("name", unit="a" * 64) + + with self.assertLogs(level=ERROR): + ChildInstrument("name", unit="ñ") + + child_instrument = ChildInstrument("name", unit="a") + self.assertEqual(child_instrument.unit, "a") + + child_instrument = ChildInstrument("name", unit="A") + self.assertEqual(child_instrument.unit, "A") + + child_instrument = ChildInstrument("name") + self.assertEqual(child_instrument.unit, "") + + child_instrument = ChildInstrument("name", unit=None) + self.assertEqual(child_instrument.unit, "") + + def test_instrument_description_syntax(self): + """ + Test that instrument description conform to the specified syntax. + """ + + child_instrument = ChildInstrument("name", description="a") + self.assertEqual(child_instrument.description, "a") + + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + ChildInstrument("name", description="a" * 1024) + + child_instrument = ChildInstrument("name") + self.assertEqual(child_instrument.description, "") + + child_instrument = ChildInstrument("name", description=None) + self.assertEqual(child_instrument.description, "") + + +class TestCounter(TestCase): + def test_create_counter(self): + """ + Test that the Counter can be created with create_counter. + """ + + self.assertTrue( + isinstance(_DefaultMeter("name").create_counter("name"), Counter) + ) + + def test_api_counter_abstract(self): + """ + Test that the API Counter is an abstract class. + """ + + self.assertTrue(isabstract(Counter)) + + def test_create_counter_api(self): + """ + Test that the API for creating a counter accepts the name of the instrument. + Test that the API for creating a counter accepts the unit of the instrument. + Test that the API for creating a counter accepts the description of the + """ + + create_counter_signature = signature(Meter.create_counter) + self.assertIn("name", create_counter_signature.parameters.keys()) + self.assertIs( + create_counter_signature.parameters["name"].default, + Signature.empty, + ) + + create_counter_signature = signature(Meter.create_counter) + self.assertIn("unit", create_counter_signature.parameters.keys()) + self.assertIs(create_counter_signature.parameters["unit"].default, "") + + create_counter_signature = signature(Meter.create_counter) + self.assertIn( + "description", create_counter_signature.parameters.keys() + ) + self.assertIs( + create_counter_signature.parameters["description"].default, "" + ) + + def test_counter_add_method(self): + """ + Test that the counter has an add method. + Test that the add method returns None. + Test that the add method accepts optional attributes. + Test that the add method accepts the increment amount. + Test that the add method accepts only positive amounts. + """ + + self.assertTrue(hasattr(Counter, "add")) + + self.assertIsNone(DefaultCounter("name").add(1)) + + add_signature = signature(Counter.add) + self.assertIn("attributes", add_signature.parameters.keys()) + self.assertIs(add_signature.parameters["attributes"].default, None) + + self.assertIn("amount", add_signature.parameters.keys()) + self.assertIs( + add_signature.parameters["amount"].default, Signature.empty + ) + + with self.assertLogs(level=ERROR): + DefaultCounter("name").add(-1) + + +class TestObservableCounter(TestCase): + def test_create_observable_counter(self): + """ + Test that the ObservableCounter can be created with create_observable_counter. + """ + + def callback(): + yield + + self.assertTrue( + isinstance( + _DefaultMeter("name").create_observable_counter( + "name", callback() + ), + ObservableCounter, + ) + ) + + def test_api_observable_counter_abstract(self): + """ + Test that the API ObservableCounter is an abstract class. + """ + + self.assertTrue(isabstract(ObservableCounter)) + + def test_create_observable_counter_api(self): + """ + Test that the API for creating a observable_counter accepts the name of the instrument. + Test that the API for creating a observable_counter accepts a callback. + Test that the API for creating a observable_counter accepts the unit of the instrument. + Test that the API for creating a observable_counter accepts the description of the instrument + """ + + create_observable_counter_signature = signature( + Meter.create_observable_counter + ) + self.assertIn( + "name", create_observable_counter_signature.parameters.keys() + ) + self.assertIs( + create_observable_counter_signature.parameters["name"].default, + Signature.empty, + ) + create_observable_counter_signature = signature( + Meter.create_observable_counter + ) + self.assertIn( + "callback", create_observable_counter_signature.parameters.keys() + ) + self.assertIs( + create_observable_counter_signature.parameters["callback"].default, + Signature.empty, + ) + create_observable_counter_signature = signature( + Meter.create_observable_counter + ) + self.assertIn( + "unit", create_observable_counter_signature.parameters.keys() + ) + self.assertIs( + create_observable_counter_signature.parameters["unit"].default, "" + ) + + create_observable_counter_signature = signature( + Meter.create_observable_counter + ) + self.assertIn( + "description", + create_observable_counter_signature.parameters.keys(), + ) + self.assertIs( + create_observable_counter_signature.parameters[ + "description" + ].default, + "", + ) + + def test_observable_counter_generator(self): + """ + Test that the API for creating a asynchronous counter accepts a generator. + Test that the generator function reports iterable of measurements. + Test that there is a way to pass state to the generator. + Test that the instrument accepts positive measurements. + Test that the instrument does not accept negative measurements. + """ + + create_observable_counter_signature = signature( + Meter.create_observable_counter + ) + self.assertIn( + "callback", create_observable_counter_signature.parameters.keys() + ) + self.assertIs( + create_observable_counter_signature.parameters["name"].default, + Signature.empty, + ) + + def callback(): + yield 1 + + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + observable_counter = DefaultObservableCounter( + "name", callback() + ) + + with self.assertLogs(level=ERROR): + # use list() to consume the whole generator returned by callback() + list(observable_counter.callback()) + + def callback(): + yield [ChildMeasurement(1), ChildMeasurement(2)] + yield [ChildMeasurement(-1)] + + observable_counter = DefaultObservableCounter("name", callback()) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + list(observable_counter.callback()) + + with self.assertLogs(level=ERROR): + list(observable_counter.callback()) + + # out of items in generator, should log once + with self.assertLogs(level=ERROR): + list(observable_counter.callback()) + + # but log only once + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + list(observable_counter.callback()) + + def test_observable_counter_callback(self): + """ + Equivalent to test_observable_counter_generator but uses the callback + form. + """ + + def callback_invalid_return(): + return 1 + + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + observable_counter = DefaultObservableCounter( + "name", callback_invalid_return + ) + + with self.assertLogs(level=ERROR): + # use list() to consume the whole generator returned by callback() + list(observable_counter.callback()) + + def callback_valid(): + return [ChildMeasurement(1), ChildMeasurement(2)] + + observable_counter = DefaultObservableCounter("name", callback_valid) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + list(observable_counter.callback()) + + def callback_one_invalid(): + return [ChildMeasurement(1), ChildMeasurement(-2)] + + observable_counter = DefaultObservableCounter( + "name", callback_one_invalid + ) + + with self.assertLogs(level=ERROR): + list(observable_counter.callback()) + + +class TestHistogram(TestCase): + def test_create_histogram(self): + """ + Test that the Histogram can be created with create_histogram. + """ + + self.assertTrue( + isinstance( + _DefaultMeter("name").create_histogram("name"), Histogram + ) + ) + + def test_api_histogram_abstract(self): + """ + Test that the API Histogram is an abstract class. + """ + + self.assertTrue(isabstract(Histogram)) + + def test_create_histogram_api(self): + """ + Test that the API for creating a histogram accepts the name of the instrument. + Test that the API for creating a histogram accepts the unit of the instrument. + Test that the API for creating a histogram accepts the description of the + """ + + create_histogram_signature = signature(Meter.create_histogram) + self.assertIn("name", create_histogram_signature.parameters.keys()) + self.assertIs( + create_histogram_signature.parameters["name"].default, + Signature.empty, + ) + + create_histogram_signature = signature(Meter.create_histogram) + self.assertIn("unit", create_histogram_signature.parameters.keys()) + self.assertIs( + create_histogram_signature.parameters["unit"].default, "" + ) + + create_histogram_signature = signature(Meter.create_histogram) + self.assertIn( + "description", create_histogram_signature.parameters.keys() + ) + self.assertIs( + create_histogram_signature.parameters["description"].default, "" + ) + + def test_histogram_record_method(self): + """ + Test that the histogram has an record method. + Test that the record method returns None. + Test that the record method accepts optional attributes. + Test that the record method accepts the increment amount. + Test that the record method returns None. + """ + + self.assertTrue(hasattr(Histogram, "record")) + + self.assertIsNone(DefaultHistogram("name").record(1)) + + record_signature = signature(Histogram.record) + self.assertIn("attributes", record_signature.parameters.keys()) + self.assertIs(record_signature.parameters["attributes"].default, None) + + self.assertIn("amount", record_signature.parameters.keys()) + self.assertIs( + record_signature.parameters["amount"].default, Signature.empty + ) + + self.assertIsNone(DefaultHistogram("name").record(1)) + + +class TestObservableGauge(TestCase): + def test_create_observable_gauge(self): + """ + Test that the ObservableGauge can be created with create_observable_gauge. + """ + + def callback(): + yield + + self.assertTrue( + isinstance( + _DefaultMeter("name").create_observable_gauge( + "name", callback() + ), + ObservableGauge, + ) + ) + + def test_api_observable_gauge_abstract(self): + """ + Test that the API ObservableGauge is an abstract class. + """ + + self.assertTrue(isabstract(ObservableGauge)) + + def test_create_observable_gauge_api(self): + """ + Test that the API for creating a observable_gauge accepts the name of the instrument. + Test that the API for creating a observable_gauge accepts a callback. + Test that the API for creating a observable_gauge accepts the unit of the instrument. + Test that the API for creating a observable_gauge accepts the description of the instrument + """ + + create_observable_gauge_signature = signature( + Meter.create_observable_gauge + ) + self.assertIn( + "name", create_observable_gauge_signature.parameters.keys() + ) + self.assertIs( + create_observable_gauge_signature.parameters["name"].default, + Signature.empty, + ) + create_observable_gauge_signature = signature( + Meter.create_observable_gauge + ) + self.assertIn( + "callback", create_observable_gauge_signature.parameters.keys() + ) + self.assertIs( + create_observable_gauge_signature.parameters["callback"].default, + Signature.empty, + ) + create_observable_gauge_signature = signature( + Meter.create_observable_gauge + ) + self.assertIn( + "unit", create_observable_gauge_signature.parameters.keys() + ) + self.assertIs( + create_observable_gauge_signature.parameters["unit"].default, "" + ) + + create_observable_gauge_signature = signature( + Meter.create_observable_gauge + ) + self.assertIn( + "description", create_observable_gauge_signature.parameters.keys() + ) + self.assertIs( + create_observable_gauge_signature.parameters[ + "description" + ].default, + "", + ) + + def test_observable_gauge_callback(self): + """ + Test that the API for creating a asynchronous gauge accepts a callback. + Test that the callback function reports measurements. + Test that there is a way to pass state to the callback. + """ + + create_observable_gauge_signature = signature( + Meter.create_observable_gauge + ) + self.assertIn( + "callback", create_observable_gauge_signature.parameters.keys() + ) + self.assertIs( + create_observable_gauge_signature.parameters["name"].default, + Signature.empty, + ) + + def callback(): + yield + + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + observable_gauge = DefaultObservableGauge("name", callback()) + + with self.assertLogs(level=ERROR): + list(observable_gauge.callback()) + + def callback(): + yield [ChildMeasurement(1), ChildMeasurement(-1)] + + observable_gauge = DefaultObservableGauge("name", callback()) + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + list(observable_gauge.callback()) + + +class TestUpDownCounter(TestCase): + def test_create_up_down_counter(self): + """ + Test that the UpDownCounter can be created with create_up_down_counter. + """ + + self.assertTrue( + isinstance( + _DefaultMeter("name").create_up_down_counter("name"), + UpDownCounter, + ) + ) + + def test_api_up_down_counter_abstract(self): + """ + Test that the API UpDownCounter is an abstract class. + """ + + self.assertTrue(isabstract(UpDownCounter)) + + def test_create_up_down_counter_api(self): + """ + Test that the API for creating a up_down_counter accepts the name of the instrument. + Test that the API for creating a up_down_counter accepts the unit of the instrument. + Test that the API for creating a up_down_counter accepts the description of the + """ + + create_up_down_counter_signature = signature( + Meter.create_up_down_counter + ) + self.assertIn( + "name", create_up_down_counter_signature.parameters.keys() + ) + self.assertIs( + create_up_down_counter_signature.parameters["name"].default, + Signature.empty, + ) + + create_up_down_counter_signature = signature( + Meter.create_up_down_counter + ) + self.assertIn( + "unit", create_up_down_counter_signature.parameters.keys() + ) + self.assertIs( + create_up_down_counter_signature.parameters["unit"].default, "" + ) + + create_up_down_counter_signature = signature( + Meter.create_up_down_counter + ) + self.assertIn( + "description", create_up_down_counter_signature.parameters.keys() + ) + self.assertIs( + create_up_down_counter_signature.parameters["description"].default, + "", + ) + + def test_up_down_counter_add_method(self): + """ + Test that the up_down_counter has an add method. + Test that the add method returns None. + Test that the add method accepts optional attributes. + Test that the add method accepts the increment or decrement amount. + Test that the add method accepts positive and negative amounts. + """ + + self.assertTrue(hasattr(UpDownCounter, "add")) + + self.assertIsNone(DefaultUpDownCounter("name").add(1)) + + add_signature = signature(UpDownCounter.add) + self.assertIn("attributes", add_signature.parameters.keys()) + self.assertIs(add_signature.parameters["attributes"].default, None) + + self.assertIn("amount", add_signature.parameters.keys()) + self.assertIs( + add_signature.parameters["amount"].default, Signature.empty + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + DefaultUpDownCounter("name").add(-1) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + DefaultUpDownCounter("name").add(1) + + +class TestObservableUpDownCounter(TestCase): + def test_create_observable_up_down_counter(self): + """ + Test that the ObservableUpDownCounter can be created with create_observable_up_down_counter. + """ + + def callback(): + yield + + self.assertTrue( + isinstance( + _DefaultMeter("name").create_observable_up_down_counter( + "name", callback() + ), + ObservableUpDownCounter, + ) + ) + + def test_api_observable_up_down_counter_abstract(self): + """ + Test that the API ObservableUpDownCounter is an abstract class. + """ + + self.assertTrue(isabstract(ObservableUpDownCounter)) + + def test_create_observable_up_down_counter_api(self): + """ + Test that the API for creating a observable_up_down_counter accepts the name of the instrument. + Test that the API for creating a observable_up_down_counter accepts a callback. + Test that the API for creating a observable_up_down_counter accepts the unit of the instrument. + Test that the API for creating a observable_up_down_counter accepts the description of the instrument + """ + + create_observable_up_down_counter_signature = signature( + Meter.create_observable_up_down_counter + ) + self.assertIn( + "name", + create_observable_up_down_counter_signature.parameters.keys(), + ) + self.assertIs( + create_observable_up_down_counter_signature.parameters[ + "name" + ].default, + Signature.empty, + ) + create_observable_up_down_counter_signature = signature( + Meter.create_observable_up_down_counter + ) + self.assertIn( + "callback", + create_observable_up_down_counter_signature.parameters.keys(), + ) + self.assertIs( + create_observable_up_down_counter_signature.parameters[ + "callback" + ].default, + Signature.empty, + ) + create_observable_up_down_counter_signature = signature( + Meter.create_observable_up_down_counter + ) + self.assertIn( + "unit", + create_observable_up_down_counter_signature.parameters.keys(), + ) + self.assertIs( + create_observable_up_down_counter_signature.parameters[ + "unit" + ].default, + "", + ) + + create_observable_up_down_counter_signature = signature( + Meter.create_observable_up_down_counter + ) + self.assertIn( + "description", + create_observable_up_down_counter_signature.parameters.keys(), + ) + self.assertIs( + create_observable_up_down_counter_signature.parameters[ + "description" + ].default, + "", + ) + + def test_observable_up_down_counter_callback(self): + """ + Test that the API for creating a asynchronous up_down_counter accepts a callback. + Test that the callback function reports measurements. + Test that there is a way to pass state to the callback. + Test that the instrument accepts positive and negative values. + """ + + create_observable_up_down_counter_signature = signature( + Meter.create_observable_up_down_counter + ) + self.assertIn( + "callback", + create_observable_up_down_counter_signature.parameters.keys(), + ) + self.assertIs( + create_observable_up_down_counter_signature.parameters[ + "name" + ].default, + Signature.empty, + ) + + def callback(): + yield ChildMeasurement(1) + yield ChildMeasurement(-1) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + observable_up_down_counter = DefaultObservableUpDownCounter( + "name", callback() + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + observable_up_down_counter.callback() + + with self.assertRaises(AssertionError): + with self.assertLogs(level=ERROR): + observable_up_down_counter.callback() diff --git a/opentelemetry-api/tests/metrics/test_meter.py b/opentelemetry-api/tests/metrics/test_meter.py new file mode 100644 index 0000000000..96543b69fe --- /dev/null +++ b/opentelemetry-api/tests/metrics/test_meter.py @@ -0,0 +1,179 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from logging import WARNING +from unittest import TestCase +from unittest.mock import Mock + +from opentelemetry.metrics import Meter + +# FIXME Test that the meter methods can be called concurrently safely. + + +class ChildMeter(Meter): + def create_counter(self, name, unit="", description=""): + super().create_counter(name, unit=unit, description=description) + + def create_up_down_counter(self, name, unit="", description=""): + super().create_up_down_counter( + name, unit=unit, description=description + ) + + def create_observable_counter( + self, name, callback, unit="", description="" + ): + super().create_observable_counter( + name, callback, unit=unit, description=description + ) + + def create_histogram(self, name, unit="", description=""): + super().create_histogram(name, unit=unit, description=description) + + def create_observable_gauge(self, name, callback, unit="", description=""): + super().create_observable_gauge( + name, callback, unit=unit, description=description + ) + + def create_observable_up_down_counter( + self, name, callback, unit="", description="" + ): + super().create_observable_up_down_counter( + name, callback, unit=unit, description=description + ) + + +class TestMeter(TestCase): + def test_create_counter(self): + """ + Test that the meter provides a function to create a new Counter + """ + + self.assertTrue(hasattr(Meter, "create_counter")) + self.assertTrue(Meter.create_counter.__isabstractmethod__) + + def test_create_up_down_counter(self): + """ + Test that the meter provides a function to create a new UpDownCounter + """ + + self.assertTrue(hasattr(Meter, "create_up_down_counter")) + self.assertTrue(Meter.create_up_down_counter.__isabstractmethod__) + + def test_create_observable_counter(self): + """ + Test that the meter provides a function to create a new ObservableCounter + """ + + self.assertTrue(hasattr(Meter, "create_observable_counter")) + self.assertTrue(Meter.create_observable_counter.__isabstractmethod__) + + def test_create_histogram(self): + """ + Test that the meter provides a function to create a new Histogram + """ + + self.assertTrue(hasattr(Meter, "create_histogram")) + self.assertTrue(Meter.create_histogram.__isabstractmethod__) + + def test_create_observable_gauge(self): + """ + Test that the meter provides a function to create a new ObservableGauge + """ + + self.assertTrue(hasattr(Meter, "create_observable_gauge")) + self.assertTrue(Meter.create_observable_gauge.__isabstractmethod__) + + def test_create_observable_up_down_counter(self): + """ + Test that the meter provides a function to create a new + ObservableUpDownCounter + """ + + self.assertTrue(hasattr(Meter, "create_observable_up_down_counter")) + self.assertTrue( + Meter.create_observable_up_down_counter.__isabstractmethod__ + ) + + def test_no_repeated_instrument_names(self): + """ + Test that the meter returns an error when multiple instruments are + registered under the same Meter using the same name. + """ + + meter = ChildMeter("name") + + meter.create_counter("name") + + with self.assertLogs(level=WARNING): + meter.create_counter("name") + + with self.assertLogs(level=WARNING): + meter.create_up_down_counter("name") + + with self.assertLogs(level=WARNING): + meter.create_observable_counter("name", Mock()) + + with self.assertLogs(level=WARNING): + meter.create_histogram("name") + + with self.assertLogs(level=WARNING): + meter.create_observable_gauge("name", Mock()) + + with self.assertLogs(level=WARNING): + meter.create_observable_up_down_counter("name", Mock()) + + def test_same_name_instrument_different_meter(self): + """ + Test that is possible to register two instruments with the same name + under different meters. + """ + + meter_0 = ChildMeter("meter_0") + meter_1 = ChildMeter("meter_1") + + meter_0.create_counter("counter") + meter_0.create_up_down_counter("up_down_counter") + meter_0.create_observable_counter("observable_counter", Mock()) + meter_0.create_histogram("histogram") + meter_0.create_observable_gauge("observable_gauge", Mock()) + meter_0.create_observable_up_down_counter( + "observable_up_down_counter", Mock() + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + meter_1.create_counter("counter") + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + meter_1.create_up_down_counter("up_down_counter") + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + meter_1.create_observable_counter("observable_counter", Mock()) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + meter_1.create_histogram("histogram") + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + meter_1.create_observable_gauge("observable_gauge", Mock()) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + meter_1.create_observable_up_down_counter( + "observable_up_down_counter", Mock() + ) diff --git a/opentelemetry-api/tests/metrics/test_meter_provider.py b/opentelemetry-api/tests/metrics/test_meter_provider.py new file mode 100644 index 0000000000..9784e505e0 --- /dev/null +++ b/opentelemetry-api/tests/metrics/test_meter_provider.py @@ -0,0 +1,260 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from logging import WARNING +from unittest import TestCase +from unittest.mock import Mock, patch + +from pytest import fixture + +from opentelemetry import metrics +from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER +from opentelemetry.metrics import ( + ProxyMeter, + ProxyMeterProvider, + _DefaultMeter, + _DefaultMeterProvider, + get_meter_provider, + set_meter_provider, +) +from opentelemetry.metrics.instrument import ( + DefaultCounter, + DefaultHistogram, + DefaultObservableCounter, + DefaultObservableGauge, + DefaultObservableUpDownCounter, + DefaultUpDownCounter, +) + +# FIXME Test that the instrument methods can be called concurrently safely. + + +@fixture +def reset_meter_provider(): + original_meter_provider_value = metrics._METER_PROVIDER + + yield + + metrics._METER_PROVIDER = original_meter_provider_value + + +def test_set_meter_provider(reset_meter_provider): + """ + Test that the API provides a way to set a global default MeterProvider + """ + + mock = Mock() + + assert metrics._METER_PROVIDER is None + + set_meter_provider(mock) + + assert metrics._METER_PROVIDER is mock + + +def test_get_meter_provider(reset_meter_provider): + """ + Test that the API provides a way to get a global default MeterProvider + """ + + assert metrics._METER_PROVIDER is None + + assert isinstance(get_meter_provider(), ProxyMeterProvider) + + metrics._METER_PROVIDER = None + + with patch.dict( + "os.environ", {OTEL_PYTHON_METER_PROVIDER: "test_meter_provider"} + ): + + with patch("opentelemetry.metrics._load_provider", Mock()): + with patch( + "opentelemetry.metrics.cast", + Mock(**{"return_value": "test_meter_provider"}), + ): + assert get_meter_provider() == "test_meter_provider" + + +class TestGetMeter(TestCase): + def test_get_meter_parameters(self): + """ + Test that get_meter accepts name, version and schema_url + """ + try: + _DefaultMeterProvider().get_meter( + "name", version="version", schema_url="schema_url" + ) + except Exception as error: + self.fail(f"Unexpected exception raised: {error}") + + def test_invalid_name(self): + """ + Test that when an invalid name is specified a working meter + implementation is returned as a fallback. + + Test that the fallback meter name property keeps its original invalid + value. + + Test that a message is logged reporting the specified value for the + fallback meter is invalid. + """ + with self.assertLogs(level=WARNING): + meter = _DefaultMeterProvider().get_meter("") + + self.assertTrue(isinstance(meter, _DefaultMeter)) + + self.assertEqual(meter.name, "") + + with self.assertLogs(level=WARNING): + meter = _DefaultMeterProvider().get_meter(None) + + self.assertTrue(isinstance(meter, _DefaultMeter)) + + self.assertEqual(meter.name, None) + + +class MockProvider(_DefaultMeterProvider): + def get_meter(self, name, version=None, schema_url=None): + return MockMeter(name, version=version, schema_url=schema_url) + + +class MockMeter(_DefaultMeter): + def create_counter(self, name, unit="", description=""): + return MockCounter("name") + + def create_up_down_counter(self, name, unit="", description=""): + return MockUpDownCounter("name") + + def create_observable_counter( + self, name, callback, unit="", description="" + ): + return MockObservableCounter("name", callback) + + def create_histogram(self, name, unit="", description=""): + return MockHistogram("name") + + def create_observable_gauge(self, name, callback, unit="", description=""): + return MockObservableGauge("name", callback) + + def create_observable_up_down_counter( + self, name, callback, unit="", description="" + ): + return MockObservableUpDownCounter("name", callback) + + +class MockCounter(DefaultCounter): + pass + + +class MockHistogram(DefaultHistogram): + pass + + +class MockObservableCounter(DefaultObservableCounter): + pass + + +class MockObservableGauge(DefaultObservableGauge): + pass + + +class MockObservableUpDownCounter(DefaultObservableUpDownCounter): + pass + + +class MockUpDownCounter(DefaultUpDownCounter): + pass + + +class TestProxy(TestCase): + def test_proxy_meter(self): + + """ + Test that the proxy meter provider and proxy meter automatically point + to updated objects. + """ + + original_provider = metrics._METER_PROVIDER + + provider = get_meter_provider() + self.assertIsInstance(provider, ProxyMeterProvider) + + meter = provider.get_meter("proxy-test") + self.assertIsInstance(meter, ProxyMeter) + + self.assertIsInstance(meter.create_counter("counter0"), DefaultCounter) + + self.assertIsInstance( + meter.create_histogram("histogram0"), DefaultHistogram + ) + + def callback(): + yield + + self.assertIsInstance( + meter.create_observable_counter("observable_counter0", callback()), + DefaultObservableCounter, + ) + + self.assertIsInstance( + meter.create_observable_gauge("observable_gauge0", callback()), + DefaultObservableGauge, + ) + + self.assertIsInstance( + meter.create_observable_up_down_counter( + "observable_up_down_counter0", callback() + ), + DefaultObservableUpDownCounter, + ) + + self.assertIsInstance( + meter.create_up_down_counter("up_down_counter0"), + DefaultUpDownCounter, + ) + + set_meter_provider(MockProvider()) + + self.assertIsInstance(get_meter_provider(), MockProvider) + self.assertIsInstance(provider.get_meter("proxy-test"), MockMeter) + + self.assertIsInstance(meter.create_counter("counter1"), MockCounter) + + self.assertIsInstance( + meter.create_histogram("histogram1"), MockHistogram + ) + + self.assertIsInstance( + meter.create_observable_counter("observable_counter1", callback()), + MockObservableCounter, + ) + + self.assertIsInstance( + meter.create_observable_gauge("observable_gauge1", callback()), + MockObservableGauge, + ) + + self.assertIsInstance( + meter.create_observable_up_down_counter( + "observable_up_down_counter1", callback() + ), + MockObservableUpDownCounter, + ) + + self.assertIsInstance( + meter.create_up_down_counter("up_down_counter1"), MockUpDownCounter + ) + + metrics._METER_PROVIDER = original_provider diff --git a/tests/util/src/opentelemetry/test/__init__.py b/tests/util/src/opentelemetry/test/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tox.ini b/tox.ini index 75772516d7..38da9cac54 100644 --- a/tox.ini +++ b/tox.ini @@ -99,7 +99,7 @@ changedir = exporter-zipkin-combined: exporter/opentelemetry-exporter-zipkin/tests exporter-zipkin-proto-http: exporter/opentelemetry-exporter-zipkin-proto-http/tests exporter-zipkin-json: exporter/opentelemetry-exporter-zipkin-json/tests - + propagator-b3: propagator/opentelemetry-propagator-b3/tests propagator-jaeger: propagator/opentelemetry-propagator-jaeger/tests From 03b3c9a8e6176059dbb526ccdc53edb4f3b86134 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Wed, 29 Sep 2021 12:13:16 -0400 Subject: [PATCH 1027/1517] Make measurement a concrete class (#2153) * Make Measurement a concrete class * comments * update changelog --- CHANGELOG.md | 2 + .../src/opentelemetry/metrics/measurement.py | 41 ++++++---- .../metrics/integration_test/test_cpu_time.py | 80 +++++++++---------- .../tests/metrics/test_instruments.py | 19 ++--- .../tests/metrics/test_measurement.py | 46 +++++++++++ 5 files changed, 118 insertions(+), 70 deletions(-) create mode 100644 opentelemetry-api/tests/metrics/test_measurement.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 37c0b3b9b6..4ff2a1ff3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2207](https://github.com/open-telemetry/opentelemetry-python/pull/2207)) - remove `X-B3-ParentSpanId` for B3 propagator as per OpenTelemetry specification ([#2237](https://github.com/open-telemetry/opentelemetry-python/pull/2237)) +- Make Measurement a concrete class + ([#2153](https://github.com/open-telemetry/opentelemetry-python/pull/2153)) - Add metrics API ([#1887](https://github.com/open-telemetry/opentelemetry-python/pull/1887)) diff --git a/opentelemetry-api/src/opentelemetry/metrics/measurement.py b/opentelemetry-api/src/opentelemetry/metrics/measurement.py index 6b5b081c26..e8fd9725bb 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/measurement.py +++ b/opentelemetry-api/src/opentelemetry/metrics/measurement.py @@ -12,28 +12,41 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=too-many-ancestors -# type:ignore +from typing import Union +from opentelemetry.util.types import Attributes -from abc import ABC, abstractmethod +class Measurement: + """A measurement observed in an asynchronous instrument + + Return/yield instances of this class from asynchronous instrument callbacks. + + Args: + value: The float or int measured value + attributes: The measurement's attributes + """ + + def __init__( + self, value: Union[int, float], attributes: Attributes = None + ) -> None: + self._value = value + self._attributes = attributes -class Measurement(ABC): @property - def value(self): + def value(self) -> Union[float, int]: return self._value @property - def attributes(self): + def attributes(self) -> Attributes: return self._attributes - @abstractmethod - def __init__(self, value, attributes=None): - self._value = value - self._attributes = attributes - + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, Measurement) + and self.value == other.value + and self.attributes == other.attributes + ) -class DefaultMeasurement(Measurement): - def __init__(self, value, attributes=None): - super().__init__(value, attributes=attributes) + def __repr__(self) -> str: + return f"Measurement(value={self.value}, attributes={self.attributes})" diff --git a/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py b/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py index 347f6c4dc4..561f0c0658 100644 --- a/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py +++ b/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py @@ -23,14 +23,6 @@ # FIXME Test that the instrument methods can be called concurrently safely. -class ChildMeasurement(Measurement): - def __init__(self, value, attributes=None): - super().__init__(value, attributes=attributes) - - def __eq__(self, o: Measurement) -> bool: - return self.value == o.value and self.attributes == o.attributes - - class TestCpuTimeIntegration(TestCase): """Integration test of scraping CPU time from proc stat with an observable counter""" @@ -48,24 +40,24 @@ class TestCpuTimeIntegration(TestCase): softirq 1644603067 0 166540056 208 309152755 8936439 0 1354908 935642970 13 222975718\n""" measurements_expected = [ - ChildMeasurement(6150, {"cpu": "cpu0", "state": "user"}), - ChildMeasurement(3177, {"cpu": "cpu0", "state": "nice"}), - ChildMeasurement(5946, {"cpu": "cpu0", "state": "system"}), - ChildMeasurement(891264, {"cpu": "cpu0", "state": "idle"}), - ChildMeasurement(1296, {"cpu": "cpu0", "state": "iowait"}), - ChildMeasurement(0, {"cpu": "cpu0", "state": "irq"}), - ChildMeasurement(8343, {"cpu": "cpu0", "state": "softirq"}), - ChildMeasurement(421, {"cpu": "cpu0", "state": "guest"}), - ChildMeasurement(0, {"cpu": "cpu0", "state": "guest_nice"}), - ChildMeasurement(5882, {"cpu": "cpu1", "state": "user"}), - ChildMeasurement(3491, {"cpu": "cpu1", "state": "nice"}), - ChildMeasurement(6404, {"cpu": "cpu1", "state": "system"}), - ChildMeasurement(891564, {"cpu": "cpu1", "state": "idle"}), - ChildMeasurement(1244, {"cpu": "cpu1", "state": "iowait"}), - ChildMeasurement(0, {"cpu": "cpu1", "state": "irq"}), - ChildMeasurement(2410, {"cpu": "cpu1", "state": "softirq"}), - ChildMeasurement(418, {"cpu": "cpu1", "state": "guest"}), - ChildMeasurement(0, {"cpu": "cpu1", "state": "guest_nice"}), + Measurement(6150, {"cpu": "cpu0", "state": "user"}), + Measurement(3177, {"cpu": "cpu0", "state": "nice"}), + Measurement(5946, {"cpu": "cpu0", "state": "system"}), + Measurement(891264, {"cpu": "cpu0", "state": "idle"}), + Measurement(1296, {"cpu": "cpu0", "state": "iowait"}), + Measurement(0, {"cpu": "cpu0", "state": "irq"}), + Measurement(8343, {"cpu": "cpu0", "state": "softirq"}), + Measurement(421, {"cpu": "cpu0", "state": "guest"}), + Measurement(0, {"cpu": "cpu0", "state": "guest_nice"}), + Measurement(5882, {"cpu": "cpu1", "state": "user"}), + Measurement(3491, {"cpu": "cpu1", "state": "nice"}), + Measurement(6404, {"cpu": "cpu1", "state": "system"}), + Measurement(891564, {"cpu": "cpu1", "state": "idle"}), + Measurement(1244, {"cpu": "cpu1", "state": "iowait"}), + Measurement(0, {"cpu": "cpu1", "state": "irq"}), + Measurement(2410, {"cpu": "cpu1", "state": "softirq"}), + Measurement(418, {"cpu": "cpu1", "state": "guest"}), + Measurement(0, {"cpu": "cpu1", "state": "guest_nice"}), ] def test_cpu_time_callback(self): @@ -78,31 +70,31 @@ def cpu_time_callback() -> Iterable[Measurement]: if not line.startswith("cpu"): break cpu, *states = line.split() - yield ChildMeasurement( + yield Measurement( int(states[0]) // 100, {"cpu": cpu, "state": "user"} ) - yield ChildMeasurement( + yield Measurement( int(states[1]) // 100, {"cpu": cpu, "state": "nice"} ) - yield ChildMeasurement( + yield Measurement( int(states[2]) // 100, {"cpu": cpu, "state": "system"} ) - yield ChildMeasurement( + yield Measurement( int(states[3]) // 100, {"cpu": cpu, "state": "idle"} ) - yield ChildMeasurement( + yield Measurement( int(states[4]) // 100, {"cpu": cpu, "state": "iowait"} ) - yield ChildMeasurement( + yield Measurement( int(states[5]) // 100, {"cpu": cpu, "state": "irq"} ) - yield ChildMeasurement( + yield Measurement( int(states[6]) // 100, {"cpu": cpu, "state": "softirq"} ) - yield ChildMeasurement( + yield Measurement( int(states[7]) // 100, {"cpu": cpu, "state": "guest"} ) - yield ChildMeasurement( + yield Measurement( int(states[8]) // 100, {"cpu": cpu, "state": "guest_nice"} ) @@ -130,54 +122,54 @@ def cpu_time_generator() -> Generator[ break cpu, *states = line.split() measurements.append( - ChildMeasurement( + Measurement( int(states[0]) // 100, {"cpu": cpu, "state": "user"}, ) ) measurements.append( - ChildMeasurement( + Measurement( int(states[1]) // 100, {"cpu": cpu, "state": "nice"}, ) ) measurements.append( - ChildMeasurement( + Measurement( int(states[2]) // 100, {"cpu": cpu, "state": "system"}, ) ) measurements.append( - ChildMeasurement( + Measurement( int(states[3]) // 100, {"cpu": cpu, "state": "idle"}, ) ) measurements.append( - ChildMeasurement( + Measurement( int(states[4]) // 100, {"cpu": cpu, "state": "iowait"}, ) ) measurements.append( - ChildMeasurement( + Measurement( int(states[5]) // 100, {"cpu": cpu, "state": "irq"} ) ) measurements.append( - ChildMeasurement( + Measurement( int(states[6]) // 100, {"cpu": cpu, "state": "softirq"}, ) ) measurements.append( - ChildMeasurement( + Measurement( int(states[7]) // 100, {"cpu": cpu, "state": "guest"}, ) ) measurements.append( - ChildMeasurement( + Measurement( int(states[8]) // 100, {"cpu": cpu, "state": "guest_nice"}, ) diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index 2dd100c9ed..9be227ec6f 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -45,11 +45,6 @@ def __init__(self, name, *args, unit="", description="", **kwargs): ) -class ChildMeasurement(Measurement): - def __init__(self, value, attributes=None): - super().__init__(value, attributes=attributes) - - class TestInstrument(TestCase): def test_instrument_has_name(self): """ @@ -341,8 +336,8 @@ def callback(): list(observable_counter.callback()) def callback(): - yield [ChildMeasurement(1), ChildMeasurement(2)] - yield [ChildMeasurement(-1)] + yield [Measurement(1), Measurement(2)] + yield [Measurement(-1)] observable_counter = DefaultObservableCounter("name", callback()) @@ -382,7 +377,7 @@ def callback_invalid_return(): list(observable_counter.callback()) def callback_valid(): - return [ChildMeasurement(1), ChildMeasurement(2)] + return [Measurement(1), Measurement(2)] observable_counter = DefaultObservableCounter("name", callback_valid) @@ -391,7 +386,7 @@ def callback_valid(): list(observable_counter.callback()) def callback_one_invalid(): - return [ChildMeasurement(1), ChildMeasurement(-2)] + return [Measurement(1), Measurement(-2)] observable_counter = DefaultObservableCounter( "name", callback_one_invalid @@ -578,7 +573,7 @@ def callback(): list(observable_gauge.callback()) def callback(): - yield [ChildMeasurement(1), ChildMeasurement(-1)] + yield [Measurement(1), Measurement(-1)] observable_gauge = DefaultObservableGauge("name", callback()) with self.assertRaises(AssertionError): @@ -786,8 +781,8 @@ def test_observable_up_down_counter_callback(self): ) def callback(): - yield ChildMeasurement(1) - yield ChildMeasurement(-1) + yield Measurement(1) + yield Measurement(-1) with self.assertRaises(AssertionError): with self.assertLogs(level=ERROR): diff --git a/opentelemetry-api/tests/metrics/test_measurement.py b/opentelemetry-api/tests/metrics/test_measurement.py new file mode 100644 index 0000000000..7fcd33d04f --- /dev/null +++ b/opentelemetry-api/tests/metrics/test_measurement.py @@ -0,0 +1,46 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +from opentelemetry.metrics.measurement import Measurement + + +class TestMeasurement(TestCase): + def test_measurement_init(self): + try: + # int + Measurement(321, {"hello": "world"}) + + # float + Measurement(321.321, {"hello": "world"}) + except Exception: # pylint: disable=broad-except + self.fail( + "Unexpected exception raised when instantiating Measurement" + ) + + def test_measurement_equality(self): + self.assertEqual( + Measurement(321, {"hello": "world"}), + Measurement(321, {"hello": "world"}), + ) + + self.assertNotEqual( + Measurement(321, {"hello": "world"}), + Measurement(321.321, {"hello": "world"}), + ) + self.assertNotEqual( + Measurement(321, {"baz": "world"}), + Measurement(321, {"hello": "world"}), + ) From 0aa3eb603564a441d3af314f30a4951db0379ca8 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 12 Oct 2021 16:20:07 +0200 Subject: [PATCH 1028/1517] Remove checks and callbacks from API (#2164) * Remove checks and callbacks from API Fixes #2151 * Fix tests --- .../src/opentelemetry/metrics/__init__.py | 24 +- .../src/opentelemetry/metrics/instrument.py | 80 +----- .../metrics/integration_test/test_cpu_time.py | 4 +- .../tests/metrics/test_instruments.py | 236 ------------------ opentelemetry-api/tests/metrics/test_meter.py | 74 ------ .../tests/metrics/test_meter_provider.py | 11 +- 6 files changed, 25 insertions(+), 404 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 83d210e063..2a60f0e202 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -55,8 +55,7 @@ def get_meter( version=None, schema_url=None, ) -> "Meter": - if name is None or name == "": - _logger.warning("Invalid name: %s", name) + pass class _DefaultMeterProvider(MeterProvider): @@ -104,24 +103,17 @@ def version(self): def schema_url(self): return self._schema_url - def _secure_instrument_name(self, name): - name = name.lower() - - if name in self._instrument_names: - _logger.error("Instrument name %s has been used already", name) - - else: - self._instrument_names.add(name) + # FIXME check that the instrument name has not been used already @abstractmethod def create_counter(self, name, unit="", description="") -> Counter: - self._secure_instrument_name(name) + pass @abstractmethod def create_up_down_counter( self, name, unit="", description="" ) -> UpDownCounter: - self._secure_instrument_name(name) + pass @abstractmethod def create_observable_counter( @@ -206,23 +198,21 @@ def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Measurem description: A description for this instrument and what it measures. """ - self._secure_instrument_name(name) - @abstractmethod def create_histogram(self, name, unit="", description="") -> Histogram: - self._secure_instrument_name(name) + pass @abstractmethod def create_observable_gauge( self, name, callback, unit="", description="" ) -> ObservableGauge: - self._secure_instrument_name(name) + pass @abstractmethod def create_observable_up_down_counter( self, name, callback, unit="", description="" ) -> ObservableUpDownCounter: - self._secure_instrument_name(name) + pass class ProxyMeter(Meter): diff --git a/opentelemetry-api/src/opentelemetry/metrics/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/instrument.py index 5d38205640..63e9dddf91 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/metrics/instrument.py @@ -19,7 +19,6 @@ from abc import ABC, abstractmethod from collections import abc as collections_abc from logging import getLogger -from re import compile as compile_ from typing import Callable, Generator, Iterable, Union from opentelemetry.metrics.measurement import Measurement @@ -33,44 +32,13 @@ class Instrument(ABC): - - _name_regex = compile_(r"[a-zA-Z][-.\w]{0,62}") - - @property - def name(self): - return self._name - - @property - def unit(self): - return self._unit - - @property - def description(self): - return self._description - @abstractmethod def __init__(self, name, unit="", description=""): + pass - if name is None or self._name_regex.fullmatch(name) is None: - _logger.error("Invalid instrument name %s", name) - - else: - self._name = name - - if unit is None: - self._unit = "" - elif len(unit) > 63: - _logger.error("unit must be 63 characters or shorter") - - elif any(ord(character) > 127 for character in unit): - _logger.error("unit must only contain ASCII characters") - else: - self._unit = unit - - if description is None: - description = "" - - self._description = description + # FIXME check that the instrument name is valid + # FIXME check that the unit is 63 characters or shorter + # FIXME check that the unit contains only ASCII characters class Synchronous(Instrument): @@ -96,11 +64,10 @@ def __init__( self._callback = callback elif isinstance(callback, collections_abc.Generator): self._callback = self._wrap_generator_callback(callback) - else: - _logger.error("callback must be a callable or generator") + # FIXME check that callback is a callable or generator + @staticmethod def _wrap_generator_callback( - self, generator_callback: _TInstrumentCallbackGenerator, ) -> _TInstrumentCallback: """Wraps a generator style callback into a callable one""" @@ -115,30 +82,13 @@ def inner() -> Iterable[Measurement]: return next(generator_callback) except StopIteration: has_items = False - _logger.error( - "callback generator for instrument %s ran out of measurements", - self._name, - ) + # FIXME handle the situation where the callback generator has + # run out of measurements return [] return inner - def callback(self): - measurements = self._callback() - if not isinstance(measurements, collections_abc.Iterable): - _logger.error( - "Callback must return an iterable of Measurement, got %s", - type(measurements), - ) - return - for measurement in measurements: - if not isinstance(measurement, Measurement): - _logger.error( - "Callback must return an iterable of Measurement, " - "iterable contained type %s", - type(measurement), - ) - yield measurement + # FIXME check that callbacks return an iterable of Measurements class _Adding(Instrument): @@ -160,8 +110,8 @@ class _NonMonotonic(_Adding): class Counter(_Monotonic, Synchronous): @abstractmethod def add(self, amount, attributes=None): - if amount < 0: - _logger.error("Amount must be non-negative") + # FIXME check that the amount is non negative + pass class DefaultCounter(Counter): @@ -187,13 +137,7 @@ def add(self, amount, attributes=None): class ObservableCounter(_Monotonic, Asynchronous): - def callback(self): - measurements = super().callback() - - for measurement in measurements: - if measurement.value < 0: - _logger.error("Amount must be non-negative") - yield measurement + pass class DefaultObservableCounter(ObservableCounter): diff --git a/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py b/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py index 561f0c0658..a45b6dfc50 100644 --- a/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py +++ b/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py @@ -104,7 +104,7 @@ def cpu_time_callback() -> Iterable[Measurement]: unit="s", description="CPU time", ) - measurements = list(observable_counter.callback()) + measurements = list(observable_counter._callback()) self.assertEqual(measurements, self.measurements_expected) def test_cpu_time_generator(self): @@ -182,5 +182,5 @@ def cpu_time_generator() -> Generator[ unit="s", description="CPU time", ) - measurements = list(observable_counter.callback()) + measurements = list(observable_counter._callback()) self.assertEqual(measurements, self.measurements_expected) diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index 9be227ec6f..05adaa4398 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -14,7 +14,6 @@ # type: ignore from inspect import Signature, isabstract, signature -from logging import ERROR from unittest import TestCase from opentelemetry.metrics import Meter, _DefaultMeter @@ -22,9 +21,6 @@ Counter, DefaultCounter, DefaultHistogram, - DefaultObservableCounter, - DefaultObservableGauge, - DefaultObservableUpDownCounter, DefaultUpDownCounter, Histogram, Instrument, @@ -33,7 +29,6 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.metrics.measurement import Measurement # FIXME Test that the instrument methods can be called concurrently safely. @@ -45,117 +40,6 @@ def __init__(self, name, *args, unit="", description="", **kwargs): ) -class TestInstrument(TestCase): - def test_instrument_has_name(self): - """ - Test that the instrument has name. - """ - - init_signature = signature(Instrument.__init__) - self.assertIn("name", init_signature.parameters.keys()) - self.assertIs( - init_signature.parameters["name"].default, Signature.empty - ) - - self.assertTrue(hasattr(Instrument, "name")) - - def test_instrument_has_unit(self): - """ - Test that the instrument has unit. - """ - - init_signature = signature(Instrument.__init__) - self.assertIn("unit", init_signature.parameters.keys()) - self.assertIs(init_signature.parameters["unit"].default, "") - - self.assertTrue(hasattr(Instrument, "unit")) - - def test_instrument_has_description(self): - """ - Test that the instrument has description. - """ - - init_signature = signature(Instrument.__init__) - self.assertIn("description", init_signature.parameters.keys()) - self.assertIs(init_signature.parameters["description"].default, "") - - self.assertTrue(hasattr(Instrument, "description")) - - def test_instrument_name_syntax(self): - """ - Test that instrument names conform to the specified syntax. - """ - - with self.assertLogs(level=ERROR): - ChildInstrument("") - - with self.assertLogs(level=ERROR): - ChildInstrument(None) - - with self.assertLogs(level=ERROR): - ChildInstrument("1a") - - with self.assertLogs(level=ERROR): - ChildInstrument("_a") - - with self.assertLogs(level=ERROR): - ChildInstrument("!a ") - - with self.assertLogs(level=ERROR): - ChildInstrument("a ") - - with self.assertLogs(level=ERROR): - ChildInstrument("a%") - - with self.assertLogs(level=ERROR): - ChildInstrument("a" * 64) - - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - ChildInstrument("abc_def_ghi") - - def test_instrument_unit_syntax(self): - """ - Test that instrument unit conform to the specified syntax. - """ - - with self.assertLogs(level=ERROR): - ChildInstrument("name", unit="a" * 64) - - with self.assertLogs(level=ERROR): - ChildInstrument("name", unit="ñ") - - child_instrument = ChildInstrument("name", unit="a") - self.assertEqual(child_instrument.unit, "a") - - child_instrument = ChildInstrument("name", unit="A") - self.assertEqual(child_instrument.unit, "A") - - child_instrument = ChildInstrument("name") - self.assertEqual(child_instrument.unit, "") - - child_instrument = ChildInstrument("name", unit=None) - self.assertEqual(child_instrument.unit, "") - - def test_instrument_description_syntax(self): - """ - Test that instrument description conform to the specified syntax. - """ - - child_instrument = ChildInstrument("name", description="a") - self.assertEqual(child_instrument.description, "a") - - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - ChildInstrument("name", description="a" * 1024) - - child_instrument = ChildInstrument("name") - self.assertEqual(child_instrument.description, "") - - child_instrument = ChildInstrument("name", description=None) - self.assertEqual(child_instrument.description, "") - - class TestCounter(TestCase): def test_create_counter(self): """ @@ -221,9 +105,6 @@ def test_counter_add_method(self): add_signature.parameters["amount"].default, Signature.empty ) - with self.assertLogs(level=ERROR): - DefaultCounter("name").add(-1) - class TestObservableCounter(TestCase): def test_create_observable_counter(self): @@ -322,79 +203,6 @@ def test_observable_counter_generator(self): Signature.empty, ) - def callback(): - yield 1 - - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - observable_counter = DefaultObservableCounter( - "name", callback() - ) - - with self.assertLogs(level=ERROR): - # use list() to consume the whole generator returned by callback() - list(observable_counter.callback()) - - def callback(): - yield [Measurement(1), Measurement(2)] - yield [Measurement(-1)] - - observable_counter = DefaultObservableCounter("name", callback()) - - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - list(observable_counter.callback()) - - with self.assertLogs(level=ERROR): - list(observable_counter.callback()) - - # out of items in generator, should log once - with self.assertLogs(level=ERROR): - list(observable_counter.callback()) - - # but log only once - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - list(observable_counter.callback()) - - def test_observable_counter_callback(self): - """ - Equivalent to test_observable_counter_generator but uses the callback - form. - """ - - def callback_invalid_return(): - return 1 - - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - observable_counter = DefaultObservableCounter( - "name", callback_invalid_return - ) - - with self.assertLogs(level=ERROR): - # use list() to consume the whole generator returned by callback() - list(observable_counter.callback()) - - def callback_valid(): - return [Measurement(1), Measurement(2)] - - observable_counter = DefaultObservableCounter("name", callback_valid) - - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - list(observable_counter.callback()) - - def callback_one_invalid(): - return [Measurement(1), Measurement(-2)] - - observable_counter = DefaultObservableCounter( - "name", callback_one_invalid - ) - - with self.assertLogs(level=ERROR): - list(observable_counter.callback()) - class TestHistogram(TestCase): def test_create_histogram(self): @@ -562,24 +370,6 @@ def test_observable_gauge_callback(self): Signature.empty, ) - def callback(): - yield - - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - observable_gauge = DefaultObservableGauge("name", callback()) - - with self.assertLogs(level=ERROR): - list(observable_gauge.callback()) - - def callback(): - yield [Measurement(1), Measurement(-1)] - - observable_gauge = DefaultObservableGauge("name", callback()) - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - list(observable_gauge.callback()) - class TestUpDownCounter(TestCase): def test_create_up_down_counter(self): @@ -662,14 +452,6 @@ def test_up_down_counter_add_method(self): add_signature.parameters["amount"].default, Signature.empty ) - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - DefaultUpDownCounter("name").add(-1) - - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - DefaultUpDownCounter("name").add(1) - class TestObservableUpDownCounter(TestCase): def test_create_observable_up_down_counter(self): @@ -779,21 +561,3 @@ def test_observable_up_down_counter_callback(self): ].default, Signature.empty, ) - - def callback(): - yield Measurement(1) - yield Measurement(-1) - - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - observable_up_down_counter = DefaultObservableUpDownCounter( - "name", callback() - ) - - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - observable_up_down_counter.callback() - - with self.assertRaises(AssertionError): - with self.assertLogs(level=ERROR): - observable_up_down_counter.callback() diff --git a/opentelemetry-api/tests/metrics/test_meter.py b/opentelemetry-api/tests/metrics/test_meter.py index 96543b69fe..5c39526ca6 100644 --- a/opentelemetry-api/tests/metrics/test_meter.py +++ b/opentelemetry-api/tests/metrics/test_meter.py @@ -13,9 +13,7 @@ # limitations under the License. # type: ignore -from logging import WARNING from unittest import TestCase -from unittest.mock import Mock from opentelemetry.metrics import Meter @@ -105,75 +103,3 @@ def test_create_observable_up_down_counter(self): self.assertTrue( Meter.create_observable_up_down_counter.__isabstractmethod__ ) - - def test_no_repeated_instrument_names(self): - """ - Test that the meter returns an error when multiple instruments are - registered under the same Meter using the same name. - """ - - meter = ChildMeter("name") - - meter.create_counter("name") - - with self.assertLogs(level=WARNING): - meter.create_counter("name") - - with self.assertLogs(level=WARNING): - meter.create_up_down_counter("name") - - with self.assertLogs(level=WARNING): - meter.create_observable_counter("name", Mock()) - - with self.assertLogs(level=WARNING): - meter.create_histogram("name") - - with self.assertLogs(level=WARNING): - meter.create_observable_gauge("name", Mock()) - - with self.assertLogs(level=WARNING): - meter.create_observable_up_down_counter("name", Mock()) - - def test_same_name_instrument_different_meter(self): - """ - Test that is possible to register two instruments with the same name - under different meters. - """ - - meter_0 = ChildMeter("meter_0") - meter_1 = ChildMeter("meter_1") - - meter_0.create_counter("counter") - meter_0.create_up_down_counter("up_down_counter") - meter_0.create_observable_counter("observable_counter", Mock()) - meter_0.create_histogram("histogram") - meter_0.create_observable_gauge("observable_gauge", Mock()) - meter_0.create_observable_up_down_counter( - "observable_up_down_counter", Mock() - ) - - with self.assertRaises(AssertionError): - with self.assertLogs(level=WARNING): - meter_1.create_counter("counter") - - with self.assertRaises(AssertionError): - with self.assertLogs(level=WARNING): - meter_1.create_up_down_counter("up_down_counter") - - with self.assertRaises(AssertionError): - with self.assertLogs(level=WARNING): - meter_1.create_observable_counter("observable_counter", Mock()) - - with self.assertRaises(AssertionError): - with self.assertLogs(level=WARNING): - meter_1.create_histogram("histogram") - - with self.assertRaises(AssertionError): - with self.assertLogs(level=WARNING): - meter_1.create_observable_gauge("observable_gauge", Mock()) - - with self.assertRaises(AssertionError): - with self.assertLogs(level=WARNING): - meter_1.create_observable_up_down_counter( - "observable_up_down_counter", Mock() - ) diff --git a/opentelemetry-api/tests/metrics/test_meter_provider.py b/opentelemetry-api/tests/metrics/test_meter_provider.py index 9784e505e0..c78de94cc7 100644 --- a/opentelemetry-api/tests/metrics/test_meter_provider.py +++ b/opentelemetry-api/tests/metrics/test_meter_provider.py @@ -13,7 +13,6 @@ # limitations under the License. # type: ignore -from logging import WARNING from unittest import TestCase from unittest.mock import Mock, patch @@ -110,17 +109,15 @@ def test_invalid_name(self): Test that a message is logged reporting the specified value for the fallback meter is invalid. """ - with self.assertLogs(level=WARNING): - meter = _DefaultMeterProvider().get_meter("") + meter = _DefaultMeterProvider().get_meter("") - self.assertTrue(isinstance(meter, _DefaultMeter)) + self.assertTrue(isinstance(meter, _DefaultMeter)) self.assertEqual(meter.name, "") - with self.assertLogs(level=WARNING): - meter = _DefaultMeterProvider().get_meter(None) + meter = _DefaultMeterProvider().get_meter(None) - self.assertTrue(isinstance(meter, _DefaultMeter)) + self.assertTrue(isinstance(meter, _DefaultMeter)) self.assertEqual(meter.name, None) From e20036149f8b8fda5772f24d720e4c9916e45c94 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 14 Oct 2021 12:46:02 -0400 Subject: [PATCH 1029/1517] Return proxy instruments from ProxyMeter (#2169) --- CHANGELOG.md | 2 + docs/conf.py | 2 + .../src/opentelemetry/metrics/__init__.py | 192 ++++++++--- .../src/opentelemetry/metrics/instrument.py | 108 ++++++- .../tests/metrics/test_meter_provider.py | 306 +++++++++++------- .../src/opentelemetry/test/globals_test.py | 24 ++ 6 files changed, 454 insertions(+), 180 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ff2a1ff3e..ad92cf02b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2207](https://github.com/open-telemetry/opentelemetry-python/pull/2207)) - remove `X-B3-ParentSpanId` for B3 propagator as per OpenTelemetry specification ([#2237](https://github.com/open-telemetry/opentelemetry-python/pull/2237)) +- Return proxy instruments from ProxyMeter + [[#2169](https://github.com/open-telemetry/opentelemetry-python/pull/2169)] - Make Measurement a concrete class ([#2153](https://github.com/open-telemetry/opentelemetry-python/pull/2153)) - Add metrics API diff --git a/docs/conf.py b/docs/conf.py index 6559298b8e..f79b12db0a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -98,6 +98,8 @@ nitpick_ignore = [ ("py:class", "ValueT"), ("py:class", "MetricT"), + ("py:class", "InstrumentT"), + ("py:obj", "opentelemetry.metrics.instrument.InstrumentT"), # Even if wrapt is added to intersphinx_mapping, sphinx keeps failing # with "class reference target not found: ObjectProxy". ("py:class", "ObjectProxy"), diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 2a60f0e202..2cf15cb267 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -25,7 +25,8 @@ from abc import ABC, abstractmethod from logging import getLogger from os import environ -from typing import Optional, cast +from threading import Lock +from typing import List, Optional, cast from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER from opentelemetry.metrics.instrument import ( @@ -41,7 +42,15 @@ ObservableGauge, ObservableUpDownCounter, UpDownCounter, + _ProxyCounter, + _ProxyHistogram, + _ProxyInstrument, + _ProxyObservableCounter, + _ProxyObservableGauge, + _ProxyObservableUpDownCounter, + _ProxyUpDownCounter, ) +from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider _logger = getLogger(__name__) @@ -69,18 +78,33 @@ def get_meter( return _DefaultMeter(name, version=version, schema_url=schema_url) -class ProxyMeterProvider(MeterProvider): +class _ProxyMeterProvider(MeterProvider): + def __init__(self) -> None: + self._lock = Lock() + self._meters: List[_ProxyMeter] = [] + self._real_meter_provider: Optional[MeterProvider] = None + def get_meter( self, name, version=None, schema_url=None, ) -> "Meter": - if _METER_PROVIDER: - return _METER_PROVIDER.get_meter( - name, version=version, schema_url=schema_url - ) - return ProxyMeter(name, version=version, schema_url=schema_url) + with self._lock: + if self._real_meter_provider is not None: + return self._real_meter_provider.get_meter( + name, version, schema_url + ) + + meter = _ProxyMeter(name, version=version, schema_url=schema_url) + self._meters.append(meter) + return meter + + def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: + with self._lock: + self._real_meter_provider = meter_provider + for meter in self._meters: + meter.on_set_meter_provider(meter_provider) class Meter(ABC): @@ -215,7 +239,7 @@ def create_observable_up_down_counter( pass -class ProxyMeter(Meter): +class _ProxyMeter(Meter): def __init__( self, name, @@ -223,43 +247,101 @@ def __init__( schema_url=None, ): super().__init__(name, version=version, schema_url=schema_url) + self._lock = Lock() + self._instruments: List[_ProxyInstrument] = [] self._real_meter: Optional[Meter] = None - self._noop_meter = _DefaultMeter( - name, version=version, schema_url=schema_url + + def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: + """Called when a real meter provider is set on the creating _ProxyMeterProvider + + Creates a real backing meter for this instance and notifies all created + instruments so they can create real backing instruments. + """ + real_meter = meter_provider.get_meter( + self._name, self._version, self._schema_url ) - @property - def _meter(self) -> Meter: - if self._real_meter is not None: - return self._real_meter - - if _METER_PROVIDER: - self._real_meter = _METER_PROVIDER.get_meter( - self._name, - self._version, - ) - return self._real_meter - return self._noop_meter + with self._lock: + self._real_meter = real_meter + # notify all proxy instruments of the new meter so they can create + # real instruments to back themselves + for instrument in self._instruments: + instrument.on_meter_set(real_meter) - def create_counter(self, *args, **kwargs) -> Counter: - return self._meter.create_counter(*args, **kwargs) + def create_counter(self, name, unit="", description="") -> Counter: + with self._lock: + if self._real_meter: + return self._real_meter.create_counter(name, unit, description) + proxy = _ProxyCounter(name, unit, description) + self._instruments.append(proxy) + return proxy - def create_up_down_counter(self, *args, **kwargs) -> UpDownCounter: - return self._meter.create_up_down_counter(*args, **kwargs) + def create_up_down_counter( + self, name, unit="", description="" + ) -> UpDownCounter: + with self._lock: + if self._real_meter: + return self._real_meter.create_up_down_counter( + name, unit, description + ) + proxy = _ProxyUpDownCounter(name, unit, description) + self._instruments.append(proxy) + return proxy - def create_observable_counter(self, *args, **kwargs) -> ObservableCounter: - return self._meter.create_observable_counter(*args, **kwargs) + def create_observable_counter( + self, name, callback, unit="", description="" + ) -> ObservableCounter: + with self._lock: + if self._real_meter: + return self._real_meter.create_observable_counter( + name, callback, unit, description + ) + proxy = _ProxyObservableCounter( + name, callback, unit=unit, description=description + ) + self._instruments.append(proxy) + return proxy - def create_histogram(self, *args, **kwargs) -> Histogram: - return self._meter.create_histogram(*args, **kwargs) + def create_histogram(self, name, unit="", description="") -> Histogram: + with self._lock: + if self._real_meter: + return self._real_meter.create_histogram( + name, unit, description + ) + proxy = _ProxyHistogram(name, unit, description) + self._instruments.append(proxy) + return proxy - def create_observable_gauge(self, *args, **kwargs) -> ObservableGauge: - return self._meter.create_observable_gauge(*args, **kwargs) + def create_observable_gauge( + self, name, callback, unit="", description="" + ) -> ObservableGauge: + with self._lock: + if self._real_meter: + return self._real_meter.create_observable_gauge( + name, callback, unit, description + ) + proxy = _ProxyObservableGauge( + name, callback, unit=unit, description=description + ) + self._instruments.append(proxy) + return proxy def create_observable_up_down_counter( - self, *args, **kwargs + self, name, callback, unit="", description="" ) -> ObservableUpDownCounter: - return self._meter.create_observable_up_down_counter(*args, **kwargs) + with self._lock: + if self._real_meter: + return self._real_meter.create_observable_up_down_counter( + name, + callback, + unit, + description, + ) + proxy = _ProxyObservableUpDownCounter( + name, callback, unit=unit, description=description + ) + self._instruments.append(proxy) + return proxy class _DefaultMeter(Meter): @@ -319,8 +401,9 @@ def create_observable_up_down_counter( ) -_METER_PROVIDER = None -_PROXY_METER_PROVIDER = None +_METER_PROVIDER_SET_ONCE = Once() +_METER_PROVIDER: Optional[MeterProvider] = None +_PROXY_METER_PROVIDER = _ProxyMeterProvider() def get_meter( @@ -340,35 +423,40 @@ def get_meter( return meter_provider.get_meter(name, version) +def _set_meter_provider(meter_provider: MeterProvider, log: bool) -> None: + def set_mp() -> None: + global _METER_PROVIDER # pylint: disable=global-statement + _METER_PROVIDER = meter_provider + + # gives all proxies real instruments off the newly set meter provider + _PROXY_METER_PROVIDER.on_set_meter_provider(meter_provider) + + did_set = _METER_PROVIDER_SET_ONCE.do_once(set_mp) + + if log and not did_set: + _logger.warning("Overriding of current MeterProvider is not allowed") + + def set_meter_provider(meter_provider: MeterProvider) -> None: """Sets the current global :class:`~.MeterProvider` object. This can only be done once, a warning will be logged if any furter attempt is made. """ - global _METER_PROVIDER # pylint: disable=global-statement - - if _METER_PROVIDER is not None: - _logger.warning("Overriding of current MeterProvider is not allowed") - return - - _METER_PROVIDER = meter_provider + _set_meter_provider(meter_provider, log=True) def get_meter_provider() -> MeterProvider: """Gets the current global :class:`~.MeterProvider` object.""" - # pylint: disable=global-statement - global _METER_PROVIDER - global _PROXY_METER_PROVIDER if _METER_PROVIDER is None: if OTEL_PYTHON_METER_PROVIDER not in environ.keys(): - if _PROXY_METER_PROVIDER is None: - _PROXY_METER_PROVIDER = ProxyMeterProvider() return _PROXY_METER_PROVIDER - _METER_PROVIDER = cast( - "MeterProvider", - _load_provider(OTEL_PYTHON_METER_PROVIDER, "meter_provider"), + meter_provider: MeterProvider = _load_provider( + OTEL_PYTHON_METER_PROVIDER, "meter_provider" ) - return _METER_PROVIDER + _set_meter_provider(meter_provider, log=False) + + # _METER_PROVIDER will have been set by one thread + return cast("MeterProvider", _METER_PROVIDER) diff --git a/opentelemetry-api/src/opentelemetry/metrics/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/instrument.py index 63e9dddf91..ce93b4ac45 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/metrics/instrument.py @@ -19,13 +19,24 @@ from abc import ABC, abstractmethod from collections import abc as collections_abc from logging import getLogger -from typing import Callable, Generator, Iterable, Union - +from typing import ( + Callable, + Generator, + Generic, + Iterable, + Optional, + TypeVar, + Union, +) + +# pylint: disable=unused-import; needed for typing and sphinx +from opentelemetry import metrics from opentelemetry.metrics.measurement import Measurement _TInstrumentCallback = Callable[[], Iterable[Measurement]] _TInstrumentCallbackGenerator = Generator[Iterable[Measurement], None, None] TCallback = Union[_TInstrumentCallback, _TInstrumentCallbackGenerator] +InstrumentT = TypeVar("InstrumentT", bound="Instrument") _logger = getLogger(__name__) @@ -41,6 +52,32 @@ def __init__(self, name, unit="", description=""): # FIXME check that the unit contains only ASCII characters +class _ProxyInstrument(ABC, Generic[InstrumentT]): + def __init__(self, name, unit, description) -> None: + self._name = name + self._unit = unit + self._description = description + self._real_instrument: Optional[InstrumentT] = None + + def on_meter_set(self, meter: "metrics.Meter") -> None: + """Called when a real meter is set on the creating _ProxyMeter""" + + # We don't need any locking on proxy instruments because it's OK if some + # measurements get dropped while a real backing instrument is being + # created. + self._real_instrument = self._create_real_instrument(meter) + + @abstractmethod + def _create_real_instrument(self, meter: "metrics.Meter") -> InstrumentT: + """Create an instance of the real instrument. Implement this.""" + + +class _ProxyAsynchronousInstrument(_ProxyInstrument[InstrumentT]): + def __init__(self, name, callback, unit, description) -> None: + super().__init__(name, unit, description) + self._callback = callback + + class Synchronous(Instrument): pass @@ -122,6 +159,15 @@ def add(self, amount, attributes=None): return super().add(amount, attributes=attributes) +class _ProxyCounter(_ProxyInstrument[Counter], Counter): + def add(self, amount, attributes=None): + if self._real_instrument: + self._real_instrument.add(amount, attributes) + + def _create_real_instrument(self, meter: "metrics.Meter") -> Counter: + return meter.create_counter(self._name, self._unit, self._description) + + class UpDownCounter(_NonMonotonic, Synchronous): @abstractmethod def add(self, amount, attributes=None): @@ -136,6 +182,17 @@ def add(self, amount, attributes=None): return super().add(amount, attributes=attributes) +class _ProxyUpDownCounter(_ProxyInstrument[UpDownCounter], UpDownCounter): + def add(self, amount, attributes=None): + if self._real_instrument: + self._real_instrument.add(amount, attributes) + + def _create_real_instrument(self, meter: "metrics.Meter") -> UpDownCounter: + return meter.create_up_down_counter( + self._name, self._unit, self._description + ) + + class ObservableCounter(_Monotonic, Asynchronous): pass @@ -145,8 +202,18 @@ def __init__(self, name, callback, unit="", description=""): super().__init__(name, callback, unit=unit, description=description) -class ObservableUpDownCounter(_NonMonotonic, Asynchronous): +class _ProxyObservableCounter( + _ProxyAsynchronousInstrument[ObservableCounter], ObservableCounter +): + def _create_real_instrument( + self, meter: "metrics.Meter" + ) -> ObservableCounter: + return meter.create_observable_counter( + self._name, self._callback, self._unit, self._description + ) + +class ObservableUpDownCounter(_NonMonotonic, Asynchronous): pass @@ -155,6 +222,18 @@ def __init__(self, name, callback, unit="", description=""): super().__init__(name, callback, unit=unit, description=description) +class _ProxyObservableUpDownCounter( + _ProxyAsynchronousInstrument[ObservableUpDownCounter], + ObservableUpDownCounter, +): + def _create_real_instrument( + self, meter: "metrics.Meter" + ) -> ObservableUpDownCounter: + return meter.create_observable_up_down_counter( + self._name, self._callback, self._unit, self._description + ) + + class Histogram(_Grouping, Synchronous): @abstractmethod def record(self, amount, attributes=None): @@ -169,6 +248,17 @@ def record(self, amount, attributes=None): return super().record(amount, attributes=attributes) +class _ProxyHistogram(_ProxyInstrument[Histogram], Histogram): + def record(self, amount, attributes=None): + if self._real_instrument: + self._real_instrument.record(amount, attributes) + + def _create_real_instrument(self, meter: "metrics.Meter") -> Histogram: + return meter.create_histogram( + self._name, self._unit, self._description + ) + + class ObservableGauge(_Grouping, Asynchronous): pass @@ -176,3 +266,15 @@ class ObservableGauge(_Grouping, Asynchronous): class DefaultObservableGauge(ObservableGauge): def __init__(self, name, callback, unit="", description=""): super().__init__(name, callback, unit=unit, description=description) + + +class _ProxyObservableGauge( + _ProxyAsynchronousInstrument[ObservableGauge], + ObservableGauge, +): + def _create_real_instrument( + self, meter: "metrics.Meter" + ) -> ObservableGauge: + return meter.create_observable_gauge( + self._name, self._callback, self._unit, self._description + ) diff --git a/opentelemetry-api/tests/metrics/test_meter_provider.py b/opentelemetry-api/tests/metrics/test_meter_provider.py index c78de94cc7..15c4d7cea7 100644 --- a/opentelemetry-api/tests/metrics/test_meter_provider.py +++ b/opentelemetry-api/tests/metrics/test_meter_provider.py @@ -21,20 +21,24 @@ from opentelemetry import metrics from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER from opentelemetry.metrics import ( - ProxyMeter, - ProxyMeterProvider, _DefaultMeter, _DefaultMeterProvider, + _ProxyMeter, + _ProxyMeterProvider, get_meter_provider, set_meter_provider, ) from opentelemetry.metrics.instrument import ( - DefaultCounter, - DefaultHistogram, - DefaultObservableCounter, - DefaultObservableGauge, - DefaultObservableUpDownCounter, - DefaultUpDownCounter, + _ProxyCounter, + _ProxyHistogram, + _ProxyObservableCounter, + _ProxyObservableGauge, + _ProxyObservableUpDownCounter, + _ProxyUpDownCounter, +) +from opentelemetry.test.globals_test import ( + MetricsGlobalsTest, + reset_metrics_globals, ) # FIXME Test that the instrument methods can be called concurrently safely. @@ -42,11 +46,9 @@ @fixture def reset_meter_provider(): - original_meter_provider_value = metrics._METER_PROVIDER - + reset_metrics_globals() yield - - metrics._METER_PROVIDER = original_meter_provider_value + reset_metrics_globals() def test_set_meter_provider(reset_meter_provider): @@ -61,6 +63,16 @@ def test_set_meter_provider(reset_meter_provider): set_meter_provider(mock) assert metrics._METER_PROVIDER is mock + assert get_meter_provider() is mock + + +def test_set_meter_provider_calls_proxy_provider(reset_meter_provider): + with patch("opentelemetry.metrics._PROXY_METER_PROVIDER") as mock_proxy_mp: + mock_real_mp = Mock() + set_meter_provider(mock_real_mp) + mock_proxy_mp.on_set_meter_provider.assert_called_once_with( + mock_real_mp + ) def test_get_meter_provider(reset_meter_provider): @@ -70,7 +82,7 @@ def test_get_meter_provider(reset_meter_provider): assert metrics._METER_PROVIDER is None - assert isinstance(get_meter_provider(), ProxyMeterProvider) + assert isinstance(get_meter_provider(), _ProxyMeterProvider) metrics._METER_PROVIDER = None @@ -122,136 +134,180 @@ def test_invalid_name(self): self.assertEqual(meter.name, None) -class MockProvider(_DefaultMeterProvider): - def get_meter(self, name, version=None, schema_url=None): - return MockMeter(name, version=version, schema_url=schema_url) - - -class MockMeter(_DefaultMeter): - def create_counter(self, name, unit="", description=""): - return MockCounter("name") - - def create_up_down_counter(self, name, unit="", description=""): - return MockUpDownCounter("name") - - def create_observable_counter( - self, name, callback, unit="", description="" - ): - return MockObservableCounter("name", callback) - - def create_histogram(self, name, unit="", description=""): - return MockHistogram("name") - - def create_observable_gauge(self, name, callback, unit="", description=""): - return MockObservableGauge("name", callback) - - def create_observable_up_down_counter( - self, name, callback, unit="", description="" - ): - return MockObservableUpDownCounter("name", callback) - - -class MockCounter(DefaultCounter): - pass +class TestProxy(MetricsGlobalsTest, TestCase): + def test_global_proxy_meter_provider(self): + # Global get_meter_provider() should initially be a _ProxyMeterProvider + # singleton + proxy_meter_provider: _ProxyMeterProvider = get_meter_provider() + self.assertIsInstance(proxy_meter_provider, _ProxyMeterProvider) + self.assertIs(get_meter_provider(), proxy_meter_provider) -class MockHistogram(DefaultHistogram): - pass + def test_proxy_provider(self): + proxy_meter_provider = _ProxyMeterProvider() + # Should return a proxy meter when no real MeterProvider is set + name = "foo" + version = "1.2" + schema_url = "schema_url" + proxy_meter: _ProxyMeter = proxy_meter_provider.get_meter( + name, version=version, schema_url=schema_url + ) + self.assertIsInstance(proxy_meter, _ProxyMeter) + + # After setting a real meter provider on the proxy, it should notify + # it's _ProxyMeters which should create their own real Meters + mock_real_mp = Mock() + proxy_meter_provider.on_set_meter_provider(mock_real_mp) + mock_real_mp.get_meter.assert_called_once_with( + name, version, schema_url + ) -class MockObservableCounter(DefaultObservableCounter): - pass - - -class MockObservableGauge(DefaultObservableGauge): - pass - - -class MockObservableUpDownCounter(DefaultObservableUpDownCounter): - pass - - -class MockUpDownCounter(DefaultUpDownCounter): - pass - + # After setting a real meter provider on the proxy, it should now return + # new meters directly from the set real meter + another_name = "bar" + meter2 = proxy_meter_provider.get_meter(another_name) + self.assertIsInstance(meter2, Mock) + mock_real_mp.get_meter.assert_called_with(another_name, None, None) -class TestProxy(TestCase): + # pylint: disable=too-many-locals def test_proxy_meter(self): - - """ - Test that the proxy meter provider and proxy meter automatically point - to updated objects. - """ - - original_provider = metrics._METER_PROVIDER - - provider = get_meter_provider() - self.assertIsInstance(provider, ProxyMeterProvider) - - meter = provider.get_meter("proxy-test") - self.assertIsInstance(meter, ProxyMeter) - - self.assertIsInstance(meter.create_counter("counter0"), DefaultCounter) - - self.assertIsInstance( - meter.create_histogram("histogram0"), DefaultHistogram + meter_name = "foo" + proxy_meter: _ProxyMeter = _ProxyMeterProvider().get_meter(meter_name) + self.assertIsInstance(proxy_meter, _ProxyMeter) + + # Should be able to create proxy instruments + name = "foo" + unit = "s" + description = "Foobar" + callback = Mock() + proxy_counter = proxy_meter.create_counter( + name, unit=unit, description=description ) - - def callback(): - yield - - self.assertIsInstance( - meter.create_observable_counter("observable_counter0", callback()), - DefaultObservableCounter, + proxy_updowncounter = proxy_meter.create_up_down_counter( + name, unit=unit, description=description ) - - self.assertIsInstance( - meter.create_observable_gauge("observable_gauge0", callback()), - DefaultObservableGauge, + proxy_histogram = proxy_meter.create_histogram( + name, unit=unit, description=description ) - - self.assertIsInstance( - meter.create_observable_up_down_counter( - "observable_up_down_counter0", callback() - ), - DefaultObservableUpDownCounter, + proxy_observable_counter = proxy_meter.create_observable_counter( + name, callback=callback, unit=unit, description=description ) - - self.assertIsInstance( - meter.create_up_down_counter("up_down_counter0"), - DefaultUpDownCounter, + proxy_observable_updowncounter = ( + proxy_meter.create_observable_up_down_counter( + name, callback=callback, unit=unit, description=description + ) ) - - set_meter_provider(MockProvider()) - - self.assertIsInstance(get_meter_provider(), MockProvider) - self.assertIsInstance(provider.get_meter("proxy-test"), MockMeter) - - self.assertIsInstance(meter.create_counter("counter1"), MockCounter) - - self.assertIsInstance( - meter.create_histogram("histogram1"), MockHistogram + proxy_overvable_gauge = proxy_meter.create_observable_gauge( + name, callback=callback, unit=unit, description=description ) - + self.assertIsInstance(proxy_counter, _ProxyCounter) + self.assertIsInstance(proxy_updowncounter, _ProxyUpDownCounter) + self.assertIsInstance(proxy_histogram, _ProxyHistogram) self.assertIsInstance( - meter.create_observable_counter("observable_counter1", callback()), - MockObservableCounter, + proxy_observable_counter, _ProxyObservableCounter ) - self.assertIsInstance( - meter.create_observable_gauge("observable_gauge1", callback()), - MockObservableGauge, + proxy_observable_updowncounter, _ProxyObservableUpDownCounter + ) + self.assertIsInstance(proxy_overvable_gauge, _ProxyObservableGauge) + + # Synchronous proxy instruments should be usable + amount = 12 + attributes = {"foo": "bar"} + proxy_counter.add(amount, attributes=attributes) + proxy_updowncounter.add(amount, attributes=attributes) + proxy_histogram.record(amount, attributes=attributes) + + # Calling _ProxyMeterProvider.on_set_meter_provider() should cascade down + # to the _ProxyInstruments which should create their own real instruments + # from the real Meter to back their calls + real_meter_provider = Mock() + proxy_meter.on_set_meter_provider(real_meter_provider) + real_meter_provider.get_meter.assert_called_once_with( + meter_name, None, None ) - self.assertIsInstance( - meter.create_observable_up_down_counter( - "observable_up_down_counter1", callback() - ), - MockObservableUpDownCounter, + real_meter: Mock = real_meter_provider.get_meter() + real_meter.create_counter.assert_called_once_with( + name, unit, description + ) + real_meter.create_up_down_counter.assert_called_once_with( + name, unit, description + ) + real_meter.create_histogram.assert_called_once_with( + name, unit, description + ) + real_meter.create_observable_counter.assert_called_once_with( + name, callback, unit, description + ) + real_meter.create_observable_up_down_counter.assert_called_once_with( + name, callback, unit, description + ) + real_meter.create_observable_gauge.assert_called_once_with( + name, callback, unit, description ) - self.assertIsInstance( - meter.create_up_down_counter("up_down_counter1"), MockUpDownCounter + # The synchronous instrument measurement methods should call through to + # the real instruments + real_counter: Mock = real_meter.create_counter() + real_updowncounter: Mock = real_meter.create_up_down_counter() + real_histogram: Mock = real_meter.create_histogram() + real_counter.assert_not_called() + real_updowncounter.assert_not_called() + real_histogram.assert_not_called() + + proxy_counter.add(amount, attributes=attributes) + real_counter.add.assert_called_once_with(amount, attributes) + proxy_updowncounter.add(amount, attributes=attributes) + real_updowncounter.add.assert_called_once_with(amount, attributes) + proxy_histogram.record(amount, attributes=attributes) + real_histogram.record.assert_called_once_with(amount, attributes) + + def test_proxy_meter_with_real_meter(self) -> None: + # Creating new instruments on the _ProxyMeter with a real meter set + # should create real instruments instead of proxies + meter_name = "foo" + proxy_meter: _ProxyMeter = _ProxyMeterProvider().get_meter(meter_name) + self.assertIsInstance(proxy_meter, _ProxyMeter) + + real_meter_provider = Mock() + proxy_meter.on_set_meter_provider(real_meter_provider) + + name = "foo" + unit = "s" + description = "Foobar" + callback = Mock() + counter = proxy_meter.create_counter( + name, unit=unit, description=description + ) + updowncounter = proxy_meter.create_up_down_counter( + name, unit=unit, description=description + ) + histogram = proxy_meter.create_histogram( + name, unit=unit, description=description + ) + observable_counter = proxy_meter.create_observable_counter( + name, callback=callback, unit=unit, description=description + ) + observable_updowncounter = ( + proxy_meter.create_observable_up_down_counter( + name, callback=callback, unit=unit, description=description + ) + ) + observable_gauge = proxy_meter.create_observable_gauge( + name, callback=callback, unit=unit, description=description ) - metrics._METER_PROVIDER = original_provider + real_meter: Mock = real_meter_provider.get_meter() + self.assertIs(counter, real_meter.create_counter()) + self.assertIs(updowncounter, real_meter.create_up_down_counter()) + self.assertIs(histogram, real_meter.create_histogram()) + self.assertIs( + observable_counter, real_meter.create_observable_counter() + ) + self.assertIs( + observable_updowncounter, + real_meter.create_observable_up_down_counter(), + ) + self.assertIs(observable_gauge, real_meter.create_observable_gauge()) diff --git a/tests/util/src/opentelemetry/test/globals_test.py b/tests/util/src/opentelemetry/test/globals_test.py index bb2cad6a0a..a2626116b1 100644 --- a/tests/util/src/opentelemetry/test/globals_test.py +++ b/tests/util/src/opentelemetry/test/globals_test.py @@ -14,6 +14,7 @@ import unittest +from opentelemetry import metrics as metrics_api from opentelemetry import trace as trace_api from opentelemetry.util._once import Once @@ -26,6 +27,14 @@ def reset_trace_globals() -> None: trace_api._PROXY_TRACER_PROVIDER = trace_api.ProxyTracerProvider() +# pylint: disable=protected-access +def reset_metrics_globals() -> None: + """WARNING: only use this for tests.""" + metrics_api._METER_PROVIDER_SET_ONCE = Once() # type: ignore[attr-defined] + metrics_api._METER_PROVIDER = None # type: ignore[attr-defined] + metrics_api._PROXY_METER_PROVIDER = metrics_api._ProxyMeterProvider() # type: ignore[attr-defined] + + class TraceGlobalsTest(unittest.TestCase): """Resets trace API globals in setUp/tearDown @@ -39,3 +48,18 @@ def setUp(self) -> None: def tearDown(self) -> None: super().tearDown() reset_trace_globals() + + +class MetricsGlobalsTest(unittest.TestCase): + """Resets metrics API globals in setUp/tearDown + + Use as a base class or mixin for your test that modifies metrics API globals. + """ + + def setUp(self) -> None: + super().setUp() + reset_metrics_globals() + + def tearDown(self) -> None: + super().tearDown() + reset_metrics_globals() From 0017a3ad716f9d2728a74de369524b913c870664 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 28 Oct 2021 20:26:10 +0200 Subject: [PATCH 1030/1517] Add MeterProvider and Meter to the SDK (#2227) * Add MeterProvider and Meter to the SDK Fixes #2200 * Add FIXMEs * Fix docstring * Add FIXME * Fix meter return * Log an error if a force flush fails * Add FIXME * Fix lint * Remove SDK API module * Unregister * Fix API names * Return _DefaultMeter * Remove properties * Pass MeterProvider as a parameter to __init__ * Add FIXMEs * Add FIXMEs * Fix lint --- .../src/opentelemetry/sdk/metrics/__init__.py | 216 ++++++++++++++++++ .../tests/metrics/test_metrics.py | 205 +++++++++++++++++ 2 files changed, 421 insertions(+) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py create mode 100644 opentelemetry-sdk/tests/metrics/test_metrics.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py new file mode 100644 index 0000000000..384e52512a --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -0,0 +1,216 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=function-redefined,too-many-ancestors + +from abc import ABC, abstractmethod +from atexit import register, unregister +from logging import getLogger +from typing import Optional + +from opentelemetry.metrics import Meter as APIMeter +from opentelemetry.metrics import MeterProvider as APIMeterProvider +from opentelemetry.metrics import _DefaultMeter +from opentelemetry.metrics.instrument import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo + +_logger = getLogger(__name__) + + +class Meter(APIMeter): + def __init__( + self, + instrumentation_info: InstrumentationInfo, + meter_provider: APIMeterProvider, + ): + super().__init__(instrumentation_info) + self._instrumentation_info = instrumentation_info + self._meter_provider = meter_provider + + def create_counter(self, name, unit=None, description=None) -> Counter: + # FIXME implement this method + pass + + def create_up_down_counter( + self, name, unit=None, description=None + ) -> UpDownCounter: + # FIXME implement this method + pass + + def create_observable_counter( + self, name, callback, unit=None, description=None + ) -> ObservableCounter: + # FIXME implement this method + pass + + def create_histogram(self, name, unit=None, description=None) -> Histogram: + # FIXME implement this method + pass + + def create_observable_gauge( + self, name, callback, unit=None, description=None + ) -> ObservableGauge: + # FIXME implement this method + pass + + def create_observable_up_down_counter( + self, name, callback, unit=None, description=None + ) -> ObservableUpDownCounter: + # FIXME implement this method + pass + + +class MeterProvider(APIMeterProvider): + """See `opentelemetry.metrics.MeterProvider`.""" + + def __init__( + self, + resource: Resource = Resource.create({}), + shutdown_on_exit: bool = True, + ): + self._resource = resource + self._atexit_handler = None + + if shutdown_on_exit: + self._atexit_handler = register(self.shutdown) + + self._metric_readers = [] + self._metric_exporters = [] + self._views = [] + self._shutdown = False + + def get_meter( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> Meter: + + if self._shutdown: + _logger.warning( + "A shutdown `MeterProvider` can not provide a `Meter`" + ) + return _DefaultMeter(name, version=version, schema_url=schema_url) + + return Meter(InstrumentationInfo(name, version, schema_url), self) + + def shutdown(self): + # FIXME implement a timeout + + if self._shutdown: + _logger.warning("shutdown can only be called once") + return False + + result = True + + for metric_reader in self._metric_readers: + result = result and metric_reader.shutdown() + + for metric_exporter in self._metric_exporters: + result = result and metric_exporter.shutdown() + + self._shutdown = True + + if self._atexit_handler is not None: + unregister(self._atexit_handler) + self._atexit_handler = None + + return result + + def force_flush(self) -> bool: + + # FIXME implement a timeout + + metric_reader_result = True + metric_exporter_result = True + + for metric_reader in self._metric_readers: + metric_reader_result = ( + metric_reader_result and metric_reader.force_flush() + ) + + if not metric_reader_result: + _logger.warning("Unable to force flush all metric readers") + + for metric_exporter in self._metric_exporters: + metric_exporter_result = ( + metric_exporter_result and metric_exporter.force_flush() + ) + + if not metric_exporter_result: + _logger.warning("Unable to force flush all metric exporters") + + return metric_reader_result and metric_exporter_result + + def register_metric_reader(self, metric_reader: "MetricReader") -> None: + # FIXME protect this method against race conditions + self._metric_readers.append(metric_reader) + + def register_metric_exporter( + self, metric_exporter: "MetricExporter" + ) -> None: + # FIXME protect this method against race conditions + self._metric_exporters.append(metric_exporter) + + def register_view(self, view: "View") -> None: + # FIXME protect this method against race conditions + self._views.append(view) + + +class MetricReader(ABC): + def __init__(self): + self._shutdown = False + + @abstractmethod + def collect(self): + pass + + def shutdown(self): + # FIXME this will need a Once wrapper + self._shutdown = True + + +class MetricExporter(ABC): + def __init__(self): + self._shutdown = False + + @abstractmethod + def export(self): + pass + + def shutdown(self): + # FIXME this will need a Once wrapper + self._shutdown = True + + +class View: + pass + + +class ConsoleMetricExporter(MetricExporter): + def export(self): + pass + + +class SDKMetricReader(MetricReader): + def collect(self): + pass diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py new file mode 100644 index 0000000000..828d66821d --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -0,0 +1,205 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from logging import WARNING +from unittest import TestCase +from unittest.mock import Mock + +from opentelemetry.sdk.metrics import ( + ConsoleMetricExporter, + MeterProvider, + SDKMetricReader, + View, +) +from opentelemetry.sdk.resources import Resource + + +class TestMeterProvider(TestCase): + def test_meter_provider_resource(self): + """ + `MeterProvider` provides a way to allow a `Resource` to be specified. + """ + + meter_provider_0 = MeterProvider() + meter_provider_1 = MeterProvider() + + self.assertIs(meter_provider_0._resource, meter_provider_1._resource) + self.assertIsInstance(meter_provider_0._resource, Resource) + self.assertIsInstance(meter_provider_1._resource, Resource) + + resource = Resource({"key": "value"}) + self.assertIs(MeterProvider(resource)._resource, resource) + + def test_get_meter(self): + """ + `MeterProvider.get_meter` arguments are used to create an + `InstrumentationInfo` object on the created `Meter`. + """ + + meter = MeterProvider().get_meter( + "name", + version="version", + schema_url="schema_url", + ) + + self.assertEqual(meter._instrumentation_info.name, "name") + self.assertEqual(meter._instrumentation_info.version, "version") + self.assertEqual(meter._instrumentation_info.schema_url, "schema_url") + + def test_register_metric_reader(self): + """ " + `MeterProvider` provides a way to configure `SDKMetricReader`s. + """ + + meter_provider = MeterProvider() + + self.assertTrue(hasattr(meter_provider, "register_metric_reader")) + + metric_reader = SDKMetricReader() + + meter_provider.register_metric_reader(metric_reader) + + self.assertTrue(meter_provider._metric_readers, [metric_reader]) + + def test_register_metric_exporter(self): + """ " + `MeterProvider` provides a way to configure `ConsoleMetricExporter`s. + """ + + meter_provider = MeterProvider() + + self.assertTrue(hasattr(meter_provider, "register_metric_exporter")) + + metric_exporter = ConsoleMetricExporter() + + meter_provider.register_metric_exporter(metric_exporter) + + self.assertTrue(meter_provider._metric_exporters, [metric_exporter]) + + def test_register_view(self): + """ " + `MeterProvider` provides a way to configure `View`s. + """ + + meter_provider = MeterProvider() + + self.assertTrue(hasattr(meter_provider, "register_view")) + + view = View() + + meter_provider.register_view(view) + + self.assertTrue(meter_provider._views, [view]) + + def test_meter_configuration(self): + """ + Any updated configuration is applied to all returned `Meter`s. + """ + + meter_provider = MeterProvider() + + view_0 = View() + + meter_provider.register_view(view_0) + + meter_0 = meter_provider.get_meter("meter_0") + meter_1 = meter_provider.get_meter("meter_1") + + self.assertEqual(meter_0._meter_provider._views, [view_0]) + self.assertEqual(meter_1._meter_provider._views, [view_0]) + + view_1 = View() + + meter_provider.register_view(view_1) + + self.assertEqual(meter_0._meter_provider._views, [view_0, view_1]) + self.assertEqual(meter_1._meter_provider._views, [view_0, view_1]) + + def test_shutdown_subsequent_calls(self): + """ + No subsequent attempts to get a `Meter` are allowed after calling + `MeterProvider.shutdown` + """ + + meter_provider = MeterProvider() + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + meter_provider.shutdown() + + with self.assertLogs(level=WARNING): + meter_provider.shutdown() + + def test_shutdown_result(self): + """ + `MeterProvider.shutdown` provides a way to let the caller know if it + succeeded or failed. + + `MeterProvider.shutdown` is implemented by at least invoking + ``shutdown`` on all registered `SDKMetricReader`s and `ConsoleMetricExporter`s. + """ + + meter_provider = MeterProvider() + + meter_provider.register_metric_reader( + Mock(**{"shutdown.return_value": True}) + ) + meter_provider.register_metric_exporter( + Mock(**{"shutdown.return_value": True}) + ) + + self.assertTrue(meter_provider.shutdown()) + + meter_provider = MeterProvider() + + meter_provider.register_metric_reader( + Mock(**{"shutdown.return_value": True}) + ) + meter_provider.register_metric_exporter( + Mock(**{"shutdown.return_value": False}) + ) + + self.assertFalse(meter_provider.shutdown()) + + def test_force_flush_result(self): + """ + `MeterProvider.force_flush` provides a way to let the caller know if it + succeeded or failed. + + `MeterProvider.force_flush` is implemented by at least invoking + ``force_flush`` on all registered `SDKMetricReader`s and `ConsoleMetricExporter`s. + """ + + meter_provider = MeterProvider() + + meter_provider.register_metric_reader( + Mock(**{"force_flush.return_value": True}) + ) + meter_provider.register_metric_exporter( + Mock(**{"force_flush.return_value": True}) + ) + + self.assertTrue(meter_provider.force_flush()) + + meter_provider = MeterProvider() + + meter_provider.register_metric_reader( + Mock(**{"force_flush.return_value": True}) + ) + meter_provider.register_metric_exporter( + Mock(**{"force_flush.return_value": False}) + ) + + self.assertFalse(meter_provider.force_flush()) From 94b8849945c4362ef174d47e114b1286b3d85afe Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 29 Oct 2021 06:31:25 -0400 Subject: [PATCH 1031/1517] Make metrics api/sdk packages private `*._metrics` (#2249) * Make metrics api/sdk packages private `*._metrics` * add warning --- docs/api/metrics.instrument.rst | 6 +++--- docs/api/metrics.measurement.rst | 6 +++--- docs/api/metrics.rst | 13 ++++++++++--- docs/conf.py | 2 +- .../{metrics => _metrics}/__init__.py | 12 ++++++------ .../{metrics => _metrics}/instrument.py | 4 ++-- .../{metrics => _metrics}/measurement.py | 0 .../metrics/integration_test/test_cpu_time.py | 4 ++-- .../tests/metrics/test_instruments.py | 4 ++-- .../tests/metrics/test_measurement.py | 2 +- opentelemetry-api/tests/metrics/test_meter.py | 2 +- .../tests/metrics/test_meter_provider.py | 16 +++++++++------- .../sdk/{metrics => _metrics}/__init__.py | 10 +++++----- opentelemetry-sdk/tests/metrics/test_metrics.py | 2 +- .../util/src/opentelemetry/test/globals_test.py | 2 +- 15 files changed, 47 insertions(+), 38 deletions(-) rename opentelemetry-api/src/opentelemetry/{metrics => _metrics}/__init__.py (97%) rename opentelemetry-api/src/opentelemetry/{metrics => _metrics}/instrument.py (98%) rename opentelemetry-api/src/opentelemetry/{metrics => _metrics}/measurement.py (100%) rename opentelemetry-sdk/src/opentelemetry/sdk/{metrics => _metrics}/__init__.py (95%) diff --git a/docs/api/metrics.instrument.rst b/docs/api/metrics.instrument.rst index efceaf74c6..4a678ef07f 100644 --- a/docs/api/metrics.instrument.rst +++ b/docs/api/metrics.instrument.rst @@ -1,7 +1,7 @@ -opentelemetry.metrics.instrument -================================ +opentelemetry._metrics.instrument +================================= -.. automodule:: opentelemetry.metrics.instrument +.. automodule:: opentelemetry._metrics.instrument :members: :private-members: :undoc-members: diff --git a/docs/api/metrics.measurement.rst b/docs/api/metrics.measurement.rst index 4674169c13..d9b70aa193 100644 --- a/docs/api/metrics.measurement.rst +++ b/docs/api/metrics.measurement.rst @@ -1,7 +1,7 @@ -opentelemetry.metrics.measurement -================================= +opentelemetry._metrics.measurement +================================== -.. automodule:: opentelemetry.metrics.measurement +.. automodule:: opentelemetry._metrics.measurement :members: :undoc-members: :show-inheritance: diff --git a/docs/api/metrics.rst b/docs/api/metrics.rst index e445a5015e..d4e29b624b 100644 --- a/docs/api/metrics.rst +++ b/docs/api/metrics.rst @@ -1,5 +1,12 @@ -opentelemetry.metrics package -============================= +opentelemetry._metrics package +============================== + +.. warning:: + OpenTelemetry Python metrics are in an experimental state. The APIs within + :mod:`opentelemetry._metrics` are subject to change in minor/patch releases and make no + backward compatability guarantees at this time. + + Once metrics become stable, this package will be be renamed to ``opentelemetry.metrics``. Submodules ---------- @@ -12,4 +19,4 @@ Submodules Module contents --------------- -.. automodule:: opentelemetry.metrics +.. automodule:: opentelemetry._metrics diff --git a/docs/conf.py b/docs/conf.py index f79b12db0a..fef6b1d3a9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -99,7 +99,7 @@ ("py:class", "ValueT"), ("py:class", "MetricT"), ("py:class", "InstrumentT"), - ("py:obj", "opentelemetry.metrics.instrument.InstrumentT"), + ("py:obj", "opentelemetry._metrics.instrument.InstrumentT"), # Even if wrapt is added to intersphinx_mapping, sphinx keeps failing # with "class reference target not found: ObjectProxy". ("py:class", "ObjectProxy"), diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py similarity index 97% rename from opentelemetry-api/src/opentelemetry/metrics/__init__.py rename to opentelemetry-api/src/opentelemetry/_metrics/__init__.py index 2cf15cb267..289f749d51 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -28,8 +28,7 @@ from threading import Lock from typing import List, Optional, cast -from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER -from opentelemetry.metrics.instrument import ( +from opentelemetry._metrics.instrument import ( Counter, DefaultCounter, DefaultHistogram, @@ -50,6 +49,7 @@ _ProxyObservableUpDownCounter, _ProxyUpDownCounter, ) +from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider @@ -147,7 +147,7 @@ def create_observable_counter( An observable counter observes a monotonically increasing count by calling a provided callback which returns multiple - :class:`~opentelemetry.metrics.measurement.Measurement`. + :class:`~opentelemetry._metrics.measurement.Measurement`. For example, an observable counter could be used to report system CPU time periodically. Here is a basic implementation:: @@ -187,7 +187,7 @@ def cpu_time_callback() -> Iterable[Measurement]: Alternatively, you can pass a generator directly instead of a callback, which should return iterables of - :class:`~opentelemetry.metrics.measurement.Measurement`:: + :class:`~opentelemetry._metrics.measurement.Measurement`:: def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Measurement]]: while True: @@ -214,9 +214,9 @@ def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Measurem Args: name: The name of the instrument to be created callback: A callback that returns an iterable of - :class:`~opentelemetry.metrics.measurement.Measurement`. + :class:`~opentelemetry._metrics.measurement.Measurement`. Alternatively, can be a generator that yields iterables of - :class:`~opentelemetry.metrics.measurement.Measurement`. + :class:`~opentelemetry._metrics.measurement.Measurement`. unit: The unit for measurements this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. diff --git a/opentelemetry-api/src/opentelemetry/metrics/instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py similarity index 98% rename from opentelemetry-api/src/opentelemetry/metrics/instrument.py rename to opentelemetry-api/src/opentelemetry/_metrics/instrument.py index ce93b4ac45..76838209eb 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py @@ -30,8 +30,8 @@ ) # pylint: disable=unused-import; needed for typing and sphinx -from opentelemetry import metrics -from opentelemetry.metrics.measurement import Measurement +from opentelemetry import _metrics as metrics +from opentelemetry._metrics.measurement import Measurement _TInstrumentCallback = Callable[[], Iterable[Measurement]] _TInstrumentCallbackGenerator = Generator[Iterable[Measurement], None, None] diff --git a/opentelemetry-api/src/opentelemetry/metrics/measurement.py b/opentelemetry-api/src/opentelemetry/_metrics/measurement.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/metrics/measurement.py rename to opentelemetry-api/src/opentelemetry/_metrics/measurement.py diff --git a/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py b/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py index a45b6dfc50..96176a58b3 100644 --- a/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py +++ b/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py @@ -17,8 +17,8 @@ from typing import Generator, Iterable from unittest import TestCase -from opentelemetry.metrics import _DefaultMeter -from opentelemetry.metrics.measurement import Measurement +from opentelemetry._metrics import _DefaultMeter +from opentelemetry._metrics.measurement import Measurement # FIXME Test that the instrument methods can be called concurrently safely. diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index 05adaa4398..f3a79aec46 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -16,8 +16,8 @@ from inspect import Signature, isabstract, signature from unittest import TestCase -from opentelemetry.metrics import Meter, _DefaultMeter -from opentelemetry.metrics.instrument import ( +from opentelemetry._metrics import Meter, _DefaultMeter +from opentelemetry._metrics.instrument import ( Counter, DefaultCounter, DefaultHistogram, diff --git a/opentelemetry-api/tests/metrics/test_measurement.py b/opentelemetry-api/tests/metrics/test_measurement.py index 7fcd33d04f..8791bc1a2f 100644 --- a/opentelemetry-api/tests/metrics/test_measurement.py +++ b/opentelemetry-api/tests/metrics/test_measurement.py @@ -14,7 +14,7 @@ from unittest import TestCase -from opentelemetry.metrics.measurement import Measurement +from opentelemetry._metrics.measurement import Measurement class TestMeasurement(TestCase): diff --git a/opentelemetry-api/tests/metrics/test_meter.py b/opentelemetry-api/tests/metrics/test_meter.py index 5c39526ca6..6834df31f8 100644 --- a/opentelemetry-api/tests/metrics/test_meter.py +++ b/opentelemetry-api/tests/metrics/test_meter.py @@ -15,7 +15,7 @@ from unittest import TestCase -from opentelemetry.metrics import Meter +from opentelemetry._metrics import Meter # FIXME Test that the meter methods can be called concurrently safely. diff --git a/opentelemetry-api/tests/metrics/test_meter_provider.py b/opentelemetry-api/tests/metrics/test_meter_provider.py index 15c4d7cea7..6d44e236a9 100644 --- a/opentelemetry-api/tests/metrics/test_meter_provider.py +++ b/opentelemetry-api/tests/metrics/test_meter_provider.py @@ -18,9 +18,8 @@ from pytest import fixture -from opentelemetry import metrics -from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER -from opentelemetry.metrics import ( +from opentelemetry import _metrics as metrics +from opentelemetry._metrics import ( _DefaultMeter, _DefaultMeterProvider, _ProxyMeter, @@ -28,7 +27,7 @@ get_meter_provider, set_meter_provider, ) -from opentelemetry.metrics.instrument import ( +from opentelemetry._metrics.instrument import ( _ProxyCounter, _ProxyHistogram, _ProxyObservableCounter, @@ -36,6 +35,7 @@ _ProxyObservableUpDownCounter, _ProxyUpDownCounter, ) +from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER from opentelemetry.test.globals_test import ( MetricsGlobalsTest, reset_metrics_globals, @@ -67,7 +67,9 @@ def test_set_meter_provider(reset_meter_provider): def test_set_meter_provider_calls_proxy_provider(reset_meter_provider): - with patch("opentelemetry.metrics._PROXY_METER_PROVIDER") as mock_proxy_mp: + with patch( + "opentelemetry._metrics._PROXY_METER_PROVIDER" + ) as mock_proxy_mp: mock_real_mp = Mock() set_meter_provider(mock_real_mp) mock_proxy_mp.on_set_meter_provider.assert_called_once_with( @@ -90,9 +92,9 @@ def test_get_meter_provider(reset_meter_provider): "os.environ", {OTEL_PYTHON_METER_PROVIDER: "test_meter_provider"} ): - with patch("opentelemetry.metrics._load_provider", Mock()): + with patch("opentelemetry._metrics._load_provider", Mock()): with patch( - "opentelemetry.metrics.cast", + "opentelemetry._metrics.cast", Mock(**{"return_value": "test_meter_provider"}), ): assert get_meter_provider() == "test_meter_provider" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py similarity index 95% rename from opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py rename to opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 384e52512a..1f306f68b3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -19,10 +19,10 @@ from logging import getLogger from typing import Optional -from opentelemetry.metrics import Meter as APIMeter -from opentelemetry.metrics import MeterProvider as APIMeterProvider -from opentelemetry.metrics import _DefaultMeter -from opentelemetry.metrics.instrument import ( +from opentelemetry._metrics import Meter as APIMeter +from opentelemetry._metrics import MeterProvider as APIMeterProvider +from opentelemetry._metrics import _DefaultMeter +from opentelemetry._metrics.instrument import ( Counter, Histogram, ObservableCounter, @@ -80,7 +80,7 @@ def create_observable_up_down_counter( class MeterProvider(APIMeterProvider): - """See `opentelemetry.metrics.MeterProvider`.""" + """See `opentelemetry._metrics.MeterProvider`.""" def __init__( self, diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 828d66821d..b473f4f69f 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -17,7 +17,7 @@ from unittest import TestCase from unittest.mock import Mock -from opentelemetry.sdk.metrics import ( +from opentelemetry.sdk._metrics import ( ConsoleMetricExporter, MeterProvider, SDKMetricReader, diff --git a/tests/util/src/opentelemetry/test/globals_test.py b/tests/util/src/opentelemetry/test/globals_test.py index a2626116b1..f5e4ddaea3 100644 --- a/tests/util/src/opentelemetry/test/globals_test.py +++ b/tests/util/src/opentelemetry/test/globals_test.py @@ -14,7 +14,7 @@ import unittest -from opentelemetry import metrics as metrics_api +from opentelemetry import _metrics as metrics_api from opentelemetry import trace as trace_api from opentelemetry.util._once import Once From 5bc91c85985c4ce67317a94483c2fede8f3e0fcd Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 1 Nov 2021 14:26:36 -0400 Subject: [PATCH 1032/1517] Make metrics envvar constants private (#2255) --- opentelemetry-api/src/opentelemetry/_metrics/__init__.py | 4 +++- opentelemetry-api/src/opentelemetry/environment_variables.py | 4 ++-- opentelemetry-api/tests/metrics/test_meter_provider.py | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index 289f749d51..54281de953 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -49,7 +49,9 @@ _ProxyObservableUpDownCounter, _ProxyUpDownCounter, ) -from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER +from opentelemetry.environment_variables import ( + _OTEL_PYTHON_METER_PROVIDER as OTEL_PYTHON_METER_PROVIDER, +) from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider diff --git a/opentelemetry-api/src/opentelemetry/environment_variables.py b/opentelemetry-api/src/opentelemetry/environment_variables.py index 1e2b8f90d3..de11e42390 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables.py @@ -37,12 +37,12 @@ .. envvar:: OTEL_PYTHON_TRACER_PROVIDER """ -OTEL_PYTHON_METER_PROVIDER = "OTEL_PYTHON_METER_PROVIDER" +_OTEL_PYTHON_METER_PROVIDER = "OTEL_PYTHON_METER_PROVIDER" """ .. envvar:: OTEL_PYTHON_METER_PROVIDER """ -OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER" +_OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER" """ .. envvar:: OTEL_METRICS_EXPORTER diff --git a/opentelemetry-api/tests/metrics/test_meter_provider.py b/opentelemetry-api/tests/metrics/test_meter_provider.py index 6d44e236a9..e2a1996810 100644 --- a/opentelemetry-api/tests/metrics/test_meter_provider.py +++ b/opentelemetry-api/tests/metrics/test_meter_provider.py @@ -35,7 +35,9 @@ _ProxyObservableUpDownCounter, _ProxyUpDownCounter, ) -from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER +from opentelemetry.environment_variables import ( + _OTEL_PYTHON_METER_PROVIDER as OTEL_PYTHON_METER_PROVIDER, +) from opentelemetry.test.globals_test import ( MetricsGlobalsTest, reset_metrics_globals, From 7c90cf4052e86e1a77e4a645eb5a902b49e781bf Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 3 Nov 2021 13:21:15 -0700 Subject: [PATCH 1033/1517] Add logging signal to main (#2251) * Add initial overall structure and classes for logs sdk (#1894) * Add global LogEmitterProvider and convenience function get_log_emitter (#1901) * Add OTLPHandler for standard library logging module (#1903) * Add LogProcessors implementation (#1916) * Fix typos in test_handler.py (#1953) * Add support for OTLP Log exporter (#1943) * Add support for user defined attributes in OTLPHandler (#1952) * use timeout in force_flush (#2118) * use timeout in force_flush * fix lint * Update opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py Co-authored-by: Srikanth Chekuri * fix lint Co-authored-by: Srikanth Chekuri * add a ConsoleExporter for logging (#2099) Co-authored-by: Srikanth Chekuri * Update SDK docs and Add example with OTEL collector logging (debug) exporter (#2050) * Fix exception in severity number transformation (#2208) * Fix exception with warning message transformation * Fix lint * Fix lint * fstring * Demonstrate how to set the Resource for LogEmitterProvider (#2209) * Demonstrate how to set the Resource for LogEmitterProvider Added a Resource to the logs example to make it more complete. Previously it was using the built-in Resource. Now it adds the service.name and service.instance.id attributes. The resulting emitted log records look like this: ``` Resource labels: -> telemetry.sdk.language: STRING(python) -> telemetry.sdk.name: STRING(opentelemetry) -> telemetry.sdk.version: STRING(1.5.0) -> service.name: STRING(shoppingcart) -> service.instance.id: STRING(instance-12) InstrumentationLibraryLogs #0 InstrumentationLibrary __main__ 0.1 LogRecord #0 Timestamp: 2021-10-14 18:33:43.425820928 +0000 UTC Severity: ERROR ShortName: Body: Hyderabad, we have a major problem. Trace ID: ce1577e4a703f42d569e72593ad71888 Span ID: f8908ac4258ceff6 Flags: 1 ``` * Fix linting * Use batch processor in example (#2225) * move logs to _logs (#2240) * move logs to _logs * fix lint * move log_exporter to _log_exporter as it's still experimental (#2252) Co-authored-by: Srikanth Chekuri Co-authored-by: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Co-authored-by: Leighton Chen Co-authored-by: Tigran Najaryan <4194920+tigrannajaryan@users.noreply.github.com> Co-authored-by: Owais Lone --- CHANGELOG.md | 8 + docs/examples/logs/README.rst | 75 +++ docs/examples/logs/example.py | 62 +++ docs/examples/logs/otel-collector-config.yaml | 10 + docs/sdk/logs.export.rst | 7 + docs/sdk/logs.rst | 22 + docs/sdk/logs.severity.rst | 7 + docs/sdk/sdk.rst | 1 + .../otlp/proto/grpc/_log_exporter/__init__.py | 186 +++++++ .../tests/logs/__init__.py | 0 .../tests/logs/test_otlp_logs_exporter.py | 474 +++++++++++++++++ opentelemetry-sdk/setup.cfg | 2 + .../src/opentelemetry/sdk/_logs/__init__.py | 500 ++++++++++++++++++ .../sdk/_logs/export/__init__.py | 301 +++++++++++ .../_logs/export/in_memory_log_exporter.py | 51 ++ .../src/opentelemetry/sdk/_logs/severity.py | 115 ++++ .../sdk/environment_variables.py | 9 + opentelemetry-sdk/tests/logs/__init__.py | 13 + opentelemetry-sdk/tests/logs/test_export.py | 322 +++++++++++ .../tests/logs/test_global_provider.py | 75 +++ opentelemetry-sdk/tests/logs/test_handler.py | 96 ++++ .../tests/logs/test_multi_log_prcessor.py | 194 +++++++ 22 files changed, 2530 insertions(+) create mode 100644 docs/examples/logs/README.rst create mode 100644 docs/examples/logs/example.py create mode 100644 docs/examples/logs/otel-collector-config.yaml create mode 100644 docs/sdk/logs.export.rst create mode 100644 docs/sdk/logs.rst create mode 100644 docs/sdk/logs.severity.rst create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/in_memory_log_exporter.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py create mode 100644 opentelemetry-sdk/tests/logs/__init__.py create mode 100644 opentelemetry-sdk/tests/logs/test_export.py create mode 100644 opentelemetry-sdk/tests/logs/test_global_provider.py create mode 100644 opentelemetry-sdk/tests/logs/test_handler.py create mode 100644 opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ad92cf02b5..5f4c305ae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added dropped count to otlp, jaeger and zipkin exporters. ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) +### Added +- Give OTLPHandler the ability to process attributes + ([#1952](https://github.com/open-telemetry/opentelemetry-python/pull/1952)) +- Add global LogEmitterProvider and convenience function get_log_emitter + ([#1901](https://github.com/open-telemetry/opentelemetry-python/pull/1901)) +- Add OTLPHandler for standard library logging module + ([#1903](https://github.com/open-telemetry/opentelemetry-python/pull/1903)) + ### Changed - Updated `opentelemetry-opencensus-exporter` to use `service_name` of spans instead of resource ([#1897](https://github.com/open-telemetry/opentelemetry-python/pull/1897)) diff --git a/docs/examples/logs/README.rst b/docs/examples/logs/README.rst new file mode 100644 index 0000000000..3c19c2eafe --- /dev/null +++ b/docs/examples/logs/README.rst @@ -0,0 +1,75 @@ +OpenTelemetry Logs SDK +====================== + +Start the Collector locally to see data being exported. Write the following file: + +.. code-block:: yaml + + # otel-collector-config.yaml + receivers: + otlp: + protocols: + grpc: + + exporters: + logging: + + processors: + batch: + +Then start the Docker container: + +.. code-block:: sh + + docker run \ + -p 4317:4317 \ + -v $(pwd)/otel-collector-config.yaml:/etc/otel/config.yaml \ + otel/opentelemetry-collector-contrib:latest + +.. code-block:: sh + + $ python example.py + +The resulting logs will appear in the output from the collector and look similar to this: + +.. code-block:: sh + + ResourceLog #0 + Resource labels: + -> telemetry.sdk.language: STRING(python) + -> telemetry.sdk.name: STRING(opentelemetry) + -> telemetry.sdk.version: STRING(1.5.0.dev0) + -> service.name: STRING(unknown_service) + InstrumentationLibraryLogs #0 + InstrumentationLibrary __main__ 0.1 + LogRecord #0 + Timestamp: 2021-08-18 08:26:53.837349888 +0000 UTC + Severity: ERROR + ShortName: + Body: Exception while exporting logs. + ResourceLog #1 + Resource labels: + -> telemetry.sdk.language: STRING(python) + -> telemetry.sdk.name: STRING(opentelemetry) + -> telemetry.sdk.version: STRING(1.5.0.dev0) + -> service.name: STRING(unknown_service) + InstrumentationLibraryLogs #0 + InstrumentationLibrary __main__ 0.1 + LogRecord #0 + Timestamp: 2021-08-18 08:26:53.842546944 +0000 UTC + Severity: ERROR + ShortName: + Body: The five boxing wizards jump quickly. + ResourceLog #2 + Resource labels: + -> telemetry.sdk.language: STRING(python) + -> telemetry.sdk.name: STRING(opentelemetry) + -> telemetry.sdk.version: STRING(1.5.0.dev0) + -> service.name: STRING(unknown_service) + InstrumentationLibraryLogs #0 + InstrumentationLibrary __main__ 0.1 + LogRecord #0 + Timestamp: 2021-08-18 08:26:53.843979008 +0000 UTC + Severity: ERROR + ShortName: + Body: Hyderabad, we have a major problem. \ No newline at end of file diff --git a/docs/examples/logs/example.py b/docs/examples/logs/example.py new file mode 100644 index 0000000000..b34d9a88cc --- /dev/null +++ b/docs/examples/logs/example.py @@ -0,0 +1,62 @@ +import logging + +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( + OTLPLogExporter, +) +from opentelemetry.sdk._logs import ( + LogEmitterProvider, + OTLPHandler, + set_log_emitter_provider, +) +from opentelemetry.sdk._logs.export import BatchLogProcessor +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, + ConsoleSpanExporter, +) + +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor( + BatchSpanProcessor(ConsoleSpanExporter()) +) + +log_emitter_provider = LogEmitterProvider( + resource=Resource.create( + { + "service.name": "shoppingcart", + "service.instance.id": "instance-12", + } + ), +) +set_log_emitter_provider(log_emitter_provider) + +exporter = OTLPLogExporter(insecure=True) +log_emitter_provider.add_log_processor(BatchLogProcessor(exporter)) +log_emitter = log_emitter_provider.get_log_emitter(__name__, "0.1") +handler = OTLPHandler(level=logging.NOTSET, log_emitter=log_emitter) + +# Attach OTLP handler to root logger +logging.getLogger("root").addHandler(handler) + +# Log directly +logging.info("Jackdaws love my big sphinx of quartz.") + +# Create different namespaced loggers +logger1 = logging.getLogger("myapp.area1") +logger2 = logging.getLogger("myapp.area2") + +logger1.debug("Quick zephyrs blow, vexing daft Jim.") +logger1.info("How quickly daft jumping zebras vex.") +logger2.warning("Jail zesty vixen who grabbed pay from quack.") +logger2.error("The five boxing wizards jump quickly.") + + +# Trace context correlation +tracer = trace.get_tracer(__name__) +with tracer.start_as_current_span("foo"): + # Do something + logger2.error("Hyderabad, we have a major problem.") + +log_emitter_provider.shutdown() diff --git a/docs/examples/logs/otel-collector-config.yaml b/docs/examples/logs/otel-collector-config.yaml new file mode 100644 index 0000000000..f29ce6476c --- /dev/null +++ b/docs/examples/logs/otel-collector-config.yaml @@ -0,0 +1,10 @@ +receivers: + otlp: + protocols: + grpc: + +exporters: + logging: + +processors: + batch: diff --git a/docs/sdk/logs.export.rst b/docs/sdk/logs.export.rst new file mode 100644 index 0000000000..19a4023742 --- /dev/null +++ b/docs/sdk/logs.export.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk._logs.export +============================== + +.. automodule:: opentelemetry.sdk._logs.export + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sdk/logs.rst b/docs/sdk/logs.rst new file mode 100644 index 0000000000..6d9f3c2548 --- /dev/null +++ b/docs/sdk/logs.rst @@ -0,0 +1,22 @@ +opentelemetry.sdk._logs package +=============================== + +.. warning:: + OpenTelemetry Python logs are in an experimental state. The APIs within + :mod:`opentelemetry.sdk._logs` are subject to change in minor/patch releases and make no + backward compatability guarantees at this time. + + Once logs become stable, this package will be be renamed to ``opentelemetry.sdk.logs``. + +Submodules +---------- + +.. toctree:: + + logs.export + logs.severity + +.. automodule:: opentelemetry.sdk._logs + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sdk/logs.severity.rst b/docs/sdk/logs.severity.rst new file mode 100644 index 0000000000..1197e8b44e --- /dev/null +++ b/docs/sdk/logs.severity.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk._logs.severity +================================ + +.. automodule:: opentelemetry.sdk._logs.severity + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/sdk/sdk.rst b/docs/sdk/sdk.rst index 333da1820b..619f3bd8cc 100644 --- a/docs/sdk/sdk.rst +++ b/docs/sdk/sdk.rst @@ -8,5 +8,6 @@ OpenTelemetry Python SDK resources trace + logs error_handler environment_variables diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py new file mode 100644 index 0000000000..211655d93a --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -0,0 +1,186 @@ +# Copyright The OpenTelemetry Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional, Sequence +from grpc import ChannelCredentials, Compression +from opentelemetry.exporter.otlp.proto.grpc.exporter import ( + OTLPExporterMixin, + _translate_key_values, + get_resource_data, + _translate_value, +) +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, +) +from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( + LogsServiceStub, +) +from opentelemetry.proto.common.v1.common_pb2 import InstrumentationLibrary +from opentelemetry.proto.logs.v1.logs_pb2 import ( + InstrumentationLibraryLogs, + ResourceLogs, +) +from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord +from opentelemetry.sdk._logs import LogRecord as SDKLogRecord +from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs.export import LogExporter, LogExportResult + + +class OTLPLogExporter( + LogExporter, + OTLPExporterMixin[SDKLogRecord, ExportLogsServiceRequest, LogExportResult], +): + + _result = LogExportResult + _stub = LogsServiceStub + + def __init__( + self, + endpoint: Optional[str] = None, + insecure: Optional[bool] = None, + credentials: Optional[ChannelCredentials] = None, + headers: Optional[Sequence] = None, + timeout: Optional[int] = None, + compression: Optional[Compression] = None, + ): + super().__init__( + **{ + "endpoint": endpoint, + "insecure": insecure, + "credentials": credentials, + "headers": headers, + "timeout": timeout, + "compression": compression, + } + ) + + def _translate_name(self, log_data: LogData) -> None: + self._collector_log_kwargs["name"] = log_data.log_record.name + + def _translate_time(self, log_data: LogData) -> None: + self._collector_log_kwargs[ + "time_unix_nano" + ] = log_data.log_record.timestamp + + def _translate_span_id(self, log_data: LogData) -> None: + self._collector_log_kwargs[ + "span_id" + ] = log_data.log_record.span_id.to_bytes(8, "big") + + def _translate_trace_id(self, log_data: LogData) -> None: + self._collector_log_kwargs[ + "trace_id" + ] = log_data.log_record.trace_id.to_bytes(16, "big") + + def _translate_trace_flags(self, log_data: LogData) -> None: + self._collector_log_kwargs["flags"] = int( + log_data.log_record.trace_flags + ) + + def _translate_body(self, log_data: LogData): + self._collector_log_kwargs["body"] = _translate_value( + log_data.log_record.body + ) + + def _translate_severity_text(self, log_data: LogData): + self._collector_log_kwargs[ + "severity_text" + ] = log_data.log_record.severity_text + + def _translate_attributes(self, log_data: LogData) -> None: + if log_data.log_record.attributes: + self._collector_log_kwargs["attributes"] = [] + for key, value in log_data.log_record.attributes.items(): + try: + self._collector_log_kwargs["attributes"].append( + _translate_key_values(key, value) + ) + except Exception: # pylint: disable=broad-except + pass + + def _translate_data( + self, data: Sequence[LogData] + ) -> ExportLogsServiceRequest: + # pylint: disable=attribute-defined-outside-init + + sdk_resource_instrumentation_library_logs = {} + + for log_data in data: + resource = log_data.log_record.resource + + instrumentation_library_logs_map = ( + sdk_resource_instrumentation_library_logs.get(resource, {}) + ) + if not instrumentation_library_logs_map: + sdk_resource_instrumentation_library_logs[ + resource + ] = instrumentation_library_logs_map + + instrumentation_library_logs = ( + instrumentation_library_logs_map.get( + log_data.instrumentation_info + ) + ) + if not instrumentation_library_logs: + if log_data.instrumentation_info is not None: + instrumentation_library_logs_map[ + log_data.instrumentation_info + ] = InstrumentationLibraryLogs( + instrumentation_library=InstrumentationLibrary( + name=log_data.instrumentation_info.name, + version=log_data.instrumentation_info.version, + ) + ) + else: + instrumentation_library_logs_map[ + log_data.instrumentation_info + ] = InstrumentationLibraryLogs() + + instrumentation_library_logs = ( + instrumentation_library_logs_map.get( + log_data.instrumentation_info + ) + ) + + self._collector_log_kwargs = {} + + self._translate_name(log_data) + self._translate_time(log_data) + self._translate_span_id(log_data) + self._translate_trace_id(log_data) + self._translate_trace_flags(log_data) + self._translate_body(log_data) + self._translate_severity_text(log_data) + self._translate_attributes(log_data) + + self._collector_log_kwargs[ + "severity_number" + ] = log_data.log_record.severity_number.value + + instrumentation_library_logs.logs.append( + PB2LogRecord(**self._collector_log_kwargs) + ) + + return ExportLogsServiceRequest( + resource_logs=get_resource_data( + sdk_resource_instrumentation_library_logs, + ResourceLogs, + "logs", + ) + ) + + def export(self, batch: Sequence[LogData]) -> LogExportResult: + return self._export(batch) + + def shutdown(self) -> None: + pass diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py new file mode 100644 index 0000000000..b9c33786e3 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -0,0 +1,474 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from concurrent.futures import ThreadPoolExecutor +from unittest import TestCase +from unittest.mock import patch + +from google.protobuf.duration_pb2 import Duration +from google.rpc.error_details_pb2 import RetryInfo +from grpc import StatusCode, server + +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( + OTLPLogExporter, +) +from opentelemetry.exporter.otlp.proto.grpc.exporter import _translate_value +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, + ExportLogsServiceResponse, +) +from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( + LogsServiceServicer, + add_LogsServiceServicer_to_server, +) +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue, + InstrumentationLibrary, + KeyValue, +) +from opentelemetry.proto.logs.v1.logs_pb2 import InstrumentationLibraryLogs +from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord +from opentelemetry.proto.logs.v1.logs_pb2 import ResourceLogs +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as OTLPResource, +) +from opentelemetry.sdk._logs import LogData, LogRecord +from opentelemetry.sdk._logs.export import LogExportResult +from opentelemetry.sdk._logs.severity import ( + SeverityNumber as SDKSeverityNumber, +) +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.trace import TraceFlags + + +class LogsServiceServicerUNAVAILABLEDelay(LogsServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.UNAVAILABLE) + + context.send_initial_metadata( + (("google.rpc.retryinfo-bin", RetryInfo().SerializeToString()),) + ) + context.set_trailing_metadata( + ( + ( + "google.rpc.retryinfo-bin", + RetryInfo( + retry_delay=Duration(seconds=4) + ).SerializeToString(), + ), + ) + ) + + return ExportLogsServiceResponse() + + +class LogsServiceServicerUNAVAILABLE(LogsServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.UNAVAILABLE) + + return ExportLogsServiceResponse() + + +class LogsServiceServicerSUCCESS(LogsServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.OK) + + return ExportLogsServiceResponse() + + +class LogsServiceServicerALREADY_EXISTS(LogsServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.ALREADY_EXISTS) + + return ExportLogsServiceResponse() + + +class TestOTLPLogExporter(TestCase): + def setUp(self): + + self.exporter = OTLPLogExporter() + + self.server = server(ThreadPoolExecutor(max_workers=10)) + + self.server.add_insecure_port("[::]:4317") + + self.server.start() + + self.log_data_1 = LogData( + log_record=LogRecord( + timestamp=int(time.time() * 1e9), + trace_id=2604504634922341076776623263868986797, + span_id=5213367945872657620, + trace_flags=TraceFlags(0x01), + severity_text="WARNING", + severity_number=SDKSeverityNumber.WARN, + name="name", + body="Zhengzhou, We have a heaviest rains in 1000 years", + resource=SDKResource({"key": "value"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_info=InstrumentationInfo( + "first_name", "first_version" + ), + ) + self.log_data_2 = LogData( + log_record=LogRecord( + timestamp=int(time.time() * 1e9), + trace_id=2604504634922341076776623263868986799, + span_id=5213367945872657623, + trace_flags=TraceFlags(0x01), + severity_text="INFO", + severity_number=SDKSeverityNumber.INFO2, + name="info name", + body="Sydney, Opera House is closed", + resource=SDKResource({"key": "value"}), + attributes={"custom_attr": [1, 2, 3]}, + ), + instrumentation_info=InstrumentationInfo( + "second_name", "second_version" + ), + ) + self.log_data_3 = LogData( + log_record=LogRecord( + timestamp=int(time.time() * 1e9), + trace_id=2604504634922341076776623263868986800, + span_id=5213367945872657628, + trace_flags=TraceFlags(0x01), + severity_text="ERROR", + severity_number=SDKSeverityNumber.WARN, + name="error name", + body="Mumbai, Boil water before drinking", + resource=SDKResource({"service": "myapp"}), + ), + instrumentation_info=InstrumentationInfo( + "third_name", "third_version" + ), + ) + + def tearDown(self): + self.server.stop(None) + + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" + ) + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") + @patch( + "opentelemetry.exporter.otlp.proto.grpc._log_exporter.OTLPLogExporter._stub" + ) + # pylint: disable=unused-argument + def test_no_credentials_error( + self, mock_ssl_channel, mock_secure, mock_stub + ): + OTLPLogExporter(insecure=False) + self.assertTrue(mock_ssl_channel.called) + + # pylint: disable=no-self-use + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") + def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): + expected_endpoint = "localhost:4317" + endpoints = [ + ( + "http://localhost:4317", + None, + mock_insecure, + ), + ( + "localhost:4317", + None, + mock_insecure, + ), + ( + "localhost:4317", + False, + mock_secure, + ), + ( + "https://localhost:4317", + None, + mock_secure, + ), + ( + "https://localhost:4317", + True, + mock_insecure, + ), + ] + # pylint: disable=C0209 + for endpoint, insecure, mock_method in endpoints: + OTLPLogExporter(endpoint=endpoint, insecure=insecure) + self.assertEqual( + 1, + mock_method.call_count, + "expected {} to be called for {} {}".format( + mock_method, endpoint, insecure + ), + ) + self.assertEqual( + expected_endpoint, + mock_method.call_args[0][0], + "expected {} got {} {}".format( + expected_endpoint, mock_method.call_args[0][0], endpoint + ), + ) + mock_method.reset_mock() + + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") + def test_unavailable(self, mock_sleep, mock_expo): + + mock_expo.configure_mock(**{"return_value": [1]}) + + add_LogsServiceServicer_to_server( + LogsServiceServicerUNAVAILABLE(), self.server + ) + self.assertEqual( + self.exporter.export([self.log_data_1]), LogExportResult.FAILURE + ) + mock_sleep.assert_called_with(1) + + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") + def test_unavailable_delay(self, mock_sleep, mock_expo): + + mock_expo.configure_mock(**{"return_value": [1]}) + + add_LogsServiceServicer_to_server( + LogsServiceServicerUNAVAILABLEDelay(), self.server + ) + self.assertEqual( + self.exporter.export([self.log_data_1]), LogExportResult.FAILURE + ) + mock_sleep.assert_called_with(4) + + def test_success(self): + add_LogsServiceServicer_to_server( + LogsServiceServicerSUCCESS(), self.server + ) + self.assertEqual( + self.exporter.export([self.log_data_1]), LogExportResult.SUCCESS + ) + + def test_failure(self): + add_LogsServiceServicer_to_server( + LogsServiceServicerALREADY_EXISTS(), self.server + ) + self.assertEqual( + self.exporter.export([self.log_data_1]), LogExportResult.FAILURE + ) + + def test_translate_log_data(self): + + expected = ExportLogsServiceRequest( + resource_logs=[ + ResourceLogs( + resource=OTLPResource( + attributes=[ + KeyValue( + key="key", value=AnyValue(string_value="value") + ), + ] + ), + instrumentation_library_logs=[ + InstrumentationLibraryLogs( + instrumentation_library=InstrumentationLibrary( + name="first_name", version="first_version" + ), + logs=[ + PB2LogRecord( + # pylint: disable=no-member + name="name", + time_unix_nano=self.log_data_1.log_record.timestamp, + severity_number=self.log_data_1.log_record.severity_number.value, + severity_text="WARNING", + span_id=int.to_bytes( + 5213367945872657620, 8, "big" + ), + trace_id=int.to_bytes( + 2604504634922341076776623263868986797, + 16, + "big", + ), + body=_translate_value( + "Zhengzhou, We have a heaviest rains in 1000 years" + ), + attributes=[ + KeyValue( + key="a", + value=AnyValue(int_value=1), + ), + KeyValue( + key="b", + value=AnyValue(string_value="c"), + ), + ], + flags=int( + self.log_data_1.log_record.trace_flags + ), + ) + ], + ) + ], + ), + ] + ) + + # pylint: disable=protected-access + self.assertEqual( + expected, self.exporter._translate_data([self.log_data_1]) + ) + + def test_translate_multiple_logs(self): + expected = ExportLogsServiceRequest( + resource_logs=[ + ResourceLogs( + resource=OTLPResource( + attributes=[ + KeyValue( + key="key", value=AnyValue(string_value="value") + ), + ] + ), + instrumentation_library_logs=[ + InstrumentationLibraryLogs( + instrumentation_library=InstrumentationLibrary( + name="first_name", version="first_version" + ), + logs=[ + PB2LogRecord( + # pylint: disable=no-member + name="name", + time_unix_nano=self.log_data_1.log_record.timestamp, + severity_number=self.log_data_1.log_record.severity_number.value, + severity_text="WARNING", + span_id=int.to_bytes( + 5213367945872657620, 8, "big" + ), + trace_id=int.to_bytes( + 2604504634922341076776623263868986797, + 16, + "big", + ), + body=_translate_value( + "Zhengzhou, We have a heaviest rains in 1000 years" + ), + attributes=[ + KeyValue( + key="a", + value=AnyValue(int_value=1), + ), + KeyValue( + key="b", + value=AnyValue(string_value="c"), + ), + ], + flags=int( + self.log_data_1.log_record.trace_flags + ), + ) + ], + ), + InstrumentationLibraryLogs( + instrumentation_library=InstrumentationLibrary( + name="second_name", version="second_version" + ), + logs=[ + PB2LogRecord( + # pylint: disable=no-member + name="info name", + time_unix_nano=self.log_data_2.log_record.timestamp, + severity_number=self.log_data_2.log_record.severity_number.value, + severity_text="INFO", + span_id=int.to_bytes( + 5213367945872657623, 8, "big" + ), + trace_id=int.to_bytes( + 2604504634922341076776623263868986799, + 16, + "big", + ), + body=_translate_value( + "Sydney, Opera House is closed" + ), + attributes=[ + KeyValue( + key="custom_attr", + value=_translate_value([1, 2, 3]), + ), + ], + flags=int( + self.log_data_2.log_record.trace_flags + ), + ) + ], + ), + ], + ), + ResourceLogs( + resource=OTLPResource( + attributes=[ + KeyValue( + key="service", + value=AnyValue(string_value="myapp"), + ), + ] + ), + instrumentation_library_logs=[ + InstrumentationLibraryLogs( + instrumentation_library=InstrumentationLibrary( + name="third_name", version="third_version" + ), + logs=[ + PB2LogRecord( + # pylint: disable=no-member + name="error name", + time_unix_nano=self.log_data_3.log_record.timestamp, + severity_number=self.log_data_3.log_record.severity_number.value, + severity_text="ERROR", + span_id=int.to_bytes( + 5213367945872657628, 8, "big" + ), + trace_id=int.to_bytes( + 2604504634922341076776623263868986800, + 16, + "big", + ), + body=_translate_value( + "Mumbai, Boil water before drinking" + ), + attributes=[], + flags=int( + self.log_data_3.log_record.trace_flags + ), + ) + ], + ) + ], + ), + ] + ) + + # pylint: disable=protected-access + self.assertEqual( + expected, + self.exporter._translate_data( + [self.log_data_1, self.log_data_2, self.log_data_3] + ), + ) diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 1996249573..158c8a198c 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -54,6 +54,8 @@ opentelemetry_tracer_provider = sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider opentelemetry_traces_exporter = console = opentelemetry.sdk.trace.export:ConsoleSpanExporter +opentelemetry_log_emitter_provider = + sdk_log_emitter_provider = opentelemetry.sdk._logs:LogEmitterProvider opentelemetry_id_generator = random = opentelemetry.sdk.trace.id_generator:RandomIdGenerator opentelemetry_environment_variables = diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py new file mode 100644 index 0000000000..6da162ed0f --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -0,0 +1,500 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import atexit +import concurrent.futures +import json +import logging +import os +import threading +from typing import Any, Callable, Optional, Tuple, Union, cast + +from opentelemetry.sdk._logs.severity import SeverityNumber, std_to_otlp +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_LOG_EMITTER_PROVIDER, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util import ns_to_iso_str +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.trace import ( + format_span_id, + format_trace_id, + get_current_span, +) +from opentelemetry.trace.span import TraceFlags +from opentelemetry.util._providers import _load_provider +from opentelemetry.util._time import _time_ns +from opentelemetry.util.types import Attributes + +_logger = logging.getLogger(__name__) + + +class LogRecord: + """A LogRecord instance represents an event being logged. + + LogRecord instances are created and emitted via `LogEmitter` + every time something is logged. They contain all the information + pertinent to the event being logged. + """ + + def __init__( + self, + timestamp: Optional[int] = None, + trace_id: Optional[int] = None, + span_id: Optional[int] = None, + trace_flags: Optional[TraceFlags] = None, + severity_text: Optional[str] = None, + severity_number: Optional[SeverityNumber] = None, + name: Optional[str] = None, + body: Optional[Any] = None, + resource: Optional[Resource] = None, + attributes: Optional[Attributes] = None, + ): + self.timestamp = timestamp + self.trace_id = trace_id + self.span_id = span_id + self.trace_flags = trace_flags + self.severity_text = severity_text + self.severity_number = severity_number + self.name = name + self.body = body + self.resource = resource + self.attributes = attributes + + def __eq__(self, other: object) -> bool: + if not isinstance(other, LogRecord): + return NotImplemented + return self.__dict__ == other.__dict__ + + def to_json(self) -> str: + return json.dumps( + { + "body": self.body, + "name": self.name, + "severity_number": repr(self.severity_number), + "severity_text": self.severity_text, + "attributes": self.attributes, + "timestamp": ns_to_iso_str(self.timestamp), + "trace_id": f"0x{format_trace_id(self.trace_id)}", + "span_id": f"0x{format_span_id(self.span_id)}", + "trace_flags": self.trace_flags, + "resource": repr(self.resource.attributes) + if self.resource + else "", + }, + indent=4, + ) + + +class LogData: + """Readable LogRecord data plus associated InstrumentationLibrary.""" + + def __init__( + self, + log_record: LogRecord, + instrumentation_info: InstrumentationInfo, + ): + self.log_record = log_record + self.instrumentation_info = instrumentation_info + + +class LogProcessor(abc.ABC): + """Interface to hook the log record emitting action. + + Log processors can be registered directly using + :func:`LogEmitterProvider.add_log_processor` and they are invoked + in the same order as they were registered. + """ + + @abc.abstractmethod + def emit(self, log_data: LogData): + """Emits the `LogData`""" + + @abc.abstractmethod + def shutdown(self): + """Called when a :class:`opentelemetry.sdk._logs.LogEmitter` is shutdown""" + + @abc.abstractmethod + def force_flush(self, timeout_millis: int = 30000): + """Export all the received logs to the configured Exporter that have not yet + been exported. + + Args: + timeout_millis: The maximum amount of time to wait for logs to be + exported. + + Returns: + False if the timeout is exceeded, True otherwise. + """ + + +# Temporary fix until https://github.com/PyCQA/pylint/issues/4098 is resolved +# pylint:disable=no-member +class SynchronousMultiLogProcessor(LogProcessor): + """Implementation of class:`LogProcessor` that forwards all received + events to a list of log processors sequentially. + + The underlying log processors are called in sequential order as they were + added. + """ + + def __init__(self): + # use a tuple to avoid race conditions when adding a new log and + # iterating through it on "emit". + self._log_processors = () # type: Tuple[LogProcessor, ...] + self._lock = threading.Lock() + + def add_log_processor(self, log_processor: LogProcessor) -> None: + """Adds a Logprocessor to the list of log processors handled by this instance""" + with self._lock: + self._log_processors = self._log_processors + (log_processor,) + + def emit(self, log_data: LogData) -> None: + for lp in self._log_processors: + lp.emit(log_data) + + def shutdown(self) -> None: + """Shutdown the log processors one by one""" + for lp in self._log_processors: + lp.shutdown() + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Force flush the log processors one by one + + Args: + timeout_millis: The maximum amount of time to wait for logs to be + exported. If the first n log processors exceeded the timeout + then remaining log processors will not be flushed. + + Returns: + True if all the log processors flushes the logs within timeout, + False otherwise. + """ + deadline_ns = _time_ns() + timeout_millis * 1000000 + for lp in self._log_processors: + current_ts = _time_ns() + if current_ts >= deadline_ns: + return False + + if not lp.force_flush((deadline_ns - current_ts) // 1000000): + return False + + return True + + +class ConcurrentMultiLogProcessor(LogProcessor): + """Implementation of :class:`LogProcessor` that forwards all received + events to a list of log processors in parallel. + + Calls to the underlying log processors are forwarded in parallel by + submitting them to a thread pool executor and waiting until each log + processor finished its work. + + Args: + max_workers: The number of threads managed by the thread pool executor + and thus defining how many log processors can work in parallel. + """ + + def __init__(self, max_workers: int = 2): + # use a tuple to avoid race conditions when adding a new log and + # iterating through it on "emit". + self._log_processors = () # type: Tuple[LogProcessor, ...] + self._lock = threading.Lock() + self._executor = concurrent.futures.ThreadPoolExecutor( + max_workers=max_workers + ) + + def add_log_processor(self, log_processor: LogProcessor): + with self._lock: + self._log_processors = self._log_processors + (log_processor,) + + def _submit_and_wait( + self, + func: Callable[[LogProcessor], Callable[..., None]], + *args: Any, + **kwargs: Any, + ): + futures = [] + for lp in self._log_processors: + future = self._executor.submit(func(lp), *args, **kwargs) + futures.append(future) + for future in futures: + future.result() + + def emit(self, log_data: LogData): + self._submit_and_wait(lambda lp: lp.emit, log_data) + + def shutdown(self): + self._submit_and_wait(lambda lp: lp.shutdown) + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Force flush the log processors in parallel. + + Args: + timeout_millis: The maximum amount of time to wait for logs to be + exported. + + Returns: + True if all the log processors flushes the logs within timeout, + False otherwise. + """ + futures = [] + for lp in self._log_processors: + future = self._executor.submit(lp.force_flush, timeout_millis) + futures.append(future) + + done_futures, not_done_futures = concurrent.futures.wait( + futures, timeout_millis / 1e3 + ) + + if not_done_futures: + return False + + for future in done_futures: + if not future.result(): + return False + + return True + + +# skip natural LogRecord attributes +# http://docs.python.org/library/logging.html#logrecord-attributes +_RESERVED_ATTRS = frozenset( + ( + "asctime", + "args", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "getMessage", + "levelname", + "levelno", + "lineno", + "module", + "msecs", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack_info", + "thread", + "threadName", + ) +) + + +class OTLPHandler(logging.Handler): + """A handler class which writes logging records, in OTLP format, to + a network destination or file. + """ + + def __init__( + self, + level=logging.NOTSET, + log_emitter=None, + ) -> None: + super().__init__(level=level) + self._log_emitter = log_emitter or get_log_emitter(__name__) + + @staticmethod + def _get_attributes(record: logging.LogRecord) -> Attributes: + return { + k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS + } + + def _translate(self, record: logging.LogRecord) -> LogRecord: + timestamp = int(record.created * 1e9) + span_context = get_current_span().get_span_context() + attributes = self._get_attributes(record) + severity_number = std_to_otlp(record.levelno) + return LogRecord( + timestamp=timestamp, + trace_id=span_context.trace_id, + span_id=span_context.span_id, + trace_flags=span_context.trace_flags, + severity_text=record.levelname, + severity_number=severity_number, + body=record.getMessage(), + resource=self._log_emitter.resource, + attributes=attributes, + ) + + def emit(self, record: logging.LogRecord) -> None: + """ + Emit a record. + + The record is translated to OTLP format, and then sent across the pipeline. + """ + self._log_emitter.emit(self._translate(record)) + + def flush(self) -> None: + """ + Flushes the logging output. + """ + self._log_emitter.flush() + + +class LogEmitter: + def __init__( + self, + resource: Resource, + multi_log_processor: Union[ + SynchronousMultiLogProcessor, ConcurrentMultiLogProcessor + ], + instrumentation_info: InstrumentationInfo, + ): + self._resource = resource + self._multi_log_processor = multi_log_processor + self._instrumentation_info = instrumentation_info + + @property + def resource(self): + return self._resource + + def emit(self, record: LogRecord): + """Emits the :class:`LogData` by associating :class:`LogRecord` + and instrumentation info. + """ + log_data = LogData(record, self._instrumentation_info) + self._multi_log_processor.emit(log_data) + + # TODO: Should this flush everything in pipeline? + # Prior discussion https://github.com/open-telemetry/opentelemetry-python/pull/1916#discussion_r659945290 + def flush(self): + """Ensure all logging output has been flushed.""" + self._multi_log_processor.force_flush() + + +class LogEmitterProvider: + def __init__( + self, + resource: Resource = Resource.create(), + shutdown_on_exit: bool = True, + multi_log_processor: Union[ + SynchronousMultiLogProcessor, ConcurrentMultiLogProcessor + ] = None, + ): + self._resource = resource + self._multi_log_processor = ( + multi_log_processor or SynchronousMultiLogProcessor() + ) + self._at_exit_handler = None + if shutdown_on_exit: + self._at_exit_handler = atexit.register(self.shutdown) + + @property + def resource(self): + return self._resource + + def get_log_emitter( + self, + instrumenting_module_name: str, + instrumenting_module_verison: str = "", + ) -> LogEmitter: + return LogEmitter( + self._resource, + self._multi_log_processor, + InstrumentationInfo( + instrumenting_module_name, instrumenting_module_verison + ), + ) + + def add_log_processor(self, log_processor: LogProcessor): + """Registers a new :class:`LogProcessor` for this `LogEmitterProvider` instance. + + The log processors are invoked in the same order they are registered. + """ + self._multi_log_processor.add_log_processor(log_processor) + + def shutdown(self): + """Shuts down the log processors.""" + self._multi_log_processor.shutdown() + if self._at_exit_handler is not None: + atexit.unregister(self._at_exit_handler) + self._at_exit_handler = None + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Force flush the log processors. + + Args: + timeout_millis: The maximum amount of time to wait for logs to be + exported. + + Returns: + True if all the log processors flushes the logs within timeout, + False otherwise. + """ + return self._multi_log_processor.force_flush(timeout_millis) + + +_LOG_EMITTER_PROVIDER = None + + +def get_log_emitter_provider() -> LogEmitterProvider: + """Gets the current global :class:`~.LogEmitterProvider` object.""" + global _LOG_EMITTER_PROVIDER # pylint: disable=global-statement + if _LOG_EMITTER_PROVIDER is None: + if _OTEL_PYTHON_LOG_EMITTER_PROVIDER not in os.environ: + _LOG_EMITTER_PROVIDER = LogEmitterProvider() + return _LOG_EMITTER_PROVIDER + + _LOG_EMITTER_PROVIDER = cast( + "LogEmitterProvider", + _load_provider( + _OTEL_PYTHON_LOG_EMITTER_PROVIDER, "log_emitter_provider" + ), + ) + + return _LOG_EMITTER_PROVIDER + + +def set_log_emitter_provider(log_emitter_provider: LogEmitterProvider) -> None: + """Sets the current global :class:`~.LogEmitterProvider` object. + + This can only be done once, a warning will be logged if any furter attempt + is made. + """ + global _LOG_EMITTER_PROVIDER # pylint: disable=global-statement + + if _LOG_EMITTER_PROVIDER is not None: + _logger.warning( + "Overriding of current LogEmitterProvider is not allowed" + ) + return + + _LOG_EMITTER_PROVIDER = log_emitter_provider + + +def get_log_emitter( + instrumenting_module_name: str, + instrumenting_library_version: str = "", + log_emitter_provider: Optional[LogEmitterProvider] = None, +) -> LogEmitter: + """Returns a `LogEmitter` for use within a python process. + + This function is a convenience wrapper for + opentelemetry.sdk._logs.LogEmitterProvider.get_log_emitter. + + If log_emitter_provider param is omitted the current configured one is used. + """ + if log_emitter_provider is None: + log_emitter_provider = get_log_emitter_provider() + return log_emitter_provider.get_log_emitter( + instrumenting_module_name, instrumenting_library_version + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py new file mode 100644 index 0000000000..f65c967534 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py @@ -0,0 +1,301 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import collections +import enum +import logging +import sys +import threading +from os import linesep +from typing import IO, Callable, Deque, List, Optional, Sequence + +from opentelemetry.context import attach, detach, set_value +from opentelemetry.sdk._logs import LogData, LogProcessor, LogRecord +from opentelemetry.util._time import _time_ns + +_logger = logging.getLogger(__name__) + + +class LogExportResult(enum.Enum): + SUCCESS = 0 + FAILURE = 1 + + +class LogExporter(abc.ABC): + """Interface for exporting logs. + + Interface to be implemented by services that want to export logs received + in their own format. + + To export data this MUST be registered to the :class`opentelemetry.sdk._logs.LogEmitter` using a + log processor. + """ + + @abc.abstractmethod + def export(self, batch: Sequence[LogData]): + """Exports a batch of logs. + + Args: + batch: The list of `LogData` objects to be exported + + Returns: + The result of the export + """ + + @abc.abstractmethod + def shutdown(self): + """Shuts down the exporter. + + Called when the SDK is shut down. + """ + + +class ConsoleExporter(LogExporter): + """Implementation of :class:`LogExporter` that prints log records to the + console. + + This class can be used for diagnostic purposes. It prints the exported + log records to the console STDOUT. + """ + + def __init__( + self, + out: IO = sys.stdout, + formatter: Callable[[LogRecord], str] = lambda record: record.to_json() + + linesep, + ): + self.out = out + self.formatter = formatter + + def export(self, batch: Sequence[LogData]): + for data in batch: + self.out.write(self.formatter(data.log_record)) + self.out.flush() + return LogExportResult.SUCCESS + + def shutdown(self): + pass + + +class SimpleLogProcessor(LogProcessor): + """This is an implementation of LogProcessor which passes + received logs in the export-friendly LogData representation to the + configured LogExporter, as soon as they are emitted. + """ + + def __init__(self, exporter: LogExporter): + self._exporter = exporter + self._shutdown = False + + def emit(self, log_data: LogData): + if self._shutdown: + _logger.warning("Processor is already shutdown, ignoring call") + return + token = attach(set_value("suppress_instrumentation", True)) + try: + self._exporter.export((log_data,)) + except Exception: # pylint: disable=broad-except + _logger.exception("Exception while exporting logs.") + detach(token) + + def shutdown(self): + self._shutdown = True + self._exporter.shutdown() + + def force_flush( + self, timeout_millis: int = 30000 + ) -> bool: # pylint: disable=no-self-use + return True + + +class _FlushRequest: + __slots__ = ["event", "num_log_records"] + + def __init__(self): + self.event = threading.Event() + self.num_log_records = 0 + + +class BatchLogProcessor(LogProcessor): + """This is an implementation of LogProcessor which creates batches of + received logs in the export-friendly LogData representation and + send to the configured LogExporter, as soon as they are emitted. + """ + + def __init__( + self, + exporter: LogExporter, + schedule_delay_millis: int = 5000, + max_export_batch_size: int = 512, + export_timeout_millis: int = 30000, + ): + self._exporter = exporter + self._schedule_delay_millis = schedule_delay_millis + self._max_export_batch_size = max_export_batch_size + self._export_timeout_millis = export_timeout_millis + self._queue = collections.deque() # type: Deque[LogData] + self._worker_thread = threading.Thread(target=self.worker, daemon=True) + self._condition = threading.Condition(threading.Lock()) + self._shutdown = False + self._flush_request = None # type: Optional[_FlushRequest] + self._log_records = [ + None + ] * self._max_export_batch_size # type: List[Optional[LogData]] + self._worker_thread.start() + + def worker(self): + timeout = self._schedule_delay_millis / 1e3 + flush_request = None # type: Optional[_FlushRequest] + while not self._shutdown: + with self._condition: + if self._shutdown: + # shutdown may have been called, avoid further processing + break + flush_request = self._get_and_unset_flush_request() + if ( + len(self._queue) < self._max_export_batch_size + and self._flush_request is None + ): + self._condition.wait(timeout) + + flush_request = self._get_and_unset_flush_request() + if not self._queue: + timeout = self._schedule_delay_millis / 1e3 + self._notify_flush_request_finished(flush_request) + flush_request = None + continue + if self._shutdown: + break + + start_ns = _time_ns() + self._export(flush_request) + end_ns = _time_ns() + # subtract the duration of this export call to the next timeout + timeout = self._schedule_delay_millis / 1e3 - ( + (end_ns - start_ns) / 1e9 + ) + + self._notify_flush_request_finished(flush_request) + flush_request = None + + # there might have been a new flush request while export was running + # and before the done flag switched to true + with self._condition: + shutdown_flush_request = self._get_and_unset_flush_request() + + # flush the remaining logs + self._drain_queue() + self._notify_flush_request_finished(flush_request) + self._notify_flush_request_finished(shutdown_flush_request) + + def _export(self, flush_request: Optional[_FlushRequest] = None): + """Exports logs considering the given flush_request. + + If flush_request is not None then logs are exported in batches + until the number of exported logs reached or exceeded the num of logs in + flush_request, otherwise exports at max max_export_batch_size logs. + """ + if flush_request is None: + self._export_batch() + return + + num_log_records = flush_request.num_log_records + while self._queue: + exported = self._export_batch() + num_log_records -= exported + + if num_log_records <= 0: + break + + def _export_batch(self) -> int: + """Exports at most max_export_batch_size logs and returns the number of + exported logs. + """ + idx = 0 + while idx < self._max_export_batch_size and self._queue: + record = self._queue.pop() + self._log_records[idx] = record + idx += 1 + token = attach(set_value("suppress_instrumentation", True)) + try: + self._exporter.export(self._log_records[:idx]) # type: ignore + except Exception: # pylint: disable=broad-except + _logger.exception("Exception while exporting logs.") + detach(token) + + for index in range(idx): + self._log_records[index] = None + return idx + + def _drain_queue(self): + """Export all elements until queue is empty. + + Can only be called from the worker thread context because it invokes + `export` that is not thread safe. + """ + while self._queue: + self._export_batch() + + def _get_and_unset_flush_request(self) -> Optional[_FlushRequest]: + flush_request = self._flush_request + self._flush_request = None + if flush_request is not None: + flush_request.num_log_records = len(self._queue) + return flush_request + + @staticmethod + def _notify_flush_request_finished( + flush_request: Optional[_FlushRequest] = None, + ): + if flush_request is not None: + flush_request.event.set() + + def _get_or_create_flush_request(self) -> _FlushRequest: + if self._flush_request is None: + self._flush_request = _FlushRequest() + return self._flush_request + + def emit(self, log_data: LogData) -> None: + """Adds the `LogData` to queue and notifies the waiting threads + when size of queue reaches max_export_batch_size. + """ + if self._shutdown: + return + self._queue.appendleft(log_data) + if len(self._queue) >= self._max_export_batch_size: + with self._condition: + self._condition.notify() + + def shutdown(self): + self._shutdown = True + with self._condition: + self._condition.notify_all() + self._worker_thread.join() + self._exporter.shutdown() + + def force_flush(self, timeout_millis: Optional[int] = None) -> bool: + if timeout_millis is None: + timeout_millis = self._export_timeout_millis + if self._shutdown: + return True + + with self._condition: + flush_request = self._get_or_create_flush_request() + self._condition.notify_all() + + ret = flush_request.event.wait(timeout_millis / 1e3) + if not ret: + _logger.warning("Timeout was exceeded in force_flush().") + return ret diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/in_memory_log_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/in_memory_log_exporter.py new file mode 100644 index 0000000000..68cb6b7389 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/in_memory_log_exporter.py @@ -0,0 +1,51 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import typing + +from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs.export import LogExporter, LogExportResult + + +class InMemoryLogExporter(LogExporter): + """Implementation of :class:`.LogExporter` that stores logs in memory. + + This class can be used for testing purposes. It stores the exported logs + in a list in memory that can be retrieved using the + :func:`.get_finished_logs` method. + """ + + def __init__(self): + self._logs = [] + self._lock = threading.Lock() + self._stopped = False + + def clear(self) -> None: + with self._lock: + self._logs.clear() + + def get_finished_logs(self) -> typing.Tuple[LogData, ...]: + with self._lock: + return tuple(self._logs) + + def export(self, batch: typing.Sequence[LogData]) -> LogExportResult: + if self._stopped: + return LogExportResult.FAILURE + with self._lock: + self._logs.extend(batch) + return LogExportResult.SUCCESS + + def shutdown(self) -> None: + self._stopped = True diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py new file mode 100644 index 0000000000..2570375990 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py @@ -0,0 +1,115 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import enum + + +class SeverityNumber(enum.Enum): + """Numerical value of severity. + + Smaller numerical values correspond to less severe events + (such as debug events), larger numerical values correspond + to more severe events (such as errors and critical events). + + See the `Log Data Model`_ spec for more info and how to map the + severity from source format to OTLP Model. + + .. _Log Data Model: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber + """ + + UNSPECIFIED = 0 + TRACE = 1 + TRACE2 = 2 + TRACE3 = 3 + TRACE4 = 4 + DEBUG = 5 + DEBUG2 = 6 + DEBUG3 = 7 + DEBUG4 = 8 + INFO = 9 + INFO2 = 10 + INFO3 = 11 + INFO4 = 12 + WARN = 13 + WARN2 = 14 + WARN3 = 15 + WARN4 = 16 + ERROR = 17 + ERROR2 = 18 + ERROR3 = 19 + ERROR4 = 20 + FATAL = 21 + FATAL2 = 22 + FATAL3 = 23 + FATAL4 = 24 + + +_STD_TO_OTLP = { + 10: SeverityNumber.DEBUG, + 11: SeverityNumber.DEBUG2, + 12: SeverityNumber.DEBUG3, + 13: SeverityNumber.DEBUG4, + 14: SeverityNumber.DEBUG4, + 15: SeverityNumber.DEBUG4, + 16: SeverityNumber.DEBUG4, + 17: SeverityNumber.DEBUG4, + 18: SeverityNumber.DEBUG4, + 19: SeverityNumber.DEBUG4, + 20: SeverityNumber.INFO, + 21: SeverityNumber.INFO2, + 22: SeverityNumber.INFO3, + 23: SeverityNumber.INFO4, + 24: SeverityNumber.INFO4, + 25: SeverityNumber.INFO4, + 26: SeverityNumber.INFO4, + 27: SeverityNumber.INFO4, + 28: SeverityNumber.INFO4, + 29: SeverityNumber.INFO4, + 30: SeverityNumber.WARN, + 31: SeverityNumber.WARN2, + 32: SeverityNumber.WARN3, + 33: SeverityNumber.WARN4, + 34: SeverityNumber.WARN4, + 35: SeverityNumber.WARN4, + 36: SeverityNumber.WARN4, + 37: SeverityNumber.WARN4, + 38: SeverityNumber.WARN4, + 39: SeverityNumber.WARN4, + 40: SeverityNumber.ERROR, + 41: SeverityNumber.ERROR2, + 42: SeverityNumber.ERROR3, + 43: SeverityNumber.ERROR4, + 44: SeverityNumber.ERROR4, + 45: SeverityNumber.ERROR4, + 46: SeverityNumber.ERROR4, + 47: SeverityNumber.ERROR4, + 48: SeverityNumber.ERROR4, + 49: SeverityNumber.ERROR4, + 50: SeverityNumber.FATAL, + 51: SeverityNumber.FATAL2, + 52: SeverityNumber.FATAL3, + 53: SeverityNumber.FATAL4, +} + + +def std_to_otlp(levelno: int) -> SeverityNumber: + """ + Map python log levelno as defined in https://docs.python.org/3/library/logging.html#logging-levels + to OTLP log severity number. + """ + if levelno < 10: + return SeverityNumber.UNSPECIFIED + if levelno > 53: + return SeverityNumber.FATAL4 + return _STD_TO_OTLP[levelno] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 8b3d4abbf8..e9d35092a6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -369,3 +369,12 @@ If both are set, :envvar:`OTEL_SERVICE_NAME` takes precedence. """ + +_OTEL_PYTHON_LOG_EMITTER_PROVIDER = "OTEL_PYTHON_LOG_EMITTER_PROVIDER" +""" +.. envvar:: OTEL_PYTHON_LOG_EMITTER_PROVIDER + +The :envvar:`OTEL_PYTHON_LOG_EMITTER_PROVIDER` environment variable allows users to +provide the entry point for loading the log emitter provider. If not specified, SDK +LogEmitterProvider is used. +""" diff --git a/opentelemetry-sdk/tests/logs/__init__.py b/opentelemetry-sdk/tests/logs/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/opentelemetry-sdk/tests/logs/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py new file mode 100644 index 0000000000..964b44f769 --- /dev/null +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -0,0 +1,322 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access +import logging +import os +import time +import unittest +from concurrent.futures import ThreadPoolExecutor +from unittest.mock import Mock, patch + +from opentelemetry.sdk import trace +from opentelemetry.sdk._logs import ( + LogData, + LogEmitterProvider, + LogRecord, + OTLPHandler, +) +from opentelemetry.sdk._logs.export import ( + BatchLogProcessor, + ConsoleExporter, + SimpleLogProcessor, +) +from opentelemetry.sdk._logs.export.in_memory_log_exporter import ( + InMemoryLogExporter, +) +from opentelemetry.sdk._logs.severity import SeverityNumber +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.trace import TraceFlags +from opentelemetry.trace.span import INVALID_SPAN_CONTEXT + + +class TestSimpleLogProcessor(unittest.TestCase): + def test_simple_log_processor_default_level(self): + exporter = InMemoryLogExporter() + log_emitter_provider = LogEmitterProvider() + log_emitter = log_emitter_provider.get_log_emitter(__name__) + + log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) + + logger = logging.getLogger("default_level") + logger.addHandler(OTLPHandler(log_emitter=log_emitter)) + + logger.warning("Something is wrong") + finished_logs = exporter.get_finished_logs() + self.assertEqual(len(finished_logs), 1) + warning_log_record = finished_logs[0].log_record + self.assertEqual(warning_log_record.body, "Something is wrong") + self.assertEqual(warning_log_record.severity_text, "WARNING") + self.assertEqual( + warning_log_record.severity_number, SeverityNumber.WARN + ) + + def test_simple_log_processor_custom_level(self): + exporter = InMemoryLogExporter() + log_emitter_provider = LogEmitterProvider() + log_emitter = log_emitter_provider.get_log_emitter(__name__) + + log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) + + logger = logging.getLogger("custom_level") + logger.setLevel(logging.ERROR) + logger.addHandler(OTLPHandler(log_emitter=log_emitter)) + + logger.warning("Warning message") + logger.debug("Debug message") + logger.error("Error message") + logger.critical("Critical message") + finished_logs = exporter.get_finished_logs() + # Make sure only level >= logging.CRITICAL logs are recorded + self.assertEqual(len(finished_logs), 2) + critical_log_record = finished_logs[0].log_record + fatal_log_record = finished_logs[1].log_record + self.assertEqual(critical_log_record.body, "Error message") + self.assertEqual(critical_log_record.severity_text, "ERROR") + self.assertEqual( + critical_log_record.severity_number, SeverityNumber.ERROR + ) + self.assertEqual(fatal_log_record.body, "Critical message") + self.assertEqual(fatal_log_record.severity_text, "CRITICAL") + self.assertEqual( + fatal_log_record.severity_number, SeverityNumber.FATAL + ) + + def test_simple_log_processor_trace_correlation(self): + exporter = InMemoryLogExporter() + log_emitter_provider = LogEmitterProvider() + log_emitter = log_emitter_provider.get_log_emitter("name", "version") + + log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) + + logger = logging.getLogger("trace_correlation") + logger.addHandler(OTLPHandler(log_emitter=log_emitter)) + + logger.warning("Warning message") + finished_logs = exporter.get_finished_logs() + self.assertEqual(len(finished_logs), 1) + log_record = finished_logs[0].log_record + self.assertEqual(log_record.body, "Warning message") + self.assertEqual(log_record.severity_text, "WARNING") + self.assertEqual(log_record.severity_number, SeverityNumber.WARN) + self.assertEqual(log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id) + self.assertEqual(log_record.span_id, INVALID_SPAN_CONTEXT.span_id) + self.assertEqual( + log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags + ) + exporter.clear() + + tracer = trace.TracerProvider().get_tracer(__name__) + with tracer.start_as_current_span("test") as span: + logger.critical("Critical message within span") + + finished_logs = exporter.get_finished_logs() + log_record = finished_logs[0].log_record + self.assertEqual(log_record.body, "Critical message within span") + self.assertEqual(log_record.severity_text, "CRITICAL") + self.assertEqual(log_record.severity_number, SeverityNumber.FATAL) + span_context = span.get_span_context() + self.assertEqual(log_record.trace_id, span_context.trace_id) + self.assertEqual(log_record.span_id, span_context.span_id) + self.assertEqual(log_record.trace_flags, span_context.trace_flags) + + def test_simple_log_processor_shutdown(self): + exporter = InMemoryLogExporter() + log_emitter_provider = LogEmitterProvider() + log_emitter = log_emitter_provider.get_log_emitter(__name__) + + log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) + + logger = logging.getLogger("shutdown") + logger.addHandler(OTLPHandler(log_emitter=log_emitter)) + + logger.warning("Something is wrong") + finished_logs = exporter.get_finished_logs() + self.assertEqual(len(finished_logs), 1) + warning_log_record = finished_logs[0].log_record + self.assertEqual(warning_log_record.body, "Something is wrong") + self.assertEqual(warning_log_record.severity_text, "WARNING") + self.assertEqual( + warning_log_record.severity_number, SeverityNumber.WARN + ) + exporter.clear() + log_emitter_provider.shutdown() + logger.warning("Log after shutdown") + finished_logs = exporter.get_finished_logs() + self.assertEqual(len(finished_logs), 0) + + +class TestBatchLogProcessor(unittest.TestCase): + def test_emit_call_log_record(self): + exporter = InMemoryLogExporter() + log_processor = Mock(wraps=BatchLogProcessor(exporter)) + provider = LogEmitterProvider() + provider.add_log_processor(log_processor) + + emitter = provider.get_log_emitter(__name__) + logger = logging.getLogger("emit_call") + logger.addHandler(OTLPHandler(log_emitter=emitter)) + + logger.error("error") + self.assertEqual(log_processor.emit.call_count, 1) + + def test_shutdown(self): + exporter = InMemoryLogExporter() + log_processor = BatchLogProcessor(exporter) + + provider = LogEmitterProvider() + provider.add_log_processor(log_processor) + + emitter = provider.get_log_emitter(__name__) + logger = logging.getLogger("shutdown") + logger.addHandler(OTLPHandler(log_emitter=emitter)) + + logger.warning("warning message: %s", "possible upcoming heatwave") + logger.error("Very high rise in temperatures across the globe") + logger.critical("Temparature hits high 420 C in Hyderabad") + + log_processor.shutdown() + self.assertTrue(exporter._stopped) + + finished_logs = exporter.get_finished_logs() + expected = [ + ("warning message: possible upcoming heatwave", "WARNING"), + ("Very high rise in temperatures across the globe", "ERROR"), + ( + "Temparature hits high 420 C in Hyderabad", + "CRITICAL", + ), + ] + emitted = [ + (item.log_record.body, item.log_record.severity_text) + for item in finished_logs + ] + self.assertEqual(expected, emitted) + + def test_force_flush(self): + exporter = InMemoryLogExporter() + log_processor = BatchLogProcessor(exporter) + + provider = LogEmitterProvider() + provider.add_log_processor(log_processor) + + emitter = provider.get_log_emitter(__name__) + logger = logging.getLogger("force_flush") + logger.addHandler(OTLPHandler(log_emitter=emitter)) + + logger.critical("Earth is burning") + log_processor.force_flush() + finished_logs = exporter.get_finished_logs() + self.assertEqual(len(finished_logs), 1) + log_record = finished_logs[0].log_record + self.assertEqual(log_record.body, "Earth is burning") + self.assertEqual(log_record.severity_number, SeverityNumber.FATAL) + + def test_log_processor_too_many_logs(self): + exporter = InMemoryLogExporter() + log_processor = BatchLogProcessor(exporter) + + provider = LogEmitterProvider() + provider.add_log_processor(log_processor) + + emitter = provider.get_log_emitter(__name__) + logger = logging.getLogger("many_logs") + logger.addHandler(OTLPHandler(log_emitter=emitter)) + + for log_no in range(1000): + logger.critical("Log no: %s", log_no) + + self.assertTrue(log_processor.force_flush()) + finised_logs = exporter.get_finished_logs() + self.assertEqual(len(finised_logs), 1000) + + def test_with_multiple_threads(self): + exporter = InMemoryLogExporter() + log_processor = BatchLogProcessor(exporter) + + provider = LogEmitterProvider() + provider.add_log_processor(log_processor) + + emitter = provider.get_log_emitter(__name__) + logger = logging.getLogger("threads") + logger.addHandler(OTLPHandler(log_emitter=emitter)) + + def bulk_log_and_flush(num_logs): + for _ in range(num_logs): + logger.critical("Critical message") + self.assertTrue(log_processor.force_flush()) + + with ThreadPoolExecutor(max_workers=69) as executor: + futures = [] + for idx in range(69): + future = executor.submit(bulk_log_and_flush, idx + 1) + futures.append(future) + + executor.shutdown() + + finished_logs = exporter.get_finished_logs() + self.assertEqual(len(finished_logs), 2415) + + +class TestConsoleExporter(unittest.TestCase): + def test_export(self): # pylint: disable=no-self-use + """Check that the console exporter prints log records.""" + log_data = LogData( + log_record=LogRecord( + timestamp=int(time.time() * 1e9), + trace_id=2604504634922341076776623263868986797, + span_id=5213367945872657620, + trace_flags=TraceFlags(0x01), + severity_text="WARN", + severity_number=SeverityNumber.WARN, + name="name", + body="Zhengzhou, We have a heaviest rains in 1000 years", + resource=SDKResource({"key": "value"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_info=InstrumentationInfo( + "first_name", "first_version" + ), + ) + exporter = ConsoleExporter() + # Mocking stdout interferes with debugging and test reporting, mock on + # the exporter instance instead. + + with patch.object(exporter, "out") as mock_stdout: + exporter.export([log_data]) + mock_stdout.write.assert_called_once_with( + log_data.log_record.to_json() + os.linesep + ) + + self.assertEqual(mock_stdout.write.call_count, 1) + self.assertEqual(mock_stdout.flush.call_count, 1) + + def test_export_custom(self): # pylint: disable=no-self-use + """Check that console exporter uses custom io, formatter.""" + mock_record_str = Mock(str) + + def formatter(record): # pylint: disable=unused-argument + return mock_record_str + + mock_stdout = Mock() + exporter = ConsoleExporter(out=mock_stdout, formatter=formatter) + log_data = LogData( + log_record=LogRecord(), + instrumentation_info=InstrumentationInfo( + "first_name", "first_version" + ), + ) + exporter.export([log_data]) + mock_stdout.write.assert_called_once_with(mock_record_str) diff --git a/opentelemetry-sdk/tests/logs/test_global_provider.py b/opentelemetry-sdk/tests/logs/test_global_provider.py new file mode 100644 index 0000000000..7a249defcf --- /dev/null +++ b/opentelemetry-sdk/tests/logs/test_global_provider.py @@ -0,0 +1,75 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# type:ignore +import unittest +from importlib import reload +from logging import WARNING +from unittest.mock import patch + +from opentelemetry.sdk import _logs +from opentelemetry.sdk._logs import ( + LogEmitterProvider, + get_log_emitter_provider, + set_log_emitter_provider, +) +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_LOG_EMITTER_PROVIDER, +) + + +class TestGlobals(unittest.TestCase): + def tearDown(self): + reload(_logs) + + def check_override_not_allowed(self): + """set_log_emitter_provider should throw a warning when overridden""" + provider = get_log_emitter_provider() + with self.assertLogs(level=WARNING) as test: + set_log_emitter_provider(LogEmitterProvider()) + self.assertEqual( + test.output, + [ + ( + "WARNING:opentelemetry.sdk._logs:Overriding of current " + "LogEmitterProvider is not allowed" + ) + ], + ) + self.assertIs(provider, get_log_emitter_provider()) + + def test_set_tracer_provider(self): + reload(_logs) + provider = LogEmitterProvider() + set_log_emitter_provider(provider) + retrieved_provider = get_log_emitter_provider() + self.assertEqual(provider, retrieved_provider) + + def test_tracer_provider_override_warning(self): + reload(_logs) + self.check_override_not_allowed() + + @patch.dict( + "os.environ", + {_OTEL_PYTHON_LOG_EMITTER_PROVIDER: "sdk_log_emitter_provider"}, + ) + def test_sdk_log_emitter_provider(self): + reload(_logs) + self.check_override_not_allowed() + + @patch.dict("os.environ", {_OTEL_PYTHON_LOG_EMITTER_PROVIDER: "unknown"}) + def test_unknown_log_emitter_provider(self): + reload(_logs) + with self.assertRaises(Exception): + get_log_emitter_provider() diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py new file mode 100644 index 0000000000..d7942f912b --- /dev/null +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -0,0 +1,96 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import unittest +from unittest.mock import Mock + +from opentelemetry.sdk import trace +from opentelemetry.sdk._logs import LogEmitter, OTLPHandler +from opentelemetry.sdk._logs.severity import SeverityNumber +from opentelemetry.trace import INVALID_SPAN_CONTEXT + + +def get_logger(level=logging.NOTSET, log_emitter=None): + logger = logging.getLogger(__name__) + handler = OTLPHandler(level=level, log_emitter=log_emitter) + logger.addHandler(handler) + return logger + + +class TestOTLPHandler(unittest.TestCase): + def test_handler_default_log_level(self): + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(log_emitter=emitter_mock) + # Make sure debug messages are ignored by default + logger.debug("Debug message") + self.assertEqual(emitter_mock.emit.call_count, 0) + # Assert emit gets called for warning message + logger.warning("Warning message") + self.assertEqual(emitter_mock.emit.call_count, 1) + + def test_handler_custom_log_level(self): + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(level=logging.ERROR, log_emitter=emitter_mock) + logger.warning("Warning message test custom log level") + # Make sure any log with level < ERROR is ignored + self.assertEqual(emitter_mock.emit.call_count, 0) + logger.error("Mumbai, we have a major problem") + logger.critical("No Time For Caution") + self.assertEqual(emitter_mock.emit.call_count, 2) + + def test_log_record_no_span_context(self): + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(log_emitter=emitter_mock) + # Assert emit gets called for warning message + logger.warning("Warning message") + args, _ = emitter_mock.emit.call_args_list[0] + log_record = args[0] + + self.assertIsNotNone(log_record) + self.assertEqual(log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id) + self.assertEqual(log_record.span_id, INVALID_SPAN_CONTEXT.span_id) + self.assertEqual( + log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags + ) + + def test_log_record_user_attributes(self): + """Attributes can be injected into logs by adding them to the LogRecord""" + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(log_emitter=emitter_mock) + # Assert emit gets called for warning message + logger.warning("Warning message", extra={"http.status_code": 200}) + args, _ = emitter_mock.emit.call_args_list[0] + log_record = args[0] + + self.assertIsNotNone(log_record) + self.assertEqual(log_record.attributes, {"http.status_code": 200}) + + def test_log_record_trace_correlation(self): + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(log_emitter=emitter_mock) + + tracer = trace.TracerProvider().get_tracer(__name__) + with tracer.start_as_current_span("test") as span: + logger.critical("Critical message within span") + + args, _ = emitter_mock.emit.call_args_list[0] + log_record = args[0] + self.assertEqual(log_record.body, "Critical message within span") + self.assertEqual(log_record.severity_text, "CRITICAL") + self.assertEqual(log_record.severity_number, SeverityNumber.FATAL) + span_context = span.get_span_context() + self.assertEqual(log_record.trace_id, span_context.trace_id) + self.assertEqual(log_record.span_id, span_context.span_id) + self.assertEqual(log_record.trace_flags, span_context.trace_flags) diff --git a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py new file mode 100644 index 0000000000..e55124edcc --- /dev/null +++ b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py @@ -0,0 +1,194 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint:disable=protected-access,no-self-use,no-member + +import logging +import threading +import time +import unittest +from abc import ABC, abstractmethod +from unittest.mock import Mock + +from opentelemetry.sdk._logs import ( + ConcurrentMultiLogProcessor, + LogEmitterProvider, + LogProcessor, + LogRecord, + OTLPHandler, + SynchronousMultiLogProcessor, +) +from opentelemetry.sdk._logs.severity import SeverityNumber + + +class AnotherLogProcessor(LogProcessor): + def __init__(self, exporter, logs_list): + self._exporter = exporter + self._log_list = logs_list + self._closed = False + + def emit(self, log_data): + if self._closed: + return + self._log_list.append( + (log_data.log_record.body, log_data.log_record.severity_text) + ) + + def shutdown(self): + self._closed = True + self._exporter.shutdown() + + def force_flush(self, timeout_millis=30000): + self._log_list.clear() + return True + + +class TestLogProcessor(unittest.TestCase): + def test_log_processor(self): + provider = LogEmitterProvider() + log_emitter = provider.get_log_emitter(__name__) + handler = OTLPHandler(log_emitter=log_emitter) + + logs_list_1 = [] + processor1 = AnotherLogProcessor(Mock(), logs_list_1) + logs_list_2 = [] + processor2 = AnotherLogProcessor(Mock(), logs_list_2) + + logger = logging.getLogger("test.span.processor") + logger.addHandler(handler) + + # Test no proessor added + logger.critical("Odisha, we have another major cyclone") + + self.assertEqual(len(logs_list_1), 0) + self.assertEqual(len(logs_list_2), 0) + + # Add one processor + provider.add_log_processor(processor1) + logger.warning("Brace yourself") + logger.error("Some error message") + + expected_list_1 = [ + ("Brace yourself", "WARNING"), + ("Some error message", "ERROR"), + ] + self.assertEqual(logs_list_1, expected_list_1) + + # Add another processor + provider.add_log_processor(processor2) + logger.critical("Something disastrous") + expected_list_1.append(("Something disastrous", "CRITICAL")) + + expected_list_2 = [("Something disastrous", "CRITICAL")] + + self.assertEqual(logs_list_1, expected_list_1) + self.assertEqual(logs_list_2, expected_list_2) + + +class MultiLogProcessorTestBase(ABC): + @abstractmethod + def _get_multi_log_processor(self): + pass + + def make_record(self): + return LogRecord( + timestamp=1622300111608942000, + severity_text="WARNING", + severity_number=SeverityNumber.WARN, + body="Warning message", + ) + + def test_on_emit(self): + multi_log_processor = self._get_multi_log_processor() + mocks = [Mock(spec=LogProcessor) for _ in range(5)] + for mock in mocks: + multi_log_processor.add_log_processor(mock) + record = self.make_record() + multi_log_processor.emit(record) + for mock in mocks: + mock.emit.assert_called_with(record) + multi_log_processor.shutdown() + + def test_on_shutdown(self): + multi_log_processor = self._get_multi_log_processor() + mocks = [Mock(spec=LogProcessor) for _ in range(5)] + for mock in mocks: + multi_log_processor.add_log_processor(mock) + multi_log_processor.shutdown() + for mock in mocks: + mock.shutdown.assert_called_once_with() + + def test_on_force_flush(self): + multi_log_processor = self._get_multi_log_processor() + mocks = [Mock(spec=LogProcessor) for _ in range(5)] + for mock in mocks: + multi_log_processor.add_log_processor(mock) + ret_value = multi_log_processor.force_flush(100) + + self.assertTrue(ret_value) + for mock_processor in mocks: + self.assertEqual(1, mock_processor.force_flush.call_count) + + +class TestSynchronousMultiLogProcessor( + MultiLogProcessorTestBase, unittest.TestCase +): + def _get_multi_log_processor(self): + return SynchronousMultiLogProcessor() + + def test_force_flush_delayed(self): + multi_log_processor = SynchronousMultiLogProcessor() + + def delay(_): + time.sleep(0.09) + + mock_processor1 = Mock(spec=LogProcessor) + mock_processor1.force_flush = Mock(side_effect=delay) + multi_log_processor.add_log_processor(mock_processor1) + mock_processor2 = Mock(spec=LogProcessor) + multi_log_processor.add_log_processor(mock_processor2) + + ret_value = multi_log_processor.force_flush(50) + self.assertFalse(ret_value) + self.assertEqual(mock_processor1.force_flush.call_count, 1) + self.assertEqual(mock_processor2.force_flush.call_count, 0) + + +class TestConcurrentMultiLogProcessor( + MultiLogProcessorTestBase, unittest.TestCase +): + def _get_multi_log_processor(self): + return ConcurrentMultiLogProcessor() + + def test_force_flush_delayed(self): + multi_log_processor = ConcurrentMultiLogProcessor() + wait_event = threading.Event() + + def delay(_): + wait_event.wait() + + mock1 = Mock(spec=LogProcessor) + mock1.force_flush = Mock(side_effect=delay) + mocks = [Mock(LogProcessor) for _ in range(5)] + mocks = [mock1] + mocks + for mock_processor in mocks: + multi_log_processor.add_log_processor(mock_processor) + + ret_value = multi_log_processor.force_flush(50) + wait_event.set() + + self.assertFalse(ret_value) + for mock in mocks: + self.assertEqual(1, mock.force_flush.call_count) + multi_log_processor.shutdown() From c7c249fe05a57fd15f888722b9d663021dd263bb Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 3 Nov 2021 15:23:57 -0700 Subject: [PATCH 1034/1517] Revert "Add logging signal to main (#2251)" (#2260) --- CHANGELOG.md | 8 - docs/examples/logs/README.rst | 75 --- docs/examples/logs/example.py | 62 --- docs/examples/logs/otel-collector-config.yaml | 10 - docs/sdk/logs.export.rst | 7 - docs/sdk/logs.rst | 22 - docs/sdk/logs.severity.rst | 7 - docs/sdk/sdk.rst | 1 - .../otlp/proto/grpc/_log_exporter/__init__.py | 186 ------- .../tests/logs/__init__.py | 0 .../tests/logs/test_otlp_logs_exporter.py | 474 ----------------- opentelemetry-sdk/setup.cfg | 2 - .../src/opentelemetry/sdk/_logs/__init__.py | 500 ------------------ .../sdk/_logs/export/__init__.py | 301 ----------- .../_logs/export/in_memory_log_exporter.py | 51 -- .../src/opentelemetry/sdk/_logs/severity.py | 115 ---- .../sdk/environment_variables.py | 9 - opentelemetry-sdk/tests/logs/__init__.py | 13 - opentelemetry-sdk/tests/logs/test_export.py | 322 ----------- .../tests/logs/test_global_provider.py | 75 --- opentelemetry-sdk/tests/logs/test_handler.py | 96 ---- .../tests/logs/test_multi_log_prcessor.py | 194 ------- 22 files changed, 2530 deletions(-) delete mode 100644 docs/examples/logs/README.rst delete mode 100644 docs/examples/logs/example.py delete mode 100644 docs/examples/logs/otel-collector-config.yaml delete mode 100644 docs/sdk/logs.export.rst delete mode 100644 docs/sdk/logs.rst delete mode 100644 docs/sdk/logs.severity.rst delete mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py delete mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/__init__.py delete mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/in_memory_log_exporter.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py delete mode 100644 opentelemetry-sdk/tests/logs/__init__.py delete mode 100644 opentelemetry-sdk/tests/logs/test_export.py delete mode 100644 opentelemetry-sdk/tests/logs/test_global_provider.py delete mode 100644 opentelemetry-sdk/tests/logs/test_handler.py delete mode 100644 opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f4c305ae7..ad92cf02b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,14 +115,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added dropped count to otlp, jaeger and zipkin exporters. ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) -### Added -- Give OTLPHandler the ability to process attributes - ([#1952](https://github.com/open-telemetry/opentelemetry-python/pull/1952)) -- Add global LogEmitterProvider and convenience function get_log_emitter - ([#1901](https://github.com/open-telemetry/opentelemetry-python/pull/1901)) -- Add OTLPHandler for standard library logging module - ([#1903](https://github.com/open-telemetry/opentelemetry-python/pull/1903)) - ### Changed - Updated `opentelemetry-opencensus-exporter` to use `service_name` of spans instead of resource ([#1897](https://github.com/open-telemetry/opentelemetry-python/pull/1897)) diff --git a/docs/examples/logs/README.rst b/docs/examples/logs/README.rst deleted file mode 100644 index 3c19c2eafe..0000000000 --- a/docs/examples/logs/README.rst +++ /dev/null @@ -1,75 +0,0 @@ -OpenTelemetry Logs SDK -====================== - -Start the Collector locally to see data being exported. Write the following file: - -.. code-block:: yaml - - # otel-collector-config.yaml - receivers: - otlp: - protocols: - grpc: - - exporters: - logging: - - processors: - batch: - -Then start the Docker container: - -.. code-block:: sh - - docker run \ - -p 4317:4317 \ - -v $(pwd)/otel-collector-config.yaml:/etc/otel/config.yaml \ - otel/opentelemetry-collector-contrib:latest - -.. code-block:: sh - - $ python example.py - -The resulting logs will appear in the output from the collector and look similar to this: - -.. code-block:: sh - - ResourceLog #0 - Resource labels: - -> telemetry.sdk.language: STRING(python) - -> telemetry.sdk.name: STRING(opentelemetry) - -> telemetry.sdk.version: STRING(1.5.0.dev0) - -> service.name: STRING(unknown_service) - InstrumentationLibraryLogs #0 - InstrumentationLibrary __main__ 0.1 - LogRecord #0 - Timestamp: 2021-08-18 08:26:53.837349888 +0000 UTC - Severity: ERROR - ShortName: - Body: Exception while exporting logs. - ResourceLog #1 - Resource labels: - -> telemetry.sdk.language: STRING(python) - -> telemetry.sdk.name: STRING(opentelemetry) - -> telemetry.sdk.version: STRING(1.5.0.dev0) - -> service.name: STRING(unknown_service) - InstrumentationLibraryLogs #0 - InstrumentationLibrary __main__ 0.1 - LogRecord #0 - Timestamp: 2021-08-18 08:26:53.842546944 +0000 UTC - Severity: ERROR - ShortName: - Body: The five boxing wizards jump quickly. - ResourceLog #2 - Resource labels: - -> telemetry.sdk.language: STRING(python) - -> telemetry.sdk.name: STRING(opentelemetry) - -> telemetry.sdk.version: STRING(1.5.0.dev0) - -> service.name: STRING(unknown_service) - InstrumentationLibraryLogs #0 - InstrumentationLibrary __main__ 0.1 - LogRecord #0 - Timestamp: 2021-08-18 08:26:53.843979008 +0000 UTC - Severity: ERROR - ShortName: - Body: Hyderabad, we have a major problem. \ No newline at end of file diff --git a/docs/examples/logs/example.py b/docs/examples/logs/example.py deleted file mode 100644 index b34d9a88cc..0000000000 --- a/docs/examples/logs/example.py +++ /dev/null @@ -1,62 +0,0 @@ -import logging - -from opentelemetry import trace -from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( - OTLPLogExporter, -) -from opentelemetry.sdk._logs import ( - LogEmitterProvider, - OTLPHandler, - set_log_emitter_provider, -) -from opentelemetry.sdk._logs.export import BatchLogProcessor -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - BatchSpanProcessor, - ConsoleSpanExporter, -) - -trace.set_tracer_provider(TracerProvider()) -trace.get_tracer_provider().add_span_processor( - BatchSpanProcessor(ConsoleSpanExporter()) -) - -log_emitter_provider = LogEmitterProvider( - resource=Resource.create( - { - "service.name": "shoppingcart", - "service.instance.id": "instance-12", - } - ), -) -set_log_emitter_provider(log_emitter_provider) - -exporter = OTLPLogExporter(insecure=True) -log_emitter_provider.add_log_processor(BatchLogProcessor(exporter)) -log_emitter = log_emitter_provider.get_log_emitter(__name__, "0.1") -handler = OTLPHandler(level=logging.NOTSET, log_emitter=log_emitter) - -# Attach OTLP handler to root logger -logging.getLogger("root").addHandler(handler) - -# Log directly -logging.info("Jackdaws love my big sphinx of quartz.") - -# Create different namespaced loggers -logger1 = logging.getLogger("myapp.area1") -logger2 = logging.getLogger("myapp.area2") - -logger1.debug("Quick zephyrs blow, vexing daft Jim.") -logger1.info("How quickly daft jumping zebras vex.") -logger2.warning("Jail zesty vixen who grabbed pay from quack.") -logger2.error("The five boxing wizards jump quickly.") - - -# Trace context correlation -tracer = trace.get_tracer(__name__) -with tracer.start_as_current_span("foo"): - # Do something - logger2.error("Hyderabad, we have a major problem.") - -log_emitter_provider.shutdown() diff --git a/docs/examples/logs/otel-collector-config.yaml b/docs/examples/logs/otel-collector-config.yaml deleted file mode 100644 index f29ce6476c..0000000000 --- a/docs/examples/logs/otel-collector-config.yaml +++ /dev/null @@ -1,10 +0,0 @@ -receivers: - otlp: - protocols: - grpc: - -exporters: - logging: - -processors: - batch: diff --git a/docs/sdk/logs.export.rst b/docs/sdk/logs.export.rst deleted file mode 100644 index 19a4023742..0000000000 --- a/docs/sdk/logs.export.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.sdk._logs.export -============================== - -.. automodule:: opentelemetry.sdk._logs.export - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/sdk/logs.rst b/docs/sdk/logs.rst deleted file mode 100644 index 6d9f3c2548..0000000000 --- a/docs/sdk/logs.rst +++ /dev/null @@ -1,22 +0,0 @@ -opentelemetry.sdk._logs package -=============================== - -.. warning:: - OpenTelemetry Python logs are in an experimental state. The APIs within - :mod:`opentelemetry.sdk._logs` are subject to change in minor/patch releases and make no - backward compatability guarantees at this time. - - Once logs become stable, this package will be be renamed to ``opentelemetry.sdk.logs``. - -Submodules ----------- - -.. toctree:: - - logs.export - logs.severity - -.. automodule:: opentelemetry.sdk._logs - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/sdk/logs.severity.rst b/docs/sdk/logs.severity.rst deleted file mode 100644 index 1197e8b44e..0000000000 --- a/docs/sdk/logs.severity.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.sdk._logs.severity -================================ - -.. automodule:: opentelemetry.sdk._logs.severity - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/sdk/sdk.rst b/docs/sdk/sdk.rst index 619f3bd8cc..333da1820b 100644 --- a/docs/sdk/sdk.rst +++ b/docs/sdk/sdk.rst @@ -8,6 +8,5 @@ OpenTelemetry Python SDK resources trace - logs error_handler environment_variables diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py deleted file mode 100644 index 211655d93a..0000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright The OpenTelemetry Authors -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Optional, Sequence -from grpc import ChannelCredentials, Compression -from opentelemetry.exporter.otlp.proto.grpc.exporter import ( - OTLPExporterMixin, - _translate_key_values, - get_resource_data, - _translate_value, -) -from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( - ExportLogsServiceRequest, -) -from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( - LogsServiceStub, -) -from opentelemetry.proto.common.v1.common_pb2 import InstrumentationLibrary -from opentelemetry.proto.logs.v1.logs_pb2 import ( - InstrumentationLibraryLogs, - ResourceLogs, -) -from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord -from opentelemetry.sdk._logs import LogRecord as SDKLogRecord -from opentelemetry.sdk._logs import LogData -from opentelemetry.sdk._logs.export import LogExporter, LogExportResult - - -class OTLPLogExporter( - LogExporter, - OTLPExporterMixin[SDKLogRecord, ExportLogsServiceRequest, LogExportResult], -): - - _result = LogExportResult - _stub = LogsServiceStub - - def __init__( - self, - endpoint: Optional[str] = None, - insecure: Optional[bool] = None, - credentials: Optional[ChannelCredentials] = None, - headers: Optional[Sequence] = None, - timeout: Optional[int] = None, - compression: Optional[Compression] = None, - ): - super().__init__( - **{ - "endpoint": endpoint, - "insecure": insecure, - "credentials": credentials, - "headers": headers, - "timeout": timeout, - "compression": compression, - } - ) - - def _translate_name(self, log_data: LogData) -> None: - self._collector_log_kwargs["name"] = log_data.log_record.name - - def _translate_time(self, log_data: LogData) -> None: - self._collector_log_kwargs[ - "time_unix_nano" - ] = log_data.log_record.timestamp - - def _translate_span_id(self, log_data: LogData) -> None: - self._collector_log_kwargs[ - "span_id" - ] = log_data.log_record.span_id.to_bytes(8, "big") - - def _translate_trace_id(self, log_data: LogData) -> None: - self._collector_log_kwargs[ - "trace_id" - ] = log_data.log_record.trace_id.to_bytes(16, "big") - - def _translate_trace_flags(self, log_data: LogData) -> None: - self._collector_log_kwargs["flags"] = int( - log_data.log_record.trace_flags - ) - - def _translate_body(self, log_data: LogData): - self._collector_log_kwargs["body"] = _translate_value( - log_data.log_record.body - ) - - def _translate_severity_text(self, log_data: LogData): - self._collector_log_kwargs[ - "severity_text" - ] = log_data.log_record.severity_text - - def _translate_attributes(self, log_data: LogData) -> None: - if log_data.log_record.attributes: - self._collector_log_kwargs["attributes"] = [] - for key, value in log_data.log_record.attributes.items(): - try: - self._collector_log_kwargs["attributes"].append( - _translate_key_values(key, value) - ) - except Exception: # pylint: disable=broad-except - pass - - def _translate_data( - self, data: Sequence[LogData] - ) -> ExportLogsServiceRequest: - # pylint: disable=attribute-defined-outside-init - - sdk_resource_instrumentation_library_logs = {} - - for log_data in data: - resource = log_data.log_record.resource - - instrumentation_library_logs_map = ( - sdk_resource_instrumentation_library_logs.get(resource, {}) - ) - if not instrumentation_library_logs_map: - sdk_resource_instrumentation_library_logs[ - resource - ] = instrumentation_library_logs_map - - instrumentation_library_logs = ( - instrumentation_library_logs_map.get( - log_data.instrumentation_info - ) - ) - if not instrumentation_library_logs: - if log_data.instrumentation_info is not None: - instrumentation_library_logs_map[ - log_data.instrumentation_info - ] = InstrumentationLibraryLogs( - instrumentation_library=InstrumentationLibrary( - name=log_data.instrumentation_info.name, - version=log_data.instrumentation_info.version, - ) - ) - else: - instrumentation_library_logs_map[ - log_data.instrumentation_info - ] = InstrumentationLibraryLogs() - - instrumentation_library_logs = ( - instrumentation_library_logs_map.get( - log_data.instrumentation_info - ) - ) - - self._collector_log_kwargs = {} - - self._translate_name(log_data) - self._translate_time(log_data) - self._translate_span_id(log_data) - self._translate_trace_id(log_data) - self._translate_trace_flags(log_data) - self._translate_body(log_data) - self._translate_severity_text(log_data) - self._translate_attributes(log_data) - - self._collector_log_kwargs[ - "severity_number" - ] = log_data.log_record.severity_number.value - - instrumentation_library_logs.logs.append( - PB2LogRecord(**self._collector_log_kwargs) - ) - - return ExportLogsServiceRequest( - resource_logs=get_resource_data( - sdk_resource_instrumentation_library_logs, - ResourceLogs, - "logs", - ) - ) - - def export(self, batch: Sequence[LogData]) -> LogExportResult: - return self._export(batch) - - def shutdown(self) -> None: - pass diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py deleted file mode 100644 index b9c33786e3..0000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ /dev/null @@ -1,474 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time -from concurrent.futures import ThreadPoolExecutor -from unittest import TestCase -from unittest.mock import patch - -from google.protobuf.duration_pb2 import Duration -from google.rpc.error_details_pb2 import RetryInfo -from grpc import StatusCode, server - -from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( - OTLPLogExporter, -) -from opentelemetry.exporter.otlp.proto.grpc.exporter import _translate_value -from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( - ExportLogsServiceRequest, - ExportLogsServiceResponse, -) -from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( - LogsServiceServicer, - add_LogsServiceServicer_to_server, -) -from opentelemetry.proto.common.v1.common_pb2 import ( - AnyValue, - InstrumentationLibrary, - KeyValue, -) -from opentelemetry.proto.logs.v1.logs_pb2 import InstrumentationLibraryLogs -from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord -from opentelemetry.proto.logs.v1.logs_pb2 import ResourceLogs -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as OTLPResource, -) -from opentelemetry.sdk._logs import LogData, LogRecord -from opentelemetry.sdk._logs.export import LogExportResult -from opentelemetry.sdk._logs.severity import ( - SeverityNumber as SDKSeverityNumber, -) -from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -from opentelemetry.trace import TraceFlags - - -class LogsServiceServicerUNAVAILABLEDelay(LogsServiceServicer): - # pylint: disable=invalid-name,unused-argument,no-self-use - def Export(self, request, context): - context.set_code(StatusCode.UNAVAILABLE) - - context.send_initial_metadata( - (("google.rpc.retryinfo-bin", RetryInfo().SerializeToString()),) - ) - context.set_trailing_metadata( - ( - ( - "google.rpc.retryinfo-bin", - RetryInfo( - retry_delay=Duration(seconds=4) - ).SerializeToString(), - ), - ) - ) - - return ExportLogsServiceResponse() - - -class LogsServiceServicerUNAVAILABLE(LogsServiceServicer): - # pylint: disable=invalid-name,unused-argument,no-self-use - def Export(self, request, context): - context.set_code(StatusCode.UNAVAILABLE) - - return ExportLogsServiceResponse() - - -class LogsServiceServicerSUCCESS(LogsServiceServicer): - # pylint: disable=invalid-name,unused-argument,no-self-use - def Export(self, request, context): - context.set_code(StatusCode.OK) - - return ExportLogsServiceResponse() - - -class LogsServiceServicerALREADY_EXISTS(LogsServiceServicer): - # pylint: disable=invalid-name,unused-argument,no-self-use - def Export(self, request, context): - context.set_code(StatusCode.ALREADY_EXISTS) - - return ExportLogsServiceResponse() - - -class TestOTLPLogExporter(TestCase): - def setUp(self): - - self.exporter = OTLPLogExporter() - - self.server = server(ThreadPoolExecutor(max_workers=10)) - - self.server.add_insecure_port("[::]:4317") - - self.server.start() - - self.log_data_1 = LogData( - log_record=LogRecord( - timestamp=int(time.time() * 1e9), - trace_id=2604504634922341076776623263868986797, - span_id=5213367945872657620, - trace_flags=TraceFlags(0x01), - severity_text="WARNING", - severity_number=SDKSeverityNumber.WARN, - name="name", - body="Zhengzhou, We have a heaviest rains in 1000 years", - resource=SDKResource({"key": "value"}), - attributes={"a": 1, "b": "c"}, - ), - instrumentation_info=InstrumentationInfo( - "first_name", "first_version" - ), - ) - self.log_data_2 = LogData( - log_record=LogRecord( - timestamp=int(time.time() * 1e9), - trace_id=2604504634922341076776623263868986799, - span_id=5213367945872657623, - trace_flags=TraceFlags(0x01), - severity_text="INFO", - severity_number=SDKSeverityNumber.INFO2, - name="info name", - body="Sydney, Opera House is closed", - resource=SDKResource({"key": "value"}), - attributes={"custom_attr": [1, 2, 3]}, - ), - instrumentation_info=InstrumentationInfo( - "second_name", "second_version" - ), - ) - self.log_data_3 = LogData( - log_record=LogRecord( - timestamp=int(time.time() * 1e9), - trace_id=2604504634922341076776623263868986800, - span_id=5213367945872657628, - trace_flags=TraceFlags(0x01), - severity_text="ERROR", - severity_number=SDKSeverityNumber.WARN, - name="error name", - body="Mumbai, Boil water before drinking", - resource=SDKResource({"service": "myapp"}), - ), - instrumentation_info=InstrumentationInfo( - "third_name", "third_version" - ), - ) - - def tearDown(self): - self.server.stop(None) - - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" - ) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") - @patch( - "opentelemetry.exporter.otlp.proto.grpc._log_exporter.OTLPLogExporter._stub" - ) - # pylint: disable=unused-argument - def test_no_credentials_error( - self, mock_ssl_channel, mock_secure, mock_stub - ): - OTLPLogExporter(insecure=False) - self.assertTrue(mock_ssl_channel.called) - - # pylint: disable=no-self-use - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") - def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): - expected_endpoint = "localhost:4317" - endpoints = [ - ( - "http://localhost:4317", - None, - mock_insecure, - ), - ( - "localhost:4317", - None, - mock_insecure, - ), - ( - "localhost:4317", - False, - mock_secure, - ), - ( - "https://localhost:4317", - None, - mock_secure, - ), - ( - "https://localhost:4317", - True, - mock_insecure, - ), - ] - # pylint: disable=C0209 - for endpoint, insecure, mock_method in endpoints: - OTLPLogExporter(endpoint=endpoint, insecure=insecure) - self.assertEqual( - 1, - mock_method.call_count, - "expected {} to be called for {} {}".format( - mock_method, endpoint, insecure - ), - ) - self.assertEqual( - expected_endpoint, - mock_method.call_args[0][0], - "expected {} got {} {}".format( - expected_endpoint, mock_method.call_args[0][0], endpoint - ), - ) - mock_method.reset_mock() - - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") - def test_unavailable(self, mock_sleep, mock_expo): - - mock_expo.configure_mock(**{"return_value": [1]}) - - add_LogsServiceServicer_to_server( - LogsServiceServicerUNAVAILABLE(), self.server - ) - self.assertEqual( - self.exporter.export([self.log_data_1]), LogExportResult.FAILURE - ) - mock_sleep.assert_called_with(1) - - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") - def test_unavailable_delay(self, mock_sleep, mock_expo): - - mock_expo.configure_mock(**{"return_value": [1]}) - - add_LogsServiceServicer_to_server( - LogsServiceServicerUNAVAILABLEDelay(), self.server - ) - self.assertEqual( - self.exporter.export([self.log_data_1]), LogExportResult.FAILURE - ) - mock_sleep.assert_called_with(4) - - def test_success(self): - add_LogsServiceServicer_to_server( - LogsServiceServicerSUCCESS(), self.server - ) - self.assertEqual( - self.exporter.export([self.log_data_1]), LogExportResult.SUCCESS - ) - - def test_failure(self): - add_LogsServiceServicer_to_server( - LogsServiceServicerALREADY_EXISTS(), self.server - ) - self.assertEqual( - self.exporter.export([self.log_data_1]), LogExportResult.FAILURE - ) - - def test_translate_log_data(self): - - expected = ExportLogsServiceRequest( - resource_logs=[ - ResourceLogs( - resource=OTLPResource( - attributes=[ - KeyValue( - key="key", value=AnyValue(string_value="value") - ), - ] - ), - instrumentation_library_logs=[ - InstrumentationLibraryLogs( - instrumentation_library=InstrumentationLibrary( - name="first_name", version="first_version" - ), - logs=[ - PB2LogRecord( - # pylint: disable=no-member - name="name", - time_unix_nano=self.log_data_1.log_record.timestamp, - severity_number=self.log_data_1.log_record.severity_number.value, - severity_text="WARNING", - span_id=int.to_bytes( - 5213367945872657620, 8, "big" - ), - trace_id=int.to_bytes( - 2604504634922341076776623263868986797, - 16, - "big", - ), - body=_translate_value( - "Zhengzhou, We have a heaviest rains in 1000 years" - ), - attributes=[ - KeyValue( - key="a", - value=AnyValue(int_value=1), - ), - KeyValue( - key="b", - value=AnyValue(string_value="c"), - ), - ], - flags=int( - self.log_data_1.log_record.trace_flags - ), - ) - ], - ) - ], - ), - ] - ) - - # pylint: disable=protected-access - self.assertEqual( - expected, self.exporter._translate_data([self.log_data_1]) - ) - - def test_translate_multiple_logs(self): - expected = ExportLogsServiceRequest( - resource_logs=[ - ResourceLogs( - resource=OTLPResource( - attributes=[ - KeyValue( - key="key", value=AnyValue(string_value="value") - ), - ] - ), - instrumentation_library_logs=[ - InstrumentationLibraryLogs( - instrumentation_library=InstrumentationLibrary( - name="first_name", version="first_version" - ), - logs=[ - PB2LogRecord( - # pylint: disable=no-member - name="name", - time_unix_nano=self.log_data_1.log_record.timestamp, - severity_number=self.log_data_1.log_record.severity_number.value, - severity_text="WARNING", - span_id=int.to_bytes( - 5213367945872657620, 8, "big" - ), - trace_id=int.to_bytes( - 2604504634922341076776623263868986797, - 16, - "big", - ), - body=_translate_value( - "Zhengzhou, We have a heaviest rains in 1000 years" - ), - attributes=[ - KeyValue( - key="a", - value=AnyValue(int_value=1), - ), - KeyValue( - key="b", - value=AnyValue(string_value="c"), - ), - ], - flags=int( - self.log_data_1.log_record.trace_flags - ), - ) - ], - ), - InstrumentationLibraryLogs( - instrumentation_library=InstrumentationLibrary( - name="second_name", version="second_version" - ), - logs=[ - PB2LogRecord( - # pylint: disable=no-member - name="info name", - time_unix_nano=self.log_data_2.log_record.timestamp, - severity_number=self.log_data_2.log_record.severity_number.value, - severity_text="INFO", - span_id=int.to_bytes( - 5213367945872657623, 8, "big" - ), - trace_id=int.to_bytes( - 2604504634922341076776623263868986799, - 16, - "big", - ), - body=_translate_value( - "Sydney, Opera House is closed" - ), - attributes=[ - KeyValue( - key="custom_attr", - value=_translate_value([1, 2, 3]), - ), - ], - flags=int( - self.log_data_2.log_record.trace_flags - ), - ) - ], - ), - ], - ), - ResourceLogs( - resource=OTLPResource( - attributes=[ - KeyValue( - key="service", - value=AnyValue(string_value="myapp"), - ), - ] - ), - instrumentation_library_logs=[ - InstrumentationLibraryLogs( - instrumentation_library=InstrumentationLibrary( - name="third_name", version="third_version" - ), - logs=[ - PB2LogRecord( - # pylint: disable=no-member - name="error name", - time_unix_nano=self.log_data_3.log_record.timestamp, - severity_number=self.log_data_3.log_record.severity_number.value, - severity_text="ERROR", - span_id=int.to_bytes( - 5213367945872657628, 8, "big" - ), - trace_id=int.to_bytes( - 2604504634922341076776623263868986800, - 16, - "big", - ), - body=_translate_value( - "Mumbai, Boil water before drinking" - ), - attributes=[], - flags=int( - self.log_data_3.log_record.trace_flags - ), - ) - ], - ) - ], - ), - ] - ) - - # pylint: disable=protected-access - self.assertEqual( - expected, - self.exporter._translate_data( - [self.log_data_1, self.log_data_2, self.log_data_3] - ), - ) diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 158c8a198c..1996249573 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -54,8 +54,6 @@ opentelemetry_tracer_provider = sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider opentelemetry_traces_exporter = console = opentelemetry.sdk.trace.export:ConsoleSpanExporter -opentelemetry_log_emitter_provider = - sdk_log_emitter_provider = opentelemetry.sdk._logs:LogEmitterProvider opentelemetry_id_generator = random = opentelemetry.sdk.trace.id_generator:RandomIdGenerator opentelemetry_environment_variables = diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py deleted file mode 100644 index 6da162ed0f..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ /dev/null @@ -1,500 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import abc -import atexit -import concurrent.futures -import json -import logging -import os -import threading -from typing import Any, Callable, Optional, Tuple, Union, cast - -from opentelemetry.sdk._logs.severity import SeverityNumber, std_to_otlp -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_LOG_EMITTER_PROVIDER, -) -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util import ns_to_iso_str -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -from opentelemetry.trace import ( - format_span_id, - format_trace_id, - get_current_span, -) -from opentelemetry.trace.span import TraceFlags -from opentelemetry.util._providers import _load_provider -from opentelemetry.util._time import _time_ns -from opentelemetry.util.types import Attributes - -_logger = logging.getLogger(__name__) - - -class LogRecord: - """A LogRecord instance represents an event being logged. - - LogRecord instances are created and emitted via `LogEmitter` - every time something is logged. They contain all the information - pertinent to the event being logged. - """ - - def __init__( - self, - timestamp: Optional[int] = None, - trace_id: Optional[int] = None, - span_id: Optional[int] = None, - trace_flags: Optional[TraceFlags] = None, - severity_text: Optional[str] = None, - severity_number: Optional[SeverityNumber] = None, - name: Optional[str] = None, - body: Optional[Any] = None, - resource: Optional[Resource] = None, - attributes: Optional[Attributes] = None, - ): - self.timestamp = timestamp - self.trace_id = trace_id - self.span_id = span_id - self.trace_flags = trace_flags - self.severity_text = severity_text - self.severity_number = severity_number - self.name = name - self.body = body - self.resource = resource - self.attributes = attributes - - def __eq__(self, other: object) -> bool: - if not isinstance(other, LogRecord): - return NotImplemented - return self.__dict__ == other.__dict__ - - def to_json(self) -> str: - return json.dumps( - { - "body": self.body, - "name": self.name, - "severity_number": repr(self.severity_number), - "severity_text": self.severity_text, - "attributes": self.attributes, - "timestamp": ns_to_iso_str(self.timestamp), - "trace_id": f"0x{format_trace_id(self.trace_id)}", - "span_id": f"0x{format_span_id(self.span_id)}", - "trace_flags": self.trace_flags, - "resource": repr(self.resource.attributes) - if self.resource - else "", - }, - indent=4, - ) - - -class LogData: - """Readable LogRecord data plus associated InstrumentationLibrary.""" - - def __init__( - self, - log_record: LogRecord, - instrumentation_info: InstrumentationInfo, - ): - self.log_record = log_record - self.instrumentation_info = instrumentation_info - - -class LogProcessor(abc.ABC): - """Interface to hook the log record emitting action. - - Log processors can be registered directly using - :func:`LogEmitterProvider.add_log_processor` and they are invoked - in the same order as they were registered. - """ - - @abc.abstractmethod - def emit(self, log_data: LogData): - """Emits the `LogData`""" - - @abc.abstractmethod - def shutdown(self): - """Called when a :class:`opentelemetry.sdk._logs.LogEmitter` is shutdown""" - - @abc.abstractmethod - def force_flush(self, timeout_millis: int = 30000): - """Export all the received logs to the configured Exporter that have not yet - been exported. - - Args: - timeout_millis: The maximum amount of time to wait for logs to be - exported. - - Returns: - False if the timeout is exceeded, True otherwise. - """ - - -# Temporary fix until https://github.com/PyCQA/pylint/issues/4098 is resolved -# pylint:disable=no-member -class SynchronousMultiLogProcessor(LogProcessor): - """Implementation of class:`LogProcessor` that forwards all received - events to a list of log processors sequentially. - - The underlying log processors are called in sequential order as they were - added. - """ - - def __init__(self): - # use a tuple to avoid race conditions when adding a new log and - # iterating through it on "emit". - self._log_processors = () # type: Tuple[LogProcessor, ...] - self._lock = threading.Lock() - - def add_log_processor(self, log_processor: LogProcessor) -> None: - """Adds a Logprocessor to the list of log processors handled by this instance""" - with self._lock: - self._log_processors = self._log_processors + (log_processor,) - - def emit(self, log_data: LogData) -> None: - for lp in self._log_processors: - lp.emit(log_data) - - def shutdown(self) -> None: - """Shutdown the log processors one by one""" - for lp in self._log_processors: - lp.shutdown() - - def force_flush(self, timeout_millis: int = 30000) -> bool: - """Force flush the log processors one by one - - Args: - timeout_millis: The maximum amount of time to wait for logs to be - exported. If the first n log processors exceeded the timeout - then remaining log processors will not be flushed. - - Returns: - True if all the log processors flushes the logs within timeout, - False otherwise. - """ - deadline_ns = _time_ns() + timeout_millis * 1000000 - for lp in self._log_processors: - current_ts = _time_ns() - if current_ts >= deadline_ns: - return False - - if not lp.force_flush((deadline_ns - current_ts) // 1000000): - return False - - return True - - -class ConcurrentMultiLogProcessor(LogProcessor): - """Implementation of :class:`LogProcessor` that forwards all received - events to a list of log processors in parallel. - - Calls to the underlying log processors are forwarded in parallel by - submitting them to a thread pool executor and waiting until each log - processor finished its work. - - Args: - max_workers: The number of threads managed by the thread pool executor - and thus defining how many log processors can work in parallel. - """ - - def __init__(self, max_workers: int = 2): - # use a tuple to avoid race conditions when adding a new log and - # iterating through it on "emit". - self._log_processors = () # type: Tuple[LogProcessor, ...] - self._lock = threading.Lock() - self._executor = concurrent.futures.ThreadPoolExecutor( - max_workers=max_workers - ) - - def add_log_processor(self, log_processor: LogProcessor): - with self._lock: - self._log_processors = self._log_processors + (log_processor,) - - def _submit_and_wait( - self, - func: Callable[[LogProcessor], Callable[..., None]], - *args: Any, - **kwargs: Any, - ): - futures = [] - for lp in self._log_processors: - future = self._executor.submit(func(lp), *args, **kwargs) - futures.append(future) - for future in futures: - future.result() - - def emit(self, log_data: LogData): - self._submit_and_wait(lambda lp: lp.emit, log_data) - - def shutdown(self): - self._submit_and_wait(lambda lp: lp.shutdown) - - def force_flush(self, timeout_millis: int = 30000) -> bool: - """Force flush the log processors in parallel. - - Args: - timeout_millis: The maximum amount of time to wait for logs to be - exported. - - Returns: - True if all the log processors flushes the logs within timeout, - False otherwise. - """ - futures = [] - for lp in self._log_processors: - future = self._executor.submit(lp.force_flush, timeout_millis) - futures.append(future) - - done_futures, not_done_futures = concurrent.futures.wait( - futures, timeout_millis / 1e3 - ) - - if not_done_futures: - return False - - for future in done_futures: - if not future.result(): - return False - - return True - - -# skip natural LogRecord attributes -# http://docs.python.org/library/logging.html#logrecord-attributes -_RESERVED_ATTRS = frozenset( - ( - "asctime", - "args", - "created", - "exc_info", - "exc_text", - "filename", - "funcName", - "getMessage", - "levelname", - "levelno", - "lineno", - "module", - "msecs", - "msg", - "name", - "pathname", - "process", - "processName", - "relativeCreated", - "stack_info", - "thread", - "threadName", - ) -) - - -class OTLPHandler(logging.Handler): - """A handler class which writes logging records, in OTLP format, to - a network destination or file. - """ - - def __init__( - self, - level=logging.NOTSET, - log_emitter=None, - ) -> None: - super().__init__(level=level) - self._log_emitter = log_emitter or get_log_emitter(__name__) - - @staticmethod - def _get_attributes(record: logging.LogRecord) -> Attributes: - return { - k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS - } - - def _translate(self, record: logging.LogRecord) -> LogRecord: - timestamp = int(record.created * 1e9) - span_context = get_current_span().get_span_context() - attributes = self._get_attributes(record) - severity_number = std_to_otlp(record.levelno) - return LogRecord( - timestamp=timestamp, - trace_id=span_context.trace_id, - span_id=span_context.span_id, - trace_flags=span_context.trace_flags, - severity_text=record.levelname, - severity_number=severity_number, - body=record.getMessage(), - resource=self._log_emitter.resource, - attributes=attributes, - ) - - def emit(self, record: logging.LogRecord) -> None: - """ - Emit a record. - - The record is translated to OTLP format, and then sent across the pipeline. - """ - self._log_emitter.emit(self._translate(record)) - - def flush(self) -> None: - """ - Flushes the logging output. - """ - self._log_emitter.flush() - - -class LogEmitter: - def __init__( - self, - resource: Resource, - multi_log_processor: Union[ - SynchronousMultiLogProcessor, ConcurrentMultiLogProcessor - ], - instrumentation_info: InstrumentationInfo, - ): - self._resource = resource - self._multi_log_processor = multi_log_processor - self._instrumentation_info = instrumentation_info - - @property - def resource(self): - return self._resource - - def emit(self, record: LogRecord): - """Emits the :class:`LogData` by associating :class:`LogRecord` - and instrumentation info. - """ - log_data = LogData(record, self._instrumentation_info) - self._multi_log_processor.emit(log_data) - - # TODO: Should this flush everything in pipeline? - # Prior discussion https://github.com/open-telemetry/opentelemetry-python/pull/1916#discussion_r659945290 - def flush(self): - """Ensure all logging output has been flushed.""" - self._multi_log_processor.force_flush() - - -class LogEmitterProvider: - def __init__( - self, - resource: Resource = Resource.create(), - shutdown_on_exit: bool = True, - multi_log_processor: Union[ - SynchronousMultiLogProcessor, ConcurrentMultiLogProcessor - ] = None, - ): - self._resource = resource - self._multi_log_processor = ( - multi_log_processor or SynchronousMultiLogProcessor() - ) - self._at_exit_handler = None - if shutdown_on_exit: - self._at_exit_handler = atexit.register(self.shutdown) - - @property - def resource(self): - return self._resource - - def get_log_emitter( - self, - instrumenting_module_name: str, - instrumenting_module_verison: str = "", - ) -> LogEmitter: - return LogEmitter( - self._resource, - self._multi_log_processor, - InstrumentationInfo( - instrumenting_module_name, instrumenting_module_verison - ), - ) - - def add_log_processor(self, log_processor: LogProcessor): - """Registers a new :class:`LogProcessor` for this `LogEmitterProvider` instance. - - The log processors are invoked in the same order they are registered. - """ - self._multi_log_processor.add_log_processor(log_processor) - - def shutdown(self): - """Shuts down the log processors.""" - self._multi_log_processor.shutdown() - if self._at_exit_handler is not None: - atexit.unregister(self._at_exit_handler) - self._at_exit_handler = None - - def force_flush(self, timeout_millis: int = 30000) -> bool: - """Force flush the log processors. - - Args: - timeout_millis: The maximum amount of time to wait for logs to be - exported. - - Returns: - True if all the log processors flushes the logs within timeout, - False otherwise. - """ - return self._multi_log_processor.force_flush(timeout_millis) - - -_LOG_EMITTER_PROVIDER = None - - -def get_log_emitter_provider() -> LogEmitterProvider: - """Gets the current global :class:`~.LogEmitterProvider` object.""" - global _LOG_EMITTER_PROVIDER # pylint: disable=global-statement - if _LOG_EMITTER_PROVIDER is None: - if _OTEL_PYTHON_LOG_EMITTER_PROVIDER not in os.environ: - _LOG_EMITTER_PROVIDER = LogEmitterProvider() - return _LOG_EMITTER_PROVIDER - - _LOG_EMITTER_PROVIDER = cast( - "LogEmitterProvider", - _load_provider( - _OTEL_PYTHON_LOG_EMITTER_PROVIDER, "log_emitter_provider" - ), - ) - - return _LOG_EMITTER_PROVIDER - - -def set_log_emitter_provider(log_emitter_provider: LogEmitterProvider) -> None: - """Sets the current global :class:`~.LogEmitterProvider` object. - - This can only be done once, a warning will be logged if any furter attempt - is made. - """ - global _LOG_EMITTER_PROVIDER # pylint: disable=global-statement - - if _LOG_EMITTER_PROVIDER is not None: - _logger.warning( - "Overriding of current LogEmitterProvider is not allowed" - ) - return - - _LOG_EMITTER_PROVIDER = log_emitter_provider - - -def get_log_emitter( - instrumenting_module_name: str, - instrumenting_library_version: str = "", - log_emitter_provider: Optional[LogEmitterProvider] = None, -) -> LogEmitter: - """Returns a `LogEmitter` for use within a python process. - - This function is a convenience wrapper for - opentelemetry.sdk._logs.LogEmitterProvider.get_log_emitter. - - If log_emitter_provider param is omitted the current configured one is used. - """ - if log_emitter_provider is None: - log_emitter_provider = get_log_emitter_provider() - return log_emitter_provider.get_log_emitter( - instrumenting_module_name, instrumenting_library_version - ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py deleted file mode 100644 index f65c967534..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py +++ /dev/null @@ -1,301 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import abc -import collections -import enum -import logging -import sys -import threading -from os import linesep -from typing import IO, Callable, Deque, List, Optional, Sequence - -from opentelemetry.context import attach, detach, set_value -from opentelemetry.sdk._logs import LogData, LogProcessor, LogRecord -from opentelemetry.util._time import _time_ns - -_logger = logging.getLogger(__name__) - - -class LogExportResult(enum.Enum): - SUCCESS = 0 - FAILURE = 1 - - -class LogExporter(abc.ABC): - """Interface for exporting logs. - - Interface to be implemented by services that want to export logs received - in their own format. - - To export data this MUST be registered to the :class`opentelemetry.sdk._logs.LogEmitter` using a - log processor. - """ - - @abc.abstractmethod - def export(self, batch: Sequence[LogData]): - """Exports a batch of logs. - - Args: - batch: The list of `LogData` objects to be exported - - Returns: - The result of the export - """ - - @abc.abstractmethod - def shutdown(self): - """Shuts down the exporter. - - Called when the SDK is shut down. - """ - - -class ConsoleExporter(LogExporter): - """Implementation of :class:`LogExporter` that prints log records to the - console. - - This class can be used for diagnostic purposes. It prints the exported - log records to the console STDOUT. - """ - - def __init__( - self, - out: IO = sys.stdout, - formatter: Callable[[LogRecord], str] = lambda record: record.to_json() - + linesep, - ): - self.out = out - self.formatter = formatter - - def export(self, batch: Sequence[LogData]): - for data in batch: - self.out.write(self.formatter(data.log_record)) - self.out.flush() - return LogExportResult.SUCCESS - - def shutdown(self): - pass - - -class SimpleLogProcessor(LogProcessor): - """This is an implementation of LogProcessor which passes - received logs in the export-friendly LogData representation to the - configured LogExporter, as soon as they are emitted. - """ - - def __init__(self, exporter: LogExporter): - self._exporter = exporter - self._shutdown = False - - def emit(self, log_data: LogData): - if self._shutdown: - _logger.warning("Processor is already shutdown, ignoring call") - return - token = attach(set_value("suppress_instrumentation", True)) - try: - self._exporter.export((log_data,)) - except Exception: # pylint: disable=broad-except - _logger.exception("Exception while exporting logs.") - detach(token) - - def shutdown(self): - self._shutdown = True - self._exporter.shutdown() - - def force_flush( - self, timeout_millis: int = 30000 - ) -> bool: # pylint: disable=no-self-use - return True - - -class _FlushRequest: - __slots__ = ["event", "num_log_records"] - - def __init__(self): - self.event = threading.Event() - self.num_log_records = 0 - - -class BatchLogProcessor(LogProcessor): - """This is an implementation of LogProcessor which creates batches of - received logs in the export-friendly LogData representation and - send to the configured LogExporter, as soon as they are emitted. - """ - - def __init__( - self, - exporter: LogExporter, - schedule_delay_millis: int = 5000, - max_export_batch_size: int = 512, - export_timeout_millis: int = 30000, - ): - self._exporter = exporter - self._schedule_delay_millis = schedule_delay_millis - self._max_export_batch_size = max_export_batch_size - self._export_timeout_millis = export_timeout_millis - self._queue = collections.deque() # type: Deque[LogData] - self._worker_thread = threading.Thread(target=self.worker, daemon=True) - self._condition = threading.Condition(threading.Lock()) - self._shutdown = False - self._flush_request = None # type: Optional[_FlushRequest] - self._log_records = [ - None - ] * self._max_export_batch_size # type: List[Optional[LogData]] - self._worker_thread.start() - - def worker(self): - timeout = self._schedule_delay_millis / 1e3 - flush_request = None # type: Optional[_FlushRequest] - while not self._shutdown: - with self._condition: - if self._shutdown: - # shutdown may have been called, avoid further processing - break - flush_request = self._get_and_unset_flush_request() - if ( - len(self._queue) < self._max_export_batch_size - and self._flush_request is None - ): - self._condition.wait(timeout) - - flush_request = self._get_and_unset_flush_request() - if not self._queue: - timeout = self._schedule_delay_millis / 1e3 - self._notify_flush_request_finished(flush_request) - flush_request = None - continue - if self._shutdown: - break - - start_ns = _time_ns() - self._export(flush_request) - end_ns = _time_ns() - # subtract the duration of this export call to the next timeout - timeout = self._schedule_delay_millis / 1e3 - ( - (end_ns - start_ns) / 1e9 - ) - - self._notify_flush_request_finished(flush_request) - flush_request = None - - # there might have been a new flush request while export was running - # and before the done flag switched to true - with self._condition: - shutdown_flush_request = self._get_and_unset_flush_request() - - # flush the remaining logs - self._drain_queue() - self._notify_flush_request_finished(flush_request) - self._notify_flush_request_finished(shutdown_flush_request) - - def _export(self, flush_request: Optional[_FlushRequest] = None): - """Exports logs considering the given flush_request. - - If flush_request is not None then logs are exported in batches - until the number of exported logs reached or exceeded the num of logs in - flush_request, otherwise exports at max max_export_batch_size logs. - """ - if flush_request is None: - self._export_batch() - return - - num_log_records = flush_request.num_log_records - while self._queue: - exported = self._export_batch() - num_log_records -= exported - - if num_log_records <= 0: - break - - def _export_batch(self) -> int: - """Exports at most max_export_batch_size logs and returns the number of - exported logs. - """ - idx = 0 - while idx < self._max_export_batch_size and self._queue: - record = self._queue.pop() - self._log_records[idx] = record - idx += 1 - token = attach(set_value("suppress_instrumentation", True)) - try: - self._exporter.export(self._log_records[:idx]) # type: ignore - except Exception: # pylint: disable=broad-except - _logger.exception("Exception while exporting logs.") - detach(token) - - for index in range(idx): - self._log_records[index] = None - return idx - - def _drain_queue(self): - """Export all elements until queue is empty. - - Can only be called from the worker thread context because it invokes - `export` that is not thread safe. - """ - while self._queue: - self._export_batch() - - def _get_and_unset_flush_request(self) -> Optional[_FlushRequest]: - flush_request = self._flush_request - self._flush_request = None - if flush_request is not None: - flush_request.num_log_records = len(self._queue) - return flush_request - - @staticmethod - def _notify_flush_request_finished( - flush_request: Optional[_FlushRequest] = None, - ): - if flush_request is not None: - flush_request.event.set() - - def _get_or_create_flush_request(self) -> _FlushRequest: - if self._flush_request is None: - self._flush_request = _FlushRequest() - return self._flush_request - - def emit(self, log_data: LogData) -> None: - """Adds the `LogData` to queue and notifies the waiting threads - when size of queue reaches max_export_batch_size. - """ - if self._shutdown: - return - self._queue.appendleft(log_data) - if len(self._queue) >= self._max_export_batch_size: - with self._condition: - self._condition.notify() - - def shutdown(self): - self._shutdown = True - with self._condition: - self._condition.notify_all() - self._worker_thread.join() - self._exporter.shutdown() - - def force_flush(self, timeout_millis: Optional[int] = None) -> bool: - if timeout_millis is None: - timeout_millis = self._export_timeout_millis - if self._shutdown: - return True - - with self._condition: - flush_request = self._get_or_create_flush_request() - self._condition.notify_all() - - ret = flush_request.event.wait(timeout_millis / 1e3) - if not ret: - _logger.warning("Timeout was exceeded in force_flush().") - return ret diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/in_memory_log_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/in_memory_log_exporter.py deleted file mode 100644 index 68cb6b7389..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/in_memory_log_exporter.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading -import typing - -from opentelemetry.sdk._logs import LogData -from opentelemetry.sdk._logs.export import LogExporter, LogExportResult - - -class InMemoryLogExporter(LogExporter): - """Implementation of :class:`.LogExporter` that stores logs in memory. - - This class can be used for testing purposes. It stores the exported logs - in a list in memory that can be retrieved using the - :func:`.get_finished_logs` method. - """ - - def __init__(self): - self._logs = [] - self._lock = threading.Lock() - self._stopped = False - - def clear(self) -> None: - with self._lock: - self._logs.clear() - - def get_finished_logs(self) -> typing.Tuple[LogData, ...]: - with self._lock: - return tuple(self._logs) - - def export(self, batch: typing.Sequence[LogData]) -> LogExportResult: - if self._stopped: - return LogExportResult.FAILURE - with self._lock: - self._logs.extend(batch) - return LogExportResult.SUCCESS - - def shutdown(self) -> None: - self._stopped = True diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py deleted file mode 100644 index 2570375990..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import enum - - -class SeverityNumber(enum.Enum): - """Numerical value of severity. - - Smaller numerical values correspond to less severe events - (such as debug events), larger numerical values correspond - to more severe events (such as errors and critical events). - - See the `Log Data Model`_ spec for more info and how to map the - severity from source format to OTLP Model. - - .. _Log Data Model: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber - """ - - UNSPECIFIED = 0 - TRACE = 1 - TRACE2 = 2 - TRACE3 = 3 - TRACE4 = 4 - DEBUG = 5 - DEBUG2 = 6 - DEBUG3 = 7 - DEBUG4 = 8 - INFO = 9 - INFO2 = 10 - INFO3 = 11 - INFO4 = 12 - WARN = 13 - WARN2 = 14 - WARN3 = 15 - WARN4 = 16 - ERROR = 17 - ERROR2 = 18 - ERROR3 = 19 - ERROR4 = 20 - FATAL = 21 - FATAL2 = 22 - FATAL3 = 23 - FATAL4 = 24 - - -_STD_TO_OTLP = { - 10: SeverityNumber.DEBUG, - 11: SeverityNumber.DEBUG2, - 12: SeverityNumber.DEBUG3, - 13: SeverityNumber.DEBUG4, - 14: SeverityNumber.DEBUG4, - 15: SeverityNumber.DEBUG4, - 16: SeverityNumber.DEBUG4, - 17: SeverityNumber.DEBUG4, - 18: SeverityNumber.DEBUG4, - 19: SeverityNumber.DEBUG4, - 20: SeverityNumber.INFO, - 21: SeverityNumber.INFO2, - 22: SeverityNumber.INFO3, - 23: SeverityNumber.INFO4, - 24: SeverityNumber.INFO4, - 25: SeverityNumber.INFO4, - 26: SeverityNumber.INFO4, - 27: SeverityNumber.INFO4, - 28: SeverityNumber.INFO4, - 29: SeverityNumber.INFO4, - 30: SeverityNumber.WARN, - 31: SeverityNumber.WARN2, - 32: SeverityNumber.WARN3, - 33: SeverityNumber.WARN4, - 34: SeverityNumber.WARN4, - 35: SeverityNumber.WARN4, - 36: SeverityNumber.WARN4, - 37: SeverityNumber.WARN4, - 38: SeverityNumber.WARN4, - 39: SeverityNumber.WARN4, - 40: SeverityNumber.ERROR, - 41: SeverityNumber.ERROR2, - 42: SeverityNumber.ERROR3, - 43: SeverityNumber.ERROR4, - 44: SeverityNumber.ERROR4, - 45: SeverityNumber.ERROR4, - 46: SeverityNumber.ERROR4, - 47: SeverityNumber.ERROR4, - 48: SeverityNumber.ERROR4, - 49: SeverityNumber.ERROR4, - 50: SeverityNumber.FATAL, - 51: SeverityNumber.FATAL2, - 52: SeverityNumber.FATAL3, - 53: SeverityNumber.FATAL4, -} - - -def std_to_otlp(levelno: int) -> SeverityNumber: - """ - Map python log levelno as defined in https://docs.python.org/3/library/logging.html#logging-levels - to OTLP log severity number. - """ - if levelno < 10: - return SeverityNumber.UNSPECIFIED - if levelno > 53: - return SeverityNumber.FATAL4 - return _STD_TO_OTLP[levelno] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index e9d35092a6..8b3d4abbf8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -369,12 +369,3 @@ If both are set, :envvar:`OTEL_SERVICE_NAME` takes precedence. """ - -_OTEL_PYTHON_LOG_EMITTER_PROVIDER = "OTEL_PYTHON_LOG_EMITTER_PROVIDER" -""" -.. envvar:: OTEL_PYTHON_LOG_EMITTER_PROVIDER - -The :envvar:`OTEL_PYTHON_LOG_EMITTER_PROVIDER` environment variable allows users to -provide the entry point for loading the log emitter provider. If not specified, SDK -LogEmitterProvider is used. -""" diff --git a/opentelemetry-sdk/tests/logs/__init__.py b/opentelemetry-sdk/tests/logs/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/opentelemetry-sdk/tests/logs/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py deleted file mode 100644 index 964b44f769..0000000000 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ /dev/null @@ -1,322 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=protected-access -import logging -import os -import time -import unittest -from concurrent.futures import ThreadPoolExecutor -from unittest.mock import Mock, patch - -from opentelemetry.sdk import trace -from opentelemetry.sdk._logs import ( - LogData, - LogEmitterProvider, - LogRecord, - OTLPHandler, -) -from opentelemetry.sdk._logs.export import ( - BatchLogProcessor, - ConsoleExporter, - SimpleLogProcessor, -) -from opentelemetry.sdk._logs.export.in_memory_log_exporter import ( - InMemoryLogExporter, -) -from opentelemetry.sdk._logs.severity import SeverityNumber -from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -from opentelemetry.trace import TraceFlags -from opentelemetry.trace.span import INVALID_SPAN_CONTEXT - - -class TestSimpleLogProcessor(unittest.TestCase): - def test_simple_log_processor_default_level(self): - exporter = InMemoryLogExporter() - log_emitter_provider = LogEmitterProvider() - log_emitter = log_emitter_provider.get_log_emitter(__name__) - - log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) - - logger = logging.getLogger("default_level") - logger.addHandler(OTLPHandler(log_emitter=log_emitter)) - - logger.warning("Something is wrong") - finished_logs = exporter.get_finished_logs() - self.assertEqual(len(finished_logs), 1) - warning_log_record = finished_logs[0].log_record - self.assertEqual(warning_log_record.body, "Something is wrong") - self.assertEqual(warning_log_record.severity_text, "WARNING") - self.assertEqual( - warning_log_record.severity_number, SeverityNumber.WARN - ) - - def test_simple_log_processor_custom_level(self): - exporter = InMemoryLogExporter() - log_emitter_provider = LogEmitterProvider() - log_emitter = log_emitter_provider.get_log_emitter(__name__) - - log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) - - logger = logging.getLogger("custom_level") - logger.setLevel(logging.ERROR) - logger.addHandler(OTLPHandler(log_emitter=log_emitter)) - - logger.warning("Warning message") - logger.debug("Debug message") - logger.error("Error message") - logger.critical("Critical message") - finished_logs = exporter.get_finished_logs() - # Make sure only level >= logging.CRITICAL logs are recorded - self.assertEqual(len(finished_logs), 2) - critical_log_record = finished_logs[0].log_record - fatal_log_record = finished_logs[1].log_record - self.assertEqual(critical_log_record.body, "Error message") - self.assertEqual(critical_log_record.severity_text, "ERROR") - self.assertEqual( - critical_log_record.severity_number, SeverityNumber.ERROR - ) - self.assertEqual(fatal_log_record.body, "Critical message") - self.assertEqual(fatal_log_record.severity_text, "CRITICAL") - self.assertEqual( - fatal_log_record.severity_number, SeverityNumber.FATAL - ) - - def test_simple_log_processor_trace_correlation(self): - exporter = InMemoryLogExporter() - log_emitter_provider = LogEmitterProvider() - log_emitter = log_emitter_provider.get_log_emitter("name", "version") - - log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) - - logger = logging.getLogger("trace_correlation") - logger.addHandler(OTLPHandler(log_emitter=log_emitter)) - - logger.warning("Warning message") - finished_logs = exporter.get_finished_logs() - self.assertEqual(len(finished_logs), 1) - log_record = finished_logs[0].log_record - self.assertEqual(log_record.body, "Warning message") - self.assertEqual(log_record.severity_text, "WARNING") - self.assertEqual(log_record.severity_number, SeverityNumber.WARN) - self.assertEqual(log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id) - self.assertEqual(log_record.span_id, INVALID_SPAN_CONTEXT.span_id) - self.assertEqual( - log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags - ) - exporter.clear() - - tracer = trace.TracerProvider().get_tracer(__name__) - with tracer.start_as_current_span("test") as span: - logger.critical("Critical message within span") - - finished_logs = exporter.get_finished_logs() - log_record = finished_logs[0].log_record - self.assertEqual(log_record.body, "Critical message within span") - self.assertEqual(log_record.severity_text, "CRITICAL") - self.assertEqual(log_record.severity_number, SeverityNumber.FATAL) - span_context = span.get_span_context() - self.assertEqual(log_record.trace_id, span_context.trace_id) - self.assertEqual(log_record.span_id, span_context.span_id) - self.assertEqual(log_record.trace_flags, span_context.trace_flags) - - def test_simple_log_processor_shutdown(self): - exporter = InMemoryLogExporter() - log_emitter_provider = LogEmitterProvider() - log_emitter = log_emitter_provider.get_log_emitter(__name__) - - log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) - - logger = logging.getLogger("shutdown") - logger.addHandler(OTLPHandler(log_emitter=log_emitter)) - - logger.warning("Something is wrong") - finished_logs = exporter.get_finished_logs() - self.assertEqual(len(finished_logs), 1) - warning_log_record = finished_logs[0].log_record - self.assertEqual(warning_log_record.body, "Something is wrong") - self.assertEqual(warning_log_record.severity_text, "WARNING") - self.assertEqual( - warning_log_record.severity_number, SeverityNumber.WARN - ) - exporter.clear() - log_emitter_provider.shutdown() - logger.warning("Log after shutdown") - finished_logs = exporter.get_finished_logs() - self.assertEqual(len(finished_logs), 0) - - -class TestBatchLogProcessor(unittest.TestCase): - def test_emit_call_log_record(self): - exporter = InMemoryLogExporter() - log_processor = Mock(wraps=BatchLogProcessor(exporter)) - provider = LogEmitterProvider() - provider.add_log_processor(log_processor) - - emitter = provider.get_log_emitter(__name__) - logger = logging.getLogger("emit_call") - logger.addHandler(OTLPHandler(log_emitter=emitter)) - - logger.error("error") - self.assertEqual(log_processor.emit.call_count, 1) - - def test_shutdown(self): - exporter = InMemoryLogExporter() - log_processor = BatchLogProcessor(exporter) - - provider = LogEmitterProvider() - provider.add_log_processor(log_processor) - - emitter = provider.get_log_emitter(__name__) - logger = logging.getLogger("shutdown") - logger.addHandler(OTLPHandler(log_emitter=emitter)) - - logger.warning("warning message: %s", "possible upcoming heatwave") - logger.error("Very high rise in temperatures across the globe") - logger.critical("Temparature hits high 420 C in Hyderabad") - - log_processor.shutdown() - self.assertTrue(exporter._stopped) - - finished_logs = exporter.get_finished_logs() - expected = [ - ("warning message: possible upcoming heatwave", "WARNING"), - ("Very high rise in temperatures across the globe", "ERROR"), - ( - "Temparature hits high 420 C in Hyderabad", - "CRITICAL", - ), - ] - emitted = [ - (item.log_record.body, item.log_record.severity_text) - for item in finished_logs - ] - self.assertEqual(expected, emitted) - - def test_force_flush(self): - exporter = InMemoryLogExporter() - log_processor = BatchLogProcessor(exporter) - - provider = LogEmitterProvider() - provider.add_log_processor(log_processor) - - emitter = provider.get_log_emitter(__name__) - logger = logging.getLogger("force_flush") - logger.addHandler(OTLPHandler(log_emitter=emitter)) - - logger.critical("Earth is burning") - log_processor.force_flush() - finished_logs = exporter.get_finished_logs() - self.assertEqual(len(finished_logs), 1) - log_record = finished_logs[0].log_record - self.assertEqual(log_record.body, "Earth is burning") - self.assertEqual(log_record.severity_number, SeverityNumber.FATAL) - - def test_log_processor_too_many_logs(self): - exporter = InMemoryLogExporter() - log_processor = BatchLogProcessor(exporter) - - provider = LogEmitterProvider() - provider.add_log_processor(log_processor) - - emitter = provider.get_log_emitter(__name__) - logger = logging.getLogger("many_logs") - logger.addHandler(OTLPHandler(log_emitter=emitter)) - - for log_no in range(1000): - logger.critical("Log no: %s", log_no) - - self.assertTrue(log_processor.force_flush()) - finised_logs = exporter.get_finished_logs() - self.assertEqual(len(finised_logs), 1000) - - def test_with_multiple_threads(self): - exporter = InMemoryLogExporter() - log_processor = BatchLogProcessor(exporter) - - provider = LogEmitterProvider() - provider.add_log_processor(log_processor) - - emitter = provider.get_log_emitter(__name__) - logger = logging.getLogger("threads") - logger.addHandler(OTLPHandler(log_emitter=emitter)) - - def bulk_log_and_flush(num_logs): - for _ in range(num_logs): - logger.critical("Critical message") - self.assertTrue(log_processor.force_flush()) - - with ThreadPoolExecutor(max_workers=69) as executor: - futures = [] - for idx in range(69): - future = executor.submit(bulk_log_and_flush, idx + 1) - futures.append(future) - - executor.shutdown() - - finished_logs = exporter.get_finished_logs() - self.assertEqual(len(finished_logs), 2415) - - -class TestConsoleExporter(unittest.TestCase): - def test_export(self): # pylint: disable=no-self-use - """Check that the console exporter prints log records.""" - log_data = LogData( - log_record=LogRecord( - timestamp=int(time.time() * 1e9), - trace_id=2604504634922341076776623263868986797, - span_id=5213367945872657620, - trace_flags=TraceFlags(0x01), - severity_text="WARN", - severity_number=SeverityNumber.WARN, - name="name", - body="Zhengzhou, We have a heaviest rains in 1000 years", - resource=SDKResource({"key": "value"}), - attributes={"a": 1, "b": "c"}, - ), - instrumentation_info=InstrumentationInfo( - "first_name", "first_version" - ), - ) - exporter = ConsoleExporter() - # Mocking stdout interferes with debugging and test reporting, mock on - # the exporter instance instead. - - with patch.object(exporter, "out") as mock_stdout: - exporter.export([log_data]) - mock_stdout.write.assert_called_once_with( - log_data.log_record.to_json() + os.linesep - ) - - self.assertEqual(mock_stdout.write.call_count, 1) - self.assertEqual(mock_stdout.flush.call_count, 1) - - def test_export_custom(self): # pylint: disable=no-self-use - """Check that console exporter uses custom io, formatter.""" - mock_record_str = Mock(str) - - def formatter(record): # pylint: disable=unused-argument - return mock_record_str - - mock_stdout = Mock() - exporter = ConsoleExporter(out=mock_stdout, formatter=formatter) - log_data = LogData( - log_record=LogRecord(), - instrumentation_info=InstrumentationInfo( - "first_name", "first_version" - ), - ) - exporter.export([log_data]) - mock_stdout.write.assert_called_once_with(mock_record_str) diff --git a/opentelemetry-sdk/tests/logs/test_global_provider.py b/opentelemetry-sdk/tests/logs/test_global_provider.py deleted file mode 100644 index 7a249defcf..0000000000 --- a/opentelemetry-sdk/tests/logs/test_global_provider.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# type:ignore -import unittest -from importlib import reload -from logging import WARNING -from unittest.mock import patch - -from opentelemetry.sdk import _logs -from opentelemetry.sdk._logs import ( - LogEmitterProvider, - get_log_emitter_provider, - set_log_emitter_provider, -) -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_LOG_EMITTER_PROVIDER, -) - - -class TestGlobals(unittest.TestCase): - def tearDown(self): - reload(_logs) - - def check_override_not_allowed(self): - """set_log_emitter_provider should throw a warning when overridden""" - provider = get_log_emitter_provider() - with self.assertLogs(level=WARNING) as test: - set_log_emitter_provider(LogEmitterProvider()) - self.assertEqual( - test.output, - [ - ( - "WARNING:opentelemetry.sdk._logs:Overriding of current " - "LogEmitterProvider is not allowed" - ) - ], - ) - self.assertIs(provider, get_log_emitter_provider()) - - def test_set_tracer_provider(self): - reload(_logs) - provider = LogEmitterProvider() - set_log_emitter_provider(provider) - retrieved_provider = get_log_emitter_provider() - self.assertEqual(provider, retrieved_provider) - - def test_tracer_provider_override_warning(self): - reload(_logs) - self.check_override_not_allowed() - - @patch.dict( - "os.environ", - {_OTEL_PYTHON_LOG_EMITTER_PROVIDER: "sdk_log_emitter_provider"}, - ) - def test_sdk_log_emitter_provider(self): - reload(_logs) - self.check_override_not_allowed() - - @patch.dict("os.environ", {_OTEL_PYTHON_LOG_EMITTER_PROVIDER: "unknown"}) - def test_unknown_log_emitter_provider(self): - reload(_logs) - with self.assertRaises(Exception): - get_log_emitter_provider() diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py deleted file mode 100644 index d7942f912b..0000000000 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import unittest -from unittest.mock import Mock - -from opentelemetry.sdk import trace -from opentelemetry.sdk._logs import LogEmitter, OTLPHandler -from opentelemetry.sdk._logs.severity import SeverityNumber -from opentelemetry.trace import INVALID_SPAN_CONTEXT - - -def get_logger(level=logging.NOTSET, log_emitter=None): - logger = logging.getLogger(__name__) - handler = OTLPHandler(level=level, log_emitter=log_emitter) - logger.addHandler(handler) - return logger - - -class TestOTLPHandler(unittest.TestCase): - def test_handler_default_log_level(self): - emitter_mock = Mock(spec=LogEmitter) - logger = get_logger(log_emitter=emitter_mock) - # Make sure debug messages are ignored by default - logger.debug("Debug message") - self.assertEqual(emitter_mock.emit.call_count, 0) - # Assert emit gets called for warning message - logger.warning("Warning message") - self.assertEqual(emitter_mock.emit.call_count, 1) - - def test_handler_custom_log_level(self): - emitter_mock = Mock(spec=LogEmitter) - logger = get_logger(level=logging.ERROR, log_emitter=emitter_mock) - logger.warning("Warning message test custom log level") - # Make sure any log with level < ERROR is ignored - self.assertEqual(emitter_mock.emit.call_count, 0) - logger.error("Mumbai, we have a major problem") - logger.critical("No Time For Caution") - self.assertEqual(emitter_mock.emit.call_count, 2) - - def test_log_record_no_span_context(self): - emitter_mock = Mock(spec=LogEmitter) - logger = get_logger(log_emitter=emitter_mock) - # Assert emit gets called for warning message - logger.warning("Warning message") - args, _ = emitter_mock.emit.call_args_list[0] - log_record = args[0] - - self.assertIsNotNone(log_record) - self.assertEqual(log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id) - self.assertEqual(log_record.span_id, INVALID_SPAN_CONTEXT.span_id) - self.assertEqual( - log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags - ) - - def test_log_record_user_attributes(self): - """Attributes can be injected into logs by adding them to the LogRecord""" - emitter_mock = Mock(spec=LogEmitter) - logger = get_logger(log_emitter=emitter_mock) - # Assert emit gets called for warning message - logger.warning("Warning message", extra={"http.status_code": 200}) - args, _ = emitter_mock.emit.call_args_list[0] - log_record = args[0] - - self.assertIsNotNone(log_record) - self.assertEqual(log_record.attributes, {"http.status_code": 200}) - - def test_log_record_trace_correlation(self): - emitter_mock = Mock(spec=LogEmitter) - logger = get_logger(log_emitter=emitter_mock) - - tracer = trace.TracerProvider().get_tracer(__name__) - with tracer.start_as_current_span("test") as span: - logger.critical("Critical message within span") - - args, _ = emitter_mock.emit.call_args_list[0] - log_record = args[0] - self.assertEqual(log_record.body, "Critical message within span") - self.assertEqual(log_record.severity_text, "CRITICAL") - self.assertEqual(log_record.severity_number, SeverityNumber.FATAL) - span_context = span.get_span_context() - self.assertEqual(log_record.trace_id, span_context.trace_id) - self.assertEqual(log_record.span_id, span_context.span_id) - self.assertEqual(log_record.trace_flags, span_context.trace_flags) diff --git a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py deleted file mode 100644 index e55124edcc..0000000000 --- a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py +++ /dev/null @@ -1,194 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint:disable=protected-access,no-self-use,no-member - -import logging -import threading -import time -import unittest -from abc import ABC, abstractmethod -from unittest.mock import Mock - -from opentelemetry.sdk._logs import ( - ConcurrentMultiLogProcessor, - LogEmitterProvider, - LogProcessor, - LogRecord, - OTLPHandler, - SynchronousMultiLogProcessor, -) -from opentelemetry.sdk._logs.severity import SeverityNumber - - -class AnotherLogProcessor(LogProcessor): - def __init__(self, exporter, logs_list): - self._exporter = exporter - self._log_list = logs_list - self._closed = False - - def emit(self, log_data): - if self._closed: - return - self._log_list.append( - (log_data.log_record.body, log_data.log_record.severity_text) - ) - - def shutdown(self): - self._closed = True - self._exporter.shutdown() - - def force_flush(self, timeout_millis=30000): - self._log_list.clear() - return True - - -class TestLogProcessor(unittest.TestCase): - def test_log_processor(self): - provider = LogEmitterProvider() - log_emitter = provider.get_log_emitter(__name__) - handler = OTLPHandler(log_emitter=log_emitter) - - logs_list_1 = [] - processor1 = AnotherLogProcessor(Mock(), logs_list_1) - logs_list_2 = [] - processor2 = AnotherLogProcessor(Mock(), logs_list_2) - - logger = logging.getLogger("test.span.processor") - logger.addHandler(handler) - - # Test no proessor added - logger.critical("Odisha, we have another major cyclone") - - self.assertEqual(len(logs_list_1), 0) - self.assertEqual(len(logs_list_2), 0) - - # Add one processor - provider.add_log_processor(processor1) - logger.warning("Brace yourself") - logger.error("Some error message") - - expected_list_1 = [ - ("Brace yourself", "WARNING"), - ("Some error message", "ERROR"), - ] - self.assertEqual(logs_list_1, expected_list_1) - - # Add another processor - provider.add_log_processor(processor2) - logger.critical("Something disastrous") - expected_list_1.append(("Something disastrous", "CRITICAL")) - - expected_list_2 = [("Something disastrous", "CRITICAL")] - - self.assertEqual(logs_list_1, expected_list_1) - self.assertEqual(logs_list_2, expected_list_2) - - -class MultiLogProcessorTestBase(ABC): - @abstractmethod - def _get_multi_log_processor(self): - pass - - def make_record(self): - return LogRecord( - timestamp=1622300111608942000, - severity_text="WARNING", - severity_number=SeverityNumber.WARN, - body="Warning message", - ) - - def test_on_emit(self): - multi_log_processor = self._get_multi_log_processor() - mocks = [Mock(spec=LogProcessor) for _ in range(5)] - for mock in mocks: - multi_log_processor.add_log_processor(mock) - record = self.make_record() - multi_log_processor.emit(record) - for mock in mocks: - mock.emit.assert_called_with(record) - multi_log_processor.shutdown() - - def test_on_shutdown(self): - multi_log_processor = self._get_multi_log_processor() - mocks = [Mock(spec=LogProcessor) for _ in range(5)] - for mock in mocks: - multi_log_processor.add_log_processor(mock) - multi_log_processor.shutdown() - for mock in mocks: - mock.shutdown.assert_called_once_with() - - def test_on_force_flush(self): - multi_log_processor = self._get_multi_log_processor() - mocks = [Mock(spec=LogProcessor) for _ in range(5)] - for mock in mocks: - multi_log_processor.add_log_processor(mock) - ret_value = multi_log_processor.force_flush(100) - - self.assertTrue(ret_value) - for mock_processor in mocks: - self.assertEqual(1, mock_processor.force_flush.call_count) - - -class TestSynchronousMultiLogProcessor( - MultiLogProcessorTestBase, unittest.TestCase -): - def _get_multi_log_processor(self): - return SynchronousMultiLogProcessor() - - def test_force_flush_delayed(self): - multi_log_processor = SynchronousMultiLogProcessor() - - def delay(_): - time.sleep(0.09) - - mock_processor1 = Mock(spec=LogProcessor) - mock_processor1.force_flush = Mock(side_effect=delay) - multi_log_processor.add_log_processor(mock_processor1) - mock_processor2 = Mock(spec=LogProcessor) - multi_log_processor.add_log_processor(mock_processor2) - - ret_value = multi_log_processor.force_flush(50) - self.assertFalse(ret_value) - self.assertEqual(mock_processor1.force_flush.call_count, 1) - self.assertEqual(mock_processor2.force_flush.call_count, 0) - - -class TestConcurrentMultiLogProcessor( - MultiLogProcessorTestBase, unittest.TestCase -): - def _get_multi_log_processor(self): - return ConcurrentMultiLogProcessor() - - def test_force_flush_delayed(self): - multi_log_processor = ConcurrentMultiLogProcessor() - wait_event = threading.Event() - - def delay(_): - wait_event.wait() - - mock1 = Mock(spec=LogProcessor) - mock1.force_flush = Mock(side_effect=delay) - mocks = [Mock(LogProcessor) for _ in range(5)] - mocks = [mock1] + mocks - for mock_processor in mocks: - multi_log_processor.add_log_processor(mock_processor) - - ret_value = multi_log_processor.force_flush(50) - wait_event.set() - - self.assertFalse(ret_value) - for mock in mocks: - self.assertEqual(1, mock.force_flush.call_count) - multi_log_processor.shutdown() From 8df19ef715e8fea68ae841767e347895b6e3eebd Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 10 Jun 2021 00:13:43 +0530 Subject: [PATCH 1035/1517] Add initial overall structure and classes for logs sdk (#1894) --- .../src/opentelemetry/sdk/logs/__init__.py | 174 ++++++++++++++++++ .../opentelemetry/sdk/logs/export/__init__.py | 53 ++++++ .../src/opentelemetry/sdk/logs/severity.py | 56 ++++++ 3 files changed, 283 insertions(+) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py new file mode 100644 index 0000000000..9a0fb2a095 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -0,0 +1,174 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import atexit +from typing import Any, Optional + +from opentelemetry.sdk.logs.severity import SeverityNumber +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.trace.span import TraceFlags +from opentelemetry.util.types import Attributes + + +class LogRecord: + """A LogRecord instance represents an event being logged. + + LogRecord instances are created and emitted via `LogEmitter` + every time something is logged. They contain all the information + pertinent to the event being logged. + """ + + def __init__( + self, + timestamp: Optional[int] = None, + trace_id: Optional[int] = None, + span_id: Optional[int] = None, + trace_flags: Optional[TraceFlags] = None, + severity_text: Optional[str] = None, + severity_number: Optional[SeverityNumber] = None, + name: Optional[str] = None, + body: Optional[Any] = None, + resource: Optional[Resource] = None, + attributes: Optional[Attributes] = None, + ): + self.timestamp = timestamp + self.trace_id = trace_id + self.span_id = span_id + self.trace_flags = trace_flags + self.severity_text = severity_text + self.severity_number = severity_number + self.name = name + self.body = body + self.resource = resource + self.attributes = attributes + + def __eq__(self, other: object) -> bool: + if not isinstance(other, LogRecord): + return NotImplemented + return self.__dict__ == other.__dict__ + + +class LogData: + """Readable LogRecord data plus associated InstrumentationLibrary.""" + + def __init__( + self, + log_record: LogRecord, + instrumentation_info: InstrumentationInfo, + ): + self.log_record = log_record + self.instrumentation_info = instrumentation_info + + +class LogProcessor(abc.ABC): + """Interface to hook the log record emitting action. + + Log processors can be registered directly using + :func:`LogEmitterProvider.add_log_processor` and they are invoked + in the same order as they were registered. + """ + + @abc.abstractmethod + def emit(self, log_data: LogData): + """Emits the `LogData`""" + + @abc.abstractmethod + def shutdown(self): + """Called when a :class:`opentelemetry.sdk.logs.LogEmitter` is shutdown""" + + @abc.abstractmethod + def force_flush(self, timeout_millis: int = 30000): + """Export all the received logs to the configured Exporter that have not yet + been exported. + + Args: + timeout_millis: The maximum amount of time to wait for logs to be + exported. + + Returns: + False if the timeout is exceeded, True otherwise. + """ + + +class LogEmitter: + # TODO: Add multi_log_processor + def __init__( + self, + resource: Resource, + instrumentation_info: InstrumentationInfo, + ): + self._resource = resource + self._instrumentation_info = instrumentation_info + + def emit(self, record: LogRecord): + # TODO: multi_log_processor.emit + pass + + def flush(self): + # TODO: multi_log_processor.force_flush + pass + + +class LogEmitterProvider: + # TODO: Add multi_log_processor + def __init__( + self, + resource: Resource = Resource.create(), + shutdown_on_exit: bool = True, + ): + self._resource = resource + self._at_exit_handler = None + if shutdown_on_exit: + self._at_exit_handler = atexit.register(self.shutdown) + + def get_log_emitter( + self, + instrumenting_module_name: str, + instrumenting_module_verison: str = "", + ) -> LogEmitter: + return LogEmitter( + self._resource, + InstrumentationInfo( + instrumenting_module_name, instrumenting_module_verison + ), + ) + + def add_log_processor(self, log_processor: LogProcessor): + """Registers a new :class:`LogProcessor` for this `LogEmitterProvider` instance. + + The log processors are invoked in the same order they are registered. + """ + # TODO: multi_log_processor.add_log_processor. + + def shutdown(self): + """Shuts down the log processors.""" + # TODO: multi_log_processor.shutdown + if self._at_exit_handler is not None: + atexit.unregister(self._at_exit_handler) + self._at_exit_handler = None + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Force flush the log processors. + + Args: + timeout_millis: The maximum amount of time to wait for logs to be + exported. + + Returns: + True if all the log processors flushes the logs within timeout, + False otherwise. + """ + # TODO: multi_log_processor.force_flush diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py new file mode 100644 index 0000000000..fd0ce8a813 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py @@ -0,0 +1,53 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import enum +from typing import Sequence + +from opentelemetry.sdk.logs import LogData + + +class LogExportResult(enum.Enum): + SUCCESS = 0 + FAILURE = 1 + + +class LogExporter(abc.ABC): + """Interface for exporting logs. + + Interface to be implemented by services that want to export logs received + in their own format. + + To export data this MUST be registered to the :class`opentelemetry.sdk.logs.LogEmitter` using a + log processor. + """ + + @abc.abstractmethod + def export(self, batch: Sequence[LogData]): + """Exports a batch of logs. + + Args: + batch: The list of `LogData` objects to be exported + + Returns: + The result of the export + """ + + @abc.abstractmethod + def shutdown(self): + """Shuts down the exporter. + + Called when the SDK is shut down. + """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py new file mode 100644 index 0000000000..13a9d4e6c3 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py @@ -0,0 +1,56 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import enum + + +class SeverityNumber(enum.Enum): + """Numerical value of severity. + + Smaller numerical values correspond to less severe events + (such as debug events), larger numerical values correspond + to more severe events (such as errors and critical events). + + See the `Log Data Model`_ spec for more info and how to map the + severity from source format to OTLP Model. + + .. _Log Data Model: + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber + """ + + UNSPECIFIED = 0 + TRACE = 1 + TRACE2 = 2 + TRACE3 = 3 + TRACE4 = 4 + DEBUG = 5 + DEBUG2 = 6 + DEBUG3 = 7 + DEBUG4 = 8 + INFO = 9 + INFO2 = 10 + INFO3 = 11 + INFO4 = 12 + WARN = 13 + WARN2 = 14 + WARN3 = 15 + WARN4 = 16 + ERROR = 17 + ERROR2 = 18 + ERROR3 = 19 + ERROR4 = 20 + FATAL = 21 + FATAL2 = 22 + FATAL3 = 23 + FATAL4 = 24 From 99ec1acbdd4ef77a2d0871b174c05e2abae3eefc Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 14 Jun 2021 22:56:27 +0530 Subject: [PATCH 1036/1517] Add global LogEmitterProvider and convenience function get_log_emitter (#1901) --- CHANGELOG.md | 4 + opentelemetry-sdk/setup.cfg | 2 + .../sdk/environment_variables.py | 9 +++ .../src/opentelemetry/sdk/logs/__init__.py | 67 ++++++++++++++++- opentelemetry-sdk/tests/logs/__init__.py | 13 ++++ .../tests/logs/test_global_provider.py | 75 +++++++++++++++++++ 6 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 opentelemetry-sdk/tests/logs/__init__.py create mode 100644 opentelemetry-sdk/tests/logs/test_global_provider.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ad92cf02b5..13629eee75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added dropped count to otlp, jaeger and zipkin exporters. ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) +### Added +- Add global LogEmitterProvider and convenience function get_log_emitter + ([#1901](https://github.com/open-telemetry/opentelemetry-python/pull/1901)) + ### Changed - Updated `opentelemetry-opencensus-exporter` to use `service_name` of spans instead of resource ([#1897](https://github.com/open-telemetry/opentelemetry-python/pull/1897)) diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 1996249573..19031c11e3 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -54,6 +54,8 @@ opentelemetry_tracer_provider = sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider opentelemetry_traces_exporter = console = opentelemetry.sdk.trace.export:ConsoleSpanExporter +opentelemetry_log_emitter_provider = + sdk_log_emitter_provider = opentelemetry.sdk.logs:LogEmitterProvider opentelemetry_id_generator = random = opentelemetry.sdk.trace.id_generator:RandomIdGenerator opentelemetry_environment_variables = diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 8b3d4abbf8..7c89e1c9d5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -369,3 +369,12 @@ If both are set, :envvar:`OTEL_SERVICE_NAME` takes precedence. """ + +OTEL_PYTHON_LOG_EMITTER_PROVIDER = "OTEL_PYTHON_LOG_EMITTER_PROVIDER" +""" +.. envvar:: OTEL_PYTHON_LOG_EMITTER_PROVIDER + +The :envvar:`OTEL_PYTHON_LOG_EMITTER_PROVIDER` environment variable allows users to +provide the entry point for loading the log emitter provider. If not specified, SDK +LogEmitterProvider is used. +""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py index 9a0fb2a095..f93987e08d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -14,14 +14,22 @@ import abc import atexit -from typing import Any, Optional +import logging +import os +from typing import Any, Optional, cast +from opentelemetry.sdk.environment_variables import ( + OTEL_PYTHON_LOG_EMITTER_PROVIDER, +) from opentelemetry.sdk.logs.severity import SeverityNumber from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace.span import TraceFlags +from opentelemetry.util._providers import _load_provider from opentelemetry.util.types import Attributes +_logger = logging.getLogger(__name__) + class LogRecord: """A LogRecord instance represents an event being logged. @@ -172,3 +180,60 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: False otherwise. """ # TODO: multi_log_processor.force_flush + + +_LOG_EMITTER_PROVIDER = None + + +def get_log_emitter_provider() -> LogEmitterProvider: + """Gets the current global :class:`~.LogEmitterProvider` object.""" + global _LOG_EMITTER_PROVIDER # pylint: disable=global-statement + if _LOG_EMITTER_PROVIDER is None: + if OTEL_PYTHON_LOG_EMITTER_PROVIDER not in os.environ: + _LOG_EMITTER_PROVIDER = LogEmitterProvider() + return _LOG_EMITTER_PROVIDER + + _LOG_EMITTER_PROVIDER = cast( + "LogEmitterProvider", + _load_provider( + OTEL_PYTHON_LOG_EMITTER_PROVIDER, "log_emitter_provider" + ), + ) + + return _LOG_EMITTER_PROVIDER + + +def set_log_emitter_provider(log_emitter_provider: LogEmitterProvider) -> None: + """Sets the current global :class:`~.LogEmitterProvider` object. + + This can only be done once, a warning will be logged if any furter attempt + is made. + """ + global _LOG_EMITTER_PROVIDER # pylint: disable=global-statement + + if _LOG_EMITTER_PROVIDER is not None: + _logger.warning( + "Overriding of current LogEmitterProvider is not allowed" + ) + return + + _LOG_EMITTER_PROVIDER = log_emitter_provider + + +def get_log_emitter( + instrumenting_module_name: str, + instrumenting_library_version: str = "", + log_emitter_provider: Optional[LogEmitterProvider] = None, +) -> LogEmitter: + """Returns a `LogEmitter` for use within a python process. + + This function is a convenience wrapper for + opentelemetry.sdk.logs.LogEmitterProvider.get_log_emitter. + + If log_emitter_provider param is omitted the current configured one is used. + """ + if log_emitter_provider is None: + log_emitter_provider = get_log_emitter_provider() + return log_emitter_provider.get_log_emitter( + instrumenting_module_name, instrumenting_library_version + ) diff --git a/opentelemetry-sdk/tests/logs/__init__.py b/opentelemetry-sdk/tests/logs/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/opentelemetry-sdk/tests/logs/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-sdk/tests/logs/test_global_provider.py b/opentelemetry-sdk/tests/logs/test_global_provider.py new file mode 100644 index 0000000000..fc687d1961 --- /dev/null +++ b/opentelemetry-sdk/tests/logs/test_global_provider.py @@ -0,0 +1,75 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# type:ignore +import unittest +from importlib import reload +from logging import WARNING +from unittest.mock import patch + +from opentelemetry.sdk import logs +from opentelemetry.sdk.environment_variables import ( + OTEL_PYTHON_LOG_EMITTER_PROVIDER, +) +from opentelemetry.sdk.logs import ( + LogEmitterProvider, + get_log_emitter_provider, + set_log_emitter_provider, +) + + +class TestGlobals(unittest.TestCase): + def tearDown(self): + reload(logs) + + def check_override_not_allowed(self): + """set_log_emitter_provider should throw a warning when overridden""" + provider = get_log_emitter_provider() + with self.assertLogs(level=WARNING) as test: + set_log_emitter_provider(LogEmitterProvider()) + self.assertEqual( + test.output, + [ + ( + "WARNING:opentelemetry.sdk.logs:Overriding of current " + "LogEmitterProvider is not allowed" + ) + ], + ) + self.assertIs(provider, get_log_emitter_provider()) + + def test_set_tracer_provider(self): + reload(logs) + provider = LogEmitterProvider() + set_log_emitter_provider(provider) + retrieved_provider = get_log_emitter_provider() + self.assertEqual(provider, retrieved_provider) + + def test_tracer_provider_override_warning(self): + reload(logs) + self.check_override_not_allowed() + + @patch.dict( + "os.environ", + {OTEL_PYTHON_LOG_EMITTER_PROVIDER: "sdk_log_emitter_provider"}, + ) + def test_sdk_log_emitter_provider(self): + reload(logs) + self.check_override_not_allowed() + + @patch.dict("os.environ", {OTEL_PYTHON_LOG_EMITTER_PROVIDER: "unknown"}) + def test_unknown_log_emitter_provider(self): + reload(logs) + with self.assertRaises(Exception): + get_log_emitter_provider() From 44ffee503ce6648fa67d335d077a62a538016bd3 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 18 Jun 2021 22:28:46 +0530 Subject: [PATCH 1037/1517] Add OTLPHandler for standard library logging module (#1903) --- CHANGELOG.md | 2 + .../src/opentelemetry/sdk/logs/__init__.py | 53 +++++++++++- .../src/opentelemetry/sdk/logs/severity.py | 60 +++++++++++++ opentelemetry-sdk/tests/logs/test_handler.py | 84 +++++++++++++++++++ 4 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 opentelemetry-sdk/tests/logs/test_handler.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 13629eee75..1f1c2529fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add global LogEmitterProvider and convenience function get_log_emitter ([#1901](https://github.com/open-telemetry/opentelemetry-python/pull/1901)) +- Add OTLPHandler for standard library logging module + ([#1903](https://github.com/open-telemetry/opentelemetry-python/pull/1903)) ### Changed - Updated `opentelemetry-opencensus-exporter` to use `service_name` of spans instead of resource diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py index f93987e08d..e436461e53 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -21,9 +21,10 @@ from opentelemetry.sdk.environment_variables import ( OTEL_PYTHON_LOG_EMITTER_PROVIDER, ) -from opentelemetry.sdk.logs.severity import SeverityNumber +from opentelemetry.sdk.logs.severity import SeverityNumber, std_to_otlp from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.trace import get_current_span from opentelemetry.trace.span import TraceFlags from opentelemetry.util._providers import _load_provider from opentelemetry.util.types import Attributes @@ -111,6 +112,48 @@ def force_flush(self, timeout_millis: int = 30000): """ +class OTLPHandler(logging.Handler): + """A handler class which writes logging records, in OTLP format, to + a network destination or file. + """ + + def __init__(self, level=logging.NOTSET, log_emitter=None) -> None: + super().__init__(level=level) + self._log_emitter = log_emitter or get_log_emitter(__name__) + + def _translate(self, record: logging.LogRecord) -> LogRecord: + timestamp = int(record.created * 1e9) + span_context = get_current_span().get_span_context() + # TODO: attributes (or resource attributes?) from record metadata + attributes: Attributes = {} + severity_number = std_to_otlp(record.levelno) + return LogRecord( + timestamp=timestamp, + trace_id=span_context.trace_id, + span_id=span_context.span_id, + trace_flags=span_context.trace_flags, + severity_text=record.levelname, + severity_number=severity_number, + body=record.getMessage(), + resource=self._log_emitter.resource, + attributes=attributes, + ) + + def emit(self, record: logging.LogRecord) -> None: + """ + Emit a record. + + The record is translated to OTLP format, and then sent across the pipeline. + """ + self._log_emitter.emit(self._translate(record)) + + def flush(self) -> None: + """ + Flushes the logging output. + """ + self._log_emitter.flush() + + class LogEmitter: # TODO: Add multi_log_processor def __init__( @@ -121,6 +164,10 @@ def __init__( self._resource = resource self._instrumentation_info = instrumentation_info + @property + def resource(self): + return self._resource + def emit(self, record: LogRecord): # TODO: multi_log_processor.emit pass @@ -142,6 +189,10 @@ def __init__( if shutdown_on_exit: self._at_exit_handler = atexit.register(self.shutdown) + @property + def resource(self): + return self._resource + def get_log_emitter( self, instrumenting_module_name: str, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py index 13a9d4e6c3..c0509ea2c1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py @@ -54,3 +54,63 @@ class SeverityNumber(enum.Enum): FATAL2 = 22 FATAL3 = 23 FATAL4 = 24 + + +_STD_TO_OTLP = { + 10: SeverityNumber.DEBUG, + 11: SeverityNumber.DEBUG2, + 12: SeverityNumber.DEBUG3, + 13: SeverityNumber.DEBUG4, + 14: SeverityNumber.DEBUG4, + 15: SeverityNumber.DEBUG4, + 16: SeverityNumber.DEBUG4, + 17: SeverityNumber.DEBUG4, + 18: SeverityNumber.DEBUG4, + 19: SeverityNumber.DEBUG4, + 20: SeverityNumber.INFO, + 21: SeverityNumber.INFO2, + 22: SeverityNumber.INFO3, + 23: SeverityNumber.INFO4, + 24: SeverityNumber.INFO4, + 25: SeverityNumber.INFO4, + 26: SeverityNumber.INFO4, + 27: SeverityNumber.INFO4, + 28: SeverityNumber.INFO4, + 29: SeverityNumber.INFO4, + 30: SeverityNumber.WARN, + 31: SeverityNumber.WARN2, + 32: SeverityNumber.WARN3, + 33: SeverityNumber.WARN4, + 34: SeverityNumber.WARN4, + 35: SeverityNumber.WARN4, + 36: SeverityNumber.WARN4, + 37: SeverityNumber.WARN4, + 38: SeverityNumber.WARN4, + 39: SeverityNumber.WARN4, + 40: SeverityNumber.ERROR, + 41: SeverityNumber.ERROR2, + 42: SeverityNumber.ERROR3, + 43: SeverityNumber.ERROR4, + 44: SeverityNumber.ERROR4, + 45: SeverityNumber.ERROR4, + 46: SeverityNumber.ERROR4, + 47: SeverityNumber.ERROR4, + 48: SeverityNumber.ERROR4, + 49: SeverityNumber.ERROR4, + 50: SeverityNumber.FATAL, + 51: SeverityNumber.FATAL2, + 52: SeverityNumber.FATAL3, + 53: SeverityNumber.FATAL4, +} + + +def std_to_otlp(levelno: int) -> SeverityNumber: + """ + Map python log levelno as defined in https://docs.python.org/3/library/logging.html#logging-levels + to OTLP log severity number. + """ + if levelno < 10: + return SeverityNumber.UNSPECIFIED + if levelno > 53: + return SeverityNumber.FATAL4 + return _STD_TO_OTLP[levelno] diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py new file mode 100644 index 0000000000..1d1b84f0fd --- /dev/null +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -0,0 +1,84 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import unittest +from unittest.mock import Mock + +from opentelemetry.sdk import trace +from opentelemetry.sdk.logs import LogEmitter, OTLPHandler +from opentelemetry.sdk.logs.severity import SeverityNumber +from opentelemetry.trace import INVALID_SPAN_CONTEXT + + +def get_logger(level=logging.NOTSET, log_emitter=None): + logger = logging.getLogger(__name__) + handler = OTLPHandler(level=level, log_emitter=log_emitter) + logger.addHandler(handler) + return logger + + +class TestOTLPHandler(unittest.TestCase): + def test_handler_default_log_level(self): + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(log_emitter=emitter_mock) + # Make sure debug messages are ignored by default + logger.debug("Debug message") + self.assertEqual(emitter_mock.emit.call_count, 0) + # Assert emit gets called for warning message + logger.warning("Wanrning message") + self.assertEqual(emitter_mock.emit.call_count, 1) + + def test_handler_custom_log_level(self): + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(level=logging.ERROR, log_emitter=emitter_mock) + logger.warning("Warning message test custom log level") + # Make sure any log with level < ERROR is ignored + self.assertEqual(emitter_mock.emit.call_count, 0) + logger.error("Mumbai, we have a major problem") + logger.critical("No Time For Caution") + self.assertEqual(emitter_mock.emit.call_count, 2) + + def test_log_record_no_span_context(self): + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(log_emitter=emitter_mock) + # Assert emit gets called for warning message + logger.warning("Wanrning message") + args, _ = emitter_mock.emit.call_args_list[0] + log_record = args[0] + + self.assertIsNotNone(log_record) + self.assertEqual(log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id) + self.assertEqual(log_record.span_id, INVALID_SPAN_CONTEXT.span_id) + self.assertEqual( + log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags + ) + + def test_log_record_trace_correlation(self): + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(log_emitter=emitter_mock) + + tracer = trace.TracerProvider().get_tracer(__name__) + with tracer.start_as_current_span("test") as span: + logger.critical("Critical message within span") + + args, _ = emitter_mock.emit.call_args_list[0] + log_record = args[0] + self.assertEqual(log_record.body, "Critical message within span") + self.assertEqual(log_record.severity_text, "CRITICAL") + self.assertEqual(log_record.severity_number, SeverityNumber.FATAL) + span_context = span.get_span_context() + self.assertEqual(log_record.trace_id, span_context.trace_id) + self.assertEqual(log_record.span_id, span_context.span_id) + self.assertEqual(log_record.trace_flags, span_context.trace_flags) From 3273aa64398225adbdf5ea4430ab34bf21aa3352 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 13 Jul 2021 22:35:22 +0530 Subject: [PATCH 1038/1517] Add LogProcessors implementation (#1916) --- .../src/opentelemetry/sdk/logs/__init__.py | 166 +++++++++++- .../opentelemetry/sdk/logs/export/__init__.py | 223 ++++++++++++++- .../sdk/logs/export/in_memory_log_exporter.py | 51 ++++ opentelemetry-sdk/tests/logs/test_export.py | 256 ++++++++++++++++++ .../tests/logs/test_multi_log_prcessor.py | 194 +++++++++++++ 5 files changed, 878 insertions(+), 12 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/logs/export/in_memory_log_exporter.py create mode 100644 opentelemetry-sdk/tests/logs/test_export.py create mode 100644 opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py index e436461e53..8b0da9e22a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -14,9 +14,11 @@ import abc import atexit +import concurrent.futures import logging import os -from typing import Any, Optional, cast +import threading +from typing import Any, Callable, Optional, Tuple, Union, cast from opentelemetry.sdk.environment_variables import ( OTEL_PYTHON_LOG_EMITTER_PROVIDER, @@ -27,6 +29,7 @@ from opentelemetry.trace import get_current_span from opentelemetry.trace.span import TraceFlags from opentelemetry.util._providers import _load_provider +from opentelemetry.util._time import _time_ns from opentelemetry.util.types import Attributes _logger = logging.getLogger(__name__) @@ -112,6 +115,135 @@ def force_flush(self, timeout_millis: int = 30000): """ +# Temporary fix until https://github.com/PyCQA/pylint/issues/4098 is resolved +# pylint:disable=no-member +class SynchronousMultiLogProcessor(LogProcessor): + """Implementation of class:`LogProcessor` that forwards all received + events to a list of log processors sequentially. + + The underlying log processors are called in sequential order as they were + added. + """ + + def __init__(self): + # use a tuple to avoid race conditions when adding a new log and + # iterating through it on "emit". + self._log_processors = () # type: Tuple[LogProcessor, ...] + self._lock = threading.Lock() + + def add_log_processor(self, log_processor: LogProcessor) -> None: + """Adds a Logprocessor to the list of log processors handled by this instance""" + with self._lock: + self._log_processors = self._log_processors + (log_processor,) + + def emit(self, log_data: LogData) -> None: + for lp in self._log_processors: + lp.emit(log_data) + + def shutdown(self) -> None: + """Shutdown the log processors one by one""" + for lp in self._log_processors: + lp.shutdown() + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Force flush the log processors one by one + + Args: + timeout_millis: The maximum amount of time to wait for logs to be + exported. If the first n log processors exceeded the timeout + then remaining log processors will not be flushed. + + Returns: + True if all the log processors flushes the logs within timeout, + False otherwise. + """ + deadline_ns = _time_ns() + timeout_millis * 1000000 + for lp in self._log_processors: + current_ts = _time_ns() + if current_ts >= deadline_ns: + return False + + if not lp.force_flush((deadline_ns - current_ts) // 1000000): + return False + + return True + + +class ConcurrentMultiLogProcessor(LogProcessor): + """Implementation of :class:`LogProcessor` that forwards all received + events to a list of log processors in parallel. + + Calls to the underlying log processors are forwarded in parallel by + submitting them to a thread pool executor and waiting until each log + processor finished its work. + + Args: + max_workers: The number of threads managed by the thread pool executor + and thus defining how many log processors can work in parallel. + """ + + def __init__(self, max_workers: int = 2): + # use a tuple to avoid race conditions when adding a new log and + # iterating through it on "emit". + self._log_processors = () # type: Tuple[LogProcessor, ...] + self._lock = threading.Lock() + self._executor = concurrent.futures.ThreadPoolExecutor( + max_workers=max_workers + ) + + def add_log_processor(self, log_processor: LogProcessor): + with self._lock: + self._log_processors = self._log_processors + (log_processor,) + + def _submit_and_wait( + self, + func: Callable[[LogProcessor], Callable[..., None]], + *args: Any, + **kwargs: Any, + ): + futures = [] + for lp in self._log_processors: + future = self._executor.submit(func(lp), *args, **kwargs) + futures.append(future) + for future in futures: + future.result() + + def emit(self, log_data: LogData): + self._submit_and_wait(lambda lp: lp.emit, log_data) + + def shutdown(self): + self._submit_and_wait(lambda lp: lp.shutdown) + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Force flush the log processors in parallel. + + Args: + timeout_millis: The maximum amount of time to wait for logs to be + exported. + + Returns: + True if all the log processors flushes the logs within timeout, + False otherwise. + """ + futures = [] + for lp in self._log_processors: + future = self._executor.submit(lp.force_flush, timeout_millis) + futures.append(future) + + done_futures, not_done_futures = concurrent.futures.wait( + futures, timeout_millis / 1e3 + ) + + if not_done_futures: + return False + + for future in done_futures: + if not future.result(): + return False + + return True + + class OTLPHandler(logging.Handler): """A handler class which writes logging records, in OTLP format, to a network destination or file. @@ -155,13 +287,16 @@ def flush(self) -> None: class LogEmitter: - # TODO: Add multi_log_processor def __init__( self, resource: Resource, + multi_log_processor: Union[ + SynchronousMultiLogProcessor, ConcurrentMultiLogProcessor + ], instrumentation_info: InstrumentationInfo, ): self._resource = resource + self._multi_log_processor = multi_log_processor self._instrumentation_info = instrumentation_info @property @@ -169,22 +304,32 @@ def resource(self): return self._resource def emit(self, record: LogRecord): - # TODO: multi_log_processor.emit - pass + """Emits the :class:`LogData` by associating :class:`LogRecord` + and instrumentation info. + """ + log_data = LogData(record, self._instrumentation_info) + self._multi_log_processor.emit(log_data) + # TODO: Should this flush everything in pipeline? + # Prior discussion https://github.com/open-telemetry/opentelemetry-python/pull/1916#discussion_r659945290 def flush(self): - # TODO: multi_log_processor.force_flush - pass + """Ensure all logging output has been flushed.""" + self._multi_log_processor.force_flush() class LogEmitterProvider: - # TODO: Add multi_log_processor def __init__( self, resource: Resource = Resource.create(), shutdown_on_exit: bool = True, + multi_log_processor: Union[ + SynchronousMultiLogProcessor, ConcurrentMultiLogProcessor + ] = None, ): self._resource = resource + self._multi_log_processor = ( + multi_log_processor or SynchronousMultiLogProcessor() + ) self._at_exit_handler = None if shutdown_on_exit: self._at_exit_handler = atexit.register(self.shutdown) @@ -200,6 +345,7 @@ def get_log_emitter( ) -> LogEmitter: return LogEmitter( self._resource, + self._multi_log_processor, InstrumentationInfo( instrumenting_module_name, instrumenting_module_verison ), @@ -210,11 +356,11 @@ def add_log_processor(self, log_processor: LogProcessor): The log processors are invoked in the same order they are registered. """ - # TODO: multi_log_processor.add_log_processor. + self._multi_log_processor.add_log_processor(log_processor) def shutdown(self): """Shuts down the log processors.""" - # TODO: multi_log_processor.shutdown + self._multi_log_processor.shutdown() if self._at_exit_handler is not None: atexit.unregister(self._at_exit_handler) self._at_exit_handler = None @@ -230,7 +376,7 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: True if all the log processors flushes the logs within timeout, False otherwise. """ - # TODO: multi_log_processor.force_flush + return self._multi_log_processor.force_flush(timeout_millis) _LOG_EMITTER_PROVIDER = None diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py index fd0ce8a813..167e487102 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py @@ -13,10 +13,17 @@ # limitations under the License. import abc +import collections import enum -from typing import Sequence +import logging +import threading +from typing import Deque, List, Optional, Sequence -from opentelemetry.sdk.logs import LogData +from opentelemetry.context import attach, detach, set_value +from opentelemetry.sdk.logs import LogData, LogProcessor +from opentelemetry.util._time import _time_ns + +_logger = logging.getLogger(__name__) class LogExportResult(enum.Enum): @@ -51,3 +58,215 @@ def shutdown(self): Called when the SDK is shut down. """ + + +class SimpleLogProcessor(LogProcessor): + """This is an implementation of LogProcessor which passes + received logs in the export-friendly LogData representation to the + configured LogExporter, as soon as they are emitted. + """ + + def __init__(self, exporter: LogExporter): + self._exporter = exporter + self._shutdown = False + + def emit(self, log_data: LogData): + if self._shutdown: + _logger.warning("Processor is already shutdown, ignoring call") + return + token = attach(set_value("suppress_instrumentation", True)) + try: + self._exporter.export((log_data,)) + except Exception: # pylint: disable=broad-except + _logger.exception("Exception while exporting logs.") + detach(token) + + def shutdown(self): + self._shutdown = True + self._exporter.shutdown() + + def force_flush( + self, timeout_millis: int = 30000 + ) -> bool: # pylint: disable=no-self-use + return True + + +class _FlushRequest: + __slots__ = ["event", "num_log_records"] + + def __init__(self): + self.event = threading.Event() + self.num_log_records = 0 + + +class BatchLogProcessor(LogProcessor): + """This is an implementation of LogProcessor which creates batches of + received logs in the export-friendly LogData representation and + send to the configured LogExporter, as soon as they are emitted. + """ + + def __init__( + self, + exporter: LogExporter, + schedule_delay_millis: int = 5000, + max_export_batch_size: int = 512, + export_timeout_millis: int = 30000, + ): + self._exporter = exporter + self._schedule_delay_millis = schedule_delay_millis + self._max_export_batch_size = max_export_batch_size + self._export_timeout_millis = export_timeout_millis + self._queue = collections.deque() # type: Deque[LogData] + self._worker_thread = threading.Thread(target=self.worker, daemon=True) + self._condition = threading.Condition(threading.Lock()) + self._shutdown = False + self._flush_request = None # type: Optional[_FlushRequest] + self._log_records = [ + None + ] * self._max_export_batch_size # type: List[Optional[LogData]] + self._worker_thread.start() + + def worker(self): + timeout = self._schedule_delay_millis / 1e3 + flush_request = None # type: Optional[_FlushRequest] + while not self._shutdown: + with self._condition: + if self._shutdown: + # shutdown may have been called, avoid further processing + break + flush_request = self._get_and_unset_flush_request() + if ( + len(self._queue) < self._max_export_batch_size + and self._flush_request is None + ): + self._condition.wait(timeout) + + flush_request = self._get_and_unset_flush_request() + if not self._queue: + timeout = self._schedule_delay_millis / 1e3 + self._notify_flush_request_finished(flush_request) + flush_request = None + continue + if self._shutdown: + break + + start_ns = _time_ns() + self._export(flush_request) + end_ns = _time_ns() + # subtract the duration of this export call to the next timeout + timeout = self._schedule_delay_millis / 1e3 - ( + (end_ns - start_ns) / 1e9 + ) + + self._notify_flush_request_finished(flush_request) + flush_request = None + + # there might have been a new flush request while export was running + # and before the done flag switched to true + with self._condition: + shutdown_flush_request = self._get_and_unset_flush_request() + + # flush the remaining logs + self._drain_queue() + self._notify_flush_request_finished(flush_request) + self._notify_flush_request_finished(shutdown_flush_request) + + def _export(self, flush_request: Optional[_FlushRequest] = None): + """Exports logs considering the given flush_request. + + If flush_request is not None then logs are exported in batches + until the number of exported logs reached or exceeded the num of logs in + flush_request, otherwise exports at max max_export_batch_size logs. + """ + if flush_request is None: + self._export_batch() + return + + num_log_records = flush_request.num_log_records + while self._queue: + exported = self._export_batch() + num_log_records -= exported + + if num_log_records <= 0: + break + + def _export_batch(self) -> int: + """Exports at most max_export_batch_size logs and returns the number of + exported logs. + """ + idx = 0 + while idx < self._max_export_batch_size and self._queue: + record = self._queue.pop() + self._log_records[idx] = record + idx += 1 + token = attach(set_value("suppress_instrumentation", True)) + try: + self._exporter.export(self._log_records[:idx]) # type: ignore + except Exception: # pylint: disable=broad-except + _logger.exception("Exception while exporting logs.") + detach(token) + + for index in range(idx): + self._log_records[index] = None + return idx + + def _drain_queue(self): + """Export all elements until queue is empty. + + Can only be called from the worker thread context because it invokes + `export` that is not thread safe. + """ + while self._queue: + self._export_batch() + + def _get_and_unset_flush_request(self) -> Optional[_FlushRequest]: + flush_request = self._flush_request + self._flush_request = None + if flush_request is not None: + flush_request.num_log_records = len(self._queue) + return flush_request + + @staticmethod + def _notify_flush_request_finished( + flush_request: Optional[_FlushRequest] = None, + ): + if flush_request is not None: + flush_request.event.set() + + def _get_or_create_flush_request(self) -> _FlushRequest: + if self._flush_request is None: + self._flush_request = _FlushRequest() + return self._flush_request + + def emit(self, log_data: LogData) -> None: + """Adds the `LogData` to queue and notifies the waiting threads + when size of queue reaches max_export_batch_size. + """ + if self._shutdown: + return + self._queue.appendleft(log_data) + if len(self._queue) >= self._max_export_batch_size: + with self._condition: + self._condition.notify() + + def shutdown(self): + self._shutdown = True + with self._condition: + self._condition.notify_all() + self._worker_thread.join() + self._exporter.shutdown() + + def force_flush(self, timeout_millis: Optional[int] = None) -> bool: + if timeout_millis is None: + timeout_millis = self._export_timeout_millis + if self._shutdown: + return True + + with self._condition: + flush_request = self._get_or_create_flush_request() + self._condition.notify_all() + + ret = flush_request.event.wait() + if not ret: + _logger.warning("Timeout was exceeded in force_flush().") + return ret diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/in_memory_log_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/in_memory_log_exporter.py new file mode 100644 index 0000000000..95cb8bccba --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/in_memory_log_exporter.py @@ -0,0 +1,51 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import typing + +from opentelemetry.sdk.logs import LogData +from opentelemetry.sdk.logs.export import LogExporter, LogExportResult + + +class InMemoryLogExporter(LogExporter): + """Implementation of :class:`.LogExporter` that stores logs in memory. + + This class can be used for testing purposes. It stores the exported logs + in a list in memory that can be retrieved using the + :func:`.get_finished_logs` method. + """ + + def __init__(self): + self._logs = [] + self._lock = threading.Lock() + self._stopped = False + + def clear(self) -> None: + with self._lock: + self._logs.clear() + + def get_finished_logs(self) -> typing.Tuple[LogData, ...]: + with self._lock: + return tuple(self._logs) + + def export(self, batch: typing.Sequence[LogData]) -> LogExportResult: + if self._stopped: + return LogExportResult.FAILURE + with self._lock: + self._logs.extend(batch) + return LogExportResult.SUCCESS + + def shutdown(self) -> None: + self._stopped = True diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py new file mode 100644 index 0000000000..f1e97c6a02 --- /dev/null +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -0,0 +1,256 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access +import logging +import unittest +from concurrent.futures import ThreadPoolExecutor +from unittest.mock import Mock + +from opentelemetry.sdk import trace +from opentelemetry.sdk.logs import LogEmitterProvider, OTLPHandler +from opentelemetry.sdk.logs.export import BatchLogProcessor, SimpleLogProcessor +from opentelemetry.sdk.logs.export.in_memory_log_exporter import ( + InMemoryLogExporter, +) +from opentelemetry.sdk.logs.severity import SeverityNumber +from opentelemetry.trace.span import INVALID_SPAN_CONTEXT + + +class TestSimpleLogProcessor(unittest.TestCase): + def test_simple_log_processor_default_level(self): + exporter = InMemoryLogExporter() + log_emitter_provider = LogEmitterProvider() + log_emitter = log_emitter_provider.get_log_emitter(__name__) + + log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) + + logger = logging.getLogger("default_level") + logger.addHandler(OTLPHandler(log_emitter=log_emitter)) + + logger.warning("Something is wrong") + finished_logs = exporter.get_finished_logs() + self.assertEqual(len(finished_logs), 1) + warning_log_record = finished_logs[0].log_record + self.assertEqual(warning_log_record.body, "Something is wrong") + self.assertEqual(warning_log_record.severity_text, "WARNING") + self.assertEqual( + warning_log_record.severity_number, SeverityNumber.WARN + ) + + def test_simple_log_processor_custom_level(self): + exporter = InMemoryLogExporter() + log_emitter_provider = LogEmitterProvider() + log_emitter = log_emitter_provider.get_log_emitter(__name__) + + log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) + + logger = logging.getLogger("custom_level") + logger.setLevel(logging.ERROR) + logger.addHandler(OTLPHandler(log_emitter=log_emitter)) + + logger.warning("Warning message") + logger.debug("Debug message") + logger.error("Error message") + logger.critical("Critical message") + finished_logs = exporter.get_finished_logs() + # Make sure only level >= logging.CRITICAL logs are recorded + self.assertEqual(len(finished_logs), 2) + critical_log_record = finished_logs[0].log_record + fatal_log_record = finished_logs[1].log_record + self.assertEqual(critical_log_record.body, "Error message") + self.assertEqual(critical_log_record.severity_text, "ERROR") + self.assertEqual( + critical_log_record.severity_number, SeverityNumber.ERROR + ) + self.assertEqual(fatal_log_record.body, "Critical message") + self.assertEqual(fatal_log_record.severity_text, "CRITICAL") + self.assertEqual( + fatal_log_record.severity_number, SeverityNumber.FATAL + ) + + def test_simple_log_processor_trace_correlation(self): + exporter = InMemoryLogExporter() + log_emitter_provider = LogEmitterProvider() + log_emitter = log_emitter_provider.get_log_emitter("name", "version") + + log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) + + logger = logging.getLogger("trace_correlation") + logger.addHandler(OTLPHandler(log_emitter=log_emitter)) + + logger.warning("Warning message") + finished_logs = exporter.get_finished_logs() + self.assertEqual(len(finished_logs), 1) + log_record = finished_logs[0].log_record + self.assertEqual(log_record.body, "Warning message") + self.assertEqual(log_record.severity_text, "WARNING") + self.assertEqual(log_record.severity_number, SeverityNumber.WARN) + self.assertEqual(log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id) + self.assertEqual(log_record.span_id, INVALID_SPAN_CONTEXT.span_id) + self.assertEqual( + log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags + ) + exporter.clear() + + tracer = trace.TracerProvider().get_tracer(__name__) + with tracer.start_as_current_span("test") as span: + logger.critical("Critical message within span") + + finished_logs = exporter.get_finished_logs() + log_record = finished_logs[0].log_record + self.assertEqual(log_record.body, "Critical message within span") + self.assertEqual(log_record.severity_text, "CRITICAL") + self.assertEqual(log_record.severity_number, SeverityNumber.FATAL) + span_context = span.get_span_context() + self.assertEqual(log_record.trace_id, span_context.trace_id) + self.assertEqual(log_record.span_id, span_context.span_id) + self.assertEqual(log_record.trace_flags, span_context.trace_flags) + + def test_simple_log_processor_shutdown(self): + exporter = InMemoryLogExporter() + log_emitter_provider = LogEmitterProvider() + log_emitter = log_emitter_provider.get_log_emitter(__name__) + + log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) + + logger = logging.getLogger("shutdown") + logger.addHandler(OTLPHandler(log_emitter=log_emitter)) + + logger.warning("Something is wrong") + finished_logs = exporter.get_finished_logs() + self.assertEqual(len(finished_logs), 1) + warning_log_record = finished_logs[0].log_record + self.assertEqual(warning_log_record.body, "Something is wrong") + self.assertEqual(warning_log_record.severity_text, "WARNING") + self.assertEqual( + warning_log_record.severity_number, SeverityNumber.WARN + ) + exporter.clear() + log_emitter_provider.shutdown() + logger.warning("Log after shutdown") + finished_logs = exporter.get_finished_logs() + self.assertEqual(len(finished_logs), 0) + + +class TestBatchLogProcessor(unittest.TestCase): + def test_emit_call_log_record(self): + exporter = InMemoryLogExporter() + log_processor = Mock(wraps=BatchLogProcessor(exporter)) + provider = LogEmitterProvider() + provider.add_log_processor(log_processor) + + emitter = provider.get_log_emitter(__name__) + logger = logging.getLogger("emit_call") + logger.addHandler(OTLPHandler(log_emitter=emitter)) + + logger.error("error") + self.assertEqual(log_processor.emit.call_count, 1) + + def test_shutdown(self): + exporter = InMemoryLogExporter() + log_processor = BatchLogProcessor(exporter) + + provider = LogEmitterProvider() + provider.add_log_processor(log_processor) + + emitter = provider.get_log_emitter(__name__) + logger = logging.getLogger("shutdown") + logger.addHandler(OTLPHandler(log_emitter=emitter)) + + logger.warning("warning message: %s", "possible upcoming heatwave") + logger.error("Very high rise in temperatures across the globe") + logger.critical("Temparature hits high 420 C in Hyderabad") + + log_processor.shutdown() + self.assertTrue(exporter._stopped) + + finished_logs = exporter.get_finished_logs() + expected = [ + ("warning message: possible upcoming heatwave", "WARNING"), + ("Very high rise in temperatures across the globe", "ERROR"), + ( + "Temparature hits high 420 C in Hyderabad", + "CRITICAL", + ), + ] + emitted = [ + (item.log_record.body, item.log_record.severity_text) + for item in finished_logs + ] + self.assertEqual(expected, emitted) + + def test_force_flush(self): + exporter = InMemoryLogExporter() + log_processor = BatchLogProcessor(exporter) + + provider = LogEmitterProvider() + provider.add_log_processor(log_processor) + + emitter = provider.get_log_emitter(__name__) + logger = logging.getLogger("force_flush") + logger.addHandler(OTLPHandler(log_emitter=emitter)) + + logger.critical("Earth is burning") + log_processor.force_flush() + finished_logs = exporter.get_finished_logs() + self.assertEqual(len(finished_logs), 1) + log_record = finished_logs[0].log_record + self.assertEqual(log_record.body, "Earth is burning") + self.assertEqual(log_record.severity_number, SeverityNumber.FATAL) + + def test_log_processor_too_many_logs(self): + exporter = InMemoryLogExporter() + log_processor = BatchLogProcessor(exporter) + + provider = LogEmitterProvider() + provider.add_log_processor(log_processor) + + emitter = provider.get_log_emitter(__name__) + logger = logging.getLogger("many_logs") + logger.addHandler(OTLPHandler(log_emitter=emitter)) + + for log_no in range(1000): + logger.critical("Log no: %s", log_no) + + self.assertTrue(log_processor.force_flush()) + finised_logs = exporter.get_finished_logs() + self.assertEqual(len(finised_logs), 1000) + + def test_with_multiple_threads(self): + exporter = InMemoryLogExporter() + log_processor = BatchLogProcessor(exporter) + + provider = LogEmitterProvider() + provider.add_log_processor(log_processor) + + emitter = provider.get_log_emitter(__name__) + logger = logging.getLogger("threads") + logger.addHandler(OTLPHandler(log_emitter=emitter)) + + def bulk_log_and_flush(num_logs): + for _ in range(num_logs): + logger.critical("Critical message") + self.assertTrue(log_processor.force_flush()) + + with ThreadPoolExecutor(max_workers=69) as executor: + futures = [] + for idx in range(69): + future = executor.submit(bulk_log_and_flush, idx + 1) + futures.append(future) + + executor.shutdown() + + finished_logs = exporter.get_finished_logs() + self.assertEqual(len(finished_logs), 2415) diff --git a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py new file mode 100644 index 0000000000..a3d095077a --- /dev/null +++ b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py @@ -0,0 +1,194 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint:disable=protected-access,no-self-use,no-member + +import logging +import threading +import time +import unittest +from abc import ABC, abstractmethod +from unittest.mock import Mock + +from opentelemetry.sdk.logs import ( + ConcurrentMultiLogProcessor, + LogEmitterProvider, + LogProcessor, + LogRecord, + OTLPHandler, + SynchronousMultiLogProcessor, +) +from opentelemetry.sdk.logs.severity import SeverityNumber + + +class AnotherLogProcessor(LogProcessor): + def __init__(self, exporter, logs_list): + self._exporter = exporter + self._log_list = logs_list + self._closed = False + + def emit(self, log_data): + if self._closed: + return + self._log_list.append( + (log_data.log_record.body, log_data.log_record.severity_text) + ) + + def shutdown(self): + self._closed = True + self._exporter.shutdown() + + def force_flush(self, timeout_millis=30000): + self._log_list.clear() + return True + + +class TestLogProcessor(unittest.TestCase): + def test_log_processor(self): + provider = LogEmitterProvider() + log_emitter = provider.get_log_emitter(__name__) + handler = OTLPHandler(log_emitter=log_emitter) + + logs_list_1 = [] + processor1 = AnotherLogProcessor(Mock(), logs_list_1) + logs_list_2 = [] + processor2 = AnotherLogProcessor(Mock(), logs_list_2) + + logger = logging.getLogger("test.span.processor") + logger.addHandler(handler) + + # Test no proessor added + logger.critical("Odisha, we have another major cyclone") + + self.assertEqual(len(logs_list_1), 0) + self.assertEqual(len(logs_list_2), 0) + + # Add one processor + provider.add_log_processor(processor1) + logger.warning("Brace yourself") + logger.error("Some error message") + + expected_list_1 = [ + ("Brace yourself", "WARNING"), + ("Some error message", "ERROR"), + ] + self.assertEqual(logs_list_1, expected_list_1) + + # Add another processor + provider.add_log_processor(processor2) + logger.critical("Something disastrous") + expected_list_1.append(("Something disastrous", "CRITICAL")) + + expected_list_2 = [("Something disastrous", "CRITICAL")] + + self.assertEqual(logs_list_1, expected_list_1) + self.assertEqual(logs_list_2, expected_list_2) + + +class MultiLogProcessorTestBase(ABC): + @abstractmethod + def _get_multi_log_processor(self): + pass + + def make_record(self): + return LogRecord( + timestamp=1622300111608942000, + severity_text="WARNING", + severity_number=SeverityNumber.WARN, + body="Warning message", + ) + + def test_on_emit(self): + multi_log_processor = self._get_multi_log_processor() + mocks = [Mock(spec=LogProcessor) for _ in range(5)] + for mock in mocks: + multi_log_processor.add_log_processor(mock) + record = self.make_record() + multi_log_processor.emit(record) + for mock in mocks: + mock.emit.assert_called_with(record) + multi_log_processor.shutdown() + + def test_on_shutdown(self): + multi_log_processor = self._get_multi_log_processor() + mocks = [Mock(spec=LogProcessor) for _ in range(5)] + for mock in mocks: + multi_log_processor.add_log_processor(mock) + multi_log_processor.shutdown() + for mock in mocks: + mock.shutdown.assert_called_once_with() + + def test_on_force_flush(self): + multi_log_processor = self._get_multi_log_processor() + mocks = [Mock(spec=LogProcessor) for _ in range(5)] + for mock in mocks: + multi_log_processor.add_log_processor(mock) + ret_value = multi_log_processor.force_flush(100) + + self.assertTrue(ret_value) + for mock_processor in mocks: + self.assertEqual(1, mock_processor.force_flush.call_count) + + +class TestSynchronousMultiLogProcessor( + MultiLogProcessorTestBase, unittest.TestCase +): + def _get_multi_log_processor(self): + return SynchronousMultiLogProcessor() + + def test_force_flush_delayed(self): + multi_log_processor = SynchronousMultiLogProcessor() + + def delay(_): + time.sleep(0.09) + + mock_processor1 = Mock(spec=LogProcessor) + mock_processor1.force_flush = Mock(side_effect=delay) + multi_log_processor.add_log_processor(mock_processor1) + mock_processor2 = Mock(spec=LogProcessor) + multi_log_processor.add_log_processor(mock_processor2) + + ret_value = multi_log_processor.force_flush(50) + self.assertFalse(ret_value) + self.assertEqual(mock_processor1.force_flush.call_count, 1) + self.assertEqual(mock_processor2.force_flush.call_count, 0) + + +class TestConcurrentMultiLogProcessor( + MultiLogProcessorTestBase, unittest.TestCase +): + def _get_multi_log_processor(self): + return ConcurrentMultiLogProcessor() + + def test_force_flush_delayed(self): + multi_log_processor = ConcurrentMultiLogProcessor() + wait_event = threading.Event() + + def delay(_): + wait_event.wait() + + mock1 = Mock(spec=LogProcessor) + mock1.force_flush = Mock(side_effect=delay) + mocks = [Mock(LogProcessor) for _ in range(5)] + mocks = [mock1] + mocks + for mock_processor in mocks: + multi_log_processor.add_log_processor(mock_processor) + + ret_value = multi_log_processor.force_flush(50) + wait_event.set() + + self.assertFalse(ret_value) + for mock in mocks: + self.assertEqual(1, mock.force_flush.call_count) + multi_log_processor.shutdown() From 6aa92f13673ec49a67b5f9e2970c7751a852c19b Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Wed, 14 Jul 2021 13:16:44 -0500 Subject: [PATCH 1039/1517] Fix typos in test_handler.py (#1953) --- opentelemetry-sdk/tests/logs/test_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 1d1b84f0fd..d9d9566d2a 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -37,7 +37,7 @@ def test_handler_default_log_level(self): logger.debug("Debug message") self.assertEqual(emitter_mock.emit.call_count, 0) # Assert emit gets called for warning message - logger.warning("Wanrning message") + logger.warning("Warning message") self.assertEqual(emitter_mock.emit.call_count, 1) def test_handler_custom_log_level(self): @@ -54,7 +54,7 @@ def test_log_record_no_span_context(self): emitter_mock = Mock(spec=LogEmitter) logger = get_logger(log_emitter=emitter_mock) # Assert emit gets called for warning message - logger.warning("Wanrning message") + logger.warning("Warning message") args, _ = emitter_mock.emit.call_args_list[0] log_record = args[0] From e5d4224a6382d57bc315ee84372709b9a8ac9735 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 29 Jul 2021 23:05:25 +0530 Subject: [PATCH 1040/1517] Add support for OTLP Log exporter (#1943) --- .../otlp/proto/grpc/log_exporter/__init__.py | 188 +++++++ .../tests/logs/__init__.py | 0 .../tests/logs/test_otlp_logs_exporter.py | 489 ++++++++++++++++++ 3 files changed, 677 insertions(+) create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py new file mode 100644 index 0000000000..cd548f15c8 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py @@ -0,0 +1,188 @@ +# Copyright The OpenTelemetry Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional, Sequence +from grpc import ChannelCredentials, Compression +from opentelemetry.exporter.otlp.proto.grpc.exporter import ( + OTLPExporterMixin, + _translate_key_values, + get_resource_data, + _translate_value, +) +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, +) +from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( + LogsServiceStub, +) +from opentelemetry.proto.common.v1.common_pb2 import InstrumentationLibrary +from opentelemetry.proto.logs.v1.logs_pb2 import ( + InstrumentationLibraryLogs, + ResourceLogs, + SeverityNumber, +) +from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord +from opentelemetry.sdk.logs import LogRecord as SDKLogRecord +from opentelemetry.sdk.logs import LogData +from opentelemetry.sdk.logs.export import LogExporter, LogExportResult + + +class OTLPLogExporter( + LogExporter, + OTLPExporterMixin[SDKLogRecord, ExportLogsServiceRequest, LogExportResult], +): + + _result = LogExportResult + _stub = LogsServiceStub + + def __init__( + self, + endpoint: Optional[str] = None, + insecure: Optional[bool] = None, + credentials: Optional[ChannelCredentials] = None, + headers: Optional[Sequence] = None, + timeout: Optional[int] = None, + compression: Optional[Compression] = None, + ): + super().__init__( + **{ + "endpoint": endpoint, + "insecure": insecure, + "credentials": credentials, + "headers": headers, + "timeout": timeout, + "compression": compression, + } + ) + + def _translate_name(self, log_data: LogData) -> None: + self._collector_log_kwargs["name"] = log_data.log_record.name + + def _translate_time(self, log_data: LogData) -> None: + self._collector_log_kwargs[ + "time_unix_nano" + ] = log_data.log_record.timestamp + + def _translate_span_id(self, log_data: LogData) -> None: + self._collector_log_kwargs[ + "span_id" + ] = log_data.log_record.span_id.to_bytes(8, "big") + + def _translate_trace_id(self, log_data: LogData) -> None: + self._collector_log_kwargs[ + "trace_id" + ] = log_data.log_record.trace_id.to_bytes(16, "big") + + def _translate_trace_flags(self, log_data: LogData) -> None: + self._collector_log_kwargs["flags"] = int( + log_data.log_record.trace_flags + ) + + def _translate_body(self, log_data: LogData): + self._collector_log_kwargs["body"] = _translate_value( + log_data.log_record.body + ) + + def _translate_severity_text(self, log_data: LogData): + self._collector_log_kwargs[ + "severity_text" + ] = log_data.log_record.severity_text + + def _translate_attributes(self, log_data: LogData) -> None: + if log_data.log_record.attributes: + self._collector_log_kwargs["attributes"] = [] + for key, value in log_data.log_record.attributes.items(): + try: + self._collector_log_kwargs["attributes"].append( + _translate_key_values(key, value) + ) + except Exception: # pylint: disable=broad-except + pass + + def _translate_data( + self, data: Sequence[LogData] + ) -> ExportLogsServiceRequest: + # pylint: disable=attribute-defined-outside-init + + sdk_resource_instrumentation_library_logs = {} + + for log_data in data: + resource = log_data.log_record.resource + + instrumentation_library_logs_map = ( + sdk_resource_instrumentation_library_logs.get(resource, {}) + ) + if not instrumentation_library_logs_map: + sdk_resource_instrumentation_library_logs[ + resource + ] = instrumentation_library_logs_map + + instrumentation_library_logs = ( + instrumentation_library_logs_map.get( + log_data.instrumentation_info + ) + ) + if not instrumentation_library_logs: + if log_data.instrumentation_info is not None: + instrumentation_library_logs_map[ + log_data.instrumentation_info + ] = InstrumentationLibraryLogs( + instrumentation_library=InstrumentationLibrary( + name=log_data.instrumentation_info.name, + version=log_data.instrumentation_info.version, + ) + ) + else: + instrumentation_library_logs_map[ + log_data.instrumentation_info + ] = InstrumentationLibraryLogs() + + instrumentation_library_logs = ( + instrumentation_library_logs_map.get( + log_data.instrumentation_info + ) + ) + + self._collector_log_kwargs = {} + + self._translate_name(log_data) + self._translate_time(log_data) + self._translate_span_id(log_data) + self._translate_trace_id(log_data) + self._translate_trace_flags(log_data) + self._translate_body(log_data) + self._translate_severity_text(log_data) + self._translate_attributes(log_data) + + self._collector_log_kwargs["severity_number"] = getattr( + SeverityNumber, + "SEVERITY_NUMBER_{}".format(log_data.log_record.severity_text), + ) + + instrumentation_library_logs.logs.append( + PB2LogRecord(**self._collector_log_kwargs) + ) + + return ExportLogsServiceRequest( + resource_logs=get_resource_data( + sdk_resource_instrumentation_library_logs, + ResourceLogs, + "logs", + ) + ) + + def export(self, batch: Sequence[LogData]) -> LogExportResult: + return self._export(batch) + + def shutdown(self) -> None: + pass diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py new file mode 100644 index 0000000000..4a898cb1bb --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -0,0 +1,489 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from concurrent.futures import ThreadPoolExecutor +from unittest import TestCase +from unittest.mock import patch + +from google.protobuf.duration_pb2 import Duration +from google.rpc.error_details_pb2 import RetryInfo +from grpc import StatusCode, server + +from opentelemetry.exporter.otlp.proto.grpc.exporter import _translate_value +from opentelemetry.exporter.otlp.proto.grpc.log_exporter import OTLPLogExporter +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, + ExportLogsServiceResponse, +) +from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( + LogsServiceServicer, + add_LogsServiceServicer_to_server, +) +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue, + InstrumentationLibrary, + KeyValue, +) +from opentelemetry.proto.logs.v1.logs_pb2 import InstrumentationLibraryLogs +from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord +from opentelemetry.proto.logs.v1.logs_pb2 import ResourceLogs, SeverityNumber +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as OTLPResource, +) +from opentelemetry.sdk.logs import LogData, LogRecord +from opentelemetry.sdk.logs.export import LogExportResult +from opentelemetry.sdk.logs.severity import SeverityNumber as SDKSeverityNumber +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.trace import TraceFlags + + +class LogsServiceServicerUNAVAILABLEDelay(LogsServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.UNAVAILABLE) + + context.send_initial_metadata( + (("google.rpc.retryinfo-bin", RetryInfo().SerializeToString()),) + ) + context.set_trailing_metadata( + ( + ( + "google.rpc.retryinfo-bin", + RetryInfo( + retry_delay=Duration(seconds=4) + ).SerializeToString(), + ), + ) + ) + + return ExportLogsServiceResponse() + + +class LogsServiceServicerUNAVAILABLE(LogsServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.UNAVAILABLE) + + return ExportLogsServiceResponse() + + +class LogsServiceServicerSUCCESS(LogsServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.OK) + + return ExportLogsServiceResponse() + + +class LogsServiceServicerALREADY_EXISTS(LogsServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.ALREADY_EXISTS) + + return ExportLogsServiceResponse() + + +class TestOTLPLogExporter(TestCase): + def setUp(self): + + self.exporter = OTLPLogExporter() + + self.server = server(ThreadPoolExecutor(max_workers=10)) + + self.server.add_insecure_port("[::]:4317") + + self.server.start() + + self.log_data_1 = LogData( + log_record=LogRecord( + timestamp=int(time.time() * 1e9), + trace_id=2604504634922341076776623263868986797, + span_id=5213367945872657620, + trace_flags=TraceFlags(0x01), + severity_text="WARN", + severity_number=SDKSeverityNumber.WARN, + name="name", + body="Zhengzhou, We have a heaviest rains in 1000 years", + resource=SDKResource({"key": "value"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_info=InstrumentationInfo( + "first_name", "first_version" + ), + ) + self.log_data_2 = LogData( + log_record=LogRecord( + timestamp=int(time.time() * 1e9), + trace_id=2604504634922341076776623263868986799, + span_id=5213367945872657623, + trace_flags=TraceFlags(0x01), + severity_text="INFO", + severity_number=SDKSeverityNumber.INFO2, + name="info name", + body="Sydney, Opera House is closed", + resource=SDKResource({"key": "value"}), + attributes={"custom_attr": [1, 2, 3]}, + ), + instrumentation_info=InstrumentationInfo( + "second_name", "second_version" + ), + ) + self.log_data_3 = LogData( + log_record=LogRecord( + timestamp=int(time.time() * 1e9), + trace_id=2604504634922341076776623263868986800, + span_id=5213367945872657628, + trace_flags=TraceFlags(0x01), + severity_text="ERROR", + severity_number=SDKSeverityNumber.WARN, + name="error name", + body="Mumbai, Boil water before drinking", + resource=SDKResource({"service": "myapp"}), + ), + instrumentation_info=InstrumentationInfo( + "third_name", "third_version" + ), + ) + + def tearDown(self): + self.server.stop(None) + + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" + ) + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.log_exporter.OTLPLogExporter._stub" + ) + # pylint: disable=unused-argument + def test_no_credentials_error( + self, mock_ssl_channel, mock_secure, mock_stub + ): + OTLPLogExporter(insecure=False) + self.assertTrue(mock_ssl_channel.called) + + # pylint: disable=no-self-use + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") + def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): + expected_endpoint = "localhost:4317" + endpoints = [ + ( + "http://localhost:4317", + None, + mock_insecure, + ), + ( + "localhost:4317", + None, + mock_insecure, + ), + ( + "localhost:4317", + False, + mock_secure, + ), + ( + "https://localhost:4317", + None, + mock_secure, + ), + ( + "https://localhost:4317", + True, + mock_insecure, + ), + ] + for endpoint, insecure, mock_method in endpoints: + OTLPLogExporter(endpoint=endpoint, insecure=insecure) + self.assertEqual( + 1, + mock_method.call_count, + "expected {} to be called for {} {}".format( + mock_method, endpoint, insecure + ), + ) + self.assertEqual( + expected_endpoint, + mock_method.call_args[0][0], + "expected {} got {} {}".format( + expected_endpoint, mock_method.call_args[0][0], endpoint + ), + ) + mock_method.reset_mock() + + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") + def test_unavailable(self, mock_sleep, mock_expo): + + mock_expo.configure_mock(**{"return_value": [1]}) + + add_LogsServiceServicer_to_server( + LogsServiceServicerUNAVAILABLE(), self.server + ) + self.assertEqual( + self.exporter.export([self.log_data_1]), LogExportResult.FAILURE + ) + mock_sleep.assert_called_with(1) + + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") + def test_unavailable_delay(self, mock_sleep, mock_expo): + + mock_expo.configure_mock(**{"return_value": [1]}) + + add_LogsServiceServicer_to_server( + LogsServiceServicerUNAVAILABLEDelay(), self.server + ) + self.assertEqual( + self.exporter.export([self.log_data_1]), LogExportResult.FAILURE + ) + mock_sleep.assert_called_with(4) + + def test_success(self): + add_LogsServiceServicer_to_server( + LogsServiceServicerSUCCESS(), self.server + ) + self.assertEqual( + self.exporter.export([self.log_data_1]), LogExportResult.SUCCESS + ) + + def test_failure(self): + add_LogsServiceServicer_to_server( + LogsServiceServicerALREADY_EXISTS(), self.server + ) + self.assertEqual( + self.exporter.export([self.log_data_1]), LogExportResult.FAILURE + ) + + def test_translate_log_data(self): + + expected = ExportLogsServiceRequest( + resource_logs=[ + ResourceLogs( + resource=OTLPResource( + attributes=[ + KeyValue( + key="key", value=AnyValue(string_value="value") + ), + ] + ), + instrumentation_library_logs=[ + InstrumentationLibraryLogs( + instrumentation_library=InstrumentationLibrary( + name="first_name", version="first_version" + ), + logs=[ + PB2LogRecord( + # pylint: disable=no-member + name="name", + time_unix_nano=self.log_data_1.log_record.timestamp, + severity_number=getattr( + SeverityNumber, + "SEVERITY_NUMBER_{}".format( + self.log_data_1.log_record.severity_text + ), + ), + severity_text="WARN", + span_id=int.to_bytes( + 5213367945872657620, 8, "big" + ), + trace_id=int.to_bytes( + 2604504634922341076776623263868986797, + 16, + "big", + ), + body=_translate_value( + "Zhengzhou, We have a heaviest rains in 1000 years" + ), + attributes=[ + KeyValue( + key="a", + value=AnyValue(int_value=1), + ), + KeyValue( + key="b", + value=AnyValue(string_value="c"), + ), + ], + flags=int( + self.log_data_1.log_record.trace_flags + ), + ) + ], + ) + ], + ), + ] + ) + + # pylint: disable=protected-access + self.assertEqual( + expected, self.exporter._translate_data([self.log_data_1]) + ) + + def test_translate_multiple_logs(self): + expected = ExportLogsServiceRequest( + resource_logs=[ + ResourceLogs( + resource=OTLPResource( + attributes=[ + KeyValue( + key="key", value=AnyValue(string_value="value") + ), + ] + ), + instrumentation_library_logs=[ + InstrumentationLibraryLogs( + instrumentation_library=InstrumentationLibrary( + name="first_name", version="first_version" + ), + logs=[ + PB2LogRecord( + # pylint: disable=no-member + name="name", + time_unix_nano=self.log_data_1.log_record.timestamp, + severity_number=getattr( + SeverityNumber, + "SEVERITY_NUMBER_{}".format( + self.log_data_1.log_record.severity_text + ), + ), + severity_text="WARN", + span_id=int.to_bytes( + 5213367945872657620, 8, "big" + ), + trace_id=int.to_bytes( + 2604504634922341076776623263868986797, + 16, + "big", + ), + body=_translate_value( + "Zhengzhou, We have a heaviest rains in 1000 years" + ), + attributes=[ + KeyValue( + key="a", + value=AnyValue(int_value=1), + ), + KeyValue( + key="b", + value=AnyValue(string_value="c"), + ), + ], + flags=int( + self.log_data_1.log_record.trace_flags + ), + ) + ], + ), + InstrumentationLibraryLogs( + instrumentation_library=InstrumentationLibrary( + name="second_name", version="second_version" + ), + logs=[ + PB2LogRecord( + # pylint: disable=no-member + name="info name", + time_unix_nano=self.log_data_2.log_record.timestamp, + severity_number=getattr( + SeverityNumber, + "SEVERITY_NUMBER_{}".format( + self.log_data_2.log_record.severity_text + ), + ), + severity_text="INFO", + span_id=int.to_bytes( + 5213367945872657623, 8, "big" + ), + trace_id=int.to_bytes( + 2604504634922341076776623263868986799, + 16, + "big", + ), + body=_translate_value( + "Sydney, Opera House is closed" + ), + attributes=[ + KeyValue( + key="custom_attr", + value=_translate_value([1, 2, 3]), + ), + ], + flags=int( + self.log_data_2.log_record.trace_flags + ), + ) + ], + ), + ], + ), + ResourceLogs( + resource=OTLPResource( + attributes=[ + KeyValue( + key="service", + value=AnyValue(string_value="myapp"), + ), + ] + ), + instrumentation_library_logs=[ + InstrumentationLibraryLogs( + instrumentation_library=InstrumentationLibrary( + name="third_name", version="third_version" + ), + logs=[ + PB2LogRecord( + # pylint: disable=no-member + name="error name", + time_unix_nano=self.log_data_3.log_record.timestamp, + severity_number=getattr( + SeverityNumber, + "SEVERITY_NUMBER_{}".format( + self.log_data_3.log_record.severity_text + ), + ), + severity_text="ERROR", + span_id=int.to_bytes( + 5213367945872657628, 8, "big" + ), + trace_id=int.to_bytes( + 2604504634922341076776623263868986800, + 16, + "big", + ), + body=_translate_value( + "Mumbai, Boil water before drinking" + ), + attributes=[], + flags=int( + self.log_data_3.log_record.trace_flags + ), + ) + ], + ) + ], + ), + ] + ) + + # pylint: disable=protected-access + self.assertEqual( + expected, + self.exporter._translate_data( + [self.log_data_1, self.log_data_2, self.log_data_3] + ), + ) From 914ee519abdc94c54c4afe0270ce5dd40dcf6b8e Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Mon, 16 Aug 2021 12:27:47 -0500 Subject: [PATCH 1041/1517] Add support for user defined attributes in OTLPHandler (#1952) --- CHANGELOG.md | 2 + .../src/opentelemetry/sdk/logs/__init__.py | 45 +++++++++++++++++-- opentelemetry-sdk/tests/logs/test_handler.py | 12 +++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f1c2529fc..5f4c305ae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) ### Added +- Give OTLPHandler the ability to process attributes + ([#1952](https://github.com/open-telemetry/opentelemetry-python/pull/1952)) - Add global LogEmitterProvider and convenience function get_log_emitter ([#1901](https://github.com/open-telemetry/opentelemetry-python/pull/1901)) - Add OTLPHandler for standard library logging module diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py index 8b0da9e22a..02c22578f5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -244,20 +244,59 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: return True +# skip natural LogRecord attributes +# http://docs.python.org/library/logging.html#logrecord-attributes +_RESERVED_ATTRS = frozenset( + ( + "asctime", + "args", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "getMessage", + "levelname", + "levelno", + "lineno", + "module", + "msecs", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack_info", + "thread", + "threadName", + ) +) + + class OTLPHandler(logging.Handler): """A handler class which writes logging records, in OTLP format, to a network destination or file. """ - def __init__(self, level=logging.NOTSET, log_emitter=None) -> None: + def __init__( + self, + level=logging.NOTSET, + log_emitter=None, + ) -> None: super().__init__(level=level) self._log_emitter = log_emitter or get_log_emitter(__name__) + @staticmethod + def _get_attributes(record: logging.LogRecord) -> Attributes: + return { + k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS + } + def _translate(self, record: logging.LogRecord) -> LogRecord: timestamp = int(record.created * 1e9) span_context = get_current_span().get_span_context() - # TODO: attributes (or resource attributes?) from record metadata - attributes: Attributes = {} + attributes = self._get_attributes(record) severity_number = std_to_otlp(record.levelno) return LogRecord( timestamp=timestamp, diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index d9d9566d2a..474a87fe8d 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -65,6 +65,18 @@ def test_log_record_no_span_context(self): log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags ) + def test_log_record_user_attributes(self): + """Attributes can be injected into logs by adding them to the LogRecord""" + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(log_emitter=emitter_mock) + # Assert emit gets called for warning message + logger.warning("Warning message", extra={"http.status_code": 200}) + args, _ = emitter_mock.emit.call_args_list[0] + log_record = args[0] + + self.assertIsNotNone(log_record) + self.assertEqual(log_record.attributes, {"http.status_code": 200}) + def test_log_record_trace_correlation(self): emitter_mock = Mock(spec=LogEmitter) logger = get_logger(log_emitter=emitter_mock) From 4714a4edae576d2a619958dbd7405a3e564b0df0 Mon Sep 17 00:00:00 2001 From: alrex Date: Sun, 19 Sep 2021 17:07:27 -0700 Subject: [PATCH 1042/1517] use timeout in force_flush (#2118) * use timeout in force_flush * fix lint * Update opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py Co-authored-by: Srikanth Chekuri * fix lint Co-authored-by: Srikanth Chekuri --- opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py index 167e487102..31dff4fa09 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py @@ -266,7 +266,7 @@ def force_flush(self, timeout_millis: Optional[int] = None) -> bool: flush_request = self._get_or_create_flush_request() self._condition.notify_all() - ret = flush_request.event.wait() + ret = flush_request.event.wait(timeout_millis / 1e3) if not ret: _logger.warning("Timeout was exceeded in force_flush().") return ret From c01f308350e6c02b23a1ccd9c3a194de66bd3bb1 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 22 Sep 2021 17:00:12 -0700 Subject: [PATCH 1043/1517] add a ConsoleExporter for logging (#2099) Co-authored-by: Srikanth Chekuri --- .../src/opentelemetry/sdk/logs/__init__.py | 27 ++++++- .../opentelemetry/sdk/logs/export/__init__.py | 33 ++++++++- opentelemetry-sdk/tests/logs/test_export.py | 72 ++++++++++++++++++- 3 files changed, 126 insertions(+), 6 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py index 02c22578f5..1a66413a15 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -15,6 +15,7 @@ import abc import atexit import concurrent.futures +import json import logging import os import threading @@ -25,8 +26,13 @@ ) from opentelemetry.sdk.logs.severity import SeverityNumber, std_to_otlp from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -from opentelemetry.trace import get_current_span +from opentelemetry.trace import ( + format_span_id, + format_trace_id, + get_current_span, +) from opentelemetry.trace.span import TraceFlags from opentelemetry.util._providers import _load_provider from opentelemetry.util._time import _time_ns @@ -72,6 +78,25 @@ def __eq__(self, other: object) -> bool: return NotImplemented return self.__dict__ == other.__dict__ + def to_json(self) -> str: + return json.dumps( + { + "body": self.body, + "name": self.name, + "severity_number": repr(self.severity_number), + "severity_text": self.severity_text, + "attributes": self.attributes, + "timestamp": ns_to_iso_str(self.timestamp), + "trace_id": "0x{}".format(format_trace_id(self.trace_id)), + "span_id": "0x{}".format(format_span_id(self.span_id)), + "trace_flags": self.trace_flags, + "resource": repr(self.resource.attributes) + if self.resource + else "", + }, + indent=4, + ) + class LogData: """Readable LogRecord data plus associated InstrumentationLibrary.""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py index 31dff4fa09..f831edc1d0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py @@ -16,11 +16,13 @@ import collections import enum import logging +import sys import threading -from typing import Deque, List, Optional, Sequence +from os import linesep +from typing import IO, Callable, Deque, List, Optional, Sequence from opentelemetry.context import attach, detach, set_value -from opentelemetry.sdk.logs import LogData, LogProcessor +from opentelemetry.sdk.logs import LogData, LogProcessor, LogRecord from opentelemetry.util._time import _time_ns _logger = logging.getLogger(__name__) @@ -60,6 +62,33 @@ def shutdown(self): """ +class ConsoleExporter(LogExporter): + """Implementation of :class:`LogExporter` that prints log records to the + console. + + This class can be used for diagnostic purposes. It prints the exported + log records to the console STDOUT. + """ + + def __init__( + self, + out: IO = sys.stdout, + formatter: Callable[[LogRecord], str] = lambda record: record.to_json() + + linesep, + ): + self.out = out + self.formatter = formatter + + def export(self, batch: Sequence[LogData]): + for data in batch: + self.out.write(self.formatter(data.log_record)) + self.out.flush() + return LogExportResult.SUCCESS + + def shutdown(self): + pass + + class SimpleLogProcessor(LogProcessor): """This is an implementation of LogProcessor which passes received logs in the export-friendly LogData representation to the diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index f1e97c6a02..51eaeb3d89 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -14,17 +14,31 @@ # pylint: disable=protected-access import logging +import os +import time import unittest from concurrent.futures import ThreadPoolExecutor -from unittest.mock import Mock +from unittest.mock import Mock, patch from opentelemetry.sdk import trace -from opentelemetry.sdk.logs import LogEmitterProvider, OTLPHandler -from opentelemetry.sdk.logs.export import BatchLogProcessor, SimpleLogProcessor +from opentelemetry.sdk.logs import ( + LogData, + LogEmitterProvider, + LogRecord, + OTLPHandler, +) +from opentelemetry.sdk.logs.export import ( + BatchLogProcessor, + ConsoleExporter, + SimpleLogProcessor, +) from opentelemetry.sdk.logs.export.in_memory_log_exporter import ( InMemoryLogExporter, ) from opentelemetry.sdk.logs.severity import SeverityNumber +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.trace import TraceFlags from opentelemetry.trace.span import INVALID_SPAN_CONTEXT @@ -254,3 +268,55 @@ def bulk_log_and_flush(num_logs): finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 2415) + + +class TestConsoleExporter(unittest.TestCase): + def test_export(self): # pylint: disable=no-self-use + """Check that the console exporter prints log records.""" + log_data = LogData( + log_record=LogRecord( + timestamp=int(time.time() * 1e9), + trace_id=2604504634922341076776623263868986797, + span_id=5213367945872657620, + trace_flags=TraceFlags(0x01), + severity_text="WARN", + severity_number=SeverityNumber.WARN, + name="name", + body="Zhengzhou, We have a heaviest rains in 1000 years", + resource=SDKResource({"key": "value"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_info=InstrumentationInfo( + "first_name", "first_version" + ), + ) + exporter = ConsoleExporter() + # Mocking stdout interferes with debugging and test reporting, mock on + # the exporter instance instead. + + with patch.object(exporter, "out") as mock_stdout: + exporter.export([log_data]) + mock_stdout.write.assert_called_once_with( + log_data.log_record.to_json() + os.linesep + ) + + self.assertEqual(mock_stdout.write.call_count, 1) + self.assertEqual(mock_stdout.flush.call_count, 1) + + def test_export_custom(self): # pylint: disable=no-self-use + """Check that console exporter uses custom io, formatter.""" + mock_record_str = Mock(str) + + def formatter(record): # pylint: disable=unused-argument + return mock_record_str + + mock_stdout = Mock() + exporter = ConsoleExporter(out=mock_stdout, formatter=formatter) + log_data = LogData( + log_record=LogRecord(), + instrumentation_info=InstrumentationInfo( + "first_name", "first_version" + ), + ) + exporter.export([log_data]) + mock_stdout.write.assert_called_once_with(mock_record_str) From af3e553b73a1d2593d61cd32246447978f5ee663 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 23 Sep 2021 22:43:45 +0530 Subject: [PATCH 1044/1517] Update SDK docs and Add example with OTEL collector logging (debug) exporter (#2050) --- docs/examples/logs/README.rst | 75 +++++++++++++++++++ docs/examples/logs/example.py | 46 ++++++++++++ docs/examples/logs/otel-collector-config.yaml | 10 +++ docs/sdk/logs.export.rst | 7 ++ docs/sdk/logs.rst | 15 ++++ docs/sdk/logs.severity.rst | 7 ++ docs/sdk/sdk.rst | 1 + .../src/opentelemetry/sdk/logs/severity.py | 3 +- 8 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 docs/examples/logs/README.rst create mode 100644 docs/examples/logs/example.py create mode 100644 docs/examples/logs/otel-collector-config.yaml create mode 100644 docs/sdk/logs.export.rst create mode 100644 docs/sdk/logs.rst create mode 100644 docs/sdk/logs.severity.rst diff --git a/docs/examples/logs/README.rst b/docs/examples/logs/README.rst new file mode 100644 index 0000000000..3c19c2eafe --- /dev/null +++ b/docs/examples/logs/README.rst @@ -0,0 +1,75 @@ +OpenTelemetry Logs SDK +====================== + +Start the Collector locally to see data being exported. Write the following file: + +.. code-block:: yaml + + # otel-collector-config.yaml + receivers: + otlp: + protocols: + grpc: + + exporters: + logging: + + processors: + batch: + +Then start the Docker container: + +.. code-block:: sh + + docker run \ + -p 4317:4317 \ + -v $(pwd)/otel-collector-config.yaml:/etc/otel/config.yaml \ + otel/opentelemetry-collector-contrib:latest + +.. code-block:: sh + + $ python example.py + +The resulting logs will appear in the output from the collector and look similar to this: + +.. code-block:: sh + + ResourceLog #0 + Resource labels: + -> telemetry.sdk.language: STRING(python) + -> telemetry.sdk.name: STRING(opentelemetry) + -> telemetry.sdk.version: STRING(1.5.0.dev0) + -> service.name: STRING(unknown_service) + InstrumentationLibraryLogs #0 + InstrumentationLibrary __main__ 0.1 + LogRecord #0 + Timestamp: 2021-08-18 08:26:53.837349888 +0000 UTC + Severity: ERROR + ShortName: + Body: Exception while exporting logs. + ResourceLog #1 + Resource labels: + -> telemetry.sdk.language: STRING(python) + -> telemetry.sdk.name: STRING(opentelemetry) + -> telemetry.sdk.version: STRING(1.5.0.dev0) + -> service.name: STRING(unknown_service) + InstrumentationLibraryLogs #0 + InstrumentationLibrary __main__ 0.1 + LogRecord #0 + Timestamp: 2021-08-18 08:26:53.842546944 +0000 UTC + Severity: ERROR + ShortName: + Body: The five boxing wizards jump quickly. + ResourceLog #2 + Resource labels: + -> telemetry.sdk.language: STRING(python) + -> telemetry.sdk.name: STRING(opentelemetry) + -> telemetry.sdk.version: STRING(1.5.0.dev0) + -> service.name: STRING(unknown_service) + InstrumentationLibraryLogs #0 + InstrumentationLibrary __main__ 0.1 + LogRecord #0 + Timestamp: 2021-08-18 08:26:53.843979008 +0000 UTC + Severity: ERROR + ShortName: + Body: Hyderabad, we have a major problem. \ No newline at end of file diff --git a/docs/examples/logs/example.py b/docs/examples/logs/example.py new file mode 100644 index 0000000000..5cf4ed838c --- /dev/null +++ b/docs/examples/logs/example.py @@ -0,0 +1,46 @@ +import logging + +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.grpc.log_exporter import OTLPLogExporter +from opentelemetry.sdk.logs import OTLPHandler, get_log_emitter_provider +from opentelemetry.sdk.logs.export import SimpleLogProcessor +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleSpanProcessor, +) + +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor( + SimpleSpanProcessor(ConsoleSpanExporter()) +) + +log_emitter_provider = get_log_emitter_provider() +exporter = OTLPLogExporter(insecure=True) +log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) +log_emitter = log_emitter_provider.get_log_emitter(__name__, "0.1") +handler = OTLPHandler(level=logging.NOTSET, log_emitter=log_emitter) + +# Attach OTLP handler to root logger +logging.getLogger("root").addHandler(handler) + +# Log directly +logging.info("Jackdaws love my big sphinx of quartz.") + +# Create different namespaced loggers +logger1 = logging.getLogger("myapp.area1") +logger2 = logging.getLogger("myapp.area2") + +logger1.debug("Quick zephyrs blow, vexing daft Jim.") +logger1.info("How quickly daft jumping zebras vex.") +logger2.warning("Jail zesty vixen who grabbed pay from quack.") +logger2.error("The five boxing wizards jump quickly.") + + +# Trace context correlation +tracer = trace.get_tracer(__name__) +with tracer.start_as_current_span("foo"): + # Do something + logger2.error("Hyderabad, we have a major problem.") + +log_emitter_provider.shutdown() diff --git a/docs/examples/logs/otel-collector-config.yaml b/docs/examples/logs/otel-collector-config.yaml new file mode 100644 index 0000000000..f29ce6476c --- /dev/null +++ b/docs/examples/logs/otel-collector-config.yaml @@ -0,0 +1,10 @@ +receivers: + otlp: + protocols: + grpc: + +exporters: + logging: + +processors: + batch: diff --git a/docs/sdk/logs.export.rst b/docs/sdk/logs.export.rst new file mode 100644 index 0000000000..d247e4db72 --- /dev/null +++ b/docs/sdk/logs.export.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.logs.export +============================= + +.. automodule:: opentelemetry.sdk.logs.export + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/sdk/logs.rst b/docs/sdk/logs.rst new file mode 100644 index 0000000000..7eb6f93264 --- /dev/null +++ b/docs/sdk/logs.rst @@ -0,0 +1,15 @@ +opentelemetry.sdk.logs package +=============================== + +Submodules +---------- + +.. toctree:: + + logs.export + logs.severity + +.. automodule:: opentelemetry.sdk.logs + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sdk/logs.severity.rst b/docs/sdk/logs.severity.rst new file mode 100644 index 0000000000..bcf30cf361 --- /dev/null +++ b/docs/sdk/logs.severity.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.logs.severity +=============================== + +.. automodule:: opentelemetry.sdk.logs.severity + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/sdk/sdk.rst b/docs/sdk/sdk.rst index 333da1820b..619f3bd8cc 100644 --- a/docs/sdk/sdk.rst +++ b/docs/sdk/sdk.rst @@ -8,5 +8,6 @@ OpenTelemetry Python SDK resources trace + logs error_handler environment_variables diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py index c0509ea2c1..2570375990 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py @@ -25,8 +25,7 @@ class SeverityNumber(enum.Enum): See the `Log Data Model`_ spec for more info and how to map the severity from source format to OTLP Model. - .. _Log Data Model: - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber + .. _Log Data Model: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber """ UNSPECIFIED = 0 From bf98c6d3ce3b27513f8c2b7f03c6149670e6be63 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 15 Oct 2021 17:19:49 +0530 Subject: [PATCH 1045/1517] Fix exception in severity number transformation (#2208) * Fix exception with warning message transformation * Fix lint * Fix lint --- .../otlp/proto/grpc/log_exporter/__init__.py | 8 ++--- .../tests/logs/test_otlp_logs_exporter.py | 36 +++++-------------- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py index cd548f15c8..ecf9e16e8f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py @@ -29,7 +29,6 @@ from opentelemetry.proto.logs.v1.logs_pb2 import ( InstrumentationLibraryLogs, ResourceLogs, - SeverityNumber, ) from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord from opentelemetry.sdk.logs import LogRecord as SDKLogRecord @@ -164,10 +163,9 @@ def _translate_data( self._translate_severity_text(log_data) self._translate_attributes(log_data) - self._collector_log_kwargs["severity_number"] = getattr( - SeverityNumber, - "SEVERITY_NUMBER_{}".format(log_data.log_record.severity_text), - ) + self._collector_log_kwargs[ + "severity_number" + ] = log_data.log_record.severity_number.value instrumentation_library_logs.logs.append( PB2LogRecord(**self._collector_log_kwargs) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index 4a898cb1bb..866d1a03b7 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -38,7 +38,7 @@ ) from opentelemetry.proto.logs.v1.logs_pb2 import InstrumentationLibraryLogs from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord -from opentelemetry.proto.logs.v1.logs_pb2 import ResourceLogs, SeverityNumber +from opentelemetry.proto.logs.v1.logs_pb2 import ResourceLogs from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as OTLPResource, ) @@ -113,7 +113,7 @@ def setUp(self): trace_id=2604504634922341076776623263868986797, span_id=5213367945872657620, trace_flags=TraceFlags(0x01), - severity_text="WARN", + severity_text="WARNING", severity_number=SDKSeverityNumber.WARN, name="name", body="Zhengzhou, We have a heaviest rains in 1000 years", @@ -291,13 +291,8 @@ def test_translate_log_data(self): # pylint: disable=no-member name="name", time_unix_nano=self.log_data_1.log_record.timestamp, - severity_number=getattr( - SeverityNumber, - "SEVERITY_NUMBER_{}".format( - self.log_data_1.log_record.severity_text - ), - ), - severity_text="WARN", + severity_number=self.log_data_1.log_record.severity_number.value, + severity_text="WARNING", span_id=int.to_bytes( 5213367945872657620, 8, "big" ), @@ -356,13 +351,8 @@ def test_translate_multiple_logs(self): # pylint: disable=no-member name="name", time_unix_nano=self.log_data_1.log_record.timestamp, - severity_number=getattr( - SeverityNumber, - "SEVERITY_NUMBER_{}".format( - self.log_data_1.log_record.severity_text - ), - ), - severity_text="WARN", + severity_number=self.log_data_1.log_record.severity_number.value, + severity_text="WARNING", span_id=int.to_bytes( 5213367945872657620, 8, "big" ), @@ -399,12 +389,7 @@ def test_translate_multiple_logs(self): # pylint: disable=no-member name="info name", time_unix_nano=self.log_data_2.log_record.timestamp, - severity_number=getattr( - SeverityNumber, - "SEVERITY_NUMBER_{}".format( - self.log_data_2.log_record.severity_text - ), - ), + severity_number=self.log_data_2.log_record.severity_number.value, severity_text="INFO", span_id=int.to_bytes( 5213367945872657623, 8, "big" @@ -450,12 +435,7 @@ def test_translate_multiple_logs(self): # pylint: disable=no-member name="error name", time_unix_nano=self.log_data_3.log_record.timestamp, - severity_number=getattr( - SeverityNumber, - "SEVERITY_NUMBER_{}".format( - self.log_data_3.log_record.severity_text - ), - ), + severity_number=self.log_data_3.log_record.severity_number.value, severity_text="ERROR", span_id=int.to_bytes( 5213367945872657628, 8, "big" From 02cc0c6b18db20c21a7f18081198a1794d4a39f8 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 15 Oct 2021 12:57:27 -0700 Subject: [PATCH 1046/1517] fstring --- .../tests/logs/test_otlp_logs_exporter.py | 1 + opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index 866d1a03b7..ef2146b5c0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -207,6 +207,7 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): mock_insecure, ), ] + # pylint: disable=C0209 for endpoint, insecure, mock_method in endpoints: OTLPLogExporter(endpoint=endpoint, insecure=insecure) self.assertEqual( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py index 1a66413a15..9917daf020 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -87,8 +87,8 @@ def to_json(self) -> str: "severity_text": self.severity_text, "attributes": self.attributes, "timestamp": ns_to_iso_str(self.timestamp), - "trace_id": "0x{}".format(format_trace_id(self.trace_id)), - "span_id": "0x{}".format(format_span_id(self.span_id)), + "trace_id": f"0x{format_trace_id(self.trace_id)}", + "span_id": f"0x{format_span_id(self.span_id)}", "trace_flags": self.trace_flags, "resource": repr(self.resource.attributes) if self.resource From c70ee09d985c6cd090670ac8735d7725d84b0f5a Mon Sep 17 00:00:00 2001 From: Tigran Najaryan <4194920+tigrannajaryan@users.noreply.github.com> Date: Sun, 17 Oct 2021 13:10:19 -0400 Subject: [PATCH 1047/1517] Demonstrate how to set the Resource for LogEmitterProvider (#2209) * Demonstrate how to set the Resource for LogEmitterProvider Added a Resource to the logs example to make it more complete. Previously it was using the built-in Resource. Now it adds the service.name and service.instance.id attributes. The resulting emitted log records look like this: ``` Resource labels: -> telemetry.sdk.language: STRING(python) -> telemetry.sdk.name: STRING(opentelemetry) -> telemetry.sdk.version: STRING(1.5.0) -> service.name: STRING(shoppingcart) -> service.instance.id: STRING(instance-12) InstrumentationLibraryLogs #0 InstrumentationLibrary __main__ 0.1 LogRecord #0 Timestamp: 2021-10-14 18:33:43.425820928 +0000 UTC Severity: ERROR ShortName: Body: Hyderabad, we have a major problem. Trace ID: ce1577e4a703f42d569e72593ad71888 Span ID: f8908ac4258ceff6 Flags: 1 ``` * Fix linting --- docs/examples/logs/example.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/examples/logs/example.py b/docs/examples/logs/example.py index 5cf4ed838c..f172c02aa1 100644 --- a/docs/examples/logs/example.py +++ b/docs/examples/logs/example.py @@ -2,8 +2,13 @@ from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.log_exporter import OTLPLogExporter -from opentelemetry.sdk.logs import OTLPHandler, get_log_emitter_provider +from opentelemetry.sdk.logs import ( + LogEmitterProvider, + OTLPHandler, + set_log_emitter_provider, +) from opentelemetry.sdk.logs.export import SimpleLogProcessor +from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -15,7 +20,16 @@ SimpleSpanProcessor(ConsoleSpanExporter()) ) -log_emitter_provider = get_log_emitter_provider() +log_emitter_provider = LogEmitterProvider( + resource=Resource.create( + { + "service.name": "shoppingcart", + "service.instance.id": "instance-12", + } + ), +) +set_log_emitter_provider(log_emitter_provider) + exporter = OTLPLogExporter(insecure=True) log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) log_emitter = log_emitter_provider.get_log_emitter(__name__, "0.1") From 0e039c6e5eb8fa207783576ef0be9ea0fbd79410 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 19 Oct 2021 04:12:01 +0530 Subject: [PATCH 1048/1517] Use batch processor in example (#2225) --- docs/examples/logs/example.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples/logs/example.py b/docs/examples/logs/example.py index f172c02aa1..f38eec3755 100644 --- a/docs/examples/logs/example.py +++ b/docs/examples/logs/example.py @@ -7,17 +7,17 @@ OTLPHandler, set_log_emitter_provider, ) -from opentelemetry.sdk.logs.export import SimpleLogProcessor +from opentelemetry.sdk.logs.export import BatchLogProcessor from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, ConsoleSpanExporter, - SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( - SimpleSpanProcessor(ConsoleSpanExporter()) + BatchSpanProcessor(ConsoleSpanExporter()) ) log_emitter_provider = LogEmitterProvider( @@ -31,7 +31,7 @@ set_log_emitter_provider(log_emitter_provider) exporter = OTLPLogExporter(insecure=True) -log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) +log_emitter_provider.add_log_processor(BatchLogProcessor(exporter)) log_emitter = log_emitter_provider.get_log_emitter(__name__, "0.1") handler = OTLPHandler(level=logging.NOTSET, log_emitter=log_emitter) From c3150393f95b1f17df39ab7f372b4f7cd5cfa459 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 28 Oct 2021 09:48:35 -0700 Subject: [PATCH 1049/1517] move logs to _logs (#2240) * move logs to _logs * fix lint --- docs/examples/logs/example.py | 4 ++-- docs/sdk/logs.export.rst | 8 +++---- docs/sdk/logs.rst | 4 ++-- docs/sdk/logs.severity.rst | 6 ++--- .../otlp/proto/grpc/log_exporter/__init__.py | 6 ++--- .../tests/logs/test_otlp_logs_exporter.py | 8 ++++--- opentelemetry-sdk/setup.cfg | 2 +- .../sdk/{logs => _logs}/__init__.py | 6 ++--- .../sdk/{logs => _logs}/export/__init__.py | 4 ++-- .../export/in_memory_log_exporter.py | 4 ++-- .../sdk/{logs => _logs}/severity.py | 0 opentelemetry-sdk/tests/logs/test_export.py | 8 +++---- .../tests/logs/test_global_provider.py | 22 +++++++++---------- opentelemetry-sdk/tests/logs/test_handler.py | 4 ++-- .../tests/logs/test_multi_log_prcessor.py | 4 ++-- 15 files changed, 46 insertions(+), 44 deletions(-) rename opentelemetry-sdk/src/opentelemetry/sdk/{logs => _logs}/__init__.py (98%) rename opentelemetry-sdk/src/opentelemetry/sdk/{logs => _logs}/export/__init__.py (98%) rename opentelemetry-sdk/src/opentelemetry/sdk/{logs => _logs}/export/in_memory_log_exporter.py (93%) rename opentelemetry-sdk/src/opentelemetry/sdk/{logs => _logs}/severity.py (100%) diff --git a/docs/examples/logs/example.py b/docs/examples/logs/example.py index f38eec3755..db41ea4086 100644 --- a/docs/examples/logs/example.py +++ b/docs/examples/logs/example.py @@ -2,12 +2,12 @@ from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.log_exporter import OTLPLogExporter -from opentelemetry.sdk.logs import ( +from opentelemetry.sdk._logs import ( LogEmitterProvider, OTLPHandler, set_log_emitter_provider, ) -from opentelemetry.sdk.logs.export import BatchLogProcessor +from opentelemetry.sdk._logs.export import BatchLogProcessor from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( diff --git a/docs/sdk/logs.export.rst b/docs/sdk/logs.export.rst index d247e4db72..19a4023742 100644 --- a/docs/sdk/logs.export.rst +++ b/docs/sdk/logs.export.rst @@ -1,7 +1,7 @@ -opentelemetry.sdk.logs.export -============================= +opentelemetry.sdk._logs.export +============================== -.. automodule:: opentelemetry.sdk.logs.export +.. automodule:: opentelemetry.sdk._logs.export :members: :undoc-members: - :show-inheritance: \ No newline at end of file + :show-inheritance: diff --git a/docs/sdk/logs.rst b/docs/sdk/logs.rst index 7eb6f93264..ade637a88d 100644 --- a/docs/sdk/logs.rst +++ b/docs/sdk/logs.rst @@ -1,4 +1,4 @@ -opentelemetry.sdk.logs package +opentelemetry.sdk._logs package =============================== Submodules @@ -9,7 +9,7 @@ Submodules logs.export logs.severity -.. automodule:: opentelemetry.sdk.logs +.. automodule:: opentelemetry.sdk._logs :members: :undoc-members: :show-inheritance: diff --git a/docs/sdk/logs.severity.rst b/docs/sdk/logs.severity.rst index bcf30cf361..1197e8b44e 100644 --- a/docs/sdk/logs.severity.rst +++ b/docs/sdk/logs.severity.rst @@ -1,7 +1,7 @@ -opentelemetry.sdk.logs.severity -=============================== +opentelemetry.sdk._logs.severity +================================ -.. automodule:: opentelemetry.sdk.logs.severity +.. automodule:: opentelemetry.sdk._logs.severity :members: :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py index ecf9e16e8f..211655d93a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py @@ -31,9 +31,9 @@ ResourceLogs, ) from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord -from opentelemetry.sdk.logs import LogRecord as SDKLogRecord -from opentelemetry.sdk.logs import LogData -from opentelemetry.sdk.logs.export import LogExporter, LogExportResult +from opentelemetry.sdk._logs import LogRecord as SDKLogRecord +from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs.export import LogExporter, LogExportResult class OTLPLogExporter( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index ef2146b5c0..aef5c722ac 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -42,9 +42,11 @@ from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as OTLPResource, ) -from opentelemetry.sdk.logs import LogData, LogRecord -from opentelemetry.sdk.logs.export import LogExportResult -from opentelemetry.sdk.logs.severity import SeverityNumber as SDKSeverityNumber +from opentelemetry.sdk._logs import LogData, LogRecord +from opentelemetry.sdk._logs.export import LogExportResult +from opentelemetry.sdk._logs.severity import ( + SeverityNumber as SDKSeverityNumber, +) from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import TraceFlags diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 19031c11e3..158c8a198c 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -55,7 +55,7 @@ opentelemetry_tracer_provider = opentelemetry_traces_exporter = console = opentelemetry.sdk.trace.export:ConsoleSpanExporter opentelemetry_log_emitter_provider = - sdk_log_emitter_provider = opentelemetry.sdk.logs:LogEmitterProvider + sdk_log_emitter_provider = opentelemetry.sdk._logs:LogEmitterProvider opentelemetry_id_generator = random = opentelemetry.sdk.trace.id_generator:RandomIdGenerator opentelemetry_environment_variables = diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py similarity index 98% rename from opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py rename to opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index 9917daf020..619482e3fd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -21,10 +21,10 @@ import threading from typing import Any, Callable, Optional, Tuple, Union, cast +from opentelemetry.sdk._logs.severity import SeverityNumber, std_to_otlp from opentelemetry.sdk.environment_variables import ( OTEL_PYTHON_LOG_EMITTER_PROVIDER, ) -from opentelemetry.sdk.logs.severity import SeverityNumber, std_to_otlp from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -124,7 +124,7 @@ def emit(self, log_data: LogData): @abc.abstractmethod def shutdown(self): - """Called when a :class:`opentelemetry.sdk.logs.LogEmitter` is shutdown""" + """Called when a :class:`opentelemetry.sdk._logs.LogEmitter` is shutdown""" @abc.abstractmethod def force_flush(self, timeout_millis: int = 30000): @@ -489,7 +489,7 @@ def get_log_emitter( """Returns a `LogEmitter` for use within a python process. This function is a convenience wrapper for - opentelemetry.sdk.logs.LogEmitterProvider.get_log_emitter. + opentelemetry.sdk._logs.LogEmitterProvider.get_log_emitter. If log_emitter_provider param is omitted the current configured one is used. """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py similarity index 98% rename from opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py rename to opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py index f831edc1d0..f65c967534 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py @@ -22,7 +22,7 @@ from typing import IO, Callable, Deque, List, Optional, Sequence from opentelemetry.context import attach, detach, set_value -from opentelemetry.sdk.logs import LogData, LogProcessor, LogRecord +from opentelemetry.sdk._logs import LogData, LogProcessor, LogRecord from opentelemetry.util._time import _time_ns _logger = logging.getLogger(__name__) @@ -39,7 +39,7 @@ class LogExporter(abc.ABC): Interface to be implemented by services that want to export logs received in their own format. - To export data this MUST be registered to the :class`opentelemetry.sdk.logs.LogEmitter` using a + To export data this MUST be registered to the :class`opentelemetry.sdk._logs.LogEmitter` using a log processor. """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/in_memory_log_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/in_memory_log_exporter.py similarity index 93% rename from opentelemetry-sdk/src/opentelemetry/sdk/logs/export/in_memory_log_exporter.py rename to opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/in_memory_log_exporter.py index 95cb8bccba..68cb6b7389 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/export/in_memory_log_exporter.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/in_memory_log_exporter.py @@ -15,8 +15,8 @@ import threading import typing -from opentelemetry.sdk.logs import LogData -from opentelemetry.sdk.logs.export import LogExporter, LogExportResult +from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs.export import LogExporter, LogExportResult class InMemoryLogExporter(LogExporter): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py similarity index 100% rename from opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py rename to opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 51eaeb3d89..964b44f769 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -21,21 +21,21 @@ from unittest.mock import Mock, patch from opentelemetry.sdk import trace -from opentelemetry.sdk.logs import ( +from opentelemetry.sdk._logs import ( LogData, LogEmitterProvider, LogRecord, OTLPHandler, ) -from opentelemetry.sdk.logs.export import ( +from opentelemetry.sdk._logs.export import ( BatchLogProcessor, ConsoleExporter, SimpleLogProcessor, ) -from opentelemetry.sdk.logs.export.in_memory_log_exporter import ( +from opentelemetry.sdk._logs.export.in_memory_log_exporter import ( InMemoryLogExporter, ) -from opentelemetry.sdk.logs.severity import SeverityNumber +from opentelemetry.sdk._logs.severity import SeverityNumber from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import TraceFlags diff --git a/opentelemetry-sdk/tests/logs/test_global_provider.py b/opentelemetry-sdk/tests/logs/test_global_provider.py index fc687d1961..96083fca2a 100644 --- a/opentelemetry-sdk/tests/logs/test_global_provider.py +++ b/opentelemetry-sdk/tests/logs/test_global_provider.py @@ -18,20 +18,20 @@ from logging import WARNING from unittest.mock import patch -from opentelemetry.sdk import logs -from opentelemetry.sdk.environment_variables import ( - OTEL_PYTHON_LOG_EMITTER_PROVIDER, -) -from opentelemetry.sdk.logs import ( +from opentelemetry.sdk import _logs +from opentelemetry.sdk._logs import ( LogEmitterProvider, get_log_emitter_provider, set_log_emitter_provider, ) +from opentelemetry.sdk.environment_variables import ( + OTEL_PYTHON_LOG_EMITTER_PROVIDER, +) class TestGlobals(unittest.TestCase): def tearDown(self): - reload(logs) + reload(_logs) def check_override_not_allowed(self): """set_log_emitter_provider should throw a warning when overridden""" @@ -42,7 +42,7 @@ def check_override_not_allowed(self): test.output, [ ( - "WARNING:opentelemetry.sdk.logs:Overriding of current " + "WARNING:opentelemetry.sdk._logs:Overriding of current " "LogEmitterProvider is not allowed" ) ], @@ -50,14 +50,14 @@ def check_override_not_allowed(self): self.assertIs(provider, get_log_emitter_provider()) def test_set_tracer_provider(self): - reload(logs) + reload(_logs) provider = LogEmitterProvider() set_log_emitter_provider(provider) retrieved_provider = get_log_emitter_provider() self.assertEqual(provider, retrieved_provider) def test_tracer_provider_override_warning(self): - reload(logs) + reload(_logs) self.check_override_not_allowed() @patch.dict( @@ -65,11 +65,11 @@ def test_tracer_provider_override_warning(self): {OTEL_PYTHON_LOG_EMITTER_PROVIDER: "sdk_log_emitter_provider"}, ) def test_sdk_log_emitter_provider(self): - reload(logs) + reload(_logs) self.check_override_not_allowed() @patch.dict("os.environ", {OTEL_PYTHON_LOG_EMITTER_PROVIDER: "unknown"}) def test_unknown_log_emitter_provider(self): - reload(logs) + reload(_logs) with self.assertRaises(Exception): get_log_emitter_provider() diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 474a87fe8d..d7942f912b 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -17,8 +17,8 @@ from unittest.mock import Mock from opentelemetry.sdk import trace -from opentelemetry.sdk.logs import LogEmitter, OTLPHandler -from opentelemetry.sdk.logs.severity import SeverityNumber +from opentelemetry.sdk._logs import LogEmitter, OTLPHandler +from opentelemetry.sdk._logs.severity import SeverityNumber from opentelemetry.trace import INVALID_SPAN_CONTEXT diff --git a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py index a3d095077a..e55124edcc 100644 --- a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py +++ b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py @@ -21,7 +21,7 @@ from abc import ABC, abstractmethod from unittest.mock import Mock -from opentelemetry.sdk.logs import ( +from opentelemetry.sdk._logs import ( ConcurrentMultiLogProcessor, LogEmitterProvider, LogProcessor, @@ -29,7 +29,7 @@ OTLPHandler, SynchronousMultiLogProcessor, ) -from opentelemetry.sdk.logs.severity import SeverityNumber +from opentelemetry.sdk._logs.severity import SeverityNumber class AnotherLogProcessor(LogProcessor): From 64e884aef5294eec8a2d94d067113e78d04b7395 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 2 Nov 2021 11:01:41 -0700 Subject: [PATCH 1050/1517] move log_exporter to _log_exporter as it's still experimental (#2252) --- docs/examples/logs/example.py | 4 +++- docs/sdk/logs.rst | 7 +++++++ .../proto/grpc/{log_exporter => _log_exporter}/__init__.py | 0 .../tests/logs/test_otlp_logs_exporter.py | 6 ++++-- opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py | 6 +++--- .../src/opentelemetry/sdk/environment_variables.py | 2 +- opentelemetry-sdk/tests/logs/test_global_provider.py | 6 +++--- 7 files changed, 21 insertions(+), 10 deletions(-) rename exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/{log_exporter => _log_exporter}/__init__.py (100%) diff --git a/docs/examples/logs/example.py b/docs/examples/logs/example.py index db41ea4086..b34d9a88cc 100644 --- a/docs/examples/logs/example.py +++ b/docs/examples/logs/example.py @@ -1,7 +1,9 @@ import logging from opentelemetry import trace -from opentelemetry.exporter.otlp.proto.grpc.log_exporter import OTLPLogExporter +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( + OTLPLogExporter, +) from opentelemetry.sdk._logs import ( LogEmitterProvider, OTLPHandler, diff --git a/docs/sdk/logs.rst b/docs/sdk/logs.rst index ade637a88d..6d9f3c2548 100644 --- a/docs/sdk/logs.rst +++ b/docs/sdk/logs.rst @@ -1,6 +1,13 @@ opentelemetry.sdk._logs package =============================== +.. warning:: + OpenTelemetry Python logs are in an experimental state. The APIs within + :mod:`opentelemetry.sdk._logs` are subject to change in minor/patch releases and make no + backward compatability guarantees at this time. + + Once logs become stable, this package will be be renamed to ``opentelemetry.sdk.logs``. + Submodules ---------- diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py rename to exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index aef5c722ac..b9c33786e3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -21,8 +21,10 @@ from google.rpc.error_details_pb2 import RetryInfo from grpc import StatusCode, server +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( + OTLPLogExporter, +) from opentelemetry.exporter.otlp.proto.grpc.exporter import _translate_value -from opentelemetry.exporter.otlp.proto.grpc.log_exporter import OTLPLogExporter from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, ExportLogsServiceResponse, @@ -168,7 +170,7 @@ def tearDown(self): ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") @patch( - "opentelemetry.exporter.otlp.proto.grpc.log_exporter.OTLPLogExporter._stub" + "opentelemetry.exporter.otlp.proto.grpc._log_exporter.OTLPLogExporter._stub" ) # pylint: disable=unused-argument def test_no_credentials_error( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index 619482e3fd..6da162ed0f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -23,7 +23,7 @@ from opentelemetry.sdk._logs.severity import SeverityNumber, std_to_otlp from opentelemetry.sdk.environment_variables import ( - OTEL_PYTHON_LOG_EMITTER_PROVIDER, + _OTEL_PYTHON_LOG_EMITTER_PROVIDER, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util import ns_to_iso_str @@ -450,14 +450,14 @@ def get_log_emitter_provider() -> LogEmitterProvider: """Gets the current global :class:`~.LogEmitterProvider` object.""" global _LOG_EMITTER_PROVIDER # pylint: disable=global-statement if _LOG_EMITTER_PROVIDER is None: - if OTEL_PYTHON_LOG_EMITTER_PROVIDER not in os.environ: + if _OTEL_PYTHON_LOG_EMITTER_PROVIDER not in os.environ: _LOG_EMITTER_PROVIDER = LogEmitterProvider() return _LOG_EMITTER_PROVIDER _LOG_EMITTER_PROVIDER = cast( "LogEmitterProvider", _load_provider( - OTEL_PYTHON_LOG_EMITTER_PROVIDER, "log_emitter_provider" + _OTEL_PYTHON_LOG_EMITTER_PROVIDER, "log_emitter_provider" ), ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 7c89e1c9d5..e9d35092a6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -370,7 +370,7 @@ If both are set, :envvar:`OTEL_SERVICE_NAME` takes precedence. """ -OTEL_PYTHON_LOG_EMITTER_PROVIDER = "OTEL_PYTHON_LOG_EMITTER_PROVIDER" +_OTEL_PYTHON_LOG_EMITTER_PROVIDER = "OTEL_PYTHON_LOG_EMITTER_PROVIDER" """ .. envvar:: OTEL_PYTHON_LOG_EMITTER_PROVIDER diff --git a/opentelemetry-sdk/tests/logs/test_global_provider.py b/opentelemetry-sdk/tests/logs/test_global_provider.py index 96083fca2a..7a249defcf 100644 --- a/opentelemetry-sdk/tests/logs/test_global_provider.py +++ b/opentelemetry-sdk/tests/logs/test_global_provider.py @@ -25,7 +25,7 @@ set_log_emitter_provider, ) from opentelemetry.sdk.environment_variables import ( - OTEL_PYTHON_LOG_EMITTER_PROVIDER, + _OTEL_PYTHON_LOG_EMITTER_PROVIDER, ) @@ -62,13 +62,13 @@ def test_tracer_provider_override_warning(self): @patch.dict( "os.environ", - {OTEL_PYTHON_LOG_EMITTER_PROVIDER: "sdk_log_emitter_provider"}, + {_OTEL_PYTHON_LOG_EMITTER_PROVIDER: "sdk_log_emitter_provider"}, ) def test_sdk_log_emitter_provider(self): reload(_logs) self.check_override_not_allowed() - @patch.dict("os.environ", {OTEL_PYTHON_LOG_EMITTER_PROVIDER: "unknown"}) + @patch.dict("os.environ", {_OTEL_PYTHON_LOG_EMITTER_PROVIDER: "unknown"}) def test_unknown_log_emitter_provider(self): reload(_logs) with self.assertRaises(Exception): From d20f4b3374ef6cf3265d72fadec349b1dd282fc5 Mon Sep 17 00:00:00 2001 From: Michael Manganiello Date: Thu, 4 Nov 2021 06:13:30 -0300 Subject: [PATCH 1051/1517] Refactor code using pyupgrade for Python 3.6 (#2238) * Refactor code using pyupgrade for Python 3.6 This diff is the result of applying the following command to the project: ```shell find . -type f -name "*.py" -exec pyupgrade --py36-plus '{}' + ``` * Revert changes in autogenerated files * Remove changes on autogenerated files Co-authored-by: Owais Lone --- docs/examples/basic_context/implicit_context.py | 4 ++-- docs/examples/datadog_exporter/server.py | 2 +- .../fork-process-model/flask-gunicorn/app.py | 2 +- docs/examples/fork-process-model/flask-uwsgi/app.py | 2 +- docs/examples/opentracing/rediscache.py | 2 +- docs/getting_started/tests/test_flask.py | 2 +- docs/getting_started/tests/test_tracing.py | 2 +- .../exporter/otlp/proto/grpc/exporter.py | 8 +++----- .../otlp/proto/grpc/trace_exporter/__init__.py | 4 ++-- .../benchmarks/test_benchmark_trace_exporter.py | 10 +++++----- .../proto/http/trace_exporter/encoder/__init__.py | 13 ++++--------- .../opentelemetry-exporter-zipkin-json/setup.cfg | 3 +-- .../setup.cfg | 3 +-- .../tests/baggage/test_baggage_propagation.py | 9 +++------ .../tests/context/test_contextvars_context.py | 4 ++-- .../tests/propagators/test_composite.py | 6 +++--- .../trace/profile_resource_usage_batch_export.py | 2 +- .../trace/profile_resource_usage_simple_export.py | 2 +- .../src/opentelemetry/propagators/b3/__init__.py | 2 +- .../tests/testbed/otel_ot_shim_tracer.py | 2 +- .../test_active_span_replacement/test_asyncio.py | 2 -- .../test_active_span_replacement/test_threads.py | 2 -- .../testbed/test_client_server/test_asyncio.py | 4 +--- .../testbed/test_client_server/test_threads.py | 4 +--- .../test_common_request_handler/request_handler.py | 2 -- .../test_common_request_handler/test_asyncio.py | 2 -- .../test_common_request_handler/test_threads.py | 2 -- .../testbed/test_late_span_finish/test_asyncio.py | 2 -- .../testbed/test_late_span_finish/test_threads.py | 2 -- .../test_listener_per_request/test_asyncio.py | 2 -- .../test_listener_per_request/test_threads.py | 2 -- .../testbed/test_multiple_callbacks/test_asyncio.py | 2 -- .../testbed/test_multiple_callbacks/test_threads.py | 2 -- .../testbed/test_nested_callbacks/test_asyncio.py | 2 -- .../testbed/test_nested_callbacks/test_threads.py | 2 -- .../test_subtask_span_propagation/test_asyncio.py | 2 -- .../test_subtask_span_propagation/test_threads.py | 2 -- .../tests/testbed/utils.py | 2 -- 38 files changed, 38 insertions(+), 86 deletions(-) diff --git a/docs/examples/basic_context/implicit_context.py b/docs/examples/basic_context/implicit_context.py index fbfdf8b55d..0d89448058 100644 --- a/docs/examples/basic_context/implicit_context.py +++ b/docs/examples/basic_context/implicit_context.py @@ -21,5 +21,5 @@ with tracer.start_span(name="root span") as root_span: ctx = baggage.set_baggage("foo", "bar") -print("Global context baggage: {}".format(baggage.get_all())) -print("Span context baggage: {}".format(baggage.get_all(context=ctx))) +print(f"Global context baggage: {baggage.get_all()}") +print(f"Span context baggage: {baggage.get_all(context=ctx)}") diff --git a/docs/examples/datadog_exporter/server.py b/docs/examples/datadog_exporter/server.py index f032887a12..4438f22f53 100644 --- a/docs/examples/datadog_exporter/server.py +++ b/docs/examples/datadog_exporter/server.py @@ -56,7 +56,7 @@ def server_request(): with tracer.start_as_current_span("server-inner"): if param == "error": raise ValueError("forced server error") - return "served: {}".format(param) + return f"served: {param}" if __name__ == "__main__": diff --git a/docs/examples/fork-process-model/flask-gunicorn/app.py b/docs/examples/fork-process-model/flask-gunicorn/app.py index 4aa3b9f4c5..008e1f04d5 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/app.py +++ b/docs/examples/fork-process-model/flask-gunicorn/app.py @@ -52,7 +52,7 @@ def fibonacci(): fast_span.set_attribute("n", n) fast_span.set_attribute("nth_fibonacci", ans) - return "F({}) is: ({})".format(n, ans) + return f"F({n}) is: ({ans})" if __name__ == "__main__": diff --git a/docs/examples/fork-process-model/flask-uwsgi/app.py b/docs/examples/fork-process-model/flask-uwsgi/app.py index f5b7bfdab0..1191bcc30e 100644 --- a/docs/examples/fork-process-model/flask-uwsgi/app.py +++ b/docs/examples/fork-process-model/flask-uwsgi/app.py @@ -72,7 +72,7 @@ def fibonacci(): fast_span.set_attribute("n", n) fast_span.set_attribute("nth_fibonacci", ans) - return "F({}) is: ({})".format(n, ans) + return f"F({n}) is: ({ans})" if __name__ == "__main__": diff --git a/docs/examples/opentracing/rediscache.py b/docs/examples/opentracing/rediscache.py index 6d9cd21662..9d2a51aab8 100644 --- a/docs/examples/opentracing/rediscache.py +++ b/docs/examples/opentracing/rediscache.py @@ -45,7 +45,7 @@ def inner(*args, **kwargs): scope1.span.log_kv({"msg": "Cache miss, calling function"}) with self.tracer.start_active_span( - 'Call "{}"'.format(func.__name__) + f'Call "{func.__name__}"' ) as scope2: scope2.span.set_tag("func", func.__name__) scope2.span.set_tag("args", str(args)) diff --git a/docs/getting_started/tests/test_flask.py b/docs/getting_started/tests/test_flask.py index d617cfd652..92425dc5cf 100644 --- a/docs/getting_started/tests/test_flask.py +++ b/docs/getting_started/tests/test_flask.py @@ -25,7 +25,7 @@ class TestFlask(unittest.TestCase): def test_flask(self): dirpath = os.path.dirname(os.path.realpath(__file__)) - server_script = "{}/../flask_example.py".format(dirpath) + server_script = f"{dirpath}/../flask_example.py" server = subprocess.Popen( [sys.executable, server_script], stdout=subprocess.PIPE, diff --git a/docs/getting_started/tests/test_tracing.py b/docs/getting_started/tests/test_tracing.py index 54738e7798..2ad571963b 100644 --- a/docs/getting_started/tests/test_tracing.py +++ b/docs/getting_started/tests/test_tracing.py @@ -20,7 +20,7 @@ class TestBasicTracerExample(unittest.TestCase): def test_basic_tracer(self): dirpath = os.path.dirname(os.path.realpath(__file__)) - test_script = "{}/../tracing_example.py".format(dirpath) + test_script = f"{dirpath}/../tracing_example.py" output = subprocess.check_output( (sys.executable, test_script) ).decode() diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 15596ebc9f..d1b3cc992d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -21,7 +21,7 @@ from time import sleep from typing import Any, Callable, Dict, Generic, List, Optional from typing import Sequence as TypingSequence -from typing import Text, TypeVar +from typing import TypeVar from urllib.parse import urlparse from backoff import expo @@ -115,14 +115,12 @@ def _translate_value(value: Any) -> KeyValue: # ) else: - raise Exception( - "Invalid type {} of value {}".format(type(value), value) - ) + raise Exception(f"Invalid type {type(value)} of value {value}") return any_value -def _translate_key_values(key: Text, value: Any) -> KeyValue: +def _translate_key_values(key: str, value: Any) -> KeyValue: return KeyValue(key=key, value=_translate_value(value)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index e2c9e6ad89..5f18de1ac0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -147,7 +147,7 @@ def _translate_context_trace_state(self, sdk_span: ReadableSpan) -> None: if sdk_span.context.trace_state is not None: self._collector_span_kwargs["trace_state"] = ",".join( [ - "{}={}".format(key, value) + f"{key}={value}" for key, value in (sdk_span.context.trace_state.items()) ] ) @@ -302,7 +302,7 @@ def _translate_data( self._collector_span_kwargs["kind"] = getattr( CollectorSpan.SpanKind, - "SPAN_KIND_{}".format(sdk_span.kind.name), + f"SPAN_KIND_{sdk_span.kind.name}", ) instrumentation_library_spans.spans.append( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/benchmarks/test_benchmark_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/benchmarks/test_benchmark_trace_exporter.py index da292d02ad..2b39a8feb3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/benchmarks/test_benchmark_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/benchmarks/test_benchmark_trace_exporter.py @@ -33,7 +33,7 @@ def get_tracer_with_processor(span_processor_class): return tracer -class MockTraceServiceStub(object): +class MockTraceServiceStub: def __init__(self, channel): self.Export = lambda *args, **kwargs: None @@ -51,8 +51,8 @@ def create_spans_to_be_exported(): ) for i in range(10): span.set_attribute( - "benchmarkAttribute_{}".format(i), - "benchmarkAttrValue_{}".format(i), + f"benchmarkAttribute_{i}", + f"benchmarkAttrValue_{i}", ) span.end() @@ -79,8 +79,8 @@ def create_spans_to_be_exported(): ) for i in range(10): span.set_attribute( - "benchmarkAttribute_{}".format(i), - "benchmarkAttrValue_{}".format(i), + f"benchmarkAttribute_{i}", + f"benchmarkAttrValue_{i}", ) span.end() diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py index 7a5adad7b3..4802dddcac 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py @@ -14,7 +14,7 @@ import logging from collections import abc -from typing import Any, List, Optional, Sequence, Text +from typing import Any, List, Optional, Sequence from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest as PB2ExportTraceServiceRequest, @@ -211,10 +211,7 @@ def _encode_trace_state(trace_state: TraceState) -> Optional[str]: pb2_trace_state = None if trace_state is not None: pb2_trace_state = ",".join( - [ - "{}={}".format(key, value) - for key, value in (trace_state.items()) - ] + [f"{key}={value}" for key, value in (trace_state.items())] ) return pb2_trace_state @@ -283,13 +280,11 @@ def _encode_value(value: Any) -> PB2AnyValue: # elif isinstance(value, abc.Mapping): # pass else: - raise Exception( - "Invalid type {} of value {}".format(type(value), value) - ) + raise Exception(f"Invalid type {type(value)} of value {value}") return any_value -def _encode_key_value(key: Text, value: Any) -> PB2KeyValue: +def _encode_key_value(key: str, value: Any) -> PB2KeyValue: any_value = _encode_value(value) return PB2KeyValue(key=key, value=any_value) diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index 99d5e3327a..09f881e8dc 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 @@ -37,7 +36,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 4198f69b10..564b0caed0 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 @@ -37,7 +36,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.5 +python_requires = >=3.6 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py index 69a1039a49..466ff3be30 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py @@ -96,16 +96,13 @@ def test_invalid_header(self): def test_header_too_long(self): long_value = "s" * (W3CBaggagePropagator._MAX_HEADER_LENGTH + 1) - header = "key1={}".format(long_value) + header = f"key1={long_value}" expected = {} self.assertEqual(self._extract(header), expected) def test_header_contains_too_many_entries(self): header = ",".join( - [ - "key{}=val".format(k) - for k in range(W3CBaggagePropagator._MAX_PAIRS + 1) - ] + [f"key{k}=val" for k in range(W3CBaggagePropagator._MAX_PAIRS + 1)] ) self.assertEqual( len(self._extract(header)), W3CBaggagePropagator._MAX_PAIRS @@ -113,7 +110,7 @@ def test_header_contains_too_many_entries(self): def test_header_contains_pair_too_long(self): long_value = "s" * (W3CBaggagePropagator._MAX_PAIR_LENGTH + 1) - header = "key1=value1,key2={},key3=value3".format(long_value) + header = f"key1=value1,key2={long_value},key3=value3" expected = {"key1": "value1", "key3": "value3"} with self.assertLogs(level=WARNING) as warning: self.assertEqual(self._extract(header), expected) diff --git a/opentelemetry-api/tests/context/test_contextvars_context.py b/opentelemetry-api/tests/context/test_contextvars_context.py index a805602159..dc27e27190 100644 --- a/opentelemetry-api/tests/context/test_contextvars_context.py +++ b/opentelemetry-api/tests/context/test_contextvars_context.py @@ -22,7 +22,7 @@ class TestContextVarsContext(ContextTestCases.BaseTest): def setUp(self) -> None: - super(TestContextVarsContext, self).setUp() + super().setUp() self.mock_runtime = patch.object( context, "_RUNTIME_CONTEXT", @@ -31,5 +31,5 @@ def setUp(self) -> None: self.mock_runtime.start() def tearDown(self) -> None: - super(TestContextVarsContext, self).tearDown() + super().tearDown() self.mock_runtime.stop() diff --git a/opentelemetry-api/tests/propagators/test_composite.py b/opentelemetry-api/tests/propagators/test_composite.py index d6bf6960cd..14d1894153 100644 --- a/opentelemetry-api/tests/propagators/test_composite.py +++ b/opentelemetry-api/tests/propagators/test_composite.py @@ -28,8 +28,8 @@ def get_as_list(dict_object, key): def mock_inject(name, value="data"): def wrapped(carrier=None, context=None, setter=None): carrier[name] = value - setter.set({}, "inject_field_{}_0".format(name), None) - setter.set({}, "inject_field_{}_1".format(name), None) + setter.set({}, f"inject_field_{name}_0", None) + setter.set({}, f"inject_field_{name}_1", None) return wrapped @@ -44,7 +44,7 @@ def wrapped(carrier=None, context=None, getter=None): def mock_fields(name): - return {"inject_field_{}_0".format(name), "inject_field_{}_1".format(name)} + return {f"inject_field_{name}_0", f"inject_field_{name}_1"} class TestCompositePropagator(unittest.TestCase): diff --git a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py index 825091331b..3e9a201c96 100644 --- a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py +++ b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_batch_export.py @@ -24,7 +24,7 @@ SPANS_PER_SECOND = 10_000 -class MockTraceServiceStub(object): +class MockTraceServiceStub: def __init__(self, channel): self.Export = lambda *args, **kwargs: None diff --git a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py index 95b56c3cea..bc27fb519d 100644 --- a/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py +++ b/opentelemetry-sdk/tests/performance/resource-usage/trace/profile_resource_usage_simple_export.py @@ -24,7 +24,7 @@ SPANS_PER_SECOND = 10_000 -class MockTraceServiceStub(object): +class MockTraceServiceStub: def __init__(self, channel): self.Export = lambda *args, **kwargs: None diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py index 1254bfeac0..1bbc3614f9 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py @@ -42,7 +42,7 @@ class B3MultiFormat(TextMapPropagator): SPAN_ID_KEY = "x-b3-spanid" SAMPLED_KEY = "x-b3-sampled" FLAGS_KEY = "x-b3-flags" - _SAMPLE_PROPAGATE_VALUES = set(["1", "True", "true", "d"]) + _SAMPLE_PROPAGATE_VALUES = {"1", "True", "true", "d"} _trace_id_regex = re_compile(r"[\da-fA-F]{16}|[\da-fA-F]{32}") _span_id_regex = re_compile(r"[\da-fA-F]{16}") diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py b/shim/opentelemetry-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py index fa1f537a99..6c0a904571 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py @@ -15,7 +15,7 @@ class MockTracer(opentracingshim.TracerShim): def __init__(self): tracer_provider = trace.TracerProvider() oteltracer = tracer_provider.get_tracer(__name__) - super(MockTracer, self).__init__(oteltracer) + super().__init__(oteltracer) exporter = InMemorySpanExporter() span_processor = SimpleSpanProcessor(exporter) tracer_provider.add_span_processor(span_processor) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py index cb555dc109..131bb70b91 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import asyncio from ..otel_ot_shim_tracer import MockTracer diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py index e382d5d716..c8d490063b 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from concurrent.futures import ThreadPoolExecutor from ..otel_ot_shim_tracer import MockTracer diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py index 5379584719..d76fffe3b3 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import asyncio import opentracing @@ -16,7 +14,7 @@ class Server: def __init__(self, *args, **kwargs): tracer = kwargs.pop("tracer") queue = kwargs.pop("queue") - super(Server, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.tracer = tracer self.queue = queue diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_threads.py index 619781edec..df382c3463 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_threads.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from queue import Queue from threading import Thread @@ -17,7 +15,7 @@ class Server(Thread): def __init__(self, *args, **kwargs): tracer = kwargs.pop("tracer") queue = kwargs.pop("queue") - super(Server, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.daemon = True self.tracer = tracer diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py index 47ff088025..22d59fbca6 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from opentracing.ext import tags from ..utils import get_logger diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py index e94a643c29..14958418a3 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import asyncio from opentracing.ext import tags diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py index 99693461e3..6f5022ccc1 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from concurrent.futures import ThreadPoolExecutor from opentracing.ext import tags diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py index 128073b056..86a47c6a73 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import asyncio from ..otel_ot_shim_tracer import MockTracer diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py index 5972eb8b92..de8acb70bf 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import time from concurrent.futures import ThreadPoolExecutor diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py index ea8690a72f..66999f04bf 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import asyncio from opentracing.ext import tags diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py index dc40373912..b810db1f01 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from concurrent.futures import ThreadPoolExecutor from opentracing.ext import tags diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py index 36043d0a9b..3754367878 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import asyncio import random diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py index b24ae643e3..dfb567c745 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import random import time from concurrent.futures import ThreadPoolExecutor diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py index 1e6d2a3604..b6b8277d38 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import asyncio from ..otel_ot_shim_tracer import MockTracer diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py index 5bb63ab9e7..1f86b2dfba 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from concurrent.futures import ThreadPoolExecutor from ..otel_ot_shim_tracer import MockTracer diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py index 2bf8b3cbd8..bd08ee6d09 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function - import asyncio from ..otel_ot_shim_tracer import MockTracer diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py index 09eddf3448..ea15aafb9a 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function - from concurrent.futures import ThreadPoolExecutor from ..otel_ot_shim_tracer import MockTracer diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/utils.py b/shim/opentelemetry-opentracing-shim/tests/testbed/utils.py index a7b977f3d7..88cc4838b8 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/utils.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/utils.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import logging import threading import time From 41c5f99e9424df9678faf75eb87ac1e345300168 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 5 Nov 2021 13:56:40 -0400 Subject: [PATCH 1052/1517] Remove links to metrics/logs feature branches from docs (#2261) --- docs/examples/logs/README.rst | 5 +++++ docs/index.rst | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/examples/logs/README.rst b/docs/examples/logs/README.rst index 3c19c2eafe..909fefd593 100644 --- a/docs/examples/logs/README.rst +++ b/docs/examples/logs/README.rst @@ -1,6 +1,11 @@ OpenTelemetry Logs SDK ====================== +.. warning:: + OpenTelemetry Python logs are in an experimental state. The APIs within + :mod:`opentelemetry.sdk._logs` are subject to change in minor/patch releases and make no + backward compatability guarantees at this time. + Start the Collector locally to see data being exported. Write the following file: .. code-block:: yaml diff --git a/docs/index.rst b/docs/index.rst index e4f1cfc4df..4344bab2ba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,9 +11,8 @@ The Python `OpenTelemetry `_ client. This documentation describes the :doc:`opentelemetry-api `, :doc:`opentelemetry-sdk `, and several `integration packages <#integrations>`_. -The library is currently stable for tracing. Support for `metrics `_ -and `logging `_ is currently under development and is considered -experimental. +The library is currently stable for tracing. Support for :doc:`metrics ` and +:doc:`logging ` are currently under development and are considered experimental. Requirement ----------- From 29e4bab54b6bfcf1d3d540bd4a41706d2395deb3 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Sat, 6 Nov 2021 00:13:23 +0530 Subject: [PATCH 1053/1517] Make batch processor fork aware and reinit when needed (#2242) Since 3.7 python provides register_at_fork which can be used to make our batch processor fork-safe. --- CHANGELOG.md | 2 + .../sdk/_logs/export/__init__.py | 12 ++++ .../sdk/trace/export/__init__.py | 17 +++++ opentelemetry-sdk/tests/logs/test_export.py | 53 +++++++++++++++- .../tests/trace/export/test_export.py | 62 ++++++++++++++++++- 5 files changed, 144 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f4c305ae7..55dd541b19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2153](https://github.com/open-telemetry/opentelemetry-python/pull/2153)) - Add metrics API ([#1887](https://github.com/open-telemetry/opentelemetry-python/pull/1887)) +- Make batch processor fork aware and reinit when needed + ([#2242](https://github.com/open-telemetry/opentelemetry-python/pull/2242)) ## [1.6.2-0.25b2](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.2-0.25b2) - 2021-10-19 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py index f65c967534..c705c2b249 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py @@ -16,6 +16,7 @@ import collections import enum import logging +import os import sys import threading from os import linesep @@ -154,6 +155,17 @@ def __init__( None ] * self._max_export_batch_size # type: List[Optional[LogData]] self._worker_thread.start() + # Only available in *nix since py37. + if hasattr(os, "register_at_fork"): + os.register_at_fork( + after_in_child=self._at_fork_reinit + ) # pylint: disable=protected-access + + def _at_fork_reinit(self): + self._condition = threading.Condition(threading.Lock()) + self._queue.clear() + self._worker_thread = threading.Thread(target=self.worker, daemon=True) + self._worker_thread.start() def worker(self): timeout = self._schedule_delay_millis / 1e3 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 4f0cc817c9..d40bb4968c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -14,6 +14,7 @@ import collections import logging +import os import sys import threading import typing @@ -197,6 +198,11 @@ def __init__( None ] * self.max_export_batch_size # type: typing.List[typing.Optional[Span]] self.worker_thread.start() + # Only available in *nix since py37. + if hasattr(os, "register_at_fork"): + os.register_at_fork( + after_in_child=self._at_fork_reinit + ) # pylint: disable=protected-access def on_start( self, span: Span, parent_context: typing.Optional[Context] = None @@ -220,6 +226,17 @@ def on_end(self, span: ReadableSpan) -> None: with self.condition: self.condition.notify() + def _at_fork_reinit(self): + self.condition = threading.Condition(threading.Lock()) + self.queue.clear() + + # worker_thread is local to a process, only the thread that issued fork continues + # to exist. A new worker thread must be started in child process. + self.worker_thread = threading.Thread( + name="OtelBatchSpanProcessor", target=self.worker, daemon=True + ) + self.worker_thread.start() + def worker(self): timeout = self.schedule_delay_millis / 1e3 flush_request = None # type: typing.Optional[_FlushRequest] diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 964b44f769..45b83358f9 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -14,7 +14,9 @@ # pylint: disable=protected-access import logging +import multiprocessing import os +import sys import time import unittest from concurrent.futures import ThreadPoolExecutor @@ -38,6 +40,7 @@ from opentelemetry.sdk._logs.severity import SeverityNumber from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.test.concurrency_test import ConcurrencyTestBase from opentelemetry.trace import TraceFlags from opentelemetry.trace.span import INVALID_SPAN_CONTEXT @@ -158,7 +161,7 @@ def test_simple_log_processor_shutdown(self): self.assertEqual(len(finished_logs), 0) -class TestBatchLogProcessor(unittest.TestCase): +class TestBatchLogProcessor(ConcurrencyTestBase): def test_emit_call_log_record(self): exporter = InMemoryLogExporter() log_processor = Mock(wraps=BatchLogProcessor(exporter)) @@ -269,6 +272,54 @@ def bulk_log_and_flush(num_logs): finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 2415) + @unittest.skipUnless( + hasattr(os, "fork") and sys.version_info >= (3, 7), + "needs *nix and minor version 7 or later", + ) + def test_batch_log_processor_fork(self): + # pylint: disable=invalid-name + exporter = InMemoryLogExporter() + log_processor = BatchLogProcessor( + exporter, + max_export_batch_size=64, + schedule_delay_millis=10, + ) + provider = LogEmitterProvider() + provider.add_log_processor(log_processor) + + emitter = provider.get_log_emitter(__name__) + logger = logging.getLogger("test-fork") + logger.addHandler(OTLPHandler(log_emitter=emitter)) + + logger.critical("yolo") + time.sleep(0.5) # give some time for the exporter to upload + + self.assertTrue(log_processor.force_flush()) + self.assertEqual(len(exporter.get_finished_logs()), 1) + exporter.clear() + + multiprocessing.set_start_method("fork") + + def child(conn): + def _target(): + logger.critical("Critical message child") + + self.run_with_many_threads(_target, 100) + + time.sleep(0.5) + + logs = exporter.get_finished_logs() + conn.send(len(logs) == 100) + conn.close() + + parent_conn, child_conn = multiprocessing.Pipe() + p = multiprocessing.Process(target=child, args=(child_conn,)) + p.start() + self.assertTrue(parent_conn.recv()) + p.join() + + log_processor.shutdown() + class TestConsoleExporter(unittest.TestCase): def test_export(self): # pylint: disable=no-self-use diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 2e4672af26..00ccfe44d3 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import multiprocessing import os +import sys import threading import time import unittest @@ -30,6 +32,10 @@ OTEL_BSP_SCHEDULE_DELAY, ) from opentelemetry.sdk.trace import export +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from opentelemetry.test.concurrency_test import ConcurrencyTestBase class MySpanExporter(export.SpanExporter): @@ -157,7 +163,7 @@ def _create_start_and_end_span(name, span_processor): span.end() -class TestBatchSpanProcessor(unittest.TestCase): +class TestBatchSpanProcessor(ConcurrencyTestBase): @mock.patch.dict( "os.environ", { @@ -356,6 +362,60 @@ def test_batch_span_processor_not_sampled(self): self.assertEqual(len(spans_names_list), 0) span_processor.shutdown() + def _check_fork_trace(self, exporter, expected): + time.sleep(0.5) # give some time for the exporter to upload spans + spans = exporter.get_finished_spans() + for span in spans: + self.assertIn(span.name, expected) + + @unittest.skipUnless( + hasattr(os, "fork") and sys.version_info >= (3, 7), + "needs *nix and minor version 7 or later", + ) + def test_batch_span_processor_fork(self): + # pylint: disable=invalid-name + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) + + exporter = InMemorySpanExporter() + span_processor = export.BatchSpanProcessor( + exporter, + max_queue_size=256, + max_export_batch_size=64, + schedule_delay_millis=10, + ) + tracer_provider.add_span_processor(span_processor) + with tracer.start_as_current_span("foo"): + pass + time.sleep(0.5) # give some time for the exporter to upload spans + + self.assertTrue(span_processor.force_flush()) + self.assertEqual(len(exporter.get_finished_spans()), 1) + exporter.clear() + + def child(conn): + def _target(): + with tracer.start_as_current_span("span") as s: + s.set_attribute("i", "1") + with tracer.start_as_current_span("temp"): + pass + + self.run_with_many_threads(_target, 100) + + time.sleep(0.5) + + spans = exporter.get_finished_spans() + conn.send(len(spans) == 200) + conn.close() + + parent_conn, child_conn = multiprocessing.Pipe() + p = multiprocessing.Process(target=child, args=(child_conn,)) + p.start() + self.assertTrue(parent_conn.recv()) + p.join() + + span_processor.shutdown() + def test_batch_span_processor_scheduled_delay(self): """Test that spans are exported each schedule_delay_millis""" spans_names_list = [] From a1903c939917c7a6129476f12c9f50a587b479e5 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 5 Nov 2021 19:10:50 -0700 Subject: [PATCH 1054/1517] Populate `auto.version` in Resource if using autoinstrumentation (#2243) --- CHANGELOG.md | 4 +++- docs/examples/auto-instrumentation/README.rst | 2 +- .../server_uninstrumented.py | 13 ----------- .../sdk/_configuration/__init__.py | 23 ++++++++++++++----- opentelemetry-sdk/tests/test_configurator.py | 6 ++++- 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55dd541b19..bdc06dd756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.6.1-0.25b1...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.6.1-0.25b2...HEAD) - Add support for Python 3.10 ([#2207](https://github.com/open-telemetry/opentelemetry-python/pull/2207)) - remove `X-B3-ParentSpanId` for B3 propagator as per OpenTelemetry specification ([#2237](https://github.com/open-telemetry/opentelemetry-python/pull/2237)) +- Populate `auto.version` in Resource if using auto-instrumentation + ([#2243](https://github.com/open-telemetry/opentelemetry-python/pull/2243)) - Return proxy instruments from ProxyMeter [[#2169](https://github.com/open-telemetry/opentelemetry-python/pull/2169)] - Make Measurement a concrete class diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index aeb1e0be8e..7ebd8a76d3 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -153,7 +153,7 @@ and run the following command instead: .. code:: sh - $ opentelemetry-instrument --trace-exporter console_span python server_uninstrumented.py + $ opentelemetry-instrument --traces_exporter console python server_uninstrumented.py In the console where you previously executed ``client.py``, run the following command again: diff --git a/docs/examples/auto-instrumentation/server_uninstrumented.py b/docs/examples/auto-instrumentation/server_uninstrumented.py index e6fa75f1b5..9c247a049a 100644 --- a/docs/examples/auto-instrumentation/server_uninstrumented.py +++ b/docs/examples/auto-instrumentation/server_uninstrumented.py @@ -14,21 +14,8 @@ from flask import Flask, request -from opentelemetry import trace -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - BatchSpanProcessor, - ConsoleSpanExporter, -) - app = Flask(__name__) -trace.set_tracer_provider(TracerProvider()) - -trace.get_tracer_provider().add_span_processor( - BatchSpanProcessor(ConsoleSpanExporter()) -) - @app.route("/server_request") def server_request(): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index b49e4d8e59..cb4aed53b5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -19,7 +19,7 @@ from abc import ABC, abstractmethod from os import environ -from typing import Sequence, Tuple +from typing import Dict, Optional, Sequence, Tuple, Type from pkg_resources import iter_entry_points @@ -28,9 +28,11 @@ OTEL_PYTHON_ID_GENERATOR, OTEL_TRACES_EXPORTER, ) +from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator +from opentelemetry.semconv.resource import ResourceAttributes _EXPORTER_OTLP = "otlp" _EXPORTER_OTLP_SPAN = "otlp_proto_grpc" @@ -64,12 +66,21 @@ def _get_exporter_names() -> Sequence[str]: def _init_tracing( - exporters: Sequence[SpanExporter], id_generator: IdGenerator + exporters: Dict[str, Type[SpanExporter]], + id_generator: IdGenerator, + auto_instrumentation_version: Optional[str] = None, ): # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name # from the env variable else defaults to "unknown_service" + auto_resource = {} + # populate version if using auto-instrumentation + if auto_instrumentation_version: + auto_resource[ + ResourceAttributes.TELEMETRY_AUTO_VERSION + ] = auto_instrumentation_version provider = TracerProvider( id_generator=id_generator(), + resource=Resource.create(auto_resource), ) trace.set_tracer_provider(provider) @@ -102,7 +113,7 @@ def _import_tracer_provider_config_components( def _import_exporters( exporter_names: Sequence[str], -) -> Sequence[SpanExporter]: +) -> Dict[str, Type[SpanExporter]]: trace_exporters = {} for ( @@ -132,12 +143,12 @@ def _import_id_generator(id_generator_name: str) -> IdGenerator: raise RuntimeError(f"{id_generator_name} is not an IdGenerator") -def _initialize_components(): +def _initialize_components(auto_instrumentation_version): exporter_names = _get_exporter_names() trace_exporters = _import_exporters(exporter_names) id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) - _init_tracing(trace_exporters, id_generator) + _init_tracing(trace_exporters, id_generator, auto_instrumentation_version) class _BaseConfigurator(ABC): @@ -180,4 +191,4 @@ class _OTelSDKConfigurator(_BaseConfigurator): """ def _configure(self, **kwargs): - _initialize_components() + _initialize_components(kwargs.get("auto_instrumentation_version")) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index e7619d64dc..9243b80c67 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -111,7 +111,7 @@ def tearDown(self): environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=my-test-service"} ) def test_trace_init_default(self): - _init_tracing({"zipkin": Exporter}, RandomIdGenerator) + _init_tracing({"zipkin": Exporter}, RandomIdGenerator, "test-version") self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] @@ -122,6 +122,10 @@ def test_trace_init_default(self): self.assertEqual( provider.processor.exporter.service_name, "my-test-service" ) + self.assertEqual( + provider.resource.attributes.get("telemetry.auto.version"), + "test-version", + ) @patch.dict( environ, From 0dd86d616cf3da248c77f077a62c3064b5278e4a Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 9 Nov 2021 16:33:38 -0600 Subject: [PATCH 1055/1517] Use augmented assignments (#2268) Fixes #2258 --- opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index 6da162ed0f..c7c4e6a90d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -159,7 +159,7 @@ def __init__(self): def add_log_processor(self, log_processor: LogProcessor) -> None: """Adds a Logprocessor to the list of log processors handled by this instance""" with self._lock: - self._log_processors = self._log_processors + (log_processor,) + self._log_processors += (log_processor,) def emit(self, log_data: LogData) -> None: for lp in self._log_processors: @@ -218,7 +218,7 @@ def __init__(self, max_workers: int = 2): def add_log_processor(self, log_processor: LogProcessor): with self._lock: - self._log_processors = self._log_processors + (log_processor,) + self._log_processors += (log_processor,) def _submit_and_wait( self, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 35fefc3e3f..2418dcada8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -146,7 +146,7 @@ def __init__(self): def add_span_processor(self, span_processor: SpanProcessor) -> None: """Adds a SpanProcessor to the list handled by this instance.""" with self._lock: - self._span_processors = self._span_processors + (span_processor,) + self._span_processors += (span_processor,) def on_start( self, @@ -216,7 +216,7 @@ def __init__(self, num_threads: int = 2): def add_span_processor(self, span_processor: SpanProcessor) -> None: """Adds a SpanProcessor to the list handled by this instance.""" with self._lock: - self._span_processors = self._span_processors + (span_processor,) + self._span_processors += (span_processor,) def _submit_and_await( self, From 9ed60ea600cd0ffda6b614fc30b62117a84aeea7 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Tue, 9 Nov 2021 14:59:37 -0800 Subject: [PATCH 1056/1517] Filter invalid resource attribute pairs (#2256) --- CHANGELOG.md | 2 ++ .../opentelemetry/sdk/resources/__init__.py | 19 +++++++++++++------ .../tests/resources/test_resources.py | 10 ++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdc06dd756..aba1ba1f00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1887](https://github.com/open-telemetry/opentelemetry-python/pull/1887)) - Make batch processor fork aware and reinit when needed ([#2242](https://github.com/open-telemetry/opentelemetry-python/pull/2242)) +- `opentelemetry-sdk` Sanitize env var resource attribute pairs + ([#2256](https://github.com/open-telemetry/opentelemetry-python/pull/2256)) ## [1.6.2-0.25b2](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.2-0.25b2) - 2021-10-19 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 5878f375d7..c8790adf11 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -266,13 +266,20 @@ class OTELResourceDetector(ResourceDetector): def detect(self) -> "Resource": env_resources_items = os.environ.get(OTEL_RESOURCE_ATTRIBUTES) env_resource_map = {} + if env_resources_items: - env_resource_map = { - key.strip(): value.strip() - for key, value in ( - item.split("=") for item in env_resources_items.split(",") - ) - } + for item in env_resources_items.split(","): + try: + key, value = item.split("=", maxsplit=1) + except ValueError as exc: + logger.warning( + "Invalid key value resource attribute pair %s: %s", + item, + exc, + ) + continue + env_resource_map[key.strip()] = value.strip() + service_name = os.environ.get(OTEL_SERVICE_NAME) if service_name: env_resource_map[SERVICE_NAME] = service_name diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index ea3de80ed1..ae3e34f9f0 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -482,6 +482,16 @@ def test_multiple_with_whitespace(self): detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) ) + def test_invalid_key_value_pairs(self): + detector = resources.OTELResourceDetector() + os.environ[ + resources.OTEL_RESOURCE_ATTRIBUTES + ] = "k=v,k2=v2,invalid,,foo=bar=baz," + self.assertEqual( + detector.detect(), + resources.Resource({"k": "v", "k2": "v2", "foo": "bar=baz"}), + ) + @mock.patch.dict( os.environ, {resources.OTEL_SERVICE_NAME: "test-srv-name"}, From fc17aa1d1c0d38a88da3322be25b3e5fdc77fbbb Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Thu, 11 Nov 2021 19:14:52 +0530 Subject: [PATCH 1057/1517] Release opentelemetry-test (#2269) fixes #2263 --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ eachdist.ini | 2 +- scripts/build.sh | 2 +- tests/opentelemetry-test/README.rst | 10 ++++++++++ tests/{util => opentelemetry-test}/setup.cfg | 2 +- tests/{util => opentelemetry-test}/setup.py | 0 .../src/opentelemetry/test/asgitestutil.py | 0 .../src/opentelemetry/test/concurrency_test.py | 0 .../src/opentelemetry/test/globals_test.py | 0 .../src/opentelemetry/test/httptest.py | 0 .../src/opentelemetry/test/mock_textmap.py | 0 .../src/opentelemetry/test/spantestutil.py | 0 .../src/opentelemetry/test/test_base.py | 0 .../src/opentelemetry/test/version.py | 0 .../src/opentelemetry/test/wsgitestutil.py | 0 tests/util/README.rst | 9 --------- tox.ini | 8 ++++---- 18 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 tests/opentelemetry-test/README.rst rename tests/{util => opentelemetry-test}/setup.cfg (98%) rename tests/{util => opentelemetry-test}/setup.py (100%) rename tests/{util => opentelemetry-test}/src/opentelemetry/test/asgitestutil.py (100%) rename tests/{util => opentelemetry-test}/src/opentelemetry/test/concurrency_test.py (100%) rename tests/{util => opentelemetry-test}/src/opentelemetry/test/globals_test.py (100%) rename tests/{util => opentelemetry-test}/src/opentelemetry/test/httptest.py (100%) rename tests/{util => opentelemetry-test}/src/opentelemetry/test/mock_textmap.py (100%) rename tests/{util => opentelemetry-test}/src/opentelemetry/test/spantestutil.py (100%) rename tests/{util => opentelemetry-test}/src/opentelemetry/test/test_base.py (100%) rename tests/{util => opentelemetry-test}/src/opentelemetry/test/version.py (100%) rename tests/{util => opentelemetry-test}/src/opentelemetry/test/wsgitestutil.py (100%) delete mode 100644 tests/util/README.rst diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2abba7c24a..059e20219a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: a7c054b257225948c68a9dccb3f2973537d9b4ec + CONTRIB_REPO_SHA: f5b91a305ffcd747d70e4d9ebbc582634d6955df # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index aba1ba1f00..a7451a55f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2242](https://github.com/open-telemetry/opentelemetry-python/pull/2242)) - `opentelemetry-sdk` Sanitize env var resource attribute pairs ([#2256](https://github.com/open-telemetry/opentelemetry-python/pull/2256)) +- `opentelemetry-test` start releasing to pypi.org + ([#2269](https://github.com/open-telemetry/opentelemetry-python/pull/2269)) ## [1.6.2-0.25b2](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.2-0.25b2) - 2021-10-19 diff --git a/eachdist.ini b/eachdist.ini index e239102e27..524bb3891a 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -7,7 +7,7 @@ sortfirst= opentelemetry-sdk opentelemetry-proto opentelemetry-distro - tests/util + tests/opentelemetry-test exporter/* [stable] diff --git a/scripts/build.sh b/scripts/build.sh index 63faa001bc..f317b840a0 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-proto/ opentelemetry-semantic-conventions/ exporter/*/ shim/*/ propagator/*/; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-proto/ opentelemetry-semantic-conventions/ exporter/*/ shim/*/ propagator/*/ tests/opentelemetry-test/; do ( echo "building $d" cd "$d" diff --git a/tests/opentelemetry-test/README.rst b/tests/opentelemetry-test/README.rst new file mode 100644 index 0000000000..774669cb8b --- /dev/null +++ b/tests/opentelemetry-test/README.rst @@ -0,0 +1,10 @@ +OpenTelemetry Test Utilities +============================ + +This package provides internal testing utilities for the OpenTelemetry Python project and provides no stability or quality guarantees. +Please do not use it for anything other than writing or running tests for the OpenTelemetry Python project (github.com/open-telemetry/opentelemetry-python). + + +References +---------- +* `OpenTelemetry Project `_ diff --git a/tests/util/setup.cfg b/tests/opentelemetry-test/setup.cfg similarity index 98% rename from tests/util/setup.cfg rename to tests/opentelemetry-test/setup.cfg index 5274a2d876..591b787ada 100644 --- a/tests/util/setup.cfg +++ b/tests/opentelemetry-test/setup.cfg @@ -17,7 +17,7 @@ name = opentelemetry-test description = Test utilities for OpenTelemetry unit tests author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tests/util +url = https://github.com/open-telemetry/opentelemetry-python/tests/opentelemetry-test platforms = any license = Apache-2.0 classifiers = diff --git a/tests/util/setup.py b/tests/opentelemetry-test/setup.py similarity index 100% rename from tests/util/setup.py rename to tests/opentelemetry-test/setup.py diff --git a/tests/util/src/opentelemetry/test/asgitestutil.py b/tests/opentelemetry-test/src/opentelemetry/test/asgitestutil.py similarity index 100% rename from tests/util/src/opentelemetry/test/asgitestutil.py rename to tests/opentelemetry-test/src/opentelemetry/test/asgitestutil.py diff --git a/tests/util/src/opentelemetry/test/concurrency_test.py b/tests/opentelemetry-test/src/opentelemetry/test/concurrency_test.py similarity index 100% rename from tests/util/src/opentelemetry/test/concurrency_test.py rename to tests/opentelemetry-test/src/opentelemetry/test/concurrency_test.py diff --git a/tests/util/src/opentelemetry/test/globals_test.py b/tests/opentelemetry-test/src/opentelemetry/test/globals_test.py similarity index 100% rename from tests/util/src/opentelemetry/test/globals_test.py rename to tests/opentelemetry-test/src/opentelemetry/test/globals_test.py diff --git a/tests/util/src/opentelemetry/test/httptest.py b/tests/opentelemetry-test/src/opentelemetry/test/httptest.py similarity index 100% rename from tests/util/src/opentelemetry/test/httptest.py rename to tests/opentelemetry-test/src/opentelemetry/test/httptest.py diff --git a/tests/util/src/opentelemetry/test/mock_textmap.py b/tests/opentelemetry-test/src/opentelemetry/test/mock_textmap.py similarity index 100% rename from tests/util/src/opentelemetry/test/mock_textmap.py rename to tests/opentelemetry-test/src/opentelemetry/test/mock_textmap.py diff --git a/tests/util/src/opentelemetry/test/spantestutil.py b/tests/opentelemetry-test/src/opentelemetry/test/spantestutil.py similarity index 100% rename from tests/util/src/opentelemetry/test/spantestutil.py rename to tests/opentelemetry-test/src/opentelemetry/test/spantestutil.py diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/opentelemetry-test/src/opentelemetry/test/test_base.py similarity index 100% rename from tests/util/src/opentelemetry/test/test_base.py rename to tests/opentelemetry-test/src/opentelemetry/test/test_base.py diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/opentelemetry-test/src/opentelemetry/test/version.py similarity index 100% rename from tests/util/src/opentelemetry/test/version.py rename to tests/opentelemetry-test/src/opentelemetry/test/version.py diff --git a/tests/util/src/opentelemetry/test/wsgitestutil.py b/tests/opentelemetry-test/src/opentelemetry/test/wsgitestutil.py similarity index 100% rename from tests/util/src/opentelemetry/test/wsgitestutil.py rename to tests/opentelemetry-test/src/opentelemetry/test/wsgitestutil.py diff --git a/tests/util/README.rst b/tests/util/README.rst deleted file mode 100644 index 58a75149bd..0000000000 --- a/tests/util/README.rst +++ /dev/null @@ -1,9 +0,0 @@ -OpenTelemetry Test Utilities -============================ - -Test utilities for OpenTelemetry unit tests - - -References ----------- -* `OpenTelemetry Project `_ diff --git a/tox.ini b/tox.ini index 38da9cac54..a0632c8387 100644 --- a/tox.ini +++ b/tox.ini @@ -79,7 +79,7 @@ setenv = ; i.e: CONTRIB_REPO_SHA=dde62cebffe519c35875af6d06fae053b3be65ec tox -e CONTRIB_REPO_SHA={env:CONTRIB_REPO_SHA:"main"} CONTRIB_REPO="git+https://github.com/open-telemetry/opentelemetry-python-contrib.git@{env:CONTRIB_REPO_SHA}" - mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/:{toxinidir}/tests/util/src/ + mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/:{toxinidir}/tests/opentelemetry-test/src/ changedir = api: opentelemetry-api/tests @@ -108,7 +108,7 @@ commands_pre = py3{6,7,8,9,10}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. - opentelemetry: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-sdk {toxinidir}/tests/util + opentelemetry: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-sdk {toxinidir}/tests/opentelemetry-test protobuf: pip install {toxinidir}/opentelemetry-proto @@ -192,7 +192,7 @@ commands_pre = python -m pip install -e {toxinidir}/opentelemetry-semantic-conventions[test] python -m pip install -e {toxinidir}/opentelemetry-sdk[test] python -m pip install -e {toxinidir}/opentelemetry-proto[test] - python -m pip install -e {toxinidir}/tests/util[test] + python -m pip install -e {toxinidir}/tests/opentelemetry-test[test] python -m pip install -e {toxinidir}/shim/opentelemetry-opentracing-shim[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift[test] @@ -254,7 +254,7 @@ commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-semantic-conventions \ -e {toxinidir}/opentelemetry-sdk \ - -e {toxinidir}/tests/util \ + -e {toxinidir}/tests/opentelemetry-test \ -e {toxinidir}/exporter/opentelemetry-exporter-opencensus \ -e {toxinidir}/opentelemetry-proto \ -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc \ From 5f8bb78f5bdc5975d7fab44698c333e41596d125 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Thu, 11 Nov 2021 23:41:50 +0530 Subject: [PATCH 1058/1517] updating changelogs and version to 1.7.0-0.26b0 (#2272) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 5 ++++- eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test/src/opentelemetry/test/version.py | 2 +- 29 files changed, 37 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 059e20219a..9082d218c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: f5b91a305ffcd747d70e4d9ebbc582634d6955df + CONTRIB_REPO_SHA: a7f34e8c5f5909500dcca44f823d72d55aacd59e # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index a7451a55f0..5127a826ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.6.1-0.25b2...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.7.0-0.26b0...HEAD) + +## [1.7.0-0.26b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11 + - Add support for Python 3.10 ([#2207](https://github.com/open-telemetry/opentelemetry-python/pull/2207)) diff --git a/eachdist.ini b/eachdist.ini index 524bb3891a..7da4c4c1ba 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.6.2 +version=1.7.0 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.25b2 +version=0.26b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 2078ab46b5..f80eb9227b 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 2078ab46b5..f80eb9227b 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 2cb1b10a3b..18c408e6da 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -40,8 +40,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.6.2 - opentelemetry-exporter-jaeger-thrift == 1.6.2 + opentelemetry-exporter-jaeger-proto-grpc == 1.7.0 + opentelemetry-exporter-jaeger-thrift == 1.7.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 2078ab46b5..f80eb9227b 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index b3fbda5fcb..f6f4e5d8b7 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.25b2" +__version__ = "0.26b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 004be68f68..53cf4b833d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.6.2 + opentelemetry-proto == 1.7.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index ce2cf11c1f..a6b9cbd89a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index 5470386391..f16d89dce5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.6.2 + opentelemetry-proto == 1.7.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index ce2cf11c1f..a6b9cbd89a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 746ebc96d4..5218031b2a 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -39,8 +39,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.6.2 - opentelemetry-exporter-otlp-proto-http == 1.6.2 + opentelemetry-exporter-otlp-proto-grpc == 1.7.0 + opentelemetry-exporter-otlp-proto-http == 1.7.0 [options.entry_points] opentelemetry_traces_exporter = diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index ce2cf11c1f..a6b9cbd89a 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index ce2cf11c1f..a6b9cbd89a 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 564b0caed0..e43ccaaacf 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.6.2 + opentelemetry-exporter-zipkin-json == 1.7.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index ce2cf11c1f..a6b9cbd89a 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index f448f6c8d6..f2b8535b3b 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -39,8 +39,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.6.2 - opentelemetry-exporter-zipkin-proto-http == 1.6.2 + opentelemetry-exporter-zipkin-json == 1.7.0 + opentelemetry-exporter-zipkin-proto-http == 1.7.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index ce2cf11c1f..a6b9cbd89a 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index ce2cf11c1f..a6b9cbd89a 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index ce2cf11c1f..a6b9cbd89a 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 158c8a198c..739e2fca73 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,8 +43,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.6.2 - opentelemetry-semantic-conventions == 0.25b2 + opentelemetry-api == 1.7.0 + opentelemetry-semantic-conventions == 0.26b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index ce2cf11c1f..a6b9cbd89a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index b3fbda5fcb..f6f4e5d8b7 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.25b2" +__version__ = "0.26b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index ce2cf11c1f..a6b9cbd89a 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index ce2cf11c1f..a6b9cbd89a 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.2" +__version__ = "1.7.0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 5d857a571c..95e5d99a46 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -47,7 +47,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test == 0.25b2 + opentelemetry-test == 0.26b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index b3fbda5fcb..f6f4e5d8b7 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.25b2" +__version__ = "0.26b0" diff --git a/tests/opentelemetry-test/src/opentelemetry/test/version.py b/tests/opentelemetry-test/src/opentelemetry/test/version.py index c5894b3aa5..fe5ac66bde 100644 --- a/tests/opentelemetry-test/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.25b2" +__version__ = "0.26b0" From 0f31cbc1b78afeb96a74553b75cfccf86b6d8113 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Fri, 12 Nov 2021 00:22:59 +0530 Subject: [PATCH 1059/1517] Rename opentelemetry-test to opentelemetry-test-utils (#2273) --- .github/workflows/test.yml | 2 +- eachdist.ini | 4 ++-- scripts/build.sh | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../README.rst | 0 .../setup.cfg | 4 ++-- .../setup.py | 0 .../src/opentelemetry/test/asgitestutil.py | 0 .../src/opentelemetry/test/concurrency_test.py | 0 .../src/opentelemetry/test/globals_test.py | 0 .../src/opentelemetry/test/httptest.py | 0 .../src/opentelemetry/test/mock_textmap.py | 0 .../src/opentelemetry/test/spantestutil.py | 0 .../src/opentelemetry/test/test_base.py | 0 .../src/opentelemetry/test/version.py | 0 .../src/opentelemetry/test/wsgitestutil.py | 0 tox.ini | 8 ++++---- 17 files changed, 11 insertions(+), 11 deletions(-) rename tests/{opentelemetry-test => opentelemetry-test-utils}/README.rst (100%) rename tests/{opentelemetry-test => opentelemetry-test-utils}/setup.cfg (96%) rename tests/{opentelemetry-test => opentelemetry-test-utils}/setup.py (100%) rename tests/{opentelemetry-test => opentelemetry-test-utils}/src/opentelemetry/test/asgitestutil.py (100%) rename tests/{opentelemetry-test => opentelemetry-test-utils}/src/opentelemetry/test/concurrency_test.py (100%) rename tests/{opentelemetry-test => opentelemetry-test-utils}/src/opentelemetry/test/globals_test.py (100%) rename tests/{opentelemetry-test => opentelemetry-test-utils}/src/opentelemetry/test/httptest.py (100%) rename tests/{opentelemetry-test => opentelemetry-test-utils}/src/opentelemetry/test/mock_textmap.py (100%) rename tests/{opentelemetry-test => opentelemetry-test-utils}/src/opentelemetry/test/spantestutil.py (100%) rename tests/{opentelemetry-test => opentelemetry-test-utils}/src/opentelemetry/test/test_base.py (100%) rename tests/{opentelemetry-test => opentelemetry-test-utils}/src/opentelemetry/test/version.py (100%) rename tests/{opentelemetry-test => opentelemetry-test-utils}/src/opentelemetry/test/wsgitestutil.py (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9082d218c5..5f5d655efa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: a7f34e8c5f5909500dcca44f823d72d55aacd59e + CONTRIB_REPO_SHA: 3bae87057ddb166490c7b43f4587b408a565d8bc # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/eachdist.ini b/eachdist.ini index 7da4c4c1ba..0164439dc1 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -7,7 +7,7 @@ sortfirst= opentelemetry-sdk opentelemetry-proto opentelemetry-distro - tests/opentelemetry-test + tests/opentelemetry-test-utils exporter/* [stable] @@ -37,7 +37,7 @@ packages= opentelemetry-exporter-opencensus opentelemetry-distro opentelemetry-semantic-conventions - opentelemetry-test + opentelemetry-test-utils tests [experimental] diff --git a/scripts/build.sh b/scripts/build.sh index f317b840a0..605884bee6 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-proto/ opentelemetry-semantic-conventions/ exporter/*/ shim/*/ propagator/*/ tests/opentelemetry-test/; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-proto/ opentelemetry-semantic-conventions/ exporter/*/ shim/*/ propagator/*/ tests/opentelemetry-test-utils/; do ( echo "building $d" cd "$d" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 95e5d99a46..5574b9d037 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -47,7 +47,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test == 0.26b0 + opentelemetry-test-utils == 0.26b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/tests/opentelemetry-test/README.rst b/tests/opentelemetry-test-utils/README.rst similarity index 100% rename from tests/opentelemetry-test/README.rst rename to tests/opentelemetry-test-utils/README.rst diff --git a/tests/opentelemetry-test/setup.cfg b/tests/opentelemetry-test-utils/setup.cfg similarity index 96% rename from tests/opentelemetry-test/setup.cfg rename to tests/opentelemetry-test-utils/setup.cfg index 591b787ada..cf5220fc3b 100644 --- a/tests/opentelemetry-test/setup.cfg +++ b/tests/opentelemetry-test-utils/setup.cfg @@ -13,11 +13,11 @@ # limitations under the License. [metadata] -name = opentelemetry-test +name = opentelemetry-test-utils description = Test utilities for OpenTelemetry unit tests author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tests/opentelemetry-test +url = https://github.com/open-telemetry/opentelemetry-python/tests/opentelemetry-test-utils platforms = any license = Apache-2.0 classifiers = diff --git a/tests/opentelemetry-test/setup.py b/tests/opentelemetry-test-utils/setup.py similarity index 100% rename from tests/opentelemetry-test/setup.py rename to tests/opentelemetry-test-utils/setup.py diff --git a/tests/opentelemetry-test/src/opentelemetry/test/asgitestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py similarity index 100% rename from tests/opentelemetry-test/src/opentelemetry/test/asgitestutil.py rename to tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py diff --git a/tests/opentelemetry-test/src/opentelemetry/test/concurrency_test.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/concurrency_test.py similarity index 100% rename from tests/opentelemetry-test/src/opentelemetry/test/concurrency_test.py rename to tests/opentelemetry-test-utils/src/opentelemetry/test/concurrency_test.py diff --git a/tests/opentelemetry-test/src/opentelemetry/test/globals_test.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py similarity index 100% rename from tests/opentelemetry-test/src/opentelemetry/test/globals_test.py rename to tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py diff --git a/tests/opentelemetry-test/src/opentelemetry/test/httptest.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/httptest.py similarity index 100% rename from tests/opentelemetry-test/src/opentelemetry/test/httptest.py rename to tests/opentelemetry-test-utils/src/opentelemetry/test/httptest.py diff --git a/tests/opentelemetry-test/src/opentelemetry/test/mock_textmap.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/mock_textmap.py similarity index 100% rename from tests/opentelemetry-test/src/opentelemetry/test/mock_textmap.py rename to tests/opentelemetry-test-utils/src/opentelemetry/test/mock_textmap.py diff --git a/tests/opentelemetry-test/src/opentelemetry/test/spantestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py similarity index 100% rename from tests/opentelemetry-test/src/opentelemetry/test/spantestutil.py rename to tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py diff --git a/tests/opentelemetry-test/src/opentelemetry/test/test_base.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py similarity index 100% rename from tests/opentelemetry-test/src/opentelemetry/test/test_base.py rename to tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py diff --git a/tests/opentelemetry-test/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py similarity index 100% rename from tests/opentelemetry-test/src/opentelemetry/test/version.py rename to tests/opentelemetry-test-utils/src/opentelemetry/test/version.py diff --git a/tests/opentelemetry-test/src/opentelemetry/test/wsgitestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py similarity index 100% rename from tests/opentelemetry-test/src/opentelemetry/test/wsgitestutil.py rename to tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py diff --git a/tox.ini b/tox.ini index a0632c8387..2999b7fe43 100644 --- a/tox.ini +++ b/tox.ini @@ -79,7 +79,7 @@ setenv = ; i.e: CONTRIB_REPO_SHA=dde62cebffe519c35875af6d06fae053b3be65ec tox -e CONTRIB_REPO_SHA={env:CONTRIB_REPO_SHA:"main"} CONTRIB_REPO="git+https://github.com/open-telemetry/opentelemetry-python-contrib.git@{env:CONTRIB_REPO_SHA}" - mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/:{toxinidir}/tests/opentelemetry-test/src/ + mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/:{toxinidir}/tests/opentelemetry-test-utils/src/ changedir = api: opentelemetry-api/tests @@ -108,7 +108,7 @@ commands_pre = py3{6,7,8,9,10}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. - opentelemetry: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-sdk {toxinidir}/tests/opentelemetry-test + opentelemetry: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-sdk {toxinidir}/tests/opentelemetry-test-utils protobuf: pip install {toxinidir}/opentelemetry-proto @@ -192,7 +192,7 @@ commands_pre = python -m pip install -e {toxinidir}/opentelemetry-semantic-conventions[test] python -m pip install -e {toxinidir}/opentelemetry-sdk[test] python -m pip install -e {toxinidir}/opentelemetry-proto[test] - python -m pip install -e {toxinidir}/tests/opentelemetry-test[test] + python -m pip install -e {toxinidir}/tests/opentelemetry-test-utils[test] python -m pip install -e {toxinidir}/shim/opentelemetry-opentracing-shim[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift[test] @@ -254,7 +254,7 @@ commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-semantic-conventions \ -e {toxinidir}/opentelemetry-sdk \ - -e {toxinidir}/tests/opentelemetry-test \ + -e {toxinidir}/tests/opentelemetry-test-utils \ -e {toxinidir}/exporter/opentelemetry-exporter-opencensus \ -e {toxinidir}/opentelemetry-proto \ -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc \ From 281c97bf8f9e31392859e006e13c9c8eac8967c3 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Fri, 12 Nov 2021 01:30:44 +0530 Subject: [PATCH 1060/1517] updating changelogs and version to 1.7.1-0.26b1 (#2274) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 +- eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- .../src/opentelemetry/test/version.py | 2 +- 29 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5f5d655efa..1e01829473 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 3bae87057ddb166490c7b43f4587b408a565d8bc + CONTRIB_REPO_SHA: 6b83f1680f9a10900cb1da4e935252176ee05c21 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 5127a826ea..e0658e06cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.7.0-0.26b0...HEAD) -## [1.7.0-0.26b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11 +## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11 - Add support for Python 3.10 diff --git a/eachdist.ini b/eachdist.ini index 0164439dc1..11e31e2e13 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.7.0 +version=1.7.1 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.26b0 +version=0.26b1 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index f80eb9227b..4788fe6398 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index f80eb9227b..4788fe6398 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 18c408e6da..7e2a66630c 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -40,8 +40,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.7.0 - opentelemetry-exporter-jaeger-thrift == 1.7.0 + opentelemetry-exporter-jaeger-proto-grpc == 1.7.1 + opentelemetry-exporter-jaeger-thrift == 1.7.1 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index f80eb9227b..4788fe6398 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index f6f4e5d8b7..a48aab26a5 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.26b0" +__version__ = "0.26b1" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 53cf4b833d..016fabcd36 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.7.0 + opentelemetry-proto == 1.7.1 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index a6b9cbd89a..d19c1f14ba 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index f16d89dce5..7afd8e02e2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.7.0 + opentelemetry-proto == 1.7.1 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index a6b9cbd89a..d19c1f14ba 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 5218031b2a..30c5b1f919 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -39,8 +39,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.7.0 - opentelemetry-exporter-otlp-proto-http == 1.7.0 + opentelemetry-exporter-otlp-proto-grpc == 1.7.1 + opentelemetry-exporter-otlp-proto-http == 1.7.1 [options.entry_points] opentelemetry_traces_exporter = diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index a6b9cbd89a..d19c1f14ba 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index a6b9cbd89a..d19c1f14ba 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index e43ccaaacf..514bc2af17 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.7.0 + opentelemetry-exporter-zipkin-json == 1.7.1 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index a6b9cbd89a..d19c1f14ba 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index f2b8535b3b..e3d72f2ec3 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -39,8 +39,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.7.0 - opentelemetry-exporter-zipkin-proto-http == 1.7.0 + opentelemetry-exporter-zipkin-json == 1.7.1 + opentelemetry-exporter-zipkin-proto-http == 1.7.1 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index a6b9cbd89a..d19c1f14ba 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index a6b9cbd89a..d19c1f14ba 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index a6b9cbd89a..d19c1f14ba 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 739e2fca73..92c3ddbcef 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,8 +43,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.7.0 - opentelemetry-semantic-conventions == 0.26b0 + opentelemetry-api == 1.7.1 + opentelemetry-semantic-conventions == 0.26b1 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index a6b9cbd89a..d19c1f14ba 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index f6f4e5d8b7..a48aab26a5 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.26b0" +__version__ = "0.26b1" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index a6b9cbd89a..d19c1f14ba 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index a6b9cbd89a..d19c1f14ba 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 5574b9d037..938408d33f 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -47,7 +47,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test-utils == 0.26b0 + opentelemetry-test-utils == 0.26b1 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index f6f4e5d8b7..a48aab26a5 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.26b0" +__version__ = "0.26b1" diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index fe5ac66bde..daf3f6620f 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.26b0" +__version__ = "0.26b1" From c991c1c4adee7f2e76859096050fc80a9f7fc1ff Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Fri, 12 Nov 2021 19:10:38 -0500 Subject: [PATCH 1061/1517] [website_docs] Link to website spec pages (#2276) Co-authored-by: Leighton Chen --- website_docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website_docs/getting-started.md b/website_docs/getting-started.md index 4453c06eb8..d993f4bbb7 100644 --- a/website_docs/getting-started.md +++ b/website_docs/getting-started.md @@ -259,7 +259,7 @@ A major feature of distributed tracing is the ability to correlate a trace acros multiple services. However, those services need to propagate information about a trace from one service to the other. -To enable this propagation, OpenTelemetry has the concept of [propagators](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md), +To enable this propagation, OpenTelemetry has the concept of [propagators]({{< relref "/docs/reference/specification/context/api-propagators" >}}), which provide a common method to encode and decode span information from a request and response, respectively. By default, `opentelemetry-python` is configured to use the [W3C Trace Context](https://www.w3.org/TR/trace-context/) From 000f42afb53209956aa427066cac44398224afa3 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Sun, 14 Nov 2021 20:00:45 -0600 Subject: [PATCH 1062/1517] Use parentheses in PR link (#2271) Fixes #2270 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0658e06cc..53f742dce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Populate `auto.version` in Resource if using auto-instrumentation ([#2243](https://github.com/open-telemetry/opentelemetry-python/pull/2243)) - Return proxy instruments from ProxyMeter - [[#2169](https://github.com/open-telemetry/opentelemetry-python/pull/2169)] + ([#2169](https://github.com/open-telemetry/opentelemetry-python/pull/2169)) - Make Measurement a concrete class ([#2153](https://github.com/open-telemetry/opentelemetry-python/pull/2153)) - Add metrics API From db75daede83a097549e80295c61af28c03639751 Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Wed, 17 Nov 2021 15:31:37 -0500 Subject: [PATCH 1063/1517] [website_docs] Link to latest release (#2279) Contributes to https://github.com/open-telemetry/opentelemetry.io/issues/905. /cc @austinlparker --- website_docs/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website_docs/_index.md b/website_docs/_index.md index 30d11e274b..040245e5dc 100644 --- a/website_docs/_index.md +++ b/website_docs/_index.md @@ -27,7 +27,7 @@ is as follows: | ------- | ------- | ------------------- | | Stable | Alpha | Not Yet Implemented | -The current release can be found [here](https://github.com/open-telemetry/opentelemetry-python/releases) +{{% latest_release "python" /%}} ## Further Reading From 1f85268a6f00de4a771e0aaa8663ca6fbc07c575 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 18 Nov 2021 00:18:02 -0600 Subject: [PATCH 1064/1517] Adds Aggregation and instruments as part of Metrics SDK (#2234) * Adds metrics API (#1887) * Adds metric prototype Fixes #1835 * Fix docs * Add API metrics doc * Add missing docs * Add files * Adding docs * Refactor to _initialize * Refactor initialize * Add more documentation * Add exporter test * Add process * Fix tests * Try to add aggregator_class argument Tests are failing here * Fix instrument parent classes * Test default aggregator * WIP * Add prototype test * Tests passing again * Use right counters * All tests passing * Rearrange instrument storage * Fix tests * Add HTTP server test * WIP * WIP * Add prototype * WIP * Fail the test * WIP * WIP * WIP * WIP * Add views * Discard instruments via views * Fix tests * WIP * WIP * Fix lint * WIP * Fix test * Fix lint * Fix method * Fix lint * Mypy workaround * Skip if 3.6 * Fix lint * Add reason * Fix 3.6 * Fix run * Fix lint * Remove SDK metrics * Remove SDK docs * Remove metrics * Remove assertnotraises mixin * Revert sdk docs conf * Remove SDK env var changes * Fix unit checking * Define positional-only arguments * Add Metrics plans * Add API tests * WIP * WIP test * WIP * WIP * WIP * Set provider test passing * Use a fixture * Add test for get_provider * Rename tests * WIP * WIP * WIP * WIP * Remove non specific requirement * Add meter requirements * Put all meter provider tests in one file * Add meter tests * Make attributes be passed as a dictionary * Make some interfaces private * Log an error instead * Remove ASCII flag * Add CHANGELOG entry * Add instrument tests * All tests passing * Add test * Add name tests * Add unit tests * Add description tests * Add counter tests * Add more tests * Add Histogram tests * Add observable gauge tests * Add updowncounter tests * Add observableupdowncounter tests * Fix lint * Fix docs * Fix lint * Ignore mypy * Remove useless pylint skip * Remove useless pylint skip * Remove useless pylint skip * Remove useless pylint skip * Remove useless pylint skip * Add locks to meter and meterprovider * Add lock to instruments * Fix fixmes * Fix lint * Add documentation placeholder * Remove blank line as requested. * Do not override Rlock * Remove unecessary super calls * Add missing super calls * Remove plan files * Add missing parameters * Rename observe to callback * Fix lint * Rename to secure_instrument_name * Remove locks * Fix lint * Remove args and kwargs * Remove implementation that gives meters access to meter provider * Allow creating async instruments with either a callback function or generator * add additional test with callback form of observable counter * add a test/example that reads measurements from proc stat * implement cpu time integration test with generator too Co-authored-by: Aaron Abbott * Make measurement a concrete class (#2153) * Make Measurement a concrete class * comments * update changelog * Return proxy instruments from ProxyMeter (#2169) * Merge main 4 (#2236) * Add MeterProvider and Meter to the SDK Fixes #2200 * Add FIXMEs * Fix docstring * Add FIXME * Fix meter return * Log an error if a force flush fails * Add FIXME * Fix lint * Remove SDK API module * Unregister * Fix API names * Return _DefaultMeter * Remove properties * Pass MeterProvider as a parameter to __init__ * Add FIXMEs * Add FIXMEs * Fix lint * Add Aggregation to the metrics SDK Fixes #2229 * lint fix wip * Fix lint * Add proto to setup.cfg * Add timestamp for last value * Rename modules to be private * Fix paths * Set value in concrete classes init * Fix test * Fix lint * Remove temporalities * Use frozenset as key * Test instruments * Handle min, max and sum in explicit bucket histogram aggregator * Add test for negative values * Remove collect method from aggregations * Add make_point_and_reset * Remove add implementation * Remove _Synchronous * Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py Co-authored-by: Aaron Abbott * Requested fixes * Remove NoneAggregation * Add changelog entry * Fix tests * Fix boundaries * More fixes * Update CHANGELOG.md Co-authored-by: Srikanth Chekuri Co-authored-by: Aaron Abbott Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 3 + .../opentelemetry/sdk/_metrics/aggregation.py | 137 +++++++++++++++ .../opentelemetry/sdk/_metrics/instrument.py | 160 ++++++++++++++++++ .../tests/metrics/test_aggregation.py | 126 ++++++++++++++ 4 files changed, 426 insertions(+) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py create mode 100644 opentelemetry-sdk/tests/metrics/test_aggregation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 53f742dce3..c7e3c2fce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.7.0-0.26b0...HEAD) +- Adds Aggregation and instruments as part of Metrics SDK + ([#2234](https://github.com/open-telemetry/opentelemetry-python/pull/2234)) + ## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py new file mode 100644 index 0000000000..456e447162 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -0,0 +1,137 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod +from collections import OrderedDict +from logging import getLogger +from math import inf + +from opentelemetry._metrics.instrument import _Monotonic +from opentelemetry.util._time import _time_ns + +_logger = getLogger(__name__) + + +class Aggregation(ABC): + @property + def value(self): + return self._value # pylint: disable=no-member + + @abstractmethod + def aggregate(self, value): + pass + + @abstractmethod + def make_point_and_reset(self): + """ + Atomically return a point for the current value of the metric and reset the internal state. + """ + + +class SumAggregation(Aggregation): + """ + This aggregation collects data for the SDK sum metric point. + """ + + def __init__(self, instrument): + self._value = 0 + + def aggregate(self, value): + self._value = self._value + value + + def make_point_and_reset(self): + pass + + +class LastValueAggregation(Aggregation): + + """ + This aggregation collects data for the SDK sum metric point. + """ + + def __init__(self, instrument): + self._value = None + self._timestamp = _time_ns() + + def aggregate(self, value): + self._value = value + self._timestamp = _time_ns() + + def make_point_and_reset(self): + pass + + +class ExplicitBucketHistogramAggregation(Aggregation): + + """ + This aggregation collects data for the SDK sum metric point. + """ + + def __init__( + self, + instrument, + *args, + boundaries=(0, 5, 10, 25, 50, 75, 100, 250, 500, 1000), + record_min_max=True, + ): + super().__init__() + self._value = OrderedDict([(key, 0) for key in (*boundaries, inf)]) + self._min = inf + self._max = -inf + self._sum = 0 + self._instrument = instrument + self._record_min_max = record_min_max + + @property + def min(self): + if not self._record_min_max: + _logger.warning("Min is not being recorded") + + return self._min + + @property + def max(self): + if not self._record_min_max: + _logger.warning("Max is not being recorded") + + return self._max + + @property + def sum(self): + if isinstance(self._instrument, _Monotonic): + return self._sum + + _logger.warning( + "Sum is not filled out when the associated " + "instrument is not monotonic" + ) + return None + + def aggregate(self, value): + if self._record_min_max: + self._min = min(self._min, value) + self._max = max(self._max, value) + + if isinstance(self._instrument, _Monotonic): + self._sum += value + + for key in self._value.keys(): + + if value < key: + self._value[key] = self._value[key] + value + + break + + def make_point_and_reset(self): + pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py new file mode 100644 index 0000000000..fc63311ce7 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -0,0 +1,160 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=function-redefined +# pylint: disable=dangerous-default-value +# Classes in this module use dictionaries as default arguments. This is +# considered dangerous by pylint because the default dictionary is shared by +# all instances. Implementations of these classes must not make any change to +# this default dictionary in __init__. + +from opentelemetry._metrics.instrument import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.sdk._metrics.aggregation import ( + ExplicitBucketHistogramAggregation, + LastValueAggregation, + SumAggregation, +) + + +class _Instrument: + def __init__( + self, + name, + unit="", + description="", + aggregation=None, + aggregation_config={}, + ): + self._attributes_aggregations = {} + self._aggregation = aggregation + self._aggregation_config = aggregation_config + aggregation(self, **aggregation_config) + + +class Counter(_Instrument, Counter): + def __init__( + self, + name, + unit="", + description="", + aggregation=SumAggregation, + aggregation_config={}, + ): + super().__init__( + name, + unit=unit, + description=description, + aggregation=aggregation, + aggregation_config=aggregation_config, + ) + + +class UpDownCounter(_Instrument, UpDownCounter): + def __init__( + self, + name, + unit="", + description="", + aggregation=SumAggregation, + aggregation_config={}, + ): + super().__init__( + name, + unit=unit, + description=description, + aggregation=aggregation, + aggregation_config=aggregation_config, + ) + + +class ObservableCounter(_Instrument, ObservableCounter): + def __init__( + self, + name, + callback, + unit="", + description="", + aggregation=SumAggregation, + aggregation_config={}, + ): + super().__init__( + name, + unit=unit, + description=description, + aggregation=aggregation, + aggregation_config=aggregation_config, + ) + + +class ObservableUpDownCounter(_Instrument, ObservableUpDownCounter): + def __init__( + self, + name, + callback, + unit="", + description="", + aggregation=SumAggregation, + aggregation_config={}, + ): + super().__init__( + name, + unit=unit, + description=description, + aggregation=aggregation, + aggregation_config=aggregation_config, + ) + + +class Histogram(_Instrument, Histogram): + def __init__( + self, + name, + unit="", + description="", + aggregation=ExplicitBucketHistogramAggregation, + aggregation_config={}, + ): + super().__init__( + name, + unit=unit, + description=description, + aggregation=aggregation, + aggregation_config=aggregation_config, + ) + + +class ObservableGauge(_Instrument, ObservableGauge): + def __init__( + self, + name, + callback, + unit="", + description="", + aggregation=LastValueAggregation, + aggregation_config={}, + ): + super().__init__( + name, + unit=unit, + description=description, + aggregation=aggregation, + aggregation_config=aggregation_config, + ) diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py new file mode 100644 index 0000000000..1c4fa1420e --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -0,0 +1,126 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from logging import WARNING +from math import inf +from unittest import TestCase +from unittest.mock import Mock + +from opentelemetry.sdk._metrics.aggregation import ( + ExplicitBucketHistogramAggregation, + LastValueAggregation, + SumAggregation, +) + + +class TestSumAggregation(TestCase): + def test_aggregate(self): + """ + `SumAggregation` collects data for sum metric points + """ + + sum_aggregation = SumAggregation(Mock()) + + sum_aggregation.aggregate(1) + sum_aggregation.aggregate(2) + sum_aggregation.aggregate(3) + + self.assertEqual(sum_aggregation.value, 6) + + sum_aggregation = SumAggregation(Mock()) + + sum_aggregation.aggregate(1) + sum_aggregation.aggregate(-2) + sum_aggregation.aggregate(3) + + self.assertEqual(sum_aggregation.value, 2) + + +class TestLastValueAggregation(TestCase): + def test_aggregate(self): + """ + `LastValueAggregation` collects data for gauge metric points with delta + temporality + """ + + last_value_aggregation = LastValueAggregation(Mock()) + + last_value_aggregation.aggregate(1) + self.assertEqual(last_value_aggregation.value, 1) + + last_value_aggregation.aggregate(2) + self.assertEqual(last_value_aggregation.value, 2) + + last_value_aggregation.aggregate(3) + self.assertEqual(last_value_aggregation.value, 3) + + +class TestExplicitBucketHistogramAggregation(TestCase): + def test_aggregate(self): + """ + `ExplicitBucketHistogramAggregation` collects data for explicit_bucket_histogram metric points + """ + + explicit_bucket_histogram_aggregation = ( + ExplicitBucketHistogramAggregation(Mock()) + ) + + explicit_bucket_histogram_aggregation.aggregate(-1) + explicit_bucket_histogram_aggregation.aggregate(2) + explicit_bucket_histogram_aggregation.aggregate(7) + explicit_bucket_histogram_aggregation.aggregate(8) + explicit_bucket_histogram_aggregation.aggregate(9999) + + self.assertEqual(explicit_bucket_histogram_aggregation.value[0], -1) + self.assertEqual(explicit_bucket_histogram_aggregation.value[5], 2) + self.assertEqual(explicit_bucket_histogram_aggregation.value[10], 15) + self.assertEqual( + explicit_bucket_histogram_aggregation.value[inf], 9999 + ) + + def test_min_max(self): + """ + `record_min_max` indicates the aggregator to record the minimum and + maximum value in the population + """ + + explicit_bucket_histogram_aggregation = ( + ExplicitBucketHistogramAggregation(Mock()) + ) + + explicit_bucket_histogram_aggregation.aggregate(-1) + explicit_bucket_histogram_aggregation.aggregate(2) + explicit_bucket_histogram_aggregation.aggregate(7) + explicit_bucket_histogram_aggregation.aggregate(8) + explicit_bucket_histogram_aggregation.aggregate(9999) + + self.assertEqual(explicit_bucket_histogram_aggregation.min, -1) + self.assertEqual(explicit_bucket_histogram_aggregation.max, 9999) + + explicit_bucket_histogram_aggregation = ( + ExplicitBucketHistogramAggregation(Mock(), record_min_max=False) + ) + + explicit_bucket_histogram_aggregation.aggregate(-1) + explicit_bucket_histogram_aggregation.aggregate(2) + explicit_bucket_histogram_aggregation.aggregate(7) + explicit_bucket_histogram_aggregation.aggregate(8) + explicit_bucket_histogram_aggregation.aggregate(9999) + + with self.assertLogs(level=WARNING): + self.assertEqual(explicit_bucket_histogram_aggregation.min, inf) + + with self.assertLogs(level=WARNING): + self.assertEqual(explicit_bucket_histogram_aggregation.max, -inf) From 80f5a20ba8f3a71450fe3020fecf362fedb76bff Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 22 Nov 2021 22:08:54 +0530 Subject: [PATCH 1065/1517] Improve log message in OTLP exporters (#2285) --- .../exporter/otlp/proto/grpc/exporter.py | 11 +++++++++-- .../otlp/proto/http/trace_exporter/__init__.py | 8 +++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index d1b3cc992d..691c7c2dc1 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -302,11 +302,18 @@ def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: + retry_info.retry_delay.nanos / 1.0e9 ) - logger.debug( - "Waiting %ss before retrying export of span", delay + logger.warning( + "Transient error %s encountered while exporting span batch, retrying in %ss.", + error.code(), + delay, ) sleep(delay) continue + else: + logger.error( + "Failed to export span batch, error code: %s", + error.code(), + ) if error.code() == StatusCode.OK: return self._result.SUCCESS diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 0bcb0a7368..b320119aee 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -138,13 +138,15 @@ def export(self, spans) -> SpanExportResult: if resp.status_code in (200, 202): return SpanExportResult.SUCCESS elif self._retryable(resp): - _logger.debug( - "Waiting %ss before retrying export of span", delay + _logger.warning( + "Transient error %s encountered while exporting span batch, retrying in %ss.", + resp.reason, + delay, ) sleep(delay) continue else: - _logger.warning( + _logger.error( "Failed to export batch code: %s, reason: %s", resp.status_code, resp.text, From 7a8ef3633dd84f2ceae220c6ea9086cc16f71541 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 29 Nov 2021 10:27:56 -0800 Subject: [PATCH 1066/1517] opentelemetry-api: update OTEL_METRICS_EXPORTER variable (#2303) * opentelemetry-api: update OTEL_METRICS_EXPORTER variable Fixes #2302 * Update CHANGELOG.md * fix lint --- CHANGELOG.md | 2 ++ .../src/opentelemetry/environment_variables.py | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7e3c2fce1..36772a49eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adds Aggregation and instruments as part of Metrics SDK ([#2234](https://github.com/open-telemetry/opentelemetry-python/pull/2234)) +- Update visibility of OTEL_METRICS_EXPORTER environment variable + ([#2303](https://github.com/open-telemetry/opentelemetry-python/pull/2303)) ## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11 diff --git a/opentelemetry-api/src/opentelemetry/environment_variables.py b/opentelemetry-api/src/opentelemetry/environment_variables.py index de11e42390..af2319453c 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables.py @@ -12,6 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER" +""" +.. envvar:: OTEL_METRICS_EXPORTER + +""" + OTEL_PROPAGATORS = "OTEL_PROPAGATORS" """ .. envvar:: OTEL_PROPAGATORS @@ -41,9 +47,3 @@ """ .. envvar:: OTEL_PYTHON_METER_PROVIDER """ - -_OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER" -""" -.. envvar:: OTEL_METRICS_EXPORTER - -""" From 5added0261a8a55f13f1f803ca051845a51f6b2c Mon Sep 17 00:00:00 2001 From: Morgan McLean Date: Mon, 29 Nov 2021 16:05:16 -0600 Subject: [PATCH 1067/1517] Remove hardcoded Zoom link (#2287) --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 9c6e9745b1..63ae6859a9 100644 --- a/README.md +++ b/README.md @@ -122,9 +122,7 @@ https://opentelemetry-python.readthedocs.io/en/latest/. For information about contributing to OpenTelemetry Python, see [CONTRIBUTING.md](CONTRIBUTING.md). -We meet weekly on Thursdays at 9AM PST. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates. - -Meetings take place via [Zoom video conference](https://zoom.us/j/8287234601?pwd=YjN2MURycXc4cEZlYTRtYjJaM0grZz09). The passcode is _77777_. +We meet weekly on Thursdays at 9AM PST. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates and Zoom meeting links. Meeting notes are available as a public [Google doc](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit). For edit access, get in touch on [GitHub Discussions](https://github.com/open-telemetry/opentelemetry-python/discussions). From 46f77d0dee69dd9781fd9fae9280a12317b51606 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 2 Dec 2021 11:51:00 -0800 Subject: [PATCH 1068/1517] Adding support for entrypoint loading of log exporters (#2253) --- CHANGELOG.md | 2 + .../setup.cfg | 2 + opentelemetry-sdk/setup.cfg | 2 + .../sdk/_configuration/__init__.py | 84 +++++++++++++------ opentelemetry-sdk/tests/test_configurator.py | 44 ++++++---- 5 files changed, 92 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36772a49eb..9f0cf21c60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2234](https://github.com/open-telemetry/opentelemetry-python/pull/2234)) - Update visibility of OTEL_METRICS_EXPORTER environment variable ([#2303](https://github.com/open-telemetry/opentelemetry-python/pull/2303)) +- Adding entrypoints for log emitter provider and console, otlp log exporters + ([#2253](https://github.com/open-telemetry/opentelemetry-python/pull/2253)) ## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 016fabcd36..fb5cfe0dc2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -57,3 +57,5 @@ where = src [options.entry_points] opentelemetry_traces_exporter = otlp_proto_grpc = opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter +opentelemetry_logs_exporter = + otlp_proto_grpc = opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 92c3ddbcef..a802596500 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -56,6 +56,8 @@ opentelemetry_traces_exporter = console = opentelemetry.sdk.trace.export:ConsoleSpanExporter opentelemetry_log_emitter_provider = sdk_log_emitter_provider = opentelemetry.sdk._logs:LogEmitterProvider +opentelemetry_logs_exporter = + console = opentelemetry.sdk._logs.export:ConsoleExporter opentelemetry_id_generator = random = opentelemetry.sdk.trace.id_generator:RandomIdGenerator opentelemetry_environment_variables = diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index cb4aed53b5..377b21c828 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -28,6 +28,11 @@ OTEL_PYTHON_ID_GENERATOR, OTEL_TRACES_EXPORTER, ) +from opentelemetry.sdk._logs import ( + LogEmitterProvider, + set_log_emitter_provider, +) +from opentelemetry.sdk._logs.export import BatchLogProcessor, LogExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter @@ -35,32 +40,28 @@ from opentelemetry.semconv.resource import ResourceAttributes _EXPORTER_OTLP = "otlp" -_EXPORTER_OTLP_SPAN = "otlp_proto_grpc" +_EXPORTER_OTLP_PROTO_GRPC = "otlp_proto_grpc" _RANDOM_ID_GENERATOR = "random" _DEFAULT_ID_GENERATOR = _RANDOM_ID_GENERATOR +# TODO: add log exporter env variable +_OTEL_LOGS_EXPORTER = "OTEL_LOGS_EXPORTER" + def _get_id_generator() -> str: return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR) -def _get_exporter_names() -> Sequence[str]: - trace_exporters = environ.get(OTEL_TRACES_EXPORTER) - +def _get_exporter_names(names: str) -> Sequence[str]: exporters = set() - if trace_exporters and trace_exporters.lower().strip() != "none": - exporters.update( - { - trace_exporter.strip() - for trace_exporter in trace_exporters.split(",") - } - ) + if names and names.lower().strip() != "none": + exporters.update({_exporter.strip() for _exporter in names.split(",")}) if _EXPORTER_OTLP in exporters: exporters.remove(_EXPORTER_OTLP) - exporters.add(_EXPORTER_OTLP_SPAN) + exporters.add(_EXPORTER_OTLP_PROTO_GRPC) return list(exporters) @@ -91,7 +92,29 @@ def _init_tracing( ) -def _import_tracer_provider_config_components( +def _init_logging( + exporters: Dict[str, Sequence[LogExporter]], + auto_instrumentation_version: Optional[str] = None, +): + # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name + # from the env variable else defaults to "unknown_service" + auto_resource = {} + # populate version if using auto-instrumentation + if auto_instrumentation_version: + auto_resource[ + ResourceAttributes.TELEMETRY_AUTO_VERSION + ] = auto_instrumentation_version + provider = LogEmitterProvider(resource=Resource.create(auto_resource)) + set_log_emitter_provider(provider) + + for _, exporter_class in exporters.items(): + exporter_args = {} + provider.add_log_processor( + BatchLogProcessor(exporter_class(**exporter_args)) + ) + + +def _import_config_components( selected_components, entry_point_name ) -> Sequence[Tuple[str, object]]: component_entry_points = { @@ -112,28 +135,34 @@ def _import_tracer_provider_config_components( def _import_exporters( - exporter_names: Sequence[str], -) -> Dict[str, Type[SpanExporter]]: + trace_exporter_names: Sequence[str], + log_exporter_names: Sequence[str], +) -> Tuple[Dict[str, Type[SpanExporter]], Dict[str, Type[LogExporter]]]: trace_exporters = {} + log_exporters = {} - for ( - exporter_name, - exporter_impl, - ) in _import_tracer_provider_config_components( - exporter_names, "opentelemetry_traces_exporter" + for (exporter_name, exporter_impl,) in _import_config_components( + trace_exporter_names, "opentelemetry_traces_exporter" ): if issubclass(exporter_impl, SpanExporter): trace_exporters[exporter_name] = exporter_impl else: raise RuntimeError(f"{exporter_name} is not a trace exporter") - return trace_exporters + + for (exporter_name, exporter_impl,) in _import_config_components( + log_exporter_names, "opentelemetry_logs_exporter" + ): + if issubclass(exporter_impl, LogExporter): + log_exporters[exporter_name] = exporter_impl + else: + raise RuntimeError(f"{exporter_name} is not a log exporter") + + return trace_exporters, log_exporters def _import_id_generator(id_generator_name: str) -> IdGenerator: # pylint: disable=unbalanced-tuple-unpacking - [ - (id_generator_name, id_generator_impl) - ] = _import_tracer_provider_config_components( + [(id_generator_name, id_generator_impl)] = _import_config_components( [id_generator_name.strip()], "opentelemetry_id_generator" ) @@ -144,11 +173,14 @@ def _import_id_generator(id_generator_name: str) -> IdGenerator: def _initialize_components(auto_instrumentation_version): - exporter_names = _get_exporter_names() - trace_exporters = _import_exporters(exporter_names) + trace_exporters, log_exporters = _import_exporters( + _get_exporter_names(environ.get(OTEL_TRACES_EXPORTER)), + _get_exporter_names(environ.get(_OTEL_LOGS_EXPORTER)), + ) id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) _init_tracing(trace_exporters, id_generator, auto_instrumentation_version) + _init_logging(log_exporters, auto_instrumentation_version) class _BaseConfigurator(ABC): diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 9243b80c67..8a4aadd479 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -18,19 +18,19 @@ from unittest.mock import patch from opentelemetry import trace -from opentelemetry.environment_variables import ( - OTEL_PYTHON_ID_GENERATOR, - OTEL_TRACES_EXPORTER, -) +from opentelemetry.environment_variables import OTEL_PYTHON_ID_GENERATOR from opentelemetry.sdk._configuration import ( _EXPORTER_OTLP, - _EXPORTER_OTLP_SPAN, + _EXPORTER_OTLP_PROTO_GRPC, _get_exporter_names, _get_id_generator, + _import_exporters, _import_id_generator, _init_tracing, ) +from opentelemetry.sdk._logs.export import ConsoleExporter from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator @@ -164,22 +164,34 @@ def test_trace_init_custom_id_generator(self, mock_iter_entry_points): class TestExporterNames(TestCase): def test_otlp_exporter_overwrite(self): - for exporter in [_EXPORTER_OTLP, _EXPORTER_OTLP_SPAN]: - with patch.dict(environ, {OTEL_TRACES_EXPORTER: exporter}): - self.assertEqual(_get_exporter_names(), [_EXPORTER_OTLP_SPAN]) + for exporter in [_EXPORTER_OTLP, _EXPORTER_OTLP_PROTO_GRPC]: + self.assertEqual( + _get_exporter_names(exporter), [_EXPORTER_OTLP_PROTO_GRPC] + ) - @patch.dict(environ, {OTEL_TRACES_EXPORTER: "jaeger,zipkin"}) def test_multiple_exporters(self): - self.assertEqual(sorted(_get_exporter_names()), ["jaeger", "zipkin"]) + self.assertEqual( + sorted(_get_exporter_names("jaeger,zipkin")), ["jaeger", "zipkin"] + ) - @patch.dict(environ, {OTEL_TRACES_EXPORTER: "none"}) def test_none_exporters(self): - self.assertEqual(sorted(_get_exporter_names()), []) + self.assertEqual(sorted(_get_exporter_names("none")), []) - @patch.dict(environ, {}, clear=True) def test_no_exporters(self): - self.assertEqual(sorted(_get_exporter_names()), []) + self.assertEqual(sorted(_get_exporter_names(None)), []) - @patch.dict(environ, {OTEL_TRACES_EXPORTER: ""}) def test_empty_exporters(self): - self.assertEqual(sorted(_get_exporter_names()), []) + self.assertEqual(sorted(_get_exporter_names("")), []) + + +class TestImportExporters(TestCase): + def test_console_exporters(self): + trace_exporters, logs_exporters = _import_exporters( + ["console"], ["console"] + ) + self.assertEqual( + trace_exporters["console"].__class__, ConsoleSpanExporter.__class__ + ) + self.assertEqual( + logs_exporters["console"].__class__, ConsoleExporter.__class__ + ) From 8bbaf76e59d6d0ac816109b6ba3378714c8b2333 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 3 Dec 2021 14:51:59 -0800 Subject: [PATCH 1069/1517] rename ConsoleExporter to ConsoleLogExporter (#2307) * rename ConsoleExporter to ConsoleLogExporter * update changelog --- CHANGELOG.md | 2 ++ opentelemetry-sdk/setup.cfg | 2 +- .../src/opentelemetry/sdk/_logs/export/__init__.py | 2 +- opentelemetry-sdk/tests/logs/test_export.py | 8 ++++---- opentelemetry-sdk/tests/test_configurator.py | 4 ++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f0cf21c60..cd154b753c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2303](https://github.com/open-telemetry/opentelemetry-python/pull/2303)) - Adding entrypoints for log emitter provider and console, otlp log exporters ([#2253](https://github.com/open-telemetry/opentelemetry-python/pull/2253)) +- Rename ConsoleExporter to ConsoleLogExporter + ([#2307](https://github.com/open-telemetry/opentelemetry-python/pull/2307)) ## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11 diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index a802596500..e78448dd82 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -57,7 +57,7 @@ opentelemetry_traces_exporter = opentelemetry_log_emitter_provider = sdk_log_emitter_provider = opentelemetry.sdk._logs:LogEmitterProvider opentelemetry_logs_exporter = - console = opentelemetry.sdk._logs.export:ConsoleExporter + console = opentelemetry.sdk._logs.export:ConsoleLogExporter opentelemetry_id_generator = random = opentelemetry.sdk.trace.id_generator:RandomIdGenerator opentelemetry_environment_variables = diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py index c705c2b249..87ac308317 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py @@ -63,7 +63,7 @@ def shutdown(self): """ -class ConsoleExporter(LogExporter): +class ConsoleLogExporter(LogExporter): """Implementation of :class:`LogExporter` that prints log records to the console. diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 45b83358f9..502c68ed75 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -31,7 +31,7 @@ ) from opentelemetry.sdk._logs.export import ( BatchLogProcessor, - ConsoleExporter, + ConsoleLogExporter, SimpleLogProcessor, ) from opentelemetry.sdk._logs.export.in_memory_log_exporter import ( @@ -321,7 +321,7 @@ def _target(): log_processor.shutdown() -class TestConsoleExporter(unittest.TestCase): +class TestConsoleLogExporter(unittest.TestCase): def test_export(self): # pylint: disable=no-self-use """Check that the console exporter prints log records.""" log_data = LogData( @@ -341,7 +341,7 @@ def test_export(self): # pylint: disable=no-self-use "first_name", "first_version" ), ) - exporter = ConsoleExporter() + exporter = ConsoleLogExporter() # Mocking stdout interferes with debugging and test reporting, mock on # the exporter instance instead. @@ -362,7 +362,7 @@ def formatter(record): # pylint: disable=unused-argument return mock_record_str mock_stdout = Mock() - exporter = ConsoleExporter(out=mock_stdout, formatter=formatter) + exporter = ConsoleLogExporter(out=mock_stdout, formatter=formatter) log_data = LogData( log_record=LogRecord(), instrumentation_info=InstrumentationInfo( diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 8a4aadd479..ca755544b7 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -28,7 +28,7 @@ _import_id_generator, _init_tracing, ) -from opentelemetry.sdk._logs.export import ConsoleExporter +from opentelemetry.sdk._logs.export import ConsoleLogExporter from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator @@ -193,5 +193,5 @@ def test_console_exporters(self): trace_exporters["console"].__class__, ConsoleSpanExporter.__class__ ) self.assertEqual( - logs_exporters["console"].__class__, ConsoleExporter.__class__ + logs_exporters["console"].__class__, ConsoleLogExporter.__class__ ) From 9ef6829b23a6bc59154ed0c644c72833d0a0e46f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 8 Dec 2021 15:09:07 -0600 Subject: [PATCH 1070/1517] Refactor Meter and MeterProvider (#2296) * Refactor Meter and MeterProvider Fixes #2292 * Remove synchronous instruments list * Undo changes to _time.py * Make lint pass * Remove unnecesary docstring * Remove _create_instrument * Add missing callback * Remove instrument creating methods from MeterProvider * Remove views and other attributes * Removed dataclass dependency * Add warning * Add export directory * Add missing callbacks --- .../src/opentelemetry/util/_time.py | 1 - .../opentelemetry/sdk/_metrics/__init__.py | 177 ++++++++---------- .../sdk/_metrics/export/__init__.py | 13 ++ .../sdk/_metrics/export/metric_exporter.py | 17 ++ .../opentelemetry/sdk/_metrics/instrument.py | 140 +++++++------- .../sdk/_metrics/metric_reader.py | 17 ++ .../tests/metrics/test_metrics.py | 162 +++++----------- 7 files changed, 235 insertions(+), 292 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/metric_exporter.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py diff --git a/opentelemetry-api/src/opentelemetry/util/_time.py b/opentelemetry-api/src/opentelemetry/util/_time.py index ceaca22e8d..a3fd113ce3 100644 --- a/opentelemetry-api/src/opentelemetry/util/_time.py +++ b/opentelemetry-api/src/opentelemetry/util/_time.py @@ -28,7 +28,6 @@ def _time_ns() -> int: return int(time() * 1e9) - else: from time import time_ns diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 1f306f68b3..bf04d8fe1f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -12,17 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=function-redefined,too-many-ancestors - -from abc import ABC, abstractmethod from atexit import register, unregister from logging import getLogger -from typing import Optional +from threading import Lock +from typing import Optional, Sequence from opentelemetry._metrics import Meter as APIMeter from opentelemetry._metrics import MeterProvider as APIMeterProvider from opentelemetry._metrics import _DefaultMeter +from opentelemetry._metrics.instrument import Counter as APICounter +from opentelemetry._metrics.instrument import Histogram as APIHistogram +from opentelemetry._metrics.instrument import ( + ObservableCounter as APIObservableCounter, +) +from opentelemetry._metrics.instrument import ( + ObservableGauge as APIObservableGauge, +) from opentelemetry._metrics.instrument import ( + ObservableUpDownCounter as APIObservableUpDownCounter, +) +from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter +from opentelemetry.sdk._metrics.export.metric_exporter import MetricExporter +from opentelemetry.sdk._metrics.instrument import ( Counter, Histogram, ObservableCounter, @@ -30,6 +41,7 @@ ObservableUpDownCounter, UpDownCounter, ) +from opentelemetry.sdk._metrics.metric_reader import MetricReader from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -46,37 +58,41 @@ def __init__( self._instrumentation_info = instrumentation_info self._meter_provider = meter_provider - def create_counter(self, name, unit=None, description=None) -> Counter: - # FIXME implement this method - pass + def create_counter(self, name, unit=None, description=None) -> APICounter: + return Counter(self._instrumentation_info, name, unit, description) def create_up_down_counter( self, name, unit=None, description=None - ) -> UpDownCounter: - # FIXME implement this method - pass + ) -> APIUpDownCounter: + return UpDownCounter( + self._instrumentation_info, name, unit, description + ) def create_observable_counter( self, name, callback, unit=None, description=None - ) -> ObservableCounter: - # FIXME implement this method - pass + ) -> APIObservableCounter: + return ObservableCounter( + self._instrumentation_info, name, callback, unit, description + ) - def create_histogram(self, name, unit=None, description=None) -> Histogram: - # FIXME implement this method - pass + def create_histogram( + self, name, unit=None, description=None + ) -> APIHistogram: + return Histogram(self._instrumentation_info, name, unit, description) def create_observable_gauge( self, name, callback, unit=None, description=None - ) -> ObservableGauge: - # FIXME implement this method - pass + ) -> APIObservableGauge: + return ObservableGauge( + self._instrumentation_info, name, callback, unit, description + ) def create_observable_up_down_counter( self, name, callback, unit=None, description=None - ) -> ObservableUpDownCounter: - # FIXME implement this method - pass + ) -> APIObservableUpDownCounter: + return ObservableUpDownCounter( + self._instrumentation_info, name, callback, unit, description + ) class MeterProvider(APIMeterProvider): @@ -84,57 +100,26 @@ class MeterProvider(APIMeterProvider): def __init__( self, + metric_exporters: Sequence[MetricExporter] = (), + metric_readers: Sequence[MetricReader] = (), resource: Resource = Resource.create({}), shutdown_on_exit: bool = True, ): - self._resource = resource + self._lock = Lock() self._atexit_handler = None if shutdown_on_exit: self._atexit_handler = register(self.shutdown) - self._metric_readers = [] - self._metric_exporters = [] - self._views = [] - self._shutdown = False - - def get_meter( - self, - name: str, - version: Optional[str] = None, - schema_url: Optional[str] = None, - ) -> Meter: - - if self._shutdown: - _logger.warning( - "A shutdown `MeterProvider` can not provide a `Meter`" - ) - return _DefaultMeter(name, version=version, schema_url=schema_url) - - return Meter(InstrumentationInfo(name, version, schema_url), self) - - def shutdown(self): - # FIXME implement a timeout - - if self._shutdown: - _logger.warning("shutdown can only be called once") - return False - - result = True + self._metric_readers = metric_readers for metric_reader in self._metric_readers: - result = result and metric_reader.shutdown() - - for metric_exporter in self._metric_exporters: - result = result and metric_exporter.shutdown() + metric_reader._register_meter_provider(self) - self._shutdown = True - - if self._atexit_handler is not None: - unregister(self._atexit_handler) - self._atexit_handler = None + self._metric_exporters = metric_exporters - return result + self._resource = resource + self._shutdown = False def force_flush(self) -> bool: @@ -161,56 +146,46 @@ def force_flush(self) -> bool: return metric_reader_result and metric_exporter_result - def register_metric_reader(self, metric_reader: "MetricReader") -> None: - # FIXME protect this method against race conditions - self._metric_readers.append(metric_reader) - - def register_metric_exporter( - self, metric_exporter: "MetricExporter" - ) -> None: - # FIXME protect this method against race conditions - self._metric_exporters.append(metric_exporter) - - def register_view(self, view: "View") -> None: - # FIXME protect this method against race conditions - self._views.append(view) - + def shutdown(self): + # FIXME implement a timeout -class MetricReader(ABC): - def __init__(self): - self._shutdown = False + if self._shutdown: + _logger.warning("shutdown can only be called once") + return False - @abstractmethod - def collect(self): - pass + result = True - def shutdown(self): - # FIXME this will need a Once wrapper - self._shutdown = True + for metric_reader in self._metric_readers: + result = result and metric_reader.shutdown() + if not result: + _logger.warning("A MetricReader failed to shutdown") -class MetricExporter(ABC): - def __init__(self): - self._shutdown = False + for metric_exporter in self._metric_exporters: + result = result and metric_exporter.shutdown() - @abstractmethod - def export(self): - pass + if not result: + _logger.warning("A MetricExporter failed to shutdown") - def shutdown(self): - # FIXME this will need a Once wrapper self._shutdown = True + if self._atexit_handler is not None: + unregister(self._atexit_handler) + self._atexit_handler = None -class View: - pass - + return result -class ConsoleMetricExporter(MetricExporter): - def export(self): - pass + def get_meter( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> Meter: + if self._shutdown: + _logger.warning( + "A shutdown `MeterProvider` can not provide a `Meter`" + ) + return _DefaultMeter(name, version=version, schema_url=schema_url) -class SDKMetricReader(MetricReader): - def collect(self): - pass + return Meter(InstrumentationInfo(name, version, schema_url), self) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/metric_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/metric_exporter.py new file mode 100644 index 0000000000..e9c6d6aae0 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/metric_exporter.py @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class MetricExporter: + pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index fc63311ce7..cd1510f422 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -19,142 +19,142 @@ # all instances. Implementations of these classes must not make any change to # this default dictionary in __init__. +from opentelemetry._metrics.instrument import Counter as APICounter +from opentelemetry._metrics.instrument import Histogram as APIHistogram from opentelemetry._metrics.instrument import ( - Counter, - Histogram, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, + ObservableCounter as APIObservableCounter, ) -from opentelemetry.sdk._metrics.aggregation import ( - ExplicitBucketHistogramAggregation, - LastValueAggregation, - SumAggregation, +from opentelemetry._metrics.instrument import ( + ObservableGauge as APIObservableGauge, +) +from opentelemetry._metrics.instrument import ( + ObservableUpDownCounter as APIObservableUpDownCounter, ) +from opentelemetry._metrics.instrument import TCallback +from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo class _Instrument: def __init__( self, - name, - unit="", - description="", - aggregation=None, - aggregation_config={}, + instrumentation_info: InstrumentationInfo, + name: str, + *args, + unit: str = "", + description: str = "", ): - self._attributes_aggregations = {} - self._aggregation = aggregation - self._aggregation_config = aggregation_config - aggregation(self, **aggregation_config) + self._instrumentation_info = instrumentation_info + super().__init__(name, *args, unit=unit, description=description) -class Counter(_Instrument, Counter): +class Counter(_Instrument, APICounter): def __init__( self, - name, - unit="", - description="", - aggregation=SumAggregation, - aggregation_config={}, + instrumentation_info: InstrumentationInfo, + name: str, + unit: str = "", + description: str = "", ): super().__init__( + instrumentation_info, name, unit=unit, description=description, - aggregation=aggregation, - aggregation_config=aggregation_config, ) + def add(self, amount, attributes=None): + # FIXME check that the amount is non negative + pass -class UpDownCounter(_Instrument, UpDownCounter): + +class UpDownCounter(_Instrument, APIUpDownCounter): def __init__( self, - name, - unit="", - description="", - aggregation=SumAggregation, - aggregation_config={}, + instrumentation_info: InstrumentationInfo, + name: str, + unit: str = "", + description: str = "", ): super().__init__( + instrumentation_info, name, unit=unit, description=description, - aggregation=aggregation, - aggregation_config=aggregation_config, ) + def add(self, amount, attributes=None): + pass + -class ObservableCounter(_Instrument, ObservableCounter): +class ObservableCounter(_Instrument, APIObservableCounter): def __init__( self, - name, - callback, - unit="", - description="", - aggregation=SumAggregation, - aggregation_config={}, + instrumentation_info: InstrumentationInfo, + name: str, + callback: TCallback, + unit: str = "", + description: str = "", ): super().__init__( + instrumentation_info, name, + callback, unit=unit, description=description, - aggregation=aggregation, - aggregation_config=aggregation_config, ) -class ObservableUpDownCounter(_Instrument, ObservableUpDownCounter): +class ObservableUpDownCounter(_Instrument, APIObservableUpDownCounter): def __init__( self, - name, - callback, - unit="", - description="", - aggregation=SumAggregation, - aggregation_config={}, + instrumentation_info: InstrumentationInfo, + name: str, + callback: TCallback, + unit: str = "", + description: str = "", ): super().__init__( + instrumentation_info, name, + callback, unit=unit, description=description, - aggregation=aggregation, - aggregation_config=aggregation_config, ) -class Histogram(_Instrument, Histogram): +class Histogram(_Instrument, APIHistogram): def __init__( self, - name, - unit="", - description="", - aggregation=ExplicitBucketHistogramAggregation, - aggregation_config={}, + instrumentation_info: InstrumentationInfo, + name: str, + unit: str = "", + description: str = "", ): super().__init__( + instrumentation_info, name, unit=unit, description=description, - aggregation=aggregation, - aggregation_config=aggregation_config, ) + def record(self, amount, attributes=None): + pass + -class ObservableGauge(_Instrument, ObservableGauge): +class ObservableGauge(_Instrument, APIObservableGauge): def __init__( self, - name, - callback, - unit="", - description="", - aggregation=LastValueAggregation, - aggregation_config={}, + instrumentation_info: InstrumentationInfo, + name: str, + callback: TCallback, + unit: str = "", + description: str = "", ): super().__init__( + instrumentation_info, name, + callback, unit=unit, description=description, - aggregation=aggregation, - aggregation_config=aggregation_config, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py new file mode 100644 index 0000000000..4bbae6501c --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class MetricReader: + pass diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index b473f4f69f..0077d5b1cd 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -17,11 +17,14 @@ from unittest import TestCase from unittest.mock import Mock -from opentelemetry.sdk._metrics import ( - ConsoleMetricExporter, - MeterProvider, - SDKMetricReader, - View, +from opentelemetry.sdk._metrics import Meter, MeterProvider +from opentelemetry.sdk._metrics.instrument import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, ) from opentelemetry.sdk.resources import Resource @@ -40,7 +43,7 @@ def test_meter_provider_resource(self): self.assertIsInstance(meter_provider_1._resource, Resource) resource = Resource({"key": "value"}) - self.assertIs(MeterProvider(resource)._resource, resource) + self.assertIs(MeterProvider(resource=resource)._resource, resource) def test_get_meter(self): """ @@ -58,75 +61,6 @@ def test_get_meter(self): self.assertEqual(meter._instrumentation_info.version, "version") self.assertEqual(meter._instrumentation_info.schema_url, "schema_url") - def test_register_metric_reader(self): - """ " - `MeterProvider` provides a way to configure `SDKMetricReader`s. - """ - - meter_provider = MeterProvider() - - self.assertTrue(hasattr(meter_provider, "register_metric_reader")) - - metric_reader = SDKMetricReader() - - meter_provider.register_metric_reader(metric_reader) - - self.assertTrue(meter_provider._metric_readers, [metric_reader]) - - def test_register_metric_exporter(self): - """ " - `MeterProvider` provides a way to configure `ConsoleMetricExporter`s. - """ - - meter_provider = MeterProvider() - - self.assertTrue(hasattr(meter_provider, "register_metric_exporter")) - - metric_exporter = ConsoleMetricExporter() - - meter_provider.register_metric_exporter(metric_exporter) - - self.assertTrue(meter_provider._metric_exporters, [metric_exporter]) - - def test_register_view(self): - """ " - `MeterProvider` provides a way to configure `View`s. - """ - - meter_provider = MeterProvider() - - self.assertTrue(hasattr(meter_provider, "register_view")) - - view = View() - - meter_provider.register_view(view) - - self.assertTrue(meter_provider._views, [view]) - - def test_meter_configuration(self): - """ - Any updated configuration is applied to all returned `Meter`s. - """ - - meter_provider = MeterProvider() - - view_0 = View() - - meter_provider.register_view(view_0) - - meter_0 = meter_provider.get_meter("meter_0") - meter_1 = meter_provider.get_meter("meter_1") - - self.assertEqual(meter_0._meter_provider._views, [view_0]) - self.assertEqual(meter_1._meter_provider._views, [view_0]) - - view_1 = View() - - meter_provider.register_view(view_1) - - self.assertEqual(meter_0._meter_provider._views, [view_0, view_1]) - self.assertEqual(meter_1._meter_provider._views, [view_0, view_1]) - def test_shutdown_subsequent_calls(self): """ No subsequent attempts to get a `Meter` are allowed after calling @@ -142,64 +76,52 @@ def test_shutdown_subsequent_calls(self): with self.assertLogs(level=WARNING): meter_provider.shutdown() - def test_shutdown_result(self): - """ - `MeterProvider.shutdown` provides a way to let the caller know if it - succeeded or failed. - `MeterProvider.shutdown` is implemented by at least invoking - ``shutdown`` on all registered `SDKMetricReader`s and `ConsoleMetricExporter`s. - """ +class TestMeter(TestCase): + def setUp(self): + self.meter = Meter(Mock(), MeterProvider()) - meter_provider = MeterProvider() - - meter_provider.register_metric_reader( - Mock(**{"shutdown.return_value": True}) - ) - meter_provider.register_metric_exporter( - Mock(**{"shutdown.return_value": True}) + def test_create_counter(self): + counter = self.meter.create_counter( + "name", unit="unit", description="description" ) - self.assertTrue(meter_provider.shutdown()) + self.assertIsInstance(counter, Counter) - meter_provider = MeterProvider() - - meter_provider.register_metric_reader( - Mock(**{"shutdown.return_value": True}) - ) - meter_provider.register_metric_exporter( - Mock(**{"shutdown.return_value": False}) + def test_create_up_down_counter(self): + up_down_counter = self.meter.create_up_down_counter( + "name", unit="unit", description="description" ) - self.assertFalse(meter_provider.shutdown()) + self.assertIsInstance(up_down_counter, UpDownCounter) - def test_force_flush_result(self): - """ - `MeterProvider.force_flush` provides a way to let the caller know if it - succeeded or failed. + def test_create_observable_counter(self): + observable_counter = self.meter.create_observable_counter( + "name", Mock(), unit="unit", description="description" + ) - `MeterProvider.force_flush` is implemented by at least invoking - ``force_flush`` on all registered `SDKMetricReader`s and `ConsoleMetricExporter`s. - """ + self.assertIsInstance(observable_counter, ObservableCounter) - meter_provider = MeterProvider() - - meter_provider.register_metric_reader( - Mock(**{"force_flush.return_value": True}) - ) - meter_provider.register_metric_exporter( - Mock(**{"force_flush.return_value": True}) + def test_create_histogram(self): + histogram = self.meter.create_histogram( + "name", unit="unit", description="description" ) - self.assertTrue(meter_provider.force_flush()) + self.assertIsInstance(histogram, Histogram) - meter_provider = MeterProvider() + def test_create_observable_gauge(self): + observable_gauge = self.meter.create_observable_gauge( + "name", Mock(), unit="unit", description="description" + ) + + self.assertIsInstance(observable_gauge, ObservableGauge) - meter_provider.register_metric_reader( - Mock(**{"force_flush.return_value": True}) + def test_create_observable_up_down_counter(self): + observable_up_down_counter = ( + self.meter.create_observable_up_down_counter( + "name", Mock(), unit="unit", description="description" + ) ) - meter_provider.register_metric_exporter( - Mock(**{"force_flush.return_value": False}) + self.assertIsInstance( + observable_up_down_counter, ObservableUpDownCounter ) - - self.assertFalse(meter_provider.force_flush()) From af862eb3292edf97a7cd5eae03f58bb6e27f97c8 Mon Sep 17 00:00:00 2001 From: Robb Kidd Date: Fri, 10 Dec 2021 11:41:49 -0500 Subject: [PATCH 1071/1517] [docs] Include opentelemetry-distro as dep for auto-instrumentation example (#2317) --- docs/examples/auto-instrumentation/README.rst | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index 7ebd8a76d3..4fdb896482 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -72,17 +72,21 @@ Install ------- Run the following commands to install the appropriate packages. The -``opentelemetry-instrumentation`` package provides several -commands that help automatically instruments a program. +``opentelemetry-distro`` package depends on a few others, like ``opentelemetry-sdk`` +for custom instrumentation of your own code and ``opentelemetry-instrumentation`` which +provides several commands that help automatically instrument a program. .. code:: sh - $ pip install opentelemetry-sdk - $ pip install opentelemetry-instrumentation + $ pip install opentelemetry-distro $ pip install opentelemetry-instrumentation-flask $ pip install flask $ pip install requests +The examples that follow send instrumentation results to the console. Learn more +about installing and configuring the `OpenTelemetry Distro <../distro>`_ to send +telemetry to other destinations, like an OpenTelemetry Collector. + Execute --------- @@ -106,8 +110,8 @@ scripts that make up this example: $ source auto_instrumentation/bin/activate $ python client.py testing -When you execute ``server_instrumented.py`` it returns a JSON response -similar to the following example: +The console running ``server_instrumented.py`` will display the spans generated by instrumentation as JSON. +The spans should appear similar to the following example: .. code:: sh @@ -162,8 +166,8 @@ command again: $ python client.py testing -When you execute ``server_uninstrumented.py`` it returns a JSON response -similar to the following example: +The console running ``server_uninstrumented.py`` will display the spans generated by instrumentation as JSON. +The spans should appear similar to the following example: .. code:: sh @@ -227,11 +231,3 @@ reloader. To run instrumentation while the debug mode is enabled, set the if __name__ == "__main__": app.run(port=8082, debug=True, use_reloader=False) - - -Additional resources -~~~~~~~~~~~~~~~~~~~~ - -In order to send telemetry to an OpenTelemetry Collector without doing any -additional configuration, read about the `OpenTelemetry Distro <../distro>`_ -package. From 8d4a804b49ac36d1cc629931960168dacde4d2ab Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 14 Dec 2021 10:06:16 -0800 Subject: [PATCH 1072/1517] add OTEL_LOGS_EXPORTER env variable (#2320) As per the change in the specification, adding the variable. There will be a follow up PR to use the variable. --- CHANGELOG.md | 2 ++ .../src/opentelemetry/environment_variables.py | 6 ++++++ .../src/opentelemetry/sdk/_configuration/__init__.py | 6 ++---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd154b753c..052b02278a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2253](https://github.com/open-telemetry/opentelemetry-python/pull/2253)) - Rename ConsoleExporter to ConsoleLogExporter ([#2307](https://github.com/open-telemetry/opentelemetry-python/pull/2307)) +- Adding OTEL_LOGS_EXPORTER environment variable + ([#2320](https://github.com/open-telemetry/opentelemetry-python/pull/2320)) ## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11 diff --git a/opentelemetry-api/src/opentelemetry/environment_variables.py b/opentelemetry-api/src/opentelemetry/environment_variables.py index af2319453c..0810acc5ff 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables.py @@ -12,6 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +OTEL_LOGS_EXPORTER = "OTEL_LOGS_EXPORTER" +""" +.. envvar:: OTEL_LOGS_EXPORTER + +""" + OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER" """ .. envvar:: OTEL_METRICS_EXPORTER diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 377b21c828..fdd3a25a7c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -25,6 +25,7 @@ from opentelemetry import trace from opentelemetry.environment_variables import ( + OTEL_LOGS_EXPORTER, OTEL_PYTHON_ID_GENERATOR, OTEL_TRACES_EXPORTER, ) @@ -45,9 +46,6 @@ _RANDOM_ID_GENERATOR = "random" _DEFAULT_ID_GENERATOR = _RANDOM_ID_GENERATOR -# TODO: add log exporter env variable -_OTEL_LOGS_EXPORTER = "OTEL_LOGS_EXPORTER" - def _get_id_generator() -> str: return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR) @@ -175,7 +173,7 @@ def _import_id_generator(id_generator_name: str) -> IdGenerator: def _initialize_components(auto_instrumentation_version): trace_exporters, log_exporters = _import_exporters( _get_exporter_names(environ.get(OTEL_TRACES_EXPORTER)), - _get_exporter_names(environ.get(_OTEL_LOGS_EXPORTER)), + _get_exporter_names(environ.get(OTEL_LOGS_EXPORTER)), ) id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) From 636262db3afa9f43e449f759c566c704da772a4c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 14 Dec 2021 12:43:13 -0600 Subject: [PATCH 1073/1517] Refactor metrics instrument (#2297) * Refactor instrument Fixes #2294 * Add Callback type back to API * Create instrument with meter * Make default_aggregation public * Use right import * Remove default aggregations --- .../src/opentelemetry/_metrics/instrument.py | 46 +----- .../opentelemetry/sdk/_metrics/instrument.py | 140 ++++++------------ .../opentelemetry/sdk/_metrics/measurement.py | 17 +++ .../metrics/integration_test/test_cpu_time.py | 10 +- .../tests/metrics/test_instrument.py | 80 ++++++++++ 5 files changed, 149 insertions(+), 144 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py rename {opentelemetry-api => opentelemetry-sdk}/tests/metrics/integration_test/test_cpu_time.py (97%) create mode 100644 opentelemetry-sdk/tests/metrics/test_instrument.py diff --git a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py index 76838209eb..d6f8633173 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py @@ -17,7 +17,6 @@ # type: ignore from abc import ABC, abstractmethod -from collections import abc as collections_abc from logging import getLogger from typing import ( Callable, @@ -33,10 +32,11 @@ from opentelemetry import _metrics as metrics from opentelemetry._metrics.measurement import Measurement -_TInstrumentCallback = Callable[[], Iterable[Measurement]] -_TInstrumentCallbackGenerator = Generator[Iterable[Measurement], None, None] -TCallback = Union[_TInstrumentCallback, _TInstrumentCallbackGenerator] InstrumentT = TypeVar("InstrumentT", bound="Instrument") +CallbackT = Union[ + Callable[[], Iterable[Measurement]], + Generator[Iterable[Measurement], None, None], +] _logger = getLogger(__name__) @@ -87,45 +87,11 @@ class Asynchronous(Instrument): def __init__( self, name, - callback: TCallback, - *args, + callback, unit="", description="", - **kwargs ): - super().__init__( - name, *args, unit=unit, description=description, **kwargs - ) - - if isinstance(callback, collections_abc.Callable): - self._callback = callback - elif isinstance(callback, collections_abc.Generator): - self._callback = self._wrap_generator_callback(callback) - # FIXME check that callback is a callable or generator - - @staticmethod - def _wrap_generator_callback( - generator_callback: _TInstrumentCallbackGenerator, - ) -> _TInstrumentCallback: - """Wraps a generator style callback into a callable one""" - has_items = True - - def inner() -> Iterable[Measurement]: - nonlocal has_items - if not has_items: - return [] - - try: - return next(generator_callback) - except StopIteration: - has_items = False - # FIXME handle the situation where the callback generator has - # run out of measurements - return [] - - return inner - - # FIXME check that callbacks return an iterable of Measurements + super().__init__(name, unit=unit, description=description) class _Adding(Instrument): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index cd1510f422..6a68dbeab6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -12,13 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=function-redefined -# pylint: disable=dangerous-default-value -# Classes in this module use dictionaries as default arguments. This is -# considered dangerous by pylint because the default dictionary is shared by -# all instances. Implementations of these classes must not make any change to -# this default dictionary in __init__. +# pylint: disable=too-many-ancestors +from typing import Dict, Generator, Iterable, Union + +from opentelemetry._metrics.instrument import CallbackT from opentelemetry._metrics.instrument import Counter as APICounter from opentelemetry._metrics.instrument import Histogram as APIHistogram from opentelemetry._metrics.instrument import ( @@ -30,131 +28,77 @@ from opentelemetry._metrics.instrument import ( ObservableUpDownCounter as APIObservableUpDownCounter, ) -from opentelemetry._metrics.instrument import TCallback from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter +from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -class _Instrument: +class _Synchronous: def __init__( self, instrumentation_info: InstrumentationInfo, name: str, - *args, unit: str = "", description: str = "", ): self._instrumentation_info = instrumentation_info - super().__init__(name, *args, unit=unit, description=description) + super().__init__(name, unit=unit, description=description) -class Counter(_Instrument, APICounter): +class _Asynchronous: def __init__( self, instrumentation_info: InstrumentationInfo, name: str, + callback: CallbackT, unit: str = "", description: str = "", ): - super().__init__( - instrumentation_info, - name, - unit=unit, - description=description, - ) - - def add(self, amount, attributes=None): - # FIXME check that the amount is non negative - pass + self._instrumentation_info = instrumentation_info + super().__init__(name, callback, unit=unit, description=description) -class UpDownCounter(_Instrument, APIUpDownCounter): - def __init__( - self, - instrumentation_info: InstrumentationInfo, - name: str, - unit: str = "", - description: str = "", - ): - super().__init__( - instrumentation_info, - name, - unit=unit, - description=description, - ) - - def add(self, amount, attributes=None): - pass + self._callback = callback + if isinstance(callback, Generator): -class ObservableCounter(_Instrument, APIObservableCounter): - def __init__( - self, - instrumentation_info: InstrumentationInfo, - name: str, - callback: TCallback, - unit: str = "", - description: str = "", - ): - super().__init__( - instrumentation_info, - name, - callback, - unit=unit, - description=description, - ) + def inner() -> Iterable[Measurement]: + return next(callback) + self._callback = inner -class ObservableUpDownCounter(_Instrument, APIObservableUpDownCounter): - def __init__( - self, - instrumentation_info: InstrumentationInfo, - name: str, - callback: TCallback, - unit: str = "", - description: str = "", + @property + def callback(self) -> CallbackT: + return self._callback + + +class Counter(_Synchronous, APICounter): + def add( + self, amount: Union[int, float], attributes: Dict[str, str] = None ): - super().__init__( - instrumentation_info, - name, - callback, - unit=unit, - description=description, - ) + if amount < 0: + raise Exception("amount must be non negative") -class Histogram(_Instrument, APIHistogram): - def __init__( - self, - instrumentation_info: InstrumentationInfo, - name: str, - unit: str = "", - description: str = "", +class UpDownCounter(_Synchronous, APIUpDownCounter): + def add( + self, amount: Union[int, float], attributes: Dict[str, str] = None ): - super().__init__( - instrumentation_info, - name, - unit=unit, - description=description, - ) + pass + +class ObservableCounter(_Asynchronous, APIObservableCounter): + pass + + +class ObservableUpDownCounter(_Asynchronous, APIObservableUpDownCounter): + pass + + +class Histogram(_Synchronous, APIHistogram): def record(self, amount, attributes=None): pass -class ObservableGauge(_Instrument, APIObservableGauge): - def __init__( - self, - instrumentation_info: InstrumentationInfo, - name: str, - callback: TCallback, - unit: str = "", - description: str = "", - ): - super().__init__( - instrumentation_info, - name, - callback, - unit=unit, - description=description, - ) +class ObservableGauge(_Asynchronous, APIObservableGauge): + pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py new file mode 100644 index 0000000000..fbaae02c78 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class Measurement: + pass diff --git a/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py similarity index 97% rename from opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py rename to opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py index 96176a58b3..d16dce0b01 100644 --- a/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py @@ -17,8 +17,8 @@ from typing import Generator, Iterable from unittest import TestCase -from opentelemetry._metrics import _DefaultMeter from opentelemetry._metrics.measurement import Measurement +from opentelemetry.sdk._metrics import MeterProvider # FIXME Test that the instrument methods can be called concurrently safely. @@ -61,8 +61,6 @@ class TestCpuTimeIntegration(TestCase): ] def test_cpu_time_callback(self): - meter = _DefaultMeter("foo") - def cpu_time_callback() -> Iterable[Measurement]: procstat = io.StringIO(self.procstat_str) procstat.readline() # skip the first line @@ -98,9 +96,10 @@ def cpu_time_callback() -> Iterable[Measurement]: int(states[8]) // 100, {"cpu": cpu, "state": "guest_nice"} ) + meter = MeterProvider().get_meter("name") observable_counter = meter.create_observable_counter( "system.cpu.time", - callback=cpu_time_callback, + cpu_time_callback, unit="s", description="CPU time", ) @@ -108,8 +107,6 @@ def cpu_time_callback() -> Iterable[Measurement]: self.assertEqual(measurements, self.measurements_expected) def test_cpu_time_generator(self): - meter = _DefaultMeter("foo") - def cpu_time_generator() -> Generator[ Iterable[Measurement], None, None ]: @@ -176,6 +173,7 @@ def cpu_time_generator() -> Generator[ ) yield measurements + meter = MeterProvider().get_meter("name") observable_counter = meter.create_observable_counter( "system.cpu.time", callback=cpu_time_generator(), diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py new file mode 100644 index 0000000000..b54391b2c0 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -0,0 +1,80 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase +from unittest.mock import Mock + +from opentelemetry.sdk._metrics.instrument import ( + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, +) + + +class TestObservableGauge(TestCase): + def test_callable_callback(self): + def callback(): + return [1, 2, 3] + + observable_gauge = ObservableGauge(Mock(), "name", callback) + + self.assertEqual(observable_gauge.callback(), [1, 2, 3]) + + def test_generator_callback(self): + def callback(): + yield [1, 2, 3] + + observable_gauge = ObservableGauge(Mock(), "name", callback()) + + self.assertEqual(observable_gauge.callback(), [1, 2, 3]) + + +class TestObservableCounter(TestCase): + def test_callable_callback(self): + def callback(): + return [1, 2, 3] + + observable_counter = ObservableCounter(Mock(), "name", callback) + + self.assertEqual(observable_counter.callback(), [1, 2, 3]) + + def test_generator_callback(self): + def callback(): + yield [1, 2, 3] + + observable_counter = ObservableCounter(Mock(), "name", callback()) + + self.assertEqual(observable_counter.callback(), [1, 2, 3]) + + +class TestObservableUpDownCounter(TestCase): + def test_callable_callback(self): + def callback(): + return [1, 2, 3] + + observable_up_down_counter = ObservableUpDownCounter( + Mock(), "name", callback + ) + + self.assertEqual(observable_up_down_counter.callback(), [1, 2, 3]) + + def test_generator_callback(self): + def callback(): + yield [1, 2, 3] + + observable_up_down_counter = ObservableUpDownCounter( + Mock(), "name", callback() + ) + + self.assertEqual(observable_up_down_counter.callback(), [1, 2, 3]) From a00a1e4b9f49a771f5f5fcfc9b3f6499c65cb573 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 14 Dec 2021 11:10:10 -0800 Subject: [PATCH 1074/1517] `tests`: added missing tests (#2325) A couple of the exporters were not being tested by the opentelemetry-exporter-otlp package tests. Co-authored-by: Diego Hurtado --- .../tests/test_otlp.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py index ab3173928c..5b1a7d7fde 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py @@ -14,16 +14,23 @@ import unittest +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( + OTLPLogExporter, +) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) +from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + OTLPSpanExporter as HTTPSpanExporter, +) class TestOTLPExporters(unittest.TestCase): def test_constructors(self): - try: - OTLPSpanExporter() - except Exception: # pylint: disable=broad-except - self.fail( - "Unexpected exception raised when instantiating OTLPSpanExporter" - ) + for exporter in [OTLPSpanExporter, HTTPSpanExporter, OTLPLogExporter]: + try: + exporter() + except Exception: # pylint: disable=broad-except + self.fail( + f"Unexpected exception raised when instantiating {exporter.__name__}" + ) From dfb5c66ae310001bb40326f6286345b7fa252aba Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 15 Dec 2021 13:40:13 -0800 Subject: [PATCH 1075/1517] add `otlp` entrypoint for logs (#2322) * add `otlp` entrypoint for logs To be in-line with the trace exporter. * update changelog * add newline --- CHANGELOG.md | 2 ++ exporter/opentelemetry-exporter-otlp/setup.cfg | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 052b02278a..4ac6ca8a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2307](https://github.com/open-telemetry/opentelemetry-python/pull/2307)) - Adding OTEL_LOGS_EXPORTER environment variable ([#2320](https://github.com/open-telemetry/opentelemetry-python/pull/2320)) +- Add otlp entrypoint for log exporter + ([#2322](https://github.com/open-telemetry/opentelemetry-python/pull/2322)) ## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11 diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 30c5b1f919..1e61f8e27e 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -45,3 +45,5 @@ install_requires = [options.entry_points] opentelemetry_traces_exporter = otlp = opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter +opentelemetry_logs_exporter = + otlp = opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter From 49e41aa4217c5a2dea937a8c13010e821be5790e Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Fri, 17 Dec 2021 22:34:09 +0530 Subject: [PATCH 1076/1517] updating changelogs and version to 1.8.0-0.27b0 (#2338) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 4 +++- eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- .../src/opentelemetry/test/version.py | 2 +- 29 files changed, 36 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1e01829473..d882313d14 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 6b83f1680f9a10900cb1da4e935252176ee05c21 + CONTRIB_REPO_SHA: b8d67dbc2f06e54dbbacd41fec085a3c0c180c71 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ac6ca8a53..85a9346301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.7.0-0.26b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.8.0-0.27b0...HEAD) + +## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17 - Adds Aggregation and instruments as part of Metrics SDK ([#2234](https://github.com/open-telemetry/opentelemetry-python/pull/2234)) diff --git a/eachdist.ini b/eachdist.ini index 11e31e2e13..d76fe73ef3 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.7.1 +version=1.8.0 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.26b1 +version=0.27b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 4788fe6398..87f6a8da6c 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 4788fe6398..87f6a8da6c 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 7e2a66630c..fc69e9c461 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -40,8 +40,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.7.1 - opentelemetry-exporter-jaeger-thrift == 1.7.1 + opentelemetry-exporter-jaeger-proto-grpc == 1.8.0 + opentelemetry-exporter-jaeger-thrift == 1.8.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 4788fe6398..87f6a8da6c 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index a48aab26a5..f0adf9ba13 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.26b1" +__version__ = "0.27b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index fb5cfe0dc2..b6fac99bbc 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.7.1 + opentelemetry-proto == 1.8.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index d19c1f14ba..bea3e76cbe 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index 7afd8e02e2..cddae1da45 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.7.1 + opentelemetry-proto == 1.8.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index d19c1f14ba..bea3e76cbe 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 1e61f8e27e..86ce4b6e2b 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -39,8 +39,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.7.1 - opentelemetry-exporter-otlp-proto-http == 1.7.1 + opentelemetry-exporter-otlp-proto-grpc == 1.8.0 + opentelemetry-exporter-otlp-proto-http == 1.8.0 [options.entry_points] opentelemetry_traces_exporter = diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index d19c1f14ba..bea3e76cbe 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index d19c1f14ba..bea3e76cbe 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 514bc2af17..7c71415912 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.7.1 + opentelemetry-exporter-zipkin-json == 1.8.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index d19c1f14ba..bea3e76cbe 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index e3d72f2ec3..a91d6a0f21 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -39,8 +39,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.7.1 - opentelemetry-exporter-zipkin-proto-http == 1.7.1 + opentelemetry-exporter-zipkin-json == 1.8.0 + opentelemetry-exporter-zipkin-proto-http == 1.8.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index d19c1f14ba..bea3e76cbe 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index d19c1f14ba..bea3e76cbe 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index d19c1f14ba..bea3e76cbe 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index e78448dd82..5606788199 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,8 +43,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.7.1 - opentelemetry-semantic-conventions == 0.26b1 + opentelemetry-api == 1.8.0 + opentelemetry-semantic-conventions == 0.27b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index d19c1f14ba..bea3e76cbe 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index a48aab26a5..f0adf9ba13 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.26b1" +__version__ = "0.27b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index d19c1f14ba..bea3e76cbe 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index d19c1f14ba..bea3e76cbe 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.1" +__version__ = "1.8.0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 938408d33f..fc1b791172 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -47,7 +47,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test-utils == 0.26b1 + opentelemetry-test-utils == 0.27b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index a48aab26a5..f0adf9ba13 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.26b1" +__version__ = "0.27b0" diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index daf3f6620f..7b984a70b3 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.26b1" +__version__ = "0.27b0" From 5771b547174386b5c957c7f6be8d9658afa38028 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Sun, 26 Dec 2021 02:37:37 +0530 Subject: [PATCH 1077/1517] Updated contrib SHA (#2342) --- .github/workflows/test.yml | 8 ++++---- opentelemetry-sdk/tests/logs/test_export.py | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d882313d14..50076d83bc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: b8d67dbc2f06e54dbbacd41fec085a3c0c180c71 + CONTRIB_REPO_SHA: 30d0c2ea90dffa7d958a28a699a9021ecb04aa71 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when @@ -54,7 +54,7 @@ jobs: path: | .tox ~/.cache/pip - key: v2-tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core + key: v3-tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core # tox fails on windows and Python3.6 when tox dir is reused between builds so we remove it - name: fix for windows + py3.6 if: ${{ matrix.os == 'windows-2019' && matrix.python-version == 'py36' }} @@ -109,7 +109,7 @@ jobs: path: | .tox ~/.cache/pip - key: v2-tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core + key: v3-tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core - name: run tox run: tox -e ${{ matrix.tox-environment }} @@ -152,6 +152,6 @@ jobs: path: | .tox ~/.cache/pip - key: v2-tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib + key: v3-tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib - name: run tox run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 502c68ed75..3e6eed3851 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -44,6 +44,9 @@ from opentelemetry.trace import TraceFlags from opentelemetry.trace.span import INVALID_SPAN_CONTEXT +is_pypy = hasattr(sys, "pypy_version_info") +supports_register_at_fork = hasattr(os, "fork") and sys.version_info >= (3, 7) + class TestSimpleLogProcessor(unittest.TestCase): def test_simple_log_processor_default_level(self): @@ -272,8 +275,9 @@ def bulk_log_and_flush(num_logs): finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 2415) - @unittest.skipUnless( - hasattr(os, "fork") and sys.version_info >= (3, 7), + # TODO: fix https://github.com/open-telemetry/opentelemetry-python/issues/2346 + @unittest.skipIf( + is_pypy or not supports_register_at_fork, "needs *nix and minor version 7 or later", ) def test_batch_log_processor_fork(self): From d432153b9c615a6d5cf2f3c9d79791881de8856c Mon Sep 17 00:00:00 2001 From: Matt Oberle Date: Sat, 25 Dec 2021 16:52:39 -0500 Subject: [PATCH 1078/1517] Add setuptools runtime requirement (#2334) * Add setuptools runtime requirement The `pkg_resources` module is provided by `setuptools`. The `setuptools` package is not part of the Python stdlib but is often available in the system environment as a build-time requirement. Explicitly listing `setuptools` as a requirement protects `opentelemetry-python` from breaking with import errors in cases where `setuptools` is not available system-wide. This commit pins the version to `setuptools >= 16.0` which is the first release that includes the required imports (2015). * Update CHANGELOG --- CHANGELOG.md | 2 ++ exporter/opentelemetry-exporter-opencensus/setup.cfg | 1 + opentelemetry-api/setup.cfg | 1 + opentelemetry-sdk/setup.cfg | 1 + 4 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85a9346301..f3f389805f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2307](https://github.com/open-telemetry/opentelemetry-python/pull/2307)) - Adding OTEL_LOGS_EXPORTER environment variable ([#2320](https://github.com/open-telemetry/opentelemetry-python/pull/2320)) +- Add `setuptools` to `install_requires` + ([#2334](https://github.com/open-telemetry/opentelemetry-python/pull/2334)) - Add otlp entrypoint for log exporter ([#2322](https://github.com/open-telemetry/opentelemetry-python/pull/2322)) diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index b9dcdbc820..f770c1e74d 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -46,6 +46,7 @@ install_requires = opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 protobuf >= 3.13.0 + setuptools >= 16.0 [options.packages.find] where = src diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 21ae69460b..ba91efcc5a 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -45,6 +45,7 @@ include_package_data = True install_requires = Deprecated >= 1.2.6 aiocontextvars; python_version<'3.7' + setuptools >= 16.0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 5606788199..d1fe563d66 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -45,6 +45,7 @@ include_package_data = True install_requires = opentelemetry-api == 1.8.0 opentelemetry-semantic-conventions == 0.27b0 + setuptools >= 16.0 [options.packages.find] where = src From d3e569c941e01170b13d863e135bdd033399a008 Mon Sep 17 00:00:00 2001 From: Phillip Carter Date: Mon, 3 Jan 2022 10:55:51 -0800 Subject: [PATCH 1079/1517] Add section in FAQ/cookbook for nested spans (#2333) --- docs/faq-and-cookbook.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/faq-and-cookbook.rst b/docs/faq-and-cookbook.rst index 05bb58f324..2aacb3eeb4 100644 --- a/docs/faq-and-cookbook.rst +++ b/docs/faq-and-cookbook.rst @@ -26,6 +26,31 @@ Getting and modifying a span current_span = trace.get_current_span() current_span.set_attribute("hometown", "seattle") +Create a nested span +-------------------- + +.. code-block:: python + + from opentelemetry import trace + import time + + tracer = trace.get_tracer(__name__) + + # Create a new span to track some work + with tracer.start_as_current_span("parent"): + time.sleep(1) + + # Create a nested span to track nested work + with tracer.start_as_current_span("child"): + time.sleep(2) + # the nested span is closed when it's out of scope + + # Now the parent span is the current span again + time.sleep(1) + + # This span is also closed when it goes out of scope + + Capturing baggage at different contexts --------------------------------------- From 438d8371288867f55797e5be97e6c6a024af6e4f Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 4 Jan 2022 00:31:58 +0530 Subject: [PATCH 1080/1517] helpful propagator entry point loading error message (#2344) --- .../src/opentelemetry/propagate/__init__.py | 27 ++++++++++--------- .../tests/propagators/test_propagators.py | 18 ++++++++++++- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index 3ad1ad544a..2963ee6c2e 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -121,26 +121,27 @@ def inject( get_global_textmap().inject(carrier, context=context, setter=setter) -try: +propagators = [] - propagators = [] +# Single use variable here to hack black and make lint pass +environ_propagators = environ.get( + OTEL_PROPAGATORS, + "tracecontext,baggage", +) - # Single use variable here to hack black and make lint pass - environ_propagators = environ.get( - OTEL_PROPAGATORS, - "tracecontext,baggage", - ) - - for propagator in environ_propagators.split(","): +for propagator in environ_propagators.split(","): + propagator = propagator.strip() + try: propagators.append( # type: ignore next( # type: ignore iter_entry_points("opentelemetry_propagator", propagator) ).load()() ) - -except Exception: # pylint: disable=broad-except - logger.exception("Failed to load configured propagators") - raise + except Exception: # pylint: disable=broad-except + logger.exception( + "Failed to load configured propagator `%s`", propagator + ) + raise _HTTP_TEXT_FORMAT = composite.CompositePropagator(propagators) # type: ignore diff --git a/opentelemetry-api/tests/propagators/test_propagators.py b/opentelemetry-api/tests/propagators/test_propagators.py index bcc0109465..74c4231e69 100644 --- a/opentelemetry-api/tests/propagators/test_propagators.py +++ b/opentelemetry-api/tests/propagators/test_propagators.py @@ -15,6 +15,7 @@ # type: ignore from importlib import reload +from logging import ERROR from os import environ from unittest import TestCase from unittest.mock import Mock, patch @@ -47,7 +48,7 @@ def test_propagators(propagators): reload(opentelemetry.propagate) - @patch.dict(environ, {OTEL_PROPAGATORS: "a,b,c"}) + @patch.dict(environ, {OTEL_PROPAGATORS: "a, b, c "}) @patch("opentelemetry.propagators.composite.CompositePropagator") @patch("pkg_resources.iter_entry_points") def test_non_default_propagators( @@ -81,3 +82,18 @@ def test_propagators(propagators): import opentelemetry.propagate reload(opentelemetry.propagate) + + @patch.dict( + environ, {OTEL_PROPAGATORS: "tracecontext , unknown , baggage"} + ) + def test_composite_propagators_error(self): + + import opentelemetry.propagate + + with self.assertRaises(Exception): + with self.assertLogs(level=ERROR) as err: + reload(opentelemetry.propagate) + self.assertIn( + "Failed to load configured propagator `unknown`", + err.output[0], + ) From d8bebdd5f7c3f08dd565a71be5d84c5ab500c32b Mon Sep 17 00:00:00 2001 From: Matt Oberle Date: Tue, 4 Jan 2022 13:47:26 -0500 Subject: [PATCH 1081/1517] Decode URL-encoded headers in environment vars (#2312) --- CHANGELOG.md | 3 +++ .../opentelemetry/exporter/otlp/proto/grpc/exporter.py | 8 ++++++-- .../tests/test_otlp_trace_exporter.py | 7 +++++++ opentelemetry-api/src/opentelemetry/util/re.py | 5 +++-- opentelemetry-api/tests/util/test_re.py | 6 ++++++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3f389805f..c009a1836d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.8.0-0.27b0...HEAD) +- Decode URL-encoded headers in environment variables + ([#2312](https://github.com/open-telemetry/opentelemetry-python/pull/2312)) + ## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17 - Adds Aggregation and instruments as part of Metrics SDK diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 691c7c2dc1..248b970b6f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -19,7 +19,7 @@ from collections.abc import Sequence from os import environ from time import sleep -from typing import Any, Callable, Dict, Generic, List, Optional +from typing import Any, Callable, Dict, Generic, List, Optional, Tuple, Union from typing import Sequence as TypingSequence from typing import TypeVar from urllib.parse import urlparse @@ -204,7 +204,9 @@ def __init__( endpoint: Optional[str] = None, insecure: Optional[bool] = None, credentials: Optional[ChannelCredentials] = None, - headers: Optional[Sequence] = None, + headers: Optional[ + Union[TypingSequence[Tuple[str, str]], Dict[str, str], str] + ] = None, timeout: Optional[int] = None, compression: Optional[Compression] = None, ): @@ -229,6 +231,8 @@ def __init__( if isinstance(self._headers, str): temp_headers = parse_headers(self._headers) self._headers = tuple(temp_headers.items()) + elif isinstance(self._headers, dict): + self._headers = tuple(self._headers.items()) self._timeout = timeout or int( environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 10) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 198ff44271..fa3c24e0f9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -281,6 +281,13 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): self.assertEqual( exporter._headers, (("key3", "value3"), ("key4", "value4")) ) + exporter = OTLPSpanExporter( + headers={"key5": "value5", "key6": "value6"} + ) + # pylint: disable=protected-access + self.assertEqual( + exporter._headers, (("key5", "value5"), ("key6", "value6")) + ) # pylint: disable=no-self-use @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") diff --git a/opentelemetry-api/src/opentelemetry/util/re.py b/opentelemetry-api/src/opentelemetry/util/re.py index 32c3e3ffb4..c107ee9150 100644 --- a/opentelemetry-api/src/opentelemetry/util/re.py +++ b/opentelemetry-api/src/opentelemetry/util/re.py @@ -15,6 +15,7 @@ import logging from re import compile, split from typing import Mapping +from urllib.parse import unquote _logger = logging.getLogger(__name__) @@ -51,8 +52,8 @@ def parse_headers(s: str) -> Mapping[str, str]: continue # value may contain any number of `=` name, value = match.string.split("=", 1) - name = name.strip().lower() - value = value.strip() + name = unquote(name).strip().lower() + value = unquote(value).strip() headers[name] = value return headers diff --git a/opentelemetry-api/tests/util/test_re.py b/opentelemetry-api/tests/util/test_re.py index 9c726a7e57..e7834ac15a 100644 --- a/opentelemetry-api/tests/util/test_re.py +++ b/opentelemetry-api/tests/util/test_re.py @@ -34,6 +34,12 @@ def test_parse_headers(self): # different header values ("name=", [("name", "")], False), ("name===value=", [("name", "==value=")], False), + # url-encoded headers + ("key=value%20with%20space", [("key", "value with space")], False), + ("key%21=value", [("key!", "value")], False), + ("%20key%20=%20value%20", [("key", "value")], False), + # header name case normalization + ("Key=Value", [("key", "Value")], False), # mix of valid and invalid headers ( "name1=value1,invalidName, name2 = value2 , name3=value3==", From f5d872050204685b5ef831d02ec593956820ebe6 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 7 Jan 2022 15:52:52 -0800 Subject: [PATCH 1082/1517] [otlp-exporter] refactor _translate_attributes (#2355) * [otlp-exporter] refactor _translate_attributes The method to translate attributes is common across signals, rafactoring the code to make it so. * fix lint --- .../otlp/proto/grpc/_log_exporter/__init__.py | 36 ++++------- .../exporter/otlp/proto/grpc/exporter.py | 16 ++++- .../proto/grpc/trace_exporter/__init__.py | 62 +++++++------------ 3 files changed, 47 insertions(+), 67 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index 211655d93a..db4ce72d26 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -15,7 +15,6 @@ from grpc import ChannelCredentials, Compression from opentelemetry.exporter.otlp.proto.grpc.exporter import ( OTLPExporterMixin, - _translate_key_values, get_resource_data, _translate_value, ) @@ -65,49 +64,36 @@ def __init__( ) def _translate_name(self, log_data: LogData) -> None: - self._collector_log_kwargs["name"] = log_data.log_record.name + self._collector_kwargs["name"] = log_data.log_record.name def _translate_time(self, log_data: LogData) -> None: - self._collector_log_kwargs[ + self._collector_kwargs[ "time_unix_nano" ] = log_data.log_record.timestamp def _translate_span_id(self, log_data: LogData) -> None: - self._collector_log_kwargs[ + self._collector_kwargs[ "span_id" ] = log_data.log_record.span_id.to_bytes(8, "big") def _translate_trace_id(self, log_data: LogData) -> None: - self._collector_log_kwargs[ + self._collector_kwargs[ "trace_id" ] = log_data.log_record.trace_id.to_bytes(16, "big") def _translate_trace_flags(self, log_data: LogData) -> None: - self._collector_log_kwargs["flags"] = int( - log_data.log_record.trace_flags - ) + self._collector_kwargs["flags"] = int(log_data.log_record.trace_flags) def _translate_body(self, log_data: LogData): - self._collector_log_kwargs["body"] = _translate_value( + self._collector_kwargs["body"] = _translate_value( log_data.log_record.body ) def _translate_severity_text(self, log_data: LogData): - self._collector_log_kwargs[ + self._collector_kwargs[ "severity_text" ] = log_data.log_record.severity_text - def _translate_attributes(self, log_data: LogData) -> None: - if log_data.log_record.attributes: - self._collector_log_kwargs["attributes"] = [] - for key, value in log_data.log_record.attributes.items(): - try: - self._collector_log_kwargs["attributes"].append( - _translate_key_values(key, value) - ) - except Exception: # pylint: disable=broad-except - pass - def _translate_data( self, data: Sequence[LogData] ) -> ExportLogsServiceRequest: @@ -152,7 +138,7 @@ def _translate_data( ) ) - self._collector_log_kwargs = {} + self._collector_kwargs = {} self._translate_name(log_data) self._translate_time(log_data) @@ -161,14 +147,14 @@ def _translate_data( self._translate_trace_flags(log_data) self._translate_body(log_data) self._translate_severity_text(log_data) - self._translate_attributes(log_data) + self._translate_attributes(log_data.log_record.attributes) - self._collector_log_kwargs[ + self._collector_kwargs[ "severity_number" ] = log_data.log_record.severity_number.value instrumentation_library_logs.logs.append( - PB2LogRecord(**self._collector_log_kwargs) + PB2LogRecord(**self._collector_kwargs) ) return ExportLogsServiceRequest( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 248b970b6f..ec30cdac1c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -237,7 +237,7 @@ def __init__( self._timeout = timeout or int( environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 10) ) - self._collector_span_kwargs = None + self._collector_kwargs = None compression = ( environ_to_compression(OTEL_EXPORTER_OTLP_COMPRESSION) @@ -263,6 +263,20 @@ def _translate_data( ) -> ExportServiceRequestT: pass + def _translate_attributes(self, attributes) -> None: + if attributes: + + self._collector_kwargs["attributes"] = [] + + for key, value in attributes.items(): + + try: + self._collector_kwargs["attributes"].append( + _translate_key_values(key, value) + ) + except Exception as error: # pylint: disable=broad-except + logger.exception(error) + def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: max_value = 64 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 5f18de1ac0..075b961250 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -117,58 +117,42 @@ def __init__( ) def _translate_name(self, sdk_span: ReadableSpan) -> None: - self._collector_span_kwargs["name"] = sdk_span.name + self._collector_kwargs["name"] = sdk_span.name def _translate_start_time(self, sdk_span: ReadableSpan) -> None: - self._collector_span_kwargs[ - "start_time_unix_nano" - ] = sdk_span.start_time + self._collector_kwargs["start_time_unix_nano"] = sdk_span.start_time def _translate_end_time(self, sdk_span: ReadableSpan) -> None: - self._collector_span_kwargs["end_time_unix_nano"] = sdk_span.end_time + self._collector_kwargs["end_time_unix_nano"] = sdk_span.end_time def _translate_span_id(self, sdk_span: ReadableSpan) -> None: - self._collector_span_kwargs[ - "span_id" - ] = sdk_span.context.span_id.to_bytes(8, "big") + self._collector_kwargs["span_id"] = sdk_span.context.span_id.to_bytes( + 8, "big" + ) def _translate_trace_id(self, sdk_span: ReadableSpan) -> None: - self._collector_span_kwargs[ + self._collector_kwargs[ "trace_id" ] = sdk_span.context.trace_id.to_bytes(16, "big") def _translate_parent(self, sdk_span: ReadableSpan) -> None: if sdk_span.parent is not None: - self._collector_span_kwargs[ + self._collector_kwargs[ "parent_span_id" ] = sdk_span.parent.span_id.to_bytes(8, "big") def _translate_context_trace_state(self, sdk_span: ReadableSpan) -> None: if sdk_span.context.trace_state is not None: - self._collector_span_kwargs["trace_state"] = ",".join( + self._collector_kwargs["trace_state"] = ",".join( [ f"{key}={value}" for key, value in (sdk_span.context.trace_state.items()) ] ) - def _translate_attributes(self, sdk_span: ReadableSpan) -> None: - if sdk_span.attributes: - - self._collector_span_kwargs["attributes"] = [] - - for key, value in sdk_span.attributes.items(): - - try: - self._collector_span_kwargs["attributes"].append( - _translate_key_values(key, value) - ) - except Exception as error: # pylint: disable=broad-except - logger.exception(error) - def _translate_events(self, sdk_span: ReadableSpan) -> None: if sdk_span.events: - self._collector_span_kwargs["events"] = [] + self._collector_kwargs["events"] = [] for sdk_span_event in sdk_span.events: @@ -187,13 +171,11 @@ def _translate_events(self, sdk_span: ReadableSpan) -> None: except Exception as error: logger.exception(error) - self._collector_span_kwargs["events"].append( - collector_span_event - ) + self._collector_kwargs["events"].append(collector_span_event) def _translate_links(self, sdk_span: ReadableSpan) -> None: if sdk_span.links: - self._collector_span_kwargs["links"] = [] + self._collector_kwargs["links"] = [] for sdk_span_link in sdk_span.links: @@ -214,9 +196,7 @@ def _translate_links(self, sdk_span: ReadableSpan) -> None: except Exception as error: logger.exception(error) - self._collector_span_kwargs["links"].append( - collector_span_link - ) + self._collector_kwargs["links"].append(collector_span_link) def _translate_status(self, sdk_span: ReadableSpan) -> None: # pylint: disable=no-member @@ -224,7 +204,7 @@ def _translate_status(self, sdk_span: ReadableSpan) -> None: deprecated_code = Status.DEPRECATED_STATUS_CODE_OK if sdk_span.status.status_code == StatusCode.ERROR: deprecated_code = Status.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR - self._collector_span_kwargs["status"] = Status( + self._collector_kwargs["status"] = Status( deprecated_code=deprecated_code, code=sdk_span.status.status_code.value, message=sdk_span.status.description, @@ -274,7 +254,7 @@ def _translate_data( sdk_span.instrumentation_info ) ) - self._collector_span_kwargs = {} + self._collector_kwargs = {} self._translate_name(sdk_span) self._translate_start_time(sdk_span) @@ -283,30 +263,30 @@ def _translate_data( self._translate_trace_id(sdk_span) self._translate_parent(sdk_span) self._translate_context_trace_state(sdk_span) - self._translate_attributes(sdk_span) + self._translate_attributes(sdk_span.attributes) self._translate_events(sdk_span) self._translate_links(sdk_span) self._translate_status(sdk_span) if sdk_span.dropped_attributes: - self._collector_span_kwargs[ + self._collector_kwargs[ "dropped_attributes_count" ] = sdk_span.dropped_attributes if sdk_span.dropped_events: - self._collector_span_kwargs[ + self._collector_kwargs[ "dropped_events_count" ] = sdk_span.dropped_events if sdk_span.dropped_links: - self._collector_span_kwargs[ + self._collector_kwargs[ "dropped_links_count" ] = sdk_span.dropped_links - self._collector_span_kwargs["kind"] = getattr( + self._collector_kwargs["kind"] = getattr( CollectorSpan.SpanKind, f"SPAN_KIND_{sdk_span.kind.name}", ) instrumentation_library_spans.spans.append( - CollectorSpan(**self._collector_span_kwargs) + CollectorSpan(**self._collector_kwargs) ) return ExportTraceServiceRequest( From a50e87914796b12bd185e454903319497ae41e90 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 10 Jan 2022 10:01:47 -0800 Subject: [PATCH 1083/1517] `exporter-otlp`: adding metrics exporter structure (#2323) --- CHANGELOG.md | 2 + .../getting_started/metrics_example.py | 5 +- .../proto/grpc/_metric_exporter/__init__.py | 125 ++++++++++ .../tests/logs/test_otlp_logs_exporter.py | 2 +- .../tests/metrics/__init__.py | 0 .../metrics/test_otlp_metrics_exporter.py | 219 ++++++++++++++++++ .../tests/test_otlp_trace_exporter.py | 2 +- .../tests/test_otlp.py | 10 +- opentelemetry-sdk/setup.cfg | 2 + .../opentelemetry/sdk/_metrics/__init__.py | 2 +- .../src/opentelemetry/sdk/_metrics/data.py | 35 +++ .../sdk/_metrics/export/__init__.py | 65 ++++++ opentelemetry-sdk/tests/test_configurator.py | 5 + 13 files changed, 467 insertions(+), 7 deletions(-) rename opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/metric_exporter.py => docs/getting_started/metrics_example.py (94%) create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/data.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c009a1836d..91b5be8309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Decode URL-encoded headers in environment variables ([#2312](https://github.com/open-telemetry/opentelemetry-python/pull/2312)) +- [exporter/opentelemetry-exporter-otlp-proto-grpc] Add OTLPMetricExporter + ([#2323](https://github.com/open-telemetry/opentelemetry-python/pull/2323)) ## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/metric_exporter.py b/docs/getting_started/metrics_example.py similarity index 94% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/metric_exporter.py rename to docs/getting_started/metrics_example.py index e9c6d6aae0..d0bb758306 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/metric_exporter.py +++ b/docs/getting_started/metrics_example.py @@ -12,6 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. - -class MetricExporter: - pass +# metrics.py +# TODO diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py new file mode 100644 index 0000000000..5e76e5c035 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py @@ -0,0 +1,125 @@ +# Copyright The OpenTelemetry Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional, Sequence +from grpc import ChannelCredentials, Compression +from opentelemetry.exporter.otlp.proto.grpc.exporter import ( + OTLPExporterMixin, + get_resource_data, +) +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( + MetricsServiceStub, +) +from opentelemetry.proto.common.v1.common_pb2 import InstrumentationLibrary +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + InstrumentationLibraryMetrics, + ResourceMetrics, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric as PB2Metric +from opentelemetry.sdk._metrics.data import ( + MetricData, +) + +from opentelemetry.sdk._metrics.export import ( + MetricExporter, + MetricExportResult, +) + + +class OTLPMetricExporter( + MetricExporter, + OTLPExporterMixin[ + MetricData, ExportMetricsServiceRequest, MetricExportResult + ], +): + _result = MetricExportResult + _stub = MetricsServiceStub + + def __init__( + self, + endpoint: Optional[str] = None, + insecure: Optional[bool] = None, + credentials: Optional[ChannelCredentials] = None, + headers: Optional[Sequence] = None, + timeout: Optional[int] = None, + compression: Optional[Compression] = None, + ): + super().__init__( + **{ + "endpoint": endpoint, + "insecure": insecure, + "credentials": credentials, + "headers": headers, + "timeout": timeout, + "compression": compression, + } + ) + + def _translate_data( + self, data: Sequence[MetricData] + ) -> ExportMetricsServiceRequest: + sdk_resource_instrumentation_library_metrics = {} + self._collector_metric_kwargs = {} + + for metric_data in data: + resource = metric_data.metric.resource + instrumentation_library_map = ( + sdk_resource_instrumentation_library_metrics.get(resource, {}) + ) + if not instrumentation_library_map: + sdk_resource_instrumentation_library_metrics[ + resource + ] = instrumentation_library_map + + instrumentation_library_metrics = instrumentation_library_map.get( + metric_data.instrumentation_info + ) + + if not instrumentation_library_metrics: + if metric_data.instrumentation_info is not None: + instrumentation_library_map[ + metric_data.instrumentation_info + ] = InstrumentationLibraryMetrics( + instrumentation_library=InstrumentationLibrary( + name=metric_data.instrumentation_info.name, + version=metric_data.instrumentation_info.version, + ) + ) + else: + instrumentation_library_map[ + metric_data.instrumentation_info + ] = InstrumentationLibraryMetrics() + + instrumentation_library_metrics = instrumentation_library_map.get( + metric_data.instrumentation_info + ) + + instrumentation_library_metrics.metrics.append( + PB2Metric(**self._collector_metric_kwargs) + ) + return ExportMetricsServiceRequest( + resource_metrics=get_resource_data( + sdk_resource_instrumentation_library_metrics, + ResourceMetrics, + "metrics", + ) + ) + + def export(self, metrics: Sequence[MetricData]) -> MetricExportResult: + return self._export(metrics) + + def shutdown(self): + pass diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index b9c33786e3..01fe424835 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -107,7 +107,7 @@ def setUp(self): self.server = server(ThreadPoolExecutor(max_workers=10)) - self.server.add_insecure_port("[::]:4317") + self.server.add_insecure_port("127.0.0.1:4317") self.server.start() diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py new file mode 100644 index 0000000000..f7cf8ec94d --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -0,0 +1,219 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from concurrent.futures import ThreadPoolExecutor +from unittest import TestCase +from unittest.mock import patch + +from google.protobuf.duration_pb2 import Duration +from google.rpc.error_details_pb2 import RetryInfo +from grpc import StatusCode, server + +from opentelemetry.exporter.otlp.proto.grpc._metric_exporter import ( + OTLPMetricExporter, +) +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceResponse, +) +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( + MetricsServiceServicer, + add_MetricsServiceServicer_to_server, +) +from opentelemetry.sdk._metrics.data import Metric, MetricData +from opentelemetry.sdk._metrics.export import MetricExportResult +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo + + +class MetricsServiceServicerUNAVAILABLEDelay(MetricsServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.UNAVAILABLE) + + context.send_initial_metadata( + (("google.rpc.retryinfo-bin", RetryInfo().SerializeToString()),) + ) + context.set_trailing_metadata( + ( + ( + "google.rpc.retryinfo-bin", + RetryInfo( + retry_delay=Duration(seconds=4) + ).SerializeToString(), + ), + ) + ) + + return ExportMetricsServiceResponse() + + +class MetricsServiceServicerUNAVAILABLE(MetricsServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.UNAVAILABLE) + + return ExportMetricsServiceResponse() + + +class MetricsServiceServicerSUCCESS(MetricsServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.OK) + + return ExportMetricsServiceResponse() + + +class MetricsServiceServicerALREADY_EXISTS(MetricsServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.ALREADY_EXISTS) + + return ExportMetricsServiceResponse() + + +class TestOTLPMetricExporter(TestCase): + def setUp(self): + + self.exporter = OTLPMetricExporter() + + self.server = server(ThreadPoolExecutor(max_workers=10)) + + self.server.add_insecure_port("127.0.0.1:4317") + + self.server.start() + + self.metric_data_1 = MetricData( + metric=Metric( + resource=SDKResource({"key": "value"}), + ), + instrumentation_info=InstrumentationInfo( + "first_name", "first_version" + ), + ) + + def tearDown(self): + self.server.stop(None) + + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" + ) + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") + @patch( + "opentelemetry.exporter.otlp.proto.grpc._metric_exporter.OTLPMetricExporter._stub" + ) + # pylint: disable=unused-argument + def test_no_credentials_error( + self, mock_ssl_channel, mock_secure, mock_stub + ): + OTLPMetricExporter(insecure=False) + self.assertTrue(mock_ssl_channel.called) + + # pylint: disable=no-self-use + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") + def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): + expected_endpoint = "localhost:4317" + endpoints = [ + ( + "http://localhost:4317", + None, + mock_insecure, + ), + ( + "localhost:4317", + None, + mock_insecure, + ), + ( + "localhost:4317", + False, + mock_secure, + ), + ( + "https://localhost:4317", + None, + mock_secure, + ), + ( + "https://localhost:4317", + True, + mock_insecure, + ), + ] + # pylint: disable=C0209 + for endpoint, insecure, mock_method in endpoints: + OTLPMetricExporter(endpoint=endpoint, insecure=insecure) + self.assertEqual( + 1, + mock_method.call_count, + "expected {} to be called for {} {}".format( + mock_method, endpoint, insecure + ), + ) + self.assertEqual( + expected_endpoint, + mock_method.call_args[0][0], + "expected {} got {} {}".format( + expected_endpoint, mock_method.call_args[0][0], endpoint + ), + ) + mock_method.reset_mock() + + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") + def test_unavailable(self, mock_sleep, mock_expo): + + mock_expo.configure_mock(**{"return_value": [1]}) + + add_MetricsServiceServicer_to_server( + MetricsServiceServicerUNAVAILABLE(), self.server + ) + self.assertEqual( + self.exporter.export([self.metric_data_1]), + MetricExportResult.FAILURE, + ) + mock_sleep.assert_called_with(1) + + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") + def test_unavailable_delay(self, mock_sleep, mock_expo): + + mock_expo.configure_mock(**{"return_value": [1]}) + + add_MetricsServiceServicer_to_server( + MetricsServiceServicerUNAVAILABLEDelay(), self.server + ) + self.assertEqual( + self.exporter.export([self.metric_data_1]), + MetricExportResult.FAILURE, + ) + mock_sleep.assert_called_with(4) + + def test_success(self): + add_MetricsServiceServicer_to_server( + MetricsServiceServicerSUCCESS(), self.server + ) + self.assertEqual( + self.exporter.export([self.metric_data_1]), + MetricExportResult.SUCCESS, + ) + + def test_failure(self): + add_MetricsServiceServicer_to_server( + MetricsServiceServicerALREADY_EXISTS(), self.server + ) + self.assertEqual( + self.exporter.export([self.metric_data_1]), + MetricExportResult.FAILURE, + ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index fa3c24e0f9..252d2f7b93 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -131,7 +131,7 @@ def setUp(self): self.server = server(ThreadPoolExecutor(max_workers=10)) - self.server.add_insecure_port("[::]:4317") + self.server.add_insecure_port("127.0.0.1:4317") self.server.start() diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py index 5b1a7d7fde..57b3d7b1b1 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py @@ -17,6 +17,9 @@ from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter, ) +from opentelemetry.exporter.otlp.proto.grpc._metric_exporter import ( + OTLPMetricExporter, +) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) @@ -27,7 +30,12 @@ class TestOTLPExporters(unittest.TestCase): def test_constructors(self): - for exporter in [OTLPSpanExporter, HTTPSpanExporter, OTLPLogExporter]: + for exporter in [ + OTLPSpanExporter, + HTTPSpanExporter, + OTLPLogExporter, + OTLPMetricExporter, + ]: try: exporter() except Exception: # pylint: disable=broad-except diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index d1fe563d66..6b4281cc2e 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -59,6 +59,8 @@ opentelemetry_log_emitter_provider = sdk_log_emitter_provider = opentelemetry.sdk._logs:LogEmitterProvider opentelemetry_logs_exporter = console = opentelemetry.sdk._logs.export:ConsoleLogExporter +opentelemetry_metrics_exporter = + console = opentelemetry.sdk._metrics.export:ConsoleMetricExporter opentelemetry_id_generator = random = opentelemetry.sdk.trace.id_generator:RandomIdGenerator opentelemetry_environment_variables = diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index bf04d8fe1f..8a87c22ff9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -32,7 +32,7 @@ ObservableUpDownCounter as APIObservableUpDownCounter, ) from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter -from opentelemetry.sdk._metrics.export.metric_exporter import MetricExporter +from opentelemetry.sdk._metrics.export import MetricExporter from opentelemetry.sdk._metrics.instrument import ( Counter, Histogram, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/data.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/data.py new file mode 100644 index 0000000000..e582d66684 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/data.py @@ -0,0 +1,35 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo + + +class Metric: + """TODO fill this in""" + + def __init__(self, resource: Resource) -> None: + self.resource = resource + + +class MetricData: + """Readable Metric data plus associated InstrumentationLibrary.""" + + def __init__( + self, + metric: Metric, + instrumentation_info: InstrumentationInfo, + ): + self.metric = metric + self.instrumentation_info = instrumentation_info diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py index b0a6f42841..e9a83861fe 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -11,3 +11,68 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +from abc import ABC, abstractmethod +from enum import Enum +from os import linesep +from sys import stdout +from typing import IO, Callable, Sequence + +from opentelemetry.sdk._metrics.data import Metric, MetricData + + +class MetricExportResult(Enum): + SUCCESS = 0 + FAILURE = 1 + + +class MetricExporter(ABC): + """Interface for exporting metrics. + + Interface to be implemented by services that want to export metrics received + in their own format. + """ + + def export(self, metrics: Sequence[MetricData]) -> "MetricExportResult": + """Exports a batch of telemetry data. + + Args: + metrics: The list of `opentelemetry.sdk._metrics.data.MetricData` objects to be exported + + Returns: + The result of the export + """ + + @abstractmethod + def shutdown(self) -> None: + """Shuts down the exporter. + + Called when the SDK is shut down. + """ + + +class ConsoleMetricExporter(MetricExporter): + """Implementation of :class:`MetricExporter` that prints metrics to the + console. + + This class can be used for diagnostic purposes. It prints the exported + metrics to the console STDOUT. + """ + + def __init__( + self, + out: IO = stdout, + formatter: Callable[[Metric], str] = lambda metric: metric.to_json() + + linesep, + ): + self.out = out + self.formatter = formatter + + def export(self, metrics: Sequence[MetricData]) -> MetricExportResult: + for data in metrics: + self.out.write(self.formatter(data.metric)) + self.out.flush() + return MetricExportResult.SUCCESS + + def shutdown(self) -> None: + pass diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index ca755544b7..32c939df14 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -29,6 +29,7 @@ _init_tracing, ) from opentelemetry.sdk._logs.export import ConsoleLogExporter +from opentelemetry.sdk._metrics.export import ConsoleMetricExporter from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator @@ -195,3 +196,7 @@ def test_console_exporters(self): self.assertEqual( logs_exporters["console"].__class__, ConsoleLogExporter.__class__ ) + self.assertEqual( + logs_exporters["console"].__class__, + ConsoleMetricExporter.__class__, + ) From 523379342b43ef3e0bb41611e982ce10f9dd6fc0 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 11 Jan 2022 11:05:16 -0800 Subject: [PATCH 1084/1517] [api/sdk] add entrypoint for meter provider (#2354) * [api/sdk] add entrypoint for meter provider Adding the opentelemtry_meter_provider entrypoints default_meter_provider & sdk_meter_provider. * update sha Co-authored-by: Diego Hurtado --- .github/workflows/test.yml | 2 +- opentelemetry-api/setup.cfg | 2 ++ opentelemetry-sdk/setup.cfg | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 50076d83bc..7c8b1b1d0a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 30d0c2ea90dffa7d958a28a699a9021ecb04aa71 + CONTRIB_REPO_SHA: 23394ccd80878a91534f8421b82a7410eb775e65 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index ba91efcc5a..3c10d64481 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -55,6 +55,8 @@ opentelemetry_context = contextvars_context = opentelemetry.context.contextvars_context:ContextVarsRuntimeContext opentelemetry_tracer_provider = default_tracer_provider = opentelemetry.trace:_DefaultTracerProvider +opentelemetry_meter_provider = + default_meter_provider = opentelemetry._metrics:_DefaultMeterProvider opentelemetry_propagator = tracecontext = opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator baggage = opentelemetry.baggage.propagation:W3CBaggagePropagator diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 6b4281cc2e..8d5d180905 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -55,6 +55,8 @@ opentelemetry_tracer_provider = sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider opentelemetry_traces_exporter = console = opentelemetry.sdk.trace.export:ConsoleSpanExporter +opentelemetry_meter_provider = + sdk_meter_provider = opentelemetry.sdk._metrics:MeterProvider opentelemetry_log_emitter_provider = sdk_log_emitter_provider = opentelemetry.sdk._logs:LogEmitterProvider opentelemetry_logs_exporter = From 6b655ce2d3826daef8f0c80c3fc0863d145f77e7 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 11 Jan 2022 13:23:04 -0600 Subject: [PATCH 1085/1517] Refactor aggregations (#2308) --- opentelemetry-sdk/setup.cfg | 1 + .../opentelemetry/sdk/_metrics/aggregation.py | 206 ++++++++----- .../opentelemetry/sdk/_metrics/measurement.py | 9 +- .../src/opentelemetry/sdk/_metrics/point.py | 43 +++ .../tests/metrics/test_aggregation.py | 278 ++++++++++++++---- 5 files changed, 415 insertions(+), 122 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 8d5d180905..09f679e4c4 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -46,6 +46,7 @@ install_requires = opentelemetry-api == 1.8.0 opentelemetry-semantic-conventions == 0.27b0 setuptools >= 16.0 + dataclasses == 0.8; python_version < '3.7' [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 456e447162..03a2debfe0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -14,124 +14,194 @@ from abc import ABC, abstractmethod from collections import OrderedDict +from enum import IntEnum from logging import getLogger from math import inf +from threading import Lock +from typing import Generic, Optional, Sequence, TypeVar -from opentelemetry._metrics.instrument import _Monotonic +from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk._metrics.point import Gauge, Histogram, PointT, Sum from opentelemetry.util._time import _time_ns + +class AggregationTemporality(IntEnum): + UNSPECIFIED = 0 + DELTA = 1 + CUMULATIVE = 2 + + +_PointVarT = TypeVar("_PointVarT", bound=PointT) + _logger = getLogger(__name__) -class Aggregation(ABC): - @property - def value(self): - return self._value # pylint: disable=no-member +class _InstrumentMonotonicityAwareAggregation: + def __init__(self, instrument_is_monotonic: bool): + self._instrument_is_monotonic = instrument_is_monotonic + super().__init__() + + +class Aggregation(ABC, Generic[_PointVarT]): + def __init__(self): + self._lock = Lock() @abstractmethod - def aggregate(self, value): + def aggregate(self, measurement: Measurement) -> None: pass @abstractmethod - def make_point_and_reset(self): + def collect(self) -> Optional[_PointVarT]: + pass + + +class SynchronousSumAggregation( + _InstrumentMonotonicityAwareAggregation, Aggregation[Sum] +): + def __init__(self, instrument_is_monotonic: bool): + super().__init__(instrument_is_monotonic) + self._value = 0 + self._start_time_unix_nano = _time_ns() + + def aggregate(self, measurement: Measurement) -> None: + with self._lock: + self._value = self._value + measurement.value + + def collect(self) -> Optional[Sum]: """ - Atomically return a point for the current value of the metric and reset the internal state. + Atomically return a point for the current value of the metric and + reset the aggregation value. """ + now = _time_ns() + with self._lock: + value = self._value + start_time_unix_nano = self._start_time_unix_nano -class SumAggregation(Aggregation): - """ - This aggregation collects data for the SDK sum metric point. - """ + self._value = 0 + self._start_time_unix_nano = now + 1 - def __init__(self, instrument): - self._value = 0 + return Sum( + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=self._instrument_is_monotonic, + start_time_unix_nano=start_time_unix_nano, + time_unix_nano=now, + value=value, + ) - def aggregate(self, value): - self._value = self._value + value - def make_point_and_reset(self): - pass +class AsynchronousSumAggregation( + _InstrumentMonotonicityAwareAggregation, Aggregation[Sum] +): + def __init__(self, instrument_is_monotonic: bool): + super().__init__(instrument_is_monotonic) + self._value = None + self._start_time_unix_nano = _time_ns() + def aggregate(self, measurement: Measurement) -> None: + with self._lock: + self._value = measurement.value -class LastValueAggregation(Aggregation): + def collect(self) -> Optional[Sum]: + """ + Atomically return a point for the current value of the metric. + """ + if self._value is None: + return None + + return Sum( + start_time_unix_nano=self._start_time_unix_nano, + time_unix_nano=_time_ns(), + value=self._value, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=self._instrument_is_monotonic, + ) - """ - This aggregation collects data for the SDK sum metric point. - """ - def __init__(self, instrument): +class LastValueAggregation(Aggregation[Gauge]): + def __init__(self): + super().__init__() self._value = None - self._timestamp = _time_ns() - def aggregate(self, value): - self._value = value - self._timestamp = _time_ns() - - def make_point_and_reset(self): - pass + def aggregate(self, measurement: Measurement): + with self._lock: + self._value = measurement.value + def collect(self) -> Optional[Gauge]: + """ + Atomically return a point for the current value of the metric. + """ + if self._value is None: + return None -class ExplicitBucketHistogramAggregation(Aggregation): + return Gauge( + time_unix_nano=_time_ns(), + value=self._value, + ) - """ - This aggregation collects data for the SDK sum metric point. - """ +class ExplicitBucketHistogramAggregation(Aggregation[Histogram]): def __init__( self, - instrument, - *args, - boundaries=(0, 5, 10, 25, 50, 75, 100, 250, 500, 1000), - record_min_max=True, + boundaries: Sequence[int] = ( + 0, + 5, + 10, + 25, + 50, + 75, + 100, + 250, + 500, + 1000, + ), + record_min_max: bool = True, ): super().__init__() self._value = OrderedDict([(key, 0) for key in (*boundaries, inf)]) self._min = inf self._max = -inf self._sum = 0 - self._instrument = instrument self._record_min_max = record_min_max + self._start_time_unix_nano = _time_ns() + self._boundaries = boundaries - @property - def min(self): - if not self._record_min_max: - _logger.warning("Min is not being recorded") - - return self._min + def aggregate(self, measurement: Measurement) -> None: - @property - def max(self): - if not self._record_min_max: - _logger.warning("Max is not being recorded") + value = measurement.value - return self._max - - @property - def sum(self): - if isinstance(self._instrument, _Monotonic): - return self._sum - - _logger.warning( - "Sum is not filled out when the associated " - "instrument is not monotonic" - ) - return None - - def aggregate(self, value): if self._record_min_max: self._min = min(self._min, value) self._max = max(self._max, value) - if isinstance(self._instrument, _Monotonic): - self._sum += value + self._sum += value for key in self._value.keys(): if value < key: - self._value[key] = self._value[key] + value + self._value[key] = self._value[key] + 1 break - def make_point_and_reset(self): - pass + def collect(self) -> Optional[Histogram]: + """ + Atomically return a point for the current value of the metric. + """ + now = _time_ns() + + with self._lock: + value = self._value + start_time_unix_nano = self._start_time_unix_nano + + self._value = OrderedDict( + [(key, 0) for key in (*self._boundaries, inf)] + ) + self._start_time_unix_nano = now + 1 + + return Histogram( + start_time_unix_nano=start_time_unix_nano, + time_unix_nano=now, + bucket_counts=tuple(value.values()), + explicit_bounds=self._boundaries, + aggregation_temporality=AggregationTemporality.DELTA, + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py index fbaae02c78..18110b0743 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py @@ -12,6 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +from dataclasses import dataclass +from typing import Union +from opentelemetry.util.types import Attributes + + +@dataclass(frozen=True) class Measurement: - pass + value: Union[int, float] + attributes: Attributes = None diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py new file mode 100644 index 0000000000..2503bb43e3 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py @@ -0,0 +1,43 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from typing import Sequence, Union + + +@dataclass(frozen=True) +class Sum: + start_time_unix_nano: int + time_unix_nano: int + value: Union[int, float] + aggregation_temporality: int + is_monotonic: bool + + +@dataclass(frozen=True) +class Gauge: + time_unix_nano: int + value: Union[int, float] + + +@dataclass(frozen=True) +class Histogram: + start_time_unix_nano: int + time_unix_nano: int + bucket_counts: Sequence[int] + explicit_bounds: Sequence[float] + aggregation_temporality: int + + +PointT = Union[Sum, Gauge, Histogram] diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 1c4fa1420e..c8064b9d1d 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -13,82 +13,227 @@ # limitations under the License. -from logging import WARNING from math import inf +from time import sleep from unittest import TestCase -from unittest.mock import Mock from opentelemetry.sdk._metrics.aggregation import ( + AsynchronousSumAggregation, ExplicitBucketHistogramAggregation, LastValueAggregation, - SumAggregation, + SynchronousSumAggregation, + _InstrumentMonotonicityAwareAggregation, ) +from opentelemetry.sdk._metrics.measurement import Measurement -class TestSumAggregation(TestCase): +class TestSynchronousSumAggregation(TestCase): + def test_instrument_monotonicity_awareness(self): + """ + `SynchronousSumAggregation` is aware of the instrument monotonicity + """ + + synchronous_sum_aggregation = SynchronousSumAggregation(True) + self.assertIsInstance( + synchronous_sum_aggregation, + _InstrumentMonotonicityAwareAggregation, + ) + self.assertTrue(synchronous_sum_aggregation._instrument_is_monotonic) + + synchronous_sum_aggregation = SynchronousSumAggregation(False) + self.assertFalse(synchronous_sum_aggregation._instrument_is_monotonic) + + def test_aggregate(self): + """ + `SynchronousSumAggregation` aggregates data for sum metric points + """ + + synchronous_sum_aggregation = SynchronousSumAggregation(True) + + synchronous_sum_aggregation.aggregate(Measurement(1)) + synchronous_sum_aggregation.aggregate(Measurement(2)) + synchronous_sum_aggregation.aggregate(Measurement(3)) + + self.assertEqual(synchronous_sum_aggregation._value, 6) + + synchronous_sum_aggregation = SynchronousSumAggregation(True) + + synchronous_sum_aggregation.aggregate(Measurement(1)) + synchronous_sum_aggregation.aggregate(Measurement(-2)) + synchronous_sum_aggregation.aggregate(Measurement(3)) + + self.assertEqual(synchronous_sum_aggregation._value, 2) + + def test_collect(self): + """ + `SynchronousSumAggregation` collects sum metric points + """ + + synchronous_sum_aggregation = SynchronousSumAggregation(True) + + synchronous_sum_aggregation.aggregate(Measurement(1)) + first_sum = synchronous_sum_aggregation.collect() + + self.assertEqual(first_sum.value, 1) + self.assertTrue(first_sum.is_monotonic) + + synchronous_sum_aggregation.aggregate(Measurement(1)) + second_sum = synchronous_sum_aggregation.collect() + + self.assertEqual(second_sum.value, 1) + self.assertTrue(second_sum.is_monotonic) + + self.assertGreater( + second_sum.start_time_unix_nano, first_sum.start_time_unix_nano + ) + + +class TestAsynchronousSumAggregation(TestCase): + def test_instrument_monotonicity_awareness(self): + """ + `AsynchronousSumAggregation` is aware of the instrument monotonicity + """ + + asynchronous_sum_aggregation = AsynchronousSumAggregation(True) + self.assertIsInstance( + asynchronous_sum_aggregation, + _InstrumentMonotonicityAwareAggregation, + ) + self.assertTrue(asynchronous_sum_aggregation._instrument_is_monotonic) + + asynchronous_sum_aggregation = AsynchronousSumAggregation(False) + self.assertFalse(asynchronous_sum_aggregation._instrument_is_monotonic) + def test_aggregate(self): """ - `SumAggregation` collects data for sum metric points + `AsynchronousSumAggregation` aggregates data for sum metric points """ - sum_aggregation = SumAggregation(Mock()) + asynchronous_sum_aggregation = AsynchronousSumAggregation(True) + + asynchronous_sum_aggregation.aggregate(Measurement(1)) + self.assertEqual(asynchronous_sum_aggregation._value, 1) + + asynchronous_sum_aggregation.aggregate(Measurement(2)) + self.assertEqual(asynchronous_sum_aggregation._value, 2) + + asynchronous_sum_aggregation.aggregate(Measurement(3)) + self.assertEqual(asynchronous_sum_aggregation._value, 3) + + asynchronous_sum_aggregation = AsynchronousSumAggregation(True) - sum_aggregation.aggregate(1) - sum_aggregation.aggregate(2) - sum_aggregation.aggregate(3) + asynchronous_sum_aggregation.aggregate(Measurement(1)) + self.assertEqual(asynchronous_sum_aggregation._value, 1) + + asynchronous_sum_aggregation.aggregate(Measurement(-2)) + self.assertEqual(asynchronous_sum_aggregation._value, -2) + + asynchronous_sum_aggregation.aggregate(Measurement(3)) + self.assertEqual(asynchronous_sum_aggregation._value, 3) + + def test_collect(self): + """ + `AsynchronousSumAggregation` collects sum metric points + """ - self.assertEqual(sum_aggregation.value, 6) + asynchronous_sum_aggregation = AsynchronousSumAggregation(True) - sum_aggregation = SumAggregation(Mock()) + self.assertIsNone(asynchronous_sum_aggregation.collect()) - sum_aggregation.aggregate(1) - sum_aggregation.aggregate(-2) - sum_aggregation.aggregate(3) + asynchronous_sum_aggregation.aggregate(Measurement(1)) + first_sum = asynchronous_sum_aggregation.collect() - self.assertEqual(sum_aggregation.value, 2) + self.assertEqual(first_sum.value, 1) + self.assertTrue(first_sum.is_monotonic) + + asynchronous_sum_aggregation.aggregate(Measurement(1)) + second_sum = asynchronous_sum_aggregation.collect() + + self.assertEqual(second_sum.value, 1) + self.assertTrue(second_sum.is_monotonic) + + self.assertEqual( + second_sum.start_time_unix_nano, first_sum.start_time_unix_nano + ) class TestLastValueAggregation(TestCase): + def test_instrument_monotonicity_awareness(self): + """ + `LastValueAggregation` is not aware of the instrument monotonicity + """ + + sum_aggregation = LastValueAggregation() + self.assertNotIsInstance( + sum_aggregation, _InstrumentMonotonicityAwareAggregation + ) + def test_aggregate(self): """ `LastValueAggregation` collects data for gauge metric points with delta temporality """ - last_value_aggregation = LastValueAggregation(Mock()) + last_value_aggregation = LastValueAggregation() - last_value_aggregation.aggregate(1) - self.assertEqual(last_value_aggregation.value, 1) + last_value_aggregation.aggregate(Measurement(1)) + self.assertEqual(last_value_aggregation._value, 1) - last_value_aggregation.aggregate(2) - self.assertEqual(last_value_aggregation.value, 2) + last_value_aggregation.aggregate(Measurement(2)) + self.assertEqual(last_value_aggregation._value, 2) - last_value_aggregation.aggregate(3) - self.assertEqual(last_value_aggregation.value, 3) + last_value_aggregation.aggregate(Measurement(3)) + self.assertEqual(last_value_aggregation._value, 3) + + def test_collect(self): + """ + `LastValueAggregation` collects sum metric points + """ + + last_value_aggregation = LastValueAggregation() + + self.assertIsNone(last_value_aggregation.collect()) + + last_value_aggregation.aggregate(Measurement(1)) + first_gauge = last_value_aggregation.collect() + + self.assertEqual(first_gauge.value, 1) + + last_value_aggregation.aggregate(Measurement(1)) + + # CI fails the last assertion without this + sleep(0.1) + + second_gauge = last_value_aggregation.collect() + + self.assertEqual(second_gauge.value, 1) + + self.assertGreater( + second_gauge.time_unix_nano, first_gauge.time_unix_nano + ) class TestExplicitBucketHistogramAggregation(TestCase): def test_aggregate(self): """ - `ExplicitBucketHistogramAggregation` collects data for explicit_bucket_histogram metric points + `ExplicitBucketHistogramAggregation` collects data for + explicit bucket histogram metric points """ explicit_bucket_histogram_aggregation = ( - ExplicitBucketHistogramAggregation(Mock()) + ExplicitBucketHistogramAggregation() ) - explicit_bucket_histogram_aggregation.aggregate(-1) - explicit_bucket_histogram_aggregation.aggregate(2) - explicit_bucket_histogram_aggregation.aggregate(7) - explicit_bucket_histogram_aggregation.aggregate(8) - explicit_bucket_histogram_aggregation.aggregate(9999) + explicit_bucket_histogram_aggregation.aggregate(Measurement(-1)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(2)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(7)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(8)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(9999)) - self.assertEqual(explicit_bucket_histogram_aggregation.value[0], -1) - self.assertEqual(explicit_bucket_histogram_aggregation.value[5], 2) - self.assertEqual(explicit_bucket_histogram_aggregation.value[10], 15) - self.assertEqual( - explicit_bucket_histogram_aggregation.value[inf], 9999 - ) + self.assertEqual(explicit_bucket_histogram_aggregation._value[0], 1) + self.assertEqual(explicit_bucket_histogram_aggregation._value[5], 1) + self.assertEqual(explicit_bucket_histogram_aggregation._value[10], 2) + self.assertEqual(explicit_bucket_histogram_aggregation._value[inf], 1) def test_min_max(self): """ @@ -97,30 +242,57 @@ def test_min_max(self): """ explicit_bucket_histogram_aggregation = ( - ExplicitBucketHistogramAggregation(Mock()) + ExplicitBucketHistogramAggregation() + ) + + explicit_bucket_histogram_aggregation.aggregate(Measurement(-1)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(2)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(7)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(8)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(9999)) + + self.assertEqual(explicit_bucket_histogram_aggregation._min, -1) + self.assertEqual(explicit_bucket_histogram_aggregation._max, 9999) + + explicit_bucket_histogram_aggregation = ( + ExplicitBucketHistogramAggregation(record_min_max=False) ) - explicit_bucket_histogram_aggregation.aggregate(-1) - explicit_bucket_histogram_aggregation.aggregate(2) - explicit_bucket_histogram_aggregation.aggregate(7) - explicit_bucket_histogram_aggregation.aggregate(8) - explicit_bucket_histogram_aggregation.aggregate(9999) + explicit_bucket_histogram_aggregation.aggregate(Measurement(-1)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(2)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(7)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(8)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(9999)) + + self.assertEqual(explicit_bucket_histogram_aggregation._min, inf) + self.assertEqual(explicit_bucket_histogram_aggregation._max, -inf) - self.assertEqual(explicit_bucket_histogram_aggregation.min, -1) - self.assertEqual(explicit_bucket_histogram_aggregation.max, 9999) + def test_collect(self): + """ + `ExplicitBucketHistogramAggregation` collects sum metric points + """ explicit_bucket_histogram_aggregation = ( - ExplicitBucketHistogramAggregation(Mock(), record_min_max=False) + ExplicitBucketHistogramAggregation() + ) + + explicit_bucket_histogram_aggregation.aggregate(Measurement(1)) + first_histogram = explicit_bucket_histogram_aggregation.collect() + + self.assertEqual( + first_histogram.bucket_counts, (0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0) ) - explicit_bucket_histogram_aggregation.aggregate(-1) - explicit_bucket_histogram_aggregation.aggregate(2) - explicit_bucket_histogram_aggregation.aggregate(7) - explicit_bucket_histogram_aggregation.aggregate(8) - explicit_bucket_histogram_aggregation.aggregate(9999) + # CI fails the last assertion without this + sleep(0.1) - with self.assertLogs(level=WARNING): - self.assertEqual(explicit_bucket_histogram_aggregation.min, inf) + explicit_bucket_histogram_aggregation.aggregate(Measurement(1)) + second_histogram = explicit_bucket_histogram_aggregation.collect() - with self.assertLogs(level=WARNING): - self.assertEqual(explicit_bucket_histogram_aggregation.max, -inf) + self.assertEqual( + second_histogram.bucket_counts, (0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0) + ) + + self.assertGreater( + second_histogram.time_unix_nano, first_histogram.time_unix_nano + ) From 13fa2201f5b3a97c08b0d7bbd48e31f76aadd14f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 11 Jan 2022 14:17:03 -0600 Subject: [PATCH 1086/1517] Remove metric exporters from MeterProvider (#2359) --- .../opentelemetry/sdk/_metrics/__init__.py | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 8a87c22ff9..5dcb599d69 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -32,7 +32,6 @@ ObservableUpDownCounter as APIObservableUpDownCounter, ) from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter -from opentelemetry.sdk._metrics.export import MetricExporter from opentelemetry.sdk._metrics.instrument import ( Counter, Histogram, @@ -100,7 +99,6 @@ class MeterProvider(APIMeterProvider): def __init__( self, - metric_exporters: Sequence[MetricExporter] = (), metric_readers: Sequence[MetricReader] = (), resource: Resource = Resource.create({}), shutdown_on_exit: bool = True, @@ -116,8 +114,6 @@ def __init__( for metric_reader in self._metric_readers: metric_reader._register_meter_provider(self) - self._metric_exporters = metric_exporters - self._resource = resource self._shutdown = False @@ -126,7 +122,6 @@ def force_flush(self) -> bool: # FIXME implement a timeout metric_reader_result = True - metric_exporter_result = True for metric_reader in self._metric_readers: metric_reader_result = ( @@ -136,15 +131,7 @@ def force_flush(self) -> bool: if not metric_reader_result: _logger.warning("Unable to force flush all metric readers") - for metric_exporter in self._metric_exporters: - metric_exporter_result = ( - metric_exporter_result and metric_exporter.force_flush() - ) - - if not metric_exporter_result: - _logger.warning("Unable to force flush all metric exporters") - - return metric_reader_result and metric_exporter_result + return metric_reader_result def shutdown(self): # FIXME implement a timeout @@ -161,12 +148,6 @@ def shutdown(self): if not result: _logger.warning("A MetricReader failed to shutdown") - for metric_exporter in self._metric_exporters: - result = result and metric_exporter.shutdown() - - if not result: - _logger.warning("A MetricExporter failed to shutdown") - self._shutdown = True if self._atexit_handler is not None: From 2a7e3fc8efe718c9dde4c30db80ca06f42a03bee Mon Sep 17 00:00:00 2001 From: Arianna Y <92831762+areveny@users.noreply.github.com> Date: Tue, 11 Jan 2022 15:29:46 -0600 Subject: [PATCH 1087/1517] Support insecure configuration (#2350) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 + .../proto/grpc/_metric_exporter/__init__.py | 10 +++++ .../exporter/otlp/proto/grpc/exporter.py | 13 ++++-- .../proto/grpc/trace_exporter/__init__.py | 7 ++++ .../tests/logs/test_otlp_logs_exporter.py | 23 ++++++++++- .../metrics/test_otlp_metrics_exporter.py | 41 ++++++++++++++++++- .../tests/test_otlp_trace_exporter.py | 41 ++++++++++++++++++- .../sdk/environment_variables.py | 38 ++++++++++++++--- 9 files changed, 165 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7c8b1b1d0a..bf077b3d85 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 23394ccd80878a91534f8421b82a7410eb775e65 + CONTRIB_REPO_SHA: 741321434f0803dd3b304f3a93022d8d451a4c90 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 91b5be8309..c91835f1d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2334](https://github.com/open-telemetry/opentelemetry-python/pull/2334)) - Add otlp entrypoint for log exporter ([#2322](https://github.com/open-telemetry/opentelemetry-python/pull/2322)) +- Support insecure configuration for OTLP gRPC exporter + ([#2350](https://github.com/open-telemetry/opentelemetry-python/pull/2350)) ## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py index 5e76e5c035..9db2699da7 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from os import environ from typing import Optional, Sequence from grpc import ChannelCredentials, Compression from opentelemetry.exporter.otlp.proto.grpc.exporter import ( @@ -29,6 +30,9 @@ ResourceMetrics, ) from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric as PB2Metric +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_METRICS_INSECURE, +) from opentelemetry.sdk._metrics.data import ( MetricData, ) @@ -57,6 +61,12 @@ def __init__( timeout: Optional[int] = None, compression: Optional[Compression] = None, ): + + if insecure is None: + insecure = environ.get(OTEL_EXPORTER_OTLP_METRICS_INSECURE) + if insecure is not None: + insecure = insecure.lower() == "true" + super().__init__( **{ "endpoint": endpoint, diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index ec30cdac1c..cbde41bb6c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -47,6 +47,7 @@ OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_INSECURE, OTEL_EXPORTER_OTLP_TIMEOUT, ) from opentelemetry.sdk.resources import Resource as SDKResource @@ -218,11 +219,17 @@ def __init__( parsed_url = urlparse(endpoint) + if parsed_url.scheme == "https": + insecure = False if insecure is None: - if parsed_url.scheme == "https": - insecure = False + insecure = environ.get(OTEL_EXPORTER_OTLP_INSECURE) + if insecure is not None: + insecure = insecure.lower() == "true" else: - insecure = True + if parsed_url.scheme == "http": + insecure = True + else: + insecure = False if parsed_url.netloc: endpoint = parsed_url.netloc diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 075b961250..18f9ad997b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -44,6 +44,7 @@ OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_INSECURE, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) from opentelemetry.sdk.trace import ReadableSpan @@ -84,6 +85,12 @@ def __init__( timeout: Optional[int] = None, compression: Optional[Compression] = None, ): + + if insecure is None: + insecure = environ.get(OTEL_EXPORTER_OTLP_TRACES_INSECURE) + if insecure is not None: + insecure = insecure.lower() == "true" + if ( not insecure and environ.get(OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE) is not None diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index 01fe424835..556700ab85 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -193,13 +193,33 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): ( "localhost:4317", None, + mock_secure, + ), + ( + "http://localhost:4317", + True, mock_insecure, ), + ( + "localhost:4317", + True, + mock_insecure, + ), + ( + "http://localhost:4317", + False, + mock_secure, + ), ( "localhost:4317", False, mock_secure, ), + ( + "https://localhost:4317", + False, + mock_secure, + ), ( "https://localhost:4317", None, @@ -208,9 +228,10 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): ( "https://localhost:4317", True, - mock_insecure, + mock_secure, ), ] + # pylint: disable=C0209 for endpoint, insecure, mock_method in endpoints: OTLPLogExporter(endpoint=endpoint, insecure=insecure) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index f7cf8ec94d..54253c9141 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -32,6 +32,9 @@ ) from opentelemetry.sdk._metrics.data import Metric, MetricData from opentelemetry.sdk._metrics.export import MetricExportResult +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_METRICS_INSECURE, +) from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -119,6 +122,22 @@ def test_no_credentials_error( OTLPMetricExporter(insecure=False) self.assertTrue(mock_ssl_channel.called) + @patch.dict( + "os.environ", + {OTEL_EXPORTER_OTLP_METRICS_INSECURE: "True"}, + ) + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") + # pylint: disable=unused-argument + def test_otlp_insecure_from_env(self, mock_insecure): + OTLPMetricExporter() + # pylint: disable=protected-access + self.assertTrue(mock_insecure.called) + self.assertEqual( + 1, + mock_insecure.call_count, + f"expected {mock_insecure} to be called", + ) + # pylint: disable=no-self-use @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") @@ -133,13 +152,33 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): ( "localhost:4317", None, + mock_secure, + ), + ( + "http://localhost:4317", + True, mock_insecure, ), + ( + "localhost:4317", + True, + mock_insecure, + ), + ( + "http://localhost:4317", + False, + mock_secure, + ), ( "localhost:4317", False, mock_secure, ), + ( + "https://localhost:4317", + False, + mock_secure, + ), ( "https://localhost:4317", None, @@ -148,7 +187,7 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): ( "https://localhost:4317", True, - mock_insecure, + mock_secure, ), ] # pylint: disable=C0209 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 252d2f7b93..d09c21f412 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -58,6 +58,7 @@ OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_INSECURE, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) from opentelemetry.sdk.resources import Resource as SDKResource @@ -123,6 +124,8 @@ def Export(self, request, context): class TestOTLPSpanExporter(TestCase): + # pylint: disable=too-many-public-methods + def setUp(self): tracer_provider = TracerProvider() self.exporter = OTLPSpanExporter(insecure=True) @@ -289,6 +292,22 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): exporter._headers, (("key5", "value5"), ("key6", "value6")) ) + @patch.dict( + "os.environ", + {OTEL_EXPORTER_OTLP_TRACES_INSECURE: "True"}, + ) + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") + # pylint: disable=unused-argument + def test_otlp_insecure_from_env(self, mock_insecure): + OTLPSpanExporter() + # pylint: disable=protected-access + self.assertTrue(mock_insecure.called) + self.assertEqual( + 1, + mock_insecure.call_count, + f"expected {mock_insecure} to be called", + ) + # pylint: disable=no-self-use @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") @@ -304,10 +323,30 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): ( "localhost:4317", None, + mock_secure, + ), + ( + "http://localhost:4317", + True, mock_insecure, ), ( "localhost:4317", + True, + mock_insecure, + ), + ( + "http://localhost:4317", + False, + mock_secure, + ), + ( + "localhost:4317", + False, + mock_secure, + ), + ( + "https://localhost:4317", False, mock_secure, ), @@ -319,7 +358,7 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): ( "https://localhost:4317", True, - mock_insecure, + mock_secure, ), ] for endpoint, insecure, mock_method in endpoints: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index e9d35092a6..d9bab1bb16 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -281,18 +281,37 @@ .. envvar:: OTEL_EXPORTER_OTLP_ENDPOINT The :envvar:`OTEL_EXPORTER_OTLP_ENDPOINT` target to which the exporter is going to send spans or metrics. -The endpoint MUST be a valid URL with scheme (http or https) and host, and MAY contain a port and path. -A scheme of https indicates a secure connection. -Default: "https://localhost:4317" +The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path. +A scheme of https indicates a secure connection and takes precedence over the insecure configuration setting. +Default: "http://localhost:4317" """ +OTEL_EXPORTER_OTLP_INSECURE = "OTEL_EXPORTER_OTLP_INSECURE" +""" +.. envvar:: OTEL_EXPORTER_OTLP_INSECURE + +The :envvar:`OTEL_EXPORTER_OTLP_INSECURE` represents whether to enable client transport security for gRPC requests. +A scheme of https takes precedence over this configuration setting. +Default: False +""" + +OTEL_EXPORTER_OTLP_TRACES_INSECURE = "OTEL_EXPORTER_OTLP_TRACES_INSECURE" +""" +.. envvar:: OTEL_EXPORTER_OTLP_TRACES_INSECURE + +The :envvar:`OTEL_EXPORTER_OTLP_TRACES_INSECURE` represents whether to enable client transport security +for gRPC requests for spans. A scheme of https takes precedence over the this configuration setting. +Default: False +""" + + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT The :envvar:`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` target to which the span exporter is going to send spans. -The endpoint MUST be a valid URL with scheme (http or https) and host, and MAY contain a port and path. -A scheme of https indicates a secure connection. +The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path. +A scheme of https indicates a secure connection and takes precedence over this configuration setting. """ OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" @@ -335,6 +354,15 @@ wait for each batch export for spans. """ +OTEL_EXPORTER_OTLP_METRICS_INSECURE = "OTEL_EXPORTER_OTLP_METRICS_INSECURE" +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_INSECURE + +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_INSECURE` represents whether to enable client transport security +for gRPC requests for metrics. A scheme of https takes precedence over the this configuration setting. +Default: False +""" + OTEL_EXPORTER_JAEGER_CERTIFICATE = "OTEL_EXPORTER_JAEGER_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_JAEGER_CERTIFICATE From 9e78bcb029f40c5801a13a38368b56dd71658cac Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Wed, 12 Jan 2022 13:45:25 -0500 Subject: [PATCH 1088/1517] Complete metric exporter format and update OTLP exporter (#2364) --- CHANGELOG.md | 2 + .../proto/grpc/_metric_exporter/__init__.py | 30 +++++++-------- .../metrics/test_otlp_metrics_exporter.py | 23 ++++++++--- .../opentelemetry/sdk/_metrics/aggregation.py | 16 ++++---- .../src/opentelemetry/sdk/_metrics/data.py | 35 ----------------- .../sdk/_metrics/export/__init__.py | 11 +++--- .../src/opentelemetry/sdk/_metrics/point.py | 38 ++++++++++++++++++- 7 files changed, 83 insertions(+), 72 deletions(-) delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/data.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c91835f1d2..85fd2794c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2312](https://github.com/open-telemetry/opentelemetry-python/pull/2312)) - [exporter/opentelemetry-exporter-otlp-proto-grpc] Add OTLPMetricExporter ([#2323](https://github.com/open-telemetry/opentelemetry-python/pull/2323)) +- Complete metric exporter format and update OTLP exporter + ([#2364](https://github.com/open-telemetry/opentelemetry-python/pull/2364)) ## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py index 9db2699da7..d31aebfb3f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py @@ -33,8 +33,8 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_INSECURE, ) -from opentelemetry.sdk._metrics.data import ( - MetricData, +from opentelemetry.sdk._metrics.point import ( + Metric, ) from opentelemetry.sdk._metrics.export import ( @@ -45,9 +45,7 @@ class OTLPMetricExporter( MetricExporter, - OTLPExporterMixin[ - MetricData, ExportMetricsServiceRequest, MetricExportResult - ], + OTLPExporterMixin[Metric, ExportMetricsServiceRequest, MetricExportResult], ): _result = MetricExportResult _stub = MetricsServiceStub @@ -79,13 +77,13 @@ def __init__( ) def _translate_data( - self, data: Sequence[MetricData] + self, data: Sequence[Metric] ) -> ExportMetricsServiceRequest: sdk_resource_instrumentation_library_metrics = {} self._collector_metric_kwargs = {} - for metric_data in data: - resource = metric_data.metric.resource + for metric in data: + resource = metric.resource instrumentation_library_map = ( sdk_resource_instrumentation_library_metrics.get(resource, {}) ) @@ -95,26 +93,26 @@ def _translate_data( ] = instrumentation_library_map instrumentation_library_metrics = instrumentation_library_map.get( - metric_data.instrumentation_info + metric.instrumentation_info ) if not instrumentation_library_metrics: - if metric_data.instrumentation_info is not None: + if metric.instrumentation_info is not None: instrumentation_library_map[ - metric_data.instrumentation_info + metric.instrumentation_info ] = InstrumentationLibraryMetrics( instrumentation_library=InstrumentationLibrary( - name=metric_data.instrumentation_info.name, - version=metric_data.instrumentation_info.version, + name=metric.instrumentation_info.name, + version=metric.instrumentation_info.version, ) ) else: instrumentation_library_map[ - metric_data.instrumentation_info + metric.instrumentation_info ] = InstrumentationLibraryMetrics() instrumentation_library_metrics = instrumentation_library_map.get( - metric_data.instrumentation_info + metric.instrumentation_info ) instrumentation_library_metrics.metrics.append( @@ -128,7 +126,7 @@ def _translate_data( ) ) - def export(self, metrics: Sequence[MetricData]) -> MetricExportResult: + def export(self, metrics: Sequence[Metric]) -> MetricExportResult: return self._export(metrics) def shutdown(self): diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index 54253c9141..82da7a0e98 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -30,8 +30,12 @@ MetricsServiceServicer, add_MetricsServiceServicer_to_server, ) -from opentelemetry.sdk._metrics.data import Metric, MetricData from opentelemetry.sdk._metrics.export import MetricExportResult +from opentelemetry.sdk._metrics.point import ( + AggregationTemporality, + Metric, + Sum, +) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_INSECURE, ) @@ -96,13 +100,22 @@ def setUp(self): self.server.start() - self.metric_data_1 = MetricData( - metric=Metric( - resource=SDKResource({"key": "value"}), - ), + self.metric_data_1 = Metric( + resource=SDKResource({"key": "value"}), instrumentation_info=InstrumentationInfo( "first_name", "first_version" ), + attributes={}, + description="foo", + name="foometric", + unit="s", + point=Sum( + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + value=33, + ), ) def tearDown(self): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 03a2debfe0..3402f1a474 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -14,23 +14,21 @@ from abc import ABC, abstractmethod from collections import OrderedDict -from enum import IntEnum from logging import getLogger from math import inf from threading import Lock from typing import Generic, Optional, Sequence, TypeVar from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import Gauge, Histogram, PointT, Sum +from opentelemetry.sdk._metrics.point import ( + AggregationTemporality, + Gauge, + Histogram, + PointT, + Sum, +) from opentelemetry.util._time import _time_ns - -class AggregationTemporality(IntEnum): - UNSPECIFIED = 0 - DELTA = 1 - CUMULATIVE = 2 - - _PointVarT = TypeVar("_PointVarT", bound=PointT) _logger = getLogger(__name__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/data.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/data.py deleted file mode 100644 index e582d66684..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/data.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo - - -class Metric: - """TODO fill this in""" - - def __init__(self, resource: Resource) -> None: - self.resource = resource - - -class MetricData: - """Readable Metric data plus associated InstrumentationLibrary.""" - - def __init__( - self, - metric: Metric, - instrumentation_info: InstrumentationInfo, - ): - self.metric = metric - self.instrumentation_info = instrumentation_info diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py index e9a83861fe..b5e2f08610 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -18,7 +18,7 @@ from sys import stdout from typing import IO, Callable, Sequence -from opentelemetry.sdk._metrics.data import Metric, MetricData +from opentelemetry.sdk._metrics.point import Metric class MetricExportResult(Enum): @@ -33,7 +33,8 @@ class MetricExporter(ABC): in their own format. """ - def export(self, metrics: Sequence[MetricData]) -> "MetricExportResult": + @abstractmethod + def export(self, metrics: Sequence[Metric]) -> "MetricExportResult": """Exports a batch of telemetry data. Args: @@ -68,9 +69,9 @@ def __init__( self.out = out self.formatter = formatter - def export(self, metrics: Sequence[MetricData]) -> MetricExportResult: - for data in metrics: - self.out.write(self.formatter(data.metric)) + def export(self, metrics: Sequence[Metric]) -> MetricExportResult: + for metric in metrics: + self.out.write(self.formatter(metric)) self.out.flush() return MetricExportResult.SUCCESS diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py index 2503bb43e3..64d62c5ba7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py @@ -13,15 +13,26 @@ # limitations under the License. from dataclasses import dataclass +from enum import IntEnum from typing import Sequence, Union +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.util.types import Attributes + + +class AggregationTemporality(IntEnum): + UNSPECIFIED = 0 + DELTA = 1 + CUMULATIVE = 2 + @dataclass(frozen=True) class Sum: start_time_unix_nano: int time_unix_nano: int value: Union[int, float] - aggregation_temporality: int + aggregation_temporality: AggregationTemporality is_monotonic: bool @@ -37,7 +48,30 @@ class Histogram: time_unix_nano: int bucket_counts: Sequence[int] explicit_bounds: Sequence[float] - aggregation_temporality: int + aggregation_temporality: AggregationTemporality PointT = Union[Sum, Gauge, Histogram] + + +@dataclass(frozen=True) +class Metric: + """Represents a metric point in the OpenTelemetry data model to be exported + + Concrete metric types contain all the information as in the OTLP proto definitions + (https://tinyurl.com/7h6yx24v) but are flattened as much as possible. + """ + + # common fields to all metric kinds + attributes: Attributes + description: str + instrumentation_info: InstrumentationInfo + name: str + resource: Resource + unit: str + + point: PointT + """Contains non-common fields for the given metric""" + + def to_json(self) -> str: + raise NotImplementedError() From 6a8898783dd784476735e02a1c6e909e5574a9a4 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 13 Jan 2022 13:54:09 -0600 Subject: [PATCH 1089/1517] Add measurement consumer (#2339) * Add measurement consumer * Fix lint * Address comments * Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py Co-authored-by: Aaron Abbott * Rename Serial to Synchronous * Fix asynchronous instrument registering bug * Fix type * Use right path * Fix lint * Fix circular import * Pass name first * Implement different fix for circular import Co-authored-by: Aaron Abbott --- .../opentelemetry/sdk/_metrics/__init__.py | 81 ++++++++++++--- .../opentelemetry/sdk/_metrics/instrument.py | 28 +++++- .../sdk/_metrics/measurement_consumer.py | 55 +++++++++++ .../tests/metrics/test_instrument.py | 16 +-- .../metrics/test_measurement_consumer.py | 99 +++++++++++++++++++ .../tests/metrics/test_metrics.py | 11 ++- 6 files changed, 265 insertions(+), 25 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py create mode 100644 opentelemetry-sdk/tests/metrics/test_measurement_consumer.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 5dcb599d69..6693bc5e25 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -40,6 +40,10 @@ ObservableUpDownCounter, UpDownCounter, ) +from opentelemetry.sdk._metrics.measurement_consumer import ( + MeasurementConsumer, + SynchronousMeasurementConsumer, +) from opentelemetry.sdk._metrics.metric_reader import MetricReader from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -51,48 +55,94 @@ class Meter(APIMeter): def __init__( self, instrumentation_info: InstrumentationInfo, - meter_provider: APIMeterProvider, + measurement_consumer: MeasurementConsumer, ): super().__init__(instrumentation_info) self._instrumentation_info = instrumentation_info - self._meter_provider = meter_provider + self._measurement_consumer = measurement_consumer def create_counter(self, name, unit=None, description=None) -> APICounter: - return Counter(self._instrumentation_info, name, unit, description) + return Counter( + name, + self._instrumentation_info, + self._measurement_consumer, + unit, + description, + ) def create_up_down_counter( self, name, unit=None, description=None ) -> APIUpDownCounter: return UpDownCounter( - self._instrumentation_info, name, unit, description + name, + self._instrumentation_info, + self._measurement_consumer, + unit, + description, ) def create_observable_counter( self, name, callback, unit=None, description=None ) -> APIObservableCounter: - return ObservableCounter( - self._instrumentation_info, name, callback, unit, description + + instrument = ObservableCounter( + name, + self._instrumentation_info, + self._measurement_consumer, + callback, + unit, + description, ) + self._measurement_consumer.register_asynchronous_instrument(instrument) + + return instrument + def create_histogram( self, name, unit=None, description=None ) -> APIHistogram: - return Histogram(self._instrumentation_info, name, unit, description) + return Histogram( + name, + self._instrumentation_info, + self._measurement_consumer, + unit, + description, + ) def create_observable_gauge( self, name, callback, unit=None, description=None ) -> APIObservableGauge: - return ObservableGauge( - self._instrumentation_info, name, callback, unit, description + + instrument = ObservableGauge( + name, + self._instrumentation_info, + self._measurement_consumer, + callback, + unit, + description, ) + self._measurement_consumer.register_asynchronous_instrument(instrument) + + return instrument + def create_observable_up_down_counter( self, name, callback, unit=None, description=None ) -> APIObservableUpDownCounter: - return ObservableUpDownCounter( - self._instrumentation_info, name, callback, unit, description + + instrument = ObservableUpDownCounter( + name, + self._instrumentation_info, + self._measurement_consumer, + callback, + unit, + description, ) + self._measurement_consumer.register_asynchronous_instrument(instrument) + + return instrument + class MeterProvider(APIMeterProvider): """See `opentelemetry._metrics.MeterProvider`.""" @@ -106,13 +156,15 @@ def __init__( self._lock = Lock() self._atexit_handler = None + self._measurement_consumer = SynchronousMeasurementConsumer() + if shutdown_on_exit: self._atexit_handler = register(self.shutdown) self._metric_readers = metric_readers for metric_reader in self._metric_readers: - metric_reader._register_meter_provider(self) + metric_reader._register_measurement_consumer(self) self._resource = resource self._shutdown = False @@ -169,4 +221,7 @@ def get_meter( ) return _DefaultMeter(name, version=version, schema_url=schema_url) - return Meter(InstrumentationInfo(name, version, schema_url), self) + return Meter( + InstrumentationInfo(name, version, schema_url), + self._measurement_consumer, + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index 6a68dbeab6..3b2db18835 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -30,32 +30,42 @@ ) from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk._metrics.measurement_consumer import MeasurementConsumer from opentelemetry.sdk.util.instrumentation import InstrumentationInfo class _Synchronous: def __init__( self, - instrumentation_info: InstrumentationInfo, name: str, + instrumentation_info: InstrumentationInfo, + measurement_consumer: MeasurementConsumer, unit: str = "", description: str = "", ): + self.name = name + self.unit = unit + self.description = description self._instrumentation_info = instrumentation_info + self._measurement_consumer = measurement_consumer super().__init__(name, unit=unit, description=description) class _Asynchronous: def __init__( self, - instrumentation_info: InstrumentationInfo, name: str, + instrumentation_info: InstrumentationInfo, + measurement_consumer: MeasurementConsumer, callback: CallbackT, unit: str = "", description: str = "", ): - + self.name = name + self.unit = unit + self.description = description self._instrumentation_info = instrumentation_info + self._measurement_consumer = measurement_consumer super().__init__(name, callback, unit=unit, description=description) self._callback = callback @@ -79,12 +89,18 @@ def add( if amount < 0: raise Exception("amount must be non negative") + self._measurement_consumer.consume_measurement( + Measurement(amount, attributes) + ) + class UpDownCounter(_Synchronous, APIUpDownCounter): def add( self, amount: Union[int, float], attributes: Dict[str, str] = None ): - pass + self._measurement_consumer.consume_measurement( + Measurement(amount, attributes) + ) class ObservableCounter(_Asynchronous, APIObservableCounter): @@ -97,7 +113,9 @@ class ObservableUpDownCounter(_Asynchronous, APIObservableUpDownCounter): class Histogram(_Synchronous, APIHistogram): def record(self, amount, attributes=None): - pass + self._measurement_consumer.consume_measurement( + Measurement(amount, attributes) + ) class ObservableGauge(_Asynchronous, APIObservableGauge): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py new file mode 100644 index 0000000000..d8c75139cf --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Iterable + +from opentelemetry.sdk._metrics.aggregation import AggregationTemporality +from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk._metrics.metric_reader import MetricReader +from opentelemetry.sdk._metrics.point import Metric + +if TYPE_CHECKING: + from opentelemetry.sdk._metrics.instrument import _Asynchronous + + +class MeasurementConsumer(ABC): + @abstractmethod + def consume_measurement(self, measurement: Measurement) -> None: + pass + + @abstractmethod + def register_asynchronous_instrument(self, instrument: "_Asynchronous"): + pass + + @abstractmethod + def collect( + self, metric_reader: MetricReader, temporality: AggregationTemporality + ) -> Iterable[Metric]: + pass + + +class SynchronousMeasurementConsumer(MeasurementConsumer): + def consume_measurement(self, measurement: Measurement) -> None: + pass + + def register_asynchronous_instrument( + self, instrument: "_Asynchronous" + ) -> None: + pass + + def collect( + self, metric_reader: MetricReader, temporality: AggregationTemporality + ) -> Iterable[Metric]: + pass diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py index b54391b2c0..64d20d6862 100644 --- a/opentelemetry-sdk/tests/metrics/test_instrument.py +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -27,7 +27,7 @@ def test_callable_callback(self): def callback(): return [1, 2, 3] - observable_gauge = ObservableGauge(Mock(), "name", callback) + observable_gauge = ObservableGauge("name", Mock(), Mock(), callback) self.assertEqual(observable_gauge.callback(), [1, 2, 3]) @@ -35,7 +35,7 @@ def test_generator_callback(self): def callback(): yield [1, 2, 3] - observable_gauge = ObservableGauge(Mock(), "name", callback()) + observable_gauge = ObservableGauge("name", Mock(), Mock(), callback()) self.assertEqual(observable_gauge.callback(), [1, 2, 3]) @@ -45,7 +45,9 @@ def test_callable_callback(self): def callback(): return [1, 2, 3] - observable_counter = ObservableCounter(Mock(), "name", callback) + observable_counter = ObservableCounter( + "name", Mock(), Mock(), callback + ) self.assertEqual(observable_counter.callback(), [1, 2, 3]) @@ -53,7 +55,9 @@ def test_generator_callback(self): def callback(): yield [1, 2, 3] - observable_counter = ObservableCounter(Mock(), "name", callback()) + observable_counter = ObservableCounter( + "name", Mock(), Mock(), callback() + ) self.assertEqual(observable_counter.callback(), [1, 2, 3]) @@ -64,7 +68,7 @@ def callback(): return [1, 2, 3] observable_up_down_counter = ObservableUpDownCounter( - Mock(), "name", callback + "name", Mock(), Mock(), callback ) self.assertEqual(observable_up_down_counter.callback(), [1, 2, 3]) @@ -74,7 +78,7 @@ def callback(): yield [1, 2, 3] observable_up_down_counter = ObservableUpDownCounter( - Mock(), "name", callback() + "name", Mock(), Mock(), callback() ) self.assertEqual(observable_up_down_counter.callback(), [1, 2, 3]) diff --git a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py new file mode 100644 index 0000000000..f1a96cadc1 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py @@ -0,0 +1,99 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase +from unittest.mock import Mock, patch + +from opentelemetry.sdk._metrics import MeterProvider +from opentelemetry.sdk._metrics.measurement_consumer import ( + MeasurementConsumer, + SynchronousMeasurementConsumer, +) + + +class TestSynchronousMeasurementConsumer(TestCase): + def test_parent(self): + + self.assertIsInstance( + SynchronousMeasurementConsumer(), MeasurementConsumer + ) + + @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + def test_measurement_consumer_class( + self, mock_serial_measurement_consumer + ): + MeterProvider() + + mock_serial_measurement_consumer.assert_called() + + @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + def test_register_asynchronous_instrument( + self, mock_serial_measurement_consumer + ): + + meter_provider = MeterProvider() + + meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( + meter_provider.get_meter("name").create_observable_counter( + "name", Mock() + ) + ) + meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( + meter_provider.get_meter("name").create_observable_up_down_counter( + "name", Mock() + ) + ) + meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( + meter_provider.get_meter("name").create_observable_gauge( + "name", Mock() + ) + ) + + @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + def test_consume_measurement_counter( + self, mock_serial_measurement_consumer + ): + + meter_provider = MeterProvider() + counter = meter_provider.get_meter("name").create_counter("name") + + counter.add(1) + + meter_provider._measurement_consumer.consume_measurement.assert_called() + + @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + def test_consume_measurement_up_down_counter( + self, mock_serial_measurement_consumer + ): + + meter_provider = MeterProvider() + counter = meter_provider.get_meter("name").create_up_down_counter( + "name" + ) + + counter.add(1) + + meter_provider._measurement_consumer.consume_measurement.assert_called() + + @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + def test_consume_measurement_histogram( + self, mock_serial_measurement_consumer + ): + + meter_provider = MeterProvider() + counter = meter_provider.get_meter("name").create_histogram("name") + + counter.record(1) + + meter_provider._measurement_consumer.consume_measurement.assert_called() diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 0077d5b1cd..d9a6dc9caf 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -26,6 +26,9 @@ ObservableUpDownCounter, UpDownCounter, ) +from opentelemetry.sdk._metrics.measurement_consumer import ( + SynchronousMeasurementConsumer, +) from opentelemetry.sdk.resources import Resource @@ -79,7 +82,7 @@ def test_shutdown_subsequent_calls(self): class TestMeter(TestCase): def setUp(self): - self.meter = Meter(Mock(), MeterProvider()) + self.meter = Meter(Mock(), SynchronousMeasurementConsumer()) def test_create_counter(self): counter = self.meter.create_counter( @@ -87,6 +90,7 @@ def test_create_counter(self): ) self.assertIsInstance(counter, Counter) + self.assertEqual(counter.name, "name") def test_create_up_down_counter(self): up_down_counter = self.meter.create_up_down_counter( @@ -94,6 +98,7 @@ def test_create_up_down_counter(self): ) self.assertIsInstance(up_down_counter, UpDownCounter) + self.assertEqual(up_down_counter.name, "name") def test_create_observable_counter(self): observable_counter = self.meter.create_observable_counter( @@ -101,6 +106,7 @@ def test_create_observable_counter(self): ) self.assertIsInstance(observable_counter, ObservableCounter) + self.assertEqual(observable_counter.name, "name") def test_create_histogram(self): histogram = self.meter.create_histogram( @@ -108,6 +114,7 @@ def test_create_histogram(self): ) self.assertIsInstance(histogram, Histogram) + self.assertEqual(histogram.name, "name") def test_create_observable_gauge(self): observable_gauge = self.meter.create_observable_gauge( @@ -115,6 +122,7 @@ def test_create_observable_gauge(self): ) self.assertIsInstance(observable_gauge, ObservableGauge) + self.assertEqual(observable_gauge.name, "name") def test_create_observable_up_down_counter(self): observable_up_down_counter = ( @@ -125,3 +133,4 @@ def test_create_observable_up_down_counter(self): self.assertIsInstance( observable_up_down_counter, ObservableUpDownCounter ) + self.assertEqual(observable_up_down_counter.name, "name") From 9a87b4930a5e6f2e7b3de957abfb9c2f9c6d8b9b Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 13 Jan 2022 18:44:38 -0600 Subject: [PATCH 1090/1517] Use binary search for histogram values (#2351) * Use binary search for histogram values Fixes #2336 * Refactor Histogram aggregation data structures --- .../opentelemetry/sdk/_metrics/aggregation.py | 22 ++++------- .../tests/metrics/test_aggregation.py | 38 ++++++++++--------- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 3402f1a474..db0b4563dd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -13,7 +13,7 @@ # limitations under the License. from abc import ABC, abstractmethod -from collections import OrderedDict +from bisect import bisect_left from logging import getLogger from math import inf from threading import Lock @@ -156,13 +156,14 @@ def __init__( record_min_max: bool = True, ): super().__init__() - self._value = OrderedDict([(key, 0) for key in (*boundaries, inf)]) + # pylint: disable=unnecessary-comprehension + self._boundaries = [boundary for boundary in (*boundaries, inf)] + self.value = [0 for _ in range(len(self._boundaries))] self._min = inf self._max = -inf self._sum = 0 self._record_min_max = record_min_max self._start_time_unix_nano = _time_ns() - self._boundaries = boundaries def aggregate(self, measurement: Measurement) -> None: @@ -174,12 +175,7 @@ def aggregate(self, measurement: Measurement) -> None: self._sum += value - for key in self._value.keys(): - - if value < key: - self._value[key] = self._value[key] + 1 - - break + self.value[bisect_left(self._boundaries, value)] += 1 def collect(self) -> Optional[Histogram]: """ @@ -188,18 +184,16 @@ def collect(self) -> Optional[Histogram]: now = _time_ns() with self._lock: - value = self._value + value = self.value start_time_unix_nano = self._start_time_unix_nano - self._value = OrderedDict( - [(key, 0) for key in (*self._boundaries, inf)] - ) + self.value = [0 for _ in range(len(self._boundaries))] self._start_time_unix_nano = now + 1 return Histogram( start_time_unix_nano=start_time_unix_nano, time_unix_nano=now, - bucket_counts=tuple(value.values()), + bucket_counts=tuple(value), explicit_bounds=self._boundaries, aggregation_temporality=AggregationTemporality.DELTA, ) diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index c8064b9d1d..bc8858b474 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -216,24 +216,32 @@ def test_collect(self): class TestExplicitBucketHistogramAggregation(TestCase): def test_aggregate(self): """ - `ExplicitBucketHistogramAggregation` collects data for - explicit bucket histogram metric points + Test `ExplicitBucketHistogramAggregation with custom boundaries """ explicit_bucket_histogram_aggregation = ( - ExplicitBucketHistogramAggregation() + ExplicitBucketHistogramAggregation(boundaries=[0, 2, 4]) ) explicit_bucket_histogram_aggregation.aggregate(Measurement(-1)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(0)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(1)) explicit_bucket_histogram_aggregation.aggregate(Measurement(2)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(7)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(8)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(9999)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(3)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(4)) + explicit_bucket_histogram_aggregation.aggregate(Measurement(5)) + + # The first bucket keeps count of values between (-inf, 0] (-1 and 0) + self.assertEqual(explicit_bucket_histogram_aggregation.value[0], 2) + + # The second bucket keeps count of values between (0, 2] (1 and 2) + self.assertEqual(explicit_bucket_histogram_aggregation.value[1], 2) - self.assertEqual(explicit_bucket_histogram_aggregation._value[0], 1) - self.assertEqual(explicit_bucket_histogram_aggregation._value[5], 1) - self.assertEqual(explicit_bucket_histogram_aggregation._value[10], 2) - self.assertEqual(explicit_bucket_histogram_aggregation._value[inf], 1) + # The third bucket keeps count of values between (2, 4] (3 and 4) + self.assertEqual(explicit_bucket_histogram_aggregation.value[2], 2) + + # The fourth bucket keeps count of values between (4, inf) (3 and 4) + self.assertEqual(explicit_bucket_histogram_aggregation.value[3], 1) def test_min_max(self): """ @@ -273,15 +281,13 @@ def test_collect(self): """ explicit_bucket_histogram_aggregation = ( - ExplicitBucketHistogramAggregation() + ExplicitBucketHistogramAggregation(boundaries=[0, 1, 2]) ) explicit_bucket_histogram_aggregation.aggregate(Measurement(1)) first_histogram = explicit_bucket_histogram_aggregation.collect() - self.assertEqual( - first_histogram.bucket_counts, (0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0) - ) + self.assertEqual(first_histogram.bucket_counts, (0, 1, 0, 0)) # CI fails the last assertion without this sleep(0.1) @@ -289,9 +295,7 @@ def test_collect(self): explicit_bucket_histogram_aggregation.aggregate(Measurement(1)) second_histogram = explicit_bucket_histogram_aggregation.collect() - self.assertEqual( - second_histogram.bucket_counts, (0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0) - ) + self.assertEqual(second_histogram.bucket_counts, (0, 1, 0, 0)) self.assertGreater( second_histogram.time_unix_nano, first_histogram.time_unix_nano From 54d36c326c900b66138d7879b3208a812b6ad675 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 14 Jan 2022 08:57:50 -0800 Subject: [PATCH 1091/1517] [docs] fix logs example (#2372) The collector configuration needed updating in the logs example. The root logger also needed fixing. Co-authored-by: Leighton Chen --- docs/examples/logs/README.rst | 79 ++++++++++--------- docs/examples/logs/example.py | 2 +- docs/examples/logs/otel-collector-config.yaml | 6 ++ 3 files changed, 47 insertions(+), 40 deletions(-) diff --git a/docs/examples/logs/README.rst b/docs/examples/logs/README.rst index 909fefd593..d9d52c42f5 100644 --- a/docs/examples/logs/README.rst +++ b/docs/examples/logs/README.rst @@ -12,15 +12,21 @@ Start the Collector locally to see data being exported. Write the following file # otel-collector-config.yaml receivers: - otlp: - protocols: - grpc: - + otlp: + protocols: + grpc: + exporters: - logging: - + logging: + processors: - batch: + batch: + + service: + pipelines: + logs: + receivers: [otlp] + exporters: [logging] Then start the Docker container: @@ -39,42 +45,37 @@ The resulting logs will appear in the output from the collector and look similar .. code-block:: sh - ResourceLog #0 - Resource labels: - -> telemetry.sdk.language: STRING(python) - -> telemetry.sdk.name: STRING(opentelemetry) - -> telemetry.sdk.version: STRING(1.5.0.dev0) - -> service.name: STRING(unknown_service) - InstrumentationLibraryLogs #0 - InstrumentationLibrary __main__ 0.1 - LogRecord #0 - Timestamp: 2021-08-18 08:26:53.837349888 +0000 UTC - Severity: ERROR - ShortName: - Body: Exception while exporting logs. - ResourceLog #1 + Resource SchemaURL: Resource labels: - -> telemetry.sdk.language: STRING(python) - -> telemetry.sdk.name: STRING(opentelemetry) - -> telemetry.sdk.version: STRING(1.5.0.dev0) - -> service.name: STRING(unknown_service) + -> telemetry.sdk.language: STRING(python) + -> telemetry.sdk.name: STRING(opentelemetry) + -> telemetry.sdk.version: STRING(1.8.0) + -> service.name: STRING(shoppingcart) + -> service.instance.id: STRING(instance-12) InstrumentationLibraryLogs #0 + InstrumentationLibraryMetrics SchemaURL: InstrumentationLibrary __main__ 0.1 LogRecord #0 - Timestamp: 2021-08-18 08:26:53.842546944 +0000 UTC + Timestamp: 2022-01-13 20:37:03.998733056 +0000 UTC + Severity: WARNING + ShortName: + Body: Jail zesty vixen who grabbed pay from quack. + Trace ID: + Span ID: + Flags: 0 + LogRecord #1 + Timestamp: 2022-01-13 20:37:04.082757888 +0000 UTC Severity: ERROR - ShortName: + ShortName: Body: The five boxing wizards jump quickly. - ResourceLog #2 - Resource labels: - -> telemetry.sdk.language: STRING(python) - -> telemetry.sdk.name: STRING(opentelemetry) - -> telemetry.sdk.version: STRING(1.5.0.dev0) - -> service.name: STRING(unknown_service) - InstrumentationLibraryLogs #0 - InstrumentationLibrary __main__ 0.1 - LogRecord #0 - Timestamp: 2021-08-18 08:26:53.843979008 +0000 UTC + Trace ID: + Span ID: + Flags: 0 + LogRecord #2 + Timestamp: 2022-01-13 20:37:04.082979072 +0000 UTC Severity: ERROR - ShortName: - Body: Hyderabad, we have a major problem. \ No newline at end of file + ShortName: + Body: Hyderabad, we have a major problem. + Trace ID: 63491217958f126f727622e41d4460f3 + Span ID: d90c57d6e1ca4f6c + Flags: 1 \ No newline at end of file diff --git a/docs/examples/logs/example.py b/docs/examples/logs/example.py index b34d9a88cc..33879fb218 100644 --- a/docs/examples/logs/example.py +++ b/docs/examples/logs/example.py @@ -38,7 +38,7 @@ handler = OTLPHandler(level=logging.NOTSET, log_emitter=log_emitter) # Attach OTLP handler to root logger -logging.getLogger("root").addHandler(handler) +logging.getLogger().addHandler(handler) # Log directly logging.info("Jackdaws love my big sphinx of quartz.") diff --git a/docs/examples/logs/otel-collector-config.yaml b/docs/examples/logs/otel-collector-config.yaml index f29ce6476c..71a97bf7c2 100644 --- a/docs/examples/logs/otel-collector-config.yaml +++ b/docs/examples/logs/otel-collector-config.yaml @@ -8,3 +8,9 @@ exporters: processors: batch: + +service: + pipelines: + logs: + receivers: [otlp] + exporters: [logging] \ No newline at end of file From 8ff97ea4473f4665eb08a8cc25801c21b01d6cdb Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 14 Jan 2022 11:45:40 -0800 Subject: [PATCH 1092/1517] [docs] add metrics examples (#2371) --- docs/examples/metrics/README.rst | 48 +++++++++++++++++++ docs/examples/metrics/example.py | 14 ++++++ .../metrics/otel-collector-config.yaml | 16 +++++++ docs/getting_started/metrics_example.py | 15 +++++- docs/sdk/metrics.rst | 17 +++++++ docs/sdk/sdk.rst | 1 + 6 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 docs/examples/metrics/README.rst create mode 100644 docs/examples/metrics/example.py create mode 100644 docs/examples/metrics/otel-collector-config.yaml create mode 100644 docs/sdk/metrics.rst diff --git a/docs/examples/metrics/README.rst b/docs/examples/metrics/README.rst new file mode 100644 index 0000000000..2a9bb70318 --- /dev/null +++ b/docs/examples/metrics/README.rst @@ -0,0 +1,48 @@ +OpenTelemetry Metrics SDK +========================= + +.. warning:: + OpenTelemetry Python metrics are in an experimental state. The APIs within + :mod:`opentelemetry.sdk._metrics` are subject to change in minor/patch releases and there are no + backward compatability guarantees at this time. + +Start the Collector locally to see data being exported. Write the following file: + +.. code-block:: yaml + + # otel-collector-config.yaml + receivers: + otlp: + protocols: + grpc: + + exporters: + logging: + + processors: + batch: + + service: + pipelines: + metrics: + receivers: [otlp] + exporters: [logging] + +Then start the Docker container: + +.. code-block:: sh + + docker run \ + -p 4317:4317 \ + -v $(pwd)/otel-collector-config.yaml:/etc/otel/config.yaml \ + otel/opentelemetry-collector-contrib:latest + +.. code-block:: sh + + $ python example.py + +The resulting metrics will appear in the output from the collector and look similar to this: + +.. code-block:: sh + +TODO \ No newline at end of file diff --git a/docs/examples/metrics/example.py b/docs/examples/metrics/example.py new file mode 100644 index 0000000000..359bb31bf3 --- /dev/null +++ b/docs/examples/metrics/example.py @@ -0,0 +1,14 @@ +from opentelemetry._metrics import get_meter_provider, set_meter_provider +from opentelemetry.exporter.otlp.proto.grpc._metric_exporter import ( + OTLPMetricExporter, +) +from opentelemetry.sdk._metrics import MeterProvider + +provider = MeterProvider() +exporter = OTLPMetricExporter(insecure=True) +# TODO: fill in details for metric reader +set_meter_provider(provider) + +meter = get_meter_provider().get_meter("getting-started") +counter = meter.create_counter("first_counter") +# TODO: fill in details for additional metrics diff --git a/docs/examples/metrics/otel-collector-config.yaml b/docs/examples/metrics/otel-collector-config.yaml new file mode 100644 index 0000000000..3ae12695e6 --- /dev/null +++ b/docs/examples/metrics/otel-collector-config.yaml @@ -0,0 +1,16 @@ +receivers: + otlp: + protocols: + grpc: + +exporters: + logging: + +processors: + batch: + +service: + pipelines: + metrics: + receivers: [otlp] + exporters: [logging] diff --git a/docs/getting_started/metrics_example.py b/docs/getting_started/metrics_example.py index d0bb758306..ccbe465d1d 100644 --- a/docs/getting_started/metrics_example.py +++ b/docs/getting_started/metrics_example.py @@ -13,4 +13,17 @@ # limitations under the License. # metrics.py -# TODO +# This is still work in progress as the metrics SDK is being implemented + +from opentelemetry._metrics import get_meter_provider, set_meter_provider +from opentelemetry.sdk._metrics import MeterProvider +from opentelemetry.sdk._metrics.export import ConsoleMetricExporter + +provider = MeterProvider() +exporter = ConsoleMetricExporter() +# TODO: fill in details for metric reader +set_meter_provider(provider) + +meter = get_meter_provider().get_meter("getting-started") +counter = meter.create_counter("first_counter") +# TODO: fill in details for additional metrics diff --git a/docs/sdk/metrics.rst b/docs/sdk/metrics.rst new file mode 100644 index 0000000000..39041ea27d --- /dev/null +++ b/docs/sdk/metrics.rst @@ -0,0 +1,17 @@ +opentelemetry.sdk._metrics package +================================== + +.. warning:: + OpenTelemetry Python metrics are in an experimental state. The APIs within + :mod:`opentelemetry.sdk._metrics` are subject to change in minor/patch releases and there are no + backward compatability guarantees at this time. + + Once logs become stable, this package will be be renamed to ``opentelemetry.sdk.metrics``. + +Submodules +---------- + +.. automodule:: opentelemetry.sdk._metrics + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sdk/sdk.rst b/docs/sdk/sdk.rst index 619f3bd8cc..47d49a5550 100644 --- a/docs/sdk/sdk.rst +++ b/docs/sdk/sdk.rst @@ -8,6 +8,7 @@ OpenTelemetry Python SDK resources trace + metrics logs error_handler environment_variables From fb1063ce89543b87a0d35be2e97f3f0a1a83dc8b Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 14 Jan 2022 16:40:44 -0800 Subject: [PATCH 1093/1517] Handle duplicate meters (#2373) --- .../opentelemetry/sdk/_metrics/__init__.py | 14 +++++++---- .../tests/metrics/test_metrics.py | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 6693bc5e25..56b0ad704f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -154,6 +154,7 @@ def __init__( shutdown_on_exit: bool = True, ): self._lock = Lock() + self._meter_lock = Lock() self._atexit_handler = None self._measurement_consumer = SynchronousMeasurementConsumer() @@ -161,6 +162,7 @@ def __init__( if shutdown_on_exit: self._atexit_handler = register(self.shutdown) + self._meters = {} self._metric_readers = metric_readers for metric_reader in self._metric_readers: @@ -221,7 +223,11 @@ def get_meter( ) return _DefaultMeter(name, version=version, schema_url=schema_url) - return Meter( - InstrumentationInfo(name, version, schema_url), - self._measurement_consumer, - ) + info = InstrumentationInfo(name, version, schema_url) + with self._meter_lock: + if not self._meters.get(info): + self._meters[info] = Meter( + info, + self._measurement_consumer, + ) + return self._meters[info] diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index d9a6dc9caf..2071416702 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -64,6 +64,30 @@ def test_get_meter(self): self.assertEqual(meter._instrumentation_info.version, "version") self.assertEqual(meter._instrumentation_info.schema_url, "schema_url") + def test_get_meter_duplicate(self): + """ + Subsequent calls to `MeterProvider.get_meter` with the same arguments + should return the same `Meter` instance. + """ + mp = MeterProvider() + meter1 = mp.get_meter( + "name", + version="version", + schema_url="schema_url", + ) + meter2 = mp.get_meter( + "name", + version="version", + schema_url="schema_url", + ) + meter3 = mp.get_meter( + "name2", + version="version", + schema_url="schema_url", + ) + self.assertIs(meter1, meter2) + self.assertIsNot(meter1, meter3) + def test_shutdown_subsequent_calls(self): """ No subsequent attempts to get a `Meter` are allowed after calling From 1a52e9df1c10a42da7562f8b4d11f43576ed0aed Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 17 Jan 2022 11:30:53 -0800 Subject: [PATCH 1094/1517] Add NoOpTracer and NoOpTracerProvider classes (#2363) * Add _NoOpTracer and _NoOpTracerProvider classes Add `_NoOpTracer` and `_NoOpTracerProvider`. Marking `_DefaultTracer` and `_DefaultTracerProvider` as deprecated. Fixes #2362 * update changelog * update class * fix lint --- CHANGELOG.md | 2 ++ opentelemetry-api/setup.cfg | 2 +- .../src/opentelemetry/trace/__init__.py | 26 ++++++++++++++++--- .../tests/test_implementation.py | 3 +-- opentelemetry-api/tests/trace/test_globals.py | 5 ++-- opentelemetry-api/tests/trace/test_proxy.py | 4 +-- opentelemetry-api/tests/trace/test_tracer.py | 3 +-- 7 files changed, 31 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85fd2794c0..2428346213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2323](https://github.com/open-telemetry/opentelemetry-python/pull/2323)) - Complete metric exporter format and update OTLP exporter ([#2364](https://github.com/open-telemetry/opentelemetry-python/pull/2364)) +- [api] Add `NoOpTracer` and `NoOpTracerProvider`. Marking `_DefaultTracer` and `_DefaultTracerProvider` as deprecated. + ([#2363](https://github.com/open-telemetry/opentelemetry-python/pull/2363)) ## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17 diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 3c10d64481..6a0fa67a70 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -54,7 +54,7 @@ where = src opentelemetry_context = contextvars_context = opentelemetry.context.contextvars_context:ContextVarsRuntimeContext opentelemetry_tracer_provider = - default_tracer_provider = opentelemetry.trace:_DefaultTracerProvider + default_tracer_provider = opentelemetry.trace:NoOpTracerProvider opentelemetry_meter_provider = default_meter_provider = opentelemetry._metrics:_DefaultMeterProvider opentelemetry_propagator = diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 26df821cbc..56927bf664 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -82,6 +82,8 @@ from logging import getLogger from typing import Iterator, Optional, Sequence, cast +from deprecated import deprecated + from opentelemetry import context as context_api from opentelemetry.attributes import BoundedAttributes # type: ignore from opentelemetry.context.context import Context @@ -216,7 +218,7 @@ def get_tracer( """ -class _DefaultTracerProvider(TracerProvider): +class NoOpTracerProvider(TracerProvider): """The default TracerProvider, used when no implementation is available. All operations are no-op. @@ -229,7 +231,15 @@ def get_tracer( schema_url: typing.Optional[str] = None, ) -> "Tracer": # pylint:disable=no-self-use,unused-argument - return _DefaultTracer() + return NoOpTracer() + + +@deprecated(version="1.9.0", reason="You should use NoOpTracerProvider") # type: ignore +class _DefaultTracerProvider(NoOpTracerProvider): + """The default TracerProvider, used when no implementation is available. + + All operations are no-op. + """ class ProxyTracerProvider(TracerProvider): @@ -393,7 +403,7 @@ def __init__( self._instrumenting_library_version = instrumenting_library_version self._schema_url = schema_url self._real_tracer: Optional[Tracer] = None - self._noop_tracer = _DefaultTracer() + self._noop_tracer = NoOpTracer() @property def _tracer(self) -> Tracer: @@ -416,7 +426,7 @@ def start_as_current_span(self, *args, **kwargs) -> Span: # type: ignore return self._tracer.start_as_current_span(*args, **kwargs) # type: ignore -class _DefaultTracer(Tracer): +class NoOpTracer(Tracer): """The default Tracer, used when no Tracer implementation is available. All operations are no-op. @@ -453,6 +463,14 @@ def start_as_current_span( yield INVALID_SPAN +@deprecated(version="1.9.0", reason="You should use NoOpTracer") # type: ignore +class _DefaultTracer(NoOpTracer): + """The default Tracer, used when no Tracer implementation is available. + + All operations are no-op. + """ + + _TRACER_PROVIDER_SET_ONCE = Once() _TRACER_PROVIDER: Optional[TracerProvider] = None _PROXY_TRACER_PROVIDER = ProxyTracerProvider() diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index 1871ea951e..913efbffb3 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -33,8 +33,7 @@ def test_tracer(self): trace.TracerProvider() # type:ignore def test_default_tracer(self): - # pylint: disable=protected-access - tracer_provider = trace._DefaultTracerProvider() + tracer_provider = trace.NoOpTracerProvider() tracer = tracer_provider.get_tracer(__name__) with tracer.start_span("test") as span: self.assertEqual( diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 421b72d65f..0ead559f86 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -83,11 +83,10 @@ def do_concurrently() -> Mock: class TestTracer(unittest.TestCase): def setUp(self): - # pylint: disable=protected-access - self.tracer = trace._DefaultTracer() + self.tracer = trace.NoOpTracer() def test_get_current_span(self): - """_DefaultTracer's start_span will also + """NoOpTracer's start_span will also be retrievable via get_current_span """ self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN) diff --git a/opentelemetry-api/tests/trace/test_proxy.py b/opentelemetry-api/tests/trace/test_proxy.py index da1d60c74e..b361540b9d 100644 --- a/opentelemetry-api/tests/trace/test_proxy.py +++ b/opentelemetry-api/tests/trace/test_proxy.py @@ -21,7 +21,7 @@ from opentelemetry.trace.span import INVALID_SPAN_CONTEXT, NonRecordingSpan -class TestProvider(trace._DefaultTracerProvider): +class TestProvider(trace.NoOpTracerProvider): def get_tracer( self, instrumenting_module_name: str, @@ -31,7 +31,7 @@ def get_tracer( return TestTracer() -class TestTracer(trace._DefaultTracer): +class TestTracer(trace.NoOpTracer): def start_span(self, *args, **kwargs): return TestSpan(INVALID_SPAN_CONTEXT) diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py index 382aeec743..774746d41a 100644 --- a/opentelemetry-api/tests/trace/test_tracer.py +++ b/opentelemetry-api/tests/trace/test_tracer.py @@ -19,8 +19,7 @@ class TestTracer(unittest.TestCase): def setUp(self): - # pylint: disable=protected-access - self.tracer = trace._DefaultTracer() + self.tracer = trace.NoOpTracer() def test_start_span(self): with self.tracer.start_span("") as span: From 06aa56489b646a8b20f05944d1839d1eb7143160 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 18 Jan 2022 08:34:02 -0800 Subject: [PATCH 1095/1517] [exporter/otlp] add support for Sum (#2370) * [exporter/otlp] add support for Sum Adding support for exporting Sum datapoints. * update based on feedback * update changelog * clean up imports * fix lint * fix lint * feedback update --- CHANGELOG.md | 2 + .../otlp/proto/grpc/_log_exporter/__init__.py | 4 +- .../proto/grpc/_metric_exporter/__init__.py | 58 +++++- .../exporter/otlp/proto/grpc/exporter.py | 11 +- .../proto/grpc/trace_exporter/__init__.py | 4 +- .../metrics/test_otlp_metrics_exporter.py | 192 ++++++++++++++++-- .../src/opentelemetry/sdk/_metrics/point.py | 1 - 7 files changed, 231 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2428346213..c674113cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2364](https://github.com/open-telemetry/opentelemetry-python/pull/2364)) - [api] Add `NoOpTracer` and `NoOpTracerProvider`. Marking `_DefaultTracer` and `_DefaultTracerProvider` as deprecated. ([#2363](https://github.com/open-telemetry/opentelemetry-python/pull/2363)) +- [exporter/opentelemetry-exporter-otlp-proto-grpc] Add Sum to OTLPMetricExporter + ([#2370](https://github.com/open-telemetry/opentelemetry-python/pull/2370)) ## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index db4ce72d26..96a5d76963 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -147,7 +147,9 @@ def _translate_data( self._translate_trace_flags(log_data) self._translate_body(log_data) self._translate_severity_text(log_data) - self._translate_attributes(log_data.log_record.attributes) + self._collector_kwargs["attributes"] = self._translate_attributes( + log_data.log_record.attributes + ) self._collector_kwargs[ "severity_number" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py index d31aebfb3f..75e5cfbed9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging from os import environ from typing import Optional, Sequence from grpc import ChannelCredentials, Compression @@ -25,16 +26,15 @@ MetricsServiceStub, ) from opentelemetry.proto.common.v1.common_pb2 import InstrumentationLibrary -from opentelemetry.proto.metrics.v1.metrics_pb2 import ( - InstrumentationLibraryMetrics, - ResourceMetrics, -) -from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric as PB2Metric +from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_INSECURE, ) from opentelemetry.sdk._metrics.point import ( + Gauge, + Histogram, Metric, + Sum, ) from opentelemetry.sdk._metrics.export import ( @@ -42,6 +42,8 @@ MetricExportResult, ) +logger = logging.getLogger(__name__) + class OTLPMetricExporter( MetricExporter, @@ -80,7 +82,6 @@ def _translate_data( self, data: Sequence[Metric] ) -> ExportMetricsServiceRequest: sdk_resource_instrumentation_library_metrics = {} - self._collector_metric_kwargs = {} for metric in data: resource = metric.resource @@ -100,7 +101,7 @@ def _translate_data( if metric.instrumentation_info is not None: instrumentation_library_map[ metric.instrumentation_info - ] = InstrumentationLibraryMetrics( + ] = pb2.InstrumentationLibraryMetrics( instrumentation_library=InstrumentationLibrary( name=metric.instrumentation_info.name, version=metric.instrumentation_info.version, @@ -109,19 +110,56 @@ def _translate_data( else: instrumentation_library_map[ metric.instrumentation_info - ] = InstrumentationLibraryMetrics() + ] = pb2.InstrumentationLibraryMetrics() instrumentation_library_metrics = instrumentation_library_map.get( metric.instrumentation_info ) + pbmetric = pb2.Metric( + name=metric.name, + description=metric.description, + unit=metric.unit, + ) + if isinstance(metric.point, Gauge): + # TODO: implement gauge + pbmetric.gauge = pb2.Gauge( + data_points=[], + ) + elif isinstance(metric.point, Histogram): + # TODO: implement histogram + pbmetric.histogram = pb2.Histogram( + data_points=[], + ) + elif isinstance(metric.point, Sum): + pt = pb2.NumberDataPoint( + attributes=self._translate_attributes(metric.attributes), + start_time_unix_nano=metric.point.start_time_unix_nano, + time_unix_nano=metric.point.time_unix_nano, + ) + if isinstance(metric.point.value, int): + pt.as_int = metric.point.value + else: + pt.as_double = metric.point.value + # note that because sum is a message type, the fields must be + # set individually rather than instantiating a pb2.Sum and setting + # it once + pbmetric.sum.aggregation_temporality = ( + metric.point.aggregation_temporality + ) + pbmetric.sum.is_monotonic = metric.point.is_monotonic + pbmetric.sum.data_points.append(pt) + else: + logger.warn("unsupported datapoint type %s", metric.point) + continue + instrumentation_library_metrics.metrics.append( - PB2Metric(**self._collector_metric_kwargs) + pbmetric, ) return ExportMetricsServiceRequest( resource_metrics=get_resource_data( sdk_resource_instrumentation_library_metrics, - ResourceMetrics, + pb2.ResourceMetrics, "metrics", ) ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index cbde41bb6c..5c098e8fe0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -270,19 +270,16 @@ def _translate_data( ) -> ExportServiceRequestT: pass - def _translate_attributes(self, attributes) -> None: + def _translate_attributes(self, attributes) -> TypingSequence[KeyValue]: + output = [] if attributes: - self._collector_kwargs["attributes"] = [] - for key, value in attributes.items(): - try: - self._collector_kwargs["attributes"].append( - _translate_key_values(key, value) - ) + output.append(_translate_key_values(key, value)) except Exception as error: # pylint: disable=broad-except logger.exception(error) + return output def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 18f9ad997b..0656e309f4 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -270,7 +270,9 @@ def _translate_data( self._translate_trace_id(sdk_span) self._translate_parent(sdk_span) self._translate_context_trace_state(sdk_span) - self._translate_attributes(sdk_span.attributes) + self._collector_kwargs["attributes"] = self._translate_attributes( + sdk_span.attributes + ) self._translate_events(sdk_span) self._translate_links(sdk_span) self._translate_status(sdk_span) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index 82da7a0e98..9cd1f6d18f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from collections import OrderedDict from concurrent.futures import ThreadPoolExecutor from unittest import TestCase from unittest.mock import patch @@ -20,16 +21,35 @@ from google.rpc.error_details_pb2 import RetryInfo from grpc import StatusCode, server +from opentelemetry.attributes import BoundedAttributes from opentelemetry.exporter.otlp.proto.grpc._metric_exporter import ( OTLPMetricExporter, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, ExportMetricsServiceResponse, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( MetricsServiceServicer, add_MetricsServiceServicer_to_server, ) +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue, + InstrumentationLibrary, + KeyValue, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + InstrumentationLibraryMetrics, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric as OTLPMetric +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + NumberDataPoint as OTLPNumberDataPoint, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ResourceMetrics +from opentelemetry.proto.metrics.v1.metrics_pb2 import Sum as OTLPSum +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as OTLPResource, +) from opentelemetry.sdk._metrics.export import MetricExportResult from opentelemetry.sdk._metrics.point import ( AggregationTemporality, @@ -89,6 +109,33 @@ def Export(self, request, context): return ExportMetricsServiceResponse() +def _generate_metric(name, point) -> Metric: + return Metric( + resource=SDKResource(OrderedDict([("a", 1), ("b", False)])), + instrumentation_info=InstrumentationInfo( + "first_name", "first_version" + ), + attributes=BoundedAttributes(attributes={"a": 1, "b": True}), + description="foo", + name=name, + unit="s", + point=point, + ) + + +def _generate_sum(name, val) -> Sum: + return _generate_metric( + name, + Sum( + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + value=val, + ), + ) + + class TestOTLPMetricExporter(TestCase): def setUp(self): @@ -100,23 +147,10 @@ def setUp(self): self.server.start() - self.metric_data_1 = Metric( - resource=SDKResource({"key": "value"}), - instrumentation_info=InstrumentationInfo( - "first_name", "first_version" - ), - attributes={}, - description="foo", - name="foometric", - unit="s", - point=Sum( - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, - start_time_unix_nano=1641946015139533244, - time_unix_nano=1641946016139533244, - value=33, - ), - ) + self.metrics = { + "sum_int": _generate_sum("sum_int", 33), + "sum_float": _generate_sum("sum_float", 2.98), + } def tearDown(self): self.server.stop(None) @@ -232,7 +266,7 @@ def test_unavailable(self, mock_sleep, mock_expo): MetricsServiceServicerUNAVAILABLE(), self.server ) self.assertEqual( - self.exporter.export([self.metric_data_1]), + self.exporter.export([self.metrics["sum_int"]]), MetricExportResult.FAILURE, ) mock_sleep.assert_called_with(1) @@ -247,7 +281,7 @@ def test_unavailable_delay(self, mock_sleep, mock_expo): MetricsServiceServicerUNAVAILABLEDelay(), self.server ) self.assertEqual( - self.exporter.export([self.metric_data_1]), + self.exporter.export([self.metrics["sum_int"]]), MetricExportResult.FAILURE, ) mock_sleep.assert_called_with(4) @@ -257,7 +291,7 @@ def test_success(self): MetricsServiceServicerSUCCESS(), self.server ) self.assertEqual( - self.exporter.export([self.metric_data_1]), + self.exporter.export([self.metrics["sum_int"]]), MetricExportResult.SUCCESS, ) @@ -266,6 +300,122 @@ def test_failure(self): MetricsServiceServicerALREADY_EXISTS(), self.server ) self.assertEqual( - self.exporter.export([self.metric_data_1]), + self.exporter.export([self.metrics["sum_int"]]), MetricExportResult.FAILURE, ) + + def test_translate_sum_int(self): + expected = ExportMetricsServiceRequest( + resource_metrics=[ + ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_metrics=[ + InstrumentationLibraryMetrics( + instrumentation_library=InstrumentationLibrary( + name="first_name", version="first_version" + ), + metrics=[ + OTLPMetric( + name="sum_int", + unit="s", + description="foo", + sum=OTLPSum( + data_points=[ + OTLPNumberDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + as_int=33, + ) + ], + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + ) + ], + ) + ], + ) + ] + ) + # pylint: disable=protected-access + actual = self.exporter._translate_data([self.metrics["sum_int"]]) + self.assertEqual(expected, actual) + + def test_translate_sum_float(self): + expected = ExportMetricsServiceRequest( + resource_metrics=[ + ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_metrics=[ + InstrumentationLibraryMetrics( + instrumentation_library=InstrumentationLibrary( + name="first_name", version="first_version" + ), + metrics=[ + OTLPMetric( + name="sum_float", + unit="s", + description="foo", + sum=OTLPSum( + data_points=[ + OTLPNumberDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + as_double=2.98, + ) + ], + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + ) + ], + ) + ], + ) + ] + ) + # pylint: disable=protected-access + actual = self.exporter._translate_data([self.metrics["sum_float"]]) + self.assertEqual(expected, actual) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py index 64d62c5ba7..bd69257b93 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py @@ -69,7 +69,6 @@ class Metric: name: str resource: Resource unit: str - point: PointT """Contains non-common fields for the given metric""" From a995e179ef3cd68c69e2cdd9ba48093352313bec Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 18 Jan 2022 09:34:09 -0800 Subject: [PATCH 1096/1517] rename _DefaultMeter & _DefaultMeterProvider to NoOp* (#2383) * rename _DefaultMeter & _DefaultMeterProvider to NoOp* * update changelog --- CHANGELOG.md | 76 +++++++++++++++++-- opentelemetry-api/setup.cfg | 2 +- .../src/opentelemetry/_metrics/__init__.py | 6 +- .../tests/metrics/test_instruments.py | 18 ++--- .../tests/metrics/test_meter_provider.py | 14 ++-- .../opentelemetry/sdk/_metrics/__init__.py | 4 +- 6 files changed, 90 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c674113cb1..339c74894a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), @@ -16,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2363](https://github.com/open-telemetry/opentelemetry-python/pull/2363)) - [exporter/opentelemetry-exporter-otlp-proto-grpc] Add Sum to OTLPMetricExporter ([#2370](https://github.com/open-telemetry/opentelemetry-python/pull/2370)) +- [api] Rename `_DefaultMeter` and `_DefaultMeterProvider` to `NoOpMeter` and `NoOpMeterProvider`. + ([#2383](https://github.com/open-telemetry/opentelemetry-python/pull/2383)) ## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17 @@ -38,7 +41,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11 - - Add support for Python 3.10 ([#2207](https://github.com/open-telemetry/opentelemetry-python/pull/2207)) - remove `X-B3-ParentSpanId` for B3 propagator as per OpenTelemetry specification @@ -110,10 +112,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add name to `BatchSpanProcessor` worker thread ([#2186](https://github.com/open-telemetry/opentelemetry-python/pull/2186)) - ## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 - - Add pre and post instrumentation entry points ([#1983](https://github.com/open-telemetry/opentelemetry-python/pull/1983)) - Fix documentation on well known exporters and variable OTEL_TRACES_EXPORTER which were misnamed @@ -141,12 +141,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.23.1](https://github.com/open-telemetry/opentelemetry-python/pull/1987) - 2021-07-26 ### Changed + - Fix opentelemetry-bootstrap dependency script. ([#1987](https://github.com/open-telemetry/opentelemetry-python/pull/1987)) ## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-21 ### Added + - Moved `opentelemetry-instrumentation` to core repository. ([#1959](https://github.com/open-telemetry/opentelemetry-python/pull/1959)) - Add support for OTLP Exporter Protobuf over HTTP @@ -157,6 +159,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) ### Added + - Give OTLPHandler the ability to process attributes ([#1952](https://github.com/open-telemetry/opentelemetry-python/pull/1952)) - Add global LogEmitterProvider and convenience function get_log_emitter @@ -165,6 +168,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1903](https://github.com/open-telemetry/opentelemetry-python/pull/1903)) ### Changed + - Updated `opentelemetry-opencensus-exporter` to use `service_name` of spans instead of resource ([#1897](https://github.com/open-telemetry/opentelemetry-python/pull/1897)) - Added descriptions to the env variables mentioned in the opentelemetry-specification @@ -187,6 +191,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1946](https://github.com/open-telemetry/opentelemetry-python/pull/1946)) ### Fixed + - Updated `opentelementry-opentracing-shim` `ScopeShim` to report exceptions in opentelemetry specification format, rather than opentracing spec format. ([#1878](https://github.com/open-telemetry/opentelemetry-python/pull/1878)) @@ -194,12 +199,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.3.0-0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01 ### Added + - Allow span limits to be set programatically via TracerProvider. ([#1877](https://github.com/open-telemetry/opentelemetry-python/pull/1877)) - Added support for CreateKey functionality. ([#1853](https://github.com/open-telemetry/opentelemetry-python/pull/1853)) ### Changed + - Updated get_tracer to return an empty string when passed an invalid name ([#1854](https://github.com/open-telemetry/opentelemetry-python/pull/1854)) - Changed AttributeValue sequences to warn mypy users on adding None values to array @@ -214,6 +221,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.2.0, 0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0-0.21b0) - 2021-05-11 ### Added + - Added example for running Django with auto instrumentation. ([#1803](https://github.com/open-telemetry/opentelemetry-python/pull/1803)) - Added `B3SingleFormat` and `B3MultiFormat` propagators to the `opentelemetry-propagator-b3` package. @@ -226,6 +234,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1863](https://github.com/open-telemetry/opentelemetry-python/pull/1863)) ### Changed + - Fixed OTLP gRPC exporter silently failing if scheme is not specified in endpoint. ([#1806](https://github.com/open-telemetry/opentelemetry-python/pull/1806)) - Rename CompositeHTTPPropagator to CompositePropagator as per specification. @@ -253,12 +262,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1856](https://github.com/open-telemetry/opentelemetry-python/pull/1856)) ### Removed + - Moved `opentelemetry-instrumentation` to contrib repository. ([#1797](https://github.com/open-telemetry/opentelemetry-python/pull/1797)) ## [1.1.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.1.0) - 2021-04-20 ### Added + - Added `py.typed` file to every package. This should resolve a bunch of mypy errors for users. ([#1720](https://github.com/open-telemetry/opentelemetry-python/pull/1720)) @@ -273,6 +284,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1799](https://github.com/open-telemetry/opentelemetry-python/pull/1799)) ### Changed + - Adjust `B3Format` propagator to be spec compliant by not modifying context when propagation headers are not present/invalid/empty ([#1728](https://github.com/open-telemetry/opentelemetry-python/pull/1728)) @@ -297,6 +309,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0) - 2021-03-26 ### Added + - Document how to work with fork process web server models(Gunicorn, uWSGI etc...) ([#1609](https://github.com/open-telemetry/opentelemetry-python/pull/1609)) - Add `max_attr_value_length` support to Jaeger exporter @@ -310,6 +323,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1500](https://github.com/open-telemetry/opentelemetry-python/pull/1500)) ### Changed + - remove `service_name` from constructor of jaeger and opencensus exporters and use of env variable `OTEL_PYTHON_SERVICE_NAME` ([#1669])(https://github.com/open-telemetry/opentelemetry-python/pull/1669) @@ -368,6 +382,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1690](https://github.com/open-telemetry/opentelemetry-python/pull/1690)) ### Removed + - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. ([#1675](https://github.com/open-telemetry/opentelemetry-python/pull/1675)) - Remove `OTEL_EXPORTER_*_ INSECURE` env var @@ -378,6 +393,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.19b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.19b0) - 2021-03-26 ### Changed + - remove `service_name` from constructor of jaeger and opencensus exporters and use of env variable `OTEL_PYTHON_SERVICE_NAME` ([#1669])(https://github.com/open-telemetry/opentelemetry-python/pull/1669) @@ -385,18 +401,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1651](https://github.com/open-telemetry/opentelemetry-python/pull/1651)) ### Removed + - Removing support for Python 3.5 ([#1706](https://github.com/open-telemetry/opentelemetry-python/pull/1706)) ## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 ### Added + - Add urllib to opentelemetry-bootstrap target list ([#1584](https://github.com/open-telemetry/opentelemetry-python/pull/1584)) ## [1.0.0rc1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0rc1) - 2021-02-12 ### Changed + - Tracer provider environment variables are now consistent with the rest ([#1571](https://github.com/open-telemetry/opentelemetry-python/pull/1571)) - Rename `TRACE_` to `TRACES_` for environment variables @@ -409,6 +428,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1521](https://github.com/open-telemetry/opentelemetry-python/pull/1521)) ### Added + - Added `end_on_exit` argument to `start_as_current_span` ([#1519](https://github.com/open-telemetry/opentelemetry-python/pull/1519)) - Add `Span.set_attributes` method to set multiple values with one call @@ -419,6 +439,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1545](https://github.com/open-telemetry/opentelemetry-python/pull/1545)) ### Removed + - Remove Configuration ([#1523](https://github.com/open-telemetry/opentelemetry-python/pull/1523)) - Remove Metrics as part of stable, marked as experimental @@ -427,6 +448,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.17b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.17b0) - 2021-01-20 ### Added + - Add support for OTLP v0.6.0 ([#1472](https://github.com/open-telemetry/opentelemetry-python/pull/1472)) - Add protobuf via gRPC exporting support for Jaeger @@ -439,7 +461,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) - Add local/remote samplers to parent based sampler ([#1440](https://github.com/open-telemetry/opentelemetry-python/pull/1440)) -- Add support for OTEL_SPAN_{ATTRIBUTE_COUNT_LIMIT,EVENT_COUNT_LIMIT,LINK_COUNT_LIMIT} +- Add support for OTEL*SPAN*{ATTRIBUTE_COUNT_LIMIT,EVENT_COUNT_LIMIT,LINK_COUNT_LIMIT} ([#1377](https://github.com/open-telemetry/opentelemetry-python/pull/1377)) - Return `None` for `DictGetter` if key not found ([#1449](https://github.com/open-telemetry/opentelemetry-python/pull/1449)) @@ -460,6 +482,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1482](https://github.com/open-telemetry/opentelemetry-python/pull/1482)) ### Changed + - `opentelemetry-exporter-zipkin` Updated zipkin exporter status code and error tag ([#1486](https://github.com/open-telemetry/opentelemetry-python/pull/1486)) - Recreate span on every run of a `start_as_current_span`-decorated function @@ -491,15 +514,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1544](https://github.com/open-telemetry/opentelemetry-python/pull/1544)) ### Removed + - `opentelemetry-api` Remove ThreadLocalRuntimeContext since python3.4 is not supported. ## [0.16b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b1) - 2020-11-26 + ### Added + - Add meter reference to observers ([#1425](https://github.com/open-telemetry/opentelemetry-python/pull/1425)) ## [0.16b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b0) - 2020-11-25 + ### Added + - Add optional parameter to `record_exception` method ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) - Add pickle support to SpanContext class @@ -514,7 +542,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1404](https://github.com/open-telemetry/opentelemetry-python/pull/1404)) - Added support for `OTEL_EXPORTER` to the `opentelemetry-instrument` command ([#1036](https://github.com/open-telemetry/opentelemetry-python/pull/1036)) + ### Changed + - Change temporality for Counter and UpDownCounter ([#1384](https://github.com/open-telemetry/opentelemetry-python/pull/1384)) - OTLP exporter: Handle error case when no credentials supplied @@ -542,6 +572,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.15b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.15b0) -2020-11-02 ### Added + - Add Env variables in OTLP exporter ([#1101](https://github.com/open-telemetry/opentelemetry-python/pull/1101)) - Add support for Jaeger Span Exporter configuration by environment variables and
@@ -549,6 +580,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1114](https://github.com/open-telemetry/opentelemetry-python/pull/1114)) ### Changed + - Updating status codes to adhere to specs ([#1282](https://github.com/open-telemetry/opentelemetry-python/pull/1282)) - Set initial checkpoint timestamp in aggregators @@ -571,6 +603,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.14b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.14b0) - 2020-10-13 ### Added + - Add optional parameter to `record_exception` method ([#1242](https://github.com/open-telemetry/opentelemetry-python/pull/1242)) - Add support for `OTEL_PROPAGATORS` @@ -585,8 +618,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1105](https://github.com/open-telemetry/opentelemetry-python/pull/1120)) - Adding Resource to MeterRecord ([#1209](https://github.com/open-telemetry/opentelemetry-python/pull/1209)) -s + s + ### Changed + - Store `int`s as `int`s in the global Configuration object ([#1118](https://github.com/open-telemetry/opentelemetry-python/pull/1118)) - Allow for Custom Trace and Span IDs Generation - `IdsGenerator` for TracerProvider @@ -599,7 +634,7 @@ s ([#1146](https://github.com/open-telemetry/opentelemetry-python/pull/1146)) - Update OpenTelemetry protos to v0.5.0 ([#1143](https://github.com/open-telemetry/opentelemetry-python/pull/1143)) -- Zipkin exporter now accepts a ``max_tag_value_length`` attribute to customize the +- Zipkin exporter now accepts a `max_tag_value_length` attribute to customize the maximum allowed size a tag value can have. ([#1151](https://github.com/open-telemetry/opentelemetry-python/pull/1151)) - Fixed OTLP events to Zipkin annotations translation. @@ -620,6 +655,7 @@ s ## [0.13b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.13b0) - 2020-09-17 ### Added + - Add instrumentation info to exported spans ([#1095](https://github.com/open-telemetry/opentelemetry-python/pull/1095)) - Add metric OTLP exporter @@ -643,6 +679,7 @@ s ([#1053](https://github.com/open-telemetry/opentelemetry-python/pull/1053)) ### Changed + - Refactor `SpanContext.is_valid` from a method to a data attribute ([#1005](https://github.com/open-telemetry/opentelemetry-python/pull/1005)) - Moved samplers from API to SDK @@ -670,16 +707,19 @@ s ([#1096](https://github.com/open-telemetry/opentelemetry-python/pull/1096)) ### Removed + - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) ## [0.12b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.12.0) - 2020-08-14 ### Added + - Implement Views in metrics SDK ([#596](https://github.com/open-telemetry/opentelemetry-python/pull/596)) ### Changed + - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - Stop TracerProvider and MeterProvider from being overridden @@ -706,9 +746,12 @@ s ## [0.11b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.11.0) - 2020-07-28 ### Added + - Add support for resources and resource detector ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) + ### Changed + - Return INVALID_SPAN if no TracerProvider set for get_current_span ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename record_error to record_exception @@ -719,6 +762,7 @@ s ## [0.10b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.10.0) - 2020-06-23 ### Changed + - Regenerate proto code and add pyi stubs ([#823](https://github.com/open-telemetry/opentelemetry-python/pull/823)) - Rename CounterAggregator -> SumAggregator @@ -727,6 +771,7 @@ s ## [0.9b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.9.0) - 2020-06-10 ### Added + - Adding trace.get_current_span, Removing Tracer.get_current_span ([#552](https://github.com/open-telemetry/opentelemetry-python/pull/552)) - Add SumObserver, UpDownSumObserver and LastValueAggregator in metrics @@ -734,7 +779,9 @@ s - Add start_pipeline to MeterProvider ([#791](https://github.com/open-telemetry/opentelemetry-python/pull/791)) - Initial release of opentelemetry-ext-otlp, opentelemetry-proto + ### Changed + - Move stateful & resource from Meter to MeterProvider ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics @@ -751,10 +798,12 @@ s ## [0.8b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.8.0) - 2020-05-27 ### Added + - Add a new bootstrap command that enables automatic instrument installations. ([#650](https://github.com/open-telemetry/opentelemetry-python/pull/650)) ### Changed + - Handle boolean, integer and float values in Configuration ([#662](https://github.com/open-telemetry/opentelemetry-python/pull/662)) - bugfix: ensure status is always string @@ -783,12 +832,14 @@ s ## [0.7b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.7.1) - 2020-05-12 ### Added + - Add reset for the global configuration object, for testing purposes ([#636](https://github.com/open-telemetry/opentelemetry-python/pull/636)) - Add support for programmatic instrumentation ([#579](https://github.com/open-telemetry/opentelemetry-python/pull/569)) ### Changed + - tracer.get_tracer now optionally accepts a TracerProvider ([#602](https://github.com/open-telemetry/opentelemetry-python/pull/602)) - Configuration object can now be used by any component of opentelemetry, @@ -826,6 +877,7 @@ s ## [0.6b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.6.0) - 2020-03-30 ### Added + - Add support for lazy events and links ([#474](https://github.com/open-telemetry/opentelemetry-python/pull/474)) - Adding is_remote flag to SpanContext, indicating when a span is remote @@ -835,6 +887,7 @@ s - Initial release: opentelemetry-instrumentation ### Changed + - Metrics API no longer uses LabelSet ([#527](https://github.com/open-telemetry/opentelemetry-python/pull/527)) - Allow digit as first char in vendor specific trace state key @@ -845,6 +898,7 @@ s ## [0.5b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.5.0) - 2020-03-16 ### Added + - Adding Correlation Context API/SDK and propagator ([#471](https://github.com/open-telemetry/opentelemetry-python/pull/471)) - Adding a global configuration module to simplify setting and getting globals @@ -858,6 +912,7 @@ s - Initial release opentelemetry-ext-otcollector ### Changed + - Rename metric handle to bound metric instrument ([#470](https://github.com/open-telemetry/opentelemetry-python/pull/470)) - Moving resources to sdk @@ -878,6 +933,7 @@ s ## [0.4a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.4.0) - 2020-02-21 ### Added + - Added named Tracers ([#301](https://github.com/open-telemetry/opentelemetry-python/pull/301)) - Add int and valid sequenced to AttributeValue type @@ -899,6 +955,7 @@ s - Initial release opentelemetry-ext-zipkin, opentelemetry-ext-prometheus ### Changed + - Separate Default classes from interface descriptions ([#311](https://github.com/open-telemetry/opentelemetry-python/pull/311)) - Export span status @@ -924,28 +981,33 @@ s ([#238](https://github.com/open-telemetry/opentelemetry-python/pull/238)) ### Removed + - Remove monotonic and absolute metric instruments ([#410](https://github.com/open-telemetry/opentelemetry-python/pull/410)) ## [0.3a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.3.0) - 2019-12-11 ### Added + - Add metrics exporters ([#192](https://github.com/open-telemetry/opentelemetry-python/pull/192)) - Implement extract and inject support for HTTP_HEADERS and TEXT_MAP formats ([#256](https://github.com/open-telemetry/opentelemetry-python/pull/256)) ### Changed + - Multiple tracing API/SDK changes - Multiple metrics API/SDK changes ### Removed + - Remove option to create unstarted spans from API ([#290](https://github.com/open-telemetry/opentelemetry-python/pull/290)) ## [0.2a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.2.0) - 2019-10-29 ### Added + - W3C TraceContext fixes and compliance tests ([#228](https://github.com/open-telemetry/opentelemetry-python/pull/228)) - Sampler API/SDK @@ -953,6 +1015,7 @@ s - Initial release: opentelemetry-ext-jaeger, opentelemetry-opentracing-shim ### Changed + - Multiple metrics API/SDK changes - Multiple tracing API/SDK changes - Multiple context API changes @@ -961,4 +1024,5 @@ s ## [0.1a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.1.0) - 2019-09-30 ### Added + - Initial release api/sdk diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 6a0fa67a70..95c2092cb9 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -56,7 +56,7 @@ opentelemetry_context = opentelemetry_tracer_provider = default_tracer_provider = opentelemetry.trace:NoOpTracerProvider opentelemetry_meter_provider = - default_meter_provider = opentelemetry._metrics:_DefaultMeterProvider + default_meter_provider = opentelemetry._metrics:NoOpMeterProvider opentelemetry_propagator = tracecontext = opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator baggage = opentelemetry.baggage.propagation:W3CBaggagePropagator diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index 54281de953..4fd4b22988 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -69,7 +69,7 @@ def get_meter( pass -class _DefaultMeterProvider(MeterProvider): +class NoOpMeterProvider(MeterProvider): def get_meter( self, name, @@ -77,7 +77,7 @@ def get_meter( schema_url=None, ) -> "Meter": super().get_meter(name, version=version, schema_url=schema_url) - return _DefaultMeter(name, version=version, schema_url=schema_url) + return NoOpMeter(name, version=version, schema_url=schema_url) class _ProxyMeterProvider(MeterProvider): @@ -346,7 +346,7 @@ def create_observable_up_down_counter( return proxy -class _DefaultMeter(Meter): +class NoOpMeter(Meter): def create_counter(self, name, unit="", description="") -> Counter: super().create_counter(name, unit=unit, description=description) return DefaultCounter(name, unit=unit, description=description) diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index f3a79aec46..b97a1640bd 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -16,7 +16,7 @@ from inspect import Signature, isabstract, signature from unittest import TestCase -from opentelemetry._metrics import Meter, _DefaultMeter +from opentelemetry._metrics import Meter, NoOpMeter from opentelemetry._metrics.instrument import ( Counter, DefaultCounter, @@ -47,7 +47,7 @@ def test_create_counter(self): """ self.assertTrue( - isinstance(_DefaultMeter("name").create_counter("name"), Counter) + isinstance(NoOpMeter("name").create_counter("name"), Counter) ) def test_api_counter_abstract(self): @@ -117,7 +117,7 @@ def callback(): self.assertTrue( isinstance( - _DefaultMeter("name").create_observable_counter( + NoOpMeter("name").create_observable_counter( "name", callback() ), ObservableCounter, @@ -211,9 +211,7 @@ def test_create_histogram(self): """ self.assertTrue( - isinstance( - _DefaultMeter("name").create_histogram("name"), Histogram - ) + isinstance(NoOpMeter("name").create_histogram("name"), Histogram) ) def test_api_histogram_abstract(self): @@ -287,9 +285,7 @@ def callback(): self.assertTrue( isinstance( - _DefaultMeter("name").create_observable_gauge( - "name", callback() - ), + NoOpMeter("name").create_observable_gauge("name", callback()), ObservableGauge, ) ) @@ -379,7 +375,7 @@ def test_create_up_down_counter(self): self.assertTrue( isinstance( - _DefaultMeter("name").create_up_down_counter("name"), + NoOpMeter("name").create_up_down_counter("name"), UpDownCounter, ) ) @@ -464,7 +460,7 @@ def callback(): self.assertTrue( isinstance( - _DefaultMeter("name").create_observable_up_down_counter( + NoOpMeter("name").create_observable_up_down_counter( "name", callback() ), ObservableUpDownCounter, diff --git a/opentelemetry-api/tests/metrics/test_meter_provider.py b/opentelemetry-api/tests/metrics/test_meter_provider.py index e2a1996810..17325c4c03 100644 --- a/opentelemetry-api/tests/metrics/test_meter_provider.py +++ b/opentelemetry-api/tests/metrics/test_meter_provider.py @@ -20,8 +20,8 @@ from opentelemetry import _metrics as metrics from opentelemetry._metrics import ( - _DefaultMeter, - _DefaultMeterProvider, + NoOpMeter, + NoOpMeterProvider, _ProxyMeter, _ProxyMeterProvider, get_meter_provider, @@ -108,7 +108,7 @@ def test_get_meter_parameters(self): Test that get_meter accepts name, version and schema_url """ try: - _DefaultMeterProvider().get_meter( + NoOpMeterProvider().get_meter( "name", version="version", schema_url="schema_url" ) except Exception as error: @@ -125,15 +125,15 @@ def test_invalid_name(self): Test that a message is logged reporting the specified value for the fallback meter is invalid. """ - meter = _DefaultMeterProvider().get_meter("") + meter = NoOpMeterProvider().get_meter("") - self.assertTrue(isinstance(meter, _DefaultMeter)) + self.assertTrue(isinstance(meter, NoOpMeter)) self.assertEqual(meter.name, "") - meter = _DefaultMeterProvider().get_meter(None) + meter = NoOpMeterProvider().get_meter(None) - self.assertTrue(isinstance(meter, _DefaultMeter)) + self.assertTrue(isinstance(meter, NoOpMeter)) self.assertEqual(meter.name, None) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 56b0ad704f..626806cee2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -19,7 +19,7 @@ from opentelemetry._metrics import Meter as APIMeter from opentelemetry._metrics import MeterProvider as APIMeterProvider -from opentelemetry._metrics import _DefaultMeter +from opentelemetry._metrics import NoOpMeter from opentelemetry._metrics.instrument import Counter as APICounter from opentelemetry._metrics.instrument import Histogram as APIHistogram from opentelemetry._metrics.instrument import ( @@ -221,7 +221,7 @@ def get_meter( _logger.warning( "A shutdown `MeterProvider` can not provide a `Meter`" ) - return _DefaultMeter(name, version=version, schema_url=schema_url) + return NoOpMeter(name, version=version, schema_url=schema_url) info = InstrumentationInfo(name, version, schema_url) with self._meter_lock: From 297b67e5d400045a97777872dd60a319ccf9f6c0 Mon Sep 17 00:00:00 2001 From: Ram Thiru Date: Tue, 18 Jan 2022 10:49:15 -0800 Subject: [PATCH 1097/1517] use bookmark to skip folderlist when linking (#2381) Co-authored-by: Leighton Chen --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 63ae6859a9..56aa890df9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +## OpenTelemetry Python ---

@@ -36,8 +37,6 @@ --- -## OpenTelemetry Python - This page describes the Python [OpenTelemetry](https://opentelemetry.io/) implementation. OpenTelemetry is an observability framework for cloud-native software. ## Requirements From 7d076cc682b8294ebd218c4975c5654760238a06 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Tue, 18 Jan 2022 16:11:50 -0500 Subject: [PATCH 1098/1517] Fix a few ExplicitBucketHistogram issues (#2377) * Fix a few ExplicitBucketHistogram issues * make default boundaries floats * Update helper func name Co-authored-by: Diego Hurtado --- .../opentelemetry/sdk/_metrics/aggregation.py | 38 ++++++++++--------- .../tests/metrics/test_aggregation.py | 16 ++++++-- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index db0b4563dd..18b43d7f72 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -17,7 +17,7 @@ from logging import getLogger from math import inf from threading import Lock -from typing import Generic, Optional, Sequence, TypeVar +from typing import Generic, List, Optional, Sequence, TypeVar from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import ( @@ -141,30 +141,32 @@ def collect(self) -> Optional[Gauge]: class ExplicitBucketHistogramAggregation(Aggregation[Histogram]): def __init__( self, - boundaries: Sequence[int] = ( - 0, - 5, - 10, - 25, - 50, - 75, - 100, - 250, - 500, - 1000, + boundaries: Sequence[float] = ( + 0.0, + 5.0, + 10.0, + 25.0, + 50.0, + 75.0, + 100.0, + 250.0, + 500.0, + 1000.0, ), record_min_max: bool = True, ): super().__init__() - # pylint: disable=unnecessary-comprehension - self._boundaries = [boundary for boundary in (*boundaries, inf)] - self.value = [0 for _ in range(len(self._boundaries))] + self._boundaries = tuple(boundaries) + self._bucket_counts = self._get_empty_bucket_counts() self._min = inf self._max = -inf self._sum = 0 self._record_min_max = record_min_max self._start_time_unix_nano = _time_ns() + def _get_empty_bucket_counts(self) -> List[int]: + return [0] * (len(self._boundaries) + 1) + def aggregate(self, measurement: Measurement) -> None: value = measurement.value @@ -175,7 +177,7 @@ def aggregate(self, measurement: Measurement) -> None: self._sum += value - self.value[bisect_left(self._boundaries, value)] += 1 + self._bucket_counts[bisect_left(self._boundaries, value)] += 1 def collect(self) -> Optional[Histogram]: """ @@ -184,10 +186,10 @@ def collect(self) -> Optional[Histogram]: now = _time_ns() with self._lock: - value = self.value + value = self._bucket_counts start_time_unix_nano = self._start_time_unix_nano - self.value = [0 for _ in range(len(self._boundaries))] + self._bucket_counts = self._get_empty_bucket_counts() self._start_time_unix_nano = now + 1 return Histogram( diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index bc8858b474..6ca443e7a9 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -232,16 +232,24 @@ def test_aggregate(self): explicit_bucket_histogram_aggregation.aggregate(Measurement(5)) # The first bucket keeps count of values between (-inf, 0] (-1 and 0) - self.assertEqual(explicit_bucket_histogram_aggregation.value[0], 2) + self.assertEqual( + explicit_bucket_histogram_aggregation._bucket_counts[0], 2 + ) # The second bucket keeps count of values between (0, 2] (1 and 2) - self.assertEqual(explicit_bucket_histogram_aggregation.value[1], 2) + self.assertEqual( + explicit_bucket_histogram_aggregation._bucket_counts[1], 2 + ) # The third bucket keeps count of values between (2, 4] (3 and 4) - self.assertEqual(explicit_bucket_histogram_aggregation.value[2], 2) + self.assertEqual( + explicit_bucket_histogram_aggregation._bucket_counts[2], 2 + ) # The fourth bucket keeps count of values between (4, inf) (3 and 4) - self.assertEqual(explicit_bucket_histogram_aggregation.value[3], 1) + self.assertEqual( + explicit_bucket_histogram_aggregation._bucket_counts[3], 1 + ) def test_min_max(self): """ From fb605388dda13933b2242a208e0039518eec01ce Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Wed, 19 Jan 2022 12:07:16 -0500 Subject: [PATCH 1099/1517] Implement SynchronousMeasurementConsumer (#2388) --- .../opentelemetry/sdk/_metrics/__init__.py | 16 ++- .../sdk/_metrics/measurement_consumer.py | 29 ++++- .../sdk/_metrics/metric_reader_storage.py | 34 ++++++ .../sdk/_metrics/sdk_configuration.py | 13 ++ .../metrics/test_measurement_consumer.py | 113 ++++++++---------- .../tests/metrics/test_metrics.py | 88 ++++++++++++-- 6 files changed, 212 insertions(+), 81 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 626806cee2..57c44160c8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -45,6 +45,7 @@ SynchronousMeasurementConsumer, ) from opentelemetry.sdk._metrics.metric_reader import MetricReader +from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -156,8 +157,12 @@ def __init__( self._lock = Lock() self._meter_lock = Lock() self._atexit_handler = None - - self._measurement_consumer = SynchronousMeasurementConsumer() + self._sdk_config = SdkConfiguration( + resource=resource, metric_readers=metric_readers + ) + self._measurement_consumer = SynchronousMeasurementConsumer( + sdk_config=self._sdk_config + ) if shutdown_on_exit: self._atexit_handler = register(self.shutdown) @@ -165,10 +170,9 @@ def __init__( self._meters = {} self._metric_readers = metric_readers - for metric_reader in self._metric_readers: + for metric_reader in self._sdk_config.metric_readers: metric_reader._register_measurement_consumer(self) - self._resource = resource self._shutdown = False def force_flush(self) -> bool: @@ -177,7 +181,7 @@ def force_flush(self) -> bool: metric_reader_result = True - for metric_reader in self._metric_readers: + for metric_reader in self._sdk_config.metric_readers: metric_reader_result = ( metric_reader_result and metric_reader.force_flush() ) @@ -196,7 +200,7 @@ def shutdown(self): result = True - for metric_reader in self._metric_readers: + for metric_reader in self._sdk_config.metric_readers: result = result and metric_reader.shutdown() if not result: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py index d8c75139cf..b602185bc9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py @@ -13,12 +13,17 @@ # limitations under the License. from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Iterable +from threading import Lock +from typing import TYPE_CHECKING, Iterable, List, Mapping from opentelemetry.sdk._metrics.aggregation import AggregationTemporality from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.metric_reader import MetricReader +from opentelemetry.sdk._metrics.metric_reader_storage import ( + MetricReaderStorage, +) from opentelemetry.sdk._metrics.point import Metric +from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration if TYPE_CHECKING: from opentelemetry.sdk._metrics.instrument import _Asynchronous @@ -41,15 +46,31 @@ def collect( class SynchronousMeasurementConsumer(MeasurementConsumer): + def __init__(self, sdk_config: SdkConfiguration) -> None: + self._lock = Lock() + self._sdk_config = sdk_config + # should never be mutated + self._reader_storages: Mapping[MetricReader, MetricReaderStorage] = { + reader: MetricReaderStorage(sdk_config) + for reader in sdk_config.metric_readers + } + self._async_instruments: List["_Asynchronous"] = [] + def consume_measurement(self, measurement: Measurement) -> None: - pass + for reader_storage in self._reader_storages.values(): + reader_storage.consume_measurement(measurement) def register_asynchronous_instrument( self, instrument: "_Asynchronous" ) -> None: - pass + with self._lock: + self._async_instruments.append(instrument) def collect( self, metric_reader: MetricReader, temporality: AggregationTemporality ) -> Iterable[Metric]: - pass + with self._lock: + for async_instrument in self._async_instruments: + for measurement in async_instrument.callback(): + self.consume_measurement(measurement) + return self._reader_storages[metric_reader].collect(temporality) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py new file mode 100644 index 0000000000..ffb204f447 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py @@ -0,0 +1,34 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Iterable + +from opentelemetry.sdk._metrics.aggregation import AggregationTemporality +from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk._metrics.point import Metric +from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration + + +# TODO: #2378 +class MetricReaderStorage: + """The SDK's storage for a given reader""" + + def __init__(self, sdk_config: SdkConfiguration) -> None: + pass + + def consume_measurement(self, measurement: Measurement) -> None: + pass + + def collect(self, temporality: AggregationTemporality) -> Iterable[Metric]: + pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py new file mode 100644 index 0000000000..3b077bdea1 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass +from typing import Sequence + +from opentelemetry.sdk._metrics.metric_reader import MetricReader +from opentelemetry.sdk.resources import Resource + + +@dataclass +class SdkConfiguration: + resource: Resource + # TODO: once views are added + # views: Sequence[View] + metric_readers: Sequence[MetricReader] diff --git a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py index f1a96cadc1..0b7072f9e4 100644 --- a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py +++ b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py @@ -13,87 +13,78 @@ # limitations under the License. from unittest import TestCase -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch -from opentelemetry.sdk._metrics import MeterProvider from opentelemetry.sdk._metrics.measurement_consumer import ( MeasurementConsumer, SynchronousMeasurementConsumer, ) +from opentelemetry.sdk._metrics.point import AggregationTemporality +from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration +@patch("opentelemetry.sdk._metrics.measurement_consumer.MetricReaderStorage") class TestSynchronousMeasurementConsumer(TestCase): - def test_parent(self): + def test_parent(self, _): self.assertIsInstance( - SynchronousMeasurementConsumer(), MeasurementConsumer + SynchronousMeasurementConsumer(MagicMock()), MeasurementConsumer ) - @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") - def test_measurement_consumer_class( - self, mock_serial_measurement_consumer - ): - MeterProvider() - - mock_serial_measurement_consumer.assert_called() - - @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") - def test_register_asynchronous_instrument( - self, mock_serial_measurement_consumer - ): - - meter_provider = MeterProvider() - - meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( - meter_provider.get_meter("name").create_observable_counter( - "name", Mock() - ) - ) - meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( - meter_provider.get_meter("name").create_observable_up_down_counter( - "name", Mock() - ) - ) - meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( - meter_provider.get_meter("name").create_observable_gauge( - "name", Mock() - ) + def test_creates_metric_reader_storages(self, MockMetricReaderStorage): + """It should create one MetricReaderStorage per metric reader passed in the SdkConfiguration""" + reader_mocks = [Mock() for _ in range(5)] + SynchronousMeasurementConsumer( + SdkConfiguration(resource=Mock(), metric_readers=reader_mocks) ) + self.assertEqual(len(MockMetricReaderStorage.mock_calls), 5) - @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") - def test_consume_measurement_counter( - self, mock_serial_measurement_consumer + def test_measurements_passed_to_each_reader_storage( + self, MockMetricReaderStorage ): + reader_mocks = [Mock() for _ in range(5)] + reader_storage_mocks = [Mock() for _ in range(5)] + MockMetricReaderStorage.side_effect = reader_storage_mocks - meter_provider = MeterProvider() - counter = meter_provider.get_meter("name").create_counter("name") - - counter.add(1) - - meter_provider._measurement_consumer.consume_measurement.assert_called() - - @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") - def test_consume_measurement_up_down_counter( - self, mock_serial_measurement_consumer - ): - - meter_provider = MeterProvider() - counter = meter_provider.get_meter("name").create_up_down_counter( - "name" + consumer = SynchronousMeasurementConsumer( + SdkConfiguration(resource=Mock(), metric_readers=reader_mocks) ) + measurement_mock = Mock() + consumer.consume_measurement(measurement_mock) - counter.add(1) + for rs_mock in reader_storage_mocks: + rs_mock.consume_measurement.assert_called_once_with( + measurement_mock + ) - meter_provider._measurement_consumer.consume_measurement.assert_called() + def test_collect_passed_to_reader_stage(self, MockMetricReaderStorage): + """Its collect() method should defer to the underlying MetricReaderStorage""" + reader_mocks = [Mock() for _ in range(5)] + reader_storage_mocks = [Mock() for _ in range(5)] + MockMetricReaderStorage.side_effect = reader_storage_mocks - @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") - def test_consume_measurement_histogram( - self, mock_serial_measurement_consumer - ): + consumer = SynchronousMeasurementConsumer( + SdkConfiguration(resource=Mock(), metric_readers=reader_mocks) + ) + for r_mock, rs_mock in zip(reader_mocks, reader_storage_mocks): + rs_mock.collect.assert_not_called() + consumer.collect(r_mock, AggregationTemporality.CUMULATIVE) + rs_mock.collect.assert_called_once_with( + AggregationTemporality.CUMULATIVE + ) - meter_provider = MeterProvider() - counter = meter_provider.get_meter("name").create_histogram("name") + def test_collect_calls_async_instruments(self, _): + """Its collect() method should invoke async instruments""" + reader_mock = Mock() + consumer = SynchronousMeasurementConsumer( + SdkConfiguration(resource=Mock(), metric_readers=[reader_mock]) + ) + async_instrument_mocks = [MagicMock() for _ in range(5)] + for i_mock in async_instrument_mocks: + consumer.register_asynchronous_instrument(i_mock) - counter.record(1) + consumer.collect(reader_mock, AggregationTemporality.CUMULATIVE) - meter_provider._measurement_consumer.consume_measurement.assert_called() + # it should call async instruments + for i_mock in async_instrument_mocks: + i_mock.callback.assert_called_once() diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 2071416702..bfe1d0df9d 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -15,7 +15,7 @@ from logging import WARNING from unittest import TestCase -from unittest.mock import Mock +from unittest.mock import Mock, patch from opentelemetry.sdk._metrics import Meter, MeterProvider from opentelemetry.sdk._metrics.instrument import ( @@ -26,14 +26,11 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk._metrics.measurement_consumer import ( - SynchronousMeasurementConsumer, -) from opentelemetry.sdk.resources import Resource class TestMeterProvider(TestCase): - def test_meter_provider_resource(self): + def test_resource(self): """ `MeterProvider` provides a way to allow a `Resource` to be specified. """ @@ -41,12 +38,17 @@ def test_meter_provider_resource(self): meter_provider_0 = MeterProvider() meter_provider_1 = MeterProvider() - self.assertIs(meter_provider_0._resource, meter_provider_1._resource) - self.assertIsInstance(meter_provider_0._resource, Resource) - self.assertIsInstance(meter_provider_1._resource, Resource) + self.assertIs( + meter_provider_0._sdk_config.resource, + meter_provider_1._sdk_config.resource, + ) + self.assertIsInstance(meter_provider_0._sdk_config.resource, Resource) + self.assertIsInstance(meter_provider_1._sdk_config.resource, Resource) resource = Resource({"key": "value"}) - self.assertIs(MeterProvider(resource=resource)._resource, resource) + self.assertIs( + MeterProvider(resource=resource)._sdk_config.resource, resource + ) def test_get_meter(self): """ @@ -103,10 +105,76 @@ def test_shutdown_subsequent_calls(self): with self.assertLogs(level=WARNING): meter_provider.shutdown() + @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + def test_creates_sync_measurement_consumer( + self, mock_sync_measurement_consumer + ): + MeterProvider() + mock_sync_measurement_consumer.assert_called() + + @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + def test_register_asynchronous_instrument( + self, mock_sync_measurement_consumer + ): + + meter_provider = MeterProvider() + + meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( + meter_provider.get_meter("name").create_observable_counter( + "name", Mock() + ) + ) + meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( + meter_provider.get_meter("name").create_observable_up_down_counter( + "name", Mock() + ) + ) + meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( + meter_provider.get_meter("name").create_observable_gauge( + "name", Mock() + ) + ) + + @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + def test_consume_measurement_counter(self, mock_sync_measurement_consumer): + sync_consumer_instance = mock_sync_measurement_consumer() + meter_provider = MeterProvider() + counter = meter_provider.get_meter("name").create_counter("name") + + counter.add(1) + + sync_consumer_instance.consume_measurement.assert_called() + + @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + def test_consume_measurement_up_down_counter( + self, mock_sync_measurement_consumer + ): + sync_consumer_instance = mock_sync_measurement_consumer() + meter_provider = MeterProvider() + counter = meter_provider.get_meter("name").create_up_down_counter( + "name" + ) + + counter.add(1) + + sync_consumer_instance.consume_measurement.assert_called() + + @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + def test_consume_measurement_histogram( + self, mock_sync_measurement_consumer + ): + sync_consumer_instance = mock_sync_measurement_consumer() + meter_provider = MeterProvider() + counter = meter_provider.get_meter("name").create_histogram("name") + + counter.record(1) + + sync_consumer_instance.consume_measurement.assert_called() + class TestMeter(TestCase): def setUp(self): - self.meter = Meter(Mock(), SynchronousMeasurementConsumer()) + self.meter = Meter(Mock(), Mock()) def test_create_counter(self): counter = self.meter.create_counter( From 6bdc5fd20ddf4394eb4630d8bb6847f7e399a817 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 20 Jan 2022 12:58:45 -0800 Subject: [PATCH 1100/1517] Validate add/record operations for instruments (#2394) --- .../opentelemetry/sdk/_metrics/instrument.py | 19 ++++++-- .../tests/metrics/test_instrument.py | 45 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index 3b2db18835..d9da9a067a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -14,6 +14,7 @@ # pylint: disable=too-many-ancestors +import logging from typing import Dict, Generator, Iterable, Union from opentelemetry._metrics.instrument import CallbackT @@ -33,6 +34,8 @@ from opentelemetry.sdk._metrics.measurement_consumer import MeasurementConsumer from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +_logger = logging.getLogger(__name__) + class _Synchronous: def __init__( @@ -87,8 +90,10 @@ def add( self, amount: Union[int, float], attributes: Dict[str, str] = None ): if amount < 0: - raise Exception("amount must be non negative") - + _logger.warning( + "Add amount must be non-negative on Counter %s.", self.name + ) + return self._measurement_consumer.consume_measurement( Measurement(amount, attributes) ) @@ -112,7 +117,15 @@ class ObservableUpDownCounter(_Asynchronous, APIObservableUpDownCounter): class Histogram(_Synchronous, APIHistogram): - def record(self, amount, attributes=None): + def record( + self, amount: Union[int, float], attributes: Dict[str, str] = None + ): + if amount < 0: + _logger.warning( + "Record amount must be non-negative on Histogram %s.", + self.name, + ) + return self._measurement_consumer.consume_measurement( Measurement(amount, attributes) ) diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py index 64d20d6862..5504fdb686 100644 --- a/opentelemetry-sdk/tests/metrics/test_instrument.py +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -16,12 +16,43 @@ from unittest.mock import Mock from opentelemetry.sdk._metrics.instrument import ( + Counter, + Histogram, ObservableCounter, ObservableGauge, ObservableUpDownCounter, + UpDownCounter, ) +class TestCounter(TestCase): + def test_add(self): + mc = Mock() + counter = Counter("name", Mock(), mc) + counter.add(1.0) + mc.consume_measurement.assert_called_once() + + def test_add_non_monotonic(self): + mc = Mock() + counter = Counter("name", Mock(), mc) + counter.add(-1.0) + mc.consume_measurement.assert_not_called() + + +class TestUpDownCounter(TestCase): + def test_add(self): + mc = Mock() + counter = UpDownCounter("name", Mock(), mc) + counter.add(1.0) + mc.consume_measurement.assert_called_once() + + def test_add_non_monotonic(self): + mc = Mock() + counter = UpDownCounter("name", Mock(), mc) + counter.add(-1.0) + mc.consume_measurement.assert_called_once() + + class TestObservableGauge(TestCase): def test_callable_callback(self): def callback(): @@ -82,3 +113,17 @@ def callback(): ) self.assertEqual(observable_up_down_counter.callback(), [1, 2, 3]) + + +class TestHistogram(TestCase): + def test_record(self): + mc = Mock() + hist = Histogram("name", Mock(), mc) + hist.record(1.0) + mc.consume_measurement.assert_called_once() + + def test_record_non_monotonic(self): + mc = Mock() + hist = Histogram("name", Mock(), mc) + hist.record(-1.0) + mc.consume_measurement.assert_not_called() From 83e6e17a4756698084895f07556d2043b4b0e7e8 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 21 Jan 2022 22:12:53 +0530 Subject: [PATCH 1101/1517] Add periodic exporting metric reader (#2340) * Add periodic exporting metric reader * Fix lint * Fix typing and env * lint: black format * formatting * lot of changes * Fix lint * Lint again * Pylint fix * Review suggestions * fix failures * Address review suggestions * Once wrapper, set callback, refactor, and more * Apply suggestions from code review Co-authored-by: Aaron Abbott * Add preferred_temporality to exporter; update doc string * Fix lint Co-authored-by: Aaron Abbott --- opentelemetry-sdk/setup.cfg | 1 + .../sdk/_metrics/export/__init__.py | 102 +++++++++++++- .../sdk/_metrics/metric_reader.py | 57 +++++++- .../test_periodic_exporting_metric_reader.py | 126 ++++++++++++++++++ 4 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 09f679e4c4..49ed93c14d 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -47,6 +47,7 @@ install_requires = opentelemetry-semantic-conventions == 0.27b0 setuptools >= 16.0 dataclasses == 0.8; python_version < '3.7' + typing-extensions >= 3.7.4 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py index b5e2f08610..13494347a8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -12,13 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging +import os from abc import ABC, abstractmethod from enum import Enum -from os import linesep +from os import environ, linesep from sys import stdout -from typing import IO, Callable, Sequence +from threading import Event, Thread +from typing import IO, Callable, Iterable, Optional, Sequence -from opentelemetry.sdk._metrics.point import Metric +from opentelemetry.context import ( + _SUPPRESS_INSTRUMENTATION_KEY, + attach, + detach, + set_value, +) +from opentelemetry.sdk._metrics.metric_reader import MetricReader +from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric +from opentelemetry.util._once import Once + +_logger = logging.getLogger(__name__) class MetricExportResult(Enum): @@ -33,6 +46,10 @@ class MetricExporter(ABC): in their own format. """ + @property + def preferred_temporality(self) -> AggregationTemporality: + return AggregationTemporality.CUMULATIVE + @abstractmethod def export(self, metrics: Sequence[Metric]) -> "MetricExportResult": """Exports a batch of telemetry data. @@ -77,3 +94,82 @@ def export(self, metrics: Sequence[Metric]) -> MetricExportResult: def shutdown(self) -> None: pass + + +class PeriodicExportingMetricReader(MetricReader): + """`PeriodicExportingMetricReader` is an implementation of `MetricReader` + that collects metrics based on a user-configurable time interval, and passes the + metrics to the configured exporter. + """ + + def __init__( + self, + exporter: MetricExporter, + export_interval_millis: Optional[float] = None, + export_timeout_millis: Optional[float] = None, + ) -> None: + super().__init__(preferred_temporality=exporter.preferred_temporality) + self._exporter = exporter + if export_interval_millis is None: + try: + export_interval_millis = float( + environ.get("OTEL_METRIC_EXPORT_INTERVAL", 60000) + ) + except ValueError: + _logger.warning( + "Found invalid value for export interval, using default" + ) + export_interval_millis = 60000 + if export_timeout_millis is None: + try: + export_timeout_millis = float( + environ.get("OTEL_METRIC_EXPORT_TIMEOUT", 30000) + ) + except ValueError: + _logger.warning( + "Found invalid value for export timeout, using default" + ) + export_timeout_millis = 30000 + self._export_interval_millis = export_interval_millis + self._export_timeout_millis = export_timeout_millis + self._shutdown = False + self._shutdown_event = Event() + self._shutdown_once = Once() + self._daemon_thread = Thread(target=self._ticker, daemon=True) + self._daemon_thread.start() + if hasattr(os, "register_at_fork"): + os.register_at_fork( + after_in_child=self._at_fork_reinit + ) # pylint: disable=protected-access + + def _at_fork_reinit(self): + self._daemon_thread = Thread(target=self._ticker, daemon=True) + self._daemon_thread.start() + + def _ticker(self) -> None: + interval_secs = self._export_interval_millis / 1e3 + while not self._shutdown_event.wait(interval_secs): + self.collect() + # one last collection below before shutting down completely + self.collect() + + def _receive_metrics(self, metrics: Iterable[Metric]) -> None: + token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) + try: + self._exporter.export(metrics) + except Exception as e: # pylint: disable=broad-except,invalid-name + _logger.exception("Exception while exporting metrics %s", str(e)) + detach(token) + + def shutdown(self) -> bool: + def _shutdown(): + self._shutdown = True + + did_set = self._shutdown_once.do_once(_shutdown) + if not did_set: + _logger.warning("Can't shutdown multiple times") + return False + + self._shutdown_event.set() + self._daemon_thread.join() + return self._exporter.shutdown() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py index 4bbae6501c..951f18e09a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py @@ -12,6 +12,59 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging +from abc import ABC, abstractmethod +from typing import Callable, Iterable -class MetricReader: - pass +from typing_extensions import final + +from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric + +_logger = logging.getLogger(__name__) + + +class MetricReader(ABC): + def __init__( + self, + preferred_temporality: AggregationTemporality = AggregationTemporality.CUMULATIVE, + ) -> None: + self._collect: Callable[ + ["MetricReader", AggregationTemporality], Iterable[Metric] + ] = None + self._preferred_temporality = preferred_temporality + + @final + def collect(self) -> None: + """Collects the metrics from the internal SDK state and + invokes the `_receive_metrics` with the collection. + """ + if self._collect is None: + _logger.warning( + "Cannot call collect on a MetricReader until it is registered on a MeterProvider" + ) + return + self._receive_metrics(self._collect(self, self._preferred_temporality)) + + @final + def _set_collect_callback( + self, + func: Callable[ + ["MetricReader", AggregationTemporality], Iterable[Metric] + ], + ) -> None: + """This function is internal to the SDK. It should not be called or overriden by users""" + self._collect = func + + @abstractmethod + def _receive_metrics(self, metrics: Iterable[Metric]): + """Called by `MetricReader.collect` when it receives a batch of metrics""" + + @abstractmethod + def shutdown(self) -> bool: + """Shuts down the MetricReader. This method provides a way + for the MetricReader to do any cleanup required. A metric reader can + only be shutdown once, any subsequent calls are ignored and return + failure status. + + When a `MetricReader` is registered on a `MeterProvider`, `MeterProvider.shutdown` will invoke this automatically. + """ diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py new file mode 100644 index 0000000000..da09b51b8d --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -0,0 +1,126 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from unittest.mock import Mock + +from opentelemetry.sdk._metrics.export import ( + MetricExporter, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk._metrics.point import Gauge, Metric, Sum +from opentelemetry.sdk.resources import Resource +from opentelemetry.test.concurrency_test import ConcurrencyTestBase +from opentelemetry.util._time import _time_ns + + +class FakeMetricsExporter(MetricExporter): + def __init__(self, wait=0): + self.wait = wait + self.metrics = [] + self._shutdown = False + + def export(self, metrics): + time.sleep(self.wait) + self.metrics.extend(metrics) + return True + + def shutdown(self): + self._shutdown = True + + +metrics_list = [ + Metric( + name="sum_name", + attributes={}, + description="", + instrumentation_info=None, + resource=Resource.create(), + unit="", + point=Sum( + start_time_unix_nano=_time_ns(), + time_unix_nano=_time_ns(), + value=2, + aggregation_temporality=1, + is_monotonic=True, + ), + ), + Metric( + name="gauge_name", + attributes={}, + description="", + instrumentation_info=None, + resource=Resource.create(), + unit="", + point=Gauge( + time_unix_nano=_time_ns(), + value=2, + ), + ), +] + + +class TestPeriodicExportingMetricReader(ConcurrencyTestBase): + def test_defaults(self): + pmr = PeriodicExportingMetricReader(FakeMetricsExporter()) + self.assertEqual(pmr._export_interval_millis, 60000) + self.assertEqual(pmr._export_timeout_millis, 30000) + pmr.shutdown() + + def _create_periodic_reader( + self, metrics, exporter, collect_wait=0, interval=60000 + ): + + pmr = PeriodicExportingMetricReader(exporter, interval) + + def _collect(reader, temp): + time.sleep(collect_wait) + pmr._receive_metrics(metrics) + + pmr._set_collect_callback(_collect) + return pmr + + def test_ticker_called(self): + collect_mock = Mock() + pmr = PeriodicExportingMetricReader(Mock(), 1) + pmr._set_collect_callback(collect_mock) + time.sleep(0.1) + self.assertTrue(collect_mock.assert_called_once) + pmr.shutdown() + + def test_ticker_collects_metrics(self): + exporter = FakeMetricsExporter() + + pmr = self._create_periodic_reader( + metrics_list, exporter, interval=100 + ) + time.sleep(0.2) + self.assertEqual(exporter.metrics, metrics_list) + pmr.shutdown() + + def test_shutdown(self): + exporter = FakeMetricsExporter() + + pmr = self._create_periodic_reader([], exporter) + pmr.shutdown() + self.assertEqual(exporter.metrics, []) + self.assertTrue(pmr._shutdown) + self.assertTrue(exporter._shutdown) + + def test_shutdown_multiple_times(self): + pmr = self._create_periodic_reader([], Mock()) + with self.assertLogs(level="WARNING") as w: + self.run_with_many_threads(pmr.shutdown) + self.assertTrue("Can't shutdown multiple times", w.output[0]) + pmr.shutdown() From 60a3b5236f7167a1a923a4e04bf508259ec283e7 Mon Sep 17 00:00:00 2001 From: Ted Kern Date: Fri, 21 Jan 2022 10:48:49 -0800 Subject: [PATCH 1102/1517] add support for 0 global attribute limit and tests (#2398) * add support for 0 global attribute limit and tests * add PR number to changelog --- CHANGELOG.md | 2 + .../src/opentelemetry/sdk/trace/__init__.py | 16 +++++-- opentelemetry-sdk/tests/trace/test_trace.py | 47 +++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 339c74894a..e0ab38fae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.8.0-0.27b0...HEAD) +- Fix SpanLimits global span limit defaulting when set to 0 + ([#2398](https://github.com/open-telemetry/opentelemetry-python/pull/2398)) - Decode URL-encoded headers in environment variables ([#2312](https://github.com/open-telemetry/opentelemetry-python/pull/2312)) - [exporter/opentelemetry-exporter-otlp-proto-grpc] Add OTLPMetricExporter diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 2418dcada8..c82fcb3927 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -594,23 +594,31 @@ def __init__( max_attributes, OTEL_ATTRIBUTE_COUNT_LIMIT ) self.max_attributes = ( - global_max_attributes or _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT + global_max_attributes + if global_max_attributes is not None + else _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT ) self.max_span_attributes = self._from_env_if_absent( max_span_attributes, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, - global_max_attributes or _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + global_max_attributes + if global_max_attributes is not None + else _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, ) self.max_event_attributes = self._from_env_if_absent( max_event_attributes, OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, - global_max_attributes or _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, + global_max_attributes + if global_max_attributes is not None + else _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, ) self.max_link_attributes = self._from_env_if_absent( max_link_attributes, OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, - global_max_attributes or _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, + global_max_attributes + if global_max_attributes is not None + else _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, ) # attribute length diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 487c537c18..1ccada0e5a 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -1594,6 +1594,53 @@ def test_span_no_limits_code(self): ) ) + def test_span_zero_global_limit(self): + self._test_span_limits( + new_tracer( + span_limits=trace.SpanLimits( + max_attributes=0, + max_events=0, + max_links=0, + ) + ), + 0, + 0, + 0, + 0, + 0, + ) + + def test_span_zero_global_nonzero_model(self): + self._test_span_limits( + new_tracer( + span_limits=trace.SpanLimits( + max_attributes=0, + max_events=0, + max_links=0, + max_span_attributes=15, + max_span_attribute_length=25, + ) + ), + 15, + 0, + 0, + 0, + 25, + ) + + def test_span_zero_global_unset_model(self): + self._test_span_no_limits( + new_tracer( + span_limits=trace.SpanLimits( + max_attributes=0, + max_span_attributes=trace.SpanLimits.UNSET, + max_links=trace.SpanLimits.UNSET, + max_events=trace.SpanLimits.UNSET, + max_attribute_length=trace.SpanLimits.UNSET, + ) + ) + ) + def test_dropped_attributes(self): span = get_span_with_dropped_attributes_events_links() self.assertEqual(1, span.dropped_links) From 67ef457d3a611efb59eee81188ec29ee151c0ffc Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 21 Jan 2022 14:56:40 -0600 Subject: [PATCH 1103/1517] Add Python version support policy (#2397) * Add Python version support policy Fixes #2395 * Address comments * Fix a mistake --- CHANGELOG.md | 2 ++ README.md | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0ab38fae3..80ee736e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix SpanLimits global span limit defaulting when set to 0 ([#2398](https://github.com/open-telemetry/opentelemetry-python/pull/2398)) +- Add Python version support policy + ([#2397](https://github.com/open-telemetry/opentelemetry-python/pull/2397)) - Decode URL-encoded headers in environment variables ([#2312](https://github.com/open-telemetry/opentelemetry-python/pull/2312)) - [exporter/opentelemetry-exporter-otlp-proto-grpc] Add OTLPMetricExporter diff --git a/README.md b/README.md index 56aa890df9..fd2d6334e9 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,14 @@ pip install scalene scalene opentelemetry-/tests/performance/resource-usage//profile_resource_usage_.py ``` +## Python Version Support Addition and Removal + +This project supports the latest Python versions. As new Python versions are released, support for them is added and +as old Python versions reach their end of life, support for them is removed. + +We add support for new Python versions no later than 3 months after they become stable. + +We remove support for old Python versions 6 months after they reach their [end of life](https://devguide.python.org/devcycle/#end-of-life-branches). ## Documentation From f0583e37ebf87e67bce6f344cacc26b540c5698f Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 25 Jan 2022 10:52:53 -0800 Subject: [PATCH 1104/1517] Adding implementation for Gauge (#2408) * Adding implementation for Gauge Fixes #2385 * update changelog * Update exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py Co-authored-by: Diego Hurtado Co-authored-by: Diego Hurtado --- CHANGELOG.md | 2 + .../proto/grpc/_metric_exporter/__init__.py | 11 +- .../metrics/test_otlp_metrics_exporter.py | 153 +++++++++++++++--- 3 files changed, 144 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80ee736e5e..c2034e0f4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2370](https://github.com/open-telemetry/opentelemetry-python/pull/2370)) - [api] Rename `_DefaultMeter` and `_DefaultMeterProvider` to `NoOpMeter` and `NoOpMeterProvider`. ([#2383](https://github.com/open-telemetry/opentelemetry-python/pull/2383)) +- [exporter/opentelemetry-exporter-otlp-proto-grpc] Add Gauge to OTLPMetricExporter + ([#2408](https://github.com/open-telemetry/opentelemetry-python/pull/2408)) ## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py index 75e5cfbed9..46cb5d7426 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py @@ -122,10 +122,15 @@ def _translate_data( unit=metric.unit, ) if isinstance(metric.point, Gauge): - # TODO: implement gauge - pbmetric.gauge = pb2.Gauge( - data_points=[], + pt = pb2.NumberDataPoint( + attributes=self._translate_attributes(metric.attributes), + time_unix_nano=metric.point.time_unix_nano, ) + if isinstance(metric.point.value, int): + pt.as_int = metric.point.value + else: + pt.as_double = metric.point.value + pbmetric.gauge.data_points.append(pt) elif isinstance(metric.point, Histogram): # TODO: implement histogram pbmetric.histogram = pb2.Histogram( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index 9cd1f6d18f..b13fe11882 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -38,21 +38,14 @@ InstrumentationLibrary, KeyValue, ) -from opentelemetry.proto.metrics.v1.metrics_pb2 import ( - InstrumentationLibraryMetrics, -) -from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric as OTLPMetric -from opentelemetry.proto.metrics.v1.metrics_pb2 import ( - NumberDataPoint as OTLPNumberDataPoint, -) -from opentelemetry.proto.metrics.v1.metrics_pb2 import ResourceMetrics -from opentelemetry.proto.metrics.v1.metrics_pb2 import Sum as OTLPSum +from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as OTLPResource, ) from opentelemetry.sdk._metrics.export import MetricExportResult from opentelemetry.sdk._metrics.point import ( AggregationTemporality, + Gauge, Metric, Sum, ) @@ -136,6 +129,16 @@ def _generate_sum(name, val) -> Sum: ) +def _generate_gauge(name, val) -> Gauge: + return _generate_metric( + name, + Gauge( + time_unix_nano=1641946016139533244, + value=val, + ), + ) + + class TestOTLPMetricExporter(TestCase): def setUp(self): @@ -150,6 +153,8 @@ def setUp(self): self.metrics = { "sum_int": _generate_sum("sum_int", 33), "sum_float": _generate_sum("sum_float", 2.98), + "gauge_int": _generate_gauge("gauge_int", 9000), + "gauge_float": _generate_gauge("gauge_float", 52.028), } def tearDown(self): @@ -307,7 +312,7 @@ def test_failure(self): def test_translate_sum_int(self): expected = ExportMetricsServiceRequest( resource_metrics=[ - ResourceMetrics( + pb2.ResourceMetrics( resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=1)), @@ -317,18 +322,18 @@ def test_translate_sum_int(self): ] ), instrumentation_library_metrics=[ - InstrumentationLibraryMetrics( + pb2.InstrumentationLibraryMetrics( instrumentation_library=InstrumentationLibrary( name="first_name", version="first_version" ), metrics=[ - OTLPMetric( + pb2.Metric( name="sum_int", unit="s", description="foo", - sum=OTLPSum( + sum=pb2.Sum( data_points=[ - OTLPNumberDataPoint( + pb2.NumberDataPoint( attributes=[ KeyValue( key="a", @@ -365,7 +370,7 @@ def test_translate_sum_int(self): def test_translate_sum_float(self): expected = ExportMetricsServiceRequest( resource_metrics=[ - ResourceMetrics( + pb2.ResourceMetrics( resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=1)), @@ -375,18 +380,18 @@ def test_translate_sum_float(self): ] ), instrumentation_library_metrics=[ - InstrumentationLibraryMetrics( + pb2.InstrumentationLibraryMetrics( instrumentation_library=InstrumentationLibrary( name="first_name", version="first_version" ), metrics=[ - OTLPMetric( + pb2.Metric( name="sum_float", unit="s", description="foo", - sum=OTLPSum( + sum=pb2.Sum( data_points=[ - OTLPNumberDataPoint( + pb2.NumberDataPoint( attributes=[ KeyValue( key="a", @@ -419,3 +424,113 @@ def test_translate_sum_float(self): # pylint: disable=protected-access actual = self.exporter._translate_data([self.metrics["sum_float"]]) self.assertEqual(expected, actual) + + def test_translate_gauge_int(self): + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_metrics=[ + pb2.InstrumentationLibraryMetrics( + instrumentation_library=InstrumentationLibrary( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="gauge_int", + unit="s", + description="foo", + gauge=pb2.Gauge( + data_points=[ + pb2.NumberDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + time_unix_nano=1641946016139533244, + as_int=9000, + ) + ], + ), + ) + ], + ) + ], + ) + ] + ) + # pylint: disable=protected-access + actual = self.exporter._translate_data([self.metrics["gauge_int"]]) + self.assertEqual(expected, actual) + + def test_translate_gauge_float(self): + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_metrics=[ + pb2.InstrumentationLibraryMetrics( + instrumentation_library=InstrumentationLibrary( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="gauge_float", + unit="s", + description="foo", + gauge=pb2.Gauge( + data_points=[ + pb2.NumberDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + time_unix_nano=1641946016139533244, + as_double=52.028, + ) + ], + ), + ) + ], + ) + ], + ) + ] + ) + # pylint: disable=protected-access + actual = self.exporter._translate_data([self.metrics["gauge_float"]]) + self.assertEqual(expected, actual) From 1b7e161fd3bd871095436ff4e5bab4863120991a Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 25 Jan 2022 14:20:30 -0600 Subject: [PATCH 1105/1517] Make shutdown function be called always (#2405) * Make shutdown function be called always Fixes #2404 * Fix warning handling * Log specific metric reader Co-authored-by: Alex Boten --- .../opentelemetry/sdk/_metrics/__init__.py | 14 +++++++---- .../tests/metrics/test_metrics.py | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 57c44160c8..705e814d69 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -198,13 +198,17 @@ def shutdown(self): _logger.warning("shutdown can only be called once") return False - result = True + overall_result = True for metric_reader in self._sdk_config.metric_readers: - result = result and metric_reader.shutdown() + metric_reader_result = metric_reader.shutdown() - if not result: - _logger.warning("A MetricReader failed to shutdown") + if not metric_reader_result: + _logger.warning( + "MetricReader {metric_reader} failed to shutdown" + ) + + overall_result = overall_result and metric_reader_result self._shutdown = True @@ -212,7 +216,7 @@ def shutdown(self): unregister(self._atexit_handler) self._atexit_handler = None - return result + return overall_result def get_meter( self, diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index bfe1d0df9d..3510e9691e 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -90,6 +90,30 @@ def test_get_meter_duplicate(self): self.assertIs(meter1, meter2) self.assertIsNot(meter1, meter3) + def test_shutdown(self): + + mock_metric_reader_0 = Mock(**{"shutdown.return_value": False}) + mock_metric_reader_1 = Mock(**{"shutdown.return_value": True}) + + meter_provider = MeterProvider( + metric_readers=[mock_metric_reader_0, mock_metric_reader_1] + ) + + self.assertFalse(meter_provider.shutdown()) + mock_metric_reader_0.shutdown.assert_called_once() + mock_metric_reader_1.shutdown.assert_called_once() + + mock_metric_reader_0 = Mock(**{"shutdown.return_value": True}) + mock_metric_reader_1 = Mock(**{"shutdown.return_value": True}) + + meter_provider = MeterProvider( + metric_readers=[mock_metric_reader_0, mock_metric_reader_1] + ) + + self.assertTrue(meter_provider.shutdown()) + mock_metric_reader_0.shutdown.assert_called_once() + mock_metric_reader_1.shutdown.assert_called_once() + def test_shutdown_subsequent_calls(self): """ No subsequent attempts to get a `Meter` are allowed after calling From a659966b950ed055f270e8700f9e39e36e5c4eda Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 25 Jan 2022 14:39:40 -0800 Subject: [PATCH 1106/1517] fixing documentation and return value (#2399) --- docs/examples/metrics/example.py | 6 ++++-- docs/getting_started/metrics_example.py | 10 +++++++--- .../src/opentelemetry/sdk/_metrics/export/__init__.py | 5 ++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/examples/metrics/example.py b/docs/examples/metrics/example.py index 359bb31bf3..7242d976d9 100644 --- a/docs/examples/metrics/example.py +++ b/docs/examples/metrics/example.py @@ -3,12 +3,14 @@ OTLPMetricExporter, ) from opentelemetry.sdk._metrics import MeterProvider +from opentelemetry.sdk._metrics.export import PeriodicExportingMetricReader -provider = MeterProvider() exporter = OTLPMetricExporter(insecure=True) -# TODO: fill in details for metric reader +reader = PeriodicExportingMetricReader(exporter) +provider = MeterProvider(metric_readers=[reader]) set_meter_provider(provider) meter = get_meter_provider().get_meter("getting-started") counter = meter.create_counter("first_counter") +counter.add(1) # TODO: fill in details for additional metrics diff --git a/docs/getting_started/metrics_example.py b/docs/getting_started/metrics_example.py index ccbe465d1d..140dc0004b 100644 --- a/docs/getting_started/metrics_example.py +++ b/docs/getting_started/metrics_example.py @@ -17,13 +17,17 @@ from opentelemetry._metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk._metrics import MeterProvider -from opentelemetry.sdk._metrics.export import ConsoleMetricExporter +from opentelemetry.sdk._metrics.export import ( + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) -provider = MeterProvider() exporter = ConsoleMetricExporter() -# TODO: fill in details for metric reader +reader = PeriodicExportingMetricReader(exporter) +provider = MeterProvider(metric_readers=[reader]) set_meter_provider(provider) meter = get_meter_provider().get_meter("getting-started") counter = meter.create_counter("first_counter") +counter.add(1) # TODO: fill in details for additional metrics diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py index 13494347a8..284629330f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -154,6 +154,8 @@ def _ticker(self) -> None: self.collect() def _receive_metrics(self, metrics: Iterable[Metric]) -> None: + if metrics is None: + return token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: self._exporter.export(metrics) @@ -172,4 +174,5 @@ def _shutdown(): self._shutdown_event.set() self._daemon_thread.join() - return self._exporter.shutdown() + self._exporter.shutdown() + return True From 44e9b5531d91c8c52f58cd4ea3930cda5c1ed662 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 26 Jan 2022 08:54:48 -0800 Subject: [PATCH 1107/1517] [logs] prevent None from causing problems (#2410) * [logs] prevent None from causing problems If the trace or span ID is None for a log entry, the result would raise an error, this change prevents that. * update changelog * Apply suggestions from code review Co-authored-by: Diego Hurtado Co-authored-by: Srikanth Chekuri * add unit test Co-authored-by: Diego Hurtado Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 + .../src/opentelemetry/sdk/_logs/__init__.py | 8 +++- .../tests/logs/test_log_record.py | 42 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 opentelemetry-sdk/tests/logs/test_log_record.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c2034e0f4d..5999fc0d32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2383](https://github.com/open-telemetry/opentelemetry-python/pull/2383)) - [exporter/opentelemetry-exporter-otlp-proto-grpc] Add Gauge to OTLPMetricExporter ([#2408](https://github.com/open-telemetry/opentelemetry-python/pull/2408)) +- [logs] prevent None from causing problems + ([#2410](https://github.com/open-telemetry/opentelemetry-python/pull/2410)) ## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index c7c4e6a90d..1de44712e8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -87,8 +87,12 @@ def to_json(self) -> str: "severity_text": self.severity_text, "attributes": self.attributes, "timestamp": ns_to_iso_str(self.timestamp), - "trace_id": f"0x{format_trace_id(self.trace_id)}", - "span_id": f"0x{format_span_id(self.span_id)}", + "trace_id": f"0x{format_trace_id(self.trace_id)}" + if self.trace_id is not None + else "", + "span_id": f"0x{format_span_id(self.span_id)}" + if self.span_id is not None + else "", "trace_flags": self.trace_flags, "resource": repr(self.resource.attributes) if self.resource diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py new file mode 100644 index 0000000000..d6d8c7a65c --- /dev/null +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -0,0 +1,42 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import unittest + +from opentelemetry.sdk._logs import LogRecord + + +class TestLogRecord(unittest.TestCase): + def test_log_record_to_json(self): + expected = json.dumps( + { + "body": "a log line", + "name": None, + "severity_number": "None", + "severity_text": None, + "attributes": None, + "timestamp": "1970-01-01T00:00:00.000000Z", + "trace_id": "", + "span_id": "", + "trace_flags": None, + "resource": "", + }, + indent=4, + ) + actual = LogRecord( + timestamp=0, + body="a log line", + ).to_json() + self.assertEqual(expected, actual) From 5cd7a74e7fff6ec13bb3853976c117eaaf48bff3 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 26 Jan 2022 23:30:31 +0530 Subject: [PATCH 1108/1517] updating changelogs and version to 1.9.0-0.28b0 (#2411) Co-authored-by: Alex Boten --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 5 ++++- eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- .../src/opentelemetry/test/version.py | 2 +- 29 files changed, 37 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf077b3d85..2275751a1a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 741321434f0803dd3b304f3a93022d8d451a4c90 + CONTRIB_REPO_SHA: eedd96a86931ba0f84112e97fc3e7db221261a20 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 5999fc0d32..d455c03080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.8.0-0.27b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.9.0-0.28b0...HEAD) + +## [1.9.0-0.28b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.0-0.28b0) - 2022-01-26 + - Fix SpanLimits global span limit defaulting when set to 0 ([#2398](https://github.com/open-telemetry/opentelemetry-python/pull/2398)) diff --git a/eachdist.ini b/eachdist.ini index d76fe73ef3..33f717b78a 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.8.0 +version=1.9.0 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.27b0 +version=0.28b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 87f6a8da6c..f1667e7652 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 87f6a8da6c..f1667e7652 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index fc69e9c461..02f232f72b 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -40,8 +40,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.8.0 - opentelemetry-exporter-jaeger-thrift == 1.8.0 + opentelemetry-exporter-jaeger-proto-grpc == 1.9.0 + opentelemetry-exporter-jaeger-thrift == 1.9.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 87f6a8da6c..f1667e7652 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index f0adf9ba13..9bea3afdaf 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.27b0" +__version__ = "0.28b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index b6fac99bbc..8d4ef0e664 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.8.0 + opentelemetry-proto == 1.9.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index bea3e76cbe..d074a5b24b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index cddae1da45..20bda92f99 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.8.0 + opentelemetry-proto == 1.9.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index bea3e76cbe..d074a5b24b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 86ce4b6e2b..0f67262d3b 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -39,8 +39,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.8.0 - opentelemetry-exporter-otlp-proto-http == 1.8.0 + opentelemetry-exporter-otlp-proto-grpc == 1.9.0 + opentelemetry-exporter-otlp-proto-http == 1.9.0 [options.entry_points] opentelemetry_traces_exporter = diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index bea3e76cbe..d074a5b24b 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index bea3e76cbe..d074a5b24b 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 7c71415912..88c81fb121 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.8.0 + opentelemetry-exporter-zipkin-json == 1.9.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index bea3e76cbe..d074a5b24b 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index a91d6a0f21..4e193df6b2 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -39,8 +39,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.8.0 - opentelemetry-exporter-zipkin-proto-http == 1.8.0 + opentelemetry-exporter-zipkin-json == 1.9.0 + opentelemetry-exporter-zipkin-proto-http == 1.9.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index bea3e76cbe..d074a5b24b 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index bea3e76cbe..d074a5b24b 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index bea3e76cbe..d074a5b24b 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 49ed93c14d..a4222db9a7 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,8 +43,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.8.0 - opentelemetry-semantic-conventions == 0.27b0 + opentelemetry-api == 1.9.0 + opentelemetry-semantic-conventions == 0.28b0 setuptools >= 16.0 dataclasses == 0.8; python_version < '3.7' typing-extensions >= 3.7.4 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index bea3e76cbe..d074a5b24b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index f0adf9ba13..9bea3afdaf 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.27b0" +__version__ = "0.28b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index bea3e76cbe..d074a5b24b 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index bea3e76cbe..d074a5b24b 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index fc1b791172..0443162638 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -47,7 +47,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test-utils == 0.27b0 + opentelemetry-test-utils == 0.28b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index f0adf9ba13..9bea3afdaf 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.27b0" +__version__ = "0.28b0" diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 7b984a70b3..116b0529b4 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.27b0" +__version__ = "0.28b0" From b339ca7cec21ec76872647e2d44d692f43184db3 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 26 Jan 2022 13:08:09 -0600 Subject: [PATCH 1109/1517] Fix log message (#2414) Fixes #2413 --- .../src/opentelemetry/sdk/_metrics/__init__.py | 2 +- opentelemetry-sdk/tests/metrics/test_metrics.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 705e814d69..4f0d5d141a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -205,7 +205,7 @@ def shutdown(self): if not metric_reader_result: _logger.warning( - "MetricReader {metric_reader} failed to shutdown" + "MetricReader %s failed to shutdown", metric_reader ) overall_result = overall_result and metric_reader_result diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 3510e9691e..3126535a90 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -15,7 +15,7 @@ from logging import WARNING from unittest import TestCase -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch from opentelemetry.sdk._metrics import Meter, MeterProvider from opentelemetry.sdk._metrics.instrument import ( @@ -92,14 +92,24 @@ def test_get_meter_duplicate(self): def test_shutdown(self): - mock_metric_reader_0 = Mock(**{"shutdown.return_value": False}) + mock_metric_reader_0 = MagicMock( + **{ + "shutdown.return_value": False, + "__str__.return_value": "mock_metric_reader_0", + } + ) mock_metric_reader_1 = Mock(**{"shutdown.return_value": True}) meter_provider = MeterProvider( metric_readers=[mock_metric_reader_0, mock_metric_reader_1] ) - self.assertFalse(meter_provider.shutdown()) + with self.assertLogs(level=WARNING) as log: + self.assertFalse(meter_provider.shutdown()) + self.assertEqual( + log.records[0].getMessage(), + "MetricReader mock_metric_reader_0 failed to shutdown", + ) mock_metric_reader_0.shutdown.assert_called_once() mock_metric_reader_1.shutdown.assert_called_once() From e780a97c9c355d98a27a5d1af8655713c91ef1a1 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 26 Jan 2022 17:10:11 -0600 Subject: [PATCH 1110/1517] Slightly increase wait time (#2417) Fixes #2416 --- .../tests/metrics/test_periodic_exporting_metric_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index da09b51b8d..ca7a167630 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -105,7 +105,7 @@ def test_ticker_collects_metrics(self): pmr = self._create_periodic_reader( metrics_list, exporter, interval=100 ) - time.sleep(0.2) + time.sleep(0.11) self.assertEqual(exporter.metrics, metrics_list) pmr.shutdown() From d1128142df0ea5860782ae3314ff1bea632f36a5 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 27 Jan 2022 04:57:34 +0530 Subject: [PATCH 1111/1517] Refactor SDK MeterProvider (#2401) --- .../opentelemetry/sdk/_metrics/__init__.py | 27 ++++++------ .../tests/metrics/test_metrics.py | 41 ++++++++++++++++++- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 4f0d5d141a..0cdcbab6e9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -48,6 +48,7 @@ from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.util._once import Once _logger = getLogger(__name__) @@ -171,30 +172,30 @@ def __init__( self._metric_readers = metric_readers for metric_reader in self._sdk_config.metric_readers: - metric_reader._register_measurement_consumer(self) + metric_reader._set_collect_callback( + self._measurement_consumer.collect + ) + self._shutdown_once = Once() self._shutdown = False def force_flush(self) -> bool: # FIXME implement a timeout - metric_reader_result = True - for metric_reader in self._sdk_config.metric_readers: - metric_reader_result = ( - metric_reader_result and metric_reader.force_flush() - ) - - if not metric_reader_result: - _logger.warning("Unable to force flush all metric readers") - - return metric_reader_result + metric_reader.collect() + return True def shutdown(self): # FIXME implement a timeout - if self._shutdown: + def _shutdown(): + self._shutdown = True + + did_shutdown = self._shutdown_once.do_once(_shutdown) + + if not did_shutdown: _logger.warning("shutdown can only be called once") return False @@ -210,8 +211,6 @@ def shutdown(self): overall_result = overall_result and metric_reader_result - self._shutdown = True - if self._atexit_handler is not None: unregister(self._atexit_handler) self._atexit_handler = None diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 3126535a90..8a78eaecd8 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -26,10 +26,24 @@ ObservableUpDownCounter, UpDownCounter, ) +from opentelemetry.sdk._metrics.metric_reader import MetricReader +from opentelemetry.sdk._metrics.point import AggregationTemporality from opentelemetry.sdk.resources import Resource +from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc -class TestMeterProvider(TestCase): +class DummyMetricReader(MetricReader): + def __init__(self): + super().__init__(AggregationTemporality.CUMULATIVE) + + def _receive_metrics(self, metrics): + pass + + def shutdown(self): + return True + + +class TestMeterProvider(ConcurrencyTestBase): def test_resource(self): """ `MeterProvider` provides a way to allow a `Resource` to be specified. @@ -139,6 +153,31 @@ def test_shutdown_subsequent_calls(self): with self.assertLogs(level=WARNING): meter_provider.shutdown() + @patch("opentelemetry.sdk._metrics._logger") + def test_shutdown_race(self, mock_logger): + mock_logger.warning = MockFunc() + meter_provider = MeterProvider() + num_threads = 70 + self.run_with_many_threads( + meter_provider.shutdown, num_threads=num_threads + ) + self.assertEqual(mock_logger.warning.call_count, num_threads - 1) + + @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + def test_measurement_collect_callback( + self, mock_sync_measurement_consumer + ): + metric_readers = [DummyMetricReader()] * 5 + sync_consumer_instance = mock_sync_measurement_consumer() + sync_consumer_instance.collect = MockFunc() + MeterProvider(metric_readers=metric_readers) + + for reader in metric_readers: + reader.collect() + self.assertEqual( + sync_consumer_instance.collect.call_count, len(metric_readers) + ) + @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") def test_creates_sync_measurement_consumer( self, mock_sync_measurement_consumer From 2f2d25211a4821a06fe86bd12b6db78b5c788fe5 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 28 Jan 2022 10:50:00 -0800 Subject: [PATCH 1112/1517] update proto to 0.12.0 (#2415) * update proto to 0.12.0 This PR updates the proto to the latest release. * remove deprecated status codes, no longer supported * update logs to log_records * remove unused import * update changelog --- CHANGELOG.md | 3 + .../otlp/proto/grpc/_log_exporter/__init__.py | 2 +- .../proto/grpc/trace_exporter/__init__.py | 5 - .../tests/logs/test_otlp_logs_exporter.py | 8 +- .../tests/test_otlp_trace_exporter.py | 8 - .../http/trace_exporter/encoder/__init__.py | 5 - .../tests/test_protobuf_encoder.py | 4 - .../opentelemetry/proto/logs/v1/logs_pb2.py | 67 +- .../opentelemetry/proto/logs/v1/logs_pb2.pyi | 38 +- .../proto/metrics/v1/metrics_pb2.py | 692 +++++++----------- .../proto/metrics/v1/metrics_pb2.pyi | 565 ++++++-------- .../opentelemetry/proto/trace/v1/trace_pb2.py | 190 ++--- .../proto/trace/v1/trace_pb2.pyi | 133 +--- scripts/proto_codegen.sh | 2 +- 14 files changed, 695 insertions(+), 1027 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d455c03080..abaab2a309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.9.0-0.28b0...HEAD) +- Update opentelemetry-proto to v0.12.0. Note that this update removes deprecated status codes. + ([#2415](https://github.com/open-telemetry/opentelemetry-python/pull/2415)) + ## [1.9.0-0.28b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.0-0.28b0) - 2022-01-26 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index 96a5d76963..187b69a781 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -155,7 +155,7 @@ def _translate_data( "severity_number" ] = log_data.log_record.severity_number.value - instrumentation_library_logs.logs.append( + instrumentation_library_logs.log_records.append( PB2LogRecord(**self._collector_kwargs) ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 0656e309f4..5cc8c885d7 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -49,7 +49,6 @@ ) from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult -from opentelemetry.trace import StatusCode logger = logging.getLogger(__name__) @@ -208,11 +207,7 @@ def _translate_links(self, sdk_span: ReadableSpan) -> None: def _translate_status(self, sdk_span: ReadableSpan) -> None: # pylint: disable=no-member if sdk_span.status is not None: - deprecated_code = Status.DEPRECATED_STATUS_CODE_OK - if sdk_span.status.status_code == StatusCode.ERROR: - deprecated_code = Status.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR self._collector_kwargs["status"] = Status( - deprecated_code=deprecated_code, code=sdk_span.status.status_code.value, message=sdk_span.status.description, ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index 556700ab85..b470bc1371 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -312,7 +312,7 @@ def test_translate_log_data(self): instrumentation_library=InstrumentationLibrary( name="first_name", version="first_version" ), - logs=[ + log_records=[ PB2LogRecord( # pylint: disable=no-member name="name", @@ -372,7 +372,7 @@ def test_translate_multiple_logs(self): instrumentation_library=InstrumentationLibrary( name="first_name", version="first_version" ), - logs=[ + log_records=[ PB2LogRecord( # pylint: disable=no-member name="name", @@ -410,7 +410,7 @@ def test_translate_multiple_logs(self): instrumentation_library=InstrumentationLibrary( name="second_name", version="second_version" ), - logs=[ + log_records=[ PB2LogRecord( # pylint: disable=no-member name="info name", @@ -456,7 +456,7 @@ def test_translate_multiple_logs(self): instrumentation_library=InstrumentationLibrary( name="third_name", version="third_version" ), - logs=[ + log_records=[ PB2LogRecord( # pylint: disable=no-member name="error name", diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index d09c21f412..80011cafb0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -762,7 +762,6 @@ def _check_translated_status( self, translated: ExportTraceServiceRequest, code_expected: Status, - deprecated_code_expected: Status, ): status = ( translated.resource_spans[0] @@ -775,10 +774,6 @@ def _check_translated_status( status.code, code_expected, ) - self.assertEqual( - status.deprecated_code, - deprecated_code_expected, - ) def test_span_status_translate(self): # pylint: disable=protected-access,no-member @@ -797,17 +792,14 @@ def test_span_status_translate(self): self._check_translated_status( unset_translated, Status.STATUS_CODE_UNSET, - Status.DEPRECATED_STATUS_CODE_OK, ) self._check_translated_status( ok_translated, Status.STATUS_CODE_OK, - Status.DEPRECATED_STATUS_CODE_OK, ) self._check_translated_status( error_translated, Status.STATUS_CODE_ERROR, - Status.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR, ) # pylint:disable=no-member diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py index 4802dddcac..24497d204d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py @@ -45,7 +45,6 @@ from opentelemetry.trace import Link from opentelemetry.trace import SpanKind from opentelemetry.trace.span import SpanContext, TraceState, Status -from opentelemetry.trace.status import StatusCode from opentelemetry.util.types import Attributes # pylint: disable=E1101 @@ -196,11 +195,7 @@ def _encode_links(links: List[Link]) -> List[PB2SPan.Link]: def _encode_status(status: Status) -> Optional[PB2Status]: pb2_status = None if status is not None: - deprecated_code = PB2Status.DEPRECATED_STATUS_CODE_OK - if status.status_code is StatusCode.ERROR: - deprecated_code = PB2Status.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR pb2_status = PB2Status( - deprecated_code=deprecated_code, code=status.status_code.value, message=status.description, ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py index 676b5f7b7a..f2d2445677 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py @@ -268,7 +268,6 @@ def get_exhaustive_test_spans( ) ], status=PB2Status( - deprecated_code=PB2Status.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR, # pylint: disable=no-member code=SDKStatusCode.ERROR.value, message="Example description", ), @@ -374,7 +373,6 @@ def test_encode_status_code_translations(self): self.assertEqual( _encode_status(SDKStatus(status_code=SDKStatusCode.UNSET)), PB2Status( - deprecated_code=PB2Status.DEPRECATED_STATUS_CODE_OK, # pylint: disable=no-member code=SDKStatusCode.UNSET.value, ), ) @@ -382,7 +380,6 @@ def test_encode_status_code_translations(self): self.assertEqual( _encode_status(SDKStatus(status_code=SDKStatusCode.OK)), PB2Status( - deprecated_code=PB2Status.DEPRECATED_STATUS_CODE_OK, # pylint: disable=no-member code=SDKStatusCode.OK.value, ), ) @@ -390,7 +387,6 @@ def test_encode_status_code_translations(self): self.assertEqual( _encode_status(SDKStatus(status_code=SDKStatusCode.ERROR)), PB2Status( - deprecated_code=PB2Status.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR, # pylint: disable=no-member code=SDKStatusCode.ERROR.value, ), ) diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py index 31adc70f13..6e16d73da1 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py @@ -22,7 +22,7 @@ syntax='proto3', serialized_options=b'\n\036io.opentelemetry.proto.logs.v1B\tLogsProtoP\001Z google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResourceLogs]: + """An array of ResourceLogs. + For data coming from a single resource this array will typically contain + one element. Intermediary nodes that receive data from multiple origins + typically batch the data before forwarding further and in that case this + array will contain multiple elements. + """ + pass + def __init__(self, + *, + resource_logs : typing.Optional[typing.Iterable[global___ResourceLogs]] = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["resource_logs",b"resource_logs"]) -> None: ... +global___LogsData = LogsData + class ResourceLogs(google.protobuf.message.Message): """A collection of InstrumentationLibraryLogs from a Resource.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... @@ -130,7 +160,7 @@ class InstrumentationLibraryLogs(google.protobuf.message.Message): """A collection of Logs produced by an InstrumentationLibrary.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int - LOGS_FIELD_NUMBER: builtins.int + LOG_RECORDS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property def instrumentation_library(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: @@ -140,7 +170,7 @@ class InstrumentationLibraryLogs(google.protobuf.message.Message): """ pass @property - def logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___LogRecord]: + def log_records(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___LogRecord]: """A list of log records.""" pass schema_url: typing.Text = ... @@ -149,11 +179,11 @@ class InstrumentationLibraryLogs(google.protobuf.message.Message): def __init__(self, *, instrumentation_library : typing.Optional[opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary] = ..., - logs : typing.Optional[typing.Iterable[global___LogRecord]] = ..., + log_records : typing.Optional[typing.Iterable[global___LogRecord]] = ..., schema_url : typing.Text = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library","logs",b"logs","schema_url",b"schema_url"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library","log_records",b"log_records","schema_url",b"schema_url"]) -> None: ... global___InstrumentationLibraryLogs = InstrumentationLibraryLogs class LogRecord(google.protobuf.message.Message): diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py index c28faca33d..5e3cccc105 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py @@ -22,7 +22,7 @@ syntax='proto3', serialized_options=b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xca\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x66\n\x1finstrumentation_library_metrics\x18\x02 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc4\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xf6\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x41\n\tint_gauge\x18\x04 \x01(\x0b\x32(.opentelemetry.proto.metrics.v1.IntGaugeB\x02\x18\x01H\x00\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12=\n\x07int_sum\x18\x06 \x01(\x0b\x32&.opentelemetry.proto.metrics.v1.IntSumB\x02\x18\x01H\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12I\n\rint_histogram\x18\x08 \x01(\x0b\x32,.opentelemetry.proto.metrics.v1.IntHistogramB\x02\x18\x01H\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61ta\"Q\n\x08IntGauge\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint:\x02\x18\x01\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xbe\x01\n\x06IntSum\x12\x41\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.IntDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08:\x02\x18\x01\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xb7\x01\n\x0cIntHistogram\x12J\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x35.opentelemetry.proto.metrics.v1.IntHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality:\x02\x18\x01\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\xd6\x01\n\x0cIntDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05value\x18\x04 \x01(\x10\x12>\n\texemplars\x18\x05 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar:\x02\x18\x01\"\xb4\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x41\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValueB\x02\x18\x01\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.ExemplarB\x07\n\x05value\"\x9c\x02\n\x15IntHistogramDataPoint\x12=\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x10\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12>\n\texemplars\x18\x08 \x03(\x0b\x32+.opentelemetry.proto.metrics.v1.IntExemplar:\x02\x18\x01\"\xd3\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x41\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValueB\x02\x18\x01\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\"\xf3\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x41\n\x06labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValueB\x02\x18\x01\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01\"\xa3\x01\n\x0bIntExemplar\x12\x46\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\r\n\x05value\x18\x03 \x01(\x10\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c:\x02\x18\x01\"\x87\x02\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12J\n\x0f\x66iltered_labels\x18\x01 \x03(\x0b\x32-.opentelemetry.proto.common.v1.StringKeyValueB\x02\x18\x01\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05value*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02\x42t\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1b\x06proto3' + serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\xca\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x66\n\x1finstrumentation_library_metrics\x18\x02 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc4\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x92\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xa5\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\rJ\x04\x08\x01\x10\x02\"\x81\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*;\n\x0e\x44\x61taPointFlags\x12\r\n\tFLAG_NONE\x10\x00\x12\x1a\n\x16\x46LAG_NO_RECORDED_VALUE\x10\x01\x42t\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -51,16 +51,76 @@ ], containing_type=None, serialized_options=None, - serialized_start=4033, - serialized_end=4173, + serialized_start=3397, + serialized_end=3537, ) _sym_db.RegisterEnumDescriptor(_AGGREGATIONTEMPORALITY) AggregationTemporality = enum_type_wrapper.EnumTypeWrapper(_AGGREGATIONTEMPORALITY) +_DATAPOINTFLAGS = _descriptor.EnumDescriptor( + name='DataPointFlags', + full_name='opentelemetry.proto.metrics.v1.DataPointFlags', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='FLAG_NONE', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='FLAG_NO_RECORDED_VALUE', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=3539, + serialized_end=3598, +) +_sym_db.RegisterEnumDescriptor(_DATAPOINTFLAGS) + +DataPointFlags = enum_type_wrapper.EnumTypeWrapper(_DATAPOINTFLAGS) AGGREGATION_TEMPORALITY_UNSPECIFIED = 0 AGGREGATION_TEMPORALITY_DELTA = 1 AGGREGATION_TEMPORALITY_CUMULATIVE = 2 +FLAG_NONE = 0 +FLAG_NO_RECORDED_VALUE = 1 + + +_METRICSDATA = _descriptor.Descriptor( + name='MetricsData', + full_name='opentelemetry.proto.metrics.v1.MetricsData', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='resource_metrics', full_name='opentelemetry.proto.metrics.v1.MetricsData.resource_metrics', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=172, + serialized_end=260, +) _RESOURCEMETRICS = _descriptor.Descriptor( @@ -104,8 +164,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=173, - serialized_end=375, + serialized_start=263, + serialized_end=465, ) @@ -150,8 +210,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=378, - serialized_end=574, + serialized_start=468, + serialized_end=664, ) @@ -185,49 +245,35 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='int_gauge', full_name='opentelemetry.proto.metrics.v1.Metric.int_gauge', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='gauge', full_name='opentelemetry.proto.metrics.v1.Metric.gauge', index=4, + name='gauge', full_name='opentelemetry.proto.metrics.v1.Metric.gauge', index=3, number=5, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='int_sum', full_name='opentelemetry.proto.metrics.v1.Metric.int_sum', index=5, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='sum', full_name='opentelemetry.proto.metrics.v1.Metric.sum', index=6, + name='sum', full_name='opentelemetry.proto.metrics.v1.Metric.sum', index=4, number=7, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='int_histogram', full_name='opentelemetry.proto.metrics.v1.Metric.int_histogram', index=7, - number=8, type=11, cpp_type=10, label=1, + name='histogram', full_name='opentelemetry.proto.metrics.v1.Metric.histogram', index=5, + number=9, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='histogram', full_name='opentelemetry.proto.metrics.v1.Metric.histogram', index=8, - number=9, type=11, cpp_type=10, label=1, + name='exponential_histogram', full_name='opentelemetry.proto.metrics.v1.Metric.exponential_histogram', index=6, + number=10, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='summary', full_name='opentelemetry.proto.metrics.v1.Metric.summary', index=9, + name='summary', full_name='opentelemetry.proto.metrics.v1.Metric.summary', index=7, number=11, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, @@ -250,40 +296,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=577, - serialized_end=1079, -) - - -_INTGAUGE = _descriptor.Descriptor( - name='IntGauge', - full_name='opentelemetry.proto.metrics.v1.IntGauge', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='data_points', full_name='opentelemetry.proto.metrics.v1.IntGauge.data_points', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=b'\030\001', - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1081, - serialized_end=1162, + serialized_start=667, + serialized_end=1069, ) @@ -314,54 +328,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1164, - serialized_end=1241, -) - - -_INTSUM = _descriptor.Descriptor( - name='IntSum', - full_name='opentelemetry.proto.metrics.v1.IntSum', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='data_points', full_name='opentelemetry.proto.metrics.v1.IntSum.data_points', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.IntSum.aggregation_temporality', index=1, - number=2, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='is_monotonic', full_name='opentelemetry.proto.metrics.v1.IntSum.is_monotonic', index=2, - number=3, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=b'\030\001', - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1244, - serialized_end=1434, + serialized_start=1071, + serialized_end=1148, ) @@ -406,28 +374,28 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1437, - serialized_end=1623, + serialized_start=1151, + serialized_end=1337, ) -_INTHISTOGRAM = _descriptor.Descriptor( - name='IntHistogram', - full_name='opentelemetry.proto.metrics.v1.IntHistogram', +_HISTOGRAM = _descriptor.Descriptor( + name='Histogram', + full_name='opentelemetry.proto.metrics.v1.Histogram', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( - name='data_points', full_name='opentelemetry.proto.metrics.v1.IntHistogram.data_points', index=0, + name='data_points', full_name='opentelemetry.proto.metrics.v1.Histogram.data_points', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.IntHistogram.aggregation_temporality', index=1, + name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.Histogram.aggregation_temporality', index=1, number=2, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, @@ -439,34 +407,34 @@ nested_types=[], enum_types=[ ], - serialized_options=b'\030\001', + serialized_options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], - serialized_start=1626, - serialized_end=1809, + serialized_start=1340, + serialized_end=1513, ) -_HISTOGRAM = _descriptor.Descriptor( - name='Histogram', - full_name='opentelemetry.proto.metrics.v1.Histogram', +_EXPONENTIALHISTOGRAM = _descriptor.Descriptor( + name='ExponentialHistogram', + full_name='opentelemetry.proto.metrics.v1.ExponentialHistogram', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( - name='data_points', full_name='opentelemetry.proto.metrics.v1.Histogram.data_points', index=0, + name='data_points', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogram.data_points', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.Histogram.aggregation_temporality', index=1, + name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogram.aggregation_temporality', index=1, number=2, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, @@ -484,8 +452,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1812, - serialized_end=1985, + serialized_start=1516, + serialized_end=1711, ) @@ -516,68 +484,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1987, - serialized_end=2067, -) - - -_INTDATAPOINT = _descriptor.Descriptor( - name='IntDataPoint', - full_name='opentelemetry.proto.metrics.v1.IntDataPoint', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='labels', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.labels', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.start_time_unix_nano', index=1, - number=2, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.time_unix_nano', index=2, - number=3, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='value', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.value', index=3, - number=4, type=16, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='exemplars', full_name='opentelemetry.proto.metrics.v1.IntDataPoint.exemplars', index=4, - number=5, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=b'\030\001', - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2070, - serialized_end=2284, + serialized_start=1713, + serialized_end=1793, ) @@ -597,47 +505,47 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='labels', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.labels', index=1, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.start_time_unix_nano', index=2, + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.start_time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.time_unix_nano', index=3, + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.time_unix_nano', index=2, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='as_double', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.as_double', index=4, + name='as_double', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.as_double', index=3, number=4, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='as_int', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.as_int', index=5, + name='as_int', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.as_int', index=4, number=6, type=16, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='exemplars', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.exemplars', index=6, + name='exemplars', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.exemplars', index=5, number=5, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='flags', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.flags', index=6, + number=8, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -655,159 +563,218 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=2287, - serialized_end=2595, + serialized_start=1796, + serialized_end=2058, ) -_INTHISTOGRAMDATAPOINT = _descriptor.Descriptor( - name='IntHistogramDataPoint', - full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint', +_HISTOGRAMDATAPOINT = _descriptor.Descriptor( + name='HistogramDataPoint', + full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( - name='labels', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.labels', index=0, - number=1, type=11, cpp_type=10, label=3, + name='attributes', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.attributes', index=0, + number=9, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.start_time_unix_nano', index=1, + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.start_time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.time_unix_nano', index=2, + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.time_unix_nano', index=2, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='count', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.count', index=3, + name='count', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.count', index=3, number=4, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='sum', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.sum', index=4, - number=5, type=16, cpp_type=2, label=1, - has_default_value=False, default_value=0, + name='sum', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.sum', index=4, + number=5, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='bucket_counts', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.bucket_counts', index=5, + name='bucket_counts', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.bucket_counts', index=5, number=6, type=6, cpp_type=4, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='explicit_bounds', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.explicit_bounds', index=6, + name='explicit_bounds', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.explicit_bounds', index=6, number=7, type=1, cpp_type=5, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='exemplars', full_name='opentelemetry.proto.metrics.v1.IntHistogramDataPoint.exemplars', index=7, + name='exemplars', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.exemplars', index=7, number=8, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='flags', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.flags', index=8, + number=10, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], - serialized_options=b'\030\001', + serialized_options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], - serialized_start=2598, - serialized_end=2882, + serialized_start=2061, + serialized_end=2354, ) -_HISTOGRAMDATAPOINT = _descriptor.Descriptor( - name='HistogramDataPoint', - full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint', +_EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS = _descriptor.Descriptor( + name='Buckets', + full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( - name='attributes', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.attributes', index=0, - number=9, type=11, cpp_type=10, label=3, + name='offset', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets.offset', index=0, + number=1, type=17, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='bucket_counts', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets.bucket_counts', index=1, + number=2, type=4, cpp_type=4, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2822, + serialized_end=2870, +) + +_EXPONENTIALHISTOGRAMDATAPOINT = _descriptor.Descriptor( + name='ExponentialHistogramDataPoint', + full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ _descriptor.FieldDescriptor( - name='labels', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.labels', index=1, + name='attributes', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.attributes', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.start_time_unix_nano', index=2, + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.start_time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.time_unix_nano', index=3, + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.time_unix_nano', index=2, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='count', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.count', index=4, + name='count', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.count', index=3, number=4, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='sum', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.sum', index=5, + name='sum', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.sum', index=4, number=5, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='bucket_counts', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.bucket_counts', index=6, - number=6, type=6, cpp_type=4, label=3, - has_default_value=False, default_value=[], + name='scale', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.scale', index=5, + number=6, type=17, cpp_type=1, label=1, + has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='explicit_bounds', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.explicit_bounds', index=7, - number=7, type=1, cpp_type=5, label=3, - has_default_value=False, default_value=[], + name='zero_count', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.zero_count', index=6, + number=7, type=6, cpp_type=4, label=1, + has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='exemplars', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.exemplars', index=8, - number=8, type=11, cpp_type=10, label=3, + name='positive', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.positive', index=7, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='negative', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.negative', index=8, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='flags', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.flags', index=9, + number=10, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='exemplars', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.exemplars', index=10, + number=11, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, @@ -815,7 +782,7 @@ ], extensions=[ ], - nested_types=[], + nested_types=[_EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS, ], enum_types=[ ], serialized_options=None, @@ -824,8 +791,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2885, - serialized_end=3224, + serialized_start=2357, + serialized_end=2870, ) @@ -863,8 +830,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3548, - serialized_end=3598, + serialized_start=3142, + serialized_end=3192, ) _SUMMARYDATAPOINT = _descriptor.Descriptor( @@ -883,121 +850,61 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='labels', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.labels', index=1, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.start_time_unix_nano', index=2, + name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.start_time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.time_unix_nano', index=3, + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.time_unix_nano', index=2, number=3, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='count', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.count', index=4, + name='count', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.count', index=3, number=4, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='sum', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.sum', index=5, + name='sum', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.sum', index=4, number=5, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='quantile_values', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.quantile_values', index=6, + name='quantile_values', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.quantile_values', index=5, number=6, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_SUMMARYDATAPOINT_VALUEATQUANTILE, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3227, - serialized_end=3598, -) - - -_INTEXEMPLAR = _descriptor.Descriptor( - name='IntExemplar', - full_name='opentelemetry.proto.metrics.v1.IntExemplar', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='filtered_labels', full_name='opentelemetry.proto.metrics.v1.IntExemplar.filtered_labels', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.IntExemplar.time_unix_nano', index=1, - number=2, type=6, cpp_type=4, label=1, + name='flags', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.flags', index=6, + number=8, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='value', full_name='opentelemetry.proto.metrics.v1.IntExemplar.value', index=2, - number=3, type=16, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='span_id', full_name='opentelemetry.proto.metrics.v1.IntExemplar.span_id', index=3, - number=4, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='trace_id', full_name='opentelemetry.proto.metrics.v1.IntExemplar.trace_id', index=4, - number=5, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], - nested_types=[], + nested_types=[_SUMMARYDATAPOINT_VALUEATQUANTILE, ], enum_types=[ ], - serialized_options=b'\030\001', + serialized_options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], - serialized_start=3601, - serialized_end=3764, + serialized_start=2873, + serialized_end=3198, ) @@ -1017,42 +924,35 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='filtered_labels', full_name='opentelemetry.proto.metrics.v1.Exemplar.filtered_labels', index=1, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.Exemplar.time_unix_nano', index=2, + name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.Exemplar.time_unix_nano', index=1, number=2, type=6, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='as_double', full_name='opentelemetry.proto.metrics.v1.Exemplar.as_double', index=3, + name='as_double', full_name='opentelemetry.proto.metrics.v1.Exemplar.as_double', index=2, number=3, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='as_int', full_name='opentelemetry.proto.metrics.v1.Exemplar.as_int', index=4, + name='as_int', full_name='opentelemetry.proto.metrics.v1.Exemplar.as_int', index=3, number=6, type=16, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='span_id', full_name='opentelemetry.proto.metrics.v1.Exemplar.span_id', index=5, + name='span_id', full_name='opentelemetry.proto.metrics.v1.Exemplar.span_id', index=4, number=4, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='trace_id', full_name='opentelemetry.proto.metrics.v1.Exemplar.trace_id', index=6, + name='trace_id', full_name='opentelemetry.proto.metrics.v1.Exemplar.trace_id', index=5, number=5, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, @@ -1075,57 +975,44 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=3767, - serialized_end=4030, + serialized_start=3201, + serialized_end=3394, ) +_METRICSDATA.fields_by_name['resource_metrics'].message_type = _RESOURCEMETRICS _RESOURCEMETRICS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE _RESOURCEMETRICS.fields_by_name['instrumentation_library_metrics'].message_type = _INSTRUMENTATIONLIBRARYMETRICS _INSTRUMENTATIONLIBRARYMETRICS.fields_by_name['instrumentation_library'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONLIBRARY _INSTRUMENTATIONLIBRARYMETRICS.fields_by_name['metrics'].message_type = _METRIC -_METRIC.fields_by_name['int_gauge'].message_type = _INTGAUGE _METRIC.fields_by_name['gauge'].message_type = _GAUGE -_METRIC.fields_by_name['int_sum'].message_type = _INTSUM _METRIC.fields_by_name['sum'].message_type = _SUM -_METRIC.fields_by_name['int_histogram'].message_type = _INTHISTOGRAM _METRIC.fields_by_name['histogram'].message_type = _HISTOGRAM +_METRIC.fields_by_name['exponential_histogram'].message_type = _EXPONENTIALHISTOGRAM _METRIC.fields_by_name['summary'].message_type = _SUMMARY -_METRIC.oneofs_by_name['data'].fields.append( - _METRIC.fields_by_name['int_gauge']) -_METRIC.fields_by_name['int_gauge'].containing_oneof = _METRIC.oneofs_by_name['data'] _METRIC.oneofs_by_name['data'].fields.append( _METRIC.fields_by_name['gauge']) _METRIC.fields_by_name['gauge'].containing_oneof = _METRIC.oneofs_by_name['data'] -_METRIC.oneofs_by_name['data'].fields.append( - _METRIC.fields_by_name['int_sum']) -_METRIC.fields_by_name['int_sum'].containing_oneof = _METRIC.oneofs_by_name['data'] _METRIC.oneofs_by_name['data'].fields.append( _METRIC.fields_by_name['sum']) _METRIC.fields_by_name['sum'].containing_oneof = _METRIC.oneofs_by_name['data'] -_METRIC.oneofs_by_name['data'].fields.append( - _METRIC.fields_by_name['int_histogram']) -_METRIC.fields_by_name['int_histogram'].containing_oneof = _METRIC.oneofs_by_name['data'] _METRIC.oneofs_by_name['data'].fields.append( _METRIC.fields_by_name['histogram']) _METRIC.fields_by_name['histogram'].containing_oneof = _METRIC.oneofs_by_name['data'] +_METRIC.oneofs_by_name['data'].fields.append( + _METRIC.fields_by_name['exponential_histogram']) +_METRIC.fields_by_name['exponential_histogram'].containing_oneof = _METRIC.oneofs_by_name['data'] _METRIC.oneofs_by_name['data'].fields.append( _METRIC.fields_by_name['summary']) _METRIC.fields_by_name['summary'].containing_oneof = _METRIC.oneofs_by_name['data'] -_INTGAUGE.fields_by_name['data_points'].message_type = _INTDATAPOINT _GAUGE.fields_by_name['data_points'].message_type = _NUMBERDATAPOINT -_INTSUM.fields_by_name['data_points'].message_type = _INTDATAPOINT -_INTSUM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY _SUM.fields_by_name['data_points'].message_type = _NUMBERDATAPOINT _SUM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY -_INTHISTOGRAM.fields_by_name['data_points'].message_type = _INTHISTOGRAMDATAPOINT -_INTHISTOGRAM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY _HISTOGRAM.fields_by_name['data_points'].message_type = _HISTOGRAMDATAPOINT _HISTOGRAM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY +_EXPONENTIALHISTOGRAM.fields_by_name['data_points'].message_type = _EXPONENTIALHISTOGRAMDATAPOINT +_EXPONENTIALHISTOGRAM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY _SUMMARY.fields_by_name['data_points'].message_type = _SUMMARYDATAPOINT -_INTDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE -_INTDATAPOINT.fields_by_name['exemplars'].message_type = _INTEXEMPLAR _NUMBERDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -_NUMBERDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE _NUMBERDATAPOINT.fields_by_name['exemplars'].message_type = _EXEMPLAR _NUMBERDATAPOINT.oneofs_by_name['value'].fields.append( _NUMBERDATAPOINT.fields_by_name['as_double']) @@ -1133,44 +1020,48 @@ _NUMBERDATAPOINT.oneofs_by_name['value'].fields.append( _NUMBERDATAPOINT.fields_by_name['as_int']) _NUMBERDATAPOINT.fields_by_name['as_int'].containing_oneof = _NUMBERDATAPOINT.oneofs_by_name['value'] -_INTHISTOGRAMDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE -_INTHISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _INTEXEMPLAR _HISTOGRAMDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -_HISTOGRAMDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE _HISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _EXEMPLAR +_EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS.containing_type = _EXPONENTIALHISTOGRAMDATAPOINT +_EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE +_EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['positive'].message_type = _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS +_EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['negative'].message_type = _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS +_EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _EXEMPLAR _SUMMARYDATAPOINT_VALUEATQUANTILE.containing_type = _SUMMARYDATAPOINT _SUMMARYDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -_SUMMARYDATAPOINT.fields_by_name['labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE _SUMMARYDATAPOINT.fields_by_name['quantile_values'].message_type = _SUMMARYDATAPOINT_VALUEATQUANTILE -_INTEXEMPLAR.fields_by_name['filtered_labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE _EXEMPLAR.fields_by_name['filtered_attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -_EXEMPLAR.fields_by_name['filtered_labels'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._STRINGKEYVALUE _EXEMPLAR.oneofs_by_name['value'].fields.append( _EXEMPLAR.fields_by_name['as_double']) _EXEMPLAR.fields_by_name['as_double'].containing_oneof = _EXEMPLAR.oneofs_by_name['value'] _EXEMPLAR.oneofs_by_name['value'].fields.append( _EXEMPLAR.fields_by_name['as_int']) _EXEMPLAR.fields_by_name['as_int'].containing_oneof = _EXEMPLAR.oneofs_by_name['value'] +DESCRIPTOR.message_types_by_name['MetricsData'] = _METRICSDATA DESCRIPTOR.message_types_by_name['ResourceMetrics'] = _RESOURCEMETRICS DESCRIPTOR.message_types_by_name['InstrumentationLibraryMetrics'] = _INSTRUMENTATIONLIBRARYMETRICS DESCRIPTOR.message_types_by_name['Metric'] = _METRIC -DESCRIPTOR.message_types_by_name['IntGauge'] = _INTGAUGE DESCRIPTOR.message_types_by_name['Gauge'] = _GAUGE -DESCRIPTOR.message_types_by_name['IntSum'] = _INTSUM DESCRIPTOR.message_types_by_name['Sum'] = _SUM -DESCRIPTOR.message_types_by_name['IntHistogram'] = _INTHISTOGRAM DESCRIPTOR.message_types_by_name['Histogram'] = _HISTOGRAM +DESCRIPTOR.message_types_by_name['ExponentialHistogram'] = _EXPONENTIALHISTOGRAM DESCRIPTOR.message_types_by_name['Summary'] = _SUMMARY -DESCRIPTOR.message_types_by_name['IntDataPoint'] = _INTDATAPOINT DESCRIPTOR.message_types_by_name['NumberDataPoint'] = _NUMBERDATAPOINT -DESCRIPTOR.message_types_by_name['IntHistogramDataPoint'] = _INTHISTOGRAMDATAPOINT DESCRIPTOR.message_types_by_name['HistogramDataPoint'] = _HISTOGRAMDATAPOINT +DESCRIPTOR.message_types_by_name['ExponentialHistogramDataPoint'] = _EXPONENTIALHISTOGRAMDATAPOINT DESCRIPTOR.message_types_by_name['SummaryDataPoint'] = _SUMMARYDATAPOINT -DESCRIPTOR.message_types_by_name['IntExemplar'] = _INTEXEMPLAR DESCRIPTOR.message_types_by_name['Exemplar'] = _EXEMPLAR DESCRIPTOR.enum_types_by_name['AggregationTemporality'] = _AGGREGATIONTEMPORALITY +DESCRIPTOR.enum_types_by_name['DataPointFlags'] = _DATAPOINTFLAGS _sym_db.RegisterFileDescriptor(DESCRIPTOR) +MetricsData = _reflection.GeneratedProtocolMessageType('MetricsData', (_message.Message,), { + 'DESCRIPTOR' : _METRICSDATA, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.MetricsData) + }) +_sym_db.RegisterMessage(MetricsData) + ResourceMetrics = _reflection.GeneratedProtocolMessageType('ResourceMetrics', (_message.Message,), { 'DESCRIPTOR' : _RESOURCEMETRICS, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' @@ -1192,13 +1083,6 @@ }) _sym_db.RegisterMessage(Metric) -IntGauge = _reflection.GeneratedProtocolMessageType('IntGauge', (_message.Message,), { - 'DESCRIPTOR' : _INTGAUGE, - '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.IntGauge) - }) -_sym_db.RegisterMessage(IntGauge) - Gauge = _reflection.GeneratedProtocolMessageType('Gauge', (_message.Message,), { 'DESCRIPTOR' : _GAUGE, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' @@ -1206,13 +1090,6 @@ }) _sym_db.RegisterMessage(Gauge) -IntSum = _reflection.GeneratedProtocolMessageType('IntSum', (_message.Message,), { - 'DESCRIPTOR' : _INTSUM, - '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.IntSum) - }) -_sym_db.RegisterMessage(IntSum) - Sum = _reflection.GeneratedProtocolMessageType('Sum', (_message.Message,), { 'DESCRIPTOR' : _SUM, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' @@ -1220,13 +1097,6 @@ }) _sym_db.RegisterMessage(Sum) -IntHistogram = _reflection.GeneratedProtocolMessageType('IntHistogram', (_message.Message,), { - 'DESCRIPTOR' : _INTHISTOGRAM, - '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.IntHistogram) - }) -_sym_db.RegisterMessage(IntHistogram) - Histogram = _reflection.GeneratedProtocolMessageType('Histogram', (_message.Message,), { 'DESCRIPTOR' : _HISTOGRAM, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' @@ -1234,6 +1104,13 @@ }) _sym_db.RegisterMessage(Histogram) +ExponentialHistogram = _reflection.GeneratedProtocolMessageType('ExponentialHistogram', (_message.Message,), { + 'DESCRIPTOR' : _EXPONENTIALHISTOGRAM, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.ExponentialHistogram) + }) +_sym_db.RegisterMessage(ExponentialHistogram) + Summary = _reflection.GeneratedProtocolMessageType('Summary', (_message.Message,), { 'DESCRIPTOR' : _SUMMARY, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' @@ -1241,13 +1118,6 @@ }) _sym_db.RegisterMessage(Summary) -IntDataPoint = _reflection.GeneratedProtocolMessageType('IntDataPoint', (_message.Message,), { - 'DESCRIPTOR' : _INTDATAPOINT, - '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.IntDataPoint) - }) -_sym_db.RegisterMessage(IntDataPoint) - NumberDataPoint = _reflection.GeneratedProtocolMessageType('NumberDataPoint', (_message.Message,), { 'DESCRIPTOR' : _NUMBERDATAPOINT, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' @@ -1255,13 +1125,6 @@ }) _sym_db.RegisterMessage(NumberDataPoint) -IntHistogramDataPoint = _reflection.GeneratedProtocolMessageType('IntHistogramDataPoint', (_message.Message,), { - 'DESCRIPTOR' : _INTHISTOGRAMDATAPOINT, - '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.IntHistogramDataPoint) - }) -_sym_db.RegisterMessage(IntHistogramDataPoint) - HistogramDataPoint = _reflection.GeneratedProtocolMessageType('HistogramDataPoint', (_message.Message,), { 'DESCRIPTOR' : _HISTOGRAMDATAPOINT, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' @@ -1269,6 +1132,21 @@ }) _sym_db.RegisterMessage(HistogramDataPoint) +ExponentialHistogramDataPoint = _reflection.GeneratedProtocolMessageType('ExponentialHistogramDataPoint', (_message.Message,), { + + 'Buckets' : _reflection.GeneratedProtocolMessageType('Buckets', (_message.Message,), { + 'DESCRIPTOR' : _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets) + }) + , + 'DESCRIPTOR' : _EXPONENTIALHISTOGRAMDATAPOINT, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint) + }) +_sym_db.RegisterMessage(ExponentialHistogramDataPoint) +_sym_db.RegisterMessage(ExponentialHistogramDataPoint.Buckets) + SummaryDataPoint = _reflection.GeneratedProtocolMessageType('SummaryDataPoint', (_message.Message,), { 'ValueAtQuantile' : _reflection.GeneratedProtocolMessageType('ValueAtQuantile', (_message.Message,), { @@ -1284,13 +1162,6 @@ _sym_db.RegisterMessage(SummaryDataPoint) _sym_db.RegisterMessage(SummaryDataPoint.ValueAtQuantile) -IntExemplar = _reflection.GeneratedProtocolMessageType('IntExemplar', (_message.Message,), { - 'DESCRIPTOR' : _INTEXEMPLAR, - '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.IntExemplar) - }) -_sym_db.RegisterMessage(IntExemplar) - Exemplar = _reflection.GeneratedProtocolMessageType('Exemplar', (_message.Message,), { 'DESCRIPTOR' : _EXEMPLAR, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' @@ -1300,17 +1171,4 @@ DESCRIPTOR._options = None -_METRIC.fields_by_name['int_gauge']._options = None -_METRIC.fields_by_name['int_sum']._options = None -_METRIC.fields_by_name['int_histogram']._options = None -_INTGAUGE._options = None -_INTSUM._options = None -_INTHISTOGRAM._options = None -_INTDATAPOINT._options = None -_NUMBERDATAPOINT.fields_by_name['labels']._options = None -_INTHISTOGRAMDATAPOINT._options = None -_HISTOGRAMDATAPOINT.fields_by_name['labels']._options = None -_SUMMARYDATAPOINT.fields_by_name['labels']._options = None -_INTEXEMPLAR._options = None -_EXEMPLAR.fields_by_name['filtered_labels']._options = None # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi index 4fb9c9ff93..7ee8a16f13 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi @@ -164,6 +164,67 @@ value was reset (e.g. Prometheus). global___AggregationTemporality = AggregationTemporality +class DataPointFlags(_DataPointFlags, metaclass=_DataPointFlagsEnumTypeWrapper): + """DataPointFlags is defined as a protobuf 'uint32' type and is to be used as a + bit-field representing 32 distinct boolean flags. Each flag defined in this + enum is a bit-mask. To test the presence of a single flag in the flags of + a data point, for example, use an expression like: + + (point.flags & FLAG_NO_RECORDED_VALUE) == FLAG_NO_RECORDED_VALUE + """ + pass +class _DataPointFlags: + V = typing.NewType('V', builtins.int) +class _DataPointFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_DataPointFlags.V], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... + FLAG_NONE = DataPointFlags.V(0) + FLAG_NO_RECORDED_VALUE = DataPointFlags.V(1) + """This DataPoint is valid but has no recorded value. This value + SHOULD be used to reflect explicitly missing data in a series, as + for an equivalent to the Prometheus "staleness marker". + """ + + +FLAG_NONE = DataPointFlags.V(0) +FLAG_NO_RECORDED_VALUE = DataPointFlags.V(1) +"""This DataPoint is valid but has no recorded value. This value +SHOULD be used to reflect explicitly missing data in a series, as +for an equivalent to the Prometheus "staleness marker". +""" + +global___DataPointFlags = DataPointFlags + + +class MetricsData(google.protobuf.message.Message): + """MetricsData represents the metrics data that can be stored in a persistent + storage, OR can be embedded by other protocols that transfer OTLP metrics + data but do not implement the OTLP protocol. + + The main difference between this message and collector protocol is that + in this message there will not be any "control" or "metadata" specific to + OTLP protocol. + + When new fields are added into this message, the OTLP request MUST be updated + as well. + """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + RESOURCE_METRICS_FIELD_NUMBER: builtins.int + @property + def resource_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResourceMetrics]: + """An array of ResourceMetrics. + For data coming from a single resource this array will typically contain + one element. Intermediary nodes that receive data from multiple origins + typically batch the data before forwarding further and in that case this + array will contain multiple elements. + """ + pass + def __init__(self, + *, + resource_metrics : typing.Optional[typing.Iterable[global___ResourceMetrics]] = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["resource_metrics",b"resource_metrics"]) -> None: ... +global___MetricsData = MetricsData + class ResourceMetrics(google.protobuf.message.Message): """A collection of InstrumentationLibraryMetrics from a Resource.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... @@ -317,12 +378,10 @@ class Metric(google.protobuf.message.Message): NAME_FIELD_NUMBER: builtins.int DESCRIPTION_FIELD_NUMBER: builtins.int UNIT_FIELD_NUMBER: builtins.int - INT_GAUGE_FIELD_NUMBER: builtins.int GAUGE_FIELD_NUMBER: builtins.int - INT_SUM_FIELD_NUMBER: builtins.int SUM_FIELD_NUMBER: builtins.int - INT_HISTOGRAM_FIELD_NUMBER: builtins.int HISTOGRAM_FIELD_NUMBER: builtins.int + EXPONENTIAL_HISTOGRAM_FIELD_NUMBER: builtins.int SUMMARY_FIELD_NUMBER: builtins.int name: typing.Text = ... """name of the metric, including its DNS name prefix. It must be unique.""" @@ -335,85 +394,34 @@ class Metric(google.protobuf.message.Message): described by http://unitsofmeasure.org/ucum.html. """ - @property - def int_gauge(self) -> global___IntGauge: - """IntGauge and IntSum are deprecated and will be removed soon. - 1. Old senders and receivers that are not aware of this change will - continue using the `int_gauge` and `int_sum` fields. - 2. New senders, which are aware of this change MUST send only `gauge` - and `sum` fields. - 3. New receivers, which are aware of this change MUST convert these into - `gauge` and `sum` by using the provided as_int field in the oneof values. - This field will be removed in ~3 months, on July 1, 2021. - """ - pass @property def gauge(self) -> global___Gauge: ... @property - def int_sum(self) -> global___IntSum: - """This field will be removed in ~3 months, on July 1, 2021.""" - pass - @property def sum(self) -> global___Sum: ... @property - def int_histogram(self) -> global___IntHistogram: - """IntHistogram is deprecated and will be removed soon. - 1. Old senders and receivers that are not aware of this change will - continue using the `int_histogram` field. - 2. New senders, which are aware of this change MUST send only `histogram`. - 3. New receivers, which are aware of this change MUST convert this into - `histogram` by simply converting all int64 values into float. - This field will be removed in ~3 months, on July 1, 2021. - """ - pass - @property def histogram(self) -> global___Histogram: ... @property + def exponential_histogram(self) -> global___ExponentialHistogram: ... + @property def summary(self) -> global___Summary: ... def __init__(self, *, name : typing.Text = ..., description : typing.Text = ..., unit : typing.Text = ..., - int_gauge : typing.Optional[global___IntGauge] = ..., gauge : typing.Optional[global___Gauge] = ..., - int_sum : typing.Optional[global___IntSum] = ..., sum : typing.Optional[global___Sum] = ..., - int_histogram : typing.Optional[global___IntHistogram] = ..., histogram : typing.Optional[global___Histogram] = ..., + exponential_histogram : typing.Optional[global___ExponentialHistogram] = ..., summary : typing.Optional[global___Summary] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["data",b"data","gauge",b"gauge","histogram",b"histogram","int_gauge",b"int_gauge","int_histogram",b"int_histogram","int_sum",b"int_sum","sum",b"sum","summary",b"summary"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["data",b"data","description",b"description","gauge",b"gauge","histogram",b"histogram","int_gauge",b"int_gauge","int_histogram",b"int_histogram","int_sum",b"int_sum","name",b"name","sum",b"sum","summary",b"summary","unit",b"unit"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["data",b"data"]) -> typing.Optional[typing_extensions.Literal["int_gauge","gauge","int_sum","sum","int_histogram","histogram","summary"]]: ... + def HasField(self, field_name: typing_extensions.Literal["data",b"data","exponential_histogram",b"exponential_histogram","gauge",b"gauge","histogram",b"histogram","sum",b"sum","summary",b"summary"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["data",b"data","description",b"description","exponential_histogram",b"exponential_histogram","gauge",b"gauge","histogram",b"histogram","name",b"name","sum",b"sum","summary",b"summary","unit",b"unit"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["data",b"data"]) -> typing.Optional[typing_extensions.Literal["gauge","sum","histogram","exponential_histogram","summary"]]: ... global___Metric = Metric -class IntGauge(google.protobuf.message.Message): - """IntGauge is deprecated. Use Gauge with an integer value in NumberDataPoint. - - IntGauge represents the type of a int scalar metric that always exports the - "current value" for every data point. It should be used for an "unknown" - aggregation. - - A Gauge does not support different aggregation temporalities. Given the - aggregation is unknown, points cannot be combined using the same - aggregation, regardless of aggregation temporalities. Therefore, - AggregationTemporality is not included. Consequently, this also means - "StartTimeUnixNano" is ignored for all data points. - """ - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - DATA_POINTS_FIELD_NUMBER: builtins.int - @property - def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntDataPoint]: ... - def __init__(self, - *, - data_points : typing.Optional[typing.Iterable[global___IntDataPoint]] = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["data_points",b"data_points"]) -> None: ... -global___IntGauge = IntGauge - class Gauge(google.protobuf.message.Message): - """Gauge represents the type of a double scalar metric that always exports the + """Gauge represents the type of a scalar metric that always exports the "current value" for every data point. It should be used for an "unknown" aggregation. @@ -434,38 +442,9 @@ class Gauge(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal["data_points",b"data_points"]) -> None: ... global___Gauge = Gauge -class IntSum(google.protobuf.message.Message): - """IntSum is deprecated. Use Sum with an integer value in NumberDataPoint. - - IntSum represents the type of a numeric int scalar metric that is calculated as - a sum of all reported measurements over a time interval. - """ - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - DATA_POINTS_FIELD_NUMBER: builtins.int - AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int - IS_MONOTONIC_FIELD_NUMBER: builtins.int - @property - def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntDataPoint]: ... - aggregation_temporality: global___AggregationTemporality.V = ... - """aggregation_temporality describes if the aggregator reports delta changes - since last report time, or cumulative changes since a fixed start time. - """ - - is_monotonic: builtins.bool = ... - """If "true" means that the sum is monotonic.""" - - def __init__(self, - *, - data_points : typing.Optional[typing.Iterable[global___IntDataPoint]] = ..., - aggregation_temporality : global___AggregationTemporality.V = ..., - is_monotonic : builtins.bool = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality",b"aggregation_temporality","data_points",b"data_points","is_monotonic",b"is_monotonic"]) -> None: ... -global___IntSum = IntSum - class Sum(google.protobuf.message.Message): - """Sum represents the type of a numeric double scalar metric that is calculated - as a sum of all reported measurements over a time interval. + """Sum represents the type of a scalar metric that is calculated as a sum of all + reported measurements over a time interval. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int @@ -490,18 +469,15 @@ class Sum(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality",b"aggregation_temporality","data_points",b"data_points","is_monotonic",b"is_monotonic"]) -> None: ... global___Sum = Sum -class IntHistogram(google.protobuf.message.Message): - """IntHistogram is deprecated, replaced by Histogram points using double- - valued exemplars. - - This represents the type of a metric that is calculated by aggregating as a - Histogram of all reported int measurements over a time interval. +class Histogram(google.protobuf.message.Message): + """Histogram represents the type of a metric that is calculated by aggregating + as a Histogram of all reported measurements over a time interval. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int @property - def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntHistogramDataPoint]: ... + def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___HistogramDataPoint]: ... aggregation_temporality: global___AggregationTemporality.V = ... """aggregation_temporality describes if the aggregator reports delta changes since last report time, or cumulative changes since a fixed start time. @@ -509,21 +485,21 @@ class IntHistogram(google.protobuf.message.Message): def __init__(self, *, - data_points : typing.Optional[typing.Iterable[global___IntHistogramDataPoint]] = ..., + data_points : typing.Optional[typing.Iterable[global___HistogramDataPoint]] = ..., aggregation_temporality : global___AggregationTemporality.V = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality",b"aggregation_temporality","data_points",b"data_points"]) -> None: ... -global___IntHistogram = IntHistogram +global___Histogram = Histogram -class Histogram(google.protobuf.message.Message): - """Histogram represents the type of a metric that is calculated by aggregating - as a Histogram of all reported double measurements over a time interval. +class ExponentialHistogram(google.protobuf.message.Message): + """ExponentialHistogram represents the type of a metric that is calculated by aggregating + as a ExponentialHistogram of all reported double measurements over a time interval. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... DATA_POINTS_FIELD_NUMBER: builtins.int AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int @property - def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___HistogramDataPoint]: ... + def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ExponentialHistogramDataPoint]: ... aggregation_temporality: global___AggregationTemporality.V = ... """aggregation_temporality describes if the aggregator reports delta changes since last report time, or cumulative changes since a fixed start time. @@ -531,11 +507,11 @@ class Histogram(google.protobuf.message.Message): def __init__(self, *, - data_points : typing.Optional[typing.Iterable[global___HistogramDataPoint]] = ..., + data_points : typing.Optional[typing.Iterable[global___ExponentialHistogramDataPoint]] = ..., aggregation_temporality : global___AggregationTemporality.V = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality",b"aggregation_temporality","data_points",b"data_points"]) -> None: ... -global___Histogram = Histogram +global___ExponentialHistogram = ExponentialHistogram class Summary(google.protobuf.message.Message): """Summary metric data are used to convey quantile summaries, @@ -556,88 +532,27 @@ class Summary(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal["data_points",b"data_points"]) -> None: ... global___Summary = Summary -class IntDataPoint(google.protobuf.message.Message): - """IntDataPoint is a single data point in a timeseries that describes the - time-varying values of a int64 metric. - """ - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - LABELS_FIELD_NUMBER: builtins.int - START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int - EXEMPLARS_FIELD_NUMBER: builtins.int - @property - def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: - """The set of labels that uniquely identify this timeseries.""" - pass - start_time_unix_nano: builtins.int = ... - """StartTimeUnixNano is optional but strongly encouraged, see the - the detiled comments above Metric. - - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - 1970. - """ - - time_unix_nano: builtins.int = ... - """TimeUnixNano is required, see the detailed comments above Metric. - - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - 1970. - """ - - value: builtins.int = ... - """value itself.""" - - @property - def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntExemplar]: - """(Optional) List of exemplars collected from - measurements that were used to form the data point - """ - pass - def __init__(self, - *, - labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., - start_time_unix_nano : builtins.int = ..., - time_unix_nano : builtins.int = ..., - value : builtins.int = ..., - exemplars : typing.Optional[typing.Iterable[global___IntExemplar]] = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["exemplars",b"exemplars","labels",b"labels","start_time_unix_nano",b"start_time_unix_nano","time_unix_nano",b"time_unix_nano","value",b"value"]) -> None: ... -global___IntDataPoint = IntDataPoint - class NumberDataPoint(google.protobuf.message.Message): """NumberDataPoint is a single data point in a timeseries that describes the - time-varying value of a double metric. + time-varying scalar value of a metric. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... ATTRIBUTES_FIELD_NUMBER: builtins.int - LABELS_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int AS_DOUBLE_FIELD_NUMBER: builtins.int AS_INT_FIELD_NUMBER: builtins.int EXEMPLARS_FIELD_NUMBER: builtins.int + FLAGS_FIELD_NUMBER: builtins.int @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that uniquely identify the timeseries from where this point belongs. The list may be empty (may contain 0 elements). """ pass - @property - def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: - """Labels is deprecated and will be removed soon. - 1. Old senders and receivers that are not aware of this change will - continue using the `labels` field. - 2. New senders, which are aware of this change MUST send only `attributes`. - 3. New receivers, which are aware of this change MUST convert this into - `labels` by simply converting all int64 values into float. - - This field will be removed in ~3 months, on July 1, 2021. - """ - pass start_time_unix_nano: builtins.int = ... """StartTimeUnixNano is optional but strongly encouraged, see the - the detiled comments above Metric. + the detailed comments above Metric. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. @@ -658,28 +573,31 @@ class NumberDataPoint(google.protobuf.message.Message): measurements that were used to form the data point """ pass + flags: builtins.int = ... + """Flags that apply to this specific data point. See DataPointFlags + for the available flags and their meaning. + """ + def __init__(self, *, attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., - labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., start_time_unix_nano : builtins.int = ..., time_unix_nano : builtins.int = ..., as_double : builtins.float = ..., as_int : builtins.int = ..., exemplars : typing.Optional[typing.Iterable[global___Exemplar]] = ..., + flags : builtins.int = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["as_double",b"as_double","as_int",b"as_int","value",b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["as_double",b"as_double","as_int",b"as_int","attributes",b"attributes","exemplars",b"exemplars","labels",b"labels","start_time_unix_nano",b"start_time_unix_nano","time_unix_nano",b"time_unix_nano","value",b"value"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["as_double",b"as_double","as_int",b"as_int","attributes",b"attributes","exemplars",b"exemplars","flags",b"flags","start_time_unix_nano",b"start_time_unix_nano","time_unix_nano",b"time_unix_nano","value",b"value"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["value",b"value"]) -> typing.Optional[typing_extensions.Literal["as_double","as_int"]]: ... global___NumberDataPoint = NumberDataPoint -class IntHistogramDataPoint(google.protobuf.message.Message): - """IntHistogramDataPoint is deprecated; use HistogramDataPoint. - - This is a single data point in a timeseries that describes - the time-varying values of a Histogram of int values. A Histogram contains - summary statistics for a population of values, it may optionally contain - the distribution of those values across a set of buckets. +class HistogramDataPoint(google.protobuf.message.Message): + """HistogramDataPoint is a single data point in a timeseries that describes the + time-varying values of a Histogram. A Histogram contains summary statistics + for a population of values, it may optionally contain the distribution of + those values across a set of buckets. If the histogram contains the distribution of values, then both "explicit_bounds" and "bucket counts" fields must be defined. @@ -688,7 +606,7 @@ class IntHistogramDataPoint(google.protobuf.message.Message): "sum" are known. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - LABELS_FIELD_NUMBER: builtins.int + ATTRIBUTES_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int COUNT_FIELD_NUMBER: builtins.int @@ -696,13 +614,16 @@ class IntHistogramDataPoint(google.protobuf.message.Message): BUCKET_COUNTS_FIELD_NUMBER: builtins.int EXPLICIT_BOUNDS_FIELD_NUMBER: builtins.int EXEMPLARS_FIELD_NUMBER: builtins.int + FLAGS_FIELD_NUMBER: builtins.int @property - def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: - """The set of labels that uniquely identify this timeseries.""" + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: + """The set of key/value pairs that uniquely identify the timeseries from + where this point belongs. The list may be empty (may contain 0 elements). + """ pass start_time_unix_nano: builtins.int = ... """StartTimeUnixNano is optional but strongly encouraged, see the - the detiled comments above Metric. + the detailed comments above Metric. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. @@ -721,10 +642,15 @@ class IntHistogramDataPoint(google.protobuf.message.Message): histogram is provided. """ - sum: builtins.int = ... + sum: builtins.float = ... """sum of the values in the population. If count is zero then this field - must be zero. This value must be equal to the sum of the "sum" fields in - buckets if a histogram is provided. + must be zero. + + Note: Sum should only be filled out when measuring non-negative discrete + events, and is assumed to be monotonic over the values of these events. + Negative events *can* be recorded, but sum should not be filled out when + doing so. This is specifically to enforce compatibility w/ OpenMetrics, + see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#histogram """ @property @@ -742,12 +668,11 @@ class IntHistogramDataPoint(google.protobuf.message.Message): def explicit_bounds(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]: """explicit_bounds specifies buckets with explicitly defined bounds for values. - This defines size(explicit_bounds) + 1 (= N) buckets. The boundaries for - bucket at index i are: + The boundaries for bucket at index i are: (-infinity, explicit_bounds[i]] for i == 0 - (explicit_bounds[i-1], explicit_bounds[i]] for 0 < i < N-1 - (explicit_bounds[i], +infinity) for i == N-1 + (explicit_bounds[i-1], explicit_bounds[i]] for 0 < i < size(explicit_bounds) + (explicit_bounds[i-1], +infinity) for i == size(explicit_bounds) The values in the explicit_bounds array must be strictly increasing. @@ -757,46 +682,81 @@ class IntHistogramDataPoint(google.protobuf.message.Message): """ pass @property - def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___IntExemplar]: + def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: """(Optional) List of exemplars collected from measurements that were used to form the data point """ pass + flags: builtins.int = ... + """Flags that apply to this specific data point. See DataPointFlags + for the available flags and their meaning. + """ + def __init__(self, *, - labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., + attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., start_time_unix_nano : builtins.int = ..., time_unix_nano : builtins.int = ..., count : builtins.int = ..., - sum : builtins.int = ..., + sum : builtins.float = ..., bucket_counts : typing.Optional[typing.Iterable[builtins.int]] = ..., explicit_bounds : typing.Optional[typing.Iterable[builtins.float]] = ..., - exemplars : typing.Optional[typing.Iterable[global___IntExemplar]] = ..., + exemplars : typing.Optional[typing.Iterable[global___Exemplar]] = ..., + flags : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["bucket_counts",b"bucket_counts","count",b"count","exemplars",b"exemplars","explicit_bounds",b"explicit_bounds","labels",b"labels","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano"]) -> None: ... -global___IntHistogramDataPoint = IntHistogramDataPoint + def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","bucket_counts",b"bucket_counts","count",b"count","exemplars",b"exemplars","explicit_bounds",b"explicit_bounds","flags",b"flags","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano"]) -> None: ... +global___HistogramDataPoint = HistogramDataPoint -class HistogramDataPoint(google.protobuf.message.Message): - """HistogramDataPoint is a single data point in a timeseries that describes the - time-varying values of a Histogram of double values. A Histogram contains +class ExponentialHistogramDataPoint(google.protobuf.message.Message): + """ExponentialHistogramDataPoint is a single data point in a timeseries that describes the + time-varying values of a ExponentialHistogram of double values. A ExponentialHistogram contains summary statistics for a population of values, it may optionally contain the distribution of those values across a set of buckets. - - If the histogram contains the distribution of values, then both - "explicit_bounds" and "bucket counts" fields must be defined. - If the histogram does not contain the distribution of values, then both - "explicit_bounds" and "bucket_counts" must be omitted and only "count" and - "sum" are known. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + class Buckets(google.protobuf.message.Message): + """Buckets are a set of bucket counts, encoded in a contiguous array + of counts. + """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + OFFSET_FIELD_NUMBER: builtins.int + BUCKET_COUNTS_FIELD_NUMBER: builtins.int + offset: builtins.int = ... + """Offset is the bucket index of the first entry in the bucket_counts array. + + Note: This uses a varint encoding as a simple form of compression. + """ + + @property + def bucket_counts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: + """Count is an array of counts, where count[i] carries the count + of the bucket at index (offset+i). count[i] is the count of + values greater than or equal to base^(offset+i) and less than + base^(offset+i+1). + + Note: By contrast, the explicit HistogramDataPoint uses + fixed64. This field is expected to have many buckets, + especially zeros, so uint64 has been selected to ensure + varint encoding. + """ + pass + def __init__(self, + *, + offset : builtins.int = ..., + bucket_counts : typing.Optional[typing.Iterable[builtins.int]] = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["bucket_counts",b"bucket_counts","offset",b"offset"]) -> None: ... + ATTRIBUTES_FIELD_NUMBER: builtins.int - LABELS_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int COUNT_FIELD_NUMBER: builtins.int SUM_FIELD_NUMBER: builtins.int - BUCKET_COUNTS_FIELD_NUMBER: builtins.int - EXPLICIT_BOUNDS_FIELD_NUMBER: builtins.int + SCALE_FIELD_NUMBER: builtins.int + ZERO_COUNT_FIELD_NUMBER: builtins.int + POSITIVE_FIELD_NUMBER: builtins.int + NEGATIVE_FIELD_NUMBER: builtins.int + FLAGS_FIELD_NUMBER: builtins.int EXEMPLARS_FIELD_NUMBER: builtins.int @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: @@ -804,21 +764,9 @@ class HistogramDataPoint(google.protobuf.message.Message): where this point belongs. The list may be empty (may contain 0 elements). """ pass - @property - def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: - """Labels is deprecated and will be removed soon. - 1. Old senders and receivers that are not aware of this change will - continue using the `labels` field. - 2. New senders, which are aware of this change MUST send only `attributes`. - 3. New receivers, which are aware of this change MUST convert this into - `labels` by simply converting all int64 values into float. - - This field will be removed in ~3 months, on July 1, 2021. - """ - pass start_time_unix_nano: builtins.int = ... """StartTimeUnixNano is optional but strongly encouraged, see the - the detiled comments above Metric. + the detailed comments above Metric. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. @@ -832,15 +780,14 @@ class HistogramDataPoint(google.protobuf.message.Message): """ count: builtins.int = ... - """count is the number of values in the population. Must be non-negative. This - value must be equal to the sum of the "count" fields in buckets if a - histogram is provided. + """count is the number of values in the population. Must be + non-negative. This value must be equal to the sum of the "bucket_counts" + values in the positive and negative Buckets plus the "zero_count" field. """ sum: builtins.float = ... """sum of the values in the population. If count is zero then this field - must be zero. This value must be equal to the sum of the "sum" fields in - buckets if a histogram is provided. + must be zero. Note: Sum should only be filled out when measuring non-negative discrete events, and is assumed to be monotonic over the values of these events. @@ -849,35 +796,48 @@ class HistogramDataPoint(google.protobuf.message.Message): see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#histogram """ - @property - def bucket_counts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: - """bucket_counts is an optional field contains the count values of histogram - for each bucket. + scale: builtins.int = ... + """scale describes the resolution of the histogram. Boundaries are + located at powers of the base, where: - The sum of the bucket_counts must equal the value in the count field. + base = (2^(2^-scale)) - The number of elements in bucket_counts array must be by one greater than - the number of elements in explicit_bounds array. - """ - pass - @property - def explicit_bounds(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]: - """explicit_bounds specifies buckets with explicitly defined bounds for values. + The histogram bucket identified by `index`, a signed integer, + contains values that are greater than or equal to (base^index) and + less than (base^(index+1)). - This defines size(explicit_bounds) + 1 (= N) buckets. The boundaries for - bucket at index i are: + The positive and negative ranges of the histogram are expressed + separately. Negative values are mapped by their absolute value + into the negative range using the same scale as the positive range. - (-infinity, explicit_bounds[i]] for i == 0 - (explicit_bounds[i-1], explicit_bounds[i]] for 0 < i < N-1 - (explicit_bounds[i], +infinity) for i == N-1 + scale is not restricted by the protocol, as the permissible + values depend on the range of the data. + """ - The values in the explicit_bounds array must be strictly increasing. + zero_count: builtins.int = ... + """zero_count is the count of values that are either exactly zero or + within the region considered zero by the instrumentation at the + tolerated degree of precision. This bucket stores values that + cannot be expressed using the standard exponential formula as + well as values that have been rounded to zero. - Histogram buckets are inclusive of their upper boundary, except the last - bucket where the boundary is at infinity. This format is intentionally - compatible with the OpenMetrics histogram definition. - """ + Implementations MAY consider the zero bucket to have probability + mass equal to (zero_count / count). + """ + + @property + def positive(self) -> global___ExponentialHistogramDataPoint.Buckets: + """positive carries the positive range of exponential bucket counts.""" pass + @property + def negative(self) -> global___ExponentialHistogramDataPoint.Buckets: + """negative carries the negative range of exponential bucket counts.""" + pass + flags: builtins.int = ... + """Flags that apply to this specific data point. See DataPointFlags + for the available flags and their meaning. + """ + @property def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: """(Optional) List of exemplars collected from @@ -887,17 +847,20 @@ class HistogramDataPoint(google.protobuf.message.Message): def __init__(self, *, attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., - labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., start_time_unix_nano : builtins.int = ..., time_unix_nano : builtins.int = ..., count : builtins.int = ..., sum : builtins.float = ..., - bucket_counts : typing.Optional[typing.Iterable[builtins.int]] = ..., - explicit_bounds : typing.Optional[typing.Iterable[builtins.float]] = ..., + scale : builtins.int = ..., + zero_count : builtins.int = ..., + positive : typing.Optional[global___ExponentialHistogramDataPoint.Buckets] = ..., + negative : typing.Optional[global___ExponentialHistogramDataPoint.Buckets] = ..., + flags : builtins.int = ..., exemplars : typing.Optional[typing.Iterable[global___Exemplar]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","bucket_counts",b"bucket_counts","count",b"count","exemplars",b"exemplars","explicit_bounds",b"explicit_bounds","labels",b"labels","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano"]) -> None: ... -global___HistogramDataPoint = HistogramDataPoint + def HasField(self, field_name: typing_extensions.Literal["negative",b"negative","positive",b"positive"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","count",b"count","exemplars",b"exemplars","flags",b"flags","negative",b"negative","positive",b"positive","scale",b"scale","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano","zero_count",b"zero_count"]) -> None: ... +global___ExponentialHistogramDataPoint = ExponentialHistogramDataPoint class SummaryDataPoint(google.protobuf.message.Message): """SummaryDataPoint is a single data point in a timeseries that describes the @@ -936,33 +899,21 @@ class SummaryDataPoint(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal["quantile",b"quantile","value",b"value"]) -> None: ... ATTRIBUTES_FIELD_NUMBER: builtins.int - LABELS_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int COUNT_FIELD_NUMBER: builtins.int SUM_FIELD_NUMBER: builtins.int QUANTILE_VALUES_FIELD_NUMBER: builtins.int + FLAGS_FIELD_NUMBER: builtins.int @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that uniquely identify the timeseries from where this point belongs. The list may be empty (may contain 0 elements). """ pass - @property - def labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: - """Labels is deprecated and will be removed soon. - 1. Old senders and receivers that are not aware of this change will - continue using the `labels` field. - 2. New senders, which are aware of this change MUST send only `attributes`. - 3. New receivers, which are aware of this change MUST convert this into - `labels` by simply converting all int64 values into float. - - This field will be removed in ~3 months, on July 1, 2021. - """ - pass start_time_unix_nano: builtins.int = ... """StartTimeUnixNano is optional but strongly encouraged, see the - the detiled comments above Metric. + the detailed comments above Metric. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. @@ -995,71 +946,24 @@ class SummaryDataPoint(google.protobuf.message.Message): from the current snapshot. The quantiles must be strictly increasing. """ pass + flags: builtins.int = ... + """Flags that apply to this specific data point. See DataPointFlags + for the available flags and their meaning. + """ + def __init__(self, *, attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., - labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., start_time_unix_nano : builtins.int = ..., time_unix_nano : builtins.int = ..., count : builtins.int = ..., sum : builtins.float = ..., quantile_values : typing.Optional[typing.Iterable[global___SummaryDataPoint.ValueAtQuantile]] = ..., + flags : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","count",b"count","labels",b"labels","quantile_values",b"quantile_values","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","count",b"count","flags",b"flags","quantile_values",b"quantile_values","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano"]) -> None: ... global___SummaryDataPoint = SummaryDataPoint -class IntExemplar(google.protobuf.message.Message): - """A representation of an exemplar, which is a sample input int measurement. - Exemplars also hold information about the environment when the measurement - was recorded, for example the span and trace ID of the active span when the - exemplar was recorded. - """ - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - FILTERED_LABELS_FIELD_NUMBER: builtins.int - TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int - SPAN_ID_FIELD_NUMBER: builtins.int - TRACE_ID_FIELD_NUMBER: builtins.int - @property - def filtered_labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: - """The set of labels that were filtered out by the aggregator, but recorded - alongside the original measurement. Only labels that were filtered out - by the aggregator should be included - """ - pass - time_unix_nano: builtins.int = ... - """time_unix_nano is the exact time when this exemplar was recorded - - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - 1970. - """ - - value: builtins.int = ... - """Numerical int value of the measurement that was recorded.""" - - span_id: builtins.bytes = ... - """(Optional) Span ID of the exemplar trace. - span_id may be missing if the measurement is not recorded inside a trace - or if the trace is not sampled. - """ - - trace_id: builtins.bytes = ... - """(Optional) Trace ID of the exemplar trace. - trace_id may be missing if the measurement is not recorded inside a trace - or if the trace is not sampled. - """ - - def __init__(self, - *, - filtered_labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., - time_unix_nano : builtins.int = ..., - value : builtins.int = ..., - span_id : builtins.bytes = ..., - trace_id : builtins.bytes = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["filtered_labels",b"filtered_labels","span_id",b"span_id","time_unix_nano",b"time_unix_nano","trace_id",b"trace_id","value",b"value"]) -> None: ... -global___IntExemplar = IntExemplar - class Exemplar(google.protobuf.message.Message): """A representation of an exemplar, which is a sample input measurement. Exemplars also hold information about the environment when the measurement @@ -1068,7 +972,6 @@ class Exemplar(google.protobuf.message.Message): """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... FILTERED_ATTRIBUTES_FIELD_NUMBER: builtins.int - FILTERED_LABELS_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int AS_DOUBLE_FIELD_NUMBER: builtins.int AS_INT_FIELD_NUMBER: builtins.int @@ -1081,19 +984,6 @@ class Exemplar(google.protobuf.message.Message): filtered out by the aggregator should be included """ pass - @property - def filtered_labels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]: - """Labels is deprecated and will be removed soon. - 1. Old senders and receivers that are not aware of this change will - continue using the `filtered_labels` field. - 2. New senders, which are aware of this change MUST send only - `filtered_attributes`. - 3. New receivers, which are aware of this change MUST convert this into - `filtered_labels` by simply converting all int64 values into float. - - This field will be removed in ~3 months, on July 1, 2021. - """ - pass time_unix_nano: builtins.int = ... """time_unix_nano is the exact time when this exemplar was recorded @@ -1118,7 +1008,6 @@ class Exemplar(google.protobuf.message.Message): def __init__(self, *, filtered_attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., - filtered_labels : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.StringKeyValue]] = ..., time_unix_nano : builtins.int = ..., as_double : builtins.float = ..., as_int : builtins.int = ..., @@ -1126,6 +1015,6 @@ class Exemplar(google.protobuf.message.Message): trace_id : builtins.bytes = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["as_double",b"as_double","as_int",b"as_int","value",b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["as_double",b"as_double","as_int",b"as_int","filtered_attributes",b"filtered_attributes","filtered_labels",b"filtered_labels","span_id",b"span_id","time_unix_nano",b"time_unix_nano","trace_id",b"trace_id","value",b"value"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["as_double",b"as_double","as_int",b"as_int","filtered_attributes",b"filtered_attributes","span_id",b"span_id","time_unix_nano",b"time_unix_nano","trace_id",b"trace_id","value",b"value"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["value",b"value"]) -> typing.Optional[typing_extensions.Literal["as_double","as_int"]]: ... global___Exemplar = Exemplar diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py index d46196bcf5..b231fd0d14 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py @@ -21,7 +21,7 @@ syntax='proto3', serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xc2\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xbc\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xdd\x07\n\x06Status\x12V\n\x0f\x64\x65precated_code\x18\x01 \x01(\x0e\x32\x39.opentelemetry.proto.trace.v1.Status.DeprecatedStatusCodeB\x02\x18\x01\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"\xda\x05\n\x14\x44\x65precatedStatusCode\x12\x1d\n\x19\x44\x45PRECATED_STATUS_CODE_OK\x10\x00\x12$\n DEPRECATED_STATUS_CODE_CANCELLED\x10\x01\x12(\n$DEPRECATED_STATUS_CODE_UNKNOWN_ERROR\x10\x02\x12+\n\'DEPRECATED_STATUS_CODE_INVALID_ARGUMENT\x10\x03\x12,\n(DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED\x10\x04\x12$\n DEPRECATED_STATUS_CODE_NOT_FOUND\x10\x05\x12)\n%DEPRECATED_STATUS_CODE_ALREADY_EXISTS\x10\x06\x12,\n(DEPRECATED_STATUS_CODE_PERMISSION_DENIED\x10\x07\x12-\n)DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED\x10\x08\x12.\n*DEPRECATED_STATUS_CODE_FAILED_PRECONDITION\x10\t\x12\"\n\x1e\x44\x45PRECATED_STATUS_CODE_ABORTED\x10\n\x12\'\n#DEPRECATED_STATUS_CODE_OUT_OF_RANGE\x10\x0b\x12(\n$DEPRECATED_STATUS_CODE_UNIMPLEMENTED\x10\x0c\x12)\n%DEPRECATED_STATUS_CODE_INTERNAL_ERROR\x10\r\x12&\n\"DEPRECATED_STATUS_CODE_UNAVAILABLE\x10\x0e\x12$\n DEPRECATED_STATUS_CODE_DATA_LOSS\x10\x0f\x12*\n&DEPRECATED_STATUS_CODE_UNAUTHENTICATED\x10\x10\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' + serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"Q\n\nTracesData\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\xc2\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xbc\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xae\x01\n\x06Status\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02J\x04\x08\x01\x10\x02\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -67,111 +67,11 @@ ], containing_type=None, serialized_options=None, - serialized_start=1400, - serialized_end=1553, + serialized_start=1483, + serialized_end=1636, ) _sym_db.RegisterEnumDescriptor(_SPAN_SPANKIND) -_STATUS_DEPRECATEDSTATUSCODE = _descriptor.EnumDescriptor( - name='DeprecatedStatusCode', - full_name='opentelemetry.proto.trace.v1.Status.DeprecatedStatusCode', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_OK', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_CANCELLED', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_UNKNOWN_ERROR', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_INVALID_ARGUMENT', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_NOT_FOUND', index=5, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_ALREADY_EXISTS', index=6, number=6, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_PERMISSION_DENIED', index=7, number=7, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED', index=8, number=8, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_FAILED_PRECONDITION', index=9, number=9, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_ABORTED', index=10, number=10, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_OUT_OF_RANGE', index=11, number=11, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_UNIMPLEMENTED', index=12, number=12, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_INTERNAL_ERROR', index=13, number=13, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_UNAVAILABLE', index=14, number=14, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_DATA_LOSS', index=15, number=15, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEPRECATED_STATUS_CODE_UNAUTHENTICATED', index=16, number=16, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1735, - serialized_end=2465, -) -_sym_db.RegisterEnumDescriptor(_STATUS_DEPRECATEDSTATUSCODE) - _STATUS_STATUSCODE = _descriptor.EnumDescriptor( name='StatusCode', full_name='opentelemetry.proto.trace.v1.Status.StatusCode', @@ -197,12 +97,44 @@ ], containing_type=None, serialized_options=None, - serialized_start=2467, - serialized_end=2545, + serialized_start=1729, + serialized_end=1807, ) _sym_db.RegisterEnumDescriptor(_STATUS_STATUSCODE) +_TRACESDATA = _descriptor.Descriptor( + name='TracesData', + full_name='opentelemetry.proto.trace.v1.TracesData', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='resource_spans', full_name='opentelemetry.proto.trace.v1.TracesData.resource_spans', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=166, + serialized_end=247, +) + + _RESOURCESPANS = _descriptor.Descriptor( name='ResourceSpans', full_name='opentelemetry.proto.trace.v1.ResourceSpans', @@ -244,8 +176,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=167, - serialized_end=361, + serialized_start=250, + serialized_end=444, ) @@ -290,8 +222,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=364, - serialized_end=552, + serialized_start=447, + serialized_end=635, ) @@ -343,8 +275,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1097, - serialized_end=1237, + serialized_start=1180, + serialized_end=1320, ) _SPAN_LINK = _descriptor.Descriptor( @@ -402,8 +334,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1240, - serialized_end=1397, + serialized_start=1323, + serialized_end=1480, ) _SPAN = _descriptor.Descriptor( @@ -532,8 +464,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=555, - serialized_end=1553, + serialized_start=638, + serialized_end=1636, ) @@ -546,21 +478,14 @@ create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( - name='deprecated_code', full_name='opentelemetry.proto.trace.v1.Status.deprecated_code', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='message', full_name='opentelemetry.proto.trace.v1.Status.message', index=1, + name='message', full_name='opentelemetry.proto.trace.v1.Status.message', index=0, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='code', full_name='opentelemetry.proto.trace.v1.Status.code', index=2, + name='code', full_name='opentelemetry.proto.trace.v1.Status.code', index=1, number=3, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, @@ -571,7 +496,6 @@ ], nested_types=[], enum_types=[ - _STATUS_DEPRECATEDSTATUSCODE, _STATUS_STATUSCODE, ], serialized_options=None, @@ -580,10 +504,11 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1556, - serialized_end=2545, + serialized_start=1639, + serialized_end=1813, ) +_TRACESDATA.fields_by_name['resource_spans'].message_type = _RESOURCESPANS _RESOURCESPANS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE _RESOURCESPANS.fields_by_name['instrumentation_library_spans'].message_type = _INSTRUMENTATIONLIBRARYSPANS _INSTRUMENTATIONLIBRARYSPANS.fields_by_name['instrumentation_library'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONLIBRARY @@ -598,16 +523,22 @@ _SPAN.fields_by_name['links'].message_type = _SPAN_LINK _SPAN.fields_by_name['status'].message_type = _STATUS _SPAN_SPANKIND.containing_type = _SPAN -_STATUS.fields_by_name['deprecated_code'].enum_type = _STATUS_DEPRECATEDSTATUSCODE _STATUS.fields_by_name['code'].enum_type = _STATUS_STATUSCODE -_STATUS_DEPRECATEDSTATUSCODE.containing_type = _STATUS _STATUS_STATUSCODE.containing_type = _STATUS +DESCRIPTOR.message_types_by_name['TracesData'] = _TRACESDATA DESCRIPTOR.message_types_by_name['ResourceSpans'] = _RESOURCESPANS DESCRIPTOR.message_types_by_name['InstrumentationLibrarySpans'] = _INSTRUMENTATIONLIBRARYSPANS DESCRIPTOR.message_types_by_name['Span'] = _SPAN DESCRIPTOR.message_types_by_name['Status'] = _STATUS _sym_db.RegisterFileDescriptor(DESCRIPTOR) +TracesData = _reflection.GeneratedProtocolMessageType('TracesData', (_message.Message,), { + 'DESCRIPTOR' : _TRACESDATA, + '__module__' : 'opentelemetry.proto.trace.v1.trace_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.TracesData) + }) +_sym_db.RegisterMessage(TracesData) + ResourceSpans = _reflection.GeneratedProtocolMessageType('ResourceSpans', (_message.Message,), { 'DESCRIPTOR' : _RESOURCESPANS, '__module__' : 'opentelemetry.proto.trace.v1.trace_pb2' @@ -654,5 +585,4 @@ DESCRIPTOR._options = None -_STATUS.fields_by_name['deprecated_code']._options = None # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi index e187f03d5e..16491d9230 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi @@ -14,6 +14,36 @@ import typing_extensions DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... +class TracesData(google.protobuf.message.Message): + """TracesData represents the traces data that can be stored in a persistent storage, + OR can be embedded by other protocols that transfer OTLP traces data but do + not implement the OTLP protocol. + + The main difference between this message and collector protocol is that + in this message there will not be any "control" or "metadata" specific to + OTLP protocol. + + When new fields are added into this message, the OTLP request MUST be updated + as well. + """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + RESOURCE_SPANS_FIELD_NUMBER: builtins.int + @property + def resource_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResourceSpans]: + """An array of ResourceSpans. + For data coming from a single resource this array will typically contain + one element. Intermediary nodes that receive data from multiple origins + typically batch the data before forwarding further and in that case this + array will contain multiple elements. + """ + pass + def __init__(self, + *, + resource_spans : typing.Optional[typing.Iterable[global___ResourceSpans]] = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["resource_spans",b"resource_spans"]) -> None: ... +global___TracesData = TracesData + class ResourceSpans(google.protobuf.message.Message): """A collection of InstrumentationLibrarySpans from a Resource.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... @@ -295,9 +325,7 @@ class Span(google.protobuf.message.Message): This makes it easier to correlate spans in different traces. This field is semantically required to be set to non-empty string. - When null or empty string received - receiver may use string "name" - as a replacement. There might be smarted algorithms implemented by - receiver to fix the empty span name. + Empty value is equivalent to an unknown span name. This field is required. """ @@ -328,14 +356,16 @@ class Span(google.protobuf.message.Message): @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """attributes is a collection of key/value pairs. The value can be a string, - an integer, a double or the Boolean values `true` or `false`. Note, global attributes + """attributes is a collection of key/value pairs. Note, global attributes like server name can be set using the resource API. Examples of attributes: "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" "/http/server_latency": 300 "abc.com/myattribute": true "abc.com/score": 10.239 + + The OpenTelemetry API specification further restricts the allowed value types: + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/common.md#attributes """ pass dropped_attributes_count: builtins.int = ... @@ -395,88 +425,8 @@ global___Span = Span class Status(google.protobuf.message.Message): """The Status type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. - IMPORTANT: Backward compatibility notes: - - To ensure any pair of senders and receivers continues to correctly signal and - interpret erroneous situations, the senders and receivers MUST follow these rules: - - 1. Old senders and receivers that are not aware of `code` field will continue using - the `deprecated_code` field to signal and interpret erroneous situation. - - 2. New senders, which are aware of the `code` field MUST set both the - `deprecated_code` and `code` fields according to the following rules: - - if code==STATUS_CODE_UNSET then `deprecated_code` MUST be - set to DEPRECATED_STATUS_CODE_OK. - - if code==STATUS_CODE_OK then `deprecated_code` MUST be - set to DEPRECATED_STATUS_CODE_OK. - - if code==STATUS_CODE_ERROR then `deprecated_code` MUST be - set to DEPRECATED_STATUS_CODE_UNKNOWN_ERROR. - - These rules allow old receivers to correctly interpret data received from new senders. - - 3. New receivers MUST look at both the `code` and `deprecated_code` fields in order - to interpret the overall status: - - If code==STATUS_CODE_UNSET then the value of `deprecated_code` is the - carrier of the overall status according to these rules: - - if deprecated_code==DEPRECATED_STATUS_CODE_OK then the receiver MUST interpret - the overall status to be STATUS_CODE_UNSET. - - if deprecated_code!=DEPRECATED_STATUS_CODE_OK then the receiver MUST interpret - the overall status to be STATUS_CODE_ERROR. - - If code!=STATUS_CODE_UNSET then the value of `deprecated_code` MUST be - ignored, the `code` field is the sole carrier of the status. - - These rules allow new receivers to correctly interpret data received from old senders. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - class DeprecatedStatusCode(_DeprecatedStatusCode, metaclass=_DeprecatedStatusCodeEnumTypeWrapper): - pass - class _DeprecatedStatusCode: - V = typing.NewType('V', builtins.int) - class _DeprecatedStatusCodeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_DeprecatedStatusCode.V], builtins.type): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... - DEPRECATED_STATUS_CODE_OK = Status.DeprecatedStatusCode.V(0) - DEPRECATED_STATUS_CODE_CANCELLED = Status.DeprecatedStatusCode.V(1) - DEPRECATED_STATUS_CODE_UNKNOWN_ERROR = Status.DeprecatedStatusCode.V(2) - DEPRECATED_STATUS_CODE_INVALID_ARGUMENT = Status.DeprecatedStatusCode.V(3) - DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED = Status.DeprecatedStatusCode.V(4) - DEPRECATED_STATUS_CODE_NOT_FOUND = Status.DeprecatedStatusCode.V(5) - DEPRECATED_STATUS_CODE_ALREADY_EXISTS = Status.DeprecatedStatusCode.V(6) - DEPRECATED_STATUS_CODE_PERMISSION_DENIED = Status.DeprecatedStatusCode.V(7) - DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED = Status.DeprecatedStatusCode.V(8) - DEPRECATED_STATUS_CODE_FAILED_PRECONDITION = Status.DeprecatedStatusCode.V(9) - DEPRECATED_STATUS_CODE_ABORTED = Status.DeprecatedStatusCode.V(10) - DEPRECATED_STATUS_CODE_OUT_OF_RANGE = Status.DeprecatedStatusCode.V(11) - DEPRECATED_STATUS_CODE_UNIMPLEMENTED = Status.DeprecatedStatusCode.V(12) - DEPRECATED_STATUS_CODE_INTERNAL_ERROR = Status.DeprecatedStatusCode.V(13) - DEPRECATED_STATUS_CODE_UNAVAILABLE = Status.DeprecatedStatusCode.V(14) - DEPRECATED_STATUS_CODE_DATA_LOSS = Status.DeprecatedStatusCode.V(15) - DEPRECATED_STATUS_CODE_UNAUTHENTICATED = Status.DeprecatedStatusCode.V(16) - - DEPRECATED_STATUS_CODE_OK = Status.DeprecatedStatusCode.V(0) - DEPRECATED_STATUS_CODE_CANCELLED = Status.DeprecatedStatusCode.V(1) - DEPRECATED_STATUS_CODE_UNKNOWN_ERROR = Status.DeprecatedStatusCode.V(2) - DEPRECATED_STATUS_CODE_INVALID_ARGUMENT = Status.DeprecatedStatusCode.V(3) - DEPRECATED_STATUS_CODE_DEADLINE_EXCEEDED = Status.DeprecatedStatusCode.V(4) - DEPRECATED_STATUS_CODE_NOT_FOUND = Status.DeprecatedStatusCode.V(5) - DEPRECATED_STATUS_CODE_ALREADY_EXISTS = Status.DeprecatedStatusCode.V(6) - DEPRECATED_STATUS_CODE_PERMISSION_DENIED = Status.DeprecatedStatusCode.V(7) - DEPRECATED_STATUS_CODE_RESOURCE_EXHAUSTED = Status.DeprecatedStatusCode.V(8) - DEPRECATED_STATUS_CODE_FAILED_PRECONDITION = Status.DeprecatedStatusCode.V(9) - DEPRECATED_STATUS_CODE_ABORTED = Status.DeprecatedStatusCode.V(10) - DEPRECATED_STATUS_CODE_OUT_OF_RANGE = Status.DeprecatedStatusCode.V(11) - DEPRECATED_STATUS_CODE_UNIMPLEMENTED = Status.DeprecatedStatusCode.V(12) - DEPRECATED_STATUS_CODE_INTERNAL_ERROR = Status.DeprecatedStatusCode.V(13) - DEPRECATED_STATUS_CODE_UNAVAILABLE = Status.DeprecatedStatusCode.V(14) - DEPRECATED_STATUS_CODE_DATA_LOSS = Status.DeprecatedStatusCode.V(15) - DEPRECATED_STATUS_CODE_UNAUTHENTICATED = Status.DeprecatedStatusCode.V(16) - class StatusCode(_StatusCode, metaclass=_StatusCodeEnumTypeWrapper): """For the semantics of status codes see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-status @@ -510,18 +460,8 @@ class Status(google.protobuf.message.Message): """The Span contains an error.""" - DEPRECATED_CODE_FIELD_NUMBER: builtins.int MESSAGE_FIELD_NUMBER: builtins.int CODE_FIELD_NUMBER: builtins.int - deprecated_code: global___Status.DeprecatedStatusCode.V = ... - """The deprecated status code. This is an optional field. - - This field is deprecated and is replaced by the `code` field below. See backward - compatibility notes below. According to our stability guarantees this field - will be removed in 12 months, on Oct 22, 2021. All usage of old senders and - receivers that do not understand the `code` field MUST be phased out by then. - """ - message: typing.Text = ... """A developer-facing human readable error message.""" @@ -530,9 +470,8 @@ class Status(google.protobuf.message.Message): def __init__(self, *, - deprecated_code : global___Status.DeprecatedStatusCode.V = ..., message : typing.Text = ..., code : global___Status.StatusCode.V = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["code",b"code","deprecated_code",b"deprecated_code","message",b"message"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["code",b"code","message",b"message"]) -> None: ... global___Status = Status diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index 9fd2dbe60e..08875e1428 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -12,7 +12,7 @@ # PROTO_REPO_DIR - the path to an existing checkout of the opentelemetry-proto repo # Pinned commit/branch/tag for the current version used in opentelemetry-proto python package. -PROTO_REPO_BRANCH_OR_COMMIT="v0.9.0" +PROTO_REPO_BRANCH_OR_COMMIT="v0.12.0" set -e From 37902d0284f86d8ef91ac8d662b99bd3fec5b58c Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Mon, 31 Jan 2022 14:59:33 +0530 Subject: [PATCH 1113/1517] updating changelogs and version to 1.9.1-0.28b1 (#2423) * updating changelogs and version to 1.9.1-0.28b1 * Update CHANGELOG.md Co-authored-by: Nathaniel Ruiz Nowell * Update CHANGELOG.md Co-authored-by: Nathaniel Ruiz Nowell Co-authored-by: Nathaniel Ruiz Nowell --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 6 +++++- eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- .../src/opentelemetry/test/version.py | 2 +- 29 files changed, 38 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2275751a1a..36c04fede8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: eedd96a86931ba0f84112e97fc3e7db221261a20 + CONTRIB_REPO_SHA: b541c59284100e617303a5c1f505ca2872045792 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index abaab2a309..a7c6f798fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.9.0-0.28b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.9.1-0.28b1...HEAD) + +## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1-0.28b1) - 2022-01-29 + + - Update opentelemetry-proto to v0.12.0. Note that this update removes deprecated status codes. ([#2415](https://github.com/open-telemetry/opentelemetry-python/pull/2415)) diff --git a/eachdist.ini b/eachdist.ini index 33f717b78a..576d46a5fc 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.9.0 +version=1.9.1 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.28b0 +version=0.28b1 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index f1667e7652..84a10efbd5 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index f1667e7652..84a10efbd5 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 02f232f72b..e120ca2564 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -40,8 +40,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.9.0 - opentelemetry-exporter-jaeger-thrift == 1.9.0 + opentelemetry-exporter-jaeger-proto-grpc == 1.9.1 + opentelemetry-exporter-jaeger-thrift == 1.9.1 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index f1667e7652..84a10efbd5 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 9bea3afdaf..a94d4ed446 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.28b0" +__version__ = "0.28b1" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 8d4ef0e664..829449b31d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.9.0 + opentelemetry-proto == 1.9.1 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index d074a5b24b..030fb6915c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index 20bda92f99..f12a5b470b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.9.0 + opentelemetry-proto == 1.9.1 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index d074a5b24b..030fb6915c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 0f67262d3b..c9af1bd46d 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -39,8 +39,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.9.0 - opentelemetry-exporter-otlp-proto-http == 1.9.0 + opentelemetry-exporter-otlp-proto-grpc == 1.9.1 + opentelemetry-exporter-otlp-proto-http == 1.9.1 [options.entry_points] opentelemetry_traces_exporter = diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index d074a5b24b..030fb6915c 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index d074a5b24b..030fb6915c 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 88c81fb121..57bf4274e4 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.9.0 + opentelemetry-exporter-zipkin-json == 1.9.1 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index d074a5b24b..030fb6915c 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 4e193df6b2..3b19d93482 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -39,8 +39,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.9.0 - opentelemetry-exporter-zipkin-proto-http == 1.9.0 + opentelemetry-exporter-zipkin-json == 1.9.1 + opentelemetry-exporter-zipkin-proto-http == 1.9.1 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index d074a5b24b..030fb6915c 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index d074a5b24b..030fb6915c 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index d074a5b24b..030fb6915c 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index a4222db9a7..b12ea1651b 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,8 +43,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.9.0 - opentelemetry-semantic-conventions == 0.28b0 + opentelemetry-api == 1.9.1 + opentelemetry-semantic-conventions == 0.28b1 setuptools >= 16.0 dataclasses == 0.8; python_version < '3.7' typing-extensions >= 3.7.4 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index d074a5b24b..030fb6915c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 9bea3afdaf..a94d4ed446 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.28b0" +__version__ = "0.28b1" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index d074a5b24b..030fb6915c 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index d074a5b24b..030fb6915c 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 0443162638..1610d7bc0b 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -47,7 +47,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test-utils == 0.28b0 + opentelemetry-test-utils == 0.28b1 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 9bea3afdaf..a94d4ed446 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.28b0" +__version__ = "0.28b1" diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 116b0529b4..a968e0037d 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.28b0" +__version__ = "0.28b1" From f772260bd85b9cd80ba1fa614eaddef28a317b5b Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 31 Jan 2022 21:20:28 +0530 Subject: [PATCH 1114/1517] Update logging component status (#2424) --- website_docs/_index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website_docs/_index.md b/website_docs/_index.md index 040245e5dc..f13cf3f96c 100644 --- a/website_docs/_index.md +++ b/website_docs/_index.md @@ -23,9 +23,9 @@ get started using OpenTelemetry for Python. The current status of the major functional components for OpenTelemetry Python is as follows: -| Tracing | Metrics | Logging | -| ------- | ------- | ------------------- | -| Stable | Alpha | Not Yet Implemented | +| Tracing | Metrics | Logging | +| ------- | ------- | ------------ | +| Stable | Alpha | Experimental | {{% latest_release "python" /%}} From 02863de932d2c1b681f29b6a48d95ddfd8b53edd Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 1 Feb 2022 01:57:07 +0530 Subject: [PATCH 1115/1517] Upgraded black to 22.1.0 (#2427) --- dev-requirements.txt | 2 +- .../jaeger/proto/grpc/translate/__init__.py | 2 +- .../tests/test_jaeger_exporter_protobuf.py | 10 +++--- .../jaeger/thrift/translate/__init__.py | 2 +- .../tests/test_jaeger_exporter_thrift.py | 24 ++++++------- .../tests/test_otcollector_trace_exporter.py | 10 +++--- .../tests/test_protobuf_encoder.py | 18 +++++----- .../exporter/zipkin/encoder/__init__.py | 2 +- .../tests/encoder/common_tests.py | 36 +++++++++---------- .../tests/encoder/test_v1_json.py | 30 ++++++++-------- .../tests/encoder/test_v2_json.py | 30 ++++++++-------- .../tests/encoder/common_tests.py | 36 +++++++++---------- .../tests/encoder/test_v2_protobuf.py | 4 +-- .../src/opentelemetry/trace/span.py | 4 +-- .../opentelemetry/sdk/util/instrumentation.py | 14 +++----- .../tests/trace/test_sampling.py | 6 ++-- scripts/eachdist.py | 2 +- tests/opentelemetry-test-utils/setup.cfg | 2 +- 18 files changed, 115 insertions(+), 119 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 22595d5af2..44e2080141 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,7 @@ pylint==2.11.0 flake8~=3.7 isort~=5.8 -black~=21.7b0 +black~=22.1.0 httpretty~=1.0 mypy==0.812 sphinx~=3.5.4 diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py index 65eef76d0f..3eb59755c9 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py @@ -39,7 +39,7 @@ def _nsec_to_usec_round(nsec: int) -> int: """Round nanoseconds to microseconds""" - return (nsec + 500) // 10 ** 3 + return (nsec + 500) // 10**3 def _convert_int_to_i64(val): diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py index 4448b2c838..926bbff085 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py @@ -108,13 +108,13 @@ def test_translate_to_jaeger(self): parent_id = 0x1111111111111111 other_id = 0x2222222222222222 - base_time = 683647322 * 10 ** 9 # in ns + base_time = 683647322 * 10**9 # in ns start_times = ( base_time, - base_time + 150 * 10 ** 6, - base_time + 300 * 10 ** 6, + base_time + 150 * 10**6, + base_time + 300 * 10**6, ) - durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6) + durations = (50 * 10**6, 100 * 10**6, 200 * 10**6) end_times = ( start_times[0] + durations[0], start_times[1] + durations[1], @@ -139,7 +139,7 @@ def test_translate_to_jaeger(self): ] ) - event_timestamp = base_time + 50 * 10 ** 6 + event_timestamp = base_time + 50 * 10**6 # pylint:disable=protected-access event_timestamp_proto = ( pb_translator._proto_timestamp_from_epoch_nanos(event_timestamp) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py index f21ede6f8b..1649b000e9 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py @@ -36,7 +36,7 @@ def _nsec_to_usec_round(nsec: int) -> int: """Round nanoseconds to microseconds""" - return (nsec + 500) // 10 ** 3 + return (nsec + 500) // 10**3 def _convert_int_to_i64(val): diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py index 8a30527eeb..8bd3e3e359 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py @@ -230,13 +230,13 @@ def test_translate_to_jaeger(self): parent_id = 0x1111111111111111 other_id = 0x2222222222222222 - base_time = 683647322 * 10 ** 9 # in ns + base_time = 683647322 * 10**9 # in ns start_times = ( base_time, - base_time + 150 * 10 ** 6, - base_time + 300 * 10 ** 6, + base_time + 150 * 10**6, + base_time + 300 * 10**6, ) - durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6) + durations = (50 * 10**6, 100 * 10**6, 200 * 10**6) end_times = ( start_times[0] + durations[0], start_times[1] + durations[1], @@ -259,7 +259,7 @@ def test_translate_to_jaeger(self): "key_float": 0.3, } - event_timestamp = base_time + 50 * 10 ** 6 + event_timestamp = base_time + 50 * 10**6 event = trace.Event( name="event0", timestamp=event_timestamp, @@ -338,8 +338,8 @@ def test_translate_to_jaeger(self): traceIdLow=trace_id_low, spanId=span_id, parentSpanId=parent_id, - startTime=start_times[0] // 10 ** 3, - duration=durations[0] // 10 ** 3, + startTime=start_times[0] // 10**3, + duration=durations[0] // 10**3, flags=0, tags=[ jaeger.Tag( @@ -394,7 +394,7 @@ def test_translate_to_jaeger(self): ], logs=[ jaeger.Log( - timestamp=event_timestamp // 10 ** 3, + timestamp=event_timestamp // 10**3, fields=[ jaeger.Tag( key="annotation_bool", @@ -426,8 +426,8 @@ def test_translate_to_jaeger(self): traceIdLow=trace_id_low, spanId=parent_id, parentSpanId=0, - startTime=start_times[1] // 10 ** 3, - duration=durations[1] // 10 ** 3, + startTime=start_times[1] // 10**3, + duration=durations[1] // 10**3, flags=0, tags=default_tags, ), @@ -437,8 +437,8 @@ def test_translate_to_jaeger(self): traceIdLow=trace_id_low, spanId=other_id, parentSpanId=0, - startTime=start_times[2] // 10 ** 3, - duration=durations[2] // 10 ** 3, + startTime=start_times[2] // 10**3, + duration=durations[2] // 10**3, flags=0, tags=[ jaeger.Tag( diff --git a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py index cd4dcb1a08..fa546cde7a 100644 --- a/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py +++ b/exporter/opentelemetry-exporter-opencensus/tests/test_otcollector_trace_exporter.py @@ -84,13 +84,13 @@ def test_translate_to_collector(self): trace_id = 0x6E0C63257DE34C926F9EFCD03927272E span_id = 0x34BF92DEEFC58C92 parent_id = 0x1111111111111111 - base_time = 683647322 * 10 ** 9 # in ns + base_time = 683647322 * 10**9 # in ns start_times = ( base_time, - base_time + 150 * 10 ** 6, - base_time + 300 * 10 ** 6, + base_time + 150 * 10**6, + base_time + 300 * 10**6, ) - durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6) + durations = (50 * 10**6, 100 * 10**6, 200 * 10**6) end_times = ( start_times[0] + durations[0], start_times[1] + durations[1], @@ -114,7 +114,7 @@ def test_translate_to_collector(self): "annotation_string": "annotation_test", "key_float": 0.3, } - event_timestamp = base_time + 50 * 10 ** 6 + event_timestamp = base_time + 50 * 10**6 event = trace.Event( name="event0", timestamp=event_timestamp, diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py index f2d2445677..a6ebc05145 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py @@ -80,18 +80,18 @@ def test_content_type(self): def get_exhaustive_otel_span_list() -> List[SDKSpan]: trace_id = 0x3E0C63257DE34C926F9EFCD03927272E - base_time = 683647322 * 10 ** 9 # in ns + base_time = 683647322 * 10**9 # in ns start_times = ( base_time, - base_time + 150 * 10 ** 6, - base_time + 300 * 10 ** 6, - base_time + 400 * 10 ** 6, + base_time + 150 * 10**6, + base_time + 300 * 10**6, + base_time + 400 * 10**6, ) end_times = ( - start_times[0] + (50 * 10 ** 6), - start_times[1] + (100 * 10 ** 6), - start_times[2] + (200 * 10 ** 6), - start_times[3] + (300 * 10 ** 6), + start_times[0] + (50 * 10**6), + start_times[1] + (100 * 10**6), + start_times[2] + (200 * 10**6), + start_times[3] + (300 * 10**6), ) parent_span_context = SDKSpanContext( @@ -114,7 +114,7 @@ def get_exhaustive_otel_span_list() -> List[SDKSpan]: events=( SDKEvent( name="event0", - timestamp=base_time + 50 * 10 ** 6, + timestamp=base_time + 50 * 10**6, attributes={ "annotation_bool": True, "annotation_string": "annotation_test", diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py index 07ad735ece..5353a896cf 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py @@ -256,7 +256,7 @@ def _nsec_to_usec_round(nsec: int) -> int: Timestamp in zipkin spans is int of microseconds. See: https://zipkin.io/pages/instrumenting.html """ - return (nsec + 500) // 10 ** 3 + return (nsec + 500) // 10**3 class JsonEncoder(Encoder): diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py index a04148b6ea..8eaa5b1563 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py @@ -96,15 +96,15 @@ def test_constructor_max_tag_value_length(self): ) def test_nsec_to_usec_round(self): - base_time_nsec = 683647322 * 10 ** 9 + base_time_nsec = 683647322 * 10**9 for nsec in ( base_time_nsec, - base_time_nsec + 150 * 10 ** 6, - base_time_nsec + 300 * 10 ** 6, - base_time_nsec + 400 * 10 ** 6, + base_time_nsec + 150 * 10**6, + base_time_nsec + 300 * 10**6, + base_time_nsec + 400 * 10**6, ): self.assertEqual( - (nsec + 500) // 10 ** 3, + (nsec + 500) // 10**3, self.get_encoder_default()._nsec_to_usec_round(nsec), ) @@ -168,8 +168,8 @@ def test_get_parent_id_from_span_context(self): def get_data_for_max_tag_length_test( max_tag_length: int, ) -> (trace._Span, Dict): - start_time = 683647322 * 10 ** 9 # in ns - duration = 50 * 10 ** 6 + start_time = 683647322 * 10**9 # in ns + duration = 50 * 10**6 end_time = start_time + duration span = trace._Span( @@ -332,18 +332,18 @@ def get_data_for_max_tag_length_test( def get_exhaustive_otel_span_list() -> List[trace._Span]: trace_id = 0x6E0C63257DE34C926F9EFCD03927272E - base_time = 683647322 * 10 ** 9 # in ns + base_time = 683647322 * 10**9 # in ns start_times = ( base_time, - base_time + 150 * 10 ** 6, - base_time + 300 * 10 ** 6, - base_time + 400 * 10 ** 6, + base_time + 150 * 10**6, + base_time + 300 * 10**6, + base_time + 400 * 10**6, ) end_times = ( - start_times[0] + (50 * 10 ** 6), - start_times[1] + (100 * 10 ** 6), - start_times[2] + (200 * 10 ** 6), - start_times[3] + (300 * 10 ** 6), + start_times[0] + (50 * 10**6), + start_times[1] + (100 * 10**6), + start_times[2] + (200 * 10**6), + start_times[3] + (300 * 10**6), ) parent_span_context = trace_api.SpanContext( @@ -366,7 +366,7 @@ def get_exhaustive_otel_span_list() -> List[trace._Span]: events=( trace.Event( name="event0", - timestamp=base_time + 50 * 10 ** 6, + timestamp=base_time + 50 * 10**6, attributes={ "annotation_bool": True, "annotation_string": "annotation_test", @@ -429,14 +429,14 @@ def get_exhaustive_otel_span_list() -> List[trace._Span]: # pylint: disable=W0223 class CommonJsonEncoderTest(CommonEncoderTest, abc.ABC): def test_encode_trace_id(self): - for trace_id in (1, 1024, 2 ** 32, 2 ** 64, 2 ** 65): + for trace_id in (1, 1024, 2**32, 2**64, 2**65): self.assertEqual( format(trace_id, "032x"), self.get_encoder_default()._encode_trace_id(trace_id), ) def test_encode_span_id(self): - for span_id in (1, 1024, 2 ** 8, 2 ** 16, 2 ** 32, 2 ** 64): + for span_id in (1, 1024, 2**8, 2**16, 2**32, 2**64): self.assertEqual( format(span_id, "016x"), self.get_encoder_default()._encode_span_id(span_id), diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py index a7e0480f0e..b099040acc 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py @@ -48,13 +48,13 @@ def test_encode(self): otel_spans[0].context.span_id ), "name": otel_spans[0].name, - "timestamp": otel_spans[0].start_time // 10 ** 3, - "duration": (otel_spans[0].end_time // 10 ** 3) - - (otel_spans[0].start_time // 10 ** 3), + "timestamp": otel_spans[0].start_time // 10**3, + "duration": (otel_spans[0].end_time // 10**3) + - (otel_spans[0].start_time // 10**3), "annotations": [ { "timestamp": otel_spans[0].events[0].timestamp - // 10 ** 3, + // 10**3, "value": json.dumps( { "event0": { @@ -101,9 +101,9 @@ def test_encode(self): otel_spans[1].context.span_id ), "name": otel_spans[1].name, - "timestamp": otel_spans[1].start_time // 10 ** 3, - "duration": (otel_spans[1].end_time // 10 ** 3) - - (otel_spans[1].start_time // 10 ** 3), + "timestamp": otel_spans[1].start_time // 10**3, + "duration": (otel_spans[1].end_time // 10**3) + - (otel_spans[1].start_time // 10**3), "binaryAnnotations": [ { "key": "key_resource", @@ -128,9 +128,9 @@ def test_encode(self): otel_spans[2].context.span_id ), "name": otel_spans[2].name, - "timestamp": otel_spans[2].start_time // 10 ** 3, - "duration": (otel_spans[2].end_time // 10 ** 3) - - (otel_spans[2].start_time // 10 ** 3), + "timestamp": otel_spans[2].start_time // 10**3, + "duration": (otel_spans[2].end_time // 10**3) + - (otel_spans[2].start_time // 10**3), "binaryAnnotations": [ { "key": "key_string", @@ -150,9 +150,9 @@ def test_encode(self): otel_spans[3].context.span_id ), "name": otel_spans[3].name, - "timestamp": otel_spans[3].start_time // 10 ** 3, - "duration": (otel_spans[3].end_time // 10 ** 3) - - (otel_spans[3].start_time // 10 ** 3), + "timestamp": otel_spans[3].start_time // 10**3, + "duration": (otel_spans[3].end_time // 10**3) + - (otel_spans[3].start_time // 10**3), "binaryAnnotations": [ { "key": NAME_KEY, @@ -177,8 +177,8 @@ def test_encode_id_zero_padding(self): trace_id = 0x0E0C63257DE34C926F9EFCD03927272E span_id = 0x04BF92DEEFC58C92 parent_id = 0x0AAAAAAAAAAAAAAA - start_time = 683647322 * 10 ** 9 # in ns - duration = 50 * 10 ** 6 + start_time = 683647322 * 10**9 # in ns + duration = 50 * 10**6 end_time = start_time + duration otel_span = trace._Span( diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py index 2898604a25..9ceb9383fd 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py @@ -48,9 +48,9 @@ def test_encode(self): otel_spans[0].context.span_id ), "name": otel_spans[0].name, - "timestamp": otel_spans[0].start_time // 10 ** 3, - "duration": (otel_spans[0].end_time // 10 ** 3) - - (otel_spans[0].start_time // 10 ** 3), + "timestamp": otel_spans[0].start_time // 10**3, + "duration": (otel_spans[0].end_time // 10**3) + - (otel_spans[0].start_time // 10**3), "localEndpoint": local_endpoint, "kind": span_kind, "tags": { @@ -62,7 +62,7 @@ def test_encode(self): "annotations": [ { "timestamp": otel_spans[0].events[0].timestamp - // 10 ** 3, + // 10**3, "value": json.dumps( { "event0": { @@ -86,9 +86,9 @@ def test_encode(self): otel_spans[1].context.span_id ), "name": otel_spans[1].name, - "timestamp": otel_spans[1].start_time // 10 ** 3, - "duration": (otel_spans[1].end_time // 10 ** 3) - - (otel_spans[1].start_time // 10 ** 3), + "timestamp": otel_spans[1].start_time // 10**3, + "duration": (otel_spans[1].end_time // 10**3) + - (otel_spans[1].start_time // 10**3), "localEndpoint": local_endpoint, "kind": span_kind, "tags": { @@ -103,9 +103,9 @@ def test_encode(self): otel_spans[2].context.span_id ), "name": otel_spans[2].name, - "timestamp": otel_spans[2].start_time // 10 ** 3, - "duration": (otel_spans[2].end_time // 10 ** 3) - - (otel_spans[2].start_time // 10 ** 3), + "timestamp": otel_spans[2].start_time // 10**3, + "duration": (otel_spans[2].end_time // 10**3) + - (otel_spans[2].start_time // 10**3), "localEndpoint": local_endpoint, "kind": span_kind, "tags": { @@ -119,9 +119,9 @@ def test_encode(self): otel_spans[3].context.span_id ), "name": otel_spans[3].name, - "timestamp": otel_spans[3].start_time // 10 ** 3, - "duration": (otel_spans[3].end_time // 10 ** 3) - - (otel_spans[3].start_time // 10 ** 3), + "timestamp": otel_spans[3].start_time // 10**3, + "duration": (otel_spans[3].end_time // 10**3) + - (otel_spans[3].start_time // 10**3), "localEndpoint": local_endpoint, "kind": span_kind, "tags": {NAME_KEY: "name", VERSION_KEY: "version"}, @@ -137,8 +137,8 @@ def test_encode_id_zero_padding(self): trace_id = 0x0E0C63257DE34C926F9EFCD03927272E span_id = 0x04BF92DEEFC58C92 parent_id = 0x0AAAAAAAAAAAAAAA - start_time = 683647322 * 10 ** 9 # in ns - duration = 50 * 10 ** 6 + start_time = 683647322 * 10**9 # in ns + duration = 50 * 10**6 end_time = start_time + duration otel_span = trace._Span( diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py index a04148b6ea..8eaa5b1563 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py @@ -96,15 +96,15 @@ def test_constructor_max_tag_value_length(self): ) def test_nsec_to_usec_round(self): - base_time_nsec = 683647322 * 10 ** 9 + base_time_nsec = 683647322 * 10**9 for nsec in ( base_time_nsec, - base_time_nsec + 150 * 10 ** 6, - base_time_nsec + 300 * 10 ** 6, - base_time_nsec + 400 * 10 ** 6, + base_time_nsec + 150 * 10**6, + base_time_nsec + 300 * 10**6, + base_time_nsec + 400 * 10**6, ): self.assertEqual( - (nsec + 500) // 10 ** 3, + (nsec + 500) // 10**3, self.get_encoder_default()._nsec_to_usec_round(nsec), ) @@ -168,8 +168,8 @@ def test_get_parent_id_from_span_context(self): def get_data_for_max_tag_length_test( max_tag_length: int, ) -> (trace._Span, Dict): - start_time = 683647322 * 10 ** 9 # in ns - duration = 50 * 10 ** 6 + start_time = 683647322 * 10**9 # in ns + duration = 50 * 10**6 end_time = start_time + duration span = trace._Span( @@ -332,18 +332,18 @@ def get_data_for_max_tag_length_test( def get_exhaustive_otel_span_list() -> List[trace._Span]: trace_id = 0x6E0C63257DE34C926F9EFCD03927272E - base_time = 683647322 * 10 ** 9 # in ns + base_time = 683647322 * 10**9 # in ns start_times = ( base_time, - base_time + 150 * 10 ** 6, - base_time + 300 * 10 ** 6, - base_time + 400 * 10 ** 6, + base_time + 150 * 10**6, + base_time + 300 * 10**6, + base_time + 400 * 10**6, ) end_times = ( - start_times[0] + (50 * 10 ** 6), - start_times[1] + (100 * 10 ** 6), - start_times[2] + (200 * 10 ** 6), - start_times[3] + (300 * 10 ** 6), + start_times[0] + (50 * 10**6), + start_times[1] + (100 * 10**6), + start_times[2] + (200 * 10**6), + start_times[3] + (300 * 10**6), ) parent_span_context = trace_api.SpanContext( @@ -366,7 +366,7 @@ def get_exhaustive_otel_span_list() -> List[trace._Span]: events=( trace.Event( name="event0", - timestamp=base_time + 50 * 10 ** 6, + timestamp=base_time + 50 * 10**6, attributes={ "annotation_bool": True, "annotation_string": "annotation_test", @@ -429,14 +429,14 @@ def get_exhaustive_otel_span_list() -> List[trace._Span]: # pylint: disable=W0223 class CommonJsonEncoderTest(CommonEncoderTest, abc.ABC): def test_encode_trace_id(self): - for trace_id in (1, 1024, 2 ** 32, 2 ** 64, 2 ** 65): + for trace_id in (1, 1024, 2**32, 2**64, 2**65): self.assertEqual( format(trace_id, "032x"), self.get_encoder_default()._encode_trace_id(trace_id), ) def test_encode_span_id(self): - for span_id in (1, 1024, 2 ** 8, 2 ** 16, 2 ** 32, 2 ** 64): + for span_id in (1, 1024, 2**8, 2**16, 2**32, 2**64): self.assertEqual( format(span_id, "016x"), self.get_encoder_default()._encode_span_id(span_id), diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py index 787566e710..4ba1ea220a 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py @@ -33,14 +33,14 @@ def get_encoder(*args, **kwargs) -> ProtobufEncoder: return ProtobufEncoder(*args, **kwargs) def test_encode_trace_id(self): - for trace_id in (1, 1024, 2 ** 32, 2 ** 64, 2 ** 127): + for trace_id in (1, 1024, 2**32, 2**64, 2**127): self.assertEqual( self.get_encoder_default()._encode_trace_id(trace_id), trace_id.to_bytes(length=16, byteorder="big", signed=False), ) def test_encode_span_id(self): - for span_id in (1, 1024, 2 ** 8, 2 ** 16, 2 ** 32, 2 ** 63): + for span_id in (1, 1024, 2**8, 2**16, 2**32, 2**63): self.assertEqual( self.get_encoder_default()._encode_span_id(span_id), span_id.to_bytes(length=8, byteorder="big", signed=False), diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 23eac7337e..228e505281 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -387,8 +387,8 @@ def values(self) -> typing.ValuesView[str]: DEFAULT_TRACE_STATE = TraceState.get_default() -_TRACE_ID_MAX_VALUE = 2 ** 128 - 1 -_SPAN_ID_MAX_VALUE = 2 ** 64 - 1 +_TRACE_ID_MAX_VALUE = 2**128 - 1 +_SPAN_ID_MAX_VALUE = 2**64 - 1 class SpanContext( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index 7f565ba8ea..d634e1b833 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -40,15 +40,11 @@ def __hash__(self): return hash((self._name, self._version, self._schema_url)) def __eq__(self, value): - return ( - type(value) is type(self) - and ( - self._name, - self._version, - self._schema_url, - ) - == (value._name, value._version, value._schema_url) - ) + return type(value) is type(self) and ( + self._name, + self._version, + self._schema_url, + ) == (value._name, value._version, value._schema_url) def __lt__(self, value): if type(value) is not type(self): diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index ba6c5f5636..c5f922c8e5 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -255,7 +255,7 @@ def test_probability_sampler_limits(self): # Sample one of every 2^64 (= 5e-20) traces. This is the lowest # possible meaningful sampling rate, only traces with trace ID 0x0 # should get sampled. - almost_always_off = sampling.TraceIdRatioBased(2 ** -64) + almost_always_off = sampling.TraceIdRatioBased(2**-64) self.assertTrue( almost_always_off.should_sample( None, 0x0, "span name" @@ -267,7 +267,7 @@ def test_probability_sampler_limits(self): ).decision.is_sampled() ) self.assertEqual( - sampling.TraceIdRatioBased.get_bound_for_rate(2 ** -64), 0x1 + sampling.TraceIdRatioBased.get_bound_for_rate(2**-64), 0x1 ) # Sample every trace with trace ID less than 0xffffffffffffffff. In @@ -278,7 +278,7 @@ def test_probability_sampler_limits(self): # # 1 - sys.float_info.epsilon - almost_always_on = sampling.TraceIdRatioBased(1 - 2 ** -64) + almost_always_on = sampling.TraceIdRatioBased(1 - 2**-64) self.assertTrue( almost_always_on.should_sample( None, 0xFFFFFFFFFFFFFFFE, "span name" diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 58064a7400..427f990fe1 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -630,7 +630,7 @@ def update_dependencies(targets, version, packages): update_files( targets, "setup.cfg", - fr"({basename(pkg)}.*)==(.*)", + rf"({basename(pkg)}.*)==(.*)", r"\1== " + version, ) diff --git a/tests/opentelemetry-test-utils/setup.cfg b/tests/opentelemetry-test-utils/setup.cfg index cf5220fc3b..dc2c84d6d4 100644 --- a/tests/opentelemetry-test-utils/setup.cfg +++ b/tests/opentelemetry-test-utils/setup.cfg @@ -42,7 +42,7 @@ install_requires = opentelemetry-sdk ~= 1.3 [options.extras_require] -test = flask~=1.0 +test = flask~=2.0 [options.packages.find] where = src From d71cf688d466186d9597d3466b22239a647d20dd Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 1 Feb 2022 12:03:16 -0800 Subject: [PATCH 1116/1517] Return noopmeter on invalid name getmeter (#2428) --- .github/workflows/test.yml | 2 +- .../opentelemetry/sdk/_metrics/__init__.py | 4 ++++ .../tests/metrics/test_metrics.py | 23 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 36c04fede8..3b2ded8747 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: b541c59284100e617303a5c1f505ca2872045792 + CONTRIB_REPO_SHA: ad2594e166bd7f4cd40780df418f82389de970a6 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 0cdcbab6e9..b3232b367e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -230,6 +230,10 @@ def get_meter( ) return NoOpMeter(name, version=version, schema_url=schema_url) + if not name: + _logger.warning("Meter name cannot be None or empty.") + return NoOpMeter(name, version=version, schema_url=schema_url) + info = InstrumentationInfo(name, version, schema_url) with self._meter_lock: if not self._meters.get(info): diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 8a78eaecd8..f72d059384 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -17,6 +17,7 @@ from unittest import TestCase from unittest.mock import MagicMock, Mock, patch +from opentelemetry._metrics import NoOpMeter from opentelemetry.sdk._metrics import Meter, MeterProvider from opentelemetry.sdk._metrics.instrument import ( Counter, @@ -80,6 +81,28 @@ def test_get_meter(self): self.assertEqual(meter._instrumentation_info.version, "version") self.assertEqual(meter._instrumentation_info.schema_url, "schema_url") + def test_get_meter_empty(self): + """ + `MeterProvider.get_meter` called with None or empty string as name + should return a NoOpMeter. + """ + + meter = MeterProvider().get_meter( + None, + version="version", + schema_url="schema_url", + ) + self.assertIsInstance(meter, NoOpMeter) + self.assertEqual(meter._name, None) + + meter = MeterProvider().get_meter( + "", + version="version", + schema_url="schema_url", + ) + self.assertIsInstance(meter, NoOpMeter) + self.assertEqual(meter._name, "") + def test_get_meter_duplicate(self): """ Subsequent calls to `MeterProvider.get_meter` with the same arguments From 57a3b17c7085c2db38155948f86a63ab4d8536f0 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 2 Feb 2022 10:17:27 -0800 Subject: [PATCH 1117/1517] [metrics] add sum attribute to Histogram (#2431) --- .../src/opentelemetry/sdk/_metrics/aggregation.py | 1 + opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py | 1 + opentelemetry-sdk/tests/metrics/test_aggregation.py | 3 +++ 3 files changed, 5 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 18b43d7f72..5048d146b4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -198,4 +198,5 @@ def collect(self) -> Optional[Histogram]: bucket_counts=tuple(value), explicit_bounds=self._boundaries, aggregation_temporality=AggregationTemporality.DELTA, + sum=self._sum, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py index bd69257b93..1586beb4d7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py @@ -48,6 +48,7 @@ class Histogram: time_unix_nano: int bucket_counts: Sequence[int] explicit_bounds: Sequence[float] + sum: Union[int, float] aggregation_temporality: AggregationTemporality diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 6ca443e7a9..535d8d44d5 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -251,6 +251,9 @@ def test_aggregate(self): explicit_bucket_histogram_aggregation._bucket_counts[3], 1 ) + histo = explicit_bucket_histogram_aggregation.collect() + self.assertEqual(histo.sum, 14) + def test_min_max(self): """ `record_min_max` indicates the aggregator to record the minimum and From cade607300178ebb992ebf8e7a5cfea88a3f0480 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 2 Feb 2022 17:02:08 -0800 Subject: [PATCH 1118/1517] [exporter/otlp-proto-grpc] add histogram (#2429) * [exporter/otlp-proto-grpc] add histogram Adding support for Histogram data point types. Note this doesn't support the sum of the histogram yet. * update changelog * update changelog * fix test --- CHANGELOG.md | 2 - .../proto/grpc/_metric_exporter/__init__.py | 14 ++- .../metrics/test_otlp_metrics_exporter.py | 89 +++++++++++++++++-- 3 files changed, 92 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7c6f798fb..7f0c93f41d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1-0.28b1) - 2022-01-29 - - - Update opentelemetry-proto to v0.12.0. Note that this update removes deprecated status codes. ([#2415](https://github.com/open-telemetry/opentelemetry-python/pull/2415)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py index 46cb5d7426..df48a15a7b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py @@ -132,10 +132,18 @@ def _translate_data( pt.as_double = metric.point.value pbmetric.gauge.data_points.append(pt) elif isinstance(metric.point, Histogram): - # TODO: implement histogram - pbmetric.histogram = pb2.Histogram( - data_points=[], + pt = pb2.HistogramDataPoint( + attributes=self._translate_attributes(metric.attributes), + time_unix_nano=metric.point.time_unix_nano, + start_time_unix_nano=metric.point.start_time_unix_nano, + count=sum(metric.point.bucket_counts), + bucket_counts=metric.point.bucket_counts, + explicit_bounds=metric.point.explicit_bounds, + ) + pbmetric.histogram.aggregation_temporality = ( + metric.point.aggregation_temporality ) + pbmetric.histogram.data_points.append(pt) elif isinstance(metric.point, Sum): pt = pb2.NumberDataPoint( attributes=self._translate_attributes(metric.attributes), diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index b13fe11882..9c4bd860f5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -46,6 +46,7 @@ from opentelemetry.sdk._metrics.point import ( AggregationTemporality, Gauge, + Histogram, Metric, Sum, ) @@ -152,9 +153,20 @@ def setUp(self): self.metrics = { "sum_int": _generate_sum("sum_int", 33), - "sum_float": _generate_sum("sum_float", 2.98), + "sum_double": _generate_sum("sum_double", 2.98), "gauge_int": _generate_gauge("gauge_int", 9000), - "gauge_float": _generate_gauge("gauge_float", 52.028), + "gauge_double": _generate_gauge("gauge_double", 52.028), + "histogram": _generate_metric( + "histogram", + Histogram( + time_unix_nano=1641946016139533244, + start_time_unix_nano=1641946016139533244, + bucket_counts=[1, 4], + sum=67, + explicit_bounds=[10.0, 20.0], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ), } def tearDown(self): @@ -367,7 +379,7 @@ def test_translate_sum_int(self): actual = self.exporter._translate_data([self.metrics["sum_int"]]) self.assertEqual(expected, actual) - def test_translate_sum_float(self): + def test_translate_sum_double(self): expected = ExportMetricsServiceRequest( resource_metrics=[ pb2.ResourceMetrics( @@ -386,7 +398,7 @@ def test_translate_sum_float(self): ), metrics=[ pb2.Metric( - name="sum_float", + name="sum_double", unit="s", description="foo", sum=pb2.Sum( @@ -422,7 +434,7 @@ def test_translate_sum_float(self): ] ) # pylint: disable=protected-access - actual = self.exporter._translate_data([self.metrics["sum_float"]]) + actual = self.exporter._translate_data([self.metrics["sum_double"]]) self.assertEqual(expected, actual) def test_translate_gauge_int(self): @@ -480,7 +492,7 @@ def test_translate_gauge_int(self): actual = self.exporter._translate_data([self.metrics["gauge_int"]]) self.assertEqual(expected, actual) - def test_translate_gauge_float(self): + def test_translate_gauge_double(self): expected = ExportMetricsServiceRequest( resource_metrics=[ pb2.ResourceMetrics( @@ -499,7 +511,7 @@ def test_translate_gauge_float(self): ), metrics=[ pb2.Metric( - name="gauge_float", + name="gauge_double", unit="s", description="foo", gauge=pb2.Gauge( @@ -532,5 +544,66 @@ def test_translate_gauge_float(self): ] ) # pylint: disable=protected-access - actual = self.exporter._translate_data([self.metrics["gauge_float"]]) + actual = self.exporter._translate_data([self.metrics["gauge_double"]]) + self.assertEqual(expected, actual) + + def test_translate_histogram(self): + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_metrics=[ + pb2.InstrumentationLibraryMetrics( + instrumentation_library=InstrumentationLibrary( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + flags=pb2.DataPointFlags.FLAG_NONE, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + ], + ) + ], + ) + ] + ) + # pylint: disable=protected-access + actual = self.exporter._translate_data([self.metrics["histogram"]]) self.assertEqual(expected, actual) From 28a67e3b6536bca93a3d464e3c94742cef5c3d90 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 3 Feb 2022 09:45:53 -0800 Subject: [PATCH 1119/1517] OTLP histo sum (#2435) --- .../exporter/otlp/proto/grpc/_metric_exporter/__init__.py | 1 + .../tests/metrics/test_otlp_metrics_exporter.py | 1 + 2 files changed, 2 insertions(+) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py index df48a15a7b..92aba2958c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py @@ -137,6 +137,7 @@ def _translate_data( time_unix_nano=metric.point.time_unix_nano, start_time_unix_nano=metric.point.start_time_unix_nano, count=sum(metric.point.bucket_counts), + sum=metric.point.sum, bucket_counts=metric.point.bucket_counts, explicit_bounds=metric.point.explicit_bounds, ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index 9c4bd860f5..9c00ee4340 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -589,6 +589,7 @@ def test_translate_histogram(self): start_time_unix_nano=1641946016139533244, time_unix_nano=1641946016139533244, count=5, + sum=67, bucket_counts=[1, 4], explicit_bounds=[10.0, 20.0], exemplars=[], From d00aaeaaa8c50f69d1a306cf41264b22ae4d952b Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 3 Feb 2022 12:01:15 -0600 Subject: [PATCH 1120/1517] Mark test as flaky (#2437) Fixes #2436 Co-authored-by: Alex Boten --- .../tests/metrics/test_periodic_exporting_metric_reader.py | 5 ++++- tox.ini | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index ca7a167630..4552a69063 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -15,6 +15,8 @@ import time from unittest.mock import Mock +from flaky import flaky + from opentelemetry.sdk._metrics.export import ( MetricExporter, PeriodicExportingMetricReader, @@ -99,13 +101,14 @@ def test_ticker_called(self): self.assertTrue(collect_mock.assert_called_once) pmr.shutdown() + @flaky(max_runs=3, min_passes=1) def test_ticker_collects_metrics(self): exporter = FakeMetricsExporter() pmr = self._create_periodic_reader( metrics_list, exporter, interval=100 ) - time.sleep(0.11) + time.sleep(0.15) self.assertEqual(exporter.metrics, metrics_list) pmr.shutdown() diff --git a/tox.ini b/tox.ini index 2999b7fe43..4f7e100469 100644 --- a/tox.ini +++ b/tox.ini @@ -70,6 +70,7 @@ deps = -c dev-requirements.txt opentelemetry: pytest opentelemetry: pytest-benchmark + opentelemetry: flaky coverage: pytest coverage: pytest-cov mypy,mypyinstalled: mypy From b9815b5bcec5704de0094718835b87b361abe767 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 3 Feb 2022 12:22:38 -0600 Subject: [PATCH 1121/1517] Add aggregation temporality conversion algorithm (#2380) * Implement temporality conversion Fixes #2329 * Add aggregation temporality conversion algorithm Fixes #2329 * Fix start_time_unix_nano * Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py Co-authored-by: Srikanth Chekuri * Add warning test * Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py Co-authored-by: Aaron Abbott * Add comments to algorigthm These comments intend to identify the conditional blocks of the algorithm with the sections defined in the design document. * Add check for previous point cumulative aggregation temporality * Added named arguments for gauge * Change to gauge * Fix lint * Remove numbers from comment * Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py Co-authored-by: Srikanth Chekuri * Revert "Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py" This reverts commit 8bdbe4943f4965a3cf3c81dc189374211c6bffd4. Co-authored-by: Srikanth Chekuri Co-authored-by: Aaron Abbott Co-authored-by: Alex Boten --- .../opentelemetry/sdk/_metrics/aggregation.py | 78 +++++ .../tests/metrics/test_aggregation.py | 302 +++++++++++++++++- 2 files changed, 379 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 5048d146b4..9f1bee7a38 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -14,6 +14,7 @@ from abc import ABC, abstractmethod from bisect import bisect_left +from dataclasses import replace from logging import getLogger from math import inf from threading import Lock @@ -200,3 +201,80 @@ def collect(self) -> Optional[Histogram]: aggregation_temporality=AggregationTemporality.DELTA, sum=self._sum, ) + + +def _convert_aggregation_temporality( + previous_point: Optional[_PointVarT], + current_point: _PointVarT, + aggregation_temporality: AggregationTemporality, +) -> _PointVarT: + """Converts `current_point` to the requested `aggregation_temporality` + given the `previous_point`. + + `previous_point` must have `CUMULATIVE` temporality. `current_point` may + have `DELTA` or `CUMULATIVE` temporality. + + The output point will have temporality `aggregation_temporality`. Since + `GAUGE` points have no temporality, they are returned unchanged. + """ + + current_point_type = type(current_point) + + if current_point_type is Gauge: + return current_point + + if previous_point is not None and type(previous_point) is not type( + current_point + ): + _logger.warning( + "convert_aggregation_temporality called with mismatched " + "point types: %s and %s", + type(previous_point), + current_point_type, + ) + + return current_point + + if current_point_type is Sum: + if previous_point is None: + # Output CUMULATIVE for a synchronous instrument + # There is no previous value, return the delta point as a + # cumulative + return replace( + current_point, aggregation_temporality=aggregation_temporality + ) + if previous_point.aggregation_temporality is not ( + AggregationTemporality.CUMULATIVE + ): + raise Exception( + "previous_point aggregation temporality must be CUMULATIVE" + ) + + if current_point.aggregation_temporality is aggregation_temporality: + # Output DELTA for a synchronous instrument + # Output CUMULATIVE for an asynchronous instrument + return current_point + + if aggregation_temporality is AggregationTemporality.DELTA: + # Output temporality DELTA for an asynchronous instrument + value = current_point.value - previous_point.value + output_start_time_unix_nano = previous_point.time_unix_nano + + else: + # Output CUMULATIVE for a synchronous instrument + value = current_point.value + previous_point.value + output_start_time_unix_nano = previous_point.start_time_unix_nano + + is_monotonic = ( + previous_point.is_monotonic and current_point.is_monotonic + ) + + return Sum( + start_time_unix_nano=output_start_time_unix_nano, + time_unix_nano=current_point.time_unix_nano, + value=value, + aggregation_temporality=aggregation_temporality, + is_monotonic=is_monotonic, + ) + + return None diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 535d8d44d5..cd3bbea765 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -12,19 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. - +from dataclasses import replace +from logging import WARNING from math import inf from time import sleep from unittest import TestCase from opentelemetry.sdk._metrics.aggregation import ( + AggregationTemporality, AsynchronousSumAggregation, ExplicitBucketHistogramAggregation, LastValueAggregation, SynchronousSumAggregation, + _convert_aggregation_temporality, _InstrumentMonotonicityAwareAggregation, ) from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk._metrics.point import Gauge, Sum class TestSynchronousSumAggregation(TestCase): @@ -311,3 +315,299 @@ def test_collect(self): self.assertGreater( second_histogram.time_unix_nano, first_histogram.time_unix_nano ) + + +class TestConvertAggregationTemporality(TestCase): + """ + Test aggregation temporality conversion algorithm + """ + + def test_previous_point_non_cumulative(self): + + with self.assertRaises(Exception): + + _convert_aggregation_temporality( + Sum( + start_time_unix_nano=0, + time_unix_nano=0, + value=0, + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=False, + ), + Sum( + start_time_unix_nano=0, + time_unix_nano=0, + value=0, + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=False, + ), + AggregationTemporality.DELTA, + ), + + def test_mismatched_point_types(self): + + current_point = Sum( + start_time_unix_nano=0, + time_unix_nano=0, + value=0, + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=False, + ) + + with self.assertLogs(level=WARNING): + self.assertIs( + _convert_aggregation_temporality( + Gauge(time_unix_nano=0, value=0), + current_point, + AggregationTemporality.DELTA, + ), + current_point, + ) + + def test_current_point_sum_previous_point_none(self): + + current_point = Sum( + start_time_unix_nano=0, + time_unix_nano=0, + value=0, + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=False, + ) + + self.assertEqual( + _convert_aggregation_temporality( + None, current_point, AggregationTemporality.CUMULATIVE + ), + replace( + current_point, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + ), + ) + + def test_current_point_sum_current_point_same_aggregation_temporality( + self, + ): + + current_point = Sum( + start_time_unix_nano=0, + time_unix_nano=0, + value=0, + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=False, + ) + + self.assertEqual( + _convert_aggregation_temporality( + Sum( + start_time_unix_nano=0, + time_unix_nano=0, + value=0, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=False, + ), + current_point, + AggregationTemporality.DELTA, + ), + current_point, + ) + + current_point = Sum( + start_time_unix_nano=0, + time_unix_nano=0, + value=0, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=False, + ) + + self.assertEqual( + _convert_aggregation_temporality( + Sum( + start_time_unix_nano=0, + time_unix_nano=0, + value=0, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=False, + ), + current_point, + AggregationTemporality.CUMULATIVE, + ), + current_point, + ) + + def test_current_point_sum_aggregation_temporality_delta(self): + + self.assertEqual( + _convert_aggregation_temporality( + Sum( + start_time_unix_nano=1, + time_unix_nano=2, + value=3, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=False, + ), + Sum( + start_time_unix_nano=4, + time_unix_nano=5, + value=6, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=False, + ), + AggregationTemporality.DELTA, + ), + Sum( + start_time_unix_nano=2, + time_unix_nano=5, + value=3, + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=False, + ), + ) + + self.assertEqual( + _convert_aggregation_temporality( + Sum( + start_time_unix_nano=1, + time_unix_nano=2, + value=3, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + Sum( + start_time_unix_nano=4, + time_unix_nano=5, + value=6, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=False, + ), + AggregationTemporality.DELTA, + ), + Sum( + start_time_unix_nano=2, + time_unix_nano=5, + value=3, + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=False, + ), + ) + + self.assertEqual( + _convert_aggregation_temporality( + Sum( + start_time_unix_nano=1, + time_unix_nano=2, + value=3, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + Sum( + start_time_unix_nano=4, + time_unix_nano=5, + value=6, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + AggregationTemporality.DELTA, + ), + Sum( + start_time_unix_nano=2, + time_unix_nano=5, + value=3, + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=True, + ), + ) + + def test_current_point_sum_aggregation_temporality_cumulative(self): + + self.assertEqual( + _convert_aggregation_temporality( + Sum( + start_time_unix_nano=1, + time_unix_nano=2, + value=3, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=False, + ), + Sum( + start_time_unix_nano=4, + time_unix_nano=5, + value=6, + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=False, + ), + AggregationTemporality.CUMULATIVE, + ), + Sum( + start_time_unix_nano=1, + time_unix_nano=5, + value=9, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=False, + ), + ) + + self.assertEqual( + _convert_aggregation_temporality( + Sum( + start_time_unix_nano=1, + time_unix_nano=2, + value=3, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + Sum( + start_time_unix_nano=4, + time_unix_nano=5, + value=6, + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=False, + ), + AggregationTemporality.CUMULATIVE, + ), + Sum( + start_time_unix_nano=1, + time_unix_nano=5, + value=9, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=False, + ), + ) + + self.assertEqual( + _convert_aggregation_temporality( + Sum( + start_time_unix_nano=1, + time_unix_nano=2, + value=3, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + Sum( + start_time_unix_nano=4, + time_unix_nano=5, + value=6, + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=True, + ), + AggregationTemporality.CUMULATIVE, + ), + Sum( + start_time_unix_nano=1, + time_unix_nano=5, + value=9, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + ) + + def test_current_point_gauge(self): + + current_point = Gauge(time_unix_nano=1, value=0) + self.assertEqual( + _convert_aggregation_temporality( + Gauge(time_unix_nano=0, value=0), + current_point, + AggregationTemporality.CUMULATIVE, + ), + current_point, + ) From fd9862aee1ef305fca39879931c92429d57106d6 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Sun, 6 Feb 2022 01:04:17 +0530 Subject: [PATCH 1122/1517] Update opentelemetry-sdk min compatible version (#2442) --- CHANGELOG.md | 3 +++ exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f0c93f41d..79a095b3ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.9.1-0.28b1...HEAD) +- `opentelemetry-exporter-otlp-grpc` update SDK dependency to ~1.9. + ([#2442](https://github.com/open-telemetry/opentelemetry-python/pull/2442)) + ## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1-0.28b1) - 2022-01-29 - Update opentelemetry-proto to v0.12.0. Note that this update removes deprecated status codes. diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 829449b31d..1031bea398 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -43,7 +43,7 @@ install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.3 + opentelemetry-sdk ~= 1.9 opentelemetry-proto == 1.9.1 backoff ~= 1.10.0 From d5868fa3d0557662cb073bc075e59d62121493ef Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Tue, 8 Feb 2022 18:51:40 -0500 Subject: [PATCH 1123/1517] [website_docs] Remove folder (#2452) --- scripts/generate_website_docs.sh | 3 +- website_docs/_index.md | 36 ---- website_docs/getting-started.md | 360 ------------------------------- 3 files changed, 1 insertion(+), 398 deletions(-) delete mode 100644 website_docs/_index.md delete mode 100644 website_docs/getting-started.md diff --git a/scripts/generate_website_docs.sh b/scripts/generate_website_docs.sh index aabd6f3e0c..a36c00e712 100755 --- a/scripts/generate_website_docs.sh +++ b/scripts/generate_website_docs.sh @@ -4,8 +4,7 @@ pip install -r docs-requirements.txt -TMP_DIR=/tmp/website_docs +TMP_DIR=/tmp/python_otel_docs rm -Rf ${TMP_DIR} sphinx-build -M jekyll ./docs ${TMP_DIR} -cp ${TMP_DIR}/jekyll/getting-started.md ./website_docs/ \ No newline at end of file diff --git a/website_docs/_index.md b/website_docs/_index.md deleted file mode 100644 index f13cf3f96c..0000000000 --- a/website_docs/_index.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Python -description: >- - - A language-specific implementation of OpenTelemetry in Python. -aliases: [/python, /python/metrics, /python/tracing] -cascade: - github_repo: &repo https://github.com/open-telemetry/opentelemetry-python - github_subdir: website_docs - path_base_for_github_subdir: content/en/docs/python/ - github_project_repo: *repo -weight: 22 ---- - -This is the OpenTelemetry for Python documentation. OpenTelemetry is an -observability framework -- an API, SDK, and tools that are designed to aid in -the generation and collection of application telemetry data such as metrics, -logs, and traces. This documentation is designed to help you understand how to -get started using OpenTelemetry for Python. - -## Status and Releases - -The current status of the major functional components for OpenTelemetry Python -is as follows: - -| Tracing | Metrics | Logging | -| ------- | ------- | ------------ | -| Stable | Alpha | Experimental | - -{{% latest_release "python" /%}} - -## Further Reading - -- [Read the Docs](https://opentelemetry-python.readthedocs.io/en/stable/) -- [Examples](https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples) -- [Contrib Repository](https://github.com/open-telemetry/opentelemetry-python-contrib) diff --git a/website_docs/getting-started.md b/website_docs/getting-started.md deleted file mode 100644 index d993f4bbb7..0000000000 --- a/website_docs/getting-started.md +++ /dev/null @@ -1,360 +0,0 @@ ---- -date: '2021-10-05T20:20:20.000Z' -docname: getting-started -images: {} -path: /getting-started -title: Getting Started ---- - -This guide walks you through instrumenting a Python application with `opentelemetry-python`. - -For more elaborate examples, see [examples](https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples/). - -## Hello world: emit a trace to your console - -To get started, install both the opentelemetry API and SDK: - -``` -pip install opentelemetry-api -pip install opentelemetry-sdk -``` - -The API package provides the interfaces required by the application owner, as well -as some helper logic to load implementations. - -The SDK provides an implementation of those interfaces. The implementation is designed to be generic and extensible enough -that in many situations, the SDK is sufficient. - -Once installed, you can use the packages to emit spans from your application. A span -represents an action within your application that you want to instrument, such as an HTTP request -or a database call. Once instrumented, you can extract helpful information such as -how long the action took. You can also add arbitrary attributes to the span that provide more insight for debugging. - -The following example script emits a trace containing three named spans: “foo”, “bar”, and “baz”: - -``` -# tracing.py -from opentelemetry import trace -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - BatchSpanProcessor, - ConsoleSpanExporter, -) - -provider = TracerProvider() -processor = BatchSpanProcessor(ConsoleSpanExporter()) -provider.add_span_processor(processor) -trace.set_tracer_provider(provider) - - -tracer = trace.get_tracer(__name__) - -with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("baz"): - print("Hello world from OpenTelemetry Python!") -``` - -When you run the script you can see the traces printed to your console: - -``` -$ python tracing_example.py -{ - "name": "baz", - "context": { - "trace_id": "0xb51058883c02f880111c959f3aa786a2", - "span_id": "0xb2fa4c39f5f35e13", - "trace_state": "{}" - }, - "kind": "SpanKind.INTERNAL", - "parent_id": "0x77e577e6a8813bf4", - "start_time": "2020-05-07T14:39:52.906272Z", - "end_time": "2020-05-07T14:39:52.906343Z", - "status": { - "status_code": "OK" - }, - "attributes": {}, - "events": [], - "links": [] -} -{ - "name": "bar", - "context": { - "trace_id": "0xb51058883c02f880111c959f3aa786a2", - "span_id": "0x77e577e6a8813bf4", - "trace_state": "{}" - }, - "kind": "SpanKind.INTERNAL", - "parent_id": "0x3791d950cc5140c5", - "start_time": "2020-05-07T14:39:52.906230Z", - "end_time": "2020-05-07T14:39:52.906601Z", - "status": { - "status_code": "OK" - }, - "attributes": {}, - "events": [], - "links": [] -} -{ - "name": "foo", - "context": { - "trace_id": "0xb51058883c02f880111c959f3aa786a2", - "span_id": "0x3791d950cc5140c5", - "trace_state": "{}" - }, - "kind": "SpanKind.INTERNAL", - "parent_id": null, - "start_time": "2020-05-07T14:39:52.906157Z", - "end_time": "2020-05-07T14:39:52.906743Z", - "status": { - "status_code": "OK" - }, - "attributes": {}, - "events": [], - "links": [] -} -``` - -Each span typically represents a single operation or unit of work. -Spans can be nested, and have a parent-child relationship with other spans. -While a given span is active, newly-created spans inherit the active span’s trace ID, options, and other attributes of its context. -A span without a parent is called the root span, and a trace is comprised of one root span and its descendants. - -In this example, the OpenTelemetry Python library creates one trace containing three spans and prints it to STDOUT. - -## Configure exporters to emit spans elsewhere - -The previous example does emit information about all spans, but the output is a bit hard to read. -In most cases, you can instead *export* this data to an application performance monitoring backend to be visualized and queried. -It’s also common to aggregate span and trace information from multiple services into a single database, so that actions requiring multiple services can still all be visualized together. - -This concept of aggregating span and trace information is known as distributed tracing. One such distributed tracing backend is known as Jaeger. The Jaeger project provides an all-in-one Docker container with a UI, database, and consumer. - -Run the following command to start Jaeger: - -``` -docker run -p 16686:16686 -p 6831:6831/udp jaegertracing/all-in-one -``` - -This command starts Jaeger locally on port 16686 and exposes the Jaeger thrift agent on port 6831. You can visit Jaeger at [http://localhost:16686](http://localhost:16686). - -After you spin up the backend, your application needs to export traces to this system. Although `opentelemetry-sdk` doesn’t provide an exporter -for Jaeger, you can install it as a separate package with the following command: - -``` -pip install opentelemetry-exporter-jaeger -``` - -After you install the exporter, update your code to import the Jaeger exporter and use that instead: - -``` -# jaeger_example.py -from opentelemetry import trace -from opentelemetry.exporter.jaeger.thrift import JaegerExporter -from opentelemetry.sdk.resources import SERVICE_NAME, Resource -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor - -trace.set_tracer_provider( - TracerProvider( - resource=Resource.create({SERVICE_NAME: "my-helloworld-service"}) - ) -) - -jaeger_exporter = JaegerExporter( - agent_host_name="localhost", - agent_port=6831, -) - -trace.get_tracer_provider().add_span_processor( - BatchSpanProcessor(jaeger_exporter) -) - -tracer = trace.get_tracer(__name__) - -with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("baz"): - print("Hello world from OpenTelemetry Python!") -``` - -Finally, run the Python script: - -``` -python jaeger_example.py -``` - -You can then visit the Jaeger UI, see your service under “services”, and find your traces! - -## Instrumentation example with Flask - -While the example in the previous section is great, it’s very manual. The following are common actions you might want to track and include as part of your distributed tracing. - - -* HTTP responses from web services - - -* HTTP requests from clients - - -* Database calls - -To track these common actions, OpenTelemetry has the concept of instrumentations. Instrumentations are packages designed to interface -with a specific framework or library, such as Flask and psycopg2. You can find a list of the currently curated extension packages in the [Contrib repository](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation). - -Instrument a basic Flask application that uses the requests library to send HTTP requests. First, install the instrumentation packages themselves: - -``` -pip install opentelemetry-instrumentation-flask -pip install opentelemetry-instrumentation-requests -``` - -The following small Flask application sends an HTTP request and also activates each instrumentation during its initialization: - -``` -# flask_example.py -import flask -import requests - -from opentelemetry import trace -from opentelemetry.instrumentation.flask import FlaskInstrumentor -from opentelemetry.instrumentation.requests import RequestsInstrumentor -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - BatchSpanProcessor, - ConsoleSpanExporter, -) - -trace.set_tracer_provider(TracerProvider()) -trace.get_tracer_provider().add_span_processor( - BatchSpanProcessor(ConsoleSpanExporter()) -) - -app = flask.Flask(__name__) -FlaskInstrumentor().instrument_app(app) -RequestsInstrumentor().instrument() - -tracer = trace.get_tracer(__name__) - - -@app.route("/") -def hello(): - with tracer.start_as_current_span("example-request"): - requests.get("http://www.example.com") - return "hello" - - -app.run(debug=True, port=5000) -``` - -Now run the script, hit the root URL ([http://localhost:5000/](http://localhost:5000/)) a few times, and watch your spans be emitted! - -``` -python flask_example.py -``` - -## Configure Your HTTP propagator (b3, Baggage) - -A major feature of distributed tracing is the ability to correlate a trace across -multiple services. However, those services need to propagate information about a -trace from one service to the other. - -To enable this propagation, OpenTelemetry has the concept of [propagators]({{< relref "/docs/reference/specification/context/api-propagators" >}}), -which provide a common method to encode and decode span information from a request and response, respectively. - -By default, `opentelemetry-python` is configured to use the [W3C Trace Context](https://www.w3.org/TR/trace-context/) -and [W3C Baggage](https://www.w3.org/TR/baggage/) HTTP headers for HTTP requests, but you can configure it to leverage different propagators. Here’s -an example using Zipkin’s [b3 propagation](https://github.com/openzipkin/b3-propagation): - -``` -pip install opentelemetry-propagator-b3 -``` - -Following the installation of the package containing the b3 propagator, configure the propagator as follows: - -``` -from opentelemetry.propagate import set_global_textmap -from opentelemetry.propagators.b3 import B3Format - -set_global_textmap(B3Format()) -``` - -## Use the OpenTelemetry Collector for traces - -Although it’s possible to directly export your telemetry data to specific backends, you might have more complex use cases such as the following: - - -* A single telemetry sink shared by multiple services, to reduce overhead of switching exporters. - - -* Aggregating traces across multiple services, running on multiple hosts. - -To enable a broad range of aggregation strategies, OpenTelemetry provides the [opentelemetry-collector](https://github.com/open-telemetry/opentelemetry-collector). -The Collector is a flexible application that can consume trace data and export to multiple other backends, including to another instance of the Collector. - -Start the Collector locally to see how the Collector works in practice. Write the following file: - -``` -# /tmp/otel-collector-config.yaml -receivers: - otlp: - protocols: - grpc: - http: -exporters: - logging: - loglevel: debug -processors: - batch: -service: - pipelines: - traces: - receivers: [otlp] - exporters: [logging] - processors: [batch] -``` - -Then start the Docker container: - -``` -docker run -p 4317:4317 \ - -v /tmp/otel-collector-config.yaml:/etc/otel-collector-config.yaml \ - otel/opentelemetry-collector:latest \ - --config=/etc/otel-collector-config.yaml -``` - -Install the OpenTelemetry Collector exporter: - -``` -pip install opentelemetry-exporter-otlp -``` - -Finally, execute the following script: - -``` -# otcollector.py - -from opentelemetry import trace -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( - OTLPSpanExporter, -) -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor - -span_exporter = OTLPSpanExporter( - # optional - # endpoint="myCollectorURL:4317", - # credentials=ChannelCredentials(credentials), - # headers=(("metadata", "metadata")), -) -tracer_provider = TracerProvider() -trace.set_tracer_provider(tracer_provider) -span_processor = BatchSpanProcessor(span_exporter) -tracer_provider.add_span_processor(span_processor) - -# Configure the tracer to use the collector exporter -tracer = trace.get_tracer_provider().get_tracer(__name__) - -with tracer.start_as_current_span("foo"): - print("Hello world!") -``` From e0a4f318df4ba3a230ecd7121e0a59ac445da34d Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 10 Feb 2022 22:26:30 +0530 Subject: [PATCH 1124/1517] bugfix(auto-instrumentation): attach OTLPHandler to root logger (#2450) --- CHANGELOG.md | 2 + .../sdk/_configuration/__init__.py | 9 +- opentelemetry-sdk/tests/test_configurator.py | 121 +++++++++++++++++- 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a095b3ad..10d2949154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-exporter-otlp-grpc` update SDK dependency to ~1.9. ([#2442](https://github.com/open-telemetry/opentelemetry-python/pull/2442)) +- bugfix(auto-instrumentation): attach OTLPHandler to root logger + ([#2450](https://github.com/open-telemetry/opentelemetry-python/pull/2450)) ## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1-0.28b1) - 2022-01-29 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index fdd3a25a7c..124573c254 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -17,6 +17,7 @@ OpenTelemetry SDK Configurator for Easy Instrumentation with Distros """ +import logging from abc import ABC, abstractmethod from os import environ from typing import Dict, Optional, Sequence, Tuple, Type @@ -31,6 +32,7 @@ ) from opentelemetry.sdk._logs import ( LogEmitterProvider, + OTLPHandler, set_log_emitter_provider, ) from opentelemetry.sdk._logs.export import BatchLogProcessor, LogExporter @@ -91,7 +93,7 @@ def _init_tracing( def _init_logging( - exporters: Dict[str, Sequence[LogExporter]], + exporters: Dict[str, Type[LogExporter]], auto_instrumentation_version: Optional[str] = None, ): # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name @@ -111,6 +113,11 @@ def _init_logging( BatchLogProcessor(exporter_class(**exporter_args)) ) + log_emitter = provider.get_log_emitter(__name__) + handler = OTLPHandler(level=logging.NOTSET, log_emitter=log_emitter) + + logging.getLogger().addHandler(handler) + def _import_config_components( selected_components, entry_point_name diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 32c939df14..e0dd1e7b4b 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -13,6 +13,7 @@ # limitations under the License. # type: ignore +import logging from os import environ from unittest import TestCase from unittest.mock import patch @@ -26,8 +27,10 @@ _get_id_generator, _import_exporters, _import_id_generator, + _init_logging, _init_tracing, ) +from opentelemetry.sdk._logs import OTLPHandler from opentelemetry.sdk._logs.export import ConsoleLogExporter from opentelemetry.sdk._metrics.export import ConsoleMetricExporter from opentelemetry.sdk.resources import SERVICE_NAME, Resource @@ -45,6 +48,45 @@ def add_span_processor(self, processor): self.processor = processor +class DummyLogEmitterProvider: + def __init__(self, resource=None): + self.resource = resource + self.processor = DummyLogProcessor(DummyOTLPLogExporter()) + + def add_log_processor(self, processor): + self.processor = processor + + def get_log_emitter(self, name): + return DummyLogEmitter(name, self.resource, self.processor) + + +class DummyLogEmitter: + def __init__(self, name, resource, processor): + self.name = name + self.resource = resource + self.processor = processor + + def emit(self, record): + self.processor.emit(record) + + def flush(self): + pass + + +class DummyLogProcessor: + def __init__(self, exporter): + self.exporter = exporter + + def emit(self, record): + self.exporter.export([record]) + + def force_flush(self, time): + pass + + def shutdown(self): + pass + + class Processor: def __init__(self, exporter): self.exporter = exporter @@ -63,10 +105,21 @@ def shutdown(self): pass -class OTLPExporter: +class OTLPSpanExporter: pass +class DummyOTLPLogExporter: + def __init__(self, *args, **kwargs): + self.export_called = False + + def export(self, batch): + self.export_called = True + + def shutdown(self): + pass + + class CustomIdGenerator(IdGenerator): def generate_span_id(self): pass @@ -133,14 +186,14 @@ def test_trace_init_default(self): {"OTEL_RESOURCE_ATTRIBUTES": "service.name=my-otlp-test-service"}, ) def test_trace_init_otlp(self): - _init_tracing({"otlp": OTLPExporter}, RandomIdGenerator) + _init_tracing({"otlp": OTLPSpanExporter}, RandomIdGenerator) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, Provider) self.assertIsInstance(provider.id_generator, RandomIdGenerator) self.assertIsInstance(provider.processor, Processor) - self.assertIsInstance(provider.processor.exporter, OTLPExporter) + self.assertIsInstance(provider.processor.exporter, OTLPSpanExporter) self.assertIsInstance(provider.resource, Resource) self.assertEqual( provider.resource.attributes.get("service.name"), @@ -163,6 +216,68 @@ def test_trace_init_custom_id_generator(self, mock_iter_entry_points): self.assertIsInstance(provider.id_generator, CustomIdGenerator) +class TestLoggingInit(TestCase): + def setUp(self): + self.processor_patch = patch( + "opentelemetry.sdk._configuration.BatchLogProcessor", + DummyLogProcessor, + ) + self.provider_patch = patch( + "opentelemetry.sdk._configuration.LogEmitterProvider", + DummyLogEmitterProvider, + ) + self.set_provider_patch = patch( + "opentelemetry.sdk._configuration.set_log_emitter_provider" + ) + + self.processor_mock = self.processor_patch.start() + self.provider_mock = self.provider_patch.start() + self.set_provider_mock = self.set_provider_patch.start() + + def tearDown(self): + self.processor_patch.stop() + self.set_provider_patch.stop() + self.provider_patch.stop() + root_logger = logging.getLogger("root") + root_logger.handlers = [ + handler + for handler in root_logger.handlers + if not isinstance(handler, OTLPHandler) + ] + + def test_logging_init_empty(self): + _init_logging({}, "auto-version") + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyLogEmitterProvider) + self.assertIsInstance(provider.resource, Resource) + self.assertEqual( + provider.resource.attributes.get("telemetry.auto.version"), + "auto-version", + ) + + @patch.dict( + environ, + {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, + ) + def test_logging_init_exporter(self): + _init_logging({"otlp": DummyOTLPLogExporter}) + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyLogEmitterProvider) + self.assertIsInstance(provider.resource, Resource) + self.assertEqual( + provider.resource.attributes.get("service.name"), + "otlp-service", + ) + self.assertIsInstance(provider.processor, DummyLogProcessor) + self.assertIsInstance( + provider.processor.exporter, DummyOTLPLogExporter + ) + logging.getLogger(__name__).error("hello") + self.assertTrue(provider.processor.exporter.export_called) + + class TestExporterNames(TestCase): def test_otlp_exporter_overwrite(self): for exporter in [_EXPORTER_OTLP, _EXPORTER_OTLP_PROTO_GRPC]: From a01c49f43ff677b144ec15042dfadff5f642549e Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 11 Feb 2022 13:13:10 -0500 Subject: [PATCH 1125/1517] Only check for public symbols in public packages (#2458) --- scripts/public_symbols_checker.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/public_symbols_checker.py b/scripts/public_symbols_checker.py index 3be1c3ee8b..d359008f1f 100644 --- a/scripts/public_symbols_checker.py +++ b/scripts/public_symbols_checker.py @@ -34,10 +34,18 @@ def get_symbols(change_type, diff_lines_getter, prefix): ): b_file_path = diff_lines.b_blob.path + b_file_path_obj = Path(b_file_path) if ( - Path(b_file_path).suffix != ".py" + b_file_path_obj.suffix != ".py" or "opentelemetry" not in b_file_path + or any( + # single leading underscore + part[0] == "_" and part[1] != "_" + # tests directories + or part == "tests" + for part in b_file_path_obj.parts + ) ): continue From 8006a496791bef5a342e0510930050e59a6543f0 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Sat, 12 Feb 2022 00:09:47 +0530 Subject: [PATCH 1126/1517] Bump semantic conventions from 1.6.1 to 1.8.0 (#2461) --- CHANGELOG.md | 2 + .../semconv/resource/__init__.py | 33 ++++- .../opentelemetry/semconv/trace/__init__.py | 140 +++++++++++++++--- scripts/semconv/generate.sh | 4 +- 4 files changed, 157 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10d2949154..eb28471034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2442](https://github.com/open-telemetry/opentelemetry-python/pull/2442)) - bugfix(auto-instrumentation): attach OTLPHandler to root logger ([#2450](https://github.com/open-telemetry/opentelemetry-python/pull/2450)) +- Bump semantic conventions from 1.6.1 to 1.8.0 + ([#2461](https://github.com/open-telemetry/opentelemetry-python/pull/2461)) ## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1-0.28b1) - 2022-01-29 diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py index af10a64c9a..710c30a77b 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py @@ -28,7 +28,8 @@ class ResourceAttributes: CLOUD_REGION = "cloud.region" """ - The geographical region the resource is running. Refer to your provider's docs to see the available regions, for example [Alibaba Cloud regions](https://www.alibabacloud.com/help/doc-detail/40654.htm), [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/en-us/global-infrastructure/geographies/), or [Google Cloud regions](https://cloud.google.com/about/locations). + The geographical region the resource is running. + Note: Refer to your provider's docs to see the available regions, for example [Alibaba Cloud regions](https://www.alibabacloud.com/help/doc-detail/40654.htm), [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/en-us/global-infrastructure/geographies/), [Google Cloud regions](https://cloud.google.com/about/locations), or [Tencent Cloud regions](https://intl.cloud.tencent.com/document/product/213/6091). """ CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone" @@ -103,7 +104,7 @@ class ResourceAttributes: CONTAINER_NAME = "container.name" """ - Container name. + Container name used by container runtime. """ CONTAINER_ID = "container.id" @@ -267,7 +268,12 @@ class ResourceAttributes: K8S_CONTAINER_NAME = "k8s.container.name" """ - The name of the Container in a Pod template. + The name of the Container from Pod specification, must be unique within a Pod. Container runtime usually uses different globally unique name (`container.name`). + """ + + K8S_CONTAINER_RESTART_COUNT = "k8s.container.restart_count" + """ + Number of times the container was restarted. This attribute can be used to identify a particular container (running or stopped) within a container spec. """ K8S_REPLICASET_UID = "k8s.replicaset.uid" @@ -472,6 +478,9 @@ class CloudProviderValues(Enum): GCP = "gcp" """Google Cloud Platform.""" + TENCENT_CLOUD = "tencent_cloud" + """Tencent Cloud.""" + class CloudPlatformValues(Enum): ALIBABA_CLOUD_ECS = "alibaba_cloud_ecs" @@ -495,6 +504,9 @@ class CloudPlatformValues(Enum): AWS_ELASTIC_BEANSTALK = "aws_elastic_beanstalk" """AWS Elastic Beanstalk.""" + AWS_APP_RUNNER = "aws_app_runner" + """AWS App Runner.""" + AZURE_VM = "azure_vm" """Azure Virtual Machines.""" @@ -525,6 +537,15 @@ class CloudPlatformValues(Enum): GCP_APP_ENGINE = "gcp_app_engine" """Google Cloud App Engine (GAE).""" + TENCENT_CLOUD_CVM = "tencent_cloud_cvm" + """Tencent Cloud Cloud Virtual Machine (CVM).""" + + TENCENT_CLOUD_EKS = "tencent_cloud_eks" + """Tencent Cloud Elastic Kubernetes Service (EKS).""" + + TENCENT_CLOUD_SCF = "tencent_cloud_scf" + """Tencent Cloud Serverless Cloud Function (SCF).""" + class AwsEcsLaunchtypeValues(Enum): EC2 = "ec2" @@ -553,6 +574,9 @@ class HostArchValues(Enum): PPC64 = "ppc64" """64-bit PowerPC.""" + S390X = "s390x" + """IBM z/Architecture.""" + X86 = "x86" """32-bit x86.""" @@ -622,3 +646,6 @@ class TelemetrySdkLanguageValues(Enum): WEBJS = "webjs" """webjs.""" + + SWIFT = "swift" + """swift.""" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py index 5d32bbef99..a762d88c2c 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py @@ -46,8 +46,8 @@ class SpanAttributes: DB_NAME = "db.name" """ - If no [tech-specific attribute](#call-level-attributes-for-specific-technologies) is defined, this attribute is used to report the name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). - Note: In some SQL databases, the database name to be used is called "schema name". + This attribute is used to report the name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). + Note: In some SQL databases, the database name to be used is called "schema name". In case there are multiple layers that could be considered for database name (e.g. Oracle instance name and schema name), the database name to be used is the more specific layer (e.g. Oracle schema name). """ DB_STATEMENT = "db.statement" @@ -88,11 +88,6 @@ class SpanAttributes: Note: If setting a `db.mssql.instance_name`, `net.peer.port` is no longer required (but still recommended if non-standard). """ - DB_CASSANDRA_KEYSPACE = "db.cassandra.keyspace" - """ - The name of the keyspace being accessed. To be used instead of the generic `db.name` attribute. - """ - DB_CASSANDRA_PAGE_SIZE = "db.cassandra.page_size" """ The fetch size used for paging, i.e. how many rows will be returned at once. @@ -105,7 +100,7 @@ class SpanAttributes: DB_CASSANDRA_TABLE = "db.cassandra.table" """ - The name of the primary table that the operation is acting upon, including the schema name (if applicable). + The name of the primary table that the operation is acting upon, including the keyspace name (if applicable). Note: This mirrors the db.sql.table attribute but references cassandra rather than sql. It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. """ @@ -131,11 +126,6 @@ class SpanAttributes: The data center of the coordinating node for a query. """ - DB_HBASE_NAMESPACE = "db.hbase.namespace" - """ - The [HBase namespace](https://hbase.apache.org/book.html#_namespace) being accessed. To be used instead of the generic `db.name` attribute. - """ - DB_REDIS_DATABASE_INDEX = "db.redis.database_index" """ The index of the database being accessed as used in the [`SELECT` command](https://redis.io/commands/select), provided as an integer. To be used instead of the generic `db.name` attribute. @@ -148,7 +138,7 @@ class SpanAttributes: DB_SQL_TABLE = "db.sql.table" """ - The name of the primary table that the operation is acting upon, including the schema name (if applicable). + The name of the primary table that the operation is acting upon, including the database name (if applicable). Note: It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. """ @@ -190,7 +180,16 @@ class SpanAttributes: FAAS_TRIGGER = "faas.trigger" """ - Type of the trigger on which the function is executed. + Type of the trigger which caused this function execution. + Note: For the server/consumer span on the incoming side, +`faas.trigger` MUST be set. + +Clients invoking FaaS instances usually cannot set `faas.trigger`, +since they would typically need to look in the payload to determine +the event type. If clients set it, it should be the same as the +trigger that corresponding incoming would have (i.e., this has +nothing to do with the underlying transport used to make the API +call to invoke the lambda, which is often HTTP). """ FAAS_EXECUTION = "faas.execution" @@ -236,7 +235,8 @@ class SpanAttributes: HTTP_HOST = "http.host" """ - The value of the [HTTP host header](https://tools.ietf.org/html/rfc7230#section-5.4). When the header is empty or not present, this attribute should be the same. + The value of the [HTTP host header](https://tools.ietf.org/html/rfc7230#section-5.4). An empty Host header should also be reported, see note. + Note: When the header is present but empty the attribute SHOULD be set to the empty string. Note that this is a valid situation that is expected in certain cases, according the aforementioned [section of RFC 7230](https://tools.ietf.org/html/rfc7230#section-5.4). When the header is not set the attribute MUST NOT be set. """ HTTP_SCHEME = "http.scheme" @@ -298,7 +298,17 @@ class SpanAttributes: HTTP_CLIENT_IP = "http.client_ip" """ The IP address of the original client behind all proxies, if known (e.g. from [X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)). - Note: This is not necessarily the same as `net.peer.ip`, which would identify the network-level peer, which may be a proxy. + Note: This is not necessarily the same as `net.peer.ip`, which would +identify the network-level peer, which may be a proxy. + +This attribute should be set when a source of information different +from the one used for `net.peer.ip`, is available even if that other +source just confirms the same value as `net.peer.ip`. +Rationale: For `net.peer.ip`, one typically does not know if it +comes from a proxy, reverse proxy, or the actual client. Setting +`http.client_ip` when it's the same as `net.peer.ip` means that +one is at least somewhat confident that the address is not that of +the closest proxy. """ NET_HOST_IP = "net.host.ip" @@ -632,6 +642,11 @@ class SpanAttributes: A string identifying the kind of message consumption as defined in the [Operation names](#operation-names) section above. If the operation is "send", this attribute MUST NOT be set, since the operation can be inferred from the span kind in that case. """ + MESSAGING_CONSUMER_ID = "messaging.consumer_id" + """ + The identifier for the consumer receiving a message. For Kafka, set it to `{messaging.kafka.consumer_group} - {messaging.kafka.client_id}`, if both are present, or only `messaging.kafka.consumer_group`. For brokers, such as RabbitMQ and Artemis, set it to the `client_id` of the client consuming the message. + """ + MESSAGING_RABBITMQ_ROUTING_KEY = "messaging.rabbitmq.routing_key" """ RabbitMQ message routing key. @@ -663,6 +678,43 @@ class SpanAttributes: A boolean that is true if the message is a tombstone. """ + MESSAGING_ROCKETMQ_NAMESPACE = "messaging.rocketmq.namespace" + """ + Namespace of RocketMQ resources, resources in different namespaces are individual. + """ + + MESSAGING_ROCKETMQ_CLIENT_GROUP = "messaging.rocketmq.client_group" + """ + Name of the RocketMQ producer/consumer group that is handling the message. The client type is identified by the SpanKind. + """ + + MESSAGING_ROCKETMQ_CLIENT_ID = "messaging.rocketmq.client_id" + """ + The unique identifier for each client. + """ + + MESSAGING_ROCKETMQ_MESSAGE_TYPE = "messaging.rocketmq.message_type" + """ + Type of message. + """ + + MESSAGING_ROCKETMQ_MESSAGE_TAG = "messaging.rocketmq.message_tag" + """ + The secondary classifier of message besides topic. + """ + + MESSAGING_ROCKETMQ_MESSAGE_KEYS = "messaging.rocketmq.message_keys" + """ + Key(s) of message, another way to mark message besides message id. + """ + + MESSAGING_ROCKETMQ_CONSUMPTION_MODEL = ( + "messaging.rocketmq.consumption_model" + ) + """ + Model of message consumption. This only applies to consumer spans. + """ + RPC_GRPC_STATUS_CODE = "rpc.grpc.status_code" """ The [numeric status code](https://github.com/grpc/grpc/blob/v1.33.2/doc/statuscodes.md) of the gRPC request. @@ -688,6 +740,27 @@ class SpanAttributes: `error.message` property of response if it is an error response. """ + MESSAGE_TYPE = "message.type" + """ + Whether this is a received or sent message. + """ + + MESSAGE_ID = "message.id" + """ + MUST be calculated as two different counters starting from `1` one for sent messages and one for received message. + Note: This way we guarantee that the values will be consistent between different implementations. + """ + + MESSAGE_COMPRESSED_SIZE = "message.compressed_size" + """ + Compressed size of the message in bytes. + """ + + MESSAGE_UNCOMPRESSED_SIZE = "message.uncompressed_size" + """ + Uncompressed size of the message in bytes. + """ + class DbSystemValues(Enum): OTHER_SQL = "other_sql" @@ -1038,6 +1111,9 @@ class FaasInvokedProviderValues(Enum): GCP = "gcp" """Google Cloud Platform.""" + TENCENT_CLOUD = "tencent_cloud" + """Tencent Cloud.""" + class MessagingOperationValues(Enum): RECEIVE = "receive" @@ -1047,6 +1123,28 @@ class MessagingOperationValues(Enum): """process.""" +class MessagingRocketmqMessageTypeValues(Enum): + NORMAL = "normal" + """Normal message.""" + + FIFO = "fifo" + """FIFO message.""" + + DELAY = "delay" + """Delay message.""" + + TRANSACTION = "transaction" + """Transaction message.""" + + +class MessagingRocketmqConsumptionModelValues(Enum): + CLUSTERING = "clustering" + """Clustering consumption model.""" + + BROADCASTING = "broadcasting" + """Broadcasting consumption model.""" + + class RpcGrpcStatusCodeValues(Enum): OK = 0 """OK.""" @@ -1098,3 +1196,11 @@ class RpcGrpcStatusCodeValues(Enum): UNAUTHENTICATED = 16 """UNAUTHENTICATED.""" + + +class MessageTypeValues(Enum): + SENT = "SENT" + """sent.""" + + RECEIVED = "RECEIVED" + """received.""" diff --git a/scripts/semconv/generate.sh b/scripts/semconv/generate.sh index d793bf5809..48c46a78c6 100755 --- a/scripts/semconv/generate.sh +++ b/scripts/semconv/generate.sh @@ -4,8 +4,8 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR="${SCRIPT_DIR}/../../" # freeze the spec version to make SemanticAttributes generation reproducible -SPEC_VERSION=v1.6.1 -OTEL_SEMCONV_GEN_IMG_VERSION=0.5.0 +SPEC_VERSION=v1.8.0 +OTEL_SEMCONV_GEN_IMG_VERSION=0.9.0 cd ${SCRIPT_DIR} From ec3053efd835d1776112447808712ba52f9c70ad Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 11 Feb 2022 14:24:38 -0600 Subject: [PATCH 1127/1517] Merge Asynchronous and Synchronous sum aggregations (#2379) * Merge Asynchronous and Synchronous sum aggregations Fixes #2353 * Don't use None for self._value * Remove temporality check from sum aggregate * Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py Co-authored-by: Aaron Abbott * Add cumulative test cases * Fix lint * Set value to None This is done in order to identify the situation when aggregate has not been called before collect is. * Refactor collect * Fix initial value * Update opentelemetry-sdk/tests/metrics/test_aggregation.py Co-authored-by: Aaron Abbott * Undo changes to lastvalueaggrgation * Fix test case Co-authored-by: Aaron Abbott --- .../opentelemetry/sdk/_metrics/aggregation.py | 78 +++++------ .../tests/metrics/test_aggregation.py | 125 +++++++----------- 2 files changed, 79 insertions(+), 124 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 9f1bee7a38..b2341d9063 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -35,12 +35,6 @@ _logger = getLogger(__name__) -class _InstrumentMonotonicityAwareAggregation: - def __init__(self, instrument_is_monotonic: bool): - self._instrument_is_monotonic = instrument_is_monotonic - super().__init__() - - class Aggregation(ABC, Generic[_PointVarT]): def __init__(self): self._lock = Lock() @@ -54,16 +48,27 @@ def collect(self) -> Optional[_PointVarT]: pass -class SynchronousSumAggregation( - _InstrumentMonotonicityAwareAggregation, Aggregation[Sum] -): - def __init__(self, instrument_is_monotonic: bool): - super().__init__(instrument_is_monotonic) - self._value = 0 +class SumAggregation(Aggregation[Sum]): + def __init__( + self, + instrument_is_monotonic: bool, + instrument_temporality: AggregationTemporality, + ): + super().__init__() + self._start_time_unix_nano = _time_ns() + self._instrument_temporality = instrument_temporality + self._instrument_is_monotonic = instrument_is_monotonic + + if self._instrument_temporality is AggregationTemporality.DELTA: + self._value = 0 + else: + self._value = None def aggregate(self, measurement: Measurement) -> None: with self._lock: + if self._value is None: + self._value = 0 self._value = self._value + measurement.value def collect(self) -> Optional[Sum]: @@ -73,47 +78,32 @@ def collect(self) -> Optional[Sum]: """ now = _time_ns() - with self._lock: - value = self._value - start_time_unix_nano = self._start_time_unix_nano - - self._value = 0 - self._start_time_unix_nano = now + 1 - - return Sum( - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=self._instrument_is_monotonic, - start_time_unix_nano=start_time_unix_nano, - time_unix_nano=now, - value=value, - ) + if self._instrument_temporality is AggregationTemporality.DELTA: + with self._lock: + value = self._value + start_time_unix_nano = self._start_time_unix_nano -class AsynchronousSumAggregation( - _InstrumentMonotonicityAwareAggregation, Aggregation[Sum] -): - def __init__(self, instrument_is_monotonic: bool): - super().__init__(instrument_is_monotonic) - self._value = None - self._start_time_unix_nano = _time_ns() + self._value = 0 + self._start_time_unix_nano = now + 1 - def aggregate(self, measurement: Measurement) -> None: - with self._lock: - self._value = measurement.value + return Sum( + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=self._instrument_is_monotonic, + start_time_unix_nano=start_time_unix_nano, + time_unix_nano=now, + value=value, + ) - def collect(self) -> Optional[Sum]: - """ - Atomically return a point for the current value of the metric. - """ if self._value is None: return None return Sum( - start_time_unix_nano=self._start_time_unix_nano, - time_unix_nano=_time_ns(), - value=self._value, aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=self._instrument_is_monotonic, + start_time_unix_nano=self._start_time_unix_nano, + time_unix_nano=now, + value=self._value, ) @@ -180,7 +170,7 @@ def aggregate(self, measurement: Measurement) -> None: self._bucket_counts[bisect_left(self._boundaries, value)] += 1 - def collect(self) -> Optional[Histogram]: + def collect(self) -> Histogram: """ Atomically return a point for the current value of the metric. """ diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index cd3bbea765..f6d3ce3ca8 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -20,39 +20,49 @@ from opentelemetry.sdk._metrics.aggregation import ( AggregationTemporality, - AsynchronousSumAggregation, ExplicitBucketHistogramAggregation, LastValueAggregation, - SynchronousSumAggregation, + SumAggregation, _convert_aggregation_temporality, - _InstrumentMonotonicityAwareAggregation, ) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import Gauge, Sum class TestSynchronousSumAggregation(TestCase): - def test_instrument_monotonicity_awareness(self): + def test_aggregate_delta(self): """ - `SynchronousSumAggregation` is aware of the instrument monotonicity + `SynchronousSumAggregation` aggregates data for sum metric points """ - synchronous_sum_aggregation = SynchronousSumAggregation(True) - self.assertIsInstance( - synchronous_sum_aggregation, - _InstrumentMonotonicityAwareAggregation, + synchronous_sum_aggregation = SumAggregation( + True, AggregationTemporality.DELTA ) - self.assertTrue(synchronous_sum_aggregation._instrument_is_monotonic) - synchronous_sum_aggregation = SynchronousSumAggregation(False) - self.assertFalse(synchronous_sum_aggregation._instrument_is_monotonic) + synchronous_sum_aggregation.aggregate(Measurement(1)) + synchronous_sum_aggregation.aggregate(Measurement(2)) + synchronous_sum_aggregation.aggregate(Measurement(3)) - def test_aggregate(self): + self.assertEqual(synchronous_sum_aggregation._value, 6) + + synchronous_sum_aggregation = SumAggregation( + True, AggregationTemporality.DELTA + ) + + synchronous_sum_aggregation.aggregate(Measurement(1)) + synchronous_sum_aggregation.aggregate(Measurement(-2)) + synchronous_sum_aggregation.aggregate(Measurement(3)) + + self.assertEqual(synchronous_sum_aggregation._value, 2) + + def test_aggregate_cumulative(self): """ `SynchronousSumAggregation` aggregates data for sum metric points """ - synchronous_sum_aggregation = SynchronousSumAggregation(True) + synchronous_sum_aggregation = SumAggregation( + True, AggregationTemporality.CUMULATIVE + ) synchronous_sum_aggregation.aggregate(Measurement(1)) synchronous_sum_aggregation.aggregate(Measurement(2)) @@ -60,7 +70,9 @@ def test_aggregate(self): self.assertEqual(synchronous_sum_aggregation._value, 6) - synchronous_sum_aggregation = SynchronousSumAggregation(True) + synchronous_sum_aggregation = SumAggregation( + True, AggregationTemporality.CUMULATIVE + ) synchronous_sum_aggregation.aggregate(Measurement(1)) synchronous_sum_aggregation.aggregate(Measurement(-2)) @@ -68,12 +80,14 @@ def test_aggregate(self): self.assertEqual(synchronous_sum_aggregation._value, 2) - def test_collect(self): + def test_collect_delta(self): """ `SynchronousSumAggregation` collects sum metric points """ - synchronous_sum_aggregation = SynchronousSumAggregation(True) + synchronous_sum_aggregation = SumAggregation( + True, AggregationTemporality.DELTA + ) synchronous_sum_aggregation.aggregate(Measurement(1)) first_sum = synchronous_sum_aggregation.collect() @@ -91,87 +105,37 @@ def test_collect(self): second_sum.start_time_unix_nano, first_sum.start_time_unix_nano ) - -class TestAsynchronousSumAggregation(TestCase): - def test_instrument_monotonicity_awareness(self): + def test_collect_cumulative(self): """ - `AsynchronousSumAggregation` is aware of the instrument monotonicity + `SynchronousSumAggregation` collects sum metric points """ - asynchronous_sum_aggregation = AsynchronousSumAggregation(True) - self.assertIsInstance( - asynchronous_sum_aggregation, - _InstrumentMonotonicityAwareAggregation, + sum_aggregation = SumAggregation( + True, AggregationTemporality.CUMULATIVE ) - self.assertTrue(asynchronous_sum_aggregation._instrument_is_monotonic) - - asynchronous_sum_aggregation = AsynchronousSumAggregation(False) - self.assertFalse(asynchronous_sum_aggregation._instrument_is_monotonic) - - def test_aggregate(self): - """ - `AsynchronousSumAggregation` aggregates data for sum metric points - """ - - asynchronous_sum_aggregation = AsynchronousSumAggregation(True) - - asynchronous_sum_aggregation.aggregate(Measurement(1)) - self.assertEqual(asynchronous_sum_aggregation._value, 1) - - asynchronous_sum_aggregation.aggregate(Measurement(2)) - self.assertEqual(asynchronous_sum_aggregation._value, 2) - - asynchronous_sum_aggregation.aggregate(Measurement(3)) - self.assertEqual(asynchronous_sum_aggregation._value, 3) - - asynchronous_sum_aggregation = AsynchronousSumAggregation(True) - - asynchronous_sum_aggregation.aggregate(Measurement(1)) - self.assertEqual(asynchronous_sum_aggregation._value, 1) - - asynchronous_sum_aggregation.aggregate(Measurement(-2)) - self.assertEqual(asynchronous_sum_aggregation._value, -2) - - asynchronous_sum_aggregation.aggregate(Measurement(3)) - self.assertEqual(asynchronous_sum_aggregation._value, 3) - - def test_collect(self): - """ - `AsynchronousSumAggregation` collects sum metric points - """ - - asynchronous_sum_aggregation = AsynchronousSumAggregation(True) - self.assertIsNone(asynchronous_sum_aggregation.collect()) - - asynchronous_sum_aggregation.aggregate(Measurement(1)) - first_sum = asynchronous_sum_aggregation.collect() + sum_aggregation.aggregate(Measurement(1)) + first_sum = sum_aggregation.collect() self.assertEqual(first_sum.value, 1) self.assertTrue(first_sum.is_monotonic) - asynchronous_sum_aggregation.aggregate(Measurement(1)) - second_sum = asynchronous_sum_aggregation.collect() + sum_aggregation.aggregate(Measurement(1)) + second_sum = sum_aggregation.collect() - self.assertEqual(second_sum.value, 1) + self.assertEqual(second_sum.value, 2) self.assertTrue(second_sum.is_monotonic) self.assertEqual( second_sum.start_time_unix_nano, first_sum.start_time_unix_nano ) - -class TestLastValueAggregation(TestCase): - def test_instrument_monotonicity_awareness(self): - """ - `LastValueAggregation` is not aware of the instrument monotonicity - """ - - sum_aggregation = LastValueAggregation() - self.assertNotIsInstance( - sum_aggregation, _InstrumentMonotonicityAwareAggregation + self.assertIsNone( + SumAggregation(True, AggregationTemporality.CUMULATIVE).collect() ) + +class TestLastValueAggregation(TestCase): def test_aggregate(self): """ `LastValueAggregation` collects data for gauge metric points with delta @@ -200,6 +164,7 @@ def test_collect(self): last_value_aggregation.aggregate(Measurement(1)) first_gauge = last_value_aggregation.collect() + self.assertIsInstance(first_gauge, Gauge) self.assertEqual(first_gauge.value, 1) From a07e03935b5d54af19ec6959f9eb89a85b80fcb1 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 17 Feb 2022 12:36:21 -0600 Subject: [PATCH 1128/1517] Add _ViewInstrumentMatch (#2400) --- .../sdk/_metrics/_view_instrument_match.py | 110 +++++++++++++++++ .../metrics/test_view_instrument_match.py | 115 ++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py create mode 100644 opentelemetry-sdk/tests/metrics/test_view_instrument_match.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py new file mode 100644 index 0000000000..0fcb254cb5 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py @@ -0,0 +1,110 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from logging import getLogger +from threading import Lock +from typing import Iterable, Set + +from opentelemetry.sdk._metrics.aggregation import ( + _convert_aggregation_temporality, +) +from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo + +_logger = getLogger(__name__) + + +class _ViewInstrumentMatch: + def __init__( + self, + name: str, + unit: str, + description: str, + aggregation: type, + instrumentation_info: InstrumentationInfo, + resource: Resource, + attribute_keys: Set[str] = None, + ): + self._name = name + self._unit = unit + self._description = description + self._aggregation = aggregation + self._instrumentation_info = instrumentation_info + self._resource = resource + self._attribute_keys = attribute_keys + self._attributes_aggregation = {} + self._attributes_previous_point = {} + self._lock = Lock() + + def consume_measurement(self, measurement: Measurement) -> None: + + if self._attribute_keys is not None: + + attributes = {} + + for key, value in measurement.attributes.items(): + if key in self._attribute_keys: + attributes[key] = value + else: + attributes = measurement.attributes + + attributes = frozenset(attributes.items()) + + if attributes not in self._attributes_aggregation.keys(): + with self._lock: + self._attributes_aggregation[attributes] = self._aggregation() + + self._attributes_aggregation[attributes].aggregate(measurement.value) + + def collect(self, temporality: int) -> Iterable[Metric]: + + with self._lock: + for ( + attributes, + aggregation, + ) in self._attributes_aggregation.items(): + + previous_point = self._attributes_previous_point.get( + attributes + ) + + current_point = aggregation.collect() + + # pylint: disable=assignment-from-none + self._attributes_previous_point[ + attributes + ] = _convert_aggregation_temporality( + previous_point, + current_point, + AggregationTemporality.CUMULATIVE, + ) + + if current_point is not None: + + yield Metric( + attributes=dict(attributes), + description=self._description, + instrumentation_info=self._instrumentation_info, + name=self._name, + resource=self._resource, + unit=self._unit, + point=_convert_aggregation_temporality( + previous_point, + current_point, + temporality, + ), + ) diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py new file mode 100644 index 0000000000..769a0c3dd7 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -0,0 +1,115 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase +from unittest.mock import Mock + +from opentelemetry.sdk._metrics._view_instrument_match import ( + _ViewInstrumentMatch, +) +from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk._metrics.point import Metric + + +class Test_ViewInstrumentMatch(TestCase): + @classmethod + def setUpClass(cls): + + cls.mock_aggregation_instance = Mock() + cls.mock_aggregation_class = Mock( + return_value=cls.mock_aggregation_instance + ) + cls.mock_resource = Mock() + cls.mock_instrumentation_info = Mock() + + def test_consume_measurement(self): + + view_instrument_match = _ViewInstrumentMatch( + "name", + "unit", + "description", + self.mock_aggregation_class, + self.mock_instrumentation_info, + self.mock_resource, + {"a", "c"}, + ) + + view_instrument_match.consume_measurement( + Measurement(value=0, attributes={"c": "d", "f": "g"}) + ) + self.assertEqual( + view_instrument_match._attributes_aggregation, + {frozenset([("c", "d")]): self.mock_aggregation_instance}, + ) + + view_instrument_match.consume_measurement( + Measurement(value=0, attributes={"w": "x", "y": "z"}) + ) + + self.assertEqual( + view_instrument_match._attributes_aggregation, + { + frozenset(): self.mock_aggregation_instance, + frozenset([("c", "d")]): self.mock_aggregation_instance, + }, + ) + + view_instrument_match = _ViewInstrumentMatch( + "name", + "unit", + "description", + self.mock_aggregation_class, + self.mock_instrumentation_info, + self.mock_resource, + ) + + view_instrument_match.consume_measurement( + Measurement(value=0, attributes={"c": "d", "f": "g"}) + ) + self.assertEqual( + view_instrument_match._attributes_aggregation, + { + frozenset( + [("c", "d"), ("f", "g")] + ): self.mock_aggregation_instance + }, + ) + + def test_collect(self): + + view_instrument_match = _ViewInstrumentMatch( + "name", + "unit", + "description", + self.mock_aggregation_class, + self.mock_instrumentation_info, + self.mock_resource, + {"a", "c"}, + ) + + view_instrument_match.consume_measurement( + Measurement(value=0, attributes={"c": "d", "f": "g"}) + ) + self.assertEqual( + next(view_instrument_match.collect(1)), + Metric( + attributes={"c": "d"}, + description="description", + instrumentation_info=self.mock_instrumentation_info, + name="name", + resource=self.mock_resource, + unit="unit", + point=None, + ), + ) From c1174eac3b1d29be1357261084a19499f63db654 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 17 Feb 2022 11:11:43 -0800 Subject: [PATCH 1129/1517] Bump protobuf from 3.14.0 to 3.15.0 in /docs/examples/fork-process-model/flask-gunicorn (#2466) --- .../examples/fork-process-model/flask-gunicorn/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt index e36515841e..e91747d59e 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt +++ b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt @@ -12,7 +12,7 @@ opentelemetry-instrumentation==0.18b0 opentelemetry-instrumentation-flask==0.18b1 opentelemetry-instrumentation-wsgi==0.18b1 opentelemetry-sdk==0.18b0 -protobuf==3.14.0 +protobuf==3.15.0 six==1.15.0 thrift==0.13.0 uWSGI==2.0.19.1 From e117db19ac55542e36443cd9d5df7b4d405d549f Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 17 Feb 2022 15:25:10 -0800 Subject: [PATCH 1130/1517] [bugfix] aggregate expects a measurement (#2469) The previous code was passing in a value. --- .../src/opentelemetry/sdk/_metrics/_view_instrument_match.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py index 0fcb254cb5..e0697fc9de 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py @@ -68,7 +68,7 @@ def consume_measurement(self, measurement: Measurement) -> None: with self._lock: self._attributes_aggregation[attributes] = self._aggregation() - self._attributes_aggregation[attributes].aggregate(measurement.value) + self._attributes_aggregation[attributes].aggregate(measurement) def collect(self, temporality: int) -> Iterable[Metric]: From b516a946ccfe988a0e75707360bcadcd9230864b Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 18 Feb 2022 08:59:03 -0800 Subject: [PATCH 1131/1517] add check for measurement attributes (#2468) --- .../sdk/_metrics/_view_instrument_match.py | 4 +++- .../tests/metrics/test_view_instrument_match.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py index e0697fc9de..a9b1d50185 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py @@ -59,8 +59,10 @@ def consume_measurement(self, measurement: Measurement) -> None: for key, value in measurement.attributes.items(): if key in self._attribute_keys: attributes[key] = value - else: + elif measurement.attributes is not None: attributes = measurement.attributes + else: + attributes = {} attributes = frozenset(attributes.items()) diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 769a0c3dd7..38f9249c43 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -86,6 +86,22 @@ def test_consume_measurement(self): }, ) + view_instrument_match = _ViewInstrumentMatch( + "name", + "unit", + "description", + self.mock_aggregation_class, + self.mock_instrumentation_info, + self.mock_resource, + ) + view_instrument_match.consume_measurement( + Measurement(value=0, attributes=None) + ) + self.assertEqual( + view_instrument_match._attributes_aggregation, + {frozenset({}): self.mock_aggregation_instance}, + ) + def test_collect(self): view_instrument_match = _ViewInstrumentMatch( From 4ed4fd08db67de69369f87862e43562c2e43fed5 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Sat, 19 Feb 2022 04:13:55 +0530 Subject: [PATCH 1132/1517] pin markupsafe version to 2.0.1 (#2474) --- dev-requirements.txt | 4 ++++ docs-requirements.txt | 4 ++++ tox.ini | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 44e2080141..c60f36745f 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -13,3 +13,7 @@ readme-renderer~=24.0 grpcio-tools~=1.41.0 mypy-protobuf~=3.0.0 protobuf~=3.18.1 +# temporary fix. we should update the jinja, flask deps +# See https://github.com/pallets/markupsafe/issues/282 +# breaking change introduced in markupsafe causes jinja, flask to break +markupsafe==2.0.1 diff --git a/docs-requirements.txt b/docs-requirements.txt index a93eb8aa70..c70cefc8aa 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -19,3 +19,7 @@ flask~=1.0 opentracing~=2.2.0 thrift>=0.10.0 wrapt>=1.0.0,<2.0.0 +# temporary fix. we should update the jinja, flask deps +# See https://github.com/pallets/markupsafe/issues/282 +# breaking change introduced in markupsafe causes jinja, flask to break +markupsafe==2.0.1 diff --git a/tox.ini b/tox.ini index 4f7e100469..694d3d78a6 100644 --- a/tox.ini +++ b/tox.ini @@ -230,6 +230,10 @@ deps = # needed for example trace integration flask~=1.1 requests~=2.7 + # temporary fix. we should update the jinja, flask deps + # See https://github.com/pallets/markupsafe/issues/282 + # breaking change introduced in markupsafe causes jinja, flask to break + markupsafe==2.0.1 commands_pre = pip install -e {toxinidir}/opentelemetry-api \ From 672c4498d16f5a734257f8dc812b82b75d062c45 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 22 Feb 2022 09:59:44 -0800 Subject: [PATCH 1133/1517] implement to_json for Metric (#2475) --- .../src/opentelemetry/sdk/_metrics/point.py | 19 ++++- opentelemetry-sdk/tests/metrics/test_point.py | 74 +++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 opentelemetry-sdk/tests/metrics/test_point.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py index 1586beb4d7..8cc3a9bcd1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dataclasses import dataclass +import json +from dataclasses import asdict, dataclass from enum import IntEnum from typing import Sequence, Union @@ -74,4 +75,18 @@ class Metric: """Contains non-common fields for the given metric""" def to_json(self) -> str: - raise NotImplementedError() + return json.dumps( + { + "attributes": self.attributes if self.attributes else "", + "description": self.description if self.description else "", + "instrumentation_info": repr(self.instrumentation_info) + if self.instrumentation_info + else "", + "name": self.name, + "resource": repr(self.resource.attributes) + if self.resource + else "", + "unit": self.unit if self.unit else "", + "point": asdict(self.point) if self.point else "", + } + ) diff --git a/opentelemetry-sdk/tests/metrics/test_point.py b/opentelemetry-sdk/tests/metrics/test_point.py new file mode 100644 index 0000000000..ff741115fc --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_point.py @@ -0,0 +1,74 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +from opentelemetry.sdk._metrics.point import Gauge, Histogram, Metric, Sum +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo + + +def _create_metric(value): + return Metric( + attributes={"attr-key": "test-val"}, + description="test-description", + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), + name="test-name", + resource=Resource({"resource-key": "resource-val"}), + unit="test-unit", + point=value, + ) + + +class TestDatapointToJSON(TestCase): + def test_sum(self): + point = _create_metric( + Sum( + start_time_unix_nano=10, + time_unix_nano=20, + value=9, + aggregation_temporality=2, + is_monotonic=True, + ) + ) + self.assertEqual( + '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_info": "InstrumentationInfo(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"start_time_unix_nano": 10, "time_unix_nano": 20, "value": 9, "aggregation_temporality": 2, "is_monotonic": true}}', + point.to_json(), + ) + + def test_gauge(self): + point = _create_metric(Gauge(time_unix_nano=40, value=20)) + self.assertEqual( + '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_info": "InstrumentationInfo(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"time_unix_nano": 40, "value": 20}}', + point.to_json(), + ) + + def test_histogram(self): + point = _create_metric( + Histogram( + start_time_unix_nano=50, + time_unix_nano=60, + bucket_counts=[0, 0, 1, 0], + explicit_bounds=[0.1, 0.5, 0.9, 1], + aggregation_temporality=1, + sum=0.8, + ) + ) + self.maxDiff = None + self.assertEqual( + '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_info": "InstrumentationInfo(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"start_time_unix_nano": 50, "time_unix_nano": 60, "bucket_counts": [0, 0, 1, 0], "explicit_bounds": [0.1, 0.5, 0.9, 1], "sum": 0.8, "aggregation_temporality": 1}}', + point.to_json(), + ) From c9e40c35d7f1e01b4ff49b3ed5b3a0ec3a039a8d Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 23 Feb 2022 00:00:22 +0530 Subject: [PATCH 1134/1517] Add temporality conversion for histogram (#2445) * Add temporality conversion for histogram * Add tests and update histogram point * point->count * Fix lint Co-authored-by: Alex Boten --- .../opentelemetry/sdk/_metrics/aggregation.py | 43 +++++ .../tests/metrics/test_aggregation.py | 166 +++++++++++++++++- 2 files changed, 208 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index b2341d9063..9518434ee1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -193,6 +193,7 @@ def collect(self) -> Histogram: ) +# pylint: disable=too-many-return-statements,too-many-branches def _convert_aggregation_temporality( previous_point: Optional[_PointVarT], current_point: _PointVarT, @@ -267,4 +268,46 @@ def _convert_aggregation_temporality( is_monotonic=is_monotonic, ) + if current_point_type is Histogram: + if previous_point is None: + return replace( + current_point, aggregation_temporality=aggregation_temporality + ) + if previous_point.aggregation_temporality is not ( + AggregationTemporality.CUMULATIVE + ): + raise Exception( + "previous_point aggregation temporality must be CUMULATIVE" + ) + + if current_point.aggregation_temporality is aggregation_temporality: + return current_point + + if aggregation_temporality is AggregationTemporality.CUMULATIVE: + start_time_unix_nano = previous_point.start_time_unix_nano + sum_ = current_point.sum + previous_point.sum + bucket_counts = [ + curr_count + prev_count + for curr_count, prev_count in zip( + current_point.bucket_counts, previous_point.bucket_counts + ) + ] + else: + start_time_unix_nano = previous_point.time_unix_nano + sum_ = current_point.sum - previous_point.sum + bucket_counts = [ + curr_count - prev_count + for curr_count, prev_count in zip( + current_point.bucket_counts, previous_point.bucket_counts + ) + ] + + return Histogram( + start_time_unix_nano=start_time_unix_nano, + time_unix_nano=current_point.time_unix_nano, + bucket_counts=bucket_counts, + explicit_bounds=current_point.explicit_bounds, + sum=sum_, + aggregation_temporality=aggregation_temporality, + ) return None diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index f6d3ce3ca8..cf9c315ab3 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -26,7 +26,7 @@ _convert_aggregation_temporality, ) from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import Gauge, Sum +from opentelemetry.sdk._metrics.point import Gauge, Histogram, Sum class TestSynchronousSumAggregation(TestCase): @@ -576,3 +576,167 @@ def test_current_point_gauge(self): ), current_point, ) + + +class TestHistogramConvertAggregationTemporality(TestCase): + def test_previous_point_none(self): + + current_point = Histogram( + start_time_unix_nano=0, + time_unix_nano=1, + bucket_counts=[0, 2, 1, 2, 0], + explicit_bounds=[0, 5, 10, 25], + sum=70, + aggregation_temporality=AggregationTemporality.DELTA, + ) + + self.assertEqual( + _convert_aggregation_temporality( + None, current_point, AggregationTemporality.CUMULATIVE + ), + replace( + current_point, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + ), + ) + + def test_previous_point_non_cumulative(self): + + with self.assertRaises(Exception): + + _convert_aggregation_temporality( + Histogram( + start_time_unix_nano=0, + time_unix_nano=1, + bucket_counts=[0, 2, 1, 2, 0], + explicit_bounds=[0, 5, 10, 25], + sum=70, + aggregation_temporality=AggregationTemporality.DELTA, + ), + Histogram( + start_time_unix_nano=1, + time_unix_nano=2, + bucket_counts=[0, 1, 3, 0, 0], + explicit_bounds=[0, 5, 10, 25], + sum=35, + aggregation_temporality=AggregationTemporality.DELTA, + ), + AggregationTemporality.DELTA, + ), + + def test_same_aggregation_temporality_cumulative(self): + current_point = Histogram( + start_time_unix_nano=0, + time_unix_nano=2, + bucket_counts=[0, 3, 4, 2, 0], + explicit_bounds=[0, 5, 10, 25], + sum=105, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + _convert_aggregation_temporality( + Histogram( + start_time_unix_nano=0, + time_unix_nano=1, + bucket_counts=[0, 2, 1, 2, 0], + explicit_bounds=[0, 5, 10, 25], + sum=70, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + ), + current_point, + AggregationTemporality.CUMULATIVE, + ), + current_point, + ) + + def test_same_aggregation_temporality_delta(self): + current_point = Histogram( + start_time_unix_nano=1, + time_unix_nano=2, + bucket_counts=[0, 1, 3, 0, 0], + explicit_bounds=[0, 5, 10, 25], + sum=35, + aggregation_temporality=AggregationTemporality.DELTA, + ) + + self.assertEqual( + _convert_aggregation_temporality( + Histogram( + start_time_unix_nano=0, + time_unix_nano=2, + bucket_counts=[0, 3, 4, 2, 0], + explicit_bounds=[0, 5, 10, 25], + sum=105, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + ), + current_point, + AggregationTemporality.DELTA, + ), + current_point, + ) + + def test_aggregation_temporality_to_cumulative(self): + current_point = Histogram( + start_time_unix_nano=1, + time_unix_nano=2, + bucket_counts=[0, 1, 3, 0, 0], + explicit_bounds=[0, 5, 10, 25], + sum=35, + aggregation_temporality=AggregationTemporality.DELTA, + ) + + self.assertEqual( + _convert_aggregation_temporality( + Histogram( + start_time_unix_nano=0, + time_unix_nano=1, + bucket_counts=[0, 2, 1, 2, 0], + explicit_bounds=[0, 5, 10, 25], + sum=70, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + ), + current_point, + AggregationTemporality.CUMULATIVE, + ), + Histogram( + start_time_unix_nano=0, + time_unix_nano=2, + bucket_counts=[0, 3, 4, 2, 0], + explicit_bounds=[0, 5, 10, 25], + sum=105, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + ), + ) + + def test_aggregation_temporality_to_delta(self): + current_point = Histogram( + start_time_unix_nano=0, + time_unix_nano=2, + bucket_counts=[0, 3, 4, 2, 0], + explicit_bounds=[0, 5, 10, 25], + sum=105, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + ) + + self.assertEqual( + _convert_aggregation_temporality( + Histogram( + start_time_unix_nano=0, + time_unix_nano=1, + bucket_counts=[0, 2, 1, 2, 0], + explicit_bounds=[0, 5, 10, 25], + sum=70, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + ), + current_point, + AggregationTemporality.DELTA, + ), + Histogram( + start_time_unix_nano=1, + time_unix_nano=2, + bucket_counts=[0, 1, 3, 0, 0], + explicit_bounds=[0, 5, 10, 25], + sum=35, + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) From 50413bec2232e08595c7ce5603c70c5d03cd022c Mon Sep 17 00:00:00 2001 From: Abhishek Munagekar <10258799+munagekar@users.noreply.github.com> Date: Wed, 23 Feb 2022 04:26:18 +0900 Subject: [PATCH 1135/1517] fix except handling in get_aggregated_resources (#2464) * fix except handling in get_aggregated_resources Avoids UnboundLocalError: local variable 'detected_resource' referenced before assignment if raise_on_error is True * add changelog Co-authored-by: Srikanth Chekuri Co-authored-by: Leighton Chen --- CHANGELOG.md | 2 ++ opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb28471034..4031880bf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2450](https://github.com/open-telemetry/opentelemetry-python/pull/2450)) - Bump semantic conventions from 1.6.1 to 1.8.0 ([#2461](https://github.com/open-telemetry/opentelemetry-python/pull/2461)) +- fix exception handling in get_aggregated_resources + ([#2464](https://github.com/open-telemetry/opentelemetry-python/pull/2464)) ## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1-0.28b1) - 2022-01-29 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index c8790adf11..d32d943829 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -308,12 +308,12 @@ def get_aggregated_resources( detected_resource = future.result(timeout=timeout) # pylint: disable=broad-except except Exception as ex: + detected_resource = _EMPTY_RESOURCE if detector.raise_on_error: raise ex logger.warning( "Exception %s in detector %s, ignoring", ex, detector ) - detected_resource = _EMPTY_RESOURCE finally: detectors_merged_resource = detectors_merged_resource.merge( detected_resource From cd4c5e76a1fa4ecb9d295a1333978fd42de7ba48 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Tue, 22 Feb 2022 19:25:58 -0500 Subject: [PATCH 1136/1517] Implement MetricReaderStorage (#2456) * Implement MetricReaderStorage * Apply suggestions from code review Co-authored-by: Diego Hurtado * fix tset * syntax error * move async instrument callback invocation into the metric reader storage * Rename ViewStorage -> ViewInstrumentMatch Tests still need to be fixed. * Implement MetricReaderStorage * Apply suggestions from code review Co-authored-by: Diego Hurtado * fix tset * syntax error * move async instrument callback invocation into the metric reader storage * Rename ViewStorage -> ViewInstrumentMatch * fix lint * fix lint * Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py Co-authored-by: Diego Hurtado * refactor to have the measurement consumer handle async callbacks again * remove print * lint Co-authored-by: Diego Hurtado Co-authored-by: Alex Boten Co-authored-by: Alex Boten --- .../opentelemetry/sdk/_metrics/__init__.py | 2 +- .../opentelemetry/sdk/_metrics/instrument.py | 6 +- .../opentelemetry/sdk/_metrics/measurement.py | 2 + .../sdk/_metrics/measurement_consumer.py | 3 +- .../sdk/_metrics/metric_reader_storage.py | 101 ++++++++++++- .../sdk/_metrics/sdk_configuration.py | 4 +- .../src/opentelemetry/sdk/_metrics/view.py | 20 +++ .../tests/metrics/test_aggregation.py | 89 ++++++----- .../metrics/test_measurement_consumer.py | 29 +++- .../metrics/test_metric_reader_storage.py | 142 ++++++++++++++++++ .../metrics/test_view_instrument_match.py | 28 +++- 11 files changed, 362 insertions(+), 64 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py create mode 100644 opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index b3232b367e..b6837d1a38 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -159,7 +159,7 @@ def __init__( self._meter_lock = Lock() self._atexit_handler = None self._sdk_config = SdkConfiguration( - resource=resource, metric_readers=metric_readers + resource=resource, metric_readers=metric_readers, views=() ) self._measurement_consumer = SynchronousMeasurementConsumer( sdk_config=self._sdk_config diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index d9da9a067a..bf0c4d5f40 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -95,7 +95,7 @@ def add( ) return self._measurement_consumer.consume_measurement( - Measurement(amount, attributes) + Measurement(amount, self, attributes) ) @@ -104,7 +104,7 @@ def add( self, amount: Union[int, float], attributes: Dict[str, str] = None ): self._measurement_consumer.consume_measurement( - Measurement(amount, attributes) + Measurement(amount, self, attributes) ) @@ -127,7 +127,7 @@ def record( ) return self._measurement_consumer.consume_measurement( - Measurement(amount, attributes) + Measurement(amount, self, attributes) ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py index 18110b0743..6026787d82 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py @@ -15,10 +15,12 @@ from dataclasses import dataclass from typing import Union +from opentelemetry._metrics.instrument import Instrument from opentelemetry.util.types import Attributes @dataclass(frozen=True) class Measurement: value: Union[int, float] + instrument: Instrument attributes: Attributes = None diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py index b602185bc9..c4b6770276 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py @@ -70,7 +70,8 @@ def collect( self, metric_reader: MetricReader, temporality: AggregationTemporality ) -> Iterable[Metric]: with self._lock: + metric_reader_storage = self._reader_storages[metric_reader] for async_instrument in self._async_instruments: for measurement in async_instrument.callback(): - self.consume_measurement(measurement) + metric_reader_storage.consume_measurement(measurement) return self._reader_storages[metric_reader].collect(temporality) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py index ffb204f447..18991f307c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py @@ -12,23 +12,112 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Iterable +from threading import RLock +from typing import Dict, Iterable, List -from opentelemetry.sdk._metrics.aggregation import AggregationTemporality +from opentelemetry._metrics.instrument import Counter, Histogram, Instrument +from opentelemetry.sdk._metrics._view_instrument_match import ( + _ViewInstrumentMatch, +) +from opentelemetry.sdk._metrics.aggregation import ( + AggregationTemporality, + ExplicitBucketHistogramAggregation, + LastValueAggregation, + SumAggregation, +) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import Metric from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration +from opentelemetry.sdk._metrics.view import View -# TODO: #2378 class MetricReaderStorage: """The SDK's storage for a given reader""" def __init__(self, sdk_config: SdkConfiguration) -> None: - pass + self._lock = RLock() + self._sdk_config = sdk_config + self._view_instrument_match: Dict[ + Instrument, List[_ViewInstrumentMatch] + ] = {} + + def _get_or_init_view_instrument_match( + self, instrument: Instrument + ) -> List["_ViewInstrumentMatch"]: + # Optimistically get the relevant views for the given instrument. Once set for a given + # instrument, the mapping will never change + if instrument in self._view_instrument_match: + return self._view_instrument_match[instrument] + + with self._lock: + # double check if it was set before we held the lock + if instrument in self._view_instrument_match: + return self._view_instrument_match[instrument] + + # not present, hold the lock and add a new mapping + matches = [] + for view in self._sdk_config.views: + if view.match(instrument): + # Note: if a view matches multiple instruments, this will create a separate + # _ViewInstrumentMatch per instrument. If the user's View configuration includes a + # name, this will cause multiple conflicting output streams. + matches.append( + _ViewInstrumentMatch( + name=view.name or instrument.name, + resource=self._sdk_config.resource, + instrumentation_info=None, + aggregation=view.aggregation, + unit=instrument.unit, + description=view.description, + ) + ) + + # if no view targeted the instrument, use the default + if not matches: + # TODO: the logic to select aggregation could be moved + if isinstance(instrument, Counter): + agg = SumAggregation(True, AggregationTemporality.DELTA) + elif isinstance(instrument, Histogram): + agg = ExplicitBucketHistogramAggregation() + else: + agg = LastValueAggregation() + matches.append( + _ViewInstrumentMatch( + resource=self._sdk_config.resource, + instrumentation_info=None, + aggregation=agg, + unit=instrument.unit, + description=instrument.description, + name=instrument.name, + ) + ) + self._view_instrument_match[instrument] = matches + return matches def consume_measurement(self, measurement: Measurement) -> None: - pass + for matches in self._get_or_init_view_instrument_match( + measurement.instrument + ): + matches.consume_measurement(measurement) def collect(self, temporality: AggregationTemporality) -> Iterable[Metric]: - pass + # use a list instead of yielding to prevent a slow reader from holding SDK locks + metrics: List[Metric] = [] + + # While holding the lock, new _ViewInstrumentMatch can't be added from another thread (so we are + # sure we collect all existing view). However, instruments can still send measurements + # that will make it into the individual aggregations; collection will acquire those + # locks iteratively to keep locking as fine-grained as possible. One side effect is + # that end times can be slightly skewed among the metric streams produced by the SDK, + # but we still align the output timestamps for a single instrument. + with self._lock: + for matches in self._view_instrument_match.values(): + for match in matches: + metrics.extend(match.collect(temporality)) + + return metrics + + +def default_view(instrument: Instrument) -> View: + # TODO: #2247 + return View() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py index 3b077bdea1..2c603b5e4d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py @@ -2,12 +2,12 @@ from typing import Sequence from opentelemetry.sdk._metrics.metric_reader import MetricReader +from opentelemetry.sdk._metrics.view import View from opentelemetry.sdk.resources import Resource @dataclass class SdkConfiguration: resource: Resource - # TODO: once views are added - # views: Sequence[View] metric_readers: Sequence[MetricReader] + views: Sequence[View] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py new file mode 100644 index 0000000000..e103425c03 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py @@ -0,0 +1,20 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# TODO: #2247 +# pylint: disable=no-self-use +class View: + def match(self) -> bool: + return False diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index cf9c315ab3..cbeb9bf4ac 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -16,7 +16,9 @@ from logging import WARNING from math import inf from time import sleep +from typing import Union from unittest import TestCase +from unittest.mock import Mock from opentelemetry.sdk._metrics.aggregation import ( AggregationTemporality, @@ -27,6 +29,13 @@ ) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import Gauge, Histogram, Sum +from opentelemetry.util.types import Attributes + + +def measurement( + value: Union[int, float], attributes: Attributes = None +) -> Measurement: + return Measurement(value, instrument=Mock(), attributes=attributes) class TestSynchronousSumAggregation(TestCase): @@ -39,9 +48,9 @@ def test_aggregate_delta(self): True, AggregationTemporality.DELTA ) - synchronous_sum_aggregation.aggregate(Measurement(1)) - synchronous_sum_aggregation.aggregate(Measurement(2)) - synchronous_sum_aggregation.aggregate(Measurement(3)) + synchronous_sum_aggregation.aggregate(measurement(1)) + synchronous_sum_aggregation.aggregate(measurement(2)) + synchronous_sum_aggregation.aggregate(measurement(3)) self.assertEqual(synchronous_sum_aggregation._value, 6) @@ -49,9 +58,9 @@ def test_aggregate_delta(self): True, AggregationTemporality.DELTA ) - synchronous_sum_aggregation.aggregate(Measurement(1)) - synchronous_sum_aggregation.aggregate(Measurement(-2)) - synchronous_sum_aggregation.aggregate(Measurement(3)) + synchronous_sum_aggregation.aggregate(measurement(1)) + synchronous_sum_aggregation.aggregate(measurement(-2)) + synchronous_sum_aggregation.aggregate(measurement(3)) self.assertEqual(synchronous_sum_aggregation._value, 2) @@ -64,9 +73,9 @@ def test_aggregate_cumulative(self): True, AggregationTemporality.CUMULATIVE ) - synchronous_sum_aggregation.aggregate(Measurement(1)) - synchronous_sum_aggregation.aggregate(Measurement(2)) - synchronous_sum_aggregation.aggregate(Measurement(3)) + synchronous_sum_aggregation.aggregate(measurement(1)) + synchronous_sum_aggregation.aggregate(measurement(2)) + synchronous_sum_aggregation.aggregate(measurement(3)) self.assertEqual(synchronous_sum_aggregation._value, 6) @@ -74,9 +83,9 @@ def test_aggregate_cumulative(self): True, AggregationTemporality.CUMULATIVE ) - synchronous_sum_aggregation.aggregate(Measurement(1)) - synchronous_sum_aggregation.aggregate(Measurement(-2)) - synchronous_sum_aggregation.aggregate(Measurement(3)) + synchronous_sum_aggregation.aggregate(measurement(1)) + synchronous_sum_aggregation.aggregate(measurement(-2)) + synchronous_sum_aggregation.aggregate(measurement(3)) self.assertEqual(synchronous_sum_aggregation._value, 2) @@ -89,13 +98,13 @@ def test_collect_delta(self): True, AggregationTemporality.DELTA ) - synchronous_sum_aggregation.aggregate(Measurement(1)) + synchronous_sum_aggregation.aggregate(measurement(1)) first_sum = synchronous_sum_aggregation.collect() self.assertEqual(first_sum.value, 1) self.assertTrue(first_sum.is_monotonic) - synchronous_sum_aggregation.aggregate(Measurement(1)) + synchronous_sum_aggregation.aggregate(measurement(1)) second_sum = synchronous_sum_aggregation.collect() self.assertEqual(second_sum.value, 1) @@ -114,13 +123,13 @@ def test_collect_cumulative(self): True, AggregationTemporality.CUMULATIVE ) - sum_aggregation.aggregate(Measurement(1)) + sum_aggregation.aggregate(measurement(1)) first_sum = sum_aggregation.collect() self.assertEqual(first_sum.value, 1) self.assertTrue(first_sum.is_monotonic) - sum_aggregation.aggregate(Measurement(1)) + sum_aggregation.aggregate(measurement(1)) second_sum = sum_aggregation.collect() self.assertEqual(second_sum.value, 2) @@ -144,13 +153,13 @@ def test_aggregate(self): last_value_aggregation = LastValueAggregation() - last_value_aggregation.aggregate(Measurement(1)) + last_value_aggregation.aggregate(measurement(1)) self.assertEqual(last_value_aggregation._value, 1) - last_value_aggregation.aggregate(Measurement(2)) + last_value_aggregation.aggregate(measurement(2)) self.assertEqual(last_value_aggregation._value, 2) - last_value_aggregation.aggregate(Measurement(3)) + last_value_aggregation.aggregate(measurement(3)) self.assertEqual(last_value_aggregation._value, 3) def test_collect(self): @@ -162,13 +171,13 @@ def test_collect(self): self.assertIsNone(last_value_aggregation.collect()) - last_value_aggregation.aggregate(Measurement(1)) + last_value_aggregation.aggregate(measurement(1)) first_gauge = last_value_aggregation.collect() self.assertIsInstance(first_gauge, Gauge) self.assertEqual(first_gauge.value, 1) - last_value_aggregation.aggregate(Measurement(1)) + last_value_aggregation.aggregate(measurement(1)) # CI fails the last assertion without this sleep(0.1) @@ -192,13 +201,13 @@ def test_aggregate(self): ExplicitBucketHistogramAggregation(boundaries=[0, 2, 4]) ) - explicit_bucket_histogram_aggregation.aggregate(Measurement(-1)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(0)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(1)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(2)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(3)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(4)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(5)) + explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) + explicit_bucket_histogram_aggregation.aggregate(measurement(0)) + explicit_bucket_histogram_aggregation.aggregate(measurement(1)) + explicit_bucket_histogram_aggregation.aggregate(measurement(2)) + explicit_bucket_histogram_aggregation.aggregate(measurement(3)) + explicit_bucket_histogram_aggregation.aggregate(measurement(4)) + explicit_bucket_histogram_aggregation.aggregate(measurement(5)) # The first bucket keeps count of values between (-inf, 0] (-1 and 0) self.assertEqual( @@ -233,11 +242,11 @@ def test_min_max(self): ExplicitBucketHistogramAggregation() ) - explicit_bucket_histogram_aggregation.aggregate(Measurement(-1)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(2)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(7)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(8)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(9999)) + explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) + explicit_bucket_histogram_aggregation.aggregate(measurement(2)) + explicit_bucket_histogram_aggregation.aggregate(measurement(7)) + explicit_bucket_histogram_aggregation.aggregate(measurement(8)) + explicit_bucket_histogram_aggregation.aggregate(measurement(9999)) self.assertEqual(explicit_bucket_histogram_aggregation._min, -1) self.assertEqual(explicit_bucket_histogram_aggregation._max, 9999) @@ -246,11 +255,11 @@ def test_min_max(self): ExplicitBucketHistogramAggregation(record_min_max=False) ) - explicit_bucket_histogram_aggregation.aggregate(Measurement(-1)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(2)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(7)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(8)) - explicit_bucket_histogram_aggregation.aggregate(Measurement(9999)) + explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) + explicit_bucket_histogram_aggregation.aggregate(measurement(2)) + explicit_bucket_histogram_aggregation.aggregate(measurement(7)) + explicit_bucket_histogram_aggregation.aggregate(measurement(8)) + explicit_bucket_histogram_aggregation.aggregate(measurement(9999)) self.assertEqual(explicit_bucket_histogram_aggregation._min, inf) self.assertEqual(explicit_bucket_histogram_aggregation._max, -inf) @@ -264,7 +273,7 @@ def test_collect(self): ExplicitBucketHistogramAggregation(boundaries=[0, 1, 2]) ) - explicit_bucket_histogram_aggregation.aggregate(Measurement(1)) + explicit_bucket_histogram_aggregation.aggregate(measurement(1)) first_histogram = explicit_bucket_histogram_aggregation.collect() self.assertEqual(first_histogram.bucket_counts, (0, 1, 0, 0)) @@ -272,7 +281,7 @@ def test_collect(self): # CI fails the last assertion without this sleep(0.1) - explicit_bucket_histogram_aggregation.aggregate(Measurement(1)) + explicit_bucket_histogram_aggregation.aggregate(measurement(1)) second_histogram = explicit_bucket_histogram_aggregation.collect() self.assertEqual(second_histogram.bucket_counts, (0, 1, 0, 0)) diff --git a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py index 0b7072f9e4..aa481482ec 100644 --- a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py +++ b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py @@ -35,7 +35,9 @@ def test_creates_metric_reader_storages(self, MockMetricReaderStorage): """It should create one MetricReaderStorage per metric reader passed in the SdkConfiguration""" reader_mocks = [Mock() for _ in range(5)] SynchronousMeasurementConsumer( - SdkConfiguration(resource=Mock(), metric_readers=reader_mocks) + SdkConfiguration( + resource=Mock(), metric_readers=reader_mocks, views=() + ) ) self.assertEqual(len(MockMetricReaderStorage.mock_calls), 5) @@ -47,7 +49,9 @@ def test_measurements_passed_to_each_reader_storage( MockMetricReaderStorage.side_effect = reader_storage_mocks consumer = SynchronousMeasurementConsumer( - SdkConfiguration(resource=Mock(), metric_readers=reader_mocks) + SdkConfiguration( + resource=Mock(), metric_readers=reader_mocks, views=() + ) ) measurement_mock = Mock() consumer.consume_measurement(measurement_mock) @@ -64,7 +68,9 @@ def test_collect_passed_to_reader_stage(self, MockMetricReaderStorage): MockMetricReaderStorage.side_effect = reader_storage_mocks consumer = SynchronousMeasurementConsumer( - SdkConfiguration(resource=Mock(), metric_readers=reader_mocks) + SdkConfiguration( + resource=Mock(), metric_readers=reader_mocks, views=() + ) ) for r_mock, rs_mock in zip(reader_mocks, reader_storage_mocks): rs_mock.collect.assert_not_called() @@ -73,14 +79,20 @@ def test_collect_passed_to_reader_stage(self, MockMetricReaderStorage): AggregationTemporality.CUMULATIVE ) - def test_collect_calls_async_instruments(self, _): - """Its collect() method should invoke async instruments""" + def test_collect_calls_async_instruments(self, MockMetricReaderStorage): + """Its collect() method should invoke async instruments and pass measurements to the + corresponding metric reader storage""" reader_mock = Mock() + reader_storage_mock = Mock() + MockMetricReaderStorage.return_value = reader_storage_mock consumer = SynchronousMeasurementConsumer( - SdkConfiguration(resource=Mock(), metric_readers=[reader_mock]) + SdkConfiguration( + resource=Mock(), metric_readers=[reader_mock], views=() + ) ) async_instrument_mocks = [MagicMock() for _ in range(5)] for i_mock in async_instrument_mocks: + i_mock.callback.return_value = [Mock()] consumer.register_asynchronous_instrument(i_mock) consumer.collect(reader_mock, AggregationTemporality.CUMULATIVE) @@ -88,3 +100,8 @@ def test_collect_calls_async_instruments(self, _): # it should call async instruments for i_mock in async_instrument_mocks: i_mock.callback.assert_called_once() + + # it should pass measurements to reader storage + self.assertEqual( + len(reader_storage_mock.consume_measurement.mock_calls), 5 + ) diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py new file mode 100644 index 0000000000..a5b44b2333 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -0,0 +1,142 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest.mock import Mock, patch + +from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk._metrics.metric_reader_storage import ( + MetricReaderStorage, +) +from opentelemetry.sdk._metrics.point import AggregationTemporality +from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration +from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc + + +def mock_view_matching(*instruments) -> Mock: + mock = Mock() + mock.match.side_effect = lambda instrument: instrument in instruments + return mock + + +def mock_instrument() -> Mock: + instr = Mock() + instr.attributes = {} + return instr + + +@patch("opentelemetry.sdk._metrics.metric_reader_storage._ViewInstrumentMatch") +class TestMetricReaderStorage(ConcurrencyTestBase): + def test_creates_view_instrument_matches( + self, MockViewInstrumentMatch: Mock + ): + """It should create a MockViewInstrumentMatch when an instrument matches a view""" + instrument1 = Mock(name="instrument1") + instrument2 = Mock(name="instrument2") + + view1 = mock_view_matching(instrument1) + view2 = mock_view_matching(instrument1, instrument2) + storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), metric_readers=(), views=(view1, view2) + ) + ) + + # instrument1 matches view1 and view2, so should create two ViewInstrumentMatch objects + storage.consume_measurement(Measurement(1, instrument1)) + self.assertEqual( + len(MockViewInstrumentMatch.call_args_list), + 2, + MockViewInstrumentMatch.mock_calls, + ) + # they should only be created the first time the instrument is seen + storage.consume_measurement(Measurement(1, instrument1)) + self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 2) + + # instrument2 matches view2, so should create a single ViewInstrumentMatch + MockViewInstrumentMatch.call_args_list.clear() + storage.consume_measurement(Measurement(1, instrument2)) + self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 1) + + def test_forwards_calls_to_view_instrument_match( + self, MockViewInstrumentMatch: Mock + ): + view_instrument_match1 = Mock() + view_instrument_match2 = Mock() + view_instrument_match3 = Mock() + MockViewInstrumentMatch.side_effect = [ + view_instrument_match1, + view_instrument_match2, + view_instrument_match3, + ] + + instrument1 = Mock(name="instrument1") + instrument2 = Mock(name="instrument2") + view1 = mock_view_matching(instrument1) + view2 = mock_view_matching(instrument1, instrument2) + storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), metric_readers=(), views=(view1, view2) + ) + ) + + # Measurements from an instrument should be passed on to each ViewInstrumentMatch objects + # created for that instrument + measurement = Measurement(1, instrument1) + storage.consume_measurement(measurement) + view_instrument_match1.consume_measurement.assert_called_once_with( + measurement + ) + view_instrument_match2.consume_measurement.assert_called_once_with( + measurement + ) + view_instrument_match3.consume_measurement.assert_not_called() + + measurement = Measurement(1, instrument2) + storage.consume_measurement(measurement) + view_instrument_match3.consume_measurement.assert_called_once_with( + measurement + ) + + # collect() should call collect on all of its _ViewInstrumentMatch objects and combine them together + all_metrics = [Mock() for _ in range(6)] + view_instrument_match1.collect.return_value = all_metrics[:2] + view_instrument_match2.collect.return_value = all_metrics[2:4] + view_instrument_match3.collect.return_value = all_metrics[4:] + + result = storage.collect(AggregationTemporality.CUMULATIVE) + view_instrument_match1.collect.assert_called_once() + view_instrument_match2.collect.assert_called_once() + view_instrument_match3.collect.assert_called_once() + self.assertEqual(result, all_metrics) + + def test_race_concurrent_measurements(self, MockViewInstrumentMatch: Mock): + mock_view_instrument_match_ctor = MockFunc() + MockViewInstrumentMatch.side_effect = mock_view_instrument_match_ctor + + instrument1 = Mock(name="instrument1") + view1 = mock_view_matching(instrument1) + storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), metric_readers=(), views=(view1,) + ) + ) + + def send_measurement(): + storage.consume_measurement(Measurement(1, instrument1)) + + # race sending many measurements concurrently + self.run_with_many_threads(send_measurement) + + # _ViewInstrumentMatch constructor should have only been called once + self.assertEqual(mock_view_instrument_match_ctor.call_count, 1) diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 38f9249c43..c074729403 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -45,8 +45,14 @@ def test_consume_measurement(self): {"a", "c"}, ) + instrument1 = Mock(name="instrument1") + view_instrument_match.consume_measurement( - Measurement(value=0, attributes={"c": "d", "f": "g"}) + Measurement( + value=0, + instrument=instrument1, + attributes={"c": "d", "f": "g"}, + ) ) self.assertEqual( view_instrument_match._attributes_aggregation, @@ -54,7 +60,11 @@ def test_consume_measurement(self): ) view_instrument_match.consume_measurement( - Measurement(value=0, attributes={"w": "x", "y": "z"}) + Measurement( + value=0, + instrument=instrument1, + attributes={"w": "x", "y": "z"}, + ) ) self.assertEqual( @@ -75,7 +85,11 @@ def test_consume_measurement(self): ) view_instrument_match.consume_measurement( - Measurement(value=0, attributes={"c": "d", "f": "g"}) + Measurement( + value=0, + instrument=instrument1, + attributes={"c": "d", "f": "g"}, + ) ) self.assertEqual( view_instrument_match._attributes_aggregation, @@ -95,7 +109,7 @@ def test_consume_measurement(self): self.mock_resource, ) view_instrument_match.consume_measurement( - Measurement(value=0, attributes=None) + Measurement(value=0, instrument=instrument1, attributes=None) ) self.assertEqual( view_instrument_match._attributes_aggregation, @@ -115,7 +129,11 @@ def test_collect(self): ) view_instrument_match.consume_measurement( - Measurement(value=0, attributes={"c": "d", "f": "g"}) + Measurement( + value=0, + instrument=Mock(name="instrument1"), + attributes={"c": "d", "f": "g"}, + ) ) self.assertEqual( next(view_instrument_match.collect(1)), From 89514cd8d5c110175ae6fde50741bf6651d6f004 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 24 Feb 2022 15:35:34 -0800 Subject: [PATCH 1137/1517] add factories for aggregations (#2482) * add factories for aggregations Fixes #2481 * fix lint * make classes/methods private * updates from feedback * remove Factory suffix * Apply suggestions from code review Co-authored-by: Diego Hurtado Co-authored-by: Diego Hurtado --- .../opentelemetry/sdk/_metrics/aggregation.py | 67 +++++++++++++- .../sdk/_metrics/metric_reader_storage.py | 12 +-- .../tests/metrics/test_aggregation.py | 89 ++++++++++++++++--- 3 files changed, 144 insertions(+), 24 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 9518434ee1..9258cc8d7a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -20,6 +20,12 @@ from threading import Lock from typing import Generic, List, Optional, Sequence, TypeVar +from opentelemetry._metrics.instrument import ( + Asynchronous, + Instrument, + Synchronous, + _Monotonic, +) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import ( AggregationTemporality, @@ -35,7 +41,7 @@ _logger = getLogger(__name__) -class Aggregation(ABC, Generic[_PointVarT]): +class _Aggregation(ABC, Generic[_PointVarT]): def __init__(self): self._lock = Lock() @@ -48,7 +54,7 @@ def collect(self) -> Optional[_PointVarT]: pass -class SumAggregation(Aggregation[Sum]): +class _SumAggregation(_Aggregation[Sum]): def __init__( self, instrument_is_monotonic: bool, @@ -107,7 +113,7 @@ def collect(self) -> Optional[Sum]: ) -class LastValueAggregation(Aggregation[Gauge]): +class _LastValueAggregation(_Aggregation[Gauge]): def __init__(self): super().__init__() self._value = None @@ -129,7 +135,7 @@ def collect(self) -> Optional[Gauge]: ) -class ExplicitBucketHistogramAggregation(Aggregation[Histogram]): +class _ExplicitBucketHistogramAggregation(_Aggregation[Histogram]): def __init__( self, boundaries: Sequence[float] = ( @@ -311,3 +317,56 @@ def _convert_aggregation_temporality( aggregation_temporality=aggregation_temporality, ) return None + + +class _AggregationFactory(ABC): + @abstractmethod + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + """Creates an aggregation""" + + +class ExplicitBucketHistogramAggregation(_AggregationFactory): + def __init__( + self, + boundaries: Sequence[float] = ( + 0.0, + 5.0, + 10.0, + 25.0, + 50.0, + 75.0, + 100.0, + 250.0, + 500.0, + 1000.0, + ), + record_min_max: bool = True, + ) -> None: + self._boundaries = boundaries + self._record_min_max = record_min_max + + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + return _ExplicitBucketHistogramAggregation( + boundaries=self._boundaries, + record_min_max=self._record_min_max, + ) + + +class SumAggregation(_AggregationFactory): + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + + temporality = AggregationTemporality.UNSPECIFIED + if isinstance(instrument, Synchronous): + temporality = AggregationTemporality.DELTA + elif isinstance(instrument, Asynchronous): + temporality = AggregationTemporality.CUMULATIVE + + return _SumAggregation( + isinstance(instrument, _Monotonic), + temporality, + ) + + +class LastValueAggregation(_AggregationFactory): + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + return _LastValueAggregation() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py index 18991f307c..01724742f2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py @@ -21,9 +21,9 @@ ) from opentelemetry.sdk._metrics.aggregation import ( AggregationTemporality, - ExplicitBucketHistogramAggregation, - LastValueAggregation, - SumAggregation, + _ExplicitBucketHistogramAggregation, + _LastValueAggregation, + _SumAggregation, ) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import Metric @@ -76,11 +76,11 @@ def _get_or_init_view_instrument_match( if not matches: # TODO: the logic to select aggregation could be moved if isinstance(instrument, Counter): - agg = SumAggregation(True, AggregationTemporality.DELTA) + agg = _SumAggregation(True, AggregationTemporality.DELTA) elif isinstance(instrument, Histogram): - agg = ExplicitBucketHistogramAggregation() + agg = _ExplicitBucketHistogramAggregation() else: - agg = LastValueAggregation() + agg = _LastValueAggregation() matches.append( _ViewInstrumentMatch( resource=self._sdk_config.resource, diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index cbeb9bf4ac..8263e2e6bc 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -20,12 +20,16 @@ from unittest import TestCase from unittest.mock import Mock +from opentelemetry.sdk._metrics import instrument from opentelemetry.sdk._metrics.aggregation import ( AggregationTemporality, ExplicitBucketHistogramAggregation, LastValueAggregation, SumAggregation, _convert_aggregation_temporality, + _ExplicitBucketHistogramAggregation, + _LastValueAggregation, + _SumAggregation, ) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import Gauge, Histogram, Sum @@ -44,7 +48,7 @@ def test_aggregate_delta(self): `SynchronousSumAggregation` aggregates data for sum metric points """ - synchronous_sum_aggregation = SumAggregation( + synchronous_sum_aggregation = _SumAggregation( True, AggregationTemporality.DELTA ) @@ -54,7 +58,7 @@ def test_aggregate_delta(self): self.assertEqual(synchronous_sum_aggregation._value, 6) - synchronous_sum_aggregation = SumAggregation( + synchronous_sum_aggregation = _SumAggregation( True, AggregationTemporality.DELTA ) @@ -69,7 +73,7 @@ def test_aggregate_cumulative(self): `SynchronousSumAggregation` aggregates data for sum metric points """ - synchronous_sum_aggregation = SumAggregation( + synchronous_sum_aggregation = _SumAggregation( True, AggregationTemporality.CUMULATIVE ) @@ -79,7 +83,7 @@ def test_aggregate_cumulative(self): self.assertEqual(synchronous_sum_aggregation._value, 6) - synchronous_sum_aggregation = SumAggregation( + synchronous_sum_aggregation = _SumAggregation( True, AggregationTemporality.CUMULATIVE ) @@ -94,7 +98,7 @@ def test_collect_delta(self): `SynchronousSumAggregation` collects sum metric points """ - synchronous_sum_aggregation = SumAggregation( + synchronous_sum_aggregation = _SumAggregation( True, AggregationTemporality.DELTA ) @@ -119,7 +123,7 @@ def test_collect_cumulative(self): `SynchronousSumAggregation` collects sum metric points """ - sum_aggregation = SumAggregation( + sum_aggregation = _SumAggregation( True, AggregationTemporality.CUMULATIVE ) @@ -140,7 +144,7 @@ def test_collect_cumulative(self): ) self.assertIsNone( - SumAggregation(True, AggregationTemporality.CUMULATIVE).collect() + _SumAggregation(True, AggregationTemporality.CUMULATIVE).collect() ) @@ -151,7 +155,7 @@ def test_aggregate(self): temporality """ - last_value_aggregation = LastValueAggregation() + last_value_aggregation = _LastValueAggregation() last_value_aggregation.aggregate(measurement(1)) self.assertEqual(last_value_aggregation._value, 1) @@ -167,7 +171,7 @@ def test_collect(self): `LastValueAggregation` collects sum metric points """ - last_value_aggregation = LastValueAggregation() + last_value_aggregation = _LastValueAggregation() self.assertIsNone(last_value_aggregation.collect()) @@ -198,7 +202,7 @@ def test_aggregate(self): """ explicit_bucket_histogram_aggregation = ( - ExplicitBucketHistogramAggregation(boundaries=[0, 2, 4]) + _ExplicitBucketHistogramAggregation(boundaries=[0, 2, 4]) ) explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) @@ -239,7 +243,7 @@ def test_min_max(self): """ explicit_bucket_histogram_aggregation = ( - ExplicitBucketHistogramAggregation() + _ExplicitBucketHistogramAggregation() ) explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) @@ -252,7 +256,7 @@ def test_min_max(self): self.assertEqual(explicit_bucket_histogram_aggregation._max, 9999) explicit_bucket_histogram_aggregation = ( - ExplicitBucketHistogramAggregation(record_min_max=False) + _ExplicitBucketHistogramAggregation(record_min_max=False) ) explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) @@ -266,11 +270,11 @@ def test_min_max(self): def test_collect(self): """ - `ExplicitBucketHistogramAggregation` collects sum metric points + `_ExplicitBucketHistogramAggregation` collects sum metric points """ explicit_bucket_histogram_aggregation = ( - ExplicitBucketHistogramAggregation(boundaries=[0, 1, 2]) + _ExplicitBucketHistogramAggregation(boundaries=[0, 1, 2]) ) explicit_bucket_histogram_aggregation.aggregate(measurement(1)) @@ -749,3 +753,60 @@ def test_aggregation_temporality_to_delta(self): aggregation_temporality=AggregationTemporality.DELTA, ), ) + + +class TestAggregationFactory(TestCase): + def test_sum_factory(self): + counter = instrument.Counter("name", Mock(), Mock()) + factory = SumAggregation() + aggregation = factory._create_aggregation(counter) + self.assertIsInstance(aggregation, _SumAggregation) + self.assertTrue(aggregation._instrument_is_monotonic) + self.assertEqual( + aggregation._instrument_temporality, AggregationTemporality.DELTA + ) + aggregation2 = factory._create_aggregation(counter) + self.assertNotEqual(aggregation, aggregation2) + + counter = instrument.UpDownCounter("name", Mock(), Mock()) + factory = SumAggregation() + aggregation = factory._create_aggregation(counter) + self.assertIsInstance(aggregation, _SumAggregation) + self.assertFalse(aggregation._instrument_is_monotonic) + self.assertEqual( + aggregation._instrument_temporality, AggregationTemporality.DELTA + ) + + counter = instrument.ObservableCounter("name", Mock(), Mock(), None) + factory = SumAggregation() + aggregation = factory._create_aggregation(counter) + self.assertIsInstance(aggregation, _SumAggregation) + self.assertTrue(aggregation._instrument_is_monotonic) + self.assertEqual( + aggregation._instrument_temporality, + AggregationTemporality.CUMULATIVE, + ) + + def test_explicit_bucket_histogram_factory(self): + histo = instrument.Histogram("name", Mock(), Mock()) + factory = ExplicitBucketHistogramAggregation( + boundaries=( + 0.0, + 5.0, + ), + record_min_max=False, + ) + aggregation = factory._create_aggregation(histo) + self.assertIsInstance(aggregation, _ExplicitBucketHistogramAggregation) + self.assertFalse(aggregation._record_min_max) + self.assertEqual(aggregation._boundaries, (0.0, 5.0)) + aggregation2 = factory._create_aggregation(histo) + self.assertNotEqual(aggregation, aggregation2) + + def test_last_value_factory(self): + counter = instrument.Counter("name", Mock(), Mock()) + factory = LastValueAggregation() + aggregation = factory._create_aggregation(counter) + self.assertIsInstance(aggregation, _LastValueAggregation) + aggregation2 = factory._create_aggregation(counter) + self.assertNotEqual(aggregation, aggregation2) From 19489bc1b9d16b58715f930bc2bde3dc81fb8ff9 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 25 Feb 2022 11:09:57 -0800 Subject: [PATCH 1138/1517] move metrics util function to test package (#2484) * move metrics util function to test package This will make the code available for other exporters that will need to test different metric types. * fix lint --- .../metrics/test_otlp_metrics_exporter.py | 54 ++--------- .../src/opentelemetry/test/metrictestutil.py | 93 +++++++++++++++++++ 2 files changed, 99 insertions(+), 48 deletions(-) create mode 100644 tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index 9c00ee4340..b6d0d5b9b0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections import OrderedDict from concurrent.futures import ThreadPoolExecutor from unittest import TestCase from unittest.mock import patch @@ -21,7 +20,6 @@ from google.rpc.error_details_pb2 import RetryInfo from grpc import StatusCode, server -from opentelemetry.attributes import BoundedAttributes from opentelemetry.exporter.otlp.proto.grpc._metric_exporter import ( OTLPMetricExporter, ) @@ -43,18 +41,15 @@ Resource as OTLPResource, ) from opentelemetry.sdk._metrics.export import MetricExportResult -from opentelemetry.sdk._metrics.point import ( - AggregationTemporality, - Gauge, - Histogram, - Metric, - Sum, -) +from opentelemetry.sdk._metrics.point import AggregationTemporality, Histogram from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_INSECURE, ) -from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.test.metrictestutil import ( + _generate_gauge, + _generate_metric, + _generate_sum, +) class MetricsServiceServicerUNAVAILABLEDelay(MetricsServiceServicer): @@ -103,43 +98,6 @@ def Export(self, request, context): return ExportMetricsServiceResponse() -def _generate_metric(name, point) -> Metric: - return Metric( - resource=SDKResource(OrderedDict([("a", 1), ("b", False)])), - instrumentation_info=InstrumentationInfo( - "first_name", "first_version" - ), - attributes=BoundedAttributes(attributes={"a": 1, "b": True}), - description="foo", - name=name, - unit="s", - point=point, - ) - - -def _generate_sum(name, val) -> Sum: - return _generate_metric( - name, - Sum( - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, - start_time_unix_nano=1641946015139533244, - time_unix_nano=1641946016139533244, - value=val, - ), - ) - - -def _generate_gauge(name, val) -> Gauge: - return _generate_metric( - name, - Gauge( - time_unix_nano=1641946016139533244, - value=val, - ), - ) - - class TestOTLPMetricExporter(TestCase): def setUp(self): diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py new file mode 100644 index 0000000000..4bf45f357d --- /dev/null +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py @@ -0,0 +1,93 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from collections import OrderedDict + +from opentelemetry.attributes import BoundedAttributes +from opentelemetry.sdk._metrics.point import ( + AggregationTemporality, + Gauge, + Metric, + Sum, +) +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo + + +def _generate_metric( + name, point, attributes=None, description=None, unit=None +) -> Metric: + if not attributes: + attributes = BoundedAttributes(attributes={"a": 1, "b": True}) + if not description: + description = "foo" + if not unit: + unit = "s" + return Metric( + resource=SDKResource(OrderedDict([("a", 1), ("b", False)])), + instrumentation_info=InstrumentationInfo( + "first_name", "first_version" + ), + attributes=attributes, + description=description, + name=name, + unit=unit, + point=point, + ) + + +def _generate_sum( + name, val, attributes=None, description=None, unit=None +) -> Sum: + return _generate_metric( + name, + Sum( + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + value=val, + ), + attributes=attributes, + description=description, + unit=unit, + ) + + +def _generate_gauge( + name, val, attributes=None, description=None, unit=None +) -> Gauge: + return _generate_metric( + name, + Gauge( + time_unix_nano=1641946016139533244, + value=val, + ), + attributes=attributes, + description=description, + unit=unit, + ) + + +def _generate_unsupported_metric( + name, attributes=None, description=None, unit=None +) -> Sum: + return _generate_metric( + name, + None, + attributes=attributes, + description=description, + unit=unit, + ) From 78d7497c9e174d23dc638e5d2a1e2068bcea433c Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 28 Feb 2022 21:34:47 +0530 Subject: [PATCH 1139/1517] Re-enable pypy test (#2480) --- opentelemetry-sdk/tests/logs/test_export.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 3e6eed3851..95c132769f 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -44,7 +44,6 @@ from opentelemetry.trace import TraceFlags from opentelemetry.trace.span import INVALID_SPAN_CONTEXT -is_pypy = hasattr(sys, "pypy_version_info") supports_register_at_fork = hasattr(os, "fork") and sys.version_info >= (3, 7) @@ -275,9 +274,8 @@ def bulk_log_and_flush(num_logs): finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 2415) - # TODO: fix https://github.com/open-telemetry/opentelemetry-python/issues/2346 @unittest.skipIf( - is_pypy or not supports_register_at_fork, + not supports_register_at_fork, "needs *nix and minor version 7 or later", ) def test_batch_log_processor_fork(self): From baaec6657a7554929a0d6b7b8d654cdb41e3e2f1 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 28 Feb 2022 12:47:19 -0600 Subject: [PATCH 1140/1517] Update protobuf (#2492) --- docs/examples/fork-process-model/flask-uwsgi/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt index e36515841e..e91747d59e 100644 --- a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt +++ b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt @@ -12,7 +12,7 @@ opentelemetry-instrumentation==0.18b0 opentelemetry-instrumentation-flask==0.18b1 opentelemetry-instrumentation-wsgi==0.18b1 opentelemetry-sdk==0.18b0 -protobuf==3.14.0 +protobuf==3.15.0 six==1.15.0 thrift==0.13.0 uWSGI==2.0.19.1 From 91ce1ffae1dce2e54def9c7964102d9cd495bcde Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 28 Feb 2022 13:12:34 -0600 Subject: [PATCH 1141/1517] Fix documentation (#2490) --- opentelemetry-api/src/opentelemetry/trace/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 56927bf664..34ac532e30 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -202,8 +202,11 @@ def get_tracer( vs. a functional tracer). Args: - instrumenting_module_name: The name of the instrumenting module - (usually just ``__name__``). + instrumenting_module_name: The name of the instrumenting module. + ``__name__`` may not be used as this can result in + different tracer names if the tracers are in different files. + It is better to use a fixed string that can be imported where + needed and used consistently as the name of the tracer. This should *not* be the name of the module that is instrumented but the name of the module doing the instrumentation. From 6f8e9d3d96c89655972ace66b40e551045549962 Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Mon, 28 Feb 2022 14:59:15 -0500 Subject: [PATCH 1142/1517] [docs] Rework API docs (#2453) --- CHANGELOG.md | 5 + docs/api/{api.rst => index.rst} | 0 docs/examples/index.rst | 10 ++ docs/exporter/index.rst | 10 ++ docs/faq-and-cookbook.rst | 136 ----------------- docs/getting-started.rst | 253 -------------------------------- docs/index.rst | 113 +++----------- docs/performance/benchmarks.rst | 4 - docs/sdk/{sdk.rst => index.rst} | 0 docs/shim/index.rst | 10 ++ 10 files changed, 55 insertions(+), 486 deletions(-) rename docs/api/{api.rst => index.rst} (100%) create mode 100644 docs/examples/index.rst create mode 100644 docs/exporter/index.rst delete mode 100644 docs/faq-and-cookbook.rst delete mode 100644 docs/getting-started.rst delete mode 100644 docs/performance/benchmarks.rst rename docs/sdk/{sdk.rst => index.rst} (100%) create mode 100644 docs/shim/index.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 4031880bf7..8e0c8f6f2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.9.1-0.28b1...HEAD) +- Docs rework: [non-API docs are + moving](https://github.com/open-telemetry/opentelemetry-python/issues/2172) to + [opentelemetry.io](https://opentelemetry.io). For details, including a list of + pages that have moved, see + [#2453](https://github.com/open-telemetry/opentelemetry-python/pull/2453). - `opentelemetry-exporter-otlp-grpc` update SDK dependency to ~1.9. ([#2442](https://github.com/open-telemetry/opentelemetry-python/pull/2442)) - bugfix(auto-instrumentation): attach OTLPHandler to root logger diff --git a/docs/api/api.rst b/docs/api/index.rst similarity index 100% rename from docs/api/api.rst rename to docs/api/index.rst diff --git a/docs/examples/index.rst b/docs/examples/index.rst new file mode 100644 index 0000000000..92fc679b70 --- /dev/null +++ b/docs/examples/index.rst @@ -0,0 +1,10 @@ +:orphan: + +Examples +======== + +.. toctree:: + :maxdepth: 1 + :glob: + + ** diff --git a/docs/exporter/index.rst b/docs/exporter/index.rst new file mode 100644 index 0000000000..9316ba0e6d --- /dev/null +++ b/docs/exporter/index.rst @@ -0,0 +1,10 @@ +:orphan: + +Exporters +========= + +.. toctree:: + :maxdepth: 1 + :glob: + + ** diff --git a/docs/faq-and-cookbook.rst b/docs/faq-and-cookbook.rst deleted file mode 100644 index 2aacb3eeb4..0000000000 --- a/docs/faq-and-cookbook.rst +++ /dev/null @@ -1,136 +0,0 @@ -Frequently Asked Questions and Cookbook -======================================= - -This page answers frequently asked questions, and serves as a cookbook -for common scenarios. - -Create a new span ------------------ - -.. code-block:: python - - from opentelemetry import trace - - tracer = trace.get_tracer(__name__) - with tracer.start_as_current_span("print") as span: - print("foo") - span.set_attribute("printed_string", "foo") - -Getting and modifying a span ----------------------------- - -.. code-block:: python - - from opentelemetry import trace - - current_span = trace.get_current_span() - current_span.set_attribute("hometown", "seattle") - -Create a nested span --------------------- - -.. code-block:: python - - from opentelemetry import trace - import time - - tracer = trace.get_tracer(__name__) - - # Create a new span to track some work - with tracer.start_as_current_span("parent"): - time.sleep(1) - - # Create a nested span to track nested work - with tracer.start_as_current_span("child"): - time.sleep(2) - # the nested span is closed when it's out of scope - - # Now the parent span is the current span again - time.sleep(1) - - # This span is also closed when it goes out of scope - - -Capturing baggage at different contexts ---------------------------------------- - -.. code-block:: python - - from opentelemetry import trace - - tracer = trace.get_tracer(__name__) - with tracer.start_as_current_span(name="root span") as root_span: - parent_ctx = baggage.set_baggage("context", "parent") - with tracer.start_as_current_span( - name="child span", context=parent_ctx - ) as child_span: - child_ctx = baggage.set_baggage("context", "child") - - print(baggage.get_baggage("context", parent_ctx)) - print(baggage.get_baggage("context", child_ctx)) - -Manually setting span context ------------------------------ - -.. code-block:: python - - from opentelemetry import trace - from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import ConsoleSpanExporter, BatchSpanProcessor - from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator - - trace.set_tracer_provider(TracerProvider()) - trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) - - tracer = trace.get_tracer(__name__) - - # Extracting from carrier header - carrier = {'traceparent': '00-a9c3b99a95cc045e573e163c3ac80a77-d99d251a8caecd06-01'} - ctx = TraceContextTextMapPropagator().extract(carrier=carrier) - - with tracer.start_as_current_span('child', context=ctx) as span: - span.set_attribute('primes', [2, 3, 5, 7]) - - # Or if you have a SpanContext object already. - span_context = SpanContext( - trace_id=2604504634922341076776623263868986797, - span_id=5213367945872657620, - is_remote=True, - trace_flags=TraceFlags(0x01) - ) - ctx = trace.set_span_in_context(NonRecordingSpan(span_context)) - - with tracer.start_as_current_span("child", context=ctx) as span: - span.set_attribute('evens', [2, 4, 6, 8]) - -Using multiple tracer providers with different Resource -------------------------------------------------------- - -.. code-block:: python - - from opentelemetry import trace - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.resources import Resource - from opentelemetry.sdk.trace.export import ConsoleSpanExporter, BatchSpanProcessor - - # Global tracer provider which can be set only once - trace.set_tracer_provider( - TracerProvider(resource=Resource.create({"service.name": "service1"})) - ) - trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) - - tracer = trace.get_tracer(__name__) - with tracer.start_as_current_span("some-name") as span: - span.set_attribute("key", "value") - - - - another_tracer_provider = TracerProvider( - resource=Resource.create({"service.name": "service2"}) - ) - another_tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) - - another_tracer = trace.get_tracer(__name__, tracer_provider=another_tracer_provider) - with another_tracer.start_as_current_span("name-here") as span: - span.set_attribute("another-key", "another-value") diff --git a/docs/getting-started.rst b/docs/getting-started.rst deleted file mode 100644 index c4db11c24a..0000000000 --- a/docs/getting-started.rst +++ /dev/null @@ -1,253 +0,0 @@ -Getting Started -=============== - -This guide walks you through instrumenting a Python application with ``opentelemetry-python``. - -For more elaborate examples, see `examples `_. - -Hello world: emit a trace to your console ---------------------------------------------- - -To get started, install both the opentelemetry API and SDK: - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - -The API package provides the interfaces required by the application owner, as well -as some helper logic to load implementations. - -The SDK provides an implementation of those interfaces. The implementation is designed to be generic and extensible enough -that in many situations, the SDK is sufficient. - -Once installed, you can use the packages to emit spans from your application. A span -represents an action within your application that you want to instrument, such as an HTTP request -or a database call. Once instrumented, you can extract helpful information such as -how long the action took. You can also add arbitrary attributes to the span that provide more insight for debugging. - -The following example script emits a trace containing three named spans: "foo", "bar", and "baz": - -.. literalinclude:: getting_started/tracing_example.py - :language: python - :lines: 15- - -When you run the script you can see the traces printed to your console: - -.. code-block:: sh - - $ python tracing_example.py - { - "name": "baz", - "context": { - "trace_id": "0xb51058883c02f880111c959f3aa786a2", - "span_id": "0xb2fa4c39f5f35e13", - "trace_state": "{}" - }, - "kind": "SpanKind.INTERNAL", - "parent_id": "0x77e577e6a8813bf4", - "start_time": "2020-05-07T14:39:52.906272Z", - "end_time": "2020-05-07T14:39:52.906343Z", - "status": { - "status_code": "OK" - }, - "attributes": {}, - "events": [], - "links": [] - } - { - "name": "bar", - "context": { - "trace_id": "0xb51058883c02f880111c959f3aa786a2", - "span_id": "0x77e577e6a8813bf4", - "trace_state": "{}" - }, - "kind": "SpanKind.INTERNAL", - "parent_id": "0x3791d950cc5140c5", - "start_time": "2020-05-07T14:39:52.906230Z", - "end_time": "2020-05-07T14:39:52.906601Z", - "status": { - "status_code": "OK" - }, - "attributes": {}, - "events": [], - "links": [] - } - { - "name": "foo", - "context": { - "trace_id": "0xb51058883c02f880111c959f3aa786a2", - "span_id": "0x3791d950cc5140c5", - "trace_state": "{}" - }, - "kind": "SpanKind.INTERNAL", - "parent_id": null, - "start_time": "2020-05-07T14:39:52.906157Z", - "end_time": "2020-05-07T14:39:52.906743Z", - "status": { - "status_code": "OK" - }, - "attributes": {}, - "events": [], - "links": [] - } - -Each span typically represents a single operation or unit of work. -Spans can be nested, and have a parent-child relationship with other spans. -While a given span is active, newly-created spans inherit the active span's trace ID, options, and other attributes of its context. -A span without a parent is called the root span, and a trace is comprised of one root span and its descendants. - -In this example, the OpenTelemetry Python library creates one trace containing three spans and prints it to STDOUT. - -Configure exporters to emit spans elsewhere -------------------------------------------- - -The previous example does emit information about all spans, but the output is a bit hard to read. -In most cases, you can instead *export* this data to an application performance monitoring backend to be visualized and queried. -It's also common to aggregate span and trace information from multiple services into a single database, so that actions requiring multiple services can still all be visualized together. - -This concept of aggregating span and trace information is known as distributed tracing. One such distributed tracing backend is known as Jaeger. The Jaeger project provides an all-in-one Docker container with a UI, database, and consumer. - -Run the following command to start Jaeger: - -.. code-block:: sh - - docker run -p 16686:16686 -p 6831:6831/udp jaegertracing/all-in-one - -This command starts Jaeger locally on port 16686 and exposes the Jaeger thrift agent on port 6831. You can visit Jaeger at http://localhost:16686. - -After you spin up the backend, your application needs to export traces to this system. Although ``opentelemetry-sdk`` doesn't provide an exporter -for Jaeger, you can install it as a separate package with the following command: - -.. code-block:: sh - - pip install opentelemetry-exporter-jaeger - -After you install the exporter, update your code to import the Jaeger exporter and use that instead: - -.. literalinclude:: getting_started/jaeger_example.py - :language: python - :lines: 15- - -Finally, run the Python script: - -.. code-block:: python - - python jaeger_example.py - -You can then visit the Jaeger UI, see your service under "services", and find your traces! - -Instrumentation example with Flask ------------------------------------- - -While the example in the previous section is great, it's very manual. The following are common actions you might want to track and include as part of your distributed tracing. - -* HTTP responses from web services -* HTTP requests from clients -* Database calls - -To track these common actions, OpenTelemetry has the concept of instrumentations. Instrumentations are packages designed to interface -with a specific framework or library, such as Flask and psycopg2. You can find a list of the currently curated extension packages in the `Contrib repository `_. - -Instrument a basic Flask application that uses the requests library to send HTTP requests. First, install the instrumentation packages themselves: - -.. code-block:: sh - - pip install opentelemetry-instrumentation-flask - pip install opentelemetry-instrumentation-requests - - -The following small Flask application sends an HTTP request and also activates each instrumentation during its initialization: - -.. literalinclude:: getting_started/flask_example.py - :language: python - :lines: 15- - - -Now run the script, hit the root URL (http://localhost:5000/) a few times, and watch your spans be emitted! - -.. code-block:: sh - - python flask_example.py - - -Configure Your HTTP propagator (b3, Baggage) -------------------------------------------------------- - -A major feature of distributed tracing is the ability to correlate a trace across -multiple services. However, those services need to propagate information about a -trace from one service to the other. - -To enable this propagation, OpenTelemetry has the concept of `propagators `_, -which provide a common method to encode and decode span information from a request and response, respectively. - -By default, ``opentelemetry-python`` is configured to use the `W3C Trace Context `_ -and `W3C Baggage `_ HTTP headers for HTTP requests, but you can configure it to leverage different propagators. Here's -an example using Zipkin's `b3 propagation `_: - -.. code-block:: sh - - pip install opentelemetry-propagator-b3 - -Following the installation of the package containing the b3 propagator, configure the propagator as follows: - -.. code-block:: python - - from opentelemetry.propagate import set_global_textmap - from opentelemetry.propagators.b3 import B3Format - - set_global_textmap(B3Format()) - -Use the OpenTelemetry Collector for traces ------------------------------------------- - -Although it's possible to directly export your telemetry data to specific backends, you might have more complex use cases such as the following: - -* A single telemetry sink shared by multiple services, to reduce overhead of switching exporters. -* Aggregating traces across multiple services, running on multiple hosts. - -To enable a broad range of aggregation strategies, OpenTelemetry provides the `opentelemetry-collector `_. -The Collector is a flexible application that can consume trace data and export to multiple other backends, including to another instance of the Collector. - -Start the Collector locally to see how the Collector works in practice. Write the following file: - -.. code-block:: yaml - - # /tmp/otel-collector-config.yaml - receivers: - otlp: - protocols: - grpc: - http: - exporters: - logging: - loglevel: debug - processors: - batch: - service: - pipelines: - traces: - receivers: [otlp] - exporters: [logging] - processors: [batch] - -Then start the Docker container: - -.. code-block:: sh - - docker run -p 4317:4317 \ - -v /tmp/otel-collector-config.yaml:/etc/otel-collector-config.yaml \ - otel/opentelemetry-collector:latest \ - --config=/etc/otel-collector-config.yaml - -Install the OpenTelemetry Collector exporter: - -.. code-block:: sh - - pip install opentelemetry-exporter-otlp - -Finally, execute the following script: - -.. literalinclude:: getting_started/otlpcollector_example.py - :language: python - :lines: 15- diff --git a/docs/index.rst b/docs/index.rst index 4344bab2ba..a66cc4f1ec 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,117 +1,44 @@ -OpenTelemetry-Python -==================== - -The Python `OpenTelemetry `_ client. +OpenTelemetry-Python API Reference +================================== .. image:: https://img.shields.io/badge/slack-chat-green.svg :target: https://cloud-native.slack.com/archives/C01PD4HUVBL :alt: Slack Chat +Welcome to the docs for the `Python OpenTelemetry implementation +`_. -This documentation describes the :doc:`opentelemetry-api `, -:doc:`opentelemetry-sdk `, and several `integration packages <#integrations>`_. - -The library is currently stable for tracing. Support for :doc:`metrics ` and -:doc:`logging ` are currently under development and are considered experimental. - -Requirement ------------ -OpenTelemetry-Python supports Python 3.6 and higher. - -Installation ------------- - -The API and SDK packages are available on PyPI, and can installed via pip: - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - -In addition, there are several extension packages which can be installed separately as:: - - pip install opentelemetry-exporter-{exporter} - pip install opentelemetry-instrumentation-{instrumentation} +For an introduction to OpenTelemetry, see the `OpenTelemetry website docs +`_. -These are for exporter and instrumentation packages respectively. -The Jaeger, Zipkin, OTLP and OpenCensus Exporters can be found in the :scm_web:`exporter ` -directory of the repository. Instrumentations and additional exporters can be found in the -`Contrib repo instrumentation `_ -and `Contrib repo exporter `_ directories. +To learn how to instrument your Python code, see `Getting Started +`_. For +project status, information about releases, installation instructions and more, +see `Python `_. -Extensions ----------- - -Visit `OpenTelemetry Registry `_ to find -related projects like exporters, instrumentation libraries, tracer implementations, etc. - -Installing Cutting Edge Packages -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -While the project is pre-1.0, there may be significant functionality that -has not yet been released to PyPI. In that situation, you may want to -install the packages directly from the repo. This can be done by cloning the -repository and doing an `editable -install `_: - -.. code-block:: sh - - git clone https://github.com/open-telemetry/opentelemetry-python.git - cd opentelemetry-python - pip install -e ./opentelemetry-api - pip install -e ./opentelemetry-sdk +Getting Started +--------------- - -.. toctree:: - :maxdepth: 1 - :caption: Getting Started - :name: getting-started - - getting-started - faq-and-cookbook +* `Getting Started `_ +* `Frequently Asked Questions and Cookbook `_ .. toctree:: :maxdepth: 1 :caption: Core Packages :name: packages - api/api - sdk/sdk - -.. toctree:: - :maxdepth: 2 - :caption: Exporters - :name: exporters - :glob: - - exporter/** + api/index + sdk/index .. toctree:: :maxdepth: 2 - :caption: Shims - :name: Shims - :glob: - - shim/** - -.. toctree:: - :maxdepth: 1 - :caption: Performance - :name: performance-tests - :glob: - - performance/** - -.. toctree:: - :maxdepth: 1 - :caption: Examples - :name: examples + :caption: More :glob: - examples/** + exporter/index + shim/index + examples/index -Indices and tables ------------------- * :ref:`genindex` * :ref:`modindex` diff --git a/docs/performance/benchmarks.rst b/docs/performance/benchmarks.rst deleted file mode 100644 index 9c406c0323..0000000000 --- a/docs/performance/benchmarks.rst +++ /dev/null @@ -1,4 +0,0 @@ -Performance Tests - Benchmarks -============================== - -Click `here `_ to view the latest performance benchmarks for packages in this repo. diff --git a/docs/sdk/sdk.rst b/docs/sdk/index.rst similarity index 100% rename from docs/sdk/sdk.rst rename to docs/sdk/index.rst diff --git a/docs/shim/index.rst b/docs/shim/index.rst new file mode 100644 index 0000000000..5fad3b3663 --- /dev/null +++ b/docs/shim/index.rst @@ -0,0 +1,10 @@ +:orphan: + +Shims +===== + +.. toctree:: + :maxdepth: 1 + :glob: + + ** From 6bb3eceb93ce302c958874f03d1c9a9daabe28d2 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 2 Mar 2022 03:32:58 +0530 Subject: [PATCH 1143/1517] Bump mypy to 0.931 (#2494) --- dev-requirements.txt | 2 +- .../src/opentelemetry/baggage/propagation/__init__.py | 4 ++-- opentelemetry-api/src/opentelemetry/trace/__init__.py | 6 +++--- .../opentelemetry/trace/propagation/tracecontext.py | 10 +++++----- opentelemetry-api/src/opentelemetry/trace/span.py | 8 +++++--- opentelemetry-api/src/opentelemetry/util/re.py | 7 ++++--- tox.ini | 6 +++--- 7 files changed, 23 insertions(+), 20 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index c60f36745f..e02e1d9eec 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,7 +3,7 @@ flake8~=3.7 isort~=5.8 black~=22.1.0 httpretty~=1.0 -mypy==0.812 +mypy==0.931 sphinx~=3.5.4 sphinx-rtd-theme~=0.5 sphinx-autodoc-typehints~=1.12.0 diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index 8430dc2301..d194acfe2e 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -14,7 +14,7 @@ # from logging import getLogger from re import split -from typing import Iterable, Mapping, Optional, Set +from typing import Iterable, List, Mapping, Optional, Set from urllib.parse import quote_plus, unquote_plus from opentelemetry.baggage import _is_valid_pair, get_all, set_baggage @@ -63,7 +63,7 @@ def extract( ) return context - baggage_entries = split(_DELIMITER_PATTERN, header) + baggage_entries: List[str] = split(_DELIMITER_PATTERN, header) total_baggage_entries = self._MAX_PAIRS if len(baggage_entries) > self._MAX_PAIRS: diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 34ac532e30..cc5152ce83 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -326,7 +326,7 @@ def start_span( The newly-created span. """ - @contextmanager # type: ignore + @contextmanager @abstractmethod def start_as_current_span( self, @@ -449,7 +449,7 @@ def start_span( # pylint: disable=unused-argument,no-self-use return INVALID_SPAN - @contextmanager # type: ignore + @contextmanager def start_as_current_span( self, name: str, @@ -535,7 +535,7 @@ def get_tracer_provider() -> TracerProvider: return cast("TracerProvider", _TRACER_PROVIDER) -@contextmanager # type: ignore +@contextmanager def use_span( span: Span, end_on_exit: bool = False, diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index 14b68876b3..441a4c7eab 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -55,16 +55,16 @@ def extract( if not match: return context - version = match.group(1) - trace_id = match.group(2) - span_id = match.group(3) - trace_flags = match.group(4) + version: str = match.group(1) + trace_id: str = match.group(2) + span_id: str = match.group(3) + trace_flags: str = match.group(4) if trace_id == "0" * 32 or span_id == "0" * 16: return context if version == "00": - if match.group(5): + if match.group(5): # type: ignore return context if version == "ff": return context diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 228e505281..8846ff50a5 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -352,9 +352,10 @@ def from_header(cls, header_list: typing.List[str]) -> "TraceState": If the number of keys is beyond the maximum, all values will be discarded and an empty tracestate will be returned. """ - pairs = OrderedDict() + pairs = OrderedDict() # type: OrderedDict[str, str] for header in header_list: - for member in re.split(_delimiter_pattern, header): + members: typing.List[str] = re.split(_delimiter_pattern, header) + for member in members: # empty members are valid, but no need to process further. if not member: continue @@ -365,7 +366,8 @@ def from_header(cls, header_list: typing.List[str]) -> "TraceState": member, ) return cls() - key, _eq, value = match.groups() + groups: typing.Tuple[str, ...] = match.groups() + key, _eq, value = groups # duplicate keys are not legal in header if key in pairs: return cls() diff --git a/opentelemetry-api/src/opentelemetry/util/re.py b/opentelemetry-api/src/opentelemetry/util/re.py index c107ee9150..3dcbfe08b7 100644 --- a/opentelemetry-api/src/opentelemetry/util/re.py +++ b/opentelemetry-api/src/opentelemetry/util/re.py @@ -14,7 +14,7 @@ import logging from re import compile, split -from typing import Mapping +from typing import Dict, List, Mapping from urllib.parse import unquote _logger = logging.getLogger(__name__) @@ -42,8 +42,9 @@ def parse_headers(s: str) -> Mapping[str, str]: HTTP header format https://www.w3.org/TR/baggage/#baggage-http-header-format, except that additional semi-colon delimited metadata is not supported. """ - headers = {} - for header in split(_DELIMITER_PATTERN, s): + headers: Dict[str, str] = {} + headers_list: List[str] = split(_DELIMITER_PATTERN, s) + for header in headers_list: if not header: # empty string continue match = _HEADER_PATTERN.fullmatch(header.strip()) diff --git a/tox.ini b/tox.ini index 694d3d78a6..44c4367588 100644 --- a/tox.ini +++ b/tox.ini @@ -165,13 +165,13 @@ commands = opentelemetry: pytest {posargs} coverage: {toxinidir}/scripts/coverage.sh - mypy: mypy --namespace-packages --explicit-package-bases opentelemetry-api/src/opentelemetry/ + mypy: mypy --install-types --non-interactive --namespace-packages --explicit-package-bases opentelemetry-api/src/opentelemetry/ ; For test code, we don't want to enforce the full mypy strictness - mypy: mypy --namespace-packages --config-file=mypy-relaxed.ini opentelemetry-api/tests/ + mypy: mypy --install-types --non-interactive --namespace-packages --config-file=mypy-relaxed.ini opentelemetry-api/tests/ ; Test that mypy can pick up typeinfo from an installed package (otherwise, ; implicit Any due to unfollowed import would result). - mypyinstalled: mypy --namespace-packages opentelemetry-api/tests/mypysmoke.py --strict + mypyinstalled: mypy --install-types --non-interactive --namespace-packages opentelemetry-api/tests/mypysmoke.py --strict [testenv:lint] basepython: python3.9 From 50093f220f945ae38e769ab539c78c975e582bef Mon Sep 17 00:00:00 2001 From: Tyler Helmuth <12352919+TylerHelmuth@users.noreply.github.com> Date: Wed, 2 Mar 2022 15:06:28 -0700 Subject: [PATCH 1144/1517] Fix OTLP HTTP Endpoint Usage (#2493) --- CHANGELOG.md | 2 ++ .../proto/http/trace_exporter/__init__.py | 13 +++++-- .../tests/test_proto_span_exporter.py | 34 +++++++++++++++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0c8f6f2f..d5d58c8293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2461](https://github.com/open-telemetry/opentelemetry-python/pull/2461)) - fix exception handling in get_aggregated_resources ([#2464](https://github.com/open-telemetry/opentelemetry-python/pull/2464)) +- Fix `OTEL_EXPORTER_OTLP_ENDPOINT` usage in OTLP HTTP trace exporter + ([#2493](https://github.com/open-telemetry/opentelemetry-python/pull/2493)) ## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1-0.28b1) - 2022-01-29 diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index b320119aee..156afc247d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -47,7 +47,8 @@ DEFAULT_COMPRESSION = Compression.NoCompression -DEFAULT_ENDPOINT = "http://localhost:4318/v1/traces" +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_TRACES_EXPORT_PATH = "v1/traces" DEFAULT_TIMEOUT = 10 # in seconds @@ -65,7 +66,9 @@ def __init__( ): self._endpoint = endpoint or environ.get( OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, - environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT), + _append_trace_path( + environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) + ), ) self._certificate_file = certificate_file or environ.get( OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, @@ -172,3 +175,9 @@ def _compression_from_env() -> Compression: .strip() ) return Compression(compression) + + +def _append_trace_path(endpoint: str) -> str: + if endpoint.endswith("/"): + return endpoint + DEFAULT_TRACES_EXPORT_PATH + return endpoint + f"/{DEFAULT_TRACES_EXPORT_PATH}" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index 7f91e05984..e3cd204626 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -20,6 +20,7 @@ DEFAULT_COMPRESSION, DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, + DEFAULT_TRACES_EXPORT_PATH, OTLPSpanExporter, ) from opentelemetry.sdk.environment_variables import ( @@ -47,7 +48,9 @@ def test_constructor_default(self): exporter = OTLPSpanExporter() - self.assertEqual(exporter._endpoint, DEFAULT_ENDPOINT) + self.assertEqual( + exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH + ) self.assertEqual(exporter._certificate_file, True) self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) self.assertIs(exporter._compression, DEFAULT_COMPRESSION) @@ -90,6 +93,7 @@ def test_exporter_traces_env_take_priority(self): OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://traces.endpoint.env", OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, }, @@ -117,7 +121,6 @@ def test_exporter_constructor_take_priority(self): { OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, - OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, }, @@ -126,7 +129,6 @@ def test_exporter_env(self): exporter = OTLPSpanExporter() - self.assertEqual(exporter._endpoint, OS_ENV_ENDPOINT) self.assertEqual(exporter._certificate_file, OS_ENV_CERTIFICATE) self.assertEqual(exporter._timeout, int(OS_ENV_TIMEOUT)) self.assertIs(exporter._compression, Compression.Gzip) @@ -134,6 +136,32 @@ def test_exporter_env(self): exporter._headers, {"envheader1": "val1", "envheader2": "val2"} ) + @patch.dict( + "os.environ", + {OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT}, + ) + def test_exporter_env_endpoint_without_slash(self): + + exporter = OTLPSpanExporter() + + self.assertEqual( + exporter._endpoint, + OS_ENV_ENDPOINT + f"/{DEFAULT_TRACES_EXPORT_PATH}", + ) + + @patch.dict( + "os.environ", + {OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT + "/"}, + ) + def test_exporter_env_endpoint_with_slash(self): + + exporter = OTLPSpanExporter() + + self.assertEqual( + exporter._endpoint, + OS_ENV_ENDPOINT + f"/{DEFAULT_TRACES_EXPORT_PATH}", + ) + @patch.dict( "os.environ", { From 24bd23d916a728e0f7a6119800fc99e12409db8b Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 3 Mar 2022 12:30:44 -0600 Subject: [PATCH 1145/1517] Adds Views to metrics SDK (#2391) * Adds Views to metrics SDK * Add view documentation * Add type for instrument * Add more restrictions to matching criteria * Remove regex matching from meter name * Remove name from matching criteria * Add some fixes * Rename attribute * Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py Co-authored-by: Aaron Abbott * Update attribute keys type * More fixes * Remove regex and default view mechanism * Raise exceptions instead of logging warnings * Refactor warning * Fix issues * Add support for fnmatch and fix view matching * Fix test case * Add some fixes * WIP * WIP * Add final decorator * Fix lint * Use f string * Fix documentation * Fix documentation * Remove check * Remove unnecessary attribute * Fix documentation * Fix documentation * Fix tests * More fixes * Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py Co-authored-by: Alex Boten * Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py Co-authored-by: Alex Boten * More fixes * WIP fix tests * WIP * More fixing * Fix issue with 3.6 * Add comment for private view attributes * Fix view matching * Handle view aggregations in metric reader storage * Fix docs * Remove wildcard support for meter attributes * Fix documentation * Rename to _default_aggregation * Use default view argument * Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py Co-authored-by: Aaron Abbott Co-authored-by: Aaron Abbott Co-authored-by: Alex Boten Co-authored-by: Alex Boten --- .../opentelemetry/sdk/_metrics/__init__.py | 8 +- .../sdk/_metrics/_view_instrument_match.py | 9 +- .../opentelemetry/sdk/_metrics/instrument.py | 59 ++++++- .../sdk/_metrics/metric_reader_storage.py | 83 +++++----- .../sdk/_metrics/sdk_configuration.py | 1 + .../src/opentelemetry/sdk/_metrics/view.py | 152 +++++++++++++++++- .../metrics/test_measurement_consumer.py | 20 ++- .../metrics/test_metric_reader_storage.py | 134 +++++++++++++-- opentelemetry-sdk/tests/metrics/test_view.py | 112 +++++++++++++ .../metrics/test_view_instrument_match.py | 13 +- 10 files changed, 515 insertions(+), 76 deletions(-) create mode 100644 opentelemetry-sdk/tests/metrics/test_view.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index b6837d1a38..869f85d62e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -46,6 +46,7 @@ ) from opentelemetry.sdk._metrics.metric_reader import MetricReader from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration +from opentelemetry.sdk._metrics.view import View from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.util._once import Once @@ -154,12 +155,17 @@ def __init__( metric_readers: Sequence[MetricReader] = (), resource: Resource = Resource.create({}), shutdown_on_exit: bool = True, + views: Sequence[View] = (), + enable_default_view: bool = True, ): self._lock = Lock() self._meter_lock = Lock() self._atexit_handler = None self._sdk_config = SdkConfiguration( - resource=resource, metric_readers=metric_readers, views=() + resource=resource, + metric_readers=metric_readers, + views=views, + enable_default_view=enable_default_view, ) self._measurement_consumer = SynchronousMeasurementConsumer( sdk_config=self._sdk_config diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py index a9b1d50185..fbdea22a5b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py @@ -18,6 +18,7 @@ from typing import Iterable, Set from opentelemetry.sdk._metrics.aggregation import ( + _Aggregation, _convert_aggregation_temporality, ) from opentelemetry.sdk._metrics.measurement import Measurement @@ -34,10 +35,10 @@ def __init__( name: str, unit: str, description: str, - aggregation: type, + aggregation: _Aggregation, instrumentation_info: InstrumentationInfo, resource: Resource, - attribute_keys: Set[str] = None, + attribute_keys: Set[str], ): self._name = name self._unit = unit @@ -52,7 +53,7 @@ def __init__( def consume_measurement(self, measurement: Measurement) -> None: - if self._attribute_keys is not None: + if self._attribute_keys: attributes = {} @@ -68,7 +69,7 @@ def consume_measurement(self, measurement: Measurement) -> None: if attributes not in self._attributes_aggregation.keys(): with self._lock: - self._attributes_aggregation[attributes] = self._aggregation() + self._attributes_aggregation[attributes] = self._aggregation self._attributes_aggregation[attributes].aggregate(measurement) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index bf0c4d5f40..e3d0aa6331 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -15,6 +15,7 @@ # pylint: disable=too-many-ancestors import logging +from abc import ABC, abstractmethod from typing import Dict, Generator, Iterable, Union from opentelemetry._metrics.instrument import CallbackT @@ -30,14 +31,28 @@ ObservableUpDownCounter as APIObservableUpDownCounter, ) from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter +from opentelemetry.sdk._metrics.aggregation import ( + _Aggregation, + _ExplicitBucketHistogramAggregation, + _LastValueAggregation, + _SumAggregation, +) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.measurement_consumer import MeasurementConsumer +from opentelemetry.sdk._metrics.point import AggregationTemporality from opentelemetry.sdk.util.instrumentation import InstrumentationInfo _logger = logging.getLogger(__name__) -class _Synchronous: +class _Instrument(ABC): + @property + @abstractmethod + def _default_aggregation(self) -> _Aggregation: + pass + + +class _Synchronous(_Instrument): def __init__( self, name: str, @@ -49,12 +64,12 @@ def __init__( self.name = name self.unit = unit self.description = description - self._instrumentation_info = instrumentation_info + self.instrumentation_info = instrumentation_info self._measurement_consumer = measurement_consumer super().__init__(name, unit=unit, description=description) -class _Asynchronous: +class _Asynchronous(_Instrument): def __init__( self, name: str, @@ -67,7 +82,7 @@ def __init__( self.name = name self.unit = unit self.description = description - self._instrumentation_info = instrumentation_info + self.instrumentation_info = instrumentation_info self._measurement_consumer = measurement_consumer super().__init__(name, callback, unit=unit, description=description) @@ -86,6 +101,13 @@ def callback(self) -> CallbackT: class Counter(_Synchronous, APICounter): + @property + def _default_aggregation(self) -> _Aggregation: + return _SumAggregation( + instrument_is_monotonic=True, + instrument_temporality=AggregationTemporality.DELTA, + ) + def add( self, amount: Union[int, float], attributes: Dict[str, str] = None ): @@ -100,6 +122,13 @@ def add( class UpDownCounter(_Synchronous, APIUpDownCounter): + @property + def _default_aggregation(self) -> _Aggregation: + return _SumAggregation( + instrument_is_monotonic=False, + instrument_temporality=AggregationTemporality.DELTA, + ) + def add( self, amount: Union[int, float], attributes: Dict[str, str] = None ): @@ -109,14 +138,28 @@ def add( class ObservableCounter(_Asynchronous, APIObservableCounter): - pass + @property + def _default_aggregation(self) -> _Aggregation: + return _SumAggregation( + instrument_is_monotonic=True, + instrument_temporality=AggregationTemporality.CUMULATIVE, + ) class ObservableUpDownCounter(_Asynchronous, APIObservableUpDownCounter): - pass + @property + def _default_aggregation(self) -> _Aggregation: + return _SumAggregation( + instrument_is_monotonic=False, + instrument_temporality=AggregationTemporality.CUMULATIVE, + ) class Histogram(_Synchronous, APIHistogram): + @property + def _default_aggregation(self) -> _Aggregation: + return _ExplicitBucketHistogramAggregation() + def record( self, amount: Union[int, float], attributes: Dict[str, str] = None ): @@ -132,4 +175,6 @@ def record( class ObservableGauge(_Asynchronous, APIObservableGauge): - pass + @property + def _default_aggregation(self) -> _Aggregation: + return _LastValueAggregation() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py index 01724742f2..a02ec07f88 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py @@ -15,16 +15,11 @@ from threading import RLock from typing import Dict, Iterable, List -from opentelemetry._metrics.instrument import Counter, Histogram, Instrument +from opentelemetry._metrics.instrument import Instrument from opentelemetry.sdk._metrics._view_instrument_match import ( _ViewInstrumentMatch, ) -from opentelemetry.sdk._metrics.aggregation import ( - AggregationTemporality, - _ExplicitBucketHistogramAggregation, - _LastValueAggregation, - _SumAggregation, -) +from opentelemetry.sdk._metrics.aggregation import AggregationTemporality from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import Metric from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration @@ -46,6 +41,7 @@ def _get_or_init_view_instrument_match( ) -> List["_ViewInstrumentMatch"]: # Optimistically get the relevant views for the given instrument. Once set for a given # instrument, the mapping will never change + if instrument in self._view_instrument_match: return self._view_instrument_match[instrument] @@ -55,50 +51,59 @@ def _get_or_init_view_instrument_match( return self._view_instrument_match[instrument] # not present, hold the lock and add a new mapping - matches = [] + view_instrument_matches = [] for view in self._sdk_config.views: - if view.match(instrument): - # Note: if a view matches multiple instruments, this will create a separate - # _ViewInstrumentMatch per instrument. If the user's View configuration includes a - # name, this will cause multiple conflicting output streams. - matches.append( + # pylint: disable=protected-access + if view._match(instrument): + + if view._aggregation is not None: + aggregation = view._aggregation._create_aggregation( + instrument + ) + else: + aggregation = instrument._default_aggregation + + view_instrument_matches.append( _ViewInstrumentMatch( - name=view.name or instrument.name, - resource=self._sdk_config.resource, - instrumentation_info=None, - aggregation=view.aggregation, + name=view._name or instrument.name, unit=instrument.unit, - description=view.description, + description=( + view._description or instrument.description + ), + aggregation=aggregation, + instrumentation_info=( + instrument.instrumentation_info + ), + resource=self._sdk_config.resource, + attribute_keys=view._attribute_keys, ) ) # if no view targeted the instrument, use the default - if not matches: - # TODO: the logic to select aggregation could be moved - if isinstance(instrument, Counter): - agg = _SumAggregation(True, AggregationTemporality.DELTA) - elif isinstance(instrument, Histogram): - agg = _ExplicitBucketHistogramAggregation() - else: - agg = _LastValueAggregation() - matches.append( + if ( + not view_instrument_matches + and self._sdk_config.enable_default_view + ): + view_instrument_matches.append( _ViewInstrumentMatch( - resource=self._sdk_config.resource, - instrumentation_info=None, - aggregation=agg, + name=instrument.name, unit=instrument.unit, description=instrument.description, - name=instrument.name, + # pylint: disable=protected-access + aggregation=instrument._default_aggregation, + instrumentation_info=instrument.instrumentation_info, + resource=self._sdk_config.resource, + attribute_keys=set(), ) ) - self._view_instrument_match[instrument] = matches - return matches + self._view_instrument_match[instrument] = view_instrument_matches + return view_instrument_matches def consume_measurement(self, measurement: Measurement) -> None: - for matches in self._get_or_init_view_instrument_match( + for view_instrument_match in self._get_or_init_view_instrument_match( measurement.instrument ): - matches.consume_measurement(measurement) + view_instrument_match.consume_measurement(measurement) def collect(self, temporality: AggregationTemporality) -> Iterable[Metric]: # use a list instead of yielding to prevent a slow reader from holding SDK locks @@ -111,9 +116,11 @@ def collect(self, temporality: AggregationTemporality) -> Iterable[Metric]: # that end times can be slightly skewed among the metric streams produced by the SDK, # but we still align the output timestamps for a single instrument. with self._lock: - for matches in self._view_instrument_match.values(): - for match in matches: - metrics.extend(match.collect(temporality)) + for ( + view_instrument_matches + ) in self._view_instrument_match.values(): + for view_instrument_match in view_instrument_matches: + metrics.extend(view_instrument_match.collect(temporality)) return metrics diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py index 2c603b5e4d..c617696717 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py @@ -11,3 +11,4 @@ class SdkConfiguration: resource: Resource metric_readers: Sequence[MetricReader] views: Sequence[View] + enable_default_view: bool diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py index e103425c03..b4b2797111 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py @@ -13,8 +13,152 @@ # limitations under the License. -# TODO: #2247 -# pylint: disable=no-self-use +from fnmatch import fnmatch +from logging import getLogger +from typing import Optional, Set, Type + +# FIXME import from typing when support for 3.6 is removed +from typing_extensions import final + +from opentelemetry._metrics.instrument import Instrument +from opentelemetry.sdk._metrics.aggregation import _AggregationFactory + +_logger = getLogger(__name__) + + class View: - def match(self) -> bool: - return False + def __init__( + self, + instrument_type: Optional[Type[Instrument]] = None, + instrument_name: Optional[str] = None, + meter_name: Optional[str] = None, + meter_version: Optional[str] = None, + meter_schema_url: Optional[str] = None, + name: Optional[str] = None, + description: Optional[str] = None, + attribute_keys: Optional[Set[str]] = None, + aggregation: Optional[_AggregationFactory] = None, + ): + """ + A `View` configuration parameters can be used for the following + purposes: + + 1. Match instruments: When an instrument matches a view, measurements + received by that instrument will be processed. + 2. Customize metric streams: A metric stream is identified by a match + between a view and an instrument and a set of attributes. The metric + stream can be customized by certain attributes of the corresponding + view. + + The attributes documented next serve one of the previous two purposes. + + Args: + instrument_type: This is an instrument matching attribute: the + class the instrument must be to match the view. + + instrument_name: This is an instrument matching attribute: the name + the instrument must have to match the view. Wild card + characters are supported. Wild card characters should not be + used with this attribute if the view has also a + ``name`` defined. + + meter_name: This is an instrument matching attribute: the name + the instrument meter must have to match the view. + + meter_version : This is an instrument matching attribute: the + version the instrument meter must have to match the view. + + meter_schema URL : This is an instrument matching attribute: the + schema URL the instrument meter must have to match the view. + + name: This is a metric stream customizing attribute: the name of + the metric stream. If `None`, the name of the instrument will + be used. + + description: This is a metric stream customizing attribute: the + description of the metric stream. If `None`, the description of + the instrument will be used. + + attribute_keys: This is a metric stream customizing attribute: this + is a set of attribute keys. If not `None` then only the + measurement attributes that are in `attribute_keys` will be + used to identify the metric stream. + + aggregation: This is a metric stream customizing attribute: the + aggregatation instance to use when data is aggregated for the + corresponding metrics stream. If `None` the default aggregation + of the instrument will be used. + + This class is not intended to be subclassed by the user. + """ + + if ( + instrument_type + is instrument_name + is meter_name + is meter_version + is meter_schema_url + is None + ): + raise Exception( + "Some instrument selection " + f"criteria must be provided for View {name}" + ) + + if ( + name is not None + and instrument_name is not None + and ("*" in instrument_name or "?" in instrument_name) + ): + + raise Exception( + f"View {name} declared with wildcard " + "characters in instrument_name" + ) + + # _name, _description, _aggregation and _attribute_keys will be + # accessed when instantiating a _ViewInstrumentMatch. + self._name = name + self._instrument_type = instrument_type + self._instrument_name = instrument_name + self._meter_name = meter_name + self._meter_version = meter_version + self._meter_schema_url = meter_schema_url + + self._description = description + self._attribute_keys = attribute_keys + + if self._attribute_keys is None: + self._attribute_keys = set() + + self._aggregation = aggregation + + # pylint: disable=too-many-return-statements + # pylint: disable=too-many-branches + @final + def _match(self, instrument: Instrument) -> bool: + + if self._instrument_type is not None: + if not isinstance(instrument, self._instrument_type): + return False + + if self._instrument_name is not None: + if not fnmatch(instrument.name, self._instrument_name): + return False + + if self._meter_name is not None: + if instrument.instrumentation_info.name != self._meter_name: + return False + + if self._meter_version is not None: + if instrument.instrumentation_info.version != self._meter_version: + return False + + if self._meter_schema_url is not None: + if ( + instrument.instrumentation_info.schema_url + != self._meter_schema_url + ): + return False + + return True diff --git a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py index aa481482ec..9e2416a7c1 100644 --- a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py +++ b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py @@ -36,7 +36,10 @@ def test_creates_metric_reader_storages(self, MockMetricReaderStorage): reader_mocks = [Mock() for _ in range(5)] SynchronousMeasurementConsumer( SdkConfiguration( - resource=Mock(), metric_readers=reader_mocks, views=() + resource=Mock(), + metric_readers=reader_mocks, + views=Mock(), + enable_default_view=Mock(), ) ) self.assertEqual(len(MockMetricReaderStorage.mock_calls), 5) @@ -50,7 +53,10 @@ def test_measurements_passed_to_each_reader_storage( consumer = SynchronousMeasurementConsumer( SdkConfiguration( - resource=Mock(), metric_readers=reader_mocks, views=() + resource=Mock(), + metric_readers=reader_mocks, + views=Mock(), + enable_default_view=Mock(), ) ) measurement_mock = Mock() @@ -69,7 +75,10 @@ def test_collect_passed_to_reader_stage(self, MockMetricReaderStorage): consumer = SynchronousMeasurementConsumer( SdkConfiguration( - resource=Mock(), metric_readers=reader_mocks, views=() + resource=Mock(), + metric_readers=reader_mocks, + views=Mock(), + enable_default_view=Mock(), ) ) for r_mock, rs_mock in zip(reader_mocks, reader_storage_mocks): @@ -87,7 +96,10 @@ def test_collect_calls_async_instruments(self, MockMetricReaderStorage): MockMetricReaderStorage.return_value = reader_storage_mock consumer = SynchronousMeasurementConsumer( SdkConfiguration( - resource=Mock(), metric_readers=[reader_mock], views=() + resource=Mock(), + metric_readers=[reader_mock], + views=Mock(), + enable_default_view=Mock(), ) ) async_instrument_mocks = [MagicMock() for _ in range(5)] diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index a5b44b2333..0b338b5013 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -14,18 +14,21 @@ from unittest.mock import Mock, patch +from opentelemetry.sdk._metrics.aggregation import SumAggregation +from opentelemetry.sdk._metrics.instrument import Counter from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.metric_reader_storage import ( MetricReaderStorage, ) from opentelemetry.sdk._metrics.point import AggregationTemporality from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration +from opentelemetry.sdk._metrics.view import View from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc -def mock_view_matching(*instruments) -> Mock: - mock = Mock() - mock.match.side_effect = lambda instrument: instrument in instruments +def mock_view_matching(name, *instruments) -> Mock: + mock = Mock(name=name) + mock._match.side_effect = lambda instrument: instrument in instruments return mock @@ -35,8 +38,10 @@ def mock_instrument() -> Mock: return instr -@patch("opentelemetry.sdk._metrics.metric_reader_storage._ViewInstrumentMatch") class TestMetricReaderStorage(ConcurrencyTestBase): + @patch( + "opentelemetry.sdk._metrics.metric_reader_storage._ViewInstrumentMatch" + ) def test_creates_view_instrument_matches( self, MockViewInstrumentMatch: Mock ): @@ -44,11 +49,14 @@ def test_creates_view_instrument_matches( instrument1 = Mock(name="instrument1") instrument2 = Mock(name="instrument2") - view1 = mock_view_matching(instrument1) - view2 = mock_view_matching(instrument1, instrument2) + view1 = mock_view_matching("view_1", instrument1) + view2 = mock_view_matching("view_2", instrument1, instrument2) storage = MetricReaderStorage( SdkConfiguration( - resource=Mock(), metric_readers=(), views=(view1, view2) + resource=Mock(), + metric_readers=(), + views=(view1, view2), + enable_default_view=True, ) ) @@ -68,6 +76,9 @@ def test_creates_view_instrument_matches( storage.consume_measurement(Measurement(1, instrument2)) self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 1) + @patch( + "opentelemetry.sdk._metrics.metric_reader_storage._ViewInstrumentMatch" + ) def test_forwards_calls_to_view_instrument_match( self, MockViewInstrumentMatch: Mock ): @@ -82,11 +93,15 @@ def test_forwards_calls_to_view_instrument_match( instrument1 = Mock(name="instrument1") instrument2 = Mock(name="instrument2") - view1 = mock_view_matching(instrument1) - view2 = mock_view_matching(instrument1, instrument2) + view1 = mock_view_matching("view1", instrument1) + view2 = mock_view_matching("view2", instrument1, instrument2) + storage = MetricReaderStorage( SdkConfiguration( - resource=Mock(), metric_readers=(), views=(view1, view2) + resource=Mock(), + metric_readers=(), + views=(view1, view2), + enable_default_view=True, ) ) @@ -120,6 +135,9 @@ def test_forwards_calls_to_view_instrument_match( view_instrument_match3.collect.assert_called_once() self.assertEqual(result, all_metrics) + @patch( + "opentelemetry.sdk._metrics.metric_reader_storage._ViewInstrumentMatch" + ) def test_race_concurrent_measurements(self, MockViewInstrumentMatch: Mock): mock_view_instrument_match_ctor = MockFunc() MockViewInstrumentMatch.side_effect = mock_view_instrument_match_ctor @@ -128,7 +146,10 @@ def test_race_concurrent_measurements(self, MockViewInstrumentMatch: Mock): view1 = mock_view_matching(instrument1) storage = MetricReaderStorage( SdkConfiguration( - resource=Mock(), metric_readers=(), views=(view1,) + resource=Mock(), + metric_readers=(), + views=(view1,), + enable_default_view=True, ) ) @@ -140,3 +161,94 @@ def send_measurement(): # _ViewInstrumentMatch constructor should have only been called once self.assertEqual(mock_view_instrument_match_ctor.call_count, 1) + + def test_aggregations(self): + + view = View(instrument_name="counter*", aggregation=SumAggregation()) + + metric_reader_storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=(view,), + enable_default_view=True, + ) + ) + + counter_0 = Counter("counter_0", Mock(), Mock()) + counter_1 = Counter("counter_1", Mock(), Mock()) + + metric_reader_storage.consume_measurement(Measurement(1, counter_0)) + metric_reader_storage.consume_measurement(Measurement(2, counter_1)) + + self.assertEqual( + 1, + metric_reader_storage._view_instrument_match[counter_0][ + 0 + ]._aggregation._value, + ) + self.assertEqual( + 2, + metric_reader_storage._view_instrument_match[counter_1][ + 0 + ]._aggregation._value, + ) + + @patch( + "opentelemetry.sdk._metrics.metric_reader_storage._ViewInstrumentMatch" + ) + def test_default_view_enabled(self, MockViewInstrumentMatch: Mock): + """Instruments should be matched with default views when enabled""" + instrument1 = Mock(name="instrument1") + instrument2 = Mock(name="instrument2") + + storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=(), + enable_default_view=True, + ) + ) + + storage.consume_measurement(Measurement(1, instrument1)) + self.assertEqual( + len(MockViewInstrumentMatch.call_args_list), + 1, + MockViewInstrumentMatch.mock_calls, + ) + storage.consume_measurement(Measurement(1, instrument1)) + self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 1) + + MockViewInstrumentMatch.call_args_list.clear() + storage.consume_measurement(Measurement(1, instrument2)) + self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 1) + + @patch( + "opentelemetry.sdk._metrics.metric_reader_storage._ViewInstrumentMatch" + ) + def test_default_view_disabled(self, MockViewInstrumentMatch: Mock): + """Instruments should not be matched with default views when disabled""" + instrument1 = Mock(name="instrument1") + instrument2 = Mock(name="instrument2") + storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=(), + enable_default_view=False, + ) + ) + + storage.consume_measurement(Measurement(1, instrument1)) + self.assertEqual( + len(MockViewInstrumentMatch.call_args_list), + 0, + MockViewInstrumentMatch.mock_calls, + ) + storage.consume_measurement(Measurement(1, instrument1)) + self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 0) + + MockViewInstrumentMatch.call_args_list.clear() + storage.consume_measurement(Measurement(1, instrument2)) + self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 0) diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py new file mode 100644 index 0000000000..80f5884c8d --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -0,0 +1,112 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase +from unittest.mock import Mock + +from opentelemetry.sdk._metrics.view import View + + +class TestView(TestCase): + def test_required_instrument_criteria(self): + + with self.assertRaises(Exception): + View() + + def test_instrument_type(self): + + self.assertTrue(View(instrument_type=Mock)._match(Mock())) + + def test_instrument_name(self): + + mock_instrument = Mock() + mock_instrument.configure_mock(**{"name": "instrument_name"}) + + self.assertTrue( + View(instrument_name="instrument_name")._match(mock_instrument) + ) + + def test_meter_name(self): + + self.assertTrue( + View(meter_name="meter_name")._match( + Mock(**{"instrumentation_info.name": "meter_name"}) + ) + ) + + def test_meter_version(self): + + self.assertTrue( + View(meter_version="meter_version")._match( + Mock(**{"instrumentation_info.version": "meter_version"}) + ) + ) + + def test_meter_schema_url(self): + + self.assertTrue( + View(meter_schema_url="meter_schema_url")._match( + Mock(**{"instrumentation_info.schema_url": "meter_schema_url"}) + ) + ) + self.assertFalse( + View(meter_schema_url="meter_schema_url")._match( + Mock( + **{ + "instrumentation_info.schema_url": "meter_schema_urlabc" + } + ) + ) + ) + self.assertTrue( + View(meter_schema_url="meter_schema_url")._match( + Mock(**{"instrumentation_info.schema_url": "meter_schema_url"}) + ) + ) + + def test_additive_criteria(self): + + view = View( + meter_name="meter_name", + meter_version="meter_version", + meter_schema_url="meter_schema_url", + ) + + self.assertTrue( + view._match( + Mock( + **{ + "instrumentation_info.name": "meter_name", + "instrumentation_info.version": "meter_version", + "instrumentation_info.schema_url": "meter_schema_url", + } + ) + ) + ) + self.assertFalse( + view._match( + Mock( + **{ + "instrumentation_info.name": "meter_name", + "instrumentation_info.version": "meter_version", + "instrumentation_info.schema_url": "meter_schema_vrl", + } + ) + ) + ) + + def test_view_name(self): + + with self.assertRaises(Exception): + View(name="name", instrument_name="instrument_name*") diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index c074729403..fc2c0e7514 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -27,9 +27,6 @@ class Test_ViewInstrumentMatch(TestCase): def setUpClass(cls): cls.mock_aggregation_instance = Mock() - cls.mock_aggregation_class = Mock( - return_value=cls.mock_aggregation_instance - ) cls.mock_resource = Mock() cls.mock_instrumentation_info = Mock() @@ -39,7 +36,7 @@ def test_consume_measurement(self): "name", "unit", "description", - self.mock_aggregation_class, + self.mock_aggregation_instance, self.mock_instrumentation_info, self.mock_resource, {"a", "c"}, @@ -79,9 +76,10 @@ def test_consume_measurement(self): "name", "unit", "description", - self.mock_aggregation_class, + self.mock_aggregation_instance, self.mock_instrumentation_info, self.mock_resource, + set(), ) view_instrument_match.consume_measurement( @@ -104,9 +102,10 @@ def test_consume_measurement(self): "name", "unit", "description", - self.mock_aggregation_class, + self.mock_aggregation_instance, self.mock_instrumentation_info, self.mock_resource, + set(), ) view_instrument_match.consume_measurement( Measurement(value=0, instrument=instrument1, attributes=None) @@ -122,7 +121,7 @@ def test_collect(self): "name", "unit", "description", - self.mock_aggregation_class, + self.mock_aggregation_instance, self.mock_instrumentation_info, self.mock_resource, {"a", "c"}, From e93294748d39503f0fa4aee70ebc81a906b2cf04 Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Thu, 3 Mar 2022 17:34:46 -0500 Subject: [PATCH 1146/1517] CONTRIBUTING: add cmd to install tox on first mention of tox (#2497) --- CONTRIBUTING.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac76ccc70d..35d5b179c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,10 +53,15 @@ as well as the project's packages themselves (in `--editable` mode). You can then run `scripts/eachdist.py test` to test everything or `scripts/eachdist.py lint` to lint everything (fixing anything that is auto-fixable). -Additionally, this project uses [`tox`](https://tox.readthedocs.io) to automate some aspects -of development, including testing against multiple Python versions. +Additionally, this project uses [tox](https://tox.readthedocs.io) to automate +some aspects of development, including testing against multiple Python versions. +To install `tox`, run: -You can run: +```console +$ pip install tox +``` + +You can run `tox` with the following arguments: - `tox` to run all existing tox commands, including unit tests for all packages under multiple Python versions @@ -88,7 +93,7 @@ with a specific git commit hash by setting an environment variable before runnin CONTRIB_REPO_SHA=dde62cebffe519c35875af6d06fae053b3be65ec tox ``` -The continuation integration overrides that environment variable with as per the configuration +The continuation integration overrides that environment variable with as per the configuration [here](https://github.com/open-telemetry/opentelemetry-python/blob/9020b0baaeb41b7137badca988bb5c2d562cddee/.github/workflows/test.yml#L13). ### Benchmarks @@ -125,13 +130,13 @@ pull requests (PRs). To create a new PR, fork the project in GitHub and clone the upstream repo: -```sh +```console $ git clone https://github.com/open-telemetry/opentelemetry-python.git ``` Add your fork as an origin: -```sh +```console $ git remote add fork https://github.com/YOUR_GITHUB_USERNAME/opentelemetry-python.git ``` @@ -154,7 +159,7 @@ $ git push fork feature Open a pull request against the main `opentelemetry-python` repo. -Pull requests are also tested for their compatibility with packages distributed +Pull requests are also tested for their compatibility with packages distributed by OpenTelemetry in the [OpenTelemetry Python Contrib Repository](https://github.com/open-telemetry/opentelemetry-python.git). If a pull request (PR) introduces a change that would break the compatibility of @@ -196,7 +201,7 @@ Any Approver / Maintainer can merge the PR once it is **ready to merge**. ## Design Choices -As with other OpenTelemetry clients, opentelemetry-python follows the +As with other OpenTelemetry clients, opentelemetry-python follows the [opentelemetry-specification](https://github.com/open-telemetry/opentelemetry-specification). It's especially valuable to read through the [library guidelines](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/library-guidelines.md). @@ -209,7 +214,7 @@ use cases are clear, but the method to satisfy those uses cases are not. As such, contributions should provide functionality and behavior that conforms to the specification, but the interface and structure is flexible. -It is preferable to have contributions follow the idioms of the language +It is preferable to have contributions follow the idioms of the language rather than conform to specific API names or argument patterns in the spec. For a deeper discussion, see: https://github.com/open-telemetry/opentelemetry-specification/issues/165 From b783d166f42ac9d87729d2efabd01193bf82733b Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 4 Mar 2022 12:15:44 -0800 Subject: [PATCH 1147/1517] `opentelemetry-exporter-prometheus`: restore package (#2321) --- CHANGELOG.md | 2 + eachdist.ini | 2 +- .../opentelemetry-exporter-prometheus/LICENSE | 201 +++++++++++++++++ .../MANIFEST.in | 9 + .../README.rst | 23 ++ .../setup.cfg | 55 +++++ .../setup.py | 26 +++ .../exporter/prometheus/__init__.py | 208 ++++++++++++++++++ .../exporter/prometheus/version.py | 15 ++ .../tests/__init__.py | 13 ++ .../tests/test_prometheus_exporter.py | 154 +++++++++++++ tox.ini | 7 + 12 files changed, 714 insertions(+), 1 deletion(-) create mode 100644 exporter/opentelemetry-exporter-prometheus/LICENSE create mode 100644 exporter/opentelemetry-exporter-prometheus/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-prometheus/README.rst create mode 100644 exporter/opentelemetry-exporter-prometheus/setup.cfg create mode 100644 exporter/opentelemetry-exporter-prometheus/setup.py create mode 100644 exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py create mode 100644 exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py create mode 100644 exporter/opentelemetry-exporter-prometheus/tests/__init__.py create mode 100644 exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d5d58c8293..fc12fea820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2464](https://github.com/open-telemetry/opentelemetry-python/pull/2464)) - Fix `OTEL_EXPORTER_OTLP_ENDPOINT` usage in OTLP HTTP trace exporter ([#2493](https://github.com/open-telemetry/opentelemetry-python/pull/2493)) +- [exporter/opentelemetry-exporter-prometheus] restore package using the new metrics API + ([#2321](https://github.com/open-telemetry/opentelemetry-python/pull/2321)) ## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1-0.28b1) - 2022-01-29 diff --git a/eachdist.ini b/eachdist.ini index 576d46a5fc..f65be079be 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -35,6 +35,7 @@ version=0.28b1 packages= opentelemetry-opentracing-shim opentelemetry-exporter-opencensus + opentelemetry-exporter-prometheus opentelemetry-distro opentelemetry-semantic-conventions opentelemetry-test-utils @@ -45,7 +46,6 @@ version=1.10a0 packages= opentelemetry-exporter-prometheus-remote-write - opentelemetry-exporter-prometheus opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc diff --git a/exporter/opentelemetry-exporter-prometheus/LICENSE b/exporter/opentelemetry-exporter-prometheus/LICENSE new file mode 100644 index 0000000000..1ef7dad2c5 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/LICENSE @@ -0,0 +1,201 @@ + 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 The OpenTelemetry Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/exporter/opentelemetry-exporter-prometheus/MANIFEST.in b/exporter/opentelemetry-exporter-prometheus/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/exporter/opentelemetry-exporter-prometheus/README.rst b/exporter/opentelemetry-exporter-prometheus/README.rst new file mode 100644 index 0000000000..a3eb920000 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry Prometheus Exporter +================================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-prometheus.svg + :target: https://pypi.org/project/opentelemetry-exporter-prometheus/ + +This library allows to export metrics data to `Prometheus `_. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-prometheus + +References +---------- + +* `OpenTelemetry Prometheus Exporter `_ +* `Prometheus `_ +* `OpenTelemetry Project `_ diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg new file mode 100644 index 0000000000..53e9ef17ed --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-exporter-prometheus +description = Prometheus Metric Exporter for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-prometheus +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + +[options] +python_requires = >=3.6 +package_dir= + =src +packages=find_namespace: +install_requires = + prometheus_client >= 0.5.0, < 1.0.0 + opentelemetry-api >= 1.9.1 + opentelemetry-sdk >= 1.9.1 + +[options.packages.find] +where = src + +[options.extras_require] +test = + +[options.entry_points] +opentelemetry_metric_reader = + prometheus = opentelemetry.exporter.prometheus:PrometheusMetricReader diff --git a/exporter/opentelemetry-exporter-prometheus/setup.py b/exporter/opentelemetry-exporter-prometheus/setup.py new file mode 100644 index 0000000000..9fdbd7de49 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +_BASE_DIR = os.path.dirname(__file__) +_VERSION_FILENAME = os.path.join( + _BASE_DIR, "src", "opentelemetry", "exporter", "prometheus", "version.py" +) +_PACKAGE_INFO = {} +with open(_VERSION_FILENAME, encoding="utf-8") as f: + exec(f.read(), _PACKAGE_INFO) + +setuptools.setup(version=_PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py new file mode 100644 index 0000000000..a952e56bcf --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -0,0 +1,208 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This library allows export of metrics data to `Prometheus `_. + +Usage +----- + +The **OpenTelemetry Prometheus Exporter** allows export of `OpenTelemetry`_ +metrics to `Prometheus`_. + + +.. _Prometheus: https://prometheus.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + +.. code:: python + + from prometheus_client import start_http_server + + from opentelemetry._metrics import get_meter_provider, set_meter_provider + from opentelemetry.exporter.prometheus import PrometheusMetricReader + from opentelemetry.sdk._metrics import MeterProvider + + # Start Prometheus client + start_http_server(port=8000, addr="localhost") + + # Exporter to export metrics to Prometheus + prefix = "MyAppPrefix" + reader = PrometheusMetricReader(prefix) + + # Meter is responsible for creating and recording metrics + set_meter_provider(MeterProvider(metric_readers=[reader])) + meter = get_meter_provider().get_meter("myapp", "0.1.2") + + counter = meter.create_counter( + "requests", + "requests", + "number of requests", + ) + + # Labels are used to identify key-values that are associated with a specific + # metric that you want to record. These are useful for pre-aggregation and can + # be used to store custom dimensions pertaining to a metric + labels = {"environment": "staging"} + + counter.add(25, labels) + input("Press any key to exit...") + +API +--- +""" + +import collections +import logging +import re +from typing import Iterable, Optional, Sequence, Tuple + +from prometheus_client import core + +from opentelemetry.sdk._metrics.export import MetricReader +from opentelemetry.sdk._metrics.point import Gauge, Histogram, Metric, Sum + +_logger = logging.getLogger(__name__) + + +def _convert_buckets(metric: Metric) -> Sequence[Tuple[str, int]]: + buckets = [] + total_count = 0 + for index, value in enumerate(metric.point.bucket_counts): + total_count += value + buckets.append( + ( + f"{metric.point.explicit_bounds[index]}", + total_count, + ) + ) + return buckets + + +class PrometheusMetricReader(MetricReader): + """Prometheus metric exporter for OpenTelemetry. + + Args: + prefix: single-word application prefix relevant to the domain + the metric belongs to. + """ + + def __init__(self, prefix: str = "") -> None: + super().__init__() + self._collector = _CustomCollector(prefix) + core.REGISTRY.register(self._collector) + self._collector._callback = self.collect + + def _receive_metrics(self, metrics: Iterable[Metric]) -> None: + if metrics is None: + return + self._collector.add_metrics_data(metrics) + + def shutdown(self) -> bool: + core.REGISTRY.unregister(self._collector) + return True + + +class _CustomCollector: + """_CustomCollector represents the Prometheus Collector object + + See more: + https://github.com/prometheus/client_python#custom-collectors + """ + + def __init__(self, prefix: str = ""): + self._prefix = prefix + self._callback = None + self._metrics_to_export = collections.deque() + self._non_letters_digits_underscore_re = re.compile( + r"[^\w]", re.UNICODE | re.IGNORECASE + ) + + def add_metrics_data(self, export_records: Sequence[Metric]) -> None: + """Add metrics to Prometheus data""" + self._metrics_to_export.append(export_records) + + def collect(self) -> None: + """Collect fetches the metrics from OpenTelemetry + and delivers them as Prometheus Metrics. + Collect is invoked every time a ``prometheus.Gatherer`` is run + for example when the HTTP endpoint is invoked by Prometheus. + """ + if self._callback is not None: + self._callback() + + while self._metrics_to_export: + for export_record in self._metrics_to_export.popleft(): + prometheus_metric = self._translate_to_prometheus( + export_record + ) + if prometheus_metric is not None: + yield prometheus_metric + + def _translate_to_prometheus( + self, metric: Metric + ) -> Optional[core.Metric]: + prometheus_metric = None + label_values = [] + label_keys = [] + for key, value in metric.attributes.items(): + label_keys.append(self._sanitize(key)) + label_values.append(str(value)) + + metric_name = "" + if self._prefix != "": + metric_name = self._prefix + "_" + metric_name += self._sanitize(metric.name) + + description = metric.description or "" + if isinstance(metric.point, Sum): + prometheus_metric = core.CounterMetricFamily( + name=metric_name, + documentation=description, + labels=label_keys, + unit=metric.unit, + ) + prometheus_metric.add_metric( + labels=label_values, value=metric.point.value + ) + elif isinstance(metric.point, Gauge): + prometheus_metric = core.GaugeMetricFamily( + name=metric_name, + documentation=description, + labels=label_keys, + unit=metric.unit, + ) + prometheus_metric.add_metric( + labels=label_values, value=metric.point.value + ) + elif isinstance(metric.point, Histogram): + value = metric.point.sum + prometheus_metric = core.HistogramMetricFamily( + name=metric_name, + documentation=description, + labels=label_keys, + unit=metric.unit, + ) + buckets = _convert_buckets(metric) + prometheus_metric.add_metric( + labels=label_values, buckets=buckets, sum_value=value + ) + else: + _logger.warning("Unsupported metric type. %s", type(metric.point)) + return prometheus_metric + + def _sanitize(self, key: str) -> str: + """sanitize the given metric name or label according to Prometheus rule. + Replace all characters other than [A-Za-z0-9_] with '_'. + """ + return self._non_letters_digits_underscore_re.sub("_", key) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py new file mode 100644 index 0000000000..a94d4ed446 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.28b1" diff --git a/exporter/opentelemetry-exporter-prometheus/tests/__init__.py b/exporter/opentelemetry-exporter-prometheus/tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py new file mode 100644 index 0000000000..af4bc7c0fd --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -0,0 +1,154 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from prometheus_client import generate_latest +from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily + +from opentelemetry.exporter.prometheus import ( + PrometheusMetricReader, + _CustomCollector, +) +from opentelemetry.sdk._metrics.point import AggregationTemporality, Histogram +from opentelemetry.test.metrictestutil import ( + _generate_gauge, + _generate_metric, + _generate_sum, + _generate_unsupported_metric, +) + + +class TestPrometheusMetricReader(unittest.TestCase): + def setUp(self): + self._mock_registry_register = mock.Mock() + self._registry_register_patch = mock.patch( + "prometheus_client.core.REGISTRY.register", + side_effect=self._mock_registry_register, + ) + + # pylint: disable=protected-access + def test_constructor(self): + """Test the constructor.""" + with self._registry_register_patch: + exporter = PrometheusMetricReader("testprefix") + self.assertEqual(exporter._collector._prefix, "testprefix") + self.assertTrue(self._mock_registry_register.called) + + def test_shutdown(self): + with mock.patch( + "prometheus_client.core.REGISTRY.unregister" + ) as registry_unregister_patch: + exporter = PrometheusMetricReader() + exporter.shutdown() + self.assertTrue(registry_unregister_patch.called) + + def test_histogram_to_prometheus(self): + record = _generate_metric( + "test@name", + Histogram( + time_unix_nano=1641946016139533244, + start_time_unix_nano=1641946016139533244, + bucket_counts=[1, 1], + sum=579.0, + explicit_bounds=[123.0, 456.0], + aggregation_temporality=AggregationTemporality.DELTA, + ), + attributes={"histo": 1}, + ) + + collector = _CustomCollector("testprefix") + collector.add_metrics_data([record]) + result_bytes = generate_latest(collector) + result = result_bytes.decode("utf-8") + self.assertIn('testprefix_test_name_s_sum{histo="1"} 579.0', result) + self.assertIn('testprefix_test_name_s_count{histo="1"} 2.0', result) + + def test_sum_to_prometheus(self): + labels = {"environment@": "staging", "os": "Windows"} + record = _generate_sum( + "test@sum", + 123, + attributes=labels, + description="testdesc", + unit="testunit", + ) + collector = _CustomCollector("testprefix") + collector.add_metrics_data([record]) + + for prometheus_metric in collector.collect(): + self.assertEqual(type(prometheus_metric), CounterMetricFamily) + self.assertEqual( + prometheus_metric.name, "testprefix_test_sum_testunit" + ) + self.assertEqual(prometheus_metric.documentation, "testdesc") + self.assertTrue(len(prometheus_metric.samples) == 1) + self.assertEqual(prometheus_metric.samples[0].value, 123) + self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) + self.assertEqual( + prometheus_metric.samples[0].labels["environment_"], "staging" + ) + self.assertEqual( + prometheus_metric.samples[0].labels["os"], "Windows" + ) + + def test_gauge_to_prometheus(self): + labels = {"environment@": "dev", "os": "Unix"} + record = _generate_gauge( + "test@gauge", + 123, + attributes=labels, + description="testdesc", + unit="testunit", + ) + collector = _CustomCollector("testprefix") + collector.add_metrics_data([record]) + + for prometheus_metric in collector.collect(): + self.assertEqual(type(prometheus_metric), GaugeMetricFamily) + self.assertEqual( + prometheus_metric.name, "testprefix_test_gauge_testunit" + ) + self.assertEqual(prometheus_metric.documentation, "testdesc") + self.assertTrue(len(prometheus_metric.samples) == 1) + self.assertEqual(prometheus_metric.samples[0].value, 123) + self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) + self.assertEqual( + prometheus_metric.samples[0].labels["environment_"], "dev" + ) + self.assertEqual(prometheus_metric.samples[0].labels["os"], "Unix") + + def test_invalid_metric(self): + labels = {"environment": "staging"} + record = _generate_unsupported_metric( + "tesname", + attributes=labels, + description="testdesc", + unit="testunit", + ) + collector = _CustomCollector("testprefix") + collector.add_metrics_data([record]) + collector.collect() + self.assertLogs("opentelemetry.exporter.prometheus", level="WARNING") + + def test_sanitize(self): + collector = _CustomCollector("testprefix") + self.assertEqual( + collector._sanitize("1!2@3#4$5%6^7&8*9(0)_-"), + "1_2_3_4_5_6_7_8_9_0___", + ) + self.assertEqual(collector._sanitize(",./?;:[]{}"), "__________") + self.assertEqual(collector._sanitize("TestString"), "TestString") + self.assertEqual(collector._sanitize("aAbBcC_12_oi"), "aAbBcC_12_oi") diff --git a/tox.ini b/tox.ini index 44c4367588..8d5973b166 100644 --- a/tox.ini +++ b/tox.ini @@ -42,6 +42,9 @@ envlist = py3{6,7,8,9,10}-opentelemetry-exporter-otlp-proto-http pypy3-opentelemetry-exporter-otlp-proto-http + py3{6,7,8,9,10}-opentelemetry-exporter-prometheus + pypy3-opentelemetry-exporter-prometheus + ; opentelemetry-exporter-zipkin py3{6,7,8,9,10}-opentelemetry-exporter-zipkin-combined pypy3-opentelemetry-exporter-zipkin-combined @@ -97,6 +100,7 @@ changedir = exporter-otlp-combined: exporter/opentelemetry-exporter-otlp/tests exporter-otlp-proto-grpc: exporter/opentelemetry-exporter-otlp-proto-grpc/tests exporter-otlp-proto-http: exporter/opentelemetry-exporter-otlp-proto-http/tests + exporter-prometheus: exporter/opentelemetry-exporter-prometheus/tests exporter-zipkin-combined: exporter/opentelemetry-exporter-zipkin/tests exporter-zipkin-proto-http: exporter/opentelemetry-exporter-zipkin-proto-http/tests exporter-zipkin-json: exporter/opentelemetry-exporter-zipkin-json/tests @@ -140,6 +144,8 @@ commands_pre = opentracing-shim: pip install {toxinidir}/opentelemetry-sdk opentracing-shim: pip install {toxinidir}/shim/opentelemetry-opentracing-shim + exporter-prometheus: pip install {toxinidir}/exporter/opentelemetry-exporter-prometheus + exporter-zipkin-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin-json exporter-zipkin-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin-proto-http exporter-zipkin-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin @@ -202,6 +208,7 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-prometheus[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin-json[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin-proto-http[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin[test] From 6fc9e4c89b06c7cb1f78bb231f9365507446fb6e Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 4 Mar 2022 19:07:23 -0500 Subject: [PATCH 1148/1517] Fix SDK observable instruments to emit SDK Measurement (#2501) --- .../opentelemetry/sdk/_metrics/instrument.py | 19 +- .../metrics/integration_test/test_cpu_time.py | 212 ++++++++++++------ .../tests/metrics/test_instrument.py | 169 +++++++++++--- 3 files changed, 300 insertions(+), 100 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index e3d0aa6331..990684b817 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -16,7 +16,7 @@ import logging from abc import ABC, abstractmethod -from typing import Dict, Generator, Iterable, Union +from typing import Callable, Dict, Generator, Iterable, Union from opentelemetry._metrics.instrument import CallbackT from opentelemetry._metrics.instrument import Counter as APICounter @@ -31,6 +31,7 @@ ObservableUpDownCounter as APIObservableUpDownCounter, ) from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter +from opentelemetry._metrics.measurement import Measurement as APIMeasurement from opentelemetry.sdk._metrics.aggregation import ( _Aggregation, _ExplicitBucketHistogramAggregation, @@ -86,7 +87,7 @@ def __init__( self._measurement_consumer = measurement_consumer super().__init__(name, callback, unit=unit, description=description) - self._callback = callback + self._callback: Callable[[], Iterable[APIMeasurement]] if isinstance(callback, Generator): @@ -94,10 +95,16 @@ def inner() -> Iterable[Measurement]: return next(callback) self._callback = inner - - @property - def callback(self) -> CallbackT: - return self._callback + else: + self._callback = callback + + def callback(self) -> Iterable[Measurement]: + for api_measurement in self._callback(): + yield Measurement( + api_measurement.value, + instrument=self, + attributes=api_measurement.attributes, + ) class Counter(_Synchronous, APICounter): diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py index d16dce0b01..0135faa772 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py @@ -14,11 +14,13 @@ # type: ignore import io -from typing import Generator, Iterable +from typing import Generator, Iterable, List from unittest import TestCase -from opentelemetry._metrics.measurement import Measurement +from opentelemetry._metrics.instrument import Instrument +from opentelemetry._metrics.measurement import Measurement as APIMeasurement from opentelemetry.sdk._metrics import MeterProvider +from opentelemetry.sdk._metrics.measurement import Measurement # FIXME Test that the instrument methods can be called concurrently safely. @@ -39,61 +41,137 @@ class TestCpuTimeIntegration(TestCase): procs_blocked 0 softirq 1644603067 0 166540056 208 309152755 8936439 0 1354908 935642970 13 222975718\n""" - measurements_expected = [ - Measurement(6150, {"cpu": "cpu0", "state": "user"}), - Measurement(3177, {"cpu": "cpu0", "state": "nice"}), - Measurement(5946, {"cpu": "cpu0", "state": "system"}), - Measurement(891264, {"cpu": "cpu0", "state": "idle"}), - Measurement(1296, {"cpu": "cpu0", "state": "iowait"}), - Measurement(0, {"cpu": "cpu0", "state": "irq"}), - Measurement(8343, {"cpu": "cpu0", "state": "softirq"}), - Measurement(421, {"cpu": "cpu0", "state": "guest"}), - Measurement(0, {"cpu": "cpu0", "state": "guest_nice"}), - Measurement(5882, {"cpu": "cpu1", "state": "user"}), - Measurement(3491, {"cpu": "cpu1", "state": "nice"}), - Measurement(6404, {"cpu": "cpu1", "state": "system"}), - Measurement(891564, {"cpu": "cpu1", "state": "idle"}), - Measurement(1244, {"cpu": "cpu1", "state": "iowait"}), - Measurement(0, {"cpu": "cpu1", "state": "irq"}), - Measurement(2410, {"cpu": "cpu1", "state": "softirq"}), - Measurement(418, {"cpu": "cpu1", "state": "guest"}), - Measurement(0, {"cpu": "cpu1", "state": "guest_nice"}), - ] + @staticmethod + def create_measurements_expected( + instrument: Instrument, + ) -> List[Measurement]: + return [ + Measurement( + 6150.29, + instrument=instrument, + attributes={"cpu": "cpu0", "state": "user"}, + ), + Measurement( + 3177.46, + instrument=instrument, + attributes={"cpu": "cpu0", "state": "nice"}, + ), + Measurement( + 5946.01, + instrument=instrument, + attributes={"cpu": "cpu0", "state": "system"}, + ), + Measurement( + 891264.59, + instrument=instrument, + attributes={"cpu": "cpu0", "state": "idle"}, + ), + Measurement( + 1296.29, + instrument=instrument, + attributes={"cpu": "cpu0", "state": "iowait"}, + ), + Measurement( + 0.0, + instrument=instrument, + attributes={"cpu": "cpu0", "state": "irq"}, + ), + Measurement( + 8343.46, + instrument=instrument, + attributes={"cpu": "cpu0", "state": "softirq"}, + ), + Measurement( + 421.37, + instrument=instrument, + attributes={"cpu": "cpu0", "state": "guest"}, + ), + Measurement( + 0, + instrument=instrument, + attributes={"cpu": "cpu0", "state": "guest_nice"}, + ), + Measurement( + 5882.32, + instrument=instrument, + attributes={"cpu": "cpu1", "state": "user"}, + ), + Measurement( + 3491.85, + instrument=instrument, + attributes={"cpu": "cpu1", "state": "nice"}, + ), + Measurement( + 6404.92, + instrument=instrument, + attributes={"cpu": "cpu1", "state": "system"}, + ), + Measurement( + 891564.11, + instrument=instrument, + attributes={"cpu": "cpu1", "state": "idle"}, + ), + Measurement( + 1244.85, + instrument=instrument, + attributes={"cpu": "cpu1", "state": "iowait"}, + ), + Measurement( + 0, + instrument=instrument, + attributes={"cpu": "cpu1", "state": "irq"}, + ), + Measurement( + 2410.04, + instrument=instrument, + attributes={"cpu": "cpu1", "state": "softirq"}, + ), + Measurement( + 418.62, + instrument=instrument, + attributes={"cpu": "cpu1", "state": "guest"}, + ), + Measurement( + 0, + instrument=instrument, + attributes={"cpu": "cpu1", "state": "guest_nice"}, + ), + ] def test_cpu_time_callback(self): - def cpu_time_callback() -> Iterable[Measurement]: + def cpu_time_callback() -> Iterable[APIMeasurement]: procstat = io.StringIO(self.procstat_str) procstat.readline() # skip the first line for line in procstat: if not line.startswith("cpu"): break cpu, *states = line.split() - yield Measurement( - int(states[0]) // 100, {"cpu": cpu, "state": "user"} + yield APIMeasurement( + int(states[0]) / 100, {"cpu": cpu, "state": "user"} ) - yield Measurement( - int(states[1]) // 100, {"cpu": cpu, "state": "nice"} + yield APIMeasurement( + int(states[1]) / 100, {"cpu": cpu, "state": "nice"} ) - yield Measurement( - int(states[2]) // 100, {"cpu": cpu, "state": "system"} + yield APIMeasurement( + int(states[2]) / 100, {"cpu": cpu, "state": "system"} ) - yield Measurement( - int(states[3]) // 100, {"cpu": cpu, "state": "idle"} + yield APIMeasurement( + int(states[3]) / 100, {"cpu": cpu, "state": "idle"} ) - yield Measurement( - int(states[4]) // 100, {"cpu": cpu, "state": "iowait"} + yield APIMeasurement( + int(states[4]) / 100, {"cpu": cpu, "state": "iowait"} ) - yield Measurement( - int(states[5]) // 100, {"cpu": cpu, "state": "irq"} + yield APIMeasurement( + int(states[5]) / 100, {"cpu": cpu, "state": "irq"} ) - yield Measurement( - int(states[6]) // 100, {"cpu": cpu, "state": "softirq"} + yield APIMeasurement( + int(states[6]) / 100, {"cpu": cpu, "state": "softirq"} ) - yield Measurement( - int(states[7]) // 100, {"cpu": cpu, "state": "guest"} + yield APIMeasurement( + int(states[7]) / 100, {"cpu": cpu, "state": "guest"} ) - yield Measurement( - int(states[8]) // 100, {"cpu": cpu, "state": "guest_nice"} + yield APIMeasurement( + int(states[8]) / 100, {"cpu": cpu, "state": "guest_nice"} ) meter = MeterProvider().get_meter("name") @@ -103,12 +181,14 @@ def cpu_time_callback() -> Iterable[Measurement]: unit="s", description="CPU time", ) - measurements = list(observable_counter._callback()) - self.assertEqual(measurements, self.measurements_expected) + measurements = list(observable_counter.callback()) + self.assertEqual( + measurements, self.create_measurements_expected(observable_counter) + ) def test_cpu_time_generator(self): def cpu_time_generator() -> Generator[ - Iterable[Measurement], None, None + Iterable[APIMeasurement], None, None ]: while True: measurements = [] @@ -119,55 +199,55 @@ def cpu_time_generator() -> Generator[ break cpu, *states = line.split() measurements.append( - Measurement( - int(states[0]) // 100, + APIMeasurement( + int(states[0]) / 100, {"cpu": cpu, "state": "user"}, ) ) measurements.append( - Measurement( - int(states[1]) // 100, + APIMeasurement( + int(states[1]) / 100, {"cpu": cpu, "state": "nice"}, ) ) measurements.append( - Measurement( - int(states[2]) // 100, + APIMeasurement( + int(states[2]) / 100, {"cpu": cpu, "state": "system"}, ) ) measurements.append( - Measurement( - int(states[3]) // 100, + APIMeasurement( + int(states[3]) / 100, {"cpu": cpu, "state": "idle"}, ) ) measurements.append( - Measurement( - int(states[4]) // 100, + APIMeasurement( + int(states[4]) / 100, {"cpu": cpu, "state": "iowait"}, ) ) measurements.append( - Measurement( - int(states[5]) // 100, {"cpu": cpu, "state": "irq"} + APIMeasurement( + int(states[5]) / 100, {"cpu": cpu, "state": "irq"} ) ) measurements.append( - Measurement( - int(states[6]) // 100, + APIMeasurement( + int(states[6]) / 100, {"cpu": cpu, "state": "softirq"}, ) ) measurements.append( - Measurement( - int(states[7]) // 100, + APIMeasurement( + int(states[7]) / 100, {"cpu": cpu, "state": "guest"}, ) ) measurements.append( - Measurement( - int(states[8]) // 100, + APIMeasurement( + int(states[8]) / 100, {"cpu": cpu, "state": "guest_nice"}, ) ) @@ -180,5 +260,9 @@ def cpu_time_generator() -> Generator[ unit="s", description="CPU time", ) - measurements = list(observable_counter._callback()) - self.assertEqual(measurements, self.measurements_expected) + measurements = list(observable_counter.callback()) + self.assertEqual( + measurements, self.create_measurements_expected(observable_counter) + ) + + maxDiff = None diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py index 5504fdb686..8e0b3b2a6b 100644 --- a/opentelemetry-sdk/tests/metrics/test_instrument.py +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -15,6 +15,7 @@ from unittest import TestCase from unittest.mock import Mock +from opentelemetry._metrics.measurement import Measurement as APIMeasurement from opentelemetry.sdk._metrics.instrument import ( Counter, Histogram, @@ -23,6 +24,7 @@ ObservableUpDownCounter, UpDownCounter, ) +from opentelemetry.sdk._metrics.measurement import Measurement class TestCounter(TestCase): @@ -53,66 +55,173 @@ def test_add_non_monotonic(self): mc.consume_measurement.assert_called_once() +TEST_ATTRIBUTES = {"foo": "bar"} + + +def callable_callback(): + return [ + APIMeasurement(1, attributes=TEST_ATTRIBUTES), + APIMeasurement(2, attributes=TEST_ATTRIBUTES), + APIMeasurement(3, attributes=TEST_ATTRIBUTES), + ] + + +def generator_callback(): + yield [ + APIMeasurement(1, attributes=TEST_ATTRIBUTES), + APIMeasurement(2, attributes=TEST_ATTRIBUTES), + APIMeasurement(3, attributes=TEST_ATTRIBUTES), + ] + + class TestObservableGauge(TestCase): def test_callable_callback(self): - def callback(): - return [1, 2, 3] - - observable_gauge = ObservableGauge("name", Mock(), Mock(), callback) + observable_gauge = ObservableGauge( + "name", Mock(), Mock(), callable_callback + ) - self.assertEqual(observable_gauge.callback(), [1, 2, 3]) + self.assertEqual( + list(observable_gauge.callback()), + [ + Measurement( + 1, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + Measurement( + 2, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + Measurement( + 3, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + ], + ) def test_generator_callback(self): - def callback(): - yield [1, 2, 3] - - observable_gauge = ObservableGauge("name", Mock(), Mock(), callback()) + observable_gauge = ObservableGauge( + "name", Mock(), Mock(), generator_callback() + ) - self.assertEqual(observable_gauge.callback(), [1, 2, 3]) + self.assertEqual( + list(observable_gauge.callback()), + [ + Measurement( + 1, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + Measurement( + 2, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + Measurement( + 3, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + ], + ) class TestObservableCounter(TestCase): def test_callable_callback(self): - def callback(): - return [1, 2, 3] - observable_counter = ObservableCounter( - "name", Mock(), Mock(), callback + "name", Mock(), Mock(), callable_callback ) - self.assertEqual(observable_counter.callback(), [1, 2, 3]) + self.assertEqual( + list(observable_counter.callback()), + [ + Measurement( + 1, + instrument=observable_counter, + attributes=TEST_ATTRIBUTES, + ), + Measurement( + 2, + instrument=observable_counter, + attributes=TEST_ATTRIBUTES, + ), + Measurement( + 3, + instrument=observable_counter, + attributes=TEST_ATTRIBUTES, + ), + ], + ) def test_generator_callback(self): - def callback(): - yield [1, 2, 3] - observable_counter = ObservableCounter( - "name", Mock(), Mock(), callback() + "name", Mock(), Mock(), generator_callback() ) - self.assertEqual(observable_counter.callback(), [1, 2, 3]) + self.assertEqual( + list(observable_counter.callback()), + [ + Measurement( + 1, + instrument=observable_counter, + attributes=TEST_ATTRIBUTES, + ), + Measurement( + 2, + instrument=observable_counter, + attributes=TEST_ATTRIBUTES, + ), + Measurement( + 3, + instrument=observable_counter, + attributes=TEST_ATTRIBUTES, + ), + ], + ) class TestObservableUpDownCounter(TestCase): def test_callable_callback(self): - def callback(): - return [1, 2, 3] - observable_up_down_counter = ObservableUpDownCounter( - "name", Mock(), Mock(), callback + "name", Mock(), Mock(), callable_callback ) - self.assertEqual(observable_up_down_counter.callback(), [1, 2, 3]) + self.assertEqual( + list(observable_up_down_counter.callback()), + [ + Measurement( + 1, + instrument=observable_up_down_counter, + attributes=TEST_ATTRIBUTES, + ), + Measurement( + 2, + instrument=observable_up_down_counter, + attributes=TEST_ATTRIBUTES, + ), + Measurement( + 3, + instrument=observable_up_down_counter, + attributes=TEST_ATTRIBUTES, + ), + ], + ) def test_generator_callback(self): - def callback(): - yield [1, 2, 3] - observable_up_down_counter = ObservableUpDownCounter( - "name", Mock(), Mock(), callback() + "name", Mock(), Mock(), generator_callback() ) - self.assertEqual(observable_up_down_counter.callback(), [1, 2, 3]) + self.assertEqual( + list(observable_up_down_counter.callback()), + [ + Measurement( + 1, + instrument=observable_up_down_counter, + attributes=TEST_ATTRIBUTES, + ), + Measurement( + 2, + instrument=observable_up_down_counter, + attributes=TEST_ATTRIBUTES, + ), + Measurement( + 3, + instrument=observable_up_down_counter, + attributes=TEST_ATTRIBUTES, + ), + ], + ) class TestHistogram(TestCase): From 5e0e2de49a05a760d85c4b21c8714e5cdd13b4f5 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 7 Mar 2022 12:31:35 -0500 Subject: [PATCH 1149/1517] Fix ViewInstrumentMatch to create new aggregation for each attribute set (#2503) --- .../sdk/_metrics/_view_instrument_match.py | 63 +++++----- .../opentelemetry/sdk/_metrics/measurement.py | 8 +- .../sdk/_metrics/metric_reader_storage.py | 35 ++---- .../src/opentelemetry/sdk/_metrics/view.py | 4 - .../metrics/test_metric_reader_storage.py | 35 ------ .../metrics/test_view_instrument_match.py | 108 +++++++++++------- 6 files changed, 116 insertions(+), 137 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py index fbdea22a5b..c12aed3fcb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py @@ -15,16 +15,18 @@ from logging import getLogger from threading import Lock -from typing import Iterable, Set +from typing import TYPE_CHECKING, Iterable from opentelemetry.sdk._metrics.aggregation import ( - _Aggregation, _convert_aggregation_temporality, ) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration +from opentelemetry.sdk._metrics.view import View + +if TYPE_CHECKING: + from opentelemetry.sdk._metrics.instrument import _Instrument _logger = getLogger(__name__) @@ -32,33 +34,26 @@ class _ViewInstrumentMatch: def __init__( self, - name: str, - unit: str, - description: str, - aggregation: _Aggregation, - instrumentation_info: InstrumentationInfo, - resource: Resource, - attribute_keys: Set[str], + view: View, + instrument: "_Instrument", + sdk_config: SdkConfiguration, ): - self._name = name - self._unit = unit - self._description = description - self._aggregation = aggregation - self._instrumentation_info = instrumentation_info - self._resource = resource - self._attribute_keys = attribute_keys + self._view = view + self._instrument = instrument + self._sdk_config = sdk_config self._attributes_aggregation = {} self._attributes_previous_point = {} self._lock = Lock() + # pylint: disable=protected-access def consume_measurement(self, measurement: Measurement) -> None: - if self._attribute_keys: + if self._view._attribute_keys is not None: attributes = {} - for key, value in measurement.attributes.items(): - if key in self._attribute_keys: + for key, value in (measurement.attributes or {}).items(): + if key in self._view._attribute_keys: attributes[key] = value elif measurement.attributes is not None: attributes = measurement.attributes @@ -67,9 +62,18 @@ def consume_measurement(self, measurement: Measurement) -> None: attributes = frozenset(attributes.items()) - if attributes not in self._attributes_aggregation.keys(): + if attributes not in self._attributes_aggregation: with self._lock: - self._attributes_aggregation[attributes] = self._aggregation + if attributes not in self._attributes_aggregation: + if self._view._aggregation: + aggregation = ( + self._view._aggregation._create_aggregation( + self._instrument + ) + ) + else: + aggregation = self._instrument._default_aggregation + self._attributes_aggregation[attributes] = aggregation self._attributes_aggregation[attributes].aggregate(measurement) @@ -100,11 +104,14 @@ def collect(self, temporality: int) -> Iterable[Metric]: yield Metric( attributes=dict(attributes), - description=self._description, - instrumentation_info=self._instrumentation_info, - name=self._name, - resource=self._resource, - unit=self._unit, + description=( + self._view._description + or self._instrument.description + ), + instrumentation_info=self._instrument.instrumentation_info, + name=self._view._name or self._instrument.name, + resource=self._sdk_config.resource, + unit=self._instrument.unit, point=_convert_aggregation_temporality( previous_point, current_point, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py index 6026787d82..7ed6d13c1f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py @@ -13,14 +13,16 @@ # limitations under the License. from dataclasses import dataclass -from typing import Union +from typing import TYPE_CHECKING, Union -from opentelemetry._metrics.instrument import Instrument from opentelemetry.util.types import Attributes +if TYPE_CHECKING: + from opentelemetry.sdk._metrics.instrument import _Instrument + @dataclass(frozen=True) class Measurement: value: Union[int, float] - instrument: Instrument + instrument: "_Instrument" attributes: Attributes = None diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py index a02ec07f88..f7a2410024 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py @@ -25,6 +25,8 @@ from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration from opentelemetry.sdk._metrics.view import View +_DEFAULT_VIEW = View(instrument_name="") + class MetricReaderStorage: """The SDK's storage for a given reader""" @@ -55,27 +57,11 @@ def _get_or_init_view_instrument_match( for view in self._sdk_config.views: # pylint: disable=protected-access if view._match(instrument): - - if view._aggregation is not None: - aggregation = view._aggregation._create_aggregation( - instrument - ) - else: - aggregation = instrument._default_aggregation - view_instrument_matches.append( _ViewInstrumentMatch( - name=view._name or instrument.name, - unit=instrument.unit, - description=( - view._description or instrument.description - ), - aggregation=aggregation, - instrumentation_info=( - instrument.instrumentation_info - ), - resource=self._sdk_config.resource, - attribute_keys=view._attribute_keys, + view=view, + instrument=instrument, + sdk_config=self._sdk_config, ) ) @@ -86,14 +72,9 @@ def _get_or_init_view_instrument_match( ): view_instrument_matches.append( _ViewInstrumentMatch( - name=instrument.name, - unit=instrument.unit, - description=instrument.description, - # pylint: disable=protected-access - aggregation=instrument._default_aggregation, - instrumentation_info=instrument.instrumentation_info, - resource=self._sdk_config.resource, - attribute_keys=set(), + view=_DEFAULT_VIEW, + instrument=instrument, + sdk_config=self._sdk_config, ) ) self._view_instrument_match[instrument] = view_instrument_matches diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py index b4b2797111..a59f734904 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py @@ -127,10 +127,6 @@ class the instrument must be to match the view. self._description = description self._attribute_keys = attribute_keys - - if self._attribute_keys is None: - self._attribute_keys = set() - self._aggregation = aggregation # pylint: disable=too-many-return-statements diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index 0b338b5013..936aa0433f 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -14,15 +14,12 @@ from unittest.mock import Mock, patch -from opentelemetry.sdk._metrics.aggregation import SumAggregation -from opentelemetry.sdk._metrics.instrument import Counter from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.metric_reader_storage import ( MetricReaderStorage, ) from opentelemetry.sdk._metrics.point import AggregationTemporality from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration -from opentelemetry.sdk._metrics.view import View from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc @@ -162,38 +159,6 @@ def send_measurement(): # _ViewInstrumentMatch constructor should have only been called once self.assertEqual(mock_view_instrument_match_ctor.call_count, 1) - def test_aggregations(self): - - view = View(instrument_name="counter*", aggregation=SumAggregation()) - - metric_reader_storage = MetricReaderStorage( - SdkConfiguration( - resource=Mock(), - metric_readers=(), - views=(view,), - enable_default_view=True, - ) - ) - - counter_0 = Counter("counter_0", Mock(), Mock()) - counter_1 = Counter("counter_1", Mock(), Mock()) - - metric_reader_storage.consume_measurement(Measurement(1, counter_0)) - metric_reader_storage.consume_measurement(Measurement(2, counter_1)) - - self.assertEqual( - 1, - metric_reader_storage._view_instrument_match[counter_0][ - 0 - ]._aggregation._value, - ) - self.assertEqual( - 2, - metric_reader_storage._view_instrument_match[counter_1][ - 0 - ]._aggregation._value, - ) - @patch( "opentelemetry.sdk._metrics.metric_reader_storage._ViewInstrumentMatch" ) diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index fc2c0e7514..c366309b41 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -19,31 +19,42 @@ _ViewInstrumentMatch, ) from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import Metric +from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric +from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration +from opentelemetry.sdk._metrics.view import View class Test_ViewInstrumentMatch(TestCase): @classmethod def setUpClass(cls): - cls.mock_aggregation_instance = Mock() + cls.mock_aggregation_factory = Mock() + cls.mock_created_aggregation = ( + cls.mock_aggregation_factory._create_aggregation() + ) cls.mock_resource = Mock() cls.mock_instrumentation_info = Mock() def test_consume_measurement(self): - + instrument1 = Mock(name="instrument1") + instrument1.instrumentation_info = self.mock_instrumentation_info + sdk_config = SdkConfiguration( + resource=self.mock_resource, + metric_readers=[], + views=[], + enable_default_view=True, + ) view_instrument_match = _ViewInstrumentMatch( - "name", - "unit", - "description", - self.mock_aggregation_instance, - self.mock_instrumentation_info, - self.mock_resource, - {"a", "c"}, + view=View( + instrument_name="instrument1", + name="name", + aggregation=self.mock_aggregation_factory, + attribute_keys={"a", "c"}, + ), + instrument=instrument1, + sdk_config=sdk_config, ) - instrument1 = Mock(name="instrument1") - view_instrument_match.consume_measurement( Measurement( value=0, @@ -53,7 +64,7 @@ def test_consume_measurement(self): ) self.assertEqual( view_instrument_match._attributes_aggregation, - {frozenset([("c", "d")]): self.mock_aggregation_instance}, + {frozenset([("c", "d")]): self.mock_created_aggregation}, ) view_instrument_match.consume_measurement( @@ -67,19 +78,20 @@ def test_consume_measurement(self): self.assertEqual( view_instrument_match._attributes_aggregation, { - frozenset(): self.mock_aggregation_instance, - frozenset([("c", "d")]): self.mock_aggregation_instance, + frozenset(): self.mock_created_aggregation, + frozenset([("c", "d")]): self.mock_created_aggregation, }, ) + # None attribute_keys (default) will keep all attributes view_instrument_match = _ViewInstrumentMatch( - "name", - "unit", - "description", - self.mock_aggregation_instance, - self.mock_instrumentation_info, - self.mock_resource, - set(), + view=View( + instrument_name="instrument1", + name="name", + aggregation=self.mock_aggregation_factory, + ), + instrument=instrument1, + sdk_config=sdk_config, ) view_instrument_match.consume_measurement( @@ -94,37 +106,49 @@ def test_consume_measurement(self): { frozenset( [("c", "d"), ("f", "g")] - ): self.mock_aggregation_instance + ): self.mock_created_aggregation }, ) + # empty set attribute_keys will drop all labels and aggregate everything together view_instrument_match = _ViewInstrumentMatch( - "name", - "unit", - "description", - self.mock_aggregation_instance, - self.mock_instrumentation_info, - self.mock_resource, - set(), + view=View( + instrument_name="instrument1", + name="name", + aggregation=self.mock_aggregation_factory, + attribute_keys={}, + ), + instrument=instrument1, + sdk_config=sdk_config, ) view_instrument_match.consume_measurement( Measurement(value=0, instrument=instrument1, attributes=None) ) self.assertEqual( view_instrument_match._attributes_aggregation, - {frozenset({}): self.mock_aggregation_instance}, + {frozenset({}): self.mock_created_aggregation}, ) def test_collect(self): - + instrument1 = Mock( + name="instrument1", description="description", unit="unit" + ) + instrument1.instrumentation_info = self.mock_instrumentation_info + sdk_config = SdkConfiguration( + resource=self.mock_resource, + metric_readers=[], + views=[], + enable_default_view=True, + ) view_instrument_match = _ViewInstrumentMatch( - "name", - "unit", - "description", - self.mock_aggregation_instance, - self.mock_instrumentation_info, - self.mock_resource, - {"a", "c"}, + view=View( + instrument_name="instrument1", + name="name", + aggregation=self.mock_aggregation_factory, + attribute_keys={"a", "c"}, + ), + instrument=instrument1, + sdk_config=sdk_config, ) view_instrument_match.consume_measurement( @@ -135,7 +159,11 @@ def test_collect(self): ) ) self.assertEqual( - next(view_instrument_match.collect(1)), + next( + view_instrument_match.collect( + AggregationTemporality.CUMULATIVE + ) + ), Metric( attributes={"c": "d"}, description="description", From cdd62d744e1a5ce00035c16fad458a8be05a9b19 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 7 Mar 2022 09:54:04 -0800 Subject: [PATCH 1150/1517] [docs] add version to get_meter (#2499) --- docs/examples/metrics/example.py | 2 +- docs/getting_started/metrics_example.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/metrics/example.py b/docs/examples/metrics/example.py index 7242d976d9..30def5b7aa 100644 --- a/docs/examples/metrics/example.py +++ b/docs/examples/metrics/example.py @@ -10,7 +10,7 @@ provider = MeterProvider(metric_readers=[reader]) set_meter_provider(provider) -meter = get_meter_provider().get_meter("getting-started") +meter = get_meter_provider().get_meter("getting-started", "0.1.2") counter = meter.create_counter("first_counter") counter.add(1) # TODO: fill in details for additional metrics diff --git a/docs/getting_started/metrics_example.py b/docs/getting_started/metrics_example.py index 140dc0004b..92ab8e33e7 100644 --- a/docs/getting_started/metrics_example.py +++ b/docs/getting_started/metrics_example.py @@ -27,7 +27,7 @@ provider = MeterProvider(metric_readers=[reader]) set_meter_provider(provider) -meter = get_meter_provider().get_meter("getting-started") +meter = get_meter_provider().get_meter("getting-started", "0.1.2") counter = meter.create_counter("first_counter") counter.add(1) # TODO: fill in details for additional metrics From 061e1290fee901814fb5f4358160026bae6c44a5 Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Mon, 7 Mar 2022 14:17:56 -0500 Subject: [PATCH 1151/1517] [docs] Remove / adjusted auto-instr and distro pages since they've moved (#2498) --- CHANGELOG.md | 5 +- docs/examples/auto-instrumentation/README.rst | 232 +----------------- docs/examples/distro/README.rst | 104 -------- 3 files changed, 6 insertions(+), 335 deletions(-) delete mode 100644 docs/examples/distro/README.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index fc12fea820..e3db0c198c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 moving](https://github.com/open-telemetry/opentelemetry-python/issues/2172) to [opentelemetry.io](https://opentelemetry.io). For details, including a list of pages that have moved, see - [#2453](https://github.com/open-telemetry/opentelemetry-python/pull/2453). + [#2453](https://github.com/open-telemetry/opentelemetry-python/pull/2453), and + [#2498](https://github.com/open-telemetry/opentelemetry-python/pull/2498). - `opentelemetry-exporter-otlp-grpc` update SDK dependency to ~1.9. ([#2442](https://github.com/open-telemetry/opentelemetry-python/pull/2442)) - bugfix(auto-instrumentation): attach OTLPHandler to root logger @@ -20,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2461](https://github.com/open-telemetry/opentelemetry-python/pull/2461)) - fix exception handling in get_aggregated_resources ([#2464](https://github.com/open-telemetry/opentelemetry-python/pull/2464)) -- Fix `OTEL_EXPORTER_OTLP_ENDPOINT` usage in OTLP HTTP trace exporter +- Fix `OTEL_EXPORTER_OTLP_ENDPOINT` usage in OTLP HTTP trace exporter ([#2493](https://github.com/open-telemetry/opentelemetry-python/pull/2493)) - [exporter/opentelemetry-exporter-prometheus] restore package using the new metrics API ([#2321](https://github.com/open-telemetry/opentelemetry-python/pull/2321)) diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index 4fdb896482..3fe9366fb2 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -1,233 +1,7 @@ Auto-instrumentation ==================== -One of the best ways to instrument Python applications is to use OpenTelemetry automatic instrumentation (auto-instrumentation). This approach is simple, easy, and doesn't require many code changes. You only need to install a few Python packages to successfully instrument your application's code. +To learn about automatic instrumentation and how to run the example in this +directory, see `Automatic Instrumentation`_. -Overview --------- - -This example demonstrates how to use auto-instrumentation in OpenTelemetry. -The example is based on a previous OpenTracing example that -you can find -`here `__. - -The source files for these examples are available `here `__. - -This example uses two different scripts. The main difference between them is -whether or not they're instrumented manually: - -1. ``server_instrumented.py`` - instrumented manually -2. ``server_uninstrumented.py`` - not instrumented manually - -Run the first script without the automatic instrumentation agent and -the second with the agent. They should both produce the same results, -demonstrating that the automatic instrumentation agent does -exactly the same thing as manual instrumentation. - -To better understand auto-instrumentation, see the relevant part of both scripts: - -Manually instrumented server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``server_instrumented.py`` - -.. code:: python - - @app.route("/server_request") - def server_request(): - with tracer.start_as_current_span( - "server_request", - context=extract(request.headers), - kind=trace.SpanKind.SERVER, - attributes=collect_request_attributes(request.environ), - ): - print(request.args.get("param")) - return "served" - -Server not instrumented manually -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``server_uninstrumented.py`` - -.. code:: python - - @app.route("/server_request") - def server_request(): - print(request.args.get("param")) - return "served" - -Prepare -------- - -Execute the following example in a separate virtual environment. -Run the following commands to prepare for auto-instrumentation: - -.. code:: sh - - $ mkdir auto_instrumentation - $ virtualenv auto_instrumentation - $ source auto_instrumentation/bin/activate - -Install -------- - -Run the following commands to install the appropriate packages. The -``opentelemetry-distro`` package depends on a few others, like ``opentelemetry-sdk`` -for custom instrumentation of your own code and ``opentelemetry-instrumentation`` which -provides several commands that help automatically instrument a program. - -.. code:: sh - - $ pip install opentelemetry-distro - $ pip install opentelemetry-instrumentation-flask - $ pip install flask - $ pip install requests - -The examples that follow send instrumentation results to the console. Learn more -about installing and configuring the `OpenTelemetry Distro <../distro>`_ to send -telemetry to other destinations, like an OpenTelemetry Collector. - -Execute ---------- - -This section guides you through the manual process of instrumenting -a server as well as the process of executing an automatically -instrumented server. - -Execute a manually instrumented server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Execute the server in two separate consoles, one to run each of the -scripts that make up this example: - -.. code:: sh - - $ source auto_instrumentation/bin/activate - $ python server_instrumented.py - -.. code:: sh - - $ source auto_instrumentation/bin/activate - $ python client.py testing - -The console running ``server_instrumented.py`` will display the spans generated by instrumentation as JSON. -The spans should appear similar to the following example: - -.. code:: sh - - { - "name": "server_request", - "context": { - "trace_id": "0xfa002aad260b5f7110db674a9ddfcd23", - "span_id": "0x8b8bbaf3ca9c5131", - "trace_state": "{}" - }, - "kind": "SpanKind.SERVER", - "parent_id": null, - "start_time": "2020-04-30T17:28:57.886397Z", - "end_time": "2020-04-30T17:28:57.886490Z", - "status": { - "status_code": "OK" - }, - "attributes": { - "http.method": "GET", - "http.server_name": "127.0.0.1", - "http.scheme": "http", - "host.port": 8082, - "http.host": "localhost:8082", - "http.target": "/server_request?param=testing", - "net.peer.ip": "127.0.0.1", - "net.peer.port": 52872, - "http.flavor": "1.1" - }, - "events": [], - "links": [], - "resource": { - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": "0.16b1" - } - } - -Execute an automatically instrumented server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Stop the execution of ``server_instrumented.py`` with ``ctrl + c`` -and run the following command instead: - -.. code:: sh - - $ opentelemetry-instrument --traces_exporter console python server_uninstrumented.py - -In the console where you previously executed ``client.py``, run the following -command again: - -.. code:: sh - - $ python client.py testing - -The console running ``server_uninstrumented.py`` will display the spans generated by instrumentation as JSON. -The spans should appear similar to the following example: - -.. code:: sh - - { - "name": "server_request", - "context": { - "trace_id": "0x9f528e0b76189f539d9c21b1a7a2fc24", - "span_id": "0xd79760685cd4c269", - "trace_state": "{}" - }, - "kind": "SpanKind.SERVER", - "parent_id": "0xb4fb7eee22ef78e4", - "start_time": "2020-04-30T17:10:02.400604Z", - "end_time": "2020-04-30T17:10:02.401858Z", - "status": { - "status_code": "OK" - }, - "attributes": { - "http.method": "GET", - "http.server_name": "127.0.0.1", - "http.scheme": "http", - "host.port": 8082, - "http.host": "localhost:8082", - "http.target": "/server_request?param=testing", - "net.peer.ip": "127.0.0.1", - "net.peer.port": 48240, - "http.flavor": "1.1", - "http.route": "/server_request", - "http.status_text": "OK", - "http.status_code": 200 - }, - "events": [], - "links": [], - "resource": { - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": "0.16b1", - "service.name": "" - } - } - -You can see that both outputs are the same because automatic instrumentation does -exactly what manual instrumentation does. - -Instrumentation while debugging -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The debug mode can be enabled in the Flask app like this: - - -.. code:: python - - if __name__ == "__main__": - app.run(port=8082, debug=True) - -The debug mode can break instrumentation from happening because it enables a -reloader. To run instrumentation while the debug mode is enabled, set the -``use_reloader`` option to ``False``: - -.. code:: python - - if __name__ == "__main__": - app.run(port=8082, debug=True, use_reloader=False) +.. _Automatic Instrumentation: https://opentelemetry.io/docs/instrumentation/python/automatic/ diff --git a/docs/examples/distro/README.rst b/docs/examples/distro/README.rst deleted file mode 100644 index f58680609a..0000000000 --- a/docs/examples/distro/README.rst +++ /dev/null @@ -1,104 +0,0 @@ -OpenTelemetry Distro -==================== - -In order to make using OpenTelemetry and auto-instrumentation as quick as possible without sacrificing flexibility, -OpenTelemetry distros provide a mechanism to automatically configure some of the more common options for users. By -harnessing their power, users of OpenTelemetry can configure the components as they need. The ``opentelemetry-distro`` -package provides some defaults to users looking to get started, it configures: - -- the SDK TracerProvider -- a BatchSpanProcessor -- the OTLP ``SpanExporter`` to send data to an OpenTelemetry collector - -The package also provides a starting point for anyone interested in producing an alternative distro. The -interfaces implemented by the package are loaded by the auto-instrumentation via the ``opentelemetry_distro`` -and ``opentelemetry_configurator`` entry points to configure the application before any other code is -executed. - -In order to automatically export data from OpenTelemetry to the OpenTelemetry collector, installing the -package will setup all the required entry points. - -.. code:: sh - - $ pip install opentelemetry-distro[otlp] opentelemetry-instrumentation - -Start the Collector locally to see data being exported. Write the following file: - -.. code-block:: yaml - - # /tmp/otel-collector-config.yaml - receivers: - otlp: - protocols: - grpc: - http: - exporters: - logging: - loglevel: debug - processors: - batch: - service: - pipelines: - traces: - receivers: [otlp] - exporters: [logging] - processors: [batch] - -Then start the Docker container: - -.. code-block:: sh - - docker run -p 4317:4317 \ - -v /tmp/otel-collector-config.yaml:/etc/otel-collector-config.yaml \ - otel/opentelemetry-collector:latest \ - --config=/etc/otel-collector-config.yaml - -The following code will create a span with no configuration. - -.. code:: python - - # no_configuration.py - from opentelemetry import trace - - with trace.get_tracer(__name__).start_as_current_span("foo"): - with trace.get_tracer(__name__).start_as_current_span("bar"): - print("baz") - -Lastly, run the ``no_configuration.py`` with the auto-instrumentation: - -.. code-block:: sh - - $ opentelemetry-instrument python no_configuration.py - -The resulting span will appear in the output from the collector and look similar to this: - -.. code-block:: sh - - Resource labels: - -> telemetry.sdk.language: STRING(python) - -> telemetry.sdk.name: STRING(opentelemetry) - -> telemetry.sdk.version: STRING(1.1.0) - -> service.name: STRING(unknown_service) - InstrumentationLibrarySpans #0 - InstrumentationLibrary __main__ - Span #0 - Trace ID : db3c99e5bfc50ef8be1773c3765e8845 - Parent ID : 0677126a4d110cb8 - ID : 3163b3022808ed1b - Name : bar - Kind : SPAN_KIND_INTERNAL - Start time : 2021-05-06 22:54:51.23063 +0000 UTC - End time : 2021-05-06 22:54:51.230684 +0000 UTC - Status code : STATUS_CODE_UNSET - Status message : - Span #1 - Trace ID : db3c99e5bfc50ef8be1773c3765e8845 - Parent ID : - ID : 0677126a4d110cb8 - Name : foo - Kind : SPAN_KIND_INTERNAL - Start time : 2021-05-06 22:54:51.230549 +0000 UTC - End time : 2021-05-06 22:54:51.230706 +0000 UTC - Status code : STATUS_CODE_UNSET - Status message : - From 9277351d652b20886df7089f69cca21f5e9bf487 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Tue, 8 Mar 2022 15:45:51 -0500 Subject: [PATCH 1152/1517] Fix prometheus histogram "+Inf" bucket (#2508) --- .../exporter/prometheus/__init__.py | 16 +++++++------- .../tests/test_prometheus_exporter.py | 21 +++++++++++++++---- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index a952e56bcf..62967eec83 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -65,6 +65,7 @@ import collections import logging import re +from itertools import chain from typing import Iterable, Optional, Sequence, Tuple from prometheus_client import core @@ -78,14 +79,13 @@ def _convert_buckets(metric: Metric) -> Sequence[Tuple[str, int]]: buckets = [] total_count = 0 - for index, value in enumerate(metric.point.bucket_counts): - total_count += value - buckets.append( - ( - f"{metric.point.explicit_bounds[index]}", - total_count, - ) - ) + for upper_bound, count in zip( + chain(metric.point.explicit_bounds, ["+Inf"]), + metric.point.bucket_counts, + ): + total_count += count + buckets.append((f"{upper_bound}", total_count)) + return buckets diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index af4bc7c0fd..092ed54481 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest +from textwrap import dedent from unittest import mock from prometheus_client import generate_latest @@ -61,10 +62,10 @@ def test_histogram_to_prometheus(self): Histogram( time_unix_nano=1641946016139533244, start_time_unix_nano=1641946016139533244, - bucket_counts=[1, 1], + bucket_counts=[1, 3, 2], sum=579.0, explicit_bounds=[123.0, 456.0], - aggregation_temporality=AggregationTemporality.DELTA, + aggregation_temporality=AggregationTemporality.CUMULATIVE, ), attributes={"histo": 1}, ) @@ -73,8 +74,20 @@ def test_histogram_to_prometheus(self): collector.add_metrics_data([record]) result_bytes = generate_latest(collector) result = result_bytes.decode("utf-8") - self.assertIn('testprefix_test_name_s_sum{histo="1"} 579.0', result) - self.assertIn('testprefix_test_name_s_count{histo="1"} 2.0', result) + self.assertEqual( + result, + dedent( + """\ + # HELP testprefix_test_name_s foo + # TYPE testprefix_test_name_s histogram + testprefix_test_name_s_bucket{histo="1",le="123.0"} 1.0 + testprefix_test_name_s_bucket{histo="1",le="456.0"} 4.0 + testprefix_test_name_s_bucket{histo="1",le="+Inf"} 6.0 + testprefix_test_name_s_count{histo="1"} 6.0 + testprefix_test_name_s_sum{histo="1"} 579.0 + """ + ), + ) def test_sum_to_prometheus(self): labels = {"environment@": "staging", "os": "Windows"} From 4f8de5e96eddabaff85cd6bafed938ab4ba16848 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Wed, 9 Mar 2022 21:32:56 +0530 Subject: [PATCH 1153/1517] Moving owais to approvers (#2515) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fd2d6334e9..b31231e9df 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Aaron Abbott](https://github.com/aabmass), Google - [Alex Boten](https://github.com/codeboten), Lightstep - [Srikanth Chekuri](https://github.com/lonewolf3739) +- [Owais Lone](https://github.com/owais), Splunk - [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), AWS *For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver).* @@ -146,7 +147,6 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t - [Diego Hurtado](https://github.com/ocelotl), Lightstep - [Leighton Chen](https://github.com/lzchen), Microsoft -- [Owais Lone](https://github.com/owais), Splunk *For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer).* From 534772760f9c2b9307bb1392b5b4a21f9189e0ed Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 9 Mar 2022 09:47:07 -0800 Subject: [PATCH 1154/1517] [docs] updating metrics examples (#2507) * [docs] updating metrics examples Adding different instruments to the example code. * use correct Measurement Co-authored-by: Diego Hurtado --- docs/examples/metrics/example.py | 43 +++++++++++++++++++++++-- docs/getting_started/metrics_example.py | 43 +++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/docs/examples/metrics/example.py b/docs/examples/metrics/example.py index 30def5b7aa..60165c6755 100644 --- a/docs/examples/metrics/example.py +++ b/docs/examples/metrics/example.py @@ -1,4 +1,7 @@ +from typing import Iterable + from opentelemetry._metrics import get_meter_provider, set_meter_provider +from opentelemetry._metrics.measurement import Measurement from opentelemetry.exporter.otlp.proto.grpc._metric_exporter import ( OTLPMetricExporter, ) @@ -10,7 +13,43 @@ provider = MeterProvider(metric_readers=[reader]) set_meter_provider(provider) + +def observable_counter_func() -> Iterable[Measurement]: + yield Measurement(1, {}) + + +def observable_up_down_counter_func() -> Iterable[Measurement]: + yield Measurement(-10, {}) + + +def observable_gauge_func() -> Iterable[Measurement]: + yield Measurement(9, {}) + + meter = get_meter_provider().get_meter("getting-started", "0.1.2") -counter = meter.create_counter("first_counter") + +# Counter +counter = meter.create_counter("counter") counter.add(1) -# TODO: fill in details for additional metrics + +# Async Counter +observable_counter = meter.create_observable_counter( + "observable_counter", observable_counter_func +) + +# UpDownCounter +updown_counter = meter.create_up_down_counter("updown_counter") +updown_counter.add(1) +updown_counter.add(-5) + +# Async UpDownCounter +observable_updown_counter = meter.create_observable_up_down_counter( + "observable_updown_counter", observable_up_down_counter_func +) + +# Histogram +histogram = meter.create_histogram("histogram") +histogram.record(99.9) + +# Async Gauge +gauge = meter.create_observable_gauge("gauge", observable_gauge_func) diff --git a/docs/getting_started/metrics_example.py b/docs/getting_started/metrics_example.py index 92ab8e33e7..c763e3855c 100644 --- a/docs/getting_started/metrics_example.py +++ b/docs/getting_started/metrics_example.py @@ -15,7 +15,10 @@ # metrics.py # This is still work in progress as the metrics SDK is being implemented +from typing import Iterable + from opentelemetry._metrics import get_meter_provider, set_meter_provider +from opentelemetry._metrics.measurement import Measurement from opentelemetry.sdk._metrics import MeterProvider from opentelemetry.sdk._metrics.export import ( ConsoleMetricExporter, @@ -27,7 +30,43 @@ provider = MeterProvider(metric_readers=[reader]) set_meter_provider(provider) + +def observable_counter_func() -> Iterable[Measurement]: + yield Measurement(1, {}) + + +def observable_up_down_counter_func() -> Iterable[Measurement]: + yield Measurement(-10, {}) + + +def observable_gauge_func() -> Iterable[Measurement]: + yield Measurement(9, {}) + + meter = get_meter_provider().get_meter("getting-started", "0.1.2") -counter = meter.create_counter("first_counter") + +# Counter +counter = meter.create_counter("counter") counter.add(1) -# TODO: fill in details for additional metrics + +# Async Counter +observable_counter = meter.create_observable_counter( + "observable_counter", observable_counter_func +) + +# UpDownCounter +updown_counter = meter.create_up_down_counter("updown_counter") +updown_counter.add(1) +updown_counter.add(-5) + +# Async UpDownCounter +observable_updown_counter = meter.create_observable_up_down_counter( + "observable_updown_counter", observable_up_down_counter_func +) + +# Histogram +histogram = meter.create_histogram("histogram") +histogram.record(99.9) + +# Async Gauge +gauge = meter.create_observable_gauge("gauge", observable_gauge_func) From 290d7b51799dde183913bdd395e90a171cc185ab Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Wed, 9 Mar 2022 13:35:12 -0500 Subject: [PATCH 1155/1517] Reset async instrumentation aggregations each collection interval (#2510) Co-authored-by: Alex Boten --- .../opentelemetry/sdk/_metrics/aggregation.py | 18 ++++++++++++------ .../tests/metrics/test_aggregation.py | 13 +++++++++---- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 9258cc8d7a..e0020fa27d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -101,15 +101,18 @@ def collect(self) -> Optional[Sum]: value=value, ) - if self._value is None: - return None + with self._lock: + if self._value is None: + return None + value = self._value + self._value = None return Sum( aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=self._instrument_is_monotonic, start_time_unix_nano=self._start_time_unix_nano, time_unix_nano=now, - value=self._value, + value=value, ) @@ -126,12 +129,15 @@ def collect(self) -> Optional[Gauge]: """ Atomically return a point for the current value of the metric. """ - if self._value is None: - return None + with self._lock: + if self._value is None: + return None + value = self._value + self._value = None return Gauge( time_unix_nano=_time_ns(), - value=self._value, + value=value, ) diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 8263e2e6bc..ec6ae8dcd4 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -133,19 +133,20 @@ def test_collect_cumulative(self): self.assertEqual(first_sum.value, 1) self.assertTrue(first_sum.is_monotonic) + # should have been reset after first collect sum_aggregation.aggregate(measurement(1)) second_sum = sum_aggregation.collect() - self.assertEqual(second_sum.value, 2) + self.assertEqual(second_sum.value, 1) self.assertTrue(second_sum.is_monotonic) self.assertEqual( second_sum.start_time_unix_nano, first_sum.start_time_unix_nano ) - self.assertIsNone( - _SumAggregation(True, AggregationTemporality.CUMULATIVE).collect() - ) + # if no point seen for a whole interval, should return None + third_sum = sum_aggregation.collect() + self.assertIsNone(third_sum) class TestLastValueAggregation(TestCase): @@ -194,6 +195,10 @@ def test_collect(self): second_gauge.time_unix_nano, first_gauge.time_unix_nano ) + # if no observation seen for the interval, it should return None + third_gauge = last_value_aggregation.collect() + self.assertIsNone(third_gauge) + class TestExplicitBucketHistogramAggregation(TestCase): def test_aggregate(self): From 7505272f65028fa84191563c7c53b41adea6da37 Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Thu, 10 Mar 2022 18:10:26 -0500 Subject: [PATCH 1156/1517] datadog-apm: deprecate opentelemetry datadog exporter (#2517) --- docs/examples/datadog_exporter/README.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/examples/datadog_exporter/README.rst b/docs/examples/datadog_exporter/README.rst index 39e0798da3..da495723bb 100644 --- a/docs/examples/datadog_exporter/README.rst +++ b/docs/examples/datadog_exporter/README.rst @@ -1,5 +1,8 @@ -Datadog Exporter -================ +Datadog Span Exporter +===================== + +.. warning:: This exporter has been deprecated. To export your OTLP traces from OpenTelemetry SDK directly to Datadog Agent, please refer to `OTLP Ingest in Datadog Agent `_ . + These examples show how to use OpenTelemetry to send tracing data to Datadog. From 0b4f247e9d401e94ec77f72ddfcf917298ea27e8 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 10 Mar 2022 17:49:47 -0600 Subject: [PATCH 1157/1517] updating changelogs and version to 1.10.0-0.29b0 (#2522) (#2523) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 6 +++++- eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-prometheus/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- .../src/opentelemetry/test/version.py | 2 +- 31 files changed, 42 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b2ded8747..bb2498b026 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: ad2594e166bd7f4cd40780df418f82389de970a6 + CONTRIB_REPO_SHA: e71a5157c184266002186cf853dfb8e2f2d6b924 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index e3db0c198c..17966ed52c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.9.1-0.28b1...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.10.0-0.29b0...HEAD) + +## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 + + - Docs rework: [non-API docs are moving](https://github.com/open-telemetry/opentelemetry-python/issues/2172) to diff --git a/eachdist.ini b/eachdist.ini index f65be079be..9db9614e08 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.9.1 +version=1.10.0 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.28b1 +version=0.29b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 84a10efbd5..1d5653f5b4 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 84a10efbd5..1d5653f5b4 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index e120ca2564..d16e075227 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -40,8 +40,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.9.1 - opentelemetry-exporter-jaeger-thrift == 1.9.1 + opentelemetry-exporter-jaeger-proto-grpc == 1.10.0 + opentelemetry-exporter-jaeger-thrift == 1.10.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 84a10efbd5..1d5653f5b4 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index a94d4ed446..5743fb8ce1 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.28b1" +__version__ = "0.29b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 1031bea398..6362a4c553 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -43,8 +43,8 @@ install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.9 - opentelemetry-proto == 1.9.1 + opentelemetry-sdk ~= 1.10.0 + opentelemetry-proto == 1.10.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 030fb6915c..7cb3f8387a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index f12a5b470b..2b3b37f01a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.9.1 + opentelemetry-proto == 1.10.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 030fb6915c..7cb3f8387a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index c9af1bd46d..9e6a1e3f4a 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -39,8 +39,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.9.1 - opentelemetry-exporter-otlp-proto-http == 1.9.1 + opentelemetry-exporter-otlp-proto-grpc == 1.10.0 + opentelemetry-exporter-otlp-proto-http == 1.10.0 [options.entry_points] opentelemetry_traces_exporter = diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 030fb6915c..7cb3f8387a 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index 53e9ef17ed..be3f16ca6f 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api >= 1.9.1 - opentelemetry-sdk >= 1.9.1 + opentelemetry-api >= 1.10.0 + opentelemetry-sdk >= 1.10.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index a94d4ed446..5743fb8ce1 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.28b1" +__version__ = "0.29b0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 030fb6915c..7cb3f8387a 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 57bf4274e4..e9f7827918 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.9.1 + opentelemetry-exporter-zipkin-json == 1.10.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 030fb6915c..7cb3f8387a 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 3b19d93482..910c7ccd80 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -39,8 +39,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.9.1 - opentelemetry-exporter-zipkin-proto-http == 1.9.1 + opentelemetry-exporter-zipkin-json == 1.10.0 + opentelemetry-exporter-zipkin-proto-http == 1.10.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 030fb6915c..7cb3f8387a 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 030fb6915c..7cb3f8387a 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 030fb6915c..7cb3f8387a 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index b12ea1651b..1300e98e33 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,8 +43,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.9.1 - opentelemetry-semantic-conventions == 0.28b1 + opentelemetry-api == 1.10.0 + opentelemetry-semantic-conventions == 0.29b0 setuptools >= 16.0 dataclasses == 0.8; python_version < '3.7' typing-extensions >= 3.7.4 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 030fb6915c..7cb3f8387a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index a94d4ed446..5743fb8ce1 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.28b1" +__version__ = "0.29b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 030fb6915c..7cb3f8387a 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 030fb6915c..7cb3f8387a 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.1" +__version__ = "1.10.0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 1610d7bc0b..335f2acfb7 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -47,7 +47,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test-utils == 0.28b1 + opentelemetry-test-utils == 0.29b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index a94d4ed446..5743fb8ce1 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.28b1" +__version__ = "0.29b0" diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index a968e0037d..6d36ac277e 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.28b1" +__version__ = "0.29b0" From ae09476f927df9ebca6e3115b71373792673d0a9 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 11 Mar 2022 12:48:07 -0600 Subject: [PATCH 1158/1517] Add instructions to fix black and isort errors (#2513) --- CONTRIBUTING.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 35d5b179c2..779493efd9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,6 +71,12 @@ You can run `tox` with the following arguments: Python version - `tox -e lint` to run lint checks on all code +`black` and `isort` are executed when `tox -e lint` is run. The reported errors can be tedious to fix manually. +An easier way to do so is: + +1. Run `.tox/lint/bin/black .` +2. Run `.tox/lint/bin/isort .` + We try to keep the amount of _public symbols_ in our code minimal. A public symbol is any Python identifier that does not start with an underscore. Every public symbol is something that has to be kept in order to maintain backwards compatibility, so we try to have as few as possible. From c6dd672f129ee8e0623f39e718528c21621fac7e Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Mon, 14 Mar 2022 12:49:50 -0400 Subject: [PATCH 1159/1517] Fix a mixed-up changelog entry (#2526) --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17966ed52c..5da80b1966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.6.2-0.25b2](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.2-0.25b2) - 2021-10-19 +- Fix parental trace relationship for opentracing `follows_from` reference + ([#2180](https://github.com/open-telemetry/opentelemetry-python/pull/2180)) + ## [1.6.1-0.25b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.1-0.25b1) - 2021-10-18 - Fix ReadableSpan property types attempting to create a mapping from a list @@ -110,9 +113,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Propagation: only warn about oversized baggage headers when headers exist ([#2212](https://github.com/open-telemetry/opentelemetry-python/pull/2212)) -- Fix parental trace relationship for opentracing `follows_from` reference - ([#2180](https://github.com/open-telemetry/opentelemetry-python/pull/2180)) - ## [1.6.0-0.25b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.0-0.25b0) - 2021-10-13 - Fix race in `set_tracer_provider()` From f5520c79b17a89a8bac332a5e3e75c5e3e13e8b7 Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Mon, 14 Mar 2022 13:15:36 -0400 Subject: [PATCH 1160/1517] Fix exporter-{jaeger,otlp,zipkin} install paths (#2525) --- CHANGELOG.md | 4 ++++ exporter/opentelemetry-exporter-jaeger/setup.cfg | 6 +++++- exporter/opentelemetry-exporter-otlp/setup.cfg | 5 +++++ exporter/opentelemetry-exporter-zipkin/setup.cfg | 5 +++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5da80b1966..91f4482334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.10.0-0.29b0...HEAD) +- Fix incorrect installation of some exporter “convenience” packages into + “site-packages/src” + ([#2525](https://github.com/open-telemetry/opentelemetry-python/pull/2525)) + ## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index d16e075227..0c3fe8a311 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -37,12 +37,16 @@ classifiers = [options] python_requires = >=3.6 - +package_dir= + =src packages=find_namespace: install_requires = opentelemetry-exporter-jaeger-proto-grpc == 1.10.0 opentelemetry-exporter-jaeger-thrift == 1.10.0 +[options.packages.find] +where = src + [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 9e6a1e3f4a..6dfc63ee52 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -37,11 +37,16 @@ classifiers = [options] python_requires = >=3.6 +package_dir= + =src packages=find_namespace: install_requires = opentelemetry-exporter-otlp-proto-grpc == 1.10.0 opentelemetry-exporter-otlp-proto-http == 1.10.0 +[options.packages.find] +where = src + [options.entry_points] opentelemetry_traces_exporter = otlp = opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 910c7ccd80..c431d302dd 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -37,11 +37,16 @@ classifiers = [options] python_requires = >=3.6 +package_dir= + =src packages=find_namespace: install_requires = opentelemetry-exporter-zipkin-json == 1.10.0 opentelemetry-exporter-zipkin-proto-http == 1.10.0 +[options.packages.find] +where = src + [options.extras_require] test = From 69c9e39bc98077ddafdad6946d22fe582aee9261 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 15 Mar 2022 12:18:56 -0700 Subject: [PATCH 1161/1517] Rename OTLPHandler -> LoggingHandler (#2528) --- CHANGELOG.md | 4 ++-- docs/examples/logs/example.py | 4 ++-- .../sdk/_configuration/__init__.py | 4 ++-- .../src/opentelemetry/sdk/_logs/__init__.py | 5 +++-- opentelemetry-sdk/tests/logs/test_export.py | 22 +++++++++---------- opentelemetry-sdk/tests/logs/test_handler.py | 6 ++--- .../tests/logs/test_multi_log_prcessor.py | 4 ++-- opentelemetry-sdk/tests/test_configurator.py | 4 ++-- 8 files changed, 27 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91f4482334..fbd4575bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix incorrect installation of some exporter “convenience” packages into “site-packages/src” ([#2525](https://github.com/open-telemetry/opentelemetry-python/pull/2525)) +- Change OTLPHandler to LoggingHandler + ([#2528](https://github.com/open-telemetry/opentelemetry-python/pull/2528)) ## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 - - - Docs rework: [non-API docs are moving](https://github.com/open-telemetry/opentelemetry-python/issues/2172) to [opentelemetry.io](https://opentelemetry.io). For details, including a list of diff --git a/docs/examples/logs/example.py b/docs/examples/logs/example.py index 33879fb218..28fca75e7c 100644 --- a/docs/examples/logs/example.py +++ b/docs/examples/logs/example.py @@ -6,7 +6,7 @@ ) from opentelemetry.sdk._logs import ( LogEmitterProvider, - OTLPHandler, + LoggingHandler, set_log_emitter_provider, ) from opentelemetry.sdk._logs.export import BatchLogProcessor @@ -35,7 +35,7 @@ exporter = OTLPLogExporter(insecure=True) log_emitter_provider.add_log_processor(BatchLogProcessor(exporter)) log_emitter = log_emitter_provider.get_log_emitter(__name__, "0.1") -handler = OTLPHandler(level=logging.NOTSET, log_emitter=log_emitter) +handler = LoggingHandler(level=logging.NOTSET, log_emitter=log_emitter) # Attach OTLP handler to root logger logging.getLogger().addHandler(handler) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 124573c254..efa6f31f89 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -32,7 +32,7 @@ ) from opentelemetry.sdk._logs import ( LogEmitterProvider, - OTLPHandler, + LoggingHandler, set_log_emitter_provider, ) from opentelemetry.sdk._logs.export import BatchLogProcessor, LogExporter @@ -114,7 +114,7 @@ def _init_logging( ) log_emitter = provider.get_log_emitter(__name__) - handler = OTLPHandler(level=logging.NOTSET, log_emitter=log_emitter) + handler = LoggingHandler(level=logging.NOTSET, log_emitter=log_emitter) logging.getLogger().addHandler(handler) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index 1de44712e8..d13c8c59ea 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -303,9 +303,10 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: ) -class OTLPHandler(logging.Handler): +class LoggingHandler(logging.Handler): """A handler class which writes logging records, in OTLP format, to - a network destination or file. + a network destination or file. Supports signals from the `logging` module. + https://docs.python.org/3/library/logging.html """ def __init__( diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 95c132769f..f398fe3fd9 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -26,8 +26,8 @@ from opentelemetry.sdk._logs import ( LogData, LogEmitterProvider, + LoggingHandler, LogRecord, - OTLPHandler, ) from opentelemetry.sdk._logs.export import ( BatchLogProcessor, @@ -56,7 +56,7 @@ def test_simple_log_processor_default_level(self): log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) logger = logging.getLogger("default_level") - logger.addHandler(OTLPHandler(log_emitter=log_emitter)) + logger.addHandler(LoggingHandler(log_emitter=log_emitter)) logger.warning("Something is wrong") finished_logs = exporter.get_finished_logs() @@ -77,7 +77,7 @@ def test_simple_log_processor_custom_level(self): logger = logging.getLogger("custom_level") logger.setLevel(logging.ERROR) - logger.addHandler(OTLPHandler(log_emitter=log_emitter)) + logger.addHandler(LoggingHandler(log_emitter=log_emitter)) logger.warning("Warning message") logger.debug("Debug message") @@ -107,7 +107,7 @@ def test_simple_log_processor_trace_correlation(self): log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) logger = logging.getLogger("trace_correlation") - logger.addHandler(OTLPHandler(log_emitter=log_emitter)) + logger.addHandler(LoggingHandler(log_emitter=log_emitter)) logger.warning("Warning message") finished_logs = exporter.get_finished_logs() @@ -145,7 +145,7 @@ def test_simple_log_processor_shutdown(self): log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) logger = logging.getLogger("shutdown") - logger.addHandler(OTLPHandler(log_emitter=log_emitter)) + logger.addHandler(LoggingHandler(log_emitter=log_emitter)) logger.warning("Something is wrong") finished_logs = exporter.get_finished_logs() @@ -172,7 +172,7 @@ def test_emit_call_log_record(self): emitter = provider.get_log_emitter(__name__) logger = logging.getLogger("emit_call") - logger.addHandler(OTLPHandler(log_emitter=emitter)) + logger.addHandler(LoggingHandler(log_emitter=emitter)) logger.error("error") self.assertEqual(log_processor.emit.call_count, 1) @@ -186,7 +186,7 @@ def test_shutdown(self): emitter = provider.get_log_emitter(__name__) logger = logging.getLogger("shutdown") - logger.addHandler(OTLPHandler(log_emitter=emitter)) + logger.addHandler(LoggingHandler(log_emitter=emitter)) logger.warning("warning message: %s", "possible upcoming heatwave") logger.error("Very high rise in temperatures across the globe") @@ -219,7 +219,7 @@ def test_force_flush(self): emitter = provider.get_log_emitter(__name__) logger = logging.getLogger("force_flush") - logger.addHandler(OTLPHandler(log_emitter=emitter)) + logger.addHandler(LoggingHandler(log_emitter=emitter)) logger.critical("Earth is burning") log_processor.force_flush() @@ -238,7 +238,7 @@ def test_log_processor_too_many_logs(self): emitter = provider.get_log_emitter(__name__) logger = logging.getLogger("many_logs") - logger.addHandler(OTLPHandler(log_emitter=emitter)) + logger.addHandler(LoggingHandler(log_emitter=emitter)) for log_no in range(1000): logger.critical("Log no: %s", log_no) @@ -256,7 +256,7 @@ def test_with_multiple_threads(self): emitter = provider.get_log_emitter(__name__) logger = logging.getLogger("threads") - logger.addHandler(OTLPHandler(log_emitter=emitter)) + logger.addHandler(LoggingHandler(log_emitter=emitter)) def bulk_log_and_flush(num_logs): for _ in range(num_logs): @@ -291,7 +291,7 @@ def test_batch_log_processor_fork(self): emitter = provider.get_log_emitter(__name__) logger = logging.getLogger("test-fork") - logger.addHandler(OTLPHandler(log_emitter=emitter)) + logger.addHandler(LoggingHandler(log_emitter=emitter)) logger.critical("yolo") time.sleep(0.5) # give some time for the exporter to upload diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index d7942f912b..d80356c454 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -17,19 +17,19 @@ from unittest.mock import Mock from opentelemetry.sdk import trace -from opentelemetry.sdk._logs import LogEmitter, OTLPHandler +from opentelemetry.sdk._logs import LogEmitter, LoggingHandler from opentelemetry.sdk._logs.severity import SeverityNumber from opentelemetry.trace import INVALID_SPAN_CONTEXT def get_logger(level=logging.NOTSET, log_emitter=None): logger = logging.getLogger(__name__) - handler = OTLPHandler(level=level, log_emitter=log_emitter) + handler = LoggingHandler(level=level, log_emitter=log_emitter) logger.addHandler(handler) return logger -class TestOTLPHandler(unittest.TestCase): +class TestLoggingHandler(unittest.TestCase): def test_handler_default_log_level(self): emitter_mock = Mock(spec=LogEmitter) logger = get_logger(log_emitter=emitter_mock) diff --git a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py index e55124edcc..6a5838231b 100644 --- a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py +++ b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py @@ -24,9 +24,9 @@ from opentelemetry.sdk._logs import ( ConcurrentMultiLogProcessor, LogEmitterProvider, + LoggingHandler, LogProcessor, LogRecord, - OTLPHandler, SynchronousMultiLogProcessor, ) from opentelemetry.sdk._logs.severity import SeverityNumber @@ -58,7 +58,7 @@ class TestLogProcessor(unittest.TestCase): def test_log_processor(self): provider = LogEmitterProvider() log_emitter = provider.get_log_emitter(__name__) - handler = OTLPHandler(log_emitter=log_emitter) + handler = LoggingHandler(log_emitter=log_emitter) logs_list_1 = [] processor1 = AnotherLogProcessor(Mock(), logs_list_1) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index e0dd1e7b4b..42d3859f5e 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -30,7 +30,7 @@ _init_logging, _init_tracing, ) -from opentelemetry.sdk._logs import OTLPHandler +from opentelemetry.sdk._logs import LoggingHandler from opentelemetry.sdk._logs.export import ConsoleLogExporter from opentelemetry.sdk._metrics.export import ConsoleMetricExporter from opentelemetry.sdk.resources import SERVICE_NAME, Resource @@ -242,7 +242,7 @@ def tearDown(self): root_logger.handlers = [ handler for handler in root_logger.handlers - if not isinstance(handler, OTLPHandler) + if not isinstance(handler, LoggingHandler) ] def test_logging_init_empty(self): From 09a03aef6e45184f2853762c44e8f0c11215a866 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 17 Mar 2022 18:03:06 -0400 Subject: [PATCH 1162/1517] Small fixes to SpanProcessor docs around shutdown() (#2535) --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index c82fcb3927..6a4c132ee7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -112,7 +112,7 @@ def on_end(self, span: "ReadableSpan") -> None: """ def shutdown(self) -> None: - """Called when a :class:`opentelemetry.sdk.trace.Tracer` is shutdown.""" + """Called when a :class:`opentelemetry.sdk.trace.TracerProvider` is shutdown.""" def force_flush(self, timeout_millis: int = 30000) -> bool: """Export all ended spans to the configured Exporter that have not yet @@ -1140,7 +1140,7 @@ def add_span_processor(self, span_processor: SpanProcessor) -> None: self._active_span_processor.add_span_processor(span_processor) def shutdown(self): - """Shut down the span processors added to the tracer.""" + """Shut down the span processors added to the tracer provider.""" self._active_span_processor.shutdown() if self._atexit_handler is not None: atexit.unregister(self._atexit_handler) From f7b33c6f1deaf86aedf5333743eda48b4a72b675 Mon Sep 17 00:00:00 2001 From: Marc Pichler Date: Mon, 21 Mar 2022 15:14:04 +0100 Subject: [PATCH 1163/1517] Fix delta histogram sum not being reset on collection (#2533) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/_metrics/aggregation.py | 4 +++- opentelemetry-sdk/tests/metrics/test_aggregation.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd4575bae..0a10cc3543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2525](https://github.com/open-telemetry/opentelemetry-python/pull/2525)) - Change OTLPHandler to LoggingHandler ([#2528](https://github.com/open-telemetry/opentelemetry-python/pull/2528)) +- Fix delta histogram sum not being reset on collection + ([#2533](https://github.com/open-telemetry/opentelemetry-python/pull/2533)) ## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index e0020fa27d..23596112fd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -191,9 +191,11 @@ def collect(self) -> Histogram: with self._lock: value = self._bucket_counts start_time_unix_nano = self._start_time_unix_nano + histogram_sum = self._sum self._bucket_counts = self._get_empty_bucket_counts() self._start_time_unix_nano = now + 1 + self._sum = 0 return Histogram( start_time_unix_nano=start_time_unix_nano, @@ -201,7 +203,7 @@ def collect(self) -> Histogram: bucket_counts=tuple(value), explicit_bounds=self._boundaries, aggregation_temporality=AggregationTemporality.DELTA, - sum=self._sum, + sum=histogram_sum, ) diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index ec6ae8dcd4..414939e30c 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -286,6 +286,7 @@ def test_collect(self): first_histogram = explicit_bucket_histogram_aggregation.collect() self.assertEqual(first_histogram.bucket_counts, (0, 1, 0, 0)) + self.assertEqual(first_histogram.sum, 1) # CI fails the last assertion without this sleep(0.1) @@ -294,6 +295,7 @@ def test_collect(self): second_histogram = explicit_bucket_histogram_aggregation.collect() self.assertEqual(second_histogram.bucket_counts, (0, 1, 0, 0)) + self.assertEqual(second_histogram.sum, 1) self.assertGreater( second_histogram.time_unix_nano, first_histogram.time_unix_nano From 97a36ea941b964b8488386427f3ea74670bb6f05 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 21 Mar 2022 09:36:29 -0700 Subject: [PATCH 1164/1517] Capture exception information in log attributes (#2531) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/_logs/__init__.py | 22 ++++++++++++- opentelemetry-sdk/tests/logs/test_handler.py | 32 ++++++++++++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a10cc3543..d6ff166ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix incorrect installation of some exporter “convenience” packages into “site-packages/src” ([#2525](https://github.com/open-telemetry/opentelemetry-python/pull/2525)) +- Capture exception information as part of log attributes + ([#2531](https://github.com/open-telemetry/opentelemetry-python/pull/2531)) - Change OTLPHandler to LoggingHandler ([#2528](https://github.com/open-telemetry/opentelemetry-python/pull/2528)) - Fix delta histogram sum not being reset on collection diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index d13c8c59ea..c278635c6b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -19,6 +19,7 @@ import logging import os import threading +import traceback from typing import Any, Callable, Optional, Tuple, Union, cast from opentelemetry.sdk._logs.severity import SeverityNumber, std_to_otlp @@ -28,6 +29,7 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import ( format_span_id, format_trace_id, @@ -319,9 +321,27 @@ def __init__( @staticmethod def _get_attributes(record: logging.LogRecord) -> Attributes: - return { + attributes = { k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS } + if record.exc_info is not None: + exc_type = "" + message = "" + stack_trace = "" + exctype, value, tb = record.exc_info + if exctype is not None: + exc_type = exctype.__name__ + if value is not None and value.args: + message = value.args[0] + if tb is not None: + # https://github.com/open-telemetry/opentelemetry-specification/blob/9fa7c656b26647b27e485a6af7e38dc716eba98a/specification/trace/semantic_conventions/exceptions.md#stacktrace-representation + stack_trace = "".join( + traceback.format_exception(*record.exc_info) + ) + attributes[SpanAttributes.EXCEPTION_TYPE] = exc_type + attributes[SpanAttributes.EXCEPTION_MESSAGE] = message + attributes[SpanAttributes.EXCEPTION_STACKTRACE] = stack_trace + return attributes def _translate(self, record: logging.LogRecord) -> LogRecord: timestamp = int(record.created * 1e9) diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index d80356c454..7ea478e844 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import logging import unittest from unittest.mock import Mock @@ -19,6 +18,7 @@ from opentelemetry.sdk import trace from opentelemetry.sdk._logs import LogEmitter, LoggingHandler from opentelemetry.sdk._logs.severity import SeverityNumber +from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import INVALID_SPAN_CONTEXT @@ -77,6 +77,36 @@ def test_log_record_user_attributes(self): self.assertIsNotNone(log_record) self.assertEqual(log_record.attributes, {"http.status_code": 200}) + def test_log_record_exception(self): + """Exception information will be included in attributes""" + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(log_emitter=emitter_mock) + try: + raise ZeroDivisionError("division by zero") + except ZeroDivisionError: + logger.exception("Zero Division Error") + args, _ = emitter_mock.emit.call_args_list[0] + log_record = args[0] + + self.assertIsNotNone(log_record) + self.assertEqual(log_record.body, "Zero Division Error") + self.assertEqual( + log_record.attributes[SpanAttributes.EXCEPTION_TYPE], + ZeroDivisionError.__name__, + ) + self.assertEqual( + log_record.attributes[SpanAttributes.EXCEPTION_MESSAGE], + "division by zero", + ) + stack_trace = log_record.attributes[ + SpanAttributes.EXCEPTION_STACKTRACE + ] + self.assertIsInstance(stack_trace, str) + self.assertTrue("Traceback" in stack_trace) + self.assertTrue("ZeroDivisionError" in stack_trace) + self.assertTrue("division by zero" in stack_trace) + self.assertTrue(__file__ in stack_trace) + def test_log_record_trace_correlation(self): emitter_mock = Mock(spec=LogEmitter) logger = get_logger(log_emitter=emitter_mock) From 95aeeccb59945fee7f8cc40299df12a85ebc5783 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Tue, 22 Mar 2022 14:49:02 -0400 Subject: [PATCH 1165/1517] Add InMemoryMetricReader to metrics SDK (#2540) --- CHANGELOG.md | 2 + .../sdk/_metrics/export/__init__.py | 34 +++++++- .../metrics/test_in_memory_metric_reader.py | 80 +++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ff166ec8..7e2017006f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2528](https://github.com/open-telemetry/opentelemetry-python/pull/2528)) - Fix delta histogram sum not being reset on collection ([#2533](https://github.com/open-telemetry/opentelemetry-python/pull/2533)) +- Add InMemoryMetricReader to metrics SDK + ([#2540](https://github.com/open-telemetry/opentelemetry-python/pull/2540)) ## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py index 284629330f..dc172a48af 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -18,8 +18,8 @@ from enum import Enum from os import environ, linesep from sys import stdout -from threading import Event, Thread -from typing import IO, Callable, Iterable, Optional, Sequence +from threading import Event, RLock, Thread +from typing import IO, Callable, Iterable, List, Optional, Sequence from opentelemetry.context import ( _SUPPRESS_INSTRUMENTATION_KEY, @@ -96,6 +96,36 @@ def shutdown(self) -> None: pass +class InMemoryMetricReader(MetricReader): + """Implementation of :class:`MetricReader` that returns its metrics from :func:`metrics`. + + This is useful for e.g. unit tests. + """ + + def __init__( + self, + preferred_temporality: AggregationTemporality = AggregationTemporality.CUMULATIVE, + ) -> None: + super().__init__(preferred_temporality=preferred_temporality) + self._lock = RLock() + self._metrics: List[Metric] = [] + + def get_metrics(self) -> List[Metric]: + """Reads and returns current metrics from the SDK""" + with self._lock: + self.collect() + metrics = self._metrics + self._metrics = [] + return metrics + + def _receive_metrics(self, metrics: Iterable[Metric]): + with self._lock: + self._metrics = list(metrics) + + def shutdown(self) -> bool: + return True + + class PeriodicExportingMetricReader(MetricReader): """`PeriodicExportingMetricReader` is an implementation of `MetricReader` that collects metrics based on a user-configurable time interval, and passes the diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py new file mode 100644 index 0000000000..f69fcea9e5 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py @@ -0,0 +1,80 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase +from unittest.mock import Mock + +from opentelemetry._metrics.measurement import Measurement +from opentelemetry.sdk._metrics import MeterProvider +from opentelemetry.sdk._metrics.export import InMemoryMetricReader +from opentelemetry.sdk._metrics.point import ( + AggregationTemporality, + Metric, + Sum, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo + + +class TestInMemoryMetricReader(TestCase): + def test_no_metrics(self): + mock_collect_callback = Mock(return_value=[]) + reader = InMemoryMetricReader() + reader._set_collect_callback(mock_collect_callback) + self.assertEqual(reader.get_metrics(), []) + mock_collect_callback.assert_called_once() + + def test_converts_metrics_to_list(self): + metric = Metric( + attributes={"myattr": "baz"}, + description="", + instrumentation_info=InstrumentationInfo("testmetrics"), + name="foo", + resource=Resource.create(), + unit="", + point=Sum( + start_time_unix_nano=1647626444152947792, + time_unix_nano=1647626444153163239, + value=72.3309814450449, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + ) + mock_collect_callback = Mock(return_value=(metric,)) + reader = InMemoryMetricReader() + reader._set_collect_callback(mock_collect_callback) + + returned_metrics = reader.get_metrics() + mock_collect_callback.assert_called_once() + self.assertIsInstance(returned_metrics, list) + self.assertEqual(len(returned_metrics), 1) + self.assertIs(returned_metrics[0], metric) + + def test_shutdown(self): + # shutdown should always be successful + self.assertTrue(InMemoryMetricReader().shutdown()) + + def test_integration(self): + reader = InMemoryMetricReader() + meter = MeterProvider(metric_readers=[reader]).get_meter("test_meter") + counter1 = meter.create_counter("counter1") + meter.create_observable_gauge( + "observable_gauge1", lambda: [Measurement(value=12)] + ) + counter1.add(1, {"foo": "1"}) + counter1.add(1, {"foo": "2"}) + + metrics = reader.get_metrics() + # should be 3 metrics, one from the observable gauge and one for each labelset from the counter + self.assertEqual(len(metrics), 3) From b7f91579abc218b95c4219fd5cca9f3056d7527a Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 24 Mar 2022 12:28:46 -0600 Subject: [PATCH 1166/1517] Add drop aggregation (#2544) --- .../opentelemetry/sdk/_metrics/aggregation.py | 15 +++++++++++ .../metrics/test_metric_reader_storage.py | 24 +++++++++++++++++ .../metrics/test_view_instrument_match.py | 26 +++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 23596112fd..420385c69c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -54,6 +54,14 @@ def collect(self) -> Optional[_PointVarT]: pass +class _DropAggregation(_Aggregation): + def aggregate(self, measurement: Measurement) -> None: + pass + + def collect(self) -> Optional[_PointVarT]: + pass + + class _SumAggregation(_Aggregation[Sum]): def __init__( self, @@ -378,3 +386,10 @@ def _create_aggregation(self, instrument: Instrument) -> _Aggregation: class LastValueAggregation(_AggregationFactory): def _create_aggregation(self, instrument: Instrument) -> _Aggregation: return _LastValueAggregation() + + +class DropAggregation(_AggregationFactory): + """Using this aggregation will make all measurements be ignored.""" + + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + return _DropAggregation() diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index 936aa0433f..d73cf893a9 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -14,12 +14,15 @@ from unittest.mock import Mock, patch +from opentelemetry.sdk._metrics.aggregation import DropAggregation +from opentelemetry.sdk._metrics.instrument import Counter from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.metric_reader_storage import ( MetricReaderStorage, ) from opentelemetry.sdk._metrics.point import AggregationTemporality from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration +from opentelemetry.sdk._metrics.view import View from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc @@ -217,3 +220,24 @@ def test_default_view_disabled(self, MockViewInstrumentMatch: Mock): MockViewInstrumentMatch.call_args_list.clear() storage.consume_measurement(Measurement(1, instrument2)) self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 0) + + def test_drop_aggregation(self): + + counter = Counter("name", Mock(), Mock()) + metric_reader_storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=( + View( + instrument_name="name", aggregation=DropAggregation() + ), + ), + enable_default_view=False, + ) + ) + metric_reader_storage.consume_measurement(Measurement(1, counter)) + + self.assertEqual( + [], metric_reader_storage.collect(AggregationTemporality.DELTA) + ) diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index c366309b41..210963d547 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -18,6 +18,10 @@ from opentelemetry.sdk._metrics._view_instrument_match import ( _ViewInstrumentMatch, ) +from opentelemetry.sdk._metrics.aggregation import ( + DropAggregation, + _DropAggregation, +) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration @@ -129,6 +133,28 @@ def test_consume_measurement(self): {frozenset({}): self.mock_created_aggregation}, ) + # Test that a drop aggregation is handled in the same way as any + # other aggregation. + drop_aggregation = DropAggregation() + + view_instrument_match = _ViewInstrumentMatch( + view=View( + instrument_name="instrument1", + name="name", + aggregation=drop_aggregation, + attribute_keys={}, + ), + instrument=instrument1, + sdk_config=sdk_config, + ) + view_instrument_match.consume_measurement( + Measurement(value=0, instrument=instrument1, attributes=None) + ) + self.assertIsInstance( + view_instrument_match._attributes_aggregation[frozenset({})], + _DropAggregation, + ) + def test_collect(self): instrument1 = Mock( name="instrument1", description="description", unit="unit" From 070285218b5444d31c308a464ec45e52829a1622 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 24 Mar 2022 18:59:24 -0400 Subject: [PATCH 1167/1517] Fix View doc string (#2551) --- .../src/opentelemetry/sdk/_metrics/view.py | 101 +++++++++--------- 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py index a59f734904..7cdcb1ca27 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py @@ -27,71 +27,68 @@ class View: - def __init__( - self, - instrument_type: Optional[Type[Instrument]] = None, - instrument_name: Optional[str] = None, - meter_name: Optional[str] = None, - meter_version: Optional[str] = None, - meter_schema_url: Optional[str] = None, - name: Optional[str] = None, - description: Optional[str] = None, - attribute_keys: Optional[Set[str]] = None, - aggregation: Optional[_AggregationFactory] = None, - ): - """ - A `View` configuration parameters can be used for the following - purposes: + """ + A `View` configuration parameters can be used for the following + purposes: - 1. Match instruments: When an instrument matches a view, measurements - received by that instrument will be processed. - 2. Customize metric streams: A metric stream is identified by a match - between a view and an instrument and a set of attributes. The metric - stream can be customized by certain attributes of the corresponding - view. + 1. Match instruments: When an instrument matches a view, measurements + received by that instrument will be processed. + 2. Customize metric streams: A metric stream is identified by a match + between a view and an instrument and a set of attributes. The metric + stream can be customized by certain attributes of the corresponding view. - The attributes documented next serve one of the previous two purposes. + The attributes documented next serve one of the previous two purposes. - Args: - instrument_type: This is an instrument matching attribute: the - class the instrument must be to match the view. + Args: + instrument_type: This is an instrument matching attribute: the class the + instrument must be to match the view. - instrument_name: This is an instrument matching attribute: the name - the instrument must have to match the view. Wild card - characters are supported. Wild card characters should not be - used with this attribute if the view has also a - ``name`` defined. + instrument_name: This is an instrument matching attribute: the name the + instrument must have to match the view. Wild card characters are + supported. Wild card characters should not be used with this + attribute if the view has also a ``name`` defined. - meter_name: This is an instrument matching attribute: the name - the instrument meter must have to match the view. + meter_name: This is an instrument matching attribute: the name the + instrument meter must have to match the view. - meter_version : This is an instrument matching attribute: the - version the instrument meter must have to match the view. + meter_version : This is an instrument matching attribute: the version + the instrument meter must have to match the view. - meter_schema URL : This is an instrument matching attribute: the - schema URL the instrument meter must have to match the view. + meter_schema URL : This is an instrument matching attribute: the schema + URL the instrument meter must have to match the view. - name: This is a metric stream customizing attribute: the name of - the metric stream. If `None`, the name of the instrument will - be used. + name: This is a metric stream customizing attribute: the name of the + metric stream. If `None`, the name of the instrument will be used. - description: This is a metric stream customizing attribute: the - description of the metric stream. If `None`, the description of - the instrument will be used. + description: This is a metric stream customizing attribute: the + description of the metric stream. If `None`, the description of the + instrument will be used. - attribute_keys: This is a metric stream customizing attribute: this - is a set of attribute keys. If not `None` then only the - measurement attributes that are in `attribute_keys` will be - used to identify the metric stream. + attribute_keys: This is a metric stream customizing attribute: this is + a set of attribute keys. If not `None` then only the measurement + attributes that are in `attribute_keys` will be used to identify the + metric stream. - aggregation: This is a metric stream customizing attribute: the - aggregatation instance to use when data is aggregated for the - corresponding metrics stream. If `None` the default aggregation - of the instrument will be used. + aggregation: This is a metric stream customizing attribute: the + aggregatation instance to use when data is aggregated for the + corresponding metrics stream. If `None` the default aggregation of + the instrument will be used. - This class is not intended to be subclassed by the user. - """ + This class is not intended to be subclassed by the user. + """ + def __init__( + self, + instrument_type: Optional[Type[Instrument]] = None, + instrument_name: Optional[str] = None, + meter_name: Optional[str] = None, + meter_version: Optional[str] = None, + meter_schema_url: Optional[str] = None, + name: Optional[str] = None, + description: Optional[str] = None, + attribute_keys: Optional[Set[str]] = None, + aggregation: Optional[_AggregationFactory] = None, + ): if ( instrument_type is instrument_name From 0bfc92356c4baf17ff24b866df9bd2ec7267d1c0 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 25 Mar 2022 10:09:52 -0700 Subject: [PATCH 1168/1517] [docs] add documentation for no-op components (#2563) --- .../src/opentelemetry/_metrics/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index 4fd4b22988..134237fc91 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -70,12 +70,15 @@ def get_meter( class NoOpMeterProvider(MeterProvider): + """The default MeterProvider used when no MeterProvider implementation is available.""" + def get_meter( self, name, version=None, schema_url=None, ) -> "Meter": + """Returns a NoOpMeter.""" super().get_meter(name, version=version, schema_url=schema_url) return NoOpMeter(name, version=version, schema_url=schema_url) @@ -347,13 +350,20 @@ def create_observable_up_down_counter( class NoOpMeter(Meter): + """The default Meter used when no Meter implementation is available. + + All operations are no-op. + """ + def create_counter(self, name, unit="", description="") -> Counter: + """Returns a no-op Counter.""" super().create_counter(name, unit=unit, description=description) return DefaultCounter(name, unit=unit, description=description) def create_up_down_counter( self, name, unit="", description="" ) -> UpDownCounter: + """Returns a no-op UpDownCounter.""" super().create_up_down_counter( name, unit=unit, description=description ) @@ -362,6 +372,7 @@ def create_up_down_counter( def create_observable_counter( self, name, callback, unit="", description="" ) -> ObservableCounter: + """Returns a no-op ObservableCounter.""" super().create_observable_counter( name, callback, unit=unit, description=description ) @@ -373,12 +384,14 @@ def create_observable_counter( ) def create_histogram(self, name, unit="", description="") -> Histogram: + """Returns a no-op Histogram.""" super().create_histogram(name, unit=unit, description=description) return DefaultHistogram(name, unit=unit, description=description) def create_observable_gauge( self, name, callback, unit="", description="" ) -> ObservableGauge: + """Returns a no-op ObservableGauge.""" super().create_observable_gauge( name, callback, unit=unit, description=description ) @@ -392,6 +405,7 @@ def create_observable_gauge( def create_observable_up_down_counter( self, name, callback, unit="", description="" ) -> ObservableUpDownCounter: + """Returns a no-op ObservableUpDownCounter.""" super().create_observable_up_down_counter( name, callback, unit=unit, description=description ) From 64a25a0addcc873949b6e7277edec0d92f5fd842 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Sat, 26 Mar 2022 01:42:27 +0530 Subject: [PATCH 1169/1517] Drop the usage of name field (#2565) * Drop the usage of name field * Add CHANGELOG entry * Update CHANGELOG.md Co-authored-by: Leighton Chen Co-authored-by: Leighton Chen --- CHANGELOG.md | 2 ++ .../exporter/otlp/proto/grpc/_log_exporter/__init__.py | 4 ---- .../tests/logs/test_otlp_logs_exporter.py | 7 ------- opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py | 3 --- opentelemetry-sdk/tests/logs/test_export.py | 1 - opentelemetry-sdk/tests/logs/test_log_record.py | 1 - 6 files changed, 2 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e2017006f..f1f33a4385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2533](https://github.com/open-telemetry/opentelemetry-python/pull/2533)) - Add InMemoryMetricReader to metrics SDK ([#2540](https://github.com/open-telemetry/opentelemetry-python/pull/2540)) +- Drop the usage of name field from log model in OTLP + ([#2565](https://github.com/open-telemetry/opentelemetry-python/pull/2565)) ## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index 187b69a781..522d2de8b5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -63,9 +63,6 @@ def __init__( } ) - def _translate_name(self, log_data: LogData) -> None: - self._collector_kwargs["name"] = log_data.log_record.name - def _translate_time(self, log_data: LogData) -> None: self._collector_kwargs[ "time_unix_nano" @@ -140,7 +137,6 @@ def _translate_data( self._collector_kwargs = {} - self._translate_name(log_data) self._translate_time(log_data) self._translate_span_id(log_data) self._translate_trace_id(log_data) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index b470bc1371..5e3a54fbcb 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -119,7 +119,6 @@ def setUp(self): trace_flags=TraceFlags(0x01), severity_text="WARNING", severity_number=SDKSeverityNumber.WARN, - name="name", body="Zhengzhou, We have a heaviest rains in 1000 years", resource=SDKResource({"key": "value"}), attributes={"a": 1, "b": "c"}, @@ -136,7 +135,6 @@ def setUp(self): trace_flags=TraceFlags(0x01), severity_text="INFO", severity_number=SDKSeverityNumber.INFO2, - name="info name", body="Sydney, Opera House is closed", resource=SDKResource({"key": "value"}), attributes={"custom_attr": [1, 2, 3]}, @@ -153,7 +151,6 @@ def setUp(self): trace_flags=TraceFlags(0x01), severity_text="ERROR", severity_number=SDKSeverityNumber.WARN, - name="error name", body="Mumbai, Boil water before drinking", resource=SDKResource({"service": "myapp"}), ), @@ -315,7 +312,6 @@ def test_translate_log_data(self): log_records=[ PB2LogRecord( # pylint: disable=no-member - name="name", time_unix_nano=self.log_data_1.log_record.timestamp, severity_number=self.log_data_1.log_record.severity_number.value, severity_text="WARNING", @@ -375,7 +371,6 @@ def test_translate_multiple_logs(self): log_records=[ PB2LogRecord( # pylint: disable=no-member - name="name", time_unix_nano=self.log_data_1.log_record.timestamp, severity_number=self.log_data_1.log_record.severity_number.value, severity_text="WARNING", @@ -413,7 +408,6 @@ def test_translate_multiple_logs(self): log_records=[ PB2LogRecord( # pylint: disable=no-member - name="info name", time_unix_nano=self.log_data_2.log_record.timestamp, severity_number=self.log_data_2.log_record.severity_number.value, severity_text="INFO", @@ -459,7 +453,6 @@ def test_translate_multiple_logs(self): log_records=[ PB2LogRecord( # pylint: disable=no-member - name="error name", time_unix_nano=self.log_data_3.log_record.timestamp, severity_number=self.log_data_3.log_record.severity_number.value, severity_text="ERROR", diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index c278635c6b..c6887a6718 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -59,7 +59,6 @@ def __init__( trace_flags: Optional[TraceFlags] = None, severity_text: Optional[str] = None, severity_number: Optional[SeverityNumber] = None, - name: Optional[str] = None, body: Optional[Any] = None, resource: Optional[Resource] = None, attributes: Optional[Attributes] = None, @@ -70,7 +69,6 @@ def __init__( self.trace_flags = trace_flags self.severity_text = severity_text self.severity_number = severity_number - self.name = name self.body = body self.resource = resource self.attributes = attributes @@ -84,7 +82,6 @@ def to_json(self) -> str: return json.dumps( { "body": self.body, - "name": self.name, "severity_number": repr(self.severity_number), "severity_text": self.severity_text, "attributes": self.attributes, diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index f398fe3fd9..42d66176fd 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -334,7 +334,6 @@ def test_export(self): # pylint: disable=no-self-use trace_flags=TraceFlags(0x01), severity_text="WARN", severity_number=SeverityNumber.WARN, - name="name", body="Zhengzhou, We have a heaviest rains in 1000 years", resource=SDKResource({"key": "value"}), attributes={"a": 1, "b": "c"}, diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index d6d8c7a65c..7142408c4c 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -23,7 +23,6 @@ def test_log_record_to_json(self): expected = json.dumps( { "body": "a log line", - "name": None, "severity_number": "None", "severity_text": None, "attributes": None, From 3bc59008841a5fa1520b481aa9c3941e592fed75 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 28 Mar 2022 15:29:35 -0400 Subject: [PATCH 1170/1517] Fix typo in metrics sdk docs (#2571) --- docs/sdk/metrics.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sdk/metrics.rst b/docs/sdk/metrics.rst index 39041ea27d..c79696fc44 100644 --- a/docs/sdk/metrics.rst +++ b/docs/sdk/metrics.rst @@ -6,7 +6,7 @@ opentelemetry.sdk._metrics package :mod:`opentelemetry.sdk._metrics` are subject to change in minor/patch releases and there are no backward compatability guarantees at this time. - Once logs become stable, this package will be be renamed to ``opentelemetry.sdk.metrics``. + Once metrics become stable, this package will be be renamed to ``opentelemetry.sdk.metrics``. Submodules ---------- From 780c08efe649718365c01fc8846b22b2aef67b81 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 29 Mar 2022 01:57:32 +0530 Subject: [PATCH 1171/1517] Bump black version (#2572) --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index e02e1d9eec..5051ddac2a 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,7 @@ pylint==2.11.0 flake8~=3.7 isort~=5.8 -black~=22.1.0 +black~=22.3.0 httpretty~=1.0 mypy==0.931 sphinx~=3.5.4 From 3fb9399d47c5b8dbbcebe7cb41fee340a18d2e6b Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 28 Mar 2022 15:50:55 -0600 Subject: [PATCH 1172/1517] Add type annotations (#2568) Fixes #2471 Co-authored-by: Srikanth Chekuri --- .../opentelemetry/sdk/_metrics/_view_instrument_match.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py index c12aed3fcb..385a8516d1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py @@ -15,10 +15,12 @@ from logging import getLogger from threading import Lock -from typing import TYPE_CHECKING, Iterable +from typing import TYPE_CHECKING, Dict, Iterable from opentelemetry.sdk._metrics.aggregation import ( + _Aggregation, _convert_aggregation_temporality, + _PointVarT, ) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric @@ -41,8 +43,8 @@ def __init__( self._view = view self._instrument = instrument self._sdk_config = sdk_config - self._attributes_aggregation = {} - self._attributes_previous_point = {} + self._attributes_aggregation: Dict[frozenset, _Aggregation] = {} + self._attributes_previous_point: Dict[frozenset, _PointVarT] = {} self._lock = Lock() # pylint: disable=protected-access From 44138690c41556470779e38c17f1d38f67652e22 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 28 Mar 2022 18:22:34 -0400 Subject: [PATCH 1173/1517] Remove `enable_default_view` option from sdk MeterProvider (#2547) * Remove enable_default_view option from sdk MeterProvider * document rest of MeterProvider constructor params * fix docs build * remove unused function Co-authored-by: Diego Hurtado Co-authored-by: Leighton Chen Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 + docs/sdk/metrics.aggregation.rst | 7 +++ docs/sdk/metrics.metric_reader.rst | 7 +++ docs/sdk/metrics.rst | 6 ++ docs/sdk/metrics.view.rst | 7 +++ .../opentelemetry/sdk/_metrics/__init__.py | 31 +++++++++- .../opentelemetry/sdk/_metrics/aggregation.py | 5 ++ .../sdk/_metrics/metric_reader.py | 10 +++- .../sdk/_metrics/metric_reader_storage.py | 10 +--- .../sdk/_metrics/sdk_configuration.py | 1 - .../src/opentelemetry/sdk/_metrics/view.py | 24 ++++---- .../test_disable_default_views.py | 57 +++++++++++++++++++ .../metrics/test_measurement_consumer.py | 4 -- .../metrics/test_metric_reader_storage.py | 34 ----------- .../metrics/test_view_instrument_match.py | 2 - 15 files changed, 140 insertions(+), 67 deletions(-) create mode 100644 docs/sdk/metrics.aggregation.rst create mode 100644 docs/sdk/metrics.metric_reader.rst create mode 100644 docs/sdk/metrics.view.rst create mode 100644 opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f1f33a4385..3f9a93cca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2540](https://github.com/open-telemetry/opentelemetry-python/pull/2540)) - Drop the usage of name field from log model in OTLP ([#2565](https://github.com/open-telemetry/opentelemetry-python/pull/2565)) +- Remove `enable_default_view` option from sdk MeterProvider + ([#2547](https://github.com/open-telemetry/opentelemetry-python/pull/2547)) ## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 diff --git a/docs/sdk/metrics.aggregation.rst b/docs/sdk/metrics.aggregation.rst new file mode 100644 index 0000000000..add890e11a --- /dev/null +++ b/docs/sdk/metrics.aggregation.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk._metrics.aggregation +========================================== + +.. automodule:: opentelemetry.sdk._metrics.aggregation + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sdk/metrics.metric_reader.rst b/docs/sdk/metrics.metric_reader.rst new file mode 100644 index 0000000000..b2bc9423f1 --- /dev/null +++ b/docs/sdk/metrics.metric_reader.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk._metrics.metric_reader +========================================== + +.. automodule:: opentelemetry.sdk._metrics.metric_reader + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sdk/metrics.rst b/docs/sdk/metrics.rst index c79696fc44..76bb064b52 100644 --- a/docs/sdk/metrics.rst +++ b/docs/sdk/metrics.rst @@ -11,6 +11,12 @@ opentelemetry.sdk._metrics package Submodules ---------- +.. toctree:: + + metrics.view + metrics.aggregation + metrics.metric_reader + .. automodule:: opentelemetry.sdk._metrics :members: :undoc-members: diff --git a/docs/sdk/metrics.view.rst b/docs/sdk/metrics.view.rst new file mode 100644 index 0000000000..b2f78b9bf2 --- /dev/null +++ b/docs/sdk/metrics.view.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk._metrics.view +========================================== + +.. automodule:: opentelemetry.sdk._metrics.view + :members: + :undoc-members: + :show-inheritance: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 869f85d62e..90ec677251 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -148,7 +148,34 @@ def create_observable_up_down_counter( class MeterProvider(APIMeterProvider): - """See `opentelemetry._metrics.MeterProvider`.""" + r"""See `opentelemetry._metrics.MeterProvider`. + + Args: + metric_readers: Register metric readers to collect metrics from the SDK on demand. Each + `MetricReader` is completely independent and will collect separate streams of + metrics. TODO: reference ``PeriodicExportingMetricReader`` usage with push + exporters here. + resource: The resource representing what the metrics emitted from the SDK pertain to. + shutdown_on_exit: If true, registers an `atexit` handler to call + `MeterProvider.shutdown` + views: The views to configure the metric output the SDK + + By default, instruments which do not match any `View` (or if no `View`\ s are provided) + will report metrics with the default aggregation for the instrument's kind. To disable + instruments by default, configure a match-all `View` with `DropAggregation` and then create + `View`\ s to re-enable individual instruments: + + .. code-block:: python + :caption: Disable default views + + MeterProvider( + views=[ + View(instrument_name="*", aggregation=DropAggregation()), + View(instrument_name="mycounter"), + ], + # ... + ) + """ def __init__( self, @@ -156,7 +183,6 @@ def __init__( resource: Resource = Resource.create({}), shutdown_on_exit: bool = True, views: Sequence[View] = (), - enable_default_view: bool = True, ): self._lock = Lock() self._meter_lock = Lock() @@ -165,7 +191,6 @@ def __init__( resource=resource, metric_readers=metric_readers, views=views, - enable_default_view=enable_default_view, ) self._measurement_consumer = SynchronousMeasurementConsumer( sdk_config=self._sdk_config diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 420385c69c..9771833cf0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -12,6 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +.. Explicitly document private _AggregationFactory +.. autoclass:: _AggregationFactory +""" + from abc import ABC, abstractmethod from bisect import bisect_left from dataclasses import replace diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py index 951f18e09a..6b345587d6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py @@ -24,6 +24,11 @@ class MetricReader(ABC): + """ + .. document protected _receive_metrics which is a intended to be overriden by subclass + .. automethod:: _receive_metrics + """ + def __init__( self, preferred_temporality: AggregationTemporality = AggregationTemporality.CUMULATIVE, @@ -66,5 +71,8 @@ def shutdown(self) -> bool: only be shutdown once, any subsequent calls are ignored and return failure status. - When a `MetricReader` is registered on a `MeterProvider`, `MeterProvider.shutdown` will invoke this automatically. + When a `MetricReader` is registered on a + :class:`~opentelemetry.sdk._metrics.MeterProvider`, + :meth:`~opentelemetry.sdk._metrics.MeterProvider.shutdown` will invoke this + automatically. """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py index f7a2410024..45c4d4dd79 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py @@ -66,10 +66,7 @@ def _get_or_init_view_instrument_match( ) # if no view targeted the instrument, use the default - if ( - not view_instrument_matches - and self._sdk_config.enable_default_view - ): + if not view_instrument_matches: view_instrument_matches.append( _ViewInstrumentMatch( view=_DEFAULT_VIEW, @@ -104,8 +101,3 @@ def collect(self, temporality: AggregationTemporality) -> Iterable[Metric]: metrics.extend(view_instrument_match.collect(temporality)) return metrics - - -def default_view(instrument: Instrument) -> View: - # TODO: #2247 - return View() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py index c617696717..2c603b5e4d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py @@ -11,4 +11,3 @@ class SdkConfiguration: resource: Resource metric_readers: Sequence[MetricReader] views: Sequence[View] - enable_default_view: bool diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py index 7cdcb1ca27..faecd77d3a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py @@ -44,35 +44,33 @@ class View: instrument must be to match the view. instrument_name: This is an instrument matching attribute: the name the - instrument must have to match the view. Wild card characters are - supported. Wild card characters should not be used with this - attribute if the view has also a ``name`` defined. + instrument must have to match the view. Wild card characters are supported. Wild + card characters should not be used with this attribute if the view has also a + ``name`` defined. meter_name: This is an instrument matching attribute: the name the instrument meter must have to match the view. - meter_version : This is an instrument matching attribute: the version + meter_version: This is an instrument matching attribute: the version the instrument meter must have to match the view. - meter_schema URL : This is an instrument matching attribute: the schema + meter_schema_url: This is an instrument matching attribute: the schema URL the instrument meter must have to match the view. name: This is a metric stream customizing attribute: the name of the metric stream. If `None`, the name of the instrument will be used. description: This is a metric stream customizing attribute: the - description of the metric stream. If `None`, the description of the - instrument will be used. + description of the metric stream. If `None`, the description of the instrument will + be used. attribute_keys: This is a metric stream customizing attribute: this is - a set of attribute keys. If not `None` then only the measurement - attributes that are in `attribute_keys` will be used to identify the - metric stream. + a set of attribute keys. If not `None` then only the measurement attributes that + are in ``attribute_keys`` will be used to identify the metric stream. aggregation: This is a metric stream customizing attribute: the - aggregatation instance to use when data is aggregated for the - corresponding metrics stream. If `None` the default aggregation of - the instrument will be used. + aggregatation instance to use when data is aggregated for the corresponding metrics + stream. If `None` the default aggregation of the instrument will be used. This class is not intended to be subclassed by the user. """ diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py b/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py new file mode 100644 index 0000000000..cb2ab8b54c --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py @@ -0,0 +1,57 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +from opentelemetry.sdk._metrics import MeterProvider +from opentelemetry.sdk._metrics.aggregation import DropAggregation +from opentelemetry.sdk._metrics.export import InMemoryMetricReader +from opentelemetry.sdk._metrics.view import View + + +class TestDisableDefaultViews(TestCase): + def test_disable_default_views(self): + reader = InMemoryMetricReader() + meter_provider = MeterProvider( + metric_readers=[reader], + views=[View(instrument_name="*", aggregation=DropAggregation())], + ) + meter = meter_provider.get_meter("testmeter") + counter = meter.create_counter("testcounter") + counter.add(10, {"label": "value1"}) + counter.add(10, {"label": "value2"}) + counter.add(10, {"label": "value3"}) + + self.assertEqual(reader.get_metrics(), []) + + def test_disable_default_views_add_custom(self): + reader = InMemoryMetricReader() + meter_provider = MeterProvider( + metric_readers=[reader], + views=[ + View(instrument_name="*", aggregation=DropAggregation()), + View(instrument_name="testhist"), + ], + ) + meter = meter_provider.get_meter("testmeter") + counter = meter.create_counter("testcounter") + histogram = meter.create_histogram("testhist") + counter.add(10, {"label": "value1"}) + counter.add(10, {"label": "value2"}) + counter.add(10, {"label": "value3"}) + histogram.record(12, {"label": "value"}) + + metrics = reader.get_metrics() + self.assertEqual(len(metrics), 1) + self.assertEqual(metrics[0].name, "testhist") diff --git a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py index 9e2416a7c1..3b58ba4488 100644 --- a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py +++ b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py @@ -39,7 +39,6 @@ def test_creates_metric_reader_storages(self, MockMetricReaderStorage): resource=Mock(), metric_readers=reader_mocks, views=Mock(), - enable_default_view=Mock(), ) ) self.assertEqual(len(MockMetricReaderStorage.mock_calls), 5) @@ -56,7 +55,6 @@ def test_measurements_passed_to_each_reader_storage( resource=Mock(), metric_readers=reader_mocks, views=Mock(), - enable_default_view=Mock(), ) ) measurement_mock = Mock() @@ -78,7 +76,6 @@ def test_collect_passed_to_reader_stage(self, MockMetricReaderStorage): resource=Mock(), metric_readers=reader_mocks, views=Mock(), - enable_default_view=Mock(), ) ) for r_mock, rs_mock in zip(reader_mocks, reader_storage_mocks): @@ -99,7 +96,6 @@ def test_collect_calls_async_instruments(self, MockMetricReaderStorage): resource=Mock(), metric_readers=[reader_mock], views=Mock(), - enable_default_view=Mock(), ) ) async_instrument_mocks = [MagicMock() for _ in range(5)] diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index d73cf893a9..f7ffc6993a 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -56,7 +56,6 @@ def test_creates_view_instrument_matches( resource=Mock(), metric_readers=(), views=(view1, view2), - enable_default_view=True, ) ) @@ -101,7 +100,6 @@ def test_forwards_calls_to_view_instrument_match( resource=Mock(), metric_readers=(), views=(view1, view2), - enable_default_view=True, ) ) @@ -149,7 +147,6 @@ def test_race_concurrent_measurements(self, MockViewInstrumentMatch: Mock): resource=Mock(), metric_readers=(), views=(view1,), - enable_default_view=True, ) ) @@ -175,7 +172,6 @@ def test_default_view_enabled(self, MockViewInstrumentMatch: Mock): resource=Mock(), metric_readers=(), views=(), - enable_default_view=True, ) ) @@ -192,35 +188,6 @@ def test_default_view_enabled(self, MockViewInstrumentMatch: Mock): storage.consume_measurement(Measurement(1, instrument2)) self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 1) - @patch( - "opentelemetry.sdk._metrics.metric_reader_storage._ViewInstrumentMatch" - ) - def test_default_view_disabled(self, MockViewInstrumentMatch: Mock): - """Instruments should not be matched with default views when disabled""" - instrument1 = Mock(name="instrument1") - instrument2 = Mock(name="instrument2") - storage = MetricReaderStorage( - SdkConfiguration( - resource=Mock(), - metric_readers=(), - views=(), - enable_default_view=False, - ) - ) - - storage.consume_measurement(Measurement(1, instrument1)) - self.assertEqual( - len(MockViewInstrumentMatch.call_args_list), - 0, - MockViewInstrumentMatch.mock_calls, - ) - storage.consume_measurement(Measurement(1, instrument1)) - self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 0) - - MockViewInstrumentMatch.call_args_list.clear() - storage.consume_measurement(Measurement(1, instrument2)) - self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 0) - def test_drop_aggregation(self): counter = Counter("name", Mock(), Mock()) @@ -233,7 +200,6 @@ def test_drop_aggregation(self): instrument_name="name", aggregation=DropAggregation() ), ), - enable_default_view=False, ) ) metric_reader_storage.consume_measurement(Measurement(1, counter)) diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 210963d547..6edba9b4eb 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -46,7 +46,6 @@ def test_consume_measurement(self): resource=self.mock_resource, metric_readers=[], views=[], - enable_default_view=True, ) view_instrument_match = _ViewInstrumentMatch( view=View( @@ -164,7 +163,6 @@ def test_collect(self): resource=self.mock_resource, metric_readers=[], views=[], - enable_default_view=True, ) view_instrument_match = _ViewInstrumentMatch( view=View( From 9087d23d1921e765306820e1d7d4c947f673a656 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 28 Mar 2022 18:09:05 -0600 Subject: [PATCH 1174/1517] Add default aggregation (#2543) * Add default aggregation Fixes #2537 * Add changelog * Raise exception * Fix lint * Small fixes * Add more fixes --- CHANGELOG.md | 2 + .../sdk/_metrics/_view_instrument_match.py | 14 +-- .../opentelemetry/sdk/_metrics/aggregation.py | 92 +++++++++++--- .../opentelemetry/sdk/_metrics/instrument.py | 55 +-------- .../src/opentelemetry/sdk/_metrics/view.py | 14 ++- .../tests/metrics/test_aggregation.py | 116 +++++++++++++++--- 6 files changed, 198 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f9a93cca3..648e72d5f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.10.0-0.29b0...HEAD) +- Add default aggregation + ([#2543](https://github.com/open-telemetry/opentelemetry-python/pull/2543)) - Fix incorrect installation of some exporter “convenience” packages into “site-packages/src” ([#2525](https://github.com/open-telemetry/opentelemetry-python/pull/2525)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py index 385a8516d1..b124f77318 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py @@ -67,15 +67,11 @@ def consume_measurement(self, measurement: Measurement) -> None: if attributes not in self._attributes_aggregation: with self._lock: if attributes not in self._attributes_aggregation: - if self._view._aggregation: - aggregation = ( - self._view._aggregation._create_aggregation( - self._instrument - ) - ) - else: - aggregation = self._instrument._default_aggregation - self._attributes_aggregation[attributes] = aggregation + self._attributes_aggregation[ + attributes + ] = self._view._aggregation._create_aggregation( + self._instrument + ) self._attributes_aggregation[attributes].aggregate(measurement) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 9771833cf0..4471e5f3f4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -27,18 +27,20 @@ from opentelemetry._metrics.instrument import ( Asynchronous, + Counter, + Histogram, Instrument, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, Synchronous, + UpDownCounter, _Monotonic, ) from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import ( - AggregationTemporality, - Gauge, - Histogram, - PointT, - Sum, -) +from opentelemetry.sdk._metrics.point import AggregationTemporality, Gauge +from opentelemetry.sdk._metrics.point import Histogram as HistogramPoint +from opentelemetry.sdk._metrics.point import PointT, Sum from opentelemetry.util._time import _time_ns _PointVarT = TypeVar("_PointVarT", bound=PointT) @@ -67,6 +69,66 @@ def collect(self) -> Optional[_PointVarT]: pass +class _AggregationFactory(ABC): + @abstractmethod + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + """Creates an aggregation""" + + +class DefaultAggregation(_AggregationFactory): + """ + The default aggregation to be used in a `View`. + + This aggregation will create an actual aggregation depending on the + instrument type, as specified next: + + ========================= ==================================== + Instrument Aggregation + ========================= ==================================== + `Counter` `SumAggregation` + `UpDownCounter` `SumAggregation` + `ObservableCounter` `SumAggregation` + `ObservableUpDownCounter` `SumAggregation` + `Histogram` `ExplicitBucketHistogramAggregation` + `ObservableGauge` `LastValueAggregation` + ========================= ==================================== + """ + + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + + # pylint: disable=too-many-return-statements + if isinstance(instrument, Counter): + return _SumAggregation( + instrument_is_monotonic=True, + instrument_temporality=AggregationTemporality.DELTA, + ) + if isinstance(instrument, UpDownCounter): + return _SumAggregation( + instrument_is_monotonic=False, + instrument_temporality=AggregationTemporality.DELTA, + ) + + if isinstance(instrument, ObservableCounter): + return _SumAggregation( + instrument_is_monotonic=True, + instrument_temporality=AggregationTemporality.CUMULATIVE, + ) + + if isinstance(instrument, ObservableUpDownCounter): + return _SumAggregation( + instrument_is_monotonic=False, + instrument_temporality=AggregationTemporality.CUMULATIVE, + ) + + if isinstance(instrument, Histogram): + return _ExplicitBucketHistogramAggregation() + + if isinstance(instrument, ObservableGauge): + return _LastValueAggregation() + + raise Exception(f"Invalid instrument type {type(instrument)} found") + + class _SumAggregation(_Aggregation[Sum]): def __init__( self, @@ -154,7 +216,7 @@ def collect(self) -> Optional[Gauge]: ) -class _ExplicitBucketHistogramAggregation(_Aggregation[Histogram]): +class _ExplicitBucketHistogramAggregation(_Aggregation[HistogramPoint]): def __init__( self, boundaries: Sequence[float] = ( @@ -195,7 +257,7 @@ def aggregate(self, measurement: Measurement) -> None: self._bucket_counts[bisect_left(self._boundaries, value)] += 1 - def collect(self) -> Histogram: + def collect(self) -> HistogramPoint: """ Atomically return a point for the current value of the metric. """ @@ -210,7 +272,7 @@ def collect(self) -> Histogram: self._start_time_unix_nano = now + 1 self._sum = 0 - return Histogram( + return HistogramPoint( start_time_unix_nano=start_time_unix_nano, time_unix_nano=now, bucket_counts=tuple(value), @@ -295,7 +357,7 @@ def _convert_aggregation_temporality( is_monotonic=is_monotonic, ) - if current_point_type is Histogram: + if current_point_type is HistogramPoint: if previous_point is None: return replace( current_point, aggregation_temporality=aggregation_temporality @@ -329,7 +391,7 @@ def _convert_aggregation_temporality( ) ] - return Histogram( + return HistogramPoint( start_time_unix_nano=start_time_unix_nano, time_unix_nano=current_point.time_unix_nano, bucket_counts=bucket_counts, @@ -340,12 +402,6 @@ def _convert_aggregation_temporality( return None -class _AggregationFactory(ABC): - @abstractmethod - def _create_aggregation(self, instrument: Instrument) -> _Aggregation: - """Creates an aggregation""" - - class ExplicitBucketHistogramAggregation(_AggregationFactory): def __init__( self, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index 990684b817..f2ac050d97 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -15,7 +15,6 @@ # pylint: disable=too-many-ancestors import logging -from abc import ABC, abstractmethod from typing import Callable, Dict, Generator, Iterable, Union from opentelemetry._metrics.instrument import CallbackT @@ -32,28 +31,14 @@ ) from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter from opentelemetry._metrics.measurement import Measurement as APIMeasurement -from opentelemetry.sdk._metrics.aggregation import ( - _Aggregation, - _ExplicitBucketHistogramAggregation, - _LastValueAggregation, - _SumAggregation, -) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.measurement_consumer import MeasurementConsumer -from opentelemetry.sdk._metrics.point import AggregationTemporality from opentelemetry.sdk.util.instrumentation import InstrumentationInfo _logger = logging.getLogger(__name__) -class _Instrument(ABC): - @property - @abstractmethod - def _default_aggregation(self) -> _Aggregation: - pass - - -class _Synchronous(_Instrument): +class _Synchronous: def __init__( self, name: str, @@ -70,7 +55,7 @@ def __init__( super().__init__(name, unit=unit, description=description) -class _Asynchronous(_Instrument): +class _Asynchronous: def __init__( self, name: str, @@ -108,13 +93,6 @@ def callback(self) -> Iterable[Measurement]: class Counter(_Synchronous, APICounter): - @property - def _default_aggregation(self) -> _Aggregation: - return _SumAggregation( - instrument_is_monotonic=True, - instrument_temporality=AggregationTemporality.DELTA, - ) - def add( self, amount: Union[int, float], attributes: Dict[str, str] = None ): @@ -129,13 +107,6 @@ def add( class UpDownCounter(_Synchronous, APIUpDownCounter): - @property - def _default_aggregation(self) -> _Aggregation: - return _SumAggregation( - instrument_is_monotonic=False, - instrument_temporality=AggregationTemporality.DELTA, - ) - def add( self, amount: Union[int, float], attributes: Dict[str, str] = None ): @@ -145,28 +116,14 @@ def add( class ObservableCounter(_Asynchronous, APIObservableCounter): - @property - def _default_aggregation(self) -> _Aggregation: - return _SumAggregation( - instrument_is_monotonic=True, - instrument_temporality=AggregationTemporality.CUMULATIVE, - ) + pass class ObservableUpDownCounter(_Asynchronous, APIObservableUpDownCounter): - @property - def _default_aggregation(self) -> _Aggregation: - return _SumAggregation( - instrument_is_monotonic=False, - instrument_temporality=AggregationTemporality.CUMULATIVE, - ) + pass class Histogram(_Synchronous, APIHistogram): - @property - def _default_aggregation(self) -> _Aggregation: - return _ExplicitBucketHistogramAggregation() - def record( self, amount: Union[int, float], attributes: Dict[str, str] = None ): @@ -182,6 +139,4 @@ def record( class ObservableGauge(_Asynchronous, APIObservableGauge): - @property - def _default_aggregation(self) -> _Aggregation: - return _LastValueAggregation() + pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py index faecd77d3a..d83ce47cf6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py @@ -21,7 +21,10 @@ from typing_extensions import final from opentelemetry._metrics.instrument import Instrument -from opentelemetry.sdk._metrics.aggregation import _AggregationFactory +from opentelemetry.sdk._metrics.aggregation import ( + DefaultAggregation, + _AggregationFactory, +) _logger = getLogger(__name__) @@ -69,12 +72,15 @@ class View: are in ``attribute_keys`` will be used to identify the metric stream. aggregation: This is a metric stream customizing attribute: the - aggregatation instance to use when data is aggregated for the corresponding metrics - stream. If `None` the default aggregation of the instrument will be used. + aggregation instance to use when data is aggregated for the + corresponding metrics stream. If `None` an instance of + `DefaultAggregation` will be used. This class is not intended to be subclassed by the user. """ + _default_aggregation = DefaultAggregation() + def __init__( self, instrument_type: Optional[Type[Instrument]] = None, @@ -122,7 +128,7 @@ def __init__( self._description = description self._attribute_keys = attribute_keys - self._aggregation = aggregation + self._aggregation = aggregation or self._default_aggregation # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 414939e30c..675f5775cf 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -23,6 +23,7 @@ from opentelemetry.sdk._metrics import instrument from opentelemetry.sdk._metrics.aggregation import ( AggregationTemporality, + DefaultAggregation, ExplicitBucketHistogramAggregation, LastValueAggregation, SumAggregation, @@ -31,8 +32,18 @@ _LastValueAggregation, _SumAggregation, ) +from opentelemetry.sdk._metrics.instrument import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import Gauge, Histogram, Sum +from opentelemetry.sdk._metrics.point import Gauge +from opentelemetry.sdk._metrics.point import Histogram as HistogramPoint +from opentelemetry.sdk._metrics.point import Sum from opentelemetry.util.types import Attributes @@ -601,7 +612,7 @@ def test_current_point_gauge(self): class TestHistogramConvertAggregationTemporality(TestCase): def test_previous_point_none(self): - current_point = Histogram( + current_point = HistogramPoint( start_time_unix_nano=0, time_unix_nano=1, bucket_counts=[0, 2, 1, 2, 0], @@ -625,7 +636,7 @@ def test_previous_point_non_cumulative(self): with self.assertRaises(Exception): _convert_aggregation_temporality( - Histogram( + HistogramPoint( start_time_unix_nano=0, time_unix_nano=1, bucket_counts=[0, 2, 1, 2, 0], @@ -633,7 +644,7 @@ def test_previous_point_non_cumulative(self): sum=70, aggregation_temporality=AggregationTemporality.DELTA, ), - Histogram( + HistogramPoint( start_time_unix_nano=1, time_unix_nano=2, bucket_counts=[0, 1, 3, 0, 0], @@ -645,7 +656,7 @@ def test_previous_point_non_cumulative(self): ), def test_same_aggregation_temporality_cumulative(self): - current_point = Histogram( + current_point = HistogramPoint( start_time_unix_nano=0, time_unix_nano=2, bucket_counts=[0, 3, 4, 2, 0], @@ -655,7 +666,7 @@ def test_same_aggregation_temporality_cumulative(self): ) self.assertEqual( _convert_aggregation_temporality( - Histogram( + HistogramPoint( start_time_unix_nano=0, time_unix_nano=1, bucket_counts=[0, 2, 1, 2, 0], @@ -670,7 +681,7 @@ def test_same_aggregation_temporality_cumulative(self): ) def test_same_aggregation_temporality_delta(self): - current_point = Histogram( + current_point = HistogramPoint( start_time_unix_nano=1, time_unix_nano=2, bucket_counts=[0, 1, 3, 0, 0], @@ -681,7 +692,7 @@ def test_same_aggregation_temporality_delta(self): self.assertEqual( _convert_aggregation_temporality( - Histogram( + HistogramPoint( start_time_unix_nano=0, time_unix_nano=2, bucket_counts=[0, 3, 4, 2, 0], @@ -696,7 +707,7 @@ def test_same_aggregation_temporality_delta(self): ) def test_aggregation_temporality_to_cumulative(self): - current_point = Histogram( + current_point = HistogramPoint( start_time_unix_nano=1, time_unix_nano=2, bucket_counts=[0, 1, 3, 0, 0], @@ -707,7 +718,7 @@ def test_aggregation_temporality_to_cumulative(self): self.assertEqual( _convert_aggregation_temporality( - Histogram( + HistogramPoint( start_time_unix_nano=0, time_unix_nano=1, bucket_counts=[0, 2, 1, 2, 0], @@ -718,7 +729,7 @@ def test_aggregation_temporality_to_cumulative(self): current_point, AggregationTemporality.CUMULATIVE, ), - Histogram( + HistogramPoint( start_time_unix_nano=0, time_unix_nano=2, bucket_counts=[0, 3, 4, 2, 0], @@ -729,7 +740,7 @@ def test_aggregation_temporality_to_cumulative(self): ) def test_aggregation_temporality_to_delta(self): - current_point = Histogram( + current_point = HistogramPoint( start_time_unix_nano=0, time_unix_nano=2, bucket_counts=[0, 3, 4, 2, 0], @@ -740,7 +751,7 @@ def test_aggregation_temporality_to_delta(self): self.assertEqual( _convert_aggregation_temporality( - Histogram( + HistogramPoint( start_time_unix_nano=0, time_unix_nano=1, bucket_counts=[0, 2, 1, 2, 0], @@ -751,7 +762,7 @@ def test_aggregation_temporality_to_delta(self): current_point, AggregationTemporality.DELTA, ), - Histogram( + HistogramPoint( start_time_unix_nano=1, time_unix_nano=2, bucket_counts=[0, 1, 3, 0, 0], @@ -817,3 +828,80 @@ def test_last_value_factory(self): self.assertIsInstance(aggregation, _LastValueAggregation) aggregation2 = factory._create_aggregation(counter) self.assertNotEqual(aggregation, aggregation2) + + +class TestDefaultAggregation(TestCase): + @classmethod + def setUpClass(cls): + cls.default_aggregation = DefaultAggregation() + + def test_counter(self): + + aggregation = self.default_aggregation._create_aggregation( + Counter(Mock(), Mock(), Mock()) + ) + self.assertIsInstance(aggregation, _SumAggregation) + self.assertTrue(aggregation._instrument_is_monotonic) + self.assertEqual( + aggregation._instrument_temporality, AggregationTemporality.DELTA + ) + + def test_up_down_counter(self): + + aggregation = self.default_aggregation._create_aggregation( + UpDownCounter(Mock(), Mock(), Mock()) + ) + self.assertIsInstance(aggregation, _SumAggregation) + self.assertFalse(aggregation._instrument_is_monotonic) + self.assertEqual( + aggregation._instrument_temporality, AggregationTemporality.DELTA + ) + + def test_observable_counter(self): + + aggregation = self.default_aggregation._create_aggregation( + ObservableCounter(Mock(), Mock(), Mock(), Mock()) + ) + self.assertIsInstance(aggregation, _SumAggregation) + self.assertTrue(aggregation._instrument_is_monotonic) + self.assertEqual( + aggregation._instrument_temporality, + AggregationTemporality.CUMULATIVE, + ) + + def test_observable_up_down_counter(self): + + aggregation = self.default_aggregation._create_aggregation( + ObservableUpDownCounter(Mock(), Mock(), Mock(), Mock()) + ) + self.assertIsInstance(aggregation, _SumAggregation) + self.assertFalse(aggregation._instrument_is_monotonic) + self.assertEqual( + aggregation._instrument_temporality, + AggregationTemporality.CUMULATIVE, + ) + + def test_histogram(self): + + aggregation = self.default_aggregation._create_aggregation( + Histogram( + Mock(), + Mock(), + Mock(), + Mock(), + Mock(), + ) + ) + self.assertIsInstance(aggregation, _ExplicitBucketHistogramAggregation) + + def test_observable_gauge(self): + + aggregation = self.default_aggregation._create_aggregation( + ObservableGauge( + Mock(), + Mock(), + Mock(), + Mock(), + ) + ) + self.assertIsInstance(aggregation, _LastValueAggregation) From a19f80a8b69b56281bb720cbe93a85c27e9056c7 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 29 Mar 2022 09:14:06 +0530 Subject: [PATCH 1175/1517] Update opentelemetry-proto to v0.15.0 (#2566) --- CHANGELOG.md | 2 + opentelemetry-proto/README.rst | 4 +- .../collector/logs/v1/logs_service_pb2.py | 4 +- .../metrics/v1/metrics_service_pb2.py | 4 +- .../collector/trace/v1/trace_service_pb2.py | 4 +- .../proto/common/v1/common_pb2.py | 48 +++--- .../proto/common/v1/common_pb2.pyi | 37 +++-- .../opentelemetry/proto/logs/v1/logs_pb2.py | 122 +++++++++++--- .../opentelemetry/proto/logs/v1/logs_pb2.pyi | 109 +++++++++++-- .../proto/metrics/v1/metrics_pb2.py | 150 +++++++++++++----- .../proto/metrics/v1/metrics_pb2.pyi | 90 ++++++++++- .../proto/resource/v1/resource_pb2.py | 4 +- .../proto/resource/v1/resource_pb2.pyi | 5 +- .../proto/trace/v1/trace_config_pb2.py | 4 +- .../opentelemetry/proto/trace/v1/trace_pb2.py | 106 ++++++++++--- .../proto/trace/v1/trace_pb2.pyi | 90 ++++++++++- scripts/proto_codegen.sh | 2 +- 17 files changed, 626 insertions(+), 159 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 648e72d5f5..305f9f9201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2540](https://github.com/open-telemetry/opentelemetry-python/pull/2540)) - Drop the usage of name field from log model in OTLP ([#2565](https://github.com/open-telemetry/opentelemetry-python/pull/2565)) +- Update opentelemetry-proto to v0.15.0 + ([#2566](https://github.com/open-telemetry/opentelemetry-python/pull/2566)) - Remove `enable_default_view` option from sdk MeterProvider ([#2547](https://github.com/open-telemetry/opentelemetry-python/pull/2547)) diff --git a/opentelemetry-proto/README.rst b/opentelemetry-proto/README.rst index 03c53dbb67..bb3a1e9796 100644 --- a/opentelemetry-proto/README.rst +++ b/opentelemetry-proto/README.rst @@ -7,9 +7,9 @@ OpenTelemetry Python Proto :target: https://pypi.org/project/opentelemetry-proto/ This library contains the generated code for OpenTelemetry protobuf data model. The code in the current -package was generated using the v0.9.0 release_ of opentelemetry-proto. +package was generated using the v0.15.0 release_ of opentelemetry-proto. -.. _release: https://github.com/open-telemetry/opentelemetry-proto/releases/tag/v0.9.0 +.. _release: https://github.com/open-telemetry/opentelemetry-proto/releases/tag/v0.15.0 Installation ------------ diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py index e315109d13..d439d077b3 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py @@ -18,9 +18,9 @@ name='opentelemetry/proto/collector/logs/v1/logs_service.proto', package='opentelemetry.proto.collector.logs.v1', syntax='proto3', - serialized_options=b'\n(io.opentelemetry.proto.collector.logs.v1B\020LogsServiceProtoP\001ZFgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/logs/v1', + serialized_options=b'\n(io.opentelemetry.proto.collector.logs.v1B\020LogsServiceProtoP\001Z0go.opentelemetry.io/proto/otlp/collector/logs/v1', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n8opentelemetry/proto/collector/logs/v1/logs_service.proto\x12%opentelemetry.proto.collector.logs.v1\x1a&opentelemetry/proto/logs/v1/logs.proto\"\\\n\x18\x45xportLogsServiceRequest\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\x1b\n\x19\x45xportLogsServiceResponse2\x9d\x01\n\x0bLogsService\x12\x8d\x01\n\x06\x45xport\x12?.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest\x1a@.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse\"\x00\x42\x86\x01\n(io.opentelemetry.proto.collector.logs.v1B\x10LogsServiceProtoP\x01ZFgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/logs/v1b\x06proto3' + serialized_pb=b'\n8opentelemetry/proto/collector/logs/v1/logs_service.proto\x12%opentelemetry.proto.collector.logs.v1\x1a&opentelemetry/proto/logs/v1/logs.proto\"\\\n\x18\x45xportLogsServiceRequest\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\x1b\n\x19\x45xportLogsServiceResponse2\x9d\x01\n\x0bLogsService\x12\x8d\x01\n\x06\x45xport\x12?.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest\x1a@.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse\"\x00\x42p\n(io.opentelemetry.proto.collector.logs.v1B\x10LogsServiceProtoP\x01Z0go.opentelemetry.io/proto/otlp/collector/logs/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_logs_dot_v1_dot_logs__pb2.DESCRIPTOR,]) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py index ba3c7902ff..2fb3bf86de 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py @@ -18,9 +18,9 @@ name='opentelemetry/proto/collector/metrics/v1/metrics_service.proto', package='opentelemetry.proto.collector.metrics.v1', syntax='proto3', - serialized_options=b'\n+io.opentelemetry.proto.collector.metrics.v1B\023MetricsServiceProtoP\001ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/metrics/v1', + serialized_options=b'\n+io.opentelemetry.proto.collector.metrics.v1B\023MetricsServiceProtoP\001Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n>opentelemetry/proto/collector/metrics/v1/metrics_service.proto\x12(opentelemetry.proto.collector.metrics.v1\x1a,opentelemetry/proto/metrics/v1/metrics.proto\"h\n\x1b\x45xportMetricsServiceRequest\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\x1e\n\x1c\x45xportMetricsServiceResponse2\xac\x01\n\x0eMetricsService\x12\x99\x01\n\x06\x45xport\x12\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest\x1a\x46.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse\"\x00\x42\x8f\x01\n+io.opentelemetry.proto.collector.metrics.v1B\x13MetricsServiceProtoP\x01ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/metrics/v1b\x06proto3' + serialized_pb=b'\n>opentelemetry/proto/collector/metrics/v1/metrics_service.proto\x12(opentelemetry.proto.collector.metrics.v1\x1a,opentelemetry/proto/metrics/v1/metrics.proto\"h\n\x1b\x45xportMetricsServiceRequest\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\x1e\n\x1c\x45xportMetricsServiceResponse2\xac\x01\n\x0eMetricsService\x12\x99\x01\n\x06\x45xport\x12\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest\x1a\x46.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse\"\x00\x42y\n+io.opentelemetry.proto.collector.metrics.v1B\x13MetricsServiceProtoP\x01Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_metrics_dot_v1_dot_metrics__pb2.DESCRIPTOR,]) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py index 4648414b79..7d988f1117 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py @@ -18,9 +18,9 @@ name='opentelemetry/proto/collector/trace/v1/trace_service.proto', package='opentelemetry.proto.collector.trace.v1', syntax='proto3', - serialized_options=b'\n)io.opentelemetry.proto.collector.trace.v1B\021TraceServiceProtoP\001ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1', + serialized_options=b'\n)io.opentelemetry.proto.collector.trace.v1B\021TraceServiceProtoP\001Z1go.opentelemetry.io/proto/otlp/collector/trace/v1', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n:opentelemetry/proto/collector/trace/v1/trace_service.proto\x12&opentelemetry.proto.collector.trace.v1\x1a(opentelemetry/proto/trace/v1/trace.proto\"`\n\x19\x45xportTraceServiceRequest\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\x1c\n\x1a\x45xportTraceServiceResponse2\xa2\x01\n\x0cTraceService\x12\x91\x01\n\x06\x45xport\x12\x41.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\x1a\x42.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse\"\x00\x42\x89\x01\n)io.opentelemetry.proto.collector.trace.v1B\x11TraceServiceProtoP\x01ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1b\x06proto3' + serialized_pb=b'\n:opentelemetry/proto/collector/trace/v1/trace_service.proto\x12&opentelemetry.proto.collector.trace.v1\x1a(opentelemetry/proto/trace/v1/trace.proto\"`\n\x19\x45xportTraceServiceRequest\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\x1c\n\x1a\x45xportTraceServiceResponse2\xa2\x01\n\x0cTraceService\x12\x91\x01\n\x06\x45xport\x12\x41.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\x1a\x42.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse\"\x00\x42s\n)io.opentelemetry.proto.collector.trace.v1B\x11TraceServiceProtoP\x01Z1go.opentelemetry.io/proto/otlp/collector/trace/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_trace_dot_v1_dot_trace__pb2.DESCRIPTOR,]) diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py index 4578f9409f..ba7d33ef61 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py @@ -17,9 +17,9 @@ name='opentelemetry/proto/common/v1/common.proto', package='opentelemetry.proto.common.v1', syntax='proto3', - serialized_options=b'\n io.opentelemetry.proto.common.v1B\013CommonProtoP\001Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1', + serialized_options=b'\n io.opentelemetry.proto.common.v1B\013CommonProtoP\001Z(go.opentelemetry.io/proto/otlp/common/v1', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\x8c\x02\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"O\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"0\n\x0eStringKeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x18\x01\"7\n\x16InstrumentationLibrary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\tBq\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z>github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1b\x06proto3' + serialized_pb=b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\x8c\x02\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"O\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\";\n\x16InstrumentationLibrary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t:\x02\x18\x01\"5\n\x14InstrumentationScope\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\tB[\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z(go.opentelemetry.io/proto/otlp/common/v1b\x06proto3' ) @@ -207,23 +207,23 @@ ) -_STRINGKEYVALUE = _descriptor.Descriptor( - name='StringKeyValue', - full_name='opentelemetry.proto.common.v1.StringKeyValue', +_INSTRUMENTATIONLIBRARY = _descriptor.Descriptor( + name='InstrumentationLibrary', + full_name='opentelemetry.proto.common.v1.InstrumentationLibrary', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( - name='key', full_name='opentelemetry.proto.common.v1.StringKeyValue.key', index=0, + name='name', full_name='opentelemetry.proto.common.v1.InstrumentationLibrary.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='value', full_name='opentelemetry.proto.common.v1.StringKeyValue.value', index=1, + name='version', full_name='opentelemetry.proto.common.v1.InstrumentationLibrary.version', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, @@ -242,27 +242,27 @@ oneofs=[ ], serialized_start=573, - serialized_end=621, + serialized_end=632, ) -_INSTRUMENTATIONLIBRARY = _descriptor.Descriptor( - name='InstrumentationLibrary', - full_name='opentelemetry.proto.common.v1.InstrumentationLibrary', +_INSTRUMENTATIONSCOPE = _descriptor.Descriptor( + name='InstrumentationScope', + full_name='opentelemetry.proto.common.v1.InstrumentationScope', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( - name='name', full_name='opentelemetry.proto.common.v1.InstrumentationLibrary.name', index=0, + name='name', full_name='opentelemetry.proto.common.v1.InstrumentationScope.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='version', full_name='opentelemetry.proto.common.v1.InstrumentationLibrary.version', index=1, + name='version', full_name='opentelemetry.proto.common.v1.InstrumentationScope.version', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, @@ -280,8 +280,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=623, - serialized_end=678, + serialized_start=634, + serialized_end=687, ) _ANYVALUE.fields_by_name['array_value'].message_type = _ARRAYVALUE @@ -314,8 +314,8 @@ DESCRIPTOR.message_types_by_name['ArrayValue'] = _ARRAYVALUE DESCRIPTOR.message_types_by_name['KeyValueList'] = _KEYVALUELIST DESCRIPTOR.message_types_by_name['KeyValue'] = _KEYVALUE -DESCRIPTOR.message_types_by_name['StringKeyValue'] = _STRINGKEYVALUE DESCRIPTOR.message_types_by_name['InstrumentationLibrary'] = _INSTRUMENTATIONLIBRARY +DESCRIPTOR.message_types_by_name['InstrumentationScope'] = _INSTRUMENTATIONSCOPE _sym_db.RegisterFileDescriptor(DESCRIPTOR) AnyValue = _reflection.GeneratedProtocolMessageType('AnyValue', (_message.Message,), { @@ -346,13 +346,6 @@ }) _sym_db.RegisterMessage(KeyValue) -StringKeyValue = _reflection.GeneratedProtocolMessageType('StringKeyValue', (_message.Message,), { - 'DESCRIPTOR' : _STRINGKEYVALUE, - '__module__' : 'opentelemetry.proto.common.v1.common_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.common.v1.StringKeyValue) - }) -_sym_db.RegisterMessage(StringKeyValue) - InstrumentationLibrary = _reflection.GeneratedProtocolMessageType('InstrumentationLibrary', (_message.Message,), { 'DESCRIPTOR' : _INSTRUMENTATIONLIBRARY, '__module__' : 'opentelemetry.proto.common.v1.common_pb2' @@ -360,7 +353,14 @@ }) _sym_db.RegisterMessage(InstrumentationLibrary) +InstrumentationScope = _reflection.GeneratedProtocolMessageType('InstrumentationScope', (_message.Message,), { + 'DESCRIPTOR' : _INSTRUMENTATIONSCOPE, + '__module__' : 'opentelemetry.proto.common.v1.common_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.common.v1.InstrumentationScope) + }) +_sym_db.RegisterMessage(InstrumentationScope) + DESCRIPTOR._options = None -_STRINGKEYVALUE._options = None +_INSTRUMENTATIONLIBRARY._options = None # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi index 54789ef893..0d1ff4f098 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi @@ -78,6 +78,8 @@ class KeyValueList(google.protobuf.message.Message): def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___KeyValue]: """A collection of key/value pairs of key-value pairs. The list may be empty (may contain 0 elements). + The keys MUST be unique (it is not allowed to have more than one + value with the same key). """ pass def __init__(self, @@ -106,32 +108,37 @@ class KeyValue(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal["key",b"key","value",b"value"]) -> None: ... global___KeyValue = KeyValue -class StringKeyValue(google.protobuf.message.Message): - """StringKeyValue is a pair of key/value strings. This is the simpler (and faster) version - of KeyValue that only supports string values. +class InstrumentationLibrary(google.protobuf.message.Message): + """InstrumentationLibrary is a message representing the instrumentation library information + such as the fully qualified name and version. + InstrumentationLibrary is wire-compatible with InstrumentationScope for binary + Protobuf format. + This message is deprecated and will be removed on June 15, 2022. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - KEY_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int - key: typing.Text = ... - value: typing.Text = ... + NAME_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + name: typing.Text = ... + """An empty instrumentation library name means the name is unknown.""" + + version: typing.Text = ... def __init__(self, *, - key : typing.Text = ..., - value : typing.Text = ..., + name : typing.Text = ..., + version : typing.Text = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["key",b"key","value",b"value"]) -> None: ... -global___StringKeyValue = StringKeyValue + def ClearField(self, field_name: typing_extensions.Literal["name",b"name","version",b"version"]) -> None: ... +global___InstrumentationLibrary = InstrumentationLibrary -class InstrumentationLibrary(google.protobuf.message.Message): - """InstrumentationLibrary is a message representing the instrumentation library information +class InstrumentationScope(google.protobuf.message.Message): + """InstrumentationScope is a message representing the instrumentation scope information such as the fully qualified name and version. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... NAME_FIELD_NUMBER: builtins.int VERSION_FIELD_NUMBER: builtins.int name: typing.Text = ... - """An empty instrumentation library name means the name is unknown.""" + """An empty instrumentation scope name means the name is unknown.""" version: typing.Text = ... def __init__(self, @@ -140,4 +147,4 @@ class InstrumentationLibrary(google.protobuf.message.Message): version : typing.Text = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["name",b"name","version",b"version"]) -> None: ... -global___InstrumentationLibrary = InstrumentationLibrary +global___InstrumentationScope = InstrumentationScope diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py index 6e16d73da1..a1569de0da 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py @@ -20,9 +20,9 @@ name='opentelemetry/proto/logs/v1/logs.proto', package='opentelemetry.proto.logs.v1', syntax='proto3', - serialized_options=b'\n\036io.opentelemetry.proto.logs.v1B\tLogsProtoP\001Z google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeLogs]: + """A list of ScopeLogs that originate from a resource.""" + pass + @property def instrumentation_library_logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibraryLogs]: - """A list of InstrumentationLibraryLogs that originate from a resource.""" + """A list of InstrumentationLibraryLogs that originate from a resource. + This field is deprecated and will be removed after grace period expires on June 15, 2022. + + During the grace period the following rules SHOULD be followed: + + For Binary Protobufs + ==================== + Binary Protobuf senders SHOULD NOT set instrumentation_library_logs. Instead + scope_logs SHOULD be set. + + Binary Protobuf receivers SHOULD check if instrumentation_library_logs is set + and scope_logs is not set then the value in instrumentation_library_logs + SHOULD be used instead by converting InstrumentationLibraryLogs into ScopeLogs. + If scope_logs is set then instrumentation_library_logs SHOULD be ignored. + + For JSON + ======== + JSON senders that set instrumentation_library_logs field MAY also set + scope_logs to carry the same logs, essentially double-publishing the same data. + Such double-publishing MAY be controlled by a user-settable option. + If double-publishing is not used then the senders SHOULD set scope_logs and + SHOULD NOT set instrumentation_library_logs. + + JSON receivers SHOULD check if instrumentation_library_logs is set and + scope_logs is not set then the value in instrumentation_library_logs + SHOULD be used instead by converting InstrumentationLibraryLogs into ScopeLogs. + If scope_logs is set then instrumentation_library_logs field SHOULD be ignored. + """ pass schema_url: typing.Text = ... """This schema_url applies to the data in the "resource" field. It does not apply - to the data in the "instrumentation_library_logs" field which have their own - schema_url field. + to the data in the "scope_logs" field which have their own schema_url field. """ def __init__(self, *, resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., + scope_logs : typing.Optional[typing.Iterable[global___ScopeLogs]] = ..., instrumentation_library_logs : typing.Optional[typing.Iterable[global___InstrumentationLibraryLogs]] = ..., schema_url : typing.Text = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["resource",b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library_logs",b"instrumentation_library_logs","resource",b"resource","schema_url",b"schema_url"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library_logs",b"instrumentation_library_logs","resource",b"resource","schema_url",b"schema_url","scope_logs",b"scope_logs"]) -> None: ... global___ResourceLogs = ResourceLogs +class ScopeLogs(google.protobuf.message.Message): + """A collection of Logs produced by a Scope.""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + SCOPE_FIELD_NUMBER: builtins.int + LOG_RECORDS_FIELD_NUMBER: builtins.int + SCHEMA_URL_FIELD_NUMBER: builtins.int + @property + def scope(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationScope: + """The instrumentation scope information for the logs in this message. + Semantically when InstrumentationScope isn't set, it is equivalent with + an empty instrumentation scope name (unknown). + """ + pass + @property + def log_records(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___LogRecord]: + """A list of log records.""" + pass + schema_url: typing.Text = ... + """This schema_url applies to all logs in the "logs" field.""" + + def __init__(self, + *, + scope : typing.Optional[opentelemetry.proto.common.v1.common_pb2.InstrumentationScope] = ..., + log_records : typing.Optional[typing.Iterable[global___LogRecord]] = ..., + schema_url : typing.Text = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["scope",b"scope"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["log_records",b"log_records","schema_url",b"schema_url","scope",b"scope"]) -> None: ... +global___ScopeLogs = ScopeLogs + class InstrumentationLibraryLogs(google.protobuf.message.Message): - """A collection of Logs produced by an InstrumentationLibrary.""" + """A collection of Logs produced by an InstrumentationLibrary. + InstrumentationLibraryLogs is wire-compatible with ScopeLogs for binary + Protobuf format. + This message is deprecated and will be removed on June 15, 2022. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int LOG_RECORDS_FIELD_NUMBER: builtins.int @@ -171,7 +237,7 @@ class InstrumentationLibraryLogs(google.protobuf.message.Message): pass @property def log_records(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___LogRecord]: - """A list of log records.""" + """A list of logs that originate from an instrumentation library.""" pass schema_url: typing.Text = ... """This schema_url applies to all logs in the "logs" field.""" @@ -192,6 +258,7 @@ class LogRecord(google.protobuf.message.Message): """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + OBSERVED_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int SEVERITY_NUMBER_FIELD_NUMBER: builtins.int SEVERITY_TEXT_FIELD_NUMBER: builtins.int NAME_FIELD_NUMBER: builtins.int @@ -207,6 +274,24 @@ class LogRecord(google.protobuf.message.Message): Value of 0 indicates unknown or missing timestamp. """ + observed_time_unix_nano: builtins.int = ... + """Time when the event was observed by the collection system. + For events that originate in OpenTelemetry (e.g. using OpenTelemetry Logging SDK) + this timestamp is typically set at the generation time and is equal to Timestamp. + For events originating externally and collected by OpenTelemetry (e.g. using + Collector) this is the time when OpenTelemetry's code observed the event measured + by the clock of the OpenTelemetry code. This field MUST be set once the event is + observed by OpenTelemetry. + + For converting OpenTelemetry log data to formats that support only one timestamp or + when receiving OpenTelemetry log data by recipients that support only one timestamp + internally the following logic is recommended: + - Use time_unix_nano if it is present, otherwise use observed_time_unix_nano. + + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + Value of 0 indicates unknown or missing timestamp. + """ + severity_number: global___SeverityNumber.V = ... """Numerical value of the severity, normalized to values described in Log Data Model. [Optional]. @@ -221,6 +306,8 @@ class LogRecord(google.protobuf.message.Message): """Short event identifier that does not contain varying parts. Name describes what happened (e.g. "ProcessStarted"). Recommended to be no longer than 50 characters. Not guaranteed to be unique in any way. [Optional]. + This deprecated field is planned to be removed March 15, 2022. Receivers can + ignore this field. """ @property @@ -232,7 +319,10 @@ class LogRecord(google.protobuf.message.Message): pass @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """Additional attributes that describe the specific event occurrence. [Optional].""" + """Additional attributes that describe the specific event occurrence. [Optional]. + Attribute keys MUST be unique (it is not allowed to have more than one + attribute with the same key). + """ pass dropped_attributes_count: builtins.int = ... flags: builtins.int = ... @@ -260,6 +350,7 @@ class LogRecord(google.protobuf.message.Message): def __init__(self, *, time_unix_nano : builtins.int = ..., + observed_time_unix_nano : builtins.int = ..., severity_number : global___SeverityNumber.V = ..., severity_text : typing.Text = ..., name : typing.Text = ..., @@ -271,5 +362,5 @@ class LogRecord(google.protobuf.message.Message): span_id : builtins.bytes = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["body",b"body"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","body",b"body","dropped_attributes_count",b"dropped_attributes_count","flags",b"flags","name",b"name","severity_number",b"severity_number","severity_text",b"severity_text","span_id",b"span_id","time_unix_nano",b"time_unix_nano","trace_id",b"trace_id"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","body",b"body","dropped_attributes_count",b"dropped_attributes_count","flags",b"flags","name",b"name","observed_time_unix_nano",b"observed_time_unix_nano","severity_number",b"severity_number","severity_text",b"severity_text","span_id",b"span_id","time_unix_nano",b"time_unix_nano","trace_id",b"trace_id"]) -> None: ... global___LogRecord = LogRecord diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py index 5e3cccc105..898f3d00ab 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py @@ -20,9 +20,9 @@ name='opentelemetry/proto/metrics/v1/metrics.proto', package='opentelemetry.proto.metrics.v1', syntax='proto3', - serialized_options=b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1', + serialized_options=b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z)go.opentelemetry.io/proto/otlp/metrics/v1', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\xca\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x66\n\x1finstrumentation_library_metrics\x18\x02 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc4\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x92\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xa5\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\rJ\x04\x08\x01\x10\x02\"\x81\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*;\n\x0e\x44\x61taPointFlags\x12\r\n\tFLAG_NONE\x10\x00\x12\x1a\n\x16\x46LAG_NO_RECORDED_VALUE\x10\x01\x42t\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z?github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1b\x06proto3' + serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\x94\x02\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x43\n\rscope_metrics\x18\x02 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.ScopeMetrics\x12k\n\x1finstrumentation_library_metrics\x18\xe8\x07 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetricsB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x9f\x01\n\x0cScopeMetrics\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc8\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\x92\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xb2\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\rB\x06\n\x04_sumJ\x04\x08\x01\x10\x02\"\x81\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*;\n\x0e\x44\x61taPointFlags\x12\r\n\tFLAG_NONE\x10\x00\x12\x1a\n\x16\x46LAG_NO_RECORDED_VALUE\x10\x01\x42^\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z)go.opentelemetry.io/proto/otlp/metrics/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -51,8 +51,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=3397, - serialized_end=3537, + serialized_start=3650, + serialized_end=3790, ) _sym_db.RegisterEnumDescriptor(_AGGREGATIONTEMPORALITY) @@ -77,8 +77,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=3539, - serialized_end=3598, + serialized_start=3792, + serialized_end=3851, ) _sym_db.RegisterEnumDescriptor(_DATAPOINTFLAGS) @@ -139,14 +139,21 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='instrumentation_library_metrics', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.instrumentation_library_metrics', index=1, + name='scope_metrics', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.scope_metrics', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='schema_url', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.schema_url', index=2, + name='instrumentation_library_metrics', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.instrumentation_library_metrics', index=2, + number=1000, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='schema_url', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.schema_url', index=3, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, @@ -165,7 +172,53 @@ oneofs=[ ], serialized_start=263, - serialized_end=465, + serialized_end=539, +) + + +_SCOPEMETRICS = _descriptor.Descriptor( + name='ScopeMetrics', + full_name='opentelemetry.proto.metrics.v1.ScopeMetrics', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='scope', full_name='opentelemetry.proto.metrics.v1.ScopeMetrics.scope', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='metrics', full_name='opentelemetry.proto.metrics.v1.ScopeMetrics.metrics', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='schema_url', full_name='opentelemetry.proto.metrics.v1.ScopeMetrics.schema_url', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=542, + serialized_end=701, ) @@ -204,14 +257,14 @@ nested_types=[], enum_types=[ ], - serialized_options=None, + serialized_options=b'\030\001', is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], - serialized_start=468, - serialized_end=664, + serialized_start=704, + serialized_end=904, ) @@ -296,8 +349,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=667, - serialized_end=1069, + serialized_start=907, + serialized_end=1309, ) @@ -328,8 +381,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1071, - serialized_end=1148, + serialized_start=1311, + serialized_end=1388, ) @@ -374,8 +427,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1151, - serialized_end=1337, + serialized_start=1391, + serialized_end=1577, ) @@ -413,8 +466,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1340, - serialized_end=1513, + serialized_start=1580, + serialized_end=1753, ) @@ -452,8 +505,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1516, - serialized_end=1711, + serialized_start=1756, + serialized_end=1951, ) @@ -484,8 +537,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1713, - serialized_end=1793, + serialized_start=1953, + serialized_end=2033, ) @@ -563,8 +616,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=1796, - serialized_end=2058, + serialized_start=2036, + serialized_end=2298, ) @@ -650,9 +703,14 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_sum', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint._sum', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=2061, - serialized_end=2354, + serialized_start=2301, + serialized_end=2607, ) @@ -690,8 +748,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2822, - serialized_end=2870, + serialized_start=3075, + serialized_end=3123, ) _EXPONENTIALHISTOGRAMDATAPOINT = _descriptor.Descriptor( @@ -791,8 +849,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2357, - serialized_end=2870, + serialized_start=2610, + serialized_end=3123, ) @@ -830,8 +888,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3142, - serialized_end=3192, + serialized_start=3395, + serialized_end=3445, ) _SUMMARYDATAPOINT = _descriptor.Descriptor( @@ -903,8 +961,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2873, - serialized_end=3198, + serialized_start=3126, + serialized_end=3451, ) @@ -975,13 +1033,16 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=3201, - serialized_end=3394, + serialized_start=3454, + serialized_end=3647, ) _METRICSDATA.fields_by_name['resource_metrics'].message_type = _RESOURCEMETRICS _RESOURCEMETRICS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE +_RESOURCEMETRICS.fields_by_name['scope_metrics'].message_type = _SCOPEMETRICS _RESOURCEMETRICS.fields_by_name['instrumentation_library_metrics'].message_type = _INSTRUMENTATIONLIBRARYMETRICS +_SCOPEMETRICS.fields_by_name['scope'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONSCOPE +_SCOPEMETRICS.fields_by_name['metrics'].message_type = _METRIC _INSTRUMENTATIONLIBRARYMETRICS.fields_by_name['instrumentation_library'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONLIBRARY _INSTRUMENTATIONLIBRARYMETRICS.fields_by_name['metrics'].message_type = _METRIC _METRIC.fields_by_name['gauge'].message_type = _GAUGE @@ -1022,6 +1083,9 @@ _NUMBERDATAPOINT.fields_by_name['as_int'].containing_oneof = _NUMBERDATAPOINT.oneofs_by_name['value'] _HISTOGRAMDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE _HISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _EXEMPLAR +_HISTOGRAMDATAPOINT.oneofs_by_name['_sum'].fields.append( + _HISTOGRAMDATAPOINT.fields_by_name['sum']) +_HISTOGRAMDATAPOINT.fields_by_name['sum'].containing_oneof = _HISTOGRAMDATAPOINT.oneofs_by_name['_sum'] _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS.containing_type = _EXPONENTIALHISTOGRAMDATAPOINT _EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE _EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['positive'].message_type = _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS @@ -1039,6 +1103,7 @@ _EXEMPLAR.fields_by_name['as_int'].containing_oneof = _EXEMPLAR.oneofs_by_name['value'] DESCRIPTOR.message_types_by_name['MetricsData'] = _METRICSDATA DESCRIPTOR.message_types_by_name['ResourceMetrics'] = _RESOURCEMETRICS +DESCRIPTOR.message_types_by_name['ScopeMetrics'] = _SCOPEMETRICS DESCRIPTOR.message_types_by_name['InstrumentationLibraryMetrics'] = _INSTRUMENTATIONLIBRARYMETRICS DESCRIPTOR.message_types_by_name['Metric'] = _METRIC DESCRIPTOR.message_types_by_name['Gauge'] = _GAUGE @@ -1069,6 +1134,13 @@ }) _sym_db.RegisterMessage(ResourceMetrics) +ScopeMetrics = _reflection.GeneratedProtocolMessageType('ScopeMetrics', (_message.Message,), { + 'DESCRIPTOR' : _SCOPEMETRICS, + '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.ScopeMetrics) + }) +_sym_db.RegisterMessage(ScopeMetrics) + InstrumentationLibraryMetrics = _reflection.GeneratedProtocolMessageType('InstrumentationLibraryMetrics', (_message.Message,), { 'DESCRIPTOR' : _INSTRUMENTATIONLIBRARYMETRICS, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' @@ -1171,4 +1243,6 @@ DESCRIPTOR._options = None +_RESOURCEMETRICS.fields_by_name['instrumentation_library_metrics']._options = None +_INSTRUMENTATIONLIBRARYMETRICS._options = None # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi index 7ee8a16f13..9b49ee0429 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi @@ -226,9 +226,10 @@ class MetricsData(google.protobuf.message.Message): global___MetricsData = MetricsData class ResourceMetrics(google.protobuf.message.Message): - """A collection of InstrumentationLibraryMetrics from a Resource.""" + """A collection of ScopeMetrics from a Resource.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... RESOURCE_FIELD_NUMBER: builtins.int + SCOPE_METRICS_FIELD_NUMBER: builtins.int INSTRUMENTATION_LIBRARY_METRICS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property @@ -238,27 +239,92 @@ class ResourceMetrics(google.protobuf.message.Message): """ pass @property - def instrumentation_library_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibraryMetrics]: + def scope_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeMetrics]: """A list of metrics that originate from a resource.""" pass + @property + def instrumentation_library_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibraryMetrics]: + """A list of InstrumentationLibraryMetrics that originate from a resource. + This field is deprecated and will be removed after grace period expires on June 15, 2022. + + During the grace period the following rules SHOULD be followed: + + For Binary Protobufs + ==================== + Binary Protobuf senders SHOULD NOT set instrumentation_library_metrics. Instead + scope_metrics SHOULD be set. + + Binary Protobuf receivers SHOULD check if instrumentation_library_metrics is set + and scope_metrics is not set then the value in instrumentation_library_metrics + SHOULD be used instead by converting InstrumentationLibraryMetrics into ScopeMetrics. + If scope_metrics is set then instrumentation_library_metrics SHOULD be ignored. + + For JSON + ======== + JSON senders that set instrumentation_library_metrics field MAY also set + scope_metrics to carry the same metrics, essentially double-publishing the same data. + Such double-publishing MAY be controlled by a user-settable option. + If double-publishing is not used then the senders SHOULD set scope_metrics and + SHOULD NOT set instrumentation_library_metrics. + + JSON receivers SHOULD check if instrumentation_library_metrics is set and + scope_metrics is not set then the value in instrumentation_library_metrics + SHOULD be used instead by converting InstrumentationLibraryMetrics into ScopeMetrics. + If scope_metrics is set then instrumentation_library_metrics field SHOULD be ignored. + """ + pass schema_url: typing.Text = ... """This schema_url applies to the data in the "resource" field. It does not apply - to the data in the "instrumentation_library_metrics" field which have their own - schema_url field. + to the data in the "scope_metrics" field which have their own schema_url field. """ def __init__(self, *, resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., + scope_metrics : typing.Optional[typing.Iterable[global___ScopeMetrics]] = ..., instrumentation_library_metrics : typing.Optional[typing.Iterable[global___InstrumentationLibraryMetrics]] = ..., schema_url : typing.Text = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["resource",b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library_metrics",b"instrumentation_library_metrics","resource",b"resource","schema_url",b"schema_url"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library_metrics",b"instrumentation_library_metrics","resource",b"resource","schema_url",b"schema_url","scope_metrics",b"scope_metrics"]) -> None: ... global___ResourceMetrics = ResourceMetrics +class ScopeMetrics(google.protobuf.message.Message): + """A collection of Metrics produced by an Scope.""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + SCOPE_FIELD_NUMBER: builtins.int + METRICS_FIELD_NUMBER: builtins.int + SCHEMA_URL_FIELD_NUMBER: builtins.int + @property + def scope(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationScope: + """The instrumentation scope information for the metrics in this message. + Semantically when InstrumentationScope isn't set, it is equivalent with + an empty instrumentation scope name (unknown). + """ + pass + @property + def metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Metric]: + """A list of metrics that originate from an instrumentation library.""" + pass + schema_url: typing.Text = ... + """This schema_url applies to all metrics in the "metrics" field.""" + + def __init__(self, + *, + scope : typing.Optional[opentelemetry.proto.common.v1.common_pb2.InstrumentationScope] = ..., + metrics : typing.Optional[typing.Iterable[global___Metric]] = ..., + schema_url : typing.Text = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["scope",b"scope"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["metrics",b"metrics","schema_url",b"schema_url","scope",b"scope"]) -> None: ... +global___ScopeMetrics = ScopeMetrics + class InstrumentationLibraryMetrics(google.protobuf.message.Message): - """A collection of Metrics produced by an InstrumentationLibrary.""" + """A collection of Metrics produced by an InstrumentationLibrary. + InstrumentationLibraryMetrics is wire-compatible with ScopeMetrics for binary + Protobuf format. + This message is deprecated and will be removed on June 15, 2022. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int METRICS_FIELD_NUMBER: builtins.int @@ -548,6 +614,8 @@ class NumberDataPoint(google.protobuf.message.Message): def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that uniquely identify the timeseries from where this point belongs. The list may be empty (may contain 0 elements). + Attribute keys MUST be unique (it is not allowed to have more than one + attribute with the same key). """ pass start_time_unix_nano: builtins.int = ... @@ -619,6 +687,8 @@ class HistogramDataPoint(google.protobuf.message.Message): def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that uniquely identify the timeseries from where this point belongs. The list may be empty (may contain 0 elements). + Attribute keys MUST be unique (it is not allowed to have more than one + attribute with the same key). """ pass start_time_unix_nano: builtins.int = ... @@ -704,7 +774,9 @@ class HistogramDataPoint(google.protobuf.message.Message): exemplars : typing.Optional[typing.Iterable[global___Exemplar]] = ..., flags : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","bucket_counts",b"bucket_counts","count",b"count","exemplars",b"exemplars","explicit_bounds",b"explicit_bounds","flags",b"flags","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["_sum",b"_sum","sum",b"sum"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["_sum",b"_sum","attributes",b"attributes","bucket_counts",b"bucket_counts","count",b"count","exemplars",b"exemplars","explicit_bounds",b"explicit_bounds","flags",b"flags","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["_sum",b"_sum"]) -> typing.Optional[typing_extensions.Literal["sum"]]: ... global___HistogramDataPoint = HistogramDataPoint class ExponentialHistogramDataPoint(google.protobuf.message.Message): @@ -762,6 +834,8 @@ class ExponentialHistogramDataPoint(google.protobuf.message.Message): def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that uniquely identify the timeseries from where this point belongs. The list may be empty (may contain 0 elements). + Attribute keys MUST be unique (it is not allowed to have more than one + attribute with the same key). """ pass start_time_unix_nano: builtins.int = ... @@ -909,6 +983,8 @@ class SummaryDataPoint(google.protobuf.message.Message): def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that uniquely identify the timeseries from where this point belongs. The list may be empty (may contain 0 elements). + Attribute keys MUST be unique (it is not allowed to have more than one + attribute with the same key). """ pass start_time_unix_nano: builtins.int = ... diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py index f64160f602..553053c28a 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py @@ -18,9 +18,9 @@ name='opentelemetry/proto/resource/v1/resource.proto', package='opentelemetry.proto.resource.v1', syntax='proto3', - serialized_options=b'\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\001Z@github.com/open-telemetry/opentelemetry-proto/gen/go/resource/v1', + serialized_options=b'\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\001Z*go.opentelemetry.io/proto/otlp/resource/v1', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"i\n\x08Resource\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\rBw\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z@github.com/open-telemetry/opentelemetry-proto/gen/go/resource/v1b\x06proto3' + serialized_pb=b'\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"i\n\x08Resource\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\rBa\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z*go.opentelemetry.io/proto/otlp/resource/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,]) diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi index 957a7b6d1b..f660c7f229 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi @@ -19,7 +19,10 @@ class Resource(google.protobuf.message.Message): DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """Set of labels that describe the resource.""" + """Set of attributes that describe the resource. + Attribute keys MUST be unique (it is not allowed to have more than one + attribute with the same key). + """ pass dropped_attributes_count: builtins.int = ... """dropped_attributes_count is the number of dropped attributes. If the value is 0, then diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py index 99428b3c89..4b7cc3a69b 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py @@ -17,9 +17,9 @@ name='opentelemetry/proto/trace/v1/trace_config.proto', package='opentelemetry.proto.trace.v1', syntax='proto3', - serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\020TraceConfigProtoP\001ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1', + serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\020TraceConfigProtoP\001Z1go.opentelemetry.io/proto/otlp/collector/trace/v1', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n/opentelemetry/proto/trace/v1/trace_config.proto\x12\x1copentelemetry.proto.trace.v1\"\xc8\x03\n\x0bTraceConfig\x12I\n\x10\x63onstant_sampler\x18\x01 \x01(\x0b\x32-.opentelemetry.proto.trace.v1.ConstantSamplerH\x00\x12O\n\x14trace_id_ratio_based\x18\x02 \x01(\x0b\x32/.opentelemetry.proto.trace.v1.TraceIdRatioBasedH\x00\x12R\n\x15rate_limiting_sampler\x18\x03 \x01(\x0b\x32\x31.opentelemetry.proto.trace.v1.RateLimitingSamplerH\x00\x12 \n\x18max_number_of_attributes\x18\x04 \x01(\x03\x12\"\n\x1amax_number_of_timed_events\x18\x05 \x01(\x03\x12\x30\n(max_number_of_attributes_per_timed_event\x18\x06 \x01(\x03\x12\x1b\n\x13max_number_of_links\x18\x07 \x01(\x03\x12)\n!max_number_of_attributes_per_link\x18\x08 \x01(\x03\x42\t\n\x07sampler\"\xa9\x01\n\x0f\x43onstantSampler\x12P\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32>.opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision\"D\n\x10\x43onstantDecision\x12\x0e\n\nALWAYS_OFF\x10\x00\x12\r\n\tALWAYS_ON\x10\x01\x12\x11\n\rALWAYS_PARENT\x10\x02\"*\n\x11TraceIdRatioBased\x12\x15\n\rsamplingRatio\x18\x01 \x01(\x01\"\"\n\x13RateLimitingSampler\x12\x0b\n\x03qps\x18\x01 \x01(\x03\x42~\n\x1fio.opentelemetry.proto.trace.v1B\x10TraceConfigProtoP\x01ZGgithub.com/open-telemetry/opentelemetry-proto/gen/go/collector/trace/v1b\x06proto3' + serialized_pb=b'\n/opentelemetry/proto/trace/v1/trace_config.proto\x12\x1copentelemetry.proto.trace.v1\"\xc8\x03\n\x0bTraceConfig\x12I\n\x10\x63onstant_sampler\x18\x01 \x01(\x0b\x32-.opentelemetry.proto.trace.v1.ConstantSamplerH\x00\x12O\n\x14trace_id_ratio_based\x18\x02 \x01(\x0b\x32/.opentelemetry.proto.trace.v1.TraceIdRatioBasedH\x00\x12R\n\x15rate_limiting_sampler\x18\x03 \x01(\x0b\x32\x31.opentelemetry.proto.trace.v1.RateLimitingSamplerH\x00\x12 \n\x18max_number_of_attributes\x18\x04 \x01(\x03\x12\"\n\x1amax_number_of_timed_events\x18\x05 \x01(\x03\x12\x30\n(max_number_of_attributes_per_timed_event\x18\x06 \x01(\x03\x12\x1b\n\x13max_number_of_links\x18\x07 \x01(\x03\x12)\n!max_number_of_attributes_per_link\x18\x08 \x01(\x03\x42\t\n\x07sampler\"\xa9\x01\n\x0f\x43onstantSampler\x12P\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32>.opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision\"D\n\x10\x43onstantDecision\x12\x0e\n\nALWAYS_OFF\x10\x00\x12\r\n\tALWAYS_ON\x10\x01\x12\x11\n\rALWAYS_PARENT\x10\x02\"*\n\x11TraceIdRatioBased\x12\x15\n\rsamplingRatio\x18\x01 \x01(\x01\"\"\n\x13RateLimitingSampler\x12\x0b\n\x03qps\x18\x01 \x01(\x03\x42h\n\x1fio.opentelemetry.proto.trace.v1B\x10TraceConfigProtoP\x01Z1go.opentelemetry.io/proto/otlp/collector/trace/v1b\x06proto3' ) diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py index b231fd0d14..7c4057ca7b 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py @@ -19,9 +19,9 @@ name='opentelemetry/proto/trace/v1/trace.proto', package='opentelemetry.proto.trace.v1', syntax='proto3', - serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1', + serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z\'go.opentelemetry.io/proto/otlp/trace/v1', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"Q\n\nTracesData\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\xc2\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12`\n\x1dinstrumentation_library_spans\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xbc\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xae\x01\n\x06Status\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02J\x04\x08\x01\x10\x02\x42n\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z=github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1b\x06proto3' + serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"Q\n\nTracesData\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\x86\x02\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12=\n\x0bscope_spans\x18\x02 \x03(\x0b\x32(.opentelemetry.proto.trace.v1.ScopeSpans\x12\x65\n\x1dinstrumentation_library_spans\x18\xe8\x07 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpansB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x97\x01\n\nScopeSpans\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc0\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xae\x01\n\x06Status\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02J\x04\x08\x01\x10\x02\x42X\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z\'go.opentelemetry.io/proto/otlp/trace/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -67,8 +67,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1483, - serialized_end=1636, + serialized_start=1709, + serialized_end=1862, ) _sym_db.RegisterEnumDescriptor(_SPAN_SPANKIND) @@ -97,8 +97,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1729, - serialized_end=1807, + serialized_start=1955, + serialized_end=2033, ) _sym_db.RegisterEnumDescriptor(_STATUS_STATUSCODE) @@ -151,14 +151,21 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='instrumentation_library_spans', full_name='opentelemetry.proto.trace.v1.ResourceSpans.instrumentation_library_spans', index=1, + name='scope_spans', full_name='opentelemetry.proto.trace.v1.ResourceSpans.scope_spans', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='schema_url', full_name='opentelemetry.proto.trace.v1.ResourceSpans.schema_url', index=2, + name='instrumentation_library_spans', full_name='opentelemetry.proto.trace.v1.ResourceSpans.instrumentation_library_spans', index=2, + number=1000, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='schema_url', full_name='opentelemetry.proto.trace.v1.ResourceSpans.schema_url', index=3, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, @@ -177,7 +184,53 @@ oneofs=[ ], serialized_start=250, - serialized_end=444, + serialized_end=512, +) + + +_SCOPESPANS = _descriptor.Descriptor( + name='ScopeSpans', + full_name='opentelemetry.proto.trace.v1.ScopeSpans', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='scope', full_name='opentelemetry.proto.trace.v1.ScopeSpans.scope', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='spans', full_name='opentelemetry.proto.trace.v1.ScopeSpans.spans', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='schema_url', full_name='opentelemetry.proto.trace.v1.ScopeSpans.schema_url', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=515, + serialized_end=666, ) @@ -216,14 +269,14 @@ nested_types=[], enum_types=[ ], - serialized_options=None, + serialized_options=b'\030\001', is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], - serialized_start=447, - serialized_end=635, + serialized_start=669, + serialized_end=861, ) @@ -275,8 +328,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1180, - serialized_end=1320, + serialized_start=1406, + serialized_end=1546, ) _SPAN_LINK = _descriptor.Descriptor( @@ -334,8 +387,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1323, - serialized_end=1480, + serialized_start=1549, + serialized_end=1706, ) _SPAN = _descriptor.Descriptor( @@ -464,8 +517,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=638, - serialized_end=1636, + serialized_start=864, + serialized_end=1862, ) @@ -504,13 +557,16 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1639, - serialized_end=1813, + serialized_start=1865, + serialized_end=2039, ) _TRACESDATA.fields_by_name['resource_spans'].message_type = _RESOURCESPANS _RESOURCESPANS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE +_RESOURCESPANS.fields_by_name['scope_spans'].message_type = _SCOPESPANS _RESOURCESPANS.fields_by_name['instrumentation_library_spans'].message_type = _INSTRUMENTATIONLIBRARYSPANS +_SCOPESPANS.fields_by_name['scope'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONSCOPE +_SCOPESPANS.fields_by_name['spans'].message_type = _SPAN _INSTRUMENTATIONLIBRARYSPANS.fields_by_name['instrumentation_library'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONLIBRARY _INSTRUMENTATIONLIBRARYSPANS.fields_by_name['spans'].message_type = _SPAN _SPAN_EVENT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE @@ -527,6 +583,7 @@ _STATUS_STATUSCODE.containing_type = _STATUS DESCRIPTOR.message_types_by_name['TracesData'] = _TRACESDATA DESCRIPTOR.message_types_by_name['ResourceSpans'] = _RESOURCESPANS +DESCRIPTOR.message_types_by_name['ScopeSpans'] = _SCOPESPANS DESCRIPTOR.message_types_by_name['InstrumentationLibrarySpans'] = _INSTRUMENTATIONLIBRARYSPANS DESCRIPTOR.message_types_by_name['Span'] = _SPAN DESCRIPTOR.message_types_by_name['Status'] = _STATUS @@ -546,6 +603,13 @@ }) _sym_db.RegisterMessage(ResourceSpans) +ScopeSpans = _reflection.GeneratedProtocolMessageType('ScopeSpans', (_message.Message,), { + 'DESCRIPTOR' : _SCOPESPANS, + '__module__' : 'opentelemetry.proto.trace.v1.trace_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.ScopeSpans) + }) +_sym_db.RegisterMessage(ScopeSpans) + InstrumentationLibrarySpans = _reflection.GeneratedProtocolMessageType('InstrumentationLibrarySpans', (_message.Message,), { 'DESCRIPTOR' : _INSTRUMENTATIONLIBRARYSPANS, '__module__' : 'opentelemetry.proto.trace.v1.trace_pb2' @@ -585,4 +649,6 @@ DESCRIPTOR._options = None +_RESOURCESPANS.fields_by_name['instrumentation_library_spans']._options = None +_INSTRUMENTATIONLIBRARYSPANS._options = None # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi index 16491d9230..170d66e1c5 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi @@ -45,9 +45,10 @@ class TracesData(google.protobuf.message.Message): global___TracesData = TracesData class ResourceSpans(google.protobuf.message.Message): - """A collection of InstrumentationLibrarySpans from a Resource.""" + """A collection of ScopeSpans from a Resource.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... RESOURCE_FIELD_NUMBER: builtins.int + SCOPE_SPANS_FIELD_NUMBER: builtins.int INSTRUMENTATION_LIBRARY_SPANS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property @@ -57,27 +58,92 @@ class ResourceSpans(google.protobuf.message.Message): """ pass @property + def scope_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeSpans]: + """A list of ScopeSpans that originate from a resource.""" + pass + @property def instrumentation_library_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibrarySpans]: - """A list of InstrumentationLibrarySpans that originate from a resource.""" + """A list of InstrumentationLibrarySpans that originate from a resource. + This field is deprecated and will be removed after grace period expires on June 15, 2022. + + During the grace period the following rules SHOULD be followed: + + For Binary Protobufs + ==================== + Binary Protobuf senders SHOULD NOT set instrumentation_library_spans. Instead + scope_spans SHOULD be set. + + Binary Protobuf receivers SHOULD check if instrumentation_library_spans is set + and scope_spans is not set then the value in instrumentation_library_spans + SHOULD be used instead by converting InstrumentationLibrarySpans into ScopeSpans. + If scope_spans is set then instrumentation_library_spans SHOULD be ignored. + + For JSON + ======== + JSON senders that set instrumentation_library_spans field MAY also set + scope_spans to carry the same spans, essentially double-publishing the same data. + Such double-publishing MAY be controlled by a user-settable option. + If double-publishing is not used then the senders SHOULD set scope_spans and + SHOULD NOT set instrumentation_library_spans. + + JSON receivers SHOULD check if instrumentation_library_spans is set and + scope_spans is not set then the value in instrumentation_library_spans + SHOULD be used instead by converting InstrumentationLibrarySpans into ScopeSpans. + If scope_spans is set then instrumentation_library_spans field SHOULD be ignored. + """ pass schema_url: typing.Text = ... """This schema_url applies to the data in the "resource" field. It does not apply - to the data in the "instrumentation_library_spans" field which have their own - schema_url field. + to the data in the "scope_spans" field which have their own schema_url field. """ def __init__(self, *, resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., + scope_spans : typing.Optional[typing.Iterable[global___ScopeSpans]] = ..., instrumentation_library_spans : typing.Optional[typing.Iterable[global___InstrumentationLibrarySpans]] = ..., schema_url : typing.Text = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["resource",b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library_spans",b"instrumentation_library_spans","resource",b"resource","schema_url",b"schema_url"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library_spans",b"instrumentation_library_spans","resource",b"resource","schema_url",b"schema_url","scope_spans",b"scope_spans"]) -> None: ... global___ResourceSpans = ResourceSpans +class ScopeSpans(google.protobuf.message.Message): + """A collection of Spans produced by an InstrumentationScope.""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + SCOPE_FIELD_NUMBER: builtins.int + SPANS_FIELD_NUMBER: builtins.int + SCHEMA_URL_FIELD_NUMBER: builtins.int + @property + def scope(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationScope: + """The instrumentation scope information for the spans in this message. + Semantically when InstrumentationScope isn't set, it is equivalent with + an empty instrumentation scope name (unknown). + """ + pass + @property + def spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span]: + """A list of Spans that originate from an instrumentation scope.""" + pass + schema_url: typing.Text = ... + """This schema_url applies to all spans and span events in the "spans" field.""" + + def __init__(self, + *, + scope : typing.Optional[opentelemetry.proto.common.v1.common_pb2.InstrumentationScope] = ..., + spans : typing.Optional[typing.Iterable[global___Span]] = ..., + schema_url : typing.Text = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["scope",b"scope"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["schema_url",b"schema_url","scope",b"scope","spans",b"spans"]) -> None: ... +global___ScopeSpans = ScopeSpans + class InstrumentationLibrarySpans(google.protobuf.message.Message): - """A collection of Spans produced by an InstrumentationLibrary.""" + """A collection of Spans produced by an InstrumentationLibrary. + InstrumentationLibrarySpans is wire-compatible with ScopeSpans for binary + Protobuf format. + This message is deprecated and will be removed on June 15, 2022. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int SPANS_FIELD_NUMBER: builtins.int @@ -210,7 +276,10 @@ class Span(google.protobuf.message.Message): @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """attributes is a collection of attribute key/value pairs on the event.""" + """attributes is a collection of attribute key/value pairs on the event. + Attribute keys MUST be unique (it is not allowed to have more than one + attribute with the same key). + """ pass dropped_attributes_count: builtins.int = ... """dropped_attributes_count is the number of dropped attributes. If the value is 0, @@ -251,7 +320,10 @@ class Span(google.protobuf.message.Message): @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """attributes is a collection of attribute key/value pairs on the link.""" + """attributes is a collection of attribute key/value pairs on the link. + Attribute keys MUST be unique (it is not allowed to have more than one + attribute with the same key). + """ pass dropped_attributes_count: builtins.int = ... """dropped_attributes_count is the number of dropped attributes. If the value is 0, @@ -366,6 +438,8 @@ class Span(google.protobuf.message.Message): The OpenTelemetry API specification further restricts the allowed value types: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/common.md#attributes + Attribute keys MUST be unique (it is not allowed to have more than one + attribute with the same key). """ pass dropped_attributes_count: builtins.int = ... diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index 08875e1428..62df015d36 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -12,7 +12,7 @@ # PROTO_REPO_DIR - the path to an existing checkout of the opentelemetry-proto repo # Pinned commit/branch/tag for the current version used in opentelemetry-proto python package. -PROTO_REPO_BRANCH_OR_COMMIT="v0.12.0" +PROTO_REPO_BRANCH_OR_COMMIT="v0.15.0" set -e From 02d716437fd47bfd58df55a4e0f8383961036b76 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 30 Mar 2022 11:05:37 -0600 Subject: [PATCH 1176/1517] Check for current point type before warning (#2576) --- .../src/opentelemetry/sdk/_metrics/aggregation.py | 6 ++++-- opentelemetry-sdk/tests/metrics/test_aggregation.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 4471e5f3f4..8a61e78e72 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -303,8 +303,10 @@ def _convert_aggregation_temporality( if current_point_type is Gauge: return current_point - if previous_point is not None and type(previous_point) is not type( - current_point + if ( + previous_point is not None + and current_point is not None + and type(previous_point) is not type(current_point) ): _logger.warning( "convert_aggregation_temporality called with mismatched " diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 675f5775cf..7a00a8b321 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -360,6 +360,17 @@ def test_mismatched_point_types(self): current_point, ) + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + self.assertIs( + _convert_aggregation_temporality( + Gauge(time_unix_nano=0, value=0), + None, + AggregationTemporality.DELTA, + ), + current_point, + ) + def test_current_point_sum_previous_point_none(self): current_point = Sum( From 37e9387d74ab0914ad695e8b71bb3437c8d4d69e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 30 Mar 2022 12:33:13 -0600 Subject: [PATCH 1177/1517] Avoid multiple registration of MetricReaders (#2564) * Avoid multiple registration of MetricReaders Fixes #2561 * Remove unused attribute * Add lock * Fix typo --- .../opentelemetry/sdk/_metrics/__init__.py | 14 +++++++++- .../tests/metrics/test_metrics.py | 28 ++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 90ec677251..8a87bb1fa2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -177,6 +177,9 @@ class MeterProvider(APIMeterProvider): ) """ + _all_metric_readers_lock = Lock() + _all_metric_readers = set() + def __init__( self, metric_readers: Sequence[MetricReader] = (), @@ -200,9 +203,18 @@ def __init__( self._atexit_handler = register(self.shutdown) self._meters = {} - self._metric_readers = metric_readers for metric_reader in self._sdk_config.metric_readers: + + with self._all_metric_readers_lock: + if metric_reader in self._all_metric_readers: + raise Exception( + f"MetricReader {metric_reader} has been registered " + "already in other MeterProvider instance" + ) + + self._all_metric_readers.add(metric_reader) + metric_reader._set_collect_callback( self._measurement_consumer.collect ) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index f72d059384..07d9894c96 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -19,6 +19,7 @@ from opentelemetry._metrics import NoOpMeter from opentelemetry.sdk._metrics import Meter, MeterProvider +from opentelemetry.sdk._metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk._metrics.instrument import ( Counter, Histogram, @@ -45,6 +46,25 @@ def shutdown(self): class TestMeterProvider(ConcurrencyTestBase): + def tearDown(self): + + MeterProvider._all_metric_readers = set() + + def test_register_metric_readers(self): + + metric_reader_0 = PeriodicExportingMetricReader(Mock()) + metric_reader_1 = PeriodicExportingMetricReader(Mock()) + + try: + MeterProvider(metric_readers=(metric_reader_0,)) + MeterProvider(metric_readers=(metric_reader_1,)) + except Exception as error: + self.fail(f"Unexpected exception {error} raised") + + with self.assertRaises(Exception): + MeterProvider(metric_readers=(metric_reader_0,)) + MeterProvider(metric_readers=(metric_reader_0,)) + def test_resource(self): """ `MeterProvider` provides a way to allow a `Resource` to be specified. @@ -190,7 +210,13 @@ def test_shutdown_race(self, mock_logger): def test_measurement_collect_callback( self, mock_sync_measurement_consumer ): - metric_readers = [DummyMetricReader()] * 5 + metric_readers = [ + DummyMetricReader(), + DummyMetricReader(), + DummyMetricReader(), + DummyMetricReader(), + DummyMetricReader(), + ] sync_consumer_instance = mock_sync_measurement_consumer() sync_consumer_instance.collect = MockFunc() MeterProvider(metric_readers=metric_readers) From 450cce19eb8b800337459bfdbb0ea8c7d33ec91d Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 30 Mar 2022 14:35:55 -0700 Subject: [PATCH 1178/1517] [docs] adding more metrics documentation (#2574) * [docs] adding more metrics documentation Adding docs around instruments and the methods to create them. I've also added an exclusion list for things that aren't really useful for users. * fix lint Co-authored-by: Srikanth Chekuri --- docs/conf.py | 1 + .../src/opentelemetry/_metrics/__init__.py | 55 +++++++++++++++++-- .../src/opentelemetry/_metrics/instrument.py | 23 +++++++- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index fef6b1d3a9..4f2458d1e7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -147,6 +147,7 @@ "undoc-members": True, "show-inheritance": True, "member-order": "bysource", + "exclude-members": "_ProxyObservableUpDownCounter,_ProxyHistogram,_ProxyObservableGauge,_ProxyInstrument,_ProxyAsynchronousInstrument,_ProxyCounter,_ProxyUpDownCounter,_ProxyObservableCounter,_ProxyObservableGauge,_abc_impl", } # -- Options for HTML output ------------------------------------------------- diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index 134237fc91..a2f430c5f7 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -136,19 +136,33 @@ def schema_url(self): @abstractmethod def create_counter(self, name, unit="", description="") -> Counter: - pass + """Creates a `Counter` instrument + + Args: + name: The name of the instrument to be created + unit: The unit for measurements this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ @abstractmethod def create_up_down_counter( self, name, unit="", description="" ) -> UpDownCounter: - pass + """Creates an `UpDownCounter` instrument + + Args: + name: The name of the instrument to be created + unit: The unit for measurements this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ @abstractmethod def create_observable_counter( self, name, callback, unit="", description="" ) -> ObservableCounter: - """Creates an observable counter instrument + """Creates an `ObservableCounter` instrument An observable counter observes a monotonically increasing count by calling a provided callback which returns multiple @@ -229,19 +243,48 @@ def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Measurem @abstractmethod def create_histogram(self, name, unit="", description="") -> Histogram: - pass + """Creates a `Histogram` instrument + + Args: + name: The name of the instrument to be created + unit: The unit for measurements this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ @abstractmethod def create_observable_gauge( self, name, callback, unit="", description="" ) -> ObservableGauge: - pass + """Creates an `ObservableGauge` instrument + + Args: + name: The name of the instrument to be created + callback: A callback that returns an iterable of + :class:`~opentelemetry._metrics.measurement.Measurement`. + Alternatively, can be a generator that yields iterables of + :class:`~opentelemetry._metrics.measurement.Measurement`. + unit: The unit for measurements this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ @abstractmethod def create_observable_up_down_counter( self, name, callback, unit="", description="" ) -> ObservableUpDownCounter: - pass + """Creates an `ObservableUpDownCounter` instrument + + Args: + name: The name of the instrument to be created + callback: A callback that returns an iterable of + :class:`~opentelemetry._metrics.measurement.Measurement`. + Alternatively, can be a generator that yields iterables of + :class:`~opentelemetry._metrics.measurement.Measurement`. + unit: The unit for measurements this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ class _ProxyMeter(Meter): diff --git a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py index d6f8633173..aee2a5359e 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py @@ -111,6 +111,8 @@ class _NonMonotonic(_Adding): class Counter(_Monotonic, Synchronous): + """A Counter is a synchronous `Instrument` which supports non-negative increments.""" + @abstractmethod def add(self, amount, attributes=None): # FIXME check that the amount is non negative @@ -135,6 +137,8 @@ def _create_real_instrument(self, meter: "metrics.Meter") -> Counter: class UpDownCounter(_NonMonotonic, Synchronous): + """An UpDownCounter is a synchronous `Instrument` which supports increments and decrements.""" + @abstractmethod def add(self, amount, attributes=None): pass @@ -160,7 +164,9 @@ def _create_real_instrument(self, meter: "metrics.Meter") -> UpDownCounter: class ObservableCounter(_Monotonic, Asynchronous): - pass + """An ObservableCounter is an asynchronous `Instrument` which reports monotonically + increasing value(s) when the instrument is being observed. + """ class DefaultObservableCounter(ObservableCounter): @@ -180,7 +186,10 @@ def _create_real_instrument( class ObservableUpDownCounter(_NonMonotonic, Asynchronous): - pass + """An ObservableUpDownCounter is an asynchronous `Instrument` which reports additive value(s) (e.g. + the process heap size - it makes sense to report the heap size from multiple processes and sum them + up, so we get the total heap usage) when the instrument is being observed. + """ class DefaultObservableUpDownCounter(ObservableUpDownCounter): @@ -201,6 +210,11 @@ def _create_real_instrument( class Histogram(_Grouping, Synchronous): + """Histogram is a synchronous `Instrument` which can be used to report arbitrary values + that are likely to be statistically meaningful. It is intended for statistics such as + histograms, summaries, and percentile. + """ + @abstractmethod def record(self, amount, attributes=None): pass @@ -226,7 +240,10 @@ def _create_real_instrument(self, meter: "metrics.Meter") -> Histogram: class ObservableGauge(_Grouping, Asynchronous): - pass + """Asynchronous Gauge is an asynchronous `Instrument` which reports non-additive value(s) (e.g. + the room temperature - it makes no sense to report the temperature value from multiple rooms + and sum them up) when the instrument is being observed. + """ class DefaultObservableGauge(ObservableGauge): From b2e938787231aecf1d9cc3d3aa71a08cd6c344e9 Mon Sep 17 00:00:00 2001 From: teddylear Date: Thu, 31 Mar 2022 13:17:33 -0400 Subject: [PATCH 1179/1517] Loosen up the dependency on backoff (#2575) --- CHANGELOG.md | 2 ++ exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 305f9f9201..183df40999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2566](https://github.com/open-telemetry/opentelemetry-python/pull/2566)) - Remove `enable_default_view` option from sdk MeterProvider ([#2547](https://github.com/open-telemetry/opentelemetry-python/pull/2547)) +- Update otlp-proto-grpc and otlp-proto-http exporters to have more lax requirements for `backoff` lib + ([#2575](https://github.com/open-telemetry/opentelemetry-python/pull/2575)) ## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 6362a4c553..08b13b56c6 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -45,7 +45,7 @@ install_requires = opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.10.0 opentelemetry-proto == 1.10.0 - backoff ~= 1.10.0 + backoff >= 1.10.0, < 2.0.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index 2b3b37f01a..9b5c0b0afb 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 opentelemetry-proto == 1.10.0 - backoff ~= 1.10.0 + backoff >= 1.10.0, < 2.0.0 [options.extras_require] test = From e280a8aa8524b1a74270d4be553360bba07a29c8 Mon Sep 17 00:00:00 2001 From: DJ Gregor Date: Fri, 1 Apr 2022 12:27:25 -0400 Subject: [PATCH 1180/1517] Make sure trace flags are parsed properly when extracting traceparent and expand tests (#2577) --- CHANGELOG.md | 2 + .../trace/propagation/tracecontext.py | 2 +- .../tests/propagators/test_propagators.py | 147 ++++++++++++++++++ opentelemetry-sdk/tests/trace/test_trace.py | 24 +++ 4 files changed, 174 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 183df40999..bbd3371593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.10.0-0.29b0...HEAD) +- Fix parsing of trace flags when extracting traceparent + ([#2577](https://github.com/open-telemetry/opentelemetry-python/pull/2577)) - Add default aggregation ([#2543](https://github.com/open-telemetry/opentelemetry-python/pull/2543)) - Fix incorrect installation of some exporter “convenience” packages into diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index 441a4c7eab..82cc078efc 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -79,7 +79,7 @@ def extract( trace_id=int(trace_id, 16), span_id=int(span_id, 16), is_remote=True, - trace_flags=trace.TraceFlags(trace_flags), + trace_flags=trace.TraceFlags(int(trace_flags, 16)), trace_state=tracestate, ) return trace.set_span_in_context( diff --git a/opentelemetry-api/tests/propagators/test_propagators.py b/opentelemetry-api/tests/propagators/test_propagators.py index 74c4231e69..a8fed620be 100644 --- a/opentelemetry-api/tests/propagators/test_propagators.py +++ b/opentelemetry-api/tests/propagators/test_propagators.py @@ -20,7 +20,9 @@ from unittest import TestCase from unittest.mock import Mock, patch +from opentelemetry import trace from opentelemetry.baggage.propagation import W3CBaggagePropagator +from opentelemetry.context.context import Context from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, @@ -44,6 +46,7 @@ def test_propagators(propagators): **{"side_effect": test_propagators} ) + # pylint: disable=import-outside-toplevel import opentelemetry.propagate reload(opentelemetry.propagate) @@ -79,6 +82,7 @@ def test_propagators(propagators): **{"side_effect": test_propagators} ) + # pylint: disable=import-outside-toplevel import opentelemetry.propagate reload(opentelemetry.propagate) @@ -88,6 +92,7 @@ def test_propagators(propagators): ) def test_composite_propagators_error(self): + # pylint: disable=import-outside-toplevel import opentelemetry.propagate with self.assertRaises(Exception): @@ -97,3 +102,145 @@ def test_composite_propagators_error(self): "Failed to load configured propagator `unknown`", err.output[0], ) + + +class TestTraceContextTextMapPropagator(TestCase): + def setUp(self): + self.propagator = TraceContextTextMapPropagator() + + def traceparent_helper( + self, + carrier, + ): + # We purposefully start with an empty context so we can test later if anything is added to it. + initial_context = Context() + + context = self.propagator.extract(carrier, context=initial_context) + self.assertIsNotNone(context) + self.assertIsInstance(context, Context) + + return context + + def traceparent_helper_generator( + self, + version=0x00, + trace_id=0x00000000000000000000000000000001, + span_id=0x0000000000000001, + trace_flags=0x00, + suffix="", + ): + traceparent = f"{version:02x}-{trace_id:032x}-{span_id:016x}-{trace_flags:02x}{suffix}" + carrier = {"traceparent": traceparent} + return self.traceparent_helper(carrier) + + def valid_traceparent_helper( + self, + version=0x00, + trace_id=0x00000000000000000000000000000001, + span_id=0x0000000000000001, + trace_flags=0x00, + suffix="", + assert_context_msg="A valid traceparent was provided, so the context should be non-empty.", + ): + context = self.traceparent_helper_generator( + version=version, + trace_id=trace_id, + span_id=span_id, + trace_flags=trace_flags, + suffix=suffix, + ) + + self.assertNotEqual( + context, + Context(), + assert_context_msg, + ) + + span = trace.get_current_span(context) + self.assertIsNotNone(span) + self.assertIsInstance(span, trace.span.Span) + + span_context = span.get_span_context() + self.assertIsNotNone(span_context) + self.assertIsInstance(span_context, trace.span.SpanContext) + + # Note: No version in SpanContext, it is only used locally in TraceContextTextMapPropagator + self.assertEqual(span_context.trace_id, trace_id) + self.assertEqual(span_context.span_id, span_id) + self.assertEqual(span_context.trace_flags, trace_flags) + + self.assertIsInstance(span_context.trace_state, trace.TraceState) + self.assertCountEqual(span_context.trace_state, []) + self.assertEqual(span_context.is_remote, True) + + return context, span, span_context + + def invalid_traceparent_helper( + self, + version=0x00, + trace_id=0x00000000000000000000000000000001, + span_id=0x0000000000000001, + trace_flags=0x00, + suffix="", + assert_context_msg="An invalid traceparent was provided, so the context should still be empty.", + ): + context = self.traceparent_helper_generator( + version=version, + trace_id=trace_id, + span_id=span_id, + trace_flags=trace_flags, + suffix=suffix, + ) + + self.assertEqual( + context, + Context(), + assert_context_msg, + ) + + return context + + def test_extract_nothing(self): + context = self.traceparent_helper(carrier={}) + self.assertEqual( + context, + {}, + "We didn't provide a valid traceparent, so we should still have an empty Context.", + ) + + def test_extract_simple_traceparent(self): + self.valid_traceparent_helper() + + # https://www.w3.org/TR/trace-context/#version + def test_extract_version_forbidden_ff(self): + self.invalid_traceparent_helper( + version=0xFF, + assert_context_msg="We provided ann invalid traceparent with a forbidden version=0xff, so the context should still be empty.", + ) + + # https://www.w3.org/TR/trace-context/#version-format + def test_extract_version_00_with_unsupported_suffix(self): + self.invalid_traceparent_helper( + suffix="-f00", + assert_context_msg="We provided an invalid traceparent with version=0x00 and suffix information which is not supported in this version, so the context should still be empty.", + ) + + # https://www.w3.org/TR/trace-context/#versioning-of-traceparent + # See the parsing of the sampled bit of flags. + def test_extract_future_version_with_future_suffix_data(self): + self.valid_traceparent_helper( + version=0x99, + suffix="-f00", + assert_context_msg="We provided a traceparent that is possibly valid in the future with version=0x99 and suffix information, so the context be non-empty.", + ) + + # https://www.w3.org/TR/trace-context/#trace-id + def test_extract_trace_id_invalid_all_zeros(self): + self.invalid_traceparent_helper(trace_id=0) + + # https://www.w3.org/TR/trace-context/#parent-id + def test_extract_span_id_invalid_all_zeros(self): + self.invalid_traceparent_helper(span_id=0) + + def test_extract_non_decimal_trace_flags(self): + self.valid_traceparent_helper(trace_flags=0xA0) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 1ccada0e5a..ad16578973 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -1745,3 +1745,27 @@ def _test_span_no_limits(self, tracer): self.assertEqual(len(root.attributes), num_attributes) for attr_val in root.attributes.values(): self.assertEqual(attr_val, self.long_val) + + +class TestTraceFlags(unittest.TestCase): + def test_constant_default(self): + self.assertEqual(trace_api.TraceFlags.DEFAULT, 0) + + def test_constant_sampled(self): + self.assertEqual(trace_api.TraceFlags.SAMPLED, 1) + + def test_get_default(self): + self.assertEqual( + trace_api.TraceFlags.get_default(), trace_api.TraceFlags.DEFAULT + ) + + def test_sampled_true(self): + self.assertTrue(trace_api.TraceFlags(0xF1).sampled) + + def test_sampled_false(self): + self.assertFalse(trace_api.TraceFlags(0xF0).sampled) + + def test_constant_default_trace_options(self): + self.assertEqual( + trace_api.DEFAULT_TRACE_OPTIONS, trace_api.TraceFlags.DEFAULT + ) From f81381cf8aca64a707d934f20c6c27d40b949dce Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 4 Apr 2022 15:22:51 -0700 Subject: [PATCH 1181/1517] Fix dependencies for test utils (#2587) --- tests/opentelemetry-test-utils/setup.cfg | 3 ++- tox.ini | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/opentelemetry-test-utils/setup.cfg b/tests/opentelemetry-test-utils/setup.cfg index dc2c84d6d4..67d67d5942 100644 --- a/tests/opentelemetry-test-utils/setup.cfg +++ b/tests/opentelemetry-test-utils/setup.cfg @@ -40,9 +40,10 @@ packages=find_namespace: install_requires = opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 + asgiref ~= 3.0 [options.extras_require] -test = flask~=2.0 +test = [options.packages.find] where = src diff --git a/tox.ini b/tox.ini index 8d5973b166..e3a1c6e1a3 100644 --- a/tox.ini +++ b/tox.ini @@ -198,6 +198,9 @@ commands_pre = python -m pip install -e {toxinidir}/opentelemetry-api[test] python -m pip install -e {toxinidir}/opentelemetry-semantic-conventions[test] python -m pip install -e {toxinidir}/opentelemetry-sdk[test] + # Pin protobuf version due to lint failing on v3.20.0 + # https://github.com/protocolbuffers/protobuf/issues/9730 + python -m pip install protobuf==3.19.4 python -m pip install -e {toxinidir}/opentelemetry-proto[test] python -m pip install -e {toxinidir}/tests/opentelemetry-test-utils[test] python -m pip install -e {toxinidir}/shim/opentelemetry-opentracing-shim[test] From f0cfd5de066ff4913a1fc367957b1ab2106b33ee Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 11 Apr 2022 15:38:39 -0600 Subject: [PATCH 1182/1517] Fix bleach version (#2596) Fixes #2595 --- dev-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 5051ddac2a..77caf0ff2e 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -17,3 +17,4 @@ protobuf~=3.18.1 # See https://github.com/pallets/markupsafe/issues/282 # breaking change introduced in markupsafe causes jinja, flask to break markupsafe==2.0.1 +bleach==4.1.0 # This dependency was updated to a breaking version. From df3ffc36c841d73694d2a7744a9c7950ca101d37 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 11 Apr 2022 16:41:04 -0700 Subject: [PATCH 1183/1517] [docs] update maintainers (#2592) Co-authored-by: Diego Hurtado --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b31231e9df..550e08936d 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,6 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Aaron Abbott](https://github.com/aabmass), Google - [Alex Boten](https://github.com/codeboten), Lightstep -- [Srikanth Chekuri](https://github.com/lonewolf3739) - [Owais Lone](https://github.com/owais), Splunk - [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), AWS @@ -147,6 +146,7 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t - [Diego Hurtado](https://github.com/ocelotl), Lightstep - [Leighton Chen](https://github.com/lzchen), Microsoft +- [Srikanth Chekuri](https://github.com/srikanthccv) *For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer).* From e113d541aeb650438af71e2bad1aabef5423272a Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 12 Apr 2022 11:07:21 -0700 Subject: [PATCH 1184/1517] Fix typo in example (#2597) --- opentelemetry-api/src/opentelemetry/propagate/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index 2963ee6c2e..5493c5f088 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -34,10 +34,10 @@ import flask import requests - from opentelemetry import propagators + from opentelemetry import propagate - PROPAGATOR = propagators.get_global_textmap() + PROPAGATOR = propagate.get_global_textmap() def get_header_from_flask_request(request, key): From cb60b549a59324f731d454432bcd5f1b392033b7 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 12 Apr 2022 15:57:12 -0600 Subject: [PATCH 1185/1517] Add check for duplicate instrument names (#2409) * Add check for duplicate instrument names Fixes #2142 * Add repeated instrument names for SDK as well * Update opentelemetry-api/src/opentelemetry/_metrics/__init__.py Co-authored-by: Srikanth Chekuri * Add instrument names lock * Replace raised exception for a warning * Added some fiexes * Add double check * Add type annotations * Update opentelemetry-api/src/opentelemetry/_metrics/__init__.py Co-authored-by: Srikanth Chekuri * Move check invocation to SDK * Fix lint * Separate instrument checking from warning logging * Use consistent class * Fix typo in example (#2597) * Fix docs Co-authored-by: Srikanth Chekuri Co-authored-by: Alex Boten --- .../src/opentelemetry/_metrics/__init__.py | 91 +++++++++++++++++- opentelemetry-api/tests/metrics/test_meter.py | 40 +++++++- .../opentelemetry/sdk/_metrics/__init__.py | 95 +++++++++++++++++-- .../tests/metrics/test_metrics.py | 39 +++++++- 4 files changed, 249 insertions(+), 16 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index a2f430c5f7..b3a2ee5563 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -26,7 +26,7 @@ from logging import getLogger from os import environ from threading import Lock -from typing import List, Optional, cast +from typing import List, Optional, Set, cast from opentelemetry._metrics.instrument import ( Counter, @@ -118,7 +118,8 @@ def __init__(self, name, version=None, schema_url=None): self._name = name self._version = version self._schema_url = schema_url - self._instrument_names = set() + self._instrument_ids: Set[str] = set() + self._instrument_ids_lock = Lock() @property def name(self): @@ -132,7 +133,27 @@ def version(self): def schema_url(self): return self._schema_url - # FIXME check that the instrument name has not been used already + def _check_instrument_id( + self, name: str, type_: type, unit: str, description: str + ) -> bool: + """ + Check if an instrument with the same name, type, unit and description + has been registered already. + """ + + result = False + + instrument_id = ",".join( + [name.strip().lower(), type_.__name__, unit, description] + ) + + with self._instrument_ids_lock: + if instrument_id in self._instrument_ids: + result = True + else: + self._instrument_ids.add(instrument_id) + + return result @abstractmethod def create_counter(self, name, unit="", description="") -> Counter: @@ -401,6 +422,15 @@ class NoOpMeter(Meter): def create_counter(self, name, unit="", description="") -> Counter: """Returns a no-op Counter.""" super().create_counter(name, unit=unit, description=description) + if self._check_instrument_id(name, DefaultCounter, unit, description): + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + Counter.__name__, + unit, + description, + ) return DefaultCounter(name, unit=unit, description=description) def create_up_down_counter( @@ -410,6 +440,17 @@ def create_up_down_counter( super().create_up_down_counter( name, unit=unit, description=description ) + if self._check_instrument_id( + name, DefaultUpDownCounter, unit, description + ): + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + UpDownCounter.__name__, + unit, + description, + ) return DefaultUpDownCounter(name, unit=unit, description=description) def create_observable_counter( @@ -419,6 +460,17 @@ def create_observable_counter( super().create_observable_counter( name, callback, unit=unit, description=description ) + if self._check_instrument_id( + name, DefaultObservableCounter, unit, description + ): + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + ObservableCounter.__name__, + unit, + description, + ) return DefaultObservableCounter( name, callback, @@ -429,6 +481,17 @@ def create_observable_counter( def create_histogram(self, name, unit="", description="") -> Histogram: """Returns a no-op Histogram.""" super().create_histogram(name, unit=unit, description=description) + if self._check_instrument_id( + name, DefaultHistogram, unit, description + ): + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + Histogram.__name__, + unit, + description, + ) return DefaultHistogram(name, unit=unit, description=description) def create_observable_gauge( @@ -438,6 +501,17 @@ def create_observable_gauge( super().create_observable_gauge( name, callback, unit=unit, description=description ) + if self._check_instrument_id( + name, DefaultObservableGauge, unit, description + ): + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + ObservableGauge.__name__, + unit, + description, + ) return DefaultObservableGauge( name, callback, @@ -452,6 +526,17 @@ def create_observable_up_down_counter( super().create_observable_up_down_counter( name, callback, unit=unit, description=description ) + if self._check_instrument_id( + name, DefaultObservableUpDownCounter, unit, description + ): + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + ObservableUpDownCounter.__name__, + unit, + description, + ) return DefaultObservableUpDownCounter( name, callback, diff --git a/opentelemetry-api/tests/metrics/test_meter.py b/opentelemetry-api/tests/metrics/test_meter.py index 6834df31f8..3b7f702dfb 100644 --- a/opentelemetry-api/tests/metrics/test_meter.py +++ b/opentelemetry-api/tests/metrics/test_meter.py @@ -13,9 +13,11 @@ # limitations under the License. # type: ignore +from logging import WARNING from unittest import TestCase +from unittest.mock import Mock -from opentelemetry._metrics import Meter +from opentelemetry._metrics import Meter, NoOpMeter # FIXME Test that the meter methods can be called concurrently safely. @@ -53,6 +55,42 @@ def create_observable_up_down_counter( class TestMeter(TestCase): + def test_repeated_instrument_names(self): + + try: + test_meter = NoOpMeter("name") + + test_meter.create_counter("counter") + test_meter.create_up_down_counter("up_down_counter") + test_meter.create_observable_counter("observable_counter", Mock()) + test_meter.create_histogram("histogram") + test_meter.create_observable_gauge("observable_gauge", Mock()) + test_meter.create_observable_up_down_counter( + "observable_up_down_counter", Mock() + ) + except Exception as error: + self.fail(f"Unexpected exception raised {error}") + + for instrument_name in [ + "counter", + "up_down_counter", + "histogram", + ]: + with self.assertLogs(level=WARNING): + getattr(test_meter, f"create_{instrument_name}")( + instrument_name + ) + + for instrument_name in [ + "observable_counter", + "observable_gauge", + "observable_up_down_counter", + ]: + with self.assertLogs(level=WARNING): + getattr(test_meter, f"create_{instrument_name}")( + instrument_name, Mock() + ) + def test_create_counter(self): """ Test that the meter provides a function to create a new Counter diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 8a87bb1fa2..fc71a15cf7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -64,7 +64,20 @@ def __init__( self._instrumentation_info = instrumentation_info self._measurement_consumer = measurement_consumer - def create_counter(self, name, unit=None, description=None) -> APICounter: + def create_counter(self, name, unit="", description="") -> APICounter: + if self._check_instrument_id(name, Counter, unit, description): + # FIXME #2558 go through all views here and check if this + # instrument registration conflict can be fixed. If it can be, do + # not log the following warning. + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + APICounter.__name__, + unit, + description, + ) + return Counter( name, self._instrumentation_info, @@ -74,8 +87,21 @@ def create_counter(self, name, unit=None, description=None) -> APICounter: ) def create_up_down_counter( - self, name, unit=None, description=None + self, name, unit="", description="" ) -> APIUpDownCounter: + if self._check_instrument_id(name, UpDownCounter, unit, description): + # FIXME #2558 go through all views here and check if this + # instrument registration conflict can be fixed. If it can be, do + # not log the following warning. + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + APIUpDownCounter.__name__, + unit, + description, + ) + return UpDownCounter( name, self._instrumentation_info, @@ -85,9 +111,22 @@ def create_up_down_counter( ) def create_observable_counter( - self, name, callback, unit=None, description=None + self, name, callback, unit="", description="" ) -> APIObservableCounter: - + if self._check_instrument_id( + name, ObservableCounter, unit, description + ): + # FIXME #2558 go through all views here and check if this + # instrument registration conflict can be fixed. If it can be, do + # not log the following warning. + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + APIObservableCounter.__name__, + unit, + description, + ) instrument = ObservableCounter( name, self._instrumentation_info, @@ -101,9 +140,19 @@ def create_observable_counter( return instrument - def create_histogram( - self, name, unit=None, description=None - ) -> APIHistogram: + def create_histogram(self, name, unit="", description="") -> APIHistogram: + if self._check_instrument_id(name, Histogram, unit, description): + # FIXME #2558 go through all views here and check if this + # instrument registration conflict can be fixed. If it can be, do + # not log the following warning. + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + APIHistogram.__name__, + unit, + description, + ) return Histogram( name, self._instrumentation_info, @@ -113,8 +162,20 @@ def create_histogram( ) def create_observable_gauge( - self, name, callback, unit=None, description=None + self, name, callback, unit="", description="" ) -> APIObservableGauge: + if self._check_instrument_id(name, ObservableGauge, unit, description): + # FIXME #2558 go through all views here and check if this + # instrument registration conflict can be fixed. If it can be, do + # not log the following warning. + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + APIObservableGauge.__name__, + unit, + description, + ) instrument = ObservableGauge( name, @@ -130,8 +191,22 @@ def create_observable_gauge( return instrument def create_observable_up_down_counter( - self, name, callback, unit=None, description=None + self, name, callback, unit="", description="" ) -> APIObservableUpDownCounter: + if self._check_instrument_id( + name, ObservableUpDownCounter, unit, description + ): + # FIXME #2558 go through all views here and check if this + # instrument registration conflict can be fixed. If it can be, do + # not log the following warning. + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + APIObservableUpDownCounter.__name__, + unit, + description, + ) instrument = ObservableUpDownCounter( name, @@ -280,6 +355,8 @@ def get_meter( info = InstrumentationInfo(name, version, schema_url) with self._meter_lock: if not self._meters.get(info): + # FIXME #2558 pass SDKConfig object to meter so that the meter + # has access to views. self._meters[info] = Meter( info, self._measurement_consumer, diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 07d9894c96..d33002c921 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -243,17 +243,17 @@ def test_register_asynchronous_instrument( meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( meter_provider.get_meter("name").create_observable_counter( - "name", Mock() + "name0", Mock() ) ) meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( meter_provider.get_meter("name").create_observable_up_down_counter( - "name", Mock() + "name1", Mock() ) ) meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( meter_provider.get_meter("name").create_observable_gauge( - "name", Mock() + "name2", Mock() ) ) @@ -298,6 +298,39 @@ class TestMeter(TestCase): def setUp(self): self.meter = Meter(Mock(), Mock()) + def test_repeated_instrument_names(self): + try: + self.meter.create_counter("counter") + self.meter.create_up_down_counter("up_down_counter") + self.meter.create_observable_counter("observable_counter", Mock()) + self.meter.create_histogram("histogram") + self.meter.create_observable_gauge("observable_gauge", Mock()) + self.meter.create_observable_up_down_counter( + "observable_up_down_counter", Mock() + ) + except Exception as error: + self.fail(f"Unexpected exception raised {error}") + + for instrument_name in [ + "counter", + "up_down_counter", + "histogram", + ]: + with self.assertLogs(level=WARNING): + getattr(self.meter, f"create_{instrument_name}")( + instrument_name + ) + + for instrument_name in [ + "observable_counter", + "observable_gauge", + "observable_up_down_counter", + ]: + with self.assertLogs(level=WARNING): + getattr(self.meter, f"create_{instrument_name}")( + instrument_name, Mock() + ) + def test_create_counter(self): counter = self.meter.create_counter( "name", unit="unit", description="description" From ee2a0f4915a5c7021c390747117f5353fae39b3d Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 13 Apr 2022 08:50:18 -0700 Subject: [PATCH 1186/1517] Include min/max information in Histogram point (#2581) * hist * changelog * tests * agg Co-authored-by: Diego Hurtado --- CHANGELOG.md | 2 + .../metrics/test_otlp_metrics_exporter.py | 10 +- .../tests/test_prometheus_exporter.py | 10 +- .../opentelemetry/sdk/_metrics/aggregation.py | 26 ++++- .../src/opentelemetry/sdk/_metrics/point.py | 12 +- .../tests/metrics/test_aggregation.py | 104 +++++++++++------- opentelemetry-sdk/tests/metrics/test_point.py | 16 +-- 7 files changed, 115 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbd3371593..3828829336 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2547](https://github.com/open-telemetry/opentelemetry-python/pull/2547)) - Update otlp-proto-grpc and otlp-proto-http exporters to have more lax requirements for `backoff` lib ([#2575](https://github.com/open-telemetry/opentelemetry-python/pull/2575)) +- Add min/max to histogram point + ([#2581](https://github.com/open-telemetry/opentelemetry-python/pull/2581)) ## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index b6d0d5b9b0..4a171b7986 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -117,12 +117,14 @@ def setUp(self): "histogram": _generate_metric( "histogram", Histogram( - time_unix_nano=1641946016139533244, - start_time_unix_nano=1641946016139533244, + aggregation_temporality=AggregationTemporality.DELTA, bucket_counts=[1, 4], - sum=67, explicit_bounds=[10.0, 20.0], - aggregation_temporality=AggregationTemporality.DELTA, + max=18, + min=8, + start_time_unix_nano=1641946016139533244, + sum=67, + time_unix_nano=1641946016139533244, ), ), } diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 092ed54481..445c9448ad 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -60,12 +60,14 @@ def test_histogram_to_prometheus(self): record = _generate_metric( "test@name", Histogram( - time_unix_nano=1641946016139533244, - start_time_unix_nano=1641946016139533244, + aggregation_temporality=AggregationTemporality.CUMULATIVE, bucket_counts=[1, 3, 2], - sum=579.0, explicit_bounds=[123.0, 456.0], - aggregation_temporality=AggregationTemporality.CUMULATIVE, + start_time_unix_nano=1641946016139533244, + max=457, + min=1, + sum=579.0, + time_unix_nano=1641946016139533244, ), attributes={"histo": 1}, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 8a61e78e72..c6ecff0cfa 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -267,18 +267,24 @@ def collect(self) -> HistogramPoint: value = self._bucket_counts start_time_unix_nano = self._start_time_unix_nano histogram_sum = self._sum + histogram_max = self._max + histogram_min = self._min self._bucket_counts = self._get_empty_bucket_counts() self._start_time_unix_nano = now + 1 self._sum = 0 + self._min = inf + self._max = -inf return HistogramPoint( - start_time_unix_nano=start_time_unix_nano, - time_unix_nano=now, + aggregation_temporality=AggregationTemporality.DELTA, bucket_counts=tuple(value), explicit_bounds=self._boundaries, - aggregation_temporality=AggregationTemporality.DELTA, + max=histogram_max, + min=histogram_min, + start_time_unix_nano=start_time_unix_nano, sum=histogram_sum, + time_unix_nano=now, ) @@ -374,9 +380,15 @@ def _convert_aggregation_temporality( if current_point.aggregation_temporality is aggregation_temporality: return current_point + max_ = current_point.max + min_ = current_point.min + if aggregation_temporality is AggregationTemporality.CUMULATIVE: start_time_unix_nano = previous_point.start_time_unix_nano sum_ = current_point.sum + previous_point.sum + # Only update min/max on delta -> cumulative + max_ = max(current_point.max, previous_point.max) + min_ = min(current_point.min, previous_point.min) bucket_counts = [ curr_count + prev_count for curr_count, prev_count in zip( @@ -394,12 +406,14 @@ def _convert_aggregation_temporality( ] return HistogramPoint( - start_time_unix_nano=start_time_unix_nano, - time_unix_nano=current_point.time_unix_nano, + aggregation_temporality=aggregation_temporality, bucket_counts=bucket_counts, explicit_bounds=current_point.explicit_bounds, + max=max_, + min=min_, + start_time_unix_nano=start_time_unix_nano, sum=sum_, - aggregation_temporality=aggregation_temporality, + time_unix_nano=current_point.time_unix_nano, ) return None diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py index 8cc3a9bcd1..fcc127e949 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py @@ -30,11 +30,11 @@ class AggregationTemporality(IntEnum): @dataclass(frozen=True) class Sum: + aggregation_temporality: AggregationTemporality + is_monotonic: bool start_time_unix_nano: int time_unix_nano: int value: Union[int, float] - aggregation_temporality: AggregationTemporality - is_monotonic: bool @dataclass(frozen=True) @@ -45,12 +45,14 @@ class Gauge: @dataclass(frozen=True) class Histogram: - start_time_unix_nano: int - time_unix_nano: int + aggregation_temporality: AggregationTemporality bucket_counts: Sequence[int] explicit_bounds: Sequence[float] + max: int + min: int + start_time_unix_nano: int sum: Union[int, float] - aggregation_temporality: AggregationTemporality + time_unix_nano: int PointT = Union[Sum, Gauge, Histogram] diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 7a00a8b321..99f3478267 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -624,12 +624,14 @@ class TestHistogramConvertAggregationTemporality(TestCase): def test_previous_point_none(self): current_point = HistogramPoint( - start_time_unix_nano=0, - time_unix_nano=1, + aggregation_temporality=AggregationTemporality.DELTA, bucket_counts=[0, 2, 1, 2, 0], explicit_bounds=[0, 5, 10, 25], + max=24, + min=2, + start_time_unix_nano=0, sum=70, - aggregation_temporality=AggregationTemporality.DELTA, + time_unix_nano=1, ) self.assertEqual( @@ -648,42 +650,50 @@ def test_previous_point_non_cumulative(self): _convert_aggregation_temporality( HistogramPoint( - start_time_unix_nano=0, - time_unix_nano=1, + aggregation_temporality=AggregationTemporality.DELTA, bucket_counts=[0, 2, 1, 2, 0], explicit_bounds=[0, 5, 10, 25], + max=24, + min=2, + start_time_unix_nano=0, sum=70, - aggregation_temporality=AggregationTemporality.DELTA, + time_unix_nano=1, ), HistogramPoint( - start_time_unix_nano=1, - time_unix_nano=2, + aggregation_temporality=AggregationTemporality.DELTA, bucket_counts=[0, 1, 3, 0, 0], explicit_bounds=[0, 5, 10, 25], + max=9, + min=2, + start_time_unix_nano=1, sum=35, - aggregation_temporality=AggregationTemporality.DELTA, + time_unix_nano=2, ), AggregationTemporality.DELTA, ), def test_same_aggregation_temporality_cumulative(self): current_point = HistogramPoint( - start_time_unix_nano=0, - time_unix_nano=2, + aggregation_temporality=AggregationTemporality.CUMULATIVE, bucket_counts=[0, 3, 4, 2, 0], explicit_bounds=[0, 5, 10, 25], + max=24, + min=1, + start_time_unix_nano=0, sum=105, - aggregation_temporality=AggregationTemporality.CUMULATIVE, + time_unix_nano=2, ) self.assertEqual( _convert_aggregation_temporality( HistogramPoint( - start_time_unix_nano=0, - time_unix_nano=1, + aggregation_temporality=AggregationTemporality.CUMULATIVE, bucket_counts=[0, 2, 1, 2, 0], explicit_bounds=[0, 5, 10, 25], + max=24, + min=1, + start_time_unix_nano=0, sum=70, - aggregation_temporality=AggregationTemporality.CUMULATIVE, + time_unix_nano=1, ), current_point, AggregationTemporality.CUMULATIVE, @@ -693,23 +703,27 @@ def test_same_aggregation_temporality_cumulative(self): def test_same_aggregation_temporality_delta(self): current_point = HistogramPoint( - start_time_unix_nano=1, - time_unix_nano=2, + aggregation_temporality=AggregationTemporality.DELTA, bucket_counts=[0, 1, 3, 0, 0], explicit_bounds=[0, 5, 10, 25], + max=9, + min=1, + start_time_unix_nano=1, sum=35, - aggregation_temporality=AggregationTemporality.DELTA, + time_unix_nano=2, ) self.assertEqual( _convert_aggregation_temporality( HistogramPoint( - start_time_unix_nano=0, - time_unix_nano=2, + aggregation_temporality=AggregationTemporality.CUMULATIVE, bucket_counts=[0, 3, 4, 2, 0], explicit_bounds=[0, 5, 10, 25], + max=24, + min=1, + start_time_unix_nano=0, sum=105, - aggregation_temporality=AggregationTemporality.CUMULATIVE, + time_unix_nano=2, ), current_point, AggregationTemporality.DELTA, @@ -719,67 +733,79 @@ def test_same_aggregation_temporality_delta(self): def test_aggregation_temporality_to_cumulative(self): current_point = HistogramPoint( - start_time_unix_nano=1, - time_unix_nano=2, + aggregation_temporality=AggregationTemporality.DELTA, bucket_counts=[0, 1, 3, 0, 0], explicit_bounds=[0, 5, 10, 25], + max=24, + min=1, + start_time_unix_nano=1, sum=35, - aggregation_temporality=AggregationTemporality.DELTA, + time_unix_nano=2, ) self.assertEqual( _convert_aggregation_temporality( HistogramPoint( - start_time_unix_nano=0, - time_unix_nano=1, + aggregation_temporality=AggregationTemporality.CUMULATIVE, bucket_counts=[0, 2, 1, 2, 0], explicit_bounds=[0, 5, 10, 25], + max=24, + min=1, + start_time_unix_nano=0, sum=70, - aggregation_temporality=AggregationTemporality.CUMULATIVE, + time_unix_nano=1, ), current_point, AggregationTemporality.CUMULATIVE, ), HistogramPoint( - start_time_unix_nano=0, - time_unix_nano=2, + aggregation_temporality=AggregationTemporality.CUMULATIVE, bucket_counts=[0, 3, 4, 2, 0], explicit_bounds=[0, 5, 10, 25], + max=24, + min=1, + start_time_unix_nano=0, sum=105, - aggregation_temporality=AggregationTemporality.CUMULATIVE, + time_unix_nano=2, ), ) def test_aggregation_temporality_to_delta(self): current_point = HistogramPoint( - start_time_unix_nano=0, - time_unix_nano=2, + aggregation_temporality=AggregationTemporality.CUMULATIVE, bucket_counts=[0, 3, 4, 2, 0], explicit_bounds=[0, 5, 10, 25], + max=22, + min=3, + start_time_unix_nano=0, sum=105, - aggregation_temporality=AggregationTemporality.CUMULATIVE, + time_unix_nano=2, ) self.assertEqual( _convert_aggregation_temporality( HistogramPoint( - start_time_unix_nano=0, - time_unix_nano=1, + aggregation_temporality=AggregationTemporality.CUMULATIVE, bucket_counts=[0, 2, 1, 2, 0], explicit_bounds=[0, 5, 10, 25], + max=24, + min=1, + start_time_unix_nano=0, sum=70, - aggregation_temporality=AggregationTemporality.CUMULATIVE, + time_unix_nano=1, ), current_point, AggregationTemporality.DELTA, ), HistogramPoint( - start_time_unix_nano=1, - time_unix_nano=2, + aggregation_temporality=AggregationTemporality.DELTA, bucket_counts=[0, 1, 3, 0, 0], explicit_bounds=[0, 5, 10, 25], + max=22, + min=3, + start_time_unix_nano=1, sum=35, - aggregation_temporality=AggregationTemporality.DELTA, + time_unix_nano=2, ), ) diff --git a/opentelemetry-sdk/tests/metrics/test_point.py b/opentelemetry-sdk/tests/metrics/test_point.py index ff741115fc..0e5d99a726 100644 --- a/opentelemetry-sdk/tests/metrics/test_point.py +++ b/opentelemetry-sdk/tests/metrics/test_point.py @@ -37,15 +37,15 @@ class TestDatapointToJSON(TestCase): def test_sum(self): point = _create_metric( Sum( + aggregation_temporality=2, + is_monotonic=True, start_time_unix_nano=10, time_unix_nano=20, value=9, - aggregation_temporality=2, - is_monotonic=True, ) ) self.assertEqual( - '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_info": "InstrumentationInfo(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"start_time_unix_nano": 10, "time_unix_nano": 20, "value": 9, "aggregation_temporality": 2, "is_monotonic": true}}', + '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_info": "InstrumentationInfo(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"aggregation_temporality": 2, "is_monotonic": true, "start_time_unix_nano": 10, "time_unix_nano": 20, "value": 9}}', point.to_json(), ) @@ -59,16 +59,18 @@ def test_gauge(self): def test_histogram(self): point = _create_metric( Histogram( - start_time_unix_nano=50, - time_unix_nano=60, + aggregation_temporality=1, bucket_counts=[0, 0, 1, 0], explicit_bounds=[0.1, 0.5, 0.9, 1], - aggregation_temporality=1, + max=0.8, + min=0.8, + start_time_unix_nano=50, sum=0.8, + time_unix_nano=60, ) ) self.maxDiff = None self.assertEqual( - '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_info": "InstrumentationInfo(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"start_time_unix_nano": 50, "time_unix_nano": 60, "bucket_counts": [0, 0, 1, 0], "explicit_bounds": [0.1, 0.5, 0.9, 1], "sum": 0.8, "aggregation_temporality": 1}}', + '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_info": "InstrumentationInfo(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"aggregation_temporality": 1, "bucket_counts": [0, 0, 1, 0], "explicit_bounds": [0.1, 0.5, 0.9, 1], "max": 0.8, "min": 0.8, "start_time_unix_nano": 50, "sum": 0.8, "time_unix_nano": 60}}', point.to_json(), ) From 77d96baa7b81278f40d4675168b318fa695364a0 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 13 Apr 2022 12:51:51 -0600 Subject: [PATCH 1187/1517] Use exceptions to report shutdown result (#2599) * Use exceptions to report shutdown result Fixes #2406 * Fix mypy * Make exceptions private * Fix exceptions module path * Remove exceptions and show failed metric readers --- .../opentelemetry/sdk/_metrics/__init__.py | 32 ++++++++++++----- .../sdk/_metrics/export/__init__.py | 9 +++-- .../sdk/_metrics/metric_reader.py | 2 +- .../metrics/test_in_memory_metric_reader.py | 2 +- .../tests/metrics/test_metrics.py | 36 ++++++++++++------- 5 files changed, 53 insertions(+), 28 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index fc71a15cf7..e78e69454c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -315,25 +315,39 @@ def _shutdown(): if not did_shutdown: _logger.warning("shutdown can only be called once") - return False + return - overall_result = True + metric_reader_error = {} for metric_reader in self._sdk_config.metric_readers: - metric_reader_result = metric_reader.shutdown() + try: + metric_reader.shutdown() - if not metric_reader_result: - _logger.warning( - "MetricReader %s failed to shutdown", metric_reader - ) + # pylint: disable=broad-except + except Exception as error: - overall_result = overall_result and metric_reader_result + metric_reader_error[metric_reader] = error if self._atexit_handler is not None: unregister(self._atexit_handler) self._atexit_handler = None - return overall_result + if metric_reader_error: + + metric_reader_error_string = "\n".join( + [ + f"{metric_reader.__class__.__name__}: {repr(error)}" + for metric_reader, error in metric_reader_error.items() + ] + ) + + raise Exception( + ( + "MeterProvider.shutdown failed because the following " + "metric readers failed during shutdown:\n" + f"{metric_reader_error_string}" + ) + ) def get_meter( self, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py index dc172a48af..9145f4e0fb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -122,8 +122,8 @@ def _receive_metrics(self, metrics: Iterable[Metric]): with self._lock: self._metrics = list(metrics) - def shutdown(self) -> bool: - return True + def shutdown(self): + pass class PeriodicExportingMetricReader(MetricReader): @@ -193,16 +193,15 @@ def _receive_metrics(self, metrics: Iterable[Metric]) -> None: _logger.exception("Exception while exporting metrics %s", str(e)) detach(token) - def shutdown(self) -> bool: + def shutdown(self): def _shutdown(): self._shutdown = True did_set = self._shutdown_once.do_once(_shutdown) if not did_set: _logger.warning("Can't shutdown multiple times") - return False + return self._shutdown_event.set() self._daemon_thread.join() self._exporter.shutdown() - return True diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py index 6b345587d6..e14877d87d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py @@ -65,7 +65,7 @@ def _receive_metrics(self, metrics: Iterable[Metric]): """Called by `MetricReader.collect` when it receives a batch of metrics""" @abstractmethod - def shutdown(self) -> bool: + def shutdown(self): """Shuts down the MetricReader. This method provides a way for the MetricReader to do any cleanup required. A metric reader can only be shutdown once, any subsequent calls are ignored and return diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py index f69fcea9e5..441b6df2d8 100644 --- a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py @@ -63,7 +63,7 @@ def test_converts_metrics_to_list(self): def test_shutdown(self): # shutdown should always be successful - self.assertTrue(InMemoryMetricReader().shutdown()) + self.assertIsNone(InMemoryMetricReader().shutdown()) def test_integration(self): reader = InMemoryMetricReader() diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index d33002c921..df51ee668c 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -151,33 +151,45 @@ def test_shutdown(self): mock_metric_reader_0 = MagicMock( **{ - "shutdown.return_value": False, - "__str__.return_value": "mock_metric_reader_0", + "shutdown.side_effect": ZeroDivisionError(), + } + ) + mock_metric_reader_1 = MagicMock( + **{ + "shutdown.side_effect": AssertionError(), } ) - mock_metric_reader_1 = Mock(**{"shutdown.return_value": True}) meter_provider = MeterProvider( metric_readers=[mock_metric_reader_0, mock_metric_reader_1] ) - with self.assertLogs(level=WARNING) as log: - self.assertFalse(meter_provider.shutdown()) - self.assertEqual( - log.records[0].getMessage(), - "MetricReader mock_metric_reader_0 failed to shutdown", - ) + with self.assertRaises(Exception) as error: + meter_provider.shutdown() + + error = error.exception + + self.assertEqual( + str(error), + ( + "MeterProvider.shutdown failed because the following " + "metric readers failed during shutdown:\n" + "MagicMock: ZeroDivisionError()\n" + "MagicMock: AssertionError()" + ), + ) + mock_metric_reader_0.shutdown.assert_called_once() mock_metric_reader_1.shutdown.assert_called_once() - mock_metric_reader_0 = Mock(**{"shutdown.return_value": True}) - mock_metric_reader_1 = Mock(**{"shutdown.return_value": True}) + mock_metric_reader_0 = Mock() + mock_metric_reader_1 = Mock() meter_provider = MeterProvider( metric_readers=[mock_metric_reader_0, mock_metric_reader_1] ) - self.assertTrue(meter_provider.shutdown()) + self.assertIsNone(meter_provider.shutdown()) mock_metric_reader_0.shutdown.assert_called_once() mock_metric_reader_1.shutdown.assert_called_once() From ba97fe99cc0b98049c25bc8ee96f10ebdaff542c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 14 Apr 2022 16:55:13 -0600 Subject: [PATCH 1188/1517] Add support for zero or more callbacks (#2602) * Add support for zero or more callbacks Fixes #2601 * Add changelog entry * Update opentelemetry-api/src/opentelemetry/_metrics/__init__.py Co-authored-by: Leighton Chen * Update opentelemetry-api/src/opentelemetry/_metrics/__init__.py Co-authored-by: Leighton Chen Co-authored-by: Leighton Chen --- CHANGELOG.md | 2 + .../src/opentelemetry/_metrics/__init__.py | 61 +++++----- .../src/opentelemetry/_metrics/instrument.py | 24 ++-- .../tests/metrics/test_instruments.py | 44 ++++---- .../tests/metrics/test_meter_provider.py | 18 +-- .../opentelemetry/sdk/_metrics/__init__.py | 12 +- .../opentelemetry/sdk/_metrics/instrument.py | 41 ++++--- .../metrics/integration_test/test_cpu_time.py | 4 +- .../tests/metrics/test_aggregation.py | 6 +- .../metrics/test_in_memory_metric_reader.py | 2 +- .../tests/metrics/test_instrument.py | 106 +++++++++++++++--- .../tests/metrics/test_metrics.py | 27 +++-- 12 files changed, 223 insertions(+), 124 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3828829336..29317e4f20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.10.0-0.29b0...HEAD) +- Add support for zero or more callbacks + ([#2602](https://github.com/open-telemetry/opentelemetry-python/pull/2602)) - Fix parsing of trace flags when extracting traceparent ([#2577](https://github.com/open-telemetry/opentelemetry-python/pull/2577)) - Add default aggregation diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index b3a2ee5563..0abe07b38b 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -181,12 +181,12 @@ def create_up_down_counter( @abstractmethod def create_observable_counter( - self, name, callback, unit="", description="" + self, name, callbacks=None, unit="", description="" ) -> ObservableCounter: """Creates an `ObservableCounter` instrument An observable counter observes a monotonically increasing count by - calling a provided callback which returns multiple + calling provided callbacks which returns multiple :class:`~opentelemetry._metrics.measurement.Measurement`. For example, an observable counter could be used to report system CPU @@ -207,7 +207,7 @@ def cpu_time_callback() -> Iterable[Measurement]: meter.create_observable_counter( "system.cpu.time", - callback=cpu_time_callback, + callbacks=[cpu_time_callback], unit="s", description="CPU time" ) @@ -225,8 +225,8 @@ def cpu_time_callback() -> Iterable[Measurement]: yield Measurement(int(states[1]) // 100, {"cpu": cpu, "state": "nice"}) # ... other states - Alternatively, you can pass a generator directly instead of a callback, - which should return iterables of + Alternatively, you can pass a sequence of generators directly instead + of a sequence of callbacks, which each should return iterables of :class:`~opentelemetry._metrics.measurement.Measurement`:: def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Measurement]]: @@ -246,16 +246,17 @@ def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Measurem meter.create_observable_counter( "system.cpu.time", - callback=cpu_time_callback({"user", "system"}), + callbacks=[cpu_time_callback({"user", "system"})], unit="s", description="CPU time" ) Args: name: The name of the instrument to be created - callback: A callback that returns an iterable of + callbacks: A sequence of callbacks that return an iterable of :class:`~opentelemetry._metrics.measurement.Measurement`. - Alternatively, can be a generator that yields iterables of + Alternatively, can be a sequence of generators that each yields + iterables of :class:`~opentelemetry._metrics.measurement.Measurement`. unit: The unit for measurements this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. @@ -275,13 +276,13 @@ def create_histogram(self, name, unit="", description="") -> Histogram: @abstractmethod def create_observable_gauge( - self, name, callback, unit="", description="" + self, name, callbacks=None, unit="", description="" ) -> ObservableGauge: """Creates an `ObservableGauge` instrument Args: name: The name of the instrument to be created - callback: A callback that returns an iterable of + callbacks: A sequence of callbacks that return an iterable of :class:`~opentelemetry._metrics.measurement.Measurement`. Alternatively, can be a generator that yields iterables of :class:`~opentelemetry._metrics.measurement.Measurement`. @@ -292,13 +293,13 @@ def create_observable_gauge( @abstractmethod def create_observable_up_down_counter( - self, name, callback, unit="", description="" + self, name, callbacks=None, unit="", description="" ) -> ObservableUpDownCounter: """Creates an `ObservableUpDownCounter` instrument Args: name: The name of the instrument to be created - callback: A callback that returns an iterable of + callbacks: A sequence of callbacks that return an iterable of :class:`~opentelemetry._metrics.measurement.Measurement`. Alternatively, can be a generator that yields iterables of :class:`~opentelemetry._metrics.measurement.Measurement`. @@ -358,15 +359,15 @@ def create_up_down_counter( return proxy def create_observable_counter( - self, name, callback, unit="", description="" + self, name, callbacks=None, unit="", description="" ) -> ObservableCounter: with self._lock: if self._real_meter: return self._real_meter.create_observable_counter( - name, callback, unit, description + name, callbacks, unit, description ) proxy = _ProxyObservableCounter( - name, callback, unit=unit, description=description + name, callbacks, unit=unit, description=description ) self._instruments.append(proxy) return proxy @@ -382,32 +383,32 @@ def create_histogram(self, name, unit="", description="") -> Histogram: return proxy def create_observable_gauge( - self, name, callback, unit="", description="" + self, name, callbacks=None, unit="", description="" ) -> ObservableGauge: with self._lock: if self._real_meter: return self._real_meter.create_observable_gauge( - name, callback, unit, description + name, callbacks, unit, description ) proxy = _ProxyObservableGauge( - name, callback, unit=unit, description=description + name, callbacks, unit=unit, description=description ) self._instruments.append(proxy) return proxy def create_observable_up_down_counter( - self, name, callback, unit="", description="" + self, name, callbacks=None, unit="", description="" ) -> ObservableUpDownCounter: with self._lock: if self._real_meter: return self._real_meter.create_observable_up_down_counter( name, - callback, + callbacks, unit, description, ) proxy = _ProxyObservableUpDownCounter( - name, callback, unit=unit, description=description + name, callbacks, unit=unit, description=description ) self._instruments.append(proxy) return proxy @@ -454,11 +455,11 @@ def create_up_down_counter( return DefaultUpDownCounter(name, unit=unit, description=description) def create_observable_counter( - self, name, callback, unit="", description="" + self, name, callbacks=None, unit="", description="" ) -> ObservableCounter: """Returns a no-op ObservableCounter.""" super().create_observable_counter( - name, callback, unit=unit, description=description + name, callbacks, unit=unit, description=description ) if self._check_instrument_id( name, DefaultObservableCounter, unit, description @@ -473,7 +474,7 @@ def create_observable_counter( ) return DefaultObservableCounter( name, - callback, + callbacks, unit=unit, description=description, ) @@ -495,11 +496,11 @@ def create_histogram(self, name, unit="", description="") -> Histogram: return DefaultHistogram(name, unit=unit, description=description) def create_observable_gauge( - self, name, callback, unit="", description="" + self, name, callbacks=None, unit="", description="" ) -> ObservableGauge: """Returns a no-op ObservableGauge.""" super().create_observable_gauge( - name, callback, unit=unit, description=description + name, callbacks, unit=unit, description=description ) if self._check_instrument_id( name, DefaultObservableGauge, unit, description @@ -514,17 +515,17 @@ def create_observable_gauge( ) return DefaultObservableGauge( name, - callback, + callbacks, unit=unit, description=description, ) def create_observable_up_down_counter( - self, name, callback, unit="", description="" + self, name, callbacks=None, unit="", description="" ) -> ObservableUpDownCounter: """Returns a no-op ObservableUpDownCounter.""" super().create_observable_up_down_counter( - name, callback, unit=unit, description=description + name, callbacks, unit=unit, description=description ) if self._check_instrument_id( name, DefaultObservableUpDownCounter, unit, description @@ -539,7 +540,7 @@ def create_observable_up_down_counter( ) return DefaultObservableUpDownCounter( name, - callback, + callbacks, unit=unit, description=description, ) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py index aee2a5359e..f707b05e9f 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py @@ -73,9 +73,9 @@ def _create_real_instrument(self, meter: "metrics.Meter") -> InstrumentT: class _ProxyAsynchronousInstrument(_ProxyInstrument[InstrumentT]): - def __init__(self, name, callback, unit, description) -> None: + def __init__(self, name, callbacks, unit, description) -> None: super().__init__(name, unit, description) - self._callback = callback + self._callbacks = callbacks class Synchronous(Instrument): @@ -87,7 +87,7 @@ class Asynchronous(Instrument): def __init__( self, name, - callback, + callbacks=None, unit="", description="", ): @@ -170,8 +170,8 @@ class ObservableCounter(_Monotonic, Asynchronous): class DefaultObservableCounter(ObservableCounter): - def __init__(self, name, callback, unit="", description=""): - super().__init__(name, callback, unit=unit, description=description) + def __init__(self, name, callbacks=None, unit="", description=""): + super().__init__(name, callbacks, unit=unit, description=description) class _ProxyObservableCounter( @@ -181,7 +181,7 @@ def _create_real_instrument( self, meter: "metrics.Meter" ) -> ObservableCounter: return meter.create_observable_counter( - self._name, self._callback, self._unit, self._description + self._name, self._callbacks, self._unit, self._description ) @@ -193,8 +193,8 @@ class ObservableUpDownCounter(_NonMonotonic, Asynchronous): class DefaultObservableUpDownCounter(ObservableUpDownCounter): - def __init__(self, name, callback, unit="", description=""): - super().__init__(name, callback, unit=unit, description=description) + def __init__(self, name, callbacks=None, unit="", description=""): + super().__init__(name, callbacks, unit=unit, description=description) class _ProxyObservableUpDownCounter( @@ -205,7 +205,7 @@ def _create_real_instrument( self, meter: "metrics.Meter" ) -> ObservableUpDownCounter: return meter.create_observable_up_down_counter( - self._name, self._callback, self._unit, self._description + self._name, self._callbacks, self._unit, self._description ) @@ -247,8 +247,8 @@ class ObservableGauge(_Grouping, Asynchronous): class DefaultObservableGauge(ObservableGauge): - def __init__(self, name, callback, unit="", description=""): - super().__init__(name, callback, unit=unit, description=description) + def __init__(self, name, callbacks=None, unit="", description=""): + super().__init__(name, callbacks, unit=unit, description=description) class _ProxyObservableGauge( @@ -259,5 +259,5 @@ def _create_real_instrument( self, meter: "metrics.Meter" ) -> ObservableGauge: return meter.create_observable_gauge( - self._name, self._callback, self._unit, self._description + self._name, self._callbacks, self._unit, self._description ) diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index b97a1640bd..34bc4cec80 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -118,7 +118,7 @@ def callback(): self.assertTrue( isinstance( NoOpMeter("name").create_observable_counter( - "name", callback() + "name", callbacks=[callback()] ), ObservableCounter, ) @@ -134,7 +134,7 @@ def test_api_observable_counter_abstract(self): def test_create_observable_counter_api(self): """ Test that the API for creating a observable_counter accepts the name of the instrument. - Test that the API for creating a observable_counter accepts a callback. + Test that the API for creating a observable_counter accepts a sequence of callbacks. Test that the API for creating a observable_counter accepts the unit of the instrument. Test that the API for creating a observable_counter accepts the description of the instrument """ @@ -153,11 +153,13 @@ def test_create_observable_counter_api(self): Meter.create_observable_counter ) self.assertIn( - "callback", create_observable_counter_signature.parameters.keys() + "callbacks", create_observable_counter_signature.parameters.keys() ) self.assertIs( - create_observable_counter_signature.parameters["callback"].default, - Signature.empty, + create_observable_counter_signature.parameters[ + "callbacks" + ].default, + None, ) create_observable_counter_signature = signature( Meter.create_observable_counter @@ -196,7 +198,7 @@ def test_observable_counter_generator(self): Meter.create_observable_counter ) self.assertIn( - "callback", create_observable_counter_signature.parameters.keys() + "callbacks", create_observable_counter_signature.parameters.keys() ) self.assertIs( create_observable_counter_signature.parameters["name"].default, @@ -285,7 +287,9 @@ def callback(): self.assertTrue( isinstance( - NoOpMeter("name").create_observable_gauge("name", callback()), + NoOpMeter("name").create_observable_gauge( + "name", [callback()] + ), ObservableGauge, ) ) @@ -300,7 +304,7 @@ def test_api_observable_gauge_abstract(self): def test_create_observable_gauge_api(self): """ Test that the API for creating a observable_gauge accepts the name of the instrument. - Test that the API for creating a observable_gauge accepts a callback. + Test that the API for creating a observable_gauge accepts a sequence of callbacks. Test that the API for creating a observable_gauge accepts the unit of the instrument. Test that the API for creating a observable_gauge accepts the description of the instrument """ @@ -319,11 +323,11 @@ def test_create_observable_gauge_api(self): Meter.create_observable_gauge ) self.assertIn( - "callback", create_observable_gauge_signature.parameters.keys() + "callbacks", create_observable_gauge_signature.parameters.keys() ) self.assertIs( - create_observable_gauge_signature.parameters["callback"].default, - Signature.empty, + create_observable_gauge_signature.parameters["callbacks"].default, + None, ) create_observable_gauge_signature = signature( Meter.create_observable_gauge @@ -350,7 +354,7 @@ def test_create_observable_gauge_api(self): def test_observable_gauge_callback(self): """ - Test that the API for creating a asynchronous gauge accepts a callback. + Test that the API for creating a asynchronous gauge accepts a sequence of callbacks. Test that the callback function reports measurements. Test that there is a way to pass state to the callback. """ @@ -359,7 +363,7 @@ def test_observable_gauge_callback(self): Meter.create_observable_gauge ) self.assertIn( - "callback", create_observable_gauge_signature.parameters.keys() + "callbacks", create_observable_gauge_signature.parameters.keys() ) self.assertIs( create_observable_gauge_signature.parameters["name"].default, @@ -461,7 +465,7 @@ def callback(): self.assertTrue( isinstance( NoOpMeter("name").create_observable_up_down_counter( - "name", callback() + "name", [callback()] ), ObservableUpDownCounter, ) @@ -477,7 +481,7 @@ def test_api_observable_up_down_counter_abstract(self): def test_create_observable_up_down_counter_api(self): """ Test that the API for creating a observable_up_down_counter accepts the name of the instrument. - Test that the API for creating a observable_up_down_counter accepts a callback. + Test that the API for creating a observable_up_down_counter accepts a sequence of callbacks. Test that the API for creating a observable_up_down_counter accepts the unit of the instrument. Test that the API for creating a observable_up_down_counter accepts the description of the instrument """ @@ -499,14 +503,14 @@ def test_create_observable_up_down_counter_api(self): Meter.create_observable_up_down_counter ) self.assertIn( - "callback", + "callbacks", create_observable_up_down_counter_signature.parameters.keys(), ) self.assertIs( create_observable_up_down_counter_signature.parameters[ - "callback" + "callbacks" ].default, - Signature.empty, + None, ) create_observable_up_down_counter_signature = signature( Meter.create_observable_up_down_counter @@ -538,7 +542,7 @@ def test_create_observable_up_down_counter_api(self): def test_observable_up_down_counter_callback(self): """ - Test that the API for creating a asynchronous up_down_counter accepts a callback. + Test that the API for creating a asynchronous up_down_counter accepts a sequence of callbacks. Test that the callback function reports measurements. Test that there is a way to pass state to the callback. Test that the instrument accepts positive and negative values. @@ -548,7 +552,7 @@ def test_observable_up_down_counter_callback(self): Meter.create_observable_up_down_counter ) self.assertIn( - "callback", + "callbacks", create_observable_up_down_counter_signature.parameters.keys(), ) self.assertIs( diff --git a/opentelemetry-api/tests/metrics/test_meter_provider.py b/opentelemetry-api/tests/metrics/test_meter_provider.py index 17325c4c03..9efcfeb33a 100644 --- a/opentelemetry-api/tests/metrics/test_meter_provider.py +++ b/opentelemetry-api/tests/metrics/test_meter_provider.py @@ -195,15 +195,15 @@ def test_proxy_meter(self): name, unit=unit, description=description ) proxy_observable_counter = proxy_meter.create_observable_counter( - name, callback=callback, unit=unit, description=description + name, callbacks=[callback], unit=unit, description=description ) proxy_observable_updowncounter = ( proxy_meter.create_observable_up_down_counter( - name, callback=callback, unit=unit, description=description + name, callbacks=[callback], unit=unit, description=description ) ) proxy_overvable_gauge = proxy_meter.create_observable_gauge( - name, callback=callback, unit=unit, description=description + name, callbacks=[callback], unit=unit, description=description ) self.assertIsInstance(proxy_counter, _ProxyCounter) self.assertIsInstance(proxy_updowncounter, _ProxyUpDownCounter) @@ -243,13 +243,13 @@ def test_proxy_meter(self): name, unit, description ) real_meter.create_observable_counter.assert_called_once_with( - name, callback, unit, description + name, [callback], unit, description ) real_meter.create_observable_up_down_counter.assert_called_once_with( - name, callback, unit, description + name, [callback], unit, description ) real_meter.create_observable_gauge.assert_called_once_with( - name, callback, unit, description + name, [callback], unit, description ) # The synchronous instrument measurement methods should call through to @@ -292,15 +292,15 @@ def test_proxy_meter_with_real_meter(self) -> None: name, unit=unit, description=description ) observable_counter = proxy_meter.create_observable_counter( - name, callback=callback, unit=unit, description=description + name, callbacks=[callback], unit=unit, description=description ) observable_updowncounter = ( proxy_meter.create_observable_up_down_counter( - name, callback=callback, unit=unit, description=description + name, callbacks=[callback], unit=unit, description=description ) ) observable_gauge = proxy_meter.create_observable_gauge( - name, callback=callback, unit=unit, description=description + name, callbacks=[callback], unit=unit, description=description ) real_meter: Mock = real_meter_provider.get_meter() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index e78e69454c..29e4972d80 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -111,7 +111,7 @@ def create_up_down_counter( ) def create_observable_counter( - self, name, callback, unit="", description="" + self, name, callbacks=None, unit="", description="" ) -> APIObservableCounter: if self._check_instrument_id( name, ObservableCounter, unit, description @@ -131,7 +131,7 @@ def create_observable_counter( name, self._instrumentation_info, self._measurement_consumer, - callback, + callbacks, unit, description, ) @@ -162,7 +162,7 @@ def create_histogram(self, name, unit="", description="") -> APIHistogram: ) def create_observable_gauge( - self, name, callback, unit="", description="" + self, name, callbacks=None, unit="", description="" ) -> APIObservableGauge: if self._check_instrument_id(name, ObservableGauge, unit, description): # FIXME #2558 go through all views here and check if this @@ -181,7 +181,7 @@ def create_observable_gauge( name, self._instrumentation_info, self._measurement_consumer, - callback, + callbacks, unit, description, ) @@ -191,7 +191,7 @@ def create_observable_gauge( return instrument def create_observable_up_down_counter( - self, name, callback, unit="", description="" + self, name, callbacks=None, unit="", description="" ) -> APIObservableUpDownCounter: if self._check_instrument_id( name, ObservableUpDownCounter, unit, description @@ -212,7 +212,7 @@ def create_observable_up_down_counter( name, self._instrumentation_info, self._measurement_consumer, - callback, + callbacks, unit, description, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index f2ac050d97..c6e6e4c273 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -15,7 +15,7 @@ # pylint: disable=too-many-ancestors import logging -from typing import Callable, Dict, Generator, Iterable, Union +from typing import Dict, Generator, Iterable, Optional, Union from opentelemetry._metrics.instrument import CallbackT from opentelemetry._metrics.instrument import Counter as APICounter @@ -30,7 +30,6 @@ ObservableUpDownCounter as APIObservableUpDownCounter, ) from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter -from opentelemetry._metrics.measurement import Measurement as APIMeasurement from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.measurement_consumer import MeasurementConsumer from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -61,7 +60,7 @@ def __init__( name: str, instrumentation_info: InstrumentationInfo, measurement_consumer: MeasurementConsumer, - callback: CallbackT, + callbacks: Optional[Iterable[CallbackT]] = None, unit: str = "", description: str = "", ): @@ -70,26 +69,34 @@ def __init__( self.description = description self.instrumentation_info = instrumentation_info self._measurement_consumer = measurement_consumer - super().__init__(name, callback, unit=unit, description=description) + super().__init__(name, callbacks, unit=unit, description=description) - self._callback: Callable[[], Iterable[APIMeasurement]] + self._callbacks = [] - if isinstance(callback, Generator): + if callbacks is not None: - def inner() -> Iterable[Measurement]: - return next(callback) + for callback in callbacks: - self._callback = inner - else: - self._callback = callback + if isinstance(callback, Generator): + + def inner(callback=callback) -> Iterable[Measurement]: + return next(callback) + + self._callbacks.append(inner) + else: + self._callbacks.append(callback) def callback(self) -> Iterable[Measurement]: - for api_measurement in self._callback(): - yield Measurement( - api_measurement.value, - instrument=self, - attributes=api_measurement.attributes, - ) + for callback in self._callbacks: + try: + for api_measurement in callback(): + yield Measurement( + api_measurement.value, + instrument=self, + attributes=api_measurement.attributes, + ) + except StopIteration: + pass class Counter(_Synchronous, APICounter): diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py index 0135faa772..b21091c3c8 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py @@ -177,7 +177,7 @@ def cpu_time_callback() -> Iterable[APIMeasurement]: meter = MeterProvider().get_meter("name") observable_counter = meter.create_observable_counter( "system.cpu.time", - cpu_time_callback, + callbacks=[cpu_time_callback], unit="s", description="CPU time", ) @@ -256,7 +256,7 @@ def cpu_time_generator() -> Generator[ meter = MeterProvider().get_meter("name") observable_counter = meter.create_observable_counter( "system.cpu.time", - callback=cpu_time_generator(), + callbacks=[cpu_time_generator()], unit="s", description="CPU time", ) diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 99f3478267..ad483a41d4 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -897,7 +897,7 @@ def test_up_down_counter(self): def test_observable_counter(self): aggregation = self.default_aggregation._create_aggregation( - ObservableCounter(Mock(), Mock(), Mock(), Mock()) + ObservableCounter(Mock(), Mock(), Mock(), callbacks=[Mock()]) ) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) @@ -909,7 +909,7 @@ def test_observable_counter(self): def test_observable_up_down_counter(self): aggregation = self.default_aggregation._create_aggregation( - ObservableUpDownCounter(Mock(), Mock(), Mock(), Mock()) + ObservableUpDownCounter(Mock(), Mock(), Mock(), callbacks=[Mock()]) ) self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) @@ -938,7 +938,7 @@ def test_observable_gauge(self): Mock(), Mock(), Mock(), - Mock(), + callbacks=[Mock()], ) ) self.assertIsInstance(aggregation, _LastValueAggregation) diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py index 441b6df2d8..fcafea38db 100644 --- a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py @@ -70,7 +70,7 @@ def test_integration(self): meter = MeterProvider(metric_readers=[reader]).get_meter("test_meter") counter1 = meter.create_counter("counter1") meter.create_observable_gauge( - "observable_gauge1", lambda: [Measurement(value=12)] + "observable_gauge1", callbacks=[lambda: [Measurement(value=12)]] ) counter1.add(1, {"foo": "1"}) counter1.add(1, {"foo": "2"}) diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py index 8e0b3b2a6b..b52216560f 100644 --- a/opentelemetry-sdk/tests/metrics/test_instrument.py +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -58,7 +58,7 @@ def test_add_non_monotonic(self): TEST_ATTRIBUTES = {"foo": "bar"} -def callable_callback(): +def callable_callback_0(): return [ APIMeasurement(1, attributes=TEST_ATTRIBUTES), APIMeasurement(2, attributes=TEST_ATTRIBUTES), @@ -66,7 +66,15 @@ def callable_callback(): ] -def generator_callback(): +def callable_callback_1(): + return [ + APIMeasurement(4, attributes=TEST_ATTRIBUTES), + APIMeasurement(5, attributes=TEST_ATTRIBUTES), + APIMeasurement(6, attributes=TEST_ATTRIBUTES), + ] + + +def generator_callback_0(): yield [ APIMeasurement(1, attributes=TEST_ATTRIBUTES), APIMeasurement(2, attributes=TEST_ATTRIBUTES), @@ -74,10 +82,67 @@ def generator_callback(): ] +def generator_callback_1(): + yield [ + APIMeasurement(4, attributes=TEST_ATTRIBUTES), + APIMeasurement(5, attributes=TEST_ATTRIBUTES), + APIMeasurement(6, attributes=TEST_ATTRIBUTES), + ] + + class TestObservableGauge(TestCase): - def test_callable_callback(self): + def test_callable_callback_0(self): + observable_gauge = ObservableGauge( + "name", Mock(), Mock(), [callable_callback_0] + ) + + self.assertEqual( + list(observable_gauge.callback()), + [ + Measurement( + 1, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + Measurement( + 2, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + Measurement( + 3, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + ], + ) + + def test_callable_multiple_callable_callback(self): + observable_gauge = ObservableGauge( + "name", Mock(), Mock(), [callable_callback_0, callable_callback_1] + ) + + self.assertEqual( + list(observable_gauge.callback()), + [ + Measurement( + 1, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + Measurement( + 2, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + Measurement( + 3, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + Measurement( + 4, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + Measurement( + 5, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + Measurement( + 6, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + ], + ) + + def test_generator_callback_0(self): observable_gauge = ObservableGauge( - "name", Mock(), Mock(), callable_callback + "name", Mock(), Mock(), [generator_callback_0()] ) self.assertEqual( @@ -95,9 +160,13 @@ def test_callable_callback(self): ], ) - def test_generator_callback(self): + def test_generator_multiple_generator_callback(self): + self.maxDiff = None observable_gauge = ObservableGauge( - "name", Mock(), Mock(), generator_callback() + "name", + Mock(), + Mock(), + callbacks=[generator_callback_0(), generator_callback_1()], ) self.assertEqual( @@ -112,14 +181,23 @@ def test_generator_callback(self): Measurement( 3, instrument=observable_gauge, attributes=TEST_ATTRIBUTES ), + Measurement( + 4, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + Measurement( + 5, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), + Measurement( + 6, instrument=observable_gauge, attributes=TEST_ATTRIBUTES + ), ], ) class TestObservableCounter(TestCase): - def test_callable_callback(self): + def test_callable_callback_0(self): observable_counter = ObservableCounter( - "name", Mock(), Mock(), callable_callback + "name", Mock(), Mock(), [callable_callback_0] ) self.assertEqual( @@ -143,9 +221,9 @@ def test_callable_callback(self): ], ) - def test_generator_callback(self): + def test_generator_callback_0(self): observable_counter = ObservableCounter( - "name", Mock(), Mock(), generator_callback() + "name", Mock(), Mock(), [generator_callback_0()] ) self.assertEqual( @@ -171,9 +249,9 @@ def test_generator_callback(self): class TestObservableUpDownCounter(TestCase): - def test_callable_callback(self): + def test_callable_callback_0(self): observable_up_down_counter = ObservableUpDownCounter( - "name", Mock(), Mock(), callable_callback + "name", Mock(), Mock(), [callable_callback_0] ) self.assertEqual( @@ -197,9 +275,9 @@ def test_callable_callback(self): ], ) - def test_generator_callback(self): + def test_generator_callback_0(self): observable_up_down_counter = ObservableUpDownCounter( - "name", Mock(), Mock(), generator_callback() + "name", Mock(), Mock(), [generator_callback_0()] ) self.assertEqual( diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index df51ee668c..2c2d48a793 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -255,17 +255,17 @@ def test_register_asynchronous_instrument( meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( meter_provider.get_meter("name").create_observable_counter( - "name0", Mock() + "name0", callbacks=[Mock()] ) ) meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( meter_provider.get_meter("name").create_observable_up_down_counter( - "name1", Mock() + "name1", callbacks=[Mock()] ) ) meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( meter_provider.get_meter("name").create_observable_gauge( - "name2", Mock() + "name2", callbacks=[Mock()] ) ) @@ -314,11 +314,15 @@ def test_repeated_instrument_names(self): try: self.meter.create_counter("counter") self.meter.create_up_down_counter("up_down_counter") - self.meter.create_observable_counter("observable_counter", Mock()) + self.meter.create_observable_counter( + "observable_counter", callbacks=[Mock()] + ) self.meter.create_histogram("histogram") - self.meter.create_observable_gauge("observable_gauge", Mock()) + self.meter.create_observable_gauge( + "observable_gauge", callbacks=[Mock()] + ) self.meter.create_observable_up_down_counter( - "observable_up_down_counter", Mock() + "observable_up_down_counter", callbacks=[Mock()] ) except Exception as error: self.fail(f"Unexpected exception raised {error}") @@ -340,7 +344,7 @@ def test_repeated_instrument_names(self): ]: with self.assertLogs(level=WARNING): getattr(self.meter, f"create_{instrument_name}")( - instrument_name, Mock() + instrument_name, callbacks=[Mock()] ) def test_create_counter(self): @@ -361,7 +365,7 @@ def test_create_up_down_counter(self): def test_create_observable_counter(self): observable_counter = self.meter.create_observable_counter( - "name", Mock(), unit="unit", description="description" + "name", callbacks=[Mock()], unit="unit", description="description" ) self.assertIsInstance(observable_counter, ObservableCounter) @@ -377,7 +381,7 @@ def test_create_histogram(self): def test_create_observable_gauge(self): observable_gauge = self.meter.create_observable_gauge( - "name", Mock(), unit="unit", description="description" + "name", callbacks=[Mock()], unit="unit", description="description" ) self.assertIsInstance(observable_gauge, ObservableGauge) @@ -386,7 +390,10 @@ def test_create_observable_gauge(self): def test_create_observable_up_down_counter(self): observable_up_down_counter = ( self.meter.create_observable_up_down_counter( - "name", Mock(), unit="unit", description="description" + "name", + callbacks=[Mock()], + unit="unit", + description="description", ) ) self.assertIsInstance( From 3cde5a97a1068a4436a999a9f84fc14667a06647 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 14 Apr 2022 17:26:59 -0700 Subject: [PATCH 1189/1517] Fix dependency for otlp exporter (#2605) `opentelemetry-sdk ~= 1.10.0` prevents from upgrading to 1.11.0. Changing to greater than. --- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 08b13b56c6..1c1c3aade5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -43,7 +43,7 @@ install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.10.0 + opentelemetry-sdk >= 1.10.0 opentelemetry-proto == 1.10.0 backoff >= 1.10.0, < 2.0.0 From 13f3b5aecfe210b052cb29684dbfd011d819d58e Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 15 Apr 2022 17:39:56 -0700 Subject: [PATCH 1190/1517] Update setup.cfg (#2607) --- tests/opentelemetry-test-utils/setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/opentelemetry-test-utils/setup.cfg b/tests/opentelemetry-test-utils/setup.cfg index 67d67d5942..09c3ff0c84 100644 --- a/tests/opentelemetry-test-utils/setup.cfg +++ b/tests/opentelemetry-test-utils/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.3 + opentelemetry-api == 1.10.0 + opentelemetry-sdk == 1.10.0 asgiref ~= 3.0 [options.extras_require] From 1b00f31114c42ccb2e7da94a99e6e1dc1885c722 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 18 Apr 2022 10:04:07 -0700 Subject: [PATCH 1191/1517] Release 1.11.0-0.30b0 (#2604) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 4 +++- eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/setup.cfg | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 31 files changed, 40 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bb2498b026..d808868a14 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: e71a5157c184266002186cf853dfb8e2f2d6b924 + CONTRIB_REPO_SHA: de897daa279f6c4aa4572614d0bead2146097068 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 29317e4f20..5b5069a4b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.10.0-0.29b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.0-0.30b0...HEAD) + +## [1.11.0-0.30b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.0-0.30b0) - 2022-04-18 - Add support for zero or more callbacks ([#2602](https://github.com/open-telemetry/opentelemetry-python/pull/2602)) diff --git a/eachdist.ini b/eachdist.ini index 9db9614e08..a49edda125 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.10.0 +version=1.11.0 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.29b0 +version=0.30b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 1d5653f5b4..45c8d2c9fc 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 1d5653f5b4..45c8d2c9fc 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 0c3fe8a311..94b1f34619 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.10.0 - opentelemetry-exporter-jaeger-thrift == 1.10.0 + opentelemetry-exporter-jaeger-proto-grpc == 1.11.0 + opentelemetry-exporter-jaeger-thrift == 1.11.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 1d5653f5b4..45c8d2c9fc 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 5743fb8ce1..75e49aa583 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.29b0" +__version__ = "0.30b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 1c1c3aade5..5361ce1e0a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -43,8 +43,8 @@ install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 - opentelemetry-sdk >= 1.10.0 - opentelemetry-proto == 1.10.0 + opentelemetry-sdk ~= 1.11 + opentelemetry-proto == 1.11.0 backoff >= 1.10.0, < 2.0.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 7cb3f8387a..e9b0f0343e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index 9b5c0b0afb..50e3c14b54 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.10.0 + opentelemetry-proto == 1.11.0 backoff >= 1.10.0, < 2.0.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 7cb3f8387a..e9b0f0343e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 6dfc63ee52..bf4ff52bdd 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.10.0 - opentelemetry-exporter-otlp-proto-http == 1.10.0 + opentelemetry-exporter-otlp-proto-grpc == 1.11.0 + opentelemetry-exporter-otlp-proto-http == 1.11.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 7cb3f8387a..e9b0f0343e 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 5743fb8ce1..75e49aa583 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.29b0" +__version__ = "0.30b0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 7cb3f8387a..e9b0f0343e 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index e9f7827918..fe7c40a3b1 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.10.0 + opentelemetry-exporter-zipkin-json == 1.11.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 7cb3f8387a..e9b0f0343e 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index c431d302dd..46a0d21d0b 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.10.0 - opentelemetry-exporter-zipkin-proto-http == 1.10.0 + opentelemetry-exporter-zipkin-json == 1.11.0 + opentelemetry-exporter-zipkin-proto-http == 1.11.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 7cb3f8387a..e9b0f0343e 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 7cb3f8387a..e9b0f0343e 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 7cb3f8387a..e9b0f0343e 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 1300e98e33..0805d89618 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,8 +43,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.10.0 - opentelemetry-semantic-conventions == 0.29b0 + opentelemetry-api == 1.11.0 + opentelemetry-semantic-conventions == 0.30b0 setuptools >= 16.0 dataclasses == 0.8; python_version < '3.7' typing-extensions >= 3.7.4 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 7cb3f8387a..e9b0f0343e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 5743fb8ce1..75e49aa583 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.29b0" +__version__ = "0.30b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 7cb3f8387a..e9b0f0343e 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 7cb3f8387a..e9b0f0343e 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 335f2acfb7..2153a81a5c 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -47,7 +47,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test-utils == 0.29b0 + opentelemetry-test-utils == 0.30b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 5743fb8ce1..75e49aa583 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.29b0" +__version__ = "0.30b0" diff --git a/tests/opentelemetry-test-utils/setup.cfg b/tests/opentelemetry-test-utils/setup.cfg index 09c3ff0c84..75bd316a27 100644 --- a/tests/opentelemetry-test-utils/setup.cfg +++ b/tests/opentelemetry-test-utils/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.10.0 - opentelemetry-sdk == 1.10.0 + opentelemetry-api == 1.11.0 + opentelemetry-sdk == 1.11.0 asgiref ~= 3.0 [options.extras_require] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 6d36ac277e..903bf25a28 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.29b0" +__version__ = "0.30b0" From a41581faf7dd34b9df20ce77b66b27a42552a776 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 19 Apr 2022 08:27:57 -0700 Subject: [PATCH 1192/1517] [opentelemetry-api] rename Default instruments to NoOp (#2616) --- CHANGELOG.md | 6 +++ .../src/opentelemetry/_metrics/__init__.py | 38 +++++++++---------- .../src/opentelemetry/_metrics/instrument.py | 12 +++--- .../tests/metrics/test_instruments.py | 14 +++---- 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b5069a4b9..ecba0eb239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.0-0.30b0...HEAD) +- Rename `DefaultCounter`, `DefaultHistogram`, `DefaultObservableCounter`, + `DefaultObservableGauge`, `DefaultObservableUpDownCounter`, `DefaultUpDownCounter` + instruments to `NoOpCounter`, `NoOpHistogram`, `NoOpObservableCounter`, + `NoOpObservableGauge`, `NoOpObservableUpDownCounter`, `NoOpUpDownCounter` + ([#2616](https://github.com/open-telemetry/opentelemetry-python/pull/2616)) + ## [1.11.0-0.30b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.0-0.30b0) - 2022-04-18 - Add support for zero or more callbacks diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index 0abe07b38b..3edbad6bfc 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -30,13 +30,13 @@ from opentelemetry._metrics.instrument import ( Counter, - DefaultCounter, - DefaultHistogram, - DefaultObservableCounter, - DefaultObservableGauge, - DefaultObservableUpDownCounter, - DefaultUpDownCounter, Histogram, + NoOpCounter, + NoOpHistogram, + NoOpObservableCounter, + NoOpObservableGauge, + NoOpObservableUpDownCounter, + NoOpUpDownCounter, ObservableCounter, ObservableGauge, ObservableUpDownCounter, @@ -423,7 +423,7 @@ class NoOpMeter(Meter): def create_counter(self, name, unit="", description="") -> Counter: """Returns a no-op Counter.""" super().create_counter(name, unit=unit, description=description) - if self._check_instrument_id(name, DefaultCounter, unit, description): + if self._check_instrument_id(name, NoOpCounter, unit, description): _logger.warning( "An instrument with name %s, type %s, unit %s and " "description %s has been created already.", @@ -432,7 +432,7 @@ def create_counter(self, name, unit="", description="") -> Counter: unit, description, ) - return DefaultCounter(name, unit=unit, description=description) + return NoOpCounter(name, unit=unit, description=description) def create_up_down_counter( self, name, unit="", description="" @@ -442,7 +442,7 @@ def create_up_down_counter( name, unit=unit, description=description ) if self._check_instrument_id( - name, DefaultUpDownCounter, unit, description + name, NoOpUpDownCounter, unit, description ): _logger.warning( "An instrument with name %s, type %s, unit %s and " @@ -452,7 +452,7 @@ def create_up_down_counter( unit, description, ) - return DefaultUpDownCounter(name, unit=unit, description=description) + return NoOpUpDownCounter(name, unit=unit, description=description) def create_observable_counter( self, name, callbacks=None, unit="", description="" @@ -462,7 +462,7 @@ def create_observable_counter( name, callbacks, unit=unit, description=description ) if self._check_instrument_id( - name, DefaultObservableCounter, unit, description + name, NoOpObservableCounter, unit, description ): _logger.warning( "An instrument with name %s, type %s, unit %s and " @@ -472,7 +472,7 @@ def create_observable_counter( unit, description, ) - return DefaultObservableCounter( + return NoOpObservableCounter( name, callbacks, unit=unit, @@ -482,9 +482,7 @@ def create_observable_counter( def create_histogram(self, name, unit="", description="") -> Histogram: """Returns a no-op Histogram.""" super().create_histogram(name, unit=unit, description=description) - if self._check_instrument_id( - name, DefaultHistogram, unit, description - ): + if self._check_instrument_id(name, NoOpHistogram, unit, description): _logger.warning( "An instrument with name %s, type %s, unit %s and " "description %s has been created already.", @@ -493,7 +491,7 @@ def create_histogram(self, name, unit="", description="") -> Histogram: unit, description, ) - return DefaultHistogram(name, unit=unit, description=description) + return NoOpHistogram(name, unit=unit, description=description) def create_observable_gauge( self, name, callbacks=None, unit="", description="" @@ -503,7 +501,7 @@ def create_observable_gauge( name, callbacks, unit=unit, description=description ) if self._check_instrument_id( - name, DefaultObservableGauge, unit, description + name, NoOpObservableGauge, unit, description ): _logger.warning( "An instrument with name %s, type %s, unit %s and " @@ -513,7 +511,7 @@ def create_observable_gauge( unit, description, ) - return DefaultObservableGauge( + return NoOpObservableGauge( name, callbacks, unit=unit, @@ -528,7 +526,7 @@ def create_observable_up_down_counter( name, callbacks, unit=unit, description=description ) if self._check_instrument_id( - name, DefaultObservableUpDownCounter, unit, description + name, NoOpObservableUpDownCounter, unit, description ): _logger.warning( "An instrument with name %s, type %s, unit %s and " @@ -538,7 +536,7 @@ def create_observable_up_down_counter( unit, description, ) - return DefaultObservableUpDownCounter( + return NoOpObservableUpDownCounter( name, callbacks, unit=unit, diff --git a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py index f707b05e9f..87e9d33f5d 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py @@ -119,7 +119,7 @@ def add(self, amount, attributes=None): pass -class DefaultCounter(Counter): +class NoOpCounter(Counter): def __init__(self, name, unit="", description=""): super().__init__(name, unit=unit, description=description) @@ -144,7 +144,7 @@ def add(self, amount, attributes=None): pass -class DefaultUpDownCounter(UpDownCounter): +class NoOpUpDownCounter(UpDownCounter): def __init__(self, name, unit="", description=""): super().__init__(name, unit=unit, description=description) @@ -169,7 +169,7 @@ class ObservableCounter(_Monotonic, Asynchronous): """ -class DefaultObservableCounter(ObservableCounter): +class NoOpObservableCounter(ObservableCounter): def __init__(self, name, callbacks=None, unit="", description=""): super().__init__(name, callbacks, unit=unit, description=description) @@ -192,7 +192,7 @@ class ObservableUpDownCounter(_NonMonotonic, Asynchronous): """ -class DefaultObservableUpDownCounter(ObservableUpDownCounter): +class NoOpObservableUpDownCounter(ObservableUpDownCounter): def __init__(self, name, callbacks=None, unit="", description=""): super().__init__(name, callbacks, unit=unit, description=description) @@ -220,7 +220,7 @@ def record(self, amount, attributes=None): pass -class DefaultHistogram(Histogram): +class NoOpHistogram(Histogram): def __init__(self, name, unit="", description=""): super().__init__(name, unit=unit, description=description) @@ -246,7 +246,7 @@ class ObservableGauge(_Grouping, Asynchronous): """ -class DefaultObservableGauge(ObservableGauge): +class NoOpObservableGauge(ObservableGauge): def __init__(self, name, callbacks=None, unit="", description=""): super().__init__(name, callbacks, unit=unit, description=description) diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index 34bc4cec80..8207970040 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -19,11 +19,11 @@ from opentelemetry._metrics import Meter, NoOpMeter from opentelemetry._metrics.instrument import ( Counter, - DefaultCounter, - DefaultHistogram, - DefaultUpDownCounter, Histogram, Instrument, + NoOpCounter, + NoOpHistogram, + NoOpUpDownCounter, ObservableCounter, ObservableGauge, ObservableUpDownCounter, @@ -94,7 +94,7 @@ def test_counter_add_method(self): self.assertTrue(hasattr(Counter, "add")) - self.assertIsNone(DefaultCounter("name").add(1)) + self.assertIsNone(NoOpCounter("name").add(1)) add_signature = signature(Counter.add) self.assertIn("attributes", add_signature.parameters.keys()) @@ -262,7 +262,7 @@ def test_histogram_record_method(self): self.assertTrue(hasattr(Histogram, "record")) - self.assertIsNone(DefaultHistogram("name").record(1)) + self.assertIsNone(NoOpHistogram("name").record(1)) record_signature = signature(Histogram.record) self.assertIn("attributes", record_signature.parameters.keys()) @@ -273,7 +273,7 @@ def test_histogram_record_method(self): record_signature.parameters["amount"].default, Signature.empty ) - self.assertIsNone(DefaultHistogram("name").record(1)) + self.assertIsNone(NoOpHistogram("name").record(1)) class TestObservableGauge(TestCase): @@ -441,7 +441,7 @@ def test_up_down_counter_add_method(self): self.assertTrue(hasattr(UpDownCounter, "add")) - self.assertIsNone(DefaultUpDownCounter("name").add(1)) + self.assertIsNone(NoOpUpDownCounter("name").add(1)) add_signature = signature(UpDownCounter.add) self.assertIn("attributes", add_signature.parameters.keys()) From bfeb4b8c1c40d5930da3a1501d73f437650e59b8 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 19 Apr 2022 09:13:53 -0700 Subject: [PATCH 1193/1517] [docs] add more documentation to metrics API (#2615) * [docs] add more documentation to metrics API * Update opentelemetry-api/src/opentelemetry/_metrics/__init__.py Co-authored-by: Diego Hurtado * Update opentelemetry-api/src/opentelemetry/_metrics/__init__.py Co-authored-by: Srikanth Chekuri Co-authored-by: Diego Hurtado Co-authored-by: Srikanth Chekuri --- .../src/opentelemetry/_metrics/__init__.py | 86 ++++++++++++++++--- 1 file changed, 76 insertions(+), 10 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index 3edbad6bfc..8e23eef18f 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -15,10 +15,30 @@ # pylint: disable=too-many-ancestors # type: ignore -# FIXME enhance the documentation of this module """ -This module provides abstract and concrete (but noop) classes that can be used -to generate metrics. +The OpenTelemetry metrics API describes the classes used to generate +metrics. + +The :class:`.MeterProvider` provides users access to the :class:`.Meter` which in +turn is used to create :class:`.Instrument` objects. The :class:`.Instrument` objects are +used to record measurements. + +This module provides abstract (i.e. unimplemented) classes required for +metrics, and a concrete no-op implementation :class:`.NoOpMeter` that allows applications +to use the API package alone without a supporting implementation. + +To get a meter, you need to provide the package name from which you are +calling the meter APIs to OpenTelemetry by calling `MeterProvider.get_meter` +with the calling instrumentation name and the version of your package. + +The following code shows how to obtain a meter using the global :class:`.MeterProvider`:: + + from opentelemetry._metrics import get_meter + + meter = get_meter("example-meter") + counter = meter.create_counter("example-counter") + +.. versionadded:: 1.10.0 """ @@ -59,14 +79,43 @@ class MeterProvider(ABC): + """ + MeterProvider is the entry point of the API. It provides access to `Meter` instances. + """ + @abstractmethod def get_meter( self, - name, - version=None, - schema_url=None, + name: str, + version: str = None, + schema_url: str = None, ) -> "Meter": - pass + """Returns a `Meter` for use by the given instrumentation library. + + For any two calls it is undefined whether the same or different + `Meter` instances are returned, even for different library names. + + This function may return different `Meter` types (e.g. a no-op meter + vs. a functional meter). + + Args: + name: The name of the instrumenting module. + ``__name__`` may not be used as this can result in + different meter names if the meters are in different files. + It is better to use a fixed string that can be imported where + needed and used consistently as the name of the meter. + + This should *not* be the name of the module that is + instrumented but the name of the module doing the instrumentation. + E.g., instead of ``"requests"``, use + ``"opentelemetry.instrumentation.requests"``. + + version: Optional. The version string of the + instrumenting library. Usually this should be the same as + ``pkg_resources.get_distribution(instrumenting_library_name).version``. + + schema_url: Optional. Specifies the Schema URL of the emitted telemetry. + """ class NoOpMeterProvider(MeterProvider): @@ -113,7 +162,13 @@ def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: class Meter(ABC): - def __init__(self, name, version=None, schema_url=None): + """Handles instrument creation. + + This class provides methods for creating instruments which are then + used to produce measurements. + """ + + def __init__(self, name: str, version: str = None, schema_url: str = None): super().__init__() self._name = name self._version = version @@ -123,14 +178,23 @@ def __init__(self, name, version=None, schema_url=None): @property def name(self): + """ + The name of the instrumenting module. + """ return self._name @property def version(self): + """ + The version string of the instrumenting library. + """ return self._version @property def schema_url(self): + """ + Specifies the Schema URL of the emitted telemetry + """ return self._schema_url def _check_instrument_id( @@ -156,7 +220,9 @@ def _check_instrument_id( return result @abstractmethod - def create_counter(self, name, unit="", description="") -> Counter: + def create_counter( + self, name: str, unit: str = "", description: str = "" + ) -> Counter: """Creates a `Counter` instrument Args: @@ -168,7 +234,7 @@ def create_counter(self, name, unit="", description="") -> Counter: @abstractmethod def create_up_down_counter( - self, name, unit="", description="" + self, name: str, unit: str = "", description: str = "" ) -> UpDownCounter: """Creates an `UpDownCounter` instrument From dd56e7f0b364d4540365e2ddacd34b2fb16b7717 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 19 Apr 2022 09:46:46 -0700 Subject: [PATCH 1194/1517] Continue processing async instruments if callback fails (#2614) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/_metrics/instrument.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecba0eb239..00afde11a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.0-0.30b0...HEAD) +- Fix unhandled callback exceptions on async instruments + ([#2614](https://github.com/open-telemetry/opentelemetry-python/pull/2614)) - Rename `DefaultCounter`, `DefaultHistogram`, `DefaultObservableCounter`, `DefaultObservableGauge`, `DefaultObservableUpDownCounter`, `DefaultUpDownCounter` instruments to `NoOpCounter`, `NoOpHistogram`, `NoOpObservableCounter`, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index c6e6e4c273..260a9a7767 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -97,6 +97,10 @@ def callback(self) -> Iterable[Measurement]: ) except StopIteration: pass + except Exception: # pylint: disable=broad-except + _logger.exception( + "Callback failed for instrument %s.", self.name + ) class Counter(_Synchronous, APICounter): From 782ac3ef0d0a15fa30c76e152ea281657f5caa12 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 19 Apr 2022 11:50:27 -0700 Subject: [PATCH 1195/1517] update OTLP to v0.16.0 (#2619) * update OTLP to v0.16.0 Bumping OTLP to 0.16.0. This only removes the name field in the log data model. * update changelog * update readme --- CHANGELOG.md | 2 ++ opentelemetry-proto/README.rst | 4 +-- .../opentelemetry/proto/logs/v1/logs_pb2.py | 32 +++++++------------ .../opentelemetry/proto/logs/v1/logs_pb2.pyi | 12 +------ scripts/proto_codegen.sh | 2 +- 5 files changed, 18 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00afde11a1..0b949f8c01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2575](https://github.com/open-telemetry/opentelemetry-python/pull/2575)) - Add min/max to histogram point ([#2581](https://github.com/open-telemetry/opentelemetry-python/pull/2581)) +- Update opentelemetry-proto to v0.16.0 + ([#2619](https://github.com/open-telemetry/opentelemetry-python/pull/2619)) ## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 diff --git a/opentelemetry-proto/README.rst b/opentelemetry-proto/README.rst index bb3a1e9796..d012aa70d4 100644 --- a/opentelemetry-proto/README.rst +++ b/opentelemetry-proto/README.rst @@ -7,9 +7,9 @@ OpenTelemetry Python Proto :target: https://pypi.org/project/opentelemetry-proto/ This library contains the generated code for OpenTelemetry protobuf data model. The code in the current -package was generated using the v0.15.0 release_ of opentelemetry-proto. +package was generated using the v0.16.0 release_ of opentelemetry-proto. -.. _release: https://github.com/open-telemetry/opentelemetry-proto/releases/tag/v0.15.0 +.. _release: https://github.com/open-telemetry/opentelemetry-proto/releases/tag/v0.16.0 Installation ------------ diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py index a1569de0da..f9e74eb67e 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py @@ -22,7 +22,7 @@ syntax='proto3', serialized_options=b'\n\036io.opentelemetry.proto.logs.v1B\tLogsProtoP\001Z&go.opentelemetry.io/proto/otlp/logs/v1', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n&opentelemetry/proto/logs/v1/logs.proto\x12\x1bopentelemetry.proto.logs.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"L\n\x08LogsData\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\xff\x01\n\x0cResourceLogs\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12:\n\nscope_logs\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.ScopeLogs\x12\x62\n\x1cinstrumentation_library_logs\x18\xe8\x07 \x03(\x0b\x32\x37.opentelemetry.proto.logs.v1.InstrumentationLibraryLogsB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xa0\x01\n\tScopeLogs\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc9\x01\n\x1aInstrumentationLibraryLogs\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\xfb\x02\n\tLogRecord\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x1f\n\x17observed_time_unix_nano\x18\x0b \x01(\x06\x12\x44\n\x0fseverity_number\x18\x02 \x01(\x0e\x32+.opentelemetry.proto.logs.v1.SeverityNumber\x12\x15\n\rseverity_text\x18\x03 \x01(\t\x12\x10\n\x04name\x18\x04 \x01(\tB\x02\x18\x01\x12\x35\n\x04\x62ody\x18\x05 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12;\n\nattributes\x18\x06 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x07 \x01(\r\x12\r\n\x05\x66lags\x18\x08 \x01(\x07\x12\x10\n\x08trace_id\x18\t \x01(\x0c\x12\x0f\n\x07span_id\x18\n \x01(\x0c*\xc3\x05\n\x0eSeverityNumber\x12\x1f\n\x1bSEVERITY_NUMBER_UNSPECIFIED\x10\x00\x12\x19\n\x15SEVERITY_NUMBER_TRACE\x10\x01\x12\x1a\n\x16SEVERITY_NUMBER_TRACE2\x10\x02\x12\x1a\n\x16SEVERITY_NUMBER_TRACE3\x10\x03\x12\x1a\n\x16SEVERITY_NUMBER_TRACE4\x10\x04\x12\x19\n\x15SEVERITY_NUMBER_DEBUG\x10\x05\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG2\x10\x06\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG3\x10\x07\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG4\x10\x08\x12\x18\n\x14SEVERITY_NUMBER_INFO\x10\t\x12\x19\n\x15SEVERITY_NUMBER_INFO2\x10\n\x12\x19\n\x15SEVERITY_NUMBER_INFO3\x10\x0b\x12\x19\n\x15SEVERITY_NUMBER_INFO4\x10\x0c\x12\x18\n\x14SEVERITY_NUMBER_WARN\x10\r\x12\x19\n\x15SEVERITY_NUMBER_WARN2\x10\x0e\x12\x19\n\x15SEVERITY_NUMBER_WARN3\x10\x0f\x12\x19\n\x15SEVERITY_NUMBER_WARN4\x10\x10\x12\x19\n\x15SEVERITY_NUMBER_ERROR\x10\x11\x12\x1a\n\x16SEVERITY_NUMBER_ERROR2\x10\x12\x12\x1a\n\x16SEVERITY_NUMBER_ERROR3\x10\x13\x12\x1a\n\x16SEVERITY_NUMBER_ERROR4\x10\x14\x12\x19\n\x15SEVERITY_NUMBER_FATAL\x10\x15\x12\x1a\n\x16SEVERITY_NUMBER_FATAL2\x10\x16\x12\x1a\n\x16SEVERITY_NUMBER_FATAL3\x10\x17\x12\x1a\n\x16SEVERITY_NUMBER_FATAL4\x10\x18*X\n\x0eLogRecordFlags\x12\x1f\n\x1bLOG_RECORD_FLAG_UNSPECIFIED\x10\x00\x12%\n LOG_RECORD_FLAG_TRACE_FLAGS_MASK\x10\xff\x01\x42U\n\x1eio.opentelemetry.proto.logs.v1B\tLogsProtoP\x01Z&go.opentelemetry.io/proto/otlp/logs/v1b\x06proto3' + serialized_pb=b'\n&opentelemetry/proto/logs/v1/logs.proto\x12\x1bopentelemetry.proto.logs.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"L\n\x08LogsData\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\xff\x01\n\x0cResourceLogs\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12:\n\nscope_logs\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.ScopeLogs\x12\x62\n\x1cinstrumentation_library_logs\x18\xe8\x07 \x03(\x0b\x32\x37.opentelemetry.proto.logs.v1.InstrumentationLibraryLogsB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xa0\x01\n\tScopeLogs\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc9\x01\n\x1aInstrumentationLibraryLogs\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\xef\x02\n\tLogRecord\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x1f\n\x17observed_time_unix_nano\x18\x0b \x01(\x06\x12\x44\n\x0fseverity_number\x18\x02 \x01(\x0e\x32+.opentelemetry.proto.logs.v1.SeverityNumber\x12\x15\n\rseverity_text\x18\x03 \x01(\t\x12\x35\n\x04\x62ody\x18\x05 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12;\n\nattributes\x18\x06 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x07 \x01(\r\x12\r\n\x05\x66lags\x18\x08 \x01(\x07\x12\x10\n\x08trace_id\x18\t \x01(\x0c\x12\x0f\n\x07span_id\x18\n \x01(\x0cJ\x04\x08\x04\x10\x05*\xc3\x05\n\x0eSeverityNumber\x12\x1f\n\x1bSEVERITY_NUMBER_UNSPECIFIED\x10\x00\x12\x19\n\x15SEVERITY_NUMBER_TRACE\x10\x01\x12\x1a\n\x16SEVERITY_NUMBER_TRACE2\x10\x02\x12\x1a\n\x16SEVERITY_NUMBER_TRACE3\x10\x03\x12\x1a\n\x16SEVERITY_NUMBER_TRACE4\x10\x04\x12\x19\n\x15SEVERITY_NUMBER_DEBUG\x10\x05\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG2\x10\x06\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG3\x10\x07\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG4\x10\x08\x12\x18\n\x14SEVERITY_NUMBER_INFO\x10\t\x12\x19\n\x15SEVERITY_NUMBER_INFO2\x10\n\x12\x19\n\x15SEVERITY_NUMBER_INFO3\x10\x0b\x12\x19\n\x15SEVERITY_NUMBER_INFO4\x10\x0c\x12\x18\n\x14SEVERITY_NUMBER_WARN\x10\r\x12\x19\n\x15SEVERITY_NUMBER_WARN2\x10\x0e\x12\x19\n\x15SEVERITY_NUMBER_WARN3\x10\x0f\x12\x19\n\x15SEVERITY_NUMBER_WARN4\x10\x10\x12\x19\n\x15SEVERITY_NUMBER_ERROR\x10\x11\x12\x1a\n\x16SEVERITY_NUMBER_ERROR2\x10\x12\x12\x1a\n\x16SEVERITY_NUMBER_ERROR3\x10\x13\x12\x1a\n\x16SEVERITY_NUMBER_ERROR4\x10\x14\x12\x19\n\x15SEVERITY_NUMBER_FATAL\x10\x15\x12\x1a\n\x16SEVERITY_NUMBER_FATAL2\x10\x16\x12\x1a\n\x16SEVERITY_NUMBER_FATAL3\x10\x17\x12\x1a\n\x16SEVERITY_NUMBER_FATAL4\x10\x18*X\n\x0eLogRecordFlags\x12\x1f\n\x1bLOG_RECORD_FLAG_UNSPECIFIED\x10\x00\x12%\n LOG_RECORD_FLAG_TRACE_FLAGS_MASK\x10\xff\x01\x42U\n\x1eio.opentelemetry.proto.logs.v1B\tLogsProtoP\x01Z&go.opentelemetry.io/proto/otlp/logs/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -161,8 +161,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1249, - serialized_end=1956, + serialized_start=1237, + serialized_end=1944, ) _sym_db.RegisterEnumDescriptor(_SEVERITYNUMBER) @@ -187,8 +187,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1958, - serialized_end=2046, + serialized_start=1946, + serialized_end=2034, ) _sym_db.RegisterEnumDescriptor(_LOGRECORDFLAGS) @@ -437,49 +437,42 @@ is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='name', full_name='opentelemetry.proto.logs.v1.LogRecord.name', index=4, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='body', full_name='opentelemetry.proto.logs.v1.LogRecord.body', index=5, + name='body', full_name='opentelemetry.proto.logs.v1.LogRecord.body', index=4, number=5, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='attributes', full_name='opentelemetry.proto.logs.v1.LogRecord.attributes', index=6, + name='attributes', full_name='opentelemetry.proto.logs.v1.LogRecord.attributes', index=5, number=6, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='dropped_attributes_count', full_name='opentelemetry.proto.logs.v1.LogRecord.dropped_attributes_count', index=7, + name='dropped_attributes_count', full_name='opentelemetry.proto.logs.v1.LogRecord.dropped_attributes_count', index=6, number=7, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='flags', full_name='opentelemetry.proto.logs.v1.LogRecord.flags', index=8, + name='flags', full_name='opentelemetry.proto.logs.v1.LogRecord.flags', index=7, number=8, type=7, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='trace_id', full_name='opentelemetry.proto.logs.v1.LogRecord.trace_id', index=9, + name='trace_id', full_name='opentelemetry.proto.logs.v1.LogRecord.trace_id', index=8, number=9, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='span_id', full_name='opentelemetry.proto.logs.v1.LogRecord.span_id', index=10, + name='span_id', full_name='opentelemetry.proto.logs.v1.LogRecord.span_id', index=9, number=10, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, @@ -498,7 +491,7 @@ oneofs=[ ], serialized_start=867, - serialized_end=1246, + serialized_end=1234, ) _LOGSDATA.fields_by_name['resource_logs'].message_type = _RESOURCELOGS @@ -560,5 +553,4 @@ DESCRIPTOR._options = None _RESOURCELOGS.fields_by_name['instrumentation_library_logs']._options = None _INSTRUMENTATIONLIBRARYLOGS._options = None -_LOGRECORD.fields_by_name['name']._options = None # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi index cd774a1afa..db4da2afd1 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi @@ -261,7 +261,6 @@ class LogRecord(google.protobuf.message.Message): OBSERVED_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int SEVERITY_NUMBER_FIELD_NUMBER: builtins.int SEVERITY_TEXT_FIELD_NUMBER: builtins.int - NAME_FIELD_NUMBER: builtins.int BODY_FIELD_NUMBER: builtins.int ATTRIBUTES_FIELD_NUMBER: builtins.int DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int @@ -302,14 +301,6 @@ class LogRecord(google.protobuf.message.Message): it is known at the source. [Optional]. """ - name: typing.Text = ... - """Short event identifier that does not contain varying parts. Name describes - what happened (e.g. "ProcessStarted"). Recommended to be no longer than 50 - characters. Not guaranteed to be unique in any way. [Optional]. - This deprecated field is planned to be removed March 15, 2022. Receivers can - ignore this field. - """ - @property def body(self) -> opentelemetry.proto.common.v1.common_pb2.AnyValue: """A value containing the body of the log record. Can be for example a human-readable @@ -353,7 +344,6 @@ class LogRecord(google.protobuf.message.Message): observed_time_unix_nano : builtins.int = ..., severity_number : global___SeverityNumber.V = ..., severity_text : typing.Text = ..., - name : typing.Text = ..., body : typing.Optional[opentelemetry.proto.common.v1.common_pb2.AnyValue] = ..., attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., dropped_attributes_count : builtins.int = ..., @@ -362,5 +352,5 @@ class LogRecord(google.protobuf.message.Message): span_id : builtins.bytes = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["body",b"body"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","body",b"body","dropped_attributes_count",b"dropped_attributes_count","flags",b"flags","name",b"name","observed_time_unix_nano",b"observed_time_unix_nano","severity_number",b"severity_number","severity_text",b"severity_text","span_id",b"span_id","time_unix_nano",b"time_unix_nano","trace_id",b"trace_id"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","body",b"body","dropped_attributes_count",b"dropped_attributes_count","flags",b"flags","observed_time_unix_nano",b"observed_time_unix_nano","severity_number",b"severity_number","severity_text",b"severity_text","span_id",b"span_id","time_unix_nano",b"time_unix_nano","trace_id",b"trace_id"]) -> None: ... global___LogRecord = LogRecord diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index 62df015d36..8b1b986ee5 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -12,7 +12,7 @@ # PROTO_REPO_DIR - the path to an existing checkout of the opentelemetry-proto repo # Pinned commit/branch/tag for the current version used in opentelemetry-proto python package. -PROTO_REPO_BRANCH_OR_COMMIT="v0.15.0" +PROTO_REPO_BRANCH_OR_COMMIT="v0.16.0" set -e From 3323ce2cfca121613c56d30d0dbb9d728c8c46a9 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 20 Apr 2022 10:10:18 -0600 Subject: [PATCH 1196/1517] Rename Measurement to Observation (#2617) * Rename Measurement to Observation Fixes #2451 * Fix docs * Update CHANGELOG.md Co-authored-by: Leighton Chen * Update CHANGELOG.md Co-authored-by: Leighton Chen * Fix examples Co-authored-by: Leighton Chen --- CHANGELOG.md | 2 + ...easurement.rst => metrics.observation.rst} | 4 +- docs/api/metrics.rst | 2 +- docs/examples/metrics/example.py | 14 ++--- docs/getting_started/metrics_example.py | 14 ++--- .../src/opentelemetry/_metrics/__init__.py | 56 +++++++++---------- .../src/opentelemetry/_metrics/instrument.py | 6 +- .../{measurement.py => observation.py} | 6 +- .../tests/metrics/test_measurement.py | 22 ++++---- .../metrics/integration_test/test_cpu_time.py | 42 +++++++------- .../metrics/test_in_memory_metric_reader.py | 4 +- .../tests/metrics/test_instrument.py | 26 ++++----- 12 files changed, 100 insertions(+), 98 deletions(-) rename docs/api/{metrics.measurement.rst => metrics.observation.rst} (51%) rename opentelemetry-api/src/opentelemetry/_metrics/{measurement.py => observation.py} (92%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b949f8c01..004e3598ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.11.0-0.30b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.0-0.30b0) - 2022-04-18 +- Rename API Measurement for async instruments to Observation + ([#2617](https://github.com/open-telemetry/opentelemetry-python/pull/2617)) - Add support for zero or more callbacks ([#2602](https://github.com/open-telemetry/opentelemetry-python/pull/2602)) - Fix parsing of trace flags when extracting traceparent diff --git a/docs/api/metrics.measurement.rst b/docs/api/metrics.observation.rst similarity index 51% rename from docs/api/metrics.measurement.rst rename to docs/api/metrics.observation.rst index d9b70aa193..3df89ae6a5 100644 --- a/docs/api/metrics.measurement.rst +++ b/docs/api/metrics.observation.rst @@ -1,7 +1,7 @@ -opentelemetry._metrics.measurement +opentelemetry._metrics.observation ================================== -.. automodule:: opentelemetry._metrics.measurement +.. automodule:: opentelemetry._metrics.observation :members: :undoc-members: :show-inheritance: diff --git a/docs/api/metrics.rst b/docs/api/metrics.rst index d4e29b624b..08c4cbcc70 100644 --- a/docs/api/metrics.rst +++ b/docs/api/metrics.rst @@ -14,7 +14,7 @@ Submodules .. toctree:: metrics.instrument - metrics.measurement + metrics.observation Module contents --------------- diff --git a/docs/examples/metrics/example.py b/docs/examples/metrics/example.py index 60165c6755..0c4db25cf0 100644 --- a/docs/examples/metrics/example.py +++ b/docs/examples/metrics/example.py @@ -1,7 +1,7 @@ from typing import Iterable from opentelemetry._metrics import get_meter_provider, set_meter_provider -from opentelemetry._metrics.measurement import Measurement +from opentelemetry._metrics.observation import Observation from opentelemetry.exporter.otlp.proto.grpc._metric_exporter import ( OTLPMetricExporter, ) @@ -14,16 +14,16 @@ set_meter_provider(provider) -def observable_counter_func() -> Iterable[Measurement]: - yield Measurement(1, {}) +def observable_counter_func() -> Iterable[Observation]: + yield Observation(1, {}) -def observable_up_down_counter_func() -> Iterable[Measurement]: - yield Measurement(-10, {}) +def observable_up_down_counter_func() -> Iterable[Observation]: + yield Observation(-10, {}) -def observable_gauge_func() -> Iterable[Measurement]: - yield Measurement(9, {}) +def observable_gauge_func() -> Iterable[Observation]: + yield Observation(9, {}) meter = get_meter_provider().get_meter("getting-started", "0.1.2") diff --git a/docs/getting_started/metrics_example.py b/docs/getting_started/metrics_example.py index c763e3855c..2a7199b99c 100644 --- a/docs/getting_started/metrics_example.py +++ b/docs/getting_started/metrics_example.py @@ -18,7 +18,7 @@ from typing import Iterable from opentelemetry._metrics import get_meter_provider, set_meter_provider -from opentelemetry._metrics.measurement import Measurement +from opentelemetry._metrics.observation import Observation from opentelemetry.sdk._metrics import MeterProvider from opentelemetry.sdk._metrics.export import ( ConsoleMetricExporter, @@ -31,16 +31,16 @@ set_meter_provider(provider) -def observable_counter_func() -> Iterable[Measurement]: - yield Measurement(1, {}) +def observable_counter_func() -> Iterable[Observation]: + yield Observation(1, {}) -def observable_up_down_counter_func() -> Iterable[Measurement]: - yield Measurement(-10, {}) +def observable_up_down_counter_func() -> Iterable[Observation]: + yield Observation(-10, {}) -def observable_gauge_func() -> Iterable[Measurement]: - yield Measurement(9, {}) +def observable_gauge_func() -> Iterable[Observation]: + yield Observation(9, {}) meter = get_meter_provider().get_meter("getting-started", "0.1.2") diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index 8e23eef18f..a42cfd53f3 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -227,7 +227,7 @@ def create_counter( Args: name: The name of the instrument to be created - unit: The unit for measurements this instrument reports. For + unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. """ @@ -240,7 +240,7 @@ def create_up_down_counter( Args: name: The name of the instrument to be created - unit: The unit for measurements this instrument reports. For + unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. """ @@ -253,23 +253,23 @@ def create_observable_counter( An observable counter observes a monotonically increasing count by calling provided callbacks which returns multiple - :class:`~opentelemetry._metrics.measurement.Measurement`. + :class:`~opentelemetry._metrics.observation.Observation`. For example, an observable counter could be used to report system CPU time periodically. Here is a basic implementation:: - def cpu_time_callback() -> Iterable[Measurement]: - measurements = [] + def cpu_time_callback() -> Iterable[Observation]: + observations = [] with open("/proc/stat") as procstat: procstat.readline() # skip the first line for line in procstat: if not line.startswith("cpu"): break cpu, *states = line.split() - measurements.append(Measurement(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) - measurements.append(Measurement(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) - measurements.append(Measurement(int(states[2]) // 100, {"cpu": cpu, "state": "system"})) + observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) + observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) + observations.append(Observation(int(states[2]) // 100, {"cpu": cpu, "state": "system"})) # ... other states - return measurements + return observations meter.create_observable_counter( "system.cpu.time", @@ -281,34 +281,34 @@ def cpu_time_callback() -> Iterable[Measurement]: To reduce memory usage, you can use generator callbacks instead of building the full list:: - def cpu_time_callback() -> Iterable[Measurement]: + def cpu_time_callback() -> Iterable[Observation]: with open("/proc/stat") as procstat: procstat.readline() # skip the first line for line in procstat: if not line.startswith("cpu"): break cpu, *states = line.split() - yield Measurement(int(states[0]) // 100, {"cpu": cpu, "state": "user"}) - yield Measurement(int(states[1]) // 100, {"cpu": cpu, "state": "nice"}) + yield Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"}) + yield Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"}) # ... other states Alternatively, you can pass a sequence of generators directly instead of a sequence of callbacks, which each should return iterables of - :class:`~opentelemetry._metrics.measurement.Measurement`:: + :class:`~opentelemetry._metrics.observation.Observation`:: - def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Measurement]]: + def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observation]]: while True: - measurements = [] + observations = [] with open("/proc/stat") as procstat: procstat.readline() # skip the first line for line in procstat: if not line.startswith("cpu"): break cpu, *states = line.split() if "user" in states_to_include: - measurements.append(Measurement(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) + observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) if "nice" in states_to_include: - measurements.append(Measurement(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) + observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) # ... other states - yield measurements + yield observations meter.create_observable_counter( "system.cpu.time", @@ -320,11 +320,11 @@ def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Measurem Args: name: The name of the instrument to be created callbacks: A sequence of callbacks that return an iterable of - :class:`~opentelemetry._metrics.measurement.Measurement`. + :class:`~opentelemetry._metrics.observation.Observation`. Alternatively, can be a sequence of generators that each yields iterables of - :class:`~opentelemetry._metrics.measurement.Measurement`. - unit: The unit for measurements this instrument reports. For + :class:`~opentelemetry._metrics.observation.Observation`. + unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. """ @@ -335,7 +335,7 @@ def create_histogram(self, name, unit="", description="") -> Histogram: Args: name: The name of the instrument to be created - unit: The unit for measurements this instrument reports. For + unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. """ @@ -349,10 +349,10 @@ def create_observable_gauge( Args: name: The name of the instrument to be created callbacks: A sequence of callbacks that return an iterable of - :class:`~opentelemetry._metrics.measurement.Measurement`. + :class:`~opentelemetry._metrics.observation.Observation`. Alternatively, can be a generator that yields iterables of - :class:`~opentelemetry._metrics.measurement.Measurement`. - unit: The unit for measurements this instrument reports. For + :class:`~opentelemetry._metrics.observation.Observation`. + unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. """ @@ -366,10 +366,10 @@ def create_observable_up_down_counter( Args: name: The name of the instrument to be created callbacks: A sequence of callbacks that return an iterable of - :class:`~opentelemetry._metrics.measurement.Measurement`. + :class:`~opentelemetry._metrics.observation.Observation`. Alternatively, can be a generator that yields iterables of - :class:`~opentelemetry._metrics.measurement.Measurement`. - unit: The unit for measurements this instrument reports. For + :class:`~opentelemetry._metrics.observation.Observation`. + unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. """ diff --git a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py index 87e9d33f5d..0718a312e8 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py @@ -30,12 +30,12 @@ # pylint: disable=unused-import; needed for typing and sphinx from opentelemetry import _metrics as metrics -from opentelemetry._metrics.measurement import Measurement +from opentelemetry._metrics.observation import Observation InstrumentT = TypeVar("InstrumentT", bound="Instrument") CallbackT = Union[ - Callable[[], Iterable[Measurement]], - Generator[Iterable[Measurement], None, None], + Callable[[], Iterable[Observation]], + Generator[Iterable[Observation], None, None], ] diff --git a/opentelemetry-api/src/opentelemetry/_metrics/measurement.py b/opentelemetry-api/src/opentelemetry/_metrics/observation.py similarity index 92% rename from opentelemetry-api/src/opentelemetry/_metrics/measurement.py rename to opentelemetry-api/src/opentelemetry/_metrics/observation.py index e8fd9725bb..7aa24e3342 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/measurement.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/observation.py @@ -17,7 +17,7 @@ from opentelemetry.util.types import Attributes -class Measurement: +class Observation: """A measurement observed in an asynchronous instrument Return/yield instances of this class from asynchronous instrument callbacks. @@ -43,10 +43,10 @@ def attributes(self) -> Attributes: def __eq__(self, other: object) -> bool: return ( - isinstance(other, Measurement) + isinstance(other, Observation) and self.value == other.value and self.attributes == other.attributes ) def __repr__(self) -> str: - return f"Measurement(value={self.value}, attributes={self.attributes})" + return f"Observation(value={self.value}, attributes={self.attributes})" diff --git a/opentelemetry-api/tests/metrics/test_measurement.py b/opentelemetry-api/tests/metrics/test_measurement.py index 8791bc1a2f..295c5b9a6b 100644 --- a/opentelemetry-api/tests/metrics/test_measurement.py +++ b/opentelemetry-api/tests/metrics/test_measurement.py @@ -14,33 +14,33 @@ from unittest import TestCase -from opentelemetry._metrics.measurement import Measurement +from opentelemetry._metrics.observation import Observation -class TestMeasurement(TestCase): +class TestObservation(TestCase): def test_measurement_init(self): try: # int - Measurement(321, {"hello": "world"}) + Observation(321, {"hello": "world"}) # float - Measurement(321.321, {"hello": "world"}) + Observation(321.321, {"hello": "world"}) except Exception: # pylint: disable=broad-except self.fail( - "Unexpected exception raised when instantiating Measurement" + "Unexpected exception raised when instantiating Observation" ) def test_measurement_equality(self): self.assertEqual( - Measurement(321, {"hello": "world"}), - Measurement(321, {"hello": "world"}), + Observation(321, {"hello": "world"}), + Observation(321, {"hello": "world"}), ) self.assertNotEqual( - Measurement(321, {"hello": "world"}), - Measurement(321.321, {"hello": "world"}), + Observation(321, {"hello": "world"}), + Observation(321.321, {"hello": "world"}), ) self.assertNotEqual( - Measurement(321, {"baz": "world"}), - Measurement(321, {"hello": "world"}), + Observation(321, {"baz": "world"}), + Observation(321, {"hello": "world"}), ) diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py index b21091c3c8..e8c541c090 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py @@ -18,7 +18,7 @@ from unittest import TestCase from opentelemetry._metrics.instrument import Instrument -from opentelemetry._metrics.measurement import Measurement as APIMeasurement +from opentelemetry._metrics.observation import Observation from opentelemetry.sdk._metrics import MeterProvider from opentelemetry.sdk._metrics.measurement import Measurement @@ -139,38 +139,38 @@ def create_measurements_expected( ] def test_cpu_time_callback(self): - def cpu_time_callback() -> Iterable[APIMeasurement]: + def cpu_time_callback() -> Iterable[Observation]: procstat = io.StringIO(self.procstat_str) procstat.readline() # skip the first line for line in procstat: if not line.startswith("cpu"): break cpu, *states = line.split() - yield APIMeasurement( + yield Observation( int(states[0]) / 100, {"cpu": cpu, "state": "user"} ) - yield APIMeasurement( + yield Observation( int(states[1]) / 100, {"cpu": cpu, "state": "nice"} ) - yield APIMeasurement( + yield Observation( int(states[2]) / 100, {"cpu": cpu, "state": "system"} ) - yield APIMeasurement( + yield Observation( int(states[3]) / 100, {"cpu": cpu, "state": "idle"} ) - yield APIMeasurement( + yield Observation( int(states[4]) / 100, {"cpu": cpu, "state": "iowait"} ) - yield APIMeasurement( + yield Observation( int(states[5]) / 100, {"cpu": cpu, "state": "irq"} ) - yield APIMeasurement( + yield Observation( int(states[6]) / 100, {"cpu": cpu, "state": "softirq"} ) - yield APIMeasurement( + yield Observation( int(states[7]) / 100, {"cpu": cpu, "state": "guest"} ) - yield APIMeasurement( + yield Observation( int(states[8]) / 100, {"cpu": cpu, "state": "guest_nice"} ) @@ -188,7 +188,7 @@ def cpu_time_callback() -> Iterable[APIMeasurement]: def test_cpu_time_generator(self): def cpu_time_generator() -> Generator[ - Iterable[APIMeasurement], None, None + Iterable[Observation], None, None ]: while True: measurements = [] @@ -199,54 +199,54 @@ def cpu_time_generator() -> Generator[ break cpu, *states = line.split() measurements.append( - APIMeasurement( + Observation( int(states[0]) / 100, {"cpu": cpu, "state": "user"}, ) ) measurements.append( - APIMeasurement( + Observation( int(states[1]) / 100, {"cpu": cpu, "state": "nice"}, ) ) measurements.append( - APIMeasurement( + Observation( int(states[2]) / 100, {"cpu": cpu, "state": "system"}, ) ) measurements.append( - APIMeasurement( + Observation( int(states[3]) / 100, {"cpu": cpu, "state": "idle"}, ) ) measurements.append( - APIMeasurement( + Observation( int(states[4]) / 100, {"cpu": cpu, "state": "iowait"}, ) ) measurements.append( - APIMeasurement( + Observation( int(states[5]) / 100, {"cpu": cpu, "state": "irq"} ) ) measurements.append( - APIMeasurement( + Observation( int(states[6]) / 100, {"cpu": cpu, "state": "softirq"}, ) ) measurements.append( - APIMeasurement( + Observation( int(states[7]) / 100, {"cpu": cpu, "state": "guest"}, ) ) measurements.append( - APIMeasurement( + Observation( int(states[8]) / 100, {"cpu": cpu, "state": "guest_nice"}, ) diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py index fcafea38db..89d39b5e2d 100644 --- a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py @@ -15,7 +15,7 @@ from unittest import TestCase from unittest.mock import Mock -from opentelemetry._metrics.measurement import Measurement +from opentelemetry._metrics.observation import Observation from opentelemetry.sdk._metrics import MeterProvider from opentelemetry.sdk._metrics.export import InMemoryMetricReader from opentelemetry.sdk._metrics.point import ( @@ -70,7 +70,7 @@ def test_integration(self): meter = MeterProvider(metric_readers=[reader]).get_meter("test_meter") counter1 = meter.create_counter("counter1") meter.create_observable_gauge( - "observable_gauge1", callbacks=[lambda: [Measurement(value=12)]] + "observable_gauge1", callbacks=[lambda: [Observation(value=12)]] ) counter1.add(1, {"foo": "1"}) counter1.add(1, {"foo": "2"}) diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py index b52216560f..ba792842a1 100644 --- a/opentelemetry-sdk/tests/metrics/test_instrument.py +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -15,7 +15,7 @@ from unittest import TestCase from unittest.mock import Mock -from opentelemetry._metrics.measurement import Measurement as APIMeasurement +from opentelemetry._metrics.observation import Observation from opentelemetry.sdk._metrics.instrument import ( Counter, Histogram, @@ -60,33 +60,33 @@ def test_add_non_monotonic(self): def callable_callback_0(): return [ - APIMeasurement(1, attributes=TEST_ATTRIBUTES), - APIMeasurement(2, attributes=TEST_ATTRIBUTES), - APIMeasurement(3, attributes=TEST_ATTRIBUTES), + Observation(1, attributes=TEST_ATTRIBUTES), + Observation(2, attributes=TEST_ATTRIBUTES), + Observation(3, attributes=TEST_ATTRIBUTES), ] def callable_callback_1(): return [ - APIMeasurement(4, attributes=TEST_ATTRIBUTES), - APIMeasurement(5, attributes=TEST_ATTRIBUTES), - APIMeasurement(6, attributes=TEST_ATTRIBUTES), + Observation(4, attributes=TEST_ATTRIBUTES), + Observation(5, attributes=TEST_ATTRIBUTES), + Observation(6, attributes=TEST_ATTRIBUTES), ] def generator_callback_0(): yield [ - APIMeasurement(1, attributes=TEST_ATTRIBUTES), - APIMeasurement(2, attributes=TEST_ATTRIBUTES), - APIMeasurement(3, attributes=TEST_ATTRIBUTES), + Observation(1, attributes=TEST_ATTRIBUTES), + Observation(2, attributes=TEST_ATTRIBUTES), + Observation(3, attributes=TEST_ATTRIBUTES), ] def generator_callback_1(): yield [ - APIMeasurement(4, attributes=TEST_ATTRIBUTES), - APIMeasurement(5, attributes=TEST_ATTRIBUTES), - APIMeasurement(6, attributes=TEST_ATTRIBUTES), + Observation(4, attributes=TEST_ATTRIBUTES), + Observation(5, attributes=TEST_ATTRIBUTES), + Observation(6, attributes=TEST_ATTRIBUTES), ] From faf8868f7a3d7f64ea3214cf6f201bb95d8ec513 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 20 Apr 2022 11:23:32 -0700 Subject: [PATCH 1197/1517] [docs] SDK documentation fixes (#2620) --- docs/api/metrics.instrument.rst | 2 +- docs/conf.py | 21 ++++++++++++++++++- docs/sdk/metrics.export.rst | 7 +++++++ docs/sdk/metrics.point.rst | 7 +++++++ docs/sdk/metrics.rst | 2 ++ .../src/opentelemetry/_metrics/__init__.py | 2 +- .../src/opentelemetry/_metrics/instrument.py | 14 +++++++++++++ .../opentelemetry/sdk/_metrics/aggregation.py | 20 +++++++++--------- .../sdk/_metrics/export/__init__.py | 4 ++-- 9 files changed, 64 insertions(+), 15 deletions(-) create mode 100644 docs/sdk/metrics.export.rst create mode 100644 docs/sdk/metrics.point.rst diff --git a/docs/api/metrics.instrument.rst b/docs/api/metrics.instrument.rst index 4a678ef07f..7d23f6fa77 100644 --- a/docs/api/metrics.instrument.rst +++ b/docs/api/metrics.instrument.rst @@ -5,4 +5,4 @@ opentelemetry._metrics.instrument :members: :private-members: :undoc-members: - :show-inheritance: + :no-show-inheritance: diff --git a/docs/conf.py b/docs/conf.py index 4f2458d1e7..d250de2b53 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -142,12 +142,31 @@ "examples/error_handler/error_handler_1", ] +_exclude_members = [ + "_ProxyObservableUpDownCounter", + "_ProxyHistogram", + "_ProxyObservableGauge", + "_ProxyInstrument", + "_ProxyAsynchronousInstrument", + "_ProxyCounter", + "_ProxyUpDownCounter", + "_ProxyObservableCounter", + "_ProxyObservableGauge", + "_abc_impl", + "_Adding", + "_Grouping", + "_Monotonic", + "_NonMonotonic", + "Synchronous", + "Asynchronous", +] + autodoc_default_options = { "members": True, "undoc-members": True, "show-inheritance": True, "member-order": "bysource", - "exclude-members": "_ProxyObservableUpDownCounter,_ProxyHistogram,_ProxyObservableGauge,_ProxyInstrument,_ProxyAsynchronousInstrument,_ProxyCounter,_ProxyUpDownCounter,_ProxyObservableCounter,_ProxyObservableGauge,_abc_impl", + "exclude-members": ",".join(_exclude_members), } # -- Options for HTML output ------------------------------------------------- diff --git a/docs/sdk/metrics.export.rst b/docs/sdk/metrics.export.rst new file mode 100644 index 0000000000..775c0a6611 --- /dev/null +++ b/docs/sdk/metrics.export.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk._metrics.export +================================= + +.. automodule:: opentelemetry.sdk._metrics.export + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sdk/metrics.point.rst b/docs/sdk/metrics.point.rst new file mode 100644 index 0000000000..b19f27c460 --- /dev/null +++ b/docs/sdk/metrics.point.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk._metrics.point +================================ + +.. automodule:: opentelemetry.sdk._metrics.point + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sdk/metrics.rst b/docs/sdk/metrics.rst index 76bb064b52..d62ad456b3 100644 --- a/docs/sdk/metrics.rst +++ b/docs/sdk/metrics.rst @@ -16,6 +16,8 @@ Submodules metrics.view metrics.aggregation metrics.metric_reader + metrics.point + metrics.export .. automodule:: opentelemetry.sdk._metrics :members: diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index a42cfd53f3..30b9230be5 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -331,7 +331,7 @@ def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observat @abstractmethod def create_histogram(self, name, unit="", description="") -> Histogram: - """Creates a `Histogram` instrument + """Creates a `opentelemetry._metrics.instrument.Histogram` instrument Args: name: The name of the instrument to be created diff --git a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py index 0718a312e8..d79b0d9aa6 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py @@ -43,6 +43,8 @@ class Instrument(ABC): + """Abstract class that serves as base for all instruments.""" + @abstractmethod def __init__(self, name, unit="", description=""): pass @@ -120,6 +122,8 @@ def add(self, amount, attributes=None): class NoOpCounter(Counter): + """No-op implementation of `Counter`.""" + def __init__(self, name, unit="", description=""): super().__init__(name, unit=unit, description=description) @@ -145,6 +149,8 @@ def add(self, amount, attributes=None): class NoOpUpDownCounter(UpDownCounter): + """No-op implementation of `UpDownCounter`.""" + def __init__(self, name, unit="", description=""): super().__init__(name, unit=unit, description=description) @@ -170,6 +176,8 @@ class ObservableCounter(_Monotonic, Asynchronous): class NoOpObservableCounter(ObservableCounter): + """No-op implementation of `ObservableCounter`.""" + def __init__(self, name, callbacks=None, unit="", description=""): super().__init__(name, callbacks, unit=unit, description=description) @@ -193,6 +201,8 @@ class ObservableUpDownCounter(_NonMonotonic, Asynchronous): class NoOpObservableUpDownCounter(ObservableUpDownCounter): + """No-op implementation of `ObservableUpDownCounter`.""" + def __init__(self, name, callbacks=None, unit="", description=""): super().__init__(name, callbacks, unit=unit, description=description) @@ -221,6 +231,8 @@ def record(self, amount, attributes=None): class NoOpHistogram(Histogram): + """No-op implementation of `Histogram`.""" + def __init__(self, name, unit="", description=""): super().__init__(name, unit=unit, description=description) @@ -247,6 +259,8 @@ class ObservableGauge(_Grouping, Asynchronous): class NoOpObservableGauge(ObservableGauge): + """No-op implementation of `ObservableGauge`.""" + def __init__(self, name, callbacks=None, unit="", description=""): super().__init__(name, callbacks, unit=unit, description=description) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index c6ecff0cfa..ec6497e9c9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -82,16 +82,16 @@ class DefaultAggregation(_AggregationFactory): This aggregation will create an actual aggregation depending on the instrument type, as specified next: - ========================= ==================================== - Instrument Aggregation - ========================= ==================================== - `Counter` `SumAggregation` - `UpDownCounter` `SumAggregation` - `ObservableCounter` `SumAggregation` - `ObservableUpDownCounter` `SumAggregation` - `Histogram` `ExplicitBucketHistogramAggregation` - `ObservableGauge` `LastValueAggregation` - ========================= ==================================== + ============================================= ==================================== + Instrument Aggregation + ============================================= ==================================== + `Counter` `SumAggregation` + `UpDownCounter` `SumAggregation` + `ObservableCounter` `SumAggregation` + `ObservableUpDownCounter` `SumAggregation` + `opentelemetry._metrics.instrument.Histogram` `ExplicitBucketHistogramAggregation` + `ObservableGauge` `LastValueAggregation` + ============================================= ==================================== """ def _create_aggregation(self, instrument: Instrument) -> _Aggregation: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py index 9145f4e0fb..ff505ed4df 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -55,7 +55,7 @@ def export(self, metrics: Sequence[Metric]) -> "MetricExportResult": """Exports a batch of telemetry data. Args: - metrics: The list of `opentelemetry.sdk._metrics.data.MetricData` objects to be exported + metrics: The list of `opentelemetry.sdk._metrics.point.Metric` objects to be exported Returns: The result of the export @@ -97,7 +97,7 @@ def shutdown(self) -> None: class InMemoryMetricReader(MetricReader): - """Implementation of :class:`MetricReader` that returns its metrics from :func:`metrics`. + """Implementation of `MetricReader` that returns its metrics from :func:`get_metrics`. This is useful for e.g. unit tests. """ From 7647a11774a029ead5e8ac864e6f55e9d06f463e Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 21 Apr 2022 12:20:24 +0530 Subject: [PATCH 1198/1517] Add InstrumentationScope and deprecate InstrumentationLibraryInfo (#2583) --- CHANGELOG.md | 2 + .../jaeger/proto/grpc/translate/__init__.py | 17 +++-- .../tests/test_jaeger_exporter_protobuf.py | 16 ++++- .../jaeger/thrift/translate/__init__.py | 16 ++++- .../tests/test_jaeger_exporter_thrift.py | 14 ++++- .../otlp/proto/grpc/_log_exporter/__init__.py | 56 +++++++---------- .../proto/grpc/_metric_exporter/__init__.py | 50 ++++++--------- .../exporter/otlp/proto/grpc/exporter.py | 12 ++-- .../proto/grpc/trace_exporter/__init__.py | 62 +++++++------------ .../tests/logs/test_otlp_logs_exporter.py | 39 ++++++------ .../metrics/test_otlp_metrics_exporter.py | 32 +++++----- .../tests/test_otlp_trace_exporter.py | 61 ++++++++---------- .../http/trace_exporter/encoder/__init__.py | 38 ++++++------ .../tests/test_protobuf_encoder.py | 26 ++++---- .../exporter/zipkin/encoder/__init__.py | 10 ++- .../tests/encoder/common_tests.py | 4 +- .../tests/encoder/test_v1_json.py | 17 ++++- .../tests/encoder/test_v2_json.py | 14 ++++- .../tests/encoder/common_tests.py | 4 +- .../tests/encoder/test_v2_protobuf.py | 14 ++++- .../src/opentelemetry/trace/__init__.py | 3 +- .../src/opentelemetry/sdk/_logs/__init__.py | 14 ++--- .../opentelemetry/sdk/_metrics/__init__.py | 22 +++---- .../sdk/_metrics/_view_instrument_match.py | 2 +- .../opentelemetry/sdk/_metrics/instrument.py | 10 +-- .../src/opentelemetry/sdk/_metrics/point.py | 8 +-- .../src/opentelemetry/sdk/_metrics/view.py | 6 +- .../src/opentelemetry/sdk/trace/__init__.py | 27 +++++++- .../opentelemetry/sdk/util/instrumentation.py | 58 +++++++++++++++++ opentelemetry-sdk/tests/logs/test_export.py | 6 +- .../metrics/test_in_memory_metric_reader.py | 4 +- .../tests/metrics/test_metrics.py | 8 +-- .../test_periodic_exporting_metric_reader.py | 4 +- opentelemetry-sdk/tests/metrics/test_point.py | 11 ++-- opentelemetry-sdk/tests/metrics/test_view.py | 26 ++++---- .../metrics/test_view_instrument_match.py | 8 +-- .../src/opentelemetry/test/metrictestutil.py | 4 +- 37 files changed, 420 insertions(+), 305 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 004e3598ad..412ff0f845 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 instruments to `NoOpCounter`, `NoOpHistogram`, `NoOpObservableCounter`, `NoOpObservableGauge`, `NoOpObservableUpDownCounter`, `NoOpUpDownCounter` ([#2616](https://github.com/open-telemetry/opentelemetry-python/pull/2616)) +- Deprecate InstrumentationLibraryInfo and Add InstrumentationScope + ([#2583](https://github.com/open-telemetry/opentelemetry-python/pull/2583)) ## [1.11.0-0.30b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.0-0.30b0) - 2022-04-18 diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py index 3eb59755c9..ea1f17c00f 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py @@ -35,6 +35,8 @@ NAME_KEY = "otel.library.name" VERSION_KEY = "otel.library.version" +_SCOPE_NAME_KEY = "otel.scope.name" +_SCOPE_VERSION_KEY = "otel.scope.version" def _nsec_to_usec_round(nsec: int) -> int: @@ -299,15 +301,22 @@ def _extract_tags( ) ) - # Instrumentation info KeyValues - if span.instrumentation_info: + # Instrumentation scope KeyValues + if span.instrumentation_scope: name = _get_string_key_value( - NAME_KEY, span.instrumentation_info.name + NAME_KEY, span.instrumentation_scope.name ) version = _get_string_key_value( - VERSION_KEY, span.instrumentation_info.version + VERSION_KEY, span.instrumentation_scope.version + ) + scope_name = _get_string_key_value( + _SCOPE_NAME_KEY, span.instrumentation_scope.name + ) + scope_version = _get_string_key_value( + _SCOPE_VERSION_KEY, span.instrumentation_scope.version ) translated.extend([name, version]) + translated.extend([scope_name, scope_version]) # Make sure to add "error" tag if span status is not OK if not span.status.is_ok: diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py index 926bbff085..6cc4419526 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py @@ -25,6 +25,8 @@ # pylint:disable=import-error from opentelemetry.exporter.jaeger.proto.grpc.gen import model_pb2 from opentelemetry.exporter.jaeger.proto.grpc.translate import ( + _SCOPE_NAME_KEY, + _SCOPE_VERSION_KEY, NAME_KEY, VERSION_KEY, Translate, @@ -39,7 +41,7 @@ from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SpanExportResult -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, ) @@ -188,7 +190,7 @@ def test_translate_to_jaeger(self): context=other_context, parent=None, resource=Resource({}), - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( name="name", version="version" ), ), @@ -391,6 +393,16 @@ def test_translate_to_jaeger(self): v_type=model_pb2.ValueType.STRING, v_str="version", ), + model_pb2.KeyValue( + key=_SCOPE_NAME_KEY, + v_type=model_pb2.ValueType.STRING, + v_str="name", + ), + model_pb2.KeyValue( + key=_SCOPE_VERSION_KEY, + v_type=model_pb2.ValueType.STRING, + v_str="version", + ), ], process=model_pb2.Process( service_name="svc", diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py index 1649b000e9..922833fcca 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py @@ -32,6 +32,8 @@ NAME_KEY = "otel.library.name" VERSION_KEY = "otel.library.version" +_SCOPE_NAME_KEY = "otel.scope.name" +_SCOPE_VERSION_KEY = "otel.scope.version" def _nsec_to_usec_round(nsec: int) -> int: @@ -220,12 +222,20 @@ def _extract_tags(self, span: ReadableSpan) -> Sequence[TCollector.Tag]: ) # Instrumentation info tags - if span.instrumentation_info: - name = _get_string_tag(NAME_KEY, span.instrumentation_info.name) + if span.instrumentation_scope: + name = _get_string_tag(NAME_KEY, span.instrumentation_scope.name) version = _get_string_tag( - VERSION_KEY, span.instrumentation_info.version + VERSION_KEY, span.instrumentation_scope.version ) + scope_name = _get_string_tag( + _SCOPE_NAME_KEY, span.instrumentation_scope.name + ) + scope_version = _get_string_tag( + _SCOPE_VERSION_KEY, span.instrumentation_scope.version + ) + translated.extend([name, version]) + translated.extend([scope_name, scope_version]) # Make sure to add "error" tag if span status is not OK if not span.status.is_ok: diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py index 8bd3e3e359..8d9f2a4247 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py @@ -36,7 +36,7 @@ ) from opentelemetry.sdk.resources import SERVICE_NAME from opentelemetry.sdk.trace import Resource, TracerProvider -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.globals_test import TraceGlobalsTest from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, @@ -303,7 +303,7 @@ def test_translate_to_jaeger(self): context=other_context, parent=None, resource=Resource({}), - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( name="name", version="version" ), ), @@ -461,6 +461,16 @@ def test_translate_to_jaeger(self): vType=jaeger.TagType.STRING, vStr="version", ), + jaeger.Tag( + key=jaeger_exporter.translate._SCOPE_NAME_KEY, + vType=jaeger.TagType.STRING, + vStr="name", + ), + jaeger.Tag( + key=jaeger_exporter.translate._SCOPE_VERSION_KEY, + vType=jaeger.TagType.STRING, + vStr="version", + ), ], ), ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index 522d2de8b5..51433d5740 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -24,9 +24,9 @@ from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( LogsServiceStub, ) -from opentelemetry.proto.common.v1.common_pb2 import InstrumentationLibrary +from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope from opentelemetry.proto.logs.v1.logs_pb2 import ( - InstrumentationLibraryLogs, + ScopeLogs, ResourceLogs, ) from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord @@ -96,44 +96,30 @@ def _translate_data( ) -> ExportLogsServiceRequest: # pylint: disable=attribute-defined-outside-init - sdk_resource_instrumentation_library_logs = {} + sdk_resource_scope_logs = {} for log_data in data: resource = log_data.log_record.resource - instrumentation_library_logs_map = ( - sdk_resource_instrumentation_library_logs.get(resource, {}) - ) - if not instrumentation_library_logs_map: - sdk_resource_instrumentation_library_logs[ - resource - ] = instrumentation_library_logs_map - - instrumentation_library_logs = ( - instrumentation_library_logs_map.get( - log_data.instrumentation_info - ) - ) - if not instrumentation_library_logs: - if log_data.instrumentation_info is not None: - instrumentation_library_logs_map[ - log_data.instrumentation_info - ] = InstrumentationLibraryLogs( - instrumentation_library=InstrumentationLibrary( - name=log_data.instrumentation_info.name, - version=log_data.instrumentation_info.version, + scope_logs_map = sdk_resource_scope_logs.get(resource, {}) + if not scope_logs_map: + sdk_resource_scope_logs[resource] = scope_logs_map + + scope_logs = scope_logs_map.get(log_data.instrumentation_scope) + if not scope_logs: + if log_data.instrumentation_scope is not None: + scope_logs_map[log_data.instrumentation_scope] = ScopeLogs( + scope=InstrumentationScope( + name=log_data.instrumentation_scope.name, + version=log_data.instrumentation_scope.version, ) ) else: - instrumentation_library_logs_map[ - log_data.instrumentation_info - ] = InstrumentationLibraryLogs() - - instrumentation_library_logs = ( - instrumentation_library_logs_map.get( - log_data.instrumentation_info - ) - ) + scope_logs_map[ + log_data.instrumentation_scope + ] = ScopeLogs() + + scope_logs = scope_logs_map.get(log_data.instrumentation_scope) self._collector_kwargs = {} @@ -151,13 +137,13 @@ def _translate_data( "severity_number" ] = log_data.log_record.severity_number.value - instrumentation_library_logs.log_records.append( + scope_logs.log_records.append( PB2LogRecord(**self._collector_kwargs) ) return ExportLogsServiceRequest( resource_logs=get_resource_data( - sdk_resource_instrumentation_library_logs, + sdk_resource_scope_logs, ResourceLogs, "logs", ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py index 92aba2958c..1ad5cc4d80 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py @@ -25,7 +25,7 @@ from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( MetricsServiceStub, ) -from opentelemetry.proto.common.v1.common_pb2 import InstrumentationLibrary +from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_INSECURE, @@ -81,40 +81,30 @@ def __init__( def _translate_data( self, data: Sequence[Metric] ) -> ExportMetricsServiceRequest: - sdk_resource_instrumentation_library_metrics = {} + sdk_resource_scope_metrics = {} for metric in data: resource = metric.resource - instrumentation_library_map = ( - sdk_resource_instrumentation_library_metrics.get(resource, {}) - ) - if not instrumentation_library_map: - sdk_resource_instrumentation_library_metrics[ - resource - ] = instrumentation_library_map - - instrumentation_library_metrics = instrumentation_library_map.get( - metric.instrumentation_info - ) - - if not instrumentation_library_metrics: - if metric.instrumentation_info is not None: - instrumentation_library_map[ - metric.instrumentation_info - ] = pb2.InstrumentationLibraryMetrics( - instrumentation_library=InstrumentationLibrary( - name=metric.instrumentation_info.name, - version=metric.instrumentation_info.version, + scope_map = sdk_resource_scope_metrics.get(resource, {}) + if not scope_map: + sdk_resource_scope_metrics[resource] = scope_map + + scope_metrics = scope_map.get(metric.instrumentation_scope) + + if not scope_metrics: + if metric.instrumentation_scope is not None: + scope_map[metric.instrumentation_scope] = pb2.ScopeMetrics( + scope=InstrumentationScope( + name=metric.instrumentation_scope.name, + version=metric.instrumentation_scope.version, ) ) else: - instrumentation_library_map[ - metric.instrumentation_info - ] = pb2.InstrumentationLibraryMetrics() + scope_map[ + metric.instrumentation_scope + ] = pb2.ScopeMetrics() - instrumentation_library_metrics = instrumentation_library_map.get( - metric.instrumentation_info - ) + scope_metrics = scope_map.get(metric.instrumentation_scope) pbmetric = pb2.Metric( name=metric.name, @@ -167,12 +157,12 @@ def _translate_data( logger.warn("unsupported datapoint type %s", metric.point) continue - instrumentation_library_metrics.metrics.append( + scope_metrics.metrics.append( pbmetric, ) return ExportMetricsServiceRequest( resource_metrics=get_resource_data( - sdk_resource_instrumentation_library_metrics, + sdk_resource_scope_metrics, pb2.ResourceMetrics, "metrics", ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 5c098e8fe0..b965061c5c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -126,9 +126,7 @@ def _translate_key_values(key: str, value: Any) -> KeyValue: def get_resource_data( - sdk_resource_instrumentation_library_data: Dict[ - SDKResource, ResourceDataT - ], + sdk_resource_scope_data: Dict[SDKResource, ResourceDataT], resource_class: Callable[..., TypingResourceT], name: str, ) -> List[TypingResourceT]: @@ -137,8 +135,8 @@ def get_resource_data( for ( sdk_resource, - instrumentation_library_data, - ) in sdk_resource_instrumentation_library_data.items(): + scope_data, + ) in sdk_resource_scope_data.items(): collector_resource = Resource() @@ -156,9 +154,7 @@ def get_resource_data( resource_class( **{ "resource": collector_resource, - "instrumentation_library_{}".format( - name - ): instrumentation_library_data.values(), + "scope_{}".format(name): scope_data.values(), } ) ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 5cc8c885d7..084a5d93b1 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -32,9 +32,9 @@ from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( TraceServiceStub, ) -from opentelemetry.proto.common.v1.common_pb2 import InstrumentationLibrary +from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope from opentelemetry.proto.trace.v1.trace_pb2 import ( - InstrumentationLibrarySpans, + ScopeSpans, ResourceSpans, ) from opentelemetry.proto.trace.v1.trace_pb2 import Span as CollectorSpan @@ -217,45 +217,33 @@ def _translate_data( ) -> ExportTraceServiceRequest: # pylint: disable=attribute-defined-outside-init - sdk_resource_instrumentation_library_spans = {} + sdk_resource_scope_spans = {} for sdk_span in data: - instrumentation_library_spans_map = ( - sdk_resource_instrumentation_library_spans.get( - sdk_span.resource, {} - ) + scope_spans_map = sdk_resource_scope_spans.get( + sdk_span.resource, {} ) # If we haven't seen the Resource yet, add it to the map - if not instrumentation_library_spans_map: - sdk_resource_instrumentation_library_spans[ - sdk_span.resource - ] = instrumentation_library_spans_map - instrumentation_library_spans = ( - instrumentation_library_spans_map.get( - sdk_span.instrumentation_info - ) - ) - # If we haven't seen the InstrumentationInfo for this Resource yet, add it to the map - if not instrumentation_library_spans: - if sdk_span.instrumentation_info is not None: - instrumentation_library_spans_map[ - sdk_span.instrumentation_info - ] = InstrumentationLibrarySpans( - instrumentation_library=InstrumentationLibrary( - name=sdk_span.instrumentation_info.name, - version=sdk_span.instrumentation_info.version, + if not scope_spans_map: + sdk_resource_scope_spans[sdk_span.resource] = scope_spans_map + scope_spans = scope_spans_map.get(sdk_span.instrumentation_scope) + # If we haven't seen the InstrumentationScope for this Resource yet, add it to the map + if not scope_spans: + if sdk_span.instrumentation_scope is not None: + scope_spans_map[ + sdk_span.instrumentation_scope + ] = ScopeSpans( + scope=InstrumentationScope( + name=sdk_span.instrumentation_scope.name, + version=sdk_span.instrumentation_scope.version, ) ) else: - # If no InstrumentationInfo, store in None key - instrumentation_library_spans_map[ - sdk_span.instrumentation_info - ] = InstrumentationLibrarySpans() - instrumentation_library_spans = ( - instrumentation_library_spans_map.get( - sdk_span.instrumentation_info - ) - ) + # If no InstrumentationScope, store in None key + scope_spans_map[ + sdk_span.instrumentation_scope + ] = ScopeSpans() + scope_spans = scope_spans_map.get(sdk_span.instrumentation_scope) self._collector_kwargs = {} self._translate_name(sdk_span) @@ -289,13 +277,11 @@ def _translate_data( f"SPAN_KIND_{sdk_span.kind.name}", ) - instrumentation_library_spans.spans.append( - CollectorSpan(**self._collector_kwargs) - ) + scope_spans.spans.append(CollectorSpan(**self._collector_kwargs)) return ExportTraceServiceRequest( resource_spans=get_resource_data( - sdk_resource_instrumentation_library_spans, + sdk_resource_scope_spans, ResourceSpans, "spans", ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index 5e3a54fbcb..4ee8f6a0b3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -33,14 +33,13 @@ LogsServiceServicer, add_LogsServiceServicer_to_server, ) +from opentelemetry.proto.common.v1.common_pb2 import AnyValue from opentelemetry.proto.common.v1.common_pb2 import ( - AnyValue, - InstrumentationLibrary, - KeyValue, + InstrumentationScope as PB2InstrumentationScope, ) -from opentelemetry.proto.logs.v1.logs_pb2 import InstrumentationLibraryLogs +from opentelemetry.proto.common.v1.common_pb2 import KeyValue from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord -from opentelemetry.proto.logs.v1.logs_pb2 import ResourceLogs +from opentelemetry.proto.logs.v1.logs_pb2 import ResourceLogs, ScopeLogs from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as OTLPResource, ) @@ -50,7 +49,7 @@ SeverityNumber as SDKSeverityNumber, ) from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import TraceFlags @@ -123,7 +122,7 @@ def setUp(self): resource=SDKResource({"key": "value"}), attributes={"a": 1, "b": "c"}, ), - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( "first_name", "first_version" ), ) @@ -139,7 +138,7 @@ def setUp(self): resource=SDKResource({"key": "value"}), attributes={"custom_attr": [1, 2, 3]}, ), - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( "second_name", "second_version" ), ) @@ -154,7 +153,7 @@ def setUp(self): body="Mumbai, Boil water before drinking", resource=SDKResource({"service": "myapp"}), ), - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( "third_name", "third_version" ), ) @@ -304,9 +303,9 @@ def test_translate_log_data(self): ), ] ), - instrumentation_library_logs=[ - InstrumentationLibraryLogs( - instrumentation_library=InstrumentationLibrary( + scope_logs=[ + ScopeLogs( + scope=PB2InstrumentationScope( name="first_name", version="first_version" ), log_records=[ @@ -363,9 +362,9 @@ def test_translate_multiple_logs(self): ), ] ), - instrumentation_library_logs=[ - InstrumentationLibraryLogs( - instrumentation_library=InstrumentationLibrary( + scope_logs=[ + ScopeLogs( + scope=PB2InstrumentationScope( name="first_name", version="first_version" ), log_records=[ @@ -401,8 +400,8 @@ def test_translate_multiple_logs(self): ) ], ), - InstrumentationLibraryLogs( - instrumentation_library=InstrumentationLibrary( + ScopeLogs( + scope=PB2InstrumentationScope( name="second_name", version="second_version" ), log_records=[ @@ -445,9 +444,9 @@ def test_translate_multiple_logs(self): ), ] ), - instrumentation_library_logs=[ - InstrumentationLibraryLogs( - instrumentation_library=InstrumentationLibrary( + scope_logs=[ + ScopeLogs( + scope=PB2InstrumentationScope( name="third_name", version="third_version" ), log_records=[ diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index 4a171b7986..af5d39ff59 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -33,7 +33,7 @@ ) from opentelemetry.proto.common.v1.common_pb2 import ( AnyValue, - InstrumentationLibrary, + InstrumentationScope, KeyValue, ) from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 @@ -293,9 +293,9 @@ def test_translate_sum_int(self): ), ] ), - instrumentation_library_metrics=[ - pb2.InstrumentationLibraryMetrics( - instrumentation_library=InstrumentationLibrary( + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( name="first_name", version="first_version" ), metrics=[ @@ -351,9 +351,9 @@ def test_translate_sum_double(self): ), ] ), - instrumentation_library_metrics=[ - pb2.InstrumentationLibraryMetrics( - instrumentation_library=InstrumentationLibrary( + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( name="first_name", version="first_version" ), metrics=[ @@ -409,9 +409,9 @@ def test_translate_gauge_int(self): ), ] ), - instrumentation_library_metrics=[ - pb2.InstrumentationLibraryMetrics( - instrumentation_library=InstrumentationLibrary( + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( name="first_name", version="first_version" ), metrics=[ @@ -464,9 +464,9 @@ def test_translate_gauge_double(self): ), ] ), - instrumentation_library_metrics=[ - pb2.InstrumentationLibraryMetrics( - instrumentation_library=InstrumentationLibrary( + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( name="first_name", version="first_version" ), metrics=[ @@ -519,9 +519,9 @@ def test_translate_histogram(self): ), ] ), - instrumentation_library_metrics=[ - pb2.InstrumentationLibraryMetrics( - instrumentation_library=InstrumentationLibrary( + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( name="first_name", version="first_version" ), metrics=[ diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 80011cafb0..3d83672901 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -37,19 +37,15 @@ TraceServiceServicer, add_TraceServiceServicer_to_server, ) +from opentelemetry.proto.common.v1.common_pb2 import AnyValue, ArrayValue from opentelemetry.proto.common.v1.common_pb2 import ( - AnyValue, - ArrayValue, - InstrumentationLibrary, - KeyValue, + InstrumentationScope as PB2InstrumentationScope, ) +from opentelemetry.proto.common.v1.common_pb2 import KeyValue from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as OTLPResource, ) -from opentelemetry.proto.trace.v1.trace_pb2 import ( - InstrumentationLibrarySpans, - ResourceSpans, -) +from opentelemetry.proto.trace.v1.trace_pb2 import ResourceSpans, ScopeSpans from opentelemetry.proto.trace.v1.trace_pb2 import Span as OTLPSpan from opentelemetry.proto.trace.v1.trace_pb2 import Status from opentelemetry.sdk.environment_variables import ( @@ -69,7 +65,7 @@ SimpleSpanProcessor, SpanExportResult, ) -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, ) @@ -174,7 +170,7 @@ def setUp(self): } ) ], - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( name="name", version="version" ), ) @@ -190,7 +186,7 @@ def setUp(self): ), resource=SDKResource(OrderedDict([("a", 2), ("b", False)])), parent=Mock(**{"span_id": 12345}), - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( name="name", version="version" ), ) @@ -206,7 +202,7 @@ def setUp(self): ), resource=SDKResource(OrderedDict([("a", 1), ("b", False)])), parent=Mock(**{"span_id": 12345}), - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( name="name2", version="version2" ), ) @@ -493,9 +489,9 @@ def test_translate_spans(self): ), ] ), - instrumentation_library_spans=[ - InstrumentationLibrarySpans( - instrumentation_library=InstrumentationLibrary( + scope_spans=[ + ScopeSpans( + scope=PB2InstrumentationScope( name="name", version="version" ), spans=[ @@ -595,9 +591,9 @@ def test_translate_spans_multi(self): ), ] ), - instrumentation_library_spans=[ - InstrumentationLibrarySpans( - instrumentation_library=InstrumentationLibrary( + scope_spans=[ + ScopeSpans( + scope=PB2InstrumentationScope( name="name", version="version" ), spans=[ @@ -677,8 +673,8 @@ def test_translate_spans_multi(self): ) ], ), - InstrumentationLibrarySpans( - instrumentation_library=InstrumentationLibrary( + ScopeSpans( + scope=PB2InstrumentationScope( name="name2", version="version2" ), spans=[ @@ -717,9 +713,9 @@ def test_translate_spans_multi(self): ), ] ), - instrumentation_library_spans=[ - InstrumentationLibrarySpans( - instrumentation_library=InstrumentationLibrary( + scope_spans=[ + ScopeSpans( + scope=PB2InstrumentationScope( name="name", version="version" ), spans=[ @@ -763,12 +759,7 @@ def _check_translated_status( translated: ExportTraceServiceRequest, code_expected: Status, ): - status = ( - translated.resource_spans[0] - .instrumentation_library_spans[0] - .spans[0] - .status - ) + status = translated.resource_spans[0].scope_spans[0].spans[0].status self.assertEqual( status.code, @@ -861,28 +852,28 @@ def test_dropped_values(self): self.assertEqual( 1, translated.resource_spans[0] - .instrumentation_library_spans[0] + .scope_spans[0] .spans[0] .dropped_links_count, ) self.assertEqual( 2, translated.resource_spans[0] - .instrumentation_library_spans[0] + .scope_spans[0] .spans[0] .dropped_attributes_count, ) self.assertEqual( 3, translated.resource_spans[0] - .instrumentation_library_spans[0] + .scope_spans[0] .spans[0] .dropped_events_count, ) self.assertEqual( 2, translated.resource_spans[0] - .instrumentation_library_spans[0] + .scope_spans[0] .spans[0] .links[0] .dropped_attributes_count, @@ -890,7 +881,7 @@ def test_dropped_values(self): self.assertEqual( 2, translated.resource_spans[0] - .instrumentation_library_spans[0] + .scope_spans[0] .spans[0] .events[0] .dropped_attributes_count, @@ -908,7 +899,7 @@ def _create_span_with_status(status: SDKStatus): } ), parent=Mock(**{"span_id": 12345}), - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( name="name", version="version" ), ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py index 24497d204d..fc0d9608ef 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py @@ -24,14 +24,14 @@ ArrayValue as PB2ArrayValue, ) from opentelemetry.proto.common.v1.common_pb2 import ( - InstrumentationLibrary as PB2InstrumentationLibrary, + InstrumentationScope as PB2InstrumentationScope, ) from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as PB2Resource, ) from opentelemetry.proto.trace.v1.trace_pb2 import ( - InstrumentationLibrarySpans as PB2InstrumentationLibrarySpans, + ScopeSpans as PB2ScopeSpans, ) from opentelemetry.proto.trace.v1.trace_pb2 import ( ResourceSpans as PB2ResourceSpans, @@ -39,7 +39,7 @@ from opentelemetry.proto.trace.v1.trace_pb2 import Span as PB2SPan from opentelemetry.proto.trace.v1.trace_pb2 import Status as PB2Status from opentelemetry.sdk.trace import Event -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.trace import Span as SDKSpan from opentelemetry.trace import Link @@ -91,7 +91,7 @@ def _encode_resource_spans( for sdk_span in sdk_spans: sdk_resource = sdk_span.resource - sdk_instrumentation = sdk_span.instrumentation_info or None + sdk_instrumentation = sdk_span.instrumentation_scope or None pb2_span = _encode_span(sdk_span) if sdk_resource not in sdk_resource_spans.keys(): @@ -110,20 +110,18 @@ def _encode_resource_spans( pb2_resource_spans = [] for sdk_resource, sdk_instrumentations in sdk_resource_spans.items(): - instrumentation_library_spans = [] + scope_spans = [] for sdk_instrumentation, pb2_spans in sdk_instrumentations.items(): - instrumentation_library_spans.append( - PB2InstrumentationLibrarySpans( - instrumentation_library=( - _encode_instrumentation_library(sdk_instrumentation) - ), + scope_spans.append( + PB2ScopeSpans( + scope=(_encode_instrumentation_scope(sdk_instrumentation)), spans=pb2_spans, ) ) pb2_resource_spans.append( PB2ResourceSpans( resource=_encode_resource(sdk_resource), - instrumentation_library_spans=instrumentation_library_spans, + scope_spans=scope_spans, ) ) @@ -245,17 +243,17 @@ def _encode_resource(resource: Resource) -> PB2Resource: return pb2_resource -def _encode_instrumentation_library( - instrumentation_info: InstrumentationInfo, -) -> PB2InstrumentationLibrary: - if instrumentation_info is None: - pb2_instrumentation_library = PB2InstrumentationLibrary() +def _encode_instrumentation_scope( + instrumentation_scope: InstrumentationScope, +) -> PB2InstrumentationScope: + if instrumentation_scope is None: + pb2_instrumentation_scope = PB2InstrumentationScope() else: - pb2_instrumentation_library = PB2InstrumentationLibrary( - name=instrumentation_info.name, - version=instrumentation_info.version, + pb2_instrumentation_scope = PB2InstrumentationScope( + name=instrumentation_scope.name, + version=instrumentation_scope.version, ) - return pb2_instrumentation_library + return pb2_instrumentation_scope def _encode_value(value: Any) -> PB2AnyValue: diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py index a6ebc05145..b3718623c1 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py @@ -29,18 +29,16 @@ ) from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue from opentelemetry.proto.common.v1.common_pb2 import ( - InstrumentationLibrary as PB2InstrumentationLibrary, + InstrumentationScope as PB2InstrumentationScope, ) from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as PB2Resource, ) -from opentelemetry.proto.trace.v1.trace_pb2 import ( - InstrumentationLibrarySpans as PB2InstrumentationLibrarySpans, -) from opentelemetry.proto.trace.v1.trace_pb2 import ( ResourceSpans as PB2ResourceSpans, ) +from opentelemetry.proto.trace.v1.trace_pb2 import ScopeSpans as PB2ScopeSpans from opentelemetry.proto.trace.v1.trace_pb2 import Span as PB2SPan from opentelemetry.proto.trace.v1.trace_pb2 import Status as PB2Status from opentelemetry.sdk.trace import Event as SDKEvent @@ -48,7 +46,7 @@ from opentelemetry.sdk.trace import SpanContext as SDKSpanContext from opentelemetry.sdk.trace import _Span as SDKSpan from opentelemetry.sdk.util.instrumentation import ( - InstrumentationInfo as SDKInstrumentationInfo, + InstrumentationScope as SDKInstrumentationScope, ) from opentelemetry.trace import Link as SDKLink from opentelemetry.trace import SpanKind as SDKSpanKind @@ -158,7 +156,7 @@ def get_exhaustive_otel_span_list() -> List[SDKSpan]: context=other_context, parent=None, resource=SDKResource({}), - instrumentation_info=SDKInstrumentationInfo( + instrumentation_scope=SDKInstrumentationScope( name="name", version="version" ), ) @@ -178,9 +176,9 @@ def get_exhaustive_test_spans( resource_spans=[ PB2ResourceSpans( resource=PB2Resource(), - instrumentation_library_spans=[ - PB2InstrumentationLibrarySpans( - instrumentation_library=PB2InstrumentationLibrary(), + scope_spans=[ + PB2ScopeSpans( + scope=PB2InstrumentationScope(), spans=[ PB2SPan( trace_id=trace_id, @@ -274,8 +272,8 @@ def get_exhaustive_test_spans( ) ], ), - PB2InstrumentationLibrarySpans( - instrumentation_library=PB2InstrumentationLibrary( + PB2ScopeSpans( + scope=PB2InstrumentationScope( name="name", version="version", ), @@ -313,9 +311,9 @@ def get_exhaustive_test_spans( ) ] ), - instrumentation_library_spans=[ - PB2InstrumentationLibrarySpans( - instrumentation_library=PB2InstrumentationLibrary(), + scope_spans=[ + PB2ScopeSpans( + scope=PB2InstrumentationScope(), spans=[ PB2SPan( trace_id=trace_id, diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py index 5353a896cf..bb90daa37c 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder/__init__.py @@ -38,6 +38,8 @@ DEFAULT_MAX_TAG_VALUE_LENGTH = 128 NAME_KEY = "otel.library.name" VERSION_KEY = "otel.library.version" +_SCOPE_NAME_KEY = "otel.scope.name" +_SCOPE_VERSION_KEY = "otel.scope.version" logger = logging.getLogger(__name__) @@ -196,11 +198,13 @@ def _extract_tags_from_span(self, span: Span) -> Dict[str, str]: tags = self._extract_tags_from_dict(span.attributes) if span.resource: tags.update(self._extract_tags_from_dict(span.resource.attributes)) - if span.instrumentation_info is not None: + if span.instrumentation_scope is not None: tags.update( { - NAME_KEY: span.instrumentation_info.name, - VERSION_KEY: span.instrumentation_info.version, + NAME_KEY: span.instrumentation_scope.name, + VERSION_KEY: span.instrumentation_scope.version, + _SCOPE_NAME_KEY: span.instrumentation_scope.name, + _SCOPE_VERSION_KEY: span.instrumentation_scope.version, } ) if span.status.status_code is not StatusCode.UNSET: diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py index 8eaa5b1563..aae4b3bb23 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py @@ -24,7 +24,7 @@ ) from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import TraceFlags from opentelemetry.trace.status import Status, StatusCode @@ -417,7 +417,7 @@ def get_exhaustive_otel_span_list() -> List[trace._Span]: context=other_context, parent=None, resource=trace.Resource({}), - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( name="name", version="version" ), ) diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py index b099040acc..59a750eb51 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py @@ -14,7 +14,12 @@ import json from opentelemetry import trace as trace_api -from opentelemetry.exporter.zipkin.encoder import NAME_KEY, VERSION_KEY +from opentelemetry.exporter.zipkin.encoder import ( + _SCOPE_NAME_KEY, + _SCOPE_VERSION_KEY, + NAME_KEY, + VERSION_KEY, +) from opentelemetry.exporter.zipkin.json.v1 import JsonV1Encoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace @@ -164,6 +169,16 @@ def test_encode(self): "value": "version", "endpoint": local_endpoint, }, + { + "key": _SCOPE_NAME_KEY, + "value": "name", + "endpoint": local_endpoint, + }, + { + "key": _SCOPE_VERSION_KEY, + "value": "version", + "endpoint": local_endpoint, + }, ], }, ] diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py index 9ceb9383fd..85cc91a0d9 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py @@ -14,7 +14,12 @@ import json from opentelemetry import trace as trace_api -from opentelemetry.exporter.zipkin.encoder import NAME_KEY, VERSION_KEY +from opentelemetry.exporter.zipkin.encoder import ( + _SCOPE_NAME_KEY, + _SCOPE_VERSION_KEY, + NAME_KEY, + VERSION_KEY, +) from opentelemetry.exporter.zipkin.json.v2 import JsonV2Encoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace @@ -124,7 +129,12 @@ def test_encode(self): - (otel_spans[3].start_time // 10**3), "localEndpoint": local_endpoint, "kind": span_kind, - "tags": {NAME_KEY: "name", VERSION_KEY: "version"}, + "tags": { + NAME_KEY: "name", + VERSION_KEY: "version", + _SCOPE_NAME_KEY: "name", + _SCOPE_VERSION_KEY: "version", + }, }, ] diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py index 8eaa5b1563..aae4b3bb23 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py @@ -24,7 +24,7 @@ ) from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import TraceFlags from opentelemetry.trace.status import Status, StatusCode @@ -417,7 +417,7 @@ def get_exhaustive_otel_span_list() -> List[trace._Span]: context=other_context, parent=None, resource=trace.Resource({}), - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( name="name", version="version" ), ) diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py index 4ba1ea220a..8ce61a92a1 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py @@ -14,7 +14,12 @@ import ipaddress import json -from opentelemetry.exporter.zipkin.encoder import NAME_KEY, VERSION_KEY +from opentelemetry.exporter.zipkin.encoder import ( + _SCOPE_NAME_KEY, + _SCOPE_VERSION_KEY, + NAME_KEY, + VERSION_KEY, +) from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.exporter.zipkin.proto.http.v2 import ProtobufEncoder from opentelemetry.exporter.zipkin.proto.http.v2.gen import zipkin_pb2 @@ -183,7 +188,12 @@ def test_encode(self): ), local_endpoint=local_endpoint, kind=span_kind, - tags={NAME_KEY: "name", VERSION_KEY: "version"}, + tags={ + NAME_KEY: "name", + VERSION_KEY: "version", + _SCOPE_NAME_KEY: "name", + _SCOPE_VERSION_KEY: "version", + }, debug=False, ), ], diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index cc5152ce83..53eb0e96db 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -202,7 +202,8 @@ def get_tracer( vs. a functional tracer). Args: - instrumenting_module_name: The name of the instrumenting module. + instrumenting_module_name: The uniquely identifiable name for instrumentaion + scope, such as instrumentation library, package, module or class name. ``__name__`` may not be used as this can result in different tracer names if the tracers are in different files. It is better to use a fixed string that can be imported where diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index c6887a6718..ae75df1d02 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -28,7 +28,7 @@ ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util import ns_to_iso_str -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import ( format_span_id, @@ -107,10 +107,10 @@ class LogData: def __init__( self, log_record: LogRecord, - instrumentation_info: InstrumentationInfo, + instrumentation_scope: InstrumentationScope, ): self.log_record = log_record - self.instrumentation_info = instrumentation_info + self.instrumentation_scope = instrumentation_scope class LogProcessor(abc.ABC): @@ -379,11 +379,11 @@ def __init__( multi_log_processor: Union[ SynchronousMultiLogProcessor, ConcurrentMultiLogProcessor ], - instrumentation_info: InstrumentationInfo, + instrumentation_scope: InstrumentationScope, ): self._resource = resource self._multi_log_processor = multi_log_processor - self._instrumentation_info = instrumentation_info + self._instrumentation_scope = instrumentation_scope @property def resource(self): @@ -393,7 +393,7 @@ def emit(self, record: LogRecord): """Emits the :class:`LogData` by associating :class:`LogRecord` and instrumentation info. """ - log_data = LogData(record, self._instrumentation_info) + log_data = LogData(record, self._instrumentation_scope) self._multi_log_processor.emit(log_data) # TODO: Should this flush everything in pipeline? @@ -432,7 +432,7 @@ def get_log_emitter( return LogEmitter( self._resource, self._multi_log_processor, - InstrumentationInfo( + InstrumentationScope( instrumenting_module_name, instrumenting_module_verison ), ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 29e4972d80..afa445b359 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -48,7 +48,7 @@ from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration from opentelemetry.sdk._metrics.view import View from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util._once import Once _logger = getLogger(__name__) @@ -57,11 +57,11 @@ class Meter(APIMeter): def __init__( self, - instrumentation_info: InstrumentationInfo, + instrumentation_scope: InstrumentationScope, measurement_consumer: MeasurementConsumer, ): - super().__init__(instrumentation_info) - self._instrumentation_info = instrumentation_info + super().__init__(instrumentation_scope) + self._instrumentation_scope = instrumentation_scope self._measurement_consumer = measurement_consumer def create_counter(self, name, unit="", description="") -> APICounter: @@ -80,7 +80,7 @@ def create_counter(self, name, unit="", description="") -> APICounter: return Counter( name, - self._instrumentation_info, + self._instrumentation_scope, self._measurement_consumer, unit, description, @@ -104,7 +104,7 @@ def create_up_down_counter( return UpDownCounter( name, - self._instrumentation_info, + self._instrumentation_scope, self._measurement_consumer, unit, description, @@ -129,7 +129,7 @@ def create_observable_counter( ) instrument = ObservableCounter( name, - self._instrumentation_info, + self._instrumentation_scope, self._measurement_consumer, callbacks, unit, @@ -155,7 +155,7 @@ def create_histogram(self, name, unit="", description="") -> APIHistogram: ) return Histogram( name, - self._instrumentation_info, + self._instrumentation_scope, self._measurement_consumer, unit, description, @@ -179,7 +179,7 @@ def create_observable_gauge( instrument = ObservableGauge( name, - self._instrumentation_info, + self._instrumentation_scope, self._measurement_consumer, callbacks, unit, @@ -210,7 +210,7 @@ def create_observable_up_down_counter( instrument = ObservableUpDownCounter( name, - self._instrumentation_info, + self._instrumentation_scope, self._measurement_consumer, callbacks, unit, @@ -366,7 +366,7 @@ def get_meter( _logger.warning("Meter name cannot be None or empty.") return NoOpMeter(name, version=version, schema_url=schema_url) - info = InstrumentationInfo(name, version, schema_url) + info = InstrumentationScope(name, version, schema_url) with self._meter_lock: if not self._meters.get(info): # FIXME #2558 pass SDKConfig object to meter so that the meter diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py index b124f77318..af1d9eb351 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py @@ -106,7 +106,7 @@ def collect(self, temporality: int) -> Iterable[Metric]: self._view._description or self._instrument.description ), - instrumentation_info=self._instrument.instrumentation_info, + instrumentation_scope=self._instrument.instrumentation_scope, name=self._view._name or self._instrument.name, resource=self._sdk_config.resource, unit=self._instrument.unit, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index 260a9a7767..b637bba9f0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -32,7 +32,7 @@ from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.measurement_consumer import MeasurementConsumer -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope _logger = logging.getLogger(__name__) @@ -41,7 +41,7 @@ class _Synchronous: def __init__( self, name: str, - instrumentation_info: InstrumentationInfo, + instrumentation_scope: InstrumentationScope, measurement_consumer: MeasurementConsumer, unit: str = "", description: str = "", @@ -49,7 +49,7 @@ def __init__( self.name = name self.unit = unit self.description = description - self.instrumentation_info = instrumentation_info + self.instrumentation_scope = instrumentation_scope self._measurement_consumer = measurement_consumer super().__init__(name, unit=unit, description=description) @@ -58,7 +58,7 @@ class _Asynchronous: def __init__( self, name: str, - instrumentation_info: InstrumentationInfo, + instrumentation_scope: InstrumentationScope, measurement_consumer: MeasurementConsumer, callbacks: Optional[Iterable[CallbackT]] = None, unit: str = "", @@ -67,7 +67,7 @@ def __init__( self.name = name self.unit = unit self.description = description - self.instrumentation_info = instrumentation_info + self.instrumentation_scope = instrumentation_scope self._measurement_consumer = measurement_consumer super().__init__(name, callbacks, unit=unit, description=description) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py index fcc127e949..3417c197e6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py @@ -18,7 +18,7 @@ from typing import Sequence, Union from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util.types import Attributes @@ -69,7 +69,7 @@ class Metric: # common fields to all metric kinds attributes: Attributes description: str - instrumentation_info: InstrumentationInfo + instrumentation_scope: InstrumentationScope name: str resource: Resource unit: str @@ -81,8 +81,8 @@ def to_json(self) -> str: { "attributes": self.attributes if self.attributes else "", "description": self.description if self.description else "", - "instrumentation_info": repr(self.instrumentation_info) - if self.instrumentation_info + "instrumentation_scope": repr(self.instrumentation_scope) + if self.instrumentation_scope else "", "name": self.name, "resource": repr(self.resource.attributes) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py index d83ce47cf6..28adebe643 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py @@ -144,16 +144,16 @@ def _match(self, instrument: Instrument) -> bool: return False if self._meter_name is not None: - if instrument.instrumentation_info.name != self._meter_name: + if instrument.instrumentation_scope.name != self._meter_name: return False if self._meter_version is not None: - if instrument.instrumentation_info.version != self._meter_version: + if instrument.instrumentation_scope.version != self._meter_version: return False if self._meter_schema_url is not None: if ( - instrument.instrumentation_info.schema_url + instrument.instrumentation_scope.schema_url != self._meter_schema_url ): return False diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 6a4c132ee7..f527967378 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -37,6 +37,8 @@ Union, ) +from deprecated import deprecated + from opentelemetry import context as context_api from opentelemetry import trace as trace_api from opentelemetry.attributes import BoundedAttributes @@ -55,7 +57,10 @@ from opentelemetry.sdk.trace import sampling from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator from opentelemetry.sdk.util import BoundedList -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationInfo, + InstrumentationScope, +) from opentelemetry.trace import SpanContext from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types @@ -357,11 +362,13 @@ def __init__( status: Status = Status(StatusCode.UNSET), start_time: Optional[int] = None, end_time: Optional[int] = None, + instrumentation_scope: InstrumentationScope = None, ) -> None: self._name = name self._context = context self._kind = kind self._instrumentation_info = instrumentation_info + self._instrumentation_scope = instrumentation_scope self._parent = parent self._start_time = start_time self._end_time = end_time @@ -437,9 +444,16 @@ def resource(self) -> Resource: return self._resource @property + @deprecated( + version="1.11.1", reason="You should use instrumentation_scope" + ) def instrumentation_info(self) -> InstrumentationInfo: return self._instrumentation_info + @property + def instrumentation_scope(self) -> InstrumentationScope: + return self._instrumentation_scope + def to_json(self, indent=4): parent_id = None if self.parent is not None: @@ -729,6 +743,7 @@ def __init__( record_exception: bool = True, set_status_on_exception: bool = True, limits=_UnsetLimits, + instrumentation_scope: InstrumentationScope = None, ) -> None: super().__init__( name=name, @@ -737,6 +752,7 @@ def __init__( kind=kind, resource=resource, instrumentation_info=instrumentation_info, + instrumentation_scope=instrumentation_scope, ) self._sampler = sampler self._trace_config = trace_config @@ -835,6 +851,7 @@ def _readable_span(self) -> ReadableSpan: start_time=self._start_time, end_time=self._end_time, instrumentation_info=self._instrumentation_info, + instrumentation_scope=self._instrumentation_scope, ) def start( @@ -956,6 +973,7 @@ def __init__( id_generator: IdGenerator, instrumentation_info: InstrumentationInfo, span_limits: SpanLimits, + instrumentation_scope: InstrumentationScope, ) -> None: self.sampler = sampler self.resource = resource @@ -963,6 +981,7 @@ def __init__( self.id_generator = id_generator self.instrumentation_info = instrumentation_info self._span_limits = span_limits + self._instrumentation_scope = instrumentation_scope @contextmanager def start_as_current_span( @@ -1065,6 +1084,7 @@ def start_span( # pylint: disable=too-many-locals record_exception=record_exception, set_status_on_exception=set_status_on_exception, limits=self._span_limits, + instrumentation_scope=self._instrumentation_scope, ) span.start(start_time=start_time, parent_context=context) else: @@ -1127,6 +1147,11 @@ def get_tracer( schema_url, ), self._span_limits, + InstrumentationScope( + instrumenting_module_name, + instrumenting_library_version, + schema_url, + ), ) def add_span_processor(self, span_processor: SpanProcessor) -> None: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index d634e1b833..1b2c1c74a7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -13,6 +13,8 @@ # limitations under the License. import typing +from deprecated import deprecated + class InstrumentationInfo: """Immutable information about an instrumentation library module. @@ -23,6 +25,62 @@ class InstrumentationInfo: __slots__ = ("_name", "_version", "_schema_url") + @deprecated(version="1.11.1", reason="You should use InstrumentationScope") + def __init__( + self, + name: str, + version: typing.Optional[str] = None, + schema_url: typing.Optional[str] = None, + ): + self._name = name + self._version = version + self._schema_url = schema_url + + def __repr__(self): + return f"{type(self).__name__}({self._name}, {self._version}, {self._schema_url})" + + def __hash__(self): + return hash((self._name, self._version, self._schema_url)) + + def __eq__(self, value): + return type(value) is type(self) and ( + self._name, + self._version, + self._schema_url, + ) == (value._name, value._version, value._schema_url) + + def __lt__(self, value): + if type(value) is not type(self): + return NotImplemented + return (self._name, self._version, self._schema_url) < ( + value._name, + value._version, + value._schema_url, + ) + + @property + def schema_url(self) -> typing.Optional[str]: + return self._schema_url + + @property + def version(self) -> typing.Optional[str]: + return self._version + + @property + def name(self) -> str: + return self._name + + +class InstrumentationScope: + """A logical unit of the application code with which the emitted telemetry can be + associated. + + See `opentelemetry.trace.TracerProvider.get_tracer` for the meaning of these + properties. + """ + + __slots__ = ("_name", "_version", "_schema_url") + def __init__( self, name: str, diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 42d66176fd..a1c39d6df2 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -39,7 +39,7 @@ ) from opentelemetry.sdk._logs.severity import SeverityNumber from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.concurrency_test import ConcurrencyTestBase from opentelemetry.trace import TraceFlags from opentelemetry.trace.span import INVALID_SPAN_CONTEXT @@ -338,7 +338,7 @@ def test_export(self): # pylint: disable=no-self-use resource=SDKResource({"key": "value"}), attributes={"a": 1, "b": "c"}, ), - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( "first_name", "first_version" ), ) @@ -366,7 +366,7 @@ def formatter(record): # pylint: disable=unused-argument exporter = ConsoleLogExporter(out=mock_stdout, formatter=formatter) log_data = LogData( log_record=LogRecord(), - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( "first_name", "first_version" ), ) diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py index 89d39b5e2d..3da776191a 100644 --- a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py @@ -24,7 +24,7 @@ Sum, ) from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope class TestInMemoryMetricReader(TestCase): @@ -39,7 +39,7 @@ def test_converts_metrics_to_list(self): metric = Metric( attributes={"myattr": "baz"}, description="", - instrumentation_info=InstrumentationInfo("testmetrics"), + instrumentation_scope=InstrumentationScope("testmetrics"), name="foo", resource=Resource.create(), unit="", diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 2c2d48a793..f4f31a6294 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -88,7 +88,7 @@ def test_resource(self): def test_get_meter(self): """ `MeterProvider.get_meter` arguments are used to create an - `InstrumentationInfo` object on the created `Meter`. + `InstrumentationScope` object on the created `Meter`. """ meter = MeterProvider().get_meter( @@ -97,9 +97,9 @@ def test_get_meter(self): schema_url="schema_url", ) - self.assertEqual(meter._instrumentation_info.name, "name") - self.assertEqual(meter._instrumentation_info.version, "version") - self.assertEqual(meter._instrumentation_info.schema_url, "schema_url") + self.assertEqual(meter._instrumentation_scope.name, "name") + self.assertEqual(meter._instrumentation_scope.version, "version") + self.assertEqual(meter._instrumentation_scope.schema_url, "schema_url") def test_get_meter_empty(self): """ diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index 4552a69063..728e291180 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -47,7 +47,7 @@ def shutdown(self): name="sum_name", attributes={}, description="", - instrumentation_info=None, + instrumentation_scope=None, resource=Resource.create(), unit="", point=Sum( @@ -62,7 +62,7 @@ def shutdown(self): name="gauge_name", attributes={}, description="", - instrumentation_info=None, + instrumentation_scope=None, resource=Resource.create(), unit="", point=Gauge( diff --git a/opentelemetry-sdk/tests/metrics/test_point.py b/opentelemetry-sdk/tests/metrics/test_point.py index 0e5d99a726..1748fb7ba4 100644 --- a/opentelemetry-sdk/tests/metrics/test_point.py +++ b/opentelemetry-sdk/tests/metrics/test_point.py @@ -16,14 +16,14 @@ from opentelemetry.sdk._metrics.point import Gauge, Histogram, Metric, Sum from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope def _create_metric(value): return Metric( attributes={"attr-key": "test-val"}, description="test-description", - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( name="name", version="version" ), name="test-name", @@ -35,6 +35,7 @@ def _create_metric(value): class TestDatapointToJSON(TestCase): def test_sum(self): + self.maxDiff = None point = _create_metric( Sum( aggregation_temporality=2, @@ -45,14 +46,14 @@ def test_sum(self): ) ) self.assertEqual( - '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_info": "InstrumentationInfo(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"aggregation_temporality": 2, "is_monotonic": true, "start_time_unix_nano": 10, "time_unix_nano": 20, "value": 9}}', + '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_scope": "InstrumentationScope(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"aggregation_temporality": 2, "is_monotonic": true, "start_time_unix_nano": 10, "time_unix_nano": 20, "value": 9}}', point.to_json(), ) def test_gauge(self): point = _create_metric(Gauge(time_unix_nano=40, value=20)) self.assertEqual( - '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_info": "InstrumentationInfo(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"time_unix_nano": 40, "value": 20}}', + '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_scope": "InstrumentationScope(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"time_unix_nano": 40, "value": 20}}', point.to_json(), ) @@ -71,6 +72,6 @@ def test_histogram(self): ) self.maxDiff = None self.assertEqual( - '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_info": "InstrumentationInfo(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"aggregation_temporality": 1, "bucket_counts": [0, 0, 1, 0], "explicit_bounds": [0.1, 0.5, 0.9, 1], "max": 0.8, "min": 0.8, "start_time_unix_nano": 50, "sum": 0.8, "time_unix_nano": 60}}', + '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_scope": "InstrumentationScope(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"aggregation_temporality": 1, "bucket_counts": [0, 0, 1, 0], "explicit_bounds": [0.1, 0.5, 0.9, 1], "max": 0.8, "min": 0.8, "start_time_unix_nano": 50, "sum": 0.8, "time_unix_nano": 60}}', point.to_json(), ) diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py index 80f5884c8d..a78e282244 100644 --- a/opentelemetry-sdk/tests/metrics/test_view.py +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -41,7 +41,7 @@ def test_meter_name(self): self.assertTrue( View(meter_name="meter_name")._match( - Mock(**{"instrumentation_info.name": "meter_name"}) + Mock(**{"instrumentation_scope.name": "meter_name"}) ) ) @@ -49,7 +49,7 @@ def test_meter_version(self): self.assertTrue( View(meter_version="meter_version")._match( - Mock(**{"instrumentation_info.version": "meter_version"}) + Mock(**{"instrumentation_scope.version": "meter_version"}) ) ) @@ -57,21 +57,25 @@ def test_meter_schema_url(self): self.assertTrue( View(meter_schema_url="meter_schema_url")._match( - Mock(**{"instrumentation_info.schema_url": "meter_schema_url"}) + Mock( + **{"instrumentation_scope.schema_url": "meter_schema_url"} + ) ) ) self.assertFalse( View(meter_schema_url="meter_schema_url")._match( Mock( **{ - "instrumentation_info.schema_url": "meter_schema_urlabc" + "instrumentation_scope.schema_url": "meter_schema_urlabc" } ) ) ) self.assertTrue( View(meter_schema_url="meter_schema_url")._match( - Mock(**{"instrumentation_info.schema_url": "meter_schema_url"}) + Mock( + **{"instrumentation_scope.schema_url": "meter_schema_url"} + ) ) ) @@ -87,9 +91,9 @@ def test_additive_criteria(self): view._match( Mock( **{ - "instrumentation_info.name": "meter_name", - "instrumentation_info.version": "meter_version", - "instrumentation_info.schema_url": "meter_schema_url", + "instrumentation_scope.name": "meter_name", + "instrumentation_scope.version": "meter_version", + "instrumentation_scope.schema_url": "meter_schema_url", } ) ) @@ -98,9 +102,9 @@ def test_additive_criteria(self): view._match( Mock( **{ - "instrumentation_info.name": "meter_name", - "instrumentation_info.version": "meter_version", - "instrumentation_info.schema_url": "meter_schema_vrl", + "instrumentation_scope.name": "meter_name", + "instrumentation_scope.version": "meter_version", + "instrumentation_scope.schema_url": "meter_schema_vrl", } ) ) diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 6edba9b4eb..20b1a04169 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -37,11 +37,11 @@ def setUpClass(cls): cls.mock_aggregation_factory._create_aggregation() ) cls.mock_resource = Mock() - cls.mock_instrumentation_info = Mock() + cls.mock_instrumentation_scope = Mock() def test_consume_measurement(self): instrument1 = Mock(name="instrument1") - instrument1.instrumentation_info = self.mock_instrumentation_info + instrument1.instrumentation_scope = self.mock_instrumentation_scope sdk_config = SdkConfiguration( resource=self.mock_resource, metric_readers=[], @@ -158,7 +158,7 @@ def test_collect(self): instrument1 = Mock( name="instrument1", description="description", unit="unit" ) - instrument1.instrumentation_info = self.mock_instrumentation_info + instrument1.instrumentation_scope = self.mock_instrumentation_scope sdk_config = SdkConfiguration( resource=self.mock_resource, metric_readers=[], @@ -191,7 +191,7 @@ def test_collect(self): Metric( attributes={"c": "d"}, description="description", - instrumentation_info=self.mock_instrumentation_info, + instrumentation_scope=self.mock_instrumentation_scope, name="name", resource=self.mock_resource, unit="unit", diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py index 4bf45f357d..97b411ac4b 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py @@ -23,7 +23,7 @@ Sum, ) from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import InstrumentationScope def _generate_metric( @@ -37,7 +37,7 @@ def _generate_metric( unit = "s" return Metric( resource=SDKResource(OrderedDict([("a", 1), ("b", False)])), - instrumentation_info=InstrumentationInfo( + instrumentation_scope=InstrumentationScope( "first_name", "first_version" ), attributes=attributes, From 5ce889f6aff194493a86fc4adec0d8ad68f5ca9f Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 21 Apr 2022 10:32:05 -0700 Subject: [PATCH 1199/1517] [docs] adding documentation for aggregation types (#2630) --- docs/sdk/metrics.aggregation.rst | 4 +-- docs/sdk/metrics.point.rst | 2 +- .../opentelemetry/sdk/_metrics/aggregation.py | 25 +++++++++++++++++++ .../src/opentelemetry/sdk/_metrics/point.py | 2 +- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/sdk/metrics.aggregation.rst b/docs/sdk/metrics.aggregation.rst index add890e11a..4214444ed1 100644 --- a/docs/sdk/metrics.aggregation.rst +++ b/docs/sdk/metrics.aggregation.rst @@ -1,7 +1,7 @@ opentelemetry.sdk._metrics.aggregation -========================================== +====================================== .. automodule:: opentelemetry.sdk._metrics.aggregation :members: :undoc-members: - :show-inheritance: + :no-show-inheritance: diff --git a/docs/sdk/metrics.point.rst b/docs/sdk/metrics.point.rst index b19f27c460..de0ed7f659 100644 --- a/docs/sdk/metrics.point.rst +++ b/docs/sdk/metrics.point.rst @@ -4,4 +4,4 @@ opentelemetry.sdk._metrics.point .. automodule:: opentelemetry.sdk._metrics.point :members: :undoc-members: - :show-inheritance: + :no-show-inheritance: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index ec6497e9c9..76accd205b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -419,6 +419,19 @@ def _convert_aggregation_temporality( class ExplicitBucketHistogramAggregation(_AggregationFactory): + """This aggregation informs the SDK to collect: + + - Count of Measurement values falling within explicit bucket boundaries. + - Arithmetic sum of Measurement values in population. This SHOULD NOT be collected when used with instruments that record negative measurements, e.g. UpDownCounter or ObservableGauge. + - Min (optional) Measurement value in population. + - Max (optional) Measurement value in population. + + + Args: + boundaries: Array of increasing values representing explicit bucket boundary values. + record_min_max: Whether to record min and max. + """ + def __init__( self, boundaries: Sequence[float] = ( @@ -446,6 +459,11 @@ def _create_aggregation(self, instrument: Instrument) -> _Aggregation: class SumAggregation(_AggregationFactory): + """This aggregation informs the SDK to collect: + + - The arithmetic sum of Measurement values. + """ + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: temporality = AggregationTemporality.UNSPECIFIED @@ -461,6 +479,13 @@ def _create_aggregation(self, instrument: Instrument) -> _Aggregation: class LastValueAggregation(_AggregationFactory): + """ + This aggregation informs the SDK to collect: + + - The last Measurement. + - The timestamp of the last Measurement. + """ + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: return _LastValueAggregation() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py index 3417c197e6..e08140c33e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py @@ -63,7 +63,7 @@ class Metric: """Represents a metric point in the OpenTelemetry data model to be exported Concrete metric types contain all the information as in the OTLP proto definitions - (https://tinyurl.com/7h6yx24v) but are flattened as much as possible. + (https://github.com/open-telemetry/opentelemetry-proto/blob/b43e9b18b76abf3ee040164b55b9c355217151f3/opentelemetry/proto/metrics/v1/metrics.proto#L37) but are flattened as much as possible. """ # common fields to all metric kinds From acaef9675e5e816f387c1eb2fa3c5c15581fc6ff Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 22 Apr 2022 02:20:59 +0530 Subject: [PATCH 1200/1517] Release/1.11.1 0.30b1 (#2634) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 6 +++++- eachdist.ini | 4 ++-- exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg | 2 +- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-json/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/setup.cfg | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 34 files changed, 46 insertions(+), 42 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d808868a14..2c3fea117d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: de897daa279f6c4aa4572614d0bead2146097068 + CONTRIB_REPO_SHA: 4cfca481f8e2c8af5f7cfd032997fac692995f67 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 412ff0f845..c070efc9ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.0-0.30b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.1-0.30b1...HEAD) + +## [1.11.1-0.30b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.1-0.30b1) - 2022-04-21 + + - Fix unhandled callback exceptions on async instruments ([#2614](https://github.com/open-telemetry/opentelemetry-python/pull/2614)) diff --git a/eachdist.ini b/eachdist.ini index a49edda125..7b0af1484f 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.11.0 +version=1.11.1 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.30b0 +version=0.30b1 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index dc4ff16c24..2bc9200fc3 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -44,7 +44,7 @@ install_requires = grpcio >= 1.0.0, < 2.0.0 googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.3 + opentelemetry-sdk ~= 1.11 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 45c8d2c9fc..2324ecd18a 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index 7885c053a9..8f55cd34d4 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -43,7 +43,7 @@ packages=find_namespace: install_requires = thrift >= 0.10.0 opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.3 + opentelemetry-sdk ~= 1.11 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 45c8d2c9fc..2324ecd18a 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 94b1f34619..a8df93626b 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.11.0 - opentelemetry-exporter-jaeger-thrift == 1.11.0 + opentelemetry-exporter-jaeger-proto-grpc == 1.11.1 + opentelemetry-exporter-jaeger-thrift == 1.11.1 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 45c8d2c9fc..2324ecd18a 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 75e49aa583..88015aae34 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.30b0" +__version__ = "0.30b1" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 5361ce1e0a..71f07ea4e4 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 - opentelemetry-proto == 1.11.0 + opentelemetry-proto == 1.11.1 backoff >= 1.10.0, < 2.0.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index e9b0f0343e..ded7b2039b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index 50e3c14b54..500c5729e2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -43,8 +43,8 @@ install_requires = requests ~= 2.7 googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.11.0 + opentelemetry-sdk ~= 1.11 + opentelemetry-proto == 1.11.1 backoff >= 1.10.0, < 2.0.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index e9b0f0343e..ded7b2039b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index bf4ff52bdd..12be1ec1bd 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.11.0 - opentelemetry-exporter-otlp-proto-http == 1.11.0 + opentelemetry-exporter-otlp-proto-grpc == 1.11.1 + opentelemetry-exporter-otlp-proto-http == 1.11.1 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index e9b0f0343e..ded7b2039b 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 75e49aa583..88015aae34 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.30b0" +__version__ = "0.30b1" diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index 09f881e8dc..8e86fac255 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -43,7 +43,7 @@ packages=find_namespace: install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.3 + opentelemetry-sdk ~= 1.11 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index e9b0f0343e..ded7b2039b 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index fe7c40a3b1..9fbfc74686 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -44,8 +44,8 @@ install_requires = protobuf >= 3.12 requests ~= 2.7 opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.11.0 + opentelemetry-sdk ~= 1.11 + opentelemetry-exporter-zipkin-json == 1.11.1 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index e9b0f0343e..ded7b2039b 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 46a0d21d0b..3a8074e83a 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.11.0 - opentelemetry-exporter-zipkin-proto-http == 1.11.0 + opentelemetry-exporter-zipkin-json == 1.11.1 + opentelemetry-exporter-zipkin-proto-http == 1.11.1 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index e9b0f0343e..ded7b2039b 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index e9b0f0343e..ded7b2039b 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index e9b0f0343e..ded7b2039b 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 0805d89618..c25015eae8 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,8 +43,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.11.0 - opentelemetry-semantic-conventions == 0.30b0 + opentelemetry-api == 1.11.1 + opentelemetry-semantic-conventions == 0.30b1 setuptools >= 16.0 dataclasses == 0.8; python_version < '3.7' typing-extensions >= 3.7.4 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index e9b0f0343e..ded7b2039b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 75e49aa583..88015aae34 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.30b0" +__version__ = "0.30b1" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index e9b0f0343e..ded7b2039b 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index e9b0f0343e..ded7b2039b 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 2153a81a5c..533d206b7b 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -47,7 +47,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test-utils == 0.30b0 + opentelemetry-test-utils == 0.30b1 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 75e49aa583..88015aae34 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.30b0" +__version__ = "0.30b1" diff --git a/tests/opentelemetry-test-utils/setup.cfg b/tests/opentelemetry-test-utils/setup.cfg index 75bd316a27..c99951b27d 100644 --- a/tests/opentelemetry-test-utils/setup.cfg +++ b/tests/opentelemetry-test-utils/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.11.0 - opentelemetry-sdk == 1.11.0 + opentelemetry-api == 1.11.1 + opentelemetry-sdk == 1.11.1 asgiref ~= 3.0 [options.extras_require] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 903bf25a28..07903a7484 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.30b0" +__version__ = "0.30b1" From 0de0eab82f4b51c5a45d15bb61300fdc043f5acc Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 21 Apr 2022 18:35:06 -0600 Subject: [PATCH 1201/1517] Aggregate together data from identical instruments (#2603) * Aggregate together data from identical instruments Fixes #2557 * Remove duplicate test * Update boolean value * Update opentelemetry-api/src/opentelemetry/_metrics/__init__.py Co-authored-by: Alex Boten * Add sleep Co-authored-by: Alex Boten --- .../src/opentelemetry/_metrics/__init__.py | 40 ++++--- .../opentelemetry/sdk/_metrics/__init__.py | 101 +++++++++++++++--- .../tests/metrics/test_metrics.py | 97 ++++++++++++++++- 3 files changed, 205 insertions(+), 33 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index 30b9230be5..584e3cb5d7 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -46,7 +46,7 @@ from logging import getLogger from os import environ from threading import Lock -from typing import List, Optional, Set, cast +from typing import List, Optional, Set, Tuple, cast from opentelemetry._metrics.instrument import ( Counter, @@ -197,27 +197,31 @@ def schema_url(self): """ return self._schema_url - def _check_instrument_id( + def _is_instrument_registered( self, name: str, type_: type, unit: str, description: str - ) -> bool: + ) -> Tuple[bool, str]: """ Check if an instrument with the same name, type, unit and description has been registered already. - """ - result = False + Returns a tuple. The first value is `True` if the instrument has been + registered already, `False` otherwise. The second value is the + instrument id. + """ instrument_id = ",".join( [name.strip().lower(), type_.__name__, unit, description] ) + result = False + with self._instrument_ids_lock: if instrument_id in self._instrument_ids: result = True else: self._instrument_ids.add(instrument_id) - return result + return (result, instrument_id) @abstractmethod def create_counter( @@ -489,7 +493,9 @@ class NoOpMeter(Meter): def create_counter(self, name, unit="", description="") -> Counter: """Returns a no-op Counter.""" super().create_counter(name, unit=unit, description=description) - if self._check_instrument_id(name, NoOpCounter, unit, description): + if self._is_instrument_registered( + name, NoOpCounter, unit, description + )[0]: _logger.warning( "An instrument with name %s, type %s, unit %s and " "description %s has been created already.", @@ -507,9 +513,9 @@ def create_up_down_counter( super().create_up_down_counter( name, unit=unit, description=description ) - if self._check_instrument_id( + if self._is_instrument_registered( name, NoOpUpDownCounter, unit, description - ): + )[0]: _logger.warning( "An instrument with name %s, type %s, unit %s and " "description %s has been created already.", @@ -527,9 +533,9 @@ def create_observable_counter( super().create_observable_counter( name, callbacks, unit=unit, description=description ) - if self._check_instrument_id( + if self._is_instrument_registered( name, NoOpObservableCounter, unit, description - ): + )[0]: _logger.warning( "An instrument with name %s, type %s, unit %s and " "description %s has been created already.", @@ -548,7 +554,9 @@ def create_observable_counter( def create_histogram(self, name, unit="", description="") -> Histogram: """Returns a no-op Histogram.""" super().create_histogram(name, unit=unit, description=description) - if self._check_instrument_id(name, NoOpHistogram, unit, description): + if self._is_instrument_registered( + name, NoOpHistogram, unit, description + )[0]: _logger.warning( "An instrument with name %s, type %s, unit %s and " "description %s has been created already.", @@ -566,9 +574,9 @@ def create_observable_gauge( super().create_observable_gauge( name, callbacks, unit=unit, description=description ) - if self._check_instrument_id( + if self._is_instrument_registered( name, NoOpObservableGauge, unit, description - ): + )[0]: _logger.warning( "An instrument with name %s, type %s, unit %s and " "description %s has been created already.", @@ -591,9 +599,9 @@ def create_observable_up_down_counter( super().create_observable_up_down_counter( name, callbacks, unit=unit, description=description ) - if self._check_instrument_id( + if self._is_instrument_registered( name, NoOpObservableUpDownCounter, unit, description - ): + )[0]: _logger.warning( "An instrument with name %s, type %s, unit %s and " "description %s has been created already.", diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index afa445b359..efe77c02b5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -63,9 +63,17 @@ def __init__( super().__init__(instrumentation_scope) self._instrumentation_scope = instrumentation_scope self._measurement_consumer = measurement_consumer + self._instrument_id_instrument = {} + self._instrument_id_instrument_lock = Lock() def create_counter(self, name, unit="", description="") -> APICounter: - if self._check_instrument_id(name, Counter, unit, description): + + ( + is_instrument_registered, + instrument_id, + ) = self._is_instrument_registered(name, Counter, unit, description) + + if is_instrument_registered: # FIXME #2558 go through all views here and check if this # instrument registration conflict can be fixed. If it can be, do # not log the following warning. @@ -77,8 +85,10 @@ def create_counter(self, name, unit="", description="") -> APICounter: unit, description, ) + with self._instrument_id_instrument_lock: + return self._instrument_id_instrument[instrument_id] - return Counter( + instrument = Counter( name, self._instrumentation_scope, self._measurement_consumer, @@ -86,10 +96,22 @@ def create_counter(self, name, unit="", description="") -> APICounter: description, ) + with self._instrument_id_instrument_lock: + self._instrument_id_instrument[instrument_id] = instrument + return instrument + def create_up_down_counter( self, name, unit="", description="" ) -> APIUpDownCounter: - if self._check_instrument_id(name, UpDownCounter, unit, description): + + ( + is_instrument_registered, + instrument_id, + ) = self._is_instrument_registered( + name, UpDownCounter, unit, description + ) + + if is_instrument_registered: # FIXME #2558 go through all views here and check if this # instrument registration conflict can be fixed. If it can be, do # not log the following warning. @@ -101,8 +123,10 @@ def create_up_down_counter( unit, description, ) + with self._instrument_id_instrument_lock: + return self._instrument_id_instrument[instrument_id] - return UpDownCounter( + instrument = UpDownCounter( name, self._instrumentation_scope, self._measurement_consumer, @@ -110,12 +134,22 @@ def create_up_down_counter( description, ) + with self._instrument_id_instrument_lock: + self._instrument_id_instrument[instrument_id] = instrument + return instrument + def create_observable_counter( self, name, callbacks=None, unit="", description="" ) -> APIObservableCounter: - if self._check_instrument_id( + + ( + is_instrument_registered, + instrument_id, + ) = self._is_instrument_registered( name, ObservableCounter, unit, description - ): + ) + + if is_instrument_registered: # FIXME #2558 go through all views here and check if this # instrument registration conflict can be fixed. If it can be, do # not log the following warning. @@ -127,6 +161,9 @@ def create_observable_counter( unit, description, ) + with self._instrument_id_instrument_lock: + return self._instrument_id_instrument[instrument_id] + instrument = ObservableCounter( name, self._instrumentation_scope, @@ -138,10 +175,18 @@ def create_observable_counter( self._measurement_consumer.register_asynchronous_instrument(instrument) - return instrument + with self._instrument_id_instrument_lock: + self._instrument_id_instrument[instrument_id] = instrument + return instrument def create_histogram(self, name, unit="", description="") -> APIHistogram: - if self._check_instrument_id(name, Histogram, unit, description): + + ( + is_instrument_registered, + instrument_id, + ) = self._is_instrument_registered(name, Histogram, unit, description) + + if is_instrument_registered: # FIXME #2558 go through all views here and check if this # instrument registration conflict can be fixed. If it can be, do # not log the following warning. @@ -153,18 +198,32 @@ def create_histogram(self, name, unit="", description="") -> APIHistogram: unit, description, ) - return Histogram( + with self._instrument_id_instrument_lock: + return self._instrument_id_instrument[instrument_id] + + instrument = Histogram( name, self._instrumentation_scope, self._measurement_consumer, unit, description, ) + with self._instrument_id_instrument_lock: + self._instrument_id_instrument[instrument_id] = instrument + return instrument def create_observable_gauge( self, name, callbacks=None, unit="", description="" ) -> APIObservableGauge: - if self._check_instrument_id(name, ObservableGauge, unit, description): + + ( + is_instrument_registered, + instrument_id, + ) = self._is_instrument_registered( + name, ObservableGauge, unit, description + ) + + if is_instrument_registered: # FIXME #2558 go through all views here and check if this # instrument registration conflict can be fixed. If it can be, do # not log the following warning. @@ -176,6 +235,8 @@ def create_observable_gauge( unit, description, ) + with self._instrument_id_instrument_lock: + return self._instrument_id_instrument[instrument_id] instrument = ObservableGauge( name, @@ -188,14 +249,20 @@ def create_observable_gauge( self._measurement_consumer.register_asynchronous_instrument(instrument) - return instrument + with self._instrument_id_instrument_lock: + self._instrument_id_instrument[instrument_id] = instrument + return instrument def create_observable_up_down_counter( self, name, callbacks=None, unit="", description="" ) -> APIObservableUpDownCounter: - if self._check_instrument_id( - name, ObservableUpDownCounter, unit, description - ): + + ( + is_instrument_registered, + instrument_id, + ) = self._is_instrument_registered(name, Counter, unit, description) + + if is_instrument_registered: # FIXME #2558 go through all views here and check if this # instrument registration conflict can be fixed. If it can be, do # not log the following warning. @@ -207,6 +274,8 @@ def create_observable_up_down_counter( unit, description, ) + with self._instrument_id_instrument_lock: + return self._instrument_id_instrument[instrument_id] instrument = ObservableUpDownCounter( name, @@ -219,7 +288,9 @@ def create_observable_up_down_counter( self._measurement_consumer.register_asynchronous_instrument(instrument) - return instrument + with self._instrument_id_instrument_lock: + self._instrument_id_instrument[instrument_id] = instrument + return instrument class MeterProvider(APIMeterProvider): diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index f4f31a6294..0279dfbe61 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -14,12 +14,19 @@ from logging import WARNING +from time import sleep +from typing import Sequence from unittest import TestCase from unittest.mock import MagicMock, Mock, patch from opentelemetry._metrics import NoOpMeter from opentelemetry.sdk._metrics import Meter, MeterProvider -from opentelemetry.sdk._metrics.export import PeriodicExportingMetricReader +from opentelemetry.sdk._metrics.aggregation import SumAggregation +from opentelemetry.sdk._metrics.export import ( + MetricExporter, + MetricExportResult, + PeriodicExportingMetricReader, +) from opentelemetry.sdk._metrics.instrument import ( Counter, Histogram, @@ -29,7 +36,8 @@ UpDownCounter, ) from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.point import AggregationTemporality +from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric +from opentelemetry.sdk._metrics.view import View from opentelemetry.sdk.resources import Resource from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc @@ -400,3 +408,88 @@ def test_create_observable_up_down_counter(self): observable_up_down_counter, ObservableUpDownCounter ) self.assertEqual(observable_up_down_counter.name, "name") + + +class InMemoryMetricExporter(MetricExporter): + def __init__(self): + self.metrics = {} + self._counter = 0 + + def export(self, metrics: Sequence[Metric]) -> MetricExportResult: + self.metrics[self._counter] = metrics + self._counter += 1 + return MetricExportResult.SUCCESS + + def shutdown(self) -> None: + pass + + +class TestDuplicateInstrumentAggregateData(TestCase): + def test_duplicate_instrument_aggregate_data(self): + + exporter = InMemoryMetricExporter() + reader = PeriodicExportingMetricReader( + exporter, export_interval_millis=500 + ) + view = View( + instrument_type=Counter, + attribute_keys=[], + aggregation=SumAggregation(), + ) + provider = MeterProvider( + metric_readers=[reader], + resource=Resource.create(), + views=[view], + ) + + meter_0 = provider.get_meter( + name="meter_0", + version="version", + schema_url="schema_url", + ) + meter_1 = provider.get_meter( + name="meter_1", + version="version", + schema_url="schema_url", + ) + counter_0_0 = meter_0.create_counter( + "counter", unit="unit", description="description" + ) + counter_0_1 = meter_0.create_counter( + "counter", unit="unit", description="description" + ) + counter_1_0 = meter_1.create_counter( + "counter", unit="unit", description="description" + ) + + self.assertIs(counter_0_0, counter_0_1) + self.assertIsNot(counter_0_0, counter_1_0) + + counter_0_0.add(1, {}) + counter_0_1.add(2, {}) + + counter_1_0.add(7, {}) + + sleep(1) + + reader.shutdown() + + sleep(1) + + metrics = exporter.metrics[0] + + self.assertEqual(len(metrics), 2) + + metric_0 = metrics[0] + + self.assertEqual(metric_0.name, "counter") + self.assertEqual(metric_0.unit, "unit") + self.assertEqual(metric_0.description, "description") + self.assertEqual(metric_0.point.value, 3) + + metric_1 = metrics[1] + + self.assertEqual(metric_1.name, "counter") + self.assertEqual(metric_1.unit, "unit") + self.assertEqual(metric_1.description, "description") + self.assertEqual(metric_1.point.value, 7) From 24ac96eddffbfd0019c0a186ce073b40c552047f Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Sat, 23 Apr 2022 05:08:14 +0530 Subject: [PATCH 1202/1517] Run mypy checks on Metrics API (#2335) * Run mypy checks on sdk * Add more api type * Update callbacks * Fix conflicts * Fix incorrect comparision * Fix lint * Fix lint --- .../src/opentelemetry/_metrics/__init__.py | 165 +++++++++++++----- .../src/opentelemetry/_metrics/instrument.py | 134 +++++++++++--- .../src/opentelemetry/util/_providers.py | 5 +- .../opentelemetry/sdk/util/instrumentation.py | 24 +-- tox.ini | 1 + 5 files changed, 253 insertions(+), 76 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index 584e3cb5d7..6447135743 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -13,7 +13,6 @@ # limitations under the License. # pylint: disable=too-many-ancestors -# type: ignore """ The OpenTelemetry metrics API describes the classes used to generate @@ -46,9 +45,10 @@ from logging import getLogger from os import environ from threading import Lock -from typing import List, Optional, Set, Tuple, cast +from typing import List, Optional, Sequence, Set, Tuple, Union, cast from opentelemetry._metrics.instrument import ( + CallbackT, Counter, Histogram, NoOpCounter, @@ -63,7 +63,6 @@ UpDownCounter, _ProxyCounter, _ProxyHistogram, - _ProxyInstrument, _ProxyObservableCounter, _ProxyObservableGauge, _ProxyObservableUpDownCounter, @@ -78,6 +77,16 @@ _logger = getLogger(__name__) +ProxyInstrumentT = Union[ + _ProxyCounter, + _ProxyHistogram, + _ProxyObservableCounter, + _ProxyObservableGauge, + _ProxyObservableUpDownCounter, + _ProxyUpDownCounter, +] + + class MeterProvider(ABC): """ MeterProvider is the entry point of the API. It provides access to `Meter` instances. @@ -87,8 +96,8 @@ class MeterProvider(ABC): def get_meter( self, name: str, - version: str = None, - schema_url: str = None, + version: Optional[str] = None, + schema_url: Optional[str] = None, ) -> "Meter": """Returns a `Meter` for use by the given instrumentation library. @@ -123,9 +132,9 @@ class NoOpMeterProvider(MeterProvider): def get_meter( self, - name, - version=None, - schema_url=None, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, ) -> "Meter": """Returns a NoOpMeter.""" super().get_meter(name, version=version, schema_url=schema_url) @@ -140,9 +149,9 @@ def __init__(self) -> None: def get_meter( self, - name, - version=None, - schema_url=None, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, ) -> "Meter": with self._lock: if self._real_meter_provider is not None: @@ -168,7 +177,12 @@ class Meter(ABC): used to produce measurements. """ - def __init__(self, name: str, version: str = None, schema_url: str = None): + def __init__( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> None: super().__init__() self._name = name self._version = version @@ -177,21 +191,21 @@ def __init__(self, name: str, version: str = None, schema_url: str = None): self._instrument_ids_lock = Lock() @property - def name(self): + def name(self) -> str: """ The name of the instrumenting module. """ return self._name @property - def version(self): + def version(self) -> Optional[str]: """ The version string of the instrumenting library. """ return self._version @property - def schema_url(self): + def schema_url(self) -> Optional[str]: """ Specifies the Schema URL of the emitted telemetry """ @@ -225,7 +239,10 @@ def _is_instrument_registered( @abstractmethod def create_counter( - self, name: str, unit: str = "", description: str = "" + self, + name: str, + unit: str = "", + description: str = "", ) -> Counter: """Creates a `Counter` instrument @@ -238,7 +255,10 @@ def create_counter( @abstractmethod def create_up_down_counter( - self, name: str, unit: str = "", description: str = "" + self, + name: str, + unit: str = "", + description: str = "", ) -> UpDownCounter: """Creates an `UpDownCounter` instrument @@ -251,7 +271,11 @@ def create_up_down_counter( @abstractmethod def create_observable_counter( - self, name, callbacks=None, unit="", description="" + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", ) -> ObservableCounter: """Creates an `ObservableCounter` instrument @@ -334,7 +358,12 @@ def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observat """ @abstractmethod - def create_histogram(self, name, unit="", description="") -> Histogram: + def create_histogram( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Histogram: """Creates a `opentelemetry._metrics.instrument.Histogram` instrument Args: @@ -346,7 +375,11 @@ def create_histogram(self, name, unit="", description="") -> Histogram: @abstractmethod def create_observable_gauge( - self, name, callbacks=None, unit="", description="" + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", ) -> ObservableGauge: """Creates an `ObservableGauge` instrument @@ -363,7 +396,11 @@ def create_observable_gauge( @abstractmethod def create_observable_up_down_counter( - self, name, callbacks=None, unit="", description="" + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", ) -> ObservableUpDownCounter: """Creates an `ObservableUpDownCounter` instrument @@ -382,13 +419,13 @@ def create_observable_up_down_counter( class _ProxyMeter(Meter): def __init__( self, - name, - version=None, - schema_url=None, - ): + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> None: super().__init__(name, version=version, schema_url=schema_url) self._lock = Lock() - self._instruments: List[_ProxyInstrument] = [] + self._instruments: List[ProxyInstrumentT] = [] self._real_meter: Optional[Meter] = None def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: @@ -408,7 +445,12 @@ def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: for instrument in self._instruments: instrument.on_meter_set(real_meter) - def create_counter(self, name, unit="", description="") -> Counter: + def create_counter( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Counter: with self._lock: if self._real_meter: return self._real_meter.create_counter(name, unit, description) @@ -417,7 +459,10 @@ def create_counter(self, name, unit="", description="") -> Counter: return proxy def create_up_down_counter( - self, name, unit="", description="" + self, + name: str, + unit: str = "", + description: str = "", ) -> UpDownCounter: with self._lock: if self._real_meter: @@ -429,7 +474,11 @@ def create_up_down_counter( return proxy def create_observable_counter( - self, name, callbacks=None, unit="", description="" + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", ) -> ObservableCounter: with self._lock: if self._real_meter: @@ -442,7 +491,12 @@ def create_observable_counter( self._instruments.append(proxy) return proxy - def create_histogram(self, name, unit="", description="") -> Histogram: + def create_histogram( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Histogram: with self._lock: if self._real_meter: return self._real_meter.create_histogram( @@ -453,7 +507,11 @@ def create_histogram(self, name, unit="", description="") -> Histogram: return proxy def create_observable_gauge( - self, name, callbacks=None, unit="", description="" + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", ) -> ObservableGauge: with self._lock: if self._real_meter: @@ -467,7 +525,11 @@ def create_observable_gauge( return proxy def create_observable_up_down_counter( - self, name, callbacks=None, unit="", description="" + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", ) -> ObservableUpDownCounter: with self._lock: if self._real_meter: @@ -490,7 +552,12 @@ class NoOpMeter(Meter): All operations are no-op. """ - def create_counter(self, name, unit="", description="") -> Counter: + def create_counter( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Counter: """Returns a no-op Counter.""" super().create_counter(name, unit=unit, description=description) if self._is_instrument_registered( @@ -507,7 +574,10 @@ def create_counter(self, name, unit="", description="") -> Counter: return NoOpCounter(name, unit=unit, description=description) def create_up_down_counter( - self, name, unit="", description="" + self, + name: str, + unit: str = "", + description: str = "", ) -> UpDownCounter: """Returns a no-op UpDownCounter.""" super().create_up_down_counter( @@ -527,7 +597,11 @@ def create_up_down_counter( return NoOpUpDownCounter(name, unit=unit, description=description) def create_observable_counter( - self, name, callbacks=None, unit="", description="" + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", ) -> ObservableCounter: """Returns a no-op ObservableCounter.""" super().create_observable_counter( @@ -551,7 +625,12 @@ def create_observable_counter( description=description, ) - def create_histogram(self, name, unit="", description="") -> Histogram: + def create_histogram( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Histogram: """Returns a no-op Histogram.""" super().create_histogram(name, unit=unit, description=description) if self._is_instrument_registered( @@ -568,7 +647,11 @@ def create_histogram(self, name, unit="", description="") -> Histogram: return NoOpHistogram(name, unit=unit, description=description) def create_observable_gauge( - self, name, callbacks=None, unit="", description="" + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", ) -> ObservableGauge: """Returns a no-op ObservableGauge.""" super().create_observable_gauge( @@ -593,7 +676,11 @@ def create_observable_gauge( ) def create_observable_up_down_counter( - self, name, callbacks=None, unit="", description="" + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", ) -> ObservableUpDownCounter: """Returns a no-op ObservableUpDownCounter.""" super().create_observable_up_down_counter( @@ -670,7 +757,7 @@ def get_meter_provider() -> MeterProvider: if OTEL_PYTHON_METER_PROVIDER not in environ.keys(): return _PROXY_METER_PROVIDER - meter_provider: MeterProvider = _load_provider( + meter_provider: MeterProvider = _load_provider( # type: ignore OTEL_PYTHON_METER_PROVIDER, "meter_provider" ) _set_meter_provider(meter_provider, log=False) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py index d79b0d9aa6..1a855621f4 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py @@ -14,7 +14,6 @@ # pylint: disable=too-many-ancestors -# type: ignore from abc import ABC, abstractmethod from logging import getLogger @@ -24,6 +23,7 @@ Generic, Iterable, Optional, + Sequence, TypeVar, Union, ) @@ -31,6 +31,7 @@ # pylint: disable=unused-import; needed for typing and sphinx from opentelemetry import _metrics as metrics from opentelemetry._metrics.observation import Observation +from opentelemetry.util.types import Attributes InstrumentT = TypeVar("InstrumentT", bound="Instrument") CallbackT = Union[ @@ -46,7 +47,12 @@ class Instrument(ABC): """Abstract class that serves as base for all instruments.""" @abstractmethod - def __init__(self, name, unit="", description=""): + def __init__( + self, + name: str, + unit: str = "", + description: str = "", + ) -> None: pass # FIXME check that the instrument name is valid @@ -55,7 +61,12 @@ def __init__(self, name, unit="", description=""): class _ProxyInstrument(ABC, Generic[InstrumentT]): - def __init__(self, name, unit, description) -> None: + def __init__( + self, + name: str, + unit: str = "", + description: str = "", + ) -> None: self._name = name self._unit = unit self._description = description @@ -75,7 +86,13 @@ def _create_real_instrument(self, meter: "metrics.Meter") -> InstrumentT: class _ProxyAsynchronousInstrument(_ProxyInstrument[InstrumentT]): - def __init__(self, name, callbacks, unit, description) -> None: + def __init__( + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> None: super().__init__(name, unit, description) self._callbacks = callbacks @@ -88,11 +105,11 @@ class Asynchronous(Instrument): @abstractmethod def __init__( self, - name, - callbacks=None, - unit="", - description="", - ): + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> None: super().__init__(name, unit=unit, description=description) @@ -116,7 +133,11 @@ class Counter(_Monotonic, Synchronous): """A Counter is a synchronous `Instrument` which supports non-negative increments.""" @abstractmethod - def add(self, amount, attributes=None): + def add( + self, + amount: Union[int, float], + attributes: Optional[Attributes] = None, + ) -> None: # FIXME check that the amount is non negative pass @@ -124,15 +145,28 @@ def add(self, amount, attributes=None): class NoOpCounter(Counter): """No-op implementation of `Counter`.""" - def __init__(self, name, unit="", description=""): + def __init__( + self, + name: str, + unit: str = "", + description: str = "", + ) -> None: super().__init__(name, unit=unit, description=description) - def add(self, amount, attributes=None): + def add( + self, + amount: Union[int, float], + attributes: Optional[Attributes] = None, + ) -> None: return super().add(amount, attributes=attributes) class _ProxyCounter(_ProxyInstrument[Counter], Counter): - def add(self, amount, attributes=None): + def add( + self, + amount: Union[int, float], + attributes: Optional[Attributes] = None, + ) -> None: if self._real_instrument: self._real_instrument.add(amount, attributes) @@ -144,22 +178,39 @@ class UpDownCounter(_NonMonotonic, Synchronous): """An UpDownCounter is a synchronous `Instrument` which supports increments and decrements.""" @abstractmethod - def add(self, amount, attributes=None): + def add( + self, + amount: Union[int, float], + attributes: Optional[Attributes] = None, + ) -> None: pass class NoOpUpDownCounter(UpDownCounter): """No-op implementation of `UpDownCounter`.""" - def __init__(self, name, unit="", description=""): + def __init__( + self, + name: str, + unit: str = "", + description: str = "", + ) -> None: super().__init__(name, unit=unit, description=description) - def add(self, amount, attributes=None): + def add( + self, + amount: Union[int, float], + attributes: Optional[Attributes] = None, + ) -> None: return super().add(amount, attributes=attributes) class _ProxyUpDownCounter(_ProxyInstrument[UpDownCounter], UpDownCounter): - def add(self, amount, attributes=None): + def add( + self, + amount: Union[int, float], + attributes: Optional[Attributes] = None, + ) -> None: if self._real_instrument: self._real_instrument.add(amount, attributes) @@ -178,7 +229,13 @@ class ObservableCounter(_Monotonic, Asynchronous): class NoOpObservableCounter(ObservableCounter): """No-op implementation of `ObservableCounter`.""" - def __init__(self, name, callbacks=None, unit="", description=""): + def __init__( + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> None: super().__init__(name, callbacks, unit=unit, description=description) @@ -203,7 +260,13 @@ class ObservableUpDownCounter(_NonMonotonic, Asynchronous): class NoOpObservableUpDownCounter(ObservableUpDownCounter): """No-op implementation of `ObservableUpDownCounter`.""" - def __init__(self, name, callbacks=None, unit="", description=""): + def __init__( + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> None: super().__init__(name, callbacks, unit=unit, description=description) @@ -226,22 +289,39 @@ class Histogram(_Grouping, Synchronous): """ @abstractmethod - def record(self, amount, attributes=None): + def record( + self, + amount: Union[int, float], + attributes: Optional[Attributes] = None, + ) -> None: pass class NoOpHistogram(Histogram): """No-op implementation of `Histogram`.""" - def __init__(self, name, unit="", description=""): + def __init__( + self, + name: str, + unit: str = "", + description: str = "", + ) -> None: super().__init__(name, unit=unit, description=description) - def record(self, amount, attributes=None): + def record( + self, + amount: Union[int, float], + attributes: Optional[Attributes] = None, + ) -> None: return super().record(amount, attributes=attributes) class _ProxyHistogram(_ProxyInstrument[Histogram], Histogram): - def record(self, amount, attributes=None): + def record( + self, + amount: Union[int, float], + attributes: Optional[Attributes] = None, + ) -> None: if self._real_instrument: self._real_instrument.record(amount, attributes) @@ -261,7 +341,13 @@ class ObservableGauge(_Grouping, Asynchronous): class NoOpObservableGauge(ObservableGauge): """No-op implementation of `ObservableGauge`.""" - def __init__(self, name, callbacks=None, unit="", description=""): + def __init__( + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> None: super().__init__(name, callbacks, unit=unit, description=description) diff --git a/opentelemetry-api/src/opentelemetry/util/_providers.py b/opentelemetry-api/src/opentelemetry/util/_providers.py index 98c75ed06b..f19c32ee86 100644 --- a/opentelemetry-api/src/opentelemetry/util/_providers.py +++ b/opentelemetry-api/src/opentelemetry/util/_providers.py @@ -14,14 +14,15 @@ from logging import getLogger from os import environ -from typing import TYPE_CHECKING, Union, cast +from typing import TYPE_CHECKING, TypeVar, cast from pkg_resources import iter_entry_points if TYPE_CHECKING: + from opentelemetry._metrics import MeterProvider from opentelemetry.trace import TracerProvider -Provider = Union["TracerProvider"] +Provider = TypeVar("Provider", "TracerProvider", "MeterProvider") logger = getLogger(__name__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index 1b2c1c74a7..a489c207a0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -86,26 +86,28 @@ def __init__( name: str, version: typing.Optional[str] = None, schema_url: typing.Optional[str] = None, - ): + ) -> None: self._name = name self._version = version self._schema_url = schema_url - def __repr__(self): + def __repr__(self) -> str: return f"{type(self).__name__}({self._name}, {self._version}, {self._schema_url})" - def __hash__(self): + def __hash__(self) -> int: return hash((self._name, self._version, self._schema_url)) - def __eq__(self, value): - return type(value) is type(self) and ( - self._name, - self._version, - self._schema_url, - ) == (value._name, value._version, value._schema_url) + def __eq__(self, value: object) -> bool: + if not isinstance(value, InstrumentationScope): + return NotImplemented + return (self._name, self._version, self._schema_url) == ( + value._name, + value._version, + value._schema_url, + ) - def __lt__(self, value): - if type(value) is not type(self): + def __lt__(self, value: object) -> bool: + if not isinstance(value, InstrumentationScope): return NotImplemented return (self._name, self._version, self._schema_url) < ( value._name, diff --git a/tox.ini b/tox.ini index e3a1c6e1a3..73df1f6928 100644 --- a/tox.ini +++ b/tox.ini @@ -172,6 +172,7 @@ commands = coverage: {toxinidir}/scripts/coverage.sh mypy: mypy --install-types --non-interactive --namespace-packages --explicit-package-bases opentelemetry-api/src/opentelemetry/ + ; For test code, we don't want to enforce the full mypy strictness mypy: mypy --install-types --non-interactive --namespace-packages --config-file=mypy-relaxed.ini opentelemetry-api/tests/ From ebdc1011ed9f4e783fcaee12303bdce7611a328c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 26 Apr 2022 11:35:17 -0600 Subject: [PATCH 1203/1517] Add more documentation (#2641) Fixes #2129 --- .../src/opentelemetry/_metrics/instrument.py | 4 +++- .../src/opentelemetry/sdk/_metrics/__init__.py | 2 ++ .../sdk/_metrics/export/__init__.py | 4 ++++ .../opentelemetry/sdk/_metrics/measurement.py | 4 ++++ .../src/opentelemetry/sdk/_metrics/point.py | 16 ++++++++++++++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py index 1a855621f4..ed70f2ddc7 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py @@ -98,10 +98,12 @@ def __init__( class Synchronous(Instrument): - pass + """Base class for all synchronous instruments""" class Asynchronous(Instrument): + """Base class for all asynchronous instruments""" + @abstractmethod def __init__( self, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index efe77c02b5..886b07aead 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -55,6 +55,8 @@ class Meter(APIMeter): + """See `opentelemetry._metrics.Meter`.""" + def __init__( self, instrumentation_scope: InstrumentationScope, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py index ff505ed4df..88171f975f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -35,6 +35,10 @@ class MetricExportResult(Enum): + """Result of exporting a metric + + Can be any of the following values:""" + SUCCESS = 0 FAILURE = 1 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py index 7ed6d13c1f..730d1509d0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py @@ -23,6 +23,10 @@ @dataclass(frozen=True) class Measurement: + """ + Represents a data point reported via the metrics API to the SDK. + """ + value: Union[int, float] instrument: "_Instrument" attributes: Attributes = None diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py index e08140c33e..6d55858a63 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py @@ -23,6 +23,12 @@ class AggregationTemporality(IntEnum): + """ + The temporality to use when aggregating data. + + Can be one of the following values: + """ + UNSPECIFIED = 0 DELTA = 1 CUMULATIVE = 2 @@ -30,6 +36,9 @@ class AggregationTemporality(IntEnum): @dataclass(frozen=True) class Sum: + """Represents the type of a scalar metric that is calculated as a sum of + all reported measurements over a time interval.""" + aggregation_temporality: AggregationTemporality is_monotonic: bool start_time_unix_nano: int @@ -39,12 +48,19 @@ class Sum: @dataclass(frozen=True) class Gauge: + """Represents the type of a scalar metric that always exports the current + value for every data point. It should be used for an unknown + aggregation.""" + time_unix_nano: int value: Union[int, float] @dataclass(frozen=True) class Histogram: + """Represents the type of a metric that is calculated by aggregating as a + histogram of all reported measurements over a time interval.""" + aggregation_temporality: AggregationTemporality bucket_counts: Sequence[int] explicit_bounds: Sequence[float] From 8f7094b23eb0bb996aa17e9373c8dc8456786529 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 26 Apr 2022 17:12:35 -0600 Subject: [PATCH 1204/1517] Convert label sequences to JSON in Prometheus exporter (#2642) * JSON Convert Values for prometheus collector * Add more fixes and a test case * Use Sequence instead * Add test case and separate function Co-authored-by: Arvind Mishra --- .../exporter/prometheus/__init__.py | 26 ++++++++----- .../tests/test_prometheus_exporter.py | 38 +++++++++++++++++++ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 62967eec83..59b5a424eb 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -62,18 +62,19 @@ --- """ -import collections -import logging -import re +from collections import deque from itertools import chain -from typing import Iterable, Optional, Sequence, Tuple +from json import dumps +from logging import getLogger +from re import IGNORECASE, UNICODE, compile +from typing import Iterable, Optional, Sequence, Tuple, Union from prometheus_client import core from opentelemetry.sdk._metrics.export import MetricReader from opentelemetry.sdk._metrics.point import Gauge, Histogram, Metric, Sum -_logger = logging.getLogger(__name__) +_logger = getLogger(__name__) def _convert_buckets(metric: Metric) -> Sequence[Tuple[str, int]]: @@ -123,9 +124,9 @@ class _CustomCollector: def __init__(self, prefix: str = ""): self._prefix = prefix self._callback = None - self._metrics_to_export = collections.deque() - self._non_letters_digits_underscore_re = re.compile( - r"[^\w]", re.UNICODE | re.IGNORECASE + self._metrics_to_export = deque() + self._non_letters_digits_underscore_re = compile( + r"[^\w]", UNICODE | IGNORECASE ) def add_metrics_data(self, export_records: Sequence[Metric]) -> None: @@ -157,7 +158,7 @@ def _translate_to_prometheus( label_keys = [] for key, value in metric.attributes.items(): label_keys.append(self._sanitize(key)) - label_values.append(str(value)) + label_values.append(self._check_value(value)) metric_name = "" if self._prefix != "": @@ -206,3 +207,10 @@ def _sanitize(self, key: str) -> str: Replace all characters other than [A-Za-z0-9_] with '_'. """ return self._non_letters_digits_underscore_re.sub("_", key) + + # pylint: disable=no-self-use + def _check_value(self, value: Union[int, float, str, Sequence]) -> str: + """Check the label value and return is appropriate representation""" + if not isinstance(value, str) and isinstance(value, Sequence): + return dumps(value, default=str) + return str(value) diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 445c9448ad..d734859dc3 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -167,3 +167,41 @@ def test_sanitize(self): self.assertEqual(collector._sanitize(",./?;:[]{}"), "__________") self.assertEqual(collector._sanitize("TestString"), "TestString") self.assertEqual(collector._sanitize("aAbBcC_12_oi"), "aAbBcC_12_oi") + + def test_list_labels(self): + labels = {"environment@": ["1", "2", "3"], "os": "Unix"} + record = _generate_gauge( + "test@gauge", + 123, + attributes=labels, + description="testdesc", + unit="testunit", + ) + collector = _CustomCollector("testprefix") + collector.add_metrics_data([record]) + + for prometheus_metric in collector.collect(): + self.assertEqual(type(prometheus_metric), GaugeMetricFamily) + self.assertEqual( + prometheus_metric.name, "testprefix_test_gauge_testunit" + ) + self.assertEqual(prometheus_metric.documentation, "testdesc") + self.assertTrue(len(prometheus_metric.samples) == 1) + self.assertEqual(prometheus_metric.samples[0].value, 123) + self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) + self.assertEqual( + prometheus_metric.samples[0].labels["environment_"], + '["1", "2", "3"]', + ) + self.assertEqual(prometheus_metric.samples[0].labels["os"], "Unix") + + def test_check_value(self): + + collector = _CustomCollector("") + + self.assertEqual(collector._check_value(1), "1") + self.assertEqual(collector._check_value(1.0), "1.0") + self.assertEqual(collector._check_value("a"), "a") + self.assertEqual(collector._check_value([1, 2]), "[1, 2]") + self.assertEqual(collector._check_value((1, 2)), "[1, 2]") + self.assertEqual(collector._check_value(["a", 2]), '["a", 2]') From 1a1421f8cfadb000fc89d1940a258ef7f0a4db63 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 26 Apr 2022 23:29:42 -0600 Subject: [PATCH 1205/1517] Use JSON formatting for all values except strings (#2644) --- .../src/opentelemetry/exporter/prometheus/__init__.py | 2 +- .../tests/test_prometheus_exporter.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 59b5a424eb..6e71395da5 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -211,6 +211,6 @@ def _sanitize(self, key: str) -> str: # pylint: disable=no-self-use def _check_value(self, value: Union[int, float, str, Sequence]) -> str: """Check the label value and return is appropriate representation""" - if not isinstance(value, str) and isinstance(value, Sequence): + if not isinstance(value, str): return dumps(value, default=str) return str(value) diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index d734859dc3..fd963b139a 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -205,3 +205,6 @@ def test_check_value(self): self.assertEqual(collector._check_value([1, 2]), "[1, 2]") self.assertEqual(collector._check_value((1, 2)), "[1, 2]") self.assertEqual(collector._check_value(["a", 2]), '["a", 2]') + self.assertEqual(collector._check_value(True), "true") + self.assertEqual(collector._check_value(False), "false") + self.assertEqual(collector._check_value(None), "null") From b0777a3c3affe3e99aecb599b19c97338dd08cee Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 27 Apr 2022 10:21:30 -0600 Subject: [PATCH 1206/1517] Implement MetricReader temporality controls (#2637) * Implement MetricReader temporality controls Fixes #2627 Fixes #2636 * Fix type * Add temporality check * Fix check * Fix check * Fix lint * Fix check * Fix docs * Add changelog * Update changelog message Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 3 +- .../sdk/_metrics/_view_instrument_match.py | 12 +- .../sdk/_metrics/export/__init__.py | 12 +- .../opentelemetry/sdk/_metrics/instrument.py | 12 +- .../sdk/_metrics/measurement_consumer.py | 14 +- .../sdk/_metrics/metric_reader.py | 85 ++++++++- .../sdk/_metrics/metric_reader_storage.py | 27 ++- .../sdk/environment_variables.py | 15 ++ .../tests/metrics/test_metric_reader.py | 175 ++++++++++++++++++ .../tests/metrics/test_metrics.py | 4 +- .../test_periodic_exporting_metric_reader.py | 6 +- .../metrics/test_view_instrument_match.py | 8 +- 12 files changed, 331 insertions(+), 42 deletions(-) create mode 100644 opentelemetry-sdk/tests/metrics/test_metric_reader.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c070efc9ab..644cfcdefa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.11.1-0.30b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.1-0.30b1) - 2022-04-21 - +- Add parameter to MetricReader constructor to select temporality per instrument kind + ([#2637](https://github.com/open-telemetry/opentelemetry-python/pull/2637)) - Fix unhandled callback exceptions on async instruments ([#2614](https://github.com/open-telemetry/opentelemetry-python/pull/2614)) - Rename `DefaultCounter`, `DefaultHistogram`, `DefaultObservableCounter`, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py index af1d9eb351..a75ce7d4c8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py @@ -75,7 +75,9 @@ def consume_measurement(self, measurement: Measurement) -> None: self._attributes_aggregation[attributes].aggregate(measurement) - def collect(self, temporality: int) -> Iterable[Metric]: + def collect( + self, instrument_class_temporality: Dict[type, AggregationTemporality] + ) -> Iterable[Metric]: with self._lock: for ( @@ -106,13 +108,17 @@ def collect(self, temporality: int) -> Iterable[Metric]: self._view._description or self._instrument.description ), - instrumentation_scope=self._instrument.instrumentation_scope, + instrumentation_scope=( + self._instrument.instrumentation_scope + ), name=self._view._name or self._instrument.name, resource=self._sdk_config.resource, unit=self._instrument.unit, point=_convert_aggregation_temporality( previous_point, current_point, - temporality, + instrument_class_temporality[ + self._instrument.__class__ + ], ), ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py index 88171f975f..a76b648959 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -19,7 +19,7 @@ from os import environ, linesep from sys import stdout from threading import Event, RLock, Thread -from typing import IO, Callable, Iterable, List, Optional, Sequence +from typing import IO, Callable, Dict, Iterable, List, Optional, Sequence from opentelemetry.context import ( _SUPPRESS_INSTRUMENTATION_KEY, @@ -50,10 +50,6 @@ class MetricExporter(ABC): in their own format. """ - @property - def preferred_temporality(self) -> AggregationTemporality: - return AggregationTemporality.CUMULATIVE - @abstractmethod def export(self, metrics: Sequence[Metric]) -> "MetricExportResult": """Exports a batch of telemetry data. @@ -107,8 +103,7 @@ class InMemoryMetricReader(MetricReader): """ def __init__( - self, - preferred_temporality: AggregationTemporality = AggregationTemporality.CUMULATIVE, + self, preferred_temporality: Dict[type, AggregationTemporality] = None ) -> None: super().__init__(preferred_temporality=preferred_temporality) self._lock = RLock() @@ -139,10 +134,11 @@ class PeriodicExportingMetricReader(MetricReader): def __init__( self, exporter: MetricExporter, + preferred_temporality: Dict[type, AggregationTemporality] = None, export_interval_millis: Optional[float] = None, export_timeout_millis: Optional[float] = None, ) -> None: - super().__init__(preferred_temporality=exporter.preferred_temporality) + super().__init__(preferred_temporality=preferred_temporality) self._exporter = exporter if export_interval_millis is None: try: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index b637bba9f0..9b74c064ad 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -15,7 +15,7 @@ # pylint: disable=too-many-ancestors import logging -from typing import Dict, Generator, Iterable, Optional, Union +from typing import TYPE_CHECKING, Dict, Generator, Iterable, Optional, Union from opentelemetry._metrics.instrument import CallbackT from opentelemetry._metrics.instrument import Counter as APICounter @@ -31,9 +31,13 @@ ) from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.measurement_consumer import MeasurementConsumer from opentelemetry.sdk.util.instrumentation import InstrumentationScope +if TYPE_CHECKING: + from opentelemetry.sdk._metrics.measurement_consumer import ( + MeasurementConsumer, + ) + _logger = logging.getLogger(__name__) @@ -42,7 +46,7 @@ def __init__( self, name: str, instrumentation_scope: InstrumentationScope, - measurement_consumer: MeasurementConsumer, + measurement_consumer: "MeasurementConsumer", unit: str = "", description: str = "", ): @@ -59,7 +63,7 @@ def __init__( self, name: str, instrumentation_scope: InstrumentationScope, - measurement_consumer: MeasurementConsumer, + measurement_consumer: "MeasurementConsumer", callbacks: Optional[Iterable[CallbackT]] = None, unit: str = "", description: str = "", diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py index c4b6770276..7ee0c4ea85 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py @@ -14,7 +14,7 @@ from abc import ABC, abstractmethod from threading import Lock -from typing import TYPE_CHECKING, Iterable, List, Mapping +from typing import TYPE_CHECKING, Dict, Iterable, List, Mapping from opentelemetry.sdk._metrics.aggregation import AggregationTemporality from opentelemetry.sdk._metrics.measurement import Measurement @@ -40,7 +40,9 @@ def register_asynchronous_instrument(self, instrument: "_Asynchronous"): @abstractmethod def collect( - self, metric_reader: MetricReader, temporality: AggregationTemporality + self, + metric_reader: MetricReader, + instrument_type_temporality: Dict[type, AggregationTemporality], ) -> Iterable[Metric]: pass @@ -67,11 +69,15 @@ def register_asynchronous_instrument( self._async_instruments.append(instrument) def collect( - self, metric_reader: MetricReader, temporality: AggregationTemporality + self, + metric_reader: MetricReader, + instrument_type_temporality: Dict[type, AggregationTemporality], ) -> Iterable[Metric]: with self._lock: metric_reader_storage = self._reader_storages[metric_reader] for async_instrument in self._async_instruments: for measurement in async_instrument.callback(): metric_reader_storage.consume_measurement(measurement) - return self._reader_storages[metric_reader].collect(temporality) + return self._reader_storages[metric_reader].collect( + instrument_type_temporality + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py index e14877d87d..85ef1cd351 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py @@ -12,31 +12,100 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging from abc import ABC, abstractmethod -from typing import Callable, Iterable +from logging import getLogger +from os import environ +from typing import Callable, Dict, Iterable from typing_extensions import final +from opentelemetry.sdk._metrics.instrument import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric +from opentelemetry.sdk.environment_variables import ( + _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, +) -_logger = logging.getLogger(__name__) +_logger = getLogger(__name__) class MetricReader(ABC): """ + Base class for all metric readers + + Args: + preferred_temporality: A mapping between instrument classes and + aggregation temporality. By default uses CUMULATIVE for all instrument + classes. This mapping will be used to define the default aggregation + temporality of every instrument class. If the user wants to make a + change in the default aggregation temporality of an instrument class, + it is enough to pass here a dictionary whose keys are the instrument + classes and the values are the corresponding desired aggregation + temporalities of the classes that the user wants to change, not all of + them. The classes not included in the passed dictionary will retain + their association to their default aggregation temporalities. + The value passed here will override the corresponding values set + via the environment variable + .. document protected _receive_metrics which is a intended to be overriden by subclass .. automethod:: _receive_metrics """ + # FIXME add :std:envvar:`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` + # to the end of the documentation paragraph above. + def __init__( - self, - preferred_temporality: AggregationTemporality = AggregationTemporality.CUMULATIVE, + self, preferred_temporality: Dict[type, AggregationTemporality] = None ) -> None: self._collect: Callable[ ["MetricReader", AggregationTemporality], Iterable[Metric] ] = None - self._preferred_temporality = preferred_temporality + + if ( + environ.get( + _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + "CUMULATIVE", + ) + .upper() + .strip() + == "DELTA" + ): + self._instrument_class_temporality = { + Counter: AggregationTemporality.DELTA, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.DELTA, + ObservableCounter: AggregationTemporality.DELTA, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + else: + self._instrument_class_temporality = { + Counter: AggregationTemporality.CUMULATIVE, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.CUMULATIVE, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + if preferred_temporality is not None: + for temporality in preferred_temporality.values(): + if temporality not in ( + AggregationTemporality.CUMULATIVE, + AggregationTemporality.DELTA, + ): + raise Exception( + f"Invalid temporality value found {temporality}" + ) + + self._instrument_class_temporality.update(preferred_temporality or {}) @final def collect(self) -> None: @@ -48,7 +117,9 @@ def collect(self) -> None: "Cannot call collect on a MetricReader until it is registered on a MeterProvider" ) return - self._receive_metrics(self._collect(self, self._preferred_temporality)) + self._receive_metrics( + self._collect(self, self._instrument_class_temporality) + ) @final def _set_collect_callback( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py index 45c4d4dd79..7835bf5858 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py @@ -83,21 +83,30 @@ def consume_measurement(self, measurement: Measurement) -> None: ): view_instrument_match.consume_measurement(measurement) - def collect(self, temporality: AggregationTemporality) -> Iterable[Metric]: - # use a list instead of yielding to prevent a slow reader from holding SDK locks + def collect( + self, instrument_type_temporality: Dict[type, AggregationTemporality] + ) -> Iterable[Metric]: + # Use a list instead of yielding to prevent a slow reader from holding + # SDK locks metrics: List[Metric] = [] - # While holding the lock, new _ViewInstrumentMatch can't be added from another thread (so we are - # sure we collect all existing view). However, instruments can still send measurements - # that will make it into the individual aggregations; collection will acquire those - # locks iteratively to keep locking as fine-grained as possible. One side effect is - # that end times can be slightly skewed among the metric streams produced by the SDK, - # but we still align the output timestamps for a single instrument. + # While holding the lock, new _ViewInstrumentMatch can't be added from + # another thread (so we are sure we collect all existing view). + # However, instruments can still send measurements that will make it + # into the individual aggregations; collection will acquire those locks + # iteratively to keep locking as fine-grained as possible. One side + # effect is that end times can be slightly skewed among the metric + # streams produced by the SDK, but we still align the output timestamps + # for a single instrument. with self._lock: for ( view_instrument_matches ) in self._view_instrument_match.values(): for view_instrument_match in view_instrument_matches: - metrics.extend(view_instrument_match.collect(temporality)) + metrics.extend( + view_instrument_match.collect( + instrument_type_temporality + ) + ) return metrics diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index d9bab1bb16..3e632bc521 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -406,3 +406,18 @@ provide the entry point for loading the log emitter provider. If not specified, SDK LogEmitterProvider is used. """ + +_OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = ( + "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE" +) +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE + +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` environment +variable allows users to set the default aggregation temporality policy to use +on the basis of instrument kind. The valid (case-insensitive) values are: + +``CUMULATIVE``: Choose ``CUMULATIVE`` aggregation temporality for all instrument kinds. +``DELTA``: Choose ``DELTA`` aggregation temporality for ``Counter``, ``Asynchronous Counter`` and ``Histogram``. +Choose ``CUMULATIVE`` aggregation temporality for ``UpDownCounter`` and ``Asynchronous UpDownCounter``. +""" diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_metric_reader.py new file mode 100644 index 0000000000..0f9792313a --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader.py @@ -0,0 +1,175 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import environ +from typing import Dict +from unittest import TestCase +from unittest.mock import patch + +from opentelemetry.sdk._metrics.aggregation import AggregationTemporality +from opentelemetry.sdk._metrics.instrument import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.sdk._metrics.metric_reader import MetricReader +from opentelemetry.sdk.environment_variables import ( + _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, +) + + +class DummyMetricReader(MetricReader): + def __init__( + self, preferred_temporality: Dict[type, AggregationTemporality] = None + ) -> None: + super().__init__( + preferred_temporality=preferred_temporality, + ) + + def _receive_metrics(self, metrics): + pass + + def shutdown(self): + return True + + +class TestMetricReader(TestCase): + @patch.dict( + environ, + {_OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "CUMULATIVE"}, + ) + def test_configure_temporality_cumulative(self): + + dummy_metric_reader = DummyMetricReader() + + self.assertEqual( + dummy_metric_reader._instrument_class_temporality.keys(), + set( + [ + Counter, + UpDownCounter, + Histogram, + ObservableCounter, + ObservableUpDownCounter, + ObservableGauge, + ] + ), + ) + for ( + value + ) in dummy_metric_reader._instrument_class_temporality.values(): + self.assertEqual(value, AggregationTemporality.CUMULATIVE) + + @patch.dict( + environ, {_OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"} + ) + def test_configure_temporality_delta(self): + + dummy_metric_reader = DummyMetricReader() + + self.assertEqual( + dummy_metric_reader._instrument_class_temporality.keys(), + set( + [ + Counter, + UpDownCounter, + Histogram, + ObservableCounter, + ObservableUpDownCounter, + ObservableGauge, + ] + ), + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[Counter], + AggregationTemporality.DELTA, + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[UpDownCounter], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[Histogram], + AggregationTemporality.DELTA, + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[ + ObservableCounter + ], + AggregationTemporality.DELTA, + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[ + ObservableUpDownCounter + ], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[ObservableGauge], + AggregationTemporality.CUMULATIVE, + ) + + def test_configure_temporality_parameter(self): + + dummy_metric_reader = DummyMetricReader( + preferred_temporality={ + Histogram: AggregationTemporality.DELTA, + ObservableGauge: AggregationTemporality.DELTA, + } + ) + + self.assertEqual( + dummy_metric_reader._instrument_class_temporality.keys(), + set( + [ + Counter, + UpDownCounter, + Histogram, + ObservableCounter, + ObservableUpDownCounter, + ObservableGauge, + ] + ), + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[Counter], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[UpDownCounter], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[Histogram], + AggregationTemporality.DELTA, + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[ + ObservableCounter + ], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[ + ObservableUpDownCounter + ], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[ObservableGauge], + AggregationTemporality.DELTA, + ) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 0279dfbe61..81d1068fb8 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -36,7 +36,7 @@ UpDownCounter, ) from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric +from opentelemetry.sdk._metrics.point import Metric from opentelemetry.sdk._metrics.view import View from opentelemetry.sdk.resources import Resource from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc @@ -44,7 +44,7 @@ class DummyMetricReader(MetricReader): def __init__(self): - super().__init__(AggregationTemporality.CUMULATIVE) + super().__init__() def _receive_metrics(self, metrics): pass diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index 728e291180..ff67e848af 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -84,7 +84,9 @@ def _create_periodic_reader( self, metrics, exporter, collect_wait=0, interval=60000 ): - pmr = PeriodicExportingMetricReader(exporter, interval) + pmr = PeriodicExportingMetricReader( + exporter, export_interval_millis=interval + ) def _collect(reader, temp): time.sleep(collect_wait) @@ -95,7 +97,7 @@ def _collect(reader, temp): def test_ticker_called(self): collect_mock = Mock() - pmr = PeriodicExportingMetricReader(Mock(), 1) + pmr = PeriodicExportingMetricReader(Mock(), export_interval_millis=1) pmr._set_collect_callback(collect_mock) time.sleep(0.1) self.assertTrue(collect_mock.assert_called_once) diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 20b1a04169..9ccdd90933 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -13,7 +13,7 @@ # limitations under the License. from unittest import TestCase -from unittest.mock import Mock +from unittest.mock import MagicMock, Mock from opentelemetry.sdk._metrics._view_instrument_match import ( _ViewInstrumentMatch, @@ -185,7 +185,11 @@ def test_collect(self): self.assertEqual( next( view_instrument_match.collect( - AggregationTemporality.CUMULATIVE + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ) ) ), Metric( From 7aac852e408bf2fda8eaa1a758961523a0a3d624 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 27 Apr 2022 10:51:18 -0600 Subject: [PATCH 1207/1517] Reuse metric family (#2639) * Reuse metric family Fixes #2628 * Add typing --- .../exporter/prometheus/__init__.py | 105 ++++++++++++------ 1 file changed, 72 insertions(+), 33 deletions(-) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 6e71395da5..09ca38c57b 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -67,9 +67,15 @@ from json import dumps from logging import getLogger from re import IGNORECASE, UNICODE, compile -from typing import Iterable, Optional, Sequence, Tuple, Union +from typing import Dict, Iterable, Sequence, Tuple, Union -from prometheus_client import core +from prometheus_client.core import ( + REGISTRY, + CounterMetricFamily, + GaugeMetricFamily, + HistogramMetricFamily, +) +from prometheus_client.core import Metric as PrometheusMetric from opentelemetry.sdk._metrics.export import MetricReader from opentelemetry.sdk._metrics.point import Gauge, Histogram, Metric, Sum @@ -101,7 +107,7 @@ class PrometheusMetricReader(MetricReader): def __init__(self, prefix: str = "") -> None: super().__init__() self._collector = _CustomCollector(prefix) - core.REGISTRY.register(self._collector) + REGISTRY.register(self._collector) self._collector._callback = self.collect def _receive_metrics(self, metrics: Iterable[Metric]) -> None: @@ -110,7 +116,7 @@ def _receive_metrics(self, metrics: Iterable[Metric]) -> None: self._collector.add_metrics_data(metrics) def shutdown(self) -> bool: - core.REGISTRY.unregister(self._collector) + REGISTRY.unregister(self._collector) return True @@ -142,18 +148,23 @@ def collect(self) -> None: if self._callback is not None: self._callback() + metric_family_id_metric_family = {} + while self._metrics_to_export: for export_record in self._metrics_to_export.popleft(): - prometheus_metric = self._translate_to_prometheus( - export_record + self._translate_to_prometheus( + export_record, metric_family_id_metric_family ) - if prometheus_metric is not None: - yield prometheus_metric + + if metric_family_id_metric_family: + for metric_family in metric_family_id_metric_family.values(): + yield metric_family def _translate_to_prometheus( - self, metric: Metric - ) -> Optional[core.Metric]: - prometheus_metric = None + self, + metric: Metric, + metric_family_id_metric_family: Dict[str, PrometheusMetric], + ): label_values = [] label_keys = [] for key, value in metric.attributes.items(): @@ -166,41 +177,69 @@ def _translate_to_prometheus( metric_name += self._sanitize(metric.name) description = metric.description or "" + + metric_family_id = "|".join( + [metric_name, description, "%".join(label_keys), metric.unit] + ) + if isinstance(metric.point, Sum): - prometheus_metric = core.CounterMetricFamily( - name=metric_name, - documentation=description, - labels=label_keys, - unit=metric.unit, + + metric_family_id = "|".join( + [metric_family_id, CounterMetricFamily.__name__] ) - prometheus_metric.add_metric( + + if metric_family_id not in metric_family_id_metric_family.keys(): + metric_family_id_metric_family[ + metric_family_id + ] = CounterMetricFamily( + name=metric_name, + documentation=description, + labels=label_keys, + unit=metric.unit, + ) + metric_family_id_metric_family[metric_family_id].add_metric( labels=label_values, value=metric.point.value ) elif isinstance(metric.point, Gauge): - prometheus_metric = core.GaugeMetricFamily( - name=metric_name, - documentation=description, - labels=label_keys, - unit=metric.unit, + + metric_family_id = "|".join( + [metric_family_id, GaugeMetricFamily.__name__] ) - prometheus_metric.add_metric( + + if metric_family_id not in metric_family_id_metric_family.keys(): + metric_family_id_metric_family[ + metric_family_id + ] = GaugeMetricFamily( + name=metric_name, + documentation=description, + labels=label_keys, + unit=metric.unit, + ) + metric_family_id_metric_family[metric_family_id].add_metric( labels=label_values, value=metric.point.value ) elif isinstance(metric.point, Histogram): - value = metric.point.sum - prometheus_metric = core.HistogramMetricFamily( - name=metric_name, - documentation=description, - labels=label_keys, - unit=metric.unit, + + metric_family_id = "|".join( + [metric_family_id, HistogramMetricFamily.__name__] ) - buckets = _convert_buckets(metric) - prometheus_metric.add_metric( - labels=label_values, buckets=buckets, sum_value=value + + if metric_family_id not in metric_family_id_metric_family.keys(): + metric_family_id_metric_family[ + metric_family_id + ] = HistogramMetricFamily( + name=metric_name, + documentation=description, + labels=label_keys, + unit=metric.unit, + ) + metric_family_id_metric_family[metric_family_id].add_metric( + labels=label_values, + buckets=_convert_buckets(metric), + sum_value=metric.point.sum, ) else: _logger.warning("Unsupported metric type. %s", type(metric.point)) - return prometheus_metric def _sanitize(self, key: str) -> str: """sanitize the given metric name or label according to Prometheus rule. From a4b4c458e6963daf1b3d389394c93e303bbc904e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 27 Apr 2022 14:15:49 -0600 Subject: [PATCH 1208/1517] Implement MetricReader default aggregation controls (#2638) --- CHANGELOG.md | 2 + .../sdk/_metrics/_view_instrument_match.py | 22 +++++-- .../sdk/_metrics/export/__init__.py | 16 ++++- .../opentelemetry/sdk/_metrics/instrument.py | 5 +- .../sdk/_metrics/measurement_consumer.py | 4 +- .../sdk/_metrics/metric_reader.py | 32 +++++++++- .../sdk/_metrics/metric_reader_storage.py | 18 +++++- .../tests/metrics/test_metric_reader.py | 53 +++++++++++++++- .../metrics/test_metric_reader_storage.py | 22 ++++--- .../metrics/test_view_instrument_match.py | 60 +++++++++++++++++++ 10 files changed, 211 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 644cfcdefa..36e49f5140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.11.1-0.30b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.1-0.30b1) - 2022-04-21 +- Add parameter to MetricReader constructor to select aggregation per instrument kind + ([#2638](https://github.com/open-telemetry/opentelemetry-python/pull/2638)) - Add parameter to MetricReader constructor to select temporality per instrument kind ([#2637](https://github.com/open-telemetry/opentelemetry-python/pull/2637)) - Fix unhandled callback exceptions on async instruments diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py index a75ce7d4c8..3754f117a3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py @@ -18,7 +18,9 @@ from typing import TYPE_CHECKING, Dict, Iterable from opentelemetry.sdk._metrics.aggregation import ( + DefaultAggregation, _Aggregation, + _AggregationFactory, _convert_aggregation_temporality, _PointVarT, ) @@ -39,6 +41,7 @@ def __init__( view: View, instrument: "_Instrument", sdk_config: SdkConfiguration, + instrument_class_aggregation: Dict[type, _AggregationFactory], ): self._view = view self._instrument = instrument @@ -46,6 +49,7 @@ def __init__( self._attributes_aggregation: Dict[frozenset, _Aggregation] = {} self._attributes_previous_point: Dict[frozenset, _PointVarT] = {} self._lock = Lock() + self._instrument_class_aggregation = instrument_class_aggregation # pylint: disable=protected-access def consume_measurement(self, measurement: Measurement) -> None: @@ -67,11 +71,19 @@ def consume_measurement(self, measurement: Measurement) -> None: if attributes not in self._attributes_aggregation: with self._lock: if attributes not in self._attributes_aggregation: - self._attributes_aggregation[ - attributes - ] = self._view._aggregation._create_aggregation( - self._instrument - ) + if not isinstance( + self._view._aggregation, DefaultAggregation + ): + aggregation = ( + self._view._aggregation._create_aggregation( + self._instrument + ) + ) + else: + aggregation = self._instrument_class_aggregation[ + self._instrument.__class__ + ]._create_aggregation(self._instrument) + self._attributes_aggregation[attributes] = aggregation self._attributes_aggregation[attributes].aggregate(measurement) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py index a76b648959..00db5c1a91 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -27,6 +27,7 @@ detach, set_value, ) +from opentelemetry.sdk._metrics.aggregation import _AggregationFactory from opentelemetry.sdk._metrics.metric_reader import MetricReader from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric from opentelemetry.util._once import Once @@ -103,9 +104,14 @@ class InMemoryMetricReader(MetricReader): """ def __init__( - self, preferred_temporality: Dict[type, AggregationTemporality] = None + self, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[type, _AggregationFactory] = None, ) -> None: - super().__init__(preferred_temporality=preferred_temporality) + super().__init__( + preferred_temporality=preferred_temporality, + preferred_aggregation=preferred_aggregation, + ) self._lock = RLock() self._metrics: List[Metric] = [] @@ -135,10 +141,14 @@ def __init__( self, exporter: MetricExporter, preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[type, _AggregationFactory] = None, export_interval_millis: Optional[float] = None, export_timeout_millis: Optional[float] = None, ) -> None: - super().__init__(preferred_temporality=preferred_temporality) + super().__init__( + preferred_temporality=preferred_temporality, + preferred_aggregation=preferred_aggregation, + ) self._exporter = exporter if export_interval_millis is None: try: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index 9b74c064ad..92de877550 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -14,7 +14,7 @@ # pylint: disable=too-many-ancestors -import logging +from logging import getLogger from typing import TYPE_CHECKING, Dict, Generator, Iterable, Optional, Union from opentelemetry._metrics.instrument import CallbackT @@ -38,7 +38,8 @@ MeasurementConsumer, ) -_logger = logging.getLogger(__name__) + +_logger = getLogger(__name__) class _Synchronous: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py index 7ee0c4ea85..df77bf71e4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py @@ -53,7 +53,9 @@ def __init__(self, sdk_config: SdkConfiguration) -> None: self._sdk_config = sdk_config # should never be mutated self._reader_storages: Mapping[MetricReader, MetricReaderStorage] = { - reader: MetricReaderStorage(sdk_config) + reader: MetricReaderStorage( + sdk_config, reader._instrument_class_aggregation + ) for reader in sdk_config.metric_readers } self._async_instruments: List["_Asynchronous"] = [] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py index 85ef1cd351..7bcdce465d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py @@ -19,6 +19,10 @@ from typing_extensions import final +from opentelemetry.sdk._metrics.aggregation import ( + DefaultAggregation, + _AggregationFactory, +) from opentelemetry.sdk._metrics.instrument import ( Counter, Histogram, @@ -52,6 +56,19 @@ class MetricReader(ABC): their association to their default aggregation temporalities. The value passed here will override the corresponding values set via the environment variable + preferred_aggregation: A mapping between instrument classes and + aggregation instances. By default maps all instrument classes to an + instance of `DefaultAggregation`. This mapping will be used to + define the default aggregation of every instrument class. If the + user wants to make a change in the default aggregation of an + instrument class, it is enough to pass here a dictionary whose keys + are the instrument classes and the values are the corresponding + desired aggregation for the instrument classes that the user wants + to change, not necessarily all of them. The classes not included in + the passed dictionary will retain their association to their + default aggregations. The aggregation defined here will be + overriden by an aggregation defined by a view that is not + `DefaultAggregation`. .. document protected _receive_metrics which is a intended to be overriden by subclass .. automethod:: _receive_metrics @@ -61,7 +78,9 @@ class MetricReader(ABC): # to the end of the documentation paragraph above. def __init__( - self, preferred_temporality: Dict[type, AggregationTemporality] = None + self, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[type, _AggregationFactory] = None, ) -> None: self._collect: Callable[ ["MetricReader", AggregationTemporality], Iterable[Metric] @@ -106,6 +125,17 @@ def __init__( ) self._instrument_class_temporality.update(preferred_temporality or {}) + self._preferred_temporality = preferred_temporality + self._instrument_class_aggregation = { + Counter: DefaultAggregation(), + UpDownCounter: DefaultAggregation(), + Histogram: DefaultAggregation(), + ObservableCounter: DefaultAggregation(), + ObservableUpDownCounter: DefaultAggregation(), + ObservableGauge: DefaultAggregation(), + } + + self._instrument_class_aggregation.update(preferred_aggregation or {}) @final def collect(self) -> None: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py index 7835bf5858..d76457f41b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py @@ -19,7 +19,10 @@ from opentelemetry.sdk._metrics._view_instrument_match import ( _ViewInstrumentMatch, ) -from opentelemetry.sdk._metrics.aggregation import AggregationTemporality +from opentelemetry.sdk._metrics.aggregation import ( + AggregationTemporality, + _AggregationFactory, +) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import Metric from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration @@ -31,12 +34,17 @@ class MetricReaderStorage: """The SDK's storage for a given reader""" - def __init__(self, sdk_config: SdkConfiguration) -> None: + def __init__( + self, + sdk_config: SdkConfiguration, + instrument_class_aggregation: Dict[type, _AggregationFactory], + ) -> None: self._lock = RLock() self._sdk_config = sdk_config self._view_instrument_match: Dict[ Instrument, List[_ViewInstrumentMatch] ] = {} + self._instrument_class_aggregation = instrument_class_aggregation def _get_or_init_view_instrument_match( self, instrument: Instrument @@ -62,6 +70,9 @@ def _get_or_init_view_instrument_match( view=view, instrument=instrument, sdk_config=self._sdk_config, + instrument_class_aggregation=( + self._instrument_class_aggregation + ), ) ) @@ -72,6 +83,9 @@ def _get_or_init_view_instrument_match( view=_DEFAULT_VIEW, instrument=instrument, sdk_config=self._sdk_config, + instrument_class_aggregation=( + self._instrument_class_aggregation + ), ) ) self._view_instrument_match[instrument] = view_instrument_matches diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_metric_reader.py index 0f9792313a..200b8824b8 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader.py @@ -17,7 +17,12 @@ from unittest import TestCase from unittest.mock import patch -from opentelemetry.sdk._metrics.aggregation import AggregationTemporality +from opentelemetry.sdk._metrics.aggregation import ( + AggregationTemporality, + DefaultAggregation, + LastValueAggregation, + _AggregationFactory, +) from opentelemetry.sdk._metrics.instrument import ( Counter, Histogram, @@ -34,10 +39,13 @@ class DummyMetricReader(MetricReader): def __init__( - self, preferred_temporality: Dict[type, AggregationTemporality] = None + self, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[type, _AggregationFactory] = None, ) -> None: super().__init__( preferred_temporality=preferred_temporality, + preferred_aggregation=preferred_aggregation, ) def _receive_metrics(self, metrics): @@ -173,3 +181,44 @@ def test_configure_temporality_parameter(self): dummy_metric_reader._instrument_class_temporality[ObservableGauge], AggregationTemporality.DELTA, ) + + def test_default_temporality(self): + dummy_metric_reader = DummyMetricReader() + self.assertEqual( + dummy_metric_reader._instrument_class_aggregation.keys(), + set( + [ + Counter, + UpDownCounter, + Histogram, + ObservableCounter, + ObservableUpDownCounter, + ObservableGauge, + ] + ), + ) + for ( + value + ) in dummy_metric_reader._instrument_class_aggregation.values(): + self.assertIsInstance(value, DefaultAggregation) + + dummy_metric_reader = DummyMetricReader( + preferred_aggregation={Counter: LastValueAggregation()} + ) + self.assertEqual( + dummy_metric_reader._instrument_class_aggregation.keys(), + set( + [ + Counter, + UpDownCounter, + Histogram, + ObservableCounter, + ObservableUpDownCounter, + ObservableGauge, + ] + ), + ) + self.assertIsInstance( + dummy_metric_reader._instrument_class_aggregation[Counter], + LastValueAggregation, + ) diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index f7ffc6993a..d0d05854d8 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -12,9 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch -from opentelemetry.sdk._metrics.aggregation import DropAggregation +from opentelemetry.sdk._metrics.aggregation import ( + DefaultAggregation, + DropAggregation, +) from opentelemetry.sdk._metrics.instrument import Counter from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.metric_reader_storage import ( @@ -56,7 +59,8 @@ def test_creates_view_instrument_matches( resource=Mock(), metric_readers=(), views=(view1, view2), - ) + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) # instrument1 matches view1 and view2, so should create two ViewInstrumentMatch objects @@ -100,7 +104,8 @@ def test_forwards_calls_to_view_instrument_match( resource=Mock(), metric_readers=(), views=(view1, view2), - ) + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) # Measurements from an instrument should be passed on to each ViewInstrumentMatch objects @@ -147,7 +152,8 @@ def test_race_concurrent_measurements(self, MockViewInstrumentMatch: Mock): resource=Mock(), metric_readers=(), views=(view1,), - ) + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) def send_measurement(): @@ -172,7 +178,8 @@ def test_default_view_enabled(self, MockViewInstrumentMatch: Mock): resource=Mock(), metric_readers=(), views=(), - ) + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) storage.consume_measurement(Measurement(1, instrument1)) @@ -200,7 +207,8 @@ def test_drop_aggregation(self): instrument_name="name", aggregation=DropAggregation() ), ), - ) + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) metric_reader_storage.consume_measurement(Measurement(1, counter)) diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 9ccdd90933..9d91c3675d 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -19,9 +19,13 @@ _ViewInstrumentMatch, ) from opentelemetry.sdk._metrics.aggregation import ( + DefaultAggregation, DropAggregation, + LastValueAggregation, _DropAggregation, + _LastValueAggregation, ) +from opentelemetry.sdk._metrics.instrument import Counter from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration @@ -56,6 +60,9 @@ def test_consume_measurement(self): ), instrument=instrument1, sdk_config=sdk_config, + instrument_class_aggregation=MagicMock( + **{"__getitem__.return_value": DefaultAggregation()} + ), ) view_instrument_match.consume_measurement( @@ -95,6 +102,9 @@ def test_consume_measurement(self): ), instrument=instrument1, sdk_config=sdk_config, + instrument_class_aggregation=MagicMock( + **{"__getitem__.return_value": DefaultAggregation()} + ), ) view_instrument_match.consume_measurement( @@ -123,6 +133,9 @@ def test_consume_measurement(self): ), instrument=instrument1, sdk_config=sdk_config, + instrument_class_aggregation=MagicMock( + **{"__getitem__.return_value": DefaultAggregation()} + ), ) view_instrument_match.consume_measurement( Measurement(value=0, instrument=instrument1, attributes=None) @@ -145,6 +158,9 @@ def test_consume_measurement(self): ), instrument=instrument1, sdk_config=sdk_config, + instrument_class_aggregation=MagicMock( + **{"__getitem__.return_value": DefaultAggregation()} + ), ) view_instrument_match.consume_measurement( Measurement(value=0, instrument=instrument1, attributes=None) @@ -173,6 +189,9 @@ def test_collect(self): ), instrument=instrument1, sdk_config=sdk_config, + instrument_class_aggregation=MagicMock( + **{"__getitem__.return_value": DefaultAggregation()} + ), ) view_instrument_match.consume_measurement( @@ -202,3 +221,44 @@ def test_collect(self): point=None, ), ) + + def test_setting_aggregation(self): + instrument1 = Counter( + name="instrument1", + instrumentation_scope=Mock(), + measurement_consumer=Mock(), + description="description", + unit="unit", + ) + instrument1.instrumentation_scope = self.mock_instrumentation_scope + sdk_config = SdkConfiguration( + resource=self.mock_resource, + metric_readers=[], + views=[], + ) + view_instrument_match = _ViewInstrumentMatch( + view=View( + instrument_name="instrument1", + name="name", + aggregation=DefaultAggregation(), + attribute_keys={"a", "c"}, + ), + instrument=instrument1, + sdk_config=sdk_config, + instrument_class_aggregation={Counter: LastValueAggregation()}, + ) + + view_instrument_match.consume_measurement( + Measurement( + value=0, + instrument=Mock(name="instrument1"), + attributes={"c": "d", "f": "g"}, + ) + ) + + self.assertIsInstance( + view_instrument_match._attributes_aggregation[ + frozenset({("c", "d")}) + ], + _LastValueAggregation, + ) From 2972ebc0d1ee54b53af1af847d5c029ac44422c9 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 29 Apr 2022 00:02:36 -0400 Subject: [PATCH 1209/1517] Move Metrics API behind internal package (#2651) * Move __init__.py file * Move Metrics API behind internal package --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 4 +- docs/api/metrics.instrument.rst | 8 - docs/api/metrics.observation.rst | 7 - docs/api/metrics.rst | 5 - docs/conf.py | 22 +- docs/examples/metrics/example.py | 7 +- docs/getting_started/metrics_example.py | 7 +- .../src/opentelemetry/_metrics/__init__.py | 776 ++---------------- .../_metrics/_internal/__init__.py | 760 +++++++++++++++++ .../_metrics/{ => _internal}/instrument.py | 30 +- .../_metrics/{ => _internal}/observation.py | 0 .../tests/metrics/test_instruments.py | 5 +- .../tests/metrics/test_meter_provider.py | 21 +- ...est_measurement.py => test_observation.py} | 2 +- .../opentelemetry/sdk/_metrics/__init__.py | 16 +- .../opentelemetry/sdk/_metrics/aggregation.py | 7 +- .../opentelemetry/sdk/_metrics/instrument.py | 18 +- .../sdk/_metrics/metric_reader_storage.py | 2 +- .../src/opentelemetry/sdk/_metrics/view.py | 2 +- .../metrics/integration_test/test_cpu_time.py | 3 +- .../metrics/test_in_memory_metric_reader.py | 2 +- .../tests/metrics/test_instrument.py | 2 +- .../src/opentelemetry/test/globals_test.py | 5 +- 24 files changed, 888 insertions(+), 825 deletions(-) delete mode 100644 docs/api/metrics.instrument.rst delete mode 100644 docs/api/metrics.observation.rst create mode 100644 opentelemetry-api/src/opentelemetry/_metrics/_internal/__init__.py rename opentelemetry-api/src/opentelemetry/_metrics/{ => _internal}/instrument.py (95%) rename opentelemetry-api/src/opentelemetry/_metrics/{ => _internal}/observation.py (100%) rename opentelemetry-api/tests/metrics/{test_measurement.py => test_observation.py} (96%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c3fea117d..2115e71f9f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 4cfca481f8e2c8af5f7cfd032997fac692995f67 + CONTRIB_REPO_SHA: 008cd2370dcd3e87cca8c0ddbb0b820681fd7346 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 36e49f5140..d43389891c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.1-0.30b1...HEAD) -## [1.11.1-0.30b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.1-0.30b1) - 2022-04-21 +- Move Metrics API behind internal package + ([#2651](https://github.com/open-telemetry/opentelemetry-python/pull/2651)) +## [1.11.1-0.30b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.1-0.30b1) - 2022-04-21 - Add parameter to MetricReader constructor to select aggregation per instrument kind ([#2638](https://github.com/open-telemetry/opentelemetry-python/pull/2638)) diff --git a/docs/api/metrics.instrument.rst b/docs/api/metrics.instrument.rst deleted file mode 100644 index 7d23f6fa77..0000000000 --- a/docs/api/metrics.instrument.rst +++ /dev/null @@ -1,8 +0,0 @@ -opentelemetry._metrics.instrument -================================= - -.. automodule:: opentelemetry._metrics.instrument - :members: - :private-members: - :undoc-members: - :no-show-inheritance: diff --git a/docs/api/metrics.observation.rst b/docs/api/metrics.observation.rst deleted file mode 100644 index 3df89ae6a5..0000000000 --- a/docs/api/metrics.observation.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry._metrics.observation -================================== - -.. automodule:: opentelemetry._metrics.observation - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/metrics.rst b/docs/api/metrics.rst index 08c4cbcc70..4fd464ae1d 100644 --- a/docs/api/metrics.rst +++ b/docs/api/metrics.rst @@ -8,13 +8,8 @@ opentelemetry._metrics package Once metrics become stable, this package will be be renamed to ``opentelemetry.metrics``. -Submodules ----------- - .. toctree:: - metrics.instrument - metrics.observation Module contents --------------- diff --git a/docs/conf.py b/docs/conf.py index d250de2b53..03fad0837f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -97,9 +97,6 @@ # https://github.com/sphinx-doc/sphinx/pull/3744 nitpick_ignore = [ ("py:class", "ValueT"), - ("py:class", "MetricT"), - ("py:class", "InstrumentT"), - ("py:obj", "opentelemetry._metrics.instrument.InstrumentT"), # Even if wrapt is added to intersphinx_mapping, sphinx keeps failing # with "class reference target not found: ObjectProxy". ("py:class", "ObjectProxy"), @@ -142,24 +139,7 @@ "examples/error_handler/error_handler_1", ] -_exclude_members = [ - "_ProxyObservableUpDownCounter", - "_ProxyHistogram", - "_ProxyObservableGauge", - "_ProxyInstrument", - "_ProxyAsynchronousInstrument", - "_ProxyCounter", - "_ProxyUpDownCounter", - "_ProxyObservableCounter", - "_ProxyObservableGauge", - "_abc_impl", - "_Adding", - "_Grouping", - "_Monotonic", - "_NonMonotonic", - "Synchronous", - "Asynchronous", -] +_exclude_members = ["_abc_impl"] autodoc_default_options = { "members": True, diff --git a/docs/examples/metrics/example.py b/docs/examples/metrics/example.py index 0c4db25cf0..11c2b8d8e0 100644 --- a/docs/examples/metrics/example.py +++ b/docs/examples/metrics/example.py @@ -1,7 +1,10 @@ from typing import Iterable -from opentelemetry._metrics import get_meter_provider, set_meter_provider -from opentelemetry._metrics.observation import Observation +from opentelemetry._metrics import ( + Observation, + get_meter_provider, + set_meter_provider, +) from opentelemetry.exporter.otlp.proto.grpc._metric_exporter import ( OTLPMetricExporter, ) diff --git a/docs/getting_started/metrics_example.py b/docs/getting_started/metrics_example.py index 2a7199b99c..a1e1b7ff07 100644 --- a/docs/getting_started/metrics_example.py +++ b/docs/getting_started/metrics_example.py @@ -17,8 +17,11 @@ from typing import Iterable -from opentelemetry._metrics import get_meter_provider, set_meter_provider -from opentelemetry._metrics.observation import Observation +from opentelemetry._metrics import ( + Observation, + get_meter_provider, + set_meter_provider, +) from opentelemetry.sdk._metrics import MeterProvider from opentelemetry.sdk._metrics.export import ( ConsoleMetricExporter, diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index 6447135743..eac37b2cfe 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=too-many-ancestors - """ The OpenTelemetry metrics API describes the classes used to generate metrics. @@ -40,17 +38,21 @@ .. versionadded:: 1.10.0 """ - -from abc import ABC, abstractmethod -from logging import getLogger -from os import environ -from threading import Lock -from typing import List, Optional, Sequence, Set, Tuple, Union, cast - -from opentelemetry._metrics.instrument import ( +from opentelemetry._metrics._internal import ( + Meter, + MeterProvider, + NoOpMeter, + NoOpMeterProvider, + get_meter, + get_meter_provider, + set_meter_provider, +) +from opentelemetry._metrics._internal.instrument import ( + Asynchronous, CallbackT, Counter, Histogram, + Instrument, NoOpCounter, NoOpHistogram, NoOpObservableCounter, @@ -60,707 +62,61 @@ ObservableCounter, ObservableGauge, ObservableUpDownCounter, + Synchronous, UpDownCounter, - _ProxyCounter, - _ProxyHistogram, - _ProxyObservableCounter, - _ProxyObservableGauge, - _ProxyObservableUpDownCounter, - _ProxyUpDownCounter, ) -from opentelemetry.environment_variables import ( - _OTEL_PYTHON_METER_PROVIDER as OTEL_PYTHON_METER_PROVIDER, -) -from opentelemetry.util._once import Once -from opentelemetry.util._providers import _load_provider - -_logger = getLogger(__name__) - +from opentelemetry._metrics._internal.observation import Observation -ProxyInstrumentT = Union[ - _ProxyCounter, - _ProxyHistogram, - _ProxyObservableCounter, - _ProxyObservableGauge, - _ProxyObservableUpDownCounter, - _ProxyUpDownCounter, +for obj in [ + Counter, + Synchronous, + Asynchronous, + get_meter_provider, + get_meter, + Histogram, + Meter, + MeterProvider, + Instrument, + NoOpCounter, + NoOpHistogram, + NoOpMeter, + NoOpMeterProvider, + NoOpObservableCounter, + NoOpObservableGauge, + NoOpObservableUpDownCounter, + NoOpUpDownCounter, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + Observation, + set_meter_provider, + UpDownCounter, +]: + obj.__module__ = __name__ + +__all__ = [ + "MeterProvider", + "NoOpMeterProvider", + "Meter", + "Counter", + "NoOpCounter", + "UpDownCounter", + "NoOpUpDownCounter", + "Histogram", + "NoOpHistogram", + "ObservableCounter", + "NoOpObservableCounter", + "ObservableUpDownCounter", + "Instrument", + "Synchronous", + "Asynchronous", + "NoOpObservableGauge", + "ObservableGauge", + "NoOpObservableUpDownCounter", + "get_meter", + "get_meter_provider", + "set_meter_provider", + "Observation", + "CallbackT", + "NoOpMeter", ] - - -class MeterProvider(ABC): - """ - MeterProvider is the entry point of the API. It provides access to `Meter` instances. - """ - - @abstractmethod - def get_meter( - self, - name: str, - version: Optional[str] = None, - schema_url: Optional[str] = None, - ) -> "Meter": - """Returns a `Meter` for use by the given instrumentation library. - - For any two calls it is undefined whether the same or different - `Meter` instances are returned, even for different library names. - - This function may return different `Meter` types (e.g. a no-op meter - vs. a functional meter). - - Args: - name: The name of the instrumenting module. - ``__name__`` may not be used as this can result in - different meter names if the meters are in different files. - It is better to use a fixed string that can be imported where - needed and used consistently as the name of the meter. - - This should *not* be the name of the module that is - instrumented but the name of the module doing the instrumentation. - E.g., instead of ``"requests"``, use - ``"opentelemetry.instrumentation.requests"``. - - version: Optional. The version string of the - instrumenting library. Usually this should be the same as - ``pkg_resources.get_distribution(instrumenting_library_name).version``. - - schema_url: Optional. Specifies the Schema URL of the emitted telemetry. - """ - - -class NoOpMeterProvider(MeterProvider): - """The default MeterProvider used when no MeterProvider implementation is available.""" - - def get_meter( - self, - name: str, - version: Optional[str] = None, - schema_url: Optional[str] = None, - ) -> "Meter": - """Returns a NoOpMeter.""" - super().get_meter(name, version=version, schema_url=schema_url) - return NoOpMeter(name, version=version, schema_url=schema_url) - - -class _ProxyMeterProvider(MeterProvider): - def __init__(self) -> None: - self._lock = Lock() - self._meters: List[_ProxyMeter] = [] - self._real_meter_provider: Optional[MeterProvider] = None - - def get_meter( - self, - name: str, - version: Optional[str] = None, - schema_url: Optional[str] = None, - ) -> "Meter": - with self._lock: - if self._real_meter_provider is not None: - return self._real_meter_provider.get_meter( - name, version, schema_url - ) - - meter = _ProxyMeter(name, version=version, schema_url=schema_url) - self._meters.append(meter) - return meter - - def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: - with self._lock: - self._real_meter_provider = meter_provider - for meter in self._meters: - meter.on_set_meter_provider(meter_provider) - - -class Meter(ABC): - """Handles instrument creation. - - This class provides methods for creating instruments which are then - used to produce measurements. - """ - - def __init__( - self, - name: str, - version: Optional[str] = None, - schema_url: Optional[str] = None, - ) -> None: - super().__init__() - self._name = name - self._version = version - self._schema_url = schema_url - self._instrument_ids: Set[str] = set() - self._instrument_ids_lock = Lock() - - @property - def name(self) -> str: - """ - The name of the instrumenting module. - """ - return self._name - - @property - def version(self) -> Optional[str]: - """ - The version string of the instrumenting library. - """ - return self._version - - @property - def schema_url(self) -> Optional[str]: - """ - Specifies the Schema URL of the emitted telemetry - """ - return self._schema_url - - def _is_instrument_registered( - self, name: str, type_: type, unit: str, description: str - ) -> Tuple[bool, str]: - """ - Check if an instrument with the same name, type, unit and description - has been registered already. - - Returns a tuple. The first value is `True` if the instrument has been - registered already, `False` otherwise. The second value is the - instrument id. - """ - - instrument_id = ",".join( - [name.strip().lower(), type_.__name__, unit, description] - ) - - result = False - - with self._instrument_ids_lock: - if instrument_id in self._instrument_ids: - result = True - else: - self._instrument_ids.add(instrument_id) - - return (result, instrument_id) - - @abstractmethod - def create_counter( - self, - name: str, - unit: str = "", - description: str = "", - ) -> Counter: - """Creates a `Counter` instrument - - Args: - name: The name of the instrument to be created - unit: The unit for observations this instrument reports. For - example, ``By`` for bytes. UCUM units are recommended. - description: A description for this instrument and what it measures. - """ - - @abstractmethod - def create_up_down_counter( - self, - name: str, - unit: str = "", - description: str = "", - ) -> UpDownCounter: - """Creates an `UpDownCounter` instrument - - Args: - name: The name of the instrument to be created - unit: The unit for observations this instrument reports. For - example, ``By`` for bytes. UCUM units are recommended. - description: A description for this instrument and what it measures. - """ - - @abstractmethod - def create_observable_counter( - self, - name: str, - callbacks: Optional[Sequence[CallbackT]] = None, - unit: str = "", - description: str = "", - ) -> ObservableCounter: - """Creates an `ObservableCounter` instrument - - An observable counter observes a monotonically increasing count by - calling provided callbacks which returns multiple - :class:`~opentelemetry._metrics.observation.Observation`. - - For example, an observable counter could be used to report system CPU - time periodically. Here is a basic implementation:: - - def cpu_time_callback() -> Iterable[Observation]: - observations = [] - with open("/proc/stat") as procstat: - procstat.readline() # skip the first line - for line in procstat: - if not line.startswith("cpu"): break - cpu, *states = line.split() - observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) - observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) - observations.append(Observation(int(states[2]) // 100, {"cpu": cpu, "state": "system"})) - # ... other states - return observations - - meter.create_observable_counter( - "system.cpu.time", - callbacks=[cpu_time_callback], - unit="s", - description="CPU time" - ) - - To reduce memory usage, you can use generator callbacks instead of - building the full list:: - - def cpu_time_callback() -> Iterable[Observation]: - with open("/proc/stat") as procstat: - procstat.readline() # skip the first line - for line in procstat: - if not line.startswith("cpu"): break - cpu, *states = line.split() - yield Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"}) - yield Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"}) - # ... other states - - Alternatively, you can pass a sequence of generators directly instead - of a sequence of callbacks, which each should return iterables of - :class:`~opentelemetry._metrics.observation.Observation`:: - - def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observation]]: - while True: - observations = [] - with open("/proc/stat") as procstat: - procstat.readline() # skip the first line - for line in procstat: - if not line.startswith("cpu"): break - cpu, *states = line.split() - if "user" in states_to_include: - observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) - if "nice" in states_to_include: - observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) - # ... other states - yield observations - - meter.create_observable_counter( - "system.cpu.time", - callbacks=[cpu_time_callback({"user", "system"})], - unit="s", - description="CPU time" - ) - - Args: - name: The name of the instrument to be created - callbacks: A sequence of callbacks that return an iterable of - :class:`~opentelemetry._metrics.observation.Observation`. - Alternatively, can be a sequence of generators that each yields - iterables of - :class:`~opentelemetry._metrics.observation.Observation`. - unit: The unit for observations this instrument reports. For - example, ``By`` for bytes. UCUM units are recommended. - description: A description for this instrument and what it measures. - """ - - @abstractmethod - def create_histogram( - self, - name: str, - unit: str = "", - description: str = "", - ) -> Histogram: - """Creates a `opentelemetry._metrics.instrument.Histogram` instrument - - Args: - name: The name of the instrument to be created - unit: The unit for observations this instrument reports. For - example, ``By`` for bytes. UCUM units are recommended. - description: A description for this instrument and what it measures. - """ - - @abstractmethod - def create_observable_gauge( - self, - name: str, - callbacks: Optional[Sequence[CallbackT]] = None, - unit: str = "", - description: str = "", - ) -> ObservableGauge: - """Creates an `ObservableGauge` instrument - - Args: - name: The name of the instrument to be created - callbacks: A sequence of callbacks that return an iterable of - :class:`~opentelemetry._metrics.observation.Observation`. - Alternatively, can be a generator that yields iterables of - :class:`~opentelemetry._metrics.observation.Observation`. - unit: The unit for observations this instrument reports. For - example, ``By`` for bytes. UCUM units are recommended. - description: A description for this instrument and what it measures. - """ - - @abstractmethod - def create_observable_up_down_counter( - self, - name: str, - callbacks: Optional[Sequence[CallbackT]] = None, - unit: str = "", - description: str = "", - ) -> ObservableUpDownCounter: - """Creates an `ObservableUpDownCounter` instrument - - Args: - name: The name of the instrument to be created - callbacks: A sequence of callbacks that return an iterable of - :class:`~opentelemetry._metrics.observation.Observation`. - Alternatively, can be a generator that yields iterables of - :class:`~opentelemetry._metrics.observation.Observation`. - unit: The unit for observations this instrument reports. For - example, ``By`` for bytes. UCUM units are recommended. - description: A description for this instrument and what it measures. - """ - - -class _ProxyMeter(Meter): - def __init__( - self, - name: str, - version: Optional[str] = None, - schema_url: Optional[str] = None, - ) -> None: - super().__init__(name, version=version, schema_url=schema_url) - self._lock = Lock() - self._instruments: List[ProxyInstrumentT] = [] - self._real_meter: Optional[Meter] = None - - def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: - """Called when a real meter provider is set on the creating _ProxyMeterProvider - - Creates a real backing meter for this instance and notifies all created - instruments so they can create real backing instruments. - """ - real_meter = meter_provider.get_meter( - self._name, self._version, self._schema_url - ) - - with self._lock: - self._real_meter = real_meter - # notify all proxy instruments of the new meter so they can create - # real instruments to back themselves - for instrument in self._instruments: - instrument.on_meter_set(real_meter) - - def create_counter( - self, - name: str, - unit: str = "", - description: str = "", - ) -> Counter: - with self._lock: - if self._real_meter: - return self._real_meter.create_counter(name, unit, description) - proxy = _ProxyCounter(name, unit, description) - self._instruments.append(proxy) - return proxy - - def create_up_down_counter( - self, - name: str, - unit: str = "", - description: str = "", - ) -> UpDownCounter: - with self._lock: - if self._real_meter: - return self._real_meter.create_up_down_counter( - name, unit, description - ) - proxy = _ProxyUpDownCounter(name, unit, description) - self._instruments.append(proxy) - return proxy - - def create_observable_counter( - self, - name: str, - callbacks: Optional[Sequence[CallbackT]] = None, - unit: str = "", - description: str = "", - ) -> ObservableCounter: - with self._lock: - if self._real_meter: - return self._real_meter.create_observable_counter( - name, callbacks, unit, description - ) - proxy = _ProxyObservableCounter( - name, callbacks, unit=unit, description=description - ) - self._instruments.append(proxy) - return proxy - - def create_histogram( - self, - name: str, - unit: str = "", - description: str = "", - ) -> Histogram: - with self._lock: - if self._real_meter: - return self._real_meter.create_histogram( - name, unit, description - ) - proxy = _ProxyHistogram(name, unit, description) - self._instruments.append(proxy) - return proxy - - def create_observable_gauge( - self, - name: str, - callbacks: Optional[Sequence[CallbackT]] = None, - unit: str = "", - description: str = "", - ) -> ObservableGauge: - with self._lock: - if self._real_meter: - return self._real_meter.create_observable_gauge( - name, callbacks, unit, description - ) - proxy = _ProxyObservableGauge( - name, callbacks, unit=unit, description=description - ) - self._instruments.append(proxy) - return proxy - - def create_observable_up_down_counter( - self, - name: str, - callbacks: Optional[Sequence[CallbackT]] = None, - unit: str = "", - description: str = "", - ) -> ObservableUpDownCounter: - with self._lock: - if self._real_meter: - return self._real_meter.create_observable_up_down_counter( - name, - callbacks, - unit, - description, - ) - proxy = _ProxyObservableUpDownCounter( - name, callbacks, unit=unit, description=description - ) - self._instruments.append(proxy) - return proxy - - -class NoOpMeter(Meter): - """The default Meter used when no Meter implementation is available. - - All operations are no-op. - """ - - def create_counter( - self, - name: str, - unit: str = "", - description: str = "", - ) -> Counter: - """Returns a no-op Counter.""" - super().create_counter(name, unit=unit, description=description) - if self._is_instrument_registered( - name, NoOpCounter, unit, description - )[0]: - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - Counter.__name__, - unit, - description, - ) - return NoOpCounter(name, unit=unit, description=description) - - def create_up_down_counter( - self, - name: str, - unit: str = "", - description: str = "", - ) -> UpDownCounter: - """Returns a no-op UpDownCounter.""" - super().create_up_down_counter( - name, unit=unit, description=description - ) - if self._is_instrument_registered( - name, NoOpUpDownCounter, unit, description - )[0]: - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - UpDownCounter.__name__, - unit, - description, - ) - return NoOpUpDownCounter(name, unit=unit, description=description) - - def create_observable_counter( - self, - name: str, - callbacks: Optional[Sequence[CallbackT]] = None, - unit: str = "", - description: str = "", - ) -> ObservableCounter: - """Returns a no-op ObservableCounter.""" - super().create_observable_counter( - name, callbacks, unit=unit, description=description - ) - if self._is_instrument_registered( - name, NoOpObservableCounter, unit, description - )[0]: - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - ObservableCounter.__name__, - unit, - description, - ) - return NoOpObservableCounter( - name, - callbacks, - unit=unit, - description=description, - ) - - def create_histogram( - self, - name: str, - unit: str = "", - description: str = "", - ) -> Histogram: - """Returns a no-op Histogram.""" - super().create_histogram(name, unit=unit, description=description) - if self._is_instrument_registered( - name, NoOpHistogram, unit, description - )[0]: - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - Histogram.__name__, - unit, - description, - ) - return NoOpHistogram(name, unit=unit, description=description) - - def create_observable_gauge( - self, - name: str, - callbacks: Optional[Sequence[CallbackT]] = None, - unit: str = "", - description: str = "", - ) -> ObservableGauge: - """Returns a no-op ObservableGauge.""" - super().create_observable_gauge( - name, callbacks, unit=unit, description=description - ) - if self._is_instrument_registered( - name, NoOpObservableGauge, unit, description - )[0]: - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - ObservableGauge.__name__, - unit, - description, - ) - return NoOpObservableGauge( - name, - callbacks, - unit=unit, - description=description, - ) - - def create_observable_up_down_counter( - self, - name: str, - callbacks: Optional[Sequence[CallbackT]] = None, - unit: str = "", - description: str = "", - ) -> ObservableUpDownCounter: - """Returns a no-op ObservableUpDownCounter.""" - super().create_observable_up_down_counter( - name, callbacks, unit=unit, description=description - ) - if self._is_instrument_registered( - name, NoOpObservableUpDownCounter, unit, description - )[0]: - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - ObservableUpDownCounter.__name__, - unit, - description, - ) - return NoOpObservableUpDownCounter( - name, - callbacks, - unit=unit, - description=description, - ) - - -_METER_PROVIDER_SET_ONCE = Once() -_METER_PROVIDER: Optional[MeterProvider] = None -_PROXY_METER_PROVIDER = _ProxyMeterProvider() - - -def get_meter( - name: str, - version: str = "", - meter_provider: Optional[MeterProvider] = None, -) -> "Meter": - """Returns a `Meter` for use by the given instrumentation library. - - This function is a convenience wrapper for - opentelemetry.trace.MeterProvider.get_meter. - - If meter_provider is omitted the current configured one is used. - """ - if meter_provider is None: - meter_provider = get_meter_provider() - return meter_provider.get_meter(name, version) - - -def _set_meter_provider(meter_provider: MeterProvider, log: bool) -> None: - def set_mp() -> None: - global _METER_PROVIDER # pylint: disable=global-statement - _METER_PROVIDER = meter_provider - - # gives all proxies real instruments off the newly set meter provider - _PROXY_METER_PROVIDER.on_set_meter_provider(meter_provider) - - did_set = _METER_PROVIDER_SET_ONCE.do_once(set_mp) - - if log and not did_set: - _logger.warning("Overriding of current MeterProvider is not allowed") - - -def set_meter_provider(meter_provider: MeterProvider) -> None: - """Sets the current global :class:`~.MeterProvider` object. - - This can only be done once, a warning will be logged if any furter attempt - is made. - """ - _set_meter_provider(meter_provider, log=True) - - -def get_meter_provider() -> MeterProvider: - """Gets the current global :class:`~.MeterProvider` object.""" - - if _METER_PROVIDER is None: - if OTEL_PYTHON_METER_PROVIDER not in environ.keys(): - return _PROXY_METER_PROVIDER - - meter_provider: MeterProvider = _load_provider( # type: ignore - OTEL_PYTHON_METER_PROVIDER, "meter_provider" - ) - _set_meter_provider(meter_provider, log=False) - - # _METER_PROVIDER will have been set by one thread - return cast("MeterProvider", _METER_PROVIDER) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/_internal/__init__.py new file mode 100644 index 0000000000..f7203859f0 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/_metrics/_internal/__init__.py @@ -0,0 +1,760 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=too-many-ancestors + +""" +The OpenTelemetry metrics API describes the classes used to generate +metrics. + +The :class:`.MeterProvider` provides users access to the :class:`.Meter` which in +turn is used to create :class:`.Instrument` objects. The :class:`.Instrument` objects are +used to record measurements. + +This module provides abstract (i.e. unimplemented) classes required for +metrics, and a concrete no-op implementation :class:`.NoOpMeter` that allows applications +to use the API package alone without a supporting implementation. + +To get a meter, you need to provide the package name from which you are +calling the meter APIs to OpenTelemetry by calling `MeterProvider.get_meter` +with the calling instrumentation name and the version of your package. + +The following code shows how to obtain a meter using the global :class:`.MeterProvider`:: + + from opentelemetry._metrics import get_meter + + meter = get_meter("example-meter") + counter = meter.create_counter("example-counter") + +.. versionadded:: 1.10.0 +""" + + +from abc import ABC, abstractmethod +from logging import getLogger +from os import environ +from threading import Lock +from typing import List, Optional, Sequence, Set, Tuple, Union, cast + +from opentelemetry._metrics._internal.instrument import ( + CallbackT, + Counter, + Histogram, + NoOpCounter, + NoOpHistogram, + NoOpObservableCounter, + NoOpObservableGauge, + NoOpObservableUpDownCounter, + NoOpUpDownCounter, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, + _ProxyCounter, + _ProxyHistogram, + _ProxyObservableCounter, + _ProxyObservableGauge, + _ProxyObservableUpDownCounter, + _ProxyUpDownCounter, +) +from opentelemetry.environment_variables import ( + _OTEL_PYTHON_METER_PROVIDER as OTEL_PYTHON_METER_PROVIDER, +) +from opentelemetry.util._once import Once +from opentelemetry.util._providers import _load_provider + +_logger = getLogger(__name__) + + +_ProxyInstrumentT = Union[ + _ProxyCounter, + _ProxyHistogram, + _ProxyObservableCounter, + _ProxyObservableGauge, + _ProxyObservableUpDownCounter, + _ProxyUpDownCounter, +] + + +class MeterProvider(ABC): + """ + MeterProvider is the entry point of the API. It provides access to `Meter` instances. + """ + + @abstractmethod + def get_meter( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> "Meter": + """Returns a `Meter` for use by the given instrumentation library. + + For any two calls it is undefined whether the same or different + `Meter` instances are returned, even for different library names. + + This function may return different `Meter` types (e.g. a no-op meter + vs. a functional meter). + + Args: + name: The name of the instrumenting module. + ``__name__`` may not be used as this can result in + different meter names if the meters are in different files. + It is better to use a fixed string that can be imported where + needed and used consistently as the name of the meter. + + This should *not* be the name of the module that is + instrumented but the name of the module doing the instrumentation. + E.g., instead of ``"requests"``, use + ``"opentelemetry.instrumentation.requests"``. + + version: Optional. The version string of the + instrumenting library. Usually this should be the same as + ``pkg_resources.get_distribution(instrumenting_library_name).version``. + + schema_url: Optional. Specifies the Schema URL of the emitted telemetry. + """ + + +class NoOpMeterProvider(MeterProvider): + """The default MeterProvider used when no MeterProvider implementation is available.""" + + def get_meter( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> "Meter": + """Returns a NoOpMeter.""" + super().get_meter(name, version=version, schema_url=schema_url) + return NoOpMeter(name, version=version, schema_url=schema_url) + + +class _ProxyMeterProvider(MeterProvider): + def __init__(self) -> None: + self._lock = Lock() + self._meters: List[_ProxyMeter] = [] + self._real_meter_provider: Optional[MeterProvider] = None + + def get_meter( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> "Meter": + with self._lock: + if self._real_meter_provider is not None: + return self._real_meter_provider.get_meter( + name, version, schema_url + ) + + meter = _ProxyMeter(name, version=version, schema_url=schema_url) + self._meters.append(meter) + return meter + + def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: + with self._lock: + self._real_meter_provider = meter_provider + for meter in self._meters: + meter.on_set_meter_provider(meter_provider) + + +class Meter(ABC): + """Handles instrument creation. + + This class provides methods for creating instruments which are then + used to produce measurements. + """ + + def __init__( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> None: + super().__init__() + self._name = name + self._version = version + self._schema_url = schema_url + self._instrument_ids: Set[str] = set() + self._instrument_ids_lock = Lock() + + @property + def name(self) -> str: + """ + The name of the instrumenting module. + """ + return self._name + + @property + def version(self) -> Optional[str]: + """ + The version string of the instrumenting library. + """ + return self._version + + @property + def schema_url(self) -> Optional[str]: + """ + Specifies the Schema URL of the emitted telemetry + """ + return self._schema_url + + def _is_instrument_registered( + self, name: str, type_: type, unit: str, description: str + ) -> Tuple[bool, str]: + """ + Check if an instrument with the same name, type, unit and description + has been registered already. + + Returns a tuple. The first value is `True` if the instrument has been + registered already, `False` otherwise. The second value is the + instrument id. + """ + + instrument_id = ",".join( + [name.strip().lower(), type_.__name__, unit, description] + ) + + result = False + + with self._instrument_ids_lock: + if instrument_id in self._instrument_ids: + result = True + else: + self._instrument_ids.add(instrument_id) + + return (result, instrument_id) + + @abstractmethod + def create_counter( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Counter: + """Creates a `Counter` instrument + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + + @abstractmethod + def create_up_down_counter( + self, + name: str, + unit: str = "", + description: str = "", + ) -> UpDownCounter: + """Creates an `UpDownCounter` instrument + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + + @abstractmethod + def create_observable_counter( + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> ObservableCounter: + """Creates an `ObservableCounter` instrument + + An observable counter observes a monotonically increasing count by calling provided + callbacks which returns multiple :class:`~opentelemetry._metrics.Observation`. + + For example, an observable counter could be used to report system CPU + time periodically. Here is a basic implementation:: + + def cpu_time_callback() -> Iterable[Observation]: + observations = [] + with open("/proc/stat") as procstat: + procstat.readline() # skip the first line + for line in procstat: + if not line.startswith("cpu"): break + cpu, *states = line.split() + observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) + observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) + observations.append(Observation(int(states[2]) // 100, {"cpu": cpu, "state": "system"})) + # ... other states + return observations + + meter.create_observable_counter( + "system.cpu.time", + callbacks=[cpu_time_callback], + unit="s", + description="CPU time" + ) + + To reduce memory usage, you can use generator callbacks instead of + building the full list:: + + def cpu_time_callback() -> Iterable[Observation]: + with open("/proc/stat") as procstat: + procstat.readline() # skip the first line + for line in procstat: + if not line.startswith("cpu"): break + cpu, *states = line.split() + yield Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"}) + yield Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"}) + # ... other states + + Alternatively, you can pass a sequence of generators directly instead of a sequence of + callbacks, which each should return iterables of :class:`~opentelemetry._metrics.Observation`:: + + def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observation]]: + while True: + observations = [] + with open("/proc/stat") as procstat: + procstat.readline() # skip the first line + for line in procstat: + if not line.startswith("cpu"): break + cpu, *states = line.split() + if "user" in states_to_include: + observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) + if "nice" in states_to_include: + observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) + # ... other states + yield observations + + meter.create_observable_counter( + "system.cpu.time", + callbacks=[cpu_time_callback({"user", "system"})], + unit="s", + description="CPU time" + ) + + Args: + name: The name of the instrument to be created + callbacks: A sequence of callbacks that return an iterable of + :class:`~opentelemetry._metrics.Observation`. Alternatively, can be a sequence of generators that each + yields iterables of :class:`~opentelemetry._metrics.Observation`. + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + + @abstractmethod + def create_histogram( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Histogram: + """Creates a :class:`~opentelemetry._metrics.Histogram` instrument + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + + @abstractmethod + def create_observable_gauge( + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> ObservableGauge: + """Creates an `ObservableGauge` instrument + + Args: + name: The name of the instrument to be created + callbacks: A sequence of callbacks that return an iterable of + :class:`~opentelemetry._metrics.Observation`. Alternatively, can be a generator that yields iterables + of :class:`~opentelemetry._metrics.Observation`. + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + + @abstractmethod + def create_observable_up_down_counter( + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> ObservableUpDownCounter: + """Creates an `ObservableUpDownCounter` instrument + + Args: + name: The name of the instrument to be created + callbacks: A sequence of callbacks that return an iterable of + :class:`~opentelemetry._metrics.Observation`. Alternatively, can be a generator that yields iterables + of :class:`~opentelemetry._metrics.Observation`. + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + + +class _ProxyMeter(Meter): + def __init__( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> None: + super().__init__(name, version=version, schema_url=schema_url) + self._lock = Lock() + self._instruments: List[_ProxyInstrumentT] = [] + self._real_meter: Optional[Meter] = None + + def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: + """Called when a real meter provider is set on the creating _ProxyMeterProvider + + Creates a real backing meter for this instance and notifies all created + instruments so they can create real backing instruments. + """ + real_meter = meter_provider.get_meter( + self._name, self._version, self._schema_url + ) + + with self._lock: + self._real_meter = real_meter + # notify all proxy instruments of the new meter so they can create + # real instruments to back themselves + for instrument in self._instruments: + instrument.on_meter_set(real_meter) + + def create_counter( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Counter: + with self._lock: + if self._real_meter: + return self._real_meter.create_counter(name, unit, description) + proxy = _ProxyCounter(name, unit, description) + self._instruments.append(proxy) + return proxy + + def create_up_down_counter( + self, + name: str, + unit: str = "", + description: str = "", + ) -> UpDownCounter: + with self._lock: + if self._real_meter: + return self._real_meter.create_up_down_counter( + name, unit, description + ) + proxy = _ProxyUpDownCounter(name, unit, description) + self._instruments.append(proxy) + return proxy + + def create_observable_counter( + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> ObservableCounter: + with self._lock: + if self._real_meter: + return self._real_meter.create_observable_counter( + name, callbacks, unit, description + ) + proxy = _ProxyObservableCounter( + name, callbacks, unit=unit, description=description + ) + self._instruments.append(proxy) + return proxy + + def create_histogram( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Histogram: + with self._lock: + if self._real_meter: + return self._real_meter.create_histogram( + name, unit, description + ) + proxy = _ProxyHistogram(name, unit, description) + self._instruments.append(proxy) + return proxy + + def create_observable_gauge( + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> ObservableGauge: + with self._lock: + if self._real_meter: + return self._real_meter.create_observable_gauge( + name, callbacks, unit, description + ) + proxy = _ProxyObservableGauge( + name, callbacks, unit=unit, description=description + ) + self._instruments.append(proxy) + return proxy + + def create_observable_up_down_counter( + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> ObservableUpDownCounter: + with self._lock: + if self._real_meter: + return self._real_meter.create_observable_up_down_counter( + name, + callbacks, + unit, + description, + ) + proxy = _ProxyObservableUpDownCounter( + name, callbacks, unit=unit, description=description + ) + self._instruments.append(proxy) + return proxy + + +class NoOpMeter(Meter): + """The default Meter used when no Meter implementation is available. + + All operations are no-op. + """ + + def create_counter( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Counter: + """Returns a no-op Counter.""" + super().create_counter(name, unit=unit, description=description) + if self._is_instrument_registered( + name, NoOpCounter, unit, description + )[0]: + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + Counter.__name__, + unit, + description, + ) + return NoOpCounter(name, unit=unit, description=description) + + def create_up_down_counter( + self, + name: str, + unit: str = "", + description: str = "", + ) -> UpDownCounter: + """Returns a no-op UpDownCounter.""" + super().create_up_down_counter( + name, unit=unit, description=description + ) + if self._is_instrument_registered( + name, NoOpUpDownCounter, unit, description + )[0]: + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + UpDownCounter.__name__, + unit, + description, + ) + return NoOpUpDownCounter(name, unit=unit, description=description) + + def create_observable_counter( + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> ObservableCounter: + """Returns a no-op ObservableCounter.""" + super().create_observable_counter( + name, callbacks, unit=unit, description=description + ) + if self._is_instrument_registered( + name, NoOpObservableCounter, unit, description + )[0]: + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + ObservableCounter.__name__, + unit, + description, + ) + return NoOpObservableCounter( + name, + callbacks, + unit=unit, + description=description, + ) + + def create_histogram( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Histogram: + """Returns a no-op Histogram.""" + super().create_histogram(name, unit=unit, description=description) + if self._is_instrument_registered( + name, NoOpHistogram, unit, description + )[0]: + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + Histogram.__name__, + unit, + description, + ) + return NoOpHistogram(name, unit=unit, description=description) + + def create_observable_gauge( + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> ObservableGauge: + """Returns a no-op ObservableGauge.""" + super().create_observable_gauge( + name, callbacks, unit=unit, description=description + ) + if self._is_instrument_registered( + name, NoOpObservableGauge, unit, description + )[0]: + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + ObservableGauge.__name__, + unit, + description, + ) + return NoOpObservableGauge( + name, + callbacks, + unit=unit, + description=description, + ) + + def create_observable_up_down_counter( + self, + name: str, + callbacks: Optional[Sequence[CallbackT]] = None, + unit: str = "", + description: str = "", + ) -> ObservableUpDownCounter: + """Returns a no-op ObservableUpDownCounter.""" + super().create_observable_up_down_counter( + name, callbacks, unit=unit, description=description + ) + if self._is_instrument_registered( + name, NoOpObservableUpDownCounter, unit, description + )[0]: + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + ObservableUpDownCounter.__name__, + unit, + description, + ) + return NoOpObservableUpDownCounter( + name, + callbacks, + unit=unit, + description=description, + ) + + +_METER_PROVIDER_SET_ONCE = Once() +_METER_PROVIDER: Optional[MeterProvider] = None +_PROXY_METER_PROVIDER = _ProxyMeterProvider() + + +def get_meter( + name: str, + version: str = "", + meter_provider: Optional[MeterProvider] = None, +) -> "Meter": + """Returns a `Meter` for use by the given instrumentation library. + + This function is a convenience wrapper for + `opentelemetry._metrics.MeterProvider.get_meter`. + + If meter_provider is omitted the current configured one is used. + """ + if meter_provider is None: + meter_provider = get_meter_provider() + return meter_provider.get_meter(name, version) + + +def _set_meter_provider(meter_provider: MeterProvider, log: bool) -> None: + def set_mp() -> None: + global _METER_PROVIDER # pylint: disable=global-statement + _METER_PROVIDER = meter_provider + + # gives all proxies real instruments off the newly set meter provider + _PROXY_METER_PROVIDER.on_set_meter_provider(meter_provider) + + did_set = _METER_PROVIDER_SET_ONCE.do_once(set_mp) + + if log and not did_set: + _logger.warning("Overriding of current MeterProvider is not allowed") + + +def set_meter_provider(meter_provider: MeterProvider) -> None: + """Sets the current global :class:`~.MeterProvider` object. + + This can only be done once, a warning will be logged if any furter attempt + is made. + """ + _set_meter_provider(meter_provider, log=True) + + +def get_meter_provider() -> MeterProvider: + """Gets the current global :class:`~.MeterProvider` object.""" + + if _METER_PROVIDER is None: + if OTEL_PYTHON_METER_PROVIDER not in environ.keys(): + return _PROXY_METER_PROVIDER + + meter_provider: MeterProvider = _load_provider( # type: ignore + OTEL_PYTHON_METER_PROVIDER, "meter_provider" + ) + _set_meter_provider(meter_provider, log=False) + + # _METER_PROVIDER will have been set by one thread + return cast("MeterProvider", _METER_PROVIDER) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py similarity index 95% rename from opentelemetry-api/src/opentelemetry/_metrics/instrument.py rename to opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py index ed70f2ddc7..04179524cc 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py @@ -30,7 +30,7 @@ # pylint: disable=unused-import; needed for typing and sphinx from opentelemetry import _metrics as metrics -from opentelemetry._metrics.observation import Observation +from opentelemetry._metrics._internal.observation import Observation from opentelemetry.util.types import Attributes InstrumentT = TypeVar("InstrumentT", bound="Instrument") @@ -115,23 +115,7 @@ def __init__( super().__init__(name, unit=unit, description=description) -class _Adding(Instrument): - pass - - -class _Grouping(Instrument): - pass - - -class _Monotonic(_Adding): - pass - - -class _NonMonotonic(_Adding): - pass - - -class Counter(_Monotonic, Synchronous): +class Counter(Synchronous): """A Counter is a synchronous `Instrument` which supports non-negative increments.""" @abstractmethod @@ -176,7 +160,7 @@ def _create_real_instrument(self, meter: "metrics.Meter") -> Counter: return meter.create_counter(self._name, self._unit, self._description) -class UpDownCounter(_NonMonotonic, Synchronous): +class UpDownCounter(Synchronous): """An UpDownCounter is a synchronous `Instrument` which supports increments and decrements.""" @abstractmethod @@ -222,7 +206,7 @@ def _create_real_instrument(self, meter: "metrics.Meter") -> UpDownCounter: ) -class ObservableCounter(_Monotonic, Asynchronous): +class ObservableCounter(Asynchronous): """An ObservableCounter is an asynchronous `Instrument` which reports monotonically increasing value(s) when the instrument is being observed. """ @@ -252,7 +236,7 @@ def _create_real_instrument( ) -class ObservableUpDownCounter(_NonMonotonic, Asynchronous): +class ObservableUpDownCounter(Asynchronous): """An ObservableUpDownCounter is an asynchronous `Instrument` which reports additive value(s) (e.g. the process heap size - it makes sense to report the heap size from multiple processes and sum them up, so we get the total heap usage) when the instrument is being observed. @@ -284,7 +268,7 @@ def _create_real_instrument( ) -class Histogram(_Grouping, Synchronous): +class Histogram(Synchronous): """Histogram is a synchronous `Instrument` which can be used to report arbitrary values that are likely to be statistically meaningful. It is intended for statistics such as histograms, summaries, and percentile. @@ -333,7 +317,7 @@ def _create_real_instrument(self, meter: "metrics.Meter") -> Histogram: ) -class ObservableGauge(_Grouping, Asynchronous): +class ObservableGauge(Asynchronous): """Asynchronous Gauge is an asynchronous `Instrument` which reports non-additive value(s) (e.g. the room temperature - it makes no sense to report the temperature value from multiple rooms and sum them up) when the instrument is being observed. diff --git a/opentelemetry-api/src/opentelemetry/_metrics/observation.py b/opentelemetry-api/src/opentelemetry/_metrics/_internal/observation.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/_metrics/observation.py rename to opentelemetry-api/src/opentelemetry/_metrics/_internal/observation.py diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index 8207970040..30791fa212 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -16,13 +16,14 @@ from inspect import Signature, isabstract, signature from unittest import TestCase -from opentelemetry._metrics import Meter, NoOpMeter -from opentelemetry._metrics.instrument import ( +from opentelemetry._metrics import ( Counter, Histogram, Instrument, + Meter, NoOpCounter, NoOpHistogram, + NoOpMeter, NoOpUpDownCounter, ObservableCounter, ObservableGauge, diff --git a/opentelemetry-api/tests/metrics/test_meter_provider.py b/opentelemetry-api/tests/metrics/test_meter_provider.py index 9efcfeb33a..ef49ddaf87 100644 --- a/opentelemetry-api/tests/metrics/test_meter_provider.py +++ b/opentelemetry-api/tests/metrics/test_meter_provider.py @@ -18,16 +18,16 @@ from pytest import fixture +import opentelemetry._metrics._internal as metrics_internal from opentelemetry import _metrics as metrics from opentelemetry._metrics import ( NoOpMeter, NoOpMeterProvider, - _ProxyMeter, - _ProxyMeterProvider, get_meter_provider, set_meter_provider, ) -from opentelemetry._metrics.instrument import ( +from opentelemetry._metrics._internal import _ProxyMeter, _ProxyMeterProvider +from opentelemetry._metrics._internal.instrument import ( _ProxyCounter, _ProxyHistogram, _ProxyObservableCounter, @@ -48,8 +48,10 @@ @fixture def reset_meter_provider(): + print(f"calling reset_metrics_globals() {reset_metrics_globals}") reset_metrics_globals() yield + print("teardown - calling reset_metrics_globals()") reset_metrics_globals() @@ -60,18 +62,19 @@ def test_set_meter_provider(reset_meter_provider): mock = Mock() - assert metrics._METER_PROVIDER is None + assert metrics_internal._METER_PROVIDER is None set_meter_provider(mock) - assert metrics._METER_PROVIDER is mock + assert metrics_internal._METER_PROVIDER is mock assert get_meter_provider() is mock def test_set_meter_provider_calls_proxy_provider(reset_meter_provider): with patch( - "opentelemetry._metrics._PROXY_METER_PROVIDER" + "opentelemetry._metrics._internal._PROXY_METER_PROVIDER" ) as mock_proxy_mp: + assert metrics_internal._PROXY_METER_PROVIDER is mock_proxy_mp mock_real_mp = Mock() set_meter_provider(mock_real_mp) mock_proxy_mp.on_set_meter_provider.assert_called_once_with( @@ -84,7 +87,7 @@ def test_get_meter_provider(reset_meter_provider): Test that the API provides a way to get a global default MeterProvider """ - assert metrics._METER_PROVIDER is None + assert metrics_internal._METER_PROVIDER is None assert isinstance(get_meter_provider(), _ProxyMeterProvider) @@ -94,9 +97,9 @@ def test_get_meter_provider(reset_meter_provider): "os.environ", {OTEL_PYTHON_METER_PROVIDER: "test_meter_provider"} ): - with patch("opentelemetry._metrics._load_provider", Mock()): + with patch("opentelemetry._metrics._internal._load_provider", Mock()): with patch( - "opentelemetry._metrics.cast", + "opentelemetry._metrics._internal.cast", Mock(**{"return_value": "test_meter_provider"}), ): assert get_meter_provider() == "test_meter_provider" diff --git a/opentelemetry-api/tests/metrics/test_measurement.py b/opentelemetry-api/tests/metrics/test_observation.py similarity index 96% rename from opentelemetry-api/tests/metrics/test_measurement.py rename to opentelemetry-api/tests/metrics/test_observation.py index 295c5b9a6b..05c644dbd3 100644 --- a/opentelemetry-api/tests/metrics/test_measurement.py +++ b/opentelemetry-api/tests/metrics/test_observation.py @@ -14,7 +14,7 @@ from unittest import TestCase -from opentelemetry._metrics.observation import Observation +from opentelemetry._metrics import Observation class TestObservation(TestCase): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 886b07aead..ff495213ed 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -17,21 +17,17 @@ from threading import Lock from typing import Optional, Sequence +from opentelemetry._metrics import Counter as APICounter +from opentelemetry._metrics import Histogram as APIHistogram from opentelemetry._metrics import Meter as APIMeter from opentelemetry._metrics import MeterProvider as APIMeterProvider from opentelemetry._metrics import NoOpMeter -from opentelemetry._metrics.instrument import Counter as APICounter -from opentelemetry._metrics.instrument import Histogram as APIHistogram -from opentelemetry._metrics.instrument import ( - ObservableCounter as APIObservableCounter, -) -from opentelemetry._metrics.instrument import ( - ObservableGauge as APIObservableGauge, -) -from opentelemetry._metrics.instrument import ( +from opentelemetry._metrics import ObservableCounter as APIObservableCounter +from opentelemetry._metrics import ObservableGauge as APIObservableGauge +from opentelemetry._metrics import ( ObservableUpDownCounter as APIObservableUpDownCounter, ) -from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter +from opentelemetry._metrics import UpDownCounter as APIUpDownCounter from opentelemetry.sdk._metrics.instrument import ( Counter, Histogram, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index 76accd205b..f977925c83 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -25,7 +25,7 @@ from threading import Lock from typing import Generic, List, Optional, Sequence, TypeVar -from opentelemetry._metrics.instrument import ( +from opentelemetry._metrics import ( Asynchronous, Counter, Histogram, @@ -35,7 +35,6 @@ ObservableUpDownCounter, Synchronous, UpDownCounter, - _Monotonic, ) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import AggregationTemporality, Gauge @@ -89,7 +88,7 @@ class DefaultAggregation(_AggregationFactory): `UpDownCounter` `SumAggregation` `ObservableCounter` `SumAggregation` `ObservableUpDownCounter` `SumAggregation` - `opentelemetry._metrics.instrument.Histogram` `ExplicitBucketHistogramAggregation` + `opentelemetry._metrics.Histogram` `ExplicitBucketHistogramAggregation` `ObservableGauge` `LastValueAggregation` ============================================= ==================================== """ @@ -473,7 +472,7 @@ def _create_aggregation(self, instrument: Instrument) -> _Aggregation: temporality = AggregationTemporality.CUMULATIVE return _SumAggregation( - isinstance(instrument, _Monotonic), + isinstance(instrument, (Counter, ObservableCounter)), temporality, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index 92de877550..8d383e5402 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -17,19 +17,15 @@ from logging import getLogger from typing import TYPE_CHECKING, Dict, Generator, Iterable, Optional, Union -from opentelemetry._metrics.instrument import CallbackT -from opentelemetry._metrics.instrument import Counter as APICounter -from opentelemetry._metrics.instrument import Histogram as APIHistogram -from opentelemetry._metrics.instrument import ( - ObservableCounter as APIObservableCounter, -) -from opentelemetry._metrics.instrument import ( - ObservableGauge as APIObservableGauge, -) -from opentelemetry._metrics.instrument import ( +from opentelemetry._metrics import CallbackT +from opentelemetry._metrics import Counter as APICounter +from opentelemetry._metrics import Histogram as APIHistogram +from opentelemetry._metrics import ObservableCounter as APIObservableCounter +from opentelemetry._metrics import ObservableGauge as APIObservableGauge +from opentelemetry._metrics import ( ObservableUpDownCounter as APIObservableUpDownCounter, ) -from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter +from opentelemetry._metrics import UpDownCounter as APIUpDownCounter from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk.util.instrumentation import InstrumentationScope diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py index d76457f41b..d2685391c1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py @@ -15,7 +15,7 @@ from threading import RLock from typing import Dict, Iterable, List -from opentelemetry._metrics.instrument import Instrument +from opentelemetry._metrics import Instrument from opentelemetry.sdk._metrics._view_instrument_match import ( _ViewInstrumentMatch, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py index 28adebe643..6bfde007bd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py @@ -20,7 +20,7 @@ # FIXME import from typing when support for 3.6 is removed from typing_extensions import final -from opentelemetry._metrics.instrument import Instrument +from opentelemetry._metrics import Instrument from opentelemetry.sdk._metrics.aggregation import ( DefaultAggregation, _AggregationFactory, diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py index e8c541c090..d1688e02d4 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py @@ -17,8 +17,7 @@ from typing import Generator, Iterable, List from unittest import TestCase -from opentelemetry._metrics.instrument import Instrument -from opentelemetry._metrics.observation import Observation +from opentelemetry._metrics import Instrument, Observation from opentelemetry.sdk._metrics import MeterProvider from opentelemetry.sdk._metrics.measurement import Measurement diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py index 3da776191a..58b2aad3e8 100644 --- a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py @@ -15,7 +15,7 @@ from unittest import TestCase from unittest.mock import Mock -from opentelemetry._metrics.observation import Observation +from opentelemetry._metrics import Observation from opentelemetry.sdk._metrics import MeterProvider from opentelemetry.sdk._metrics.export import InMemoryMetricReader from opentelemetry.sdk._metrics.point import ( diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py index ba792842a1..a86715023e 100644 --- a/opentelemetry-sdk/tests/metrics/test_instrument.py +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -15,7 +15,7 @@ from unittest import TestCase from unittest.mock import Mock -from opentelemetry._metrics.observation import Observation +from opentelemetry._metrics import Observation from opentelemetry.sdk._metrics.instrument import ( Counter, Histogram, diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py index f5e4ddaea3..e3d2ba731d 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py @@ -14,8 +14,9 @@ import unittest -from opentelemetry import _metrics as metrics_api from opentelemetry import trace as trace_api +from opentelemetry._metrics import _internal as metrics_api +from opentelemetry._metrics._internal import _ProxyMeterProvider from opentelemetry.util._once import Once @@ -32,7 +33,7 @@ def reset_metrics_globals() -> None: """WARNING: only use this for tests.""" metrics_api._METER_PROVIDER_SET_ONCE = Once() # type: ignore[attr-defined] metrics_api._METER_PROVIDER = None # type: ignore[attr-defined] - metrics_api._PROXY_METER_PROVIDER = metrics_api._ProxyMeterProvider() # type: ignore[attr-defined] + metrics_api._PROXY_METER_PROVIDER = _ProxyMeterProvider() # type: ignore[attr-defined] class TraceGlobalsTest(unittest.TestCase): From 5456988febc429a4f7fa32cc024b6c9714be9368 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 29 Apr 2022 10:42:03 -0600 Subject: [PATCH 1210/1517] Check instrument names and units (#2648) * Check instrument names and units Fixes #2647 * Update opentelemetry-api/src/opentelemetry/_metrics/instrument.py Co-authored-by: Srikanth Chekuri * Add missing type * Add missing type * Fix error message * Update opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py Co-authored-by: Srikanth Chekuri Co-authored-by: Srikanth Chekuri --- .../_metrics/_internal/instrument.py | 25 ++++++++++++++++--- .../tests/metrics/test_instruments.py | 22 ++++++++++++++++ .../opentelemetry/sdk/_metrics/instrument.py | 25 +++++++++++++++++-- .../tests/metrics/test_aggregation.py | 14 +++++------ .../tests/metrics/test_instrument.py | 8 ++++++ 5 files changed, 80 insertions(+), 14 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py index 04179524cc..254c03a5e6 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py @@ -17,6 +17,8 @@ from abc import ABC, abstractmethod from logging import getLogger +from re import ASCII +from re import compile as re_compile from typing import ( Callable, Generator, @@ -24,6 +26,7 @@ Iterable, Optional, Sequence, + Tuple, TypeVar, Union, ) @@ -42,6 +45,9 @@ _logger = getLogger(__name__) +_name_regex = re_compile(r"[a-zA-Z][-.\w]{0,62}", ASCII) +_unit_regex = re_compile(r"\w{0,63}", ASCII) + class Instrument(ABC): """Abstract class that serves as base for all instruments.""" @@ -55,9 +61,21 @@ def __init__( ) -> None: pass - # FIXME check that the instrument name is valid - # FIXME check that the unit is 63 characters or shorter - # FIXME check that the unit contains only ASCII characters + @staticmethod + def _check_name_and_unit(name: str, unit: str) -> Tuple[bool, bool]: + """ + Checks the following instrument name and unit for compliance with the + spec. + + Returns a tuple of boolean value, the first one will be `True` if the + name is valid, `False` otherwise. The second value will be `True` if + the unit is valid, `False` otherwise. + """ + + return ( + _name_regex.fullmatch(name) is not None, + _unit_regex.fullmatch(unit) is not None, + ) class _ProxyInstrument(ABC, Generic[InstrumentT]): @@ -124,7 +142,6 @@ def add( amount: Union[int, float], attributes: Optional[Attributes] = None, ) -> None: - # FIXME check that the amount is non negative pass diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index 30791fa212..42967d4901 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -562,3 +562,25 @@ def test_observable_up_down_counter_callback(self): ].default, Signature.empty, ) + + def test_name_regex(self): + + instrument = ChildInstrument("name") + + self.assertTrue(instrument._check_name_and_unit("a" * 63, "unit")[0]) + self.assertTrue(instrument._check_name_and_unit("a.", "unit")[0]) + self.assertTrue(instrument._check_name_and_unit("a-", "unit")[0]) + self.assertTrue(instrument._check_name_and_unit("a_", "unit")[0]) + self.assertFalse(instrument._check_name_and_unit("a" * 64, "unit")[0]) + self.assertFalse(instrument._check_name_and_unit("Ñ", "unit")[0]) + self.assertFalse(instrument._check_name_and_unit("_a", "unit")[0]) + self.assertFalse(instrument._check_name_and_unit("1a", "unit")[0]) + self.assertFalse(instrument._check_name_and_unit("", "unit")[0]) + + def test_unit_regex(self): + + instrument = ChildInstrument("name") + + self.assertTrue(instrument._check_name_and_unit("name", "a" * 63)[1]) + self.assertFalse(instrument._check_name_and_unit("name", "a" * 64)[1]) + self.assertFalse(instrument._check_name_and_unit("name", "Ñ")[1]) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index 8d383e5402..19902ac652 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -38,6 +38,11 @@ _logger = getLogger(__name__) +_ERROR_MESSAGE = ( + "Expected ASCII string of maximum length 63 characters but got {}" +) + + class _Synchronous: def __init__( self, @@ -47,7 +52,15 @@ def __init__( unit: str = "", description: str = "", ): - self.name = name + # pylint: disable=no-member + is_name_valid, is_unit_valid = self._check_name_and_unit(name, unit) + + if not is_name_valid: + raise Exception(_ERROR_MESSAGE.format(name)) + + if not is_unit_valid: + raise Exception(_ERROR_MESSAGE.format(unit)) + self.name = name.lower() self.unit = unit self.description = description self.instrumentation_scope = instrumentation_scope @@ -65,7 +78,15 @@ def __init__( unit: str = "", description: str = "", ): - self.name = name + # pylint: disable=no-member + is_name_valid, is_unit_valid = self._check_name_and_unit(name, unit) + + if not is_name_valid: + raise Exception(_ERROR_MESSAGE.format(name)) + + if not is_unit_valid: + raise Exception(_ERROR_MESSAGE.format(unit)) + self.name = name.lower() self.unit = unit self.description = description self.instrumentation_scope = instrumentation_scope diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index ad483a41d4..6f4582ddce 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -875,7 +875,7 @@ def setUpClass(cls): def test_counter(self): aggregation = self.default_aggregation._create_aggregation( - Counter(Mock(), Mock(), Mock()) + Counter("name", Mock(), Mock()) ) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) @@ -886,7 +886,7 @@ def test_counter(self): def test_up_down_counter(self): aggregation = self.default_aggregation._create_aggregation( - UpDownCounter(Mock(), Mock(), Mock()) + UpDownCounter("name", Mock(), Mock()) ) self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) @@ -897,7 +897,7 @@ def test_up_down_counter(self): def test_observable_counter(self): aggregation = self.default_aggregation._create_aggregation( - ObservableCounter(Mock(), Mock(), Mock(), callbacks=[Mock()]) + ObservableCounter("name", Mock(), Mock(), callbacks=[Mock()]) ) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) @@ -909,7 +909,7 @@ def test_observable_counter(self): def test_observable_up_down_counter(self): aggregation = self.default_aggregation._create_aggregation( - ObservableUpDownCounter(Mock(), Mock(), Mock(), callbacks=[Mock()]) + ObservableUpDownCounter("name", Mock(), Mock(), callbacks=[Mock()]) ) self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) @@ -922,9 +922,7 @@ def test_histogram(self): aggregation = self.default_aggregation._create_aggregation( Histogram( - Mock(), - Mock(), - Mock(), + "name", Mock(), Mock(), ) @@ -935,7 +933,7 @@ def test_observable_gauge(self): aggregation = self.default_aggregation._create_aggregation( ObservableGauge( - Mock(), + "name", Mock(), Mock(), callbacks=[Mock()], diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py index a86715023e..8b30791832 100644 --- a/opentelemetry-sdk/tests/metrics/test_instrument.py +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -28,6 +28,10 @@ class TestCounter(TestCase): + def testname(self): + self.assertEqual(Counter("name", Mock(), Mock()).name, "name") + self.assertEqual(Counter("Name", Mock(), Mock()).name, "name") + def test_add(self): mc = Mock() counter = Counter("name", Mock(), mc) @@ -91,6 +95,10 @@ def generator_callback_1(): class TestObservableGauge(TestCase): + def testname(self): + self.assertEqual(ObservableGauge("name", Mock(), Mock()).name, "name") + self.assertEqual(ObservableGauge("Name", Mock(), Mock()).name, "name") + def test_callable_callback_0(self): observable_gauge = ObservableGauge( "name", Mock(), Mock(), [callable_callback_0] From cdab6e174a1b9afc68aaf57ca04fc972a14281bc Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 2 May 2022 13:17:01 -0600 Subject: [PATCH 1211/1517] Make necessary SDK metrics symbols private (#2625) Fixes #2389 --- .../exporter/prometheus/__init__.py | 2 +- .../opentelemetry/sdk/_metrics/__init__.py | 439 +--------------- .../sdk/_metrics/_internal/__init__.py | 450 ++++++++++++++++ .../{ => _internal}/_view_instrument_match.py | 18 +- .../sdk/_metrics/_internal/aggregation.py | 495 ++++++++++++++++++ .../sdk/_metrics/_internal/export/__init__.py | 217 ++++++++ .../sdk/_metrics/_internal/instrument.py | 175 +++++++ .../sdk/_metrics/_internal/measurement.py | 32 ++ .../{ => _internal}/measurement_consumer.py | 13 +- .../sdk/_metrics/_internal/metric_reader.py | 179 +++++++ .../{ => _internal}/metric_reader_storage.py | 15 +- .../sdk/_metrics/_internal/point.py | 110 ++++ .../{ => _internal}/sdk_configuration.py | 8 +- .../sdk/_metrics/_internal/view.py | 159 ++++++ .../opentelemetry/sdk/_metrics/aggregation.py | 491 +---------------- .../sdk/_metrics/export/__init__.py | 214 +------- .../opentelemetry/sdk/_metrics/instrument.py | 173 +----- .../opentelemetry/sdk/_metrics/measurement.py | 25 +- .../sdk/_metrics/metric_reader.py | 170 +----- .../src/opentelemetry/sdk/_metrics/point.py | 117 +---- .../src/opentelemetry/sdk/_metrics/view.py | 152 +----- .../tests/metrics/test_aggregation.py | 24 +- .../metrics/test_measurement_consumer.py | 11 +- .../tests/metrics/test_metric_reader.py | 6 +- .../metrics/test_metric_reader_storage.py | 22 +- .../tests/metrics/test_metrics.py | 32 +- .../metrics/test_view_instrument_match.py | 12 +- 27 files changed, 2011 insertions(+), 1750 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py rename opentelemetry-sdk/src/opentelemetry/sdk/_metrics/{ => _internal}/_view_instrument_match.py (91%) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement.py rename opentelemetry-sdk/src/opentelemetry/sdk/_metrics/{ => _internal}/measurement_consumer.py (91%) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py rename opentelemetry-sdk/src/opentelemetry/sdk/_metrics/{ => _internal}/metric_reader_storage.py (91%) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py rename opentelemetry-sdk/src/opentelemetry/sdk/_metrics/{ => _internal}/sdk_configuration.py (63%) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/view.py diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 09ca38c57b..4d50662675 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -77,7 +77,7 @@ ) from prometheus_client.core import Metric as PrometheusMetric -from opentelemetry.sdk._metrics.export import MetricReader +from opentelemetry.sdk._metrics.metric_reader import MetricReader from opentelemetry.sdk._metrics.point import Gauge, Histogram, Metric, Sum _logger = getLogger(__name__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index ff495213ed..c3b45fff8c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -12,436 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from atexit import register, unregister -from logging import getLogger -from threading import Lock -from typing import Optional, Sequence +# pylint: disable=unused-import -from opentelemetry._metrics import Counter as APICounter -from opentelemetry._metrics import Histogram as APIHistogram -from opentelemetry._metrics import Meter as APIMeter -from opentelemetry._metrics import MeterProvider as APIMeterProvider -from opentelemetry._metrics import NoOpMeter -from opentelemetry._metrics import ObservableCounter as APIObservableCounter -from opentelemetry._metrics import ObservableGauge as APIObservableGauge -from opentelemetry._metrics import ( - ObservableUpDownCounter as APIObservableUpDownCounter, +from opentelemetry.sdk._metrics._internal import ( # noqa: F401 + Meter, + MeterProvider, ) -from opentelemetry._metrics import UpDownCounter as APIUpDownCounter -from opentelemetry.sdk._metrics.instrument import ( - Counter, - Histogram, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, -) -from opentelemetry.sdk._metrics.measurement_consumer import ( - MeasurementConsumer, - SynchronousMeasurementConsumer, -) -from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration -from opentelemetry.sdk._metrics.view import View -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.util._once import Once - -_logger = getLogger(__name__) - - -class Meter(APIMeter): - """See `opentelemetry._metrics.Meter`.""" - - def __init__( - self, - instrumentation_scope: InstrumentationScope, - measurement_consumer: MeasurementConsumer, - ): - super().__init__(instrumentation_scope) - self._instrumentation_scope = instrumentation_scope - self._measurement_consumer = measurement_consumer - self._instrument_id_instrument = {} - self._instrument_id_instrument_lock = Lock() - - def create_counter(self, name, unit="", description="") -> APICounter: - - ( - is_instrument_registered, - instrument_id, - ) = self._is_instrument_registered(name, Counter, unit, description) - - if is_instrument_registered: - # FIXME #2558 go through all views here and check if this - # instrument registration conflict can be fixed. If it can be, do - # not log the following warning. - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - APICounter.__name__, - unit, - description, - ) - with self._instrument_id_instrument_lock: - return self._instrument_id_instrument[instrument_id] - - instrument = Counter( - name, - self._instrumentation_scope, - self._measurement_consumer, - unit, - description, - ) - - with self._instrument_id_instrument_lock: - self._instrument_id_instrument[instrument_id] = instrument - return instrument - - def create_up_down_counter( - self, name, unit="", description="" - ) -> APIUpDownCounter: - - ( - is_instrument_registered, - instrument_id, - ) = self._is_instrument_registered( - name, UpDownCounter, unit, description - ) - - if is_instrument_registered: - # FIXME #2558 go through all views here and check if this - # instrument registration conflict can be fixed. If it can be, do - # not log the following warning. - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - APIUpDownCounter.__name__, - unit, - description, - ) - with self._instrument_id_instrument_lock: - return self._instrument_id_instrument[instrument_id] - - instrument = UpDownCounter( - name, - self._instrumentation_scope, - self._measurement_consumer, - unit, - description, - ) - - with self._instrument_id_instrument_lock: - self._instrument_id_instrument[instrument_id] = instrument - return instrument - - def create_observable_counter( - self, name, callbacks=None, unit="", description="" - ) -> APIObservableCounter: - - ( - is_instrument_registered, - instrument_id, - ) = self._is_instrument_registered( - name, ObservableCounter, unit, description - ) - - if is_instrument_registered: - # FIXME #2558 go through all views here and check if this - # instrument registration conflict can be fixed. If it can be, do - # not log the following warning. - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - APIObservableCounter.__name__, - unit, - description, - ) - with self._instrument_id_instrument_lock: - return self._instrument_id_instrument[instrument_id] - - instrument = ObservableCounter( - name, - self._instrumentation_scope, - self._measurement_consumer, - callbacks, - unit, - description, - ) - - self._measurement_consumer.register_asynchronous_instrument(instrument) - - with self._instrument_id_instrument_lock: - self._instrument_id_instrument[instrument_id] = instrument - return instrument - - def create_histogram(self, name, unit="", description="") -> APIHistogram: - - ( - is_instrument_registered, - instrument_id, - ) = self._is_instrument_registered(name, Histogram, unit, description) - - if is_instrument_registered: - # FIXME #2558 go through all views here and check if this - # instrument registration conflict can be fixed. If it can be, do - # not log the following warning. - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - APIHistogram.__name__, - unit, - description, - ) - with self._instrument_id_instrument_lock: - return self._instrument_id_instrument[instrument_id] - - instrument = Histogram( - name, - self._instrumentation_scope, - self._measurement_consumer, - unit, - description, - ) - with self._instrument_id_instrument_lock: - self._instrument_id_instrument[instrument_id] = instrument - return instrument - - def create_observable_gauge( - self, name, callbacks=None, unit="", description="" - ) -> APIObservableGauge: - - ( - is_instrument_registered, - instrument_id, - ) = self._is_instrument_registered( - name, ObservableGauge, unit, description - ) - - if is_instrument_registered: - # FIXME #2558 go through all views here and check if this - # instrument registration conflict can be fixed. If it can be, do - # not log the following warning. - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - APIObservableGauge.__name__, - unit, - description, - ) - with self._instrument_id_instrument_lock: - return self._instrument_id_instrument[instrument_id] - - instrument = ObservableGauge( - name, - self._instrumentation_scope, - self._measurement_consumer, - callbacks, - unit, - description, - ) - - self._measurement_consumer.register_asynchronous_instrument(instrument) - - with self._instrument_id_instrument_lock: - self._instrument_id_instrument[instrument_id] = instrument - return instrument - - def create_observable_up_down_counter( - self, name, callbacks=None, unit="", description="" - ) -> APIObservableUpDownCounter: - - ( - is_instrument_registered, - instrument_id, - ) = self._is_instrument_registered(name, Counter, unit, description) - - if is_instrument_registered: - # FIXME #2558 go through all views here and check if this - # instrument registration conflict can be fixed. If it can be, do - # not log the following warning. - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - APIObservableUpDownCounter.__name__, - unit, - description, - ) - with self._instrument_id_instrument_lock: - return self._instrument_id_instrument[instrument_id] - - instrument = ObservableUpDownCounter( - name, - self._instrumentation_scope, - self._measurement_consumer, - callbacks, - unit, - description, - ) - - self._measurement_consumer.register_asynchronous_instrument(instrument) - - with self._instrument_id_instrument_lock: - self._instrument_id_instrument[instrument_id] = instrument - return instrument - - -class MeterProvider(APIMeterProvider): - r"""See `opentelemetry._metrics.MeterProvider`. - - Args: - metric_readers: Register metric readers to collect metrics from the SDK on demand. Each - `MetricReader` is completely independent and will collect separate streams of - metrics. TODO: reference ``PeriodicExportingMetricReader`` usage with push - exporters here. - resource: The resource representing what the metrics emitted from the SDK pertain to. - shutdown_on_exit: If true, registers an `atexit` handler to call - `MeterProvider.shutdown` - views: The views to configure the metric output the SDK - - By default, instruments which do not match any `View` (or if no `View`\ s are provided) - will report metrics with the default aggregation for the instrument's kind. To disable - instruments by default, configure a match-all `View` with `DropAggregation` and then create - `View`\ s to re-enable individual instruments: - - .. code-block:: python - :caption: Disable default views - - MeterProvider( - views=[ - View(instrument_name="*", aggregation=DropAggregation()), - View(instrument_name="mycounter"), - ], - # ... - ) - """ - - _all_metric_readers_lock = Lock() - _all_metric_readers = set() - - def __init__( - self, - metric_readers: Sequence[MetricReader] = (), - resource: Resource = Resource.create({}), - shutdown_on_exit: bool = True, - views: Sequence[View] = (), - ): - self._lock = Lock() - self._meter_lock = Lock() - self._atexit_handler = None - self._sdk_config = SdkConfiguration( - resource=resource, - metric_readers=metric_readers, - views=views, - ) - self._measurement_consumer = SynchronousMeasurementConsumer( - sdk_config=self._sdk_config - ) - - if shutdown_on_exit: - self._atexit_handler = register(self.shutdown) - - self._meters = {} - - for metric_reader in self._sdk_config.metric_readers: - - with self._all_metric_readers_lock: - if metric_reader in self._all_metric_readers: - raise Exception( - f"MetricReader {metric_reader} has been registered " - "already in other MeterProvider instance" - ) - - self._all_metric_readers.add(metric_reader) - - metric_reader._set_collect_callback( - self._measurement_consumer.collect - ) - - self._shutdown_once = Once() - self._shutdown = False - - def force_flush(self) -> bool: - - # FIXME implement a timeout - - for metric_reader in self._sdk_config.metric_readers: - metric_reader.collect() - return True - - def shutdown(self): - # FIXME implement a timeout - - def _shutdown(): - self._shutdown = True - - did_shutdown = self._shutdown_once.do_once(_shutdown) - - if not did_shutdown: - _logger.warning("shutdown can only be called once") - return - - metric_reader_error = {} - - for metric_reader in self._sdk_config.metric_readers: - try: - metric_reader.shutdown() - - # pylint: disable=broad-except - except Exception as error: - - metric_reader_error[metric_reader] = error - - if self._atexit_handler is not None: - unregister(self._atexit_handler) - self._atexit_handler = None - - if metric_reader_error: - - metric_reader_error_string = "\n".join( - [ - f"{metric_reader.__class__.__name__}: {repr(error)}" - for metric_reader, error in metric_reader_error.items() - ] - ) - - raise Exception( - ( - "MeterProvider.shutdown failed because the following " - "metric readers failed during shutdown:\n" - f"{metric_reader_error_string}" - ) - ) - - def get_meter( - self, - name: str, - version: Optional[str] = None, - schema_url: Optional[str] = None, - ) -> Meter: - - if self._shutdown: - _logger.warning( - "A shutdown `MeterProvider` can not provide a `Meter`" - ) - return NoOpMeter(name, version=version, schema_url=schema_url) - - if not name: - _logger.warning("Meter name cannot be None or empty.") - return NoOpMeter(name, version=version, schema_url=schema_url) - info = InstrumentationScope(name, version, schema_url) - with self._meter_lock: - if not self._meters.get(info): - # FIXME #2558 pass SDKConfig object to meter so that the meter - # has access to views. - self._meters[info] = Meter( - info, - self._measurement_consumer, - ) - return self._meters[info] +__all__ = [] +for key, value in globals().copy().items(): + if not key.startswith("_"): + value.__module__ = __name__ + __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py new file mode 100644 index 0000000000..39256b7327 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py @@ -0,0 +1,450 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from atexit import register, unregister +from logging import getLogger +from threading import Lock +from typing import Optional, Sequence + +from opentelemetry._metrics import Counter as APICounter +from opentelemetry._metrics import Histogram as APIHistogram +from opentelemetry._metrics import Meter as APIMeter +from opentelemetry._metrics import MeterProvider as APIMeterProvider +from opentelemetry._metrics import NoOpMeter +from opentelemetry._metrics import ObservableCounter as APIObservableCounter +from opentelemetry._metrics import ObservableGauge as APIObservableGauge +from opentelemetry._metrics import ( + ObservableUpDownCounter as APIObservableUpDownCounter, +) +from opentelemetry._metrics import UpDownCounter as APIUpDownCounter +from opentelemetry.sdk._metrics._internal.measurement_consumer import ( + MeasurementConsumer, + SynchronousMeasurementConsumer, +) +from opentelemetry.sdk._metrics._internal.sdk_configuration import ( + SdkConfiguration, +) +from opentelemetry.sdk._metrics.instrument import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.sdk._metrics.metric_reader import MetricReader +from opentelemetry.sdk._metrics.view import View +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.util._once import Once + +_logger = getLogger(__name__) + + +class Meter(APIMeter): + """See `opentelemetry._metrics.Meter`.""" + + def __init__( + self, + instrumentation_scope: InstrumentationScope, + measurement_consumer: MeasurementConsumer, + ): + super().__init__(instrumentation_scope) + self._instrumentation_scope = instrumentation_scope + self._measurement_consumer = measurement_consumer + self._instrument_id_instrument = {} + self._instrument_id_instrument_lock = Lock() + + def create_counter(self, name, unit="", description="") -> APICounter: + + ( + is_instrument_registered, + instrument_id, + ) = self._is_instrument_registered(name, Counter, unit, description) + + if is_instrument_registered: + # FIXME #2558 go through all views here and check if this + # instrument registration conflict can be fixed. If it can be, do + # not log the following warning. + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + APICounter.__name__, + unit, + description, + ) + with self._instrument_id_instrument_lock: + return self._instrument_id_instrument[instrument_id] + + instrument = Counter( + name, + self._instrumentation_scope, + self._measurement_consumer, + unit, + description, + ) + + with self._instrument_id_instrument_lock: + self._instrument_id_instrument[instrument_id] = instrument + return instrument + + def create_up_down_counter( + self, name, unit="", description="" + ) -> APIUpDownCounter: + + ( + is_instrument_registered, + instrument_id, + ) = self._is_instrument_registered( + name, UpDownCounter, unit, description + ) + + if is_instrument_registered: + # FIXME #2558 go through all views here and check if this + # instrument registration conflict can be fixed. If it can be, do + # not log the following warning. + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + APIUpDownCounter.__name__, + unit, + description, + ) + with self._instrument_id_instrument_lock: + return self._instrument_id_instrument[instrument_id] + + instrument = UpDownCounter( + name, + self._instrumentation_scope, + self._measurement_consumer, + unit, + description, + ) + + with self._instrument_id_instrument_lock: + self._instrument_id_instrument[instrument_id] = instrument + return instrument + + def create_observable_counter( + self, name, callbacks=None, unit="", description="" + ) -> APIObservableCounter: + + ( + is_instrument_registered, + instrument_id, + ) = self._is_instrument_registered( + name, ObservableCounter, unit, description + ) + + if is_instrument_registered: + # FIXME #2558 go through all views here and check if this + # instrument registration conflict can be fixed. If it can be, do + # not log the following warning. + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + APIObservableCounter.__name__, + unit, + description, + ) + with self._instrument_id_instrument_lock: + return self._instrument_id_instrument[instrument_id] + + instrument = ObservableCounter( + name, + self._instrumentation_scope, + self._measurement_consumer, + callbacks, + unit, + description, + ) + + self._measurement_consumer.register_asynchronous_instrument(instrument) + + with self._instrument_id_instrument_lock: + self._instrument_id_instrument[instrument_id] = instrument + return instrument + + def create_histogram(self, name, unit="", description="") -> APIHistogram: + + ( + is_instrument_registered, + instrument_id, + ) = self._is_instrument_registered(name, Histogram, unit, description) + + if is_instrument_registered: + # FIXME #2558 go through all views here and check if this + # instrument registration conflict can be fixed. If it can be, do + # not log the following warning. + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + APIHistogram.__name__, + unit, + description, + ) + with self._instrument_id_instrument_lock: + return self._instrument_id_instrument[instrument_id] + + instrument = Histogram( + name, + self._instrumentation_scope, + self._measurement_consumer, + unit, + description, + ) + with self._instrument_id_instrument_lock: + self._instrument_id_instrument[instrument_id] = instrument + return instrument + + def create_observable_gauge( + self, name, callbacks=None, unit="", description="" + ) -> APIObservableGauge: + + ( + is_instrument_registered, + instrument_id, + ) = self._is_instrument_registered( + name, ObservableGauge, unit, description + ) + + if is_instrument_registered: + # FIXME #2558 go through all views here and check if this + # instrument registration conflict can be fixed. If it can be, do + # not log the following warning. + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + APIObservableGauge.__name__, + unit, + description, + ) + with self._instrument_id_instrument_lock: + return self._instrument_id_instrument[instrument_id] + + instrument = ObservableGauge( + name, + self._instrumentation_scope, + self._measurement_consumer, + callbacks, + unit, + description, + ) + + self._measurement_consumer.register_asynchronous_instrument(instrument) + + with self._instrument_id_instrument_lock: + self._instrument_id_instrument[instrument_id] = instrument + return instrument + + def create_observable_up_down_counter( + self, name, callbacks=None, unit="", description="" + ) -> APIObservableUpDownCounter: + + ( + is_instrument_registered, + instrument_id, + ) = self._is_instrument_registered(name, Counter, unit, description) + + if is_instrument_registered: + # FIXME #2558 go through all views here and check if this + # instrument registration conflict can be fixed. If it can be, do + # not log the following warning. + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + APIObservableUpDownCounter.__name__, + unit, + description, + ) + with self._instrument_id_instrument_lock: + return self._instrument_id_instrument[instrument_id] + + instrument = ObservableUpDownCounter( + name, + self._instrumentation_scope, + self._measurement_consumer, + callbacks, + unit, + description, + ) + + self._measurement_consumer.register_asynchronous_instrument(instrument) + + with self._instrument_id_instrument_lock: + self._instrument_id_instrument[instrument_id] = instrument + return instrument + + +class MeterProvider(APIMeterProvider): + r"""See `opentelemetry._metrics.MeterProvider`. + + Args: + metric_readers: Register metric readers to collect metrics from the SDK on demand. Each + `MetricReader` is completely independent and will collect separate streams of + metrics. TODO: reference ``PeriodicExportingMetricReader`` usage with push + exporters here. + resource: The resource representing what the metrics emitted from the SDK pertain to. + shutdown_on_exit: If true, registers an `atexit` handler to call + `MeterProvider.shutdown` + views: The views to configure the metric output the SDK + + By default, instruments which do not match any `View` (or if no `View`\ s + are provided) will report metrics with the default aggregation for the + instrument's kind. To disable instruments by default, configure a match-all + `View` with `DropAggregation` and then create `View`\ s to re-enable + individual instruments: + + .. code-block:: python + :caption: Disable default views + + MeterProvider( + views=[ + View(instrument_name="*", aggregation=DropAggregation()), + View(instrument_name="mycounter"), + ], + # ... + ) + """ + + _all_metric_readers_lock = Lock() + _all_metric_readers = set() + + def __init__( + self, + metric_readers: Sequence[MetricReader] = (), + resource: Resource = Resource.create({}), + shutdown_on_exit: bool = True, + views: Sequence[View] = (), + ): + self._lock = Lock() + self._meter_lock = Lock() + self._atexit_handler = None + self._sdk_config = SdkConfiguration( + resource=resource, + metric_readers=metric_readers, + views=views, + ) + self._measurement_consumer = SynchronousMeasurementConsumer( + sdk_config=self._sdk_config + ) + + if shutdown_on_exit: + self._atexit_handler = register(self.shutdown) + + self._meters = {} + + for metric_reader in self._sdk_config.metric_readers: + + with self._all_metric_readers_lock: + if metric_reader in self._all_metric_readers: + raise Exception( + f"MetricReader {metric_reader} has been registered " + "already in other MeterProvider instance" + ) + + self._all_metric_readers.add(metric_reader) + + metric_reader._set_collect_callback( + self._measurement_consumer.collect + ) + + self._shutdown_once = Once() + self._shutdown = False + + def force_flush(self) -> bool: + + # FIXME implement a timeout + + for metric_reader in self._sdk_config.metric_readers: + metric_reader.collect() + return True + + def shutdown(self): + # FIXME implement a timeout + + def _shutdown(): + self._shutdown = True + + did_shutdown = self._shutdown_once.do_once(_shutdown) + + if not did_shutdown: + _logger.warning("shutdown can only be called once") + return + + metric_reader_error = {} + + for metric_reader in self._sdk_config.metric_readers: + try: + metric_reader.shutdown() + + # pylint: disable=broad-except + except Exception as error: + + metric_reader_error[metric_reader] = error + + if self._atexit_handler is not None: + unregister(self._atexit_handler) + self._atexit_handler = None + + if metric_reader_error: + + metric_reader_error_string = "\n".join( + [ + f"{metric_reader.__class__.__name__}: {repr(error)}" + for metric_reader, error in metric_reader_error.items() + ] + ) + + raise Exception( + ( + "MeterProvider.shutdown failed because the following " + "metric readers failed during shutdown:\n" + f"{metric_reader_error_string}" + ) + ) + + def get_meter( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> Meter: + + if self._shutdown: + _logger.warning( + "A shutdown `MeterProvider` can not provide a `Meter`" + ) + return NoOpMeter(name, version=version, schema_url=schema_url) + + if not name: + _logger.warning("Meter name cannot be None or empty.") + return NoOpMeter(name, version=version, schema_url=schema_url) + + info = InstrumentationScope(name, version, schema_url) + with self._meter_lock: + if not self._meters.get(info): + # FIXME #2558 pass SDKConfig object to meter so that the meter + # has access to views. + self._meters[info] = Meter( + info, + self._measurement_consumer, + ) + return self._meters[info] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py similarity index 91% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py rename to opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py index 3754f117a3..752a863a93 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py @@ -17,20 +17,22 @@ from threading import Lock from typing import TYPE_CHECKING, Dict, Iterable -from opentelemetry.sdk._metrics.aggregation import ( - DefaultAggregation, +from opentelemetry.sdk._metrics._internal.aggregation import ( + Aggregation, _Aggregation, - _AggregationFactory, _convert_aggregation_temporality, _PointVarT, ) +from opentelemetry.sdk._metrics._internal.sdk_configuration import ( + SdkConfiguration, +) +from opentelemetry.sdk._metrics.aggregation import DefaultAggregation from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric -from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration -from opentelemetry.sdk._metrics.view import View if TYPE_CHECKING: - from opentelemetry.sdk._metrics.instrument import _Instrument + from opentelemetry.sdk._metrics._internal.instrument import _Instrument + from opentelemetry.sdk._metrics.view import View _logger = getLogger(__name__) @@ -38,10 +40,10 @@ class _ViewInstrumentMatch: def __init__( self, - view: View, + view: "View", instrument: "_Instrument", sdk_config: SdkConfiguration, - instrument_class_aggregation: Dict[type, _AggregationFactory], + instrument_class_aggregation: Dict[type, Aggregation], ): self._view = view self._instrument = instrument diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py new file mode 100644 index 0000000000..92aee89143 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py @@ -0,0 +1,495 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod +from bisect import bisect_left +from dataclasses import replace +from logging import getLogger +from math import inf +from threading import Lock +from typing import Generic, List, Optional, Sequence, TypeVar + +from opentelemetry._metrics import ( + Asynchronous, + Counter, + Histogram, + Instrument, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + Synchronous, + UpDownCounter, +) +from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk._metrics.point import AggregationTemporality, Gauge +from opentelemetry.sdk._metrics.point import Histogram as HistogramPoint +from opentelemetry.sdk._metrics.point import PointT, Sum +from opentelemetry.util._time import _time_ns + +_PointVarT = TypeVar("_PointVarT", bound=PointT) + +_logger = getLogger(__name__) + + +class _Aggregation(ABC, Generic[_PointVarT]): + def __init__(self): + self._lock = Lock() + + @abstractmethod + def aggregate(self, measurement: Measurement) -> None: + pass + + @abstractmethod + def collect(self) -> Optional[_PointVarT]: + pass + + +class _DropAggregation(_Aggregation): + def aggregate(self, measurement: Measurement) -> None: + pass + + def collect(self) -> Optional[_PointVarT]: + pass + + +class Aggregation(ABC): + """ + Base class for all aggregation types. + """ + + @abstractmethod + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + """Creates an aggregation""" + + +class DefaultAggregation(Aggregation): + """ + The default aggregation to be used in a `View`. + + This aggregation will create an actual aggregation depending on the + instrument type, as specified next: + + ============================================= ==================================== + Instrument Aggregation + ============================================= ==================================== + `Counter` `SumAggregation` + `UpDownCounter` `SumAggregation` + `ObservableCounter` `SumAggregation` + `ObservableUpDownCounter` `SumAggregation` + `opentelemetry._metrics.Histogram` `ExplicitBucketHistogramAggregation` + `ObservableGauge` `LastValueAggregation` + ============================================= ==================================== + """ + + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + + # pylint: disable=too-many-return-statements + if isinstance(instrument, Counter): + return _SumAggregation( + instrument_is_monotonic=True, + instrument_temporality=AggregationTemporality.DELTA, + ) + if isinstance(instrument, UpDownCounter): + return _SumAggregation( + instrument_is_monotonic=False, + instrument_temporality=AggregationTemporality.DELTA, + ) + + if isinstance(instrument, ObservableCounter): + return _SumAggregation( + instrument_is_monotonic=True, + instrument_temporality=AggregationTemporality.CUMULATIVE, + ) + + if isinstance(instrument, ObservableUpDownCounter): + return _SumAggregation( + instrument_is_monotonic=False, + instrument_temporality=AggregationTemporality.CUMULATIVE, + ) + + if isinstance(instrument, Histogram): + return _ExplicitBucketHistogramAggregation() + + if isinstance(instrument, ObservableGauge): + return _LastValueAggregation() + + raise Exception(f"Invalid instrument type {type(instrument)} found") + + +class _SumAggregation(_Aggregation[Sum]): + def __init__( + self, + instrument_is_monotonic: bool, + instrument_temporality: AggregationTemporality, + ): + super().__init__() + + self._start_time_unix_nano = _time_ns() + self._instrument_temporality = instrument_temporality + self._instrument_is_monotonic = instrument_is_monotonic + + if self._instrument_temporality is AggregationTemporality.DELTA: + self._value = 0 + else: + self._value = None + + def aggregate(self, measurement: Measurement) -> None: + with self._lock: + if self._value is None: + self._value = 0 + self._value = self._value + measurement.value + + def collect(self) -> Optional[Sum]: + """ + Atomically return a point for the current value of the metric and + reset the aggregation value. + """ + now = _time_ns() + + if self._instrument_temporality is AggregationTemporality.DELTA: + + with self._lock: + value = self._value + start_time_unix_nano = self._start_time_unix_nano + + self._value = 0 + self._start_time_unix_nano = now + 1 + + return Sum( + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=self._instrument_is_monotonic, + start_time_unix_nano=start_time_unix_nano, + time_unix_nano=now, + value=value, + ) + + with self._lock: + if self._value is None: + return None + value = self._value + self._value = None + + return Sum( + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=self._instrument_is_monotonic, + start_time_unix_nano=self._start_time_unix_nano, + time_unix_nano=now, + value=value, + ) + + +class _LastValueAggregation(_Aggregation[Gauge]): + def __init__(self): + super().__init__() + self._value = None + + def aggregate(self, measurement: Measurement): + with self._lock: + self._value = measurement.value + + def collect(self) -> Optional[Gauge]: + """ + Atomically return a point for the current value of the metric. + """ + with self._lock: + if self._value is None: + return None + value = self._value + self._value = None + + return Gauge( + time_unix_nano=_time_ns(), + value=value, + ) + + +class _ExplicitBucketHistogramAggregation(_Aggregation[HistogramPoint]): + def __init__( + self, + boundaries: Sequence[float] = ( + 0.0, + 5.0, + 10.0, + 25.0, + 50.0, + 75.0, + 100.0, + 250.0, + 500.0, + 1000.0, + ), + record_min_max: bool = True, + ): + super().__init__() + self._boundaries = tuple(boundaries) + self._bucket_counts = self._get_empty_bucket_counts() + self._min = inf + self._max = -inf + self._sum = 0 + self._record_min_max = record_min_max + self._start_time_unix_nano = _time_ns() + + def _get_empty_bucket_counts(self) -> List[int]: + return [0] * (len(self._boundaries) + 1) + + def aggregate(self, measurement: Measurement) -> None: + + value = measurement.value + + if self._record_min_max: + self._min = min(self._min, value) + self._max = max(self._max, value) + + self._sum += value + + self._bucket_counts[bisect_left(self._boundaries, value)] += 1 + + def collect(self) -> HistogramPoint: + """ + Atomically return a point for the current value of the metric. + """ + now = _time_ns() + + with self._lock: + value = self._bucket_counts + start_time_unix_nano = self._start_time_unix_nano + histogram_sum = self._sum + histogram_max = self._max + histogram_min = self._min + + self._bucket_counts = self._get_empty_bucket_counts() + self._start_time_unix_nano = now + 1 + self._sum = 0 + self._min = inf + self._max = -inf + + return HistogramPoint( + aggregation_temporality=AggregationTemporality.DELTA, + bucket_counts=tuple(value), + explicit_bounds=self._boundaries, + max=histogram_max, + min=histogram_min, + start_time_unix_nano=start_time_unix_nano, + sum=histogram_sum, + time_unix_nano=now, + ) + + +# pylint: disable=too-many-return-statements,too-many-branches +def _convert_aggregation_temporality( + previous_point: Optional[_PointVarT], + current_point: _PointVarT, + aggregation_temporality: AggregationTemporality, +) -> _PointVarT: + """Converts `current_point` to the requested `aggregation_temporality` + given the `previous_point`. + + `previous_point` must have `CUMULATIVE` temporality. `current_point` may + have `DELTA` or `CUMULATIVE` temporality. + + The output point will have temporality `aggregation_temporality`. Since + `GAUGE` points have no temporality, they are returned unchanged. + """ + + current_point_type = type(current_point) + + if current_point_type is Gauge: + return current_point + + if ( + previous_point is not None + and current_point is not None + and type(previous_point) is not type(current_point) + ): + _logger.warning( + "convert_aggregation_temporality called with mismatched " + "point types: %s and %s", + type(previous_point), + current_point_type, + ) + + return current_point + + if current_point_type is Sum: + if previous_point is None: + # Output CUMULATIVE for a synchronous instrument + # There is no previous value, return the delta point as a + # cumulative + return replace( + current_point, aggregation_temporality=aggregation_temporality + ) + if previous_point.aggregation_temporality is not ( + AggregationTemporality.CUMULATIVE + ): + raise Exception( + "previous_point aggregation temporality must be CUMULATIVE" + ) + + if current_point.aggregation_temporality is aggregation_temporality: + # Output DELTA for a synchronous instrument + # Output CUMULATIVE for an asynchronous instrument + return current_point + + if aggregation_temporality is AggregationTemporality.DELTA: + # Output temporality DELTA for an asynchronous instrument + value = current_point.value - previous_point.value + output_start_time_unix_nano = previous_point.time_unix_nano + + else: + # Output CUMULATIVE for a synchronous instrument + value = current_point.value + previous_point.value + output_start_time_unix_nano = previous_point.start_time_unix_nano + + is_monotonic = ( + previous_point.is_monotonic and current_point.is_monotonic + ) + + return Sum( + start_time_unix_nano=output_start_time_unix_nano, + time_unix_nano=current_point.time_unix_nano, + value=value, + aggregation_temporality=aggregation_temporality, + is_monotonic=is_monotonic, + ) + + if current_point_type is HistogramPoint: + if previous_point is None: + return replace( + current_point, aggregation_temporality=aggregation_temporality + ) + if previous_point.aggregation_temporality is not ( + AggregationTemporality.CUMULATIVE + ): + raise Exception( + "previous_point aggregation temporality must be CUMULATIVE" + ) + + if current_point.aggregation_temporality is aggregation_temporality: + return current_point + + max_ = current_point.max + min_ = current_point.min + + if aggregation_temporality is AggregationTemporality.CUMULATIVE: + start_time_unix_nano = previous_point.start_time_unix_nano + sum_ = current_point.sum + previous_point.sum + # Only update min/max on delta -> cumulative + max_ = max(current_point.max, previous_point.max) + min_ = min(current_point.min, previous_point.min) + bucket_counts = [ + curr_count + prev_count + for curr_count, prev_count in zip( + current_point.bucket_counts, previous_point.bucket_counts + ) + ] + else: + start_time_unix_nano = previous_point.time_unix_nano + sum_ = current_point.sum - previous_point.sum + bucket_counts = [ + curr_count - prev_count + for curr_count, prev_count in zip( + current_point.bucket_counts, previous_point.bucket_counts + ) + ] + + return HistogramPoint( + aggregation_temporality=aggregation_temporality, + bucket_counts=bucket_counts, + explicit_bounds=current_point.explicit_bounds, + max=max_, + min=min_, + start_time_unix_nano=start_time_unix_nano, + sum=sum_, + time_unix_nano=current_point.time_unix_nano, + ) + return None + + +class ExplicitBucketHistogramAggregation(Aggregation): + """This aggregation informs the SDK to collect: + + - Count of Measurement values falling within explicit bucket boundaries. + - Arithmetic sum of Measurement values in population. This SHOULD NOT be collected when used with instruments that record negative measurements, e.g. UpDownCounter or ObservableGauge. + - Min (optional) Measurement value in population. + - Max (optional) Measurement value in population. + + + Args: + boundaries: Array of increasing values representing explicit bucket boundary values. + record_min_max: Whether to record min and max. + """ + + def __init__( + self, + boundaries: Sequence[float] = ( + 0.0, + 5.0, + 10.0, + 25.0, + 50.0, + 75.0, + 100.0, + 250.0, + 500.0, + 1000.0, + ), + record_min_max: bool = True, + ) -> None: + self._boundaries = boundaries + self._record_min_max = record_min_max + + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + return _ExplicitBucketHistogramAggregation( + boundaries=self._boundaries, + record_min_max=self._record_min_max, + ) + + +class SumAggregation(Aggregation): + """This aggregation informs the SDK to collect: + + - The arithmetic sum of Measurement values. + """ + + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + + temporality = AggregationTemporality.UNSPECIFIED + if isinstance(instrument, Synchronous): + temporality = AggregationTemporality.DELTA + elif isinstance(instrument, Asynchronous): + temporality = AggregationTemporality.CUMULATIVE + + return _SumAggregation( + isinstance(instrument, (Counter, ObservableCounter)), + temporality, + ) + + +class LastValueAggregation(Aggregation): + """ + This aggregation informs the SDK to collect: + + - The last Measurement. + - The timestamp of the last Measurement. + """ + + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + return _LastValueAggregation() + + +class DropAggregation(Aggregation): + """Using this aggregation will make all measurements be ignored.""" + + def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + return _DropAggregation() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py new file mode 100644 index 0000000000..b99d7f8f03 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py @@ -0,0 +1,217 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os +from abc import ABC, abstractmethod +from enum import Enum +from os import environ, linesep +from sys import stdout +from threading import Event, RLock, Thread +from typing import IO, Callable, Dict, Iterable, List, Optional, Sequence + +from opentelemetry.context import ( + _SUPPRESS_INSTRUMENTATION_KEY, + attach, + detach, + set_value, +) +from opentelemetry.sdk._metrics._internal.aggregation import Aggregation +from opentelemetry.sdk._metrics.metric_reader import MetricReader +from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric +from opentelemetry.util._once import Once + +_logger = logging.getLogger(__name__) + + +class MetricExportResult(Enum): + """Result of exporting a metric + + Can be any of the following values:""" + + SUCCESS = 0 + FAILURE = 1 + + +class MetricExporter(ABC): + """Interface for exporting metrics. + + Interface to be implemented by services that want to export metrics received + in their own format. + """ + + @abstractmethod + def export(self, metrics: Sequence[Metric]) -> "MetricExportResult": + """Exports a batch of telemetry data. + + Args: + metrics: The list of `opentelemetry.sdk._metrics.point.Metric` objects to be exported + + Returns: + The result of the export + """ + + @abstractmethod + def shutdown(self) -> None: + """Shuts down the exporter. + + Called when the SDK is shut down. + """ + + +class ConsoleMetricExporter(MetricExporter): + """Implementation of :class:`MetricExporter` that prints metrics to the + console. + + This class can be used for diagnostic purposes. It prints the exported + metrics to the console STDOUT. + """ + + def __init__( + self, + out: IO = stdout, + formatter: Callable[[Metric], str] = lambda metric: metric.to_json() + + linesep, + ): + self.out = out + self.formatter = formatter + + def export(self, metrics: Sequence[Metric]) -> MetricExportResult: + for metric in metrics: + self.out.write(self.formatter(metric)) + self.out.flush() + return MetricExportResult.SUCCESS + + def shutdown(self) -> None: + pass + + +class InMemoryMetricReader(MetricReader): + """Implementation of `MetricReader` that returns its metrics from :func:`get_metrics`. + + This is useful for e.g. unit tests. + """ + + def __init__( + self, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[type, Aggregation] = None, + ) -> None: + super().__init__( + preferred_temporality=preferred_temporality, + preferred_aggregation=preferred_aggregation, + ) + self._lock = RLock() + self._metrics: List[Metric] = [] + + def get_metrics(self) -> List[Metric]: + """Reads and returns current metrics from the SDK""" + with self._lock: + self.collect() + metrics = self._metrics + self._metrics = [] + return metrics + + def _receive_metrics(self, metrics: Iterable[Metric]): + with self._lock: + self._metrics = list(metrics) + + def shutdown(self): + pass + + +class PeriodicExportingMetricReader(MetricReader): + """`PeriodicExportingMetricReader` is an implementation of `MetricReader` + that collects metrics based on a user-configurable time interval, and passes the + metrics to the configured exporter. + """ + + def __init__( + self, + exporter: MetricExporter, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[type, Aggregation] = None, + export_interval_millis: Optional[float] = None, + export_timeout_millis: Optional[float] = None, + ) -> None: + super().__init__( + preferred_temporality=preferred_temporality, + preferred_aggregation=preferred_aggregation, + ) + self._exporter = exporter + if export_interval_millis is None: + try: + export_interval_millis = float( + environ.get("OTEL_METRIC_EXPORT_INTERVAL", 60000) + ) + except ValueError: + _logger.warning( + "Found invalid value for export interval, using default" + ) + export_interval_millis = 60000 + if export_timeout_millis is None: + try: + export_timeout_millis = float( + environ.get("OTEL_METRIC_EXPORT_TIMEOUT", 30000) + ) + except ValueError: + _logger.warning( + "Found invalid value for export timeout, using default" + ) + export_timeout_millis = 30000 + self._export_interval_millis = export_interval_millis + self._export_timeout_millis = export_timeout_millis + self._shutdown = False + self._shutdown_event = Event() + self._shutdown_once = Once() + self._daemon_thread = Thread(target=self._ticker, daemon=True) + self._daemon_thread.start() + if hasattr(os, "register_at_fork"): + os.register_at_fork( + after_in_child=self._at_fork_reinit + ) # pylint: disable=protected-access + + def _at_fork_reinit(self): + self._daemon_thread = Thread(target=self._ticker, daemon=True) + self._daemon_thread.start() + + def _ticker(self) -> None: + interval_secs = self._export_interval_millis / 1e3 + while not self._shutdown_event.wait(interval_secs): + self.collect() + # one last collection below before shutting down completely + self.collect() + + def _receive_metrics(self, metrics: Iterable[Metric]) -> None: + if metrics is None: + return + token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) + try: + self._exporter.export(metrics) + except Exception as e: # pylint: disable=broad-except,invalid-name + _logger.exception("Exception while exporting metrics %s", str(e)) + detach(token) + + def shutdown(self): + def _shutdown(): + self._shutdown = True + + did_set = self._shutdown_once.do_once(_shutdown) + if not did_set: + _logger.warning("Can't shutdown multiple times") + return + + self._shutdown_event.set() + self._daemon_thread.join() + self._exporter.shutdown() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py new file mode 100644 index 0000000000..9e11e22059 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py @@ -0,0 +1,175 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=too-many-ancestors + +from logging import getLogger +from typing import TYPE_CHECKING, Dict, Generator, Iterable, Optional, Union + +from opentelemetry._metrics import CallbackT +from opentelemetry._metrics import Counter as APICounter +from opentelemetry._metrics import Histogram as APIHistogram +from opentelemetry._metrics import ObservableCounter as APIObservableCounter +from opentelemetry._metrics import ObservableGauge as APIObservableGauge +from opentelemetry._metrics import ( + ObservableUpDownCounter as APIObservableUpDownCounter, +) +from opentelemetry._metrics import UpDownCounter as APIUpDownCounter +from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk.util.instrumentation import InstrumentationScope + +if TYPE_CHECKING: + from opentelemetry.sdk._metrics._internal.measurement_consumer import ( + MeasurementConsumer, + ) + + +_logger = getLogger(__name__) + + +_ERROR_MESSAGE = ( + "Expected ASCII string of maximum length 63 characters but got {}" +) + + +class _Synchronous: + def __init__( + self, + name: str, + instrumentation_scope: InstrumentationScope, + measurement_consumer: "MeasurementConsumer", + unit: str = "", + description: str = "", + ): + # pylint: disable=no-member + is_name_valid, is_unit_valid = self._check_name_and_unit(name, unit) + + if not is_name_valid: + raise Exception(_ERROR_MESSAGE.format(name)) + + if not is_unit_valid: + raise Exception(_ERROR_MESSAGE.format(unit)) + self.name = name.lower() + self.unit = unit + self.description = description + self.instrumentation_scope = instrumentation_scope + self._measurement_consumer = measurement_consumer + super().__init__(name, unit=unit, description=description) + + +class _Asynchronous: + def __init__( + self, + name: str, + instrumentation_scope: InstrumentationScope, + measurement_consumer: "MeasurementConsumer", + callbacks: Optional[Iterable[CallbackT]] = None, + unit: str = "", + description: str = "", + ): + # pylint: disable=no-member + is_name_valid, is_unit_valid = self._check_name_and_unit(name, unit) + + if not is_name_valid: + raise Exception(_ERROR_MESSAGE.format(name)) + + if not is_unit_valid: + raise Exception(_ERROR_MESSAGE.format(unit)) + self.name = name.lower() + self.unit = unit + self.description = description + self.instrumentation_scope = instrumentation_scope + self._measurement_consumer = measurement_consumer + super().__init__(name, callbacks, unit=unit, description=description) + + self._callbacks = [] + + if callbacks is not None: + + for callback in callbacks: + + if isinstance(callback, Generator): + + def inner(callback=callback) -> Iterable[Measurement]: + return next(callback) + + self._callbacks.append(inner) + else: + self._callbacks.append(callback) + + def callback(self) -> Iterable[Measurement]: + for callback in self._callbacks: + try: + for api_measurement in callback(): + yield Measurement( + api_measurement.value, + instrument=self, + attributes=api_measurement.attributes, + ) + except StopIteration: + pass + except Exception: # pylint: disable=broad-except + _logger.exception( + "Callback failed for instrument %s.", self.name + ) + + +class Counter(_Synchronous, APICounter): + def add( + self, amount: Union[int, float], attributes: Dict[str, str] = None + ): + if amount < 0: + _logger.warning( + "Add amount must be non-negative on Counter %s.", self.name + ) + return + self._measurement_consumer.consume_measurement( + Measurement(amount, self, attributes) + ) + + +class UpDownCounter(_Synchronous, APIUpDownCounter): + def add( + self, amount: Union[int, float], attributes: Dict[str, str] = None + ): + self._measurement_consumer.consume_measurement( + Measurement(amount, self, attributes) + ) + + +class ObservableCounter(_Asynchronous, APIObservableCounter): + pass + + +class ObservableUpDownCounter(_Asynchronous, APIObservableUpDownCounter): + pass + + +class Histogram(_Synchronous, APIHistogram): + def record( + self, amount: Union[int, float], attributes: Dict[str, str] = None + ): + if amount < 0: + _logger.warning( + "Record amount must be non-negative on Histogram %s.", + self.name, + ) + return + self._measurement_consumer.consume_measurement( + Measurement(amount, self, attributes) + ) + + +class ObservableGauge(_Asynchronous, APIObservableGauge): + pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement.py new file mode 100644 index 0000000000..053c8f6fa6 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement.py @@ -0,0 +1,32 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Union + +from opentelemetry.util.types import Attributes + +if TYPE_CHECKING: + from opentelemetry.sdk._metrics._internal.instrument import _Instrument + + +@dataclass(frozen=True) +class Measurement: + """ + Represents a data point reported via the metrics API to the SDK. + """ + + value: Union[int, float] + instrument: "_Instrument" + attributes: Attributes = None diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py similarity index 91% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py rename to opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py index df77bf71e4..ce9b1f0d5e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py @@ -16,14 +16,15 @@ from threading import Lock from typing import TYPE_CHECKING, Dict, Iterable, List, Mapping -from opentelemetry.sdk._metrics.aggregation import AggregationTemporality -from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.metric_reader_storage import ( +from opentelemetry.sdk._metrics._internal.metric_reader_storage import ( MetricReaderStorage, ) -from opentelemetry.sdk._metrics.point import Metric -from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration +from opentelemetry.sdk._metrics._internal.sdk_configuration import ( + SdkConfiguration, +) +from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk._metrics.metric_reader import MetricReader +from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric if TYPE_CHECKING: from opentelemetry.sdk._metrics.instrument import _Asynchronous diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py new file mode 100644 index 0000000000..b84b873854 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py @@ -0,0 +1,179 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod +from logging import getLogger +from os import environ +from typing import Callable, Dict, Iterable + +from typing_extensions import final + +from opentelemetry.sdk._metrics.aggregation import ( + Aggregation, + DefaultAggregation, +) +from opentelemetry.sdk._metrics.instrument import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric +from opentelemetry.sdk.environment_variables import ( + _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, +) + +_logger = getLogger(__name__) + + +class MetricReader(ABC): + """ + Base class for all metric readers + + Args: + preferred_temporality: A mapping between instrument classes and + aggregation temporality. By default uses CUMULATIVE for all instrument + classes. This mapping will be used to define the default aggregation + temporality of every instrument class. If the user wants to make a + change in the default aggregation temporality of an instrument class, + it is enough to pass here a dictionary whose keys are the instrument + classes and the values are the corresponding desired aggregation + temporalities of the classes that the user wants to change, not all of + them. The classes not included in the passed dictionary will retain + their association to their default aggregation temporalities. + The value passed here will override the corresponding values set + via the environment variable + preferred_aggregation: A mapping between instrument classes and + aggregation instances. By default maps all instrument classes to an + instance of `DefaultAggregation`. This mapping will be used to + define the default aggregation of every instrument class. If the + user wants to make a change in the default aggregation of an + instrument class, it is enough to pass here a dictionary whose keys + are the instrument classes and the values are the corresponding + desired aggregation for the instrument classes that the user wants + to change, not necessarily all of them. The classes not included in + the passed dictionary will retain their association to their + default aggregations. The aggregation defined here will be + overriden by an aggregation defined by a view that is not + `DefaultAggregation`. + + .. document protected _receive_metrics which is a intended to be overriden by subclass + .. automethod:: _receive_metrics + """ + + # FIXME add :std:envvar:`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` + # to the end of the documentation paragraph above. + + def __init__( + self, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[type, Aggregation] = None, + ) -> None: + self._collect: Callable[ + ["MetricReader", AggregationTemporality], Iterable[Metric] + ] = None + + if ( + environ.get( + _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + "CUMULATIVE", + ) + .upper() + .strip() + == "DELTA" + ): + self._instrument_class_temporality = { + Counter: AggregationTemporality.DELTA, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.DELTA, + ObservableCounter: AggregationTemporality.DELTA, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + else: + self._instrument_class_temporality = { + Counter: AggregationTemporality.CUMULATIVE, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.CUMULATIVE, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + if preferred_temporality is not None: + for temporality in preferred_temporality.values(): + if temporality not in ( + AggregationTemporality.CUMULATIVE, + AggregationTemporality.DELTA, + ): + raise Exception( + f"Invalid temporality value found {temporality}" + ) + + self._instrument_class_temporality.update(preferred_temporality or {}) + self._preferred_temporality = preferred_temporality + self._instrument_class_aggregation = { + Counter: DefaultAggregation(), + UpDownCounter: DefaultAggregation(), + Histogram: DefaultAggregation(), + ObservableCounter: DefaultAggregation(), + ObservableUpDownCounter: DefaultAggregation(), + ObservableGauge: DefaultAggregation(), + } + + self._instrument_class_aggregation.update(preferred_aggregation or {}) + + @final + def collect(self) -> None: + """Collects the metrics from the internal SDK state and + invokes the `_receive_metrics` with the collection. + """ + if self._collect is None: + _logger.warning( + "Cannot call collect on a MetricReader until it is registered on a MeterProvider" + ) + return + self._receive_metrics( + self._collect(self, self._instrument_class_temporality) + ) + + @final + def _set_collect_callback( + self, + func: Callable[ + ["MetricReader", AggregationTemporality], Iterable[Metric] + ], + ) -> None: + """This function is internal to the SDK. It should not be called or overriden by users""" + self._collect = func + + @abstractmethod + def _receive_metrics(self, metrics: Iterable[Metric]): + """Called by `MetricReader.collect` when it receives a batch of metrics""" + + @abstractmethod + def shutdown(self): + """Shuts down the MetricReader. This method provides a way + for the MetricReader to do any cleanup required. A metric reader can + only be shutdown once, any subsequent calls are ignored and return + failure status. + + When a `MetricReader` is registered on a + :class:`~opentelemetry.sdk._metrics.MeterProvider`, + :meth:`~opentelemetry.sdk._metrics.MeterProvider.shutdown` will invoke this + automatically. + """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py similarity index 91% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py rename to opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py index d2685391c1..fd046991fb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py @@ -16,17 +16,16 @@ from typing import Dict, Iterable, List from opentelemetry._metrics import Instrument -from opentelemetry.sdk._metrics._view_instrument_match import ( +from opentelemetry.sdk._metrics._internal._view_instrument_match import ( _ViewInstrumentMatch, ) -from opentelemetry.sdk._metrics.aggregation import ( - AggregationTemporality, - _AggregationFactory, +from opentelemetry.sdk._metrics._internal.aggregation import Aggregation +from opentelemetry.sdk._metrics._internal.sdk_configuration import ( + SdkConfiguration, ) +from opentelemetry.sdk._metrics._internal.view import View from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import Metric -from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration -from opentelemetry.sdk._metrics.view import View +from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric _DEFAULT_VIEW = View(instrument_name="") @@ -37,7 +36,7 @@ class MetricReaderStorage: def __init__( self, sdk_config: SdkConfiguration, - instrument_class_aggregation: Dict[type, _AggregationFactory], + instrument_class_aggregation: Dict[type, Aggregation], ) -> None: self._lock = RLock() self._sdk_config = sdk_config diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py new file mode 100644 index 0000000000..6d55858a63 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py @@ -0,0 +1,110 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from dataclasses import asdict, dataclass +from enum import IntEnum +from typing import Sequence, Union + +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.util.types import Attributes + + +class AggregationTemporality(IntEnum): + """ + The temporality to use when aggregating data. + + Can be one of the following values: + """ + + UNSPECIFIED = 0 + DELTA = 1 + CUMULATIVE = 2 + + +@dataclass(frozen=True) +class Sum: + """Represents the type of a scalar metric that is calculated as a sum of + all reported measurements over a time interval.""" + + aggregation_temporality: AggregationTemporality + is_monotonic: bool + start_time_unix_nano: int + time_unix_nano: int + value: Union[int, float] + + +@dataclass(frozen=True) +class Gauge: + """Represents the type of a scalar metric that always exports the current + value for every data point. It should be used for an unknown + aggregation.""" + + time_unix_nano: int + value: Union[int, float] + + +@dataclass(frozen=True) +class Histogram: + """Represents the type of a metric that is calculated by aggregating as a + histogram of all reported measurements over a time interval.""" + + aggregation_temporality: AggregationTemporality + bucket_counts: Sequence[int] + explicit_bounds: Sequence[float] + max: int + min: int + start_time_unix_nano: int + sum: Union[int, float] + time_unix_nano: int + + +PointT = Union[Sum, Gauge, Histogram] + + +@dataclass(frozen=True) +class Metric: + """Represents a metric point in the OpenTelemetry data model to be exported + + Concrete metric types contain all the information as in the OTLP proto definitions + (https://github.com/open-telemetry/opentelemetry-proto/blob/b43e9b18b76abf3ee040164b55b9c355217151f3/opentelemetry/proto/metrics/v1/metrics.proto#L37) but are flattened as much as possible. + """ + + # common fields to all metric kinds + attributes: Attributes + description: str + instrumentation_scope: InstrumentationScope + name: str + resource: Resource + unit: str + point: PointT + """Contains non-common fields for the given metric""" + + def to_json(self) -> str: + return json.dumps( + { + "attributes": self.attributes if self.attributes else "", + "description": self.description if self.description else "", + "instrumentation_scope": repr(self.instrumentation_scope) + if self.instrumentation_scope + else "", + "name": self.name, + "resource": repr(self.resource.attributes) + if self.resource + else "", + "unit": self.unit if self.unit else "", + "point": asdict(self.point) if self.point else "", + } + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/sdk_configuration.py similarity index 63% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py rename to opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/sdk_configuration.py index 2c603b5e4d..b48d7f22e7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/sdk_configuration.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/sdk_configuration.py @@ -1,13 +1,15 @@ from dataclasses import dataclass -from typing import Sequence +from typing import TYPE_CHECKING, Sequence from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.view import View from opentelemetry.sdk.resources import Resource +if TYPE_CHECKING: + from opentelemetry.sdk._metrics.view import View + @dataclass class SdkConfiguration: resource: Resource metric_readers: Sequence[MetricReader] - views: Sequence[View] + views: Sequence["View"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/view.py new file mode 100644 index 0000000000..06f5355b17 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/view.py @@ -0,0 +1,159 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from fnmatch import fnmatch +from logging import getLogger +from typing import Optional, Set, Type + +# FIXME import from typing when support for 3.6 is removed +from typing_extensions import final + +from opentelemetry._metrics import Instrument +from opentelemetry.sdk._metrics._internal.aggregation import Aggregation +from opentelemetry.sdk._metrics.aggregation import DefaultAggregation + +_logger = getLogger(__name__) + + +class View: + """ + A `View` configuration parameters can be used for the following + purposes: + + 1. Match instruments: When an instrument matches a view, measurements + received by that instrument will be processed. + 2. Customize metric streams: A metric stream is identified by a match + between a view and an instrument and a set of attributes. The metric + stream can be customized by certain attributes of the corresponding view. + + The attributes documented next serve one of the previous two purposes. + + Args: + instrument_type: This is an instrument matching attribute: the class the + instrument must be to match the view. + + instrument_name: This is an instrument matching attribute: the name the + instrument must have to match the view. Wild card characters are supported. Wild + card characters should not be used with this attribute if the view has also a + ``name`` defined. + + meter_name: This is an instrument matching attribute: the name the + instrument meter must have to match the view. + + meter_version: This is an instrument matching attribute: the version + the instrument meter must have to match the view. + + meter_schema_url: This is an instrument matching attribute: the schema + URL the instrument meter must have to match the view. + + name: This is a metric stream customizing attribute: the name of the + metric stream. If `None`, the name of the instrument will be used. + + description: This is a metric stream customizing attribute: the + description of the metric stream. If `None`, the description of the instrument will + be used. + + attribute_keys: This is a metric stream customizing attribute: this is + a set of attribute keys. If not `None` then only the measurement attributes that + are in ``attribute_keys`` will be used to identify the metric stream. + + aggregation: This is a metric stream customizing attribute: the + aggregation instance to use when data is aggregated for the + corresponding metrics stream. If `None` an instance of + `DefaultAggregation` will be used. + + This class is not intended to be subclassed by the user. + """ + + _default_aggregation = DefaultAggregation() + + def __init__( + self, + instrument_type: Optional[Type[Instrument]] = None, + instrument_name: Optional[str] = None, + meter_name: Optional[str] = None, + meter_version: Optional[str] = None, + meter_schema_url: Optional[str] = None, + name: Optional[str] = None, + description: Optional[str] = None, + attribute_keys: Optional[Set[str]] = None, + aggregation: Optional[Aggregation] = None, + ): + if ( + instrument_type + is instrument_name + is meter_name + is meter_version + is meter_schema_url + is None + ): + raise Exception( + "Some instrument selection " + f"criteria must be provided for View {name}" + ) + + if ( + name is not None + and instrument_name is not None + and ("*" in instrument_name or "?" in instrument_name) + ): + + raise Exception( + f"View {name} declared with wildcard " + "characters in instrument_name" + ) + + # _name, _description, _aggregation and _attribute_keys will be + # accessed when instantiating a _ViewInstrumentMatch. + self._name = name + self._instrument_type = instrument_type + self._instrument_name = instrument_name + self._meter_name = meter_name + self._meter_version = meter_version + self._meter_schema_url = meter_schema_url + + self._description = description + self._attribute_keys = attribute_keys + self._aggregation = aggregation or self._default_aggregation + + # pylint: disable=too-many-return-statements + # pylint: disable=too-many-branches + @final + def _match(self, instrument: Instrument) -> bool: + + if self._instrument_type is not None: + if not isinstance(instrument, self._instrument_type): + return False + + if self._instrument_name is not None: + if not fnmatch(instrument.name, self._instrument_name): + return False + + if self._meter_name is not None: + if instrument.instrumentation_scope.name != self._meter_name: + return False + + if self._meter_version is not None: + if instrument.instrumentation_scope.version != self._meter_version: + return False + + if self._meter_schema_url is not None: + if ( + instrument.instrumentation_scope.schema_url + != self._meter_schema_url + ): + return False + + return True diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index f977925c83..9566793db7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -12,485 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -.. Explicitly document private _AggregationFactory -.. autoclass:: _AggregationFactory -""" +# pylint: disable=unused-import -from abc import ABC, abstractmethod -from bisect import bisect_left -from dataclasses import replace -from logging import getLogger -from math import inf -from threading import Lock -from typing import Generic, List, Optional, Sequence, TypeVar -from opentelemetry._metrics import ( - Asynchronous, - Counter, - Histogram, - Instrument, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - Synchronous, - UpDownCounter, +from opentelemetry.sdk._metrics._internal.aggregation import ( # noqa: F401 + Aggregation, + DefaultAggregation, + DropAggregation, + ExplicitBucketHistogramAggregation, + LastValueAggregation, + SumAggregation, ) -from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import AggregationTemporality, Gauge -from opentelemetry.sdk._metrics.point import Histogram as HistogramPoint -from opentelemetry.sdk._metrics.point import PointT, Sum -from opentelemetry.util._time import _time_ns -_PointVarT = TypeVar("_PointVarT", bound=PointT) - -_logger = getLogger(__name__) - - -class _Aggregation(ABC, Generic[_PointVarT]): - def __init__(self): - self._lock = Lock() - - @abstractmethod - def aggregate(self, measurement: Measurement) -> None: - pass - - @abstractmethod - def collect(self) -> Optional[_PointVarT]: - pass - - -class _DropAggregation(_Aggregation): - def aggregate(self, measurement: Measurement) -> None: - pass - - def collect(self) -> Optional[_PointVarT]: - pass - - -class _AggregationFactory(ABC): - @abstractmethod - def _create_aggregation(self, instrument: Instrument) -> _Aggregation: - """Creates an aggregation""" - - -class DefaultAggregation(_AggregationFactory): - """ - The default aggregation to be used in a `View`. - - This aggregation will create an actual aggregation depending on the - instrument type, as specified next: - - ============================================= ==================================== - Instrument Aggregation - ============================================= ==================================== - `Counter` `SumAggregation` - `UpDownCounter` `SumAggregation` - `ObservableCounter` `SumAggregation` - `ObservableUpDownCounter` `SumAggregation` - `opentelemetry._metrics.Histogram` `ExplicitBucketHistogramAggregation` - `ObservableGauge` `LastValueAggregation` - ============================================= ==================================== - """ - - def _create_aggregation(self, instrument: Instrument) -> _Aggregation: - - # pylint: disable=too-many-return-statements - if isinstance(instrument, Counter): - return _SumAggregation( - instrument_is_monotonic=True, - instrument_temporality=AggregationTemporality.DELTA, - ) - if isinstance(instrument, UpDownCounter): - return _SumAggregation( - instrument_is_monotonic=False, - instrument_temporality=AggregationTemporality.DELTA, - ) - - if isinstance(instrument, ObservableCounter): - return _SumAggregation( - instrument_is_monotonic=True, - instrument_temporality=AggregationTemporality.CUMULATIVE, - ) - - if isinstance(instrument, ObservableUpDownCounter): - return _SumAggregation( - instrument_is_monotonic=False, - instrument_temporality=AggregationTemporality.CUMULATIVE, - ) - - if isinstance(instrument, Histogram): - return _ExplicitBucketHistogramAggregation() - - if isinstance(instrument, ObservableGauge): - return _LastValueAggregation() - - raise Exception(f"Invalid instrument type {type(instrument)} found") - - -class _SumAggregation(_Aggregation[Sum]): - def __init__( - self, - instrument_is_monotonic: bool, - instrument_temporality: AggregationTemporality, - ): - super().__init__() - - self._start_time_unix_nano = _time_ns() - self._instrument_temporality = instrument_temporality - self._instrument_is_monotonic = instrument_is_monotonic - - if self._instrument_temporality is AggregationTemporality.DELTA: - self._value = 0 - else: - self._value = None - - def aggregate(self, measurement: Measurement) -> None: - with self._lock: - if self._value is None: - self._value = 0 - self._value = self._value + measurement.value - - def collect(self) -> Optional[Sum]: - """ - Atomically return a point for the current value of the metric and - reset the aggregation value. - """ - now = _time_ns() - - if self._instrument_temporality is AggregationTemporality.DELTA: - - with self._lock: - value = self._value - start_time_unix_nano = self._start_time_unix_nano - - self._value = 0 - self._start_time_unix_nano = now + 1 - - return Sum( - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=self._instrument_is_monotonic, - start_time_unix_nano=start_time_unix_nano, - time_unix_nano=now, - value=value, - ) - - with self._lock: - if self._value is None: - return None - value = self._value - self._value = None - - return Sum( - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=self._instrument_is_monotonic, - start_time_unix_nano=self._start_time_unix_nano, - time_unix_nano=now, - value=value, - ) - - -class _LastValueAggregation(_Aggregation[Gauge]): - def __init__(self): - super().__init__() - self._value = None - - def aggregate(self, measurement: Measurement): - with self._lock: - self._value = measurement.value - - def collect(self) -> Optional[Gauge]: - """ - Atomically return a point for the current value of the metric. - """ - with self._lock: - if self._value is None: - return None - value = self._value - self._value = None - - return Gauge( - time_unix_nano=_time_ns(), - value=value, - ) - - -class _ExplicitBucketHistogramAggregation(_Aggregation[HistogramPoint]): - def __init__( - self, - boundaries: Sequence[float] = ( - 0.0, - 5.0, - 10.0, - 25.0, - 50.0, - 75.0, - 100.0, - 250.0, - 500.0, - 1000.0, - ), - record_min_max: bool = True, - ): - super().__init__() - self._boundaries = tuple(boundaries) - self._bucket_counts = self._get_empty_bucket_counts() - self._min = inf - self._max = -inf - self._sum = 0 - self._record_min_max = record_min_max - self._start_time_unix_nano = _time_ns() - - def _get_empty_bucket_counts(self) -> List[int]: - return [0] * (len(self._boundaries) + 1) - - def aggregate(self, measurement: Measurement) -> None: - - value = measurement.value - - if self._record_min_max: - self._min = min(self._min, value) - self._max = max(self._max, value) - - self._sum += value - - self._bucket_counts[bisect_left(self._boundaries, value)] += 1 - - def collect(self) -> HistogramPoint: - """ - Atomically return a point for the current value of the metric. - """ - now = _time_ns() - - with self._lock: - value = self._bucket_counts - start_time_unix_nano = self._start_time_unix_nano - histogram_sum = self._sum - histogram_max = self._max - histogram_min = self._min - - self._bucket_counts = self._get_empty_bucket_counts() - self._start_time_unix_nano = now + 1 - self._sum = 0 - self._min = inf - self._max = -inf - - return HistogramPoint( - aggregation_temporality=AggregationTemporality.DELTA, - bucket_counts=tuple(value), - explicit_bounds=self._boundaries, - max=histogram_max, - min=histogram_min, - start_time_unix_nano=start_time_unix_nano, - sum=histogram_sum, - time_unix_nano=now, - ) - - -# pylint: disable=too-many-return-statements,too-many-branches -def _convert_aggregation_temporality( - previous_point: Optional[_PointVarT], - current_point: _PointVarT, - aggregation_temporality: AggregationTemporality, -) -> _PointVarT: - """Converts `current_point` to the requested `aggregation_temporality` - given the `previous_point`. - - `previous_point` must have `CUMULATIVE` temporality. `current_point` may - have `DELTA` or `CUMULATIVE` temporality. - - The output point will have temporality `aggregation_temporality`. Since - `GAUGE` points have no temporality, they are returned unchanged. - """ - - current_point_type = type(current_point) - - if current_point_type is Gauge: - return current_point - - if ( - previous_point is not None - and current_point is not None - and type(previous_point) is not type(current_point) - ): - _logger.warning( - "convert_aggregation_temporality called with mismatched " - "point types: %s and %s", - type(previous_point), - current_point_type, - ) - - return current_point - - if current_point_type is Sum: - if previous_point is None: - # Output CUMULATIVE for a synchronous instrument - # There is no previous value, return the delta point as a - # cumulative - return replace( - current_point, aggregation_temporality=aggregation_temporality - ) - if previous_point.aggregation_temporality is not ( - AggregationTemporality.CUMULATIVE - ): - raise Exception( - "previous_point aggregation temporality must be CUMULATIVE" - ) - - if current_point.aggregation_temporality is aggregation_temporality: - # Output DELTA for a synchronous instrument - # Output CUMULATIVE for an asynchronous instrument - return current_point - - if aggregation_temporality is AggregationTemporality.DELTA: - # Output temporality DELTA for an asynchronous instrument - value = current_point.value - previous_point.value - output_start_time_unix_nano = previous_point.time_unix_nano - - else: - # Output CUMULATIVE for a synchronous instrument - value = current_point.value + previous_point.value - output_start_time_unix_nano = previous_point.start_time_unix_nano - - is_monotonic = ( - previous_point.is_monotonic and current_point.is_monotonic - ) - - return Sum( - start_time_unix_nano=output_start_time_unix_nano, - time_unix_nano=current_point.time_unix_nano, - value=value, - aggregation_temporality=aggregation_temporality, - is_monotonic=is_monotonic, - ) - - if current_point_type is HistogramPoint: - if previous_point is None: - return replace( - current_point, aggregation_temporality=aggregation_temporality - ) - if previous_point.aggregation_temporality is not ( - AggregationTemporality.CUMULATIVE - ): - raise Exception( - "previous_point aggregation temporality must be CUMULATIVE" - ) - - if current_point.aggregation_temporality is aggregation_temporality: - return current_point - - max_ = current_point.max - min_ = current_point.min - - if aggregation_temporality is AggregationTemporality.CUMULATIVE: - start_time_unix_nano = previous_point.start_time_unix_nano - sum_ = current_point.sum + previous_point.sum - # Only update min/max on delta -> cumulative - max_ = max(current_point.max, previous_point.max) - min_ = min(current_point.min, previous_point.min) - bucket_counts = [ - curr_count + prev_count - for curr_count, prev_count in zip( - current_point.bucket_counts, previous_point.bucket_counts - ) - ] - else: - start_time_unix_nano = previous_point.time_unix_nano - sum_ = current_point.sum - previous_point.sum - bucket_counts = [ - curr_count - prev_count - for curr_count, prev_count in zip( - current_point.bucket_counts, previous_point.bucket_counts - ) - ] - - return HistogramPoint( - aggregation_temporality=aggregation_temporality, - bucket_counts=bucket_counts, - explicit_bounds=current_point.explicit_bounds, - max=max_, - min=min_, - start_time_unix_nano=start_time_unix_nano, - sum=sum_, - time_unix_nano=current_point.time_unix_nano, - ) - return None - - -class ExplicitBucketHistogramAggregation(_AggregationFactory): - """This aggregation informs the SDK to collect: - - - Count of Measurement values falling within explicit bucket boundaries. - - Arithmetic sum of Measurement values in population. This SHOULD NOT be collected when used with instruments that record negative measurements, e.g. UpDownCounter or ObservableGauge. - - Min (optional) Measurement value in population. - - Max (optional) Measurement value in population. - - - Args: - boundaries: Array of increasing values representing explicit bucket boundary values. - record_min_max: Whether to record min and max. - """ - - def __init__( - self, - boundaries: Sequence[float] = ( - 0.0, - 5.0, - 10.0, - 25.0, - 50.0, - 75.0, - 100.0, - 250.0, - 500.0, - 1000.0, - ), - record_min_max: bool = True, - ) -> None: - self._boundaries = boundaries - self._record_min_max = record_min_max - - def _create_aggregation(self, instrument: Instrument) -> _Aggregation: - return _ExplicitBucketHistogramAggregation( - boundaries=self._boundaries, - record_min_max=self._record_min_max, - ) - - -class SumAggregation(_AggregationFactory): - """This aggregation informs the SDK to collect: - - - The arithmetic sum of Measurement values. - """ - - def _create_aggregation(self, instrument: Instrument) -> _Aggregation: - - temporality = AggregationTemporality.UNSPECIFIED - if isinstance(instrument, Synchronous): - temporality = AggregationTemporality.DELTA - elif isinstance(instrument, Asynchronous): - temporality = AggregationTemporality.CUMULATIVE - - return _SumAggregation( - isinstance(instrument, (Counter, ObservableCounter)), - temporality, - ) - - -class LastValueAggregation(_AggregationFactory): - """ - This aggregation informs the SDK to collect: - - - The last Measurement. - - The timestamp of the last Measurement. - """ - - def _create_aggregation(self, instrument: Instrument) -> _Aggregation: - return _LastValueAggregation() - - -class DropAggregation(_AggregationFactory): - """Using this aggregation will make all measurements be ignored.""" - - def _create_aggregation(self, instrument: Instrument) -> _Aggregation: - return _DropAggregation() +__all__ = [] +for key, value in globals().copy().items(): + if not key.startswith("_"): + value.__module__ = __name__ + __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py index 00db5c1a91..06dc1e7776 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -12,206 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging -import os -from abc import ABC, abstractmethod -from enum import Enum -from os import environ, linesep -from sys import stdout -from threading import Event, RLock, Thread -from typing import IO, Callable, Dict, Iterable, List, Optional, Sequence - -from opentelemetry.context import ( - _SUPPRESS_INSTRUMENTATION_KEY, - attach, - detach, - set_value, +# pylint: disable=unused-import + +from opentelemetry.sdk._metrics._internal.export import ( # noqa: F401 + ConsoleMetricExporter, + InMemoryMetricReader, + MetricExporter, + MetricExportResult, + PeriodicExportingMetricReader, ) -from opentelemetry.sdk._metrics.aggregation import _AggregationFactory -from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric -from opentelemetry.util._once import Once - -_logger = logging.getLogger(__name__) - - -class MetricExportResult(Enum): - """Result of exporting a metric - - Can be any of the following values:""" - - SUCCESS = 0 - FAILURE = 1 - - -class MetricExporter(ABC): - """Interface for exporting metrics. - - Interface to be implemented by services that want to export metrics received - in their own format. - """ - - @abstractmethod - def export(self, metrics: Sequence[Metric]) -> "MetricExportResult": - """Exports a batch of telemetry data. - - Args: - metrics: The list of `opentelemetry.sdk._metrics.point.Metric` objects to be exported - - Returns: - The result of the export - """ - - @abstractmethod - def shutdown(self) -> None: - """Shuts down the exporter. - - Called when the SDK is shut down. - """ - - -class ConsoleMetricExporter(MetricExporter): - """Implementation of :class:`MetricExporter` that prints metrics to the - console. - - This class can be used for diagnostic purposes. It prints the exported - metrics to the console STDOUT. - """ - - def __init__( - self, - out: IO = stdout, - formatter: Callable[[Metric], str] = lambda metric: metric.to_json() - + linesep, - ): - self.out = out - self.formatter = formatter - - def export(self, metrics: Sequence[Metric]) -> MetricExportResult: - for metric in metrics: - self.out.write(self.formatter(metric)) - self.out.flush() - return MetricExportResult.SUCCESS - - def shutdown(self) -> None: - pass - - -class InMemoryMetricReader(MetricReader): - """Implementation of `MetricReader` that returns its metrics from :func:`get_metrics`. - - This is useful for e.g. unit tests. - """ - - def __init__( - self, - preferred_temporality: Dict[type, AggregationTemporality] = None, - preferred_aggregation: Dict[type, _AggregationFactory] = None, - ) -> None: - super().__init__( - preferred_temporality=preferred_temporality, - preferred_aggregation=preferred_aggregation, - ) - self._lock = RLock() - self._metrics: List[Metric] = [] - - def get_metrics(self) -> List[Metric]: - """Reads and returns current metrics from the SDK""" - with self._lock: - self.collect() - metrics = self._metrics - self._metrics = [] - return metrics - - def _receive_metrics(self, metrics: Iterable[Metric]): - with self._lock: - self._metrics = list(metrics) - - def shutdown(self): - pass - - -class PeriodicExportingMetricReader(MetricReader): - """`PeriodicExportingMetricReader` is an implementation of `MetricReader` - that collects metrics based on a user-configurable time interval, and passes the - metrics to the configured exporter. - """ - - def __init__( - self, - exporter: MetricExporter, - preferred_temporality: Dict[type, AggregationTemporality] = None, - preferred_aggregation: Dict[type, _AggregationFactory] = None, - export_interval_millis: Optional[float] = None, - export_timeout_millis: Optional[float] = None, - ) -> None: - super().__init__( - preferred_temporality=preferred_temporality, - preferred_aggregation=preferred_aggregation, - ) - self._exporter = exporter - if export_interval_millis is None: - try: - export_interval_millis = float( - environ.get("OTEL_METRIC_EXPORT_INTERVAL", 60000) - ) - except ValueError: - _logger.warning( - "Found invalid value for export interval, using default" - ) - export_interval_millis = 60000 - if export_timeout_millis is None: - try: - export_timeout_millis = float( - environ.get("OTEL_METRIC_EXPORT_TIMEOUT", 30000) - ) - except ValueError: - _logger.warning( - "Found invalid value for export timeout, using default" - ) - export_timeout_millis = 30000 - self._export_interval_millis = export_interval_millis - self._export_timeout_millis = export_timeout_millis - self._shutdown = False - self._shutdown_event = Event() - self._shutdown_once = Once() - self._daemon_thread = Thread(target=self._ticker, daemon=True) - self._daemon_thread.start() - if hasattr(os, "register_at_fork"): - os.register_at_fork( - after_in_child=self._at_fork_reinit - ) # pylint: disable=protected-access - - def _at_fork_reinit(self): - self._daemon_thread = Thread(target=self._ticker, daemon=True) - self._daemon_thread.start() - - def _ticker(self) -> None: - interval_secs = self._export_interval_millis / 1e3 - while not self._shutdown_event.wait(interval_secs): - self.collect() - # one last collection below before shutting down completely - self.collect() - - def _receive_metrics(self, metrics: Iterable[Metric]) -> None: - if metrics is None: - return - token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) - try: - self._exporter.export(metrics) - except Exception as e: # pylint: disable=broad-except,invalid-name - _logger.exception("Exception while exporting metrics %s", str(e)) - detach(token) - - def shutdown(self): - def _shutdown(): - self._shutdown = True - - did_set = self._shutdown_once.do_once(_shutdown) - if not did_set: - _logger.warning("Can't shutdown multiple times") - return - self._shutdown_event.set() - self._daemon_thread.join() - self._exporter.shutdown() +__all__ = [] +for key, value in globals().copy().items(): + if not key.startswith("_"): + value.__module__ = __name__ + __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py index 19902ac652..a154535c16 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py @@ -12,164 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=too-many-ancestors - -from logging import getLogger -from typing import TYPE_CHECKING, Dict, Generator, Iterable, Optional, Union - -from opentelemetry._metrics import CallbackT -from opentelemetry._metrics import Counter as APICounter -from opentelemetry._metrics import Histogram as APIHistogram -from opentelemetry._metrics import ObservableCounter as APIObservableCounter -from opentelemetry._metrics import ObservableGauge as APIObservableGauge -from opentelemetry._metrics import ( - ObservableUpDownCounter as APIObservableUpDownCounter, +# pylint: disable=unused-import + +from opentelemetry.sdk._metrics._internal.instrument import ( # noqa: F401 + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, ) -from opentelemetry._metrics import UpDownCounter as APIUpDownCounter -from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk.util.instrumentation import InstrumentationScope - -if TYPE_CHECKING: - from opentelemetry.sdk._metrics.measurement_consumer import ( - MeasurementConsumer, - ) - - -_logger = getLogger(__name__) - - -_ERROR_MESSAGE = ( - "Expected ASCII string of maximum length 63 characters but got {}" -) - - -class _Synchronous: - def __init__( - self, - name: str, - instrumentation_scope: InstrumentationScope, - measurement_consumer: "MeasurementConsumer", - unit: str = "", - description: str = "", - ): - # pylint: disable=no-member - is_name_valid, is_unit_valid = self._check_name_and_unit(name, unit) - - if not is_name_valid: - raise Exception(_ERROR_MESSAGE.format(name)) - - if not is_unit_valid: - raise Exception(_ERROR_MESSAGE.format(unit)) - self.name = name.lower() - self.unit = unit - self.description = description - self.instrumentation_scope = instrumentation_scope - self._measurement_consumer = measurement_consumer - super().__init__(name, unit=unit, description=description) - - -class _Asynchronous: - def __init__( - self, - name: str, - instrumentation_scope: InstrumentationScope, - measurement_consumer: "MeasurementConsumer", - callbacks: Optional[Iterable[CallbackT]] = None, - unit: str = "", - description: str = "", - ): - # pylint: disable=no-member - is_name_valid, is_unit_valid = self._check_name_and_unit(name, unit) - - if not is_name_valid: - raise Exception(_ERROR_MESSAGE.format(name)) - - if not is_unit_valid: - raise Exception(_ERROR_MESSAGE.format(unit)) - self.name = name.lower() - self.unit = unit - self.description = description - self.instrumentation_scope = instrumentation_scope - self._measurement_consumer = measurement_consumer - super().__init__(name, callbacks, unit=unit, description=description) - - self._callbacks = [] - - if callbacks is not None: - - for callback in callbacks: - - if isinstance(callback, Generator): - - def inner(callback=callback) -> Iterable[Measurement]: - return next(callback) - - self._callbacks.append(inner) - else: - self._callbacks.append(callback) - - def callback(self) -> Iterable[Measurement]: - for callback in self._callbacks: - try: - for api_measurement in callback(): - yield Measurement( - api_measurement.value, - instrument=self, - attributes=api_measurement.attributes, - ) - except StopIteration: - pass - except Exception: # pylint: disable=broad-except - _logger.exception( - "Callback failed for instrument %s.", self.name - ) - - -class Counter(_Synchronous, APICounter): - def add( - self, amount: Union[int, float], attributes: Dict[str, str] = None - ): - if amount < 0: - _logger.warning( - "Add amount must be non-negative on Counter %s.", self.name - ) - return - self._measurement_consumer.consume_measurement( - Measurement(amount, self, attributes) - ) - - -class UpDownCounter(_Synchronous, APIUpDownCounter): - def add( - self, amount: Union[int, float], attributes: Dict[str, str] = None - ): - self._measurement_consumer.consume_measurement( - Measurement(amount, self, attributes) - ) - - -class ObservableCounter(_Asynchronous, APIObservableCounter): - pass - - -class ObservableUpDownCounter(_Asynchronous, APIObservableUpDownCounter): - pass - - -class Histogram(_Synchronous, APIHistogram): - def record( - self, amount: Union[int, float], attributes: Dict[str, str] = None - ): - if amount < 0: - _logger.warning( - "Record amount must be non-negative on Histogram %s.", - self.name, - ) - return - self._measurement_consumer.consume_measurement( - Measurement(amount, self, attributes) - ) - -class ObservableGauge(_Asynchronous, APIObservableGauge): - pass +__all__ = [] +for key, value in globals().copy().items(): + if not key.startswith("_"): + value.__module__ = __name__ + __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py index 730d1509d0..2756930d44 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py @@ -12,21 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dataclasses import dataclass -from typing import TYPE_CHECKING, Union +# pylint: disable=unused-import -from opentelemetry.util.types import Attributes +from opentelemetry.sdk._metrics._internal.measurement import ( # noqa: F401 + Measurement, +) -if TYPE_CHECKING: - from opentelemetry.sdk._metrics.instrument import _Instrument - - -@dataclass(frozen=True) -class Measurement: - """ - Represents a data point reported via the metrics API to the SDK. - """ - - value: Union[int, float] - instrument: "_Instrument" - attributes: Attributes = None +__all__ = [] +for key, value in globals().copy().items(): + if not key.startswith("_"): + value.__module__ = __name__ + __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py index 7bcdce465d..d184e9fbef 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py @@ -12,168 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from abc import ABC, abstractmethod -from logging import getLogger -from os import environ -from typing import Callable, Dict, Iterable +# pylint: disable=unused-import -from typing_extensions import final - -from opentelemetry.sdk._metrics.aggregation import ( - DefaultAggregation, - _AggregationFactory, -) -from opentelemetry.sdk._metrics.instrument import ( - Counter, - Histogram, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, -) -from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric -from opentelemetry.sdk.environment_variables import ( - _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, +from opentelemetry.sdk._metrics._internal.metric_reader import ( # noqa: F401 + MetricReader, ) -_logger = getLogger(__name__) - - -class MetricReader(ABC): - """ - Base class for all metric readers - - Args: - preferred_temporality: A mapping between instrument classes and - aggregation temporality. By default uses CUMULATIVE for all instrument - classes. This mapping will be used to define the default aggregation - temporality of every instrument class. If the user wants to make a - change in the default aggregation temporality of an instrument class, - it is enough to pass here a dictionary whose keys are the instrument - classes and the values are the corresponding desired aggregation - temporalities of the classes that the user wants to change, not all of - them. The classes not included in the passed dictionary will retain - their association to their default aggregation temporalities. - The value passed here will override the corresponding values set - via the environment variable - preferred_aggregation: A mapping between instrument classes and - aggregation instances. By default maps all instrument classes to an - instance of `DefaultAggregation`. This mapping will be used to - define the default aggregation of every instrument class. If the - user wants to make a change in the default aggregation of an - instrument class, it is enough to pass here a dictionary whose keys - are the instrument classes and the values are the corresponding - desired aggregation for the instrument classes that the user wants - to change, not necessarily all of them. The classes not included in - the passed dictionary will retain their association to their - default aggregations. The aggregation defined here will be - overriden by an aggregation defined by a view that is not - `DefaultAggregation`. - - .. document protected _receive_metrics which is a intended to be overriden by subclass - .. automethod:: _receive_metrics - """ - - # FIXME add :std:envvar:`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` - # to the end of the documentation paragraph above. - - def __init__( - self, - preferred_temporality: Dict[type, AggregationTemporality] = None, - preferred_aggregation: Dict[type, _AggregationFactory] = None, - ) -> None: - self._collect: Callable[ - ["MetricReader", AggregationTemporality], Iterable[Metric] - ] = None - - if ( - environ.get( - _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, - "CUMULATIVE", - ) - .upper() - .strip() - == "DELTA" - ): - self._instrument_class_temporality = { - Counter: AggregationTemporality.DELTA, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.DELTA, - ObservableCounter: AggregationTemporality.DELTA, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - else: - self._instrument_class_temporality = { - Counter: AggregationTemporality.CUMULATIVE, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.CUMULATIVE, - ObservableCounter: AggregationTemporality.CUMULATIVE, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - if preferred_temporality is not None: - for temporality in preferred_temporality.values(): - if temporality not in ( - AggregationTemporality.CUMULATIVE, - AggregationTemporality.DELTA, - ): - raise Exception( - f"Invalid temporality value found {temporality}" - ) - - self._instrument_class_temporality.update(preferred_temporality or {}) - self._preferred_temporality = preferred_temporality - self._instrument_class_aggregation = { - Counter: DefaultAggregation(), - UpDownCounter: DefaultAggregation(), - Histogram: DefaultAggregation(), - ObservableCounter: DefaultAggregation(), - ObservableUpDownCounter: DefaultAggregation(), - ObservableGauge: DefaultAggregation(), - } - - self._instrument_class_aggregation.update(preferred_aggregation or {}) - - @final - def collect(self) -> None: - """Collects the metrics from the internal SDK state and - invokes the `_receive_metrics` with the collection. - """ - if self._collect is None: - _logger.warning( - "Cannot call collect on a MetricReader until it is registered on a MeterProvider" - ) - return - self._receive_metrics( - self._collect(self, self._instrument_class_temporality) - ) - - @final - def _set_collect_callback( - self, - func: Callable[ - ["MetricReader", AggregationTemporality], Iterable[Metric] - ], - ) -> None: - """This function is internal to the SDK. It should not be called or overriden by users""" - self._collect = func - - @abstractmethod - def _receive_metrics(self, metrics: Iterable[Metric]): - """Called by `MetricReader.collect` when it receives a batch of metrics""" - - @abstractmethod - def shutdown(self): - """Shuts down the MetricReader. This method provides a way - for the MetricReader to do any cleanup required. A metric reader can - only be shutdown once, any subsequent calls are ignored and return - failure status. - - When a `MetricReader` is registered on a - :class:`~opentelemetry.sdk._metrics.MeterProvider`, - :meth:`~opentelemetry.sdk._metrics.MeterProvider.shutdown` will invoke this - automatically. - """ +__all__ = [] +for key, value in globals().copy().items(): + if not key.startswith("_"): + value.__module__ = __name__ + __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py index 6d55858a63..190b85533e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py @@ -12,99 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -from dataclasses import asdict, dataclass -from enum import IntEnum -from typing import Sequence, Union - -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.util.types import Attributes - - -class AggregationTemporality(IntEnum): - """ - The temporality to use when aggregating data. - - Can be one of the following values: - """ - - UNSPECIFIED = 0 - DELTA = 1 - CUMULATIVE = 2 - - -@dataclass(frozen=True) -class Sum: - """Represents the type of a scalar metric that is calculated as a sum of - all reported measurements over a time interval.""" - - aggregation_temporality: AggregationTemporality - is_monotonic: bool - start_time_unix_nano: int - time_unix_nano: int - value: Union[int, float] - - -@dataclass(frozen=True) -class Gauge: - """Represents the type of a scalar metric that always exports the current - value for every data point. It should be used for an unknown - aggregation.""" - - time_unix_nano: int - value: Union[int, float] - - -@dataclass(frozen=True) -class Histogram: - """Represents the type of a metric that is calculated by aggregating as a - histogram of all reported measurements over a time interval.""" - - aggregation_temporality: AggregationTemporality - bucket_counts: Sequence[int] - explicit_bounds: Sequence[float] - max: int - min: int - start_time_unix_nano: int - sum: Union[int, float] - time_unix_nano: int - - -PointT = Union[Sum, Gauge, Histogram] - - -@dataclass(frozen=True) -class Metric: - """Represents a metric point in the OpenTelemetry data model to be exported - - Concrete metric types contain all the information as in the OTLP proto definitions - (https://github.com/open-telemetry/opentelemetry-proto/blob/b43e9b18b76abf3ee040164b55b9c355217151f3/opentelemetry/proto/metrics/v1/metrics.proto#L37) but are flattened as much as possible. - """ - - # common fields to all metric kinds - attributes: Attributes - description: str - instrumentation_scope: InstrumentationScope - name: str - resource: Resource - unit: str - point: PointT - """Contains non-common fields for the given metric""" - - def to_json(self) -> str: - return json.dumps( - { - "attributes": self.attributes if self.attributes else "", - "description": self.description if self.description else "", - "instrumentation_scope": repr(self.instrumentation_scope) - if self.instrumentation_scope - else "", - "name": self.name, - "resource": repr(self.resource.attributes) - if self.resource - else "", - "unit": self.unit if self.unit else "", - "point": asdict(self.point) if self.point else "", - } - ) +# pylint: disable=unused-import + +# FIXME Remove when 3.6 is no longer supported +from sys import version_info as _version_info + +from opentelemetry.sdk._metrics._internal.point import ( # noqa: F401 + AggregationTemporality, + Gauge, + Histogram, + Metric, + PointT, + Sum, +) + +__all__ = [] +for key, value in globals().copy().items(): + if not key.startswith("_"): + if _version_info.minor == 6 and key == "PointT": + continue + value.__module__ = __name__ + __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py index 6bfde007bd..debd9c3946 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py @@ -12,150 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=unused-import -from fnmatch import fnmatch -from logging import getLogger -from typing import Optional, Set, Type +from opentelemetry.sdk._metrics._internal.view import View # noqa: F401 -# FIXME import from typing when support for 3.6 is removed -from typing_extensions import final - -from opentelemetry._metrics import Instrument -from opentelemetry.sdk._metrics.aggregation import ( - DefaultAggregation, - _AggregationFactory, -) - -_logger = getLogger(__name__) - - -class View: - """ - A `View` configuration parameters can be used for the following - purposes: - - 1. Match instruments: When an instrument matches a view, measurements - received by that instrument will be processed. - 2. Customize metric streams: A metric stream is identified by a match - between a view and an instrument and a set of attributes. The metric - stream can be customized by certain attributes of the corresponding view. - - The attributes documented next serve one of the previous two purposes. - - Args: - instrument_type: This is an instrument matching attribute: the class the - instrument must be to match the view. - - instrument_name: This is an instrument matching attribute: the name the - instrument must have to match the view. Wild card characters are supported. Wild - card characters should not be used with this attribute if the view has also a - ``name`` defined. - - meter_name: This is an instrument matching attribute: the name the - instrument meter must have to match the view. - - meter_version: This is an instrument matching attribute: the version - the instrument meter must have to match the view. - - meter_schema_url: This is an instrument matching attribute: the schema - URL the instrument meter must have to match the view. - - name: This is a metric stream customizing attribute: the name of the - metric stream. If `None`, the name of the instrument will be used. - - description: This is a metric stream customizing attribute: the - description of the metric stream. If `None`, the description of the instrument will - be used. - - attribute_keys: This is a metric stream customizing attribute: this is - a set of attribute keys. If not `None` then only the measurement attributes that - are in ``attribute_keys`` will be used to identify the metric stream. - - aggregation: This is a metric stream customizing attribute: the - aggregation instance to use when data is aggregated for the - corresponding metrics stream. If `None` an instance of - `DefaultAggregation` will be used. - - This class is not intended to be subclassed by the user. - """ - - _default_aggregation = DefaultAggregation() - - def __init__( - self, - instrument_type: Optional[Type[Instrument]] = None, - instrument_name: Optional[str] = None, - meter_name: Optional[str] = None, - meter_version: Optional[str] = None, - meter_schema_url: Optional[str] = None, - name: Optional[str] = None, - description: Optional[str] = None, - attribute_keys: Optional[Set[str]] = None, - aggregation: Optional[_AggregationFactory] = None, - ): - if ( - instrument_type - is instrument_name - is meter_name - is meter_version - is meter_schema_url - is None - ): - raise Exception( - "Some instrument selection " - f"criteria must be provided for View {name}" - ) - - if ( - name is not None - and instrument_name is not None - and ("*" in instrument_name or "?" in instrument_name) - ): - - raise Exception( - f"View {name} declared with wildcard " - "characters in instrument_name" - ) - - # _name, _description, _aggregation and _attribute_keys will be - # accessed when instantiating a _ViewInstrumentMatch. - self._name = name - self._instrument_type = instrument_type - self._instrument_name = instrument_name - self._meter_name = meter_name - self._meter_version = meter_version - self._meter_schema_url = meter_schema_url - - self._description = description - self._attribute_keys = attribute_keys - self._aggregation = aggregation or self._default_aggregation - - # pylint: disable=too-many-return-statements - # pylint: disable=too-many-branches - @final - def _match(self, instrument: Instrument) -> bool: - - if self._instrument_type is not None: - if not isinstance(instrument, self._instrument_type): - return False - - if self._instrument_name is not None: - if not fnmatch(instrument.name, self._instrument_name): - return False - - if self._meter_name is not None: - if instrument.instrumentation_scope.name != self._meter_name: - return False - - if self._meter_version is not None: - if instrument.instrumentation_scope.version != self._meter_version: - return False - - if self._meter_schema_url is not None: - if ( - instrument.instrumentation_scope.schema_url - != self._meter_schema_url - ): - return False - - return True +__all__ = [] +for key, value in globals().copy().items(): + if not key.startswith("_"): + value.__module__ = __name__ + __all__.append(key) diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 6f4582ddce..9ef7c469ff 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -20,17 +20,17 @@ from unittest import TestCase from unittest.mock import Mock -from opentelemetry.sdk._metrics import instrument +from opentelemetry.sdk._metrics._internal.aggregation import ( + _convert_aggregation_temporality, + _ExplicitBucketHistogramAggregation, + _LastValueAggregation, + _SumAggregation, +) from opentelemetry.sdk._metrics.aggregation import ( - AggregationTemporality, DefaultAggregation, ExplicitBucketHistogramAggregation, LastValueAggregation, SumAggregation, - _convert_aggregation_temporality, - _ExplicitBucketHistogramAggregation, - _LastValueAggregation, - _SumAggregation, ) from opentelemetry.sdk._metrics.instrument import ( Counter, @@ -41,7 +41,7 @@ UpDownCounter, ) from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import Gauge +from opentelemetry.sdk._metrics.point import AggregationTemporality, Gauge from opentelemetry.sdk._metrics.point import Histogram as HistogramPoint from opentelemetry.sdk._metrics.point import Sum from opentelemetry.util.types import Attributes @@ -812,7 +812,7 @@ def test_aggregation_temporality_to_delta(self): class TestAggregationFactory(TestCase): def test_sum_factory(self): - counter = instrument.Counter("name", Mock(), Mock()) + counter = Counter("name", Mock(), Mock()) factory = SumAggregation() aggregation = factory._create_aggregation(counter) self.assertIsInstance(aggregation, _SumAggregation) @@ -823,7 +823,7 @@ def test_sum_factory(self): aggregation2 = factory._create_aggregation(counter) self.assertNotEqual(aggregation, aggregation2) - counter = instrument.UpDownCounter("name", Mock(), Mock()) + counter = UpDownCounter("name", Mock(), Mock()) factory = SumAggregation() aggregation = factory._create_aggregation(counter) self.assertIsInstance(aggregation, _SumAggregation) @@ -832,7 +832,7 @@ def test_sum_factory(self): aggregation._instrument_temporality, AggregationTemporality.DELTA ) - counter = instrument.ObservableCounter("name", Mock(), Mock(), None) + counter = ObservableCounter("name", Mock(), Mock(), None) factory = SumAggregation() aggregation = factory._create_aggregation(counter) self.assertIsInstance(aggregation, _SumAggregation) @@ -843,7 +843,7 @@ def test_sum_factory(self): ) def test_explicit_bucket_histogram_factory(self): - histo = instrument.Histogram("name", Mock(), Mock()) + histo = Histogram("name", Mock(), Mock()) factory = ExplicitBucketHistogramAggregation( boundaries=( 0.0, @@ -859,7 +859,7 @@ def test_explicit_bucket_histogram_factory(self): self.assertNotEqual(aggregation, aggregation2) def test_last_value_factory(self): - counter = instrument.Counter("name", Mock(), Mock()) + counter = Counter("name", Mock(), Mock()) factory = LastValueAggregation() aggregation = factory._create_aggregation(counter) self.assertIsInstance(aggregation, _LastValueAggregation) diff --git a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py index 3b58ba4488..4d086588d7 100644 --- a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py +++ b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py @@ -15,15 +15,20 @@ from unittest import TestCase from unittest.mock import MagicMock, Mock, patch -from opentelemetry.sdk._metrics.measurement_consumer import ( +from opentelemetry.sdk._metrics._internal.measurement_consumer import ( MeasurementConsumer, SynchronousMeasurementConsumer, ) +from opentelemetry.sdk._metrics._internal.sdk_configuration import ( + SdkConfiguration, +) from opentelemetry.sdk._metrics.point import AggregationTemporality -from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration -@patch("opentelemetry.sdk._metrics.measurement_consumer.MetricReaderStorage") +@patch( + "opentelemetry.sdk._metrics._internal." + "measurement_consumer.MetricReaderStorage" +) class TestSynchronousMeasurementConsumer(TestCase): def test_parent(self, _): diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_metric_reader.py index 200b8824b8..48e69ea800 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader.py @@ -17,11 +17,10 @@ from unittest import TestCase from unittest.mock import patch +from opentelemetry.sdk._metrics._internal.aggregation import Aggregation from opentelemetry.sdk._metrics.aggregation import ( - AggregationTemporality, DefaultAggregation, LastValueAggregation, - _AggregationFactory, ) from opentelemetry.sdk._metrics.instrument import ( Counter, @@ -32,6 +31,7 @@ UpDownCounter, ) from opentelemetry.sdk._metrics.metric_reader import MetricReader +from opentelemetry.sdk._metrics.point import AggregationTemporality from opentelemetry.sdk.environment_variables import ( _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, ) @@ -41,7 +41,7 @@ class DummyMetricReader(MetricReader): def __init__( self, preferred_temporality: Dict[type, AggregationTemporality] = None, - preferred_aggregation: Dict[type, _AggregationFactory] = None, + preferred_aggregation: Dict[type, Aggregation] = None, ) -> None: super().__init__( preferred_temporality=preferred_temporality, diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index d0d05854d8..919efece68 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -14,17 +14,19 @@ from unittest.mock import MagicMock, Mock, patch +from opentelemetry.sdk._metrics._internal.metric_reader_storage import ( + MetricReaderStorage, +) +from opentelemetry.sdk._metrics._internal.sdk_configuration import ( + SdkConfiguration, +) from opentelemetry.sdk._metrics.aggregation import ( DefaultAggregation, DropAggregation, ) from opentelemetry.sdk._metrics.instrument import Counter from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.metric_reader_storage import ( - MetricReaderStorage, -) from opentelemetry.sdk._metrics.point import AggregationTemporality -from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration from opentelemetry.sdk._metrics.view import View from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc @@ -43,7 +45,8 @@ def mock_instrument() -> Mock: class TestMetricReaderStorage(ConcurrencyTestBase): @patch( - "opentelemetry.sdk._metrics.metric_reader_storage._ViewInstrumentMatch" + "opentelemetry.sdk._metrics._internal" + ".metric_reader_storage._ViewInstrumentMatch" ) def test_creates_view_instrument_matches( self, MockViewInstrumentMatch: Mock @@ -80,7 +83,8 @@ def test_creates_view_instrument_matches( self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 1) @patch( - "opentelemetry.sdk._metrics.metric_reader_storage._ViewInstrumentMatch" + "opentelemetry.sdk._metrics._internal." + "metric_reader_storage._ViewInstrumentMatch" ) def test_forwards_calls_to_view_instrument_match( self, MockViewInstrumentMatch: Mock @@ -139,7 +143,8 @@ def test_forwards_calls_to_view_instrument_match( self.assertEqual(result, all_metrics) @patch( - "opentelemetry.sdk._metrics.metric_reader_storage._ViewInstrumentMatch" + "opentelemetry.sdk._metrics._internal." + "metric_reader_storage._ViewInstrumentMatch" ) def test_race_concurrent_measurements(self, MockViewInstrumentMatch: Mock): mock_view_instrument_match_ctor = MockFunc() @@ -166,7 +171,8 @@ def send_measurement(): self.assertEqual(mock_view_instrument_match_ctor.call_count, 1) @patch( - "opentelemetry.sdk._metrics.metric_reader_storage._ViewInstrumentMatch" + "opentelemetry.sdk._metrics._internal." + "metric_reader_storage._ViewInstrumentMatch" ) def test_default_view_enabled(self, MockViewInstrumentMatch: Mock): """Instruments should be matched with default views when enabled""" diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 81d1068fb8..ca25631935 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -216,7 +216,7 @@ def test_shutdown_subsequent_calls(self): with self.assertLogs(level=WARNING): meter_provider.shutdown() - @patch("opentelemetry.sdk._metrics._logger") + @patch("opentelemetry.sdk._metrics._internal._logger") def test_shutdown_race(self, mock_logger): mock_logger.warning = MockFunc() meter_provider = MeterProvider() @@ -226,7 +226,10 @@ def test_shutdown_race(self, mock_logger): ) self.assertEqual(mock_logger.warning.call_count, num_threads - 1) - @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + @patch( + "opentelemetry.sdk._metrics._internal." + "SynchronousMeasurementConsumer" + ) def test_measurement_collect_callback( self, mock_sync_measurement_consumer ): @@ -247,14 +250,20 @@ def test_measurement_collect_callback( sync_consumer_instance.collect.call_count, len(metric_readers) ) - @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + @patch( + "opentelemetry.sdk._metrics." + "_internal.SynchronousMeasurementConsumer" + ) def test_creates_sync_measurement_consumer( self, mock_sync_measurement_consumer ): MeterProvider() mock_sync_measurement_consumer.assert_called() - @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + @patch( + "opentelemetry.sdk._metrics." + "_internal.SynchronousMeasurementConsumer" + ) def test_register_asynchronous_instrument( self, mock_sync_measurement_consumer ): @@ -277,7 +286,10 @@ def test_register_asynchronous_instrument( ) ) - @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + @patch( + "opentelemetry.sdk._metrics._internal." + "SynchronousMeasurementConsumer" + ) def test_consume_measurement_counter(self, mock_sync_measurement_consumer): sync_consumer_instance = mock_sync_measurement_consumer() meter_provider = MeterProvider() @@ -287,7 +299,10 @@ def test_consume_measurement_counter(self, mock_sync_measurement_consumer): sync_consumer_instance.consume_measurement.assert_called() - @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + @patch( + "opentelemetry.sdk._metrics." + "_internal.SynchronousMeasurementConsumer" + ) def test_consume_measurement_up_down_counter( self, mock_sync_measurement_consumer ): @@ -301,7 +316,10 @@ def test_consume_measurement_up_down_counter( sync_consumer_instance.consume_measurement.assert_called() - @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer") + @patch( + "opentelemetry.sdk._metrics._internal." + "SynchronousMeasurementConsumer" + ) def test_consume_measurement_histogram( self, mock_sync_measurement_consumer ): diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 9d91c3675d..7e6f17644a 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -15,20 +15,24 @@ from unittest import TestCase from unittest.mock import MagicMock, Mock -from opentelemetry.sdk._metrics._view_instrument_match import ( +from opentelemetry.sdk._metrics._internal._view_instrument_match import ( _ViewInstrumentMatch, ) +from opentelemetry.sdk._metrics._internal.aggregation import ( + _DropAggregation, + _LastValueAggregation, +) +from opentelemetry.sdk._metrics._internal.sdk_configuration import ( + SdkConfiguration, +) from opentelemetry.sdk._metrics.aggregation import ( DefaultAggregation, DropAggregation, LastValueAggregation, - _DropAggregation, - _LastValueAggregation, ) from opentelemetry.sdk._metrics.instrument import Counter from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric -from opentelemetry.sdk._metrics.sdk_configuration import SdkConfiguration from opentelemetry.sdk._metrics.view import View From 739760586ade075b18d29fa58ce7ac3dbae90869 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 4 May 2022 14:26:30 -0600 Subject: [PATCH 1212/1517] Add variadic arguments to metric exporter/reader interfaces (#2654) * Add variadic arguments to metric exporter/reader interfaces Fixes #2650 * Add changelog entry * Remove args * Add args again This reverts commit 7eb61349ee0f45aaeb64888bc107027c3deaa2d1. * Add missing args --- CHANGELOG.md | 2 ++ .../proto/grpc/_metric_exporter/__init__.py | 6 +++-- .../exporter/prometheus/__init__.py | 6 +++-- .../sdk/_metrics/_internal/export/__init__.py | 22 ++++++++++++------- .../sdk/_metrics/_internal/metric_reader.py | 4 ++-- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d43389891c..c98bb873ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.1-0.30b1...HEAD) +- Add variadic arguments to metric exporter/reader interfaces + ([#2654](https://github.com/open-telemetry/opentelemetry-python/pull/2654)) - Move Metrics API behind internal package ([#2651](https://github.com/open-telemetry/opentelemetry-python/pull/2651)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py index 1ad5cc4d80..c40ff1ddd2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py @@ -168,8 +168,10 @@ def _translate_data( ) ) - def export(self, metrics: Sequence[Metric]) -> MetricExportResult: + def export( + self, metrics: Sequence[Metric], *args, **kwargs + ) -> MetricExportResult: return self._export(metrics) - def shutdown(self): + def shutdown(self, *args, **kwargs): pass diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 4d50662675..1b81f062c0 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -110,12 +110,14 @@ def __init__(self, prefix: str = "") -> None: REGISTRY.register(self._collector) self._collector._callback = self.collect - def _receive_metrics(self, metrics: Iterable[Metric]) -> None: + def _receive_metrics( + self, metrics: Iterable[Metric], *args, **kwargs + ) -> None: if metrics is None: return self._collector.add_metrics_data(metrics) - def shutdown(self) -> bool: + def shutdown(self, *args, **kwargs) -> bool: REGISTRY.unregister(self._collector) return True diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py index b99d7f8f03..959006e234 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py @@ -52,7 +52,9 @@ class MetricExporter(ABC): """ @abstractmethod - def export(self, metrics: Sequence[Metric]) -> "MetricExportResult": + def export( + self, metrics: Sequence[Metric], *args, **kwargs + ) -> "MetricExportResult": """Exports a batch of telemetry data. Args: @@ -63,7 +65,7 @@ def export(self, metrics: Sequence[Metric]) -> "MetricExportResult": """ @abstractmethod - def shutdown(self) -> None: + def shutdown(self, *args, **kwargs) -> None: """Shuts down the exporter. Called when the SDK is shut down. @@ -87,13 +89,15 @@ def __init__( self.out = out self.formatter = formatter - def export(self, metrics: Sequence[Metric]) -> MetricExportResult: + def export( + self, metrics: Sequence[Metric], *args, **kwargs + ) -> MetricExportResult: for metric in metrics: self.out.write(self.formatter(metric)) self.out.flush() return MetricExportResult.SUCCESS - def shutdown(self) -> None: + def shutdown(self, *args, **kwargs) -> None: pass @@ -123,11 +127,11 @@ def get_metrics(self) -> List[Metric]: self._metrics = [] return metrics - def _receive_metrics(self, metrics: Iterable[Metric]): + def _receive_metrics(self, metrics: Iterable[Metric], *args, **kwargs): with self._lock: self._metrics = list(metrics) - def shutdown(self): + def shutdown(self, *args, **kwargs): pass @@ -193,7 +197,9 @@ def _ticker(self) -> None: # one last collection below before shutting down completely self.collect() - def _receive_metrics(self, metrics: Iterable[Metric]) -> None: + def _receive_metrics( + self, metrics: Iterable[Metric], *args, **kwargs + ) -> None: if metrics is None: return token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) @@ -203,7 +209,7 @@ def _receive_metrics(self, metrics: Iterable[Metric]) -> None: _logger.exception("Exception while exporting metrics %s", str(e)) detach(token) - def shutdown(self): + def shutdown(self, *args, **kwargs): def _shutdown(): self._shutdown = True diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py index b84b873854..afdc708316 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py @@ -162,11 +162,11 @@ def _set_collect_callback( self._collect = func @abstractmethod - def _receive_metrics(self, metrics: Iterable[Metric]): + def _receive_metrics(self, metrics: Iterable[Metric], *args, **kwargs): """Called by `MetricReader.collect` when it receives a batch of metrics""" @abstractmethod - def shutdown(self): + def shutdown(self, *args, **kwargs): """Shuts down the MetricReader. This method provides a way for the MetricReader to do any cleanup required. A metric reader can only be shutdown once, any subsequent calls are ignored and return From a72c7de6e6c374cf4d403755c7f8a5cfbd07e29f Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 6 May 2022 14:29:11 -0400 Subject: [PATCH 1213/1517] Add timeouts to metric SDK (#2653) * Add timeouts to metric SDK * comments * don't use TimeoutError as it is intended for OS related timeouts * changelog and typo * isort * fix _time_ns import * Update CHANGELOG.md Co-authored-by: Srikanth Chekuri * use self.fail in tests Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 + .../proto/grpc/_metric_exporter/__init__.py | 8 +- .../exporter/prometheus/__init__.py | 8 +- .../sdk/_metrics/_internal/__init__.py | 26 ++++-- .../sdk/_metrics/_internal/export/__init__.py | 45 +++++++--- .../sdk/_metrics/_internal/metric_reader.py | 14 ++- .../tests/metrics/test_backward_compat.py | 89 +++++++++++++++++++ .../tests/metrics/test_metric_reader.py | 15 ++-- .../tests/metrics/test_metrics.py | 20 +++-- .../test_periodic_exporting_metric_reader.py | 11 ++- 10 files changed, 196 insertions(+), 42 deletions(-) create mode 100644 opentelemetry-sdk/tests/metrics/test_backward_compat.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c98bb873ce..1614019b28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.1-0.30b1...HEAD) +- Add timeouts to metric SDK + ([#2653](https://github.com/open-telemetry/opentelemetry-python/pull/2653)) - Add variadic arguments to metric exporter/reader interfaces ([#2654](https://github.com/open-telemetry/opentelemetry-python/pull/2654)) - Move Metrics API behind internal package diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py index c40ff1ddd2..693eb261db 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py @@ -169,9 +169,13 @@ def _translate_data( ) def export( - self, metrics: Sequence[Metric], *args, **kwargs + self, + metrics: Sequence[Metric], + timeout_millis: float = 10_000, + **kwargs, ) -> MetricExportResult: + # TODO(#2663): OTLPExporterMixin should pass timeout to gRPC return self._export(metrics) - def shutdown(self, *args, **kwargs): + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 1b81f062c0..448a04bf29 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -111,15 +111,17 @@ def __init__(self, prefix: str = "") -> None: self._collector._callback = self.collect def _receive_metrics( - self, metrics: Iterable[Metric], *args, **kwargs + self, + metrics: Iterable[Metric], + timeout_millis: float = 10_000, + **kwargs, ) -> None: if metrics is None: return self._collector.add_metrics_data(metrics) - def shutdown(self, *args, **kwargs) -> bool: + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: REGISTRY.unregister(self._collector) - return True class _CustomCollector: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py index 39256b7327..eebcb0b13c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py @@ -48,6 +48,7 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util._once import Once +from opentelemetry.util._time import _time_ns _logger = getLogger(__name__) @@ -369,16 +370,20 @@ def __init__( self._shutdown_once = Once() self._shutdown = False - def force_flush(self) -> bool: - - # FIXME implement a timeout + def force_flush(self, timeout_millis: float = 10_000) -> bool: + deadline_ns = _time_ns() + timeout_millis * 10**6 for metric_reader in self._sdk_config.metric_readers: - metric_reader.collect() + current_ts = _time_ns() + if current_ts >= deadline_ns: + raise Exception("Timed out while flushing metric readers") + metric_reader.collect( + timeout_millis=(deadline_ns - current_ts) / 10**6 + ) return True - def shutdown(self): - # FIXME implement a timeout + def shutdown(self, timeout_millis: float = 30_000): + deadline_ns = _time_ns() + timeout_millis * 10**6 def _shutdown(): self._shutdown = True @@ -392,8 +397,15 @@ def _shutdown(): metric_reader_error = {} for metric_reader in self._sdk_config.metric_readers: + current_ts = _time_ns() try: - metric_reader.shutdown() + if current_ts >= deadline_ns: + raise Exception( + "Didn't get to execute, deadline already exceeded" + ) + metric_reader.shutdown( + timeout_millis=(deadline_ns - current_ts) / 10**6 + ) # pylint: disable=broad-except except Exception as error: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py index 959006e234..c8dcb43327 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py @@ -31,6 +31,7 @@ from opentelemetry.sdk._metrics.metric_reader import MetricReader from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric from opentelemetry.util._once import Once +from opentelemetry.util._time import _time_ns _logger = logging.getLogger(__name__) @@ -53,8 +54,11 @@ class MetricExporter(ABC): @abstractmethod def export( - self, metrics: Sequence[Metric], *args, **kwargs - ) -> "MetricExportResult": + self, + metrics: Sequence[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> MetricExportResult: """Exports a batch of telemetry data. Args: @@ -65,7 +69,7 @@ def export( """ @abstractmethod - def shutdown(self, *args, **kwargs) -> None: + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: """Shuts down the exporter. Called when the SDK is shut down. @@ -90,14 +94,17 @@ def __init__( self.formatter = formatter def export( - self, metrics: Sequence[Metric], *args, **kwargs + self, + metrics: Sequence[Metric], + timeout_millis: float = 10_000, + **kwargs, ) -> MetricExportResult: for metric in metrics: self.out.write(self.formatter(metric)) self.out.flush() return MetricExportResult.SUCCESS - def shutdown(self, *args, **kwargs) -> None: + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass @@ -127,11 +134,16 @@ def get_metrics(self) -> List[Metric]: self._metrics = [] return metrics - def _receive_metrics(self, metrics: Iterable[Metric], *args, **kwargs): + def _receive_metrics( + self, + metrics: Iterable[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> None: with self._lock: self._metrics = list(metrics) - def shutdown(self, *args, **kwargs): + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass @@ -193,23 +205,28 @@ def _at_fork_reinit(self): def _ticker(self) -> None: interval_secs = self._export_interval_millis / 1e3 while not self._shutdown_event.wait(interval_secs): - self.collect() + self.collect(timeout_millis=self._export_timeout_millis) # one last collection below before shutting down completely - self.collect() + self.collect(timeout_millis=self._export_interval_millis) def _receive_metrics( - self, metrics: Iterable[Metric], *args, **kwargs + self, + metrics: Iterable[Metric], + timeout_millis: float = 10_000, + **kwargs, ) -> None: if metrics is None: return token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: - self._exporter.export(metrics) + self._exporter.export(metrics, timeout_millis=timeout_millis) except Exception as e: # pylint: disable=broad-except,invalid-name _logger.exception("Exception while exporting metrics %s", str(e)) detach(token) - def shutdown(self, *args, **kwargs): + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + deadline_ns = _time_ns() + timeout_millis * 10**6 + def _shutdown(): self._shutdown = True @@ -219,5 +236,5 @@ def _shutdown(): return self._shutdown_event.set() - self._daemon_thread.join() - self._exporter.shutdown() + self._daemon_thread.join(timeout=(deadline_ns - _time_ns()) / 10**9) + self._exporter.shutdown(timeout=(deadline_ns - _time_ns()) / 10**6) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py index afdc708316..d949a34047 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py @@ -138,7 +138,7 @@ def __init__( self._instrument_class_aggregation.update(preferred_aggregation or {}) @final - def collect(self) -> None: + def collect(self, timeout_millis: float = 10_000) -> None: """Collects the metrics from the internal SDK state and invokes the `_receive_metrics` with the collection. """ @@ -148,7 +148,8 @@ def collect(self) -> None: ) return self._receive_metrics( - self._collect(self, self._instrument_class_temporality) + self._collect(self, self._instrument_class_temporality), + timeout_millis=timeout_millis, ) @final @@ -162,11 +163,16 @@ def _set_collect_callback( self._collect = func @abstractmethod - def _receive_metrics(self, metrics: Iterable[Metric], *args, **kwargs): + def _receive_metrics( + self, + metrics: Iterable[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> None: """Called by `MetricReader.collect` when it receives a batch of metrics""" @abstractmethod - def shutdown(self, *args, **kwargs): + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: """Shuts down the MetricReader. This method provides a way for the MetricReader to do any cleanup required. A metric reader can only be shutdown once, any subsequent calls are ignored and return diff --git a/opentelemetry-sdk/tests/metrics/test_backward_compat.py b/opentelemetry-sdk/tests/metrics/test_backward_compat.py new file mode 100644 index 0000000000..4b3b504d65 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_backward_compat.py @@ -0,0 +1,89 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The purpose of this test is to test for backward compatibility with any user-implementable +interfaces as they were originally defined. For example, changes to the MetricExporter ABC must +be made in such a way that existing implementations (outside of this repo) continue to work +when *called* by the SDK. + +This does not apply to classes which are not intended to be overriden by the user e.g. Meter +and PeriodicExportingMetricReader concrete class. Those may freely be modified in a +backward-compatible way for *callers*. + +Ideally, we could use mypy for this as well, but SDK is not type checked atm. +""" + +from typing import Iterable, Sequence +from unittest import TestCase + +from opentelemetry.sdk._metrics import MeterProvider +from opentelemetry.sdk._metrics.export import ( + MetricExporter, + MetricExportResult, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk._metrics.metric_reader import MetricReader +from opentelemetry.sdk._metrics.point import Metric + + +# Do not change these classes until after major version 1 +class OrigMetricExporter(MetricExporter): + def export( + self, + metrics: Sequence[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> MetricExportResult: + pass + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + pass + + +class OrigMetricReader(MetricReader): + def _receive_metrics( + self, + metrics: Iterable[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> None: + pass + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + self.collect() + + +class TestBackwardCompat(TestCase): + def test_metric_exporter(self): + exporter = OrigMetricExporter() + meter_provider = MeterProvider( + metric_readers=[PeriodicExportingMetricReader(exporter)] + ) + # produce some data + meter_provider.get_meter("foo").create_counter("mycounter").add(12) + try: + meter_provider.shutdown() + except Exception: + self.fail() + + def test_metric_reader(self): + reader = OrigMetricReader() + meter_provider = MeterProvider(metric_readers=[reader]) + # produce some data + meter_provider.get_meter("foo").create_counter("mycounter").add(12) + try: + meter_provider.shutdown() + except Exception: + self.fail() diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_metric_reader.py index 48e69ea800..1b95953cf8 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader.py @@ -13,12 +13,12 @@ # limitations under the License. from os import environ -from typing import Dict +from typing import Dict, Iterable from unittest import TestCase from unittest.mock import patch -from opentelemetry.sdk._metrics._internal.aggregation import Aggregation from opentelemetry.sdk._metrics.aggregation import ( + Aggregation, DefaultAggregation, LastValueAggregation, ) @@ -31,7 +31,7 @@ UpDownCounter, ) from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.point import AggregationTemporality +from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric from opentelemetry.sdk.environment_variables import ( _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, ) @@ -48,10 +48,15 @@ def __init__( preferred_aggregation=preferred_aggregation, ) - def _receive_metrics(self, metrics): + def _receive_metrics( + self, + metrics: Iterable[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> None: pass - def shutdown(self): + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: return True diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index ca25631935..3b57163801 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -15,7 +15,7 @@ from logging import WARNING from time import sleep -from typing import Sequence +from typing import Iterable, Sequence from unittest import TestCase from unittest.mock import MagicMock, Mock, patch @@ -46,10 +46,15 @@ class DummyMetricReader(MetricReader): def __init__(self): super().__init__() - def _receive_metrics(self, metrics): + def _receive_metrics( + self, + metrics: Iterable[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> None: pass - def shutdown(self): + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: return True @@ -433,12 +438,17 @@ def __init__(self): self.metrics = {} self._counter = 0 - def export(self, metrics: Sequence[Metric]) -> MetricExportResult: + def export( + self, + metrics: Sequence[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> MetricExportResult: self.metrics[self._counter] = metrics self._counter += 1 return MetricExportResult.SUCCESS - def shutdown(self) -> None: + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index ff67e848af..d2ad8cdccc 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -13,12 +13,14 @@ # limitations under the License. import time +from typing import Sequence from unittest.mock import Mock from flaky import flaky from opentelemetry.sdk._metrics.export import ( MetricExporter, + MetricExportResult, PeriodicExportingMetricReader, ) from opentelemetry.sdk._metrics.point import Gauge, Metric, Sum @@ -33,12 +35,17 @@ def __init__(self, wait=0): self.metrics = [] self._shutdown = False - def export(self, metrics): + def export( + self, + metrics: Sequence[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> MetricExportResult: time.sleep(self.wait) self.metrics.extend(metrics) return True - def shutdown(self): + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: self._shutdown = True From 07a5b64a3a9dd1025e65b4bdcfc0a6377e34accc Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 6 May 2022 14:00:39 -0600 Subject: [PATCH 1214/1517] Inform the user of conflicting view configuration (#2608) * Inform the user if there is a view configuration conflict Fixes #2556 * Add test case * Fix lint * Add test case for another kind of collision * Fix test cases * Fix lint * Fix tests * Add equal comparison between views * Rename __eq__ to conflicts * Refactor deep code into a function * Refactor conflict handling methods * Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py Co-authored-by: Aaron Abbott * Update opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py Co-authored-by: Leighton Chen * Use continue Co-authored-by: Aaron Abbott Co-authored-by: Leighton Chen --- .../_internal/_view_instrument_match.py | 41 +- .../_internal/metric_reader_storage.py | 113 ++++- .../metrics/test_metric_reader_storage.py | 470 +++++++++++++++++- 3 files changed, 596 insertions(+), 28 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py index 752a863a93..fef40bdae4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py @@ -22,6 +22,7 @@ _Aggregation, _convert_aggregation_temporality, _PointVarT, + _SumAggregation, ) from opentelemetry.sdk._metrics._internal.sdk_configuration import ( SdkConfiguration, @@ -52,6 +53,39 @@ def __init__( self._attributes_previous_point: Dict[frozenset, _PointVarT] = {} self._lock = Lock() self._instrument_class_aggregation = instrument_class_aggregation + self._name = self._view._name or self._instrument.name + self._description = ( + self._view._description or self._instrument.description + ) + if not isinstance(self._view._aggregation, DefaultAggregation): + self._aggregation = self._view._aggregation._create_aggregation( + self._instrument + ) + else: + self._aggregation = self._instrument_class_aggregation[ + self._instrument.__class__ + ]._create_aggregation(self._instrument) + + def conflicts(self, other: "_ViewInstrumentMatch") -> bool: + # pylint: disable=protected-access + + result = ( + self._name == other._name + and self._instrument.unit == other._instrument.unit + # The aggregation class is being used here instead of data point + # type since they are functionally equivalent. + and self._aggregation.__class__ == other._aggregation.__class__ + ) + if isinstance(self._aggregation, _SumAggregation): + result = ( + result + and self._aggregation._instrument_is_monotonic + == other._aggregation._instrument_is_monotonic + and self._aggregation._instrument_temporality + == other._aggregation._instrument_temporality + ) + + return result # pylint: disable=protected-access def consume_measurement(self, measurement: Measurement) -> None: @@ -118,14 +152,11 @@ def collect( yield Metric( attributes=dict(attributes), - description=( - self._view._description - or self._instrument.description - ), + description=self._description, instrumentation_scope=( self._instrument.instrumentation_scope ), - name=self._view._name or self._instrument.name, + name=self._name, resource=self._sdk_config.resource, unit=self._instrument.unit, point=_convert_aggregation_temporality( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py index fd046991fb..78e25a376d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py @@ -12,21 +12,27 @@ # See the License for the specific language governing permissions and # limitations under the License. +from logging import getLogger from threading import RLock from typing import Dict, Iterable, List -from opentelemetry._metrics import Instrument +from opentelemetry._metrics import Asynchronous, Instrument from opentelemetry.sdk._metrics._internal._view_instrument_match import ( _ViewInstrumentMatch, ) -from opentelemetry.sdk._metrics._internal.aggregation import Aggregation from opentelemetry.sdk._metrics._internal.sdk_configuration import ( SdkConfiguration, ) from opentelemetry.sdk._metrics._internal.view import View +from opentelemetry.sdk._metrics.aggregation import ( + Aggregation, + ExplicitBucketHistogramAggregation, +) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric +_logger = getLogger(__name__) + _DEFAULT_VIEW = View(instrument_name="") @@ -40,7 +46,7 @@ def __init__( ) -> None: self._lock = RLock() self._sdk_config = sdk_config - self._view_instrument_match: Dict[ + self._instrument_view_instrument_matches: Dict[ Instrument, List[_ViewInstrumentMatch] ] = {} self._instrument_class_aggregation = instrument_class_aggregation @@ -51,29 +57,20 @@ def _get_or_init_view_instrument_match( # Optimistically get the relevant views for the given instrument. Once set for a given # instrument, the mapping will never change - if instrument in self._view_instrument_match: - return self._view_instrument_match[instrument] + if instrument in self._instrument_view_instrument_matches: + return self._instrument_view_instrument_matches[instrument] with self._lock: # double check if it was set before we held the lock - if instrument in self._view_instrument_match: - return self._view_instrument_match[instrument] + if instrument in self._instrument_view_instrument_matches: + return self._instrument_view_instrument_matches[instrument] # not present, hold the lock and add a new mapping view_instrument_matches = [] - for view in self._sdk_config.views: - # pylint: disable=protected-access - if view._match(instrument): - view_instrument_matches.append( - _ViewInstrumentMatch( - view=view, - instrument=instrument, - sdk_config=self._sdk_config, - instrument_class_aggregation=( - self._instrument_class_aggregation - ), - ) - ) + + self._handle_view_instrument_match( + instrument, view_instrument_matches + ) # if no view targeted the instrument, use the default if not view_instrument_matches: @@ -87,7 +84,10 @@ def _get_or_init_view_instrument_match( ), ) ) - self._view_instrument_match[instrument] = view_instrument_matches + self._instrument_view_instrument_matches[ + instrument + ] = view_instrument_matches + return view_instrument_matches def consume_measurement(self, measurement: Measurement) -> None: @@ -114,7 +114,7 @@ def collect( with self._lock: for ( view_instrument_matches - ) in self._view_instrument_match.values(): + ) in self._instrument_view_instrument_matches.values(): for view_instrument_match in view_instrument_matches: metrics.extend( view_instrument_match.collect( @@ -123,3 +123,72 @@ def collect( ) return metrics + + def _handle_view_instrument_match( + self, + instrument: Instrument, + view_instrument_matches: List["_ViewInstrumentMatch"], + ) -> None: + for view in self._sdk_config.views: + # pylint: disable=protected-access + if not view._match(instrument): + continue + + if not self._check_view_instrument_compatibility(view, instrument): + continue + + new_view_instrument_match = _ViewInstrumentMatch( + view=view, + instrument=instrument, + sdk_config=self._sdk_config, + instrument_class_aggregation=( + self._instrument_class_aggregation + ), + ) + + for ( + existing_view_instrument_matches + ) in self._instrument_view_instrument_matches.values(): + for ( + existing_view_instrument_match + ) in existing_view_instrument_matches: + if existing_view_instrument_match.conflicts( + new_view_instrument_match + ): + + _logger.warning( + "Views %s and %s will cause conflicting " + "metrics identities", + existing_view_instrument_match._view, + new_view_instrument_match._view, + ) + + view_instrument_matches.append(new_view_instrument_match) + + @staticmethod + def _check_view_instrument_compatibility( + view: View, instrument: Instrument + ) -> bool: + """ + Checks if a view and an instrument are compatible. + + Returns `true` if they are compatible and a `_ViewInstrumentMatch` + object should be created, `false` otherwise. + """ + + result = True + + # pylint: disable=protected-access + if isinstance(instrument, Asynchronous) and isinstance( + view._aggregation, ExplicitBucketHistogramAggregation + ): + _logger.warning( + "View %s and instrument %s will produce " + "semantic errors when matched, the view " + "has not been applied.", + view, + instrument, + ) + result = False + + return result diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index 919efece68..bd9d0d3a3c 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from logging import WARNING from unittest.mock import MagicMock, Mock, patch from opentelemetry.sdk._metrics._internal.metric_reader_storage import ( + _DEFAULT_VIEW, MetricReaderStorage, ) from opentelemetry.sdk._metrics._internal.sdk_configuration import ( @@ -23,8 +25,15 @@ from opentelemetry.sdk._metrics.aggregation import ( DefaultAggregation, DropAggregation, + ExplicitBucketHistogramAggregation, + SumAggregation, +) +from opentelemetry.sdk._metrics.instrument import ( + Counter, + Histogram, + ObservableCounter, + UpDownCounter, ) -from opentelemetry.sdk._metrics.instrument import Counter from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import AggregationTemporality from opentelemetry.sdk._metrics.view import View @@ -221,3 +230,462 @@ def test_drop_aggregation(self): self.assertEqual( [], metric_reader_storage.collect(AggregationTemporality.DELTA) ) + + def test_conflicting_view_configuration(self): + + observable_counter = ObservableCounter( + "observable_counter", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + metric_reader_storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=( + View( + instrument_name="observable_counter", + aggregation=ExplicitBucketHistogramAggregation(), + ), + ), + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), + ) + + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter) + ) + + self.assertIs( + metric_reader_storage._instrument_view_instrument_matches[ + observable_counter + ][0]._view, + _DEFAULT_VIEW, + ) + + def test_view_instrument_match_conflict_0(self): + # There is a conflict between views and instruments. + + observable_counter_0 = ObservableCounter( + "observable_counter_0", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + observable_counter_1 = ObservableCounter( + "observable_counter_1", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + metric_reader_storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=( + View(instrument_name="observable_counter_0", name="foo"), + View(instrument_name="observable_counter_1", name="foo"), + ), + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter_0) + ) + + with self.assertLogs(level=WARNING) as log: + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter_1) + ) + + self.assertIn( + "will cause conflicting metrics", + log.records[0].message, + ) + + def test_view_instrument_match_conflict_1(self): + # There is a conflict between views and instruments. + + observable_counter_foo = ObservableCounter( + "foo", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + observable_counter_bar = ObservableCounter( + "bar", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + observable_counter_baz = ObservableCounter( + "baz", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + metric_reader_storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=( + View(instrument_name="bar", name="foo"), + View(instrument_name="baz", name="foo"), + ), + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter_foo) + ) + + with self.assertLogs(level=WARNING) as log: + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter_bar) + ) + + self.assertIn( + "will cause conflicting metrics", + log.records[0].message, + ) + + with self.assertLogs(level=WARNING) as log: + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter_baz) + ) + + self.assertIn( + "will cause conflicting metrics", + log.records[0].message, + ) + + for ( + view_instrument_matches + ) in ( + metric_reader_storage._instrument_view_instrument_matches.values() + ): + for view_instrument_match in view_instrument_matches: + self.assertEqual(view_instrument_match._name, "foo") + + def test_view_instrument_match_conflict_2(self): + # There is no conflict because the metric streams names are different. + observable_counter_foo = ObservableCounter( + "foo", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + observable_counter_bar = ObservableCounter( + "bar", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + + metric_reader_storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=( + View(instrument_name="foo"), + View(instrument_name="bar"), + ), + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter_foo) + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter_bar) + ) + + def test_view_instrument_match_conflict_3(self): + # There is no conflict because the aggregation temporality of the + # instruments is different. + + counter_bar = Counter( + "bar", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + observable_counter_baz = ObservableCounter( + "baz", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + + metric_reader_storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=( + View(instrument_name="bar", name="foo"), + View(instrument_name="baz", name="foo"), + ), + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, counter_bar) + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter_baz) + ) + + def test_view_instrument_match_conflict_4(self): + # There is no conflict because the monotonicity of the instruments is + # different. + + counter_bar = Counter( + "bar", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + up_down_counter_baz = UpDownCounter( + "baz", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + + metric_reader_storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=( + View(instrument_name="bar", name="foo"), + View(instrument_name="baz", name="foo"), + ), + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, counter_bar) + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, up_down_counter_baz) + ) + + def test_view_instrument_match_conflict_5(self): + # There is no conflict because the instrument units are different. + + observable_counter_0 = ObservableCounter( + "observable_counter_0", + Mock(), + [Mock()], + unit="unit_0", + description="description", + ) + observable_counter_1 = ObservableCounter( + "observable_counter_1", + Mock(), + [Mock()], + unit="unit_1", + description="description", + ) + metric_reader_storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=( + View(instrument_name="observable_counter_0", name="foo"), + View(instrument_name="observable_counter_1", name="foo"), + ), + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter_0) + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter_1) + ) + + def test_view_instrument_match_conflict_6(self): + # There is no conflict because the instrument data points are + # different. + + observable_counter = ObservableCounter( + "observable_counter", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + histogram = Histogram( + "histogram", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + metric_reader_storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=( + View(instrument_name="observable_counter", name="foo"), + View(instrument_name="histogram", name="foo"), + ), + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter) + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, histogram) + ) + + def test_view_instrument_match_conflict_7(self): + # There is a conflict between views and instruments because the + # description being different does not avoid a conflict. + + observable_counter_0 = ObservableCounter( + "observable_counter_0", + Mock(), + [Mock()], + unit="unit", + description="description_0", + ) + observable_counter_1 = ObservableCounter( + "observable_counter_1", + Mock(), + [Mock()], + unit="unit", + description="description_1", + ) + metric_reader_storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=( + View(instrument_name="observable_counter_0", name="foo"), + View(instrument_name="observable_counter_1", name="foo"), + ), + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter_0) + ) + + with self.assertLogs(level=WARNING) as log: + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter_1) + ) + + self.assertIn( + "will cause conflicting metrics", + log.records[0].message, + ) + + def test_view_instrument_match_conflict_8(self): + # There is a conflict because the histogram-matching view changes the + # default aggregation of the histogram to Sum aggregation which is the + # same aggregation as the default aggregation of the up down counter + # and also the temporality and monotonicity of the up down counter and + # the histogram are the same. + + observable_counter = UpDownCounter( + "up_down_counter", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + histogram = Histogram( + "histogram", + Mock(), + [Mock()], + unit="unit", + description="description", + ) + metric_reader_storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=( + View(instrument_name="up_down_counter", name="foo"), + View( + instrument_name="histogram", + name="foo", + aggregation=SumAggregation(), + ), + ), + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), + ) + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, observable_counter) + ) + + with self.assertLogs(level=WARNING) as log: + metric_reader_storage.consume_measurement( + Measurement(1, histogram) + ) + + self.assertIn( + "will cause conflicting metrics", + log.records[0].message, + ) From db9d40bcb9fe83fdea9afcd763338d92acfc78cd Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 6 May 2022 20:19:39 -0400 Subject: [PATCH 1215/1517] Add callback options with timeout to observable callbacks (#2664) * Add callback options with timeout to observable callbacks * remove debugging code * changelog --- CHANGELOG.md | 2 ++ docs/examples/metrics/example.py | 16 +++++---- docs/getting_started/metrics_example.py | 15 ++++---- .../src/opentelemetry/_metrics/__init__.py | 3 ++ .../_metrics/_internal/__init__.py | 21 +++++++++--- .../_metrics/_internal/instrument.py | 27 +++++++++++---- .../sdk/_metrics/_internal/instrument.py | 34 ++++++++++++++----- .../_internal/measurement_consumer.py | 7 ++-- .../metrics/integration_test/test_cpu_time.py | 14 +++++--- .../tests/metrics/test_backward_compat.py | 18 ++++++++++ .../metrics/test_in_memory_metric_reader.py | 3 +- .../tests/metrics/test_instrument.py | 31 ++++++++++------- 12 files changed, 140 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1614019b28..a3970db53b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.1-0.30b1...HEAD) +- Add CallbackOptions to observable instrument callback params + ([#2664](https://github.com/open-telemetry/opentelemetry-python/pull/2664)) - Add timeouts to metric SDK ([#2653](https://github.com/open-telemetry/opentelemetry-python/pull/2653)) - Add variadic arguments to metric exporter/reader interfaces diff --git a/docs/examples/metrics/example.py b/docs/examples/metrics/example.py index 11c2b8d8e0..9843d65953 100644 --- a/docs/examples/metrics/example.py +++ b/docs/examples/metrics/example.py @@ -5,6 +5,7 @@ get_meter_provider, set_meter_provider, ) +from opentelemetry._metrics._internal.instrument import CallbackOptions from opentelemetry.exporter.otlp.proto.grpc._metric_exporter import ( OTLPMetricExporter, ) @@ -17,15 +18,17 @@ set_meter_provider(provider) -def observable_counter_func() -> Iterable[Observation]: +def observable_counter_func(options: CallbackOptions) -> Iterable[Observation]: yield Observation(1, {}) -def observable_up_down_counter_func() -> Iterable[Observation]: +def observable_up_down_counter_func( + options: CallbackOptions, +) -> Iterable[Observation]: yield Observation(-10, {}) -def observable_gauge_func() -> Iterable[Observation]: +def observable_gauge_func(options: CallbackOptions) -> Iterable[Observation]: yield Observation(9, {}) @@ -37,7 +40,8 @@ def observable_gauge_func() -> Iterable[Observation]: # Async Counter observable_counter = meter.create_observable_counter( - "observable_counter", observable_counter_func + "observable_counter", + [observable_counter_func], ) # UpDownCounter @@ -47,7 +51,7 @@ def observable_gauge_func() -> Iterable[Observation]: # Async UpDownCounter observable_updown_counter = meter.create_observable_up_down_counter( - "observable_updown_counter", observable_up_down_counter_func + "observable_updown_counter", [observable_up_down_counter_func] ) # Histogram @@ -55,4 +59,4 @@ def observable_gauge_func() -> Iterable[Observation]: histogram.record(99.9) # Async Gauge -gauge = meter.create_observable_gauge("gauge", observable_gauge_func) +gauge = meter.create_observable_gauge("gauge", [observable_gauge_func]) diff --git a/docs/getting_started/metrics_example.py b/docs/getting_started/metrics_example.py index a1e1b7ff07..32342e7212 100644 --- a/docs/getting_started/metrics_example.py +++ b/docs/getting_started/metrics_example.py @@ -18,6 +18,7 @@ from typing import Iterable from opentelemetry._metrics import ( + CallbackOptions, Observation, get_meter_provider, set_meter_provider, @@ -34,15 +35,17 @@ set_meter_provider(provider) -def observable_counter_func() -> Iterable[Observation]: +def observable_counter_func(options: CallbackOptions) -> Iterable[Observation]: yield Observation(1, {}) -def observable_up_down_counter_func() -> Iterable[Observation]: +def observable_up_down_counter_func( + options: CallbackOptions, +) -> Iterable[Observation]: yield Observation(-10, {}) -def observable_gauge_func() -> Iterable[Observation]: +def observable_gauge_func(options: CallbackOptions) -> Iterable[Observation]: yield Observation(9, {}) @@ -54,7 +57,7 @@ def observable_gauge_func() -> Iterable[Observation]: # Async Counter observable_counter = meter.create_observable_counter( - "observable_counter", observable_counter_func + "observable_counter", [observable_counter_func] ) # UpDownCounter @@ -64,7 +67,7 @@ def observable_gauge_func() -> Iterable[Observation]: # Async UpDownCounter observable_updown_counter = meter.create_observable_up_down_counter( - "observable_updown_counter", observable_up_down_counter_func + "observable_updown_counter", [observable_up_down_counter_func] ) # Histogram @@ -72,4 +75,4 @@ def observable_gauge_func() -> Iterable[Observation]: histogram.record(99.9) # Async Gauge -gauge = meter.create_observable_gauge("gauge", observable_gauge_func) +gauge = meter.create_observable_gauge("gauge", [observable_gauge_func]) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index eac37b2cfe..c205adde0b 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -49,6 +49,7 @@ ) from opentelemetry._metrics._internal.instrument import ( Asynchronous, + CallbackOptions, CallbackT, Counter, Histogram, @@ -71,6 +72,7 @@ Counter, Synchronous, Asynchronous, + CallbackOptions, get_meter_provider, get_meter, Histogram, @@ -95,6 +97,7 @@ obj.__module__ = __name__ __all__ = [ + "CallbackOptions", "MeterProvider", "NoOpMeterProvider", "Meter", diff --git a/opentelemetry-api/src/opentelemetry/_metrics/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/_internal/__init__.py index f7203859f0..b2c67af226 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/_internal/__init__.py @@ -280,12 +280,13 @@ def create_observable_counter( """Creates an `ObservableCounter` instrument An observable counter observes a monotonically increasing count by calling provided - callbacks which returns multiple :class:`~opentelemetry._metrics.Observation`. + callbacks which accept a :class:`~opentelemetry._metrics.CallbackOptions` and return + multiple :class:`~opentelemetry._metrics.Observation`. For example, an observable counter could be used to report system CPU time periodically. Here is a basic implementation:: - def cpu_time_callback() -> Iterable[Observation]: + def cpu_time_callback(options: CallbackOptions) -> Iterable[Observation]: observations = [] with open("/proc/stat") as procstat: procstat.readline() # skip the first line @@ -308,7 +309,7 @@ def cpu_time_callback() -> Iterable[Observation]: To reduce memory usage, you can use generator callbacks instead of building the full list:: - def cpu_time_callback() -> Iterable[Observation]: + def cpu_time_callback(options: CallbackOptions) -> Iterable[Observation]: with open("/proc/stat") as procstat: procstat.readline() # skip the first line for line in procstat: @@ -322,6 +323,8 @@ def cpu_time_callback() -> Iterable[Observation]: callbacks, which each should return iterables of :class:`~opentelemetry._metrics.Observation`:: def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observation]]: + # accept options sent in from OpenTelemetry + options = yield while True: observations = [] with open("/proc/stat") as procstat: @@ -334,7 +337,8 @@ def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observat if "nice" in states_to_include: observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) # ... other states - yield observations + # yield the observations and receive the options for next iteration + options = yield observations meter.create_observable_counter( "system.cpu.time", @@ -343,6 +347,15 @@ def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observat description="CPU time" ) + The :class:`~opentelemetry._metrics.CallbackOptions` contain a timeout which the + callback should respect. For example if the callback does asynchronous work, like + making HTTP requests, it should respect the timeout:: + + def scrape_http_callback(options: CallbackOptions) -> Iterable[Observation]: + r = requests.get('http://scrapethis.com', timeout=options.timeout_millis / 10**3) + for value in r.json(): + yield Observation(value) + Args: name: The name of the instrument to be created callbacks: A sequence of callbacks that return an iterable of diff --git a/opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py index 254c03a5e6..301ba1c392 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py @@ -16,6 +16,7 @@ from abc import ABC, abstractmethod +from dataclasses import dataclass from logging import getLogger from re import ASCII from re import compile as re_compile @@ -36,19 +37,31 @@ from opentelemetry._metrics._internal.observation import Observation from opentelemetry.util.types import Attributes -InstrumentT = TypeVar("InstrumentT", bound="Instrument") -CallbackT = Union[ - Callable[[], Iterable[Observation]], - Generator[Iterable[Observation], None, None], -] - - _logger = getLogger(__name__) _name_regex = re_compile(r"[a-zA-Z][-.\w]{0,62}", ASCII) _unit_regex = re_compile(r"\w{0,63}", ASCII) +@dataclass(frozen=True) +class CallbackOptions: + """Options for the callback + + Args: + timeout_millis: Timeout for the callback's execution. If the callback does asynchronous + work (e.g. HTTP requests), it should respect this timeout. + """ + + timeout_millis: float = 10_000 + + +InstrumentT = TypeVar("InstrumentT", bound="Instrument") +CallbackT = Union[ + Callable[[CallbackOptions], Iterable[Observation]], + Generator[Iterable[Observation], CallbackOptions, None], +] + + class Instrument(ABC): """Abstract class that serves as base for all instruments.""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py index 9e11e22059..37f010ced7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py @@ -15,7 +15,15 @@ # pylint: disable=too-many-ancestors from logging import getLogger -from typing import TYPE_CHECKING, Dict, Generator, Iterable, Optional, Union +from typing import ( + TYPE_CHECKING, + Dict, + Generator, + Iterable, + List, + Optional, + Union, +) from opentelemetry._metrics import CallbackT from opentelemetry._metrics import Counter as APICounter @@ -26,6 +34,7 @@ ObservableUpDownCounter as APIObservableUpDownCounter, ) from opentelemetry._metrics import UpDownCounter as APIUpDownCounter +from opentelemetry._metrics._internal.instrument import CallbackOptions from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk.util.instrumentation import InstrumentationScope @@ -93,7 +102,7 @@ def __init__( self._measurement_consumer = measurement_consumer super().__init__(name, callbacks, unit=unit, description=description) - self._callbacks = [] + self._callbacks: List[CallbackT] = [] if callbacks is not None: @@ -101,24 +110,33 @@ def __init__( if isinstance(callback, Generator): - def inner(callback=callback) -> Iterable[Measurement]: - return next(callback) + # advance generator to it's first yield + next(callback) + + def inner( + options: CallbackOptions, + callback=callback, + ) -> Iterable[Measurement]: + try: + return callback.send(options) + except StopIteration: + return [] self._callbacks.append(inner) else: self._callbacks.append(callback) - def callback(self) -> Iterable[Measurement]: + def callback( + self, callback_options: CallbackOptions + ) -> Iterable[Measurement]: for callback in self._callbacks: try: - for api_measurement in callback(): + for api_measurement in callback(callback_options): yield Measurement( api_measurement.value, instrument=self, attributes=api_measurement.attributes, ) - except StopIteration: - pass except Exception: # pylint: disable=broad-except _logger.exception( "Callback failed for instrument %s.", self.name diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py index ce9b1f0d5e..d50d20f25e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py @@ -16,6 +16,7 @@ from threading import Lock from typing import TYPE_CHECKING, Dict, Iterable, List, Mapping +from opentelemetry._metrics._internal.instrument import CallbackOptions from opentelemetry.sdk._metrics._internal.metric_reader_storage import ( MetricReaderStorage, ) @@ -27,7 +28,7 @@ from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric if TYPE_CHECKING: - from opentelemetry.sdk._metrics.instrument import _Asynchronous + from opentelemetry.sdk._metrics._internal.instrument import _Asynchronous class MeasurementConsumer(ABC): @@ -78,8 +79,10 @@ def collect( ) -> Iterable[Metric]: with self._lock: metric_reader_storage = self._reader_storages[metric_reader] + # for now, just use the defaults + callback_options = CallbackOptions() for async_instrument in self._async_instruments: - for measurement in async_instrument.callback(): + for measurement in async_instrument.callback(callback_options): metric_reader_storage.consume_measurement(measurement) return self._reader_storages[metric_reader].collect( instrument_type_temporality diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py index d1688e02d4..90fa4f7d29 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py @@ -17,7 +17,7 @@ from typing import Generator, Iterable, List from unittest import TestCase -from opentelemetry._metrics import Instrument, Observation +from opentelemetry._metrics import CallbackOptions, Instrument, Observation from opentelemetry.sdk._metrics import MeterProvider from opentelemetry.sdk._metrics.measurement import Measurement @@ -138,7 +138,9 @@ def create_measurements_expected( ] def test_cpu_time_callback(self): - def cpu_time_callback() -> Iterable[Observation]: + def cpu_time_callback( + options: CallbackOptions, + ) -> Iterable[Observation]: procstat = io.StringIO(self.procstat_str) procstat.readline() # skip the first line for line in procstat: @@ -180,7 +182,7 @@ def cpu_time_callback() -> Iterable[Observation]: unit="s", description="CPU time", ) - measurements = list(observable_counter.callback()) + measurements = list(observable_counter.callback(CallbackOptions())) self.assertEqual( measurements, self.create_measurements_expected(observable_counter) ) @@ -189,7 +191,9 @@ def test_cpu_time_generator(self): def cpu_time_generator() -> Generator[ Iterable[Observation], None, None ]: + options = yield while True: + self.assertIsInstance(options, CallbackOptions) measurements = [] procstat = io.StringIO(self.procstat_str) procstat.readline() # skip the first line @@ -250,7 +254,7 @@ def cpu_time_generator() -> Generator[ {"cpu": cpu, "state": "guest_nice"}, ) ) - yield measurements + options = yield measurements meter = MeterProvider().get_meter("name") observable_counter = meter.create_observable_counter( @@ -259,7 +263,7 @@ def cpu_time_generator() -> Generator[ unit="s", description="CPU time", ) - measurements = list(observable_counter.callback()) + measurements = list(observable_counter.callback(CallbackOptions())) self.assertEqual( measurements, self.create_measurements_expected(observable_counter) ) diff --git a/opentelemetry-sdk/tests/metrics/test_backward_compat.py b/opentelemetry-sdk/tests/metrics/test_backward_compat.py index 4b3b504d65..4d814230cc 100644 --- a/opentelemetry-sdk/tests/metrics/test_backward_compat.py +++ b/opentelemetry-sdk/tests/metrics/test_backward_compat.py @@ -28,7 +28,9 @@ from typing import Iterable, Sequence from unittest import TestCase +from opentelemetry._metrics import CallbackOptions, Observation from opentelemetry.sdk._metrics import MeterProvider +from opentelemetry.sdk._metrics._internal.export import InMemoryMetricReader from opentelemetry.sdk._metrics.export import ( MetricExporter, MetricExportResult, @@ -65,6 +67,10 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: self.collect() +def orig_callback(options: CallbackOptions) -> Iterable[Observation]: + yield Observation(2) + + class TestBackwardCompat(TestCase): def test_metric_exporter(self): exporter = OrigMetricExporter() @@ -87,3 +93,15 @@ def test_metric_reader(self): meter_provider.shutdown() except Exception: self.fail() + + def test_observable_callback(self): + reader = InMemoryMetricReader() + meter_provider = MeterProvider(metric_readers=[reader]) + # produce some data + meter_provider.get_meter("foo").create_counter("mycounter").add(12) + try: + metrics = reader.get_metrics() + except Exception: + self.fail() + + self.assertEqual(len(metrics), 1) diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py index 58b2aad3e8..8ed026e245 100644 --- a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py @@ -70,7 +70,8 @@ def test_integration(self): meter = MeterProvider(metric_readers=[reader]).get_meter("test_meter") counter1 = meter.create_counter("counter1") meter.create_observable_gauge( - "observable_gauge1", callbacks=[lambda: [Observation(value=12)]] + "observable_gauge1", + callbacks=[lambda options: [Observation(value=12)]], ) counter1.add(1, {"foo": "1"}) counter1.add(1, {"foo": "2"}) diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py index 8b30791832..0402a3d651 100644 --- a/opentelemetry-sdk/tests/metrics/test_instrument.py +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -16,6 +16,7 @@ from unittest.mock import Mock from opentelemetry._metrics import Observation +from opentelemetry._metrics._internal.instrument import CallbackOptions from opentelemetry.sdk._metrics.instrument import ( Counter, Histogram, @@ -62,7 +63,7 @@ def test_add_non_monotonic(self): TEST_ATTRIBUTES = {"foo": "bar"} -def callable_callback_0(): +def callable_callback_0(options: CallbackOptions): return [ Observation(1, attributes=TEST_ATTRIBUTES), Observation(2, attributes=TEST_ATTRIBUTES), @@ -70,7 +71,7 @@ def callable_callback_0(): ] -def callable_callback_1(): +def callable_callback_1(options: CallbackOptions): return [ Observation(4, attributes=TEST_ATTRIBUTES), Observation(5, attributes=TEST_ATTRIBUTES), @@ -79,19 +80,25 @@ def callable_callback_1(): def generator_callback_0(): - yield [ + options = yield + assert isinstance(options, CallbackOptions) + options = yield [ Observation(1, attributes=TEST_ATTRIBUTES), Observation(2, attributes=TEST_ATTRIBUTES), Observation(3, attributes=TEST_ATTRIBUTES), ] + assert isinstance(options, CallbackOptions) def generator_callback_1(): - yield [ + options = yield + assert isinstance(options, CallbackOptions) + options = yield [ Observation(4, attributes=TEST_ATTRIBUTES), Observation(5, attributes=TEST_ATTRIBUTES), Observation(6, attributes=TEST_ATTRIBUTES), ] + assert isinstance(options, CallbackOptions) class TestObservableGauge(TestCase): @@ -105,7 +112,7 @@ def test_callable_callback_0(self): ) self.assertEqual( - list(observable_gauge.callback()), + list(observable_gauge.callback(CallbackOptions())), [ Measurement( 1, instrument=observable_gauge, attributes=TEST_ATTRIBUTES @@ -125,7 +132,7 @@ def test_callable_multiple_callable_callback(self): ) self.assertEqual( - list(observable_gauge.callback()), + list(observable_gauge.callback(CallbackOptions())), [ Measurement( 1, instrument=observable_gauge, attributes=TEST_ATTRIBUTES @@ -154,7 +161,7 @@ def test_generator_callback_0(self): ) self.assertEqual( - list(observable_gauge.callback()), + list(observable_gauge.callback(CallbackOptions())), [ Measurement( 1, instrument=observable_gauge, attributes=TEST_ATTRIBUTES @@ -178,7 +185,7 @@ def test_generator_multiple_generator_callback(self): ) self.assertEqual( - list(observable_gauge.callback()), + list(observable_gauge.callback(CallbackOptions())), [ Measurement( 1, instrument=observable_gauge, attributes=TEST_ATTRIBUTES @@ -209,7 +216,7 @@ def test_callable_callback_0(self): ) self.assertEqual( - list(observable_counter.callback()), + list(observable_counter.callback(CallbackOptions())), [ Measurement( 1, @@ -235,7 +242,7 @@ def test_generator_callback_0(self): ) self.assertEqual( - list(observable_counter.callback()), + list(observable_counter.callback(CallbackOptions())), [ Measurement( 1, @@ -263,7 +270,7 @@ def test_callable_callback_0(self): ) self.assertEqual( - list(observable_up_down_counter.callback()), + list(observable_up_down_counter.callback(CallbackOptions())), [ Measurement( 1, @@ -289,7 +296,7 @@ def test_generator_callback_0(self): ) self.assertEqual( - list(observable_up_down_counter.callback()), + list(observable_up_down_counter.callback(CallbackOptions())), [ Measurement( 1, From 4497dd5d3b16c8092ecb106a686b53911e1d0995 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Sat, 7 May 2022 07:43:15 +0530 Subject: [PATCH 1216/1517] Update opentelemetry-proto to v0.17.0 (#2668) * Update opentelemetry-proto to v0.17.0 * Add CHANGELOG entry --- CHANGELOG.md | 2 + opentelemetry-proto/README.rst | 4 +- .../proto/metrics/v1/metrics_pb2.py | 92 +++++++++++++++---- .../proto/metrics/v1/metrics_pb2.pyi | 37 +++++++- scripts/proto_codegen.sh | 2 +- 5 files changed, 114 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3970db53b..979231d18c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.1-0.30b1...HEAD) +- Update opentelemetry-proto to v0.17.0 + ([#2668](https://github.com/open-telemetry/opentelemetry-python/pull/2668)) - Add CallbackOptions to observable instrument callback params ([#2664](https://github.com/open-telemetry/opentelemetry-python/pull/2664)) - Add timeouts to metric SDK diff --git a/opentelemetry-proto/README.rst b/opentelemetry-proto/README.rst index d012aa70d4..555fbd70dc 100644 --- a/opentelemetry-proto/README.rst +++ b/opentelemetry-proto/README.rst @@ -7,9 +7,9 @@ OpenTelemetry Python Proto :target: https://pypi.org/project/opentelemetry-proto/ This library contains the generated code for OpenTelemetry protobuf data model. The code in the current -package was generated using the v0.16.0 release_ of opentelemetry-proto. +package was generated using the v0.17.0 release_ of opentelemetry-proto. -.. _release: https://github.com/open-telemetry/opentelemetry-proto/releases/tag/v0.16.0 +.. _release: https://github.com/open-telemetry/opentelemetry-proto/releases/tag/v0.17.0 Installation ------------ diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py index 898f3d00ab..a64e29d673 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py @@ -22,7 +22,7 @@ syntax='proto3', serialized_options=b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z)go.opentelemetry.io/proto/otlp/metrics/v1', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\x94\x02\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x43\n\rscope_metrics\x18\x02 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.ScopeMetrics\x12k\n\x1finstrumentation_library_metrics\x18\xe8\x07 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetricsB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x9f\x01\n\x0cScopeMetrics\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc8\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\x92\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xb2\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\rB\x06\n\x04_sumJ\x04\x08\x01\x10\x02\"\x81\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*;\n\x0e\x44\x61taPointFlags\x12\r\n\tFLAG_NONE\x10\x00\x12\x1a\n\x16\x46LAG_NO_RECORDED_VALUE\x10\x01\x42^\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z)go.opentelemetry.io/proto/otlp/metrics/v1b\x06proto3' + serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\x94\x02\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x43\n\rscope_metrics\x18\x02 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.ScopeMetrics\x12k\n\x1finstrumentation_library_metrics\x18\xe8\x07 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetricsB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x9f\x01\n\x0cScopeMetrics\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc8\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\x92\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xe6\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\r\x12\x10\n\x03min\x18\x0b \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\x0c \x01(\x01H\x02\x88\x01\x01\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_maxJ\x04\x08\x01\x10\x02\"\xb5\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\x10\n\x03min\x18\x0c \x01(\x01H\x00\x88\x01\x01\x12\x10\n\x03max\x18\r \x01(\x01H\x01\x88\x01\x01\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\x42\x06\n\x04_minB\x06\n\x04_max\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*;\n\x0e\x44\x61taPointFlags\x12\r\n\tFLAG_NONE\x10\x00\x12\x1a\n\x16\x46LAG_NO_RECORDED_VALUE\x10\x01\x42^\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z)go.opentelemetry.io/proto/otlp/metrics/v1b\x06proto3' , dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) @@ -51,8 +51,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=3650, - serialized_end=3790, + serialized_start=3754, + serialized_end=3894, ) _sym_db.RegisterEnumDescriptor(_AGGREGATIONTEMPORALITY) @@ -77,8 +77,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=3792, - serialized_end=3851, + serialized_start=3896, + serialized_end=3955, ) _sym_db.RegisterEnumDescriptor(_DATAPOINTFLAGS) @@ -692,6 +692,20 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='min', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.min', index=9, + number=11, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='max', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.max', index=10, + number=12, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -708,9 +722,19 @@ index=0, containing_type=None, create_key=_descriptor._internal_create_key, fields=[]), + _descriptor.OneofDescriptor( + name='_min', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint._min', + index=1, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + _descriptor.OneofDescriptor( + name='_max', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint._max', + index=2, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], serialized_start=2301, - serialized_end=2607, + serialized_end=2659, ) @@ -748,8 +772,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3075, - serialized_end=3123, + serialized_start=3163, + serialized_end=3211, ) _EXPONENTIALHISTOGRAMDATAPOINT = _descriptor.Descriptor( @@ -837,6 +861,20 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='min', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.min', index=11, + number=12, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='max', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.max', index=12, + number=13, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -848,9 +886,19 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_min', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint._min', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + _descriptor.OneofDescriptor( + name='_max', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint._max', + index=1, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=2610, - serialized_end=3123, + serialized_start=2662, + serialized_end=3227, ) @@ -888,8 +936,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3395, - serialized_end=3445, + serialized_start=3499, + serialized_end=3549, ) _SUMMARYDATAPOINT = _descriptor.Descriptor( @@ -961,8 +1009,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3126, - serialized_end=3451, + serialized_start=3230, + serialized_end=3555, ) @@ -1033,8 +1081,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=3454, - serialized_end=3647, + serialized_start=3558, + serialized_end=3751, ) _METRICSDATA.fields_by_name['resource_metrics'].message_type = _RESOURCEMETRICS @@ -1086,11 +1134,23 @@ _HISTOGRAMDATAPOINT.oneofs_by_name['_sum'].fields.append( _HISTOGRAMDATAPOINT.fields_by_name['sum']) _HISTOGRAMDATAPOINT.fields_by_name['sum'].containing_oneof = _HISTOGRAMDATAPOINT.oneofs_by_name['_sum'] +_HISTOGRAMDATAPOINT.oneofs_by_name['_min'].fields.append( + _HISTOGRAMDATAPOINT.fields_by_name['min']) +_HISTOGRAMDATAPOINT.fields_by_name['min'].containing_oneof = _HISTOGRAMDATAPOINT.oneofs_by_name['_min'] +_HISTOGRAMDATAPOINT.oneofs_by_name['_max'].fields.append( + _HISTOGRAMDATAPOINT.fields_by_name['max']) +_HISTOGRAMDATAPOINT.fields_by_name['max'].containing_oneof = _HISTOGRAMDATAPOINT.oneofs_by_name['_max'] _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS.containing_type = _EXPONENTIALHISTOGRAMDATAPOINT _EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE _EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['positive'].message_type = _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS _EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['negative'].message_type = _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS _EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _EXEMPLAR +_EXPONENTIALHISTOGRAMDATAPOINT.oneofs_by_name['_min'].fields.append( + _EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['min']) +_EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['min'].containing_oneof = _EXPONENTIALHISTOGRAMDATAPOINT.oneofs_by_name['_min'] +_EXPONENTIALHISTOGRAMDATAPOINT.oneofs_by_name['_max'].fields.append( + _EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['max']) +_EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['max'].containing_oneof = _EXPONENTIALHISTOGRAMDATAPOINT.oneofs_by_name['_max'] _SUMMARYDATAPOINT_VALUEATQUANTILE.containing_type = _SUMMARYDATAPOINT _SUMMARYDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE _SUMMARYDATAPOINT.fields_by_name['quantile_values'].message_type = _SUMMARYDATAPOINT_VALUEATQUANTILE diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi index 9b49ee0429..ed1291a757 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi @@ -683,6 +683,8 @@ class HistogramDataPoint(google.protobuf.message.Message): EXPLICIT_BOUNDS_FIELD_NUMBER: builtins.int EXEMPLARS_FIELD_NUMBER: builtins.int FLAGS_FIELD_NUMBER: builtins.int + MIN_FIELD_NUMBER: builtins.int + MAX_FIELD_NUMBER: builtins.int @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that uniquely identify the timeseries from @@ -762,6 +764,12 @@ class HistogramDataPoint(google.protobuf.message.Message): for the available flags and their meaning. """ + min: builtins.float = ... + """min is the minimum value over (start_time, end_time].""" + + max: builtins.float = ... + """max is the maximum value over (start_time, end_time].""" + def __init__(self, *, attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., @@ -773,9 +781,16 @@ class HistogramDataPoint(google.protobuf.message.Message): explicit_bounds : typing.Optional[typing.Iterable[builtins.float]] = ..., exemplars : typing.Optional[typing.Iterable[global___Exemplar]] = ..., flags : builtins.int = ..., + min : builtins.float = ..., + max : builtins.float = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_sum",b"_sum","sum",b"sum"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_sum",b"_sum","attributes",b"attributes","bucket_counts",b"bucket_counts","count",b"count","exemplars",b"exemplars","explicit_bounds",b"explicit_bounds","flags",b"flags","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["_max",b"_max","_min",b"_min","_sum",b"_sum","max",b"max","min",b"min","sum",b"sum"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["_max",b"_max","_min",b"_min","_sum",b"_sum","attributes",b"attributes","bucket_counts",b"bucket_counts","count",b"count","exemplars",b"exemplars","explicit_bounds",b"explicit_bounds","flags",b"flags","max",b"max","min",b"min","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano"]) -> None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing_extensions.Literal["_max",b"_max"]) -> typing.Optional[typing_extensions.Literal["max"]]: ... + @typing.overload + def WhichOneof(self, oneof_group: typing_extensions.Literal["_min",b"_min"]) -> typing.Optional[typing_extensions.Literal["min"]]: ... + @typing.overload def WhichOneof(self, oneof_group: typing_extensions.Literal["_sum",b"_sum"]) -> typing.Optional[typing_extensions.Literal["sum"]]: ... global___HistogramDataPoint = HistogramDataPoint @@ -830,6 +845,8 @@ class ExponentialHistogramDataPoint(google.protobuf.message.Message): NEGATIVE_FIELD_NUMBER: builtins.int FLAGS_FIELD_NUMBER: builtins.int EXEMPLARS_FIELD_NUMBER: builtins.int + MIN_FIELD_NUMBER: builtins.int + MAX_FIELD_NUMBER: builtins.int @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that uniquely identify the timeseries from @@ -918,6 +935,12 @@ class ExponentialHistogramDataPoint(google.protobuf.message.Message): measurements that were used to form the data point """ pass + min: builtins.float = ... + """min is the minimum value over (start_time, end_time].""" + + max: builtins.float = ... + """max is the maximum value over (start_time, end_time].""" + def __init__(self, *, attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., @@ -931,9 +954,15 @@ class ExponentialHistogramDataPoint(google.protobuf.message.Message): negative : typing.Optional[global___ExponentialHistogramDataPoint.Buckets] = ..., flags : builtins.int = ..., exemplars : typing.Optional[typing.Iterable[global___Exemplar]] = ..., + min : builtins.float = ..., + max : builtins.float = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["negative",b"negative","positive",b"positive"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","count",b"count","exemplars",b"exemplars","flags",b"flags","negative",b"negative","positive",b"positive","scale",b"scale","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano","zero_count",b"zero_count"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["_max",b"_max","_min",b"_min","max",b"max","min",b"min","negative",b"negative","positive",b"positive"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["_max",b"_max","_min",b"_min","attributes",b"attributes","count",b"count","exemplars",b"exemplars","flags",b"flags","max",b"max","min",b"min","negative",b"negative","positive",b"positive","scale",b"scale","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano","zero_count",b"zero_count"]) -> None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing_extensions.Literal["_max",b"_max"]) -> typing.Optional[typing_extensions.Literal["max"]]: ... + @typing.overload + def WhichOneof(self, oneof_group: typing_extensions.Literal["_min",b"_min"]) -> typing.Optional[typing_extensions.Literal["min"]]: ... global___ExponentialHistogramDataPoint = ExponentialHistogramDataPoint class SummaryDataPoint(google.protobuf.message.Message): diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index 8b1b986ee5..7ba991ea4e 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -12,7 +12,7 @@ # PROTO_REPO_DIR - the path to an existing checkout of the opentelemetry-proto repo # Pinned commit/branch/tag for the current version used in opentelemetry-proto python package. -PROTO_REPO_BRANCH_OR_COMMIT="v0.16.0" +PROTO_REPO_BRANCH_OR_COMMIT="v0.17.0" set -e From 3658e217c8530d6639dccffe621f07e0e12f814c Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Sat, 7 May 2022 08:26:45 +0530 Subject: [PATCH 1217/1517] Update to semantic conventions v1.11.0 (#2669) --- CHANGELOG.md | 2 + .../semconv/resource/__init__.py | 8 ++- .../opentelemetry/semconv/trace/__init__.py | 70 +++++++++++++++++-- scripts/semconv/generate.sh | 4 +- 4 files changed, 77 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 979231d18c..fc3201cf8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.1-0.30b1...HEAD) +- Update to semantic conventions v1.11.0 + ([#2669](https://github.com/open-telemetry/opentelemetry-python/pull/2669)) - Update opentelemetry-proto to v0.17.0 ([#2668](https://github.com/open-telemetry/opentelemetry-python/pull/2668)) - Add CallbackOptions to observable instrument callback params diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py index 710c30a77b..dc24d22b5f 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py @@ -150,6 +150,12 @@ class ResourceAttributes: Note: It's recommended this value represents a human readable version of the device model rather than a machine readable alternative. """ + DEVICE_MANUFACTURER = "device.manufacturer" + """ + The name of the device manufacturer. + Note: The Android OS provides this field via [Build](https://developer.android.com/reference/android/os/Build#MANUFACTURER). iOS apps SHOULD hardcode the value `Apple`. + """ + FAAS_NAME = "faas.name" """ The name of the single function that this runtime instance executes. @@ -610,7 +616,7 @@ class OsTypeValues(Enum): """AIX (Advanced Interactive eXecutive).""" SOLARIS = "solaris" - """Oracle Solaris.""" + """SunOS, Oracle Solaris.""" Z_OS = "z_os" """IBM z/OS.""" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py index a762d88c2c..36bab3d1a1 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py @@ -24,6 +24,37 @@ class SpanAttributes: Note: This may be different from `faas.id` if an alias is involved. """ + CLOUDEVENTS_EVENT_ID = "cloudevents.event_id" + """ + The [event_id](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#id) uniquely identifies the event. + """ + + CLOUDEVENTS_EVENT_SOURCE = "cloudevents.event_source" + """ + The [source](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#source-1) identifies the context in which an event happened. + """ + + CLOUDEVENTS_EVENT_SPEC_VERSION = "cloudevents.event_spec_version" + """ + The [version of the CloudEvents specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#specversion) which the event uses. + """ + + CLOUDEVENTS_EVENT_TYPE = "cloudevents.event_type" + """ + The [event_type](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#type) contains a value describing the type of event related to the originating occurrence. + """ + + CLOUDEVENTS_EVENT_SUBJECT = "cloudevents.event_subject" + """ + The [subject](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#subject) of the event in the context of the event producer (identified by source). + """ + + OPENTRACING_REF_TYPE = "opentracing.ref_type" + """ + Parent-child Reference type. + Note: The causal relationship between a child Span and a parent Span. + """ + DB_SYSTEM = "db.system" """ An identifier for the database management system (DBMS) product being used. See below for a list of well-known identifiers. @@ -65,6 +96,7 @@ class SpanAttributes: NET_PEER_NAME = "net.peer.name" """ Remote hostname or similar, see note below. + Note: `net.peer.name` SHOULD NOT be set if capturing it would require an extra DNS lookup. """ NET_PEER_IP = "net.peer.ip" @@ -170,7 +202,7 @@ class SpanAttributes: whether it will escape the scope of a span. However, it is trivial to know that an exception will escape, if one checks for an active exception just before ending the span, -as done in the [example above](#exception-end-example). +as done in the [example above](#recording-an-exception). It follows that an exception may still escape the scope of the span even if the `exception.escaped` attribute was not set or set to false, @@ -284,6 +316,11 @@ class SpanAttributes: The size of the uncompressed response payload body after transport decoding. Not set if transport encoding not used. """ + HTTP_RETRY_COUNT = "http.retry_count" + """ + The ordinal number of request re-sending attempt. + """ + HTTP_SERVER_NAME = "http.server_name" """ The primary server name of the matched virtual host. This should be obtained via configuration. If no such configuration can be obtained, this attribute MUST NOT be set ( `net.host.name` should be used instead). @@ -762,6 +799,14 @@ class SpanAttributes: """ +class OpentracingRefTypeValues(Enum): + CHILD_OF = "child_of" + """The parent Span depends on the child Span in some capacity.""" + + FOLLOWS_FROM = "follows_from" + """The parent Span does not depend in any way on the result of the child Span.""" + + class DbSystemValues(Enum): OTHER_SQL = "other_sql" """Some other SQL database. Fallback only. See notes.""" @@ -993,13 +1038,16 @@ class FaasDocumentOperationValues(Enum): class HttpFlavorValues(Enum): HTTP_1_0 = "1.0" - """HTTP 1.0.""" + """HTTP/1.0.""" HTTP_1_1 = "1.1" - """HTTP 1.1.""" + """HTTP/1.1.""" HTTP_2_0 = "2.0" - """HTTP 2.""" + """HTTP/2.""" + + HTTP_3_0 = "3.0" + """HTTP/3.""" SPDY = "SPDY" """SPDY protocol.""" @@ -1115,6 +1163,20 @@ class FaasInvokedProviderValues(Enum): """Tencent Cloud.""" +class RpcSystemValues(Enum): + GRPC = "grpc" + """gRPC.""" + + JAVA_RMI = "java_rmi" + """Java RMI.""" + + DOTNET_WCF = "dotnet_wcf" + """.NET WCF.""" + + APACHE_DUBBO = "apache_dubbo" + """Apache Dubbo.""" + + class MessagingOperationValues(Enum): RECEIVE = "receive" """receive.""" diff --git a/scripts/semconv/generate.sh b/scripts/semconv/generate.sh index 48c46a78c6..866832fe6b 100755 --- a/scripts/semconv/generate.sh +++ b/scripts/semconv/generate.sh @@ -4,8 +4,8 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR="${SCRIPT_DIR}/../../" # freeze the spec version to make SemanticAttributes generation reproducible -SPEC_VERSION=v1.8.0 -OTEL_SEMCONV_GEN_IMG_VERSION=0.9.0 +SPEC_VERSION=v1.11.0 +OTEL_SEMCONV_GEN_IMG_VERSION=0.11.1 cd ${SCRIPT_DIR} From e2edc9dfccc411930131bceb4df3d2aeed7d9feb Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Sat, 7 May 2022 21:07:26 -0600 Subject: [PATCH 1218/1517] Check exceptions in force_flush too (#2671) --- .../sdk/_metrics/_internal/__init__.py | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py index eebcb0b13c..733f84e6c2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py @@ -373,12 +373,35 @@ def __init__( def force_flush(self, timeout_millis: float = 10_000) -> bool: deadline_ns = _time_ns() + timeout_millis * 10**6 + metric_reader_error = {} + for metric_reader in self._sdk_config.metric_readers: current_ts = _time_ns() - if current_ts >= deadline_ns: - raise Exception("Timed out while flushing metric readers") - metric_reader.collect( - timeout_millis=(deadline_ns - current_ts) / 10**6 + try: + if current_ts >= deadline_ns: + raise Exception("Timed out while flushing metric readers") + metric_reader.collect( + timeout_millis=(deadline_ns - current_ts) / 10**6 + ) + + # pylint: disable=broad-except + except Exception as error: + + metric_reader_error[metric_reader] = error + + if metric_reader_error: + + metric_reader_error_string = "\n".join( + [ + f"{metric_reader.__class__.__name__}: {repr(error)}" + for metric_reader, error in metric_reader_error.items() + ] + ) + + raise Exception( + "MeterProvider.force_flush failed because the following " + "metric readers failed during collect:\n" + f"{metric_reader_error_string}" ) return True From 606d535551c14fbc5a68921d485be35ddb5a6ce2 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 9 May 2022 09:57:29 -0600 Subject: [PATCH 1219/1517] Refactor metrics SDK paths (#2670) * Refactor metrics SDK paths Fixes #2666 * Add fix for 3.6 * Fix OTLP metrics exporter * Address some feedback * Address more feedback * Add comments explaining imports * Add view module Co-authored-by: Srikanth Chekuri --- docs/conf.py | 8 + docs/sdk/metrics.aggregation.rst | 7 - docs/sdk/metrics.metric_reader.rst | 7 - docs/sdk/metrics.point.rst | 7 - docs/sdk/metrics.rst | 5 +- docs/sdk/metrics.view.rst | 2 +- .../proto/grpc/_metric_exporter/__init__.py | 2 +- .../metrics/test_otlp_metrics_exporter.py | 7 +- .../exporter/prometheus/__init__.py | 9 +- .../tests/test_prometheus_exporter.py | 2 +- .../opentelemetry/sdk/_metrics/__init__.py | 8 + .../sdk/_metrics/_internal/__init__.py | 35 +-- .../_internal/_view_instrument_match.py | 19 +- .../sdk/_metrics/_internal/aggregation.py | 43 ++-- .../sdk/_metrics/_internal/export/__init__.py | 208 ++++++++++++++++-- .../sdk/_metrics/_internal/instrument.py | 26 +-- .../sdk/_metrics/_internal/measurement.py | 8 +- .../_internal/measurement_consumer.py | 49 +++-- .../sdk/_metrics/_internal/metric_reader.py | 185 ---------------- .../_internal/metric_reader_storage.py | 15 +- .../sdk/_metrics/_internal/point.py | 29 +-- .../_metrics/_internal/sdk_configuration.py | 32 ++- .../sdk/_metrics/_internal/view.py | 6 +- .../sdk/_metrics/export/__init__.py | 15 ++ .../opentelemetry/sdk/_metrics/instrument.py | 30 --- .../opentelemetry/sdk/_metrics/measurement.py | 25 --- .../sdk/_metrics/metric_reader.py | 25 --- .../src/opentelemetry/sdk/_metrics/point.py | 35 --- .../src/opentelemetry/sdk/_metrics/view.py | 23 -- .../{aggregation.py => view/__init__.py} | 2 +- .../metrics/integration_test/test_cpu_time.py | 2 +- .../test_disable_default_views.py | 3 +- .../tests/metrics/test_aggregation.py | 26 +-- .../tests/metrics/test_backward_compat.py | 4 +- .../tests/metrics/test_import.py | 79 +++++++ .../metrics/test_in_memory_metric_reader.py | 4 +- .../tests/metrics/test_instrument.py | 4 +- .../metrics/test_measurement_consumer.py | 2 +- .../tests/metrics/test_metric_reader.py | 19 +- .../metrics/test_metric_reader_storage.py | 20 +- .../tests/metrics/test_metrics.py | 22 +- .../test_periodic_exporting_metric_reader.py | 4 +- opentelemetry-sdk/tests/metrics/test_point.py | 2 +- .../metrics/test_view_instrument_match.py | 10 +- .../src/opentelemetry/test/metrictestutil.py | 2 +- 45 files changed, 530 insertions(+), 547 deletions(-) delete mode 100644 docs/sdk/metrics.aggregation.rst delete mode 100644 docs/sdk/metrics.metric_reader.rst delete mode 100644 docs/sdk/metrics.point.rst delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py rename opentelemetry-sdk/src/opentelemetry/sdk/_metrics/{aggregation.py => view/__init__.py} (93%) create mode 100644 opentelemetry-sdk/tests/metrics/test_import.py diff --git a/docs/conf.py b/docs/conf.py index 03fad0837f..3472ff507c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -97,6 +97,14 @@ # https://github.com/sphinx-doc/sphinx/pull/3744 nitpick_ignore = [ ("py:class", "ValueT"), + ( + "py:class", + "opentelemetry.sdk._metrics._internal.instrument._Synchronous", + ), + ( + "py:class", + "opentelemetry.sdk._metrics._internal.instrument._Asynchronous", + ), # Even if wrapt is added to intersphinx_mapping, sphinx keeps failing # with "class reference target not found: ObjectProxy". ("py:class", "ObjectProxy"), diff --git a/docs/sdk/metrics.aggregation.rst b/docs/sdk/metrics.aggregation.rst deleted file mode 100644 index 4214444ed1..0000000000 --- a/docs/sdk/metrics.aggregation.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.sdk._metrics.aggregation -====================================== - -.. automodule:: opentelemetry.sdk._metrics.aggregation - :members: - :undoc-members: - :no-show-inheritance: diff --git a/docs/sdk/metrics.metric_reader.rst b/docs/sdk/metrics.metric_reader.rst deleted file mode 100644 index b2bc9423f1..0000000000 --- a/docs/sdk/metrics.metric_reader.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.sdk._metrics.metric_reader -========================================== - -.. automodule:: opentelemetry.sdk._metrics.metric_reader - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/sdk/metrics.point.rst b/docs/sdk/metrics.point.rst deleted file mode 100644 index de0ed7f659..0000000000 --- a/docs/sdk/metrics.point.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.sdk._metrics.point -================================ - -.. automodule:: opentelemetry.sdk._metrics.point - :members: - :undoc-members: - :no-show-inheritance: diff --git a/docs/sdk/metrics.rst b/docs/sdk/metrics.rst index d62ad456b3..1bef70185e 100644 --- a/docs/sdk/metrics.rst +++ b/docs/sdk/metrics.rst @@ -13,11 +13,8 @@ Submodules .. toctree:: - metrics.view - metrics.aggregation - metrics.metric_reader - metrics.point metrics.export + metrics.view .. automodule:: opentelemetry.sdk._metrics :members: diff --git a/docs/sdk/metrics.view.rst b/docs/sdk/metrics.view.rst index b2f78b9bf2..b18754177a 100644 --- a/docs/sdk/metrics.view.rst +++ b/docs/sdk/metrics.view.rst @@ -1,5 +1,5 @@ opentelemetry.sdk._metrics.view -========================================== +=============================== .. automodule:: opentelemetry.sdk._metrics.view :members: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py index 693eb261db..1742579e4b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py @@ -30,7 +30,7 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_INSECURE, ) -from opentelemetry.sdk._metrics.point import ( +from opentelemetry.sdk._metrics.export import ( Gauge, Histogram, Metric, diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index af5d39ff59..9edf193374 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -40,8 +40,11 @@ from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as OTLPResource, ) -from opentelemetry.sdk._metrics.export import MetricExportResult -from opentelemetry.sdk._metrics.point import AggregationTemporality, Histogram +from opentelemetry.sdk._metrics.export import ( + AggregationTemporality, + Histogram, + MetricExportResult, +) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_INSECURE, ) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 448a04bf29..3bf6e54c98 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -77,8 +77,13 @@ ) from prometheus_client.core import Metric as PrometheusMetric -from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.point import Gauge, Histogram, Metric, Sum +from opentelemetry.sdk._metrics.export import ( + Gauge, + Histogram, + Metric, + MetricReader, + Sum, +) _logger = getLogger(__name__) diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index fd963b139a..e5dcb79683 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -23,7 +23,7 @@ PrometheusMetricReader, _CustomCollector, ) -from opentelemetry.sdk._metrics.point import AggregationTemporality, Histogram +from opentelemetry.sdk._metrics.export import AggregationTemporality, Histogram from opentelemetry.test.metrictestutil import ( _generate_gauge, _generate_metric, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index c3b45fff8c..feecdd2177 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -18,6 +18,14 @@ Meter, MeterProvider, ) +from opentelemetry.sdk._metrics._internal.instrument import ( # noqa: F401 + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) __all__ = [] for key, value in globals().copy().items(): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py index 733f84e6c2..1d8c908a0a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py @@ -17,6 +17,8 @@ from threading import Lock from typing import Optional, Sequence +# This kind of import is needed to avoid Sphinx errors. +import opentelemetry.sdk._metrics from opentelemetry._metrics import Counter as APICounter from opentelemetry._metrics import Histogram as APIHistogram from opentelemetry._metrics import Meter as APIMeter @@ -28,14 +30,7 @@ ObservableUpDownCounter as APIObservableUpDownCounter, ) from opentelemetry._metrics import UpDownCounter as APIUpDownCounter -from opentelemetry.sdk._metrics._internal.measurement_consumer import ( - MeasurementConsumer, - SynchronousMeasurementConsumer, -) -from opentelemetry.sdk._metrics._internal.sdk_configuration import ( - SdkConfiguration, -) -from opentelemetry.sdk._metrics.instrument import ( +from opentelemetry.sdk._metrics._internal.instrument import ( Counter, Histogram, ObservableCounter, @@ -43,8 +38,13 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.view import View +from opentelemetry.sdk._metrics._internal.measurement_consumer import ( + MeasurementConsumer, + SynchronousMeasurementConsumer, +) +from opentelemetry.sdk._metrics._internal.sdk_configuration import ( + SdkConfiguration, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util._once import Once @@ -298,8 +298,9 @@ class MeterProvider(APIMeterProvider): r"""See `opentelemetry._metrics.MeterProvider`. Args: - metric_readers: Register metric readers to collect metrics from the SDK on demand. Each - `MetricReader` is completely independent and will collect separate streams of + metric_readers: Register metric readers to collect metrics from the SDK + on demand. Each :class:`opentelemetry.sdk._metrics.export.MetricReader` is + completely independent and will collect separate streams of metrics. TODO: reference ``PeriodicExportingMetricReader`` usage with push exporters here. resource: The resource representing what the metrics emitted from the SDK pertain to. @@ -307,10 +308,10 @@ class MeterProvider(APIMeterProvider): `MeterProvider.shutdown` views: The views to configure the metric output the SDK - By default, instruments which do not match any `View` (or if no `View`\ s + By default, instruments which do not match any :class:`opentelemetry.sdk._metrics.view.View` (or if no :class:`opentelemetry.sdk._metrics.view.View`\ s are provided) will report metrics with the default aggregation for the instrument's kind. To disable instruments by default, configure a match-all - `View` with `DropAggregation` and then create `View`\ s to re-enable + :class:`opentelemetry.sdk._metrics.view.View` with `DropAggregation` and then create :class:`opentelemetry.sdk._metrics.view.View`\ s to re-enable individual instruments: .. code-block:: python @@ -330,10 +331,12 @@ class MeterProvider(APIMeterProvider): def __init__( self, - metric_readers: Sequence[MetricReader] = (), + metric_readers: Sequence[ + "opentelemetry.sdk._metrics.export.MetricReader" + ] = (), resource: Resource = Resource.create({}), shutdown_on_exit: bool = True, - views: Sequence[View] = (), + views: Sequence["opentelemetry.sdk._metrics.view.View"] = (), ): self._lock = Lock() self._meter_lock = Lock() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py index fef40bdae4..d0f0dd42d1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py @@ -15,25 +15,24 @@ from logging import getLogger from threading import Lock -from typing import TYPE_CHECKING, Dict, Iterable +from typing import Dict, Iterable +from opentelemetry._metrics import Instrument from opentelemetry.sdk._metrics._internal.aggregation import ( Aggregation, + DefaultAggregation, _Aggregation, _convert_aggregation_temporality, _PointVarT, _SumAggregation, ) +from opentelemetry.sdk._metrics._internal.export import AggregationTemporality +from opentelemetry.sdk._metrics._internal.measurement import Measurement +from opentelemetry.sdk._metrics._internal.point import Metric from opentelemetry.sdk._metrics._internal.sdk_configuration import ( SdkConfiguration, ) -from opentelemetry.sdk._metrics.aggregation import DefaultAggregation -from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric - -if TYPE_CHECKING: - from opentelemetry.sdk._metrics._internal.instrument import _Instrument - from opentelemetry.sdk._metrics.view import View +from opentelemetry.sdk._metrics._internal.view import View _logger = getLogger(__name__) @@ -41,8 +40,8 @@ class _ViewInstrumentMatch: def __init__( self, - view: "View", - instrument: "_Instrument", + view: View, + instrument: Instrument, sdk_config: SdkConfiguration, instrument_class_aggregation: Dict[type, Aggregation], ): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py index 92aee89143..95993c663b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py @@ -15,6 +15,7 @@ from abc import ABC, abstractmethod from bisect import bisect_left from dataclasses import replace +from enum import IntEnum from logging import getLogger from math import inf from threading import Lock @@ -31,10 +32,12 @@ Synchronous, UpDownCounter, ) -from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import AggregationTemporality, Gauge -from opentelemetry.sdk._metrics.point import Histogram as HistogramPoint -from opentelemetry.sdk._metrics.point import PointT, Sum +from opentelemetry.sdk._metrics._internal.measurement import Measurement +from opentelemetry.sdk._metrics._internal.point import Gauge +from opentelemetry.sdk._metrics._internal.point import ( + Histogram as HistogramPoint, +) +from opentelemetry.sdk._metrics._internal.point import PointT, Sum from opentelemetry.util._time import _time_ns _PointVarT = TypeVar("_PointVarT", bound=PointT) @@ -42,6 +45,18 @@ _logger = getLogger(__name__) +class AggregationTemporality(IntEnum): + """ + The temporality to use when aggregating data. + + Can be one of the following values: + """ + + UNSPECIFIED = 0 + DELTA = 1 + CUMULATIVE = 2 + + class _Aggregation(ABC, Generic[_PointVarT]): def __init__(self): self._lock = Lock() @@ -80,16 +95,16 @@ class DefaultAggregation(Aggregation): This aggregation will create an actual aggregation depending on the instrument type, as specified next: - ============================================= ==================================== - Instrument Aggregation - ============================================= ==================================== - `Counter` `SumAggregation` - `UpDownCounter` `SumAggregation` - `ObservableCounter` `SumAggregation` - `ObservableUpDownCounter` `SumAggregation` - `opentelemetry._metrics.Histogram` `ExplicitBucketHistogramAggregation` - `ObservableGauge` `LastValueAggregation` - ============================================= ==================================== + ==================================================== ==================================== + Instrument Aggregation + ==================================================== ==================================== + `opentelemetry.sdk._metrics.Counter` `SumAggregation` + `opentelemetry.sdk._metrics.UpDownCounter` `SumAggregation` + `opentelemetry.sdk._metrics.ObservableCounter` `SumAggregation` + `opentelemetry.sdk._metrics.ObservableUpDownCounter` `SumAggregation` + `opentelemetry.sdk._metrics.Histogram` `ExplicitBucketHistogramAggregation` + `opentelemetry.sdk._metrics.ObservableGauge` `LastValueAggregation` + ==================================================== ==================================== """ def _create_aggregation(self, instrument: Instrument) -> _Aggregation: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py index c8dcb43327..7b17025466 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py @@ -12,28 +12,44 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging import os from abc import ABC, abstractmethod from enum import Enum +from logging import getLogger from os import environ, linesep from sys import stdout from threading import Event, RLock, Thread from typing import IO, Callable, Dict, Iterable, List, Optional, Sequence +from typing_extensions import final + +# This kind of import is needed to avoid Sphinx errors. +import opentelemetry.sdk._metrics._internal from opentelemetry.context import ( _SUPPRESS_INSTRUMENTATION_KEY, attach, detach, set_value, ) -from opentelemetry.sdk._metrics._internal.aggregation import Aggregation -from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric +from opentelemetry.sdk._metrics._internal.aggregation import ( + AggregationTemporality, + DefaultAggregation, +) +from opentelemetry.sdk._metrics._internal.instrument import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.sdk.environment_variables import ( + _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, +) from opentelemetry.util._once import Once from opentelemetry.util._time import _time_ns -_logger = logging.getLogger(__name__) +_logger = getLogger(__name__) class MetricExportResult(Enum): @@ -55,14 +71,14 @@ class MetricExporter(ABC): @abstractmethod def export( self, - metrics: Sequence[Metric], + metrics: Sequence["opentelemetry.sdk._metrics.export.Metric"], timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: """Exports a batch of telemetry data. Args: - metrics: The list of `opentelemetry.sdk._metrics.point.Metric` objects to be exported + metrics: The list of `opentelemetry.sdk._metrics.export.Metric` objects to be exported Returns: The result of the export @@ -87,7 +103,9 @@ class ConsoleMetricExporter(MetricExporter): def __init__( self, out: IO = stdout, - formatter: Callable[[Metric], str] = lambda metric: metric.to_json() + formatter: Callable[ + ["opentelemetry.sdk._metrics.export.Metric"], str + ] = lambda metric: metric.to_json() + linesep, ): self.out = out @@ -95,7 +113,7 @@ def __init__( def export( self, - metrics: Sequence[Metric], + metrics: Sequence["opentelemetry.sdk._metrics.export.Metric"], timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: @@ -108,6 +126,162 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass +class MetricReader(ABC): + """ + Base class for all metric readers + + Args: + preferred_temporality: A mapping between instrument classes and + aggregation temporality. By default uses CUMULATIVE for all instrument + classes. This mapping will be used to define the default aggregation + temporality of every instrument class. If the user wants to make a + change in the default aggregation temporality of an instrument class, + it is enough to pass here a dictionary whose keys are the instrument + classes and the values are the corresponding desired aggregation + temporalities of the classes that the user wants to change, not all of + them. The classes not included in the passed dictionary will retain + their association to their default aggregation temporalities. + The value passed here will override the corresponding values set + via the environment variable + preferred_aggregation: A mapping between instrument classes and + aggregation instances. By default maps all instrument classes to an + instance of `DefaultAggregation`. This mapping will be used to + define the default aggregation of every instrument class. If the + user wants to make a change in the default aggregation of an + instrument class, it is enough to pass here a dictionary whose keys + are the instrument classes and the values are the corresponding + desired aggregation for the instrument classes that the user wants + to change, not necessarily all of them. The classes not included in + the passed dictionary will retain their association to their + default aggregations. The aggregation defined here will be + overriden by an aggregation defined by a view that is not + `DefaultAggregation`. + + .. document protected _receive_metrics which is a intended to be overriden by subclass + .. automethod:: _receive_metrics + """ + + # FIXME add :std:envvar:`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` + # to the end of the documentation paragraph above. + + def __init__( + self, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[ + type, "opentelemetry.sdk._metrics.view.Aggregation" + ] = None, + ) -> None: + self._collect: Callable[ + [ + "opentelemetry.sdk._metrics.export.MetricReader", + AggregationTemporality, + ], + Iterable["opentelemetry.sdk._metrics.export.Metric"], + ] = None + + if ( + environ.get( + _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + "CUMULATIVE", + ) + .upper() + .strip() + == "DELTA" + ): + self._instrument_class_temporality = { + Counter: AggregationTemporality.DELTA, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.DELTA, + ObservableCounter: AggregationTemporality.DELTA, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + else: + self._instrument_class_temporality = { + Counter: AggregationTemporality.CUMULATIVE, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.CUMULATIVE, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + if preferred_temporality is not None: + for temporality in preferred_temporality.values(): + if temporality not in ( + AggregationTemporality.CUMULATIVE, + AggregationTemporality.DELTA, + ): + raise Exception( + f"Invalid temporality value found {temporality}" + ) + + self._instrument_class_temporality.update(preferred_temporality or {}) + self._preferred_temporality = preferred_temporality + self._instrument_class_aggregation = { + Counter: DefaultAggregation(), + UpDownCounter: DefaultAggregation(), + Histogram: DefaultAggregation(), + ObservableCounter: DefaultAggregation(), + ObservableUpDownCounter: DefaultAggregation(), + ObservableGauge: DefaultAggregation(), + } + + self._instrument_class_aggregation.update(preferred_aggregation or {}) + + @final + def collect(self, timeout_millis: float = 10_000) -> None: + """Collects the metrics from the internal SDK state and + invokes the `_receive_metrics` with the collection. + """ + if self._collect is None: + _logger.warning( + "Cannot call collect on a MetricReader until it is registered on a MeterProvider" + ) + return + self._receive_metrics( + self._collect(self, self._instrument_class_temporality), + timeout_millis=timeout_millis, + ) + + @final + def _set_collect_callback( + self, + func: Callable[ + [ + "opentelemetry.sdk._metrics.export.MetricReader", + AggregationTemporality, + ], + Iterable["opentelemetry.sdk._metrics.export.Metric"], + ], + ) -> None: + """This function is internal to the SDK. It should not be called or overriden by users""" + self._collect = func + + @abstractmethod + def _receive_metrics( + self, + metrics: Iterable["opentelemetry.sdk._metrics.export.Metric"], + timeout_millis: float = 10_000, + **kwargs, + ) -> None: + """Called by `MetricReader.collect` when it receives a batch of metrics""" + + @abstractmethod + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + """Shuts down the MetricReader. This method provides a way + for the MetricReader to do any cleanup required. A metric reader can + only be shutdown once, any subsequent calls are ignored and return + failure status. + + When a `MetricReader` is registered on a + :class:`~opentelemetry.sdk._metrics.MeterProvider`, + :meth:`~opentelemetry.sdk._metrics.MeterProvider.shutdown` will invoke this + automatically. + """ + + class InMemoryMetricReader(MetricReader): """Implementation of `MetricReader` that returns its metrics from :func:`get_metrics`. @@ -117,16 +291,18 @@ class InMemoryMetricReader(MetricReader): def __init__( self, preferred_temporality: Dict[type, AggregationTemporality] = None, - preferred_aggregation: Dict[type, Aggregation] = None, + preferred_aggregation: Dict[ + type, "opentelemetry.sdk._metrics.view.Aggregation" + ] = None, ) -> None: super().__init__( preferred_temporality=preferred_temporality, preferred_aggregation=preferred_aggregation, ) self._lock = RLock() - self._metrics: List[Metric] = [] + self._metrics: List["opentelemetry.sdk._metrics.export.Metric"] = [] - def get_metrics(self) -> List[Metric]: + def get_metrics(self) -> List["opentelemetry.sdk._metrics.export.Metric"]: """Reads and returns current metrics from the SDK""" with self._lock: self.collect() @@ -136,7 +312,7 @@ def get_metrics(self) -> List[Metric]: def _receive_metrics( self, - metrics: Iterable[Metric], + metrics: Iterable["opentelemetry.sdk._metrics.export.Metric"], timeout_millis: float = 10_000, **kwargs, ) -> None: @@ -157,7 +333,9 @@ def __init__( self, exporter: MetricExporter, preferred_temporality: Dict[type, AggregationTemporality] = None, - preferred_aggregation: Dict[type, Aggregation] = None, + preferred_aggregation: Dict[ + type, "opentelemetry.sdk._metrics.view.Aggregation" + ] = None, export_interval_millis: Optional[float] = None, export_timeout_millis: Optional[float] = None, ) -> None: @@ -211,7 +389,7 @@ def _ticker(self) -> None: def _receive_metrics( self, - metrics: Iterable[Metric], + metrics: Iterable["opentelemetry.sdk._metrics.export.Metric"], timeout_millis: float = 10_000, **kwargs, ) -> None: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py index 37f010ced7..be0feb7f86 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py @@ -12,19 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=too-many-ancestors +# pylint: disable=too-many-ancestors, unused-import from logging import getLogger -from typing import ( - TYPE_CHECKING, - Dict, - Generator, - Iterable, - List, - Optional, - Union, -) +from typing import Dict, Generator, Iterable, List, Optional, Union +# This kind of import is needed to avoid Sphinx errors. +import opentelemetry.sdk._metrics from opentelemetry._metrics import CallbackT from opentelemetry._metrics import Counter as APICounter from opentelemetry._metrics import Histogram as APIHistogram @@ -35,15 +29,9 @@ ) from opentelemetry._metrics import UpDownCounter as APIUpDownCounter from opentelemetry._metrics._internal.instrument import CallbackOptions -from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk._metrics._internal.measurement import Measurement from opentelemetry.sdk.util.instrumentation import InstrumentationScope -if TYPE_CHECKING: - from opentelemetry.sdk._metrics._internal.measurement_consumer import ( - MeasurementConsumer, - ) - - _logger = getLogger(__name__) @@ -57,7 +45,7 @@ def __init__( self, name: str, instrumentation_scope: InstrumentationScope, - measurement_consumer: "MeasurementConsumer", + measurement_consumer: "opentelemetry.sdk._metrics.MeasurementConsumer", unit: str = "", description: str = "", ): @@ -82,7 +70,7 @@ def __init__( self, name: str, instrumentation_scope: InstrumentationScope, - measurement_consumer: "MeasurementConsumer", + measurement_consumer: "opentelemetry.sdk._metrics.MeasurementConsumer", callbacks: Optional[Iterable[CallbackT]] = None, unit: str = "", description: str = "", diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement.py index 053c8f6fa6..8a3b807450 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement.py @@ -13,13 +13,11 @@ # limitations under the License. from dataclasses import dataclass -from typing import TYPE_CHECKING, Union +from typing import Union +from opentelemetry._metrics import Instrument from opentelemetry.util.types import Attributes -if TYPE_CHECKING: - from opentelemetry.sdk._metrics._internal.instrument import _Instrument - @dataclass(frozen=True) class Measurement: @@ -28,5 +26,5 @@ class Measurement: """ value: Union[int, float] - instrument: "_Instrument" + instrument: Instrument attributes: Attributes = None diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py index d50d20f25e..99475344b4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py @@ -12,23 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=unused-import + from abc import ABC, abstractmethod from threading import Lock -from typing import TYPE_CHECKING, Dict, Iterable, List, Mapping +from typing import Dict, Iterable, List, Mapping +# This kind of import is needed to avoid Sphinx errors. +import opentelemetry.sdk._metrics +import opentelemetry.sdk._metrics._internal.instrument +import opentelemetry.sdk._metrics._internal.sdk_configuration from opentelemetry._metrics._internal.instrument import CallbackOptions +from opentelemetry.sdk._metrics._internal.export import AggregationTemporality +from opentelemetry.sdk._metrics._internal.measurement import Measurement from opentelemetry.sdk._metrics._internal.metric_reader_storage import ( MetricReaderStorage, ) -from opentelemetry.sdk._metrics._internal.sdk_configuration import ( - SdkConfiguration, -) -from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric - -if TYPE_CHECKING: - from opentelemetry.sdk._metrics._internal.instrument import _Asynchronous +from opentelemetry.sdk._metrics._internal.point import Metric class MeasurementConsumer(ABC): @@ -37,44 +37,59 @@ def consume_measurement(self, measurement: Measurement) -> None: pass @abstractmethod - def register_asynchronous_instrument(self, instrument: "_Asynchronous"): + def register_asynchronous_instrument( + self, + instrument: ( + "opentelemetry.sdk._metrics._internal.instrument_Asynchronous" + ), + ): pass @abstractmethod def collect( self, - metric_reader: MetricReader, + metric_reader: "opentelemetry.sdk._metrics.MetricReader", instrument_type_temporality: Dict[type, AggregationTemporality], ) -> Iterable[Metric]: pass class SynchronousMeasurementConsumer(MeasurementConsumer): - def __init__(self, sdk_config: SdkConfiguration) -> None: + def __init__( + self, + sdk_config: "opentelemetry.sdk._metrics._internal.SdkConfiguration", + ) -> None: self._lock = Lock() self._sdk_config = sdk_config # should never be mutated - self._reader_storages: Mapping[MetricReader, MetricReaderStorage] = { + self._reader_storages: Mapping[ + "opentelemetry.sdk._metrics.MetricReader", MetricReaderStorage + ] = { reader: MetricReaderStorage( sdk_config, reader._instrument_class_aggregation ) for reader in sdk_config.metric_readers } - self._async_instruments: List["_Asynchronous"] = [] + self._async_instruments: List[ + "opentelemetry.sdk._metrics._internal.instrument._Asynchronous" + ] = [] def consume_measurement(self, measurement: Measurement) -> None: for reader_storage in self._reader_storages.values(): reader_storage.consume_measurement(measurement) def register_asynchronous_instrument( - self, instrument: "_Asynchronous" + self, + instrument: ( + "opentelemetry.sdk._metrics._internal.instrument._Asynchronous" + ), ) -> None: with self._lock: self._async_instruments.append(instrument) def collect( self, - metric_reader: MetricReader, + metric_reader: "opentelemetry.sdk._metrics.MetricReader", instrument_type_temporality: Dict[type, AggregationTemporality], ) -> Iterable[Metric]: with self._lock: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py deleted file mode 100644 index d949a34047..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader.py +++ /dev/null @@ -1,185 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod -from logging import getLogger -from os import environ -from typing import Callable, Dict, Iterable - -from typing_extensions import final - -from opentelemetry.sdk._metrics.aggregation import ( - Aggregation, - DefaultAggregation, -) -from opentelemetry.sdk._metrics.instrument import ( - Counter, - Histogram, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, -) -from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric -from opentelemetry.sdk.environment_variables import ( - _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, -) - -_logger = getLogger(__name__) - - -class MetricReader(ABC): - """ - Base class for all metric readers - - Args: - preferred_temporality: A mapping between instrument classes and - aggregation temporality. By default uses CUMULATIVE for all instrument - classes. This mapping will be used to define the default aggregation - temporality of every instrument class. If the user wants to make a - change in the default aggregation temporality of an instrument class, - it is enough to pass here a dictionary whose keys are the instrument - classes and the values are the corresponding desired aggregation - temporalities of the classes that the user wants to change, not all of - them. The classes not included in the passed dictionary will retain - their association to their default aggregation temporalities. - The value passed here will override the corresponding values set - via the environment variable - preferred_aggregation: A mapping between instrument classes and - aggregation instances. By default maps all instrument classes to an - instance of `DefaultAggregation`. This mapping will be used to - define the default aggregation of every instrument class. If the - user wants to make a change in the default aggregation of an - instrument class, it is enough to pass here a dictionary whose keys - are the instrument classes and the values are the corresponding - desired aggregation for the instrument classes that the user wants - to change, not necessarily all of them. The classes not included in - the passed dictionary will retain their association to their - default aggregations. The aggregation defined here will be - overriden by an aggregation defined by a view that is not - `DefaultAggregation`. - - .. document protected _receive_metrics which is a intended to be overriden by subclass - .. automethod:: _receive_metrics - """ - - # FIXME add :std:envvar:`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` - # to the end of the documentation paragraph above. - - def __init__( - self, - preferred_temporality: Dict[type, AggregationTemporality] = None, - preferred_aggregation: Dict[type, Aggregation] = None, - ) -> None: - self._collect: Callable[ - ["MetricReader", AggregationTemporality], Iterable[Metric] - ] = None - - if ( - environ.get( - _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, - "CUMULATIVE", - ) - .upper() - .strip() - == "DELTA" - ): - self._instrument_class_temporality = { - Counter: AggregationTemporality.DELTA, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.DELTA, - ObservableCounter: AggregationTemporality.DELTA, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - else: - self._instrument_class_temporality = { - Counter: AggregationTemporality.CUMULATIVE, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.CUMULATIVE, - ObservableCounter: AggregationTemporality.CUMULATIVE, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - if preferred_temporality is not None: - for temporality in preferred_temporality.values(): - if temporality not in ( - AggregationTemporality.CUMULATIVE, - AggregationTemporality.DELTA, - ): - raise Exception( - f"Invalid temporality value found {temporality}" - ) - - self._instrument_class_temporality.update(preferred_temporality or {}) - self._preferred_temporality = preferred_temporality - self._instrument_class_aggregation = { - Counter: DefaultAggregation(), - UpDownCounter: DefaultAggregation(), - Histogram: DefaultAggregation(), - ObservableCounter: DefaultAggregation(), - ObservableUpDownCounter: DefaultAggregation(), - ObservableGauge: DefaultAggregation(), - } - - self._instrument_class_aggregation.update(preferred_aggregation or {}) - - @final - def collect(self, timeout_millis: float = 10_000) -> None: - """Collects the metrics from the internal SDK state and - invokes the `_receive_metrics` with the collection. - """ - if self._collect is None: - _logger.warning( - "Cannot call collect on a MetricReader until it is registered on a MeterProvider" - ) - return - self._receive_metrics( - self._collect(self, self._instrument_class_temporality), - timeout_millis=timeout_millis, - ) - - @final - def _set_collect_callback( - self, - func: Callable[ - ["MetricReader", AggregationTemporality], Iterable[Metric] - ], - ) -> None: - """This function is internal to the SDK. It should not be called or overriden by users""" - self._collect = func - - @abstractmethod - def _receive_metrics( - self, - metrics: Iterable[Metric], - timeout_millis: float = 10_000, - **kwargs, - ) -> None: - """Called by `MetricReader.collect` when it receives a batch of metrics""" - - @abstractmethod - def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: - """Shuts down the MetricReader. This method provides a way - for the MetricReader to do any cleanup required. A metric reader can - only be shutdown once, any subsequent calls are ignored and return - failure status. - - When a `MetricReader` is registered on a - :class:`~opentelemetry.sdk._metrics.MeterProvider`, - :meth:`~opentelemetry.sdk._metrics.MeterProvider.shutdown` will invoke this - automatically. - """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py index 78e25a376d..4909b85bae 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py @@ -20,16 +20,17 @@ from opentelemetry.sdk._metrics._internal._view_instrument_match import ( _ViewInstrumentMatch, ) +from opentelemetry.sdk._metrics._internal.aggregation import ( + Aggregation, + ExplicitBucketHistogramAggregation, +) +from opentelemetry.sdk._metrics._internal.export import AggregationTemporality +from opentelemetry.sdk._metrics._internal.measurement import Measurement +from opentelemetry.sdk._metrics._internal.point import Metric from opentelemetry.sdk._metrics._internal.sdk_configuration import ( SdkConfiguration, ) from opentelemetry.sdk._metrics._internal.view import View -from opentelemetry.sdk._metrics.aggregation import ( - Aggregation, - ExplicitBucketHistogramAggregation, -) -from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric _logger = getLogger(__name__) @@ -53,7 +54,7 @@ def __init__( def _get_or_init_view_instrument_match( self, instrument: Instrument - ) -> List["_ViewInstrumentMatch"]: + ) -> List[_ViewInstrumentMatch]: # Optimistically get the relevant views for the given instrument. Once set for a given # instrument, the mapping will never change diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py index 6d55858a63..eca9783eee 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py @@ -12,34 +12,27 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json +# pylint: disable=unused-import + from dataclasses import asdict, dataclass -from enum import IntEnum +from json import dumps from typing import Sequence, Union +# This kind of import is needed to avoid Sphinx errors. +import opentelemetry.sdk._metrics._internal from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util.types import Attributes -class AggregationTemporality(IntEnum): - """ - The temporality to use when aggregating data. - - Can be one of the following values: - """ - - UNSPECIFIED = 0 - DELTA = 1 - CUMULATIVE = 2 - - @dataclass(frozen=True) class Sum: """Represents the type of a scalar metric that is calculated as a sum of all reported measurements over a time interval.""" - aggregation_temporality: AggregationTemporality + aggregation_temporality: ( + "opentelemetry.sdk._metrics.export.AggregationTemporality" + ) is_monotonic: bool start_time_unix_nano: int time_unix_nano: int @@ -61,7 +54,9 @@ class Histogram: """Represents the type of a metric that is calculated by aggregating as a histogram of all reported measurements over a time interval.""" - aggregation_temporality: AggregationTemporality + aggregation_temporality: ( + "opentelemetry.sdk._metrics.export.AggregationTemporality" + ) bucket_counts: Sequence[int] explicit_bounds: Sequence[float] max: int @@ -93,7 +88,7 @@ class Metric: """Contains non-common fields for the given metric""" def to_json(self) -> str: - return json.dumps( + return dumps( { "attributes": self.attributes if self.attributes else "", "description": self.description if self.description else "", diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/sdk_configuration.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/sdk_configuration.py index b48d7f22e7..94d7e110a0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/sdk_configuration.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/sdk_configuration.py @@ -1,15 +1,29 @@ -from dataclasses import dataclass -from typing import TYPE_CHECKING, Sequence +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=unused-import -from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk.resources import Resource +from dataclasses import dataclass +from typing import Sequence -if TYPE_CHECKING: - from opentelemetry.sdk._metrics.view import View +# This kind of import is needed to avoid Sphinx errors. +import opentelemetry.sdk._metrics +import opentelemetry.sdk.resources @dataclass class SdkConfiguration: - resource: Resource - metric_readers: Sequence[MetricReader] - views: Sequence["View"] + resource: "opentelemetry.sdk.resources.Resource" + metric_readers: Sequence["opentelemetry.sdk._metrics.MetricReader"] + views: Sequence["opentelemetry.sdk._metrics.View"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/view.py index 06f5355b17..4cdca65220 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/view.py @@ -21,8 +21,10 @@ from typing_extensions import final from opentelemetry._metrics import Instrument -from opentelemetry.sdk._metrics._internal.aggregation import Aggregation -from opentelemetry.sdk._metrics.aggregation import DefaultAggregation +from opentelemetry.sdk._metrics._internal.aggregation import ( + Aggregation, + DefaultAggregation, +) _logger = getLogger(__name__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py index 06dc1e7776..107f73f79d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -13,17 +13,32 @@ # limitations under the License. # pylint: disable=unused-import +# FIXME Remove when 3.6 is no longer supported +from sys import version_info as _version_info from opentelemetry.sdk._metrics._internal.export import ( # noqa: F401 + AggregationTemporality, ConsoleMetricExporter, InMemoryMetricReader, MetricExporter, MetricExportResult, + MetricReader, PeriodicExportingMetricReader, ) +# The point module is not in the export directory to avoid a circular import. +from opentelemetry.sdk._metrics._internal.point import ( # noqa: F401 + Gauge, + Histogram, + Metric, + PointT, + Sum, +) + __all__ = [] for key, value in globals().copy().items(): if not key.startswith("_"): + if _version_info.minor == 6 and key == "PointT": + continue value.__module__ = __name__ __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py deleted file mode 100644 index a154535c16..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=unused-import - -from opentelemetry.sdk._metrics._internal.instrument import ( # noqa: F401 - Counter, - Histogram, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, -) - -__all__ = [] -for key, value in globals().copy().items(): - if not key.startswith("_"): - value.__module__ = __name__ - __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py deleted file mode 100644 index 2756930d44..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=unused-import - -from opentelemetry.sdk._metrics._internal.measurement import ( # noqa: F401 - Measurement, -) - -__all__ = [] -for key, value in globals().copy().items(): - if not key.startswith("_"): - value.__module__ = __name__ - __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py deleted file mode 100644 index d184e9fbef..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=unused-import - -from opentelemetry.sdk._metrics._internal.metric_reader import ( # noqa: F401 - MetricReader, -) - -__all__ = [] -for key, value in globals().copy().items(): - if not key.startswith("_"): - value.__module__ = __name__ - __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py deleted file mode 100644 index 190b85533e..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/point.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=unused-import - -# FIXME Remove when 3.6 is no longer supported -from sys import version_info as _version_info - -from opentelemetry.sdk._metrics._internal.point import ( # noqa: F401 - AggregationTemporality, - Gauge, - Histogram, - Metric, - PointT, - Sum, -) - -__all__ = [] -for key, value in globals().copy().items(): - if not key.startswith("_"): - if _version_info.minor == 6 and key == "PointT": - continue - value.__module__ = __name__ - __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py deleted file mode 100644 index debd9c3946..0000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=unused-import - -from opentelemetry.sdk._metrics._internal.view import View # noqa: F401 - -__all__ = [] -for key, value in globals().copy().items(): - if not key.startswith("_"): - value.__module__ = __name__ - __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view/__init__.py similarity index 93% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py rename to opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view/__init__.py index 9566793db7..11218dbb49 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view/__init__.py @@ -14,7 +14,6 @@ # pylint: disable=unused-import - from opentelemetry.sdk._metrics._internal.aggregation import ( # noqa: F401 Aggregation, DefaultAggregation, @@ -23,6 +22,7 @@ LastValueAggregation, SumAggregation, ) +from opentelemetry.sdk._metrics._internal.view import View # noqa: F401 __all__ = [] for key, value in globals().copy().items(): diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py index 90fa4f7d29..b0c07c975c 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py @@ -19,7 +19,7 @@ from opentelemetry._metrics import CallbackOptions, Instrument, Observation from opentelemetry.sdk._metrics import MeterProvider -from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk._metrics._internal.measurement import Measurement # FIXME Test that the instrument methods can be called concurrently safely. diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py b/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py index cb2ab8b54c..5534e85090 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py @@ -15,9 +15,8 @@ from unittest import TestCase from opentelemetry.sdk._metrics import MeterProvider -from opentelemetry.sdk._metrics.aggregation import DropAggregation from opentelemetry.sdk._metrics.export import InMemoryMetricReader -from opentelemetry.sdk._metrics.view import View +from opentelemetry.sdk._metrics.view import DropAggregation, View class TestDisableDefaultViews(TestCase): diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 9ef7c469ff..6fb49fce73 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -20,30 +20,30 @@ from unittest import TestCase from unittest.mock import Mock +from opentelemetry.sdk._metrics import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) from opentelemetry.sdk._metrics._internal.aggregation import ( _convert_aggregation_temporality, _ExplicitBucketHistogramAggregation, _LastValueAggregation, _SumAggregation, ) -from opentelemetry.sdk._metrics.aggregation import ( +from opentelemetry.sdk._metrics._internal.measurement import Measurement +from opentelemetry.sdk._metrics.export import AggregationTemporality, Gauge +from opentelemetry.sdk._metrics.export import Histogram as HistogramPoint +from opentelemetry.sdk._metrics.export import Sum +from opentelemetry.sdk._metrics.view import ( DefaultAggregation, ExplicitBucketHistogramAggregation, LastValueAggregation, SumAggregation, ) -from opentelemetry.sdk._metrics.instrument import ( - Counter, - Histogram, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, -) -from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import AggregationTemporality, Gauge -from opentelemetry.sdk._metrics.point import Histogram as HistogramPoint -from opentelemetry.sdk._metrics.point import Sum from opentelemetry.util.types import Attributes diff --git a/opentelemetry-sdk/tests/metrics/test_backward_compat.py b/opentelemetry-sdk/tests/metrics/test_backward_compat.py index 4d814230cc..f9e543db51 100644 --- a/opentelemetry-sdk/tests/metrics/test_backward_compat.py +++ b/opentelemetry-sdk/tests/metrics/test_backward_compat.py @@ -32,12 +32,12 @@ from opentelemetry.sdk._metrics import MeterProvider from opentelemetry.sdk._metrics._internal.export import InMemoryMetricReader from opentelemetry.sdk._metrics.export import ( + Metric, MetricExporter, MetricExportResult, + MetricReader, PeriodicExportingMetricReader, ) -from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.point import Metric # Do not change these classes until after major version 1 diff --git a/opentelemetry-sdk/tests/metrics/test_import.py b/opentelemetry-sdk/tests/metrics/test_import.py new file mode 100644 index 0000000000..1a31ab3b88 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_import.py @@ -0,0 +1,79 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=unused-import + +from unittest import TestCase + + +class TestImport(TestCase): + def test_import_init(self): + """ + Test that the metrics root module has the right symbols + """ + + try: + from opentelemetry.sdk._metrics import ( # noqa: F401 + Counter, + Histogram, + Meter, + MeterProvider, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, + ) + except Exception as error: + self.fail(f"Unexpected error {error} was raised") + + def test_import_export(self): + """ + Test that the metrics export module has the right symbols + """ + + try: + from opentelemetry.sdk._metrics.export import ( # noqa: F401 + AggregationTemporality, + ConsoleMetricExporter, + Gauge, + Histogram, + InMemoryMetricReader, + Metric, + MetricExporter, + MetricExportResult, + MetricReader, + PeriodicExportingMetricReader, + PointT, + Sum, + ) + except Exception as error: + self.fail(f"Unexpected error {error} was raised") + + def test_import_view(self): + """ + Test that the metrics view module has the right symbols + """ + + try: + from opentelemetry.sdk._metrics.view import ( # noqa: F401 + Aggregation, + DefaultAggregation, + DropAggregation, + ExplicitBucketHistogramAggregation, + LastValueAggregation, + SumAggregation, + View, + ) + except Exception as error: + self.fail(f"Unexpected error {error} was raised") diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py index 8ed026e245..15be13ac9a 100644 --- a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py @@ -17,9 +17,9 @@ from opentelemetry._metrics import Observation from opentelemetry.sdk._metrics import MeterProvider -from opentelemetry.sdk._metrics.export import InMemoryMetricReader -from opentelemetry.sdk._metrics.point import ( +from opentelemetry.sdk._metrics.export import ( AggregationTemporality, + InMemoryMetricReader, Metric, Sum, ) diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py index 0402a3d651..909c44ebd4 100644 --- a/opentelemetry-sdk/tests/metrics/test_instrument.py +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -17,7 +17,7 @@ from opentelemetry._metrics import Observation from opentelemetry._metrics._internal.instrument import CallbackOptions -from opentelemetry.sdk._metrics.instrument import ( +from opentelemetry.sdk._metrics import ( Counter, Histogram, ObservableCounter, @@ -25,7 +25,7 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk._metrics.measurement import Measurement +from opentelemetry.sdk._metrics._internal.measurement import Measurement class TestCounter(TestCase): diff --git a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py index 4d086588d7..435c366234 100644 --- a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py +++ b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py @@ -22,7 +22,7 @@ from opentelemetry.sdk._metrics._internal.sdk_configuration import ( SdkConfiguration, ) -from opentelemetry.sdk._metrics.point import AggregationTemporality +from opentelemetry.sdk._metrics.export import AggregationTemporality @patch( diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_metric_reader.py index 1b95953cf8..714dc1cf96 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader.py @@ -17,12 +17,7 @@ from unittest import TestCase from unittest.mock import patch -from opentelemetry.sdk._metrics.aggregation import ( - Aggregation, - DefaultAggregation, - LastValueAggregation, -) -from opentelemetry.sdk._metrics.instrument import ( +from opentelemetry.sdk._metrics import ( Counter, Histogram, ObservableCounter, @@ -30,8 +25,16 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric +from opentelemetry.sdk._metrics.export import ( + AggregationTemporality, + Metric, + MetricReader, +) +from opentelemetry.sdk._metrics.view import ( + Aggregation, + DefaultAggregation, + LastValueAggregation, +) from opentelemetry.sdk.environment_variables import ( _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, ) diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index bd9d0d3a3c..d6443cb4ab 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -15,6 +15,13 @@ from logging import WARNING from unittest.mock import MagicMock, Mock, patch +from opentelemetry.sdk._metrics import ( + Counter, + Histogram, + ObservableCounter, + UpDownCounter, +) +from opentelemetry.sdk._metrics._internal.measurement import Measurement from opentelemetry.sdk._metrics._internal.metric_reader_storage import ( _DEFAULT_VIEW, MetricReaderStorage, @@ -22,21 +29,14 @@ from opentelemetry.sdk._metrics._internal.sdk_configuration import ( SdkConfiguration, ) -from opentelemetry.sdk._metrics.aggregation import ( +from opentelemetry.sdk._metrics.export import AggregationTemporality +from opentelemetry.sdk._metrics.view import ( DefaultAggregation, DropAggregation, ExplicitBucketHistogramAggregation, SumAggregation, + View, ) -from opentelemetry.sdk._metrics.instrument import ( - Counter, - Histogram, - ObservableCounter, - UpDownCounter, -) -from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import AggregationTemporality -from opentelemetry.sdk._metrics.view import View from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 3b57163801..b20dd567a2 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -20,24 +20,24 @@ from unittest.mock import MagicMock, Mock, patch from opentelemetry._metrics import NoOpMeter -from opentelemetry.sdk._metrics import Meter, MeterProvider -from opentelemetry.sdk._metrics.aggregation import SumAggregation -from opentelemetry.sdk._metrics.export import ( - MetricExporter, - MetricExportResult, - PeriodicExportingMetricReader, -) -from opentelemetry.sdk._metrics.instrument import ( +from opentelemetry.sdk._metrics import ( Counter, Histogram, + Meter, + MeterProvider, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk._metrics.metric_reader import MetricReader -from opentelemetry.sdk._metrics.point import Metric -from opentelemetry.sdk._metrics.view import View +from opentelemetry.sdk._metrics.export import ( + Metric, + MetricExporter, + MetricExportResult, + MetricReader, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk._metrics.view import SumAggregation, View from opentelemetry.sdk.resources import Resource from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index d2ad8cdccc..c206ce7245 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -19,11 +19,13 @@ from flaky import flaky from opentelemetry.sdk._metrics.export import ( + Gauge, + Metric, MetricExporter, MetricExportResult, PeriodicExportingMetricReader, + Sum, ) -from opentelemetry.sdk._metrics.point import Gauge, Metric, Sum from opentelemetry.sdk.resources import Resource from opentelemetry.test.concurrency_test import ConcurrencyTestBase from opentelemetry.util._time import _time_ns diff --git a/opentelemetry-sdk/tests/metrics/test_point.py b/opentelemetry-sdk/tests/metrics/test_point.py index 1748fb7ba4..70b2235f16 100644 --- a/opentelemetry-sdk/tests/metrics/test_point.py +++ b/opentelemetry-sdk/tests/metrics/test_point.py @@ -14,7 +14,7 @@ from unittest import TestCase -from opentelemetry.sdk._metrics.point import Gauge, Histogram, Metric, Sum +from opentelemetry.sdk._metrics.export import Gauge, Histogram, Metric, Sum from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 7e6f17644a..b96fb8916e 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -15,6 +15,7 @@ from unittest import TestCase from unittest.mock import MagicMock, Mock +from opentelemetry.sdk._metrics import Counter from opentelemetry.sdk._metrics._internal._view_instrument_match import ( _ViewInstrumentMatch, ) @@ -22,18 +23,17 @@ _DropAggregation, _LastValueAggregation, ) +from opentelemetry.sdk._metrics._internal.measurement import Measurement from opentelemetry.sdk._metrics._internal.sdk_configuration import ( SdkConfiguration, ) -from opentelemetry.sdk._metrics.aggregation import ( +from opentelemetry.sdk._metrics.export import AggregationTemporality, Metric +from opentelemetry.sdk._metrics.view import ( DefaultAggregation, DropAggregation, LastValueAggregation, + View, ) -from opentelemetry.sdk._metrics.instrument import Counter -from opentelemetry.sdk._metrics.measurement import Measurement -from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric -from opentelemetry.sdk._metrics.view import View class Test_ViewInstrumentMatch(TestCase): diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py index 97b411ac4b..b11e164624 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py @@ -16,7 +16,7 @@ from collections import OrderedDict from opentelemetry.attributes import BoundedAttributes -from opentelemetry.sdk._metrics.point import ( +from opentelemetry.sdk._metrics.export import ( AggregationTemporality, Gauge, Metric, From f367ec2045b2588be95dfa11913868c1d4fcbbc2 Mon Sep 17 00:00:00 2001 From: Michele Mancioppi Date: Mon, 9 May 2022 21:27:34 +0200 Subject: [PATCH 1220/1517] Implement 'process.runtime.*' resource detector (#2660) --- CHANGELOG.md | 4 ++++ .../opentelemetry/sdk/resources/__init__.py | 23 +++++++++++++++++++ .../tests/resources/test_resources.py | 19 +++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc3201cf8c..8bfc60619d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2653](https://github.com/open-telemetry/opentelemetry-python/pull/2653)) - Add variadic arguments to metric exporter/reader interfaces ([#2654](https://github.com/open-telemetry/opentelemetry-python/pull/2654)) +- Added a `opentelemetry.sdk.resources.ProcessResourceDetector` that adds the + 'process.runtime.{name,version,description}' resource attributes when used + with the `opentelemetry.sdk.resources.get_aggregated_resources` API + ([#2660](https://github.com/open-telemetry/opentelemetry-python/pull/2660)) - Move Metrics API behind internal package ([#2651](https://github.com/open-telemetry/opentelemetry-python/pull/2651)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index d32d943829..d99f097b38 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -59,6 +59,7 @@ import concurrent.futures import logging import os +import sys import typing from json import dumps @@ -286,6 +287,28 @@ def detect(self) -> "Resource": return Resource(env_resource_map) +class ProcessResourceDetector(ResourceDetector): + # pylint: disable=no-self-use + def detect(self) -> "Resource": + _runtime_version = ".".join( + map( + str, + sys.version_info[:3] + if sys.version_info.releaselevel == "final" + and not sys.version_info.serial + else sys.version_info, + ) + ) + + return Resource( + { + PROCESS_RUNTIME_DESCRIPTION: sys.version, + PROCESS_RUNTIME_NAME: sys.implementation.name, + PROCESS_RUNTIME_VERSION: _runtime_version, + } + ) + + def get_aggregated_resources( detectors: typing.List["ResourceDetector"], initial_resource: typing.Optional[Resource] = None, diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index ae3e34f9f0..2d584981e6 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -516,3 +516,22 @@ def test_service_name_env_precedence(self): detector.detect(), resources.Resource({"service.name": "from-service-name"}), ) + + def test_process_detector(self): + initial_resource = resources.Resource({"foo": "bar"}) + aggregated_resource = resources.get_aggregated_resources( + [resources.ProcessResourceDetector()], initial_resource + ) + + self.assertIn( + resources.PROCESS_RUNTIME_NAME, + aggregated_resource.attributes.keys(), + ) + self.assertIn( + resources.PROCESS_RUNTIME_DESCRIPTION, + aggregated_resource.attributes.keys(), + ) + self.assertIn( + resources.PROCESS_RUNTIME_VERSION, + aggregated_resource.attributes.keys(), + ) From 87b459ff87ee0a70010417f00250b01ba4f2ef6f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 10 May 2022 10:02:37 -0600 Subject: [PATCH 1221/1517] Remove instrument_class_temporality from collect args (#2674) Fixes #2673 --- .../_internal/_view_instrument_match.py | 9 +- .../sdk/_metrics/_internal/export/__init__.py | 2 +- .../_internal/measurement_consumer.py | 13 +-- .../_internal/metric_reader_storage.py | 18 ++-- .../metrics/test_measurement_consumer.py | 9 +- .../metrics/test_metric_reader_storage.py | 97 +++++++++++++++++-- .../test_periodic_exporting_metric_reader.py | 2 +- .../metrics/test_view_instrument_match.py | 75 ++++++++------ 8 files changed, 156 insertions(+), 69 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py index d0f0dd42d1..3fdc65352d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py @@ -43,6 +43,7 @@ def __init__( view: View, instrument: Instrument, sdk_config: SdkConfiguration, + instrument_class_temporality: Dict[type, AggregationTemporality], instrument_class_aggregation: Dict[type, Aggregation], ): self._view = view @@ -51,6 +52,7 @@ def __init__( self._attributes_aggregation: Dict[frozenset, _Aggregation] = {} self._attributes_previous_point: Dict[frozenset, _PointVarT] = {} self._lock = Lock() + self._instrument_class_temporality = instrument_class_temporality self._instrument_class_aggregation = instrument_class_aggregation self._name = self._view._name or self._instrument.name self._description = ( @@ -122,9 +124,7 @@ def consume_measurement(self, measurement: Measurement) -> None: self._attributes_aggregation[attributes].aggregate(measurement) - def collect( - self, instrument_class_temporality: Dict[type, AggregationTemporality] - ) -> Iterable[Metric]: + def collect(self) -> Iterable[Metric]: with self._lock: for ( @@ -139,6 +139,7 @@ def collect( current_point = aggregation.collect() # pylint: disable=assignment-from-none + self._attributes_previous_point[ attributes ] = _convert_aggregation_temporality( @@ -161,7 +162,7 @@ def collect( point=_convert_aggregation_temporality( previous_point, current_point, - instrument_class_temporality[ + self._instrument_class_temporality[ self._instrument.__class__ ], ), diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py index 7b17025466..d8a753ba7b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py @@ -241,7 +241,7 @@ def collect(self, timeout_millis: float = 10_000) -> None: ) return self._receive_metrics( - self._collect(self, self._instrument_class_temporality), + self._collect(self), timeout_millis=timeout_millis, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py index 99475344b4..e4d3b56bd8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py @@ -16,14 +16,13 @@ from abc import ABC, abstractmethod from threading import Lock -from typing import Dict, Iterable, List, Mapping +from typing import Iterable, List, Mapping # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk._metrics import opentelemetry.sdk._metrics._internal.instrument import opentelemetry.sdk._metrics._internal.sdk_configuration from opentelemetry._metrics._internal.instrument import CallbackOptions -from opentelemetry.sdk._metrics._internal.export import AggregationTemporality from opentelemetry.sdk._metrics._internal.measurement import Measurement from opentelemetry.sdk._metrics._internal.metric_reader_storage import ( MetricReaderStorage, @@ -49,7 +48,6 @@ def register_asynchronous_instrument( def collect( self, metric_reader: "opentelemetry.sdk._metrics.MetricReader", - instrument_type_temporality: Dict[type, AggregationTemporality], ) -> Iterable[Metric]: pass @@ -66,7 +64,9 @@ def __init__( "opentelemetry.sdk._metrics.MetricReader", MetricReaderStorage ] = { reader: MetricReaderStorage( - sdk_config, reader._instrument_class_aggregation + sdk_config, + reader._instrument_class_temporality, + reader._instrument_class_aggregation, ) for reader in sdk_config.metric_readers } @@ -90,7 +90,6 @@ def register_asynchronous_instrument( def collect( self, metric_reader: "opentelemetry.sdk._metrics.MetricReader", - instrument_type_temporality: Dict[type, AggregationTemporality], ) -> Iterable[Metric]: with self._lock: metric_reader_storage = self._reader_storages[metric_reader] @@ -99,6 +98,4 @@ def collect( for async_instrument in self._async_instruments: for measurement in async_instrument.callback(callback_options): metric_reader_storage.consume_measurement(measurement) - return self._reader_storages[metric_reader].collect( - instrument_type_temporality - ) + return self._reader_storages[metric_reader].collect() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py index 4909b85bae..b163f414af 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py @@ -43,6 +43,7 @@ class MetricReaderStorage: def __init__( self, sdk_config: SdkConfiguration, + instrument_class_temporality: Dict[type, AggregationTemporality], instrument_class_aggregation: Dict[type, Aggregation], ) -> None: self._lock = RLock() @@ -50,6 +51,7 @@ def __init__( self._instrument_view_instrument_matches: Dict[ Instrument, List[_ViewInstrumentMatch] ] = {} + self._instrument_class_temporality = instrument_class_temporality self._instrument_class_aggregation = instrument_class_aggregation def _get_or_init_view_instrument_match( @@ -80,6 +82,9 @@ def _get_or_init_view_instrument_match( view=_DEFAULT_VIEW, instrument=instrument, sdk_config=self._sdk_config, + instrument_class_temporality=( + self._instrument_class_temporality + ), instrument_class_aggregation=( self._instrument_class_aggregation ), @@ -97,9 +102,7 @@ def consume_measurement(self, measurement: Measurement) -> None: ): view_instrument_match.consume_measurement(measurement) - def collect( - self, instrument_type_temporality: Dict[type, AggregationTemporality] - ) -> Iterable[Metric]: + def collect(self) -> Iterable[Metric]: # Use a list instead of yielding to prevent a slow reader from holding # SDK locks metrics: List[Metric] = [] @@ -117,11 +120,7 @@ def collect( view_instrument_matches ) in self._instrument_view_instrument_matches.values(): for view_instrument_match in view_instrument_matches: - metrics.extend( - view_instrument_match.collect( - instrument_type_temporality - ) - ) + metrics.extend(view_instrument_match.collect()) return metrics @@ -142,6 +141,9 @@ def _handle_view_instrument_match( view=view, instrument=instrument, sdk_config=self._sdk_config, + instrument_class_temporality=( + self._instrument_class_temporality + ), instrument_class_aggregation=( self._instrument_class_aggregation ), diff --git a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py index 435c366234..f508780b52 100644 --- a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py +++ b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py @@ -22,7 +22,6 @@ from opentelemetry.sdk._metrics._internal.sdk_configuration import ( SdkConfiguration, ) -from opentelemetry.sdk._metrics.export import AggregationTemporality @patch( @@ -85,10 +84,8 @@ def test_collect_passed_to_reader_stage(self, MockMetricReaderStorage): ) for r_mock, rs_mock in zip(reader_mocks, reader_storage_mocks): rs_mock.collect.assert_not_called() - consumer.collect(r_mock, AggregationTemporality.CUMULATIVE) - rs_mock.collect.assert_called_once_with( - AggregationTemporality.CUMULATIVE - ) + consumer.collect(r_mock) + rs_mock.collect.assert_called_once_with() def test_collect_calls_async_instruments(self, MockMetricReaderStorage): """Its collect() method should invoke async instruments and pass measurements to the @@ -108,7 +105,7 @@ def test_collect_calls_async_instruments(self, MockMetricReaderStorage): i_mock.callback.return_value = [Mock()] consumer.register_asynchronous_instrument(i_mock) - consumer.collect(reader_mock, AggregationTemporality.CUMULATIVE) + consumer.collect(reader_mock) # it should call async instruments for i_mock in async_instrument_mocks: diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index d6443cb4ab..cd1a8481ee 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -60,7 +60,8 @@ class TestMetricReaderStorage(ConcurrencyTestBase): def test_creates_view_instrument_matches( self, MockViewInstrumentMatch: Mock ): - """It should create a MockViewInstrumentMatch when an instrument matches a view""" + """It should create a MockViewInstrumentMatch when an instrument + matches a view""" instrument1 = Mock(name="instrument1") instrument2 = Mock(name="instrument2") @@ -72,10 +73,16 @@ def test_creates_view_instrument_matches( metric_readers=(), views=(view1, view2), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) - # instrument1 matches view1 and view2, so should create two ViewInstrumentMatch objects + # instrument1 matches view1 and view2, so should create two + # ViewInstrumentMatch objects storage.consume_measurement(Measurement(1, instrument1)) self.assertEqual( len(MockViewInstrumentMatch.call_args_list), @@ -86,7 +93,8 @@ def test_creates_view_instrument_matches( storage.consume_measurement(Measurement(1, instrument1)) self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 2) - # instrument2 matches view2, so should create a single ViewInstrumentMatch + # instrument2 matches view2, so should create a single + # ViewInstrumentMatch MockViewInstrumentMatch.call_args_list.clear() storage.consume_measurement(Measurement(1, instrument2)) self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 1) @@ -118,11 +126,16 @@ def test_forwards_calls_to_view_instrument_match( metric_readers=(), views=(view1, view2), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) - # Measurements from an instrument should be passed on to each ViewInstrumentMatch objects - # created for that instrument + # Measurements from an instrument should be passed on to each + # ViewInstrumentMatch objects created for that instrument measurement = Measurement(1, instrument1) storage.consume_measurement(measurement) view_instrument_match1.consume_measurement.assert_called_once_with( @@ -139,13 +152,14 @@ def test_forwards_calls_to_view_instrument_match( measurement ) - # collect() should call collect on all of its _ViewInstrumentMatch objects and combine them together + # collect() should call collect on all of its _ViewInstrumentMatch + # objects and combine them together all_metrics = [Mock() for _ in range(6)] view_instrument_match1.collect.return_value = all_metrics[:2] view_instrument_match2.collect.return_value = all_metrics[2:4] view_instrument_match3.collect.return_value = all_metrics[4:] - result = storage.collect(AggregationTemporality.CUMULATIVE) + result = storage.collect() view_instrument_match1.collect.assert_called_once() view_instrument_match2.collect.assert_called_once() view_instrument_match3.collect.assert_called_once() @@ -167,6 +181,11 @@ def test_race_concurrent_measurements(self, MockViewInstrumentMatch: Mock): metric_readers=(), views=(view1,), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) @@ -194,6 +213,11 @@ def test_default_view_enabled(self, MockViewInstrumentMatch: Mock): metric_readers=(), views=(), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) @@ -223,13 +247,16 @@ def test_drop_aggregation(self): ), ), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) metric_reader_storage.consume_measurement(Measurement(1, counter)) - self.assertEqual( - [], metric_reader_storage.collect(AggregationTemporality.DELTA) - ) + self.assertEqual([], metric_reader_storage.collect()) def test_conflicting_view_configuration(self): @@ -251,6 +278,11 @@ def test_conflicting_view_configuration(self): ), ), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) @@ -292,6 +324,11 @@ def test_view_instrument_match_conflict_0(self): View(instrument_name="observable_counter_1", name="foo"), ), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) @@ -344,6 +381,11 @@ def test_view_instrument_match_conflict_1(self): View(instrument_name="baz", name="foo"), ), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) @@ -407,6 +449,11 @@ def test_view_instrument_match_conflict_2(self): View(instrument_name="bar"), ), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) @@ -450,6 +497,11 @@ def test_view_instrument_match_conflict_3(self): View(instrument_name="baz", name="foo"), ), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) @@ -493,6 +545,11 @@ def test_view_instrument_match_conflict_4(self): View(instrument_name="baz", name="foo"), ), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) @@ -534,6 +591,11 @@ def test_view_instrument_match_conflict_5(self): View(instrument_name="observable_counter_1", name="foo"), ), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) @@ -576,6 +638,11 @@ def test_view_instrument_match_conflict_6(self): View(instrument_name="histogram", name="foo"), ), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) @@ -618,6 +685,11 @@ def test_view_instrument_match_conflict_7(self): View(instrument_name="observable_counter_1", name="foo"), ), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) @@ -671,6 +743,11 @@ def test_view_instrument_match_conflict_8(self): ), ), ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index c206ce7245..531ef90c95 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -97,7 +97,7 @@ def _create_periodic_reader( exporter, export_interval_millis=interval ) - def _collect(reader, temp): + def _collect(reader): time.sleep(collect_wait) pmr._receive_metrics(metrics) diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index b96fb8916e..9f05d2bfee 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -46,15 +46,15 @@ def setUpClass(cls): ) cls.mock_resource = Mock() cls.mock_instrumentation_scope = Mock() + cls.sdk_configuration = SdkConfiguration( + resource=cls.mock_resource, + metric_readers=[], + views=[], + ) def test_consume_measurement(self): instrument1 = Mock(name="instrument1") instrument1.instrumentation_scope = self.mock_instrumentation_scope - sdk_config = SdkConfiguration( - resource=self.mock_resource, - metric_readers=[], - views=[], - ) view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", @@ -63,7 +63,12 @@ def test_consume_measurement(self): attribute_keys={"a", "c"}, ), instrument=instrument1, - sdk_config=sdk_config, + sdk_config=self.sdk_configuration, + instrument_class_temporality=MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), instrument_class_aggregation=MagicMock( **{"__getitem__.return_value": DefaultAggregation()} ), @@ -105,7 +110,12 @@ def test_consume_measurement(self): aggregation=self.mock_aggregation_factory, ), instrument=instrument1, - sdk_config=sdk_config, + sdk_config=self.sdk_configuration, + instrument_class_temporality=MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), instrument_class_aggregation=MagicMock( **{"__getitem__.return_value": DefaultAggregation()} ), @@ -127,7 +137,8 @@ def test_consume_measurement(self): }, ) - # empty set attribute_keys will drop all labels and aggregate everything together + # empty set attribute_keys will drop all labels and aggregate + # everything together view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", @@ -136,7 +147,12 @@ def test_consume_measurement(self): attribute_keys={}, ), instrument=instrument1, - sdk_config=sdk_config, + sdk_config=self.sdk_configuration, + instrument_class_temporality=MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), instrument_class_aggregation=MagicMock( **{"__getitem__.return_value": DefaultAggregation()} ), @@ -161,7 +177,12 @@ def test_consume_measurement(self): attribute_keys={}, ), instrument=instrument1, - sdk_config=sdk_config, + sdk_config=self.sdk_configuration, + instrument_class_temporality=MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), instrument_class_aggregation=MagicMock( **{"__getitem__.return_value": DefaultAggregation()} ), @@ -179,11 +200,6 @@ def test_collect(self): name="instrument1", description="description", unit="unit" ) instrument1.instrumentation_scope = self.mock_instrumentation_scope - sdk_config = SdkConfiguration( - resource=self.mock_resource, - metric_readers=[], - views=[], - ) view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", @@ -192,7 +208,12 @@ def test_collect(self): attribute_keys={"a", "c"}, ), instrument=instrument1, - sdk_config=sdk_config, + sdk_config=self.sdk_configuration, + instrument_class_temporality=MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), instrument_class_aggregation=MagicMock( **{"__getitem__.return_value": DefaultAggregation()} ), @@ -206,15 +227,7 @@ def test_collect(self): ) ) self.assertEqual( - next( - view_instrument_match.collect( - MagicMock( - **{ - "__getitem__.return_value": AggregationTemporality.CUMULATIVE - } - ) - ) - ), + next(view_instrument_match.collect()), Metric( attributes={"c": "d"}, description="description", @@ -235,11 +248,6 @@ def test_setting_aggregation(self): unit="unit", ) instrument1.instrumentation_scope = self.mock_instrumentation_scope - sdk_config = SdkConfiguration( - resource=self.mock_resource, - metric_readers=[], - views=[], - ) view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", @@ -248,7 +256,12 @@ def test_setting_aggregation(self): attribute_keys={"a", "c"}, ), instrument=instrument1, - sdk_config=sdk_config, + sdk_config=self.sdk_configuration, + instrument_class_temporality=MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), instrument_class_aggregation={Counter: LastValueAggregation()}, ) From a821311a954f027f4ae00f87611a73706c679f47 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 10 May 2022 10:15:05 -0600 Subject: [PATCH 1222/1517] Add attributes to aggregation constructor parameters (#2676) Fixes #2675 --- .../_internal/_view_instrument_match.py | 8 +-- .../sdk/_metrics/_internal/aggregation.py | 56 ++++++++++++------ .../tests/metrics/test_aggregation.py | 58 ++++++++++--------- 3 files changed, 75 insertions(+), 47 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py index 3fdc65352d..2f002a272e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py @@ -60,12 +60,12 @@ def __init__( ) if not isinstance(self._view._aggregation, DefaultAggregation): self._aggregation = self._view._aggregation._create_aggregation( - self._instrument + self._instrument, None ) else: self._aggregation = self._instrument_class_aggregation[ self._instrument.__class__ - ]._create_aggregation(self._instrument) + ]._create_aggregation(self._instrument, None) def conflicts(self, other: "_ViewInstrumentMatch") -> bool: # pylint: disable=protected-access @@ -113,13 +113,13 @@ def consume_measurement(self, measurement: Measurement) -> None: ): aggregation = ( self._view._aggregation._create_aggregation( - self._instrument + self._instrument, attributes ) ) else: aggregation = self._instrument_class_aggregation[ self._instrument.__class__ - ]._create_aggregation(self._instrument) + ]._create_aggregation(self._instrument, attributes) self._attributes_aggregation[attributes] = aggregation self._attributes_aggregation[attributes].aggregate(measurement) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py index 95993c663b..1d10b3f8ea 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py @@ -39,6 +39,7 @@ ) from opentelemetry.sdk._metrics._internal.point import PointT, Sum from opentelemetry.util._time import _time_ns +from opentelemetry.util.types import Attributes _PointVarT = TypeVar("_PointVarT", bound=PointT) @@ -58,8 +59,9 @@ class AggregationTemporality(IntEnum): class _Aggregation(ABC, Generic[_PointVarT]): - def __init__(self): + def __init__(self, attributes: Attributes): self._lock = Lock() + self._attributes = attributes @abstractmethod def aggregate(self, measurement: Measurement) -> None: @@ -84,7 +86,9 @@ class Aggregation(ABC): """ @abstractmethod - def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + def _create_aggregation( + self, instrument: Instrument, attributes: Attributes + ) -> _Aggregation: """Creates an aggregation""" @@ -107,37 +111,43 @@ class DefaultAggregation(Aggregation): ==================================================== ==================================== """ - def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + def _create_aggregation( + self, instrument: Instrument, attributes: Attributes + ) -> _Aggregation: # pylint: disable=too-many-return-statements if isinstance(instrument, Counter): return _SumAggregation( + attributes, instrument_is_monotonic=True, instrument_temporality=AggregationTemporality.DELTA, ) if isinstance(instrument, UpDownCounter): return _SumAggregation( + attributes, instrument_is_monotonic=False, instrument_temporality=AggregationTemporality.DELTA, ) if isinstance(instrument, ObservableCounter): return _SumAggregation( + attributes, instrument_is_monotonic=True, instrument_temporality=AggregationTemporality.CUMULATIVE, ) if isinstance(instrument, ObservableUpDownCounter): return _SumAggregation( + attributes, instrument_is_monotonic=False, instrument_temporality=AggregationTemporality.CUMULATIVE, ) if isinstance(instrument, Histogram): - return _ExplicitBucketHistogramAggregation() + return _ExplicitBucketHistogramAggregation(attributes) if isinstance(instrument, ObservableGauge): - return _LastValueAggregation() + return _LastValueAggregation(attributes) raise Exception(f"Invalid instrument type {type(instrument)} found") @@ -145,10 +155,11 @@ def _create_aggregation(self, instrument: Instrument) -> _Aggregation: class _SumAggregation(_Aggregation[Sum]): def __init__( self, + attributes: Attributes, instrument_is_monotonic: bool, instrument_temporality: AggregationTemporality, ): - super().__init__() + super().__init__(attributes) self._start_time_unix_nano = _time_ns() self._instrument_temporality = instrument_temporality @@ -205,8 +216,8 @@ def collect(self) -> Optional[Sum]: class _LastValueAggregation(_Aggregation[Gauge]): - def __init__(self): - super().__init__() + def __init__(self, attributes: Attributes): + super().__init__(attributes) self._value = None def aggregate(self, measurement: Measurement): @@ -232,6 +243,7 @@ def collect(self) -> Optional[Gauge]: class _ExplicitBucketHistogramAggregation(_Aggregation[HistogramPoint]): def __init__( self, + attributes: Attributes, boundaries: Sequence[float] = ( 0.0, 5.0, @@ -246,7 +258,7 @@ def __init__( ), record_min_max: bool = True, ): - super().__init__() + super().__init__(attributes) self._boundaries = tuple(boundaries) self._bucket_counts = self._get_empty_bucket_counts() self._min = inf @@ -464,10 +476,13 @@ def __init__( self._boundaries = boundaries self._record_min_max = record_min_max - def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + def _create_aggregation( + self, instrument: Instrument, attributes: Attributes + ) -> _Aggregation: return _ExplicitBucketHistogramAggregation( - boundaries=self._boundaries, - record_min_max=self._record_min_max, + attributes, + self._boundaries, + self._record_min_max, ) @@ -477,7 +492,9 @@ class SumAggregation(Aggregation): - The arithmetic sum of Measurement values. """ - def _create_aggregation(self, instrument: Instrument) -> _Aggregation: + def _create_aggregation( + self, instrument: Instrument, attributes: Attributes + ) -> _Aggregation: temporality = AggregationTemporality.UNSPECIFIED if isinstance(instrument, Synchronous): @@ -486,6 +503,7 @@ def _create_aggregation(self, instrument: Instrument) -> _Aggregation: temporality = AggregationTemporality.CUMULATIVE return _SumAggregation( + attributes, isinstance(instrument, (Counter, ObservableCounter)), temporality, ) @@ -499,12 +517,16 @@ class LastValueAggregation(Aggregation): - The timestamp of the last Measurement. """ - def _create_aggregation(self, instrument: Instrument) -> _Aggregation: - return _LastValueAggregation() + def _create_aggregation( + self, instrument: Instrument, attributes: Attributes + ) -> _Aggregation: + return _LastValueAggregation(attributes) class DropAggregation(Aggregation): """Using this aggregation will make all measurements be ignored.""" - def _create_aggregation(self, instrument: Instrument) -> _Aggregation: - return _DropAggregation() + def _create_aggregation( + self, instrument: Instrument, attributes: Attributes + ) -> _Aggregation: + return _DropAggregation(attributes) diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 6fb49fce73..e747e49cdb 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -60,7 +60,7 @@ def test_aggregate_delta(self): """ synchronous_sum_aggregation = _SumAggregation( - True, AggregationTemporality.DELTA + True, AggregationTemporality.DELTA, Mock() ) synchronous_sum_aggregation.aggregate(measurement(1)) @@ -70,7 +70,7 @@ def test_aggregate_delta(self): self.assertEqual(synchronous_sum_aggregation._value, 6) synchronous_sum_aggregation = _SumAggregation( - True, AggregationTemporality.DELTA + Mock(), True, AggregationTemporality.DELTA ) synchronous_sum_aggregation.aggregate(measurement(1)) @@ -85,7 +85,7 @@ def test_aggregate_cumulative(self): """ synchronous_sum_aggregation = _SumAggregation( - True, AggregationTemporality.CUMULATIVE + True, AggregationTemporality.CUMULATIVE, Mock() ) synchronous_sum_aggregation.aggregate(measurement(1)) @@ -95,7 +95,7 @@ def test_aggregate_cumulative(self): self.assertEqual(synchronous_sum_aggregation._value, 6) synchronous_sum_aggregation = _SumAggregation( - True, AggregationTemporality.CUMULATIVE + Mock(), True, AggregationTemporality.CUMULATIVE ) synchronous_sum_aggregation.aggregate(measurement(1)) @@ -110,7 +110,7 @@ def test_collect_delta(self): """ synchronous_sum_aggregation = _SumAggregation( - True, AggregationTemporality.DELTA + Mock(), True, AggregationTemporality.DELTA ) synchronous_sum_aggregation.aggregate(measurement(1)) @@ -135,7 +135,7 @@ def test_collect_cumulative(self): """ sum_aggregation = _SumAggregation( - True, AggregationTemporality.CUMULATIVE + True, AggregationTemporality.CUMULATIVE, Mock() ) sum_aggregation.aggregate(measurement(1)) @@ -167,7 +167,7 @@ def test_aggregate(self): temporality """ - last_value_aggregation = _LastValueAggregation() + last_value_aggregation = _LastValueAggregation(Mock()) last_value_aggregation.aggregate(measurement(1)) self.assertEqual(last_value_aggregation._value, 1) @@ -183,7 +183,7 @@ def test_collect(self): `LastValueAggregation` collects sum metric points """ - last_value_aggregation = _LastValueAggregation() + last_value_aggregation = _LastValueAggregation(Mock()) self.assertIsNone(last_value_aggregation.collect()) @@ -218,7 +218,7 @@ def test_aggregate(self): """ explicit_bucket_histogram_aggregation = ( - _ExplicitBucketHistogramAggregation(boundaries=[0, 2, 4]) + _ExplicitBucketHistogramAggregation(Mock(), boundaries=[0, 2, 4]) ) explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) @@ -259,7 +259,7 @@ def test_min_max(self): """ explicit_bucket_histogram_aggregation = ( - _ExplicitBucketHistogramAggregation() + _ExplicitBucketHistogramAggregation(Mock()) ) explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) @@ -272,7 +272,7 @@ def test_min_max(self): self.assertEqual(explicit_bucket_histogram_aggregation._max, 9999) explicit_bucket_histogram_aggregation = ( - _ExplicitBucketHistogramAggregation(record_min_max=False) + _ExplicitBucketHistogramAggregation(Mock(), record_min_max=False) ) explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) @@ -290,7 +290,7 @@ def test_collect(self): """ explicit_bucket_histogram_aggregation = ( - _ExplicitBucketHistogramAggregation(boundaries=[0, 1, 2]) + _ExplicitBucketHistogramAggregation(Mock(), boundaries=[0, 1, 2]) ) explicit_bucket_histogram_aggregation.aggregate(measurement(1)) @@ -814,18 +814,18 @@ class TestAggregationFactory(TestCase): def test_sum_factory(self): counter = Counter("name", Mock(), Mock()) factory = SumAggregation() - aggregation = factory._create_aggregation(counter) + aggregation = factory._create_aggregation(counter, Mock()) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) self.assertEqual( aggregation._instrument_temporality, AggregationTemporality.DELTA ) - aggregation2 = factory._create_aggregation(counter) + aggregation2 = factory._create_aggregation(counter, Mock()) self.assertNotEqual(aggregation, aggregation2) counter = UpDownCounter("name", Mock(), Mock()) factory = SumAggregation() - aggregation = factory._create_aggregation(counter) + aggregation = factory._create_aggregation(counter, Mock()) self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) self.assertEqual( @@ -834,7 +834,7 @@ def test_sum_factory(self): counter = ObservableCounter("name", Mock(), Mock(), None) factory = SumAggregation() - aggregation = factory._create_aggregation(counter) + aggregation = factory._create_aggregation(counter, Mock()) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) self.assertEqual( @@ -851,19 +851,19 @@ def test_explicit_bucket_histogram_factory(self): ), record_min_max=False, ) - aggregation = factory._create_aggregation(histo) + aggregation = factory._create_aggregation(histo, Mock()) self.assertIsInstance(aggregation, _ExplicitBucketHistogramAggregation) self.assertFalse(aggregation._record_min_max) self.assertEqual(aggregation._boundaries, (0.0, 5.0)) - aggregation2 = factory._create_aggregation(histo) + aggregation2 = factory._create_aggregation(histo, Mock()) self.assertNotEqual(aggregation, aggregation2) def test_last_value_factory(self): counter = Counter("name", Mock(), Mock()) factory = LastValueAggregation() - aggregation = factory._create_aggregation(counter) + aggregation = factory._create_aggregation(counter, Mock()) self.assertIsInstance(aggregation, _LastValueAggregation) - aggregation2 = factory._create_aggregation(counter) + aggregation2 = factory._create_aggregation(counter, Mock()) self.assertNotEqual(aggregation, aggregation2) @@ -875,7 +875,7 @@ def setUpClass(cls): def test_counter(self): aggregation = self.default_aggregation._create_aggregation( - Counter("name", Mock(), Mock()) + Counter("name", Mock(), Mock()), Mock() ) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) @@ -886,7 +886,7 @@ def test_counter(self): def test_up_down_counter(self): aggregation = self.default_aggregation._create_aggregation( - UpDownCounter("name", Mock(), Mock()) + UpDownCounter("name", Mock(), Mock()), Mock() ) self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) @@ -897,7 +897,8 @@ def test_up_down_counter(self): def test_observable_counter(self): aggregation = self.default_aggregation._create_aggregation( - ObservableCounter("name", Mock(), Mock(), callbacks=[Mock()]) + ObservableCounter("name", Mock(), Mock(), callbacks=[Mock()]), + Mock(), ) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) @@ -909,7 +910,10 @@ def test_observable_counter(self): def test_observable_up_down_counter(self): aggregation = self.default_aggregation._create_aggregation( - ObservableUpDownCounter("name", Mock(), Mock(), callbacks=[Mock()]) + ObservableUpDownCounter( + "name", Mock(), Mock(), callbacks=[Mock()] + ), + Mock(), ) self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) @@ -925,7 +929,8 @@ def test_histogram(self): "name", Mock(), Mock(), - ) + ), + Mock(), ) self.assertIsInstance(aggregation, _ExplicitBucketHistogramAggregation) @@ -937,6 +942,7 @@ def test_observable_gauge(self): Mock(), Mock(), callbacks=[Mock()], - ) + ), + Mock(), ) self.assertIsInstance(aggregation, _LastValueAggregation) From e8fbb08f6b7c7e983755f22185473014175aaa0e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 12 May 2022 16:09:13 -0600 Subject: [PATCH 1223/1517] Refactor metric format (#2658) * Refactor metric format Fixes #2646 * Do not overwrite pb2_scope_metrics * Refactor for loops * Add multiple scope test case * Fix interfaces * Fix docs * Update exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py Co-authored-by: Aaron Abbott * Fix lint * Remove resource check * Remove instrumentation_scope check * Group metrics by instrumentation scopes in the SDK * Remove label_keyss * Use strings instead of mocks * Return generator instead of a list * Fix lint * Rename variables Co-authored-by: Aaron Abbott --- .../proto/grpc/_metric_exporter/__init__.py | 189 +++--- .../metrics/test_otlp_metrics_exporter.py | 399 +++++++++++- .../exporter/prometheus/__init__.py | 234 ++++--- .../tests/test_prometheus_exporter.py | 132 +++- .../_internal/_view_instrument_match.py | 58 +- .../sdk/_metrics/_internal/aggregation.py | 256 ++++---- .../sdk/_metrics/_internal/export/__init__.py | 38 +- .../_internal/metric_reader_storage.py | 131 +++- .../sdk/_metrics/_internal/point.py | 127 +++- .../sdk/_metrics/export/__init__.py | 10 +- .../test_disable_default_views.py | 24 +- .../tests/metrics/test_aggregation.py | 603 +++--------------- .../tests/metrics/test_backward_compat.py | 10 +- .../tests/metrics/test_import.py | 8 +- .../metrics/test_in_memory_metric_reader.py | 59 +- .../metrics/test_metric_reader_storage.py | 74 ++- .../tests/metrics/test_metrics.py | 11 +- .../test_periodic_exporting_metric_reader.py | 33 +- opentelemetry-sdk/tests/metrics/test_point.py | 72 ++- .../metrics/test_view_instrument_match.py | 69 +- .../src/opentelemetry/test/metrictestutil.py | 54 +- 21 files changed, 1430 insertions(+), 1161 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py index 1742579e4b..902f683e7b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging +from logging import getLogger from os import environ from typing import Optional, Sequence from grpc import ChannelCredentials, Compression @@ -40,9 +40,10 @@ from opentelemetry.sdk._metrics.export import ( MetricExporter, MetricExportResult, + MetricsData, ) -logger = logging.getLogger(__name__) +_logger = getLogger(__name__) class OTLPMetricExporter( @@ -79,90 +80,114 @@ def __init__( ) def _translate_data( - self, data: Sequence[Metric] + self, data: MetricsData ) -> ExportMetricsServiceRequest: - sdk_resource_scope_metrics = {} - - for metric in data: - resource = metric.resource - scope_map = sdk_resource_scope_metrics.get(resource, {}) - if not scope_map: - sdk_resource_scope_metrics[resource] = scope_map - - scope_metrics = scope_map.get(metric.instrumentation_scope) - - if not scope_metrics: - if metric.instrumentation_scope is not None: - scope_map[metric.instrumentation_scope] = pb2.ScopeMetrics( - scope=InstrumentationScope( - name=metric.instrumentation_scope.name, - version=metric.instrumentation_scope.version, - ) - ) - else: - scope_map[ - metric.instrumentation_scope - ] = pb2.ScopeMetrics() - scope_metrics = scope_map.get(metric.instrumentation_scope) + resource_metrics_dict = {} - pbmetric = pb2.Metric( - name=metric.name, - description=metric.description, - unit=metric.unit, - ) - if isinstance(metric.point, Gauge): - pt = pb2.NumberDataPoint( - attributes=self._translate_attributes(metric.attributes), - time_unix_nano=metric.point.time_unix_nano, - ) - if isinstance(metric.point.value, int): - pt.as_int = metric.point.value - else: - pt.as_double = metric.point.value - pbmetric.gauge.data_points.append(pt) - elif isinstance(metric.point, Histogram): - pt = pb2.HistogramDataPoint( - attributes=self._translate_attributes(metric.attributes), - time_unix_nano=metric.point.time_unix_nano, - start_time_unix_nano=metric.point.start_time_unix_nano, - count=sum(metric.point.bucket_counts), - sum=metric.point.sum, - bucket_counts=metric.point.bucket_counts, - explicit_bounds=metric.point.explicit_bounds, - ) - pbmetric.histogram.aggregation_temporality = ( - metric.point.aggregation_temporality - ) - pbmetric.histogram.data_points.append(pt) - elif isinstance(metric.point, Sum): - pt = pb2.NumberDataPoint( - attributes=self._translate_attributes(metric.attributes), - start_time_unix_nano=metric.point.start_time_unix_nano, - time_unix_nano=metric.point.time_unix_nano, - ) - if isinstance(metric.point.value, int): - pt.as_int = metric.point.value - else: - pt.as_double = metric.point.value - # note that because sum is a message type, the fields must be - # set individually rather than instantiating a pb2.Sum and setting - # it once - pbmetric.sum.aggregation_temporality = ( - metric.point.aggregation_temporality + for resource_metrics in data.resource_metrics: + + resource = resource_metrics.resource + + # It is safe to assume that each entry in data.resource_metrics is + # associated with an unique resource. + scope_metrics_dict = {} + + resource_metrics_dict[resource] = scope_metrics_dict + + for scope_metrics in resource_metrics.scope_metrics: + + instrumentation_scope = scope_metrics.scope + + # The SDK groups metrics in instrumentation scopes already so + # there is no need to check for existing instrumentation scopes + # here. + pb2_scope_metrics = pb2.ScopeMetrics( + scope=InstrumentationScope( + name=instrumentation_scope.name, + version=instrumentation_scope.version, + ) ) - pbmetric.sum.is_monotonic = metric.point.is_monotonic - pbmetric.sum.data_points.append(pt) - else: - logger.warn("unsupported datapoint type %s", metric.point) - continue - - scope_metrics.metrics.append( - pbmetric, - ) + + scope_metrics_dict[instrumentation_scope] = pb2_scope_metrics + + for metric in scope_metrics.metrics: + pb2_metric = pb2.Metric( + name=metric.name, + description=metric.description, + unit=metric.unit, + ) + + if isinstance(metric.data, Gauge): + for data_point in metric.data.data_points: + pt = pb2.NumberDataPoint( + attributes=self._translate_attributes( + data_point.attributes + ), + time_unix_nano=data_point.time_unix_nano, + ) + if isinstance(data_point.value, int): + pt.as_int = data_point.value + else: + pt.as_double = data_point.value + pb2_metric.gauge.data_points.append(pt) + + elif isinstance(metric.data, Histogram): + for data_point in metric.data.data_points: + pt = pb2.HistogramDataPoint( + attributes=self._translate_attributes( + data_point.attributes + ), + time_unix_nano=data_point.time_unix_nano, + start_time_unix_nano=( + data_point.start_time_unix_nano + ), + count=data_point.count, + sum=data_point.sum, + bucket_counts=data_point.bucket_counts, + explicit_bounds=data_point.explicit_bounds, + ) + pb2_metric.histogram.aggregation_temporality = ( + metric.data.aggregation_temporality + ) + pb2_metric.histogram.data_points.append(pt) + + elif isinstance(metric.data, Sum): + for data_point in metric.data.data_points: + pt = pb2.NumberDataPoint( + attributes=self._translate_attributes( + data_point.attributes + ), + start_time_unix_nano=( + data_point.start_time_unix_nano + ), + time_unix_nano=data_point.time_unix_nano, + ) + if isinstance(data_point.value, int): + pt.as_int = data_point.value + else: + pt.as_double = data_point.value + # note that because sum is a message type, the + # fields must be set individually rather than + # instantiating a pb2.Sum and setting it once + pb2_metric.sum.aggregation_temporality = ( + metric.data.aggregation_temporality + ) + pb2_metric.sum.is_monotonic = ( + metric.data.is_monotonic + ) + pb2_metric.sum.data_points.append(pt) + else: + _logger.warn( + "unsupported datapoint type %s", metric.point + ) + continue + + pb2_scope_metrics.metrics.append(pb2_metric) + return ExportMetricsServiceRequest( resource_metrics=get_resource_data( - sdk_resource_scope_metrics, + resource_metrics_dict, pb2.ResourceMetrics, "metrics", ) @@ -170,12 +195,12 @@ def _translate_data( def export( self, - metrics: Sequence[Metric], + metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: # TODO(#2663): OTLPExporterMixin should pass timeout to gRPC - return self._export(metrics) + return self._export(metrics_data) def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index 9edf193374..009e1d2de5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -43,16 +43,21 @@ from opentelemetry.sdk._metrics.export import ( AggregationTemporality, Histogram, + HistogramDataPoint, + Metric, MetricExportResult, + MetricsData, + ResourceMetrics, + ScopeMetrics, ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_INSECURE, ) -from opentelemetry.test.metrictestutil import ( - _generate_gauge, - _generate_metric, - _generate_sum, +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationScope as SDKInstrumentationScope, ) +from opentelemetry.test.metrictestutil import _generate_gauge, _generate_sum class MetricsServiceServicerUNAVAILABLEDelay(MetricsServiceServicer): @@ -112,26 +117,184 @@ def setUp(self): self.server.start() + histogram = Metric( + name="histogram", + description="foo", + unit="s", + data=Histogram( + data_points=[ + HistogramDataPoint( + attributes={"a": 1, "b": True}, + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + min=8, + max=18, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + self.metrics = { - "sum_int": _generate_sum("sum_int", 33), - "sum_double": _generate_sum("sum_double", 2.98), - "gauge_int": _generate_gauge("gauge_int", 9000), - "gauge_double": _generate_gauge("gauge_double", 52.028), - "histogram": _generate_metric( - "histogram", - Histogram( - aggregation_temporality=AggregationTemporality.DELTA, - bucket_counts=[1, 4], - explicit_bounds=[10.0, 20.0], - max=18, - min=8, - start_time_unix_nano=1641946016139533244, - sum=67, - time_unix_nano=1641946016139533244, - ), + "sum_int": MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[_generate_sum("sum_int", 33)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ), + "sum_double": MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[_generate_sum("sum_double", 2.98)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ), + "gauge_int": MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[_generate_gauge("gauge_int", 9000)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ), + "gauge_double": MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[ + _generate_gauge("gauge_double", 52.028) + ], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ), + "histogram": MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[histogram], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] ), } + self.multiple_scope_histogram = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[histogram, histogram], + schema_url="instrumentation_scope_schema_url", + ), + ScopeMetrics( + scope=SDKInstrumentationScope( + name="second_name", + version="second_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[histogram], + schema_url="instrumentation_scope_schema_url", + ), + ScopeMetrics( + scope=SDKInstrumentationScope( + name="third_name", + version="third_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[histogram], + schema_url="instrumentation_scope_schema_url", + ), + ], + schema_url="resource_schema_url", + ) + ] + ) + def tearDown(self): self.server.stop(None) @@ -246,7 +409,7 @@ def test_unavailable(self, mock_sleep, mock_expo): MetricsServiceServicerUNAVAILABLE(), self.server ) self.assertEqual( - self.exporter.export([self.metrics["sum_int"]]), + self.exporter.export(self.metrics["sum_int"]), MetricExportResult.FAILURE, ) mock_sleep.assert_called_with(1) @@ -261,7 +424,7 @@ def test_unavailable_delay(self, mock_sleep, mock_expo): MetricsServiceServicerUNAVAILABLEDelay(), self.server ) self.assertEqual( - self.exporter.export([self.metrics["sum_int"]]), + self.exporter.export(self.metrics["sum_int"]), MetricExportResult.FAILURE, ) mock_sleep.assert_called_with(4) @@ -271,7 +434,7 @@ def test_success(self): MetricsServiceServicerSUCCESS(), self.server ) self.assertEqual( - self.exporter.export([self.metrics["sum_int"]]), + self.exporter.export(self.metrics["sum_int"]), MetricExportResult.SUCCESS, ) @@ -280,7 +443,7 @@ def test_failure(self): MetricsServiceServicerALREADY_EXISTS(), self.server ) self.assertEqual( - self.exporter.export([self.metrics["sum_int"]]), + self.exporter.export(self.metrics["sum_int"]), MetricExportResult.FAILURE, ) @@ -339,7 +502,7 @@ def test_translate_sum_int(self): ] ) # pylint: disable=protected-access - actual = self.exporter._translate_data([self.metrics["sum_int"]]) + actual = self.exporter._translate_data(self.metrics["sum_int"]) self.assertEqual(expected, actual) def test_translate_sum_double(self): @@ -397,7 +560,7 @@ def test_translate_sum_double(self): ] ) # pylint: disable=protected-access - actual = self.exporter._translate_data([self.metrics["sum_double"]]) + actual = self.exporter._translate_data(self.metrics["sum_double"]) self.assertEqual(expected, actual) def test_translate_gauge_int(self): @@ -452,7 +615,7 @@ def test_translate_gauge_int(self): ] ) # pylint: disable=protected-access - actual = self.exporter._translate_data([self.metrics["gauge_int"]]) + actual = self.exporter._translate_data(self.metrics["gauge_int"]) self.assertEqual(expected, actual) def test_translate_gauge_double(self): @@ -507,7 +670,7 @@ def test_translate_gauge_double(self): ] ) # pylint: disable=protected-access - actual = self.exporter._translate_data([self.metrics["gauge_double"]]) + actual = self.exporter._translate_data(self.metrics["gauge_double"]) self.assertEqual(expected, actual) def test_translate_histogram(self): @@ -569,5 +732,183 @@ def test_translate_histogram(self): ] ) # pylint: disable=protected-access - actual = self.exporter._translate_data([self.metrics["histogram"]]) + actual = self.exporter._translate_data(self.metrics["histogram"]) + self.assertEqual(expected, actual) + + def test_translate_multiple_scope_histogram(self): + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + flags=pb2.DataPointFlags.FLAG_NONE, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ), + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + flags=pb2.DataPointFlags.FLAG_NONE, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ), + ], + ), + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="second_name", version="second_version" + ), + metrics=[ + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + flags=pb2.DataPointFlags.FLAG_NONE, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + ], + ), + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="third_name", version="third_version" + ), + metrics=[ + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + flags=pb2.DataPointFlags.FLAG_NONE, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + ], + ), + ], + ) + ] + ) + # pylint: disable=protected-access + actual = self.exporter._translate_data(self.multiple_scope_histogram) self.assertEqual(expected, actual) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 3bf6e54c98..13032e319e 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -67,7 +67,7 @@ from json import dumps from logging import getLogger from re import IGNORECASE, UNICODE, compile -from typing import Dict, Iterable, Sequence, Tuple, Union +from typing import Dict, Sequence, Tuple, Union from prometheus_client.core import ( REGISTRY, @@ -80,20 +80,23 @@ from opentelemetry.sdk._metrics.export import ( Gauge, Histogram, - Metric, + HistogramDataPoint, MetricReader, + MetricsData, Sum, ) _logger = getLogger(__name__) -def _convert_buckets(metric: Metric) -> Sequence[Tuple[str, int]]: +def _convert_buckets( + bucket_counts: Sequence[int], explicit_bounds: Sequence[float] +) -> Sequence[Tuple[str, int]]: buckets = [] total_count = 0 for upper_bound, count in zip( - chain(metric.point.explicit_bounds, ["+Inf"]), - metric.point.bucket_counts, + chain(explicit_bounds, ["+Inf"]), + bucket_counts, ): total_count += count buckets.append((f"{upper_bound}", total_count)) @@ -117,13 +120,13 @@ def __init__(self, prefix: str = "") -> None: def _receive_metrics( self, - metrics: Iterable[Metric], + metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> None: - if metrics is None: + if metrics_data is None: return - self._collector.add_metrics_data(metrics) + self._collector.add_metrics_data(metrics_data) def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: REGISTRY.unregister(self._collector) @@ -139,14 +142,14 @@ class _CustomCollector: def __init__(self, prefix: str = ""): self._prefix = prefix self._callback = None - self._metrics_to_export = deque() + self._metrics_datas = deque() self._non_letters_digits_underscore_re = compile( r"[^\w]", UNICODE | IGNORECASE ) - def add_metrics_data(self, export_records: Sequence[Metric]) -> None: + def add_metrics_data(self, metrics_data: MetricsData) -> None: """Add metrics to Prometheus data""" - self._metrics_to_export.append(export_records) + self._metrics_datas.append(metrics_data) def collect(self) -> None: """Collect fetches the metrics from OpenTelemetry @@ -159,96 +162,147 @@ def collect(self) -> None: metric_family_id_metric_family = {} - while self._metrics_to_export: - for export_record in self._metrics_to_export.popleft(): - self._translate_to_prometheus( - export_record, metric_family_id_metric_family - ) + while self._metrics_datas: + self._translate_to_prometheus( + self._metrics_datas.popleft(), metric_family_id_metric_family + ) if metric_family_id_metric_family: for metric_family in metric_family_id_metric_family.values(): yield metric_family + # pylint: disable=too-many-locals,too-many-branches def _translate_to_prometheus( self, - metric: Metric, + metrics_data: MetricsData, metric_family_id_metric_family: Dict[str, PrometheusMetric], ): - label_values = [] - label_keys = [] - for key, value in metric.attributes.items(): - label_keys.append(self._sanitize(key)) - label_values.append(self._check_value(value)) - - metric_name = "" - if self._prefix != "": - metric_name = self._prefix + "_" - metric_name += self._sanitize(metric.name) - - description = metric.description or "" - - metric_family_id = "|".join( - [metric_name, description, "%".join(label_keys), metric.unit] - ) - - if isinstance(metric.point, Sum): - - metric_family_id = "|".join( - [metric_family_id, CounterMetricFamily.__name__] - ) - - if metric_family_id not in metric_family_id_metric_family.keys(): - metric_family_id_metric_family[ - metric_family_id - ] = CounterMetricFamily( - name=metric_name, - documentation=description, - labels=label_keys, - unit=metric.unit, + metrics = [] + + for resource_metrics in metrics_data.resource_metrics: + for scope_metrics in resource_metrics.scope_metrics: + for metric in scope_metrics.metrics: + metrics.append(metric) + + for metric in metrics: + label_valuess = [] + values = [] + + pre_metric_family_ids = [] + + metric_name = "" + if self._prefix != "": + metric_name = self._prefix + "_" + metric_name += self._sanitize(metric.name) + + metric_description = metric.description or "" + + for number_data_point in metric.data.data_points: + label_keys = [] + label_values = [] + + for key, value in number_data_point.attributes.items(): + label_keys.append(self._sanitize(key)) + label_values.append(self._check_value(value)) + + pre_metric_family_ids.append( + "|".join( + [ + metric_name, + metric_description, + "%".join(label_keys), + metric.unit, + ] + ) ) - metric_family_id_metric_family[metric_family_id].add_metric( - labels=label_values, value=metric.point.value - ) - elif isinstance(metric.point, Gauge): - - metric_family_id = "|".join( - [metric_family_id, GaugeMetricFamily.__name__] - ) - - if metric_family_id not in metric_family_id_metric_family.keys(): - metric_family_id_metric_family[ - metric_family_id - ] = GaugeMetricFamily( - name=metric_name, - documentation=description, - labels=label_keys, - unit=metric.unit, - ) - metric_family_id_metric_family[metric_family_id].add_metric( - labels=label_values, value=metric.point.value - ) - elif isinstance(metric.point, Histogram): - - metric_family_id = "|".join( - [metric_family_id, HistogramMetricFamily.__name__] - ) - if metric_family_id not in metric_family_id_metric_family.keys(): - metric_family_id_metric_family[ - metric_family_id - ] = HistogramMetricFamily( - name=metric_name, - documentation=description, - labels=label_keys, - unit=metric.unit, - ) - metric_family_id_metric_family[metric_family_id].add_metric( - labels=label_values, - buckets=_convert_buckets(metric), - sum_value=metric.point.sum, - ) - else: - _logger.warning("Unsupported metric type. %s", type(metric.point)) + label_valuess.append(label_values) + if isinstance(number_data_point, HistogramDataPoint): + values.append( + { + "bucket_counts": number_data_point.bucket_counts, + "explicit_bounds": ( + number_data_point.explicit_bounds + ), + "sum": number_data_point.sum, + } + ) + else: + values.append(number_data_point.value) + + for pre_metric_family_id, label_values, value in zip( + pre_metric_family_ids, label_valuess, values + ): + if isinstance(metric.data, Sum): + + metric_family_id = "|".join( + [pre_metric_family_id, CounterMetricFamily.__name__] + ) + + if metric_family_id not in metric_family_id_metric_family: + metric_family_id_metric_family[ + metric_family_id + ] = CounterMetricFamily( + name=metric_name, + documentation=metric_description, + labels=label_keys, + unit=metric.unit, + ) + metric_family_id_metric_family[ + metric_family_id + ].add_metric(labels=label_values, value=value) + elif isinstance(metric.data, Gauge): + + metric_family_id = "|".join( + [pre_metric_family_id, GaugeMetricFamily.__name__] + ) + + if ( + metric_family_id + not in metric_family_id_metric_family.keys() + ): + metric_family_id_metric_family[ + metric_family_id + ] = GaugeMetricFamily( + name=metric_name, + documentation=metric_description, + labels=label_keys, + unit=metric.unit, + ) + metric_family_id_metric_family[ + metric_family_id + ].add_metric(labels=label_values, value=value) + elif isinstance(metric.data, Histogram): + + metric_family_id = "|".join( + [pre_metric_family_id, HistogramMetricFamily.__name__] + ) + + if ( + metric_family_id + not in metric_family_id_metric_family.keys() + ): + metric_family_id_metric_family[ + metric_family_id + ] = HistogramMetricFamily( + name=metric_name, + documentation=metric_description, + labels=label_keys, + unit=metric.unit, + ) + metric_family_id_metric_family[ + metric_family_id + ].add_metric( + labels=label_values, + buckets=_convert_buckets( + value["bucket_counts"], value["explicit_bounds"] + ), + sum_value=value["sum"], + ) + else: + _logger.warning( + "Unsupported metric data. %s", type(metric.data) + ) def _sanitize(self, key: str) -> str: """sanitize the given metric name or label according to Prometheus rule. diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index e5dcb79683..9ad3a69247 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from textwrap import dedent -from unittest import mock +from unittest import TestCase +from unittest.mock import Mock, patch from prometheus_client import generate_latest from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily @@ -23,19 +23,26 @@ PrometheusMetricReader, _CustomCollector, ) -from opentelemetry.sdk._metrics.export import AggregationTemporality, Histogram +from opentelemetry.sdk._metrics.export import ( + AggregationTemporality, + Histogram, + HistogramDataPoint, + Metric, + MetricsData, + ResourceMetrics, + ScopeMetrics, +) from opentelemetry.test.metrictestutil import ( _generate_gauge, - _generate_metric, _generate_sum, _generate_unsupported_metric, ) -class TestPrometheusMetricReader(unittest.TestCase): +class TestPrometheusMetricReader(TestCase): def setUp(self): - self._mock_registry_register = mock.Mock() - self._registry_register_patch = mock.patch( + self._mock_registry_register = Mock() + self._registry_register_patch = patch( "prometheus_client.core.REGISTRY.register", side_effect=self._mock_registry_register, ) @@ -49,7 +56,7 @@ def test_constructor(self): self.assertTrue(self._mock_registry_register.called) def test_shutdown(self): - with mock.patch( + with patch( "prometheus_client.core.REGISTRY.unregister" ) as registry_unregister_patch: exporter = PrometheusMetricReader() @@ -57,23 +64,45 @@ def test_shutdown(self): self.assertTrue(registry_unregister_patch.called) def test_histogram_to_prometheus(self): - record = _generate_metric( - "test@name", - Histogram( - aggregation_temporality=AggregationTemporality.CUMULATIVE, - bucket_counts=[1, 3, 2], - explicit_bounds=[123.0, 456.0], - start_time_unix_nano=1641946016139533244, - max=457, - min=1, - sum=579.0, - time_unix_nano=1641946016139533244, + metric = Metric( + name="test@name", + description="foo", + unit="s", + data=Histogram( + data_points=[ + HistogramDataPoint( + attributes={"histo": 1}, + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=6, + sum=579.0, + bucket_counts=[1, 3, 2], + explicit_bounds=[123.0, 456.0], + min=1, + max=457, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, ), - attributes={"histo": 1}, + ) + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Mock(), + scope_metrics=[ + ScopeMetrics( + scope=Mock(), + metrics=[metric], + schema_url="schema_url", + ) + ], + schema_url="schema_url", + ) + ] ) collector = _CustomCollector("testprefix") - collector.add_metrics_data([record]) + collector.add_metrics_data(metrics_data) result_bytes = generate_latest(collector) result = result_bytes.decode("utf-8") self.assertEqual( @@ -93,15 +122,32 @@ def test_histogram_to_prometheus(self): def test_sum_to_prometheus(self): labels = {"environment@": "staging", "os": "Windows"} - record = _generate_sum( + metric = _generate_sum( "test@sum", 123, attributes=labels, description="testdesc", unit="testunit", ) + + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Mock(), + scope_metrics=[ + ScopeMetrics( + scope=Mock(), + metrics=[metric], + schema_url="schema_url", + ) + ], + schema_url="schema_url", + ) + ] + ) + collector = _CustomCollector("testprefix") - collector.add_metrics_data([record]) + collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): self.assertEqual(type(prometheus_metric), CounterMetricFamily) @@ -121,15 +167,32 @@ def test_sum_to_prometheus(self): def test_gauge_to_prometheus(self): labels = {"environment@": "dev", "os": "Unix"} - record = _generate_gauge( + metric = _generate_gauge( "test@gauge", 123, attributes=labels, description="testdesc", unit="testunit", ) + + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Mock(), + scope_metrics=[ + ScopeMetrics( + scope=Mock(), + metrics=[metric], + schema_url="schema_url", + ) + ], + schema_url="schema_url", + ) + ] + ) + collector = _CustomCollector("testprefix") - collector.add_metrics_data([record]) + collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): self.assertEqual(type(prometheus_metric), GaugeMetricFamily) @@ -170,15 +233,30 @@ def test_sanitize(self): def test_list_labels(self): labels = {"environment@": ["1", "2", "3"], "os": "Unix"} - record = _generate_gauge( + metric = _generate_gauge( "test@gauge", 123, attributes=labels, description="testdesc", unit="testunit", ) + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Mock(), + scope_metrics=[ + ScopeMetrics( + scope=Mock(), + metrics=[metric], + schema_url="schema_url", + ) + ], + schema_url="schema_url", + ) + ] + ) collector = _CustomCollector("testprefix") - collector.add_metrics_data([record]) + collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): self.assertEqual(type(prometheus_metric), GaugeMetricFamily) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py index 2f002a272e..77a95cbf89 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py @@ -22,16 +22,11 @@ Aggregation, DefaultAggregation, _Aggregation, - _convert_aggregation_temporality, - _PointVarT, _SumAggregation, ) from opentelemetry.sdk._metrics._internal.export import AggregationTemporality from opentelemetry.sdk._metrics._internal.measurement import Measurement -from opentelemetry.sdk._metrics._internal.point import Metric -from opentelemetry.sdk._metrics._internal.sdk_configuration import ( - SdkConfiguration, -) +from opentelemetry.sdk._metrics._internal.point import DataPointT from opentelemetry.sdk._metrics._internal.view import View _logger = getLogger(__name__) @@ -42,17 +37,12 @@ def __init__( self, view: View, instrument: Instrument, - sdk_config: SdkConfiguration, - instrument_class_temporality: Dict[type, AggregationTemporality], instrument_class_aggregation: Dict[type, Aggregation], ): self._view = view self._instrument = instrument - self._sdk_config = sdk_config self._attributes_aggregation: Dict[frozenset, _Aggregation] = {} - self._attributes_previous_point: Dict[frozenset, _PointVarT] = {} self._lock = Lock() - self._instrument_class_temporality = instrument_class_temporality self._instrument_class_aggregation = instrument_class_aggregation self._name = self._view._name or self._instrument.name self._description = ( @@ -124,46 +114,10 @@ def consume_measurement(self, measurement: Measurement) -> None: self._attributes_aggregation[attributes].aggregate(measurement) - def collect(self) -> Iterable[Metric]: + def collect( + self, aggregation_temporality: AggregationTemporality + ) -> Iterable[DataPointT]: with self._lock: - for ( - attributes, - aggregation, - ) in self._attributes_aggregation.items(): - - previous_point = self._attributes_previous_point.get( - attributes - ) - - current_point = aggregation.collect() - - # pylint: disable=assignment-from-none - - self._attributes_previous_point[ - attributes - ] = _convert_aggregation_temporality( - previous_point, - current_point, - AggregationTemporality.CUMULATIVE, - ) - - if current_point is not None: - - yield Metric( - attributes=dict(attributes), - description=self._description, - instrumentation_scope=( - self._instrument.instrumentation_scope - ), - name=self._name, - resource=self._sdk_config.resource, - unit=self._instrument.unit, - point=_convert_aggregation_temporality( - previous_point, - current_point, - self._instrument_class_temporality[ - self._instrument.__class__ - ], - ), - ) + for aggregation in self._attributes_aggregation.values(): + yield aggregation.collect(aggregation_temporality) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py index 1d10b3f8ea..91a5b4a02f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py @@ -14,7 +14,6 @@ from abc import ABC, abstractmethod from bisect import bisect_left -from dataclasses import replace from enum import IntEnum from logging import getLogger from math import inf @@ -37,11 +36,15 @@ from opentelemetry.sdk._metrics._internal.point import ( Histogram as HistogramPoint, ) -from opentelemetry.sdk._metrics._internal.point import PointT, Sum +from opentelemetry.sdk._metrics._internal.point import ( + HistogramDataPoint, + NumberDataPoint, + Sum, +) from opentelemetry.util._time import _time_ns from opentelemetry.util.types import Attributes -_PointVarT = TypeVar("_PointVarT", bound=PointT) +_DataPointVarT = TypeVar("_DataPointVarT", NumberDataPoint, HistogramDataPoint) _logger = getLogger(__name__) @@ -58,17 +61,21 @@ class AggregationTemporality(IntEnum): CUMULATIVE = 2 -class _Aggregation(ABC, Generic[_PointVarT]): +class _Aggregation(ABC, Generic[_DataPointVarT]): def __init__(self, attributes: Attributes): self._lock = Lock() self._attributes = attributes + self._previous_point = None @abstractmethod def aggregate(self, measurement: Measurement) -> None: pass @abstractmethod - def collect(self) -> Optional[_PointVarT]: + def collect( + self, + aggregation_temporality: AggregationTemporality, + ) -> Optional[_DataPointVarT]: pass @@ -76,7 +83,10 @@ class _DropAggregation(_Aggregation): def aggregate(self, measurement: Measurement) -> None: pass - def collect(self) -> Optional[_PointVarT]: + def collect( + self, + aggregation_temporality: AggregationTemporality, + ) -> Optional[_DataPointVarT]: pass @@ -176,7 +186,9 @@ def aggregate(self, measurement: Measurement) -> None: self._value = 0 self._value = self._value + measurement.value - def collect(self) -> Optional[Sum]: + def collect( + self, aggregation_temporality: AggregationTemporality + ) -> Optional[NumberDataPoint]: """ Atomically return a point for the current value of the metric and reset the aggregation value. @@ -192,28 +204,52 @@ def collect(self) -> Optional[Sum]: self._value = 0 self._start_time_unix_nano = now + 1 - return Sum( - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=self._instrument_is_monotonic, - start_time_unix_nano=start_time_unix_nano, - time_unix_nano=now, - value=value, - ) + else: - with self._lock: - if self._value is None: - return None - value = self._value - self._value = None + with self._lock: + if self._value is None: + return None + value = self._value + self._value = None + start_time_unix_nano = self._start_time_unix_nano - return Sum( - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=self._instrument_is_monotonic, - start_time_unix_nano=self._start_time_unix_nano, + current_point = NumberDataPoint( + attributes=self._attributes, + start_time_unix_nano=start_time_unix_nano, time_unix_nano=now, value=value, ) + if self._previous_point is None or ( + self._instrument_temporality is aggregation_temporality + ): + # Output DELTA for a synchronous instrument + # Output CUMULATIVE for an asynchronous instrument + self._previous_point = current_point + return current_point + + if aggregation_temporality is AggregationTemporality.DELTA: + # Output temporality DELTA for an asynchronous instrument + value = current_point.value - self._previous_point.value + output_start_time_unix_nano = self._previous_point.time_unix_nano + + else: + # Output CUMULATIVE for a synchronous instrument + value = current_point.value + self._previous_point.value + output_start_time_unix_nano = ( + self._previous_point.start_time_unix_nano + ) + + current_point = NumberDataPoint( + attributes=self._attributes, + start_time_unix_nano=output_start_time_unix_nano, + time_unix_nano=current_point.time_unix_nano, + value=value, + ) + + self._previous_point = current_point + return current_point + class _LastValueAggregation(_Aggregation[Gauge]): def __init__(self, attributes: Attributes): @@ -224,7 +260,10 @@ def aggregate(self, measurement: Measurement): with self._lock: self._value = measurement.value - def collect(self) -> Optional[Gauge]: + def collect( + self, + aggregation_temporality: AggregationTemporality, + ) -> Optional[_DataPointVarT]: """ Atomically return a point for the current value of the metric. """ @@ -234,7 +273,9 @@ def collect(self) -> Optional[Gauge]: value = self._value self._value = None - return Gauge( + return NumberDataPoint( + attributes=self._attributes, + start_time_unix_nano=0, time_unix_nano=_time_ns(), value=value, ) @@ -266,6 +307,11 @@ def __init__( self._sum = 0 self._record_min_max = record_min_max self._start_time_unix_nano = _time_ns() + # It is assumed that the "natural" aggregation temporality for a + # Histogram instrument is DELTA, like the "natural" aggregation + # temporality for a Counter is DELTA and the "natural" aggregation + # temporality for an ObservableCounter is CUMULATIVE. + self._instrument_temporality = AggregationTemporality.DELTA def _get_empty_bucket_counts(self) -> List[int]: return [0] * (len(self._boundaries) + 1) @@ -282,18 +328,24 @@ def aggregate(self, measurement: Measurement) -> None: self._bucket_counts[bisect_left(self._boundaries, value)] += 1 - def collect(self) -> HistogramPoint: + def collect( + self, + aggregation_temporality: AggregationTemporality, + ) -> Optional[_DataPointVarT]: """ Atomically return a point for the current value of the metric. """ now = _time_ns() with self._lock: - value = self._bucket_counts + if not any(self._bucket_counts): + return None + + bucket_counts = self._bucket_counts start_time_unix_nano = self._start_time_unix_nano - histogram_sum = self._sum - histogram_max = self._max - histogram_min = self._min + sum_ = self._sum + max_ = self._max + min_ = self._min self._bucket_counts = self._get_empty_bucket_counts() self._start_time_unix_nano = now + 1 @@ -301,146 +353,64 @@ def collect(self) -> HistogramPoint: self._min = inf self._max = -inf - return HistogramPoint( - aggregation_temporality=AggregationTemporality.DELTA, - bucket_counts=tuple(value), - explicit_bounds=self._boundaries, - max=histogram_max, - min=histogram_min, + current_point = HistogramDataPoint( + attributes=self._attributes, start_time_unix_nano=start_time_unix_nano, - sum=histogram_sum, time_unix_nano=now, + count=sum(bucket_counts), + sum=sum_, + bucket_counts=tuple(bucket_counts), + explicit_bounds=self._boundaries, + min=min_, + max=max_, ) - -# pylint: disable=too-many-return-statements,too-many-branches -def _convert_aggregation_temporality( - previous_point: Optional[_PointVarT], - current_point: _PointVarT, - aggregation_temporality: AggregationTemporality, -) -> _PointVarT: - """Converts `current_point` to the requested `aggregation_temporality` - given the `previous_point`. - - `previous_point` must have `CUMULATIVE` temporality. `current_point` may - have `DELTA` or `CUMULATIVE` temporality. - - The output point will have temporality `aggregation_temporality`. Since - `GAUGE` points have no temporality, they are returned unchanged. - """ - - current_point_type = type(current_point) - - if current_point_type is Gauge: - return current_point - - if ( - previous_point is not None - and current_point is not None - and type(previous_point) is not type(current_point) - ): - _logger.warning( - "convert_aggregation_temporality called with mismatched " - "point types: %s and %s", - type(previous_point), - current_point_type, - ) - - return current_point - - if current_point_type is Sum: - if previous_point is None: - # Output CUMULATIVE for a synchronous instrument - # There is no previous value, return the delta point as a - # cumulative - return replace( - current_point, aggregation_temporality=aggregation_temporality - ) - if previous_point.aggregation_temporality is not ( - AggregationTemporality.CUMULATIVE - ): - raise Exception( - "previous_point aggregation temporality must be CUMULATIVE" - ) - - if current_point.aggregation_temporality is aggregation_temporality: - # Output DELTA for a synchronous instrument - # Output CUMULATIVE for an asynchronous instrument - return current_point - - if aggregation_temporality is AggregationTemporality.DELTA: - # Output temporality DELTA for an asynchronous instrument - value = current_point.value - previous_point.value - output_start_time_unix_nano = previous_point.time_unix_nano - - else: - # Output CUMULATIVE for a synchronous instrument - value = current_point.value + previous_point.value - output_start_time_unix_nano = previous_point.start_time_unix_nano - - is_monotonic = ( - previous_point.is_monotonic and current_point.is_monotonic - ) - - return Sum( - start_time_unix_nano=output_start_time_unix_nano, - time_unix_nano=current_point.time_unix_nano, - value=value, - aggregation_temporality=aggregation_temporality, - is_monotonic=is_monotonic, - ) - - if current_point_type is HistogramPoint: - if previous_point is None: - return replace( - current_point, aggregation_temporality=aggregation_temporality - ) - if previous_point.aggregation_temporality is not ( - AggregationTemporality.CUMULATIVE + if self._previous_point is None or ( + self._instrument_temporality is aggregation_temporality ): - raise Exception( - "previous_point aggregation temporality must be CUMULATIVE" - ) - - if current_point.aggregation_temporality is aggregation_temporality: + self._previous_point = current_point return current_point max_ = current_point.max min_ = current_point.min if aggregation_temporality is AggregationTemporality.CUMULATIVE: - start_time_unix_nano = previous_point.start_time_unix_nano - sum_ = current_point.sum + previous_point.sum + start_time_unix_nano = self._previous_point.start_time_unix_nano + sum_ = current_point.sum + self._previous_point.sum # Only update min/max on delta -> cumulative - max_ = max(current_point.max, previous_point.max) - min_ = min(current_point.min, previous_point.min) + max_ = max(current_point.max, self._previous_point.max) + min_ = min(current_point.min, self._previous_point.min) bucket_counts = [ curr_count + prev_count for curr_count, prev_count in zip( - current_point.bucket_counts, previous_point.bucket_counts + current_point.bucket_counts, + self._previous_point.bucket_counts, ) ] else: - start_time_unix_nano = previous_point.time_unix_nano - sum_ = current_point.sum - previous_point.sum + start_time_unix_nano = self._previous_point.time_unix_nano + sum_ = current_point.sum - self._previous_point.sum bucket_counts = [ curr_count - prev_count for curr_count, prev_count in zip( - current_point.bucket_counts, previous_point.bucket_counts + current_point.bucket_counts, + self._previous_point.bucket_counts, ) ] - return HistogramPoint( - aggregation_temporality=aggregation_temporality, - bucket_counts=bucket_counts, - explicit_bounds=current_point.explicit_bounds, - max=max_, - min=min_, + current_point = HistogramDataPoint( + attributes=self._attributes, start_time_unix_nano=start_time_unix_nano, - sum=sum_, time_unix_nano=current_point.time_unix_nano, + count=sum(bucket_counts), + sum=sum_, + bucket_counts=tuple(bucket_counts), + explicit_bounds=current_point.explicit_bounds, + min=min_, + max=max_, ) - return None + self._previous_point = current_point + return current_point class ExplicitBucketHistogramAggregation(Aggregation): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py index d8a753ba7b..1b10d2da66 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py @@ -19,7 +19,7 @@ from os import environ, linesep from sys import stdout from threading import Event, RLock, Thread -from typing import IO, Callable, Dict, Iterable, List, Optional, Sequence +from typing import IO, Callable, Dict, Iterable, Optional from typing_extensions import final @@ -43,6 +43,7 @@ ObservableUpDownCounter, UpDownCounter, ) +from opentelemetry.sdk._metrics._internal.point import MetricsData from opentelemetry.sdk.environment_variables import ( _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, ) @@ -71,7 +72,7 @@ class MetricExporter(ABC): @abstractmethod def export( self, - metrics: Sequence["opentelemetry.sdk._metrics.export.Metric"], + metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: @@ -113,12 +114,11 @@ def __init__( def export( self, - metrics: Sequence["opentelemetry.sdk._metrics.export.Metric"], + metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: - for metric in metrics: - self.out.write(self.formatter(metric)) + self.out.write(self.formatter(metrics_data)) self.out.flush() return MetricExportResult.SUCCESS @@ -262,7 +262,7 @@ def _set_collect_callback( @abstractmethod def _receive_metrics( self, - metrics: Iterable["opentelemetry.sdk._metrics.export.Metric"], + metrics_data: "opentelemetry.sdk._metrics.export.MetricsData", timeout_millis: float = 10_000, **kwargs, ) -> None: @@ -283,7 +283,7 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: class InMemoryMetricReader(MetricReader): - """Implementation of `MetricReader` that returns its metrics from :func:`get_metrics`. + """Implementation of `MetricReader` that returns its metrics from :func:`get_metrics_data`. This is useful for e.g. unit tests. """ @@ -300,24 +300,28 @@ def __init__( preferred_aggregation=preferred_aggregation, ) self._lock = RLock() - self._metrics: List["opentelemetry.sdk._metrics.export.Metric"] = [] + self._metrics_data: ( + "opentelemetry.sdk._metrics.export.MetricsData" + ) = None - def get_metrics(self) -> List["opentelemetry.sdk._metrics.export.Metric"]: + def get_metrics_data( + self, + ) -> ("opentelemetry.sdk._metrics.export.MetricsData"): """Reads and returns current metrics from the SDK""" with self._lock: self.collect() - metrics = self._metrics - self._metrics = [] - return metrics + metrics_data = self._metrics_data + self._metrics_data = None + return metrics_data def _receive_metrics( self, - metrics: Iterable["opentelemetry.sdk._metrics.export.Metric"], + metrics_data: "opentelemetry.sdk._metrics.export.MetricsData", timeout_millis: float = 10_000, **kwargs, ) -> None: with self._lock: - self._metrics = list(metrics) + self._metrics_data = metrics_data def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass @@ -389,15 +393,15 @@ def _ticker(self) -> None: def _receive_metrics( self, - metrics: Iterable["opentelemetry.sdk._metrics.export.Metric"], + metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> None: - if metrics is None: + if metrics_data is None: return token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: - self._exporter.export(metrics, timeout_millis=timeout_millis) + self._exporter.export(metrics_data, timeout_millis=timeout_millis) except Exception as e: # pylint: disable=broad-except,invalid-name _logger.exception("Exception while exporting metrics %s", str(e)) detach(token) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py index b163f414af..9b452379d1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py @@ -14,23 +14,41 @@ from logging import getLogger from threading import RLock -from typing import Dict, Iterable, List +from typing import Dict, List -from opentelemetry._metrics import Asynchronous, Instrument +from opentelemetry._metrics import ( + Asynchronous, + Counter, + Instrument, + ObservableCounter, +) from opentelemetry.sdk._metrics._internal._view_instrument_match import ( _ViewInstrumentMatch, ) from opentelemetry.sdk._metrics._internal.aggregation import ( Aggregation, ExplicitBucketHistogramAggregation, + _DropAggregation, + _ExplicitBucketHistogramAggregation, + _LastValueAggregation, + _SumAggregation, ) from opentelemetry.sdk._metrics._internal.export import AggregationTemporality from opentelemetry.sdk._metrics._internal.measurement import Measurement -from opentelemetry.sdk._metrics._internal.point import Metric +from opentelemetry.sdk._metrics._internal.point import ( + Gauge, + Histogram, + Metric, + MetricsData, + ResourceMetrics, + ScopeMetrics, + Sum, +) from opentelemetry.sdk._metrics._internal.sdk_configuration import ( SdkConfiguration, ) from opentelemetry.sdk._metrics._internal.view import View +from opentelemetry.sdk.util.instrumentation import InstrumentationScope _logger = getLogger(__name__) @@ -81,10 +99,6 @@ def _get_or_init_view_instrument_match( _ViewInstrumentMatch( view=_DEFAULT_VIEW, instrument=instrument, - sdk_config=self._sdk_config, - instrument_class_temporality=( - self._instrument_class_temporality - ), instrument_class_aggregation=( self._instrument_class_aggregation ), @@ -102,10 +116,9 @@ def consume_measurement(self, measurement: Measurement) -> None: ): view_instrument_match.consume_measurement(measurement) - def collect(self) -> Iterable[Metric]: + def collect(self) -> MetricsData: # Use a list instead of yielding to prevent a slow reader from holding # SDK locks - metrics: List[Metric] = [] # While holding the lock, new _ViewInstrumentMatch can't be added from # another thread (so we are sure we collect all existing view). @@ -116,13 +129,101 @@ def collect(self) -> Iterable[Metric]: # streams produced by the SDK, but we still align the output timestamps # for a single instrument. with self._lock: + + instrumentation_scope_scope_metrics: ( + Dict[InstrumentationScope, ScopeMetrics] + ) = {} + for ( - view_instrument_matches - ) in self._instrument_view_instrument_matches.values(): + instrument, + view_instrument_matches, + ) in self._instrument_view_instrument_matches.items(): + aggregation_temporality = self._instrument_class_temporality[ + instrument.__class__ + ] + + metrics: List[Metric] = [] + for view_instrument_match in view_instrument_matches: - metrics.extend(view_instrument_match.collect()) - return metrics + if isinstance( + # pylint: disable=protected-access + view_instrument_match._aggregation, + _SumAggregation, + ): + data = Sum( + aggregation_temporality=aggregation_temporality, + data_points=view_instrument_match.collect( + aggregation_temporality + ), + is_monotonic=isinstance( + instrument, (Counter, ObservableCounter) + ), + ) + elif isinstance( + # pylint: disable=protected-access + view_instrument_match._aggregation, + _LastValueAggregation, + ): + data = Gauge( + data_points=view_instrument_match.collect( + aggregation_temporality + ) + ) + elif isinstance( + # pylint: disable=protected-access + view_instrument_match._aggregation, + _ExplicitBucketHistogramAggregation, + ): + data = Histogram( + data_points=view_instrument_match.collect( + aggregation_temporality + ), + aggregation_temporality=aggregation_temporality, + ) + elif isinstance( + # pylint: disable=protected-access + view_instrument_match._aggregation, + _DropAggregation, + ): + continue + + metrics.append( + Metric( + # pylint: disable=protected-access + name=view_instrument_match._name, + description=view_instrument_match._description, + unit=view_instrument_match._instrument.unit, + data=data, + ) + ) + + if instrument.instrumentation_scope not in ( + instrumentation_scope_scope_metrics + ): + instrumentation_scope_scope_metrics[ + instrument.instrumentation_scope + ] = ScopeMetrics( + scope=instrument.instrumentation_scope, + metrics=metrics, + schema_url=instrument.instrumentation_scope.schema_url, + ) + else: + instrumentation_scope_scope_metrics[ + instrument.instrumentation_scope + ].metrics.extend(metrics) + + return MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=self._sdk_config.resource, + scope_metrics=list( + instrumentation_scope_scope_metrics.values() + ), + schema_url=self._sdk_config.resource.schema_url, + ) + ] + ) def _handle_view_instrument_match( self, @@ -140,10 +241,6 @@ def _handle_view_instrument_match( new_view_instrument_match = _ViewInstrumentMatch( view=view, instrument=instrument, - sdk_config=self._sdk_config, - instrument_class_temporality=( - self._instrument_class_temporality - ), instrument_class_aggregation=( self._instrument_class_aggregation ), diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py index eca9783eee..4e57f7a377 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py @@ -25,18 +25,39 @@ from opentelemetry.util.types import Attributes +@dataclass(frozen=True) +class NumberDataPoint: + """Single data point in a timeseries that describes the time-varying scalar + value of a metric. + """ + + attributes: Attributes + start_time_unix_nano: int + time_unix_nano: int + value: Union[int, float] + + @dataclass(frozen=True) class Sum: """Represents the type of a scalar metric that is calculated as a sum of all reported measurements over a time interval.""" + data_points: Sequence[NumberDataPoint] aggregation_temporality: ( "opentelemetry.sdk._metrics.export.AggregationTemporality" ) is_monotonic: bool - start_time_unix_nano: int - time_unix_nano: int - value: Union[int, float] + + def to_json(self) -> str: + return dumps( + { + "data_points": dumps( + [asdict(data_point) for data_point in self.data_points] + ), + "aggregation_temporality": self.aggregation_temporality, + "is_monotonic": self.is_monotonic, + } + ) @dataclass(frozen=True) @@ -45,8 +66,33 @@ class Gauge: value for every data point. It should be used for an unknown aggregation.""" + data_points: Sequence[NumberDataPoint] + + def to_json(self) -> str: + return dumps( + { + "data_points": dumps( + [asdict(data_point) for data_point in self.data_points] + ) + } + ) + + +@dataclass(frozen=True) +class HistogramDataPoint: + """Single data point in a timeseries that describes the time-varying scalar + value of a metric. + """ + + attributes: Attributes + start_time_unix_nano: int time_unix_nano: int - value: Union[int, float] + count: int + sum: Union[int, float] + bucket_counts: Sequence[int] + explicit_bounds: Sequence[float] + min: float + max: float @dataclass(frozen=True) @@ -54,52 +100,67 @@ class Histogram: """Represents the type of a metric that is calculated by aggregating as a histogram of all reported measurements over a time interval.""" + data_points: Sequence[HistogramDataPoint] aggregation_temporality: ( "opentelemetry.sdk._metrics.export.AggregationTemporality" ) - bucket_counts: Sequence[int] - explicit_bounds: Sequence[float] - max: int - min: int - start_time_unix_nano: int - sum: Union[int, float] - time_unix_nano: int + def to_json(self) -> str: + return dumps( + { + "data_points": dumps( + [asdict(data_point) for data_point in self.data_points] + ), + "aggregation_temporality": self.aggregation_temporality, + } + ) -PointT = Union[Sum, Gauge, Histogram] + +DataT = Union[Sum, Gauge, Histogram] +DataPointT = Union[NumberDataPoint, HistogramDataPoint] @dataclass(frozen=True) class Metric: - """Represents a metric point in the OpenTelemetry data model to be exported - - Concrete metric types contain all the information as in the OTLP proto definitions - (https://github.com/open-telemetry/opentelemetry-proto/blob/b43e9b18b76abf3ee040164b55b9c355217151f3/opentelemetry/proto/metrics/v1/metrics.proto#L37) but are flattened as much as possible. - """ + """Represents a metric point in the OpenTelemetry data model to be + exported.""" - # common fields to all metric kinds - attributes: Attributes - description: str - instrumentation_scope: InstrumentationScope name: str - resource: Resource + description: str unit: str - point: PointT - """Contains non-common fields for the given metric""" + data: DataT def to_json(self) -> str: return dumps( { - "attributes": self.attributes if self.attributes else "", - "description": self.description if self.description else "", - "instrumentation_scope": repr(self.instrumentation_scope) - if self.instrumentation_scope - else "", "name": self.name, - "resource": repr(self.resource.attributes) - if self.resource - else "", + "description": self.description if self.description else "", "unit": self.unit if self.unit else "", - "point": asdict(self.point) if self.point else "", + "data": self.data.to_json(), } ) + + +@dataclass(frozen=True) +class ScopeMetrics: + """A collection of Metrics produced by a scope""" + + scope: InstrumentationScope + metrics: Sequence[Metric] + schema_url: str + + +@dataclass(frozen=True) +class ResourceMetrics: + """A collection of ScopeMetrics from a Resource""" + + resource: Resource + scope_metrics: Sequence[ScopeMetrics] + schema_url: str + + +@dataclass(frozen=True) +class MetricsData: + """An array of ResourceMetrics""" + + resource_metrics: Sequence[ResourceMetrics] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py index 107f73f79d..806755997e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py @@ -28,17 +28,23 @@ # The point module is not in the export directory to avoid a circular import. from opentelemetry.sdk._metrics._internal.point import ( # noqa: F401 + DataPointT, + DataT, Gauge, Histogram, + HistogramDataPoint, Metric, - PointT, + MetricsData, + NumberDataPoint, + ResourceMetrics, + ScopeMetrics, Sum, ) __all__ = [] for key, value in globals().copy().items(): if not key.startswith("_"): - if _version_info.minor == 6 and key == "PointT": + if _version_info.minor == 6 and key in ["DataPointT", "DataT"]: continue value.__module__ = __name__ __all__.append(key) diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py b/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py index 5534e85090..cd8d77ef51 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py @@ -31,8 +31,15 @@ def test_disable_default_views(self): counter.add(10, {"label": "value1"}) counter.add(10, {"label": "value2"}) counter.add(10, {"label": "value3"}) - - self.assertEqual(reader.get_metrics(), []) + self.assertEqual( + ( + reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics + ), + [], + ) def test_disable_default_views_add_custom(self): reader = InMemoryMetricReader() @@ -51,6 +58,13 @@ def test_disable_default_views_add_custom(self): counter.add(10, {"label": "value3"}) histogram.record(12, {"label": "value"}) - metrics = reader.get_metrics() - self.assertEqual(len(metrics), 1) - self.assertEqual(metrics[0].name, "testhist") + metrics = reader.get_metrics_data() + self.assertEqual(len(metrics.resource_metrics), 1) + self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1) + self.assertEqual( + len(metrics.resource_metrics[0].scope_metrics[0].metrics), 1 + ) + self.assertEqual( + metrics.resource_metrics[0].scope_metrics[0].metrics[0].name, + "testhist", + ) diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index e747e49cdb..e71f99d9ee 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dataclasses import replace -from logging import WARNING from math import inf from time import sleep from typing import Union @@ -29,15 +27,15 @@ UpDownCounter, ) from opentelemetry.sdk._metrics._internal.aggregation import ( - _convert_aggregation_temporality, _ExplicitBucketHistogramAggregation, _LastValueAggregation, _SumAggregation, ) from opentelemetry.sdk._metrics._internal.measurement import Measurement -from opentelemetry.sdk._metrics.export import AggregationTemporality, Gauge -from opentelemetry.sdk._metrics.export import Histogram as HistogramPoint -from opentelemetry.sdk._metrics.export import Sum +from opentelemetry.sdk._metrics.export import ( + AggregationTemporality, + NumberDataPoint, +) from opentelemetry.sdk._metrics.view import ( DefaultAggregation, ExplicitBucketHistogramAggregation, @@ -110,20 +108,44 @@ def test_collect_delta(self): """ synchronous_sum_aggregation = _SumAggregation( - Mock(), True, AggregationTemporality.DELTA + {}, True, AggregationTemporality.DELTA + ) + + synchronous_sum_aggregation.aggregate(measurement(1)) + first_sum = synchronous_sum_aggregation.collect( + AggregationTemporality.CUMULATIVE + ) + + self.assertEqual(first_sum.value, 1) + + synchronous_sum_aggregation.aggregate(measurement(1)) + second_sum = synchronous_sum_aggregation.collect( + AggregationTemporality.CUMULATIVE + ) + + self.assertEqual(second_sum.value, 2) + + self.assertEqual( + second_sum.start_time_unix_nano, first_sum.start_time_unix_nano + ) + + synchronous_sum_aggregation = _SumAggregation( + {}, True, AggregationTemporality.DELTA ) synchronous_sum_aggregation.aggregate(measurement(1)) - first_sum = synchronous_sum_aggregation.collect() + first_sum = synchronous_sum_aggregation.collect( + AggregationTemporality.DELTA + ) self.assertEqual(first_sum.value, 1) - self.assertTrue(first_sum.is_monotonic) synchronous_sum_aggregation.aggregate(measurement(1)) - second_sum = synchronous_sum_aggregation.collect() + second_sum = synchronous_sum_aggregation.collect( + AggregationTemporality.DELTA + ) self.assertEqual(second_sum.value, 1) - self.assertTrue(second_sum.is_monotonic) self.assertGreater( second_sum.start_time_unix_nano, first_sum.start_time_unix_nano @@ -131,32 +153,30 @@ def test_collect_delta(self): def test_collect_cumulative(self): """ - `SynchronousSumAggregation` collects sum metric points + `SynchronousSumAggregation` collects number data points """ sum_aggregation = _SumAggregation( - True, AggregationTemporality.CUMULATIVE, Mock() + {}, True, AggregationTemporality.CUMULATIVE ) sum_aggregation.aggregate(measurement(1)) - first_sum = sum_aggregation.collect() + first_sum = sum_aggregation.collect(AggregationTemporality.CUMULATIVE) self.assertEqual(first_sum.value, 1) - self.assertTrue(first_sum.is_monotonic) # should have been reset after first collect sum_aggregation.aggregate(measurement(1)) - second_sum = sum_aggregation.collect() + second_sum = sum_aggregation.collect(AggregationTemporality.CUMULATIVE) self.assertEqual(second_sum.value, 1) - self.assertTrue(second_sum.is_monotonic) self.assertEqual( second_sum.start_time_unix_nano, first_sum.start_time_unix_nano ) # if no point seen for a whole interval, should return None - third_sum = sum_aggregation.collect() + third_sum = sum_aggregation.collect(AggregationTemporality.CUMULATIVE) self.assertIsNone(third_sum) @@ -180,35 +200,44 @@ def test_aggregate(self): def test_collect(self): """ - `LastValueAggregation` collects sum metric points + `LastValueAggregation` collects number data points """ last_value_aggregation = _LastValueAggregation(Mock()) - self.assertIsNone(last_value_aggregation.collect()) + self.assertIsNone( + last_value_aggregation.collect(AggregationTemporality.CUMULATIVE) + ) last_value_aggregation.aggregate(measurement(1)) - first_gauge = last_value_aggregation.collect() - self.assertIsInstance(first_gauge, Gauge) + first_number_data_point = last_value_aggregation.collect( + AggregationTemporality.CUMULATIVE + ) + self.assertIsInstance(first_number_data_point, NumberDataPoint) - self.assertEqual(first_gauge.value, 1) + self.assertEqual(first_number_data_point.value, 1) last_value_aggregation.aggregate(measurement(1)) # CI fails the last assertion without this sleep(0.1) - second_gauge = last_value_aggregation.collect() + second_number_data_point = last_value_aggregation.collect( + AggregationTemporality.CUMULATIVE + ) - self.assertEqual(second_gauge.value, 1) + self.assertEqual(second_number_data_point.value, 1) self.assertGreater( - second_gauge.time_unix_nano, first_gauge.time_unix_nano + second_number_data_point.time_unix_nano, + first_number_data_point.time_unix_nano, ) # if no observation seen for the interval, it should return None - third_gauge = last_value_aggregation.collect() - self.assertIsNone(third_gauge) + third_number_data_point = last_value_aggregation.collect( + AggregationTemporality.CUMULATIVE + ) + self.assertIsNone(third_number_data_point) class TestExplicitBucketHistogramAggregation(TestCase): @@ -249,7 +278,9 @@ def test_aggregate(self): explicit_bucket_histogram_aggregation._bucket_counts[3], 1 ) - histo = explicit_bucket_histogram_aggregation.collect() + histo = explicit_bucket_histogram_aggregation.collect( + AggregationTemporality.CUMULATIVE + ) self.assertEqual(histo.sum, 14) def test_min_max(self): @@ -294,7 +325,9 @@ def test_collect(self): ) explicit_bucket_histogram_aggregation.aggregate(measurement(1)) - first_histogram = explicit_bucket_histogram_aggregation.collect() + first_histogram = explicit_bucket_histogram_aggregation.collect( + AggregationTemporality.CUMULATIVE + ) self.assertEqual(first_histogram.bucket_counts, (0, 1, 0, 0)) self.assertEqual(first_histogram.sum, 1) @@ -303,510 +336,42 @@ def test_collect(self): sleep(0.1) explicit_bucket_histogram_aggregation.aggregate(measurement(1)) - second_histogram = explicit_bucket_histogram_aggregation.collect() + second_histogram = explicit_bucket_histogram_aggregation.collect( + AggregationTemporality.CUMULATIVE + ) - self.assertEqual(second_histogram.bucket_counts, (0, 1, 0, 0)) - self.assertEqual(second_histogram.sum, 1) + self.assertEqual(second_histogram.bucket_counts, (0, 2, 0, 0)) + self.assertEqual(second_histogram.sum, 2) self.assertGreater( second_histogram.time_unix_nano, first_histogram.time_unix_nano ) - -class TestConvertAggregationTemporality(TestCase): - """ - Test aggregation temporality conversion algorithm - """ - - def test_previous_point_non_cumulative(self): - - with self.assertRaises(Exception): - - _convert_aggregation_temporality( - Sum( - start_time_unix_nano=0, - time_unix_nano=0, - value=0, - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=False, - ), - Sum( - start_time_unix_nano=0, - time_unix_nano=0, - value=0, - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=False, - ), - AggregationTemporality.DELTA, - ), - - def test_mismatched_point_types(self): - - current_point = Sum( - start_time_unix_nano=0, - time_unix_nano=0, - value=0, - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=False, - ) - - with self.assertLogs(level=WARNING): - self.assertIs( - _convert_aggregation_temporality( - Gauge(time_unix_nano=0, value=0), - current_point, - AggregationTemporality.DELTA, - ), - current_point, - ) - - with self.assertRaises(AssertionError): - with self.assertLogs(level=WARNING): - self.assertIs( - _convert_aggregation_temporality( - Gauge(time_unix_nano=0, value=0), - None, - AggregationTemporality.DELTA, - ), - current_point, - ) - - def test_current_point_sum_previous_point_none(self): - - current_point = Sum( - start_time_unix_nano=0, - time_unix_nano=0, - value=0, - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=False, - ) - - self.assertEqual( - _convert_aggregation_temporality( - None, current_point, AggregationTemporality.CUMULATIVE - ), - replace( - current_point, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - ), - ) - - def test_current_point_sum_current_point_same_aggregation_temporality( - self, - ): - - current_point = Sum( - start_time_unix_nano=0, - time_unix_nano=0, - value=0, - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=False, - ) - - self.assertEqual( - _convert_aggregation_temporality( - Sum( - start_time_unix_nano=0, - time_unix_nano=0, - value=0, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=False, - ), - current_point, - AggregationTemporality.DELTA, - ), - current_point, - ) - - current_point = Sum( - start_time_unix_nano=0, - time_unix_nano=0, - value=0, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=False, - ) - - self.assertEqual( - _convert_aggregation_temporality( - Sum( - start_time_unix_nano=0, - time_unix_nano=0, - value=0, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=False, - ), - current_point, - AggregationTemporality.CUMULATIVE, - ), - current_point, - ) - - def test_current_point_sum_aggregation_temporality_delta(self): - - self.assertEqual( - _convert_aggregation_temporality( - Sum( - start_time_unix_nano=1, - time_unix_nano=2, - value=3, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=False, - ), - Sum( - start_time_unix_nano=4, - time_unix_nano=5, - value=6, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=False, - ), - AggregationTemporality.DELTA, - ), - Sum( - start_time_unix_nano=2, - time_unix_nano=5, - value=3, - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=False, - ), - ) - - self.assertEqual( - _convert_aggregation_temporality( - Sum( - start_time_unix_nano=1, - time_unix_nano=2, - value=3, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, - ), - Sum( - start_time_unix_nano=4, - time_unix_nano=5, - value=6, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=False, - ), - AggregationTemporality.DELTA, - ), - Sum( - start_time_unix_nano=2, - time_unix_nano=5, - value=3, - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=False, - ), - ) - - self.assertEqual( - _convert_aggregation_temporality( - Sum( - start_time_unix_nano=1, - time_unix_nano=2, - value=3, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, - ), - Sum( - start_time_unix_nano=4, - time_unix_nano=5, - value=6, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, - ), - AggregationTemporality.DELTA, - ), - Sum( - start_time_unix_nano=2, - time_unix_nano=5, - value=3, - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=True, - ), - ) - - def test_current_point_sum_aggregation_temporality_cumulative(self): - - self.assertEqual( - _convert_aggregation_temporality( - Sum( - start_time_unix_nano=1, - time_unix_nano=2, - value=3, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=False, - ), - Sum( - start_time_unix_nano=4, - time_unix_nano=5, - value=6, - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=False, - ), - AggregationTemporality.CUMULATIVE, - ), - Sum( - start_time_unix_nano=1, - time_unix_nano=5, - value=9, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=False, - ), - ) - - self.assertEqual( - _convert_aggregation_temporality( - Sum( - start_time_unix_nano=1, - time_unix_nano=2, - value=3, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, - ), - Sum( - start_time_unix_nano=4, - time_unix_nano=5, - value=6, - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=False, - ), - AggregationTemporality.CUMULATIVE, - ), - Sum( - start_time_unix_nano=1, - time_unix_nano=5, - value=9, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=False, - ), - ) - - self.assertEqual( - _convert_aggregation_temporality( - Sum( - start_time_unix_nano=1, - time_unix_nano=2, - value=3, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, - ), - Sum( - start_time_unix_nano=4, - time_unix_nano=5, - value=6, - aggregation_temporality=AggregationTemporality.DELTA, - is_monotonic=True, - ), - AggregationTemporality.CUMULATIVE, - ), - Sum( - start_time_unix_nano=1, - time_unix_nano=5, - value=9, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, - ), - ) - - def test_current_point_gauge(self): - - current_point = Gauge(time_unix_nano=1, value=0) - self.assertEqual( - _convert_aggregation_temporality( - Gauge(time_unix_nano=0, value=0), - current_point, - AggregationTemporality.CUMULATIVE, - ), - current_point, - ) - - -class TestHistogramConvertAggregationTemporality(TestCase): - def test_previous_point_none(self): - - current_point = HistogramPoint( - aggregation_temporality=AggregationTemporality.DELTA, - bucket_counts=[0, 2, 1, 2, 0], - explicit_bounds=[0, 5, 10, 25], - max=24, - min=2, - start_time_unix_nano=0, - sum=70, - time_unix_nano=1, - ) - - self.assertEqual( - _convert_aggregation_temporality( - None, current_point, AggregationTemporality.CUMULATIVE - ), - replace( - current_point, - aggregation_temporality=AggregationTemporality.CUMULATIVE, - ), - ) - - def test_previous_point_non_cumulative(self): - - with self.assertRaises(Exception): - - _convert_aggregation_temporality( - HistogramPoint( - aggregation_temporality=AggregationTemporality.DELTA, - bucket_counts=[0, 2, 1, 2, 0], - explicit_bounds=[0, 5, 10, 25], - max=24, - min=2, - start_time_unix_nano=0, - sum=70, - time_unix_nano=1, - ), - HistogramPoint( - aggregation_temporality=AggregationTemporality.DELTA, - bucket_counts=[0, 1, 3, 0, 0], - explicit_bounds=[0, 5, 10, 25], - max=9, - min=2, - start_time_unix_nano=1, - sum=35, - time_unix_nano=2, - ), - AggregationTemporality.DELTA, - ), - - def test_same_aggregation_temporality_cumulative(self): - current_point = HistogramPoint( - aggregation_temporality=AggregationTemporality.CUMULATIVE, - bucket_counts=[0, 3, 4, 2, 0], - explicit_bounds=[0, 5, 10, 25], - max=24, - min=1, - start_time_unix_nano=0, - sum=105, - time_unix_nano=2, - ) - self.assertEqual( - _convert_aggregation_temporality( - HistogramPoint( - aggregation_temporality=AggregationTemporality.CUMULATIVE, - bucket_counts=[0, 2, 1, 2, 0], - explicit_bounds=[0, 5, 10, 25], - max=24, - min=1, - start_time_unix_nano=0, - sum=70, - time_unix_nano=1, - ), - current_point, - AggregationTemporality.CUMULATIVE, - ), - current_point, + explicit_bucket_histogram_aggregation = ( + _ExplicitBucketHistogramAggregation(Mock(), boundaries=[0, 1, 2]) ) - def test_same_aggregation_temporality_delta(self): - current_point = HistogramPoint( - aggregation_temporality=AggregationTemporality.DELTA, - bucket_counts=[0, 1, 3, 0, 0], - explicit_bounds=[0, 5, 10, 25], - max=9, - min=1, - start_time_unix_nano=1, - sum=35, - time_unix_nano=2, + explicit_bucket_histogram_aggregation.aggregate(measurement(1)) + first_histogram = explicit_bucket_histogram_aggregation.collect( + AggregationTemporality.DELTA ) - self.assertEqual( - _convert_aggregation_temporality( - HistogramPoint( - aggregation_temporality=AggregationTemporality.CUMULATIVE, - bucket_counts=[0, 3, 4, 2, 0], - explicit_bounds=[0, 5, 10, 25], - max=24, - min=1, - start_time_unix_nano=0, - sum=105, - time_unix_nano=2, - ), - current_point, - AggregationTemporality.DELTA, - ), - current_point, - ) + self.assertEqual(first_histogram.bucket_counts, (0, 1, 0, 0)) + self.assertEqual(first_histogram.sum, 1) - def test_aggregation_temporality_to_cumulative(self): - current_point = HistogramPoint( - aggregation_temporality=AggregationTemporality.DELTA, - bucket_counts=[0, 1, 3, 0, 0], - explicit_bounds=[0, 5, 10, 25], - max=24, - min=1, - start_time_unix_nano=1, - sum=35, - time_unix_nano=2, - ) + # CI fails the last assertion without this + sleep(0.1) - self.assertEqual( - _convert_aggregation_temporality( - HistogramPoint( - aggregation_temporality=AggregationTemporality.CUMULATIVE, - bucket_counts=[0, 2, 1, 2, 0], - explicit_bounds=[0, 5, 10, 25], - max=24, - min=1, - start_time_unix_nano=0, - sum=70, - time_unix_nano=1, - ), - current_point, - AggregationTemporality.CUMULATIVE, - ), - HistogramPoint( - aggregation_temporality=AggregationTemporality.CUMULATIVE, - bucket_counts=[0, 3, 4, 2, 0], - explicit_bounds=[0, 5, 10, 25], - max=24, - min=1, - start_time_unix_nano=0, - sum=105, - time_unix_nano=2, - ), + explicit_bucket_histogram_aggregation.aggregate(measurement(1)) + second_histogram = explicit_bucket_histogram_aggregation.collect( + AggregationTemporality.DELTA ) - def test_aggregation_temporality_to_delta(self): - current_point = HistogramPoint( - aggregation_temporality=AggregationTemporality.CUMULATIVE, - bucket_counts=[0, 3, 4, 2, 0], - explicit_bounds=[0, 5, 10, 25], - max=22, - min=3, - start_time_unix_nano=0, - sum=105, - time_unix_nano=2, - ) + self.assertEqual(second_histogram.bucket_counts, (0, 1, 0, 0)) + self.assertEqual(second_histogram.sum, 1) - self.assertEqual( - _convert_aggregation_temporality( - HistogramPoint( - aggregation_temporality=AggregationTemporality.CUMULATIVE, - bucket_counts=[0, 2, 1, 2, 0], - explicit_bounds=[0, 5, 10, 25], - max=24, - min=1, - start_time_unix_nano=0, - sum=70, - time_unix_nano=1, - ), - current_point, - AggregationTemporality.DELTA, - ), - HistogramPoint( - aggregation_temporality=AggregationTemporality.DELTA, - bucket_counts=[0, 1, 3, 0, 0], - explicit_bounds=[0, 5, 10, 25], - max=22, - min=3, - start_time_unix_nano=1, - sum=35, - time_unix_nano=2, - ), + self.assertGreater( + second_histogram.time_unix_nano, first_histogram.time_unix_nano ) diff --git a/opentelemetry-sdk/tests/metrics/test_backward_compat.py b/opentelemetry-sdk/tests/metrics/test_backward_compat.py index f9e543db51..b59313c1d9 100644 --- a/opentelemetry-sdk/tests/metrics/test_backward_compat.py +++ b/opentelemetry-sdk/tests/metrics/test_backward_compat.py @@ -100,8 +100,14 @@ def test_observable_callback(self): # produce some data meter_provider.get_meter("foo").create_counter("mycounter").add(12) try: - metrics = reader.get_metrics() + metrics_data = reader.get_metrics_data() except Exception: self.fail() - self.assertEqual(len(metrics), 1) + self.assertEqual(len(metrics_data.resource_metrics), 1) + self.assertEqual( + len(metrics_data.resource_metrics[0].scope_metrics), 1 + ) + self.assertEqual( + len(metrics_data.resource_metrics[0].scope_metrics[0].metrics), 1 + ) diff --git a/opentelemetry-sdk/tests/metrics/test_import.py b/opentelemetry-sdk/tests/metrics/test_import.py index 1a31ab3b88..47aa61be1f 100644 --- a/opentelemetry-sdk/tests/metrics/test_import.py +++ b/opentelemetry-sdk/tests/metrics/test_import.py @@ -46,15 +46,21 @@ def test_import_export(self): from opentelemetry.sdk._metrics.export import ( # noqa: F401 AggregationTemporality, ConsoleMetricExporter, + DataPointT, + DataT, Gauge, Histogram, + HistogramDataPoint, InMemoryMetricReader, Metric, MetricExporter, MetricExportResult, MetricReader, + MetricsData, + NumberDataPoint, PeriodicExportingMetricReader, - PointT, + ResourceMetrics, + ScopeMetrics, Sum, ) except Exception as error: diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py index 15be13ac9a..99e5b66b51 100644 --- a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py @@ -21,10 +21,9 @@ AggregationTemporality, InMemoryMetricReader, Metric, + NumberDataPoint, Sum, ) -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationScope class TestInMemoryMetricReader(TestCase): @@ -32,21 +31,23 @@ def test_no_metrics(self): mock_collect_callback = Mock(return_value=[]) reader = InMemoryMetricReader() reader._set_collect_callback(mock_collect_callback) - self.assertEqual(reader.get_metrics(), []) + self.assertEqual(reader.get_metrics_data(), []) mock_collect_callback.assert_called_once() def test_converts_metrics_to_list(self): metric = Metric( - attributes={"myattr": "baz"}, - description="", - instrumentation_scope=InstrumentationScope("testmetrics"), name="foo", - resource=Resource.create(), + description="", unit="", - point=Sum( - start_time_unix_nano=1647626444152947792, - time_unix_nano=1647626444153163239, - value=72.3309814450449, + data=Sum( + data_points=[ + NumberDataPoint( + attributes={"myattr": "baz"}, + start_time_unix_nano=1647626444152947792, + time_unix_nano=1647626444153163239, + value=72.3309814450449, + ) + ], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), @@ -55,9 +56,9 @@ def test_converts_metrics_to_list(self): reader = InMemoryMetricReader() reader._set_collect_callback(mock_collect_callback) - returned_metrics = reader.get_metrics() + returned_metrics = reader.get_metrics_data() mock_collect_callback.assert_called_once() - self.assertIsInstance(returned_metrics, list) + self.assertIsInstance(returned_metrics, tuple) self.assertEqual(len(returned_metrics), 1) self.assertIs(returned_metrics[0], metric) @@ -76,6 +77,32 @@ def test_integration(self): counter1.add(1, {"foo": "1"}) counter1.add(1, {"foo": "2"}) - metrics = reader.get_metrics() - # should be 3 metrics, one from the observable gauge and one for each labelset from the counter - self.assertEqual(len(metrics), 3) + metrics = reader.get_metrics_data() + # should be 3 number data points, one from the observable gauge and one + # for each labelset from the counter + self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1) + self.assertEqual( + len(metrics.resource_metrics[0].scope_metrics[0].metrics), 2 + ) + self.assertEqual( + len( + list( + metrics.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points + ) + ), + 2, + ) + self.assertEqual( + len( + list( + metrics.resource_metrics[0] + .scope_metrics[0] + .metrics[1] + .data.data_points + ) + ), + 1, + ) diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index cd1a8481ee..33c901de6a 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -21,6 +21,9 @@ ObservableCounter, UpDownCounter, ) +from opentelemetry.sdk._metrics._internal.aggregation import ( + _LastValueAggregation, +) from opentelemetry.sdk._metrics._internal.measurement import Measurement from opentelemetry.sdk._metrics._internal.metric_reader_storage import ( _DEFAULT_VIEW, @@ -106,9 +109,9 @@ def test_creates_view_instrument_matches( def test_forwards_calls_to_view_instrument_match( self, MockViewInstrumentMatch: Mock ): - view_instrument_match1 = Mock() - view_instrument_match2 = Mock() - view_instrument_match3 = Mock() + view_instrument_match1 = Mock(_aggregation=_LastValueAggregation({})) + view_instrument_match2 = Mock(_aggregation=_LastValueAggregation({})) + view_instrument_match3 = Mock(_aggregation=_LastValueAggregation({})) MockViewInstrumentMatch.side_effect = [ view_instrument_match1, view_instrument_match2, @@ -163,7 +166,60 @@ def test_forwards_calls_to_view_instrument_match( view_instrument_match1.collect.assert_called_once() view_instrument_match2.collect.assert_called_once() view_instrument_match3.collect.assert_called_once() - self.assertEqual(result, all_metrics) + self.assertEqual( + ( + result.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + ), + all_metrics[0], + ) + self.assertEqual( + ( + result.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[1] + ), + all_metrics[1], + ) + self.assertEqual( + ( + result.resource_metrics[0] + .scope_metrics[0] + .metrics[1] + .data.data_points[0] + ), + all_metrics[2], + ) + self.assertEqual( + ( + result.resource_metrics[0] + .scope_metrics[0] + .metrics[1] + .data.data_points[1] + ), + all_metrics[3], + ) + self.assertEqual( + ( + result.resource_metrics[0] + .scope_metrics[1] + .metrics[0] + .data.data_points[0] + ), + all_metrics[4], + ) + self.assertEqual( + ( + result.resource_metrics[0] + .scope_metrics[1] + .metrics[0] + .data.data_points[1] + ), + all_metrics[5], + ) @patch( "opentelemetry.sdk._metrics._internal." @@ -256,7 +312,15 @@ def test_drop_aggregation(self): ) metric_reader_storage.consume_measurement(Measurement(1, counter)) - self.assertEqual([], metric_reader_storage.collect()) + self.assertEqual( + [], + ( + metric_reader_storage.collect() + .resource_metrics[0] + .scope_metrics[0] + .metrics + ), + ) def test_conflicting_view_configuration(self): diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index b20dd567a2..66efdb833a 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -506,18 +506,19 @@ def test_duplicate_instrument_aggregate_data(self): metrics = exporter.metrics[0] - self.assertEqual(len(metrics), 2) + scope_metrics = metrics.resource_metrics[0].scope_metrics + self.assertEqual(len(scope_metrics), 2) - metric_0 = metrics[0] + metric_0 = scope_metrics[0].metrics[0] self.assertEqual(metric_0.name, "counter") self.assertEqual(metric_0.unit, "unit") self.assertEqual(metric_0.description, "description") - self.assertEqual(metric_0.point.value, 3) + self.assertEqual(next(metric_0.data.data_points).value, 3) - metric_1 = metrics[1] + metric_1 = scope_metrics[1].metrics[0] self.assertEqual(metric_1.name, "counter") self.assertEqual(metric_1.unit, "unit") self.assertEqual(metric_1.description, "description") - self.assertEqual(metric_1.point.value, 7) + self.assertEqual(next(metric_1.data.data_points).value, 7) diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index 531ef90c95..597bd37259 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -23,10 +23,10 @@ Metric, MetricExporter, MetricExportResult, + NumberDataPoint, PeriodicExportingMetricReader, Sum, ) -from opentelemetry.sdk.resources import Resource from opentelemetry.test.concurrency_test import ConcurrencyTestBase from opentelemetry.util._time import _time_ns @@ -54,29 +54,34 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: metrics_list = [ Metric( name="sum_name", - attributes={}, description="", - instrumentation_scope=None, - resource=Resource.create(), unit="", - point=Sum( - start_time_unix_nano=_time_ns(), - time_unix_nano=_time_ns(), - value=2, + data=Sum( + data_points=[ + NumberDataPoint( + attributes={}, + start_time_unix_nano=_time_ns(), + time_unix_nano=_time_ns(), + value=2, + ) + ], aggregation_temporality=1, is_monotonic=True, ), ), Metric( name="gauge_name", - attributes={}, description="", - instrumentation_scope=None, - resource=Resource.create(), unit="", - point=Gauge( - time_unix_nano=_time_ns(), - value=2, + data=Gauge( + data_points=[ + NumberDataPoint( + attributes={}, + start_time_unix_nano=_time_ns(), + time_unix_nano=_time_ns(), + value=2, + ) + ] ), ), ] diff --git a/opentelemetry-sdk/tests/metrics/test_point.py b/opentelemetry-sdk/tests/metrics/test_point.py index 70b2235f16..7488c85c99 100644 --- a/opentelemetry-sdk/tests/metrics/test_point.py +++ b/opentelemetry-sdk/tests/metrics/test_point.py @@ -14,22 +14,22 @@ from unittest import TestCase -from opentelemetry.sdk._metrics.export import Gauge, Histogram, Metric, Sum -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.sdk._metrics.export import ( + Gauge, + Histogram, + HistogramDataPoint, + Metric, + NumberDataPoint, + Sum, +) -def _create_metric(value): +def _create_metric(data): return Metric( - attributes={"attr-key": "test-val"}, - description="test-description", - instrumentation_scope=InstrumentationScope( - name="name", version="version" - ), name="test-name", - resource=Resource({"resource-key": "resource-val"}), + description="test-description", unit="test-unit", - point=value, + data=data, ) @@ -38,40 +38,62 @@ def test_sum(self): self.maxDiff = None point = _create_metric( Sum( + data_points=[ + NumberDataPoint( + attributes={"attr-key": "test-val"}, + start_time_unix_nano=10, + time_unix_nano=20, + value=9, + ) + ], aggregation_temporality=2, is_monotonic=True, - start_time_unix_nano=10, - time_unix_nano=20, - value=9, ) ) self.assertEqual( - '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_scope": "InstrumentationScope(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"aggregation_temporality": 2, "is_monotonic": true, "start_time_unix_nano": 10, "time_unix_nano": 20, "value": 9}}', + '{"name": "test-name", "description": "test-description", "unit": "test-unit", "data": "{\\"data_points\\": \\"[{\\\\\\"attributes\\\\\\": {\\\\\\"attr-key\\\\\\": \\\\\\"test-val\\\\\\"}, \\\\\\"start_time_unix_nano\\\\\\": 10, \\\\\\"time_unix_nano\\\\\\": 20, \\\\\\"value\\\\\\": 9}]\\", \\"aggregation_temporality\\": 2, \\"is_monotonic\\": true}"}', point.to_json(), ) def test_gauge(self): - point = _create_metric(Gauge(time_unix_nano=40, value=20)) + point = _create_metric( + Gauge( + data_points=[ + NumberDataPoint( + attributes={"attr-key": "test-val"}, + start_time_unix_nano=10, + time_unix_nano=20, + value=9, + ) + ] + ) + ) self.assertEqual( - '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_scope": "InstrumentationScope(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"time_unix_nano": 40, "value": 20}}', + '{"name": "test-name", "description": "test-description", "unit": "test-unit", "data": "{\\"data_points\\": \\"[{\\\\\\"attributes\\\\\\": {\\\\\\"attr-key\\\\\\": \\\\\\"test-val\\\\\\"}, \\\\\\"start_time_unix_nano\\\\\\": 10, \\\\\\"time_unix_nano\\\\\\": 20, \\\\\\"value\\\\\\": 9}]\\"}"}', point.to_json(), ) def test_histogram(self): point = _create_metric( Histogram( + data_points=[ + HistogramDataPoint( + attributes={"attr-key": "test-val"}, + start_time_unix_nano=50, + time_unix_nano=60, + count=1, + sum=0.8, + bucket_counts=[0, 0, 1, 0], + explicit_bounds=[0.1, 0.5, 0.9, 1], + min=0.8, + max=0.8, + ) + ], aggregation_temporality=1, - bucket_counts=[0, 0, 1, 0], - explicit_bounds=[0.1, 0.5, 0.9, 1], - max=0.8, - min=0.8, - start_time_unix_nano=50, - sum=0.8, - time_unix_nano=60, ) ) self.maxDiff = None self.assertEqual( - '{"attributes": {"attr-key": "test-val"}, "description": "test-description", "instrumentation_scope": "InstrumentationScope(name, version, None)", "name": "test-name", "resource": "BoundedAttributes({\'resource-key\': \'resource-val\'}, maxlen=None)", "unit": "test-unit", "point": {"aggregation_temporality": 1, "bucket_counts": [0, 0, 1, 0], "explicit_bounds": [0.1, 0.5, 0.9, 1], "max": 0.8, "min": 0.8, "start_time_unix_nano": 50, "sum": 0.8, "time_unix_nano": 60}}', + '{"name": "test-name", "description": "test-description", "unit": "test-unit", "data": "{\\"data_points\\": \\"[{\\\\\\"attributes\\\\\\": {\\\\\\"attr-key\\\\\\": \\\\\\"test-val\\\\\\"}, \\\\\\"start_time_unix_nano\\\\\\": 50, \\\\\\"time_unix_nano\\\\\\": 60, \\\\\\"count\\\\\\": 1, \\\\\\"sum\\\\\\": 0.8, \\\\\\"bucket_counts\\\\\\": [0, 0, 1, 0], \\\\\\"explicit_bounds\\\\\\": [0.1, 0.5, 0.9, 1], \\\\\\"min\\\\\\": 0.8, \\\\\\"max\\\\\\": 0.8}]\\", \\"aggregation_temporality\\": 1}"}', point.to_json(), ) diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 9f05d2bfee..90e3708604 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -27,7 +27,7 @@ from opentelemetry.sdk._metrics._internal.sdk_configuration import ( SdkConfiguration, ) -from opentelemetry.sdk._metrics.export import AggregationTemporality, Metric +from opentelemetry.sdk._metrics.export import AggregationTemporality from opentelemetry.sdk._metrics.view import ( DefaultAggregation, DropAggregation, @@ -63,12 +63,6 @@ def test_consume_measurement(self): attribute_keys={"a", "c"}, ), instrument=instrument1, - sdk_config=self.sdk_configuration, - instrument_class_temporality=MagicMock( - **{ - "__getitem__.return_value": AggregationTemporality.CUMULATIVE - } - ), instrument_class_aggregation=MagicMock( **{"__getitem__.return_value": DefaultAggregation()} ), @@ -110,12 +104,6 @@ def test_consume_measurement(self): aggregation=self.mock_aggregation_factory, ), instrument=instrument1, - sdk_config=self.sdk_configuration, - instrument_class_temporality=MagicMock( - **{ - "__getitem__.return_value": AggregationTemporality.CUMULATIVE - } - ), instrument_class_aggregation=MagicMock( **{"__getitem__.return_value": DefaultAggregation()} ), @@ -147,12 +135,6 @@ def test_consume_measurement(self): attribute_keys={}, ), instrument=instrument1, - sdk_config=self.sdk_configuration, - instrument_class_temporality=MagicMock( - **{ - "__getitem__.return_value": AggregationTemporality.CUMULATIVE - } - ), instrument_class_aggregation=MagicMock( **{"__getitem__.return_value": DefaultAggregation()} ), @@ -177,12 +159,6 @@ def test_consume_measurement(self): attribute_keys={}, ), instrument=instrument1, - sdk_config=self.sdk_configuration, - instrument_class_temporality=MagicMock( - **{ - "__getitem__.return_value": AggregationTemporality.CUMULATIVE - } - ), instrument_class_aggregation=MagicMock( **{"__getitem__.return_value": DefaultAggregation()} ), @@ -196,24 +172,22 @@ def test_consume_measurement(self): ) def test_collect(self): - instrument1 = Mock( - name="instrument1", description="description", unit="unit" + instrument1 = Counter( + "instrument1", + Mock(), + Mock(), + description="description", + unit="unit", ) instrument1.instrumentation_scope = self.mock_instrumentation_scope view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", - aggregation=self.mock_aggregation_factory, + aggregation=DefaultAggregation(), attribute_keys={"a", "c"}, ), instrument=instrument1, - sdk_config=self.sdk_configuration, - instrument_class_temporality=MagicMock( - **{ - "__getitem__.return_value": AggregationTemporality.CUMULATIVE - } - ), instrument_class_aggregation=MagicMock( **{"__getitem__.return_value": DefaultAggregation()} ), @@ -226,18 +200,17 @@ def test_collect(self): attributes={"c": "d", "f": "g"}, ) ) - self.assertEqual( - next(view_instrument_match.collect()), - Metric( - attributes={"c": "d"}, - description="description", - instrumentation_scope=self.mock_instrumentation_scope, - name="name", - resource=self.mock_resource, - unit="unit", - point=None, - ), + + number_data_points = view_instrument_match.collect( + AggregationTemporality.CUMULATIVE ) + number_data_points = list(number_data_points) + self.assertEqual(len(number_data_points), 1) + + number_data_point = number_data_points[0] + + self.assertEqual(number_data_point.attributes, frozenset({("c", "d")})) + self.assertEqual(number_data_point.value, 0) def test_setting_aggregation(self): instrument1 = Counter( @@ -256,12 +229,6 @@ def test_setting_aggregation(self): attribute_keys={"a", "c"}, ), instrument=instrument1, - sdk_config=self.sdk_configuration, - instrument_class_temporality=MagicMock( - **{ - "__getitem__.return_value": AggregationTemporality.CUMULATIVE - } - ), instrument_class_aggregation={Counter: LastValueAggregation()}, ) diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py index b11e164624..d89b4480a0 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py @@ -13,69 +13,72 @@ # limitations under the License. -from collections import OrderedDict - from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk._metrics.export import ( AggregationTemporality, Gauge, Metric, + NumberDataPoint, Sum, ) -from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.sdk.util.instrumentation import InstrumentationScope def _generate_metric( - name, point, attributes=None, description=None, unit=None + name, data, attributes=None, description=None, unit=None ) -> Metric: - if not attributes: - attributes = BoundedAttributes(attributes={"a": 1, "b": True}) - if not description: + if description is None: description = "foo" - if not unit: + if unit is None: unit = "s" return Metric( - resource=SDKResource(OrderedDict([("a", 1), ("b", False)])), - instrumentation_scope=InstrumentationScope( - "first_name", "first_version" - ), - attributes=attributes, - description=description, name=name, + description=description, unit=unit, - point=point, + data=data, ) def _generate_sum( - name, val, attributes=None, description=None, unit=None + name, value, attributes=None, description=None, unit=None ) -> Sum: + if attributes is None: + attributes = BoundedAttributes(attributes={"a": 1, "b": True}) return _generate_metric( name, Sum( + data_points=[ + NumberDataPoint( + attributes=attributes, + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + value=value, + ) + ], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, - start_time_unix_nano=1641946015139533244, - time_unix_nano=1641946016139533244, - value=val, ), - attributes=attributes, description=description, unit=unit, ) def _generate_gauge( - name, val, attributes=None, description=None, unit=None + name, value, attributes=None, description=None, unit=None ) -> Gauge: + if attributes is None: + attributes = BoundedAttributes(attributes={"a": 1, "b": True}) return _generate_metric( name, Gauge( - time_unix_nano=1641946016139533244, - value=val, + data_points=[ + NumberDataPoint( + attributes=attributes, + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + value=value, + ) + ], ), - attributes=attributes, description=description, unit=unit, ) @@ -87,7 +90,6 @@ def _generate_unsupported_metric( return _generate_metric( name, None, - attributes=attributes, description=description, unit=unit, ) From 91211b3a9ab1a025c55a24c9b28044c428a8483e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 12 May 2022 20:04:33 -0600 Subject: [PATCH 1224/1517] Align collection start times (#2679) * Align collection start times Fixes #2677 * Undo unnecessary change * Use fixed values for more deterministic tests * Align start times * Rename variable * Add time align test case * Remove unnecessary views * Add more test cases --- .../_internal/_view_instrument_match.py | 24 +- .../sdk/_metrics/_internal/aggregation.py | 207 +++++++------ .../_internal/metric_reader_storage.py | 10 +- .../integration_test/test_time_align.py | 289 ++++++++++++++++++ .../tests/metrics/test_aggregation.py | 134 ++++---- .../metrics/test_metric_reader_storage.py | 41 +++ .../metrics/test_view_instrument_match.py | 2 +- 7 files changed, 545 insertions(+), 162 deletions(-) create mode 100644 opentelemetry-sdk/tests/metrics/integration_test/test_time_align.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py index 77a95cbf89..53ddf0a0ab 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py @@ -28,6 +28,7 @@ from opentelemetry.sdk._metrics._internal.measurement import Measurement from opentelemetry.sdk._metrics._internal.point import DataPointT from opentelemetry.sdk._metrics._internal.view import View +from opentelemetry.util._time import _time_ns _logger = getLogger(__name__) @@ -39,6 +40,7 @@ def __init__( instrument: Instrument, instrument_class_aggregation: Dict[type, Aggregation], ): + self._start_time_unix_nano = _time_ns() self._view = view self._instrument = instrument self._attributes_aggregation: Dict[frozenset, _Aggregation] = {} @@ -50,12 +52,12 @@ def __init__( ) if not isinstance(self._view._aggregation, DefaultAggregation): self._aggregation = self._view._aggregation._create_aggregation( - self._instrument, None + self._instrument, None, 0 ) else: self._aggregation = self._instrument_class_aggregation[ self._instrument.__class__ - ]._create_aggregation(self._instrument, None) + ]._create_aggregation(self._instrument, None, 0) def conflicts(self, other: "_ViewInstrumentMatch") -> bool: # pylint: disable=protected-access @@ -103,21 +105,31 @@ def consume_measurement(self, measurement: Measurement) -> None: ): aggregation = ( self._view._aggregation._create_aggregation( - self._instrument, attributes + self._instrument, + attributes, + self._start_time_unix_nano, ) ) else: aggregation = self._instrument_class_aggregation[ self._instrument.__class__ - ]._create_aggregation(self._instrument, attributes) + ]._create_aggregation( + self._instrument, + attributes, + self._start_time_unix_nano, + ) self._attributes_aggregation[attributes] = aggregation self._attributes_aggregation[attributes].aggregate(measurement) def collect( - self, aggregation_temporality: AggregationTemporality + self, + aggregation_temporality: AggregationTemporality, + collection_start_nanos: int, ) -> Iterable[DataPointT]: with self._lock: for aggregation in self._attributes_aggregation.values(): - yield aggregation.collect(aggregation_temporality) + yield aggregation.collect( + aggregation_temporality, collection_start_nanos + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py index 91a5b4a02f..c629eb0da2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py @@ -41,7 +41,6 @@ NumberDataPoint, Sum, ) -from opentelemetry.util._time import _time_ns from opentelemetry.util.types import Attributes _DataPointVarT = TypeVar("_DataPointVarT", NumberDataPoint, HistogramDataPoint) @@ -75,6 +74,7 @@ def aggregate(self, measurement: Measurement) -> None: def collect( self, aggregation_temporality: AggregationTemporality, + collection_start_nano: int, ) -> Optional[_DataPointVarT]: pass @@ -86,92 +86,22 @@ def aggregate(self, measurement: Measurement) -> None: def collect( self, aggregation_temporality: AggregationTemporality, + collection_start_nano: int, ) -> Optional[_DataPointVarT]: pass -class Aggregation(ABC): - """ - Base class for all aggregation types. - """ - - @abstractmethod - def _create_aggregation( - self, instrument: Instrument, attributes: Attributes - ) -> _Aggregation: - """Creates an aggregation""" - - -class DefaultAggregation(Aggregation): - """ - The default aggregation to be used in a `View`. - - This aggregation will create an actual aggregation depending on the - instrument type, as specified next: - - ==================================================== ==================================== - Instrument Aggregation - ==================================================== ==================================== - `opentelemetry.sdk._metrics.Counter` `SumAggregation` - `opentelemetry.sdk._metrics.UpDownCounter` `SumAggregation` - `opentelemetry.sdk._metrics.ObservableCounter` `SumAggregation` - `opentelemetry.sdk._metrics.ObservableUpDownCounter` `SumAggregation` - `opentelemetry.sdk._metrics.Histogram` `ExplicitBucketHistogramAggregation` - `opentelemetry.sdk._metrics.ObservableGauge` `LastValueAggregation` - ==================================================== ==================================== - """ - - def _create_aggregation( - self, instrument: Instrument, attributes: Attributes - ) -> _Aggregation: - - # pylint: disable=too-many-return-statements - if isinstance(instrument, Counter): - return _SumAggregation( - attributes, - instrument_is_monotonic=True, - instrument_temporality=AggregationTemporality.DELTA, - ) - if isinstance(instrument, UpDownCounter): - return _SumAggregation( - attributes, - instrument_is_monotonic=False, - instrument_temporality=AggregationTemporality.DELTA, - ) - - if isinstance(instrument, ObservableCounter): - return _SumAggregation( - attributes, - instrument_is_monotonic=True, - instrument_temporality=AggregationTemporality.CUMULATIVE, - ) - - if isinstance(instrument, ObservableUpDownCounter): - return _SumAggregation( - attributes, - instrument_is_monotonic=False, - instrument_temporality=AggregationTemporality.CUMULATIVE, - ) - - if isinstance(instrument, Histogram): - return _ExplicitBucketHistogramAggregation(attributes) - - if isinstance(instrument, ObservableGauge): - return _LastValueAggregation(attributes) - - raise Exception(f"Invalid instrument type {type(instrument)} found") - - class _SumAggregation(_Aggregation[Sum]): def __init__( self, attributes: Attributes, instrument_is_monotonic: bool, instrument_temporality: AggregationTemporality, + start_time_unix_nano: int, ): super().__init__(attributes) - self._start_time_unix_nano = _time_ns() + self._start_time_unix_nano = start_time_unix_nano self._instrument_temporality = instrument_temporality self._instrument_is_monotonic = instrument_is_monotonic @@ -187,14 +117,14 @@ def aggregate(self, measurement: Measurement) -> None: self._value = self._value + measurement.value def collect( - self, aggregation_temporality: AggregationTemporality + self, + aggregation_temporality: AggregationTemporality, + collection_start_nano: int, ) -> Optional[NumberDataPoint]: """ Atomically return a point for the current value of the metric and reset the aggregation value. """ - now = _time_ns() - if self._instrument_temporality is AggregationTemporality.DELTA: with self._lock: @@ -202,7 +132,7 @@ def collect( start_time_unix_nano = self._start_time_unix_nano self._value = 0 - self._start_time_unix_nano = now + 1 + self._start_time_unix_nano = collection_start_nano else: @@ -216,7 +146,7 @@ def collect( current_point = NumberDataPoint( attributes=self._attributes, start_time_unix_nano=start_time_unix_nano, - time_unix_nano=now, + time_unix_nano=collection_start_nano, value=value, ) @@ -263,6 +193,7 @@ def aggregate(self, measurement: Measurement): def collect( self, aggregation_temporality: AggregationTemporality, + collection_start_nano: int, ) -> Optional[_DataPointVarT]: """ Atomically return a point for the current value of the metric. @@ -276,7 +207,7 @@ def collect( return NumberDataPoint( attributes=self._attributes, start_time_unix_nano=0, - time_unix_nano=_time_ns(), + time_unix_nano=collection_start_nano, value=value, ) @@ -285,6 +216,7 @@ class _ExplicitBucketHistogramAggregation(_Aggregation[HistogramPoint]): def __init__( self, attributes: Attributes, + start_time_unix_nano: int, boundaries: Sequence[float] = ( 0.0, 5.0, @@ -306,7 +238,7 @@ def __init__( self._max = -inf self._sum = 0 self._record_min_max = record_min_max - self._start_time_unix_nano = _time_ns() + self._start_time_unix_nano = start_time_unix_nano # It is assumed that the "natural" aggregation temporality for a # Histogram instrument is DELTA, like the "natural" aggregation # temporality for a Counter is DELTA and the "natural" aggregation @@ -331,12 +263,11 @@ def aggregate(self, measurement: Measurement) -> None: def collect( self, aggregation_temporality: AggregationTemporality, + collection_start_nano: int, ) -> Optional[_DataPointVarT]: """ Atomically return a point for the current value of the metric. """ - now = _time_ns() - with self._lock: if not any(self._bucket_counts): return None @@ -348,7 +279,7 @@ def collect( min_ = self._min self._bucket_counts = self._get_empty_bucket_counts() - self._start_time_unix_nano = now + 1 + self._start_time_unix_nano = collection_start_nano self._sum = 0 self._min = inf self._max = -inf @@ -356,7 +287,7 @@ def collect( current_point = HistogramDataPoint( attributes=self._attributes, start_time_unix_nano=start_time_unix_nano, - time_unix_nano=now, + time_unix_nano=collection_start_nano, count=sum(bucket_counts), sum=sum_, bucket_counts=tuple(bucket_counts), @@ -413,6 +344,90 @@ def collect( return current_point +class Aggregation(ABC): + """ + Base class for all aggregation types. + """ + + @abstractmethod + def _create_aggregation( + self, + instrument: Instrument, + attributes: Attributes, + start_time_unix_nano: int, + ) -> _Aggregation: + """Creates an aggregation""" + + +class DefaultAggregation(Aggregation): + """ + The default aggregation to be used in a `View`. + + This aggregation will create an actual aggregation depending on the + instrument type, as specified next: + + ==================================================== ==================================== + Instrument Aggregation + ==================================================== ==================================== + `opentelemetry.sdk._metrics.Counter` `SumAggregation` + `opentelemetry.sdk._metrics.UpDownCounter` `SumAggregation` + `opentelemetry.sdk._metrics.ObservableCounter` `SumAggregation` + `opentelemetry.sdk._metrics.ObservableUpDownCounter` `SumAggregation` + `opentelemetry.sdk._metrics.Histogram` `ExplicitBucketHistogramAggregation` + `opentelemetry.sdk._metrics.ObservableGauge` `LastValueAggregation` + ==================================================== ==================================== + """ + + def _create_aggregation( + self, + instrument: Instrument, + attributes: Attributes, + start_time_unix_nano: int, + ) -> _Aggregation: + + # pylint: disable=too-many-return-statements + if isinstance(instrument, Counter): + return _SumAggregation( + attributes, + instrument_is_monotonic=True, + instrument_temporality=AggregationTemporality.DELTA, + start_time_unix_nano=start_time_unix_nano, + ) + if isinstance(instrument, UpDownCounter): + return _SumAggregation( + attributes, + instrument_is_monotonic=False, + instrument_temporality=AggregationTemporality.DELTA, + start_time_unix_nano=start_time_unix_nano, + ) + + if isinstance(instrument, ObservableCounter): + return _SumAggregation( + attributes, + instrument_is_monotonic=True, + instrument_temporality=AggregationTemporality.CUMULATIVE, + start_time_unix_nano=start_time_unix_nano, + ) + + if isinstance(instrument, ObservableUpDownCounter): + return _SumAggregation( + attributes, + instrument_is_monotonic=False, + instrument_temporality=AggregationTemporality.CUMULATIVE, + start_time_unix_nano=start_time_unix_nano, + ) + + if isinstance(instrument, Histogram): + return _ExplicitBucketHistogramAggregation( + attributes, start_time_unix_nano + ) + + if isinstance(instrument, ObservableGauge): + return _LastValueAggregation(attributes) + + raise Exception(f"Invalid instrument type {type(instrument)} found") + + class ExplicitBucketHistogramAggregation(Aggregation): """This aggregation informs the SDK to collect: @@ -447,10 +462,14 @@ def __init__( self._record_min_max = record_min_max def _create_aggregation( - self, instrument: Instrument, attributes: Attributes + self, + instrument: Instrument, + attributes: Attributes, + start_time_unix_nano: int, ) -> _Aggregation: return _ExplicitBucketHistogramAggregation( attributes, + start_time_unix_nano, self._boundaries, self._record_min_max, ) @@ -463,7 +482,10 @@ class SumAggregation(Aggregation): """ def _create_aggregation( - self, instrument: Instrument, attributes: Attributes + self, + instrument: Instrument, + attributes: Attributes, + start_time_unix_nano: int, ) -> _Aggregation: temporality = AggregationTemporality.UNSPECIFIED @@ -476,6 +498,7 @@ def _create_aggregation( attributes, isinstance(instrument, (Counter, ObservableCounter)), temporality, + start_time_unix_nano, ) @@ -488,7 +511,10 @@ class LastValueAggregation(Aggregation): """ def _create_aggregation( - self, instrument: Instrument, attributes: Attributes + self, + instrument: Instrument, + attributes: Attributes, + start_time_unix_nano: int, ) -> _Aggregation: return _LastValueAggregation(attributes) @@ -497,6 +523,9 @@ class DropAggregation(Aggregation): """Using this aggregation will make all measurements be ignored.""" def _create_aggregation( - self, instrument: Instrument, attributes: Attributes + self, + instrument: Instrument, + attributes: Attributes, + start_time_unix_nano: int, ) -> _Aggregation: return _DropAggregation(attributes) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py index 9b452379d1..e0397bb729 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py @@ -49,6 +49,7 @@ ) from opentelemetry.sdk._metrics._internal.view import View from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.util._time import _time_ns _logger = getLogger(__name__) @@ -128,6 +129,9 @@ def collect(self) -> MetricsData: # effect is that end times can be slightly skewed among the metric # streams produced by the SDK, but we still align the output timestamps # for a single instrument. + + collection_start_nanos = _time_ns() + with self._lock: instrumentation_scope_scope_metrics: ( @@ -154,7 +158,7 @@ def collect(self) -> MetricsData: data = Sum( aggregation_temporality=aggregation_temporality, data_points=view_instrument_match.collect( - aggregation_temporality + aggregation_temporality, collection_start_nanos ), is_monotonic=isinstance( instrument, (Counter, ObservableCounter) @@ -167,7 +171,7 @@ def collect(self) -> MetricsData: ): data = Gauge( data_points=view_instrument_match.collect( - aggregation_temporality + aggregation_temporality, collection_start_nanos ) ) elif isinstance( @@ -177,7 +181,7 @@ def collect(self) -> MetricsData: ): data = Histogram( data_points=view_instrument_match.collect( - aggregation_temporality + aggregation_temporality, collection_start_nanos ), aggregation_temporality=aggregation_temporality, ) diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_time_align.py b/opentelemetry-sdk/tests/metrics/integration_test/test_time_align.py new file mode 100644 index 0000000000..64b1899414 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_time_align.py @@ -0,0 +1,289 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from platform import system +from time import sleep +from unittest import TestCase + +from pytest import mark + +from opentelemetry.sdk._metrics import Counter, MeterProvider +from opentelemetry.sdk._metrics.export import ( + AggregationTemporality, + InMemoryMetricReader, +) + + +class TestTimeAlign(TestCase): + def test_time_align_cumulative(self): + reader = InMemoryMetricReader() + meter_provider = MeterProvider(metric_readers=[reader]) + + meter = meter_provider.get_meter("testmeter") + + counter_0 = meter.create_counter("counter_0") + counter_1 = meter.create_counter("counter_1") + + counter_0.add(10, {"label": "value1"}) + counter_0.add(10, {"label": "value2"}) + sleep(0.5) + counter_1.add(10, {"label": "value1"}) + counter_1.add(10, {"label": "value2"}) + + metrics = reader.get_metrics_data() + + data_points_0_0 = list( + metrics.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points + ) + data_points_0_1 = list( + metrics.resource_metrics[0] + .scope_metrics[0] + .metrics[1] + .data.data_points + ) + + self.assertEqual( + data_points_0_0[0].start_time_unix_nano, + data_points_0_0[1].start_time_unix_nano, + ) + self.assertEqual( + data_points_0_1[0].start_time_unix_nano, + data_points_0_1[1].start_time_unix_nano, + ) + self.assertNotEqual( + data_points_0_0[1].start_time_unix_nano, + data_points_0_1[0].start_time_unix_nano, + ) + + self.assertEqual( + data_points_0_0[0].time_unix_nano, + data_points_0_0[1].time_unix_nano, + ) + self.assertEqual( + data_points_0_1[0].time_unix_nano, + data_points_0_1[1].time_unix_nano, + ) + self.assertEqual( + data_points_0_0[1].time_unix_nano, + data_points_0_1[0].time_unix_nano, + ) + + counter_0.add(10, {"label": "value1"}) + counter_0.add(10, {"label": "value2"}) + sleep(0.5) + counter_1.add(10, {"label": "value1"}) + counter_1.add(10, {"label": "value2"}) + + metrics = reader.get_metrics_data() + + data_points_1_0 = list( + metrics.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points + ) + data_points_1_1 = list( + metrics.resource_metrics[0] + .scope_metrics[0] + .metrics[1] + .data.data_points + ) + + self.assertEqual( + data_points_1_0[0].start_time_unix_nano, + data_points_1_0[1].start_time_unix_nano, + ) + self.assertEqual( + data_points_1_1[0].start_time_unix_nano, + data_points_1_1[1].start_time_unix_nano, + ) + self.assertNotEqual( + data_points_1_0[1].start_time_unix_nano, + data_points_1_1[0].start_time_unix_nano, + ) + + self.assertEqual( + data_points_1_0[0].time_unix_nano, + data_points_1_0[1].time_unix_nano, + ) + self.assertEqual( + data_points_1_1[0].time_unix_nano, + data_points_1_1[1].time_unix_nano, + ) + self.assertEqual( + data_points_1_0[1].time_unix_nano, + data_points_1_1[0].time_unix_nano, + ) + + self.assertEqual( + data_points_0_0[0].start_time_unix_nano, + data_points_1_0[0].start_time_unix_nano, + ) + self.assertEqual( + data_points_0_0[1].start_time_unix_nano, + data_points_1_0[1].start_time_unix_nano, + ) + self.assertEqual( + data_points_0_1[0].start_time_unix_nano, + data_points_1_1[0].start_time_unix_nano, + ) + self.assertEqual( + data_points_0_1[1].start_time_unix_nano, + data_points_1_1[1].start_time_unix_nano, + ) + + @mark.skipif( + system() != "Linux", reason="test failing in CI when run in Windows" + ) + def test_time_align_delta(self): + reader = InMemoryMetricReader( + preferred_temporality={Counter: AggregationTemporality.DELTA} + ) + meter_provider = MeterProvider(metric_readers=[reader]) + + meter = meter_provider.get_meter("testmeter") + + counter_0 = meter.create_counter("counter_0") + counter_1 = meter.create_counter("counter_1") + + counter_0.add(10, {"label": "value1"}) + counter_0.add(10, {"label": "value2"}) + sleep(0.5) + counter_1.add(10, {"label": "value1"}) + counter_1.add(10, {"label": "value2"}) + + metrics = reader.get_metrics_data() + + data_points_0_0 = list( + metrics.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points + ) + data_points_0_1 = list( + metrics.resource_metrics[0] + .scope_metrics[0] + .metrics[1] + .data.data_points + ) + + self.assertEqual( + data_points_0_0[0].start_time_unix_nano, + data_points_0_0[1].start_time_unix_nano, + ) + self.assertEqual( + data_points_0_1[0].start_time_unix_nano, + data_points_0_1[1].start_time_unix_nano, + ) + self.assertNotEqual( + data_points_0_0[1].start_time_unix_nano, + data_points_0_1[0].start_time_unix_nano, + ) + + self.assertEqual( + data_points_0_0[0].time_unix_nano, + data_points_0_0[1].time_unix_nano, + ) + self.assertEqual( + data_points_0_1[0].time_unix_nano, + data_points_0_1[1].time_unix_nano, + ) + self.assertEqual( + data_points_0_0[1].time_unix_nano, + data_points_0_1[0].time_unix_nano, + ) + + counter_0.add(10, {"label": "value1"}) + counter_0.add(10, {"label": "value2"}) + sleep(0.5) + counter_1.add(10, {"label": "value1"}) + counter_1.add(10, {"label": "value2"}) + + metrics = reader.get_metrics_data() + + data_points_1_0 = list( + metrics.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points + ) + data_points_1_1 = list( + metrics.resource_metrics[0] + .scope_metrics[0] + .metrics[1] + .data.data_points + ) + + self.assertEqual( + data_points_1_0[0].start_time_unix_nano, + data_points_1_0[1].start_time_unix_nano, + ) + self.assertEqual( + data_points_1_1[0].start_time_unix_nano, + data_points_1_1[1].start_time_unix_nano, + ) + self.assertEqual( + data_points_1_0[1].start_time_unix_nano, + data_points_1_1[0].start_time_unix_nano, + ) + + self.assertEqual( + data_points_1_0[0].time_unix_nano, + data_points_1_0[1].time_unix_nano, + ) + self.assertEqual( + data_points_1_1[0].time_unix_nano, + data_points_1_1[1].time_unix_nano, + ) + self.assertEqual( + data_points_1_0[1].time_unix_nano, + data_points_1_1[0].time_unix_nano, + ) + + self.assertNotEqual( + data_points_0_0[0].start_time_unix_nano, + data_points_1_0[0].start_time_unix_nano, + ) + self.assertNotEqual( + data_points_0_0[1].start_time_unix_nano, + data_points_1_0[1].start_time_unix_nano, + ) + self.assertNotEqual( + data_points_0_1[0].start_time_unix_nano, + data_points_1_1[0].start_time_unix_nano, + ) + self.assertNotEqual( + data_points_0_1[1].start_time_unix_nano, + data_points_1_1[1].start_time_unix_nano, + ) + + self.assertEqual( + data_points_0_0[0].time_unix_nano, + data_points_1_0[0].start_time_unix_nano, + ) + self.assertEqual( + data_points_0_0[1].time_unix_nano, + data_points_1_0[1].start_time_unix_nano, + ) + self.assertEqual( + data_points_0_1[0].time_unix_nano, + data_points_1_1[0].start_time_unix_nano, + ) + self.assertEqual( + data_points_0_1[1].time_unix_nano, + data_points_1_1[1].start_time_unix_nano, + ) diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index e71f99d9ee..21c156f700 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -58,7 +58,7 @@ def test_aggregate_delta(self): """ synchronous_sum_aggregation = _SumAggregation( - True, AggregationTemporality.DELTA, Mock() + Mock(), True, AggregationTemporality.DELTA, 0 ) synchronous_sum_aggregation.aggregate(measurement(1)) @@ -68,7 +68,7 @@ def test_aggregate_delta(self): self.assertEqual(synchronous_sum_aggregation._value, 6) synchronous_sum_aggregation = _SumAggregation( - Mock(), True, AggregationTemporality.DELTA + Mock(), True, AggregationTemporality.DELTA, 0 ) synchronous_sum_aggregation.aggregate(measurement(1)) @@ -83,7 +83,7 @@ def test_aggregate_cumulative(self): """ synchronous_sum_aggregation = _SumAggregation( - True, AggregationTemporality.CUMULATIVE, Mock() + Mock(), True, AggregationTemporality.CUMULATIVE, 0 ) synchronous_sum_aggregation.aggregate(measurement(1)) @@ -93,7 +93,7 @@ def test_aggregate_cumulative(self): self.assertEqual(synchronous_sum_aggregation._value, 6) synchronous_sum_aggregation = _SumAggregation( - Mock(), True, AggregationTemporality.CUMULATIVE + Mock(), True, AggregationTemporality.CUMULATIVE, 0 ) synchronous_sum_aggregation.aggregate(measurement(1)) @@ -108,19 +108,23 @@ def test_collect_delta(self): """ synchronous_sum_aggregation = _SumAggregation( - {}, True, AggregationTemporality.DELTA + Mock(), True, AggregationTemporality.DELTA, 0 ) synchronous_sum_aggregation.aggregate(measurement(1)) + # 1 is used here directly to simulate the instant the first + # collection process starts. first_sum = synchronous_sum_aggregation.collect( - AggregationTemporality.CUMULATIVE + AggregationTemporality.CUMULATIVE, 1 ) self.assertEqual(first_sum.value, 1) synchronous_sum_aggregation.aggregate(measurement(1)) + # 2 is used here directly to simulate the instant the first + # collection process starts. second_sum = synchronous_sum_aggregation.collect( - AggregationTemporality.CUMULATIVE + AggregationTemporality.CUMULATIVE, 2 ) self.assertEqual(second_sum.value, 2) @@ -130,19 +134,23 @@ def test_collect_delta(self): ) synchronous_sum_aggregation = _SumAggregation( - {}, True, AggregationTemporality.DELTA + Mock(), True, AggregationTemporality.DELTA, 0 ) synchronous_sum_aggregation.aggregate(measurement(1)) + # 1 is used here directly to simulate the instant the first + # collection process starts. first_sum = synchronous_sum_aggregation.collect( - AggregationTemporality.DELTA + AggregationTemporality.DELTA, 1 ) self.assertEqual(first_sum.value, 1) synchronous_sum_aggregation.aggregate(measurement(1)) + # 2 is used here directly to simulate the instant the first + # collection process starts. second_sum = synchronous_sum_aggregation.collect( - AggregationTemporality.DELTA + AggregationTemporality.DELTA, 2 ) self.assertEqual(second_sum.value, 1) @@ -157,17 +165,21 @@ def test_collect_cumulative(self): """ sum_aggregation = _SumAggregation( - {}, True, AggregationTemporality.CUMULATIVE + Mock(), True, AggregationTemporality.CUMULATIVE, 0 ) sum_aggregation.aggregate(measurement(1)) - first_sum = sum_aggregation.collect(AggregationTemporality.CUMULATIVE) + first_sum = sum_aggregation.collect( + AggregationTemporality.CUMULATIVE, 1 + ) self.assertEqual(first_sum.value, 1) # should have been reset after first collect sum_aggregation.aggregate(measurement(1)) - second_sum = sum_aggregation.collect(AggregationTemporality.CUMULATIVE) + second_sum = sum_aggregation.collect( + AggregationTemporality.CUMULATIVE, 1 + ) self.assertEqual(second_sum.value, 1) @@ -176,7 +188,9 @@ def test_collect_cumulative(self): ) # if no point seen for a whole interval, should return None - third_sum = sum_aggregation.collect(AggregationTemporality.CUMULATIVE) + third_sum = sum_aggregation.collect( + AggregationTemporality.CUMULATIVE, 1 + ) self.assertIsNone(third_sum) @@ -206,12 +220,16 @@ def test_collect(self): last_value_aggregation = _LastValueAggregation(Mock()) self.assertIsNone( - last_value_aggregation.collect(AggregationTemporality.CUMULATIVE) + last_value_aggregation.collect( + AggregationTemporality.CUMULATIVE, 1 + ) ) last_value_aggregation.aggregate(measurement(1)) + # 1 is used here directly to simulate the instant the first + # collection process starts. first_number_data_point = last_value_aggregation.collect( - AggregationTemporality.CUMULATIVE + AggregationTemporality.CUMULATIVE, 1 ) self.assertIsInstance(first_number_data_point, NumberDataPoint) @@ -222,8 +240,10 @@ def test_collect(self): # CI fails the last assertion without this sleep(0.1) + # 2 is used here directly to simulate the instant the second + # collection process starts. second_number_data_point = last_value_aggregation.collect( - AggregationTemporality.CUMULATIVE + AggregationTemporality.CUMULATIVE, 2 ) self.assertEqual(second_number_data_point.value, 1) @@ -233,9 +253,10 @@ def test_collect(self): first_number_data_point.time_unix_nano, ) - # if no observation seen for the interval, it should return None + # 3 is used here directly to simulate the instant the second + # collection process starts. third_number_data_point = last_value_aggregation.collect( - AggregationTemporality.CUMULATIVE + AggregationTemporality.CUMULATIVE, 3 ) self.assertIsNone(third_number_data_point) @@ -247,7 +268,9 @@ def test_aggregate(self): """ explicit_bucket_histogram_aggregation = ( - _ExplicitBucketHistogramAggregation(Mock(), boundaries=[0, 2, 4]) + _ExplicitBucketHistogramAggregation( + Mock(), 0, boundaries=[0, 2, 4] + ) ) explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) @@ -279,7 +302,7 @@ def test_aggregate(self): ) histo = explicit_bucket_histogram_aggregation.collect( - AggregationTemporality.CUMULATIVE + AggregationTemporality.CUMULATIVE, 1 ) self.assertEqual(histo.sum, 14) @@ -290,7 +313,7 @@ def test_min_max(self): """ explicit_bucket_histogram_aggregation = ( - _ExplicitBucketHistogramAggregation(Mock()) + _ExplicitBucketHistogramAggregation(Mock(), 0) ) explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) @@ -303,7 +326,9 @@ def test_min_max(self): self.assertEqual(explicit_bucket_histogram_aggregation._max, 9999) explicit_bucket_histogram_aggregation = ( - _ExplicitBucketHistogramAggregation(Mock(), record_min_max=False) + _ExplicitBucketHistogramAggregation( + Mock(), 0, record_min_max=False + ) ) explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) @@ -321,12 +346,16 @@ def test_collect(self): """ explicit_bucket_histogram_aggregation = ( - _ExplicitBucketHistogramAggregation(Mock(), boundaries=[0, 1, 2]) + _ExplicitBucketHistogramAggregation( + Mock(), 0, boundaries=[0, 1, 2] + ) ) explicit_bucket_histogram_aggregation.aggregate(measurement(1)) + # 1 is used here directly to simulate the instant the first + # collection process starts. first_histogram = explicit_bucket_histogram_aggregation.collect( - AggregationTemporality.CUMULATIVE + AggregationTemporality.CUMULATIVE, 1 ) self.assertEqual(first_histogram.bucket_counts, (0, 1, 0, 0)) @@ -336,8 +365,10 @@ def test_collect(self): sleep(0.1) explicit_bucket_histogram_aggregation.aggregate(measurement(1)) + # 2 is used here directly to simulate the instant the second + # collection process starts. second_histogram = explicit_bucket_histogram_aggregation.collect( - AggregationTemporality.CUMULATIVE + AggregationTemporality.CUMULATIVE, 2 ) self.assertEqual(second_histogram.bucket_counts, (0, 2, 0, 0)) @@ -347,50 +378,23 @@ def test_collect(self): second_histogram.time_unix_nano, first_histogram.time_unix_nano ) - explicit_bucket_histogram_aggregation = ( - _ExplicitBucketHistogramAggregation(Mock(), boundaries=[0, 1, 2]) - ) - - explicit_bucket_histogram_aggregation.aggregate(measurement(1)) - first_histogram = explicit_bucket_histogram_aggregation.collect( - AggregationTemporality.DELTA - ) - - self.assertEqual(first_histogram.bucket_counts, (0, 1, 0, 0)) - self.assertEqual(first_histogram.sum, 1) - - # CI fails the last assertion without this - sleep(0.1) - - explicit_bucket_histogram_aggregation.aggregate(measurement(1)) - second_histogram = explicit_bucket_histogram_aggregation.collect( - AggregationTemporality.DELTA - ) - - self.assertEqual(second_histogram.bucket_counts, (0, 1, 0, 0)) - self.assertEqual(second_histogram.sum, 1) - - self.assertGreater( - second_histogram.time_unix_nano, first_histogram.time_unix_nano - ) - class TestAggregationFactory(TestCase): def test_sum_factory(self): counter = Counter("name", Mock(), Mock()) factory = SumAggregation() - aggregation = factory._create_aggregation(counter, Mock()) + aggregation = factory._create_aggregation(counter, Mock(), 0) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) self.assertEqual( aggregation._instrument_temporality, AggregationTemporality.DELTA ) - aggregation2 = factory._create_aggregation(counter, Mock()) + aggregation2 = factory._create_aggregation(counter, Mock(), 0) self.assertNotEqual(aggregation, aggregation2) counter = UpDownCounter("name", Mock(), Mock()) factory = SumAggregation() - aggregation = factory._create_aggregation(counter, Mock()) + aggregation = factory._create_aggregation(counter, Mock(), 0) self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) self.assertEqual( @@ -399,7 +403,7 @@ def test_sum_factory(self): counter = ObservableCounter("name", Mock(), Mock(), None) factory = SumAggregation() - aggregation = factory._create_aggregation(counter, Mock()) + aggregation = factory._create_aggregation(counter, Mock(), 0) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) self.assertEqual( @@ -416,19 +420,19 @@ def test_explicit_bucket_histogram_factory(self): ), record_min_max=False, ) - aggregation = factory._create_aggregation(histo, Mock()) + aggregation = factory._create_aggregation(histo, Mock(), 0) self.assertIsInstance(aggregation, _ExplicitBucketHistogramAggregation) self.assertFalse(aggregation._record_min_max) self.assertEqual(aggregation._boundaries, (0.0, 5.0)) - aggregation2 = factory._create_aggregation(histo, Mock()) + aggregation2 = factory._create_aggregation(histo, Mock(), 0) self.assertNotEqual(aggregation, aggregation2) def test_last_value_factory(self): counter = Counter("name", Mock(), Mock()) factory = LastValueAggregation() - aggregation = factory._create_aggregation(counter, Mock()) + aggregation = factory._create_aggregation(counter, Mock(), 0) self.assertIsInstance(aggregation, _LastValueAggregation) - aggregation2 = factory._create_aggregation(counter, Mock()) + aggregation2 = factory._create_aggregation(counter, Mock(), 0) self.assertNotEqual(aggregation, aggregation2) @@ -440,7 +444,7 @@ def setUpClass(cls): def test_counter(self): aggregation = self.default_aggregation._create_aggregation( - Counter("name", Mock(), Mock()), Mock() + Counter("name", Mock(), Mock()), Mock(), 0 ) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) @@ -451,7 +455,7 @@ def test_counter(self): def test_up_down_counter(self): aggregation = self.default_aggregation._create_aggregation( - UpDownCounter("name", Mock(), Mock()), Mock() + UpDownCounter("name", Mock(), Mock()), Mock(), 0 ) self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) @@ -464,6 +468,7 @@ def test_observable_counter(self): aggregation = self.default_aggregation._create_aggregation( ObservableCounter("name", Mock(), Mock(), callbacks=[Mock()]), Mock(), + 0, ) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) @@ -479,6 +484,7 @@ def test_observable_up_down_counter(self): "name", Mock(), Mock(), callbacks=[Mock()] ), Mock(), + 0, ) self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) @@ -496,6 +502,7 @@ def test_histogram(self): Mock(), ), Mock(), + 0, ) self.assertIsInstance(aggregation, _ExplicitBucketHistogramAggregation) @@ -509,5 +516,6 @@ def test_observable_gauge(self): callbacks=[Mock()], ), Mock(), + 0, ) self.assertIsInstance(aggregation, _LastValueAggregation) diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index 33c901de6a..d80a491beb 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -322,6 +322,47 @@ def test_drop_aggregation(self): ), ) + def test_same_collection_start(self): + + counter = Counter("name", Mock(), Mock()) + up_down_counter = UpDownCounter("name", Mock(), Mock()) + + metric_reader_storage = MetricReaderStorage( + SdkConfiguration( + resource=Mock(), + metric_readers=(), + views=(View(instrument_name="name"),), + ), + MagicMock( + **{ + "__getitem__.return_value": AggregationTemporality.CUMULATIVE + } + ), + MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), + ) + + metric_reader_storage.consume_measurement(Measurement(1, counter)) + metric_reader_storage.consume_measurement( + Measurement(1, up_down_counter) + ) + + actual = metric_reader_storage.collect() + + self.assertEqual( + list( + actual.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points + )[0].time_unix_nano, + list( + actual.resource_metrics[0] + .scope_metrics[1] + .metrics[0] + .data.data_points + )[0].time_unix_nano, + ) + def test_conflicting_view_configuration(self): observable_counter = ObservableCounter( diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 90e3708604..3031290719 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -202,7 +202,7 @@ def test_collect(self): ) number_data_points = view_instrument_match.collect( - AggregationTemporality.CUMULATIVE + AggregationTemporality.CUMULATIVE, 0 ) number_data_points = list(number_data_points) self.assertEqual(len(number_data_points), 1) From 741389585b5d6d1af808a8939c5348113158f969 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 13 May 2022 20:09:23 -0600 Subject: [PATCH 1225/1517] Make metrics public (#2684) * Make metrics public Fixes #2682 * Make environment variable public * Make environment variable public * Fix example * Make exporter public * Fix environment variable import path * Fix exporter * Fix lint * Fix import order * Add tag * Fix import path --- CHANGELOG.md | 2 + docs/api/metrics.rst | 13 ++---- docs/conf.py | 4 +- docs/examples/metrics/README.rst | 7 +-- docs/examples/metrics/example.py | 14 +++--- docs/getting_started/metrics_example.py | 6 +-- docs/sdk/metrics.export.rst | 6 +-- docs/sdk/metrics.rst | 11 +---- docs/sdk/metrics.view.rst | 6 +-- .../__init__.py | 4 +- .../metrics/test_otlp_metrics_exporter.py | 12 ++--- .../tests/test_otlp.py | 2 +- .../exporter/prometheus/__init__.py | 6 +-- .../tests/test_prometheus_exporter.py | 2 +- .../opentelemetry/environment_variables.py | 2 +- .../{_metrics => metrics}/__init__.py | 9 ++-- .../_internal/__init__.py | 32 ++++++------- .../_internal/instrument.py | 4 +- .../_internal/observation.py | 0 .../src/opentelemetry/util/_providers.py | 2 +- .../tests/metrics/test_instruments.py | 2 +- opentelemetry-api/tests/metrics/test_meter.py | 2 +- .../tests/metrics/test_meter_provider.py | 20 ++++---- .../tests/metrics/test_observation.py | 2 +- .../sdk/environment_variables.py | 2 +- .../sdk/{_metrics => metrics}/__init__.py | 4 +- .../_internal/__init__.py | 40 ++++++++-------- .../_internal/_view_instrument_match.py | 12 ++--- .../_internal/aggregation.py | 22 ++++----- .../_internal/export/__init__.py | 46 +++++++++---------- .../_internal/instrument.py | 24 +++++----- .../_internal/measurement.py | 2 +- .../_internal/measurement_consumer.py | 28 +++++------ .../_internal/metric_reader_storage.py | 16 +++---- .../{_metrics => metrics}/_internal/point.py | 6 +-- .../_internal/sdk_configuration.py | 6 +-- .../{_metrics => metrics}/_internal/view.py | 4 +- .../{_metrics => metrics}/export/__init__.py | 4 +- .../{_metrics => metrics}/view/__init__.py | 4 +- .../metrics/integration_test/test_cpu_time.py | 6 +-- .../test_disable_default_views.py | 6 +-- .../integration_test/test_time_align.py | 4 +- .../tests/metrics/test_aggregation.py | 10 ++-- .../tests/metrics/test_backward_compat.py | 8 ++-- .../tests/metrics/test_import.py | 6 +-- .../metrics/test_in_memory_metric_reader.py | 6 +-- .../tests/metrics/test_instrument.py | 8 ++-- .../metrics/test_measurement_consumer.py | 6 +-- .../tests/metrics/test_metric_reader.py | 16 +++---- .../metrics/test_metric_reader_storage.py | 22 ++++----- .../tests/metrics/test_metrics.py | 28 +++++------ .../test_periodic_exporting_metric_reader.py | 2 +- opentelemetry-sdk/tests/metrics/test_point.py | 2 +- opentelemetry-sdk/tests/metrics/test_view.py | 2 +- .../metrics/test_view_instrument_match.py | 14 +++--- opentelemetry-sdk/tests/test_configurator.py | 2 +- .../src/opentelemetry/test/globals_test.py | 4 +- .../src/opentelemetry/test/metrictestutil.py | 2 +- 58 files changed, 259 insertions(+), 285 deletions(-) rename exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/{_metric_exporter => metric_exporter}/__init__.py (98%) rename opentelemetry-api/src/opentelemetry/{_metrics => metrics}/__init__.py (92%) rename opentelemetry-api/src/opentelemetry/{_metrics => metrics}/_internal/__init__.py (95%) rename opentelemetry-api/src/opentelemetry/{_metrics => metrics}/_internal/instrument.py (98%) rename opentelemetry-api/src/opentelemetry/{_metrics => metrics}/_internal/observation.py (100%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/__init__.py (86%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/_internal/__init__.py (92%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/_internal/_view_instrument_match.py (92%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/_internal/aggregation.py (96%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/_internal/export/__init__.py (90%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/_internal/instrument.py (87%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/_internal/measurement.py (95%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/_internal/measurement_consumer.py (74%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/_internal/metric_reader_storage.py (95%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/_internal/point.py (96%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/_internal/sdk_configuration.py (84%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/_internal/view.py (98%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/export/__init__.py (91%) rename opentelemetry-sdk/src/opentelemetry/sdk/{_metrics => metrics}/view/__init__.py (86%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bfc60619d..f1993e316d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.1-0.30b1...HEAD) +- Make metrics components public + ([#2684](https://github.com/open-telemetry/opentelemetry-python/pull/2684)) - Update to semantic conventions v1.11.0 ([#2669](https://github.com/open-telemetry/opentelemetry-python/pull/2669)) - Update opentelemetry-proto to v0.17.0 diff --git a/docs/api/metrics.rst b/docs/api/metrics.rst index 4fd464ae1d..93a8cbe720 100644 --- a/docs/api/metrics.rst +++ b/docs/api/metrics.rst @@ -1,12 +1,5 @@ -opentelemetry._metrics package -============================== - -.. warning:: - OpenTelemetry Python metrics are in an experimental state. The APIs within - :mod:`opentelemetry._metrics` are subject to change in minor/patch releases and make no - backward compatability guarantees at this time. - - Once metrics become stable, this package will be be renamed to ``opentelemetry.metrics``. +opentelemetry.metrics package +============================= .. toctree:: @@ -14,4 +7,4 @@ opentelemetry._metrics package Module contents --------------- -.. automodule:: opentelemetry._metrics +.. automodule:: opentelemetry.metrics diff --git a/docs/conf.py b/docs/conf.py index 3472ff507c..55b1af7331 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -99,11 +99,11 @@ ("py:class", "ValueT"), ( "py:class", - "opentelemetry.sdk._metrics._internal.instrument._Synchronous", + "opentelemetry.sdk.metrics._internal.instrument._Synchronous", ), ( "py:class", - "opentelemetry.sdk._metrics._internal.instrument._Asynchronous", + "opentelemetry.sdk.metrics._internal.instrument._Asynchronous", ), # Even if wrapt is added to intersphinx_mapping, sphinx keeps failing # with "class reference target not found: ObjectProxy". diff --git a/docs/examples/metrics/README.rst b/docs/examples/metrics/README.rst index 2a9bb70318..50e80a945e 100644 --- a/docs/examples/metrics/README.rst +++ b/docs/examples/metrics/README.rst @@ -1,11 +1,6 @@ OpenTelemetry Metrics SDK ========================= -.. warning:: - OpenTelemetry Python metrics are in an experimental state. The APIs within - :mod:`opentelemetry.sdk._metrics` are subject to change in minor/patch releases and there are no - backward compatability guarantees at this time. - Start the Collector locally to see data being exported. Write the following file: .. code-block:: yaml @@ -45,4 +40,4 @@ The resulting metrics will appear in the output from the collector and look simi .. code-block:: sh -TODO \ No newline at end of file +TODO diff --git a/docs/examples/metrics/example.py b/docs/examples/metrics/example.py index 9843d65953..fde20308a2 100644 --- a/docs/examples/metrics/example.py +++ b/docs/examples/metrics/example.py @@ -1,16 +1,16 @@ from typing import Iterable -from opentelemetry._metrics import ( +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( + OTLPMetricExporter, +) +from opentelemetry.metrics import ( + CallbackOptions, Observation, get_meter_provider, set_meter_provider, ) -from opentelemetry._metrics._internal.instrument import CallbackOptions -from opentelemetry.exporter.otlp.proto.grpc._metric_exporter import ( - OTLPMetricExporter, -) -from opentelemetry.sdk._metrics import MeterProvider -from opentelemetry.sdk._metrics.export import PeriodicExportingMetricReader +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader exporter = OTLPMetricExporter(insecure=True) reader = PeriodicExportingMetricReader(exporter) diff --git a/docs/getting_started/metrics_example.py b/docs/getting_started/metrics_example.py index 32342e7212..83c9a1b8c4 100644 --- a/docs/getting_started/metrics_example.py +++ b/docs/getting_started/metrics_example.py @@ -17,14 +17,14 @@ from typing import Iterable -from opentelemetry._metrics import ( +from opentelemetry.metrics import ( CallbackOptions, Observation, get_meter_provider, set_meter_provider, ) -from opentelemetry.sdk._metrics import MeterProvider -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) diff --git a/docs/sdk/metrics.export.rst b/docs/sdk/metrics.export.rst index 775c0a6611..0c0efaaf91 100644 --- a/docs/sdk/metrics.export.rst +++ b/docs/sdk/metrics.export.rst @@ -1,7 +1,7 @@ -opentelemetry.sdk._metrics.export -================================= +opentelemetry.sdk.metrics.export +================================ -.. automodule:: opentelemetry.sdk._metrics.export +.. automodule:: opentelemetry.sdk.metrics.export :members: :undoc-members: :show-inheritance: diff --git a/docs/sdk/metrics.rst b/docs/sdk/metrics.rst index 1bef70185e..28f33f097c 100644 --- a/docs/sdk/metrics.rst +++ b/docs/sdk/metrics.rst @@ -1,13 +1,6 @@ -opentelemetry.sdk._metrics package +opentelemetry.sdk.metrics package ================================== -.. warning:: - OpenTelemetry Python metrics are in an experimental state. The APIs within - :mod:`opentelemetry.sdk._metrics` are subject to change in minor/patch releases and there are no - backward compatability guarantees at this time. - - Once metrics become stable, this package will be be renamed to ``opentelemetry.sdk.metrics``. - Submodules ---------- @@ -16,7 +9,7 @@ Submodules metrics.export metrics.view -.. automodule:: opentelemetry.sdk._metrics +.. automodule:: opentelemetry.sdk.metrics :members: :undoc-members: :show-inheritance: diff --git a/docs/sdk/metrics.view.rst b/docs/sdk/metrics.view.rst index b18754177a..d7fa96b235 100644 --- a/docs/sdk/metrics.view.rst +++ b/docs/sdk/metrics.view.rst @@ -1,7 +1,7 @@ -opentelemetry.sdk._metrics.view -=============================== +opentelemetry.sdk.metrics.view +============================== -.. automodule:: opentelemetry.sdk._metrics.view +.. automodule:: opentelemetry.sdk.metrics.view :members: :undoc-members: :show-inheritance: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py similarity index 98% rename from exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py rename to exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index 902f683e7b..83846da81f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -30,14 +30,14 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_INSECURE, ) -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.sdk.metrics.export import ( Gauge, Histogram, Metric, Sum, ) -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.sdk.metrics.export import ( MetricExporter, MetricExportResult, MetricsData, diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index 009e1d2de5..0d4418030b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -20,7 +20,7 @@ from google.rpc.error_details_pb2 import RetryInfo from grpc import StatusCode, server -from opentelemetry.exporter.otlp.proto.grpc._metric_exporter import ( +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( OTLPMetricExporter, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( @@ -40,7 +40,10 @@ from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as OTLPResource, ) -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_METRICS_INSECURE, +) +from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Histogram, HistogramDataPoint, @@ -50,9 +53,6 @@ ResourceMetrics, ScopeMetrics, ) -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_METRICS_INSECURE, -) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import ( InstrumentationScope as SDKInstrumentationScope, @@ -303,7 +303,7 @@ def tearDown(self): ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") @patch( - "opentelemetry.exporter.otlp.proto.grpc._metric_exporter.OTLPMetricExporter._stub" + "opentelemetry.exporter.otlp.proto.grpc.metric_exporter.OTLPMetricExporter._stub" ) # pylint: disable=unused-argument def test_no_credentials_error( diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py index 57b3d7b1b1..5b574a9d60 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py @@ -17,7 +17,7 @@ from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter, ) -from opentelemetry.exporter.otlp.proto.grpc._metric_exporter import ( +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( OTLPMetricExporter, ) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 13032e319e..ac1ea58c66 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -29,9 +29,9 @@ from prometheus_client import start_http_server - from opentelemetry._metrics import get_meter_provider, set_meter_provider from opentelemetry.exporter.prometheus import PrometheusMetricReader - from opentelemetry.sdk._metrics import MeterProvider + from opentelemetry.metrics import get_meter_provider, set_meter_provider + from opentelemetry.sdk.metrics import MeterProvider # Start Prometheus client start_http_server(port=8000, addr="localhost") @@ -77,7 +77,7 @@ ) from prometheus_client.core import Metric as PrometheusMetric -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.sdk.metrics.export import ( Gauge, Histogram, HistogramDataPoint, diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 9ad3a69247..4b6118bdab 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -23,7 +23,7 @@ PrometheusMetricReader, _CustomCollector, ) -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Histogram, HistogramDataPoint, diff --git a/opentelemetry-api/src/opentelemetry/environment_variables.py b/opentelemetry-api/src/opentelemetry/environment_variables.py index 0810acc5ff..5c5a9b3c4a 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables.py @@ -49,7 +49,7 @@ .. envvar:: OTEL_PYTHON_TRACER_PROVIDER """ -_OTEL_PYTHON_METER_PROVIDER = "OTEL_PYTHON_METER_PROVIDER" +OTEL_PYTHON_METER_PROVIDER = "OTEL_PYTHON_METER_PROVIDER" """ .. envvar:: OTEL_PYTHON_METER_PROVIDER """ diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py similarity index 92% rename from opentelemetry-api/src/opentelemetry/_metrics/__init__.py rename to opentelemetry-api/src/opentelemetry/metrics/__init__.py index c205adde0b..0de88ccdaa 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -30,15 +30,16 @@ The following code shows how to obtain a meter using the global :class:`.MeterProvider`:: - from opentelemetry._metrics import get_meter + from opentelemetry.metrics import get_meter meter = get_meter("example-meter") counter = meter.create_counter("example-counter") .. versionadded:: 1.10.0 +.. versionchanged:: 1.12.0rc """ -from opentelemetry._metrics._internal import ( +from opentelemetry.metrics._internal import ( Meter, MeterProvider, NoOpMeter, @@ -47,7 +48,7 @@ get_meter_provider, set_meter_provider, ) -from opentelemetry._metrics._internal.instrument import ( +from opentelemetry.metrics._internal.instrument import ( Asynchronous, CallbackOptions, CallbackT, @@ -66,7 +67,7 @@ Synchronous, UpDownCounter, ) -from opentelemetry._metrics._internal.observation import Observation +from opentelemetry.metrics._internal.observation import Observation for obj in [ Counter, diff --git a/opentelemetry-api/src/opentelemetry/_metrics/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py similarity index 95% rename from opentelemetry-api/src/opentelemetry/_metrics/_internal/__init__.py rename to opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py index b2c67af226..f3c37b3856 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py @@ -32,7 +32,7 @@ The following code shows how to obtain a meter using the global :class:`.MeterProvider`:: - from opentelemetry._metrics import get_meter + from opentelemetry.metrics import get_meter meter = get_meter("example-meter") counter = meter.create_counter("example-counter") @@ -47,7 +47,8 @@ from threading import Lock from typing import List, Optional, Sequence, Set, Tuple, Union, cast -from opentelemetry._metrics._internal.instrument import ( +from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER +from opentelemetry.metrics._internal.instrument import ( CallbackT, Counter, Histogram, @@ -68,9 +69,6 @@ _ProxyObservableUpDownCounter, _ProxyUpDownCounter, ) -from opentelemetry.environment_variables import ( - _OTEL_PYTHON_METER_PROVIDER as OTEL_PYTHON_METER_PROVIDER, -) from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider @@ -280,8 +278,8 @@ def create_observable_counter( """Creates an `ObservableCounter` instrument An observable counter observes a monotonically increasing count by calling provided - callbacks which accept a :class:`~opentelemetry._metrics.CallbackOptions` and return - multiple :class:`~opentelemetry._metrics.Observation`. + callbacks which accept a :class:`~opentelemetry.metrics.CallbackOptions` and return + multiple :class:`~opentelemetry.metrics.Observation`. For example, an observable counter could be used to report system CPU time periodically. Here is a basic implementation:: @@ -320,7 +318,7 @@ def cpu_time_callback(options: CallbackOptions) -> Iterable[Observation]: # ... other states Alternatively, you can pass a sequence of generators directly instead of a sequence of - callbacks, which each should return iterables of :class:`~opentelemetry._metrics.Observation`:: + callbacks, which each should return iterables of :class:`~opentelemetry.metrics.Observation`:: def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observation]]: # accept options sent in from OpenTelemetry @@ -347,7 +345,7 @@ def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observat description="CPU time" ) - The :class:`~opentelemetry._metrics.CallbackOptions` contain a timeout which the + The :class:`~opentelemetry.metrics.CallbackOptions` contain a timeout which the callback should respect. For example if the callback does asynchronous work, like making HTTP requests, it should respect the timeout:: @@ -359,8 +357,8 @@ def scrape_http_callback(options: CallbackOptions) -> Iterable[Observation]: Args: name: The name of the instrument to be created callbacks: A sequence of callbacks that return an iterable of - :class:`~opentelemetry._metrics.Observation`. Alternatively, can be a sequence of generators that each - yields iterables of :class:`~opentelemetry._metrics.Observation`. + :class:`~opentelemetry.metrics.Observation`. Alternatively, can be a sequence of generators that each + yields iterables of :class:`~opentelemetry.metrics.Observation`. unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. @@ -373,7 +371,7 @@ def create_histogram( unit: str = "", description: str = "", ) -> Histogram: - """Creates a :class:`~opentelemetry._metrics.Histogram` instrument + """Creates a :class:`~opentelemetry.metrics.Histogram` instrument Args: name: The name of the instrument to be created @@ -395,8 +393,8 @@ def create_observable_gauge( Args: name: The name of the instrument to be created callbacks: A sequence of callbacks that return an iterable of - :class:`~opentelemetry._metrics.Observation`. Alternatively, can be a generator that yields iterables - of :class:`~opentelemetry._metrics.Observation`. + :class:`~opentelemetry.metrics.Observation`. Alternatively, can be a generator that yields iterables + of :class:`~opentelemetry.metrics.Observation`. unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. @@ -415,8 +413,8 @@ def create_observable_up_down_counter( Args: name: The name of the instrument to be created callbacks: A sequence of callbacks that return an iterable of - :class:`~opentelemetry._metrics.Observation`. Alternatively, can be a generator that yields iterables - of :class:`~opentelemetry._metrics.Observation`. + :class:`~opentelemetry.metrics.Observation`. Alternatively, can be a generator that yields iterables + of :class:`~opentelemetry.metrics.Observation`. unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. @@ -725,7 +723,7 @@ def get_meter( """Returns a `Meter` for use by the given instrumentation library. This function is a convenience wrapper for - `opentelemetry._metrics.MeterProvider.get_meter`. + `opentelemetry.metrics.MeterProvider.get_meter`. If meter_provider is omitted the current configured one is used. """ diff --git a/opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py similarity index 98% rename from opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py rename to opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py index 301ba1c392..f98cbd7243 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py @@ -33,8 +33,8 @@ ) # pylint: disable=unused-import; needed for typing and sphinx -from opentelemetry import _metrics as metrics -from opentelemetry._metrics._internal.observation import Observation +from opentelemetry import metrics +from opentelemetry.metrics._internal.observation import Observation from opentelemetry.util.types import Attributes _logger = getLogger(__name__) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/_internal/observation.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/observation.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/_metrics/_internal/observation.py rename to opentelemetry-api/src/opentelemetry/metrics/_internal/observation.py diff --git a/opentelemetry-api/src/opentelemetry/util/_providers.py b/opentelemetry-api/src/opentelemetry/util/_providers.py index f19c32ee86..d8feb88d62 100644 --- a/opentelemetry-api/src/opentelemetry/util/_providers.py +++ b/opentelemetry-api/src/opentelemetry/util/_providers.py @@ -19,7 +19,7 @@ from pkg_resources import iter_entry_points if TYPE_CHECKING: - from opentelemetry._metrics import MeterProvider + from opentelemetry.metrics import MeterProvider from opentelemetry.trace import TracerProvider Provider = TypeVar("Provider", "TracerProvider", "MeterProvider") diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index 42967d4901..ff2ab2b3e5 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -16,7 +16,7 @@ from inspect import Signature, isabstract, signature from unittest import TestCase -from opentelemetry._metrics import ( +from opentelemetry.metrics import ( Counter, Histogram, Instrument, diff --git a/opentelemetry-api/tests/metrics/test_meter.py b/opentelemetry-api/tests/metrics/test_meter.py index 3b7f702dfb..44e81bdc8c 100644 --- a/opentelemetry-api/tests/metrics/test_meter.py +++ b/opentelemetry-api/tests/metrics/test_meter.py @@ -17,7 +17,7 @@ from unittest import TestCase from unittest.mock import Mock -from opentelemetry._metrics import Meter, NoOpMeter +from opentelemetry.metrics import Meter, NoOpMeter # FIXME Test that the meter methods can be called concurrently safely. diff --git a/opentelemetry-api/tests/metrics/test_meter_provider.py b/opentelemetry-api/tests/metrics/test_meter_provider.py index ef49ddaf87..2fa9fe1e73 100644 --- a/opentelemetry-api/tests/metrics/test_meter_provider.py +++ b/opentelemetry-api/tests/metrics/test_meter_provider.py @@ -18,16 +18,17 @@ from pytest import fixture -import opentelemetry._metrics._internal as metrics_internal -from opentelemetry import _metrics as metrics -from opentelemetry._metrics import ( +import opentelemetry.metrics._internal as metrics_internal +from opentelemetry import metrics +from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER +from opentelemetry.metrics import ( NoOpMeter, NoOpMeterProvider, get_meter_provider, set_meter_provider, ) -from opentelemetry._metrics._internal import _ProxyMeter, _ProxyMeterProvider -from opentelemetry._metrics._internal.instrument import ( +from opentelemetry.metrics._internal import _ProxyMeter, _ProxyMeterProvider +from opentelemetry.metrics._internal.instrument import ( _ProxyCounter, _ProxyHistogram, _ProxyObservableCounter, @@ -35,9 +36,6 @@ _ProxyObservableUpDownCounter, _ProxyUpDownCounter, ) -from opentelemetry.environment_variables import ( - _OTEL_PYTHON_METER_PROVIDER as OTEL_PYTHON_METER_PROVIDER, -) from opentelemetry.test.globals_test import ( MetricsGlobalsTest, reset_metrics_globals, @@ -72,7 +70,7 @@ def test_set_meter_provider(reset_meter_provider): def test_set_meter_provider_calls_proxy_provider(reset_meter_provider): with patch( - "opentelemetry._metrics._internal._PROXY_METER_PROVIDER" + "opentelemetry.metrics._internal._PROXY_METER_PROVIDER" ) as mock_proxy_mp: assert metrics_internal._PROXY_METER_PROVIDER is mock_proxy_mp mock_real_mp = Mock() @@ -97,9 +95,9 @@ def test_get_meter_provider(reset_meter_provider): "os.environ", {OTEL_PYTHON_METER_PROVIDER: "test_meter_provider"} ): - with patch("opentelemetry._metrics._internal._load_provider", Mock()): + with patch("opentelemetry.metrics._internal._load_provider", Mock()): with patch( - "opentelemetry._metrics._internal.cast", + "opentelemetry.metrics._internal.cast", Mock(**{"return_value": "test_meter_provider"}), ): assert get_meter_provider() == "test_meter_provider" diff --git a/opentelemetry-api/tests/metrics/test_observation.py b/opentelemetry-api/tests/metrics/test_observation.py index 05c644dbd3..0881f043b7 100644 --- a/opentelemetry-api/tests/metrics/test_observation.py +++ b/opentelemetry-api/tests/metrics/test_observation.py @@ -14,7 +14,7 @@ from unittest import TestCase -from opentelemetry._metrics import Observation +from opentelemetry.metrics import Observation class TestObservation(TestCase): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 3e632bc521..145489a683 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -407,7 +407,7 @@ LogEmitterProvider is used. """ -_OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = ( +OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = ( "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE" ) """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py similarity index 86% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index feecdd2177..d57c3b5896 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -14,11 +14,11 @@ # pylint: disable=unused-import -from opentelemetry.sdk._metrics._internal import ( # noqa: F401 +from opentelemetry.sdk.metrics._internal import ( # noqa: F401 Meter, MeterProvider, ) -from opentelemetry.sdk._metrics._internal.instrument import ( # noqa: F401 +from opentelemetry.sdk.metrics._internal.instrument import ( # noqa: F401 Counter, Histogram, ObservableCounter, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py similarity index 92% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index 1d8c908a0a..c75a175007 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -18,19 +18,19 @@ from typing import Optional, Sequence # This kind of import is needed to avoid Sphinx errors. -import opentelemetry.sdk._metrics -from opentelemetry._metrics import Counter as APICounter -from opentelemetry._metrics import Histogram as APIHistogram -from opentelemetry._metrics import Meter as APIMeter -from opentelemetry._metrics import MeterProvider as APIMeterProvider -from opentelemetry._metrics import NoOpMeter -from opentelemetry._metrics import ObservableCounter as APIObservableCounter -from opentelemetry._metrics import ObservableGauge as APIObservableGauge -from opentelemetry._metrics import ( +import opentelemetry.sdk.metrics +from opentelemetry.metrics import Counter as APICounter +from opentelemetry.metrics import Histogram as APIHistogram +from opentelemetry.metrics import Meter as APIMeter +from opentelemetry.metrics import MeterProvider as APIMeterProvider +from opentelemetry.metrics import NoOpMeter +from opentelemetry.metrics import ObservableCounter as APIObservableCounter +from opentelemetry.metrics import ObservableGauge as APIObservableGauge +from opentelemetry.metrics import ( ObservableUpDownCounter as APIObservableUpDownCounter, ) -from opentelemetry._metrics import UpDownCounter as APIUpDownCounter -from opentelemetry.sdk._metrics._internal.instrument import ( +from opentelemetry.metrics import UpDownCounter as APIUpDownCounter +from opentelemetry.sdk.metrics._internal.instrument import ( Counter, Histogram, ObservableCounter, @@ -38,11 +38,11 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk._metrics._internal.measurement_consumer import ( +from opentelemetry.sdk.metrics._internal.measurement_consumer import ( MeasurementConsumer, SynchronousMeasurementConsumer, ) -from opentelemetry.sdk._metrics._internal.sdk_configuration import ( +from opentelemetry.sdk.metrics._internal.sdk_configuration import ( SdkConfiguration, ) from opentelemetry.sdk.resources import Resource @@ -54,7 +54,7 @@ class Meter(APIMeter): - """See `opentelemetry._metrics.Meter`.""" + """See `opentelemetry.metrics.Meter`.""" def __init__( self, @@ -295,11 +295,11 @@ def create_observable_up_down_counter( class MeterProvider(APIMeterProvider): - r"""See `opentelemetry._metrics.MeterProvider`. + r"""See `opentelemetry.metrics.MeterProvider`. Args: metric_readers: Register metric readers to collect metrics from the SDK - on demand. Each :class:`opentelemetry.sdk._metrics.export.MetricReader` is + on demand. Each :class:`opentelemetry.sdk.metrics.export.MetricReader` is completely independent and will collect separate streams of metrics. TODO: reference ``PeriodicExportingMetricReader`` usage with push exporters here. @@ -308,10 +308,10 @@ class MeterProvider(APIMeterProvider): `MeterProvider.shutdown` views: The views to configure the metric output the SDK - By default, instruments which do not match any :class:`opentelemetry.sdk._metrics.view.View` (or if no :class:`opentelemetry.sdk._metrics.view.View`\ s + By default, instruments which do not match any :class:`opentelemetry.sdk.metrics.view.View` (or if no :class:`opentelemetry.sdk.metrics.view.View`\ s are provided) will report metrics with the default aggregation for the instrument's kind. To disable instruments by default, configure a match-all - :class:`opentelemetry.sdk._metrics.view.View` with `DropAggregation` and then create :class:`opentelemetry.sdk._metrics.view.View`\ s to re-enable + :class:`opentelemetry.sdk.metrics.view.View` with `DropAggregation` and then create :class:`opentelemetry.sdk.metrics.view.View`\ s to re-enable individual instruments: .. code-block:: python @@ -332,11 +332,11 @@ class MeterProvider(APIMeterProvider): def __init__( self, metric_readers: Sequence[ - "opentelemetry.sdk._metrics.export.MetricReader" + "opentelemetry.sdk.metrics.export.MetricReader" ] = (), resource: Resource = Resource.create({}), shutdown_on_exit: bool = True, - views: Sequence["opentelemetry.sdk._metrics.view.View"] = (), + views: Sequence["opentelemetry.sdk.metrics.view.View"] = (), ): self._lock = Lock() self._meter_lock = Lock() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py similarity index 92% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index 53ddf0a0ab..972b39a321 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -17,17 +17,17 @@ from threading import Lock from typing import Dict, Iterable -from opentelemetry._metrics import Instrument -from opentelemetry.sdk._metrics._internal.aggregation import ( +from opentelemetry.metrics import Instrument +from opentelemetry.sdk.metrics._internal.aggregation import ( Aggregation, DefaultAggregation, _Aggregation, _SumAggregation, ) -from opentelemetry.sdk._metrics._internal.export import AggregationTemporality -from opentelemetry.sdk._metrics._internal.measurement import Measurement -from opentelemetry.sdk._metrics._internal.point import DataPointT -from opentelemetry.sdk._metrics._internal.view import View +from opentelemetry.sdk.metrics._internal.export import AggregationTemporality +from opentelemetry.sdk.metrics._internal.measurement import Measurement +from opentelemetry.sdk.metrics._internal.point import DataPointT +from opentelemetry.sdk.metrics._internal.view import View from opentelemetry.util._time import _time_ns _logger = getLogger(__name__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py similarity index 96% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py index c629eb0da2..d7d825c9c5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py @@ -20,7 +20,7 @@ from threading import Lock from typing import Generic, List, Optional, Sequence, TypeVar -from opentelemetry._metrics import ( +from opentelemetry.metrics import ( Asynchronous, Counter, Histogram, @@ -31,12 +31,12 @@ Synchronous, UpDownCounter, ) -from opentelemetry.sdk._metrics._internal.measurement import Measurement -from opentelemetry.sdk._metrics._internal.point import Gauge -from opentelemetry.sdk._metrics._internal.point import ( +from opentelemetry.sdk.metrics._internal.measurement import Measurement +from opentelemetry.sdk.metrics._internal.point import Gauge +from opentelemetry.sdk.metrics._internal.point import ( Histogram as HistogramPoint, ) -from opentelemetry.sdk._metrics._internal.point import ( +from opentelemetry.sdk.metrics._internal.point import ( HistogramDataPoint, NumberDataPoint, Sum, @@ -369,12 +369,12 @@ class DefaultAggregation(Aggregation): ==================================================== ==================================== Instrument Aggregation ==================================================== ==================================== - `opentelemetry.sdk._metrics.Counter` `SumAggregation` - `opentelemetry.sdk._metrics.UpDownCounter` `SumAggregation` - `opentelemetry.sdk._metrics.ObservableCounter` `SumAggregation` - `opentelemetry.sdk._metrics.ObservableUpDownCounter` `SumAggregation` - `opentelemetry.sdk._metrics.Histogram` `ExplicitBucketHistogramAggregation` - `opentelemetry.sdk._metrics.ObservableGauge` `LastValueAggregation` + `opentelemetry.sdk.metrics.Counter` `SumAggregation` + `opentelemetry.sdk.metrics.UpDownCounter` `SumAggregation` + `opentelemetry.sdk.metrics.ObservableCounter` `SumAggregation` + `opentelemetry.sdk.metrics.ObservableUpDownCounter` `SumAggregation` + `opentelemetry.sdk.metrics.Histogram` `ExplicitBucketHistogramAggregation` + `opentelemetry.sdk.metrics.ObservableGauge` `LastValueAggregation` ==================================================== ==================================== """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py similarity index 90% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 1b10d2da66..bcaca4281e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -24,18 +24,21 @@ from typing_extensions import final # This kind of import is needed to avoid Sphinx errors. -import opentelemetry.sdk._metrics._internal +import opentelemetry.sdk.metrics._internal from opentelemetry.context import ( _SUPPRESS_INSTRUMENTATION_KEY, attach, detach, set_value, ) -from opentelemetry.sdk._metrics._internal.aggregation import ( +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, +) +from opentelemetry.sdk.metrics._internal.aggregation import ( AggregationTemporality, DefaultAggregation, ) -from opentelemetry.sdk._metrics._internal.instrument import ( +from opentelemetry.sdk.metrics._internal.instrument import ( Counter, Histogram, ObservableCounter, @@ -43,10 +46,7 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk._metrics._internal.point import MetricsData -from opentelemetry.sdk.environment_variables import ( - _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, -) +from opentelemetry.sdk.metrics._internal.point import MetricsData from opentelemetry.util._once import Once from opentelemetry.util._time import _time_ns @@ -79,7 +79,7 @@ def export( """Exports a batch of telemetry data. Args: - metrics: The list of `opentelemetry.sdk._metrics.export.Metric` objects to be exported + metrics: The list of `opentelemetry.sdk.metrics.export.Metric` objects to be exported Returns: The result of the export @@ -105,7 +105,7 @@ def __init__( self, out: IO = stdout, formatter: Callable[ - ["opentelemetry.sdk._metrics.export.Metric"], str + ["opentelemetry.sdk.metrics.export.Metric"], str ] = lambda metric: metric.to_json() + linesep, ): @@ -168,20 +168,20 @@ def __init__( self, preferred_temporality: Dict[type, AggregationTemporality] = None, preferred_aggregation: Dict[ - type, "opentelemetry.sdk._metrics.view.Aggregation" + type, "opentelemetry.sdk.metrics.view.Aggregation" ] = None, ) -> None: self._collect: Callable[ [ - "opentelemetry.sdk._metrics.export.MetricReader", + "opentelemetry.sdk.metrics.export.MetricReader", AggregationTemporality, ], - Iterable["opentelemetry.sdk._metrics.export.Metric"], + Iterable["opentelemetry.sdk.metrics.export.Metric"], ] = None if ( environ.get( - _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, "CUMULATIVE", ) .upper() @@ -250,10 +250,10 @@ def _set_collect_callback( self, func: Callable[ [ - "opentelemetry.sdk._metrics.export.MetricReader", + "opentelemetry.sdk.metrics.export.MetricReader", AggregationTemporality, ], - Iterable["opentelemetry.sdk._metrics.export.Metric"], + Iterable["opentelemetry.sdk.metrics.export.Metric"], ], ) -> None: """This function is internal to the SDK. It should not be called or overriden by users""" @@ -262,7 +262,7 @@ def _set_collect_callback( @abstractmethod def _receive_metrics( self, - metrics_data: "opentelemetry.sdk._metrics.export.MetricsData", + metrics_data: "opentelemetry.sdk.metrics.export.MetricsData", timeout_millis: float = 10_000, **kwargs, ) -> None: @@ -276,8 +276,8 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: failure status. When a `MetricReader` is registered on a - :class:`~opentelemetry.sdk._metrics.MeterProvider`, - :meth:`~opentelemetry.sdk._metrics.MeterProvider.shutdown` will invoke this + :class:`~opentelemetry.sdk.metrics.MeterProvider`, + :meth:`~opentelemetry.sdk.metrics.MeterProvider.shutdown` will invoke this automatically. """ @@ -292,7 +292,7 @@ def __init__( self, preferred_temporality: Dict[type, AggregationTemporality] = None, preferred_aggregation: Dict[ - type, "opentelemetry.sdk._metrics.view.Aggregation" + type, "opentelemetry.sdk.metrics.view.Aggregation" ] = None, ) -> None: super().__init__( @@ -301,12 +301,12 @@ def __init__( ) self._lock = RLock() self._metrics_data: ( - "opentelemetry.sdk._metrics.export.MetricsData" + "opentelemetry.sdk.metrics.export.MetricsData" ) = None def get_metrics_data( self, - ) -> ("opentelemetry.sdk._metrics.export.MetricsData"): + ) -> ("opentelemetry.sdk.metrics.export.MetricsData"): """Reads and returns current metrics from the SDK""" with self._lock: self.collect() @@ -316,7 +316,7 @@ def get_metrics_data( def _receive_metrics( self, - metrics_data: "opentelemetry.sdk._metrics.export.MetricsData", + metrics_data: "opentelemetry.sdk.metrics.export.MetricsData", timeout_millis: float = 10_000, **kwargs, ) -> None: @@ -338,7 +338,7 @@ def __init__( exporter: MetricExporter, preferred_temporality: Dict[type, AggregationTemporality] = None, preferred_aggregation: Dict[ - type, "opentelemetry.sdk._metrics.view.Aggregation" + type, "opentelemetry.sdk.metrics.view.Aggregation" ] = None, export_interval_millis: Optional[float] = None, export_timeout_millis: Optional[float] = None, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py similarity index 87% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py index be0feb7f86..efef08c67f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py @@ -18,18 +18,18 @@ from typing import Dict, Generator, Iterable, List, Optional, Union # This kind of import is needed to avoid Sphinx errors. -import opentelemetry.sdk._metrics -from opentelemetry._metrics import CallbackT -from opentelemetry._metrics import Counter as APICounter -from opentelemetry._metrics import Histogram as APIHistogram -from opentelemetry._metrics import ObservableCounter as APIObservableCounter -from opentelemetry._metrics import ObservableGauge as APIObservableGauge -from opentelemetry._metrics import ( +import opentelemetry.sdk.metrics +from opentelemetry.metrics import CallbackT +from opentelemetry.metrics import Counter as APICounter +from opentelemetry.metrics import Histogram as APIHistogram +from opentelemetry.metrics import ObservableCounter as APIObservableCounter +from opentelemetry.metrics import ObservableGauge as APIObservableGauge +from opentelemetry.metrics import ( ObservableUpDownCounter as APIObservableUpDownCounter, ) -from opentelemetry._metrics import UpDownCounter as APIUpDownCounter -from opentelemetry._metrics._internal.instrument import CallbackOptions -from opentelemetry.sdk._metrics._internal.measurement import Measurement +from opentelemetry.metrics import UpDownCounter as APIUpDownCounter +from opentelemetry.metrics._internal.instrument import CallbackOptions +from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.util.instrumentation import InstrumentationScope _logger = getLogger(__name__) @@ -45,7 +45,7 @@ def __init__( self, name: str, instrumentation_scope: InstrumentationScope, - measurement_consumer: "opentelemetry.sdk._metrics.MeasurementConsumer", + measurement_consumer: "opentelemetry.sdk.metrics.MeasurementConsumer", unit: str = "", description: str = "", ): @@ -70,7 +70,7 @@ def __init__( self, name: str, instrumentation_scope: InstrumentationScope, - measurement_consumer: "opentelemetry.sdk._metrics.MeasurementConsumer", + measurement_consumer: "opentelemetry.sdk.metrics.MeasurementConsumer", callbacks: Optional[Iterable[CallbackT]] = None, unit: str = "", description: str = "", diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement.py similarity index 95% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement.py index 8a3b807450..0dced5bcd3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement.py @@ -15,7 +15,7 @@ from dataclasses import dataclass from typing import Union -from opentelemetry._metrics import Instrument +from opentelemetry.metrics import Instrument from opentelemetry.util.types import Attributes diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py similarity index 74% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py index e4d3b56bd8..4ed72bca65 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py @@ -19,15 +19,15 @@ from typing import Iterable, List, Mapping # This kind of import is needed to avoid Sphinx errors. -import opentelemetry.sdk._metrics -import opentelemetry.sdk._metrics._internal.instrument -import opentelemetry.sdk._metrics._internal.sdk_configuration -from opentelemetry._metrics._internal.instrument import CallbackOptions -from opentelemetry.sdk._metrics._internal.measurement import Measurement -from opentelemetry.sdk._metrics._internal.metric_reader_storage import ( +import opentelemetry.sdk.metrics +import opentelemetry.sdk.metrics._internal.instrument +import opentelemetry.sdk.metrics._internal.sdk_configuration +from opentelemetry.metrics._internal.instrument import CallbackOptions +from opentelemetry.sdk.metrics._internal.measurement import Measurement +from opentelemetry.sdk.metrics._internal.metric_reader_storage import ( MetricReaderStorage, ) -from opentelemetry.sdk._metrics._internal.point import Metric +from opentelemetry.sdk.metrics._internal.point import Metric class MeasurementConsumer(ABC): @@ -39,7 +39,7 @@ def consume_measurement(self, measurement: Measurement) -> None: def register_asynchronous_instrument( self, instrument: ( - "opentelemetry.sdk._metrics._internal.instrument_Asynchronous" + "opentelemetry.sdk.metrics._internal.instrument_Asynchronous" ), ): pass @@ -47,7 +47,7 @@ def register_asynchronous_instrument( @abstractmethod def collect( self, - metric_reader: "opentelemetry.sdk._metrics.MetricReader", + metric_reader: "opentelemetry.sdk.metrics.MetricReader", ) -> Iterable[Metric]: pass @@ -55,13 +55,13 @@ def collect( class SynchronousMeasurementConsumer(MeasurementConsumer): def __init__( self, - sdk_config: "opentelemetry.sdk._metrics._internal.SdkConfiguration", + sdk_config: "opentelemetry.sdk.metrics._internal.SdkConfiguration", ) -> None: self._lock = Lock() self._sdk_config = sdk_config # should never be mutated self._reader_storages: Mapping[ - "opentelemetry.sdk._metrics.MetricReader", MetricReaderStorage + "opentelemetry.sdk.metrics.MetricReader", MetricReaderStorage ] = { reader: MetricReaderStorage( sdk_config, @@ -71,7 +71,7 @@ def __init__( for reader in sdk_config.metric_readers } self._async_instruments: List[ - "opentelemetry.sdk._metrics._internal.instrument._Asynchronous" + "opentelemetry.sdk.metrics._internal.instrument._Asynchronous" ] = [] def consume_measurement(self, measurement: Measurement) -> None: @@ -81,7 +81,7 @@ def consume_measurement(self, measurement: Measurement) -> None: def register_asynchronous_instrument( self, instrument: ( - "opentelemetry.sdk._metrics._internal.instrument._Asynchronous" + "opentelemetry.sdk.metrics._internal.instrument._Asynchronous" ), ) -> None: with self._lock: @@ -89,7 +89,7 @@ def register_asynchronous_instrument( def collect( self, - metric_reader: "opentelemetry.sdk._metrics.MetricReader", + metric_reader: "opentelemetry.sdk.metrics.MetricReader", ) -> Iterable[Metric]: with self._lock: metric_reader_storage = self._reader_storages[metric_reader] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py similarity index 95% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py index e0397bb729..702eab4a1e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py @@ -16,16 +16,16 @@ from threading import RLock from typing import Dict, List -from opentelemetry._metrics import ( +from opentelemetry.metrics import ( Asynchronous, Counter, Instrument, ObservableCounter, ) -from opentelemetry.sdk._metrics._internal._view_instrument_match import ( +from opentelemetry.sdk.metrics._internal._view_instrument_match import ( _ViewInstrumentMatch, ) -from opentelemetry.sdk._metrics._internal.aggregation import ( +from opentelemetry.sdk.metrics._internal.aggregation import ( Aggregation, ExplicitBucketHistogramAggregation, _DropAggregation, @@ -33,9 +33,9 @@ _LastValueAggregation, _SumAggregation, ) -from opentelemetry.sdk._metrics._internal.export import AggregationTemporality -from opentelemetry.sdk._metrics._internal.measurement import Measurement -from opentelemetry.sdk._metrics._internal.point import ( +from opentelemetry.sdk.metrics._internal.export import AggregationTemporality +from opentelemetry.sdk.metrics._internal.measurement import Measurement +from opentelemetry.sdk.metrics._internal.point import ( Gauge, Histogram, Metric, @@ -44,10 +44,10 @@ ScopeMetrics, Sum, ) -from opentelemetry.sdk._metrics._internal.sdk_configuration import ( +from opentelemetry.sdk.metrics._internal.sdk_configuration import ( SdkConfiguration, ) -from opentelemetry.sdk._metrics._internal.view import View +from opentelemetry.sdk.metrics._internal.view import View from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util._time import _time_ns diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py similarity index 96% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py index 4e57f7a377..4ae6862967 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py @@ -19,7 +19,7 @@ from typing import Sequence, Union # This kind of import is needed to avoid Sphinx errors. -import opentelemetry.sdk._metrics._internal +import opentelemetry.sdk.metrics._internal from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util.types import Attributes @@ -44,7 +44,7 @@ class Sum: data_points: Sequence[NumberDataPoint] aggregation_temporality: ( - "opentelemetry.sdk._metrics.export.AggregationTemporality" + "opentelemetry.sdk.metrics.export.AggregationTemporality" ) is_monotonic: bool @@ -102,7 +102,7 @@ class Histogram: data_points: Sequence[HistogramDataPoint] aggregation_temporality: ( - "opentelemetry.sdk._metrics.export.AggregationTemporality" + "opentelemetry.sdk.metrics.export.AggregationTemporality" ) def to_json(self) -> str: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/sdk_configuration.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/sdk_configuration.py similarity index 84% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/sdk_configuration.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/sdk_configuration.py index 94d7e110a0..9594ab38a7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/sdk_configuration.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/sdk_configuration.py @@ -18,12 +18,12 @@ from typing import Sequence # This kind of import is needed to avoid Sphinx errors. -import opentelemetry.sdk._metrics +import opentelemetry.sdk.metrics import opentelemetry.sdk.resources @dataclass class SdkConfiguration: resource: "opentelemetry.sdk.resources.Resource" - metric_readers: Sequence["opentelemetry.sdk._metrics.MetricReader"] - views: Sequence["opentelemetry.sdk._metrics.View"] + metric_readers: Sequence["opentelemetry.sdk.metrics.MetricReader"] + views: Sequence["opentelemetry.sdk.metrics.View"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py similarity index 98% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/view.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py index 4cdca65220..e4f67f0ff2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/_internal/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py @@ -20,8 +20,8 @@ # FIXME import from typing when support for 3.6 is removed from typing_extensions import final -from opentelemetry._metrics import Instrument -from opentelemetry.sdk._metrics._internal.aggregation import ( +from opentelemetry.metrics import Instrument +from opentelemetry.sdk.metrics._internal.aggregation import ( Aggregation, DefaultAggregation, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py similarity index 91% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index 806755997e..02d3f2aabb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -16,7 +16,7 @@ # FIXME Remove when 3.6 is no longer supported from sys import version_info as _version_info -from opentelemetry.sdk._metrics._internal.export import ( # noqa: F401 +from opentelemetry.sdk.metrics._internal.export import ( # noqa: F401 AggregationTemporality, ConsoleMetricExporter, InMemoryMetricReader, @@ -27,7 +27,7 @@ ) # The point module is not in the export directory to avoid a circular import. -from opentelemetry.sdk._metrics._internal.point import ( # noqa: F401 +from opentelemetry.sdk.metrics._internal.point import ( # noqa: F401 DataPointT, DataT, Gauge, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view/__init__.py similarity index 86% rename from opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view/__init__.py rename to opentelemetry-sdk/src/opentelemetry/sdk/metrics/view/__init__.py index 11218dbb49..b20053d876 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/view/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view/__init__.py @@ -14,7 +14,7 @@ # pylint: disable=unused-import -from opentelemetry.sdk._metrics._internal.aggregation import ( # noqa: F401 +from opentelemetry.sdk.metrics._internal.aggregation import ( # noqa: F401 Aggregation, DefaultAggregation, DropAggregation, @@ -22,7 +22,7 @@ LastValueAggregation, SumAggregation, ) -from opentelemetry.sdk._metrics._internal.view import View # noqa: F401 +from opentelemetry.sdk.metrics._internal.view import View # noqa: F401 __all__ = [] for key, value in globals().copy().items(): diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py index b0c07c975c..7b440c0332 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py @@ -17,9 +17,9 @@ from typing import Generator, Iterable, List from unittest import TestCase -from opentelemetry._metrics import CallbackOptions, Instrument, Observation -from opentelemetry.sdk._metrics import MeterProvider -from opentelemetry.sdk._metrics._internal.measurement import Measurement +from opentelemetry.metrics import CallbackOptions, Instrument, Observation +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics._internal.measurement import Measurement # FIXME Test that the instrument methods can be called concurrently safely. diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py b/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py index cd8d77ef51..ad90fe9a29 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py @@ -14,9 +14,9 @@ from unittest import TestCase -from opentelemetry.sdk._metrics import MeterProvider -from opentelemetry.sdk._metrics.export import InMemoryMetricReader -from opentelemetry.sdk._metrics.view import DropAggregation, View +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import InMemoryMetricReader +from opentelemetry.sdk.metrics.view import DropAggregation, View class TestDisableDefaultViews(TestCase): diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_time_align.py b/opentelemetry-sdk/tests/metrics/integration_test/test_time_align.py index 64b1899414..ad34f5622f 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_time_align.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_time_align.py @@ -18,8 +18,8 @@ from pytest import mark -from opentelemetry.sdk._metrics import Counter, MeterProvider -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, ) diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 21c156f700..b2245c4136 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -18,7 +18,7 @@ from unittest import TestCase from unittest.mock import Mock -from opentelemetry.sdk._metrics import ( +from opentelemetry.sdk.metrics import ( Counter, Histogram, ObservableCounter, @@ -26,17 +26,17 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk._metrics._internal.aggregation import ( +from opentelemetry.sdk.metrics._internal.aggregation import ( _ExplicitBucketHistogramAggregation, _LastValueAggregation, _SumAggregation, ) -from opentelemetry.sdk._metrics._internal.measurement import Measurement -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.sdk.metrics._internal.measurement import Measurement +from opentelemetry.sdk.metrics.export import ( AggregationTemporality, NumberDataPoint, ) -from opentelemetry.sdk._metrics.view import ( +from opentelemetry.sdk.metrics.view import ( DefaultAggregation, ExplicitBucketHistogramAggregation, LastValueAggregation, diff --git a/opentelemetry-sdk/tests/metrics/test_backward_compat.py b/opentelemetry-sdk/tests/metrics/test_backward_compat.py index b59313c1d9..571d7a336a 100644 --- a/opentelemetry-sdk/tests/metrics/test_backward_compat.py +++ b/opentelemetry-sdk/tests/metrics/test_backward_compat.py @@ -28,10 +28,10 @@ from typing import Iterable, Sequence from unittest import TestCase -from opentelemetry._metrics import CallbackOptions, Observation -from opentelemetry.sdk._metrics import MeterProvider -from opentelemetry.sdk._metrics._internal.export import InMemoryMetricReader -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.metrics import CallbackOptions, Observation +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics._internal.export import InMemoryMetricReader +from opentelemetry.sdk.metrics.export import ( Metric, MetricExporter, MetricExportResult, diff --git a/opentelemetry-sdk/tests/metrics/test_import.py b/opentelemetry-sdk/tests/metrics/test_import.py index 47aa61be1f..70afa91497 100644 --- a/opentelemetry-sdk/tests/metrics/test_import.py +++ b/opentelemetry-sdk/tests/metrics/test_import.py @@ -24,7 +24,7 @@ def test_import_init(self): """ try: - from opentelemetry.sdk._metrics import ( # noqa: F401 + from opentelemetry.sdk.metrics import ( # noqa: F401 Counter, Histogram, Meter, @@ -43,7 +43,7 @@ def test_import_export(self): """ try: - from opentelemetry.sdk._metrics.export import ( # noqa: F401 + from opentelemetry.sdk.metrics.export import ( # noqa: F401 AggregationTemporality, ConsoleMetricExporter, DataPointT, @@ -72,7 +72,7 @@ def test_import_view(self): """ try: - from opentelemetry.sdk._metrics.view import ( # noqa: F401 + from opentelemetry.sdk.metrics.view import ( # noqa: F401 Aggregation, DefaultAggregation, DropAggregation, diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py index 99e5b66b51..c32acc7aac 100644 --- a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py @@ -15,9 +15,9 @@ from unittest import TestCase from unittest.mock import Mock -from opentelemetry._metrics import Observation -from opentelemetry.sdk._metrics import MeterProvider -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.metrics import Observation +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, Metric, diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py index 909c44ebd4..7a7bef4efc 100644 --- a/opentelemetry-sdk/tests/metrics/test_instrument.py +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -15,9 +15,9 @@ from unittest import TestCase from unittest.mock import Mock -from opentelemetry._metrics import Observation -from opentelemetry._metrics._internal.instrument import CallbackOptions -from opentelemetry.sdk._metrics import ( +from opentelemetry.metrics import Observation +from opentelemetry.metrics._internal.instrument import CallbackOptions +from opentelemetry.sdk.metrics import ( Counter, Histogram, ObservableCounter, @@ -25,7 +25,7 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk._metrics._internal.measurement import Measurement +from opentelemetry.sdk.metrics._internal.measurement import Measurement class TestCounter(TestCase): diff --git a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py index f508780b52..3a2a60626b 100644 --- a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py +++ b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py @@ -15,17 +15,17 @@ from unittest import TestCase from unittest.mock import MagicMock, Mock, patch -from opentelemetry.sdk._metrics._internal.measurement_consumer import ( +from opentelemetry.sdk.metrics._internal.measurement_consumer import ( MeasurementConsumer, SynchronousMeasurementConsumer, ) -from opentelemetry.sdk._metrics._internal.sdk_configuration import ( +from opentelemetry.sdk.metrics._internal.sdk_configuration import ( SdkConfiguration, ) @patch( - "opentelemetry.sdk._metrics._internal." + "opentelemetry.sdk.metrics._internal." "measurement_consumer.MetricReaderStorage" ) class TestSynchronousMeasurementConsumer(TestCase): diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_metric_reader.py index 714dc1cf96..bc72e8bf98 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader.py @@ -17,7 +17,10 @@ from unittest import TestCase from unittest.mock import patch -from opentelemetry.sdk._metrics import ( +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, +) +from opentelemetry.sdk.metrics import ( Counter, Histogram, ObservableCounter, @@ -25,19 +28,16 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Metric, MetricReader, ) -from opentelemetry.sdk._metrics.view import ( +from opentelemetry.sdk.metrics.view import ( Aggregation, DefaultAggregation, LastValueAggregation, ) -from opentelemetry.sdk.environment_variables import ( - _OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, -) class DummyMetricReader(MetricReader): @@ -66,7 +66,7 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: class TestMetricReader(TestCase): @patch.dict( environ, - {_OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "CUMULATIVE"}, + {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "CUMULATIVE"}, ) def test_configure_temporality_cumulative(self): @@ -91,7 +91,7 @@ def test_configure_temporality_cumulative(self): self.assertEqual(value, AggregationTemporality.CUMULATIVE) @patch.dict( - environ, {_OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"} + environ, {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"} ) def test_configure_temporality_delta(self): diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index d80a491beb..1ab0af6ad6 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -15,25 +15,25 @@ from logging import WARNING from unittest.mock import MagicMock, Mock, patch -from opentelemetry.sdk._metrics import ( +from opentelemetry.sdk.metrics import ( Counter, Histogram, ObservableCounter, UpDownCounter, ) -from opentelemetry.sdk._metrics._internal.aggregation import ( +from opentelemetry.sdk.metrics._internal.aggregation import ( _LastValueAggregation, ) -from opentelemetry.sdk._metrics._internal.measurement import Measurement -from opentelemetry.sdk._metrics._internal.metric_reader_storage import ( +from opentelemetry.sdk.metrics._internal.measurement import Measurement +from opentelemetry.sdk.metrics._internal.metric_reader_storage import ( _DEFAULT_VIEW, MetricReaderStorage, ) -from opentelemetry.sdk._metrics._internal.sdk_configuration import ( +from opentelemetry.sdk.metrics._internal.sdk_configuration import ( SdkConfiguration, ) -from opentelemetry.sdk._metrics.export import AggregationTemporality -from opentelemetry.sdk._metrics.view import ( +from opentelemetry.sdk.metrics.export import AggregationTemporality +from opentelemetry.sdk.metrics.view import ( DefaultAggregation, DropAggregation, ExplicitBucketHistogramAggregation, @@ -57,7 +57,7 @@ def mock_instrument() -> Mock: class TestMetricReaderStorage(ConcurrencyTestBase): @patch( - "opentelemetry.sdk._metrics._internal" + "opentelemetry.sdk.metrics._internal" ".metric_reader_storage._ViewInstrumentMatch" ) def test_creates_view_instrument_matches( @@ -103,7 +103,7 @@ def test_creates_view_instrument_matches( self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 1) @patch( - "opentelemetry.sdk._metrics._internal." + "opentelemetry.sdk.metrics._internal." "metric_reader_storage._ViewInstrumentMatch" ) def test_forwards_calls_to_view_instrument_match( @@ -222,7 +222,7 @@ def test_forwards_calls_to_view_instrument_match( ) @patch( - "opentelemetry.sdk._metrics._internal." + "opentelemetry.sdk.metrics._internal." "metric_reader_storage._ViewInstrumentMatch" ) def test_race_concurrent_measurements(self, MockViewInstrumentMatch: Mock): @@ -255,7 +255,7 @@ def send_measurement(): self.assertEqual(mock_view_instrument_match_ctor.call_count, 1) @patch( - "opentelemetry.sdk._metrics._internal." + "opentelemetry.sdk.metrics._internal." "metric_reader_storage._ViewInstrumentMatch" ) def test_default_view_enabled(self, MockViewInstrumentMatch: Mock): diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 66efdb833a..67950c4aaa 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -19,8 +19,8 @@ from unittest import TestCase from unittest.mock import MagicMock, Mock, patch -from opentelemetry._metrics import NoOpMeter -from opentelemetry.sdk._metrics import ( +from opentelemetry.metrics import NoOpMeter +from opentelemetry.sdk.metrics import ( Counter, Histogram, Meter, @@ -30,14 +30,14 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.sdk.metrics.export import ( Metric, MetricExporter, MetricExportResult, MetricReader, PeriodicExportingMetricReader, ) -from opentelemetry.sdk._metrics.view import SumAggregation, View +from opentelemetry.sdk.metrics.view import SumAggregation, View from opentelemetry.sdk.resources import Resource from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc @@ -221,7 +221,7 @@ def test_shutdown_subsequent_calls(self): with self.assertLogs(level=WARNING): meter_provider.shutdown() - @patch("opentelemetry.sdk._metrics._internal._logger") + @patch("opentelemetry.sdk.metrics._internal._logger") def test_shutdown_race(self, mock_logger): mock_logger.warning = MockFunc() meter_provider = MeterProvider() @@ -232,8 +232,7 @@ def test_shutdown_race(self, mock_logger): self.assertEqual(mock_logger.warning.call_count, num_threads - 1) @patch( - "opentelemetry.sdk._metrics._internal." - "SynchronousMeasurementConsumer" + "opentelemetry.sdk.metrics._internal." "SynchronousMeasurementConsumer" ) def test_measurement_collect_callback( self, mock_sync_measurement_consumer @@ -256,8 +255,7 @@ def test_measurement_collect_callback( ) @patch( - "opentelemetry.sdk._metrics." - "_internal.SynchronousMeasurementConsumer" + "opentelemetry.sdk.metrics." "_internal.SynchronousMeasurementConsumer" ) def test_creates_sync_measurement_consumer( self, mock_sync_measurement_consumer @@ -266,8 +264,7 @@ def test_creates_sync_measurement_consumer( mock_sync_measurement_consumer.assert_called() @patch( - "opentelemetry.sdk._metrics." - "_internal.SynchronousMeasurementConsumer" + "opentelemetry.sdk.metrics." "_internal.SynchronousMeasurementConsumer" ) def test_register_asynchronous_instrument( self, mock_sync_measurement_consumer @@ -292,8 +289,7 @@ def test_register_asynchronous_instrument( ) @patch( - "opentelemetry.sdk._metrics._internal." - "SynchronousMeasurementConsumer" + "opentelemetry.sdk.metrics._internal." "SynchronousMeasurementConsumer" ) def test_consume_measurement_counter(self, mock_sync_measurement_consumer): sync_consumer_instance = mock_sync_measurement_consumer() @@ -305,8 +301,7 @@ def test_consume_measurement_counter(self, mock_sync_measurement_consumer): sync_consumer_instance.consume_measurement.assert_called() @patch( - "opentelemetry.sdk._metrics." - "_internal.SynchronousMeasurementConsumer" + "opentelemetry.sdk.metrics." "_internal.SynchronousMeasurementConsumer" ) def test_consume_measurement_up_down_counter( self, mock_sync_measurement_consumer @@ -322,8 +317,7 @@ def test_consume_measurement_up_down_counter( sync_consumer_instance.consume_measurement.assert_called() @patch( - "opentelemetry.sdk._metrics._internal." - "SynchronousMeasurementConsumer" + "opentelemetry.sdk.metrics._internal." "SynchronousMeasurementConsumer" ) def test_consume_measurement_histogram( self, mock_sync_measurement_consumer diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index 597bd37259..d0f5fc2c45 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -18,7 +18,7 @@ from flaky import flaky -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.sdk.metrics.export import ( Gauge, Metric, MetricExporter, diff --git a/opentelemetry-sdk/tests/metrics/test_point.py b/opentelemetry-sdk/tests/metrics/test_point.py index 7488c85c99..ce3e73b7b0 100644 --- a/opentelemetry-sdk/tests/metrics/test_point.py +++ b/opentelemetry-sdk/tests/metrics/test_point.py @@ -14,7 +14,7 @@ from unittest import TestCase -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.sdk.metrics.export import ( Gauge, Histogram, HistogramDataPoint, diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py index a78e282244..2d1fee490f 100644 --- a/opentelemetry-sdk/tests/metrics/test_view.py +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -15,7 +15,7 @@ from unittest import TestCase from unittest.mock import Mock -from opentelemetry.sdk._metrics.view import View +from opentelemetry.sdk.metrics.view import View class TestView(TestCase): diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 3031290719..4904f74ddd 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -15,20 +15,20 @@ from unittest import TestCase from unittest.mock import MagicMock, Mock -from opentelemetry.sdk._metrics import Counter -from opentelemetry.sdk._metrics._internal._view_instrument_match import ( +from opentelemetry.sdk.metrics import Counter +from opentelemetry.sdk.metrics._internal._view_instrument_match import ( _ViewInstrumentMatch, ) -from opentelemetry.sdk._metrics._internal.aggregation import ( +from opentelemetry.sdk.metrics._internal.aggregation import ( _DropAggregation, _LastValueAggregation, ) -from opentelemetry.sdk._metrics._internal.measurement import Measurement -from opentelemetry.sdk._metrics._internal.sdk_configuration import ( +from opentelemetry.sdk.metrics._internal.measurement import Measurement +from opentelemetry.sdk.metrics._internal.sdk_configuration import ( SdkConfiguration, ) -from opentelemetry.sdk._metrics.export import AggregationTemporality -from opentelemetry.sdk._metrics.view import ( +from opentelemetry.sdk.metrics.export import AggregationTemporality +from opentelemetry.sdk.metrics.view import ( DefaultAggregation, DropAggregation, LastValueAggregation, diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 42d3859f5e..1905e6fc38 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -32,7 +32,7 @@ ) from opentelemetry.sdk._logs import LoggingHandler from opentelemetry.sdk._logs.export import ConsoleLogExporter -from opentelemetry.sdk._metrics.export import ConsoleMetricExporter +from opentelemetry.sdk.metrics.export import ConsoleMetricExporter from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py index e3d2ba731d..19ee5f85ea 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py @@ -15,8 +15,8 @@ import unittest from opentelemetry import trace as trace_api -from opentelemetry._metrics import _internal as metrics_api -from opentelemetry._metrics._internal import _ProxyMeterProvider +from opentelemetry.metrics import _internal as metrics_api +from opentelemetry.metrics._internal import _ProxyMeterProvider from opentelemetry.util._once import Once diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py index d89b4480a0..59b3a45d20 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py @@ -14,7 +14,7 @@ from opentelemetry.attributes import BoundedAttributes -from opentelemetry.sdk._metrics.export import ( +from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Gauge, Metric, From f1ff2a0c61c322585e3eb96c2aef8812613a21e8 Mon Sep 17 00:00:00 2001 From: Gen Xu Date: Mon, 16 May 2022 09:19:52 -0700 Subject: [PATCH 1226/1517] Fix #2689 LoggingHandler to handle exc_info=False (#2690) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/_logs/__init__.py | 2 +- opentelemetry-sdk/tests/logs/test_handler.py | 21 +++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1993e316d..e13fd93901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.1-0.30b1...HEAD) +- Fix LoggingHandler to handle LogRecord with exc_info=False + ([#2690](https://github.com/open-telemetry/opentelemetry-python/pull/2690)) - Make metrics components public ([#2684](https://github.com/open-telemetry/opentelemetry-python/pull/2684)) - Update to semantic conventions v1.11.0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index ae75df1d02..efb074fd0e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -321,7 +321,7 @@ def _get_attributes(record: logging.LogRecord) -> Attributes: attributes = { k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS } - if record.exc_info is not None: + if record.exc_info: exc_type = "" message = "" stack_trace = "" diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 7ea478e844..a5c8c85643 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -107,6 +107,27 @@ def test_log_record_exception(self): self.assertTrue("division by zero" in stack_trace) self.assertTrue(__file__ in stack_trace) + def test_log_exc_info_false(self): + """Exception information will be included in attributes""" + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(log_emitter=emitter_mock) + try: + raise ZeroDivisionError("division by zero") + except ZeroDivisionError: + logger.error("Zero Division Error", exc_info=False) + args, _ = emitter_mock.emit.call_args_list[0] + log_record = args[0] + + self.assertIsNotNone(log_record) + self.assertEqual(log_record.body, "Zero Division Error") + self.assertNotIn(SpanAttributes.EXCEPTION_TYPE, log_record.attributes) + self.assertNotIn( + SpanAttributes.EXCEPTION_MESSAGE, log_record.attributes + ) + self.assertNotIn( + SpanAttributes.EXCEPTION_STACKTRACE, log_record.attributes + ) + def test_log_record_trace_correlation(self): emitter_mock = Mock(spec=LogEmitter) logger = get_logger(log_emitter=emitter_mock) From e45d833bd06b9309ce82f8e6085eeb2b7948edad Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 17 May 2022 13:18:54 -0600 Subject: [PATCH 1227/1517] Release 1.12.0rc1-0.31b0 (#2698) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 6 +++++- eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/setup.cfg | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 31 files changed, 41 insertions(+), 37 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2115e71f9f..fd6261b8d4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 008cd2370dcd3e87cca8c0ddbb0b820681fd7346 + CONTRIB_REPO_SHA: 7b42e4354dc3244ef2878bfd0d7d4c80d25cba0a # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index e13fd93901..5a29ec31f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.11.1-0.30b1...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc1-0.31b0...HEAD) + +## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 + + - Fix LoggingHandler to handle LogRecord with exc_info=False ([#2690](https://github.com/open-telemetry/opentelemetry-python/pull/2690)) diff --git a/eachdist.ini b/eachdist.ini index 7b0af1484f..ebf0088042 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.11.1 +version=1.12.0rc1 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.30b1 +version=0.31b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 2324ecd18a..8307312152 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 2324ecd18a..8307312152 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index a8df93626b..0962901163 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.11.1 - opentelemetry-exporter-jaeger-thrift == 1.11.1 + opentelemetry-exporter-jaeger-proto-grpc == 1.12.0rc1 + opentelemetry-exporter-jaeger-thrift == 1.12.0rc1 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 2324ecd18a..8307312152 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 88015aae34..d8dc1e1ed7 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.30b1" +__version__ = "0.31b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 71f07ea4e4..13d06b2b15 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 - opentelemetry-proto == 1.11.1 + opentelemetry-proto == 1.12.0rc1 backoff >= 1.10.0, < 2.0.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index ded7b2039b..54a13456e7 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index 500c5729e2..cf73900ed2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 - opentelemetry-proto == 1.11.1 + opentelemetry-proto == 1.12.0rc1 backoff >= 1.10.0, < 2.0.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index ded7b2039b..54a13456e7 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 12be1ec1bd..13a7ab92b6 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.11.1 - opentelemetry-exporter-otlp-proto-http == 1.11.1 + opentelemetry-exporter-otlp-proto-grpc == 1.12.0rc1 + opentelemetry-exporter-otlp-proto-http == 1.12.0rc1 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index ded7b2039b..54a13456e7 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 88015aae34..54a13456e7 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.30b1" +__version__ = "1.12.0rc1" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index ded7b2039b..54a13456e7 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 9fbfc74686..a7a35e4c98 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 - opentelemetry-exporter-zipkin-json == 1.11.1 + opentelemetry-exporter-zipkin-json == 1.12.0rc1 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index ded7b2039b..54a13456e7 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 3a8074e83a..98589854ee 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.11.1 - opentelemetry-exporter-zipkin-proto-http == 1.11.1 + opentelemetry-exporter-zipkin-json == 1.12.0rc1 + opentelemetry-exporter-zipkin-proto-http == 1.12.0rc1 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index ded7b2039b..54a13456e7 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index ded7b2039b..54a13456e7 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index ded7b2039b..54a13456e7 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index c25015eae8..0581931d8e 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,8 +43,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.11.1 - opentelemetry-semantic-conventions == 0.30b1 + opentelemetry-api == 1.12.0rc1 + opentelemetry-semantic-conventions == 0.31b0 setuptools >= 16.0 dataclasses == 0.8; python_version < '3.7' typing-extensions >= 3.7.4 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index ded7b2039b..54a13456e7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 88015aae34..d8dc1e1ed7 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.30b1" +__version__ = "0.31b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index ded7b2039b..54a13456e7 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index ded7b2039b..54a13456e7 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0rc1" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 533d206b7b..367586ed83 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -47,7 +47,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test-utils == 0.30b1 + opentelemetry-test-utils == 0.31b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 88015aae34..d8dc1e1ed7 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.30b1" +__version__ = "0.31b0" diff --git a/tests/opentelemetry-test-utils/setup.cfg b/tests/opentelemetry-test-utils/setup.cfg index c99951b27d..4b8f20e118 100644 --- a/tests/opentelemetry-test-utils/setup.cfg +++ b/tests/opentelemetry-test-utils/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.11.1 - opentelemetry-sdk == 1.11.1 + opentelemetry-api == 1.12.0rc1 + opentelemetry-sdk == 1.12.0rc1 asgiref ~= 3.0 [options.extras_require] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 07903a7484..8ae7aaafbe 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.30b1" +__version__ = "0.31b0" From 54e2fef02acc8c7bc1ad8afef42a868aad124f34 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Sat, 21 May 2022 01:51:56 +0530 Subject: [PATCH 1228/1517] Add meter provider and in memory reader to TestBase (#2707) --- .../src/opentelemetry/test/test_base.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py index f176238add..98e820438d 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py @@ -15,13 +15,20 @@ import logging import unittest from contextlib import contextmanager +from typing import Tuple +from opentelemetry import metrics as metrics_api from opentelemetry import trace as trace_api +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import InMemoryMetricReader, MetricReader from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) -from opentelemetry.test.globals_test import reset_trace_globals +from opentelemetry.test.globals_test import ( + reset_metrics_globals, + reset_trace_globals, +) class TestBase(unittest.TestCase): @@ -36,6 +43,11 @@ def setUpClass(cls): reset_trace_globals() trace_api.set_tracer_provider(cls.tracer_provider) + result = cls.create_meter_provider() + cls.meter_provider, cls.memory_metrics_reader = result + reset_metrics_globals() + metrics_api.set_meter_provider(cls.meter_provider) + @classmethod def tearDownClass(cls): # This is done because set_tracer_provider cannot override the @@ -92,6 +104,21 @@ def create_tracer_provider(**kwargs): return tracer_provider, memory_exporter + @staticmethod + def create_meter_provider(**kwargs) -> Tuple[MeterProvider, MetricReader]: + """Helper to create a configured meter provider + Creates a `MeterProvider` and an `InMemoryMetricReader`. + Returns: + A tuple with the meter provider in the first element and the + in-memory metrics exporter in the second + """ + memory_reader = InMemoryMetricReader() + metric_readers = kwargs.get("metric_readers", []) + metric_readers.append(memory_reader) + kwargs["metric_readers"] = metric_readers + meter_provider = MeterProvider(**kwargs) + return meter_provider, memory_reader + @staticmethod @contextmanager def disable_logging(highest_level=logging.CRITICAL): From abd9a5bd29525af969f7ab188e5e8e36ca548c53 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 24 May 2022 18:00:39 +0200 Subject: [PATCH 1229/1517] Document usage of stat_as_current_span as decorator (#2708) --- .../src/opentelemetry/trace/__init__.py | 8 +++ opentelemetry-api/tests/trace/test_tracer.py | 52 +++++++++++++++---- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 53eb0e96db..be0d8933b7 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -371,6 +371,14 @@ def start_as_current_span( with opentelemetry.trace.use_span(span, end_on_exit=True): do_work() + This can also be used as a decorator:: + + @tracer.start_as_current_span("name"): + def function(): + ... + + function() + Args: name: The name of the span to be created. context: An optional Context containing the span's parent. Defaults to the diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py index 774746d41a..a7ad589ae6 100644 --- a/opentelemetry-api/tests/trace/test_tracer.py +++ b/opentelemetry-api/tests/trace/test_tracer.py @@ -12,25 +12,59 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest -from opentelemetry import trace +from contextlib import contextmanager +from unittest import TestCase +from unittest.mock import Mock +from opentelemetry.trace import ( + INVALID_SPAN, + NoOpTracer, + Span, + Tracer, + get_current_span, +) -class TestTracer(unittest.TestCase): + +class TestTracer(TestCase): def setUp(self): - self.tracer = trace.NoOpTracer() + self.tracer = NoOpTracer() def test_start_span(self): with self.tracer.start_span("") as span: - self.assertIsInstance(span, trace.Span) + self.assertIsInstance(span, Span) - def test_start_as_current_span(self): + def test_start_as_current_span_context_manager(self): with self.tracer.start_as_current_span("") as span: - self.assertIsInstance(span, trace.Span) + self.assertIsInstance(span, Span) + + def test_start_as_current_span_decorator(self): + + mock_call = Mock() + + class MockTracer(Tracer): + def start_span(self, *args, **kwargs): + return INVALID_SPAN + + @contextmanager + def start_as_current_span(self, *args, **kwargs): # type: ignore + mock_call() + yield INVALID_SPAN + + mock_tracer = MockTracer() + + @mock_tracer.start_as_current_span("name") + def function(): # type: ignore + pass + + function() # type: ignore + function() # type: ignore + function() # type: ignore + + self.assertEqual(mock_call.call_count, 3) def test_get_current_span(self): with self.tracer.start_as_current_span("test") as span: - trace.get_current_span().set_attribute("test", "test") - self.assertEqual(span, trace.INVALID_SPAN) + get_current_span().set_attribute("test", "test") + self.assertEqual(span, INVALID_SPAN) self.assertFalse(hasattr("span", "attributes")) From 8200f45a1fa1eb56c2b9ad73c12468542fb5184e Mon Sep 17 00:00:00 2001 From: Olivier VERMEULEN Date: Wed, 25 May 2022 12:02:16 +0200 Subject: [PATCH 1230/1517] Fix LogEmitterProvider.force_flush hanging randomly (#2714) Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 3 +++ .../src/opentelemetry/sdk/_logs/export/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a29ec31f0..965e4802c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc1-0.31b0...HEAD) +- Fix LogEmitterProvider.force_flush hanging randomly + ([#2714](https://github.com/open-telemetry/opentelemetry-python/pull/2714)) + ## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py index 87ac308317..def39e32c9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py @@ -178,7 +178,7 @@ def worker(self): flush_request = self._get_and_unset_flush_request() if ( len(self._queue) < self._max_export_batch_size - and self._flush_request is None + and flush_request is None ): self._condition.wait(timeout) From 709afdd89e831780f9f76ade073f397990b8b70c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 25 May 2022 18:48:15 +0200 Subject: [PATCH 1231/1517] Document preferred method for attribute setting (#2712) --- opentelemetry-api/src/opentelemetry/trace/span.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 8846ff50a5..cb9992557c 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -86,7 +86,10 @@ def set_attributes( Sets Attributes with the key and value passed as arguments dict. - Note: The behavior of `None` value attributes is undefined, and hence strongly discouraged. + Note: The behavior of `None` value attributes is undefined, and hence + strongly discouraged. It is also preferred to set attributes at span + creation, instead of calling this method later since samplers can only + consider information already present during span creation. """ @abc.abstractmethod @@ -95,7 +98,10 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: Sets a single Attribute with the key and value passed as arguments. - Note: The behavior of `None` value attributes is undefined, and hence strongly discouraged. + Note: The behavior of `None` value attributes is undefined, and hence + strongly discouraged. It is also preferred to set attributes at span + creation, instead of calling this method later since samplers can only + consider information already present during span creation. """ @abc.abstractmethod From 77b47993f86bb9a9f9dde6b4f3a33cfd43658128 Mon Sep 17 00:00:00 2001 From: "Paul \"TBBle\" Hampson" Date: Fri, 27 May 2022 06:05:03 +1000 Subject: [PATCH 1232/1517] narrow protobuf dependencies to exclude protobuf >= 4 (#2720) --- CHANGELOG.md | 2 ++ exporter/opentelemetry-exporter-opencensus/setup.cfg | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- opentelemetry-proto/setup.cfg | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 965e4802c6..e6f75e7b96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix LogEmitterProvider.force_flush hanging randomly ([#2714](https://github.com/open-telemetry/opentelemetry-python/pull/2714)) +- narrow protobuf dependencies to exclude protobuf >= 4 + ([#2720](https://github.com/open-telemetry/opentelemetry-python/pull/2720)) ## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index f770c1e74d..dd984a2346 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -45,7 +45,7 @@ install_requires = opencensus-proto >= 0.1.0, < 1.0.0 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - protobuf >= 3.13.0 + protobuf ~= 3.13 setuptools >= 16.0 [options.packages.find] diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index a7a35e4c98..70cbd52b13 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -41,7 +41,7 @@ package_dir= =src packages=find_namespace: install_requires = - protobuf >= 3.12 + protobuf ~= 3.12 requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 diff --git a/opentelemetry-proto/setup.cfg b/opentelemetry-proto/setup.cfg index 6f19ae8fd3..94ec991771 100644 --- a/opentelemetry-proto/setup.cfg +++ b/opentelemetry-proto/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - protobuf>=3.13.0 + protobuf~=3.13 [options.packages.find] where = src From eed40f7ef5206d6c0503b7408847ec3c002388c7 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 27 May 2022 00:29:01 +0200 Subject: [PATCH 1233/1517] Fix link in README (#2715) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 550e08936d..f5620db91c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ---

- Getting Started + Getting Started   •   API Documentation   •   From cad776a2031c84fb3c3a1af90ee2a939f3394b9a Mon Sep 17 00:00:00 2001 From: Alan Isaac Date: Fri, 27 May 2022 01:41:13 -0400 Subject: [PATCH 1234/1517] Added generic to textmap getter and setter (#2657) * added generic to textmap getter and setter * added CHANGELOG entry Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 ++ .../baggage/propagation/__init__.py | 4 ++-- .../src/opentelemetry/propagate/__init__.py | 4 ++-- .../opentelemetry/propagators/composite.py | 4 ++-- .../src/opentelemetry/propagators/textmap.py | 24 +++++++++---------- .../trace/propagation/tracecontext.py | 4 ++-- 6 files changed, 22 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6f75e7b96..6604cccea1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc1-0.31b0...HEAD) +- Fix type hints for textmap `Getter` and `Setter` + ([#2657](https://github.com/open-telemetry/opentelemetry-python/pull/2657)) - Fix LogEmitterProvider.force_flush hanging randomly ([#2714](https://github.com/open-telemetry/opentelemetry-python/pull/2714)) - narrow protobuf dependencies to exclude protobuf >= 4 diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index d194acfe2e..edba837b81 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -38,7 +38,7 @@ def extract( self, carrier: textmap.CarrierT, context: Optional[Context] = None, - getter: textmap.Getter = textmap.default_getter, + getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter, ) -> Context: """Extract Baggage from the carrier. @@ -109,7 +109,7 @@ def inject( self, carrier: textmap.CarrierT, context: Optional[Context] = None, - setter: textmap.Setter = textmap.default_setter, + setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter, ) -> None: """Injects Baggage into the carrier. diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index 5493c5f088..f197f1f914 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -84,7 +84,7 @@ def example_route(): def extract( carrier: textmap.CarrierT, context: typing.Optional[Context] = None, - getter: textmap.Getter = textmap.default_getter, + getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter, ) -> Context: """Uses the configured propagator to extract a Context from the carrier. @@ -105,7 +105,7 @@ def extract( def inject( carrier: textmap.CarrierT, context: typing.Optional[Context] = None, - setter: textmap.Setter = textmap.default_setter, + setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter, ) -> None: """Uses the configured propagator to inject a Context into the carrier. diff --git a/opentelemetry-api/src/opentelemetry/propagators/composite.py b/opentelemetry-api/src/opentelemetry/propagators/composite.py index b06e385b58..77330d9410 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/composite.py +++ b/opentelemetry-api/src/opentelemetry/propagators/composite.py @@ -39,7 +39,7 @@ def extract( self, carrier: textmap.CarrierT, context: typing.Optional[Context] = None, - getter: textmap.Getter = textmap.default_getter, + getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter, ) -> Context: """Run each of the configured propagators with the given context and carrier. Propagators are run in the order they are configured, if multiple @@ -56,7 +56,7 @@ def inject( self, carrier: textmap.CarrierT, context: typing.Optional[Context] = None, - setter: textmap.Setter = textmap.default_setter, + setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter, ) -> None: """Run each of the configured propagators with the given context and carrier. Propagators are run in the order they are configured, if multiple diff --git a/opentelemetry-api/src/opentelemetry/propagators/textmap.py b/opentelemetry-api/src/opentelemetry/propagators/textmap.py index 0011315cf2..afadd35000 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/textmap.py +++ b/opentelemetry-api/src/opentelemetry/propagators/textmap.py @@ -21,7 +21,7 @@ CarrierValT = typing.Union[typing.List[str], str] -class Getter(abc.ABC): +class Getter(abc.ABC, typing.Generic[CarrierT]): """This class implements a Getter that enables extracting propagated fields from a carrier. """ @@ -54,7 +54,7 @@ def keys(self, carrier: CarrierT) -> typing.List[str]: """ -class Setter(abc.ABC): +class Setter(abc.ABC, typing.Generic[CarrierT]): """This class implements a Setter that enables injecting propagated fields into a carrier. """ @@ -71,8 +71,8 @@ def set(self, carrier: CarrierT, key: str, value: str) -> None: """ -class DefaultGetter(Getter): - def get( # type: ignore +class DefaultGetter(Getter[typing.Mapping[str, CarrierValT]]): + def get( self, carrier: typing.Mapping[str, CarrierValT], key: str ) -> typing.Optional[typing.List[str]]: """Getter implementation to retrieve a value from a dictionary. @@ -90,18 +90,18 @@ def get( # type: ignore return list(val) return [val] - def keys( # type: ignore - self, carrier: typing.Dict[str, CarrierValT] + def keys( + self, carrier: typing.Mapping[str, CarrierValT] ) -> typing.List[str]: """Keys implementation that returns all keys from a dictionary.""" return list(carrier.keys()) -default_getter = DefaultGetter() +default_getter: Getter[CarrierT] = DefaultGetter() # type: ignore -class DefaultSetter(Setter): - def set( # type: ignore +class DefaultSetter(Setter[typing.MutableMapping[str, CarrierValT]]): + def set( self, carrier: typing.MutableMapping[str, CarrierValT], key: str, @@ -117,7 +117,7 @@ def set( # type: ignore carrier[key] = value -default_setter = DefaultSetter() +default_setter: Setter[CarrierT] = DefaultSetter() # type: ignore class TextMapPropagator(abc.ABC): @@ -134,7 +134,7 @@ def extract( self, carrier: CarrierT, context: typing.Optional[Context] = None, - getter: Getter = default_getter, + getter: Getter[CarrierT] = default_getter, ) -> Context: """Create a Context from values in the carrier. @@ -162,7 +162,7 @@ def inject( self, carrier: CarrierT, context: typing.Optional[Context] = None, - setter: Setter = default_setter, + setter: Setter[CarrierT] = default_setter, ) -> None: """Inject values from a Context into a carrier. diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index 82cc078efc..af16a08f0b 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -37,7 +37,7 @@ def extract( self, carrier: textmap.CarrierT, context: typing.Optional[Context] = None, - getter: textmap.Getter = textmap.default_getter, + getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter, ) -> Context: """Extracts SpanContext from the carrier. @@ -90,7 +90,7 @@ def inject( self, carrier: textmap.CarrierT, context: typing.Optional[Context] = None, - setter: textmap.Setter = textmap.default_setter, + setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter, ) -> None: """Injects SpanContext into the carrier. From 0bfa9a64d51d597a506ae92ddb6a1c764fa3812c Mon Sep 17 00:00:00 2001 From: Hannes Ljungberg Date: Tue, 31 May 2022 19:20:04 +0200 Subject: [PATCH 1235/1517] Loosen dependency on backoff for newer Python versions (#2726) --- CHANGELOG.md | 2 ++ exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 3 ++- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6604cccea1..ea2027a09c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2714](https://github.com/open-telemetry/opentelemetry-python/pull/2714)) - narrow protobuf dependencies to exclude protobuf >= 4 ([#2720](https://github.com/open-telemetry/opentelemetry-python/pull/2720)) +- Loosen dependency on `backoff` for newer Python versions + ([#2726](https://github.com/open-telemetry/opentelemetry-python/pull/2726)) ## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 13d06b2b15..4206782846 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -45,7 +45,8 @@ install_requires = opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 opentelemetry-proto == 1.12.0rc1 - backoff >= 1.10.0, < 2.0.0 + backoff >= 1.10.0, < 2.0.0; python_version<'3.7' + backoff >= 1.10.0, < 3.0.0; python_version>='3.7' [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index cf73900ed2..e434fb794e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -45,7 +45,8 @@ install_requires = opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 opentelemetry-proto == 1.12.0rc1 - backoff >= 1.10.0, < 2.0.0 + backoff >= 1.10.0, < 2.0.0; python_version<'3.7' + backoff >= 1.10.0, < 3.0.0; python_version>='3.7' [options.extras_require] test = From 1dd18556dfe1089d04c417adeddfdd3b18e6d67e Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 1 Jun 2022 23:58:01 +0530 Subject: [PATCH 1236/1517] fix: `frozenset` object has no attribute `items` (#2727) --- CHANGELOG.md | 4 ++-- .../sdk/metrics/_internal/_view_instrument_match.py | 10 +++++----- .../tests/metrics/test_view_instrument_match.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea2027a09c..fbedb9a39f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,11 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2720](https://github.com/open-telemetry/opentelemetry-python/pull/2720)) - Loosen dependency on `backoff` for newer Python versions ([#2726](https://github.com/open-telemetry/opentelemetry-python/pull/2726)) +- fix: frozenset object has no attribute items + ([#2727](https://github.com/open-telemetry/opentelemetry-python/pull/2727)) ## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 - - - Fix LoggingHandler to handle LogRecord with exc_info=False ([#2690](https://github.com/open-telemetry/opentelemetry-python/pull/2690)) - Make metrics components public diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index 972b39a321..026b970235 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -95,11 +95,11 @@ def consume_measurement(self, measurement: Measurement) -> None: else: attributes = {} - attributes = frozenset(attributes.items()) + aggr_key = frozenset(attributes.items()) - if attributes not in self._attributes_aggregation: + if aggr_key not in self._attributes_aggregation: with self._lock: - if attributes not in self._attributes_aggregation: + if aggr_key not in self._attributes_aggregation: if not isinstance( self._view._aggregation, DefaultAggregation ): @@ -118,9 +118,9 @@ def consume_measurement(self, measurement: Measurement) -> None: attributes, self._start_time_unix_nano, ) - self._attributes_aggregation[attributes] = aggregation + self._attributes_aggregation[aggr_key] = aggregation - self._attributes_aggregation[attributes].aggregate(measurement) + self._attributes_aggregation[aggr_key].aggregate(measurement) def collect( self, diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 4904f74ddd..839f4ccfa2 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -209,7 +209,7 @@ def test_collect(self): number_data_point = number_data_points[0] - self.assertEqual(number_data_point.attributes, frozenset({("c", "d")})) + self.assertEqual(number_data_point.attributes, {"c": "d"}) self.assertEqual(number_data_point.value, 0) def test_setting_aggregation(self): From a31dcc3a9e947681ba955623849125bd6aecd610 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 2 Jun 2022 06:48:10 -0700 Subject: [PATCH 1237/1517] Specify name for worker threads for logging and metrics (#2724) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/_logs/export/__init__.py | 12 ++++++++++-- .../sdk/metrics/_internal/export/__init__.py | 12 ++++++++++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbedb9a39f..ca239dc80e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2714](https://github.com/open-telemetry/opentelemetry-python/pull/2714)) - narrow protobuf dependencies to exclude protobuf >= 4 ([#2720](https://github.com/open-telemetry/opentelemetry-python/pull/2720)) +- Specify worker thread names + ([#2724](https://github.com/open-telemetry/opentelemetry-python/pull/2724)) - Loosen dependency on `backoff` for newer Python versions ([#2726](https://github.com/open-telemetry/opentelemetry-python/pull/2726)) - fix: frozenset object has no attribute items diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py index def39e32c9..fb1fc82882 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py @@ -147,7 +147,11 @@ def __init__( self._max_export_batch_size = max_export_batch_size self._export_timeout_millis = export_timeout_millis self._queue = collections.deque() # type: Deque[LogData] - self._worker_thread = threading.Thread(target=self.worker, daemon=True) + self._worker_thread = threading.Thread( + name="OtelBatchLogProcessor", + target=self.worker, + daemon=True, + ) self._condition = threading.Condition(threading.Lock()) self._shutdown = False self._flush_request = None # type: Optional[_FlushRequest] @@ -164,7 +168,11 @@ def __init__( def _at_fork_reinit(self): self._condition = threading.Condition(threading.Lock()) self._queue.clear() - self._worker_thread = threading.Thread(target=self.worker, daemon=True) + self._worker_thread = threading.Thread( + name="OtelBatchLogProcessor", + target=self.worker, + daemon=True, + ) self._worker_thread.start() def worker(self): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index bcaca4281e..684cb4c506 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -373,7 +373,11 @@ def __init__( self._shutdown = False self._shutdown_event = Event() self._shutdown_once = Once() - self._daemon_thread = Thread(target=self._ticker, daemon=True) + self._daemon_thread = Thread( + name="OtelPeriodicExportingMetricReader", + target=self._ticker, + daemon=True, + ) self._daemon_thread.start() if hasattr(os, "register_at_fork"): os.register_at_fork( @@ -381,7 +385,11 @@ def __init__( ) # pylint: disable=protected-access def _at_fork_reinit(self): - self._daemon_thread = Thread(target=self._ticker, daemon=True) + self._daemon_thread = Thread( + name="OtelPeriodicExportingMetricReader", + target=self._ticker, + daemon=True, + ) self._daemon_thread.start() def _ticker(self) -> None: From f8d6795b658b83599842d1b250fe4619e299e194 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 2 Jun 2022 22:08:34 +0530 Subject: [PATCH 1238/1517] fix: update entry point object references for metrics (#2731) --- CHANGELOG.md | 2 ++ opentelemetry-api/setup.cfg | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca239dc80e..5807688a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2726](https://github.com/open-telemetry/opentelemetry-python/pull/2726)) - fix: frozenset object has no attribute items ([#2727](https://github.com/open-telemetry/opentelemetry-python/pull/2727)) +- fix: update entry point object references for metrics + ([#2731](https://github.com/open-telemetry/opentelemetry-python/pull/2731)) ## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 95c2092cb9..c15cd53a37 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -56,7 +56,7 @@ opentelemetry_context = opentelemetry_tracer_provider = default_tracer_provider = opentelemetry.trace:NoOpTracerProvider opentelemetry_meter_provider = - default_meter_provider = opentelemetry._metrics:NoOpMeterProvider + default_meter_provider = opentelemetry.metrics:NoOpMeterProvider opentelemetry_propagator = tracecontext = opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator baggage = opentelemetry.baggage.propagation:W3CBaggagePropagator diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 0581931d8e..7ee57a00a3 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -58,13 +58,13 @@ opentelemetry_tracer_provider = opentelemetry_traces_exporter = console = opentelemetry.sdk.trace.export:ConsoleSpanExporter opentelemetry_meter_provider = - sdk_meter_provider = opentelemetry.sdk._metrics:MeterProvider + sdk_meter_provider = opentelemetry.sdk.metrics:MeterProvider opentelemetry_log_emitter_provider = sdk_log_emitter_provider = opentelemetry.sdk._logs:LogEmitterProvider opentelemetry_logs_exporter = console = opentelemetry.sdk._logs.export:ConsoleLogExporter opentelemetry_metrics_exporter = - console = opentelemetry.sdk._metrics.export:ConsoleMetricExporter + console = opentelemetry.sdk.metrics.export:ConsoleMetricExporter opentelemetry_id_generator = random = opentelemetry.sdk.trace.id_generator:RandomIdGenerator opentelemetry_environment_variables = From 928d333546a5ea792bc977a4fdb415e565627e7b Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 7 Jun 2022 20:02:00 +0530 Subject: [PATCH 1239/1517] Support logs SDK auto instrumentation enable/disable with env (#2728) --- CHANGELOG.md | 2 ++ .../sdk/_configuration/__init__.py | 10 ++++++- .../sdk/environment_variables.py | 14 ++++++++++ opentelemetry-sdk/tests/test_configurator.py | 26 +++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5807688a18..b0ee24bfd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2726](https://github.com/open-telemetry/opentelemetry-python/pull/2726)) - fix: frozenset object has no attribute items ([#2727](https://github.com/open-telemetry/opentelemetry-python/pull/2727)) +- Support logs SDK auto instrumentation enable/disable with env + ([#2728](https://github.com/open-telemetry/opentelemetry-python/pull/2728)) - fix: update entry point object references for metrics ([#2731](https://github.com/open-telemetry/opentelemetry-python/pull/2731)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index efa6f31f89..8cb1d0f412 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -18,6 +18,7 @@ """ import logging +import os from abc import ABC, abstractmethod from os import environ from typing import Dict, Optional, Sequence, Tuple, Type @@ -36,6 +37,9 @@ set_log_emitter_provider, ) from opentelemetry.sdk._logs.export import BatchLogProcessor, LogExporter +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter @@ -185,7 +189,11 @@ def _initialize_components(auto_instrumentation_version): id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) _init_tracing(trace_exporters, id_generator, auto_instrumentation_version) - _init_logging(log_exporters, auto_instrumentation_version) + logging_enabled = os.getenv( + _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "false" + ) + if logging_enabled.strip().lower() == "true": + _init_logging(log_exporters, auto_instrumentation_version) class _BaseConfigurator(ABC): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 145489a683..be105c3fbb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -407,6 +407,20 @@ LogEmitterProvider is used. """ +_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED = ( + "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED" +) +""" +.. envvar:: OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED + +The :envvar:`OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED` environment variable allows users to +enable/disabe the logging SDK auto instrumentation. +Default: False + +Note: Logs SDK and its related settings are experimental. +""" + + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = ( "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE" ) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 1905e6fc38..1b007d3b16 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -29,6 +29,7 @@ _import_id_generator, _init_logging, _init_tracing, + _initialize_components, ) from opentelemetry.sdk._logs import LoggingHandler from opentelemetry.sdk._logs.export import ConsoleLogExporter @@ -277,6 +278,31 @@ def test_logging_init_exporter(self): logging.getLogger(__name__).error("hello") self.assertTrue(provider.processor.exporter.export_called) + @patch.dict( + environ, + {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, + ) + @patch("opentelemetry.sdk._configuration._init_tracing") + @patch("opentelemetry.sdk._configuration._init_logging") + def test_logging_init_disable_default(self, logging_mock, tracing_mock): + _initialize_components("auto-version") + self.assertEqual(logging_mock.call_count, 0) + self.assertEqual(tracing_mock.call_count, 1) + + @patch.dict( + environ, + { + "OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service", + "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED": "True", + }, + ) + @patch("opentelemetry.sdk._configuration._init_tracing") + @patch("opentelemetry.sdk._configuration._init_logging") + def test_logging_init_enable_env(self, logging_mock, tracing_mock): + _initialize_components("auto-version") + self.assertEqual(logging_mock.call_count, 1) + self.assertEqual(tracing_mock.call_count, 1) + class TestExporterNames(TestCase): def test_otlp_exporter_overwrite(self): From b83c2ae75c6e69c0484aa2141f47ff9ac63236f6 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 8 Jun 2022 16:28:16 +0530 Subject: [PATCH 1240/1517] Configure auto instrumentation to support metrics (#2705) * Configure auto instrumentation to support metrics * Add tests * Update test_configurator.py * Add CHANGELOG entry * lint --- CHANGELOG.md | 2 + .../sdk/_configuration/__init__.py | 60 ++++++++- opentelemetry-sdk/tests/test_configurator.py | 118 +++++++++++++++++- 3 files changed, 170 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0ee24bfd9..e33019f119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2728](https://github.com/open-telemetry/opentelemetry-python/pull/2728)) - fix: update entry point object references for metrics ([#2731](https://github.com/open-telemetry/opentelemetry-python/pull/2731)) +- Configure auto instrumentation to support metrics + ([#2705](https://github.com/open-telemetry/opentelemetry-python/pull/2705)) ## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 8cb1d0f412..55463e00be 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -25,12 +25,13 @@ from pkg_resources import iter_entry_points -from opentelemetry import trace from opentelemetry.environment_variables import ( OTEL_LOGS_EXPORTER, + OTEL_METRICS_EXPORTER, OTEL_PYTHON_ID_GENERATOR, OTEL_TRACES_EXPORTER, ) +from opentelemetry.metrics import set_meter_provider from opentelemetry.sdk._logs import ( LogEmitterProvider, LoggingHandler, @@ -40,11 +41,17 @@ from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, ) +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + MetricExporter, + PeriodicExportingMetricReader, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator from opentelemetry.semconv.resource import ResourceAttributes +from opentelemetry.trace import set_tracer_provider _EXPORTER_OTLP = "otlp" _EXPORTER_OTLP_PROTO_GRPC = "otlp_proto_grpc" @@ -87,7 +94,7 @@ def _init_tracing( id_generator=id_generator(), resource=Resource.create(auto_resource), ) - trace.set_tracer_provider(provider) + set_tracer_provider(provider) for _, exporter_class in exporters.items(): exporter_args = {} @@ -96,6 +103,33 @@ def _init_tracing( ) +def _init_metrics( + exporters: Dict[str, Type[MetricExporter]], + auto_instrumentation_version: Optional[str] = None, +): + # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name + # from the env variable else defaults to "unknown_service" + auto_resource = {} + # populate version if using auto-instrumentation + if auto_instrumentation_version: + auto_resource[ + ResourceAttributes.TELEMETRY_AUTO_VERSION + ] = auto_instrumentation_version + + metric_readers = [] + + for _, exporter_class in exporters.items(): + exporter_args = {} + metric_readers.append( + PeriodicExportingMetricReader(exporter_class(**exporter_args)) + ) + + provider = MeterProvider( + resource=Resource.create(auto_resource), metric_readers=metric_readers + ) + set_meter_provider(provider) + + def _init_logging( exporters: Dict[str, Type[LogExporter]], auto_instrumentation_version: Optional[str] = None, @@ -145,9 +179,15 @@ def _import_config_components( def _import_exporters( trace_exporter_names: Sequence[str], + metric_exporter_names: Sequence[str], log_exporter_names: Sequence[str], -) -> Tuple[Dict[str, Type[SpanExporter]], Dict[str, Type[LogExporter]]]: +) -> Tuple[ + Dict[str, Type[SpanExporter]], + Dict[str, Type[MetricExporter]], + Dict[str, Type[LogExporter]], +]: trace_exporters = {} + metric_exporters = {} log_exporters = {} for (exporter_name, exporter_impl,) in _import_config_components( @@ -158,6 +198,14 @@ def _import_exporters( else: raise RuntimeError(f"{exporter_name} is not a trace exporter") + for (exporter_name, exporter_impl,) in _import_config_components( + metric_exporter_names, "opentelemetry_metrics_exporter" + ): + if issubclass(exporter_impl, MetricExporter): + metric_exporters[exporter_name] = exporter_impl + else: + raise RuntimeError(f"{exporter_name} is not a metric exporter") + for (exporter_name, exporter_impl,) in _import_config_components( log_exporter_names, "opentelemetry_logs_exporter" ): @@ -166,7 +214,7 @@ def _import_exporters( else: raise RuntimeError(f"{exporter_name} is not a log exporter") - return trace_exporters, log_exporters + return trace_exporters, metric_exporters, log_exporters def _import_id_generator(id_generator_name: str) -> IdGenerator: @@ -182,13 +230,15 @@ def _import_id_generator(id_generator_name: str) -> IdGenerator: def _initialize_components(auto_instrumentation_version): - trace_exporters, log_exporters = _import_exporters( + trace_exporters, metric_exporters, log_exporters = _import_exporters( _get_exporter_names(environ.get(OTEL_TRACES_EXPORTER)), + _get_exporter_names(environ.get(OTEL_METRICS_EXPORTER)), _get_exporter_names(environ.get(OTEL_LOGS_EXPORTER)), ) id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) _init_tracing(trace_exporters, id_generator, auto_instrumentation_version) + _init_metrics(metric_exporters, auto_instrumentation_version) logging_enabled = os.getenv( _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "false" ) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 1b007d3b16..39548ff5d4 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # type: ignore +# pylint: skip-file import logging from os import environ +from typing import Dict, Iterable, Optional from unittest import TestCase from unittest.mock import patch @@ -28,12 +30,21 @@ _import_exporters, _import_id_generator, _init_logging, + _init_metrics, _init_tracing, _initialize_components, ) from opentelemetry.sdk._logs import LoggingHandler from opentelemetry.sdk._logs.export import ConsoleLogExporter -from opentelemetry.sdk.metrics.export import ConsoleMetricExporter +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + ConsoleMetricExporter, + Metric, + MetricExporter, + MetricReader, +) +from opentelemetry.sdk.metrics.view import Aggregation from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator @@ -61,6 +72,10 @@ def get_log_emitter(self, name): return DummyLogEmitter(name, self.resource, self.processor) +class DummyMeterProvider(MeterProvider): + pass + + class DummyLogEmitter: def __init__(self, name, resource, processor): self.name = name @@ -93,6 +108,44 @@ def __init__(self, exporter): self.exporter = exporter +class DummyMetricReader(MetricReader): + def __init__( + self, + exporter: MetricExporter, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[type, Aggregation] = None, + export_interval_millis: Optional[float] = None, + export_timeout_millis: Optional[float] = None, + ) -> None: + super().__init__( + preferred_temporality=preferred_temporality, + preferred_aggregation=preferred_aggregation, + ) + self.exporter = exporter + + def _receive_metrics( + self, + metrics: Iterable[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> None: + self.exporter.export(None) + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + return True + + +class DummyOTLPMetricExporter: + def __init__(self, *args, **kwargs): + self.export_called = False + + def export(self, batch): + self.export_called = True + + def shutdown(self): + pass + + class Exporter: def __init__(self): tracer_provider = trace.get_tracer_provider() @@ -148,7 +201,7 @@ def setUp(self): "opentelemetry.sdk._configuration.BatchSpanProcessor", Processor ) self.set_provider_patcher = patch( - "opentelemetry.trace.set_tracer_provider" + "opentelemetry.sdk._configuration.set_tracer_provider" ) self.get_provider_mock = self.get_provider_patcher.start() @@ -304,6 +357,61 @@ def test_logging_init_enable_env(self, logging_mock, tracing_mock): self.assertEqual(tracing_mock.call_count, 1) +class TestMetricsInit(TestCase): + def setUp(self): + self.metric_reader_patch = patch( + "opentelemetry.sdk._configuration.PeriodicExportingMetricReader", + DummyMetricReader, + ) + self.provider_patch = patch( + "opentelemetry.sdk._configuration.MeterProvider", + DummyMeterProvider, + ) + self.set_provider_patch = patch( + "opentelemetry.sdk._configuration.set_meter_provider" + ) + + self.metric_reader_mock = self.metric_reader_patch.start() + self.provider_mock = self.provider_patch.start() + self.set_provider_mock = self.set_provider_patch.start() + + def tearDown(self): + self.metric_reader_patch.stop() + self.set_provider_patch.stop() + self.provider_patch.stop() + + def test_metrics_init_empty(self): + _init_metrics({}, "auto-version") + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyMeterProvider) + self.assertIsInstance(provider._sdk_config.resource, Resource) + self.assertEqual( + provider._sdk_config.resource.attributes.get( + "telemetry.auto.version" + ), + "auto-version", + ) + + @patch.dict( + environ, + {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, + ) + def test_metrics_init_exporter(self): + _init_metrics({"otlp": DummyOTLPMetricExporter}) + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyMeterProvider) + self.assertIsInstance(provider._sdk_config.resource, Resource) + self.assertEqual( + provider._sdk_config.resource.attributes.get("service.name"), + "otlp-service", + ) + reader = provider._sdk_config.metric_readers[0] + self.assertIsInstance(reader, DummyMetricReader) + self.assertIsInstance(reader.exporter, DummyOTLPMetricExporter) + + class TestExporterNames(TestCase): def test_otlp_exporter_overwrite(self): for exporter in [_EXPORTER_OTLP, _EXPORTER_OTLP_PROTO_GRPC]: @@ -328,8 +436,8 @@ def test_empty_exporters(self): class TestImportExporters(TestCase): def test_console_exporters(self): - trace_exporters, logs_exporters = _import_exporters( - ["console"], ["console"] + trace_exporters, metric_exporterts, logs_exporters = _import_exporters( + ["console"], ["console"], ["console"] ) self.assertEqual( trace_exporters["console"].__class__, ConsoleSpanExporter.__class__ @@ -338,6 +446,6 @@ def test_console_exporters(self): logs_exporters["console"].__class__, ConsoleLogExporter.__class__ ) self.assertEqual( - logs_exporters["console"].__class__, + metric_exporterts["console"].__class__, ConsoleMetricExporter.__class__, ) From 9fbc93baddd27f00ab36b64ba9626dd94b29eb54 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 8 Jun 2022 21:44:43 +0530 Subject: [PATCH 1241/1517] bugfix: batch processor doesn't work with uwsgi (#2277) --- .../src/opentelemetry/sdk/_logs/export/__init__.py | 9 +++++++++ .../src/opentelemetry/sdk/trace/export/__init__.py | 9 +++++++++ opentelemetry-sdk/tests/logs/test_export.py | 9 +++------ opentelemetry-sdk/tests/trace/export/test_export.py | 5 ++--- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py index fb1fc82882..826ba3f3d6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py @@ -24,6 +24,7 @@ from opentelemetry.context import attach, detach, set_value from opentelemetry.sdk._logs import LogData, LogProcessor, LogRecord +from opentelemetry.util._once import Once from opentelemetry.util._time import _time_ns _logger = logging.getLogger(__name__) @@ -129,6 +130,9 @@ def __init__(self): self.num_log_records = 0 +_BSP_RESET_ONCE = Once() + + class BatchLogProcessor(LogProcessor): """This is an implementation of LogProcessor which creates batches of received logs in the export-friendly LogData representation and @@ -164,6 +168,7 @@ def __init__( os.register_at_fork( after_in_child=self._at_fork_reinit ) # pylint: disable=protected-access + self._pid = os.getpid() def _at_fork_reinit(self): self._condition = threading.Condition(threading.Lock()) @@ -174,6 +179,7 @@ def _at_fork_reinit(self): daemon=True, ) self._worker_thread.start() + self._pid = os.getpid() def worker(self): timeout = self._schedule_delay_millis / 1e3 @@ -293,6 +299,9 @@ def emit(self, log_data: LogData) -> None: """ if self._shutdown: return + if self._pid != os.getpid(): + _BSP_RESET_ONCE.do_once(self._at_fork_reinit) + self._queue.appendleft(log_data) if len(self._queue) >= self._max_export_batch_size: with self._condition: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index d40bb4968c..c574a327f0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -36,6 +36,7 @@ OTEL_BSP_SCHEDULE_DELAY, ) from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor +from opentelemetry.util._once import Once from opentelemetry.util._time import _time_ns logger = logging.getLogger(__name__) @@ -119,6 +120,9 @@ def __init__(self): self.num_spans = 0 +_BSP_RESET_ONCE = Once() + + class BatchSpanProcessor(SpanProcessor): """Batch span processor implementation. @@ -203,6 +207,7 @@ def __init__( os.register_at_fork( after_in_child=self._at_fork_reinit ) # pylint: disable=protected-access + self._pid = os.getpid() def on_start( self, span: Span, parent_context: typing.Optional[Context] = None @@ -215,6 +220,9 @@ def on_end(self, span: ReadableSpan) -> None: return if not span.context.trace_flags.sampled: return + if self._pid != os.getpid(): + _BSP_RESET_ONCE.do_once(self._at_fork_reinit) + if len(self.queue) == self.max_queue_size: if not self._spans_dropped: logger.warning("Queue is full, likely spans will be dropped.") @@ -236,6 +244,7 @@ def _at_fork_reinit(self): name="OtelBatchSpanProcessor", target=self.worker, daemon=True ) self.worker_thread.start() + self._pid = os.getpid() def worker(self): timeout = self.schedule_delay_millis / 1e3 diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index a1c39d6df2..d7dbed76a6 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -16,7 +16,6 @@ import logging import multiprocessing import os -import sys import time import unittest from concurrent.futures import ThreadPoolExecutor @@ -44,8 +43,6 @@ from opentelemetry.trace import TraceFlags from opentelemetry.trace.span import INVALID_SPAN_CONTEXT -supports_register_at_fork = hasattr(os, "fork") and sys.version_info >= (3, 7) - class TestSimpleLogProcessor(unittest.TestCase): def test_simple_log_processor_default_level(self): @@ -274,9 +271,9 @@ def bulk_log_and_flush(num_logs): finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 2415) - @unittest.skipIf( - not supports_register_at_fork, - "needs *nix and minor version 7 or later", + @unittest.skipUnless( + hasattr(os, "fork"), + "needs *nix", ) def test_batch_log_processor_fork(self): # pylint: disable=invalid-name diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 00ccfe44d3..b1eb98ce2e 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -14,7 +14,6 @@ import multiprocessing import os -import sys import threading import time import unittest @@ -369,8 +368,8 @@ def _check_fork_trace(self, exporter, expected): self.assertIn(span.name, expected) @unittest.skipUnless( - hasattr(os, "fork") and sys.version_info >= (3, 7), - "needs *nix and minor version 7 or later", + hasattr(os, "fork"), + "needs *nix", ) def test_batch_span_processor_fork(self): # pylint: disable=invalid-name From 101db90768fb2008cad6e504ea565a3ee93bb9a8 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 8 Jun 2022 17:54:01 +0100 Subject: [PATCH 1242/1517] Add missing to_json methods (#2722) * Add missing to_json methods Fixes #2716 * Added resource to_json method * Rename to metrics data * Add to_json to Instrumentation Scope as well Fixes #2716 --- CHANGELOG.md | 2 + .../sdk/metrics/_internal/export/__init__.py | 4 +- .../sdk/metrics/_internal/point.py | 131 +++++--- .../opentelemetry/sdk/resources/__init__.py | 9 + .../opentelemetry/sdk/util/instrumentation.py | 29 +- .../integration_test/test_console_exporter.py | 37 +++ opentelemetry-sdk/tests/metrics/test_point.py | 279 ++++++++++++++---- 7 files changed, 380 insertions(+), 111 deletions(-) create mode 100644 opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e33019f119..8209e3c182 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc1-0.31b0...HEAD) +- Add missing `to_json` methods + ([#2722](https://github.com/open-telemetry/opentelemetry-python/pull/2722) - Fix type hints for textmap `Getter` and `Setter` ([#2657](https://github.com/open-telemetry/opentelemetry-python/pull/2657)) - Fix LogEmitterProvider.force_flush hanging randomly diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 684cb4c506..8ed5596c81 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -105,8 +105,8 @@ def __init__( self, out: IO = stdout, formatter: Callable[ - ["opentelemetry.sdk.metrics.export.Metric"], str - ] = lambda metric: metric.to_json() + ["opentelemetry.sdk.metrics.export.MetricsData"], str + ] = lambda metrics_data: metrics_data.to_json() + linesep, ): self.out = out diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py index 4ae6862967..b4d813acca 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py @@ -15,8 +15,8 @@ # pylint: disable=unused-import from dataclasses import asdict, dataclass -from json import dumps -from typing import Sequence, Union +from json import dumps, loads +from typing import Optional, Sequence, Union # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics._internal @@ -36,6 +36,29 @@ class NumberDataPoint: time_unix_nano: int value: Union[int, float] + def to_json(self, indent=4) -> str: + return dumps(asdict(self), indent=indent) + + +@dataclass(frozen=True) +class HistogramDataPoint: + """Single data point in a timeseries that describes the time-varying scalar + value of a metric. + """ + + attributes: Attributes + start_time_unix_nano: int + time_unix_nano: int + count: int + sum: Union[int, float] + bucket_counts: Sequence[int] + explicit_bounds: Sequence[float] + min: float + max: float + + def to_json(self, indent=4) -> str: + return dumps(asdict(self), indent=indent) + @dataclass(frozen=True) class Sum: @@ -48,15 +71,17 @@ class Sum: ) is_monotonic: bool - def to_json(self) -> str: + def to_json(self, indent=4) -> str: return dumps( { - "data_points": dumps( - [asdict(data_point) for data_point in self.data_points] - ), + "data_points": [ + loads(data_point.to_json(indent=indent)) + for data_point in self.data_points + ], "aggregation_temporality": self.aggregation_temporality, "is_monotonic": self.is_monotonic, - } + }, + indent=indent, ) @@ -68,33 +93,18 @@ class Gauge: data_points: Sequence[NumberDataPoint] - def to_json(self) -> str: + def to_json(self, indent=4) -> str: return dumps( { - "data_points": dumps( - [asdict(data_point) for data_point in self.data_points] - ) - } + "data_points": [ + loads(data_point.to_json(indent=indent)) + for data_point in self.data_points + ], + }, + indent=indent, ) -@dataclass(frozen=True) -class HistogramDataPoint: - """Single data point in a timeseries that describes the time-varying scalar - value of a metric. - """ - - attributes: Attributes - start_time_unix_nano: int - time_unix_nano: int - count: int - sum: Union[int, float] - bucket_counts: Sequence[int] - explicit_bounds: Sequence[float] - min: float - max: float - - @dataclass(frozen=True) class Histogram: """Represents the type of a metric that is calculated by aggregating as a @@ -105,14 +115,16 @@ class Histogram: "opentelemetry.sdk.metrics.export.AggregationTemporality" ) - def to_json(self) -> str: + def to_json(self, indent=4) -> str: return dumps( { - "data_points": dumps( - [asdict(data_point) for data_point in self.data_points] - ), + "data_points": [ + loads(data_point.to_json(indent=indent)) + for data_point in self.data_points + ], "aggregation_temporality": self.aggregation_temporality, - } + }, + indent=indent, ) @@ -126,18 +138,19 @@ class Metric: exported.""" name: str - description: str - unit: str + description: Optional[str] + unit: Optional[str] data: DataT - def to_json(self) -> str: + def to_json(self, indent=4) -> str: return dumps( { "name": self.name, - "description": self.description if self.description else "", - "unit": self.unit if self.unit else "", - "data": self.data.to_json(), - } + "description": self.description or "", + "unit": self.unit or "", + "data": loads(self.data.to_json(indent=indent)), + }, + indent=indent, ) @@ -149,6 +162,19 @@ class ScopeMetrics: metrics: Sequence[Metric] schema_url: str + def to_json(self, indent=4) -> str: + return dumps( + { + "scope": loads(self.scope.to_json(indent=indent)), + "metrics": [ + loads(metric.to_json(indent=indent)) + for metric in self.metrics + ], + "schema_url": self.schema_url, + }, + indent=indent, + ) + @dataclass(frozen=True) class ResourceMetrics: @@ -158,9 +184,32 @@ class ResourceMetrics: scope_metrics: Sequence[ScopeMetrics] schema_url: str + def to_json(self, indent=4) -> str: + return dumps( + { + "resource": loads(self.resource.to_json(indent=indent)), + "scope_metrics": [ + loads(scope_metrics.to_json(indent=indent)) + for scope_metrics in self.scope_metrics + ], + "schema_url": self.schema_url, + }, + indent=indent, + ) + @dataclass(frozen=True) class MetricsData: """An array of ResourceMetrics""" resource_metrics: Sequence[ResourceMetrics] + + def to_json(self, indent=4) -> str: + return dumps( + { + "resource_metrics": [ + loads(resource_metrics.to_json(indent=indent)) + for resource_metrics in self.resource_metrics + ] + } + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index d99f097b38..996a7f2800 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -242,6 +242,15 @@ def __hash__(self): f"{dumps(self._attributes.copy(), sort_keys=True)}|{self._schema_url}" ) + def to_json(self, indent=4) -> str: + return dumps( + { + "attributes": dict(self._attributes), + "schema_url": self._schema_url, + }, + indent=indent, + ) + _EMPTY_RESOURCE = Resource({}) _DEFAULT_RESOURCE = Resource( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index a489c207a0..55d7c6277b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -11,7 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import typing +from json import dumps +from typing import Optional from deprecated import deprecated @@ -29,8 +30,8 @@ class InstrumentationInfo: def __init__( self, name: str, - version: typing.Optional[str] = None, - schema_url: typing.Optional[str] = None, + version: Optional[str] = None, + schema_url: Optional[str] = None, ): self._name = name self._version = version @@ -59,11 +60,11 @@ def __lt__(self, value): ) @property - def schema_url(self) -> typing.Optional[str]: + def schema_url(self) -> Optional[str]: return self._schema_url @property - def version(self) -> typing.Optional[str]: + def version(self) -> Optional[str]: return self._version @property @@ -84,8 +85,8 @@ class InstrumentationScope: def __init__( self, name: str, - version: typing.Optional[str] = None, - schema_url: typing.Optional[str] = None, + version: Optional[str] = None, + schema_url: Optional[str] = None, ) -> None: self._name = name self._version = version @@ -116,13 +117,23 @@ def __lt__(self, value: object) -> bool: ) @property - def schema_url(self) -> typing.Optional[str]: + def schema_url(self) -> Optional[str]: return self._schema_url @property - def version(self) -> typing.Optional[str]: + def version(self) -> Optional[str]: return self._version @property def name(self) -> str: return self._name + + def to_json(self, indent=4) -> str: + return dumps( + { + "name": self._name, + "version": self._version, + "schema_url": self._schema_url, + }, + indent=indent, + ) diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py b/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py new file mode 100644 index 0000000000..d09ade6a29 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py @@ -0,0 +1,37 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) + + +class TestConsoleExporter(TestCase): + def test_console_exporter(self): + + try: + exporter = ConsoleMetricExporter() + reader = PeriodicExportingMetricReader(exporter) + provider = MeterProvider(metric_readers=[reader]) + metrics.set_meter_provider(provider) + meter = metrics.get_meter(__name__) + counter = meter.create_counter("test") + counter.add(1) + except Exception as error: + self.fail(f"Unexpected exception {error} raised") diff --git a/opentelemetry-sdk/tests/metrics/test_point.py b/opentelemetry-sdk/tests/metrics/test_point.py index ce3e73b7b0..5d6640fdea 100644 --- a/opentelemetry-sdk/tests/metrics/test_point.py +++ b/opentelemetry-sdk/tests/metrics/test_point.py @@ -15,85 +15,246 @@ from unittest import TestCase from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, Gauge, Histogram, HistogramDataPoint, Metric, + MetricsData, NumberDataPoint, + ResourceMetrics, + ScopeMetrics, Sum, ) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope -def _create_metric(data): - return Metric( - name="test-name", - description="test-description", - unit="test-unit", - data=data, - ) +class TestToJson(TestCase): + @classmethod + def setUpClass(cls): + cls.attributes_0 = { + "a": "b", + "b": True, + "c": 1, + "d": 1.1, + "e": ["a", "b"], + "f": [True, False], + "g": [1, 2], + "h": [1.1, 2.2], + } + cls.attributes_0_str = '{"a": "b", "b": true, "c": 1, "d": 1.1, "e": ["a", "b"], "f": [true, false], "g": [1, 2], "h": [1.1, 2.2]}' -class TestDatapointToJSON(TestCase): - def test_sum(self): - self.maxDiff = None - point = _create_metric( - Sum( - data_points=[ - NumberDataPoint( - attributes={"attr-key": "test-val"}, - start_time_unix_nano=10, - time_unix_nano=20, - value=9, - ) - ], - aggregation_temporality=2, - is_monotonic=True, - ) + cls.attributes_1 = { + "i": "a", + "j": False, + "k": 2, + "l": 2.2, + "m": ["b", "a"], + "n": [False, True], + "o": [2, 1], + "p": [2.2, 1.1], + } + cls.attributes_1_str = '{"i": "a", "j": false, "k": 2, "l": 2.2, "m": ["b", "a"], "n": [false, true], "o": [2, 1], "p": [2.2, 1.1]}' + + cls.number_data_point_0 = NumberDataPoint( + attributes=cls.attributes_0, + start_time_unix_nano=1, + time_unix_nano=2, + value=3.3, + ) + cls.number_data_point_0_str = f'{{"attributes": {cls.attributes_0_str}, "start_time_unix_nano": 1, "time_unix_nano": 2, "value": 3.3}}' + + cls.number_data_point_1 = NumberDataPoint( + attributes=cls.attributes_1, + start_time_unix_nano=2, + time_unix_nano=3, + value=4.4, + ) + cls.number_data_point_1_str = f'{{"attributes": {cls.attributes_1_str}, "start_time_unix_nano": 2, "time_unix_nano": 3, "value": 4.4}}' + + cls.histogram_data_point_0 = HistogramDataPoint( + attributes=cls.attributes_0, + start_time_unix_nano=1, + time_unix_nano=2, + count=3, + sum=3.3, + bucket_counts=[1, 1, 1], + explicit_bounds=[0.1, 1.2, 2.3, 3.4], + min=0.2, + max=3.3, + ) + cls.histogram_data_point_0_str = f'{{"attributes": {cls.attributes_0_str}, "start_time_unix_nano": 1, "time_unix_nano": 2, "count": 3, "sum": 3.3, "bucket_counts": [1, 1, 1], "explicit_bounds": [0.1, 1.2, 2.3, 3.4], "min": 0.2, "max": 3.3}}' + + cls.histogram_data_point_1 = HistogramDataPoint( + attributes=cls.attributes_1, + start_time_unix_nano=2, + time_unix_nano=3, + count=4, + sum=4.4, + bucket_counts=[2, 1, 1], + explicit_bounds=[1.2, 2.3, 3.4, 4.5], + min=0.3, + max=4.4, + ) + cls.histogram_data_point_1_str = f'{{"attributes": {cls.attributes_1_str}, "start_time_unix_nano": 2, "time_unix_nano": 3, "count": 4, "sum": 4.4, "bucket_counts": [2, 1, 1], "explicit_bounds": [1.2, 2.3, 3.4, 4.5], "min": 0.3, "max": 4.4}}' + + cls.sum_0 = Sum( + data_points=[cls.number_data_point_0, cls.number_data_point_1], + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=False, + ) + cls.sum_0_str = f'{{"data_points": [{cls.number_data_point_0_str}, {cls.number_data_point_1_str}], "aggregation_temporality": 1, "is_monotonic": false}}' + + cls.gauge_0 = Gauge( + data_points=[cls.number_data_point_0, cls.number_data_point_1], + ) + cls.gauge_0_str = f'{{"data_points": [{cls.number_data_point_0_str}, {cls.number_data_point_1_str}]}}' + + cls.histogram_0 = Histogram( + data_points=[ + cls.histogram_data_point_0, + cls.histogram_data_point_1, + ], + aggregation_temporality=AggregationTemporality.DELTA, + ) + cls.histogram_0_str = f'{{"data_points": [{cls.histogram_data_point_0_str}, {cls.histogram_data_point_1_str}], "aggregation_temporality": 1}}' + + cls.metric_0 = Metric( + name="metric_0", + description="description_0", + unit="unit_0", + data=cls.sum_0, ) + cls.metric_0_str = f'{{"name": "metric_0", "description": "description_0", "unit": "unit_0", "data": {cls.sum_0_str}}}' + + cls.metric_1 = Metric( + name="metric_1", description=None, unit="unit_1", data=cls.gauge_0 + ) + cls.metric_1_str = f'{{"name": "metric_1", "description": "", "unit": "unit_1", "data": {cls.gauge_0_str}}}' + + cls.metric_2 = Metric( + name="metric_2", + description="description_2", + unit=None, + data=cls.histogram_0, + ) + cls.metric_2_str = f'{{"name": "metric_2", "description": "description_2", "unit": "", "data": {cls.histogram_0_str}}}' + + cls.scope_metrics_0 = ScopeMetrics( + scope=InstrumentationScope( + name="name_0", + version="version_0", + schema_url="schema_url_0", + ), + metrics=[cls.metric_0, cls.metric_1, cls.metric_2], + schema_url="schema_url_0", + ) + cls.scope_metrics_0_str = f'{{"scope": {{"name": "name_0", "version": "version_0", "schema_url": "schema_url_0"}}, "metrics": [{cls.metric_0_str}, {cls.metric_1_str}, {cls.metric_2_str}], "schema_url": "schema_url_0"}}' + + cls.scope_metrics_1 = ScopeMetrics( + scope=InstrumentationScope( + name="name_1", + version="version_1", + schema_url="schema_url_1", + ), + metrics=[cls.metric_0, cls.metric_1, cls.metric_2], + schema_url="schema_url_1", + ) + cls.scope_metrics_1_str = f'{{"scope": {{"name": "name_1", "version": "version_1", "schema_url": "schema_url_1"}}, "metrics": [{cls.metric_0_str}, {cls.metric_1_str}, {cls.metric_2_str}], "schema_url": "schema_url_1"}}' + + cls.resource_metrics_0 = ResourceMetrics( + resource=Resource( + attributes=cls.attributes_0, schema_url="schema_url_0" + ), + scope_metrics=[cls.scope_metrics_0, cls.scope_metrics_1], + schema_url="schema_url_0", + ) + cls.resource_metrics_0_str = f'{{"resource": {{"attributes": {cls.attributes_0_str}, "schema_url": "schema_url_0"}}, "scope_metrics": [{cls.scope_metrics_0_str}, {cls.scope_metrics_1_str}], "schema_url": "schema_url_0"}}' + + cls.resource_metrics_1 = ResourceMetrics( + resource=Resource( + attributes=cls.attributes_1, schema_url="schema_url_1" + ), + scope_metrics=[cls.scope_metrics_0, cls.scope_metrics_1], + schema_url="schema_url_1", + ) + cls.resource_metrics_1_str = f'{{"resource": {{"attributes": {cls.attributes_1_str}, "schema_url": "schema_url_1"}}, "scope_metrics": [{cls.scope_metrics_0_str}, {cls.scope_metrics_1_str}], "schema_url": "schema_url_1"}}' + + cls.metrics_data_0 = MetricsData( + resource_metrics=[cls.resource_metrics_0, cls.resource_metrics_1] + ) + cls.metrics_data_0_str = f'{{"resource_metrics": [{cls.resource_metrics_0_str}, {cls.resource_metrics_1_str}]}}' + + def test_number_data_point(self): + self.assertEqual( - '{"name": "test-name", "description": "test-description", "unit": "test-unit", "data": "{\\"data_points\\": \\"[{\\\\\\"attributes\\\\\\": {\\\\\\"attr-key\\\\\\": \\\\\\"test-val\\\\\\"}, \\\\\\"start_time_unix_nano\\\\\\": 10, \\\\\\"time_unix_nano\\\\\\": 20, \\\\\\"value\\\\\\": 9}]\\", \\"aggregation_temporality\\": 2, \\"is_monotonic\\": true}"}', - point.to_json(), + self.number_data_point_0.to_json(indent=None), + self.number_data_point_0_str, + ) + self.assertEqual( + self.number_data_point_1.to_json(indent=None), + self.number_data_point_1_str, ) - def test_gauge(self): - point = _create_metric( - Gauge( - data_points=[ - NumberDataPoint( - attributes={"attr-key": "test-val"}, - start_time_unix_nano=10, - time_unix_nano=20, - value=9, - ) - ] - ) + def test_histogram_data_point(self): + + self.assertEqual( + self.histogram_data_point_0.to_json(indent=None), + self.histogram_data_point_0_str, ) self.assertEqual( - '{"name": "test-name", "description": "test-description", "unit": "test-unit", "data": "{\\"data_points\\": \\"[{\\\\\\"attributes\\\\\\": {\\\\\\"attr-key\\\\\\": \\\\\\"test-val\\\\\\"}, \\\\\\"start_time_unix_nano\\\\\\": 10, \\\\\\"time_unix_nano\\\\\\": 20, \\\\\\"value\\\\\\": 9}]\\"}"}', - point.to_json(), + self.histogram_data_point_1.to_json(indent=None), + self.histogram_data_point_1_str, ) + def test_sum(self): + + self.assertEqual(self.sum_0.to_json(indent=None), self.sum_0_str) + + def test_gauge(self): + + self.maxDiff = None + + self.assertEqual(self.gauge_0.to_json(indent=None), self.gauge_0_str) + def test_histogram(self): - point = _create_metric( - Histogram( - data_points=[ - HistogramDataPoint( - attributes={"attr-key": "test-val"}, - start_time_unix_nano=50, - time_unix_nano=60, - count=1, - sum=0.8, - bucket_counts=[0, 0, 1, 0], - explicit_bounds=[0.1, 0.5, 0.9, 1], - min=0.8, - max=0.8, - ) - ], - aggregation_temporality=1, - ) + + self.assertEqual( + self.histogram_0.to_json(indent=None), self.histogram_0_str ) - self.maxDiff = None + + def test_metric(self): + + self.assertEqual(self.metric_0.to_json(indent=None), self.metric_0_str) + + self.assertEqual(self.metric_1.to_json(indent=None), self.metric_1_str) + + self.assertEqual(self.metric_2.to_json(indent=None), self.metric_2_str) + + def test_scope_metrics(self): + + self.assertEqual( + self.scope_metrics_0.to_json(indent=None), self.scope_metrics_0_str + ) + self.assertEqual( + self.scope_metrics_1.to_json(indent=None), self.scope_metrics_1_str + ) + + def test_resource_metrics(self): + + self.assertEqual( + self.resource_metrics_0.to_json(indent=None), + self.resource_metrics_0_str, + ) + self.assertEqual( + self.resource_metrics_1.to_json(indent=None), + self.resource_metrics_1_str, + ) + + def test_metrics_data(self): + self.assertEqual( - '{"name": "test-name", "description": "test-description", "unit": "test-unit", "data": "{\\"data_points\\": \\"[{\\\\\\"attributes\\\\\\": {\\\\\\"attr-key\\\\\\": \\\\\\"test-val\\\\\\"}, \\\\\\"start_time_unix_nano\\\\\\": 50, \\\\\\"time_unix_nano\\\\\\": 60, \\\\\\"count\\\\\\": 1, \\\\\\"sum\\\\\\": 0.8, \\\\\\"bucket_counts\\\\\\": [0, 0, 1, 0], \\\\\\"explicit_bounds\\\\\\": [0.1, 0.5, 0.9, 1], \\\\\\"min\\\\\\": 0.8, \\\\\\"max\\\\\\": 0.8}]\\", \\"aggregation_temporality\\": 1}"}', - point.to_json(), + self.metrics_data_0.to_json(indent=None), self.metrics_data_0_str ) From 953e422a6123fd433fed7285e2562e03b77a2493 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 10 Jun 2022 03:18:53 +0530 Subject: [PATCH 1243/1517] Add entrypoint for metrics exporter (#2748) --- CHANGELOG.md | 2 ++ exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 ++ exporter/opentelemetry-exporter-otlp/setup.cfg | 2 ++ 3 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8209e3c182..eb7e4d6550 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2731](https://github.com/open-telemetry/opentelemetry-python/pull/2731)) - Configure auto instrumentation to support metrics ([#2705](https://github.com/open-telemetry/opentelemetry-python/pull/2705)) +- Add entrypoint for metrics exporter + ([#2748](https://github.com/open-telemetry/opentelemetry-python/pull/2748)) ## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 4206782846..3c35fef771 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -58,5 +58,7 @@ where = src [options.entry_points] opentelemetry_traces_exporter = otlp_proto_grpc = opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter +opentelemetry_metrics_exporter = + otlp_proto_grpc = opentelemetry.exporter.otlp.proto.grpc.metric_exporter:OTLPMetricExporter opentelemetry_logs_exporter = otlp_proto_grpc = opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 13a7ab92b6..6157365001 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -50,5 +50,7 @@ where = src [options.entry_points] opentelemetry_traces_exporter = otlp = opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter +opentelemetry_metrics_exporter = + otlp = opentelemetry.exporter.otlp.proto.grpc.metric_exporter:OTLPMetricExporter opentelemetry_logs_exporter = otlp = opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter From 6aee8195fdc30fe670c01782cf394da00c5858cc Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 10 Jun 2022 20:19:06 +0100 Subject: [PATCH 1244/1517] Check data point before yielding (#2745) --- CHANGELOG.md | 2 + .../_internal/_view_instrument_match.py | 4 +- .../metrics/test_view_instrument_match.py | 72 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb7e4d6550..cb8b44adb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc1-0.31b0...HEAD) +- Fix yield of `None`-valued points + ([#2745](https://github.com/open-telemetry/opentelemetry-python/pull/2745)) - Add missing `to_json` methods ([#2722](https://github.com/open-telemetry/opentelemetry-python/pull/2722) - Fix type hints for textmap `Getter` and `Setter` diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index 026b970235..4ada52d91b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -130,6 +130,8 @@ def collect( with self._lock: for aggregation in self._attributes_aggregation.values(): - yield aggregation.collect( + data_point = aggregation.collect( aggregation_temporality, collection_start_nanos ) + if data_point is not None: + yield data_point diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 839f4ccfa2..d2730086a4 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -212,6 +212,78 @@ def test_collect(self): self.assertEqual(number_data_point.attributes, {"c": "d"}) self.assertEqual(number_data_point.value, 0) + def test_data_point_check(self): + instrument1 = Counter( + "instrument1", + Mock(), + Mock(), + description="description", + unit="unit", + ) + instrument1.instrumentation_scope = self.mock_instrumentation_scope + + view_instrument_match = _ViewInstrumentMatch( + view=View( + instrument_name="instrument1", + name="name", + aggregation=DefaultAggregation(), + ), + instrument=instrument1, + instrument_class_aggregation=MagicMock( + **{ + "__getitem__.return_value": Mock( + **{ + "_create_aggregation.return_value": Mock( + **{ + "collect.side_effect": [ + Mock(), + Mock(), + None, + Mock(), + ] + } + ) + } + ) + } + ), + ) + + view_instrument_match.consume_measurement( + Measurement( + value=0, + instrument=Mock(name="instrument1"), + attributes={"c": "d", "f": "g"}, + ) + ) + view_instrument_match.consume_measurement( + Measurement( + value=0, + instrument=Mock(name="instrument1"), + attributes={"h": "i", "j": "k"}, + ) + ) + view_instrument_match.consume_measurement( + Measurement( + value=0, + instrument=Mock(name="instrument1"), + attributes={"l": "m", "n": "o"}, + ) + ) + view_instrument_match.consume_measurement( + Measurement( + value=0, + instrument=Mock(name="instrument1"), + attributes={"p": "q", "r": "s"}, + ) + ) + + result = view_instrument_match.collect( + AggregationTemporality.CUMULATIVE, 0 + ) + + self.assertEqual(len(list(result)), 3) + def test_setting_aggregation(self): instrument1 = Counter( name="instrument1", From 6e282d27e5a7fa337322dda154fe6eecf64380f0 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Sun, 12 Jun 2022 00:41:46 +0530 Subject: [PATCH 1245/1517] Allow `set_status` to accept the StatusCode and optional description (#2735) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/trace/span.py | 14 ++++++-- opentelemetry-api/tests/trace/test_globals.py | 2 +- .../src/opentelemetry/sdk/trace/__init__.py | 34 +++++++++++++----- opentelemetry-sdk/tests/trace/test_trace.py | 35 ++++++++++++++++++- 5 files changed, 74 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb8b44adb9..068fdf7bc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2728](https://github.com/open-telemetry/opentelemetry-python/pull/2728)) - fix: update entry point object references for metrics ([#2731](https://github.com/open-telemetry/opentelemetry-python/pull/2731)) +- Allow set_status to accept the StatusCode and optional description + ([#2735](https://github.com/open-telemetry/opentelemetry-python/pull/2735)) - Configure auto instrumentation to support metrics ([#2705](https://github.com/open-telemetry/opentelemetry-python/pull/2705)) - Add entrypoint for metrics exporter diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index cb9992557c..805b2b06b1 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -5,7 +5,7 @@ import typing from collections import OrderedDict -from opentelemetry.trace.status import Status +from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types # The key MUST begin with a lowercase letter or a digit, @@ -137,7 +137,11 @@ def is_recording(self) -> bool: """ @abc.abstractmethod - def set_status(self, status: Status) -> None: + def set_status( + self, + status: typing.Union[Status, StatusCode], + description: typing.Optional[str] = None, + ) -> None: """Sets the Status of the Span. If used, this will override the default Span status. """ @@ -524,7 +528,11 @@ def add_event( def update_name(self, name: str) -> None: pass - def set_status(self, status: Status) -> None: + def set_status( + self, + status: typing.Union[Status, StatusCode], + description: typing.Optional[str] = None, + ) -> None: pass def record_exception( diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 0ead559f86..a448437e98 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -12,7 +12,7 @@ class TestSpan(trace.NonRecordingSpan): recorded_exception = None recorded_status = Status(status_code=StatusCode.UNSET) - def set_status(self, status): + def set_status(self, status, description=None): self.recorded_status = status def end(self, end_time=None): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index f527967378..7dc65600f4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -889,16 +889,34 @@ def is_recording(self) -> bool: return self._end_time is None @_check_span_ended - def set_status(self, status: trace_api.Status) -> None: + def set_status( + self, + status: typing.Union[Status, StatusCode], + description: typing.Optional[str] = None, + ) -> None: # Ignore future calls if status is already set to OK # Ignore calls to set to StatusCode.UNSET - if ( - self._status - and self._status.status_code is StatusCode.OK - or status.status_code is StatusCode.UNSET - ): - return - self._status = status + if isinstance(status, Status): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status.status_code is StatusCode.UNSET + ): + return + if description is not None: + logger.warning( + "Description %s ignored. Use either `Status` or `(StatusCode, Description)`", + description, + ) + self._status = status + elif isinstance(status, StatusCode): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status is StatusCode.UNSET + ): + return + self._status = Status(status, description) def __exit__( self, diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index ad16578973..b89734db2f 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -45,7 +45,7 @@ get_span_with_dropped_attributes_events_links, new_tracer, ) -from opentelemetry.trace import StatusCode +from opentelemetry.trace import Status, StatusCode from opentelemetry.util._time import _time_ns @@ -903,6 +903,39 @@ def test_span_override_start_and_end_time(self): span.end(end_time) self.assertEqual(end_time, span.end_time) + def test_span_set_status(self): + + span1 = self.tracer.start_span("span1") + span1.set_status(Status(status_code=StatusCode.ERROR)) + self.assertEqual(span1.status.status_code, StatusCode.ERROR) + self.assertEqual(span1.status.description, None) + + span2 = self.tracer.start_span("span2") + span2.set_status( + Status(status_code=StatusCode.ERROR, description="desc") + ) + self.assertEqual(span2.status.status_code, StatusCode.ERROR) + self.assertEqual(span2.status.description, "desc") + + span3 = self.tracer.start_span("span3") + span3.set_status(StatusCode.ERROR) + self.assertEqual(span3.status.status_code, StatusCode.ERROR) + self.assertEqual(span3.status.description, None) + + span4 = self.tracer.start_span("span4") + span4.set_status(StatusCode.ERROR, "span4 desc") + self.assertEqual(span4.status.status_code, StatusCode.ERROR) + self.assertEqual(span4.status.description, "span4 desc") + + span5 = self.tracer.start_span("span5") + with self.assertLogs(level=WARNING): + span5.set_status( + Status(status_code=StatusCode.ERROR, description="desc"), + description="ignored", + ) + self.assertEqual(span5.status.status_code, StatusCode.ERROR) + self.assertEqual(span5.status.description, "desc") + def test_ended_span(self): """Events, attributes are not allowed after span is ended""" From e073d4dafaad42506c08d70727a8c9d47663ca23 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 13 Jun 2022 19:42:23 +0530 Subject: [PATCH 1246/1517] Add support for OTLP/HTTP log exporter (#2462) --- CHANGELOG.md | 4 +- .../otlp/proto/http/_log_exporter/__init__.py | 167 +++++++++ .../http/_log_exporter/encoder/__init__.py | 102 +++++ .../tests/test_proto_log_exporter.py | 354 ++++++++++++++++++ 4 files changed, 626 insertions(+), 1 deletion(-) create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 068fdf7bc5..9d449a57e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc1-0.31b0...HEAD) +- `opentelemetry-exporter-otlp-proto-http` Add support for OTLP/HTTP log exporter + ([#2462](https://github.com/open-telemetry/opentelemetry-python/pull/2462)) - Fix yield of `None`-valued points ([#2745](https://github.com/open-telemetry/opentelemetry-python/pull/2745)) - Add missing `to_json` methods @@ -115,7 +117,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 pages that have moved, see [#2453](https://github.com/open-telemetry/opentelemetry-python/pull/2453), and [#2498](https://github.com/open-telemetry/opentelemetry-python/pull/2498). -- `opentelemetry-exporter-otlp-grpc` update SDK dependency to ~1.9. +- `opentelemetry-exporter-otlp-proto-grpc` update SDK dependency to ~1.9. ([#2442](https://github.com/open-telemetry/opentelemetry-python/pull/2442)) - bugfix(auto-instrumentation): attach OTLPHandler to root logger ([#2450](https://github.com/open-telemetry/opentelemetry-python/pull/2450)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py new file mode 100644 index 0000000000..0cca699567 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -0,0 +1,167 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import gzip +import logging +import zlib +from io import BytesIO +from os import environ +from typing import Dict, Optional, Sequence +from time import sleep + +import requests +from backoff import expo + +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT, +) +from opentelemetry.sdk._logs.export import ( + LogExporter, + LogExportResult, + LogData, +) +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http._log_exporter.encoder import ( + _ProtobufEncoder, +) +from opentelemetry.util.re import parse_headers + + +_logger = logging.getLogger(__name__) + + +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_LOGS_EXPORT_PATH = "v1/logs" +DEFAULT_TIMEOUT = 10 # in seconds + + +class OTLPLogExporter(LogExporter): + + _MAX_RETRY_TIMEOUT = 64 + + def __init__( + self, + endpoint: Optional[str] = None, + certificate_file: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + timeout: Optional[int] = None, + compression: Optional[Compression] = None, + ): + self._endpoint = endpoint or _append_logs_path( + environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) + ) + self._certificate_file = certificate_file or environ.get( + OTEL_EXPORTER_OTLP_CERTIFICATE, True + ) + headers_string = environ.get(OTEL_EXPORTER_OTLP_HEADERS, "") + self._headers = headers or parse_headers(headers_string) + self._timeout = timeout or int( + environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT) + ) + self._compression = compression or _compression_from_env() + self._session = requests.Session() + self._session.headers.update(self._headers) + self._session.headers.update( + {"Content-Type": _ProtobufEncoder._CONTENT_TYPE} + ) + if self._compression is not Compression.NoCompression: + self._session.headers.update( + {"Content-Encoding": self._compression.value} + ) + self._shutdown = False + + def _export(self, serialized_data: str): + data = serialized_data + if self._compression == Compression.Gzip: + gzip_data = BytesIO() + with gzip.GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: + gzip_stream.write(serialized_data) + data = gzip_data.getvalue() + elif self._compression == Compression.Deflate: + data = zlib.compress(bytes(serialized_data)) + + return self._session.post( + url=self._endpoint, + data=data, + verify=self._certificate_file, + timeout=self._timeout, + ) + + @staticmethod + def _retryable(resp: requests.Response) -> bool: + if resp.status_code == 408: + return True + if resp.status_code >= 500 and resp.status_code <= 599: + return True + return False + + def export(self, batch: Sequence[LogData]) -> LogExportResult: + # After the call to Shutdown subsequent calls to Export are + # not allowed and should return a Failure result. + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring batch") + return LogExportResult.FAILURE + + serialized_data = _ProtobufEncoder.serialize(batch) + + for delay in expo(max_value=self._MAX_RETRY_TIMEOUT): + + if delay == self._MAX_RETRY_TIMEOUT: + return LogExportResult.FAILURE + + resp = self._export(serialized_data) + # pylint: disable=no-else-return + if resp.status_code in (200, 202): + return LogExportResult.SUCCESS + elif self._retryable(resp): + _logger.warning( + "Transient error %s encountered while exporting logs batch, retrying in %ss.", + resp.reason, + delay, + ) + sleep(delay) + continue + else: + _logger.error( + "Failed to export logs batch code: %s, reason: %s", + resp.status_code, + resp.text, + ) + return LogExportResult.FAILURE + return LogExportResult.FAILURE + + def shutdown(self): + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring call") + return + self._session.close() + self._shutdown = True + + +def _compression_from_env() -> Compression: + compression = ( + environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none").lower().strip() + ) + return Compression(compression) + + +def _append_logs_path(endpoint: str) -> str: + if endpoint.endswith("/"): + return endpoint + DEFAULT_LOGS_EXPORT_PATH + return endpoint + f"/{DEFAULT_LOGS_EXPORT_PATH}" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py new file mode 100644 index 0000000000..bf8784aacf --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py @@ -0,0 +1,102 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Sequence, List + +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, +) +from opentelemetry.proto.logs.v1.logs_pb2 import ( + ScopeLogs, + ResourceLogs, +) +from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord +from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import ( + _encode_instrumentation_scope, + _encode_resource, + _encode_span_id, + _encode_trace_id, + _encode_value, + _encode_attributes, +) + + +from opentelemetry.sdk._logs.export import LogData + + +class _ProtobufEncoder: + _CONTENT_TYPE = "application/x-protobuf" + + @classmethod + def serialize(cls, batch: Sequence[LogData]) -> str: + return cls.encode(batch).SerializeToString() + + @staticmethod + def encode(batch: Sequence[LogData]) -> ExportLogsServiceRequest: + return ExportLogsServiceRequest( + resource_logs=_encode_resource_logs(batch) + ) + + +def _encode_log(log_data: LogData) -> PB2LogRecord: + kwargs = {} + kwargs["time_unix_nano"] = log_data.log_record.timestamp + kwargs["span_id"] = _encode_span_id(log_data.log_record.span_id) + kwargs["trace_id"] = _encode_trace_id(log_data.log_record.trace_id) + kwargs["flags"] = int(log_data.log_record.trace_flags) + kwargs["body"] = _encode_value(log_data.log_record.body) + kwargs["severity_text"] = log_data.log_record.severity_text + kwargs["attributes"] = _encode_attributes(log_data.log_record.attributes) + kwargs["severity_number"] = log_data.log_record.severity_number.value + + return PB2LogRecord(**kwargs) + + +def _encode_resource_logs(batch: Sequence[LogData]) -> List[ResourceLogs]: + + sdk_resource_logs = {} + + for sdk_log in batch: + sdk_resource = sdk_log.log_record.resource + sdk_instrumentation = sdk_log.instrumentation_scope or None + pb2_log = _encode_log(sdk_log) + + if sdk_resource not in sdk_resource_logs.keys(): + sdk_resource_logs[sdk_resource] = {sdk_instrumentation: [pb2_log]} + elif sdk_instrumentation not in sdk_resource_logs[sdk_resource].keys(): + sdk_resource_logs[sdk_resource][sdk_instrumentation] = [pb2_log] + else: + sdk_resource_logs[sdk_resource][sdk_instrumentation].append( + pb2_log + ) + + pb2_resource_logs = [] + + for sdk_resource, sdk_instrumentations in sdk_resource_logs.items(): + scope_logs = [] + for sdk_instrumentation, pb2_logs in sdk_instrumentations.items(): + scope_logs.append( + ScopeLogs( + scope=(_encode_instrumentation_scope(sdk_instrumentation)), + log_records=pb2_logs, + ) + ) + pb2_resource_logs.append( + ResourceLogs( + resource=_encode_resource(sdk_resource), + scope_logs=scope_logs, + ) + ) + + return pb2_resource_logs diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py new file mode 100644 index 0000000000..13b20190da --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -0,0 +1,354 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access + +import unittest +from typing import List, Tuple +from unittest.mock import patch + +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http._log_exporter import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_LOGS_EXPORT_PATH, + DEFAULT_TIMEOUT, + OTLPLogExporter, +) +from opentelemetry.exporter.otlp.proto.http._log_exporter.encoder import ( + _encode_attributes, + _encode_span_id, + _encode_trace_id, + _encode_value, + _ProtobufEncoder, +) +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue +from opentelemetry.proto.common.v1.common_pb2 import ( + InstrumentationScope as PB2InstrumentationScope, +) +from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue +from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord +from opentelemetry.proto.logs.v1.logs_pb2 import ( + ResourceLogs as PB2ResourceLogs, +) +from opentelemetry.proto.logs.v1.logs_pb2 import ScopeLogs as PB2ScopeLogs +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as PB2Resource, +) +from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs import LogRecord as SDKLogRecord +from opentelemetry.sdk._logs.severity import SeverityNumber +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT, +) +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.trace import TraceFlags + +ENV_ENDPOINT = "http://localhost.env:8080/" +ENV_CERTIFICATE = "/etc/base.crt" +ENV_HEADERS = "envHeader1=val1,envHeader2=val2" +ENV_TIMEOUT = "30" + + +class TestOTLPHTTPLogExporter(unittest.TestCase): + def test_constructor_default(self): + + exporter = OTLPLogExporter() + + self.assertEqual( + exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_LOGS_EXPORT_PATH + ) + self.assertEqual(exporter._certificate_file, True) + self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) + self.assertIs(exporter._compression, DEFAULT_COMPRESSION) + self.assertEqual(exporter._headers, {}) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: ENV_TIMEOUT, + }, + ) + def test_exporter_constructor_take_priority(self): + exporter = OTLPLogExporter( + endpoint="endpoint.local:69/logs", + certificate_file="/hello.crt", + headers={"testHeader1": "value1", "testHeader2": "value2"}, + timeout=70, + compression=Compression.NoCompression, + ) + + self.assertEqual(exporter._endpoint, "endpoint.local:69/logs") + self.assertEqual(exporter._certificate_file, "/hello.crt") + self.assertEqual(exporter._timeout, 70) + self.assertIs(exporter._compression, Compression.NoCompression) + self.assertEqual( + exporter._headers, + {"testHeader1": "value1", "testHeader2": "value2"}, + ) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: ENV_TIMEOUT, + }, + ) + def test_exporter_env(self): + + exporter = OTLPLogExporter() + + self.assertEqual( + exporter._endpoint, ENV_ENDPOINT + DEFAULT_LOGS_EXPORT_PATH + ) + self.assertEqual(exporter._certificate_file, ENV_CERTIFICATE) + self.assertEqual(exporter._timeout, int(ENV_TIMEOUT)) + self.assertIs(exporter._compression, Compression.Gzip) + self.assertEqual( + exporter._headers, {"envheader1": "val1", "envheader2": "val2"} + ) + + def test_encode(self): + sdk_logs, expected_encoding = self.get_test_logs() + self.assertEqual( + _ProtobufEncoder().encode(sdk_logs), expected_encoding + ) + + def test_serialize(self): + sdk_logs, expected_encoding = self.get_test_logs() + self.assertEqual( + _ProtobufEncoder().serialize(sdk_logs), + expected_encoding.SerializeToString(), + ) + + def test_content_type(self): + self.assertEqual( + _ProtobufEncoder._CONTENT_TYPE, "application/x-protobuf" + ) + + @staticmethod + def _get_sdk_log_data() -> List[LogData]: + log1 = LogData( + log_record=SDKLogRecord( + timestamp=1644650195189786880, + trace_id=89564621134313219400156819398935297684, + span_id=1312458408527513268, + trace_flags=TraceFlags(0x01), + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Do not go gentle into that good night. Rage, rage against the dying of the light", + resource=SDKResource({"first_resource": "value"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_scope=InstrumentationScope( + "first_name", "first_version" + ), + ) + + log2 = LogData( + log_record=SDKLogRecord( + timestamp=1644650249738562048, + trace_id=0, + span_id=0, + trace_flags=TraceFlags.DEFAULT, + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Cooper, this is no time for caution!", + resource=SDKResource({"second_resource": "CASE"}), + attributes={}, + ), + instrumentation_scope=InstrumentationScope( + "second_name", "second_version" + ), + ) + + log3 = LogData( + log_record=SDKLogRecord( + timestamp=1644650427658989056, + trace_id=271615924622795969659406376515024083555, + span_id=4242561578944770265, + trace_flags=TraceFlags(0x01), + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG, + body="To our galaxy", + resource=SDKResource({"second_resource": "CASE"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_scope=None, + ) + + log4 = LogData( + log_record=SDKLogRecord( + timestamp=1644650584292683008, + trace_id=212592107417388365804938480559624925555, + span_id=6077757853989569223, + trace_flags=TraceFlags(0x01), + severity_text="INFO", + severity_number=SeverityNumber.INFO, + body="Love is the one thing that transcends time and space", + resource=SDKResource({"first_resource": "value"}), + attributes={"filename": "model.py", "func_name": "run_method"}, + ), + instrumentation_scope=InstrumentationScope( + "another_name", "another_version" + ), + ) + + return [log1, log2, log3, log4] + + def get_test_logs( + self, + ) -> Tuple[List[SDKLogRecord], ExportLogsServiceRequest]: + sdk_logs = self._get_sdk_log_data() + + pb2_service_request = ExportLogsServiceRequest( + resource_logs=[ + PB2ResourceLogs( + resource=PB2Resource( + attributes=[ + PB2KeyValue( + key="first_resource", + value=PB2AnyValue(string_value="value"), + ) + ] + ), + scope_logs=[ + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="first_name", version="first_version" + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650195189786880, + trace_id=_encode_trace_id( + 89564621134313219400156819398935297684 + ), + span_id=_encode_span_id( + 1312458408527513268 + ), + flags=int(TraceFlags(0x01)), + severity_text="WARN", + severity_number=SeverityNumber.WARN.value, + body=_encode_value( + "Do not go gentle into that good night. Rage, rage against the dying of the light" + ), + attributes=_encode_attributes( + {"a": 1, "b": "c"} + ), + ) + ], + ), + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="another_name", + version="another_version", + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650584292683008, + trace_id=_encode_trace_id( + 212592107417388365804938480559624925555 + ), + span_id=_encode_span_id( + 6077757853989569223 + ), + flags=int(TraceFlags(0x01)), + severity_text="INFO", + severity_number=SeverityNumber.INFO.value, + body=_encode_value( + "Love is the one thing that transcends time and space" + ), + attributes=_encode_attributes( + { + "filename": "model.py", + "func_name": "run_method", + } + ), + ) + ], + ), + ], + ), + PB2ResourceLogs( + resource=PB2Resource( + attributes=[ + PB2KeyValue( + key="second_resource", + value=PB2AnyValue(string_value="CASE"), + ) + ] + ), + scope_logs=[ + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="second_name", + version="second_version", + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650249738562048, + trace_id=_encode_trace_id(0), + span_id=_encode_span_id(0), + flags=int(TraceFlags.DEFAULT), + severity_text="WARN", + severity_number=SeverityNumber.WARN.value, + body=_encode_value( + "Cooper, this is no time for caution!" + ), + attributes={}, + ), + ], + ), + PB2ScopeLogs( + scope=PB2InstrumentationScope(), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650427658989056, + trace_id=_encode_trace_id( + 271615924622795969659406376515024083555 + ), + span_id=_encode_span_id( + 4242561578944770265 + ), + flags=int(TraceFlags(0x01)), + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG.value, + body=_encode_value("To our galaxy"), + attributes=_encode_attributes( + {"a": 1, "b": "c"} + ), + ), + ], + ), + ], + ), + ] + ) + + return sdk_logs, pb2_service_request From ca7a3fab278e8c8222b3a46d6e2e52af9dd5462f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 14 Jun 2022 02:12:02 +0100 Subject: [PATCH 1247/1517] Add min max fields to Histogram (#2759) --- CHANGELOG.md | 2 ++ .../otlp/proto/grpc/metric_exporter/__init__.py | 2 ++ .../tests/metrics/test_otlp_metrics_exporter.py | 10 ++++++++++ 3 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d449a57e2..cfc30d5886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc1-0.31b0...HEAD) +- Add min/max fields to Histogram + ([#2759](https://github.com/open-telemetry/opentelemetry-python/pull/2759)) - `opentelemetry-exporter-otlp-proto-http` Add support for OTLP/HTTP log exporter ([#2462](https://github.com/open-telemetry/opentelemetry-python/pull/2462)) - Fix yield of `None`-valued points diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index 83846da81f..c5f4acad06 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -146,6 +146,8 @@ def _translate_data( sum=data_point.sum, bucket_counts=data_point.bucket_counts, explicit_bounds=data_point.explicit_bounds, + max=data_point.max, + min=data_point.min, ) pb2_metric.histogram.aggregation_temporality = ( metric.data.aggregation_temporality diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index 0d4418030b..8936272bef 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -720,6 +720,8 @@ def test_translate_histogram(self): explicit_bounds=[10.0, 20.0], exemplars=[], flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, @@ -782,6 +784,8 @@ def test_translate_multiple_scope_histogram(self): explicit_bounds=[10.0, 20.0], exemplars=[], flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, @@ -816,6 +820,8 @@ def test_translate_multiple_scope_histogram(self): explicit_bounds=[10.0, 20.0], exemplars=[], flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, @@ -857,6 +863,8 @@ def test_translate_multiple_scope_histogram(self): explicit_bounds=[10.0, 20.0], exemplars=[], flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, @@ -898,6 +906,8 @@ def test_translate_multiple_scope_histogram(self): explicit_bounds=[10.0, 20.0], exemplars=[], flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, From b03b5ca6912e355ef81ef81ed340a3e8d014305e Mon Sep 17 00:00:00 2001 From: Brian Saville Date: Wed, 15 Jun 2022 16:15:56 -0600 Subject: [PATCH 1248/1517] Fix #2348, add handling for non recording span in jaeger propagator (#2762) * Fix #2348, add handling for non recording span in jaeger exporter * Fix pylint errors * Changes for black --- CHANGELOG.md | 2 ++ .../src/opentelemetry/propagators/jaeger/__init__.py | 5 ++++- .../tests/test_jaeger_propagator.py | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc30d5886..6f387f22f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2705](https://github.com/open-telemetry/opentelemetry-python/pull/2705)) - Add entrypoint for metrics exporter ([#2748](https://github.com/open-telemetry/opentelemetry-python/pull/2748)) +- Fix Jaeger propagator usage with NonRecordingSpan + ([#2762](https://github.com/open-telemetry/opentelemetry-python/pull/2762)) ## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py index 9589f97619..201d8bf3d3 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py @@ -81,7 +81,10 @@ def inject( if span_context == trace.INVALID_SPAN_CONTEXT: return - span_parent_id = span.parent.span_id if span.parent else 0 + # Non-recording spans do not have a parent + span_parent_id = ( + span.parent.span_id if span.is_recording() and span.parent else 0 + ) trace_flags = span_context.trace_flags if trace_flags.sampled: trace_flags |= self.DEBUG_FLAG diff --git a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py index f01e0e53da..81187389a1 100644 --- a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py @@ -230,3 +230,13 @@ def test_extract_invalid_uber_trace_id_header_to_implicit_ctx(self): ctx = FORMAT.extract(carrier) self.assertDictEqual(Context(), ctx) + + def test_non_recording_span_does_not_crash(self): + """Make sure propagator does not crash when working with NonRecordingSpan""" + mock_setter = Mock() + span = trace_api.NonRecordingSpan(trace_api.SpanContext(1, 1, True)) + with trace_api.use_span(span, end_on_exit=True): + try: + FORMAT.inject({}, setter=mock_setter) + except Exception as exc: # pylint: disable=broad-except + self.fail(f"Injecting failed for NonRecordingSpan with {exc}") From 537e235c1be88edfc86aebc1d47d1c8c67ced656 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 16 Jun 2022 22:36:29 +0530 Subject: [PATCH 1249/1517] TestBase and SpanTestBase are redundant (#2757) --- .../src/opentelemetry/test/asgitestutil.py | 4 +-- .../src/opentelemetry/test/spantestutil.py | 28 +------------------ .../src/opentelemetry/test/test_base.py | 13 +++++---- .../src/opentelemetry/test/wsgitestutil.py | 4 +-- 4 files changed, 13 insertions(+), 36 deletions(-) diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py index 7d23039bf3..05be4e0214 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py @@ -16,7 +16,7 @@ from asgiref.testing import ApplicationCommunicator -from opentelemetry.test.spantestutil import SpanTestBase +from opentelemetry.test.test_base import TestBase def setup_testing_defaults(scope): @@ -35,7 +35,7 @@ def setup_testing_defaults(scope): ) -class AsgiTestBase(SpanTestBase): +class AsgiTestBase(TestBase): def setUp(self): super().setUp() diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py index ea83b90b8d..912de9ee03 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py @@ -12,18 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from functools import partial -from importlib import reload from opentelemetry import trace as trace_api from opentelemetry.sdk import trace as trace_sdk -from opentelemetry.sdk.trace import Resource, TracerProvider, export -from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, -) - -_MEMORY_EXPORTER = None +from opentelemetry.sdk.trace import Resource def new_tracer(span_limits=None, resource=None) -> trace_api.Tracer: @@ -33,25 +26,6 @@ def new_tracer(span_limits=None, resource=None) -> trace_api.Tracer: return provider_factory(span_limits=span_limits).get_tracer(__name__) -class SpanTestBase(unittest.TestCase): - @classmethod - def setUpClass(cls): - global _MEMORY_EXPORTER # pylint:disable=global-statement - trace_api.set_tracer_provider(TracerProvider()) - tracer_provider = trace_api.get_tracer_provider() - _MEMORY_EXPORTER = InMemorySpanExporter() - span_processor = export.SimpleSpanProcessor(_MEMORY_EXPORTER) - tracer_provider.add_span_processor(span_processor) - - @classmethod - def tearDownClass(cls): - reload(trace_api) - - def setUp(self): - self.memory_exporter = _MEMORY_EXPORTER - self.memory_exporter.clear() - - def get_span_with_dropped_attributes_events_links(): attributes = {} for index in range(130): diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py index 98e820438d..b0a9f08fd5 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py @@ -43,11 +43,6 @@ def setUpClass(cls): reset_trace_globals() trace_api.set_tracer_provider(cls.tracer_provider) - result = cls.create_meter_provider() - cls.meter_provider, cls.memory_metrics_reader = result - reset_metrics_globals() - metrics_api.set_meter_provider(cls.meter_provider) - @classmethod def tearDownClass(cls): # This is done because set_tracer_provider cannot override the @@ -56,6 +51,14 @@ def tearDownClass(cls): def setUp(self): self.memory_exporter.clear() + # This is done because set_meter_provider cannot override the + # current meter provider. + reset_metrics_globals() + ( + self.meter_provider, + self.memory_metrics_reader, + ) = self.create_meter_provider() + metrics_api.set_meter_provider(self.meter_provider) def get_finished_spans(self): return FinishedTestSpans( diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py index aa2379a2b7..28a4c2698e 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py @@ -16,10 +16,10 @@ import wsgiref.util as wsgiref_util from opentelemetry import trace -from opentelemetry.test.spantestutil import SpanTestBase +from opentelemetry.test.test_base import TestBase -class WsgiTestBase(SpanTestBase): +class WsgiTestBase(TestBase): def setUp(self): super().setUp() From d4d7c67663cc22615748d632e1c8c5799e8eacae Mon Sep 17 00:00:00 2001 From: Carol Abadeer <60774943+carolabadeer@users.noreply.github.com> Date: Thu, 16 Jun 2022 14:20:47 -0700 Subject: [PATCH 1250/1517] Create Suppress HTTP Instrumentation key in opentelemetry context (#2729) --- CHANGELOG.md | 3 +++ opentelemetry-api/src/opentelemetry/context/__init__.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f387f22f9..40b62a40d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2726](https://github.com/open-telemetry/opentelemetry-python/pull/2726)) - fix: frozenset object has no attribute items ([#2727](https://github.com/open-telemetry/opentelemetry-python/pull/2727)) +- fix: create suppress HTTP instrumentation key in opentelemetry context + ([#2729](https://github.com/open-telemetry/opentelemetry-python/pull/2729)) - Support logs SDK auto instrumentation enable/disable with env ([#2728](https://github.com/open-telemetry/opentelemetry-python/pull/2728)) - fix: update entry point object references for metrics @@ -40,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix Jaeger propagator usage with NonRecordingSpan ([#2762](https://github.com/open-telemetry/opentelemetry-python/pull/2762)) + ## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 - Fix LoggingHandler to handle LogRecord with exc_info=False diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 7f56cdb216..388dc0fba9 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -163,3 +163,6 @@ def detach(token: object) -> None: # Once the decision around how to suppress instrumentation is made in the # spec, this key should be moved accordingly. _SUPPRESS_INSTRUMENTATION_KEY = create_key("suppress_instrumentation") +_SUPPRESS_HTTP_INSTRUMENTATION_KEY = create_key( + "suppress_http_instrumentation" +) From aa367302989978a1ae04badee7e3948502b96616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Tue, 21 Jun 2022 12:45:59 +0200 Subject: [PATCH 1251/1517] Python 3.11: Enhanced error locations in tracebacks (#2771) * Python 3.11: Enhanced error locations in tracebacks Expect ^^^^^^^^^ in tracebacks when testing them * Update opentelemetry-sdk/tests/trace/test_trace.py Co-authored-by: Srikanth Chekuri --- opentelemetry-sdk/tests/trace/test_trace.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index b89734db2f..890ae3e1e2 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -15,6 +15,7 @@ # pylint: disable=too-many-lines import shutil import subprocess +import sys import unittest from importlib import reload from logging import ERROR, WARNING @@ -1186,6 +1187,13 @@ def test_record_exception_context_manager(self): stacktrace = """in test_record_exception_context_manager raise RuntimeError("example error") RuntimeError: example error""" + if sys.version_info >= (3, 11): + # https://docs.python.org/3.11/whatsnew/3.11.html#enhanced-error-locations-in-tracebacks + tracelines = stacktrace.splitlines() + tracelines.insert( + -1, " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" + ) + stacktrace = "\n".join(tracelines) self.assertIn(stacktrace, event.attributes["exception.stacktrace"]) try: From 4d615015f1d5d636f211ce1f73e26e963ae60f48 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 24 Jun 2022 03:00:36 -0700 Subject: [PATCH 1252/1517] Update setup.cfg (#2777) --- exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index 2bc9200fc3..36a5d2b27e 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -42,7 +42,7 @@ package_dir= packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 - googleapis-common-protos ~= 1.52 + googleapis-common-protos ~= 1.52, < 1.56.3 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 From 71d2ab47661c2175d77a87b523e773d5fa10b9cb Mon Sep 17 00:00:00 2001 From: kasium <15907922+kasium@users.noreply.github.com> Date: Fri, 24 Jun 2022 12:42:07 +0200 Subject: [PATCH 1253/1517] Change log of detach error to exception (#2774) Co-authored-by: Diego Hurtado --- opentelemetry-api/src/opentelemetry/context/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 388dc0fba9..97ffcf8f72 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -156,7 +156,7 @@ def detach(token: object) -> None: try: _RUNTIME_CONTEXT.detach(token) # type: ignore except Exception: # pylint: disable=broad-except - logger.error("Failed to detach context") + logger.exception("Failed to detach context") # FIXME This is a temporary location for the suppress instrumentation key. From 25771ecdac685a5bf7ada1da21092d2061dbfc02 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 24 Jun 2022 17:46:00 +0200 Subject: [PATCH 1254/1517] Use always empty for None schema url value (#2764) * Use always None for empty schema url value Fixes #2750 * Revert "Use always None for empty schema url value" This reverts commit 8ef29dbd665c5c3ed812266452cb510081f50f16. * Use empty string as default value for schema_url --- .../src/opentelemetry/sdk/util/instrumentation.py | 4 ++++ opentelemetry-sdk/tests/trace/test_trace.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index 55d7c6277b..085d3fd874 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -35,6 +35,8 @@ def __init__( ): self._name = name self._version = version + if schema_url is None: + schema_url = "" self._schema_url = schema_url def __repr__(self): @@ -90,6 +92,8 @@ def __init__( ) -> None: self._name = name self._version = version + if schema_url is None: + schema_url = "" self._schema_url = schema_url def __repr__(self) -> str: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 890ae3e1e2..24d7b6fa3d 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -271,7 +271,7 @@ def test_invalid_instrumentation_info(self): ) span1 = tracer1.start_span("foo") self.assertTrue(span1.is_recording()) - self.assertEqual(tracer1.instrumentation_info.schema_url, None) + self.assertEqual(tracer1.instrumentation_info.schema_url, "") self.assertEqual(tracer1.instrumentation_info.version, "") self.assertEqual(tracer1.instrumentation_info.name, "") @@ -280,7 +280,7 @@ def test_invalid_instrumentation_info(self): ) span2 = tracer2.start_span("bar") self.assertTrue(span2.is_recording()) - self.assertEqual(tracer2.instrumentation_info.schema_url, None) + self.assertEqual(tracer2.instrumentation_info.schema_url, "") self.assertEqual(tracer2.instrumentation_info.version, "") self.assertEqual(tracer2.instrumentation_info.name, "") From ef9847f5effb81368ce4030002ca42cb27504be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 29 Jun 2022 11:50:57 +0200 Subject: [PATCH 1255/1517] Propagate API module & propagators package missing from API docs (#2785) * Propagate API module missing from docs. * Add CHANGELOG. * Sphinx complains about more missing classes. * More fixes, understood why Sphinx didn't find some link targets. * Update CHANGELOG. --- CHANGELOG.md | 3 +++ docs/api/index.rst | 2 ++ docs/api/propagate.rst | 7 +++++++ docs/api/propagators.composite.rst | 7 +++++++ docs/api/propagators.rst | 10 ++++++++++ docs/api/propagators.textmap.rst | 7 +++++++ docs/conf.py | 20 +++----------------- 7 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 docs/api/propagate.rst create mode 100644 docs/api/propagators.composite.rst create mode 100644 docs/api/propagators.rst create mode 100644 docs/api/propagators.textmap.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 40b62a40d0..2aa395855a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2748](https://github.com/open-telemetry/opentelemetry-python/pull/2748)) - Fix Jaeger propagator usage with NonRecordingSpan ([#2762](https://github.com/open-telemetry/opentelemetry-python/pull/2762)) +- Add `opentelemetry.propagate` module and `opentelemetry.propagators` package + to the API reference documentation + ([#2785](https://github.com/open-telemetry/opentelemetry-python/pull/2785)) ## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 diff --git a/docs/api/index.rst b/docs/api/index.rst index a13c9e698b..22d77fc5a0 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -8,6 +8,8 @@ OpenTelemetry Python API baggage context + propagate + propagators trace metrics environment_variables diff --git a/docs/api/propagate.rst b/docs/api/propagate.rst new file mode 100644 index 0000000000..a86beeaddc --- /dev/null +++ b/docs/api/propagate.rst @@ -0,0 +1,7 @@ +opentelemetry.propagate package +======================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.propagate diff --git a/docs/api/propagators.composite.rst b/docs/api/propagators.composite.rst new file mode 100644 index 0000000000..930ca0b88d --- /dev/null +++ b/docs/api/propagators.composite.rst @@ -0,0 +1,7 @@ +opentelemetry.propagators.composite +==================================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.propagators.composite diff --git a/docs/api/propagators.rst b/docs/api/propagators.rst new file mode 100644 index 0000000000..08825315be --- /dev/null +++ b/docs/api/propagators.rst @@ -0,0 +1,10 @@ +opentelemetry.propagators package +======================================== + +Subpackages +----------- + +.. toctree:: + + propagators.textmap + propagators.composite diff --git a/docs/api/propagators.textmap.rst b/docs/api/propagators.textmap.rst new file mode 100644 index 0000000000..a5db537b80 --- /dev/null +++ b/docs/api/propagators.textmap.rst @@ -0,0 +1,7 @@ +opentelemetry.propagators.textmap +==================================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.propagators.textmap diff --git a/docs/conf.py b/docs/conf.py index 55b1af7331..6e42aa1bd1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -97,6 +97,9 @@ # https://github.com/sphinx-doc/sphinx/pull/3744 nitpick_ignore = [ ("py:class", "ValueT"), + ("py:class", "CarrierT"), + ("py:obj", "opentelemetry.propagators.textmap.CarrierT"), + ("py:obj", "Union"), ( "py:class", "opentelemetry.sdk.metrics._internal.instrument._Synchronous", @@ -112,23 +115,6 @@ "py:class", "opentelemetry.trace._LinkBase", ), - # TODO: Understand why sphinx is not able to find this local class - ( - "py:class", - "opentelemetry.propagators.textmap.TextMapPropagator", - ), - ( - "py:class", - "opentelemetry.propagators.textmap.DefaultGetter", - ), - ( - "any", - "opentelemetry.propagators.textmap.TextMapPropagator.extract", - ), - ( - "any", - "opentelemetry.propagators.textmap.TextMapPropagator.inject", - ), ] # Add any paths that contain templates here, relative to this directory. From af09df390680829fe366cefee367a19a0d4557f1 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 30 Jun 2022 19:27:41 +0200 Subject: [PATCH 1256/1517] Allow git to check out files with long names in Windows (#2790) * Allow git to check out files with long names in Windows Fixes #2789 * Update contrib SHA --- .github/workflows/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd6261b8d4..78619460a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 7b42e4354dc3244ef2878bfd0d7d4c80d25cba0a + CONTRIB_REPO_SHA: ac84e9968fc5bfb16016a4e0ca82059bf1e86511 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when @@ -60,6 +60,9 @@ jobs: if: ${{ matrix.os == 'windows-2019' && matrix.python-version == 'py36' }} shell: pwsh run: Remove-Item .\.tox\ -Force -Recurse -ErrorAction Ignore + - name: Windows does not let git check out files with long names + if: ${{ matrix.os == 'windows-2019'}} + run: git config --system core.longpaths true - name: run tox run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json - name: Find and merge benchmarks From f7a3e6277703c1dd06659d0b30e3fe92dce8555b Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 30 Jun 2022 23:00:20 +0200 Subject: [PATCH 1257/1517] Verify previous point is returned for cumulative instruments (#2773) * Verify previous point is returned for cumulative instruments Fixes #2755 * Reject non-cumulative temporalities for Prometheus metric reader * Add test case for InMemoryMetricReader * Fix prometheus import paths * Remove parameters for Prometheus init * Add sleep for windows tests * Add temporality overriding dictionary --- .../exporter/prometheus/__init__.py | 21 +++++++- .../tests/test_prometheus_exporter.py | 16 +++++- .../metrics/test_in_memory_metric_reader.py | 50 ++++++++++++++++++- 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index ac1ea58c66..dbf4493f9f 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -77,7 +77,16 @@ ) from prometheus_client.core import Metric as PrometheusMetric +from opentelemetry.sdk.metrics import Counter +from opentelemetry.sdk.metrics import Histogram as HistogramInstrument +from opentelemetry.sdk.metrics import ( + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, Gauge, Histogram, HistogramDataPoint, @@ -113,7 +122,17 @@ class PrometheusMetricReader(MetricReader): """ def __init__(self, prefix: str = "") -> None: - super().__init__() + + super().__init__( + preferred_temporality={ + Counter: AggregationTemporality.CUMULATIVE, + UpDownCounter: AggregationTemporality.CUMULATIVE, + HistogramInstrument: AggregationTemporality.CUMULATIVE, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + ) self._collector = _CustomCollector(prefix) REGISTRY.register(self._collector) self._collector._callback = self.collect diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 4b6118bdab..9e7f143e01 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -23,6 +23,7 @@ PrometheusMetricReader, _CustomCollector, ) +from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Histogram, @@ -51,7 +52,7 @@ def setUp(self): def test_constructor(self): """Test the constructor.""" with self._registry_register_patch: - exporter = PrometheusMetricReader("testprefix") + exporter = PrometheusMetricReader(prefix="testprefix") self.assertEqual(exporter._collector._prefix, "testprefix") self.assertTrue(self._mock_registry_register.called) @@ -286,3 +287,16 @@ def test_check_value(self): self.assertEqual(collector._check_value(True), "true") self.assertEqual(collector._check_value(False), "false") self.assertEqual(collector._check_value(None), "null") + + def test_multiple_collection_calls(self): + + metric_reader = PrometheusMetricReader(prefix="prefix") + provider = MeterProvider(metric_readers=[metric_reader]) + meter = provider.get_meter("getting-started", "0.1.2") + counter = meter.create_counter("counter") + counter.add(1) + result_0 = list(metric_reader._collector.collect()) + result_1 = list(metric_reader._collector.collect()) + result_2 = list(metric_reader._collector.collect()) + self.assertEqual(result_0, result_1) + self.assertEqual(result_1, result_2) diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py index c32acc7aac..68c81e8b7e 100644 --- a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from time import sleep from unittest import TestCase from unittest.mock import Mock from opentelemetry.metrics import Observation -from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, @@ -106,3 +107,50 @@ def test_integration(self): ), 1, ) + + def test_cumulative_multiple_collect(self): + + reader = InMemoryMetricReader( + preferred_temporality={Counter: AggregationTemporality.CUMULATIVE} + ) + meter = MeterProvider(metric_readers=[reader]).get_meter("test_meter") + counter = meter.create_counter("counter1") + counter.add(1, attributes={"key": "value"}) + + reader.collect() + + number_data_point_0 = list( + reader._metrics_data.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points + )[0] + + # Windows tests fail without this sleep because both time_unix_nano + # values are the same. + sleep(0.1) + reader.collect() + + number_data_point_1 = list( + reader._metrics_data.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points + )[0] + + self.assertEqual( + number_data_point_0.attributes, number_data_point_1.attributes + ) + self.assertEqual( + number_data_point_0.start_time_unix_nano, + number_data_point_1.start_time_unix_nano, + ) + self.assertEqual( + number_data_point_0.start_time_unix_nano, + number_data_point_1.start_time_unix_nano, + ) + self.assertEqual(number_data_point_0.value, number_data_point_1.value) + self.assertGreater( + number_data_point_1.time_unix_nano, + number_data_point_0.time_unix_nano, + ) From 05906171301bd5d6852bbdb2046f4ad12646f322 Mon Sep 17 00:00:00 2001 From: Ron Nathaniel <45116635+ronnathaniel@users.noreply.github.com> Date: Fri, 1 Jul 2022 09:34:18 -0400 Subject: [PATCH 1258/1517] Adding optional session parameter to Trace Exporters constructors (#2783) --- CHANGELOG.md | 2 ++ .../otlp/proto/http/_log_exporter/__init__.py | 3 ++- .../otlp/proto/http/trace_exporter/__init__.py | 3 ++- .../tests/test_proto_log_exporter.py | 9 ++++++++- .../tests/test_proto_span_exporter.py | 6 ++++++ .../opentelemetry/exporter/zipkin/json/__init__.py | 11 ++++++++--- .../tests/test_zipkin_exporter.py | 7 +++++++ .../exporter/zipkin/proto/http/__init__.py | 11 ++++++++--- .../tests/test_zipkin_exporter.py | 7 +++++++ 9 files changed, 50 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aa395855a..51135293b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc1-0.31b0...HEAD) +- Add optional sessions parameter to all Exporters leveraging requests.Session +([#2783](https://github.com/open-telemetry/opentelemetry-python/pull/2783)) - Add min/max fields to Histogram ([#2759](https://github.com/open-telemetry/opentelemetry-python/pull/2759)) - `opentelemetry-exporter-otlp-proto-http` Add support for OTLP/HTTP log exporter diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 0cca699567..041f1ab3c0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -62,6 +62,7 @@ def __init__( headers: Optional[Dict[str, str]] = None, timeout: Optional[int] = None, compression: Optional[Compression] = None, + session: Optional[requests.Session] = None, ): self._endpoint = endpoint or _append_logs_path( environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) @@ -75,7 +76,7 @@ def __init__( environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT) ) self._compression = compression or _compression_from_env() - self._session = requests.Session() + self._session = session or requests.Session() self._session.headers.update(self._headers) self._session.headers.update( {"Content-Type": _ProtobufEncoder._CONTENT_TYPE} diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 156afc247d..6f0d6ee58d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -63,6 +63,7 @@ def __init__( headers: Optional[Dict[str, str]] = None, timeout: Optional[int] = None, compression: Optional[Compression] = None, + session: Optional[requests.Session] = None, ): self._endpoint = endpoint or environ.get( OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, @@ -86,7 +87,7 @@ def __init__( ) ) self._compression = compression or _compression_from_env() - self._session = requests.Session() + self._session = session or requests.Session() self._session.headers.update(self._headers) self._session.headers.update( {"Content-Type": _ProtobufEncoder._CONTENT_TYPE} diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index 13b20190da..d5e34b7463 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -16,7 +16,9 @@ import unittest from typing import List, Tuple -from unittest.mock import patch +from unittest.mock import MagicMock, patch + +import requests from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._log_exporter import ( @@ -81,6 +83,7 @@ def test_constructor_default(self): self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) self.assertIs(exporter._compression, DEFAULT_COMPRESSION) self.assertEqual(exporter._headers, {}) + self.assertIsInstance(exporter._session, requests.Session) @patch.dict( "os.environ", @@ -93,12 +96,14 @@ def test_constructor_default(self): }, ) def test_exporter_constructor_take_priority(self): + sess = MagicMock() exporter = OTLPLogExporter( endpoint="endpoint.local:69/logs", certificate_file="/hello.crt", headers={"testHeader1": "value1", "testHeader2": "value2"}, timeout=70, compression=Compression.NoCompression, + session=sess(), ) self.assertEqual(exporter._endpoint, "endpoint.local:69/logs") @@ -109,6 +114,7 @@ def test_exporter_constructor_take_priority(self): exporter._headers, {"testHeader1": "value1", "testHeader2": "value2"}, ) + self.assertTrue(sess.called) @patch.dict( "os.environ", @@ -133,6 +139,7 @@ def test_exporter_env(self): self.assertEqual( exporter._headers, {"envheader1": "val1", "envheader2": "val2"} ) + self.assertIsInstance(exporter._session, requests.Session) def test_encode(self): sdk_logs, expected_encoding = self.get_test_logs() diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index e3cd204626..4eb0db6160 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -15,6 +15,8 @@ import unittest from unittest.mock import patch +import requests + from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( DEFAULT_COMPRESSION, @@ -55,6 +57,7 @@ def test_constructor_default(self): self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) self.assertIs(exporter._compression, DEFAULT_COMPRESSION) self.assertEqual(exporter._headers, {}) + self.assertIsInstance(exporter._session, requests.Session) @patch.dict( "os.environ", @@ -86,6 +89,7 @@ def test_exporter_traces_env_take_priority(self): "traceenv3": "==val3==", }, ) + self.assertIsInstance(exporter._session, requests.Session) @patch.dict( "os.environ", @@ -105,6 +109,7 @@ def test_exporter_constructor_take_priority(self): headers={"testHeader1": "value1", "testHeader2": "value2"}, timeout=20, compression=Compression.NoCompression, + session=requests.Session(), ) self.assertEqual(exporter._endpoint, "example.com/1234") @@ -115,6 +120,7 @@ def test_exporter_constructor_take_priority(self): exporter._headers, {"testHeader1": "value1", "testHeader2": "value2"}, ) + self.assertIsInstance(exporter._session, requests.Session) @patch.dict( "os.environ", diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py index 0e0642d0be..7728090f54 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py @@ -31,6 +31,8 @@ .. code:: python + import requests + from opentelemetry import trace from opentelemetry.exporter.zipkin.json import ZipkinExporter from opentelemetry.sdk.trace import TracerProvider @@ -47,8 +49,9 @@ # local_node_ipv4="192.168.0.1", # local_node_ipv6="2001:db8::c001", # local_node_port=31313, - # max_tag_value_length=256 - # timeout=5 (in seconds) + # max_tag_value_length=256, + # timeout=5 (in seconds), + # session=requests.Session(), ) # Create a BatchSpanProcessor and add the exporter to it @@ -103,6 +106,7 @@ def __init__( local_node_port: Optional[int] = None, max_tag_value_length: Optional[int] = None, timeout: Optional[int] = None, + session: Optional[requests.Session] = None, ): """Zipkin exporter. @@ -116,6 +120,7 @@ def __init__( max_tag_value_length: Max length string attribute values can have. timeout: Maximum time the Zipkin exporter will wait for each batch export. The default value is 10s. + session: Connection session to the Zipkin collector endpoint. The tuple (local_node_ipv4, local_node_ipv6, local_node_port) is used to represent the network context of a node in the service graph. @@ -135,7 +140,7 @@ def __init__( elif version == Protocol.V2: self.encoder = JsonV2Encoder(max_tag_value_length) - self.session = requests.Session() + self.session = session or requests.Session() self.session.headers.update( {"Content-Type": self.encoder.content_type()} ) diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py index 5c2aa0cbe6..77e3ef5375 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py @@ -17,6 +17,8 @@ import unittest from unittest.mock import patch +import requests + from opentelemetry import trace from opentelemetry.exporter.zipkin.encoder import Protocol from opentelemetry.exporter.zipkin.json import DEFAULT_ENDPOINT, ZipkinExporter @@ -55,6 +57,7 @@ def tearDown(self): def test_constructor_default(self): exporter = ZipkinExporter() self.assertIsInstance(exporter.encoder, JsonV2Encoder) + self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, DEFAULT_ENDPOINT) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) @@ -83,6 +86,7 @@ def test_constructor_protocol_endpoint(self): exporter = ZipkinExporter(endpoint=endpoint) self.assertIsInstance(exporter.encoder, JsonV2Encoder) + self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) @@ -104,6 +108,7 @@ def test_constructor_all_params_and_env_vars(self): local_node_port = 30301 max_tag_value_length = 56 timeout_param = 20 + session_param = requests.Session() exporter = ZipkinExporter( constructor_param_version, @@ -113,9 +118,11 @@ def test_constructor_all_params_and_env_vars(self): local_node_port, max_tag_value_length, timeout_param, + session_param, ) self.assertIsInstance(exporter.encoder, JsonV2Encoder) + self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, constructor_param_endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual( diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py index bd98a1ff06..5856cd7e4e 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py @@ -31,6 +31,8 @@ .. code:: python + import requests + from opentelemetry import trace from opentelemetry.exporter.zipkin.proto.http import ZipkinExporter from opentelemetry.sdk.trace import TracerProvider @@ -46,8 +48,9 @@ # local_node_ipv4="192.168.0.1", # local_node_ipv6="2001:db8::c001", # local_node_port=31313, - # max_tag_value_length=256 - # timeout=5 (in seconds) + # max_tag_value_length=256, + # timeout=5 (in seconds), + # session=requests.Session() ) # Create a BatchSpanProcessor and add the exporter to it @@ -99,6 +102,7 @@ def __init__( local_node_port: Optional[int] = None, max_tag_value_length: Optional[int] = None, timeout: Optional[int] = None, + session: Optional[requests.Session] = None, ): """Zipkin exporter. @@ -112,6 +116,7 @@ def __init__( max_tag_value_length: Max length string attribute values can have. timeout: Maximum time the Zipkin exporter will wait for each batch export. The default value is 10s. + session: Connection session to the Zipkin collector endpoint. The tuple (local_node_ipv4, local_node_ipv6, local_node_port) is used to represent the network context of a node in the service graph. @@ -128,7 +133,7 @@ def __init__( self.encoder = ProtobufEncoder(max_tag_value_length) - self.session = requests.Session() + self.session = session or requests.Session() self.session.headers.update( {"Content-Type": self.encoder.content_type()} ) diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py index 8b8b01438e..8a3c055437 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py @@ -17,6 +17,8 @@ import unittest from unittest.mock import patch +import requests + from opentelemetry import trace from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.exporter.zipkin.proto.http import ( @@ -57,6 +59,7 @@ def tearDown(self): def test_constructor_default(self): exporter = ZipkinExporter() self.assertIsInstance(exporter.encoder, ProtobufEncoder) + self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, DEFAULT_ENDPOINT) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) @@ -85,6 +88,7 @@ def test_constructor_protocol_endpoint(self): exporter = ZipkinExporter(endpoint) self.assertIsInstance(exporter.encoder, ProtobufEncoder) + self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) @@ -105,6 +109,7 @@ def test_constructor_all_params_and_env_vars(self): local_node_port = 30301 max_tag_value_length = 56 timeout_param = 20 + session_param = requests.Session() exporter = ZipkinExporter( constructor_param_endpoint, @@ -113,9 +118,11 @@ def test_constructor_all_params_and_env_vars(self): local_node_port, max_tag_value_length, timeout_param, + session_param, ) self.assertIsInstance(exporter.encoder, ProtobufEncoder) + self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, constructor_param_endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual( From e27f2b866bd840623bb128a1c04296c52b35b919 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 4 Jul 2022 12:23:09 +0200 Subject: [PATCH 1259/1517] Fix unit and name regexes (#2796) * Fix unit and name regexes Fixes #2793 * Add changelog entry --- CHANGELOG.md | 4 +++- .../src/opentelemetry/metrics/_internal/instrument.py | 5 ++--- opentelemetry-api/tests/metrics/test_instruments.py | 3 +++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51135293b7..22b479fc49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc1-0.31b0...HEAD) +- Fix instrument name and unit regexes + ([#2796](https://github.com/open-telemetry/opentelemetry-python/pull/2796)) - Add optional sessions parameter to all Exporters leveraging requests.Session -([#2783](https://github.com/open-telemetry/opentelemetry-python/pull/2783)) + ([#2783](https://github.com/open-telemetry/opentelemetry-python/pull/2783)) - Add min/max fields to Histogram ([#2759](https://github.com/open-telemetry/opentelemetry-python/pull/2759)) - `opentelemetry-exporter-otlp-proto-http` Add support for OTLP/HTTP log exporter diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py index f98cbd7243..b203b8ffe7 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py @@ -18,7 +18,6 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from logging import getLogger -from re import ASCII from re import compile as re_compile from typing import ( Callable, @@ -39,8 +38,8 @@ _logger = getLogger(__name__) -_name_regex = re_compile(r"[a-zA-Z][-.\w]{0,62}", ASCII) -_unit_regex = re_compile(r"\w{0,63}", ASCII) +_name_regex = re_compile(r"[a-zA-Z][-_.a-zA-Z0-9]{0,62}") +_unit_regex = re_compile(r"[\x00-\x7F]{0,63}") @dataclass(frozen=True) diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index ff2ab2b3e5..3e1e3fe745 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -571,6 +571,7 @@ def test_name_regex(self): self.assertTrue(instrument._check_name_and_unit("a.", "unit")[0]) self.assertTrue(instrument._check_name_and_unit("a-", "unit")[0]) self.assertTrue(instrument._check_name_and_unit("a_", "unit")[0]) + self.assertFalse(instrument._check_name_and_unit("a" * 64, "unit")[0]) self.assertFalse(instrument._check_name_and_unit("Ñ", "unit")[0]) self.assertFalse(instrument._check_name_and_unit("_a", "unit")[0]) @@ -582,5 +583,7 @@ def test_unit_regex(self): instrument = ChildInstrument("name") self.assertTrue(instrument._check_name_and_unit("name", "a" * 63)[1]) + self.assertTrue(instrument._check_name_and_unit("name", "{a}")[1]) + self.assertFalse(instrument._check_name_and_unit("name", "a" * 64)[1]) self.assertFalse(instrument._check_name_and_unit("name", "Ñ")[1]) From b9a6358583c0676482a043279c01b252dc6c308e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 4 Jul 2022 15:14:46 +0200 Subject: [PATCH 1260/1517] Release 1.12.0rc2-0.32b0 (#2795) * Release 1.12.0rc2-0.32b0 * Update CHANGELOG.md Co-authored-by: Srikanth Chekuri Co-authored-by: Srikanth Chekuri --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 6 +++++- eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/setup.cfg | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 31 files changed, 41 insertions(+), 37 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 78619460a2..3f2566451b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: ac84e9968fc5bfb16016a4e0ca82059bf1e86511 + CONTRIB_REPO_SHA: 42ff80bef8a03ff214a54323a2631da06e6dc5e4 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 22b479fc49..2bb41eee5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc1-0.31b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc2-0.32b0...HEAD) + +## [1.12.0rc2-0.32b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc2-0.32b0) - 2022-07-04 + + - Fix instrument name and unit regexes ([#2796](https://github.com/open-telemetry/opentelemetry-python/pull/2796)) diff --git a/eachdist.ini b/eachdist.ini index ebf0088042..dca92043e9 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.12.0rc1 +version=1.12.0rc2 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.31b0 +version=0.32b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 8307312152..414738e76e 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 8307312152..414738e76e 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 0962901163..14818cc366 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.12.0rc1 - opentelemetry-exporter-jaeger-thrift == 1.12.0rc1 + opentelemetry-exporter-jaeger-proto-grpc == 1.12.0rc2 + opentelemetry-exporter-jaeger-thrift == 1.12.0rc2 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 8307312152..414738e76e 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index d8dc1e1ed7..268a795344 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.31b0" +__version__ = "0.32b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 3c35fef771..a98639cf68 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 - opentelemetry-proto == 1.12.0rc1 + opentelemetry-proto == 1.12.0rc2 backoff >= 1.10.0, < 2.0.0; python_version<'3.7' backoff >= 1.10.0, < 3.0.0; python_version>='3.7' diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 54a13456e7..5744f723da 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index e434fb794e..61d29f78b7 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 - opentelemetry-proto == 1.12.0rc1 + opentelemetry-proto == 1.12.0rc2 backoff >= 1.10.0, < 2.0.0; python_version<'3.7' backoff >= 1.10.0, < 3.0.0; python_version>='3.7' diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 54a13456e7..5744f723da 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 6157365001..0dcd327d60 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.12.0rc1 - opentelemetry-exporter-otlp-proto-http == 1.12.0rc1 + opentelemetry-exporter-otlp-proto-grpc == 1.12.0rc2 + opentelemetry-exporter-otlp-proto-http == 1.12.0rc2 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 54a13456e7..5744f723da 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 54a13456e7..268a795344 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "0.32b0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 54a13456e7..5744f723da 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 70cbd52b13..fcd9d37c96 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 - opentelemetry-exporter-zipkin-json == 1.12.0rc1 + opentelemetry-exporter-zipkin-json == 1.12.0rc2 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 54a13456e7..5744f723da 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 98589854ee..949b0dfb3a 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.12.0rc1 - opentelemetry-exporter-zipkin-proto-http == 1.12.0rc1 + opentelemetry-exporter-zipkin-json == 1.12.0rc2 + opentelemetry-exporter-zipkin-proto-http == 1.12.0rc2 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 54a13456e7..5744f723da 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 54a13456e7..5744f723da 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 54a13456e7..5744f723da 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 7ee57a00a3..b1c9ed9c1b 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,8 +43,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.12.0rc1 - opentelemetry-semantic-conventions == 0.31b0 + opentelemetry-api == 1.12.0rc2 + opentelemetry-semantic-conventions == 0.32b0 setuptools >= 16.0 dataclasses == 0.8; python_version < '3.7' typing-extensions >= 3.7.4 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 54a13456e7..5744f723da 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index d8dc1e1ed7..268a795344 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.31b0" +__version__ = "0.32b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 54a13456e7..5744f723da 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 54a13456e7..5744f723da 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 367586ed83..14f45de7f0 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -47,7 +47,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test-utils == 0.31b0 + opentelemetry-test-utils == 0.32b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index d8dc1e1ed7..268a795344 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.31b0" +__version__ = "0.32b0" diff --git a/tests/opentelemetry-test-utils/setup.cfg b/tests/opentelemetry-test-utils/setup.cfg index 4b8f20e118..659778feba 100644 --- a/tests/opentelemetry-test-utils/setup.cfg +++ b/tests/opentelemetry-test-utils/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.12.0rc1 - opentelemetry-sdk == 1.12.0rc1 + opentelemetry-api == 1.12.0rc2 + opentelemetry-sdk == 1.12.0rc2 asgiref ~= 3.0 [options.extras_require] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 8ae7aaafbe..6a7b29001a 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.31b0" +__version__ = "0.32b0" From dbcec9f3b143e4de424c16be9ecd19a16be3e630 Mon Sep 17 00:00:00 2001 From: oceyral Date: Wed, 6 Jul 2022 18:45:57 +0200 Subject: [PATCH 1261/1517] Fix tracing decorator with late configuration (#2754) --- CHANGELOG.md | 7 ++++- .../src/opentelemetry/trace/__init__.py | 6 ++-- opentelemetry-api/tests/trace/test_proxy.py | 30 ++++++++++++++++++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb41eee5e..2d874d0b4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc2-0.32b0...HEAD) + +- Fix tracing decorator with late configuration + ([#2754](https://github.com/open-telemetry/opentelemetry-python/pull/2754)) + + ## [1.12.0rc2-0.32b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc2-0.32b0) - 2022-07-04 @@ -14,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix instrument name and unit regexes ([#2796](https://github.com/open-telemetry/opentelemetry-python/pull/2796)) - Add optional sessions parameter to all Exporters leveraging requests.Session - ([#2783](https://github.com/open-telemetry/opentelemetry-python/pull/2783)) + ([#2783](https://github.com/open-telemetry/opentelemetry-python/pull/2783) - Add min/max fields to Histogram ([#2759](https://github.com/open-telemetry/opentelemetry-python/pull/2759)) - `opentelemetry-exporter-otlp-proto-http` Add support for OTLP/HTTP log exporter diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index be0d8933b7..53bb40f0e2 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -434,8 +434,10 @@ def _tracer(self) -> Tracer: def start_span(self, *args, **kwargs) -> Span: # type: ignore return self._tracer.start_span(*args, **kwargs) # type: ignore - def start_as_current_span(self, *args, **kwargs) -> Span: # type: ignore - return self._tracer.start_as_current_span(*args, **kwargs) # type: ignore + @contextmanager # type: ignore + def start_as_current_span(self, *args, **kwargs) -> Iterator[Span]: # type: ignore + with self._tracer.start_as_current_span(*args, **kwargs) as span: # type: ignore + yield span class NoOpTracer(Tracer): diff --git a/opentelemetry-api/tests/trace/test_proxy.py b/opentelemetry-api/tests/trace/test_proxy.py index b361540b9d..e48a2157ae 100644 --- a/opentelemetry-api/tests/trace/test_proxy.py +++ b/opentelemetry-api/tests/trace/test_proxy.py @@ -15,10 +15,15 @@ # pylint: disable=W0212,W0222,W0221 import typing import unittest +from contextlib import contextmanager from opentelemetry import trace from opentelemetry.test.globals_test import TraceGlobalsTest -from opentelemetry.trace.span import INVALID_SPAN_CONTEXT, NonRecordingSpan +from opentelemetry.trace.span import ( + INVALID_SPAN_CONTEXT, + NonRecordingSpan, + Span, +) class TestProvider(trace.NoOpTracerProvider): @@ -35,6 +40,11 @@ class TestTracer(trace.NoOpTracer): def start_span(self, *args, **kwargs): return TestSpan(INVALID_SPAN_CONTEXT) + @contextmanager + def start_as_current_span(self, *args, **kwargs): # type: ignore + with trace.use_span(self.start_span(*args, **kwargs)) as span: # type: ignore + yield span + class TestSpan(NonRecordingSpan): pass @@ -73,3 +83,21 @@ def test_proxy_tracer(self): # creates real spans with tracer.start_span("") as span: self.assertIsInstance(span, TestSpan) + + def test_late_config(self): + # get a tracer and instrument a function as we would at the + # root of a module + tracer = trace.get_tracer("test") + + @tracer.start_as_current_span("span") + def my_function() -> Span: + return trace.get_current_span() + + # call function before configuring tracing provider, should + # return INVALID_SPAN from the NoOpTracer + self.assertEqual(my_function(), trace.INVALID_SPAN) + + # configure tracing provider + trace.set_tracer_provider(TestProvider()) + # call function again, we should now be getting a TestSpan + self.assertIsInstance(my_function(), TestSpan) From fe26adaa07f88324c933a75f1a4dd199c9c8b2d7 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 7 Jul 2022 16:16:57 +0200 Subject: [PATCH 1262/1517] Fix warning message for OTLP gRPC exporter mixin (#2781) * Fix warning message for OTLP gRPC exporter mixin Fixes #2780 * Refactor export parameter type * Add changelog entry * Use fixed warning messages for traces and metrics * Use subclass-specific error messages * Fix test cases * Fix lint --- CHANGELOG.md | 6 +- .../otlp/proto/grpc/_log_exporter/__init__.py | 4 ++ .../exporter/otlp/proto/grpc/exporter.py | 39 +++++++++-- .../proto/grpc/metric_exporter/__init__.py | 4 ++ .../proto/grpc/trace_exporter/__init__.py | 4 ++ .../tests/logs/test_otlp_logs_exporter.py | 4 ++ .../metrics/test_otlp_metrics_exporter.py | 4 ++ .../tests/test_otlp_exporter_mixin.py | 68 ++++++++++++++++++- .../tests/test_otlp_trace_exporter.py | 4 ++ 9 files changed, 126 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d874d0b4f..5026184349 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc2-0.32b0...HEAD) - +- Fix OTLP gRPC exporter warning message + ([#2781](https://github.com/open-telemetry/opentelemetry-python/pull/2781)) - Fix tracing decorator with late configuration ([#2754](https://github.com/open-telemetry/opentelemetry-python/pull/2754)) - ## [1.12.0rc2-0.32b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc2-0.32b0) - 2022-07-04 - - - Fix instrument name and unit regexes ([#2796](https://github.com/open-telemetry/opentelemetry-python/pull/2796)) - Add optional sessions parameter to all Exporters leveraging requests.Session diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index 51433d5740..489cf35c37 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -154,3 +154,7 @@ def export(self, batch: Sequence[LogData]) -> LogExportResult: def shutdown(self) -> None: pass + + @property + def _exporting(self) -> str: + return "logs" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index b965061c5c..4405bcad68 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -14,7 +14,7 @@ """OTLP Exporter""" -import logging +from logging import getLogger from abc import ABC, abstractmethod from collections.abc import Sequence from os import environ @@ -23,6 +23,7 @@ from typing import Sequence as TypingSequence from typing import TypeVar from urllib.parse import urlparse +from opentelemetry.sdk.trace import ReadableSpan from backoff import expo from google.rpc.error_details_pb2 import RetryInfo @@ -52,8 +53,9 @@ ) from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.util.re import parse_headers +from opentelemetry.sdk.metrics.export import MetricsData -logger = logging.getLogger(__name__) +logger = getLogger(__name__) SDKDataT = TypeVar("SDKDataT") ResourceDataT = TypeVar("ResourceDataT") TypingResourceT = TypeVar("TypingResourceT") @@ -277,8 +279,19 @@ def _translate_attributes(self, attributes) -> TypingSequence[KeyValue]: logger.exception(error) return output - def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: - + def _export( + self, data: Union[TypingSequence[ReadableSpan], MetricsData] + ) -> ExportResultT: + + # FIXME remove this check if the export type for traces + # gets updated to a class that represents the proto + # TracesData and use the code below instead. + # logger.warning( + # "Transient error %s encountered while exporting %s, retrying in %ss.", + # error.code(), + # data.__class__.__name__, + # delay, + # ) max_value = 64 # expo returns a generator that yields delay values which grow # exponentially. Once delay is greater than max_value, the yielded @@ -321,15 +334,20 @@ def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: ) logger.warning( - "Transient error %s encountered while exporting span batch, retrying in %ss.", + ( + "Transient error %s encountered while exporting " + "%s, retrying in %ss." + ), error.code(), + self._exporting, delay, ) sleep(delay) continue else: logger.error( - "Failed to export span batch, error code: %s", + "Failed to export %s, error code: %s", + self._exporting, error.code(), ) @@ -342,3 +360,12 @@ def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: def shutdown(self) -> None: pass + + @property + @abstractmethod + def _exporting(self) -> str: + """ + Returns a string that describes the overall exporter, to be used in + warning messages. + """ + pass diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index c5f4acad06..fb316ab2e8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -206,3 +206,7 @@ def export( def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass + + @property + def _exporting(self) -> str: + return "metrics" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 084a5d93b1..5626012536 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -289,3 +289,7 @@ def _translate_data( def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return self._export(spans) + + @property + def _exporting(self): + return "traces" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index 4ee8f6a0b3..a9c63eaa0a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -161,6 +161,10 @@ def setUp(self): def tearDown(self): self.server.stop(None) + def test_exporting(self): + # pylint: disable=protected-access + self.assertEqual(self.exporter._exporting, "logs") + @patch( "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index 8936272bef..c25ab06263 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -298,6 +298,10 @@ def setUp(self): def tearDown(self): self.server.stop(None) + def test_exporting(self): + # pylint: disable=protected-access + self.assertEqual(self.exporter._exporting, "metrics") + @patch( "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index a7627b237c..3f44ef228e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -12,13 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +from logging import WARNING +from types import MethodType +from typing import Sequence from unittest import TestCase -from unittest.mock import patch +from unittest.mock import Mock, patch from grpc import Compression from opentelemetry.exporter.otlp.proto.grpc.exporter import ( + ExportServiceRequestT, InvalidCompressionValueException, + OTLPExporterMixin, + RpcError, + SDKDataT, + StatusCode, environ_to_compression, ) @@ -47,3 +55,61 @@ def test_environ_to_compression(self): ) with self.assertRaises(InvalidCompressionValueException): environ_to_compression("test_invalid") + + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + def test_export_warning(self, mock_expo): + + mock_expo.configure_mock(**{"return_value": [0]}) + + rpc_error = RpcError() + + def code(self): + return None + + rpc_error.code = MethodType(code, rpc_error) + + class OTLPMockExporter(OTLPExporterMixin): + + _result = Mock() + _stub = Mock( + **{"return_value": Mock(**{"Export.side_effect": rpc_error})} + ) + + def _translate_data( + self, data: Sequence[SDKDataT] + ) -> ExportServiceRequestT: + pass + + @property + def _exporting(self) -> str: + return "mock" + + otlp_mock_exporter = OTLPMockExporter() + + with self.assertLogs(level=WARNING) as warning: + # pylint: disable=protected-access + otlp_mock_exporter._export(Mock()) + self.assertEqual( + warning.records[0].message, + "Failed to export mock, error code: None", + ) + + def code(self): # pylint: disable=function-redefined + return StatusCode.CANCELLED + + def trailing_metadata(self): + return {} + + rpc_error.code = MethodType(code, rpc_error) + rpc_error.trailing_metadata = MethodType(trailing_metadata, rpc_error) + + with self.assertLogs(level=WARNING) as warning: + # pylint: disable=protected-access + otlp_mock_exporter._export([]) + self.assertEqual( + warning.records[0].message, + ( + "Transient error StatusCode.CANCELLED encountered " + "while exporting mock, retrying in 0s." + ), + ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 3d83672901..a5cb4e699a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -217,6 +217,10 @@ def setUp(self): def tearDown(self): self.server.stop(None) + def test_exporting(self): + # pylint: disable=protected-access + self.assertEqual(self.exporter._exporting, "traces") + @patch.dict( "os.environ", { From 12c9b0f731d3b80f72bcc402b57c203fcc9ba77f Mon Sep 17 00:00:00 2001 From: Aaron Ai Date: Tue, 12 Jul 2022 00:39:48 +0800 Subject: [PATCH 1263/1517] Add markdown links check (#2812) --- .github/workflows/check-links.yml | 42 +++++++++++++++++++++++ .github/workflows/check_links_config.json | 14 ++++++++ 2 files changed, 56 insertions(+) create mode 100644 .github/workflows/check-links.yml create mode 100644 .github/workflows/check_links_config.json diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 0000000000..cbf945b682 --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,42 @@ +name: check-links +on: + push: + branches: [ main ] + pull_request: + +jobs: + changedfiles: + name: changed files + runs-on: ubuntu-latest + if: ${{ github.actor != 'dependabot[bot]' }} + outputs: + md: ${{ steps.changes.outputs.md }} + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Get changed files + id: changes + run: | + echo "::set-output name=md::$(git diff --name-only --diff-filter=ACMRTUXB $(git merge-base origin/main ${{ github.event.pull_request.head.sha }}) ${{ github.event.pull_request.head.sha }} | grep .md$ | xargs)" + check-links: + runs-on: ubuntu-latest + needs: changedfiles + if: ${{needs.changedfiles.outputs.md}} + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Install markdown-link-check + run: npm install -g markdown-link-check + + - name: Run markdown-link-check + run: | + markdown-link-check \ + --verbose \ + --config .github/workflows/check_links_config.json \ + ${{needs.changedfiles.outputs.md}} \ + || { echo "Check that anchor links are lowercase"; exit 1; } \ No newline at end of file diff --git a/.github/workflows/check_links_config.json b/.github/workflows/check_links_config.json new file mode 100644 index 0000000000..4f17e90626 --- /dev/null +++ b/.github/workflows/check_links_config.json @@ -0,0 +1,14 @@ +{ + "ignorePatterns": [ + { + "pattern": "http(s)?://\\d+\\.\\d+\\.\\d+\\.\\d+" + }, + { + "pattern": "http(s)?://localhost" + }, + { + "pattern": "http(s)?://example.com" + } + ], + "aliveStatusCodes": [429, 200] +} From b86a9a5e3ddc8f8299f8d07715785aabbe2f9e08 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 11 Jul 2022 12:15:17 -0700 Subject: [PATCH 1264/1517] [chore] update approvers list (#2814) --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f5620db91c..9a060129db 100644 --- a/README.md +++ b/README.md @@ -136,10 +136,17 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): - [Aaron Abbott](https://github.com/aabmass), Google -- [Alex Boten](https://github.com/codeboten), Lightstep - [Owais Lone](https://github.com/owais), Splunk - [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), AWS +Emeritus Approvers + +- [Carlos Alberto Cortez](https://github.com/carlosalberto), Lightstep +- [Christian Neumüller](https://github.com/Oberon00), Dynatrace +- [Hector Hernandez](https://github.com/hectorhdzg), Microsoft +- [Mauricio Vásquez](https://github.com/mauriciovasquezbernal), Kinvolk +- [Tahir H. Butt](https://github.com/majorgreys) DataDog + *For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver).* Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-telemetry/teams/python-maintainers)): @@ -148,6 +155,13 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t - [Leighton Chen](https://github.com/lzchen), Microsoft - [Srikanth Chekuri](https://github.com/srikanthccv) +Emeritus Maintainers: + +- [Alex Boten](https://github.com/codeboten), Lightstep +- [Chris Kleinknecht](https://github.com/c24t), Google +- [Reiley Yang](https://github.com/reyang), Microsoft +- [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google + *For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer).* ### Thanks to all the people who already contributed! From 6ded4dec098c2b2d8e0aaaef588ac38ddd2caa7b Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 13 Jul 2022 19:37:32 +0300 Subject: [PATCH 1265/1517] Remove eachdist from contributing documentation (#2782) Fixes #2772 Co-authored-by: Srikanth Chekuri --- CONTRIBUTING.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 779493efd9..2f327c690d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,15 +45,7 @@ during their normal contribution hours. ## Development -To quickly get up and running, you can use the `scripts/eachdist.py` tool that -ships with this project. First create a virtualenv and activate it. -Then run `python scripts/eachdist.py develop` to install all required packages -as well as the project's packages themselves (in `--editable` mode). - -You can then run `scripts/eachdist.py test` to test everything or -`scripts/eachdist.py lint` to lint everything (fixing anything that is auto-fixable). - -Additionally, this project uses [tox](https://tox.readthedocs.io) to automate +This project uses [tox](https://tox.readthedocs.io) to automate some aspects of development, including testing against multiple Python versions. To install `tox`, run: From d127b598b187de5be6a0e43c112bf954a79f57d3 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 14 Jul 2022 01:52:57 +0530 Subject: [PATCH 1266/1517] docs: add examples to show common view scenarios (#2811) --- docs/examples/views/README.rst | 35 +++++++++++ docs/examples/views/change_aggregation.py | 53 +++++++++++++++++ docs/examples/views/change_name.py | 55 ++++++++++++++++++ docs/examples/views/limit_num_of_attrs.py | 71 +++++++++++++++++++++++ docs/examples/views/requirements.txt | 6 ++ 5 files changed, 220 insertions(+) create mode 100644 docs/examples/views/README.rst create mode 100644 docs/examples/views/change_aggregation.py create mode 100644 docs/examples/views/change_name.py create mode 100644 docs/examples/views/limit_num_of_attrs.py create mode 100644 docs/examples/views/requirements.txt diff --git a/docs/examples/views/README.rst b/docs/examples/views/README.rst new file mode 100644 index 0000000000..3bf6010660 --- /dev/null +++ b/docs/examples/views/README.rst @@ -0,0 +1,35 @@ +View common scenarios +===================== + +These examples show how to customize the metrics that are output by the SDK using Views. There are multiple examples: + +* change_aggregation: Shows how to configure to change the default aggregation for an instrument. +* change_name: Shows how to change the name of a metric. +* limit_num_of_attrs: Shows how to limit the number of attributes that are output for a metric. + +The source files of these examples are available :scm_web:`here `. + + +Installation +------------ + +.. code-block:: sh + + pip install -r requirements.txt + +Run the Example +--------------- + +.. code-block:: sh + + python .py + +The output will be shown in the console. + +Useful links +------------ + +- OpenTelemetry_ +- :doc:`../../api/metrics` + +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/docs/examples/views/change_aggregation.py b/docs/examples/views/change_aggregation.py new file mode 100644 index 0000000000..5dad07e64b --- /dev/null +++ b/docs/examples/views/change_aggregation.py @@ -0,0 +1,53 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import time + +from opentelemetry.metrics import get_meter_provider, set_meter_provider +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk.metrics.view import SumAggregation, View + +# Create a view matching the histogram instrument name `http.client.request.latency` +# and configure the `SumAggregation` for the result metrics stream +hist_to_sum_view = View( + instrument_name="http.client.request.latency", aggregation=SumAggregation() +) + +# Use console exporter for the example +exporter = ConsoleMetricExporter() + +# Create a metric reader with stdout exporter +reader = PeriodicExportingMetricReader(exporter, export_interval_millis=1_000) +provider = MeterProvider( + metric_readers=[ + reader, + ], + views=[ + hist_to_sum_view, + ], +) +set_meter_provider(provider) + +meter = get_meter_provider().get_meter("view-change-aggregation", "0.1.2") + +histogram = meter.create_histogram("http.client.request.latency") + +while 1: + histogram.record(99.9) + time.sleep(random.random()) diff --git a/docs/examples/views/change_name.py b/docs/examples/views/change_name.py new file mode 100644 index 0000000000..c70f7852a2 --- /dev/null +++ b/docs/examples/views/change_name.py @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import time + +from opentelemetry.metrics import get_meter_provider, set_meter_provider +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk.metrics.view import View + +# Create a view matching the counter instrument `my.counter` +# and configure the new name `my.counter.total` for the result metrics stream +change_metric_name_view = View( + instrument_type=Counter, + instrument_name="my.counter", + name="my.counter.total", +) + +# Use console exporter for the example +exporter = ConsoleMetricExporter() + +# Create a metric reader with stdout exporter +reader = PeriodicExportingMetricReader(exporter, export_interval_millis=1_000) +provider = MeterProvider( + metric_readers=[ + reader, + ], + views=[ + change_metric_name_view, + ], +) +set_meter_provider(provider) + +meter = get_meter_provider().get_meter("view-name-change", "0.1.2") + +my_counter = meter.create_counter("my.counter") + +while 1: + my_counter.add(random.randint(1, 10)) + time.sleep(random.random()) diff --git a/docs/examples/views/limit_num_of_attrs.py b/docs/examples/views/limit_num_of_attrs.py new file mode 100644 index 0000000000..d9f0e9484c --- /dev/null +++ b/docs/examples/views/limit_num_of_attrs.py @@ -0,0 +1,71 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import time +from typing import Iterable + +from opentelemetry.metrics import ( + CallbackOptions, + Observation, + get_meter_provider, + set_meter_provider, +) +from opentelemetry.sdk.metrics import MeterProvider, ObservableGauge +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk.metrics.view import View + +# Create a view matching the observable gauge instrument `observable_gauge` +# and configure the attributes in the result metric stream +# to contain only the attributes with keys with `k_3` and `k_5` +view_with_attributes_limit = View( + instrument_type=ObservableGauge, + instrument_name="observable_gauge", + attribute_keys={"k_3", "k_5"}, +) + +exporter = ConsoleMetricExporter() + +reader = PeriodicExportingMetricReader(exporter, export_interval_millis=1_000) +provider = MeterProvider( + metric_readers=[ + reader, + ], + views=[ + view_with_attributes_limit, + ], +) +set_meter_provider(provider) + +meter = get_meter_provider().get_meter("reduce-cardinality-with-view", "0.1.2") + + +def observable_gauge_func(options: CallbackOptions) -> Iterable[Observation]: + attrs = {} + for i in range(random.randint(1, 100)): + attrs[f"k_{i}"] = f"v_{i}" + yield Observation(1, attrs) + + +# Async gauge +observable_gauge = meter.create_observable_gauge( + "observable_gauge", + [observable_gauge_func], +) + +while 1: + time.sleep(1) diff --git a/docs/examples/views/requirements.txt b/docs/examples/views/requirements.txt new file mode 100644 index 0000000000..f690f9d1ca --- /dev/null +++ b/docs/examples/views/requirements.txt @@ -0,0 +1,6 @@ +Deprecated==1.2.13 +opentelemetry-api==1.12.0rc2 +opentelemetry-sdk==1.12.0rc2 +opentelemetry-semantic-conventions==0.32b0 +typing_extensions==4.3.0 +wrapt==1.14.1 \ No newline at end of file From 4e0a2a840c279b75975ff7addaa76a0b8c9fbd0c Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Wed, 13 Jul 2022 15:41:40 -0700 Subject: [PATCH 1267/1517] LabelValue can be list of strings (#2810) --- opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 996a7f2800..3088dbc5bb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -71,8 +71,9 @@ OTEL_SERVICE_NAME, ) from opentelemetry.semconv.resource import ResourceAttributes +from opentelemetry.util.types import AttributeValue -LabelValue = typing.Union[str, bool, int, float] +LabelValue = AttributeValue Attributes = typing.Dict[str, LabelValue] logger = logging.getLogger(__name__) From 623a4762a167ed992294886724df79b710cfcd05 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 14 Jul 2022 02:03:21 +0300 Subject: [PATCH 1268/1517] Remove dead link (#2818) --- CONTRIBUTING.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f327c690d..ebf5aad8b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,8 +96,6 @@ The continuation integration overrides that environment variable with as per the ### Benchmarks -Performance progression of benchmarks for packages distributed by OpenTelemetry Python can be viewed as a [graph of throughput vs commit history](https://opentelemetry-python.readthedocs.io/en/latest/performance/benchmarks.html). From the linked page, you can download a JSON file with the performance results. - Running the `tox` tests also runs the performance tests if any are available. Benchmarking tests are done with `pytest-benchmark` and they output a table with results to the console. To write benchmarks, simply use the [pytest benchmark fixture](https://pytest-benchmark.readthedocs.io/en/latest/usage.html#usage) like the following: From b7a37abe05a40f156c0cc7e7d30d84e880716a6e Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 15 Jul 2022 09:54:51 -0700 Subject: [PATCH 1269/1517] Fix links for pass tests (#2823) * links * fix links * link --- CHANGELOG.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5026184349..490c11d0a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc2-0.32b0...HEAD) +## [Unreleased] - Fix OTLP gRPC exporter warning message ([#2781](https://github.com/open-telemetry/opentelemetry-python/pull/2781)) - Fix tracing decorator with late configuration ([#2754](https://github.com/open-telemetry/opentelemetry-python/pull/2754)) -## [1.12.0rc2-0.32b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc2-0.32b0) - 2022-07-04 +## [1.12.0rc2-0.32b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc2) - 2022-07-04 - Fix instrument name and unit regexes ([#2796](https://github.com/open-telemetry/opentelemetry-python/pull/2796)) @@ -57,7 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2785](https://github.com/open-telemetry/opentelemetry-python/pull/2785)) -## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 +## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1) - 2022-05-17 - Fix LoggingHandler to handle LogRecord with exc_info=False ([#2690](https://github.com/open-telemetry/opentelemetry-python/pull/2690)) @@ -80,7 +80,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Move Metrics API behind internal package ([#2651](https://github.com/open-telemetry/opentelemetry-python/pull/2651)) -## [1.11.1-0.30b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.1-0.30b1) - 2022-04-21 +## [1.11.1-0.30b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.1) - 2022-04-21 - Add parameter to MetricReader constructor to select aggregation per instrument kind ([#2638](https://github.com/open-telemetry/opentelemetry-python/pull/2638)) @@ -96,7 +96,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Deprecate InstrumentationLibraryInfo and Add InstrumentationScope ([#2583](https://github.com/open-telemetry/opentelemetry-python/pull/2583)) -## [1.11.0-0.30b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.0-0.30b0) - 2022-04-18 +## [1.11.0-0.30b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.0) - 2022-04-18 - Rename API Measurement for async instruments to Observation ([#2617](https://github.com/open-telemetry/opentelemetry-python/pull/2617)) @@ -130,7 +130,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update opentelemetry-proto to v0.16.0 ([#2619](https://github.com/open-telemetry/opentelemetry-python/pull/2619)) -## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 +## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0) - 2022-03-10 - Docs rework: [non-API docs are moving](https://github.com/open-telemetry/opentelemetry-python/issues/2172) to @@ -151,12 +151,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [exporter/opentelemetry-exporter-prometheus] restore package using the new metrics API ([#2321](https://github.com/open-telemetry/opentelemetry-python/pull/2321)) -## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1-0.28b1) - 2022-01-29 +## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1) - 2022-01-29 - Update opentelemetry-proto to v0.12.0. Note that this update removes deprecated status codes. ([#2415](https://github.com/open-telemetry/opentelemetry-python/pull/2415)) -## [1.9.0-0.28b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.0-0.28b0) - 2022-01-26 +## [1.9.0-0.28b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.0) - 2022-01-26 - Fix SpanLimits global span limit defaulting when set to 0 @@ -180,7 +180,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [logs] prevent None from causing problems ([#2410](https://github.com/open-telemetry/opentelemetry-python/pull/2410)) -## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17 +## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0) - 2021-12-17 - Adds Aggregation and instruments as part of Metrics SDK ([#2234](https://github.com/open-telemetry/opentelemetry-python/pull/2234)) @@ -199,7 +199,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support insecure configuration for OTLP gRPC exporter ([#2350](https://github.com/open-telemetry/opentelemetry-python/pull/2350)) -## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11 +## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0) - 2021-11-11 - Add support for Python 3.10 ([#2207](https://github.com/open-telemetry/opentelemetry-python/pull/2207)) @@ -220,12 +220,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-test` start releasing to pypi.org ([#2269](https://github.com/open-telemetry/opentelemetry-python/pull/2269)) -## [1.6.2-0.25b2](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.2-0.25b2) - 2021-10-19 +## [1.6.2-0.25b2](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.2) - 2021-10-19 - Fix parental trace relationship for opentracing `follows_from` reference ([#2180](https://github.com/open-telemetry/opentelemetry-python/pull/2180)) -## [1.6.1-0.25b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.1-0.25b1) - 2021-10-18 +## [1.6.1-0.25b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.1) - 2021-10-18 - Fix ReadableSpan property types attempting to create a mapping from a list ([#2215](https://github.com/open-telemetry/opentelemetry-python/pull/2215)) @@ -234,7 +234,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Propagation: only warn about oversized baggage headers when headers exist ([#2212](https://github.com/open-telemetry/opentelemetry-python/pull/2212)) -## [1.6.0-0.25b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.0-0.25b0) - 2021-10-13 +## [1.6.0-0.25b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.0) - 2021-10-13 - Fix race in `set_tracer_provider()` ([#2182](https://github.com/open-telemetry/opentelemetry-python/pull/2182)) @@ -272,7 +272,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add name to `BatchSpanProcessor` worker thread ([#2186](https://github.com/open-telemetry/opentelemetry-python/pull/2186)) -## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 +## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0) - 2021-08-26 - Add pre and post instrumentation entry points ([#1983](https://github.com/open-telemetry/opentelemetry-python/pull/1983)) @@ -305,7 +305,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix opentelemetry-bootstrap dependency script. ([#1987](https://github.com/open-telemetry/opentelemetry-python/pull/1987)) -## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-21 +## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0) - 2021-07-21 ### Added @@ -356,7 +356,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 opentelemetry specification format, rather than opentracing spec format. ([#1878](https://github.com/open-telemetry/opentelemetry-python/pull/1878)) -## [1.3.0-0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01 +## [1.3.0-0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0) - 2021-06-01 ### Added @@ -378,7 +378,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update protos to latest version release 0.9.0 ([#1873](https://github.com/open-telemetry/opentelemetry-python/pull/1873)) -## [1.2.0, 0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0-0.21b0) - 2021-05-11 +## [1.2.0, 0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0) - 2021-05-11 ### Added From a3346ae9cf76d7f6b49ff4d3b9c358bb60f3c6ed Mon Sep 17 00:00:00 2001 From: Povilas Versockas Date: Mon, 18 Jul 2022 15:39:03 +0300 Subject: [PATCH 1270/1517] fix instrumenting_module_version typo (#2830) --- CHANGELOG.md | 2 ++ opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 490c11d0a3..187922d7df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Fix get_log_emitter instrumenting_module_version args typo + ([#2830](https://github.com/open-telemetry/opentelemetry-python/pull/2830)) - Fix OTLP gRPC exporter warning message ([#2781](https://github.com/open-telemetry/opentelemetry-python/pull/2781)) - Fix tracing decorator with late configuration diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index efb074fd0e..114e03b263 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -427,13 +427,13 @@ def resource(self): def get_log_emitter( self, instrumenting_module_name: str, - instrumenting_module_verison: str = "", + instrumenting_module_version: str = "", ) -> LogEmitter: return LogEmitter( self._resource, self._multi_log_processor, InstrumentationScope( - instrumenting_module_name, instrumenting_module_verison + instrumenting_module_name, instrumenting_module_version ), ) From 9b8646db5fae8036059f41a2a766fce53a8a53db Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 18 Jul 2022 23:15:20 +0530 Subject: [PATCH 1271/1517] move tracing sdk setup from setUpClass to setUp (#2819) * move tracing sdk setup from setUpClass to setUp * Update contrib SHA --- .github/workflows/test.yml | 2 +- .../src/opentelemetry/test/test_base.py | 22 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f2566451b..9fcaa406f7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 42ff80bef8a03ff214a54323a2631da06e6dc5e4 + CONTRIB_REPO_SHA: c37a77e2efa9b67d4b017a3aa6d5e5f0bc5433c9 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py index b0a9f08fd5..0d81fcb4f5 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py @@ -34,22 +34,15 @@ class TestBase(unittest.TestCase): # pylint: disable=C0103 - @classmethod - def setUpClass(cls): - result = cls.create_tracer_provider() - cls.tracer_provider, cls.memory_exporter = result - # This is done because set_tracer_provider cannot override the - # current tracer provider. - reset_trace_globals() - trace_api.set_tracer_provider(cls.tracer_provider) - - @classmethod - def tearDownClass(cls): + def setUp(self): + super().setUp() + result = self.create_tracer_provider() + self.tracer_provider, self.memory_exporter = result # This is done because set_tracer_provider cannot override the # current tracer provider. reset_trace_globals() + trace_api.set_tracer_provider(self.tracer_provider) - def setUp(self): self.memory_exporter.clear() # This is done because set_meter_provider cannot override the # current meter provider. @@ -60,6 +53,11 @@ def setUp(self): ) = self.create_meter_provider() metrics_api.set_meter_provider(self.meter_provider) + def tearDown(self): + super().tearDown() + reset_trace_globals() + reset_metrics_globals() + def get_finished_spans(self): return FinishedTestSpans( self, self.memory_exporter.get_finished_spans() From 30a4d453e551c82da0cb5c3a684fa37226b34af4 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 19 Jul 2022 00:26:22 +0530 Subject: [PATCH 1272/1517] docs: add example to show how to drop measurements from an instrument (#2826) --- docs/examples/views/README.rst | 1 + .../views/drop_metrics_from_instrument.py | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 docs/examples/views/drop_metrics_from_instrument.py diff --git a/docs/examples/views/README.rst b/docs/examples/views/README.rst index 3bf6010660..07fddfc2fb 100644 --- a/docs/examples/views/README.rst +++ b/docs/examples/views/README.rst @@ -6,6 +6,7 @@ These examples show how to customize the metrics that are output by the SDK usin * change_aggregation: Shows how to configure to change the default aggregation for an instrument. * change_name: Shows how to change the name of a metric. * limit_num_of_attrs: Shows how to limit the number of attributes that are output for a metric. +* drop_metrics_from_instrument: Shows how to drop measurements from an instrument. The source files of these examples are available :scm_web:`here `. diff --git a/docs/examples/views/drop_metrics_from_instrument.py b/docs/examples/views/drop_metrics_from_instrument.py new file mode 100644 index 0000000000..c8ca1008e5 --- /dev/null +++ b/docs/examples/views/drop_metrics_from_instrument.py @@ -0,0 +1,53 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import time + +from opentelemetry.metrics import get_meter_provider, set_meter_provider +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk.metrics.view import DropAggregation, View + +# Create a view matching the counter instrument `my.counter` +# and configure the view to drop the aggregation. +drop_aggregation_view = View( + instrument_type=Counter, + instrument_name="my.counter", + aggregation=DropAggregation(), +) + +exporter = ConsoleMetricExporter() + +reader = PeriodicExportingMetricReader(exporter, export_interval_millis=1_000) +provider = MeterProvider( + metric_readers=[ + reader, + ], + views=[ + drop_aggregation_view, + ], +) +set_meter_provider(provider) + +meter = get_meter_provider().get_meter("view-drop-aggregation", "0.1.2") + +my_counter = meter.create_counter("my.counter") + +while 1: + my_counter.add(random.randint(1, 10)) + time.sleep(random.random()) From f14d93609fbf542a6270d97bf83647782e70ced6 Mon Sep 17 00:00:00 2001 From: Jeremy Voss Date: Mon, 18 Jul 2022 13:46:06 -0700 Subject: [PATCH 1273/1517] Change tracing to use `Resource.to_json()` (#2784) * Change tracing to use Resource.to_json() * Edited CHANGELOG * Added PR # to CHANGELOG * Change tracing to use Resource.to_json() * Edited CHANGELOG * Added PR # to CHANGELOG * Generalized removal of empty or blank values * Simplified trace to_json to include empty and null values. Co-authored-by: Diego Hurtado Co-authored-by: Leighton Chen Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/trace/__init__.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 9 ++++++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 187922d7df..5af80680b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Change tracing to use `Resource.to_json()` + ([#2784](https://github.com/open-telemetry/opentelemetry-python/pull/2784)) - Fix get_log_emitter instrumenting_module_version args typo ([#2830](https://github.com/open-telemetry/opentelemetry-python/pull/2830)) - Fix OTLP gRPC exporter warning message diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 7dc65600f4..6d30e94ed5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -492,7 +492,7 @@ def to_json(self, indent=4): f_span["attributes"] = self._format_attributes(self._attributes) f_span["events"] = self._format_events(self._events) f_span["links"] = self._format_links(self._links) - f_span["resource"] = self._format_attributes(self._resource.attributes) + f_span["resource"] = json.loads(self.resource.to_json()) return json.dumps(f_span, indent=indent) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 24d7b6fa3d..a6683ebd03 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -1354,12 +1354,15 @@ def test_to_json(self): "attributes": {}, "events": [], "links": [], - "resource": {} + "resource": { + "attributes": {}, + "schema_url": "" + } }""", ) self.assertEqual( span.to_json(indent=None), - '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "[]"}, "kind": "SpanKind.INTERNAL", "parent_id": "0x00000000deadbef0", "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {}, "events": [], "links": [], "resource": {}}', + '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "[]"}, "kind": "SpanKind.INTERNAL", "parent_id": "0x00000000deadbef0", "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {}, "events": [], "links": [], "resource": {"attributes": {}, "schema_url": ""}}', ) def test_attributes_to_json(self): @@ -1377,7 +1380,7 @@ def test_attributes_to_json(self): span.to_json(indent=None), '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "[]"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {"key": "value"}, "events": [{"name": "event", "timestamp": "' + date_str - + '", "attributes": {"key2": "value2"}}], "links": [], "resource": {}}', + + '", "attributes": {"key2": "value2"}}], "links": [], "resource": {"attributes": {}, "schema_url": ""}}', ) From 2ad9515affa1f8f16b076e79d82bd450a922a53e Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 21 Jul 2022 04:19:27 -0700 Subject: [PATCH 1274/1517] docs: Add an example for preferred temporality and aggregation for metric reader (#2832) * example * remove test * comments * docs * docs * seconds --- .../metrics/{ => instruments}/README.rst | 0 .../metrics/{ => instruments}/example.py | 0 .../otel-collector-config.yaml | 0 docs/examples/metrics/reader/README.rst | 34 +++++++++++ .../metrics/reader/preferred_aggregation.py | 50 ++++++++++++++++ .../metrics/reader/preferred_temporality.py | 58 +++++++++++++++++++ .../reader}/requirements.txt | 0 docs/examples/{ => metrics}/views/README.rst | 12 ++-- .../{ => metrics}/views/change_aggregation.py | 0 .../{ => metrics}/views/change_name.py | 0 .../views/drop_metrics_from_instrument.py | 0 .../{ => metrics}/views/limit_num_of_attrs.py | 0 docs/examples/metrics/views/requirements.txt | 6 ++ 13 files changed, 154 insertions(+), 6 deletions(-) rename docs/examples/metrics/{ => instruments}/README.rst (100%) rename docs/examples/metrics/{ => instruments}/example.py (100%) rename docs/examples/metrics/{ => instruments}/otel-collector-config.yaml (100%) create mode 100644 docs/examples/metrics/reader/README.rst create mode 100644 docs/examples/metrics/reader/preferred_aggregation.py create mode 100644 docs/examples/metrics/reader/preferred_temporality.py rename docs/examples/{views => metrics/reader}/requirements.txt (100%) rename docs/examples/{ => metrics}/views/README.rst (57%) rename docs/examples/{ => metrics}/views/change_aggregation.py (100%) rename docs/examples/{ => metrics}/views/change_name.py (100%) rename docs/examples/{ => metrics}/views/drop_metrics_from_instrument.py (100%) rename docs/examples/{ => metrics}/views/limit_num_of_attrs.py (100%) create mode 100644 docs/examples/metrics/views/requirements.txt diff --git a/docs/examples/metrics/README.rst b/docs/examples/metrics/instruments/README.rst similarity index 100% rename from docs/examples/metrics/README.rst rename to docs/examples/metrics/instruments/README.rst diff --git a/docs/examples/metrics/example.py b/docs/examples/metrics/instruments/example.py similarity index 100% rename from docs/examples/metrics/example.py rename to docs/examples/metrics/instruments/example.py diff --git a/docs/examples/metrics/otel-collector-config.yaml b/docs/examples/metrics/instruments/otel-collector-config.yaml similarity index 100% rename from docs/examples/metrics/otel-collector-config.yaml rename to docs/examples/metrics/instruments/otel-collector-config.yaml diff --git a/docs/examples/metrics/reader/README.rst b/docs/examples/metrics/reader/README.rst new file mode 100644 index 0000000000..1751e4bd81 --- /dev/null +++ b/docs/examples/metrics/reader/README.rst @@ -0,0 +1,34 @@ +MetricReader configuration scenarios +==================================== + +These examples show how to customize the metrics that are output by the SDK using configuration on metric readers. There are multiple examples: + +* preferred_aggregation.py: Shows how to configure the preferred aggregation for metric instrument types. +* preferred_temporality.py: Shows how to configure the preferred temporality for metric instrument types. + +The source files of these examples are available :scm_web:`here `. + + +Installation +------------ + +.. code-block:: sh + + pip install -r requirements.txt + +Run the Example +--------------- + +.. code-block:: sh + + python .py + +The output will be shown in the console. + +Useful links +------------ + +- OpenTelemetry_ +- :doc:`../../../api/metrics` + +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/docs/examples/metrics/reader/preferred_aggregation.py b/docs/examples/metrics/reader/preferred_aggregation.py new file mode 100644 index 0000000000..9ed69c8179 --- /dev/null +++ b/docs/examples/metrics/reader/preferred_aggregation.py @@ -0,0 +1,50 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +from opentelemetry.metrics import get_meter_provider, set_meter_provider +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk.metrics.view import LastValueAggregation + +# Use console exporter for the example +exporter = ConsoleMetricExporter() + +aggregation_last_value = {Counter: LastValueAggregation()} + +# Create a metric reader with custom preferred aggregation +reader = PeriodicExportingMetricReader( + exporter, + preferred_aggregation=aggregation_last_value, + export_interval_millis=5_000, +) + +provider = MeterProvider(metric_readers=[reader]) +set_meter_provider(provider) + +meter = get_meter_provider().get_meter("preferred-aggregation", "0.1.2") + +counter = meter.create_counter("my-counter") + +# A counter normally would have an aggregation type of SumAggregation, +# in which it's value would be determined by a cumulative sum. +# In this example, the counter is configured with the LastValueAggregation, +# which will only hold the most recent value. +for x in range(10): + counter.add(x) + time.sleep(2.0) diff --git a/docs/examples/metrics/reader/preferred_temporality.py b/docs/examples/metrics/reader/preferred_temporality.py new file mode 100644 index 0000000000..d82fd71598 --- /dev/null +++ b/docs/examples/metrics/reader/preferred_temporality.py @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +from opentelemetry.metrics import get_meter_provider, set_meter_provider +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) + +# Use console exporter for the example +exporter = ConsoleMetricExporter() + +temporality_cumulative = {Counter: AggregationTemporality.CUMULATIVE} +temporality_delta = {Counter: AggregationTemporality.DELTA} +# Create a metric reader with cumulative preferred temporality +# The metrics that are exported using this reader will represent a cumulative value +reader = PeriodicExportingMetricReader( + exporter, + preferred_temporality=temporality_cumulative, + export_interval_millis=5_000, +) +# Create a metric reader with delta preferred temporality +# The metrics that are exported using this reader will represent a delta value +reader2 = PeriodicExportingMetricReader( + exporter, + preferred_temporality=temporality_delta, + export_interval_millis=5_000, +) +provider = MeterProvider(metric_readers=[reader, reader2]) +set_meter_provider(provider) + +meter = get_meter_provider().get_meter("preferred-temporality", "0.1.2") + +counter = meter.create_counter("my-counter") + +# Two metrics are expected to be printed to the console per export interval. +# The metric originating from the metric reader with a preferred temporality +# of cumulative will keep a running sum of all values added. +# The metric originating from the metric reader with a preferred temporality +# of delta will have the sum value reset each export interval. +for x in range(10): + counter.add(x) + time.sleep(2.0) diff --git a/docs/examples/views/requirements.txt b/docs/examples/metrics/reader/requirements.txt similarity index 100% rename from docs/examples/views/requirements.txt rename to docs/examples/metrics/reader/requirements.txt diff --git a/docs/examples/views/README.rst b/docs/examples/metrics/views/README.rst similarity index 57% rename from docs/examples/views/README.rst rename to docs/examples/metrics/views/README.rst index 07fddfc2fb..cc9afd97d0 100644 --- a/docs/examples/views/README.rst +++ b/docs/examples/metrics/views/README.rst @@ -3,12 +3,12 @@ View common scenarios These examples show how to customize the metrics that are output by the SDK using Views. There are multiple examples: -* change_aggregation: Shows how to configure to change the default aggregation for an instrument. -* change_name: Shows how to change the name of a metric. -* limit_num_of_attrs: Shows how to limit the number of attributes that are output for a metric. -* drop_metrics_from_instrument: Shows how to drop measurements from an instrument. +* change_aggregation.py: Shows how to configure to change the default aggregation for an instrument. +* change_name.py: Shows how to change the name of a metric. +* limit_num_of_attrs.py: Shows how to limit the number of attributes that are output for a metric. +* drop_metrics_from_instrument.py: Shows how to drop measurements from an instrument. -The source files of these examples are available :scm_web:`here `. +The source files of these examples are available :scm_web:`here `. Installation @@ -31,6 +31,6 @@ Useful links ------------ - OpenTelemetry_ -- :doc:`../../api/metrics` +- :doc:`../../../api/metrics` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/docs/examples/views/change_aggregation.py b/docs/examples/metrics/views/change_aggregation.py similarity index 100% rename from docs/examples/views/change_aggregation.py rename to docs/examples/metrics/views/change_aggregation.py diff --git a/docs/examples/views/change_name.py b/docs/examples/metrics/views/change_name.py similarity index 100% rename from docs/examples/views/change_name.py rename to docs/examples/metrics/views/change_name.py diff --git a/docs/examples/views/drop_metrics_from_instrument.py b/docs/examples/metrics/views/drop_metrics_from_instrument.py similarity index 100% rename from docs/examples/views/drop_metrics_from_instrument.py rename to docs/examples/metrics/views/drop_metrics_from_instrument.py diff --git a/docs/examples/views/limit_num_of_attrs.py b/docs/examples/metrics/views/limit_num_of_attrs.py similarity index 100% rename from docs/examples/views/limit_num_of_attrs.py rename to docs/examples/metrics/views/limit_num_of_attrs.py diff --git a/docs/examples/metrics/views/requirements.txt b/docs/examples/metrics/views/requirements.txt new file mode 100644 index 0000000000..f690f9d1ca --- /dev/null +++ b/docs/examples/metrics/views/requirements.txt @@ -0,0 +1,6 @@ +Deprecated==1.2.13 +opentelemetry-api==1.12.0rc2 +opentelemetry-sdk==1.12.0rc2 +opentelemetry-semantic-conventions==0.32b0 +typing_extensions==4.3.0 +wrapt==1.14.1 \ No newline at end of file From 05b27be9b75fd315976766662979c0d5790f185e Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Mon, 25 Jul 2022 08:59:58 -0400 Subject: [PATCH 1275/1517] Revert "Python 3.11: Enhanced error locations in tracebacks (#2771)" (#2839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit aa367302989978a1ae04badee7e3948502b96616. It is no longer required on Python 3.11.0b4 and later; see https://github.com/open-telemetry/opentelemetry-python/pull/2771#issuecomment-1193421508 and https://mail.python.org/archives/list/python-dev@python.org/message/73YP4RS4QOJXUS63BQZVLTQHK3OP3L3H/. Specifically, the ^^^^… are now omitted when they would “underline” the whole preceding line, which is the case here, so the expected output is the same as for Python 3.10 and earlier. --- opentelemetry-sdk/tests/trace/test_trace.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index a6683ebd03..24621a5978 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -15,7 +15,6 @@ # pylint: disable=too-many-lines import shutil import subprocess -import sys import unittest from importlib import reload from logging import ERROR, WARNING @@ -1187,13 +1186,6 @@ def test_record_exception_context_manager(self): stacktrace = """in test_record_exception_context_manager raise RuntimeError("example error") RuntimeError: example error""" - if sys.version_info >= (3, 11): - # https://docs.python.org/3.11/whatsnew/3.11.html#enhanced-error-locations-in-tracebacks - tracelines = stacktrace.splitlines() - tracelines.insert( - -1, " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" - ) - stacktrace = "\n".join(tracelines) self.assertIn(stacktrace, event.attributes["exception.stacktrace"]) try: From c9222bfc18ec91f041c2f0a9eac8560f61dcb338 Mon Sep 17 00:00:00 2001 From: XS Date: Tue, 26 Jul 2022 16:39:19 +0800 Subject: [PATCH 1276/1517] feat:: Added `--insecure` of CLI argument (#2696) --- CHANGELOG.md | 2 ++ .../opentelemetry/exporter/jaeger/proto/grpc/__init__.py | 9 ++++++++- .../tests/test_jaeger_exporter_protobuf.py | 3 +++ .../src/opentelemetry/sdk/environment_variables.py | 7 +++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5af80680b2..97869c80d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2781](https://github.com/open-telemetry/opentelemetry-python/pull/2781)) - Fix tracing decorator with late configuration ([#2754](https://github.com/open-telemetry/opentelemetry-python/pull/2754)) +- Fix --insecure of CLI argument + ([#2696](https://github.com/open-telemetry/opentelemetry-python/pull/2696)) ## [1.12.0rc2-0.32b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc2) - 2022-07-04 diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py index ab751b0fb9..f738ae336c 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py @@ -87,6 +87,7 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_JAEGER_ENDPOINT, OTEL_EXPORTER_JAEGER_TIMEOUT, + OTEL_EXPORTER_JAEGER_GRPC_INSECURE, ) from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult @@ -122,11 +123,17 @@ def __init__( self.collector_endpoint = collector_endpoint or environ.get( OTEL_EXPORTER_JAEGER_ENDPOINT, DEFAULT_GRPC_COLLECTOR_ENDPOINT ) + self.insecure = ( + insecure + or environ.get(OTEL_EXPORTER_JAEGER_GRPC_INSECURE, "") + .strip() + .lower() + == "true" + ) self._timeout = timeout or int( environ.get(OTEL_EXPORTER_JAEGER_TIMEOUT, DEFAULT_EXPORT_TIMEOUT) ) self._grpc_client = None - self.insecure = insecure self.credentials = util._get_credentials(credentials) tracer_provider = trace.get_tracer_provider() self.service_name = ( diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py index 6cc4419526..a886a43c52 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py @@ -35,6 +35,7 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_JAEGER_CERTIFICATE, OTEL_EXPORTER_JAEGER_ENDPOINT, + OTEL_EXPORTER_JAEGER_GRPC_INSECURE, OTEL_EXPORTER_JAEGER_TIMEOUT, OTEL_RESOURCE_ATTRIBUTES, ) @@ -87,6 +88,7 @@ def test_constructor_by_environment_variables(self): + "/certs/cred.cert", OTEL_RESOURCE_ATTRIBUTES: "service.name=my-opentelemetry-jaeger", OTEL_EXPORTER_JAEGER_TIMEOUT: "5", + OTEL_EXPORTER_JAEGER_GRPC_INSECURE: "False", }, ) @@ -99,6 +101,7 @@ def test_constructor_by_environment_variables(self): self.assertEqual(exporter.collector_endpoint, collector_endpoint) self.assertEqual(exporter._timeout, 5) self.assertIsNotNone(exporter.credentials) + self.assertEqual(exporter.insecure, False) env_patch.stop() # pylint: disable=too-many-locals,too-many-statements diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index be105c3fbb..4a62fe41c2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -435,3 +435,10 @@ ``DELTA``: Choose ``DELTA`` aggregation temporality for ``Counter``, ``Asynchronous Counter`` and ``Histogram``. Choose ``CUMULATIVE`` aggregation temporality for ``UpDownCounter`` and ``Asynchronous UpDownCounter``. """ + +OTEL_EXPORTER_JAEGER_GRPC_INSECURE = "OTEL_EXPORTER_JAEGER_GRPC_INSECURE" +""" +.. envvar:: OTEL_EXPORTER_JAEGER_GRPC_INSECURE + +The :envvar:`OTEL_EXPORTER_JAEGER_GRPC_INSECURE` is a boolean flag to True if collector has no encryption or authentication. +""" From 43288ca9a36144668797c11ca2654836ec8b5e99 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 1 Aug 2022 23:25:05 +0530 Subject: [PATCH 1277/1517] Instrument instances are always created through a Meter (#2844) * Instrument instances are always created through a Meter * Fix lint and add changelog * fix lint --- CHANGELOG.md | 2 + .../sdk/metrics/_internal/__init__.py | 38 +++--- .../sdk/metrics/_internal/export/__init__.py | 98 ++++++++++++--- .../sdk/metrics/_internal/instrument.py | 59 ++++++++- .../tests/metrics/test_aggregation.py | 38 +++--- .../tests/metrics/test_instrument.py | 74 ++++++++--- .../tests/metrics/test_metric_reader.py | 117 +++++++----------- .../metrics/test_metric_reader_storage.py | 58 ++++----- .../metrics/test_view_instrument_match.py | 10 +- 9 files changed, 309 insertions(+), 185 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97869c80d5..641d382961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2754](https://github.com/open-telemetry/opentelemetry-python/pull/2754)) - Fix --insecure of CLI argument ([#2696](https://github.com/open-telemetry/opentelemetry-python/pull/2696)) +- Instrument instances are always created through a Meter + ([#2844](https://github.com/open-telemetry/opentelemetry-python/pull/2844)) ## [1.12.0rc2-0.32b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc2) - 2022-07-04 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index c75a175007..97d910a67b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -31,12 +31,12 @@ ) from opentelemetry.metrics import UpDownCounter as APIUpDownCounter from opentelemetry.sdk.metrics._internal.instrument import ( - Counter, - Histogram, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, + _Counter, + _Histogram, + _ObservableCounter, + _ObservableGauge, + _ObservableUpDownCounter, + _UpDownCounter, ) from opentelemetry.sdk.metrics._internal.measurement_consumer import ( MeasurementConsumer, @@ -72,7 +72,7 @@ def create_counter(self, name, unit="", description="") -> APICounter: ( is_instrument_registered, instrument_id, - ) = self._is_instrument_registered(name, Counter, unit, description) + ) = self._is_instrument_registered(name, _Counter, unit, description) if is_instrument_registered: # FIXME #2558 go through all views here and check if this @@ -89,7 +89,7 @@ def create_counter(self, name, unit="", description="") -> APICounter: with self._instrument_id_instrument_lock: return self._instrument_id_instrument[instrument_id] - instrument = Counter( + instrument = _Counter( name, self._instrumentation_scope, self._measurement_consumer, @@ -109,7 +109,7 @@ def create_up_down_counter( is_instrument_registered, instrument_id, ) = self._is_instrument_registered( - name, UpDownCounter, unit, description + name, _UpDownCounter, unit, description ) if is_instrument_registered: @@ -127,7 +127,7 @@ def create_up_down_counter( with self._instrument_id_instrument_lock: return self._instrument_id_instrument[instrument_id] - instrument = UpDownCounter( + instrument = _UpDownCounter( name, self._instrumentation_scope, self._measurement_consumer, @@ -147,7 +147,7 @@ def create_observable_counter( is_instrument_registered, instrument_id, ) = self._is_instrument_registered( - name, ObservableCounter, unit, description + name, _ObservableCounter, unit, description ) if is_instrument_registered: @@ -165,7 +165,7 @@ def create_observable_counter( with self._instrument_id_instrument_lock: return self._instrument_id_instrument[instrument_id] - instrument = ObservableCounter( + instrument = _ObservableCounter( name, self._instrumentation_scope, self._measurement_consumer, @@ -185,7 +185,7 @@ def create_histogram(self, name, unit="", description="") -> APIHistogram: ( is_instrument_registered, instrument_id, - ) = self._is_instrument_registered(name, Histogram, unit, description) + ) = self._is_instrument_registered(name, _Histogram, unit, description) if is_instrument_registered: # FIXME #2558 go through all views here and check if this @@ -202,7 +202,7 @@ def create_histogram(self, name, unit="", description="") -> APIHistogram: with self._instrument_id_instrument_lock: return self._instrument_id_instrument[instrument_id] - instrument = Histogram( + instrument = _Histogram( name, self._instrumentation_scope, self._measurement_consumer, @@ -221,7 +221,7 @@ def create_observable_gauge( is_instrument_registered, instrument_id, ) = self._is_instrument_registered( - name, ObservableGauge, unit, description + name, _ObservableGauge, unit, description ) if is_instrument_registered: @@ -239,7 +239,7 @@ def create_observable_gauge( with self._instrument_id_instrument_lock: return self._instrument_id_instrument[instrument_id] - instrument = ObservableGauge( + instrument = _ObservableGauge( name, self._instrumentation_scope, self._measurement_consumer, @@ -261,7 +261,9 @@ def create_observable_up_down_counter( ( is_instrument_registered, instrument_id, - ) = self._is_instrument_registered(name, Counter, unit, description) + ) = self._is_instrument_registered( + name, _ObservableUpDownCounter, unit, description + ) if is_instrument_registered: # FIXME #2558 go through all views here and check if this @@ -278,7 +280,7 @@ def create_observable_up_down_counter( with self._instrument_id_instrument_lock: return self._instrument_id_instrument[instrument_id] - instrument = ObservableUpDownCounter( + instrument = _ObservableUpDownCounter( name, self._instrumentation_scope, self._measurement_consumer, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 8ed5596c81..ed5db17d97 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -45,6 +45,12 @@ ObservableGauge, ObservableUpDownCounter, UpDownCounter, + _Counter, + _Histogram, + _ObservableCounter, + _ObservableGauge, + _ObservableUpDownCounter, + _UpDownCounter, ) from opentelemetry.sdk.metrics._internal.point import MetricsData from opentelemetry.util._once import Once @@ -164,6 +170,7 @@ class MetricReader(ABC): # FIXME add :std:envvar:`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` # to the end of the documentation paragraph above. + # pylint: disable=too-many-branches def __init__( self, preferred_temporality: Dict[type, AggregationTemporality] = None, @@ -189,22 +196,22 @@ def __init__( == "DELTA" ): self._instrument_class_temporality = { - Counter: AggregationTemporality.DELTA, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.DELTA, - ObservableCounter: AggregationTemporality.DELTA, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, + _Counter: AggregationTemporality.DELTA, + _UpDownCounter: AggregationTemporality.CUMULATIVE, + _Histogram: AggregationTemporality.DELTA, + _ObservableCounter: AggregationTemporality.DELTA, + _ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + _ObservableGauge: AggregationTemporality.CUMULATIVE, } else: self._instrument_class_temporality = { - Counter: AggregationTemporality.CUMULATIVE, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.CUMULATIVE, - ObservableCounter: AggregationTemporality.CUMULATIVE, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, + _Counter: AggregationTemporality.CUMULATIVE, + _UpDownCounter: AggregationTemporality.CUMULATIVE, + _Histogram: AggregationTemporality.CUMULATIVE, + _ObservableCounter: AggregationTemporality.CUMULATIVE, + _ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + _ObservableGauge: AggregationTemporality.CUMULATIVE, } if preferred_temporality is not None: @@ -217,18 +224,69 @@ def __init__( f"Invalid temporality value found {temporality}" ) - self._instrument_class_temporality.update(preferred_temporality or {}) + if preferred_temporality is not None: + for typ, temporality in preferred_temporality.items(): + if typ is Counter: + self._instrument_class_temporality[_Counter] = temporality + elif typ is UpDownCounter: + self._instrument_class_temporality[ + _UpDownCounter + ] = temporality + elif typ is Histogram: + self._instrument_class_temporality[ + _Histogram + ] = temporality + elif typ is ObservableCounter: + self._instrument_class_temporality[ + _ObservableCounter + ] = temporality + elif typ is ObservableUpDownCounter: + self._instrument_class_temporality[ + _ObservableUpDownCounter + ] = temporality + elif typ is ObservableGauge: + self._instrument_class_temporality[ + _ObservableGauge + ] = temporality + else: + raise Exception(f"Invalid instrument class found {typ}") + self._preferred_temporality = preferred_temporality self._instrument_class_aggregation = { - Counter: DefaultAggregation(), - UpDownCounter: DefaultAggregation(), - Histogram: DefaultAggregation(), - ObservableCounter: DefaultAggregation(), - ObservableUpDownCounter: DefaultAggregation(), - ObservableGauge: DefaultAggregation(), + _Counter: DefaultAggregation(), + _UpDownCounter: DefaultAggregation(), + _Histogram: DefaultAggregation(), + _ObservableCounter: DefaultAggregation(), + _ObservableUpDownCounter: DefaultAggregation(), + _ObservableGauge: DefaultAggregation(), } - self._instrument_class_aggregation.update(preferred_aggregation or {}) + if preferred_aggregation is not None: + for typ, aggregation in preferred_aggregation.items(): + if typ is Counter: + self._instrument_class_aggregation[_Counter] = aggregation + elif typ is UpDownCounter: + self._instrument_class_aggregation[ + _UpDownCounter + ] = aggregation + elif typ is Histogram: + self._instrument_class_aggregation[ + _Histogram + ] = aggregation + elif typ is ObservableCounter: + self._instrument_class_aggregation[ + _ObservableCounter + ] = aggregation + elif typ is ObservableUpDownCounter: + self._instrument_class_aggregation[ + _ObservableUpDownCounter + ] = aggregation + elif typ is ObservableGauge: + self._instrument_class_aggregation[ + _ObservableGauge + ] = aggregation + else: + raise Exception(f"Invalid instrument class found {typ}") @final def collect(self, timeout_millis: float = 10_000) -> None: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py index efef08c67f..a0dc9c8da9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py @@ -132,6 +132,11 @@ def callback( class Counter(_Synchronous, APICounter): + def __new__(cls, *args, **kwargs): + if cls is Counter: + raise TypeError("Counter must be instantiated via a meter.") + return super().__new__(cls) + def add( self, amount: Union[int, float], attributes: Dict[str, str] = None ): @@ -146,6 +151,11 @@ def add( class UpDownCounter(_Synchronous, APIUpDownCounter): + def __new__(cls, *args, **kwargs): + if cls is UpDownCounter: + raise TypeError("UpDownCounter must be instantiated via a meter.") + return super().__new__(cls) + def add( self, amount: Union[int, float], attributes: Dict[str, str] = None ): @@ -155,14 +165,29 @@ def add( class ObservableCounter(_Asynchronous, APIObservableCounter): - pass + def __new__(cls, *args, **kwargs): + if cls is ObservableCounter: + raise TypeError( + "ObservableCounter must be instantiated via a meter." + ) + return super().__new__(cls) class ObservableUpDownCounter(_Asynchronous, APIObservableUpDownCounter): - pass + def __new__(cls, *args, **kwargs): + if cls is ObservableUpDownCounter: + raise TypeError( + "ObservableUpDownCounter must be instantiated via a meter." + ) + return super().__new__(cls) class Histogram(_Synchronous, APIHistogram): + def __new__(cls, *args, **kwargs): + if cls is Histogram: + raise TypeError("Histogram must be instantiated via a meter.") + return super().__new__(cls) + def record( self, amount: Union[int, float], attributes: Dict[str, str] = None ): @@ -178,4 +203,34 @@ def record( class ObservableGauge(_Asynchronous, APIObservableGauge): + def __new__(cls, *args, **kwargs): + if cls is ObservableGauge: + raise TypeError( + "ObservableGauge must be instantiated via a meter." + ) + return super().__new__(cls) + + +# Below classes exist to prevent the direct instantiation +class _Counter(Counter): + pass + + +class _UpDownCounter(UpDownCounter): + pass + + +class _ObservableCounter(ObservableCounter): + pass + + +class _ObservableUpDownCounter(ObservableUpDownCounter): + pass + + +class _Histogram(Histogram): + pass + + +class _ObservableGauge(ObservableGauge): pass diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index b2245c4136..5acf701b18 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -18,19 +18,19 @@ from unittest import TestCase from unittest.mock import Mock -from opentelemetry.sdk.metrics import ( - Counter, - Histogram, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, -) from opentelemetry.sdk.metrics._internal.aggregation import ( _ExplicitBucketHistogramAggregation, _LastValueAggregation, _SumAggregation, ) +from opentelemetry.sdk.metrics._internal.instrument import ( + _Counter, + _Histogram, + _ObservableCounter, + _ObservableGauge, + _ObservableUpDownCounter, + _UpDownCounter, +) from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics.export import ( AggregationTemporality, @@ -381,7 +381,7 @@ def test_collect(self): class TestAggregationFactory(TestCase): def test_sum_factory(self): - counter = Counter("name", Mock(), Mock()) + counter = _Counter("name", Mock(), Mock()) factory = SumAggregation() aggregation = factory._create_aggregation(counter, Mock(), 0) self.assertIsInstance(aggregation, _SumAggregation) @@ -392,7 +392,7 @@ def test_sum_factory(self): aggregation2 = factory._create_aggregation(counter, Mock(), 0) self.assertNotEqual(aggregation, aggregation2) - counter = UpDownCounter("name", Mock(), Mock()) + counter = _UpDownCounter("name", Mock(), Mock()) factory = SumAggregation() aggregation = factory._create_aggregation(counter, Mock(), 0) self.assertIsInstance(aggregation, _SumAggregation) @@ -401,7 +401,7 @@ def test_sum_factory(self): aggregation._instrument_temporality, AggregationTemporality.DELTA ) - counter = ObservableCounter("name", Mock(), Mock(), None) + counter = _ObservableCounter("name", Mock(), Mock(), None) factory = SumAggregation() aggregation = factory._create_aggregation(counter, Mock(), 0) self.assertIsInstance(aggregation, _SumAggregation) @@ -412,7 +412,7 @@ def test_sum_factory(self): ) def test_explicit_bucket_histogram_factory(self): - histo = Histogram("name", Mock(), Mock()) + histo = _Histogram("name", Mock(), Mock()) factory = ExplicitBucketHistogramAggregation( boundaries=( 0.0, @@ -428,7 +428,7 @@ def test_explicit_bucket_histogram_factory(self): self.assertNotEqual(aggregation, aggregation2) def test_last_value_factory(self): - counter = Counter("name", Mock(), Mock()) + counter = _Counter("name", Mock(), Mock()) factory = LastValueAggregation() aggregation = factory._create_aggregation(counter, Mock(), 0) self.assertIsInstance(aggregation, _LastValueAggregation) @@ -444,7 +444,7 @@ def setUpClass(cls): def test_counter(self): aggregation = self.default_aggregation._create_aggregation( - Counter("name", Mock(), Mock()), Mock(), 0 + _Counter("name", Mock(), Mock()), Mock(), 0 ) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) @@ -455,7 +455,7 @@ def test_counter(self): def test_up_down_counter(self): aggregation = self.default_aggregation._create_aggregation( - UpDownCounter("name", Mock(), Mock()), Mock(), 0 + _UpDownCounter("name", Mock(), Mock()), Mock(), 0 ) self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) @@ -466,7 +466,7 @@ def test_up_down_counter(self): def test_observable_counter(self): aggregation = self.default_aggregation._create_aggregation( - ObservableCounter("name", Mock(), Mock(), callbacks=[Mock()]), + _ObservableCounter("name", Mock(), Mock(), callbacks=[Mock()]), Mock(), 0, ) @@ -480,7 +480,7 @@ def test_observable_counter(self): def test_observable_up_down_counter(self): aggregation = self.default_aggregation._create_aggregation( - ObservableUpDownCounter( + _ObservableUpDownCounter( "name", Mock(), Mock(), callbacks=[Mock()] ), Mock(), @@ -496,7 +496,7 @@ def test_observable_up_down_counter(self): def test_histogram(self): aggregation = self.default_aggregation._create_aggregation( - Histogram( + _Histogram( "name", Mock(), Mock(), @@ -509,7 +509,7 @@ def test_histogram(self): def test_observable_gauge(self): aggregation = self.default_aggregation._create_aggregation( - ObservableGauge( + _ObservableGauge( "name", Mock(), Mock(), diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py index 7a7bef4efc..8add3e69a0 100644 --- a/opentelemetry-sdk/tests/metrics/test_instrument.py +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -25,40 +25,58 @@ ObservableUpDownCounter, UpDownCounter, ) +from opentelemetry.sdk.metrics._internal.instrument import ( + _Counter, + _Histogram, + _ObservableCounter, + _ObservableGauge, + _ObservableUpDownCounter, + _UpDownCounter, +) from opentelemetry.sdk.metrics._internal.measurement import Measurement class TestCounter(TestCase): def testname(self): - self.assertEqual(Counter("name", Mock(), Mock()).name, "name") - self.assertEqual(Counter("Name", Mock(), Mock()).name, "name") + self.assertEqual(_Counter("name", Mock(), Mock()).name, "name") + self.assertEqual(_Counter("Name", Mock(), Mock()).name, "name") def test_add(self): mc = Mock() - counter = Counter("name", Mock(), mc) + counter = _Counter("name", Mock(), mc) counter.add(1.0) mc.consume_measurement.assert_called_once() def test_add_non_monotonic(self): mc = Mock() - counter = Counter("name", Mock(), mc) + counter = _Counter("name", Mock(), mc) counter.add(-1.0) mc.consume_measurement.assert_not_called() + def test_disallow_direct_counter_creation(self): + with self.assertRaises(TypeError): + # pylint: disable=abstract-class-instantiated + Counter("name", Mock(), Mock()) + class TestUpDownCounter(TestCase): def test_add(self): mc = Mock() - counter = UpDownCounter("name", Mock(), mc) + counter = _UpDownCounter("name", Mock(), mc) counter.add(1.0) mc.consume_measurement.assert_called_once() def test_add_non_monotonic(self): mc = Mock() - counter = UpDownCounter("name", Mock(), mc) + counter = _UpDownCounter("name", Mock(), mc) counter.add(-1.0) mc.consume_measurement.assert_called_once() + def test_disallow_direct_up_down_counter_creation(self): + with self.assertRaises(TypeError): + # pylint: disable=abstract-class-instantiated + UpDownCounter("name", Mock(), Mock()) + TEST_ATTRIBUTES = {"foo": "bar"} @@ -103,11 +121,11 @@ def generator_callback_1(): class TestObservableGauge(TestCase): def testname(self): - self.assertEqual(ObservableGauge("name", Mock(), Mock()).name, "name") - self.assertEqual(ObservableGauge("Name", Mock(), Mock()).name, "name") + self.assertEqual(_ObservableGauge("name", Mock(), Mock()).name, "name") + self.assertEqual(_ObservableGauge("Name", Mock(), Mock()).name, "name") def test_callable_callback_0(self): - observable_gauge = ObservableGauge( + observable_gauge = _ObservableGauge( "name", Mock(), Mock(), [callable_callback_0] ) @@ -127,7 +145,7 @@ def test_callable_callback_0(self): ) def test_callable_multiple_callable_callback(self): - observable_gauge = ObservableGauge( + observable_gauge = _ObservableGauge( "name", Mock(), Mock(), [callable_callback_0, callable_callback_1] ) @@ -156,7 +174,7 @@ def test_callable_multiple_callable_callback(self): ) def test_generator_callback_0(self): - observable_gauge = ObservableGauge( + observable_gauge = _ObservableGauge( "name", Mock(), Mock(), [generator_callback_0()] ) @@ -177,7 +195,7 @@ def test_generator_callback_0(self): def test_generator_multiple_generator_callback(self): self.maxDiff = None - observable_gauge = ObservableGauge( + observable_gauge = _ObservableGauge( "name", Mock(), Mock(), @@ -208,10 +226,15 @@ def test_generator_multiple_generator_callback(self): ], ) + def test_disallow_direct_observable_gauge_creation(self): + with self.assertRaises(TypeError): + # pylint: disable=abstract-class-instantiated + ObservableGauge("name", Mock(), Mock()) + class TestObservableCounter(TestCase): def test_callable_callback_0(self): - observable_counter = ObservableCounter( + observable_counter = _ObservableCounter( "name", Mock(), Mock(), [callable_callback_0] ) @@ -237,7 +260,7 @@ def test_callable_callback_0(self): ) def test_generator_callback_0(self): - observable_counter = ObservableCounter( + observable_counter = _ObservableCounter( "name", Mock(), Mock(), [generator_callback_0()] ) @@ -262,10 +285,15 @@ def test_generator_callback_0(self): ], ) + def test_disallow_direct_observable_counter_creation(self): + with self.assertRaises(TypeError): + # pylint: disable=abstract-class-instantiated + ObservableCounter("name", Mock(), Mock()) + class TestObservableUpDownCounter(TestCase): def test_callable_callback_0(self): - observable_up_down_counter = ObservableUpDownCounter( + observable_up_down_counter = _ObservableUpDownCounter( "name", Mock(), Mock(), [callable_callback_0] ) @@ -291,7 +319,7 @@ def test_callable_callback_0(self): ) def test_generator_callback_0(self): - observable_up_down_counter = ObservableUpDownCounter( + observable_up_down_counter = _ObservableUpDownCounter( "name", Mock(), Mock(), [generator_callback_0()] ) @@ -316,16 +344,26 @@ def test_generator_callback_0(self): ], ) + def test_disallow_direct_observable_up_down_counter_creation(self): + with self.assertRaises(TypeError): + # pylint: disable=abstract-class-instantiated + ObservableUpDownCounter("name", Mock(), Mock()) + class TestHistogram(TestCase): def test_record(self): mc = Mock() - hist = Histogram("name", Mock(), mc) + hist = _Histogram("name", Mock(), mc) hist.record(1.0) mc.consume_measurement.assert_called_once() def test_record_non_monotonic(self): mc = Mock() - hist = Histogram("name", Mock(), mc) + hist = _Histogram("name", Mock(), mc) hist.record(-1.0) mc.consume_measurement.assert_not_called() + + def test_disallow_direct_histogram_creation(self): + with self.assertRaises(TypeError): + # pylint: disable=abstract-class-instantiated + Histogram("name", Mock(), Mock()) diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_metric_reader.py index bc72e8bf98..e0d2813a5f 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader.py @@ -20,13 +20,14 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, ) -from opentelemetry.sdk.metrics import ( - Counter, - Histogram, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, +from opentelemetry.sdk.metrics import Counter, Histogram, ObservableGauge +from opentelemetry.sdk.metrics._internal.instrument import ( + _Counter, + _Histogram, + _ObservableCounter, + _ObservableGauge, + _ObservableUpDownCounter, + _UpDownCounter, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, @@ -39,6 +40,15 @@ LastValueAggregation, ) +_expected_keys = [ + _Counter, + _UpDownCounter, + _Histogram, + _ObservableCounter, + _ObservableUpDownCounter, + _ObservableGauge, +] + class DummyMetricReader(MetricReader): def __init__( @@ -74,16 +84,7 @@ def test_configure_temporality_cumulative(self): self.assertEqual( dummy_metric_reader._instrument_class_temporality.keys(), - set( - [ - Counter, - UpDownCounter, - Histogram, - ObservableCounter, - ObservableUpDownCounter, - ObservableGauge, - ] - ), + set(_expected_keys), ) for ( value @@ -99,43 +100,36 @@ def test_configure_temporality_delta(self): self.assertEqual( dummy_metric_reader._instrument_class_temporality.keys(), - set( - [ - Counter, - UpDownCounter, - Histogram, - ObservableCounter, - ObservableUpDownCounter, - ObservableGauge, - ] - ), - ) - self.assertEqual( - dummy_metric_reader._instrument_class_temporality[Counter], + set(_expected_keys), + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[_Counter], AggregationTemporality.DELTA, ) self.assertEqual( - dummy_metric_reader._instrument_class_temporality[UpDownCounter], + dummy_metric_reader._instrument_class_temporality[_UpDownCounter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( - dummy_metric_reader._instrument_class_temporality[Histogram], + dummy_metric_reader._instrument_class_temporality[_Histogram], AggregationTemporality.DELTA, ) self.assertEqual( dummy_metric_reader._instrument_class_temporality[ - ObservableCounter + _ObservableCounter ], AggregationTemporality.DELTA, ) self.assertEqual( dummy_metric_reader._instrument_class_temporality[ - ObservableUpDownCounter + _ObservableUpDownCounter ], AggregationTemporality.CUMULATIVE, ) self.assertEqual( - dummy_metric_reader._instrument_class_temporality[ObservableGauge], + dummy_metric_reader._instrument_class_temporality[ + _ObservableGauge + ], AggregationTemporality.CUMULATIVE, ) @@ -150,43 +144,36 @@ def test_configure_temporality_parameter(self): self.assertEqual( dummy_metric_reader._instrument_class_temporality.keys(), - set( - [ - Counter, - UpDownCounter, - Histogram, - ObservableCounter, - ObservableUpDownCounter, - ObservableGauge, - ] - ), - ) - self.assertEqual( - dummy_metric_reader._instrument_class_temporality[Counter], + set(_expected_keys), + ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[_Counter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( - dummy_metric_reader._instrument_class_temporality[UpDownCounter], + dummy_metric_reader._instrument_class_temporality[_UpDownCounter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( - dummy_metric_reader._instrument_class_temporality[Histogram], + dummy_metric_reader._instrument_class_temporality[_Histogram], AggregationTemporality.DELTA, ) self.assertEqual( dummy_metric_reader._instrument_class_temporality[ - ObservableCounter + _ObservableCounter ], AggregationTemporality.CUMULATIVE, ) self.assertEqual( dummy_metric_reader._instrument_class_temporality[ - ObservableUpDownCounter + _ObservableUpDownCounter ], AggregationTemporality.CUMULATIVE, ) self.assertEqual( - dummy_metric_reader._instrument_class_temporality[ObservableGauge], + dummy_metric_reader._instrument_class_temporality[ + _ObservableGauge + ], AggregationTemporality.DELTA, ) @@ -194,16 +181,7 @@ def test_default_temporality(self): dummy_metric_reader = DummyMetricReader() self.assertEqual( dummy_metric_reader._instrument_class_aggregation.keys(), - set( - [ - Counter, - UpDownCounter, - Histogram, - ObservableCounter, - ObservableUpDownCounter, - ObservableGauge, - ] - ), + set(_expected_keys), ) for ( value @@ -215,18 +193,9 @@ def test_default_temporality(self): ) self.assertEqual( dummy_metric_reader._instrument_class_aggregation.keys(), - set( - [ - Counter, - UpDownCounter, - Histogram, - ObservableCounter, - ObservableUpDownCounter, - ObservableGauge, - ] - ), + set(_expected_keys), ) self.assertIsInstance( - dummy_metric_reader._instrument_class_aggregation[Counter], + dummy_metric_reader._instrument_class_aggregation[_Counter], LastValueAggregation, ) diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index 1ab0af6ad6..7cea60d393 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -15,15 +15,15 @@ from logging import WARNING from unittest.mock import MagicMock, Mock, patch -from opentelemetry.sdk.metrics import ( - Counter, - Histogram, - ObservableCounter, - UpDownCounter, -) from opentelemetry.sdk.metrics._internal.aggregation import ( _LastValueAggregation, ) +from opentelemetry.sdk.metrics._internal.instrument import ( + _Counter, + _Histogram, + _ObservableCounter, + _UpDownCounter, +) from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics._internal.metric_reader_storage import ( _DEFAULT_VIEW, @@ -292,7 +292,7 @@ def test_default_view_enabled(self, MockViewInstrumentMatch: Mock): def test_drop_aggregation(self): - counter = Counter("name", Mock(), Mock()) + counter = _Counter("name", Mock(), Mock()) metric_reader_storage = MetricReaderStorage( SdkConfiguration( resource=Mock(), @@ -324,8 +324,8 @@ def test_drop_aggregation(self): def test_same_collection_start(self): - counter = Counter("name", Mock(), Mock()) - up_down_counter = UpDownCounter("name", Mock(), Mock()) + counter = _Counter("name", Mock(), Mock()) + up_down_counter = _UpDownCounter("name", Mock(), Mock()) metric_reader_storage = MetricReaderStorage( SdkConfiguration( @@ -365,7 +365,7 @@ def test_same_collection_start(self): def test_conflicting_view_configuration(self): - observable_counter = ObservableCounter( + observable_counter = _ObservableCounter( "observable_counter", Mock(), [Mock()], @@ -406,14 +406,14 @@ def test_conflicting_view_configuration(self): def test_view_instrument_match_conflict_0(self): # There is a conflict between views and instruments. - observable_counter_0 = ObservableCounter( + observable_counter_0 = _ObservableCounter( "observable_counter_0", Mock(), [Mock()], unit="unit", description="description", ) - observable_counter_1 = ObservableCounter( + observable_counter_1 = _ObservableCounter( "observable_counter_1", Mock(), [Mock()], @@ -456,21 +456,21 @@ def test_view_instrument_match_conflict_0(self): def test_view_instrument_match_conflict_1(self): # There is a conflict between views and instruments. - observable_counter_foo = ObservableCounter( + observable_counter_foo = _ObservableCounter( "foo", Mock(), [Mock()], unit="unit", description="description", ) - observable_counter_bar = ObservableCounter( + observable_counter_bar = _ObservableCounter( "bar", Mock(), [Mock()], unit="unit", description="description", ) - observable_counter_baz = ObservableCounter( + observable_counter_baz = _ObservableCounter( "baz", Mock(), [Mock()], @@ -530,14 +530,14 @@ def test_view_instrument_match_conflict_1(self): def test_view_instrument_match_conflict_2(self): # There is no conflict because the metric streams names are different. - observable_counter_foo = ObservableCounter( + observable_counter_foo = _ObservableCounter( "foo", Mock(), [Mock()], unit="unit", description="description", ) - observable_counter_bar = ObservableCounter( + observable_counter_bar = _ObservableCounter( "bar", Mock(), [Mock()], @@ -578,14 +578,14 @@ def test_view_instrument_match_conflict_3(self): # There is no conflict because the aggregation temporality of the # instruments is different. - counter_bar = Counter( + counter_bar = _Counter( "bar", Mock(), [Mock()], unit="unit", description="description", ) - observable_counter_baz = ObservableCounter( + observable_counter_baz = _ObservableCounter( "baz", Mock(), [Mock()], @@ -626,14 +626,14 @@ def test_view_instrument_match_conflict_4(self): # There is no conflict because the monotonicity of the instruments is # different. - counter_bar = Counter( + counter_bar = _Counter( "bar", Mock(), [Mock()], unit="unit", description="description", ) - up_down_counter_baz = UpDownCounter( + up_down_counter_baz = _UpDownCounter( "baz", Mock(), [Mock()], @@ -673,14 +673,14 @@ def test_view_instrument_match_conflict_4(self): def test_view_instrument_match_conflict_5(self): # There is no conflict because the instrument units are different. - observable_counter_0 = ObservableCounter( + observable_counter_0 = _ObservableCounter( "observable_counter_0", Mock(), [Mock()], unit="unit_0", description="description", ) - observable_counter_1 = ObservableCounter( + observable_counter_1 = _ObservableCounter( "observable_counter_1", Mock(), [Mock()], @@ -720,14 +720,14 @@ def test_view_instrument_match_conflict_6(self): # There is no conflict because the instrument data points are # different. - observable_counter = ObservableCounter( + observable_counter = _ObservableCounter( "observable_counter", Mock(), [Mock()], unit="unit", description="description", ) - histogram = Histogram( + histogram = _Histogram( "histogram", Mock(), [Mock()], @@ -767,14 +767,14 @@ def test_view_instrument_match_conflict_7(self): # There is a conflict between views and instruments because the # description being different does not avoid a conflict. - observable_counter_0 = ObservableCounter( + observable_counter_0 = _ObservableCounter( "observable_counter_0", Mock(), [Mock()], unit="unit", description="description_0", ) - observable_counter_1 = ObservableCounter( + observable_counter_1 = _ObservableCounter( "observable_counter_1", Mock(), [Mock()], @@ -821,14 +821,14 @@ def test_view_instrument_match_conflict_8(self): # and also the temporality and monotonicity of the up down counter and # the histogram are the same. - observable_counter = UpDownCounter( + observable_counter = _UpDownCounter( "up_down_counter", Mock(), [Mock()], unit="unit", description="description", ) - histogram = Histogram( + histogram = _Histogram( "histogram", Mock(), [Mock()], diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index d2730086a4..c22c2d7a96 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -15,7 +15,6 @@ from unittest import TestCase from unittest.mock import MagicMock, Mock -from opentelemetry.sdk.metrics import Counter from opentelemetry.sdk.metrics._internal._view_instrument_match import ( _ViewInstrumentMatch, ) @@ -23,6 +22,7 @@ _DropAggregation, _LastValueAggregation, ) +from opentelemetry.sdk.metrics._internal.instrument import _Counter from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics._internal.sdk_configuration import ( SdkConfiguration, @@ -172,7 +172,7 @@ def test_consume_measurement(self): ) def test_collect(self): - instrument1 = Counter( + instrument1 = _Counter( "instrument1", Mock(), Mock(), @@ -213,7 +213,7 @@ def test_collect(self): self.assertEqual(number_data_point.value, 0) def test_data_point_check(self): - instrument1 = Counter( + instrument1 = _Counter( "instrument1", Mock(), Mock(), @@ -285,7 +285,7 @@ def test_data_point_check(self): self.assertEqual(len(list(result)), 3) def test_setting_aggregation(self): - instrument1 = Counter( + instrument1 = _Counter( name="instrument1", instrumentation_scope=Mock(), measurement_consumer=Mock(), @@ -301,7 +301,7 @@ def test_setting_aggregation(self): attribute_keys={"a", "c"}, ), instrument=instrument1, - instrument_class_aggregation={Counter: LastValueAggregation()}, + instrument_class_aggregation={_Counter: LastValueAggregation()}, ) view_instrument_match.consume_measurement( From c6d7b9f3dd4864445050f86e1e5a88100f1eb84e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 2 Aug 2022 19:41:05 +0200 Subject: [PATCH 1278/1517] Add force flush method to metric exporter (#2852) --- CHANGELOG.md | 2 ++ .../otlp/proto/grpc/metric_exporter/__init__.py | 3 +++ .../sdk/metrics/_internal/export/__init__.py | 10 ++++++++++ .../tests/metrics/test_backward_compat.py | 3 +++ opentelemetry-sdk/tests/metrics/test_metrics.py | 3 +++ .../metrics/test_periodic_exporting_metric_reader.py | 3 +++ 6 files changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 641d382961..d1332dda74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Add `force_flush` method to metrics exporter + ([#2852](https://github.com/open-telemetry/opentelemetry-python/pull/2852)) - Change tracing to use `Resource.to_json()` ([#2784](https://github.com/open-telemetry/opentelemetry-python/pull/2784)) - Fix get_log_emitter instrumenting_module_version args typo diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index fb316ab2e8..fce7129a32 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -210,3 +210,6 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: @property def _exporting(self) -> str: return "metrics" + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return True diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index ed5db17d97..a9b13f5c3f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -91,6 +91,13 @@ def export( The result of the export """ + @abstractmethod + def force_flush(self, timeout_millis: float = 10_000) -> bool: + """ + Ensure that export of any metrics currently received by the exporter + are completed as soon as possible. + """ + @abstractmethod def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: """Shuts down the exporter. @@ -131,6 +138,9 @@ def export( def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return True + class MetricReader(ABC): """ diff --git a/opentelemetry-sdk/tests/metrics/test_backward_compat.py b/opentelemetry-sdk/tests/metrics/test_backward_compat.py index 571d7a336a..a559c8378b 100644 --- a/opentelemetry-sdk/tests/metrics/test_backward_compat.py +++ b/opentelemetry-sdk/tests/metrics/test_backward_compat.py @@ -53,6 +53,9 @@ def export( def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return True + class OrigMetricReader(MetricReader): def _receive_metrics( diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 67950c4aaa..52154981d5 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -445,6 +445,9 @@ def export( def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return True + class TestDuplicateInstrumentAggregateData(TestCase): def test_duplicate_instrument_aggregate_data(self): diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index d0f5fc2c45..65b831d633 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -50,6 +50,9 @@ def export( def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: self._shutdown = True + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return True + metrics_list = [ Metric( From d8f32c95f73ffcfc81cd4f7ac1acabfae5b81dd5 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 3 Aug 2022 14:09:43 -0700 Subject: [PATCH 1279/1517] Respect OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE only for otlp exporter (#2843) --- CHANGELOG.md | 3 + .../proto/grpc/metric_exporter/__init__.py | 73 +++++++++++++++---- .../metrics/test_otlp_metrics_exporter.py | 49 ++++++++++++- .../sdk/metrics/_internal/export/__init__.py | 73 ++++++++----------- .../tests/metrics/test_metric_reader.py | 68 +---------------- .../tests/metrics/test_metrics.py | 9 ++- .../test_periodic_exporting_metric_reader.py | 47 +++++++++++- 7 files changed, 190 insertions(+), 132 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1332dda74..d6a9df74f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2754](https://github.com/open-telemetry/opentelemetry-python/pull/2754)) - Fix --insecure of CLI argument ([#2696](https://github.com/open-telemetry/opentelemetry-python/pull/2696)) +- Add temporality and aggregation configuration for metrics exporters, + use `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` only for OTLP metrics exporter + ([#2843](https://github.com/open-telemetry/opentelemetry-python/pull/2843)) - Instrument instances are always created through a Meter ([#2844](https://github.com/open-telemetry/opentelemetry-python/pull/2844)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index fce7129a32..04645021f9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -13,8 +13,9 @@ from logging import getLogger from os import environ -from typing import Optional, Sequence +from typing import Dict, Optional, Sequence from grpc import ChannelCredentials, Compression +from opentelemetry.sdk.metrics._internal.aggregation import Aggregation from opentelemetry.exporter.otlp.proto.grpc.exporter import ( OTLPExporterMixin, get_resource_data, @@ -29,18 +30,25 @@ from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_INSECURE, + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, ) -from opentelemetry.sdk.metrics.export import ( - Gauge, +from opentelemetry.sdk.metrics import ( + Counter, Histogram, - Metric, - Sum, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, ) - from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + Gauge, + Histogram as HistogramType, + Metric, MetricExporter, MetricExportResult, MetricsData, + Sum, ) _logger = getLogger(__name__) @@ -61,6 +69,8 @@ def __init__( headers: Optional[Sequence] = None, timeout: Optional[int] = None, compression: Optional[Compression] = None, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[type, Aggregation] = None, ): if insecure is None: @@ -68,15 +78,48 @@ def __init__( if insecure is not None: insecure = insecure.lower() == "true" - super().__init__( - **{ - "endpoint": endpoint, - "insecure": insecure, - "credentials": credentials, - "headers": headers, - "timeout": timeout, - "compression": compression, + instrument_class_temporality = {} + if ( + environ.get( + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + "CUMULATIVE", + ) + .upper() + .strip() + == "DELTA" + ): + instrument_class_temporality = { + Counter: AggregationTemporality.DELTA, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.DELTA, + ObservableCounter: AggregationTemporality.DELTA, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, } + else: + instrument_class_temporality = { + Counter: AggregationTemporality.CUMULATIVE, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.CUMULATIVE, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + instrument_class_temporality.update(preferred_temporality or {}) + + MetricExporter.__init__( + self, + preferred_temporality=instrument_class_temporality, + preferred_aggregation=preferred_aggregation, + ) + OTLPExporterMixin.__init__( + self, + endpoint=endpoint, + insecure=insecure, + credentials=credentials, + headers=headers, + timeout=timeout, + compression=compression, ) def _translate_data( @@ -132,7 +175,7 @@ def _translate_data( pt.as_double = data_point.value pb2_metric.gauge.data_points.append(pt) - elif isinstance(metric.data, Histogram): + elif isinstance(metric.data, HistogramType): for data_point in metric.data.data_points: pt = pb2.HistogramDataPoint( attributes=self._translate_attributes( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index c25ab06263..66151d0993 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -42,10 +42,19 @@ ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_INSECURE, + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, ) -from opentelemetry.sdk.metrics.export import ( - AggregationTemporality, +from opentelemetry.sdk.metrics import ( + Counter, Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.sdk.metrics.export import AggregationTemporality +from opentelemetry.sdk.metrics.export import Histogram as HistogramType +from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, Metric, MetricExportResult, @@ -121,7 +130,7 @@ def setUp(self): name="histogram", description="foo", unit="s", - data=Histogram( + data=HistogramType( data_points=[ HistogramDataPoint( attributes={"a": 1, "b": True}, @@ -302,6 +311,40 @@ def test_exporting(self): # pylint: disable=protected-access self.assertEqual(self.exporter._exporting, "metrics") + @patch.dict( + "os.environ", + {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"}, + ) + def test_preferred_temporality(self): + # pylint: disable=protected-access + exporter = OTLPMetricExporter( + preferred_temporality={Counter: AggregationTemporality.CUMULATIVE} + ) + self.assertEqual( + exporter._preferred_temporality[Counter], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + exporter._preferred_temporality[UpDownCounter], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + exporter._preferred_temporality[Histogram], + AggregationTemporality.DELTA, + ) + self.assertEqual( + exporter._preferred_temporality[ObservableCounter], + AggregationTemporality.DELTA, + ) + self.assertEqual( + exporter._preferred_temporality[ObservableUpDownCounter], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + exporter._preferred_temporality[ObservableGauge], + AggregationTemporality.CUMULATIVE, + ) + @patch( "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index a9b13f5c3f..e97557226f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -31,9 +31,6 @@ detach, set_value, ) -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, -) from opentelemetry.sdk.metrics._internal.aggregation import ( AggregationTemporality, DefaultAggregation, @@ -73,8 +70,26 @@ class MetricExporter(ABC): Interface to be implemented by services that want to export metrics received in their own format. + + Args: + preferred_temporality: Used by `opentelemetry.sdk.metrics.export.PeriodicExportingMetricReader` to + configure exporter level preferred temporality. See `opentelemetry.sdk.metrics.export.MetricReader` for + more details on what preferred temporality is. + preferred_aggregation: Used by `opentelemetry.sdk.metrics.export.PeriodicExportingMetricReader` to + configure exporter level preferred aggregation. See `opentelemetry.sdk.metrics.export.MetricReader` for + more details on what preferred aggregation is. """ + def __init__( + self, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[ + type, "opentelemetry.sdk.metrics.view.Aggregation" + ] = None, + ) -> None: + self._preferred_temporality = preferred_temporality + self._preferred_aggregation = preferred_aggregation + @abstractmethod def export( self, @@ -122,6 +137,7 @@ def __init__( ] = lambda metrics_data: metrics_data.to_json() + linesep, ): + super().__init__() self.out = out self.formatter = formatter @@ -143,6 +159,7 @@ def force_flush(self, timeout_millis: float = 10_000) -> bool: class MetricReader(ABC): + # pylint: disable=too-many-branches """ Base class for all metric readers @@ -157,8 +174,6 @@ class MetricReader(ABC): temporalities of the classes that the user wants to change, not all of them. The classes not included in the passed dictionary will retain their association to their default aggregation temporalities. - The value passed here will override the corresponding values set - via the environment variable preferred_aggregation: A mapping between instrument classes and aggregation instances. By default maps all instrument classes to an instance of `DefaultAggregation`. This mapping will be used to @@ -177,10 +192,6 @@ class MetricReader(ABC): .. automethod:: _receive_metrics """ - # FIXME add :std:envvar:`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` - # to the end of the documentation paragraph above. - - # pylint: disable=too-many-branches def __init__( self, preferred_temporality: Dict[type, AggregationTemporality] = None, @@ -196,33 +207,14 @@ def __init__( Iterable["opentelemetry.sdk.metrics.export.Metric"], ] = None - if ( - environ.get( - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, - "CUMULATIVE", - ) - .upper() - .strip() - == "DELTA" - ): - self._instrument_class_temporality = { - _Counter: AggregationTemporality.DELTA, - _UpDownCounter: AggregationTemporality.CUMULATIVE, - _Histogram: AggregationTemporality.DELTA, - _ObservableCounter: AggregationTemporality.DELTA, - _ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - _ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - else: - self._instrument_class_temporality = { - _Counter: AggregationTemporality.CUMULATIVE, - _UpDownCounter: AggregationTemporality.CUMULATIVE, - _Histogram: AggregationTemporality.CUMULATIVE, - _ObservableCounter: AggregationTemporality.CUMULATIVE, - _ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - _ObservableGauge: AggregationTemporality.CUMULATIVE, - } + self._instrument_class_temporality = { + _Counter: AggregationTemporality.CUMULATIVE, + _UpDownCounter: AggregationTemporality.CUMULATIVE, + _Histogram: AggregationTemporality.CUMULATIVE, + _ObservableCounter: AggregationTemporality.CUMULATIVE, + _ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + _ObservableGauge: AggregationTemporality.CUMULATIVE, + } if preferred_temporality is not None: for temporality in preferred_temporality.values(): @@ -404,16 +396,13 @@ class PeriodicExportingMetricReader(MetricReader): def __init__( self, exporter: MetricExporter, - preferred_temporality: Dict[type, AggregationTemporality] = None, - preferred_aggregation: Dict[ - type, "opentelemetry.sdk.metrics.view.Aggregation" - ] = None, export_interval_millis: Optional[float] = None, export_timeout_millis: Optional[float] = None, ) -> None: + # PeriodicExportingMetricReader defers to exporter for configuration super().__init__( - preferred_temporality=preferred_temporality, - preferred_aggregation=preferred_aggregation, + preferred_temporality=exporter._preferred_temporality, + preferred_aggregation=exporter._preferred_aggregation, ) self._exporter = exporter if export_interval_millis is None: diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_metric_reader.py index e0d2813a5f..82b65bd90c 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader.py @@ -12,14 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from os import environ from typing import Dict, Iterable from unittest import TestCase -from unittest.mock import patch -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, -) from opentelemetry.sdk.metrics import Counter, Histogram, ObservableGauge from opentelemetry.sdk.metrics._internal.instrument import ( _Counter, @@ -74,66 +69,7 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: class TestMetricReader(TestCase): - @patch.dict( - environ, - {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "CUMULATIVE"}, - ) - def test_configure_temporality_cumulative(self): - - dummy_metric_reader = DummyMetricReader() - - self.assertEqual( - dummy_metric_reader._instrument_class_temporality.keys(), - set(_expected_keys), - ) - for ( - value - ) in dummy_metric_reader._instrument_class_temporality.values(): - self.assertEqual(value, AggregationTemporality.CUMULATIVE) - - @patch.dict( - environ, {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"} - ) - def test_configure_temporality_delta(self): - - dummy_metric_reader = DummyMetricReader() - - self.assertEqual( - dummy_metric_reader._instrument_class_temporality.keys(), - set(_expected_keys), - ) - self.assertEqual( - dummy_metric_reader._instrument_class_temporality[_Counter], - AggregationTemporality.DELTA, - ) - self.assertEqual( - dummy_metric_reader._instrument_class_temporality[_UpDownCounter], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - dummy_metric_reader._instrument_class_temporality[_Histogram], - AggregationTemporality.DELTA, - ) - self.assertEqual( - dummy_metric_reader._instrument_class_temporality[ - _ObservableCounter - ], - AggregationTemporality.DELTA, - ) - self.assertEqual( - dummy_metric_reader._instrument_class_temporality[ - _ObservableUpDownCounter - ], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - dummy_metric_reader._instrument_class_temporality[ - _ObservableGauge - ], - AggregationTemporality.CUMULATIVE, - ) - - def test_configure_temporality_parameter(self): + def test_configure_temporality(self): dummy_metric_reader = DummyMetricReader( preferred_temporality={ @@ -177,7 +113,7 @@ def test_configure_temporality_parameter(self): AggregationTemporality.DELTA, ) - def test_default_temporality(self): + def test_configure_aggregation(self): dummy_metric_reader = DummyMetricReader() self.assertEqual( dummy_metric_reader._instrument_class_aggregation.keys(), diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 52154981d5..b128456531 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -64,9 +64,11 @@ def tearDown(self): MeterProvider._all_metric_readers = set() def test_register_metric_readers(self): - - metric_reader_0 = PeriodicExportingMetricReader(Mock()) - metric_reader_1 = PeriodicExportingMetricReader(Mock()) + mock_exporter = Mock() + mock_exporter._preferred_temporality = None + mock_exporter._preferred_aggregation = None + metric_reader_0 = PeriodicExportingMetricReader(mock_exporter) + metric_reader_1 = PeriodicExportingMetricReader(mock_exporter) try: MeterProvider(metric_readers=(metric_reader_0,)) @@ -429,6 +431,7 @@ def test_create_observable_up_down_counter(self): class InMemoryMetricExporter(MetricExporter): def __init__(self): + super().__init__() self.metrics = {} self._counter = 0 diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index 65b831d633..4213f9218e 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -18,7 +18,10 @@ from flaky import flaky +from opentelemetry.sdk.metrics import Counter +from opentelemetry.sdk.metrics._internal import _Counter from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, Gauge, Metric, MetricExporter, @@ -27,15 +30,25 @@ PeriodicExportingMetricReader, Sum, ) +from opentelemetry.sdk.metrics.view import ( + DefaultAggregation, + LastValueAggregation, +) from opentelemetry.test.concurrency_test import ConcurrencyTestBase from opentelemetry.util._time import _time_ns class FakeMetricsExporter(MetricExporter): - def __init__(self, wait=0): + def __init__( + self, wait=0, preferred_temporality=None, preferred_aggregation=None + ): self.wait = wait self.metrics = [] self._shutdown = False + super().__init__( + preferred_temporality=preferred_temporality, + preferred_aggregation=preferred_aggregation, + ) def export( self, @@ -114,7 +127,9 @@ def _collect(reader): def test_ticker_called(self): collect_mock = Mock() - pmr = PeriodicExportingMetricReader(Mock(), export_interval_millis=1) + exporter = FakeMetricsExporter() + exporter.export = Mock() + pmr = PeriodicExportingMetricReader(exporter, export_interval_millis=1) pmr._set_collect_callback(collect_mock) time.sleep(0.1) self.assertTrue(collect_mock.assert_called_once) @@ -141,8 +156,34 @@ def test_shutdown(self): self.assertTrue(exporter._shutdown) def test_shutdown_multiple_times(self): - pmr = self._create_periodic_reader([], Mock()) + pmr = self._create_periodic_reader([], FakeMetricsExporter()) with self.assertLogs(level="WARNING") as w: self.run_with_many_threads(pmr.shutdown) self.assertTrue("Can't shutdown multiple times", w.output[0]) pmr.shutdown() + + def test_exporter_temporality_preference(self): + exporter = FakeMetricsExporter( + preferred_temporality={ + Counter: AggregationTemporality.DELTA, + }, + ) + pmr = PeriodicExportingMetricReader(exporter) + for key, value in pmr._instrument_class_temporality.items(): + if key is not _Counter: + self.assertEqual(value, AggregationTemporality.CUMULATIVE) + else: + self.assertEqual(value, AggregationTemporality.DELTA) + + def test_exporter_aggregation_preference(self): + exporter = FakeMetricsExporter( + preferred_aggregation={ + Counter: LastValueAggregation(), + }, + ) + pmr = PeriodicExportingMetricReader(exporter) + for key, value in pmr._instrument_class_aggregation.items(): + if key is not _Counter: + self.assertTrue(isinstance(value, DefaultAggregation)) + else: + self.assertTrue(isinstance(value, LastValueAggregation)) From 22bfe8223594d459424fa70146a04c11162d5c0f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 4 Aug 2022 16:14:45 +0200 Subject: [PATCH 1280/1517] Refactor instrument name, unit and description checks (#2849) Fixes #2848 --- .../metrics/_internal/instrument.py | 41 +++++-- .../tests/metrics/test_instruments.py | 113 +++++++++++++++--- .../sdk/metrics/_internal/instrument.py | 22 +++- 3 files changed, 143 insertions(+), 33 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py index b203b8ffe7..fec2879ef6 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py @@ -21,12 +21,12 @@ from re import compile as re_compile from typing import ( Callable, + Dict, Generator, Generic, Iterable, Optional, Sequence, - Tuple, TypeVar, Union, ) @@ -74,20 +74,39 @@ def __init__( pass @staticmethod - def _check_name_and_unit(name: str, unit: str) -> Tuple[bool, bool]: + def _check_name_unit_description( + name: str, unit: str, description: str + ) -> Dict[str, Optional[str]]: """ - Checks the following instrument name and unit for compliance with the - spec. + Checks the following instrument name, unit and description for + compliance with the spec. - Returns a tuple of boolean value, the first one will be `True` if the - name is valid, `False` otherwise. The second value will be `True` if - the unit is valid, `False` otherwise. + Returns a dict with keys "name", "unit" and "description", the + corresponding values will be the checked strings or `None` if the value + is invalid. If valid, the checked strings should be used instead of the + original values. """ - return ( - _name_regex.fullmatch(name) is not None, - _unit_regex.fullmatch(unit) is not None, - ) + result: Dict[str, Optional[str]] = {} + + if _name_regex.fullmatch(name) is not None: + result["name"] = name + else: + result["name"] = None + + if unit is None: + unit = "" + if _unit_regex.fullmatch(unit) is not None: + result["unit"] = unit + else: + result["unit"] = None + + if description is None: + result["description"] = "" + else: + result["description"] = description + + return result class _ProxyInstrument(ABC, Generic[InstrumentT]): diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index 3e1e3fe745..4a3d3d448b 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -563,27 +563,108 @@ def test_observable_up_down_counter_callback(self): Signature.empty, ) - def test_name_regex(self): + def test_name_check(self): instrument = ChildInstrument("name") - self.assertTrue(instrument._check_name_and_unit("a" * 63, "unit")[0]) - self.assertTrue(instrument._check_name_and_unit("a.", "unit")[0]) - self.assertTrue(instrument._check_name_and_unit("a-", "unit")[0]) - self.assertTrue(instrument._check_name_and_unit("a_", "unit")[0]) - - self.assertFalse(instrument._check_name_and_unit("a" * 64, "unit")[0]) - self.assertFalse(instrument._check_name_and_unit("Ñ", "unit")[0]) - self.assertFalse(instrument._check_name_and_unit("_a", "unit")[0]) - self.assertFalse(instrument._check_name_and_unit("1a", "unit")[0]) - self.assertFalse(instrument._check_name_and_unit("", "unit")[0]) + self.assertEqual( + instrument._check_name_unit_description( + "a" * 63, "unit", "description" + )["name"], + "a" * 63, + ) + self.assertEqual( + instrument._check_name_unit_description( + "a.", "unit", "description" + )["name"], + "a.", + ) + self.assertEqual( + instrument._check_name_unit_description( + "a-", "unit", "description" + )["name"], + "a-", + ) + self.assertEqual( + instrument._check_name_unit_description( + "a_", "unit", "description" + )["name"], + "a_", + ) + + self.assertIsNone( + instrument._check_name_unit_description( + "a" * 64, "unit", "description" + )["name"] + ) + self.assertIsNone( + instrument._check_name_unit_description( + "Ñ", "unit", "description" + )["name"] + ) + self.assertIsNone( + instrument._check_name_unit_description( + "_a", "unit", "description" + )["name"] + ) + self.assertIsNone( + instrument._check_name_unit_description( + "1a", "unit", "description" + )["name"] + ) + self.assertIsNone( + instrument._check_name_unit_description("", "unit", "description")[ + "name" + ] + ) - def test_unit_regex(self): + def test_unit_check(self): instrument = ChildInstrument("name") - self.assertTrue(instrument._check_name_and_unit("name", "a" * 63)[1]) - self.assertTrue(instrument._check_name_and_unit("name", "{a}")[1]) + self.assertEqual( + instrument._check_name_unit_description( + "name", "a" * 63, "description" + )["unit"], + "a" * 63, + ) + self.assertEqual( + instrument._check_name_unit_description( + "name", "{a}", "description" + )["unit"], + "{a}", + ) + + self.assertIsNone( + instrument._check_name_unit_description( + "name", "a" * 64, "description" + )["unit"] + ) + self.assertIsNone( + instrument._check_name_unit_description( + "name", "Ñ", "description" + )["unit"] + ) + self.assertEqual( + instrument._check_name_unit_description( + "name", None, "description" + )["unit"], + "", + ) + + def test_description_check(self): - self.assertFalse(instrument._check_name_and_unit("name", "a" * 64)[1]) - self.assertFalse(instrument._check_name_and_unit("name", "Ñ")[1]) + instrument = ChildInstrument("name") + + self.assertEqual( + instrument._check_name_unit_description( + "name", "unit", "description" + )["description"], + "description", + ) + self.assertEqual( + instrument._check_name_unit_description("name", "unit", None)[ + "description" + ], + "", + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py index a0dc9c8da9..6c0320c479 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py @@ -50,13 +50,18 @@ def __init__( description: str = "", ): # pylint: disable=no-member - is_name_valid, is_unit_valid = self._check_name_and_unit(name, unit) + result = self._check_name_unit_description(name, unit, description) - if not is_name_valid: + if result["name"] is None: raise Exception(_ERROR_MESSAGE.format(name)) - if not is_unit_valid: + if result["unit"] is None: raise Exception(_ERROR_MESSAGE.format(unit)) + + name = result["name"] + unit = result["unit"] + description = result["description"] + self.name = name.lower() self.unit = unit self.description = description @@ -76,13 +81,18 @@ def __init__( description: str = "", ): # pylint: disable=no-member - is_name_valid, is_unit_valid = self._check_name_and_unit(name, unit) + result = self._check_name_unit_description(name, unit, description) - if not is_name_valid: + if result["name"] is None: raise Exception(_ERROR_MESSAGE.format(name)) - if not is_unit_valid: + if result["unit"] is None: raise Exception(_ERROR_MESSAGE.format(unit)) + + name = result["name"] + unit = result["unit"] + description = result["description"] + self.name = name.lower() self.unit = unit self.description = description From 1cae35f5ad3c5beabc067e57a3940d3dfeaac8e3 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 5 Aug 2022 22:27:02 +0200 Subject: [PATCH 1281/1517] Use force_flush (#2853) * Use force_flush Fixes #2816 * Refactor time out passing * Fix test cases * Add documentation * Add test case * Fix docs * Remove final decorator * Check for timeout in async calls * Remove timeout checking in metric reader * Add force_flush to PeriodicExportingMetricReader * Use TimeoutError * Fix lint * Address timeout comments * Add MetricsTimeoutError * Fix test case --- .../src/opentelemetry/sdk/metrics/__init__.py | 3 + .../sdk/metrics/_internal/__init__.py | 7 +- .../sdk/metrics/_internal/exceptions.py | 17 +++++ .../sdk/metrics/_internal/export/__init__.py | 20 ++++- .../metrics/_internal/measurement_consumer.py | 27 ++++++- .../metrics/test_measurement_consumer.py | 73 +++++++++++++++++++ .../tests/metrics/test_metric_reader.py | 7 ++ .../metrics/test_metric_reader_storage.py | 4 +- .../test_periodic_exporting_metric_reader.py | 4 +- 9 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exceptions.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index d57c3b5896..2219bc35c5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -18,6 +18,9 @@ Meter, MeterProvider, ) +from opentelemetry.sdk.metrics._internal.exceptions import ( # noqa: F401 + MetricsTimeoutError, +) from opentelemetry.sdk.metrics._internal.instrument import ( # noqa: F401 Counter, Histogram, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index 97d910a67b..8f9e10d9c2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -30,6 +30,7 @@ ObservableUpDownCounter as APIObservableUpDownCounter, ) from opentelemetry.metrics import UpDownCounter as APIUpDownCounter +from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError from opentelemetry.sdk.metrics._internal.instrument import ( _Counter, _Histogram, @@ -384,8 +385,10 @@ def force_flush(self, timeout_millis: float = 10_000) -> bool: current_ts = _time_ns() try: if current_ts >= deadline_ns: - raise Exception("Timed out while flushing metric readers") - metric_reader.collect( + raise MetricsTimeoutError( + "Timed out while flushing metric readers" + ) + metric_reader.force_flush( timeout_millis=(deadline_ns - current_ts) / 10**6 ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exceptions.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exceptions.py new file mode 100644 index 0000000000..0f8c3a7552 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exceptions.py @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class MetricsTimeoutError(Exception): + """Raised when a metrics function times out""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index e97557226f..3701229346 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -294,14 +294,23 @@ def __init__( def collect(self, timeout_millis: float = 10_000) -> None: """Collects the metrics from the internal SDK state and invokes the `_receive_metrics` with the collection. + + Args: + timeout_millis: Amount of time in milliseconds before this function + raises a timeout error. + + If any of the underlying ``collect`` methods called by this method + fails by any reason (including timeout) an exception will be raised + detailing the individual errors that caused this function to fail. """ if self._collect is None: _logger.warning( "Cannot call collect on a MetricReader until it is registered on a MeterProvider" ) return + self._receive_metrics( - self._collect(self), + self._collect(self, timeout_millis=timeout_millis), timeout_millis=timeout_millis, ) @@ -328,6 +337,10 @@ def _receive_metrics( ) -> None: """Called by `MetricReader.collect` when it receives a batch of metrics""" + def force_flush(self, timeout_millis: float = 10_000) -> bool: + self.collect(timeout_millis=timeout_millis) + return True + @abstractmethod def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: """Shuts down the MetricReader. This method provides a way @@ -485,3 +498,8 @@ def _shutdown(): self._shutdown_event.set() self._daemon_thread.join(timeout=(deadline_ns - _time_ns()) / 10**9) self._exporter.shutdown(timeout=(deadline_ns - _time_ns()) / 10**6) + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + super().force_flush(timeout_millis=timeout_millis) + self._exporter.force_flush(timeout_millis=timeout_millis) + return True diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py index 4ed72bca65..4da62b3f03 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py @@ -23,11 +23,13 @@ import opentelemetry.sdk.metrics._internal.instrument import opentelemetry.sdk.metrics._internal.sdk_configuration from opentelemetry.metrics._internal.instrument import CallbackOptions +from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics._internal.metric_reader_storage import ( MetricReaderStorage, ) from opentelemetry.sdk.metrics._internal.point import Metric +from opentelemetry.util._time import _time_ns class MeasurementConsumer(ABC): @@ -48,6 +50,7 @@ def register_asynchronous_instrument( def collect( self, metric_reader: "opentelemetry.sdk.metrics.MetricReader", + timeout_millis: float = 10_000, ) -> Iterable[Metric]: pass @@ -90,12 +93,34 @@ def register_asynchronous_instrument( def collect( self, metric_reader: "opentelemetry.sdk.metrics.MetricReader", + timeout_millis: float = 10_000, ) -> Iterable[Metric]: + with self._lock: metric_reader_storage = self._reader_storages[metric_reader] # for now, just use the defaults callback_options = CallbackOptions() + deadline_ns = _time_ns() + timeout_millis * 10**6 + + default_timeout_millis = 10000 * 10**6 + for async_instrument in self._async_instruments: - for measurement in async_instrument.callback(callback_options): + + remaining_time = deadline_ns - _time_ns() + + if remaining_time < default_timeout_millis: + + callback_options = CallbackOptions( + timeout_millis=remaining_time + ) + + measurements = async_instrument.callback(callback_options) + if _time_ns() >= deadline_ns: + raise MetricsTimeoutError( + "Timed out while executing callback" + ) + + for measurement in measurements: metric_reader_storage.consume_measurement(measurement) + return self._reader_storages[metric_reader].collect() diff --git a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py index 3a2a60626b..b1f3dc2a38 100644 --- a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py +++ b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from sys import version_info +from time import sleep from unittest import TestCase from unittest.mock import MagicMock, Mock, patch @@ -115,3 +117,74 @@ def test_collect_calls_async_instruments(self, MockMetricReaderStorage): self.assertEqual( len(reader_storage_mock.consume_measurement.mock_calls), 5 ) + + def test_collect_timeout(self, MockMetricReaderStorage): + reader_mock = Mock() + reader_storage_mock = Mock() + MockMetricReaderStorage.return_value = reader_storage_mock + consumer = SynchronousMeasurementConsumer( + SdkConfiguration( + resource=Mock(), + metric_readers=[reader_mock], + views=Mock(), + ) + ) + + def sleep_1(*args, **kwargs): + sleep(1) + + consumer.register_asynchronous_instrument( + Mock(**{"callback.side_effect": sleep_1}) + ) + + with self.assertRaises(Exception) as error: + consumer.collect(reader_mock, timeout_millis=10) + + self.assertIn( + "Timed out while executing callback", error.exception.args[0] + ) + + @patch( + "opentelemetry.sdk.metrics._internal." + "measurement_consumer.CallbackOptions" + ) + def test_collect_deadline( + self, mock_callback_options, MockMetricReaderStorage + ): + reader_mock = Mock() + reader_storage_mock = Mock() + MockMetricReaderStorage.return_value = reader_storage_mock + consumer = SynchronousMeasurementConsumer( + SdkConfiguration( + resource=Mock(), + metric_readers=[reader_mock], + views=Mock(), + ) + ) + + def sleep_1(*args, **kwargs): + sleep(1) + return [] + + consumer.register_asynchronous_instrument( + Mock(**{"callback.side_effect": sleep_1}) + ) + consumer.register_asynchronous_instrument( + Mock(**{"callback.side_effect": sleep_1}) + ) + + consumer.collect(reader_mock) + + if version_info < (3, 8): + callback_options_time_call = mock_callback_options.mock_calls[-1][ + 2 + ]["timeout_millis"] + else: + callback_options_time_call = mock_callback_options.mock_calls[ + -1 + ].kwargs["timeout_millis"] + + self.assertLess( + callback_options_time_call, + 10000 * 10**6, + ) diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_metric_reader.py index 82b65bd90c..86404328d6 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader.py @@ -14,6 +14,7 @@ from typing import Dict, Iterable from unittest import TestCase +from unittest.mock import patch from opentelemetry.sdk.metrics import Counter, Histogram, ObservableGauge from opentelemetry.sdk.metrics._internal.instrument import ( @@ -135,3 +136,9 @@ def test_configure_aggregation(self): dummy_metric_reader._instrument_class_aggregation[_Counter], LastValueAggregation, ) + + def test_force_flush(self): + + with patch.object(DummyMetricReader, "collect") as mock_collect: + DummyMetricReader().force_flush(timeout_millis=10) + mock_collect.assert_called_with(timeout_millis=10) diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index 7cea60d393..da45ffe4ae 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -821,7 +821,7 @@ def test_view_instrument_match_conflict_8(self): # and also the temporality and monotonicity of the up down counter and # the histogram are the same. - observable_counter = _UpDownCounter( + up_down_counter = _UpDownCounter( "up_down_counter", Mock(), [Mock()], @@ -859,7 +859,7 @@ def test_view_instrument_match_conflict_8(self): with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement( - Measurement(1, observable_counter) + Measurement(1, up_down_counter) ) with self.assertLogs(level=WARNING) as log: diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index 4213f9218e..bdc15eb963 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -118,9 +118,9 @@ def _create_periodic_reader( exporter, export_interval_millis=interval ) - def _collect(reader): + def _collect(reader, timeout_millis): time.sleep(collect_wait) - pmr._receive_metrics(metrics) + pmr._receive_metrics(metrics, timeout_millis) pmr._set_collect_callback(_collect) return pmr From 506300083e4b58947f8de64a9d42be5757fdccdd Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 9 Aug 2022 21:32:21 +0200 Subject: [PATCH 1282/1517] Release 1.12.0-0.33b0 (#2865) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ docs/examples/metrics/reader/requirements.txt | 8 ++++---- docs/examples/metrics/views/requirements.txt | 8 ++++---- eachdist.ini | 4 ++-- .../opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/setup.cfg | 2 +- .../opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/setup.cfg | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/setup.cfg | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/setup.cfg | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/setup.cfg | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 33 files changed, 46 insertions(+), 44 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9fcaa406f7..6068e43d5c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: c37a77e2efa9b67d4b017a3aa6d5e5f0bc5433c9 + CONTRIB_REPO_SHA: 1e7ea92f2e4df1ee8cd4b24ce4fa1dcefa20347a # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index d6a9df74f7..4db9af65e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0-0.33b0) - 2022-08-08 + - Add `force_flush` method to metrics exporter ([#2852](https://github.com/open-telemetry/opentelemetry-python/pull/2852)) - Change tracing to use `Resource.to_json()` diff --git a/docs/examples/metrics/reader/requirements.txt b/docs/examples/metrics/reader/requirements.txt index f690f9d1ca..be61271135 100644 --- a/docs/examples/metrics/reader/requirements.txt +++ b/docs/examples/metrics/reader/requirements.txt @@ -1,6 +1,6 @@ Deprecated==1.2.13 -opentelemetry-api==1.12.0rc2 -opentelemetry-sdk==1.12.0rc2 -opentelemetry-semantic-conventions==0.32b0 +opentelemetry-api==1.12.0 +opentelemetry-sdk==1.12.0 +opentelemetry-semantic-conventions==0.33b0 typing_extensions==4.3.0 -wrapt==1.14.1 \ No newline at end of file +wrapt==1.14.1 diff --git a/docs/examples/metrics/views/requirements.txt b/docs/examples/metrics/views/requirements.txt index f690f9d1ca..be61271135 100644 --- a/docs/examples/metrics/views/requirements.txt +++ b/docs/examples/metrics/views/requirements.txt @@ -1,6 +1,6 @@ Deprecated==1.2.13 -opentelemetry-api==1.12.0rc2 -opentelemetry-sdk==1.12.0rc2 -opentelemetry-semantic-conventions==0.32b0 +opentelemetry-api==1.12.0 +opentelemetry-sdk==1.12.0 +opentelemetry-semantic-conventions==0.33b0 typing_extensions==4.3.0 -wrapt==1.14.1 \ No newline at end of file +wrapt==1.14.1 diff --git a/eachdist.ini b/eachdist.ini index dca92043e9..44b1d15eb8 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.12.0rc2 +version=1.12.0 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.32b0 +version=0.33b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 414738e76e..9b9b7e7bef 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 414738e76e..9b9b7e7bef 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 14818cc366..5498969bd3 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.12.0rc2 - opentelemetry-exporter-jaeger-thrift == 1.12.0rc2 + opentelemetry-exporter-jaeger-proto-grpc == 1.12.0 + opentelemetry-exporter-jaeger-thrift == 1.12.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 414738e76e..9b9b7e7bef 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 268a795344..6b2801561b 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.32b0" +__version__ = "0.33b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index a98639cf68..d589bd075a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 - opentelemetry-proto == 1.12.0rc2 + opentelemetry-proto == 1.12.0 backoff >= 1.10.0, < 2.0.0; python_version<'3.7' backoff >= 1.10.0, < 3.0.0; python_version>='3.7' diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 5744f723da..797af7e49e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index 61d29f78b7..5df6d9dd64 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -44,7 +44,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 - opentelemetry-proto == 1.12.0rc2 + opentelemetry-proto == 1.12.0 backoff >= 1.10.0, < 2.0.0; python_version<'3.7' backoff >= 1.10.0, < 3.0.0; python_version>='3.7' diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 5744f723da..797af7e49e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 0dcd327d60..04cf27218f 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.12.0rc2 - opentelemetry-exporter-otlp-proto-http == 1.12.0rc2 + opentelemetry-exporter-otlp-proto-grpc == 1.12.0 + opentelemetry-exporter-otlp-proto-http == 1.12.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 5744f723da..797af7e49e 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 268a795344..6b2801561b 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.32b0" +__version__ = "0.33b0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 5744f723da..797af7e49e 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index fcd9d37c96..8382149a02 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 - opentelemetry-exporter-zipkin-json == 1.12.0rc2 + opentelemetry-exporter-zipkin-json == 1.12.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 5744f723da..797af7e49e 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 949b0dfb3a..af9d84f322 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.12.0rc2 - opentelemetry-exporter-zipkin-proto-http == 1.12.0rc2 + opentelemetry-exporter-zipkin-json == 1.12.0 + opentelemetry-exporter-zipkin-proto-http == 1.12.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 5744f723da..797af7e49e 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 5744f723da..797af7e49e 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 5744f723da..797af7e49e 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index b1c9ed9c1b..407b06a0da 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,8 +43,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.12.0rc2 - opentelemetry-semantic-conventions == 0.32b0 + opentelemetry-api == 1.12.0 + opentelemetry-semantic-conventions == 0.33b0 setuptools >= 16.0 dataclasses == 0.8; python_version < '3.7' typing-extensions >= 3.7.4 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 5744f723da..797af7e49e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 268a795344..6b2801561b 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.32b0" +__version__ = "0.33b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 5744f723da..797af7e49e 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 5744f723da..797af7e49e 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc2" +__version__ = "1.12.0" diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 14f45de7f0..5edba807d9 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -47,7 +47,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test-utils == 0.32b0 + opentelemetry-test-utils == 0.33b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 268a795344..6b2801561b 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.32b0" +__version__ = "0.33b0" diff --git a/tests/opentelemetry-test-utils/setup.cfg b/tests/opentelemetry-test-utils/setup.cfg index 659778feba..90769491b1 100644 --- a/tests/opentelemetry-test-utils/setup.cfg +++ b/tests/opentelemetry-test-utils/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.12.0rc2 - opentelemetry-sdk == 1.12.0rc2 + opentelemetry-api == 1.12.0 + opentelemetry-sdk == 1.12.0 asgiref ~= 3.0 [options.extras_require] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 6a7b29001a..3884a25fee 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.32b0" +__version__ = "0.33b0" From b26c5e8b4522ba6857464acfb3de4ece5fa6e614 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 15 Aug 2022 19:45:50 +0200 Subject: [PATCH 1283/1517] Stop MeterProvier attribute error message from showing up (#2875) --- .../src/opentelemetry/sdk/metrics/_internal/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index 8f9e10d9c2..cfb032a39b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -357,6 +357,8 @@ def __init__( self._atexit_handler = register(self.shutdown) self._meters = {} + self._shutdown_once = Once() + self._shutdown = False for metric_reader in self._sdk_config.metric_readers: @@ -373,9 +375,6 @@ def __init__( self._measurement_consumer.collect ) - self._shutdown_once = Once() - self._shutdown = False - def force_flush(self, timeout_millis: float = 10_000) -> bool: deadline_ns = _time_ns() + timeout_millis * 10**6 From ad3e23955273ab653f6140bd40d9d05c8a37e481 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 15 Aug 2022 14:19:33 -0400 Subject: [PATCH 1284/1517] Update PeriodicExportingMetricReader to never call export() concurrently (#2873) --- CHANGELOG.md | 3 + .../sdk/metrics/_internal/export/__init__.py | 16 ++- .../test_exporter_concurrency.py | 105 ++++++++++++++++++ 3 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 opentelemetry-sdk/tests/metrics/integration_test/test_exporter_concurrency.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4db9af65e7..96c13d3401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Update PeriodicExportingMetricReader to never call export() concurrently + ([#2873](https://github.com/open-telemetry/opentelemetry-python/pull/2873)) + ## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0-0.33b0) - 2022-08-08 - Add `force_flush` method to metrics exporter diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 3701229346..7137fd5395 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -18,7 +18,7 @@ from logging import getLogger from os import environ, linesep from sys import stdout -from threading import Event, RLock, Thread +from threading import Event, Lock, RLock, Thread from typing import IO, Callable, Dict, Iterable, Optional from typing_extensions import final @@ -404,6 +404,9 @@ class PeriodicExportingMetricReader(MetricReader): """`PeriodicExportingMetricReader` is an implementation of `MetricReader` that collects metrics based on a user-configurable time interval, and passes the metrics to the configured exporter. + + The configured exporter's :py:meth:`~MetricExporter.export` method will not be called + concurrently. """ def __init__( @@ -417,6 +420,12 @@ def __init__( preferred_temporality=exporter._preferred_temporality, preferred_aggregation=exporter._preferred_aggregation, ) + + # This lock is held whenever calling self._exporter.export() to prevent concurrent + # execution of MetricExporter.export() + # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exportbatch + self._export_lock = Lock() + self._exporter = exporter if export_interval_millis is None: try: @@ -479,7 +488,10 @@ def _receive_metrics( return token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: - self._exporter.export(metrics_data, timeout_millis=timeout_millis) + with self._export_lock: + self._exporter.export( + metrics_data, timeout_millis=timeout_millis + ) except Exception as e: # pylint: disable=broad-except,invalid-name _logger.exception("Exception while exporting metrics %s", str(e)) detach(token) diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_exporter_concurrency.py b/opentelemetry-sdk/tests/metrics/integration_test/test_exporter_concurrency.py new file mode 100644 index 0000000000..045afe0b29 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_exporter_concurrency.py @@ -0,0 +1,105 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from threading import Lock + +from opentelemetry.metrics import CallbackOptions, Observation +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + MetricExporter, + MetricExportResult, + MetricsData, + PeriodicExportingMetricReader, +) +from opentelemetry.test.concurrency_test import ConcurrencyTestBase + + +class MaxCountExporter(MetricExporter): + def __init__(self) -> None: + super().__init__(None, None) + self._lock = Lock() + + # the number of threads inside of export() + self.count_in_export = 0 + + # the total count of calls to export() + self.export_count = 0 + + # the maximum number of threads in export() ever + self.max_count_in_export = 0 + + def export( + self, + metrics_data: MetricsData, + timeout_millis: float = 10_000, + **kwargs, + ) -> MetricExportResult: + with self._lock: + self.export_count += 1 + self.count_in_export += 1 + + # yield to other threads + time.sleep(0) + + with self._lock: + self.max_count_in_export = max( + self.max_count_in_export, self.count_in_export + ) + self.count_in_export -= 1 + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return True + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + pass + + +class TestExporterConcurrency(ConcurrencyTestBase): + """ + Tests the requirement that: + + > `Export` will never be called concurrently for the same exporter instance. `Export` can + > be called again only after the current call returns. + + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exportbatch + """ + + def test_exporter_not_called_concurrently(self): + exporter = MaxCountExporter() + reader = PeriodicExportingMetricReader( + exporter=exporter, + export_interval_millis=100_000, + ) + meter_provider = MeterProvider(metric_readers=[reader]) + + def counter_cb(options: CallbackOptions): + yield Observation(2) + + meter_provider.get_meter(__name__).create_observable_counter( + "testcounter", callbacks=[counter_cb] + ) + + # call collect from a bunch of threads to try and enter export() concurrently + def test_many_threads(): + reader.collect() + + self.run_with_many_threads(test_many_threads, num_threads=100) + + # no thread should be in export() now + self.assertEqual(exporter.count_in_export, 0) + # should be one call for each thread + self.assertEqual(exporter.export_count, 100) + # should never have been more than one concurrent call + self.assertEqual(exporter.max_count_in_export, 1) From edb1391a3d29d844141b15e486cd1107e163ee0b Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Wed, 17 Aug 2022 21:02:16 +0800 Subject: [PATCH 1285/1517] Fix typos (#2868) --- CHANGELOG.md | 4 ++-- CONTRIBUTING.md | 2 +- RELEASING.md | 4 ++-- docs/examples/error_handler/README.rst | 2 +- docs/examples/logs/README.rst | 2 +- docs/examples/opentracing/main.py | 2 +- docs/sdk/logs.rst | 2 +- .../exporter/jaeger/thrift/gen/zipkincore/ttypes.py | 4 ++-- .../thrift/zipkincore.thrift | 6 +++--- mypy-relaxed.ini | 2 +- .../src/opentelemetry/context/contextvars_context.py | 2 +- .../src/opentelemetry/metrics/_internal/__init__.py | 2 +- opentelemetry-api/src/opentelemetry/trace/__init__.py | 10 +++++----- .../propagation/test_tracecontexthttptextformat.py | 2 +- opentelemetry-api/tests/trace/test_globals.py | 2 +- .../src/opentelemetry/sdk/_configuration/__init__.py | 2 +- .../src/opentelemetry/sdk/_logs/__init__.py | 2 +- .../src/opentelemetry/sdk/environment_variables.py | 6 +++--- .../sdk/metrics/_internal/export/__init__.py | 6 +++--- .../src/opentelemetry/sdk/trace/sampling.py | 2 +- opentelemetry-sdk/tests/logs/test_export.py | 4 ++-- .../tests/metrics/test_backward_compat.py | 2 +- opentelemetry-sdk/tests/trace/test_sampling.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- .../src/opentelemetry/semconv/resource/__init__.py | 2 +- scripts/eachdist.py | 2 +- .../opentelemetry/shim/opentracing_shim/__init__.py | 2 +- 27 files changed, 41 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96c13d3401..acd2377f2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -378,7 +378,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Allow span limits to be set programatically via TracerProvider. +- Allow span limits to be set programmatically via TracerProvider. ([#1877](https://github.com/open-telemetry/opentelemetry-python/pull/1877)) - Added support for CreateKey functionality. ([#1853](https://github.com/open-telemetry/opentelemetry-python/pull/1853)) @@ -808,7 +808,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1194](https://github.com/open-telemetry/opentelemetry-python/pull/1194)) - Make instances of SpanContext immutable ([#1134](https://github.com/open-telemetry/opentelemetry-python/pull/1134)) -- Parent is now always passed in via Context, intead of Span or SpanContext +- Parent is now always passed in via Context, instead of Span or SpanContext ([#1146](https://github.com/open-telemetry/opentelemetry-python/pull/1146)) - Update OpenTelemetry protos to v0.5.0 ([#1143](https://github.com/open-telemetry/opentelemetry-python/pull/1143)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ebf5aad8b4..283ad008fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -229,6 +229,6 @@ automatically load as options for the `opentelemetry-instrument` command. * docstrings should adhere to the [Google Python Style Guide](http://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) - as specified with the [napolean + as specified with the [napoleon extension](http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#google-vs-numpy) extension in [Sphinx](http://www.sphinx-doc.org/en/master/index.html). diff --git a/RELEASING.md b/RELEASING.md index cc02d74b59..4ce00da75d 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -80,12 +80,12 @@ A `hotfix` is defined as a small change developed to correct a bug that should b 1. Identify the packages that are affected by the bug. Make the changes to those packages, merging to `main`, as quickly as possible. 2. On your local machine, remove the `dev0` tags from the version number and increment the patch version number. 3. On your local machine, update `CHANGELOG.md` with the date of the hotfix change. -4. With administrator priviledges for PyPi, manually publish the affected packages. +4. With administrator privileges for PyPi, manually publish the affected packages. a. Install [twine](https://pypi.org/project/twine/) b. Navigate to where the `setup.py` file exists for the package you want to publish. c. Run `python setup.py sdist bdist_wheel`. You may have to install [wheel](https://pypi.org/project/wheel/) as well. d. Validate your built distributions by running `twine check dist/*`. - e. Upload distibutions to PyPi by running `twine upload dist/*`. + e. Upload distributions to PyPi by running `twine upload dist/*`. 5. Note that since hotfixes are manually published, the build scripts for publish after creating a release are not run. ## Troubleshooting diff --git a/docs/examples/error_handler/README.rst b/docs/examples/error_handler/README.rst index 45b3d0bdac..b879e53e9b 100644 --- a/docs/examples/error_handler/README.rst +++ b/docs/examples/error_handler/README.rst @@ -109,7 +109,7 @@ which just logs the exception to standard logging, as seen here: AssertionError When no exception is raised, the code inside the scope of -``GlobalErrorHandler`` is exectued normally: +``GlobalErrorHandler`` is executed normally: .. code:: diff --git a/docs/examples/logs/README.rst b/docs/examples/logs/README.rst index d9d52c42f5..a15150a1a8 100644 --- a/docs/examples/logs/README.rst +++ b/docs/examples/logs/README.rst @@ -4,7 +4,7 @@ OpenTelemetry Logs SDK .. warning:: OpenTelemetry Python logs are in an experimental state. The APIs within :mod:`opentelemetry.sdk._logs` are subject to change in minor/patch releases and make no - backward compatability guarantees at this time. + backward compatibility guarantees at this time. Start the Collector locally to see data being exported. Write the following file: diff --git a/docs/examples/opentracing/main.py b/docs/examples/opentracing/main.py index 8258b672e1..3975c4a45d 100755 --- a/docs/examples/opentracing/main.py +++ b/docs/examples/opentracing/main.py @@ -27,7 +27,7 @@ # Our example caching library expects an OpenTracing-compliant tracer. redis_cache = RedisCache(opentracing_tracer) -# Appication code uses an OpenTelemetry Tracer as usual. +# Application code uses an OpenTelemetry Tracer as usual. tracer = trace.get_tracer(__name__) diff --git a/docs/sdk/logs.rst b/docs/sdk/logs.rst index 6d9f3c2548..569ad30b79 100644 --- a/docs/sdk/logs.rst +++ b/docs/sdk/logs.rst @@ -4,7 +4,7 @@ opentelemetry.sdk._logs package .. warning:: OpenTelemetry Python logs are in an experimental state. The APIs within :mod:`opentelemetry.sdk._logs` are subject to change in minor/patch releases and make no - backward compatability guarantees at this time. + backward compatibility guarantees at this time. Once logs become stable, this package will be be renamed to ``opentelemetry.sdk.logs``. diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ttypes.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ttypes.py index 251c5a3694..a31db167ee 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ttypes.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ttypes.py @@ -387,7 +387,7 @@ class Span(object): precise value possible. For example, gettimeofday or syncing nanoTime against a tick of currentTimeMillis. - For compatibilty with instrumentation that precede this field, collectors + For compatibility with instrumentation that precede this field, collectors or span stores can derive this via Annotation.timestamp. For example, SERVER_RECV.timestamp or CLIENT_SEND.timestamp. @@ -399,7 +399,7 @@ class Span(object): precise measurement decoupled from problems of clocks, such as skew or NTP updates causing time to move backwards. - For compatibilty with instrumentation that precede this field, collectors + For compatibility with instrumentation that precede this field, collectors or span stores can derive this by subtracting Annotation.timestamp. For example, SERVER_SEND.timestamp - SERVER_RECV.timestamp. diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/thrift/zipkincore.thrift b/exporter/opentelemetry-exporter-jaeger-thrift/thrift/zipkincore.thrift index d5259e78b9..2004ed581b 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/thrift/zipkincore.thrift +++ b/exporter/opentelemetry-exporter-jaeger-thrift/thrift/zipkincore.thrift @@ -292,7 +292,7 @@ struct Span { 3: string name, 4: i64 id, # unique span id, only used for this span 5: optional i64 parent_id, # parent span id - 6: list annotations, # all annotations/events that occured, sorted by timestamp + 6: list annotations, # all annotations/events that occurred, sorted by timestamp 8: list binary_annotations # any binary annotations 9: optional bool debug = 0 # if true, we DEMAND that this span passes all samplers /** @@ -302,7 +302,7 @@ struct Span { * precise value possible. For example, gettimeofday or syncing nanoTime * against a tick of currentTimeMillis. * - * For compatibilty with instrumentation that precede this field, collectors + * For compatibility with instrumentation that precede this field, collectors * or span stores can derive this via Annotation.timestamp. * For example, SERVER_RECV.timestamp or CLIENT_SEND.timestamp. * @@ -317,7 +317,7 @@ struct Span { * precise measurement decoupled from problems of clocks, such as skew or NTP * updates causing time to move backwards. * - * For compatibilty with instrumentation that precede this field, collectors + * For compatibility with instrumentation that precede this field, collectors * or span stores can derive this by subtracting Annotation.timestamp. * For example, SERVER_SEND.timestamp - SERVER_RECV.timestamp. * diff --git a/mypy-relaxed.ini b/mypy-relaxed.ini index 205688353e..34538bdd79 100644 --- a/mypy-relaxed.ini +++ b/mypy-relaxed.ini @@ -1,4 +1,4 @@ -; This is mainly intended for unit tests and such. So proably going forward, we +; This is mainly intended for unit tests and such. So probably going forward, we ; will disable even more warnings here. [mypy] disallow_any_unimported = True diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index 5daee59a4d..7afded5701 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -24,7 +24,7 @@ class ContextVarsRuntimeContext(_RuntimeContext): """An implementation of the RuntimeContext interface which wraps ContextVar under - the hood. This is the prefered implementation for usage with Python 3.5+ + the hood. This is the preferred implementation for usage with Python 3.5+ """ _CONTEXT_KEY = "current_context" diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py index f3c37b3856..ccde6900d8 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py @@ -749,7 +749,7 @@ def set_mp() -> None: def set_meter_provider(meter_provider: MeterProvider) -> None: """Sets the current global :class:`~.MeterProvider` object. - This can only be done once, a warning will be logged if any furter attempt + This can only be done once, a warning will be logged if any further attempt is made. """ _set_meter_provider(meter_provider, log=True) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 53bb40f0e2..6f63eddc33 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -202,7 +202,7 @@ def get_tracer( vs. a functional tracer). Args: - instrumenting_module_name: The uniquely identifiable name for instrumentaion + instrumenting_module_name: The uniquely identifiable name for instrumentation scope, such as instrumentation library, package, module or class name. ``__name__`` may not be used as this can result in different tracer names if the tracers are in different files. @@ -318,7 +318,7 @@ def start_span( record_exception: Whether to record any exceptions raised within the context as error event on the span. set_status_on_exception: Only relevant if the returned span is used - in a with/context manager. Defines wether the span status will + in a with/context manager. Defines whether the span status will be automatically set to ERROR when an uncaught exception is raised in the span with block. The span status won't be set by this mechanism if it was previously set manually. @@ -391,7 +391,7 @@ def function(): record_exception: Whether to record any exceptions raised within the context as error event on the span. set_status_on_exception: Only relevant if the returned span is used - in a with/context manager. Defines wether the span status will + in a with/context manager. Defines whether the span status will be automatically set to ERROR when an uncaught exception is raised in the span with block. The span status won't be set by this mechanism if it was previously set manually. @@ -524,7 +524,7 @@ def set_tp() -> None: def set_tracer_provider(tracer_provider: TracerProvider) -> None: """Sets the current global :class:`~.TracerProvider` object. - This can only be done once, a warning will be logged if any furter attempt + This can only be done once, a warning will be logged if any further attempt is made. """ _set_tracer_provider(tracer_provider, log=True) @@ -562,7 +562,7 @@ def use_span( record_exception: Whether to record any exceptions raised within the context as error event on the span. set_status_on_exception: Only relevant if the returned span is used - in a with/context manager. Defines wether the span status will + in a with/context manager. Defines whether the span status will be automatically set to ERROR when an uncaught exception is raised in the span with block. The span status won't be set by this mechanism if it was previously set manually. diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 9d9561a4a5..ac57b5c919 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -45,7 +45,7 @@ def test_no_traceparent_header(self): def test_headers_with_tracestate(self): """When there is a traceparent and tracestate header, data from - both should be addded to the SpanContext. + both should be added to the SpanContext. """ traceparent_value = "00-{trace_id}-{span_id}-00".format( trace_id=format(self.TRACE_ID, "032x"), diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index a448437e98..c2cc80db82 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -77,7 +77,7 @@ def do_concurrently() -> Mock: mock_tps_with_any_call[0].get_tracer.call_count, num_threads ) - # should have warned everytime except for the successful set + # should have warned every time except for the successful set self.assertEqual(mock_logger.warning.call_count, num_threads - 1) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 55463e00be..d186cac6e2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -274,7 +274,7 @@ def configure(self, **kwargs): class _OTelSDKConfigurator(_BaseConfigurator): - """A basic Configurator by OTel Python for initalizing OTel SDK components + """A basic Configurator by OTel Python for initializing OTel SDK components Initializes several crucial OTel SDK components (i.e. TracerProvider, MeterProvider, Processors...) according to a default implementation. Other diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index 114e03b263..22ee11aafe 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -489,7 +489,7 @@ def get_log_emitter_provider() -> LogEmitterProvider: def set_log_emitter_provider(log_emitter_provider: LogEmitterProvider) -> None: """Sets the current global :class:`~.LogEmitterProvider` object. - This can only be done once, a warning will be logged if any furter attempt + This can only be done once, a warning will be logged if any further attempt is made. """ global _LOG_EMITTER_PROVIDER # pylint: disable=global-statement diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 4a62fe41c2..384d563d25 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -103,7 +103,7 @@ .. envvar:: OTEL_ATTRIBUTE_COUNT_LIMIT The :envvar:`OTEL_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed attribute count for spans, events and links. -This limit is overriden by model specific limits such as OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT. +This limit is overridden by model specific limits such as OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT. Default: 128 """ @@ -343,7 +343,7 @@ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_COMPRESSION Same as :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` but only for the span -exporter. If both are present, this takes higher precendence. +exporter. If both are present, this takes higher precedence. """ OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT" @@ -414,7 +414,7 @@ .. envvar:: OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED The :envvar:`OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED` environment variable allows users to -enable/disabe the logging SDK auto instrumentation. +enable/disable the logging SDK auto instrumentation. Default: False Note: Logs SDK and its related settings are experimental. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 7137fd5395..04354fc24a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -185,10 +185,10 @@ class MetricReader(ABC): to change, not necessarily all of them. The classes not included in the passed dictionary will retain their association to their default aggregations. The aggregation defined here will be - overriden by an aggregation defined by a view that is not + overridden by an aggregation defined by a view that is not `DefaultAggregation`. - .. document protected _receive_metrics which is a intended to be overriden by subclass + .. document protected _receive_metrics which is a intended to be overridden by subclass .. automethod:: _receive_metrics """ @@ -325,7 +325,7 @@ def _set_collect_callback( Iterable["opentelemetry.sdk.metrics.export.Metric"], ], ) -> None: - """This function is internal to the SDK. It should not be called or overriden by users""" + """This function is internal to the SDK. It should not be called or overridden by users""" self._collect = func @abstractmethod diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 4aac5f6f88..3cdd34cfe8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -76,7 +76,7 @@ Sampling probability can be set with ``OTEL_TRACES_SAMPLER_ARG`` if the sampler is traceidratio or parentbased_traceidratio, when not provided rate will be set to 1.0 (maximum rate possible). -Prev example but with environment vairables. Please make sure to set the env ``OTEL_TRACES_SAMPLER=traceidratio`` and ``OTEL_TRACES_SAMPLER_ARG=0.001``. +Prev example but with environment variables. Please make sure to set the env ``OTEL_TRACES_SAMPLER=traceidratio`` and ``OTEL_TRACES_SAMPLER_ARG=0.001``. .. code:: python diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index d7dbed76a6..6d597cc479 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -187,7 +187,7 @@ def test_shutdown(self): logger.warning("warning message: %s", "possible upcoming heatwave") logger.error("Very high rise in temperatures across the globe") - logger.critical("Temparature hits high 420 C in Hyderabad") + logger.critical("Temperature hits high 420 C in Hyderabad") log_processor.shutdown() self.assertTrue(exporter._stopped) @@ -197,7 +197,7 @@ def test_shutdown(self): ("warning message: possible upcoming heatwave", "WARNING"), ("Very high rise in temperatures across the globe", "ERROR"), ( - "Temparature hits high 420 C in Hyderabad", + "Temperature hits high 420 C in Hyderabad", "CRITICAL", ), ] diff --git a/opentelemetry-sdk/tests/metrics/test_backward_compat.py b/opentelemetry-sdk/tests/metrics/test_backward_compat.py index a559c8378b..7f8fff2acf 100644 --- a/opentelemetry-sdk/tests/metrics/test_backward_compat.py +++ b/opentelemetry-sdk/tests/metrics/test_backward_compat.py @@ -18,7 +18,7 @@ be made in such a way that existing implementations (outside of this repo) continue to work when *called* by the SDK. -This does not apply to classes which are not intended to be overriden by the user e.g. Meter +This does not apply to classes which are not intended to be overridden by the user e.g. Meter and PeriodicExportingMetricReader concrete class. Those may freely be modified in a backward-compatible way for *callers*. diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index c5f922c8e5..e976b0f551 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -311,7 +311,7 @@ def test_probability_sampler_limits(self): None, 0xFFFFFFFFFFFFFFFF, "span name" ).decision.is_sampled() ) - # Check that the higest effective sampling rate is actually lower than + # Check that the highest effective sampling rate is actually lower than # the highest theoretical sampling rate. If this test fails the test # above is wrong. self.assertLess( diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 24621a5978..13ccc20f10 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -118,7 +118,7 @@ def run_general_code(shutdown_on_exit, explicit_shutdown): self.assertTrue(out.startswith(b"1")) # test that shutdown is called only once even if Tracer.shutdown is - # called explicitely + # called explicitly out = run_general_code(True, True) self.assertTrue(out.startswith(b"1")) diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py index dc24d22b5f..028fedd6df 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py @@ -169,7 +169,7 @@ class ResourceAttributes: * **AWS Lambda:** The function [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html). Take care not to use the "invoked ARN" directly but replace any -[alias suffix](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html) with the resolved function version, as the same runtime instance may be invokable with multiple +[alias suffix](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html) with the resolved function version, as the same runtime instance may be invocable with multiple different aliases. * **GCP:** The [URI of the resource](https://cloud.google.com/iam/docs/full-resource-names) * **Azure:** The [Fully Qualified Resource ID](https://docs.microsoft.com/en-us/rest/api/resources/resources/get-by-id). diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 427f990fe1..cd2a3938f4 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -554,7 +554,7 @@ def update_changelog(path, version, new_entry): text = changelog.read() if f"## [{version}]" in text: raise AttributeError( - f"{path} already contans version {version}" + f"{path} already contains version {version}" ) with open(path, encoding="utf-8") as changelog: for line in changelog: diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index 9bc9ee89f1..7d29a3412f 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -730,7 +730,7 @@ def extract(self, format: object, carrier: object): """ # pylint: disable=redefined-builtin - # This implementation does not perform the extracing by itself but + # This implementation does not perform the extracting by itself but # uses the configured propagators in opentelemetry.propagators. # TODO: Support Format.BINARY once it is supported in # opentelemetry-python. From cd2b6a9d6857f3f9815c3ded29ad153fec74c816 Mon Sep 17 00:00:00 2001 From: Gal Bashan Date: Thu, 18 Aug 2022 00:26:28 +0300 Subject: [PATCH 1286/1517] Adding Codespell to the CI (#2878) --- .codespellrc | 4 ++++ .github/workflows/test.yml | 2 +- CONTRIBUTING.md | 1 + dev-requirements.txt | 1 + tox.ini | 10 ++++++++++ 5 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 .codespellrc diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000000..06101c9f48 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,4 @@ +[codespell] +# skipping auto generated folders +skip = ./.tox,./.mypy_cache,./docs/_build,./target +ignore-words-list = ans,ue,ot,hist diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6068e43d5c..5d7277e910 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -92,7 +92,7 @@ jobs: strategy: fail-fast: false matrix: - tox-environment: [ "docker-tests", "lint", "docs", "mypy", "mypyinstalled", "tracecontext" ] + tox-environment: [ "docker-tests", "lint", "spellcheck", "docs", "mypy", "mypyinstalled", "tracecontext" ] name: ${{ matrix.tox-environment }} runs-on: ubuntu-20.04 steps: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 283ad008fe..7ef351b38c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,6 +61,7 @@ You can run `tox` with the following arguments: - `tox -e opentelemetry-api` and `tox -e opentelemetry-sdk` to run the API and SDK unit tests - `tox -e py37-opentelemetry-api` to e.g. run the API unit tests under a specific Python version +- `tox -e spellcheck` to run a spellcheck on all the code - `tox -e lint` to run lint checks on all code `black` and `isort` are executed when `tox -e lint` is run. The reported errors can be tedious to fix manually. diff --git a/dev-requirements.txt b/dev-requirements.txt index 77caf0ff2e..3af6328fad 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -18,3 +18,4 @@ protobuf~=3.18.1 # breaking change introduced in markupsafe causes jinja, flask to break markupsafe==2.0.1 bleach==4.1.0 # This dependency was updated to a breaking version. +codespell==2.1.0 diff --git a/tox.ini b/tox.ini index 73df1f6928..ec7bb09bda 100644 --- a/tox.ini +++ b/tox.ini @@ -62,6 +62,7 @@ envlist = pypy3-opentelemetry-propagator-jaeger lint + spellcheck tracecontext mypy,mypyinstalled docs @@ -180,6 +181,15 @@ commands = ; implicit Any due to unfollowed import would result). mypyinstalled: mypy --install-types --non-interactive --namespace-packages opentelemetry-api/tests/mypysmoke.py --strict +[testenv:spellcheck] +basepython: python3.9 +recreate = True +deps = + codespell + +commands = + codespell + [testenv:lint] basepython: python3.9 recreate = True From 91e3072abb032e4cd7f687703e78439675fafb6a Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Sat, 20 Aug 2022 03:32:20 +0530 Subject: [PATCH 1287/1517] Skip LICENSE files for spellcheck (#2879) --- .codespellrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.codespellrc b/.codespellrc index 06101c9f48..ab0fb6bb92 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,4 +1,4 @@ [codespell] # skipping auto generated folders -skip = ./.tox,./.mypy_cache,./docs/_build,./target -ignore-words-list = ans,ue,ot,hist +skip = ./.tox,./.mypy_cache,./docs/_build,./target,*/LICENSE,./venv +ignore-words-list = ans,ue,ot,hist,ro From e6c802088580db2bf819f482b31cf0997c11ab7c Mon Sep 17 00:00:00 2001 From: Doug Ramirez Date: Sat, 20 Aug 2022 01:26:11 -0400 Subject: [PATCH 1288/1517] Add param for indent size to LogRecord.to_json() (#2870) --- CHANGELOG.md | 2 ++ opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd2377f2b..645527eaef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update PeriodicExportingMetricReader to never call export() concurrently ([#2873](https://github.com/open-telemetry/opentelemetry-python/pull/2873)) +- Add param for `indent` size to `LogRecord.to_json()` + ([#2870](https://github.com/open-telemetry/opentelemetry-python/pull/2870)) ## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0-0.33b0) - 2022-08-08 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index 22ee11aafe..31d4e15f55 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -78,7 +78,7 @@ def __eq__(self, other: object) -> bool: return NotImplemented return self.__dict__ == other.__dict__ - def to_json(self) -> str: + def to_json(self, indent=4) -> str: return json.dumps( { "body": self.body, @@ -97,7 +97,7 @@ def to_json(self) -> str: if self.resource else "", }, - indent=4, + indent=indent, ) From 776b150660136f894f0c93033218a2c023754ef6 Mon Sep 17 00:00:00 2001 From: Pranav Marla <6486632+pranavmarla@users.noreply.github.com> Date: Mon, 22 Aug 2022 16:44:43 -0400 Subject: [PATCH 1289/1517] Remove LogEmitter.flush() to align with OTel Log Spec (#2863) --- CHANGELOG.md | 4 +- docs/examples/logs/example.py | 5 +- .../sdk/_configuration/__init__.py | 5 +- .../src/opentelemetry/sdk/_logs/__init__.py | 17 +++-- opentelemetry-sdk/tests/logs/test_export.py | 38 ++++++----- opentelemetry-sdk/tests/logs/test_handler.py | 63 ++++++++++++++----- .../tests/logs/test_multi_log_prcessor.py | 3 +- opentelemetry-sdk/tests/test_configurator.py | 8 +-- 8 files changed, 86 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 645527eaef..9024086922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2873](https://github.com/open-telemetry/opentelemetry-python/pull/2873)) - Add param for `indent` size to `LogRecord.to_json()` ([#2870](https://github.com/open-telemetry/opentelemetry-python/pull/2870)) +- Fix: Remove `LogEmitter.flush()` to align with OTel Log spec + ([#2863](https://github.com/open-telemetry/opentelemetry-python/pull/2863)) ## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0-0.33b0) - 2022-08-08 @@ -29,7 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add temporality and aggregation configuration for metrics exporters, use `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` only for OTLP metrics exporter ([#2843](https://github.com/open-telemetry/opentelemetry-python/pull/2843)) -- Instrument instances are always created through a Meter +- Instrument instances are always created through a Meter ([#2844](https://github.com/open-telemetry/opentelemetry-python/pull/2844)) ## [1.12.0rc2-0.32b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc2) - 2022-07-04 diff --git a/docs/examples/logs/example.py b/docs/examples/logs/example.py index 28fca75e7c..ff05251212 100644 --- a/docs/examples/logs/example.py +++ b/docs/examples/logs/example.py @@ -34,8 +34,9 @@ exporter = OTLPLogExporter(insecure=True) log_emitter_provider.add_log_processor(BatchLogProcessor(exporter)) -log_emitter = log_emitter_provider.get_log_emitter(__name__, "0.1") -handler = LoggingHandler(level=logging.NOTSET, log_emitter=log_emitter) +handler = LoggingHandler( + level=logging.NOTSET, log_emitter_provider=log_emitter_provider +) # Attach OTLP handler to root logger logging.getLogger().addHandler(handler) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index d186cac6e2..25a6580f1b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -151,8 +151,9 @@ def _init_logging( BatchLogProcessor(exporter_class(**exporter_args)) ) - log_emitter = provider.get_log_emitter(__name__) - handler = LoggingHandler(level=logging.NOTSET, log_emitter=log_emitter) + handler = LoggingHandler( + level=logging.NOTSET, log_emitter_provider=provider + ) logging.getLogger().addHandler(handler) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index 31d4e15f55..247e8ca7dc 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -311,10 +311,15 @@ class LoggingHandler(logging.Handler): def __init__( self, level=logging.NOTSET, - log_emitter=None, + log_emitter_provider=None, ) -> None: super().__init__(level=level) - self._log_emitter = log_emitter or get_log_emitter(__name__) + self._log_emitter_provider = ( + log_emitter_provider or get_log_emitter_provider() + ) + self._log_emitter = get_log_emitter( + __name__, log_emitter_provider=self._log_emitter_provider + ) @staticmethod def _get_attributes(record: logging.LogRecord) -> Attributes: @@ -369,7 +374,7 @@ def flush(self) -> None: """ Flushes the logging output. """ - self._log_emitter.flush() + self._log_emitter_provider.force_flush() class LogEmitter: @@ -396,12 +401,6 @@ def emit(self, record: LogRecord): log_data = LogData(record, self._instrumentation_scope) self._multi_log_processor.emit(log_data) - # TODO: Should this flush everything in pipeline? - # Prior discussion https://github.com/open-telemetry/opentelemetry-python/pull/1916#discussion_r659945290 - def flush(self): - """Ensure all logging output has been flushed.""" - self._multi_log_processor.force_flush() - class LogEmitterProvider: def __init__( diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 6d597cc479..3c55415d52 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -48,12 +48,13 @@ class TestSimpleLogProcessor(unittest.TestCase): def test_simple_log_processor_default_level(self): exporter = InMemoryLogExporter() log_emitter_provider = LogEmitterProvider() - log_emitter = log_emitter_provider.get_log_emitter(__name__) log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) logger = logging.getLogger("default_level") - logger.addHandler(LoggingHandler(log_emitter=log_emitter)) + logger.addHandler( + LoggingHandler(log_emitter_provider=log_emitter_provider) + ) logger.warning("Something is wrong") finished_logs = exporter.get_finished_logs() @@ -68,13 +69,14 @@ def test_simple_log_processor_default_level(self): def test_simple_log_processor_custom_level(self): exporter = InMemoryLogExporter() log_emitter_provider = LogEmitterProvider() - log_emitter = log_emitter_provider.get_log_emitter(__name__) log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) logger = logging.getLogger("custom_level") logger.setLevel(logging.ERROR) - logger.addHandler(LoggingHandler(log_emitter=log_emitter)) + logger.addHandler( + LoggingHandler(log_emitter_provider=log_emitter_provider) + ) logger.warning("Warning message") logger.debug("Debug message") @@ -99,12 +101,13 @@ def test_simple_log_processor_custom_level(self): def test_simple_log_processor_trace_correlation(self): exporter = InMemoryLogExporter() log_emitter_provider = LogEmitterProvider() - log_emitter = log_emitter_provider.get_log_emitter("name", "version") log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) logger = logging.getLogger("trace_correlation") - logger.addHandler(LoggingHandler(log_emitter=log_emitter)) + logger.addHandler( + LoggingHandler(log_emitter_provider=log_emitter_provider) + ) logger.warning("Warning message") finished_logs = exporter.get_finished_logs() @@ -137,12 +140,13 @@ def test_simple_log_processor_trace_correlation(self): def test_simple_log_processor_shutdown(self): exporter = InMemoryLogExporter() log_emitter_provider = LogEmitterProvider() - log_emitter = log_emitter_provider.get_log_emitter(__name__) log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) logger = logging.getLogger("shutdown") - logger.addHandler(LoggingHandler(log_emitter=log_emitter)) + logger.addHandler( + LoggingHandler(log_emitter_provider=log_emitter_provider) + ) logger.warning("Something is wrong") finished_logs = exporter.get_finished_logs() @@ -167,9 +171,8 @@ def test_emit_call_log_record(self): provider = LogEmitterProvider() provider.add_log_processor(log_processor) - emitter = provider.get_log_emitter(__name__) logger = logging.getLogger("emit_call") - logger.addHandler(LoggingHandler(log_emitter=emitter)) + logger.addHandler(LoggingHandler(log_emitter_provider=provider)) logger.error("error") self.assertEqual(log_processor.emit.call_count, 1) @@ -181,9 +184,8 @@ def test_shutdown(self): provider = LogEmitterProvider() provider.add_log_processor(log_processor) - emitter = provider.get_log_emitter(__name__) logger = logging.getLogger("shutdown") - logger.addHandler(LoggingHandler(log_emitter=emitter)) + logger.addHandler(LoggingHandler(log_emitter_provider=provider)) logger.warning("warning message: %s", "possible upcoming heatwave") logger.error("Very high rise in temperatures across the globe") @@ -214,9 +216,8 @@ def test_force_flush(self): provider = LogEmitterProvider() provider.add_log_processor(log_processor) - emitter = provider.get_log_emitter(__name__) logger = logging.getLogger("force_flush") - logger.addHandler(LoggingHandler(log_emitter=emitter)) + logger.addHandler(LoggingHandler(log_emitter_provider=provider)) logger.critical("Earth is burning") log_processor.force_flush() @@ -233,9 +234,8 @@ def test_log_processor_too_many_logs(self): provider = LogEmitterProvider() provider.add_log_processor(log_processor) - emitter = provider.get_log_emitter(__name__) logger = logging.getLogger("many_logs") - logger.addHandler(LoggingHandler(log_emitter=emitter)) + logger.addHandler(LoggingHandler(log_emitter_provider=provider)) for log_no in range(1000): logger.critical("Log no: %s", log_no) @@ -251,9 +251,8 @@ def test_with_multiple_threads(self): provider = LogEmitterProvider() provider.add_log_processor(log_processor) - emitter = provider.get_log_emitter(__name__) logger = logging.getLogger("threads") - logger.addHandler(LoggingHandler(log_emitter=emitter)) + logger.addHandler(LoggingHandler(log_emitter_provider=provider)) def bulk_log_and_flush(num_logs): for _ in range(num_logs): @@ -286,9 +285,8 @@ def test_batch_log_processor_fork(self): provider = LogEmitterProvider() provider.add_log_processor(log_processor) - emitter = provider.get_log_emitter(__name__) logger = logging.getLogger("test-fork") - logger.addHandler(LoggingHandler(log_emitter=emitter)) + logger.addHandler(LoggingHandler(log_emitter_provider=provider)) logger.critical("yolo") time.sleep(0.5) # give some time for the exporter to upload diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index a5c8c85643..19fa891dea 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -16,23 +16,32 @@ from unittest.mock import Mock from opentelemetry.sdk import trace -from opentelemetry.sdk._logs import LogEmitter, LoggingHandler +from opentelemetry.sdk._logs import ( + LogEmitterProvider, + LoggingHandler, + get_log_emitter, +) from opentelemetry.sdk._logs.severity import SeverityNumber from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import INVALID_SPAN_CONTEXT -def get_logger(level=logging.NOTSET, log_emitter=None): +def get_logger(level=logging.NOTSET, log_emitter_provider=None): logger = logging.getLogger(__name__) - handler = LoggingHandler(level=level, log_emitter=log_emitter) + handler = LoggingHandler( + level=level, log_emitter_provider=log_emitter_provider + ) logger.addHandler(handler) return logger class TestLoggingHandler(unittest.TestCase): def test_handler_default_log_level(self): - emitter_mock = Mock(spec=LogEmitter) - logger = get_logger(log_emitter=emitter_mock) + emitter_provider_mock = Mock(spec=LogEmitterProvider) + emitter_mock = get_log_emitter( + __name__, log_emitter_provider=emitter_provider_mock + ) + logger = get_logger(log_emitter_provider=emitter_provider_mock) # Make sure debug messages are ignored by default logger.debug("Debug message") self.assertEqual(emitter_mock.emit.call_count, 0) @@ -41,8 +50,13 @@ def test_handler_default_log_level(self): self.assertEqual(emitter_mock.emit.call_count, 1) def test_handler_custom_log_level(self): - emitter_mock = Mock(spec=LogEmitter) - logger = get_logger(level=logging.ERROR, log_emitter=emitter_mock) + emitter_provider_mock = Mock(spec=LogEmitterProvider) + emitter_mock = get_log_emitter( + __name__, log_emitter_provider=emitter_provider_mock + ) + logger = get_logger( + level=logging.ERROR, log_emitter_provider=emitter_provider_mock + ) logger.warning("Warning message test custom log level") # Make sure any log with level < ERROR is ignored self.assertEqual(emitter_mock.emit.call_count, 0) @@ -51,8 +65,11 @@ def test_handler_custom_log_level(self): self.assertEqual(emitter_mock.emit.call_count, 2) def test_log_record_no_span_context(self): - emitter_mock = Mock(spec=LogEmitter) - logger = get_logger(log_emitter=emitter_mock) + emitter_provider_mock = Mock(spec=LogEmitterProvider) + emitter_mock = get_log_emitter( + __name__, log_emitter_provider=emitter_provider_mock + ) + logger = get_logger(log_emitter_provider=emitter_provider_mock) # Assert emit gets called for warning message logger.warning("Warning message") args, _ = emitter_mock.emit.call_args_list[0] @@ -67,8 +84,11 @@ def test_log_record_no_span_context(self): def test_log_record_user_attributes(self): """Attributes can be injected into logs by adding them to the LogRecord""" - emitter_mock = Mock(spec=LogEmitter) - logger = get_logger(log_emitter=emitter_mock) + emitter_provider_mock = Mock(spec=LogEmitterProvider) + emitter_mock = get_log_emitter( + __name__, log_emitter_provider=emitter_provider_mock + ) + logger = get_logger(log_emitter_provider=emitter_provider_mock) # Assert emit gets called for warning message logger.warning("Warning message", extra={"http.status_code": 200}) args, _ = emitter_mock.emit.call_args_list[0] @@ -79,8 +99,11 @@ def test_log_record_user_attributes(self): def test_log_record_exception(self): """Exception information will be included in attributes""" - emitter_mock = Mock(spec=LogEmitter) - logger = get_logger(log_emitter=emitter_mock) + emitter_provider_mock = Mock(spec=LogEmitterProvider) + emitter_mock = get_log_emitter( + __name__, log_emitter_provider=emitter_provider_mock + ) + logger = get_logger(log_emitter_provider=emitter_provider_mock) try: raise ZeroDivisionError("division by zero") except ZeroDivisionError: @@ -109,8 +132,11 @@ def test_log_record_exception(self): def test_log_exc_info_false(self): """Exception information will be included in attributes""" - emitter_mock = Mock(spec=LogEmitter) - logger = get_logger(log_emitter=emitter_mock) + emitter_provider_mock = Mock(spec=LogEmitterProvider) + emitter_mock = get_log_emitter( + __name__, log_emitter_provider=emitter_provider_mock + ) + logger = get_logger(log_emitter_provider=emitter_provider_mock) try: raise ZeroDivisionError("division by zero") except ZeroDivisionError: @@ -129,8 +155,11 @@ def test_log_exc_info_false(self): ) def test_log_record_trace_correlation(self): - emitter_mock = Mock(spec=LogEmitter) - logger = get_logger(log_emitter=emitter_mock) + emitter_provider_mock = Mock(spec=LogEmitterProvider) + emitter_mock = get_log_emitter( + __name__, log_emitter_provider=emitter_provider_mock + ) + logger = get_logger(log_emitter_provider=emitter_provider_mock) tracer = trace.TracerProvider().get_tracer(__name__) with tracer.start_as_current_span("test") as span: diff --git a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py index 6a5838231b..c87b8ba409 100644 --- a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py +++ b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py @@ -57,8 +57,7 @@ def force_flush(self, timeout_millis=30000): class TestLogProcessor(unittest.TestCase): def test_log_processor(self): provider = LogEmitterProvider() - log_emitter = provider.get_log_emitter(__name__) - handler = LoggingHandler(log_emitter=log_emitter) + handler = LoggingHandler(log_emitter_provider=provider) logs_list_1 = [] processor1 = AnotherLogProcessor(Mock(), logs_list_1) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 39548ff5d4..6afbf4d295 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -68,9 +68,12 @@ def __init__(self, resource=None): def add_log_processor(self, processor): self.processor = processor - def get_log_emitter(self, name): + def get_log_emitter(self, name, *args, **kwargs): return DummyLogEmitter(name, self.resource, self.processor) + def force_flush(self, *args, **kwargs): + pass + class DummyMeterProvider(MeterProvider): pass @@ -85,9 +88,6 @@ def __init__(self, name, resource, processor): def emit(self, record): self.processor.emit(record) - def flush(self): - pass - class DummyLogProcessor: def __init__(self, exporter): From 55977d92aa8d2b83694015da628371cc22256bb7 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 25 Aug 2022 18:17:03 +0200 Subject: [PATCH 1290/1517] Mark test as flaky (#2886) --- opentelemetry-sdk/tests/trace/export/test_export.py | 8 ++++++++ tox.ini | 1 + 2 files changed, 9 insertions(+) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index b1eb98ce2e..de9b1564ef 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -19,8 +19,11 @@ import unittest from concurrent.futures import ThreadPoolExecutor from logging import WARNING +from platform import python_implementation from unittest import mock +from flaky import flaky + from opentelemetry import trace as trace_api from opentelemetry.context import Context from opentelemetry.sdk import trace @@ -477,6 +480,11 @@ def test_batch_span_processor_reset_timeout(self): span_processor.shutdown() + if python_implementation() == "PyPy": + test_batch_span_processor_reset_timeout = flaky( + max_runs=2, min_passes=1 + )(test_batch_span_processor_reset_timeout) + def test_batch_span_processor_parameters(self): # zero max_queue_size self.assertRaises( diff --git a/tox.ini b/tox.ini index ec7bb09bda..d31e4adf38 100644 --- a/tox.ini +++ b/tox.ini @@ -204,6 +204,7 @@ deps = readme_renderer httpretty GitPython + flaky commands_pre = python -m pip install -e {toxinidir}/opentelemetry-api[test] From 9f198fdf8e7d04c6380a6e04d8e2c89b86f8bf18 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 25 Aug 2022 19:47:31 +0200 Subject: [PATCH 1291/1517] Remove support for 3.6 (#2763) --- .github/workflows/test.yml | 16 +++----- CHANGELOG.md | 4 +- README.md | 2 +- .../error_handler/error_handler_0/setup.cfg | 3 +- .../error_handler/error_handler_1/setup.cfg | 3 +- .../setup.cfg | 3 +- .../setup.cfg | 3 +- .../opentelemetry-exporter-jaeger/setup.cfg | 3 +- .../setup.cfg | 3 +- .../setup.cfg | 3 +- .../setup.cfg | 3 +- .../opentelemetry-exporter-otlp/setup.cfg | 3 +- .../setup.cfg | 3 +- .../setup.cfg | 3 +- .../tests/encoder/common_tests.py | 28 +------------- .../setup.cfg | 3 +- .../tests/encoder/common_tests.py | 28 +------------- .../opentelemetry-exporter-zipkin/setup.cfg | 3 +- opentelemetry-api/setup.cfg | 3 +- .../context/contextvars_context.py | 6 --- .../src/opentelemetry/util/_time.py | 34 ----------------- opentelemetry-proto/setup.cfg | 3 +- opentelemetry-sdk/setup.cfg | 3 +- .../src/opentelemetry/sdk/_logs/__init__.py | 6 +-- .../sdk/_logs/export/__init__.py | 6 +-- .../sdk/metrics/_internal/__init__.py | 10 ++--- .../_internal/_view_instrument_match.py | 4 +- .../sdk/metrics/_internal/export/__init__.py | 8 ++-- .../metrics/_internal/measurement_consumer.py | 8 ++-- .../_internal/metric_reader_storage.py | 4 +- .../sdk/metrics/_internal/view.py | 2 +- .../sdk/metrics/export/__init__.py | 4 -- .../src/opentelemetry/sdk/trace/__init__.py | 12 +++--- .../sdk/trace/export/__init__.py | 6 +-- .../tests/context/test_asyncio.py | 9 +---- .../test_periodic_exporting_metric_reader.py | 19 +++++----- opentelemetry-sdk/tests/trace/test_trace.py | 5 ++- opentelemetry-semantic-conventions/setup.cfg | 5 +-- .../opentelemetry-propagator-b3/setup.cfg | 5 +-- .../opentelemetry-propagator-jaeger/setup.cfg | 5 +-- shim/opentelemetry-opentracing-shim/setup.cfg | 3 +- .../tests/test_util.py | 5 +-- tests/opentelemetry-test-utils/setup.cfg | 3 +- tox.ini | 38 +++++++++---------- 44 files changed, 102 insertions(+), 231 deletions(-) delete mode 100644 opentelemetry-api/src/opentelemetry/util/_time.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5d7277e910..4af72ac18a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 1e7ea92f2e4df1ee8cd4b24ce4fa1dcefa20347a + CONTRIB_REPO_SHA: 23bfe9fcadc1e205fbce1b6cf58fc3004eecace3 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when @@ -23,7 +23,6 @@ jobs: build: env: # We use these variables to convert between tox and GHA version literals - py36: 3.6 py37: 3.7 py38: 3.8 py39: 3.9 @@ -34,7 +33,7 @@ jobs: strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [ py36, py37, py38, py39, py310, pypy3 ] + python-version: [ py37, py38, py39, py310, pypy3 ] package: ["api", "sdk", "semantic", "getting", "shim", "exporter", "protobuf", "propagator"] os: [ ubuntu-20.04, windows-2019 ] steps: @@ -55,11 +54,6 @@ jobs: .tox ~/.cache/pip key: v3-tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core - # tox fails on windows and Python3.6 when tox dir is reused between builds so we remove it - - name: fix for windows + py3.6 - if: ${{ matrix.os == 'windows-2019' && matrix.python-version == 'py36' }} - shell: pwsh - run: Remove-Item .\.tox\ -Force -Recurse -ErrorAction Ignore - name: Windows does not let git check out files with long names if: ${{ matrix.os == 'windows-2019'}} run: git config --system core.longpaths true @@ -117,17 +111,17 @@ jobs: run: tox -e ${{ matrix.tox-environment }} # Contrib unit test suite in order to ensure changes in core do not break anything in contrib. - # We only run contrib unit tests on the oldest supported Python version (3.6) as running the same tests + # We only run contrib unit tests on the oldest supported Python version (3.7) as running the same tests # on all versions is somewhat redundant. contrib-build: env: # We use these variables to convert between tox and GHA version literals - py36: 3.6 + py37: 3.7 runs-on: ${{ matrix.os }} strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [ py36 ] + python-version: [ py37 ] package: ["instrumentation", "exporter"] os: [ ubuntu-20.04] steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9024086922..a4af2ef82e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Remove support for 3.6 + ([#2763](https://github.com/open-telemetry/opentelemetry-python/pull/2763)) - Update PeriodicExportingMetricReader to never call export() concurrently ([#2873](https://github.com/open-telemetry/opentelemetry-python/pull/2873)) - Add param for `indent` size to `LogRecord.to_json()` @@ -14,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix: Remove `LogEmitter.flush()` to align with OTel Log spec ([#2863](https://github.com/open-telemetry/opentelemetry-python/pull/2863)) -## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0-0.33b0) - 2022-08-08 +## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0) - 2022-08-08 - Add `force_flush` method to metrics exporter ([#2852](https://github.com/open-telemetry/opentelemetry-python/pull/2852)) diff --git a/README.md b/README.md index 9a060129db..d41ba8a9c0 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ This page describes the Python [OpenTelemetry](https://opentelemetry.io/) implementation. OpenTelemetry is an observability framework for cloud-native software. ## Requirements -Unless otherwise noted, all published artifacts support Python 3.6 or higher. See CONTRIBUTING.md for additional instructions for building this project for development. +Unless otherwise noted, all published artifacts support Python 3.7 or higher. See CONTRIBUTING.md for additional instructions for building this project for development. ## Getting started diff --git a/docs/examples/error_handler/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg index 9eced88d7c..6f14742b62 100644 --- a/docs/examples/error_handler/error_handler_0/setup.cfg +++ b/docs/examples/error_handler/error_handler_0/setup.cfg @@ -25,14 +25,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/docs/examples/error_handler/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg index 8d09f423db..7b31bc84ce 100644 --- a/docs/examples/error_handler/error_handler_1/setup.cfg +++ b/docs/examples/error_handler/error_handler_1/setup.cfg @@ -25,14 +25,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index 36a5d2b27e..be0e897ef3 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -36,7 +35,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg index 8f55cd34d4..23cde387a4 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -36,7 +35,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 5498969bd3..890f99561e 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -36,7 +35,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index dd984a2346..cf613eca3c 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -36,7 +35,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index d589bd075a..9c28be2d1f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index 5df6d9dd64..18f029981e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 04cf27218f..91aa081840 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -36,7 +35,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg index be3f16ca6f..44df363114 100644 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ b/exporter/opentelemetry-exporter-prometheus/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg index 8e86fac255..a0e41cf8a3 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -36,7 +35,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py index aae4b3bb23..ada00c7c8e 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/common_tests.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. import abc -import json -import sys import unittest from typing import Dict, List @@ -478,28 +476,4 @@ def pop_and_sort(source_list, source_index, sort_key): return popped_item def assert_equal_encoded_spans(self, expected_spans, actual_spans): - if sys.version_info.major == 3 and sys.version_info.minor <= 5: - expected_spans = json.loads(expected_spans) - actual_spans = json.loads(actual_spans) - for expected_span, actual_span in zip( - expected_spans, actual_spans - ): - actual_annotations = self.pop_and_sort( - actual_span, "annotations", "timestamp" - ) - expected_annotations = self.pop_and_sort( - expected_span, "annotations", "timestamp" - ) - expected_binary_annotations = self.pop_and_sort( - expected_span, "binaryAnnotations", "key" - ) - actual_binary_annotations = self.pop_and_sort( - actual_span, "binaryAnnotations", "key" - ) - self.assertEqual(actual_span, expected_span) - self.assertEqual(actual_annotations, expected_annotations) - self.assertEqual( - actual_binary_annotations, expected_binary_annotations - ) - else: - self.assertEqual(expected_spans, actual_spans) + self.assertEqual(expected_spans, actual_spans) diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index 8382149a02..bdffdb7161 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -36,7 +35,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py index aae4b3bb23..ada00c7c8e 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/common_tests.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. import abc -import json -import sys import unittest from typing import Dict, List @@ -478,28 +476,4 @@ def pop_and_sort(source_list, source_index, sort_key): return popped_item def assert_equal_encoded_spans(self, expected_spans, actual_spans): - if sys.version_info.major == 3 and sys.version_info.minor <= 5: - expected_spans = json.loads(expected_spans) - actual_spans = json.loads(actual_spans) - for expected_span, actual_span in zip( - expected_spans, actual_spans - ): - actual_annotations = self.pop_and_sort( - actual_span, "annotations", "timestamp" - ) - expected_annotations = self.pop_and_sort( - expected_span, "annotations", "timestamp" - ) - expected_binary_annotations = self.pop_and_sort( - expected_span, "binaryAnnotations", "key" - ) - actual_binary_annotations = self.pop_and_sort( - actual_span, "binaryAnnotations", "key" - ) - self.assertEqual(actual_span, expected_span) - self.assertEqual(actual_annotations, expected_annotations) - self.assertEqual( - actual_binary_annotations, expected_binary_annotations - ) - else: - self.assertEqual(expected_spans, actual_spans) + self.assertEqual(expected_spans, actual_spans) diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index af9d84f322..09921337ad 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -36,7 +35,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index c15cd53a37..5b40671c42 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -36,7 +35,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index 7afded5701..5f606764fc 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -12,15 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. from contextvars import ContextVar -from sys import version_info from opentelemetry.context.context import Context, _RuntimeContext -if version_info < (3, 7): - import aiocontextvars # type: ignore # pylint: disable=import-error - - aiocontextvars # pylint: disable=pointless-statement - class ContextVarsRuntimeContext(_RuntimeContext): """An implementation of the RuntimeContext interface which wraps ContextVar under diff --git a/opentelemetry-api/src/opentelemetry/util/_time.py b/opentelemetry-api/src/opentelemetry/util/_time.py deleted file mode 100644 index a3fd113ce3..0000000000 --- a/opentelemetry-api/src/opentelemetry/util/_time.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from logging import getLogger -from sys import version_info - -if version_info.minor < 7: - getLogger(__name__).warning( # pylint: disable=logging-not-lazy - "You are using Python 3.%s. This version does not support timestamps " # pylint: disable=C0209 - "with nanosecond precision and the OpenTelemetry SDK will use " - "millisecond precision instead. Please refer to PEP 564 for more " - "information. Please upgrade to Python 3.7 or newer to use nanosecond " - "precision." % version_info.minor - ) - from time import time - - def _time_ns() -> int: - return int(time() * 1e9) - -else: - from time import time_ns - - _time_ns = time_ns diff --git a/opentelemetry-proto/setup.cfg b/opentelemetry-proto/setup.cfg index 94ec991771..5f67d7c3e8 100644 --- a/opentelemetry-proto/setup.cfg +++ b/opentelemetry-proto/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 407b06a0da..03c31ed0c7 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -36,7 +35,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index 247e8ca7dc..1a617ed3f0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -20,6 +20,7 @@ import os import threading import traceback +from time import time_ns from typing import Any, Callable, Optional, Tuple, Union, cast from opentelemetry.sdk._logs.severity import SeverityNumber, std_to_otlp @@ -37,7 +38,6 @@ ) from opentelemetry.trace.span import TraceFlags from opentelemetry.util._providers import _load_provider -from opentelemetry.util._time import _time_ns from opentelemetry.util.types import Attributes _logger = logging.getLogger(__name__) @@ -185,9 +185,9 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: True if all the log processors flushes the logs within timeout, False otherwise. """ - deadline_ns = _time_ns() + timeout_millis * 1000000 + deadline_ns = time_ns() + timeout_millis * 1000000 for lp in self._log_processors: - current_ts = _time_ns() + current_ts = time_ns() if current_ts >= deadline_ns: return False diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py index 826ba3f3d6..af0221d991 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py @@ -20,12 +20,12 @@ import sys import threading from os import linesep +from time import time_ns from typing import IO, Callable, Deque, List, Optional, Sequence from opentelemetry.context import attach, detach, set_value from opentelemetry.sdk._logs import LogData, LogProcessor, LogRecord from opentelemetry.util._once import Once -from opentelemetry.util._time import _time_ns _logger = logging.getLogger(__name__) @@ -205,9 +205,9 @@ def worker(self): if self._shutdown: break - start_ns = _time_ns() + start_ns = time_ns() self._export(flush_request) - end_ns = _time_ns() + end_ns = time_ns() # subtract the duration of this export call to the next timeout timeout = self._schedule_delay_millis / 1e3 - ( (end_ns - start_ns) / 1e9 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index cfb032a39b..92c8c3ed3f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -15,6 +15,7 @@ from atexit import register, unregister from logging import getLogger from threading import Lock +from time import time_ns from typing import Optional, Sequence # This kind of import is needed to avoid Sphinx errors. @@ -49,7 +50,6 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util._once import Once -from opentelemetry.util._time import _time_ns _logger = getLogger(__name__) @@ -376,12 +376,12 @@ def __init__( ) def force_flush(self, timeout_millis: float = 10_000) -> bool: - deadline_ns = _time_ns() + timeout_millis * 10**6 + deadline_ns = time_ns() + timeout_millis * 10**6 metric_reader_error = {} for metric_reader in self._sdk_config.metric_readers: - current_ts = _time_ns() + current_ts = time_ns() try: if current_ts >= deadline_ns: raise MetricsTimeoutError( @@ -413,7 +413,7 @@ def force_flush(self, timeout_millis: float = 10_000) -> bool: return True def shutdown(self, timeout_millis: float = 30_000): - deadline_ns = _time_ns() + timeout_millis * 10**6 + deadline_ns = time_ns() + timeout_millis * 10**6 def _shutdown(): self._shutdown = True @@ -427,7 +427,7 @@ def _shutdown(): metric_reader_error = {} for metric_reader in self._sdk_config.metric_readers: - current_ts = _time_ns() + current_ts = time_ns() try: if current_ts >= deadline_ns: raise Exception( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index 4ada52d91b..4cff25ae78 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -15,6 +15,7 @@ from logging import getLogger from threading import Lock +from time import time_ns from typing import Dict, Iterable from opentelemetry.metrics import Instrument @@ -28,7 +29,6 @@ from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics._internal.point import DataPointT from opentelemetry.sdk.metrics._internal.view import View -from opentelemetry.util._time import _time_ns _logger = getLogger(__name__) @@ -40,7 +40,7 @@ def __init__( instrument: Instrument, instrument_class_aggregation: Dict[type, Aggregation], ): - self._start_time_unix_nano = _time_ns() + self._start_time_unix_nano = time_ns() self._view = view self._instrument = instrument self._attributes_aggregation: Dict[frozenset, _Aggregation] = {} diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 04354fc24a..c5f4d3885e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -19,6 +19,7 @@ from os import environ, linesep from sys import stdout from threading import Event, Lock, RLock, Thread +from time import time_ns from typing import IO, Callable, Dict, Iterable, Optional from typing_extensions import final @@ -51,7 +52,6 @@ ) from opentelemetry.sdk.metrics._internal.point import MetricsData from opentelemetry.util._once import Once -from opentelemetry.util._time import _time_ns _logger = getLogger(__name__) @@ -497,7 +497,7 @@ def _receive_metrics( detach(token) def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: - deadline_ns = _time_ns() + timeout_millis * 10**6 + deadline_ns = time_ns() + timeout_millis * 10**6 def _shutdown(): self._shutdown = True @@ -508,8 +508,8 @@ def _shutdown(): return self._shutdown_event.set() - self._daemon_thread.join(timeout=(deadline_ns - _time_ns()) / 10**9) - self._exporter.shutdown(timeout=(deadline_ns - _time_ns()) / 10**6) + self._daemon_thread.join(timeout=(deadline_ns - time_ns()) / 10**9) + self._exporter.shutdown(timeout=(deadline_ns - time_ns()) / 10**6) def force_flush(self, timeout_millis: float = 10_000) -> bool: super().force_flush(timeout_millis=timeout_millis) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py index 4da62b3f03..9daf1eff46 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py @@ -16,6 +16,7 @@ from abc import ABC, abstractmethod from threading import Lock +from time import time_ns from typing import Iterable, List, Mapping # This kind of import is needed to avoid Sphinx errors. @@ -29,7 +30,6 @@ MetricReaderStorage, ) from opentelemetry.sdk.metrics._internal.point import Metric -from opentelemetry.util._time import _time_ns class MeasurementConsumer(ABC): @@ -100,13 +100,13 @@ def collect( metric_reader_storage = self._reader_storages[metric_reader] # for now, just use the defaults callback_options = CallbackOptions() - deadline_ns = _time_ns() + timeout_millis * 10**6 + deadline_ns = time_ns() + timeout_millis * 10**6 default_timeout_millis = 10000 * 10**6 for async_instrument in self._async_instruments: - remaining_time = deadline_ns - _time_ns() + remaining_time = deadline_ns - time_ns() if remaining_time < default_timeout_millis: @@ -115,7 +115,7 @@ def collect( ) measurements = async_instrument.callback(callback_options) - if _time_ns() >= deadline_ns: + if time_ns() >= deadline_ns: raise MetricsTimeoutError( "Timed out while executing callback" ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py index 702eab4a1e..4602b7b6bc 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py @@ -14,6 +14,7 @@ from logging import getLogger from threading import RLock +from time import time_ns from typing import Dict, List from opentelemetry.metrics import ( @@ -49,7 +50,6 @@ ) from opentelemetry.sdk.metrics._internal.view import View from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.util._time import _time_ns _logger = getLogger(__name__) @@ -130,7 +130,7 @@ def collect(self) -> MetricsData: # streams produced by the SDK, but we still align the output timestamps # for a single instrument. - collection_start_nanos = _time_ns() + collection_start_nanos = time_ns() with self._lock: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py index e4f67f0ff2..b930c7a966 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py @@ -17,7 +17,7 @@ from logging import getLogger from typing import Optional, Set, Type -# FIXME import from typing when support for 3.6 is removed +# FIXME import from typing when support for 3.7 is removed from typing_extensions import final from opentelemetry.metrics import Instrument diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index 02d3f2aabb..76021bd576 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -13,8 +13,6 @@ # limitations under the License. # pylint: disable=unused-import -# FIXME Remove when 3.6 is no longer supported -from sys import version_info as _version_info from opentelemetry.sdk.metrics._internal.export import ( # noqa: F401 AggregationTemporality, @@ -44,7 +42,5 @@ __all__ = [] for key, value in globals().copy().items(): if not key.startswith("_"): - if _version_info.minor == 6 and key in ["DataPointT", "DataT"]: - continue value.__module__ = __name__ __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 6d30e94ed5..51ede5e121 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -24,6 +24,7 @@ from collections import OrderedDict from contextlib import contextmanager from os import environ +from time import time_ns from types import MappingProxyType, TracebackType from typing import ( Any, @@ -64,7 +65,6 @@ from opentelemetry.trace import SpanContext from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types -from opentelemetry.util._time import _time_ns logger = logging.getLogger(__name__) @@ -184,9 +184,9 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: True if all span processors flushed their spans within the given timeout, False otherwise. """ - deadline_ns = _time_ns() + timeout_millis * 1000000 + deadline_ns = time_ns() + timeout_millis * 1000000 for sp in self._span_processors: - current_time_ns = _time_ns() + current_time_ns = time_ns() if current_time_ns >= deadline_ns: return False @@ -286,7 +286,7 @@ class EventBase(abc.ABC): def __init__(self, name: str, timestamp: Optional[int] = None) -> None: self._name = name if timestamp is None: - self._timestamp = _time_ns() + self._timestamp = time_ns() else: self._timestamp = timestamp @@ -864,7 +864,7 @@ def start( logger.warning("Calling start() on a started span.") return self._start_time = ( - start_time if start_time is not None else _time_ns() + start_time if start_time is not None else time_ns() ) self._span_processor.on_start(self, parent_context=parent_context) @@ -877,7 +877,7 @@ def end(self, end_time: Optional[int] = None) -> None: logger.warning("Calling end() on an ended span.") return - self._end_time = end_time if end_time is not None else _time_ns() + self._end_time = end_time if end_time is not None else time_ns() self._span_processor.on_end(self._readable_span()) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index c574a327f0..558618c8d7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -20,6 +20,7 @@ import typing from enum import Enum from os import environ, linesep +from time import time_ns from typing import Optional from opentelemetry.context import ( @@ -37,7 +38,6 @@ ) from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor from opentelemetry.util._once import Once -from opentelemetry.util._time import _time_ns logger = logging.getLogger(__name__) @@ -273,9 +273,9 @@ def worker(self): break # subtract the duration of this export call to the next timeout - start = _time_ns() + start = time_ns() self._export(flush_request) - end = _time_ns() + end = time_ns() duration = (end - start) / 1e9 timeout = self.schedule_delay_millis / 1e3 - duration diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index b6012a0d7b..7c5288a274 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -14,23 +14,16 @@ import asyncio import unittest -from sys import version_info from unittest.mock import patch from opentelemetry import context +from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext from opentelemetry.sdk import trace from opentelemetry.sdk.trace import export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) -if version_info.minor < 7: - raise unittest.SkipTest("contextvars not available") - -from opentelemetry.context.contextvars_context import ( # pylint:disable=wrong-import-position - ContextVarsRuntimeContext, -) - _SPAN_NAMES = [ "test_span1", "test_span2", diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index bdc15eb963..b95c1d8806 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import time +from time import sleep, time_ns from typing import Sequence from unittest.mock import Mock @@ -35,7 +35,6 @@ LastValueAggregation, ) from opentelemetry.test.concurrency_test import ConcurrencyTestBase -from opentelemetry.util._time import _time_ns class FakeMetricsExporter(MetricExporter): @@ -56,7 +55,7 @@ def export( timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: - time.sleep(self.wait) + sleep(self.wait) self.metrics.extend(metrics) return True @@ -76,8 +75,8 @@ def force_flush(self, timeout_millis: float = 10_000) -> bool: data_points=[ NumberDataPoint( attributes={}, - start_time_unix_nano=_time_ns(), - time_unix_nano=_time_ns(), + start_time_unix_nano=time_ns(), + time_unix_nano=time_ns(), value=2, ) ], @@ -93,8 +92,8 @@ def force_flush(self, timeout_millis: float = 10_000) -> bool: data_points=[ NumberDataPoint( attributes={}, - start_time_unix_nano=_time_ns(), - time_unix_nano=_time_ns(), + start_time_unix_nano=time_ns(), + time_unix_nano=time_ns(), value=2, ) ] @@ -119,7 +118,7 @@ def _create_periodic_reader( ) def _collect(reader, timeout_millis): - time.sleep(collect_wait) + sleep(collect_wait) pmr._receive_metrics(metrics, timeout_millis) pmr._set_collect_callback(_collect) @@ -131,7 +130,7 @@ def test_ticker_called(self): exporter.export = Mock() pmr = PeriodicExportingMetricReader(exporter, export_interval_millis=1) pmr._set_collect_callback(collect_mock) - time.sleep(0.1) + sleep(0.1) self.assertTrue(collect_mock.assert_called_once) pmr.shutdown() @@ -142,7 +141,7 @@ def test_ticker_collects_metrics(self): pmr = self._create_periodic_reader( metrics_list, exporter, interval=100 ) - time.sleep(0.15) + sleep(0.15) self.assertEqual(exporter.metrics, metrics_list) pmr.shutdown() diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 13ccc20f10..8b8d33faa4 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -13,12 +13,14 @@ # limitations under the License. # pylint: disable=too-many-lines + import shutil import subprocess import unittest from importlib import reload from logging import ERROR, WARNING from random import randint +from time import time_ns from typing import Optional from unittest import mock @@ -46,7 +48,6 @@ new_tracer, ) from opentelemetry.trace import Status, StatusCode -from opentelemetry.util._time import _time_ns class TestTracer(unittest.TestCase): @@ -740,7 +741,7 @@ def test_events(self): ) # event name, attributes and timestamp - now = _time_ns() + now = time_ns() root.add_event("event2", {"name": ["birthday"]}, now) mutable_list = ["original_contents"] diff --git a/opentelemetry-semantic-conventions/setup.cfg b/opentelemetry-semantic-conventions/setup.cfg index 377bb6ffdf..383158d1c4 100644 --- a/opentelemetry-semantic-conventions/setup.cfg +++ b/opentelemetry-semantic-conventions/setup.cfg @@ -28,14 +28,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: @@ -46,4 +45,4 @@ include_package_data = True where = src [options.extras_require] -test = \ No newline at end of file +test = diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg index ee9dbc4d15..3756ab806f 100644 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ b/propagator/opentelemetry-propagator-b3/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -36,7 +35,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: @@ -53,4 +52,4 @@ where = src [options.entry_points] opentelemetry_propagator = b3 = opentelemetry.propagators.b3:B3SingleFormat - b3multi = opentelemetry.propagators.b3:B3MultiFormat \ No newline at end of file + b3multi = opentelemetry.propagators.b3:B3MultiFormat diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg index 0f6c8fe249..932f7e1647 100644 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ b/propagator/opentelemetry-propagator-jaeger/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -36,7 +35,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: @@ -51,4 +50,4 @@ where = src [options.entry_points] opentelemetry_propagator = - jaeger = opentelemetry.propagators.jaeger:JaegerPropagator \ No newline at end of file + jaeger = opentelemetry.propagators.jaeger:JaegerPropagator diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 5edba807d9..377b976183 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -28,7 +28,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -36,7 +35,7 @@ classifiers = Typing :: Typed [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/shim/opentelemetry-opentracing-shim/tests/test_util.py b/shim/opentelemetry-opentracing-shim/tests/test_util.py index e9a57a9d1f..c8f7571e77 100644 --- a/shim/opentelemetry-opentracing-shim/tests/test_util.py +++ b/shim/opentelemetry-opentracing-shim/tests/test_util.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from time import time +from time import time, time_ns from unittest import TestCase from opentelemetry.shim.opentracing_shim.util import ( @@ -21,7 +21,6 @@ time_seconds_from_ns, time_seconds_to_ns, ) -from opentelemetry.util._time import _time_ns class TestUtil(TestCase): @@ -50,7 +49,7 @@ def test_time_seconds_to_ns(self): self.assertEqual(result, int(time_seconds * 1e9)) def test_time_seconds_from_ns(self): - time_nanoseconds = _time_ns() + time_nanoseconds = time_ns() result = time_seconds_from_ns(time_nanoseconds) self.assertEqual(result, time_nanoseconds / 1e9) diff --git a/tests/opentelemetry-test-utils/setup.cfg b/tests/opentelemetry-test-utils/setup.cfg index 90769491b1..abb338c6f6 100644 --- a/tests/opentelemetry-test-utils/setup.cfg +++ b/tests/opentelemetry-test-utils/setup.cfg @@ -26,14 +26,13 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 [options] -python_requires = >=3.6 +python_requires = >=3.7 package_dir= =src packages=find_namespace: diff --git a/tox.ini b/tox.ini index d31e4adf38..893c0d2103 100644 --- a/tox.ini +++ b/tox.ini @@ -4,61 +4,61 @@ skip_missing_interpreters = True envlist = ; Environments are organized by individual package, allowing ; for specifying supported Python versions per package. - py3{6,7,8,9,10}-opentelemetry-api + py3{7,8,9,10}-opentelemetry-api pypy3-opentelemetry-api - py3{6,7,8,9,10}-opentelemetry-protobuf + py3{7,8,9,10}-opentelemetry-protobuf pypy3-opentelemetry-protobuf - py3{6,7,8,9,10}-opentelemetry-sdk + py3{7,8,9,10}-opentelemetry-sdk pypy3-opentelemetry-sdk - py3{6,7,8,9,10}-opentelemetry-semantic-conventions + py3{7,8,9,10}-opentelemetry-semantic-conventions pypy3-opentelemetry-semantic-conventions ; docs/getting-started - py3{6,7,8,9,10}-opentelemetry-getting-started + py3{7,8,9,10}-opentelemetry-getting-started pypy3-opentelemetry-getting-started - py3{6,7,8,9,10}-opentelemetry-opentracing-shim + py3{7,8,9,10}-opentelemetry-opentracing-shim pypy3-opentelemetry-opentracing-shim - py3{6,7,8,9,10}-opentelemetry-exporter-jaeger-combined + py3{7,8,9,10}-opentelemetry-exporter-jaeger-combined - py3{6,7,8,9,10}-opentelemetry-exporter-jaeger-proto-grpc + py3{7,8,9,10}-opentelemetry-exporter-jaeger-proto-grpc - py3{6,7,8,9,10}-opentelemetry-exporter-jaeger-thrift + py3{7,8,9,10}-opentelemetry-exporter-jaeger-thrift - py3{6,7,8,9,10}-opentelemetry-exporter-opencensus + py3{7,8,9,10}-opentelemetry-exporter-opencensus ; exporter-opencensus intentionally excluded from pypy3 ; opentelemetry-exporter-otlp - py3{6,7,8,9,10}-opentelemetry-exporter-otlp-combined + py3{7,8,9,10}-opentelemetry-exporter-otlp-combined ; intentionally excluded from pypy3 - py3{6,7,8,9,10}-opentelemetry-exporter-otlp-proto-grpc + py3{7,8,9,10}-opentelemetry-exporter-otlp-proto-grpc ; intentionally excluded from pypy3 - py3{6,7,8,9,10}-opentelemetry-exporter-otlp-proto-http + py3{7,8,9,10}-opentelemetry-exporter-otlp-proto-http pypy3-opentelemetry-exporter-otlp-proto-http py3{6,7,8,9,10}-opentelemetry-exporter-prometheus pypy3-opentelemetry-exporter-prometheus ; opentelemetry-exporter-zipkin - py3{6,7,8,9,10}-opentelemetry-exporter-zipkin-combined + py3{7,8,9,10}-opentelemetry-exporter-zipkin-combined pypy3-opentelemetry-exporter-zipkin-combined - py3{6,7,8,9,10}-opentelemetry-exporter-zipkin-proto-http + py3{7,8,9,10}-opentelemetry-exporter-zipkin-proto-http pypy3-opentelemetry-exporter-zipkin-proto-http - py3{6,7,8,9,10}-opentelemetry-exporter-zipkin-json + py3{7,8,9,10}-opentelemetry-exporter-zipkin-json pypy3-opentelemetry-exporter-zipkin-json - py3{6,7,8,9,10}-opentelemetry-propagator-b3 + py3{7,8,9,10}-opentelemetry-propagator-b3 pypy3-opentelemetry-propagator-b3 - py3{6,7,8,9,10}-opentelemetry-propagator-jaeger + py3{7,8,9,10}-opentelemetry-propagator-jaeger pypy3-opentelemetry-propagator-jaeger lint @@ -111,7 +111,7 @@ changedir = commands_pre = ; Install without -e to test the actual installation - py3{6,7,8,9,10}: python -m pip install -U pip setuptools wheel + py3{7,8,9,10}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. opentelemetry: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-sdk {toxinidir}/tests/opentelemetry-test-utils From a602192a69182bec1ed850ea8e25646b5355b92e Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Thu, 25 Aug 2022 14:30:46 -0400 Subject: [PATCH 1292/1517] Update package metadata (#2867) * Update package metadata * support Python 3.6 * address review * update Co-authored-by: Srikanth Chekuri Co-authored-by: Leighton Chen --- RELEASING.md | 33 ++++++++-------- opentelemetry-api/MANIFEST.in | 9 ----- opentelemetry-api/pyproject.toml | 66 ++++++++++++++++++++++++++++++++ opentelemetry-api/setup.cfg | 66 -------------------------------- opentelemetry-api/setup.py | 27 ------------- scripts/build.sh | 4 +- tox.ini | 5 ++- 7 files changed, 90 insertions(+), 120 deletions(-) delete mode 100644 opentelemetry-api/MANIFEST.in create mode 100644 opentelemetry-api/pyproject.toml delete mode 100644 opentelemetry-api/setup.cfg delete mode 100644 opentelemetry-api/setup.py diff --git a/RELEASING.md b/RELEASING.md index 4ce00da75d..5ba0fcb1b4 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,16 +2,17 @@ This document explains how to publish all OT modules at version x.y.z. Ensure that you’re following semver when choosing a version number. Release Process: -* [Checkout a clean repo](#checkout-a-clean-repo) -* [Update versions](#update-versions) -* [Create a new branch](#create-a-new-branch) -* [Open a Pull Request](#open-a-pull-request) -* [Create a Release](#Create-a-Release) -* [Move stable tag](#Move-stable-tag) -* [Update main](#Update-main) -* [Update docs](#Update-docs) -* [Check PyPI](#Check-PyPI) -* [Troubleshooting](#troubleshooting) +- [Checkout a clean repo](#checkout-a-clean-repo) +- [Update versions](#update-versions) +- [Create a new branch](#create-a-new-branch) +- [Open a Pull Request](#open-a-pull-request) +- [Create a Release](#create-a-release) +- [Check PyPI](#check-pypi) +- [Move stable tag](#move-stable-tag) +- [Update main](#update-main) +- [Hotfix procedure](#hotfix-procedure) +- [Troubleshooting](#troubleshooting) + - [Publish failed](#publish-failed) ## Checkout a clean repo To avoid pushing untracked changes, check out the repo in a new dir @@ -81,11 +82,13 @@ A `hotfix` is defined as a small change developed to correct a bug that should b 2. On your local machine, remove the `dev0` tags from the version number and increment the patch version number. 3. On your local machine, update `CHANGELOG.md` with the date of the hotfix change. 4. With administrator privileges for PyPi, manually publish the affected packages. - a. Install [twine](https://pypi.org/project/twine/) - b. Navigate to where the `setup.py` file exists for the package you want to publish. - c. Run `python setup.py sdist bdist_wheel`. You may have to install [wheel](https://pypi.org/project/wheel/) as well. - d. Validate your built distributions by running `twine check dist/*`. - e. Upload distributions to PyPi by running `twine upload dist/*`. + 1. Install [twine](https://pypi.org/project/twine/) and [build](https://pypi.org/project/build/) + 2. Navigate to where the `setup.py` or `pyproject.toml` file exists for the package you want to publish. + 3. To build the package: + - If a `setup.py` file exists, run `python setup.py sdist bdist_wheel`. You may have to install [wheel](https://pypi.org/project/wheel/) as well. + - Otherwise, run `python -m build`. + 4. Validate your built distributions by running `twine check dist/*`. + 5. Upload distributions to PyPi by running `twine upload dist/*`. 5. Note that since hotfixes are manually published, the build scripts for publish after creating a release are not run. ## Troubleshooting diff --git a/opentelemetry-api/MANIFEST.in b/opentelemetry-api/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/opentelemetry-api/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/opentelemetry-api/pyproject.toml b/opentelemetry-api/pyproject.toml new file mode 100644 index 0000000000..9de9b0d12d --- /dev/null +++ b/opentelemetry-api/pyproject.toml @@ -0,0 +1,66 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-api" +description = "OpenTelemetry Python API" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Typing :: Typed", +] +dependencies = [ + "Deprecated >= 1.2.6", + "setuptools >= 16.0", +] +dynamic = [ + "version", +] + +[project.optional-dependencies] +test = [] + +[project.entry-points.opentelemetry_context] +contextvars_context = "opentelemetry.context.contextvars_context:ContextVarsRuntimeContext" + +[project.entry-points.opentelemetry_environment_variables] +api = "opentelemetry.environment_variables" + +[project.entry-points.opentelemetry_meter_provider] +default_meter_provider = "opentelemetry.metrics:NoOpMeterProvider" + +[project.entry-points.opentelemetry_propagator] +baggage = "opentelemetry.baggage.propagation:W3CBaggagePropagator" +tracecontext = "opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator" + +[project.entry-points.opentelemetry_tracer_provider] +default_tracer_provider = "opentelemetry.trace:NoOpTracerProvider" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-api" + +[tool.hatch.version] +path = "src/opentelemetry/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg deleted file mode 100644 index 5b40671c42..0000000000 --- a/opentelemetry-api/setup.cfg +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-api -description = OpenTelemetry Python API -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-api -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -zip_safe = False -include_package_data = True -install_requires = - Deprecated >= 1.2.6 - aiocontextvars; python_version<'3.7' - setuptools >= 16.0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_context = - contextvars_context = opentelemetry.context.contextvars_context:ContextVarsRuntimeContext -opentelemetry_tracer_provider = - default_tracer_provider = opentelemetry.trace:NoOpTracerProvider -opentelemetry_meter_provider = - default_meter_provider = opentelemetry.metrics:NoOpMeterProvider -opentelemetry_propagator = - tracecontext = opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator - baggage = opentelemetry.baggage.propagation:W3CBaggagePropagator -opentelemetry_environment_variables = - api = opentelemetry.environment_variables - -[options.extras_require] -test = diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py deleted file mode 100644 index aac1579996..0000000000 --- a/opentelemetry-api/setup.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join(BASE_DIR, "src", "opentelemetry", "version.py") -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup( - version=PACKAGE_INFO["__version__"], -) diff --git a/scripts/build.sh b/scripts/build.sh index 605884bee6..2ba7e4c0ab 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -6,7 +6,7 @@ set -ev # Get the latest versions of packaging tools -python3 -m pip install --upgrade pip setuptools wheel +python3 -m pip install --upgrade pip build setuptools wheel BASEDIR=$(dirname $(readlink -f $(dirname $0))) DISTDIR=dist @@ -24,6 +24,8 @@ DISTDIR=dist # packaged. Verify the intent by looking for a setup.py. if [ -f setup.py ]; then python3 setup.py sdist --dist-dir "$BASEDIR/dist/" clean --all + else if [ -f pyproject.toml ]; then + HATCH_BUILD_CLEAN=1 python3 -m build --outdir "$BASEDIR/dist/" fi ) done diff --git a/tox.ini b/tox.ini index 893c0d2103..5a2dfb7dfe 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [tox] +isolated_build = True skipsdist = True skip_missing_interpreters = True envlist = @@ -42,7 +43,7 @@ envlist = py3{7,8,9,10}-opentelemetry-exporter-otlp-proto-http pypy3-opentelemetry-exporter-otlp-proto-http - py3{6,7,8,9,10}-opentelemetry-exporter-prometheus + py3{7,8,9,10}-opentelemetry-exporter-prometheus pypy3-opentelemetry-exporter-prometheus ; opentelemetry-exporter-zipkin @@ -287,7 +288,7 @@ commands_pre = -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc \ -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http \ -e {toxinidir}/exporter/opentelemetry-exporter-otlp - docker-compose up -d + docker-compose up -d commands = pytest {posargs} commands_post = From 6ccdada25f8b6c0e38955e955efb235aea9945ee Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 26 Aug 2022 13:41:46 +0200 Subject: [PATCH 1293/1517] Refactor testing console exporter (#2877) * Use devnull instead of stdout when testing console exporter Fixes #2876 * Fix lint * Refactor test case * Debug test in CI * Fix test * Move reset metrics to setup and teardown * Remove repeated code --- .../integration_test/test_console_exporter.py | 59 +++++++++++++++---- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py b/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py index d09ade6a29..59f026661c 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py @@ -12,26 +12,63 @@ # See the License for the specific language governing permissions and # limitations under the License. +from io import StringIO +from json import loads from unittest import TestCase -from opentelemetry import metrics +from opentelemetry.metrics import get_meter, set_meter_provider from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) +from opentelemetry.test.globals_test import reset_metrics_globals class TestConsoleExporter(TestCase): + def setUp(self): + reset_metrics_globals() + + def tearDown(self): + reset_metrics_globals() + def test_console_exporter(self): - try: - exporter = ConsoleMetricExporter() - reader = PeriodicExportingMetricReader(exporter) - provider = MeterProvider(metric_readers=[reader]) - metrics.set_meter_provider(provider) - meter = metrics.get_meter(__name__) - counter = meter.create_counter("test") - counter.add(1) - except Exception as error: - self.fail(f"Unexpected exception {error} raised") + output = StringIO() + exporter = ConsoleMetricExporter(out=output) + reader = PeriodicExportingMetricReader( + exporter, export_interval_millis=100 + ) + provider = MeterProvider(metric_readers=[reader]) + set_meter_provider(provider) + meter = get_meter(__name__) + counter = meter.create_counter( + "name", description="description", unit="unit" + ) + counter.add(1, attributes={"a": "b"}) + provider.shutdown() + + output.seek(0) + result_0 = loads(output.readlines()[0]) + + self.assertGreater(len(result_0), 0) + + metrics = result_0["resource_metrics"][0]["scope_metrics"][0] + + self.assertEqual(metrics["scope"]["name"], "test_console_exporter") + + metrics = metrics["metrics"][0] + + self.assertEqual(metrics["name"], "name") + self.assertEqual(metrics["description"], "description") + self.assertEqual(metrics["unit"], "unit") + + metrics = metrics["data"] + + self.assertEqual(metrics["aggregation_temporality"], 2) + self.assertTrue(metrics["is_monotonic"]) + + metrics = metrics["data_points"][0] + + self.assertEqual(metrics["attributes"], {"a": "b"}) + self.assertEqual(metrics["value"], 1) From 2ffc59246cfa159fc7d1c1d08e537f2315097ba6 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Mon, 29 Aug 2022 13:44:39 -0400 Subject: [PATCH 1294/1517] Update package metadata opentelemetry-proto (#2905) --- opentelemetry-proto/MANIFEST.in | 9 ------ opentelemetry-proto/pyproject.toml | 46 +++++++++++++++++++++++++++ opentelemetry-proto/setup.cfg | 50 ------------------------------ opentelemetry-proto/setup.py | 29 ----------------- 4 files changed, 46 insertions(+), 88 deletions(-) delete mode 100644 opentelemetry-proto/MANIFEST.in create mode 100644 opentelemetry-proto/pyproject.toml delete mode 100644 opentelemetry-proto/setup.cfg delete mode 100644 opentelemetry-proto/setup.py diff --git a/opentelemetry-proto/MANIFEST.in b/opentelemetry-proto/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/opentelemetry-proto/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/opentelemetry-proto/pyproject.toml b/opentelemetry-proto/pyproject.toml new file mode 100644 index 0000000000..d7c47210c2 --- /dev/null +++ b/opentelemetry-proto/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-proto" +dynamic = ["version"] +description = "OpenTelemetry Python Proto" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] +dependencies = [ + "protobuf~=3.13", +] + +[project.optional-dependencies] +test = [] + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-proto" + +[tool.hatch.version] +path = "src/opentelemetry/proto/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/opentelemetry-proto/setup.cfg b/opentelemetry-proto/setup.cfg deleted file mode 100644 index 5f67d7c3e8..0000000000 --- a/opentelemetry-proto/setup.cfg +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-proto -description = OpenTelemetry Python Proto -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-proto -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -zip_safe = False -include_package_data = True -install_requires = - protobuf~=3.13 - -[options.packages.find] -where = src - -[options.extras_require] -test = diff --git a/opentelemetry-proto/setup.py b/opentelemetry-proto/setup.py deleted file mode 100644 index 6f80c6c0d4..0000000000 --- a/opentelemetry-proto/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "proto", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup( - version=PACKAGE_INFO["__version__"], -) From ea9761721fae9858a7fe9a5d171ef2bf2383f8e0 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Mon, 29 Aug 2022 22:22:06 -0400 Subject: [PATCH 1295/1517] Update package metadata opentelemetry-sdk (#2906) --- opentelemetry-sdk/MANIFEST.in | 9 ---- opentelemetry-sdk/pyproject.toml | 74 ++++++++++++++++++++++++++++++++ opentelemetry-sdk/setup.cfg | 73 ------------------------------- opentelemetry-sdk/setup.py | 29 ------------- 4 files changed, 74 insertions(+), 111 deletions(-) delete mode 100644 opentelemetry-sdk/MANIFEST.in create mode 100644 opentelemetry-sdk/pyproject.toml delete mode 100644 opentelemetry-sdk/setup.cfg delete mode 100644 opentelemetry-sdk/setup.py diff --git a/opentelemetry-sdk/MANIFEST.in b/opentelemetry-sdk/MANIFEST.in deleted file mode 100644 index b3bfe49c7d..0000000000 --- a/opentelemetry-sdk/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include LICENSE -include MANIFEST.in -include README.rst diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml new file mode 100644 index 0000000000..d217ae7354 --- /dev/null +++ b/opentelemetry-sdk/pyproject.toml @@ -0,0 +1,74 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-sdk" +dynamic = ["version"] +description = "OpenTelemetry Python SDK" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Typing :: Typed", +] +dependencies = [ + "opentelemetry-api == 1.12.0", + "opentelemetry-semantic-conventions == 0.33b0", + "setuptools >= 16.0", + "typing-extensions >= 3.7.4", +] + +[project.optional-dependencies] +test = [] + +[project.entry-points.opentelemetry_environment_variables] +sdk = "opentelemetry.sdk.environment_variables" + +[project.entry-points.opentelemetry_id_generator] +random = "opentelemetry.sdk.trace.id_generator:RandomIdGenerator" + +[project.entry-points.opentelemetry_log_emitter_provider] +sdk_log_emitter_provider = "opentelemetry.sdk._logs:LogEmitterProvider" + +[project.entry-points.opentelemetry_logs_exporter] +console = "opentelemetry.sdk._logs.export:ConsoleLogExporter" + +[project.entry-points.opentelemetry_meter_provider] +sdk_meter_provider = "opentelemetry.sdk.metrics:MeterProvider" + +[project.entry-points.opentelemetry_metrics_exporter] +console = "opentelemetry.sdk.metrics.export:ConsoleMetricExporter" + +[project.entry-points.opentelemetry_tracer_provider] +sdk_tracer_provider = "opentelemetry.sdk.trace:TracerProvider" + +[project.entry-points.opentelemetry_traces_exporter] +console = "opentelemetry.sdk.trace.export:ConsoleSpanExporter" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk" + +[tool.hatch.version] +path = "src/opentelemetry/sdk/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg deleted file mode 100644 index 03c31ed0c7..0000000000 --- a/opentelemetry-sdk/setup.cfg +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-sdk -description = OpenTelemetry Python SDK -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -zip_safe = False -include_package_data = True -install_requires = - opentelemetry-api == 1.12.0 - opentelemetry-semantic-conventions == 0.33b0 - setuptools >= 16.0 - dataclasses == 0.8; python_version < '3.7' - typing-extensions >= 3.7.4 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_tracer_provider = - sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider -opentelemetry_traces_exporter = - console = opentelemetry.sdk.trace.export:ConsoleSpanExporter -opentelemetry_meter_provider = - sdk_meter_provider = opentelemetry.sdk.metrics:MeterProvider -opentelemetry_log_emitter_provider = - sdk_log_emitter_provider = opentelemetry.sdk._logs:LogEmitterProvider -opentelemetry_logs_exporter = - console = opentelemetry.sdk._logs.export:ConsoleLogExporter -opentelemetry_metrics_exporter = - console = opentelemetry.sdk.metrics.export:ConsoleMetricExporter -opentelemetry_id_generator = - random = opentelemetry.sdk.trace.id_generator:RandomIdGenerator -opentelemetry_environment_variables = - sdk = opentelemetry.sdk.environment_variables - -[options.extras_require] -test = diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py deleted file mode 100644 index 61fc07cbbf..0000000000 --- a/opentelemetry-sdk/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "sdk", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup( - version=PACKAGE_INFO["__version__"], -) From 6ed381ad941a6653d95a80b7c52bdc85983dd676 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 01:47:23 -0400 Subject: [PATCH 1296/1517] Update package metadata opentelemetry-semantic-conventions (#2907) --- .../MANIFEST.in | 9 ---- .../pyproject.toml | 43 +++++++++++++++++ opentelemetry-semantic-conventions/setup.cfg | 48 ------------------- opentelemetry-semantic-conventions/setup.py | 29 ----------- 4 files changed, 43 insertions(+), 86 deletions(-) delete mode 100644 opentelemetry-semantic-conventions/MANIFEST.in create mode 100644 opentelemetry-semantic-conventions/pyproject.toml delete mode 100644 opentelemetry-semantic-conventions/setup.cfg delete mode 100644 opentelemetry-semantic-conventions/setup.py diff --git a/opentelemetry-semantic-conventions/MANIFEST.in b/opentelemetry-semantic-conventions/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/opentelemetry-semantic-conventions/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/opentelemetry-semantic-conventions/pyproject.toml b/opentelemetry-semantic-conventions/pyproject.toml new file mode 100644 index 0000000000..f3da7a3650 --- /dev/null +++ b/opentelemetry-semantic-conventions/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-semantic-conventions" +dynamic = ["version"] +description = "OpenTelemetry Semantic Conventions" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] + +[project.optional-dependencies] +test = [] + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-semantic-conventions" + +[tool.hatch.version] +path = "src/opentelemetry/semconv/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/opentelemetry-semantic-conventions/setup.cfg b/opentelemetry-semantic-conventions/setup.cfg deleted file mode 100644 index 383158d1c4..0000000000 --- a/opentelemetry-semantic-conventions/setup.cfg +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-semantic-conventions -description = OpenTelemetry Semantic Conventions -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-semantic-conventions -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -zip_safe = False -include_package_data = True - -[options.packages.find] -where = src - -[options.extras_require] -test = diff --git a/opentelemetry-semantic-conventions/setup.py b/opentelemetry-semantic-conventions/setup.py deleted file mode 100644 index 3ec350247d..0000000000 --- a/opentelemetry-semantic-conventions/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "semconv", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup( - version=PACKAGE_INFO["__version__"], -) From a6889255c17ed03d2812abd86c0e9169676c7699 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 03:45:45 -0400 Subject: [PATCH 1297/1517] Update package metadata opentelemetry-propagator-b3 (#2908) --- .../opentelemetry-propagator-b3/MANIFEST.in | 9 --- .../pyproject.toml | 52 ++++++++++++++++++ .../opentelemetry-propagator-b3/setup.cfg | 55 ------------------- .../opentelemetry-propagator-b3/setup.py | 26 --------- 4 files changed, 52 insertions(+), 90 deletions(-) delete mode 100644 propagator/opentelemetry-propagator-b3/MANIFEST.in create mode 100644 propagator/opentelemetry-propagator-b3/pyproject.toml delete mode 100644 propagator/opentelemetry-propagator-b3/setup.cfg delete mode 100644 propagator/opentelemetry-propagator-b3/setup.py diff --git a/propagator/opentelemetry-propagator-b3/MANIFEST.in b/propagator/opentelemetry-propagator-b3/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/propagator/opentelemetry-propagator-b3/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/propagator/opentelemetry-propagator-b3/pyproject.toml b/propagator/opentelemetry-propagator-b3/pyproject.toml new file mode 100644 index 0000000000..a4231e0e62 --- /dev/null +++ b/propagator/opentelemetry-propagator-b3/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-propagator-b3" +dynamic = ["version"] +description = "OpenTelemetry B3 Propagator" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Typing :: Typed", +] +dependencies = [ + "deprecated >= 1.2.6", + "opentelemetry-api ~= 1.3", +] + +[project.optional-dependencies] +test = [] + +[project.entry-points.opentelemetry_propagator] +b3 = "opentelemetry.propagators.b3:B3SingleFormat" +b3multi = "opentelemetry.propagators.b3:B3MultiFormat" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/propagator/opentelemetry-propagator-b3" + +[tool.hatch.version] +path = "src/opentelemetry/propagators/b3/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/propagator/opentelemetry-propagator-b3/setup.cfg b/propagator/opentelemetry-propagator-b3/setup.cfg deleted file mode 100644 index 3756ab806f..0000000000 --- a/propagator/opentelemetry-propagator-b3/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-propagator-b3 -description = OpenTelemetry B3 Propagator -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/propagator/opentelemetry-propagator-b3 -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api ~= 1.3 - deprecated >= 1.2.6 - -[options.extras_require] -test = - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_propagator = - b3 = opentelemetry.propagators.b3:B3SingleFormat - b3multi = opentelemetry.propagators.b3:B3MultiFormat diff --git a/propagator/opentelemetry-propagator-b3/setup.py b/propagator/opentelemetry-propagator-b3/setup.py deleted file mode 100644 index ca5569293e..0000000000 --- a/propagator/opentelemetry-propagator-b3/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "propagators", "b3", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From 1674b59ebd4680e2b5258142e9d7f61afd6d01c1 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 08:43:48 -0400 Subject: [PATCH 1298/1517] Update package metadata opentelemetry-propagator-jaeger (#2909) --- .../MANIFEST.in | 9 ---- .../pyproject.toml | 50 +++++++++++++++++ .../opentelemetry-propagator-jaeger/setup.cfg | 53 ------------------- .../opentelemetry-propagator-jaeger/setup.py | 26 --------- 4 files changed, 50 insertions(+), 88 deletions(-) delete mode 100644 propagator/opentelemetry-propagator-jaeger/MANIFEST.in create mode 100644 propagator/opentelemetry-propagator-jaeger/pyproject.toml delete mode 100644 propagator/opentelemetry-propagator-jaeger/setup.cfg delete mode 100644 propagator/opentelemetry-propagator-jaeger/setup.py diff --git a/propagator/opentelemetry-propagator-jaeger/MANIFEST.in b/propagator/opentelemetry-propagator-jaeger/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/propagator/opentelemetry-propagator-jaeger/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/propagator/opentelemetry-propagator-jaeger/pyproject.toml b/propagator/opentelemetry-propagator-jaeger/pyproject.toml new file mode 100644 index 0000000000..d713984b93 --- /dev/null +++ b/propagator/opentelemetry-propagator-jaeger/pyproject.toml @@ -0,0 +1,50 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-propagator-jaeger" +dynamic = ["version"] +description = "OpenTelemetry Jaeger Propagator" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Typing :: Typed", +] +dependencies = [ + "opentelemetry-api ~= 1.3", +] + +[project.optional-dependencies] +test = [] + +[project.entry-points.opentelemetry_propagator] +jaeger = "opentelemetry.propagators.jaeger:JaegerPropagator" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/propagator/opentelemetry-propagator-jaeger" + +[tool.hatch.version] +path = "src/opentelemetry/propagators/jaeger/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/propagator/opentelemetry-propagator-jaeger/setup.cfg b/propagator/opentelemetry-propagator-jaeger/setup.cfg deleted file mode 100644 index 932f7e1647..0000000000 --- a/propagator/opentelemetry-propagator-jaeger/setup.cfg +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-propagator-jaeger -description = OpenTelemetry Jaeger Propagator -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/propagator/opentelemetry-propagator-jaeger -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api ~= 1.3 - -[options.extras_require] -test = - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_propagator = - jaeger = opentelemetry.propagators.jaeger:JaegerPropagator diff --git a/propagator/opentelemetry-propagator-jaeger/setup.py b/propagator/opentelemetry-propagator-jaeger/setup.py deleted file mode 100644 index dc0eaf6906..0000000000 --- a/propagator/opentelemetry-propagator-jaeger/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "propagators", "jaeger", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From c69d10cacee5988912036749cc88538ac2447864 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 14:04:48 -0400 Subject: [PATCH 1299/1517] Update package metadata (#2896) --- .../MANIFEST.in | 9 --- .../pyproject.toml | 52 ++++++++++++++++++ .../setup.cfg | 55 ------------------- .../setup.py | 32 ----------- 4 files changed, 52 insertions(+), 96 deletions(-) delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/setup.py diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/MANIFEST.in b/exporter/opentelemetry-exporter-jaeger-thrift/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/pyproject.toml b/exporter/opentelemetry-exporter-jaeger-thrift/pyproject.toml new file mode 100644 index 0000000000..b10a35a85e --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-thrift/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-jaeger-thrift" +dynamic = ["version"] +description = "Jaeger Thrift Exporter for OpenTelemetry" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Typing :: Typed", +] +dependencies = [ + "opentelemetry-api ~= 1.3", + "opentelemetry-sdk ~= 1.11", + "thrift >= 0.10.0", +] + +[project.optional-dependencies] +test = [] + +[project.entry-points.opentelemetry_traces_exporter] +jaeger_thrift = "opentelemetry.exporter.jaeger.thrift:JaegerExporter" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger-thrift" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/jaeger/thrift/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg b/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg deleted file mode 100644 index 23cde387a4..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-jaeger-thrift -description = Jaeger Thrift Exporter for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger-thrift -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - thrift >= 0.10.0 - opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.11 - -[options.packages.find] -where = src - -[options.extras_require] -test = - -[options.entry_points] -opentelemetry_traces_exporter = - jaeger_thrift = opentelemetry.exporter.jaeger.thrift:JaegerExporter diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/setup.py b/exporter/opentelemetry-exporter-jaeger-thrift/setup.py deleted file mode 100644 index 9a2312e312..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/setup.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "exporter", - "jaeger", - "thrift", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From 4cdcac684feb9480f4bfdaaee9962df69ecbbe02 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 15:35:10 -0400 Subject: [PATCH 1300/1517] Update package metadata (#2903) --- .../MANIFEST.in | 9 --- .../pyproject.toml | 52 ++++++++++++++++++ .../setup.cfg | 55 ------------------- .../setup.py | 32 ----------- 4 files changed, 52 insertions(+), 96 deletions(-) delete mode 100644 exporter/opentelemetry-exporter-zipkin-json/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-zipkin-json/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-zipkin-json/setup.cfg delete mode 100644 exporter/opentelemetry-exporter-zipkin-json/setup.py diff --git a/exporter/opentelemetry-exporter-zipkin-json/MANIFEST.in b/exporter/opentelemetry-exporter-zipkin-json/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/exporter/opentelemetry-exporter-zipkin-json/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/exporter/opentelemetry-exporter-zipkin-json/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-json/pyproject.toml new file mode 100644 index 0000000000..07c0a74d23 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-json/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-zipkin-json" +dynamic = ["version"] +description = "Zipkin Span JSON Exporter for OpenTelemetry" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Typing :: Typed", +] +dependencies = [ + "opentelemetry-api ~= 1.3", + "opentelemetry-sdk ~= 1.11", + "requests ~= 2.7", +] + +[project.optional-dependencies] +test = [] + +[project.entry-points.opentelemetry_traces_exporter] +zipkin_json = "opentelemetry.exporter.zipkin.json:ZipkinExporter" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-zipkin-json" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/zipkin/json/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg b/exporter/opentelemetry-exporter-zipkin-json/setup.cfg deleted file mode 100644 index a0e41cf8a3..0000000000 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-zipkin-json -description = Zipkin Span JSON Exporter for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-zipkin-json -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - requests ~= 2.7 - opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.11 - -[options.packages.find] -where = src - -[options.extras_require] -test = - -[options.entry_points] -opentelemetry_traces_exporter = - zipkin_json = opentelemetry.exporter.zipkin.json:ZipkinExporter diff --git a/exporter/opentelemetry-exporter-zipkin-json/setup.py b/exporter/opentelemetry-exporter-zipkin-json/setup.py deleted file mode 100644 index b4a98ab648..0000000000 --- a/exporter/opentelemetry-exporter-zipkin-json/setup.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "exporter", - "zipkin", - "json", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From f49f7645c57ddc4fb51305187f5fc7ecf2f5814b Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 16:04:14 -0400 Subject: [PATCH 1301/1517] Update package metadata (#2897) --- .../MANIFEST.in | 9 --- .../pyproject.toml | 55 ++++++++++++++++++ .../setup.cfg | 58 ------------------- .../setup.py | 26 --------- 4 files changed, 55 insertions(+), 93 deletions(-) delete mode 100644 exporter/opentelemetry-exporter-opencensus/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-opencensus/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-opencensus/setup.cfg delete mode 100644 exporter/opentelemetry-exporter-opencensus/setup.py diff --git a/exporter/opentelemetry-exporter-opencensus/MANIFEST.in b/exporter/opentelemetry-exporter-opencensus/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/exporter/opentelemetry-exporter-opencensus/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/exporter/opentelemetry-exporter-opencensus/pyproject.toml b/exporter/opentelemetry-exporter-opencensus/pyproject.toml new file mode 100644 index 0000000000..282b5f0c42 --- /dev/null +++ b/exporter/opentelemetry-exporter-opencensus/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-opencensus" +dynamic = ["version"] +description = "OpenCensus Exporter" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Typing :: Typed", +] +dependencies = [ + "grpcio >= 1.0.0, < 2.0.0", + "opencensus-proto >= 0.1.0, < 1.0.0", + "opentelemetry-api ~= 1.3", + "opentelemetry-sdk ~= 1.3", + "protobuf ~= 3.13", + "setuptools >= 16.0", +] + +[project.optional-dependencies] +test = [] + +[project.entry-points.opentelemetry_traces_exporter] +opencensus = "opentelemetry.exporter.opencensus.trace_exporter:OpenCensusSpanExporter" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-opencensus" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/opencensus/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg deleted file mode 100644 index cf613eca3c..0000000000 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-opencensus -description = OpenCensus Exporter -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-opencensus -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - grpcio >= 1.0.0, < 2.0.0 - opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.3 - protobuf ~= 3.13 - setuptools >= 16.0 - -[options.packages.find] -where = src - -[options.extras_require] -test = - -[options.entry_points] -opentelemetry_traces_exporter = - opencensus = opentelemetry.exporter.opencensus.trace_exporter:OpenCensusSpanExporter diff --git a/exporter/opentelemetry-exporter-opencensus/setup.py b/exporter/opentelemetry-exporter-opencensus/setup.py deleted file mode 100644 index ecf4915d53..0000000000 --- a/exporter/opentelemetry-exporter-opencensus/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "exporter", "opencensus", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From da51dbc596306f24369bd13fd74d6e5fec2b9d31 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 16:48:00 -0400 Subject: [PATCH 1302/1517] Update package metadata opentelemetry-exporter-zipkin-proto-http (#2904) --- .../MANIFEST.in | 9 --- .../pyproject.toml | 54 ++++++++++++++++++ .../setup.cfg | 57 ------------------- .../setup.py | 33 ----------- 4 files changed, 54 insertions(+), 99 deletions(-) delete mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg delete mode 100644 exporter/opentelemetry-exporter-zipkin-proto-http/setup.py diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/MANIFEST.in b/exporter/opentelemetry-exporter-zipkin-proto-http/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml new file mode 100644 index 0000000000..d3e6501e60 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml @@ -0,0 +1,54 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-zipkin-proto-http" +dynamic = ["version"] +description = "Zipkin Span Protobuf Exporter for OpenTelemetry" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Typing :: Typed", +] +dependencies = [ + "opentelemetry-api ~= 1.3", + "opentelemetry-exporter-zipkin-json == 1.12.0", + "opentelemetry-sdk ~= 1.11", + "protobuf ~= 3.12", + "requests ~= 2.7", +] + +[project.optional-dependencies] +test = [] + +[project.entry-points.opentelemetry_traces_exporter] +zipkin_proto = "opentelemetry.exporter.zipkin.proto.http:ZipkinExporter" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-zipkin-proto-http" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/zipkin/proto/http/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg deleted file mode 100644 index bdffdb7161..0000000000 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-zipkin-proto-http -description = Zipkin Span Protobuf Exporter for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-zipkin-proto-http -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - protobuf ~= 3.12 - requests ~= 2.7 - opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.11 - opentelemetry-exporter-zipkin-json == 1.12.0 - -[options.packages.find] -where = src - -[options.extras_require] -test = - -[options.entry_points] -opentelemetry_traces_exporter = - zipkin_proto = opentelemetry.exporter.zipkin.proto.http:ZipkinExporter diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.py b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.py deleted file mode 100644 index 62cf7e5ffd..0000000000 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "exporter", - "zipkin", - "proto", - "http", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From a12708566b8b7a8b6b1da7d94a08cd85bcd8b11b Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 16:56:48 -0400 Subject: [PATCH 1303/1517] Update package metadata opentelemetry-exporter-zipkin (#2902) --- .../opentelemetry-exporter-zipkin/MANIFEST.in | 9 ---- .../pyproject.toml | 51 ++++++++++++++++++ .../opentelemetry-exporter-zipkin/setup.cfg | 54 ------------------- .../opentelemetry-exporter-zipkin/setup.py | 26 --------- 4 files changed, 51 insertions(+), 89 deletions(-) delete mode 100644 exporter/opentelemetry-exporter-zipkin/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-zipkin/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-zipkin/setup.cfg delete mode 100644 exporter/opentelemetry-exporter-zipkin/setup.py diff --git a/exporter/opentelemetry-exporter-zipkin/MANIFEST.in b/exporter/opentelemetry-exporter-zipkin/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/exporter/opentelemetry-exporter-zipkin/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/exporter/opentelemetry-exporter-zipkin/pyproject.toml b/exporter/opentelemetry-exporter-zipkin/pyproject.toml new file mode 100644 index 0000000000..7da93bc37b --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/pyproject.toml @@ -0,0 +1,51 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-zipkin" +dynamic = ["version"] +description = "Zipkin Span Exporters for OpenTelemetry" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Typing :: Typed", +] +dependencies = [ + "opentelemetry-exporter-zipkin-json == 1.12.0", + "opentelemetry-exporter-zipkin-proto-http == 1.12.0", +] + +[project.optional-dependencies] +test = [] + +[project.entry-points.opentelemetry_traces_exporter] +zipkin = "opentelemetry.exporter.zipkin.proto.http:ZipkinExporter" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-zipkin" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/zipkin/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg deleted file mode 100644 index 09921337ad..0000000000 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-zipkin -description = Zipkin Span Exporters for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-zipkin -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-exporter-zipkin-json == 1.12.0 - opentelemetry-exporter-zipkin-proto-http == 1.12.0 - -[options.packages.find] -where = src - -[options.extras_require] -test = - -[options.entry_points] -opentelemetry_traces_exporter = - zipkin = opentelemetry.exporter.zipkin.proto.http:ZipkinExporter diff --git a/exporter/opentelemetry-exporter-zipkin/setup.py b/exporter/opentelemetry-exporter-zipkin/setup.py deleted file mode 100644 index c763810cf5..0000000000 --- a/exporter/opentelemetry-exporter-zipkin/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "exporter", "zipkin", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From 35882fdfb90f16dbe0b0e3970efa5cdfe49c51cb Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 17:07:24 -0400 Subject: [PATCH 1304/1517] Update package metadata opentelemetry-exporter-prometheus (#2901) --- .../MANIFEST.in | 9 ---- .../pyproject.toml | 51 ++++++++++++++++++ .../setup.cfg | 54 ------------------- .../setup.py | 26 --------- 4 files changed, 51 insertions(+), 89 deletions(-) delete mode 100644 exporter/opentelemetry-exporter-prometheus/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-prometheus/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-prometheus/setup.cfg delete mode 100644 exporter/opentelemetry-exporter-prometheus/setup.py diff --git a/exporter/opentelemetry-exporter-prometheus/MANIFEST.in b/exporter/opentelemetry-exporter-prometheus/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/exporter/opentelemetry-exporter-prometheus/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/exporter/opentelemetry-exporter-prometheus/pyproject.toml b/exporter/opentelemetry-exporter-prometheus/pyproject.toml new file mode 100644 index 0000000000..d7f9cef33d --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/pyproject.toml @@ -0,0 +1,51 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-prometheus" +dynamic = ["version"] +description = "Prometheus Metric Exporter for OpenTelemetry" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] +dependencies = [ + "opentelemetry-api >= 1.10.0", + "opentelemetry-sdk >= 1.10.0", + "prometheus_client >= 0.5.0, < 1.0.0", +] + +[project.optional-dependencies] +test = [] + +[project.entry-points.opentelemetry_metric_reader] +prometheus = "opentelemetry.exporter.prometheus:PrometheusMetricReader" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-prometheus" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/prometheus/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-prometheus/setup.cfg b/exporter/opentelemetry-exporter-prometheus/setup.cfg deleted file mode 100644 index 44df363114..0000000000 --- a/exporter/opentelemetry-exporter-prometheus/setup.cfg +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-prometheus -description = Prometheus Metric Exporter for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-prometheus -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api >= 1.10.0 - opentelemetry-sdk >= 1.10.0 - -[options.packages.find] -where = src - -[options.extras_require] -test = - -[options.entry_points] -opentelemetry_metric_reader = - prometheus = opentelemetry.exporter.prometheus:PrometheusMetricReader diff --git a/exporter/opentelemetry-exporter-prometheus/setup.py b/exporter/opentelemetry-exporter-prometheus/setup.py deleted file mode 100644 index 9fdbd7de49..0000000000 --- a/exporter/opentelemetry-exporter-prometheus/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -_BASE_DIR = os.path.dirname(__file__) -_VERSION_FILENAME = os.path.join( - _BASE_DIR, "src", "opentelemetry", "exporter", "prometheus", "version.py" -) -_PACKAGE_INFO = {} -with open(_VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), _PACKAGE_INFO) - -setuptools.setup(version=_PACKAGE_INFO["__version__"]) From 14c07804c45c3b4aaf2712a669316a89ef69b263 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 17:15:56 -0400 Subject: [PATCH 1305/1517] Update package metadata opentelemetry-exporter-otlp-proto-http (#2900) --- .../MANIFEST.in | 9 --- .../pyproject.toml | 55 ++++++++++++++++++ .../setup.cfg | 58 ------------------- .../setup.py | 33 ----------- 4 files changed, 55 insertions(+), 100 deletions(-) delete mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg delete mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/setup.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/MANIFEST.in b/exporter/opentelemetry-exporter-otlp-proto-http/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-http/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml new file mode 100644 index 0000000000..edf4db8a93 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-otlp-proto-http" +dynamic = ["version"] +description = "OpenTelemetry Collector Protobuf over HTTP Exporter" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] +dependencies = [ + "backoff >= 1.10.0, < 2.0.0; python_version<'3.7'", + "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", + "googleapis-common-protos ~= 1.52", + "opentelemetry-api ~= 1.3", + "opentelemetry-proto == 1.12.0", + "opentelemetry-sdk ~= 1.11", + "requests ~= 2.7", +] + +[project.optional-dependencies] +test = [] + +[project.entry-points.opentelemetry_traces_exporter] +otlp_proto_http = "opentelemetry.exporter.otlp.proto.http.trace_exporter:OTLPSpanExporter" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-http" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/otlp/proto/http/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg deleted file mode 100644 index 18f029981e..0000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-otlp-proto-http -description = OpenTelemetry Collector Protobuf over HTTP Exporter -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-http -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - requests ~= 2.7 - googleapis-common-protos ~= 1.52 - opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.11 - opentelemetry-proto == 1.12.0 - backoff >= 1.10.0, < 2.0.0; python_version<'3.7' - backoff >= 1.10.0, < 3.0.0; python_version>='3.7' - -[options.extras_require] -test = - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_traces_exporter = - otlp_proto_http = opentelemetry.exporter.otlp.proto.http.trace_exporter:OTLPSpanExporter diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.py b/exporter/opentelemetry-exporter-otlp-proto-http/setup.py deleted file mode 100644 index 5d3210b183..0000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "exporter", - "otlp", - "proto", - "http", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From e494a750cf2e937483aa9b26a7f6c0574e948549 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 17:25:25 -0400 Subject: [PATCH 1306/1517] Update package metadata opentelemetry-exporter-otlp-proto-grpc (#2899) --- .../MANIFEST.in | 9 --- .../pyproject.toml | 63 +++++++++++++++++++ .../setup.cfg | 63 ------------------- .../setup.py | 33 ---------- 4 files changed, 63 insertions(+), 105 deletions(-) delete mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg delete mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/setup.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/MANIFEST.in b/exporter/opentelemetry-exporter-otlp-proto-grpc/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml new file mode 100644 index 0000000000..2e05816e8d --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -0,0 +1,63 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-otlp-proto-grpc" +dynamic = ["version"] +description = "OpenTelemetry Collector Protobuf over gRPC Exporter" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] +dependencies = [ + "backoff >= 1.10.0, < 2.0.0; python_version<'3.7'", + "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", + "googleapis-common-protos ~= 1.52", + "grpcio >= 1.0.0, < 2.0.0", + "opentelemetry-api ~= 1.3", + "opentelemetry-proto == 1.12.0", + "opentelemetry-sdk ~= 1.11", +] + +[project.optional-dependencies] +test = [ + "pytest-grpc", +] + +[project.entry-points.opentelemetry_logs_exporter] +otlp_proto_grpc = "opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter" + +[project.entry-points.opentelemetry_metrics_exporter] +otlp_proto_grpc = "opentelemetry.exporter.otlp.proto.grpc.metric_exporter:OTLPMetricExporter" + +[project.entry-points.opentelemetry_traces_exporter] +otlp_proto_grpc = "opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-grpc" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/otlp/proto/grpc/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg deleted file mode 100644 index 9c28be2d1f..0000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-otlp-proto-grpc -description = OpenTelemetry Collector Protobuf over gRPC Exporter -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-grpc -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - grpcio >= 1.0.0, < 2.0.0 - googleapis-common-protos ~= 1.52 - opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.11 - opentelemetry-proto == 1.12.0 - backoff >= 1.10.0, < 2.0.0; python_version<'3.7' - backoff >= 1.10.0, < 3.0.0; python_version>='3.7' - -[options.extras_require] -test = - pytest-grpc - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_traces_exporter = - otlp_proto_grpc = opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter -opentelemetry_metrics_exporter = - otlp_proto_grpc = opentelemetry.exporter.otlp.proto.grpc.metric_exporter:OTLPMetricExporter -opentelemetry_logs_exporter = - otlp_proto_grpc = opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.py deleted file mode 100644 index 6b30ebfbfe..0000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "exporter", - "otlp", - "proto", - "grpc", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From e3ec9296ca747040ee265fc0a3a77d8eb092a778 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 19:04:53 -0400 Subject: [PATCH 1307/1517] Update package metadata (#2895) --- .../MANIFEST.in | 9 --- .../pyproject.toml | 53 ++++++++++++++++++ .../setup.cfg | 56 ------------------- .../setup.py | 33 ----------- 4 files changed, 53 insertions(+), 98 deletions(-) delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.py diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/MANIFEST.in b/exporter/opentelemetry-exporter-jaeger-proto-grpc/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml new file mode 100644 index 0000000000..a3a4f76f59 --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml @@ -0,0 +1,53 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-jaeger-proto-grpc" +dynamic = ["version"] +description = "Jaeger Protobuf Exporter for OpenTelemetry" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Typing :: Typed", +] +dependencies = [ + "googleapis-common-protos ~= 1.52, < 1.56.3", + "grpcio >= 1.0.0, < 2.0.0", + "opentelemetry-api ~= 1.3", + "opentelemetry-sdk ~= 1.11", +] + +[project.optional-dependencies] +test = [] + +[project.entry-points.opentelemetry_traces_exporter] +jaeger_proto = "opentelemetry.exporter.jaeger.proto.grpc:JaegerExporter" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger-proto-grpc" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/jaeger/proto/grpc/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg deleted file mode 100644 index be0e897ef3..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-jaeger-proto-grpc -description = Jaeger Protobuf Exporter for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger-proto-grpc -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - grpcio >= 1.0.0, < 2.0.0 - googleapis-common-protos ~= 1.52, < 1.56.3 - opentelemetry-api ~= 1.3 - opentelemetry-sdk ~= 1.11 - -[options.packages.find] -where = src - -[options.extras_require] -test = - -[options.entry_points] -opentelemetry_traces_exporter = - jaeger_proto = opentelemetry.exporter.jaeger.proto.grpc:JaegerExporter diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.py deleted file mode 100644 index 93d478eb97..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "exporter", - "jaeger", - "proto", - "grpc", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From 03cc61b38cf33c52168cc6d9d3c759acefe40466 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 19:19:03 -0400 Subject: [PATCH 1308/1517] Update package metadata opentelemetry-exporter-otlp (#2898) --- .../opentelemetry-exporter-otlp/MANIFEST.in | 9 --- .../pyproject.toml | 54 ++++++++++++++++++ .../opentelemetry-exporter-otlp/setup.cfg | 55 ------------------- exporter/opentelemetry-exporter-otlp/setup.py | 26 --------- 4 files changed, 54 insertions(+), 90 deletions(-) delete mode 100644 exporter/opentelemetry-exporter-otlp/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-otlp/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-otlp/setup.cfg delete mode 100644 exporter/opentelemetry-exporter-otlp/setup.py diff --git a/exporter/opentelemetry-exporter-otlp/MANIFEST.in b/exporter/opentelemetry-exporter-otlp/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/exporter/opentelemetry-exporter-otlp/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml new file mode 100644 index 0000000000..0f1b603fd4 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -0,0 +1,54 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-otlp" +dynamic = ["version"] +description = "OpenTelemetry Collector Exporters" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Typing :: Typed", +] +dependencies = [ + "opentelemetry-exporter-otlp-proto-grpc == 1.12.0", + "opentelemetry-exporter-otlp-proto-http == 1.12.0", +] + +[project.entry-points.opentelemetry_logs_exporter] +otlp = "opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter" + +[project.entry-points.opentelemetry_metrics_exporter] +otlp = "opentelemetry.exporter.otlp.proto.grpc.metric_exporter:OTLPMetricExporter" + +[project.entry-points.opentelemetry_traces_exporter] +otlp = "opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/otlp/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg deleted file mode 100644 index 91aa081840..0000000000 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-otlp -description = OpenTelemetry Collector Exporters -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.12.0 - opentelemetry-exporter-otlp-proto-http == 1.12.0 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_traces_exporter = - otlp = opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter -opentelemetry_metrics_exporter = - otlp = opentelemetry.exporter.otlp.proto.grpc.metric_exporter:OTLPMetricExporter -opentelemetry_logs_exporter = - otlp = opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter diff --git a/exporter/opentelemetry-exporter-otlp/setup.py b/exporter/opentelemetry-exporter-otlp/setup.py deleted file mode 100644 index ec0269160e..0000000000 --- a/exporter/opentelemetry-exporter-otlp/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "exporter", "otlp", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From 0db9d19a0128b9ec84867bc016e5ec915b4fb6ac Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 30 Aug 2022 23:29:50 -0400 Subject: [PATCH 1309/1517] Update package metadata opentelemetry-exporter-jaeger (#2894) --- .../opentelemetry-exporter-jaeger/MANIFEST.in | 9 ---- .../pyproject.toml | 51 ++++++++++++++++++ .../opentelemetry-exporter-jaeger/setup.cfg | 54 ------------------- .../opentelemetry-exporter-jaeger/setup.py | 26 --------- 4 files changed, 51 insertions(+), 89 deletions(-) delete mode 100644 exporter/opentelemetry-exporter-jaeger/MANIFEST.in create mode 100644 exporter/opentelemetry-exporter-jaeger/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-jaeger/setup.cfg delete mode 100644 exporter/opentelemetry-exporter-jaeger/setup.py diff --git a/exporter/opentelemetry-exporter-jaeger/MANIFEST.in b/exporter/opentelemetry-exporter-jaeger/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/exporter/opentelemetry-exporter-jaeger/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/exporter/opentelemetry-exporter-jaeger/pyproject.toml b/exporter/opentelemetry-exporter-jaeger/pyproject.toml new file mode 100644 index 0000000000..eb32a59c1f --- /dev/null +++ b/exporter/opentelemetry-exporter-jaeger/pyproject.toml @@ -0,0 +1,51 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-jaeger" +dynamic = ["version"] +description = "Jaeger Exporters for OpenTelemetry" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Typing :: Typed", +] +dependencies = [ + "opentelemetry-exporter-jaeger-proto-grpc == 1.12.0", + "opentelemetry-exporter-jaeger-thrift == 1.12.0", +] + +[project.optional-dependencies] +test = [] + +[project.entry-points.opentelemetry_traces_exporter] +jaeger = "opentelemetry.exporter.jaeger.proto.grpc:JaegerExporter" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/jaeger/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg deleted file mode 100644 index 890f99561e..0000000000 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-exporter-jaeger -description = Jaeger Exporters for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.12.0 - opentelemetry-exporter-jaeger-thrift == 1.12.0 - -[options.packages.find] -where = src - -[options.extras_require] -test = - -[options.entry_points] -opentelemetry_traces_exporter = - jaeger = opentelemetry.exporter.jaeger.proto.grpc:JaegerExporter diff --git a/exporter/opentelemetry-exporter-jaeger/setup.py b/exporter/opentelemetry-exporter-jaeger/setup.py deleted file mode 100644 index 2303890631..0000000000 --- a/exporter/opentelemetry-exporter-jaeger/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "exporter", "jaeger", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From 2b3260607c4690ec503769199c47b46e12581473 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Thu, 1 Sep 2022 03:53:51 -0400 Subject: [PATCH 1310/1517] Update package metadata opentracing-shim (#2912) --- .../MANIFEST.in | 9 ---- .../pyproject.toml | 52 ++++++++++++++++++ shim/opentelemetry-opentracing-shim/setup.cfg | 53 ------------------- shim/opentelemetry-opentracing-shim/setup.py | 31 ----------- 4 files changed, 52 insertions(+), 93 deletions(-) delete mode 100644 shim/opentelemetry-opentracing-shim/MANIFEST.in create mode 100644 shim/opentelemetry-opentracing-shim/pyproject.toml delete mode 100644 shim/opentelemetry-opentracing-shim/setup.cfg delete mode 100644 shim/opentelemetry-opentracing-shim/setup.py diff --git a/shim/opentelemetry-opentracing-shim/MANIFEST.in b/shim/opentelemetry-opentracing-shim/MANIFEST.in deleted file mode 100644 index aed3e33273..0000000000 --- a/shim/opentelemetry-opentracing-shim/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE diff --git a/shim/opentelemetry-opentracing-shim/pyproject.toml b/shim/opentelemetry-opentracing-shim/pyproject.toml new file mode 100644 index 0000000000..2d60f490e5 --- /dev/null +++ b/shim/opentelemetry-opentracing-shim/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-opentracing-shim" +dynamic = ["version"] +description = "OpenTracing Shim for OpenTelemetry" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Typing :: Typed", +] +dependencies = [ + "Deprecated >= 1.2.6", + "opentelemetry-api ~= 1.3", + "opentracing ~= 2.0", +] + +[project.optional-dependencies] +test = [ + "opentelemetry-test-utils == 0.33b0", + "opentracing ~= 2.2.0", +] + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/shim/opentelemetry-opentracing-shim" + +[tool.hatch.version] +path = "src/opentelemetry/shim/opentracing_shim/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg deleted file mode 100644 index 377b976183..0000000000 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = opentelemetry-opentracing-shim -description = OpenTracing Shim for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/main/shim/opentelemetry-opentracing-shim -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Typing :: Typed - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - Deprecated >= 1.2.6 - opentracing ~= 2.0 - opentelemetry-api ~= 1.3 - -[options.extras_require] -test = - opentelemetry-test-utils == 0.33b0 - opentracing ~= 2.2.0 - -[options.packages.find] -where = src diff --git a/shim/opentelemetry-opentracing-shim/setup.py b/shim/opentelemetry-opentracing-shim/setup.py deleted file mode 100644 index 35cdc05f4c..0000000000 --- a/shim/opentelemetry-opentracing-shim/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "shim", - "opentracing_shim", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From e569196344c4643a8c68f76105515ae5008f7bc6 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Thu, 1 Sep 2022 04:02:29 -0400 Subject: [PATCH 1311/1517] Update package metadata docs/examples/error_handler (#2914) --- .../error_handler_0/pyproject.toml | 42 +++++++++++++++++ .../error_handler/error_handler_0/setup.cfg | 46 ------------------- .../error_handler/error_handler_0/setup.py | 26 ----------- .../error_handler_1/pyproject.toml | 42 +++++++++++++++++ .../error_handler/error_handler_1/setup.cfg | 46 ------------------- .../error_handler/error_handler_1/setup.py | 26 ----------- 6 files changed, 84 insertions(+), 144 deletions(-) create mode 100644 docs/examples/error_handler/error_handler_0/pyproject.toml delete mode 100644 docs/examples/error_handler/error_handler_0/setup.cfg delete mode 100644 docs/examples/error_handler/error_handler_0/setup.py create mode 100644 docs/examples/error_handler/error_handler_1/pyproject.toml delete mode 100644 docs/examples/error_handler/error_handler_1/setup.cfg delete mode 100644 docs/examples/error_handler/error_handler_1/setup.py diff --git a/docs/examples/error_handler/error_handler_0/pyproject.toml b/docs/examples/error_handler/error_handler_0/pyproject.toml new file mode 100644 index 0000000000..8874896851 --- /dev/null +++ b/docs/examples/error_handler/error_handler_0/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "error-handler-0" +dynamic = ["version"] +description = "This is just an error handler example package" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] +dependencies = [ + "opentelemetry-sdk ~= 1.3", +] + +[project.entry-points.opentelemetry_error_handler] +error_handler_0 = "error_handler_0:ErrorHandler0" + +[tool.hatch.version] +path = "src/error_handler_0/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/docs/examples/error_handler/error_handler_0/setup.cfg b/docs/examples/error_handler/error_handler_0/setup.cfg deleted file mode 100644 index 6f14742b62..0000000000 --- a/docs/examples/error_handler/error_handler_0/setup.cfg +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = error-handler-0 -description = This is just an error handler example package -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-sdk ~= 1.3 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_error_handler = - error_handler_0 = error_handler_0:ErrorHandler0 diff --git a/docs/examples/error_handler/error_handler_0/setup.py b/docs/examples/error_handler/error_handler_0/setup.py deleted file mode 100644 index 327b1ce600..0000000000 --- a/docs/examples/error_handler/error_handler_0/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "error_handler_0", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/docs/examples/error_handler/error_handler_1/pyproject.toml b/docs/examples/error_handler/error_handler_1/pyproject.toml new file mode 100644 index 0000000000..d2a6caca3e --- /dev/null +++ b/docs/examples/error_handler/error_handler_1/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "error-handler-1" +dynamic = ["version"] +description = "This is just an error handler example package" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] +dependencies = [ + "opentelemetry-sdk ~= 1.3", +] + +[project.entry-points.opentelemetry_error_handler] +error_handler_1 = "error_handler_1:ErrorHandler1" + +[tool.hatch.version] +path = "src/error_handler_1/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/docs/examples/error_handler/error_handler_1/setup.cfg b/docs/examples/error_handler/error_handler_1/setup.cfg deleted file mode 100644 index 7b31bc84ce..0000000000 --- a/docs/examples/error_handler/error_handler_1/setup.cfg +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[metadata] -name = error_handler_1 -description = This is just an error handler example package -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-sdk ~= 1.3 - -[options.packages.find] -where = src - -[options.entry_points] -opentelemetry_error_handler = - error_handler_1 = error_handler_1:ErrorHandler1 diff --git a/docs/examples/error_handler/error_handler_1/setup.py b/docs/examples/error_handler/error_handler_1/setup.py deleted file mode 100644 index 705d2a2be4..0000000000 --- a/docs/examples/error_handler/error_handler_1/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "error_handler_1", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From e1a4c38edace6550697e588eb2b5a3b6d48b3719 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Thu, 1 Sep 2022 12:01:01 -0400 Subject: [PATCH 1312/1517] Update package metadata opentelemetry-test-utils (#2913) --- tests/opentelemetry-test-utils/pyproject.toml | 47 ++++++++++++++++++ tests/opentelemetry-test-utils/setup.cfg | 48 ------------------- tests/opentelemetry-test-utils/setup.py | 26 ---------- 3 files changed, 47 insertions(+), 74 deletions(-) create mode 100644 tests/opentelemetry-test-utils/pyproject.toml delete mode 100644 tests/opentelemetry-test-utils/setup.cfg delete mode 100644 tests/opentelemetry-test-utils/setup.py diff --git a/tests/opentelemetry-test-utils/pyproject.toml b/tests/opentelemetry-test-utils/pyproject.toml new file mode 100644 index 0000000000..5e930fded3 --- /dev/null +++ b/tests/opentelemetry-test-utils/pyproject.toml @@ -0,0 +1,47 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-test-utils" +dynamic = ["version"] +description = "Test utilities for OpenTelemetry unit tests" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] +dependencies = [ + "asgiref ~= 3.0", + "opentelemetry-api == 1.12.0", + "opentelemetry-sdk == 1.12.0", +] + +[project.optional-dependencies] +test = [] + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tests/opentelemetry-test-utils" + +[tool.hatch.version] +path = "src/opentelemetry/test/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/tests/opentelemetry-test-utils/setup.cfg b/tests/opentelemetry-test-utils/setup.cfg deleted file mode 100644 index abb338c6f6..0000000000 --- a/tests/opentelemetry-test-utils/setup.cfg +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -[metadata] -name = opentelemetry-test-utils -description = Test utilities for OpenTelemetry unit tests -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tests/opentelemetry-test-utils -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - -[options] -python_requires = >=3.7 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api == 1.12.0 - opentelemetry-sdk == 1.12.0 - asgiref ~= 3.0 - -[options.extras_require] -test = - -[options.packages.find] -where = src diff --git a/tests/opentelemetry-test-utils/setup.py b/tests/opentelemetry-test-utils/setup.py deleted file mode 100644 index fa0cf101e7..0000000000 --- a/tests/opentelemetry-test-utils/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "test", "version.py" -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup(version=PACKAGE_INFO["__version__"]) From 71c114879cdacee1caabd9d0dfa811ab979f9efb Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 6 Sep 2022 11:32:33 +0530 Subject: [PATCH 1313/1517] Update README.md (#2916) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d41ba8a9c0..03fcdf73b0 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t - [Diego Hurtado](https://github.com/ocelotl), Lightstep - [Leighton Chen](https://github.com/lzchen), Microsoft -- [Srikanth Chekuri](https://github.com/srikanthccv) +- [Srikanth Chekuri](https://github.com/srikanthccv), signoz.io Emeritus Maintainers: From 2d591e4f29d0f14fd9a66e4a74cc123a340a57a3 Mon Sep 17 00:00:00 2001 From: Ron Yishai Date: Tue, 6 Sep 2022 20:15:04 +0300 Subject: [PATCH 1314/1517] Adding support for setting OTLP exporter protocol by env vars (#2893) --- CHANGELOG.md | 3 + .../sdk/_configuration/__init__.py | 92 ++++++++++++++++--- .../sdk/environment_variables.py | 28 ++++-- opentelemetry-sdk/tests/test_configurator.py | 71 ++++++++++++-- 4 files changed, 169 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4af2ef82e..307d9ae91a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2870](https://github.com/open-telemetry/opentelemetry-python/pull/2870)) - Fix: Remove `LogEmitter.flush()` to align with OTel Log spec ([#2863](https://github.com/open-telemetry/opentelemetry-python/pull/2863)) +- Add support for setting OTLP export protocol with env vars, as defined in the + [specifications](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specify-protocol) + ([#2893](https://github.com/open-telemetry/opentelemetry-python/pull/2893)) ## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0) - 2022-08-08 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 25a6580f1b..159d471900 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -24,6 +24,7 @@ from typing import Dict, Optional, Sequence, Tuple, Type from pkg_resources import iter_entry_points +from typing_extensions import Literal from opentelemetry.environment_variables import ( OTEL_LOGS_EXPORTER, @@ -40,6 +41,10 @@ from opentelemetry.sdk._logs.export import BatchLogProcessor, LogExporter from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, + OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, + OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, + OTEL_EXPORTER_OTLP_PROTOCOL, + OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( @@ -55,26 +60,91 @@ _EXPORTER_OTLP = "otlp" _EXPORTER_OTLP_PROTO_GRPC = "otlp_proto_grpc" +_EXPORTER_OTLP_PROTO_HTTP = "otlp_proto_http" + +_EXPORTER_BY_OTLP_PROTOCOL = { + "grpc": _EXPORTER_OTLP_PROTO_GRPC, + "http/protobuf": _EXPORTER_OTLP_PROTO_HTTP, +} + +_EXPORTER_ENV_BY_SIGNAL_TYPE = { + "traces": OTEL_TRACES_EXPORTER, + "metrics": OTEL_METRICS_EXPORTER, + "logs": OTEL_LOGS_EXPORTER, +} + +_PROTOCOL_ENV_BY_SIGNAL_TYPE = { + "traces": OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, + "metrics": OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, + "logs": OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, +} _RANDOM_ID_GENERATOR = "random" _DEFAULT_ID_GENERATOR = _RANDOM_ID_GENERATOR +_logger = logging.getLogger(__name__) + def _get_id_generator() -> str: return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR) -def _get_exporter_names(names: str) -> Sequence[str]: - exporters = set() +def _get_exporter_entry_point( + exporter_name: str, signal_type: Literal["traces", "metrics", "logs"] +): + if exporter_name not in ( + _EXPORTER_OTLP, + _EXPORTER_OTLP_PROTO_GRPC, + _EXPORTER_OTLP_PROTO_HTTP, + ): + return exporter_name + + # Checking env vars for OTLP protocol (grpc/http). + otlp_protocol = environ.get( + _PROTOCOL_ENV_BY_SIGNAL_TYPE[signal_type] + ) or environ.get(OTEL_EXPORTER_OTLP_PROTOCOL) + + if not otlp_protocol: + if exporter_name == _EXPORTER_OTLP: + return _EXPORTER_OTLP_PROTO_GRPC + return exporter_name + + otlp_protocol = otlp_protocol.strip() + + if exporter_name == _EXPORTER_OTLP: + if otlp_protocol not in _EXPORTER_BY_OTLP_PROTOCOL: + # Invalid value was set by the env var + raise RuntimeError( + f"Unsupported OTLP protocol '{otlp_protocol}' is configured" + ) + + return _EXPORTER_BY_OTLP_PROTOCOL[otlp_protocol] + + # grpc/http already specified by exporter_name, only add a warning in case + # of a conflict. + exporter_name_by_env = _EXPORTER_BY_OTLP_PROTOCOL.get(otlp_protocol) + if exporter_name_by_env and exporter_name != exporter_name_by_env: + _logger.warning( + "Conflicting values for %s OTLP exporter protocol, using '%s'", + signal_type, + exporter_name, + ) + + return exporter_name + - if names and names.lower().strip() != "none": - exporters.update({_exporter.strip() for _exporter in names.split(",")}) +def _get_exporter_names( + signal_type: Literal["traces", "metrics", "logs"] +) -> Sequence[str]: + names = environ.get(_EXPORTER_ENV_BY_SIGNAL_TYPE.get(signal_type, "")) - if _EXPORTER_OTLP in exporters: - exporters.remove(_EXPORTER_OTLP) - exporters.add(_EXPORTER_OTLP_PROTO_GRPC) + if not names or names.lower().strip() == "none": + return [] - return list(exporters) + return [ + _get_exporter_entry_point(_exporter.strip(), signal_type) + for _exporter in names.split(",") + ] def _init_tracing( @@ -232,9 +302,9 @@ def _import_id_generator(id_generator_name: str) -> IdGenerator: def _initialize_components(auto_instrumentation_version): trace_exporters, metric_exporters, log_exporters = _import_exporters( - _get_exporter_names(environ.get(OTEL_TRACES_EXPORTER)), - _get_exporter_names(environ.get(OTEL_METRICS_EXPORTER)), - _get_exporter_names(environ.get(OTEL_LOGS_EXPORTER)), + _get_exporter_names("traces"), + _get_exporter_names("metrics"), + _get_exporter_names("logs"), ) id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 384d563d25..ad635e89d5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -234,6 +234,27 @@ OTLP exporter. """ +OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" +""" +.. envvar:: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL + +The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for spans. +""" + +OTEL_EXPORTER_OTLP_METRICS_PROTOCOL = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL" +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_PROTOCOL + +The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for metrics. +""" + +OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL" +""" +.. envvar:: OTEL_EXPORTER_OTLP_LOGS_PROTOCOL + +The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for logs. +""" + OTEL_EXPORTER_OTLP_CERTIFICATE = "OTEL_EXPORTER_OTLP_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_CERTIFICATE @@ -314,13 +335,6 @@ A scheme of https indicates a secure connection and takes precedence over this configuration setting. """ -OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" -""" -.. envvar:: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL - -The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for spans. -""" - OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE = "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 6afbf4d295..4aae8aa53b 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -25,6 +25,7 @@ from opentelemetry.sdk._configuration import ( _EXPORTER_OTLP, _EXPORTER_OTLP_PROTO_GRPC, + _EXPORTER_OTLP_PROTO_HTTP, _get_exporter_names, _get_id_generator, _import_exporters, @@ -413,25 +414,81 @@ def test_metrics_init_exporter(self): class TestExporterNames(TestCase): - def test_otlp_exporter_overwrite(self): - for exporter in [_EXPORTER_OTLP, _EXPORTER_OTLP_PROTO_GRPC]: + @patch.dict( + environ, + { + "OTEL_TRACES_EXPORTER": _EXPORTER_OTLP, + "OTEL_METRICS_EXPORTER": _EXPORTER_OTLP_PROTO_GRPC, + "OTEL_LOGS_EXPORTER": _EXPORTER_OTLP_PROTO_HTTP, + }, + ) + def test_otlp_exporter(self): + self.assertEqual( + _get_exporter_names("traces"), [_EXPORTER_OTLP_PROTO_GRPC] + ) + self.assertEqual( + _get_exporter_names("metrics"), [_EXPORTER_OTLP_PROTO_GRPC] + ) + self.assertEqual( + _get_exporter_names("logs"), [_EXPORTER_OTLP_PROTO_HTTP] + ) + + @patch.dict( + environ, + { + "OTEL_TRACES_EXPORTER": _EXPORTER_OTLP, + "OTEL_METRICS_EXPORTER": _EXPORTER_OTLP, + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", + "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": "grpc", + }, + ) + def test_otlp_custom_exporter(self): + self.assertEqual( + _get_exporter_names("traces"), [_EXPORTER_OTLP_PROTO_HTTP] + ) + self.assertEqual( + _get_exporter_names("metrics"), [_EXPORTER_OTLP_PROTO_GRPC] + ) + + @patch.dict( + environ, + { + "OTEL_TRACES_EXPORTER": _EXPORTER_OTLP_PROTO_HTTP, + "OTEL_METRICS_EXPORTER": _EXPORTER_OTLP_PROTO_GRPC, + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", + "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": "http/protobuf", + }, + ) + def test_otlp_exporter_conflict(self): + # Verify that OTEL_*_EXPORTER is used, and a warning is logged + with self.assertLogs(level="WARNING") as logs_context: + self.assertEqual( + _get_exporter_names("traces"), [_EXPORTER_OTLP_PROTO_HTTP] + ) + assert len(logs_context.output) == 1 + + with self.assertLogs(level="WARNING") as logs_context: self.assertEqual( - _get_exporter_names(exporter), [_EXPORTER_OTLP_PROTO_GRPC] + _get_exporter_names("metrics"), [_EXPORTER_OTLP_PROTO_GRPC] ) + assert len(logs_context.output) == 1 + @patch.dict(environ, {"OTEL_TRACES_EXPORTER": "jaeger,zipkin"}) def test_multiple_exporters(self): self.assertEqual( - sorted(_get_exporter_names("jaeger,zipkin")), ["jaeger", "zipkin"] + sorted(_get_exporter_names("traces")), ["jaeger", "zipkin"] ) + @patch.dict(environ, {"OTEL_TRACES_EXPORTER": "none"}) def test_none_exporters(self): - self.assertEqual(sorted(_get_exporter_names("none")), []) + self.assertEqual(sorted(_get_exporter_names("traces")), []) def test_no_exporters(self): - self.assertEqual(sorted(_get_exporter_names(None)), []) + self.assertEqual(sorted(_get_exporter_names("traces")), []) + @patch.dict(environ, {"OTEL_TRACES_EXPORTER": ""}) def test_empty_exporters(self): - self.assertEqual(sorted(_get_exporter_names("")), []) + self.assertEqual(sorted(_get_exporter_names("traces")), []) class TestImportExporters(TestCase): From 05fd6f3399b1a214c46e71367e124be5d504ad26 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 6 Sep 2022 12:30:12 -0700 Subject: [PATCH 1315/1517] Fix preferred_aggregation + preferred_temporality example (#2911) --- CHANGELOG.md | 3 ++ .../metrics/reader/preferred_aggregation.py | 12 +++--- .../metrics/reader/preferred_temporality.py | 40 ++++++++++++------- .../sdk/metrics/_internal/export/__init__.py | 9 ++++- 4 files changed, 43 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 307d9ae91a..f583aba78e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2870](https://github.com/open-telemetry/opentelemetry-python/pull/2870)) - Fix: Remove `LogEmitter.flush()` to align with OTel Log spec ([#2863](https://github.com/open-telemetry/opentelemetry-python/pull/2863)) +- Fix metric reader examples + added `preferred_temporality` and `preferred_aggregation` + for `ConsoleMetricExporter` + ([#2911](https://github.com/open-telemetry/opentelemetry-python/pull/2911)) - Add support for setting OTLP export protocol with env vars, as defined in the [specifications](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specify-protocol) ([#2893](https://github.com/open-telemetry/opentelemetry-python/pull/2893)) diff --git a/docs/examples/metrics/reader/preferred_aggregation.py b/docs/examples/metrics/reader/preferred_aggregation.py index 9ed69c8179..a332840d3f 100644 --- a/docs/examples/metrics/reader/preferred_aggregation.py +++ b/docs/examples/metrics/reader/preferred_aggregation.py @@ -22,15 +22,17 @@ ) from opentelemetry.sdk.metrics.view import LastValueAggregation -# Use console exporter for the example -exporter = ConsoleMetricExporter() - aggregation_last_value = {Counter: LastValueAggregation()} -# Create a metric reader with custom preferred aggregation +# Use console exporter for the example +exporter = ConsoleMetricExporter( + preferred_aggregation=aggregation_last_value, +) + +# The PeriodicExportingMetricReader takes the preferred aggregation +# from the passed in exporter reader = PeriodicExportingMetricReader( exporter, - preferred_aggregation=aggregation_last_value, export_interval_millis=5_000, ) diff --git a/docs/examples/metrics/reader/preferred_temporality.py b/docs/examples/metrics/reader/preferred_temporality.py index d82fd71598..910c3fc953 100644 --- a/docs/examples/metrics/reader/preferred_temporality.py +++ b/docs/examples/metrics/reader/preferred_temporality.py @@ -22,25 +22,35 @@ PeriodicExportingMetricReader, ) -# Use console exporter for the example -exporter = ConsoleMetricExporter() - temporality_cumulative = {Counter: AggregationTemporality.CUMULATIVE} temporality_delta = {Counter: AggregationTemporality.DELTA} -# Create a metric reader with cumulative preferred temporality -# The metrics that are exported using this reader will represent a cumulative value + +# Use console exporters for the example + +# The metrics that are exported using this exporter will represent a cumulative value +exporter = ConsoleMetricExporter( + preferred_temporality=temporality_cumulative, +) + +# The metrics that are exported using this exporter will represent a delta value +exporter2 = ConsoleMetricExporter( + preferred_temporality=temporality_delta, +) + +# The PeriodicExportingMetricReader takes the preferred aggregation +# from the passed in exporter reader = PeriodicExportingMetricReader( exporter, - preferred_temporality=temporality_cumulative, export_interval_millis=5_000, ) -# Create a metric reader with delta preferred temporality -# The metrics that are exported using this reader will represent a delta value + +# The PeriodicExportingMetricReader takes the preferred aggregation +# from the passed in exporter reader2 = PeriodicExportingMetricReader( - exporter, - preferred_temporality=temporality_delta, + exporter2, export_interval_millis=5_000, ) + provider = MeterProvider(metric_readers=[reader, reader2]) set_meter_provider(provider) @@ -49,10 +59,10 @@ counter = meter.create_counter("my-counter") # Two metrics are expected to be printed to the console per export interval. -# The metric originating from the metric reader with a preferred temporality +# The metric originating from the metric exporter with a preferred temporality # of cumulative will keep a running sum of all values added. -# The metric originating from the metric reader with a preferred temporality +# The metric originating from the metric exporter with a preferred temporality # of delta will have the sum value reset each export interval. -for x in range(10): - counter.add(x) - time.sleep(2.0) +counter.add(5) +time.sleep(10) +counter.add(20) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index c5f4d3885e..4c63c4288b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -136,8 +136,15 @@ def __init__( ["opentelemetry.sdk.metrics.export.MetricsData"], str ] = lambda metrics_data: metrics_data.to_json() + linesep, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[ + type, "opentelemetry.sdk.metrics.view.Aggregation" + ] = None, ): - super().__init__() + super().__init__( + preferred_temporality=preferred_temporality, + preferred_aggregation=preferred_aggregation, + ) self.out = out self.formatter = formatter From 438ca5b4696617e2092a6dc67beef7c2d0d9aa8d Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 8 Sep 2022 22:29:38 +0530 Subject: [PATCH 1316/1517] Bump API/SDK version for exporters (#2918) --- CHANGELOG.md | 2 ++ .../opentelemetry-exporter-otlp-proto-grpc/pyproject.toml | 4 ++-- .../opentelemetry-exporter-otlp-proto-http/pyproject.toml | 4 ++-- exporter/opentelemetry-exporter-prometheus/pyproject.toml | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f583aba78e..d2e3ff6e2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2870](https://github.com/open-telemetry/opentelemetry-python/pull/2870)) - Fix: Remove `LogEmitter.flush()` to align with OTel Log spec ([#2863](https://github.com/open-telemetry/opentelemetry-python/pull/2863)) +- Bump minimum required API/SDK version for exporters that support metrics + ([#2918](https://github.com/open-telemetry/opentelemetry-python/pull/2918)) - Fix metric reader examples + added `preferred_temporality` and `preferred_aggregation` for `ConsoleMetricExporter` ([#2911](https://github.com/open-telemetry/opentelemetry-python/pull/2911)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index 2e05816e8d..f8172f6f9b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -28,9 +28,9 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "grpcio >= 1.0.0, < 2.0.0", - "opentelemetry-api ~= 1.3", + "opentelemetry-api ~= 1.12", "opentelemetry-proto == 1.12.0", - "opentelemetry-sdk ~= 1.11", + "opentelemetry-sdk ~= 1.12", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index edf4db8a93..e24085625b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -27,9 +27,9 @@ dependencies = [ "backoff >= 1.10.0, < 2.0.0; python_version<'3.7'", "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", - "opentelemetry-api ~= 1.3", + "opentelemetry-api ~= 1.12", "opentelemetry-proto == 1.12.0", - "opentelemetry-sdk ~= 1.11", + "opentelemetry-sdk ~= 1.12", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-prometheus/pyproject.toml b/exporter/opentelemetry-exporter-prometheus/pyproject.toml index d7f9cef33d..9dd2d63517 100644 --- a/exporter/opentelemetry-exporter-prometheus/pyproject.toml +++ b/exporter/opentelemetry-exporter-prometheus/pyproject.toml @@ -24,8 +24,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", ] dependencies = [ - "opentelemetry-api >= 1.10.0", - "opentelemetry-sdk >= 1.10.0", + "opentelemetry-api ~= 1.12", + "opentelemetry-sdk ~= 1.12", "prometheus_client >= 0.5.0, < 1.0.0", ] From 41b9e26d8324ae0496c85326b35e92bf617932d9 Mon Sep 17 00:00:00 2001 From: Olivier VERMEULEN Date: Fri, 9 Sep 2022 11:19:13 +0200 Subject: [PATCH 1317/1517] Add a configurable max_export_batch_size to the gRPC metrics exporter (#2809) --- CHANGELOG.md | 2 + .../proto/grpc/metric_exporter/__init__.py | 118 ++++++- .../metrics/test_otlp_metrics_exporter.py | 314 +++++++++++++++++- 3 files changed, 429 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2e3ff6e2f..31f0155507 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Add a configurable max_export_batch_size to the gRPC metrics exporter + ([#2809](https://github.com/open-telemetry/opentelemetry-python/pull/2809)) - Remove support for 3.6 ([#2763](https://github.com/open-telemetry/opentelemetry-python/pull/2763)) - Update PeriodicExportingMetricReader to never call export() concurrently diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index 04645021f9..a377ed9b2e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -11,9 +11,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import dataclasses from logging import getLogger from os import environ -from typing import Dict, Optional, Sequence +from typing import Dict, Iterable, List, Optional, Sequence from grpc import ChannelCredentials, Compression from opentelemetry.sdk.metrics._internal.aggregation import Aggregation from opentelemetry.exporter.otlp.proto.grpc.exporter import ( @@ -42,12 +43,15 @@ ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, + DataPointT, Gauge, Histogram as HistogramType, Metric, MetricExporter, MetricExportResult, MetricsData, + ResourceMetrics, + ScopeMetrics, Sum, ) @@ -58,6 +62,14 @@ class OTLPMetricExporter( MetricExporter, OTLPExporterMixin[Metric, ExportMetricsServiceRequest, MetricExportResult], ): + """OTLP metric exporter + + Args: + max_export_batch_size: Maximum number of data points to export in a single request. This is to deal with + gRPC's 4MB message size limit. If not set there is no limit to the number of data points in a request. + If it is set and the number of data points exceeds the max, the request will be split. + """ + _result = MetricExportResult _stub = MetricsServiceStub @@ -71,6 +83,7 @@ def __init__( compression: Optional[Compression] = None, preferred_temporality: Dict[type, AggregationTemporality] = None, preferred_aggregation: Dict[type, Aggregation] = None, + max_export_batch_size: Optional[int] = None, ): if insecure is None: @@ -122,6 +135,8 @@ def __init__( compression=compression, ) + self._max_export_batch_size: Optional[int] = max_export_batch_size + def _translate_data( self, data: MetricsData ) -> ExportMetricsServiceRequest: @@ -223,8 +238,9 @@ def _translate_data( ) pb2_metric.sum.data_points.append(pt) else: - _logger.warn( - "unsupported datapoint type %s", metric.point + _logger.warning( + "unsupported data type %s", + metric.data.__class__.__name__, ) continue @@ -245,7 +261,101 @@ def export( **kwargs, ) -> MetricExportResult: # TODO(#2663): OTLPExporterMixin should pass timeout to gRPC - return self._export(metrics_data) + if self._max_export_batch_size is None: + return self._export(data=metrics_data) + + export_result = MetricExportResult.SUCCESS + + for split_metrics_data in self._split_metrics_data(metrics_data): + split_export_result = self._export(data=split_metrics_data) + + if split_export_result is MetricExportResult.FAILURE: + export_result = MetricExportResult.FAILURE + + return export_result + + def _split_metrics_data( + self, + metrics_data: MetricsData, + ) -> Iterable[MetricsData]: + batch_size: int = 0 + split_resource_metrics: List[ResourceMetrics] = [] + + for resource_metrics in metrics_data.resource_metrics: + split_scope_metrics: List[ScopeMetrics] = [] + split_resource_metrics.append( + dataclasses.replace( + resource_metrics, + scope_metrics=split_scope_metrics, + ) + ) + for scope_metrics in resource_metrics.scope_metrics: + split_metrics: List[Metric] = [] + split_scope_metrics.append( + dataclasses.replace( + scope_metrics, + metrics=split_metrics, + ) + ) + for metric in scope_metrics.metrics: + split_data_points: List[DataPointT] = [] + split_metrics.append( + dataclasses.replace( + metric, + data=dataclasses.replace( + metric.data, + data_points=split_data_points, + ), + ) + ) + + for data_point in metric.data.data_points: + split_data_points.append(data_point) + batch_size += 1 + + if batch_size >= self._max_export_batch_size: + yield MetricsData( + resource_metrics=split_resource_metrics + ) + # Reset all the variables + batch_size = 0 + split_data_points = [] + split_metrics = [ + dataclasses.replace( + metric, + data=dataclasses.replace( + metric.data, + data_points=split_data_points, + ), + ) + ] + split_scope_metrics = [ + dataclasses.replace( + scope_metrics, + metrics=split_metrics, + ) + ] + split_resource_metrics = [ + dataclasses.replace( + resource_metrics, + scope_metrics=split_scope_metrics, + ) + ] + + if not split_data_points: + # If data_points is empty remove the whole metric + split_metrics.pop() + + if not split_metrics: + # If metrics is empty remove the whole scope_metrics + split_scope_metrics.pop() + + if not split_scope_metrics: + # If scope_metrics is empty remove the whole resource_metrics + split_resource_metrics.pop() + + if batch_size > 0: + yield MetricsData(resource_metrics=split_resource_metrics) def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index 66151d0993..262e26ed63 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines from concurrent.futures import ThreadPoolExecutor +from typing import List from unittest import TestCase from unittest.mock import patch @@ -52,13 +54,14 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk.metrics.export import AggregationTemporality +from opentelemetry.sdk.metrics.export import AggregationTemporality, Gauge from opentelemetry.sdk.metrics.export import Histogram as HistogramType from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, Metric, MetricExportResult, MetricsData, + NumberDataPoint, ResourceMetrics, ScopeMetrics, ) @@ -969,3 +972,312 @@ def test_translate_multiple_scope_histogram(self): # pylint: disable=protected-access actual = self.exporter._translate_data(self.multiple_scope_histogram) self.assertEqual(expected, actual) + + def test_split_metrics_data_many_data_points(self): + # GIVEN + metrics_data = MetricsData( + resource_metrics=[ + _resource_metrics( + index=1, + scope_metrics=[ + _scope_metrics( + index=1, + metrics=[ + _gauge( + index=1, + data_points=[ + _number_data_point(11), + _number_data_point(12), + _number_data_point(13), + ], + ), + ], + ), + ], + ), + ] + ) + # WHEN + split_metrics_data: List[MetricsData] = list( + # pylint: disable=protected-access + OTLPMetricExporter(max_export_batch_size=2)._split_metrics_data( + metrics_data=metrics_data, + ) + ) + # THEN + self.assertEqual( + [ + MetricsData( + resource_metrics=[ + _resource_metrics( + index=1, + scope_metrics=[ + _scope_metrics( + index=1, + metrics=[ + _gauge( + index=1, + data_points=[ + _number_data_point(11), + _number_data_point(12), + ], + ), + ], + ), + ], + ), + ] + ), + MetricsData( + resource_metrics=[ + _resource_metrics( + index=1, + scope_metrics=[ + _scope_metrics( + index=1, + metrics=[ + _gauge( + index=1, + data_points=[ + _number_data_point(13), + ], + ), + ], + ), + ], + ), + ] + ), + ], + split_metrics_data, + ) + + def test_split_metrics_data_nb_data_points_equal_batch_size(self): + # GIVEN + metrics_data = MetricsData( + resource_metrics=[ + _resource_metrics( + index=1, + scope_metrics=[ + _scope_metrics( + index=1, + metrics=[ + _gauge( + index=1, + data_points=[ + _number_data_point(11), + _number_data_point(12), + _number_data_point(13), + ], + ), + ], + ), + ], + ), + ] + ) + # WHEN + split_metrics_data: List[MetricsData] = list( + # pylint: disable=protected-access + OTLPMetricExporter(max_export_batch_size=3)._split_metrics_data( + metrics_data=metrics_data, + ) + ) + # THEN + self.assertEqual( + [ + MetricsData( + resource_metrics=[ + _resource_metrics( + index=1, + scope_metrics=[ + _scope_metrics( + index=1, + metrics=[ + _gauge( + index=1, + data_points=[ + _number_data_point(11), + _number_data_point(12), + _number_data_point(13), + ], + ), + ], + ), + ], + ), + ] + ), + ], + split_metrics_data, + ) + + def test_split_metrics_data_many_resources_scopes_metrics(self): + # GIVEN + metrics_data = MetricsData( + resource_metrics=[ + _resource_metrics( + index=1, + scope_metrics=[ + _scope_metrics( + index=1, + metrics=[ + _gauge( + index=1, + data_points=[ + _number_data_point(11), + ], + ), + _gauge( + index=2, + data_points=[ + _number_data_point(12), + ], + ), + ], + ), + _scope_metrics( + index=2, + metrics=[ + _gauge( + index=3, + data_points=[ + _number_data_point(13), + ], + ), + ], + ), + ], + ), + _resource_metrics( + index=2, + scope_metrics=[ + _scope_metrics( + index=3, + metrics=[ + _gauge( + index=4, + data_points=[ + _number_data_point(14), + ], + ), + ], + ), + ], + ), + ] + ) + # WHEN + split_metrics_data: List[MetricsData] = list( + # pylint: disable=protected-access + OTLPMetricExporter(max_export_batch_size=2)._split_metrics_data( + metrics_data=metrics_data, + ) + ) + # THEN + self.assertEqual( + [ + MetricsData( + resource_metrics=[ + _resource_metrics( + index=1, + scope_metrics=[ + _scope_metrics( + index=1, + metrics=[ + _gauge( + index=1, + data_points=[ + _number_data_point(11), + ], + ), + _gauge( + index=2, + data_points=[ + _number_data_point(12), + ], + ), + ], + ), + ], + ), + ] + ), + MetricsData( + resource_metrics=[ + _resource_metrics( + index=1, + scope_metrics=[ + _scope_metrics( + index=2, + metrics=[ + _gauge( + index=3, + data_points=[ + _number_data_point(13), + ], + ), + ], + ), + ], + ), + _resource_metrics( + index=2, + scope_metrics=[ + _scope_metrics( + index=3, + metrics=[ + _gauge( + index=4, + data_points=[ + _number_data_point(14), + ], + ), + ], + ), + ], + ), + ] + ), + ], + split_metrics_data, + ) + + +def _resource_metrics( + index: int, scope_metrics: List[ScopeMetrics] +) -> ResourceMetrics: + return ResourceMetrics( + resource=Resource( + attributes={"a": index}, + schema_url=f"resource_url_{index}", + ), + schema_url=f"resource_url_{index}", + scope_metrics=scope_metrics, + ) + + +def _scope_metrics(index: int, metrics: List[Metric]) -> ScopeMetrics: + return ScopeMetrics( + scope=InstrumentationScope(name=f"scope_{index}"), + schema_url=f"scope_url_{index}", + metrics=metrics, + ) + + +def _gauge(index: int, data_points: List[NumberDataPoint]) -> Metric: + return Metric( + name=f"gauge_{index}", + description="description", + unit="unit", + data=Gauge(data_points=data_points), + ) + + +def _number_data_point(value: int) -> NumberDataPoint: + return NumberDataPoint( + attributes={"a": 1, "b": True}, + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + value=value, + ) From 75313b6c6d58945c1401622b6683ccdd28657984 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 13 Sep 2022 22:28:28 +0530 Subject: [PATCH 1318/1517] Update scripts and lint configs (#2929) --- .coveragerc | 1 - .flake8 | 1 + RELEASING.md | 6 ++---- eachdist.ini | 11 ----------- .../src/opentelemetry/propagate/__init__.py | 2 +- pyproject.toml | 4 +++- scripts/build.sh | 8 +++----- scripts/coverage.sh | 2 -- scripts/eachdist.py | 16 ++++++++-------- 9 files changed, 18 insertions(+), 33 deletions(-) diff --git a/.coveragerc b/.coveragerc index 6f2257aba5..cc035b2382 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,4 @@ [run] omit = */tests/* - */setup.py */gen/* diff --git a/.flake8 b/.flake8 index 9c1f4b2d41..cd5fe860b5 100644 --- a/.flake8 +++ b/.flake8 @@ -24,3 +24,4 @@ exclude = docs/examples/opentelemetry-example-app/build/* opentelemetry-proto/build/* opentelemetry-proto/src/opentelemetry/proto/ + */build/lib/* diff --git a/RELEASING.md b/RELEASING.md index 5ba0fcb1b4..975fdf9440 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -83,10 +83,8 @@ A `hotfix` is defined as a small change developed to correct a bug that should b 3. On your local machine, update `CHANGELOG.md` with the date of the hotfix change. 4. With administrator privileges for PyPi, manually publish the affected packages. 1. Install [twine](https://pypi.org/project/twine/) and [build](https://pypi.org/project/build/) - 2. Navigate to where the `setup.py` or `pyproject.toml` file exists for the package you want to publish. - 3. To build the package: - - If a `setup.py` file exists, run `python setup.py sdist bdist_wheel`. You may have to install [wheel](https://pypi.org/project/wheel/) as well. - - Otherwise, run `python -m build`. + 2. Navigate to where the `pyproject.toml` file exists for the package you want to publish. + 3. To build the package: run `python -m build`. 4. Validate your built distributions by running `twine check dist/*`. 5. Upload distributions to PyPi by running `twine upload dist/*`. 5. Note that since hotfixes are manually published, the build scripts for publish after creating a release are not run. diff --git a/eachdist.ini b/eachdist.ini index 44b1d15eb8..5d42f3c298 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -41,17 +41,6 @@ packages= opentelemetry-test-utils tests -[experimental] -version=1.10a0 - -packages= - opentelemetry-exporter-prometheus-remote-write - opentelemetry-api - opentelemetry-sdk - opentelemetry-exporter-otlp-proto-grpc - opentelemetry-exporter-otlp-proto-http - opentelemetry-exporter-otlp - [lintroots] extraroots=examples/*,scripts/ subglob=*.py,tests/,test/,src/*,examples/* diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index f197f1f914..a39d8a44d1 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -27,7 +27,7 @@ ``opentelemetry.trace.propagation.tracecontext.TraceContextTextMapPropagator`` and other of type ``opentelemetry.baggage.propagation.W3CBaggagePropagator``. Notice that these propagator classes are defined as -``opentelemetry_propagator`` entry points in the ``setup.cfg`` file of +``opentelemetry_propagator`` entry points in the ``pyproject.toml`` file of ``opentelemetry``. Example:: diff --git a/pyproject.toml b/pyproject.toml index 15e2fed2e5..a20a405675 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,10 +5,12 @@ exclude = ''' /( # generated files .tox| venv| + .*/build/lib/.*| exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen| exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen| exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen| - opentelemetry-proto/src/opentelemetry/proto/.*/.* + opentelemetry-proto/src/opentelemetry/proto/.*/.*| + scripts )/ ) ''' diff --git a/scripts/build.sh b/scripts/build.sh index 2ba7e4c0ab..ed87583860 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -21,11 +21,9 @@ DISTDIR=dist echo "building $d" cd "$d" # Some ext directories (such as docker tests) are not intended to be - # packaged. Verify the intent by looking for a setup.py. - if [ -f setup.py ]; then - python3 setup.py sdist --dist-dir "$BASEDIR/dist/" clean --all - else if [ -f pyproject.toml ]; then - HATCH_BUILD_CLEAN=1 python3 -m build --outdir "$BASEDIR/dist/" + # packaged. Verify the intent by looking for a pyproject.toml. + if [ -f pyproject.toml ]; then + python3 -m build --outdir "$BASEDIR/dist/" fi ) done diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 72b7adec74..df43010ec6 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -6,7 +6,6 @@ function cov { if [ ${TOX_ENV_NAME:0:4} == "py34" ] then pytest \ - --ignore-glob=*/setup.py \ --ignore-glob=instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/* \ --cov ${1} \ --cov-append \ @@ -15,7 +14,6 @@ function cov { ${1} else pytest \ - --ignore-glob=*/setup.py \ --cov ${1} \ --cov-append \ --cov-branch \ diff --git a/scripts/eachdist.py b/scripts/eachdist.py index cd2a3938f4..dc803af4cf 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -81,7 +81,7 @@ def parse_args(args=None): commands according to `format` and `--all`. Target paths are initially all Python distribution root paths - (as determined by the existence of setup.py, etc. files). + (as determined by the existence of pyproject.toml, etc. files). They are then augmented according to the section of the `PROJECT_ROOT/eachdist.ini` config file specified by the `--mode` option. @@ -518,18 +518,18 @@ def lint_args(args): runsubprocess( args.dry_run, - ("black", ".") + (("--diff", "--check") if args.check_only else ()), + ("black", "--config", "pyproject.toml", ".") + (("--diff", "--check") if args.check_only else ()), cwd=rootdir, check=True, ) runsubprocess( args.dry_run, - ("isort", ".") + ("isort", "--settings-path", ".isort.cfg", ".") + (("--diff", "--check-only") if args.check_only else ()), cwd=rootdir, check=True, ) - runsubprocess(args.dry_run, ("flake8", rootdir), check=True) + runsubprocess(args.dry_run, ("flake8", "--config", ".flake8", rootdir), check=True) execute_args( parse_subargs( args, ("exec", "pylint {}", "--all", "--mode", "lintroots") @@ -629,7 +629,7 @@ def update_dependencies(targets, version, packages): for pkg in packages: update_files( targets, - "setup.cfg", + "pyproject.toml", rf"({basename(pkg)}.*)==(.*)", r"\1== " + version, ) @@ -694,19 +694,19 @@ def test_args(args): def format_args(args): - format_dir = str(find_projectroot()) + root_dir = format_dir = str(find_projectroot()) if args.path: format_dir = os.path.join(format_dir, args.path) runsubprocess( args.dry_run, - ("black", "."), + ("black", "--config", f"{root_dir}/pyproject.toml", "."), cwd=format_dir, check=True, ) runsubprocess( args.dry_run, - ("isort", "--profile", "black", "."), + ("isort", "--settings-path", f"{root_dir}/.isort.cfg", "--profile", "black", "."), cwd=format_dir, check=True, ) From 725c5800121362f773c7a1432f91ae0f6f68c33f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 20 Sep 2022 17:43:34 +0200 Subject: [PATCH 1319/1517] Update contrib repo SHA (#2937) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4af72ac18a..4295e306e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 23bfe9fcadc1e205fbce1b6cf58fc3004eecace3 + CONTRIB_REPO_SHA: d5ada286e138e394593454e15f1b79e35149b83e # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when From c0e8f4056db774e4eeaf3f30016a19b2ba8b0408 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 22 Sep 2022 18:57:58 +0530 Subject: [PATCH 1320/1517] Add force_flush to span exporters (#2919) * Add force_flush to span exporters * Fix lint * add missing method to grpc exporter * Update contrib SHA * Add CHANGELOG entry --- CHANGELOG.md | 2 ++ .../opentelemetry/exporter/jaeger/proto/grpc/__init__.py | 3 +++ .../src/opentelemetry/exporter/jaeger/thrift/__init__.py | 3 +++ .../exporter/opencensus/trace_exporter/__init__.py | 3 +++ .../exporter/otlp/proto/grpc/trace_exporter/__init__.py | 3 +++ .../exporter/otlp/proto/http/trace_exporter/__init__.py | 3 +++ .../src/opentelemetry/exporter/zipkin/json/__init__.py | 3 +++ .../opentelemetry/exporter/zipkin/proto/http/__init__.py | 3 +++ .../src/opentelemetry/sdk/trace/export/__init__.py | 9 +++++++++ .../sdk/trace/export/in_memory_span_exporter.py | 3 +++ 10 files changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31f0155507..5a50353d5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add support for setting OTLP export protocol with env vars, as defined in the [specifications](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specify-protocol) ([#2893](https://github.com/open-telemetry/opentelemetry-python/pull/2893)) +- Add force_flush to span exporters + ([#2919](https://github.com/open-telemetry/opentelemetry-python/pull/2919)) ## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0) - 2022-08-08 diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py index f738ae336c..3f623327af 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py @@ -186,3 +186,6 @@ def export(self, spans) -> SpanExportResult: def shutdown(self): pass + + def force_flush(self, timeout_millis: int = 30000) -> bool: + return True diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py index 1071085777..a4f8e36bb9 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py @@ -222,3 +222,6 @@ def export(self, spans) -> SpanExportResult: def shutdown(self): pass + + def force_flush(self, timeout_millis: int = 30000) -> bool: + return True diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py index f1987ffb98..b855728cad 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py @@ -101,6 +101,9 @@ def generate_span_requests(self, spans): ) yield service_request + def force_flush(self, timeout_millis: int = 30000) -> bool: + return True + # pylint: disable=too-many-branches def translate_to_collector(spans: Sequence[ReadableSpan]): diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 5626012536..555c903156 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -290,6 +290,9 @@ def _translate_data( def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return self._export(spans) + def force_flush(self, timeout_millis: int = 30000) -> bool: + return True + @property def _exporting(self): return "traces" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 6f0d6ee58d..3c95a325b8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -165,6 +165,9 @@ def shutdown(self): self._session.close() self._shutdown = True + def force_flush(self, timeout_millis: int = 30000) -> bool: + return True + def _compression_from_env() -> Compression: compression = ( diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py index 7728090f54..ba313db942 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py @@ -186,3 +186,6 @@ def shutdown(self) -> None: return self.session.close() self._closed = True + + def force_flush(self, timeout_millis: int = 30000) -> bool: + return True diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py index 5856cd7e4e..8177efc07b 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py @@ -178,3 +178,6 @@ def shutdown(self) -> None: return self.session.close() self._closed = True + + def force_flush(self, timeout_millis: int = 30000) -> bool: + return True diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 558618c8d7..ef04895819 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -75,6 +75,12 @@ def shutdown(self) -> None: Called when the SDK is shut down. """ + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Hint to ensure that the export of any spans the exporter has received + prior to the call to ForceFlush SHOULD be completed as soon as possible, preferably + before returning from this method. + """ + class SimpleSpanProcessor(SpanProcessor): """Simple SpanProcessor implementation. @@ -438,3 +444,6 @@ def export(self, spans: typing.Sequence[ReadableSpan]) -> SpanExportResult: self.out.write(self.formatter(span)) self.out.flush() return SpanExportResult.SUCCESS + + def force_flush(self, timeout_millis: int = 30000) -> bool: + return True diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py index e46266b93b..de86fba277 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py @@ -56,3 +56,6 @@ def shutdown(self): Calls to export after the exporter has been shut down will fail. """ self._stopped = True + + def force_flush(self, timeout_millis: int = 30000) -> bool: + return True From 42db164fb1b783f708eaabc84ca7a03e2fe763de Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 26 Sep 2022 18:59:33 +0530 Subject: [PATCH 1321/1517] Update scripts (#2944) --- scripts/build.sh | 7 ------- scripts/eachdist.py | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index ed87583860..c5b305cef8 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -27,11 +27,4 @@ DISTDIR=dist fi ) done - # Build a wheel for each source distribution - ( - cd $DISTDIR - for x in *.tar.gz ; do - pip wheel --no-deps $x - done - ) ) diff --git a/scripts/eachdist.py b/scripts/eachdist.py index dc803af4cf..c02a75b256 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -631,7 +631,7 @@ def update_dependencies(targets, version, packages): targets, "pyproject.toml", rf"({basename(pkg)}.*)==(.*)", - r"\1== " + version, + r"\1== " + version + '",', ) From 6a61ebacdcd6e865c6e8d4fc6ea74422ce87820a Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 27 Sep 2022 02:57:28 +0530 Subject: [PATCH 1322/1517] updating changelogs and version to 1.13.0-0.34b0 (#2950) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 6 +++++- eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-grpc/pyproject.toml | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-http/pyproject.toml | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/pyproject.toml | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/pyproject.toml | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/pyproject.toml | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 31 files changed, 41 insertions(+), 37 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4295e306e8..6d749632c2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: d5ada286e138e394593454e15f1b79e35149b83e + CONTRIB_REPO_SHA: 639f503cc2c8eb626484490e7c4b671d5595bb33 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a50353d5c..605a68ed0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0-0.34b0...HEAD) + +## [1.13.0-0.34b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.13.0-0.34b0) - 2022-09-26 + + - Add a configurable max_export_batch_size to the gRPC metrics exporter ([#2809](https://github.com/open-telemetry/opentelemetry-python/pull/2809)) diff --git a/eachdist.ini b/eachdist.ini index 5d42f3c298..7cc26cc2ca 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.12.0 +version=1.13.0 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.33b0 +version=0.34b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 9b9b7e7bef..b65ab4af2b 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 9b9b7e7bef..b65ab4af2b 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/exporter/opentelemetry-exporter-jaeger/pyproject.toml b/exporter/opentelemetry-exporter-jaeger/pyproject.toml index eb32a59c1f..3f7cf695b0 100644 --- a/exporter/opentelemetry-exporter-jaeger/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-jaeger-proto-grpc == 1.12.0", - "opentelemetry-exporter-jaeger-thrift == 1.12.0", + "opentelemetry-exporter-jaeger-proto-grpc == 1.13.0", + "opentelemetry-exporter-jaeger-thrift == 1.13.0", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 9b9b7e7bef..b65ab4af2b 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 6b2801561b..09b3473b7d 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.33b0" +__version__ = "0.34b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index f8172f6f9b..bf7a261d9f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "googleapis-common-protos ~= 1.52", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.12", - "opentelemetry-proto == 1.12.0", + "opentelemetry-proto == 1.13.0", "opentelemetry-sdk ~= 1.12", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 797af7e49e..b9536c2461 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index e24085625b..edfc307c17 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.12", - "opentelemetry-proto == 1.12.0", + "opentelemetry-proto == 1.13.0", "opentelemetry-sdk ~= 1.12", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 797af7e49e..b9536c2461 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index 0f1b603fd4..0f3936ad57 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-otlp-proto-grpc == 1.12.0", - "opentelemetry-exporter-otlp-proto-http == 1.12.0", + "opentelemetry-exporter-otlp-proto-grpc == 1.13.0", + "opentelemetry-exporter-otlp-proto-http == 1.13.0", ] [project.entry-points.opentelemetry_logs_exporter] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 797af7e49e..b9536c2461 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 6b2801561b..09b3473b7d 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.33b0" +__version__ = "0.34b0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 797af7e49e..b9536c2461 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml index d3e6501e60..eb5bde0440 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.3", - "opentelemetry-exporter-zipkin-json == 1.12.0", + "opentelemetry-exporter-zipkin-json == 1.13.0", "opentelemetry-sdk ~= 1.11", "protobuf ~= 3.12", "requests ~= 2.7", diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 797af7e49e..b9536c2461 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/exporter/opentelemetry-exporter-zipkin/pyproject.toml b/exporter/opentelemetry-exporter-zipkin/pyproject.toml index 7da93bc37b..76cac43f4b 100644 --- a/exporter/opentelemetry-exporter-zipkin/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-zipkin-json == 1.12.0", - "opentelemetry-exporter-zipkin-proto-http == 1.12.0", + "opentelemetry-exporter-zipkin-json == 1.13.0", + "opentelemetry-exporter-zipkin-proto-http == 1.13.0", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 797af7e49e..b9536c2461 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 797af7e49e..b9536c2461 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 797af7e49e..b9536c2461 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index d217ae7354..9b106b83f8 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-api == 1.12.0", - "opentelemetry-semantic-conventions == 0.33b0", + "opentelemetry-api == 1.13.0", + "opentelemetry-semantic-conventions == 0.34b0", "setuptools >= 16.0", "typing-extensions >= 3.7.4", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 797af7e49e..b9536c2461 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 6b2801561b..09b3473b7d 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.33b0" +__version__ = "0.34b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 797af7e49e..b9536c2461 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 797af7e49e..b9536c2461 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/shim/opentelemetry-opentracing-shim/pyproject.toml b/shim/opentelemetry-opentracing-shim/pyproject.toml index 2d60f490e5..46ba16884e 100644 --- a/shim/opentelemetry-opentracing-shim/pyproject.toml +++ b/shim/opentelemetry-opentracing-shim/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.33b0", + "opentelemetry-test-utils == 0.34b0", "opentracing ~= 2.2.0", ] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 6b2801561b..09b3473b7d 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.33b0" +__version__ = "0.34b0" diff --git a/tests/opentelemetry-test-utils/pyproject.toml b/tests/opentelemetry-test-utils/pyproject.toml index 5e930fded3..e4307e16f6 100644 --- a/tests/opentelemetry-test-utils/pyproject.toml +++ b/tests/opentelemetry-test-utils/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ ] dependencies = [ "asgiref ~= 3.0", - "opentelemetry-api == 1.12.0", - "opentelemetry-sdk == 1.12.0", + "opentelemetry-api == 1.13.0", + "opentelemetry-sdk == 1.13.0", ] [project.optional-dependencies] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 3884a25fee..a590883f7e 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.33b0" +__version__ = "0.34b0" From cd4ccab1841347102b412bdc209a74133b766cea Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 28 Sep 2022 13:57:42 +0100 Subject: [PATCH 1323/1517] Update default explicit bucket histogram boundaries (#2952) --- CHANGELOG.md | 5 ++++- .../sdk/metrics/_internal/aggregation.py | 10 +++++++++ .../tests/metrics/test_aggregation.py | 22 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 605a68ed0b..ea4f873b37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0-0.34b0...HEAD) -## [1.13.0-0.34b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.13.0-0.34b0) - 2022-09-26 +- Update explicit histogram bucket boundaries + ([#2947](https://github.com/open-telemetry/opentelemetry-python/pull/2947)) + +## [1.13.0-0.34b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.13.0) - 2022-09-26 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py index d7d825c9c5..4fb9041d4c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py @@ -227,7 +227,12 @@ def __init__( 100.0, 250.0, 500.0, + 750.0, 1000.0, + 2500.0, + 5000.0, + 7500.0, + 10000.0, ), record_min_max: bool = True, ): @@ -454,7 +459,12 @@ def __init__( 100.0, 250.0, 500.0, + 750.0, 1000.0, + 2500.0, + 5000.0, + 7500.0, + 10000.0, ), record_min_max: bool = True, ) -> None: diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 5acf701b18..9c9de1f2cb 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -378,6 +378,28 @@ def test_collect(self): second_histogram.time_unix_nano, first_histogram.time_unix_nano ) + def test_boundaries(self): + self.assertEqual( + _ExplicitBucketHistogramAggregation(Mock(), 0)._boundaries, + ( + 0.0, + 5.0, + 10.0, + 25.0, + 50.0, + 75.0, + 100.0, + 250.0, + 500.0, + 750.0, + 1000.0, + 2500.0, + 5000.0, + 7500.0, + 10000.0, + ), + ) + class TestAggregationFactory(TestCase): def test_sum_factory(self): From 6e9af76f9fdbf03cc0bb7c1e2bcf87af28452aeb Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 6 Oct 2022 17:04:41 +0100 Subject: [PATCH 1324/1517] Fix dead link (#2962) Fixes #2961 --- CHANGELOG.md | 2 +- scripts/eachdist.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea4f873b37..6d3b1d2fed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0-0.34b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0...HEAD) - Update explicit histogram bucket boundaries ([#2947](https://github.com/open-telemetry/opentelemetry-python/pull/2947)) diff --git a/scripts/eachdist.py b/scripts/eachdist.py index c02a75b256..0c0620e95a 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -675,7 +675,7 @@ def release_args(args): update_dependencies(targets, version, packages) update_version_files(targets, version, packages) - update_changelogs("-".join(updated_versions)) + update_changelogs(updated_versions[0]) def test_args(args): From 96414a770e04f7a6e3214cb8694acdb2f4afc83b Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 7 Oct 2022 17:48:19 +0100 Subject: [PATCH 1325/1517] Update contrib repo SHA (#2966) Fixes #2965 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6d749632c2..c185eb1a5e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 639f503cc2c8eb626484490e7c4b671d5595bb33 + CONTRIB_REPO_SHA: a47c45e61b4a7906f826b203f15f0dcf646ee185 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when From 6f6f8d19babbb166816cb1d49b59d7985cf9ed11 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 10 Oct 2022 21:42:59 +0530 Subject: [PATCH 1326/1517] docs: bump protobuf from 3.15.0 to 3.18.3 (#2969) --- .../examples/fork-process-model/flask-gunicorn/requirements.txt | 2 +- docs/examples/fork-process-model/flask-uwsgi/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt index e91747d59e..86b654a95d 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt +++ b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt @@ -12,7 +12,7 @@ opentelemetry-instrumentation==0.18b0 opentelemetry-instrumentation-flask==0.18b1 opentelemetry-instrumentation-wsgi==0.18b1 opentelemetry-sdk==0.18b0 -protobuf==3.15.0 +protobuf==3.18.3 six==1.15.0 thrift==0.13.0 uWSGI==2.0.19.1 diff --git a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt index e91747d59e..86b654a95d 100644 --- a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt +++ b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt @@ -12,7 +12,7 @@ opentelemetry-instrumentation==0.18b0 opentelemetry-instrumentation-flask==0.18b1 opentelemetry-instrumentation-wsgi==0.18b1 opentelemetry-sdk==0.18b0 -protobuf==3.15.0 +protobuf==3.18.3 six==1.15.0 thrift==0.13.0 uWSGI==2.0.19.1 From 8927ae5085c5cc3d62603e7bdd473f34abf882ff Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 12 Oct 2022 05:07:08 -0700 Subject: [PATCH 1327/1517] `exporter-otlp-proto-http`: add user agent string (#2959) * `exporter-otlp-proto-http`: add user agent string Adding user agent string to OTLP HTTP exporter. As part of the change, I refactored the content-type header as well. Part of #2958 * update changelog * fix linting, fix link Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 ++ .../opentelemetry/exporter/otlp/proto/http/__init__.py | 8 ++++++++ .../exporter/otlp/proto/http/_log_exporter/__init__.py | 9 +++++---- .../otlp/proto/http/_log_exporter/encoder/__init__.py | 2 -- .../otlp/proto/http/trace_exporter/__init__.py | 9 +++++---- .../otlp/proto/http/trace_exporter/encoder/__init__.py | 2 -- .../tests/test_proto_log_exporter.py | 10 +++++----- .../tests/test_proto_span_exporter.py | 5 +++++ .../tests/test_protobuf_encoder.py | 5 ----- 9 files changed, 30 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d3b1d2fed..879ec02598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update explicit histogram bucket boundaries ([#2947](https://github.com/open-telemetry/opentelemetry-python/pull/2947)) +- `exporter-otlp-proto-http`: add user agent string + ([#2959](https://github.com/open-telemetry/opentelemetry-python/pull/2959)) ## [1.13.0-0.34b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.13.0) - 2022-09-26 diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py index 08b0725835..a14f0e2992 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py @@ -71,6 +71,14 @@ """ import enum +from .version import __version__ + + +_OTLP_HTTP_HEADERS = { + "Content-Type": "application/x-protobuf", + "User-Agent": "OTel OTLP Exporter Python/" + __version__, +} + class Compression(enum.Enum): NoCompression = "none" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 041f1ab3c0..a74f849f40 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -35,7 +35,10 @@ LogExportResult, LogData, ) -from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http import ( + _OTLP_HTTP_HEADERS, + Compression, +) from opentelemetry.exporter.otlp.proto.http._log_exporter.encoder import ( _ProtobufEncoder, ) @@ -78,9 +81,7 @@ def __init__( self._compression = compression or _compression_from_env() self._session = session or requests.Session() self._session.headers.update(self._headers) - self._session.headers.update( - {"Content-Type": _ProtobufEncoder._CONTENT_TYPE} - ) + self._session.headers.update(_OTLP_HTTP_HEADERS) if self._compression is not Compression.NoCompression: self._session.headers.update( {"Content-Encoding": self._compression.value} diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py index bf8784aacf..c8f2dd8456 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py @@ -36,8 +36,6 @@ class _ProtobufEncoder: - _CONTENT_TYPE = "application/x-protobuf" - @classmethod def serialize(cls, batch: Sequence[LogData]) -> str: return cls.encode(batch).SerializeToString() diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 3c95a325b8..a65bc44320 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -36,7 +36,10 @@ OTEL_EXPORTER_OTLP_TIMEOUT, ) from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult -from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http import ( + _OTLP_HTTP_HEADERS, + Compression, +) from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import ( _ProtobufEncoder, ) @@ -89,9 +92,7 @@ def __init__( self._compression = compression or _compression_from_env() self._session = session or requests.Session() self._session.headers.update(self._headers) - self._session.headers.update( - {"Content-Type": _ProtobufEncoder._CONTENT_TYPE} - ) + self._session.headers.update(_OTLP_HTTP_HEADERS) if self._compression is not Compression.NoCompression: self._session.headers.update( {"Content-Encoding": self._compression.value} diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py index fc0d9608ef..c1c9fe8864 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py @@ -60,8 +60,6 @@ class _ProtobufEncoder: - _CONTENT_TYPE = "application/x-protobuf" - @classmethod def serialize(cls, sdk_spans: Sequence[SDKSpan]) -> str: return cls.encode(sdk_spans).SerializeToString() diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index d5e34b7463..2063820b5d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -84,6 +84,11 @@ def test_constructor_default(self): self.assertIs(exporter._compression, DEFAULT_COMPRESSION) self.assertEqual(exporter._headers, {}) self.assertIsInstance(exporter._session, requests.Session) + self.assertIn("User-Agent", exporter._session.headers) + self.assertEqual( + exporter._session.headers.get("Content-Type"), + "application/x-protobuf", + ) @patch.dict( "os.environ", @@ -154,11 +159,6 @@ def test_serialize(self): expected_encoding.SerializeToString(), ) - def test_content_type(self): - self.assertEqual( - _ProtobufEncoder._CONTENT_TYPE, "application/x-protobuf" - ) - @staticmethod def _get_sdk_log_data() -> List[LogData]: log1 = LogData( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index 4eb0db6160..73e54e86c0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -58,6 +58,11 @@ def test_constructor_default(self): self.assertIs(exporter._compression, DEFAULT_COMPRESSION) self.assertEqual(exporter._headers, {}) self.assertIsInstance(exporter._session, requests.Session) + self.assertIn("User-Agent", exporter._session.headers) + self.assertEqual( + exporter._session.headers.get("Content-Type"), + "application/x-protobuf", + ) @patch.dict( "os.environ", diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py index b3718623c1..7145ddbfa9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py @@ -69,11 +69,6 @@ def test_serialize(self): expected_encoding.SerializeToString(), ) - def test_content_type(self): - self.assertEqual( - _ProtobufEncoder._CONTENT_TYPE, "application/x-protobuf" - ) - @staticmethod def get_exhaustive_otel_span_list() -> List[SDKSpan]: trace_id = 0x3E0C63257DE34C926F9EFCD03927272E From 321f90f96b80dc8118d58cc639e26a950418d138 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 13 Oct 2022 04:43:33 -0700 Subject: [PATCH 1328/1517] Add http-metrics to semantic conventions (#2976) * sem * lint --- CHANGELOG.md | 2 ++ .../opentelemetry/semconv/metrics/__init__.py | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/__init__.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 879ec02598..7cab9302a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2947](https://github.com/open-telemetry/opentelemetry-python/pull/2947)) - `exporter-otlp-proto-http`: add user agent string ([#2959](https://github.com/open-telemetry/opentelemetry-python/pull/2959)) +- Add http-metric instrument names to semantic conventions + ([#2976](https://github.com/open-telemetry/opentelemetry-python/pull/2976)) ## [1.13.0-0.34b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.13.0) - 2022-09-26 diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/__init__.py new file mode 100644 index 0000000000..8ff6720165 --- /dev/null +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/__init__.py @@ -0,0 +1,32 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=too-many-lines + + +class MetricInstruments: + + HTTP_SERVER_DURATION = "http.server.duration" + + HTTP_SERVER_REQUEST_SIZE = "http.server.request.size" + + HTTP_SERVER_RESPONSE_SIZE = "http.server.response.size" + + HTTP_SERVER_ACTIVE_REQUESTS = "http.server.active_requests" + + HTTP_CLIENT_DURATION = "http.client.duration" + + HTTP_CLIENT_REQUEST_SIZE = "http.client.request.size" + + HTTP_CLIENT_RESPONSE_SIZE = "http.client.response.size" From 6453a3294c83ee65a113b2c19b2a623cdadfbb07 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 25 Oct 2022 20:53:28 +0100 Subject: [PATCH 1329/1517] Fix Pytest version (#2987) Fixes #2986 --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 3af6328fad..e3fced6b83 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -7,7 +7,7 @@ mypy==0.931 sphinx~=3.5.4 sphinx-rtd-theme~=0.5 sphinx-autodoc-typehints~=1.12.0 -pytest>=6.0 +pytest==7.1.3 pytest-cov>=2.8 readme-renderer~=24.0 grpcio-tools~=1.41.0 From 4c740f9cdb83a8135876e91e824f71c3932a5010 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 26 Oct 2022 20:47:21 +0100 Subject: [PATCH 1330/1517] Fix Python version for docs (#2985) * Fix Python version for docs Fixes #2984 * Add FIXME --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 5a2dfb7dfe..1a24186fbd 100644 --- a/tox.ini +++ b/tox.ini @@ -235,6 +235,8 @@ commands = python scripts/eachdist.py lint --check-only [testenv:docs] +# FIXME See #2984 +basepython: python3.9 recreate = True deps = -c {toxinidir}/dev-requirements.txt From 8318f0121733cabf9e69743d07213e65cc300d88 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 26 Oct 2022 22:23:50 +0100 Subject: [PATCH 1331/1517] Update log symbol names (#2943) --- CHANGELOG.md | 3 +- docs/examples/logs/example.py | 18 +- opentelemetry-sdk/pyproject.toml | 4 +- .../sdk/_configuration/__init__.py | 18 +- .../src/opentelemetry/sdk/_logs/__init__.py | 164 +++++++++--------- .../sdk/_logs/export/__init__.py | 16 +- .../sdk/environment_variables.py | 8 +- opentelemetry-sdk/tests/logs/test_export.py | 126 +++++++------- .../tests/logs/test_global_provider.py | 34 ++-- opentelemetry-sdk/tests/logs/test_handler.py | 69 ++++---- .../tests/logs/test_multi_log_prcessor.py | 96 +++++----- opentelemetry-sdk/tests/test_configurator.py | 30 ++-- 12 files changed, 291 insertions(+), 295 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cab9302a0..53cb313e4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0...HEAD) +- Update log symbol names + ([#2943](https://github.com/open-telemetry/opentelemetry-python/pull/2943)) - Update explicit histogram bucket boundaries ([#2947](https://github.com/open-telemetry/opentelemetry-python/pull/2947)) - `exporter-otlp-proto-http`: add user agent string @@ -17,7 +19,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.13.0-0.34b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.13.0) - 2022-09-26 - - Add a configurable max_export_batch_size to the gRPC metrics exporter ([#2809](https://github.com/open-telemetry/opentelemetry-python/pull/2809)) - Remove support for 3.6 diff --git a/docs/examples/logs/example.py b/docs/examples/logs/example.py index ff05251212..b2b1593262 100644 --- a/docs/examples/logs/example.py +++ b/docs/examples/logs/example.py @@ -5,11 +5,11 @@ OTLPLogExporter, ) from opentelemetry.sdk._logs import ( - LogEmitterProvider, + LoggerProvider, LoggingHandler, - set_log_emitter_provider, + set_logger_provider, ) -from opentelemetry.sdk._logs.export import BatchLogProcessor +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( @@ -22,7 +22,7 @@ BatchSpanProcessor(ConsoleSpanExporter()) ) -log_emitter_provider = LogEmitterProvider( +logger_provider = LoggerProvider( resource=Resource.create( { "service.name": "shoppingcart", @@ -30,13 +30,11 @@ } ), ) -set_log_emitter_provider(log_emitter_provider) +set_logger_provider(logger_provider) exporter = OTLPLogExporter(insecure=True) -log_emitter_provider.add_log_processor(BatchLogProcessor(exporter)) -handler = LoggingHandler( - level=logging.NOTSET, log_emitter_provider=log_emitter_provider -) +logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) +handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider) # Attach OTLP handler to root logger logging.getLogger().addHandler(handler) @@ -60,4 +58,4 @@ # Do something logger2.error("Hyderabad, we have a major problem.") -log_emitter_provider.shutdown() +logger_provider.shutdown() diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index 9b106b83f8..e076a6a40e 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -40,8 +40,8 @@ sdk = "opentelemetry.sdk.environment_variables" [project.entry-points.opentelemetry_id_generator] random = "opentelemetry.sdk.trace.id_generator:RandomIdGenerator" -[project.entry-points.opentelemetry_log_emitter_provider] -sdk_log_emitter_provider = "opentelemetry.sdk._logs:LogEmitterProvider" +[project.entry-points.opentelemetry_logger_provider] +sdk_logger_provider = "opentelemetry.sdk._logs:LoggerProvider" [project.entry-points.opentelemetry_logs_exporter] console = "opentelemetry.sdk._logs.export:ConsoleLogExporter" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 159d471900..fa057f785c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -34,11 +34,11 @@ ) from opentelemetry.metrics import set_meter_provider from opentelemetry.sdk._logs import ( - LogEmitterProvider, + LoggerProvider, LoggingHandler, - set_log_emitter_provider, + set_logger_provider, ) -from opentelemetry.sdk._logs.export import BatchLogProcessor, LogExporter +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, LogExporter from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, @@ -212,18 +212,16 @@ def _init_logging( auto_resource[ ResourceAttributes.TELEMETRY_AUTO_VERSION ] = auto_instrumentation_version - provider = LogEmitterProvider(resource=Resource.create(auto_resource)) - set_log_emitter_provider(provider) + provider = LoggerProvider(resource=Resource.create(auto_resource)) + set_logger_provider(provider) for _, exporter_class in exporters.items(): exporter_args = {} - provider.add_log_processor( - BatchLogProcessor(exporter_class(**exporter_args)) + provider.add_log_record_processor( + BatchLogRecordProcessor(exporter_class(**exporter_args)) ) - handler = LoggingHandler( - level=logging.NOTSET, log_emitter_provider=provider - ) + handler = LoggingHandler(level=logging.NOTSET, logger_provider=provider) logging.getLogger().addHandler(handler) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index 1a617ed3f0..1ca3aa48b0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -25,7 +25,7 @@ from opentelemetry.sdk._logs.severity import SeverityNumber, std_to_otlp from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_LOG_EMITTER_PROVIDER, + _OTEL_PYTHON_LOGGER_PROVIDER, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util import ns_to_iso_str @@ -46,7 +46,7 @@ class LogRecord: """A LogRecord instance represents an event being logged. - LogRecord instances are created and emitted via `LogEmitter` + LogRecord instances are created and emitted via `Logger` every time something is logged. They contain all the information pertinent to the event being logged. """ @@ -113,11 +113,11 @@ def __init__( self.instrumentation_scope = instrumentation_scope -class LogProcessor(abc.ABC): +class LogRecordProcessor(abc.ABC): """Interface to hook the log record emitting action. Log processors can be registered directly using - :func:`LogEmitterProvider.add_log_processor` and they are invoked + :func:`LoggerProvider.add_log_record_processor` and they are invoked in the same order as they were registered. """ @@ -127,7 +127,7 @@ def emit(self, log_data: LogData): @abc.abstractmethod def shutdown(self): - """Called when a :class:`opentelemetry.sdk._logs.LogEmitter` is shutdown""" + """Called when a :class:`opentelemetry.sdk._logs.Logger` is shutdown""" @abc.abstractmethod def force_flush(self, timeout_millis: int = 30000): @@ -145,8 +145,8 @@ def force_flush(self, timeout_millis: int = 30000): # Temporary fix until https://github.com/PyCQA/pylint/issues/4098 is resolved # pylint:disable=no-member -class SynchronousMultiLogProcessor(LogProcessor): - """Implementation of class:`LogProcessor` that forwards all received +class SynchronousMultiLogRecordProcessor(LogRecordProcessor): + """Implementation of class:`LogRecordProcessor` that forwards all received events to a list of log processors sequentially. The underlying log processors are called in sequential order as they were @@ -156,21 +156,23 @@ class SynchronousMultiLogProcessor(LogProcessor): def __init__(self): # use a tuple to avoid race conditions when adding a new log and # iterating through it on "emit". - self._log_processors = () # type: Tuple[LogProcessor, ...] + self._log_record_processors = () # type: Tuple[LogRecordProcessor, ...] self._lock = threading.Lock() - def add_log_processor(self, log_processor: LogProcessor) -> None: + def add_log_record_processor( + self, log_record_processor: LogRecordProcessor + ) -> None: """Adds a Logprocessor to the list of log processors handled by this instance""" with self._lock: - self._log_processors += (log_processor,) + self._log_record_processors += (log_record_processor,) def emit(self, log_data: LogData) -> None: - for lp in self._log_processors: + for lp in self._log_record_processors: lp.emit(log_data) def shutdown(self) -> None: """Shutdown the log processors one by one""" - for lp in self._log_processors: + for lp in self._log_record_processors: lp.shutdown() def force_flush(self, timeout_millis: int = 30000) -> bool: @@ -186,7 +188,7 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: False otherwise. """ deadline_ns = time_ns() + timeout_millis * 1000000 - for lp in self._log_processors: + for lp in self._log_record_processors: current_ts = time_ns() if current_ts >= deadline_ns: return False @@ -197,8 +199,8 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: return True -class ConcurrentMultiLogProcessor(LogProcessor): - """Implementation of :class:`LogProcessor` that forwards all received +class ConcurrentMultiLogRecordProcessor(LogRecordProcessor): + """Implementation of :class:`LogRecordProcessor` that forwards all received events to a list of log processors in parallel. Calls to the underlying log processors are forwarded in parallel by @@ -213,24 +215,26 @@ class ConcurrentMultiLogProcessor(LogProcessor): def __init__(self, max_workers: int = 2): # use a tuple to avoid race conditions when adding a new log and # iterating through it on "emit". - self._log_processors = () # type: Tuple[LogProcessor, ...] + self._log_record_processors = () # type: Tuple[LogRecordProcessor, ...] self._lock = threading.Lock() self._executor = concurrent.futures.ThreadPoolExecutor( max_workers=max_workers ) - def add_log_processor(self, log_processor: LogProcessor): + def add_log_record_processor( + self, log_record_processor: LogRecordProcessor + ): with self._lock: - self._log_processors += (log_processor,) + self._log_record_processors += (log_record_processor,) def _submit_and_wait( self, - func: Callable[[LogProcessor], Callable[..., None]], + func: Callable[[LogRecordProcessor], Callable[..., None]], *args: Any, **kwargs: Any, ): futures = [] - for lp in self._log_processors: + for lp in self._log_record_processors: future = self._executor.submit(func(lp), *args, **kwargs) futures.append(future) for future in futures: @@ -254,7 +258,7 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: False otherwise. """ futures = [] - for lp in self._log_processors: + for lp in self._log_record_processors: future = self._executor.submit(lp.force_flush, timeout_millis) futures.append(future) @@ -311,14 +315,12 @@ class LoggingHandler(logging.Handler): def __init__( self, level=logging.NOTSET, - log_emitter_provider=None, + logger_provider=None, ) -> None: super().__init__(level=level) - self._log_emitter_provider = ( - log_emitter_provider or get_log_emitter_provider() - ) - self._log_emitter = get_log_emitter( - __name__, log_emitter_provider=self._log_emitter_provider + self._logger_provider = logger_provider or get_logger_provider() + self._logger = get_logger( + __name__, logger_provider=self._logger_provider ) @staticmethod @@ -358,7 +360,7 @@ def _translate(self, record: logging.LogRecord) -> LogRecord: severity_text=record.levelname, severity_number=severity_number, body=record.getMessage(), - resource=self._log_emitter.resource, + resource=self._logger.resource, attributes=attributes, ) @@ -368,26 +370,27 @@ def emit(self, record: logging.LogRecord) -> None: The record is translated to OTLP format, and then sent across the pipeline. """ - self._log_emitter.emit(self._translate(record)) + self._logger.emit(self._translate(record)) def flush(self) -> None: """ Flushes the logging output. """ - self._log_emitter_provider.force_flush() + self._logger_provider.force_flush() -class LogEmitter: +class Logger: def __init__( self, resource: Resource, - multi_log_processor: Union[ - SynchronousMultiLogProcessor, ConcurrentMultiLogProcessor + multi_log_record_processor: Union[ + SynchronousMultiLogRecordProcessor, + ConcurrentMultiLogRecordProcessor, ], instrumentation_scope: InstrumentationScope, ): self._resource = resource - self._multi_log_processor = multi_log_processor + self._multi_log_record_processor = multi_log_record_processor self._instrumentation_scope = instrumentation_scope @property @@ -399,21 +402,22 @@ def emit(self, record: LogRecord): and instrumentation info. """ log_data = LogData(record, self._instrumentation_scope) - self._multi_log_processor.emit(log_data) + self._multi_log_record_processor.emit(log_data) -class LogEmitterProvider: +class LoggerProvider: def __init__( self, resource: Resource = Resource.create(), shutdown_on_exit: bool = True, - multi_log_processor: Union[ - SynchronousMultiLogProcessor, ConcurrentMultiLogProcessor + multi_log_record_processor: Union[ + SynchronousMultiLogRecordProcessor, + ConcurrentMultiLogRecordProcessor, ] = None, ): self._resource = resource - self._multi_log_processor = ( - multi_log_processor or SynchronousMultiLogProcessor() + self._multi_log_record_processor = ( + multi_log_record_processor or SynchronousMultiLogRecordProcessor() ) self._at_exit_handler = None if shutdown_on_exit: @@ -423,29 +427,33 @@ def __init__( def resource(self): return self._resource - def get_log_emitter( + def get_logger( self, instrumenting_module_name: str, instrumenting_module_version: str = "", - ) -> LogEmitter: - return LogEmitter( + ) -> Logger: + return Logger( self._resource, - self._multi_log_processor, + self._multi_log_record_processor, InstrumentationScope( instrumenting_module_name, instrumenting_module_version ), ) - def add_log_processor(self, log_processor: LogProcessor): - """Registers a new :class:`LogProcessor` for this `LogEmitterProvider` instance. + def add_log_record_processor( + self, log_record_processor: LogRecordProcessor + ): + """Registers a new :class:`LogRecordProcessor` for this `LoggerProvider` instance. The log processors are invoked in the same order they are registered. """ - self._multi_log_processor.add_log_processor(log_processor) + self._multi_log_record_processor.add_log_record_processor( + log_record_processor + ) def shutdown(self): """Shuts down the log processors.""" - self._multi_log_processor.shutdown() + self._multi_log_record_processor.shutdown() if self._at_exit_handler is not None: atexit.unregister(self._at_exit_handler) self._at_exit_handler = None @@ -461,61 +469,57 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: True if all the log processors flushes the logs within timeout, False otherwise. """ - return self._multi_log_processor.force_flush(timeout_millis) + return self._multi_log_record_processor.force_flush(timeout_millis) -_LOG_EMITTER_PROVIDER = None +_LOGGER_PROVIDER = None -def get_log_emitter_provider() -> LogEmitterProvider: - """Gets the current global :class:`~.LogEmitterProvider` object.""" - global _LOG_EMITTER_PROVIDER # pylint: disable=global-statement - if _LOG_EMITTER_PROVIDER is None: - if _OTEL_PYTHON_LOG_EMITTER_PROVIDER not in os.environ: - _LOG_EMITTER_PROVIDER = LogEmitterProvider() - return _LOG_EMITTER_PROVIDER +def get_logger_provider() -> LoggerProvider: + """Gets the current global :class:`~.LoggerProvider` object.""" + global _LOGGER_PROVIDER # pylint: disable=global-statement + if _LOGGER_PROVIDER is None: + if _OTEL_PYTHON_LOGGER_PROVIDER not in os.environ: + _LOGGER_PROVIDER = LoggerProvider() + return _LOGGER_PROVIDER - _LOG_EMITTER_PROVIDER = cast( - "LogEmitterProvider", - _load_provider( - _OTEL_PYTHON_LOG_EMITTER_PROVIDER, "log_emitter_provider" - ), + _LOGGER_PROVIDER = cast( + "LoggerProvider", + _load_provider(_OTEL_PYTHON_LOGGER_PROVIDER, "logger_provider"), ) - return _LOG_EMITTER_PROVIDER + return _LOGGER_PROVIDER -def set_log_emitter_provider(log_emitter_provider: LogEmitterProvider) -> None: - """Sets the current global :class:`~.LogEmitterProvider` object. +def set_logger_provider(logger_provider: LoggerProvider) -> None: + """Sets the current global :class:`~.LoggerProvider` object. This can only be done once, a warning will be logged if any further attempt is made. """ - global _LOG_EMITTER_PROVIDER # pylint: disable=global-statement + global _LOGGER_PROVIDER # pylint: disable=global-statement - if _LOG_EMITTER_PROVIDER is not None: - _logger.warning( - "Overriding of current LogEmitterProvider is not allowed" - ) + if _LOGGER_PROVIDER is not None: + _logger.warning("Overriding of current LoggerProvider is not allowed") return - _LOG_EMITTER_PROVIDER = log_emitter_provider + _LOGGER_PROVIDER = logger_provider -def get_log_emitter( +def get_logger( instrumenting_module_name: str, instrumenting_library_version: str = "", - log_emitter_provider: Optional[LogEmitterProvider] = None, -) -> LogEmitter: - """Returns a `LogEmitter` for use within a python process. + logger_provider: Optional[LoggerProvider] = None, +) -> Logger: + """Returns a `Logger` for use within a python process. This function is a convenience wrapper for - opentelemetry.sdk._logs.LogEmitterProvider.get_log_emitter. + opentelemetry.sdk._logs.LoggerProvider.get_logger. - If log_emitter_provider param is omitted the current configured one is used. + If logger_provider param is omitted the current configured one is used. """ - if log_emitter_provider is None: - log_emitter_provider = get_log_emitter_provider() - return log_emitter_provider.get_log_emitter( + if logger_provider is None: + logger_provider = get_logger_provider() + return logger_provider.get_logger( instrumenting_module_name, instrumenting_library_version ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py index af0221d991..3f19b79e10 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py @@ -24,7 +24,7 @@ from typing import IO, Callable, Deque, List, Optional, Sequence from opentelemetry.context import attach, detach, set_value -from opentelemetry.sdk._logs import LogData, LogProcessor, LogRecord +from opentelemetry.sdk._logs import LogData, LogRecord, LogRecordProcessor from opentelemetry.util._once import Once _logger = logging.getLogger(__name__) @@ -41,7 +41,7 @@ class LogExporter(abc.ABC): Interface to be implemented by services that want to export logs received in their own format. - To export data this MUST be registered to the :class`opentelemetry.sdk._logs.LogEmitter` using a + To export data this MUST be registered to the :class`opentelemetry.sdk._logs.Logger` using a log processor. """ @@ -91,8 +91,8 @@ def shutdown(self): pass -class SimpleLogProcessor(LogProcessor): - """This is an implementation of LogProcessor which passes +class SimpleLogRecordProcessor(LogRecordProcessor): + """This is an implementation of LogRecordProcessor which passes received logs in the export-friendly LogData representation to the configured LogExporter, as soon as they are emitted. """ @@ -133,8 +133,8 @@ def __init__(self): _BSP_RESET_ONCE = Once() -class BatchLogProcessor(LogProcessor): - """This is an implementation of LogProcessor which creates batches of +class BatchLogRecordProcessor(LogRecordProcessor): + """This is an implementation of LogRecordProcessor which creates batches of received logs in the export-friendly LogData representation and send to the configured LogExporter, as soon as they are emitted. """ @@ -152,7 +152,7 @@ def __init__( self._export_timeout_millis = export_timeout_millis self._queue = collections.deque() # type: Deque[LogData] self._worker_thread = threading.Thread( - name="OtelBatchLogProcessor", + name="OtelBatchLogRecordProcessor", target=self.worker, daemon=True, ) @@ -174,7 +174,7 @@ def _at_fork_reinit(self): self._condition = threading.Condition(threading.Lock()) self._queue.clear() self._worker_thread = threading.Thread( - name="OtelBatchLogProcessor", + name="OtelBatchLogRecordProcessor", target=self.worker, daemon=True, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index ad635e89d5..d469c24478 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -412,13 +412,13 @@ If both are set, :envvar:`OTEL_SERVICE_NAME` takes precedence. """ -_OTEL_PYTHON_LOG_EMITTER_PROVIDER = "OTEL_PYTHON_LOG_EMITTER_PROVIDER" +_OTEL_PYTHON_LOGGER_PROVIDER = "OTEL_PYTHON_LOGGER_PROVIDER" """ -.. envvar:: OTEL_PYTHON_LOG_EMITTER_PROVIDER +.. envvar:: OTEL_PYTHON_LOGGER_PROVIDER -The :envvar:`OTEL_PYTHON_LOG_EMITTER_PROVIDER` environment variable allows users to +The :envvar:`OTEL_PYTHON_LOGGER_PROVIDER` environment variable allows users to provide the entry point for loading the log emitter provider. If not specified, SDK -LogEmitterProvider is used. +LoggerProvider is used. """ _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED = ( diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 3c55415d52..e65fc04a42 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -24,14 +24,14 @@ from opentelemetry.sdk import trace from opentelemetry.sdk._logs import ( LogData, - LogEmitterProvider, + LoggerProvider, LoggingHandler, LogRecord, ) from opentelemetry.sdk._logs.export import ( - BatchLogProcessor, + BatchLogRecordProcessor, ConsoleLogExporter, - SimpleLogProcessor, + SimpleLogRecordProcessor, ) from opentelemetry.sdk._logs.export.in_memory_log_exporter import ( InMemoryLogExporter, @@ -44,17 +44,17 @@ from opentelemetry.trace.span import INVALID_SPAN_CONTEXT -class TestSimpleLogProcessor(unittest.TestCase): - def test_simple_log_processor_default_level(self): +class TestSimpleLogRecordProcessor(unittest.TestCase): + def test_simple_log_record_processor_default_level(self): exporter = InMemoryLogExporter() - log_emitter_provider = LogEmitterProvider() + logger_provider = LoggerProvider() - log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) + logger_provider.add_log_record_processor( + SimpleLogRecordProcessor(exporter) + ) logger = logging.getLogger("default_level") - logger.addHandler( - LoggingHandler(log_emitter_provider=log_emitter_provider) - ) + logger.addHandler(LoggingHandler(logger_provider=logger_provider)) logger.warning("Something is wrong") finished_logs = exporter.get_finished_logs() @@ -66,17 +66,17 @@ def test_simple_log_processor_default_level(self): warning_log_record.severity_number, SeverityNumber.WARN ) - def test_simple_log_processor_custom_level(self): + def test_simple_log_record_processor_custom_level(self): exporter = InMemoryLogExporter() - log_emitter_provider = LogEmitterProvider() + logger_provider = LoggerProvider() - log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) + logger_provider.add_log_record_processor( + SimpleLogRecordProcessor(exporter) + ) logger = logging.getLogger("custom_level") logger.setLevel(logging.ERROR) - logger.addHandler( - LoggingHandler(log_emitter_provider=log_emitter_provider) - ) + logger.addHandler(LoggingHandler(logger_provider=logger_provider)) logger.warning("Warning message") logger.debug("Debug message") @@ -98,16 +98,16 @@ def test_simple_log_processor_custom_level(self): fatal_log_record.severity_number, SeverityNumber.FATAL ) - def test_simple_log_processor_trace_correlation(self): + def test_simple_log_record_processor_trace_correlation(self): exporter = InMemoryLogExporter() - log_emitter_provider = LogEmitterProvider() + logger_provider = LoggerProvider() - log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) + logger_provider.add_log_record_processor( + SimpleLogRecordProcessor(exporter) + ) logger = logging.getLogger("trace_correlation") - logger.addHandler( - LoggingHandler(log_emitter_provider=log_emitter_provider) - ) + logger.addHandler(LoggingHandler(logger_provider=logger_provider)) logger.warning("Warning message") finished_logs = exporter.get_finished_logs() @@ -137,16 +137,16 @@ def test_simple_log_processor_trace_correlation(self): self.assertEqual(log_record.span_id, span_context.span_id) self.assertEqual(log_record.trace_flags, span_context.trace_flags) - def test_simple_log_processor_shutdown(self): + def test_simple_log_record_processor_shutdown(self): exporter = InMemoryLogExporter() - log_emitter_provider = LogEmitterProvider() + logger_provider = LoggerProvider() - log_emitter_provider.add_log_processor(SimpleLogProcessor(exporter)) + logger_provider.add_log_record_processor( + SimpleLogRecordProcessor(exporter) + ) logger = logging.getLogger("shutdown") - logger.addHandler( - LoggingHandler(log_emitter_provider=log_emitter_provider) - ) + logger.addHandler(LoggingHandler(logger_provider=logger_provider)) logger.warning("Something is wrong") finished_logs = exporter.get_finished_logs() @@ -158,40 +158,40 @@ def test_simple_log_processor_shutdown(self): warning_log_record.severity_number, SeverityNumber.WARN ) exporter.clear() - log_emitter_provider.shutdown() + logger_provider.shutdown() logger.warning("Log after shutdown") finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 0) -class TestBatchLogProcessor(ConcurrencyTestBase): +class TestBatchLogRecordProcessor(ConcurrencyTestBase): def test_emit_call_log_record(self): exporter = InMemoryLogExporter() - log_processor = Mock(wraps=BatchLogProcessor(exporter)) - provider = LogEmitterProvider() - provider.add_log_processor(log_processor) + log_record_processor = Mock(wraps=BatchLogRecordProcessor(exporter)) + provider = LoggerProvider() + provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("emit_call") - logger.addHandler(LoggingHandler(log_emitter_provider=provider)) + logger.addHandler(LoggingHandler(logger_provider=provider)) logger.error("error") - self.assertEqual(log_processor.emit.call_count, 1) + self.assertEqual(log_record_processor.emit.call_count, 1) def test_shutdown(self): exporter = InMemoryLogExporter() - log_processor = BatchLogProcessor(exporter) + log_record_processor = BatchLogRecordProcessor(exporter) - provider = LogEmitterProvider() - provider.add_log_processor(log_processor) + provider = LoggerProvider() + provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("shutdown") - logger.addHandler(LoggingHandler(log_emitter_provider=provider)) + logger.addHandler(LoggingHandler(logger_provider=provider)) logger.warning("warning message: %s", "possible upcoming heatwave") logger.error("Very high rise in temperatures across the globe") logger.critical("Temperature hits high 420 C in Hyderabad") - log_processor.shutdown() + log_record_processor.shutdown() self.assertTrue(exporter._stopped) finished_logs = exporter.get_finished_logs() @@ -211,53 +211,53 @@ def test_shutdown(self): def test_force_flush(self): exporter = InMemoryLogExporter() - log_processor = BatchLogProcessor(exporter) + log_record_processor = BatchLogRecordProcessor(exporter) - provider = LogEmitterProvider() - provider.add_log_processor(log_processor) + provider = LoggerProvider() + provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("force_flush") - logger.addHandler(LoggingHandler(log_emitter_provider=provider)) + logger.addHandler(LoggingHandler(logger_provider=provider)) logger.critical("Earth is burning") - log_processor.force_flush() + log_record_processor.force_flush() finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 1) log_record = finished_logs[0].log_record self.assertEqual(log_record.body, "Earth is burning") self.assertEqual(log_record.severity_number, SeverityNumber.FATAL) - def test_log_processor_too_many_logs(self): + def test_log_record_processor_too_many_logs(self): exporter = InMemoryLogExporter() - log_processor = BatchLogProcessor(exporter) + log_record_processor = BatchLogRecordProcessor(exporter) - provider = LogEmitterProvider() - provider.add_log_processor(log_processor) + provider = LoggerProvider() + provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("many_logs") - logger.addHandler(LoggingHandler(log_emitter_provider=provider)) + logger.addHandler(LoggingHandler(logger_provider=provider)) for log_no in range(1000): logger.critical("Log no: %s", log_no) - self.assertTrue(log_processor.force_flush()) + self.assertTrue(log_record_processor.force_flush()) finised_logs = exporter.get_finished_logs() self.assertEqual(len(finised_logs), 1000) def test_with_multiple_threads(self): exporter = InMemoryLogExporter() - log_processor = BatchLogProcessor(exporter) + log_record_processor = BatchLogRecordProcessor(exporter) - provider = LogEmitterProvider() - provider.add_log_processor(log_processor) + provider = LoggerProvider() + provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("threads") - logger.addHandler(LoggingHandler(log_emitter_provider=provider)) + logger.addHandler(LoggingHandler(logger_provider=provider)) def bulk_log_and_flush(num_logs): for _ in range(num_logs): logger.critical("Critical message") - self.assertTrue(log_processor.force_flush()) + self.assertTrue(log_record_processor.force_flush()) with ThreadPoolExecutor(max_workers=69) as executor: futures = [] @@ -274,24 +274,24 @@ def bulk_log_and_flush(num_logs): hasattr(os, "fork"), "needs *nix", ) - def test_batch_log_processor_fork(self): + def test_batch_log_record_processor_fork(self): # pylint: disable=invalid-name exporter = InMemoryLogExporter() - log_processor = BatchLogProcessor( + log_record_processor = BatchLogRecordProcessor( exporter, max_export_batch_size=64, schedule_delay_millis=10, ) - provider = LogEmitterProvider() - provider.add_log_processor(log_processor) + provider = LoggerProvider() + provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("test-fork") - logger.addHandler(LoggingHandler(log_emitter_provider=provider)) + logger.addHandler(LoggingHandler(logger_provider=provider)) logger.critical("yolo") time.sleep(0.5) # give some time for the exporter to upload - self.assertTrue(log_processor.force_flush()) + self.assertTrue(log_record_processor.force_flush()) self.assertEqual(len(exporter.get_finished_logs()), 1) exporter.clear() @@ -315,7 +315,7 @@ def _target(): self.assertTrue(parent_conn.recv()) p.join() - log_processor.shutdown() + log_record_processor.shutdown() class TestConsoleLogExporter(unittest.TestCase): diff --git a/opentelemetry-sdk/tests/logs/test_global_provider.py b/opentelemetry-sdk/tests/logs/test_global_provider.py index 7a249defcf..d888789898 100644 --- a/opentelemetry-sdk/tests/logs/test_global_provider.py +++ b/opentelemetry-sdk/tests/logs/test_global_provider.py @@ -20,12 +20,12 @@ from opentelemetry.sdk import _logs from opentelemetry.sdk._logs import ( - LogEmitterProvider, - get_log_emitter_provider, - set_log_emitter_provider, + LoggerProvider, + get_logger_provider, + set_logger_provider, ) from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_LOG_EMITTER_PROVIDER, + _OTEL_PYTHON_LOGGER_PROVIDER, ) @@ -34,26 +34,26 @@ def tearDown(self): reload(_logs) def check_override_not_allowed(self): - """set_log_emitter_provider should throw a warning when overridden""" - provider = get_log_emitter_provider() + """set_logger_provider should throw a warning when overridden""" + provider = get_logger_provider() with self.assertLogs(level=WARNING) as test: - set_log_emitter_provider(LogEmitterProvider()) + set_logger_provider(LoggerProvider()) self.assertEqual( test.output, [ ( "WARNING:opentelemetry.sdk._logs:Overriding of current " - "LogEmitterProvider is not allowed" + "LoggerProvider is not allowed" ) ], ) - self.assertIs(provider, get_log_emitter_provider()) + self.assertIs(provider, get_logger_provider()) def test_set_tracer_provider(self): reload(_logs) - provider = LogEmitterProvider() - set_log_emitter_provider(provider) - retrieved_provider = get_log_emitter_provider() + provider = LoggerProvider() + set_logger_provider(provider) + retrieved_provider = get_logger_provider() self.assertEqual(provider, retrieved_provider) def test_tracer_provider_override_warning(self): @@ -62,14 +62,14 @@ def test_tracer_provider_override_warning(self): @patch.dict( "os.environ", - {_OTEL_PYTHON_LOG_EMITTER_PROVIDER: "sdk_log_emitter_provider"}, + {_OTEL_PYTHON_LOGGER_PROVIDER: "sdk_logger_provider"}, ) - def test_sdk_log_emitter_provider(self): + def test_sdk_logger_provider(self): reload(_logs) self.check_override_not_allowed() - @patch.dict("os.environ", {_OTEL_PYTHON_LOG_EMITTER_PROVIDER: "unknown"}) - def test_unknown_log_emitter_provider(self): + @patch.dict("os.environ", {_OTEL_PYTHON_LOGGER_PROVIDER: "unknown"}) + def test_unknown_logger_provider(self): reload(_logs) with self.assertRaises(Exception): - get_log_emitter_provider() + get_logger_provider() diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 19fa891dea..771179c3d2 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -16,32 +16,27 @@ from unittest.mock import Mock from opentelemetry.sdk import trace -from opentelemetry.sdk._logs import ( - LogEmitterProvider, - LoggingHandler, - get_log_emitter, -) +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.sdk._logs import get_logger as sdk_get_logger from opentelemetry.sdk._logs.severity import SeverityNumber from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import INVALID_SPAN_CONTEXT -def get_logger(level=logging.NOTSET, log_emitter_provider=None): +def get_logger(level=logging.NOTSET, logger_provider=None): logger = logging.getLogger(__name__) - handler = LoggingHandler( - level=level, log_emitter_provider=log_emitter_provider - ) + handler = LoggingHandler(level=level, logger_provider=logger_provider) logger.addHandler(handler) return logger class TestLoggingHandler(unittest.TestCase): def test_handler_default_log_level(self): - emitter_provider_mock = Mock(spec=LogEmitterProvider) - emitter_mock = get_log_emitter( - __name__, log_emitter_provider=emitter_provider_mock + emitter_provider_mock = Mock(spec=LoggerProvider) + emitter_mock = sdk_get_logger( + __name__, logger_provider=emitter_provider_mock ) - logger = get_logger(log_emitter_provider=emitter_provider_mock) + logger = get_logger(logger_provider=emitter_provider_mock) # Make sure debug messages are ignored by default logger.debug("Debug message") self.assertEqual(emitter_mock.emit.call_count, 0) @@ -50,12 +45,12 @@ def test_handler_default_log_level(self): self.assertEqual(emitter_mock.emit.call_count, 1) def test_handler_custom_log_level(self): - emitter_provider_mock = Mock(spec=LogEmitterProvider) - emitter_mock = get_log_emitter( - __name__, log_emitter_provider=emitter_provider_mock + emitter_provider_mock = Mock(spec=LoggerProvider) + emitter_mock = sdk_get_logger( + __name__, logger_provider=emitter_provider_mock ) logger = get_logger( - level=logging.ERROR, log_emitter_provider=emitter_provider_mock + level=logging.ERROR, logger_provider=emitter_provider_mock ) logger.warning("Warning message test custom log level") # Make sure any log with level < ERROR is ignored @@ -65,11 +60,11 @@ def test_handler_custom_log_level(self): self.assertEqual(emitter_mock.emit.call_count, 2) def test_log_record_no_span_context(self): - emitter_provider_mock = Mock(spec=LogEmitterProvider) - emitter_mock = get_log_emitter( - __name__, log_emitter_provider=emitter_provider_mock + emitter_provider_mock = Mock(spec=LoggerProvider) + emitter_mock = sdk_get_logger( + __name__, logger_provider=emitter_provider_mock ) - logger = get_logger(log_emitter_provider=emitter_provider_mock) + logger = get_logger(logger_provider=emitter_provider_mock) # Assert emit gets called for warning message logger.warning("Warning message") args, _ = emitter_mock.emit.call_args_list[0] @@ -84,11 +79,11 @@ def test_log_record_no_span_context(self): def test_log_record_user_attributes(self): """Attributes can be injected into logs by adding them to the LogRecord""" - emitter_provider_mock = Mock(spec=LogEmitterProvider) - emitter_mock = get_log_emitter( - __name__, log_emitter_provider=emitter_provider_mock + emitter_provider_mock = Mock(spec=LoggerProvider) + emitter_mock = sdk_get_logger( + __name__, logger_provider=emitter_provider_mock ) - logger = get_logger(log_emitter_provider=emitter_provider_mock) + logger = get_logger(logger_provider=emitter_provider_mock) # Assert emit gets called for warning message logger.warning("Warning message", extra={"http.status_code": 200}) args, _ = emitter_mock.emit.call_args_list[0] @@ -99,11 +94,11 @@ def test_log_record_user_attributes(self): def test_log_record_exception(self): """Exception information will be included in attributes""" - emitter_provider_mock = Mock(spec=LogEmitterProvider) - emitter_mock = get_log_emitter( - __name__, log_emitter_provider=emitter_provider_mock + emitter_provider_mock = Mock(spec=LoggerProvider) + emitter_mock = sdk_get_logger( + __name__, logger_provider=emitter_provider_mock ) - logger = get_logger(log_emitter_provider=emitter_provider_mock) + logger = get_logger(logger_provider=emitter_provider_mock) try: raise ZeroDivisionError("division by zero") except ZeroDivisionError: @@ -132,11 +127,11 @@ def test_log_record_exception(self): def test_log_exc_info_false(self): """Exception information will be included in attributes""" - emitter_provider_mock = Mock(spec=LogEmitterProvider) - emitter_mock = get_log_emitter( - __name__, log_emitter_provider=emitter_provider_mock + emitter_provider_mock = Mock(spec=LoggerProvider) + emitter_mock = sdk_get_logger( + __name__, logger_provider=emitter_provider_mock ) - logger = get_logger(log_emitter_provider=emitter_provider_mock) + logger = get_logger(logger_provider=emitter_provider_mock) try: raise ZeroDivisionError("division by zero") except ZeroDivisionError: @@ -155,11 +150,11 @@ def test_log_exc_info_false(self): ) def test_log_record_trace_correlation(self): - emitter_provider_mock = Mock(spec=LogEmitterProvider) - emitter_mock = get_log_emitter( - __name__, log_emitter_provider=emitter_provider_mock + emitter_provider_mock = Mock(spec=LoggerProvider) + emitter_mock = sdk_get_logger( + __name__, logger_provider=emitter_provider_mock ) - logger = get_logger(log_emitter_provider=emitter_provider_mock) + logger = get_logger(logger_provider=emitter_provider_mock) tracer = trace.TracerProvider().get_tracer(__name__) with tracer.start_as_current_span("test") as span: diff --git a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py index c87b8ba409..004bc296be 100644 --- a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py +++ b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py @@ -22,17 +22,17 @@ from unittest.mock import Mock from opentelemetry.sdk._logs import ( - ConcurrentMultiLogProcessor, - LogEmitterProvider, + ConcurrentMultiLogRecordProcessor, + LoggerProvider, LoggingHandler, - LogProcessor, LogRecord, - SynchronousMultiLogProcessor, + LogRecordProcessor, + SynchronousMultiLogRecordProcessor, ) from opentelemetry.sdk._logs.severity import SeverityNumber -class AnotherLogProcessor(LogProcessor): +class AnotherLogRecordProcessor(LogRecordProcessor): def __init__(self, exporter, logs_list): self._exporter = exporter self._log_list = logs_list @@ -54,15 +54,15 @@ def force_flush(self, timeout_millis=30000): return True -class TestLogProcessor(unittest.TestCase): - def test_log_processor(self): - provider = LogEmitterProvider() - handler = LoggingHandler(log_emitter_provider=provider) +class TestLogRecordProcessor(unittest.TestCase): + def test_log_record_processor(self): + provider = LoggerProvider() + handler = LoggingHandler(logger_provider=provider) logs_list_1 = [] - processor1 = AnotherLogProcessor(Mock(), logs_list_1) + processor1 = AnotherLogRecordProcessor(Mock(), logs_list_1) logs_list_2 = [] - processor2 = AnotherLogProcessor(Mock(), logs_list_2) + processor2 = AnotherLogRecordProcessor(Mock(), logs_list_2) logger = logging.getLogger("test.span.processor") logger.addHandler(handler) @@ -74,7 +74,7 @@ def test_log_processor(self): self.assertEqual(len(logs_list_2), 0) # Add one processor - provider.add_log_processor(processor1) + provider.add_log_record_processor(processor1) logger.warning("Brace yourself") logger.error("Some error message") @@ -85,7 +85,7 @@ def test_log_processor(self): self.assertEqual(logs_list_1, expected_list_1) # Add another processor - provider.add_log_processor(processor2) + provider.add_log_record_processor(processor2) logger.critical("Something disastrous") expected_list_1.append(("Something disastrous", "CRITICAL")) @@ -95,9 +95,9 @@ def test_log_processor(self): self.assertEqual(logs_list_2, expected_list_2) -class MultiLogProcessorTestBase(ABC): +class MultiLogRecordProcessorTestBase(ABC): @abstractmethod - def _get_multi_log_processor(self): + def _get_multi_log_record_processor(self): pass def make_record(self): @@ -109,85 +109,85 @@ def make_record(self): ) def test_on_emit(self): - multi_log_processor = self._get_multi_log_processor() - mocks = [Mock(spec=LogProcessor) for _ in range(5)] + multi_log_record_processor = self._get_multi_log_record_processor() + mocks = [Mock(spec=LogRecordProcessor) for _ in range(5)] for mock in mocks: - multi_log_processor.add_log_processor(mock) + multi_log_record_processor.add_log_record_processor(mock) record = self.make_record() - multi_log_processor.emit(record) + multi_log_record_processor.emit(record) for mock in mocks: mock.emit.assert_called_with(record) - multi_log_processor.shutdown() + multi_log_record_processor.shutdown() def test_on_shutdown(self): - multi_log_processor = self._get_multi_log_processor() - mocks = [Mock(spec=LogProcessor) for _ in range(5)] + multi_log_record_processor = self._get_multi_log_record_processor() + mocks = [Mock(spec=LogRecordProcessor) for _ in range(5)] for mock in mocks: - multi_log_processor.add_log_processor(mock) - multi_log_processor.shutdown() + multi_log_record_processor.add_log_record_processor(mock) + multi_log_record_processor.shutdown() for mock in mocks: mock.shutdown.assert_called_once_with() def test_on_force_flush(self): - multi_log_processor = self._get_multi_log_processor() - mocks = [Mock(spec=LogProcessor) for _ in range(5)] + multi_log_record_processor = self._get_multi_log_record_processor() + mocks = [Mock(spec=LogRecordProcessor) for _ in range(5)] for mock in mocks: - multi_log_processor.add_log_processor(mock) - ret_value = multi_log_processor.force_flush(100) + multi_log_record_processor.add_log_record_processor(mock) + ret_value = multi_log_record_processor.force_flush(100) self.assertTrue(ret_value) for mock_processor in mocks: self.assertEqual(1, mock_processor.force_flush.call_count) -class TestSynchronousMultiLogProcessor( - MultiLogProcessorTestBase, unittest.TestCase +class TestSynchronousMultiLogRecordProcessor( + MultiLogRecordProcessorTestBase, unittest.TestCase ): - def _get_multi_log_processor(self): - return SynchronousMultiLogProcessor() + def _get_multi_log_record_processor(self): + return SynchronousMultiLogRecordProcessor() def test_force_flush_delayed(self): - multi_log_processor = SynchronousMultiLogProcessor() + multi_log_record_processor = SynchronousMultiLogRecordProcessor() def delay(_): time.sleep(0.09) - mock_processor1 = Mock(spec=LogProcessor) + mock_processor1 = Mock(spec=LogRecordProcessor) mock_processor1.force_flush = Mock(side_effect=delay) - multi_log_processor.add_log_processor(mock_processor1) - mock_processor2 = Mock(spec=LogProcessor) - multi_log_processor.add_log_processor(mock_processor2) + multi_log_record_processor.add_log_record_processor(mock_processor1) + mock_processor2 = Mock(spec=LogRecordProcessor) + multi_log_record_processor.add_log_record_processor(mock_processor2) - ret_value = multi_log_processor.force_flush(50) + ret_value = multi_log_record_processor.force_flush(50) self.assertFalse(ret_value) self.assertEqual(mock_processor1.force_flush.call_count, 1) self.assertEqual(mock_processor2.force_flush.call_count, 0) -class TestConcurrentMultiLogProcessor( - MultiLogProcessorTestBase, unittest.TestCase +class TestConcurrentMultiLogRecordProcessor( + MultiLogRecordProcessorTestBase, unittest.TestCase ): - def _get_multi_log_processor(self): - return ConcurrentMultiLogProcessor() + def _get_multi_log_record_processor(self): + return ConcurrentMultiLogRecordProcessor() def test_force_flush_delayed(self): - multi_log_processor = ConcurrentMultiLogProcessor() + multi_log_record_processor = ConcurrentMultiLogRecordProcessor() wait_event = threading.Event() def delay(_): wait_event.wait() - mock1 = Mock(spec=LogProcessor) + mock1 = Mock(spec=LogRecordProcessor) mock1.force_flush = Mock(side_effect=delay) - mocks = [Mock(LogProcessor) for _ in range(5)] + mocks = [Mock(LogRecordProcessor) for _ in range(5)] mocks = [mock1] + mocks for mock_processor in mocks: - multi_log_processor.add_log_processor(mock_processor) + multi_log_record_processor.add_log_record_processor(mock_processor) - ret_value = multi_log_processor.force_flush(50) + ret_value = multi_log_record_processor.force_flush(50) wait_event.set() self.assertFalse(ret_value) for mock in mocks: self.assertEqual(1, mock.force_flush.call_count) - multi_log_processor.shutdown() + multi_log_record_processor.shutdown() diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 4aae8aa53b..cf1b8253dd 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -61,16 +61,16 @@ def add_span_processor(self, processor): self.processor = processor -class DummyLogEmitterProvider: +class DummyLoggerProvider: def __init__(self, resource=None): self.resource = resource - self.processor = DummyLogProcessor(DummyOTLPLogExporter()) + self.processor = DummyLogRecordProcessor(DummyOTLPLogExporter()) - def add_log_processor(self, processor): + def add_log_record_processor(self, processor): self.processor = processor - def get_log_emitter(self, name, *args, **kwargs): - return DummyLogEmitter(name, self.resource, self.processor) + def get_logger(self, name, *args, **kwargs): + return DummyLogger(name, self.resource, self.processor) def force_flush(self, *args, **kwargs): pass @@ -80,7 +80,7 @@ class DummyMeterProvider(MeterProvider): pass -class DummyLogEmitter: +class DummyLogger: def __init__(self, name, resource, processor): self.name = name self.resource = resource @@ -90,7 +90,7 @@ def emit(self, record): self.processor.emit(record) -class DummyLogProcessor: +class DummyLogRecordProcessor: def __init__(self, exporter): self.exporter = exporter @@ -274,15 +274,15 @@ def test_trace_init_custom_id_generator(self, mock_iter_entry_points): class TestLoggingInit(TestCase): def setUp(self): self.processor_patch = patch( - "opentelemetry.sdk._configuration.BatchLogProcessor", - DummyLogProcessor, + "opentelemetry.sdk._configuration.BatchLogRecordProcessor", + DummyLogRecordProcessor, ) self.provider_patch = patch( - "opentelemetry.sdk._configuration.LogEmitterProvider", - DummyLogEmitterProvider, + "opentelemetry.sdk._configuration.LoggerProvider", + DummyLoggerProvider, ) self.set_provider_patch = patch( - "opentelemetry.sdk._configuration.set_log_emitter_provider" + "opentelemetry.sdk._configuration.set_logger_provider" ) self.processor_mock = self.processor_patch.start() @@ -304,7 +304,7 @@ def test_logging_init_empty(self): _init_logging({}, "auto-version") self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] - self.assertIsInstance(provider, DummyLogEmitterProvider) + self.assertIsInstance(provider, DummyLoggerProvider) self.assertIsInstance(provider.resource, Resource) self.assertEqual( provider.resource.attributes.get("telemetry.auto.version"), @@ -319,13 +319,13 @@ def test_logging_init_exporter(self): _init_logging({"otlp": DummyOTLPLogExporter}) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] - self.assertIsInstance(provider, DummyLogEmitterProvider) + self.assertIsInstance(provider, DummyLoggerProvider) self.assertIsInstance(provider.resource, Resource) self.assertEqual( provider.resource.attributes.get("service.name"), "otlp-service", ) - self.assertIsInstance(provider.processor, DummyLogProcessor) + self.assertIsInstance(provider.processor, DummyLogRecordProcessor) self.assertIsInstance( provider.processor.exporter, DummyOTLPLogExporter ) From c338eb17698bc0ea8b93afddbc5982bb3e122d85 Mon Sep 17 00:00:00 2001 From: Rangel Reale Date: Wed, 26 Oct 2022 18:41:42 -0300 Subject: [PATCH 1332/1517] Add OTLP/HTTP metric exporter (#2891) --- CHANGELOG.md | 2 + .../proto/http/metric_exporter/__init__.py | 449 ++++++++++++++++++ .../tests/metrics/__init__.py | 0 .../metrics/test_otlp_metrics_exporter.py | 271 +++++++++++ .../sdk/environment_variables.py | 45 ++ 5 files changed, 767 insertions(+) create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 53cb313e4b..26ed7c10d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2959](https://github.com/open-telemetry/opentelemetry-python/pull/2959)) - Add http-metric instrument names to semantic conventions ([#2976](https://github.com/open-telemetry/opentelemetry-python/pull/2976)) +- [exporter/opentelemetry-exporter-otlp-proto-http] Add OTLPMetricExporter + ([#2891](https://github.com/open-telemetry/opentelemetry-python/pull/2891)) ## [1.13.0-0.34b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.13.0) - 2022-09-26 diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py new file mode 100644 index 0000000000..1423c31238 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -0,0 +1,449 @@ +# Copyright The OpenTelemetry Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import gzip +import logging +import zlib +from os import environ +from typing import Dict, Optional, Sequence, Any, Callable, List, Mapping +from io import BytesIO +from time import sleep + +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.sdk.metrics._internal.aggregation import Aggregation +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue, + ArrayValue, + KeyValue, + KeyValueList, +) +from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope +from opentelemetry.proto.resource.v1.resource_pb2 import Resource +from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_HEADERS, + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, +) +from opentelemetry.sdk.metrics import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + Gauge, + Histogram as HistogramType, + MetricExporter, + MetricExportResult, + MetricsData, + Sum, +) +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.util.re import parse_headers + +import requests +from backoff import expo + +_logger = logging.getLogger(__name__) + + +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_METRICS_EXPORT_PATH = "v1/metrics" +DEFAULT_TIMEOUT = 10 # in seconds + + +class OTLPMetricExporter(MetricExporter): + + _MAX_RETRY_TIMEOUT = 64 + + def __init__( + self, + endpoint: Optional[str] = None, + certificate_file: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + timeout: Optional[int] = None, + compression: Optional[Compression] = None, + session: Optional[requests.Session] = None, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[type, Aggregation] = None, + ): + self._endpoint = endpoint or environ.get( + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + _append_metrics_path( + environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) + ), + ) + self._certificate_file = certificate_file or environ.get( + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), + ) + headers_string = environ.get( + OTEL_EXPORTER_OTLP_METRICS_HEADERS, + environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), + ) + self._headers = headers or parse_headers(headers_string) + self._timeout = timeout or int( + environ.get( + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), + ) + ) + self._compression = compression or _compression_from_env() + self._session = session or requests.Session() + self._session.headers.update(self._headers) + self._session.headers.update( + {"Content-Type": "application/x-protobuf"} + ) + if self._compression is not Compression.NoCompression: + self._session.headers.update( + {"Content-Encoding": self._compression.value} + ) + + instrument_class_temporality = {} + if ( + environ.get( + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + "CUMULATIVE", + ) + .upper() + .strip() + == "DELTA" + ): + instrument_class_temporality = { + Counter: AggregationTemporality.DELTA, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.DELTA, + ObservableCounter: AggregationTemporality.DELTA, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + else: + instrument_class_temporality = { + Counter: AggregationTemporality.CUMULATIVE, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.CUMULATIVE, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + instrument_class_temporality.update(preferred_temporality or {}) + + MetricExporter.__init__( + self, + preferred_temporality=instrument_class_temporality, + preferred_aggregation=preferred_aggregation, + ) + + def _export(self, serialized_data: str): + data = serialized_data + if self._compression == Compression.Gzip: + gzip_data = BytesIO() + with gzip.GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: + gzip_stream.write(serialized_data) + data = gzip_data.getvalue() + elif self._compression == Compression.Deflate: + data = zlib.compress(bytes(serialized_data)) + + return self._session.post( + url=self._endpoint, + data=data, + verify=self._certificate_file, + timeout=self._timeout, + ) + + @staticmethod + def _retryable(resp: requests.Response) -> bool: + if resp.status_code == 408: + return True + if resp.status_code >= 500 and resp.status_code <= 599: + return True + return False + + def _translate_data( + self, data: MetricsData + ) -> ExportMetricsServiceRequest: + + resource_metrics_dict = {} + + for resource_metrics in data.resource_metrics: + + resource = resource_metrics.resource + + # It is safe to assume that each entry in data.resource_metrics is + # associated with an unique resource. + scope_metrics_dict = {} + + resource_metrics_dict[resource] = scope_metrics_dict + + for scope_metrics in resource_metrics.scope_metrics: + + instrumentation_scope = scope_metrics.scope + + # The SDK groups metrics in instrumentation scopes already so + # there is no need to check for existing instrumentation scopes + # here. + pb2_scope_metrics = pb2.ScopeMetrics( + scope=InstrumentationScope( + name=instrumentation_scope.name, + version=instrumentation_scope.version, + ) + ) + + scope_metrics_dict[instrumentation_scope] = pb2_scope_metrics + + for metric in scope_metrics.metrics: + pb2_metric = pb2.Metric( + name=metric.name, + description=metric.description, + unit=metric.unit, + ) + + if isinstance(metric.data, Gauge): + for data_point in metric.data.data_points: + pt = pb2.NumberDataPoint( + attributes=self._translate_attributes( + data_point.attributes + ), + time_unix_nano=data_point.time_unix_nano, + ) + if isinstance(data_point.value, int): + pt.as_int = data_point.value + else: + pt.as_double = data_point.value + pb2_metric.gauge.data_points.append(pt) + + elif isinstance(metric.data, HistogramType): + for data_point in metric.data.data_points: + pt = pb2.HistogramDataPoint( + attributes=self._translate_attributes( + data_point.attributes + ), + time_unix_nano=data_point.time_unix_nano, + start_time_unix_nano=( + data_point.start_time_unix_nano + ), + count=data_point.count, + sum=data_point.sum, + bucket_counts=data_point.bucket_counts, + explicit_bounds=data_point.explicit_bounds, + max=data_point.max, + min=data_point.min, + ) + pb2_metric.histogram.aggregation_temporality = ( + metric.data.aggregation_temporality + ) + pb2_metric.histogram.data_points.append(pt) + + elif isinstance(metric.data, Sum): + for data_point in metric.data.data_points: + pt = pb2.NumberDataPoint( + attributes=self._translate_attributes( + data_point.attributes + ), + start_time_unix_nano=( + data_point.start_time_unix_nano + ), + time_unix_nano=data_point.time_unix_nano, + ) + if isinstance(data_point.value, int): + pt.as_int = data_point.value + else: + pt.as_double = data_point.value + # note that because sum is a message type, the + # fields must be set individually rather than + # instantiating a pb2.Sum and setting it once + pb2_metric.sum.aggregation_temporality = ( + metric.data.aggregation_temporality + ) + pb2_metric.sum.is_monotonic = ( + metric.data.is_monotonic + ) + pb2_metric.sum.data_points.append(pt) + else: + _logger.warn( + "unsupported datapoint type %s", metric.point + ) + continue + + pb2_scope_metrics.metrics.append(pb2_metric) + + return ExportMetricsServiceRequest( + resource_metrics=get_resource_data( + resource_metrics_dict, + pb2.ResourceMetrics, + "metrics", + ) + ) + + def _translate_attributes(self, attributes) -> Sequence[KeyValue]: + output = [] + if attributes: + + for key, value in attributes.items(): + try: + output.append(_translate_key_values(key, value)) + except Exception as error: # pylint: disable=broad-except + _logger.exception(error) + return output + + def export( + self, + metrics_data: MetricsData, + timeout_millis: float = 10_000, + **kwargs, + ) -> MetricExportResult: + serialized_data = self._translate_data(metrics_data) + for delay in expo(max_value=self._MAX_RETRY_TIMEOUT): + + if delay == self._MAX_RETRY_TIMEOUT: + return MetricExportResult.FAILURE + + resp = self._export(serialized_data.SerializeToString()) + # pylint: disable=no-else-return + if resp.status_code in (200, 202): + return MetricExportResult.SUCCESS + elif self._retryable(resp): + _logger.warning( + "Transient error %s encountered while exporting metric batch, retrying in %ss.", + resp.reason, + delay, + ) + sleep(delay) + continue + else: + _logger.error( + "Failed to export batch code: %s, reason: %s", + resp.status_code, + resp.text, + ) + return MetricExportResult.FAILURE + return MetricExportResult.FAILURE + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + pass + + @property + def _exporting(self) -> str: + return "metrics" + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return True + + +def _translate_value(value: Any) -> KeyValue: + + if isinstance(value, bool): + any_value = AnyValue(bool_value=value) + + elif isinstance(value, str): + any_value = AnyValue(string_value=value) + + elif isinstance(value, int): + any_value = AnyValue(int_value=value) + + elif isinstance(value, float): + any_value = AnyValue(double_value=value) + + elif isinstance(value, Sequence): + any_value = AnyValue( + array_value=ArrayValue(values=[_translate_value(v) for v in value]) + ) + + elif isinstance(value, Mapping): + any_value = AnyValue( + kvlist_value=KeyValueList( + values=[ + _translate_key_values(str(k), v) for k, v in value.items() + ] + ) + ) + + else: + raise Exception(f"Invalid type {type(value)} of value {value}") + + return any_value + + +def _translate_key_values(key: str, value: Any) -> KeyValue: + return KeyValue(key=key, value=_translate_value(value)) + + +def get_resource_data( + sdk_resource_scope_data: Dict[SDKResource, Any], # ResourceDataT? + resource_class: Callable[..., Resource], + name: str, +) -> List[Resource]: + + resource_data = [] + + for ( + sdk_resource, + scope_data, + ) in sdk_resource_scope_data.items(): + + collector_resource = Resource() + + for key, value in sdk_resource.attributes.items(): + + try: + # pylint: disable=no-member + collector_resource.attributes.append( + _translate_key_values(key, value) + ) + except Exception as error: # pylint: disable=broad-except + _logger.exception(error) + + resource_data.append( + resource_class( + **{ + "resource": collector_resource, + "scope_{}".format(name): scope_data.values(), + } + ) + ) + + return resource_data + + +def _compression_from_env() -> Compression: + compression = ( + environ.get( + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), + ) + .lower() + .strip() + ) + return Compression(compression) + + +def _append_metrics_path(endpoint: str) -> str: + if endpoint.endswith("/"): + return endpoint + DEFAULT_METRICS_EXPORT_PATH + return endpoint + f"/{DEFAULT_METRICS_EXPORT_PATH}" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py new file mode 100644 index 0000000000..e4665a47fc --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -0,0 +1,271 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import patch + +import requests + +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_METRICS_EXPORT_PATH, + DEFAULT_TIMEOUT, + OTLPMetricExporter, +) +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + OTEL_EXPORTER_OTLP_METRICS_HEADERS, + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + OTEL_EXPORTER_OTLP_TIMEOUT, +) +from opentelemetry.sdk.metrics.export import ( + MetricExportResult, + MetricsData, + ResourceMetrics, + ScopeMetrics, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationScope as SDKInstrumentationScope, +) +from opentelemetry.test.metrictestutil import _generate_sum + +OS_ENV_ENDPOINT = "os.env.base" +OS_ENV_CERTIFICATE = "os/env/base.crt" +OS_ENV_HEADERS = "envHeader1=val1,envHeader2=val2" +OS_ENV_TIMEOUT = "30" + + +# pylint: disable=protected-access +class TestOTLPMetricExporter(unittest.TestCase): + def setUp(self): + + self.metrics = { + "sum_int": MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[_generate_sum("sum_int", 33)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ), + } + + def test_constructor_default(self): + + exporter = OTLPMetricExporter() + + self.assertEqual( + exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_METRICS_EXPORT_PATH + ) + self.assertEqual(exporter._certificate_file, True) + self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) + self.assertIs(exporter._compression, DEFAULT_COMPRESSION) + self.assertEqual(exporter._headers, {}) + self.assertIsInstance(exporter._session, requests.Session) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: "metrics/certificate.env", + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: Compression.Deflate.value, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "https://metrics.endpoint.env", + OTEL_EXPORTER_OTLP_METRICS_HEADERS: "metricsEnv1=val1,metricsEnv2=val2,metricEnv3===val3==", + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "40", + }, + ) + def test_exporter_metrics_env_take_priority(self): + exporter = OTLPMetricExporter() + + self.assertEqual(exporter._endpoint, "https://metrics.endpoint.env") + self.assertEqual(exporter._certificate_file, "metrics/certificate.env") + self.assertEqual(exporter._timeout, 40) + self.assertIs(exporter._compression, Compression.Deflate) + self.assertEqual( + exporter._headers, + { + "metricsenv1": "val1", + "metricsenv2": "val2", + "metricenv3": "==val3==", + }, + ) + self.assertIsInstance(exporter._session, requests.Session) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "https://metrics.endpoint.env", + OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, + }, + ) + def test_exporter_constructor_take_priority(self): + exporter = OTLPMetricExporter( + endpoint="example.com/1234", + certificate_file="path/to/service.crt", + headers={"testHeader1": "value1", "testHeader2": "value2"}, + timeout=20, + compression=Compression.NoCompression, + session=requests.Session(), + ) + + self.assertEqual(exporter._endpoint, "example.com/1234") + self.assertEqual(exporter._certificate_file, "path/to/service.crt") + self.assertEqual(exporter._timeout, 20) + self.assertIs(exporter._compression, Compression.NoCompression) + self.assertEqual( + exporter._headers, + {"testHeader1": "value1", "testHeader2": "value2"}, + ) + self.assertIsInstance(exporter._session, requests.Session) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, + }, + ) + def test_exporter_env(self): + + exporter = OTLPMetricExporter() + + self.assertEqual(exporter._certificate_file, OS_ENV_CERTIFICATE) + self.assertEqual(exporter._timeout, int(OS_ENV_TIMEOUT)) + self.assertIs(exporter._compression, Compression.Gzip) + self.assertEqual( + exporter._headers, {"envheader1": "val1", "envheader2": "val2"} + ) + + @patch.dict( + "os.environ", + {OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT}, + ) + def test_exporter_env_endpoint_without_slash(self): + + exporter = OTLPMetricExporter() + + self.assertEqual( + exporter._endpoint, + OS_ENV_ENDPOINT + f"/{DEFAULT_METRICS_EXPORT_PATH}", + ) + + @patch.dict( + "os.environ", + {OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT + "/"}, + ) + def test_exporter_env_endpoint_with_slash(self): + + exporter = OTLPMetricExporter() + + self.assertEqual( + exporter._endpoint, + OS_ENV_ENDPOINT + f"/{DEFAULT_METRICS_EXPORT_PATH}", + ) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_HEADERS: "envHeader1=val1,envHeader2=val2,missingValue" + }, + ) + def test_headers_parse_from_env(self): + + with self.assertLogs(level="WARNING") as cm: + _ = OTLPMetricExporter() + + self.assertEqual( + cm.records[0].message, + "Header doesn't match the format: missingValue.", + ) + + @patch.object(requests.Session, "post") + def test_success(self, mock_post): + resp = requests.models.Response() + resp.status_code = 200 + mock_post.return_value = resp + + exporter = OTLPMetricExporter() + + self.assertEqual( + exporter.export(self.metrics["sum_int"]), + MetricExportResult.SUCCESS, + ) + + @patch.object(requests.Session, "post") + def test_failure(self, mock_post): + resp = requests.models.Response() + resp.status_code = 401 + mock_post.return_value = resp + + exporter = OTLPMetricExporter() + + self.assertEqual( + exporter.export(self.metrics["sum_int"]), + MetricExportResult.FAILURE, + ) + + @patch.object(requests.Session, "post") + def test_serialization(self, mock_post): + + resp = requests.models.Response() + resp.status_code = 200 + mock_post.return_value = resp + + exporter = OTLPMetricExporter() + + self.assertEqual( + exporter.export(self.metrics["sum_int"]), + MetricExportResult.SUCCESS, + ) + + serialized_data = exporter._translate_data(self.metrics["sum_int"]) + mock_post.assert_called_once_with( + url=exporter._endpoint, + data=serialized_data.SerializeToString(), + verify=exporter._certificate_file, + timeout=exporter._timeout, + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index d469c24478..3f31b3b71c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -377,6 +377,51 @@ Default: False """ +OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT + +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` target to which the metric exporter is going to send spans. +The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path. +A scheme of https indicates a secure connection and takes precedence over this configuration setting. +""" + +OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE = ( + "OTEL_EXPORTER_OTLP_METRRICS_CERTIFICATE" +) +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE + +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE` stores the path to the certificate file for +TLS credentials of gRPC client for traces. Should only be used for a secure connection for tracing. +""" + +OTEL_EXPORTER_OTLP_METRICS_HEADERS = "OTEL_EXPORTER_OTLP_METRICS_HEADERS" +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_HEADERS + +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_HEADERS` contains the key-value pairs to be used as headers for metrics +associated with gRPC or HTTP requests. +""" + +OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT" +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_TIMEOUT + +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` is the maximum time the OTLP exporter will +wait for each batch export for metrics. +""" + +OTEL_EXPORTER_OTLP_METRICS_COMPRESSION = ( + "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION" +) +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_COMPRESSION + +Same as :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` but only for the metric +exporter. If both are present, this takes higher precedence. +""" + OTEL_EXPORTER_JAEGER_CERTIFICATE = "OTEL_EXPORTER_JAEGER_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_JAEGER_CERTIFICATE From 35ba2571a633210d3b21701bc6cdc11d51bad0a8 Mon Sep 17 00:00:00 2001 From: Jeremy Voss Date: Fri, 28 Oct 2022 23:46:52 -0700 Subject: [PATCH 1333/1517] Enabled custom sampler configuration via env vars (#2972) --- CHANGELOG.md | 2 + .../sdk/_configuration/__init__.py | 27 +- .../src/opentelemetry/sdk/trace/sampling.py | 91 ++++-- .../src/opentelemetry/sdk/util/__init__.py | 30 +- opentelemetry-sdk/tests/test_configurator.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 266 +++++++++++++++++- 6 files changed, 358 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ed7c10d0..2358664bdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0...HEAD) +- Enabled custom samplers via entry points + ([#2972](https://github.com/open-telemetry/opentelemetry-python/pull/2972)) - Update log symbol names ([#2943](https://github.com/open-telemetry/opentelemetry-python/pull/2943)) - Update explicit histogram bucket boundaries diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index fa057f785c..c2280e1b27 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -23,7 +23,6 @@ from os import environ from typing import Dict, Optional, Sequence, Tuple, Type -from pkg_resources import iter_entry_points from typing_extensions import Literal from opentelemetry.environment_variables import ( @@ -55,6 +54,7 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator +from opentelemetry.sdk.util import _import_config_components from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.trace import set_tracer_provider @@ -226,26 +226,6 @@ def _init_logging( logging.getLogger().addHandler(handler) -def _import_config_components( - selected_components, entry_point_name -) -> Sequence[Tuple[str, object]]: - component_entry_points = { - ep.name: ep for ep in iter_entry_points(entry_point_name) - } - component_impls = [] - for selected_component in selected_components: - entry_point = component_entry_points.get(selected_component, None) - if not entry_point: - raise RuntimeError( - f"Requested component '{selected_component}' not found in entry points for '{entry_point_name}'" - ) - - component_impl = entry_point.load() - component_impls.append((selected_component, component_impl)) - - return component_impls - - def _import_exporters( trace_exporter_names: Sequence[str], metric_exporter_names: Sequence[str], @@ -287,10 +267,9 @@ def _import_exporters( def _import_id_generator(id_generator_name: str) -> IdGenerator: - # pylint: disable=unbalanced-tuple-unpacking - [(id_generator_name, id_generator_impl)] = _import_config_components( + id_generator_name, id_generator_impl = _import_config_components( [id_generator_name.strip()], "opentelemetry_id_generator" - ) + )[0] if issubclass(id_generator_impl, IdGenerator): return id_generator_impl diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 3cdd34cfe8..38a3338b02 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -64,7 +64,7 @@ ... The tracer sampler can also be configured via environment variables ``OTEL_TRACES_SAMPLER`` and ``OTEL_TRACES_SAMPLER_ARG`` (only if applicable). -The list of known values for ``OTEL_TRACES_SAMPLER`` are: +The list of built-in values for ``OTEL_TRACES_SAMPLER`` are: * always_on - Sampler that always samples spans, regardless of the parent span's sampling decision. * always_off - Sampler that never samples spans, regardless of the parent span's sampling decision. @@ -73,8 +73,7 @@ * parentbased_always_off - Sampler that respects its parent span's sampling decision, but otherwise never samples. * parentbased_traceidratio - Sampler that respects its parent span's sampling decision, but otherwise samples probabalistically based on rate. -Sampling probability can be set with ``OTEL_TRACES_SAMPLER_ARG`` if the sampler is traceidratio or parentbased_traceidratio, when not provided rate will be set to 1.0 (maximum rate possible). - +Sampling probability can be set with ``OTEL_TRACES_SAMPLER_ARG`` if the sampler is traceidratio or parentbased_traceidratio. Rate must be in the range [0.0,1.0]. When not provided rate will be set to 1.0 (maximum rate possible). Prev example but with environment variables. Please make sure to set the env ``OTEL_TRACES_SAMPLER=traceidratio`` and ``OTEL_TRACES_SAMPLER_ARG=0.001``. @@ -97,13 +96,45 @@ # created spans will now be sampled by the TraceIdRatioBased sampler with rate 1/1000. with trace.get_tracer(__name__).start_as_current_span("Test Span"): ... + +In order to create a configurable custom sampler, create an entry point for the custom sampler factory method under the entry point group, ``opentelemetry_traces_sampler``. The custom sampler factory +method must be of type ``Callable[[str], Sampler]``, taking a single string argument and returning a Sampler object. The single input will come from the string value of the +``OTEL_TRACES_SAMPLER_ARG`` environment variable. If ``OTEL_TRACES_SAMPLER_ARG`` is not configured, the input will be an empty string. For example: + +.. code:: python + + setup( + ... + entry_points={ + ... + "opentelemetry_traces_sampler": [ + "custom_sampler_name = path.to.sampler.factory.method:CustomSamplerFactory.get_sampler" + ] + } + ) + # ... + class CustomRatioSampler(Sampler): + def __init__(rate): + # ... + # ... + class CustomSamplerFactory: + @staticmethod + get_sampler(sampler_argument): + try: + rate = float(sampler_argument) + return CustomSampler(rate) + except ValueError: # In case argument is empty string. + return CustomSampler(0.5) + +In order to configure you application with a custom sampler's entry point, set the ``OTEL_TRACES_SAMPLER`` environment variable to the key name of the entry point. For example, to configured the +above sampler, set ``OTEL_TRACES_SAMPLER=custom_sampler_name`` and ``OTEL_TRACES_SAMPLER_ARG=0.5``. """ import abc import enum import os from logging import getLogger from types import MappingProxyType -from typing import Optional, Sequence +from typing import Callable, Optional, Sequence # pylint: disable=unused-import from opentelemetry.context import Context @@ -111,6 +142,7 @@ OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) +from opentelemetry.sdk.util import _import_config_components from opentelemetry.trace import Link, SpanKind, get_current_span from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes @@ -161,6 +193,9 @@ def __init__( self.trace_state = trace_state +_OTEL_SAMPLER_ENTRY_POINT_GROUP = "opentelemetry_traces_sampler" + + class Sampler(abc.ABC): @abc.abstractmethod def should_sample( @@ -372,22 +407,37 @@ def __init__(self, rate: float): def _get_from_env_or_default() -> Sampler: - trace_sampler = os.getenv( + traces_sampler_name = os.getenv( OTEL_TRACES_SAMPLER, "parentbased_always_on" ).lower() - if trace_sampler not in _KNOWN_SAMPLERS: - _logger.warning("Couldn't recognize sampler %s.", trace_sampler) - trace_sampler = "parentbased_always_on" - - if trace_sampler in ("traceidratio", "parentbased_traceidratio"): - try: - rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG)) - except ValueError: - _logger.warning("Could not convert TRACES_SAMPLER_ARG to float.") - rate = 1.0 - return _KNOWN_SAMPLERS[trace_sampler](rate) - return _KNOWN_SAMPLERS[trace_sampler] + if traces_sampler_name in _KNOWN_SAMPLERS: + if traces_sampler_name in ("traceidratio", "parentbased_traceidratio"): + try: + rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG)) + except ValueError: + _logger.warning( + "Could not convert TRACES_SAMPLER_ARG to float." + ) + rate = 1.0 + return _KNOWN_SAMPLERS[traces_sampler_name](rate) + return _KNOWN_SAMPLERS[traces_sampler_name] + try: + traces_sampler_factory = _import_sampler_factory(traces_sampler_name) + sampler_arg = os.getenv(OTEL_TRACES_SAMPLER_ARG, "") + traces_sampler = traces_sampler_factory(sampler_arg) + if not isinstance(traces_sampler, Sampler): + message = f"Traces sampler factory, {traces_sampler_factory}, produced output, {traces_sampler}, which is not a Sampler object." + _logger.warning(message) + raise ValueError(message) + return traces_sampler + except Exception as exc: # pylint: disable=broad-except + _logger.warning( + "Using default sampler. Failed to initialize custom sampler, %s: %s", + traces_sampler_name, + exc, + ) + return _KNOWN_SAMPLERS["parentbased_always_on"] def _get_parent_trace_state(parent_context) -> Optional["TraceState"]: @@ -395,3 +445,10 @@ def _get_parent_trace_state(parent_context) -> Optional["TraceState"]: if parent_span_context is None or not parent_span_context.is_valid: return None return parent_span_context.trace_state + + +def _import_sampler_factory(sampler_name: str) -> Callable[[str], Sampler]: + _, sampler_impl = _import_config_components( + [sampler_name.strip()], _OTEL_SAMPLER_ENTRY_POINT_GROUP + )[0] + return sampler_impl diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index e1857d8e62..5210424353 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -14,11 +14,11 @@ import datetime import threading -from collections import OrderedDict, deque -from collections.abc import MutableMapping, Sequence -from typing import Optional +from collections import OrderedDict, abc, deque +from typing import List, Optional, Sequence, Tuple from deprecated import deprecated +from pkg_resources import iter_entry_points def ns_to_iso_str(nanoseconds): @@ -41,7 +41,27 @@ def get_dict_as_key(labels): ) -class BoundedList(Sequence): +def _import_config_components( + selected_components: List[str], entry_point_name: str +) -> Sequence[Tuple[str, object]]: + component_entry_points = { + ep.name: ep for ep in iter_entry_points(entry_point_name) + } + component_impls = [] + for selected_component in selected_components: + entry_point = component_entry_points.get(selected_component, None) + if not entry_point: + raise RuntimeError( + f"Requested component '{selected_component}' not found in entry points for '{entry_point_name}'" + ) + + component_impl = entry_point.load() + component_impls.append((selected_component, component_impl)) + + return component_impls + + +class BoundedList(abc.Sequence): """An append only list with a fixed max size. Calls to `append` and `extend` will drop the oldest elements if there is @@ -92,7 +112,7 @@ def from_seq(cls, maxlen, seq): @deprecated(version="1.4.0") # type: ignore -class BoundedDict(MutableMapping): +class BoundedDict(abc.MutableMapping): """An ordered dict with a fixed max capacity. Oldest elements are dropped when the dict is full and a new element is diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index cf1b8253dd..947ae623bc 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -257,7 +257,7 @@ def test_trace_init_otlp(self): @patch.dict(environ, {OTEL_PYTHON_ID_GENERATOR: "custom_id_generator"}) @patch("opentelemetry.sdk._configuration.IdGenerator", new=IdGenerator) - @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch("opentelemetry.sdk.util.iter_entry_points") def test_trace_init_custom_id_generator(self, mock_iter_entry_points): mock_iter_entry_points.configure_mock( return_value=[ diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 8b8d33faa4..3f4d0d0da1 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -21,7 +21,7 @@ from logging import ERROR, WARNING from random import randint from time import time_ns -from typing import Optional +from typing import Optional, Sequence from unittest import mock from opentelemetry import trace as trace_api @@ -39,15 +39,27 @@ OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) -from opentelemetry.sdk.trace import Resource, sampling +from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.trace.id_generator import RandomIdGenerator +from opentelemetry.sdk.trace.sampling import ( + ALWAYS_OFF, + ALWAYS_ON, + Decision, + ParentBased, + Sampler, + SamplingResult, + StaticSampler, + TraceIdRatioBased, +) from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, new_tracer, ) -from opentelemetry.trace import Status, StatusCode +from opentelemetry.trace import Link, SpanKind, Status, StatusCode +from opentelemetry.trace.span import TraceState +from opentelemetry.util.types import Attributes class TestTracer(unittest.TestCase): @@ -139,6 +151,78 @@ def test_tracer_provider_accepts_concurrent_multi_span_processor(self): ) +class CustomSampler(Sampler): + def __init__(self) -> None: + pass + + def get_description(self) -> str: + return "CustomSampler" + + def should_sample( + self, + parent_context: Optional["Context"], + trace_id: int, + name: str, + kind: SpanKind = None, + attributes: Attributes = None, + links: Sequence[Link] = None, + trace_state: TraceState = None, + ) -> "SamplingResult": + return SamplingResult( + Decision.RECORD_AND_SAMPLE, + None, + None, + ) + + +class CustomRatioSampler(TraceIdRatioBased): + def __init__(self, ratio): + self.ratio = ratio + super().__init__(ratio) + + def get_description(self) -> str: + return "CustomSampler" + + def should_sample( + self, + parent_context: Optional["Context"], + trace_id: int, + name: str, + kind: SpanKind = None, + attributes: Attributes = None, + links: Sequence[Link] = None, + trace_state: TraceState = None, + ) -> "SamplingResult": + return SamplingResult( + Decision.RECORD_AND_SAMPLE, + None, + None, + ) + + +class CustomSamplerFactory: + @staticmethod + def get_custom_sampler(unused_sampler_arg): + return CustomSampler() + + @staticmethod + def get_custom_ratio_sampler(sampler_arg): + return CustomRatioSampler(float(sampler_arg)) + + @staticmethod + def empty_get_custom_sampler(sampler_arg): + return + + +class IterEntryPoint: + def __init__(self, name, class_type): + self.name = name + self.class_type = class_type + + def load(self): + return self.class_type + + class TestTracerSampling(unittest.TestCase): def tearDown(self): reload(trace) @@ -165,12 +249,10 @@ def test_default_sampler(self): def test_default_sampler_type(self): tracer_provider = trace.TracerProvider() - self.assertIsInstance(tracer_provider.sampler, sampling.ParentBased) - # pylint: disable=protected-access - self.assertEqual(tracer_provider.sampler._root, sampling.ALWAYS_ON) + self.verify_default_sampler(tracer_provider) def test_sampler_no_sampling(self): - tracer_provider = trace.TracerProvider(sampling.ALWAYS_OFF) + tracer_provider = trace.TracerProvider(ALWAYS_OFF) tracer = tracer_provider.get_tracer(__name__) # Check that the default tracer creates no-op spans if the sampler @@ -194,10 +276,8 @@ def test_sampler_with_env(self): # pylint: disable=protected-access reload(trace) tracer_provider = trace.TracerProvider() - self.assertIsInstance(tracer_provider.sampler, sampling.StaticSampler) - self.assertEqual( - tracer_provider.sampler._decision, sampling.Decision.DROP - ) + self.assertIsInstance(tracer_provider.sampler, StaticSampler) + self.assertEqual(tracer_provider.sampler._decision, Decision.DROP) tracer = tracer_provider.get_tracer(__name__) @@ -216,9 +296,169 @@ def test_ratio_sampler_with_env(self): # pylint: disable=protected-access reload(trace) tracer_provider = trace.TracerProvider() - self.assertIsInstance(tracer_provider.sampler, sampling.ParentBased) + self.assertIsInstance(tracer_provider.sampler, ParentBased) self.assertEqual(tracer_provider.sampler._root.rate, 0.25) + @mock.patch.dict( + "os.environ", {OTEL_TRACES_SAMPLER: "non_existent_entry_point"} + ) + def test_sampler_with_env_non_existent_entry_point(self): + # pylint: disable=protected-access + reload(trace) + tracer_provider = trace.TracerProvider() + self.verify_default_sampler(tracer_provider) + + @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") + @mock.patch.dict( + "os.environ", {OTEL_TRACES_SAMPLER: "custom_sampler_factory"} + ) + def test_custom_sampler_with_env(self, mock_iter_entry_points): + mock_iter_entry_points.return_value = [ + IterEntryPoint( + "custom_sampler_factory", + CustomSamplerFactory.get_custom_sampler, + ) + ] + # pylint: disable=protected-access + reload(trace) + tracer_provider = trace.TracerProvider() + self.assertIsInstance(tracer_provider.sampler, CustomSampler) + + @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") + @mock.patch.dict( + "os.environ", {OTEL_TRACES_SAMPLER: "custom_sampler_factory"} + ) + def test_custom_sampler_with_env_bad_factory(self, mock_iter_entry_points): + mock_iter_entry_points.return_value = [ + IterEntryPoint( + "custom_sampler_factory", + CustomSamplerFactory.empty_get_custom_sampler, + ) + ] + # pylint: disable=protected-access + reload(trace) + tracer_provider = trace.TracerProvider() + self.verify_default_sampler(tracer_provider) + + @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") + @mock.patch.dict( + "os.environ", + { + OTEL_TRACES_SAMPLER: "custom_sampler_factory", + OTEL_TRACES_SAMPLER_ARG: "0.5", + }, + ) + def test_custom_sampler_with_env_unused_arg(self, mock_iter_entry_points): + mock_iter_entry_points.return_value = [ + IterEntryPoint( + "custom_sampler_factory", + CustomSamplerFactory.get_custom_sampler, + ) + ] + # pylint: disable=protected-access + reload(trace) + tracer_provider = trace.TracerProvider() + self.assertIsInstance(tracer_provider.sampler, CustomSampler) + + @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") + @mock.patch.dict( + "os.environ", + { + OTEL_TRACES_SAMPLER: "custom_ratio_sampler_factory", + OTEL_TRACES_SAMPLER_ARG: "0.5", + }, + ) + def test_custom_ratio_sampler_with_env(self, mock_iter_entry_points): + mock_iter_entry_points.return_value = [ + IterEntryPoint( + "custom_ratio_sampler_factory", + CustomSamplerFactory.get_custom_ratio_sampler, + ) + ] + # pylint: disable=protected-access + reload(trace) + tracer_provider = trace.TracerProvider() + self.assertIsInstance(tracer_provider.sampler, CustomRatioSampler) + self.assertEqual(tracer_provider.sampler.ratio, 0.5) + + @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") + @mock.patch.dict( + "os.environ", + { + OTEL_TRACES_SAMPLER: "custom_ratio_sampler_factory", + OTEL_TRACES_SAMPLER_ARG: "foobar", + }, + ) + def test_custom_ratio_sampler_with_env_bad_arg( + self, mock_iter_entry_points + ): + mock_iter_entry_points.return_value = [ + IterEntryPoint( + "custom_ratio_sampler_factory", + CustomSamplerFactory.get_custom_ratio_sampler, + ) + ] + # pylint: disable=protected-access + reload(trace) + tracer_provider = trace.TracerProvider() + self.verify_default_sampler(tracer_provider) + + @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") + @mock.patch.dict( + "os.environ", + { + OTEL_TRACES_SAMPLER: "custom_ratio_sampler_factory", + }, + ) + def test_custom_ratio_sampler_with_env_no_arg( + self, mock_iter_entry_points + ): + mock_iter_entry_points.return_value = [ + IterEntryPoint( + "custom_ratio_sampler_factory", + CustomSamplerFactory.get_custom_ratio_sampler, + ) + ] + # pylint: disable=protected-access + reload(trace) + tracer_provider = trace.TracerProvider() + self.verify_default_sampler(tracer_provider) + + @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") + @mock.patch.dict( + "os.environ", + { + OTEL_TRACES_SAMPLER: "custom_sampler_factory", + OTEL_TRACES_SAMPLER_ARG: "0.5", + }, + ) + def test_custom_ratio_sampler_with_env_multiple_entry_points( + self, mock_iter_entry_points + ): + mock_iter_entry_points.return_value = [ + IterEntryPoint( + "custom_ratio_sampler_factory", + CustomSamplerFactory.get_custom_ratio_sampler, + ), + IterEntryPoint( + "custom_sampler_factory", + CustomSamplerFactory.get_custom_sampler, + ), + IterEntryPoint( + "custom_z_sampler_factory", + CustomSamplerFactory.empty_get_custom_sampler, + ), + ] + # pylint: disable=protected-access + reload(trace) + tracer_provider = trace.TracerProvider() + self.assertIsInstance(tracer_provider.sampler, CustomSampler) + + def verify_default_sampler(self, tracer_provider): + self.assertIsInstance(tracer_provider.sampler, ParentBased) + # pylint: disable=protected-access + self.assertEqual(tracer_provider.sampler._root, ALWAYS_ON) + class TestSpanCreation(unittest.TestCase): def test_start_span_invalid_spancontext(self): @@ -712,7 +952,7 @@ def test_sampling_attributes(self): "attr-in-both": "decision-attr", } tracer_provider = trace.TracerProvider( - sampling.StaticSampler(sampling.Decision.RECORD_AND_SAMPLE) + StaticSampler(Decision.RECORD_AND_SAMPLE) ) self.tracer = tracer_provider.get_tracer(__name__) From 11f49d556d2ad4634ddcd38c367fdf95fdada727 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 31 Oct 2022 08:43:05 -0700 Subject: [PATCH 1334/1517] Temporarily disable benchmarks workflow (#2999) --- .github/workflows/test.yml | 46 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c185eb1a5e..28fbec883f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,29 +59,29 @@ jobs: run: git config --system core.longpaths true - name: run tox run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json - - name: Find and merge benchmarks - id: find_and_merge_benchmarks - run: >- - jq -s '.[0].benchmarks = ([.[].benchmarks] | add) - | if .[0].benchmarks == null then null else .[0] end' - $(find . -name '*${{ matrix.package }}*-benchmark.json') > output.json - && echo "::set-output name=json_plaintext::$(cat output.json)" - - name: Report on benchmark results - if: steps.find_and_merge_benchmarks.outputs.json_plaintext != 'null' - uses: rhysd/github-action-benchmark@v1 - with: - name: OpenTelemetry Python Benchmarks - Python ${{ env[matrix.python-version ]}} - ${{ matrix.package }} - tool: pytest - output-file-path: output.json - github-token: ${{ secrets.GITHUB_TOKEN }} - max-items-in-chart: 100 - # Alert with a commit comment on possible performance regression - alert-threshold: 200% - fail-on-alert: true - # Make a commit on `gh-pages` with benchmarks from previous step - auto-push: ${{ github.ref == 'refs/heads/main' }} - gh-pages-branch: gh-pages - benchmark-data-dir-path: benchmarks +# - name: Find and merge benchmarks +# id: find_and_merge_benchmarks +# run: >- +# jq -s '.[0].benchmarks = ([.[].benchmarks] | add) +# | if .[0].benchmarks == null then null else .[0] end' +# $(find . -name '*${{ matrix.package }}*-benchmark.json') > output.json +# && echo "::set-output name=json_plaintext::$(cat output.json)" +# - name: Report on benchmark results +# if: steps.find_and_merge_benchmarks.outputs.json_plaintext != 'null' +# uses: rhysd/github-action-benchmark@v1 +# with: +# name: OpenTelemetry Python Benchmarks - Python ${{ env[matrix.python-version ]}} - ${{ matrix.package }} +# tool: pytest +# output-file-path: output.json +# github-token: ${{ secrets.GITHUB_TOKEN }} +# max-items-in-chart: 100 +# # Alert with a commit comment on possible performance regression +# alert-threshold: 200% +# fail-on-alert: true +# # Make a commit on `gh-pages` with benchmarks from previous step +# auto-push: ${{ github.ref == 'refs/heads/main' }} +# gh-pages-branch: gh-pages +# benchmark-data-dir-path: benchmarks misc: strategy: fail-fast: false From 370af5f5f1265107c673a556204ea0acee646f09 Mon Sep 17 00:00:00 2001 From: Nick Stenning Date: Mon, 31 Oct 2022 20:01:04 +0100 Subject: [PATCH 1335/1517] Bug fix: detect and adapt to backoff package version (#2980) --- CHANGELOG.md | 4 +- .../exporter/otlp/proto/grpc/exporter.py | 17 +++++++- .../tests/logs/test_otlp_logs_exporter.py | 4 +- .../metrics/test_otlp_metrics_exporter.py | 4 +- .../tests/test_otlp_exporter_mixin.py | 2 +- .../tests/test_otlp_trace_exporter.py | 20 ++++++++- .../pyproject.toml | 4 +- .../otlp/proto/http/_log_exporter/__init__.py | 16 ++++++- .../proto/http/metric_exporter/__init__.py | 16 ++++++- .../proto/http/trace_exporter/__init__.py | 16 ++++++- .../metrics/test_otlp_metrics_exporter.py | 28 +++++++++++++ .../tests/test_proto_log_exporter.py | 26 ++++++++++++ .../tests/test_proto_span_exporter.py | 42 ++++++++++++++++++- tox.ini | 2 +- 14 files changed, 182 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2358664bdf..bfbbe3d2ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2976](https://github.com/open-telemetry/opentelemetry-python/pull/2976)) - [exporter/opentelemetry-exporter-otlp-proto-http] Add OTLPMetricExporter ([#2891](https://github.com/open-telemetry/opentelemetry-python/pull/2891)) +- Fix a bug with exporter retries for with newer versions of the backoff library + ([#2980](https://github.com/open-telemetry/opentelemetry-python/pull/2980)) ## [1.13.0-0.34b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.13.0) - 2022-09-26 @@ -90,7 +92,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2726](https://github.com/open-telemetry/opentelemetry-python/pull/2726)) - fix: frozenset object has no attribute items ([#2727](https://github.com/open-telemetry/opentelemetry-python/pull/2727)) -- fix: create suppress HTTP instrumentation key in opentelemetry context +- fix: create suppress HTTP instrumentation key in opentelemetry context ([#2729](https://github.com/open-telemetry/opentelemetry-python/pull/2729)) - Support logs SDK auto instrumentation enable/disable with env ([#2728](https://github.com/open-telemetry/opentelemetry-python/pull/2728)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 4405bcad68..aef66b79de 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -25,7 +25,7 @@ from urllib.parse import urlparse from opentelemetry.sdk.trace import ReadableSpan -from backoff import expo +import backoff from google.rpc.error_details_pb2 import RetryInfo from grpc import ( ChannelCredentials, @@ -183,6 +183,19 @@ def _get_credentials(creds, environ_key): return ssl_channel_credentials() +# Work around API change between backoff 1.x and 2.x. Since 2.0.0 the backoff +# wait generator API requires a first .send(None) before reading the backoff +# values from the generator. +_is_backoff_v2 = next(backoff.expo()) is None + + +def _expo(*args, **kwargs): + gen = backoff.expo(*args, **kwargs) + if _is_backoff_v2: + gen.send(None) + return gen + + # pylint: disable=no-member class OTLPExporterMixin( ABC, Generic[SDKDataT, ExportServiceRequestT, ExportResultT] @@ -296,7 +309,7 @@ def _export( # expo returns a generator that yields delay values which grow # exponentially. Once delay is greater than max_value, the yielded # value will remain constant. - for delay in expo(max_value=max_value): + for delay in _expo(max_value=max_value): if delay == max_value: return self._result.FAILURE diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index a9c63eaa0a..0761609083 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -251,7 +251,7 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): ) mock_method.reset_mock() - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): @@ -265,7 +265,7 @@ def test_unavailable(self, mock_sleep, mock_expo): ) mock_sleep.assert_called_with(1) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable_delay(self, mock_sleep, mock_expo): diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index 262e26ed63..0e3e26b747 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -449,7 +449,7 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): ) mock_method.reset_mock() - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): @@ -464,7 +464,7 @@ def test_unavailable(self, mock_sleep, mock_expo): ) mock_sleep.assert_called_with(1) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable_delay(self, mock_sleep, mock_expo): diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index 3f44ef228e..81a874af70 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -56,7 +56,7 @@ def test_environ_to_compression(self): with self.assertRaises(InvalidCompressionValueException): environ_to_compression("test_invalid") - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") def test_export_warning(self, mock_expo): mock_expo.configure_mock(**{"return_value": [0]}) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index a5cb4e699a..cfb286edee 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -436,7 +436,23 @@ def test_otlp_headers(self, mock_ssl_channel, mock_secure): # pylint: disable=protected-access self.assertIsNone(exporter._headers, None) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.backoff") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") + def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff): + # In backoff ~= 2.0.0 the first value yielded from expo is None. + def generate_delays(*args, **kwargs): + yield None + yield 1 + + mock_backoff.expo.configure_mock(**{"side_effect": generate_delays}) + + add_TraceServiceServicer_to_server( + TraceServiceServicerUNAVAILABLE(), self.server + ) + self.exporter.export([self.span]) + mock_sleep.assert_called_once_with(1) + + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): @@ -450,7 +466,7 @@ def test_unavailable(self, mock_sleep, mock_expo): ) mock_sleep.assert_called_with(1) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable_delay(self, mock_sleep, mock_expo): diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index edfc307c17..a89e7fa188 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -34,7 +34,9 @@ dependencies = [ ] [project.optional-dependencies] -test = [] +test = [ + "responses == 0.22.0", +] [project.entry-points.opentelemetry_traces_exporter] otlp_proto_http = "opentelemetry.exporter.otlp.proto.http.trace_exporter:OTLPSpanExporter" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index a74f849f40..580e084f4f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -20,8 +20,8 @@ from typing import Dict, Optional, Sequence from time import sleep +import backoff import requests -from backoff import expo from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, @@ -53,6 +53,18 @@ DEFAULT_LOGS_EXPORT_PATH = "v1/logs" DEFAULT_TIMEOUT = 10 # in seconds +# Work around API change between backoff 1.x and 2.x. Since 2.0.0 the backoff +# wait generator API requires a first .send(None) before reading the backoff +# values from the generator. +_is_backoff_v2 = next(backoff.expo()) is None + + +def _expo(*args, **kwargs): + gen = backoff.expo(*args, **kwargs) + if _is_backoff_v2: + gen.send(None) + return gen + class OTLPLogExporter(LogExporter): @@ -122,7 +134,7 @@ def export(self, batch: Sequence[LogData]) -> LogExportResult: serialized_data = _ProtobufEncoder.serialize(batch) - for delay in expo(max_value=self._MAX_RETRY_TIMEOUT): + for delay in _expo(max_value=self._MAX_RETRY_TIMEOUT): if delay == self._MAX_RETRY_TIMEOUT: return LogExportResult.FAILURE diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index 1423c31238..c83ca00e06 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -66,8 +66,8 @@ from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.util.re import parse_headers +import backoff import requests -from backoff import expo _logger = logging.getLogger(__name__) @@ -77,6 +77,18 @@ DEFAULT_METRICS_EXPORT_PATH = "v1/metrics" DEFAULT_TIMEOUT = 10 # in seconds +# Work around API change between backoff 1.x and 2.x. Since 2.0.0 the backoff +# wait generator API requires a first .send(None) before reading the backoff +# values from the generator. +_is_backoff_v2 = next(backoff.expo()) is None + + +def _expo(*args, **kwargs): + gen = backoff.expo(*args, **kwargs) + if _is_backoff_v2: + gen.send(None) + return gen + class OTLPMetricExporter(MetricExporter): @@ -319,7 +331,7 @@ def export( **kwargs, ) -> MetricExportResult: serialized_data = self._translate_data(metrics_data) - for delay in expo(max_value=self._MAX_RETRY_TIMEOUT): + for delay in _expo(max_value=self._MAX_RETRY_TIMEOUT): if delay == self._MAX_RETRY_TIMEOUT: return MetricExportResult.FAILURE diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index a65bc44320..9dc3b65014 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -20,8 +20,8 @@ from typing import Dict, Optional from time import sleep +import backoff import requests -from backoff import expo from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, @@ -54,6 +54,18 @@ DEFAULT_TRACES_EXPORT_PATH = "v1/traces" DEFAULT_TIMEOUT = 10 # in seconds +# Work around API change between backoff 1.x and 2.x. Since 2.0.0 the backoff +# wait generator API requires a first .send(None) before reading the backoff +# values from the generator. +_is_backoff_v2 = next(backoff.expo()) is None + + +def _expo(*args, **kwargs): + gen = backoff.expo(*args, **kwargs) + if _is_backoff_v2: + gen.send(None) + return gen + class OTLPSpanExporter(SpanExporter): @@ -133,7 +145,7 @@ def export(self, spans) -> SpanExportResult: serialized_data = _ProtobufEncoder.serialize(spans) - for delay in expo(max_value=self._MAX_RETRY_TIMEOUT): + for delay in _expo(max_value=self._MAX_RETRY_TIMEOUT): if delay == self._MAX_RETRY_TIMEOUT: return SpanExportResult.FAILURE diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index e4665a47fc..74ba53b3c5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -16,6 +16,7 @@ from unittest.mock import patch import requests +import responses from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( @@ -269,3 +270,30 @@ def test_serialization(self, mock_post): verify=exporter._certificate_file, timeout=exporter._timeout, ) + + @responses.activate + @patch("opentelemetry.exporter.otlp.proto.http.metric_exporter.backoff") + @patch("opentelemetry.exporter.otlp.proto.http.metric_exporter.sleep") + def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff): + # In backoff ~= 2.0.0 the first value yielded from expo is None. + def generate_delays(*args, **kwargs): + yield None + yield 1 + + mock_backoff.expo.configure_mock(**{"side_effect": generate_delays}) + + # return a retryable error + responses.add( + responses.POST, + "http://metrics.example.com/export", + json={"error": "something exploded"}, + status=500, + ) + + exporter = OTLPMetricExporter( + endpoint="http://metrics.example.com/export" + ) + metrics_data = self.metrics["sum_int"] + + exporter.export(metrics_data) + mock_sleep.assert_called_once_with(1) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index 2063820b5d..d94ddfb8f7 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -19,6 +19,7 @@ from unittest.mock import MagicMock, patch import requests +import responses from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._log_exporter import ( @@ -159,6 +160,31 @@ def test_serialize(self): expected_encoding.SerializeToString(), ) + @responses.activate + @patch("opentelemetry.exporter.otlp.proto.http._log_exporter.backoff") + @patch("opentelemetry.exporter.otlp.proto.http._log_exporter.sleep") + def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff): + # In backoff ~= 2.0.0 the first value yielded from expo is None. + def generate_delays(*args, **kwargs): + yield None + yield 1 + + mock_backoff.expo.configure_mock(**{"side_effect": generate_delays}) + + # return a retryable error + responses.add( + responses.POST, + "http://logs.example.com/export", + json={"error": "something exploded"}, + status=500, + ) + + exporter = OTLPLogExporter(endpoint="http://logs.example.com/export") + logs = self._get_sdk_log_data() + + exporter.export(logs) + mock_sleep.assert_called_once_with(1) + @staticmethod def _get_sdk_log_data() -> List[LogData]: log1 = LogData( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index 73e54e86c0..c7ff9a15dd 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -13,9 +13,11 @@ # limitations under the License. import unittest -from unittest.mock import patch +from collections import OrderedDict +from unittest.mock import Mock, patch import requests +import responses from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( @@ -37,6 +39,7 @@ OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) +from opentelemetry.sdk.trace import _Span OS_ENV_ENDPOINT = "os.env.base" OS_ENV_CERTIFICATE = "os/env/base.crt" @@ -188,3 +191,40 @@ def test_headers_parse_from_env(self): cm.records[0].message, "Header doesn't match the format: missingValue.", ) + + # pylint: disable=no-self-use + @responses.activate + @patch("opentelemetry.exporter.otlp.proto.http.trace_exporter.backoff") + @patch("opentelemetry.exporter.otlp.proto.http.trace_exporter.sleep") + def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff): + # In backoff ~= 2.0.0 the first value yielded from expo is None. + def generate_delays(*args, **kwargs): + yield None + yield 1 + + mock_backoff.expo.configure_mock(**{"side_effect": generate_delays}) + + # return a retryable error + responses.add( + responses.POST, + "http://traces.example.com/export", + json={"error": "something exploded"}, + status=500, + ) + + exporter = OTLPSpanExporter( + endpoint="http://traces.example.com/export" + ) + span = _Span( + "abc", + context=Mock( + **{ + "trace_state": OrderedDict([("a", "b"), ("c", "d")]), + "span_id": 10217189687419569865, + "trace_id": 67545097771067222548457157018666467027, + } + ), + ) + + exporter.export([span]) + mock_sleep.assert_called_once_with(1) diff --git a/tox.ini b/tox.ini index 1a24186fbd..2248ab1e42 100644 --- a/tox.ini +++ b/tox.ini @@ -137,7 +137,7 @@ commands_pre = exporter-otlp-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc exporter-otlp-proto-http: pip install {toxinidir}/opentelemetry-proto - exporter-otlp-proto-http: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http + exporter-otlp-proto-http: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http[test] exporter-jaeger-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift {toxinidir}/exporter/opentelemetry-exporter-jaeger exporter-jaeger-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc From 8beae7d2c4a0e692541330a9d33868cdace6d562 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 1 Nov 2022 02:50:04 +0530 Subject: [PATCH 1336/1517] Add py3.11 to workflow (#2997) * Add py3.11 to workflow * Update classifiers * Spacing * Add CHANGELOG entry Co-authored-by: Leighton Chen --- .github/workflows/test.yml | 3 +- CHANGELOG.md | 2 + .../error_handler_0/pyproject.toml | 1 + .../error_handler_1/pyproject.toml | 1 + .../pyproject.toml | 1 + .../pyproject.toml | 1 + .../pyproject.toml | 1 + .../pyproject.toml | 1 + .../pyproject.toml | 1 + .../pyproject.toml | 1 + .../pyproject.toml | 1 + .../pyproject.toml | 1 + .../pyproject.toml | 1 + .../pyproject.toml | 1 + .../pyproject.toml | 1 + opentelemetry-api/pyproject.toml | 1 + opentelemetry-proto/pyproject.toml | 1 + opentelemetry-sdk/pyproject.toml | 1 + .../pyproject.toml | 1 + .../pyproject.toml | 1 + .../pyproject.toml | 1 + .../pyproject.toml | 1 + tox.ini | 40 +++++++++---------- 23 files changed, 44 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28fbec883f..71d31be729 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,13 +27,14 @@ jobs: py38: 3.8 py39: 3.9 py310: "3.10" + py311: "3.11" pypy3: pypy-3.7 RUN_MATRIX_COMBINATION: ${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [ py37, py38, py39, py310, pypy3 ] + python-version: [ py37, py38, py39, py310, py311, pypy3 ] package: ["api", "sdk", "semantic", "getting", "shim", "exporter", "protobuf", "propagator"] os: [ ubuntu-20.04, windows-2019 ] steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index bfbbe3d2ca..aa0d969b82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2976](https://github.com/open-telemetry/opentelemetry-python/pull/2976)) - [exporter/opentelemetry-exporter-otlp-proto-http] Add OTLPMetricExporter ([#2891](https://github.com/open-telemetry/opentelemetry-python/pull/2891)) +- Add support for py3.11 + ([#2997](https://github.com/open-telemetry/opentelemetry-python/pull/2997)) - Fix a bug with exporter retries for with newer versions of the backoff library ([#2980](https://github.com/open-telemetry/opentelemetry-python/pull/2980)) diff --git a/docs/examples/error_handler/error_handler_0/pyproject.toml b/docs/examples/error_handler/error_handler_0/pyproject.toml index 8874896851..b148d0b13a 100644 --- a/docs/examples/error_handler/error_handler_0/pyproject.toml +++ b/docs/examples/error_handler/error_handler_0/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-sdk ~= 1.3", diff --git a/docs/examples/error_handler/error_handler_1/pyproject.toml b/docs/examples/error_handler/error_handler_1/pyproject.toml index d2a6caca3e..506b8e24ae 100644 --- a/docs/examples/error_handler/error_handler_1/pyproject.toml +++ b/docs/examples/error_handler/error_handler_1/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-sdk ~= 1.3", diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml index a3a4f76f59..1f481fc2a3 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dependencies = [ diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/pyproject.toml b/exporter/opentelemetry-exporter-jaeger-thrift/pyproject.toml index b10a35a85e..48e3a76848 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger-thrift/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dependencies = [ diff --git a/exporter/opentelemetry-exporter-jaeger/pyproject.toml b/exporter/opentelemetry-exporter-jaeger/pyproject.toml index 3f7cf695b0..b28066a2b0 100644 --- a/exporter/opentelemetry-exporter-jaeger/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dependencies = [ diff --git a/exporter/opentelemetry-exporter-opencensus/pyproject.toml b/exporter/opentelemetry-exporter-opencensus/pyproject.toml index 282b5f0c42..fe03741dfb 100644 --- a/exporter/opentelemetry-exporter-opencensus/pyproject.toml +++ b/exporter/opentelemetry-exporter-opencensus/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dependencies = [ diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index bf7a261d9f..ca760e01b9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "backoff >= 1.10.0, < 2.0.0; python_version<'3.7'", diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index a89e7fa188..c1734a275f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "backoff >= 1.10.0, < 2.0.0; python_version<'3.7'", diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index 0f3936ad57..47abbc7df1 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dependencies = [ diff --git a/exporter/opentelemetry-exporter-prometheus/pyproject.toml b/exporter/opentelemetry-exporter-prometheus/pyproject.toml index 9dd2d63517..f7018469ba 100644 --- a/exporter/opentelemetry-exporter-prometheus/pyproject.toml +++ b/exporter/opentelemetry-exporter-prometheus/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", diff --git a/exporter/opentelemetry-exporter-zipkin-json/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-json/pyproject.toml index 07c0a74d23..70292809f9 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin-json/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dependencies = [ diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml index eb5bde0440..c28edc85d2 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dependencies = [ diff --git a/exporter/opentelemetry-exporter-zipkin/pyproject.toml b/exporter/opentelemetry-exporter-zipkin/pyproject.toml index 76cac43f4b..26eb8781ad 100644 --- a/exporter/opentelemetry-exporter-zipkin/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dependencies = [ diff --git a/opentelemetry-api/pyproject.toml b/opentelemetry-api/pyproject.toml index 9de9b0d12d..50b2a8b8a0 100644 --- a/opentelemetry-api/pyproject.toml +++ b/opentelemetry-api/pyproject.toml @@ -21,6 +21,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dependencies = [ diff --git a/opentelemetry-proto/pyproject.toml b/opentelemetry-proto/pyproject.toml index d7c47210c2..3b7875dcd5 100644 --- a/opentelemetry-proto/pyproject.toml +++ b/opentelemetry-proto/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "protobuf~=3.13", diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index e076a6a40e..bd27168f55 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dependencies = [ diff --git a/opentelemetry-semantic-conventions/pyproject.toml b/opentelemetry-semantic-conventions/pyproject.toml index f3da7a3650..8ad02bb35c 100644 --- a/opentelemetry-semantic-conventions/pyproject.toml +++ b/opentelemetry-semantic-conventions/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] [project.optional-dependencies] diff --git a/propagator/opentelemetry-propagator-b3/pyproject.toml b/propagator/opentelemetry-propagator-b3/pyproject.toml index a4231e0e62..4a2bc08001 100644 --- a/propagator/opentelemetry-propagator-b3/pyproject.toml +++ b/propagator/opentelemetry-propagator-b3/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dependencies = [ diff --git a/propagator/opentelemetry-propagator-jaeger/pyproject.toml b/propagator/opentelemetry-propagator-jaeger/pyproject.toml index d713984b93..519fc6e48d 100644 --- a/propagator/opentelemetry-propagator-jaeger/pyproject.toml +++ b/propagator/opentelemetry-propagator-jaeger/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dependencies = [ diff --git a/shim/opentelemetry-opentracing-shim/pyproject.toml b/shim/opentelemetry-opentracing-shim/pyproject.toml index 46ba16884e..e6119df7f6 100644 --- a/shim/opentelemetry-opentracing-shim/pyproject.toml +++ b/shim/opentelemetry-opentracing-shim/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dependencies = [ diff --git a/tox.ini b/tox.ini index 2248ab1e42..90f8f8d94b 100644 --- a/tox.ini +++ b/tox.ini @@ -5,61 +5,61 @@ skip_missing_interpreters = True envlist = ; Environments are organized by individual package, allowing ; for specifying supported Python versions per package. - py3{7,8,9,10}-opentelemetry-api + py3{7,8,9,10,11}-opentelemetry-api pypy3-opentelemetry-api - py3{7,8,9,10}-opentelemetry-protobuf + py3{7,8,9,10,11}-opentelemetry-protobuf pypy3-opentelemetry-protobuf - py3{7,8,9,10}-opentelemetry-sdk + py3{7,8,9,10,11}-opentelemetry-sdk pypy3-opentelemetry-sdk - py3{7,8,9,10}-opentelemetry-semantic-conventions + py3{7,8,9,10,11}-opentelemetry-semantic-conventions pypy3-opentelemetry-semantic-conventions ; docs/getting-started - py3{7,8,9,10}-opentelemetry-getting-started + py3{7,8,9,10,11}-opentelemetry-getting-started pypy3-opentelemetry-getting-started - py3{7,8,9,10}-opentelemetry-opentracing-shim + py3{7,8,9,10,11}-opentelemetry-opentracing-shim pypy3-opentelemetry-opentracing-shim - py3{7,8,9,10}-opentelemetry-exporter-jaeger-combined + py3{7,8,9,10,11}-opentelemetry-exporter-jaeger-combined - py3{7,8,9,10}-opentelemetry-exporter-jaeger-proto-grpc + py3{7,8,9,10,11}-opentelemetry-exporter-jaeger-proto-grpc - py3{7,8,9,10}-opentelemetry-exporter-jaeger-thrift + py3{7,8,9,10,11}-opentelemetry-exporter-jaeger-thrift - py3{7,8,9,10}-opentelemetry-exporter-opencensus + py3{7,8,9,10,11}-opentelemetry-exporter-opencensus ; exporter-opencensus intentionally excluded from pypy3 ; opentelemetry-exporter-otlp - py3{7,8,9,10}-opentelemetry-exporter-otlp-combined + py3{7,8,9,10,11}-opentelemetry-exporter-otlp-combined ; intentionally excluded from pypy3 - py3{7,8,9,10}-opentelemetry-exporter-otlp-proto-grpc + py3{7,8,9,10,11}-opentelemetry-exporter-otlp-proto-grpc ; intentionally excluded from pypy3 - py3{7,8,9,10}-opentelemetry-exporter-otlp-proto-http + py3{7,8,9,10,11}-opentelemetry-exporter-otlp-proto-http pypy3-opentelemetry-exporter-otlp-proto-http - py3{7,8,9,10}-opentelemetry-exporter-prometheus + py3{7,8,9,10,11}-opentelemetry-exporter-prometheus pypy3-opentelemetry-exporter-prometheus ; opentelemetry-exporter-zipkin - py3{7,8,9,10}-opentelemetry-exporter-zipkin-combined + py3{7,8,9,10,11}-opentelemetry-exporter-zipkin-combined pypy3-opentelemetry-exporter-zipkin-combined - py3{7,8,9,10}-opentelemetry-exporter-zipkin-proto-http + py3{7,8,9,10,11}-opentelemetry-exporter-zipkin-proto-http pypy3-opentelemetry-exporter-zipkin-proto-http - py3{7,8,9,10}-opentelemetry-exporter-zipkin-json + py3{7,8,9,10,11}-opentelemetry-exporter-zipkin-json pypy3-opentelemetry-exporter-zipkin-json - py3{7,8,9,10}-opentelemetry-propagator-b3 + py3{7,8,9,10,11}-opentelemetry-propagator-b3 pypy3-opentelemetry-propagator-b3 - py3{7,8,9,10}-opentelemetry-propagator-jaeger + py3{7,8,9,10,11}-opentelemetry-propagator-jaeger pypy3-opentelemetry-propagator-jaeger lint @@ -112,7 +112,7 @@ changedir = commands_pre = ; Install without -e to test the actual installation - py3{7,8,9,10}: python -m pip install -U pip setuptools wheel + py3{7,8,9,10,11}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. opentelemetry: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-semantic-conventions {toxinidir}/opentelemetry-sdk {toxinidir}/tests/opentelemetry-test-utils From fa19e1f3753d622697bb4d69d394409c6127158e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 31 Oct 2022 22:39:30 +0000 Subject: [PATCH 1337/1517] Add and use missing metrics environment variables (#2968) * Add and use missing metrics environment variables Fixes #2967 * Add missing environment variables * Remove redundant test * Add test case for HTTPS endpoint * Fix mocks --- CHANGELOG.md | 2 + .../proto/grpc/metric_exporter/__init__.py | 35 +++++- .../tests/metrics/__init__.py | 0 .../test_otlp_metrics_exporter.py | 105 +++++++++++++++++- .../sdk/environment_variables.py | 90 +++++++++++++++ .../sdk/metrics/_internal/export/__init__.py | 8 +- 6 files changed, 234 insertions(+), 6 deletions(-) delete mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/__init__.py rename exporter/opentelemetry-exporter-otlp-proto-grpc/tests/{metrics => }/test_otlp_metrics_exporter.py (92%) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa0d969b82..37b04c5074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0...HEAD) +- Add and use missing metrics environment variables + ([#2968](https://github.com/open-telemetry/opentelemetry-python/pull/2968)) - Enabled custom samplers via entry points ([#2972](https://github.com/open-telemetry/opentelemetry-python/pull/2972)) - Update log symbol names diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index a377ed9b2e..3c6f59c35f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -20,6 +20,8 @@ from opentelemetry.exporter.otlp.proto.grpc.exporter import ( OTLPExporterMixin, get_resource_data, + _get_credentials, + environ_to_compression, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, @@ -30,7 +32,12 @@ from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_INSECURE, + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, ) from opentelemetry.sdk.metrics import ( @@ -65,6 +72,7 @@ class OTLPMetricExporter( """OTLP metric exporter Args: + endpoint: Target URL to which the exporter is going to send metrics max_export_batch_size: Maximum number of data points to export in a single request. This is to deal with gRPC's 4MB message size limit. If not set there is no limit to the number of data points in a request. If it is set and the number of data points exceeds the max, the request will be split. @@ -91,6 +99,25 @@ def __init__( if insecure is not None: insecure = insecure.lower() == "true" + if ( + not insecure + and environ.get(OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE) is not None + ): + credentials = _get_credentials( + credentials, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE + ) + + environ_timeout = environ.get(OTEL_EXPORTER_OTLP_METRICS_TIMEOUT) + environ_timeout = ( + int(environ_timeout) if environ_timeout is not None else None + ) + + compression = ( + environ_to_compression(OTEL_EXPORTER_OTLP_METRICS_COMPRESSION) + if compression is None + else compression + ) + instrument_class_temporality = {} if ( environ.get( @@ -125,13 +152,15 @@ def __init__( preferred_temporality=instrument_class_temporality, preferred_aggregation=preferred_aggregation, ) + OTLPExporterMixin.__init__( self, - endpoint=endpoint, + endpoint=endpoint + or environ.get(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT), insecure=insecure, credentials=credentials, - headers=headers, - timeout=timeout, + headers=headers or environ.get(OTEL_EXPORTER_OTLP_METRICS_HEADERS), + timeout=timeout or environ_timeout, compression=compression, ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py similarity index 92% rename from exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py index 0e3e26b747..4f0349257d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py @@ -14,13 +14,14 @@ # pylint: disable=too-many-lines from concurrent.futures import ThreadPoolExecutor +from os.path import dirname from typing import List from unittest import TestCase from unittest.mock import patch from google.protobuf.duration_pb2 import Duration from google.rpc.error_details_pb2 import RetryInfo -from grpc import StatusCode, server +from grpc import ChannelCredentials, Compression, StatusCode, server from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( OTLPMetricExporter, @@ -43,8 +44,14 @@ Resource as OTLPResource, ) from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_INSECURE, OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, ) from opentelemetry.sdk.metrics import ( Counter, @@ -71,6 +78,8 @@ ) from opentelemetry.test.metrictestutil import _generate_gauge, _generate_sum +THIS_DIR = dirname(__file__) + class MetricsServiceServicerUNAVAILABLEDelay(MetricsServiceServicer): # pylint: disable=invalid-name,unused-argument,no-self-use @@ -119,6 +128,8 @@ def Export(self, request, context): class TestOTLPMetricExporter(TestCase): + # pylint: disable=too-many-public-methods + def setUp(self): self.exporter = OTLPMetricExporter() @@ -348,6 +359,33 @@ def test_preferred_temporality(self): AggregationTemporality.CUMULATIVE, ) + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "collector:4317", + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: THIS_DIR + + "/fixtures/test.cert", + OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = value=2", + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "10", + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "gzip", + }, + ) + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__" + ) + def test_env_variables(self, mock_exporter_mixin): + OTLPMetricExporter() + + self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) + _, kwargs = mock_exporter_mixin.call_args_list[0] + + self.assertEqual(kwargs["endpoint"], "collector:4317") + self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") + self.assertEqual(kwargs["timeout"], 10) + self.assertEqual(kwargs["compression"], Compression.Gzip) + self.assertIsNotNone(kwargs["credentials"]) + self.assertIsInstance(kwargs["credentials"], ChannelCredentials) + @patch( "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" ) @@ -362,6 +400,29 @@ def test_no_credentials_error( OTLPMetricExporter(insecure=False) self.assertTrue(mock_ssl_channel.called) + @patch.dict( + "os.environ", + {OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = VALUE=2 "}, + ) + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" + ) + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") + # pylint: disable=unused-argument + def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): + exporter = OTLPMetricExporter() + # pylint: disable=protected-access + self.assertEqual( + exporter._headers, (("key1", "value1"), ("key2", "VALUE=2")) + ) + exporter = OTLPMetricExporter( + headers=(("key3", "value3"), ("key4", "value4")) + ) + # pylint: disable=protected-access + self.assertEqual( + exporter._headers, (("key3", "value3"), ("key4", "value4")) + ) + @patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_METRICS_INSECURE: "True"}, @@ -449,6 +510,43 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): ) mock_method.reset_mock() + # pylint: disable=no-self-use + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") + @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) + def test_otlp_exporter_otlp_compression_envvar( + self, mock_insecure_channel, mock_expo + ): + """Just OTEL_EXPORTER_OTLP_COMPRESSION should work""" + OTLPMetricExporter(insecure=True) + mock_insecure_channel.assert_called_once_with( + "localhost:4317", compression=Compression.Gzip + ) + + # pylint: disable=no-self-use + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") + @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) + def test_otlp_exporter_otlp_compression_kwarg(self, mock_insecure_channel): + """Specifying kwarg should take precedence over env""" + OTLPMetricExporter( + insecure=True, compression=Compression.NoCompression + ) + mock_insecure_channel.assert_called_once_with( + "localhost:4317", compression=Compression.NoCompression + ) + + # pylint: disable=no-self-use + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") + @patch.dict("os.environ", {}) + def test_otlp_exporter_otlp_compression_unspecified( + self, mock_insecure_channel + ): + """No env or kwarg should be NoCompression""" + OTLPMetricExporter(insecure=True) + mock_insecure_channel.assert_called_once_with( + "localhost:4317", compression=Compression.NoCompression + ) + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): @@ -1243,6 +1341,11 @@ def test_split_metrics_data_many_resources_scopes_metrics(self): split_metrics_data, ) + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") + def test_insecure_https_endpoint(self, mock_secure_channel): + OTLPMetricExporter(endpoint="https://ab.c:123", insecure=True) + mock_secure_channel.assert_called() + def _resource_metrics( index: int, scope_metrics: List[ScopeMetrics] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 3f31b3b71c..e8afdd6cbf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -335,6 +335,15 @@ A scheme of https indicates a secure connection and takes precedence over this configuration setting. """ +OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT + +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` target to which the metrics exporter is going to send metrics. +The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path. +A scheme of https indicates a secure connection and takes precedence over this configuration setting. +""" + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE = "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE @@ -343,6 +352,16 @@ TLS credentials of gRPC client for traces. Should only be used for a secure connection for tracing. """ +OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE = ( + "OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE" +) +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE + +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE` stores the path to the certificate file for +TLS credentials of gRPC client for metrics. Should only be used for a secure connection for exporting metrics. +""" + OTEL_EXPORTER_OTLP_TRACES_HEADERS = "OTEL_EXPORTER_OTLP_TRACES_HEADERS" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_HEADERS @@ -351,6 +370,13 @@ associated with gRPC or HTTP requests. """ +OTEL_EXPORTER_OTLP_METRICS_HEADERS = "OTEL_EXPORTER_OTLP_METRICS_HEADERS" +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_HEADERS + +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_HEADERS` contains the key-value pairs to be used as headers for metrics +associated with gRPC or HTTP requests. +""" OTEL_EXPORTER_OTLP_TRACES_COMPRESSION = "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION" """ @@ -360,6 +386,16 @@ exporter. If both are present, this takes higher precedence. """ +OTEL_EXPORTER_OTLP_METRICS_COMPRESSION = ( + "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION" +) +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_COMPRESSION + +Same as :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` but only for the metric +exporter. If both are present, this takes higher precedence. +""" + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_TIMEOUT @@ -368,6 +404,14 @@ wait for each batch export for spans. """ +OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT" +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_TIMEOUT + +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` is the maximum time the OTLP exporter will +wait for each batch export for metrics. +""" + OTEL_EXPORTER_OTLP_METRICS_INSECURE = "OTEL_EXPORTER_OTLP_METRICS_INSECURE" """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_INSECURE @@ -501,3 +545,49 @@ The :envvar:`OTEL_EXPORTER_JAEGER_GRPC_INSECURE` is a boolean flag to True if collector has no encryption or authentication. """ + +OTEL_METRIC_EXPORT_INTERVAL = "OTEL_METRIC_EXPORT_INTERVAL" +""" +.. envvar:: OTEL_METRIC_EXPORT_INTERVAL + +The :envvar:`OTEL_METRIC_EXPORT_INTERVAL` is the time interval (in milliseconds) between the start of two export attempts. +""" + +OTEL_METRIC_EXPORT_TIMEOUT = "OTEL_METRIC_EXPORT_TIMEOUT" +""" +.. envvar:: OTEL_METRIC_EXPORT_TIMEOUT + +The :envvar:`OTEL_METRIC_EXPORT_TIMEOUT` is the maximum allowed time (in milliseconds) to export data. +""" + +OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY = "OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY" +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY + +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY` is the clients private key to use in mTLS communication in PEM format. +""" + +OTEL_METRICS_EXEMPLAR_FILTER = "OTEL_METRICS_EXEMPLAR_FILTER" +""" +.. envvar:: OTEL_METRICS_EXEMPLAR_FILTER + +The :envvar:`OTEL_METRICS_EXEMPLAR_FILTER` is the filter for which measurements can become Exemplars. +""" + +_OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION = ( + "OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION" +) +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION + +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` is the default aggregation to use for histogram instruments. +""" + +OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE = ( + "OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE" +) +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE + +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE` is the client certificate/chain trust for clients private key to use in mTLS communication in PEM format. +""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 4c63c4288b..591f7e8a88 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -32,6 +32,10 @@ detach, set_value, ) +from opentelemetry.sdk.environment_variables import ( + OTEL_METRIC_EXPORT_INTERVAL, + OTEL_METRIC_EXPORT_TIMEOUT, +) from opentelemetry.sdk.metrics._internal.aggregation import ( AggregationTemporality, DefaultAggregation, @@ -437,7 +441,7 @@ def __init__( if export_interval_millis is None: try: export_interval_millis = float( - environ.get("OTEL_METRIC_EXPORT_INTERVAL", 60000) + environ.get(OTEL_METRIC_EXPORT_INTERVAL, 60000) ) except ValueError: _logger.warning( @@ -447,7 +451,7 @@ def __init__( if export_timeout_millis is None: try: export_timeout_millis = float( - environ.get("OTEL_METRIC_EXPORT_TIMEOUT", 30000) + environ.get(OTEL_METRIC_EXPORT_TIMEOUT, 30000) ) except ValueError: _logger.warning( From 7d51d203dedc37955e2495f7f48851bde1e5a536 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 1 Nov 2022 17:40:42 +0000 Subject: [PATCH 1338/1517] Add exponent and logarithm mappings (#2960) * Add exponent and logarithm mappings Fixes #2957 * Fix comments * Remove example function * Fix lint and spelling * Add link to spec * Fix documentation to reference the exceptions * Refactor min and max scale * Set self._scale in parent only * Add explanation for IEEE 754 * Use mantissa consistently * Refactor lock definition * Fix wrong fixed value * Fix lint * Fix test name * Update opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py Co-authored-by: Aaron Abbott * Fix operator separator * Rename boundary functions * Add links to reference implementation * Fix lint and spelling * Revert "Refactor lock definition" This reverts commit 064bb2b7d7f04f93891580c9cce4a6ee135bacd1. * Refactor initialization * Fix math format * Rename to normal and denormal * Remove errors from public interface Co-authored-by: Aaron Abbott --- CHANGELOG.md | 2 + .../exponential_histogram/mapping/__init__.py | 97 +++++ .../exponential_histogram/mapping/errors.py | 26 ++ .../mapping/exponent_mapping.py | 141 +++++++ .../exponential_histogram/mapping/ieee_754.md | 175 ++++++++ .../exponential_histogram/mapping/ieee_754.py | 118 ++++++ .../mapping/logarithm_mapping.py | 139 ++++++ .../test_exponent_mapping.py | 395 ++++++++++++++++++ .../test_logarithm_mapping.py | 241 +++++++++++ 9 files changed, 1334 insertions(+) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py create mode 100644 opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py create mode 100644 opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 37b04c5074..27a8b61a8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0...HEAD) +- Add logarithm and exponent mappings + ([#2960](https://github.com/open-telemetry/opentelemetry-python/pull/2960)) - Add and use missing metrics environment variables ([#2968](https://github.com/open-telemetry/opentelemetry-python/pull/2968)) - Enabled custom samplers via entry points diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py new file mode 100644 index 0000000000..d8c780cf40 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py @@ -0,0 +1,97 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod + + +class Mapping(ABC): + """ + Parent class for `LogarithmMapping` and `ExponentialMapping`. + """ + + # pylint: disable=no-member + def __new__(cls, scale: int): + + with cls._mappings_lock: + # cls._mappings and cls._mappings_lock are implemented in each of + # the child classes as a dictionary and a lock, respectively. They + # are not instantiated here because that would lead to both child + # classes having the same instance of cls._mappings and + # cls._mappings_lock. + if scale not in cls._mappings: + cls._mappings[scale] = super().__new__(cls) + cls._mappings[scale]._init(scale) + + return cls._mappings[scale] + + @abstractmethod + def _init(self, scale: int) -> None: + # pylint: disable=attribute-defined-outside-init + + if scale > self._get_max_scale(): + raise Exception(f"scale is larger than {self._max_scale}") + + if scale < self._get_min_scale(): + raise Exception(f"scale is smaller than {self._min_scale}") + + # The size of the exponential histogram buckets is determined by a + # parameter known as scale, larger values of scale will produce smaller + # buckets. Bucket boundaries of the exponential histogram are located + # at integer powers of the base, where: + # + # base = 2 ** (2 ** (-scale)) + # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#all-scales-use-the-logarithm-function + self._scale = scale + + @abstractmethod + def _get_min_scale(self) -> int: + """ + Return the smallest possible value for the mapping scale + """ + + @abstractmethod + def _get_max_scale(self) -> int: + """ + Return the largest possible value for the mapping scale + """ + + @abstractmethod + def map_to_index(self, value: float) -> int: + """ + Maps positive floating point values to indexes corresponding to + `Mapping.scale`. Implementations are not expected to handle zeros, + +inf, NaN, or negative values. + """ + + @abstractmethod + def get_lower_boundary(self, index: int) -> float: + """ + Returns the lower boundary of a given bucket index. The index is + expected to map onto a range that is at least partially inside the + range of normal floating point values. If the corresponding + bucket's upper boundary is less than or equal to 2 ** -1022, + :class:`~opentelemetry.sdk.metrics.MappingUnderflowError` + will be raised. If the corresponding bucket's lower boundary is greater + than ``sys.float_info.max``, + :class:`~opentelemetry.sdk.metrics.MappingOverflowError` + will be raised. + """ + + @property + def scale(self) -> int: + """ + Returns the parameter that controls the resolution of this mapping. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponential-scale + """ + return self._scale diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py new file mode 100644 index 0000000000..477ed6f0f5 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class MappingUnderflowError(Exception): + """ + Raised when computing the lower boundary of an index that maps into a + denormal floating point value. + """ + + +class MappingOverflowError(Exception): + """ + Raised when computing the lower boundary of an index that maps into +inf. + """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py new file mode 100644 index 0000000000..297bb7a483 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py @@ -0,0 +1,141 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from math import ldexp +from threading import Lock + +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( + Mapping, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( + MappingOverflowError, + MappingUnderflowError, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( + MANTISSA_WIDTH, + MAX_NORMAL_EXPONENT, + MIN_NORMAL_EXPONENT, + MIN_NORMAL_VALUE, + get_ieee_754_exponent, + get_ieee_754_mantissa, +) + + +class ExponentMapping(Mapping): + # Reference implementation here: + # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/exponent/exponent.go + + _mappings = {} + _mappings_lock = Lock() + + _min_scale = -10 + _max_scale = 0 + + def _get_min_scale(self): + # _min_scale defines the point at which the exponential mapping + # function becomes useless for 64-bit floats. With scale -10, ignoring + # subnormal values, bucket indices range from -1 to 1. + return -10 + + def _get_max_scale(self): + # _max_scale is the largest scale supported by exponential mapping. Use + # a logarithm mapping for larger scales. + return 0 + + def _init(self, scale: int): + # pylint: disable=attribute-defined-outside-init + + super()._init(scale) + + # self._min_normal_lower_boundary_index is the largest index such that + # base ** index < MIN_NORMAL_VALUE and + # base ** (index + 1) >= MIN_NORMAL_VALUE. An exponential histogram + # bucket with this index covers the range + # (base ** index, base (index + 1)], including MIN_NORMAL_VALUE. This + # is the smallest valid index that contains at least one normal value. + index = MIN_NORMAL_EXPONENT >> -self._scale + + if -self._scale < 2: + # For scales -1 and 0, the maximum value 2 ** -1022 is a + # power-of-two multiple, meaning base ** index == MIN_NORMAL_VALUE. + # Subtracting 1 so that base ** (index + 1) == MIN_NORMAL_VALUE. + index -= 1 + + self._min_normal_lower_boundary_index = index + + # self._max_normal_lower_boundary_index is the index such that + # base**index equals the greatest representable lower boundary. An + # exponential histogram bucket with this index covers the range + # ((2 ** 1024) / base, 2 ** 1024], which includes opentelemetry.sdk. + # metrics._internal.exponential_histogram.ieee_754.MAX_NORMAL_VALUE. + # This bucket is incomplete, since the upper boundary cannot be + # represented. One greater than this index corresponds with the bucket + # containing values > 2 ** 1024. + self._max_normal_lower_boundary_index = ( + MAX_NORMAL_EXPONENT >> -self._scale + ) + + def map_to_index(self, value: float) -> int: + if value < MIN_NORMAL_VALUE: + return self._min_normal_lower_boundary_index + + exponent = get_ieee_754_exponent(value) + + # Positive integers are represented in binary as having an infinite + # amount of leading zeroes, for example 2 is represented as ...00010. + + # A negative integer -x is represented in binary as the complement of + # (x - 1). For example, -4 is represented as the complement of 4 - 1 + # == 3. 3 is represented as ...00011. Its compliment is ...11100, the + # binary representation of -4. + + # get_ieee_754_mantissa(value) gets the positive integer made up + # from the rightmost MANTISSA_WIDTH bits (the mantissa) of the IEEE + # 754 representation of value. If value is an exact power of 2, all + # these MANTISSA_WIDTH bits would be all zeroes, and when 1 is + # subtracted the resulting value is -1. The binary representation of + # -1 is ...111, so when these bits are right shifted MANTISSA_WIDTH + # places, the resulting value for correction is -1. If value is not an + # exact power of 2, at least one of the rightmost MANTISSA_WIDTH + # bits would be 1 (even for values whose decimal part is 0, like 5.0 + # since the IEEE 754 of such number is too the product of a power of 2 + # (defined in the exponent part of the IEEE 754 representation) and the + # value defined in the mantissa). Having at least one of the rightmost + # MANTISSA_WIDTH bit being 1 means that get_ieee_754(value) will + # always be greater or equal to 1, and when 1 is subtracted, the + # result will be greater or equal to 0, whose representation in binary + # will be of at most MANTISSA_WIDTH ones that have an infinite + # amount of leading zeroes. When those MANTISSA_WIDTH bits are + # shifted to the right MANTISSA_WIDTH places, the resulting value + # will be 0. + + # In summary, correction will be -1 if value is a power of 2, 0 if not. + + # FIXME Document why we can assume value will not be 0, inf, or NaN. + correction = (get_ieee_754_mantissa(value) - 1) >> MANTISSA_WIDTH + + return (exponent + correction) >> -self._scale + + def get_lower_boundary(self, index: int) -> float: + if index < self._min_normal_lower_boundary_index: + raise MappingUnderflowError() + + if index > self._max_normal_lower_boundary_index: + raise MappingOverflowError() + + return ldexp(1, index << -self._scale) + + @property + def scale(self) -> int: + return self._scale diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md new file mode 100644 index 0000000000..ba9601bdf9 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md @@ -0,0 +1,175 @@ +# IEEE 754 Explained + +IEEE 754 is a standard that defines a way to represent certain mathematical +objects using binary numbers. + +## Binary Number Fields + +The binary numbers used in IEEE 754 can have different lengths, the length that +is interesting for the purposes of this project is 64 bits. These binary +numbers are made up of 3 contiguous fields of bits, from left to right: + +1. 1 sign bit +2. 11 exponent bits +3. 52 mantissa bits + +Depending on the values these fields have, the represented mathematical object +can be one of: + +* Floating point number +* Zero +* NaN +* Infinite + +## Floating Point Numbers + +IEEE 754 represents a floating point number $f$ using an exponential +notation with 4 components: $sign$, $mantissa$, $base$ and $exponent$: + +$$f = sign \times mantissa \times base ^ {exponent}$$ + +There are two possible representations of floating point numbers: +_normal_ and _denormal_, which have different valid values for +their $mantissa$ and $exponent$ fields. + +### Binary Representation + +$sign$, $mantissa$, and $exponent$ are represented in binary, the +representation of each component has certain details explained next. + +$base$ is always $2$ and it is not represented in binary. + +#### Sign + +$sign$ can have 2 values: + +1. $1$ if the `sign` bit is `0` +2. $-1$ if the `sign` bit is `1`. + +#### Mantissa + +##### Normal Floating Point Numbers + +$mantissa$ is a positive fractional number whose integer part is $1$, for example +$1.2345 \dots$. The `mantissa` bits represent only the fractional part and the +$mantissa$ value can be calculated as: + +$$mantissa = 1 + \sum_{i=1}^{52} b_{i} \times 2^{-i} = 1 + \frac{b_{1}}{2^{1}} + \frac{b_{2}}{2^{2}} + \dots + \frac{b_{63}}{2^{63}} + \frac{b_{52}}{2^{52}}$$ + +Where $b_{i}$ is: + +1. $0$ if the bit at the position `i - 1` is `0`. +2. $1$ if the bit at the position `i - 1` is `1`. + +##### Denormal Floating Point Numbers + +$mantissa$ is a positive fractional number whose integer part is $0$, for example +$0.12345 \dots$. The `mantissa` bits represent only the fractional part and the +$mantissa$ value can be calculated as: + +$$mantissa = \sum_{i=1}^{52} b_{i} \times 2^{-i} = \frac{b_{1}}{2^{1}} + \frac{b_{2}}{2^{2}} + \dots + \frac{b_{63}}{2^{63}} + \frac{b_{52}}{2^{52}}$$ + +Where $b_{i}$ is: + +1. $0$ if the bit at the position `i - 1` is `0`. +2. $1$ if the bit at the position `i - 1` is `1`. + +#### Exponent + +##### Normal Floating Point Numbers + +Only the following bit sequences are allowed: `00000000001` to `11111111110`. +That is, there must be at least one `0` and one `1` in the exponent bits. + +The actual value of the $exponent$ can be calculated as: + +$$exponent = v - bias$$ + +where $v$ is the value of the binary number in the exponent bits and $bias$ is $1023$. +Considering the restrictions above, the respective minimum and maximum values for the +exponent are: + +1. `00000000001` = $1$, $1 - 1023 = -1022$ +2. `11111111110` = $2046$, $2046 - 1023 = 1023$ + +So, $exponent$ is an integer in the range $\left[-1022, 1023\right]$. + + +##### Denormal Floating Point Numbers + +$exponent$ is always $-1022$. Nevertheless, it is always represented as `00000000000`. + +### Normal and Denormal Floating Point Numbers + +The smallest absolute value a normal floating point number can have is calculated +like this: + +$$1 \times 1.0\dots0 \times 2^{-1022} = 2.2250738585072014 \times 10^{-308}$$ + +Since normal floating point numbers always have a $1$ as the integer part of the +$mantissa$, then smaller values can be achieved by using the smallest possible exponent +( $-1022$ ) and a $0$ in the integer part of the $mantissa$, but significant digits are lost. + +The smallest absolute value a denormal floating point number can have is calculated +like this: + +$$1 \times 2^{-52} \times 2^{-1022} = 5 \times 10^{-324}$$ + +## Zero + +Zero is represented like this: + +* Sign bit: `X` +* Exponent bits: `00000000000` +* Mantissa bits: `0000000000000000000000000000000000000000000000000000` + +where `X` means `0` or `1`. + +## NaN + +There are 2 kinds of NaNs that are represented: + +1. QNaNs (Quiet NaNs): represent the result of indeterminate operations. +2. SNaNs (Signalling NaNs): represent the result of invalid operations. + +### QNaNs + +QNaNs are represented like this: + +* Sign bit: `X` +* Exponent bits: `11111111111` +* Mantissa bits: `1XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` + +where `X` means `0` or `1`. + +### SNaNs + +SNaNs are represented like this: + +* Sign bit: `X` +* Exponent bits: `11111111111` +* Mantissa bits: `0XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1` + +where `X` means `0` or `1`. + +## Infinite + +### Positive Infinite + +Positive infinite is represented like this: + +* Sign bit: `0` +* Exponent bits: `11111111111` +* Mantissa bits: `0000000000000000000000000000000000000000000000000000` + +where `X` means `0` or `1`. + +### Negative Infinite + +Negative infinite is represented like this: + +* Sign bit: `1` +* Exponent bits: `11111111111` +* Mantissa bits: `0000000000000000000000000000000000000000000000000000` + +where `X` means `0` or `1`. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py new file mode 100644 index 0000000000..9503b57c0e --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py @@ -0,0 +1,118 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ctypes import c_double, c_uint64 +from sys import float_info + +# IEEE 754 64-bit floating point numbers use 11 bits for the exponent and 52 +# bits for the mantissa. +MANTISSA_WIDTH = 52 +EXPONENT_WIDTH = 11 + +# This mask is equivalent to 52 "1" bits (there are 13 hexadecimal 4-bit "f"s +# in the mantissa mask, 13 * 4 == 52) or 0xfffffffffffff in hexadecimal. +MANTISSA_MASK = (1 << MANTISSA_WIDTH) - 1 + +# There are 11 bits for the exponent, but the exponent values 0 (11 "0" +# bits) and 2047 (11 "1" bits) have special meanings so the exponent range is +# from 1 to 2046. To calculate the exponent value, 1023 (the bias) is +# subtracted from the exponent, so the exponent value range is from -1022 to +# +1023. +EXPONENT_BIAS = (2 ** (EXPONENT_WIDTH - 1)) - 1 + +# All the exponent mask bits are set to 1 for the 11 exponent bits. +EXPONENT_MASK = ((1 << EXPONENT_WIDTH) - 1) << MANTISSA_WIDTH + +# The sign mask has the first bit set to 1 and the rest to 0. +SIGN_MASK = 1 << (EXPONENT_WIDTH + MANTISSA_WIDTH) + +# For normal floating point numbers, the exponent can have a value in the +# range [-1022, 1023]. +MIN_NORMAL_EXPONENT = -EXPONENT_BIAS + 1 +MAX_NORMAL_EXPONENT = EXPONENT_BIAS + +# The smallest possible normal value is 2.2250738585072014e-308. +# This value is the result of using the smallest possible number in the +# mantissa, 1.0000000000000000000000000000000000000000000000000000 (52 "0"s in +# the fractional part) and a single "1" in the exponent. +# Finally 1 * (2 ** -1022) = 2.2250738585072014e-308. +MIN_NORMAL_VALUE = float_info.min + +# Greatest possible normal value (1.7976931348623157e+308) +# The binary representation of a float in scientific notation uses (for the +# mantissa) one bit for the integer part (which is implicit) and 52 bits for +# the fractional part. Consider a float binary 1.111. It is equal to 1 + 1/2 + +# 1/4 + 1/8. The greatest possible value in the 52-bit binary mantissa would be +# then 1.1111111111111111111111111111111111111111111111111111 (52 "1"s in the +# fractional part) whose decimal value is 1.9999999999999998. Finally, +# 1.9999999999999998 * (2 ** 1023) = 1.7976931348623157e+308. +MAX_NORMAL_VALUE = float_info.max + + +def get_ieee_754_exponent(value: float) -> int: + """ + Gets the exponent of the IEEE 754 representation of a float. + """ + + return ( + ( + # This step gives the integer that corresponds to the IEEE 754 + # representation of a float. For example, consider + # -MAX_NORMAL_VALUE for an example. We choose this value because + # of its binary representation which makes easy to understand the + # subsequent operations. + # + # c_uint64.from_buffer(c_double(-MAX_NORMAL_VALUE)).value == 18442240474082181119 + # bin(18442240474082181119) == '0b1111111111101111111111111111111111111111111111111111111111111111' + # + # The first bit of the previous binary number is the sign bit: 1 (1 means negative, 0 means positive) + # The next 11 bits are the exponent bits: 11111111110 + # The next 52 bits are the mantissa bits: 1111111111111111111111111111111111111111111111111111 + # + # This step isolates the exponent bits, turning every bit outside + # of the exponent field (sign and mantissa bits) to 0. + c_uint64.from_buffer(c_double(value)).value + & EXPONENT_MASK + # For the example this means: + # 18442240474082181119 & EXPONENT_MASK == 9214364837600034816 + # bin(9214364837600034816) == '0b111111111100000000000000000000000000000000000000000000000000000' + # Notice that the previous binary representation does not include + # leading zeroes, so the sign bit is not included since it is a + # zero. + ) + # This step moves the exponent bits to the right, removing the + # mantissa bits that were set to 0 by the previous step. This + # leaves the IEEE 754 exponent value, ready for the next step. + >> MANTISSA_WIDTH + # For the example this means: + # 9214364837600034816 >> MANTISSA_WIDTH == 2046 + # bin(2046) == '0b11111111110' + # As shown above, these are the original 11 bits that correspond to the + # exponent. + # This step subtracts the exponent bias from the IEEE 754 value, + # leaving the actual exponent value. + ) - EXPONENT_BIAS + # For the example this means: + # 2046 - EXPONENT_BIAS == 1023 + # As mentioned in a comment above, the largest value for the exponent is + + +def get_ieee_754_mantissa(value: float) -> int: + return ( + c_uint64.from_buffer(c_double(value)).value + # This step isolates the mantissa bits. There is no need to do any + # bit shifting as the mantissa bits are already the rightmost field + # in an IEEE 754 representation. + & MANTISSA_MASK + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py new file mode 100644 index 0000000000..5abf9238b9 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py @@ -0,0 +1,139 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from math import exp, floor, ldexp, log +from threading import Lock + +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( + Mapping, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( + MappingOverflowError, + MappingUnderflowError, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( + MAX_NORMAL_EXPONENT, + MIN_NORMAL_EXPONENT, + MIN_NORMAL_VALUE, + get_ieee_754_exponent, + get_ieee_754_mantissa, +) + + +class LogarithmMapping(Mapping): + # Reference implementation here: + # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/logarithm/logarithm.go + + _mappings = {} + _mappings_lock = Lock() + + _min_scale = 1 + _max_scale = 20 + + def _get_min_scale(self): + # _min_scale ensures that ExponentMapping is used for zero and negative + # scale values. + return self._min_scale + + def _get_max_scale(self): + # FIXME The Go implementation uses a value of 20 here, find out the + # right value for this implementation, more information here: + # https://github.com/lightstep/otel-launcher-go/blob/c9ca8483be067a39ab306b09060446e7fda65f35/lightstep/sdk/metric/aggregator/histogram/structure/README.md#mapping-function + # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/logarithm/logarithm.go#L32-L45 + return self._max_scale + + def _init(self, scale: int): + # pylint: disable=attribute-defined-outside-init + + super()._init(scale) + + # self._scale_factor is defined as a multiplier because multiplication + # is faster than division. self._scale_factor is defined as: + # index = log(value) * self._scale_factor + # Where: + # index = log(value) / log(base) + # index = log(value) / log(2 ** (2 ** -scale)) + # index = log(value) / ((2 ** -scale) * log(2)) + # index = log(value) * ((1 / log(2)) * (2 ** scale)) + # self._scale_factor = ((1 / log(2)) * (2 ** scale)) + # self._scale_factor = (1 /log(2)) * (2 ** scale) + # self._scale_factor = ldexp(1 / log(2), scale) + # This implementation was copied from a Java prototype. See: + # https://github.com/newrelic-experimental/newrelic-sketch-java/blob/1ce245713603d61ba3a4510f6df930a5479cd3f6/src/main/java/com/newrelic/nrsketch/indexer/LogIndexer.java + # for the equations used here. + self._scale_factor = ldexp(1 / log(2), scale) + + # self._min_normal_lower_boundary_index is the index such that + # base ** index == MIN_NORMAL_VALUE. An exponential histogram bucket + # with this index covers the range + # (MIN_NORMAL_VALUE, MIN_NORMAL_VALUE * base]. One less than this index + # corresponds with the bucket containing values <= MIN_NORMAL_VALUE. + self._min_normal_lower_boundary_index = ( + MIN_NORMAL_EXPONENT << self._scale + ) + + # self._max_normal_lower_boundary_index is the index such that + # base ** index equals the greatest representable lower boundary. An + # exponential histogram bucket with this index covers the range + # ((2 ** 1024) / base, 2 ** 1024], which includes opentelemetry.sdk. + # metrics._internal.exponential_histogram.ieee_754.MAX_NORMAL_VALUE. + # This bucket is incomplete, since the upper boundary cannot be + # represented. One greater than this index corresponds with the bucket + # containing values > 2 ** 1024. + self._max_normal_lower_boundary_index = ( + (MAX_NORMAL_EXPONENT + 1) << self._scale + ) - 1 + + def map_to_index(self, value: float) -> int: + """ + Maps positive floating point values to indexes corresponding to scale. + """ + + # value is subnormal + if value <= MIN_NORMAL_VALUE: + return self._min_normal_lower_boundary_index - 1 + + # value is an exact power of two. + if get_ieee_754_mantissa(value) == 0: + exponent = get_ieee_754_exponent(value) + return (exponent << self._scale) - 1 + + return min( + floor(log(value) * self._scale_factor), + self._max_normal_lower_boundary_index, + ) + + def get_lower_boundary(self, index: int) -> float: + + if index >= self._max_normal_lower_boundary_index: + if index == self._max_normal_lower_boundary_index: + return 2 * exp( + (index - (1 << self._scale)) / self._scale_factor + ) + raise MappingOverflowError() + + if index <= self._min_normal_lower_boundary_index: + if index == self._min_normal_lower_boundary_index: + return MIN_NORMAL_VALUE + if index == self._min_normal_lower_boundary_index - 1: + return ( + exp((index + (1 << self._scale)) / self._scale_factor) / 2 + ) + raise MappingUnderflowError() + + return exp(index / self._scale_factor) + + @property + def scale(self) -> int: + return self._scale diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py new file mode 100644 index 0000000000..ae06d963ab --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py @@ -0,0 +1,395 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from math import inf +from sys import float_info, version_info +from unittest import TestCase +from unittest.mock import patch + +from pytest import mark + +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( + MappingUnderflowError, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.exponent_mapping import ( + ExponentMapping, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( + MAX_NORMAL_EXPONENT, + MAX_NORMAL_VALUE, + MIN_NORMAL_EXPONENT, + MIN_NORMAL_VALUE, +) + +if version_info >= (3, 9): + from math import nextafter + + +def right_boundary(scale: int, index: int) -> float: + result = 2**index + + for _ in range(scale, 0): + result = result * result + + return result + + +class TestExponentMapping(TestCase): + def test_singleton(self): + + self.assertIs(ExponentMapping(-3), ExponentMapping(-3)) + self.assertIsNot(ExponentMapping(-3), ExponentMapping(-5)) + + @patch( + "opentelemetry.sdk.metrics._internal.exponential_histogram.mapping." + "exponent_mapping.ExponentMapping._mappings", + new={}, + ) + @patch( + "opentelemetry.sdk.metrics._internal.exponential_histogram.mapping." + "exponent_mapping.ExponentMapping._init" + ) + def test_init_called_once(self, mock_init): + + ExponentMapping(-3) + ExponentMapping(-3) + + mock_init.assert_called_once() + + def test_exponent_mapping_0(self): + + try: + ExponentMapping(0) + + except Exception as error: + self.fail(f"Unexpected exception raised: {error}") + + def test_exponent_mapping_zero(self): + + exponent_mapping = ExponentMapping(0) + + # This is the equivalent to 1.1 in hexadecimal + hex_1_1 = 1 + (1 / 16) + + # Testing with values near +inf + self.assertEqual( + exponent_mapping.map_to_index(MAX_NORMAL_VALUE), + MAX_NORMAL_EXPONENT, + ) + self.assertEqual(exponent_mapping.map_to_index(MAX_NORMAL_VALUE), 1023) + self.assertEqual(exponent_mapping.map_to_index(2**1023), 1022) + self.assertEqual(exponent_mapping.map_to_index(2**1022), 1021) + self.assertEqual( + exponent_mapping.map_to_index(hex_1_1 * (2**1023)), 1023 + ) + self.assertEqual( + exponent_mapping.map_to_index(hex_1_1 * (2**1022)), 1022 + ) + + # Testing with values near 1 + self.assertEqual(exponent_mapping.map_to_index(4), 1) + self.assertEqual(exponent_mapping.map_to_index(3), 1) + self.assertEqual(exponent_mapping.map_to_index(2), 0) + self.assertEqual(exponent_mapping.map_to_index(1), -1) + self.assertEqual(exponent_mapping.map_to_index(0.75), -1) + self.assertEqual(exponent_mapping.map_to_index(0.51), -1) + self.assertEqual(exponent_mapping.map_to_index(0.5), -2) + self.assertEqual(exponent_mapping.map_to_index(0.26), -2) + self.assertEqual(exponent_mapping.map_to_index(0.25), -3) + self.assertEqual(exponent_mapping.map_to_index(0.126), -3) + self.assertEqual(exponent_mapping.map_to_index(0.125), -4) + + # Testing with values near 0 + self.assertEqual(exponent_mapping.map_to_index(2**-1022), -1023) + self.assertEqual( + exponent_mapping.map_to_index(hex_1_1 * (2**-1022)), -1022 + ) + self.assertEqual(exponent_mapping.map_to_index(2**-1021), -1022) + self.assertEqual( + exponent_mapping.map_to_index(hex_1_1 * (2**-1021)), -1021 + ) + self.assertEqual( + exponent_mapping.map_to_index(2**-1022), MIN_NORMAL_EXPONENT - 1 + ) + self.assertEqual( + exponent_mapping.map_to_index(2**-1021), MIN_NORMAL_EXPONENT + ) + # The smallest subnormal value is 2 ** -1074 = 5e-324. + # This value is also the result of: + # s = 1 + # while s / 2: + # s = s / 2 + # s == 5e-324 + self.assertEqual( + exponent_mapping.map_to_index(2**-1074), MIN_NORMAL_EXPONENT - 1 + ) + + def test_exponent_mapping_min_scale(self): + + exponent_mapping = ExponentMapping(ExponentMapping._min_scale) + self.assertEqual(exponent_mapping.map_to_index(1.000001), 0) + self.assertEqual(exponent_mapping.map_to_index(1), -1) + self.assertEqual(exponent_mapping.map_to_index(float_info.max), 0) + self.assertEqual(exponent_mapping.map_to_index(float_info.min), -1) + + def test_invalid_scale(self): + with self.assertRaises(Exception): + ExponentMapping(1) + + with self.assertRaises(Exception): + ExponentMapping(ExponentMapping._min_scale - 1) + + def test_exponent_mapping_neg_one(self): + exponent_mapping = ExponentMapping(-1) + self.assertEqual(exponent_mapping.map_to_index(17), 2) + self.assertEqual(exponent_mapping.map_to_index(16), 1) + self.assertEqual(exponent_mapping.map_to_index(15), 1) + self.assertEqual(exponent_mapping.map_to_index(9), 1) + self.assertEqual(exponent_mapping.map_to_index(8), 1) + self.assertEqual(exponent_mapping.map_to_index(5), 1) + self.assertEqual(exponent_mapping.map_to_index(4), 0) + self.assertEqual(exponent_mapping.map_to_index(3), 0) + self.assertEqual(exponent_mapping.map_to_index(2), 0) + self.assertEqual(exponent_mapping.map_to_index(1.5), 0) + self.assertEqual(exponent_mapping.map_to_index(1), -1) + self.assertEqual(exponent_mapping.map_to_index(0.75), -1) + self.assertEqual(exponent_mapping.map_to_index(0.5), -1) + self.assertEqual(exponent_mapping.map_to_index(0.25), -2) + self.assertEqual(exponent_mapping.map_to_index(0.20), -2) + self.assertEqual(exponent_mapping.map_to_index(0.13), -2) + self.assertEqual(exponent_mapping.map_to_index(0.125), -2) + self.assertEqual(exponent_mapping.map_to_index(0.10), -2) + self.assertEqual(exponent_mapping.map_to_index(0.0625), -3) + self.assertEqual(exponent_mapping.map_to_index(0.06), -3) + + def test_exponent_mapping_neg_four(self): + exponent_mapping = ExponentMapping(-4) + self.assertEqual(exponent_mapping.map_to_index(float(0x1)), -1) + self.assertEqual(exponent_mapping.map_to_index(float(0x10)), 0) + self.assertEqual(exponent_mapping.map_to_index(float(0x100)), 0) + self.assertEqual(exponent_mapping.map_to_index(float(0x1000)), 0) + self.assertEqual( + exponent_mapping.map_to_index(float(0x10000)), 0 + ) # base == 2 ** 16 + self.assertEqual(exponent_mapping.map_to_index(float(0x100000)), 1) + self.assertEqual(exponent_mapping.map_to_index(float(0x1000000)), 1) + self.assertEqual(exponent_mapping.map_to_index(float(0x10000000)), 1) + self.assertEqual( + exponent_mapping.map_to_index(float(0x100000000)), 1 + ) # base == 2 ** 32 + + self.assertEqual(exponent_mapping.map_to_index(float(0x1000000000)), 2) + self.assertEqual( + exponent_mapping.map_to_index(float(0x10000000000)), 2 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x100000000000)), 2 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x1000000000000)), 2 + ) # base == 2 ** 48 + + self.assertEqual( + exponent_mapping.map_to_index(float(0x10000000000000)), 3 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x100000000000000)), 3 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x1000000000000000)), 3 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x10000000000000000)), 3 + ) # base == 2 ** 64 + + self.assertEqual( + exponent_mapping.map_to_index(float(0x100000000000000000)), 4 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x1000000000000000000)), 4 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x10000000000000000000)), 4 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x100000000000000000000)), 4 + ) # base == 2 ** 80 + self.assertEqual( + exponent_mapping.map_to_index(float(0x1000000000000000000000)), 5 + ) + + self.assertEqual(exponent_mapping.map_to_index(1 / float(0x1)), -1) + self.assertEqual(exponent_mapping.map_to_index(1 / float(0x10)), -1) + self.assertEqual(exponent_mapping.map_to_index(1 / float(0x100)), -1) + self.assertEqual(exponent_mapping.map_to_index(1 / float(0x1000)), -1) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x10000)), -2 + ) # base == 2 ** -16 + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x100000)), -2 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x1000000)), -2 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x10000000)), -2 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x100000000)), -3 + ) # base == 2 ** -32 + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x1000000000)), -3 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x10000000000)), -3 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x100000000000)), -3 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x1000000000000)), -4 + ) # base == 2 ** -32 + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x10000000000000)), -4 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x100000000000000)), -4 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x1000000000000000)), -4 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x10000000000000000)), -5 + ) # base == 2 ** -64 + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x100000000000000000)), -5 + ) + + self.assertEqual(exponent_mapping.map_to_index(float_info.max), 63) + self.assertEqual(exponent_mapping.map_to_index(2**1023), 63) + self.assertEqual(exponent_mapping.map_to_index(2**1019), 63) + self.assertEqual(exponent_mapping.map_to_index(2**1009), 63) + self.assertEqual(exponent_mapping.map_to_index(2**1008), 62) + self.assertEqual(exponent_mapping.map_to_index(2**1007), 62) + self.assertEqual(exponent_mapping.map_to_index(2**1000), 62) + self.assertEqual(exponent_mapping.map_to_index(2**993), 62) + self.assertEqual(exponent_mapping.map_to_index(2**992), 61) + self.assertEqual(exponent_mapping.map_to_index(2**991), 61) + + self.assertEqual(exponent_mapping.map_to_index(2**-1074), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1073), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1072), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1057), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1056), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1041), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1040), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1025), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1024), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1023), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1022), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1009), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1008), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1007), -63) + self.assertEqual(exponent_mapping.map_to_index(2**-993), -63) + self.assertEqual(exponent_mapping.map_to_index(2**-992), -63) + self.assertEqual(exponent_mapping.map_to_index(2**-991), -62) + self.assertEqual(exponent_mapping.map_to_index(2**-977), -62) + self.assertEqual(exponent_mapping.map_to_index(2**-976), -62) + self.assertEqual(exponent_mapping.map_to_index(2**-975), -61) + + def test_exponent_index_max(self): + + for scale in range( + ExponentMapping._min_scale, ExponentMapping._max_scale + ): + exponent_mapping = ExponentMapping(scale) + + index = exponent_mapping.map_to_index(MAX_NORMAL_VALUE) + + max_index = ((MAX_NORMAL_EXPONENT + 1) >> -scale) - 1 + + self.assertEqual(index, max_index) + + boundary = exponent_mapping.get_lower_boundary(index) + + self.assertEqual(boundary, right_boundary(scale, max_index)) + + with self.assertRaises(Exception): + exponent_mapping.get_lower_boundary(index + 1) + + @mark.skipif( + version_info < (3, 9), + reason="math.nextafter is only available for Python >= 3.9", + ) + def test_exponent_index_min(self): + for scale in range( + ExponentMapping._min_scale, ExponentMapping._max_scale + 1 + ): + exponent_mapping = ExponentMapping(scale) + + min_index = exponent_mapping.map_to_index(MIN_NORMAL_VALUE) + boundary = exponent_mapping.get_lower_boundary(min_index) + + correct_min_index = MIN_NORMAL_EXPONENT >> -scale + + if MIN_NORMAL_EXPONENT % (1 << -scale) == 0: + correct_min_index -= 1 + + # We do not check for correct_min_index to be greater than the + # smallest integer because the smallest integer in Python is -inf. + + self.assertEqual(correct_min_index, min_index) + + correct_boundary = right_boundary(scale, correct_min_index) + + self.assertEqual(correct_boundary, boundary) + self.assertGreater( + right_boundary(scale, correct_min_index + 1), boundary + ) + + self.assertEqual( + correct_min_index, + exponent_mapping.map_to_index(MIN_NORMAL_VALUE / 2), + ) + self.assertEqual( + correct_min_index, + exponent_mapping.map_to_index(MIN_NORMAL_VALUE / 3), + ) + self.assertEqual( + correct_min_index, + exponent_mapping.map_to_index(MIN_NORMAL_VALUE / 100), + ) + self.assertEqual( + correct_min_index, exponent_mapping.map_to_index(2**-1050) + ) + self.assertEqual( + correct_min_index, exponent_mapping.map_to_index(2**-1073) + ) + self.assertEqual( + correct_min_index, + exponent_mapping.map_to_index(1.1 * (2**-1073)), + ) + self.assertEqual( + correct_min_index, exponent_mapping.map_to_index(2**-1074) + ) + + with self.assertRaises(MappingUnderflowError): + exponent_mapping.get_lower_boundary(min_index - 1) + + self.assertEqual( + exponent_mapping.map_to_index( + nextafter(MIN_NORMAL_VALUE, inf) + ), + MIN_NORMAL_EXPONENT >> -scale, + ) diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py new file mode 100644 index 0000000000..1fd18845bb --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py @@ -0,0 +1,241 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from math import sqrt +from unittest import TestCase +from unittest.mock import patch + +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( + MappingOverflowError, + MappingUnderflowError, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( + MAX_NORMAL_EXPONENT, + MAX_NORMAL_VALUE, + MIN_NORMAL_EXPONENT, + MIN_NORMAL_VALUE, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.logarithm_mapping import ( + LogarithmMapping, +) + + +def left_boundary(scale: int, index: int) -> float: + + # This is implemented in this way to avoid using a third-party bigfloat + # package. The Go implementation uses a bigfloat package that is part of + # their standard library. The assumption here is that the smallest float + # available in Python is 2 ** -1022 (from sys.float_info.min). + while scale > 0: + if index < -1022: + index /= 2 + scale -= 1 + else: + break + + result = 2**index + + for _ in range(scale, 0, -1): + result = sqrt(result) + + return result + + +class TestLogarithmMapping(TestCase): + def assertInEpsilon(self, first, second, epsilon): + self.assertLessEqual(first, (second * (1 + epsilon))) + self.assertGreaterEqual(first, (second * (1 - epsilon))) + + @patch( + "opentelemetry.sdk.metrics._internal.exponential_histogram.mapping." + "logarithm_mapping.LogarithmMapping._mappings", + new={}, + ) + @patch( + "opentelemetry.sdk.metrics._internal.exponential_histogram.mapping." + "logarithm_mapping.LogarithmMapping._init" + ) + def test_init_called_once(self, mock_init): + + LogarithmMapping(3) + LogarithmMapping(3) + + mock_init.assert_called_once() + + def test_invalid_scale(self): + with self.assertRaises(Exception): + LogarithmMapping(-1) + + def test_logarithm_mapping_scale_one(self): + + # The exponentiation factor for this logarithm exponent histogram + # mapping is square_root(2). + # Scale 1 means 1 division between every power of two, having + # a factor sqare_root(2) times the lower boundary. + logarithm_exponent_histogram_mapping = LogarithmMapping(1) + + self.assertEqual(logarithm_exponent_histogram_mapping.scale, 1) + + # Note: Do not test exact boundaries, with the exception of + # 1, because we expect errors in that case (e.g., + # MapToIndex(8) returns 5, an off-by-one. See the following + # test. + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(15), 7 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(9), 6 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(7), 5 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(5), 4 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(3), 3 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(2.5), 2 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(1.5), 1 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(1.2), 0 + ) + # This one is actually an exact test + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(1), -1 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(0.75), -1 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(0.55), -2 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(0.45), -3 + ) + + def test_logarithm_boundary(self): + + for scale in [1, 2, 3, 4, 10, 15]: + logarithm_exponent_histogram_mapping = LogarithmMapping(scale) + + for index in [-100, -10, -1, 0, 1, 10, 100]: + + lower_boundary = ( + logarithm_exponent_histogram_mapping.get_lower_boundary( + index + ) + ) + + mapped_index = ( + logarithm_exponent_histogram_mapping.map_to_index( + lower_boundary + ) + ) + + self.assertLessEqual(index - 1, mapped_index) + self.assertGreaterEqual(index, mapped_index) + + self.assertInEpsilon( + lower_boundary, left_boundary(scale, index), 1e-9 + ) + + def test_logarithm_index_max(self): + + for scale in range( + LogarithmMapping._min_scale, LogarithmMapping._max_scale + 1 + ): + logarithm_mapping = LogarithmMapping(scale) + + index = logarithm_mapping.map_to_index(MAX_NORMAL_VALUE) + + max_index = ((MAX_NORMAL_EXPONENT + 1) << scale) - 1 + + # We do not check for max_index to be lesser than the + # greatest integer because the greatest integer in Python is inf. + + self.assertEqual(index, max_index) + + boundary = logarithm_mapping.get_lower_boundary(index) + + base = logarithm_mapping.get_lower_boundary(1) + + self.assertLess(boundary, MAX_NORMAL_VALUE) + + self.assertInEpsilon( + (MAX_NORMAL_VALUE - boundary) / boundary, base - 1, 1e-6 + ) + + with self.assertRaises(MappingOverflowError): + logarithm_mapping.get_lower_boundary(index + 1) + + with self.assertRaises(MappingOverflowError): + logarithm_mapping.get_lower_boundary(index + 2) + + def test_logarithm_index_min(self): + for scale in range( + LogarithmMapping._min_scale, LogarithmMapping._max_scale + 1 + ): + logarithm_mapping = LogarithmMapping(scale) + + min_index = logarithm_mapping.map_to_index(MIN_NORMAL_VALUE) + + correct_min_index = (MIN_NORMAL_EXPONENT << scale) - 1 + self.assertEqual(min_index, correct_min_index) + + correct_mapped = left_boundary(scale, correct_min_index) + self.assertLess(correct_mapped, MIN_NORMAL_VALUE) + + correct_mapped_upper = left_boundary(scale, correct_min_index + 1) + self.assertEqual(correct_mapped_upper, MIN_NORMAL_VALUE) + + mapped = logarithm_mapping.get_lower_boundary(min_index + 1) + + self.assertInEpsilon(mapped, MIN_NORMAL_VALUE, 1e-6) + + self.assertEqual( + logarithm_mapping.map_to_index(MIN_NORMAL_VALUE / 2), + correct_min_index, + ) + self.assertEqual( + logarithm_mapping.map_to_index(MIN_NORMAL_VALUE / 3), + correct_min_index, + ) + self.assertEqual( + logarithm_mapping.map_to_index(MIN_NORMAL_VALUE / 100), + correct_min_index, + ) + self.assertEqual( + logarithm_mapping.map_to_index(2**-1050), correct_min_index + ) + self.assertEqual( + logarithm_mapping.map_to_index(2**-1073), correct_min_index + ) + self.assertEqual( + logarithm_mapping.map_to_index(1.1 * 2**-1073), + correct_min_index, + ) + self.assertEqual( + logarithm_mapping.map_to_index(2**-1074), correct_min_index + ) + + mapped_lower = logarithm_mapping.get_lower_boundary(min_index) + self.assertInEpsilon(correct_mapped, mapped_lower, 1e-6) + + with self.assertRaises(MappingUnderflowError): + logarithm_mapping.get_lower_boundary(min_index - 1) From 3132a56feac0998d2cf1342f5cde8ca003d73b14 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Tue, 1 Nov 2022 13:34:14 -0700 Subject: [PATCH 1339/1517] Automated release workflow (#2649) --- .github/scripts/update-version.sh | 6 + .../scripts/use-cla-approved-github-bot.sh | 4 + .github/workflows/backport.yml | 43 ++++ .github/workflows/changelog.yml | 4 +- .github/workflows/prepare-patch-release.yml | 73 +++++++ .github/workflows/prepare-release-branch.yml | 177 +++++++++++++++ .github/workflows/publish.yml | 37 ---- .github/workflows/release.yml | 205 ++++++++++++++++++ CHANGELOG.md | 89 ++++---- RELEASING.md | 177 ++++++++------- eachdist.ini | 4 +- .../exporter/jaeger/proto/grpc/version.py | 2 +- .../exporter/jaeger/thrift/version.py | 2 +- .../pyproject.toml | 4 +- .../opentelemetry/exporter/jaeger/version.py | 2 +- .../exporter/opencensus/version.py | 2 +- .../pyproject.toml | 2 +- .../exporter/otlp/proto/grpc/version.py | 2 +- .../pyproject.toml | 2 +- .../exporter/otlp/proto/http/version.py | 2 +- .../pyproject.toml | 4 +- .../opentelemetry/exporter/otlp/version.py | 2 +- .../exporter/prometheus/version.py | 2 +- .../exporter/zipkin/json/version.py | 2 +- .../pyproject.toml | 2 +- .../exporter/zipkin/proto/http/version.py | 2 +- .../pyproject.toml | 4 +- .../opentelemetry/exporter/zipkin/version.py | 2 +- .../src/opentelemetry/version.py | 2 +- .../src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/pyproject.toml | 4 +- .../src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../opentelemetry/propagators/b3/version.py | 2 +- .../propagators/jaeger/version.py | 2 +- scripts/eachdist.py | 52 ----- scripts/prepare_release.sh | 34 --- .../pyproject.toml | 2 +- .../shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/pyproject.toml | 4 +- .../src/opentelemetry/test/version.py | 2 +- 41 files changed, 676 insertions(+), 295 deletions(-) create mode 100755 .github/scripts/update-version.sh create mode 100755 .github/scripts/use-cla-approved-github-bot.sh create mode 100644 .github/workflows/backport.yml create mode 100644 .github/workflows/prepare-patch-release.yml create mode 100644 .github/workflows/prepare-release-branch.yml delete mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yml delete mode 100755 scripts/prepare_release.sh diff --git a/.github/scripts/update-version.sh b/.github/scripts/update-version.sh new file mode 100755 index 0000000000..ba1bd22955 --- /dev/null +++ b/.github/scripts/update-version.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +sed -i "/\[stable\]/{n;s/version=.*/version=$1/}" eachdist.ini +sed -i "/\[prerelease\]/{n;s/version=.*/version=$2/}" eachdist.ini + +./scripts/eachdist.py update_versions --versions stable,prerelease diff --git a/.github/scripts/use-cla-approved-github-bot.sh b/.github/scripts/use-cla-approved-github-bot.sh new file mode 100755 index 0000000000..a4c68b0e30 --- /dev/null +++ b/.github/scripts/use-cla-approved-github-bot.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e + +git config user.name opentelemetrybot +git config user.email 107717825+opentelemetrybot@users.noreply.github.com diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000000..a9a7fdab6d --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,43 @@ +name: Backport +on: + workflow_dispatch: + inputs: + number: + description: "The pull request # to backport" + required: true + +jobs: + backport: + runs-on: ubuntu-latest + steps: + - run: | + if [[ ! $GITHUB_REF_NAME =~ ^release/v[0-9]+\.[0-9]+\.x-0\.[0-9]+bx$ ]]; then + echo this workflow should only be run against long-term release branches + exit 1 + fi + + - uses: actions/checkout@v3 + with: + # history is needed to run git cherry-pick below + fetch-depth: 0 + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - name: Create pull request + env: + NUMBER: ${{ github.event.inputs.number }} + # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows + GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + run: | + commit=$(gh pr view $NUMBER --json mergeCommit --jq .mergeCommit.oid) + title=$(gh pr view $NUMBER --json title --jq .title) + + branch="opentelemetrybot/backport-${NUMBER}-to-${GITHUB_REF_NAME//\//-}" + + git cherry-pick $commit + git push origin HEAD:$branch + gh pr create --title "[$GITHUB_REF_NAME] $title" \ + --body "Clean cherry-pick of #$NUMBER to the \`$GITHUB_REF_NAME\` branch." \ + --head $branch \ + --base $GITHUB_REF_NAME diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 11e1a61fc5..5238e01c4b 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -13,7 +13,9 @@ on: jobs: changelog: runs-on: ubuntu-latest - if: "!contains(github.event.pull_request.labels.*.name, 'Skip Changelog')" + if: | + !contains(github.event.pull_request.labels.*.name, 'Skip Changelog') + && github.actor != 'opentelemetrybot' steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/prepare-patch-release.yml b/.github/workflows/prepare-patch-release.yml new file mode 100644 index 0000000000..c215b7c9e4 --- /dev/null +++ b/.github/workflows/prepare-patch-release.yml @@ -0,0 +1,73 @@ +name: Prepare patch release +on: + workflow_dispatch: + +jobs: + prepare-patch-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - run: | + if [[ ! $GITHUB_REF_NAME =~ ^release/v[0-9]+\.[0-9]+\.x-0\.[0-9]+bx$ ]]; then + echo this workflow should only be run against long-term release branches + exit 1 + fi + + if ! grep --quiet "^## Unreleased$" CHANGELOG.md; then + echo the change log is missing an \"Unreleased\" section + exit 1 + fi + + - name: Set environment variables + run: | + stable_version=$(./scripts/eachdist.py version --mode stable) + unstable_version=$(./scripts/eachdist.py version --mode prerelease) + + if [[ $stable_version =~ ^([0-9]+\.[0-9]+)\.([0-9]+)$ ]]; then + stable_major_minor="${BASH_REMATCH[1]}" + stable_patch="${BASH_REMATCH[2]}" + else + echo "unexpected stable_version: $stable_version" + exit 1 + fi + + if [[ $unstable_version =~ ^0\.([0-9]+)b([0-9]+)$ ]]; then + unstable_minor="${BASH_REMATCH[1]}" + unstable_patch="${BASH_REMATCH[2]}" + else + echo "unexpected unstable_version: $unstable_version" + exit 1 + fi + + stable_version="$stable_major_minor.$((stable_patch + 1))" + unstable_version="0.${unstable_minor}b$((unstable_patch + 1))" + + echo "STABLE_VERSION=$stable_version" >> $GITHUB_ENV + echo "UNSTABLE_VERSION=$unstable_version" >> $GITHUB_ENV + + - name: Update version + run: .github/scripts/update-version.sh $STABLE_VERSION $UNSTABLE_VERSION + + - name: Update the change log with the approximate release date + run: | + date=$(date "+%Y-%m-%d") + sed -Ei "s/^## Unreleased$/## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/" CHANGELOG.md + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - name: Create pull request + env: + # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows + GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + run: | + message="Prepare release ${STABLE_VERSION}/${UNSTABLE_VERSION}" + branch="opentelemetrybot/prepare-release-${STABLE_VERSION}-${UNSTABLE_VERSION}" + + git commit -a -m "$message" + git push origin HEAD:$branch + gh pr create --title "[$GITHUB_REF_NAME] $message" \ + --body "$message." \ + --head $branch \ + --base $GITHUB_REF_NAME diff --git a/.github/workflows/prepare-release-branch.yml b/.github/workflows/prepare-release-branch.yml new file mode 100644 index 0000000000..c77a96c06a --- /dev/null +++ b/.github/workflows/prepare-release-branch.yml @@ -0,0 +1,177 @@ +name: Prepare release branch +on: + workflow_dispatch: + inputs: + prerelease_version: + description: "Pre-release version number? (e.g. 1.9.0rc2)" + required: false + +jobs: + prereqs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Verify prerequisites + env: + PRERELEASE_VERSION: ${{ github.event.inputs.prerelease_version }} + run: | + if [[ $GITHUB_REF_NAME != main ]]; then + echo this workflow should only be run against main + exit 1 + fi + + if ! grep --quiet "^## Unreleased$" CHANGELOG.md; then + echo the change log is missing an \"Unreleased\" section + exit 1 + fi + + if [[ ! -z $PRERELEASE_VERSION ]]; then + stable_version=$(./scripts/eachdist.py version --mode stable) + stable_version=${stable_version//.dev/} + if [[ $PRERELEASE_VERSION != ${stable_version}* ]]; then + echo "$PRERELEASE_VERSION is not a prerelease for the version on main ($stable_version)" + exit 1 + fi + fi + + create-pull-request-against-release-branch: + runs-on: ubuntu-latest + needs: prereqs + steps: + - uses: actions/checkout@v3 + + - name: Create release branch + env: + PRERELEASE_VERSION: ${{ github.event.inputs.prerelease_version }} + run: | + if [[ -z $PRERELEASE_VERSION ]]; then + stable_version=$(./scripts/eachdist.py version --mode stable) + stable_version=${stable_version//.dev/} + else + stable_version=$PRERELEASE_VERSION + fi + + unstable_version=$(./scripts/eachdist.py version --mode prerelease) + unstable_version=${unstable_version//.dev/} + + if [[ $stable_version =~ ^([0-9]+)\.([0-9]+)\.0$ ]]; then + stable_version_branch_part=$(echo $stable_version | sed -E 's/([0-9]+)\.([0-9]+)\.0/\1.\2.x/') + unstable_version_branch_part=$(echo $unstable_version | sed -E 's/0\.([0-9]+)b0/0.\1bx/') + release_branch_name="release/v${stable_version_branch_part}-${unstable_version_branch_part}" + elif [[ $stable_version =~ ^([0-9]+)\.([0-9]+)\.0 ]]; then + # pre-release version, e.g. 1.9.0rc2 + release_branch_name="release/v$stable_version-$unstable_version" + else + echo "unexpected version: $stable_version" + exit 1 + fi + + git push origin HEAD:$release_branch_name + + echo "STABLE_VERSION=$stable_version" >> $GITHUB_ENV + echo "UNSTABLE_VERSION=$unstable_version" >> $GITHUB_ENV + echo "RELEASE_BRANCH_NAME=$release_branch_name" >> $GITHUB_ENV + + - name: Update version + run: .github/scripts/update-version.sh $STABLE_VERSION $UNSTABLE_VERSION + + - name: Update the change log with the approximate release date + run: | + date=$(date "+%Y-%m-%d") + sed -Ei "s/^## Unreleased$/## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/" CHANGELOG.md + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - name: Create pull request against the release branch + env: + # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows + GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + run: | + message="Prepare release ${STABLE_VERSION}/${UNSTABLE_VERSION}" + branch="opentelemetrybot/prepare-release-${STABLE_VERSION}-${UNSTABLE_VERSION}" + + git commit -a -m "$message" + git push origin HEAD:$branch + gh pr create --title "[$RELEASE_BRANCH_NAME] $message" \ + --body "$message." \ + --head $branch \ + --base $RELEASE_BRANCH_NAME + + create-pull-request-against-main: + runs-on: ubuntu-latest + needs: prereqs + steps: + - uses: actions/checkout@v3 + + - name: Set environment variables + env: + PRERELEASE_VERSION: ${{ github.event.inputs.prerelease_version }} + run: | + if [[ -z $PRERELEASE_VERSION ]]; then + stable_version=$(./scripts/eachdist.py version --mode stable) + stable_version=${stable_version//.dev/} + else + stable_version=$PRERELEASE_VERSION + fi + + unstable_version=$(./scripts/eachdist.py version --mode prerelease) + unstable_version=${unstable_version//.dev/} + + if [[ $stable_version =~ ^([0-9]+)\.([0-9]+)\.0$ ]]; then + stable_major="${BASH_REMATCH[1]}" + stable_minor="${BASH_REMATCH[2]}" + stable_next_version="$stable_major.$((stable_minor + 1)).0" + elif [[ $stable_version =~ ^([0-9]+)\.([0-9]+)\.0 ]]; then + # pre-release version, e.g. 1.9.0rc2 + stable_major="${BASH_REMATCH[1]}" + stable_minor="${BASH_REMATCH[2]}" + stable_next_version="$stable_major.$stable_minor.0" + else + echo "unexpected stable_version: $stable_version" + exit 1 + fi + + if [[ $unstable_version =~ ^0\.([0-9]+)b[0-9]+$ ]]; then + unstable_minor="${BASH_REMATCH[1]}" + else + echo "unexpected unstable_version: $unstable_version" + exit 1 + fi + + unstable_next_version="0.$((unstable_minor + 1))b0" + + echo "STABLE_VERSION=${stable_version}" >> $GITHUB_ENV + echo "STABLE_NEXT_VERSION=${stable_next_version}.dev" >> $GITHUB_ENV + + echo "UNSTABLE_VERSION=${unstable_version}" >> $GITHUB_ENV + echo "UNSTABLE_NEXT_VERSION=${unstable_next_version}.dev" >> $GITHUB_ENV + + - name: Update version + run: .github/scripts/update-version.sh $STABLE_NEXT_VERSION $UNSTABLE_NEXT_VERSION + + - name: Update the change log on main + run: | + # the actual release date on main will be updated at the end of the release workflow + date=$(date "+%Y-%m-%d") + sed -Ei "s/^## Unreleased$/## Unreleased\n\n## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/" CHANGELOG.md + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - name: Create pull request against main + env: + # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows + GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + run: | + message="Update version to ${STABLE_NEXT_VERSION}/${UNSTABLE_NEXT_VERSION}" + body="Update version to \`${STABLE_NEXT_VERSION}/${UNSTABLE_NEXT_VERSION}\`." + branch="opentelemetrybot/update-version-to-${STABLE_NEXT_VERSION}-${UNSTABLE_NEXT_VERSION}" + + git commit -a -m "$message" + git push origin HEAD:$branch + gh pr create --title "$message" \ + --body "$body" \ + --head $branch \ + --base main diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 0921357cb3..0000000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Publish - -on: - release: - types: [published] - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions/setup-python@v1 - with: - python-version: '3.7' - - name: Build wheels - run: ./scripts/build.sh - - name: Install twine - run: | - pip install twine - # The step below publishes to testpypi in order to catch any issues - # with the package configuration that would cause a failure to upload - # to pypi. One example of such a failure is if a classifier is - # rejected by pypi (e.g "3 - Beta"). This would cause a failure during the - # middle of the package upload causing the action to fail, and certain packages - # might have already been updated, this would be bad. - - name: Publish to TestPyPI - env: - TWINE_USERNAME: '__token__' - TWINE_PASSWORD: ${{ secrets.test_pypi_token }} - run: | - twine upload --repository testpypi --skip-existing --verbose dist/* - - name: Publish to PyPI - env: - TWINE_USERNAME: '__token__' - TWINE_PASSWORD: ${{ secrets.pypi_password }} - run: | - twine upload --skip-existing --verbose dist/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..bd822518f6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,205 @@ +name: Release +on: + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + steps: + - run: | + if [[ $GITHUB_REF_NAME != release/* ]]; then + echo this workflow should only be run against release branches + exit 1 + fi + + - uses: actions/checkout@v3 + + - name: Set environment variables + run: | + stable_version=$(./scripts/eachdist.py version --mode stable) + unstable_version=$(./scripts/eachdist.py version --mode prerelease) + + if [[ $stable_version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + stable_major="${BASH_REMATCH[1]}" + stable_minor="${BASH_REMATCH[2]}" + stable_patch="${BASH_REMATCH[3]}" + else + echo "unexpected stable_version: $stable_version" + exit 1 + fi + if [[ $stable_patch != 0 ]]; then + if [[ $unstable_version =~ ^0\.([0-9]+)b([0-9]+)$ ]]; then + unstable_minor="${BASH_REMATCH[1]}" + unstable_patch="${BASH_REMATCH[2]}" + else + echo "unexpected unstable_version: $unstable_version" + exit 1 + fi + if [[ $unstable_patch != 0 ]]; then + prior_version_when_patch="$stable_major.$stable_minor.$((stable_patch - 1))/0.${unstable_minor}b$((unstable_patch - 1))" + fi + fi + + echo "STABLE_VERSION=$stable_version" >> $GITHUB_ENV + echo "UNSTABLE_VERSION=$unstable_version" >> $GITHUB_ENV + + echo "PRIOR_VERSION_WHEN_PATCH=$prior_version_when_patch" >> $GITHUB_ENV + + # check out main branch to verify there won't be problems with merging the change log + # at the end of this workflow + - uses: actions/checkout@v3 + with: + ref: main + + - run: | + if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then + # not making a patch release + if ! grep --quiet "^## Version ${STABLE_VERSION}/${UNSTABLE_VERSION} " CHANGELOG.md; then + echo the pull request generated by prepare-release-branch.yml needs to be merged first + exit 1 + fi + fi + + # back to the release branch + - uses: actions/checkout@v3 + + # next few steps publish to pypi + - uses: actions/setup-python@v1 + with: + python-version: '3.7' + + - name: Build wheels + run: ./scripts/build.sh + + - name: Install twine + run: | + pip install twine + + # The step below publishes to testpypi in order to catch any issues + # with the package configuration that would cause a failure to upload + # to pypi. One example of such a failure is if a classifier is + # rejected by pypi (e.g "3 - Beta"). This would cause a failure during the + # middle of the package upload causing the action to fail, and certain packages + # might have already been updated, this would be bad. + - name: Publish to TestPyPI + env: + TWINE_USERNAME: '__token__' + TWINE_PASSWORD: ${{ secrets.test_pypi_token }} + run: | + twine upload --repository testpypi --skip-existing --verbose dist/* + + - name: Publish to PyPI + env: + TWINE_USERNAME: '__token__' + TWINE_PASSWORD: ${{ secrets.pypi_password }} + run: | + twine upload --skip-existing --verbose dist/* + + - name: Generate release notes + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # conditional block not indented because of the heredoc + if [[ ! -z $PRIOR_VERSION_WHEN_PATCH ]]; then + cat > /tmp/release-notes.txt << EOF + This is a patch release on the previous $PRIOR_VERSION_WHEN_PATCH release, fixing the issue(s) below. + + EOF + fi + + # CHANGELOG_SECTION.md is also used at the end of the release workflow + # for copying the change log updates to main + sed -n "0,/^## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} /d;/^## Version /q;p" CHANGELOG.md \ + > /tmp/CHANGELOG_SECTION.md + + # the complex perl regex is needed because markdown docs render newlines as soft wraps + # while release notes render them as line breaks + perl -0pe 's/(?> /tmp/release-notes.txt + + - name: Create GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create --target $GITHUB_REF_NAME \ + --title "Version ${STABLE_VERSION}/${UNSTABLE_VERSION}" \ + --notes-file /tmp/release-notes.txt \ + --discussion-category announcements \ + v$STABLE_VERSION + + - uses: actions/checkout@v3 + with: + # the step below is creating a pull request against main + ref: main + + - name: Copy change log updates to main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then + # this was not a patch release, so the version exists already in the CHANGELOG.md + + # update the release date + date=$(gh release view v$STABLE_VERSION --json publishedAt --jq .publishedAt | sed 's/T.*//') + sed -Ei "s/## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} .*/## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/" CHANGELOG.md + + # the entries are copied over from the release branch to support workflows + # where change log entries may be updated after preparing the release branch + + # copy the portion above the release, up to and including the heading + sed -n "0,/^## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/p" CHANGELOG.md > /tmp/CHANGELOG.md + + # copy the release notes + cat /tmp/CHANGELOG_SECTION.md >> /tmp/CHANGELOG.md + + # copy the portion below the release + sed -n "0,/^## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} /d;0,/^## Version /{/^## Version/!d};p" CHANGELOG.md \ + >> /tmp/CHANGELOG.md + + # update the real CHANGELOG.md + cp /tmp/CHANGELOG.md CHANGELOG.md + else + # this was a patch release, so the version does not exist already in the CHANGELOG.md + + # copy the portion above the top-most release, not including the heading + sed -n "0,/^## Version /{ /^## Version /!p }" CHANGELOG.md > /tmp/CHANGELOG.md + + # add the heading + date=$(gh release view v$STABLE_VERSION --json publishedAt --jq .publishedAt | sed 's/T.*//') + echo "## Version ${STABLE_VERSION}/${UNSTABLE_VERSION} ($date)" >> /tmp/CHANGELOG.md + + # copy the release notes + cat /tmp/CHANGELOG_SECTION.md >> /tmp/CHANGELOG.md + + # copy the portion starting from the top-most release + sed -n "/^## Version /,\$p" CHANGELOG.md >> /tmp/CHANGELOG.md + + # update the real CHANGELOG.md + cp /tmp/CHANGELOG.md CHANGELOG.md + fi + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - name: Create pull request against main + env: + # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows + GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + run: | + message="Copy change log updates from $GITHUB_REF_NAME" + body="Copy log updates from \`$GITHUB_REF_NAME\`." + branch="opentelemetrybot/copy-change-log-updates-from-${GITHUB_REF_NAME//\//-}" + + if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then + if git diff --quiet; then + echo there are no updates needed to the change log on main, not creating pull request + exit 0 # success + fi + fi + + git commit -a -m "$message" + git push origin HEAD:$branch + gh pr create --title "$message" \ + --body "$body" \ + --head $branch \ + --base main diff --git a/CHANGELOG.md b/CHANGELOG.md index 27a8b61a8f..1aebb9fbe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0...HEAD) +## Unreleased - Add logarithm and exponent mappings ([#2960](https://github.com/open-telemetry/opentelemetry-python/pull/2960)) @@ -28,8 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix a bug with exporter retries for with newer versions of the backoff library ([#2980](https://github.com/open-telemetry/opentelemetry-python/pull/2980)) -## [1.13.0-0.34b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.13.0) - 2022-09-26 - +## Version 1.13.0/0.34b0 (2022-09-26) - Add a configurable max_export_batch_size to the gRPC metrics exporter ([#2809](https://github.com/open-telemetry/opentelemetry-python/pull/2809)) @@ -52,7 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add force_flush to span exporters ([#2919](https://github.com/open-telemetry/opentelemetry-python/pull/2919)) -## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0) - 2022-08-08 +## Version 1.12.0/0.33b0 (2022-08-08) - Add `force_flush` method to metrics exporter ([#2852](https://github.com/open-telemetry/opentelemetry-python/pull/2852)) @@ -72,7 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Instrument instances are always created through a Meter ([#2844](https://github.com/open-telemetry/opentelemetry-python/pull/2844)) -## [1.12.0rc2-0.32b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc2) - 2022-07-04 +## Version 1.12.0rc2/0.32b0 (2022-07-04) - Fix instrument name and unit regexes ([#2796](https://github.com/open-telemetry/opentelemetry-python/pull/2796)) @@ -116,8 +115,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 to the API reference documentation ([#2785](https://github.com/open-telemetry/opentelemetry-python/pull/2785)) - -## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1) - 2022-05-17 +## Version 1.12.0rc1/0.31b0 (2022-05-17) - Fix LoggingHandler to handle LogRecord with exc_info=False ([#2690](https://github.com/open-telemetry/opentelemetry-python/pull/2690)) @@ -140,7 +138,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Move Metrics API behind internal package ([#2651](https://github.com/open-telemetry/opentelemetry-python/pull/2651)) -## [1.11.1-0.30b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.1) - 2022-04-21 +## Version 1.11.1/0.30b1 (2022-04-21) - Add parameter to MetricReader constructor to select aggregation per instrument kind ([#2638](https://github.com/open-telemetry/opentelemetry-python/pull/2638)) @@ -156,7 +154,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Deprecate InstrumentationLibraryInfo and Add InstrumentationScope ([#2583](https://github.com/open-telemetry/opentelemetry-python/pull/2583)) -## [1.11.0-0.30b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.0) - 2022-04-18 +## Version 1.11.0/0.30b0 (2022-04-18) - Rename API Measurement for async instruments to Observation ([#2617](https://github.com/open-telemetry/opentelemetry-python/pull/2617)) @@ -190,7 +188,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update opentelemetry-proto to v0.16.0 ([#2619](https://github.com/open-telemetry/opentelemetry-python/pull/2619)) -## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0) - 2022-03-10 +## Version 1.10.0/0.29b0 (2022-03-10) - Docs rework: [non-API docs are moving](https://github.com/open-telemetry/opentelemetry-python/issues/2172) to @@ -211,13 +209,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [exporter/opentelemetry-exporter-prometheus] restore package using the new metrics API ([#2321](https://github.com/open-telemetry/opentelemetry-python/pull/2321)) -## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1) - 2022-01-29 +## Version 1.9.1/0.28b1 (2022-01-29) - Update opentelemetry-proto to v0.12.0. Note that this update removes deprecated status codes. ([#2415](https://github.com/open-telemetry/opentelemetry-python/pull/2415)) -## [1.9.0-0.28b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.0) - 2022-01-26 - +## Version 1.9.0/0.28b0 (2022-01-26) - Fix SpanLimits global span limit defaulting when set to 0 ([#2398](https://github.com/open-telemetry/opentelemetry-python/pull/2398)) @@ -240,7 +237,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [logs] prevent None from causing problems ([#2410](https://github.com/open-telemetry/opentelemetry-python/pull/2410)) -## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0) - 2021-12-17 +## Version 1.8.0/0.27b0 (2021-12-17) - Adds Aggregation and instruments as part of Metrics SDK ([#2234](https://github.com/open-telemetry/opentelemetry-python/pull/2234)) @@ -259,7 +256,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support insecure configuration for OTLP gRPC exporter ([#2350](https://github.com/open-telemetry/opentelemetry-python/pull/2350)) -## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0) - 2021-11-11 +## Version 1.7.1/0.26b1 (2021-11-11) - Add support for Python 3.10 ([#2207](https://github.com/open-telemetry/opentelemetry-python/pull/2207)) @@ -280,12 +277,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-test` start releasing to pypi.org ([#2269](https://github.com/open-telemetry/opentelemetry-python/pull/2269)) -## [1.6.2-0.25b2](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.2) - 2021-10-19 +## Version 1.6.2/0.25b2 (2021-10-19) - Fix parental trace relationship for opentracing `follows_from` reference ([#2180](https://github.com/open-telemetry/opentelemetry-python/pull/2180)) -## [1.6.1-0.25b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.1) - 2021-10-18 +## Version 1.6.1/0.25b1 (2021-10-18) - Fix ReadableSpan property types attempting to create a mapping from a list ([#2215](https://github.com/open-telemetry/opentelemetry-python/pull/2215)) @@ -294,7 +291,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Propagation: only warn about oversized baggage headers when headers exist ([#2212](https://github.com/open-telemetry/opentelemetry-python/pull/2212)) -## [1.6.0-0.25b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.0) - 2021-10-13 +## Version 1.6.0/0.25b0 (2021-10-13) - Fix race in `set_tracer_provider()` ([#2182](https://github.com/open-telemetry/opentelemetry-python/pull/2182)) @@ -332,7 +329,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add name to `BatchSpanProcessor` worker thread ([#2186](https://github.com/open-telemetry/opentelemetry-python/pull/2186)) -## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0) - 2021-08-26 +## Version 1.5.0/0.24b0 (2021-08-26) - Add pre and post instrumentation entry points ([#1983](https://github.com/open-telemetry/opentelemetry-python/pull/1983)) @@ -358,14 +355,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-api` Attribute keys must be non-empty strings. ([#2057](https://github.com/open-telemetry/opentelemetry-python/pull/2057)) -## [0.23.1](https://github.com/open-telemetry/opentelemetry-python/pull/1987) - 2021-07-26 +## Version 0.23.1 (2021-07-26) ### Changed - Fix opentelemetry-bootstrap dependency script. ([#1987](https://github.com/open-telemetry/opentelemetry-python/pull/1987)) -## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0) - 2021-07-21 +## Version 1.4.0/0.23b0 (2021-07-21) ### Added @@ -416,7 +413,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 opentelemetry specification format, rather than opentracing spec format. ([#1878](https://github.com/open-telemetry/opentelemetry-python/pull/1878)) -## [1.3.0-0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0) - 2021-06-01 +## Version 1.3.0/0.22b0 (2021-06-01) ### Added @@ -438,7 +435,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update protos to latest version release 0.9.0 ([#1873](https://github.com/open-telemetry/opentelemetry-python/pull/1873)) -## [1.2.0, 0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0) - 2021-05-11 +## Version 1.2.0/0.21b0 (2021-05-11) ### Added @@ -486,7 +483,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Moved `opentelemetry-instrumentation` to contrib repository. ([#1797](https://github.com/open-telemetry/opentelemetry-python/pull/1797)) -## [1.1.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.1.0) - 2021-04-20 +## Version 1.1.0 (2021-04-20) ### Added @@ -526,7 +523,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 a secure connection or not. ([#1771](https://github.com/open-telemetry/opentelemetry-python/pull/1771)) -## [1.0.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0) - 2021-03-26 +## Version 1.0.0 (2021-03-26) ### Added @@ -610,7 +607,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removing support for Python 3.5 ([#1706](https://github.com/open-telemetry/opentelemetry-python/pull/1706)) -## [0.19b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.19b0) - 2021-03-26 +## Version 0.19b0 (2021-03-26) ### Changed @@ -625,14 +622,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removing support for Python 3.5 ([#1706](https://github.com/open-telemetry/opentelemetry-python/pull/1706)) -## [0.18b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.18b0) - 2021-02-16 +## Version 0.18b0 (2021-02-16) ### Added - Add urllib to opentelemetry-bootstrap target list ([#1584](https://github.com/open-telemetry/opentelemetry-python/pull/1584)) -## [1.0.0rc1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0rc1) - 2021-02-12 +## Version 1.0.0rc1 (2021-02-12) ### Changed @@ -665,7 +662,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove Metrics as part of stable, marked as experimental ([#1568](https://github.com/open-telemetry/opentelemetry-python/pull/1568)) -## [0.17b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.17b0) - 2021-01-20 +## Version 0.17b0 (2021-01-20) ### Added @@ -737,14 +734,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-api` Remove ThreadLocalRuntimeContext since python3.4 is not supported. -## [0.16b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b1) - 2020-11-26 +## Version 0.16b1 (2020-11-26) ### Added - Add meter reference to observers ([#1425](https://github.com/open-telemetry/opentelemetry-python/pull/1425)) -## [0.16b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b0) - 2020-11-25 +## Version 0.16b0 (2020-11-25) ### Added @@ -789,7 +786,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 erasure for dropped spans or spans sampled by the `TraceIdRatioBased` sampler. ([#1394](https://github.com/open-telemetry/opentelemetry-python/pull/1394)) -## [0.15b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.15b0) -2020-11-02 +## Version 0.15b0 (2020-11-02) ### Added @@ -820,7 +817,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove TracerProvider coupling from Tracer init ([#1295](https://github.com/open-telemetry/opentelemetry-python/pull/1295)) -## [0.14b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.14b0) - 2020-10-13 +## Version 0.14b0 (2020-10-13) ### Added @@ -872,7 +869,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `start_as_current_span` and `use_span` can now optionally auto-record any exceptions raised inside the context manager. ([#1162](https://github.com/open-telemetry/opentelemetry-python/pull/1162)) -## [0.13b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.13b0) - 2020-09-17 +## Version 0.13b0 (2020-09-17) ### Added @@ -931,7 +928,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) -## [0.12b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.12.0) - 2020-08-14 +## Version 0.12b0 (2020-08-14) ### Added @@ -963,7 +960,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update environment variable `OTEL_RESOURCE` to `OTEL_RESOURCE_ATTRIBUTES` as per the specification -## [0.11b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.11.0) - 2020-07-28 +## Version 0.11b0 (2020-07-28) ### Added @@ -979,7 +976,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update span exporter to use OpenTelemetry Proto v0.4.0 ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/889)) -## [0.10b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.10.0) - 2020-06-23 +## Version 0.10b0 (2020-06-23) ### Changed @@ -988,7 +985,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Rename CounterAggregator -> SumAggregator ([#816](https://github.com/open-telemetry/opentelemetry-python/pull/816)) -## [0.9b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.9.0) - 2020-06-10 +## Version 0.9b0 (2020-06-10) ### Added @@ -1015,7 +1012,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Rename opentelemetry-auto-instrumentation to opentelemetry-instrumentation, and console script `opentelemetry-auto-instrumentation` to `opentelemetry-instrument` -## [0.8b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.8.0) - 2020-05-27 +## Version 0.8b0 (2020-05-27) ### Added @@ -1049,7 +1046,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - deep copy empty attributes ([#714](https://github.com/open-telemetry/opentelemetry-python/pull/714)) -## [0.7b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.7.1) - 2020-05-12 +## Version 0.7b1 (2020-05-12) ### Added @@ -1094,7 +1091,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - bugfix: freezing span attribute sequences, reducing potential user errors ([#529](https://github.com/open-telemetry/opentelemetry-python/pull/529)) -## [0.6b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.6.0) - 2020-03-30 +## Version 0.6b0 (2020-03-30) ### Added @@ -1115,7 +1112,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Exporting to collector now works ([#508](https://github.com/open-telemetry/opentelemetry-python/pull/508)) -## [0.5b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.5.0) - 2020-03-16 +## Version 0.5b0 (2020-03-16) ### Added @@ -1150,7 +1147,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement observer instrument ([#425](https://github.com/open-telemetry/opentelemetry-python/pull/425)) -## [0.4a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.4.0) - 2020-02-21 +## Version 0.4a0 (2020-02-21) ### Added @@ -1205,7 +1202,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove monotonic and absolute metric instruments ([#410](https://github.com/open-telemetry/opentelemetry-python/pull/410)) -## [0.3a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.3.0) - 2019-12-11 +## Version 0.3a0 (2019-12-11) ### Added @@ -1224,7 +1221,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove option to create unstarted spans from API ([#290](https://github.com/open-telemetry/opentelemetry-python/pull/290)) -## [0.2a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.2.0) - 2019-10-29 +## Version 0.2a0 (2019-10-29) ### Added @@ -1241,7 +1238,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Multiple context API changes - Multiple bugfixes and improvements -## [0.1a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.1.0) - 2019-09-30 +## Version 0.1a0 (2019-09-30) ### Added diff --git a/RELEASING.md b/RELEASING.md index 975fdf9440..21adcce02f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,93 +1,90 @@ -# Releasing OpenTelemetry Packages (for maintainers only) -This document explains how to publish all OT modules at version x.y.z. Ensure that you’re following semver when choosing a version number. - -Release Process: -- [Checkout a clean repo](#checkout-a-clean-repo) -- [Update versions](#update-versions) -- [Create a new branch](#create-a-new-branch) -- [Open a Pull Request](#open-a-pull-request) -- [Create a Release](#create-a-release) -- [Check PyPI](#check-pypi) -- [Move stable tag](#move-stable-tag) -- [Update main](#update-main) -- [Hotfix procedure](#hotfix-procedure) -- [Troubleshooting](#troubleshooting) - - [Publish failed](#publish-failed) - -## Checkout a clean repo -To avoid pushing untracked changes, check out the repo in a new dir - -## Update versions -The update of the version information relies on the information in eachdist.ini to identify which packages are stable, prerelease or -experimental. Update the desired version there to begin the release process. - -## Create a new branch -The following script does the following: -- update main locally -- creates a new release branch `release/` -- updates version and changelog files -- commits the change - -*NOTE: This script was run by a GitHub Action but required the Action bot to be excluded from the CLA check, which it currently is not.* - -```bash -./scripts/prepare_release.sh -``` - -## Open a Pull Request - -The PR should be opened from the `release/` branch created as part of running `prepare_release.sh` in the steps above. - -## Create a Release - -- Create the GH release from the main branch, using a new tag for this micro version, e.g. `v0.7.0` -- Copy the changelogs from all packages that changed into the release notes (and reformat to remove hard line wraps) - - -## Check PyPI - -This should be handled automatically on release by the [publish action](https://github.com/open-telemetry/opentelemetry-python/blob/main/.github/workflows/publish.yml). - -- Check the [action logs](https://github.com/open-telemetry/opentelemetry-python/actions?query=workflow%3APublish) to make sure packages have been uploaded to PyPI -- Check the release history (e.g. https://pypi.org/project/opentelemetry-api/#history) on PyPI - -If for some reason the action failed, see [Publish failed](#publish-failed) below - -## Move stable tag - -This will ensure the docs are pointing at the stable release. - -```bash -git tag -d stable -git tag stable -git push --delete origin tagname -git push origin stable -``` - -To validate this worked, ensure the stable build has run successfully: https://readthedocs.org/projects/opentelemetry-python/builds/. If the build has not run automatically, it can be manually trigger via the readthedocs interface. - -## Update main - -Ensure the version and changelog updates have been applied to main. Update the versions in eachdist.ini once again this time to include the `.dev0` tag and -run eachdist once again: -```bash -./scripts/eachdist.py update_versions --versions stable,prerelease -``` - -## Hotfix procedure - -A `hotfix` is defined as a small change developed to correct a bug that should be released as quickly as possible. Due to the nature of hotfixes, they usually will only affect one or a few packages. Therefore, it usually is not necessary to go through the entire release process outlined above for hotfixes. Follow the below steps how to release a hotfix: - -1. Identify the packages that are affected by the bug. Make the changes to those packages, merging to `main`, as quickly as possible. -2. On your local machine, remove the `dev0` tags from the version number and increment the patch version number. -3. On your local machine, update `CHANGELOG.md` with the date of the hotfix change. -4. With administrator privileges for PyPi, manually publish the affected packages. - 1. Install [twine](https://pypi.org/project/twine/) and [build](https://pypi.org/project/build/) - 2. Navigate to where the `pyproject.toml` file exists for the package you want to publish. - 3. To build the package: run `python -m build`. - 4. Validate your built distributions by running `twine check dist/*`. - 5. Upload distributions to PyPi by running `twine upload dist/*`. -5. Note that since hotfixes are manually published, the build scripts for publish after creating a release are not run. +# Release instructions + +## Preparing a new major or minor release + +* Run the [Prepare release branch workflow](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/prepare-release-branch.yml). + * Press the "Run workflow" button, and leave the default branch `main` selected. + * If making a pre-release of stable components (e.g. release candidate), + enter the pre-release version number, e.g. `1.9.0rc2`. + (otherwise the workflow will pick up the version from `main` and just remove the `.dev` suffix). + * Review and merge the two pull requests that it creates + (one is targeted to the release branch and one is targeted to `main`). + +## Preparing a new patch release + +* Backport pull request(s) to the release branch. + * Run the [Backport workflow](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/backport.yml). + * Press the "Run workflow" button, then select the release branch from the dropdown list, + e.g. `release/v1.9.x`, then enter the pull request number that you want to backport, + then click the "Run workflow" button below that. + * Review and merge the backport pull request that it generates. +* Merge a pull request to the release branch updating the `CHANGELOG.md`. + * The heading for the unreleased entries should be `## Unreleased`. +* Run the [Prepare patch release workflow](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/prepare-patch-release.yml). + * Press the "Run workflow" button, then select the release branch from the dropdown list, + e.g. `release/v1.9.x`, and click the "Run workflow" button below that. + * Review and merge the pull request that it creates for updating the version. + +## Making the release + +* Run the [Release workflow](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/release.yml). + * Press the "Run workflow" button, then select the release branch from the dropdown list, + e.g. `release/v1.9.x`, and click the "Run workflow" button below that. + * This workflow will publish the artifacts and publish a GitHub release with release notes based on the change log. + * Review and merge the pull request that it creates for updating the change log in main + (note that if this is not a patch release then the change log on main may already be up-to-date, + in which case no pull request will be created). + +## Notes about version numbering for stable components + +* The version number for stable components in the `main` branch is always `X.Y.0.dev`, + where `X.Y.0` represents the next minor release. +* When the release branch is created, you can opt to make a "pre-release", e.g. `X.Y.0rc2`. +* If you ARE NOT making a "pre-release": + * A "long-term" release branch will be created, e.g. `release/v1.9.x-0.21bx` (notice the wildcard x's). + Later on, after the initial release, you can backport PRs to a "long-term" release branch and make patch releases + from it. + * The version number for stable components in the release branch will be bumped to remove the `.dev`, + e.g. `X.Y.0`. + * The version number for stable components in the `main` branch will be bumped to the next version, + e.g. `X.{Y+1}.0.dev`. +* If you ARE making a "pre-release": + * A "short-term" release branch will be created, e.g. `release/v1.9.0rc2-0.21b0` (notice the precise version with no + wildcard x's). "Short-term" release branches do not support backports or patch releases after the initial release. + * The version number for stable components in the `main` branch will not be bumped, e.g. it will remain `X.Y.0.dev` + since the next minor release will still be `X.Y.0`. + +## Notes about version numbering for unstable components + +* The version number for unstable components in the `main` branch is always `0.Yb0.dev`, + where `0.Yb0` represents the next minor release. + * _Question: Is "b" (beta) redundant on "0." releases, or is this a python thing? I'm wondering if we can change it to `0.Y.0` to match up with the practice in js and go repos._ +* Unstable components do not need "pre-releases", and so whether or not you are making a "pre-release" of stable + components: + * The version number for unstable components in the release branch will be bumped to remove the `.dev`, + e.g. `0.Yb0`. + * The version number for unstable components in the `main` branch will be bumped to the next version, + e.g. `0.{Y+1}b0.dev`. + +## After the release + +* Check PyPI + * This should be handled automatically on release by the [publish action](https://github.com/open-telemetry/opentelemetry-python/blob/main/.github/workflows/publish.yml). + * Check the [action logs](https://github.com/open-telemetry/opentelemetry-python/actions?query=workflow%3APublish) to make sure packages have been uploaded to PyPI + * Check the release history (e.g. https://pypi.org/project/opentelemetry-api/#history) on PyPI + * If for some reason the action failed, see [Publish failed](#publish-failed) below +* Move stable tag + * Run the following (TODO automate): + ```bash + git tag -d stable + git tag stable + git push --delete origin tagname + git push origin stable + ``` + * This will ensure the docs are pointing at the stable release. + * To validate this worked, ensure the stable build has run successfully: + https://readthedocs.org/projects/opentelemetry-python/builds/. + If the build has not run automatically, it can be manually trigger via the readthedocs interface. ## Troubleshooting diff --git a/eachdist.ini b/eachdist.ini index 7cc26cc2ca..2909ba627b 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.13.0 +version=1.14.0.dev packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.34b0 +version=0.35b0.dev packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index b65ab4af2b..b1fba7a3e4 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index b65ab4af2b..b1fba7a3e4 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger/pyproject.toml b/exporter/opentelemetry-exporter-jaeger/pyproject.toml index b28066a2b0..191c6c63ca 100644 --- a/exporter/opentelemetry-exporter-jaeger/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-jaeger-proto-grpc == 1.13.0", - "opentelemetry-exporter-jaeger-thrift == 1.13.0", + "opentelemetry-exporter-jaeger-proto-grpc == 1.14.0.dev", + "opentelemetry-exporter-jaeger-thrift == 1.14.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index b65ab4af2b..b1fba7a3e4 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 09b3473b7d..7bcf6c0a7b 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.35b0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index ca760e01b9..bf6ebf5d85 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "googleapis-common-protos ~= 1.52", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.12", - "opentelemetry-proto == 1.13.0", + "opentelemetry-proto == 1.14.0.dev", "opentelemetry-sdk ~= 1.12", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index b9536c2461..f3bc9d478d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index c1734a275f..21feb23674 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.12", - "opentelemetry-proto == 1.13.0", + "opentelemetry-proto == 1.14.0.dev", "opentelemetry-sdk ~= 1.12", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index b9536c2461..f3bc9d478d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index 47abbc7df1..e2550bfdda 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-otlp-proto-grpc == 1.13.0", - "opentelemetry-exporter-otlp-proto-http == 1.13.0", + "opentelemetry-exporter-otlp-proto-grpc == 1.14.0.dev", + "opentelemetry-exporter-otlp-proto-http == 1.14.0.dev", ] [project.entry-points.opentelemetry_logs_exporter] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index b9536c2461..f3bc9d478d 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 09b3473b7d..7bcf6c0a7b 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.35b0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index b9536c2461..f3bc9d478d 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml index c28edc85d2..2c518d9de1 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.3", - "opentelemetry-exporter-zipkin-json == 1.13.0", + "opentelemetry-exporter-zipkin-json == 1.14.0.dev", "opentelemetry-sdk ~= 1.11", "protobuf ~= 3.12", "requests ~= 2.7", diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index b9536c2461..f3bc9d478d 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin/pyproject.toml b/exporter/opentelemetry-exporter-zipkin/pyproject.toml index 26eb8781ad..49cee96b0d 100644 --- a/exporter/opentelemetry-exporter-zipkin/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-zipkin-json == 1.13.0", - "opentelemetry-exporter-zipkin-proto-http == 1.13.0", + "opentelemetry-exporter-zipkin-json == 1.14.0.dev", + "opentelemetry-exporter-zipkin-proto-http == 1.14.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index b9536c2461..f3bc9d478d 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index b9536c2461..f3bc9d478d 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index b9536c2461..f3bc9d478d 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index bd27168f55..8749cf2529 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-api == 1.13.0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-api == 1.14.0.dev", + "opentelemetry-semantic-conventions == 0.35b0.dev", "setuptools >= 16.0", "typing-extensions >= 3.7.4", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index b9536c2461..f3bc9d478d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 09b3473b7d..7bcf6c0a7b 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.35b0.dev" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index b9536c2461..f3bc9d478d 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index b9536c2461..f3bc9d478d 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0.dev" diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 0c0620e95a..2f6faa6cf3 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -8,7 +8,6 @@ import subprocess import sys from configparser import ConfigParser -from datetime import datetime from inspect import cleandoc from itertools import chain from os.path import basename @@ -547,55 +546,6 @@ def lint_args(args): ) -def update_changelog(path, version, new_entry): - unreleased_changes = False - try: - with open(path, encoding="utf-8") as changelog: - text = changelog.read() - if f"## [{version}]" in text: - raise AttributeError( - f"{path} already contains version {version}" - ) - with open(path, encoding="utf-8") as changelog: - for line in changelog: - if line.startswith("## [Unreleased]"): - unreleased_changes = False - elif line.startswith("## "): - break - elif len(line.strip()) > 0: - unreleased_changes = True - - except FileNotFoundError: - print(f"file missing: {path}") - return - - if unreleased_changes: - print(f"updating: {path}") - text = re.sub(r"## \[Unreleased\].*", new_entry, text) - with open(path, "w", encoding="utf-8") as changelog: - changelog.write(text) - - -def update_changelogs(version): - today = datetime.now().strftime("%Y-%m-%d") - new_entry = """## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v{version}...HEAD) - -## [{version}](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v{version}) - {today} - -""".format( - version=version, today=today - ) - errors = False - try: - update_changelog("./CHANGELOG.md", version, new_entry) - except Exception as err: # pylint: disable=broad-except - print(str(err)) - errors = True - - if errors: - sys.exit(1) - - def find(name, path): for root, _, files in os.walk(path): if name in files: @@ -675,8 +625,6 @@ def release_args(args): update_dependencies(targets, version, packages) update_version_files(targets, version, packages) - update_changelogs(updated_versions[0]) - def test_args(args): clean_remainder_args(args.pytestargs) diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh deleted file mode 100755 index 607b6189bb..0000000000 --- a/scripts/prepare_release.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -# -# This script: -# 1. parses the version number from the branch name -# 2. updates version.py files to match that version -# 3. iterates through CHANGELOG.md files and updates any files containing -# unreleased changes -# 4. sets the output variable 'version_updated' to determine whether -# the github action to create a pull request should run. this allows -# maintainers to merge changes back into the release branch without -# triggering unnecessary pull requests -# - -VERSION=$(./scripts/eachdist.py version --mode stable)-$(./scripts/eachdist.py version --mode prerelease) -echo "Using version ${VERSION}" - - -# create the release branch -git checkout -b release/${VERSION} -git push origin release/${VERSION} - -./scripts/eachdist.py update_versions --versions stable,prerelease -rc=$? -if [ $rc != 0 ]; then - echo "::set-output name=version_updated::0" - exit 0 -fi - -git add . - -git commit -m "updating changelogs and version to ${VERSION}" - -echo "Time to create a release, here's a sample title:" -echo "[pre-release] Update changelogs, version [${VERSION}]" diff --git a/shim/opentelemetry-opentracing-shim/pyproject.toml b/shim/opentelemetry-opentracing-shim/pyproject.toml index e6119df7f6..37b68fd179 100644 --- a/shim/opentelemetry-opentracing-shim/pyproject.toml +++ b/shim/opentelemetry-opentracing-shim/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.35b0.dev", "opentracing ~= 2.2.0", ] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 09b3473b7d..7bcf6c0a7b 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.35b0.dev" diff --git a/tests/opentelemetry-test-utils/pyproject.toml b/tests/opentelemetry-test-utils/pyproject.toml index e4307e16f6..eaf2d69207 100644 --- a/tests/opentelemetry-test-utils/pyproject.toml +++ b/tests/opentelemetry-test-utils/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ ] dependencies = [ "asgiref ~= 3.0", - "opentelemetry-api == 1.13.0", - "opentelemetry-sdk == 1.13.0", + "opentelemetry-api == 1.14.0.dev", + "opentelemetry-sdk == 1.14.0.dev", ] [project.optional-dependencies] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index a590883f7e..737eebffad 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.34b0" +__version__ = "0.35b0.dev" From 4055dcf6323bf8dc4b5d33390f6ec04da8d8782c Mon Sep 17 00:00:00 2001 From: Rytis Bagdziunas Date: Wed, 2 Nov 2022 04:53:01 +0100 Subject: [PATCH 1340/1517] Added metrics export to gunicorn example (#3002) --- .../flask-gunicorn/README.rst | 2 +- .../{gunicorn.config.py => gunicorn.conf.py} | 31 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) rename docs/examples/fork-process-model/flask-gunicorn/{gunicorn.config.py => gunicorn.conf.py} (57%) diff --git a/docs/examples/fork-process-model/flask-gunicorn/README.rst b/docs/examples/fork-process-model/flask-gunicorn/README.rst index ec7a2e9e60..6ca9790dcd 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/README.rst +++ b/docs/examples/fork-process-model/flask-gunicorn/README.rst @@ -8,4 +8,4 @@ Run application --------------- .. code-block:: sh - gunicorn app -c gunicorn.config.py + gunicorn app -c gunicorn.conf.py diff --git a/docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py b/docs/examples/fork-process-model/flask-gunicorn/gunicorn.conf.py similarity index 57% rename from docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py rename to docs/examples/fork-process-model/flask-gunicorn/gunicorn.conf.py index 902a0133e3..34b4591596 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/gunicorn.config.py +++ b/docs/examples/fork-process-model/flask-gunicorn/gunicorn.conf.py @@ -12,10 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from opentelemetry import trace +from opentelemetry import metrics, trace +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( + OTLPMetricExporter, +) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor @@ -41,7 +46,19 @@ def post_fork(server, worker): server.log.info("Worker spawned (pid: %s)", worker.pid) - resource = Resource.create(attributes={"service.name": "api-service"}) + resource = Resource.create( + attributes={ + "service.name": "api-service", + # If workers are not distinguished within attributes, traces and + # metrics exported from each worker will be indistinguishable. While + # not necessarily an issue for traces, it is confusing for almost + # all metric types. A built-in way to identify a worker is by PID + # but this may lead to high label cardinality. An alternative + # workaround and additional discussion are available here: + # https://github.com/benoitc/gunicorn/issues/1352 + "worker": worker.pid, + } + ) trace.set_tracer_provider(TracerProvider(resource=resource)) # This uses insecure connection for the purpose of example. Please see the @@ -50,3 +67,13 @@ def post_fork(server, worker): OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True) ) trace.get_tracer_provider().add_span_processor(span_processor) + + reader = PeriodicExportingMetricReader( + OTLPMetricExporter(endpoint="http://localhost:4317") + ) + metrics.set_meter_provider( + MeterProvider( + resource=resource, + metric_readers=[reader], + ) + ) From d655eb8ecdd6e311365c385dc32bec8179a51e10 Mon Sep 17 00:00:00 2001 From: Kalungi Deborah Date: Wed, 2 Nov 2022 19:43:51 +0300 Subject: [PATCH 1341/1517] Add disable default views to common view scenarios (#2983) --- .../views/disable_default_aggregation.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/examples/metrics/views/disable_default_aggregation.py diff --git a/docs/examples/metrics/views/disable_default_aggregation.py b/docs/examples/metrics/views/disable_default_aggregation.py new file mode 100644 index 0000000000..387bfc465d --- /dev/null +++ b/docs/examples/metrics/views/disable_default_aggregation.py @@ -0,0 +1,57 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import time + +from opentelemetry.metrics import get_meter_provider, set_meter_provider +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk.metrics.view import ( + DropAggregation, + SumAggregation, + View, +) + +# disable_default_aggregation. +disable_default_aggregation = View( + instrument_name="*", aggregation=DropAggregation() +) + +exporter = ConsoleMetricExporter() + +reader = PeriodicExportingMetricReader(exporter, export_interval_millis=1_000) +provider = MeterProvider( + metric_readers=[ + reader, + ], + views=[ + disable_default_aggregation, + View(instrument_name="mycounter", aggregation=SumAggregation()), + ], +) +set_meter_provider(provider) + +meter = get_meter_provider().get_meter( + "view-disable-default-aggregation", "0.1.2" +) +# Create a view to configure aggregation specific for this counter. +my_counter = meter.create_counter("mycounter") + +while 1: + my_counter.add(random.randint(1, 10)) + time.sleep(random.random()) From b34782f6d764748804b3977ea489ff555cd3ab16 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 2 Nov 2022 11:00:05 -0700 Subject: [PATCH 1342/1517] Update SHA (#3012) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 71d31be729..c5ddb2e21b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: a47c45e61b4a7906f826b203f15f0dcf646ee185 + CONTRIB_REPO_SHA: 10918b8e0df18dc944dc93116b18ba81b097e52f # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when From 778c4b1cffdd448c531c868a9470618eb8a6517c Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Thu, 3 Nov 2022 12:45:28 -0700 Subject: [PATCH 1343/1517] Update version to 1.15.0.dev/0.36b0.dev (#3016) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-grpc/pyproject.toml | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-http/pyproject.toml | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/pyproject.toml | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/pyproject.toml | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/pyproject.toml | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 31 files changed, 38 insertions(+), 36 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c5ddb2e21b..aedcce541a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 10918b8e0df18dc944dc93116b18ba81b097e52f + CONTRIB_REPO_SHA: 66edf69811e142c397d8500cafe6eddeb5565d6e # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aebb9fbe1..e2bf579c22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## Version 1.14.0/0.35b0 (2022-11-04) + - Add logarithm and exponent mappings ([#2960](https://github.com/open-telemetry/opentelemetry-python/pull/2960)) - Add and use missing metrics environment variables diff --git a/eachdist.ini b/eachdist.ini index 2909ba627b..40c6db3c18 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.14.0.dev +version=1.15.0.dev packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.35b0.dev +version=0.36b0.dev packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index b1fba7a3e4..65fcc50d7d 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index b1fba7a3e4..65fcc50d7d 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger/pyproject.toml b/exporter/opentelemetry-exporter-jaeger/pyproject.toml index 191c6c63ca..622e90d4ba 100644 --- a/exporter/opentelemetry-exporter-jaeger/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-jaeger-proto-grpc == 1.14.0.dev", - "opentelemetry-exporter-jaeger-thrift == 1.14.0.dev", + "opentelemetry-exporter-jaeger-proto-grpc == 1.15.0.dev", + "opentelemetry-exporter-jaeger-thrift == 1.15.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index b1fba7a3e4..65fcc50d7d 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 7bcf6c0a7b..fa69afa640 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.35b0.dev" +__version__ = "0.36b0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index bf6ebf5d85..6c8d44ecec 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "googleapis-common-protos ~= 1.52", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.12", - "opentelemetry-proto == 1.14.0.dev", + "opentelemetry-proto == 1.15.0.dev", "opentelemetry-sdk ~= 1.12", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index f3bc9d478d..0fffbf9870 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index 21feb23674..dcab6569ab 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.12", - "opentelemetry-proto == 1.14.0.dev", + "opentelemetry-proto == 1.15.0.dev", "opentelemetry-sdk ~= 1.12", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index f3bc9d478d..0fffbf9870 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index e2550bfdda..1e61767eb1 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-otlp-proto-grpc == 1.14.0.dev", - "opentelemetry-exporter-otlp-proto-http == 1.14.0.dev", + "opentelemetry-exporter-otlp-proto-grpc == 1.15.0.dev", + "opentelemetry-exporter-otlp-proto-http == 1.15.0.dev", ] [project.entry-points.opentelemetry_logs_exporter] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index f3bc9d478d..0fffbf9870 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 7bcf6c0a7b..fa69afa640 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.35b0.dev" +__version__ = "0.36b0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index f3bc9d478d..0fffbf9870 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml index 2c518d9de1..a9268258e7 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.3", - "opentelemetry-exporter-zipkin-json == 1.14.0.dev", + "opentelemetry-exporter-zipkin-json == 1.15.0.dev", "opentelemetry-sdk ~= 1.11", "protobuf ~= 3.12", "requests ~= 2.7", diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index f3bc9d478d..0fffbf9870 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin/pyproject.toml b/exporter/opentelemetry-exporter-zipkin/pyproject.toml index 49cee96b0d..9f7f5e8274 100644 --- a/exporter/opentelemetry-exporter-zipkin/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-zipkin-json == 1.14.0.dev", - "opentelemetry-exporter-zipkin-proto-http == 1.14.0.dev", + "opentelemetry-exporter-zipkin-json == 1.15.0.dev", + "opentelemetry-exporter-zipkin-proto-http == 1.15.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index f3bc9d478d..0fffbf9870 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index f3bc9d478d..0fffbf9870 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index f3bc9d478d..0fffbf9870 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index 8749cf2529..88882d0164 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-api == 1.14.0.dev", - "opentelemetry-semantic-conventions == 0.35b0.dev", + "opentelemetry-api == 1.15.0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", "setuptools >= 16.0", "typing-extensions >= 3.7.4", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index f3bc9d478d..0fffbf9870 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 7bcf6c0a7b..fa69afa640 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.35b0.dev" +__version__ = "0.36b0.dev" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index f3bc9d478d..0fffbf9870 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index f3bc9d478d..0fffbf9870 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0.dev" +__version__ = "1.15.0.dev" diff --git a/shim/opentelemetry-opentracing-shim/pyproject.toml b/shim/opentelemetry-opentracing-shim/pyproject.toml index 37b68fd179..c0e1a8cfe2 100644 --- a/shim/opentelemetry-opentracing-shim/pyproject.toml +++ b/shim/opentelemetry-opentracing-shim/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.35b0.dev", + "opentelemetry-test-utils == 0.36b0.dev", "opentracing ~= 2.2.0", ] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 7bcf6c0a7b..fa69afa640 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.35b0.dev" +__version__ = "0.36b0.dev" diff --git a/tests/opentelemetry-test-utils/pyproject.toml b/tests/opentelemetry-test-utils/pyproject.toml index eaf2d69207..f280dff89f 100644 --- a/tests/opentelemetry-test-utils/pyproject.toml +++ b/tests/opentelemetry-test-utils/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ ] dependencies = [ "asgiref ~= 3.0", - "opentelemetry-api == 1.14.0.dev", - "opentelemetry-sdk == 1.14.0.dev", + "opentelemetry-api == 1.15.0.dev", + "opentelemetry-sdk == 1.15.0.dev", ] [project.optional-dependencies] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 737eebffad..5ea2af4ddd 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.35b0.dev" +__version__ = "0.36b0.dev" From 787e499aaadb5db6e0eb308a9fa7dca58139e44c Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 8 Nov 2022 08:24:27 +0530 Subject: [PATCH 1344/1517] Add missing entry points for OTLP/HTTP (#3027) --- CHANGELOG.md | 3 +++ .../opentelemetry-exporter-otlp-proto-http/pyproject.toml | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2bf579c22..339cdb6246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Add missing entry points for OTLP/HTTP exporter + ([#3027](https://github.com/open-telemetry/opentelemetry-python/pull/3027)) + ## Version 1.14.0/0.35b0 (2022-11-04) - Add logarithm and exponent mappings diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index dcab6569ab..a29fa8a682 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -42,6 +42,12 @@ test = [ [project.entry-points.opentelemetry_traces_exporter] otlp_proto_http = "opentelemetry.exporter.otlp.proto.http.trace_exporter:OTLPSpanExporter" +[project.entry-points.opentelemetry_metrics_exporter] +otlp_proto_http = "opentelemetry.exporter.otlp.proto.http.metric_exporter:OTLPMetricExporter" + +[project.entry-points.opentelemetry_logs_exporter] +otlp_proto_http = "opentelemetry.exporter.otlp.proto.http._log_exporter:OTLPLogExporter" + [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-http" From 74ced8508a71a263e56d92073af2498b7630978b Mon Sep 17 00:00:00 2001 From: pridhi-arora <110390842+pridhi-arora@users.noreply.github.com> Date: Thu, 17 Nov 2022 12:06:18 +0530 Subject: [PATCH 1345/1517] Fixes typo in OTEL_EXPORTER_OTLP_METRRICS_CERTIFICATE (#3039) --- .../src/opentelemetry/sdk/environment_variables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index e8afdd6cbf..17a669e01f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -431,7 +431,7 @@ """ OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE = ( - "OTEL_EXPORTER_OTLP_METRRICS_CERTIFICATE" + "OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE" ) """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE From b02ff47a6f0048efa35d8a0083ed4e1f7dc1ff9b Mon Sep 17 00:00:00 2001 From: Jeremy Voss Date: Mon, 21 Nov 2022 07:43:27 -0800 Subject: [PATCH 1346/1517] Custom sampler fix (#3026) * Fixed circular dependency that can happen when injecting custom samplers * lint * Deleted duplicate tests * lint * lint * lint * lint * lint * Update opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py Co-authored-by: Leighton Chen * Update opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py Co-authored-by: Leighton Chen * typing * Update opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py Co-authored-by: Leighton Chen * Update opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py Co-authored-by: Srikanth Chekuri * Retry tests * Fixed circular dependency that can happen when injecting custom samplers * lint * Deleted duplicate tests * lint * lint * lint * lint * lint * Update opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py Co-authored-by: Leighton Chen * Update opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py Co-authored-by: Leighton Chen * typing * Retry tests * Update opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py Co-authored-by: Leighton Chen * Update opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py Co-authored-by: Srikanth Chekuri * Updated contrib sha Co-authored-by: Srikanth Chekuri Co-authored-by: Leighton Chen Co-authored-by: Diego Hurtado --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 + .../sdk/_configuration/__init__.py | 78 ++++- .../src/opentelemetry/sdk/trace/__init__.py | 37 ++- .../src/opentelemetry/sdk/trace/sampling.py | 64 ++-- .../src/opentelemetry/sdk/util/__init__.py | 30 +- opentelemetry-sdk/tests/test_configurator.py | 288 +++++++++++++++++- opentelemetry-sdk/tests/trace/test_trace.py | 240 +-------------- 8 files changed, 412 insertions(+), 329 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aedcce541a..a75f357623 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 66edf69811e142c397d8500cafe6eddeb5565d6e + CONTRIB_REPO_SHA: c6134843900e2eeb1b8b3383a897b38cc0905c38 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 339cdb6246..030a8a3745 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Fixed circular dependency issue with custom samplers + ([#3026](https://github.com/open-telemetry/opentelemetry-python/pull/3026)) - Add missing entry points for OTLP/HTTP exporter ([#3027](https://github.com/open-telemetry/opentelemetry-python/pull/3027)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index c2280e1b27..27f3a334c7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -21,8 +21,9 @@ import os from abc import ABC, abstractmethod from os import environ -from typing import Dict, Optional, Sequence, Tuple, Type +from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type +from pkg_resources import iter_entry_points from typing_extensions import Literal from opentelemetry.environment_variables import ( @@ -44,6 +45,8 @@ OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, + OTEL_TRACES_SAMPLER, + OTEL_TRACES_SAMPLER_ARG, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( @@ -54,7 +57,7 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator -from opentelemetry.sdk.util import _import_config_components +from opentelemetry.sdk.trace.sampling import Sampler from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.trace import set_tracer_provider @@ -82,9 +85,35 @@ _RANDOM_ID_GENERATOR = "random" _DEFAULT_ID_GENERATOR = _RANDOM_ID_GENERATOR +_OTEL_SAMPLER_ENTRY_POINT_GROUP = "opentelemetry_traces_sampler" + _logger = logging.getLogger(__name__) +def _import_config_components( + selected_components: List[str], entry_point_name: str +) -> Sequence[Tuple[str, object]]: + component_entry_points = { + ep.name: ep for ep in iter_entry_points(entry_point_name) + } + component_impls = [] + for selected_component in selected_components: + entry_point = component_entry_points.get(selected_component, None) + if not entry_point: + raise RuntimeError( + f"Requested component '{selected_component}' not found in entry points for '{entry_point_name}'" + ) + + component_impl = entry_point.load() + component_impls.append((selected_component, component_impl)) + + return component_impls + + +def _get_sampler() -> Optional[str]: + return environ.get(OTEL_TRACES_SAMPLER, None) + + def _get_id_generator() -> str: return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR) @@ -149,7 +178,8 @@ def _get_exporter_names( def _init_tracing( exporters: Dict[str, Type[SpanExporter]], - id_generator: IdGenerator, + id_generator: IdGenerator = None, + sampler: Sampler = None, auto_instrumentation_version: Optional[str] = None, ): # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name @@ -161,7 +191,8 @@ def _init_tracing( ResourceAttributes.TELEMETRY_AUTO_VERSION ] = auto_instrumentation_version provider = TracerProvider( - id_generator=id_generator(), + id_generator=id_generator, + sampler=sampler, resource=Resource.create(auto_resource), ) set_tracer_provider(provider) @@ -266,13 +297,41 @@ def _import_exporters( return trace_exporters, metric_exporters, log_exporters +def _import_sampler_factory(sampler_name: str) -> Callable[[str], Sampler]: + _, sampler_impl = _import_config_components( + [sampler_name.strip()], _OTEL_SAMPLER_ENTRY_POINT_GROUP + )[0] + return sampler_impl + + +def _import_sampler(sampler_name: str) -> Optional[Sampler]: + if not sampler_name: + return None + try: + sampler_factory = _import_sampler_factory(sampler_name) + sampler_arg = os.getenv(OTEL_TRACES_SAMPLER_ARG, "") + sampler = sampler_factory(sampler_arg) + if not isinstance(sampler, Sampler): + message = f"Sampler factory, {sampler_factory}, produced output, {sampler}, which is not a Sampler." + _logger.warning(message) + raise ValueError(message) + return sampler + except Exception as exc: # pylint: disable=broad-except + _logger.warning( + "Using default sampler. Failed to initialize custom sampler, %s: %s", + sampler_name, + exc, + ) + return None + + def _import_id_generator(id_generator_name: str) -> IdGenerator: id_generator_name, id_generator_impl = _import_config_components( [id_generator_name.strip()], "opentelemetry_id_generator" )[0] if issubclass(id_generator_impl, IdGenerator): - return id_generator_impl + return id_generator_impl() raise RuntimeError(f"{id_generator_name} is not an IdGenerator") @@ -283,9 +342,16 @@ def _initialize_components(auto_instrumentation_version): _get_exporter_names("metrics"), _get_exporter_names("logs"), ) + sampler_name = _get_sampler() + sampler = _import_sampler(sampler_name) id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) - _init_tracing(trace_exporters, id_generator, auto_instrumentation_version) + _init_tracing( + exporters=trace_exporters, + id_generator=id_generator, + sampler=sampler, + auto_instrumentation_version=auto_instrumentation_version, + ) _init_metrics(metric_exporters, auto_instrumentation_version) logging_enabled = os.getenv( _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "false" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 51ede5e121..0ddf531a4d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -78,9 +78,6 @@ _ENV_VALUE_UNSET = "" -# pylint: disable=protected-access -_TRACE_SAMPLER = sampling._get_from_env_or_default() - class SpanProcessor: """Interface which allows hooks for SDK's `Span` start and end method @@ -334,7 +331,7 @@ def _check_span_ended(func): def wrapper(self, *args, **kwargs): already_ended = False with self._lock: # pylint: disable=protected-access - if self._end_time is None: + if self._end_time is None: # pylint: disable=protected-access func(self, *args, **kwargs) else: already_ended = True @@ -519,7 +516,11 @@ def _format_events(events): f_event = OrderedDict() f_event["name"] = event.name f_event["timestamp"] = util.ns_to_iso_str(event.timestamp) - f_event["attributes"] = Span._format_attributes(event.attributes) + f_event[ + "attributes" + ] = Span._format_attributes( # pylint: disable=protected-access + event.attributes + ) f_events.append(f_event) return f_events @@ -528,8 +529,16 @@ def _format_links(links): f_links = [] for link in links: f_link = OrderedDict() - f_link["context"] = Span._format_context(link.context) - f_link["attributes"] = Span._format_attributes(link.attributes) + f_link[ + "context" + ] = Span._format_context( # pylint: disable=protected-access + link.context + ) + f_link[ + "attributes" + ] = Span._format_attributes( # pylint: disable=protected-access + link.attributes + ) f_links.append(f_link) return f_links @@ -691,10 +700,12 @@ def _from_env_if_absent( ) # not removed for backward compat. please use SpanLimits instead. -SPAN_ATTRIBUTE_COUNT_LIMIT = SpanLimits._from_env_if_absent( - None, - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, - _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, +SPAN_ATTRIBUTE_COUNT_LIMIT = ( + SpanLimits._from_env_if_absent( # pylint: disable=protected-access + None, + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + ) ) @@ -1115,7 +1126,7 @@ class TracerProvider(trace_api.TracerProvider): def __init__( self, - sampler: sampling.Sampler = _TRACE_SAMPLER, + sampler: sampling.Sampler = None, resource: Resource = Resource.create({}), shutdown_on_exit: bool = True, active_span_processor: Union[ @@ -1132,6 +1143,8 @@ def __init__( else: self.id_generator = id_generator self._resource = resource + if not sampler: + sampler = sampling._get_from_env_or_default() self.sampler = sampler self._span_limits = span_limits or SpanLimits() self._atexit_handler = None diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 38a3338b02..8af41f3d66 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -73,7 +73,8 @@ * parentbased_always_off - Sampler that respects its parent span's sampling decision, but otherwise never samples. * parentbased_traceidratio - Sampler that respects its parent span's sampling decision, but otherwise samples probabalistically based on rate. -Sampling probability can be set with ``OTEL_TRACES_SAMPLER_ARG`` if the sampler is traceidratio or parentbased_traceidratio. Rate must be in the range [0.0,1.0]. When not provided rate will be set to 1.0 (maximum rate possible). +Sampling probability can be set with ``OTEL_TRACES_SAMPLER_ARG`` if the sampler is traceidratio or parentbased_traceidratio. Rate must be in the range [0.0,1.0]. When not provided rate will be set to +1.0 (maximum rate possible). Prev example but with environment variables. Please make sure to set the env ``OTEL_TRACES_SAMPLER=traceidratio`` and ``OTEL_TRACES_SAMPLER_ARG=0.001``. @@ -97,9 +98,10 @@ with trace.get_tracer(__name__).start_as_current_span("Test Span"): ... -In order to create a configurable custom sampler, create an entry point for the custom sampler factory method under the entry point group, ``opentelemetry_traces_sampler``. The custom sampler factory -method must be of type ``Callable[[str], Sampler]``, taking a single string argument and returning a Sampler object. The single input will come from the string value of the -``OTEL_TRACES_SAMPLER_ARG`` environment variable. If ``OTEL_TRACES_SAMPLER_ARG`` is not configured, the input will be an empty string. For example: +When utilizing a configurator, you can configure a custom sampler. In order to create a configurable custom sampler, create an entry point for the custom sampler +factory method or function under the entry point group, ``opentelemetry_traces_sampler``. The custom sampler factory method must be of type ``Callable[[str], Sampler]``, taking a single string argument and +returning a Sampler object. The single input will come from the string value of the ``OTEL_TRACES_SAMPLER_ARG`` environment variable. If ``OTEL_TRACES_SAMPLER_ARG`` is not configured, the input will +be an empty string. For example: .. code:: python @@ -134,7 +136,7 @@ class CustomSamplerFactory: import os from logging import getLogger from types import MappingProxyType -from typing import Callable, Optional, Sequence +from typing import Optional, Sequence # pylint: disable=unused-import from opentelemetry.context import Context @@ -142,7 +144,6 @@ class CustomSamplerFactory: OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) -from opentelemetry.sdk.util import _import_config_components from opentelemetry.trace import Link, SpanKind, get_current_span from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes @@ -193,9 +194,6 @@ def __init__( self.trace_state = trace_state -_OTEL_SAMPLER_ENTRY_POINT_GROUP = "opentelemetry_traces_sampler" - - class Sampler(abc.ABC): @abc.abstractmethod def should_sample( @@ -407,37 +405,22 @@ def __init__(self, rate: float): def _get_from_env_or_default() -> Sampler: - traces_sampler_name = os.getenv( + trace_sampler = os.getenv( OTEL_TRACES_SAMPLER, "parentbased_always_on" ).lower() + if trace_sampler not in _KNOWN_SAMPLERS: + _logger.warning("Couldn't recognize sampler %s.", trace_sampler) + trace_sampler = "parentbased_always_on" - if traces_sampler_name in _KNOWN_SAMPLERS: - if traces_sampler_name in ("traceidratio", "parentbased_traceidratio"): - try: - rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG)) - except ValueError: - _logger.warning( - "Could not convert TRACES_SAMPLER_ARG to float." - ) - rate = 1.0 - return _KNOWN_SAMPLERS[traces_sampler_name](rate) - return _KNOWN_SAMPLERS[traces_sampler_name] - try: - traces_sampler_factory = _import_sampler_factory(traces_sampler_name) - sampler_arg = os.getenv(OTEL_TRACES_SAMPLER_ARG, "") - traces_sampler = traces_sampler_factory(sampler_arg) - if not isinstance(traces_sampler, Sampler): - message = f"Traces sampler factory, {traces_sampler_factory}, produced output, {traces_sampler}, which is not a Sampler object." - _logger.warning(message) - raise ValueError(message) - return traces_sampler - except Exception as exc: # pylint: disable=broad-except - _logger.warning( - "Using default sampler. Failed to initialize custom sampler, %s: %s", - traces_sampler_name, - exc, - ) - return _KNOWN_SAMPLERS["parentbased_always_on"] + if trace_sampler in ("traceidratio", "parentbased_traceidratio"): + try: + rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG)) + except ValueError: + _logger.warning("Could not convert TRACES_SAMPLER_ARG to float.") + rate = 1.0 + return _KNOWN_SAMPLERS[trace_sampler](rate) + + return _KNOWN_SAMPLERS[trace_sampler] def _get_parent_trace_state(parent_context) -> Optional["TraceState"]: @@ -445,10 +428,3 @@ def _get_parent_trace_state(parent_context) -> Optional["TraceState"]: if parent_span_context is None or not parent_span_context.is_valid: return None return parent_span_context.trace_state - - -def _import_sampler_factory(sampler_name: str) -> Callable[[str], Sampler]: - _, sampler_impl = _import_config_components( - [sampler_name.strip()], _OTEL_SAMPLER_ENTRY_POINT_GROUP - )[0] - return sampler_impl diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index 5210424353..e1857d8e62 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -14,11 +14,11 @@ import datetime import threading -from collections import OrderedDict, abc, deque -from typing import List, Optional, Sequence, Tuple +from collections import OrderedDict, deque +from collections.abc import MutableMapping, Sequence +from typing import Optional from deprecated import deprecated -from pkg_resources import iter_entry_points def ns_to_iso_str(nanoseconds): @@ -41,27 +41,7 @@ def get_dict_as_key(labels): ) -def _import_config_components( - selected_components: List[str], entry_point_name: str -) -> Sequence[Tuple[str, object]]: - component_entry_points = { - ep.name: ep for ep in iter_entry_points(entry_point_name) - } - component_impls = [] - for selected_component in selected_components: - entry_point = component_entry_points.get(selected_component, None) - if not entry_point: - raise RuntimeError( - f"Requested component '{selected_component}' not found in entry points for '{entry_point_name}'" - ) - - component_impl = entry_point.load() - component_impls.append((selected_component, component_impl)) - - return component_impls - - -class BoundedList(abc.Sequence): +class BoundedList(Sequence): """An append only list with a fixed max size. Calls to `append` and `extend` will drop the oldest elements if there is @@ -112,7 +92,7 @@ def from_seq(cls, maxlen, seq): @deprecated(version="1.4.0") # type: ignore -class BoundedDict(abc.MutableMapping): +class BoundedDict(MutableMapping): """An ordered dict with a fixed max capacity. Oldest elements are dropped when the dict is full and a new element is diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 947ae623bc..a27c7a49a1 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -16,11 +16,12 @@ import logging from os import environ -from typing import Dict, Iterable, Optional +from typing import Dict, Iterable, Optional, Sequence from unittest import TestCase from unittest.mock import patch from opentelemetry import trace +from opentelemetry.context import Context from opentelemetry.environment_variables import OTEL_PYTHON_ID_GENERATOR from opentelemetry.sdk._configuration import ( _EXPORTER_OTLP, @@ -28,8 +29,10 @@ _EXPORTER_OTLP_PROTO_HTTP, _get_exporter_names, _get_id_generator, + _get_sampler, _import_exporters, _import_id_generator, + _import_sampler, _init_logging, _init_metrics, _init_tracing, @@ -37,6 +40,10 @@ ) from opentelemetry.sdk._logs import LoggingHandler from opentelemetry.sdk._logs.export import ConsoleLogExporter +from opentelemetry.sdk.environment_variables import ( + OTEL_TRACES_SAMPLER, + OTEL_TRACES_SAMPLER_ARG, +) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, @@ -49,10 +56,22 @@ from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator +from opentelemetry.sdk.trace.sampling import ( + ALWAYS_ON, + Decision, + ParentBased, + Sampler, + SamplingResult, + TraceIdRatioBased, +) +from opentelemetry.trace import Link, SpanKind +from opentelemetry.trace.span import TraceState +from opentelemetry.util.types import Attributes class Provider: - def __init__(self, resource=None, id_generator=None): + def __init__(self, resource=None, sampler=None, id_generator=None): + self.sampler = sampler self.id_generator = id_generator self.processor = None self.resource = resource or Resource.create({}) @@ -175,6 +194,73 @@ def shutdown(self): pass +class CustomSampler(Sampler): + def __init__(self) -> None: + pass + + def get_description(self) -> str: + return "CustomSampler" + + def should_sample( + self, + parent_context: Optional["Context"], + trace_id: int, + name: str, + kind: SpanKind = None, + attributes: Attributes = None, + links: Sequence[Link] = None, + trace_state: TraceState = None, + ) -> "SamplingResult": + return SamplingResult( + Decision.RECORD_AND_SAMPLE, + None, + None, + ) + + +class CustomRatioSampler(TraceIdRatioBased): + def __init__(self, ratio): + if not isinstance(ratio, float): + raise ValueError( + "CustomRatioSampler ratio argument is not a float." + ) + self.ratio = ratio + super().__init__(ratio) + + def get_description(self) -> str: + return "CustomSampler" + + def should_sample( + self, + parent_context: Optional["Context"], + trace_id: int, + name: str, + kind: SpanKind = None, + attributes: Attributes = None, + links: Sequence[Link] = None, + trace_state: TraceState = None, + ) -> "SamplingResult": + return SamplingResult( + Decision.RECORD_AND_SAMPLE, + None, + None, + ) + + +class CustomSamplerFactory: + @staticmethod + def get_custom_sampler(unused_sampler_arg): + return CustomSampler() + + @staticmethod + def get_custom_ratio_sampler(sampler_arg): + return CustomRatioSampler(float(sampler_arg)) + + @staticmethod + def empty_get_custom_sampler(sampler_arg): + return + + class CustomIdGenerator(IdGenerator): def generate_span_id(self): pass @@ -220,7 +306,11 @@ def tearDown(self): environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=my-test-service"} ) def test_trace_init_default(self): - _init_tracing({"zipkin": Exporter}, RandomIdGenerator, "test-version") + _init_tracing( + {"zipkin": Exporter}, + id_generator=RandomIdGenerator(), + auto_instrumentation_version="test-version", + ) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] @@ -241,7 +331,9 @@ def test_trace_init_default(self): {"OTEL_RESOURCE_ATTRIBUTES": "service.name=my-otlp-test-service"}, ) def test_trace_init_otlp(self): - _init_tracing({"otlp": OTLPSpanExporter}, RandomIdGenerator) + _init_tracing( + {"otlp": OTLPSpanExporter}, id_generator=RandomIdGenerator() + ) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] @@ -257,7 +349,7 @@ def test_trace_init_otlp(self): @patch.dict(environ, {OTEL_PYTHON_ID_GENERATOR: "custom_id_generator"}) @patch("opentelemetry.sdk._configuration.IdGenerator", new=IdGenerator) - @patch("opentelemetry.sdk.util.iter_entry_points") + @patch("opentelemetry.sdk._configuration.iter_entry_points") def test_trace_init_custom_id_generator(self, mock_iter_entry_points): mock_iter_entry_points.configure_mock( return_value=[ @@ -266,10 +358,194 @@ def test_trace_init_custom_id_generator(self, mock_iter_entry_points): ) id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) - _init_tracing({}, id_generator) + _init_tracing({}, id_generator=id_generator) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider.id_generator, CustomIdGenerator) + @patch.dict( + "os.environ", {OTEL_TRACES_SAMPLER: "non_existent_entry_point"} + ) + def test_trace_init_custom_sampler_with_env_non_existent_entry_point(self): + sampler_name = _get_sampler() + sampler = _import_sampler(sampler_name) + _init_tracing({}, sampler=sampler) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsNone(provider.sampler) + + @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch.dict("os.environ", {OTEL_TRACES_SAMPLER: "custom_sampler_factory"}) + def test_trace_init_custom_sampler_with_env(self, mock_iter_entry_points): + mock_iter_entry_points.configure_mock( + return_value=[ + IterEntryPoint( + "custom_sampler_factory", + CustomSamplerFactory.get_custom_sampler, + ) + ] + ) + sampler_name = _get_sampler() + sampler = _import_sampler(sampler_name) + _init_tracing({}, sampler=sampler) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider.sampler, CustomSampler) + + @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch.dict("os.environ", {OTEL_TRACES_SAMPLER: "custom_sampler_factory"}) + def test_trace_init_custom_sampler_with_env_bad_factory( + self, mock_iter_entry_points + ): + mock_iter_entry_points.configure_mock( + return_value=[ + IterEntryPoint( + "custom_sampler_factory", + CustomSamplerFactory.empty_get_custom_sampler, + ) + ] + ) + sampler_name = _get_sampler() + sampler = _import_sampler(sampler_name) + _init_tracing({}, sampler=sampler) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsNone(provider.sampler) + + @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch.dict( + "os.environ", + { + OTEL_TRACES_SAMPLER: "custom_sampler_factory", + OTEL_TRACES_SAMPLER_ARG: "0.5", + }, + ) + def test_trace_init_custom_sampler_with_env_unused_arg( + self, mock_iter_entry_points + ): + mock_iter_entry_points.configure_mock( + return_value=[ + IterEntryPoint( + "custom_sampler_factory", + CustomSamplerFactory.get_custom_sampler, + ) + ] + ) + sampler_name = _get_sampler() + sampler = _import_sampler(sampler_name) + _init_tracing({}, sampler=sampler) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider.sampler, CustomSampler) + + @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch.dict( + "os.environ", + { + OTEL_TRACES_SAMPLER: "custom_ratio_sampler_factory", + OTEL_TRACES_SAMPLER_ARG: "0.5", + }, + ) + def test_trace_init_custom_ratio_sampler_with_env( + self, mock_iter_entry_points + ): + mock_iter_entry_points.configure_mock( + return_value=[ + IterEntryPoint( + "custom_ratio_sampler_factory", + CustomSamplerFactory.get_custom_ratio_sampler, + ) + ] + ) + sampler_name = _get_sampler() + sampler = _import_sampler(sampler_name) + _init_tracing({}, sampler=sampler) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider.sampler, CustomRatioSampler) + self.assertEqual(provider.sampler.ratio, 0.5) + + @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch.dict( + "os.environ", + { + OTEL_TRACES_SAMPLER: "custom_ratio_sampler_factory", + OTEL_TRACES_SAMPLER_ARG: "foobar", + }, + ) + def test_trace_init_custom_ratio_sampler_with_env_bad_arg( + self, mock_iter_entry_points + ): + mock_iter_entry_points.configure_mock( + return_value=[ + IterEntryPoint( + "custom_ratio_sampler_factory", + CustomSamplerFactory.get_custom_ratio_sampler, + ) + ] + ) + sampler_name = _get_sampler() + sampler = _import_sampler(sampler_name) + _init_tracing({}, sampler=sampler) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsNone(provider.sampler) + + @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch.dict( + "os.environ", + { + OTEL_TRACES_SAMPLER: "custom_ratio_sampler_factory", + }, + ) + def test_trace_init_custom_ratio_sampler_with_env_missing_arg( + self, mock_iter_entry_points + ): + mock_iter_entry_points.configure_mock( + return_value=[ + IterEntryPoint( + "custom_ratio_sampler_factory", + CustomSamplerFactory.get_custom_ratio_sampler, + ) + ] + ) + sampler_name = _get_sampler() + sampler = _import_sampler(sampler_name) + _init_tracing({}, sampler=sampler) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsNone(provider.sampler) + + @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch.dict( + "os.environ", + { + OTEL_TRACES_SAMPLER: "custom_sampler_factory", + OTEL_TRACES_SAMPLER_ARG: "0.5", + }, + ) + def test_trace_init_custom_ratio_sampler_with_env_multiple_entry_points( + self, mock_iter_entry_points + ): + mock_iter_entry_points.configure_mock( + return_value=[ + IterEntryPoint( + "custom_ratio_sampler_factory", + CustomSamplerFactory.get_custom_ratio_sampler, + ), + IterEntryPoint( + "custom_sampler_factory", + CustomSamplerFactory.get_custom_sampler, + ), + IterEntryPoint( + "custom_z_sampler_factory", + CustomSamplerFactory.empty_get_custom_sampler, + ), + ] + ) + sampler_name = _get_sampler() + sampler = _import_sampler(sampler_name) + _init_tracing({}, sampler=sampler) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider.sampler, CustomSampler) + + def verify_default_sampler(self, tracer_provider): + self.assertIsInstance(tracer_provider.sampler, ParentBased) + # pylint: disable=protected-access + self.assertEqual(tracer_provider.sampler._root, ALWAYS_ON) + class TestLoggingInit(TestCase): def setUp(self): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 3f4d0d0da1..5bb8d87c65 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -21,7 +21,7 @@ from logging import ERROR, WARNING from random import randint from time import time_ns -from typing import Optional, Sequence +from typing import Optional from unittest import mock from opentelemetry import trace as trace_api @@ -46,10 +46,7 @@ ALWAYS_ON, Decision, ParentBased, - Sampler, - SamplingResult, StaticSampler, - TraceIdRatioBased, ) from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo @@ -57,9 +54,7 @@ get_span_with_dropped_attributes_events_links, new_tracer, ) -from opentelemetry.trace import Link, SpanKind, Status, StatusCode -from opentelemetry.trace.span import TraceState -from opentelemetry.util.types import Attributes +from opentelemetry.trace import Status, StatusCode class TestTracer(unittest.TestCase): @@ -151,78 +146,6 @@ def test_tracer_provider_accepts_concurrent_multi_span_processor(self): ) -class CustomSampler(Sampler): - def __init__(self) -> None: - pass - - def get_description(self) -> str: - return "CustomSampler" - - def should_sample( - self, - parent_context: Optional["Context"], - trace_id: int, - name: str, - kind: SpanKind = None, - attributes: Attributes = None, - links: Sequence[Link] = None, - trace_state: TraceState = None, - ) -> "SamplingResult": - return SamplingResult( - Decision.RECORD_AND_SAMPLE, - None, - None, - ) - - -class CustomRatioSampler(TraceIdRatioBased): - def __init__(self, ratio): - self.ratio = ratio - super().__init__(ratio) - - def get_description(self) -> str: - return "CustomSampler" - - def should_sample( - self, - parent_context: Optional["Context"], - trace_id: int, - name: str, - kind: SpanKind = None, - attributes: Attributes = None, - links: Sequence[Link] = None, - trace_state: TraceState = None, - ) -> "SamplingResult": - return SamplingResult( - Decision.RECORD_AND_SAMPLE, - None, - None, - ) - - -class CustomSamplerFactory: - @staticmethod - def get_custom_sampler(unused_sampler_arg): - return CustomSampler() - - @staticmethod - def get_custom_ratio_sampler(sampler_arg): - return CustomRatioSampler(float(sampler_arg)) - - @staticmethod - def empty_get_custom_sampler(sampler_arg): - return - - -class IterEntryPoint: - def __init__(self, name, class_type): - self.name = name - self.class_type = class_type - - def load(self): - return self.class_type - - class TestTracerSampling(unittest.TestCase): def tearDown(self): reload(trace) @@ -251,7 +174,8 @@ def test_default_sampler_type(self): tracer_provider = trace.TracerProvider() self.verify_default_sampler(tracer_provider) - def test_sampler_no_sampling(self): + @mock.patch("opentelemetry.sdk.trace.sampling._get_from_env_or_default") + def test_sampler_no_sampling(self, _get_from_env_or_default): tracer_provider = trace.TracerProvider(ALWAYS_OFF) tracer = tracer_provider.get_tracer(__name__) @@ -270,6 +194,7 @@ def test_sampler_no_sampling(self): child_span.get_span_context().trace_flags, trace_api.TraceFlags.DEFAULT, ) + self.assertFalse(_get_from_env_or_default.called) @mock.patch.dict("os.environ", {OTEL_TRACES_SAMPLER: "always_off"}) def test_sampler_with_env(self): @@ -299,161 +224,6 @@ def test_ratio_sampler_with_env(self): self.assertIsInstance(tracer_provider.sampler, ParentBased) self.assertEqual(tracer_provider.sampler._root.rate, 0.25) - @mock.patch.dict( - "os.environ", {OTEL_TRACES_SAMPLER: "non_existent_entry_point"} - ) - def test_sampler_with_env_non_existent_entry_point(self): - # pylint: disable=protected-access - reload(trace) - tracer_provider = trace.TracerProvider() - self.verify_default_sampler(tracer_provider) - - @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") - @mock.patch.dict( - "os.environ", {OTEL_TRACES_SAMPLER: "custom_sampler_factory"} - ) - def test_custom_sampler_with_env(self, mock_iter_entry_points): - mock_iter_entry_points.return_value = [ - IterEntryPoint( - "custom_sampler_factory", - CustomSamplerFactory.get_custom_sampler, - ) - ] - # pylint: disable=protected-access - reload(trace) - tracer_provider = trace.TracerProvider() - self.assertIsInstance(tracer_provider.sampler, CustomSampler) - - @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") - @mock.patch.dict( - "os.environ", {OTEL_TRACES_SAMPLER: "custom_sampler_factory"} - ) - def test_custom_sampler_with_env_bad_factory(self, mock_iter_entry_points): - mock_iter_entry_points.return_value = [ - IterEntryPoint( - "custom_sampler_factory", - CustomSamplerFactory.empty_get_custom_sampler, - ) - ] - # pylint: disable=protected-access - reload(trace) - tracer_provider = trace.TracerProvider() - self.verify_default_sampler(tracer_provider) - - @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") - @mock.patch.dict( - "os.environ", - { - OTEL_TRACES_SAMPLER: "custom_sampler_factory", - OTEL_TRACES_SAMPLER_ARG: "0.5", - }, - ) - def test_custom_sampler_with_env_unused_arg(self, mock_iter_entry_points): - mock_iter_entry_points.return_value = [ - IterEntryPoint( - "custom_sampler_factory", - CustomSamplerFactory.get_custom_sampler, - ) - ] - # pylint: disable=protected-access - reload(trace) - tracer_provider = trace.TracerProvider() - self.assertIsInstance(tracer_provider.sampler, CustomSampler) - - @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") - @mock.patch.dict( - "os.environ", - { - OTEL_TRACES_SAMPLER: "custom_ratio_sampler_factory", - OTEL_TRACES_SAMPLER_ARG: "0.5", - }, - ) - def test_custom_ratio_sampler_with_env(self, mock_iter_entry_points): - mock_iter_entry_points.return_value = [ - IterEntryPoint( - "custom_ratio_sampler_factory", - CustomSamplerFactory.get_custom_ratio_sampler, - ) - ] - # pylint: disable=protected-access - reload(trace) - tracer_provider = trace.TracerProvider() - self.assertIsInstance(tracer_provider.sampler, CustomRatioSampler) - self.assertEqual(tracer_provider.sampler.ratio, 0.5) - - @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") - @mock.patch.dict( - "os.environ", - { - OTEL_TRACES_SAMPLER: "custom_ratio_sampler_factory", - OTEL_TRACES_SAMPLER_ARG: "foobar", - }, - ) - def test_custom_ratio_sampler_with_env_bad_arg( - self, mock_iter_entry_points - ): - mock_iter_entry_points.return_value = [ - IterEntryPoint( - "custom_ratio_sampler_factory", - CustomSamplerFactory.get_custom_ratio_sampler, - ) - ] - # pylint: disable=protected-access - reload(trace) - tracer_provider = trace.TracerProvider() - self.verify_default_sampler(tracer_provider) - - @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") - @mock.patch.dict( - "os.environ", - { - OTEL_TRACES_SAMPLER: "custom_ratio_sampler_factory", - }, - ) - def test_custom_ratio_sampler_with_env_no_arg( - self, mock_iter_entry_points - ): - mock_iter_entry_points.return_value = [ - IterEntryPoint( - "custom_ratio_sampler_factory", - CustomSamplerFactory.get_custom_ratio_sampler, - ) - ] - # pylint: disable=protected-access - reload(trace) - tracer_provider = trace.TracerProvider() - self.verify_default_sampler(tracer_provider) - - @mock.patch("opentelemetry.sdk.trace.util.iter_entry_points") - @mock.patch.dict( - "os.environ", - { - OTEL_TRACES_SAMPLER: "custom_sampler_factory", - OTEL_TRACES_SAMPLER_ARG: "0.5", - }, - ) - def test_custom_ratio_sampler_with_env_multiple_entry_points( - self, mock_iter_entry_points - ): - mock_iter_entry_points.return_value = [ - IterEntryPoint( - "custom_ratio_sampler_factory", - CustomSamplerFactory.get_custom_ratio_sampler, - ), - IterEntryPoint( - "custom_sampler_factory", - CustomSamplerFactory.get_custom_sampler, - ), - IterEntryPoint( - "custom_z_sampler_factory", - CustomSamplerFactory.empty_get_custom_sampler, - ), - ] - # pylint: disable=protected-access - reload(trace) - tracer_provider = trace.TracerProvider() - self.assertIsInstance(tracer_provider.sampler, CustomSampler) - def verify_default_sampler(self, tracer_provider): self.assertIsInstance(tracer_provider.sampler, ParentBased) # pylint: disable=protected-access From 5574f7ea6b63c0fd7f5b32be4b0ff030f29b9efe Mon Sep 17 00:00:00 2001 From: Lance Erickson Date: Tue, 22 Nov 2022 12:54:37 -0600 Subject: [PATCH 1347/1517] Avoid generator in _ViewInstrumentMatch.collect() (#3035) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ CONTRIBUTING.md | 2 +- .../sdk/metrics/_internal/_view_instrument_match.py | 8 +++++--- opentelemetry-sdk/tests/metrics/test_metrics.py | 4 ++-- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a75f357623..c452961526 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: c6134843900e2eeb1b8b3383a897b38cc0905c38 + CONTRIB_REPO_SHA: main # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 030a8a3745..624a41a467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3026](https://github.com/open-telemetry/opentelemetry-python/pull/3026)) - Add missing entry points for OTLP/HTTP exporter ([#3027](https://github.com/open-telemetry/opentelemetry-python/pull/3027)) +- Fix: Avoid generator in metrics _ViewInstrumentMatch.collect() + ([#3035](https://github.com/open-telemetry/opentelemetry-python/pull/3035) ## Version 1.14.0/0.35b0 (2022-11-04) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ef351b38c..0653deb60c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ Please take a look at this list first, your contributions may belong in one of t # Find the right branch -The default branch for this repo is `main`. Changes that pertain to `metrics` go into the `metrics` branch. Any changes that pertain to components marked as `stable` in the [specifications](https://github.com/open-telemetry/opentelemetry-specification) or anything that is not `metrics` related go into this branch. +The default branch for this repo is `main`. All feature work is accomplished on branches from `main`. ## Find a Buddy and get Started Quickly! diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index 4cff25ae78..ab4645c82f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -16,7 +16,7 @@ from logging import getLogger from threading import Lock from time import time_ns -from typing import Dict, Iterable +from typing import Dict, List, Sequence from opentelemetry.metrics import Instrument from opentelemetry.sdk.metrics._internal.aggregation import ( @@ -126,12 +126,14 @@ def collect( self, aggregation_temporality: AggregationTemporality, collection_start_nanos: int, - ) -> Iterable[DataPointT]: + ) -> Sequence[DataPointT]: + data_points: List[DataPointT] = [] with self._lock: for aggregation in self._attributes_aggregation.values(): data_point = aggregation.collect( aggregation_temporality, collection_start_nanos ) if data_point is not None: - yield data_point + data_points.append(data_point) + return data_points diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index b128456531..13efbb91c0 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -514,11 +514,11 @@ def test_duplicate_instrument_aggregate_data(self): self.assertEqual(metric_0.name, "counter") self.assertEqual(metric_0.unit, "unit") self.assertEqual(metric_0.description, "description") - self.assertEqual(next(metric_0.data.data_points).value, 3) + self.assertEqual(next(iter(metric_0.data.data_points)).value, 3) metric_1 = scope_metrics[1].metrics[0] self.assertEqual(metric_1.name, "counter") self.assertEqual(metric_1.unit, "unit") self.assertEqual(metric_1.description, "description") - self.assertEqual(next(metric_1.data.data_points).value, 7) + self.assertEqual(next(iter(metric_1.data.data_points)).value, 7) From a11c0ccf416c4fde4dfeb2a970c72e4701ad0695 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 28 Nov 2022 20:15:53 +0100 Subject: [PATCH 1348/1517] Suppress warning for SDK instantiation of InstrumentationInfo (#3041) --- .../src/opentelemetry/sdk/trace/__init__.py | 15 ++++++++++----- opentelemetry-sdk/tests/trace/test_trace.py | 8 +++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0ddf531a4d..739f59ee70 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -37,6 +37,7 @@ Type, Union, ) +from warnings import filterwarnings, resetwarnings from deprecated import deprecated @@ -1167,16 +1168,20 @@ def get_tracer( logger.error("get_tracer called with missing module name.") if instrumenting_library_version is None: instrumenting_library_version = "" + + filterwarnings("ignore", category=DeprecationWarning) + instrumentation_info = InstrumentationInfo( + instrumenting_module_name, + instrumenting_library_version, + schema_url, + ) + resetwarnings() return Tracer( self.sampler, self.resource, self._active_span_processor, self.id_generator, - InstrumentationInfo( - instrumenting_module_name, - instrumenting_library_version, - schema_url, - ), + instrumentation_info, self._span_limits, InstrumentationScope( instrumenting_module_name, diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 5bb8d87c65..7b86fa7247 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -23,6 +23,7 @@ from time import time_ns from typing import Optional from unittest import mock +from unittest.mock import Mock from opentelemetry import trace as trace_api from opentelemetry.context import Context @@ -39,7 +40,7 @@ OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) -from opentelemetry.sdk.trace import Resource +from opentelemetry.sdk.trace import Resource, TracerProvider from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.trace.sampling import ( ALWAYS_OFF, @@ -58,6 +59,11 @@ class TestTracer(unittest.TestCase): + def test_no_deprecated_warning(self): + with self.assertRaises(AssertionError): + with self.assertWarns(DeprecationWarning): + TracerProvider(Mock(), Mock()).get_tracer(Mock(), Mock()) + def test_extends_api(self): tracer = new_tracer() self.assertIsInstance(tracer, trace.Tracer) From 501bfa5ce7b31f84204573ddf26c1304ae9d75ef Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 29 Nov 2022 18:44:18 +0100 Subject: [PATCH 1349/1517] Refactor comments in .flake8 (#3057) --- .flake8 | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.flake8 b/.flake8 index cd5fe860b5..292bb01461 100644 --- a/.flake8 +++ b/.flake8 @@ -1,9 +1,17 @@ [flake8] ignore = - E501 # line too long, defer to black - W503 # allow line breaks before binary ops - W504 # allow line breaks after binary ops - E203 # allow whitespace before ':' (https://github.com/psf/black#slices) + # line too long, defer to black + E501 + + # allow line breaks before binary ops + W503 + + # allow line breaks after binary ops + W504 + + # allow whitespace before ':' (https://github.com/psf/black#slices) + E203 + exclude = .bzr .git From 8a0ce154ae27a699598cbf3ccc6396eb012902d6 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 29 Nov 2022 10:44:03 -0800 Subject: [PATCH 1350/1517] Update logging to include logging api as per specification (#3038) --- CHANGELOG.md | 2 + docs/api/_logs.rst | 14 ++ docs/api/_logs.severity.rst | 4 + docs/api/index.rst | 1 + docs/sdk/_logs.rst | 7 + docs/sdk/index.rst | 2 +- docs/sdk/logs.export.rst | 7 - docs/sdk/logs.rst | 22 -- docs/sdk/logs.severity.rst | 7 - .../tests/logs/test_otlp_logs_exporter.py | 10 +- .../tests/test_proto_log_exporter.py | 2 +- .../src/opentelemetry/_logs/__init__.py | 58 +++++ .../opentelemetry/_logs/_internal/__init__.py | 227 ++++++++++++++++++ .../opentelemetry/_logs/severity/__init__.py | 8 +- .../opentelemetry/environment_variables.py | 5 + .../tests/logs/test_logger_provider.py | 62 +++++ .../sdk/_configuration/__init__.py | 7 +- .../src/opentelemetry/sdk/_logs/__init__.py | 116 +++------ .../sdk/environment_variables.py | 8 - opentelemetry-sdk/tests/logs/test_export.py | 2 +- .../tests/logs/test_global_provider.py | 75 ------ opentelemetry-sdk/tests/logs/test_handler.py | 18 +- opentelemetry-sdk/tests/logs/test_logs.py | 58 +++++ .../tests/logs/test_multi_log_prcessor.py | 2 +- .../src/opentelemetry/test/globals_test.py | 9 + 25 files changed, 510 insertions(+), 223 deletions(-) create mode 100644 docs/api/_logs.rst create mode 100644 docs/api/_logs.severity.rst create mode 100644 docs/sdk/_logs.rst delete mode 100644 docs/sdk/logs.export.rst delete mode 100644 docs/sdk/logs.rst delete mode 100644 docs/sdk/logs.severity.rst create mode 100644 opentelemetry-api/src/opentelemetry/_logs/__init__.py create mode 100644 opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py rename opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py => opentelemetry-api/src/opentelemetry/_logs/severity/__init__.py (91%) create mode 100644 opentelemetry-api/tests/logs/test_logger_provider.py delete mode 100644 opentelemetry-sdk/tests/logs/test_global_provider.py create mode 100644 opentelemetry-sdk/tests/logs/test_logs.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 624a41a467..00748e5ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3026](https://github.com/open-telemetry/opentelemetry-python/pull/3026)) - Add missing entry points for OTLP/HTTP exporter ([#3027](https://github.com/open-telemetry/opentelemetry-python/pull/3027)) +- Update logging to include logging api as per specification + ([#3038](https://github.com/open-telemetry/opentelemetry-python/pull/3038)) - Fix: Avoid generator in metrics _ViewInstrumentMatch.collect() ([#3035](https://github.com/open-telemetry/opentelemetry-python/pull/3035) diff --git a/docs/api/_logs.rst b/docs/api/_logs.rst new file mode 100644 index 0000000000..85ae72dc0d --- /dev/null +++ b/docs/api/_logs.rst @@ -0,0 +1,14 @@ +opentelemetry._logs package +============================= + +Submodules +---------- + +.. toctree:: + + _logs.severity + +Module contents +--------------- + +.. automodule:: opentelemetry._logs diff --git a/docs/api/_logs.severity.rst b/docs/api/_logs.severity.rst new file mode 100644 index 0000000000..4e31e70cf8 --- /dev/null +++ b/docs/api/_logs.severity.rst @@ -0,0 +1,4 @@ +opentelemetry._logs.severity +============================ + +.. automodule:: opentelemetry._logs.severity \ No newline at end of file diff --git a/docs/api/index.rst b/docs/api/index.rst index 22d77fc5a0..c1dffd6e75 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -6,6 +6,7 @@ OpenTelemetry Python API .. toctree:: :maxdepth: 1 + _logs baggage context propagate diff --git a/docs/sdk/_logs.rst b/docs/sdk/_logs.rst new file mode 100644 index 0000000000..185e7006e4 --- /dev/null +++ b/docs/sdk/_logs.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk._logs package +=============================== + +.. automodule:: opentelemetry.sdk._logs + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sdk/index.rst b/docs/sdk/index.rst index 47d49a5550..d5d3688443 100644 --- a/docs/sdk/index.rst +++ b/docs/sdk/index.rst @@ -6,9 +6,9 @@ OpenTelemetry Python SDK .. toctree:: :maxdepth: 1 + _logs resources trace metrics - logs error_handler environment_variables diff --git a/docs/sdk/logs.export.rst b/docs/sdk/logs.export.rst deleted file mode 100644 index 19a4023742..0000000000 --- a/docs/sdk/logs.export.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.sdk._logs.export -============================== - -.. automodule:: opentelemetry.sdk._logs.export - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/sdk/logs.rst b/docs/sdk/logs.rst deleted file mode 100644 index 569ad30b79..0000000000 --- a/docs/sdk/logs.rst +++ /dev/null @@ -1,22 +0,0 @@ -opentelemetry.sdk._logs package -=============================== - -.. warning:: - OpenTelemetry Python logs are in an experimental state. The APIs within - :mod:`opentelemetry.sdk._logs` are subject to change in minor/patch releases and make no - backward compatibility guarantees at this time. - - Once logs become stable, this package will be be renamed to ``opentelemetry.sdk.logs``. - -Submodules ----------- - -.. toctree:: - - logs.export - logs.severity - -.. automodule:: opentelemetry.sdk._logs - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/sdk/logs.severity.rst b/docs/sdk/logs.severity.rst deleted file mode 100644 index 1197e8b44e..0000000000 --- a/docs/sdk/logs.severity.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.sdk._logs.severity -================================ - -.. automodule:: opentelemetry.sdk._logs.severity - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index 0761609083..c5f0fcd92e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -21,6 +21,7 @@ from google.rpc.error_details_pb2 import RetryInfo from grpc import StatusCode, server +from opentelemetry._logs import SeverityNumber from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter, ) @@ -45,9 +46,6 @@ ) from opentelemetry.sdk._logs import LogData, LogRecord from opentelemetry.sdk._logs.export import LogExportResult -from opentelemetry.sdk._logs.severity import ( - SeverityNumber as SDKSeverityNumber, -) from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import TraceFlags @@ -117,7 +115,7 @@ def setUp(self): span_id=5213367945872657620, trace_flags=TraceFlags(0x01), severity_text="WARNING", - severity_number=SDKSeverityNumber.WARN, + severity_number=SeverityNumber.WARN, body="Zhengzhou, We have a heaviest rains in 1000 years", resource=SDKResource({"key": "value"}), attributes={"a": 1, "b": "c"}, @@ -133,7 +131,7 @@ def setUp(self): span_id=5213367945872657623, trace_flags=TraceFlags(0x01), severity_text="INFO", - severity_number=SDKSeverityNumber.INFO2, + severity_number=SeverityNumber.INFO2, body="Sydney, Opera House is closed", resource=SDKResource({"key": "value"}), attributes={"custom_attr": [1, 2, 3]}, @@ -149,7 +147,7 @@ def setUp(self): span_id=5213367945872657628, trace_flags=TraceFlags(0x01), severity_text="ERROR", - severity_number=SDKSeverityNumber.WARN, + severity_number=SeverityNumber.WARN, body="Mumbai, Boil water before drinking", resource=SDKResource({"service": "myapp"}), ), diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index d94ddfb8f7..7dcb82030d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -21,6 +21,7 @@ import requests import responses +from opentelemetry._logs import SeverityNumber from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._log_exporter import ( DEFAULT_COMPRESSION, @@ -54,7 +55,6 @@ ) from opentelemetry.sdk._logs import LogData from opentelemetry.sdk._logs import LogRecord as SDKLogRecord -from opentelemetry.sdk._logs.severity import SeverityNumber from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_COMPRESSION, diff --git a/opentelemetry-api/src/opentelemetry/_logs/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/__init__.py new file mode 100644 index 0000000000..d9677a5f22 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/_logs/__init__.py @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +The OpenTelemetry logging API describes the classes used to generate logs and events. + +The :class:`.LoggerProvider` provides users access to the :class:`.Logger` which in +turn is used to create :class:`.Event` and :class:`.Log` objects. + +This module provides abstract (i.e. unimplemented) classes required for +logging, and a concrete no-op implementation :class:`.NoOpLogger` that allows applications +to use the API package alone without a supporting implementation. + +To get a logger, you need to provide the package name from which you are +calling the logging APIs to OpenTelemetry by calling `LoggerProvider.get_logger` +with the calling module name and the version of your package. + +The following code shows how to obtain a logger using the global :class:`.LoggerProvider`:: + + from opentelemetry._logs import get_logger + + logger = get_logger("example-logger") + +.. versionadded:: 1.15.0 +""" + +# pylint: disable=unused-import + +from opentelemetry._logs._internal import ( # noqa: F401 + Logger, + LoggerProvider, + LogRecord, + NoOpLogger, + NoOpLoggerProvider, + get_logger, + get_logger_provider, + set_logger_provider, +) +from opentelemetry._logs.severity import ( # noqa: F401 + SeverityNumber, + std_to_otel, +) + +__all__ = [] +for key, value in globals().copy().items(): # type: ignore + if not key.startswith("_"): + value.__module__ = __name__ # type: ignore + __all__.append(key) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py new file mode 100644 index 0000000000..0b844e24d9 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -0,0 +1,227 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +The OpenTelemetry logging API describes the classes used to generate logs and events. + +The :class:`.LoggerProvider` provides users access to the :class:`.Logger` which in +turn is used to create :class:`.Event` and :class:`.Log` objects. + +This module provides abstract (i.e. unimplemented) classes required for +logging, and a concrete no-op implementation :class:`.NoOpLogger` that allows applications +to use the API package alone without a supporting implementation. + +To get a logger, you need to provide the package name from which you are +calling the logging APIs to OpenTelemetry by calling `LoggerProvider.get_logger` +with the calling module name and the version of your package. + +The following code shows how to obtain a logger using the global :class:`.LoggerProvider`:: + + from opentelemetry._logs import get_logger + + logger = get_logger("example-logger") + +.. versionadded:: 1.15.0 +""" + +from abc import ABC, abstractmethod +from logging import getLogger +from os import environ +from typing import Any, Optional, cast + +from opentelemetry._logs.severity import SeverityNumber +from opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER +from opentelemetry.trace.span import TraceFlags +from opentelemetry.util._once import Once +from opentelemetry.util._providers import _load_provider +from opentelemetry.util.types import Attributes + +_logger = getLogger(__name__) + + +class LogRecord(ABC): + """A LogRecord instance represents an event being logged. + + LogRecord instances are created and emitted via `Logger` + every time something is logged. They contain all the information + pertinent to the event being logged. + """ + + def __init__( + self, + timestamp: Optional[int] = None, + observed_timestamp: Optional[int] = None, + trace_id: Optional[int] = None, + span_id: Optional[int] = None, + trace_flags: Optional["TraceFlags"] = None, + severity_text: Optional[str] = None, + severity_number: Optional[SeverityNumber] = None, + body: Optional[Any] = None, + attributes: Optional["Attributes"] = None, + ): + self.timestamp = timestamp + self.observed_timestamp = observed_timestamp + self.trace_id = trace_id + self.span_id = span_id + self.trace_flags = trace_flags + self.severity_text = severity_text + self.severity_number = severity_number + self.body = body # type: ignore + self.attributes = attributes + + +class Logger(ABC): + """Handles emitting events and logs via `LogRecord`.""" + + def __init__( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> None: + super().__init__() + self._name = name + self._version = version + self._schema_url = schema_url + + @abstractmethod + def emit(self, record: "LogRecord") -> None: + """Emits a :class:`LogRecord` representing a log to the processing pipeline.""" + + +class NoOpLogger(Logger): + """The default Logger used when no Logger implementation is available. + + All operations are no-op. + """ + + def emit(self, record: "LogRecord") -> None: + pass + + +class LoggerProvider(ABC): + """ + LoggerProvider is the entry point of the API. It provides access to Logger instances. + """ + + @abstractmethod + def get_logger( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> Logger: + """Returns a `Logger` for use by the given instrumentation library. + + For any two calls it is undefined whether the same or different + `Logger` instances are returned, even for different library names. + + This function may return different `Logger` types (e.g. a no-op logger + vs. a functional logger). + + Args: + name: The name of the instrumenting module. + ``__name__`` may not be used as this can result in + different logger names if the loggers are in different files. + It is better to use a fixed string that can be imported where + needed and used consistently as the name of the logger. + + This should *not* be the name of the module that is + instrumented but the name of the module doing the instrumentation. + E.g., instead of ``"requests"``, use + ``"opentelemetry.instrumentation.requests"``. + + version: Optional. The version string of the + instrumenting library. Usually this should be the same as + ``pkg_resources.get_distribution(instrumenting_library_name).version``. + + schema_url: Optional. Specifies the Schema URL of the emitted telemetry. + """ + + +class NoOpLoggerProvider(LoggerProvider): + """The default LoggerProvider used when no LoggerProvider implementation is available.""" + + def get_logger( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> Logger: + """Returns a NoOpLogger.""" + super().get_logger(name, version=version, schema_url=schema_url) + return NoOpLogger(name, version=version, schema_url=schema_url) + + +# TODO: ProxyLoggerProvider + + +_LOGGER_PROVIDER_SET_ONCE = Once() +_LOGGER_PROVIDER = None + + +def get_logger_provider() -> LoggerProvider: + """Gets the current global :class:`~.LoggerProvider` object.""" + global _LOGGER_PROVIDER # pylint: disable=global-statement + if _LOGGER_PROVIDER is None: + if _OTEL_PYTHON_LOGGER_PROVIDER not in environ.keys(): + # TODO: return proxy + _LOGGER_PROVIDER = NoOpLoggerProvider() + return _LOGGER_PROVIDER + + logger_provider: LoggerProvider = _load_provider( # type: ignore + _OTEL_PYTHON_LOGGER_PROVIDER, "logger_provider" + ) + _set_logger_provider(logger_provider, log=False) + + # _LOGGER_PROVIDER will have been set by one thread + return cast("LoggerProvider", _LOGGER_PROVIDER) + + +def _set_logger_provider(logger_provider: LoggerProvider, log: bool) -> None: + def set_lp() -> None: + global _LOGGER_PROVIDER # pylint: disable=global-statement + _LOGGER_PROVIDER = logger_provider # type: ignore + + did_set = _LOGGER_PROVIDER_SET_ONCE.do_once(set_lp) + + if log and not did_set: + _logger.warning("Overriding of current LoggerProvider is not allowed") + + +def set_logger_provider(meter_provider: LoggerProvider) -> None: + """Sets the current global :class:`~.LoggerProvider` object. + + This can only be done once, a warning will be logged if any further attempt + is made. + """ + _set_logger_provider(meter_provider, log=True) + + +def get_logger( + instrumenting_module_name: str, + instrumenting_library_version: str = "", + logger_provider: Optional[LoggerProvider] = None, +) -> "Logger": + """Returns a `Logger` for use within a python process. + + This function is a convenience wrapper for + opentelemetry.sdk._logs.LoggerProvider.get_logger. + + If logger_provider param is omitted the current configured one is used. + """ + if logger_provider is None: + logger_provider = get_logger_provider() + return logger_provider.get_logger( + instrumenting_module_name, instrumenting_library_version + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py b/opentelemetry-api/src/opentelemetry/_logs/severity/__init__.py similarity index 91% rename from opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py rename to opentelemetry-api/src/opentelemetry/_logs/severity/__init__.py index 2570375990..1daaa19f44 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/severity.py +++ b/opentelemetry-api/src/opentelemetry/_logs/severity/__init__.py @@ -55,7 +55,7 @@ class SeverityNumber(enum.Enum): FATAL4 = 24 -_STD_TO_OTLP = { +_STD_TO_OTEL = { 10: SeverityNumber.DEBUG, 11: SeverityNumber.DEBUG2, 12: SeverityNumber.DEBUG3, @@ -103,13 +103,13 @@ class SeverityNumber(enum.Enum): } -def std_to_otlp(levelno: int) -> SeverityNumber: +def std_to_otel(levelno: int) -> SeverityNumber: """ Map python log levelno as defined in https://docs.python.org/3/library/logging.html#logging-levels - to OTLP log severity number. + to OTel log severity number as defined here: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber """ if levelno < 10: return SeverityNumber.UNSPECIFIED if levelno > 53: return SeverityNumber.FATAL4 - return _STD_TO_OTLP[levelno] + return _STD_TO_OTEL[levelno] diff --git a/opentelemetry-api/src/opentelemetry/environment_variables.py b/opentelemetry-api/src/opentelemetry/environment_variables.py index 5c5a9b3c4a..c54b13c6da 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables.py @@ -53,3 +53,8 @@ """ .. envvar:: OTEL_PYTHON_METER_PROVIDER """ + +_OTEL_PYTHON_LOGGER_PROVIDER = "OTEL_PYTHON_LOGGER_PROVIDER" +""" +.. envvar:: OTEL_PYTHON_LOGGER_PROVIDER +""" diff --git a/opentelemetry-api/tests/logs/test_logger_provider.py b/opentelemetry-api/tests/logs/test_logger_provider.py new file mode 100644 index 0000000000..5943924bd8 --- /dev/null +++ b/opentelemetry-api/tests/logs/test_logger_provider.py @@ -0,0 +1,62 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# type:ignore +import unittest +from unittest.mock import Mock, patch + +import opentelemetry._logs._internal as logs_internal +from opentelemetry._logs import get_logger_provider, set_logger_provider +from opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER +from opentelemetry.test.globals_test import reset_logging_globals + + +class TestGlobals(unittest.TestCase): + def setUp(self): + super().tearDown() + reset_logging_globals() + + def tearDown(self): + super().tearDown() + reset_logging_globals() + + def test_set_logger_provider(self): + lp_mock = Mock() + # pylint: disable=protected-access + assert logs_internal._LOGGER_PROVIDER is None + set_logger_provider(lp_mock) + assert logs_internal._LOGGER_PROVIDER is lp_mock + assert get_logger_provider() is lp_mock + + def test_get_logger_provider(self): + # pylint: disable=protected-access + assert logs_internal._LOGGER_PROVIDER is None + + assert isinstance( + get_logger_provider(), logs_internal.NoOpLoggerProvider + ) + + logs_internal._LOGGER_PROVIDER = None + + with patch.dict( + "os.environ", + {_OTEL_PYTHON_LOGGER_PROVIDER: "test_logger_provider"}, + ): + + with patch("opentelemetry._logs._internal._load_provider", Mock()): + with patch( + "opentelemetry._logs._internal.cast", + Mock(**{"return_value": "test_logger_provider"}), + ): + assert get_logger_provider() == "test_logger_provider" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 27f3a334c7..a1ef3b76a2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -26,6 +26,7 @@ from pkg_resources import iter_entry_points from typing_extensions import Literal +from opentelemetry._logs import set_logger_provider from opentelemetry.environment_variables import ( OTEL_LOGS_EXPORTER, OTEL_METRICS_EXPORTER, @@ -33,11 +34,7 @@ OTEL_TRACES_EXPORTER, ) from opentelemetry.metrics import set_meter_provider -from opentelemetry.sdk._logs import ( - LoggerProvider, - LoggingHandler, - set_logger_provider, -) +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, LogExporter from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index 1ca3aa48b0..83cef93149 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -17,15 +17,19 @@ import concurrent.futures import json import logging -import os import threading import traceback from time import time_ns -from typing import Any, Callable, Optional, Tuple, Union, cast - -from opentelemetry.sdk._logs.severity import SeverityNumber, std_to_otlp -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_LOGGER_PROVIDER, +from typing import Any, Callable, Optional, Tuple, Union + +from opentelemetry._logs import Logger as APILogger +from opentelemetry._logs import LoggerProvider as APILoggerProvider +from opentelemetry._logs import LogRecord as APILogRecord +from opentelemetry._logs import ( + SeverityNumber, + get_logger, + get_logger_provider, + std_to_otel, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util import ns_to_iso_str @@ -37,13 +41,12 @@ get_current_span, ) from opentelemetry.trace.span import TraceFlags -from opentelemetry.util._providers import _load_provider from opentelemetry.util.types import Attributes _logger = logging.getLogger(__name__) -class LogRecord: +class LogRecord(APILogRecord): """A LogRecord instance represents an event being logged. LogRecord instances are created and emitted via `Logger` @@ -54,6 +57,7 @@ class LogRecord: def __init__( self, timestamp: Optional[int] = None, + observed_timestamp: Optional[int] = None, trace_id: Optional[int] = None, span_id: Optional[int] = None, trace_flags: Optional[TraceFlags] = None, @@ -63,15 +67,20 @@ def __init__( resource: Optional[Resource] = None, attributes: Optional[Attributes] = None, ): - self.timestamp = timestamp - self.trace_id = trace_id - self.span_id = span_id - self.trace_flags = trace_flags - self.severity_text = severity_text - self.severity_number = severity_number - self.body = body + super().__init__( + **{ + "timestamp": timestamp, + "observed_timestamp": observed_timestamp, + "trace_id": trace_id, + "span_id": span_id, + "trace_flags": trace_flags, + "severity_text": severity_text, + "severity_number": severity_number, + "body": body, + "attributes": attributes, + } + ) self.resource = resource - self.attributes = attributes def __eq__(self, other: object) -> bool: if not isinstance(other, LogRecord): @@ -351,7 +360,7 @@ def _translate(self, record: logging.LogRecord) -> LogRecord: timestamp = int(record.created * 1e9) span_context = get_current_span().get_span_context() attributes = self._get_attributes(record) - severity_number = std_to_otlp(record.levelno) + severity_number = std_to_otel(record.levelno) return LogRecord( timestamp=timestamp, trace_id=span_context.trace_id, @@ -368,7 +377,7 @@ def emit(self, record: logging.LogRecord) -> None: """ Emit a record. - The record is translated to OTLP format, and then sent across the pipeline. + The record is translated to OTel format, and then sent across the pipeline. """ self._logger.emit(self._translate(record)) @@ -379,7 +388,7 @@ def flush(self) -> None: self._logger_provider.force_flush() -class Logger: +class Logger(APILogger): def __init__( self, resource: Resource, @@ -389,6 +398,11 @@ def __init__( ], instrumentation_scope: InstrumentationScope, ): + super().__init__( + instrumentation_scope.name, + instrumentation_scope.version, + instrumentation_scope.schema_url, + ) self._resource = resource self._multi_log_record_processor = multi_log_record_processor self._instrumentation_scope = instrumentation_scope @@ -405,7 +419,7 @@ def emit(self, record: LogRecord): self._multi_log_record_processor.emit(log_data) -class LoggerProvider: +class LoggerProvider(APILoggerProvider): def __init__( self, resource: Resource = Resource.create(), @@ -429,14 +443,17 @@ def resource(self): def get_logger( self, - instrumenting_module_name: str, - instrumenting_module_version: str = "", + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, ) -> Logger: return Logger( self._resource, self._multi_log_record_processor, InstrumentationScope( - instrumenting_module_name, instrumenting_module_version + name, + version, + schema_url, ), ) @@ -470,56 +487,3 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: False otherwise. """ return self._multi_log_record_processor.force_flush(timeout_millis) - - -_LOGGER_PROVIDER = None - - -def get_logger_provider() -> LoggerProvider: - """Gets the current global :class:`~.LoggerProvider` object.""" - global _LOGGER_PROVIDER # pylint: disable=global-statement - if _LOGGER_PROVIDER is None: - if _OTEL_PYTHON_LOGGER_PROVIDER not in os.environ: - _LOGGER_PROVIDER = LoggerProvider() - return _LOGGER_PROVIDER - - _LOGGER_PROVIDER = cast( - "LoggerProvider", - _load_provider(_OTEL_PYTHON_LOGGER_PROVIDER, "logger_provider"), - ) - - return _LOGGER_PROVIDER - - -def set_logger_provider(logger_provider: LoggerProvider) -> None: - """Sets the current global :class:`~.LoggerProvider` object. - - This can only be done once, a warning will be logged if any further attempt - is made. - """ - global _LOGGER_PROVIDER # pylint: disable=global-statement - - if _LOGGER_PROVIDER is not None: - _logger.warning("Overriding of current LoggerProvider is not allowed") - return - - _LOGGER_PROVIDER = logger_provider - - -def get_logger( - instrumenting_module_name: str, - instrumenting_library_version: str = "", - logger_provider: Optional[LoggerProvider] = None, -) -> Logger: - """Returns a `Logger` for use within a python process. - - This function is a convenience wrapper for - opentelemetry.sdk._logs.LoggerProvider.get_logger. - - If logger_provider param is omitted the current configured one is used. - """ - if logger_provider is None: - logger_provider = get_logger_provider() - return logger_provider.get_logger( - instrumenting_module_name, instrumenting_library_version - ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 17a669e01f..562863156f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -501,14 +501,6 @@ If both are set, :envvar:`OTEL_SERVICE_NAME` takes precedence. """ -_OTEL_PYTHON_LOGGER_PROVIDER = "OTEL_PYTHON_LOGGER_PROVIDER" -""" -.. envvar:: OTEL_PYTHON_LOGGER_PROVIDER - -The :envvar:`OTEL_PYTHON_LOGGER_PROVIDER` environment variable allows users to -provide the entry point for loading the log emitter provider. If not specified, SDK -LoggerProvider is used. -""" _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED = ( "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED" diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index e65fc04a42..bcca6fc740 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -21,6 +21,7 @@ from concurrent.futures import ThreadPoolExecutor from unittest.mock import Mock, patch +from opentelemetry._logs import SeverityNumber from opentelemetry.sdk import trace from opentelemetry.sdk._logs import ( LogData, @@ -36,7 +37,6 @@ from opentelemetry.sdk._logs.export.in_memory_log_exporter import ( InMemoryLogExporter, ) -from opentelemetry.sdk._logs.severity import SeverityNumber from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.concurrency_test import ConcurrencyTestBase diff --git a/opentelemetry-sdk/tests/logs/test_global_provider.py b/opentelemetry-sdk/tests/logs/test_global_provider.py deleted file mode 100644 index d888789898..0000000000 --- a/opentelemetry-sdk/tests/logs/test_global_provider.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# type:ignore -import unittest -from importlib import reload -from logging import WARNING -from unittest.mock import patch - -from opentelemetry.sdk import _logs -from opentelemetry.sdk._logs import ( - LoggerProvider, - get_logger_provider, - set_logger_provider, -) -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_LOGGER_PROVIDER, -) - - -class TestGlobals(unittest.TestCase): - def tearDown(self): - reload(_logs) - - def check_override_not_allowed(self): - """set_logger_provider should throw a warning when overridden""" - provider = get_logger_provider() - with self.assertLogs(level=WARNING) as test: - set_logger_provider(LoggerProvider()) - self.assertEqual( - test.output, - [ - ( - "WARNING:opentelemetry.sdk._logs:Overriding of current " - "LoggerProvider is not allowed" - ) - ], - ) - self.assertIs(provider, get_logger_provider()) - - def test_set_tracer_provider(self): - reload(_logs) - provider = LoggerProvider() - set_logger_provider(provider) - retrieved_provider = get_logger_provider() - self.assertEqual(provider, retrieved_provider) - - def test_tracer_provider_override_warning(self): - reload(_logs) - self.check_override_not_allowed() - - @patch.dict( - "os.environ", - {_OTEL_PYTHON_LOGGER_PROVIDER: "sdk_logger_provider"}, - ) - def test_sdk_logger_provider(self): - reload(_logs) - self.check_override_not_allowed() - - @patch.dict("os.environ", {_OTEL_PYTHON_LOGGER_PROVIDER: "unknown"}) - def test_unknown_logger_provider(self): - reload(_logs) - with self.assertRaises(Exception): - get_logger_provider() diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 771179c3d2..b9c40608e1 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -15,10 +15,10 @@ import unittest from unittest.mock import Mock +from opentelemetry._logs import SeverityNumber +from opentelemetry._logs import get_logger as APIGetLogger from opentelemetry.sdk import trace from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler -from opentelemetry.sdk._logs import get_logger as sdk_get_logger -from opentelemetry.sdk._logs.severity import SeverityNumber from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import INVALID_SPAN_CONTEXT @@ -33,7 +33,7 @@ def get_logger(level=logging.NOTSET, logger_provider=None): class TestLoggingHandler(unittest.TestCase): def test_handler_default_log_level(self): emitter_provider_mock = Mock(spec=LoggerProvider) - emitter_mock = sdk_get_logger( + emitter_mock = APIGetLogger( __name__, logger_provider=emitter_provider_mock ) logger = get_logger(logger_provider=emitter_provider_mock) @@ -46,7 +46,7 @@ def test_handler_default_log_level(self): def test_handler_custom_log_level(self): emitter_provider_mock = Mock(spec=LoggerProvider) - emitter_mock = sdk_get_logger( + emitter_mock = APIGetLogger( __name__, logger_provider=emitter_provider_mock ) logger = get_logger( @@ -61,7 +61,7 @@ def test_handler_custom_log_level(self): def test_log_record_no_span_context(self): emitter_provider_mock = Mock(spec=LoggerProvider) - emitter_mock = sdk_get_logger( + emitter_mock = APIGetLogger( __name__, logger_provider=emitter_provider_mock ) logger = get_logger(logger_provider=emitter_provider_mock) @@ -80,7 +80,7 @@ def test_log_record_no_span_context(self): def test_log_record_user_attributes(self): """Attributes can be injected into logs by adding them to the LogRecord""" emitter_provider_mock = Mock(spec=LoggerProvider) - emitter_mock = sdk_get_logger( + emitter_mock = APIGetLogger( __name__, logger_provider=emitter_provider_mock ) logger = get_logger(logger_provider=emitter_provider_mock) @@ -95,7 +95,7 @@ def test_log_record_user_attributes(self): def test_log_record_exception(self): """Exception information will be included in attributes""" emitter_provider_mock = Mock(spec=LoggerProvider) - emitter_mock = sdk_get_logger( + emitter_mock = APIGetLogger( __name__, logger_provider=emitter_provider_mock ) logger = get_logger(logger_provider=emitter_provider_mock) @@ -128,7 +128,7 @@ def test_log_record_exception(self): def test_log_exc_info_false(self): """Exception information will be included in attributes""" emitter_provider_mock = Mock(spec=LoggerProvider) - emitter_mock = sdk_get_logger( + emitter_mock = APIGetLogger( __name__, logger_provider=emitter_provider_mock ) logger = get_logger(logger_provider=emitter_provider_mock) @@ -151,7 +151,7 @@ def test_log_exc_info_false(self): def test_log_record_trace_correlation(self): emitter_provider_mock = Mock(spec=LoggerProvider) - emitter_mock = sdk_get_logger( + emitter_mock = APIGetLogger( __name__, logger_provider=emitter_provider_mock ) logger = get_logger(logger_provider=emitter_provider_mock) diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py new file mode 100644 index 0000000000..80a7d66f1c --- /dev/null +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access + +import unittest + +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk.resources import Resource + + +class TestLoggerProvider(unittest.TestCase): + def test_resource(self): + """ + `LoggerProvider` provides a way to allow a `Resource` to be specified. + """ + + logger_provider_0 = LoggerProvider() + logger_provider_1 = LoggerProvider() + + self.assertIs( + logger_provider_0.resource, + logger_provider_1.resource, + ) + self.assertIsInstance(logger_provider_0.resource, Resource) + self.assertIsInstance(logger_provider_1.resource, Resource) + + resource = Resource({"key": "value"}) + self.assertIs(LoggerProvider(resource=resource).resource, resource) + + def test_get_logger(self): + """ + `LoggerProvider.get_logger` arguments are used to create an + `InstrumentationScope` object on the created `Logger`. + """ + + logger = LoggerProvider().get_logger( + "name", + version="version", + schema_url="schema_url", + ) + + self.assertEqual(logger._instrumentation_scope.name, "name") + self.assertEqual(logger._instrumentation_scope.version, "version") + self.assertEqual( + logger._instrumentation_scope.schema_url, "schema_url" + ) diff --git a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py index 004bc296be..e270812eb7 100644 --- a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py +++ b/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py @@ -21,6 +21,7 @@ from abc import ABC, abstractmethod from unittest.mock import Mock +from opentelemetry._logs import SeverityNumber from opentelemetry.sdk._logs import ( ConcurrentMultiLogRecordProcessor, LoggerProvider, @@ -29,7 +30,6 @@ LogRecordProcessor, SynchronousMultiLogRecordProcessor, ) -from opentelemetry.sdk._logs.severity import SeverityNumber class AnotherLogRecordProcessor(LogRecordProcessor): diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py index 19ee5f85ea..23b3112430 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py @@ -15,6 +15,7 @@ import unittest from opentelemetry import trace as trace_api +from opentelemetry._logs import _internal as logging_api from opentelemetry.metrics import _internal as metrics_api from opentelemetry.metrics._internal import _ProxyMeterProvider from opentelemetry.util._once import Once @@ -36,6 +37,14 @@ def reset_metrics_globals() -> None: metrics_api._PROXY_METER_PROVIDER = _ProxyMeterProvider() # type: ignore[attr-defined] +# pylint: disable=protected-access +def reset_logging_globals() -> None: + """WARNING: only use this for tests.""" + logging_api._LOGGER_PROVIDER_SET_ONCE = Once() # type: ignore[attr-defined] + logging_api._LOGGER_PROVIDER = None # type: ignore[attr-defined] + # logging_api._PROXY_LOGGER_PROVIDER = _ProxyLoggerProvider() # type: ignore[attr-defined] + + class TraceGlobalsTest(unittest.TestCase): """Resets trace API globals in setUp/tearDown From 638988c56574c19ff4a0ea5a357ac971cfe94cb2 Mon Sep 17 00:00:00 2001 From: Shalev Roda <65566801+shalevr@users.noreply.github.com> Date: Wed, 30 Nov 2022 03:57:58 +0200 Subject: [PATCH 1351/1517] Add url decode for OTEL_RESOUCE_ATTRIBUTES (#3046) --- CHANGELOG.md | 2 ++ .../opentelemetry/sdk/resources/__init__.py | 4 +++- .../tests/resources/test_resources.py | 20 +++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00748e5ac2..58323ea50d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Add url decode values from OTEL_RESOURCE_ATTRIBUTES + ([#3046](https://github.com/open-telemetry/opentelemetry-python/pull/3046)) - Fixed circular dependency issue with custom samplers ([#3026](https://github.com/open-telemetry/opentelemetry-python/pull/3026)) - Add missing entry points for OTLP/HTTP exporter diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 3088dbc5bb..56707415e5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -62,6 +62,7 @@ import sys import typing from json import dumps +from urllib import parse import pkg_resources @@ -289,7 +290,8 @@ def detect(self) -> "Resource": exc, ) continue - env_resource_map[key.strip()] = value.strip() + value_url_decoded = parse.unquote(value.strip()) + env_resource_map[key.strip()] = value_url_decoded service_name = os.environ.get(OTEL_SERVICE_NAME) if service_name: diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 2d584981e6..c9649f685a 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -19,6 +19,7 @@ import uuid from logging import ERROR from unittest import mock +from urllib import parse from opentelemetry.sdk import resources @@ -492,6 +493,25 @@ def test_invalid_key_value_pairs(self): resources.Resource({"k": "v", "k2": "v2", "foo": "bar=baz"}), ) + def test_multiple_with_url_decode(self): + detector = resources.OTELResourceDetector() + os.environ[ + resources.OTEL_RESOURCE_ATTRIBUTES + ] = "key=value%20test%0A, key2=value+%202" + self.assertEqual( + detector.detect(), + resources.Resource({"key": "value test\n", "key2": "value+ 2"}), + ) + self.assertEqual( + detector.detect(), + resources.Resource( + { + "key": parse.unquote("value%20test%0A"), + "key2": parse.unquote("value+%202"), + } + ), + ) + @mock.patch.dict( os.environ, {resources.OTEL_SERVICE_NAME: "test-srv-name"}, From 212d50a4b9c5fa3fafd39e3b8446a82b45ccb06d Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 1 Dec 2022 00:09:50 +0100 Subject: [PATCH 1352/1517] Add a more informative error message when parsing ENV headers (#3044) * Add a more informative error message when parsing ENV headers Also, rename the function to make it clear that this parsing is specific to headers provided via ENV variables. Fixes #2376 * Use parse_env_headers in metrics and logs as well * Fix lint * Fix mypy --- CHANGELOG.md | 2 ++ .../exporter/otlp/proto/grpc/exporter.py | 4 +-- .../otlp/proto/http/_log_exporter/__init__.py | 4 +-- .../proto/http/metric_exporter/__init__.py | 4 +-- .../proto/http/trace_exporter/__init__.py | 4 +-- .../metrics/test_otlp_metrics_exporter.py | 6 +++- .../tests/test_proto_span_exporter.py | 6 +++- .../src/opentelemetry/util/re.py | 34 ++++++++++++++----- opentelemetry-api/tests/util/test_re.py | 12 ++++--- 9 files changed, 53 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58323ea50d..2da9ec1e98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Rename parse_headers to parse_env_headers and improve error message + ([#2376](https://github.com/open-telemetry/opentelemetry-python/pull/2376)) - Add url decode values from OTEL_RESOURCE_ATTRIBUTES ([#3046](https://github.com/open-telemetry/opentelemetry-python/pull/3046)) - Fixed circular dependency issue with custom samplers diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index aef66b79de..d1427e41fa 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -52,8 +52,8 @@ OTEL_EXPORTER_OTLP_TIMEOUT, ) from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.util.re import parse_headers from opentelemetry.sdk.metrics.export import MetricsData +from opentelemetry.util.re import parse_env_headers logger = getLogger(__name__) SDKDataT = TypeVar("SDKDataT") @@ -247,7 +247,7 @@ def __init__( self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS) if isinstance(self._headers, str): - temp_headers = parse_headers(self._headers) + temp_headers = parse_env_headers(self._headers) self._headers = tuple(temp_headers.items()) elif isinstance(self._headers, dict): self._headers = tuple(self._headers.items()) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 580e084f4f..5da0443828 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -42,7 +42,7 @@ from opentelemetry.exporter.otlp.proto.http._log_exporter.encoder import ( _ProtobufEncoder, ) -from opentelemetry.util.re import parse_headers +from opentelemetry.util.re import parse_env_headers _logger = logging.getLogger(__name__) @@ -86,7 +86,7 @@ def __init__( OTEL_EXPORTER_OTLP_CERTIFICATE, True ) headers_string = environ.get(OTEL_EXPORTER_OTLP_HEADERS, "") - self._headers = headers or parse_headers(headers_string) + self._headers = headers or parse_env_headers(headers_string) self._timeout = timeout or int( environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT) ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index c83ca00e06..c04f5bbbce 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -64,7 +64,7 @@ Sum, ) from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.util.re import parse_headers +from opentelemetry.util.re import parse_env_headers import backoff import requests @@ -119,7 +119,7 @@ def __init__( OTEL_EXPORTER_OTLP_METRICS_HEADERS, environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), ) - self._headers = headers or parse_headers(headers_string) + self._headers = headers or parse_env_headers(headers_string) self._timeout = timeout or int( environ.get( OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 9dc3b65014..623d7dbf0a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -43,7 +43,7 @@ from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import ( _ProtobufEncoder, ) -from opentelemetry.util.re import parse_headers +from opentelemetry.util.re import parse_env_headers _logger = logging.getLogger(__name__) @@ -94,7 +94,7 @@ def __init__( OTEL_EXPORTER_OTLP_TRACES_HEADERS, environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), ) - self._headers = headers or parse_headers(headers_string) + self._headers = headers or parse_env_headers(headers_string) self._timeout = timeout or int( environ.get( OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index 74ba53b3c5..99f9ea12d3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -220,7 +220,11 @@ def test_headers_parse_from_env(self): self.assertEqual( cm.records[0].message, - "Header doesn't match the format: missingValue.", + ( + "Header format invalid! Header values in environment " + "variables must be URL encoded per the OpenTelemetry " + "Protocol Exporter specification: missingValue" + ), ) @patch.object(requests.Session, "post") diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index c7ff9a15dd..73d08e08c8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -189,7 +189,11 @@ def test_headers_parse_from_env(self): self.assertEqual( cm.records[0].message, - "Header doesn't match the format: missingValue.", + ( + "Header format invalid! Header values in environment " + "variables must be URL encoded per the OpenTelemetry " + "Protocol Exporter specification: missingValue" + ), ) # pylint: disable=no-self-use diff --git a/opentelemetry-api/src/opentelemetry/util/re.py b/opentelemetry-api/src/opentelemetry/util/re.py index 3dcbfe08b7..5f19521d04 100644 --- a/opentelemetry-api/src/opentelemetry/util/re.py +++ b/opentelemetry-api/src/opentelemetry/util/re.py @@ -12,23 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging +from logging import getLogger from re import compile, split from typing import Dict, List, Mapping from urllib.parse import unquote -_logger = logging.getLogger(__name__) +from deprecated import deprecated +_logger = getLogger(__name__) -# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables +# The following regexes reference this spec: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables + +# Optional whitespace _OWS = r"[ \t]*" -# A key contains one or more US-ASCII character except CTLs or separators. +# A key contains printable US-ASCII characters except: SP and "(),/:;<=>?@[\]{} _KEY_FORMAT = ( r"[\x21\x23-\x27\x2a\x2b\x2d\x2e\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+" ) -# A value contains a URL encoded UTF-8 string. +# A value contains a URL-encoded UTF-8 string. The encoded form can contain any +# printable US-ASCII characters (0x20-0x7f) other than SP, DEL, and ",;/ _VALUE_FORMAT = r"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*" +# A key-value is key=value, with optional whitespace surrounding key and value _KEY_VALUE_FORMAT = rf"{_OWS}{_KEY_FORMAT}{_OWS}={_OWS}{_VALUE_FORMAT}{_OWS}" + _HEADER_PATTERN = compile(_KEY_VALUE_FORMAT) _DELIMITER_PATTERN = compile(r"[ \t]*,[ \t]*") @@ -36,10 +42,18 @@ # pylint: disable=invalid-name + + +@deprecated(version="1.15.0", reason="You should use parse_env_headers") # type: ignore def parse_headers(s: str) -> Mapping[str, str]: + return parse_env_headers(s) + + +def parse_env_headers(s: str) -> Mapping[str, str]: """ - Parse ``s`` (a ``str`` instance containing HTTP headers). Uses W3C Baggage - HTTP header format https://www.w3.org/TR/baggage/#baggage-http-header-format, except that + Parse ``s``, which is a ``str`` instance containing HTTP headers encoded + for use in ENV variables per the W3C Baggage HTTP header format at + https://www.w3.org/TR/baggage/#baggage-http-header-format, except that additional semi-colon delimited metadata is not supported. """ headers: Dict[str, str] = {} @@ -49,7 +63,11 @@ def parse_headers(s: str) -> Mapping[str, str]: continue match = _HEADER_PATTERN.fullmatch(header.strip()) if not match: - _logger.warning("Header doesn't match the format: %s.", header) + _logger.warning( + "Header format invalid! Header values in environment variables must be " + "URL encoded per the OpenTelemetry Protocol Exporter specification: %s", + header, + ) continue # value may contain any number of `=` name, value = match.string.split("=", 1) diff --git a/opentelemetry-api/tests/util/test_re.py b/opentelemetry-api/tests/util/test_re.py index e7834ac15a..99d79ba9d2 100644 --- a/opentelemetry-api/tests/util/test_re.py +++ b/opentelemetry-api/tests/util/test_re.py @@ -16,11 +16,11 @@ import unittest -from opentelemetry.util.re import parse_headers +from opentelemetry.util.re import parse_env_headers class TestParseHeaders(unittest.TestCase): - def test_parse_headers(self): + def test_parse_env_headers(self): inp = [ # invalid header name ("=value", [], True), @@ -63,10 +63,12 @@ def test_parse_headers(self): s, expected, warn = case if warn: with self.assertLogs(level="WARNING") as cm: - self.assertEqual(parse_headers(s), dict(expected)) + self.assertEqual(parse_env_headers(s), dict(expected)) self.assertTrue( - "Header doesn't match the format:" + "Header format invalid! Header values in environment " + "variables must be URL encoded per the OpenTelemetry " + "Protocol Exporter specification:" in cm.records[0].message, ) else: - self.assertEqual(parse_headers(s), dict(expected)) + self.assertEqual(parse_env_headers(s), dict(expected)) From 90a0b68cf60aadfaf9881f1b17024f6341cd43ac Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 1 Dec 2022 12:11:27 +0100 Subject: [PATCH 1353/1517] Exclude .git from codespell checking (#3063) --- .codespellrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codespellrc b/.codespellrc index ab0fb6bb92..f5236d7c1b 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,4 +1,4 @@ [codespell] # skipping auto generated folders -skip = ./.tox,./.mypy_cache,./docs/_build,./target,*/LICENSE,./venv +skip = ./.tox,./.mypy_cache,./docs/_build,./target,*/LICENSE,./venv,.git ignore-words-list = ans,ue,ot,hist,ro From 4f83b73c02af0c32785f019dde397cd3d89d0ded Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 1 Dec 2022 20:29:48 +0100 Subject: [PATCH 1354/1517] Skip test case when it runs under Windows and PyPy (#3065) --- opentelemetry-sdk/tests/trace/export/test_export.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index de9b1564ef..422945798f 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -19,10 +19,10 @@ import unittest from concurrent.futures import ThreadPoolExecutor from logging import WARNING -from platform import python_implementation +from platform import python_implementation, system from unittest import mock -from flaky import flaky +from pytest import mark from opentelemetry import trace as trace_api from opentelemetry.context import Context @@ -442,6 +442,10 @@ def test_batch_span_processor_scheduled_delay(self): span_processor.shutdown() + @mark.skipif( + python_implementation() == "PyPy" and system() == "Windows", + reason="This test randomly fails in Windows with PyPy", + ) def test_batch_span_processor_reset_timeout(self): """Test that the scheduled timeout is reset on cycles without spans""" spans_names_list = [] @@ -480,11 +484,6 @@ def test_batch_span_processor_reset_timeout(self): span_processor.shutdown() - if python_implementation() == "PyPy": - test_batch_span_processor_reset_timeout = flaky( - max_runs=2, min_passes=1 - )(test_batch_span_processor_reset_timeout) - def test_batch_span_processor_parameters(self): # zero max_queue_size self.assertRaises( From edd6796e35af17221726127bdfef4619df2b53cc Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 2 Dec 2022 16:24:47 -0800 Subject: [PATCH 1355/1517] Move logs to `_internal` folder (#3069) --- .../otlp/proto/http/_log_exporter/__init__.py | 2 +- .../http/_log_exporter/encoder/__init__.py | 2 +- .../src/opentelemetry/sdk/_logs/__init__.py | 487 +---------------- .../sdk/_logs/_internal/__init__.py | 489 ++++++++++++++++++ .../sdk/_logs/_internal/export/__init__.py | 330 ++++++++++++ .../export/in_memory_log_exporter.py | 0 .../sdk/_logs/export/__init__.py | 336 +----------- opentelemetry-sdk/tests/logs/test_export.py | 4 +- ...rcessor.py => test_multi_log_processor.py} | 2 +- 9 files changed, 857 insertions(+), 795 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py rename opentelemetry-sdk/src/opentelemetry/sdk/_logs/{ => _internal}/export/in_memory_log_exporter.py (100%) rename opentelemetry-sdk/tests/logs/{test_multi_log_prcessor.py => test_multi_log_processor.py} (99%) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 5da0443828..49daa29e8a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -30,10 +30,10 @@ OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, ) +from opentelemetry.sdk._logs import LogData from opentelemetry.sdk._logs.export import ( LogExporter, LogExportResult, - LogData, ) from opentelemetry.exporter.otlp.proto.http import ( _OTLP_HTTP_HEADERS, diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py index c8f2dd8456..866f8878bb 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py @@ -32,7 +32,7 @@ ) -from opentelemetry.sdk._logs.export import LogData +from opentelemetry.sdk._logs import LogData class _ProtobufEncoder: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index 83cef93149..a548ef3793 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -12,478 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import abc -import atexit -import concurrent.futures -import json -import logging -import threading -import traceback -from time import time_ns -from typing import Any, Callable, Optional, Tuple, Union - -from opentelemetry._logs import Logger as APILogger -from opentelemetry._logs import LoggerProvider as APILoggerProvider -from opentelemetry._logs import LogRecord as APILogRecord -from opentelemetry._logs import ( - SeverityNumber, - get_logger, - get_logger_provider, - std_to_otel, -) -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util import ns_to_iso_str -from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import ( - format_span_id, - format_trace_id, - get_current_span, -) -from opentelemetry.trace.span import TraceFlags -from opentelemetry.util.types import Attributes - -_logger = logging.getLogger(__name__) - - -class LogRecord(APILogRecord): - """A LogRecord instance represents an event being logged. - - LogRecord instances are created and emitted via `Logger` - every time something is logged. They contain all the information - pertinent to the event being logged. - """ - - def __init__( - self, - timestamp: Optional[int] = None, - observed_timestamp: Optional[int] = None, - trace_id: Optional[int] = None, - span_id: Optional[int] = None, - trace_flags: Optional[TraceFlags] = None, - severity_text: Optional[str] = None, - severity_number: Optional[SeverityNumber] = None, - body: Optional[Any] = None, - resource: Optional[Resource] = None, - attributes: Optional[Attributes] = None, - ): - super().__init__( - **{ - "timestamp": timestamp, - "observed_timestamp": observed_timestamp, - "trace_id": trace_id, - "span_id": span_id, - "trace_flags": trace_flags, - "severity_text": severity_text, - "severity_number": severity_number, - "body": body, - "attributes": attributes, - } - ) - self.resource = resource - - def __eq__(self, other: object) -> bool: - if not isinstance(other, LogRecord): - return NotImplemented - return self.__dict__ == other.__dict__ - - def to_json(self, indent=4) -> str: - return json.dumps( - { - "body": self.body, - "severity_number": repr(self.severity_number), - "severity_text": self.severity_text, - "attributes": self.attributes, - "timestamp": ns_to_iso_str(self.timestamp), - "trace_id": f"0x{format_trace_id(self.trace_id)}" - if self.trace_id is not None - else "", - "span_id": f"0x{format_span_id(self.span_id)}" - if self.span_id is not None - else "", - "trace_flags": self.trace_flags, - "resource": repr(self.resource.attributes) - if self.resource - else "", - }, - indent=indent, - ) - - -class LogData: - """Readable LogRecord data plus associated InstrumentationLibrary.""" - - def __init__( - self, - log_record: LogRecord, - instrumentation_scope: InstrumentationScope, - ): - self.log_record = log_record - self.instrumentation_scope = instrumentation_scope - - -class LogRecordProcessor(abc.ABC): - """Interface to hook the log record emitting action. - - Log processors can be registered directly using - :func:`LoggerProvider.add_log_record_processor` and they are invoked - in the same order as they were registered. - """ - - @abc.abstractmethod - def emit(self, log_data: LogData): - """Emits the `LogData`""" - - @abc.abstractmethod - def shutdown(self): - """Called when a :class:`opentelemetry.sdk._logs.Logger` is shutdown""" - - @abc.abstractmethod - def force_flush(self, timeout_millis: int = 30000): - """Export all the received logs to the configured Exporter that have not yet - been exported. - - Args: - timeout_millis: The maximum amount of time to wait for logs to be - exported. - - Returns: - False if the timeout is exceeded, True otherwise. - """ - - -# Temporary fix until https://github.com/PyCQA/pylint/issues/4098 is resolved -# pylint:disable=no-member -class SynchronousMultiLogRecordProcessor(LogRecordProcessor): - """Implementation of class:`LogRecordProcessor` that forwards all received - events to a list of log processors sequentially. - - The underlying log processors are called in sequential order as they were - added. - """ - - def __init__(self): - # use a tuple to avoid race conditions when adding a new log and - # iterating through it on "emit". - self._log_record_processors = () # type: Tuple[LogRecordProcessor, ...] - self._lock = threading.Lock() - - def add_log_record_processor( - self, log_record_processor: LogRecordProcessor - ) -> None: - """Adds a Logprocessor to the list of log processors handled by this instance""" - with self._lock: - self._log_record_processors += (log_record_processor,) - - def emit(self, log_data: LogData) -> None: - for lp in self._log_record_processors: - lp.emit(log_data) - - def shutdown(self) -> None: - """Shutdown the log processors one by one""" - for lp in self._log_record_processors: - lp.shutdown() - - def force_flush(self, timeout_millis: int = 30000) -> bool: - """Force flush the log processors one by one - - Args: - timeout_millis: The maximum amount of time to wait for logs to be - exported. If the first n log processors exceeded the timeout - then remaining log processors will not be flushed. - - Returns: - True if all the log processors flushes the logs within timeout, - False otherwise. - """ - deadline_ns = time_ns() + timeout_millis * 1000000 - for lp in self._log_record_processors: - current_ts = time_ns() - if current_ts >= deadline_ns: - return False - - if not lp.force_flush((deadline_ns - current_ts) // 1000000): - return False - - return True - - -class ConcurrentMultiLogRecordProcessor(LogRecordProcessor): - """Implementation of :class:`LogRecordProcessor` that forwards all received - events to a list of log processors in parallel. - - Calls to the underlying log processors are forwarded in parallel by - submitting them to a thread pool executor and waiting until each log - processor finished its work. - - Args: - max_workers: The number of threads managed by the thread pool executor - and thus defining how many log processors can work in parallel. - """ - - def __init__(self, max_workers: int = 2): - # use a tuple to avoid race conditions when adding a new log and - # iterating through it on "emit". - self._log_record_processors = () # type: Tuple[LogRecordProcessor, ...] - self._lock = threading.Lock() - self._executor = concurrent.futures.ThreadPoolExecutor( - max_workers=max_workers - ) - - def add_log_record_processor( - self, log_record_processor: LogRecordProcessor - ): - with self._lock: - self._log_record_processors += (log_record_processor,) - - def _submit_and_wait( - self, - func: Callable[[LogRecordProcessor], Callable[..., None]], - *args: Any, - **kwargs: Any, - ): - futures = [] - for lp in self._log_record_processors: - future = self._executor.submit(func(lp), *args, **kwargs) - futures.append(future) - for future in futures: - future.result() - - def emit(self, log_data: LogData): - self._submit_and_wait(lambda lp: lp.emit, log_data) - - def shutdown(self): - self._submit_and_wait(lambda lp: lp.shutdown) - - def force_flush(self, timeout_millis: int = 30000) -> bool: - """Force flush the log processors in parallel. - - Args: - timeout_millis: The maximum amount of time to wait for logs to be - exported. - - Returns: - True if all the log processors flushes the logs within timeout, - False otherwise. - """ - futures = [] - for lp in self._log_record_processors: - future = self._executor.submit(lp.force_flush, timeout_millis) - futures.append(future) - - done_futures, not_done_futures = concurrent.futures.wait( - futures, timeout_millis / 1e3 - ) - - if not_done_futures: - return False - - for future in done_futures: - if not future.result(): - return False - - return True - - -# skip natural LogRecord attributes -# http://docs.python.org/library/logging.html#logrecord-attributes -_RESERVED_ATTRS = frozenset( - ( - "asctime", - "args", - "created", - "exc_info", - "exc_text", - "filename", - "funcName", - "getMessage", - "levelname", - "levelno", - "lineno", - "module", - "msecs", - "msg", - "name", - "pathname", - "process", - "processName", - "relativeCreated", - "stack_info", - "thread", - "threadName", - ) +# pylint: disable=unused-import + +from opentelemetry.sdk._logs._internal import ( # noqa: F401 + LogData, + Logger, + LoggerProvider, + LoggingHandler, + LogRecord, + LogRecordProcessor, ) - -class LoggingHandler(logging.Handler): - """A handler class which writes logging records, in OTLP format, to - a network destination or file. Supports signals from the `logging` module. - https://docs.python.org/3/library/logging.html - """ - - def __init__( - self, - level=logging.NOTSET, - logger_provider=None, - ) -> None: - super().__init__(level=level) - self._logger_provider = logger_provider or get_logger_provider() - self._logger = get_logger( - __name__, logger_provider=self._logger_provider - ) - - @staticmethod - def _get_attributes(record: logging.LogRecord) -> Attributes: - attributes = { - k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS - } - if record.exc_info: - exc_type = "" - message = "" - stack_trace = "" - exctype, value, tb = record.exc_info - if exctype is not None: - exc_type = exctype.__name__ - if value is not None and value.args: - message = value.args[0] - if tb is not None: - # https://github.com/open-telemetry/opentelemetry-specification/blob/9fa7c656b26647b27e485a6af7e38dc716eba98a/specification/trace/semantic_conventions/exceptions.md#stacktrace-representation - stack_trace = "".join( - traceback.format_exception(*record.exc_info) - ) - attributes[SpanAttributes.EXCEPTION_TYPE] = exc_type - attributes[SpanAttributes.EXCEPTION_MESSAGE] = message - attributes[SpanAttributes.EXCEPTION_STACKTRACE] = stack_trace - return attributes - - def _translate(self, record: logging.LogRecord) -> LogRecord: - timestamp = int(record.created * 1e9) - span_context = get_current_span().get_span_context() - attributes = self._get_attributes(record) - severity_number = std_to_otel(record.levelno) - return LogRecord( - timestamp=timestamp, - trace_id=span_context.trace_id, - span_id=span_context.span_id, - trace_flags=span_context.trace_flags, - severity_text=record.levelname, - severity_number=severity_number, - body=record.getMessage(), - resource=self._logger.resource, - attributes=attributes, - ) - - def emit(self, record: logging.LogRecord) -> None: - """ - Emit a record. - - The record is translated to OTel format, and then sent across the pipeline. - """ - self._logger.emit(self._translate(record)) - - def flush(self) -> None: - """ - Flushes the logging output. - """ - self._logger_provider.force_flush() - - -class Logger(APILogger): - def __init__( - self, - resource: Resource, - multi_log_record_processor: Union[ - SynchronousMultiLogRecordProcessor, - ConcurrentMultiLogRecordProcessor, - ], - instrumentation_scope: InstrumentationScope, - ): - super().__init__( - instrumentation_scope.name, - instrumentation_scope.version, - instrumentation_scope.schema_url, - ) - self._resource = resource - self._multi_log_record_processor = multi_log_record_processor - self._instrumentation_scope = instrumentation_scope - - @property - def resource(self): - return self._resource - - def emit(self, record: LogRecord): - """Emits the :class:`LogData` by associating :class:`LogRecord` - and instrumentation info. - """ - log_data = LogData(record, self._instrumentation_scope) - self._multi_log_record_processor.emit(log_data) - - -class LoggerProvider(APILoggerProvider): - def __init__( - self, - resource: Resource = Resource.create(), - shutdown_on_exit: bool = True, - multi_log_record_processor: Union[ - SynchronousMultiLogRecordProcessor, - ConcurrentMultiLogRecordProcessor, - ] = None, - ): - self._resource = resource - self._multi_log_record_processor = ( - multi_log_record_processor or SynchronousMultiLogRecordProcessor() - ) - self._at_exit_handler = None - if shutdown_on_exit: - self._at_exit_handler = atexit.register(self.shutdown) - - @property - def resource(self): - return self._resource - - def get_logger( - self, - name: str, - version: Optional[str] = None, - schema_url: Optional[str] = None, - ) -> Logger: - return Logger( - self._resource, - self._multi_log_record_processor, - InstrumentationScope( - name, - version, - schema_url, - ), - ) - - def add_log_record_processor( - self, log_record_processor: LogRecordProcessor - ): - """Registers a new :class:`LogRecordProcessor` for this `LoggerProvider` instance. - - The log processors are invoked in the same order they are registered. - """ - self._multi_log_record_processor.add_log_record_processor( - log_record_processor - ) - - def shutdown(self): - """Shuts down the log processors.""" - self._multi_log_record_processor.shutdown() - if self._at_exit_handler is not None: - atexit.unregister(self._at_exit_handler) - self._at_exit_handler = None - - def force_flush(self, timeout_millis: int = 30000) -> bool: - """Force flush the log processors. - - Args: - timeout_millis: The maximum amount of time to wait for logs to be - exported. - - Returns: - True if all the log processors flushes the logs within timeout, - False otherwise. - """ - return self._multi_log_record_processor.force_flush(timeout_millis) +__all__ = [] +for key, value in globals().copy().items(): + if not key.startswith("_"): + value.__module__ = __name__ + __all__.append(key) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py new file mode 100644 index 0000000000..83cef93149 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -0,0 +1,489 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import atexit +import concurrent.futures +import json +import logging +import threading +import traceback +from time import time_ns +from typing import Any, Callable, Optional, Tuple, Union + +from opentelemetry._logs import Logger as APILogger +from opentelemetry._logs import LoggerProvider as APILoggerProvider +from opentelemetry._logs import LogRecord as APILogRecord +from opentelemetry._logs import ( + SeverityNumber, + get_logger, + get_logger_provider, + std_to_otel, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util import ns_to_iso_str +from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import ( + format_span_id, + format_trace_id, + get_current_span, +) +from opentelemetry.trace.span import TraceFlags +from opentelemetry.util.types import Attributes + +_logger = logging.getLogger(__name__) + + +class LogRecord(APILogRecord): + """A LogRecord instance represents an event being logged. + + LogRecord instances are created and emitted via `Logger` + every time something is logged. They contain all the information + pertinent to the event being logged. + """ + + def __init__( + self, + timestamp: Optional[int] = None, + observed_timestamp: Optional[int] = None, + trace_id: Optional[int] = None, + span_id: Optional[int] = None, + trace_flags: Optional[TraceFlags] = None, + severity_text: Optional[str] = None, + severity_number: Optional[SeverityNumber] = None, + body: Optional[Any] = None, + resource: Optional[Resource] = None, + attributes: Optional[Attributes] = None, + ): + super().__init__( + **{ + "timestamp": timestamp, + "observed_timestamp": observed_timestamp, + "trace_id": trace_id, + "span_id": span_id, + "trace_flags": trace_flags, + "severity_text": severity_text, + "severity_number": severity_number, + "body": body, + "attributes": attributes, + } + ) + self.resource = resource + + def __eq__(self, other: object) -> bool: + if not isinstance(other, LogRecord): + return NotImplemented + return self.__dict__ == other.__dict__ + + def to_json(self, indent=4) -> str: + return json.dumps( + { + "body": self.body, + "severity_number": repr(self.severity_number), + "severity_text": self.severity_text, + "attributes": self.attributes, + "timestamp": ns_to_iso_str(self.timestamp), + "trace_id": f"0x{format_trace_id(self.trace_id)}" + if self.trace_id is not None + else "", + "span_id": f"0x{format_span_id(self.span_id)}" + if self.span_id is not None + else "", + "trace_flags": self.trace_flags, + "resource": repr(self.resource.attributes) + if self.resource + else "", + }, + indent=indent, + ) + + +class LogData: + """Readable LogRecord data plus associated InstrumentationLibrary.""" + + def __init__( + self, + log_record: LogRecord, + instrumentation_scope: InstrumentationScope, + ): + self.log_record = log_record + self.instrumentation_scope = instrumentation_scope + + +class LogRecordProcessor(abc.ABC): + """Interface to hook the log record emitting action. + + Log processors can be registered directly using + :func:`LoggerProvider.add_log_record_processor` and they are invoked + in the same order as they were registered. + """ + + @abc.abstractmethod + def emit(self, log_data: LogData): + """Emits the `LogData`""" + + @abc.abstractmethod + def shutdown(self): + """Called when a :class:`opentelemetry.sdk._logs.Logger` is shutdown""" + + @abc.abstractmethod + def force_flush(self, timeout_millis: int = 30000): + """Export all the received logs to the configured Exporter that have not yet + been exported. + + Args: + timeout_millis: The maximum amount of time to wait for logs to be + exported. + + Returns: + False if the timeout is exceeded, True otherwise. + """ + + +# Temporary fix until https://github.com/PyCQA/pylint/issues/4098 is resolved +# pylint:disable=no-member +class SynchronousMultiLogRecordProcessor(LogRecordProcessor): + """Implementation of class:`LogRecordProcessor` that forwards all received + events to a list of log processors sequentially. + + The underlying log processors are called in sequential order as they were + added. + """ + + def __init__(self): + # use a tuple to avoid race conditions when adding a new log and + # iterating through it on "emit". + self._log_record_processors = () # type: Tuple[LogRecordProcessor, ...] + self._lock = threading.Lock() + + def add_log_record_processor( + self, log_record_processor: LogRecordProcessor + ) -> None: + """Adds a Logprocessor to the list of log processors handled by this instance""" + with self._lock: + self._log_record_processors += (log_record_processor,) + + def emit(self, log_data: LogData) -> None: + for lp in self._log_record_processors: + lp.emit(log_data) + + def shutdown(self) -> None: + """Shutdown the log processors one by one""" + for lp in self._log_record_processors: + lp.shutdown() + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Force flush the log processors one by one + + Args: + timeout_millis: The maximum amount of time to wait for logs to be + exported. If the first n log processors exceeded the timeout + then remaining log processors will not be flushed. + + Returns: + True if all the log processors flushes the logs within timeout, + False otherwise. + """ + deadline_ns = time_ns() + timeout_millis * 1000000 + for lp in self._log_record_processors: + current_ts = time_ns() + if current_ts >= deadline_ns: + return False + + if not lp.force_flush((deadline_ns - current_ts) // 1000000): + return False + + return True + + +class ConcurrentMultiLogRecordProcessor(LogRecordProcessor): + """Implementation of :class:`LogRecordProcessor` that forwards all received + events to a list of log processors in parallel. + + Calls to the underlying log processors are forwarded in parallel by + submitting them to a thread pool executor and waiting until each log + processor finished its work. + + Args: + max_workers: The number of threads managed by the thread pool executor + and thus defining how many log processors can work in parallel. + """ + + def __init__(self, max_workers: int = 2): + # use a tuple to avoid race conditions when adding a new log and + # iterating through it on "emit". + self._log_record_processors = () # type: Tuple[LogRecordProcessor, ...] + self._lock = threading.Lock() + self._executor = concurrent.futures.ThreadPoolExecutor( + max_workers=max_workers + ) + + def add_log_record_processor( + self, log_record_processor: LogRecordProcessor + ): + with self._lock: + self._log_record_processors += (log_record_processor,) + + def _submit_and_wait( + self, + func: Callable[[LogRecordProcessor], Callable[..., None]], + *args: Any, + **kwargs: Any, + ): + futures = [] + for lp in self._log_record_processors: + future = self._executor.submit(func(lp), *args, **kwargs) + futures.append(future) + for future in futures: + future.result() + + def emit(self, log_data: LogData): + self._submit_and_wait(lambda lp: lp.emit, log_data) + + def shutdown(self): + self._submit_and_wait(lambda lp: lp.shutdown) + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Force flush the log processors in parallel. + + Args: + timeout_millis: The maximum amount of time to wait for logs to be + exported. + + Returns: + True if all the log processors flushes the logs within timeout, + False otherwise. + """ + futures = [] + for lp in self._log_record_processors: + future = self._executor.submit(lp.force_flush, timeout_millis) + futures.append(future) + + done_futures, not_done_futures = concurrent.futures.wait( + futures, timeout_millis / 1e3 + ) + + if not_done_futures: + return False + + for future in done_futures: + if not future.result(): + return False + + return True + + +# skip natural LogRecord attributes +# http://docs.python.org/library/logging.html#logrecord-attributes +_RESERVED_ATTRS = frozenset( + ( + "asctime", + "args", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "getMessage", + "levelname", + "levelno", + "lineno", + "module", + "msecs", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack_info", + "thread", + "threadName", + ) +) + + +class LoggingHandler(logging.Handler): + """A handler class which writes logging records, in OTLP format, to + a network destination or file. Supports signals from the `logging` module. + https://docs.python.org/3/library/logging.html + """ + + def __init__( + self, + level=logging.NOTSET, + logger_provider=None, + ) -> None: + super().__init__(level=level) + self._logger_provider = logger_provider or get_logger_provider() + self._logger = get_logger( + __name__, logger_provider=self._logger_provider + ) + + @staticmethod + def _get_attributes(record: logging.LogRecord) -> Attributes: + attributes = { + k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS + } + if record.exc_info: + exc_type = "" + message = "" + stack_trace = "" + exctype, value, tb = record.exc_info + if exctype is not None: + exc_type = exctype.__name__ + if value is not None and value.args: + message = value.args[0] + if tb is not None: + # https://github.com/open-telemetry/opentelemetry-specification/blob/9fa7c656b26647b27e485a6af7e38dc716eba98a/specification/trace/semantic_conventions/exceptions.md#stacktrace-representation + stack_trace = "".join( + traceback.format_exception(*record.exc_info) + ) + attributes[SpanAttributes.EXCEPTION_TYPE] = exc_type + attributes[SpanAttributes.EXCEPTION_MESSAGE] = message + attributes[SpanAttributes.EXCEPTION_STACKTRACE] = stack_trace + return attributes + + def _translate(self, record: logging.LogRecord) -> LogRecord: + timestamp = int(record.created * 1e9) + span_context = get_current_span().get_span_context() + attributes = self._get_attributes(record) + severity_number = std_to_otel(record.levelno) + return LogRecord( + timestamp=timestamp, + trace_id=span_context.trace_id, + span_id=span_context.span_id, + trace_flags=span_context.trace_flags, + severity_text=record.levelname, + severity_number=severity_number, + body=record.getMessage(), + resource=self._logger.resource, + attributes=attributes, + ) + + def emit(self, record: logging.LogRecord) -> None: + """ + Emit a record. + + The record is translated to OTel format, and then sent across the pipeline. + """ + self._logger.emit(self._translate(record)) + + def flush(self) -> None: + """ + Flushes the logging output. + """ + self._logger_provider.force_flush() + + +class Logger(APILogger): + def __init__( + self, + resource: Resource, + multi_log_record_processor: Union[ + SynchronousMultiLogRecordProcessor, + ConcurrentMultiLogRecordProcessor, + ], + instrumentation_scope: InstrumentationScope, + ): + super().__init__( + instrumentation_scope.name, + instrumentation_scope.version, + instrumentation_scope.schema_url, + ) + self._resource = resource + self._multi_log_record_processor = multi_log_record_processor + self._instrumentation_scope = instrumentation_scope + + @property + def resource(self): + return self._resource + + def emit(self, record: LogRecord): + """Emits the :class:`LogData` by associating :class:`LogRecord` + and instrumentation info. + """ + log_data = LogData(record, self._instrumentation_scope) + self._multi_log_record_processor.emit(log_data) + + +class LoggerProvider(APILoggerProvider): + def __init__( + self, + resource: Resource = Resource.create(), + shutdown_on_exit: bool = True, + multi_log_record_processor: Union[ + SynchronousMultiLogRecordProcessor, + ConcurrentMultiLogRecordProcessor, + ] = None, + ): + self._resource = resource + self._multi_log_record_processor = ( + multi_log_record_processor or SynchronousMultiLogRecordProcessor() + ) + self._at_exit_handler = None + if shutdown_on_exit: + self._at_exit_handler = atexit.register(self.shutdown) + + @property + def resource(self): + return self._resource + + def get_logger( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> Logger: + return Logger( + self._resource, + self._multi_log_record_processor, + InstrumentationScope( + name, + version, + schema_url, + ), + ) + + def add_log_record_processor( + self, log_record_processor: LogRecordProcessor + ): + """Registers a new :class:`LogRecordProcessor` for this `LoggerProvider` instance. + + The log processors are invoked in the same order they are registered. + """ + self._multi_log_record_processor.add_log_record_processor( + log_record_processor + ) + + def shutdown(self): + """Shuts down the log processors.""" + self._multi_log_record_processor.shutdown() + if self._at_exit_handler is not None: + atexit.unregister(self._at_exit_handler) + self._at_exit_handler = None + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Force flush the log processors. + + Args: + timeout_millis: The maximum amount of time to wait for logs to be + exported. + + Returns: + True if all the log processors flushes the logs within timeout, + False otherwise. + """ + return self._multi_log_record_processor.force_flush(timeout_millis) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py new file mode 100644 index 0000000000..3f19b79e10 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -0,0 +1,330 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import collections +import enum +import logging +import os +import sys +import threading +from os import linesep +from time import time_ns +from typing import IO, Callable, Deque, List, Optional, Sequence + +from opentelemetry.context import attach, detach, set_value +from opentelemetry.sdk._logs import LogData, LogRecord, LogRecordProcessor +from opentelemetry.util._once import Once + +_logger = logging.getLogger(__name__) + + +class LogExportResult(enum.Enum): + SUCCESS = 0 + FAILURE = 1 + + +class LogExporter(abc.ABC): + """Interface for exporting logs. + + Interface to be implemented by services that want to export logs received + in their own format. + + To export data this MUST be registered to the :class`opentelemetry.sdk._logs.Logger` using a + log processor. + """ + + @abc.abstractmethod + def export(self, batch: Sequence[LogData]): + """Exports a batch of logs. + + Args: + batch: The list of `LogData` objects to be exported + + Returns: + The result of the export + """ + + @abc.abstractmethod + def shutdown(self): + """Shuts down the exporter. + + Called when the SDK is shut down. + """ + + +class ConsoleLogExporter(LogExporter): + """Implementation of :class:`LogExporter` that prints log records to the + console. + + This class can be used for diagnostic purposes. It prints the exported + log records to the console STDOUT. + """ + + def __init__( + self, + out: IO = sys.stdout, + formatter: Callable[[LogRecord], str] = lambda record: record.to_json() + + linesep, + ): + self.out = out + self.formatter = formatter + + def export(self, batch: Sequence[LogData]): + for data in batch: + self.out.write(self.formatter(data.log_record)) + self.out.flush() + return LogExportResult.SUCCESS + + def shutdown(self): + pass + + +class SimpleLogRecordProcessor(LogRecordProcessor): + """This is an implementation of LogRecordProcessor which passes + received logs in the export-friendly LogData representation to the + configured LogExporter, as soon as they are emitted. + """ + + def __init__(self, exporter: LogExporter): + self._exporter = exporter + self._shutdown = False + + def emit(self, log_data: LogData): + if self._shutdown: + _logger.warning("Processor is already shutdown, ignoring call") + return + token = attach(set_value("suppress_instrumentation", True)) + try: + self._exporter.export((log_data,)) + except Exception: # pylint: disable=broad-except + _logger.exception("Exception while exporting logs.") + detach(token) + + def shutdown(self): + self._shutdown = True + self._exporter.shutdown() + + def force_flush( + self, timeout_millis: int = 30000 + ) -> bool: # pylint: disable=no-self-use + return True + + +class _FlushRequest: + __slots__ = ["event", "num_log_records"] + + def __init__(self): + self.event = threading.Event() + self.num_log_records = 0 + + +_BSP_RESET_ONCE = Once() + + +class BatchLogRecordProcessor(LogRecordProcessor): + """This is an implementation of LogRecordProcessor which creates batches of + received logs in the export-friendly LogData representation and + send to the configured LogExporter, as soon as they are emitted. + """ + + def __init__( + self, + exporter: LogExporter, + schedule_delay_millis: int = 5000, + max_export_batch_size: int = 512, + export_timeout_millis: int = 30000, + ): + self._exporter = exporter + self._schedule_delay_millis = schedule_delay_millis + self._max_export_batch_size = max_export_batch_size + self._export_timeout_millis = export_timeout_millis + self._queue = collections.deque() # type: Deque[LogData] + self._worker_thread = threading.Thread( + name="OtelBatchLogRecordProcessor", + target=self.worker, + daemon=True, + ) + self._condition = threading.Condition(threading.Lock()) + self._shutdown = False + self._flush_request = None # type: Optional[_FlushRequest] + self._log_records = [ + None + ] * self._max_export_batch_size # type: List[Optional[LogData]] + self._worker_thread.start() + # Only available in *nix since py37. + if hasattr(os, "register_at_fork"): + os.register_at_fork( + after_in_child=self._at_fork_reinit + ) # pylint: disable=protected-access + self._pid = os.getpid() + + def _at_fork_reinit(self): + self._condition = threading.Condition(threading.Lock()) + self._queue.clear() + self._worker_thread = threading.Thread( + name="OtelBatchLogRecordProcessor", + target=self.worker, + daemon=True, + ) + self._worker_thread.start() + self._pid = os.getpid() + + def worker(self): + timeout = self._schedule_delay_millis / 1e3 + flush_request = None # type: Optional[_FlushRequest] + while not self._shutdown: + with self._condition: + if self._shutdown: + # shutdown may have been called, avoid further processing + break + flush_request = self._get_and_unset_flush_request() + if ( + len(self._queue) < self._max_export_batch_size + and flush_request is None + ): + self._condition.wait(timeout) + + flush_request = self._get_and_unset_flush_request() + if not self._queue: + timeout = self._schedule_delay_millis / 1e3 + self._notify_flush_request_finished(flush_request) + flush_request = None + continue + if self._shutdown: + break + + start_ns = time_ns() + self._export(flush_request) + end_ns = time_ns() + # subtract the duration of this export call to the next timeout + timeout = self._schedule_delay_millis / 1e3 - ( + (end_ns - start_ns) / 1e9 + ) + + self._notify_flush_request_finished(flush_request) + flush_request = None + + # there might have been a new flush request while export was running + # and before the done flag switched to true + with self._condition: + shutdown_flush_request = self._get_and_unset_flush_request() + + # flush the remaining logs + self._drain_queue() + self._notify_flush_request_finished(flush_request) + self._notify_flush_request_finished(shutdown_flush_request) + + def _export(self, flush_request: Optional[_FlushRequest] = None): + """Exports logs considering the given flush_request. + + If flush_request is not None then logs are exported in batches + until the number of exported logs reached or exceeded the num of logs in + flush_request, otherwise exports at max max_export_batch_size logs. + """ + if flush_request is None: + self._export_batch() + return + + num_log_records = flush_request.num_log_records + while self._queue: + exported = self._export_batch() + num_log_records -= exported + + if num_log_records <= 0: + break + + def _export_batch(self) -> int: + """Exports at most max_export_batch_size logs and returns the number of + exported logs. + """ + idx = 0 + while idx < self._max_export_batch_size and self._queue: + record = self._queue.pop() + self._log_records[idx] = record + idx += 1 + token = attach(set_value("suppress_instrumentation", True)) + try: + self._exporter.export(self._log_records[:idx]) # type: ignore + except Exception: # pylint: disable=broad-except + _logger.exception("Exception while exporting logs.") + detach(token) + + for index in range(idx): + self._log_records[index] = None + return idx + + def _drain_queue(self): + """Export all elements until queue is empty. + + Can only be called from the worker thread context because it invokes + `export` that is not thread safe. + """ + while self._queue: + self._export_batch() + + def _get_and_unset_flush_request(self) -> Optional[_FlushRequest]: + flush_request = self._flush_request + self._flush_request = None + if flush_request is not None: + flush_request.num_log_records = len(self._queue) + return flush_request + + @staticmethod + def _notify_flush_request_finished( + flush_request: Optional[_FlushRequest] = None, + ): + if flush_request is not None: + flush_request.event.set() + + def _get_or_create_flush_request(self) -> _FlushRequest: + if self._flush_request is None: + self._flush_request = _FlushRequest() + return self._flush_request + + def emit(self, log_data: LogData) -> None: + """Adds the `LogData` to queue and notifies the waiting threads + when size of queue reaches max_export_batch_size. + """ + if self._shutdown: + return + if self._pid != os.getpid(): + _BSP_RESET_ONCE.do_once(self._at_fork_reinit) + + self._queue.appendleft(log_data) + if len(self._queue) >= self._max_export_batch_size: + with self._condition: + self._condition.notify() + + def shutdown(self): + self._shutdown = True + with self._condition: + self._condition.notify_all() + self._worker_thread.join() + self._exporter.shutdown() + + def force_flush(self, timeout_millis: Optional[int] = None) -> bool: + if timeout_millis is None: + timeout_millis = self._export_timeout_millis + if self._shutdown: + return True + + with self._condition: + flush_request = self._get_or_create_flush_request() + self._condition.notify_all() + + ret = flush_request.event.wait(timeout_millis / 1e3) + if not ret: + _logger.warning("Timeout was exceeded in force_flush().") + return ret diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/in_memory_log_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/in_memory_log_exporter.py similarity index 100% rename from opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/in_memory_log_exporter.py rename to opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/in_memory_log_exporter.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py index 3f19b79e10..1040288667 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py @@ -12,319 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -import abc -import collections -import enum -import logging -import os -import sys -import threading -from os import linesep -from time import time_ns -from typing import IO, Callable, Deque, List, Optional, Sequence - -from opentelemetry.context import attach, detach, set_value -from opentelemetry.sdk._logs import LogData, LogRecord, LogRecordProcessor -from opentelemetry.util._once import Once - -_logger = logging.getLogger(__name__) - - -class LogExportResult(enum.Enum): - SUCCESS = 0 - FAILURE = 1 - - -class LogExporter(abc.ABC): - """Interface for exporting logs. - - Interface to be implemented by services that want to export logs received - in their own format. - - To export data this MUST be registered to the :class`opentelemetry.sdk._logs.Logger` using a - log processor. - """ - - @abc.abstractmethod - def export(self, batch: Sequence[LogData]): - """Exports a batch of logs. - - Args: - batch: The list of `LogData` objects to be exported - - Returns: - The result of the export - """ - - @abc.abstractmethod - def shutdown(self): - """Shuts down the exporter. - - Called when the SDK is shut down. - """ - - -class ConsoleLogExporter(LogExporter): - """Implementation of :class:`LogExporter` that prints log records to the - console. - - This class can be used for diagnostic purposes. It prints the exported - log records to the console STDOUT. - """ - - def __init__( - self, - out: IO = sys.stdout, - formatter: Callable[[LogRecord], str] = lambda record: record.to_json() - + linesep, - ): - self.out = out - self.formatter = formatter - - def export(self, batch: Sequence[LogData]): - for data in batch: - self.out.write(self.formatter(data.log_record)) - self.out.flush() - return LogExportResult.SUCCESS - - def shutdown(self): - pass - - -class SimpleLogRecordProcessor(LogRecordProcessor): - """This is an implementation of LogRecordProcessor which passes - received logs in the export-friendly LogData representation to the - configured LogExporter, as soon as they are emitted. - """ - - def __init__(self, exporter: LogExporter): - self._exporter = exporter - self._shutdown = False - - def emit(self, log_data: LogData): - if self._shutdown: - _logger.warning("Processor is already shutdown, ignoring call") - return - token = attach(set_value("suppress_instrumentation", True)) - try: - self._exporter.export((log_data,)) - except Exception: # pylint: disable=broad-except - _logger.exception("Exception while exporting logs.") - detach(token) - - def shutdown(self): - self._shutdown = True - self._exporter.shutdown() - - def force_flush( - self, timeout_millis: int = 30000 - ) -> bool: # pylint: disable=no-self-use - return True - - -class _FlushRequest: - __slots__ = ["event", "num_log_records"] - - def __init__(self): - self.event = threading.Event() - self.num_log_records = 0 - - -_BSP_RESET_ONCE = Once() - - -class BatchLogRecordProcessor(LogRecordProcessor): - """This is an implementation of LogRecordProcessor which creates batches of - received logs in the export-friendly LogData representation and - send to the configured LogExporter, as soon as they are emitted. - """ - - def __init__( - self, - exporter: LogExporter, - schedule_delay_millis: int = 5000, - max_export_batch_size: int = 512, - export_timeout_millis: int = 30000, - ): - self._exporter = exporter - self._schedule_delay_millis = schedule_delay_millis - self._max_export_batch_size = max_export_batch_size - self._export_timeout_millis = export_timeout_millis - self._queue = collections.deque() # type: Deque[LogData] - self._worker_thread = threading.Thread( - name="OtelBatchLogRecordProcessor", - target=self.worker, - daemon=True, - ) - self._condition = threading.Condition(threading.Lock()) - self._shutdown = False - self._flush_request = None # type: Optional[_FlushRequest] - self._log_records = [ - None - ] * self._max_export_batch_size # type: List[Optional[LogData]] - self._worker_thread.start() - # Only available in *nix since py37. - if hasattr(os, "register_at_fork"): - os.register_at_fork( - after_in_child=self._at_fork_reinit - ) # pylint: disable=protected-access - self._pid = os.getpid() - - def _at_fork_reinit(self): - self._condition = threading.Condition(threading.Lock()) - self._queue.clear() - self._worker_thread = threading.Thread( - name="OtelBatchLogRecordProcessor", - target=self.worker, - daemon=True, - ) - self._worker_thread.start() - self._pid = os.getpid() - - def worker(self): - timeout = self._schedule_delay_millis / 1e3 - flush_request = None # type: Optional[_FlushRequest] - while not self._shutdown: - with self._condition: - if self._shutdown: - # shutdown may have been called, avoid further processing - break - flush_request = self._get_and_unset_flush_request() - if ( - len(self._queue) < self._max_export_batch_size - and flush_request is None - ): - self._condition.wait(timeout) - - flush_request = self._get_and_unset_flush_request() - if not self._queue: - timeout = self._schedule_delay_millis / 1e3 - self._notify_flush_request_finished(flush_request) - flush_request = None - continue - if self._shutdown: - break - - start_ns = time_ns() - self._export(flush_request) - end_ns = time_ns() - # subtract the duration of this export call to the next timeout - timeout = self._schedule_delay_millis / 1e3 - ( - (end_ns - start_ns) / 1e9 - ) - - self._notify_flush_request_finished(flush_request) - flush_request = None - - # there might have been a new flush request while export was running - # and before the done flag switched to true - with self._condition: - shutdown_flush_request = self._get_and_unset_flush_request() - - # flush the remaining logs - self._drain_queue() - self._notify_flush_request_finished(flush_request) - self._notify_flush_request_finished(shutdown_flush_request) - - def _export(self, flush_request: Optional[_FlushRequest] = None): - """Exports logs considering the given flush_request. - - If flush_request is not None then logs are exported in batches - until the number of exported logs reached or exceeded the num of logs in - flush_request, otherwise exports at max max_export_batch_size logs. - """ - if flush_request is None: - self._export_batch() - return - - num_log_records = flush_request.num_log_records - while self._queue: - exported = self._export_batch() - num_log_records -= exported - - if num_log_records <= 0: - break - - def _export_batch(self) -> int: - """Exports at most max_export_batch_size logs and returns the number of - exported logs. - """ - idx = 0 - while idx < self._max_export_batch_size and self._queue: - record = self._queue.pop() - self._log_records[idx] = record - idx += 1 - token = attach(set_value("suppress_instrumentation", True)) - try: - self._exporter.export(self._log_records[:idx]) # type: ignore - except Exception: # pylint: disable=broad-except - _logger.exception("Exception while exporting logs.") - detach(token) - - for index in range(idx): - self._log_records[index] = None - return idx - - def _drain_queue(self): - """Export all elements until queue is empty. - - Can only be called from the worker thread context because it invokes - `export` that is not thread safe. - """ - while self._queue: - self._export_batch() - - def _get_and_unset_flush_request(self) -> Optional[_FlushRequest]: - flush_request = self._flush_request - self._flush_request = None - if flush_request is not None: - flush_request.num_log_records = len(self._queue) - return flush_request - - @staticmethod - def _notify_flush_request_finished( - flush_request: Optional[_FlushRequest] = None, - ): - if flush_request is not None: - flush_request.event.set() - - def _get_or_create_flush_request(self) -> _FlushRequest: - if self._flush_request is None: - self._flush_request = _FlushRequest() - return self._flush_request - - def emit(self, log_data: LogData) -> None: - """Adds the `LogData` to queue and notifies the waiting threads - when size of queue reaches max_export_batch_size. - """ - if self._shutdown: - return - if self._pid != os.getpid(): - _BSP_RESET_ONCE.do_once(self._at_fork_reinit) - - self._queue.appendleft(log_data) - if len(self._queue) >= self._max_export_batch_size: - with self._condition: - self._condition.notify() - - def shutdown(self): - self._shutdown = True - with self._condition: - self._condition.notify_all() - self._worker_thread.join() - self._exporter.shutdown() - - def force_flush(self, timeout_millis: Optional[int] = None) -> bool: - if timeout_millis is None: - timeout_millis = self._export_timeout_millis - if self._shutdown: - return True - - with self._condition: - flush_request = self._get_or_create_flush_request() - self._condition.notify_all() - - ret = flush_request.event.wait(timeout_millis / 1e3) - if not ret: - _logger.warning("Timeout was exceeded in force_flush().") - return ret +# pylint: disable=unused-import + +from opentelemetry.sdk._logs._internal.export import ( # noqa: F401 + BatchLogRecordProcessor, + ConsoleLogExporter, + LogExporter, + LogExportResult, + SimpleLogRecordProcessor, +) + +# The point module is not in the export directory to avoid a circular import. +from opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import ( # noqa: F401 + InMemoryLogExporter, +) + +__all__ = [] +for key, value in globals().copy().items(): + if not key.startswith("_"): + value.__module__ = __name__ + __all__.append(key) diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index bcca6fc740..6a6b1d5bf8 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -32,10 +32,8 @@ from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, ConsoleLogExporter, - SimpleLogRecordProcessor, -) -from opentelemetry.sdk._logs.export.in_memory_log_exporter import ( InMemoryLogExporter, + SimpleLogRecordProcessor, ) from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationScope diff --git a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py b/opentelemetry-sdk/tests/logs/test_multi_log_processor.py similarity index 99% rename from opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py rename to opentelemetry-sdk/tests/logs/test_multi_log_processor.py index e270812eb7..0d5ac8b115 100644 --- a/opentelemetry-sdk/tests/logs/test_multi_log_prcessor.py +++ b/opentelemetry-sdk/tests/logs/test_multi_log_processor.py @@ -22,7 +22,7 @@ from unittest.mock import Mock from opentelemetry._logs import SeverityNumber -from opentelemetry.sdk._logs import ( +from opentelemetry.sdk._logs._internal import ( ConcurrentMultiLogRecordProcessor, LoggerProvider, LoggingHandler, From e4a4410dc046f011194eff78e801fb230961eec8 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Sat, 3 Dec 2022 06:31:55 -0500 Subject: [PATCH 1356/1517] Regenerate opentelemetry-proto to be compatible with protobuf 3 and 4 (#3070) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 + dev-requirements.txt | 7 +- docs-requirements.txt | 4 + opentelemetry-proto/pyproject.toml | 2 +- .../collector/logs/v1/logs_service_pb2.py | 114 +- .../metrics/v1/metrics_service_pb2.py | 114 +- .../collector/trace/v1/trace_service_pb2.py | 114 +- .../proto/common/v1/common_pb2.py | 331 +---- .../opentelemetry/proto/logs/v1/logs_pb2.py | 501 +------ .../metrics_config_service_pb2.py | 276 ---- .../metrics_config_service_pb2.pyi | 136 -- .../metrics_config_service_pb2_grpc.py | 84 -- .../proto/metrics/v1/metrics_pb2.py | 1225 +---------------- .../proto/resource/v1/resource_pb2.py | 61 +- .../proto/trace/v1/trace_config_pb2.py | 264 +--- .../opentelemetry/proto/trace/v1/trace_pb2.py | 615 +-------- scripts/proto_codegen.sh | 3 + tox.ini | 33 +- 19 files changed, 278 insertions(+), 3610 deletions(-) delete mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.py delete mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.pyi delete mode 100644 opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2_grpc.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c452961526..cda3a2c57d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -87,7 +87,7 @@ jobs: strategy: fail-fast: false matrix: - tox-environment: [ "docker-tests", "lint", "spellcheck", "docs", "mypy", "mypyinstalled", "tracecontext" ] + tox-environment: [ "docker-tests-proto3", "docker-tests-proto4", "lint", "spellcheck", "docs", "mypy", "mypyinstalled", "tracecontext" ] name: ${{ matrix.tox-environment }} runs-on: ubuntu-20.04 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2da9ec1e98..83fd52921a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Regenerate opentelemetry-proto to be compatible with protobuf 3 and 4 + ([#3070](https://github.com/open-telemetry/opentelemetry-python/pull/3070)) - Rename parse_headers to parse_env_headers and improve error message ([#2376](https://github.com/open-telemetry/opentelemetry-python/pull/2376)) - Add url decode values from OTEL_RESOURCE_ATTRIBUTES diff --git a/dev-requirements.txt b/dev-requirements.txt index e3fced6b83..cb17b50040 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,9 +10,12 @@ sphinx-autodoc-typehints~=1.12.0 pytest==7.1.3 pytest-cov>=2.8 readme-renderer~=24.0 -grpcio-tools~=1.41.0 +# This version of grpcio-tools ships with protoc 3.19.4 which appears to be compatible with +# both protobuf 3.19.x and 4.x (see https://github.com/protocolbuffers/protobuf/issues/11123). +# Bump this version with caution to preserve compatibility with protobuf 3. +# https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-proto/pyproject.toml#L28 +grpcio-tools==1.48.1 mypy-protobuf~=3.0.0 -protobuf~=3.18.1 # temporary fix. we should update the jinja, flask deps # See https://github.com/pallets/markupsafe/issues/282 # breaking change introduced in markupsafe causes jinja, flask to break diff --git a/docs-requirements.txt b/docs-requirements.txt index c70cefc8aa..c167ab785d 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -23,3 +23,7 @@ wrapt>=1.0.0,<2.0.0 # See https://github.com/pallets/markupsafe/issues/282 # breaking change introduced in markupsafe causes jinja, flask to break markupsafe==2.0.1 + +# Jaeger generated protobufs do not currently support protobuf 4.x. This can be removed once +# they're regenerated. +protobuf~=3.19 diff --git a/opentelemetry-proto/pyproject.toml b/opentelemetry-proto/pyproject.toml index 3b7875dcd5..b44b61b50e 100644 --- a/opentelemetry-proto/pyproject.toml +++ b/opentelemetry-proto/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ - "protobuf~=3.13", + "protobuf>=3.19, < 5.0", ] [project.optional-dependencies] diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py index d439d077b3..aef1671830 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py @@ -3,6 +3,7 @@ # source: opentelemetry/proto/collector/logs/v1/logs_service.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 @@ -14,80 +15,12 @@ from opentelemetry.proto.logs.v1 import logs_pb2 as opentelemetry_dot_proto_dot_logs_dot_v1_dot_logs__pb2 -DESCRIPTOR = _descriptor.FileDescriptor( - name='opentelemetry/proto/collector/logs/v1/logs_service.proto', - package='opentelemetry.proto.collector.logs.v1', - syntax='proto3', - serialized_options=b'\n(io.opentelemetry.proto.collector.logs.v1B\020LogsServiceProtoP\001Z0go.opentelemetry.io/proto/otlp/collector/logs/v1', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n8opentelemetry/proto/collector/logs/v1/logs_service.proto\x12%opentelemetry.proto.collector.logs.v1\x1a&opentelemetry/proto/logs/v1/logs.proto\"\\\n\x18\x45xportLogsServiceRequest\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\x1b\n\x19\x45xportLogsServiceResponse2\x9d\x01\n\x0bLogsService\x12\x8d\x01\n\x06\x45xport\x12?.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest\x1a@.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse\"\x00\x42p\n(io.opentelemetry.proto.collector.logs.v1B\x10LogsServiceProtoP\x01Z0go.opentelemetry.io/proto/otlp/collector/logs/v1b\x06proto3' - , - dependencies=[opentelemetry_dot_proto_dot_logs_dot_v1_dot_logs__pb2.DESCRIPTOR,]) +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n8opentelemetry/proto/collector/logs/v1/logs_service.proto\x12%opentelemetry.proto.collector.logs.v1\x1a&opentelemetry/proto/logs/v1/logs.proto\"\\\n\x18\x45xportLogsServiceRequest\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\x1b\n\x19\x45xportLogsServiceResponse2\x9d\x01\n\x0bLogsService\x12\x8d\x01\n\x06\x45xport\x12?.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest\x1a@.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse\"\x00\x42p\n(io.opentelemetry.proto.collector.logs.v1B\x10LogsServiceProtoP\x01Z0go.opentelemetry.io/proto/otlp/collector/logs/v1b\x06proto3') - -_EXPORTLOGSSERVICEREQUEST = _descriptor.Descriptor( - name='ExportLogsServiceRequest', - full_name='opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='resource_logs', full_name='opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest.resource_logs', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=139, - serialized_end=231, -) - - -_EXPORTLOGSSERVICERESPONSE = _descriptor.Descriptor( - name='ExportLogsServiceResponse', - full_name='opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=233, - serialized_end=260, -) - -_EXPORTLOGSSERVICEREQUEST.fields_by_name['resource_logs'].message_type = opentelemetry_dot_proto_dot_logs_dot_v1_dot_logs__pb2._RESOURCELOGS -DESCRIPTOR.message_types_by_name['ExportLogsServiceRequest'] = _EXPORTLOGSSERVICEREQUEST -DESCRIPTOR.message_types_by_name['ExportLogsServiceResponse'] = _EXPORTLOGSSERVICERESPONSE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - +_EXPORTLOGSSERVICEREQUEST = DESCRIPTOR.message_types_by_name['ExportLogsServiceRequest'] +_EXPORTLOGSSERVICERESPONSE = DESCRIPTOR.message_types_by_name['ExportLogsServiceResponse'] ExportLogsServiceRequest = _reflection.GeneratedProtocolMessageType('ExportLogsServiceRequest', (_message.Message,), { 'DESCRIPTOR' : _EXPORTLOGSSERVICEREQUEST, '__module__' : 'opentelemetry.proto.collector.logs.v1.logs_service_pb2' @@ -102,32 +35,15 @@ }) _sym_db.RegisterMessage(ExportLogsServiceResponse) - -DESCRIPTOR._options = None - -_LOGSSERVICE = _descriptor.ServiceDescriptor( - name='LogsService', - full_name='opentelemetry.proto.collector.logs.v1.LogsService', - file=DESCRIPTOR, - index=0, - serialized_options=None, - create_key=_descriptor._internal_create_key, - serialized_start=263, - serialized_end=420, - methods=[ - _descriptor.MethodDescriptor( - name='Export', - full_name='opentelemetry.proto.collector.logs.v1.LogsService.Export', - index=0, - containing_service=None, - input_type=_EXPORTLOGSSERVICEREQUEST, - output_type=_EXPORTLOGSSERVICERESPONSE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), -]) -_sym_db.RegisterServiceDescriptor(_LOGSSERVICE) - -DESCRIPTOR.services_by_name['LogsService'] = _LOGSSERVICE - +_LOGSSERVICE = DESCRIPTOR.services_by_name['LogsService'] +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n(io.opentelemetry.proto.collector.logs.v1B\020LogsServiceProtoP\001Z0go.opentelemetry.io/proto/otlp/collector/logs/v1' + _EXPORTLOGSSERVICEREQUEST._serialized_start=139 + _EXPORTLOGSSERVICEREQUEST._serialized_end=231 + _EXPORTLOGSSERVICERESPONSE._serialized_start=233 + _EXPORTLOGSSERVICERESPONSE._serialized_end=260 + _LOGSSERVICE._serialized_start=263 + _LOGSSERVICE._serialized_end=420 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py index 2fb3bf86de..ca026163b1 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py @@ -3,6 +3,7 @@ # source: opentelemetry/proto/collector/metrics/v1/metrics_service.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 @@ -14,80 +15,12 @@ from opentelemetry.proto.metrics.v1 import metrics_pb2 as opentelemetry_dot_proto_dot_metrics_dot_v1_dot_metrics__pb2 -DESCRIPTOR = _descriptor.FileDescriptor( - name='opentelemetry/proto/collector/metrics/v1/metrics_service.proto', - package='opentelemetry.proto.collector.metrics.v1', - syntax='proto3', - serialized_options=b'\n+io.opentelemetry.proto.collector.metrics.v1B\023MetricsServiceProtoP\001Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n>opentelemetry/proto/collector/metrics/v1/metrics_service.proto\x12(opentelemetry.proto.collector.metrics.v1\x1a,opentelemetry/proto/metrics/v1/metrics.proto\"h\n\x1b\x45xportMetricsServiceRequest\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\x1e\n\x1c\x45xportMetricsServiceResponse2\xac\x01\n\x0eMetricsService\x12\x99\x01\n\x06\x45xport\x12\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest\x1a\x46.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse\"\x00\x42y\n+io.opentelemetry.proto.collector.metrics.v1B\x13MetricsServiceProtoP\x01Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1b\x06proto3' - , - dependencies=[opentelemetry_dot_proto_dot_metrics_dot_v1_dot_metrics__pb2.DESCRIPTOR,]) +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n>opentelemetry/proto/collector/metrics/v1/metrics_service.proto\x12(opentelemetry.proto.collector.metrics.v1\x1a,opentelemetry/proto/metrics/v1/metrics.proto\"h\n\x1b\x45xportMetricsServiceRequest\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\x1e\n\x1c\x45xportMetricsServiceResponse2\xac\x01\n\x0eMetricsService\x12\x99\x01\n\x06\x45xport\x12\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest\x1a\x46.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse\"\x00\x42y\n+io.opentelemetry.proto.collector.metrics.v1B\x13MetricsServiceProtoP\x01Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1b\x06proto3') - -_EXPORTMETRICSSERVICEREQUEST = _descriptor.Descriptor( - name='ExportMetricsServiceRequest', - full_name='opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='resource_metrics', full_name='opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest.resource_metrics', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=154, - serialized_end=258, -) - - -_EXPORTMETRICSSERVICERESPONSE = _descriptor.Descriptor( - name='ExportMetricsServiceResponse', - full_name='opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=260, - serialized_end=290, -) - -_EXPORTMETRICSSERVICEREQUEST.fields_by_name['resource_metrics'].message_type = opentelemetry_dot_proto_dot_metrics_dot_v1_dot_metrics__pb2._RESOURCEMETRICS -DESCRIPTOR.message_types_by_name['ExportMetricsServiceRequest'] = _EXPORTMETRICSSERVICEREQUEST -DESCRIPTOR.message_types_by_name['ExportMetricsServiceResponse'] = _EXPORTMETRICSSERVICERESPONSE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - +_EXPORTMETRICSSERVICEREQUEST = DESCRIPTOR.message_types_by_name['ExportMetricsServiceRequest'] +_EXPORTMETRICSSERVICERESPONSE = DESCRIPTOR.message_types_by_name['ExportMetricsServiceResponse'] ExportMetricsServiceRequest = _reflection.GeneratedProtocolMessageType('ExportMetricsServiceRequest', (_message.Message,), { 'DESCRIPTOR' : _EXPORTMETRICSSERVICEREQUEST, '__module__' : 'opentelemetry.proto.collector.metrics.v1.metrics_service_pb2' @@ -102,32 +35,15 @@ }) _sym_db.RegisterMessage(ExportMetricsServiceResponse) - -DESCRIPTOR._options = None - -_METRICSSERVICE = _descriptor.ServiceDescriptor( - name='MetricsService', - full_name='opentelemetry.proto.collector.metrics.v1.MetricsService', - file=DESCRIPTOR, - index=0, - serialized_options=None, - create_key=_descriptor._internal_create_key, - serialized_start=293, - serialized_end=465, - methods=[ - _descriptor.MethodDescriptor( - name='Export', - full_name='opentelemetry.proto.collector.metrics.v1.MetricsService.Export', - index=0, - containing_service=None, - input_type=_EXPORTMETRICSSERVICEREQUEST, - output_type=_EXPORTMETRICSSERVICERESPONSE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), -]) -_sym_db.RegisterServiceDescriptor(_METRICSSERVICE) - -DESCRIPTOR.services_by_name['MetricsService'] = _METRICSSERVICE - +_METRICSSERVICE = DESCRIPTOR.services_by_name['MetricsService'] +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n+io.opentelemetry.proto.collector.metrics.v1B\023MetricsServiceProtoP\001Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1' + _EXPORTMETRICSSERVICEREQUEST._serialized_start=154 + _EXPORTMETRICSSERVICEREQUEST._serialized_end=258 + _EXPORTMETRICSSERVICERESPONSE._serialized_start=260 + _EXPORTMETRICSSERVICERESPONSE._serialized_end=290 + _METRICSSERVICE._serialized_start=293 + _METRICSSERVICE._serialized_end=465 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py index 7d988f1117..d32dc61933 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py @@ -3,6 +3,7 @@ # source: opentelemetry/proto/collector/trace/v1/trace_service.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 @@ -14,80 +15,12 @@ from opentelemetry.proto.trace.v1 import trace_pb2 as opentelemetry_dot_proto_dot_trace_dot_v1_dot_trace__pb2 -DESCRIPTOR = _descriptor.FileDescriptor( - name='opentelemetry/proto/collector/trace/v1/trace_service.proto', - package='opentelemetry.proto.collector.trace.v1', - syntax='proto3', - serialized_options=b'\n)io.opentelemetry.proto.collector.trace.v1B\021TraceServiceProtoP\001Z1go.opentelemetry.io/proto/otlp/collector/trace/v1', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n:opentelemetry/proto/collector/trace/v1/trace_service.proto\x12&opentelemetry.proto.collector.trace.v1\x1a(opentelemetry/proto/trace/v1/trace.proto\"`\n\x19\x45xportTraceServiceRequest\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\x1c\n\x1a\x45xportTraceServiceResponse2\xa2\x01\n\x0cTraceService\x12\x91\x01\n\x06\x45xport\x12\x41.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\x1a\x42.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse\"\x00\x42s\n)io.opentelemetry.proto.collector.trace.v1B\x11TraceServiceProtoP\x01Z1go.opentelemetry.io/proto/otlp/collector/trace/v1b\x06proto3' - , - dependencies=[opentelemetry_dot_proto_dot_trace_dot_v1_dot_trace__pb2.DESCRIPTOR,]) +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n:opentelemetry/proto/collector/trace/v1/trace_service.proto\x12&opentelemetry.proto.collector.trace.v1\x1a(opentelemetry/proto/trace/v1/trace.proto\"`\n\x19\x45xportTraceServiceRequest\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\x1c\n\x1a\x45xportTraceServiceResponse2\xa2\x01\n\x0cTraceService\x12\x91\x01\n\x06\x45xport\x12\x41.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\x1a\x42.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse\"\x00\x42s\n)io.opentelemetry.proto.collector.trace.v1B\x11TraceServiceProtoP\x01Z1go.opentelemetry.io/proto/otlp/collector/trace/v1b\x06proto3') - -_EXPORTTRACESERVICEREQUEST = _descriptor.Descriptor( - name='ExportTraceServiceRequest', - full_name='opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='resource_spans', full_name='opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest.resource_spans', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=144, - serialized_end=240, -) - - -_EXPORTTRACESERVICERESPONSE = _descriptor.Descriptor( - name='ExportTraceServiceResponse', - full_name='opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=242, - serialized_end=270, -) - -_EXPORTTRACESERVICEREQUEST.fields_by_name['resource_spans'].message_type = opentelemetry_dot_proto_dot_trace_dot_v1_dot_trace__pb2._RESOURCESPANS -DESCRIPTOR.message_types_by_name['ExportTraceServiceRequest'] = _EXPORTTRACESERVICEREQUEST -DESCRIPTOR.message_types_by_name['ExportTraceServiceResponse'] = _EXPORTTRACESERVICERESPONSE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - +_EXPORTTRACESERVICEREQUEST = DESCRIPTOR.message_types_by_name['ExportTraceServiceRequest'] +_EXPORTTRACESERVICERESPONSE = DESCRIPTOR.message_types_by_name['ExportTraceServiceResponse'] ExportTraceServiceRequest = _reflection.GeneratedProtocolMessageType('ExportTraceServiceRequest', (_message.Message,), { 'DESCRIPTOR' : _EXPORTTRACESERVICEREQUEST, '__module__' : 'opentelemetry.proto.collector.trace.v1.trace_service_pb2' @@ -102,32 +35,15 @@ }) _sym_db.RegisterMessage(ExportTraceServiceResponse) - -DESCRIPTOR._options = None - -_TRACESERVICE = _descriptor.ServiceDescriptor( - name='TraceService', - full_name='opentelemetry.proto.collector.trace.v1.TraceService', - file=DESCRIPTOR, - index=0, - serialized_options=None, - create_key=_descriptor._internal_create_key, - serialized_start=273, - serialized_end=435, - methods=[ - _descriptor.MethodDescriptor( - name='Export', - full_name='opentelemetry.proto.collector.trace.v1.TraceService.Export', - index=0, - containing_service=None, - input_type=_EXPORTTRACESERVICEREQUEST, - output_type=_EXPORTTRACESERVICERESPONSE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), -]) -_sym_db.RegisterServiceDescriptor(_TRACESERVICE) - -DESCRIPTOR.services_by_name['TraceService'] = _TRACESERVICE - +_TRACESERVICE = DESCRIPTOR.services_by_name['TraceService'] +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n)io.opentelemetry.proto.collector.trace.v1B\021TraceServiceProtoP\001Z1go.opentelemetry.io/proto/otlp/collector/trace/v1' + _EXPORTTRACESERVICEREQUEST._serialized_start=144 + _EXPORTTRACESERVICEREQUEST._serialized_end=240 + _EXPORTTRACESERVICERESPONSE._serialized_start=242 + _EXPORTTRACESERVICERESPONSE._serialized_end=270 + _TRACESERVICE._serialized_start=273 + _TRACESERVICE._serialized_end=435 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py index ba7d33ef61..a38431a589 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py @@ -3,6 +3,7 @@ # source: opentelemetry/proto/common/v1/common.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 @@ -13,311 +14,16 @@ -DESCRIPTOR = _descriptor.FileDescriptor( - name='opentelemetry/proto/common/v1/common.proto', - package='opentelemetry.proto.common.v1', - syntax='proto3', - serialized_options=b'\n io.opentelemetry.proto.common.v1B\013CommonProtoP\001Z(go.opentelemetry.io/proto/otlp/common/v1', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\x8c\x02\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"O\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\";\n\x16InstrumentationLibrary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t:\x02\x18\x01\"5\n\x14InstrumentationScope\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\tB[\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z(go.opentelemetry.io/proto/otlp/common/v1b\x06proto3' -) +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\x8c\x02\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"O\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\";\n\x16InstrumentationLibrary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t:\x02\x18\x01\"5\n\x14InstrumentationScope\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\tB[\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z(go.opentelemetry.io/proto/otlp/common/v1b\x06proto3') - -_ANYVALUE = _descriptor.Descriptor( - name='AnyValue', - full_name='opentelemetry.proto.common.v1.AnyValue', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='string_value', full_name='opentelemetry.proto.common.v1.AnyValue.string_value', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='bool_value', full_name='opentelemetry.proto.common.v1.AnyValue.bool_value', index=1, - number=2, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='int_value', full_name='opentelemetry.proto.common.v1.AnyValue.int_value', index=2, - number=3, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='double_value', full_name='opentelemetry.proto.common.v1.AnyValue.double_value', index=3, - number=4, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='array_value', full_name='opentelemetry.proto.common.v1.AnyValue.array_value', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='kvlist_value', full_name='opentelemetry.proto.common.v1.AnyValue.kvlist_value', index=5, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='bytes_value', full_name='opentelemetry.proto.common.v1.AnyValue.bytes_value', index=6, - number=7, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='value', full_name='opentelemetry.proto.common.v1.AnyValue.value', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=78, - serialized_end=346, -) - - -_ARRAYVALUE = _descriptor.Descriptor( - name='ArrayValue', - full_name='opentelemetry.proto.common.v1.ArrayValue', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='values', full_name='opentelemetry.proto.common.v1.ArrayValue.values', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=348, - serialized_end=417, -) - - -_KEYVALUELIST = _descriptor.Descriptor( - name='KeyValueList', - full_name='opentelemetry.proto.common.v1.KeyValueList', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='values', full_name='opentelemetry.proto.common.v1.KeyValueList.values', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=419, - serialized_end=490, -) - - -_KEYVALUE = _descriptor.Descriptor( - name='KeyValue', - full_name='opentelemetry.proto.common.v1.KeyValue', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='opentelemetry.proto.common.v1.KeyValue.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='value', full_name='opentelemetry.proto.common.v1.KeyValue.value', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=492, - serialized_end=571, -) - - -_INSTRUMENTATIONLIBRARY = _descriptor.Descriptor( - name='InstrumentationLibrary', - full_name='opentelemetry.proto.common.v1.InstrumentationLibrary', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='opentelemetry.proto.common.v1.InstrumentationLibrary.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='version', full_name='opentelemetry.proto.common.v1.InstrumentationLibrary.version', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=b'\030\001', - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=573, - serialized_end=632, -) - - -_INSTRUMENTATIONSCOPE = _descriptor.Descriptor( - name='InstrumentationScope', - full_name='opentelemetry.proto.common.v1.InstrumentationScope', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='opentelemetry.proto.common.v1.InstrumentationScope.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='version', full_name='opentelemetry.proto.common.v1.InstrumentationScope.version', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=634, - serialized_end=687, -) - -_ANYVALUE.fields_by_name['array_value'].message_type = _ARRAYVALUE -_ANYVALUE.fields_by_name['kvlist_value'].message_type = _KEYVALUELIST -_ANYVALUE.oneofs_by_name['value'].fields.append( - _ANYVALUE.fields_by_name['string_value']) -_ANYVALUE.fields_by_name['string_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] -_ANYVALUE.oneofs_by_name['value'].fields.append( - _ANYVALUE.fields_by_name['bool_value']) -_ANYVALUE.fields_by_name['bool_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] -_ANYVALUE.oneofs_by_name['value'].fields.append( - _ANYVALUE.fields_by_name['int_value']) -_ANYVALUE.fields_by_name['int_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] -_ANYVALUE.oneofs_by_name['value'].fields.append( - _ANYVALUE.fields_by_name['double_value']) -_ANYVALUE.fields_by_name['double_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] -_ANYVALUE.oneofs_by_name['value'].fields.append( - _ANYVALUE.fields_by_name['array_value']) -_ANYVALUE.fields_by_name['array_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] -_ANYVALUE.oneofs_by_name['value'].fields.append( - _ANYVALUE.fields_by_name['kvlist_value']) -_ANYVALUE.fields_by_name['kvlist_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] -_ANYVALUE.oneofs_by_name['value'].fields.append( - _ANYVALUE.fields_by_name['bytes_value']) -_ANYVALUE.fields_by_name['bytes_value'].containing_oneof = _ANYVALUE.oneofs_by_name['value'] -_ARRAYVALUE.fields_by_name['values'].message_type = _ANYVALUE -_KEYVALUELIST.fields_by_name['values'].message_type = _KEYVALUE -_KEYVALUE.fields_by_name['value'].message_type = _ANYVALUE -DESCRIPTOR.message_types_by_name['AnyValue'] = _ANYVALUE -DESCRIPTOR.message_types_by_name['ArrayValue'] = _ARRAYVALUE -DESCRIPTOR.message_types_by_name['KeyValueList'] = _KEYVALUELIST -DESCRIPTOR.message_types_by_name['KeyValue'] = _KEYVALUE -DESCRIPTOR.message_types_by_name['InstrumentationLibrary'] = _INSTRUMENTATIONLIBRARY -DESCRIPTOR.message_types_by_name['InstrumentationScope'] = _INSTRUMENTATIONSCOPE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - +_ANYVALUE = DESCRIPTOR.message_types_by_name['AnyValue'] +_ARRAYVALUE = DESCRIPTOR.message_types_by_name['ArrayValue'] +_KEYVALUELIST = DESCRIPTOR.message_types_by_name['KeyValueList'] +_KEYVALUE = DESCRIPTOR.message_types_by_name['KeyValue'] +_INSTRUMENTATIONLIBRARY = DESCRIPTOR.message_types_by_name['InstrumentationLibrary'] +_INSTRUMENTATIONSCOPE = DESCRIPTOR.message_types_by_name['InstrumentationScope'] AnyValue = _reflection.GeneratedProtocolMessageType('AnyValue', (_message.Message,), { 'DESCRIPTOR' : _ANYVALUE, '__module__' : 'opentelemetry.proto.common.v1.common_pb2' @@ -360,7 +66,22 @@ }) _sym_db.RegisterMessage(InstrumentationScope) - -DESCRIPTOR._options = None -_INSTRUMENTATIONLIBRARY._options = None +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n io.opentelemetry.proto.common.v1B\013CommonProtoP\001Z(go.opentelemetry.io/proto/otlp/common/v1' + _INSTRUMENTATIONLIBRARY._options = None + _INSTRUMENTATIONLIBRARY._serialized_options = b'\030\001' + _ANYVALUE._serialized_start=78 + _ANYVALUE._serialized_end=346 + _ARRAYVALUE._serialized_start=348 + _ARRAYVALUE._serialized_end=417 + _KEYVALUELIST._serialized_start=419 + _KEYVALUELIST._serialized_end=490 + _KEYVALUE._serialized_start=492 + _KEYVALUE._serialized_end=571 + _INSTRUMENTATIONLIBRARY._serialized_start=573 + _INSTRUMENTATIONLIBRARY._serialized_end=632 + _INSTRUMENTATIONSCOPE._serialized_start=634 + _INSTRUMENTATIONSCOPE._serialized_end=687 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py index f9e74eb67e..3967fa967b 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py @@ -4,6 +4,7 @@ """Generated protocol buffer code.""" from google.protobuf.internal import enum_type_wrapper 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 @@ -16,182 +17,11 @@ from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 -DESCRIPTOR = _descriptor.FileDescriptor( - name='opentelemetry/proto/logs/v1/logs.proto', - package='opentelemetry.proto.logs.v1', - syntax='proto3', - serialized_options=b'\n\036io.opentelemetry.proto.logs.v1B\tLogsProtoP\001Z&go.opentelemetry.io/proto/otlp/logs/v1', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n&opentelemetry/proto/logs/v1/logs.proto\x12\x1bopentelemetry.proto.logs.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"L\n\x08LogsData\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\xff\x01\n\x0cResourceLogs\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12:\n\nscope_logs\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.ScopeLogs\x12\x62\n\x1cinstrumentation_library_logs\x18\xe8\x07 \x03(\x0b\x32\x37.opentelemetry.proto.logs.v1.InstrumentationLibraryLogsB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xa0\x01\n\tScopeLogs\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc9\x01\n\x1aInstrumentationLibraryLogs\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\xef\x02\n\tLogRecord\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x1f\n\x17observed_time_unix_nano\x18\x0b \x01(\x06\x12\x44\n\x0fseverity_number\x18\x02 \x01(\x0e\x32+.opentelemetry.proto.logs.v1.SeverityNumber\x12\x15\n\rseverity_text\x18\x03 \x01(\t\x12\x35\n\x04\x62ody\x18\x05 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12;\n\nattributes\x18\x06 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x07 \x01(\r\x12\r\n\x05\x66lags\x18\x08 \x01(\x07\x12\x10\n\x08trace_id\x18\t \x01(\x0c\x12\x0f\n\x07span_id\x18\n \x01(\x0cJ\x04\x08\x04\x10\x05*\xc3\x05\n\x0eSeverityNumber\x12\x1f\n\x1bSEVERITY_NUMBER_UNSPECIFIED\x10\x00\x12\x19\n\x15SEVERITY_NUMBER_TRACE\x10\x01\x12\x1a\n\x16SEVERITY_NUMBER_TRACE2\x10\x02\x12\x1a\n\x16SEVERITY_NUMBER_TRACE3\x10\x03\x12\x1a\n\x16SEVERITY_NUMBER_TRACE4\x10\x04\x12\x19\n\x15SEVERITY_NUMBER_DEBUG\x10\x05\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG2\x10\x06\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG3\x10\x07\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG4\x10\x08\x12\x18\n\x14SEVERITY_NUMBER_INFO\x10\t\x12\x19\n\x15SEVERITY_NUMBER_INFO2\x10\n\x12\x19\n\x15SEVERITY_NUMBER_INFO3\x10\x0b\x12\x19\n\x15SEVERITY_NUMBER_INFO4\x10\x0c\x12\x18\n\x14SEVERITY_NUMBER_WARN\x10\r\x12\x19\n\x15SEVERITY_NUMBER_WARN2\x10\x0e\x12\x19\n\x15SEVERITY_NUMBER_WARN3\x10\x0f\x12\x19\n\x15SEVERITY_NUMBER_WARN4\x10\x10\x12\x19\n\x15SEVERITY_NUMBER_ERROR\x10\x11\x12\x1a\n\x16SEVERITY_NUMBER_ERROR2\x10\x12\x12\x1a\n\x16SEVERITY_NUMBER_ERROR3\x10\x13\x12\x1a\n\x16SEVERITY_NUMBER_ERROR4\x10\x14\x12\x19\n\x15SEVERITY_NUMBER_FATAL\x10\x15\x12\x1a\n\x16SEVERITY_NUMBER_FATAL2\x10\x16\x12\x1a\n\x16SEVERITY_NUMBER_FATAL3\x10\x17\x12\x1a\n\x16SEVERITY_NUMBER_FATAL4\x10\x18*X\n\x0eLogRecordFlags\x12\x1f\n\x1bLOG_RECORD_FLAG_UNSPECIFIED\x10\x00\x12%\n LOG_RECORD_FLAG_TRACE_FLAGS_MASK\x10\xff\x01\x42U\n\x1eio.opentelemetry.proto.logs.v1B\tLogsProtoP\x01Z&go.opentelemetry.io/proto/otlp/logs/v1b\x06proto3' - , - dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) - -_SEVERITYNUMBER = _descriptor.EnumDescriptor( - name='SeverityNumber', - full_name='opentelemetry.proto.logs.v1.SeverityNumber', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_UNSPECIFIED', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_TRACE', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_TRACE2', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_TRACE3', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_TRACE4', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_DEBUG', index=5, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_DEBUG2', index=6, number=6, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_DEBUG3', index=7, number=7, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_DEBUG4', index=8, number=8, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_INFO', index=9, number=9, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_INFO2', index=10, number=10, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_INFO3', index=11, number=11, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_INFO4', index=12, number=12, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_WARN', index=13, number=13, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_WARN2', index=14, number=14, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_WARN3', index=15, number=15, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_WARN4', index=16, number=16, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_ERROR', index=17, number=17, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_ERROR2', index=18, number=18, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_ERROR3', index=19, number=19, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_ERROR4', index=20, number=20, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_FATAL', index=21, number=21, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_FATAL2', index=22, number=22, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_FATAL3', index=23, number=23, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SEVERITY_NUMBER_FATAL4', index=24, number=24, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1237, - serialized_end=1944, -) -_sym_db.RegisterEnumDescriptor(_SEVERITYNUMBER) +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&opentelemetry/proto/logs/v1/logs.proto\x12\x1bopentelemetry.proto.logs.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"L\n\x08LogsData\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\xff\x01\n\x0cResourceLogs\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12:\n\nscope_logs\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.ScopeLogs\x12\x62\n\x1cinstrumentation_library_logs\x18\xe8\x07 \x03(\x0b\x32\x37.opentelemetry.proto.logs.v1.InstrumentationLibraryLogsB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xa0\x01\n\tScopeLogs\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc9\x01\n\x1aInstrumentationLibraryLogs\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\xef\x02\n\tLogRecord\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x1f\n\x17observed_time_unix_nano\x18\x0b \x01(\x06\x12\x44\n\x0fseverity_number\x18\x02 \x01(\x0e\x32+.opentelemetry.proto.logs.v1.SeverityNumber\x12\x15\n\rseverity_text\x18\x03 \x01(\t\x12\x35\n\x04\x62ody\x18\x05 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12;\n\nattributes\x18\x06 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x07 \x01(\r\x12\r\n\x05\x66lags\x18\x08 \x01(\x07\x12\x10\n\x08trace_id\x18\t \x01(\x0c\x12\x0f\n\x07span_id\x18\n \x01(\x0cJ\x04\x08\x04\x10\x05*\xc3\x05\n\x0eSeverityNumber\x12\x1f\n\x1bSEVERITY_NUMBER_UNSPECIFIED\x10\x00\x12\x19\n\x15SEVERITY_NUMBER_TRACE\x10\x01\x12\x1a\n\x16SEVERITY_NUMBER_TRACE2\x10\x02\x12\x1a\n\x16SEVERITY_NUMBER_TRACE3\x10\x03\x12\x1a\n\x16SEVERITY_NUMBER_TRACE4\x10\x04\x12\x19\n\x15SEVERITY_NUMBER_DEBUG\x10\x05\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG2\x10\x06\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG3\x10\x07\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG4\x10\x08\x12\x18\n\x14SEVERITY_NUMBER_INFO\x10\t\x12\x19\n\x15SEVERITY_NUMBER_INFO2\x10\n\x12\x19\n\x15SEVERITY_NUMBER_INFO3\x10\x0b\x12\x19\n\x15SEVERITY_NUMBER_INFO4\x10\x0c\x12\x18\n\x14SEVERITY_NUMBER_WARN\x10\r\x12\x19\n\x15SEVERITY_NUMBER_WARN2\x10\x0e\x12\x19\n\x15SEVERITY_NUMBER_WARN3\x10\x0f\x12\x19\n\x15SEVERITY_NUMBER_WARN4\x10\x10\x12\x19\n\x15SEVERITY_NUMBER_ERROR\x10\x11\x12\x1a\n\x16SEVERITY_NUMBER_ERROR2\x10\x12\x12\x1a\n\x16SEVERITY_NUMBER_ERROR3\x10\x13\x12\x1a\n\x16SEVERITY_NUMBER_ERROR4\x10\x14\x12\x19\n\x15SEVERITY_NUMBER_FATAL\x10\x15\x12\x1a\n\x16SEVERITY_NUMBER_FATAL2\x10\x16\x12\x1a\n\x16SEVERITY_NUMBER_FATAL3\x10\x17\x12\x1a\n\x16SEVERITY_NUMBER_FATAL4\x10\x18*X\n\x0eLogRecordFlags\x12\x1f\n\x1bLOG_RECORD_FLAG_UNSPECIFIED\x10\x00\x12%\n LOG_RECORD_FLAG_TRACE_FLAGS_MASK\x10\xff\x01\x42U\n\x1eio.opentelemetry.proto.logs.v1B\tLogsProtoP\x01Z&go.opentelemetry.io/proto/otlp/logs/v1b\x06proto3') +_SEVERITYNUMBER = DESCRIPTOR.enum_types_by_name['SeverityNumber'] SeverityNumber = enum_type_wrapper.EnumTypeWrapper(_SEVERITYNUMBER) -_LOGRECORDFLAGS = _descriptor.EnumDescriptor( - name='LogRecordFlags', - full_name='opentelemetry.proto.logs.v1.LogRecordFlags', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='LOG_RECORD_FLAG_UNSPECIFIED', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='LOG_RECORD_FLAG_TRACE_FLAGS_MASK', index=1, number=255, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1946, - serialized_end=2034, -) -_sym_db.RegisterEnumDescriptor(_LOGRECORDFLAGS) - +_LOGRECORDFLAGS = DESCRIPTOR.enum_types_by_name['LogRecordFlags'] LogRecordFlags = enum_type_wrapper.EnumTypeWrapper(_LOGRECORDFLAGS) SEVERITY_NUMBER_UNSPECIFIED = 0 SEVERITY_NUMBER_TRACE = 1 @@ -222,298 +52,11 @@ LOG_RECORD_FLAG_TRACE_FLAGS_MASK = 255 - -_LOGSDATA = _descriptor.Descriptor( - name='LogsData', - full_name='opentelemetry.proto.logs.v1.LogsData', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='resource_logs', full_name='opentelemetry.proto.logs.v1.LogsData.resource_logs', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=163, - serialized_end=239, -) - - -_RESOURCELOGS = _descriptor.Descriptor( - name='ResourceLogs', - full_name='opentelemetry.proto.logs.v1.ResourceLogs', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='resource', full_name='opentelemetry.proto.logs.v1.ResourceLogs.resource', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='scope_logs', full_name='opentelemetry.proto.logs.v1.ResourceLogs.scope_logs', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='instrumentation_library_logs', full_name='opentelemetry.proto.logs.v1.ResourceLogs.instrumentation_library_logs', index=2, - number=1000, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='schema_url', full_name='opentelemetry.proto.logs.v1.ResourceLogs.schema_url', index=3, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=242, - serialized_end=497, -) - - -_SCOPELOGS = _descriptor.Descriptor( - name='ScopeLogs', - full_name='opentelemetry.proto.logs.v1.ScopeLogs', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='scope', full_name='opentelemetry.proto.logs.v1.ScopeLogs.scope', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='log_records', full_name='opentelemetry.proto.logs.v1.ScopeLogs.log_records', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='schema_url', full_name='opentelemetry.proto.logs.v1.ScopeLogs.schema_url', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=500, - serialized_end=660, -) - - -_INSTRUMENTATIONLIBRARYLOGS = _descriptor.Descriptor( - name='InstrumentationLibraryLogs', - full_name='opentelemetry.proto.logs.v1.InstrumentationLibraryLogs', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='instrumentation_library', full_name='opentelemetry.proto.logs.v1.InstrumentationLibraryLogs.instrumentation_library', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='log_records', full_name='opentelemetry.proto.logs.v1.InstrumentationLibraryLogs.log_records', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='schema_url', full_name='opentelemetry.proto.logs.v1.InstrumentationLibraryLogs.schema_url', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=b'\030\001', - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=663, - serialized_end=864, -) - - -_LOGRECORD = _descriptor.Descriptor( - name='LogRecord', - full_name='opentelemetry.proto.logs.v1.LogRecord', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.logs.v1.LogRecord.time_unix_nano', index=0, - number=1, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='observed_time_unix_nano', full_name='opentelemetry.proto.logs.v1.LogRecord.observed_time_unix_nano', index=1, - number=11, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='severity_number', full_name='opentelemetry.proto.logs.v1.LogRecord.severity_number', index=2, - number=2, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='severity_text', full_name='opentelemetry.proto.logs.v1.LogRecord.severity_text', index=3, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='body', full_name='opentelemetry.proto.logs.v1.LogRecord.body', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='attributes', full_name='opentelemetry.proto.logs.v1.LogRecord.attributes', index=5, - number=6, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='dropped_attributes_count', full_name='opentelemetry.proto.logs.v1.LogRecord.dropped_attributes_count', index=6, - number=7, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='flags', full_name='opentelemetry.proto.logs.v1.LogRecord.flags', index=7, - number=8, type=7, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='trace_id', full_name='opentelemetry.proto.logs.v1.LogRecord.trace_id', index=8, - number=9, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='span_id', full_name='opentelemetry.proto.logs.v1.LogRecord.span_id', index=9, - number=10, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=867, - serialized_end=1234, -) - -_LOGSDATA.fields_by_name['resource_logs'].message_type = _RESOURCELOGS -_RESOURCELOGS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE -_RESOURCELOGS.fields_by_name['scope_logs'].message_type = _SCOPELOGS -_RESOURCELOGS.fields_by_name['instrumentation_library_logs'].message_type = _INSTRUMENTATIONLIBRARYLOGS -_SCOPELOGS.fields_by_name['scope'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONSCOPE -_SCOPELOGS.fields_by_name['log_records'].message_type = _LOGRECORD -_INSTRUMENTATIONLIBRARYLOGS.fields_by_name['instrumentation_library'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONLIBRARY -_INSTRUMENTATIONLIBRARYLOGS.fields_by_name['log_records'].message_type = _LOGRECORD -_LOGRECORD.fields_by_name['severity_number'].enum_type = _SEVERITYNUMBER -_LOGRECORD.fields_by_name['body'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._ANYVALUE -_LOGRECORD.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -DESCRIPTOR.message_types_by_name['LogsData'] = _LOGSDATA -DESCRIPTOR.message_types_by_name['ResourceLogs'] = _RESOURCELOGS -DESCRIPTOR.message_types_by_name['ScopeLogs'] = _SCOPELOGS -DESCRIPTOR.message_types_by_name['InstrumentationLibraryLogs'] = _INSTRUMENTATIONLIBRARYLOGS -DESCRIPTOR.message_types_by_name['LogRecord'] = _LOGRECORD -DESCRIPTOR.enum_types_by_name['SeverityNumber'] = _SEVERITYNUMBER -DESCRIPTOR.enum_types_by_name['LogRecordFlags'] = _LOGRECORDFLAGS -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - +_LOGSDATA = DESCRIPTOR.message_types_by_name['LogsData'] +_RESOURCELOGS = DESCRIPTOR.message_types_by_name['ResourceLogs'] +_SCOPELOGS = DESCRIPTOR.message_types_by_name['ScopeLogs'] +_INSTRUMENTATIONLIBRARYLOGS = DESCRIPTOR.message_types_by_name['InstrumentationLibraryLogs'] +_LOGRECORD = DESCRIPTOR.message_types_by_name['LogRecord'] LogsData = _reflection.GeneratedProtocolMessageType('LogsData', (_message.Message,), { 'DESCRIPTOR' : _LOGSDATA, '__module__' : 'opentelemetry.proto.logs.v1.logs_pb2' @@ -549,8 +92,26 @@ }) _sym_db.RegisterMessage(LogRecord) - -DESCRIPTOR._options = None -_RESOURCELOGS.fields_by_name['instrumentation_library_logs']._options = None -_INSTRUMENTATIONLIBRARYLOGS._options = None +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\036io.opentelemetry.proto.logs.v1B\tLogsProtoP\001Z&go.opentelemetry.io/proto/otlp/logs/v1' + _RESOURCELOGS.fields_by_name['instrumentation_library_logs']._options = None + _RESOURCELOGS.fields_by_name['instrumentation_library_logs']._serialized_options = b'\030\001' + _INSTRUMENTATIONLIBRARYLOGS._options = None + _INSTRUMENTATIONLIBRARYLOGS._serialized_options = b'\030\001' + _SEVERITYNUMBER._serialized_start=1237 + _SEVERITYNUMBER._serialized_end=1944 + _LOGRECORDFLAGS._serialized_start=1946 + _LOGRECORDFLAGS._serialized_end=2034 + _LOGSDATA._serialized_start=163 + _LOGSDATA._serialized_end=239 + _RESOURCELOGS._serialized_start=242 + _RESOURCELOGS._serialized_end=497 + _SCOPELOGS._serialized_start=500 + _SCOPELOGS._serialized_end=660 + _INSTRUMENTATIONLIBRARYLOGS._serialized_start=663 + _INSTRUMENTATIONLIBRARYLOGS._serialized_end=864 + _LOGRECORD._serialized_start=867 + _LOGRECORD._serialized_end=1234 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.py deleted file mode 100644 index 212840ca03..0000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.py +++ /dev/null @@ -1,276 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: opentelemetry/proto/metrics/experimental/metrics_config_service.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -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() - - -from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='opentelemetry/proto/metrics/experimental/metrics_config_service.proto', - package='opentelemetry.proto.metrics.experimental', - syntax='proto3', - serialized_options=b'\n+io.opentelemetry.proto.metrics.experimentalB\030MetricConfigServiceProtoP\001ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/metrics/experimental', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\nEopentelemetry/proto/metrics/experimental/metrics_config_service.proto\x12(opentelemetry.proto.metrics.experimental\x1a.opentelemetry/proto/resource/v1/resource.proto\"r\n\x13MetricConfigRequest\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x1e\n\x16last_known_fingerprint\x18\x02 \x01(\x0c\"\xe0\x03\n\x14MetricConfigResponse\x12\x13\n\x0b\x66ingerprint\x18\x01 \x01(\x0c\x12Z\n\tschedules\x18\x02 \x03(\x0b\x32G.opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule\x12\x1f\n\x17suggested_wait_time_sec\x18\x03 \x01(\x05\x1a\xb5\x02\n\x08Schedule\x12k\n\x12\x65xclusion_patterns\x18\x01 \x03(\x0b\x32O.opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern\x12k\n\x12inclusion_patterns\x18\x02 \x03(\x0b\x32O.opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern\x12\x12\n\nperiod_sec\x18\x03 \x01(\x05\x1a;\n\x07Pattern\x12\x10\n\x06\x65quals\x18\x01 \x01(\tH\x00\x12\x15\n\x0bstarts_with\x18\x02 \x01(\tH\x00\x42\x07\n\x05match2\xa1\x01\n\x0cMetricConfig\x12\x90\x01\n\x0fGetMetricConfig\x12=.opentelemetry.proto.metrics.experimental.MetricConfigRequest\x1a>.opentelemetry.proto.metrics.experimental.MetricConfigResponseB\x94\x01\n+io.opentelemetry.proto.metrics.experimentalB\x18MetricConfigServiceProtoP\x01ZIgithub.com/open-telemetry/opentelemetry-proto/gen/go/metrics/experimentalb\x06proto3' - , - dependencies=[opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) - - - - -_METRICCONFIGREQUEST = _descriptor.Descriptor( - name='MetricConfigRequest', - full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='resource', full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest.resource', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='last_known_fingerprint', full_name='opentelemetry.proto.metrics.experimental.MetricConfigRequest.last_known_fingerprint', index=1, - number=2, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=163, - serialized_end=277, -) - - -_METRICCONFIGRESPONSE_SCHEDULE_PATTERN = _descriptor.Descriptor( - name='Pattern', - full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='equals', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.equals', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='starts_with', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.starts_with', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='match', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern.match', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=701, - serialized_end=760, -) - -_METRICCONFIGRESPONSE_SCHEDULE = _descriptor.Descriptor( - name='Schedule', - full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='exclusion_patterns', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.exclusion_patterns', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='inclusion_patterns', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.inclusion_patterns', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='period_sec', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.period_sec', index=2, - number=3, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_METRICCONFIGRESPONSE_SCHEDULE_PATTERN, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=451, - serialized_end=760, -) - -_METRICCONFIGRESPONSE = _descriptor.Descriptor( - name='MetricConfigResponse', - full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='fingerprint', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.fingerprint', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='schedules', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.schedules', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='suggested_wait_time_sec', full_name='opentelemetry.proto.metrics.experimental.MetricConfigResponse.suggested_wait_time_sec', index=2, - number=3, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_METRICCONFIGRESPONSE_SCHEDULE, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=280, - serialized_end=760, -) - -_METRICCONFIGREQUEST.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE -_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.containing_type = _METRICCONFIGRESPONSE_SCHEDULE -_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'].fields.append( - _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['equals']) -_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['equals'].containing_oneof = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'] -_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'].fields.append( - _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['starts_with']) -_METRICCONFIGRESPONSE_SCHEDULE_PATTERN.fields_by_name['starts_with'].containing_oneof = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN.oneofs_by_name['match'] -_METRICCONFIGRESPONSE_SCHEDULE.fields_by_name['exclusion_patterns'].message_type = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN -_METRICCONFIGRESPONSE_SCHEDULE.fields_by_name['inclusion_patterns'].message_type = _METRICCONFIGRESPONSE_SCHEDULE_PATTERN -_METRICCONFIGRESPONSE_SCHEDULE.containing_type = _METRICCONFIGRESPONSE -_METRICCONFIGRESPONSE.fields_by_name['schedules'].message_type = _METRICCONFIGRESPONSE_SCHEDULE -DESCRIPTOR.message_types_by_name['MetricConfigRequest'] = _METRICCONFIGREQUEST -DESCRIPTOR.message_types_by_name['MetricConfigResponse'] = _METRICCONFIGRESPONSE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -MetricConfigRequest = _reflection.GeneratedProtocolMessageType('MetricConfigRequest', (_message.Message,), { - 'DESCRIPTOR' : _METRICCONFIGREQUEST, - '__module__' : 'opentelemetry.proto.metrics.experimental.metrics_config_service_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigRequest) - }) -_sym_db.RegisterMessage(MetricConfigRequest) - -MetricConfigResponse = _reflection.GeneratedProtocolMessageType('MetricConfigResponse', (_message.Message,), { - - 'Schedule' : _reflection.GeneratedProtocolMessageType('Schedule', (_message.Message,), { - - 'Pattern' : _reflection.GeneratedProtocolMessageType('Pattern', (_message.Message,), { - 'DESCRIPTOR' : _METRICCONFIGRESPONSE_SCHEDULE_PATTERN, - '__module__' : 'opentelemetry.proto.metrics.experimental.metrics_config_service_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule.Pattern) - }) - , - 'DESCRIPTOR' : _METRICCONFIGRESPONSE_SCHEDULE, - '__module__' : 'opentelemetry.proto.metrics.experimental.metrics_config_service_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigResponse.Schedule) - }) - , - 'DESCRIPTOR' : _METRICCONFIGRESPONSE, - '__module__' : 'opentelemetry.proto.metrics.experimental.metrics_config_service_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.experimental.MetricConfigResponse) - }) -_sym_db.RegisterMessage(MetricConfigResponse) -_sym_db.RegisterMessage(MetricConfigResponse.Schedule) -_sym_db.RegisterMessage(MetricConfigResponse.Schedule.Pattern) - - -DESCRIPTOR._options = None - -_METRICCONFIG = _descriptor.ServiceDescriptor( - name='MetricConfig', - full_name='opentelemetry.proto.metrics.experimental.MetricConfig', - file=DESCRIPTOR, - index=0, - serialized_options=None, - create_key=_descriptor._internal_create_key, - serialized_start=763, - serialized_end=924, - methods=[ - _descriptor.MethodDescriptor( - name='GetMetricConfig', - full_name='opentelemetry.proto.metrics.experimental.MetricConfig.GetMetricConfig', - index=0, - containing_service=None, - input_type=_METRICCONFIGREQUEST, - output_type=_METRICCONFIGRESPONSE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), -]) -_sym_db.RegisterServiceDescriptor(_METRICCONFIG) - -DESCRIPTOR.services_by_name['MetricConfig'] = _METRICCONFIG - -# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.pyi deleted file mode 100644 index ee8050802b..0000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2.pyi +++ /dev/null @@ -1,136 +0,0 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -""" -import builtins -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.message -import opentelemetry.proto.resource.v1.resource_pb2 -import typing -import typing_extensions - -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... - -class MetricConfigRequest(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - RESOURCE_FIELD_NUMBER: builtins.int - LAST_KNOWN_FINGERPRINT_FIELD_NUMBER: builtins.int - @property - def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: - """Required. The resource for which configuration should be returned.""" - pass - last_known_fingerprint: builtins.bytes = ... - """Optional. The value of MetricConfigResponse.fingerprint for the last - configuration that the caller received and successfully applied. - """ - - def __init__(self, - *, - resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., - last_known_fingerprint : builtins.bytes = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["resource",b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["last_known_fingerprint",b"last_known_fingerprint","resource",b"resource"]) -> None: ... -global___MetricConfigRequest = MetricConfigRequest - -class MetricConfigResponse(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - class Schedule(google.protobuf.message.Message): - """A Schedule is used to apply a particular scheduling configuration to - a metric. If a metric name matches a schedule's patterns, then the metric - adopts the configuration specified by the schedule. - """ - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - class Pattern(google.protobuf.message.Message): - """A light-weight pattern that can match 1 or more - metrics, for which this schedule will apply. The string is used to - match against metric names. It should not exceed 100k characters. - """ - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - EQUALS_FIELD_NUMBER: builtins.int - STARTS_WITH_FIELD_NUMBER: builtins.int - equals: typing.Text = ... - """matches the metric name exactly""" - - starts_with: typing.Text = ... - """prefix-matches the metric name""" - - def __init__(self, - *, - equals : typing.Text = ..., - starts_with : typing.Text = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["equals",b"equals","match",b"match","starts_with",b"starts_with"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["equals",b"equals","match",b"match","starts_with",b"starts_with"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["match",b"match"]) -> typing.Optional[typing_extensions.Literal["equals","starts_with"]]: ... - - EXCLUSION_PATTERNS_FIELD_NUMBER: builtins.int - INCLUSION_PATTERNS_FIELD_NUMBER: builtins.int - PERIOD_SEC_FIELD_NUMBER: builtins.int - @property - def exclusion_patterns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule.Pattern]: - """Metrics with names that match a rule in the inclusion_patterns are - targeted by this schedule. Metrics that match the exclusion_patterns - are not targeted for this schedule, even if they match an inclusion - pattern. - """ - pass - @property - def inclusion_patterns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule.Pattern]: ... - period_sec: builtins.int = ... - """Describes the collection period for each metric in seconds. - A period of 0 means to not export. - """ - - def __init__(self, - *, - exclusion_patterns : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule.Pattern]] = ..., - inclusion_patterns : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule.Pattern]] = ..., - period_sec : builtins.int = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["exclusion_patterns",b"exclusion_patterns","inclusion_patterns",b"inclusion_patterns","period_sec",b"period_sec"]) -> None: ... - - FINGERPRINT_FIELD_NUMBER: builtins.int - SCHEDULES_FIELD_NUMBER: builtins.int - SUGGESTED_WAIT_TIME_SEC_FIELD_NUMBER: builtins.int - fingerprint: builtins.bytes = ... - """Optional. The fingerprint associated with this MetricConfigResponse. Each - change in configs yields a different fingerprint. The resource SHOULD copy - this value to MetricConfigRequest.last_known_fingerprint for the next - configuration request. If there are no changes between fingerprint and - MetricConfigRequest.last_known_fingerprint, then all other fields besides - fingerprint in the response are optional, or the same as the last update if - present. - - The exact mechanics of generating the fingerprint is up to the - implementation. However, a fingerprint must be deterministically determined - by the configurations -- the same configuration will generate the same - fingerprint on any instance of an implementation. Hence using a timestamp is - unacceptable, but a deterministic hash is fine. - """ - - @property - def schedules(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricConfigResponse.Schedule]: - """A single metric may match multiple schedules. In such cases, the schedule - that specifies the smallest period is applied. - - Note, for optimization purposes, it is recommended to use as few schedules - as possible to capture all required metric updates. Where you can be - conservative, do take full advantage of the inclusion/exclusion patterns to - capture as much of your targeted metrics. - """ - pass - suggested_wait_time_sec: builtins.int = ... - """Optional. The client is suggested to wait this long (in seconds) before - pinging the configuration service again. - """ - - def __init__(self, - *, - fingerprint : builtins.bytes = ..., - schedules : typing.Optional[typing.Iterable[global___MetricConfigResponse.Schedule]] = ..., - suggested_wait_time_sec : builtins.int = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["fingerprint",b"fingerprint","schedules",b"schedules","suggested_wait_time_sec",b"suggested_wait_time_sec"]) -> None: ... -global___MetricConfigResponse = MetricConfigResponse diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2_grpc.py deleted file mode 100644 index 409ddfa261..0000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/experimental/metrics_config_service_pb2_grpc.py +++ /dev/null @@ -1,84 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc - -from opentelemetry.proto.metrics.experimental import metrics_config_service_pb2 as opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2 - - -class MetricConfigStub(object): - """MetricConfig is a service that enables updating metric schedules, trace - parameters, and other configurations on the SDK without having to restart the - instrumented application. The collector can also serve as the configuration - service, acting as a bridge between third-party configuration services and - the SDK, piping updated configs from a third-party source to an instrumented - application. - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.GetMetricConfig = channel.unary_unary( - '/opentelemetry.proto.metrics.experimental.MetricConfig/GetMetricConfig', - request_serializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigRequest.SerializeToString, - response_deserializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigResponse.FromString, - ) - - -class MetricConfigServicer(object): - """MetricConfig is a service that enables updating metric schedules, trace - parameters, and other configurations on the SDK without having to restart the - instrumented application. The collector can also serve as the configuration - service, acting as a bridge between third-party configuration services and - the SDK, piping updated configs from a third-party source to an instrumented - application. - """ - - def GetMetricConfig(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_MetricConfigServicer_to_server(servicer, server): - rpc_method_handlers = { - 'GetMetricConfig': grpc.unary_unary_rpc_method_handler( - servicer.GetMetricConfig, - request_deserializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigRequest.FromString, - response_serializer=opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'opentelemetry.proto.metrics.experimental.MetricConfig', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - - - # This class is part of an EXPERIMENTAL API. -class MetricConfig(object): - """MetricConfig is a service that enables updating metric schedules, trace - parameters, and other configurations on the SDK without having to restart the - instrumented application. The collector can also serve as the configuration - service, acting as a bridge between third-party configuration services and - the SDK, piping updated configs from a third-party source to an instrumented - application. - """ - - @staticmethod - def GetMetricConfig(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary(request, target, '/opentelemetry.proto.metrics.experimental.MetricConfig/GetMetricConfig', - opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigRequest.SerializeToString, - opentelemetry_dot_proto_dot_metrics_dot_experimental_dot_metrics__config__service__pb2.MetricConfigResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py index a64e29d673..e94d087a45 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py @@ -4,6 +4,7 @@ """Generated protocol buffer code.""" from google.protobuf.internal import enum_type_wrapper 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 @@ -16,72 +17,11 @@ from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 -DESCRIPTOR = _descriptor.FileDescriptor( - name='opentelemetry/proto/metrics/v1/metrics.proto', - package='opentelemetry.proto.metrics.v1', - syntax='proto3', - serialized_options=b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z)go.opentelemetry.io/proto/otlp/metrics/v1', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\x94\x02\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x43\n\rscope_metrics\x18\x02 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.ScopeMetrics\x12k\n\x1finstrumentation_library_metrics\x18\xe8\x07 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetricsB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x9f\x01\n\x0cScopeMetrics\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc8\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\x92\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xe6\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\r\x12\x10\n\x03min\x18\x0b \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\x0c \x01(\x01H\x02\x88\x01\x01\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_maxJ\x04\x08\x01\x10\x02\"\xb5\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\x10\n\x03min\x18\x0c \x01(\x01H\x00\x88\x01\x01\x12\x10\n\x03max\x18\r \x01(\x01H\x01\x88\x01\x01\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\x42\x06\n\x04_minB\x06\n\x04_max\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*;\n\x0e\x44\x61taPointFlags\x12\r\n\tFLAG_NONE\x10\x00\x12\x1a\n\x16\x46LAG_NO_RECORDED_VALUE\x10\x01\x42^\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z)go.opentelemetry.io/proto/otlp/metrics/v1b\x06proto3' - , - dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) - -_AGGREGATIONTEMPORALITY = _descriptor.EnumDescriptor( - name='AggregationTemporality', - full_name='opentelemetry.proto.metrics.v1.AggregationTemporality', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='AGGREGATION_TEMPORALITY_UNSPECIFIED', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='AGGREGATION_TEMPORALITY_DELTA', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='AGGREGATION_TEMPORALITY_CUMULATIVE', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=3754, - serialized_end=3894, -) -_sym_db.RegisterEnumDescriptor(_AGGREGATIONTEMPORALITY) +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\x94\x02\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x43\n\rscope_metrics\x18\x02 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.ScopeMetrics\x12k\n\x1finstrumentation_library_metrics\x18\xe8\x07 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetricsB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x9f\x01\n\x0cScopeMetrics\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc8\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\x92\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xe6\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\r\x12\x10\n\x03min\x18\x0b \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\x0c \x01(\x01H\x02\x88\x01\x01\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_maxJ\x04\x08\x01\x10\x02\"\xb5\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\x10\n\x03min\x18\x0c \x01(\x01H\x00\x88\x01\x01\x12\x10\n\x03max\x18\r \x01(\x01H\x01\x88\x01\x01\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\x42\x06\n\x04_minB\x06\n\x04_max\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*;\n\x0e\x44\x61taPointFlags\x12\r\n\tFLAG_NONE\x10\x00\x12\x1a\n\x16\x46LAG_NO_RECORDED_VALUE\x10\x01\x42^\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z)go.opentelemetry.io/proto/otlp/metrics/v1b\x06proto3') +_AGGREGATIONTEMPORALITY = DESCRIPTOR.enum_types_by_name['AggregationTemporality'] AggregationTemporality = enum_type_wrapper.EnumTypeWrapper(_AGGREGATIONTEMPORALITY) -_DATAPOINTFLAGS = _descriptor.EnumDescriptor( - name='DataPointFlags', - full_name='opentelemetry.proto.metrics.v1.DataPointFlags', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='FLAG_NONE', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='FLAG_NO_RECORDED_VALUE', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=3896, - serialized_end=3955, -) -_sym_db.RegisterEnumDescriptor(_DATAPOINTFLAGS) - +_DATAPOINTFLAGS = DESCRIPTOR.enum_types_by_name['DataPointFlags'] DataPointFlags = enum_type_wrapper.EnumTypeWrapper(_DATAPOINTFLAGS) AGGREGATION_TEMPORALITY_UNSPECIFIED = 0 AGGREGATION_TEMPORALITY_DELTA = 1 @@ -90,1096 +30,23 @@ FLAG_NO_RECORDED_VALUE = 1 - -_METRICSDATA = _descriptor.Descriptor( - name='MetricsData', - full_name='opentelemetry.proto.metrics.v1.MetricsData', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='resource_metrics', full_name='opentelemetry.proto.metrics.v1.MetricsData.resource_metrics', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=172, - serialized_end=260, -) - - -_RESOURCEMETRICS = _descriptor.Descriptor( - name='ResourceMetrics', - full_name='opentelemetry.proto.metrics.v1.ResourceMetrics', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='resource', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.resource', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='scope_metrics', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.scope_metrics', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='instrumentation_library_metrics', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.instrumentation_library_metrics', index=2, - number=1000, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='schema_url', full_name='opentelemetry.proto.metrics.v1.ResourceMetrics.schema_url', index=3, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=263, - serialized_end=539, -) - - -_SCOPEMETRICS = _descriptor.Descriptor( - name='ScopeMetrics', - full_name='opentelemetry.proto.metrics.v1.ScopeMetrics', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='scope', full_name='opentelemetry.proto.metrics.v1.ScopeMetrics.scope', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='metrics', full_name='opentelemetry.proto.metrics.v1.ScopeMetrics.metrics', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='schema_url', full_name='opentelemetry.proto.metrics.v1.ScopeMetrics.schema_url', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=542, - serialized_end=701, -) - - -_INSTRUMENTATIONLIBRARYMETRICS = _descriptor.Descriptor( - name='InstrumentationLibraryMetrics', - full_name='opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='instrumentation_library', full_name='opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics.instrumentation_library', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='metrics', full_name='opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics.metrics', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='schema_url', full_name='opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics.schema_url', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=b'\030\001', - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=704, - serialized_end=904, -) - - -_METRIC = _descriptor.Descriptor( - name='Metric', - full_name='opentelemetry.proto.metrics.v1.Metric', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='opentelemetry.proto.metrics.v1.Metric.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='description', full_name='opentelemetry.proto.metrics.v1.Metric.description', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='unit', full_name='opentelemetry.proto.metrics.v1.Metric.unit', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='gauge', full_name='opentelemetry.proto.metrics.v1.Metric.gauge', index=3, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='sum', full_name='opentelemetry.proto.metrics.v1.Metric.sum', index=4, - number=7, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='histogram', full_name='opentelemetry.proto.metrics.v1.Metric.histogram', index=5, - number=9, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='exponential_histogram', full_name='opentelemetry.proto.metrics.v1.Metric.exponential_histogram', index=6, - number=10, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='summary', full_name='opentelemetry.proto.metrics.v1.Metric.summary', index=7, - number=11, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='data', full_name='opentelemetry.proto.metrics.v1.Metric.data', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=907, - serialized_end=1309, -) - - -_GAUGE = _descriptor.Descriptor( - name='Gauge', - full_name='opentelemetry.proto.metrics.v1.Gauge', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='data_points', full_name='opentelemetry.proto.metrics.v1.Gauge.data_points', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1311, - serialized_end=1388, -) - - -_SUM = _descriptor.Descriptor( - name='Sum', - full_name='opentelemetry.proto.metrics.v1.Sum', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='data_points', full_name='opentelemetry.proto.metrics.v1.Sum.data_points', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.Sum.aggregation_temporality', index=1, - number=2, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='is_monotonic', full_name='opentelemetry.proto.metrics.v1.Sum.is_monotonic', index=2, - number=3, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1391, - serialized_end=1577, -) - - -_HISTOGRAM = _descriptor.Descriptor( - name='Histogram', - full_name='opentelemetry.proto.metrics.v1.Histogram', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='data_points', full_name='opentelemetry.proto.metrics.v1.Histogram.data_points', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.Histogram.aggregation_temporality', index=1, - number=2, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1580, - serialized_end=1753, -) - - -_EXPONENTIALHISTOGRAM = _descriptor.Descriptor( - name='ExponentialHistogram', - full_name='opentelemetry.proto.metrics.v1.ExponentialHistogram', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='data_points', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogram.data_points', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='aggregation_temporality', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogram.aggregation_temporality', index=1, - number=2, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1756, - serialized_end=1951, -) - - -_SUMMARY = _descriptor.Descriptor( - name='Summary', - full_name='opentelemetry.proto.metrics.v1.Summary', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='data_points', full_name='opentelemetry.proto.metrics.v1.Summary.data_points', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1953, - serialized_end=2033, -) - - -_NUMBERDATAPOINT = _descriptor.Descriptor( - name='NumberDataPoint', - full_name='opentelemetry.proto.metrics.v1.NumberDataPoint', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='attributes', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.attributes', index=0, - number=7, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.start_time_unix_nano', index=1, - number=2, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.time_unix_nano', index=2, - number=3, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='as_double', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.as_double', index=3, - number=4, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='as_int', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.as_int', index=4, - number=6, type=16, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='exemplars', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.exemplars', index=5, - number=5, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='flags', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.flags', index=6, - number=8, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='value', full_name='opentelemetry.proto.metrics.v1.NumberDataPoint.value', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=2036, - serialized_end=2298, -) - - -_HISTOGRAMDATAPOINT = _descriptor.Descriptor( - name='HistogramDataPoint', - full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='attributes', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.attributes', index=0, - number=9, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.start_time_unix_nano', index=1, - number=2, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.time_unix_nano', index=2, - number=3, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='count', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.count', index=3, - number=4, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='sum', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.sum', index=4, - number=5, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='bucket_counts', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.bucket_counts', index=5, - number=6, type=6, cpp_type=4, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='explicit_bounds', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.explicit_bounds', index=6, - number=7, type=1, cpp_type=5, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='exemplars', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.exemplars', index=7, - number=8, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='flags', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.flags', index=8, - number=10, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='min', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.min', index=9, - number=11, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='max', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint.max', index=10, - number=12, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='_sum', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint._sum', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - _descriptor.OneofDescriptor( - name='_min', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint._min', - index=1, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - _descriptor.OneofDescriptor( - name='_max', full_name='opentelemetry.proto.metrics.v1.HistogramDataPoint._max', - index=2, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=2301, - serialized_end=2659, -) - - -_EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS = _descriptor.Descriptor( - name='Buckets', - full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='offset', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets.offset', index=0, - number=1, type=17, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='bucket_counts', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets.bucket_counts', index=1, - number=2, type=4, cpp_type=4, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3163, - serialized_end=3211, -) - -_EXPONENTIALHISTOGRAMDATAPOINT = _descriptor.Descriptor( - name='ExponentialHistogramDataPoint', - full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='attributes', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.attributes', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.start_time_unix_nano', index=1, - number=2, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.time_unix_nano', index=2, - number=3, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='count', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.count', index=3, - number=4, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='sum', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.sum', index=4, - number=5, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='scale', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.scale', index=5, - number=6, type=17, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='zero_count', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.zero_count', index=6, - number=7, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='positive', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.positive', index=7, - number=8, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='negative', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.negative', index=8, - number=9, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='flags', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.flags', index=9, - number=10, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='exemplars', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.exemplars', index=10, - number=11, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='min', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.min', index=11, - number=12, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='max', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.max', index=12, - number=13, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='_min', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint._min', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - _descriptor.OneofDescriptor( - name='_max', full_name='opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint._max', - index=1, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=2662, - serialized_end=3227, -) - - -_SUMMARYDATAPOINT_VALUEATQUANTILE = _descriptor.Descriptor( - name='ValueAtQuantile', - full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='quantile', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile.quantile', index=0, - number=1, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='value', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile.value', index=1, - number=2, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3499, - serialized_end=3549, -) - -_SUMMARYDATAPOINT = _descriptor.Descriptor( - name='SummaryDataPoint', - full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='attributes', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.attributes', index=0, - number=7, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.start_time_unix_nano', index=1, - number=2, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.time_unix_nano', index=2, - number=3, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='count', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.count', index=3, - number=4, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='sum', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.sum', index=4, - number=5, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='quantile_values', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.quantile_values', index=5, - number=6, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='flags', full_name='opentelemetry.proto.metrics.v1.SummaryDataPoint.flags', index=6, - number=8, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_SUMMARYDATAPOINT_VALUEATQUANTILE, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3230, - serialized_end=3555, -) - - -_EXEMPLAR = _descriptor.Descriptor( - name='Exemplar', - full_name='opentelemetry.proto.metrics.v1.Exemplar', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='filtered_attributes', full_name='opentelemetry.proto.metrics.v1.Exemplar.filtered_attributes', index=0, - number=7, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.metrics.v1.Exemplar.time_unix_nano', index=1, - number=2, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='as_double', full_name='opentelemetry.proto.metrics.v1.Exemplar.as_double', index=2, - number=3, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='as_int', full_name='opentelemetry.proto.metrics.v1.Exemplar.as_int', index=3, - number=6, type=16, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='span_id', full_name='opentelemetry.proto.metrics.v1.Exemplar.span_id', index=4, - number=4, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='trace_id', full_name='opentelemetry.proto.metrics.v1.Exemplar.trace_id', index=5, - number=5, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='value', full_name='opentelemetry.proto.metrics.v1.Exemplar.value', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=3558, - serialized_end=3751, -) - -_METRICSDATA.fields_by_name['resource_metrics'].message_type = _RESOURCEMETRICS -_RESOURCEMETRICS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE -_RESOURCEMETRICS.fields_by_name['scope_metrics'].message_type = _SCOPEMETRICS -_RESOURCEMETRICS.fields_by_name['instrumentation_library_metrics'].message_type = _INSTRUMENTATIONLIBRARYMETRICS -_SCOPEMETRICS.fields_by_name['scope'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONSCOPE -_SCOPEMETRICS.fields_by_name['metrics'].message_type = _METRIC -_INSTRUMENTATIONLIBRARYMETRICS.fields_by_name['instrumentation_library'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONLIBRARY -_INSTRUMENTATIONLIBRARYMETRICS.fields_by_name['metrics'].message_type = _METRIC -_METRIC.fields_by_name['gauge'].message_type = _GAUGE -_METRIC.fields_by_name['sum'].message_type = _SUM -_METRIC.fields_by_name['histogram'].message_type = _HISTOGRAM -_METRIC.fields_by_name['exponential_histogram'].message_type = _EXPONENTIALHISTOGRAM -_METRIC.fields_by_name['summary'].message_type = _SUMMARY -_METRIC.oneofs_by_name['data'].fields.append( - _METRIC.fields_by_name['gauge']) -_METRIC.fields_by_name['gauge'].containing_oneof = _METRIC.oneofs_by_name['data'] -_METRIC.oneofs_by_name['data'].fields.append( - _METRIC.fields_by_name['sum']) -_METRIC.fields_by_name['sum'].containing_oneof = _METRIC.oneofs_by_name['data'] -_METRIC.oneofs_by_name['data'].fields.append( - _METRIC.fields_by_name['histogram']) -_METRIC.fields_by_name['histogram'].containing_oneof = _METRIC.oneofs_by_name['data'] -_METRIC.oneofs_by_name['data'].fields.append( - _METRIC.fields_by_name['exponential_histogram']) -_METRIC.fields_by_name['exponential_histogram'].containing_oneof = _METRIC.oneofs_by_name['data'] -_METRIC.oneofs_by_name['data'].fields.append( - _METRIC.fields_by_name['summary']) -_METRIC.fields_by_name['summary'].containing_oneof = _METRIC.oneofs_by_name['data'] -_GAUGE.fields_by_name['data_points'].message_type = _NUMBERDATAPOINT -_SUM.fields_by_name['data_points'].message_type = _NUMBERDATAPOINT -_SUM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY -_HISTOGRAM.fields_by_name['data_points'].message_type = _HISTOGRAMDATAPOINT -_HISTOGRAM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY -_EXPONENTIALHISTOGRAM.fields_by_name['data_points'].message_type = _EXPONENTIALHISTOGRAMDATAPOINT -_EXPONENTIALHISTOGRAM.fields_by_name['aggregation_temporality'].enum_type = _AGGREGATIONTEMPORALITY -_SUMMARY.fields_by_name['data_points'].message_type = _SUMMARYDATAPOINT -_NUMBERDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -_NUMBERDATAPOINT.fields_by_name['exemplars'].message_type = _EXEMPLAR -_NUMBERDATAPOINT.oneofs_by_name['value'].fields.append( - _NUMBERDATAPOINT.fields_by_name['as_double']) -_NUMBERDATAPOINT.fields_by_name['as_double'].containing_oneof = _NUMBERDATAPOINT.oneofs_by_name['value'] -_NUMBERDATAPOINT.oneofs_by_name['value'].fields.append( - _NUMBERDATAPOINT.fields_by_name['as_int']) -_NUMBERDATAPOINT.fields_by_name['as_int'].containing_oneof = _NUMBERDATAPOINT.oneofs_by_name['value'] -_HISTOGRAMDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -_HISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _EXEMPLAR -_HISTOGRAMDATAPOINT.oneofs_by_name['_sum'].fields.append( - _HISTOGRAMDATAPOINT.fields_by_name['sum']) -_HISTOGRAMDATAPOINT.fields_by_name['sum'].containing_oneof = _HISTOGRAMDATAPOINT.oneofs_by_name['_sum'] -_HISTOGRAMDATAPOINT.oneofs_by_name['_min'].fields.append( - _HISTOGRAMDATAPOINT.fields_by_name['min']) -_HISTOGRAMDATAPOINT.fields_by_name['min'].containing_oneof = _HISTOGRAMDATAPOINT.oneofs_by_name['_min'] -_HISTOGRAMDATAPOINT.oneofs_by_name['_max'].fields.append( - _HISTOGRAMDATAPOINT.fields_by_name['max']) -_HISTOGRAMDATAPOINT.fields_by_name['max'].containing_oneof = _HISTOGRAMDATAPOINT.oneofs_by_name['_max'] -_EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS.containing_type = _EXPONENTIALHISTOGRAMDATAPOINT -_EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -_EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['positive'].message_type = _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS -_EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['negative'].message_type = _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS -_EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['exemplars'].message_type = _EXEMPLAR -_EXPONENTIALHISTOGRAMDATAPOINT.oneofs_by_name['_min'].fields.append( - _EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['min']) -_EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['min'].containing_oneof = _EXPONENTIALHISTOGRAMDATAPOINT.oneofs_by_name['_min'] -_EXPONENTIALHISTOGRAMDATAPOINT.oneofs_by_name['_max'].fields.append( - _EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['max']) -_EXPONENTIALHISTOGRAMDATAPOINT.fields_by_name['max'].containing_oneof = _EXPONENTIALHISTOGRAMDATAPOINT.oneofs_by_name['_max'] -_SUMMARYDATAPOINT_VALUEATQUANTILE.containing_type = _SUMMARYDATAPOINT -_SUMMARYDATAPOINT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -_SUMMARYDATAPOINT.fields_by_name['quantile_values'].message_type = _SUMMARYDATAPOINT_VALUEATQUANTILE -_EXEMPLAR.fields_by_name['filtered_attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -_EXEMPLAR.oneofs_by_name['value'].fields.append( - _EXEMPLAR.fields_by_name['as_double']) -_EXEMPLAR.fields_by_name['as_double'].containing_oneof = _EXEMPLAR.oneofs_by_name['value'] -_EXEMPLAR.oneofs_by_name['value'].fields.append( - _EXEMPLAR.fields_by_name['as_int']) -_EXEMPLAR.fields_by_name['as_int'].containing_oneof = _EXEMPLAR.oneofs_by_name['value'] -DESCRIPTOR.message_types_by_name['MetricsData'] = _METRICSDATA -DESCRIPTOR.message_types_by_name['ResourceMetrics'] = _RESOURCEMETRICS -DESCRIPTOR.message_types_by_name['ScopeMetrics'] = _SCOPEMETRICS -DESCRIPTOR.message_types_by_name['InstrumentationLibraryMetrics'] = _INSTRUMENTATIONLIBRARYMETRICS -DESCRIPTOR.message_types_by_name['Metric'] = _METRIC -DESCRIPTOR.message_types_by_name['Gauge'] = _GAUGE -DESCRIPTOR.message_types_by_name['Sum'] = _SUM -DESCRIPTOR.message_types_by_name['Histogram'] = _HISTOGRAM -DESCRIPTOR.message_types_by_name['ExponentialHistogram'] = _EXPONENTIALHISTOGRAM -DESCRIPTOR.message_types_by_name['Summary'] = _SUMMARY -DESCRIPTOR.message_types_by_name['NumberDataPoint'] = _NUMBERDATAPOINT -DESCRIPTOR.message_types_by_name['HistogramDataPoint'] = _HISTOGRAMDATAPOINT -DESCRIPTOR.message_types_by_name['ExponentialHistogramDataPoint'] = _EXPONENTIALHISTOGRAMDATAPOINT -DESCRIPTOR.message_types_by_name['SummaryDataPoint'] = _SUMMARYDATAPOINT -DESCRIPTOR.message_types_by_name['Exemplar'] = _EXEMPLAR -DESCRIPTOR.enum_types_by_name['AggregationTemporality'] = _AGGREGATIONTEMPORALITY -DESCRIPTOR.enum_types_by_name['DataPointFlags'] = _DATAPOINTFLAGS -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - +_METRICSDATA = DESCRIPTOR.message_types_by_name['MetricsData'] +_RESOURCEMETRICS = DESCRIPTOR.message_types_by_name['ResourceMetrics'] +_SCOPEMETRICS = DESCRIPTOR.message_types_by_name['ScopeMetrics'] +_INSTRUMENTATIONLIBRARYMETRICS = DESCRIPTOR.message_types_by_name['InstrumentationLibraryMetrics'] +_METRIC = DESCRIPTOR.message_types_by_name['Metric'] +_GAUGE = DESCRIPTOR.message_types_by_name['Gauge'] +_SUM = DESCRIPTOR.message_types_by_name['Sum'] +_HISTOGRAM = DESCRIPTOR.message_types_by_name['Histogram'] +_EXPONENTIALHISTOGRAM = DESCRIPTOR.message_types_by_name['ExponentialHistogram'] +_SUMMARY = DESCRIPTOR.message_types_by_name['Summary'] +_NUMBERDATAPOINT = DESCRIPTOR.message_types_by_name['NumberDataPoint'] +_HISTOGRAMDATAPOINT = DESCRIPTOR.message_types_by_name['HistogramDataPoint'] +_EXPONENTIALHISTOGRAMDATAPOINT = DESCRIPTOR.message_types_by_name['ExponentialHistogramDataPoint'] +_EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS = _EXPONENTIALHISTOGRAMDATAPOINT.nested_types_by_name['Buckets'] +_SUMMARYDATAPOINT = DESCRIPTOR.message_types_by_name['SummaryDataPoint'] +_SUMMARYDATAPOINT_VALUEATQUANTILE = _SUMMARYDATAPOINT.nested_types_by_name['ValueAtQuantile'] +_EXEMPLAR = DESCRIPTOR.message_types_by_name['Exemplar'] MetricsData = _reflection.GeneratedProtocolMessageType('MetricsData', (_message.Message,), { 'DESCRIPTOR' : _METRICSDATA, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' @@ -1301,8 +168,50 @@ }) _sym_db.RegisterMessage(Exemplar) - -DESCRIPTOR._options = None -_RESOURCEMETRICS.fields_by_name['instrumentation_library_metrics']._options = None -_INSTRUMENTATIONLIBRARYMETRICS._options = None +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z)go.opentelemetry.io/proto/otlp/metrics/v1' + _RESOURCEMETRICS.fields_by_name['instrumentation_library_metrics']._options = None + _RESOURCEMETRICS.fields_by_name['instrumentation_library_metrics']._serialized_options = b'\030\001' + _INSTRUMENTATIONLIBRARYMETRICS._options = None + _INSTRUMENTATIONLIBRARYMETRICS._serialized_options = b'\030\001' + _AGGREGATIONTEMPORALITY._serialized_start=3754 + _AGGREGATIONTEMPORALITY._serialized_end=3894 + _DATAPOINTFLAGS._serialized_start=3896 + _DATAPOINTFLAGS._serialized_end=3955 + _METRICSDATA._serialized_start=172 + _METRICSDATA._serialized_end=260 + _RESOURCEMETRICS._serialized_start=263 + _RESOURCEMETRICS._serialized_end=539 + _SCOPEMETRICS._serialized_start=542 + _SCOPEMETRICS._serialized_end=701 + _INSTRUMENTATIONLIBRARYMETRICS._serialized_start=704 + _INSTRUMENTATIONLIBRARYMETRICS._serialized_end=904 + _METRIC._serialized_start=907 + _METRIC._serialized_end=1309 + _GAUGE._serialized_start=1311 + _GAUGE._serialized_end=1388 + _SUM._serialized_start=1391 + _SUM._serialized_end=1577 + _HISTOGRAM._serialized_start=1580 + _HISTOGRAM._serialized_end=1753 + _EXPONENTIALHISTOGRAM._serialized_start=1756 + _EXPONENTIALHISTOGRAM._serialized_end=1951 + _SUMMARY._serialized_start=1953 + _SUMMARY._serialized_end=2033 + _NUMBERDATAPOINT._serialized_start=2036 + _NUMBERDATAPOINT._serialized_end=2298 + _HISTOGRAMDATAPOINT._serialized_start=2301 + _HISTOGRAMDATAPOINT._serialized_end=2659 + _EXPONENTIALHISTOGRAMDATAPOINT._serialized_start=2662 + _EXPONENTIALHISTOGRAMDATAPOINT._serialized_end=3227 + _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS._serialized_start=3163 + _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS._serialized_end=3211 + _SUMMARYDATAPOINT._serialized_start=3230 + _SUMMARYDATAPOINT._serialized_end=3555 + _SUMMARYDATAPOINT_VALUEATQUANTILE._serialized_start=3499 + _SUMMARYDATAPOINT_VALUEATQUANTILE._serialized_end=3549 + _EXEMPLAR._serialized_start=3558 + _EXEMPLAR._serialized_end=3751 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py index 553053c28a..2c31f9a3fa 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py @@ -3,6 +3,7 @@ # source: opentelemetry/proto/resource/v1/resource.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 @@ -14,61 +15,11 @@ from opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 -DESCRIPTOR = _descriptor.FileDescriptor( - name='opentelemetry/proto/resource/v1/resource.proto', - package='opentelemetry.proto.resource.v1', - syntax='proto3', - serialized_options=b'\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\001Z*go.opentelemetry.io/proto/otlp/resource/v1', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"i\n\x08Resource\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\rBa\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z*go.opentelemetry.io/proto/otlp/resource/v1b\x06proto3' - , - dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,]) +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"i\n\x08Resource\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\rBa\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z*go.opentelemetry.io/proto/otlp/resource/v1b\x06proto3') - -_RESOURCE = _descriptor.Descriptor( - name='Resource', - full_name='opentelemetry.proto.resource.v1.Resource', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='attributes', full_name='opentelemetry.proto.resource.v1.Resource.attributes', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='dropped_attributes_count', full_name='opentelemetry.proto.resource.v1.Resource.dropped_attributes_count', index=1, - number=2, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=127, - serialized_end=232, -) - -_RESOURCE.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -DESCRIPTOR.message_types_by_name['Resource'] = _RESOURCE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - +_RESOURCE = DESCRIPTOR.message_types_by_name['Resource'] Resource = _reflection.GeneratedProtocolMessageType('Resource', (_message.Message,), { 'DESCRIPTOR' : _RESOURCE, '__module__' : 'opentelemetry.proto.resource.v1.resource_pb2' @@ -76,6 +27,10 @@ }) _sym_db.RegisterMessage(Resource) +if _descriptor._USE_C_DESCRIPTORS == False: -DESCRIPTOR._options = None + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\001Z*go.opentelemetry.io/proto/otlp/resource/v1' + _RESOURCE._serialized_start=127 + _RESOURCE._serialized_end=232 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py index 4b7cc3a69b..a54a2b68d0 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py @@ -3,6 +3,7 @@ # source: opentelemetry/proto/trace/v1/trace_config.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 @@ -13,250 +14,15 @@ -DESCRIPTOR = _descriptor.FileDescriptor( - name='opentelemetry/proto/trace/v1/trace_config.proto', - package='opentelemetry.proto.trace.v1', - syntax='proto3', - serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\020TraceConfigProtoP\001Z1go.opentelemetry.io/proto/otlp/collector/trace/v1', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n/opentelemetry/proto/trace/v1/trace_config.proto\x12\x1copentelemetry.proto.trace.v1\"\xc8\x03\n\x0bTraceConfig\x12I\n\x10\x63onstant_sampler\x18\x01 \x01(\x0b\x32-.opentelemetry.proto.trace.v1.ConstantSamplerH\x00\x12O\n\x14trace_id_ratio_based\x18\x02 \x01(\x0b\x32/.opentelemetry.proto.trace.v1.TraceIdRatioBasedH\x00\x12R\n\x15rate_limiting_sampler\x18\x03 \x01(\x0b\x32\x31.opentelemetry.proto.trace.v1.RateLimitingSamplerH\x00\x12 \n\x18max_number_of_attributes\x18\x04 \x01(\x03\x12\"\n\x1amax_number_of_timed_events\x18\x05 \x01(\x03\x12\x30\n(max_number_of_attributes_per_timed_event\x18\x06 \x01(\x03\x12\x1b\n\x13max_number_of_links\x18\x07 \x01(\x03\x12)\n!max_number_of_attributes_per_link\x18\x08 \x01(\x03\x42\t\n\x07sampler\"\xa9\x01\n\x0f\x43onstantSampler\x12P\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32>.opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision\"D\n\x10\x43onstantDecision\x12\x0e\n\nALWAYS_OFF\x10\x00\x12\r\n\tALWAYS_ON\x10\x01\x12\x11\n\rALWAYS_PARENT\x10\x02\"*\n\x11TraceIdRatioBased\x12\x15\n\rsamplingRatio\x18\x01 \x01(\x01\"\"\n\x13RateLimitingSampler\x12\x0b\n\x03qps\x18\x01 \x01(\x03\x42h\n\x1fio.opentelemetry.proto.trace.v1B\x10TraceConfigProtoP\x01Z1go.opentelemetry.io/proto/otlp/collector/trace/v1b\x06proto3' -) +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n/opentelemetry/proto/trace/v1/trace_config.proto\x12\x1copentelemetry.proto.trace.v1\"\xc8\x03\n\x0bTraceConfig\x12I\n\x10\x63onstant_sampler\x18\x01 \x01(\x0b\x32-.opentelemetry.proto.trace.v1.ConstantSamplerH\x00\x12O\n\x14trace_id_ratio_based\x18\x02 \x01(\x0b\x32/.opentelemetry.proto.trace.v1.TraceIdRatioBasedH\x00\x12R\n\x15rate_limiting_sampler\x18\x03 \x01(\x0b\x32\x31.opentelemetry.proto.trace.v1.RateLimitingSamplerH\x00\x12 \n\x18max_number_of_attributes\x18\x04 \x01(\x03\x12\"\n\x1amax_number_of_timed_events\x18\x05 \x01(\x03\x12\x30\n(max_number_of_attributes_per_timed_event\x18\x06 \x01(\x03\x12\x1b\n\x13max_number_of_links\x18\x07 \x01(\x03\x12)\n!max_number_of_attributes_per_link\x18\x08 \x01(\x03\x42\t\n\x07sampler\"\xa9\x01\n\x0f\x43onstantSampler\x12P\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32>.opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision\"D\n\x10\x43onstantDecision\x12\x0e\n\nALWAYS_OFF\x10\x00\x12\r\n\tALWAYS_ON\x10\x01\x12\x11\n\rALWAYS_PARENT\x10\x02\"*\n\x11TraceIdRatioBased\x12\x15\n\rsamplingRatio\x18\x01 \x01(\x01\"\"\n\x13RateLimitingSampler\x12\x0b\n\x03qps\x18\x01 \x01(\x03\x42h\n\x1fio.opentelemetry.proto.trace.v1B\x10TraceConfigProtoP\x01Z1go.opentelemetry.io/proto/otlp/collector/trace/v1b\x06proto3') -_CONSTANTSAMPLER_CONSTANTDECISION = _descriptor.EnumDescriptor( - name='ConstantDecision', - full_name='opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='ALWAYS_OFF', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ALWAYS_ON', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ALWAYS_PARENT', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=642, - serialized_end=710, -) -_sym_db.RegisterEnumDescriptor(_CONSTANTSAMPLER_CONSTANTDECISION) - - -_TRACECONFIG = _descriptor.Descriptor( - name='TraceConfig', - full_name='opentelemetry.proto.trace.v1.TraceConfig', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='constant_sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.constant_sampler', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='trace_id_ratio_based', full_name='opentelemetry.proto.trace.v1.TraceConfig.trace_id_ratio_based', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='rate_limiting_sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.rate_limiting_sampler', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='max_number_of_attributes', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_attributes', index=3, - number=4, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='max_number_of_timed_events', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_timed_events', index=4, - number=5, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='max_number_of_attributes_per_timed_event', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_attributes_per_timed_event', index=5, - number=6, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='max_number_of_links', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_links', index=6, - number=7, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='max_number_of_attributes_per_link', full_name='opentelemetry.proto.trace.v1.TraceConfig.max_number_of_attributes_per_link', index=7, - number=8, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='sampler', full_name='opentelemetry.proto.trace.v1.TraceConfig.sampler', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=82, - serialized_end=538, -) - - -_CONSTANTSAMPLER = _descriptor.Descriptor( - name='ConstantSampler', - full_name='opentelemetry.proto.trace.v1.ConstantSampler', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='decision', full_name='opentelemetry.proto.trace.v1.ConstantSampler.decision', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _CONSTANTSAMPLER_CONSTANTDECISION, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=541, - serialized_end=710, -) - - -_TRACEIDRATIOBASED = _descriptor.Descriptor( - name='TraceIdRatioBased', - full_name='opentelemetry.proto.trace.v1.TraceIdRatioBased', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='samplingRatio', full_name='opentelemetry.proto.trace.v1.TraceIdRatioBased.samplingRatio', index=0, - number=1, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=712, - serialized_end=754, -) - - -_RATELIMITINGSAMPLER = _descriptor.Descriptor( - name='RateLimitingSampler', - full_name='opentelemetry.proto.trace.v1.RateLimitingSampler', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='qps', full_name='opentelemetry.proto.trace.v1.RateLimitingSampler.qps', index=0, - number=1, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=756, - serialized_end=790, -) - -_TRACECONFIG.fields_by_name['constant_sampler'].message_type = _CONSTANTSAMPLER -_TRACECONFIG.fields_by_name['trace_id_ratio_based'].message_type = _TRACEIDRATIOBASED -_TRACECONFIG.fields_by_name['rate_limiting_sampler'].message_type = _RATELIMITINGSAMPLER -_TRACECONFIG.oneofs_by_name['sampler'].fields.append( - _TRACECONFIG.fields_by_name['constant_sampler']) -_TRACECONFIG.fields_by_name['constant_sampler'].containing_oneof = _TRACECONFIG.oneofs_by_name['sampler'] -_TRACECONFIG.oneofs_by_name['sampler'].fields.append( - _TRACECONFIG.fields_by_name['trace_id_ratio_based']) -_TRACECONFIG.fields_by_name['trace_id_ratio_based'].containing_oneof = _TRACECONFIG.oneofs_by_name['sampler'] -_TRACECONFIG.oneofs_by_name['sampler'].fields.append( - _TRACECONFIG.fields_by_name['rate_limiting_sampler']) -_TRACECONFIG.fields_by_name['rate_limiting_sampler'].containing_oneof = _TRACECONFIG.oneofs_by_name['sampler'] -_CONSTANTSAMPLER.fields_by_name['decision'].enum_type = _CONSTANTSAMPLER_CONSTANTDECISION -_CONSTANTSAMPLER_CONSTANTDECISION.containing_type = _CONSTANTSAMPLER -DESCRIPTOR.message_types_by_name['TraceConfig'] = _TRACECONFIG -DESCRIPTOR.message_types_by_name['ConstantSampler'] = _CONSTANTSAMPLER -DESCRIPTOR.message_types_by_name['TraceIdRatioBased'] = _TRACEIDRATIOBASED -DESCRIPTOR.message_types_by_name['RateLimitingSampler'] = _RATELIMITINGSAMPLER -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - +_TRACECONFIG = DESCRIPTOR.message_types_by_name['TraceConfig'] +_CONSTANTSAMPLER = DESCRIPTOR.message_types_by_name['ConstantSampler'] +_TRACEIDRATIOBASED = DESCRIPTOR.message_types_by_name['TraceIdRatioBased'] +_RATELIMITINGSAMPLER = DESCRIPTOR.message_types_by_name['RateLimitingSampler'] +_CONSTANTSAMPLER_CONSTANTDECISION = _CONSTANTSAMPLER.enum_types_by_name['ConstantDecision'] TraceConfig = _reflection.GeneratedProtocolMessageType('TraceConfig', (_message.Message,), { 'DESCRIPTOR' : _TRACECONFIG, '__module__' : 'opentelemetry.proto.trace.v1.trace_config_pb2' @@ -285,6 +51,18 @@ }) _sym_db.RegisterMessage(RateLimitingSampler) - -DESCRIPTOR._options = None +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\037io.opentelemetry.proto.trace.v1B\020TraceConfigProtoP\001Z1go.opentelemetry.io/proto/otlp/collector/trace/v1' + _TRACECONFIG._serialized_start=82 + _TRACECONFIG._serialized_end=538 + _CONSTANTSAMPLER._serialized_start=541 + _CONSTANTSAMPLER._serialized_end=710 + _CONSTANTSAMPLER_CONSTANTDECISION._serialized_start=642 + _CONSTANTSAMPLER_CONSTANTDECISION._serialized_end=710 + _TRACEIDRATIOBASED._serialized_start=712 + _TRACEIDRATIOBASED._serialized_end=754 + _RATELIMITINGSAMPLER._serialized_start=756 + _RATELIMITINGSAMPLER._serialized_end=790 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py index 7c4057ca7b..0827c14301 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py @@ -3,6 +3,7 @@ # source: opentelemetry/proto/trace/v1/trace.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 @@ -15,580 +16,20 @@ from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 -DESCRIPTOR = _descriptor.FileDescriptor( - name='opentelemetry/proto/trace/v1/trace.proto', - package='opentelemetry.proto.trace.v1', - syntax='proto3', - serialized_options=b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z\'go.opentelemetry.io/proto/otlp/trace/v1', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"Q\n\nTracesData\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\x86\x02\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12=\n\x0bscope_spans\x18\x02 \x03(\x0b\x32(.opentelemetry.proto.trace.v1.ScopeSpans\x12\x65\n\x1dinstrumentation_library_spans\x18\xe8\x07 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpansB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x97\x01\n\nScopeSpans\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc0\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xae\x01\n\x06Status\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02J\x04\x08\x01\x10\x02\x42X\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z\'go.opentelemetry.io/proto/otlp/trace/v1b\x06proto3' - , - dependencies=[opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2.DESCRIPTOR,opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2.DESCRIPTOR,]) - - - -_SPAN_SPANKIND = _descriptor.EnumDescriptor( - name='SpanKind', - full_name='opentelemetry.proto.trace.v1.Span.SpanKind', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='SPAN_KIND_UNSPECIFIED', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SPAN_KIND_INTERNAL', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SPAN_KIND_SERVER', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SPAN_KIND_CLIENT', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SPAN_KIND_PRODUCER', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SPAN_KIND_CONSUMER', index=5, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1709, - serialized_end=1862, -) -_sym_db.RegisterEnumDescriptor(_SPAN_SPANKIND) - -_STATUS_STATUSCODE = _descriptor.EnumDescriptor( - name='StatusCode', - full_name='opentelemetry.proto.trace.v1.Status.StatusCode', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='STATUS_CODE_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='STATUS_CODE_OK', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='STATUS_CODE_ERROR', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1955, - serialized_end=2033, -) -_sym_db.RegisterEnumDescriptor(_STATUS_STATUSCODE) - - -_TRACESDATA = _descriptor.Descriptor( - name='TracesData', - full_name='opentelemetry.proto.trace.v1.TracesData', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='resource_spans', full_name='opentelemetry.proto.trace.v1.TracesData.resource_spans', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=166, - serialized_end=247, -) - - -_RESOURCESPANS = _descriptor.Descriptor( - name='ResourceSpans', - full_name='opentelemetry.proto.trace.v1.ResourceSpans', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='resource', full_name='opentelemetry.proto.trace.v1.ResourceSpans.resource', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='scope_spans', full_name='opentelemetry.proto.trace.v1.ResourceSpans.scope_spans', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='instrumentation_library_spans', full_name='opentelemetry.proto.trace.v1.ResourceSpans.instrumentation_library_spans', index=2, - number=1000, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='schema_url', full_name='opentelemetry.proto.trace.v1.ResourceSpans.schema_url', index=3, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=250, - serialized_end=512, -) - +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"Q\n\nTracesData\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\x86\x02\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12=\n\x0bscope_spans\x18\x02 \x03(\x0b\x32(.opentelemetry.proto.trace.v1.ScopeSpans\x12\x65\n\x1dinstrumentation_library_spans\x18\xe8\x07 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpansB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x97\x01\n\nScopeSpans\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc0\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xae\x01\n\x06Status\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02J\x04\x08\x01\x10\x02\x42X\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z\'go.opentelemetry.io/proto/otlp/trace/v1b\x06proto3') -_SCOPESPANS = _descriptor.Descriptor( - name='ScopeSpans', - full_name='opentelemetry.proto.trace.v1.ScopeSpans', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='scope', full_name='opentelemetry.proto.trace.v1.ScopeSpans.scope', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='spans', full_name='opentelemetry.proto.trace.v1.ScopeSpans.spans', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='schema_url', full_name='opentelemetry.proto.trace.v1.ScopeSpans.schema_url', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=515, - serialized_end=666, -) -_INSTRUMENTATIONLIBRARYSPANS = _descriptor.Descriptor( - name='InstrumentationLibrarySpans', - full_name='opentelemetry.proto.trace.v1.InstrumentationLibrarySpans', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='instrumentation_library', full_name='opentelemetry.proto.trace.v1.InstrumentationLibrarySpans.instrumentation_library', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='spans', full_name='opentelemetry.proto.trace.v1.InstrumentationLibrarySpans.spans', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='schema_url', full_name='opentelemetry.proto.trace.v1.InstrumentationLibrarySpans.schema_url', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=b'\030\001', - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=669, - serialized_end=861, -) - - -_SPAN_EVENT = _descriptor.Descriptor( - name='Event', - full_name='opentelemetry.proto.trace.v1.Span.Event', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='time_unix_nano', full_name='opentelemetry.proto.trace.v1.Span.Event.time_unix_nano', index=0, - number=1, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='name', full_name='opentelemetry.proto.trace.v1.Span.Event.name', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='attributes', full_name='opentelemetry.proto.trace.v1.Span.Event.attributes', index=2, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='dropped_attributes_count', full_name='opentelemetry.proto.trace.v1.Span.Event.dropped_attributes_count', index=3, - number=4, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1406, - serialized_end=1546, -) - -_SPAN_LINK = _descriptor.Descriptor( - name='Link', - full_name='opentelemetry.proto.trace.v1.Span.Link', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='trace_id', full_name='opentelemetry.proto.trace.v1.Span.Link.trace_id', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='span_id', full_name='opentelemetry.proto.trace.v1.Span.Link.span_id', index=1, - number=2, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='trace_state', full_name='opentelemetry.proto.trace.v1.Span.Link.trace_state', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='attributes', full_name='opentelemetry.proto.trace.v1.Span.Link.attributes', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='dropped_attributes_count', full_name='opentelemetry.proto.trace.v1.Span.Link.dropped_attributes_count', index=4, - number=5, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1549, - serialized_end=1706, -) - -_SPAN = _descriptor.Descriptor( - name='Span', - full_name='opentelemetry.proto.trace.v1.Span', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='trace_id', full_name='opentelemetry.proto.trace.v1.Span.trace_id', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='span_id', full_name='opentelemetry.proto.trace.v1.Span.span_id', index=1, - number=2, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='trace_state', full_name='opentelemetry.proto.trace.v1.Span.trace_state', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='parent_span_id', full_name='opentelemetry.proto.trace.v1.Span.parent_span_id', index=3, - number=4, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='name', full_name='opentelemetry.proto.trace.v1.Span.name', index=4, - number=5, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='kind', full_name='opentelemetry.proto.trace.v1.Span.kind', index=5, - number=6, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='start_time_unix_nano', full_name='opentelemetry.proto.trace.v1.Span.start_time_unix_nano', index=6, - number=7, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='end_time_unix_nano', full_name='opentelemetry.proto.trace.v1.Span.end_time_unix_nano', index=7, - number=8, type=6, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='attributes', full_name='opentelemetry.proto.trace.v1.Span.attributes', index=8, - number=9, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='dropped_attributes_count', full_name='opentelemetry.proto.trace.v1.Span.dropped_attributes_count', index=9, - number=10, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='events', full_name='opentelemetry.proto.trace.v1.Span.events', index=10, - number=11, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='dropped_events_count', full_name='opentelemetry.proto.trace.v1.Span.dropped_events_count', index=11, - number=12, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='links', full_name='opentelemetry.proto.trace.v1.Span.links', index=12, - number=13, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='dropped_links_count', full_name='opentelemetry.proto.trace.v1.Span.dropped_links_count', index=13, - number=14, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='status', full_name='opentelemetry.proto.trace.v1.Span.status', index=14, - number=15, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_SPAN_EVENT, _SPAN_LINK, ], - enum_types=[ - _SPAN_SPANKIND, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=864, - serialized_end=1862, -) - - -_STATUS = _descriptor.Descriptor( - name='Status', - full_name='opentelemetry.proto.trace.v1.Status', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='message', full_name='opentelemetry.proto.trace.v1.Status.message', index=0, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='code', full_name='opentelemetry.proto.trace.v1.Status.code', index=1, - number=3, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _STATUS_STATUSCODE, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1865, - serialized_end=2039, -) - -_TRACESDATA.fields_by_name['resource_spans'].message_type = _RESOURCESPANS -_RESOURCESPANS.fields_by_name['resource'].message_type = opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2._RESOURCE -_RESOURCESPANS.fields_by_name['scope_spans'].message_type = _SCOPESPANS -_RESOURCESPANS.fields_by_name['instrumentation_library_spans'].message_type = _INSTRUMENTATIONLIBRARYSPANS -_SCOPESPANS.fields_by_name['scope'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONSCOPE -_SCOPESPANS.fields_by_name['spans'].message_type = _SPAN -_INSTRUMENTATIONLIBRARYSPANS.fields_by_name['instrumentation_library'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._INSTRUMENTATIONLIBRARY -_INSTRUMENTATIONLIBRARYSPANS.fields_by_name['spans'].message_type = _SPAN -_SPAN_EVENT.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -_SPAN_EVENT.containing_type = _SPAN -_SPAN_LINK.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -_SPAN_LINK.containing_type = _SPAN -_SPAN.fields_by_name['kind'].enum_type = _SPAN_SPANKIND -_SPAN.fields_by_name['attributes'].message_type = opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2._KEYVALUE -_SPAN.fields_by_name['events'].message_type = _SPAN_EVENT -_SPAN.fields_by_name['links'].message_type = _SPAN_LINK -_SPAN.fields_by_name['status'].message_type = _STATUS -_SPAN_SPANKIND.containing_type = _SPAN -_STATUS.fields_by_name['code'].enum_type = _STATUS_STATUSCODE -_STATUS_STATUSCODE.containing_type = _STATUS -DESCRIPTOR.message_types_by_name['TracesData'] = _TRACESDATA -DESCRIPTOR.message_types_by_name['ResourceSpans'] = _RESOURCESPANS -DESCRIPTOR.message_types_by_name['ScopeSpans'] = _SCOPESPANS -DESCRIPTOR.message_types_by_name['InstrumentationLibrarySpans'] = _INSTRUMENTATIONLIBRARYSPANS -DESCRIPTOR.message_types_by_name['Span'] = _SPAN -DESCRIPTOR.message_types_by_name['Status'] = _STATUS -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - +_TRACESDATA = DESCRIPTOR.message_types_by_name['TracesData'] +_RESOURCESPANS = DESCRIPTOR.message_types_by_name['ResourceSpans'] +_SCOPESPANS = DESCRIPTOR.message_types_by_name['ScopeSpans'] +_INSTRUMENTATIONLIBRARYSPANS = DESCRIPTOR.message_types_by_name['InstrumentationLibrarySpans'] +_SPAN = DESCRIPTOR.message_types_by_name['Span'] +_SPAN_EVENT = _SPAN.nested_types_by_name['Event'] +_SPAN_LINK = _SPAN.nested_types_by_name['Link'] +_STATUS = DESCRIPTOR.message_types_by_name['Status'] +_SPAN_SPANKIND = _SPAN.enum_types_by_name['SpanKind'] +_STATUS_STATUSCODE = _STATUS.enum_types_by_name['StatusCode'] TracesData = _reflection.GeneratedProtocolMessageType('TracesData', (_message.Message,), { 'DESCRIPTOR' : _TRACESDATA, '__module__' : 'opentelemetry.proto.trace.v1.trace_pb2' @@ -647,8 +88,32 @@ }) _sym_db.RegisterMessage(Status) - -DESCRIPTOR._options = None -_RESOURCESPANS.fields_by_name['instrumentation_library_spans']._options = None -_INSTRUMENTATIONLIBRARYSPANS._options = None +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z\'go.opentelemetry.io/proto/otlp/trace/v1' + _RESOURCESPANS.fields_by_name['instrumentation_library_spans']._options = None + _RESOURCESPANS.fields_by_name['instrumentation_library_spans']._serialized_options = b'\030\001' + _INSTRUMENTATIONLIBRARYSPANS._options = None + _INSTRUMENTATIONLIBRARYSPANS._serialized_options = b'\030\001' + _TRACESDATA._serialized_start=166 + _TRACESDATA._serialized_end=247 + _RESOURCESPANS._serialized_start=250 + _RESOURCESPANS._serialized_end=512 + _SCOPESPANS._serialized_start=515 + _SCOPESPANS._serialized_end=666 + _INSTRUMENTATIONLIBRARYSPANS._serialized_start=669 + _INSTRUMENTATIONLIBRARYSPANS._serialized_end=861 + _SPAN._serialized_start=864 + _SPAN._serialized_end=1862 + _SPAN_EVENT._serialized_start=1406 + _SPAN_EVENT._serialized_end=1546 + _SPAN_LINK._serialized_start=1549 + _SPAN_LINK._serialized_end=1706 + _SPAN_SPANKIND._serialized_start=1709 + _SPAN_SPANKIND._serialized_end=1862 + _STATUS._serialized_start=1865 + _STATUS._serialized_end=2039 + _STATUS_STATUSCODE._serialized_start=1955 + _STATUS_STATUSCODE._serialized_end=2033 # @@protoc_insertion_point(module_scope) diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index 7ba991ea4e..7625848f71 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -34,6 +34,8 @@ source $venv_dir/bin/activate python -m pip install \ -c $repo_root/dev-requirements.txt \ grpcio-tools mypy-protobuf +echo 'python -m grpc_tools.protoc --version' +python -m grpc_tools.protoc --version # Clone the proto repo if it doesn't exist if [ ! -d "$PROTO_REPO_DIR" ]; then @@ -64,6 +66,7 @@ python -m grpc_tools.protoc \ # generate grpc output only for protos with service definitions service_protos=$(grep -REl "service \w+ {" $PROTO_REPO_DIR/opentelemetry/) + python -m grpc_tools.protoc \ -I $PROTO_REPO_DIR \ --python_out=. \ diff --git a/tox.ini b/tox.ini index 90f8f8d94b..147931eb6b 100644 --- a/tox.ini +++ b/tox.ini @@ -8,8 +8,9 @@ envlist = py3{7,8,9,10,11}-opentelemetry-api pypy3-opentelemetry-api - py3{7,8,9,10,11}-opentelemetry-protobuf - pypy3-opentelemetry-protobuf + ; Test against both protobuf 3.x and 4.x + py3{7,8,9,10,11}-proto{3,4}-opentelemetry-protobuf + pypy3-proto{3,4}-opentelemetry-protobuf py3{7,8,9,10,11}-opentelemetry-sdk pypy3-opentelemetry-sdk @@ -37,11 +38,11 @@ envlist = py3{7,8,9,10,11}-opentelemetry-exporter-otlp-combined ; intentionally excluded from pypy3 - py3{7,8,9,10,11}-opentelemetry-exporter-otlp-proto-grpc + py3{7,8,9,10,11}-proto{3,4}-opentelemetry-exporter-otlp-proto-grpc ; intentionally excluded from pypy3 - py3{7,8,9,10,11}-opentelemetry-exporter-otlp-proto-http - pypy3-opentelemetry-exporter-otlp-proto-http + py3{7,8,9,10,11}-proto{3,4}-opentelemetry-exporter-otlp-proto-http + pypy3-opentelemetry-proto{3,4}-exporter-otlp-proto-http py3{7,8,9,10,11}-opentelemetry-exporter-prometheus pypy3-opentelemetry-exporter-prometheus @@ -67,7 +68,7 @@ envlist = tracecontext mypy,mypyinstalled docs - docker-tests + docker-tests-proto{3,4} public-symbols-check [testenv] @@ -80,6 +81,11 @@ deps = coverage: pytest-cov mypy,mypyinstalled: mypy + ; proto 3 and 4 tests install the respective version of protobuf + proto3: protobuf~=3.19.0 + proto4: protobuf~=4.0 + + setenv = ; override CONTRIB_REPO_SHA via env variable when testing other branches/commits than main ; i.e: CONTRIB_REPO_SHA=dde62cebffe519c35875af6d06fae053b3be65ec tox -e @@ -272,27 +278,36 @@ commands_pre = commands = {toxinidir}/scripts/tracecontext-integration-test.sh -[testenv:docker-tests] +[testenv:docker-tests-proto{3,4}] deps = pytest docker-compose >= 1.25.2 + ; proto 3 and 4 tests install the respective version of protobuf + proto3: protobuf~=3.19.0 + proto4: protobuf~=4.0 + changedir = tests/opentelemetry-docker-tests/tests commands_pre = + pip freeze pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-semantic-conventions \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/tests/opentelemetry-test-utils \ - -e {toxinidir}/exporter/opentelemetry-exporter-opencensus \ + ; opencensus exporter does not work with protobuf 4 + proto3: -e {toxinidir}/exporter/opentelemetry-exporter-opencensus \ -e {toxinidir}/opentelemetry-proto \ -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc \ -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http \ -e {toxinidir}/exporter/opentelemetry-exporter-otlp docker-compose up -d commands = - pytest {posargs} + proto3: pytest {posargs} + ; opencensus exporter does not work with protobuf 4 + proto4: pytest --ignore opencensus {posargs} + commands_post = docker-compose down -v From edf4d6cd07b5ea922180b52c9e737b7be0d59f9f Mon Sep 17 00:00:00 2001 From: pridhi-arora <110390842+pridhi-arora@users.noreply.github.com> Date: Tue, 6 Dec 2022 20:11:37 +0530 Subject: [PATCH 1357/1517] Adds user agent string to grpc headers (#3009) --- CHANGELOG.md | 2 ++ .../exporter/otlp/proto/grpc/__init__.py | 4 +++ .../exporter/otlp/proto/grpc/exporter.py | 7 +++++ .../tests/logs/test_otlp_logs_exporter.py | 8 ++++++ .../tests/test_otlp_metrics_exporter.py | 15 ++++++++-- .../tests/test_otlp_trace_exporter.py | 28 ++++++++++++++++--- 6 files changed, 58 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83fd52921a..17c23e4fa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3038](https://github.com/open-telemetry/opentelemetry-python/pull/3038)) - Fix: Avoid generator in metrics _ViewInstrumentMatch.collect() ([#3035](https://github.com/open-telemetry/opentelemetry-python/pull/3035) +- [exporter-otlp-proto-grpc] add user agent string + ([#3009](https://github.com/open-telemetry/opentelemetry-python/pull/3009)) ## Version 1.14.0/0.35b0 (2022-11-04) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py index 0a33b6325a..60af18de2e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py @@ -69,3 +69,7 @@ API --- """ +from .version import __version__ + +_USER_AGENT_HEADER_VALUE = "OTel OTLP Exporter Python/" + __version__ +_OTLP_GRPC_HEADERS = [("user-agent", _USER_AGENT_HEADER_VALUE)] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index d1427e41fa..c068f87d78 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -54,6 +54,9 @@ from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.metrics.export import MetricsData from opentelemetry.util.re import parse_env_headers +from opentelemetry.exporter.otlp.proto.grpc import ( + _OTLP_GRPC_HEADERS, +) logger = getLogger(__name__) SDKDataT = TypeVar("SDKDataT") @@ -251,6 +254,10 @@ def __init__( self._headers = tuple(temp_headers.items()) elif isinstance(self._headers, dict): self._headers = tuple(self._headers.items()) + if self._headers is None: + self._headers = tuple(_OTLP_GRPC_HEADERS) + else: + self._headers = self._headers + tuple(_OTLP_GRPC_HEADERS) self._timeout = timeout or int( environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 10) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index c5f0fcd92e..0861d06d65 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -26,6 +26,7 @@ OTLPLogExporter, ) from opentelemetry.exporter.otlp.proto.grpc.exporter import _translate_value +from opentelemetry.exporter.otlp.proto.grpc.version import __version__ from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, ExportLogsServiceResponse, @@ -249,6 +250,13 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): ) mock_method.reset_mock() + def test_otlp_headers_from_env(self): + # pylint: disable=protected-access + self.assertEqual( + self.exporter._headers, + (("user-agent", "OTel OTLP Exporter Python/" + __version__),), + ) + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py index 4f0349257d..6436661a98 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py @@ -26,6 +26,7 @@ from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( OTLPMetricExporter, ) +from opentelemetry.exporter.otlp.proto.grpc.version import __version__ from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, ExportMetricsServiceResponse, @@ -413,14 +414,24 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): exporter = OTLPMetricExporter() # pylint: disable=protected-access self.assertEqual( - exporter._headers, (("key1", "value1"), ("key2", "VALUE=2")) + exporter._headers, + ( + ("key1", "value1"), + ("key2", "VALUE=2"), + ("user-agent", "OTel OTLP Exporter Python/" + __version__), + ), ) exporter = OTLPMetricExporter( headers=(("key3", "value3"), ("key4", "value4")) ) # pylint: disable=protected-access self.assertEqual( - exporter._headers, (("key3", "value3"), ("key4", "value4")) + exporter._headers, + ( + ("key3", "value3"), + ("key4", "value4"), + ("user-agent", "OTel OTLP Exporter Python/" + __version__), + ), ) @patch.dict( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index cfb286edee..89cf7f6639 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -29,6 +29,7 @@ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) +from opentelemetry.exporter.otlp.proto.grpc.version import __version__ from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest, ExportTraceServiceResponse, @@ -275,21 +276,36 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): exporter = OTLPSpanExporter() # pylint: disable=protected-access self.assertEqual( - exporter._headers, (("key1", "value1"), ("key2", "VALUE=2")) + exporter._headers, + ( + ("key1", "value1"), + ("key2", "VALUE=2"), + ("user-agent", "OTel OTLP Exporter Python/" + __version__), + ), ) exporter = OTLPSpanExporter( headers=(("key3", "value3"), ("key4", "value4")) ) # pylint: disable=protected-access self.assertEqual( - exporter._headers, (("key3", "value3"), ("key4", "value4")) + exporter._headers, + ( + ("key3", "value3"), + ("key4", "value4"), + ("user-agent", "OTel OTLP Exporter Python/" + __version__), + ), ) exporter = OTLPSpanExporter( headers={"key5": "value5", "key6": "value6"} ) # pylint: disable=protected-access self.assertEqual( - exporter._headers, (("key5", "value5"), ("key6", "value6")) + exporter._headers, + ( + ("key5", "value5"), + ("key6", "value6"), + ("user-agent", "OTel OTLP Exporter Python/" + __version__), + ), ) @patch.dict( @@ -434,7 +450,11 @@ def test_otlp_exporter_otlp_compression_precendence( def test_otlp_headers(self, mock_ssl_channel, mock_secure): exporter = OTLPSpanExporter() # pylint: disable=protected-access - self.assertIsNone(exporter._headers, None) + # This ensures that there is no other header than standard user-agent. + self.assertEqual( + exporter._headers, + (("user-agent", "OTel OTLP Exporter Python/" + __version__),), + ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.backoff") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") From 66f3b0b41f60f73e6f85151afc86e9e66422c025 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 9 Dec 2022 14:05:20 -0600 Subject: [PATCH 1358/1517] Fix tox version for CI (#3081) Fixes #3079 --- .github/workflows/public-api-check.yml | 2 +- .github/workflows/test.yml | 6 +++--- CONTRIBUTING.md | 6 ++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/public-api-check.yml b/.github/workflows/public-api-check.yml index 5d85ef3f89..eead4fa368 100644 --- a/.github/workflows/public-api-check.yml +++ b/.github/workflows/public-api-check.yml @@ -34,7 +34,7 @@ jobs: python-version: 3.9 - name: Install tox - run: pip install -U tox-factor + run: pip install tox==3.27.1 -U tox-factor - name: Public API Check run: tox -e public-symbols-check diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cda3a2c57d..8cb0527798 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,7 +46,7 @@ jobs: python-version: ${{ env[matrix.python-version] }} architecture: 'x64' - name: Install tox - run: pip install -U tox-factor + run: pip install tox==3.27.1 -U tox-factor - name: Cache tox environment # Preserves .tox directory between runs for faster installs uses: actions/cache@v2 @@ -99,7 +99,7 @@ jobs: python-version: 3.9 architecture: 'x64' - name: Install tox - run: pip install -U tox + run: pip install tox==3.27.1 - name: Cache tox environment # Preserves .tox directory between runs for faster installs uses: actions/cache@v2 @@ -142,7 +142,7 @@ jobs: python-version: ${{ env[matrix.python-version] }} architecture: 'x64' - name: Install tox - run: pip install -U tox-factor + run: pip install tox==3.27.1 -U tox-factor - name: Cache tox environment # Preserves .tox directory between runs for faster installs uses: actions/cache@v2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0653deb60c..e97e9daff2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,12 +47,14 @@ during their normal contribution hours. This project uses [tox](https://tox.readthedocs.io) to automate some aspects of development, including testing against multiple Python versions. -To install `tox`, run: +To install `tox`, run[^1]: ```console -$ pip install tox +$ pip install tox==3.27.1 ``` +[^1]: Right now we are experiencing issues with `tox==4.x.y`, so we recommend you use this version. + You can run `tox` with the following arguments: - `tox` to run all existing tox commands, including unit tests for all packages From 0f9cfdd3b60914e6779aa6d09a58915565d89389 Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Fri, 9 Dec 2022 14:19:16 -0800 Subject: [PATCH 1359/1517] Update version to 1.16.0.dev/0.37b0.dev (#3084) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-grpc/pyproject.toml | 2 +- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-http/pyproject.toml | 2 +- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/pyproject.toml | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/pyproject.toml | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/pyproject.toml | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 31 files changed, 38 insertions(+), 36 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8cb0527798..02395ca73c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: main + CONTRIB_REPO_SHA: ce5dac763790203d68217cd3870b6be5517692fe # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 17c23e4fa6..e0f43a10da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## Version 1.15.0/0.36b0 (2022-12-09) + - Regenerate opentelemetry-proto to be compatible with protobuf 3 and 4 ([#3070](https://github.com/open-telemetry/opentelemetry-python/pull/3070)) - Rename parse_headers to parse_env_headers and improve error message diff --git a/eachdist.ini b/eachdist.ini index 40c6db3c18..6606776ecb 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.15.0.dev +version=1.16.0.dev packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.36b0.dev +version=0.37b0.dev packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 65fcc50d7d..4209aaad38 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 65fcc50d7d..4209aaad38 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger/pyproject.toml b/exporter/opentelemetry-exporter-jaeger/pyproject.toml index 622e90d4ba..c595f817f3 100644 --- a/exporter/opentelemetry-exporter-jaeger/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-jaeger-proto-grpc == 1.15.0.dev", - "opentelemetry-exporter-jaeger-thrift == 1.15.0.dev", + "opentelemetry-exporter-jaeger-proto-grpc == 1.16.0.dev", + "opentelemetry-exporter-jaeger-thrift == 1.16.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 65fcc50d7d..4209aaad38 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index fa69afa640..0a47df4f5a 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.36b0.dev" +__version__ = "0.37b0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index 6c8d44ecec..1daa274423 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "googleapis-common-protos ~= 1.52", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.12", - "opentelemetry-proto == 1.15.0.dev", + "opentelemetry-proto == 1.16.0.dev", "opentelemetry-sdk ~= 1.12", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 0fffbf9870..0b4a18e0a2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index a29fa8a682..680fb8b76d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.12", - "opentelemetry-proto == 1.15.0.dev", + "opentelemetry-proto == 1.16.0.dev", "opentelemetry-sdk ~= 1.12", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 0fffbf9870..0b4a18e0a2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index 1e61767eb1..ab605eb3e9 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-otlp-proto-grpc == 1.15.0.dev", - "opentelemetry-exporter-otlp-proto-http == 1.15.0.dev", + "opentelemetry-exporter-otlp-proto-grpc == 1.16.0.dev", + "opentelemetry-exporter-otlp-proto-http == 1.16.0.dev", ] [project.entry-points.opentelemetry_logs_exporter] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 0fffbf9870..0b4a18e0a2 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index fa69afa640..0a47df4f5a 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.36b0.dev" +__version__ = "0.37b0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 0fffbf9870..0b4a18e0a2 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml index a9268258e7..1c4137c929 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.3", - "opentelemetry-exporter-zipkin-json == 1.15.0.dev", + "opentelemetry-exporter-zipkin-json == 1.16.0.dev", "opentelemetry-sdk ~= 1.11", "protobuf ~= 3.12", "requests ~= 2.7", diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 0fffbf9870..0b4a18e0a2 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin/pyproject.toml b/exporter/opentelemetry-exporter-zipkin/pyproject.toml index 9f7f5e8274..d982aaf896 100644 --- a/exporter/opentelemetry-exporter-zipkin/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-zipkin-json == 1.15.0.dev", - "opentelemetry-exporter-zipkin-proto-http == 1.15.0.dev", + "opentelemetry-exporter-zipkin-json == 1.16.0.dev", + "opentelemetry-exporter-zipkin-proto-http == 1.16.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 0fffbf9870..0b4a18e0a2 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 0fffbf9870..0b4a18e0a2 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 0fffbf9870..0b4a18e0a2 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index 88882d0164..4798f4fbc6 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-api == 1.15.0.dev", - "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-api == 1.16.0.dev", + "opentelemetry-semantic-conventions == 0.37b0.dev", "setuptools >= 16.0", "typing-extensions >= 3.7.4", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 0fffbf9870..0b4a18e0a2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index fa69afa640..0a47df4f5a 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.36b0.dev" +__version__ = "0.37b0.dev" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 0fffbf9870..0b4a18e0a2 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 0fffbf9870..0b4a18e0a2 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0.dev" +__version__ = "1.16.0.dev" diff --git a/shim/opentelemetry-opentracing-shim/pyproject.toml b/shim/opentelemetry-opentracing-shim/pyproject.toml index c0e1a8cfe2..d831379655 100644 --- a/shim/opentelemetry-opentracing-shim/pyproject.toml +++ b/shim/opentelemetry-opentracing-shim/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.36b0.dev", + "opentelemetry-test-utils == 0.37b0.dev", "opentracing ~= 2.2.0", ] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index fa69afa640..0a47df4f5a 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.36b0.dev" +__version__ = "0.37b0.dev" diff --git a/tests/opentelemetry-test-utils/pyproject.toml b/tests/opentelemetry-test-utils/pyproject.toml index f280dff89f..b9f84602a2 100644 --- a/tests/opentelemetry-test-utils/pyproject.toml +++ b/tests/opentelemetry-test-utils/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ ] dependencies = [ "asgiref ~= 3.0", - "opentelemetry-api == 1.15.0.dev", - "opentelemetry-sdk == 1.15.0.dev", + "opentelemetry-api == 1.16.0.dev", + "opentelemetry-sdk == 1.16.0.dev", ] [project.optional-dependencies] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 5ea2af4ddd..6b7ff9a946 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.36b0.dev" +__version__ = "0.37b0.dev" From d646029ed402c8bdbb2adb8b196d300febb1acf3 Mon Sep 17 00:00:00 2001 From: Vasi Vasireddy <41936996+vasireddy99@users.noreply.github.com> Date: Sun, 1 Jan 2023 10:15:23 -0800 Subject: [PATCH 1360/1517] Update the usage of set-output command in GH actions (#3045) --- .github/workflows/check-links.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index cbf945b682..f298f6d291 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -19,7 +19,7 @@ jobs: - name: Get changed files id: changes run: | - echo "::set-output name=md::$(git diff --name-only --diff-filter=ACMRTUXB $(git merge-base origin/main ${{ github.event.pull_request.head.sha }}) ${{ github.event.pull_request.head.sha }} | grep .md$ | xargs)" + echo "md=$(git diff --name-only --diff-filter=ACMRTUXB $(git merge-base origin/main ${{ github.event.pull_request.head.sha }}) ${{ github.event.pull_request.head.sha }} | grep .md$ | xargs)" >> $GITHUB_OUTPUT check-links: runs-on: ubuntu-latest needs: changedfiles diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 02395ca73c..b138e95482 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,7 +66,7 @@ jobs: # jq -s '.[0].benchmarks = ([.[].benchmarks] | add) # | if .[0].benchmarks == null then null else .[0] end' # $(find . -name '*${{ matrix.package }}*-benchmark.json') > output.json -# && echo "::set-output name=json_plaintext::$(cat output.json)" +# && echo "json_plaintext=$(cat output.json)" >> $GITHUB_OUTPUT # - name: Report on benchmark results # if: steps.find_and_merge_benchmarks.outputs.json_plaintext != 'null' # uses: rhysd/github-action-benchmark@v1 From b184dc95844561f0dce3f5c0a496bc89d0fb9a6d Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Sun, 1 Jan 2023 21:45:07 -0500 Subject: [PATCH 1361/1517] Fix tests with pre-2.0 backoff (fix #3087) (#3106) --- .../tests/test_otlp_trace_exporter.py | 4 +++- .../tests/metrics/test_otlp_metrics_exporter.py | 4 +++- .../tests/test_proto_log_exporter.py | 4 +++- .../tests/test_proto_span_exporter.py | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 89cf7f6639..5c8f040725 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -24,6 +24,7 @@ from opentelemetry.attributes import BoundedAttributes from opentelemetry.exporter.otlp.proto.grpc.exporter import ( + _is_backoff_v2, _translate_key_values, ) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( @@ -461,7 +462,8 @@ def test_otlp_headers(self, mock_ssl_channel, mock_secure): def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff): # In backoff ~= 2.0.0 the first value yielded from expo is None. def generate_delays(*args, **kwargs): - yield None + if _is_backoff_v2: + yield None yield 1 mock_backoff.expo.configure_mock(**{"side_effect": generate_delays}) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index 99f9ea12d3..45bc0c6faa 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -25,6 +25,7 @@ DEFAULT_METRICS_EXPORT_PATH, DEFAULT_TIMEOUT, OTLPMetricExporter, + _is_backoff_v2, ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, @@ -281,7 +282,8 @@ def test_serialization(self, mock_post): def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff): # In backoff ~= 2.0.0 the first value yielded from expo is None. def generate_delays(*args, **kwargs): - yield None + if _is_backoff_v2: + yield None yield 1 mock_backoff.expo.configure_mock(**{"side_effect": generate_delays}) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index 7dcb82030d..b273a618cf 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -29,6 +29,7 @@ DEFAULT_LOGS_EXPORT_PATH, DEFAULT_TIMEOUT, OTLPLogExporter, + _is_backoff_v2, ) from opentelemetry.exporter.otlp.proto.http._log_exporter.encoder import ( _encode_attributes, @@ -166,7 +167,8 @@ def test_serialize(self): def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff): # In backoff ~= 2.0.0 the first value yielded from expo is None. def generate_delays(*args, **kwargs): - yield None + if _is_backoff_v2: + yield None yield 1 mock_backoff.expo.configure_mock(**{"side_effect": generate_delays}) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index 73d08e08c8..64ca6fbbb8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -26,6 +26,7 @@ DEFAULT_TIMEOUT, DEFAULT_TRACES_EXPORT_PATH, OTLPSpanExporter, + _is_backoff_v2, ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, @@ -203,7 +204,8 @@ def test_headers_parse_from_env(self): def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff): # In backoff ~= 2.0.0 the first value yielded from expo is None. def generate_delays(*args, **kwargs): - yield None + if _is_backoff_v2: + yield None yield 1 mock_backoff.expo.configure_mock(**{"side_effect": generate_delays}) From af83ef151fae90a2bd96224add771e9bb20c33df Mon Sep 17 00:00:00 2001 From: Hannah Date: Fri, 6 Jan 2023 10:12:54 -0500 Subject: [PATCH 1362/1517] Fixes #3088 updated import library for set_logger_provider (#3113) --- docs/examples/logs/example.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/examples/logs/example.py b/docs/examples/logs/example.py index b2b1593262..2505aacea7 100644 --- a/docs/examples/logs/example.py +++ b/docs/examples/logs/example.py @@ -1,14 +1,11 @@ import logging from opentelemetry import trace +from opentelemetry._logs import set_logger_provider from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter, ) -from opentelemetry.sdk._logs import ( - LoggerProvider, - LoggingHandler, - set_logger_provider, -) +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler from opentelemetry.sdk._logs.export import BatchLogRecordProcessor from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider From 2fa6210d162f2be2281b99573314ef41280b9d4b Mon Sep 17 00:00:00 2001 From: Hsuan-Ting Lu Date: Tue, 10 Jan 2023 13:08:19 -0500 Subject: [PATCH 1363/1517] Fix bug in example (#3111) --- opentelemetry-api/src/opentelemetry/trace/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 6f63eddc33..1b60eb2ca9 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -352,7 +352,7 @@ def start_as_current_span( with tracer.start_as_current_span("one") as parent: parent.add_event("parent's event") - with trace.start_as_current_span("two") as child: + with tracer.start_as_current_span("two") as child: child.add_event("child's event") trace.get_current_span() # returns child trace.get_current_span() # returns parent From 665bd48b240bc4b8c73b738bf8a729f1cb20a0d1 Mon Sep 17 00:00:00 2001 From: pridhi-arora <110390842+pridhi-arora@users.noreply.github.com> Date: Wed, 11 Jan 2023 22:58:46 +0530 Subject: [PATCH 1364/1517] Adds environment variables for log exporter (#3037) --- CHANGELOG.md | 2 + .../pyproject.toml | 2 +- .../otlp/proto/grpc/_log_exporter/__init__.py | 41 ++++++++++++++- .../tests/logs/test_otlp_logs_exporter.py | 39 ++++++++++++++- .../pyproject.toml | 2 +- .../otlp/proto/http/_log_exporter/__init__.py | 32 +++++++++--- .../tests/test_proto_log_exporter.py | 37 ++++++++++++++ .../sdk/environment_variables.py | 50 +++++++++++++++++++ 8 files changed, 194 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0f43a10da..460c439e73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +- Adds environment variables for log exporter + ([#3037](https://github.com/open-telemetry/opentelemetry-python/pull/3037)) ## Version 1.15.0/0.36b0 (2022-12-09) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index 1daa274423..8a07a45bb6 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.12", "opentelemetry-proto == 1.16.0.dev", - "opentelemetry-sdk ~= 1.12", + "opentelemetry-sdk ~= 1.16.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index 489cf35c37..f7cbadfec8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -11,12 +11,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +from os import environ from typing import Optional, Sequence from grpc import ChannelCredentials, Compression from opentelemetry.exporter.otlp.proto.grpc.exporter import ( OTLPExporterMixin, get_resource_data, + _get_credentials, _translate_value, + environ_to_compression, ) from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, @@ -34,6 +37,15 @@ from opentelemetry.sdk._logs import LogData from opentelemetry.sdk._logs.export import LogExporter, LogExportResult +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_INSECURE, + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, +) + class OTLPLogExporter( LogExporter, @@ -52,13 +64,40 @@ def __init__( timeout: Optional[int] = None, compression: Optional[Compression] = None, ): + if insecure is None: + insecure = environ.get(OTEL_EXPORTER_OTLP_LOGS_INSECURE) + if insecure is not None: + insecure = insecure.lower() == "true" + + if ( + not insecure + and environ.get(OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE) is not None + ): + credentials = _get_credentials( + credentials, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE + ) + + environ_timeout = environ.get(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT) + environ_timeout = ( + int(environ_timeout) if environ_timeout is not None else None + ) + + compression = ( + environ_to_compression(OTEL_EXPORTER_OTLP_LOGS_COMPRESSION) + if compression is None + else compression + ) + endpoint = endpoint or environ.get(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) + + headers = headers or environ.get(OTEL_EXPORTER_OTLP_LOGS_HEADERS) + super().__init__( **{ "endpoint": endpoint, "insecure": insecure, "credentials": credentials, "headers": headers, - "timeout": timeout, + "timeout": timeout or environ_timeout, "compression": compression, } ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index 0861d06d65..c7996b9bf9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -14,12 +14,13 @@ import time from concurrent.futures import ThreadPoolExecutor +from os.path import dirname from unittest import TestCase from unittest.mock import patch from google.protobuf.duration_pb2 import Duration from google.rpc.error_details_pb2 import RetryInfo -from grpc import StatusCode, server +from grpc import ChannelCredentials, Compression, StatusCode, server from opentelemetry._logs import SeverityNumber from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( @@ -47,10 +48,19 @@ ) from opentelemetry.sdk._logs import LogData, LogRecord from opentelemetry.sdk._logs.export import LogExportResult +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, +) from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import TraceFlags +THIS_DIR = dirname(__file__) + class LogsServiceServicerUNAVAILABLEDelay(LogsServiceServicer): # pylint: disable=invalid-name,unused-argument,no-self-use @@ -100,7 +110,6 @@ def Export(self, request, context): class TestOTLPLogExporter(TestCase): def setUp(self): - self.exporter = OTLPLogExporter() self.server = server(ThreadPoolExecutor(max_workers=10)) @@ -164,6 +173,32 @@ def test_exporting(self): # pylint: disable=protected-access self.assertEqual(self.exporter._exporting, "logs") + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317", + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: THIS_DIR + + "/../fixtures/test.cert", + OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2", + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10", + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip", + }, + ) + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__" + ) + def test_env_variables(self, mock_exporter_mixin): + OTLPLogExporter() + + self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) + _, kwargs = mock_exporter_mixin.call_args_list[0] + self.assertEqual(kwargs["endpoint"], "logs:4317") + self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = VALUE=2") + self.assertEqual(kwargs["timeout"], 10) + self.assertEqual(kwargs["compression"], Compression.Gzip) + self.assertIsNotNone(kwargs["credentials"]) + self.assertIsInstance(kwargs["credentials"], ChannelCredentials) + @patch( "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index 680fb8b76d..aec6afe1fc 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.12", "opentelemetry-proto == 1.16.0.dev", - "opentelemetry-sdk ~= 1.12", + "opentelemetry-sdk ~= 1.16.0.dev", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 49daa29e8a..dfd70180e0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -29,6 +29,11 @@ OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, ) from opentelemetry.sdk._logs import LogData from opentelemetry.sdk._logs.export import ( @@ -79,16 +84,26 @@ def __init__( compression: Optional[Compression] = None, session: Optional[requests.Session] = None, ): - self._endpoint = endpoint or _append_logs_path( - environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) + self._endpoint = endpoint or environ.get( + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + _append_logs_path( + environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) + ), ) self._certificate_file = certificate_file or environ.get( - OTEL_EXPORTER_OTLP_CERTIFICATE, True + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), + ) + headers_string = environ.get( + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), ) - headers_string = environ.get(OTEL_EXPORTER_OTLP_HEADERS, "") self._headers = headers or parse_env_headers(headers_string) self._timeout = timeout or int( - environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT) + environ.get( + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), + ) ) self._compression = compression or _compression_from_env() self._session = session or requests.Session() @@ -170,7 +185,12 @@ def shutdown(self): def _compression_from_env() -> Compression: compression = ( - environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none").lower().strip() + environ.get( + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), + ) + .lower() + .strip() ) return Compression(compression) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index b273a618cf..f61a4291ed 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -61,6 +61,11 @@ OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, OTEL_EXPORTER_OTLP_TIMEOUT, ) from opentelemetry.sdk.resources import Resource as SDKResource @@ -92,6 +97,38 @@ def test_constructor_default(self): "application/x-protobuf", ) + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: ENV_TIMEOUT, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: "logs/certificate.env", + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: Compression.Deflate.value, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "https://logs.endpoint.env", + OTEL_EXPORTER_OTLP_LOGS_HEADERS: "logsEnv1=val1,logsEnv2=val2,logsEnv3===val3==", + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "40", + }, + ) + def test_exporter_metrics_env_take_priority(self): + exporter = OTLPLogExporter() + + self.assertEqual(exporter._endpoint, "https://logs.endpoint.env") + self.assertEqual(exporter._certificate_file, "logs/certificate.env") + self.assertEqual(exporter._timeout, 40) + self.assertIs(exporter._compression, Compression.Deflate) + self.assertEqual( + exporter._headers, + { + "logsenv1": "val1", + "logsenv2": "val2", + "logsenv3": "==val3==", + }, + ) + self.assertIsInstance(exporter._session, requests.Session) + @patch.dict( "os.environ", { diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 562863156f..376fb187dc 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -344,6 +344,15 @@ A scheme of https indicates a secure connection and takes precedence over this configuration setting. """ +OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT" +""" +.. envvar:: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT + +The :envvar:`OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` target to which the log exporter is going to send logs. +The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path. +A scheme of https indicates a secure connection and takes precedence over this configuration setting. +""" + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE = "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE @@ -378,6 +387,14 @@ associated with gRPC or HTTP requests. """ +OTEL_EXPORTER_OTLP_LOGS_HEADERS = "OTEL_EXPORTER_OTLP_LOGS_HEADERS" +""" +.. envvar:: OTEL_EXPORTER_OTLP_LOGS_HEADERS + +The :envvar:`OTEL_EXPORTER_OTLP_LOGS_HEADERS` contains the key-value pairs to be used as headers for logs +associated with gRPC or HTTP requests. +""" + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION = "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_COMPRESSION @@ -396,6 +413,14 @@ exporter. If both are present, this takes higher precedence. """ +OTEL_EXPORTER_OTLP_LOGS_COMPRESSION = "OTEL_EXPORTER_OTLP_LOGS_COMPRESSION" +""" +.. envvar:: OTEL_EXPORTER_OTLP_LOGS_COMPRESSION + +Same as :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` but only for the log +exporter. If both are present, this takes higher precedence. +""" + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_TIMEOUT @@ -421,6 +446,15 @@ Default: False """ +OTEL_EXPORTER_OTLP_LOGS_INSECURE = "OTEL_EXPORTER_OTLP_LOGS_INSECURE" +""" +.. envvar:: OTEL_EXPORTER_OTLP_LOGS_INSECURE + +The :envvar:`OTEL_EXPORTER_OTLP_LOGS_INSECURE` represents whether to enable client transport security +for gRPC requests for metrics. A scheme of https takes precedence over the this configuration setting. +Default: False +""" + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT @@ -440,6 +474,14 @@ TLS credentials of gRPC client for traces. Should only be used for a secure connection for tracing. """ +OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE = "OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE" +""" +.. envvar:: OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE + +The :envvar:`OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE` stores the path to the certificate file for +TLS credentials of gRPC client for traces. Should only be used for a secure connection for tracing. +""" + OTEL_EXPORTER_OTLP_METRICS_HEADERS = "OTEL_EXPORTER_OTLP_METRICS_HEADERS" """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_HEADERS @@ -456,6 +498,14 @@ wait for each batch export for metrics. """ +OTEL_EXPORTER_OTLP_LOGS_TIMEOUT = "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT" +""" +.. envvar:: OTEL_EXPORTER_OTLP_LOGS_TIMEOUT + +The :envvar:`OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` is the maximum time the OTLP exporter will +wait for each batch export for logs. +""" + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION = ( "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION" ) From f5fb6b1353929cf8039b1d38f97450866357d901 Mon Sep 17 00:00:00 2001 From: Shalev Roda <65566801+shalevr@users.noreply.github.com> Date: Thu, 12 Jan 2023 00:32:33 +0200 Subject: [PATCH 1365/1517] Add db metric name to semantic conventions (#3115) --- CHANGELOG.md | 3 +++ .../src/opentelemetry/semconv/metrics/__init__.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 460c439e73..919d382c1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adds environment variables for log exporter ([#3037](https://github.com/open-telemetry/opentelemetry-python/pull/3037)) +- Add db metric name to semantic conventions + ([#3115](https://github.com/open-telemetry/opentelemetry-python/pull/3115)) + ## Version 1.15.0/0.36b0 (2022-12-09) - Regenerate opentelemetry-proto to be compatible with protobuf 3 and 4 diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/__init__.py index 8ff6720165..e7a03cf818 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/__init__.py @@ -30,3 +30,5 @@ class MetricInstruments: HTTP_CLIENT_REQUEST_SIZE = "http.client.request.size" HTTP_CLIENT_RESPONSE_SIZE = "http.client.response.size" + + DB_CLIENT_CONNECTIONS_USAGE = "db.client.connections.usage" From 3ff277ec9168df83cecbabddbed934c3512f0060 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 12 Jan 2023 11:33:20 -0800 Subject: [PATCH 1366/1517] Fix requirements file for example (#3126) --- docs/examples/metrics/reader/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/examples/metrics/reader/requirements.txt b/docs/examples/metrics/reader/requirements.txt index be61271135..2ccffaf392 100644 --- a/docs/examples/metrics/reader/requirements.txt +++ b/docs/examples/metrics/reader/requirements.txt @@ -1,6 +1,6 @@ Deprecated==1.2.13 -opentelemetry-api==1.12.0 -opentelemetry-sdk==1.12.0 -opentelemetry-semantic-conventions==0.33b0 +opentelemetry-api==1.15.0 +opentelemetry-sdk==1.15.0 +opentelemetry-semantic-conventions==0.36b0 typing_extensions==4.3.0 wrapt==1.14.1 From 090efb48b6cd9eb46609335b0a990bac42d854d6 Mon Sep 17 00:00:00 2001 From: Ali Ebrahim Date: Thu, 12 Jan 2023 14:43:11 -0800 Subject: [PATCH 1367/1517] Add attribute name to type warning message. (#3124) --- CHANGELOG.md | 3 ++- opentelemetry-api/src/opentelemetry/attributes/__init__.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 919d382c1c..c6f6a1e91e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - Adds environment variables for log exporter ([#3037](https://github.com/open-telemetry/opentelemetry-python/pull/3037)) - +- Add attribute name to type warning message. + ([3124](https://github.com/open-telemetry/opentelemetry-python/pull/3124)) - Add db metric name to semantic conventions ([#3115](https://github.com/open-telemetry/opentelemetry-python/pull/3115)) diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 20d9ae91e1..0b7d056da9 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -97,9 +97,10 @@ def _clean_attribute( return tuple(cleaned_seq) _logger.warning( - "Invalid type %s for attribute value. Expected one of %s or a " + "Invalid type %s for attribute '%s' value. Expected one of %s or a " "sequence of those types", type(value).__name__, + key, [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES], ) return None From 5a996fd9a4334c8f086b3cbea096f8feec6c15a2 Mon Sep 17 00:00:00 2001 From: David Richards <50757879+drichards188@users.noreply.github.com> Date: Thu, 12 Jan 2023 19:12:07 -0700 Subject: [PATCH 1368/1517] fixed all instances of @tracer.start_as_current_span("name"): to @tracer.start_as_current_span("name") as decorators do not have colons (#3127) --- opentelemetry-api/src/opentelemetry/trace/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 1b60eb2ca9..f0e92f14bb 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -373,7 +373,7 @@ def start_as_current_span( This can also be used as a decorator:: - @tracer.start_as_current_span("name"): + @tracer.start_as_current_span("name") def function(): ... From f879d38796f9a209af29f6e7383ba075398922f1 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 18 Jan 2023 22:55:43 +0530 Subject: [PATCH 1369/1517] Add script & workflow to update contrib repo SHA from branch (#3117) --- .github/workflows/test.yml | 153 ++++++++++++++++--------------- .github/workflows/update-sha.yml | 50 ++++++++++ dev-requirements.txt | 2 + scripts/update_sha.py | 58 ++++++++++++ 4 files changed, 190 insertions(+), 73 deletions(-) create mode 100644 .github/workflows/update-sha.yml create mode 100644 scripts/update_sha.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b138e95482..e0951b7afd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: ce5dac763790203d68217cd3870b6be5517692fe + CONTRIB_REPO_SHA: 1f0dda9865b3c83e6dea2f2b127a2b0971853543 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when @@ -29,37 +29,41 @@ jobs: py310: "3.10" py311: "3.11" pypy3: pypy-3.7 - RUN_MATRIX_COMBINATION: ${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }} + RUN_MATRIX_COMBINATION: ${{ matrix.python-version }}-${{ matrix.package }}-${{ + matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [ py37, py38, py39, py310, py311, pypy3 ] - package: ["api", "sdk", "semantic", "getting", "shim", "exporter", "protobuf", "propagator"] - os: [ ubuntu-20.04, windows-2019 ] + python-version: [py37, py38, py39, py310, py311, pypy3] + package: ["api", "sdk", "semantic", "getting", "shim", "exporter", "protobuf", + "propagator"] + os: [ubuntu-20.04, windows-2019] steps: - - name: Checkout Core Repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v2 - - name: Set up Python ${{ env[matrix.python-version] }} - uses: actions/setup-python@v2 - with: - python-version: ${{ env[matrix.python-version] }} - architecture: 'x64' - - name: Install tox - run: pip install tox==3.27.1 -U tox-factor - - name: Cache tox environment + - name: Checkout Core Repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v2 + - name: Set up Python ${{ env[matrix.python-version] }} + uses: actions/setup-python@v2 + with: + python-version: ${{ env[matrix.python-version] }} + architecture: 'x64' + - name: Install tox + run: pip install tox==3.27.1 -U tox-factor + - name: Cache tox environment # Preserves .tox directory between runs for faster installs - uses: actions/cache@v2 - with: - path: | - .tox - ~/.cache/pip - key: v3-tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core - - name: Windows does not let git check out files with long names - if: ${{ matrix.os == 'windows-2019'}} - run: git config --system core.longpaths true - - name: run tox - run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json + uses: actions/cache@v2 + with: + path: | + .tox + ~/.cache/pip + key: v3-tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', + 'dev-requirements.txt') }}-core + - name: Windows does not let git check out files with long names + if: ${{ matrix.os == 'windows-2019'}} + run: git config --system core.longpaths true + - name: run tox + run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- --benchmark-json=${{ + env.RUN_MATRIX_COMBINATION }}-benchmark.json # - name: Find and merge benchmarks # id: find_and_merge_benchmarks # run: >- @@ -87,29 +91,31 @@ jobs: strategy: fail-fast: false matrix: - tox-environment: [ "docker-tests-proto3", "docker-tests-proto4", "lint", "spellcheck", "docs", "mypy", "mypyinstalled", "tracecontext" ] + tox-environment: ["docker-tests-proto3", "docker-tests-proto4", "lint", "spellcheck", + "docs", "mypy", "mypyinstalled", "tracecontext"] name: ${{ matrix.tox-environment }} runs-on: ubuntu-20.04 steps: - - name: Checkout Core Repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - architecture: 'x64' - - name: Install tox - run: pip install tox==3.27.1 - - name: Cache tox environment + - name: Checkout Core Repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + architecture: 'x64' + - name: Install tox + run: pip install tox==3.27.1 + - name: Cache tox environment # Preserves .tox directory between runs for faster installs - uses: actions/cache@v2 - with: - path: | - .tox - ~/.cache/pip - key: v3-tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core - - name: run tox - run: tox -e ${{ matrix.tox-environment }} + uses: actions/cache@v2 + with: + path: | + .tox + ~/.cache/pip + key: v3-tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') + }}-core + - name: run tox + run: tox -e ${{ matrix.tox-environment }} # Contrib unit test suite in order to ensure changes in core do not break anything in contrib. # We only run contrib unit tests on the oldest supported Python version (3.7) as running the same tests @@ -122,34 +128,35 @@ jobs: strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [ py37 ] + python-version: [py37] package: ["instrumentation", "exporter"] - os: [ ubuntu-20.04] + os: [ubuntu-20.04] steps: - - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} - uses: actions/checkout@v2 - with: - repository: open-telemetry/opentelemetry-python-contrib - ref: ${{ env.CONTRIB_REPO_SHA }} - - name: Checkout Core Repo @ SHA ${{ github.sha }} - uses: actions/checkout@v2 - with: - repository: open-telemetry/opentelemetry-python - path: opentelemetry-python-core - - name: Set up Python ${{ env[matrix.python-version] }} - uses: actions/setup-python@v2 - with: - python-version: ${{ env[matrix.python-version] }} - architecture: 'x64' - - name: Install tox - run: pip install tox==3.27.1 -U tox-factor - - name: Cache tox environment + - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} + uses: actions/checkout@v2 + with: + repository: open-telemetry/opentelemetry-python-contrib + ref: ${{ env.CONTRIB_REPO_SHA }} + - name: Checkout Core Repo @ SHA ${{ github.sha }} + uses: actions/checkout@v2 + with: + repository: open-telemetry/opentelemetry-python + path: opentelemetry-python-core + - name: Set up Python ${{ env[matrix.python-version] }} + uses: actions/setup-python@v2 + with: + python-version: ${{ env[matrix.python-version] }} + architecture: 'x64' + - name: Install tox + run: pip install tox==3.27.1 -U tox-factor + - name: Cache tox environment # Preserves .tox directory between runs for faster installs - uses: actions/cache@v2 - with: - path: | - .tox - ~/.cache/pip - key: v3-tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib - - name: run tox - run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} + uses: actions/cache@v2 + with: + path: | + .tox + ~/.cache/pip + key: v3-tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os + }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib + - name: run tox + run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} diff --git a/.github/workflows/update-sha.yml b/.github/workflows/update-sha.yml new file mode 100644 index 0000000000..0a983a7e80 --- /dev/null +++ b/.github/workflows/update-sha.yml @@ -0,0 +1,50 @@ +name: Update SHA + +on: issue_comment + +jobs: + update-pr-sha: + name: Update SHA + if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, 'update-sha') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + token: ${{ secrets.BOT_TOKEN }} + + # GitHub doesn't support string split expression + - name: Extract branch name from comment to get commit SHA + uses: jungwinter/split@v2 + id: split + with: + msg: ${{ github.event.comment.body }} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - name: Checkout Pull Request + env: + GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + run: | + PR_URL="${{ github.event.issue.pull_request.url }}" + PR_NUM=${PR_URL##*/} + echo "Checking out from PR #$PR_NUM based on URL: $PR_URL" + hub pr checkout $PR_NUM + + # caching is not supported for this event type + - name: Run script + run: | + python -m pip install requests==2.28.1 ruamel.yaml==0.17.21 + python scripts/update_sha.py --branch ${{ steps.split.outputs._1 }} + + - name: Commit and Push changes + env: + GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + run: | + git commit -a -m "Update SHA" + git push diff --git a/dev-requirements.txt b/dev-requirements.txt index cb17b50040..ff7379256a 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -22,3 +22,5 @@ mypy-protobuf~=3.0.0 markupsafe==2.0.1 bleach==4.1.0 # This dependency was updated to a breaking version. codespell==2.1.0 +requests==2.28.1 +ruamel.yaml==0.17.21 diff --git a/scripts/update_sha.py b/scripts/update_sha.py new file mode 100644 index 0000000000..c7da2e1a54 --- /dev/null +++ b/scripts/update_sha.py @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=import-error,unspecified-encoding + +import argparse + +import requests +from ruamel.yaml import YAML + +API_URL = "https://api.github.com/repos/open-telemetry/opentelemetry-python-contrib/commits/" +WORKFLOW_FILE = ".github/workflows/test.yml" + + +def get_sha(branch): + url = API_URL + branch + response = requests.get(url) + response.raise_for_status() + return response.json()["sha"] + + +def update_sha(sha): + yaml = YAML() + yaml.preserve_quotes = True + with open(WORKFLOW_FILE, "r") as file: + workflow = yaml.load(file) + workflow["env"]["CONTRIB_REPO_SHA"] = sha + with open(WORKFLOW_FILE, "w") as file: + yaml.dump(workflow, file) + + +def main(): + args = parse_args() + sha = get_sha(args.branch) + update_sha(sha) + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Updates the SHA in the workflow file" + ) + parser.add_argument("-b", "--branch", help="branch to use") + return parser.parse_args() + + +if __name__ == "__main__": + main() From a99cc21152ef1a129fc28ec539e73cbe4ffa8e75 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 20 Jan 2023 11:19:50 -0600 Subject: [PATCH 1370/1517] Fix validation of baggage values (#3058) * Fix validation of baggage values Fixes #2934 * Remove test case --- CHANGELOG.md | 3 + .../src/opentelemetry/baggage/__init__.py | 11 +-- .../baggage/propagation/__init__.py | 6 +- .../src/opentelemetry/context/__init__.py | 4 +- .../tests/baggage/test_baggage.py | 75 +++++++++++-------- .../test_w3cbaggagepropagator.py} | 42 ++++++++--- 6 files changed, 84 insertions(+), 57 deletions(-) rename opentelemetry-api/tests/{baggage/test_baggage_propagation.py => propagators/test_w3cbaggagepropagator.py} (88%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f6a1e91e..4541f19129 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add db metric name to semantic conventions ([#3115](https://github.com/open-telemetry/opentelemetry-python/pull/3115)) +- Fix validation of baggage values + ([#3058](https://github.com/open-telemetry/opentelemetry-python/pull/3058)) + ## Version 1.15.0/0.36b0 (2022-12-09) - Regenerate opentelemetry-proto to be compatible with protobuf 3 and 4 diff --git a/opentelemetry-api/src/opentelemetry/baggage/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/__init__.py index 8dea6dbfb9..9a740200a6 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/__init__.py @@ -81,16 +81,7 @@ def set_baggage( A Context with the value updated """ baggage = dict(get_all(context=context)) - if not _is_valid_key(name): - _logger.warning( - "Baggage key `%s` does not match format, ignoring", name - ) - elif not _is_valid_value(str(value)): - _logger.warning( - "Baggage value `%s` does not match format, ignoring", value - ) - else: - baggage[name] = value + baggage[name] = value return set_value(_BAGGAGE_KEY, baggage, context=context) diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index edba837b81..ecdc1eab43 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -88,12 +88,14 @@ def extract( "Baggage list-member `%s` doesn't match the format", entry ) continue - name = unquote_plus(name).strip().lower() - value = unquote_plus(value).strip() + if not _is_valid_pair(name, value): _logger.warning("Invalid baggage entry: `%s`", entry) continue + name = unquote_plus(name).strip().lower() + value = unquote_plus(value).strip() + context = set_baggage( name, value, diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 97ffcf8f72..306f0937d5 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -15,9 +15,9 @@ import logging import threading import typing -import uuid from functools import wraps from os import environ +from uuid import uuid4 from pkg_resources import iter_entry_points @@ -78,7 +78,7 @@ def create_key(keyname: str) -> str: Returns: A unique string representing the newly created key. """ - return keyname + "-" + str(uuid.uuid4()) + return keyname + "-" + str(uuid4()) def get_value(key: str, context: typing.Optional[Context] = None) -> "object": diff --git a/opentelemetry-api/tests/baggage/test_baggage.py b/opentelemetry-api/tests/baggage/test_baggage.py index 223b9b2ff2..5eb73d53dc 100644 --- a/opentelemetry-api/tests/baggage/test_baggage.py +++ b/opentelemetry-api/tests/baggage/test_baggage.py @@ -14,59 +14,70 @@ # type: ignore -import unittest +from unittest import TestCase -from opentelemetry import baggage, context +from opentelemetry.baggage import ( + _is_valid_value, + clear, + get_all, + get_baggage, + remove_baggage, + set_baggage, +) +from opentelemetry.context import attach, detach -class TestBaggageManager(unittest.TestCase): +class TestBaggageManager(TestCase): def test_set_baggage(self): - self.assertEqual({}, baggage.get_all()) + self.assertEqual({}, get_all()) - ctx = baggage.set_baggage("test", "value") - self.assertEqual(baggage.get_baggage("test", context=ctx), "value") + ctx = set_baggage("test", "value") + self.assertEqual(get_baggage("test", context=ctx), "value") - ctx = baggage.set_baggage("test", "value2", context=ctx) - self.assertEqual(baggage.get_baggage("test", context=ctx), "value2") + ctx = set_baggage("test", "value2", context=ctx) + self.assertEqual(get_baggage("test", context=ctx), "value2") def test_baggages_current_context(self): - token = context.attach(baggage.set_baggage("test", "value")) - self.assertEqual(baggage.get_baggage("test"), "value") - context.detach(token) - self.assertEqual(baggage.get_baggage("test"), None) + token = attach(set_baggage("test", "value")) + self.assertEqual(get_baggage("test"), "value") + detach(token) + self.assertEqual(get_baggage("test"), None) def test_set_multiple_baggage_entries(self): - ctx = baggage.set_baggage("test", "value") - ctx = baggage.set_baggage("test2", "value2", context=ctx) - self.assertEqual(baggage.get_baggage("test", context=ctx), "value") - self.assertEqual(baggage.get_baggage("test2", context=ctx), "value2") + ctx = set_baggage("test", "value") + ctx = set_baggage("test2", "value2", context=ctx) + self.assertEqual(get_baggage("test", context=ctx), "value") + self.assertEqual(get_baggage("test2", context=ctx), "value2") self.assertEqual( - baggage.get_all(context=ctx), + get_all(context=ctx), {"test": "value", "test2": "value2"}, ) def test_modifying_baggage(self): - ctx = baggage.set_baggage("test", "value") - self.assertEqual(baggage.get_baggage("test", context=ctx), "value") - baggage_entries = baggage.get_all(context=ctx) + ctx = set_baggage("test", "value") + self.assertEqual(get_baggage("test", context=ctx), "value") + baggage_entries = get_all(context=ctx) with self.assertRaises(TypeError): baggage_entries["test"] = "mess-this-up" - self.assertEqual(baggage.get_baggage("test", context=ctx), "value") + self.assertEqual(get_baggage("test", context=ctx), "value") def test_remove_baggage_entry(self): - self.assertEqual({}, baggage.get_all()) + self.assertEqual({}, get_all()) - ctx = baggage.set_baggage("test", "value") - ctx = baggage.set_baggage("test2", "value2", context=ctx) - ctx = baggage.remove_baggage("test", context=ctx) - self.assertEqual(baggage.get_baggage("test", context=ctx), None) - self.assertEqual(baggage.get_baggage("test2", context=ctx), "value2") + ctx = set_baggage("test", "value") + ctx = set_baggage("test2", "value2", context=ctx) + ctx = remove_baggage("test", context=ctx) + self.assertEqual(get_baggage("test", context=ctx), None) + self.assertEqual(get_baggage("test2", context=ctx), "value2") def test_clear_baggage(self): - self.assertEqual({}, baggage.get_all()) + self.assertEqual({}, get_all()) - ctx = baggage.set_baggage("test", "value") - self.assertEqual(baggage.get_baggage("test", context=ctx), "value") + ctx = set_baggage("test", "value") + self.assertEqual(get_baggage("test", context=ctx), "value") - ctx = baggage.clear(context=ctx) - self.assertEqual(baggage.get_all(context=ctx), {}) + ctx = clear(context=ctx) + self.assertEqual(get_all(context=ctx), {}) + + def test__is_valid_value(self): + self.assertTrue(_is_valid_value("GET%20%2Fapi%2F%2Freport")) diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/propagators/test_w3cbaggagepropagator.py similarity index 88% rename from opentelemetry-api/tests/baggage/test_baggage_propagation.py rename to opentelemetry-api/tests/propagators/test_w3cbaggagepropagator.py index 466ff3be30..ccc4b3cb2d 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/propagators/test_w3cbaggagepropagator.py @@ -14,11 +14,11 @@ # # type: ignore -import unittest from logging import WARNING +from unittest import TestCase from unittest.mock import Mock, patch -from opentelemetry import baggage +from opentelemetry.baggage import get_all, set_baggage from opentelemetry.baggage.propagation import ( W3CBaggagePropagator, _format_baggage, @@ -26,26 +26,26 @@ from opentelemetry.context import get_current -class TestBaggagePropagation(unittest.TestCase): +class TestW3CBaggagePropagator(TestCase): def setUp(self): self.propagator = W3CBaggagePropagator() def _extract(self, header_value): """Test helper""" header = {"baggage": [header_value]} - return baggage.get_all(self.propagator.extract(header)) + return get_all(self.propagator.extract(header)) def _inject(self, values): """Test helper""" ctx = get_current() for k, v in values.items(): - ctx = baggage.set_baggage(k, v, context=ctx) + ctx = set_baggage(k, v, context=ctx) output = {} self.propagator.inject(output, context=ctx) return output.get("baggage") def test_no_context_header(self): - baggage_entries = baggage.get_all(self.propagator.extract({})) + baggage_entries = get_all(self.propagator.extract({})) self.assertEqual(baggage_entries, {}) def test_empty_context_header(self): @@ -57,10 +57,9 @@ def test_valid_header(self): expected = {"key1": "val1", "key2": "val2"} self.assertEqual(self._extract(header), expected) - def test_valid_header_with_space(self): + def test_invalid_header_with_space(self): header = "key1 = val1, key2 =val2 " - expected = {"key1": "val1", "key2": "val2"} - self.assertEqual(self._extract(header), expected) + self.assertEqual(self._extract(header), {}) def test_valid_header_with_properties(self): header = "key1=val1,key2=val2;prop=1;prop2;prop3=2" @@ -188,8 +187,8 @@ def test_inject_no_baggage_entries(self): output = self._inject(values) self.assertEqual(None, output) - def test_inject_invalid_entries(self): - self.assertEqual(None, self._inject({"key": "val ue"})) + def test_inject_space_entries(self): + self.assertEqual("key=val+ue", self._inject({"key": "val ue"})) def test_inject(self): values = { @@ -242,3 +241,24 @@ def test__format_baggage(self): _format_baggage({"key/key": "value/value"}), "key%2Fkey=value%2Fvalue", ) + + @patch("opentelemetry.baggage._BAGGAGE_KEY", new="abc") + def test_inject_extract(self): + + carrier = {} + + context = set_baggage( + "transaction", "string with spaces", context=get_current() + ) + + self.propagator.inject(carrier, context) + + context = self.propagator.extract(carrier) + + self.assertEqual( + carrier, {"baggage": "transaction=string+with+spaces"} + ) + + self.assertEqual( + context, {"abc": {"transaction": "string with spaces"}} + ) From ce27da931f1c6ac6fdda452471f3b3e809dc4a08 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 24 Jan 2023 01:51:52 +0530 Subject: [PATCH 1371/1517] Remove update-sha workflow (#3139) --- .github/workflows/update-sha.yml | 50 -------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 .github/workflows/update-sha.yml diff --git a/.github/workflows/update-sha.yml b/.github/workflows/update-sha.yml deleted file mode 100644 index 0a983a7e80..0000000000 --- a/.github/workflows/update-sha.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Update SHA - -on: issue_comment - -jobs: - update-pr-sha: - name: Update SHA - if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, 'update-sha') }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - token: ${{ secrets.BOT_TOKEN }} - - # GitHub doesn't support string split expression - - name: Extract branch name from comment to get commit SHA - uses: jungwinter/split@v2 - id: split - with: - msg: ${{ github.event.comment.body }} - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.9' - - - name: Use CLA approved github bot - run: .github/scripts/use-cla-approved-github-bot.sh - - - name: Checkout Pull Request - env: - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} - run: | - PR_URL="${{ github.event.issue.pull_request.url }}" - PR_NUM=${PR_URL##*/} - echo "Checking out from PR #$PR_NUM based on URL: $PR_URL" - hub pr checkout $PR_NUM - - # caching is not supported for this event type - - name: Run script - run: | - python -m pip install requests==2.28.1 ruamel.yaml==0.17.21 - python scripts/update_sha.py --branch ${{ steps.split.outputs._1 }} - - - name: Commit and Push changes - env: - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} - run: | - git commit -a -m "Update SHA" - git push From 8f312c49a5c140c14d1829c66abfe4e859ad8fd7 Mon Sep 17 00:00:00 2001 From: keithkroeger <10414523+keithkroeger@users.noreply.github.com> Date: Tue, 24 Jan 2023 04:34:20 +0000 Subject: [PATCH 1372/1517] Fix User-Agent header value for OTLP exporters (#3128) * Update values as required for issue * add CHANGED_FILES link * update changelog * update test to validate setting * update only oltp exporter * update grpc exporter * Update CHANGELOG.md Co-authored-by: Srikanth Chekuri * remove CHANGED_FILES# * update location of imports for linting Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 3 ++- .../opentelemetry/exporter/otlp/proto/grpc/__init__.py | 2 +- .../tests/logs/test_otlp_logs_exporter.py | 2 +- .../tests/test_otlp_metrics_exporter.py | 4 ++-- .../tests/test_otlp_trace_exporter.py | 8 ++++---- .../opentelemetry/exporter/otlp/proto/http/__init__.py | 2 +- .../tests/test_proto_log_exporter.py | 5 +++++ .../tests/test_proto_span_exporter.py | 5 +++++ 8 files changed, 21 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4541f19129..6bb6b0c701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([3124](https://github.com/open-telemetry/opentelemetry-python/pull/3124)) - Add db metric name to semantic conventions ([#3115](https://github.com/open-telemetry/opentelemetry-python/pull/3115)) - +- Fix User-Agent header value for OTLP exporters to conform to RFC7231 & RFC7230 + ([#3128](https://github.com/open-telemetry/opentelemetry-python/pull/3128)) - Fix validation of baggage values ([#3058](https://github.com/open-telemetry/opentelemetry-python/pull/3058)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py index 60af18de2e..07553d69d0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py @@ -71,5 +71,5 @@ """ from .version import __version__ -_USER_AGENT_HEADER_VALUE = "OTel OTLP Exporter Python/" + __version__ +_USER_AGENT_HEADER_VALUE = "OTel-OTLP-Exporter-Python/" + __version__ _OTLP_GRPC_HEADERS = [("user-agent", _USER_AGENT_HEADER_VALUE)] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index c7996b9bf9..f21ed06678 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -289,7 +289,7 @@ def test_otlp_headers_from_env(self): # pylint: disable=protected-access self.assertEqual( self.exporter._headers, - (("user-agent", "OTel OTLP Exporter Python/" + __version__),), + (("user-agent", "OTel-OTLP-Exporter-Python/" + __version__),), ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py index 6436661a98..31bb878a5f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py @@ -418,7 +418,7 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): ( ("key1", "value1"), ("key2", "VALUE=2"), - ("user-agent", "OTel OTLP Exporter Python/" + __version__), + ("user-agent", "OTel-OTLP-Exporter-Python/" + __version__), ), ) exporter = OTLPMetricExporter( @@ -430,7 +430,7 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): ( ("key3", "value3"), ("key4", "value4"), - ("user-agent", "OTel OTLP Exporter Python/" + __version__), + ("user-agent", "OTel-OTLP-Exporter-Python/" + __version__), ), ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 5c8f040725..36ae0a7c11 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -281,7 +281,7 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): ( ("key1", "value1"), ("key2", "VALUE=2"), - ("user-agent", "OTel OTLP Exporter Python/" + __version__), + ("user-agent", "OTel-OTLP-Exporter-Python/" + __version__), ), ) exporter = OTLPSpanExporter( @@ -293,7 +293,7 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): ( ("key3", "value3"), ("key4", "value4"), - ("user-agent", "OTel OTLP Exporter Python/" + __version__), + ("user-agent", "OTel-OTLP-Exporter-Python/" + __version__), ), ) exporter = OTLPSpanExporter( @@ -305,7 +305,7 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): ( ("key5", "value5"), ("key6", "value6"), - ("user-agent", "OTel OTLP Exporter Python/" + __version__), + ("user-agent", "OTel-OTLP-Exporter-Python/" + __version__), ), ) @@ -454,7 +454,7 @@ def test_otlp_headers(self, mock_ssl_channel, mock_secure): # This ensures that there is no other header than standard user-agent. self.assertEqual( exporter._headers, - (("user-agent", "OTel OTLP Exporter Python/" + __version__),), + (("user-agent", "OTel-OTLP-Exporter-Python/" + __version__),), ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.backoff") diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py index a14f0e2992..2c40b39590 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py @@ -76,7 +76,7 @@ _OTLP_HTTP_HEADERS = { "Content-Type": "application/x-protobuf", - "User-Agent": "OTel OTLP Exporter Python/" + __version__, + "User-Agent": "OTel-OTLP-Exporter-Python/" + __version__, } diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index f61a4291ed..05bdfb9af3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -38,6 +38,7 @@ _encode_value, _ProtobufEncoder, ) +from opentelemetry.exporter.otlp.proto.http.version import __version__ from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, ) @@ -96,6 +97,10 @@ def test_constructor_default(self): exporter._session.headers.get("Content-Type"), "application/x-protobuf", ) + self.assertEqual( + exporter._session.headers.get("User-Agent"), + "OTel-OTLP-Exporter-Python/" + __version__, + ) @patch.dict( "os.environ", diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index 64ca6fbbb8..a1d96f50b4 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -28,6 +28,7 @@ OTLPSpanExporter, _is_backoff_v2, ) +from opentelemetry.exporter.otlp.proto.http.version import __version__ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_COMPRESSION, @@ -67,6 +68,10 @@ def test_constructor_default(self): exporter._session.headers.get("Content-Type"), "application/x-protobuf", ) + self.assertEqual( + exporter._session.headers.get("User-Agent"), + "OTel-OTLP-Exporter-Python/" + __version__, + ) @patch.dict( "os.environ", From 7fcafb1bc5d5c24d186ede41004c23ca0099e4d1 Mon Sep 17 00:00:00 2001 From: Anderson Carlos Ferreira da Silva Date: Mon, 30 Jan 2023 13:49:21 +0100 Subject: [PATCH 1373/1517] Fix: name property access on Meter returns InstrumentationScope (#3142) --- .../src/opentelemetry/sdk/metrics/_internal/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index 92c8c3ed3f..a804e6e5e7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -62,7 +62,11 @@ def __init__( instrumentation_scope: InstrumentationScope, measurement_consumer: MeasurementConsumer, ): - super().__init__(instrumentation_scope) + super().__init__( + name=instrumentation_scope.name, + version=instrumentation_scope.version, + schema_url=instrumentation_scope.schema_url, + ) self._instrumentation_scope = instrumentation_scope self._measurement_consumer = measurement_consumer self._instrument_id_instrument = {} From 1d251539f22522f044fc9662c3405fac6bafb91a Mon Sep 17 00:00:00 2001 From: Amit Roy Date: Tue, 31 Jan 2023 13:19:21 -0500 Subject: [PATCH 1374/1517] remove ability to set a global metric prefix for prometheus exporter (#3137) --- CHANGELOG.md | 2 + .../exporter/prometheus/__init__.py | 16 ++----- .../tests/test_prometheus_exporter.py | 45 ++++++++----------- 3 files changed, 25 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bb6b0c701..28650a4b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +- Remove the ability to set a global metric prefix for Prometheus exporter + ([#3137](https://github.com/open-telemetry/opentelemetry-python/pull/3137)) - Adds environment variables for log exporter ([#3037](https://github.com/open-telemetry/opentelemetry-python/pull/3037)) - Add attribute name to type warning message. diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index dbf4493f9f..7442b7b242 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -114,14 +114,9 @@ def _convert_buckets( class PrometheusMetricReader(MetricReader): - """Prometheus metric exporter for OpenTelemetry. + """Prometheus metric exporter for OpenTelemetry.""" - Args: - prefix: single-word application prefix relevant to the domain - the metric belongs to. - """ - - def __init__(self, prefix: str = "") -> None: + def __init__(self) -> None: super().__init__( preferred_temporality={ @@ -133,7 +128,7 @@ def __init__(self, prefix: str = "") -> None: ObservableGauge: AggregationTemporality.CUMULATIVE, } ) - self._collector = _CustomCollector(prefix) + self._collector = _CustomCollector() REGISTRY.register(self._collector) self._collector._callback = self.collect @@ -158,8 +153,7 @@ class _CustomCollector: https://github.com/prometheus/client_python#custom-collectors """ - def __init__(self, prefix: str = ""): - self._prefix = prefix + def __init__(self): self._callback = None self._metrics_datas = deque() self._non_letters_digits_underscore_re = compile( @@ -210,8 +204,6 @@ def _translate_to_prometheus( pre_metric_family_ids = [] metric_name = "" - if self._prefix != "": - metric_name = self._prefix + "_" metric_name += self._sanitize(metric.name) metric_description = metric.description or "" diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 9e7f143e01..1180fac614 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -52,8 +52,7 @@ def setUp(self): def test_constructor(self): """Test the constructor.""" with self._registry_register_patch: - exporter = PrometheusMetricReader(prefix="testprefix") - self.assertEqual(exporter._collector._prefix, "testprefix") + _ = PrometheusMetricReader() self.assertTrue(self._mock_registry_register.called) def test_shutdown(self): @@ -102,7 +101,7 @@ def test_histogram_to_prometheus(self): ] ) - collector = _CustomCollector("testprefix") + collector = _CustomCollector() collector.add_metrics_data(metrics_data) result_bytes = generate_latest(collector) result = result_bytes.decode("utf-8") @@ -110,13 +109,13 @@ def test_histogram_to_prometheus(self): result, dedent( """\ - # HELP testprefix_test_name_s foo - # TYPE testprefix_test_name_s histogram - testprefix_test_name_s_bucket{histo="1",le="123.0"} 1.0 - testprefix_test_name_s_bucket{histo="1",le="456.0"} 4.0 - testprefix_test_name_s_bucket{histo="1",le="+Inf"} 6.0 - testprefix_test_name_s_count{histo="1"} 6.0 - testprefix_test_name_s_sum{histo="1"} 579.0 + # HELP test_name_s foo + # TYPE test_name_s histogram + test_name_s_bucket{histo="1",le="123.0"} 1.0 + test_name_s_bucket{histo="1",le="456.0"} 4.0 + test_name_s_bucket{histo="1",le="+Inf"} 6.0 + test_name_s_count{histo="1"} 6.0 + test_name_s_sum{histo="1"} 579.0 """ ), ) @@ -147,14 +146,12 @@ def test_sum_to_prometheus(self): ] ) - collector = _CustomCollector("testprefix") + collector = _CustomCollector() collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): self.assertEqual(type(prometheus_metric), CounterMetricFamily) - self.assertEqual( - prometheus_metric.name, "testprefix_test_sum_testunit" - ) + self.assertEqual(prometheus_metric.name, "test_sum_testunit") self.assertEqual(prometheus_metric.documentation, "testdesc") self.assertTrue(len(prometheus_metric.samples) == 1) self.assertEqual(prometheus_metric.samples[0].value, 123) @@ -192,14 +189,12 @@ def test_gauge_to_prometheus(self): ] ) - collector = _CustomCollector("testprefix") + collector = _CustomCollector() collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): self.assertEqual(type(prometheus_metric), GaugeMetricFamily) - self.assertEqual( - prometheus_metric.name, "testprefix_test_gauge_testunit" - ) + self.assertEqual(prometheus_metric.name, "test_gauge_testunit") self.assertEqual(prometheus_metric.documentation, "testdesc") self.assertTrue(len(prometheus_metric.samples) == 1) self.assertEqual(prometheus_metric.samples[0].value, 123) @@ -217,13 +212,13 @@ def test_invalid_metric(self): description="testdesc", unit="testunit", ) - collector = _CustomCollector("testprefix") + collector = _CustomCollector() collector.add_metrics_data([record]) collector.collect() self.assertLogs("opentelemetry.exporter.prometheus", level="WARNING") def test_sanitize(self): - collector = _CustomCollector("testprefix") + collector = _CustomCollector() self.assertEqual( collector._sanitize("1!2@3#4$5%6^7&8*9(0)_-"), "1_2_3_4_5_6_7_8_9_0___", @@ -256,14 +251,12 @@ def test_list_labels(self): ) ] ) - collector = _CustomCollector("testprefix") + collector = _CustomCollector() collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): self.assertEqual(type(prometheus_metric), GaugeMetricFamily) - self.assertEqual( - prometheus_metric.name, "testprefix_test_gauge_testunit" - ) + self.assertEqual(prometheus_metric.name, "test_gauge_testunit") self.assertEqual(prometheus_metric.documentation, "testdesc") self.assertTrue(len(prometheus_metric.samples) == 1) self.assertEqual(prometheus_metric.samples[0].value, 123) @@ -276,7 +269,7 @@ def test_list_labels(self): def test_check_value(self): - collector = _CustomCollector("") + collector = _CustomCollector() self.assertEqual(collector._check_value(1), "1") self.assertEqual(collector._check_value(1.0), "1.0") @@ -290,7 +283,7 @@ def test_check_value(self): def test_multiple_collection_calls(self): - metric_reader = PrometheusMetricReader(prefix="prefix") + metric_reader = PrometheusMetricReader() provider = MeterProvider(metric_readers=[metric_reader]) meter = provider.get_meter("getting-started", "0.1.2") counter = meter.create_counter("counter") From 2e14fe1295445d0c3cb6b5a29cc8db8c7db9f03c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 1 Feb 2023 17:48:27 -0600 Subject: [PATCH 1375/1517] Remove usage of pkg_resources (#3047) * Remove usage of pkg_resources Fixes #2927 * WIP * WIP * Add test case for _load_providers * Fix test case for 3.9 * Fix lint * Fix 3.7 sdk tests * Fix lint * Fix lint * Fix mypy again * WIP * WIP * WIP * WIP * Move to module * Fix SDK * Fix mypy * Refactor load call * Fix mypy * Fix lint * Fix opencensus exporter * Refactor implementation * Add missing dependency * Undo changes in shim * Fix dependency * Revert "Undo changes in shim" This reverts commit bd82b8feb2ab38678bd62fd6a9a53db88dc2737d. * Update dependencies * Update dependency for opencensus exporter * Add descriptive error --- .../pyproject.toml | 4 +- .../opentelemetry/exporter/opencensus/util.py | 21 ++-- .../tests/encoder/test_v1_json.py | 5 +- .../tests/encoder/test_v2_json.py | 5 +- .../tests/encoder/test_v2_protobuf.py | 5 +- opentelemetry-api/pyproject.toml | 1 + .../src/opentelemetry/context/__init__.py | 26 +++-- .../metrics/_internal/__init__.py | 2 +- .../src/opentelemetry/propagate/__init__.py | 23 ++-- .../src/opentelemetry/trace/__init__.py | 2 +- .../opentelemetry/util/_importlib_metadata.py | 38 +++++++ .../src/opentelemetry/util/_providers.py | 30 ++--- opentelemetry-api/tests/__init__.py | 9 -- .../tests/propagators/test_propagators.py | 51 ++++++--- opentelemetry-api/tests/trace/test_status.py | 6 +- .../tests/util/test__importlib_metadata.py | 34 ++++++ .../tests/util/test__providers.py | 56 +++++++++ opentelemetry-api/tests/util/test_re.py | 10 +- opentelemetry-proto/tests/test_proto.py | 7 +- .../sdk/_configuration/__init__.py | 38 +++++-- .../sdk/error_handler/__init__.py | 10 +- .../opentelemetry/sdk/resources/__init__.py | 7 +- .../tests/error_handler/test_error_handler.py | 24 ++-- opentelemetry-sdk/tests/test_configurator.py | 106 ++++++++++++------ .../tests/test_semconv.py | 7 +- .../test_asyncio.py | 17 ++- .../test_threads.py | 17 ++- .../test_client_server/test_asyncio.py | 17 ++- .../test_client_server/test_threads.py | 17 ++- .../request_handler.py | 15 +++ .../test_asyncio.py | 17 ++- .../test_threads.py | 17 ++- .../test_late_span_finish/test_asyncio.py | 17 ++- .../test_late_span_finish/test_threads.py | 17 ++- .../test_listener_per_request/test_asyncio.py | 17 ++- .../test_listener_per_request/test_threads.py | 17 ++- .../test_multiple_callbacks/test_asyncio.py | 17 ++- .../test_multiple_callbacks/test_threads.py | 17 ++- .../test_nested_callbacks/test_asyncio.py | 17 ++- .../test_nested_callbacks/test_threads.py | 19 +++- .../test_asyncio.py | 17 ++- .../test_threads.py | 17 ++- tox.ini | 2 +- 43 files changed, 637 insertions(+), 181 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py create mode 100644 opentelemetry-api/tests/util/test__importlib_metadata.py create mode 100644 opentelemetry-api/tests/util/test__providers.py diff --git a/exporter/opentelemetry-exporter-opencensus/pyproject.toml b/exporter/opentelemetry-exporter-opencensus/pyproject.toml index fe03741dfb..81d4546dd0 100644 --- a/exporter/opentelemetry-exporter-opencensus/pyproject.toml +++ b/exporter/opentelemetry-exporter-opencensus/pyproject.toml @@ -28,8 +28,8 @@ classifiers = [ dependencies = [ "grpcio >= 1.0.0, < 2.0.0", "opencensus-proto >= 0.1.0, < 1.0.0", - "opentelemetry-api ~= 1.3", - "opentelemetry-sdk ~= 1.3", + "opentelemetry-api >= 1.16.0.dev", + "opentelemetry-sdk >= 1.15", "protobuf ~= 3.13", "setuptools >= 16.0", ] diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/util.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/util.py index e08b884c3f..694e8dc6a1 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/util.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/util.py @@ -12,11 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import socket -import time +from os import getpid +from socket import gethostname +from time import time -import pkg_resources +# pylint: disable=wrong-import-position from google.protobuf.timestamp_pb2 import Timestamp from opencensus.proto.agent.common.v1 import common_pb2 from opencensus.proto.trace.v1 import trace_pb2 @@ -25,10 +25,9 @@ __version__ as opencensusexporter_exporter_version, ) from opentelemetry.trace import SpanKind +from opentelemetry.util._importlib_metadata import version -OPENTELEMETRY_VERSION = pkg_resources.get_distribution( - "opentelemetry-api" -).version +OPENTELEMETRY_VERSION = version("opentelemetry-api") def proto_timestamp_from_time_ns(time_ns): @@ -88,11 +87,9 @@ def get_node(service_name, host_name): """ return common_pb2.Node( identifier=common_pb2.ProcessIdentifier( - host_name=socket.gethostname() if host_name is None else host_name, - pid=os.getpid(), - start_timestamp=proto_timestamp_from_time_ns( - int(time.time() * 1e9) - ), + host_name=gethostname() if host_name is None else host_name, + pid=getpid(), + start_timestamp=proto_timestamp_from_time_ns(int(time() * 1e9)), ), library_info=common_pb2.LibraryInfo( language=common_pb2.LibraryInfo.Language.Value("PYTHON"), diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py index 59a750eb51..778ed74e8d 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v1_json.py @@ -28,7 +28,10 @@ ) from opentelemetry.trace import TraceFlags, format_span_id, format_trace_id -from .common_tests import TEST_SERVICE_NAME, CommonEncoderTestCases +from .common_tests import ( # pylint: disable=import-error + TEST_SERVICE_NAME, + CommonEncoderTestCases, +) # pylint: disable=protected-access diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py index 85cc91a0d9..37a0414fca 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/test_v2_json.py @@ -28,7 +28,10 @@ ) from opentelemetry.trace import SpanKind, TraceFlags -from .common_tests import TEST_SERVICE_NAME, CommonEncoderTestCases +from .common_tests import ( # pylint: disable=import-error + TEST_SERVICE_NAME, + CommonEncoderTestCases, +) # pylint: disable=protected-access diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py index 8ce61a92a1..2f2c894e4a 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/test_v2_protobuf.py @@ -28,7 +28,10 @@ ) from opentelemetry.trace import SpanKind -from .common_tests import TEST_SERVICE_NAME, CommonEncoderTestCases +from .common_tests import ( # pylint: disable=import-error + TEST_SERVICE_NAME, + CommonEncoderTestCases, +) # pylint: disable=protected-access diff --git a/opentelemetry-api/pyproject.toml b/opentelemetry-api/pyproject.toml index 50b2a8b8a0..e9490ab1d2 100644 --- a/opentelemetry-api/pyproject.toml +++ b/opentelemetry-api/pyproject.toml @@ -27,6 +27,7 @@ classifiers = [ dependencies = [ "Deprecated >= 1.2.6", "setuptools >= 16.0", + "importlib-metadata >= 5.0.0; python_version=='3.7'" ] dynamic = [ "version", diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 306f0937d5..3e85b64fe4 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -19,10 +19,10 @@ from os import environ from uuid import uuid4 -from pkg_resources import iter_entry_points - -from opentelemetry.context.context import Context, _RuntimeContext +# pylint: disable=wrong-import-position +from opentelemetry.context.context import Context, _RuntimeContext # noqa from opentelemetry.environment_variables import OTEL_PYTHON_CONTEXT +from opentelemetry.util._importlib_metadata import entry_points logger = logging.getLogger(__name__) _RUNTIME_CONTEXT = None # type: typing.Optional[_RuntimeContext] @@ -41,27 +41,33 @@ def _load_runtime_context(func: _F) -> _F: @wraps(func) # type: ignore[misc] def wrapper( # type: ignore[misc] *args: typing.Tuple[typing.Any, typing.Any], - **kwargs: typing.Dict[typing.Any, typing.Any] + **kwargs: typing.Dict[typing.Any, typing.Any], ) -> typing.Optional[typing.Any]: global _RUNTIME_CONTEXT # pylint: disable=global-statement with _RUNTIME_CONTEXT_LOCK: if _RUNTIME_CONTEXT is None: - # FIXME use a better implementation of a configuration manager to avoid having - # to get configuration values straight from environment variables + # FIXME use a better implementation of a configuration manager + # to avoid having to get configuration values straight from + # environment variables default_context = "contextvars_context" configured_context = environ.get( OTEL_PYTHON_CONTEXT, default_context ) # type: str try: - _RUNTIME_CONTEXT = next( - iter_entry_points( - "opentelemetry_context", configured_context + + _RUNTIME_CONTEXT = next( # type: ignore + iter( # type: ignore + entry_points( # type: ignore + group="opentelemetry_context", + name=configured_context, + ) ) ).load()() + except Exception: # pylint: disable=broad-except - logger.error( + logger.exception( "Failed to load context: %s", configured_context ) return func(*args, **kwargs) # type: ignore[misc] diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py index ccde6900d8..630e9c4053 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py @@ -119,7 +119,7 @@ def get_meter( version: Optional. The version string of the instrumenting library. Usually this should be the same as - ``pkg_resources.get_distribution(instrumenting_library_name).version``. + ``importlib.metadata.version(instrumenting_library_name)``. schema_url: Optional. Specifies the Schema URL of the emitted telemetry. """ diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index a39d8a44d1..56f217f282 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -68,22 +68,21 @@ def example_route(): https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md """ -import typing from logging import getLogger from os import environ - -from pkg_resources import iter_entry_points +from typing import Optional from opentelemetry.context.context import Context from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.propagators import composite, textmap +from opentelemetry.util._importlib_metadata import entry_points logger = getLogger(__name__) def extract( carrier: textmap.CarrierT, - context: typing.Optional[Context] = None, + context: Optional[Context] = None, getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter, ) -> Context: """Uses the configured propagator to extract a Context from the carrier. @@ -104,7 +103,7 @@ def extract( def inject( carrier: textmap.CarrierT, - context: typing.Optional[Context] = None, + context: Optional[Context] = None, setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter, ) -> None: """Uses the configured propagator to inject a Context into the carrier. @@ -129,20 +128,30 @@ def inject( "tracecontext,baggage", ) + for propagator in environ_propagators.split(","): propagator = propagator.strip() + try: + propagators.append( # type: ignore next( # type: ignore - iter_entry_points("opentelemetry_propagator", propagator) + iter( # type: ignore + entry_points( # type: ignore + group="opentelemetry_propagator", + name=propagator, + ) + ) ).load()() ) + except Exception: # pylint: disable=broad-except logger.exception( - "Failed to load configured propagator `%s`", propagator + "Failed to load configured propagator: %s", propagator ) raise + _HTTP_TEXT_FORMAT = composite.CompositePropagator(propagators) # type: ignore diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index f0e92f14bb..304df22754 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -216,7 +216,7 @@ def get_tracer( instrumenting_library_version: Optional. The version string of the instrumenting library. Usually this should be the same as - ``pkg_resources.get_distribution(instrumenting_library_name).version``. + ``importlib.metadata.version(instrumenting_library_name)``. schema_url: Optional. Specifies the Schema URL of the emitted telemetry. """ diff --git a/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py new file mode 100644 index 0000000000..889c4cf1ac --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py @@ -0,0 +1,38 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sys import version_info + +# FIXME remove this when support for 3.7 is dropped. +if version_info.minor == 7: + # pylint: disable=import-error + from importlib_metadata import entry_points, version # type: ignore + +# FIXME remove this file when support for 3.9 is dropped. +elif version_info.minor in (8, 9): + # pylint: disable=import-error + from importlib.metadata import ( + entry_points as importlib_metadata_entry_points, + ) + from importlib.metadata import version + + def entry_points(group: str, name: str): # type: ignore + for entry_point in importlib_metadata_entry_points()[group]: + if entry_point.name == name: + yield entry_point + +else: + from importlib.metadata import entry_points, version + +__all__ = ["entry_points", "version"] diff --git a/opentelemetry-api/src/opentelemetry/util/_providers.py b/opentelemetry-api/src/opentelemetry/util/_providers.py index d8feb88d62..d255ac999f 100644 --- a/opentelemetry-api/src/opentelemetry/util/_providers.py +++ b/opentelemetry-api/src/opentelemetry/util/_providers.py @@ -16,7 +16,7 @@ from os import environ from typing import TYPE_CHECKING, TypeVar, cast -from pkg_resources import iter_entry_points +from opentelemetry.util._importlib_metadata import entry_points if TYPE_CHECKING: from opentelemetry.metrics import MeterProvider @@ -30,23 +30,25 @@ def _load_provider( provider_environment_variable: str, provider: str ) -> Provider: + try: - entry_point = next( - iter_entry_points( - f"opentelemetry_{provider}", - name=cast( - str, - environ.get( - provider_environment_variable, - f"default_{provider}", - ), - ), - ) + + provider_name = cast( + str, + environ.get(provider_environment_variable, f"default_{provider}"), ) + return cast( Provider, - entry_point.load()(), + next( # type: ignore + iter( # type: ignore + entry_points( # type: ignore + group=f"opentelemetry_{provider}", + name=provider_name, + ) + ) + ).load()(), ) except Exception: # pylint: disable=broad-except - logger.error("Failed to load configured provider %s", provider) + logger.exception("Failed to load configured provider %s", provider) raise diff --git a/opentelemetry-api/tests/__init__.py b/opentelemetry-api/tests/__init__.py index bc48946761..b0a6f42841 100644 --- a/opentelemetry-api/tests/__init__.py +++ b/opentelemetry-api/tests/__init__.py @@ -11,12 +11,3 @@ # 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 pkg_resources - -# naming the tests module as a namespace package ensures that -# relative imports will resolve properly for other test packages, -# as it enables searching for a composite of multiple test modules. -# -# only the opentelemetry-api directory needs this code, as it is -# the first tests module found by pylint during eachdist.py lint -pkg_resources.declare_namespace(__name__) diff --git a/opentelemetry-api/tests/propagators/test_propagators.py b/opentelemetry-api/tests/propagators/test_propagators.py index a8fed620be..bb84bc4f1a 100644 --- a/opentelemetry-api/tests/propagators/test_propagators.py +++ b/opentelemetry-api/tests/propagators/test_propagators.py @@ -53,29 +53,46 @@ def test_propagators(propagators): @patch.dict(environ, {OTEL_PROPAGATORS: "a, b, c "}) @patch("opentelemetry.propagators.composite.CompositePropagator") - @patch("pkg_resources.iter_entry_points") + @patch("opentelemetry.util._importlib_metadata.entry_points") def test_non_default_propagators( - self, mock_iter_entry_points, mock_compositehttppropagator + self, mock_entry_points, mock_compositehttppropagator ): - def iter_entry_points_mock(_, propagator): - return iter( - [ - Mock( - **{ - "load.side_effect": [ - Mock(**{"side_effect": [propagator]}) - ] - } - ) - ] - ) - mock_iter_entry_points.configure_mock( - **{"side_effect": iter_entry_points_mock} + mock_entry_points.configure_mock( + **{ + "side_effect": [ + [ + Mock( + **{ + "load.return_value": Mock( + **{"return_value": "a"} + ) + } + ), + ], + [ + Mock( + **{ + "load.return_value": Mock( + **{"return_value": "b"} + ) + } + ) + ], + [ + Mock( + **{ + "load.return_value": Mock( + **{"return_value": "c"} + ) + } + ) + ], + ] + } ) def test_propagators(propagators): - self.assertEqual(propagators, ["a", "b", "c"]) mock_compositehttppropagator.configure_mock( diff --git a/opentelemetry-api/tests/trace/test_status.py b/opentelemetry-api/tests/trace/test_status.py index 74da78d6c7..6388ae9804 100644 --- a/opentelemetry-api/tests/trace/test_status.py +++ b/opentelemetry-api/tests/trace/test_status.py @@ -35,7 +35,7 @@ def test_invalid_description(self): self.assertEqual(status.description, None) self.assertIn( "Invalid status description type, expected str", - warning.output[0], + warning.output[0], # type: ignore ) def test_description_and_non_error_status(self): @@ -47,7 +47,7 @@ def test_description_and_non_error_status(self): self.assertEqual(status.description, None) self.assertIn( "description should only be set when status_code is set to StatusCode.ERROR", - warning.output[0], + warning.output[0], # type: ignore ) with self.assertLogs(level=WARNING) as warning: @@ -58,7 +58,7 @@ def test_description_and_non_error_status(self): self.assertEqual(status.description, None) self.assertIn( "description should only be set when status_code is set to StatusCode.ERROR", - warning.output[0], + warning.output[0], # type: ignore ) status = Status( diff --git a/opentelemetry-api/tests/util/test__importlib_metadata.py b/opentelemetry-api/tests/util/test__importlib_metadata.py new file mode 100644 index 0000000000..7ca58881b8 --- /dev/null +++ b/opentelemetry-api/tests/util/test__importlib_metadata.py @@ -0,0 +1,34 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +from opentelemetry.metrics import MeterProvider +from opentelemetry.util._importlib_metadata import entry_points + + +class TestEntryPoints(TestCase): + def test_entry_points(self): + + self.assertIsInstance( + next( + iter( + entry_points( + group="opentelemetry_meter_provider", + name="default_meter_provider", + ) + ) + ).load()(), + MeterProvider, + ) diff --git a/opentelemetry-api/tests/util/test__providers.py b/opentelemetry-api/tests/util/test__providers.py new file mode 100644 index 0000000000..f7b21ebacf --- /dev/null +++ b/opentelemetry-api/tests/util/test__providers.py @@ -0,0 +1,56 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from importlib import reload +from os import environ +from unittest import TestCase +from unittest.mock import Mock, patch + +from opentelemetry.util import _providers + + +class Test_Providers(TestCase): + @patch.dict( + environ, + { # type: ignore + "provider_environment_variable": "mock_provider_environment_variable" + }, + ) + @patch("opentelemetry.util._importlib_metadata.entry_points") + def test__providers(self, mock_entry_points): + + reload(_providers) + + mock_entry_points.configure_mock( + **{ + "side_effect": [ + [ + Mock( + **{ + "load.return_value": Mock( + **{"return_value": "a"} + ) + } + ), + ], + ] + } + ) + + self.assertEqual( + _providers._load_provider( + "provider_environment_variable", "provider" + ), + "a", + ) diff --git a/opentelemetry-api/tests/util/test_re.py b/opentelemetry-api/tests/util/test_re.py index 99d79ba9d2..ea86f3e700 100644 --- a/opentelemetry-api/tests/util/test_re.py +++ b/opentelemetry-api/tests/util/test_re.py @@ -59,11 +59,13 @@ def test_parse_env_headers(self): True, ), ] - for case in inp: - s, expected, warn = case + for case_ in inp: + headers, expected, warn = case_ if warn: with self.assertLogs(level="WARNING") as cm: - self.assertEqual(parse_env_headers(s), dict(expected)) + self.assertEqual( + parse_env_headers(headers), dict(expected) + ) self.assertTrue( "Header format invalid! Header values in environment " "variables must be URL encoded per the OpenTelemetry " @@ -71,4 +73,4 @@ def test_parse_env_headers(self): in cm.records[0].message, ) else: - self.assertEqual(parse_env_headers(s), dict(expected)) + self.assertEqual(parse_env_headers(headers), dict(expected)) diff --git a/opentelemetry-proto/tests/test_proto.py b/opentelemetry-proto/tests/test_proto.py index 6551e4640f..9670be4627 100644 --- a/opentelemetry-proto/tests/test_proto.py +++ b/opentelemetry-proto/tests/test_proto.py @@ -13,15 +13,12 @@ # limitations under the License. # type: ignore +from importlib.util import find_spec from unittest import TestCase -from pkg_resources import DistributionNotFound, require - class TestInstrumentor(TestCase): def test_proto(self): - try: - require(["opentelemetry-proto"]) - except DistributionNotFound: + if find_spec("opentelemetry.proto") is None: self.fail("opentelemetry-proto not installed") diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index a1ef3b76a2..a65ed85d84 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -23,7 +23,6 @@ from os import environ from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type -from pkg_resources import iter_entry_points from typing_extensions import Literal from opentelemetry._logs import set_logger_provider @@ -57,6 +56,7 @@ from opentelemetry.sdk.trace.sampling import Sampler from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.trace import set_tracer_provider +from opentelemetry.util._importlib_metadata import entry_points _EXPORTER_OTLP = "otlp" _EXPORTER_OTLP_PROTO_GRPC = "otlp_proto_grpc" @@ -90,21 +90,37 @@ def _import_config_components( selected_components: List[str], entry_point_name: str ) -> Sequence[Tuple[str, object]]: - component_entry_points = { - ep.name: ep for ep in iter_entry_points(entry_point_name) - } - component_impls = [] + + component_implementations = [] + for selected_component in selected_components: - entry_point = component_entry_points.get(selected_component, None) - if not entry_point: + try: + component_implementations.append( + ( + selected_component, + next( + iter( + entry_points( + group=entry_point_name, name=selected_component + ) + ) + ).load(), + ) + ) + except KeyError: + raise RuntimeError( - f"Requested component '{selected_component}' not found in entry points for '{entry_point_name}'" + f"Requested entry point '{entry_point_name}' not found" ) - component_impl = entry_point.load() - component_impls.append((selected_component, component_impl)) + except StopIteration: + + raise RuntimeError( + f"Requested component '{selected_component}' not found in " + f"entry point '{entry_point_name}'" + ) - return component_impls + return component_implementations def _get_sampler() -> Optional[str]: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/error_handler/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/error_handler/__init__.py index 781f42e41a..7b21d92d2a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/error_handler/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/error_handler/__init__.py @@ -62,7 +62,7 @@ def _handle(self, error: Exception, *args, **kwargs): from abc import ABC, abstractmethod from logging import getLogger -from pkg_resources import iter_entry_points +from opentelemetry.util._importlib_metadata import entry_points logger = getLogger(__name__) @@ -118,9 +118,11 @@ def __exit__(self, exc_type, exc_value, traceback): plugin_handled = False - for error_handler_entry_point in iter_entry_points( - "opentelemetry_error_handler" - ): + error_handler_entry_points = entry_points( + group="opentelemetry_error_handler" + ) + + for error_handler_entry_point in error_handler_entry_points: error_handler_class = error_handler_entry_point.load() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 56707415e5..c46b87f89c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -64,14 +64,13 @@ from json import dumps from urllib import parse -import pkg_resources - from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk.environment_variables import ( OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, ) from opentelemetry.semconv.resource import ResourceAttributes +from opentelemetry.util._importlib_metadata import version from opentelemetry.util.types import AttributeValue LabelValue = AttributeValue @@ -136,9 +135,7 @@ TELEMETRY_SDK_LANGUAGE = ResourceAttributes.TELEMETRY_SDK_LANGUAGE -_OPENTELEMETRY_SDK_VERSION = pkg_resources.get_distribution( - "opentelemetry-sdk" -).version +_OPENTELEMETRY_SDK_VERSION = version("opentelemetry-sdk") class Resource: diff --git a/opentelemetry-sdk/tests/error_handler/test_error_handler.py b/opentelemetry-sdk/tests/error_handler/test_error_handler.py index 1712894464..116771dc9a 100644 --- a/opentelemetry-sdk/tests/error_handler/test_error_handler.py +++ b/opentelemetry-sdk/tests/error_handler/test_error_handler.py @@ -25,16 +25,16 @@ class TestErrorHandler(TestCase): - @patch("opentelemetry.sdk.error_handler.iter_entry_points") - def test_default_error_handler(self, mock_iter_entry_points): + @patch("opentelemetry.sdk.error_handler.entry_points") + def test_default_error_handler(self, mock_entry_points): with self.assertLogs(logger, ERROR): with GlobalErrorHandler(): raise Exception("some exception") # pylint: disable=no-self-use - @patch("opentelemetry.sdk.error_handler.iter_entry_points") - def test_plugin_error_handler(self, mock_iter_entry_points): + @patch("opentelemetry.sdk.error_handler.entry_points") + def test_plugin_error_handler(self, mock_entry_points): class ZeroDivisionErrorHandler(ErrorHandler, ZeroDivisionError): # pylint: disable=arguments-differ @@ -54,7 +54,7 @@ class AssertionErrorHandler(ErrorHandler, AssertionError): **{"load.return_value": AssertionErrorHandler} ) - mock_iter_entry_points.configure_mock( + mock_entry_points.configure_mock( **{ "return_value": [ mock_entry_point_zero_division_error_handler, @@ -78,8 +78,8 @@ class AssertionErrorHandler(ErrorHandler, AssertionError): AssertionErrorHandler._handle.assert_called_with(error) - @patch("opentelemetry.sdk.error_handler.iter_entry_points") - def test_error_in_handler(self, mock_iter_entry_points): + @patch("opentelemetry.sdk.error_handler.entry_points") + def test_error_in_handler(self, mock_entry_points): class ErrorErrorHandler(ErrorHandler, ZeroDivisionError): # pylint: disable=arguments-differ @@ -91,7 +91,7 @@ def _handle(self, error: Exception): **{"load.return_value": ErrorErrorHandler} ) - mock_iter_entry_points.configure_mock( + mock_entry_points.configure_mock( **{"return_value": [mock_entry_point_error_error_handler]} ) @@ -102,10 +102,8 @@ def _handle(self, error: Exception): raise error # pylint: disable=no-self-use - @patch("opentelemetry.sdk.error_handler.iter_entry_points") - def test_plugin_error_handler_context_manager( - self, mock_iter_entry_points - ): + @patch("opentelemetry.sdk.error_handler.entry_points") + def test_plugin_error_handler_context_manager(self, mock_entry_points): mock_error_handler_instance = Mock() @@ -118,7 +116,7 @@ def __new__(cls): **{"load.return_value": MockErrorHandlerClass} ) - mock_iter_entry_points.configure_mock( + mock_entry_points.configure_mock( **{"return_value": [mock_entry_point_error_handler]} ) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index a27c7a49a1..e07c5e3c78 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -14,12 +14,14 @@ # type: ignore # pylint: skip-file -import logging +from logging import getLogger from os import environ from typing import Dict, Iterable, Optional, Sequence from unittest import TestCase from unittest.mock import patch +from pytest import raises + from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.environment_variables import OTEL_PYTHON_ID_GENERATOR @@ -30,6 +32,7 @@ _get_exporter_names, _get_id_generator, _get_sampler, + _import_config_components, _import_exporters, _import_id_generator, _import_sampler, @@ -349,13 +352,14 @@ def test_trace_init_otlp(self): @patch.dict(environ, {OTEL_PYTHON_ID_GENERATOR: "custom_id_generator"}) @patch("opentelemetry.sdk._configuration.IdGenerator", new=IdGenerator) - @patch("opentelemetry.sdk._configuration.iter_entry_points") - def test_trace_init_custom_id_generator(self, mock_iter_entry_points): - mock_iter_entry_points.configure_mock( + @patch("opentelemetry.sdk._configuration.entry_points") + def test_trace_init_custom_id_generator(self, mock_entry_points): + mock_entry_points.configure_mock( return_value=[ IterEntryPoint("custom_id_generator", CustomIdGenerator) ] ) + id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) _init_tracing({}, id_generator=id_generator) @@ -372,10 +376,10 @@ def test_trace_init_custom_sampler_with_env_non_existent_entry_point(self): provider = self.set_provider_mock.call_args[0][0] self.assertIsNone(provider.sampler) - @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict("os.environ", {OTEL_TRACES_SAMPLER: "custom_sampler_factory"}) - def test_trace_init_custom_sampler_with_env(self, mock_iter_entry_points): - mock_iter_entry_points.configure_mock( + def test_trace_init_custom_sampler_with_env(self, mock_entry_points): + mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_sampler_factory", @@ -383,18 +387,19 @@ def test_trace_init_custom_sampler_with_env(self, mock_iter_entry_points): ) ] ) + sampler_name = _get_sampler() sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider.sampler, CustomSampler) - @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict("os.environ", {OTEL_TRACES_SAMPLER: "custom_sampler_factory"}) def test_trace_init_custom_sampler_with_env_bad_factory( - self, mock_iter_entry_points + self, mock_entry_points ): - mock_iter_entry_points.configure_mock( + mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_sampler_factory", @@ -402,13 +407,14 @@ def test_trace_init_custom_sampler_with_env_bad_factory( ) ] ) + sampler_name = _get_sampler() sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsNone(provider.sampler) - @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict( "os.environ", { @@ -417,9 +423,9 @@ def test_trace_init_custom_sampler_with_env_bad_factory( }, ) def test_trace_init_custom_sampler_with_env_unused_arg( - self, mock_iter_entry_points + self, mock_entry_points ): - mock_iter_entry_points.configure_mock( + mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_sampler_factory", @@ -427,13 +433,14 @@ def test_trace_init_custom_sampler_with_env_unused_arg( ) ] ) + sampler_name = _get_sampler() sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider.sampler, CustomSampler) - @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict( "os.environ", { @@ -441,10 +448,8 @@ def test_trace_init_custom_sampler_with_env_unused_arg( OTEL_TRACES_SAMPLER_ARG: "0.5", }, ) - def test_trace_init_custom_ratio_sampler_with_env( - self, mock_iter_entry_points - ): - mock_iter_entry_points.configure_mock( + def test_trace_init_custom_ratio_sampler_with_env(self, mock_entry_points): + mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_ratio_sampler_factory", @@ -452,6 +457,7 @@ def test_trace_init_custom_ratio_sampler_with_env( ) ] ) + sampler_name = _get_sampler() sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) @@ -459,7 +465,7 @@ def test_trace_init_custom_ratio_sampler_with_env( self.assertIsInstance(provider.sampler, CustomRatioSampler) self.assertEqual(provider.sampler.ratio, 0.5) - @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict( "os.environ", { @@ -468,9 +474,9 @@ def test_trace_init_custom_ratio_sampler_with_env( }, ) def test_trace_init_custom_ratio_sampler_with_env_bad_arg( - self, mock_iter_entry_points + self, mock_entry_points ): - mock_iter_entry_points.configure_mock( + mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_ratio_sampler_factory", @@ -478,13 +484,14 @@ def test_trace_init_custom_ratio_sampler_with_env_bad_arg( ) ] ) + sampler_name = _get_sampler() sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsNone(provider.sampler) - @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict( "os.environ", { @@ -492,9 +499,9 @@ def test_trace_init_custom_ratio_sampler_with_env_bad_arg( }, ) def test_trace_init_custom_ratio_sampler_with_env_missing_arg( - self, mock_iter_entry_points + self, mock_entry_points ): - mock_iter_entry_points.configure_mock( + mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_ratio_sampler_factory", @@ -502,13 +509,14 @@ def test_trace_init_custom_ratio_sampler_with_env_missing_arg( ) ] ) + sampler_name = _get_sampler() sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsNone(provider.sampler) - @patch("opentelemetry.sdk._configuration.iter_entry_points") + @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict( "os.environ", { @@ -517,24 +525,17 @@ def test_trace_init_custom_ratio_sampler_with_env_missing_arg( }, ) def test_trace_init_custom_ratio_sampler_with_env_multiple_entry_points( - self, mock_iter_entry_points + self, mock_entry_points ): - mock_iter_entry_points.configure_mock( + mock_entry_points.configure_mock( return_value=[ - IterEntryPoint( - "custom_ratio_sampler_factory", - CustomSamplerFactory.get_custom_ratio_sampler, - ), IterEntryPoint( "custom_sampler_factory", CustomSamplerFactory.get_custom_sampler, ), - IterEntryPoint( - "custom_z_sampler_factory", - CustomSamplerFactory.empty_get_custom_sampler, - ), ] ) + sampler_name = _get_sampler() sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) @@ -569,7 +570,7 @@ def tearDown(self): self.processor_patch.stop() self.set_provider_patch.stop() self.provider_patch.stop() - root_logger = logging.getLogger("root") + root_logger = getLogger("root") root_logger.handlers = [ handler for handler in root_logger.handlers @@ -605,7 +606,7 @@ def test_logging_init_exporter(self): self.assertIsInstance( provider.processor.exporter, DummyOTLPLogExporter ) - logging.getLogger(__name__).error("hello") + getLogger(__name__).error("hello") self.assertTrue(provider.processor.exporter.export_called) @patch.dict( @@ -782,3 +783,34 @@ def test_console_exporters(self): metric_exporterts["console"].__class__, ConsoleMetricExporter.__class__, ) + + +class TestImportConfigComponents(TestCase): + @patch( + "opentelemetry.sdk._configuration.entry_points", + **{"side_effect": KeyError}, + ) + def test__import_config_components_missing_entry_point( + self, mock_entry_points + ): + + with raises(RuntimeError) as error: + _import_config_components(["a", "b", "c"], "name") + self.assertEqual( + str(error.value), "Requested entry point 'name' not found" + ) + + @patch( + "opentelemetry.sdk._configuration.entry_points", + **{"side_effect": StopIteration}, + ) + def test__import_config_components_missing_component( + self, mock_entry_points + ): + + with raises(RuntimeError) as error: + _import_config_components(["a", "b", "c"], "name") + self.assertEqual( + str(error.value), + "Requested component 'a' not found in entry point 'name'", + ) diff --git a/opentelemetry-semantic-conventions/tests/test_semconv.py b/opentelemetry-semantic-conventions/tests/test_semconv.py index f8e827145a..a7362a8af7 100644 --- a/opentelemetry-semantic-conventions/tests/test_semconv.py +++ b/opentelemetry-semantic-conventions/tests/test_semconv.py @@ -13,15 +13,12 @@ # limitations under the License. # type: ignore +from importlib.util import find_spec from unittest import TestCase -from pkg_resources import DistributionNotFound, require - class TestSemanticConventions(TestCase): def test_semantic_conventions(self): - try: - require(["opentelemetry-semantic-conventions"]) - except DistributionNotFound: + if find_spec("opentelemetry.semconv") is None: self.fail("opentelemetry-semantic-conventions not installed") diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py index 131bb70b91..0419ab44a2 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py @@ -1,12 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import asyncio +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import stop_loop_when class TestAsyncio(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.loop = asyncio.get_event_loop() diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py index c8d490063b..4e76c87a03 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py @@ -1,11 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from concurrent.futures import ThreadPoolExecutor +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase class TestThreads(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() # use max_workers=3 as a general example even if only one would suffice self.executor = ThreadPoolExecutor(max_workers=3) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py index d76fffe3b3..adf99e76b2 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py @@ -1,8 +1,23 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import asyncio import opentracing from opentracing.ext import tags +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_logger, get_one_by_tag, stop_loop_when @@ -50,7 +65,7 @@ async def send(self): class TestAsyncio(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.queue = asyncio.Queue() self.loop = asyncio.get_event_loop() diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_threads.py index df382c3463..6fa5974d79 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/test_threads.py @@ -1,9 +1,24 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from queue import Queue from threading import Thread import opentracing from opentracing.ext import tags +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import await_until, get_logger, get_one_by_tag @@ -52,7 +67,7 @@ def send(self): class TestThreads(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.queue = Queue() self.server = Server(tracer=self.tracer, queue=self.queue) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py index 22d59fbca6..b48a5dbc68 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py @@ -1,5 +1,20 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from opentracing.ext import tags +# pylint: disable=import-error from ..utils import get_logger logger = get_logger(__name__) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py index 14958418a3..58970a223c 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py @@ -1,7 +1,22 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import asyncio from opentracing.ext import tags +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_logger, get_one_by_operation_name, stop_loop_when @@ -45,7 +60,7 @@ class TestAsyncio(OpenTelemetryTestCase): So one issue here is setting correct parent span. """ - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.loop = asyncio.get_event_loop() self.client = Client(RequestHandler(self.tracer), self.loop) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py index 6f5022ccc1..fdc0549d62 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py @@ -1,7 +1,22 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from concurrent.futures import ThreadPoolExecutor from opentracing.ext import tags +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_logger, get_one_by_operation_name @@ -45,7 +60,7 @@ class TestThreads(OpenTelemetryTestCase): activate span. So one issue here is setting correct parent span. """ - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.executor = ThreadPoolExecutor(max_workers=3) self.client = Client(RequestHandler(self.tracer), self.executor) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py index 86a47c6a73..d27e51ca88 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py @@ -1,5 +1,20 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import asyncio +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_logger, stop_loop_when @@ -8,7 +23,7 @@ class TestAsyncio(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.loop = asyncio.get_event_loop() diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py index de8acb70bf..2cd43d7e70 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py @@ -1,12 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import time from concurrent.futures import ThreadPoolExecutor +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase class TestThreads(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.executor = ThreadPoolExecutor(max_workers=3) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py index 66999f04bf..d0f0a6a577 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py @@ -1,7 +1,22 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import asyncio from opentracing.ext import tags +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_one_by_tag @@ -28,7 +43,7 @@ def send_sync(self, message): class TestAsyncio(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.loop = asyncio.get_event_loop() diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py index b810db1f01..39d0a3d1d4 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py @@ -1,7 +1,22 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from concurrent.futures import ThreadPoolExecutor from opentracing.ext import tags +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_one_by_tag @@ -28,7 +43,7 @@ def send_sync(self, message): class TestThreads(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() def test_main(self): diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py index 3754367878..bbfb620a84 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py @@ -1,6 +1,21 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import asyncio import random +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_logger, stop_loop_when @@ -10,7 +25,7 @@ class TestAsyncio(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.loop = asyncio.get_event_loop() diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py index dfb567c745..6e8b405cce 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py @@ -1,7 +1,22 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import random import time from concurrent.futures import ThreadPoolExecutor +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import RefCount, get_logger @@ -11,7 +26,7 @@ class TestThreads(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.executor = ThreadPoolExecutor(max_workers=3) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py index b6b8277d38..f00258624c 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py @@ -1,12 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import asyncio +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import stop_loop_when class TestAsyncio(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.loop = asyncio.get_event_loop() diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py index 1f86b2dfba..955298537d 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py @@ -1,16 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from concurrent.futures import ThreadPoolExecutor +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import await_until class TestThreads(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.executor = ThreadPoolExecutor(max_workers=3) - def tearDown(self): + def tearDown(self): # pylint: disable=invalid-name self.executor.shutdown(False) def test_main(self): diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py index bd08ee6d09..653f9bd810 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py @@ -1,11 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import asyncio +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase class TestAsyncio(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.loop = asyncio.get_event_loop() diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py index ea15aafb9a..0d003c9062 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py @@ -1,11 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from concurrent.futures import ThreadPoolExecutor +# pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase class TestThreads(OpenTelemetryTestCase): - def setUp(self): + def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.executor = ThreadPoolExecutor(max_workers=3) diff --git a/tox.ini b/tox.ini index 147931eb6b..fd357987a6 100644 --- a/tox.ini +++ b/tox.ini @@ -198,7 +198,7 @@ commands = codespell [testenv:lint] -basepython: python3.9 +basepython: python3.10 recreate = True deps = -c dev-requirements.txt From 4286d63d6f9cd0acbf6fbe3fd35163cec79d7ade Mon Sep 17 00:00:00 2001 From: Mark Jan van Kampen Date: Thu, 2 Feb 2023 19:30:30 +0100 Subject: [PATCH 1376/1517] Fix bagage key lowercasing (#3151) --- CHANGELOG.md | 2 + .../baggage/propagation/__init__.py | 2 +- .../baggage/propagation/test_propagation.py | 39 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 opentelemetry-api/tests/baggage/propagation/test_propagation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 28650a4b3f..bb1666d185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3128](https://github.com/open-telemetry/opentelemetry-python/pull/3128)) - Fix validation of baggage values ([#3058](https://github.com/open-telemetry/opentelemetry-python/pull/3058)) +- Fix capitalization of baggage keys + ([#3151](https://github.com/open-telemetry/opentelemetry-python/pull/3151)) ## Version 1.15.0/0.36b0 (2022-12-09) diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index ecdc1eab43..91898d53ae 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -93,7 +93,7 @@ def extract( _logger.warning("Invalid baggage entry: `%s`", entry) continue - name = unquote_plus(name).strip().lower() + name = unquote_plus(name).strip() value = unquote_plus(value).strip() context = set_baggage( diff --git a/opentelemetry-api/tests/baggage/propagation/test_propagation.py b/opentelemetry-api/tests/baggage/propagation/test_propagation.py new file mode 100644 index 0000000000..b9de7f37b3 --- /dev/null +++ b/opentelemetry-api/tests/baggage/propagation/test_propagation.py @@ -0,0 +1,39 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# type: ignore + +from unittest import TestCase + +from opentelemetry.baggage import get_baggage, set_baggage +from opentelemetry.baggage.propagation import W3CBaggagePropagator + + +class TestBaggageManager(TestCase): + def test_propagate_baggage(self): + carrier = {} + propagator = W3CBaggagePropagator() + + ctx = set_baggage("Test1", "value1") + ctx = set_baggage("test2", "value2", context=ctx) + + propagator.inject(carrier, ctx) + ctx_propagated = propagator.extract(carrier) + + self.assertEqual( + get_baggage("Test1", context=ctx_propagated), "value1" + ) + self.assertEqual( + get_baggage("test2", context=ctx_propagated), "value2" + ) From 9551678ef37b6139589d7fc6518fa5926c85d2cf Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 3 Feb 2023 04:32:06 +0530 Subject: [PATCH 1377/1517] Update docs/examples/logs/otel-collector-config.yaml (#3116) --- docs/examples/logs/otel-collector-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/examples/logs/otel-collector-config.yaml b/docs/examples/logs/otel-collector-config.yaml index 71a97bf7c2..6c87a2e847 100644 --- a/docs/examples/logs/otel-collector-config.yaml +++ b/docs/examples/logs/otel-collector-config.yaml @@ -5,6 +5,7 @@ receivers: exporters: logging: + loglevel: debug processors: batch: From 6544004456e827cacc86d70b27783cf12f4a3e78 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 3 Feb 2023 09:42:17 +0530 Subject: [PATCH 1378/1517] Revert "Suppress warning for SDK instantiation of InstrumentationInfo (#3041)" (#3157) This reverts commit a11c0ccf416c4fde4dfeb2a970c72e4701ad0695. Co-authored-by: Diego Hurtado --- .../src/opentelemetry/sdk/trace/__init__.py | 15 +++++---------- opentelemetry-sdk/tests/trace/test_trace.py | 8 +------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 739f59ee70..0ddf531a4d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -37,7 +37,6 @@ Type, Union, ) -from warnings import filterwarnings, resetwarnings from deprecated import deprecated @@ -1168,20 +1167,16 @@ def get_tracer( logger.error("get_tracer called with missing module name.") if instrumenting_library_version is None: instrumenting_library_version = "" - - filterwarnings("ignore", category=DeprecationWarning) - instrumentation_info = InstrumentationInfo( - instrumenting_module_name, - instrumenting_library_version, - schema_url, - ) - resetwarnings() return Tracer( self.sampler, self.resource, self._active_span_processor, self.id_generator, - instrumentation_info, + InstrumentationInfo( + instrumenting_module_name, + instrumenting_library_version, + schema_url, + ), self._span_limits, InstrumentationScope( instrumenting_module_name, diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 7b86fa7247..5bb8d87c65 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -23,7 +23,6 @@ from time import time_ns from typing import Optional from unittest import mock -from unittest.mock import Mock from opentelemetry import trace as trace_api from opentelemetry.context import Context @@ -40,7 +39,7 @@ OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) -from opentelemetry.sdk.trace import Resource, TracerProvider +from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.trace.sampling import ( ALWAYS_OFF, @@ -59,11 +58,6 @@ class TestTracer(unittest.TestCase): - def test_no_deprecated_warning(self): - with self.assertRaises(AssertionError): - with self.assertWarns(DeprecationWarning): - TracerProvider(Mock(), Mock()).get_tracer(Mock(), Mock()) - def test_extends_api(self): tracer = new_tracer() self.assertIsInstance(tracer, trace.Tracer) From 3a1c3b02a9e5add51cd605c048c16ab477172316 Mon Sep 17 00:00:00 2001 From: Howard Yoo <32691630+howardyoo@users.noreply.github.com> Date: Fri, 3 Feb 2023 09:19:31 -0600 Subject: [PATCH 1379/1517] PeriodicExportingMetricsReader with value = infinity to support explicit metric collection (#3059) --- CHANGELOG.md | 3 ++ .../sdk/metrics/_internal/export/__init__.py | 39 +++++++++++++------ .../test_periodic_exporting_metric_reader.py | 27 +++++++++++++ 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb1666d185..9044795b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Version 1.15.0/0.36b0 (2022-12-09) +- PeriodicExportingMetricsReader with +Inf interval + to support explicit metric collection + ([#3059](https://github.com/open-telemetry/opentelemetry-python/pull/3059)) - Regenerate opentelemetry-proto to be compatible with protobuf 3 and 4 ([#3070](https://github.com/open-telemetry/opentelemetry-python/pull/3070)) - Rename parse_headers to parse_env_headers and improve error message diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 591f7e8a88..0f70ee05d1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -23,6 +23,7 @@ from typing import IO, Callable, Dict, Iterable, Optional from typing_extensions import final +import math # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics._internal @@ -414,7 +415,8 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: class PeriodicExportingMetricReader(MetricReader): """`PeriodicExportingMetricReader` is an implementation of `MetricReader` that collects metrics based on a user-configurable time interval, and passes the - metrics to the configured exporter. + metrics to the configured exporter. If the time interval is set to `math.inf`, the + reader will not invoke periodic collection. The configured exporter's :py:meth:`~MetricExporter.export` method will not be called concurrently. @@ -463,16 +465,26 @@ def __init__( self._shutdown = False self._shutdown_event = Event() self._shutdown_once = Once() - self._daemon_thread = Thread( - name="OtelPeriodicExportingMetricReader", - target=self._ticker, - daemon=True, - ) - self._daemon_thread.start() - if hasattr(os, "register_at_fork"): - os.register_at_fork( - after_in_child=self._at_fork_reinit - ) # pylint: disable=protected-access + self._daemon_thread = None + if ( + self._export_interval_millis > 0 + and self._export_interval_millis < math.inf + ): + self._daemon_thread = Thread( + name="OtelPeriodicExportingMetricReader", + target=self._ticker, + daemon=True, + ) + self._daemon_thread.start() + if hasattr(os, "register_at_fork"): + os.register_at_fork( + after_in_child=self._at_fork_reinit + ) # pylint: disable=protected-access + elif self._export_interval_millis <= 0: + raise ValueError( + f"interval value {self._export_interval_millis} is invalid \ + and needs to be larger than zero and lower than infinity." + ) def _at_fork_reinit(self): self._daemon_thread = Thread( @@ -519,7 +531,10 @@ def _shutdown(): return self._shutdown_event.set() - self._daemon_thread.join(timeout=(deadline_ns - time_ns()) / 10**9) + if self._daemon_thread: + self._daemon_thread.join( + timeout=(deadline_ns - time_ns()) / 10**9 + ) self._exporter.shutdown(timeout=(deadline_ns - time_ns()) / 10**6) def force_flush(self, timeout_millis: float = 10_000) -> bool: diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index b95c1d8806..3741816187 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -17,6 +17,7 @@ from unittest.mock import Mock from flaky import flaky +import math from opentelemetry.sdk.metrics import Counter from opentelemetry.sdk.metrics._internal import _Counter @@ -134,6 +135,32 @@ def test_ticker_called(self): self.assertTrue(collect_mock.assert_called_once) pmr.shutdown() + def test_ticker_not_called_on_infinity(self): + collect_mock = Mock() + exporter = FakeMetricsExporter() + exporter.export = Mock() + pmr = PeriodicExportingMetricReader(exporter, export_interval_millis=math.inf) + pmr._set_collect_callback(collect_mock) + sleep(0.1) + self.assertTrue(collect_mock.assert_not_called) + pmr.shutdown() + + def test_ticker_value_exception_on_zero(self): + exporter = FakeMetricsExporter() + exporter.export = Mock() + self.assertRaises( + ValueError, PeriodicExportingMetricReader, + exporter, export_interval_millis=0 + ) + + def test_ticker_value_exception_on_negative(self): + exporter = FakeMetricsExporter() + exporter.export = Mock() + self.assertRaises( + ValueError, PeriodicExportingMetricReader, + exporter, export_interval_millis=-100 + ) + @flaky(max_runs=3, min_passes=1) def test_ticker_collects_metrics(self): exporter = FakeMetricsExporter() From e0e6a3a940c16c1df6493e258ccfbc57ac38cf96 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Sat, 4 Feb 2023 01:04:01 +0530 Subject: [PATCH 1380/1517] Bump min required api version for OTLP exporters (#3156) --- CHANGELOG.md | 2 ++ exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml | 2 +- exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9044795b02..bb75973701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3058](https://github.com/open-telemetry/opentelemetry-python/pull/3058)) - Fix capitalization of baggage keys ([#3151](https://github.com/open-telemetry/opentelemetry-python/pull/3151)) +- Bump min required api version for OTLP exporters + ([#3156](https://github.com/open-telemetry/opentelemetry-python/pull/3156)) ## Version 1.15.0/0.36b0 (2022-12-09) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index 8a07a45bb6..36401ac106 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "grpcio >= 1.0.0, < 2.0.0", - "opentelemetry-api ~= 1.12", + "opentelemetry-api ~= 1.15", "opentelemetry-proto == 1.16.0.dev", "opentelemetry-sdk ~= 1.16.0.dev", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index aec6afe1fc..b3a12fc91d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "backoff >= 1.10.0, < 2.0.0; python_version<'3.7'", "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", - "opentelemetry-api ~= 1.12", + "opentelemetry-api ~= 1.15", "opentelemetry-proto == 1.16.0.dev", "opentelemetry-sdk ~= 1.16.0.dev", "requests ~= 2.7", From 209093bfbe3fae0fc29c693f55d6bb6a2d997aad Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 6 Feb 2023 18:23:39 +0530 Subject: [PATCH 1381/1517] Create a single resource instance (#3118) --- CHANGELOG.md | 3 + .../sdk/_configuration/__init__.py | 55 +++++++----------- opentelemetry-sdk/tests/test_configurator.py | 56 +++++++++++++++++-- 3 files changed, 73 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb75973701..13e047c366 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump min required api version for OTLP exporters ([#3156](https://github.com/open-telemetry/opentelemetry-python/pull/3156)) +- Create a single resource instance + ([#3118](https://github.com/open-telemetry/opentelemetry-python/pull/3118)) + ## Version 1.15.0/0.36b0 (2022-12-09) - PeriodicExportingMetricsReader with +Inf interval diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index a65ed85d84..c0156decaf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -193,20 +193,12 @@ def _init_tracing( exporters: Dict[str, Type[SpanExporter]], id_generator: IdGenerator = None, sampler: Sampler = None, - auto_instrumentation_version: Optional[str] = None, + resource: Resource = None, ): - # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name - # from the env variable else defaults to "unknown_service" - auto_resource = {} - # populate version if using auto-instrumentation - if auto_instrumentation_version: - auto_resource[ - ResourceAttributes.TELEMETRY_AUTO_VERSION - ] = auto_instrumentation_version provider = TracerProvider( id_generator=id_generator, sampler=sampler, - resource=Resource.create(auto_resource), + resource=resource, ) set_tracer_provider(provider) @@ -219,17 +211,8 @@ def _init_tracing( def _init_metrics( exporters: Dict[str, Type[MetricExporter]], - auto_instrumentation_version: Optional[str] = None, + resource: Resource = None, ): - # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name - # from the env variable else defaults to "unknown_service" - auto_resource = {} - # populate version if using auto-instrumentation - if auto_instrumentation_version: - auto_resource[ - ResourceAttributes.TELEMETRY_AUTO_VERSION - ] = auto_instrumentation_version - metric_readers = [] for _, exporter_class in exporters.items(): @@ -238,25 +221,15 @@ def _init_metrics( PeriodicExportingMetricReader(exporter_class(**exporter_args)) ) - provider = MeterProvider( - resource=Resource.create(auto_resource), metric_readers=metric_readers - ) + provider = MeterProvider(resource=resource, metric_readers=metric_readers) set_meter_provider(provider) def _init_logging( exporters: Dict[str, Type[LogExporter]], - auto_instrumentation_version: Optional[str] = None, + resource: Resource = None, ): - # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name - # from the env variable else defaults to "unknown_service" - auto_resource = {} - # populate version if using auto-instrumentation - if auto_instrumentation_version: - auto_resource[ - ResourceAttributes.TELEMETRY_AUTO_VERSION - ] = auto_instrumentation_version - provider = LoggerProvider(resource=Resource.create(auto_resource)) + provider = LoggerProvider(resource=resource) set_logger_provider(provider) for _, exporter_class in exporters.items(): @@ -359,18 +332,28 @@ def _initialize_components(auto_instrumentation_version): sampler = _import_sampler(sampler_name) id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) + # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name + # from the env variable else defaults to "unknown_service" + auto_resource = {} + # populate version if using auto-instrumentation + if auto_instrumentation_version: + auto_resource[ + ResourceAttributes.TELEMETRY_AUTO_VERSION + ] = auto_instrumentation_version + resource = Resource.create(auto_resource) + _init_tracing( exporters=trace_exporters, id_generator=id_generator, sampler=sampler, - auto_instrumentation_version=auto_instrumentation_version, + resource=resource, ) - _init_metrics(metric_exporters, auto_instrumentation_version) + _init_metrics(metric_exporters, resource) logging_enabled = os.getenv( _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "false" ) if logging_enabled.strip().lower() == "true": - _init_logging(log_exporters, auto_instrumentation_version) + _init_logging(log_exporters, resource) class _BaseConfigurator(ABC): diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index e07c5e3c78..aa1c58cb51 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -309,10 +309,16 @@ def tearDown(self): environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=my-test-service"} ) def test_trace_init_default(self): + + auto_resource = Resource.create( + { + "telemetry.auto.version": "test-version", + } + ) _init_tracing( {"zipkin": Exporter}, id_generator=RandomIdGenerator(), - auto_instrumentation_version="test-version", + resource=auto_resource, ) self.assertEqual(self.set_provider_mock.call_count, 1) @@ -578,7 +584,12 @@ def tearDown(self): ] def test_logging_init_empty(self): - _init_logging({}, "auto-version") + auto_resource = Resource.create( + { + "telemetry.auto.version": "auto-version", + } + ) + _init_logging({}, resource=auto_resource) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyLoggerProvider) @@ -593,7 +604,8 @@ def test_logging_init_empty(self): {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, ) def test_logging_init_exporter(self): - _init_logging({"otlp": DummyOTLPLogExporter}) + resource = Resource.create({}) + _init_logging({"otlp": DummyOTLPLogExporter}, resource=resource) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyLoggerProvider) @@ -634,6 +646,34 @@ def test_logging_init_enable_env(self, logging_mock, tracing_mock): self.assertEqual(logging_mock.call_count, 1) self.assertEqual(tracing_mock.call_count, 1) + @patch.dict( + environ, + { + "OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service", + "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED": "True", + }, + ) + @patch("opentelemetry.sdk._configuration._init_tracing") + @patch("opentelemetry.sdk._configuration._init_logging") + @patch("opentelemetry.sdk._configuration._init_metrics") + def test_initialize_components_resource( + self, metrics_mock, logging_mock, tracing_mock + ): + _initialize_components("auto-version") + self.assertEqual(logging_mock.call_count, 1) + self.assertEqual(tracing_mock.call_count, 1) + self.assertEqual(metrics_mock.call_count, 1) + + _, args, _ = logging_mock.mock_calls[0] + logging_resource = args[1] + _, _, kwargs = tracing_mock.mock_calls[0] + tracing_resource = kwargs["resource"] + _, args, _ = metrics_mock.mock_calls[0] + metrics_resource = args[1] + self.assertEqual(logging_resource, tracing_resource) + self.assertEqual(logging_resource, metrics_resource) + self.assertEqual(tracing_resource, metrics_resource) + class TestMetricsInit(TestCase): def setUp(self): @@ -659,7 +699,12 @@ def tearDown(self): self.provider_patch.stop() def test_metrics_init_empty(self): - _init_metrics({}, "auto-version") + auto_resource = Resource.create( + { + "telemetry.auto.version": "auto-version", + } + ) + _init_metrics({}, resource=auto_resource) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyMeterProvider) @@ -676,7 +721,8 @@ def test_metrics_init_empty(self): {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, ) def test_metrics_init_exporter(self): - _init_metrics({"otlp": DummyOTLPMetricExporter}) + resource = Resource.create({}) + _init_metrics({"otlp": DummyOTLPMetricExporter}, resource=resource) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyMeterProvider) From 34d11d59e9b37052eb3aa6706288641ecb3056e7 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Mon, 13 Feb 2023 17:14:47 +0100 Subject: [PATCH 1382/1517] Add attribute key to mixed types warning message (#3162) Co-authored-by: Srikanth Chekuri --- opentelemetry-api/src/opentelemetry/attributes/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 0b7d056da9..724c931c82 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -85,7 +85,8 @@ def _clean_attribute( # use equality instead of isinstance as isinstance(True, int) evaluates to True elif element_type != sequence_first_valid_type: _logger.warning( - "Mixed types %s and %s in attribute value sequence", + "Attribute %r mixes types %s and %s in attribute value sequence", + key, sequence_first_valid_type.__name__, type(element).__name__, ) From e1839359942d58ffc88d3bb2f8a8a64b229d7f0a Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 14 Feb 2023 00:00:25 +0530 Subject: [PATCH 1383/1517] update public_symbols_checker.py to detect removed symbols (#3159) * update public_symbols_checker.py to detect removed symbols * Update scripts/public_symbols_checker.py --------- Co-authored-by: Diego Hurtado --- scripts/public_symbols_checker.py | 63 ++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/scripts/public_symbols_checker.py b/scripts/public_symbols_checker.py index d359008f1f..ac8ca060ba 100644 --- a/scripts/public_symbols_checker.py +++ b/scripts/public_symbols_checker.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from collections import defaultdict from difflib import unified_diff from pathlib import Path from re import match @@ -23,10 +24,17 @@ repo = Repo(__file__, odbt=GitDB, search_parent_directories=True) -file_path_symbols = {} +added_symbols = defaultdict(list) +removed_symbols = defaultdict(list) def get_symbols(change_type, diff_lines_getter, prefix): + + if change_type == "D" or prefix == r"\-": + file_path_symbols = removed_symbols + else: + file_path_symbols = added_symbols + for diff_lines in ( repo.commit("main") .diff(repo.head.commit) @@ -60,9 +68,6 @@ def get_symbols(change_type, diff_lines_getter, prefix): ) if matching_line is not None: - if b_file_path not in file_path_symbols.keys(): - file_path_symbols[b_file_path] = [] - file_path_symbols[b_file_path].append( next(filter(bool, matching_line.groups())) ) @@ -71,6 +76,8 @@ def get_symbols(change_type, diff_lines_getter, prefix): def a_diff_lines_getter(diff_lines): return diff_lines.b_blob.data_stream.read().decode("utf-8").split("\n") +def d_diff_lines_getter(diff_lines): + return diff_lines.a_blob.data_stream.read().decode("utf-8").split("\n") def m_diff_lines_getter(diff_lines): return unified_diff( @@ -80,12 +87,42 @@ def m_diff_lines_getter(diff_lines): get_symbols("A", a_diff_lines_getter, r"") +get_symbols("D", d_diff_lines_getter, r"") get_symbols("M", m_diff_lines_getter, r"\+") +get_symbols("M", m_diff_lines_getter, r"\-") + +def remove_common_symbols(): + # For each file, we remove the symbols that are added and removed in the + # same commit. + common_symbols = defaultdict(list) + for file_path, symbols in added_symbols.items(): + for symbol in symbols: + if symbol in removed_symbols[file_path]: + common_symbols[file_path].append(symbol) + + for file_path, symbols in common_symbols.items(): + for symbol in symbols: + added_symbols[file_path].remove(symbol) + removed_symbols[file_path].remove(symbol) + + # If a file has no added or removed symbols, we remove it from the + # dictionaries. + for file_path in list(added_symbols.keys()): + if not added_symbols[file_path]: + del added_symbols[file_path] + + for file_path in list(removed_symbols.keys()): + if not removed_symbols[file_path]: + del removed_symbols[file_path] + +if added_symbols or removed_symbols: -if file_path_symbols: + # If a symbol is added and removed in the same commit, we consider it + # as not added or removed. + remove_common_symbols() print("The code in this branch adds the following public symbols:") print() - for file_path, symbols in file_path_symbols.items(): + for file_path, symbols in added_symbols.items(): print(f"- {file_path}") for symbol in symbols: print(f"\t{symbol}") @@ -97,6 +134,20 @@ def m_diff_lines_getter(diff_lines): 'private. After that, please label this PR with "Skip Public API ' 'check".' ) + print() + print("The code in this branch removes the following public symbols:") + print() + for file_path, symbols in removed_symbols.items(): + print(f"- {file_path}") + for symbol in symbols: + print(f"\t{symbol}") + print() + + print( + "Please make sure no public symbols are removed, if so, please " + "consider deprecating them instead. After that, please label this " + 'PR with "Skip Public API check".' + ) exit(1) else: print("The code in this branch will not add any public symbols") From acc11a08af33cba503301bf704127d332eb4187f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 13 Feb 2023 15:59:06 -0600 Subject: [PATCH 1384/1517] Add line-specific filter to catch warning (#3164) --- .../sdk/metrics/_internal/export/__init__.py | 2 +- .../src/opentelemetry/sdk/trace/__init__.py | 24 +++++++++++++++---- .../test_periodic_exporting_metric_reader.py | 18 +++++++++----- opentelemetry-sdk/tests/trace/test_trace.py | 16 +++++++++++-- 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 0f70ee05d1..836bb8eece 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import math import os from abc import ABC, abstractmethod from enum import Enum @@ -23,7 +24,6 @@ from typing import IO, Callable, Dict, Iterable, Optional from typing_extensions import final -import math # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics._internal diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0ddf531a4d..2d6147a436 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -37,6 +37,7 @@ Type, Union, ) +from warnings import filterwarnings from deprecated import deprecated @@ -1167,16 +1168,29 @@ def get_tracer( logger.error("get_tracer called with missing module name.") if instrumenting_library_version is None: instrumenting_library_version = "" + + filterwarnings( + "ignore", + message=( + r"Call to deprecated method __init__. \(You should use " + r"InstrumentationScope\) -- Deprecated since version 1.11.1." + ), + category=DeprecationWarning, + module="opentelemetry.sdk.trace", + ) + + instrumentation_info = InstrumentationInfo( + instrumenting_module_name, + instrumenting_library_version, + schema_url, + ) + return Tracer( self.sampler, self.resource, self._active_span_processor, self.id_generator, - InstrumentationInfo( - instrumenting_module_name, - instrumenting_library_version, - schema_url, - ), + instrumentation_info, self._span_limits, InstrumentationScope( instrumenting_module_name, diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index 3741816187..532f25b9e7 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -12,12 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import math from time import sleep, time_ns from typing import Sequence from unittest.mock import Mock from flaky import flaky -import math from opentelemetry.sdk.metrics import Counter from opentelemetry.sdk.metrics._internal import _Counter @@ -139,7 +139,9 @@ def test_ticker_not_called_on_infinity(self): collect_mock = Mock() exporter = FakeMetricsExporter() exporter.export = Mock() - pmr = PeriodicExportingMetricReader(exporter, export_interval_millis=math.inf) + pmr = PeriodicExportingMetricReader( + exporter, export_interval_millis=math.inf + ) pmr._set_collect_callback(collect_mock) sleep(0.1) self.assertTrue(collect_mock.assert_not_called) @@ -149,16 +151,20 @@ def test_ticker_value_exception_on_zero(self): exporter = FakeMetricsExporter() exporter.export = Mock() self.assertRaises( - ValueError, PeriodicExportingMetricReader, - exporter, export_interval_millis=0 + ValueError, + PeriodicExportingMetricReader, + exporter, + export_interval_millis=0, ) def test_ticker_value_exception_on_negative(self): exporter = FakeMetricsExporter() exporter.export = Mock() self.assertRaises( - ValueError, PeriodicExportingMetricReader, - exporter, export_interval_millis=-100 + ValueError, + PeriodicExportingMetricReader, + exporter, + export_interval_millis=-100, ) @flaky(max_runs=3, min_passes=1) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 5bb8d87c65..c0b192192f 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -23,6 +23,7 @@ from time import time_ns from typing import Optional from unittest import mock +from unittest.mock import Mock from opentelemetry import trace as trace_api from opentelemetry.context import Context @@ -39,7 +40,7 @@ OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) -from opentelemetry.sdk.trace import Resource +from opentelemetry.sdk.trace import Resource, TracerProvider from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.trace.sampling import ( ALWAYS_OFF, @@ -48,7 +49,7 @@ ParentBased, StaticSampler, ) -from opentelemetry.sdk.util import ns_to_iso_str +from opentelemetry.sdk.util import BoundedDict, ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, @@ -58,6 +59,17 @@ class TestTracer(unittest.TestCase): + def test_no_deprecated_warning(self): + with self.assertRaises(AssertionError): + with self.assertWarns(DeprecationWarning): + TracerProvider(Mock(), Mock()).get_tracer(Mock(), Mock()) + + # This is being added here to make sure the filter on + # InstrumentationInfo does not affect other DeprecationWarnings that + # may be raised. + with self.assertWarns(DeprecationWarning): + BoundedDict(0) + def test_extends_api(self): tracer = new_tracer() self.assertIsInstance(tracer, trace.Tracer) From 363a3b9a9f5f11f0daa1481ea0e785edd335d87e Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 14 Feb 2023 03:47:39 +0530 Subject: [PATCH 1385/1517] deprecate jaeger exporters (#3158) * deprecate jaeger exporters * correct version * Add CHANGELOG entry * Add deprecation message to README --- CHANGELOG.md | 2 ++ .../opentelemetry-exporter-jaeger-proto-grpc/README.rst | 4 ++++ .../opentelemetry/exporter/jaeger/proto/grpc/__init__.py | 5 +++++ exporter/opentelemetry-exporter-jaeger-thrift/README.rst | 4 ++++ .../src/opentelemetry/exporter/jaeger/thrift/__init__.py | 6 ++++++ exporter/opentelemetry-exporter-jaeger/README.rst | 4 ++++ 6 files changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13e047c366..f562971aeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3151](https://github.com/open-telemetry/opentelemetry-python/pull/3151)) - Bump min required api version for OTLP exporters ([#3156](https://github.com/open-telemetry/opentelemetry-python/pull/3156)) +- deprecate jaeger exporters + ([#3158](https://github.com/open-telemetry/opentelemetry-python/pull/3158)) - Create a single resource instance ([#3118](https://github.com/open-telemetry/opentelemetry-python/pull/3118)) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst b/exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst index 471e92085b..3f170bb12a 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst @@ -1,6 +1,10 @@ OpenTelemetry Jaeger Protobuf Exporter ====================================== +.. warning:: + Since v1.35, the Jaeger supports OTLP natively. Please use the OTLP exporter instead. + Support for this exporter will end July 2023. + |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger-proto-grpc.svg diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py index 3f623327af..f08b20e726 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py @@ -68,6 +68,7 @@ import logging from os import environ from typing import Optional +from deprecated import deprecated from grpc import ChannelCredentials, RpcError, insecure_channel, secure_channel @@ -110,6 +111,10 @@ class JaegerExporter(SpanExporter): timeout: Maximum time the Jaeger exporter should wait for each batch export. """ + @deprecated( + version="1.16.0", + reason="Since v1.35, the Jaeger supports OTLP natively. Please use the OTLP exporter instead. Support for this exporter will end July 2023.", + ) def __init__( self, collector_endpoint: Optional[str] = None, diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/README.rst b/exporter/opentelemetry-exporter-jaeger-thrift/README.rst index ed6bb1d792..1b01237ce4 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/README.rst +++ b/exporter/opentelemetry-exporter-jaeger-thrift/README.rst @@ -1,6 +1,10 @@ OpenTelemetry Jaeger Thrift Exporter ==================================== +.. warning:: + Since v1.35, the Jaeger supports OTLP natively. Please use the OTLP exporter instead. + Support for this exporter will end July 2023. + |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger-thrift.svg diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py index a4f8e36bb9..95181ad617 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py @@ -85,6 +85,8 @@ from os import environ from typing import Optional +from deprecated import deprecated + from opentelemetry import trace from opentelemetry.exporter.jaeger.thrift.gen.jaeger import ( Collector as jaeger_thrift, @@ -130,6 +132,10 @@ class JaegerExporter(SpanExporter): timeout: Maximum time the Jaeger exporter should wait for each batch export. """ + @deprecated( + version="1.16.0", + reason="Since v1.35, the Jaeger supports OTLP natively. Please use the OTLP exporter instead. Support for this exporter will end July 2023.", + ) def __init__( self, agent_host_name: Optional[str] = None, diff --git a/exporter/opentelemetry-exporter-jaeger/README.rst b/exporter/opentelemetry-exporter-jaeger/README.rst index 00154142ec..43d0f76d0e 100644 --- a/exporter/opentelemetry-exporter-jaeger/README.rst +++ b/exporter/opentelemetry-exporter-jaeger/README.rst @@ -1,6 +1,10 @@ OpenTelemetry Jaeger Exporter ============================= +.. warning:: + Since v1.35, the Jaeger supports OTLP natively. Please use the OTLP exporter instead. + Support for this exporter will end July 2023. + |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger.svg From 6785335aca009e45d64918f524c57a7e08d5415d Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 13 Feb 2023 17:17:42 -0800 Subject: [PATCH 1386/1517] Replace old BOT_TOKEN with org OPENTELEMETRYBOT_GITHUB_TOKEN (#3170) --- .github/workflows/backport.yml | 2 +- .github/workflows/prepare-patch-release.yml | 2 +- .github/workflows/prepare-release-branch.yml | 4 ++-- .github/workflows/release.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index a9a7fdab6d..9510b88fc8 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -28,7 +28,7 @@ jobs: env: NUMBER: ${{ github.event.inputs.number }} # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.OPENTELEMETRYBOT_GITHUB_TOKEN }} run: | commit=$(gh pr view $NUMBER --json mergeCommit --jq .mergeCommit.oid) title=$(gh pr view $NUMBER --json title --jq .title) diff --git a/.github/workflows/prepare-patch-release.yml b/.github/workflows/prepare-patch-release.yml index c215b7c9e4..30fa2d15ad 100644 --- a/.github/workflows/prepare-patch-release.yml +++ b/.github/workflows/prepare-patch-release.yml @@ -60,7 +60,7 @@ jobs: - name: Create pull request env: # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.OPENTELEMETRYBOT_GITHUB_TOKEN }} run: | message="Prepare release ${STABLE_VERSION}/${UNSTABLE_VERSION}" branch="opentelemetrybot/prepare-release-${STABLE_VERSION}-${UNSTABLE_VERSION}" diff --git a/.github/workflows/prepare-release-branch.yml b/.github/workflows/prepare-release-branch.yml index c77a96c06a..59c86b62de 100644 --- a/.github/workflows/prepare-release-branch.yml +++ b/.github/workflows/prepare-release-branch.yml @@ -87,7 +87,7 @@ jobs: - name: Create pull request against the release branch env: # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.OPENTELEMETRYBOT_GITHUB_TOKEN }} run: | message="Prepare release ${STABLE_VERSION}/${UNSTABLE_VERSION}" branch="opentelemetrybot/prepare-release-${STABLE_VERSION}-${UNSTABLE_VERSION}" @@ -163,7 +163,7 @@ jobs: - name: Create pull request against main env: # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.OPENTELEMETRYBOT_GITHUB_TOKEN }} run: | message="Update version to ${STABLE_NEXT_VERSION}/${UNSTABLE_NEXT_VERSION}" body="Update version to \`${STABLE_NEXT_VERSION}/${UNSTABLE_NEXT_VERSION}\`." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd822518f6..4420b599b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -184,7 +184,7 @@ jobs: - name: Create pull request against main env: # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.OPENTELEMETRYBOT_GITHUB_TOKEN }} run: | message="Copy change log updates from $GITHUB_REF_NAME" body="Copy log updates from \`$GITHUB_REF_NAME\`." From 0f7676975a883c5e637e277eaac09e528e137f96 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 14 Feb 2023 12:23:01 -0600 Subject: [PATCH 1387/1517] Update some test environments to 3.10 (#3149) * Update some test environments to 3.10 Fixes #3148 * Update environments in workflows file as well --- .github/workflows/public-api-check.yml | 2 +- .github/workflows/test.yml | 2 +- scripts/public_symbols_checker.py | 20 ++++++++++++-------- tox.ini | 6 +++--- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/workflows/public-api-check.yml b/.github/workflows/public-api-check.yml index eead4fa368..46432af0b5 100644 --- a/.github/workflows/public-api-check.yml +++ b/.github/workflows/public-api-check.yml @@ -31,7 +31,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: '3.10' - name: Install tox run: pip install tox==3.27.1 -U tox-factor diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0951b7afd..b2dbeb6e77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -101,7 +101,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: '3.10' architecture: 'x64' - name: Install tox run: pip install tox==3.27.1 diff --git a/scripts/public_symbols_checker.py b/scripts/public_symbols_checker.py index ac8ca060ba..c485c4740e 100644 --- a/scripts/public_symbols_checker.py +++ b/scripts/public_symbols_checker.py @@ -76,9 +76,11 @@ def get_symbols(change_type, diff_lines_getter, prefix): def a_diff_lines_getter(diff_lines): return diff_lines.b_blob.data_stream.read().decode("utf-8").split("\n") + def d_diff_lines_getter(diff_lines): return diff_lines.a_blob.data_stream.read().decode("utf-8").split("\n") + def m_diff_lines_getter(diff_lines): return unified_diff( diff_lines.a_blob.data_stream.read().decode("utf-8").split("\n"), @@ -91,6 +93,7 @@ def m_diff_lines_getter(diff_lines): get_symbols("M", m_diff_lines_getter, r"\+") get_symbols("M", m_diff_lines_getter, r"\-") + def remove_common_symbols(): # For each file, we remove the symbols that are added and removed in the # same commit. @@ -115,6 +118,7 @@ def remove_common_symbols(): if not removed_symbols[file_path]: del removed_symbols[file_path] + if added_symbols or removed_symbols: # If a symbol is added and removed in the same commit, we consider it @@ -122,10 +126,10 @@ def remove_common_symbols(): remove_common_symbols() print("The code in this branch adds the following public symbols:") print() - for file_path, symbols in added_symbols.items(): - print(f"- {file_path}") - for symbol in symbols: - print(f"\t{symbol}") + for file_path_, symbols_ in added_symbols.items(): + print(f"- {file_path_}") + for symbol_ in symbols_: + print(f"\t{symbol_}") print() print( @@ -137,10 +141,10 @@ def remove_common_symbols(): print() print("The code in this branch removes the following public symbols:") print() - for file_path, symbols in removed_symbols.items(): - print(f"- {file_path}") - for symbol in symbols: - print(f"\t{symbol}") + for file_path_, symbols_ in removed_symbols.items(): + print(f"- {file_path_}") + for symbol_ in symbols_: + print(f"\t{symbol_}") print() print( diff --git a/tox.ini b/tox.ini index fd357987a6..a757f2dd02 100644 --- a/tox.ini +++ b/tox.ini @@ -189,7 +189,7 @@ commands = mypyinstalled: mypy --install-types --non-interactive --namespace-packages opentelemetry-api/tests/mypysmoke.py --strict [testenv:spellcheck] -basepython: python3.9 +basepython: python3.10 recreate = True deps = codespell @@ -254,7 +254,7 @@ commands = sphinx-build -E -a -W -b html -T . _build/html [testenv:tracecontext] -basepython: python3.9 +basepython: python3.10 deps = # needed for tracecontext aiohttp~=3.6 @@ -312,7 +312,7 @@ commands_post = docker-compose down -v [testenv:public-symbols-check] -basepython: python3.9 +basepython: python3.10 recreate = True deps = GitPython From e03650ac45cb74510f779f6e27fe4b920095f8be Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Wed, 15 Feb 2023 01:21:52 +0100 Subject: [PATCH 1388/1517] Update `__all__` to statically define reexports (#3143) * Statically define __all__ for stable modules This prevents errors from typecheckers since they can now see that members are explicitly reexported * Add explicit __all__ to experimental _logs modules * Add changelog for 3143 --------- Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 ++ .../src/opentelemetry/_logs/__init__.py | 28 +++++++++-------- .../src/opentelemetry/sdk/_logs/__init__.py | 16 +++++----- .../sdk/_logs/export/__init__.py | 19 ++++++------ .../src/opentelemetry/sdk/metrics/__init__.py | 28 ++++++++--------- .../sdk/metrics/export/__init__.py | 30 ++++++++++++++----- .../sdk/metrics/view/__init__.py | 20 +++++++------ 7 files changed, 83 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f562971aeb..e6ddd580e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +- Change ``__all__`` to be statically defined. + ([#3143](https://github.com/open-telemetry/opentelemetry-python/pull/3143)) - Remove the ability to set a global metric prefix for Prometheus exporter ([#3137](https://github.com/open-telemetry/opentelemetry-python/pull/3137)) - Adds environment variables for log exporter diff --git a/opentelemetry-api/src/opentelemetry/_logs/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/__init__.py index d9677a5f22..a01d1b14ee 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/__init__.py @@ -34,9 +34,7 @@ .. versionadded:: 1.15.0 """ -# pylint: disable=unused-import - -from opentelemetry._logs._internal import ( # noqa: F401 +from opentelemetry._logs._internal import ( Logger, LoggerProvider, LogRecord, @@ -46,13 +44,17 @@ get_logger_provider, set_logger_provider, ) -from opentelemetry._logs.severity import ( # noqa: F401 - SeverityNumber, - std_to_otel, -) - -__all__ = [] -for key, value in globals().copy().items(): # type: ignore - if not key.startswith("_"): - value.__module__ = __name__ # type: ignore - __all__.append(key) +from opentelemetry._logs.severity import SeverityNumber, std_to_otel + +__all__ = [ + "Logger", + "LoggerProvider", + "LogRecord", + "NoOpLogger", + "NoOpLoggerProvider", + "get_logger", + "get_logger_provider", + "set_logger_provider", + "SeverityNumber", + "std_to_otel", +] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index a548ef3793..a86fbaee0f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -12,9 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=unused-import -from opentelemetry.sdk._logs._internal import ( # noqa: F401 +from opentelemetry.sdk._logs._internal import ( LogData, Logger, LoggerProvider, @@ -23,8 +22,11 @@ LogRecordProcessor, ) -__all__ = [] -for key, value in globals().copy().items(): - if not key.startswith("_"): - value.__module__ = __name__ - __all__.append(key) +__all__ = [ + "LogData", + "Logger", + "LoggerProvider", + "LoggingHandler", + "LogRecord", + "LogRecordProcessor", +] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py index 1040288667..37a9eca7a0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py @@ -12,9 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=unused-import - -from opentelemetry.sdk._logs._internal.export import ( # noqa: F401 +from opentelemetry.sdk._logs._internal.export import ( BatchLogRecordProcessor, ConsoleLogExporter, LogExporter, @@ -23,12 +21,15 @@ ) # The point module is not in the export directory to avoid a circular import. -from opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import ( # noqa: F401 +from opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import ( InMemoryLogExporter, ) -__all__ = [] -for key, value in globals().copy().items(): - if not key.startswith("_"): - value.__module__ = __name__ - __all__.append(key) +__all__ = [ + "BatchLogRecordProcessor", + "ConsoleLogExporter", + "LogExporter", + "LogExportResult", + "SimpleLogRecordProcessor", + "InMemoryLogExporter", +] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 2219bc35c5..1ca14283cf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -12,16 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=unused-import -from opentelemetry.sdk.metrics._internal import ( # noqa: F401 - Meter, - MeterProvider, -) -from opentelemetry.sdk.metrics._internal.exceptions import ( # noqa: F401 - MetricsTimeoutError, -) -from opentelemetry.sdk.metrics._internal.instrument import ( # noqa: F401 +from opentelemetry.sdk.metrics._internal import Meter, MeterProvider +from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError +from opentelemetry.sdk.metrics._internal.instrument import ( Counter, Histogram, ObservableCounter, @@ -30,8 +24,14 @@ UpDownCounter, ) -__all__ = [] -for key, value in globals().copy().items(): - if not key.startswith("_"): - value.__module__ = __name__ - __all__.append(key) +__all__ = [ + "Meter", + "MeterProvider", + "MetricsTimeoutError", + "Counter", + "Histogram", + "ObservableCounter", + "ObservableGauge", + "ObservableUpDownCounter", + "UpDownCounter", +] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index 76021bd576..a87beadce5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -12,9 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=unused-import -from opentelemetry.sdk.metrics._internal.export import ( # noqa: F401 +from opentelemetry.sdk.metrics._internal.export import ( AggregationTemporality, ConsoleMetricExporter, InMemoryMetricReader, @@ -25,7 +24,7 @@ ) # The point module is not in the export directory to avoid a circular import. -from opentelemetry.sdk.metrics._internal.point import ( # noqa: F401 +from opentelemetry.sdk.metrics._internal.point import ( DataPointT, DataT, Gauge, @@ -39,8 +38,23 @@ Sum, ) -__all__ = [] -for key, value in globals().copy().items(): - if not key.startswith("_"): - value.__module__ = __name__ - __all__.append(key) +__all__ = [ + "AggregationTemporality", + "ConsoleMetricExporter", + "InMemoryMetricReader", + "MetricExporter", + "MetricExportResult", + "MetricReader", + "PeriodicExportingMetricReader", + "DataPointT", + "DataT", + "Gauge", + "Histogram", + "HistogramDataPoint", + "Metric", + "MetricsData", + "NumberDataPoint", + "ResourceMetrics", + "ScopeMetrics", + "Sum", +] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view/__init__.py index b20053d876..f6e4dcb3aa 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view/__init__.py @@ -12,9 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=unused-import - -from opentelemetry.sdk.metrics._internal.aggregation import ( # noqa: F401 +from opentelemetry.sdk.metrics._internal.aggregation import ( Aggregation, DefaultAggregation, DropAggregation, @@ -22,10 +20,14 @@ LastValueAggregation, SumAggregation, ) -from opentelemetry.sdk.metrics._internal.view import View # noqa: F401 +from opentelemetry.sdk.metrics._internal.view import View -__all__ = [] -for key, value in globals().copy().items(): - if not key.startswith("_"): - value.__module__ = __name__ - __all__.append(key) +__all__ = [ + "Aggregation", + "DefaultAggregation", + "DropAggregation", + "ExplicitBucketHistogramAggregation", + "LastValueAggregation", + "SumAggregation", + "View", +] From 541a7496ecac19916a4a56c74134944bb8eef7dc Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Fri, 17 Feb 2023 10:11:54 -0800 Subject: [PATCH 1389/1517] Update version to 1.17.0.dev/0.38b0.dev (#3178) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/pyproject.toml | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-grpc/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-http/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/pyproject.toml | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/pyproject.toml | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opentracing-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/pyproject.toml | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 32 files changed, 41 insertions(+), 39 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b2dbeb6e77..5ee26e5247 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 1f0dda9865b3c83e6dea2f2b127a2b0971853543 + CONTRIB_REPO_SHA: ca2c23fd3b91c48f2b6515658993480f634cf1b2 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index e6ddd580e7..641e83a087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased + +## Version 1.16.0/0.37b0 (2023-02-15) - Change ``__all__`` to be statically defined. ([#3143](https://github.com/open-telemetry/opentelemetry-python/pull/3143)) - Remove the ability to set a global metric prefix for Prometheus exporter diff --git a/eachdist.ini b/eachdist.ini index 6606776ecb..164faad3d1 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.16.0.dev +version=1.17.0.dev packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.37b0.dev +version=0.38b0.dev packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 4209aaad38..f90e55e55a 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 4209aaad38..f90e55e55a 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger/pyproject.toml b/exporter/opentelemetry-exporter-jaeger/pyproject.toml index c595f817f3..5c64b989be 100644 --- a/exporter/opentelemetry-exporter-jaeger/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-jaeger-proto-grpc == 1.16.0.dev", - "opentelemetry-exporter-jaeger-thrift == 1.16.0.dev", + "opentelemetry-exporter-jaeger-proto-grpc == 1.17.0.dev", + "opentelemetry-exporter-jaeger-thrift == 1.17.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 4209aaad38..f90e55e55a 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/exporter/opentelemetry-exporter-opencensus/pyproject.toml b/exporter/opentelemetry-exporter-opencensus/pyproject.toml index 81d4546dd0..8e359f5d4d 100644 --- a/exporter/opentelemetry-exporter-opencensus/pyproject.toml +++ b/exporter/opentelemetry-exporter-opencensus/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ dependencies = [ "grpcio >= 1.0.0, < 2.0.0", "opencensus-proto >= 0.1.0, < 1.0.0", - "opentelemetry-api >= 1.16.0.dev", + "opentelemetry-api >= 1.17.0.dev", "opentelemetry-sdk >= 1.15", "protobuf ~= 3.13", "setuptools >= 16.0", diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 0a47df4f5a..8778b43b17 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.37b0.dev" +__version__ = "0.38b0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index 36401ac106..aa369caf00 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -30,8 +30,8 @@ dependencies = [ "googleapis-common-protos ~= 1.52", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.16.0.dev", - "opentelemetry-sdk ~= 1.16.0.dev", + "opentelemetry-proto == 1.17.0.dev", + "opentelemetry-sdk ~= 1.17.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 0b4a18e0a2..904e2b48ef 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index b3a12fc91d..9675d1d982 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -29,8 +29,8 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.16.0.dev", - "opentelemetry-sdk ~= 1.16.0.dev", + "opentelemetry-proto == 1.17.0.dev", + "opentelemetry-sdk ~= 1.17.0.dev", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 0b4a18e0a2..904e2b48ef 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index ab605eb3e9..6de1ff977b 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-otlp-proto-grpc == 1.16.0.dev", - "opentelemetry-exporter-otlp-proto-http == 1.16.0.dev", + "opentelemetry-exporter-otlp-proto-grpc == 1.17.0.dev", + "opentelemetry-exporter-otlp-proto-http == 1.17.0.dev", ] [project.entry-points.opentelemetry_logs_exporter] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 0b4a18e0a2..904e2b48ef 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 0a47df4f5a..8778b43b17 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.37b0.dev" +__version__ = "0.38b0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 0b4a18e0a2..904e2b48ef 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml index 1c4137c929..c6fe12e0ca 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.3", - "opentelemetry-exporter-zipkin-json == 1.16.0.dev", + "opentelemetry-exporter-zipkin-json == 1.17.0.dev", "opentelemetry-sdk ~= 1.11", "protobuf ~= 3.12", "requests ~= 2.7", diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 0b4a18e0a2..904e2b48ef 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin/pyproject.toml b/exporter/opentelemetry-exporter-zipkin/pyproject.toml index d982aaf896..50ec818a09 100644 --- a/exporter/opentelemetry-exporter-zipkin/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-zipkin-json == 1.16.0.dev", - "opentelemetry-exporter-zipkin-proto-http == 1.16.0.dev", + "opentelemetry-exporter-zipkin-json == 1.17.0.dev", + "opentelemetry-exporter-zipkin-proto-http == 1.17.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 0b4a18e0a2..904e2b48ef 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 0b4a18e0a2..904e2b48ef 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 0b4a18e0a2..904e2b48ef 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index 4798f4fbc6..bffc677fd0 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-api == 1.16.0.dev", - "opentelemetry-semantic-conventions == 0.37b0.dev", + "opentelemetry-api == 1.17.0.dev", + "opentelemetry-semantic-conventions == 0.38b0.dev", "setuptools >= 16.0", "typing-extensions >= 3.7.4", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 0b4a18e0a2..904e2b48ef 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 0a47df4f5a..8778b43b17 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.37b0.dev" +__version__ = "0.38b0.dev" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 0b4a18e0a2..904e2b48ef 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 0b4a18e0a2..904e2b48ef 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0.dev" +__version__ = "1.17.0.dev" diff --git a/shim/opentelemetry-opentracing-shim/pyproject.toml b/shim/opentelemetry-opentracing-shim/pyproject.toml index d831379655..742eabd2f4 100644 --- a/shim/opentelemetry-opentracing-shim/pyproject.toml +++ b/shim/opentelemetry-opentracing-shim/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.37b0.dev", + "opentelemetry-test-utils == 0.38b0.dev", "opentracing ~= 2.2.0", ] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 0a47df4f5a..8778b43b17 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.37b0.dev" +__version__ = "0.38b0.dev" diff --git a/tests/opentelemetry-test-utils/pyproject.toml b/tests/opentelemetry-test-utils/pyproject.toml index b9f84602a2..7db7c0145e 100644 --- a/tests/opentelemetry-test-utils/pyproject.toml +++ b/tests/opentelemetry-test-utils/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ ] dependencies = [ "asgiref ~= 3.0", - "opentelemetry-api == 1.16.0.dev", - "opentelemetry-sdk == 1.16.0.dev", + "opentelemetry-api == 1.17.0.dev", + "opentelemetry-sdk == 1.17.0.dev", ] [project.optional-dependencies] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 6b7ff9a946..9d323a2a65 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.37b0.dev" +__version__ = "0.38b0.dev" From 3df55e93a10d2dc5fb9487f04692b0396d9dd9c5 Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Tue, 21 Feb 2023 10:08:01 -0800 Subject: [PATCH 1390/1517] Copy change log updates from release/v1.16.x-0.37bx (#3187) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 641e83a087..796fda683e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -## Version 1.16.0/0.37b0 (2023-02-15) +## Version 1.16.0/0.37b0 (2023-02-17) - Change ``__all__`` to be statically defined. ([#3143](https://github.com/open-telemetry/opentelemetry-python/pull/3143)) - Remove the ability to set a global metric prefix for Prometheus exporter From 588c5fbf49a9c8e476cdca3f1bca5969c0b3a65c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 19:04:04 +0000 Subject: [PATCH 1391/1517] Bump werkzeug in /docs/examples/fork-process-model/flask-uwsgi (#3180) Bumps [werkzeug](https://github.com/pallets/werkzeug) from 1.0.1 to 2.2.3. - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/werkzeug/compare/1.0.1...2.2.3) --- updated-dependencies: - dependency-name: werkzeug dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Srikanth Chekuri --- docs/examples/fork-process-model/flask-uwsgi/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt index 86b654a95d..3f9f6f6454 100644 --- a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt +++ b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt @@ -16,5 +16,5 @@ protobuf==3.18.3 six==1.15.0 thrift==0.13.0 uWSGI==2.0.19.1 -Werkzeug==1.0.1 +Werkzeug==2.2.3 wrapt==1.12.1 From c757e4a26f0b3c5157869150965aa3341532776e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 20:00:56 +0000 Subject: [PATCH 1392/1517] Bump werkzeug in /docs/examples/fork-process-model/flask-gunicorn (#3179) Bumps [werkzeug](https://github.com/pallets/werkzeug) from 1.0.1 to 2.2.3. - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/werkzeug/compare/1.0.1...2.2.3) --- updated-dependencies: - dependency-name: werkzeug dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Leighton Chen Co-authored-by: Srikanth Chekuri --- .../examples/fork-process-model/flask-gunicorn/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt index 86b654a95d..3f9f6f6454 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt +++ b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt @@ -16,5 +16,5 @@ protobuf==3.18.3 six==1.15.0 thrift==0.13.0 uWSGI==2.0.19.1 -Werkzeug==1.0.1 +Werkzeug==2.2.3 wrapt==1.12.1 From 2d1f0b9f5fce62549d1338882f37b91b95881c75 Mon Sep 17 00:00:00 2001 From: Shalev Roda <65566801+shalevr@users.noreply.github.com> Date: Thu, 23 Feb 2023 03:38:59 +0200 Subject: [PATCH 1393/1517] Add metrics base tests (#3182) --- .../src/opentelemetry/test/test_base.py | 122 +++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py index 0d81fcb4f5..f7451f0d3c 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py @@ -15,12 +15,19 @@ import logging import unittest from contextlib import contextmanager -from typing import Tuple +from typing import Optional, Sequence, Tuple from opentelemetry import metrics as metrics_api from opentelemetry import trace as trace_api from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import InMemoryMetricReader, MetricReader +from opentelemetry.sdk.metrics._internal.point import Metric +from opentelemetry.sdk.metrics.export import ( + DataPointT, + HistogramDataPoint, + InMemoryMetricReader, + MetricReader, + NumberDataPoint, +) from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, @@ -130,6 +137,117 @@ def disable_logging(highest_level=logging.CRITICAL): finally: logging.disable(logging.NOTSET) + def get_sorted_metrics(self): + resource_metrics = ( + self.memory_metrics_reader.get_metrics_data().resource_metrics + ) + + all_metrics = [] + for metrics in resource_metrics: + for scope_metrics in metrics.scope_metrics: + all_metrics.extend(scope_metrics.metrics) + + return self.sorted_metrics(all_metrics) + + @staticmethod + def sorted_metrics(metrics): + """ + Sorts metrics by metric name. + """ + return sorted( + metrics, + key=lambda m: m.name, + ) + + def assert_metric_expected( + self, + metric: Metric, + expected_data_points: Sequence[DataPointT], + est_value_delta: Optional[float] = 0, + ): + self.assertEqual( + len(expected_data_points), len(metric.data.data_points) + ) + for expected_data_point in expected_data_points: + self.assert_data_point_expected( + expected_data_point, metric.data.data_points, est_value_delta + ) + + # pylint: disable=unidiomatic-typecheck + @staticmethod + def is_data_points_equal( + expected_data_point: DataPointT, + data_point: DataPointT, + est_value_delta: Optional[float] = 0, + ): + if type(expected_data_point) != type(data_point) or not isinstance( + expected_data_point, (HistogramDataPoint, NumberDataPoint) + ): + return False + + values_diff = None + if isinstance(data_point, NumberDataPoint): + values_diff = abs(expected_data_point.value - data_point.value) + elif isinstance(data_point, HistogramDataPoint): + values_diff = abs(expected_data_point.sum - data_point.sum) + if expected_data_point.count != data_point.count or ( + est_value_delta == 0 + and ( + expected_data_point.min != data_point.min + or expected_data_point.max != data_point.max + ) + ): + return False + + return ( + values_diff <= est_value_delta + and expected_data_point.attributes == dict(data_point.attributes) + ) + + def assert_data_point_expected( + self, + expected_data_point: DataPointT, + data_points: Sequence[DataPointT], + est_value_delta: Optional[float] = 0, + ): + is_data_point_exist = False + for data_point in data_points: + if self.is_data_points_equal( + expected_data_point, data_point, est_value_delta + ): + is_data_point_exist = True + break + + self.assertTrue( + is_data_point_exist, + msg=f"Data point {expected_data_point} does not exist", + ) + + @staticmethod + def create_number_data_point(value, attributes): + return NumberDataPoint( + value=value, + attributes=attributes, + start_time_unix_nano=0, + time_unix_nano=0, + ) + + @staticmethod + def create_histogram_data_point( + sum_data_point, count, max_data_point, min_data_point, attributes + ): + return HistogramDataPoint( + count=count, + sum=sum_data_point, + min=min_data_point, + max=max_data_point, + attributes=attributes, + start_time_unix_nano=0, + time_unix_nano=0, + bucket_counts=[], + explicit_bounds=[], + ) + class FinishedTestSpans(list): def __init__(self, test, spans): From 54a1003eed042c9eea7346fca534b41e7077960a Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 23 Feb 2023 23:11:39 +0530 Subject: [PATCH 1394/1517] Update approvers/maintainers list (#3194) --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 03fcdf73b0..ec94eb8fe2 100644 --- a/README.md +++ b/README.md @@ -136,15 +136,16 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): - [Aaron Abbott](https://github.com/aabmass), Google -- [Owais Lone](https://github.com/owais), Splunk -- [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), AWS +- [Sanket Mehta](https://github.com/sanketmehta28), Cisco Emeritus Approvers +- [Ashutosh Goel](https://github.com/ashu658), Cisco - [Carlos Alberto Cortez](https://github.com/carlosalberto), Lightstep - [Christian Neumüller](https://github.com/Oberon00), Dynatrace - [Hector Hernandez](https://github.com/hectorhdzg), Microsoft - [Mauricio Vásquez](https://github.com/mauriciovasquezbernal), Kinvolk +- [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), AWS - [Tahir H. Butt](https://github.com/majorgreys) DataDog *For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver).* @@ -159,6 +160,7 @@ Emeritus Maintainers: - [Alex Boten](https://github.com/codeboten), Lightstep - [Chris Kleinknecht](https://github.com/c24t), Google +- [Owais Lone](https://github.com/owais), Splunk - [Reiley Yang](https://github.com/reyang), Microsoft - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google From 559e06d577f50d0130cdc34a4f6a7b75b0704c18 Mon Sep 17 00:00:00 2001 From: Mark Allanson Date: Thu, 23 Feb 2023 18:59:20 +0000 Subject: [PATCH 1395/1517] PeriodicExportingMetricReader will continue collection times out (#3098) (#3100) In cases where collection times out, the period exporting reader thread should not terminate, but instead catch, log, and continue on after the regular interval seconds. Prior to this commit, a metric collection timeout would terminate the thread and stop reporting metrics to the wrapped exporter resulting in the appearance in observability tooling of metrics just stopping without reason. --- CHANGELOG.md | 5 ++- .../sdk/metrics/_internal/export/__init__.py | 10 ++++- .../test_periodic_exporting_metric_reader.py | 41 +++++++++++++++++-- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 796fda683e..e3dd0a0c56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +- PeriodicExportingMetricReader will continue if collection times out + ([#3100](https://github.com/open-telemetry/opentelemetry-python/pull/3100)) + ## Version 1.16.0/0.37b0 (2023-02-17) - Change ``__all__`` to be statically defined. @@ -28,10 +31,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3156](https://github.com/open-telemetry/opentelemetry-python/pull/3156)) - deprecate jaeger exporters ([#3158](https://github.com/open-telemetry/opentelemetry-python/pull/3158)) - - Create a single resource instance ([#3118](https://github.com/open-telemetry/opentelemetry-python/pull/3118)) + ## Version 1.15.0/0.36b0 (2022-12-09) - PeriodicExportingMetricsReader with +Inf interval diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 836bb8eece..5bd94d5aac 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -41,6 +41,7 @@ AggregationTemporality, DefaultAggregation, ) +from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError from opentelemetry.sdk.metrics._internal.instrument import ( Counter, Histogram, @@ -497,7 +498,14 @@ def _at_fork_reinit(self): def _ticker(self) -> None: interval_secs = self._export_interval_millis / 1e3 while not self._shutdown_event.wait(interval_secs): - self.collect(timeout_millis=self._export_timeout_millis) + try: + self.collect(timeout_millis=self._export_timeout_millis) + except MetricsTimeoutError: + _logger.warning( + "Metric collection timed out. Will try again after %s seconds", + interval_secs, + exc_info=True, + ) # one last collection below before shutting down completely self.collect(timeout_millis=self._export_interval_millis) diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index 532f25b9e7..aa0eed285d 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -14,12 +14,12 @@ import math from time import sleep, time_ns -from typing import Sequence +from typing import Optional, Sequence from unittest.mock import Mock from flaky import flaky -from opentelemetry.sdk.metrics import Counter +from opentelemetry.sdk.metrics import Counter, MetricsTimeoutError from opentelemetry.sdk.metrics._internal import _Counter from opentelemetry.sdk.metrics.export import ( AggregationTemporality, @@ -67,6 +67,25 @@ def force_flush(self, timeout_millis: float = 10_000) -> bool: return True +class ExceptionAtCollectionPeriodicExportingMetricReader( + PeriodicExportingMetricReader +): + def __init__( + self, + exporter: MetricExporter, + exception: Exception, + export_interval_millis: Optional[float] = None, + export_timeout_millis: Optional[float] = None, + ) -> None: + super().__init__( + exporter, export_interval_millis, export_timeout_millis + ) + self._collect_exception = exception + + def collect(self, timeout_millis: float = 10_000) -> None: + raise self._collect_exception + + metrics_list = [ Metric( name="sum_name", @@ -111,11 +130,13 @@ def test_defaults(self): pmr.shutdown() def _create_periodic_reader( - self, metrics, exporter, collect_wait=0, interval=60000 + self, metrics, exporter, collect_wait=0, interval=60000, timeout=30000 ): pmr = PeriodicExportingMetricReader( - exporter, export_interval_millis=interval + exporter, + export_interval_millis=interval, + export_timeout_millis=timeout, ) def _collect(reader, timeout_millis): @@ -219,3 +240,15 @@ def test_exporter_aggregation_preference(self): self.assertTrue(isinstance(value, DefaultAggregation)) else: self.assertTrue(isinstance(value, LastValueAggregation)) + + def test_metric_timeout_does_not_kill_worker_thread(self): + exporter = FakeMetricsExporter() + pmr = ExceptionAtCollectionPeriodicExportingMetricReader( + exporter, + MetricsTimeoutError("test timeout"), + export_timeout_millis=1, + ) + + sleep(0.1) + self.assertTrue(pmr._daemon_thread.is_alive()) + pmr.shutdown() From 69de3dc599b1f40c82d061c11e3ae259dfe22a90 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 24 Feb 2023 01:02:26 +0530 Subject: [PATCH 1396/1517] Update docs (#3188) * Update docs * fix spell --- CONTRIBUTING.md | 23 +++------ README.md | 130 ++++++++++++++++-------------------------------- rationale.md | 19 +++---- 3 files changed, 55 insertions(+), 117 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e97e9daff2..daef82f90a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,9 @@ # Contributing to opentelemetry-python -The Python special interest group (SIG) meets regularly. See the OpenTelemetry -[community](https://github.com/open-telemetry/community#python-sdk) repo for -information on this and other language SIGs. +The Python special interest group (SIG) meets weekly on Thursdays at 9AM PST. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates and Zoom meeting links. See the [public meeting notes](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit) -for a summary description of past meetings. To request edit access, join the -meeting or get in touch on [Slack](https://cloud-native.slack.com/archives/C01PD4HUVBL). +for a summary description of past meetings. See to the [community membership document](https://github.com/open-telemetry/community/blob/main/community-membership.md) on how to become a [**Member**](https://github.com/open-telemetry/community/blob/main/community-membership.md#member), @@ -19,13 +16,7 @@ This is the main repo for OpenTelemetry Python. Nevertheless, there are other re Please take a look at this list first, your contributions may belong in one of these repos better: 1. [OpenTelemetry Contrib](https://github.com/open-telemetry/opentelemetry-python-contrib): Instrumentations for third-party - libraries and frameworks. There is an ongoing effort to migrate into the OpenTelemetry Contrib repo some of the existing - programmatic instrumentations that are now in the `ext` directory in the main OpenTelemetry repo. Please ask in the Slack - channel (see below) for guidance if you want to contribute with these instrumentations. - -# Find the right branch - -The default branch for this repo is `main`. All feature work is accomplished on branches from `main`. + libraries and frameworks. ## Find a Buddy and get Started Quickly! @@ -75,8 +66,8 @@ An easier way to do so is: We try to keep the amount of _public symbols_ in our code minimal. A public symbol is any Python identifier that does not start with an underscore. Every public symbol is something that has to be kept in order to maintain backwards compatibility, so we try to have as few as possible. -To check if your PR is adding public symbols, run `tox -e public-symbols-check`. This will always fail if public symbols are being added. The idea -behind this is that every PR that adds public symbols fails in CI, forcing reviewers to check the symbols to make sure they are strictly necessary. +To check if your PR is adding public symbols, run `tox -e public-symbols-check`. This will always fail if public symbols are being added/removed. The idea +behind this is that every PR that adds/removes public symbols fails in CI, forcing reviewers to check the symbols to make sure they are strictly necessary. If after checking them, it is considered that they are indeed necessary, the PR will be labeled with `Skip Public API check` so that this check is not run. @@ -95,7 +86,7 @@ CONTRIB_REPO_SHA=dde62cebffe519c35875af6d06fae053b3be65ec tox ``` The continuation integration overrides that environment variable with as per the configuration -[here](https://github.com/open-telemetry/opentelemetry-python/blob/9020b0baaeb41b7137badca988bb5c2d562cddee/.github/workflows/test.yml#L13). +[here](https://github.com/open-telemetry/opentelemetry-python/blob/main/.github/workflows/test.yml#L13). ### Benchmarks @@ -196,7 +187,7 @@ updating the GitHub workflow to reference a PR in the Contrib repo * Trivial change (typo, cosmetic, doc, etc.) doesn't have to wait for one day. * Urgent fix can take exception as long as it has been actively communicated. -Any Approver / Maintainer can merge the PR once it is **ready to merge**. +One of the maintainers will merge the PR once it is **ready to merge**. ## Design Choices diff --git a/README.md b/README.md index ec94eb8fe2..1cad936e57 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,47 @@ -## OpenTelemetry Python ---- -

- - Getting Started -   •   - API Documentation -   •   - Getting In Touch (GitHub Discussions) - -

- -

- - GitHub release (latest by date including pre-releases) - - - Codecov Status - - - license - -
- - Build Status - - Beta -

- -

- - Contributing -   •   - Examples - -

- ---- - -This page describes the Python [OpenTelemetry](https://opentelemetry.io/) implementation. OpenTelemetry is an observability framework for cloud-native software. - -## Requirements -Unless otherwise noted, all published artifacts support Python 3.7 or higher. See CONTRIBUTING.md for additional instructions for building this project for development. +# OpenTelemetry Python +[![Slack](https://img.shields.io/badge/slack-@cncf/otel/python-brightgreen.svg?logo=slack)](https://cloud-native.slack.com/archives/C01PD4HUVBL) +[![Build Status](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/open-telemetry/opentelemetry-python/actions) +[![Minimum Python Version](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/) +[![Release](https://img.shields.io/github/v/release/open-telemetry/opentelemetry-python?include_prereleases&style=)](https://github.com/open-telemetry/opentelemetry-python/releases/) +[![Read the Docs](https://readthedocs.org/projects/opentelemetry-python/badge/?version=latest)](https://opentelemetry-python.readthedocs.io/en/latest/) + +## Project Status + +See the [OpenTelemetry Instrumentation for Python](https://opentelemetry.io/docs/instrumentation/python/#status-and-releases). + +| Signal | Status | Project | +| ------- | ------------ | ------- | +| Traces | Stable | N/A | +| Metrics | Stable | N/A | +| Logs | Experimental | N/A | + +Project versioning information and stability guarantees can be found [here](./rationale.md#versioning-and-releasing). ## Getting started -The goal of OpenTelemetry is to provide a single set of APIs to capture distributed traces and metrics from your application and send them to an observability platform. This project lets you do just that for applications written in Python. +You can find the getting started guide for OpenTelemetry Python [here](https://opentelemetry.io/docs/instrumentation/python/getting-started/). + +If you are looking for **examples** on how to use the OpenTelemetry API to +instrument your code manually, or how to set up the OpenTelemetry +Python SDK, see https://opentelemetry.io/docs/instrumentation/python/manual/. + +## Python Version Support + +This project ensures compatibility with the current supported versions of the Python. As new Python versions are released, support for them is added and +as old Python versions reach their end of life, support for them is removed. + +We add support for new Python versions no later than 3 months after they become stable. + +We remove support for old Python versions 6 months after they reach their [end of life](https://devguide.python.org/devcycle/#end-of-life-branches). + + +## Documentation + +The online documentation is available at https://opentelemetry-python.readthedocs.io/. +To access the latest version of the documentation, see +https://opentelemetry-python.readthedocs.io/en/latest/. + +## Install This repository includes multiple installable packages. The `opentelemetry-api` package includes abstract classes and no-op implementations that comprise the OpenTelemetry API following the @@ -91,47 +88,13 @@ pip install -e ./instrumentation/opentelemetry-instrumentation-{instrumentation} For additional exporter and instrumentation packages, see the [`opentelemetry-python-contrib`](https://github.com/open-telemetry/opentelemetry-python-contrib) repository. -## Running Performance Tests - -This section provides details on how to reproduce performance tests results on your own -machine. - -### Resource Usage Tests - -1. Install scalene using the following command - -```sh -pip install scalene -``` - -2. Run the `scalene` tests on any of the example Python programs - -```sh -scalene opentelemetry-/tests/performance/resource-usage//profile_resource_usage_.py -``` - -## Python Version Support Addition and Removal - -This project supports the latest Python versions. As new Python versions are released, support for them is added and -as old Python versions reach their end of life, support for them is removed. - -We add support for new Python versions no later than 3 months after they become stable. - -We remove support for old Python versions 6 months after they reach their [end of life](https://devguide.python.org/devcycle/#end-of-life-branches). - -## Documentation - -The online documentation is available at https://opentelemetry-python.readthedocs.io/. -To access the latest version of the documentation, see -https://opentelemetry-python.readthedocs.io/en/latest/. - ## Contributing For information about contributing to OpenTelemetry Python, see [CONTRIBUTING.md](CONTRIBUTING.md). We meet weekly on Thursdays at 9AM PST. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates and Zoom meeting links. -Meeting notes are available as a public [Google doc](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit). For edit access, get in touch on [GitHub Discussions](https://github.com/open-telemetry/opentelemetry-python/discussions). +Meeting notes are available as a public [Google doc](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit). Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): @@ -171,12 +134,3 @@ Emeritus Maintainers: - -## Project Status - -For project boards and milestones, see the following links: -- [Project boards](https://github.com/open-telemetry/opentelemetry-python/projects) -- [Milestones](https://github.com/open-telemetry/opentelemetry-python/milestones) - -We try to keep these links accurate, so they're the best place to go for questions about project status. The dates and features described in the issues -and milestones are estimates and subject to change. diff --git a/rationale.md b/rationale.md index 8ff92e902c..4f1dd3ab82 100644 --- a/rationale.md +++ b/rationale.md @@ -40,7 +40,7 @@ Pre-release packages are denoted by appending identifiers such as -Alpha, -Beta, ### Immature or experimental signals -Modules for experimental signals will be released in a separate versions that will be marked as pre-releases, and must be installed manually. Modules will remain at version v0.x.y to make it abundantly clear that depending on them is at your own risk. For example, the `opentelemetry-api` v0.x.y-b0 module will provide experimental access to the latest features in development, which will include the experimental metrics signals. Notice the `b0` suffix which indicates that the release is still in beta, and therefore deemed to be in pre-release. NO STABILITY GUARANTEES ARE MADE. +Modules for experimental signals will be released in the same packages as the core components, but prefixed with `_` to indicate that they are unstable and subject to change. NO STABILITY GUARANTEES ARE MADE. ## Examples @@ -53,13 +53,6 @@ Purely for illustration purposes, not intended to represent actual releases: - `opentelemetry-sdk` 1.0.0 - Contains SDK components for tracing, baggage, propagators, and context -##### Contains the following experimental packages - -- `opentelemetry-api` 1.x.y-b0 - - Contains the EXPERIMENTAL API for metrics plus other unstable features. There are no stability guarantees. -- `opentelemetry-sdk` 1.x.y-b0 - - Contains the EXPERIMENTAL SDK for metrics plus other unstable features. There are no stability guarantees. - #### V1.15.0 Release (with metrics) - `opentelemetry-api` 1.15.0 @@ -67,9 +60,9 @@ Purely for illustration purposes, not intended to represent actual releases: - `opentelemetry-sdk` 1.15.0 - Contains SDK components for tracing, baggage, propagators, context and metrics -##### Contains the following experimental packages +##### Contains the following pre-release packages -- `opentelemetry-api` 1.x.y-b0 - - Contains the EXPERIMENTAL API for logging plus other unstable features. There are no stability guarantees. -- `opentelemetry-sdk` 1.x.y-b0 - - Contains the EXPERIMENTAL SDK for logging plus other unstable features. There are no stability guarantees. +- `opentelemetry-api` 1.x.yrc1 + - Contains the experimental public API for logging plus other unstable features. There are no stability guarantees. +- `opentelemetry-sdk` 1.x.yrc1 + - Contains the experimental public SDK for logging plus other unstable features. There are no stability guarantees. From a9a96aad8b930a7efdea1ad979aefc173e1710d0 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 24 Feb 2023 11:11:05 -0800 Subject: [PATCH 1397/1517] bugfix: indent not used in MetricsData to_json (#3197) --- CHANGELOG.md | 3 ++- .../src/opentelemetry/sdk/metrics/_internal/point.py | 3 ++- .../tests/metrics/integration_test/test_console_exporter.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3dd0a0c56..d43b64cfaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - PeriodicExportingMetricReader will continue if collection times out ([#3100](https://github.com/open-telemetry/opentelemetry-python/pull/3100)) - +- Fix formatting of ConsoleMetricExporter. + ([#3197](https://github.com/open-telemetry/opentelemetry-python/pull/3197)) ## Version 1.16.0/0.37b0 (2023-02-17) - Change ``__all__`` to be statically defined. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py index b4d813acca..410c7754d8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py @@ -211,5 +211,6 @@ def to_json(self, indent=4) -> str: loads(resource_metrics.to_json(indent=indent)) for resource_metrics in self.resource_metrics ] - } + }, + indent=indent, ) diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py b/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py index 59f026661c..60c4227c3b 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py @@ -49,7 +49,7 @@ def test_console_exporter(self): provider.shutdown() output.seek(0) - result_0 = loads(output.readlines()[0]) + result_0 = loads("".join(output.readlines())) self.assertGreater(len(result_0), 0) From d2edb409e110bb50e91a750ef33d13d7299914e4 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 2 Mar 2023 16:31:32 -0500 Subject: [PATCH 1398/1517] Initialize package and docs for OpenCensus shim (#3204) * Initialize package and docs for opencensus shim * Fix lint and remove pypy tests --- docs/examples/opencensus-shim/README.rst | 71 +++++++ .../examples/opencensus-shim/requirements.txt | 4 + .../shim/opencensus_shim/opentracing_shim.rst | 5 + shim/opentelemetry-opencensus-shim/LICENSE | 201 ++++++++++++++++++ shim/opentelemetry-opencensus-shim/README.rst | 20 ++ .../pyproject.toml | 48 +++++ .../opentelemetry/shim/opencensus/__init__.py | 24 +++ .../opentelemetry/shim/opencensus/py.typed | 0 .../opentelemetry/shim/opencensus/version.py | 15 ++ .../tests/test_shim.py | 20 ++ tox.ini | 14 +- 11 files changed, 419 insertions(+), 3 deletions(-) create mode 100644 docs/examples/opencensus-shim/README.rst create mode 100644 docs/examples/opencensus-shim/requirements.txt create mode 100644 docs/shim/opencensus_shim/opentracing_shim.rst create mode 100644 shim/opentelemetry-opencensus-shim/LICENSE create mode 100644 shim/opentelemetry-opencensus-shim/README.rst create mode 100644 shim/opentelemetry-opencensus-shim/pyproject.toml create mode 100644 shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/__init__.py create mode 100644 shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/py.typed create mode 100644 shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py create mode 100644 shim/opentelemetry-opencensus-shim/tests/test_shim.py diff --git a/docs/examples/opencensus-shim/README.rst b/docs/examples/opencensus-shim/README.rst new file mode 100644 index 0000000000..64de1e36ec --- /dev/null +++ b/docs/examples/opencensus-shim/README.rst @@ -0,0 +1,71 @@ +OpenCensus Shim +================ + +This example shows how to use the :doc:`opentelemetry-opencensus-shim +package <../../shim/opencensus_shim/opencensus_shim>` +to interact with libraries instrumented with +`opencensus-python `_. + + +The source files required to run this example are available :scm_web:`here `. + +Installation +------------ + +Jaeger +****** + +Start Jaeger + +.. code-block:: sh + + docker run --rm \ + -p 6831:6831/udp \ + -p 6832:6832/udp \ + -p 16686:16686 \ + jaegertracing/all-in-one:1.13 \ + --log-level=debug + +Python Dependencies +******************* + +Install the Python dependencies in :scm_raw_web:`requirements.txt ` + +.. code-block:: sh + + pip install -r requirements.txt + + +Alternatively, you can install the Python dependencies separately: + +.. code-block:: sh + + pip install \ + opentelemetry-api \ + opentelemetry-sdk \ + opentelemetry-exporter-jaeger \ + opentelemetry-opencensus-shim + + +Run the Application +------------------- + +.. TODO implement the example + +Jaeger UI +********* + +Open the Jaeger UI in your browser at +``_ and view traces for the +"OpenCensus Shim Example" service. + +Note that tags and logs (OpenCensus) and attributes and events (OpenTelemetry) +from both tracing systems appear in the exported trace. + +Useful links +------------ + +- OpenTelemetry_ +- :doc:`../../shim/opencensus_shim/opencensus_shim` + +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/docs/examples/opencensus-shim/requirements.txt b/docs/examples/opencensus-shim/requirements.txt new file mode 100644 index 0000000000..072c825558 --- /dev/null +++ b/docs/examples/opencensus-shim/requirements.txt @@ -0,0 +1,4 @@ +opentelemetry-api +opentelemetry-sdk +opentelemetry-exporter-jaeger +opentelemetry-opencensus-shim diff --git a/docs/shim/opencensus_shim/opentracing_shim.rst b/docs/shim/opencensus_shim/opentracing_shim.rst new file mode 100644 index 0000000000..3c8bff1d3c --- /dev/null +++ b/docs/shim/opencensus_shim/opentracing_shim.rst @@ -0,0 +1,5 @@ +OpenCensus Shim for OpenTelemetry +================================== + +.. automodule:: opentelemetry.shim.opencensus + :no-show-inheritance: diff --git a/shim/opentelemetry-opencensus-shim/LICENSE b/shim/opentelemetry-opencensus-shim/LICENSE new file mode 100644 index 0000000000..1ef7dad2c5 --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/LICENSE @@ -0,0 +1,201 @@ + 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 The OpenTelemetry Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/shim/opentelemetry-opencensus-shim/README.rst b/shim/opentelemetry-opencensus-shim/README.rst new file mode 100644 index 0000000000..bb5f7d4774 --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/README.rst @@ -0,0 +1,20 @@ +OpenCensus Shim for OpenTelemetry +================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-opencensus-shim.svg + :target: https://pypi.org/project/opentelemetry-opencensus-shim/ + +Installation +------------ + +:: + + pip install opentelemetry-opencensus-shim + +References +---------- + +* `OpenCensus Shim for OpenTelemetry `_ +* `OpenTelemetry Project `_ diff --git a/shim/opentelemetry-opencensus-shim/pyproject.toml b/shim/opentelemetry-opencensus-shim/pyproject.toml new file mode 100644 index 0000000000..b4d2f76f06 --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/pyproject.toml @@ -0,0 +1,48 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-opencensus-shim" +dynamic = ["version"] +description = "OpenCensus Shim for OpenTelemetry" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Typing :: Typed", +] +dependencies = [ + "opentelemetry-api ~= 1.3", + "wrapt ~= 1.0", + # may work with older versions but this is the oldest confirmed version + "opencensus >= 0.11.0", +] + +[project.optional-dependencies] +test = ["opentelemetry-test-utils == 0.38b0.dev", "opencensus == 0.11.1"] + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/shim/opentelemetry-opencensus-shim" + +[tool.hatch.version] +path = "src/opentelemetry/shim/opencensus/version.py" + +[tool.hatch.build.targets.sdist] +include = ["/src", "/tests"] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/__init__.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/__init__.py new file mode 100644 index 0000000000..2ef69255bc --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/__init__.py @@ -0,0 +1,24 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The OpenTelemetry OpenCensus shim is a library which allows an easy migration from OpenCensus +to OpenTelemetry. Additional details can be found `in the specification +`_. + +The shim consists of a set of classes which implement the OpenCensus Python API while using +OpenTelemetry constructs behind the scenes. Its purpose is to allow applications which are +already instrumented using OpenCensus to start using OpenTelemetry with minimal effort, without +having to rewrite large portions of the codebase. +""" diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/py.typed b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py new file mode 100644 index 0000000000..8778b43b17 --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.38b0.dev" diff --git a/shim/opentelemetry-opencensus-shim/tests/test_shim.py b/shim/opentelemetry-opencensus-shim/tests/test_shim.py new file mode 100644 index 0000000000..922026ac45 --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/tests/test_shim.py @@ -0,0 +1,20 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + + +class TestShim(unittest.TestCase): + def test_shim(self): + pass diff --git a/tox.ini b/tox.ini index a757f2dd02..9c43776a2c 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,9 @@ envlist = py3{7,8,9,10,11}-opentelemetry-opentracing-shim pypy3-opentelemetry-opentracing-shim + py3{7,8,9,10,11}-opentelemetry-opencensus-shim + ; opencensus-shim intentionally excluded from pypy3 (grpcio install fails) + py3{7,8,9,10,11}-opentelemetry-exporter-jaeger-combined py3{7,8,9,10,11}-opentelemetry-exporter-jaeger-proto-grpc @@ -100,6 +103,7 @@ changedir = semantic-conventions: opentelemetry-semantic-conventions/tests getting-started: docs/getting_started/tests opentracing-shim: shim/opentelemetry-opentracing-shim/tests + opencensus-shim: shim/opentelemetry-opencensus-shim/tests exporter-jaeger-combined: exporter/opentelemetry-exporter-jaeger/tests exporter-jaeger-proto-grpc: exporter/opentelemetry-exporter-jaeger-proto-grpc/tests @@ -152,6 +156,9 @@ commands_pre = opentracing-shim: pip install {toxinidir}/opentelemetry-sdk opentracing-shim: pip install {toxinidir}/shim/opentelemetry-opentracing-shim + opencensus-shim: pip install {toxinidir}/opentelemetry-sdk + opencensus-shim: pip install {toxinidir}/shim/opentelemetry-opencensus-shim + exporter-prometheus: pip install {toxinidir}/exporter/opentelemetry-exporter-prometheus exporter-zipkin-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-zipkin-json @@ -217,12 +224,10 @@ commands_pre = python -m pip install -e {toxinidir}/opentelemetry-api[test] python -m pip install -e {toxinidir}/opentelemetry-semantic-conventions[test] python -m pip install -e {toxinidir}/opentelemetry-sdk[test] - # Pin protobuf version due to lint failing on v3.20.0 - # https://github.com/protocolbuffers/protobuf/issues/9730 - python -m pip install protobuf==3.19.4 python -m pip install -e {toxinidir}/opentelemetry-proto[test] python -m pip install -e {toxinidir}/tests/opentelemetry-test-utils[test] python -m pip install -e {toxinidir}/shim/opentelemetry-opentracing-shim[test] + python -m pip install -e {toxinidir}/shim/opentelemetry-opencensus-shim[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger[test] @@ -236,6 +241,9 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-zipkin[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-b3[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-jaeger[test] + # Pin protobuf version due to lint failing on v3.20.0 + # https://github.com/protocolbuffers/protobuf/issues/9730 + python -m pip install protobuf==3.19.4 commands = python scripts/eachdist.py lint --check-only From f05fa772e65b72a1fdc0a40e142e9342dee013f3 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 2 Mar 2023 18:30:00 -0600 Subject: [PATCH 1399/1517] Rename instrumentation examples (#3206) --- docs/examples/auto-instrumentation/README.rst | 2 +- ..._uninstrumented.py => server_automatic.py} | 0 ...erver_instrumented.py => server_manual.py} | 14 +++--- .../server_programmatic.py | 45 +++++++++++++++++++ 4 files changed, 55 insertions(+), 6 deletions(-) rename docs/examples/auto-instrumentation/{server_uninstrumented.py => server_automatic.py} (100%) rename docs/examples/auto-instrumentation/{server_instrumented.py => server_manual.py} (83%) create mode 100644 docs/examples/auto-instrumentation/server_programmatic.py diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index 3fe9366fb2..b9f3692a37 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -4,4 +4,4 @@ Auto-instrumentation To learn about automatic instrumentation and how to run the example in this directory, see `Automatic Instrumentation`_. -.. _Automatic Instrumentation: https://opentelemetry.io/docs/instrumentation/python/automatic/ +.. _Automatic Instrumentation: https://opentelemetry.io/docs/instrumentation/python/automatic/example diff --git a/docs/examples/auto-instrumentation/server_uninstrumented.py b/docs/examples/auto-instrumentation/server_automatic.py similarity index 100% rename from docs/examples/auto-instrumentation/server_uninstrumented.py rename to docs/examples/auto-instrumentation/server_automatic.py diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_manual.py similarity index 83% rename from docs/examples/auto-instrumentation/server_instrumented.py rename to docs/examples/auto-instrumentation/server_manual.py index 18db356af6..38abc02fb4 100644 --- a/docs/examples/auto-instrumentation/server_instrumented.py +++ b/docs/examples/auto-instrumentation/server_manual.py @@ -14,7 +14,6 @@ from flask import Flask, request -from opentelemetry import trace from opentelemetry.instrumentation.wsgi import collect_request_attributes from opentelemetry.propagate import extract from opentelemetry.sdk.trace import TracerProvider @@ -22,13 +21,18 @@ BatchSpanProcessor, ConsoleSpanExporter, ) +from opentelemetry.trace import ( + SpanKind, + get_tracer_provider, + set_tracer_provider, +) app = Flask(__name__) -trace.set_tracer_provider(TracerProvider()) -tracer = trace.get_tracer_provider().get_tracer(__name__) +set_tracer_provider(TracerProvider()) +tracer = get_tracer_provider().get_tracer(__name__) -trace.get_tracer_provider().add_span_processor( +get_tracer_provider().add_span_processor( BatchSpanProcessor(ConsoleSpanExporter()) ) @@ -38,7 +42,7 @@ def server_request(): with tracer.start_as_current_span( "server_request", context=extract(request.headers), - kind=trace.SpanKind.SERVER, + kind=SpanKind.SERVER, attributes=collect_request_attributes(request.environ), ): print(request.args.get("param")) diff --git a/docs/examples/auto-instrumentation/server_programmatic.py b/docs/examples/auto-instrumentation/server_programmatic.py new file mode 100644 index 0000000000..759613e50d --- /dev/null +++ b/docs/examples/auto-instrumentation/server_programmatic.py @@ -0,0 +1,45 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from flask import Flask, request + +from opentelemetry.instrumentation.flask import FlaskInstrumentor +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, + ConsoleSpanExporter, +) +from opentelemetry.trace import get_tracer_provider, set_tracer_provider + +set_tracer_provider(TracerProvider()) +get_tracer_provider().add_span_processor( + BatchSpanProcessor(ConsoleSpanExporter()) +) + +instrumentor = FlaskInstrumentor() + +app = Flask(__name__) + +instrumentor.instrument_app(app) +# instrumentor.instrument_app(app, excluded_urls="/server_request") + + +@app.route("/server_request") +def server_request(): + print(request.args.get("param")) + return "served" + + +if __name__ == "__main__": + app.run(port=8082) From f0a0cc4f466b427935d2cd5d9787e64f970d69d9 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 13 Mar 2023 10:39:41 -0700 Subject: [PATCH 1400/1517] Add Exponential Histogram (#2964) * Bump werkzeug in /docs/examples/fork-process-model/flask-gunicorn (#3179) Bumps [werkzeug](https://github.com/pallets/werkzeug) from 1.0.1 to 2.2.3. - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/werkzeug/compare/1.0.1...2.2.3) --- updated-dependencies: - dependency-name: werkzeug dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Leighton Chen Co-authored-by: Srikanth Chekuri * Refactor buckets interface * Add missing test case * Remove wrong return * Add comments * Refactor object resetting * More fixes * Refactor exporting of buckets * Fix bug * Fix spelling * Update opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py Co-authored-by: Aaron Abbott * Update opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py Co-authored-by: Aaron Abbott * Fix exception messages * Remove scaling check * Fix typo * Remove wrong instrument * Update opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py Co-authored-by: Aaron Abbott * Add attribute setting lo lock scope * Update opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/buckets.py Co-authored-by: Srikanth Chekuri * Fix collect * Remove unreachable code * Fix exporter test cases * Add comment * Expand lock scope * Revert "Expand lock scope" This reverts commit fd9c47b8c6feb5b8361d5054d199856ae2870d82. * Expand lock scope * Add min_max_size test case * Fix lint * Use properties for buckets attributes * Fix bug * Add comment * WIP * Reset values at the beginning of collect * Merging WIP * Add merge submethods * Refactor to use auxiliary methods * Add downscale method * WIP * Finish merge * Refactor storage of positive and negative WIP * WIP * Add test_merge_simple_event_test * WIP * WIP * WIP * WIP * Fix lint * Fix lint * Add delta test case * Avoid using current_point attributes --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Leighton Chen Co-authored-by: Srikanth Chekuri Co-authored-by: Aaron Abbott --- CHANGELOG.md | 6 +- .../proto/grpc/metric_exporter/__init__.py | 46 + .../tests/test_otlp_metrics_exporter.py | 130 ++- .../sdk/metrics/_internal/aggregation.py | 527 ++++++++- .../exponential_histogram/buckets.py | 176 +++ .../_internal/metric_reader_storage.py | 14 + .../sdk/metrics/_internal/point.py | 42 + .../sdk/metrics/export/__init__.py | 5 +- ...xponential_bucket_histogram_aggregation.py | 1008 +++++++++++++++++ 9 files changed, 1950 insertions(+), 4 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/buckets.py create mode 100644 opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d43b64cfaf..a72276d75d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix formatting of ConsoleMetricExporter. ([#3197](https://github.com/open-telemetry/opentelemetry-python/pull/3197)) -## Version 1.16.0/0.37b0 (2023-02-17) +- Add exponential histogram + ([#2964](https://github.com/open-telemetry/opentelemetry-python/pull/2964)) + +## Version 1.16.0/0.37b0 (2023-02-15) + - Change ``__all__`` to be statically defined. ([#3143](https://github.com/open-telemetry/opentelemetry-python/pull/3143)) - Remove the ability to set a global metric prefix for Prometheus exporter diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index 3c6f59c35f..942ef1d4aa 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -60,6 +60,7 @@ ResourceMetrics, ScopeMetrics, Sum, + ExponentialHistogram as ExponentialHistogramType, ) _logger = getLogger(__name__) @@ -266,6 +267,51 @@ def _translate_data( metric.data.is_monotonic ) pb2_metric.sum.data_points.append(pt) + + elif isinstance(metric.data, ExponentialHistogramType): + for data_point in metric.data.data_points: + + if data_point.positive.bucket_counts: + positive = pb2.ExponentialHistogramDataPoint.Buckets( + offset=data_point.positive.offset, + bucket_counts=data_point.positive.bucket_counts, + ) + else: + positive = None + + if data_point.negative.bucket_counts: + negative = pb2.ExponentialHistogramDataPoint.Buckets( + offset=data_point.negative.offset, + bucket_counts=data_point.negative.bucket_counts, + ) + else: + negative = None + + pt = pb2.ExponentialHistogramDataPoint( + attributes=self._translate_attributes( + data_point.attributes + ), + time_unix_nano=data_point.time_unix_nano, + start_time_unix_nano=( + data_point.start_time_unix_nano + ), + count=data_point.count, + sum=data_point.sum, + scale=data_point.scale, + zero_count=data_point.zero_count, + positive=positive, + negative=negative, + flags=data_point.flags, + max=data_point.max, + min=data_point.min, + ) + pb2_metric.exponential_histogram.aggregation_temporality = ( + metric.data.aggregation_temporality + ) + pb2_metric.exponential_histogram.data_points.append( + pt + ) + else: _logger.warning( "unsupported data type %s", diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py index 31bb878a5f..b38d91eb83 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py @@ -62,7 +62,14 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk.metrics.export import AggregationTemporality, Gauge +from opentelemetry.sdk.metrics.export import AggregationTemporality, Buckets +from opentelemetry.sdk.metrics.export import ( + ExponentialHistogram as ExponentialHistogramType, +) +from opentelemetry.sdk.metrics.export import ( + ExponentialHistogramDataPoint, + Gauge, +) from opentelemetry.sdk.metrics.export import Histogram as HistogramType from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, @@ -163,6 +170,31 @@ def setUp(self): ), ) + exponential_histogram = Metric( + name="exponential_histogram", + description="description", + unit="unit", + data=ExponentialHistogramType( + data_points=[ + ExponentialHistogramDataPoint( + attributes={"a": 1, "b": True}, + start_time_unix_nano=0, + time_unix_nano=1, + count=2, + sum=3, + scale=4, + zero_count=5, + positive=Buckets(offset=6, bucket_counts=[7, 8]), + negative=Buckets(offset=9, bucket_counts=[10, 11]), + flags=12, + min=13.0, + max=14.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + self.metrics = { "sum_int": MetricsData( resource_metrics=[ @@ -276,6 +308,28 @@ def setUp(self): ) ] ), + "exponential_histogram": MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[exponential_histogram], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ), } self.multiple_scope_histogram = MetricsData( @@ -896,6 +950,80 @@ def test_translate_histogram(self): actual = self.exporter._translate_data(self.metrics["histogram"]) self.assertEqual(expected, actual) + def test_translate_exponential_histogram(self): + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="exponential_histogram", + unit="unit", + description="description", + exponential_histogram=pb2.ExponentialHistogram( + data_points=[ + pb2.ExponentialHistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=0, + time_unix_nano=1, + count=2, + sum=3, + scale=4, + zero_count=5, + positive=pb2.ExponentialHistogramDataPoint.Buckets( + offset=6, + bucket_counts=[7, 8], + ), + negative=pb2.ExponentialHistogramDataPoint.Buckets( + offset=9, + bucket_counts=[10, 11], + ), + flags=12, + exemplars=[], + min=13.0, + max=14.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + ], + ) + ], + ) + ] + ) + # pylint: disable=protected-access + actual = self.exporter._translate_data( + self.metrics["exponential_histogram"] + ) + self.assertEqual(expected, actual) + def test_translate_multiple_scope_histogram(self): expected = ExportMetricsServiceRequest( resource_metrics=[ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py index 4fb9041d4c..7312df1aa7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines + from abc import ABC, abstractmethod from bisect import bisect_left from enum import IntEnum @@ -31,8 +33,21 @@ Synchronous, UpDownCounter, ) +from opentelemetry.sdk.metrics._internal.exponential_histogram.buckets import ( + Buckets, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.exponent_mapping import ( + ExponentMapping, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.logarithm_mapping import ( + LogarithmMapping, +) from opentelemetry.sdk.metrics._internal.measurement import Measurement -from opentelemetry.sdk.metrics._internal.point import Gauge +from opentelemetry.sdk.metrics._internal.point import Buckets as BucketsPoint +from opentelemetry.sdk.metrics._internal.point import ( + ExponentialHistogramDataPoint, + Gauge, +) from opentelemetry.sdk.metrics._internal.point import ( Histogram as HistogramPoint, ) @@ -349,6 +364,495 @@ def collect( return current_point +# pylint: disable=protected-access +class _ExponentialBucketHistogramAggregation(_Aggregation[HistogramPoint]): + # _min_max_size and _max_max_size are the smallest and largest values + # the max_size parameter may have, respectively. + + # _min_max_size is is the smallest reasonable value which is small enough + # to contain the entire normal floating point range at the minimum scale. + _min_max_size = 2 + + # _max_max_size is an arbitrary limit meant to limit accidental creation of + # giant exponential bucket histograms. + _max_max_size = 16384 + + def __init__( + self, + attributes: Attributes, + start_time_unix_nano: int, + # This is the default maximum number of buckets per positive or + # negative number range. The value 160 is specified by OpenTelemetry. + # See the derivation here: + # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exponential-bucket-histogram-aggregation) + max_size: int = 160, + ): + super().__init__(attributes) + # max_size is the maximum capacity of the positive and negative + # buckets. + if max_size < self._min_max_size: + raise ValueError( + f"Buckets max size {max_size} is smaller than " + "minimum max size {self._min_max_size}" + ) + + if max_size > self._max_max_size: + raise ValueError( + f"Buckets max size {max_size} is larger than " + "maximum max size {self._max_max_size}" + ) + + self._max_size = max_size + + # _sum is the sum of all the values aggregated by this aggregator. + self._sum = 0 + + # _count is the count of all calls to aggregate. + self._count = 0 + + # _zero_count is the count of all the calls to aggregate when the value + # to be aggregated is exactly 0. + self._zero_count = 0 + + # _min is the smallest value aggregated by this aggregator. + self._min = inf + + # _max is the smallest value aggregated by this aggregator. + self._max = -inf + + # _positive holds the positive values. + self._positive = Buckets() + + # _negative holds the negative values by their absolute value. + self._negative = Buckets() + + # _mapping corresponds to the current scale, is shared by both the + # positive and negative buckets. + self._mapping = LogarithmMapping(LogarithmMapping._max_scale) + + self._instrument_temporality = AggregationTemporality.DELTA + self._start_time_unix_nano = start_time_unix_nano + + self._previous_scale = None + self._previous_start_time_unix_nano = None + self._previous_sum = None + self._previous_max = None + self._previous_min = None + self._previous_positive = None + self._previous_negative = None + + def aggregate(self, measurement: Measurement) -> None: + # pylint: disable=too-many-branches,too-many-statements, too-many-locals + + with self._lock: + + value = measurement.value + + # 0. Set the following attributes: + # _min + # _max + # _count + # _zero_count + # _sum + if value < self._min: + self._min = value + + if value > self._max: + self._max = value + + self._count += 1 + + if value == 0: + self._zero_count += 1 + # No need to do anything else if value is zero, just increment the + # zero count. + return + + self._sum += value + + # 1. Use the positive buckets for positive values and the negative + # buckets for negative values. + if value > 0: + buckets = self._positive + + else: + # Both exponential and logarithm mappings use only positive values + # so the absolute value is used here. + value = -value + buckets = self._negative + + # 2. Compute the index for the value at the current scale. + index = self._mapping.map_to_index(value) + + # IncrementIndexBy starts here + + # 3. Determine if a change of scale is needed. + is_rescaling_needed = False + + if len(buckets) == 0: + buckets.index_start = index + buckets.index_end = index + buckets.index_base = index + + elif ( + index < buckets.index_start + and (buckets.index_end - index) >= self._max_size + ): + is_rescaling_needed = True + low = index + high = buckets.index_end + + elif ( + index > buckets.index_end + and (index - buckets.index_start) >= self._max_size + ): + is_rescaling_needed = True + low = buckets.index_start + high = index + + # 4. Rescale the mapping if needed. + if is_rescaling_needed: + + self._downscale( + self._get_scale_change(low, high), + self._positive, + self._negative, + ) + + index = self._mapping.map_to_index(value) + + # 5. If the index is outside + # [buckets.index_start, buckets.index_end] readjust the buckets + # boundaries or add more buckets. + if index < buckets.index_start: + span = buckets.index_end - index + + if span >= len(buckets.counts): + buckets.grow(span + 1, self._max_size) + + buckets.index_start = index + + elif index > buckets.index_end: + span = index - buckets.index_start + + if span >= len(buckets.counts): + buckets.grow(span + 1, self._max_size) + + buckets.index_end = index + + # 6. Compute the index of the bucket to be incremented. + bucket_index = index - buckets.index_base + + if bucket_index < 0: + bucket_index += len(buckets.counts) + + # 7. Increment the bucket. + buckets.increment_bucket(bucket_index) + + def collect( + self, + aggregation_temporality: AggregationTemporality, + collection_start_nano: int, + ) -> Optional[_DataPointVarT]: + """ + Atomically return a point for the current value of the metric. + """ + # pylint: disable=too-many-statements, too-many-locals + + with self._lock: + if self._count == 0: + return None + + current_negative = self._negative + current_positive = self._positive + current_zero_count = self._zero_count + current_count = self._count + current_start_time_unix_nano = self._start_time_unix_nano + current_sum = self._sum + current_max = self._max + if current_max == -inf: + current_max = None + current_min = self._min + if current_min == inf: + current_min = None + + if self._count == self._zero_count: + current_scale = 0 + + else: + current_scale = self._mapping.scale + + self._negative = Buckets() + self._positive = Buckets() + self._start_time_unix_nano = collection_start_nano + self._sum = 0 + self._count = 0 + self._zero_count = 0 + self._min = inf + self._max = -inf + + current_point = ExponentialHistogramDataPoint( + attributes=self._attributes, + start_time_unix_nano=current_start_time_unix_nano, + time_unix_nano=collection_start_nano, + count=current_count, + sum=current_sum, + scale=current_scale, + zero_count=current_zero_count, + positive=BucketsPoint( + offset=current_positive.offset, + bucket_counts=current_positive.counts, + ), + negative=BucketsPoint( + offset=current_negative.offset, + bucket_counts=current_negative.counts, + ), + # FIXME: Find the right value for flags + flags=0, + min=current_min, + max=current_max, + ) + + if self._previous_scale is None or ( + self._instrument_temporality is aggregation_temporality + ): + self._previous_scale = current_scale + self._previous_start_time_unix_nano = ( + current_start_time_unix_nano + ) + self._previous_max = current_max + self._previous_min = current_min + self._previous_sum = current_sum + self._previous_positive = current_positive + self._previous_negative = current_negative + + return current_point + + min_scale = min(self._previous_scale, current_scale) + + low_positive, high_positive = self._get_low_high_previous_current( + self._previous_positive, current_positive, min_scale + ) + low_negative, high_negative = self._get_low_high_previous_current( + self._previous_negative, current_negative, min_scale + ) + + min_scale = min( + min_scale + - self._get_scale_change(low_positive, high_positive), + min_scale + - self._get_scale_change(low_negative, high_negative), + ) + + # FIXME Go implementation checks if the histogram (not the mapping + # but the histogram) has a count larger than zero, if not, scale + # (the histogram scale) would be zero. See exponential.go 191 + self._downscale( + self._mapping.scale - min_scale, + self._previous_positive, + self._previous_negative, + ) + + if aggregation_temporality is AggregationTemporality.CUMULATIVE: + + start_time_unix_nano = self._previous_start_time_unix_nano + sum_ = current_sum + self._previous_sum + # Only update min/max on delta -> cumulative + max_ = max(current_max, self._previous_max) + min_ = min(current_min, self._previous_min) + + self._merge( + self._previous_positive, + current_positive, + current_scale, + min_scale, + aggregation_temporality, + ) + self._merge( + self._previous_negative, + current_negative, + current_scale, + min_scale, + aggregation_temporality, + ) + + else: + start_time_unix_nano = self._previous_start_time_unix_nano + sum_ = current_sum - self._previous_sum + max_ = current_max + min_ = current_min + + self._merge( + self._previous_positive, + current_positive, + current_scale, + min_scale, + aggregation_temporality, + ) + self._merge( + self._previous_negative, + current_negative, + current_scale, + min_scale, + aggregation_temporality, + ) + + current_point = ExponentialHistogramDataPoint( + attributes=self._attributes, + start_time_unix_nano=start_time_unix_nano, + time_unix_nano=collection_start_nano, + count=current_count, + sum=sum_, + scale=current_scale, + zero_count=current_zero_count, + positive=BucketsPoint( + offset=current_positive.offset, + bucket_counts=current_positive.counts, + ), + negative=BucketsPoint( + offset=current_negative.offset, + bucket_counts=current_negative.counts, + ), + # FIXME: Find the right value for flags + flags=0, + min=min_, + max=max_, + ) + + self._previous_scale = current_scale + self._previous_positive = current_positive + self._previous_negative = current_negative + self._previous_start_time_unix_nano = current_start_time_unix_nano + self._previous_sum = current_sum + + return current_point + + def _get_low_high_previous_current( + self, previous_point_buckets, current_point_buckets, min_scale + ): + + (previous_point_low, previous_point_high) = self._get_low_high( + previous_point_buckets, min_scale + ) + (current_point_low, current_point_high) = self._get_low_high( + current_point_buckets, min_scale + ) + + if current_point_low > current_point_high: + low = previous_point_low + high = previous_point_high + + elif previous_point_low > previous_point_high: + low = current_point_low + high = current_point_high + + else: + low = min(previous_point_low, current_point_low) + high = max(previous_point_high, current_point_high) + + return low, high + + def _get_low_high(self, buckets, min_scale): + if buckets.counts == [0]: + return 0, -1 + + shift = self._mapping._scale - min_scale + + return buckets.index_start >> shift, buckets.index_end >> shift + + def _get_scale_change(self, low, high): + + change = 0 + + while high - low >= self._max_size: + high = high >> 1 + low = low >> 1 + + change += 1 + + return change + + def _downscale(self, change: int, positive, negative): + + if change == 0: + return + + if change < 0: + raise Exception("Invalid change of scale") + + new_scale = self._mapping.scale - change + + positive.downscale(change) + negative.downscale(change) + + if new_scale <= 0: + mapping = ExponentMapping(new_scale) + else: + mapping = LogarithmMapping(new_scale) + + self._mapping = mapping + + def _merge( + self, + previous_buckets, + current_buckets, + current_scale, + min_scale, + aggregation_temporality, + ): + + current_change = current_scale - min_scale + + for current_bucket_index, current_bucket in enumerate( + current_buckets.counts + ): + + if current_bucket == 0: + continue + + # Not considering the case where len(previous_buckets) == 0. This + # would not happen because self._previous_point is only assigned to + # an ExponentialHistogramDataPoint object if self._count != 0. + + index = ( + current_buckets.offset + current_bucket_index + ) >> current_change + + if index < previous_buckets.index_start: + span = previous_buckets.index_end - index + + if span >= self._max_size: + raise Exception("Incorrect merge scale") + + if span >= len(previous_buckets.counts): + previous_buckets.grow(span + 1, self._max_size) + + previous_buckets.index_start = index + + if index > previous_buckets.index_end: + span = index - previous_buckets.index_end + + if span >= self._max_size: + raise Exception("Incorrect merge scale") + + if span >= len(previous_buckets.counts): + previous_buckets.grow(span + 1, self._max_size) + + previous_buckets.index_end = index + + bucket_index = index - previous_buckets.index_base + + if bucket_index < 0: + bucket_index += len(previous_buckets.counts) + + if aggregation_temporality is AggregationTemporality.DELTA: + current_bucket = -current_bucket + + previous_buckets.increment_bucket( + bucket_index, increment=current_bucket + ) + + class Aggregation(ABC): """ Base class for all aggregation types. @@ -433,6 +937,27 @@ def _create_aggregation( raise Exception(f"Invalid instrument type {type(instrument)} found") +class ExponentialBucketHistogramAggregation(Aggregation): + def __init__( + self, + max_size: int = 160, + ): + + self._max_size = max_size + + def _create_aggregation( + self, + instrument: Instrument, + attributes: Attributes, + start_time_unix_nano: int, + ) -> _Aggregation: + return _ExponentialBucketHistogramAggregation( + attributes, + start_time_unix_nano, + max_size=self._max_size, + ) + + class ExplicitBucketHistogramAggregation(Aggregation): """This aggregation informs the SDK to collect: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/buckets.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/buckets.py new file mode 100644 index 0000000000..5c6b04bd39 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/buckets.py @@ -0,0 +1,176 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from math import ceil, log2 + + +class Buckets: + + # No method of this class is protected by locks because instances of this + # class are only used in methods that are protected by locks themselves. + + def __init__(self): + self._counts = [0] + + # The term index refers to the number of the exponential histogram bucket + # used to determine its boundaries. The lower boundary of a bucket is + # determined by base ** index and the upper boundary of a bucket is + # determined by base ** (index + 1). index values are signedto account + # for values less than or equal to 1. + + # self._index_* will all have values equal to a certain index that is + # determined by the corresponding mapping _map_to_index function and + # the value of the index depends on the value passed to _map_to_index. + + # Index of the 0th position in self._counts: self._counts[0] is the + # count in the bucket with index self.__index_base. + self.__index_base = 0 + + # self.__index_start is the smallest index value represented in + # self._counts. + self.__index_start = 0 + + # self.__index_start is the largest index value represented in + # self._counts. + self.__index_end = 0 + + @property + def index_start(self) -> int: + return self.__index_start + + @index_start.setter + def index_start(self, value: int) -> None: + self.__index_start = value + + @property + def index_end(self) -> int: + return self.__index_end + + @index_end.setter + def index_end(self, value: int) -> None: + self.__index_end = value + + @property + def index_base(self) -> int: + return self.__index_base + + @index_base.setter + def index_base(self, value: int) -> None: + self.__index_base = value + + @property + def counts(self): + return self._counts + + def grow(self, needed: int, max_size: int) -> None: + + size = len(self._counts) + bias = self.__index_base - self.__index_start + old_positive_limit = size - bias + + # 2 ** ceil(log2(needed)) finds the smallest power of two that is larger + # or equal than needed: + # 2 ** ceil(log2(1)) == 1 + # 2 ** ceil(log2(2)) == 2 + # 2 ** ceil(log2(3)) == 4 + # 2 ** ceil(log2(4)) == 4 + # 2 ** ceil(log2(5)) == 8 + # 2 ** ceil(log2(6)) == 8 + # 2 ** ceil(log2(7)) == 8 + # 2 ** ceil(log2(8)) == 8 + new_size = min(2 ** ceil(log2(needed)), max_size) + + new_positive_limit = new_size - bias + + tmp = [0] * new_size + tmp[new_positive_limit:] = self._counts[old_positive_limit:] + tmp[0:old_positive_limit] = self._counts[0:old_positive_limit] + self._counts = tmp + + @property + def offset(self) -> int: + return self.__index_start + + def __len__(self) -> int: + if len(self._counts) == 0: + return 0 + + if self.__index_end == self.__index_start and self[0] == 0: + return 0 + + return self.__index_end - self.__index_start + 1 + + def __getitem__(self, key: int) -> int: + bias = self.__index_base - self.__index_start + + if key < bias: + key += len(self._counts) + + key -= bias + + return self._counts[key] + + def downscale(self, amount: int) -> None: + """ + Rotates, then collapses 2 ** amount to 1 buckets. + """ + + bias = self.__index_base - self.__index_start + + if bias != 0: + + self.__index_base = self.__index_start + + # [0, 1, 2, 3, 4] Original backing array + + self._counts = self._counts[::-1] + # [4, 3, 2, 1, 0] + + self._counts = ( + self._counts[:bias][::-1] + self._counts[bias:][::-1] + ) + # [3, 4, 0, 1, 2] This is a rotation of the backing array. + + size = 1 + self.__index_end - self.__index_start + each = 1 << amount + inpos = 0 + outpos = 0 + + pos = self.__index_start + + while pos <= self.__index_end: + mod = pos % each + if mod < 0: + mod += each + + index = mod + + while index < each and inpos < size: + + if outpos != inpos: + self._counts[outpos] += self._counts[inpos] + self._counts[inpos] = 0 + + inpos += 1 + pos += 1 + index += 1 + + outpos += 1 + + self.__index_start >>= amount + self.__index_end >>= amount + self.__index_base = self.__index_start + + def increment_bucket(self, bucket_index: int, increment: int = 1) -> None: + self._counts[bucket_index] += increment diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py index 4602b7b6bc..bef57eaab0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py @@ -31,12 +31,14 @@ ExplicitBucketHistogramAggregation, _DropAggregation, _ExplicitBucketHistogramAggregation, + _ExponentialBucketHistogramAggregation, _LastValueAggregation, _SumAggregation, ) from opentelemetry.sdk.metrics._internal.export import AggregationTemporality from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics._internal.point import ( + ExponentialHistogram, Gauge, Histogram, Metric, @@ -192,6 +194,18 @@ def collect(self) -> MetricsData: ): continue + elif isinstance( + # pylint: disable=protected-access + view_instrument_match._aggregation, + _ExponentialBucketHistogramAggregation, + ): + data = ExponentialHistogram( + data_points=view_instrument_match.collect( + aggregation_temporality, collection_start_nanos + ), + aggregation_temporality=aggregation_temporality, + ) + metrics.append( Metric( # pylint: disable=protected-access diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py index 410c7754d8..cba37e7fdf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py @@ -60,6 +60,48 @@ def to_json(self, indent=4) -> str: return dumps(asdict(self), indent=indent) +@dataclass(frozen=True) +class Buckets: + offset: int + bucket_counts: Sequence[int] + + +@dataclass(frozen=True) +class ExponentialHistogramDataPoint: + """Single data point in a timeseries whose boundaries are defined by an + exponential function. This timeseries describes the time-varying scalar + value of a metric. + """ + + attributes: Attributes + start_time_unix_nano: int + time_unix_nano: int + count: int + sum: Union[int, float] + scale: int + zero_count: int + positive: Buckets + negative: Buckets + flags: int + min: float + max: float + + def to_json(self, indent=4) -> str: + return dumps(asdict(self), indent=indent) + + +@dataclass(frozen=True) +class ExponentialHistogram: + """Represents the type of a metric that is calculated by aggregating as an + ExponentialHistogram of all reported measurements over a time interval. + """ + + data_points: Sequence[ExponentialHistogramDataPoint] + aggregation_temporality: ( + "opentelemetry.sdk.metrics.export.AggregationTemporality" + ) + + @dataclass(frozen=True) class Sum: """Represents the type of a scalar metric that is calculated as a sum of diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index a87beadce5..97c31b97ec 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -24,9 +24,12 @@ ) # The point module is not in the export directory to avoid a circular import. -from opentelemetry.sdk.metrics._internal.point import ( +from opentelemetry.sdk.metrics._internal.point import ( # noqa: F401 + Buckets, DataPointT, DataT, + ExponentialHistogram, + ExponentialHistogramDataPoint, Gauge, Histogram, HistogramDataPoint, diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py new file mode 100644 index 0000000000..65a437bc6d --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py @@ -0,0 +1,1008 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from itertools import permutations +from math import ldexp +from sys import float_info +from types import MethodType +from unittest import TestCase +from unittest.mock import Mock, patch + +from opentelemetry.sdk.metrics._internal.aggregation import ( + AggregationTemporality, + _ExponentialBucketHistogramAggregation, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.buckets import ( + Buckets, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.exponent_mapping import ( + ExponentMapping, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( + MAX_NORMAL_EXPONENT, + MIN_NORMAL_EXPONENT, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.logarithm_mapping import ( + LogarithmMapping, +) +from opentelemetry.sdk.metrics._internal.measurement import Measurement + + +def get_counts(buckets: Buckets) -> int: + + counts = [] + + for index in range(len(buckets)): + counts.append(buckets[index]) + + return counts + + +def center_val(mapping: ExponentMapping, index: int) -> float: + return ( + mapping.get_lower_boundary(index) + + mapping.get_lower_boundary(index + 1) + ) / 2 + + +def swap( + first: _ExponentialBucketHistogramAggregation, + second: _ExponentialBucketHistogramAggregation, +): + + for attribute in [ + "_positive", + "_negative", + "_sum", + "_count", + "_zero_count", + "_min", + "_max", + "_mapping", + ]: + temp = getattr(first, attribute) + setattr(first, attribute, getattr(second, attribute)) + setattr(second, attribute, temp) + + +class TestExponentialBucketHistogramAggregation(TestCase): + def assertInEpsilon(self, first, second, epsilon): + self.assertLessEqual(first, (second * (1 + epsilon))) + self.assertGreaterEqual(first, (second * (1 - epsilon))) + + def require_equal(self, a, b): + + if a._sum == 0 or b._sum == 0: + self.assertAlmostEqual(a._sum, b._sum, 1e-6) + else: + self.assertInEpsilon(a._sum, b._sum, 1e-6) + + self.assertEqual(a._count, b._count) + self.assertEqual(a._zero_count, b._zero_count) + + self.assertEqual(a._mapping.scale, b._mapping.scale) + + self.assertEqual(len(a._positive), len(b._positive)) + self.assertEqual(len(a._negative), len(b._negative)) + + for index in range(len(a._positive)): + self.assertEqual(a._positive[index], b._positive[index]) + + for index in range(len(a._negative)): + self.assertEqual(a._negative[index], b._negative[index]) + + def test_alternating_growth_0(self): + """ + Tests insertion of [2, 4, 1]. The index of 2 (i.e., 0) becomes + `indexBase`, the 4 goes to its right and the 1 goes in the last + position of the backing array. With 3 binary orders of magnitude + and MaxSize=4, this must finish with scale=0; with minimum value 1 + this must finish with offset=-1 (all scales). + + """ + + # The corresponding Go test is TestAlternatingGrowth1 where: + # agg := NewFloat64(NewConfig(WithMaxSize(4))) + # agg is an instance of github.com/lightstep/otel-launcher-go/lightstep/sdk/metric/aggregator/histogram/structure.Histogram[float64] + + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock(), max_size=4) + ) + + exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) + exponential_histogram_aggregation.aggregate(Measurement(4, Mock())) + exponential_histogram_aggregation.aggregate(Measurement(1, Mock())) + + self.assertEqual( + exponential_histogram_aggregation._positive.offset, -1 + ) + self.assertEqual(exponential_histogram_aggregation._mapping.scale, 0) + self.assertEqual( + get_counts(exponential_histogram_aggregation._positive), [1, 1, 1] + ) + + def test_alternating_growth_1(self): + """ + Tests insertion of [2, 2, 4, 1, 8, 0.5]. The test proceeds as¶ + above but then downscales once further to scale=-1, thus index -1¶ + holds range [0.25, 1.0), index 0 holds range [1.0, 4), index 1¶ + holds range [4, 16).¶ + """ + + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock(), max_size=4) + ) + + exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) + exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) + exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) + exponential_histogram_aggregation.aggregate(Measurement(1, Mock())) + exponential_histogram_aggregation.aggregate(Measurement(8, Mock())) + exponential_histogram_aggregation.aggregate(Measurement(0.5, Mock())) + + self.assertEqual( + exponential_histogram_aggregation._positive.offset, -1 + ) + self.assertEqual(exponential_histogram_aggregation._mapping.scale, -1) + self.assertEqual( + get_counts(exponential_histogram_aggregation._positive), [2, 3, 1] + ) + + def test_permutations(self): + """ + Tests that every permutation of certain sequences with maxSize=2 + results¶ in the same scale=-1 histogram. + """ + + for test_values, expected in [ + [ + [0.5, 1.0, 2.0], + { + "scale": -1, + "offset": -1, + "len": 2, + "at_0": 2, + "at_1": 1, + }, + ], + [ + [1.0, 2.0, 4.0], + { + "scale": -1, + "offset": -1, + "len": 2, + "at_0": 1, + "at_1": 2, + }, + ], + [ + [0.25, 0.5, 1], + { + "scale": -1, + "offset": -2, + "len": 2, + "at_0": 1, + "at_1": 2, + }, + ], + ]: + + for permutation in permutations(test_values): + + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation( + Mock(), Mock(), max_size=2 + ) + ) + + for value in permutation: + + exponential_histogram_aggregation.aggregate( + Measurement(value, Mock()) + ) + + self.assertEqual( + exponential_histogram_aggregation._mapping.scale, + expected["scale"], + ) + self.assertEqual( + exponential_histogram_aggregation._positive.offset, + expected["offset"], + ) + self.assertEqual( + len(exponential_histogram_aggregation._positive), + expected["len"], + ) + self.assertEqual( + exponential_histogram_aggregation._positive[0], + expected["at_0"], + ) + self.assertEqual( + exponential_histogram_aggregation._positive[1], + expected["at_1"], + ) + + def test_ascending_sequence(self): + + for max_size in [3, 4, 6, 9]: + for offset in range(-5, 6): + for init_scale in [0, 4]: + self.ascending_sequence_test(max_size, offset, init_scale) + + def ascending_sequence_test( + self, max_size: int, offset: int, init_scale: int + ): + + for step in range(max_size, max_size * 4): + + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation( + Mock(), Mock(), max_size=max_size + ) + ) + + if init_scale <= 0: + mapping = ExponentMapping(init_scale) + else: + mapping = LogarithmMapping(init_scale) + + min_val = center_val(mapping, offset) + max_val = center_val(mapping, offset + step) + + sum_ = 0.0 + + for index in range(max_size): + value = center_val(mapping, offset + index) + exponential_histogram_aggregation.aggregate( + Measurement(value, Mock()) + ) + sum_ += value + + self.assertEqual( + init_scale, exponential_histogram_aggregation._mapping._scale + ) + self.assertEqual( + offset, exponential_histogram_aggregation._positive.offset + ) + + exponential_histogram_aggregation.aggregate( + Measurement(max_val, Mock()) + ) + sum_ += max_val + + self.assertNotEqual( + 0, exponential_histogram_aggregation._positive[0] + ) + + # The maximum-index filled bucket is at or + # above the mid-point, (otherwise we + # downscaled too much). + + max_fill = 0 + total_count = 0 + + for index in range( + len(exponential_histogram_aggregation._positive) + ): + total_count += exponential_histogram_aggregation._positive[ + index + ] + if exponential_histogram_aggregation._positive[index] != 0: + max_fill = index + + # FIXME the corresponding Go code is + # require.GreaterOrEqual(t, maxFill, uint32(maxSize)/2), make sure + # this is actually equivalent. + self.assertGreaterEqual(max_fill, int(max_size / 2)) + + self.assertGreaterEqual(max_size + 1, total_count) + self.assertGreaterEqual( + max_size + 1, exponential_histogram_aggregation._count + ) + self.assertGreaterEqual( + sum_, exponential_histogram_aggregation._sum + ) + + if init_scale <= 0: + mapping = ExponentMapping( + exponential_histogram_aggregation._mapping.scale + ) + else: + mapping = LogarithmMapping( + exponential_histogram_aggregation._mapping.scale + ) + index = mapping.map_to_index(min_val) + + self.assertEqual( + index, exponential_histogram_aggregation._positive.offset + ) + + index = mapping.map_to_index(max_val) + + self.assertEqual( + index, + exponential_histogram_aggregation._positive.offset + + len(exponential_histogram_aggregation._positive) + - 1, + ) + + def test_reset(self): + + for increment in [0x1, 0x100, 0x10000, 0x100000000, 0x200000000]: + + def mock_increment(self, bucket_index: int) -> None: + """ + Increments a bucket + """ + + self._counts[bucket_index] += increment + + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation( + Mock(), Mock(), max_size=256 + ) + ) + + self.assertEqual( + exponential_histogram_aggregation._count, + exponential_histogram_aggregation._zero_count, + ) + self.assertEqual(0, exponential_histogram_aggregation._sum) + expect = 0 + + for value in range(2, 257): + expect += value * increment + with patch.object( + exponential_histogram_aggregation._positive, + "increment_bucket", + # new=positive_mock + MethodType( + mock_increment, + exponential_histogram_aggregation._positive, + ), + ): + exponential_histogram_aggregation.aggregate( + Measurement(value, Mock()) + ) + exponential_histogram_aggregation._count *= increment + exponential_histogram_aggregation._sum *= increment + + self.assertEqual(expect, exponential_histogram_aggregation._sum) + self.assertEqual( + 255 * increment, exponential_histogram_aggregation._count + ) + + # See test_integer_aggregation about why scale is 5, len is + # 256 - (1 << scale)- 1 and offset is (1 << scale) - 1. + scale = exponential_histogram_aggregation._mapping.scale + self.assertEqual(5, scale) + + self.assertEqual( + 256 - ((1 << scale) - 1), + len(exponential_histogram_aggregation._positive), + ) + self.assertEqual( + (1 << scale) - 1, + exponential_histogram_aggregation._positive.offset, + ) + + for index in range(0, 256): + self.assertLessEqual( + exponential_histogram_aggregation._positive[index], + 6 * increment, + ) + + def test_move_into(self): + + exponential_histogram_aggregation_0 = ( + _ExponentialBucketHistogramAggregation( + Mock(), Mock(), max_size=256 + ) + ) + exponential_histogram_aggregation_1 = ( + _ExponentialBucketHistogramAggregation( + Mock(), Mock(), max_size=256 + ) + ) + + expect = 0 + + for index in range(2, 257): + expect += index + exponential_histogram_aggregation_0.aggregate( + Measurement(index, Mock()) + ) + exponential_histogram_aggregation_0.aggregate( + Measurement(0, Mock()) + ) + + swap( + exponential_histogram_aggregation_0, + exponential_histogram_aggregation_1, + ) + + self.assertEqual(0, exponential_histogram_aggregation_0._sum) + self.assertEqual(0, exponential_histogram_aggregation_0._count) + self.assertEqual(0, exponential_histogram_aggregation_0._zero_count) + + self.assertEqual(expect, exponential_histogram_aggregation_1._sum) + self.assertEqual(255 * 2, exponential_histogram_aggregation_1._count) + self.assertEqual(255, exponential_histogram_aggregation_1._zero_count) + + scale = exponential_histogram_aggregation_1._mapping.scale + self.assertEqual(5, scale) + + self.assertEqual( + 256 - ((1 << scale) - 1), + len(exponential_histogram_aggregation_1._positive), + ) + self.assertEqual( + (1 << scale) - 1, + exponential_histogram_aggregation_1._positive.offset, + ) + + for index in range(0, 256): + self.assertLessEqual( + exponential_histogram_aggregation_1._positive[index], 6 + ) + + def test_very_large_numbers(self): + + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock(), max_size=2) + ) + + def expect_balanced(count: int): + self.assertEqual( + 2, len(exponential_histogram_aggregation._positive) + ) + self.assertEqual( + -1, exponential_histogram_aggregation._positive.offset + ) + self.assertEqual( + count, exponential_histogram_aggregation._positive[0] + ) + self.assertEqual( + count, exponential_histogram_aggregation._positive[1] + ) + + exponential_histogram_aggregation.aggregate( + Measurement(2**-100, Mock()) + ) + exponential_histogram_aggregation.aggregate( + Measurement(2**100, Mock()) + ) + + self.assertLessEqual( + 2**100, (exponential_histogram_aggregation._sum * (1 + 1e-5)) + ) + self.assertGreaterEqual( + 2**100, (exponential_histogram_aggregation._sum * (1 - 1e-5)) + ) + + self.assertEqual(2, exponential_histogram_aggregation._count) + self.assertEqual(-7, exponential_histogram_aggregation._mapping.scale) + + expect_balanced(1) + + exponential_histogram_aggregation.aggregate( + Measurement(2**-127, Mock()) + ) + exponential_histogram_aggregation.aggregate( + Measurement(2**128, Mock()) + ) + + self.assertLessEqual( + 2**128, (exponential_histogram_aggregation._sum * (1 + 1e-5)) + ) + self.assertGreaterEqual( + 2**128, (exponential_histogram_aggregation._sum * (1 - 1e-5)) + ) + + self.assertEqual(4, exponential_histogram_aggregation._count) + self.assertEqual(-7, exponential_histogram_aggregation._mapping.scale) + + expect_balanced(2) + + exponential_histogram_aggregation.aggregate( + Measurement(2**-129, Mock()) + ) + exponential_histogram_aggregation.aggregate( + Measurement(2**255, Mock()) + ) + + self.assertLessEqual( + 2**255, (exponential_histogram_aggregation._sum * (1 + 1e-5)) + ) + self.assertGreaterEqual( + 2**255, (exponential_histogram_aggregation._sum * (1 - 1e-5)) + ) + self.assertEqual(6, exponential_histogram_aggregation._count) + self.assertEqual(-8, exponential_histogram_aggregation._mapping.scale) + + expect_balanced(3) + + def test_full_range(self): + + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock(), max_size=2) + ) + + exponential_histogram_aggregation.aggregate( + Measurement(float_info.max, Mock()) + ) + exponential_histogram_aggregation.aggregate(Measurement(1, Mock())) + exponential_histogram_aggregation.aggregate( + Measurement(2**-1074, Mock()) + ) + + self.assertEqual( + float_info.max, exponential_histogram_aggregation._sum + ) + self.assertEqual(3, exponential_histogram_aggregation._count) + self.assertEqual( + ExponentMapping._min_scale, + exponential_histogram_aggregation._mapping.scale, + ) + + self.assertEqual( + _ExponentialBucketHistogramAggregation._min_max_size, + len(exponential_histogram_aggregation._positive), + ) + self.assertEqual( + -1, exponential_histogram_aggregation._positive.offset + ) + self.assertLessEqual(exponential_histogram_aggregation._positive[0], 2) + self.assertLessEqual(exponential_histogram_aggregation._positive[1], 1) + + def test_aggregator_min_max(self): + + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock()) + ) + + for value in [1, 3, 5, 7, 9]: + exponential_histogram_aggregation.aggregate( + Measurement(value, Mock()) + ) + + self.assertEqual(1, exponential_histogram_aggregation._min) + self.assertEqual(9, exponential_histogram_aggregation._max) + + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock()) + ) + + for value in [-1, -3, -5, -7, -9]: + exponential_histogram_aggregation.aggregate( + Measurement(value, Mock()) + ) + + self.assertEqual(-9, exponential_histogram_aggregation._min) + self.assertEqual(-1, exponential_histogram_aggregation._max) + + def test_aggregator_copy_swap(self): + + exponential_histogram_aggregation_0 = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock()) + ) + for value in [1, 3, 5, 7, 9, -1, -3, -5]: + exponential_histogram_aggregation_0.aggregate( + Measurement(value, Mock()) + ) + exponential_histogram_aggregation_1 = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock()) + ) + for value in [5, 4, 3, 2]: + exponential_histogram_aggregation_1.aggregate( + Measurement(value, Mock()) + ) + exponential_histogram_aggregation_2 = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock()) + ) + + swap( + exponential_histogram_aggregation_0, + exponential_histogram_aggregation_1, + ) + + exponential_histogram_aggregation_2._positive.__init__() + exponential_histogram_aggregation_2._negative.__init__() + exponential_histogram_aggregation_2._sum = 0 + exponential_histogram_aggregation_2._count = 0 + exponential_histogram_aggregation_2._zero_count = 0 + exponential_histogram_aggregation_2._min = 0 + exponential_histogram_aggregation_2._max = 0 + exponential_histogram_aggregation_2._mapping = LogarithmMapping( + LogarithmMapping._max_scale + ) + + for attribute in [ + "_positive", + "_negative", + "_sum", + "_count", + "_zero_count", + "_min", + "_max", + "_mapping", + ]: + setattr( + exponential_histogram_aggregation_2, + attribute, + getattr(exponential_histogram_aggregation_1, attribute), + ) + + self.require_equal( + exponential_histogram_aggregation_1, + exponential_histogram_aggregation_2, + ) + + def test_zero_count_by_increment(self): + + exponential_histogram_aggregation_0 = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock()) + ) + + increment = 10 + + for _ in range(increment): + exponential_histogram_aggregation_0.aggregate( + Measurement(0, Mock()) + ) + exponential_histogram_aggregation_1 = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock()) + ) + + # positive_mock = Mock(wraps=exponential_histogram_aggregation_1._positive) + def mock_increment(self, bucket_index: int) -> None: + """ + Increments a bucket + """ + + self._counts[bucket_index] += increment + + with patch.object( + exponential_histogram_aggregation_1._positive, + "increment_bucket", + # new=positive_mock + MethodType( + mock_increment, exponential_histogram_aggregation_1._positive + ), + ): + exponential_histogram_aggregation_1.aggregate( + Measurement(0, Mock()) + ) + exponential_histogram_aggregation_1._count *= increment + exponential_histogram_aggregation_1._zero_count *= increment + + self.require_equal( + exponential_histogram_aggregation_0, + exponential_histogram_aggregation_1, + ) + + def test_one_count_by_increment(self): + + exponential_histogram_aggregation_0 = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock()) + ) + + increment = 10 + + for _ in range(increment): + exponential_histogram_aggregation_0.aggregate( + Measurement(1, Mock()) + ) + exponential_histogram_aggregation_1 = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock()) + ) + + # positive_mock = Mock(wraps=exponential_histogram_aggregation_1._positive) + def mock_increment(self, bucket_index: int) -> None: + """ + Increments a bucket + """ + + self._counts[bucket_index] += increment + + with patch.object( + exponential_histogram_aggregation_1._positive, + "increment_bucket", + # new=positive_mock + MethodType( + mock_increment, exponential_histogram_aggregation_1._positive + ), + ): + exponential_histogram_aggregation_1.aggregate( + Measurement(1, Mock()) + ) + exponential_histogram_aggregation_1._count *= increment + exponential_histogram_aggregation_1._sum *= increment + + self.require_equal( + exponential_histogram_aggregation_0, + exponential_histogram_aggregation_1, + ) + + def test_boundary_statistics(self): + + total = MAX_NORMAL_EXPONENT - MIN_NORMAL_EXPONENT + 1 + + for scale in range( + LogarithmMapping._min_scale, LogarithmMapping._max_scale + 1 + ): + + above = 0 + below = 0 + + if scale <= 0: + mapping = ExponentMapping(scale) + else: + mapping = LogarithmMapping(scale) + + for exp in range(MIN_NORMAL_EXPONENT, MAX_NORMAL_EXPONENT + 1): + value = ldexp(1, exp) + + index = mapping.map_to_index(value) + + try: + boundary = mapping.get_lower_boundary(index + 1) + except Exception as error: + raise error + self.fail(f"Unexpected exception {error} raised") + + if boundary < value: + above += 1 + elif boundary > value: + below += 1 + + self.assertInEpsilon(0.5, above / total, 0.05) + self.assertInEpsilon(0.5, below / total, 0.06) + + def test_min_max_size(self): + """ + Tests that the minimum max_size is the right value. + """ + + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation( + Mock(), + Mock(), + max_size=_ExponentialBucketHistogramAggregation._min_max_size, + ) + ) + + # The minimum and maximum normal floating point values are used here to + # make sure the mapping can contain the full range of values. + exponential_histogram_aggregation.aggregate(Mock(value=float_info.min)) + exponential_histogram_aggregation.aggregate(Mock(value=float_info.max)) + + # This means the smallest max_scale is enough for the full range of the + # normal floating point values. + self.assertEqual( + len(exponential_histogram_aggregation._positive._counts), + exponential_histogram_aggregation._min_max_size, + ) + + def test_aggregate_collect(self): + """ + Tests a repeated cycle of aggregation and collection. + """ + """ + try: + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation( + Mock(), + Mock(), + ) + ) + + exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) + exponential_histogram_aggregation.collect( + AggregationTemporality.CUMULATIVE, 0 + ) + exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) + exponential_histogram_aggregation.collect( + AggregationTemporality.CUMULATIVE, 0 + ) + exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) + exponential_histogram_aggregation.collect( + AggregationTemporality.CUMULATIVE, 0 + ) + except Exception as error: + self.fail(f"Unexpected exception raised: {error}") + """ + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation( + Mock(), + Mock(), + ) + ) + + exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) + exponential_histogram_aggregation.collect( + AggregationTemporality.CUMULATIVE, 0 + ) + exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) + exponential_histogram_aggregation.collect( + AggregationTemporality.CUMULATIVE, 0 + ) + exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) + exponential_histogram_aggregation.collect( + AggregationTemporality.CUMULATIVE, 0 + ) + + def test_collect_results_cumulative(self): + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation( + Mock(), + Mock(), + ) + ) + + self.assertEqual(exponential_histogram_aggregation._mapping._scale, 20) + + exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) + self.assertEqual(exponential_histogram_aggregation._mapping._scale, 20) + + exponential_histogram_aggregation.aggregate(Measurement(4, Mock())) + self.assertEqual(exponential_histogram_aggregation._mapping._scale, 7) + + exponential_histogram_aggregation.aggregate(Measurement(1, Mock())) + self.assertEqual(exponential_histogram_aggregation._mapping._scale, 6) + + collection_0 = exponential_histogram_aggregation.collect( + AggregationTemporality.CUMULATIVE, Mock() + ) + + self.assertEqual(len(collection_0.positive.bucket_counts), 160) + + self.assertEqual(collection_0.count, 3) + self.assertEqual(collection_0.sum, 7) + self.assertEqual(collection_0.scale, 6) + self.assertEqual(collection_0.zero_count, 0) + self.assertEqual( + collection_0.positive.bucket_counts, + [1, *[0] * 63, 1, *[0] * 31, 1, *[0] * 63], + ) + self.assertEqual(collection_0.flags, 0) + self.assertEqual(collection_0.min, 1) + self.assertEqual(collection_0.max, 4) + + exponential_histogram_aggregation.aggregate(Measurement(1, Mock())) + exponential_histogram_aggregation.aggregate(Measurement(8, Mock())) + exponential_histogram_aggregation.aggregate(Measurement(0.5, Mock())) + exponential_histogram_aggregation.aggregate(Measurement(0.1, Mock())) + exponential_histogram_aggregation.aggregate(Measurement(0.045, Mock())) + + collection_1 = exponential_histogram_aggregation.collect( + AggregationTemporality.CUMULATIVE, Mock() + ) + + previous_count = collection_1.positive.bucket_counts[0] + + count_counts = [[previous_count, 0]] + + for count in collection_1.positive.bucket_counts: + if count == previous_count: + count_counts[-1][1] += 1 + else: + previous_count = count + count_counts.append([previous_count, 1]) + + self.assertEqual(collection_1.count, 5) + self.assertEqual(collection_1.sum, 16.645) + self.assertEqual(collection_1.scale, 4) + self.assertEqual(collection_1.zero_count, 0) + + self.assertEqual( + collection_1.positive.bucket_counts, + [ + 1, + *[0] * 15, + 1, + *[0] * 47, + 1, + *[0] * 40, + 1, + *[0] * 17, + 1, + *[0] * 36, + ], + ) + self.assertEqual(collection_1.flags, 0) + self.assertEqual(collection_1.min, 0.045) + self.assertEqual(collection_1.max, 8) + + def test_merge_collect_cumulative(self): + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock(), max_size=4) + ) + + for value in [2, 4, 8, 16]: + exponential_histogram_aggregation.aggregate( + Measurement(value, Mock()) + ) + + self.assertEqual(exponential_histogram_aggregation._mapping.scale, 0) + self.assertEqual(exponential_histogram_aggregation._positive.offset, 0) + self.assertEqual( + exponential_histogram_aggregation._positive.counts, [1, 1, 1, 1] + ) + + result = exponential_histogram_aggregation.collect( + AggregationTemporality.CUMULATIVE, + 0, + ) + + for value in [1, 2, 4, 8]: + exponential_histogram_aggregation.aggregate( + Measurement(1 / value, Mock()) + ) + + self.assertEqual(exponential_histogram_aggregation._mapping.scale, 0) + self.assertEqual( + exponential_histogram_aggregation._positive.offset, -4 + ) + self.assertEqual( + exponential_histogram_aggregation._positive.counts, [1, 1, 1, 1] + ) + + result_1 = exponential_histogram_aggregation.collect( + AggregationTemporality.CUMULATIVE, + 0, + ) + + self.assertEqual(result.scale, result_1.scale) + + def test_merge_collect_delta(self): + exponential_histogram_aggregation = ( + _ExponentialBucketHistogramAggregation(Mock(), Mock(), max_size=4) + ) + + for value in [2, 4, 8, 16]: + exponential_histogram_aggregation.aggregate( + Measurement(value, Mock()) + ) + + self.assertEqual(exponential_histogram_aggregation._mapping.scale, 0) + self.assertEqual(exponential_histogram_aggregation._positive.offset, 0) + self.assertEqual( + exponential_histogram_aggregation._positive.counts, [1, 1, 1, 1] + ) + + result = exponential_histogram_aggregation.collect( + AggregationTemporality.DELTA, + 0, + ) + + for value in [1, 2, 4, 8]: + exponential_histogram_aggregation.aggregate( + Measurement(1 / value, Mock()) + ) + + self.assertEqual(exponential_histogram_aggregation._mapping.scale, 0) + self.assertEqual( + exponential_histogram_aggregation._positive.offset, -4 + ) + self.assertEqual( + exponential_histogram_aggregation._positive.counts, [1, 1, 1, 1] + ) + + result_1 = exponential_histogram_aggregation.collect( + AggregationTemporality.DELTA, + 0, + ) + + self.assertEqual(result.scale, result_1.scale) From af582e9c5139d02300f0b539e07d611a46fb9369 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 14 Mar 2023 22:11:07 +0530 Subject: [PATCH 1401/1517] Fix docker config mapping (#3215) --- docs/examples/logs/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/logs/README.rst b/docs/examples/logs/README.rst index a15150a1a8..8a91c7f695 100644 --- a/docs/examples/logs/README.rst +++ b/docs/examples/logs/README.rst @@ -34,7 +34,7 @@ Then start the Docker container: docker run \ -p 4317:4317 \ - -v $(pwd)/otel-collector-config.yaml:/etc/otel/config.yaml \ + -v $(pwd)/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml \ otel/opentelemetry-collector-contrib:latest .. code-block:: sh From c84ba9483ca9b4642904b69897f931813d60cff0 Mon Sep 17 00:00:00 2001 From: Girish Chandrashekar Date: Tue, 14 Mar 2023 19:38:43 +0100 Subject: [PATCH 1402/1517] Implement shutdown procedure for OTLP grpc exporters (#3138) * Implement shutdown procedure for OTLP grpc exporters - Add `_shutdown` variable for checking if the exporter has been shutdown. - Prevent export if the `_shutdown` flag has been set. Log a warning message is exporter has been shutdown. - Use thread lock to synchronize the last export call before shutdown timeout. The `shutdown` method will wait until the `timeout_millis` if there is an ongoing export. If there is no ongiong export, set the `_shutdown` flag to prevent further exports and return. - Add unit tests for the `OTLPExporterMixIn` and the sub classes for traces and metrics. * lint files * add changelog entry for fix * lint test files --------- Co-authored-by: Srikanth Chekuri Co-authored-by: Leighton Chen --- CHANGELOG.md | 27 ++-- .../exporter/otlp/proto/grpc/exporter.py | 131 ++++++++++-------- .../proto/grpc/metric_exporter/__init__.py | 2 +- .../proto/grpc/trace_exporter/__init__.py | 3 + .../tests/test_otlp_exporter_mixin.py | 93 ++++++++++++- .../tests/test_otlp_metrics_exporter.py | 49 ++++++- .../tests/test_otlp_trace_exporter.py | 44 ++++++ 7 files changed, 277 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a72276d75d..31dd72bdcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased + - PeriodicExportingMetricReader will continue if collection times out ([#3100](https://github.com/open-telemetry/opentelemetry-python/pull/3100)) - Fix formatting of ConsoleMetricExporter. ([#3197](https://github.com/open-telemetry/opentelemetry-python/pull/3197)) - +- Implement shutdown procedure forOTLP grpc exporters + ([#3138](https://github.com/open-telemetry/opentelemetry-python/pull/3138)) - Add exponential histogram ([#2964](https://github.com/open-telemetry/opentelemetry-python/pull/2964)) -## Version 1.16.0/0.37b0 (2023-02-15) +## Version 1.16.0/0.37b0 (2023-02-17) - Change ``__all__`` to be statically defined. ([#3143](https://github.com/open-telemetry/opentelemetry-python/pull/3143)) @@ -398,7 +400,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-distro` & `opentelemetry-sdk` Moved Auto Instrumentation Configurator code to SDK to let distros use its default implementation ([#1937](https://github.com/open-telemetry/opentelemetry-python/pull/1937)) -- Add Trace ID validation to meet [TraceID spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#spancontext) ([#1992](https://github.com/open-telemetry/opentelemetry-python/pull/1992)) +- Add Trace ID validation to + meet [TraceID spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#spancontext) ([#1992](https://github.com/open-telemetry/opentelemetry-python/pull/1992)) - Fixed Python 3.10 incompatibility in `opentelemetry-opentracing-shim` tests ([#2018](https://github.com/open-telemetry/opentelemetry-python/pull/2018)) - `opentelemetry-sdk` added support for `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` @@ -729,7 +732,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1471](https://github.com/open-telemetry/opentelemetry-python/pull/1471)) - Add support for Python 3.9 ([#1441](https://github.com/open-telemetry/opentelemetry-python/pull/1441)) -- Added the ability to disable instrumenting libraries specified by OTEL_PYTHON_DISABLED_INSTRUMENTATIONS env variable, when using opentelemetry-instrument command. +- Added the ability to disable instrumenting libraries specified by OTEL_PYTHON_DISABLED_INSTRUMENTATIONS env variable, + when using opentelemetry-instrument command. ([#1461](https://github.com/open-telemetry/opentelemetry-python/pull/1461)) - Add `fields` to propagators ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) @@ -778,7 +782,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1533](https://github.com/open-telemetry/opentelemetry-python/pull/1533)) - `opentelemetry-sdk` The JaegerPropagator has been moved into its own package: `opentelemetry-propagator-jaeger` ([#1525](https://github.com/open-telemetry/opentelemetry-python/pull/1525)) -- `opentelemetry-exporter-jaeger`, `opentelemetry-exporter-zipkin` Update InstrumentationInfo tag keys for Jaeger and Zipkin exporters +- `opentelemetry-exporter-jaeger`, `opentelemetry-exporter-zipkin` Update InstrumentationInfo tag keys for Jaeger and + Zipkin exporters ([#1535](https://github.com/open-telemetry/opentelemetry-python/pull/1535)) - `opentelemetry-sdk` Remove rate property setter from TraceIdRatioBasedSampler ([#1536](https://github.com/open-telemetry/opentelemetry-python/pull/1536)) @@ -888,7 +893,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1199](https://github.com/open-telemetry/opentelemetry-python/pull/1199)) - Add Global Error Handler ([#1080](https://github.com/open-telemetry/opentelemetry-python/pull/1080)) -- Add support for `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_SCHEDULE_DELAY_MILLIS`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` and `OTEL_BSP_EXPORT_TIMEOUT_MILLIS` environment variables +- Add support for `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_SCHEDULE_DELAY_MILLIS`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` + and `OTEL_BSP_EXPORT_TIMEOUT_MILLIS` environment variables ([#1105](https://github.com/open-telemetry/opentelemetry-python/pull/1120)) - Adding Resource to MeterRecord ([#1209](https://github.com/open-telemetry/opentelemetry-python/pull/1209)) @@ -913,7 +919,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1151](https://github.com/open-telemetry/opentelemetry-python/pull/1151)) - Fixed OTLP events to Zipkin annotations translation. ([#1161](https://github.com/open-telemetry/opentelemetry-python/pull/1161)) -- Fixed bootstrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask. +- Fixed bootstrap command to correctly install opentelemetry-instrumentation-falcon instead of + opentelemetry-instrumentation-flask. ([#1138](https://github.com/open-telemetry/opentelemetry-python/pull/1138)) - Update sampling result names ([#1128](https://github.com/open-telemetry/opentelemetry-python/pull/1128)) @@ -923,7 +930,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1203](https://github.com/open-telemetry/opentelemetry-python/pull/1203)) - Protect access to Span implementation ([#1188](https://github.com/open-telemetry/opentelemetry-python/pull/1188)) -- `start_as_current_span` and `use_span` can now optionally auto-record any exceptions raised inside the context manager. +- `start_as_current_span` and `use_span` can now optionally auto-record any exceptions raised inside the context + manager. ([#1162](https://github.com/open-telemetry/opentelemetry-python/pull/1162)) ## Version 0.13b0 (2020-09-17) @@ -1000,7 +1008,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#959](https://github.com/open-telemetry/opentelemetry-python/pull/959)) - Update default port to 55680 ([#977](https://github.com/open-telemetry/opentelemetry-python/pull/977)) -- Add proper length zero padding to hex strings of traceId, spanId, parentId sent on the wire, for compatibility with jaeger-collector +- Add proper length zero padding to hex strings of traceId, spanId, parentId sent on the wire, for compatibility with + jaeger-collector ([#908](https://github.com/open-telemetry/opentelemetry-python/pull/908)) - Send start_timestamp and convert labels to strings ([#937](https://github.com/open-telemetry/opentelemetry-python/pull/937)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index c068f87d78..7a56120014 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -14,16 +14,16 @@ """OTLP Exporter""" -from logging import getLogger +import threading from abc import ABC, abstractmethod from collections.abc import Sequence +from logging import getLogger from os import environ from time import sleep from typing import Any, Callable, Dict, Generic, List, Optional, Tuple, Union from typing import Sequence as TypingSequence from typing import TypeVar from urllib.parse import urlparse -from opentelemetry.sdk.trace import ReadableSpan import backoff from google.rpc.error_details_pb2 import RetryInfo @@ -37,6 +37,9 @@ ssl_channel_credentials, ) +from opentelemetry.exporter.otlp.proto.grpc import ( + _OTLP_GRPC_HEADERS, +) from opentelemetry.proto.common.v1.common_pb2 import ( AnyValue, ArrayValue, @@ -51,12 +54,10 @@ OTEL_EXPORTER_OTLP_INSECURE, OTEL_EXPORTER_OTLP_TIMEOUT, ) -from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.metrics.export import MetricsData +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.util.re import parse_env_headers -from opentelemetry.exporter.otlp.proto.grpc import ( - _OTLP_GRPC_HEADERS, -) logger = getLogger(__name__) SDKDataT = TypeVar("SDKDataT") @@ -92,7 +93,6 @@ def environ_to_compression(environ_key: str) -> Optional[Compression]: def _translate_value(value: Any) -> KeyValue: - if isinstance(value, bool): any_value = AnyValue(bool_value=value) @@ -135,7 +135,6 @@ def get_resource_data( resource_class: Callable[..., TypingResourceT], name: str, ) -> List[TypingResourceT]: - resource_data = [] for ( @@ -282,6 +281,9 @@ def __init__( secure_channel(endpoint, credentials, compression=compression) ) + self._export_lock = threading.Lock() + self._shutdown = False + @abstractmethod def _translate_data( self, data: TypingSequence[SDKDataT] @@ -302,6 +304,11 @@ def _translate_attributes(self, attributes) -> TypingSequence[KeyValue]: def _export( self, data: Union[TypingSequence[ReadableSpan], MetricsData] ) -> ExportResultT: + # After the call to shutdown, subsequent calls to Export are + # not allowed and should return a Failure result. + if self._shutdown: + logger.warning("Exporter already shutdown, ignoring batch") + return self._result.FAILURE # FIXME remove this check if the export type for traces # gets updated to a class that represents the proto @@ -317,69 +324,75 @@ def _export( # exponentially. Once delay is greater than max_value, the yielded # value will remain constant. for delay in _expo(max_value=max_value): - - if delay == max_value: + if delay == max_value or self._shutdown: return self._result.FAILURE - try: - self._client.Export( - request=self._translate_data(data), - metadata=self._headers, - timeout=self._timeout, - ) + with self._export_lock: + try: + self._client.Export( + request=self._translate_data(data), + metadata=self._headers, + timeout=self._timeout, + ) - return self._result.SUCCESS + return self._result.SUCCESS - except RpcError as error: + except RpcError as error: - if error.code() in [ - StatusCode.CANCELLED, - StatusCode.DEADLINE_EXCEEDED, - StatusCode.RESOURCE_EXHAUSTED, - StatusCode.ABORTED, - StatusCode.OUT_OF_RANGE, - StatusCode.UNAVAILABLE, - StatusCode.DATA_LOSS, - ]: + if error.code() in [ + StatusCode.CANCELLED, + StatusCode.DEADLINE_EXCEEDED, + StatusCode.RESOURCE_EXHAUSTED, + StatusCode.ABORTED, + StatusCode.OUT_OF_RANGE, + StatusCode.UNAVAILABLE, + StatusCode.DATA_LOSS, + ]: - retry_info_bin = dict(error.trailing_metadata()).get( - "google.rpc.retryinfo-bin" - ) - if retry_info_bin is not None: - retry_info = RetryInfo() - retry_info.ParseFromString(retry_info_bin) - delay = ( - retry_info.retry_delay.seconds - + retry_info.retry_delay.nanos / 1.0e9 + retry_info_bin = dict(error.trailing_metadata()).get( + "google.rpc.retryinfo-bin" + ) + if retry_info_bin is not None: + retry_info = RetryInfo() + retry_info.ParseFromString(retry_info_bin) + delay = ( + retry_info.retry_delay.seconds + + retry_info.retry_delay.nanos / 1.0e9 + ) + + logger.warning( + ( + "Transient error %s encountered while exporting " + "%s, retrying in %ss." + ), + error.code(), + self._exporting, + delay, + ) + sleep(delay) + continue + else: + logger.error( + "Failed to export %s, error code: %s", + self._exporting, + error.code(), ) - logger.warning( - ( - "Transient error %s encountered while exporting " - "%s, retrying in %ss." - ), - error.code(), - self._exporting, - delay, - ) - sleep(delay) - continue - else: - logger.error( - "Failed to export %s, error code: %s", - self._exporting, - error.code(), - ) - - if error.code() == StatusCode.OK: - return self._result.SUCCESS + if error.code() == StatusCode.OK: + return self._result.SUCCESS - return self._result.FAILURE + return self._result.FAILURE return self._result.FAILURE - def shutdown(self) -> None: - pass + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + if self._shutdown: + logger.warning("Exporter already shutdown, ignoring call") + return + # wait for the last export if any + self._export_lock.acquire(timeout=timeout_millis) + self._shutdown = True + self._export_lock.release() @property @abstractmethod diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index 942ef1d4aa..8abb381cce 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -433,7 +433,7 @@ def _split_metrics_data( yield MetricsData(resource_metrics=split_resource_metrics) def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: - pass + OTLPExporterMixin.shutdown(self, timeout_millis=timeout_millis) @property def _exporting(self) -> str: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 555c903156..0203c00ec3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -290,6 +290,9 @@ def _translate_data( def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return self._export(spans) + def shutdown(self) -> None: + OTLPExporterMixin.shutdown(self) + def force_flush(self, timeout_millis: int = 30000) -> bool: return True diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index 81a874af70..c757755740 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -12,12 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +import threading +import time from logging import WARNING from types import MethodType from typing import Sequence from unittest import TestCase from unittest.mock import Mock, patch +from google.protobuf.duration_pb2 import Duration +from google.rpc.error_details_pb2 import RetryInfo from grpc import Compression from opentelemetry.exporter.otlp.proto.grpc.exporter import ( @@ -58,7 +62,6 @@ def test_environ_to_compression(self): @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") def test_export_warning(self, mock_expo): - mock_expo.configure_mock(**{"return_value": [0]}) rpc_error = RpcError() @@ -69,7 +72,6 @@ def code(self): rpc_error.code = MethodType(code, rpc_error) class OTLPMockExporter(OTLPExporterMixin): - _result = Mock() _stub = Mock( **{"return_value": Mock(**{"Export.side_effect": rpc_error})} @@ -113,3 +115,90 @@ def trailing_metadata(self): "while exporting mock, retrying in 0s." ), ) + + def test_shutdown(self): + result_mock = Mock() + + class OTLPMockExporter(OTLPExporterMixin): + _result = result_mock + _stub = Mock(**{"return_value": Mock()}) + + def _translate_data( + self, data: Sequence[SDKDataT] + ) -> ExportServiceRequestT: + pass + + @property + def _exporting(self) -> str: + return "mock" + + otlp_mock_exporter = OTLPMockExporter() + + with self.assertLogs(level=WARNING) as warning: + # pylint: disable=protected-access + self.assertEqual( + otlp_mock_exporter._export(data={}), result_mock.SUCCESS + ) + otlp_mock_exporter.shutdown() + # pylint: disable=protected-access + self.assertEqual( + otlp_mock_exporter._export(data={}), result_mock.FAILURE + ) + self.assertEqual( + warning.records[0].message, + "Exporter already shutdown, ignoring batch", + ) + + def test_shutdown_wait_last_export(self): + result_mock = Mock() + rpc_error = RpcError() + + def code(self): + return StatusCode.UNAVAILABLE + + def trailing_metadata(self): + return { + "google.rpc.retryinfo-bin": RetryInfo( + retry_delay=Duration(seconds=1) + ).SerializeToString() + } + + rpc_error.code = MethodType(code, rpc_error) + rpc_error.trailing_metadata = MethodType(trailing_metadata, rpc_error) + + class OTLPMockExporter(OTLPExporterMixin): + _result = result_mock + _stub = Mock( + **{"return_value": Mock(**{"Export.side_effect": rpc_error})} + ) + + def _translate_data( + self, data: Sequence[SDKDataT] + ) -> ExportServiceRequestT: + pass + + @property + def _exporting(self) -> str: + return "mock" + + otlp_mock_exporter = OTLPMockExporter() + + # pylint: disable=protected-access + export_thread = threading.Thread( + target=otlp_mock_exporter._export, args=({},) + ) + export_thread.start() + try: + # pylint: disable=protected-access + self.assertTrue(otlp_mock_exporter._export_lock.locked()) + # delay is 1 second while the default shutdown timeout is 30_000 milliseconds + start_time = time.time() + otlp_mock_exporter.shutdown() + now = time.time() + self.assertGreaterEqual(now, (start_time + 30 / 1000)) + # pylint: disable=protected-access + self.assertTrue(otlp_mock_exporter._shutdown) + # pylint: disable=protected-access + self.assertFalse(otlp_mock_exporter._export_lock.locked()) + finally: + export_thread.join() diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py index b38d91eb83..64ab205ad0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py @@ -12,8 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=too-many-lines +import threading +import time from concurrent.futures import ThreadPoolExecutor + +# pylint: disable=too-many-lines +from logging import WARNING from os.path import dirname from typing import List from unittest import TestCase @@ -1485,6 +1489,49 @@ def test_insecure_https_endpoint(self, mock_secure_channel): OTLPMetricExporter(endpoint="https://ab.c:123", insecure=True) mock_secure_channel.assert_called() + def test_shutdown(self): + add_MetricsServiceServicer_to_server( + MetricsServiceServicerSUCCESS(), self.server + ) + self.assertEqual( + self.exporter.export(self.metrics["sum_int"]), + MetricExportResult.SUCCESS, + ) + self.exporter.shutdown() + with self.assertLogs(level=WARNING) as warning: + self.assertEqual( + self.exporter.export(self.metrics["sum_int"]), + MetricExportResult.FAILURE, + ) + self.assertEqual( + warning.records[0].message, + "Exporter already shutdown, ignoring batch", + ) + + def test_shutdown_wait_last_export(self): + add_MetricsServiceServicer_to_server( + MetricsServiceServicerUNAVAILABLEDelay(), self.server + ) + + export_thread = threading.Thread( + target=self.exporter.export, args=(self.metrics["sum_int"],) + ) + export_thread.start() + try: + # pylint: disable=protected-access + self.assertTrue(self.exporter._export_lock.locked()) + # delay is 4 seconds while the default shutdown timeout is 30_000 milliseconds + start_time = time.time() + self.exporter.shutdown() + now = time.time() + self.assertGreaterEqual(now, (start_time + 30 / 1000)) + # pylint: disable=protected-access + self.assertTrue(self.exporter._shutdown) + # pylint: disable=protected-access + self.assertFalse(self.exporter._export_lock.locked()) + finally: + export_thread.join() + def _resource_metrics( index: int, scope_metrics: List[ScopeMetrics] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 36ae0a7c11..2498da74b8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -13,8 +13,11 @@ # limitations under the License. import os +import threading +import time from collections import OrderedDict from concurrent.futures import ThreadPoolExecutor +from logging import WARNING from unittest import TestCase from unittest.mock import Mock, PropertyMock, patch @@ -929,6 +932,47 @@ def test_dropped_values(self): .dropped_attributes_count, ) + def test_shutdown(self): + add_TraceServiceServicer_to_server( + TraceServiceServicerSUCCESS(), self.server + ) + self.assertEqual( + self.exporter.export([self.span]), SpanExportResult.SUCCESS + ) + self.exporter.shutdown() + with self.assertLogs(level=WARNING) as warning: + self.assertEqual( + self.exporter.export([self.span]), SpanExportResult.FAILURE + ) + self.assertEqual( + warning.records[0].message, + "Exporter already shutdown, ignoring batch", + ) + + def test_shutdown_wait_last_export(self): + add_TraceServiceServicer_to_server( + TraceServiceServicerUNAVAILABLEDelay(), self.server + ) + + export_thread = threading.Thread( + target=self.exporter.export, args=([self.span],) + ) + export_thread.start() + try: + # pylint: disable=protected-access + self.assertTrue(self.exporter._export_lock.locked()) + # delay is 4 seconds while the default shutdown timeout is 30_000 milliseconds + start_time = time.time() + self.exporter.shutdown() + now = time.time() + self.assertGreaterEqual(now, (start_time + 30 / 1000)) + # pylint: disable=protected-access + self.assertTrue(self.exporter._shutdown) + # pylint: disable=protected-access + self.assertFalse(self.exporter._export_lock.locked()) + finally: + export_thread.join() + def _create_span_with_status(status: SDKStatus): span = _Span( From 2d9858ff7062cc885218437ee054604f57aab6ca Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 15 Mar 2023 09:33:39 -0700 Subject: [PATCH 1403/1517] Update opencensus docker image (#3221) Fixes #3220 --- tests/opentelemetry-docker-tests/tests/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/opentelemetry-docker-tests/tests/docker-compose.yml b/tests/opentelemetry-docker-tests/tests/docker-compose.yml index 3f74827156..17c5388634 100644 --- a/tests/opentelemetry-docker-tests/tests/docker-compose.yml +++ b/tests/opentelemetry-docker-tests/tests/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: otopencensus: - image: omnition/opencensus-collector:0.1.11 + image: rafaeljesus/opencensus-collector:latest command: --logging-exporter DEBUG ports: - "8888:8888" From a70e4a0d897e4f33d5e8da20d463c9c5f2abc908 Mon Sep 17 00:00:00 2001 From: Matej Gera <38492574+matej-g@users.noreply.github.com> Date: Wed, 15 Mar 2023 18:36:26 +0100 Subject: [PATCH 1404/1517] Fix use of built-in samplers in SDK configuration (#3176) * Add built-in sampler classes Signed-off-by: Matej Gera * Add entry points for built-in samplers Signed-off-by: Matej Gera * Add CHANGELOG Signed-off-by: Matej Gera * Handle rate arg properly Signed-off-by: Matej Gera * Adjust class and entry points naming Signed-off-by: Matej Gera --------- Signed-off-by: Matej Gera Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 ++ opentelemetry-sdk/pyproject.toml | 8 +++++++ .../sdk/_configuration/__init__.py | 18 ++++++++++++--- .../src/opentelemetry/sdk/trace/sampling.py | 22 ++++++++++++++++++- 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31dd72bdcc..9d98c2c599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3100](https://github.com/open-telemetry/opentelemetry-python/pull/3100)) - Fix formatting of ConsoleMetricExporter. ([#3197](https://github.com/open-telemetry/opentelemetry-python/pull/3197)) +- Fix use of built-in samplers in SDK configuration + ([#3176](https://github.com/open-telemetry/opentelemetry-python/pull/3176)) - Implement shutdown procedure forOTLP grpc exporters ([#3138](https://github.com/open-telemetry/opentelemetry-python/pull/3138)) - Add exponential histogram diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index bffc677fd0..70d77c57b3 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -41,6 +41,14 @@ sdk = "opentelemetry.sdk.environment_variables" [project.entry-points.opentelemetry_id_generator] random = "opentelemetry.sdk.trace.id_generator:RandomIdGenerator" +[project.entry-points.opentelemetry_traces_sampler] +always_on = "opentelemetry.sdk.trace.sampling:_AlwaysOn" +always_off = "opentelemetry.sdk.trace.sampling:_AlwaysOff" +parentbased_always_on = "opentelemetry.sdk.trace.sampling:_ParentBasedAlwaysOn" +parentbased_always_off = "opentelemetry.sdk.trace.sampling:_ParentBasedAlwaysOff" +traceidratio = "opentelemetry.sdk.trace.sampling:TraceIdRatioBased" +parentbased_traceidratio = "opentelemetry.sdk.trace.sampling:ParentBasedTraceIdRatio" + [project.entry-points.opentelemetry_logger_provider] sdk_logger_provider = "opentelemetry.sdk._logs:LoggerProvider" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index c0156decaf..958a50394e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -295,8 +295,20 @@ def _import_sampler(sampler_name: str) -> Optional[Sampler]: return None try: sampler_factory = _import_sampler_factory(sampler_name) - sampler_arg = os.getenv(OTEL_TRACES_SAMPLER_ARG, "") - sampler = sampler_factory(sampler_arg) + arg = None + if sampler_name in ("traceidratio", "parentbased_traceidratio"): + try: + rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG)) + except (ValueError, TypeError): + _logger.warning( + "Could not convert TRACES_SAMPLER_ARG to float. Using default value 1.0." + ) + rate = 1.0 + arg = rate + else: + arg = os.getenv(OTEL_TRACES_SAMPLER_ARG) + + sampler = sampler_factory(arg) if not isinstance(sampler, Sampler): message = f"Sampler factory, {sampler_factory}, produced output, {sampler}, which is not a Sampler." _logger.warning(message) @@ -304,7 +316,7 @@ def _import_sampler(sampler_name: str) -> Optional[Sampler]: return sampler except Exception as exc: # pylint: disable=broad-except _logger.warning( - "Using default sampler. Failed to initialize custom sampler, %s: %s", + "Using default sampler. Failed to initialize sampler, %s: %s", sampler_name, exc, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 8af41f3d66..0236fac6b6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -394,6 +394,26 @@ def __init__(self, rate: float): super().__init__(root=root) +class _AlwaysOff(StaticSampler): + def __init__(self, _): + super().__init__(Decision.DROP) + + +class _AlwaysOn(StaticSampler): + def __init__(self, _): + super().__init__(Decision.RECORD_AND_SAMPLE) + + +class _ParentBasedAlwaysOff(ParentBased): + def __init__(self, _): + super().__init__(ALWAYS_OFF) + + +class _ParentBasedAlwaysOn(ParentBased): + def __init__(self, _): + super().__init__(ALWAYS_ON) + + _KNOWN_SAMPLERS = { "always_on": ALWAYS_ON, "always_off": ALWAYS_OFF, @@ -415,7 +435,7 @@ def _get_from_env_or_default() -> Sampler: if trace_sampler in ("traceidratio", "parentbased_traceidratio"): try: rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG)) - except ValueError: + except (ValueError, TypeError): _logger.warning("Could not convert TRACES_SAMPLER_ARG to float.") rate = 1.0 return _KNOWN_SAMPLERS[trace_sampler](rate) From 6379c1cbe6432afaaac81f524a331a7819eaecc5 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 16 Mar 2023 16:30:27 -0600 Subject: [PATCH 1405/1517] Use importlib-metadata regardless of Python version (#3217) * Use importlib-metadata regardless of Python version Fixes #3167 * Fix lint * Add FIXME comment * Constraint importlib-metadata versions --- opentelemetry-api/pyproject.toml | 4 +- .../opentelemetry/util/_importlib_metadata.py | 35 ++++----- .../tests/util/test__importlib_metadata.py | 78 ++++++++++++++++++- 3 files changed, 92 insertions(+), 25 deletions(-) diff --git a/opentelemetry-api/pyproject.toml b/opentelemetry-api/pyproject.toml index e9490ab1d2..5b2ac54af1 100644 --- a/opentelemetry-api/pyproject.toml +++ b/opentelemetry-api/pyproject.toml @@ -27,7 +27,9 @@ classifiers = [ dependencies = [ "Deprecated >= 1.2.6", "setuptools >= 16.0", - "importlib-metadata >= 5.0.0; python_version=='3.7'" + # FIXME This should be able to be removed after 3.12 is released if there is a reliable API + # in importlib.metadata. + "importlib-metadata ~= 6.0.0", ] dynamic = [ "version", diff --git a/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py index 889c4cf1ac..cbf09f3ef8 100644 --- a/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py +++ b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py @@ -12,27 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from sys import version_info +# FIXME: Use importlib.metadata when support for 3.11 is dropped if the rest of +# the supported versions at that time have the same API. +from importlib_metadata import ( # type: ignore + EntryPoint, + EntryPoints, + entry_points, + version, +) -# FIXME remove this when support for 3.7 is dropped. -if version_info.minor == 7: - # pylint: disable=import-error - from importlib_metadata import entry_points, version # type: ignore +# The importlib-metadata library has introduced breaking changes before to its +# API, this module is kept just to act as a layer between the +# importlib-metadata library and our project if in any case it is necessary to +# do so. -# FIXME remove this file when support for 3.9 is dropped. -elif version_info.minor in (8, 9): - # pylint: disable=import-error - from importlib.metadata import ( - entry_points as importlib_metadata_entry_points, - ) - from importlib.metadata import version - - def entry_points(group: str, name: str): # type: ignore - for entry_point in importlib_metadata_entry_points()[group]: - if entry_point.name == name: - yield entry_point - -else: - from importlib.metadata import entry_points, version - -__all__ = ["entry_points", "version"] +__all__ = ["entry_points", "version", "EntryPoint", "EntryPoints"] diff --git a/opentelemetry-api/tests/util/test__importlib_metadata.py b/opentelemetry-api/tests/util/test__importlib_metadata.py index 7ca58881b8..92a4e7dd62 100644 --- a/opentelemetry-api/tests/util/test__importlib_metadata.py +++ b/opentelemetry-api/tests/util/test__importlib_metadata.py @@ -15,7 +15,10 @@ from unittest import TestCase from opentelemetry.metrics import MeterProvider -from opentelemetry.util._importlib_metadata import entry_points +from opentelemetry.util._importlib_metadata import EntryPoint, EntryPoints +from opentelemetry.util._importlib_metadata import ( + entry_points as importlib_metadata_entry_points, +) class TestEntryPoints(TestCase): @@ -24,7 +27,7 @@ def test_entry_points(self): self.assertIsInstance( next( iter( - entry_points( + importlib_metadata_entry_points( group="opentelemetry_meter_provider", name="default_meter_provider", ) @@ -32,3 +35,74 @@ def test_entry_points(self): ).load()(), MeterProvider, ) + + def test_uniform_behavior(self): + """ + Test that entry_points behaves the same regardless of the Python + version. + """ + + entry_points = importlib_metadata_entry_points() + + self.assertIsInstance(entry_points, EntryPoints) + + entry_points = entry_points.select(group="opentelemetry_propagator") + self.assertIsInstance(entry_points, EntryPoints) + + entry_points = entry_points.select(name="baggage") + self.assertIsInstance(entry_points, EntryPoints) + + entry_point = next(iter(entry_points)) + self.assertIsInstance(entry_point, EntryPoint) + + self.assertEqual(entry_point.name, "baggage") + self.assertEqual(entry_point.group, "opentelemetry_propagator") + self.assertEqual( + entry_point.value, + "opentelemetry.baggage.propagation:W3CBaggagePropagator", + ) + + entry_points = importlib_metadata_entry_points( + group="opentelemetry_propagator" + ) + self.assertIsInstance(entry_points, EntryPoints) + + entry_points = entry_points.select(name="baggage") + self.assertIsInstance(entry_points, EntryPoints) + + entry_point = next(iter(entry_points)) + self.assertIsInstance(entry_point, EntryPoint) + + self.assertEqual(entry_point.name, "baggage") + self.assertEqual(entry_point.group, "opentelemetry_propagator") + self.assertEqual( + entry_point.value, + "opentelemetry.baggage.propagation:W3CBaggagePropagator", + ) + + entry_points = importlib_metadata_entry_points(name="baggage") + self.assertIsInstance(entry_points, EntryPoints) + + entry_point = next(iter(entry_points)) + self.assertIsInstance(entry_point, EntryPoint) + + self.assertEqual(entry_point.name, "baggage") + self.assertEqual(entry_point.group, "opentelemetry_propagator") + self.assertEqual( + entry_point.value, + "opentelemetry.baggage.propagation:W3CBaggagePropagator", + ) + + entry_points = importlib_metadata_entry_points(group="abc") + self.assertIsInstance(entry_points, EntryPoints) + self.assertEqual(len(entry_points), 0) + + entry_points = importlib_metadata_entry_points( + group="opentelemetry_propagator", name="abc" + ) + self.assertIsInstance(entry_points, EntryPoints) + self.assertEqual(len(entry_points), 0) + + entry_points = importlib_metadata_entry_points(group="abc", name="abc") + self.assertIsInstance(entry_points, EntryPoints) + self.assertEqual(len(entry_points), 0) From f40be518c86024d593ed3f7f80f37fa0b07a9a1e Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 17 Mar 2023 01:36:21 -0400 Subject: [PATCH 1406/1517] Add OpenCensus trace bridge/shim (#3210) --- CHANGELOG.md | 2 + .../opentelemetry/shim/opencensus/__init__.py | 13 + .../opentelemetry/shim/opencensus/_patch.py | 56 ++++ .../shim/opencensus/_shim_span.py | 163 ++++++++++++ .../shim/opencensus/_shim_tracer.py | 90 +++++++ .../tests/test_patch.py | 84 ++++++ .../tests/test_shim.py | 112 +++++++- .../tests/test_shim_with_sdk.py | 248 ++++++++++++++++++ 8 files changed, 766 insertions(+), 2 deletions(-) create mode 100644 shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py create mode 100644 shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_span.py create mode 100644 shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_tracer.py create mode 100644 shim/opentelemetry-opencensus-shim/tests/test_patch.py create mode 100644 shim/opentelemetry-opencensus-shim/tests/test_shim_with_sdk.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d98c2c599..5ff555f4d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3138](https://github.com/open-telemetry/opentelemetry-python/pull/3138)) - Add exponential histogram ([#2964](https://github.com/open-telemetry/opentelemetry-python/pull/2964)) +- Add OpenCensus trace bridge/shim + ([#3210](https://github.com/open-telemetry/opentelemetry-python/pull/3210)) ## Version 1.16.0/0.37b0 (2023-02-17) diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/__init__.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/__init__.py index 2ef69255bc..bd49fd1987 100644 --- a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/__init__.py +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/__init__.py @@ -22,3 +22,16 @@ already instrumented using OpenCensus to start using OpenTelemetry with minimal effort, without having to rewrite large portions of the codebase. """ + +from opentelemetry.shim.opencensus._patch import install_shim, uninstall_shim + +__all__ = [ + "install_shim", + "uninstall_shim", +] + +# TODO: Decide when this should be called. +# 1. defensive import in opentelemetry-api +# 2. defensive import directly in OpenCensus, although that would require a release +# 3. ask the user to do it +# install_shim() diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py new file mode 100644 index 0000000000..42b1b189ce --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py @@ -0,0 +1,56 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from logging import getLogger +from typing import Optional + +from opencensus.trace.tracer import Tracer +from opencensus.trace.tracers.noop_tracer import NoopTracer + +from opentelemetry import trace +from opentelemetry.shim.opencensus._shim_tracer import ShimTracer +from opentelemetry.shim.opencensus.version import __version__ + +_logger = getLogger(__name__) + + +def install_shim( + tracer_provider: Optional[trace.TracerProvider] = None, +) -> None: + otel_tracer = trace.get_tracer( + "opentelemetry-opencensus-shim", + __version__, + tracer_provider=tracer_provider, + ) + shim_tracer = ShimTracer(NoopTracer(), otel_tracer=otel_tracer) + + def fget_tracer(self) -> ShimTracer: + return shim_tracer + + def fset_tracer(self, value) -> None: + # ignore attempts to set the value + pass + + # Tracer's constructor sets self.tracer to either a NoopTracer or ContextTracer depending + # on sampler: + # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/tracer.py#L63. + # We monkeypatch Tracer.tracer with a property to return the shim instance instead. This + # makes all instances of Tracer (even those already created) use the ShimTracer singleton. + Tracer.tracer = property(fget_tracer, fset_tracer) + _logger.info("Installed OpenCensus shim") + + +def uninstall_shim() -> None: + if hasattr(Tracer, "tracer"): + del Tracer.tracer diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_span.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_span.py new file mode 100644 index 0000000000..fdc6f724a4 --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_span.py @@ -0,0 +1,163 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from datetime import datetime +from typing import TYPE_CHECKING + +import wrapt +from opencensus.trace.base_span import BaseSpan +from opencensus.trace.span import SpanKind +from opencensus.trace.status import Status +from opencensus.trace.time_event import MessageEvent + +from opentelemetry import context, trace + +if TYPE_CHECKING: + from opentelemetry.shim.opencensus._shim_tracer import ShimTracer + +_logger = logging.getLogger(__name__) + +# Copied from Java +# https://github.com/open-telemetry/opentelemetry-java/blob/0d3a04669e51b33ea47b29399a7af00012d25ccb/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/SpanConverter.java#L24-L27 +_MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE = "message.event.type" +_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED = ( + "message.event.size.uncompressed" +) +_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED = "message.event.size.compressed" + +_MESSAGE_EVENT_TYPE_STR_MAPPING = { + 0: "TYPE_UNSPECIFIED", + 1: "SENT", + 2: "RECEIVED", +} + + +def _opencensus_time_to_nanos(timestamp: str) -> int: + """Converts an OpenCensus formatted time string (ISO 8601 with Z) to time.time_ns style + unix timestamp + """ + # format taken from + # https://github.com/census-instrumentation/opencensus-python/blob/c38c71b9285e71de94d0185ff3c5bf65ee163345/opencensus/common/utils/__init__.py#L76 + # + # datetime.fromisoformat() does not work with the added "Z" until python 3.11 + seconds_float = datetime.strptime( + timestamp, "%Y-%m-%dT%H:%M:%S.%fZ" + ).timestamp() + return round(seconds_float * 1e9) + + +# pylint: disable=abstract-method +class ShimSpan(wrapt.ObjectProxy): + def __init__( + self, + wrapped: BaseSpan, + *, + otel_span: trace.Span, + shim_tracer: "ShimTracer", + ) -> None: + super().__init__(wrapped) + self._self_otel_span = otel_span + self._self_shim_tracer = shim_tracer + self._self_token: object = None + + # Set a few values for BlankSpan members (they appear to be part of the "public" API + # even though they are not documented in BaseSpan). Some instrumentations may use these + # and not expect an AttributeError to be raised. Set values from OTel where possible + # and let ObjectProxy defer to the wrapped BlankSpan otherwise. + sc = self._self_otel_span.get_span_context() + self.same_process_as_parent_span = not sc.is_remote + self.span_id = sc.span_id + + def span(self, name="child_span"): + return self._self_shim_tracer.start_span(name=name) + + def add_attribute(self, attribute_key, attribute_value): + self._self_otel_span.set_attribute(attribute_key, attribute_value) + + def add_annotation(self, description, **attrs): + self._self_otel_span.add_event(description, attrs) + + def add_message_event(self, message_event: MessageEvent): + attrs = { + _MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE: _MESSAGE_EVENT_TYPE_STR_MAPPING[ + message_event.type + ], + } + if message_event.uncompressed_size_bytes is not None: + attrs[ + _MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED + ] = message_event.uncompressed_size_bytes + if message_event.compressed_size_bytes is not None: + attrs[ + _MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED + ] = message_event.compressed_size_bytes + + timestamp = _opencensus_time_to_nanos(message_event.timestamp) + self._self_otel_span.add_event( + str(message_event.id), + attrs, + timestamp=timestamp, + ) + + # pylint: disable=no-self-use + def add_link(self, link): + """span links do not work with the shim because the OpenCensus Tracer does not accept + links in start_span(). Same issue applies to SpanKind. Also see: + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/opencensus.md#known-incompatibilities + """ + _logger.warning( + "OpenTelemetry does not support links added after a span is created." + ) + + @property + def span_kind(self): + """Setting span_kind does not work with the shim because the OpenCensus Tracer does not + accept the param in start_span() and there's no way to set OTel span kind after + start_span(). + """ + return SpanKind.UNSPECIFIED + + @span_kind.setter + def span_kind(self, value): + _logger.warning( + "OpenTelemetry does not support setting span kind after a span is created." + ) + + def set_status(self, status: Status): + self._self_otel_span.set_status( + trace.StatusCode.OK if status.is_ok else trace.StatusCode.ERROR, + status.description, + ) + + def finish(self): + """Note this method does not pop the span from current context. Use Tracer.end_span() + or a `with span: ...` statement (contextmanager) to do that. + """ + self._self_otel_span.end() + + def __enter__(self): + self._self_otel_span.__enter__() + return self + + # pylint: disable=arguments-differ + def __exit__(self, exception_type, exception_value, traceback): + self._self_otel_span.__exit__( + exception_type, exception_value, traceback + ) + # OpenCensus Span.__exit__() calls Tracer.end_span() + # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/span.py#L390 + # but that would cause the OTel span to be ended twice. Instead just detach it from + # context directly. + context.detach(self._self_token) diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_tracer.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_tracer.py new file mode 100644 index 0000000000..0ce2d01120 --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_tracer.py @@ -0,0 +1,90 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +import wrapt +from opencensus.trace.blank_span import BlankSpan +from opencensus.trace.tracers.base import Tracer as BaseTracer + +from opentelemetry import context, trace +from opentelemetry.shim.opencensus._shim_span import ShimSpan + +_logger = logging.getLogger(__name__) + +_SHIM_SPAN_KEY = context.create_key("opencensus-shim-span-key") + + +def set_shim_span_in_context( + span: ShimSpan, ctx: context.Context +) -> context.Context: + return context.set_value(_SHIM_SPAN_KEY, span, ctx) + + +def get_shim_span_in_context() -> ShimSpan: + return context.get_value(_SHIM_SPAN_KEY) + + +# pylint: disable=abstract-method +class ShimTracer(wrapt.ObjectProxy): + def __init__( + self, wrapped: BaseTracer, *, otel_tracer: trace.Tracer + ) -> None: + super().__init__(wrapped) + self._self_otel_tracer = otel_tracer + + # For now, finish() is not implemented by the shim. It would require keeping a list of all + # spans created so they can all be finished. + # def finish(self): + # """End spans and send to reporter.""" + + def span(self, name="span"): + return self.start_span(name=name) + + def start_span(self, name="span"): + span = self._self_otel_tracer.start_span(name) + shim_span = ShimSpan( + BlankSpan(name=name, context_tracer=self), + otel_span=span, + shim_tracer=self, + ) + + ctx = trace.set_span_in_context(span) + ctx = set_shim_span_in_context(shim_span, ctx) + + # OpenCensus's ContextTracer calls execution_context.set_current_span(span) which is + # equivalent to the below. This can cause context to leak but is equivalent. + # pylint: disable=protected-access + shim_span._self_token = context.attach(ctx) + return shim_span + + def end_span(self): + """Finishes the current span in the context and pops restores the context from before + the span was started. + """ + span = self.current_span() + if not span: + _logger.warning("No active span, cannot do end_span.") + return + + span.finish() + # pylint: disable=protected-access + context.detach(span._self_token) + + # pylint: disable=no-self-use + def current_span(self): + return get_shim_span_in_context() + + def add_attribute_to_current_span(self, attribute_key, attribute_value): + self.current_span().add_attribute(attribute_key, attribute_value) diff --git a/shim/opentelemetry-opencensus-shim/tests/test_patch.py b/shim/opentelemetry-opencensus-shim/tests/test_patch.py new file mode 100644 index 0000000000..697ddfc352 --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/tests/test_patch.py @@ -0,0 +1,84 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opencensus.trace.tracer import Tracer +from opencensus.trace.tracers.noop_tracer import NoopTracer + +from opentelemetry.shim.opencensus import install_shim, uninstall_shim +from opentelemetry.shim.opencensus._shim_tracer import ShimTracer + + +class TestPatch(unittest.TestCase): + def setUp(self): + uninstall_shim() + + def tearDown(self): + uninstall_shim() + + def test_install_shim(self): + # Initially the shim is not installed. The Tracer class has no tracer property, it is + # instance level only. + self.assertFalse(hasattr(Tracer, "tracer")) + + install_shim() + + # The actual Tracer class should now be patched with a tracer property + self.assertTrue(hasattr(Tracer, "tracer")) + self.assertIsInstance(Tracer.tracer, property) + + def test_install_shim_affects_existing_tracers(self): + # Initially the shim is not installed. A OC Tracer instance should have a NoopTracer + oc_tracer = Tracer() + self.assertIsInstance(oc_tracer.tracer, NoopTracer) + self.assertNotIsInstance(oc_tracer.tracer, ShimTracer) + + install_shim() + + # The property should cause existing instances to get the singleton ShimTracer + self.assertIsInstance(oc_tracer.tracer, ShimTracer) + + def test_install_shim_affects_new_tracers(self): + install_shim() + + # The property should cause existing instances to get the singleton ShimTracer + oc_tracer = Tracer() + self.assertIsInstance(oc_tracer.tracer, ShimTracer) + + def test_uninstall_shim_resets_tracer(self): + install_shim() + uninstall_shim() + + # The actual Tracer class should not be patched + self.assertFalse(hasattr(Tracer, "tracer")) + + def test_uninstall_shim_resets_existing_tracers(self): + oc_tracer = Tracer() + orig = oc_tracer.tracer + install_shim() + uninstall_shim() + + # Accessing the tracer member should no longer use the property, and instead should get + # its original NoopTracer + self.assertIs(oc_tracer.tracer, orig) + + def test_uninstall_shim_resets_new_tracers(self): + install_shim() + uninstall_shim() + + # Accessing the tracer member should get the NoopTracer + oc_tracer = Tracer() + self.assertIsInstance(oc_tracer.tracer, NoopTracer) + self.assertNotIsInstance(oc_tracer.tracer, ShimTracer) diff --git a/shim/opentelemetry-opencensus-shim/tests/test_shim.py b/shim/opentelemetry-opencensus-shim/tests/test_shim.py index 922026ac45..df6abcfe1e 100644 --- a/shim/opentelemetry-opencensus-shim/tests/test_shim.py +++ b/shim/opentelemetry-opencensus-shim/tests/test_shim.py @@ -12,9 +12,117 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import unittest +from unittest.mock import patch + +from opencensus.trace.blank_span import BlankSpan as OcBlankSpan +from opencensus.trace.link import Link as OcLink +from opencensus.trace.span import SpanKind +from opencensus.trace.tracer import Tracer as OcTracer +from opencensus.trace.tracers.noop_tracer import NoopTracer as OcNoopTracer + +from opentelemetry.shim.opencensus import install_shim, uninstall_shim +from opentelemetry.shim.opencensus._shim_span import ShimSpan +from opentelemetry.shim.opencensus._shim_tracer import ShimTracer class TestShim(unittest.TestCase): - def test_shim(self): - pass + def setUp(self): + uninstall_shim() + install_shim() + + def tearDown(self): + uninstall_shim() + + def assert_hasattr(self, obj, key): + self.assertTrue(hasattr(obj, key)) + + def test_shim_tracer_wraps_noop_tracer(self): + oc_tracer = OcTracer() + + self.assertIsInstance(oc_tracer.tracer, ShimTracer) + + # wrapt.ObjectProxy does the magic here. The ShimTracer should look like the real OC + # NoopTracer. + self.assertIsInstance(oc_tracer.tracer, OcNoopTracer) + self.assert_hasattr(oc_tracer.tracer, "finish") + self.assert_hasattr(oc_tracer.tracer, "span") + self.assert_hasattr(oc_tracer.tracer, "start_span") + self.assert_hasattr(oc_tracer.tracer, "end_span") + self.assert_hasattr(oc_tracer.tracer, "current_span") + self.assert_hasattr(oc_tracer.tracer, "add_attribute_to_current_span") + self.assert_hasattr(oc_tracer.tracer, "list_collected_spans") + + def test_shim_tracer_starts_shim_spans(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("foo") as span: + self.assertIsInstance(span, ShimSpan) + + def test_shim_span_wraps_blank_span(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("foo") as span: + # wrapt.ObjectProxy does the magic here. The ShimSpan should look like the real OC + # BlankSpan. + self.assertIsInstance(span, OcBlankSpan) + + # members + self.assert_hasattr(span, "name") + self.assert_hasattr(span, "parent_span") + self.assert_hasattr(span, "start_time") + self.assert_hasattr(span, "end_time") + self.assert_hasattr(span, "span_id") + self.assert_hasattr(span, "attributes") + self.assert_hasattr(span, "stack_trace") + self.assert_hasattr(span, "annotations") + self.assert_hasattr(span, "message_events") + self.assert_hasattr(span, "links") + self.assert_hasattr(span, "status") + self.assert_hasattr(span, "same_process_as_parent_span") + self.assert_hasattr(span, "_child_spans") + self.assert_hasattr(span, "context_tracer") + self.assert_hasattr(span, "span_kind") + + # methods + self.assert_hasattr(span, "on_create") + self.assert_hasattr(span, "children") + self.assert_hasattr(span, "span") + self.assert_hasattr(span, "add_attribute") + self.assert_hasattr(span, "add_annotation") + self.assert_hasattr(span, "add_message_event") + self.assert_hasattr(span, "add_link") + self.assert_hasattr(span, "set_status") + self.assert_hasattr(span, "start") + self.assert_hasattr(span, "finish") + self.assert_hasattr(span, "__iter__") + self.assert_hasattr(span, "__enter__") + self.assert_hasattr(span, "__exit__") + + def test_add_link_logs_a_warning(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("foo") as span: + with self.assertLogs(level=logging.WARNING): + span.add_link(OcLink("1", "1")) + + def test_set_span_kind_logs_a_warning(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("foo") as span: + with self.assertLogs(level=logging.WARNING): + span.span_kind = SpanKind.CLIENT + + # pylint: disable=no-self-use,no-member,protected-access + def test_shim_span_contextmanager_calls_does_not_call_end(self): + # This was a bug in first implementation where the underlying OTel span.end() was + # called after span.__exit__ which caused double-ending the span. + oc_tracer = OcTracer() + oc_span = oc_tracer.start_span("foo") + + with patch.object( + oc_span, + "_self_otel_span", + wraps=oc_span._self_otel_span, + ) as spy_otel_span: + with oc_span: + pass + + spy_otel_span.end.assert_not_called() diff --git a/shim/opentelemetry-opencensus-shim/tests/test_shim_with_sdk.py b/shim/opentelemetry-opencensus-shim/tests/test_shim_with_sdk.py new file mode 100644 index 0000000000..9bccc82ecb --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/tests/test_shim_with_sdk.py @@ -0,0 +1,248 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import unittest +from datetime import datetime + +from opencensus.trace import time_event +from opencensus.trace.status import Status as OcStatus +from opencensus.trace.tracer import Tracer as OcTracer + +from opentelemetry import trace +from opentelemetry.sdk.trace import ReadableSpan, TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from opentelemetry.sdk.trace.sampling import ALWAYS_ON +from opentelemetry.shim.opencensus import install_shim, uninstall_shim + +_TIMESTAMP = datetime.fromisoformat("2023-01-01T00:00:00.000000") + + +class TestShimWithSdk(unittest.TestCase): + def setUp(self): + uninstall_shim() + self.tracer_provider = TracerProvider( + sampler=ALWAYS_ON, shutdown_on_exit=False + ) + self.mem_exporter = InMemorySpanExporter() + self.tracer_provider.add_span_processor( + SimpleSpanProcessor(self.mem_exporter) + ) + install_shim(self.tracer_provider) + + def tearDown(self): + uninstall_shim() + + def test_start_span_interacts_with_context(self): + oc_tracer = OcTracer() + span = oc_tracer.start_span("foo") + + # Should have created a real OTel span in implicit context under the hood. OpenCensus + # does not require another step to set the span in context. + otel_span = trace.get_current_span() + self.assertNotEqual(span.span_id, 0) + self.assertEqual(span.span_id, otel_span.get_span_context().span_id) + + # This should end the span and remove it from context + oc_tracer.end_span() + self.assertIs(trace.get_current_span(), trace.INVALID_SPAN) + + def test_context_manager_interacts_with_context(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("foo") as span: + # Should have created a real OTel span in implicit context under the hood + otel_span = trace.get_current_span() + + self.assertNotEqual(span.span_id, 0) + self.assertEqual( + span.span_id, otel_span.get_span_context().span_id + ) + + # The span should now be popped from context + self.assertIs(trace.get_current_span(), trace.INVALID_SPAN) + + def test_exports_a_span(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("span1"): + pass + + self.assertEqual(len(self.mem_exporter.get_finished_spans()), 1) + + def test_span_attributes(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("span1") as span: + span.add_attribute("key1", "value1") + span.add_attribute("key2", "value2") + + exported_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] + self.assertDictEqual( + dict(exported_span.attributes), + {"key1": "value1", "key2": "value2"}, + ) + + def test_span_annotations(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("span1") as span: + span.add_annotation("description", key1="value1", key2="value2") + + exported_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] + self.assertEqual(len(exported_span.events), 1) + event = exported_span.events[0] + self.assertEqual(event.name, "description") + self.assertDictEqual( + dict(event.attributes), {"key1": "value1", "key2": "value2"} + ) + + def test_span_message_event(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("span1") as span: + span.add_message_event( + time_event.MessageEvent( + _TIMESTAMP, "id_sent", time_event.Type.SENT, "20", "10" + ) + ) + span.add_message_event( + time_event.MessageEvent( + _TIMESTAMP, + "id_received", + time_event.Type.RECEIVED, + "20", + "10", + ) + ) + span.add_message_event( + time_event.MessageEvent( + _TIMESTAMP, + "id_unspecified", + None, + "20", + "10", + ) + ) + + exported_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] + self.assertEqual(len(exported_span.events), 3) + event1, event2, event3 = exported_span.events + + self.assertEqual(event1.name, "id_sent") + self.assertDictEqual( + dict(event1.attributes), + { + "message.event.size.compressed": "10", + "message.event.size.uncompressed": "20", + "message.event.type": "SENT", + }, + ) + self.assertEqual(event2.name, "id_received") + self.assertDictEqual( + dict(event2.attributes), + { + "message.event.size.compressed": "10", + "message.event.size.uncompressed": "20", + "message.event.type": "RECEIVED", + }, + ) + self.assertEqual(event3.name, "id_unspecified") + self.assertDictEqual( + dict(event3.attributes), + { + "message.event.size.compressed": "10", + "message.event.size.uncompressed": "20", + "message.event.type": "TYPE_UNSPECIFIED", + }, + ) + + def test_span_status(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("span_ok") as span: + # OTel will log about the message being set on a not OK span + with self.assertLogs(level=logging.WARNING) as rec: + span.set_status(OcStatus(0, "message")) + self.assertIn( + "description should only be set when status_code is set to StatusCode.ERROR", + rec.output[0], + ) + + with oc_tracer.start_span("span_exception") as span: + span.set_status( + OcStatus.from_exception(Exception("exception message")) + ) + + self.assertEqual(len(self.mem_exporter.get_finished_spans()), 2) + ok_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] + exc_span: ReadableSpan = self.mem_exporter.get_finished_spans()[1] + + self.assertTrue(ok_span.status.is_ok) + # should be none even though we provided it because OTel drops the description when + # status is not ERROR + self.assertIsNone(ok_span.status.description) + + self.assertFalse(exc_span.status.is_ok) + self.assertEqual(exc_span.status.description, "exception message") + + def assert_related(self, *, child: ReadableSpan, parent: ReadableSpan): + self.assertEqual( + child.parent.span_id, parent.get_span_context().span_id + ) + + def test_otel_sandwich(self): + oc_tracer = OcTracer() + otel_tracer = self.tracer_provider.get_tracer(__name__) + with oc_tracer.start_span("opencensus_outer"): + with otel_tracer.start_as_current_span("otel_middle"): + with oc_tracer.start_span("opencensus_inner"): + pass + + self.assertEqual(len(self.mem_exporter.get_finished_spans()), 3) + opencensus_inner: ReadableSpan = ( + self.mem_exporter.get_finished_spans()[0] + ) + otel_middle: ReadableSpan = self.mem_exporter.get_finished_spans()[1] + opencensus_outer: ReadableSpan = ( + self.mem_exporter.get_finished_spans()[2] + ) + + self.assertEqual(opencensus_outer.name, "opencensus_outer") + self.assertEqual(otel_middle.name, "otel_middle") + self.assertEqual(opencensus_inner.name, "opencensus_inner") + + self.assertIsNone(opencensus_outer.parent) + self.assert_related(parent=opencensus_outer, child=otel_middle) + self.assert_related(parent=otel_middle, child=opencensus_inner) + + def test_opencensus_sandwich(self): + oc_tracer = OcTracer() + otel_tracer = self.tracer_provider.get_tracer(__name__) + with otel_tracer.start_as_current_span("otel_outer"): + with oc_tracer.start_span("opencensus_middle"): + with otel_tracer.start_as_current_span("otel_inner"): + pass + + self.assertEqual(len(self.mem_exporter.get_finished_spans()), 3) + otel_inner: ReadableSpan = self.mem_exporter.get_finished_spans()[0] + opencensus_middle: ReadableSpan = ( + self.mem_exporter.get_finished_spans()[1] + ) + otel_outer: ReadableSpan = self.mem_exporter.get_finished_spans()[2] + + self.assertEqual(otel_outer.name, "otel_outer") + self.assertEqual(opencensus_middle.name, "opencensus_middle") + self.assertEqual(otel_inner.name, "otel_inner") + + self.assertIsNone(otel_outer.parent) + self.assert_related(parent=otel_outer, child=opencensus_middle) + self.assert_related(parent=opencensus_middle, child=otel_inner) From 42f87368a0714ed064645895e5a2c41915633dc5 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 17 Mar 2023 10:57:56 -0600 Subject: [PATCH 1407/1517] Implement LowMemory temporality presets (#3223) * Implement LowMemory temporality Fixes #3075 * Fix lint --- CHANGELOG.md | 2 + .../proto/grpc/metric_exporter/__init__.py | 47 ++++-- .../tests/test_otlp_metrics_exporter.py | 107 ++++++++++++ .../proto/http/metric_exporter/__init__.py | 29 +++- .../metrics/test_otlp_metrics_exporter.py | 152 ++++++++++++++++-- .../sdk/environment_variables.py | 8 +- 6 files changed, 310 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff555f4d0..93b1e42b0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Implement LowMemory temporality + ([#3223](https://github.com/open-telemetry/opentelemetry-python/pull/3223)) - PeriodicExportingMetricReader will continue if collection times out ([#3100](https://github.com/open-telemetry/opentelemetry-python/pull/3100)) - Fix formatting of ConsoleMetricExporter. diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index 8abb381cce..d5ba83c939 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import dataclasses +from dataclasses import replace from logging import getLogger from os import environ from typing import Dict, Iterable, List, Optional, Sequence @@ -120,15 +120,17 @@ def __init__( ) instrument_class_temporality = {} - if ( + + otel_exporter_otlp_metrics_temporality_preference = ( environ.get( OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, "CUMULATIVE", ) .upper() .strip() - == "DELTA" - ): + ) + + if otel_exporter_otlp_metrics_temporality_preference == "DELTA": instrument_class_temporality = { Counter: AggregationTemporality.DELTA, UpDownCounter: AggregationTemporality.CUMULATIVE, @@ -137,7 +139,27 @@ def __init__( ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } + + elif otel_exporter_otlp_metrics_temporality_preference == "LOWMEMORY": + instrument_class_temporality = { + Counter: AggregationTemporality.DELTA, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.DELTA, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + else: + if otel_exporter_otlp_metrics_temporality_preference != ( + "CUMULATIVE" + ): + _logger.warning( + "Unrecognized OTEL_EXPORTER_METRICS_TEMPORALITY_PREFERENCE" + " value found: " + f"{otel_exporter_otlp_metrics_temporality_preference}, " + "using CUMULATIVE" + ) instrument_class_temporality = { Counter: AggregationTemporality.CUMULATIVE, UpDownCounter: AggregationTemporality.CUMULATIVE, @@ -146,6 +168,7 @@ def __init__( ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } + instrument_class_temporality.update(preferred_temporality or {}) MetricExporter.__init__( @@ -359,7 +382,7 @@ def _split_metrics_data( for resource_metrics in metrics_data.resource_metrics: split_scope_metrics: List[ScopeMetrics] = [] split_resource_metrics.append( - dataclasses.replace( + replace( resource_metrics, scope_metrics=split_scope_metrics, ) @@ -367,7 +390,7 @@ def _split_metrics_data( for scope_metrics in resource_metrics.scope_metrics: split_metrics: List[Metric] = [] split_scope_metrics.append( - dataclasses.replace( + replace( scope_metrics, metrics=split_metrics, ) @@ -375,9 +398,9 @@ def _split_metrics_data( for metric in scope_metrics.metrics: split_data_points: List[DataPointT] = [] split_metrics.append( - dataclasses.replace( + replace( metric, - data=dataclasses.replace( + data=replace( metric.data, data_points=split_data_points, ), @@ -396,22 +419,22 @@ def _split_metrics_data( batch_size = 0 split_data_points = [] split_metrics = [ - dataclasses.replace( + replace( metric, - data=dataclasses.replace( + data=replace( metric.data, data_points=split_data_points, ), ) ] split_scope_metrics = [ - dataclasses.replace( + replace( scope_metrics, metrics=split_metrics, ) ] split_resource_metrics = [ - dataclasses.replace( + replace( resource_metrics, scope_metrics=split_scope_metrics, ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py index 64ab205ad0..9cd805e969 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py @@ -18,6 +18,7 @@ # pylint: disable=too-many-lines from logging import WARNING +from os import environ from os.path import dirname from typing import List from unittest import TestCase @@ -1532,6 +1533,112 @@ def test_shutdown_wait_last_export(self): finally: export_thread.join() + def test_aggregation_temporality(self): + # pylint: disable=protected-access + + otlp_metric_exporter = OTLPMetricExporter() + + for ( + temporality + ) in otlp_metric_exporter._preferred_temporality.values(): + self.assertEqual(temporality, AggregationTemporality.CUMULATIVE) + + with patch.dict( + environ, + {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "CUMULATIVE"}, + ): + + otlp_metric_exporter = OTLPMetricExporter() + + for ( + temporality + ) in otlp_metric_exporter._preferred_temporality.values(): + self.assertEqual( + temporality, AggregationTemporality.CUMULATIVE + ) + + with patch.dict( + environ, {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "ABC"} + ): + + with self.assertLogs(level=WARNING): + otlp_metric_exporter = OTLPMetricExporter() + + for ( + temporality + ) in otlp_metric_exporter._preferred_temporality.values(): + self.assertEqual( + temporality, AggregationTemporality.CUMULATIVE + ) + + with patch.dict( + environ, + {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"}, + ): + + otlp_metric_exporter = OTLPMetricExporter() + + self.assertEqual( + otlp_metric_exporter._preferred_temporality[Counter], + AggregationTemporality.DELTA, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[UpDownCounter], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[Histogram], + AggregationTemporality.DELTA, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ObservableCounter], + AggregationTemporality.DELTA, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ + ObservableUpDownCounter + ], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ObservableGauge], + AggregationTemporality.CUMULATIVE, + ) + + with patch.dict( + environ, + {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "LOWMEMORY"}, + ): + + otlp_metric_exporter = OTLPMetricExporter() + + self.assertEqual( + otlp_metric_exporter._preferred_temporality[Counter], + AggregationTemporality.DELTA, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[UpDownCounter], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[Histogram], + AggregationTemporality.DELTA, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ObservableCounter], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ + ObservableUpDownCounter + ], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ObservableGauge], + AggregationTemporality.CUMULATIVE, + ) + def _resource_metrics( index: int, scope_metrics: List[ScopeMetrics] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index c04f5bbbce..ffd5102a2d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -138,15 +138,17 @@ def __init__( ) instrument_class_temporality = {} - if ( + + otel_exporter_otlp_metrics_temporality_preference = ( environ.get( OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, "CUMULATIVE", ) .upper() .strip() - == "DELTA" - ): + ) + + if otel_exporter_otlp_metrics_temporality_preference == "DELTA": instrument_class_temporality = { Counter: AggregationTemporality.DELTA, UpDownCounter: AggregationTemporality.CUMULATIVE, @@ -155,7 +157,27 @@ def __init__( ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } + + elif otel_exporter_otlp_metrics_temporality_preference == "LOWMEMORY": + instrument_class_temporality = { + Counter: AggregationTemporality.DELTA, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.DELTA, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + else: + if otel_exporter_otlp_metrics_temporality_preference != ( + "CUMULATIVE" + ): + _logger.warning( + "Unrecognized OTEL_EXPORTER_METRICS_TEMPORALITY_PREFERENCE" + " value found: " + f"{otel_exporter_otlp_metrics_temporality_preference}, " + "using CUMULATIVE" + ) instrument_class_temporality = { Counter: AggregationTemporality.CUMULATIVE, UpDownCounter: AggregationTemporality.CUMULATIVE, @@ -164,6 +186,7 @@ def __init__( ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } + instrument_class_temporality.update(preferred_temporality or {}) MetricExporter.__init__( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index 45bc0c6faa..9f57a23ae2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -12,11 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest +from logging import WARNING +from os import environ +from unittest import TestCase from unittest.mock import patch -import requests -import responses +from requests import Session +from requests.models import Response +from responses import POST, activate, add from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( @@ -36,10 +39,20 @@ OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, OTEL_EXPORTER_OTLP_TIMEOUT, ) +from opentelemetry.sdk.metrics import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, MetricExportResult, MetricsData, ResourceMetrics, @@ -58,7 +71,7 @@ # pylint: disable=protected-access -class TestOTLPMetricExporter(unittest.TestCase): +class TestOTLPMetricExporter(TestCase): def setUp(self): self.metrics = { @@ -97,7 +110,7 @@ def test_constructor_default(self): self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) self.assertIs(exporter._compression, DEFAULT_COMPRESSION) self.assertEqual(exporter._headers, {}) - self.assertIsInstance(exporter._session, requests.Session) + self.assertIsInstance(exporter._session, Session) @patch.dict( "os.environ", @@ -129,7 +142,7 @@ def test_exporter_metrics_env_take_priority(self): "metricenv3": "==val3==", }, ) - self.assertIsInstance(exporter._session, requests.Session) + self.assertIsInstance(exporter._session, Session) @patch.dict( "os.environ", @@ -149,7 +162,7 @@ def test_exporter_constructor_take_priority(self): headers={"testHeader1": "value1", "testHeader2": "value2"}, timeout=20, compression=Compression.NoCompression, - session=requests.Session(), + session=Session(), ) self.assertEqual(exporter._endpoint, "example.com/1234") @@ -160,7 +173,7 @@ def test_exporter_constructor_take_priority(self): exporter._headers, {"testHeader1": "value1", "testHeader2": "value2"}, ) - self.assertIsInstance(exporter._session, requests.Session) + self.assertIsInstance(exporter._session, Session) @patch.dict( "os.environ", @@ -228,9 +241,9 @@ def test_headers_parse_from_env(self): ), ) - @patch.object(requests.Session, "post") + @patch.object(Session, "post") def test_success(self, mock_post): - resp = requests.models.Response() + resp = Response() resp.status_code = 200 mock_post.return_value = resp @@ -241,9 +254,9 @@ def test_success(self, mock_post): MetricExportResult.SUCCESS, ) - @patch.object(requests.Session, "post") + @patch.object(Session, "post") def test_failure(self, mock_post): - resp = requests.models.Response() + resp = Response() resp.status_code = 401 mock_post.return_value = resp @@ -254,10 +267,10 @@ def test_failure(self, mock_post): MetricExportResult.FAILURE, ) - @patch.object(requests.Session, "post") + @patch.object(Session, "post") def test_serialization(self, mock_post): - resp = requests.models.Response() + resp = Response() resp.status_code = 200 mock_post.return_value = resp @@ -276,7 +289,7 @@ def test_serialization(self, mock_post): timeout=exporter._timeout, ) - @responses.activate + @activate @patch("opentelemetry.exporter.otlp.proto.http.metric_exporter.backoff") @patch("opentelemetry.exporter.otlp.proto.http.metric_exporter.sleep") def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff): @@ -289,8 +302,8 @@ def generate_delays(*args, **kwargs): mock_backoff.expo.configure_mock(**{"side_effect": generate_delays}) # return a retryable error - responses.add( - responses.POST, + add( + POST, "http://metrics.example.com/export", json={"error": "something exploded"}, status=500, @@ -303,3 +316,108 @@ def generate_delays(*args, **kwargs): exporter.export(metrics_data) mock_sleep.assert_called_once_with(1) + + def test_aggregation_temporality(self): + + otlp_metric_exporter = OTLPMetricExporter() + + for ( + temporality + ) in otlp_metric_exporter._preferred_temporality.values(): + self.assertEqual(temporality, AggregationTemporality.CUMULATIVE) + + with patch.dict( + environ, + {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "CUMULATIVE"}, + ): + + otlp_metric_exporter = OTLPMetricExporter() + + for ( + temporality + ) in otlp_metric_exporter._preferred_temporality.values(): + self.assertEqual( + temporality, AggregationTemporality.CUMULATIVE + ) + + with patch.dict( + environ, {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "ABC"} + ): + + with self.assertLogs(level=WARNING): + otlp_metric_exporter = OTLPMetricExporter() + + for ( + temporality + ) in otlp_metric_exporter._preferred_temporality.values(): + self.assertEqual( + temporality, AggregationTemporality.CUMULATIVE + ) + + with patch.dict( + environ, + {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"}, + ): + + otlp_metric_exporter = OTLPMetricExporter() + + self.assertEqual( + otlp_metric_exporter._preferred_temporality[Counter], + AggregationTemporality.DELTA, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[UpDownCounter], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[Histogram], + AggregationTemporality.DELTA, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ObservableCounter], + AggregationTemporality.DELTA, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ + ObservableUpDownCounter + ], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ObservableGauge], + AggregationTemporality.CUMULATIVE, + ) + + with patch.dict( + environ, + {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "LOWMEMORY"}, + ): + + otlp_metric_exporter = OTLPMetricExporter() + + self.assertEqual( + otlp_metric_exporter._preferred_temporality[Counter], + AggregationTemporality.DELTA, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[UpDownCounter], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[Histogram], + AggregationTemporality.DELTA, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ObservableCounter], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ + ObservableUpDownCounter + ], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ObservableGauge], + AggregationTemporality.CUMULATIVE, + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 376fb187dc..3881994326 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -576,9 +576,11 @@ variable allows users to set the default aggregation temporality policy to use on the basis of instrument kind. The valid (case-insensitive) values are: -``CUMULATIVE``: Choose ``CUMULATIVE`` aggregation temporality for all instrument kinds. -``DELTA``: Choose ``DELTA`` aggregation temporality for ``Counter``, ``Asynchronous Counter`` and ``Histogram``. -Choose ``CUMULATIVE`` aggregation temporality for ``UpDownCounter`` and ``Asynchronous UpDownCounter``. +``CUMULATIVE``: Use ``CUMULATIVE`` aggregation temporality for all instrument kinds. +``DELTA``: Use ``DELTA`` aggregation temporality for ``Counter``, ``Asynchronous Counter`` and ``Histogram``. +Use ``CUMULATIVE`` aggregation temporality for ``UpDownCounter`` and ``Asynchronous UpDownCounter``. +``LOWMEMORY``: Use ``DELTA`` aggregation temporality for ``Counter`` and ``Histogram``. +Use ``CUMULATIVE`` aggregation temporality for ``UpDownCounter``, ``AsynchronousCounter`` and ``Asynchronous UpDownCounter``. """ OTEL_EXPORTER_JAEGER_GRPC_INSECURE = "OTEL_EXPORTER_JAEGER_GRPC_INSECURE" From d77ca16badd5197d66b9b4dfac1221a5fb09c028 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 21 Mar 2023 02:14:42 +0530 Subject: [PATCH 1408/1517] Exclude opencensus shim from update & build scripts (#3227) --- scripts/build.sh | 2 +- scripts/eachdist.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index c5b305cef8..46b6f1289f 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-proto/ opentelemetry-semantic-conventions/ exporter/*/ shim/*/ propagator/*/ tests/opentelemetry-test-utils/; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-proto/ opentelemetry-semantic-conventions/ exporter/*/ shim/opentelemetry-opentracing-shim/ propagator/*/ tests/opentelemetry-test-utils/; do ( echo "building $d" cd "$d" diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 2f6faa6cf3..6b8db97a30 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -576,6 +576,7 @@ def update_version_files(targets, version, packages): def update_dependencies(targets, version, packages): print("updating dependencies") + targets = filter_packages(targets, packages) for pkg in packages: update_files( targets, From b6a1b22fa65f41bdefb01d64b76e5e793d039f6d Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Wed, 22 Mar 2023 09:29:42 -0700 Subject: [PATCH 1409/1517] Update version to 1.18.0.dev/0.39b0.dev (#3228) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/pyproject.toml | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-grpc/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-http/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/pyproject.toml | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/pyproject.toml | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opencensus-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opencensus/version.py | 2 +- shim/opentelemetry-opentracing-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/pyproject.toml | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 34 files changed, 43 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ee26e5247..710dd9bc91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: ca2c23fd3b91c48f2b6515658993480f634cf1b2 + CONTRIB_REPO_SHA: 7fb0340445f5e0164bc59a3af1bf5836393013f4 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b1e42b0e..1ed8a038b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## Version 1.17.0/0.38b0 (2023-03-22) + - Implement LowMemory temporality ([#3223](https://github.com/open-telemetry/opentelemetry-python/pull/3223)) - PeriodicExportingMetricReader will continue if collection times out diff --git a/eachdist.ini b/eachdist.ini index 164faad3d1..51f52313d2 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.17.0.dev +version=1.18.0.dev packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.38b0.dev +version=0.39b0.dev packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index f90e55e55a..6b6b22d30f 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index f90e55e55a..6b6b22d30f 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger/pyproject.toml b/exporter/opentelemetry-exporter-jaeger/pyproject.toml index 5c64b989be..95c93736de 100644 --- a/exporter/opentelemetry-exporter-jaeger/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-jaeger-proto-grpc == 1.17.0.dev", - "opentelemetry-exporter-jaeger-thrift == 1.17.0.dev", + "opentelemetry-exporter-jaeger-proto-grpc == 1.18.0.dev", + "opentelemetry-exporter-jaeger-thrift == 1.18.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index f90e55e55a..6b6b22d30f 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/exporter/opentelemetry-exporter-opencensus/pyproject.toml b/exporter/opentelemetry-exporter-opencensus/pyproject.toml index 8e359f5d4d..48e0a59ea4 100644 --- a/exporter/opentelemetry-exporter-opencensus/pyproject.toml +++ b/exporter/opentelemetry-exporter-opencensus/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ dependencies = [ "grpcio >= 1.0.0, < 2.0.0", "opencensus-proto >= 0.1.0, < 1.0.0", - "opentelemetry-api >= 1.17.0.dev", + "opentelemetry-api >= 1.18.0.dev", "opentelemetry-sdk >= 1.15", "protobuf ~= 3.13", "setuptools >= 16.0", diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 8778b43b17..eb62a67e28 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.38b0.dev" +__version__ = "0.39b0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index aa369caf00..0850727b02 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -30,8 +30,8 @@ dependencies = [ "googleapis-common-protos ~= 1.52", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.17.0.dev", - "opentelemetry-sdk ~= 1.17.0.dev", + "opentelemetry-proto == 1.18.0.dev", + "opentelemetry-sdk ~= 1.18.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 904e2b48ef..80d12781d3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index 9675d1d982..55dcd3f551 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -29,8 +29,8 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.17.0.dev", - "opentelemetry-sdk ~= 1.17.0.dev", + "opentelemetry-proto == 1.18.0.dev", + "opentelemetry-sdk ~= 1.18.0.dev", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 904e2b48ef..80d12781d3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index 6de1ff977b..8af01b433c 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-otlp-proto-grpc == 1.17.0.dev", - "opentelemetry-exporter-otlp-proto-http == 1.17.0.dev", + "opentelemetry-exporter-otlp-proto-grpc == 1.18.0.dev", + "opentelemetry-exporter-otlp-proto-http == 1.18.0.dev", ] [project.entry-points.opentelemetry_logs_exporter] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 904e2b48ef..80d12781d3 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 8778b43b17..eb62a67e28 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.38b0.dev" +__version__ = "0.39b0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 904e2b48ef..80d12781d3 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml index c6fe12e0ca..8764b35d43 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.3", - "opentelemetry-exporter-zipkin-json == 1.17.0.dev", + "opentelemetry-exporter-zipkin-json == 1.18.0.dev", "opentelemetry-sdk ~= 1.11", "protobuf ~= 3.12", "requests ~= 2.7", diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 904e2b48ef..80d12781d3 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin/pyproject.toml b/exporter/opentelemetry-exporter-zipkin/pyproject.toml index 50ec818a09..03dedc6dea 100644 --- a/exporter/opentelemetry-exporter-zipkin/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-zipkin-json == 1.17.0.dev", - "opentelemetry-exporter-zipkin-proto-http == 1.17.0.dev", + "opentelemetry-exporter-zipkin-json == 1.18.0.dev", + "opentelemetry-exporter-zipkin-proto-http == 1.18.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 904e2b48ef..80d12781d3 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 904e2b48ef..80d12781d3 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 904e2b48ef..80d12781d3 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index 70d77c57b3..41538dd706 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-api == 1.17.0.dev", - "opentelemetry-semantic-conventions == 0.38b0.dev", + "opentelemetry-api == 1.18.0.dev", + "opentelemetry-semantic-conventions == 0.39b0.dev", "setuptools >= 16.0", "typing-extensions >= 3.7.4", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 904e2b48ef..80d12781d3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 8778b43b17..eb62a67e28 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.38b0.dev" +__version__ = "0.39b0.dev" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 904e2b48ef..80d12781d3 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 904e2b48ef..80d12781d3 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0.dev" +__version__ = "1.18.0.dev" diff --git a/shim/opentelemetry-opencensus-shim/pyproject.toml b/shim/opentelemetry-opencensus-shim/pyproject.toml index b4d2f76f06..fb4af68814 100644 --- a/shim/opentelemetry-opencensus-shim/pyproject.toml +++ b/shim/opentelemetry-opencensus-shim/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ ] [project.optional-dependencies] -test = ["opentelemetry-test-utils == 0.38b0.dev", "opencensus == 0.11.1"] +test = ["opentelemetry-test-utils == 0.39b0.dev", "opencensus == 0.11.1"] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/shim/opentelemetry-opencensus-shim" diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py index 8778b43b17..eb62a67e28 100644 --- a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.38b0.dev" +__version__ = "0.39b0.dev" diff --git a/shim/opentelemetry-opentracing-shim/pyproject.toml b/shim/opentelemetry-opentracing-shim/pyproject.toml index 742eabd2f4..6345b77d35 100644 --- a/shim/opentelemetry-opentracing-shim/pyproject.toml +++ b/shim/opentelemetry-opentracing-shim/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.38b0.dev", + "opentelemetry-test-utils == 0.39b0.dev", "opentracing ~= 2.2.0", ] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 8778b43b17..eb62a67e28 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.38b0.dev" +__version__ = "0.39b0.dev" diff --git a/tests/opentelemetry-test-utils/pyproject.toml b/tests/opentelemetry-test-utils/pyproject.toml index 7db7c0145e..fb1c0915f4 100644 --- a/tests/opentelemetry-test-utils/pyproject.toml +++ b/tests/opentelemetry-test-utils/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ ] dependencies = [ "asgiref ~= 3.0", - "opentelemetry-api == 1.17.0.dev", - "opentelemetry-sdk == 1.17.0.dev", + "opentelemetry-api == 1.18.0.dev", + "opentelemetry-sdk == 1.18.0.dev", ] [project.optional-dependencies] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 9d323a2a65..15f6e2a9b4 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.38b0.dev" +__version__ = "0.39b0.dev" From b30b935624495536098d057ba3d3eaed3b4efadb Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 28 Mar 2023 11:52:08 -0600 Subject: [PATCH 1410/1517] Enable Git tracing in tox.ini (#3236) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9c43776a2c..843a0680ed 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ envlist = py3{7,8,9,10,11}-opentelemetry-api pypy3-opentelemetry-api - ; Test against both protobuf 3.x and 4.x + ; Test against both protobuf 3.x and 4.x py3{7,8,9,10,11}-proto{3,4}-opentelemetry-protobuf pypy3-proto{3,4}-opentelemetry-protobuf From 58e2ef04a398c9bd8aa9fcdd19f60823d390dda4 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 28 Mar 2023 11:13:52 -0700 Subject: [PATCH 1411/1517] Fix _SUPPRESS_INSTRUMENTATION for log batch processor (#3230) --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 5 ++++- .../opentelemetry/sdk/_logs/_internal/export/__init__.py | 9 +++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 710dd9bc91..2b77fb8128 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 7fb0340445f5e0164bc59a3af1bf5836393013f4 + CONTRIB_REPO_SHA: e4d8f10ecd7bcd29c119af0e3ea7c30d4a383f4b # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ed8a038b0..258a765289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Fix suppress instrumentation for log batch processor + ([#3223](https://github.com/open-telemetry/opentelemetry-python/pull/3223)) + ## Version 1.17.0/0.38b0 (2023-03-22) - Implement LowMemory temporality ([#3223](https://github.com/open-telemetry/opentelemetry-python/pull/3223)) -- PeriodicExportingMetricReader will continue if collection times out +- PeriodicExportingMetricReader will continue if collection times out ([#3100](https://github.com/open-telemetry/opentelemetry-python/pull/3100)) - Fix formatting of ConsoleMetricExporter. ([#3197](https://github.com/open-telemetry/opentelemetry-python/pull/3197)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py index 3f19b79e10..6f2f1c2c26 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -23,7 +23,12 @@ from time import time_ns from typing import IO, Callable, Deque, List, Optional, Sequence -from opentelemetry.context import attach, detach, set_value +from opentelemetry.context import ( + _SUPPRESS_INSTRUMENTATION_KEY, + attach, + detach, + set_value, +) from opentelemetry.sdk._logs import LogData, LogRecord, LogRecordProcessor from opentelemetry.util._once import Once @@ -105,7 +110,7 @@ def emit(self, log_data: LogData): if self._shutdown: _logger.warning("Processor is already shutdown, ignoring call") return - token = attach(set_value("suppress_instrumentation", True)) + token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: self._exporter.export((log_data,)) except Exception: # pylint: disable=broad-except From 7e67d52c6b6713bdf7420e63f0e7d577d91d0e29 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Wed, 29 Mar 2023 11:49:26 -0400 Subject: [PATCH 1412/1517] Add OpenCensus tracing shim example (#3211) --- docs/examples/opencensus-shim/.gitignore | 1 + docs/examples/opencensus-shim/README.rst | 35 ++++-- docs/examples/opencensus-shim/app.py | 102 ++++++++++++++++++ .../examples/opencensus-shim/requirements.txt | 3 + ...entracing_shim.rst => opencensus_shim.rst} | 0 5 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 docs/examples/opencensus-shim/.gitignore create mode 100644 docs/examples/opencensus-shim/app.py rename docs/shim/opencensus_shim/{opentracing_shim.rst => opencensus_shim.rst} (100%) diff --git a/docs/examples/opencensus-shim/.gitignore b/docs/examples/opencensus-shim/.gitignore new file mode 100644 index 0000000000..300f4e1546 --- /dev/null +++ b/docs/examples/opencensus-shim/.gitignore @@ -0,0 +1 @@ +example.db diff --git a/docs/examples/opencensus-shim/README.rst b/docs/examples/opencensus-shim/README.rst index 64de1e36ec..4fdccba615 100644 --- a/docs/examples/opencensus-shim/README.rst +++ b/docs/examples/opencensus-shim/README.rst @@ -44,23 +44,44 @@ Alternatively, you can install the Python dependencies separately: opentelemetry-api \ opentelemetry-sdk \ opentelemetry-exporter-jaeger \ - opentelemetry-opencensus-shim + opentelemetry-opencensus-shim \ + opentelemetry-instrumentation-sqlite3 \ + opencensus \ + opencensus-ext-flask Run the Application ------------------- -.. TODO implement the example +Start the application in a terminal. + +.. code-block:: sh + + flask --app app run -h 0.0.0.0 + +Point your browser to the address printed out (probably http://127.0.0.1:5000). Alternatively, just use curl to trigger a request: + +.. code-block:: sh + + curl http://127.0.0.1:5000 Jaeger UI ********* -Open the Jaeger UI in your browser at -``_ and view traces for the -"OpenCensus Shim Example" service. +Open the Jaeger UI in your browser at ``_ and view traces for the +"opencensus-shim-example-flask" service. Click on a span named "span" in the scatter plot. You +will see a span tree with the following structure: + +* ``span`` + * ``query movies from db`` + * ``SELECT`` + * ``build response html`` -Note that tags and logs (OpenCensus) and attributes and events (OpenTelemetry) -from both tracing systems appear in the exported trace. +The root span comes from OpenCensus Flask instrumentation. The children ``query movies from +db`` and ``build response html`` come from the manual instrumentation using OpenTelemetry's +:meth:`opentelemetry.trace.Tracer.start_as_current_span`. Finally, the ``SELECT`` span is +created by OpenTelemetry's SQLite3 instrumentation. Everything is exported to Jaeger using the +OpenTelemetry exporter. Useful links ------------ diff --git a/docs/examples/opencensus-shim/app.py b/docs/examples/opencensus-shim/app.py new file mode 100644 index 0000000000..5c8b7f744b --- /dev/null +++ b/docs/examples/opencensus-shim/app.py @@ -0,0 +1,102 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sqlite3 + +from flask import Flask +from opencensus.ext.flask.flask_middleware import FlaskMiddleware + +from opentelemetry import trace +from opentelemetry.exporter.jaeger.thrift import JaegerExporter +from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.shim.opencensus import install_shim + +DB = "example.db" + +# Set up OpenTelemetry +tracer_provider = TracerProvider( + resource=Resource( + { + "service.name": "opencensus-shim-example-flask", + } + ) +) +trace.set_tracer_provider(tracer_provider) + +# Configure OTel to export traces to Jaeger +tracer_provider.add_span_processor( + BatchSpanProcessor( + JaegerExporter( + agent_host_name="localhost", + agent_port=6831, + ) + ) +) +tracer = tracer_provider.get_tracer(__name__) + +# Install the shim to start bridging spans from OpenCensus to OpenTelemetry +install_shim() + +# Instrument sqlite3 library +SQLite3Instrumentor().instrument() + +# Setup Flask with OpenCensus instrumentation +app = Flask(__name__) +FlaskMiddleware(app) + + +# Setup the application database +def setup_db(): + with sqlite3.connect(DB) as con: + cur = con.cursor() + cur.execute( + """ + CREATE TABLE IF NOT EXISTS movie( + title, + year, + PRIMARY KEY(title, year) + ) + """ + ) + cur.execute( + """ + INSERT OR IGNORE INTO movie(title, year) VALUES + ('Mission Telemetry', 2000), + ('Observing the World', 2010), + ('The Tracer', 1999), + ('The Instrument', 2020) + """ + ) + + +setup_db() + + +@app.route("/") +def hello_world(): + lines = [] + with tracer.start_as_current_span("query movies from db"), sqlite3.connect( + DB + ) as con: + cur = con.cursor() + for title, year in cur.execute("SELECT title, year from movie"): + lines.append(f"
  • {title} is from the year {year}
  • ") + + with tracer.start_as_current_span("build response html"): + html = f"
      {''.join(lines)}
    " + + return html diff --git a/docs/examples/opencensus-shim/requirements.txt b/docs/examples/opencensus-shim/requirements.txt index 072c825558..4150eec3c4 100644 --- a/docs/examples/opencensus-shim/requirements.txt +++ b/docs/examples/opencensus-shim/requirements.txt @@ -2,3 +2,6 @@ opentelemetry-api opentelemetry-sdk opentelemetry-exporter-jaeger opentelemetry-opencensus-shim +opentelemetry-instrumentation-sqlite3 +opencensus +opencensus-ext-flask diff --git a/docs/shim/opencensus_shim/opentracing_shim.rst b/docs/shim/opencensus_shim/opencensus_shim.rst similarity index 100% rename from docs/shim/opencensus_shim/opentracing_shim.rst rename to docs/shim/opencensus_shim/opencensus_shim.rst From c11d551a63d21ace111c20961bf7ad35a17931cf Mon Sep 17 00:00:00 2001 From: Sreejit Kar <40720148+sreejitkar@users.noreply.github.com> Date: Wed, 29 Mar 2023 23:47:41 +0530 Subject: [PATCH 1413/1517] Fix headers types mismatch for OTLP Exporters (#3226) --- CHANGELOG.md | 2 ++ .../exporter/otlp/proto/grpc/_log_exporter/__init__.py | 7 +++++-- .../opentelemetry/exporter/otlp/proto/grpc/exporter.py | 2 +- .../exporter/otlp/proto/grpc/metric_exporter/__init__.py | 7 +++++-- .../exporter/otlp/proto/grpc/trace_exporter/__init__.py | 8 ++++++-- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 258a765289..377303abf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Fix headers types mismatch for OTLP Exporters + ([#3226](https://github.com/open-telemetry/opentelemetry-python/pull/3226)) - Fix suppress instrumentation for log batch processor ([#3223](https://github.com/open-telemetry/opentelemetry-python/pull/3223)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index f7cbadfec8..887f9d8b2c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -12,7 +12,8 @@ # limitations under the License. from os import environ -from typing import Optional, Sequence +from typing import Dict, Optional, Tuple, Union, Sequence +from typing import Sequence as TypingSequence from grpc import ChannelCredentials, Compression from opentelemetry.exporter.otlp.proto.grpc.exporter import ( OTLPExporterMixin, @@ -60,7 +61,9 @@ def __init__( endpoint: Optional[str] = None, insecure: Optional[bool] = None, credentials: Optional[ChannelCredentials] = None, - headers: Optional[Sequence] = None, + headers: Optional[ + Union[TypingSequence[Tuple[str, str]], Dict[str, str], str] + ] = None, timeout: Optional[int] = None, compression: Optional[Compression] = None, ): diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 7a56120014..496fe365f8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -256,7 +256,7 @@ def __init__( if self._headers is None: self._headers = tuple(_OTLP_GRPC_HEADERS) else: - self._headers = self._headers + tuple(_OTLP_GRPC_HEADERS) + self._headers = tuple(self._headers) + tuple(_OTLP_GRPC_HEADERS) self._timeout = timeout or int( environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 10) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index d5ba83c939..99325b64f9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -14,7 +14,8 @@ from dataclasses import replace from logging import getLogger from os import environ -from typing import Dict, Iterable, List, Optional, Sequence +from typing import Dict, Iterable, List, Optional, Tuple, Union +from typing import Sequence as TypingSequence from grpc import ChannelCredentials, Compression from opentelemetry.sdk.metrics._internal.aggregation import Aggregation from opentelemetry.exporter.otlp.proto.grpc.exporter import ( @@ -87,7 +88,9 @@ def __init__( endpoint: Optional[str] = None, insecure: Optional[bool] = None, credentials: Optional[ChannelCredentials] = None, - headers: Optional[Sequence] = None, + headers: Optional[ + Union[TypingSequence[Tuple[str, str]], Dict[str, str], str] + ] = None, timeout: Optional[int] = None, compression: Optional[Compression] = None, preferred_temporality: Dict[type, AggregationTemporality] = None, diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 0203c00ec3..cfabe3ffce 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -15,7 +15,9 @@ import logging from os import environ -from typing import Optional, Sequence +from typing import Dict, Optional, Sequence, Tuple, Union +from typing import Sequence as TypingSequence + from grpc import ChannelCredentials, Compression @@ -80,7 +82,9 @@ def __init__( endpoint: Optional[str] = None, insecure: Optional[bool] = None, credentials: Optional[ChannelCredentials] = None, - headers: Optional[Sequence] = None, + headers: Optional[ + Union[TypingSequence[Tuple[str, str]], Dict[str, str], str] + ] = None, timeout: Optional[int] = None, compression: Optional[Compression] = None, ): From 6b38349eba166bfe042aafe72ba625c1eb91db1d Mon Sep 17 00:00:00 2001 From: Sebastian Kreft Date: Wed, 29 Mar 2023 15:37:41 -0300 Subject: [PATCH 1414/1517] fix: export ExponentialBucketHistogramAggregation from opentelemetry.sdk.metrics.view (#3240) * fix: export ExponentialBucketHistogramAggregation from opentelemetry.sdk.metrics.view Without this PR if one wants to use exponential histrograms, which were added in realease 1.17.0 (https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md#version-1170038b0-2023-03-22), one needs to import it from the private package opentelemetry.sdk.metrics._internal.aggregation. This PR fixes #3239. --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/metrics/view/__init__.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 377303abf8..1474ce01ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +- Fix exporting of ExponentialBucketHistogramAggregation from opentelemetry.sdk.metrics.view + ([#3240](https://github.com/open-telemetry/opentelemetry-python/pull/3240)) - Fix headers types mismatch for OTLP Exporters ([#3226](https://github.com/open-telemetry/opentelemetry-python/pull/3226)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view/__init__.py index f6e4dcb3aa..c07adf6cac 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view/__init__.py @@ -17,6 +17,7 @@ DefaultAggregation, DropAggregation, ExplicitBucketHistogramAggregation, + ExponentialBucketHistogramAggregation, LastValueAggregation, SumAggregation, ) @@ -27,6 +28,7 @@ "DefaultAggregation", "DropAggregation", "ExplicitBucketHistogramAggregation", + "ExponentialBucketHistogramAggregation", "LastValueAggregation", "SumAggregation", "View", From 2e0d8eece473c0b191cdbfb856edc8ce3da0a028 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 30 Mar 2023 21:35:35 -0600 Subject: [PATCH 1415/1517] Incorporate resource detectors to auto instrumentation (#3181) * Make entry_points behave the same across Python versions Fixes #3167 * Refactor function * Fix mypy * Incorporate resource detectors to auto instrumentation This is an experimental feature. Fixes #3172 * Rename environment variable * Revert previous commits * Refactor to use importlib-metadata library * Use get_aggregate_resources * Fix mypy * Add CHANGELOG entry * Fix typo in environment variable * Add OTELResourceDetector by default * Move to unreleased section * Fix entry point name in the documentation * Update opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py Co-authored-by: Srikanth Chekuri --------- Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 4 +- opentelemetry-sdk/pyproject.toml | 4 + .../sdk/environment_variables.py | 11 + .../opentelemetry/sdk/resources/__init__.py | 41 +- .../tests/resources/test_resources.py | 482 +++++++++++------- 5 files changed, 341 insertions(+), 201 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1474ce01ba..f0bb37d48e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased + +- Add experimental feature to detect resource detectors in auto instrumentation + ([#3181](https://github.com/open-telemetry/opentelemetry-python/pull/3181)) - Fix exporting of ExponentialBucketHistogramAggregation from opentelemetry.sdk.metrics.view ([#3240](https://github.com/open-telemetry/opentelemetry-python/pull/3240)) - - Fix headers types mismatch for OTLP Exporters ([#3226](https://github.com/open-telemetry/opentelemetry-python/pull/3226)) - Fix suppress instrumentation for log batch processor diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index 41538dd706..5be574f3a2 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -67,6 +67,10 @@ sdk_tracer_provider = "opentelemetry.sdk.trace:TracerProvider" [project.entry-points.opentelemetry_traces_exporter] console = "opentelemetry.sdk.trace.export:ConsoleSpanExporter" +[project.entry-points.opentelemetry_resource_detector] +otel = "opentelemetry.sdk.resources:OTELResourceDetector" +process = "opentelemetry.sdk.resources:ProcessResourceDetector" + [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 3881994326..8e36ae58c7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -635,3 +635,14 @@ The :envvar:`OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE` is the client certificate/chain trust for clients private key to use in mTLS communication in PEM format. """ + +OTEL_EXPERIMENTAL_RESOURCE_DETECTORS = "OTEL_EXPERIMENTAL_RESOURCE_DETECTORS" +""" +.. envvar:: OTEL_EXPERIMENTAL_RESOURCE_DETECTORS + +The :envvar:`OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` is a comma-separated string +of names of resource detectors. These names must be the same as the names of +entry points for the `opentelemetry_resource_detector` entry point. This is an +experimental feature and the name of this variable and its behavior can change +in a non-backwards compatible way. +""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index c46b87f89c..dd5bea43d4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -58,19 +58,20 @@ import abc import concurrent.futures import logging -import os import sys import typing from json import dumps +from os import environ from urllib import parse from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk.environment_variables import ( + OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, ) from opentelemetry.semconv.resource import ResourceAttributes -from opentelemetry.util._importlib_metadata import version +from opentelemetry.util._importlib_metadata import entry_points, version from opentelemetry.util.types import AttributeValue LabelValue = AttributeValue @@ -163,11 +164,38 @@ def create( Returns: The newly-created Resource. """ + if not attributes: attributes = {} - resource = _DEFAULT_RESOURCE.merge( - OTELResourceDetector().detect() + + resource_detectors = [] + + resource = _DEFAULT_RESOURCE + + otel_experimental_resource_detectors = environ.get( + OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "otel" + ).split(",") + + if "otel" not in otel_experimental_resource_detectors: + otel_experimental_resource_detectors.append("otel") + + for resource_detector in otel_experimental_resource_detectors: + + resource_detectors.append( + next( + iter( + entry_points( + group="opentelemetry_resource_detector", + name=resource_detector.strip(), + ) + ) + ).load()() + ) + + resource = get_aggregated_resources( + resource_detectors, _DEFAULT_RESOURCE ).merge(Resource(attributes, schema_url)) + if not resource.attributes.get(SERVICE_NAME, None): default_service_name = "unknown_service" process_executable_name = resource.attributes.get( @@ -273,7 +301,8 @@ def detect(self) -> "Resource": class OTELResourceDetector(ResourceDetector): # pylint: disable=no-self-use def detect(self) -> "Resource": - env_resources_items = os.environ.get(OTEL_RESOURCE_ATTRIBUTES) + + env_resources_items = environ.get(OTEL_RESOURCE_ATTRIBUTES) env_resource_map = {} if env_resources_items: @@ -290,7 +319,7 @@ def detect(self) -> "Resource": value_url_decoded = parse.unquote(value.strip()) env_resource_map[key.strip()] = value_url_decoded - service_name = os.environ.get(OTEL_SERVICE_NAME) + service_name = environ.get(OTEL_SERVICE_NAME) if service_name: env_resource_map[SERVICE_NAME] = service_name return Resource(env_resource_map) diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index c9649f685a..bf372edc0b 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -14,22 +14,44 @@ # pylint: disable=protected-access -import os import unittest import uuid from logging import ERROR -from unittest import mock +from os import environ +from unittest.mock import Mock, patch from urllib import parse -from opentelemetry.sdk import resources +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, +) +from opentelemetry.sdk.resources import ( + _DEFAULT_RESOURCE, + _EMPTY_RESOURCE, + _OPENTELEMETRY_SDK_VERSION, + OTEL_RESOURCE_ATTRIBUTES, + OTEL_SERVICE_NAME, + PROCESS_EXECUTABLE_NAME, + PROCESS_RUNTIME_DESCRIPTION, + PROCESS_RUNTIME_NAME, + PROCESS_RUNTIME_VERSION, + SERVICE_NAME, + TELEMETRY_SDK_LANGUAGE, + TELEMETRY_SDK_NAME, + TELEMETRY_SDK_VERSION, + OTELResourceDetector, + ProcessResourceDetector, + Resource, + ResourceDetector, + get_aggregated_resources, +) class TestResources(unittest.TestCase): def setUp(self) -> None: - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" + environ[OTEL_RESOURCE_ATTRIBUTES] = "" def tearDown(self) -> None: - os.environ.pop(resources.OTEL_RESOURCE_ATTRIBUTES) + environ.pop(OTEL_RESOURCE_ATTRIBUTES) def test_create(self): attributes = { @@ -44,109 +66,101 @@ def test_create(self): "version": 1, "has_bugs": True, "cost": 112.12, - resources.TELEMETRY_SDK_NAME: "opentelemetry", - resources.TELEMETRY_SDK_LANGUAGE: "python", - resources.TELEMETRY_SDK_VERSION: resources._OPENTELEMETRY_SDK_VERSION, - resources.SERVICE_NAME: "unknown_service", + TELEMETRY_SDK_NAME: "opentelemetry", + TELEMETRY_SDK_LANGUAGE: "python", + TELEMETRY_SDK_VERSION: _OPENTELEMETRY_SDK_VERSION, + SERVICE_NAME: "unknown_service", } - resource = resources.Resource.create(attributes) - self.assertIsInstance(resource, resources.Resource) + resource = Resource.create(attributes) + self.assertIsInstance(resource, Resource) self.assertEqual(resource.attributes, expected_attributes) self.assertEqual(resource.schema_url, "") schema_url = "https://opentelemetry.io/schemas/1.3.0" - resource = resources.Resource.create(attributes, schema_url) - self.assertIsInstance(resource, resources.Resource) + resource = Resource.create(attributes, schema_url) + self.assertIsInstance(resource, Resource) self.assertEqual(resource.attributes, expected_attributes) self.assertEqual(resource.schema_url, schema_url) - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "key=value" - resource = resources.Resource.create(attributes) - self.assertIsInstance(resource, resources.Resource) + environ[OTEL_RESOURCE_ATTRIBUTES] = "key=value" + resource = Resource.create(attributes) + self.assertIsInstance(resource, Resource) expected_with_envar = expected_attributes.copy() expected_with_envar["key"] = "value" self.assertEqual(resource.attributes, expected_with_envar) - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" + environ[OTEL_RESOURCE_ATTRIBUTES] = "" - resource = resources.Resource.get_empty() - self.assertEqual(resource, resources._EMPTY_RESOURCE) + resource = Resource.get_empty() + self.assertEqual(resource, _EMPTY_RESOURCE) - resource = resources.Resource.create(None) + resource = Resource.create(None) self.assertEqual( resource, - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ), ) self.assertEqual(resource.schema_url, "") - resource = resources.Resource.create(None, None) + resource = Resource.create(None, None) self.assertEqual( resource, - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ), ) self.assertEqual(resource.schema_url, "") - resource = resources.Resource.create({}) + resource = Resource.create({}) self.assertEqual( resource, - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ), ) self.assertEqual(resource.schema_url, "") - resource = resources.Resource.create({}, None) + resource = Resource.create({}, None) self.assertEqual( resource, - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ), ) self.assertEqual(resource.schema_url, "") def test_resource_merge(self): - left = resources.Resource({"service": "ui"}) - right = resources.Resource({"host": "service-host"}) + left = Resource({"service": "ui"}) + right = Resource({"host": "service-host"}) self.assertEqual( left.merge(right), - resources.Resource({"service": "ui", "host": "service-host"}), + Resource({"service": "ui", "host": "service-host"}), ) schema_urls = ( "https://opentelemetry.io/schemas/1.2.0", "https://opentelemetry.io/schemas/1.3.0", ) - left = resources.Resource.create({}, None) - right = resources.Resource.create({}, None) + left = Resource.create({}, None) + right = Resource.create({}, None) self.assertEqual(left.merge(right).schema_url, "") - left = resources.Resource.create({}, None) - right = resources.Resource.create({}, schema_urls[0]) + left = Resource.create({}, None) + right = Resource.create({}, schema_urls[0]) self.assertEqual(left.merge(right).schema_url, schema_urls[0]) - left = resources.Resource.create({}, schema_urls[0]) - right = resources.Resource.create({}, None) + left = Resource.create({}, schema_urls[0]) + right = Resource.create({}, None) self.assertEqual(left.merge(right).schema_url, schema_urls[0]) - left = resources.Resource.create({}, schema_urls[0]) - right = resources.Resource.create({}, schema_urls[0]) + left = Resource.create({}, schema_urls[0]) + right = Resource.create({}, schema_urls[0]) self.assertEqual(left.merge(right).schema_url, schema_urls[0]) - left = resources.Resource.create({}, schema_urls[0]) - right = resources.Resource.create({}, schema_urls[1]) + left = Resource.create({}, schema_urls[0]) + right = Resource.create({}, schema_urls[1]) with self.assertLogs(level=ERROR) as log_entry: self.assertEqual(left.merge(right), left) self.assertIn(schema_urls[0], log_entry.output[0]) @@ -159,13 +173,11 @@ def test_resource_merge_empty_string(self): the exception of the empty string. """ - left = resources.Resource({"service": "ui", "host": ""}) - right = resources.Resource( - {"host": "service-host", "service": "not-ui"} - ) + left = Resource({"service": "ui", "host": ""}) + right = Resource({"host": "service-host", "service": "not-ui"}) self.assertEqual( left.merge(right), - resources.Resource({"service": "not-ui", "host": "service-host"}), + Resource({"service": "not-ui", "host": "service-host"}), ) def test_immutability(self): @@ -177,16 +189,16 @@ def test_immutability(self): } default_attributes = { - resources.TELEMETRY_SDK_NAME: "opentelemetry", - resources.TELEMETRY_SDK_LANGUAGE: "python", - resources.TELEMETRY_SDK_VERSION: resources._OPENTELEMETRY_SDK_VERSION, - resources.SERVICE_NAME: "unknown_service", + TELEMETRY_SDK_NAME: "opentelemetry", + TELEMETRY_SDK_LANGUAGE: "python", + TELEMETRY_SDK_VERSION: _OPENTELEMETRY_SDK_VERSION, + SERVICE_NAME: "unknown_service", } attributes_copy = attributes.copy() attributes_copy.update(default_attributes) - resource = resources.Resource.create(attributes) + resource = Resource.create(attributes) self.assertEqual(resource.attributes, attributes_copy) with self.assertRaises(TypeError): @@ -202,18 +214,16 @@ def test_immutability(self): self.assertEqual(resource.schema_url, "") def test_service_name_using_process_name(self): - resource = resources.Resource.create( - {resources.PROCESS_EXECUTABLE_NAME: "test"} - ) + resource = Resource.create({PROCESS_EXECUTABLE_NAME: "test"}) self.assertEqual( - resource.attributes.get(resources.SERVICE_NAME), + resource.attributes.get(SERVICE_NAME), "unknown_service:test", ) def test_invalid_resource_attribute_values(self): - resource = resources.Resource( + resource = Resource( { - resources.SERVICE_NAME: "test", + SERVICE_NAME: "test", "non-primitive-data-type": {}, "invalid-byte-type-attribute": b"\xd8\xe1\xb7\xeb\xa8\xe5 \xd2\xb7\xe1", "": "empty-key-value", @@ -224,43 +234,39 @@ def test_invalid_resource_attribute_values(self): self.assertEqual( resource.attributes, { - resources.SERVICE_NAME: "test", + SERVICE_NAME: "test", }, ) self.assertEqual(len(resource.attributes), 1) def test_aggregated_resources_no_detectors(self): - aggregated_resources = resources.get_aggregated_resources([]) + aggregated_resources = get_aggregated_resources([]) self.assertEqual( aggregated_resources, - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ), ) def test_aggregated_resources_with_default_destroying_static_resource( self, ): - static_resource = resources.Resource({"static_key": "static_value"}) + static_resource = Resource({"static_key": "static_value"}) self.assertEqual( - resources.get_aggregated_resources( - [], initial_resource=static_resource - ), + get_aggregated_resources([], initial_resource=static_resource), static_resource, ) - resource_detector = mock.Mock(spec=resources.ResourceDetector) - resource_detector.detect.return_value = resources.Resource( + resource_detector = Mock(spec=ResourceDetector) + resource_detector.detect.return_value = Resource( {"static_key": "try_to_overwrite_existing_value", "key": "value"} ) self.assertEqual( - resources.get_aggregated_resources( + get_aggregated_resources( [resource_detector], initial_resource=static_resource ), - resources.Resource( + Resource( { "static_key": "try_to_overwrite_existing_value", "key": "value", @@ -269,16 +275,14 @@ def test_aggregated_resources_with_default_destroying_static_resource( ) def test_aggregated_resources_multiple_detectors(self): - resource_detector1 = mock.Mock(spec=resources.ResourceDetector) - resource_detector1.detect.return_value = resources.Resource( - {"key1": "value1"} - ) - resource_detector2 = mock.Mock(spec=resources.ResourceDetector) - resource_detector2.detect.return_value = resources.Resource( + resource_detector1 = Mock(spec=ResourceDetector) + resource_detector1.detect.return_value = Resource({"key1": "value1"}) + resource_detector2 = Mock(spec=ResourceDetector) + resource_detector2.detect.return_value = Resource( {"key2": "value2", "key3": "value3"} ) - resource_detector3 = mock.Mock(spec=resources.ResourceDetector) - resource_detector3.detect.return_value = resources.Resource( + resource_detector3 = Mock(spec=ResourceDetector) + resource_detector3.detect.return_value = Resource( { "key2": "try_to_overwrite_existing_value", "key3": "try_to_overwrite_existing_value", @@ -287,15 +291,13 @@ def test_aggregated_resources_multiple_detectors(self): ) self.assertEqual( - resources.get_aggregated_resources( + get_aggregated_resources( [resource_detector1, resource_detector2, resource_detector3] ), - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ).merge( - resources.Resource( + Resource( { "key1": "value1", "key2": "try_to_overwrite_existing_value", @@ -307,16 +309,16 @@ def test_aggregated_resources_multiple_detectors(self): ) def test_aggregated_resources_different_schema_urls(self): - resource_detector1 = mock.Mock(spec=resources.ResourceDetector) - resource_detector1.detect.return_value = resources.Resource( + resource_detector1 = Mock(spec=ResourceDetector) + resource_detector1.detect.return_value = Resource( {"key1": "value1"}, "" ) - resource_detector2 = mock.Mock(spec=resources.ResourceDetector) - resource_detector2.detect.return_value = resources.Resource( + resource_detector2 = Mock(spec=ResourceDetector) + resource_detector2.detect.return_value = Resource( {"key2": "value2", "key3": "value3"}, "url1" ) - resource_detector3 = mock.Mock(spec=resources.ResourceDetector) - resource_detector3.detect.return_value = resources.Resource( + resource_detector3 = Mock(spec=ResourceDetector) + resource_detector3.detect.return_value = Resource( { "key2": "try_to_overwrite_existing_value", "key3": "try_to_overwrite_existing_value", @@ -324,8 +326,8 @@ def test_aggregated_resources_different_schema_urls(self): }, "url2", ) - resource_detector4 = mock.Mock(spec=resources.ResourceDetector) - resource_detector4.detect.return_value = resources.Resource( + resource_detector4 = Mock(spec=ResourceDetector) + resource_detector4.detect.return_value = Resource( { "key2": "try_to_overwrite_existing_value", "key3": "try_to_overwrite_existing_value", @@ -334,15 +336,11 @@ def test_aggregated_resources_different_schema_urls(self): "url1", ) self.assertEqual( - resources.get_aggregated_resources( - [resource_detector1, resource_detector2] - ), - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + get_aggregated_resources([resource_detector1, resource_detector2]), + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ).merge( - resources.Resource( + Resource( {"key1": "value1", "key2": "value2", "key3": "value3"}, "url1", ) @@ -350,24 +348,20 @@ def test_aggregated_resources_different_schema_urls(self): ) with self.assertLogs(level=ERROR) as log_entry: self.assertEqual( - resources.get_aggregated_resources( + get_aggregated_resources( [resource_detector2, resource_detector3] ), - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ).merge( - resources.Resource( - {"key2": "value2", "key3": "value3"}, "url1" - ) + Resource({"key2": "value2", "key3": "value3"}, "url1") ), ) self.assertIn("url1", log_entry.output[0]) self.assertIn("url2", log_entry.output[0]) with self.assertLogs(level=ERROR): self.assertEqual( - resources.get_aggregated_resources( + get_aggregated_resources( [ resource_detector2, resource_detector3, @@ -375,12 +369,10 @@ def test_aggregated_resources_different_schema_urls(self): resource_detector1, ] ), - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ).merge( - resources.Resource( + Resource( { "key1": "value1", "key2": "try_to_overwrite_existing_value", @@ -395,116 +387,106 @@ def test_aggregated_resources_different_schema_urls(self): self.assertIn("url2", log_entry.output[0]) def test_resource_detector_ignore_error(self): - resource_detector = mock.Mock(spec=resources.ResourceDetector) + resource_detector = Mock(spec=ResourceDetector) resource_detector.detect.side_effect = Exception() resource_detector.raise_on_error = False self.assertEqual( - resources.get_aggregated_resources([resource_detector]), - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + get_aggregated_resources([resource_detector]), + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ), ) def test_resource_detector_raise_error(self): - resource_detector = mock.Mock(spec=resources.ResourceDetector) + resource_detector = Mock(spec=ResourceDetector) resource_detector.detect.side_effect = Exception() resource_detector.raise_on_error = True self.assertRaises( - Exception, resources.get_aggregated_resources, [resource_detector] + Exception, get_aggregated_resources, [resource_detector] ) - @mock.patch.dict( - os.environ, + @patch.dict( + environ, {"OTEL_RESOURCE_ATTRIBUTES": "key1=env_value1,key2=env_value2"}, ) def test_env_priority(self): - resource_env = resources.Resource.create() + resource_env = Resource.create() self.assertEqual(resource_env.attributes["key1"], "env_value1") self.assertEqual(resource_env.attributes["key2"], "env_value2") - resource_env_override = resources.Resource.create( + resource_env_override = Resource.create( {"key1": "value1", "key2": "value2"} ) self.assertEqual(resource_env_override.attributes["key1"], "value1") self.assertEqual(resource_env_override.attributes["key2"], "value2") - @mock.patch.dict( - os.environ, + @patch.dict( + environ, { - resources.OTEL_SERVICE_NAME: "test-srv-name", - resources.OTEL_RESOURCE_ATTRIBUTES: "service.name=svc-name-from-resource", + OTEL_SERVICE_NAME: "test-srv-name", + OTEL_RESOURCE_ATTRIBUTES: "service.name=svc-name-from-resource", }, ) def test_service_name_env(self): - resource = resources.Resource.create() + resource = Resource.create() self.assertEqual(resource.attributes["service.name"], "test-srv-name") - resource = resources.Resource.create({"service.name": "from-code"}) + resource = Resource.create({"service.name": "from-code"}) self.assertEqual(resource.attributes["service.name"], "from-code") class TestOTELResourceDetector(unittest.TestCase): def setUp(self) -> None: - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" + environ[OTEL_RESOURCE_ATTRIBUTES] = "" def tearDown(self) -> None: - os.environ.pop(resources.OTEL_RESOURCE_ATTRIBUTES) + environ.pop(OTEL_RESOURCE_ATTRIBUTES) def test_empty(self): - detector = resources.OTELResourceDetector() - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" - self.assertEqual(detector.detect(), resources.Resource.get_empty()) + detector = OTELResourceDetector() + environ[OTEL_RESOURCE_ATTRIBUTES] = "" + self.assertEqual(detector.detect(), Resource.get_empty()) def test_one(self): - detector = resources.OTELResourceDetector() - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "k=v" - self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) + detector = OTELResourceDetector() + environ[OTEL_RESOURCE_ATTRIBUTES] = "k=v" + self.assertEqual(detector.detect(), Resource({"k": "v"})) def test_one_with_whitespace(self): - detector = resources.OTELResourceDetector() - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = " k = v " - self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) + detector = OTELResourceDetector() + environ[OTEL_RESOURCE_ATTRIBUTES] = " k = v " + self.assertEqual(detector.detect(), Resource({"k": "v"})) def test_multiple(self): - detector = resources.OTELResourceDetector() - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "k=v,k2=v2" - self.assertEqual( - detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) - ) + detector = OTELResourceDetector() + environ[OTEL_RESOURCE_ATTRIBUTES] = "k=v,k2=v2" + self.assertEqual(detector.detect(), Resource({"k": "v", "k2": "v2"})) def test_multiple_with_whitespace(self): - detector = resources.OTELResourceDetector() - os.environ[ - resources.OTEL_RESOURCE_ATTRIBUTES - ] = " k = v , k2 = v2 " - self.assertEqual( - detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) - ) + detector = OTELResourceDetector() + environ[OTEL_RESOURCE_ATTRIBUTES] = " k = v , k2 = v2 " + self.assertEqual(detector.detect(), Resource({"k": "v", "k2": "v2"})) def test_invalid_key_value_pairs(self): - detector = resources.OTELResourceDetector() - os.environ[ - resources.OTEL_RESOURCE_ATTRIBUTES - ] = "k=v,k2=v2,invalid,,foo=bar=baz," + detector = OTELResourceDetector() + environ[OTEL_RESOURCE_ATTRIBUTES] = "k=v,k2=v2,invalid,,foo=bar=baz," self.assertEqual( detector.detect(), - resources.Resource({"k": "v", "k2": "v2", "foo": "bar=baz"}), + Resource({"k": "v", "k2": "v2", "foo": "bar=baz"}), ) def test_multiple_with_url_decode(self): - detector = resources.OTELResourceDetector() - os.environ[ - resources.OTEL_RESOURCE_ATTRIBUTES + detector = OTELResourceDetector() + environ[ + OTEL_RESOURCE_ATTRIBUTES ] = "key=value%20test%0A, key2=value+%202" self.assertEqual( detector.detect(), - resources.Resource({"key": "value test\n", "key2": "value+ 2"}), + Resource({"key": "value test\n", "key2": "value+ 2"}), ) self.assertEqual( detector.detect(), - resources.Resource( + Resource( { "key": parse.unquote("value%20test%0A"), "key2": parse.unquote("value+%202"), @@ -512,46 +494,158 @@ def test_multiple_with_url_decode(self): ), ) - @mock.patch.dict( - os.environ, - {resources.OTEL_SERVICE_NAME: "test-srv-name"}, + @patch.dict( + environ, + {OTEL_SERVICE_NAME: "test-srv-name"}, ) def test_service_name_env(self): - detector = resources.OTELResourceDetector() + detector = OTELResourceDetector() self.assertEqual( detector.detect(), - resources.Resource({"service.name": "test-srv-name"}), + Resource({"service.name": "test-srv-name"}), ) - @mock.patch.dict( - os.environ, + @patch.dict( + environ, { - resources.OTEL_SERVICE_NAME: "from-service-name", - resources.OTEL_RESOURCE_ATTRIBUTES: "service.name=from-resource-attrs", + OTEL_SERVICE_NAME: "from-service-name", + OTEL_RESOURCE_ATTRIBUTES: "service.name=from-resource-attrs", }, ) def test_service_name_env_precedence(self): - detector = resources.OTELResourceDetector() + detector = OTELResourceDetector() self.assertEqual( detector.detect(), - resources.Resource({"service.name": "from-service-name"}), + Resource({"service.name": "from-service-name"}), ) def test_process_detector(self): - initial_resource = resources.Resource({"foo": "bar"}) - aggregated_resource = resources.get_aggregated_resources( - [resources.ProcessResourceDetector()], initial_resource + initial_resource = Resource({"foo": "bar"}) + aggregated_resource = get_aggregated_resources( + [ProcessResourceDetector()], initial_resource ) self.assertIn( - resources.PROCESS_RUNTIME_NAME, + PROCESS_RUNTIME_NAME, aggregated_resource.attributes.keys(), ) self.assertIn( - resources.PROCESS_RUNTIME_DESCRIPTION, + PROCESS_RUNTIME_DESCRIPTION, aggregated_resource.attributes.keys(), ) self.assertIn( - resources.PROCESS_RUNTIME_VERSION, + PROCESS_RUNTIME_VERSION, aggregated_resource.attributes.keys(), ) + + def test_resource_detector_entry_points_default(self): + + resource = Resource({}).create() + + self.assertEqual( + resource.attributes["telemetry.sdk.language"], "python" + ) + self.assertEqual( + resource.attributes["telemetry.sdk.name"], "opentelemetry" + ) + self.assertEqual( + resource.attributes["service.name"], "unknown_service" + ) + self.assertEqual(resource.schema_url, "") + + resource = Resource({}).create({"a": "b", "c": "d"}) + + self.assertEqual( + resource.attributes["telemetry.sdk.language"], "python" + ) + self.assertEqual( + resource.attributes["telemetry.sdk.name"], "opentelemetry" + ) + self.assertEqual( + resource.attributes["service.name"], "unknown_service" + ) + self.assertEqual(resource.attributes["a"], "b") + self.assertEqual(resource.attributes["c"], "d") + self.assertEqual(resource.schema_url, "") + + @patch.dict( + environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "mock"}, clear=True + ) + @patch( + "opentelemetry.sdk.resources.entry_points", + Mock( + return_value=[ + Mock( + **{ + "load.return_value": Mock( + return_value=Mock( + **{"detect.return_value": Resource({"a": "b"})} + ) + ) + } + ) + ] + ), + ) + def test_resource_detector_entry_points_non_default(self): + resource = Resource({}).create() + self.assertEqual( + resource.attributes["telemetry.sdk.language"], "python" + ) + self.assertEqual( + resource.attributes["telemetry.sdk.name"], "opentelemetry" + ) + self.assertEqual( + resource.attributes["service.name"], "unknown_service" + ) + self.assertEqual(resource.attributes["a"], "b") + self.assertEqual(resource.schema_url, "") + + def test_resource_detector_entry_points_otel(self): + """ + Test that OTELResourceDetector-resource-generated attributes are + always being added. + """ + with patch.dict( + environ, {OTEL_RESOURCE_ATTRIBUTES: "a=b,c=d"}, clear=True + ): + resource = Resource({}).create() + self.assertEqual( + resource.attributes["telemetry.sdk.language"], "python" + ) + self.assertEqual( + resource.attributes["telemetry.sdk.name"], "opentelemetry" + ) + self.assertEqual( + resource.attributes["service.name"], "unknown_service" + ) + self.assertEqual(resource.attributes["a"], "b") + self.assertEqual(resource.attributes["c"], "d") + self.assertEqual(resource.schema_url, "") + + with patch.dict( + environ, + { + OTEL_RESOURCE_ATTRIBUTES: "a=b,c=d", + OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "process", + }, + clear=True, + ): + resource = Resource({}).create() + self.assertEqual( + resource.attributes["telemetry.sdk.language"], "python" + ) + self.assertEqual( + resource.attributes["telemetry.sdk.name"], "opentelemetry" + ) + self.assertEqual( + resource.attributes["service.name"], "unknown_service" + ) + self.assertEqual(resource.attributes["a"], "b") + self.assertEqual(resource.attributes["c"], "d") + self.assertIn(PROCESS_RUNTIME_NAME, resource.attributes.keys()) + self.assertIn( + PROCESS_RUNTIME_DESCRIPTION, resource.attributes.keys() + ) + self.assertIn(PROCESS_RUNTIME_VERSION, resource.attributes.keys()) + self.assertEqual(resource.schema_url, "") From 3c98eee24aa69b201db542f3472eee28f6fe598c Mon Sep 17 00:00:00 2001 From: Jeremy Voss Date: Fri, 31 Mar 2023 10:13:13 -0700 Subject: [PATCH 1416/1517] Add speced out environment variables and arguments for BatchLogRecordProcessor (#3237) --- CHANGELOG.md | 2 + .../sdk/_logs/_internal/export/__init__.py | 141 +++++++++++++++++- .../sdk/environment_variables.py | 40 ++++- .../sdk/trace/export/__init__.py | 124 ++++++++++++--- opentelemetry-sdk/tests/logs/test_export.py | 127 ++++++++++++++++ .../tests/trace/export/test_export.py | 33 +++- 6 files changed, 434 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0bb37d48e..77e5579688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3226](https://github.com/open-telemetry/opentelemetry-python/pull/3226)) - Fix suppress instrumentation for log batch processor ([#3223](https://github.com/open-telemetry/opentelemetry-python/pull/3223)) +- Add speced out environment variables and arguments for BatchLogRecordProcessor + ([#3237](https://github.com/open-telemetry/opentelemetry-python/pull/3237)) ## Version 1.17.0/0.38b0 (2023-03-22) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py index 6f2f1c2c26..4903873432 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -19,7 +19,7 @@ import os import sys import threading -from os import linesep +from os import environ, linesep from time import time_ns from typing import IO, Callable, Deque, List, Optional, Sequence @@ -30,8 +30,22 @@ set_value, ) from opentelemetry.sdk._logs import LogData, LogRecord, LogRecordProcessor +from opentelemetry.sdk.environment_variables import ( + OTEL_BLRP_EXPORT_TIMEOUT, + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, + OTEL_BLRP_MAX_QUEUE_SIZE, + OTEL_BLRP_SCHEDULE_DELAY, +) from opentelemetry.util._once import Once +_DEFAULT_SCHEDULE_DELAY_MILLIS = 5000 +_DEFAULT_MAX_EXPORT_BATCH_SIZE = 512 +_DEFAULT_EXPORT_TIMEOUT_MILLIS = 30000 +_DEFAULT_MAX_QUEUE_SIZE = 2048 +_ENV_VAR_INT_VALUE_ERROR_MESSAGE = ( + "Unable to parse value for %s as integer. Defaulting to %s." +) + _logger = logging.getLogger(__name__) @@ -142,20 +156,54 @@ class BatchLogRecordProcessor(LogRecordProcessor): """This is an implementation of LogRecordProcessor which creates batches of received logs in the export-friendly LogData representation and send to the configured LogExporter, as soon as they are emitted. + + `BatchLogRecordProcessor` is configurable with the following environment + variables which correspond to constructor parameters: + + - :envvar:`OTEL_BLRP_SCHEDULE_DELAY` + - :envvar:`OTEL_BLRP_MAX_QUEUE_SIZE` + - :envvar:`OTEL_BLRP_MAX_EXPORT_BATCH_SIZE` + - :envvar:`OTEL_BLRP_EXPORT_TIMEOUT` """ def __init__( self, exporter: LogExporter, - schedule_delay_millis: int = 5000, - max_export_batch_size: int = 512, - export_timeout_millis: int = 30000, + schedule_delay_millis: float = None, + max_export_batch_size: int = None, + export_timeout_millis: float = None, + max_queue_size: int = None, ): + if max_queue_size is None: + max_queue_size = BatchLogRecordProcessor._default_max_queue_size() + + if schedule_delay_millis is None: + schedule_delay_millis = ( + BatchLogRecordProcessor._default_schedule_delay_millis() + ) + + if max_export_batch_size is None: + max_export_batch_size = ( + BatchLogRecordProcessor._default_max_export_batch_size() + ) + + if export_timeout_millis is None: + export_timeout_millis = ( + BatchLogRecordProcessor._default_export_timeout_millis() + ) + + BatchLogRecordProcessor._validate_arguments( + max_queue_size, schedule_delay_millis, max_export_batch_size + ) + self._exporter = exporter + self._max_queue_size = max_queue_size self._schedule_delay_millis = schedule_delay_millis self._max_export_batch_size = max_export_batch_size self._export_timeout_millis = export_timeout_millis - self._queue = collections.deque() # type: Deque[LogData] + self._queue = collections.deque( + [], max_queue_size + ) # type: Deque[LogData] self._worker_thread = threading.Thread( name="OtelBatchLogRecordProcessor", target=self.worker, @@ -333,3 +381,86 @@ def force_flush(self, timeout_millis: Optional[int] = None) -> bool: if not ret: _logger.warning("Timeout was exceeded in force_flush().") return ret + + @staticmethod + def _default_max_queue_size(): + try: + return int( + environ.get(OTEL_BLRP_MAX_QUEUE_SIZE, _DEFAULT_MAX_QUEUE_SIZE) + ) + except ValueError: + _logger.exception( + _ENV_VAR_INT_VALUE_ERROR_MESSAGE, + OTEL_BLRP_MAX_QUEUE_SIZE, + _DEFAULT_MAX_QUEUE_SIZE, + ) + return _DEFAULT_MAX_QUEUE_SIZE + + @staticmethod + def _default_schedule_delay_millis(): + try: + return int( + environ.get( + OTEL_BLRP_SCHEDULE_DELAY, _DEFAULT_SCHEDULE_DELAY_MILLIS + ) + ) + except ValueError: + _logger.exception( + _ENV_VAR_INT_VALUE_ERROR_MESSAGE, + OTEL_BLRP_SCHEDULE_DELAY, + _DEFAULT_SCHEDULE_DELAY_MILLIS, + ) + return _DEFAULT_SCHEDULE_DELAY_MILLIS + + @staticmethod + def _default_max_export_batch_size(): + try: + return int( + environ.get( + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, + _DEFAULT_MAX_EXPORT_BATCH_SIZE, + ) + ) + except ValueError: + _logger.exception( + _ENV_VAR_INT_VALUE_ERROR_MESSAGE, + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, + _DEFAULT_MAX_EXPORT_BATCH_SIZE, + ) + return _DEFAULT_MAX_EXPORT_BATCH_SIZE + + @staticmethod + def _default_export_timeout_millis(): + try: + return int( + environ.get( + OTEL_BLRP_EXPORT_TIMEOUT, _DEFAULT_EXPORT_TIMEOUT_MILLIS + ) + ) + except ValueError: + _logger.exception( + _ENV_VAR_INT_VALUE_ERROR_MESSAGE, + OTEL_BLRP_EXPORT_TIMEOUT, + _DEFAULT_EXPORT_TIMEOUT_MILLIS, + ) + return _DEFAULT_EXPORT_TIMEOUT_MILLIS + + @staticmethod + def _validate_arguments( + max_queue_size, schedule_delay_millis, max_export_batch_size + ): + if max_queue_size <= 0: + raise ValueError("max_queue_size must be a positive integer.") + + if schedule_delay_millis <= 0: + raise ValueError("schedule_delay_millis must be positive.") + + if max_export_batch_size <= 0: + raise ValueError( + "max_export_batch_size must be a positive integer." + ) + + if max_export_batch_size > max_queue_size: + raise ValueError( + "max_export_batch_size must be less than or equal to max_queue_size." + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 8e36ae58c7..275aa48340 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -66,11 +66,43 @@ i.e. the SDK behaves as if OTEL_TRACES_SAMPLER_ARG is not set. """ +OTEL_BLRP_SCHEDULE_DELAY = "OTEL_BLRP_SCHEDULE_DELAY" +""" +.. envvar:: OTEL_BLRP_SCHEDULE_DELAY + +The :envvar:`OTEL_BLRP_SCHEDULE_DELAY` represents the delay interval between two consecutive exports of the BatchLogRecordProcessor. +Default: 5000 +""" + +OTEL_BLRP_EXPORT_TIMEOUT = "OTEL_BLRP_EXPORT_TIMEOUT" +""" +.. envvar:: OTEL_BLRP_EXPORT_TIMEOUT + +The :envvar:`OTEL_BLRP_EXPORT_TIMEOUT` represents the maximum allowed time to export data from the BatchLogRecordProcessor. +Default: 30000 +""" + +OTEL_BLRP_MAX_QUEUE_SIZE = "OTEL_BLRP_MAX_QUEUE_SIZE" +""" +.. envvar:: OTEL_BLRP_MAX_QUEUE_SIZE + +The :envvar:`OTEL_BLRP_MAX_QUEUE_SIZE` represents the maximum queue size for the data export of the BatchLogRecordProcessor. +Default: 2048 +""" + +OTEL_BLRP_MAX_EXPORT_BATCH_SIZE = "OTEL_BLRP_MAX_EXPORT_BATCH_SIZE" +""" +.. envvar:: OTEL_BLRP_MAX_EXPORT_BATCH_SIZE + +The :envvar:`OTEL_BLRP_MAX_EXPORT_BATCH_SIZE` represents the maximum batch size for the data export of the BatchLogRecordProcessor. +Default: 512 +""" + OTEL_BSP_SCHEDULE_DELAY = "OTEL_BSP_SCHEDULE_DELAY" """ .. envvar:: OTEL_BSP_SCHEDULE_DELAY -The :envvar:`OTEL_BSP_SCHEDULE_DELAY` represents the delay interval between two consecutive exports. +The :envvar:`OTEL_BSP_SCHEDULE_DELAY` represents the delay interval between two consecutive exports of the BatchSpanProcessor. Default: 5000 """ @@ -78,7 +110,7 @@ """ .. envvar:: OTEL_BSP_EXPORT_TIMEOUT -The :envvar:`OTEL_BSP_EXPORT_TIMEOUT` represents the maximum allowed time to export data. +The :envvar:`OTEL_BSP_EXPORT_TIMEOUT` represents the maximum allowed time to export data from the BatchSpanProcessor. Default: 30000 """ @@ -86,7 +118,7 @@ """ .. envvar:: OTEL_BSP_MAX_QUEUE_SIZE -The :envvar:`OTEL_BSP_MAX_QUEUE_SIZE` represents the maximum queue size for the data export. +The :envvar:`OTEL_BSP_MAX_QUEUE_SIZE` represents the maximum queue size for the data export of the BatchSpanProcessor. Default: 2048 """ @@ -94,7 +126,7 @@ """ .. envvar:: OTEL_BSP_MAX_EXPORT_BATCH_SIZE -The :envvar:`OTEL_BSP_MAX_EXPORT_BATCH_SIZE` represents the maximum batch size for the data export. +The :envvar:`OTEL_BSP_MAX_EXPORT_BATCH_SIZE` represents the maximum batch size for the data export of the BatchSpanProcessor. Default: 512 """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index ef04895819..7f56a30172 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -39,6 +39,14 @@ from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor from opentelemetry.util._once import Once +_DEFAULT_SCHEDULE_DELAY_MILLIS = 5000 +_DEFAULT_MAX_EXPORT_BATCH_SIZE = 512 +_DEFAULT_EXPORT_TIMEOUT_MILLIS = 30000 +_DEFAULT_MAX_QUEUE_SIZE = 2048 +_ENV_VAR_INT_VALUE_ERROR_MESSAGE = ( + "Unable to parse value for %s as integer. Defaulting to %s." +) + logger = logging.getLogger(__name__) @@ -152,40 +160,27 @@ def __init__( max_export_batch_size: int = None, export_timeout_millis: float = None, ): - if max_queue_size is None: - max_queue_size = int(environ.get(OTEL_BSP_MAX_QUEUE_SIZE, 2048)) + max_queue_size = BatchSpanProcessor._default_max_queue_size() if schedule_delay_millis is None: - schedule_delay_millis = int( - environ.get(OTEL_BSP_SCHEDULE_DELAY, 5000) + schedule_delay_millis = ( + BatchSpanProcessor._default_schedule_delay_millis() ) if max_export_batch_size is None: - max_export_batch_size = int( - environ.get(OTEL_BSP_MAX_EXPORT_BATCH_SIZE, 512) + max_export_batch_size = ( + BatchSpanProcessor._default_max_export_batch_size() ) if export_timeout_millis is None: - export_timeout_millis = int( - environ.get(OTEL_BSP_EXPORT_TIMEOUT, 30000) + export_timeout_millis = ( + BatchSpanProcessor._default_export_timeout_millis() ) - if max_queue_size <= 0: - raise ValueError("max_queue_size must be a positive integer.") - - if schedule_delay_millis <= 0: - raise ValueError("schedule_delay_millis must be positive.") - - if max_export_batch_size <= 0: - raise ValueError( - "max_export_batch_size must be a positive integer." - ) - - if max_export_batch_size > max_queue_size: - raise ValueError( - "max_export_batch_size must be less than or equal to max_queue_size." - ) + BatchSpanProcessor._validate_arguments( + max_queue_size, schedule_delay_millis, max_export_batch_size + ) self.span_exporter = span_exporter self.queue = collections.deque( @@ -417,6 +412,89 @@ def shutdown(self) -> None: self.worker_thread.join() self.span_exporter.shutdown() + @staticmethod + def _default_max_queue_size(): + try: + return int( + environ.get(OTEL_BSP_MAX_QUEUE_SIZE, _DEFAULT_MAX_QUEUE_SIZE) + ) + except ValueError: + logger.exception( + _ENV_VAR_INT_VALUE_ERROR_MESSAGE, + OTEL_BSP_MAX_QUEUE_SIZE, + _DEFAULT_MAX_QUEUE_SIZE, + ) + return _DEFAULT_MAX_QUEUE_SIZE + + @staticmethod + def _default_schedule_delay_millis(): + try: + return int( + environ.get( + OTEL_BSP_SCHEDULE_DELAY, _DEFAULT_SCHEDULE_DELAY_MILLIS + ) + ) + except ValueError: + logger.exception( + _ENV_VAR_INT_VALUE_ERROR_MESSAGE, + OTEL_BSP_SCHEDULE_DELAY, + _DEFAULT_SCHEDULE_DELAY_MILLIS, + ) + return _DEFAULT_SCHEDULE_DELAY_MILLIS + + @staticmethod + def _default_max_export_batch_size(): + try: + return int( + environ.get( + OTEL_BSP_MAX_EXPORT_BATCH_SIZE, + _DEFAULT_MAX_EXPORT_BATCH_SIZE, + ) + ) + except ValueError: + logger.exception( + _ENV_VAR_INT_VALUE_ERROR_MESSAGE, + OTEL_BSP_MAX_EXPORT_BATCH_SIZE, + _DEFAULT_MAX_EXPORT_BATCH_SIZE, + ) + return _DEFAULT_MAX_EXPORT_BATCH_SIZE + + @staticmethod + def _default_export_timeout_millis(): + try: + return int( + environ.get( + OTEL_BSP_EXPORT_TIMEOUT, _DEFAULT_EXPORT_TIMEOUT_MILLIS + ) + ) + except ValueError: + logger.exception( + _ENV_VAR_INT_VALUE_ERROR_MESSAGE, + OTEL_BSP_EXPORT_TIMEOUT, + _DEFAULT_EXPORT_TIMEOUT_MILLIS, + ) + return _DEFAULT_EXPORT_TIMEOUT_MILLIS + + @staticmethod + def _validate_arguments( + max_queue_size, schedule_delay_millis, max_export_batch_size + ): + if max_queue_size <= 0: + raise ValueError("max_queue_size must be a positive integer.") + + if schedule_delay_millis <= 0: + raise ValueError("schedule_delay_millis must be positive.") + + if max_export_batch_size <= 0: + raise ValueError( + "max_export_batch_size must be a positive integer." + ) + + if max_export_batch_size > max_queue_size: + raise ValueError( + "max_export_batch_size must be less than or equal to max_queue_size." + ) + class ConsoleSpanExporter(SpanExporter): """Implementation of :class:`SpanExporter` that prints spans to the diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 6a6b1d5bf8..aa68b09624 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -35,6 +35,12 @@ InMemoryLogExporter, SimpleLogRecordProcessor, ) +from opentelemetry.sdk.environment_variables import ( + OTEL_BLRP_EXPORT_TIMEOUT, + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, + OTEL_BLRP_MAX_QUEUE_SIZE, + OTEL_BLRP_SCHEDULE_DELAY, +) from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.concurrency_test import ConcurrencyTestBase @@ -175,6 +181,127 @@ def test_emit_call_log_record(self): logger.error("error") self.assertEqual(log_record_processor.emit.call_count, 1) + def test_args(self): + exporter = InMemoryLogExporter() + log_record_processor = BatchLogRecordProcessor( + exporter, + max_queue_size=1024, + schedule_delay_millis=2500, + max_export_batch_size=256, + export_timeout_millis=15000, + ) + self.assertEqual(log_record_processor._exporter, exporter) + self.assertEqual(log_record_processor._max_queue_size, 1024) + self.assertEqual(log_record_processor._schedule_delay_millis, 2500) + self.assertEqual(log_record_processor._max_export_batch_size, 256) + self.assertEqual(log_record_processor._export_timeout_millis, 15000) + + @patch.dict( + "os.environ", + { + OTEL_BLRP_MAX_QUEUE_SIZE: "1024", + OTEL_BLRP_SCHEDULE_DELAY: "2500", + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE: "256", + OTEL_BLRP_EXPORT_TIMEOUT: "15000", + }, + ) + def test_env_vars(self): + exporter = InMemoryLogExporter() + log_record_processor = BatchLogRecordProcessor(exporter) + self.assertEqual(log_record_processor._exporter, exporter) + self.assertEqual(log_record_processor._max_queue_size, 1024) + self.assertEqual(log_record_processor._schedule_delay_millis, 2500) + self.assertEqual(log_record_processor._max_export_batch_size, 256) + self.assertEqual(log_record_processor._export_timeout_millis, 15000) + + def test_args_defaults(self): + exporter = InMemoryLogExporter() + log_record_processor = BatchLogRecordProcessor(exporter) + self.assertEqual(log_record_processor._exporter, exporter) + self.assertEqual(log_record_processor._max_queue_size, 2048) + self.assertEqual(log_record_processor._schedule_delay_millis, 5000) + self.assertEqual(log_record_processor._max_export_batch_size, 512) + self.assertEqual(log_record_processor._export_timeout_millis, 30000) + + @patch.dict( + "os.environ", + { + OTEL_BLRP_MAX_QUEUE_SIZE: "a", + OTEL_BLRP_SCHEDULE_DELAY: " ", + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE: "One", + OTEL_BLRP_EXPORT_TIMEOUT: "@", + }, + ) + def test_args_env_var_value_error(self): + exporter = InMemoryLogExporter() + log_record_processor = BatchLogRecordProcessor(exporter) + self.assertEqual(log_record_processor._exporter, exporter) + self.assertEqual(log_record_processor._max_queue_size, 2048) + self.assertEqual(log_record_processor._schedule_delay_millis, 5000) + self.assertEqual(log_record_processor._max_export_batch_size, 512) + self.assertEqual(log_record_processor._export_timeout_millis, 30000) + + def test_args_none_defaults(self): + exporter = InMemoryLogExporter() + log_record_processor = BatchLogRecordProcessor( + exporter, + max_queue_size=None, + schedule_delay_millis=None, + max_export_batch_size=None, + export_timeout_millis=None, + ) + self.assertEqual(log_record_processor._exporter, exporter) + self.assertEqual(log_record_processor._max_queue_size, 2048) + self.assertEqual(log_record_processor._schedule_delay_millis, 5000) + self.assertEqual(log_record_processor._max_export_batch_size, 512) + self.assertEqual(log_record_processor._export_timeout_millis, 30000) + + def test_validation_negative_max_queue_size(self): + exporter = InMemoryLogExporter() + self.assertRaises( + ValueError, + BatchLogRecordProcessor, + exporter, + max_queue_size=0, + ) + self.assertRaises( + ValueError, + BatchLogRecordProcessor, + exporter, + max_queue_size=-1, + ) + self.assertRaises( + ValueError, + BatchLogRecordProcessor, + exporter, + schedule_delay_millis=0, + ) + self.assertRaises( + ValueError, + BatchLogRecordProcessor, + exporter, + schedule_delay_millis=-1, + ) + self.assertRaises( + ValueError, + BatchLogRecordProcessor, + exporter, + max_export_batch_size=0, + ) + self.assertRaises( + ValueError, + BatchLogRecordProcessor, + exporter, + max_export_batch_size=-1, + ) + self.assertRaises( + ValueError, + BatchLogRecordProcessor, + exporter, + max_queue_size=100, + max_export_batch_size=101, + ) + def test_shutdown(self): exporter = InMemoryLogExporter() log_record_processor = BatchLogRecordProcessor(exporter) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 422945798f..cb7739f328 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -175,7 +175,7 @@ class TestBatchSpanProcessor(ConcurrencyTestBase): OTEL_BSP_EXPORT_TIMEOUT: "4", }, ) - def test_batch_span_processor_environment_variables(self): + def test_args_env_var(self): batch_span_processor = export.BatchSpanProcessor( MySpanExporter(destination=[]) @@ -186,6 +186,37 @@ def test_batch_span_processor_environment_variables(self): self.assertEqual(batch_span_processor.max_export_batch_size, 3) self.assertEqual(batch_span_processor.export_timeout_millis, 4) + def test_args_env_var_defaults(self): + + batch_span_processor = export.BatchSpanProcessor( + MySpanExporter(destination=[]) + ) + + self.assertEqual(batch_span_processor.max_queue_size, 2048) + self.assertEqual(batch_span_processor.schedule_delay_millis, 5000) + self.assertEqual(batch_span_processor.max_export_batch_size, 512) + self.assertEqual(batch_span_processor.export_timeout_millis, 30000) + + @mock.patch.dict( + "os.environ", + { + OTEL_BSP_MAX_QUEUE_SIZE: "a", + OTEL_BSP_SCHEDULE_DELAY: " ", + OTEL_BSP_MAX_EXPORT_BATCH_SIZE: "One", + OTEL_BSP_EXPORT_TIMEOUT: "@", + }, + ) + def test_args_env_var_value_error(self): + + batch_span_processor = export.BatchSpanProcessor( + MySpanExporter(destination=[]) + ) + + self.assertEqual(batch_span_processor.max_queue_size, 2048) + self.assertEqual(batch_span_processor.schedule_delay_millis, 5000) + self.assertEqual(batch_span_processor.max_export_batch_size, 512) + self.assertEqual(batch_span_processor.export_timeout_millis, 30000) + def test_on_start_accepts_parent_context(self): # pylint: disable=no-self-use my_exporter = MySpanExporter(destination=[]) From 17aa1e862c450d27b3c138859cd08b1ec6deaa7d Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 3 Apr 2023 16:22:41 -0400 Subject: [PATCH 1417/1517] Update OpenCensus shim to update OpenCensus execution context (#3231) * Update OpenCensus shim to update OpenCensus execution context Previously, the shim was only writing to OTel context. This updates it to write to both OC and OTel. Also updated the code to use the `span_context` provided when instrumentation instantiates the `Tracer`. --- .../opentelemetry/shim/opencensus/_patch.py | 21 +++-- .../shim/opencensus/_shim_span.py | 12 ++- .../shim/opencensus/_shim_tracer.py | 72 +++++++++++++++- .../tests/test_shim.py | 83 ++++++++++++++++++- .../tests/test_shim_with_sdk.py | 70 +++++++++++++++- 5 files changed, 243 insertions(+), 15 deletions(-) diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py index 42b1b189ce..c3c6e81037 100644 --- a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from functools import lru_cache from logging import getLogger from typing import Optional +from opencensus.trace.span_context import SpanContext from opencensus.trace.tracer import Tracer from opencensus.trace.tracers.noop_tracer import NoopTracer @@ -33,10 +35,19 @@ def install_shim( __version__, tracer_provider=tracer_provider, ) - shim_tracer = ShimTracer(NoopTracer(), otel_tracer=otel_tracer) - def fget_tracer(self) -> ShimTracer: - return shim_tracer + @lru_cache() + def cached_shim_tracer(span_context: SpanContext) -> ShimTracer: + return ShimTracer( + NoopTracer(), + oc_span_context=span_context, + otel_tracer=otel_tracer, + ) + + def fget_tracer(self: Tracer) -> ShimTracer: + # self.span_context is how instrumentations pass propagated context into OpenCensus e.g. + # https://github.com/census-instrumentation/opencensus-python/blob/fd064f438c5e490d25b004ee2545be55d2e28679/contrib/opencensus-ext-flask/opencensus/ext/flask/flask_middleware.py#L147-L153 + return cached_shim_tracer(self.span_context) def fset_tracer(self, value) -> None: # ignore attempts to set the value @@ -45,8 +56,8 @@ def fset_tracer(self, value) -> None: # Tracer's constructor sets self.tracer to either a NoopTracer or ContextTracer depending # on sampler: # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/tracer.py#L63. - # We monkeypatch Tracer.tracer with a property to return the shim instance instead. This - # makes all instances of Tracer (even those already created) use the ShimTracer singleton. + # We monkeypatch Tracer.tracer with a property to return a shim instance instead. This + # makes all instances of Tracer (even those already created) use a ShimTracer. Tracer.tracer = property(fget_tracer, fset_tracer) _logger.info("Installed OpenCensus shim") diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_span.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_span.py index fdc6f724a4..f3ff804c6f 100644 --- a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_span.py +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_span.py @@ -17,7 +17,8 @@ from typing import TYPE_CHECKING import wrapt -from opencensus.trace.base_span import BaseSpan +from opencensus.trace import execution_context +from opencensus.trace.blank_span import BlankSpan from opencensus.trace.span import SpanKind from opencensus.trace.status import Status from opencensus.trace.time_event import MessageEvent @@ -62,7 +63,7 @@ def _opencensus_time_to_nanos(timestamp: str) -> int: class ShimSpan(wrapt.ObjectProxy): def __init__( self, - wrapped: BaseSpan, + wrapped: BlankSpan, *, otel_span: trace.Span, shim_tracer: "ShimTracer", @@ -158,6 +159,9 @@ def __exit__(self, exception_type, exception_value, traceback): ) # OpenCensus Span.__exit__() calls Tracer.end_span() # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/span.py#L390 - # but that would cause the OTel span to be ended twice. Instead just detach it from - # context directly. + # but that would cause the OTel span to be ended twice. Instead, this code just copies + # the context teardown from that method. context.detach(self._self_token) + execution_context.set_current_span( + self._self_shim_tracer.current_span() + ) diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_tracer.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_tracer.py index 0ce2d01120..a1e30afb50 100644 --- a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_tracer.py +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_tracer.py @@ -15,8 +15,11 @@ import logging import wrapt +from opencensus.trace import execution_context from opencensus.trace.blank_span import BlankSpan +from opencensus.trace.span_context import SpanContext from opencensus.trace.tracers.base import Tracer as BaseTracer +from opencensus.trace.tracestate import Tracestate from opentelemetry import context, trace from opentelemetry.shim.opencensus._shim_span import ShimSpan @@ -24,6 +27,7 @@ _logger = logging.getLogger(__name__) _SHIM_SPAN_KEY = context.create_key("opencensus-shim-span-key") +_SAMPLED = trace.TraceFlags(trace.TraceFlags.SAMPLED) def set_shim_span_in_context( @@ -36,12 +40,57 @@ def get_shim_span_in_context() -> ShimSpan: return context.get_value(_SHIM_SPAN_KEY) +def set_oc_span_in_context( + oc_span_context: SpanContext, ctx: context.Context +) -> context.Context: + """Returns a new OTel context based on ctx with oc_span_context set as the current span""" + + # If no SpanContext is passed to the opencensus.trace.tracer.Tracer, it creates a new one + # with a random trace ID and a None span ID to be the parent: + # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/tracer.py#L47. + # + # OpenTelemetry considers this an invalid SpanContext and will ignore it, so we can just + # return early + if oc_span_context.span_id is None: + return ctx + + trace_id = int(oc_span_context.trace_id, 16) + span_id = int(oc_span_context.span_id, 16) + is_remote = oc_span_context.from_header + trace_flags = ( + _SAMPLED if oc_span_context.trace_options.get_enabled() else None + ) + trace_state = ( + trace.TraceState(tuple(oc_span_context.tracestate.items())) + # OC SpanContext does not validate this type + if isinstance(oc_span_context.tracestate, Tracestate) + else None + ) + + return trace.set_span_in_context( + trace.NonRecordingSpan( + trace.SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=is_remote, + trace_flags=trace_flags, + trace_state=trace_state, + ) + ) + ) + + # pylint: disable=abstract-method class ShimTracer(wrapt.ObjectProxy): def __init__( - self, wrapped: BaseTracer, *, otel_tracer: trace.Tracer + self, + wrapped: BaseTracer, + *, + oc_span_context: SpanContext, + otel_tracer: trace.Tracer ) -> None: super().__init__(wrapped) + self._self_oc_span_context = oc_span_context self._self_otel_tracer = otel_tracer # For now, finish() is not implemented by the shim. It would require keeping a list of all @@ -53,7 +102,15 @@ def span(self, name="span"): return self.start_span(name=name) def start_span(self, name="span"): - span = self._self_otel_tracer.start_span(name) + parent_ctx = context.get_current() + # If there is no current span in context, use the one provided to the OC Tracer at + # creation time + if trace.get_current_span(parent_ctx) is trace.INVALID_SPAN: + parent_ctx = set_oc_span_in_context( + self._self_oc_span_context, parent_ctx + ) + + span = self._self_otel_tracer.start_span(name, context=parent_ctx) shim_span = ShimSpan( BlankSpan(name=name, context_tracer=self), otel_span=span, @@ -67,11 +124,14 @@ def start_span(self, name="span"): # equivalent to the below. This can cause context to leak but is equivalent. # pylint: disable=protected-access shim_span._self_token = context.attach(ctx) + # Also set it in OC's context, equivalent to + # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/tracers/context_tracer.py#L94 + execution_context.set_current_span(shim_span) return shim_span def end_span(self): - """Finishes the current span in the context and pops restores the context from before - the span was started. + """Finishes the current span in the context and restores the context from before the + span was started. """ span = self.current_span() if not span: @@ -79,8 +139,12 @@ def end_span(self): return span.finish() + # pylint: disable=protected-access context.detach(span._self_token) + # Also reset the OC execution_context, equivalent to + # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/tracers/context_tracer.py#L114-L117 + execution_context.set_current_span(self.current_span()) # pylint: disable=no-self-use def current_span(self): diff --git a/shim/opentelemetry-opencensus-shim/tests/test_shim.py b/shim/opentelemetry-opencensus-shim/tests/test_shim.py index df6abcfe1e..74a9eddcf2 100644 --- a/shim/opentelemetry-opencensus-shim/tests/test_shim.py +++ b/shim/opentelemetry-opencensus-shim/tests/test_shim.py @@ -16,15 +16,21 @@ import unittest from unittest.mock import patch +from opencensus.trace import trace_options, tracestate from opencensus.trace.blank_span import BlankSpan as OcBlankSpan from opencensus.trace.link import Link as OcLink from opencensus.trace.span import SpanKind +from opencensus.trace.span_context import SpanContext from opencensus.trace.tracer import Tracer as OcTracer from opencensus.trace.tracers.noop_tracer import NoopTracer as OcNoopTracer +from opentelemetry import context, trace from opentelemetry.shim.opencensus import install_shim, uninstall_shim from opentelemetry.shim.opencensus._shim_span import ShimSpan -from opentelemetry.shim.opencensus._shim_tracer import ShimTracer +from opentelemetry.shim.opencensus._shim_tracer import ( + ShimTracer, + set_oc_span_in_context, +) class TestShim(unittest.TestCase): @@ -126,3 +132,78 @@ def test_shim_span_contextmanager_calls_does_not_call_end(self): pass spy_otel_span.end.assert_not_called() + + def test_set_oc_span_in_context_no_span_id(self): + # This won't create a span ID and is the default behavior if you don't pass a context + # when creating the Tracer + ctx = set_oc_span_in_context(SpanContext(), context.get_current()) + self.assertIs(trace.get_current_span(ctx), trace.INVALID_SPAN) + + def test_set_oc_span_in_context_ids(self): + ctx = set_oc_span_in_context( + SpanContext( + trace_id="ace0216bab2b7ba249761dbb19c871b7", + span_id="1fead89ecf242225", + ), + context.get_current(), + ) + span_ctx = trace.get_current_span(ctx).get_span_context() + + self.assertEqual( + trace.format_trace_id(span_ctx.trace_id), + "ace0216bab2b7ba249761dbb19c871b7", + ) + self.assertEqual( + trace.format_span_id(span_ctx.span_id), "1fead89ecf242225" + ) + + def test_set_oc_span_in_context_remote(self): + for is_from_remote in True, False: + ctx = set_oc_span_in_context( + SpanContext( + trace_id="ace0216bab2b7ba249761dbb19c871b7", + span_id="1fead89ecf242225", + from_header=is_from_remote, + ), + context.get_current(), + ) + span_ctx = trace.get_current_span(ctx).get_span_context() + self.assertEqual(span_ctx.is_remote, is_from_remote) + + def test_set_oc_span_in_context_traceoptions(self): + for oc_trace_options, expect in [ + # Not sampled + ( + trace_options.TraceOptions("0"), + trace.TraceFlags(trace.TraceFlags.DEFAULT), + ), + # Sampled + ( + trace_options.TraceOptions("1"), + trace.TraceFlags(trace.TraceFlags.SAMPLED), + ), + ]: + ctx = set_oc_span_in_context( + SpanContext( + trace_id="ace0216bab2b7ba249761dbb19c871b7", + span_id="1fead89ecf242225", + trace_options=oc_trace_options, + ), + context.get_current(), + ) + span_ctx = trace.get_current_span(ctx).get_span_context() + self.assertEqual(span_ctx.trace_flags, expect) + + def test_set_oc_span_in_context_tracestate(self): + ctx = set_oc_span_in_context( + SpanContext( + trace_id="ace0216bab2b7ba249761dbb19c871b7", + span_id="1fead89ecf242225", + tracestate=tracestate.Tracestate({"hello": "tracestate"}), + ), + context.get_current(), + ) + span_ctx = trace.get_current_span(ctx).get_span_context() + self.assertEqual( + span_ctx.trace_state, trace.TraceState([("hello", "tracestate")]) + ) diff --git a/shim/opentelemetry-opencensus-shim/tests/test_shim_with_sdk.py b/shim/opentelemetry-opencensus-shim/tests/test_shim_with_sdk.py index 9bccc82ecb..db993d4c22 100644 --- a/shim/opentelemetry-opencensus-shim/tests/test_shim_with_sdk.py +++ b/shim/opentelemetry-opencensus-shim/tests/test_shim_with_sdk.py @@ -16,7 +16,8 @@ import unittest from datetime import datetime -from opencensus.trace import time_event +from opencensus.trace import execution_context, time_event +from opencensus.trace.span_context import SpanContext from opencensus.trace.status import Status as OcStatus from opencensus.trace.tracer import Tracer as OcTracer @@ -61,6 +62,18 @@ def test_start_span_interacts_with_context(self): oc_tracer.end_span() self.assertIs(trace.get_current_span(), trace.INVALID_SPAN) + def test_start_span_interacts_with_oc_context(self): + oc_tracer = OcTracer() + span = oc_tracer.start_span("foo") + + # Should have put the shim span in OC's implicit context under the hood. OpenCensus + # does not require another step to set the span in context. + self.assertIs(execution_context.get_current_span(), span) + + # This should end the span and remove it from context + oc_tracer.end_span() + self.assertIs(execution_context.get_current_span(), None) + def test_context_manager_interacts_with_context(self): oc_tracer = OcTracer() with oc_tracer.start_span("foo") as span: @@ -75,6 +88,15 @@ def test_context_manager_interacts_with_context(self): # The span should now be popped from context self.assertIs(trace.get_current_span(), trace.INVALID_SPAN) + def test_context_manager_interacts_with_oc_context(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("foo") as span: + # Should have placed the shim span in implicit context under the hood + self.assertIs(execution_context.get_current_span(), span) + + # The span should now be popped from context + self.assertIs(execution_context.get_current_span(), None) + def test_exports_a_span(self): oc_tracer = OcTracer() with oc_tracer.start_span("span1"): @@ -82,6 +104,52 @@ def test_exports_a_span(self): self.assertEqual(len(self.mem_exporter.get_finished_spans()), 1) + def test_uses_tracers_span_context_when_no_parent_in_context(self): + # the SpanContext passed to the Tracer will become the parent when there is no span + # already set in the OTel context + oc_tracer = OcTracer( + span_context=SpanContext( + trace_id="ace0216bab2b7ba249761dbb19c871b7", + span_id="1fead89ecf242225", + ) + ) + + with oc_tracer.start_span("span1"): + pass + + exported_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] + parent = exported_span.parent + self.assertIsNotNone(parent) + self.assertEqual( + trace.format_trace_id(parent.trace_id), + "ace0216bab2b7ba249761dbb19c871b7", + ) + self.assertEqual( + trace.format_span_id(parent.span_id), "1fead89ecf242225" + ) + + def test_ignores_tracers_span_context_when_parent_already_in_context(self): + # the SpanContext passed to the Tracer will be ignored since there is already a span + # set in the OTel context + oc_tracer = OcTracer( + span_context=SpanContext( + trace_id="ace0216bab2b7ba249761dbb19c871b7", + span_id="1fead89ecf242225", + ) + ) + otel_tracer = self.tracer_provider.get_tracer(__name__) + + with otel_tracer.start_as_current_span("some_parent"): + with oc_tracer.start_span("span1"): + pass + + oc_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] + otel_parent: ReadableSpan = self.mem_exporter.get_finished_spans()[1] + self.assertEqual( + oc_span.parent, + otel_parent.context, + ) + def test_span_attributes(self): oc_tracer = OcTracer() with oc_tracer.start_span("span1") as span: From c1a8eb426652fc9131f60ce5b913961ccf523afa Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Tue, 4 Apr 2023 15:27:16 -0400 Subject: [PATCH 1418/1517] OpenCensus shim small fix for example code (#3248) --- docs/examples/opencensus-shim/README.rst | 3 ++- docs/examples/opencensus-shim/requirements.txt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/examples/opencensus-shim/README.rst b/docs/examples/opencensus-shim/README.rst index 4fdccba615..9c24440172 100644 --- a/docs/examples/opencensus-shim/README.rst +++ b/docs/examples/opencensus-shim/README.rst @@ -47,7 +47,8 @@ Alternatively, you can install the Python dependencies separately: opentelemetry-opencensus-shim \ opentelemetry-instrumentation-sqlite3 \ opencensus \ - opencensus-ext-flask + opencensus-ext-flask \ + Flask Run the Application diff --git a/docs/examples/opencensus-shim/requirements.txt b/docs/examples/opencensus-shim/requirements.txt index 4150eec3c4..da9f0f3f96 100644 --- a/docs/examples/opencensus-shim/requirements.txt +++ b/docs/examples/opencensus-shim/requirements.txt @@ -5,3 +5,4 @@ opentelemetry-opencensus-shim opentelemetry-instrumentation-sqlite3 opencensus opencensus-ext-flask +Flask From c46ad86bd6c6ecbc38761687896fb15f0a43f61d Mon Sep 17 00:00:00 2001 From: jack-burridge-cfh <80791998+jack-burridge-cfh@users.noreply.github.com> Date: Wed, 5 Apr 2023 18:30:55 +0100 Subject: [PATCH 1419/1517] Separate protobuf encoding into a separate package (#3169) --- CHANGELOG.md | 2 + .../LICENSE | 201 +++++ .../README.rst | 27 + .../pyproject.toml | 44 + .../exporter/otlp/proto/common/__init__.py | 18 + .../otlp/proto/common/_internal/__init__.py | 132 +++ .../_internal/_log_encoder}/__init__.py | 64 +- .../_internal/metrics_encoder/__init__.py | 199 +++++ .../_internal/trace_encoder/__init__.py | 181 ++++ .../otlp/proto/common/_log_encoder.py | 20 + .../otlp/proto/common/metrics_encoder.py | 20 + .../exporter/otlp/proto/common/py.typed | 0 .../otlp/proto/common/trace_encoder.py | 20 + .../exporter/otlp/proto/common/version.py | 15 + .../tests/test_log_encoder.py | 253 ++++++ .../tests/test_metrics_encoder.py | 813 ++++++++++++++++++ .../tests/test_trace_encoder.py} | 25 +- .../pyproject.toml | 2 + .../otlp/proto/grpc/_log_exporter/__init__.py | 93 +- .../exporter/otlp/proto/grpc/exporter.py | 41 +- .../proto/grpc/metric_exporter/__init__.py | 179 +--- .../proto/grpc/trace_exporter/__init__.py | 178 +--- .../tests/test_otlp_metrics_exporter.py | 775 +---------------- .../pyproject.toml | 2 + .../otlp/proto/http/_log_exporter/__init__.py | 6 +- .../proto/http/metric_exporter/__init__.py | 238 +---- .../proto/http/trace_exporter/__init__.py | 8 +- .../http/trace_exporter/encoder/__init__.py | 296 +------ .../metrics/test_otlp_metrics_exporter.py | 5 +- .../tests/test_proto_log_exporter.py | 169 +--- .../src/opentelemetry/test/metrictestutil.py | 6 +- tox.ini | 11 + 32 files changed, 2119 insertions(+), 1924 deletions(-) create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/LICENSE create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/README.rst create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py rename exporter/{opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder => opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder}/__init__.py (58%) create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/trace_encoder/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_log_encoder.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/metrics_encoder.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/py.typed create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/trace_encoder.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/tests/test_metrics_encoder.py rename exporter/{opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py => opentelemetry-exporter-otlp-proto-common/tests/test_trace_encoder.py} (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77e5579688..3923d7eda5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Move Protobuf encoding to its own package + ([#3169](https://github.com/open-telemetry/opentelemetry-python/pull/3169)) - Add experimental feature to detect resource detectors in auto instrumentation ([#3181](https://github.com/open-telemetry/opentelemetry-python/pull/3181)) - Fix exporting of ExponentialBucketHistogramAggregation from opentelemetry.sdk.metrics.view diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/LICENSE b/exporter/opentelemetry-exporter-otlp-proto-common/LICENSE new file mode 100644 index 0000000000..1ef7dad2c5 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/LICENSE @@ -0,0 +1,201 @@ + 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 The OpenTelemetry Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/README.rst b/exporter/opentelemetry-exporter-otlp-proto-common/README.rst new file mode 100644 index 0000000000..9756a49bc3 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/README.rst @@ -0,0 +1,27 @@ +OpenTelemetry Protobuf Encoding +=============================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp-proto-common.svg + :target: https://pypi.org/project/opentelemetry-exporter-otlp-proto-common/ + +This library is provided as a convenience to encode to Protobuf. Currently used by: + +* opentelemetry-exporter-otlp-proto-grpc +* opentelemetry-exporter-otlp-proto-http + + +Installation +------------ + +:: + + pip install opentelemetry-exporter-otlp-proto-common + + +References +---------- + +* `OpenTelemetry `_ +* `OpenTelemetry Protocol Specification `_ diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml new file mode 100644 index 0000000000..5f872cd086 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-otlp-proto-common" +dynamic = ["version"] +description = "OpenTelemetry Protobuf encoding" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ + "opentelemetry-proto == 1.18.0.dev", +] + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-common" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/otlp/proto/common/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/__init__.py new file mode 100644 index 0000000000..2d336aee83 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/__init__.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from opentelemetry.exporter.otlp.proto.common.version import __version__ + +__all__ = ["__version__"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py new file mode 100644 index 0000000000..2f5d741324 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py @@ -0,0 +1,132 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import logging +from collections.abc import Sequence +from typing import Any, Mapping, Optional, List, Callable, TypeVar, Dict + +from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.proto.common.v1.common_pb2 import ( + InstrumentationScope as PB2InstrumentationScope, +) +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as PB2Resource, +) +from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue +from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue +from opentelemetry.proto.common.v1.common_pb2 import ( + KeyValueList as PB2KeyValueList, +) +from opentelemetry.proto.common.v1.common_pb2 import ( + ArrayValue as PB2ArrayValue, +) +from opentelemetry.sdk.trace import Resource +from opentelemetry.util.types import Attributes + + +_logger = logging.getLogger(__name__) + +_TypingResourceT = TypeVar("_TypingResourceT") +_ResourceDataT = TypeVar("_ResourceDataT") + + +def _encode_instrumentation_scope( + instrumentation_scope: InstrumentationScope, +) -> PB2InstrumentationScope: + if instrumentation_scope is None: + return PB2InstrumentationScope() + return PB2InstrumentationScope( + name=instrumentation_scope.name, + version=instrumentation_scope.version, + ) + + +def _encode_resource(resource: Resource) -> PB2Resource: + return PB2Resource(attributes=_encode_attributes(resource.attributes)) + + +def _encode_value(value: Any) -> PB2AnyValue: + if isinstance(value, bool): + return PB2AnyValue(bool_value=value) + if isinstance(value, str): + return PB2AnyValue(string_value=value) + if isinstance(value, int): + return PB2AnyValue(int_value=value) + if isinstance(value, float): + return PB2AnyValue(double_value=value) + if isinstance(value, Sequence): + return PB2AnyValue( + array_value=PB2ArrayValue(values=[_encode_value(v) for v in value]) + ) + elif isinstance(value, Mapping): + return PB2AnyValue( + kvlist_value=PB2KeyValueList( + values=[_encode_key_value(str(k), v) for k, v in value.items()] + ) + ) + raise Exception(f"Invalid type {type(value)} of value {value}") + + +def _encode_key_value(key: str, value: Any) -> PB2KeyValue: + return PB2KeyValue(key=key, value=_encode_value(value)) + + +def _encode_span_id(span_id: int) -> bytes: + return span_id.to_bytes(length=8, byteorder="big", signed=False) + + +def _encode_trace_id(trace_id: int) -> bytes: + return trace_id.to_bytes(length=16, byteorder="big", signed=False) + + +def _encode_attributes( + attributes: Attributes, +) -> Optional[List[PB2KeyValue]]: + if attributes: + pb2_attributes = [] + for key, value in attributes.items(): + try: + pb2_attributes.append(_encode_key_value(key, value)) + except Exception as error: # pylint: disable=broad-except + _logger.exception(error) + else: + pb2_attributes = None + return pb2_attributes + + +def _get_resource_data( + sdk_resource_scope_data: Dict[Resource, _ResourceDataT], + resource_class: Callable[..., _TypingResourceT], + name: str, +) -> List[_TypingResourceT]: + + resource_data = [] + + for ( + sdk_resource, + scope_data, + ) in sdk_resource_scope_data.items(): + collector_resource = PB2Resource( + attributes=_encode_attributes(sdk_resource.attributes) + ) + resource_data.append( + resource_class( + **{ + "resource": collector_resource, + "scope_{}".format(name): scope_data.values(), + } + ) + ) + return resource_data diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py similarity index 58% rename from exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py rename to exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py index 866f8878bb..7c135d90ba 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py @@ -11,9 +11,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from collections import defaultdict from typing import Sequence, List +from opentelemetry.exporter.otlp.proto.common._internal import ( + _encode_instrumentation_scope, + _encode_resource, + _encode_span_id, + _encode_trace_id, + _encode_value, + _encode_attributes, +) from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, ) @@ -22,62 +30,36 @@ ResourceLogs, ) from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord -from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import ( - _encode_instrumentation_scope, - _encode_resource, - _encode_span_id, - _encode_trace_id, - _encode_value, - _encode_attributes, -) - from opentelemetry.sdk._logs import LogData -class _ProtobufEncoder: - @classmethod - def serialize(cls, batch: Sequence[LogData]) -> str: - return cls.encode(batch).SerializeToString() - - @staticmethod - def encode(batch: Sequence[LogData]) -> ExportLogsServiceRequest: - return ExportLogsServiceRequest( - resource_logs=_encode_resource_logs(batch) - ) +def encode_logs(batch: Sequence[LogData]) -> ExportLogsServiceRequest: + return ExportLogsServiceRequest(resource_logs=_encode_resource_logs(batch)) def _encode_log(log_data: LogData) -> PB2LogRecord: - kwargs = {} - kwargs["time_unix_nano"] = log_data.log_record.timestamp - kwargs["span_id"] = _encode_span_id(log_data.log_record.span_id) - kwargs["trace_id"] = _encode_trace_id(log_data.log_record.trace_id) - kwargs["flags"] = int(log_data.log_record.trace_flags) - kwargs["body"] = _encode_value(log_data.log_record.body) - kwargs["severity_text"] = log_data.log_record.severity_text - kwargs["attributes"] = _encode_attributes(log_data.log_record.attributes) - kwargs["severity_number"] = log_data.log_record.severity_number.value - - return PB2LogRecord(**kwargs) + return PB2LogRecord( + time_unix_nano=log_data.log_record.timestamp, + span_id=_encode_span_id(log_data.log_record.span_id), + trace_id=_encode_trace_id(log_data.log_record.trace_id), + flags=int(log_data.log_record.trace_flags), + body=_encode_value(log_data.log_record.body), + severity_text=log_data.log_record.severity_text, + attributes=_encode_attributes(log_data.log_record.attributes), + severity_number=log_data.log_record.severity_number.value, + ) def _encode_resource_logs(batch: Sequence[LogData]) -> List[ResourceLogs]: - - sdk_resource_logs = {} + sdk_resource_logs = defaultdict(lambda: defaultdict(list)) for sdk_log in batch: sdk_resource = sdk_log.log_record.resource sdk_instrumentation = sdk_log.instrumentation_scope or None pb2_log = _encode_log(sdk_log) - if sdk_resource not in sdk_resource_logs.keys(): - sdk_resource_logs[sdk_resource] = {sdk_instrumentation: [pb2_log]} - elif sdk_instrumentation not in sdk_resource_logs[sdk_resource].keys(): - sdk_resource_logs[sdk_resource][sdk_instrumentation] = [pb2_log] - else: - sdk_resource_logs[sdk_resource][sdk_instrumentation].append( - pb2_log - ) + sdk_resource_logs[sdk_resource][sdk_instrumentation].append(pb2_log) pb2_resource_logs = [] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder/__init__.py new file mode 100644 index 0000000000..d269375930 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder/__init__.py @@ -0,0 +1,199 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from opentelemetry.exporter.otlp.proto.common._internal import ( + _encode_attributes, +) +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope +from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 +from opentelemetry.sdk.metrics.export import ( + MetricsData, + Gauge, + Histogram, + Sum, + ExponentialHistogram as ExponentialHistogramType, +) +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as PB2Resource, +) + +_logger = logging.getLogger(__name__) + + +def encode_metrics(data: MetricsData) -> ExportMetricsServiceRequest: + resource_metrics_dict = {} + + for resource_metrics in data.resource_metrics: + + resource = resource_metrics.resource + + # It is safe to assume that each entry in data.resource_metrics is + # associated with an unique resource. + scope_metrics_dict = {} + + resource_metrics_dict[resource] = scope_metrics_dict + + for scope_metrics in resource_metrics.scope_metrics: + + instrumentation_scope = scope_metrics.scope + + # The SDK groups metrics in instrumentation scopes already so + # there is no need to check for existing instrumentation scopes + # here. + pb2_scope_metrics = pb2.ScopeMetrics( + scope=InstrumentationScope( + name=instrumentation_scope.name, + version=instrumentation_scope.version, + ) + ) + + scope_metrics_dict[instrumentation_scope] = pb2_scope_metrics + + for metric in scope_metrics.metrics: + pb2_metric = pb2.Metric( + name=metric.name, + description=metric.description, + unit=metric.unit, + ) + + if isinstance(metric.data, Gauge): + for data_point in metric.data.data_points: + pt = pb2.NumberDataPoint( + attributes=_encode_attributes( + data_point.attributes + ), + time_unix_nano=data_point.time_unix_nano, + ) + if isinstance(data_point.value, int): + pt.as_int = data_point.value + else: + pt.as_double = data_point.value + pb2_metric.gauge.data_points.append(pt) + + elif isinstance(metric.data, Histogram): + for data_point in metric.data.data_points: + pt = pb2.HistogramDataPoint( + attributes=_encode_attributes( + data_point.attributes + ), + time_unix_nano=data_point.time_unix_nano, + start_time_unix_nano=( + data_point.start_time_unix_nano + ), + count=data_point.count, + sum=data_point.sum, + bucket_counts=data_point.bucket_counts, + explicit_bounds=data_point.explicit_bounds, + max=data_point.max, + min=data_point.min, + ) + pb2_metric.histogram.aggregation_temporality = ( + metric.data.aggregation_temporality + ) + pb2_metric.histogram.data_points.append(pt) + + elif isinstance(metric.data, Sum): + for data_point in metric.data.data_points: + pt = pb2.NumberDataPoint( + attributes=_encode_attributes( + data_point.attributes + ), + start_time_unix_nano=( + data_point.start_time_unix_nano + ), + time_unix_nano=data_point.time_unix_nano, + ) + if isinstance(data_point.value, int): + pt.as_int = data_point.value + else: + pt.as_double = data_point.value + # note that because sum is a message type, the + # fields must be set individually rather than + # instantiating a pb2.Sum and setting it once + pb2_metric.sum.aggregation_temporality = ( + metric.data.aggregation_temporality + ) + pb2_metric.sum.is_monotonic = metric.data.is_monotonic + pb2_metric.sum.data_points.append(pt) + + elif isinstance(metric.data, ExponentialHistogramType): + for data_point in metric.data.data_points: + + if data_point.positive.bucket_counts: + positive = pb2.ExponentialHistogramDataPoint.Buckets( + offset=data_point.positive.offset, + bucket_counts=data_point.positive.bucket_counts, + ) + else: + positive = None + + if data_point.negative.bucket_counts: + negative = pb2.ExponentialHistogramDataPoint.Buckets( + offset=data_point.negative.offset, + bucket_counts=data_point.negative.bucket_counts, + ) + else: + negative = None + + pt = pb2.ExponentialHistogramDataPoint( + attributes=_encode_attributes( + data_point.attributes + ), + time_unix_nano=data_point.time_unix_nano, + start_time_unix_nano=( + data_point.start_time_unix_nano + ), + count=data_point.count, + sum=data_point.sum, + scale=data_point.scale, + zero_count=data_point.zero_count, + positive=positive, + negative=negative, + flags=data_point.flags, + max=data_point.max, + min=data_point.min, + ) + pb2_metric.exponential_histogram.aggregation_temporality = ( + metric.data.aggregation_temporality + ) + pb2_metric.exponential_histogram.data_points.append(pt) + + else: + _logger.warning( + "unsupported data type %s", + metric.data.__class__.__name__, + ) + continue + + pb2_scope_metrics.metrics.append(pb2_metric) + + resource_data = [] + for ( + sdk_resource, + scope_data, + ) in resource_metrics_dict.items(): + resource_data.append( + pb2.ResourceMetrics( + resource=PB2Resource( + attributes=_encode_attributes(sdk_resource.attributes) + ), + scope_metrics=scope_data.values(), + ) + ) + resource_metrics = resource_data + return ExportMetricsServiceRequest(resource_metrics=resource_metrics) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/trace_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/trace_encoder/__init__.py new file mode 100644 index 0000000000..46cf628dd1 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/trace_encoder/__init__.py @@ -0,0 +1,181 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from collections import defaultdict +from typing import List, Optional, Sequence + +from opentelemetry.exporter.otlp.proto.common._internal import ( + _encode_trace_id, + _encode_span_id, + _encode_instrumentation_scope, + _encode_attributes, + _encode_resource, +) +from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( + ExportTraceServiceRequest as PB2ExportTraceServiceRequest, +) +from opentelemetry.proto.trace.v1.trace_pb2 import ( + ScopeSpans as PB2ScopeSpans, +) +from opentelemetry.proto.trace.v1.trace_pb2 import ( + ResourceSpans as PB2ResourceSpans, +) +from opentelemetry.proto.trace.v1.trace_pb2 import Span as PB2SPan +from opentelemetry.proto.trace.v1.trace_pb2 import Status as PB2Status +from opentelemetry.sdk.trace import Event, ReadableSpan +from opentelemetry.trace import Link +from opentelemetry.trace import SpanKind +from opentelemetry.trace.span import SpanContext, TraceState, Status + +# pylint: disable=E1101 +_SPAN_KIND_MAP = { + SpanKind.INTERNAL: PB2SPan.SpanKind.SPAN_KIND_INTERNAL, + SpanKind.SERVER: PB2SPan.SpanKind.SPAN_KIND_SERVER, + SpanKind.CLIENT: PB2SPan.SpanKind.SPAN_KIND_CLIENT, + SpanKind.PRODUCER: PB2SPan.SpanKind.SPAN_KIND_PRODUCER, + SpanKind.CONSUMER: PB2SPan.SpanKind.SPAN_KIND_CONSUMER, +} + +_logger = logging.getLogger(__name__) + + +def encode_spans( + sdk_spans: Sequence[ReadableSpan], +) -> PB2ExportTraceServiceRequest: + return PB2ExportTraceServiceRequest( + resource_spans=_encode_resource_spans(sdk_spans) + ) + + +def _encode_resource_spans( + sdk_spans: Sequence[ReadableSpan], +) -> List[PB2ResourceSpans]: + # We need to inspect the spans and group + structure them as: + # + # Resource + # Instrumentation Library + # Spans + # + # First loop organizes the SDK spans in this structure. Protobuf messages + # are not hashable so we stick with SDK data in this phase. + # + # Second loop encodes the data into Protobuf format. + # + sdk_resource_spans = defaultdict(lambda: defaultdict(list)) + + for sdk_span in sdk_spans: + sdk_resource = sdk_span.resource + sdk_instrumentation = sdk_span.instrumentation_scope or None + pb2_span = _encode_span(sdk_span) + + sdk_resource_spans[sdk_resource][sdk_instrumentation].append(pb2_span) + + pb2_resource_spans = [] + + for sdk_resource, sdk_instrumentations in sdk_resource_spans.items(): + scope_spans = [] + for sdk_instrumentation, pb2_spans in sdk_instrumentations.items(): + scope_spans.append( + PB2ScopeSpans( + scope=(_encode_instrumentation_scope(sdk_instrumentation)), + spans=pb2_spans, + ) + ) + pb2_resource_spans.append( + PB2ResourceSpans( + resource=_encode_resource(sdk_resource), + scope_spans=scope_spans, + ) + ) + + return pb2_resource_spans + + +def _encode_span(sdk_span: ReadableSpan) -> PB2SPan: + span_context = sdk_span.get_span_context() + return PB2SPan( + trace_id=_encode_trace_id(span_context.trace_id), + span_id=_encode_span_id(span_context.span_id), + trace_state=_encode_trace_state(span_context.trace_state), + parent_span_id=_encode_parent_id(sdk_span.parent), + name=sdk_span.name, + kind=_SPAN_KIND_MAP[sdk_span.kind], + start_time_unix_nano=sdk_span.start_time, + end_time_unix_nano=sdk_span.end_time, + attributes=_encode_attributes(sdk_span.attributes), + events=_encode_events(sdk_span.events), + links=_encode_links(sdk_span.links), + status=_encode_status(sdk_span.status), + dropped_attributes_count=sdk_span.dropped_attributes, + dropped_events_count=sdk_span.dropped_events, + dropped_links_count=sdk_span.dropped_links, + ) + + +def _encode_events( + events: Sequence[Event], +) -> Optional[List[PB2SPan.Event]]: + pb2_events = None + if events: + pb2_events = [] + for event in events: + encoded_event = PB2SPan.Event( + name=event.name, + time_unix_nano=event.timestamp, + attributes=_encode_attributes(event.attributes), + dropped_attributes_count=event.attributes.dropped, + ) + pb2_events.append(encoded_event) + return pb2_events + + +def _encode_links(links: Sequence[Link]) -> Sequence[PB2SPan.Link]: + pb2_links = None + if links: + pb2_links = [] + for link in links: + encoded_link = PB2SPan.Link( + trace_id=_encode_trace_id(link.context.trace_id), + span_id=_encode_span_id(link.context.span_id), + attributes=_encode_attributes(link.attributes), + dropped_attributes_count=link.attributes.dropped, + ) + pb2_links.append(encoded_link) + return pb2_links + + +def _encode_status(status: Status) -> Optional[PB2Status]: + pb2_status = None + if status is not None: + pb2_status = PB2Status( + code=status.status_code.value, + message=status.description, + ) + return pb2_status + + +def _encode_trace_state(trace_state: TraceState) -> Optional[str]: + pb2_trace_state = None + if trace_state is not None: + pb2_trace_state = ",".join( + [f"{key}={value}" for key, value in (trace_state.items())] + ) + return pb2_trace_state + + +def _encode_parent_id(context: Optional[SpanContext]) -> Optional[bytes]: + if context: + return _encode_span_id(context.span_id) + return None diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_log_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_log_encoder.py new file mode 100644 index 0000000000..f34ff8223c --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_log_encoder.py @@ -0,0 +1,20 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from opentelemetry.exporter.otlp.proto.common._internal._log_encoder import ( + encode_logs, +) + +__all__ = ["encode_logs"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/metrics_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/metrics_encoder.py new file mode 100644 index 0000000000..14f8fc3f0d --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/metrics_encoder.py @@ -0,0 +1,20 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( + encode_metrics, +) + +__all__ = ["encode_metrics"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/py.typed b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/trace_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/trace_encoder.py new file mode 100644 index 0000000000..2af5765200 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/trace_encoder.py @@ -0,0 +1,20 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from opentelemetry.exporter.otlp.proto.common._internal.trace_encoder import ( + encode_spans, +) + +__all__ = ["encode_spans"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py new file mode 100644 index 0000000000..80d12781d3 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "1.18.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py new file mode 100644 index 0000000000..1cd86b2833 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py @@ -0,0 +1,253 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from typing import List, Tuple + +from opentelemetry._logs import SeverityNumber +from opentelemetry.exporter.otlp.proto.common._internal import ( + _encode_attributes, + _encode_span_id, + _encode_trace_id, + _encode_value, +) +from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue +from opentelemetry.proto.common.v1.common_pb2 import ( + InstrumentationScope as PB2InstrumentationScope, +) +from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue +from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord +from opentelemetry.proto.logs.v1.logs_pb2 import ( + ResourceLogs as PB2ResourceLogs, +) +from opentelemetry.proto.logs.v1.logs_pb2 import ScopeLogs as PB2ScopeLogs +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as PB2Resource, +) +from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs import LogRecord as SDKLogRecord +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.trace import TraceFlags + + +class TestOTLPLogEncoder(unittest.TestCase): + def test_encode(self): + sdk_logs, expected_encoding = self.get_test_logs() + self.assertEqual(encode_logs(sdk_logs), expected_encoding) + + @staticmethod + def _get_sdk_log_data() -> List[LogData]: + log1 = LogData( + log_record=SDKLogRecord( + timestamp=1644650195189786880, + trace_id=89564621134313219400156819398935297684, + span_id=1312458408527513268, + trace_flags=TraceFlags(0x01), + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Do not go gentle into that good night. Rage, rage against the dying of the light", + resource=SDKResource({"first_resource": "value"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_scope=InstrumentationScope( + "first_name", "first_version" + ), + ) + + log2 = LogData( + log_record=SDKLogRecord( + timestamp=1644650249738562048, + trace_id=0, + span_id=0, + trace_flags=TraceFlags.DEFAULT, + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Cooper, this is no time for caution!", + resource=SDKResource({"second_resource": "CASE"}), + attributes={}, + ), + instrumentation_scope=InstrumentationScope( + "second_name", "second_version" + ), + ) + + log3 = LogData( + log_record=SDKLogRecord( + timestamp=1644650427658989056, + trace_id=271615924622795969659406376515024083555, + span_id=4242561578944770265, + trace_flags=TraceFlags(0x01), + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG, + body="To our galaxy", + resource=SDKResource({"second_resource": "CASE"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_scope=None, + ) + + log4 = LogData( + log_record=SDKLogRecord( + timestamp=1644650584292683008, + trace_id=212592107417388365804938480559624925555, + span_id=6077757853989569223, + trace_flags=TraceFlags(0x01), + severity_text="INFO", + severity_number=SeverityNumber.INFO, + body="Love is the one thing that transcends time and space", + resource=SDKResource({"first_resource": "value"}), + attributes={"filename": "model.py", "func_name": "run_method"}, + ), + instrumentation_scope=InstrumentationScope( + "another_name", "another_version" + ), + ) + + return [log1, log2, log3, log4] + + def get_test_logs( + self, + ) -> Tuple[List[SDKLogRecord], ExportLogsServiceRequest]: + sdk_logs = self._get_sdk_log_data() + + pb2_service_request = ExportLogsServiceRequest( + resource_logs=[ + PB2ResourceLogs( + resource=PB2Resource( + attributes=[ + PB2KeyValue( + key="first_resource", + value=PB2AnyValue(string_value="value"), + ) + ] + ), + scope_logs=[ + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="first_name", version="first_version" + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650195189786880, + trace_id=_encode_trace_id( + 89564621134313219400156819398935297684 + ), + span_id=_encode_span_id( + 1312458408527513268 + ), + flags=int(TraceFlags(0x01)), + severity_text="WARN", + severity_number=SeverityNumber.WARN.value, + body=_encode_value( + "Do not go gentle into that good night. Rage, rage against the dying of the light" + ), + attributes=_encode_attributes( + {"a": 1, "b": "c"} + ), + ) + ], + ), + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="another_name", + version="another_version", + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650584292683008, + trace_id=_encode_trace_id( + 212592107417388365804938480559624925555 + ), + span_id=_encode_span_id( + 6077757853989569223 + ), + flags=int(TraceFlags(0x01)), + severity_text="INFO", + severity_number=SeverityNumber.INFO.value, + body=_encode_value( + "Love is the one thing that transcends time and space" + ), + attributes=_encode_attributes( + { + "filename": "model.py", + "func_name": "run_method", + } + ), + ) + ], + ), + ], + ), + PB2ResourceLogs( + resource=PB2Resource( + attributes=[ + PB2KeyValue( + key="second_resource", + value=PB2AnyValue(string_value="CASE"), + ) + ] + ), + scope_logs=[ + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="second_name", + version="second_version", + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650249738562048, + trace_id=_encode_trace_id(0), + span_id=_encode_span_id(0), + flags=int(TraceFlags.DEFAULT), + severity_text="WARN", + severity_number=SeverityNumber.WARN.value, + body=_encode_value( + "Cooper, this is no time for caution!" + ), + attributes={}, + ), + ], + ), + PB2ScopeLogs( + scope=PB2InstrumentationScope(), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650427658989056, + trace_id=_encode_trace_id( + 271615924622795969659406376515024083555 + ), + span_id=_encode_span_id( + 4242561578944770265 + ), + flags=int(TraceFlags(0x01)), + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG.value, + body=_encode_value("To our galaxy"), + attributes=_encode_attributes( + {"a": 1, "b": "c"} + ), + ), + ], + ), + ], + ), + ] + ) + + return sdk_logs, pb2_service_request diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_metrics_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_metrics_encoder.py new file mode 100644 index 0000000000..f7c8ceb820 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_metrics_encoder.py @@ -0,0 +1,813 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access +import unittest + +from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( + encode_metrics, +) +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue, + InstrumentationScope, + KeyValue, +) +from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as OTLPResource, +) +from opentelemetry.sdk.metrics.export import AggregationTemporality, Buckets +from opentelemetry.sdk.metrics.export import ( + ExponentialHistogram as ExponentialHistogramType, +) +from opentelemetry.sdk.metrics.export import ExponentialHistogramDataPoint +from opentelemetry.sdk.metrics.export import Histogram as HistogramType +from opentelemetry.sdk.metrics.export import ( + HistogramDataPoint, + Metric, + MetricsData, + ResourceMetrics, + ScopeMetrics, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationScope as SDKInstrumentationScope, +) +from opentelemetry.test.metrictestutil import _generate_gauge, _generate_sum + + +class TestOTLPMetricsEncoder(unittest.TestCase): + histogram = Metric( + name="histogram", + description="foo", + unit="s", + data=HistogramType( + data_points=[ + HistogramDataPoint( + attributes={"a": 1, "b": True}, + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + min=8, + max=18, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + + def test_encode_sum_int(self): + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[_generate_sum("sum_int", 33)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="sum_int", + unit="s", + description="foo", + sum=pb2.Sum( + data_points=[ + pb2.NumberDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + as_int=33, + ) + ], + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + ) + ], + ) + ], + ) + ] + ) + actual = encode_metrics(metrics_data) + self.assertEqual(expected, actual) + + def test_encode_sum_double(self): + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[_generate_sum("sum_double", 2.98)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="sum_double", + unit="s", + description="foo", + sum=pb2.Sum( + data_points=[ + pb2.NumberDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + as_double=2.98, + ) + ], + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + ) + ], + ) + ], + ) + ] + ) + actual = encode_metrics(metrics_data) + self.assertEqual(expected, actual) + + def test_encode_gauge_int(self): + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[_generate_gauge("gauge_int", 9000)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="gauge_int", + unit="s", + description="foo", + gauge=pb2.Gauge( + data_points=[ + pb2.NumberDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + time_unix_nano=1641946016139533244, + as_int=9000, + ) + ], + ), + ) + ], + ) + ], + ) + ] + ) + actual = encode_metrics(metrics_data) + self.assertEqual(expected, actual) + + def test_encode_gauge_double(self): + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[_generate_gauge("gauge_double", 52.028)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="gauge_double", + unit="s", + description="foo", + gauge=pb2.Gauge( + data_points=[ + pb2.NumberDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + time_unix_nano=1641946016139533244, + as_double=52.028, + ) + ], + ), + ) + ], + ) + ], + ) + ] + ) + actual = encode_metrics(metrics_data) + self.assertEqual(expected, actual) + + def test_encode_histogram(self): + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[self.histogram], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + ], + ) + ], + ) + ] + ) + actual = encode_metrics(metrics_data) + self.assertEqual(expected, actual) + + def test_encode_multiple_scope_histogram(self): + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[self.histogram, self.histogram], + schema_url="instrumentation_scope_schema_url", + ), + ScopeMetrics( + scope=SDKInstrumentationScope( + name="second_name", + version="second_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[self.histogram], + schema_url="instrumentation_scope_schema_url", + ), + ScopeMetrics( + scope=SDKInstrumentationScope( + name="third_name", + version="third_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[self.histogram], + schema_url="instrumentation_scope_schema_url", + ), + ], + schema_url="resource_schema_url", + ) + ] + ) + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ), + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ), + ], + ), + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="second_name", version="second_version" + ), + metrics=[ + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + ], + ), + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="third_name", version="third_version" + ), + metrics=[ + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + ], + ), + ], + ) + ] + ) + actual = encode_metrics(metrics_data) + self.assertEqual(expected, actual) + + def test_encode_exponential_histogram(self): + exponential_histogram = Metric( + name="exponential_histogram", + description="description", + unit="unit", + data=ExponentialHistogramType( + data_points=[ + ExponentialHistogramDataPoint( + attributes={"a": 1, "b": True}, + start_time_unix_nano=0, + time_unix_nano=1, + count=2, + sum=3, + scale=4, + zero_count=5, + positive=Buckets(offset=6, bucket_counts=[7, 8]), + negative=Buckets(offset=9, bucket_counts=[10, 11]), + flags=12, + min=13.0, + max=14.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[exponential_histogram], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="exponential_histogram", + unit="unit", + description="description", + exponential_histogram=pb2.ExponentialHistogram( + data_points=[ + pb2.ExponentialHistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=0, + time_unix_nano=1, + count=2, + sum=3, + scale=4, + zero_count=5, + positive=pb2.ExponentialHistogramDataPoint.Buckets( + offset=6, + bucket_counts=[7, 8], + ), + negative=pb2.ExponentialHistogramDataPoint.Buckets( + offset=9, + bucket_counts=[10, 11], + ), + flags=12, + exemplars=[], + min=13.0, + max=14.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + ], + ) + ], + ) + ] + ) + # pylint: disable=protected-access + actual = encode_metrics(metrics_data) + self.assertEqual(expected, actual) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_trace_encoder.py similarity index 96% rename from exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py rename to exporter/opentelemetry-exporter-otlp-proto-common/tests/test_trace_encoder.py index 7145ddbfa9..c0a05483f1 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_protobuf_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_trace_encoder.py @@ -17,13 +17,15 @@ import unittest from typing import List, Tuple -from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import ( - _SPAN_KIND_MAP, +from opentelemetry.exporter.otlp.proto.common._internal import ( _encode_span_id, - _encode_status, _encode_trace_id, - _ProtobufEncoder, ) +from opentelemetry.exporter.otlp.proto.common._internal.trace_encoder import ( + _SPAN_KIND_MAP, + _encode_status, +) +from opentelemetry.exporter.otlp.proto.common.trace_encoder import encode_spans from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest as PB2ExportTraceServiceRequest, ) @@ -55,19 +57,10 @@ from opentelemetry.trace.status import StatusCode as SDKStatusCode -class TestProtobufEncoder(unittest.TestCase): - def test_encode(self): +class TestOTLPTraceEncoder(unittest.TestCase): + def test_encode_spans(self): otel_spans, expected_encoding = self.get_exhaustive_test_spans() - self.assertEqual( - _ProtobufEncoder().encode(otel_spans), expected_encoding - ) - - def test_serialize(self): - otel_spans, expected_encoding = self.get_exhaustive_test_spans() - self.assertEqual( - _ProtobufEncoder().serialize(otel_spans), - expected_encoding.SerializeToString(), - ) + self.assertEqual(encode_spans(otel_spans), expected_encoding) @staticmethod def get_exhaustive_otel_span_list() -> List[SDKSpan]: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index 0850727b02..82ec2b0a19 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ + "Deprecated >= 1.2.6", "backoff >= 1.10.0, < 2.0.0; python_version<'3.7'", "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", @@ -32,6 +33,7 @@ dependencies = [ "opentelemetry-api ~= 1.15", "opentelemetry-proto == 1.18.0.dev", "opentelemetry-sdk ~= 1.18.0.dev", + "opentelemetry-exporter-otlp-proto-common == 1.18.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index 887f9d8b2c..ef1b77de27 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -15,11 +15,11 @@ from typing import Dict, Optional, Tuple, Union, Sequence from typing import Sequence as TypingSequence from grpc import ChannelCredentials, Compression + +from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs from opentelemetry.exporter.otlp.proto.grpc.exporter import ( OTLPExporterMixin, - get_resource_data, _get_credentials, - _translate_value, environ_to_compression, ) from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( @@ -28,12 +28,6 @@ from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( LogsServiceStub, ) -from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope -from opentelemetry.proto.logs.v1.logs_pb2 import ( - ScopeLogs, - ResourceLogs, -) -from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord from opentelemetry.sdk._logs import LogRecord as SDKLogRecord from opentelemetry.sdk._logs import LogData from opentelemetry.sdk._logs.export import LogExporter, LogExportResult @@ -105,91 +99,10 @@ def __init__( } ) - def _translate_time(self, log_data: LogData) -> None: - self._collector_kwargs[ - "time_unix_nano" - ] = log_data.log_record.timestamp - - def _translate_span_id(self, log_data: LogData) -> None: - self._collector_kwargs[ - "span_id" - ] = log_data.log_record.span_id.to_bytes(8, "big") - - def _translate_trace_id(self, log_data: LogData) -> None: - self._collector_kwargs[ - "trace_id" - ] = log_data.log_record.trace_id.to_bytes(16, "big") - - def _translate_trace_flags(self, log_data: LogData) -> None: - self._collector_kwargs["flags"] = int(log_data.log_record.trace_flags) - - def _translate_body(self, log_data: LogData): - self._collector_kwargs["body"] = _translate_value( - log_data.log_record.body - ) - - def _translate_severity_text(self, log_data: LogData): - self._collector_kwargs[ - "severity_text" - ] = log_data.log_record.severity_text - def _translate_data( self, data: Sequence[LogData] ) -> ExportLogsServiceRequest: - # pylint: disable=attribute-defined-outside-init - - sdk_resource_scope_logs = {} - - for log_data in data: - resource = log_data.log_record.resource - - scope_logs_map = sdk_resource_scope_logs.get(resource, {}) - if not scope_logs_map: - sdk_resource_scope_logs[resource] = scope_logs_map - - scope_logs = scope_logs_map.get(log_data.instrumentation_scope) - if not scope_logs: - if log_data.instrumentation_scope is not None: - scope_logs_map[log_data.instrumentation_scope] = ScopeLogs( - scope=InstrumentationScope( - name=log_data.instrumentation_scope.name, - version=log_data.instrumentation_scope.version, - ) - ) - else: - scope_logs_map[ - log_data.instrumentation_scope - ] = ScopeLogs() - - scope_logs = scope_logs_map.get(log_data.instrumentation_scope) - - self._collector_kwargs = {} - - self._translate_time(log_data) - self._translate_span_id(log_data) - self._translate_trace_id(log_data) - self._translate_trace_flags(log_data) - self._translate_body(log_data) - self._translate_severity_text(log_data) - self._collector_kwargs["attributes"] = self._translate_attributes( - log_data.log_record.attributes - ) - - self._collector_kwargs[ - "severity_number" - ] = log_data.log_record.severity_number.value - - scope_logs.log_records.append( - PB2LogRecord(**self._collector_kwargs) - ) - - return ExportLogsServiceRequest( - resource_logs=get_resource_data( - sdk_resource_scope_logs, - ResourceLogs, - "logs", - ) - ) + return encode_logs(data) def export(self, batch: Sequence[LogData]) -> LogExportResult: return self._export(batch) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 496fe365f8..471d5fe301 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -25,6 +25,11 @@ from typing import TypeVar from urllib.parse import urlparse +from deprecated import deprecated + +from opentelemetry.exporter.otlp.proto.common._internal import ( + _get_resource_data, +) import backoff from google.rpc.error_details_pb2 import RetryInfo from grpc import ( @@ -45,7 +50,7 @@ ArrayValue, KeyValue, ) -from opentelemetry.proto.resource.v1.resource_pb2 import Resource +from opentelemetry.proto.resource.v1.resource_pb2 import Resource # noqa: F401 from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_COMPRESSION, @@ -130,40 +135,16 @@ def _translate_key_values(key: str, value: Any) -> KeyValue: return KeyValue(key=key, value=_translate_value(value)) +@deprecated( + version="1.18.0", + reason="Use one of the encoders from opentelemetry-exporter-otlp-proto-common instead", +) def get_resource_data( sdk_resource_scope_data: Dict[SDKResource, ResourceDataT], resource_class: Callable[..., TypingResourceT], name: str, ) -> List[TypingResourceT]: - resource_data = [] - - for ( - sdk_resource, - scope_data, - ) in sdk_resource_scope_data.items(): - - collector_resource = Resource() - - for key, value in sdk_resource.attributes.items(): - - try: - # pylint: disable=no-member - collector_resource.attributes.append( - _translate_key_values(key, value) - ) - except Exception as error: # pylint: disable=broad-except - logger.exception(error) - - resource_data.append( - resource_class( - **{ - "resource": collector_resource, - "scope_{}".format(name): scope_data.values(), - } - ) - ) - - return resource_data + return _get_resource_data(sdk_resource_scope_data, resource_class, name) def _load_credential_from_file(filepath) -> ChannelCredentials: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index 99325b64f9..c388a726b6 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -17,21 +17,29 @@ from typing import Dict, Iterable, List, Optional, Tuple, Union from typing import Sequence as TypingSequence from grpc import ChannelCredentials, Compression + +from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( + encode_metrics, +) from opentelemetry.sdk.metrics._internal.aggregation import Aggregation from opentelemetry.exporter.otlp.proto.grpc.exporter import ( OTLPExporterMixin, - get_resource_data, _get_credentials, environ_to_compression, ) +from opentelemetry.exporter.otlp.proto.grpc.exporter import ( # noqa: F401 + get_resource_data, +) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( MetricsServiceStub, ) -from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope -from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 +from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 + InstrumentationScope, +) +from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 # noqa: F401 from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, @@ -52,14 +60,16 @@ from opentelemetry.sdk.metrics.export import ( AggregationTemporality, DataPointT, - Gauge, - Histogram as HistogramType, Metric, MetricExporter, MetricExportResult, MetricsData, ResourceMetrics, ScopeMetrics, +) +from opentelemetry.sdk.metrics.export import ( # noqa: F401 + Gauge, + Histogram as HistogramType, Sum, ExponentialHistogram as ExponentialHistogramType, ) @@ -196,164 +206,7 @@ def __init__( def _translate_data( self, data: MetricsData ) -> ExportMetricsServiceRequest: - - resource_metrics_dict = {} - - for resource_metrics in data.resource_metrics: - - resource = resource_metrics.resource - - # It is safe to assume that each entry in data.resource_metrics is - # associated with an unique resource. - scope_metrics_dict = {} - - resource_metrics_dict[resource] = scope_metrics_dict - - for scope_metrics in resource_metrics.scope_metrics: - - instrumentation_scope = scope_metrics.scope - - # The SDK groups metrics in instrumentation scopes already so - # there is no need to check for existing instrumentation scopes - # here. - pb2_scope_metrics = pb2.ScopeMetrics( - scope=InstrumentationScope( - name=instrumentation_scope.name, - version=instrumentation_scope.version, - ) - ) - - scope_metrics_dict[instrumentation_scope] = pb2_scope_metrics - - for metric in scope_metrics.metrics: - pb2_metric = pb2.Metric( - name=metric.name, - description=metric.description, - unit=metric.unit, - ) - - if isinstance(metric.data, Gauge): - for data_point in metric.data.data_points: - pt = pb2.NumberDataPoint( - attributes=self._translate_attributes( - data_point.attributes - ), - time_unix_nano=data_point.time_unix_nano, - ) - if isinstance(data_point.value, int): - pt.as_int = data_point.value - else: - pt.as_double = data_point.value - pb2_metric.gauge.data_points.append(pt) - - elif isinstance(metric.data, HistogramType): - for data_point in metric.data.data_points: - pt = pb2.HistogramDataPoint( - attributes=self._translate_attributes( - data_point.attributes - ), - time_unix_nano=data_point.time_unix_nano, - start_time_unix_nano=( - data_point.start_time_unix_nano - ), - count=data_point.count, - sum=data_point.sum, - bucket_counts=data_point.bucket_counts, - explicit_bounds=data_point.explicit_bounds, - max=data_point.max, - min=data_point.min, - ) - pb2_metric.histogram.aggregation_temporality = ( - metric.data.aggregation_temporality - ) - pb2_metric.histogram.data_points.append(pt) - - elif isinstance(metric.data, Sum): - for data_point in metric.data.data_points: - pt = pb2.NumberDataPoint( - attributes=self._translate_attributes( - data_point.attributes - ), - start_time_unix_nano=( - data_point.start_time_unix_nano - ), - time_unix_nano=data_point.time_unix_nano, - ) - if isinstance(data_point.value, int): - pt.as_int = data_point.value - else: - pt.as_double = data_point.value - # note that because sum is a message type, the - # fields must be set individually rather than - # instantiating a pb2.Sum and setting it once - pb2_metric.sum.aggregation_temporality = ( - metric.data.aggregation_temporality - ) - pb2_metric.sum.is_monotonic = ( - metric.data.is_monotonic - ) - pb2_metric.sum.data_points.append(pt) - - elif isinstance(metric.data, ExponentialHistogramType): - for data_point in metric.data.data_points: - - if data_point.positive.bucket_counts: - positive = pb2.ExponentialHistogramDataPoint.Buckets( - offset=data_point.positive.offset, - bucket_counts=data_point.positive.bucket_counts, - ) - else: - positive = None - - if data_point.negative.bucket_counts: - negative = pb2.ExponentialHistogramDataPoint.Buckets( - offset=data_point.negative.offset, - bucket_counts=data_point.negative.bucket_counts, - ) - else: - negative = None - - pt = pb2.ExponentialHistogramDataPoint( - attributes=self._translate_attributes( - data_point.attributes - ), - time_unix_nano=data_point.time_unix_nano, - start_time_unix_nano=( - data_point.start_time_unix_nano - ), - count=data_point.count, - sum=data_point.sum, - scale=data_point.scale, - zero_count=data_point.zero_count, - positive=positive, - negative=negative, - flags=data_point.flags, - max=data_point.max, - min=data_point.min, - ) - pb2_metric.exponential_histogram.aggregation_temporality = ( - metric.data.aggregation_temporality - ) - pb2_metric.exponential_histogram.data_points.append( - pt - ) - - else: - _logger.warning( - "unsupported data type %s", - metric.data.__class__.__name__, - ) - continue - - pb2_scope_metrics.metrics.append(pb2_metric) - - return ExportMetricsServiceRequest( - resource_metrics=get_resource_data( - resource_metrics_dict, - pb2.ResourceMetrics, - "metrics", - ) - ) + return encode_metrics(data) def export( self, diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index cfabe3ffce..72bd036885 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -21,11 +21,15 @@ from grpc import ChannelCredentials, Compression +from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( + encode_spans, +) from opentelemetry.exporter.otlp.proto.grpc.exporter import ( OTLPExporterMixin, _get_credentials, - _translate_key_values, environ_to_compression, +) +from opentelemetry.exporter.otlp.proto.grpc.exporter import ( # noqa: F401 get_resource_data, ) from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( @@ -34,13 +38,15 @@ from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( TraceServiceStub, ) -from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope -from opentelemetry.proto.trace.v1.trace_pb2 import ( +from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 + InstrumentationScope, +) +from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 ScopeSpans, ResourceSpans, + Span as CollectorSpan, ) -from opentelemetry.proto.trace.v1.trace_pb2 import Span as CollectorSpan -from opentelemetry.proto.trace.v1.trace_pb2 import Status +from opentelemetry.proto.trace.v1.trace_pb2 import Status # noqa: F401 from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, @@ -126,170 +132,10 @@ def __init__( } ) - def _translate_name(self, sdk_span: ReadableSpan) -> None: - self._collector_kwargs["name"] = sdk_span.name - - def _translate_start_time(self, sdk_span: ReadableSpan) -> None: - self._collector_kwargs["start_time_unix_nano"] = sdk_span.start_time - - def _translate_end_time(self, sdk_span: ReadableSpan) -> None: - self._collector_kwargs["end_time_unix_nano"] = sdk_span.end_time - - def _translate_span_id(self, sdk_span: ReadableSpan) -> None: - self._collector_kwargs["span_id"] = sdk_span.context.span_id.to_bytes( - 8, "big" - ) - - def _translate_trace_id(self, sdk_span: ReadableSpan) -> None: - self._collector_kwargs[ - "trace_id" - ] = sdk_span.context.trace_id.to_bytes(16, "big") - - def _translate_parent(self, sdk_span: ReadableSpan) -> None: - if sdk_span.parent is not None: - self._collector_kwargs[ - "parent_span_id" - ] = sdk_span.parent.span_id.to_bytes(8, "big") - - def _translate_context_trace_state(self, sdk_span: ReadableSpan) -> None: - if sdk_span.context.trace_state is not None: - self._collector_kwargs["trace_state"] = ",".join( - [ - f"{key}={value}" - for key, value in (sdk_span.context.trace_state.items()) - ] - ) - - def _translate_events(self, sdk_span: ReadableSpan) -> None: - if sdk_span.events: - self._collector_kwargs["events"] = [] - - for sdk_span_event in sdk_span.events: - - collector_span_event = CollectorSpan.Event( - name=sdk_span_event.name, - time_unix_nano=sdk_span_event.timestamp, - dropped_attributes_count=sdk_span_event.attributes.dropped, - ) - - for key, value in sdk_span_event.attributes.items(): - try: - collector_span_event.attributes.append( - _translate_key_values(key, value) - ) - # pylint: disable=broad-except - except Exception as error: - logger.exception(error) - - self._collector_kwargs["events"].append(collector_span_event) - - def _translate_links(self, sdk_span: ReadableSpan) -> None: - if sdk_span.links: - self._collector_kwargs["links"] = [] - - for sdk_span_link in sdk_span.links: - - collector_span_link = CollectorSpan.Link( - trace_id=( - sdk_span_link.context.trace_id.to_bytes(16, "big") - ), - span_id=(sdk_span_link.context.span_id.to_bytes(8, "big")), - dropped_attributes_count=sdk_span_link.attributes.dropped, - ) - - for key, value in sdk_span_link.attributes.items(): - try: - collector_span_link.attributes.append( - _translate_key_values(key, value) - ) - # pylint: disable=broad-except - except Exception as error: - logger.exception(error) - - self._collector_kwargs["links"].append(collector_span_link) - - def _translate_status(self, sdk_span: ReadableSpan) -> None: - # pylint: disable=no-member - if sdk_span.status is not None: - self._collector_kwargs["status"] = Status( - code=sdk_span.status.status_code.value, - message=sdk_span.status.description, - ) - def _translate_data( self, data: Sequence[ReadableSpan] ) -> ExportTraceServiceRequest: - # pylint: disable=attribute-defined-outside-init - - sdk_resource_scope_spans = {} - - for sdk_span in data: - scope_spans_map = sdk_resource_scope_spans.get( - sdk_span.resource, {} - ) - # If we haven't seen the Resource yet, add it to the map - if not scope_spans_map: - sdk_resource_scope_spans[sdk_span.resource] = scope_spans_map - scope_spans = scope_spans_map.get(sdk_span.instrumentation_scope) - # If we haven't seen the InstrumentationScope for this Resource yet, add it to the map - if not scope_spans: - if sdk_span.instrumentation_scope is not None: - scope_spans_map[ - sdk_span.instrumentation_scope - ] = ScopeSpans( - scope=InstrumentationScope( - name=sdk_span.instrumentation_scope.name, - version=sdk_span.instrumentation_scope.version, - ) - ) - else: - # If no InstrumentationScope, store in None key - scope_spans_map[ - sdk_span.instrumentation_scope - ] = ScopeSpans() - scope_spans = scope_spans_map.get(sdk_span.instrumentation_scope) - self._collector_kwargs = {} - - self._translate_name(sdk_span) - self._translate_start_time(sdk_span) - self._translate_end_time(sdk_span) - self._translate_span_id(sdk_span) - self._translate_trace_id(sdk_span) - self._translate_parent(sdk_span) - self._translate_context_trace_state(sdk_span) - self._collector_kwargs["attributes"] = self._translate_attributes( - sdk_span.attributes - ) - self._translate_events(sdk_span) - self._translate_links(sdk_span) - self._translate_status(sdk_span) - if sdk_span.dropped_attributes: - self._collector_kwargs[ - "dropped_attributes_count" - ] = sdk_span.dropped_attributes - if sdk_span.dropped_events: - self._collector_kwargs[ - "dropped_events_count" - ] = sdk_span.dropped_events - if sdk_span.dropped_links: - self._collector_kwargs[ - "dropped_links_count" - ] = sdk_span.dropped_links - - self._collector_kwargs["kind"] = getattr( - CollectorSpan.SpanKind, - f"SPAN_KIND_{sdk_span.kind.name}", - ) - - scope_spans.spans.append(CollectorSpan(**self._collector_kwargs)) - - return ExportTraceServiceRequest( - resource_spans=get_resource_data( - sdk_resource_scope_spans, - ResourceSpans, - "spans", - ) - ) + return encode_spans(data) def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return self._export(spans) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py index 9cd805e969..6250080229 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py @@ -33,22 +33,13 @@ ) from opentelemetry.exporter.otlp.proto.grpc.version import __version__ from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( - ExportMetricsServiceRequest, ExportMetricsServiceResponse, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( MetricsServiceServicer, add_MetricsServiceServicer_to_server, ) -from opentelemetry.proto.common.v1.common_pb2 import ( - AnyValue, - InstrumentationScope, - KeyValue, -) -from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as OTLPResource, -) +from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, @@ -67,17 +58,9 @@ ObservableUpDownCounter, UpDownCounter, ) -from opentelemetry.sdk.metrics.export import AggregationTemporality, Buckets -from opentelemetry.sdk.metrics.export import ( - ExponentialHistogram as ExponentialHistogramType, -) from opentelemetry.sdk.metrics.export import ( - ExponentialHistogramDataPoint, + AggregationTemporality, Gauge, -) -from opentelemetry.sdk.metrics.export import Histogram as HistogramType -from opentelemetry.sdk.metrics.export import ( - HistogramDataPoint, Metric, MetricExportResult, MetricsData, @@ -89,7 +72,7 @@ from opentelemetry.sdk.util.instrumentation import ( InstrumentationScope as SDKInstrumentationScope, ) -from opentelemetry.test.metrictestutil import _generate_gauge, _generate_sum +from opentelemetry.test.metrictestutil import _generate_sum THIS_DIR = dirname(__file__) @@ -153,53 +136,6 @@ def setUp(self): self.server.start() - histogram = Metric( - name="histogram", - description="foo", - unit="s", - data=HistogramType( - data_points=[ - HistogramDataPoint( - attributes={"a": 1, "b": True}, - start_time_unix_nano=1641946016139533244, - time_unix_nano=1641946016139533244, - count=5, - sum=67, - bucket_counts=[1, 4], - explicit_bounds=[10.0, 20.0], - min=8, - max=18, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ) - - exponential_histogram = Metric( - name="exponential_histogram", - description="description", - unit="unit", - data=ExponentialHistogramType( - data_points=[ - ExponentialHistogramDataPoint( - attributes={"a": 1, "b": True}, - start_time_unix_nano=0, - time_unix_nano=1, - count=2, - sum=3, - scale=4, - zero_count=5, - positive=Buckets(offset=6, bucket_counts=[7, 8]), - negative=Buckets(offset=9, bucket_counts=[10, 11]), - flags=12, - min=13.0, - max=14.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ) - self.metrics = { "sum_int": MetricsData( resource_metrics=[ @@ -222,162 +158,9 @@ def setUp(self): schema_url="resource_schema_url", ) ] - ), - "sum_double": MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="insrumentation_scope_schema_url", - ), - metrics=[_generate_sum("sum_double", 2.98)], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ), - "gauge_int": MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="insrumentation_scope_schema_url", - ), - metrics=[_generate_gauge("gauge_int", 9000)], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ), - "gauge_double": MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="insrumentation_scope_schema_url", - ), - metrics=[ - _generate_gauge("gauge_double", 52.028) - ], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ), - "histogram": MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="insrumentation_scope_schema_url", - ), - metrics=[histogram], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ), - "exponential_histogram": MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="insrumentation_scope_schema_url", - ), - metrics=[exponential_histogram], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ), + ) } - self.multiple_scope_histogram = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="insrumentation_scope_schema_url", - ), - metrics=[histogram, histogram], - schema_url="instrumentation_scope_schema_url", - ), - ScopeMetrics( - scope=SDKInstrumentationScope( - name="second_name", - version="second_version", - schema_url="insrumentation_scope_schema_url", - ), - metrics=[histogram], - schema_url="instrumentation_scope_schema_url", - ), - ScopeMetrics( - scope=SDKInstrumentationScope( - name="third_name", - version="third_version", - schema_url="insrumentation_scope_schema_url", - ), - metrics=[histogram], - schema_url="instrumentation_scope_schema_url", - ), - ], - schema_url="resource_schema_url", - ) - ] - ) - def tearDown(self): self.server.stop(None) @@ -665,556 +448,6 @@ def test_failure(self): MetricExportResult.FAILURE, ) - def test_translate_sum_int(self): - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - metrics=[ - pb2.Metric( - name="sum_int", - unit="s", - description="foo", - sum=pb2.Sum( - data_points=[ - pb2.NumberDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946015139533244, - time_unix_nano=1641946016139533244, - as_int=33, - ) - ], - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, - ), - ) - ], - ) - ], - ) - ] - ) - # pylint: disable=protected-access - actual = self.exporter._translate_data(self.metrics["sum_int"]) - self.assertEqual(expected, actual) - - def test_translate_sum_double(self): - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - metrics=[ - pb2.Metric( - name="sum_double", - unit="s", - description="foo", - sum=pb2.Sum( - data_points=[ - pb2.NumberDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946015139533244, - time_unix_nano=1641946016139533244, - as_double=2.98, - ) - ], - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, - ), - ) - ], - ) - ], - ) - ] - ) - # pylint: disable=protected-access - actual = self.exporter._translate_data(self.metrics["sum_double"]) - self.assertEqual(expected, actual) - - def test_translate_gauge_int(self): - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - metrics=[ - pb2.Metric( - name="gauge_int", - unit="s", - description="foo", - gauge=pb2.Gauge( - data_points=[ - pb2.NumberDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - time_unix_nano=1641946016139533244, - as_int=9000, - ) - ], - ), - ) - ], - ) - ], - ) - ] - ) - # pylint: disable=protected-access - actual = self.exporter._translate_data(self.metrics["gauge_int"]) - self.assertEqual(expected, actual) - - def test_translate_gauge_double(self): - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - metrics=[ - pb2.Metric( - name="gauge_double", - unit="s", - description="foo", - gauge=pb2.Gauge( - data_points=[ - pb2.NumberDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - time_unix_nano=1641946016139533244, - as_double=52.028, - ) - ], - ), - ) - ], - ) - ], - ) - ] - ) - # pylint: disable=protected-access - actual = self.exporter._translate_data(self.metrics["gauge_double"]) - self.assertEqual(expected, actual) - - def test_translate_histogram(self): - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - metrics=[ - pb2.Metric( - name="histogram", - unit="s", - description="foo", - histogram=pb2.Histogram( - data_points=[ - pb2.HistogramDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946016139533244, - time_unix_nano=1641946016139533244, - count=5, - sum=67, - bucket_counts=[1, 4], - explicit_bounds=[10.0, 20.0], - exemplars=[], - flags=pb2.DataPointFlags.FLAG_NONE, - max=18.0, - min=8.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ) - ], - ) - ], - ) - ] - ) - # pylint: disable=protected-access - actual = self.exporter._translate_data(self.metrics["histogram"]) - self.assertEqual(expected, actual) - - def test_translate_exponential_histogram(self): - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - metrics=[ - pb2.Metric( - name="exponential_histogram", - unit="unit", - description="description", - exponential_histogram=pb2.ExponentialHistogram( - data_points=[ - pb2.ExponentialHistogramDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=0, - time_unix_nano=1, - count=2, - sum=3, - scale=4, - zero_count=5, - positive=pb2.ExponentialHistogramDataPoint.Buckets( - offset=6, - bucket_counts=[7, 8], - ), - negative=pb2.ExponentialHistogramDataPoint.Buckets( - offset=9, - bucket_counts=[10, 11], - ), - flags=12, - exemplars=[], - min=13.0, - max=14.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ) - ], - ) - ], - ) - ] - ) - # pylint: disable=protected-access - actual = self.exporter._translate_data( - self.metrics["exponential_histogram"] - ) - self.assertEqual(expected, actual) - - def test_translate_multiple_scope_histogram(self): - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - metrics=[ - pb2.Metric( - name="histogram", - unit="s", - description="foo", - histogram=pb2.Histogram( - data_points=[ - pb2.HistogramDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946016139533244, - time_unix_nano=1641946016139533244, - count=5, - sum=67, - bucket_counts=[1, 4], - explicit_bounds=[10.0, 20.0], - exemplars=[], - flags=pb2.DataPointFlags.FLAG_NONE, - max=18.0, - min=8.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ), - pb2.Metric( - name="histogram", - unit="s", - description="foo", - histogram=pb2.Histogram( - data_points=[ - pb2.HistogramDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946016139533244, - time_unix_nano=1641946016139533244, - count=5, - sum=67, - bucket_counts=[1, 4], - explicit_bounds=[10.0, 20.0], - exemplars=[], - flags=pb2.DataPointFlags.FLAG_NONE, - max=18.0, - min=8.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ), - ], - ), - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="second_name", version="second_version" - ), - metrics=[ - pb2.Metric( - name="histogram", - unit="s", - description="foo", - histogram=pb2.Histogram( - data_points=[ - pb2.HistogramDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946016139533244, - time_unix_nano=1641946016139533244, - count=5, - sum=67, - bucket_counts=[1, 4], - explicit_bounds=[10.0, 20.0], - exemplars=[], - flags=pb2.DataPointFlags.FLAG_NONE, - max=18.0, - min=8.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ) - ], - ), - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="third_name", version="third_version" - ), - metrics=[ - pb2.Metric( - name="histogram", - unit="s", - description="foo", - histogram=pb2.Histogram( - data_points=[ - pb2.HistogramDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946016139533244, - time_unix_nano=1641946016139533244, - count=5, - sum=67, - bucket_counts=[1, 4], - explicit_bounds=[10.0, 20.0], - exemplars=[], - flags=pb2.DataPointFlags.FLAG_NONE, - max=18.0, - min=8.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ) - ], - ), - ], - ) - ] - ) - # pylint: disable=protected-access - actual = self.exporter._translate_data(self.multiple_scope_histogram) - self.assertEqual(expected, actual) - def test_split_metrics_data_many_data_points(self): # GIVEN metrics_data = MetricsData( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index 55dcd3f551..54f2b67249 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -25,12 +25,14 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ + "Deprecated >= 1.2.6", "backoff >= 1.10.0, < 2.0.0; python_version<'3.7'", "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.15", "opentelemetry-proto == 1.18.0.dev", "opentelemetry-sdk ~= 1.18.0.dev", + "opentelemetry-exporter-otlp-proto-common == 1.18.0.dev", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index dfd70180e0..cbd6471246 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -23,6 +23,7 @@ import backoff import requests +from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_COMPRESSION, @@ -44,9 +45,6 @@ _OTLP_HTTP_HEADERS, Compression, ) -from opentelemetry.exporter.otlp.proto.http._log_exporter.encoder import ( - _ProtobufEncoder, -) from opentelemetry.util.re import parse_env_headers @@ -147,7 +145,7 @@ def export(self, batch: Sequence[LogData]) -> LogExportResult: _logger.warning("Exporter already shutdown, ignoring batch") return LogExportResult.FAILURE - serialized_data = _ProtobufEncoder.serialize(batch) + serialized_data = encode_logs(batch).SerializeToString() for delay in _expo(max_value=self._MAX_RETRY_TIMEOUT): diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index ffd5102a2d..c2950b999c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -15,24 +15,35 @@ import logging import zlib from os import environ -from typing import Dict, Optional, Sequence, Any, Callable, List, Mapping +from typing import Dict, Optional, Any, Callable, List +from typing import Sequence, Mapping # noqa: F401 + from io import BytesIO from time import sleep +from deprecated import deprecated +from opentelemetry.exporter.otlp.proto.common._internal import ( + _get_resource_data, +) +from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( + encode_metrics, +) from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.sdk.metrics._internal.aggregation import Aggregation -from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( # noqa: F401 ExportMetricsServiceRequest, ) -from opentelemetry.proto.common.v1.common_pb2 import ( +from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 AnyValue, ArrayValue, KeyValue, KeyValueList, ) -from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope -from opentelemetry.proto.resource.v1.resource_pb2 import Resource -from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 +from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 + InstrumentationScope, +) +from opentelemetry.proto.resource.v1.resource_pb2 import Resource # noqa: F401 +from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 # noqa: F401 from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, OTEL_EXPORTER_OTLP_ENDPOINT, @@ -56,11 +67,13 @@ ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, - Gauge, - Histogram as HistogramType, MetricExporter, MetricExportResult, MetricsData, +) +from opentelemetry.sdk.metrics.export import ( # noqa: F401 + Gauge, + Histogram as HistogramType, Sum, ) from opentelemetry.sdk.resources import Resource as SDKResource @@ -68,6 +81,9 @@ import backoff import requests +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as PB2Resource, +) _logger = logging.getLogger(__name__) @@ -220,140 +236,13 @@ def _retryable(resp: requests.Response) -> bool: return True return False - def _translate_data( - self, data: MetricsData - ) -> ExportMetricsServiceRequest: - - resource_metrics_dict = {} - - for resource_metrics in data.resource_metrics: - - resource = resource_metrics.resource - - # It is safe to assume that each entry in data.resource_metrics is - # associated with an unique resource. - scope_metrics_dict = {} - - resource_metrics_dict[resource] = scope_metrics_dict - - for scope_metrics in resource_metrics.scope_metrics: - - instrumentation_scope = scope_metrics.scope - - # The SDK groups metrics in instrumentation scopes already so - # there is no need to check for existing instrumentation scopes - # here. - pb2_scope_metrics = pb2.ScopeMetrics( - scope=InstrumentationScope( - name=instrumentation_scope.name, - version=instrumentation_scope.version, - ) - ) - - scope_metrics_dict[instrumentation_scope] = pb2_scope_metrics - - for metric in scope_metrics.metrics: - pb2_metric = pb2.Metric( - name=metric.name, - description=metric.description, - unit=metric.unit, - ) - - if isinstance(metric.data, Gauge): - for data_point in metric.data.data_points: - pt = pb2.NumberDataPoint( - attributes=self._translate_attributes( - data_point.attributes - ), - time_unix_nano=data_point.time_unix_nano, - ) - if isinstance(data_point.value, int): - pt.as_int = data_point.value - else: - pt.as_double = data_point.value - pb2_metric.gauge.data_points.append(pt) - - elif isinstance(metric.data, HistogramType): - for data_point in metric.data.data_points: - pt = pb2.HistogramDataPoint( - attributes=self._translate_attributes( - data_point.attributes - ), - time_unix_nano=data_point.time_unix_nano, - start_time_unix_nano=( - data_point.start_time_unix_nano - ), - count=data_point.count, - sum=data_point.sum, - bucket_counts=data_point.bucket_counts, - explicit_bounds=data_point.explicit_bounds, - max=data_point.max, - min=data_point.min, - ) - pb2_metric.histogram.aggregation_temporality = ( - metric.data.aggregation_temporality - ) - pb2_metric.histogram.data_points.append(pt) - - elif isinstance(metric.data, Sum): - for data_point in metric.data.data_points: - pt = pb2.NumberDataPoint( - attributes=self._translate_attributes( - data_point.attributes - ), - start_time_unix_nano=( - data_point.start_time_unix_nano - ), - time_unix_nano=data_point.time_unix_nano, - ) - if isinstance(data_point.value, int): - pt.as_int = data_point.value - else: - pt.as_double = data_point.value - # note that because sum is a message type, the - # fields must be set individually rather than - # instantiating a pb2.Sum and setting it once - pb2_metric.sum.aggregation_temporality = ( - metric.data.aggregation_temporality - ) - pb2_metric.sum.is_monotonic = ( - metric.data.is_monotonic - ) - pb2_metric.sum.data_points.append(pt) - else: - _logger.warn( - "unsupported datapoint type %s", metric.point - ) - continue - - pb2_scope_metrics.metrics.append(pb2_metric) - - return ExportMetricsServiceRequest( - resource_metrics=get_resource_data( - resource_metrics_dict, - pb2.ResourceMetrics, - "metrics", - ) - ) - - def _translate_attributes(self, attributes) -> Sequence[KeyValue]: - output = [] - if attributes: - - for key, value in attributes.items(): - try: - output.append(_translate_key_values(key, value)) - except Exception as error: # pylint: disable=broad-except - _logger.exception(error) - return output - def export( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: - serialized_data = self._translate_data(metrics_data) + serialized_data = encode_metrics(metrics_data) for delay in _expo(max_value=self._MAX_RETRY_TIMEOUT): if delay == self._MAX_RETRY_TIMEOUT: @@ -391,79 +280,16 @@ def force_flush(self, timeout_millis: float = 10_000) -> bool: return True -def _translate_value(value: Any) -> KeyValue: - - if isinstance(value, bool): - any_value = AnyValue(bool_value=value) - - elif isinstance(value, str): - any_value = AnyValue(string_value=value) - - elif isinstance(value, int): - any_value = AnyValue(int_value=value) - - elif isinstance(value, float): - any_value = AnyValue(double_value=value) - - elif isinstance(value, Sequence): - any_value = AnyValue( - array_value=ArrayValue(values=[_translate_value(v) for v in value]) - ) - - elif isinstance(value, Mapping): - any_value = AnyValue( - kvlist_value=KeyValueList( - values=[ - _translate_key_values(str(k), v) for k, v in value.items() - ] - ) - ) - - else: - raise Exception(f"Invalid type {type(value)} of value {value}") - - return any_value - - -def _translate_key_values(key: str, value: Any) -> KeyValue: - return KeyValue(key=key, value=_translate_value(value)) - - +@deprecated( + version="1.18.0", + reason="Use one of the encoders from opentelemetry-exporter-otlp-proto-common instead", +) def get_resource_data( sdk_resource_scope_data: Dict[SDKResource, Any], # ResourceDataT? - resource_class: Callable[..., Resource], + resource_class: Callable[..., PB2Resource], name: str, -) -> List[Resource]: - - resource_data = [] - - for ( - sdk_resource, - scope_data, - ) in sdk_resource_scope_data.items(): - - collector_resource = Resource() - - for key, value in sdk_resource.attributes.items(): - - try: - # pylint: disable=no-member - collector_resource.attributes.append( - _translate_key_values(key, value) - ) - except Exception as error: # pylint: disable=broad-except - _logger.exception(error) - - resource_data.append( - resource_class( - **{ - "resource": collector_resource, - "scope_{}".format(name): scope_data.values(), - } - ) - ) - - return resource_data +) -> List[PB2Resource]: + return _get_resource_data(sdk_resource_scope_data, resource_class, name) def _compression_from_env() -> Compression: diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 623d7dbf0a..b8e21f56af 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -23,6 +23,9 @@ import backoff import requests +from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( + encode_spans, +) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, @@ -40,9 +43,6 @@ _OTLP_HTTP_HEADERS, Compression, ) -from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import ( - _ProtobufEncoder, -) from opentelemetry.util.re import parse_env_headers @@ -143,7 +143,7 @@ def export(self, spans) -> SpanExportResult: _logger.warning("Exporter already shutdown, ignoring batch") return SpanExportResult.FAILURE - serialized_data = _ProtobufEncoder.serialize(spans) + serialized_data = encode_spans(spans).SerializeToString() for delay in _expo(max_value=self._MAX_RETRY_TIMEOUT): diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py index c1c9fe8864..a0036ecd24 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py @@ -12,277 +12,51 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging -from collections import abc -from typing import Any, List, Optional, Sequence +import logging # noqa: F401 +from collections import abc # noqa: F401 +from typing import Any, List, Optional, Sequence # noqa: F401 -from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( +from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( # noqa: F401 ExportTraceServiceRequest as PB2ExportTraceServiceRequest, ) -from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue -from opentelemetry.proto.common.v1.common_pb2 import ( +from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 + AnyValue as PB2AnyValue, +) +from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 ArrayValue as PB2ArrayValue, ) -from opentelemetry.proto.common.v1.common_pb2 import ( +from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 InstrumentationScope as PB2InstrumentationScope, ) -from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue -from opentelemetry.proto.resource.v1.resource_pb2 import ( +from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 + KeyValue as PB2KeyValue, +) +from opentelemetry.proto.resource.v1.resource_pb2 import ( # noqa: F401 Resource as PB2Resource, ) -from opentelemetry.proto.trace.v1.trace_pb2 import ( +from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 ScopeSpans as PB2ScopeSpans, ) -from opentelemetry.proto.trace.v1.trace_pb2 import ( +from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 ResourceSpans as PB2ResourceSpans, ) -from opentelemetry.proto.trace.v1.trace_pb2 import Span as PB2SPan -from opentelemetry.proto.trace.v1.trace_pb2 import Status as PB2Status -from opentelemetry.sdk.trace import Event -from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.sdk.trace import Resource -from opentelemetry.sdk.trace import Span as SDKSpan -from opentelemetry.trace import Link -from opentelemetry.trace import SpanKind -from opentelemetry.trace.span import SpanContext, TraceState, Status -from opentelemetry.util.types import Attributes - -# pylint: disable=E1101 -_SPAN_KIND_MAP = { - SpanKind.INTERNAL: PB2SPan.SpanKind.SPAN_KIND_INTERNAL, - SpanKind.SERVER: PB2SPan.SpanKind.SPAN_KIND_SERVER, - SpanKind.CLIENT: PB2SPan.SpanKind.SPAN_KIND_CLIENT, - SpanKind.PRODUCER: PB2SPan.SpanKind.SPAN_KIND_PRODUCER, - SpanKind.CONSUMER: PB2SPan.SpanKind.SPAN_KIND_CONSUMER, -} - -_logger = logging.getLogger(__name__) - - -class _ProtobufEncoder: - @classmethod - def serialize(cls, sdk_spans: Sequence[SDKSpan]) -> str: - return cls.encode(sdk_spans).SerializeToString() - - @staticmethod - def encode(sdk_spans: Sequence[SDKSpan]) -> PB2ExportTraceServiceRequest: - return PB2ExportTraceServiceRequest( - resource_spans=_encode_resource_spans(sdk_spans) - ) - - -def _encode_resource_spans( - sdk_spans: Sequence[SDKSpan], -) -> List[PB2ResourceSpans]: - # We need to inspect the spans and group + structure them as: - # - # Resource - # Instrumentation Library - # Spans - # - # First loop organizes the SDK spans in this structure. Protobuf messages - # are not hashable so we stick with SDK data in this phase. - # - # Second loop encodes the data into Protobuf format. - # - sdk_resource_spans = {} - - for sdk_span in sdk_spans: - sdk_resource = sdk_span.resource - sdk_instrumentation = sdk_span.instrumentation_scope or None - pb2_span = _encode_span(sdk_span) - - if sdk_resource not in sdk_resource_spans.keys(): - sdk_resource_spans[sdk_resource] = { - sdk_instrumentation: [pb2_span] - } - elif ( - sdk_instrumentation not in sdk_resource_spans[sdk_resource].keys() - ): - sdk_resource_spans[sdk_resource][sdk_instrumentation] = [pb2_span] - else: - sdk_resource_spans[sdk_resource][sdk_instrumentation].append( - pb2_span - ) - - pb2_resource_spans = [] - - for sdk_resource, sdk_instrumentations in sdk_resource_spans.items(): - scope_spans = [] - for sdk_instrumentation, pb2_spans in sdk_instrumentations.items(): - scope_spans.append( - PB2ScopeSpans( - scope=(_encode_instrumentation_scope(sdk_instrumentation)), - spans=pb2_spans, - ) - ) - pb2_resource_spans.append( - PB2ResourceSpans( - resource=_encode_resource(sdk_resource), - scope_spans=scope_spans, - ) - ) - - return pb2_resource_spans - - -def _encode_span(sdk_span: SDKSpan) -> PB2SPan: - span_context = sdk_span.get_span_context() - return PB2SPan( - trace_id=_encode_trace_id(span_context.trace_id), - span_id=_encode_span_id(span_context.span_id), - trace_state=_encode_trace_state(span_context.trace_state), - parent_span_id=_encode_parent_id(sdk_span.parent), - name=sdk_span.name, - kind=_SPAN_KIND_MAP[sdk_span.kind], - start_time_unix_nano=sdk_span.start_time, - end_time_unix_nano=sdk_span.end_time, - attributes=_encode_attributes(sdk_span.attributes), - events=_encode_events(sdk_span.events), - links=_encode_links(sdk_span.links), - status=_encode_status(sdk_span.status), - ) - - -def _encode_events( - events: Sequence[Event], -) -> Optional[List[PB2SPan.Event]]: - pb2_events = None - if events: - pb2_events = [] - for event in events: - encoded_event = PB2SPan.Event( - name=event.name, - time_unix_nano=event.timestamp, - ) - for key, value in event.attributes.items(): - try: - encoded_event.attributes.append( - _encode_key_value(key, value) - ) - # pylint: disable=broad-except - except Exception as error: - _logger.exception(error) - pb2_events.append(encoded_event) - return pb2_events - - -def _encode_links(links: List[Link]) -> List[PB2SPan.Link]: - pb2_links = None - if links: - pb2_links = [] - for link in links: - encoded_link = PB2SPan.Link( - trace_id=_encode_trace_id(link.context.trace_id), - span_id=_encode_span_id(link.context.span_id), - ) - for key, value in link.attributes.items(): - try: - encoded_link.attributes.append( - _encode_key_value(key, value) - ) - # pylint: disable=broad-except - except Exception as error: - _logger.exception(error) - pb2_links.append(encoded_link) - return pb2_links - - -def _encode_status(status: Status) -> Optional[PB2Status]: - pb2_status = None - if status is not None: - pb2_status = PB2Status( - code=status.status_code.value, - message=status.description, - ) - return pb2_status - - -def _encode_trace_state(trace_state: TraceState) -> Optional[str]: - pb2_trace_state = None - if trace_state is not None: - pb2_trace_state = ",".join( - [f"{key}={value}" for key, value in (trace_state.items())] - ) - return pb2_trace_state - - -def _encode_parent_id(context: Optional[SpanContext]) -> Optional[bytes]: - if isinstance(context, SpanContext): - encoded_parent_id = _encode_span_id(context.span_id) - else: - encoded_parent_id = None - return encoded_parent_id - - -def _encode_attributes( - attributes: Attributes, -) -> Optional[List[PB2KeyValue]]: - if attributes: - pb2_attributes = [] - for key, value in attributes.items(): - try: - pb2_attributes.append(_encode_key_value(key, value)) - except Exception as error: # pylint: disable=broad-except - _logger.exception(error) - else: - pb2_attributes = None - return pb2_attributes - - -def _encode_resource(resource: Resource) -> PB2Resource: - pb2_resource = PB2Resource() - for key, value in resource.attributes.items(): - try: - # pylint: disable=no-member - pb2_resource.attributes.append(_encode_key_value(key, value)) - except Exception as error: # pylint: disable=broad-except - _logger.exception(error) - return pb2_resource - - -def _encode_instrumentation_scope( - instrumentation_scope: InstrumentationScope, -) -> PB2InstrumentationScope: - if instrumentation_scope is None: - pb2_instrumentation_scope = PB2InstrumentationScope() - else: - pb2_instrumentation_scope = PB2InstrumentationScope( - name=instrumentation_scope.name, - version=instrumentation_scope.version, - ) - return pb2_instrumentation_scope - - -def _encode_value(value: Any) -> PB2AnyValue: - if isinstance(value, bool): - any_value = PB2AnyValue(bool_value=value) - elif isinstance(value, str): - any_value = PB2AnyValue(string_value=value) - elif isinstance(value, int): - any_value = PB2AnyValue(int_value=value) - elif isinstance(value, float): - any_value = PB2AnyValue(double_value=value) - elif isinstance(value, abc.Sequence): - any_value = PB2AnyValue( - array_value=PB2ArrayValue(values=[_encode_value(v) for v in value]) - ) - # tracing specs currently does not support Mapping type attributes. - # elif isinstance(value, abc.Mapping): - # pass - else: - raise Exception(f"Invalid type {type(value)} of value {value}") - return any_value - - -def _encode_key_value(key: str, value: Any) -> PB2KeyValue: - any_value = _encode_value(value) - return PB2KeyValue(key=key, value=any_value) - - -def _encode_span_id(span_id: int) -> bytes: - return span_id.to_bytes(length=8, byteorder="big", signed=False) - - -def _encode_trace_id(trace_id: int) -> bytes: - return trace_id.to_bytes(length=16, byteorder="big", signed=False) +from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 + Span as PB2SPan, +) +from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 + Status as PB2Status, +) +from opentelemetry.sdk.trace import Event # noqa: F401 +from opentelemetry.sdk.util.instrumentation import ( # noqa: F401 + InstrumentationScope, +) +from opentelemetry.sdk.trace import Resource # noqa: F401 +from opentelemetry.sdk.trace import Span as SDKSpan # noqa: F401 +from opentelemetry.trace import Link # noqa: F401 +from opentelemetry.trace import SpanKind # noqa: F401 +from opentelemetry.trace.span import ( # noqa: F401 + SpanContext, + TraceState, + Status, +) +from opentelemetry.util.types import Attributes # noqa: F401 diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index 9f57a23ae2..81e6c1442e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -21,6 +21,9 @@ from requests.models import Response from responses import POST, activate, add +from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( + encode_metrics, +) from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( DEFAULT_COMPRESSION, @@ -281,7 +284,7 @@ def test_serialization(self, mock_post): MetricExportResult.SUCCESS, ) - serialized_data = exporter._translate_data(self.metrics["sum_int"]) + serialized_data = encode_metrics(self.metrics["sum_int"]) mock_post.assert_called_once_with( url=exporter._endpoint, data=serialized_data.SerializeToString(), diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index 05bdfb9af3..5cf20b881b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -15,7 +15,7 @@ # pylint: disable=protected-access import unittest -from typing import List, Tuple +from typing import List from unittest.mock import MagicMock, patch import requests @@ -31,30 +31,7 @@ OTLPLogExporter, _is_backoff_v2, ) -from opentelemetry.exporter.otlp.proto.http._log_exporter.encoder import ( - _encode_attributes, - _encode_span_id, - _encode_trace_id, - _encode_value, - _ProtobufEncoder, -) from opentelemetry.exporter.otlp.proto.http.version import __version__ -from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( - ExportLogsServiceRequest, -) -from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue -from opentelemetry.proto.common.v1.common_pb2 import ( - InstrumentationScope as PB2InstrumentationScope, -) -from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue -from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord -from opentelemetry.proto.logs.v1.logs_pb2 import ( - ResourceLogs as PB2ResourceLogs, -) -from opentelemetry.proto.logs.v1.logs_pb2 import ScopeLogs as PB2ScopeLogs -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as PB2Resource, -) from opentelemetry.sdk._logs import LogData from opentelemetry.sdk._logs import LogRecord as SDKLogRecord from opentelemetry.sdk.environment_variables import ( @@ -190,19 +167,6 @@ def test_exporter_env(self): ) self.assertIsInstance(exporter._session, requests.Session) - def test_encode(self): - sdk_logs, expected_encoding = self.get_test_logs() - self.assertEqual( - _ProtobufEncoder().encode(sdk_logs), expected_encoding - ) - - def test_serialize(self): - sdk_logs, expected_encoding = self.get_test_logs() - self.assertEqual( - _ProtobufEncoder().serialize(sdk_logs), - expected_encoding.SerializeToString(), - ) - @responses.activate @patch("opentelemetry.exporter.otlp.proto.http._log_exporter.backoff") @patch("opentelemetry.exporter.otlp.proto.http._log_exporter.sleep") @@ -298,134 +262,3 @@ def _get_sdk_log_data() -> List[LogData]: ) return [log1, log2, log3, log4] - - def get_test_logs( - self, - ) -> Tuple[List[SDKLogRecord], ExportLogsServiceRequest]: - sdk_logs = self._get_sdk_log_data() - - pb2_service_request = ExportLogsServiceRequest( - resource_logs=[ - PB2ResourceLogs( - resource=PB2Resource( - attributes=[ - PB2KeyValue( - key="first_resource", - value=PB2AnyValue(string_value="value"), - ) - ] - ), - scope_logs=[ - PB2ScopeLogs( - scope=PB2InstrumentationScope( - name="first_name", version="first_version" - ), - log_records=[ - PB2LogRecord( - time_unix_nano=1644650195189786880, - trace_id=_encode_trace_id( - 89564621134313219400156819398935297684 - ), - span_id=_encode_span_id( - 1312458408527513268 - ), - flags=int(TraceFlags(0x01)), - severity_text="WARN", - severity_number=SeverityNumber.WARN.value, - body=_encode_value( - "Do not go gentle into that good night. Rage, rage against the dying of the light" - ), - attributes=_encode_attributes( - {"a": 1, "b": "c"} - ), - ) - ], - ), - PB2ScopeLogs( - scope=PB2InstrumentationScope( - name="another_name", - version="another_version", - ), - log_records=[ - PB2LogRecord( - time_unix_nano=1644650584292683008, - trace_id=_encode_trace_id( - 212592107417388365804938480559624925555 - ), - span_id=_encode_span_id( - 6077757853989569223 - ), - flags=int(TraceFlags(0x01)), - severity_text="INFO", - severity_number=SeverityNumber.INFO.value, - body=_encode_value( - "Love is the one thing that transcends time and space" - ), - attributes=_encode_attributes( - { - "filename": "model.py", - "func_name": "run_method", - } - ), - ) - ], - ), - ], - ), - PB2ResourceLogs( - resource=PB2Resource( - attributes=[ - PB2KeyValue( - key="second_resource", - value=PB2AnyValue(string_value="CASE"), - ) - ] - ), - scope_logs=[ - PB2ScopeLogs( - scope=PB2InstrumentationScope( - name="second_name", - version="second_version", - ), - log_records=[ - PB2LogRecord( - time_unix_nano=1644650249738562048, - trace_id=_encode_trace_id(0), - span_id=_encode_span_id(0), - flags=int(TraceFlags.DEFAULT), - severity_text="WARN", - severity_number=SeverityNumber.WARN.value, - body=_encode_value( - "Cooper, this is no time for caution!" - ), - attributes={}, - ), - ], - ), - PB2ScopeLogs( - scope=PB2InstrumentationScope(), - log_records=[ - PB2LogRecord( - time_unix_nano=1644650427658989056, - trace_id=_encode_trace_id( - 271615924622795969659406376515024083555 - ), - span_id=_encode_span_id( - 4242561578944770265 - ), - flags=int(TraceFlags(0x01)), - severity_text="DEBUG", - severity_number=SeverityNumber.DEBUG.value, - body=_encode_value("To our galaxy"), - attributes=_encode_attributes( - {"a": 1, "b": "c"} - ), - ), - ], - ), - ], - ), - ] - ) - - return sdk_logs, pb2_service_request diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py index 59b3a45d20..895904af03 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py @@ -40,7 +40,7 @@ def _generate_metric( def _generate_sum( name, value, attributes=None, description=None, unit=None -) -> Sum: +) -> Metric: if attributes is None: attributes = BoundedAttributes(attributes={"a": 1, "b": True}) return _generate_metric( @@ -64,7 +64,7 @@ def _generate_sum( def _generate_gauge( name, value, attributes=None, description=None, unit=None -) -> Gauge: +) -> Metric: if attributes is None: attributes = BoundedAttributes(attributes={"a": 1, "b": True}) return _generate_metric( @@ -86,7 +86,7 @@ def _generate_gauge( def _generate_unsupported_metric( name, attributes=None, description=None, unit=None -) -> Sum: +) -> Metric: return _generate_metric( name, None, diff --git a/tox.ini b/tox.ini index 843a0680ed..2f451ba796 100644 --- a/tox.ini +++ b/tox.ini @@ -37,6 +37,8 @@ envlist = py3{7,8,9,10,11}-opentelemetry-exporter-opencensus ; exporter-opencensus intentionally excluded from pypy3 + py3{7,8,9,10,11}-proto{3,4}-opentelemetry-exporter-otlp-proto-common + ; opentelemetry-exporter-otlp py3{7,8,9,10,11}-opentelemetry-exporter-otlp-combined ; intentionally excluded from pypy3 @@ -109,6 +111,7 @@ changedir = exporter-jaeger-proto-grpc: exporter/opentelemetry-exporter-jaeger-proto-grpc/tests exporter-jaeger-thrift: exporter/opentelemetry-exporter-jaeger-thrift/tests exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests + exporter-otlp-proto-common: exporter/opentelemetry-exporter-otlp-proto-common/tests exporter-otlp-combined: exporter/opentelemetry-exporter-otlp/tests exporter-otlp-proto-grpc: exporter/opentelemetry-exporter-otlp-proto-grpc/tests exporter-otlp-proto-http: exporter/opentelemetry-exporter-otlp-proto-http/tests @@ -138,15 +141,21 @@ commands_pre = opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus + exporter-otlp-proto-common: pip install {toxinidir}/opentelemetry-proto + exporter-otlp-proto-common: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common + exporter-otlp-combined: pip install {toxinidir}/opentelemetry-proto + exporter-otlp-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common exporter-otlp-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc exporter-otlp-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http exporter-otlp-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp exporter-otlp-proto-grpc: pip install {toxinidir}/opentelemetry-proto + exporter-otlp-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common exporter-otlp-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc exporter-otlp-proto-http: pip install {toxinidir}/opentelemetry-proto + exporter-otlp-proto-http: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common exporter-otlp-proto-http: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http[test] exporter-jaeger-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift {toxinidir}/exporter/opentelemetry-exporter-jaeger @@ -232,6 +241,7 @@ commands_pre = python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-opencensus[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp[test] @@ -307,6 +317,7 @@ commands_pre = ; opencensus exporter does not work with protobuf 4 proto3: -e {toxinidir}/exporter/opentelemetry-exporter-opencensus \ -e {toxinidir}/opentelemetry-proto \ + -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common \ -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc \ -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http \ -e {toxinidir}/exporter/opentelemetry-exporter-otlp From e00306206ea25cf8549eca289e39e0b6ba2fa560 Mon Sep 17 00:00:00 2001 From: Yoshi Yamaguchi Date: Fri, 14 Apr 2023 14:12:43 +0900 Subject: [PATCH 1420/1517] remove vendor specific example from doc (#3256) --- docs/examples/datadog_exporter/README.rst | 98 ------------------- .../datadog_exporter/basic_example.py | 38 ------- docs/examples/datadog_exporter/client.py | 58 ----------- .../datadog_exporter/datadog_client.py | 23 ----- docs/examples/datadog_exporter/server.py | 63 ------------ 5 files changed, 280 deletions(-) delete mode 100644 docs/examples/datadog_exporter/README.rst delete mode 100644 docs/examples/datadog_exporter/basic_example.py delete mode 100644 docs/examples/datadog_exporter/client.py delete mode 100644 docs/examples/datadog_exporter/datadog_client.py delete mode 100644 docs/examples/datadog_exporter/server.py diff --git a/docs/examples/datadog_exporter/README.rst b/docs/examples/datadog_exporter/README.rst deleted file mode 100644 index da495723bb..0000000000 --- a/docs/examples/datadog_exporter/README.rst +++ /dev/null @@ -1,98 +0,0 @@ -Datadog Span Exporter -===================== - -.. warning:: This exporter has been deprecated. To export your OTLP traces from OpenTelemetry SDK directly to Datadog Agent, please refer to `OTLP Ingest in Datadog Agent `_ . - - -These examples show how to use OpenTelemetry to send tracing data to Datadog. - - -Basic Example -------------- - -* Installation - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - pip install opentelemetry-exporter-datadog - -* Start Datadog Agent - -.. code-block:: sh - - docker run --rm \ - -v /var/run/docker.sock:/var/run/docker.sock:ro \ - -v /proc/:/host/proc/:ro \ - -v /sys/fs/cgroup/:/host/sys/fs/cgroup:ro \ - -p 127.0.0.1:8126:8126/tcp \ - -e DD_API_KEY="" \ - -e DD_APM_ENABLED=true \ - datadog/agent:latest - -* Run example - -.. code-block:: sh - - python basic_example.py - - -.. code-block:: sh - - python basic_example.py - -Distributed Example -------------------- - -* Installation - -.. code-block:: sh - - pip install opentelemetry-api - pip install opentelemetry-sdk - pip install opentelemetry-exporter-datadog - pip install opentelemetry-instrumentation - pip install opentelemetry-instrumentation-flask - pip install flask - pip install requests - -* Start Datadog Agent - -.. code-block:: sh - - docker run --rm \ - -v /var/run/docker.sock:/var/run/docker.sock:ro \ - -v /proc/:/host/proc/:ro \ - -v /sys/fs/cgroup/:/host/sys/fs/cgroup:ro \ - -p 127.0.0.1:8126:8126/tcp \ - -e DD_API_KEY="" \ - -e DD_APM_ENABLED=true \ - datadog/agent:latest - -* Start server - -.. code-block:: sh - - opentelemetry-instrument python server.py - -* Run client - -.. code-block:: sh - - opentelemetry-instrument python client.py testing - -* Run client with parameter to raise error - -.. code-block:: sh - - opentelemetry-instrument python client.py error - -* Run Datadog instrumented client - -The OpenTelemetry instrumented server is set up with propagation of Datadog trace context. - -.. code-block:: sh - - pip install ddtrace - ddtrace-run python datadog_client.py testing diff --git a/docs/examples/datadog_exporter/basic_example.py b/docs/examples/datadog_exporter/basic_example.py deleted file mode 100644 index 5eb470719b..0000000000 --- a/docs/examples/datadog_exporter/basic_example.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from opentelemetry import trace -from opentelemetry.exporter.datadog import ( - DatadogExportSpanProcessor, - DatadogSpanExporter, -) -from opentelemetry.sdk.trace import TracerProvider - -trace.set_tracer_provider(TracerProvider()) -tracer = trace.get_tracer(__name__) - -exporter = DatadogSpanExporter( - agent_url="http://localhost:8126", service="example" -) - -span_processor = DatadogExportSpanProcessor(exporter) -trace.get_tracer_provider().add_span_processor(span_processor) - - -with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("baz"): - print("Hello world from OpenTelemetry Python!") diff --git a/docs/examples/datadog_exporter/client.py b/docs/examples/datadog_exporter/client.py deleted file mode 100644 index 7c6196ad4a..0000000000 --- a/docs/examples/datadog_exporter/client.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from sys import argv - -from requests import get - -from opentelemetry import trace -from opentelemetry.exporter.datadog import ( - DatadogExportSpanProcessor, - DatadogSpanExporter, -) -from opentelemetry.propagate import inject -from opentelemetry.sdk import resources -from opentelemetry.sdk.trace import TracerProvider - -service_name = "example-client" - -resource = resources.Resource.create({"service.name": service_name}) - -trace.set_tracer_provider(TracerProvider(resource=resource)) - -trace.get_tracer_provider().add_span_processor( - DatadogExportSpanProcessor( - DatadogSpanExporter( - agent_url="http://localhost:8126", service=service_name - ) - ) -) - -tracer = trace.get_tracer(__name__) - -assert len(argv) == 2 - -with tracer.start_as_current_span("client"): - - with tracer.start_as_current_span("client-server"): - headers = {} - inject(headers) - requested = get( - "http://localhost:8082/server_request", - params={"param": argv[1]}, - headers=headers, - ) - - assert requested.status_code == 200 - print(requested.text) diff --git a/docs/examples/datadog_exporter/datadog_client.py b/docs/examples/datadog_exporter/datadog_client.py deleted file mode 100644 index 26c463c3f5..0000000000 --- a/docs/examples/datadog_exporter/datadog_client.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from sys import argv - -import requests - -requested = requests.get( - "http://localhost:8082/server_request", params={"param": argv[1]} -) -assert requested.status_code == 200 -print(requested.text) diff --git a/docs/examples/datadog_exporter/server.py b/docs/examples/datadog_exporter/server.py deleted file mode 100644 index 4438f22f53..0000000000 --- a/docs/examples/datadog_exporter/server.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from flask import Flask, request - -from opentelemetry import trace -from opentelemetry.exporter.datadog import ( - DatadogExportSpanProcessor, - DatadogSpanExporter, -) -from opentelemetry.exporter.datadog.propagator import DatadogFormat -from opentelemetry.propagate import get_global_textmap, set_global_textmap -from opentelemetry.propagators.composite import CompositePropagator -from opentelemetry.sdk.trace import TracerProvider - -app = Flask(__name__) - -trace.set_tracer_provider(TracerProvider()) - -trace.get_tracer_provider().add_span_processor( - DatadogExportSpanProcessor( - DatadogSpanExporter( - agent_url="http://localhost:8126", service="example-server" - ) - ) -) - -# append Datadog format for propagation to and from Datadog instrumented services -global_textmap = get_global_textmap() -if isinstance(global_textmap, CompositePropagator) and not any( - isinstance(p, DatadogFormat) for p in global_textmap._propagators -): - set_global_textmap( - CompositePropagator(global_textmap._propagators + [DatadogFormat()]) - ) -else: - set_global_textmap(DatadogFormat()) - -tracer = trace.get_tracer(__name__) - - -@app.route("/server_request") -def server_request(): - param = request.args.get("param") - with tracer.start_as_current_span("server-inner"): - if param == "error": - raise ValueError("forced server error") - return f"served: {param}" - - -if __name__ == "__main__": - app.run(port=8082) From 98c23c8c0e21bf4f7d2ec0818284e30042c69516 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 26 Apr 2023 07:17:03 -0600 Subject: [PATCH 1421/1517] Update contrib repo SHA (#3282) This is being done in order to fix CI, the underlying issue being a test case in the aiohttp instrumentation that is failing. Fixes #3278 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b77fb8128..70b278b9ac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: e4d8f10ecd7bcd29c119af0e3ea7c30d4a383f4b + CONTRIB_REPO_SHA: 511709802466a751786047b7d98c2eb84801b34f # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when From 14ca9f4169ad433e34da5b0a5323eea619b3dc62 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 28 Apr 2023 09:13:10 -0600 Subject: [PATCH 1422/1517] Select histogram aggregation with environment variables (#3265) --- CHANGELOG.md | 2 + .../_internal/metrics_encoder/__init__.py | 123 +++++++++++++++++- .../proto/grpc/metric_exporter/__init__.py | 71 +--------- .../tests/test_otlp_metrics_exporter.py | 56 ++++++++ .../proto/http/metric_exporter/__init__.py | 72 +--------- .../tests/metrics/__init__.py | 0 .../metrics/test_otlp_metrics_exporter.py | 52 ++++++++ .../sdk/environment_variables.py | 2 +- 8 files changed, 242 insertions(+), 136 deletions(-) delete mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/__init__.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3923d7eda5..aeb41a3c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Select histogram aggregation with an environment variable + ([#3265](https://github.com/open-telemetry/opentelemetry-python/pull/3265)) - Move Protobuf encoding to its own package ([#3169](https://github.com/open-telemetry/opentelemetry-python/pull/3169)) - Add experimental feature to detect resource detectors in auto instrumentation diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder/__init__.py index d269375930..d604786108 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder/__init__.py @@ -13,9 +13,27 @@ # limitations under the License. import logging +from opentelemetry.sdk.metrics.export import ( + MetricExporter, +) +from os import environ +from opentelemetry.sdk.metrics import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) from opentelemetry.exporter.otlp.proto.common._internal import ( _encode_attributes, ) +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, +) +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, +) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, ) @@ -24,17 +42,118 @@ from opentelemetry.sdk.metrics.export import ( MetricsData, Gauge, - Histogram, + Histogram as HistogramType, Sum, ExponentialHistogram as ExponentialHistogramType, ) +from typing import Dict from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as PB2Resource, ) +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, +) +from opentelemetry.sdk.metrics.view import ( + ExponentialBucketHistogramAggregation, + ExplicitBucketHistogramAggregation, +) _logger = logging.getLogger(__name__) +class OTLPMetricExporterMixin: + def _common_configuration( + self, + preferred_temporality: Dict[type, AggregationTemporality] = None, + ) -> None: + + instrument_class_temporality = {} + + otel_exporter_otlp_metrics_temporality_preference = ( + environ.get( + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + "CUMULATIVE", + ) + .upper() + .strip() + ) + + if otel_exporter_otlp_metrics_temporality_preference == "DELTA": + instrument_class_temporality = { + Counter: AggregationTemporality.DELTA, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.DELTA, + ObservableCounter: AggregationTemporality.DELTA, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + elif otel_exporter_otlp_metrics_temporality_preference == "LOWMEMORY": + instrument_class_temporality = { + Counter: AggregationTemporality.DELTA, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.DELTA, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + else: + if otel_exporter_otlp_metrics_temporality_preference != ( + "CUMULATIVE" + ): + _logger.warning( + "Unrecognized OTEL_EXPORTER_METRICS_TEMPORALITY_PREFERENCE" + " value found: " + f"{otel_exporter_otlp_metrics_temporality_preference}, " + "using CUMULATIVE" + ) + instrument_class_temporality = { + Counter: AggregationTemporality.CUMULATIVE, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.CUMULATIVE, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + instrument_class_temporality.update(preferred_temporality or {}) + + otel_exporter_otlp_metrics_default_histogram_aggregation = environ.get( + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + "explicit_bucket_histogram", + ) + + if otel_exporter_otlp_metrics_default_histogram_aggregation == ( + "base2_exponential_bucket_histogram" + ): + + histogram_aggregation_type = ExponentialBucketHistogramAggregation + + else: + + if otel_exporter_otlp_metrics_default_histogram_aggregation != ( + "explicit_bucket_histogram" + ): + + _logger.warning( + ( + "Invalid value for %s: %s, using explicit bucket " + "histogram aggregation" + ), + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + otel_exporter_otlp_metrics_default_histogram_aggregation, + ) + + histogram_aggregation_type = ExplicitBucketHistogramAggregation + + MetricExporter.__init__( + self, + preferred_temporality=instrument_class_temporality, + preferred_aggregation={Histogram: histogram_aggregation_type()}, + ) + + def encode_metrics(data: MetricsData) -> ExportMetricsServiceRequest: resource_metrics_dict = {} @@ -85,7 +204,7 @@ def encode_metrics(data: MetricsData) -> ExportMetricsServiceRequest: pt.as_double = data_point.value pb2_metric.gauge.data_points.append(pt) - elif isinstance(metric.data, Histogram): + elif isinstance(metric.data, HistogramType): for data_point in metric.data.data_points: pt = pb2.HistogramDataPoint( attributes=_encode_attributes( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index c388a726b6..ae3982b327 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -47,15 +47,6 @@ OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_INSECURE, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, -) -from opentelemetry.sdk.metrics import ( - Counter, - Histogram, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, @@ -73,6 +64,9 @@ Sum, ExponentialHistogram as ExponentialHistogramType, ) +from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( + OTLPMetricExporterMixin, +) _logger = getLogger(__name__) @@ -80,6 +74,7 @@ class OTLPMetricExporter( MetricExporter, OTLPExporterMixin[Metric, ExportMetricsServiceRequest, MetricExportResult], + OTLPMetricExporterMixin, ): """OTLP metric exporter @@ -132,63 +127,7 @@ def __init__( else compression ) - instrument_class_temporality = {} - - otel_exporter_otlp_metrics_temporality_preference = ( - environ.get( - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, - "CUMULATIVE", - ) - .upper() - .strip() - ) - - if otel_exporter_otlp_metrics_temporality_preference == "DELTA": - instrument_class_temporality = { - Counter: AggregationTemporality.DELTA, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.DELTA, - ObservableCounter: AggregationTemporality.DELTA, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - elif otel_exporter_otlp_metrics_temporality_preference == "LOWMEMORY": - instrument_class_temporality = { - Counter: AggregationTemporality.DELTA, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.DELTA, - ObservableCounter: AggregationTemporality.CUMULATIVE, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - else: - if otel_exporter_otlp_metrics_temporality_preference != ( - "CUMULATIVE" - ): - _logger.warning( - "Unrecognized OTEL_EXPORTER_METRICS_TEMPORALITY_PREFERENCE" - " value found: " - f"{otel_exporter_otlp_metrics_temporality_preference}, " - "using CUMULATIVE" - ) - instrument_class_temporality = { - Counter: AggregationTemporality.CUMULATIVE, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.CUMULATIVE, - ObservableCounter: AggregationTemporality.CUMULATIVE, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - instrument_class_temporality.update(preferred_temporality or {}) - - MetricExporter.__init__( - self, - preferred_temporality=instrument_class_temporality, - preferred_aggregation=preferred_aggregation, - ) + self._common_configuration(preferred_temporality) OTLPExporterMixin.__init__( self, diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py index 6250080229..3b4f128aca 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py @@ -44,6 +44,7 @@ OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_INSECURE, @@ -68,6 +69,10 @@ ResourceMetrics, ScopeMetrics, ) +from opentelemetry.sdk.metrics.view import ( + ExplicitBucketHistogramAggregation, + ExponentialBucketHistogramAggregation, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import ( InstrumentationScope as SDKInstrumentationScope, @@ -872,6 +877,57 @@ def test_aggregation_temporality(self): AggregationTemporality.CUMULATIVE, ) + def test_exponential_explicit_bucket_histogram(self): + + self.assertIsInstance( + # pylint: disable=protected-access + OTLPMetricExporter()._preferred_aggregation[Histogram], + ExplicitBucketHistogramAggregation, + ) + + with patch.dict( + environ, + { + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "base2_exponential_bucket_histogram" + }, + ): + self.assertIsInstance( + # pylint: disable=protected-access + OTLPMetricExporter()._preferred_aggregation[Histogram], + ExponentialBucketHistogramAggregation, + ) + + with patch.dict( + environ, + {OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "abc"}, + ): + with self.assertLogs(level=WARNING) as log: + self.assertIsInstance( + # pylint: disable=protected-access + OTLPMetricExporter()._preferred_aggregation[Histogram], + ExplicitBucketHistogramAggregation, + ) + self.assertIn( + ( + "Invalid value for OTEL_EXPORTER_OTLP_METRICS_DEFAULT_" + "HISTOGRAM_AGGREGATION: abc, using explicit bucket " + "histogram aggregation" + ), + log.output[0], + ) + + with patch.dict( + environ, + { + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "explicit_bucket_histogram" + }, + ): + self.assertIsInstance( + # pylint: disable=protected-access + OTLPMetricExporter()._preferred_aggregation[Histogram], + ExplicitBucketHistogramAggregation, + ) + def _resource_metrics( index: int, scope_metrics: List[ScopeMetrics] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index c2950b999c..da8561ebc8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -25,6 +25,9 @@ from opentelemetry.exporter.otlp.proto.common._internal import ( _get_resource_data, ) +from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( + OTLPMetricExporterMixin, +) from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( encode_metrics, ) @@ -45,7 +48,6 @@ from opentelemetry.proto.resource.v1.resource_pb2 import Resource # noqa: F401 from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 # noqa: F401 from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_HEADERS, @@ -57,14 +59,6 @@ OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, ) -from opentelemetry.sdk.metrics import ( - Counter, - Histogram, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, -) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, MetricExporter, @@ -106,7 +100,7 @@ def _expo(*args, **kwargs): return gen -class OTLPMetricExporter(MetricExporter): +class OTLPMetricExporter(MetricExporter, OTLPMetricExporterMixin): _MAX_RETRY_TIMEOUT = 64 @@ -153,63 +147,7 @@ def __init__( {"Content-Encoding": self._compression.value} ) - instrument_class_temporality = {} - - otel_exporter_otlp_metrics_temporality_preference = ( - environ.get( - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, - "CUMULATIVE", - ) - .upper() - .strip() - ) - - if otel_exporter_otlp_metrics_temporality_preference == "DELTA": - instrument_class_temporality = { - Counter: AggregationTemporality.DELTA, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.DELTA, - ObservableCounter: AggregationTemporality.DELTA, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - elif otel_exporter_otlp_metrics_temporality_preference == "LOWMEMORY": - instrument_class_temporality = { - Counter: AggregationTemporality.DELTA, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.DELTA, - ObservableCounter: AggregationTemporality.CUMULATIVE, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - else: - if otel_exporter_otlp_metrics_temporality_preference != ( - "CUMULATIVE" - ): - _logger.warning( - "Unrecognized OTEL_EXPORTER_METRICS_TEMPORALITY_PREFERENCE" - " value found: " - f"{otel_exporter_otlp_metrics_temporality_preference}, " - "using CUMULATIVE" - ) - instrument_class_temporality = { - Counter: AggregationTemporality.CUMULATIVE, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.CUMULATIVE, - ObservableCounter: AggregationTemporality.CUMULATIVE, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - instrument_class_temporality.update(preferred_temporality or {}) - - MetricExporter.__init__( - self, - preferred_temporality=instrument_class_temporality, - preferred_aggregation=preferred_aggregation, - ) + self._common_configuration(preferred_temporality) def _export(self, serialized_data: str): data = serialized_data diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index 81e6c1442e..632ecda3b9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -40,6 +40,7 @@ OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, @@ -61,6 +62,10 @@ ResourceMetrics, ScopeMetrics, ) +from opentelemetry.sdk.metrics.view import ( + ExplicitBucketHistogramAggregation, + ExponentialBucketHistogramAggregation, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import ( InstrumentationScope as SDKInstrumentationScope, @@ -424,3 +429,50 @@ def test_aggregation_temporality(self): otlp_metric_exporter._preferred_temporality[ObservableGauge], AggregationTemporality.CUMULATIVE, ) + + def test_exponential_explicit_bucket_histogram(self): + + self.assertIsInstance( + OTLPMetricExporter()._preferred_aggregation[Histogram], + ExplicitBucketHistogramAggregation, + ) + + with patch.dict( + environ, + { + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "base2_exponential_bucket_histogram" + }, + ): + self.assertIsInstance( + OTLPMetricExporter()._preferred_aggregation[Histogram], + ExponentialBucketHistogramAggregation, + ) + + with patch.dict( + environ, + {OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "abc"}, + ): + with self.assertLogs(level=WARNING) as log: + self.assertIsInstance( + OTLPMetricExporter()._preferred_aggregation[Histogram], + ExplicitBucketHistogramAggregation, + ) + self.assertIn( + ( + "Invalid value for OTEL_EXPORTER_OTLP_METRICS_DEFAULT_" + "HISTOGRAM_AGGREGATION: abc, using explicit bucket " + "histogram aggregation" + ), + log.output[0], + ) + + with patch.dict( + environ, + { + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "explicit_bucket_histogram" + }, + ): + self.assertIsInstance( + OTLPMetricExporter()._preferred_aggregation[Histogram], + ExplicitBucketHistogramAggregation, + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 275aa48340..273aebd9f1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -650,7 +650,7 @@ The :envvar:`OTEL_METRICS_EXEMPLAR_FILTER` is the filter for which measurements can become Exemplars. """ -_OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION = ( +OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION = ( "OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION" ) """ From e5c922725e26087a1b29d0b3b7618e4bcfbd92de Mon Sep 17 00:00:00 2001 From: pkt1583 Date: Wed, 3 May 2023 21:24:34 +0530 Subject: [PATCH 1423/1517] Modified getMessage to message (#3284) --- .../src/opentelemetry/sdk/_logs/_internal/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 83cef93149..eda9b093c9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -296,7 +296,7 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: "exc_text", "filename", "funcName", - "getMessage", + "message", "levelname", "levelno", "lineno", From 49c8d855964fa7c458514f6921054b6dd93fa6d2 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 4 May 2023 09:00:41 +0530 Subject: [PATCH 1424/1517] fix(docker-tests): pin requests version to < 2.29 (#3294) --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 2f451ba796..ce37337290 100644 --- a/tox.ini +++ b/tox.ini @@ -300,6 +300,7 @@ commands = deps = pytest docker-compose >= 1.25.2 + requests < 2.29.0 ; proto 3 and 4 tests install the respective version of protobuf proto3: protobuf~=3.19.0 From 3732fd4a196555715e541cfa936ddf3c43ccd4ee Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 4 May 2023 09:58:41 +0530 Subject: [PATCH 1425/1517] fix: updates pyproject.toml dev deps for all specifiers (#3280) --- eachdist.ini | 1 + scripts/eachdist.py | 11 ++++++++--- shim/opentelemetry-opencensus-shim/pyproject.toml | 5 ++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/eachdist.ini b/eachdist.ini index 51f52313d2..c33b4e0923 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -34,6 +34,7 @@ version=0.39b0.dev packages= opentelemetry-opentracing-shim + opentelemetry-opencensus-shim opentelemetry-exporter-opencensus opentelemetry-exporter-prometheus opentelemetry-distro diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 6b8db97a30..51f0f7dd2a 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -576,13 +576,18 @@ def update_version_files(targets, version, packages): def update_dependencies(targets, version, packages): print("updating dependencies") - targets = filter_packages(targets, packages) + # PEP 508 allowed specifier operators + operators = ['==', '!=', '<=', '>=', '<', '>', '===', '~=', '='] + operators_pattern = '|'.join(re.escape(op) for op in operators) + for pkg in packages: + search = rf"({basename(pkg)}[^,]*)({operators_pattern})(.*\.dev)" + replace = r"\1\2 " + version update_files( targets, "pyproject.toml", - rf"({basename(pkg)}.*)==(.*)", - r"\1== " + version + '",', + search, + replace, ) diff --git a/shim/opentelemetry-opencensus-shim/pyproject.toml b/shim/opentelemetry-opencensus-shim/pyproject.toml index fb4af68814..43bef2fdd5 100644 --- a/shim/opentelemetry-opencensus-shim/pyproject.toml +++ b/shim/opentelemetry-opencensus-shim/pyproject.toml @@ -33,7 +33,10 @@ dependencies = [ ] [project.optional-dependencies] -test = ["opentelemetry-test-utils == 0.39b0.dev", "opencensus == 0.11.1"] +test = [ + "opentelemetry-test-utils == 0.39b0.dev", + "opencensus == 0.11.1", +] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/shim/opentelemetry-opencensus-shim" From e9530c5c548d08a6aaa56268d103f9beb00cd002 Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Fri, 12 May 2023 14:30:36 -0700 Subject: [PATCH 1426/1517] Update version to 1.19.0.dev/0.40b0.dev (#3297) Co-authored-by: Diego Hurtado --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/pyproject.toml | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-common/pyproject.toml | 2 +- .../src/opentelemetry/exporter/otlp/proto/common/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-grpc/pyproject.toml | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-http/pyproject.toml | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/pyproject.toml | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/pyproject.toml | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opencensus-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opencensus/version.py | 2 +- shim/opentelemetry-opentracing-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/pyproject.toml | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 36 files changed, 47 insertions(+), 45 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70b278b9ac..1bdff92077 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 511709802466a751786047b7d98c2eb84801b34f + CONTRIB_REPO_SHA: 2edd017c22edb4896e182c934bc199d716495ce6 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb41a3c58..a589687295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## Version 1.18.0/0.39b0 (2023-05-04) + - Select histogram aggregation with an environment variable ([#3265](https://github.com/open-telemetry/opentelemetry-python/pull/3265)) - Move Protobuf encoding to its own package diff --git a/eachdist.ini b/eachdist.ini index c33b4e0923..896dabb66e 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.18.0.dev +version=1.19.0.dev packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.39b0.dev +version=0.40b0.dev packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 6b6b22d30f..8e6645935a 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 6b6b22d30f..8e6645935a 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger/pyproject.toml b/exporter/opentelemetry-exporter-jaeger/pyproject.toml index 95c93736de..cc7c45cbd1 100644 --- a/exporter/opentelemetry-exporter-jaeger/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-jaeger-proto-grpc == 1.18.0.dev", - "opentelemetry-exporter-jaeger-thrift == 1.18.0.dev", + "opentelemetry-exporter-jaeger-proto-grpc == 1.19.0.dev", + "opentelemetry-exporter-jaeger-thrift == 1.19.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 6b6b22d30f..8e6645935a 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/exporter/opentelemetry-exporter-opencensus/pyproject.toml b/exporter/opentelemetry-exporter-opencensus/pyproject.toml index 48e0a59ea4..f906b87bb7 100644 --- a/exporter/opentelemetry-exporter-opencensus/pyproject.toml +++ b/exporter/opentelemetry-exporter-opencensus/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ dependencies = [ "grpcio >= 1.0.0, < 2.0.0", "opencensus-proto >= 0.1.0, < 1.0.0", - "opentelemetry-api >= 1.18.0.dev", + "opentelemetry-api >= 1.19.0.dev", "opentelemetry-sdk >= 1.15", "protobuf ~= 3.13", "setuptools >= 16.0", diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index eb62a67e28..87b20fddc3 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.39b0.dev" +__version__ = "0.40b0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml index 5f872cd086..64e2767dee 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ - "opentelemetry-proto == 1.18.0.dev", + "opentelemetry-proto == 1.19.0.dev", ] [project.urls] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py index 80d12781d3..168a28bae9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index 82ec2b0a19..ebeaa35745 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -31,9 +31,9 @@ dependencies = [ "googleapis-common-protos ~= 1.52", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.18.0.dev", - "opentelemetry-sdk ~= 1.18.0.dev", - "opentelemetry-exporter-otlp-proto-common == 1.18.0.dev", + "opentelemetry-proto == 1.19.0.dev", + "opentelemetry-sdk ~= 1.19.0.dev", + "opentelemetry-exporter-otlp-proto-common == 1.19.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 80d12781d3..168a28bae9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index 54f2b67249..10eeafccf5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -30,9 +30,9 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.18.0.dev", - "opentelemetry-sdk ~= 1.18.0.dev", - "opentelemetry-exporter-otlp-proto-common == 1.18.0.dev", + "opentelemetry-proto == 1.19.0.dev", + "opentelemetry-sdk ~= 1.19.0.dev", + "opentelemetry-exporter-otlp-proto-common == 1.19.0.dev", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 80d12781d3..168a28bae9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index 8af01b433c..a2bb316e36 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-otlp-proto-grpc == 1.18.0.dev", - "opentelemetry-exporter-otlp-proto-http == 1.18.0.dev", + "opentelemetry-exporter-otlp-proto-grpc == 1.19.0.dev", + "opentelemetry-exporter-otlp-proto-http == 1.19.0.dev", ] [project.entry-points.opentelemetry_logs_exporter] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 80d12781d3..168a28bae9 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index eb62a67e28..87b20fddc3 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.39b0.dev" +__version__ = "0.40b0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 80d12781d3..168a28bae9 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml index 8764b35d43..6e0069848f 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.3", - "opentelemetry-exporter-zipkin-json == 1.18.0.dev", + "opentelemetry-exporter-zipkin-json == 1.19.0.dev", "opentelemetry-sdk ~= 1.11", "protobuf ~= 3.12", "requests ~= 2.7", diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 80d12781d3..168a28bae9 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin/pyproject.toml b/exporter/opentelemetry-exporter-zipkin/pyproject.toml index 03dedc6dea..5a1325e20c 100644 --- a/exporter/opentelemetry-exporter-zipkin/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-zipkin-json == 1.18.0.dev", - "opentelemetry-exporter-zipkin-proto-http == 1.18.0.dev", + "opentelemetry-exporter-zipkin-json == 1.19.0.dev", + "opentelemetry-exporter-zipkin-proto-http == 1.19.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 80d12781d3..168a28bae9 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 80d12781d3..168a28bae9 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 80d12781d3..168a28bae9 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index 5be574f3a2..88f3ca8f01 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-api == 1.18.0.dev", - "opentelemetry-semantic-conventions == 0.39b0.dev", + "opentelemetry-api == 1.19.0.dev", + "opentelemetry-semantic-conventions == 0.40b0.dev", "setuptools >= 16.0", "typing-extensions >= 3.7.4", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 80d12781d3..168a28bae9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index eb62a67e28..87b20fddc3 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.39b0.dev" +__version__ = "0.40b0.dev" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 80d12781d3..168a28bae9 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 80d12781d3..168a28bae9 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0.dev" +__version__ = "1.19.0.dev" diff --git a/shim/opentelemetry-opencensus-shim/pyproject.toml b/shim/opentelemetry-opencensus-shim/pyproject.toml index 43bef2fdd5..19b072b19a 100644 --- a/shim/opentelemetry-opencensus-shim/pyproject.toml +++ b/shim/opentelemetry-opencensus-shim/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.39b0.dev", + "opentelemetry-test-utils == 0.40b0.dev", "opencensus == 0.11.1", ] diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py index eb62a67e28..87b20fddc3 100644 --- a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.39b0.dev" +__version__ = "0.40b0.dev" diff --git a/shim/opentelemetry-opentracing-shim/pyproject.toml b/shim/opentelemetry-opentracing-shim/pyproject.toml index 6345b77d35..5ef3579520 100644 --- a/shim/opentelemetry-opentracing-shim/pyproject.toml +++ b/shim/opentelemetry-opentracing-shim/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.39b0.dev", + "opentelemetry-test-utils == 0.40b0.dev", "opentracing ~= 2.2.0", ] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index eb62a67e28..87b20fddc3 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.39b0.dev" +__version__ = "0.40b0.dev" diff --git a/tests/opentelemetry-test-utils/pyproject.toml b/tests/opentelemetry-test-utils/pyproject.toml index fb1c0915f4..41fc56244f 100644 --- a/tests/opentelemetry-test-utils/pyproject.toml +++ b/tests/opentelemetry-test-utils/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ ] dependencies = [ "asgiref ~= 3.0", - "opentelemetry-api == 1.18.0.dev", - "opentelemetry-sdk == 1.18.0.dev", + "opentelemetry-api == 1.19.0.dev", + "opentelemetry-sdk == 1.19.0.dev", ] [project.optional-dependencies] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 15f6e2a9b4..a4a2694c89 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.39b0.dev" +__version__ = "0.40b0.dev" From 4d0de5478f2e121290055a3803901dd62b56c2c8 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 23 May 2023 04:57:10 +0530 Subject: [PATCH 1427/1517] remove srikanthccv from maintainers (#3302) Co-authored-by: Leighton Chen --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1cad936e57..ce1bf2c59d 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,6 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t - [Diego Hurtado](https://github.com/ocelotl), Lightstep - [Leighton Chen](https://github.com/lzchen), Microsoft -- [Srikanth Chekuri](https://github.com/srikanthccv), signoz.io Emeritus Maintainers: @@ -125,6 +124,7 @@ Emeritus Maintainers: - [Chris Kleinknecht](https://github.com/c24t), Google - [Owais Lone](https://github.com/owais), Splunk - [Reiley Yang](https://github.com/reyang), Microsoft +- [Srikanth Chekuri](https://github.com/srikanthccv), signoz.io - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google *For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer).* From 46757ff063d7f6657e7eeef17350f08aa0f1e42f Mon Sep 17 00:00:00 2001 From: Nina Stawski Date: Tue, 23 May 2023 22:02:26 -0700 Subject: [PATCH 1428/1517] Use BoundedAttributes instead of raw dict to extract attributes from LogRecord #3114 (#3310) Co-authored-by: Diego Hurtado Co-authored-by: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> --- CHANGELOG.md | 2 +- .../src/opentelemetry/sdk/_logs/__init__.py | 2 + .../sdk/_logs/_internal/__init__.py | 119 +++++++++++++++++- opentelemetry-sdk/tests/logs/test_handler.py | 2 + .../tests/logs/test_log_limits.py | 40 ++++++ .../tests/logs/test_log_record.py | 67 +++++++++- 6 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 opentelemetry-sdk/tests/logs/test_log_limits.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a589687295..3ce6fc4251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased - +- Use BoundedAttributes instead of raw dict to extract attributes from LogRecord and Support dropped_attributes_count in LogRecord ([#3310](https://github.com/open-telemetry/opentelemetry-python/pull/3310)) ## Version 1.18.0/0.39b0 (2023-05-04) - Select histogram aggregation with an environment variable diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index a86fbaee0f..881bb9a4b2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -18,6 +18,7 @@ Logger, LoggerProvider, LoggingHandler, + LogLimits, LogRecord, LogRecordProcessor, ) @@ -27,6 +28,7 @@ "Logger", "LoggerProvider", "LoggingHandler", + "LogLimits", "LogRecord", "LogRecordProcessor", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index eda9b093c9..7410138067 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -19,6 +19,7 @@ import logging import threading import traceback +from os import environ from time import time_ns from typing import Any, Callable, Optional, Tuple, Union @@ -31,6 +32,11 @@ get_logger_provider, std_to_otel, ) +from opentelemetry.attributes import BoundedAttributes +from opentelemetry.sdk.environment_variables import ( + OTEL_ATTRIBUTE_COUNT_LIMIT, + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util.instrumentation import InstrumentationScope @@ -45,6 +51,101 @@ _logger = logging.getLogger(__name__) +_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128 +_ENV_VALUE_UNSET = "" + + +class LogLimits: + """This class is based on a SpanLimits class in the Tracing module. + + This class represents the limits that should be enforced on recorded data such as events, links, attributes etc. + + This class does not enforce any limits itself. It only provides a way to read limits from env, + default values and from user provided arguments. + + All limit arguments must be either a non-negative integer, ``None`` or ``LogLimits.UNSET``. + + - All limit arguments are optional. + - If a limit argument is not set, the class will try to read its value from the corresponding + environment variable. + - If the environment variable is not set, the default value, if any, will be used. + + Limit precedence: + + - If a model specific limit is set, it will be used. + - Else if the corresponding global limit is set, it will be used. + - Else if the model specific limit has a default value, the default value will be used. + - Else if the global limit has a default value, the default value will be used. + + Args: + max_attributes: Maximum number of attributes that can be added to a span, event, and link. + Environment variable: ``OTEL_ATTRIBUTE_COUNT_LIMIT`` + Default: {_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT} + max_attribute_length: Maximum length an attribute value can have. Values longer than + the specified length will be truncated. + """ + + UNSET = -1 + + def __init__( + self, + max_attributes: Optional[int] = None, + max_attribute_length: Optional[int] = None, + ): + + # attribute count + global_max_attributes = self._from_env_if_absent( + max_attributes, OTEL_ATTRIBUTE_COUNT_LIMIT + ) + self.max_attributes = ( + global_max_attributes + if global_max_attributes is not None + else _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT + ) + + # attribute length + self.max_attribute_length = self._from_env_if_absent( + max_attribute_length, + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, + ) + + def __repr__(self): + return f"{type(self).__name__}(max_attributes={self.max_attributes}, max_attribute_length={self.max_attribute_length})" + + @classmethod + def _from_env_if_absent( + cls, value: Optional[int], env_var: str, default: Optional[int] = None + ) -> Optional[int]: + if value == cls.UNSET: + return None + + err_msg = "{0} must be a non-negative integer but got {}" + + # if no value is provided for the limit, try to load it from env + if value is None: + # return default value if env var is not set + if env_var not in environ: + return default + + str_value = environ.get(env_var, "").strip().lower() + if str_value == _ENV_VALUE_UNSET: + return None + + try: + value = int(str_value) + except ValueError: + raise ValueError(err_msg.format(env_var, str_value)) + + if value < 0: + raise ValueError(err_msg.format(env_var, value)) + return value + + +_UnsetLogLimits = LogLimits( + max_attributes=LogLimits.UNSET, + max_attribute_length=LogLimits.UNSET, +) + class LogRecord(APILogRecord): """A LogRecord instance represents an event being logged. @@ -66,6 +167,7 @@ def __init__( body: Optional[Any] = None, resource: Optional[Resource] = None, attributes: Optional[Attributes] = None, + limits: Optional[LogLimits] = _UnsetLogLimits, ): super().__init__( **{ @@ -77,7 +179,12 @@ def __init__( "severity_text": severity_text, "severity_number": severity_number, "body": body, - "attributes": attributes, + "attributes": BoundedAttributes( + maxlen=limits.max_attributes, + attributes=attributes if bool(attributes) else None, + immutable=False, + max_value_len=limits.max_attribute_length, + ), } ) self.resource = resource @@ -93,7 +200,9 @@ def to_json(self, indent=4) -> str: "body": self.body, "severity_number": repr(self.severity_number), "severity_text": self.severity_text, - "attributes": self.attributes, + "attributes": dict(self.attributes) + if bool(self.attributes) + else None, "timestamp": ns_to_iso_str(self.timestamp), "trace_id": f"0x{format_trace_id(self.trace_id)}" if self.trace_id is not None @@ -109,6 +218,12 @@ def to_json(self, indent=4) -> str: indent=indent, ) + @property + def dropped_attributes(self) -> int: + if self.attributes: + return self.attributes.dropped + return 0 + class LogData: """Readable LogRecord data plus associated InstrumentationLibrary.""" diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index b9c40608e1..04cf5640f5 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -17,6 +17,7 @@ from opentelemetry._logs import SeverityNumber from opentelemetry._logs import get_logger as APIGetLogger +from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk import trace from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler from opentelemetry.semconv.trace import SpanAttributes @@ -91,6 +92,7 @@ def test_log_record_user_attributes(self): self.assertIsNotNone(log_record) self.assertEqual(log_record.attributes, {"http.status_code": 200}) + self.assertTrue(isinstance(log_record.attributes, BoundedAttributes)) def test_log_record_exception(self): """Exception information will be included in attributes""" diff --git a/opentelemetry-sdk/tests/logs/test_log_limits.py b/opentelemetry-sdk/tests/logs/test_log_limits.py new file mode 100644 index 0000000000..c2135b6569 --- /dev/null +++ b/opentelemetry-sdk/tests/logs/test_log_limits.py @@ -0,0 +1,40 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry.sdk._logs import LogLimits +from opentelemetry.sdk._logs._internal import ( + _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT, +) + + +class TestLogLimits(unittest.TestCase): + def test_log_limits_repr_unset(self): + expected = f"LogLimits(max_attributes={_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT}, max_attribute_length=None)" + limits = str(LogLimits()) + + self.assertEqual(expected, limits) + + def test_log_limits_max_attributes(self): + expected = 1 + limits = LogLimits(max_attributes=1) + + self.assertEqual(expected, limits.max_attributes) + + def test_log_limits_max_attribute_length(self): + expected = 1 + limits = LogLimits(max_attribute_length=1) + + self.assertEqual(expected, limits.max_attribute_length) diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index 7142408c4c..a5993e5833 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -15,7 +15,8 @@ import json import unittest -from opentelemetry.sdk._logs import LogRecord +from opentelemetry.attributes import BoundedAttributes +from opentelemetry.sdk._logs import LogLimits, LogRecord class TestLogRecord(unittest.TestCase): @@ -39,3 +40,67 @@ def test_log_record_to_json(self): body="a log line", ).to_json() self.assertEqual(expected, actual) + + def test_log_record_bounded_attributes(self): + attr = {"key": "value"} + + result = LogRecord(timestamp=0, body="a log line", attributes=attr) + + self.assertTrue(isinstance(result.attributes, BoundedAttributes)) + + def test_log_record_dropped_attributes_empty_limits(self): + attr = {"key": "value"} + + result = LogRecord(timestamp=0, body="a log line", attributes=attr) + + self.assertTrue(result.dropped_attributes == 0) + + def test_log_record_dropped_attributes_set_limits_max_attribute(self): + attr = {"key": "value", "key2": "value2"} + limits = LogLimits( + max_attributes=1, + ) + + result = LogRecord( + timestamp=0, body="a log line", attributes=attr, limits=limits + ) + self.assertTrue(result.dropped_attributes == 1) + + def test_log_record_dropped_attributes_set_limits_max_attribute_length( + self, + ): + attr = {"key": "value", "key2": "value2"} + expected = {"key": "v", "key2": "v"} + limits = LogLimits( + max_attribute_length=1, + ) + + result = LogRecord( + timestamp=0, body="a log line", attributes=attr, limits=limits + ) + self.assertTrue(result.dropped_attributes == 0) + self.assertEqual(expected, result.attributes) + + def test_log_record_dropped_attributes_set_limits(self): + attr = {"key": "value", "key2": "value2"} + expected = {"key2": "v"} + limits = LogLimits( + max_attributes=1, + max_attribute_length=1, + ) + + result = LogRecord( + timestamp=0, body="a log line", attributes=attr, limits=limits + ) + self.assertTrue(result.dropped_attributes == 1) + self.assertEqual(expected, result.attributes) + + def test_log_record_dropped_attributes_unset_limits(self): + attr = {"key": "value", "key2": "value2"} + limits = LogLimits() + + result = LogRecord( + timestamp=0, body="a log line", attributes=attr, limits=limits + ) + self.assertTrue(result.dropped_attributes == 0) + self.assertEqual(attr, result.attributes) From 9706ed004ae3c35ab393fb48c3d71d0c40e8c62b Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 5 Jun 2023 11:01:50 +0200 Subject: [PATCH 1429/1517] Update approvers list (#3316) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ce1bf2c59d..e3923e8fe5 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): - [Aaron Abbott](https://github.com/aabmass), Google +- [Jeremy Voss](https://github.com/jeremydvoss), Microsoft - [Sanket Mehta](https://github.com/sanketmehta28), Cisco Emeritus Approvers @@ -106,7 +107,7 @@ Emeritus Approvers - [Ashutosh Goel](https://github.com/ashu658), Cisco - [Carlos Alberto Cortez](https://github.com/carlosalberto), Lightstep - [Christian Neumüller](https://github.com/Oberon00), Dynatrace -- [Hector Hernandez](https://github.com/hectorhdzg), Microsoft +- [Héctor Hernández](https://github.com/hectorhdzg), Microsoft - [Mauricio Vásquez](https://github.com/mauriciovasquezbernal), Kinvolk - [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), AWS - [Tahir H. Butt](https://github.com/majorgreys) DataDog From 92bfd083a576289c7ef2b9e5e49e50243e845bc8 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 7 Jun 2023 21:03:08 +0200 Subject: [PATCH 1430/1517] Relax versioning of importlib-metadata (#3334) --- opentelemetry-api/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/pyproject.toml b/opentelemetry-api/pyproject.toml index 5b2ac54af1..dcc9a2f168 100644 --- a/opentelemetry-api/pyproject.toml +++ b/opentelemetry-api/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "setuptools >= 16.0", # FIXME This should be able to be removed after 3.12 is released if there is a reliable API # in importlib.metadata. - "importlib-metadata ~= 6.0.0", + "importlib-metadata ~= 6.0", ] dynamic = [ "version", From 2f804fa95821636f4e64e65e4cf7d50be952757f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 13 Jun 2023 14:23:18 +0200 Subject: [PATCH 1431/1517] Add max_scale option (#3323) --- CHANGELOG.md | 3 ++ .../sdk/metrics/_internal/aggregation.py | 15 +++++++- ...xponential_bucket_histogram_aggregation.py | 37 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ce6fc4251..0f10920db7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased + +- Add max_scale option to Exponential Bucket Histogram Aggregation [#3323](https://github.com/open-telemetry/opentelemetry-python/pull/3323)) - Use BoundedAttributes instead of raw dict to extract attributes from LogRecord and Support dropped_attributes_count in LogRecord ([#3310](https://github.com/open-telemetry/opentelemetry-python/pull/3310)) + ## Version 1.18.0/0.39b0 (2023-05-04) - Select histogram aggregation with an environment variable diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py index 7312df1aa7..ae21db907d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py @@ -386,6 +386,7 @@ def __init__( # See the derivation here: # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exponential-bucket-histogram-aggregation) max_size: int = 160, + max_scale: int = 20, ): super().__init__(attributes) # max_size is the maximum capacity of the positive and negative @@ -403,6 +404,7 @@ def __init__( ) self._max_size = max_size + self._max_scale = max_scale # _sum is the sum of all the values aggregated by this aggregator. self._sum = 0 @@ -428,7 +430,14 @@ def __init__( # _mapping corresponds to the current scale, is shared by both the # positive and negative buckets. - self._mapping = LogarithmMapping(LogarithmMapping._max_scale) + + if self._max_scale > 20: + _logger.warning( + "max_scale is set to %s which is " + "larger than the recommended value of 20", + self._max_scale, + ) + self._mapping = LogarithmMapping(self._max_scale) self._instrument_temporality = AggregationTemporality.DELTA self._start_time_unix_nano = start_time_unix_nano @@ -941,9 +950,10 @@ class ExponentialBucketHistogramAggregation(Aggregation): def __init__( self, max_size: int = 160, + max_scale: int = 20, ): - self._max_size = max_size + self._max_scale = max_scale def _create_aggregation( self, @@ -955,6 +965,7 @@ def _create_aggregation( attributes, start_time_unix_nano, max_size=self._max_size, + max_scale=self._max_scale, ) diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py index 65a437bc6d..9bea75e426 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py @@ -13,6 +13,7 @@ # limitations under the License. from itertools import permutations +from logging import WARNING from math import ldexp from sys import float_info from types import MethodType @@ -37,6 +38,9 @@ LogarithmMapping, ) from opentelemetry.sdk.metrics._internal.measurement import Measurement +from opentelemetry.sdk.metrics.view import ( + ExponentialBucketHistogramAggregation, +) def get_counts(buckets: Buckets) -> int: @@ -77,6 +81,39 @@ def swap( class TestExponentialBucketHistogramAggregation(TestCase): + @patch("opentelemetry.sdk.metrics._internal.aggregation.LogarithmMapping") + def test_create_aggregation(self, mock_logarithm_mapping): + exponential_bucket_histogram_aggregation = ( + ExponentialBucketHistogramAggregation() + )._create_aggregation(Mock(), Mock(), Mock()) + + self.assertEqual( + exponential_bucket_histogram_aggregation._max_scale, 20 + ) + + mock_logarithm_mapping.assert_called_with(20) + + exponential_bucket_histogram_aggregation = ( + ExponentialBucketHistogramAggregation(max_scale=10) + )._create_aggregation(Mock(), Mock(), Mock()) + + self.assertEqual( + exponential_bucket_histogram_aggregation._max_scale, 10 + ) + + mock_logarithm_mapping.assert_called_with(10) + + with self.assertLogs(level=WARNING): + exponential_bucket_histogram_aggregation = ( + ExponentialBucketHistogramAggregation(max_scale=100) + )._create_aggregation(Mock(), Mock(), Mock()) + + self.assertEqual( + exponential_bucket_histogram_aggregation._max_scale, 100 + ) + + mock_logarithm_mapping.assert_called_with(100) + def assertInEpsilon(self, first, second, epsilon): self.assertLessEqual(first, (second * (1 + epsilon))) self.assertGreaterEqual(first, (second * (1 - epsilon))) From a5520e8d14fdc31416098d3dc147d500a6a29821 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 15:15:15 +0200 Subject: [PATCH 1432/1517] Bump requests from 2.28.1 to 2.31.0 (#3317) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Srikanth Chekuri Co-authored-by: Diego Hurtado --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index ff7379256a..732acd8b2e 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -22,5 +22,5 @@ mypy-protobuf~=3.0.0 markupsafe==2.0.1 bleach==4.1.0 # This dependency was updated to a breaking version. codespell==2.1.0 -requests==2.28.1 +requests==2.31.0 ruamel.yaml==0.17.21 From 5ec506421cffb0fa5a4e0f8ad1c5505ececdd4c5 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 13 Jun 2023 19:31:01 +0530 Subject: [PATCH 1433/1517] Add a note about force_flush implementation for OTLP exporters (#3262) --- .../otlp/proto/grpc/_log_exporter/__init__.py | 8 ++- .../exporter/otlp/proto/grpc/exporter.py | 64 ++++--------------- .../proto/grpc/metric_exporter/__init__.py | 1 + .../proto/grpc/trace_exporter/__init__.py | 1 + .../tests/logs/test_otlp_logs_exporter.py | 12 ++-- .../tests/test_otlp_metrics_exporter.py | 1 + .../tests/test_otlp_trace_exporter.py | 16 ++--- .../otlp/proto/http/_log_exporter/__init__.py | 4 ++ .../proto/http/metric_exporter/__init__.py | 1 + .../proto/http/trace_exporter/__init__.py | 1 + 10 files changed, 41 insertions(+), 68 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index ef1b77de27..3a87ef1223 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -107,8 +107,12 @@ def _translate_data( def export(self, batch: Sequence[LogData]) -> LogExportResult: return self._export(batch) - def shutdown(self) -> None: - pass + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + OTLPExporterMixin.shutdown(self, timeout_millis=timeout_millis) + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + """Nothing is buffered in this exporter, so this method does nothing.""" + return True @property def _exporting(self) -> str: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 471d5fe301..fa041539c7 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -16,11 +16,20 @@ import threading from abc import ABC, abstractmethod -from collections.abc import Sequence +from collections.abc import Sequence # noqa: F401 from logging import getLogger from os import environ from time import sleep -from typing import Any, Callable, Dict, Generic, List, Optional, Tuple, Union +from typing import ( # noqa: F401 + Any, + Callable, + Dict, + Generic, + List, + Optional, + Tuple, + Union, +) from typing import Sequence as TypingSequence from typing import TypeVar from urllib.parse import urlparse @@ -45,7 +54,7 @@ from opentelemetry.exporter.otlp.proto.grpc import ( _OTLP_GRPC_HEADERS, ) -from opentelemetry.proto.common.v1.common_pb2 import ( +from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 AnyValue, ArrayValue, KeyValue, @@ -97,44 +106,6 @@ def environ_to_compression(environ_key: str) -> Optional[Compression]: return _ENVIRON_TO_COMPRESSION[environ_value] -def _translate_value(value: Any) -> KeyValue: - if isinstance(value, bool): - any_value = AnyValue(bool_value=value) - - elif isinstance(value, str): - any_value = AnyValue(string_value=value) - - elif isinstance(value, int): - any_value = AnyValue(int_value=value) - - elif isinstance(value, float): - any_value = AnyValue(double_value=value) - - elif isinstance(value, Sequence): - any_value = AnyValue( - array_value=ArrayValue(values=[_translate_value(v) for v in value]) - ) - - # Tracing specs currently does not support Mapping type attributes - # elif isinstance(value, Mapping): - # any_value = AnyValue( - # kvlist_value=KeyValueList( - # values=[ - # _translate_key_values(str(k), v) for k, v in value.items() - # ] - # ) - # ) - - else: - raise Exception(f"Invalid type {type(value)} of value {value}") - - return any_value - - -def _translate_key_values(key: str, value: Any) -> KeyValue: - return KeyValue(key=key, value=_translate_value(value)) - - @deprecated( version="1.18.0", reason="Use one of the encoders from opentelemetry-exporter-otlp-proto-common instead", @@ -271,17 +242,6 @@ def _translate_data( ) -> ExportServiceRequestT: pass - def _translate_attributes(self, attributes) -> TypingSequence[KeyValue]: - output = [] - if attributes: - - for key, value in attributes.items(): - try: - output.append(_translate_key_values(key, value)) - except Exception as error: # pylint: disable=broad-except - logger.exception(error) - return output - def _export( self, data: Union[TypingSequence[ReadableSpan], MetricsData] ) -> ExportResultT: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index ae3982b327..2560c5c305 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -258,4 +258,5 @@ def _exporting(self) -> str: return "metrics" def force_flush(self, timeout_millis: float = 10_000) -> bool: + """Nothing is buffered in this exporter, so this method does nothing.""" return True diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 72bd036885..bd120ac787 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -144,6 +144,7 @@ def shutdown(self) -> None: OTLPExporterMixin.shutdown(self) def force_flush(self, timeout_millis: int = 30000) -> bool: + """Nothing is buffered in this exporter, so this method does nothing.""" return True @property diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index f21ed06678..c2e4a5dc0c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -23,10 +23,10 @@ from grpc import ChannelCredentials, Compression, StatusCode, server from opentelemetry._logs import SeverityNumber +from opentelemetry.exporter.otlp.proto.common._internal import _encode_value from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter, ) -from opentelemetry.exporter.otlp.proto.grpc.exporter import _translate_value from opentelemetry.exporter.otlp.proto.grpc.version import __version__ from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, @@ -367,7 +367,7 @@ def test_translate_log_data(self): 16, "big", ), - body=_translate_value( + body=_encode_value( "Zhengzhou, We have a heaviest rains in 1000 years" ), attributes=[ @@ -426,7 +426,7 @@ def test_translate_multiple_logs(self): 16, "big", ), - body=_translate_value( + body=_encode_value( "Zhengzhou, We have a heaviest rains in 1000 years" ), attributes=[ @@ -463,13 +463,13 @@ def test_translate_multiple_logs(self): 16, "big", ), - body=_translate_value( + body=_encode_value( "Sydney, Opera House is closed" ), attributes=[ KeyValue( key="custom_attr", - value=_translate_value([1, 2, 3]), + value=_encode_value([1, 2, 3]), ), ], flags=int( @@ -508,7 +508,7 @@ def test_translate_multiple_logs(self): 16, "big", ), - body=_translate_value( + body=_encode_value( "Mumbai, Boil water before drinking" ), attributes=[], diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py index 3b4f128aca..58cab7175c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py @@ -746,6 +746,7 @@ def test_shutdown(self): warning.records[0].message, "Exporter already shutdown, ignoring batch", ) + self.exporter = OTLPMetricExporter() def test_shutdown_wait_last_export(self): add_MetricsServiceServicer_to_server( diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 2498da74b8..be6c71ab3a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -26,10 +26,10 @@ from grpc import ChannelCredentials, Compression, StatusCode, server from opentelemetry.attributes import BoundedAttributes -from opentelemetry.exporter.otlp.proto.grpc.exporter import ( - _is_backoff_v2, - _translate_key_values, +from opentelemetry.exporter.otlp.proto.common._internal import ( + _encode_key_value, ) +from opentelemetry.exporter.otlp.proto.grpc.exporter import _is_backoff_v2 from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) @@ -840,31 +840,31 @@ def test_span_status_translate(self): # pylint:disable=no-member def test_translate_key_values(self): - bool_value = _translate_key_values("bool_type", False) + bool_value = _encode_key_value("bool_type", False) self.assertTrue(isinstance(bool_value, KeyValue)) self.assertEqual(bool_value.key, "bool_type") self.assertTrue(isinstance(bool_value.value, AnyValue)) self.assertFalse(bool_value.value.bool_value) - str_value = _translate_key_values("str_type", "str") + str_value = _encode_key_value("str_type", "str") self.assertTrue(isinstance(str_value, KeyValue)) self.assertEqual(str_value.key, "str_type") self.assertTrue(isinstance(str_value.value, AnyValue)) self.assertEqual(str_value.value.string_value, "str") - int_value = _translate_key_values("int_type", 2) + int_value = _encode_key_value("int_type", 2) self.assertTrue(isinstance(int_value, KeyValue)) self.assertEqual(int_value.key, "int_type") self.assertTrue(isinstance(int_value.value, AnyValue)) self.assertEqual(int_value.value.int_value, 2) - double_value = _translate_key_values("double_type", 3.2) + double_value = _encode_key_value("double_type", 3.2) self.assertTrue(isinstance(double_value, KeyValue)) self.assertEqual(double_value.key, "double_type") self.assertTrue(isinstance(double_value.value, AnyValue)) self.assertEqual(double_value.value.double_value, 3.2) - seq_value = _translate_key_values("seq_type", ["asd", "123"]) + seq_value = _encode_key_value("seq_type", ["asd", "123"]) self.assertTrue(isinstance(seq_value, KeyValue)) self.assertEqual(seq_value.key, "seq_type") self.assertTrue(isinstance(seq_value.value, AnyValue)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index cbd6471246..67f83f280f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -173,6 +173,10 @@ def export(self, batch: Sequence[LogData]) -> LogExportResult: return LogExportResult.FAILURE return LogExportResult.FAILURE + def force_flush(self, timeout_millis: float = 10_000) -> bool: + """Nothing is buffered in this exporter, so this method does nothing.""" + return True + def shutdown(self): if self._shutdown: _logger.warning("Exporter already shutdown, ignoring call") diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index da8561ebc8..2c13601e0a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -215,6 +215,7 @@ def _exporting(self) -> str: return "metrics" def force_flush(self, timeout_millis: float = 10_000) -> bool: + """Nothing is buffered in this exporter, so this method does nothing.""" return True diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index b8e21f56af..dbc6a0a692 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -179,6 +179,7 @@ def shutdown(self): self._shutdown = True def force_flush(self, timeout_millis: int = 30000) -> bool: + """Nothing is buffered in this exporter, so this method does nothing.""" return True From 6b9f389940ec0123d5dafc7cb400fc23c6f691c6 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 15 Jun 2023 23:37:04 +0200 Subject: [PATCH 1434/1517] Update contrib SHA (#3348) Fixes #3347 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1bdff92077..db3a931e6e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 2edd017c22edb4896e182c934bc199d716495ce6 + CONTRIB_REPO_SHA: a5ed4da478c4360fd6e24893f7574b150431b7ee # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when From 9d3d0f88445eefe040b6854964a99b4e66b88e89 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 22 Jun 2023 11:27:04 -0400 Subject: [PATCH 1435/1517] Upgrade opentelemetry-proto to 0.20 and regen (#3355) --- CHANGELOG.md | 2 + .../tests/test_metrics_encoder.py | 5 - .../collector/logs/v1/logs_service_pb2.py | 20 ++- .../collector/logs/v1/logs_service_pb2.pyi | 53 ++++++++ .../metrics/v1/metrics_service_pb2.py | 20 ++- .../metrics/v1/metrics_service_pb2.pyi | 53 ++++++++ .../collector/trace/v1/trace_service_pb2.py | 20 ++- .../collector/trace/v1/trace_service_pb2.pyi | 53 ++++++++ .../proto/common/v1/common_pb2.py | 20 +-- .../proto/common/v1/common_pb2.pyi | 36 ++--- .../opentelemetry/proto/logs/v1/logs_pb2.py | 40 ++---- .../opentelemetry/proto/logs/v1/logs_pb2.pyi | 125 +++++++----------- .../proto/metrics/v1/metrics_pb2.py | 88 ++++++------ .../proto/metrics/v1/metrics_pb2.pyi | 116 +++++----------- .../proto/resource/v1/resource_pb2.py | 4 +- .../proto/trace/v1/trace_config_pb2.py | 68 ---------- .../proto/trace/v1/trace_config_pb2.pyi | 123 ----------------- .../opentelemetry/proto/trace/v1/trace_pb2.py | 48 +++---- .../proto/trace/v1/trace_pb2.pyi | 107 +++------------ scripts/proto_codegen.sh | 2 +- 20 files changed, 388 insertions(+), 615 deletions(-) delete mode 100644 opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py delete mode 100644 opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f10920db7..f1332776ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add max_scale option to Exponential Bucket Histogram Aggregation [#3323](https://github.com/open-telemetry/opentelemetry-python/pull/3323)) - Use BoundedAttributes instead of raw dict to extract attributes from LogRecord and Support dropped_attributes_count in LogRecord ([#3310](https://github.com/open-telemetry/opentelemetry-python/pull/3310)) +- Upgrade opentelemetry-proto to 0.20 and regen + [#3355](https://github.com/open-telemetry/opentelemetry-python/pull/3355)) ## Version 1.18.0/0.39b0 (2023-05-04) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_metrics_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_metrics_encoder.py index f7c8ceb820..69e7cda39f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_metrics_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_metrics_encoder.py @@ -451,7 +451,6 @@ def test_encode_histogram(self): bucket_counts=[1, 4], explicit_bounds=[10.0, 20.0], exemplars=[], - flags=pb2.DataPointFlags.FLAG_NONE, max=18.0, min=8.0, ) @@ -554,7 +553,6 @@ def test_encode_multiple_scope_histogram(self): bucket_counts=[1, 4], explicit_bounds=[10.0, 20.0], exemplars=[], - flags=pb2.DataPointFlags.FLAG_NONE, max=18.0, min=8.0, ) @@ -590,7 +588,6 @@ def test_encode_multiple_scope_histogram(self): bucket_counts=[1, 4], explicit_bounds=[10.0, 20.0], exemplars=[], - flags=pb2.DataPointFlags.FLAG_NONE, max=18.0, min=8.0, ) @@ -633,7 +630,6 @@ def test_encode_multiple_scope_histogram(self): bucket_counts=[1, 4], explicit_bounds=[10.0, 20.0], exemplars=[], - flags=pb2.DataPointFlags.FLAG_NONE, max=18.0, min=8.0, ) @@ -676,7 +672,6 @@ def test_encode_multiple_scope_histogram(self): bucket_counts=[1, 4], explicit_bounds=[10.0, 20.0], exemplars=[], - flags=pb2.DataPointFlags.FLAG_NONE, max=18.0, min=8.0, ) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py index aef1671830..5e6ae0ef92 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py @@ -15,12 +15,13 @@ from opentelemetry.proto.logs.v1 import logs_pb2 as opentelemetry_dot_proto_dot_logs_dot_v1_dot_logs__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n8opentelemetry/proto/collector/logs/v1/logs_service.proto\x12%opentelemetry.proto.collector.logs.v1\x1a&opentelemetry/proto/logs/v1/logs.proto\"\\\n\x18\x45xportLogsServiceRequest\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\x1b\n\x19\x45xportLogsServiceResponse2\x9d\x01\n\x0bLogsService\x12\x8d\x01\n\x06\x45xport\x12?.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest\x1a@.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse\"\x00\x42p\n(io.opentelemetry.proto.collector.logs.v1B\x10LogsServiceProtoP\x01Z0go.opentelemetry.io/proto/otlp/collector/logs/v1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n8opentelemetry/proto/collector/logs/v1/logs_service.proto\x12%opentelemetry.proto.collector.logs.v1\x1a&opentelemetry/proto/logs/v1/logs.proto\"\\\n\x18\x45xportLogsServiceRequest\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"u\n\x19\x45xportLogsServiceResponse\x12X\n\x0fpartial_success\x18\x01 \x01(\x0b\x32?.opentelemetry.proto.collector.logs.v1.ExportLogsPartialSuccess\"O\n\x18\x45xportLogsPartialSuccess\x12\x1c\n\x14rejected_log_records\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\x9d\x01\n\x0bLogsService\x12\x8d\x01\n\x06\x45xport\x12?.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest\x1a@.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse\"\x00\x42\x98\x01\n(io.opentelemetry.proto.collector.logs.v1B\x10LogsServiceProtoP\x01Z0go.opentelemetry.io/proto/otlp/collector/logs/v1\xaa\x02%OpenTelemetry.Proto.Collector.Logs.V1b\x06proto3') _EXPORTLOGSSERVICEREQUEST = DESCRIPTOR.message_types_by_name['ExportLogsServiceRequest'] _EXPORTLOGSSERVICERESPONSE = DESCRIPTOR.message_types_by_name['ExportLogsServiceResponse'] +_EXPORTLOGSPARTIALSUCCESS = DESCRIPTOR.message_types_by_name['ExportLogsPartialSuccess'] ExportLogsServiceRequest = _reflection.GeneratedProtocolMessageType('ExportLogsServiceRequest', (_message.Message,), { 'DESCRIPTOR' : _EXPORTLOGSSERVICEREQUEST, '__module__' : 'opentelemetry.proto.collector.logs.v1.logs_service_pb2' @@ -35,15 +36,24 @@ }) _sym_db.RegisterMessage(ExportLogsServiceResponse) +ExportLogsPartialSuccess = _reflection.GeneratedProtocolMessageType('ExportLogsPartialSuccess', (_message.Message,), { + 'DESCRIPTOR' : _EXPORTLOGSPARTIALSUCCESS, + '__module__' : 'opentelemetry.proto.collector.logs.v1.logs_service_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.collector.logs.v1.ExportLogsPartialSuccess) + }) +_sym_db.RegisterMessage(ExportLogsPartialSuccess) + _LOGSSERVICE = DESCRIPTOR.services_by_name['LogsService'] if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n(io.opentelemetry.proto.collector.logs.v1B\020LogsServiceProtoP\001Z0go.opentelemetry.io/proto/otlp/collector/logs/v1' + DESCRIPTOR._serialized_options = b'\n(io.opentelemetry.proto.collector.logs.v1B\020LogsServiceProtoP\001Z0go.opentelemetry.io/proto/otlp/collector/logs/v1\252\002%OpenTelemetry.Proto.Collector.Logs.V1' _EXPORTLOGSSERVICEREQUEST._serialized_start=139 _EXPORTLOGSSERVICEREQUEST._serialized_end=231 _EXPORTLOGSSERVICERESPONSE._serialized_start=233 - _EXPORTLOGSSERVICERESPONSE._serialized_end=260 - _LOGSSERVICE._serialized_start=263 - _LOGSSERVICE._serialized_end=420 + _EXPORTLOGSSERVICERESPONSE._serialized_end=350 + _EXPORTLOGSPARTIALSUCCESS._serialized_start=352 + _EXPORTLOGSPARTIALSUCCESS._serialized_end=431 + _LOGSSERVICE._serialized_start=434 + _LOGSSERVICE._serialized_end=591 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi index 5940c192b2..cdf57e9fa1 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi @@ -33,6 +33,59 @@ global___ExportLogsServiceRequest = ExportLogsServiceRequest class ExportLogsServiceResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + PARTIAL_SUCCESS_FIELD_NUMBER: builtins.int + @property + def partial_success(self) -> global___ExportLogsPartialSuccess: + """The details of a partially successful export request. + + If the request is only partially accepted + (i.e. when the server accepts only parts of the data and rejects the rest) + the server MUST initialize the `partial_success` field and MUST + set the `rejected_` with the number of items it rejected. + + Servers MAY also make use of the `partial_success` field to convey + warnings/suggestions to senders even when the request was fully accepted. + In such cases, the `rejected_` MUST have a value of `0` and + the `error_message` MUST be non-empty. + + A `partial_success` message with an empty value (rejected_ = 0 and + `error_message` = "") is equivalent to it not being set/present. Senders + SHOULD interpret it the same way as in the full success case. + """ + pass def __init__(self, + *, + partial_success : typing.Optional[global___ExportLogsPartialSuccess] = ..., ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["partial_success",b"partial_success"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["partial_success",b"partial_success"]) -> None: ... global___ExportLogsServiceResponse = ExportLogsServiceResponse + +class ExportLogsPartialSuccess(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + REJECTED_LOG_RECORDS_FIELD_NUMBER: builtins.int + ERROR_MESSAGE_FIELD_NUMBER: builtins.int + rejected_log_records: builtins.int = ... + """The number of rejected log records. + + A `rejected_` field holding a `0` value indicates that the + request was fully accepted. + """ + + error_message: typing.Text = ... + """A developer-facing human-readable message in English. It should be used + either to explain why the server rejected parts of the data during a partial + success or to convey warnings/suggestions during a full success. The message + should offer guidance on how users can address such issues. + + error_message is an optional field. An error_message with an empty value + is equivalent to it not being set. + """ + + def __init__(self, + *, + rejected_log_records : builtins.int = ..., + error_message : typing.Text = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["error_message",b"error_message","rejected_log_records",b"rejected_log_records"]) -> None: ... +global___ExportLogsPartialSuccess = ExportLogsPartialSuccess diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py index ca026163b1..1d9021d702 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py @@ -15,12 +15,13 @@ from opentelemetry.proto.metrics.v1 import metrics_pb2 as opentelemetry_dot_proto_dot_metrics_dot_v1_dot_metrics__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n>opentelemetry/proto/collector/metrics/v1/metrics_service.proto\x12(opentelemetry.proto.collector.metrics.v1\x1a,opentelemetry/proto/metrics/v1/metrics.proto\"h\n\x1b\x45xportMetricsServiceRequest\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\x1e\n\x1c\x45xportMetricsServiceResponse2\xac\x01\n\x0eMetricsService\x12\x99\x01\n\x06\x45xport\x12\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest\x1a\x46.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse\"\x00\x42y\n+io.opentelemetry.proto.collector.metrics.v1B\x13MetricsServiceProtoP\x01Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n>opentelemetry/proto/collector/metrics/v1/metrics_service.proto\x12(opentelemetry.proto.collector.metrics.v1\x1a,opentelemetry/proto/metrics/v1/metrics.proto\"h\n\x1b\x45xportMetricsServiceRequest\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"~\n\x1c\x45xportMetricsServiceResponse\x12^\n\x0fpartial_success\x18\x01 \x01(\x0b\x32\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess\"R\n\x1b\x45xportMetricsPartialSuccess\x12\x1c\n\x14rejected_data_points\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xac\x01\n\x0eMetricsService\x12\x99\x01\n\x06\x45xport\x12\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest\x1a\x46.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse\"\x00\x42\xa4\x01\n+io.opentelemetry.proto.collector.metrics.v1B\x13MetricsServiceProtoP\x01Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1\xaa\x02(OpenTelemetry.Proto.Collector.Metrics.V1b\x06proto3') _EXPORTMETRICSSERVICEREQUEST = DESCRIPTOR.message_types_by_name['ExportMetricsServiceRequest'] _EXPORTMETRICSSERVICERESPONSE = DESCRIPTOR.message_types_by_name['ExportMetricsServiceResponse'] +_EXPORTMETRICSPARTIALSUCCESS = DESCRIPTOR.message_types_by_name['ExportMetricsPartialSuccess'] ExportMetricsServiceRequest = _reflection.GeneratedProtocolMessageType('ExportMetricsServiceRequest', (_message.Message,), { 'DESCRIPTOR' : _EXPORTMETRICSSERVICEREQUEST, '__module__' : 'opentelemetry.proto.collector.metrics.v1.metrics_service_pb2' @@ -35,15 +36,24 @@ }) _sym_db.RegisterMessage(ExportMetricsServiceResponse) +ExportMetricsPartialSuccess = _reflection.GeneratedProtocolMessageType('ExportMetricsPartialSuccess', (_message.Message,), { + 'DESCRIPTOR' : _EXPORTMETRICSPARTIALSUCCESS, + '__module__' : 'opentelemetry.proto.collector.metrics.v1.metrics_service_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess) + }) +_sym_db.RegisterMessage(ExportMetricsPartialSuccess) + _METRICSSERVICE = DESCRIPTOR.services_by_name['MetricsService'] if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n+io.opentelemetry.proto.collector.metrics.v1B\023MetricsServiceProtoP\001Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1' + DESCRIPTOR._serialized_options = b'\n+io.opentelemetry.proto.collector.metrics.v1B\023MetricsServiceProtoP\001Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1\252\002(OpenTelemetry.Proto.Collector.Metrics.V1' _EXPORTMETRICSSERVICEREQUEST._serialized_start=154 _EXPORTMETRICSSERVICEREQUEST._serialized_end=258 _EXPORTMETRICSSERVICERESPONSE._serialized_start=260 - _EXPORTMETRICSSERVICERESPONSE._serialized_end=290 - _METRICSSERVICE._serialized_start=293 - _METRICSSERVICE._serialized_end=465 + _EXPORTMETRICSSERVICERESPONSE._serialized_end=386 + _EXPORTMETRICSPARTIALSUCCESS._serialized_start=388 + _EXPORTMETRICSPARTIALSUCCESS._serialized_end=470 + _METRICSSERVICE._serialized_start=473 + _METRICSSERVICE._serialized_end=645 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi index 1acc1de3f3..ffd750bdf2 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi @@ -33,6 +33,59 @@ global___ExportMetricsServiceRequest = ExportMetricsServiceRequest class ExportMetricsServiceResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + PARTIAL_SUCCESS_FIELD_NUMBER: builtins.int + @property + def partial_success(self) -> global___ExportMetricsPartialSuccess: + """The details of a partially successful export request. + + If the request is only partially accepted + (i.e. when the server accepts only parts of the data and rejects the rest) + the server MUST initialize the `partial_success` field and MUST + set the `rejected_` with the number of items it rejected. + + Servers MAY also make use of the `partial_success` field to convey + warnings/suggestions to senders even when the request was fully accepted. + In such cases, the `rejected_` MUST have a value of `0` and + the `error_message` MUST be non-empty. + + A `partial_success` message with an empty value (rejected_ = 0 and + `error_message` = "") is equivalent to it not being set/present. Senders + SHOULD interpret it the same way as in the full success case. + """ + pass def __init__(self, + *, + partial_success : typing.Optional[global___ExportMetricsPartialSuccess] = ..., ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["partial_success",b"partial_success"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["partial_success",b"partial_success"]) -> None: ... global___ExportMetricsServiceResponse = ExportMetricsServiceResponse + +class ExportMetricsPartialSuccess(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + REJECTED_DATA_POINTS_FIELD_NUMBER: builtins.int + ERROR_MESSAGE_FIELD_NUMBER: builtins.int + rejected_data_points: builtins.int = ... + """The number of rejected data points. + + A `rejected_` field holding a `0` value indicates that the + request was fully accepted. + """ + + error_message: typing.Text = ... + """A developer-facing human-readable message in English. It should be used + either to explain why the server rejected parts of the data during a partial + success or to convey warnings/suggestions during a full success. The message + should offer guidance on how users can address such issues. + + error_message is an optional field. An error_message with an empty value + is equivalent to it not being set. + """ + + def __init__(self, + *, + rejected_data_points : builtins.int = ..., + error_message : typing.Text = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["error_message",b"error_message","rejected_data_points",b"rejected_data_points"]) -> None: ... +global___ExportMetricsPartialSuccess = ExportMetricsPartialSuccess diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py index d32dc61933..fff65da1b7 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py @@ -15,12 +15,13 @@ from opentelemetry.proto.trace.v1 import trace_pb2 as opentelemetry_dot_proto_dot_trace_dot_v1_dot_trace__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n:opentelemetry/proto/collector/trace/v1/trace_service.proto\x12&opentelemetry.proto.collector.trace.v1\x1a(opentelemetry/proto/trace/v1/trace.proto\"`\n\x19\x45xportTraceServiceRequest\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\x1c\n\x1a\x45xportTraceServiceResponse2\xa2\x01\n\x0cTraceService\x12\x91\x01\n\x06\x45xport\x12\x41.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\x1a\x42.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse\"\x00\x42s\n)io.opentelemetry.proto.collector.trace.v1B\x11TraceServiceProtoP\x01Z1go.opentelemetry.io/proto/otlp/collector/trace/v1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n:opentelemetry/proto/collector/trace/v1/trace_service.proto\x12&opentelemetry.proto.collector.trace.v1\x1a(opentelemetry/proto/trace/v1/trace.proto\"`\n\x19\x45xportTraceServiceRequest\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"x\n\x1a\x45xportTraceServiceResponse\x12Z\n\x0fpartial_success\x18\x01 \x01(\x0b\x32\x41.opentelemetry.proto.collector.trace.v1.ExportTracePartialSuccess\"J\n\x19\x45xportTracePartialSuccess\x12\x16\n\x0erejected_spans\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xa2\x01\n\x0cTraceService\x12\x91\x01\n\x06\x45xport\x12\x41.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\x1a\x42.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse\"\x00\x42\x9c\x01\n)io.opentelemetry.proto.collector.trace.v1B\x11TraceServiceProtoP\x01Z1go.opentelemetry.io/proto/otlp/collector/trace/v1\xaa\x02&OpenTelemetry.Proto.Collector.Trace.V1b\x06proto3') _EXPORTTRACESERVICEREQUEST = DESCRIPTOR.message_types_by_name['ExportTraceServiceRequest'] _EXPORTTRACESERVICERESPONSE = DESCRIPTOR.message_types_by_name['ExportTraceServiceResponse'] +_EXPORTTRACEPARTIALSUCCESS = DESCRIPTOR.message_types_by_name['ExportTracePartialSuccess'] ExportTraceServiceRequest = _reflection.GeneratedProtocolMessageType('ExportTraceServiceRequest', (_message.Message,), { 'DESCRIPTOR' : _EXPORTTRACESERVICEREQUEST, '__module__' : 'opentelemetry.proto.collector.trace.v1.trace_service_pb2' @@ -35,15 +36,24 @@ }) _sym_db.RegisterMessage(ExportTraceServiceResponse) +ExportTracePartialSuccess = _reflection.GeneratedProtocolMessageType('ExportTracePartialSuccess', (_message.Message,), { + 'DESCRIPTOR' : _EXPORTTRACEPARTIALSUCCESS, + '__module__' : 'opentelemetry.proto.collector.trace.v1.trace_service_pb2' + # @@protoc_insertion_point(class_scope:opentelemetry.proto.collector.trace.v1.ExportTracePartialSuccess) + }) +_sym_db.RegisterMessage(ExportTracePartialSuccess) + _TRACESERVICE = DESCRIPTOR.services_by_name['TraceService'] if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n)io.opentelemetry.proto.collector.trace.v1B\021TraceServiceProtoP\001Z1go.opentelemetry.io/proto/otlp/collector/trace/v1' + DESCRIPTOR._serialized_options = b'\n)io.opentelemetry.proto.collector.trace.v1B\021TraceServiceProtoP\001Z1go.opentelemetry.io/proto/otlp/collector/trace/v1\252\002&OpenTelemetry.Proto.Collector.Trace.V1' _EXPORTTRACESERVICEREQUEST._serialized_start=144 _EXPORTTRACESERVICEREQUEST._serialized_end=240 _EXPORTTRACESERVICERESPONSE._serialized_start=242 - _EXPORTTRACESERVICERESPONSE._serialized_end=270 - _TRACESERVICE._serialized_start=273 - _TRACESERVICE._serialized_end=435 + _EXPORTTRACESERVICERESPONSE._serialized_end=362 + _EXPORTTRACEPARTIALSUCCESS._serialized_start=364 + _EXPORTTRACEPARTIALSUCCESS._serialized_end=438 + _TRACESERVICE._serialized_start=441 + _TRACESERVICE._serialized_end=603 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi index 7ed93e76de..4e2d064ee7 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi @@ -33,6 +33,59 @@ global___ExportTraceServiceRequest = ExportTraceServiceRequest class ExportTraceServiceResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + PARTIAL_SUCCESS_FIELD_NUMBER: builtins.int + @property + def partial_success(self) -> global___ExportTracePartialSuccess: + """The details of a partially successful export request. + + If the request is only partially accepted + (i.e. when the server accepts only parts of the data and rejects the rest) + the server MUST initialize the `partial_success` field and MUST + set the `rejected_` with the number of items it rejected. + + Servers MAY also make use of the `partial_success` field to convey + warnings/suggestions to senders even when the request was fully accepted. + In such cases, the `rejected_` MUST have a value of `0` and + the `error_message` MUST be non-empty. + + A `partial_success` message with an empty value (rejected_ = 0 and + `error_message` = "") is equivalent to it not being set/present. Senders + SHOULD interpret it the same way as in the full success case. + """ + pass def __init__(self, + *, + partial_success : typing.Optional[global___ExportTracePartialSuccess] = ..., ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["partial_success",b"partial_success"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["partial_success",b"partial_success"]) -> None: ... global___ExportTraceServiceResponse = ExportTraceServiceResponse + +class ExportTracePartialSuccess(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + REJECTED_SPANS_FIELD_NUMBER: builtins.int + ERROR_MESSAGE_FIELD_NUMBER: builtins.int + rejected_spans: builtins.int = ... + """The number of rejected spans. + + A `rejected_` field holding a `0` value indicates that the + request was fully accepted. + """ + + error_message: typing.Text = ... + """A developer-facing human-readable message in English. It should be used + either to explain why the server rejected parts of the data during a partial + success or to convey warnings/suggestions during a full success. The message + should offer guidance on how users can address such issues. + + error_message is an optional field. An error_message with an empty value + is equivalent to it not being set. + """ + + def __init__(self, + *, + rejected_spans : builtins.int = ..., + error_message : typing.Text = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["error_message",b"error_message","rejected_spans",b"rejected_spans"]) -> None: ... +global___ExportTracePartialSuccess = ExportTracePartialSuccess diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py index a38431a589..bec37ab230 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\x8c\x02\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"O\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\";\n\x16InstrumentationLibrary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t:\x02\x18\x01\"5\n\x14InstrumentationScope\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\tB[\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z(go.opentelemetry.io/proto/otlp/common/v1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\x8c\x02\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"O\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"\x94\x01\n\x14InstrumentationScope\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\rB{\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z(go.opentelemetry.io/proto/otlp/common/v1\xaa\x02\x1dOpenTelemetry.Proto.Common.V1b\x06proto3') @@ -22,7 +22,6 @@ _ARRAYVALUE = DESCRIPTOR.message_types_by_name['ArrayValue'] _KEYVALUELIST = DESCRIPTOR.message_types_by_name['KeyValueList'] _KEYVALUE = DESCRIPTOR.message_types_by_name['KeyValue'] -_INSTRUMENTATIONLIBRARY = DESCRIPTOR.message_types_by_name['InstrumentationLibrary'] _INSTRUMENTATIONSCOPE = DESCRIPTOR.message_types_by_name['InstrumentationScope'] AnyValue = _reflection.GeneratedProtocolMessageType('AnyValue', (_message.Message,), { 'DESCRIPTOR' : _ANYVALUE, @@ -52,13 +51,6 @@ }) _sym_db.RegisterMessage(KeyValue) -InstrumentationLibrary = _reflection.GeneratedProtocolMessageType('InstrumentationLibrary', (_message.Message,), { - 'DESCRIPTOR' : _INSTRUMENTATIONLIBRARY, - '__module__' : 'opentelemetry.proto.common.v1.common_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.common.v1.InstrumentationLibrary) - }) -_sym_db.RegisterMessage(InstrumentationLibrary) - InstrumentationScope = _reflection.GeneratedProtocolMessageType('InstrumentationScope', (_message.Message,), { 'DESCRIPTOR' : _INSTRUMENTATIONSCOPE, '__module__' : 'opentelemetry.proto.common.v1.common_pb2' @@ -69,9 +61,7 @@ if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n io.opentelemetry.proto.common.v1B\013CommonProtoP\001Z(go.opentelemetry.io/proto/otlp/common/v1' - _INSTRUMENTATIONLIBRARY._options = None - _INSTRUMENTATIONLIBRARY._serialized_options = b'\030\001' + DESCRIPTOR._serialized_options = b'\n io.opentelemetry.proto.common.v1B\013CommonProtoP\001Z(go.opentelemetry.io/proto/otlp/common/v1\252\002\035OpenTelemetry.Proto.Common.V1' _ANYVALUE._serialized_start=78 _ANYVALUE._serialized_end=346 _ARRAYVALUE._serialized_start=348 @@ -80,8 +70,6 @@ _KEYVALUELIST._serialized_end=490 _KEYVALUE._serialized_start=492 _KEYVALUE._serialized_end=571 - _INSTRUMENTATIONLIBRARY._serialized_start=573 - _INSTRUMENTATIONLIBRARY._serialized_end=632 - _INSTRUMENTATIONSCOPE._serialized_start=634 - _INSTRUMENTATIONSCOPE._serialized_end=687 + _INSTRUMENTATIONSCOPE._serialized_start=574 + _INSTRUMENTATIONSCOPE._serialized_end=722 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi index 0d1ff4f098..304feec5ab 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi @@ -108,28 +108,6 @@ class KeyValue(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal["key",b"key","value",b"value"]) -> None: ... global___KeyValue = KeyValue -class InstrumentationLibrary(google.protobuf.message.Message): - """InstrumentationLibrary is a message representing the instrumentation library information - such as the fully qualified name and version. - InstrumentationLibrary is wire-compatible with InstrumentationScope for binary - Protobuf format. - This message is deprecated and will be removed on June 15, 2022. - """ - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - NAME_FIELD_NUMBER: builtins.int - VERSION_FIELD_NUMBER: builtins.int - name: typing.Text = ... - """An empty instrumentation library name means the name is unknown.""" - - version: typing.Text = ... - def __init__(self, - *, - name : typing.Text = ..., - version : typing.Text = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["name",b"name","version",b"version"]) -> None: ... -global___InstrumentationLibrary = InstrumentationLibrary - class InstrumentationScope(google.protobuf.message.Message): """InstrumentationScope is a message representing the instrumentation scope information such as the fully qualified name and version. @@ -137,14 +115,26 @@ class InstrumentationScope(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... NAME_FIELD_NUMBER: builtins.int VERSION_FIELD_NUMBER: builtins.int + ATTRIBUTES_FIELD_NUMBER: builtins.int + DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int name: typing.Text = ... """An empty instrumentation scope name means the name is unknown.""" version: typing.Text = ... + @property + def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___KeyValue]: + """Additional attributes that describe the scope. [Optional]. + Attribute keys MUST be unique (it is not allowed to have more than one + attribute with the same key). + """ + pass + dropped_attributes_count: builtins.int = ... def __init__(self, *, name : typing.Text = ..., version : typing.Text = ..., + attributes : typing.Optional[typing.Iterable[global___KeyValue]] = ..., + dropped_attributes_count : builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["name",b"name","version",b"version"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["attributes",b"attributes","dropped_attributes_count",b"dropped_attributes_count","name",b"name","version",b"version"]) -> None: ... global___InstrumentationScope = InstrumentationScope diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py index 3967fa967b..90b7187155 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py @@ -17,7 +17,7 @@ from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&opentelemetry/proto/logs/v1/logs.proto\x12\x1bopentelemetry.proto.logs.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"L\n\x08LogsData\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\xff\x01\n\x0cResourceLogs\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12:\n\nscope_logs\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.ScopeLogs\x12\x62\n\x1cinstrumentation_library_logs\x18\xe8\x07 \x03(\x0b\x32\x37.opentelemetry.proto.logs.v1.InstrumentationLibraryLogsB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xa0\x01\n\tScopeLogs\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc9\x01\n\x1aInstrumentationLibraryLogs\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\xef\x02\n\tLogRecord\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x1f\n\x17observed_time_unix_nano\x18\x0b \x01(\x06\x12\x44\n\x0fseverity_number\x18\x02 \x01(\x0e\x32+.opentelemetry.proto.logs.v1.SeverityNumber\x12\x15\n\rseverity_text\x18\x03 \x01(\t\x12\x35\n\x04\x62ody\x18\x05 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12;\n\nattributes\x18\x06 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x07 \x01(\r\x12\r\n\x05\x66lags\x18\x08 \x01(\x07\x12\x10\n\x08trace_id\x18\t \x01(\x0c\x12\x0f\n\x07span_id\x18\n \x01(\x0cJ\x04\x08\x04\x10\x05*\xc3\x05\n\x0eSeverityNumber\x12\x1f\n\x1bSEVERITY_NUMBER_UNSPECIFIED\x10\x00\x12\x19\n\x15SEVERITY_NUMBER_TRACE\x10\x01\x12\x1a\n\x16SEVERITY_NUMBER_TRACE2\x10\x02\x12\x1a\n\x16SEVERITY_NUMBER_TRACE3\x10\x03\x12\x1a\n\x16SEVERITY_NUMBER_TRACE4\x10\x04\x12\x19\n\x15SEVERITY_NUMBER_DEBUG\x10\x05\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG2\x10\x06\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG3\x10\x07\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG4\x10\x08\x12\x18\n\x14SEVERITY_NUMBER_INFO\x10\t\x12\x19\n\x15SEVERITY_NUMBER_INFO2\x10\n\x12\x19\n\x15SEVERITY_NUMBER_INFO3\x10\x0b\x12\x19\n\x15SEVERITY_NUMBER_INFO4\x10\x0c\x12\x18\n\x14SEVERITY_NUMBER_WARN\x10\r\x12\x19\n\x15SEVERITY_NUMBER_WARN2\x10\x0e\x12\x19\n\x15SEVERITY_NUMBER_WARN3\x10\x0f\x12\x19\n\x15SEVERITY_NUMBER_WARN4\x10\x10\x12\x19\n\x15SEVERITY_NUMBER_ERROR\x10\x11\x12\x1a\n\x16SEVERITY_NUMBER_ERROR2\x10\x12\x12\x1a\n\x16SEVERITY_NUMBER_ERROR3\x10\x13\x12\x1a\n\x16SEVERITY_NUMBER_ERROR4\x10\x14\x12\x19\n\x15SEVERITY_NUMBER_FATAL\x10\x15\x12\x1a\n\x16SEVERITY_NUMBER_FATAL2\x10\x16\x12\x1a\n\x16SEVERITY_NUMBER_FATAL3\x10\x17\x12\x1a\n\x16SEVERITY_NUMBER_FATAL4\x10\x18*X\n\x0eLogRecordFlags\x12\x1f\n\x1bLOG_RECORD_FLAG_UNSPECIFIED\x10\x00\x12%\n LOG_RECORD_FLAG_TRACE_FLAGS_MASK\x10\xff\x01\x42U\n\x1eio.opentelemetry.proto.logs.v1B\tLogsProtoP\x01Z&go.opentelemetry.io/proto/otlp/logs/v1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&opentelemetry/proto/logs/v1/logs.proto\x12\x1bopentelemetry.proto.logs.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"L\n\x08LogsData\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\xa3\x01\n\x0cResourceLogs\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12:\n\nscope_logs\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.ScopeLogs\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\xa0\x01\n\tScopeLogs\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xef\x02\n\tLogRecord\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x1f\n\x17observed_time_unix_nano\x18\x0b \x01(\x06\x12\x44\n\x0fseverity_number\x18\x02 \x01(\x0e\x32+.opentelemetry.proto.logs.v1.SeverityNumber\x12\x15\n\rseverity_text\x18\x03 \x01(\t\x12\x35\n\x04\x62ody\x18\x05 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12;\n\nattributes\x18\x06 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x07 \x01(\r\x12\r\n\x05\x66lags\x18\x08 \x01(\x07\x12\x10\n\x08trace_id\x18\t \x01(\x0c\x12\x0f\n\x07span_id\x18\n \x01(\x0cJ\x04\x08\x04\x10\x05*\xc3\x05\n\x0eSeverityNumber\x12\x1f\n\x1bSEVERITY_NUMBER_UNSPECIFIED\x10\x00\x12\x19\n\x15SEVERITY_NUMBER_TRACE\x10\x01\x12\x1a\n\x16SEVERITY_NUMBER_TRACE2\x10\x02\x12\x1a\n\x16SEVERITY_NUMBER_TRACE3\x10\x03\x12\x1a\n\x16SEVERITY_NUMBER_TRACE4\x10\x04\x12\x19\n\x15SEVERITY_NUMBER_DEBUG\x10\x05\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG2\x10\x06\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG3\x10\x07\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG4\x10\x08\x12\x18\n\x14SEVERITY_NUMBER_INFO\x10\t\x12\x19\n\x15SEVERITY_NUMBER_INFO2\x10\n\x12\x19\n\x15SEVERITY_NUMBER_INFO3\x10\x0b\x12\x19\n\x15SEVERITY_NUMBER_INFO4\x10\x0c\x12\x18\n\x14SEVERITY_NUMBER_WARN\x10\r\x12\x19\n\x15SEVERITY_NUMBER_WARN2\x10\x0e\x12\x19\n\x15SEVERITY_NUMBER_WARN3\x10\x0f\x12\x19\n\x15SEVERITY_NUMBER_WARN4\x10\x10\x12\x19\n\x15SEVERITY_NUMBER_ERROR\x10\x11\x12\x1a\n\x16SEVERITY_NUMBER_ERROR2\x10\x12\x12\x1a\n\x16SEVERITY_NUMBER_ERROR3\x10\x13\x12\x1a\n\x16SEVERITY_NUMBER_ERROR4\x10\x14\x12\x19\n\x15SEVERITY_NUMBER_FATAL\x10\x15\x12\x1a\n\x16SEVERITY_NUMBER_FATAL2\x10\x16\x12\x1a\n\x16SEVERITY_NUMBER_FATAL3\x10\x17\x12\x1a\n\x16SEVERITY_NUMBER_FATAL4\x10\x18*Y\n\x0eLogRecordFlags\x12\x1f\n\x1bLOG_RECORD_FLAGS_DO_NOT_USE\x10\x00\x12&\n!LOG_RECORD_FLAGS_TRACE_FLAGS_MASK\x10\xff\x01\x42s\n\x1eio.opentelemetry.proto.logs.v1B\tLogsProtoP\x01Z&go.opentelemetry.io/proto/otlp/logs/v1\xaa\x02\x1bOpenTelemetry.Proto.Logs.V1b\x06proto3') _SEVERITYNUMBER = DESCRIPTOR.enum_types_by_name['SeverityNumber'] SeverityNumber = enum_type_wrapper.EnumTypeWrapper(_SEVERITYNUMBER) @@ -48,14 +48,13 @@ SEVERITY_NUMBER_FATAL2 = 22 SEVERITY_NUMBER_FATAL3 = 23 SEVERITY_NUMBER_FATAL4 = 24 -LOG_RECORD_FLAG_UNSPECIFIED = 0 -LOG_RECORD_FLAG_TRACE_FLAGS_MASK = 255 +LOG_RECORD_FLAGS_DO_NOT_USE = 0 +LOG_RECORD_FLAGS_TRACE_FLAGS_MASK = 255 _LOGSDATA = DESCRIPTOR.message_types_by_name['LogsData'] _RESOURCELOGS = DESCRIPTOR.message_types_by_name['ResourceLogs'] _SCOPELOGS = DESCRIPTOR.message_types_by_name['ScopeLogs'] -_INSTRUMENTATIONLIBRARYLOGS = DESCRIPTOR.message_types_by_name['InstrumentationLibraryLogs'] _LOGRECORD = DESCRIPTOR.message_types_by_name['LogRecord'] LogsData = _reflection.GeneratedProtocolMessageType('LogsData', (_message.Message,), { 'DESCRIPTOR' : _LOGSDATA, @@ -78,13 +77,6 @@ }) _sym_db.RegisterMessage(ScopeLogs) -InstrumentationLibraryLogs = _reflection.GeneratedProtocolMessageType('InstrumentationLibraryLogs', (_message.Message,), { - 'DESCRIPTOR' : _INSTRUMENTATIONLIBRARYLOGS, - '__module__' : 'opentelemetry.proto.logs.v1.logs_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.logs.v1.InstrumentationLibraryLogs) - }) -_sym_db.RegisterMessage(InstrumentationLibraryLogs) - LogRecord = _reflection.GeneratedProtocolMessageType('LogRecord', (_message.Message,), { 'DESCRIPTOR' : _LOGRECORD, '__module__' : 'opentelemetry.proto.logs.v1.logs_pb2' @@ -95,23 +87,17 @@ if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\036io.opentelemetry.proto.logs.v1B\tLogsProtoP\001Z&go.opentelemetry.io/proto/otlp/logs/v1' - _RESOURCELOGS.fields_by_name['instrumentation_library_logs']._options = None - _RESOURCELOGS.fields_by_name['instrumentation_library_logs']._serialized_options = b'\030\001' - _INSTRUMENTATIONLIBRARYLOGS._options = None - _INSTRUMENTATIONLIBRARYLOGS._serialized_options = b'\030\001' - _SEVERITYNUMBER._serialized_start=1237 - _SEVERITYNUMBER._serialized_end=1944 - _LOGRECORDFLAGS._serialized_start=1946 - _LOGRECORDFLAGS._serialized_end=2034 + DESCRIPTOR._serialized_options = b'\n\036io.opentelemetry.proto.logs.v1B\tLogsProtoP\001Z&go.opentelemetry.io/proto/otlp/logs/v1\252\002\033OpenTelemetry.Proto.Logs.V1' + _SEVERITYNUMBER._serialized_start=941 + _SEVERITYNUMBER._serialized_end=1648 + _LOGRECORDFLAGS._serialized_start=1650 + _LOGRECORDFLAGS._serialized_end=1739 _LOGSDATA._serialized_start=163 _LOGSDATA._serialized_end=239 _RESOURCELOGS._serialized_start=242 - _RESOURCELOGS._serialized_end=497 - _SCOPELOGS._serialized_start=500 - _SCOPELOGS._serialized_end=660 - _INSTRUMENTATIONLIBRARYLOGS._serialized_start=663 - _INSTRUMENTATIONLIBRARYLOGS._serialized_end=864 - _LOGRECORD._serialized_start=867 - _LOGRECORD._serialized_end=1234 + _RESOURCELOGS._serialized_end=405 + _SCOPELOGS._serialized_start=408 + _SCOPELOGS._serialized_end=568 + _LOGRECORD._serialized_start=571 + _LOGRECORD._serialized_end=938 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi index db4da2afd1..98b8974390 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi @@ -80,17 +80,34 @@ global___SeverityNumber = SeverityNumber class LogRecordFlags(_LogRecordFlags, metaclass=_LogRecordFlagsEnumTypeWrapper): - """Masks for LogRecord.flags field.""" + """LogRecordFlags is defined as a protobuf 'uint32' type and is to be used as + bit-fields. Each non-zero value defined in this enum is a bit-mask. + To extract the bit-field, for example, use an expression like: + + (logRecord.flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK) + """ pass class _LogRecordFlags: V = typing.NewType('V', builtins.int) class _LogRecordFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_LogRecordFlags.V], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... - LOG_RECORD_FLAG_UNSPECIFIED = LogRecordFlags.V(0) - LOG_RECORD_FLAG_TRACE_FLAGS_MASK = LogRecordFlags.V(255) + LOG_RECORD_FLAGS_DO_NOT_USE = LogRecordFlags.V(0) + """The zero value for the enum. Should not be used for comparisons. + Instead use bitwise "and" with the appropriate mask as shown above. + """ + + LOG_RECORD_FLAGS_TRACE_FLAGS_MASK = LogRecordFlags.V(255) + """Bits 0-7 are used for trace flags.""" + + +LOG_RECORD_FLAGS_DO_NOT_USE = LogRecordFlags.V(0) +"""The zero value for the enum. Should not be used for comparisons. +Instead use bitwise "and" with the appropriate mask as shown above. +""" + +LOG_RECORD_FLAGS_TRACE_FLAGS_MASK = LogRecordFlags.V(255) +"""Bits 0-7 are used for trace flags.""" -LOG_RECORD_FLAG_UNSPECIFIED = LogRecordFlags.V(0) -LOG_RECORD_FLAG_TRACE_FLAGS_MASK = LogRecordFlags.V(255) global___LogRecordFlags = LogRecordFlags @@ -129,7 +146,6 @@ class ResourceLogs(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... RESOURCE_FIELD_NUMBER: builtins.int SCOPE_LOGS_FIELD_NUMBER: builtins.int - INSTRUMENTATION_LIBRARY_LOGS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: @@ -141,37 +157,6 @@ class ResourceLogs(google.protobuf.message.Message): def scope_logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeLogs]: """A list of ScopeLogs that originate from a resource.""" pass - @property - def instrumentation_library_logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibraryLogs]: - """A list of InstrumentationLibraryLogs that originate from a resource. - This field is deprecated and will be removed after grace period expires on June 15, 2022. - - During the grace period the following rules SHOULD be followed: - - For Binary Protobufs - ==================== - Binary Protobuf senders SHOULD NOT set instrumentation_library_logs. Instead - scope_logs SHOULD be set. - - Binary Protobuf receivers SHOULD check if instrumentation_library_logs is set - and scope_logs is not set then the value in instrumentation_library_logs - SHOULD be used instead by converting InstrumentationLibraryLogs into ScopeLogs. - If scope_logs is set then instrumentation_library_logs SHOULD be ignored. - - For JSON - ======== - JSON senders that set instrumentation_library_logs field MAY also set - scope_logs to carry the same logs, essentially double-publishing the same data. - Such double-publishing MAY be controlled by a user-settable option. - If double-publishing is not used then the senders SHOULD set scope_logs and - SHOULD NOT set instrumentation_library_logs. - - JSON receivers SHOULD check if instrumentation_library_logs is set and - scope_logs is not set then the value in instrumentation_library_logs - SHOULD be used instead by converting InstrumentationLibraryLogs into ScopeLogs. - If scope_logs is set then instrumentation_library_logs field SHOULD be ignored. - """ - pass schema_url: typing.Text = ... """This schema_url applies to the data in the "resource" field. It does not apply to the data in the "scope_logs" field which have their own schema_url field. @@ -181,11 +166,10 @@ class ResourceLogs(google.protobuf.message.Message): *, resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., scope_logs : typing.Optional[typing.Iterable[global___ScopeLogs]] = ..., - instrumentation_library_logs : typing.Optional[typing.Iterable[global___InstrumentationLibraryLogs]] = ..., schema_url : typing.Text = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["resource",b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library_logs",b"instrumentation_library_logs","resource",b"resource","schema_url",b"schema_url","scope_logs",b"scope_logs"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["resource",b"resource","schema_url",b"schema_url","scope_logs",b"scope_logs"]) -> None: ... global___ResourceLogs = ResourceLogs class ScopeLogs(google.protobuf.message.Message): @@ -218,40 +202,6 @@ class ScopeLogs(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal["log_records",b"log_records","schema_url",b"schema_url","scope",b"scope"]) -> None: ... global___ScopeLogs = ScopeLogs -class InstrumentationLibraryLogs(google.protobuf.message.Message): - """A collection of Logs produced by an InstrumentationLibrary. - InstrumentationLibraryLogs is wire-compatible with ScopeLogs for binary - Protobuf format. - This message is deprecated and will be removed on June 15, 2022. - """ - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int - LOG_RECORDS_FIELD_NUMBER: builtins.int - SCHEMA_URL_FIELD_NUMBER: builtins.int - @property - def instrumentation_library(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: - """The instrumentation library information for the logs in this message. - Semantically when InstrumentationLibrary isn't set, it is equivalent with - an empty instrumentation library name (unknown). - """ - pass - @property - def log_records(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___LogRecord]: - """A list of logs that originate from an instrumentation library.""" - pass - schema_url: typing.Text = ... - """This schema_url applies to all logs in the "logs" field.""" - - def __init__(self, - *, - instrumentation_library : typing.Optional[opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary] = ..., - log_records : typing.Optional[typing.Iterable[global___LogRecord]] = ..., - schema_url : typing.Text = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library","log_records",b"log_records","schema_url",b"schema_url"]) -> None: ... -global___InstrumentationLibraryLogs = InstrumentationLibraryLogs - class LogRecord(google.protobuf.message.Message): """A log record according to OpenTelemetry Log Data Model: https://github.com/open-telemetry/oteps/blob/main/text/logs/0097-log-data-model.md @@ -321,21 +271,36 @@ class LogRecord(google.protobuf.message.Message): defined in W3C Trace Context specification. 24 most significant bits are reserved and must be set to 0. Readers must not assume that 24 most significant bits will be zero and must correctly mask the bits when reading 8-bit trace flag (use - flags & TRACE_FLAGS_MASK). [Optional]. + flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK). [Optional]. """ trace_id: builtins.bytes = ... """A unique identifier for a trace. All logs from the same trace share - the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes - is considered invalid. Can be set for logs that are part of request processing - and have an assigned trace id. [Optional]. + the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes OR + of length other than 16 bytes is considered invalid (empty string in OTLP/JSON + is zero-length and thus is also invalid). + + This field is optional. + + The receivers SHOULD assume that the log record is not associated with a + trace if any of the following is true: + - the field is not present, + - the field contains an invalid value. """ span_id: builtins.bytes = ... """A unique identifier for a span within a trace, assigned when the span - is created. The ID is an 8-byte array. An ID with all zeroes is considered - invalid. Can be set for logs that are part of a particular processing span. - If span_id is present trace_id SHOULD be also present. [Optional]. + is created. The ID is an 8-byte array. An ID with all zeroes OR of length + other than 8 bytes is considered invalid (empty string in OTLP/JSON + is zero-length and thus is also invalid). + + This field is optional. If the sender specifies a valid span_id then it SHOULD also + specify a valid trace_id. + + The receivers SHOULD assume that the log record is not associated with a + span if any of the following is true: + - the field is not present, + - the field contains an invalid value. """ def __init__(self, diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py index e94d087a45..4b938c2146 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py @@ -17,7 +17,7 @@ from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\x94\x02\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x43\n\rscope_metrics\x18\x02 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.ScopeMetrics\x12k\n\x1finstrumentation_library_metrics\x18\xe8\x07 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetricsB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x9f\x01\n\x0cScopeMetrics\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc8\x01\n\x1dInstrumentationLibraryMetrics\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\x92\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xe6\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\r\x12\x10\n\x03min\x18\x0b \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\x0c \x01(\x01H\x02\x88\x01\x01\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_maxJ\x04\x08\x01\x10\x02\"\xb5\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\x10\n\x03min\x18\x0c \x01(\x01H\x00\x88\x01\x01\x12\x10\n\x03max\x18\r \x01(\x01H\x01\x88\x01\x01\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\x42\x06\n\x04_minB\x06\n\x04_max\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*;\n\x0e\x44\x61taPointFlags\x12\r\n\tFLAG_NONE\x10\x00\x12\x1a\n\x16\x46LAG_NO_RECORDED_VALUE\x10\x01\x42^\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z)go.opentelemetry.io/proto/otlp/metrics/v1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\xaf\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x43\n\rscope_metrics\x18\x02 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.ScopeMetrics\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\x9f\x01\n\x0cScopeMetrics\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x92\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xe6\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\r\x12\x10\n\x03min\x18\x0b \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\x0c \x01(\x01H\x02\x88\x01\x01\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_maxJ\x04\x08\x01\x10\x02\"\xda\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\x10\n\x03min\x18\x0c \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\r \x01(\x01H\x02\x88\x01\x01\x12\x16\n\x0ezero_threshold\x18\x0e \x01(\x01\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_max\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*^\n\x0e\x44\x61taPointFlags\x12\x1f\n\x1b\x44\x41TA_POINT_FLAGS_DO_NOT_USE\x10\x00\x12+\n\'DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK\x10\x01\x42\x7f\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z)go.opentelemetry.io/proto/otlp/metrics/v1\xaa\x02\x1eOpenTelemetry.Proto.Metrics.V1b\x06proto3') _AGGREGATIONTEMPORALITY = DESCRIPTOR.enum_types_by_name['AggregationTemporality'] AggregationTemporality = enum_type_wrapper.EnumTypeWrapper(_AGGREGATIONTEMPORALITY) @@ -26,14 +26,13 @@ AGGREGATION_TEMPORALITY_UNSPECIFIED = 0 AGGREGATION_TEMPORALITY_DELTA = 1 AGGREGATION_TEMPORALITY_CUMULATIVE = 2 -FLAG_NONE = 0 -FLAG_NO_RECORDED_VALUE = 1 +DATA_POINT_FLAGS_DO_NOT_USE = 0 +DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK = 1 _METRICSDATA = DESCRIPTOR.message_types_by_name['MetricsData'] _RESOURCEMETRICS = DESCRIPTOR.message_types_by_name['ResourceMetrics'] _SCOPEMETRICS = DESCRIPTOR.message_types_by_name['ScopeMetrics'] -_INSTRUMENTATIONLIBRARYMETRICS = DESCRIPTOR.message_types_by_name['InstrumentationLibraryMetrics'] _METRIC = DESCRIPTOR.message_types_by_name['Metric'] _GAUGE = DESCRIPTOR.message_types_by_name['Gauge'] _SUM = DESCRIPTOR.message_types_by_name['Sum'] @@ -68,13 +67,6 @@ }) _sym_db.RegisterMessage(ScopeMetrics) -InstrumentationLibraryMetrics = _reflection.GeneratedProtocolMessageType('InstrumentationLibraryMetrics', (_message.Message,), { - 'DESCRIPTOR' : _INSTRUMENTATIONLIBRARYMETRICS, - '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics) - }) -_sym_db.RegisterMessage(InstrumentationLibraryMetrics) - Metric = _reflection.GeneratedProtocolMessageType('Metric', (_message.Message,), { 'DESCRIPTOR' : _METRIC, '__module__' : 'opentelemetry.proto.metrics.v1.metrics_pb2' @@ -171,47 +163,41 @@ if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z)go.opentelemetry.io/proto/otlp/metrics/v1' - _RESOURCEMETRICS.fields_by_name['instrumentation_library_metrics']._options = None - _RESOURCEMETRICS.fields_by_name['instrumentation_library_metrics']._serialized_options = b'\030\001' - _INSTRUMENTATIONLIBRARYMETRICS._options = None - _INSTRUMENTATIONLIBRARYMETRICS._serialized_options = b'\030\001' - _AGGREGATIONTEMPORALITY._serialized_start=3754 - _AGGREGATIONTEMPORALITY._serialized_end=3894 - _DATAPOINTFLAGS._serialized_start=3896 - _DATAPOINTFLAGS._serialized_end=3955 + DESCRIPTOR._serialized_options = b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z)go.opentelemetry.io/proto/otlp/metrics/v1\252\002\036OpenTelemetry.Proto.Metrics.V1' + _AGGREGATIONTEMPORALITY._serialized_start=3487 + _AGGREGATIONTEMPORALITY._serialized_end=3627 + _DATAPOINTFLAGS._serialized_start=3629 + _DATAPOINTFLAGS._serialized_end=3723 _METRICSDATA._serialized_start=172 _METRICSDATA._serialized_end=260 _RESOURCEMETRICS._serialized_start=263 - _RESOURCEMETRICS._serialized_end=539 - _SCOPEMETRICS._serialized_start=542 - _SCOPEMETRICS._serialized_end=701 - _INSTRUMENTATIONLIBRARYMETRICS._serialized_start=704 - _INSTRUMENTATIONLIBRARYMETRICS._serialized_end=904 - _METRIC._serialized_start=907 - _METRIC._serialized_end=1309 - _GAUGE._serialized_start=1311 - _GAUGE._serialized_end=1388 - _SUM._serialized_start=1391 - _SUM._serialized_end=1577 - _HISTOGRAM._serialized_start=1580 - _HISTOGRAM._serialized_end=1753 - _EXPONENTIALHISTOGRAM._serialized_start=1756 - _EXPONENTIALHISTOGRAM._serialized_end=1951 - _SUMMARY._serialized_start=1953 - _SUMMARY._serialized_end=2033 - _NUMBERDATAPOINT._serialized_start=2036 - _NUMBERDATAPOINT._serialized_end=2298 - _HISTOGRAMDATAPOINT._serialized_start=2301 - _HISTOGRAMDATAPOINT._serialized_end=2659 - _EXPONENTIALHISTOGRAMDATAPOINT._serialized_start=2662 - _EXPONENTIALHISTOGRAMDATAPOINT._serialized_end=3227 - _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS._serialized_start=3163 - _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS._serialized_end=3211 - _SUMMARYDATAPOINT._serialized_start=3230 - _SUMMARYDATAPOINT._serialized_end=3555 - _SUMMARYDATAPOINT_VALUEATQUANTILE._serialized_start=3499 - _SUMMARYDATAPOINT_VALUEATQUANTILE._serialized_end=3549 - _EXEMPLAR._serialized_start=3558 - _EXEMPLAR._serialized_end=3751 + _RESOURCEMETRICS._serialized_end=438 + _SCOPEMETRICS._serialized_start=441 + _SCOPEMETRICS._serialized_end=600 + _METRIC._serialized_start=603 + _METRIC._serialized_end=1005 + _GAUGE._serialized_start=1007 + _GAUGE._serialized_end=1084 + _SUM._serialized_start=1087 + _SUM._serialized_end=1273 + _HISTOGRAM._serialized_start=1276 + _HISTOGRAM._serialized_end=1449 + _EXPONENTIALHISTOGRAM._serialized_start=1452 + _EXPONENTIALHISTOGRAM._serialized_end=1647 + _SUMMARY._serialized_start=1649 + _SUMMARY._serialized_end=1729 + _NUMBERDATAPOINT._serialized_start=1732 + _NUMBERDATAPOINT._serialized_end=1994 + _HISTOGRAMDATAPOINT._serialized_start=1997 + _HISTOGRAMDATAPOINT._serialized_end=2355 + _EXPONENTIALHISTOGRAMDATAPOINT._serialized_start=2358 + _EXPONENTIALHISTOGRAMDATAPOINT._serialized_end=2960 + _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS._serialized_start=2888 + _EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS._serialized_end=2936 + _SUMMARYDATAPOINT._serialized_start=2963 + _SUMMARYDATAPOINT._serialized_end=3288 + _SUMMARYDATAPOINT_VALUEATQUANTILE._serialized_start=3232 + _SUMMARYDATAPOINT_VALUEATQUANTILE._serialized_end=3282 + _EXEMPLAR._serialized_start=3291 + _EXEMPLAR._serialized_end=3484 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi index ed1291a757..ccbbb35cfb 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi @@ -170,23 +170,31 @@ class DataPointFlags(_DataPointFlags, metaclass=_DataPointFlagsEnumTypeWrapper): enum is a bit-mask. To test the presence of a single flag in the flags of a data point, for example, use an expression like: - (point.flags & FLAG_NO_RECORDED_VALUE) == FLAG_NO_RECORDED_VALUE + (point.flags & DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK) == DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK """ pass class _DataPointFlags: V = typing.NewType('V', builtins.int) class _DataPointFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_DataPointFlags.V], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... - FLAG_NONE = DataPointFlags.V(0) - FLAG_NO_RECORDED_VALUE = DataPointFlags.V(1) + DATA_POINT_FLAGS_DO_NOT_USE = DataPointFlags.V(0) + """The zero value for the enum. Should not be used for comparisons. + Instead use bitwise "and" with the appropriate mask as shown above. + """ + + DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK = DataPointFlags.V(1) """This DataPoint is valid but has no recorded value. This value SHOULD be used to reflect explicitly missing data in a series, as for an equivalent to the Prometheus "staleness marker". """ -FLAG_NONE = DataPointFlags.V(0) -FLAG_NO_RECORDED_VALUE = DataPointFlags.V(1) +DATA_POINT_FLAGS_DO_NOT_USE = DataPointFlags.V(0) +"""The zero value for the enum. Should not be used for comparisons. +Instead use bitwise "and" with the appropriate mask as shown above. +""" + +DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK = DataPointFlags.V(1) """This DataPoint is valid but has no recorded value. This value SHOULD be used to reflect explicitly missing data in a series, as for an equivalent to the Prometheus "staleness marker". @@ -230,7 +238,6 @@ class ResourceMetrics(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... RESOURCE_FIELD_NUMBER: builtins.int SCOPE_METRICS_FIELD_NUMBER: builtins.int - INSTRUMENTATION_LIBRARY_METRICS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: @@ -242,37 +249,6 @@ class ResourceMetrics(google.protobuf.message.Message): def scope_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeMetrics]: """A list of metrics that originate from a resource.""" pass - @property - def instrumentation_library_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibraryMetrics]: - """A list of InstrumentationLibraryMetrics that originate from a resource. - This field is deprecated and will be removed after grace period expires on June 15, 2022. - - During the grace period the following rules SHOULD be followed: - - For Binary Protobufs - ==================== - Binary Protobuf senders SHOULD NOT set instrumentation_library_metrics. Instead - scope_metrics SHOULD be set. - - Binary Protobuf receivers SHOULD check if instrumentation_library_metrics is set - and scope_metrics is not set then the value in instrumentation_library_metrics - SHOULD be used instead by converting InstrumentationLibraryMetrics into ScopeMetrics. - If scope_metrics is set then instrumentation_library_metrics SHOULD be ignored. - - For JSON - ======== - JSON senders that set instrumentation_library_metrics field MAY also set - scope_metrics to carry the same metrics, essentially double-publishing the same data. - Such double-publishing MAY be controlled by a user-settable option. - If double-publishing is not used then the senders SHOULD set scope_metrics and - SHOULD NOT set instrumentation_library_metrics. - - JSON receivers SHOULD check if instrumentation_library_metrics is set and - scope_metrics is not set then the value in instrumentation_library_metrics - SHOULD be used instead by converting InstrumentationLibraryMetrics into ScopeMetrics. - If scope_metrics is set then instrumentation_library_metrics field SHOULD be ignored. - """ - pass schema_url: typing.Text = ... """This schema_url applies to the data in the "resource" field. It does not apply to the data in the "scope_metrics" field which have their own schema_url field. @@ -282,11 +258,10 @@ class ResourceMetrics(google.protobuf.message.Message): *, resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., scope_metrics : typing.Optional[typing.Iterable[global___ScopeMetrics]] = ..., - instrumentation_library_metrics : typing.Optional[typing.Iterable[global___InstrumentationLibraryMetrics]] = ..., schema_url : typing.Text = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["resource",b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library_metrics",b"instrumentation_library_metrics","resource",b"resource","schema_url",b"schema_url","scope_metrics",b"scope_metrics"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["resource",b"resource","schema_url",b"schema_url","scope_metrics",b"scope_metrics"]) -> None: ... global___ResourceMetrics = ResourceMetrics class ScopeMetrics(google.protobuf.message.Message): @@ -319,45 +294,11 @@ class ScopeMetrics(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal["metrics",b"metrics","schema_url",b"schema_url","scope",b"scope"]) -> None: ... global___ScopeMetrics = ScopeMetrics -class InstrumentationLibraryMetrics(google.protobuf.message.Message): - """A collection of Metrics produced by an InstrumentationLibrary. - InstrumentationLibraryMetrics is wire-compatible with ScopeMetrics for binary - Protobuf format. - This message is deprecated and will be removed on June 15, 2022. - """ - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int - METRICS_FIELD_NUMBER: builtins.int - SCHEMA_URL_FIELD_NUMBER: builtins.int - @property - def instrumentation_library(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: - """The instrumentation library information for the metrics in this message. - Semantically when InstrumentationLibrary isn't set, it is equivalent with - an empty instrumentation library name (unknown). - """ - pass - @property - def metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Metric]: - """A list of metrics that originate from an instrumentation library.""" - pass - schema_url: typing.Text = ... - """This schema_url applies to all metrics in the "metrics" field.""" - - def __init__(self, - *, - instrumentation_library : typing.Optional[opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary] = ..., - metrics : typing.Optional[typing.Iterable[global___Metric]] = ..., - schema_url : typing.Text = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library","metrics",b"metrics","schema_url",b"schema_url"]) -> None: ... -global___InstrumentationLibraryMetrics = InstrumentationLibraryMetrics - class Metric(google.protobuf.message.Message): """Defines a Metric which has one or more timeseries. The following is a brief summary of the Metric data model. For more details, see: - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md The data model and relation between entities is shown in the @@ -816,9 +757,9 @@ class ExponentialHistogramDataPoint(google.protobuf.message.Message): @property def bucket_counts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: - """Count is an array of counts, where count[i] carries the count - of the bucket at index (offset+i). count[i] is the count of - values greater than or equal to base^(offset+i) and less than + """bucket_counts is an array of count values, where bucket_counts[i] carries + the count of the bucket at index (offset+i). bucket_counts[i] is the count + of values greater than base^(offset+i) and less than or equal to base^(offset+i+1). Note: By contrast, the explicit HistogramDataPoint uses @@ -847,6 +788,7 @@ class ExponentialHistogramDataPoint(google.protobuf.message.Message): EXEMPLARS_FIELD_NUMBER: builtins.int MIN_FIELD_NUMBER: builtins.int MAX_FIELD_NUMBER: builtins.int + ZERO_THRESHOLD_FIELD_NUMBER: builtins.int @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that uniquely identify the timeseries from @@ -894,8 +836,8 @@ class ExponentialHistogramDataPoint(google.protobuf.message.Message): base = (2^(2^-scale)) The histogram bucket identified by `index`, a signed integer, - contains values that are greater than or equal to (base^index) and - less than (base^(index+1)). + contains values that are greater than (base^index) and + less than or equal to (base^(index+1)). The positive and negative ranges of the histogram are expressed separately. Negative values are mapped by their absolute value @@ -941,6 +883,15 @@ class ExponentialHistogramDataPoint(google.protobuf.message.Message): max: builtins.float = ... """max is the maximum value over (start_time, end_time].""" + zero_threshold: builtins.float = ... + """ZeroThreshold may be optionally set to convey the width of the zero + region. Where the zero region is defined as the closed interval + [-ZeroThreshold, ZeroThreshold]. + When ZeroThreshold is 0, zero count bucket stores values that cannot be + expressed using the standard exponential formula as well as values that + have been rounded to zero. + """ + def __init__(self, *, attributes : typing.Optional[typing.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue]] = ..., @@ -956,13 +907,16 @@ class ExponentialHistogramDataPoint(google.protobuf.message.Message): exemplars : typing.Optional[typing.Iterable[global___Exemplar]] = ..., min : builtins.float = ..., max : builtins.float = ..., + zero_threshold : builtins.float = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_max",b"_max","_min",b"_min","max",b"max","min",b"min","negative",b"negative","positive",b"positive"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_max",b"_max","_min",b"_min","attributes",b"attributes","count",b"count","exemplars",b"exemplars","flags",b"flags","max",b"max","min",b"min","negative",b"negative","positive",b"positive","scale",b"scale","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano","zero_count",b"zero_count"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["_max",b"_max","_min",b"_min","_sum",b"_sum","max",b"max","min",b"min","negative",b"negative","positive",b"positive","sum",b"sum"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["_max",b"_max","_min",b"_min","_sum",b"_sum","attributes",b"attributes","count",b"count","exemplars",b"exemplars","flags",b"flags","max",b"max","min",b"min","negative",b"negative","positive",b"positive","scale",b"scale","start_time_unix_nano",b"start_time_unix_nano","sum",b"sum","time_unix_nano",b"time_unix_nano","zero_count",b"zero_count","zero_threshold",b"zero_threshold"]) -> None: ... @typing.overload def WhichOneof(self, oneof_group: typing_extensions.Literal["_max",b"_max"]) -> typing.Optional[typing_extensions.Literal["max"]]: ... @typing.overload def WhichOneof(self, oneof_group: typing_extensions.Literal["_min",b"_min"]) -> typing.Optional[typing_extensions.Literal["min"]]: ... + @typing.overload + def WhichOneof(self, oneof_group: typing_extensions.Literal["_sum",b"_sum"]) -> typing.Optional[typing_extensions.Literal["sum"]]: ... global___ExponentialHistogramDataPoint = ExponentialHistogramDataPoint class SummaryDataPoint(google.protobuf.message.Message): diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py index 2c31f9a3fa..728e9114dc 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py @@ -15,7 +15,7 @@ from opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"i\n\x08Resource\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\rBa\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z*go.opentelemetry.io/proto/otlp/resource/v1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"i\n\x08Resource\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\rB\x83\x01\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z*go.opentelemetry.io/proto/otlp/resource/v1\xaa\x02\x1fOpenTelemetry.Proto.Resource.V1b\x06proto3') @@ -30,7 +30,7 @@ if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\001Z*go.opentelemetry.io/proto/otlp/resource/v1' + DESCRIPTOR._serialized_options = b'\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\001Z*go.opentelemetry.io/proto/otlp/resource/v1\252\002\037OpenTelemetry.Proto.Resource.V1' _RESOURCE._serialized_start=127 _RESOURCE._serialized_end=232 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py deleted file mode 100644 index a54a2b68d0..0000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: opentelemetry/proto/trace/v1/trace_config.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/opentelemetry/proto/trace/v1/trace_config.proto\x12\x1copentelemetry.proto.trace.v1\"\xc8\x03\n\x0bTraceConfig\x12I\n\x10\x63onstant_sampler\x18\x01 \x01(\x0b\x32-.opentelemetry.proto.trace.v1.ConstantSamplerH\x00\x12O\n\x14trace_id_ratio_based\x18\x02 \x01(\x0b\x32/.opentelemetry.proto.trace.v1.TraceIdRatioBasedH\x00\x12R\n\x15rate_limiting_sampler\x18\x03 \x01(\x0b\x32\x31.opentelemetry.proto.trace.v1.RateLimitingSamplerH\x00\x12 \n\x18max_number_of_attributes\x18\x04 \x01(\x03\x12\"\n\x1amax_number_of_timed_events\x18\x05 \x01(\x03\x12\x30\n(max_number_of_attributes_per_timed_event\x18\x06 \x01(\x03\x12\x1b\n\x13max_number_of_links\x18\x07 \x01(\x03\x12)\n!max_number_of_attributes_per_link\x18\x08 \x01(\x03\x42\t\n\x07sampler\"\xa9\x01\n\x0f\x43onstantSampler\x12P\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32>.opentelemetry.proto.trace.v1.ConstantSampler.ConstantDecision\"D\n\x10\x43onstantDecision\x12\x0e\n\nALWAYS_OFF\x10\x00\x12\r\n\tALWAYS_ON\x10\x01\x12\x11\n\rALWAYS_PARENT\x10\x02\"*\n\x11TraceIdRatioBased\x12\x15\n\rsamplingRatio\x18\x01 \x01(\x01\"\"\n\x13RateLimitingSampler\x12\x0b\n\x03qps\x18\x01 \x01(\x03\x42h\n\x1fio.opentelemetry.proto.trace.v1B\x10TraceConfigProtoP\x01Z1go.opentelemetry.io/proto/otlp/collector/trace/v1b\x06proto3') - - - -_TRACECONFIG = DESCRIPTOR.message_types_by_name['TraceConfig'] -_CONSTANTSAMPLER = DESCRIPTOR.message_types_by_name['ConstantSampler'] -_TRACEIDRATIOBASED = DESCRIPTOR.message_types_by_name['TraceIdRatioBased'] -_RATELIMITINGSAMPLER = DESCRIPTOR.message_types_by_name['RateLimitingSampler'] -_CONSTANTSAMPLER_CONSTANTDECISION = _CONSTANTSAMPLER.enum_types_by_name['ConstantDecision'] -TraceConfig = _reflection.GeneratedProtocolMessageType('TraceConfig', (_message.Message,), { - 'DESCRIPTOR' : _TRACECONFIG, - '__module__' : 'opentelemetry.proto.trace.v1.trace_config_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.TraceConfig) - }) -_sym_db.RegisterMessage(TraceConfig) - -ConstantSampler = _reflection.GeneratedProtocolMessageType('ConstantSampler', (_message.Message,), { - 'DESCRIPTOR' : _CONSTANTSAMPLER, - '__module__' : 'opentelemetry.proto.trace.v1.trace_config_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.ConstantSampler) - }) -_sym_db.RegisterMessage(ConstantSampler) - -TraceIdRatioBased = _reflection.GeneratedProtocolMessageType('TraceIdRatioBased', (_message.Message,), { - 'DESCRIPTOR' : _TRACEIDRATIOBASED, - '__module__' : 'opentelemetry.proto.trace.v1.trace_config_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.TraceIdRatioBased) - }) -_sym_db.RegisterMessage(TraceIdRatioBased) - -RateLimitingSampler = _reflection.GeneratedProtocolMessageType('RateLimitingSampler', (_message.Message,), { - 'DESCRIPTOR' : _RATELIMITINGSAMPLER, - '__module__' : 'opentelemetry.proto.trace.v1.trace_config_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.RateLimitingSampler) - }) -_sym_db.RegisterMessage(RateLimitingSampler) - -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\037io.opentelemetry.proto.trace.v1B\020TraceConfigProtoP\001Z1go.opentelemetry.io/proto/otlp/collector/trace/v1' - _TRACECONFIG._serialized_start=82 - _TRACECONFIG._serialized_end=538 - _CONSTANTSAMPLER._serialized_start=541 - _CONSTANTSAMPLER._serialized_end=710 - _CONSTANTSAMPLER_CONSTANTDECISION._serialized_start=642 - _CONSTANTSAMPLER_CONSTANTDECISION._serialized_end=710 - _TRACEIDRATIOBASED._serialized_start=712 - _TRACEIDRATIOBASED._serialized_end=754 - _RATELIMITINGSAMPLER._serialized_start=756 - _RATELIMITINGSAMPLER._serialized_end=790 -# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi deleted file mode 100644 index 8290baf58e..0000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_config_pb2.pyi +++ /dev/null @@ -1,123 +0,0 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -""" -import builtins -import google.protobuf.descriptor -import google.protobuf.internal.enum_type_wrapper -import google.protobuf.message -import typing -import typing_extensions - -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... - -class TraceConfig(google.protobuf.message.Message): - """Global configuration of the trace service. All fields must be specified, or - the default (zero) values will be used for each type. - """ - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - CONSTANT_SAMPLER_FIELD_NUMBER: builtins.int - TRACE_ID_RATIO_BASED_FIELD_NUMBER: builtins.int - RATE_LIMITING_SAMPLER_FIELD_NUMBER: builtins.int - MAX_NUMBER_OF_ATTRIBUTES_FIELD_NUMBER: builtins.int - MAX_NUMBER_OF_TIMED_EVENTS_FIELD_NUMBER: builtins.int - MAX_NUMBER_OF_ATTRIBUTES_PER_TIMED_EVENT_FIELD_NUMBER: builtins.int - MAX_NUMBER_OF_LINKS_FIELD_NUMBER: builtins.int - MAX_NUMBER_OF_ATTRIBUTES_PER_LINK_FIELD_NUMBER: builtins.int - @property - def constant_sampler(self) -> global___ConstantSampler: ... - @property - def trace_id_ratio_based(self) -> global___TraceIdRatioBased: ... - @property - def rate_limiting_sampler(self) -> global___RateLimitingSampler: ... - max_number_of_attributes: builtins.int = ... - """The global default max number of attributes per span.""" - - max_number_of_timed_events: builtins.int = ... - """The global default max number of annotation events per span.""" - - max_number_of_attributes_per_timed_event: builtins.int = ... - """The global default max number of attributes per timed event.""" - - max_number_of_links: builtins.int = ... - """The global default max number of link entries per span.""" - - max_number_of_attributes_per_link: builtins.int = ... - """The global default max number of attributes per span.""" - - def __init__(self, - *, - constant_sampler : typing.Optional[global___ConstantSampler] = ..., - trace_id_ratio_based : typing.Optional[global___TraceIdRatioBased] = ..., - rate_limiting_sampler : typing.Optional[global___RateLimitingSampler] = ..., - max_number_of_attributes : builtins.int = ..., - max_number_of_timed_events : builtins.int = ..., - max_number_of_attributes_per_timed_event : builtins.int = ..., - max_number_of_links : builtins.int = ..., - max_number_of_attributes_per_link : builtins.int = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["constant_sampler",b"constant_sampler","rate_limiting_sampler",b"rate_limiting_sampler","sampler",b"sampler","trace_id_ratio_based",b"trace_id_ratio_based"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["constant_sampler",b"constant_sampler","max_number_of_attributes",b"max_number_of_attributes","max_number_of_attributes_per_link",b"max_number_of_attributes_per_link","max_number_of_attributes_per_timed_event",b"max_number_of_attributes_per_timed_event","max_number_of_links",b"max_number_of_links","max_number_of_timed_events",b"max_number_of_timed_events","rate_limiting_sampler",b"rate_limiting_sampler","sampler",b"sampler","trace_id_ratio_based",b"trace_id_ratio_based"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["sampler",b"sampler"]) -> typing.Optional[typing_extensions.Literal["constant_sampler","trace_id_ratio_based","rate_limiting_sampler"]]: ... -global___TraceConfig = TraceConfig - -class ConstantSampler(google.protobuf.message.Message): - """Sampler that always makes a constant decision on span sampling.""" - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - class ConstantDecision(_ConstantDecision, metaclass=_ConstantDecisionEnumTypeWrapper): - """How spans should be sampled: - - Always off - - Always on - - Always follow the parent Span's decision (off if no parent). - """ - pass - class _ConstantDecision: - V = typing.NewType('V', builtins.int) - class _ConstantDecisionEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_ConstantDecision.V], builtins.type): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... - ALWAYS_OFF = ConstantSampler.ConstantDecision.V(0) - ALWAYS_ON = ConstantSampler.ConstantDecision.V(1) - ALWAYS_PARENT = ConstantSampler.ConstantDecision.V(2) - - ALWAYS_OFF = ConstantSampler.ConstantDecision.V(0) - ALWAYS_ON = ConstantSampler.ConstantDecision.V(1) - ALWAYS_PARENT = ConstantSampler.ConstantDecision.V(2) - - DECISION_FIELD_NUMBER: builtins.int - decision: global___ConstantSampler.ConstantDecision.V = ... - def __init__(self, - *, - decision : global___ConstantSampler.ConstantDecision.V = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["decision",b"decision"]) -> None: ... -global___ConstantSampler = ConstantSampler - -class TraceIdRatioBased(google.protobuf.message.Message): - """Sampler that tries to uniformly sample traces with a given ratio. - The ratio of sampling a trace is equal to that of the specified ratio. - """ - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - SAMPLINGRATIO_FIELD_NUMBER: builtins.int - samplingRatio: builtins.float = ... - """The desired ratio of sampling. Must be within [0.0, 1.0].""" - - def __init__(self, - *, - samplingRatio : builtins.float = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["samplingRatio",b"samplingRatio"]) -> None: ... -global___TraceIdRatioBased = TraceIdRatioBased - -class RateLimitingSampler(google.protobuf.message.Message): - """Sampler that tries to sample with a rate per time window.""" - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - QPS_FIELD_NUMBER: builtins.int - qps: builtins.int = ... - """Rate per second.""" - - def __init__(self, - *, - qps : builtins.int = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["qps",b"qps"]) -> None: ... -global___RateLimitingSampler = RateLimitingSampler diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py index 0827c14301..6e80acce51 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py @@ -16,14 +16,13 @@ from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"Q\n\nTracesData\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\x86\x02\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12=\n\x0bscope_spans\x18\x02 \x03(\x0b\x32(.opentelemetry.proto.trace.v1.ScopeSpans\x12\x65\n\x1dinstrumentation_library_spans\x18\xe8\x07 \x03(\x0b\x32\x39.opentelemetry.proto.trace.v1.InstrumentationLibrarySpansB\x02\x18\x01\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x97\x01\n\nScopeSpans\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xc0\x01\n\x1bInstrumentationLibrarySpans\x12V\n\x17instrumentation_library\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.common.v1.InstrumentationLibrary\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t:\x02\x18\x01\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xae\x01\n\x06Status\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02J\x04\x08\x01\x10\x02\x42X\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z\'go.opentelemetry.io/proto/otlp/trace/v1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"Q\n\nTracesData\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\xa7\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12=\n\x0bscope_spans\x18\x02 \x03(\x0b\x32(.opentelemetry.proto.trace.v1.ScopeSpans\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\x97\x01\n\nScopeSpans\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xae\x01\n\x06Status\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02J\x04\x08\x01\x10\x02\x42w\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z\'go.opentelemetry.io/proto/otlp/trace/v1\xaa\x02\x1cOpenTelemetry.Proto.Trace.V1b\x06proto3') _TRACESDATA = DESCRIPTOR.message_types_by_name['TracesData'] _RESOURCESPANS = DESCRIPTOR.message_types_by_name['ResourceSpans'] _SCOPESPANS = DESCRIPTOR.message_types_by_name['ScopeSpans'] -_INSTRUMENTATIONLIBRARYSPANS = DESCRIPTOR.message_types_by_name['InstrumentationLibrarySpans'] _SPAN = DESCRIPTOR.message_types_by_name['Span'] _SPAN_EVENT = _SPAN.nested_types_by_name['Event'] _SPAN_LINK = _SPAN.nested_types_by_name['Link'] @@ -51,13 +50,6 @@ }) _sym_db.RegisterMessage(ScopeSpans) -InstrumentationLibrarySpans = _reflection.GeneratedProtocolMessageType('InstrumentationLibrarySpans', (_message.Message,), { - 'DESCRIPTOR' : _INSTRUMENTATIONLIBRARYSPANS, - '__module__' : 'opentelemetry.proto.trace.v1.trace_pb2' - # @@protoc_insertion_point(class_scope:opentelemetry.proto.trace.v1.InstrumentationLibrarySpans) - }) -_sym_db.RegisterMessage(InstrumentationLibrarySpans) - Span = _reflection.GeneratedProtocolMessageType('Span', (_message.Message,), { 'Event' : _reflection.GeneratedProtocolMessageType('Event', (_message.Message,), { @@ -91,29 +83,23 @@ if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z\'go.opentelemetry.io/proto/otlp/trace/v1' - _RESOURCESPANS.fields_by_name['instrumentation_library_spans']._options = None - _RESOURCESPANS.fields_by_name['instrumentation_library_spans']._serialized_options = b'\030\001' - _INSTRUMENTATIONLIBRARYSPANS._options = None - _INSTRUMENTATIONLIBRARYSPANS._serialized_options = b'\030\001' + DESCRIPTOR._serialized_options = b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z\'go.opentelemetry.io/proto/otlp/trace/v1\252\002\034OpenTelemetry.Proto.Trace.V1' _TRACESDATA._serialized_start=166 _TRACESDATA._serialized_end=247 _RESOURCESPANS._serialized_start=250 - _RESOURCESPANS._serialized_end=512 - _SCOPESPANS._serialized_start=515 - _SCOPESPANS._serialized_end=666 - _INSTRUMENTATIONLIBRARYSPANS._serialized_start=669 - _INSTRUMENTATIONLIBRARYSPANS._serialized_end=861 - _SPAN._serialized_start=864 - _SPAN._serialized_end=1862 - _SPAN_EVENT._serialized_start=1406 - _SPAN_EVENT._serialized_end=1546 - _SPAN_LINK._serialized_start=1549 - _SPAN_LINK._serialized_end=1706 - _SPAN_SPANKIND._serialized_start=1709 - _SPAN_SPANKIND._serialized_end=1862 - _STATUS._serialized_start=1865 - _STATUS._serialized_end=2039 - _STATUS_STATUSCODE._serialized_start=1955 - _STATUS_STATUSCODE._serialized_end=2033 + _RESOURCESPANS._serialized_end=417 + _SCOPESPANS._serialized_start=420 + _SCOPESPANS._serialized_end=571 + _SPAN._serialized_start=574 + _SPAN._serialized_end=1572 + _SPAN_EVENT._serialized_start=1116 + _SPAN_EVENT._serialized_end=1256 + _SPAN_LINK._serialized_start=1259 + _SPAN_LINK._serialized_end=1416 + _SPAN_SPANKIND._serialized_start=1419 + _SPAN_SPANKIND._serialized_end=1572 + _STATUS._serialized_start=1575 + _STATUS._serialized_end=1749 + _STATUS_STATUSCODE._serialized_start=1665 + _STATUS_STATUSCODE._serialized_end=1743 # @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi index 170d66e1c5..52052ff7e9 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi @@ -49,7 +49,6 @@ class ResourceSpans(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... RESOURCE_FIELD_NUMBER: builtins.int SCOPE_SPANS_FIELD_NUMBER: builtins.int - INSTRUMENTATION_LIBRARY_SPANS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: @@ -61,37 +60,6 @@ class ResourceSpans(google.protobuf.message.Message): def scope_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeSpans]: """A list of ScopeSpans that originate from a resource.""" pass - @property - def instrumentation_library_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InstrumentationLibrarySpans]: - """A list of InstrumentationLibrarySpans that originate from a resource. - This field is deprecated and will be removed after grace period expires on June 15, 2022. - - During the grace period the following rules SHOULD be followed: - - For Binary Protobufs - ==================== - Binary Protobuf senders SHOULD NOT set instrumentation_library_spans. Instead - scope_spans SHOULD be set. - - Binary Protobuf receivers SHOULD check if instrumentation_library_spans is set - and scope_spans is not set then the value in instrumentation_library_spans - SHOULD be used instead by converting InstrumentationLibrarySpans into ScopeSpans. - If scope_spans is set then instrumentation_library_spans SHOULD be ignored. - - For JSON - ======== - JSON senders that set instrumentation_library_spans field MAY also set - scope_spans to carry the same spans, essentially double-publishing the same data. - Such double-publishing MAY be controlled by a user-settable option. - If double-publishing is not used then the senders SHOULD set scope_spans and - SHOULD NOT set instrumentation_library_spans. - - JSON receivers SHOULD check if instrumentation_library_spans is set and - scope_spans is not set then the value in instrumentation_library_spans - SHOULD be used instead by converting InstrumentationLibrarySpans into ScopeSpans. - If scope_spans is set then instrumentation_library_spans field SHOULD be ignored. - """ - pass schema_url: typing.Text = ... """This schema_url applies to the data in the "resource" field. It does not apply to the data in the "scope_spans" field which have their own schema_url field. @@ -101,11 +69,10 @@ class ResourceSpans(google.protobuf.message.Message): *, resource : typing.Optional[opentelemetry.proto.resource.v1.resource_pb2.Resource] = ..., scope_spans : typing.Optional[typing.Iterable[global___ScopeSpans]] = ..., - instrumentation_library_spans : typing.Optional[typing.Iterable[global___InstrumentationLibrarySpans]] = ..., schema_url : typing.Text = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["resource",b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library_spans",b"instrumentation_library_spans","resource",b"resource","schema_url",b"schema_url","scope_spans",b"scope_spans"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["resource",b"resource","schema_url",b"schema_url","scope_spans",b"scope_spans"]) -> None: ... global___ResourceSpans = ResourceSpans class ScopeSpans(google.protobuf.message.Message): @@ -138,48 +105,8 @@ class ScopeSpans(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal["schema_url",b"schema_url","scope",b"scope","spans",b"spans"]) -> None: ... global___ScopeSpans = ScopeSpans -class InstrumentationLibrarySpans(google.protobuf.message.Message): - """A collection of Spans produced by an InstrumentationLibrary. - InstrumentationLibrarySpans is wire-compatible with ScopeSpans for binary - Protobuf format. - This message is deprecated and will be removed on June 15, 2022. - """ - DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... - INSTRUMENTATION_LIBRARY_FIELD_NUMBER: builtins.int - SPANS_FIELD_NUMBER: builtins.int - SCHEMA_URL_FIELD_NUMBER: builtins.int - @property - def instrumentation_library(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary: - """The instrumentation library information for the spans in this message. - Semantically when InstrumentationLibrary isn't set, it is equivalent with - an empty instrumentation library name (unknown). - """ - pass - @property - def spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span]: - """A list of Spans that originate from an instrumentation library.""" - pass - schema_url: typing.Text = ... - """This schema_url applies to all spans and span events in the "spans" field.""" - - def __init__(self, - *, - instrumentation_library : typing.Optional[opentelemetry.proto.common.v1.common_pb2.InstrumentationLibrary] = ..., - spans : typing.Optional[typing.Iterable[global___Span]] = ..., - schema_url : typing.Text = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["instrumentation_library",b"instrumentation_library","schema_url",b"schema_url","spans",b"spans"]) -> None: ... -global___InstrumentationLibrarySpans = InstrumentationLibrarySpans - class Span(google.protobuf.message.Message): - """Span represents a single operation within a trace. Spans can be - nested to form a trace tree. Spans may also be linked to other spans - from the same or different trace and form graphs. Often, a trace - contains a root span that describes the end-to-end latency, and one - or more subspans for its sub-operations. A trace can also contain - multiple root spans, or none at all. Spans do not need to be - contiguous - there may be gaps or overlaps between spans in a trace. + """A Span represents a single operation performed by a single component of the system. The next available field id is 17. """ @@ -357,22 +284,18 @@ class Span(google.protobuf.message.Message): STATUS_FIELD_NUMBER: builtins.int trace_id: builtins.bytes = ... """A unique identifier for a trace. All spans from the same trace share - the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes - is considered invalid. - - This field is semantically required. Receiver should generate new - random trace_id if empty or invalid trace_id was received. + the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes OR + of length other than 16 bytes is considered invalid (empty string in OTLP/JSON + is zero-length and thus is also invalid). This field is required. """ span_id: builtins.bytes = ... """A unique identifier for a span within a trace, assigned when the span - is created. The ID is an 8-byte array. An ID with all zeroes is considered - invalid. - - This field is semantically required. Receiver should generate new - random span_id if empty or invalid span_id was received. + is created. The ID is an 8-byte array. An ID with all zeroes OR of length + other than 8 bytes is considered invalid (empty string in OTLP/JSON + is zero-length and thus is also invalid). This field is required. """ @@ -433,11 +356,11 @@ class Span(google.protobuf.message.Message): "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" "/http/server_latency": 300 - "abc.com/myattribute": true - "abc.com/score": 10.239 + "example.com/myattribute": true + "example.com/score": 10.239 The OpenTelemetry API specification further restricts the allowed value types: - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/common.md#attributes + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/README.md#attribute Attribute keys MUST be unique (it is not allowed to have more than one attribute with the same key). """ @@ -514,8 +437,8 @@ class Status(google.protobuf.message.Message): """The default status.""" STATUS_CODE_OK = Status.StatusCode.V(1) - """The Span has been validated by an Application developers or Operator to have - completed successfully. + """The Span has been validated by an Application developer or Operator to + have completed successfully. """ STATUS_CODE_ERROR = Status.StatusCode.V(2) @@ -526,8 +449,8 @@ class Status(google.protobuf.message.Message): """The default status.""" STATUS_CODE_OK = Status.StatusCode.V(1) - """The Span has been validated by an Application developers or Operator to have - completed successfully. + """The Span has been validated by an Application developer or Operator to + have completed successfully. """ STATUS_CODE_ERROR = Status.StatusCode.V(2) diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index 7625848f71..53d8ad8fc7 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -12,7 +12,7 @@ # PROTO_REPO_DIR - the path to an existing checkout of the opentelemetry-proto repo # Pinned commit/branch/tag for the current version used in opentelemetry-proto python package. -PROTO_REPO_BRANCH_OR_COMMIT="v0.17.0" +PROTO_REPO_BRANCH_OR_COMMIT="v0.20.0" set -e From 902602721cf651223f30ccdbb0e35c87745bb210 Mon Sep 17 00:00:00 2001 From: Shalev Roda <65566801+shalevr@users.noreply.github.com> Date: Mon, 26 Jun 2023 23:58:10 +0300 Subject: [PATCH 1436/1517] Add unit to view instrument selection criteria (#3341) Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/metrics/_internal/view.py | 10 ++++++++++ opentelemetry-sdk/tests/metrics/test_view.py | 9 +++++++++ 3 files changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1332776ef..1afd81b6eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add max_scale option to Exponential Bucket Histogram Aggregation [#3323](https://github.com/open-telemetry/opentelemetry-python/pull/3323)) - Use BoundedAttributes instead of raw dict to extract attributes from LogRecord and Support dropped_attributes_count in LogRecord ([#3310](https://github.com/open-telemetry/opentelemetry-python/pull/3310)) +- Add unit to view instrument selection criteria + ([#3341](https://github.com/open-telemetry/opentelemetry-python/pull/3341)) - Upgrade opentelemetry-proto to 0.20 and regen [#3355](https://github.com/open-telemetry/opentelemetry-python/pull/3355)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py index b930c7a966..28f7b4fe08 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py @@ -76,6 +76,9 @@ class View: corresponding metrics stream. If `None` an instance of `DefaultAggregation` will be used. + instrument_unit: This is an instrument matching attribute: the unit the + instrument must have to match the view. + This class is not intended to be subclassed by the user. """ @@ -92,10 +95,12 @@ def __init__( description: Optional[str] = None, attribute_keys: Optional[Set[str]] = None, aggregation: Optional[Aggregation] = None, + instrument_unit: Optional[str] = None, ): if ( instrument_type is instrument_name + is instrument_unit is meter_name is meter_version is meter_schema_url @@ -122,6 +127,7 @@ def __init__( self._name = name self._instrument_type = instrument_type self._instrument_name = instrument_name + self._instrument_unit = instrument_unit self._meter_name = meter_name self._meter_version = meter_version self._meter_schema_url = meter_schema_url @@ -143,6 +149,10 @@ def _match(self, instrument: Instrument) -> bool: if not fnmatch(instrument.name, self._instrument_name): return False + if self._instrument_unit is not None: + if not fnmatch(instrument.unit, self._instrument_unit): + return False + if self._meter_name is not None: if instrument.instrumentation_scope.name != self._meter_name: return False diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py index 2d1fee490f..00376a0068 100644 --- a/opentelemetry-sdk/tests/metrics/test_view.py +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -37,6 +37,15 @@ def test_instrument_name(self): View(instrument_name="instrument_name")._match(mock_instrument) ) + def test_instrument_unit(self): + + mock_instrument = Mock() + mock_instrument.configure_mock(**{"unit": "instrument_unit"}) + + self.assertTrue( + View(instrument_unit="instrument_unit")._match(mock_instrument) + ) + def test_meter_name(self): self.assertTrue( From ad2de35bb9373d18fae7de515a326ce07fb89158 Mon Sep 17 00:00:00 2001 From: Nina Stawski Date: Mon, 26 Jun 2023 14:16:35 -0700 Subject: [PATCH 1437/1517] Add dropped_attributes_count support in exporters (#3351) Co-authored-by: Diego Hurtado --- CHANGELOG.md | 8 ++- .../common/_internal/_log_encoder/__init__.py | 1 + .../tests/test_log_encoder.py | 54 ++++++++++++++++++- .../sdk/_logs/_internal/__init__.py | 1 + .../tests/logs/test_log_record.py | 1 + 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1afd81b6eb..0a22a6c7c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -- Add max_scale option to Exponential Bucket Histogram Aggregation [#3323](https://github.com/open-telemetry/opentelemetry-python/pull/3323)) -- Use BoundedAttributes instead of raw dict to extract attributes from LogRecord and Support dropped_attributes_count in LogRecord ([#3310](https://github.com/open-telemetry/opentelemetry-python/pull/3310)) +- Add max_scale option to Exponential Bucket Histogram Aggregation + ([#3323](https://github.com/open-telemetry/opentelemetry-python/pull/3323)) +- Use BoundedAttributes instead of raw dict to extract attributes from LogRecord + ([#3310](https://github.com/open-telemetry/opentelemetry-python/pull/3310)) +- Support dropped_attributes_count in LogRecord and exporters + ([#3351](https://github.com/open-telemetry/opentelemetry-python/pull/3351)) - Add unit to view instrument selection criteria ([#3341](https://github.com/open-telemetry/opentelemetry-python/pull/3341)) - Upgrade opentelemetry-proto to 0.20 and regen diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py index 7c135d90ba..47c254033b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py @@ -47,6 +47,7 @@ def _encode_log(log_data: LogData) -> PB2LogRecord: body=_encode_value(log_data.log_record.body), severity_text=log_data.log_record.severity_text, attributes=_encode_attributes(log_data.log_record.attributes), + dropped_attributes_count=log_data.log_record.dropped_attributes, severity_number=log_data.log_record.severity_number.value, ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py index 1cd86b2833..1fdb1977ba 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py @@ -39,7 +39,7 @@ from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as PB2Resource, ) -from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs import LogData, LogLimits from opentelemetry.sdk._logs import LogRecord as SDKLogRecord from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationScope @@ -51,6 +51,19 @@ def test_encode(self): sdk_logs, expected_encoding = self.get_test_logs() self.assertEqual(encode_logs(sdk_logs), expected_encoding) + def test_dropped_attributes_count(self): + sdk_logs = self._get_test_logs_dropped_attributes() + encoded_logs = encode_logs(sdk_logs) + self.assertTrue(hasattr(sdk_logs[0].log_record, "dropped_attributes")) + self.assertEqual( + # pylint:disable=no-member + encoded_logs.resource_logs[0] + .scope_logs[0] + .log_records[0] + .dropped_attributes_count, + 2, + ) + @staticmethod def _get_sdk_log_data() -> List[LogData]: log1 = LogData( @@ -251,3 +264,42 @@ def get_test_logs( ) return sdk_logs, pb2_service_request + + @staticmethod + def _get_test_logs_dropped_attributes() -> List[LogData]: + log1 = LogData( + log_record=SDKLogRecord( + timestamp=1644650195189786880, + trace_id=89564621134313219400156819398935297684, + span_id=1312458408527513268, + trace_flags=TraceFlags(0x01), + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Do not go gentle into that good night. Rage, rage against the dying of the light", + resource=SDKResource({"first_resource": "value"}), + attributes={"a": 1, "b": "c", "user_id": "B121092"}, + limits=LogLimits(max_attributes=1), + ), + instrumentation_scope=InstrumentationScope( + "first_name", "first_version" + ), + ) + + log2 = LogData( + log_record=SDKLogRecord( + timestamp=1644650249738562048, + trace_id=0, + span_id=0, + trace_flags=TraceFlags.DEFAULT, + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Cooper, this is no time for caution!", + resource=SDKResource({"second_resource": "CASE"}), + attributes={}, + ), + instrumentation_scope=InstrumentationScope( + "second_name", "second_version" + ), + ) + + return [log1, log2] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 7410138067..578ce2c391 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -203,6 +203,7 @@ def to_json(self, indent=4) -> str: "attributes": dict(self.attributes) if bool(self.attributes) else None, + "dropped_attributes": self.dropped_attributes, "timestamp": ns_to_iso_str(self.timestamp), "trace_id": f"0x{format_trace_id(self.trace_id)}" if self.trace_id is not None diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index a5993e5833..1f0bd785a8 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -27,6 +27,7 @@ def test_log_record_to_json(self): "severity_number": "None", "severity_text": None, "attributes": None, + "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "trace_id": "", "span_id": "", From 251b74a2bb915dfc11fa839154980868869ea291 Mon Sep 17 00:00:00 2001 From: Shalev Roda <65566801+shalevr@users.noreply.github.com> Date: Tue, 27 Jun 2023 10:38:47 +0300 Subject: [PATCH 1438/1517] Add timeout for httpTestBase (#3318) --- .../opentelemetry-test-utils/src/opentelemetry/test/httptest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/httptest.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/httptest.py index 94964ea9f1..84591ca0f1 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/httptest.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/httptest.py @@ -24,7 +24,7 @@ class HttpTestBase(unittest.TestCase): class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" # Support keep-alive. - # timeout = 3 # No timeout -- if shutdown hangs, make sure to close your connection + timeout = 3 # Seconds STATUS_RE = re.compile(r"/status/(\d+)") From f4aecdf5cee92283d44fbb2f39ee2b93cc1eb4ca Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 27 Jun 2023 13:57:49 +0530 Subject: [PATCH 1439/1517] Show helpful message when propagator is not found (#3259) Co-authored-by: Leighton Chen Co-authored-by: Diego Hurtado --- .../src/opentelemetry/propagate/__init__.py | 9 +++++---- .../tests/propagators/test_propagators.py | 19 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index 56f217f282..90f9e61744 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -144,11 +144,12 @@ def inject( ) ).load()() ) - - except Exception: # pylint: disable=broad-except - logger.exception( - "Failed to load configured propagator: %s", propagator + except StopIteration: + raise ValueError( + f"Propagator {propagator} not found. It is either misspelled or not installed." ) + except Exception: # pylint: disable=broad-except + logger.exception("Failed to load propagator: %s", propagator) raise diff --git a/opentelemetry-api/tests/propagators/test_propagators.py b/opentelemetry-api/tests/propagators/test_propagators.py index bb84bc4f1a..29065b8cb3 100644 --- a/opentelemetry-api/tests/propagators/test_propagators.py +++ b/opentelemetry-api/tests/propagators/test_propagators.py @@ -15,7 +15,6 @@ # type: ignore from importlib import reload -from logging import ERROR from os import environ from unittest import TestCase from unittest.mock import Mock, patch @@ -109,16 +108,16 @@ def test_propagators(propagators): ) def test_composite_propagators_error(self): - # pylint: disable=import-outside-toplevel - import opentelemetry.propagate + with self.assertRaises(ValueError) as cm: + # pylint: disable=import-outside-toplevel + import opentelemetry.propagate + + reload(opentelemetry.propagate) - with self.assertRaises(Exception): - with self.assertLogs(level=ERROR) as err: - reload(opentelemetry.propagate) - self.assertIn( - "Failed to load configured propagator `unknown`", - err.output[0], - ) + self.assertEqual( + str(cm.exception), + "Propagator unknown not found. It is either misspelled or not installed.", + ) class TestTraceContextTextMapPropagator(TestCase): From b15de888fee2477e371ba4908b23bc957a212b3d Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 27 Jun 2023 10:41:39 +0200 Subject: [PATCH 1440/1517] Remove log messages from console (#3357) Co-authored-by: Shalev Roda <65566801+shalevr@users.noreply.github.com> --- opentelemetry-sdk/tests/logs/test_export.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index aa68b09624..774df5771a 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -58,6 +58,7 @@ def test_simple_log_record_processor_default_level(self): ) logger = logging.getLogger("default_level") + logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=logger_provider)) logger.warning("Something is wrong") @@ -79,6 +80,7 @@ def test_simple_log_record_processor_custom_level(self): ) logger = logging.getLogger("custom_level") + logger.propagate = False logger.setLevel(logging.ERROR) logger.addHandler(LoggingHandler(logger_provider=logger_provider)) @@ -111,6 +113,7 @@ def test_simple_log_record_processor_trace_correlation(self): ) logger = logging.getLogger("trace_correlation") + logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=logger_provider)) logger.warning("Warning message") @@ -150,6 +153,7 @@ def test_simple_log_record_processor_shutdown(self): ) logger = logging.getLogger("shutdown") + logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=logger_provider)) logger.warning("Something is wrong") @@ -176,6 +180,7 @@ def test_emit_call_log_record(self): provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("emit_call") + logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=provider)) logger.error("error") @@ -310,6 +315,7 @@ def test_shutdown(self): provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("shutdown") + logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=provider)) logger.warning("warning message: %s", "possible upcoming heatwave") @@ -342,6 +348,7 @@ def test_force_flush(self): provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("force_flush") + logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=provider)) logger.critical("Earth is burning") @@ -360,6 +367,7 @@ def test_log_record_processor_too_many_logs(self): provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("many_logs") + logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=provider)) for log_no in range(1000): @@ -377,6 +385,7 @@ def test_with_multiple_threads(self): provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("threads") + logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=provider)) def bulk_log_and_flush(num_logs): @@ -411,6 +420,7 @@ def test_batch_log_record_processor_fork(self): provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("test-fork") + logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=provider)) logger.critical("yolo") From 6559e07d0f10aa947e3debbc65640690c866991a Mon Sep 17 00:00:00 2001 From: Shalev Roda <65566801+shalevr@users.noreply.github.com> Date: Tue, 27 Jun 2023 15:30:05 +0300 Subject: [PATCH 1441/1517] Update approvers list (#3358) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e3923e8fe5..4f42fd0bd5 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Aaron Abbott](https://github.com/aabmass), Google - [Jeremy Voss](https://github.com/jeremydvoss), Microsoft - [Sanket Mehta](https://github.com/sanketmehta28), Cisco +- [Shalev Roda](https://github.com/shalevr), Cisco Emeritus Approvers From adbcf820f6ff24de80278113ce9629e67111ef8e Mon Sep 17 00:00:00 2001 From: nerstak <33179821+nerstak@users.noreply.github.com> Date: Wed, 28 Jun 2023 18:34:34 +0200 Subject: [PATCH 1442/1517] Exporting resources attributes on target_info for Prometheus Exporter (#3279) Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 + .../exporter/prometheus/__init__.py | 35 +++++++++-- .../tests/test_prometheus_exporter.py | 58 +++++++++++++++++-- 3 files changed, 86 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a22a6c7c7..d6d98b5e97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased + - Add max_scale option to Exponential Bucket Histogram Aggregation ([#3323](https://github.com/open-telemetry/opentelemetry-python/pull/3323)) - Use BoundedAttributes instead of raw dict to extract attributes from LogRecord @@ -35,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add speced out environment variables and arguments for BatchLogRecordProcessor ([#3237](https://github.com/open-telemetry/opentelemetry-python/pull/3237)) + ## Version 1.17.0/0.38b0 (2023-03-22) - Implement LowMemory temporality diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 7442b7b242..9ece76755c 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -74,6 +74,7 @@ CounterMetricFamily, GaugeMetricFamily, HistogramMetricFamily, + InfoMetricFamily, ) from prometheus_client.core import Metric as PrometheusMetric @@ -97,6 +98,9 @@ _logger = getLogger(__name__) +_TARGET_INFO_NAME = "target" +_TARGET_INFO_DESCRIPTION = "Target metadata" + def _convert_buckets( bucket_counts: Sequence[int], explicit_bounds: Sequence[float] @@ -116,8 +120,7 @@ def _convert_buckets( class PrometheusMetricReader(MetricReader): """Prometheus metric exporter for OpenTelemetry.""" - def __init__(self) -> None: - + def __init__(self, disable_target_info: bool = False) -> None: super().__init__( preferred_temporality={ Counter: AggregationTemporality.CUMULATIVE, @@ -128,7 +131,7 @@ def __init__(self) -> None: ObservableGauge: AggregationTemporality.CUMULATIVE, } ) - self._collector = _CustomCollector() + self._collector = _CustomCollector(disable_target_info) REGISTRY.register(self._collector) self._collector._callback = self.collect @@ -153,12 +156,14 @@ class _CustomCollector: https://github.com/prometheus/client_python#custom-collectors """ - def __init__(self): + def __init__(self, disable_target_info: bool = False): self._callback = None self._metrics_datas = deque() self._non_letters_digits_underscore_re = compile( r"[^\w]", UNICODE | IGNORECASE ) + self._disable_target_info = disable_target_info + self._target_info = None def add_metrics_data(self, metrics_data: MetricsData) -> None: """Add metrics to Prometheus data""" @@ -175,6 +180,20 @@ def collect(self) -> None: metric_family_id_metric_family = {} + if len(self._metrics_datas): + if not self._disable_target_info: + if self._target_info is None: + attributes = {} + for res in self._metrics_datas[0].resource_metrics: + attributes = {**attributes, **res.resource.attributes} + + self._target_info = self._create_info_metric( + _TARGET_INFO_NAME, _TARGET_INFO_DESCRIPTION, attributes + ) + metric_family_id_metric_family[ + _TARGET_INFO_NAME + ] = self._target_info + while self._metrics_datas: self._translate_to_prometheus( self._metrics_datas.popleft(), metric_family_id_metric_family @@ -327,3 +346,11 @@ def _check_value(self, value: Union[int, float, str, Sequence]) -> str: if not isinstance(value, str): return dumps(value, default=str) return str(value) + + def _create_info_metric( + self, name: str, description: str, attributes: Dict[str, str] + ) -> InfoMetricFamily: + """Create an Info Metric Family with list of attributes""" + info = InfoMetricFamily(name, description, labels=attributes) + info.add_metric(labels=list(attributes.keys()), value=attributes) + return info diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 1180fac614..c7ce1afae1 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -17,7 +17,11 @@ from unittest.mock import Mock, patch from prometheus_client import generate_latest -from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily +from prometheus_client.core import ( + CounterMetricFamily, + GaugeMetricFamily, + InfoMetricFamily, +) from opentelemetry.exporter.prometheus import ( PrometheusMetricReader, @@ -33,6 +37,7 @@ ResourceMetrics, ScopeMetrics, ) +from opentelemetry.sdk.resources import Resource from opentelemetry.test.metrictestutil import ( _generate_gauge, _generate_sum, @@ -101,7 +106,7 @@ def test_histogram_to_prometheus(self): ] ) - collector = _CustomCollector() + collector = _CustomCollector(disable_target_info=True) collector.add_metrics_data(metrics_data) result_bytes = generate_latest(collector) result = result_bytes.decode("utf-8") @@ -146,7 +151,7 @@ def test_sum_to_prometheus(self): ] ) - collector = _CustomCollector() + collector = _CustomCollector(disable_target_info=True) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): @@ -189,7 +194,7 @@ def test_gauge_to_prometheus(self): ] ) - collector = _CustomCollector() + collector = _CustomCollector(disable_target_info=True) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): @@ -251,7 +256,7 @@ def test_list_labels(self): ) ] ) - collector = _CustomCollector() + collector = _CustomCollector(disable_target_info=True) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): @@ -293,3 +298,46 @@ def test_multiple_collection_calls(self): result_2 = list(metric_reader._collector.collect()) self.assertEqual(result_0, result_1) self.assertEqual(result_1, result_2) + + def test_target_info_enabled_by_default(self): + metric_reader = PrometheusMetricReader() + provider = MeterProvider( + metric_readers=[metric_reader], + resource=Resource({"os": "Unix", "histo": 1}), + ) + meter = provider.get_meter("getting-started", "0.1.2") + counter = meter.create_counter("counter") + counter.add(1) + result = list(metric_reader._collector.collect()) + + for prometheus_metric in result[:0]: + self.assertEqual(type(prometheus_metric), InfoMetricFamily) + self.assertEqual(prometheus_metric.name, "target") + self.assertEqual( + prometheus_metric.documentation, "Target metadata" + ) + self.assertTrue(len(prometheus_metric.samples) == 1) + self.assertEqual(prometheus_metric.samples[0].value, 1) + self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) + self.assertEqual(prometheus_metric.samples[0].labels["os"], "Unix") + self.assertEqual(prometheus_metric.samples[0].labels["histo"], "1") + + def test_target_info_disabled(self): + metric_reader = PrometheusMetricReader(disable_target_info=True) + provider = MeterProvider( + metric_readers=[metric_reader], + resource=Resource({"os": "Unix", "histo": 1}), + ) + meter = provider.get_meter("getting-started", "0.1.2") + counter = meter.create_counter("counter") + counter.add(1) + result = list(metric_reader._collector.collect()) + + for prometheus_metric in result: + self.assertNotEqual(type(prometheus_metric), InfoMetricFamily) + self.assertNotEqual(prometheus_metric.name, "target") + self.assertNotEqual( + prometheus_metric.documentation, "Target metadata" + ) + self.assertNotIn("os", prometheus_metric.samples[0].labels) + self.assertNotIn("histo", prometheus_metric.samples[0].labels) From 67e6debd0339918ec6f0f307d98dae6ea6116d1e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 5 Jul 2023 12:28:29 +0200 Subject: [PATCH 1443/1517] Cleanup console for SDK tests (#3360) --- CONTRIBUTING.md | 22 ++++ opentelemetry-sdk/tests/logs/test_export.py | 16 ++- opentelemetry-sdk/tests/logs/test_handler.py | 27 +++-- .../tests/logs/test_multi_log_processor.py | 12 +- .../tests/metrics/test_instrument.py | 7 +- .../metrics/test_metric_reader_storage.py | 6 +- .../tests/metrics/test_metrics.py | 32 +++--- .../test_periodic_exporting_metric_reader.py | 9 +- .../tests/resources/test_resources.py | 47 ++++---- opentelemetry-sdk/tests/test_configurator.py | 17 ++- .../tests/trace/export/test_export.py | 3 + opentelemetry-sdk/tests/trace/test_trace.py | 106 +++++++++++------- pyproject.toml | 1 - 13 files changed, 197 insertions(+), 108 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index daef82f90a..61fe760529 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,6 +71,28 @@ behind this is that every PR that adds/removes public symbols fails in CI, forci If after checking them, it is considered that they are indeed necessary, the PR will be labeled with `Skip Public API check` so that this check is not run. +Also, we try to keep our console output as clean as possible. Most of the time this means catching expected log messages in the test cases: + +``` python +from logging import WARNING + +... + + def test_case(self): + with self.assertLogs(level=WARNING): + some_function_that_will_log_a_warning_message() +``` + +Other options can be to disable logging propagation or disabling a logger altogether. + +A similar approach can be followed to catch warnings: + +``` python + def test_case(self): + with self.assertWarns(DeprecationWarning): + some_function_that_will_raise_a_deprecation_warning() +``` + See [`tox.ini`](https://github.com/open-telemetry/opentelemetry-python/blob/main/tox.ini) for more detail on available tox commands. diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 774df5771a..0a33e941ff 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -29,6 +29,7 @@ LoggingHandler, LogRecord, ) +from opentelemetry.sdk._logs._internal.export import _logger from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, ConsoleLogExporter, @@ -167,7 +168,8 @@ def test_simple_log_record_processor_shutdown(self): ) exporter.clear() logger_provider.shutdown() - logger.warning("Log after shutdown") + with self.assertLogs(level=logging.WARNING): + logger.warning("Log after shutdown") finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 0) @@ -239,7 +241,9 @@ def test_args_defaults(self): ) def test_args_env_var_value_error(self): exporter = InMemoryLogExporter() + _logger.disabled = True log_record_processor = BatchLogRecordProcessor(exporter) + _logger.disabled = False self.assertEqual(log_record_processor._exporter, exporter) self.assertEqual(log_record_processor._max_queue_size, 2048) self.assertEqual(log_record_processor._schedule_delay_millis, 5000) @@ -315,12 +319,14 @@ def test_shutdown(self): provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("shutdown") - logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=provider)) - logger.warning("warning message: %s", "possible upcoming heatwave") - logger.error("Very high rise in temperatures across the globe") - logger.critical("Temperature hits high 420 C in Hyderabad") + with self.assertLogs(level=logging.WARNING): + logger.warning("warning message: %s", "possible upcoming heatwave") + with self.assertLogs(level=logging.WARNING): + logger.error("Very high rise in temperatures across the globe") + with self.assertLogs(level=logging.WARNING): + logger.critical("Temperature hits high 420 C in Hyderabad") log_record_processor.shutdown() self.assertTrue(exporter._stopped) diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 04cf5640f5..bb4fc3a829 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -42,7 +42,8 @@ def test_handler_default_log_level(self): logger.debug("Debug message") self.assertEqual(emitter_mock.emit.call_count, 0) # Assert emit gets called for warning message - logger.warning("Warning message") + with self.assertLogs(level=logging.WARNING): + logger.warning("Warning message") self.assertEqual(emitter_mock.emit.call_count, 1) def test_handler_custom_log_level(self): @@ -53,11 +54,14 @@ def test_handler_custom_log_level(self): logger = get_logger( level=logging.ERROR, logger_provider=emitter_provider_mock ) - logger.warning("Warning message test custom log level") + with self.assertLogs(level=logging.WARNING): + logger.warning("Warning message test custom log level") # Make sure any log with level < ERROR is ignored self.assertEqual(emitter_mock.emit.call_count, 0) - logger.error("Mumbai, we have a major problem") - logger.critical("No Time For Caution") + with self.assertLogs(level=logging.ERROR): + logger.error("Mumbai, we have a major problem") + with self.assertLogs(level=logging.CRITICAL): + logger.critical("No Time For Caution") self.assertEqual(emitter_mock.emit.call_count, 2) def test_log_record_no_span_context(self): @@ -67,7 +71,8 @@ def test_log_record_no_span_context(self): ) logger = get_logger(logger_provider=emitter_provider_mock) # Assert emit gets called for warning message - logger.warning("Warning message") + with self.assertLogs(level=logging.WARNING): + logger.warning("Warning message") args, _ = emitter_mock.emit.call_args_list[0] log_record = args[0] @@ -86,7 +91,8 @@ def test_log_record_user_attributes(self): ) logger = get_logger(logger_provider=emitter_provider_mock) # Assert emit gets called for warning message - logger.warning("Warning message", extra={"http.status_code": 200}) + with self.assertLogs(level=logging.WARNING): + logger.warning("Warning message", extra={"http.status_code": 200}) args, _ = emitter_mock.emit.call_args_list[0] log_record = args[0] @@ -104,7 +110,8 @@ def test_log_record_exception(self): try: raise ZeroDivisionError("division by zero") except ZeroDivisionError: - logger.exception("Zero Division Error") + with self.assertLogs(level=logging.ERROR): + logger.exception("Zero Division Error") args, _ = emitter_mock.emit.call_args_list[0] log_record = args[0] @@ -137,7 +144,8 @@ def test_log_exc_info_false(self): try: raise ZeroDivisionError("division by zero") except ZeroDivisionError: - logger.error("Zero Division Error", exc_info=False) + with self.assertLogs(level=logging.ERROR): + logger.error("Zero Division Error", exc_info=False) args, _ = emitter_mock.emit.call_args_list[0] log_record = args[0] @@ -160,7 +168,8 @@ def test_log_record_trace_correlation(self): tracer = trace.TracerProvider().get_tracer(__name__) with tracer.start_as_current_span("test") as span: - logger.critical("Critical message within span") + with self.assertLogs(level=logging.CRITICAL): + logger.critical("Critical message within span") args, _ = emitter_mock.emit.call_args_list[0] log_record = args[0] diff --git a/opentelemetry-sdk/tests/logs/test_multi_log_processor.py b/opentelemetry-sdk/tests/logs/test_multi_log_processor.py index 0d5ac8b115..7f4bbc32c1 100644 --- a/opentelemetry-sdk/tests/logs/test_multi_log_processor.py +++ b/opentelemetry-sdk/tests/logs/test_multi_log_processor.py @@ -68,15 +68,18 @@ def test_log_record_processor(self): logger.addHandler(handler) # Test no proessor added - logger.critical("Odisha, we have another major cyclone") + with self.assertLogs(level=logging.CRITICAL): + logger.critical("Odisha, we have another major cyclone") self.assertEqual(len(logs_list_1), 0) self.assertEqual(len(logs_list_2), 0) # Add one processor provider.add_log_record_processor(processor1) - logger.warning("Brace yourself") - logger.error("Some error message") + with self.assertLogs(level=logging.WARNING): + logger.warning("Brace yourself") + with self.assertLogs(level=logging.ERROR): + logger.error("Some error message") expected_list_1 = [ ("Brace yourself", "WARNING"), @@ -86,7 +89,8 @@ def test_log_record_processor(self): # Add another processor provider.add_log_record_processor(processor2) - logger.critical("Something disastrous") + with self.assertLogs(level=logging.CRITICAL): + logger.critical("Something disastrous") expected_list_1.append(("Something disastrous", "CRITICAL")) expected_list_2 = [("Something disastrous", "CRITICAL")] diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py index 8add3e69a0..5eb1a90885 100644 --- a/opentelemetry-sdk/tests/metrics/test_instrument.py +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from logging import WARNING from unittest import TestCase from unittest.mock import Mock @@ -50,7 +51,8 @@ def test_add(self): def test_add_non_monotonic(self): mc = Mock() counter = _Counter("name", Mock(), mc) - counter.add(-1.0) + with self.assertLogs(level=WARNING): + counter.add(-1.0) mc.consume_measurement.assert_not_called() def test_disallow_direct_counter_creation(self): @@ -360,7 +362,8 @@ def test_record(self): def test_record_non_monotonic(self): mc = Mock() hist = _Histogram("name", Mock(), mc) - hist.record(-1.0) + with self.assertLogs(level=WARNING): + hist.record(-1.0) mc.consume_measurement.assert_not_called() def test_disallow_direct_histogram_creation(self): diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index da45ffe4ae..97b5532fea 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -99,7 +99,8 @@ def test_creates_view_instrument_matches( # instrument2 matches view2, so should create a single # ViewInstrumentMatch MockViewInstrumentMatch.call_args_list.clear() - storage.consume_measurement(Measurement(1, instrument2)) + with self.assertLogs(level=WARNING): + storage.consume_measurement(Measurement(1, instrument2)) self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 1) @patch( @@ -150,7 +151,8 @@ def test_forwards_calls_to_view_instrument_match( view_instrument_match3.consume_measurement.assert_not_called() measurement = Measurement(1, instrument2) - storage.consume_measurement(measurement) + with self.assertLogs(level=WARNING): + storage.consume_measurement(measurement) view_instrument_match3.consume_measurement.assert_called_once_with( measurement ) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 13efbb91c0..37a6a77aff 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -122,19 +122,21 @@ def test_get_meter_empty(self): should return a NoOpMeter. """ - meter = MeterProvider().get_meter( - None, - version="version", - schema_url="schema_url", - ) + with self.assertLogs(level=WARNING): + meter = MeterProvider().get_meter( + None, + version="version", + schema_url="schema_url", + ) self.assertIsInstance(meter, NoOpMeter) self.assertEqual(meter._name, None) - meter = MeterProvider().get_meter( - "", - version="version", - schema_url="schema_url", - ) + with self.assertLogs(level=WARNING): + meter = MeterProvider().get_meter( + "", + version="version", + schema_url="schema_url", + ) self.assertIsInstance(meter, NoOpMeter) self.assertEqual(meter._name, "") @@ -483,9 +485,10 @@ def test_duplicate_instrument_aggregate_data(self): counter_0_0 = meter_0.create_counter( "counter", unit="unit", description="description" ) - counter_0_1 = meter_0.create_counter( - "counter", unit="unit", description="description" - ) + with self.assertLogs(level=WARNING): + counter_0_1 = meter_0.create_counter( + "counter", unit="unit", description="description" + ) counter_1_0 = meter_1.create_counter( "counter", unit="unit", description="description" ) @@ -496,7 +499,8 @@ def test_duplicate_instrument_aggregate_data(self): counter_0_0.add(1, {}) counter_0_1.add(2, {}) - counter_1_0.add(7, {}) + with self.assertLogs(level=WARNING): + counter_1_0.add(7, {}) sleep(1) diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index aa0eed285d..98f59526ef 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -13,6 +13,7 @@ # limitations under the License. import math +from logging import WARNING from time import sleep, time_ns from typing import Optional, Sequence from unittest.mock import Mock @@ -127,7 +128,8 @@ def test_defaults(self): pmr = PeriodicExportingMetricReader(FakeMetricsExporter()) self.assertEqual(pmr._export_interval_millis, 60000) self.assertEqual(pmr._export_timeout_millis, 30000) - pmr.shutdown() + with self.assertLogs(level=WARNING): + pmr.shutdown() def _create_periodic_reader( self, metrics, exporter, collect_wait=0, interval=60000, timeout=30000 @@ -212,8 +214,9 @@ def test_shutdown_multiple_times(self): pmr = self._create_periodic_reader([], FakeMetricsExporter()) with self.assertLogs(level="WARNING") as w: self.run_with_many_threads(pmr.shutdown) - self.assertTrue("Can't shutdown multiple times", w.output[0]) - pmr.shutdown() + self.assertTrue("Can't shutdown multiple times", w.output[0]) + with self.assertLogs(level="WARNING") as w: + pmr.shutdown() def test_exporter_temporality_preference(self): exporter = FakeMetricsExporter( diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index bf372edc0b..1fc81c7cae 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -16,7 +16,7 @@ import unittest import uuid -from logging import ERROR +from logging import ERROR, WARNING from os import environ from unittest.mock import Mock, patch from urllib import parse @@ -221,16 +221,19 @@ def test_service_name_using_process_name(self): ) def test_invalid_resource_attribute_values(self): - resource = Resource( - { - SERVICE_NAME: "test", - "non-primitive-data-type": {}, - "invalid-byte-type-attribute": b"\xd8\xe1\xb7\xeb\xa8\xe5 \xd2\xb7\xe1", - "": "empty-key-value", - None: "null-key-value", - "another-non-primitive": uuid.uuid4(), - } - ) + with self.assertLogs(level=WARNING): + resource = Resource( + { + SERVICE_NAME: "test", + "non-primitive-data-type": {}, + "invalid-byte-type-attribute": ( + b"\xd8\xe1\xb7\xeb\xa8\xe5 \xd2\xb7\xe1" + ), + "": "empty-key-value", + None: "null-key-value", + "another-non-primitive": uuid.uuid4(), + } + ) self.assertEqual( resource.attributes, { @@ -390,12 +393,13 @@ def test_resource_detector_ignore_error(self): resource_detector = Mock(spec=ResourceDetector) resource_detector.detect.side_effect = Exception() resource_detector.raise_on_error = False - self.assertEqual( - get_aggregated_resources([resource_detector]), - _DEFAULT_RESOURCE.merge( - Resource({SERVICE_NAME: "unknown_service"}, "") - ), - ) + with self.assertLogs(level=WARNING): + self.assertEqual( + get_aggregated_resources([resource_detector]), + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") + ), + ) def test_resource_detector_raise_error(self): resource_detector = Mock(spec=ResourceDetector) @@ -470,10 +474,11 @@ def test_multiple_with_whitespace(self): def test_invalid_key_value_pairs(self): detector = OTELResourceDetector() environ[OTEL_RESOURCE_ATTRIBUTES] = "k=v,k2=v2,invalid,,foo=bar=baz," - self.assertEqual( - detector.detect(), - Resource({"k": "v", "k2": "v2", "foo": "bar=baz"}), - ) + with self.assertLogs(level=WARNING): + self.assertEqual( + detector.detect(), + Resource({"k": "v", "k2": "v2", "foo": "bar=baz"}), + ) def test_multiple_with_url_decode(self): detector = OTELResourceDetector() diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index aa1c58cb51..e64e64ade0 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -14,7 +14,7 @@ # type: ignore # pylint: skip-file -from logging import getLogger +from logging import WARNING, getLogger from os import environ from typing import Dict, Iterable, Optional, Sequence from unittest import TestCase @@ -377,7 +377,8 @@ def test_trace_init_custom_id_generator(self, mock_entry_points): ) def test_trace_init_custom_sampler_with_env_non_existent_entry_point(self): sampler_name = _get_sampler() - sampler = _import_sampler(sampler_name) + with self.assertLogs(level=WARNING): + sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsNone(provider.sampler) @@ -415,7 +416,8 @@ def test_trace_init_custom_sampler_with_env_bad_factory( ) sampler_name = _get_sampler() - sampler = _import_sampler(sampler_name) + with self.assertLogs(level=WARNING): + sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsNone(provider.sampler) @@ -492,7 +494,8 @@ def test_trace_init_custom_ratio_sampler_with_env_bad_arg( ) sampler_name = _get_sampler() - sampler = _import_sampler(sampler_name) + with self.assertLogs(level=WARNING): + sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsNone(provider.sampler) @@ -517,7 +520,8 @@ def test_trace_init_custom_ratio_sampler_with_env_missing_arg( ) sampler_name = _get_sampler() - sampler = _import_sampler(sampler_name) + with self.assertLogs(level=WARNING): + sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsNone(provider.sampler) @@ -642,7 +646,8 @@ def test_logging_init_disable_default(self, logging_mock, tracing_mock): @patch("opentelemetry.sdk._configuration._init_tracing") @patch("opentelemetry.sdk._configuration._init_logging") def test_logging_init_enable_env(self, logging_mock, tracing_mock): - _initialize_components("auto-version") + with self.assertLogs(level=WARNING): + _initialize_components("auto-version") self.assertEqual(logging_mock.call_count, 1) self.assertEqual(tracing_mock.call_count, 1) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index cb7739f328..7784ef4c9d 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -34,6 +34,7 @@ OTEL_BSP_SCHEDULE_DELAY, ) from opentelemetry.sdk.trace import export +from opentelemetry.sdk.trace.export import logger from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) @@ -208,9 +209,11 @@ def test_args_env_var_defaults(self): ) def test_args_env_var_value_error(self): + logger.disabled = True batch_span_processor = export.BatchSpanProcessor( MySpanExporter(destination=[]) ) + logger.disabled = False self.assertEqual(batch_span_processor.max_queue_size, 2048) self.assertEqual(batch_span_processor.schedule_delay_millis, 5000) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index c0b192192f..124c065c05 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -265,21 +265,27 @@ def test_instrumentation_info(self): tracer2 = tracer_provider.get_tracer("instr2", "1.3b3", schema_url) span1 = tracer1.start_span("s1") span2 = tracer2.start_span("s2") - self.assertEqual( - span1.instrumentation_info, InstrumentationInfo("instr1", "") - ) - self.assertEqual( - span2.instrumentation_info, - InstrumentationInfo("instr2", "1.3b3", schema_url), - ) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + span1.instrumentation_info, InstrumentationInfo("instr1", "") + ) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + span2.instrumentation_info, + InstrumentationInfo("instr2", "1.3b3", schema_url), + ) - self.assertEqual(span2.instrumentation_info.schema_url, schema_url) - self.assertEqual(span2.instrumentation_info.version, "1.3b3") - self.assertEqual(span2.instrumentation_info.name, "instr2") + with self.assertWarns(DeprecationWarning): + self.assertEqual(span2.instrumentation_info.schema_url, schema_url) + with self.assertWarns(DeprecationWarning): + self.assertEqual(span2.instrumentation_info.version, "1.3b3") + with self.assertWarns(DeprecationWarning): + self.assertEqual(span2.instrumentation_info.name, "instr2") - self.assertLess( - span1.instrumentation_info, span2.instrumentation_info - ) # Check sortability. + with self.assertWarns(DeprecationWarning): + self.assertLess( + span1.instrumentation_info, span2.instrumentation_info + ) # Check sortability. def test_invalid_instrumentation_info(self): tracer_provider = trace.TracerProvider() @@ -690,24 +696,34 @@ def test_attributes(self): def test_invalid_attribute_values(self): with self.tracer.start_as_current_span("root") as root: - root.set_attributes( - {"correct-value": "foo", "non-primitive-data-type": {}} - ) + with self.assertLogs(level=WARNING): + root.set_attributes( + {"correct-value": "foo", "non-primitive-data-type": {}} + ) - root.set_attribute("non-primitive-data-type", {}) - root.set_attribute( - "list-of-mixed-data-types-numeric-first", - [123, False, "string"], - ) - root.set_attribute( - "list-of-mixed-data-types-non-numeric-first", - [False, 123, "string"], - ) - root.set_attribute("list-with-non-primitive-data-type", [{}, 123]) - root.set_attribute("list-with-numeric-and-bool", [1, True]) + with self.assertLogs(level=WARNING): + root.set_attribute("non-primitive-data-type", {}) + with self.assertLogs(level=WARNING): + root.set_attribute( + "list-of-mixed-data-types-numeric-first", + [123, False, "string"], + ) + with self.assertLogs(level=WARNING): + root.set_attribute( + "list-of-mixed-data-types-non-numeric-first", + [False, 123, "string"], + ) + with self.assertLogs(level=WARNING): + root.set_attribute( + "list-with-non-primitive-data-type", [{}, 123] + ) + with self.assertLogs(level=WARNING): + root.set_attribute("list-with-numeric-and-bool", [1, True]) - root.set_attribute("", 123) - root.set_attribute(None, 123) + with self.assertLogs(level=WARNING): + root.set_attribute("", 123) + with self.assertLogs(level=WARNING): + root.set_attribute(None, 123) self.assertEqual(len(root.attributes), 1) self.assertEqual(root.attributes["correct-value"], "foo") @@ -823,10 +839,16 @@ def test_invalid_event_attributes(self): self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) with self.tracer.start_as_current_span("root") as root: - root.add_event("event0", {"attr1": True, "attr2": ["hi", False]}) - root.add_event("event0", {"attr1": {}}) - root.add_event("event0", {"attr1": [[True]]}) - root.add_event("event0", {"attr1": [{}], "attr2": [1, 2]}) + with self.assertLogs(level=WARNING): + root.add_event( + "event0", {"attr1": True, "attr2": ["hi", False]} + ) + with self.assertLogs(level=WARNING): + root.add_event("event0", {"attr1": {}}) + with self.assertLogs(level=WARNING): + root.add_event("event0", {"attr1": [[True]]}) + with self.assertLogs(level=WARNING): + root.add_event("event0", {"attr1": [{}], "attr2": [1, 2]}) self.assertEqual(len(root.events), 4) self.assertEqual(root.events[0].attributes, {"attr1": True}) @@ -1047,14 +1069,16 @@ def unset_status_test(context): root.status.description, "AssertionError: unknown" ) - unset_status_test( - trace.TracerProvider().get_tracer(__name__).start_span("root") - ) - unset_status_test( - trace.TracerProvider() - .get_tracer(__name__) - .start_as_current_span("root") - ) + with self.assertLogs(level=WARNING): + unset_status_test( + trace.TracerProvider().get_tracer(__name__).start_span("root") + ) + with self.assertLogs(level=WARNING): + unset_status_test( + trace.TracerProvider() + .get_tracer(__name__) + .start_as_current_span("root") + ) def test_last_status_wins(self): def error_status_test(context): diff --git a/pyproject.toml b/pyproject.toml index a20a405675..27d530a7f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,4 +17,3 @@ exclude = ''' [tool.pytest.ini_options] addopts = "-rs -v" log_cli = true -log_cli_level = "warning" From 84357bff18ceea60384c0cddc28888556fcf2835 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Wed, 5 Jul 2023 11:47:20 +0100 Subject: [PATCH 1444/1517] Re-export public symbols in `opentelemetry.context` (#3332) --- .../src/opentelemetry/context/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 3e85b64fe4..d170089812 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -172,3 +172,13 @@ def detach(token: object) -> None: _SUPPRESS_HTTP_INSTRUMENTATION_KEY = create_key( "suppress_http_instrumentation" ) + +__all__ = [ + "Context", + "attach", + "create_key", + "detach", + "get_current", + "get_value", + "set_value", +] From 570d27ed3235085ead5a47b42b0d99d873a7a9c7 Mon Sep 17 00:00:00 2001 From: Sebastian Kreft Date: Wed, 5 Jul 2023 08:25:05 -0400 Subject: [PATCH 1445/1517] feat: include endpoint in grpc logs (#3362) Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 ++ .../exporter/otlp/proto/grpc/exporter.py | 18 +++++++++++------- .../tests/test_otlp_exporter_mixin.py | 4 ++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d98b5e97..48929707db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3341](https://github.com/open-telemetry/opentelemetry-python/pull/3341)) - Upgrade opentelemetry-proto to 0.20 and regen [#3355](https://github.com/open-telemetry/opentelemetry-python/pull/3355)) +- Include endpoint in Grpc transient error warning + [#3362](https://github.com/open-telemetry/opentelemetry-python/pull/3362)) ## Version 1.18.0/0.39b0 (2023-05-04) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index fa041539c7..243c88b08f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -178,11 +178,11 @@ def __init__( ): super().__init__() - endpoint = endpoint or environ.get( + self._endpoint = endpoint or environ.get( OTEL_EXPORTER_OTLP_ENDPOINT, "http://localhost:4317" ) - parsed_url = urlparse(endpoint) + parsed_url = urlparse(self._endpoint) if parsed_url.scheme == "https": insecure = False @@ -197,7 +197,7 @@ def __init__( insecure = False if parsed_url.netloc: - endpoint = parsed_url.netloc + self._endpoint = parsed_url.netloc self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS) if isinstance(self._headers, str): @@ -223,14 +223,16 @@ def __init__( if insecure: self._client = self._stub( - insecure_channel(endpoint, compression=compression) + insecure_channel(self._endpoint, compression=compression) ) else: credentials = _get_credentials( credentials, OTEL_EXPORTER_OTLP_CERTIFICATE ) self._client = self._stub( - secure_channel(endpoint, credentials, compression=compression) + secure_channel( + self._endpoint, credentials, compression=compression + ) ) self._export_lock = threading.Lock() @@ -304,18 +306,20 @@ def _export( logger.warning( ( "Transient error %s encountered while exporting " - "%s, retrying in %ss." + "%s to %s, retrying in %ss." ), error.code(), self._exporting, + self._endpoint, delay, ) sleep(delay) continue else: logger.error( - "Failed to export %s, error code: %s", + "Failed to export %s to %s, error code: %s", self._exporting, + self._endpoint, error.code(), ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index c757755740..f1b95986da 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -93,7 +93,7 @@ def _exporting(self) -> str: otlp_mock_exporter._export(Mock()) self.assertEqual( warning.records[0].message, - "Failed to export mock, error code: None", + "Failed to export mock to localhost:4317, error code: None", ) def code(self): # pylint: disable=function-redefined @@ -112,7 +112,7 @@ def trailing_metadata(self): warning.records[0].message, ( "Transient error StatusCode.CANCELLED encountered " - "while exporting mock, retrying in 0s." + "while exporting mock to localhost:4317, retrying in 0s." ), ) From 3f459d3a19fa6c4bbdeb9012c4a34f714d8cca1a Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 5 Jul 2023 16:20:08 +0200 Subject: [PATCH 1446/1517] Add test case for child-parent exception handling (#3356) --- .../src/opentelemetry/trace/__init__.py | 5 + opentelemetry-sdk/tests/trace/test_trace.py | 92 ++++++++++++++++++- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 304df22754..bf9e0b89a4 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -588,6 +588,11 @@ def use_span( description=f"{type(exc).__name__}: {exc}", ) ) + + # This causes parent spans to set their status to ERROR and to record + # an exception as an event if a child span raises an exception even if + # such child span was started with both record_exception and + # set_status_on_exception attributes set to False. raise finally: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 124c065c05..a49b86b851 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -55,7 +55,12 @@ get_span_with_dropped_attributes_events_links, new_tracer, ) -from opentelemetry.trace import Status, StatusCode +from opentelemetry.trace import ( + Status, + StatusCode, + get_tracer, + set_tracer_provider, +) class TestTracer(unittest.TestCase): @@ -1852,3 +1857,88 @@ def test_constant_default_trace_options(self): self.assertEqual( trace_api.DEFAULT_TRACE_OPTIONS, trace_api.TraceFlags.DEFAULT ) + + +class TestParentChildSpanException(unittest.TestCase): + def test_parent_child_span_exception(self): + """ + Tests that a parent span has its status set to ERROR when a child span + raises an exception even when the child span has its + ``record_exception`` and ``set_status_on_exception`` attributes + set to ``False``. + """ + + set_tracer_provider(TracerProvider()) + tracer = get_tracer(__name__) + + exception = Exception("exception") + + exception_type = exception.__class__.__name__ + exception_message = exception.args[0] + + try: + with tracer.start_as_current_span( + "parent", + ) as parent_span: + with tracer.start_as_current_span( + "child", + record_exception=False, + set_status_on_exception=False, + ) as child_span: + raise exception + + except Exception: # pylint: disable=broad-except + pass + + self.assertTrue(child_span.status.is_ok) + self.assertIsNone(child_span.status.description) + self.assertTupleEqual(child_span.events, ()) + + self.assertFalse(parent_span.status.is_ok) + self.assertEqual( + parent_span.status.description, + f"{exception_type}: {exception_message}", + ) + self.assertEqual( + parent_span.events[0].attributes["exception.type"], exception_type + ) + self.assertEqual( + parent_span.events[0].attributes["exception.message"], + exception_message, + ) + + def test_child_parent_span_exception(self): + """ + Tests that a child span does not have its status set to ERROR when a + parent span raises an exception and the parent span has its + ``record_exception`` and ``set_status_on_exception`` attributes + set to ``False``. + """ + + set_tracer_provider(TracerProvider()) + tracer = get_tracer(__name__) + + exception = Exception("exception") + + try: + with tracer.start_as_current_span( + "parent", + record_exception=False, + set_status_on_exception=False, + ) as parent_span: + with tracer.start_as_current_span( + "child", + ) as child_span: + pass + raise exception + + except Exception: # pylint: disable=broad-except + pass + + self.assertTrue(child_span.status.is_ok) + self.assertIsNone(child_span.status.description) + self.assertTupleEqual(child_span.events, ()) + + self.assertTrue(parent_span.status.is_ok) + self.assertIsNone(parent_span.status.description) + self.assertTupleEqual(parent_span.events, ()) From e076e7cc04159288da9cda5fed0183980f05f63c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 6 Jul 2023 14:56:56 +0200 Subject: [PATCH 1447/1517] Fix getting started test (#3371) --- .github/workflows/test.yml | 2 +- docs/getting_started/tests/test_flask.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db3a931e6e..f1355d9e41 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: a5ed4da478c4360fd6e24893f7574b150431b7ee + CONTRIB_REPO_SHA: dadcd01524449ddee07a8d8405890a60caeb8c8e # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/docs/getting_started/tests/test_flask.py b/docs/getting_started/tests/test_flask.py index 92425dc5cf..b7a5b46d5a 100644 --- a/docs/getting_started/tests/test_flask.py +++ b/docs/getting_started/tests/test_flask.py @@ -44,6 +44,6 @@ def test_flask(self): server.terminate() output = str(server.stdout.read()) - self.assertIn('"name": "HTTP GET"', output) + self.assertIn('"name": "GET"', output) self.assertIn('"name": "example-request"', output) self.assertIn('"name": "/"', output) From 72c1f5bee5a18098f2c643b6e8d369566d12e3d9 Mon Sep 17 00:00:00 2001 From: Jeremy Voss Date: Tue, 11 Jul 2023 08:45:58 -0700 Subject: [PATCH 1448/1517] Fixing bug where BLRP export tracked as tracing (#3375) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/_logs/_internal/export/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48929707db..ea178d85f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#3355](https://github.com/open-telemetry/opentelemetry-python/pull/3355)) - Include endpoint in Grpc transient error warning [#3362](https://github.com/open-telemetry/opentelemetry-python/pull/3362)) +- Fixed bug where logging export is tracked as trace + [#3375](https://github.com/open-telemetry/opentelemetry-python/pull/3375)) ## Version 1.18.0/0.39b0 (2023-05-04) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py index 4903873432..8ccad56c7a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -307,7 +307,7 @@ def _export_batch(self) -> int: record = self._queue.pop() self._log_records[idx] = record idx += 1 - token = attach(set_value("suppress_instrumentation", True)) + token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: self._exporter.export(self._log_records[:idx]) # type: ignore except Exception: # pylint: disable=broad-except From 98985b5ab469d07a78f2a35ed04621c7b7b6558b Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Wed, 12 Jul 2023 10:02:15 -0700 Subject: [PATCH 1449/1517] Copy change log updates from release/v1.18.x-0.39bx (#3312) Co-authored-by: Diego Hurtado --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea178d85f5..d023b24996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed bug where logging export is tracked as trace [#3375](https://github.com/open-telemetry/opentelemetry-python/pull/3375)) -## Version 1.18.0/0.39b0 (2023-05-04) +## Version 1.18.0/0.39b0 (2023-05-19) - Select histogram aggregation with an environment variable ([#3265](https://github.com/open-telemetry/opentelemetry-python/pull/3265)) From 7bb21f5ee375a761c352b7848ea8b71e86f61547 Mon Sep 17 00:00:00 2001 From: GanymedeNil Date: Thu, 13 Jul 2023 01:50:24 +0800 Subject: [PATCH 1450/1517] Update the body type in the log (#3343) Co-authored-by: Shalev Roda <65566801+shalevr@users.noreply.github.com> Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 4 +- .../sdk/_logs/_internal/__init__.py | 45 ++++++++++++++++++- opentelemetry-sdk/tests/logs/test_export.py | 34 ++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d023b24996..2f84949791 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - +- Update the body type in the log + ([$3343](https://github.com/open-telemetry/opentelemetry-python/pull/3343)) - Add max_scale option to Exponential Bucket Histogram Aggregation ([#3323](https://github.com/open-telemetry/opentelemetry-python/pull/3323)) - Use BoundedAttributes instead of raw dict to extract attributes from LogRecord @@ -23,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed bug where logging export is tracked as trace [#3375](https://github.com/open-telemetry/opentelemetry-python/pull/3375)) + ## Version 1.18.0/0.39b0 (2023-05-19) - Select histogram aggregation with an environment variable diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 578ce2c391..eb5f50e9e0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -21,7 +21,7 @@ import traceback from os import environ from time import time_ns -from typing import Any, Callable, Optional, Tuple, Union +from typing import Any, Callable, Optional, Tuple, Union # noqa from opentelemetry._logs import Logger as APILogger from opentelemetry._logs import LoggerProvider as APILoggerProvider @@ -476,7 +476,48 @@ def _translate(self, record: logging.LogRecord) -> LogRecord: timestamp = int(record.created * 1e9) span_context = get_current_span().get_span_context() attributes = self._get_attributes(record) + # This comment is taken from GanyedeNil's PR #3343, I have redacted it + # slightly for clarity: + # According to the definition of the Body field type in the + # OTel 1.22.0 Logs Data Model article, the Body field should be of + # type 'any' and should not use the str method to directly translate + # the msg. This is because str only converts non-text types into a + # human-readable form, rather than a standard format, which leads to + # the need for additional operations when collected through a log + # collector. + # Considering that he Body field should be of type 'any' and should not + # use the str method but record.msg is also a string type, then the + # difference is just the self.args formatting? + # The primary consideration depends on the ultimate purpose of the log. + # Converting the default log directly into a string is acceptable as it + # will be required to be presented in a more readable format. However, + # this approach might not be as "standard" when hoping to aggregate + # logs and perform subsequent data analysis. In the context of log + # extraction, it would be more appropriate for the msg to be + # converted into JSON format or remain unchanged, as it will eventually + # be transformed into JSON. If the final output JSON data contains a + # structure that appears similar to JSON but is not, it may confuse + # users. This is particularly true for operation and maintenance + # personnel who need to deal with log data in various languages. + # Where is the JSON converting occur? and what about when the msg + # represents something else but JSON, the expected behavior change? + # For the ConsoleLogExporter, it performs the to_json operation in + # opentelemetry.sdk._logs._internal.export.ConsoleLogExporter.__init__, + # so it can handle any type of input without problems. As for the + # OTLPLogExporter, it also handles any type of input encoding in + # _encode_log located in + # opentelemetry.exporter.otlp.proto.common._internal._log_encoder. + # Therefore, no extra operation is needed to support this change. + # The only thing to consider is the users who have already been using + # this SDK. If they upgrade the SDK after this change, they will need + # to readjust their logging collection rules to adapt to the latest + # output format. Therefore, this change is considered a breaking + # change and needs to be upgraded at an appropriate time. severity_number = std_to_otel(record.levelno) + if isinstance(record.msg, str) and record.args: + body = record.msg % record.args + else: + body = record.msg return LogRecord( timestamp=timestamp, trace_id=span_context.trace_id, @@ -484,7 +525,7 @@ def _translate(self, record: logging.LogRecord) -> LogRecord: trace_flags=span_context.trace_flags, severity_text=record.levelname, severity_number=severity_number, - body=record.getMessage(), + body=body, resource=self._logger.resource, attributes=attributes, ) diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 0a33e941ff..2828504eaa 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -173,6 +173,40 @@ def test_simple_log_record_processor_shutdown(self): finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 0) + def test_simple_log_record_processor_different_msg_types(self): + exporter = InMemoryLogExporter() + log_record_processor = BatchLogRecordProcessor(exporter) + + provider = LoggerProvider() + provider.add_log_record_processor(log_record_processor) + + logger = logging.getLogger("different_msg_types") + logger.addHandler(LoggingHandler(logger_provider=provider)) + + logger.warning("warning message: %s", "possible upcoming heatwave") + logger.error("Very high rise in temperatures across the globe") + logger.critical("Temperature hits high 420 C in Hyderabad") + logger.warning(["list", "of", "strings"]) + logger.error({"key": "value"}) + log_record_processor.shutdown() + + finished_logs = exporter.get_finished_logs() + expected = [ + ("warning message: possible upcoming heatwave", "WARNING"), + ("Very high rise in temperatures across the globe", "ERROR"), + ( + "Temperature hits high 420 C in Hyderabad", + "CRITICAL", + ), + (["list", "of", "strings"], "WARNING"), + ({"key": "value"}, "ERROR"), + ] + emitted = [ + (item.log_record.body, item.log_record.severity_text) + for item in finished_logs + ] + self.assertEqual(expected, emitted) + class TestBatchLogRecordProcessor(ConcurrencyTestBase): def test_emit_call_log_record(self): From f86d2fddaa2acdc9390a2f9e297d5ffe83736c34 Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Wed, 12 Jul 2023 20:12:44 +0200 Subject: [PATCH 1451/1517] Remove setuptools runtime dependency (#3372) Co-authored-by: Diego Hurtado --- CHANGELOG.md | 4 +++- opentelemetry-api/pyproject.toml | 1 - .../src/opentelemetry/_logs/_internal/__init__.py | 2 +- opentelemetry-sdk/pyproject.toml | 1 - 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f84949791..89242259a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Drop `setuptools` runtime requirement. + ([#3372](https://github.com/open-telemetry/opentelemetry-python/pull/3372)) - Update the body type in the log ([$3343](https://github.com/open-telemetry/opentelemetry-python/pull/3343)) - Add max_scale option to Exponential Bucket Histogram Aggregation ([#3323](https://github.com/open-telemetry/opentelemetry-python/pull/3323)) - Use BoundedAttributes instead of raw dict to extract attributes from LogRecord - ([#3310](https://github.com/open-telemetry/opentelemetry-python/pull/3310)) + ([#3310](https://github.com/open-telemetry/opentelemetry-python/pull/3310)) - Support dropped_attributes_count in LogRecord and exporters ([#3351](https://github.com/open-telemetry/opentelemetry-python/pull/3351)) - Add unit to view instrument selection criteria diff --git a/opentelemetry-api/pyproject.toml b/opentelemetry-api/pyproject.toml index dcc9a2f168..f659d77757 100644 --- a/opentelemetry-api/pyproject.toml +++ b/opentelemetry-api/pyproject.toml @@ -26,7 +26,6 @@ classifiers = [ ] dependencies = [ "Deprecated >= 1.2.6", - "setuptools >= 16.0", # FIXME This should be able to be removed after 3.12 is released if there is a reliable API # in importlib.metadata. "importlib-metadata ~= 6.0", diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index 0b844e24d9..a2cec065cd 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -143,7 +143,7 @@ def get_logger( version: Optional. The version string of the instrumenting library. Usually this should be the same as - ``pkg_resources.get_distribution(instrumenting_library_name).version``. + ``importlib.metadata.version(instrumenting_library_name)``. schema_url: Optional. Specifies the Schema URL of the emitted telemetry. """ diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index 88f3ca8f01..d4fe2d29af 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -28,7 +28,6 @@ classifiers = [ dependencies = [ "opentelemetry-api == 1.19.0.dev", "opentelemetry-semantic-conventions == 0.40b0.dev", - "setuptools >= 16.0", "typing-extensions >= 3.7.4", ] From 04ab744039a8745b2cfd9235bea1f96e9e27f3dd Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Wed, 12 Jul 2023 19:24:49 +0100 Subject: [PATCH 1452/1517] Finish typing `InMemorySpanExporter` (#3285) --- .../sdk/trace/export/in_memory_span_exporter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py index de86fba277..c28ecfd214 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py @@ -27,17 +27,17 @@ class InMemorySpanExporter(SpanExporter): :func:`.get_finished_spans` method. """ - def __init__(self): - self._finished_spans = [] + def __init__(self) -> None: + self._finished_spans: typing.List[ReadableSpan] = [] self._stopped = False self._lock = threading.Lock() - def clear(self): + def clear(self) -> None: """Clear list of collected spans.""" with self._lock: self._finished_spans.clear() - def get_finished_spans(self): + def get_finished_spans(self) -> typing.Tuple[ReadableSpan, ...]: """Get list of collected spans.""" with self._lock: return tuple(self._finished_spans) @@ -50,7 +50,7 @@ def export(self, spans: typing.Sequence[ReadableSpan]) -> SpanExportResult: self._finished_spans.extend(spans) return SpanExportResult.SUCCESS - def shutdown(self): + def shutdown(self) -> None: """Shut downs the exporter. Calls to export after the exporter has been shut down will fail. From 57d4580ddb65a0f63418dccfcb8516ca69469639 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Jul 2023 21:57:24 +0200 Subject: [PATCH 1453/1517] Bump grpcio from 1.35.0 to 1.53.0 in /docs/examples/fork-process-model/flask-uwsgi (#3368) --- docs/examples/fork-process-model/flask-uwsgi/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt index 3f9f6f6454..7e734653f5 100644 --- a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt +++ b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt @@ -1,7 +1,7 @@ click==7.1.2 Flask==1.1.2 googleapis-common-protos==1.52.0 -grpcio==1.35.0 +grpcio==1.53.0 gunicorn==20.0.4 itsdangerous==1.1.0 Jinja2==2.11.3 From c094ca81d624cd7d37dc7981d86f600598ce39d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Jul 2023 23:28:39 +0200 Subject: [PATCH 1454/1517] Bump grpcio from 1.35.0 to 1.53.0 in /docs/examples/fork-process-model/flask-gunicorn (#3369) --- .../examples/fork-process-model/flask-gunicorn/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt index 3f9f6f6454..7e734653f5 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt +++ b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt @@ -1,7 +1,7 @@ click==7.1.2 Flask==1.1.2 googleapis-common-protos==1.52.0 -grpcio==1.35.0 +grpcio==1.53.0 gunicorn==20.0.4 itsdangerous==1.1.0 Jinja2==2.11.3 From 93992cefcd7b13f97311923055684fdf97764e9a Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Thu, 13 Jul 2023 07:36:39 -0700 Subject: [PATCH 1455/1517] Update version to 1.20.0.dev/0.41b0.dev (#3378) Co-authored-by: Diego Hurtado --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/pyproject.toml | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-common/pyproject.toml | 2 +- .../src/opentelemetry/exporter/otlp/proto/common/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-grpc/pyproject.toml | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-http/pyproject.toml | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/pyproject.toml | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/pyproject.toml | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opencensus-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opencensus/version.py | 2 +- shim/opentelemetry-opentracing-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/pyproject.toml | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 36 files changed, 47 insertions(+), 45 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f1355d9e41..3c96e5084b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: dadcd01524449ddee07a8d8405890a60caeb8c8e + CONTRIB_REPO_SHA: d1fb91ae0c075751b580ff4892bae44749d5810d # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 89242259a1..7c2140367c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## Version 1.19.0/0.40b0 (2023-07-13) + - Drop `setuptools` runtime requirement. ([#3372](https://github.com/open-telemetry/opentelemetry-python/pull/3372)) - Update the body type in the log diff --git a/eachdist.ini b/eachdist.ini index 896dabb66e..84bed7b943 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.19.0.dev +version=1.20.0.dev packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.40b0.dev +version=0.41b0.dev packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 8e6645935a..31245d9f80 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 8e6645935a..31245d9f80 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger/pyproject.toml b/exporter/opentelemetry-exporter-jaeger/pyproject.toml index cc7c45cbd1..7cbf2ea535 100644 --- a/exporter/opentelemetry-exporter-jaeger/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-jaeger-proto-grpc == 1.19.0.dev", - "opentelemetry-exporter-jaeger-thrift == 1.19.0.dev", + "opentelemetry-exporter-jaeger-proto-grpc == 1.20.0.dev", + "opentelemetry-exporter-jaeger-thrift == 1.20.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 8e6645935a..31245d9f80 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/exporter/opentelemetry-exporter-opencensus/pyproject.toml b/exporter/opentelemetry-exporter-opencensus/pyproject.toml index f906b87bb7..a00beff976 100644 --- a/exporter/opentelemetry-exporter-opencensus/pyproject.toml +++ b/exporter/opentelemetry-exporter-opencensus/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ dependencies = [ "grpcio >= 1.0.0, < 2.0.0", "opencensus-proto >= 0.1.0, < 1.0.0", - "opentelemetry-api >= 1.19.0.dev", + "opentelemetry-api >= 1.20.0.dev", "opentelemetry-sdk >= 1.15", "protobuf ~= 3.13", "setuptools >= 16.0", diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 87b20fddc3..7f88144cf6 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.40b0.dev" +__version__ = "0.41b0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml index 64e2767dee..a9917d492e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ - "opentelemetry-proto == 1.19.0.dev", + "opentelemetry-proto == 1.20.0.dev", ] [project.urls] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py index 168a28bae9..270451efb0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index ebeaa35745..475c431aed 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -31,9 +31,9 @@ dependencies = [ "googleapis-common-protos ~= 1.52", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.19.0.dev", - "opentelemetry-sdk ~= 1.19.0.dev", - "opentelemetry-exporter-otlp-proto-common == 1.19.0.dev", + "opentelemetry-proto == 1.20.0.dev", + "opentelemetry-sdk ~= 1.20.0.dev", + "opentelemetry-exporter-otlp-proto-common == 1.20.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 168a28bae9..270451efb0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index 10eeafccf5..e3b7f36ada 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -30,9 +30,9 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.19.0.dev", - "opentelemetry-sdk ~= 1.19.0.dev", - "opentelemetry-exporter-otlp-proto-common == 1.19.0.dev", + "opentelemetry-proto == 1.20.0.dev", + "opentelemetry-sdk ~= 1.20.0.dev", + "opentelemetry-exporter-otlp-proto-common == 1.20.0.dev", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 168a28bae9..270451efb0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index a2bb316e36..61aeb44f0d 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-otlp-proto-grpc == 1.19.0.dev", - "opentelemetry-exporter-otlp-proto-http == 1.19.0.dev", + "opentelemetry-exporter-otlp-proto-grpc == 1.20.0.dev", + "opentelemetry-exporter-otlp-proto-http == 1.20.0.dev", ] [project.entry-points.opentelemetry_logs_exporter] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 168a28bae9..270451efb0 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 87b20fddc3..7f88144cf6 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.40b0.dev" +__version__ = "0.41b0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 168a28bae9..270451efb0 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml index 6e0069848f..060fab4a05 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.3", - "opentelemetry-exporter-zipkin-json == 1.19.0.dev", + "opentelemetry-exporter-zipkin-json == 1.20.0.dev", "opentelemetry-sdk ~= 1.11", "protobuf ~= 3.12", "requests ~= 2.7", diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 168a28bae9..270451efb0 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin/pyproject.toml b/exporter/opentelemetry-exporter-zipkin/pyproject.toml index 5a1325e20c..88fb8488a6 100644 --- a/exporter/opentelemetry-exporter-zipkin/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-zipkin-json == 1.19.0.dev", - "opentelemetry-exporter-zipkin-proto-http == 1.19.0.dev", + "opentelemetry-exporter-zipkin-json == 1.20.0.dev", + "opentelemetry-exporter-zipkin-proto-http == 1.20.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 168a28bae9..270451efb0 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 168a28bae9..270451efb0 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 168a28bae9..270451efb0 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index d4fe2d29af..f9edd23542 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-api == 1.19.0.dev", - "opentelemetry-semantic-conventions == 0.40b0.dev", + "opentelemetry-api == 1.20.0.dev", + "opentelemetry-semantic-conventions == 0.41b0.dev", "typing-extensions >= 3.7.4", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 168a28bae9..270451efb0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 87b20fddc3..7f88144cf6 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.40b0.dev" +__version__ = "0.41b0.dev" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 168a28bae9..270451efb0 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 168a28bae9..270451efb0 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0.dev" +__version__ = "1.20.0.dev" diff --git a/shim/opentelemetry-opencensus-shim/pyproject.toml b/shim/opentelemetry-opencensus-shim/pyproject.toml index 19b072b19a..c6dac03c66 100644 --- a/shim/opentelemetry-opencensus-shim/pyproject.toml +++ b/shim/opentelemetry-opencensus-shim/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.40b0.dev", + "opentelemetry-test-utils == 0.41b0.dev", "opencensus == 0.11.1", ] diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py index 87b20fddc3..7f88144cf6 100644 --- a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.40b0.dev" +__version__ = "0.41b0.dev" diff --git a/shim/opentelemetry-opentracing-shim/pyproject.toml b/shim/opentelemetry-opentracing-shim/pyproject.toml index 5ef3579520..ed0011c47e 100644 --- a/shim/opentelemetry-opentracing-shim/pyproject.toml +++ b/shim/opentelemetry-opentracing-shim/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.40b0.dev", + "opentelemetry-test-utils == 0.41b0.dev", "opentracing ~= 2.2.0", ] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 87b20fddc3..7f88144cf6 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.40b0.dev" +__version__ = "0.41b0.dev" diff --git a/tests/opentelemetry-test-utils/pyproject.toml b/tests/opentelemetry-test-utils/pyproject.toml index 41fc56244f..e1e5684e51 100644 --- a/tests/opentelemetry-test-utils/pyproject.toml +++ b/tests/opentelemetry-test-utils/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ ] dependencies = [ "asgiref ~= 3.0", - "opentelemetry-api == 1.19.0.dev", - "opentelemetry-sdk == 1.19.0.dev", + "opentelemetry-api == 1.20.0.dev", + "opentelemetry-sdk == 1.20.0.dev", ] [project.optional-dependencies] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index a4a2694c89..392d092d1e 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.40b0.dev" +__version__ = "0.41b0.dev" From 8378db984edbc2a0532c5316e82df1ac649ba227 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 17 Jul 2023 17:28:40 +0200 Subject: [PATCH 1456/1517] Update grpcio version (#3384) --- .../examples/fork-process-model/flask-gunicorn/requirements.txt | 2 +- docs/examples/fork-process-model/flask-uwsgi/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt index 7e734653f5..ab140df2ae 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt +++ b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt @@ -1,7 +1,7 @@ click==7.1.2 Flask==1.1.2 googleapis-common-protos==1.52.0 -grpcio==1.53.0 +grpcio==1.56.0 gunicorn==20.0.4 itsdangerous==1.1.0 Jinja2==2.11.3 diff --git a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt index 7e734653f5..ab140df2ae 100644 --- a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt +++ b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt @@ -1,7 +1,7 @@ click==7.1.2 Flask==1.1.2 googleapis-common-protos==1.52.0 -grpcio==1.53.0 +grpcio==1.56.0 gunicorn==20.0.4 itsdangerous==1.1.0 Jinja2==2.11.3 From b9f31e91a8263bc8effc9abce889d438927d47ec Mon Sep 17 00:00:00 2001 From: Jeremy Voss Date: Thu, 27 Jul 2023 06:27:51 -0700 Subject: [PATCH 1457/1517] Default Observed Timestamp (#3377) Co-authored-by: Diego Hurtado --- CHANGELOG.md | 2 ++ .../opentelemetry/_logs/_internal/__init__.py | 3 +++ .../tests/logs/test_log_record.py | 27 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 opentelemetry-api/tests/logs/test_log_record.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c2140367c..9416dad981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#3362](https://github.com/open-telemetry/opentelemetry-python/pull/3362)) - Fixed bug where logging export is tracked as trace [#3375](https://github.com/open-telemetry/opentelemetry-python/pull/3375)) +- Default LogRecord observed_timestamp to current timestamp + [#3377](https://github.com/open-telemetry/opentelemetry-python/pull/3377)) ## Version 1.18.0/0.39b0 (2023-05-19) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index a2cec065cd..c6d35dbf07 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -37,6 +37,7 @@ from abc import ABC, abstractmethod from logging import getLogger from os import environ +from time import time_ns from typing import Any, Optional, cast from opentelemetry._logs.severity import SeverityNumber @@ -70,6 +71,8 @@ def __init__( attributes: Optional["Attributes"] = None, ): self.timestamp = timestamp + if observed_timestamp is None: + observed_timestamp = time_ns() self.observed_timestamp = observed_timestamp self.trace_id = trace_id self.span_id = span_id diff --git a/opentelemetry-api/tests/logs/test_log_record.py b/opentelemetry-api/tests/logs/test_log_record.py new file mode 100644 index 0000000000..a06ed8dabf --- /dev/null +++ b/opentelemetry-api/tests/logs/test_log_record.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import patch + +from opentelemetry._logs import LogRecord + +OBSERVED_TIMESTAMP = "OBSERVED_TIMESTAMP" + + +class TestLogRecord(unittest.TestCase): + @patch("opentelemetry._logs._internal.time_ns") + def test_log_record_observed_timestamp_default(self, time_ns_mock): # type: ignore + time_ns_mock.return_value = OBSERVED_TIMESTAMP + self.assertEqual(LogRecord().observed_timestamp, OBSERVED_TIMESTAMP) From c9277ffb137fad999ee14bee4f99bad2d00b8b03 Mon Sep 17 00:00:00 2001 From: Brett Beutell Date: Mon, 7 Aug 2023 15:15:06 +0200 Subject: [PATCH 1458/1517] Modify Prometheus exporter to translate non-monotonic Sums into gauges (#3306) Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 5 +- .../exporter/prometheus/__init__.py | 25 ++++++++- .../tests/test_prometheus_exporter.py | 54 +++++++++++++++++-- .../src/opentelemetry/test/metrictestutil.py | 9 +++- 4 files changed, 85 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9416dad981..f0b9ab2dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Modify Prometheus exporter to translate non-monotonic Sums into Gauges + ([#3306](https://github.com/open-telemetry/opentelemetry-python/pull/3306)) + + ## Version 1.19.0/0.40b0 (2023-07-13) - Drop `setuptools` runtime requirement. @@ -91,7 +95,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Create a single resource instance ([#3118](https://github.com/open-telemetry/opentelemetry-python/pull/3118)) - ## Version 1.15.0/0.36b0 (2022-12-09) - PeriodicExportingMetricsReader with +Inf interval diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 9ece76755c..252f240b35 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -263,7 +263,25 @@ def _translate_to_prometheus( for pre_metric_family_id, label_values, value in zip( pre_metric_family_ids, label_valuess, values ): - if isinstance(metric.data, Sum): + is_non_monotonic_sum = ( + isinstance(metric.data, Sum) + and metric.data.is_monotonic is False + ) + is_cumulative = ( + isinstance(metric.data, Sum) + and metric.data.aggregation_temporality + == AggregationTemporality.CUMULATIVE + ) + + # The prometheus compatibility spec for sums says: If the aggregation temporality is cumulative and the sum is non-monotonic, it MUST be converted to a Prometheus Gauge. + should_convert_sum_to_gauge = ( + is_non_monotonic_sum and is_cumulative + ) + + if ( + isinstance(metric.data, Sum) + and not should_convert_sum_to_gauge + ): metric_family_id = "|".join( [pre_metric_family_id, CounterMetricFamily.__name__] @@ -281,7 +299,10 @@ def _translate_to_prometheus( metric_family_id_metric_family[ metric_family_id ].add_metric(labels=label_values, value=value) - elif isinstance(metric.data, Gauge): + elif ( + isinstance(metric.data, Gauge) + or should_convert_sum_to_gauge + ): metric_family_id = "|".join( [pre_metric_family_id, GaugeMetricFamily.__name__] diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index c7ce1afae1..a9ab05d01e 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -125,10 +125,10 @@ def test_histogram_to_prometheus(self): ), ) - def test_sum_to_prometheus(self): + def test_monotonic_sum_to_prometheus(self): labels = {"environment@": "staging", "os": "Windows"} metric = _generate_sum( - "test@sum", + "test@sum_monotonic", 123, attributes=labels, description="testdesc", @@ -156,7 +156,55 @@ def test_sum_to_prometheus(self): for prometheus_metric in collector.collect(): self.assertEqual(type(prometheus_metric), CounterMetricFamily) - self.assertEqual(prometheus_metric.name, "test_sum_testunit") + self.assertEqual( + prometheus_metric.name, "test_sum_monotonic_testunit" + ) + self.assertEqual(prometheus_metric.documentation, "testdesc") + self.assertTrue(len(prometheus_metric.samples) == 1) + self.assertEqual(prometheus_metric.samples[0].value, 123) + self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) + self.assertEqual( + prometheus_metric.samples[0].labels["environment_"], "staging" + ) + self.assertEqual( + prometheus_metric.samples[0].labels["os"], "Windows" + ) + + def test_non_monotonic_sum_to_prometheus(self): + labels = {"environment@": "staging", "os": "Windows"} + metric = _generate_sum( + "test@sum_nonmonotonic", + 123, + attributes=labels, + description="testdesc", + unit="testunit", + is_monotonic=False, + ) + + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Mock(), + scope_metrics=[ + ScopeMetrics( + scope=Mock(), + metrics=[metric], + schema_url="schema_url", + ) + ], + schema_url="schema_url", + ) + ] + ) + + collector = _CustomCollector(disable_target_info=True) + collector.add_metrics_data(metrics_data) + + for prometheus_metric in collector.collect(): + self.assertEqual(type(prometheus_metric), GaugeMetricFamily) + self.assertEqual( + prometheus_metric.name, "test_sum_nonmonotonic_testunit" + ) self.assertEqual(prometheus_metric.documentation, "testdesc") self.assertTrue(len(prometheus_metric.samples) == 1) self.assertEqual(prometheus_metric.samples[0].value, 123) diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py index 895904af03..ff25b092a6 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py @@ -39,7 +39,12 @@ def _generate_metric( def _generate_sum( - name, value, attributes=None, description=None, unit=None + name, + value, + attributes=None, + description=None, + unit=None, + is_monotonic=True, ) -> Metric: if attributes is None: attributes = BoundedAttributes(attributes={"a": 1, "b": True}) @@ -55,7 +60,7 @@ def _generate_sum( ) ], aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, + is_monotonic=is_monotonic, ), description=description, unit=unit, From 9b713db33140f2abaae7778f84923afe67744e84 Mon Sep 17 00:00:00 2001 From: Juliano Costa Date: Thu, 17 Aug 2023 17:30:51 +0200 Subject: [PATCH 1459/1517] Fix copy and paste issue (#3404) --- .../src/opentelemetry/sdk/environment_variables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 273aebd9f1..10031e5ef3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -277,14 +277,14 @@ """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_PROTOCOL -The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for metrics. +The :envvar:`OTEL_EXPORTER_OTLP_METRICS_PROTOCOL` represents the the transport protocol for metrics. """ OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL" """ .. envvar:: OTEL_EXPORTER_OTLP_LOGS_PROTOCOL -The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for logs. +The :envvar:`OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` represents the the transport protocol for logs. """ OTEL_EXPORTER_OTLP_CERTIFICATE = "OTEL_EXPORTER_OTLP_CERTIFICATE" From fccd958e247104bd8a6772fbe639280a54752dd4 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 17 Aug 2023 18:02:49 +0200 Subject: [PATCH 1460/1517] Change importlib-metadata requirement specification (#3403) --- opentelemetry-api/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/pyproject.toml b/opentelemetry-api/pyproject.toml index f659d77757..d03e870dfb 100644 --- a/opentelemetry-api/pyproject.toml +++ b/opentelemetry-api/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "Deprecated >= 1.2.6", # FIXME This should be able to be removed after 3.12 is released if there is a reliable API # in importlib.metadata. - "importlib-metadata ~= 6.0", + "importlib-metadata >= 6.0", ] dynamic = [ "version", From 8e81bbfbc07b70980f84bc460156c128deae1079 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 17 Aug 2023 10:12:53 -0700 Subject: [PATCH 1461/1517] Add micro benchmark tests for metric instrument operations (#3267) Co-authored-by: Srikanth Chekuri --- CHANGELOG.md | 2 + .../metrics/test_benchmark_metrics.py | 77 +++++++++++ .../test_benchmark_metrics_histogram,.py | 126 ++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 opentelemetry-sdk/tests/performance/benchmarks/metrics/test_benchmark_metrics.py create mode 100644 opentelemetry-sdk/tests/performance/benchmarks/metrics/test_benchmark_metrics_histogram,.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f0b9ab2dce..b2c9ad40c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3223](https://github.com/open-telemetry/opentelemetry-python/pull/3223)) - Add speced out environment variables and arguments for BatchLogRecordProcessor ([#3237](https://github.com/open-telemetry/opentelemetry-python/pull/3237)) +- Add benchmark tests for metrics + ([#3267](https://github.com/open-telemetry/opentelemetry-python/pull/3267)) ## Version 1.17.0/0.38b0 (2023-03-22) diff --git a/opentelemetry-sdk/tests/performance/benchmarks/metrics/test_benchmark_metrics.py b/opentelemetry-sdk/tests/performance/benchmarks/metrics/test_benchmark_metrics.py new file mode 100644 index 0000000000..81fb0b6e1d --- /dev/null +++ b/opentelemetry-sdk/tests/performance/benchmarks/metrics/test_benchmark_metrics.py @@ -0,0 +1,77 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + InMemoryMetricReader, +) + +reader_cumulative = InMemoryMetricReader() +reader_delta = InMemoryMetricReader( + preferred_temporality={ + Counter: AggregationTemporality.DELTA, + }, +) +provider_reader_cumulative = MeterProvider( + metric_readers=[reader_cumulative], +) +provider_reader_delta = MeterProvider(metric_readers=[reader_delta]) +meter_cumulative = provider_reader_cumulative.get_meter("sdk_meter_provider") +meter_delta = provider_reader_delta.get_meter("sdk_meter_provider_delta") +counter_cumulative = meter_cumulative.create_counter("test_counter") +counter_delta = meter_delta.create_counter("test_counter2") +udcounter = meter_cumulative.create_up_down_counter("test_udcounter") + + +@pytest.mark.parametrize( + ("num_labels", "temporality"), + [ + (0, "delta"), + (1, "delta"), + (3, "delta"), + (5, "delta"), + (10, "delta"), + (0, "cumulative"), + (1, "cumulative"), + (3, "cumulative"), + (5, "cumulative"), + (10, "cumulative"), + ], +) +def test_counter_add(benchmark, num_labels, temporality): + labels = {} + for i in range(num_labels): + labels = {f"Key{i}": f"Value{i}" for i in range(num_labels)} + + def benchmark_counter_add(): + if temporality == "cumulative": + counter_cumulative.add(1, labels) + else: + counter_delta.add(1, labels) + + benchmark(benchmark_counter_add) + + +@pytest.mark.parametrize("num_labels", [0, 1, 3, 5, 10]) +def test_up_down_counter_add(benchmark, num_labels): + labels = {} + for i in range(num_labels): + labels = {f"Key{i}": f"Value{i}" for i in range(num_labels)} + + def benchmark_up_down_counter_add(): + udcounter.add(1, labels) + + benchmark(benchmark_up_down_counter_add) diff --git a/opentelemetry-sdk/tests/performance/benchmarks/metrics/test_benchmark_metrics_histogram,.py b/opentelemetry-sdk/tests/performance/benchmarks/metrics/test_benchmark_metrics_histogram,.py new file mode 100644 index 0000000000..2f9c440541 --- /dev/null +++ b/opentelemetry-sdk/tests/performance/benchmarks/metrics/test_benchmark_metrics_histogram,.py @@ -0,0 +1,126 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import random + +import pytest + +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import InMemoryMetricReader +from opentelemetry.sdk.metrics.view import ( + ExplicitBucketHistogramAggregation, + View, +) + +MAX_BOUND_VALUE = 10000 + + +def _generate_bounds(bound_count): + bounds = [] + for i in range(bound_count): + bounds.append(i * MAX_BOUND_VALUE / bound_count) + return bounds + + +hist_view_10 = View( + instrument_name="test_histogram_10_bound", + aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(10)), +) +hist_view_49 = View( + instrument_name="test_histogram_49_bound", + aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(49)), +) +hist_view_50 = View( + instrument_name="test_histogram_50_bound", + aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(50)), +) +hist_view_1000 = View( + instrument_name="test_histogram_1000_bound", + aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(1000)), +) +reader = InMemoryMetricReader() +provider = MeterProvider( + metric_readers=[reader], + views=[ + hist_view_10, + hist_view_49, + hist_view_50, + hist_view_1000, + ], +) +meter = provider.get_meter("sdk_meter_provider") +hist = meter.create_histogram("test_histogram_default") +hist10 = meter.create_histogram("test_histogram_10_bound") +hist49 = meter.create_histogram("test_histogram_49_bound") +hist50 = meter.create_histogram("test_histogram_50_bound") +hist1000 = meter.create_histogram("test_histogram_1000_bound") + + +@pytest.mark.parametrize("num_labels", [0, 1, 3, 5, 7]) +def test_histogram_record(benchmark, num_labels): + labels = {} + for i in range(num_labels): + labels["Key{}".format(i)] = "Value{}".format(i) + + def benchmark_histogram_record(): + hist.record(random.random() * MAX_BOUND_VALUE) + + benchmark(benchmark_histogram_record) + + +@pytest.mark.parametrize("num_labels", [0, 1, 3, 5, 7]) +def test_histogram_record_10(benchmark, num_labels): + labels = {} + for i in range(num_labels): + labels["Key{}".format(i)] = "Value{}".format(i) + + def benchmark_histogram_record_10(): + hist10.record(random.random() * MAX_BOUND_VALUE) + + benchmark(benchmark_histogram_record_10) + + +@pytest.mark.parametrize("num_labels", [0, 1, 3, 5, 7]) +def test_histogram_record_49(benchmark, num_labels): + labels = {} + for i in range(num_labels): + labels["Key{}".format(i)] = "Value{}".format(i) + + def benchmark_histogram_record_49(): + hist49.record(random.random() * MAX_BOUND_VALUE) + + benchmark(benchmark_histogram_record_49) + + +@pytest.mark.parametrize("num_labels", [0, 1, 3, 5, 7]) +def test_histogram_record_50(benchmark, num_labels): + labels = {} + for i in range(num_labels): + labels["Key{}".format(i)] = "Value{}".format(i) + + def benchmark_histogram_record_50(): + hist50.record(random.random() * MAX_BOUND_VALUE) + + benchmark(benchmark_histogram_record_50) + + +@pytest.mark.parametrize("num_labels", [0, 1, 3, 5, 7]) +def test_histogram_record_1000(benchmark, num_labels): + labels = {} + for i in range(num_labels): + labels["Key{}".format(i)] = "Value{}".format(i) + + def benchmark_histogram_record_1000(): + hist1000.record(random.random() * MAX_BOUND_VALUE) + + benchmark(benchmark_histogram_record_1000) From d8490c5f557dd7005badeb800095cb51b553c98c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 18 Aug 2023 00:35:13 +0200 Subject: [PATCH 1462/1517] Add upper bound for importlib-metadata dependency (#3406) Fixes #3405 --- opentelemetry-api/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/pyproject.toml b/opentelemetry-api/pyproject.toml index d03e870dfb..adf9512cf0 100644 --- a/opentelemetry-api/pyproject.toml +++ b/opentelemetry-api/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "Deprecated >= 1.2.6", # FIXME This should be able to be removed after 3.12 is released if there is a reliable API # in importlib.metadata. - "importlib-metadata >= 6.0", + "importlib-metadata >= 6.0, < 7.0", ] dynamic = [ "version", From 0fa0ce56ea5cd3f59e7f17ce62f33e49be01dc06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 14:12:05 +0200 Subject: [PATCH 1463/1517] Bump uwsgi from 2.0.19.1 to 2.0.22 in /docs/examples/fork-process-model/flask-uwsgi (#3415) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/examples/fork-process-model/flask-uwsgi/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt index ab140df2ae..0b290379cb 100644 --- a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt +++ b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt @@ -15,6 +15,6 @@ opentelemetry-sdk==0.18b0 protobuf==3.18.3 six==1.15.0 thrift==0.13.0 -uWSGI==2.0.19.1 +uWSGI==2.0.22 Werkzeug==2.2.3 wrapt==1.12.1 From 91213823fa42b9cd7a83f38bf023371ce57f2f99 Mon Sep 17 00:00:00 2001 From: Pablo Collins Date: Wed, 30 Aug 2023 10:28:38 -0400 Subject: [PATCH 1464/1517] Factor out duplicate backoff code (#3396) Co-authored-by: Diego Hurtado --- .../pyproject.toml | 2 ++ .../otlp/proto/common/_internal/__init__.py | 15 ++++++++++++++ .../exporter/otlp/proto/grpc/exporter.py | 17 ++-------------- .../tests/logs/test_otlp_logs_exporter.py | 8 ++++++-- .../tests/test_otlp_exporter_mixin.py | 4 +++- .../tests/test_otlp_metrics_exporter.py | 12 ++++++++--- .../tests/test_otlp_trace_exporter.py | 17 +++++++++------- .../otlp/proto/http/_log_exporter/__init__.py | 20 ++++++------------- .../proto/http/metric_exporter/__init__.py | 18 ++++------------- .../proto/http/trace_exporter/__init__.py | 20 ++++++------------- .../metrics/test_otlp_metrics_exporter.py | 4 ++-- .../tests/test_proto_log_exporter.py | 4 ++-- .../tests/test_proto_span_exporter.py | 4 ++-- 13 files changed, 69 insertions(+), 76 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml index a9917d492e..3b76f423cd 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml @@ -26,6 +26,8 @@ classifiers = [ ] dependencies = [ "opentelemetry-proto == 1.20.0.dev", + "backoff >= 1.10.0, < 2.0.0; python_version<'3.7'", + "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", ] [project.urls] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py index 2f5d741324..bd6ca4ad18 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py @@ -17,6 +17,8 @@ from collections.abc import Sequence from typing import Any, Mapping, Optional, List, Callable, TypeVar, Dict +import backoff + from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.proto.common.v1.common_pb2 import ( InstrumentationScope as PB2InstrumentationScope, @@ -130,3 +132,16 @@ def _get_resource_data( ) ) return resource_data + + +# Work around API change between backoff 1.x and 2.x. Since 2.0.0 the backoff +# wait generator API requires a first .send(None) before reading the backoff +# values from the generator. +_is_backoff_v2 = next(backoff.expo()) is None + + +def _create_exp_backoff_generator(*args, **kwargs): + gen = backoff.expo(*args, **kwargs) + if _is_backoff_v2: + gen.send(None) + return gen diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 243c88b08f..dd169ff239 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -38,8 +38,8 @@ from opentelemetry.exporter.otlp.proto.common._internal import ( _get_resource_data, + _create_exp_backoff_generator, ) -import backoff from google.rpc.error_details_pb2 import RetryInfo from grpc import ( ChannelCredentials, @@ -137,19 +137,6 @@ def _get_credentials(creds, environ_key): return ssl_channel_credentials() -# Work around API change between backoff 1.x and 2.x. Since 2.0.0 the backoff -# wait generator API requires a first .send(None) before reading the backoff -# values from the generator. -_is_backoff_v2 = next(backoff.expo()) is None - - -def _expo(*args, **kwargs): - gen = backoff.expo(*args, **kwargs) - if _is_backoff_v2: - gen.send(None) - return gen - - # pylint: disable=no-member class OTLPExporterMixin( ABC, Generic[SDKDataT, ExportServiceRequestT, ExportResultT] @@ -266,7 +253,7 @@ def _export( # expo returns a generator that yields delay values which grow # exponentially. Once delay is greater than max_value, the yielded # value will remain constant. - for delay in _expo(max_value=max_value): + for delay in _create_exp_backoff_generator(max_value=max_value): if delay == max_value or self._shutdown: return self._result.FAILURE diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index c2e4a5dc0c..a6479a1474 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -292,7 +292,9 @@ def test_otlp_headers_from_env(self): (("user-agent", "OTel-OTLP-Exporter-Python/" + __version__),), ) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter._create_exp_backoff_generator" + ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): @@ -306,7 +308,9 @@ def test_unavailable(self, mock_sleep, mock_expo): ) mock_sleep.assert_called_with(1) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter._create_exp_backoff_generator" + ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable_delay(self, mock_sleep, mock_expo): diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index f1b95986da..4dfed3e154 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -60,7 +60,9 @@ def test_environ_to_compression(self): with self.assertRaises(InvalidCompressionValueException): environ_to_compression("test_invalid") - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter._create_exp_backoff_generator" + ) def test_export_warning(self, mock_expo): mock_expo.configure_mock(**{"return_value": [0]}) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py index 58cab7175c..1d9d65405c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py @@ -369,7 +369,9 @@ def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): mock_method.reset_mock() # pylint: disable=no-self-use - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter._create_exp_backoff_generator" + ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) def test_otlp_exporter_otlp_compression_envvar( @@ -405,7 +407,9 @@ def test_otlp_exporter_otlp_compression_unspecified( "localhost:4317", compression=Compression.NoCompression ) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter._create_exp_backoff_generator" + ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): @@ -420,7 +424,9 @@ def test_unavailable(self, mock_sleep, mock_expo): ) mock_sleep.assert_called_with(1) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter._create_exp_backoff_generator" + ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable_delay(self, mock_sleep, mock_expo): diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index be6c71ab3a..5445ddf926 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -28,8 +28,8 @@ from opentelemetry.attributes import BoundedAttributes from opentelemetry.exporter.otlp.proto.common._internal import ( _encode_key_value, + _is_backoff_v2, ) -from opentelemetry.exporter.otlp.proto.grpc.exporter import _is_backoff_v2 from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) @@ -460,7 +460,7 @@ def test_otlp_headers(self, mock_ssl_channel, mock_secure): (("user-agent", "OTel-OTLP-Exporter-Python/" + __version__),), ) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.backoff") + @patch("opentelemetry.exporter.otlp.proto.common._internal.backoff") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff): # In backoff ~= 2.0.0 the first value yielded from expo is None. @@ -477,7 +477,9 @@ def generate_delays(*args, **kwargs): self.exporter.export([self.span]) mock_sleep.assert_called_once_with(1) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter._create_exp_backoff_generator" + ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): @@ -486,12 +488,13 @@ def test_unavailable(self, mock_sleep, mock_expo): add_TraceServiceServicer_to_server( TraceServiceServicerUNAVAILABLE(), self.server ) - self.assertEqual( - self.exporter.export([self.span]), SpanExportResult.FAILURE - ) + result = self.exporter.export([self.span]) + self.assertEqual(result, SpanExportResult.FAILURE) mock_sleep.assert_called_with(1) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter._expo") + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter._create_exp_backoff_generator" + ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") def test_unavailable_delay(self, mock_sleep, mock_expo): diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 67f83f280f..caacdbdfca 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -20,9 +20,11 @@ from typing import Dict, Optional, Sequence from time import sleep -import backoff import requests +from opentelemetry.exporter.otlp.proto.common._internal import ( + _create_exp_backoff_generator, +) from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, @@ -56,18 +58,6 @@ DEFAULT_LOGS_EXPORT_PATH = "v1/logs" DEFAULT_TIMEOUT = 10 # in seconds -# Work around API change between backoff 1.x and 2.x. Since 2.0.0 the backoff -# wait generator API requires a first .send(None) before reading the backoff -# values from the generator. -_is_backoff_v2 = next(backoff.expo()) is None - - -def _expo(*args, **kwargs): - gen = backoff.expo(*args, **kwargs) - if _is_backoff_v2: - gen.send(None) - return gen - class OTLPLogExporter(LogExporter): @@ -147,7 +137,9 @@ def export(self, batch: Sequence[LogData]) -> LogExportResult: serialized_data = encode_logs(batch).SerializeToString() - for delay in _expo(max_value=self._MAX_RETRY_TIMEOUT): + for delay in _create_exp_backoff_generator( + max_value=self._MAX_RETRY_TIMEOUT + ): if delay == self._MAX_RETRY_TIMEOUT: return LogExportResult.FAILURE diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index 2c13601e0a..ed878dabe8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -24,6 +24,7 @@ from opentelemetry.exporter.otlp.proto.common._internal import ( _get_resource_data, + _create_exp_backoff_generator, ) from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( OTLPMetricExporterMixin, @@ -73,7 +74,6 @@ from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.util.re import parse_env_headers -import backoff import requests from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as PB2Resource, @@ -87,18 +87,6 @@ DEFAULT_METRICS_EXPORT_PATH = "v1/metrics" DEFAULT_TIMEOUT = 10 # in seconds -# Work around API change between backoff 1.x and 2.x. Since 2.0.0 the backoff -# wait generator API requires a first .send(None) before reading the backoff -# values from the generator. -_is_backoff_v2 = next(backoff.expo()) is None - - -def _expo(*args, **kwargs): - gen = backoff.expo(*args, **kwargs) - if _is_backoff_v2: - gen.send(None) - return gen - class OTLPMetricExporter(MetricExporter, OTLPMetricExporterMixin): @@ -181,7 +169,9 @@ def export( **kwargs, ) -> MetricExportResult: serialized_data = encode_metrics(metrics_data) - for delay in _expo(max_value=self._MAX_RETRY_TIMEOUT): + for delay in _create_exp_backoff_generator( + max_value=self._MAX_RETRY_TIMEOUT + ): if delay == self._MAX_RETRY_TIMEOUT: return MetricExportResult.FAILURE diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index dbc6a0a692..2ab7e97e02 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -20,9 +20,11 @@ from typing import Dict, Optional from time import sleep -import backoff import requests +from opentelemetry.exporter.otlp.proto.common._internal import ( + _create_exp_backoff_generator, +) from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( encode_spans, ) @@ -54,18 +56,6 @@ DEFAULT_TRACES_EXPORT_PATH = "v1/traces" DEFAULT_TIMEOUT = 10 # in seconds -# Work around API change between backoff 1.x and 2.x. Since 2.0.0 the backoff -# wait generator API requires a first .send(None) before reading the backoff -# values from the generator. -_is_backoff_v2 = next(backoff.expo()) is None - - -def _expo(*args, **kwargs): - gen = backoff.expo(*args, **kwargs) - if _is_backoff_v2: - gen.send(None) - return gen - class OTLPSpanExporter(SpanExporter): @@ -145,7 +135,9 @@ def export(self, spans) -> SpanExportResult: serialized_data = encode_spans(spans).SerializeToString() - for delay in _expo(max_value=self._MAX_RETRY_TIMEOUT): + for delay in _create_exp_backoff_generator( + max_value=self._MAX_RETRY_TIMEOUT + ): if delay == self._MAX_RETRY_TIMEOUT: return SpanExportResult.FAILURE diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index 632ecda3b9..d9011322b9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -21,6 +21,7 @@ from requests.models import Response from responses import POST, activate, add +from opentelemetry.exporter.otlp.proto.common._internal import _is_backoff_v2 from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( encode_metrics, ) @@ -31,7 +32,6 @@ DEFAULT_METRICS_EXPORT_PATH, DEFAULT_TIMEOUT, OTLPMetricExporter, - _is_backoff_v2, ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, @@ -298,7 +298,7 @@ def test_serialization(self, mock_post): ) @activate - @patch("opentelemetry.exporter.otlp.proto.http.metric_exporter.backoff") + @patch("opentelemetry.exporter.otlp.proto.common._internal.backoff") @patch("opentelemetry.exporter.otlp.proto.http.metric_exporter.sleep") def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff): # In backoff ~= 2.0.0 the first value yielded from expo is None. diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index 5cf20b881b..5300ce85de 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -22,6 +22,7 @@ import responses from opentelemetry._logs import SeverityNumber +from opentelemetry.exporter.otlp.proto.common._internal import _is_backoff_v2 from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._log_exporter import ( DEFAULT_COMPRESSION, @@ -29,7 +30,6 @@ DEFAULT_LOGS_EXPORT_PATH, DEFAULT_TIMEOUT, OTLPLogExporter, - _is_backoff_v2, ) from opentelemetry.exporter.otlp.proto.http.version import __version__ from opentelemetry.sdk._logs import LogData @@ -168,7 +168,7 @@ def test_exporter_env(self): self.assertIsInstance(exporter._session, requests.Session) @responses.activate - @patch("opentelemetry.exporter.otlp.proto.http._log_exporter.backoff") + @patch("opentelemetry.exporter.otlp.proto.common._internal.backoff") @patch("opentelemetry.exporter.otlp.proto.http._log_exporter.sleep") def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff): # In backoff ~= 2.0.0 the first value yielded from expo is None. diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index a1d96f50b4..9a1d1604a2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -19,6 +19,7 @@ import requests import responses +from opentelemetry.exporter.otlp.proto.common._internal import _is_backoff_v2 from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( DEFAULT_COMPRESSION, @@ -26,7 +27,6 @@ DEFAULT_TIMEOUT, DEFAULT_TRACES_EXPORT_PATH, OTLPSpanExporter, - _is_backoff_v2, ) from opentelemetry.exporter.otlp.proto.http.version import __version__ from opentelemetry.sdk.environment_variables import ( @@ -204,7 +204,7 @@ def test_headers_parse_from_env(self): # pylint: disable=no-self-use @responses.activate - @patch("opentelemetry.exporter.otlp.proto.http.trace_exporter.backoff") + @patch("opentelemetry.exporter.otlp.proto.common._internal.backoff") @patch("opentelemetry.exporter.otlp.proto.http.trace_exporter.sleep") def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff): # In backoff ~= 2.0.0 the first value yielded from expo is None. From 5b64e4fd4a4eba3029749b921f3fe50af16db358 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 17:51:20 +0200 Subject: [PATCH 1465/1517] Bump flask from 1.1.2 to 2.3.2 in /docs/examples/fork-process-model/flask-gunicorn (#3291) --- .../examples/fork-process-model/flask-gunicorn/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt index ab140df2ae..00757707e5 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt +++ b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt @@ -1,5 +1,5 @@ click==7.1.2 -Flask==1.1.2 +Flask==2.3.2 googleapis-common-protos==1.52.0 grpcio==1.56.0 gunicorn==20.0.4 From d604f8c7aeab024392550dbb3d8843e09b36afae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 19:01:40 +0200 Subject: [PATCH 1466/1517] Bump flask from 1.1.2 to 2.3.2 in /docs/examples/fork-process-model/flask-uwsgi (#3292) --- docs/examples/fork-process-model/flask-uwsgi/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt index 0b290379cb..ab17c5debf 100644 --- a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt +++ b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt @@ -1,5 +1,5 @@ click==7.1.2 -Flask==1.1.2 +Flask==2.3.2 googleapis-common-protos==1.52.0 grpcio==1.56.0 gunicorn==20.0.4 From 9ced357e9ffe32eabe03142848d0c206d026d441 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Aug 2023 16:33:49 +0200 Subject: [PATCH 1467/1517] Bump uwsgi from 2.0.19.1 to 2.0.22 in /docs/examples/fork-process-model/flask-gunicorn (#3414) --- .../examples/fork-process-model/flask-gunicorn/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt index 00757707e5..ab17c5debf 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt +++ b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt @@ -15,6 +15,6 @@ opentelemetry-sdk==0.18b0 protobuf==3.18.3 six==1.15.0 thrift==0.13.0 -uWSGI==2.0.19.1 +uWSGI==2.0.22 Werkzeug==2.2.3 wrapt==1.12.1 From 8ce94465afc0daf5747b806b7bbc0c304611beee Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Mon, 4 Sep 2023 10:19:55 -0700 Subject: [PATCH 1468/1517] Update version to 1.21.0.dev/0.42b0.dev (#3421) Co-authored-by: Diego Hurtado --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/pyproject.toml | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-common/pyproject.toml | 2 +- .../src/opentelemetry/exporter/otlp/proto/common/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-grpc/pyproject.toml | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-http/pyproject.toml | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/pyproject.toml | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/pyproject.toml | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opencensus-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opencensus/version.py | 2 +- shim/opentelemetry-opentracing-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/pyproject.toml | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 36 files changed, 47 insertions(+), 45 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3c96e5084b..dd600c03af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: d1fb91ae0c075751b580ff4892bae44749d5810d + CONTRIB_REPO_SHA: 481972cf87c1506e789f586843ba82d93633eac7 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index b2c9ad40c3..25d28cd333 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## Version 1.20.0/0.41b0 (2023-09-01) + - Modify Prometheus exporter to translate non-monotonic Sums into Gauges ([#3306](https://github.com/open-telemetry/opentelemetry-python/pull/3306)) diff --git a/eachdist.ini b/eachdist.ini index 84bed7b943..884bff2242 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.20.0.dev +version=1.21.0.dev packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.41b0.dev +version=0.42b0.dev packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 31245d9f80..e755ddf0c9 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 31245d9f80..e755ddf0c9 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger/pyproject.toml b/exporter/opentelemetry-exporter-jaeger/pyproject.toml index 7cbf2ea535..996bf1a672 100644 --- a/exporter/opentelemetry-exporter-jaeger/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-jaeger-proto-grpc == 1.20.0.dev", - "opentelemetry-exporter-jaeger-thrift == 1.20.0.dev", + "opentelemetry-exporter-jaeger-proto-grpc == 1.21.0.dev", + "opentelemetry-exporter-jaeger-thrift == 1.21.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 31245d9f80..e755ddf0c9 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/exporter/opentelemetry-exporter-opencensus/pyproject.toml b/exporter/opentelemetry-exporter-opencensus/pyproject.toml index a00beff976..6c6439e27e 100644 --- a/exporter/opentelemetry-exporter-opencensus/pyproject.toml +++ b/exporter/opentelemetry-exporter-opencensus/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ dependencies = [ "grpcio >= 1.0.0, < 2.0.0", "opencensus-proto >= 0.1.0, < 1.0.0", - "opentelemetry-api >= 1.20.0.dev", + "opentelemetry-api >= 1.21.0.dev", "opentelemetry-sdk >= 1.15", "protobuf ~= 3.13", "setuptools >= 16.0", diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 7f88144cf6..c2996671d6 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.41b0.dev" +__version__ = "0.42b0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml index 3b76f423cd..74519c1237 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ - "opentelemetry-proto == 1.20.0.dev", + "opentelemetry-proto == 1.21.0.dev", "backoff >= 1.10.0, < 2.0.0; python_version<'3.7'", "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py index 270451efb0..87246ad9db 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index 475c431aed..1b2af79e0e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -31,9 +31,9 @@ dependencies = [ "googleapis-common-protos ~= 1.52", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.20.0.dev", - "opentelemetry-sdk ~= 1.20.0.dev", - "opentelemetry-exporter-otlp-proto-common == 1.20.0.dev", + "opentelemetry-proto == 1.21.0.dev", + "opentelemetry-sdk ~= 1.21.0.dev", + "opentelemetry-exporter-otlp-proto-common == 1.21.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 270451efb0..87246ad9db 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index e3b7f36ada..4e89ea5b81 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -30,9 +30,9 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.20.0.dev", - "opentelemetry-sdk ~= 1.20.0.dev", - "opentelemetry-exporter-otlp-proto-common == 1.20.0.dev", + "opentelemetry-proto == 1.21.0.dev", + "opentelemetry-sdk ~= 1.21.0.dev", + "opentelemetry-exporter-otlp-proto-common == 1.21.0.dev", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 270451efb0..87246ad9db 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index 61aeb44f0d..64ac953180 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-otlp-proto-grpc == 1.20.0.dev", - "opentelemetry-exporter-otlp-proto-http == 1.20.0.dev", + "opentelemetry-exporter-otlp-proto-grpc == 1.21.0.dev", + "opentelemetry-exporter-otlp-proto-http == 1.21.0.dev", ] [project.entry-points.opentelemetry_logs_exporter] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 270451efb0..87246ad9db 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 7f88144cf6..c2996671d6 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.41b0.dev" +__version__ = "0.42b0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 270451efb0..87246ad9db 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml index 060fab4a05..be59f5e54c 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.3", - "opentelemetry-exporter-zipkin-json == 1.20.0.dev", + "opentelemetry-exporter-zipkin-json == 1.21.0.dev", "opentelemetry-sdk ~= 1.11", "protobuf ~= 3.12", "requests ~= 2.7", diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 270451efb0..87246ad9db 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin/pyproject.toml b/exporter/opentelemetry-exporter-zipkin/pyproject.toml index 88fb8488a6..f4750e7c1b 100644 --- a/exporter/opentelemetry-exporter-zipkin/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-zipkin-json == 1.20.0.dev", - "opentelemetry-exporter-zipkin-proto-http == 1.20.0.dev", + "opentelemetry-exporter-zipkin-json == 1.21.0.dev", + "opentelemetry-exporter-zipkin-proto-http == 1.21.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 270451efb0..87246ad9db 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 270451efb0..87246ad9db 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 270451efb0..87246ad9db 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index f9edd23542..603a9b1fbf 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-api == 1.20.0.dev", - "opentelemetry-semantic-conventions == 0.41b0.dev", + "opentelemetry-api == 1.21.0.dev", + "opentelemetry-semantic-conventions == 0.42b0.dev", "typing-extensions >= 3.7.4", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 270451efb0..87246ad9db 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 7f88144cf6..c2996671d6 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.41b0.dev" +__version__ = "0.42b0.dev" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 270451efb0..87246ad9db 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 270451efb0..87246ad9db 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0.dev" +__version__ = "1.21.0.dev" diff --git a/shim/opentelemetry-opencensus-shim/pyproject.toml b/shim/opentelemetry-opencensus-shim/pyproject.toml index c6dac03c66..b8e0c66b39 100644 --- a/shim/opentelemetry-opencensus-shim/pyproject.toml +++ b/shim/opentelemetry-opencensus-shim/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.41b0.dev", + "opentelemetry-test-utils == 0.42b0.dev", "opencensus == 0.11.1", ] diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py index 7f88144cf6..c2996671d6 100644 --- a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.41b0.dev" +__version__ = "0.42b0.dev" diff --git a/shim/opentelemetry-opentracing-shim/pyproject.toml b/shim/opentelemetry-opentracing-shim/pyproject.toml index ed0011c47e..1dac33ee2d 100644 --- a/shim/opentelemetry-opentracing-shim/pyproject.toml +++ b/shim/opentelemetry-opentracing-shim/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.41b0.dev", + "opentelemetry-test-utils == 0.42b0.dev", "opentracing ~= 2.2.0", ] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 7f88144cf6..c2996671d6 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.41b0.dev" +__version__ = "0.42b0.dev" diff --git a/tests/opentelemetry-test-utils/pyproject.toml b/tests/opentelemetry-test-utils/pyproject.toml index e1e5684e51..d9f7908e38 100644 --- a/tests/opentelemetry-test-utils/pyproject.toml +++ b/tests/opentelemetry-test-utils/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ ] dependencies = [ "asgiref ~= 3.0", - "opentelemetry-api == 1.20.0.dev", - "opentelemetry-sdk == 1.20.0.dev", + "opentelemetry-api == 1.21.0.dev", + "opentelemetry-sdk == 1.21.0.dev", ] [project.optional-dependencies] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 392d092d1e..f4c8e12962 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.41b0.dev" +__version__ = "0.42b0.dev" From 8775c5f2d4f63b5934b0f69610c1a13010c3cde0 Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Mon, 4 Sep 2023 12:43:19 -0700 Subject: [PATCH 1469/1517] Copy change log updates from release/v1.20.x-0.41bx (#3425) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25d28cd333..7b8cfb0398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -## Version 1.20.0/0.41b0 (2023-09-01) +## Version 1.20.0/0.41b0 (2023-09-04) - Modify Prometheus exporter to translate non-monotonic Sums into Gauges ([#3306](https://github.com/open-telemetry/opentelemetry-python/pull/3306)) From 9f3c0afaa4a161fd6cc2130159ec8e06cf82e00e Mon Sep 17 00:00:00 2001 From: Gustavo Pantuza Date: Wed, 6 Sep 2023 16:18:44 -0300 Subject: [PATCH 1470/1517] Fix broken URL of Open Telemetry Semantic Conventions (#3410) --- opentelemetry-semantic-conventions/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-semantic-conventions/README.rst b/opentelemetry-semantic-conventions/README.rst index f84cf523d0..e5a40e739c 100644 --- a/opentelemetry-semantic-conventions/README.rst +++ b/opentelemetry-semantic-conventions/README.rst @@ -33,5 +33,5 @@ References ---------- * `OpenTelemetry Project `_ -* `OpenTelemetry Semantic Conventions YAML Definitions `_ +* `OpenTelemetry Semantic Conventions Definitions `_ * `generate.sh script `_ From 6070a0d33ab103cf63bb8f3704df1333ddaeabb9 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 7 Sep 2023 08:30:40 -0700 Subject: [PATCH 1471/1517] Fix exception thrown when LoggerProvider not configured (#3423) --- CHANGELOG.md | 4 +++- .../sdk/_logs/_internal/__init__.py | 6 ++++-- opentelemetry-sdk/tests/logs/test_handler.py | 17 ++++++++++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b8cfb0398..4f1cd0c79d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Fix error when no LoggerProvider configured for LoggingHandler + ([#3423](https://github.com/open-telemetry/opentelemetry-python/pull/3423)) + ## Version 1.20.0/0.41b0 (2023-09-04) - Modify Prometheus exporter to translate non-monotonic Sums into Gauges ([#3306](https://github.com/open-telemetry/opentelemetry-python/pull/3306)) - ## Version 1.19.0/0.40b0 (2023-07-13) - Drop `setuptools` runtime requirement. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index eb5f50e9e0..d3db94d624 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -27,6 +27,7 @@ from opentelemetry._logs import LoggerProvider as APILoggerProvider from opentelemetry._logs import LogRecord as APILogRecord from opentelemetry._logs import ( + NoOpLogger, SeverityNumber, get_logger, get_logger_provider, @@ -532,11 +533,12 @@ def _translate(self, record: logging.LogRecord) -> LogRecord: def emit(self, record: logging.LogRecord) -> None: """ - Emit a record. + Emit a record. Skip emitting if logger is NoOp. The record is translated to OTel format, and then sent across the pipeline. """ - self._logger.emit(self._translate(record)) + if not isinstance(self._logger, NoOpLogger): + self._logger.emit(self._translate(record)) def flush(self) -> None: """ diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index bb4fc3a829..e126cac172 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -15,7 +15,7 @@ import unittest from unittest.mock import Mock -from opentelemetry._logs import SeverityNumber +from opentelemetry._logs import NoOpLoggerProvider, SeverityNumber from opentelemetry._logs import get_logger as APIGetLogger from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk import trace @@ -64,6 +64,21 @@ def test_handler_custom_log_level(self): logger.critical("No Time For Caution") self.assertEqual(emitter_mock.emit.call_count, 2) + # pylint: disable=protected-access + def test_log_record_emit_noop(self): + noop_logger_provder = NoOpLoggerProvider() + logger_mock = APIGetLogger( + __name__, logger_provider=noop_logger_provder + ) + logger = logging.getLogger(__name__) + handler_mock = Mock(spec=LoggingHandler) + handler_mock._logger = logger_mock + handler_mock.level = logging.WARNING + logger.addHandler(handler_mock) + with self.assertLogs(level=logging.WARNING): + logger.warning("Warning message") + handler_mock._translate.assert_not_called() + def test_log_record_no_span_context(self): emitter_provider_mock = Mock(spec=LoggerProvider) emitter_mock = APIGetLogger( From 3a651c7f60a121dee5d783e64bf1c47bfb97283f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 7 Sep 2023 21:09:41 +0200 Subject: [PATCH 1472/1517] Fix handling of empty metric collection cycles (#3335) --- CHANGELOG.md | 5 +- .../_internal/_view_instrument_match.py | 10 ++- .../sdk/metrics/_internal/export/__init__.py | 15 ++-- .../metrics/_internal/measurement_consumer.py | 10 ++- .../_internal/metric_reader_storage.py | 85 ++++++++++--------- .../integration_test/test_console_exporter.py | 16 ++++ .../test_disable_default_views.py | 10 +-- .../test_exporter_concurrency.py | 14 +++ .../integration_test/test_histogram_export.py | 82 ++++++++++++++++++ .../metrics/test_metric_reader_storage.py | 10 +-- 10 files changed, 184 insertions(+), 73 deletions(-) create mode 100644 opentelemetry-sdk/tests/metrics/integration_test/test_histogram_export.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f1cd0c79d..f5a7a98398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Fix handling of empty metric collection cycles + ([#3335](https://github.com/open-telemetry/opentelemetry-python/pull/3335)) - Fix error when no LoggerProvider configured for LoggingHandler ([#3423](https://github.com/open-telemetry/opentelemetry-python/pull/3423)) - + + ## Version 1.20.0/0.41b0 (2023-09-04) - Modify Prometheus exporter to translate non-monotonic Sums into Gauges diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index ab4645c82f..110f963a48 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -16,7 +16,7 @@ from logging import getLogger from threading import Lock from time import time_ns -from typing import Dict, List, Sequence +from typing import Dict, List, Optional, Sequence from opentelemetry.metrics import Instrument from opentelemetry.sdk.metrics._internal.aggregation import ( @@ -126,7 +126,7 @@ def collect( self, aggregation_temporality: AggregationTemporality, collection_start_nanos: int, - ) -> Sequence[DataPointT]: + ) -> Optional[Sequence[DataPointT]]: data_points: List[DataPointT] = [] with self._lock: @@ -136,4 +136,8 @@ def collect( ) if data_point is not None: data_points.append(data_point) - return data_points + + # Returning here None instead of an empty list because the caller + # does not consume a sequence and to be consistent with the rest of + # collect methods that also return None. + return data_points or None diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 5bd94d5aac..0568270ae6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -322,10 +322,14 @@ def collect(self, timeout_millis: float = 10_000) -> None: ) return - self._receive_metrics( - self._collect(self, timeout_millis=timeout_millis), - timeout_millis=timeout_millis, - ) + metrics = self._collect(self, timeout_millis=timeout_millis) + + if metrics is not None: + + self._receive_metrics( + metrics, + timeout_millis=timeout_millis, + ) @final def _set_collect_callback( @@ -515,8 +519,7 @@ def _receive_metrics( timeout_millis: float = 10_000, **kwargs, ) -> None: - if metrics_data is None: - return + token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: with self._export_lock: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py index 9daf1eff46..c5e81678dc 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py @@ -17,7 +17,7 @@ from abc import ABC, abstractmethod from threading import Lock from time import time_ns -from typing import Iterable, List, Mapping +from typing import Iterable, List, Mapping, Optional # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics @@ -51,7 +51,7 @@ def collect( self, metric_reader: "opentelemetry.sdk.metrics.MetricReader", timeout_millis: float = 10_000, - ) -> Iterable[Metric]: + ) -> Optional[Iterable[Metric]]: pass @@ -94,7 +94,7 @@ def collect( self, metric_reader: "opentelemetry.sdk.metrics.MetricReader", timeout_millis: float = 10_000, - ) -> Iterable[Metric]: + ) -> Optional[Iterable[Metric]]: with self._lock: metric_reader_storage = self._reader_storages[metric_reader] @@ -123,4 +123,6 @@ def collect( for measurement in measurements: metric_reader_storage.consume_measurement(measurement) - return self._reader_storages[metric_reader].collect() + result = self._reader_storages[metric_reader].collect() + + return result diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py index bef57eaab0..700ace8720 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py @@ -15,7 +15,7 @@ from logging import getLogger from threading import RLock from time import time_ns -from typing import Dict, List +from typing import Dict, List, Optional from opentelemetry.metrics import ( Asynchronous, @@ -119,7 +119,7 @@ def consume_measurement(self, measurement: Measurement) -> None: ): view_instrument_match.consume_measurement(measurement) - def collect(self) -> MetricsData: + def collect(self) -> Optional[MetricsData]: # Use a list instead of yielding to prevent a slow reader from holding # SDK locks @@ -152,6 +152,13 @@ def collect(self) -> MetricsData: for view_instrument_match in view_instrument_matches: + data_points = view_instrument_match.collect( + aggregation_temporality, collection_start_nanos + ) + + if data_points is None: + continue + if isinstance( # pylint: disable=protected-access view_instrument_match._aggregation, @@ -159,9 +166,7 @@ def collect(self) -> MetricsData: ): data = Sum( aggregation_temporality=aggregation_temporality, - data_points=view_instrument_match.collect( - aggregation_temporality, collection_start_nanos - ), + data_points=data_points, is_monotonic=isinstance( instrument, (Counter, ObservableCounter) ), @@ -171,20 +176,14 @@ def collect(self) -> MetricsData: view_instrument_match._aggregation, _LastValueAggregation, ): - data = Gauge( - data_points=view_instrument_match.collect( - aggregation_temporality, collection_start_nanos - ) - ) + data = Gauge(data_points=data_points) elif isinstance( # pylint: disable=protected-access view_instrument_match._aggregation, _ExplicitBucketHistogramAggregation, ): data = Histogram( - data_points=view_instrument_match.collect( - aggregation_temporality, collection_start_nanos - ), + data_points=data_points, aggregation_temporality=aggregation_temporality, ) elif isinstance( @@ -200,9 +199,7 @@ def collect(self) -> MetricsData: _ExponentialBucketHistogramAggregation, ): data = ExponentialHistogram( - data_points=view_instrument_match.collect( - aggregation_temporality, collection_start_nanos - ), + data_points=data_points, aggregation_temporality=aggregation_temporality, ) @@ -216,32 +213,38 @@ def collect(self) -> MetricsData: ) ) - if instrument.instrumentation_scope not in ( - instrumentation_scope_scope_metrics - ): - instrumentation_scope_scope_metrics[ - instrument.instrumentation_scope - ] = ScopeMetrics( - scope=instrument.instrumentation_scope, - metrics=metrics, - schema_url=instrument.instrumentation_scope.schema_url, - ) - else: - instrumentation_scope_scope_metrics[ - instrument.instrumentation_scope - ].metrics.extend(metrics) - - return MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=self._sdk_config.resource, - scope_metrics=list( - instrumentation_scope_scope_metrics.values() - ), - schema_url=self._sdk_config.resource.schema_url, + if metrics: + + if instrument.instrumentation_scope not in ( + instrumentation_scope_scope_metrics + ): + instrumentation_scope_scope_metrics[ + instrument.instrumentation_scope + ] = ScopeMetrics( + scope=instrument.instrumentation_scope, + metrics=metrics, + schema_url=instrument.instrumentation_scope.schema_url, + ) + else: + instrumentation_scope_scope_metrics[ + instrument.instrumentation_scope + ].metrics.extend(metrics) + + if instrumentation_scope_scope_metrics: + + return MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=self._sdk_config.resource, + scope_metrics=list( + instrumentation_scope_scope_metrics.values() + ), + schema_url=self._sdk_config.resource.schema_url, + ) + ] ) - ] - ) + + return None def _handle_view_instrument_match( self, diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py b/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py index 60c4227c3b..1b3283717a 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py @@ -72,3 +72,19 @@ def test_console_exporter(self): self.assertEqual(metrics["attributes"], {"a": "b"}) self.assertEqual(metrics["value"], 1) + + def test_console_exporter_no_export(self): + + output = StringIO() + exporter = ConsoleMetricExporter(out=output) + reader = PeriodicExportingMetricReader( + exporter, export_interval_millis=100 + ) + provider = MeterProvider(metric_readers=[reader]) + provider.shutdown() + + output.seek(0) + actual = "".join(output.readlines()) + expected = "" + + self.assertEqual(actual, expected) diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py b/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py index ad90fe9a29..d022456415 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_disable_default_views.py @@ -31,15 +31,7 @@ def test_disable_default_views(self): counter.add(10, {"label": "value1"}) counter.add(10, {"label": "value2"}) counter.add(10, {"label": "value3"}) - self.assertEqual( - ( - reader.get_metrics_data() - .resource_metrics[0] - .scope_metrics[0] - .metrics - ), - [], - ) + self.assertIsNone(reader.get_metrics_data()) def test_disable_default_views_add_custom(self): reader = InMemoryMetricReader() diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_exporter_concurrency.py b/opentelemetry-sdk/tests/metrics/integration_test/test_exporter_concurrency.py index 045afe0b29..bbc67eac30 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_exporter_concurrency.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_exporter_concurrency.py @@ -74,6 +74,15 @@ class TestExporterConcurrency(ConcurrencyTestBase): > be called again only after the current call returns. https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exportbatch + + This test also tests that a thread that calls the a + ``MetricReader.collect`` method using an asynchronous instrument is able + to perform two actions in the same thread lock space (without it being + interrupted by another thread): + + 1. Consume the measurement produced by the callback associated to the + asynchronous instrument. + 2. Export the measurement mentioned in the step above. """ def test_exporter_not_called_concurrently(self): @@ -84,7 +93,11 @@ def test_exporter_not_called_concurrently(self): ) meter_provider = MeterProvider(metric_readers=[reader]) + counter_cb_counter = 0 + def counter_cb(options: CallbackOptions): + nonlocal counter_cb_counter + counter_cb_counter += 1 yield Observation(2) meter_provider.get_meter(__name__).create_observable_counter( @@ -97,6 +110,7 @@ def test_many_threads(): self.run_with_many_threads(test_many_threads, num_threads=100) + self.assertEqual(counter_cb_counter, 100) # no thread should be in export() now self.assertEqual(exporter.count_in_export, 0) # should be one call for each thread diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_histogram_export.py b/opentelemetry-sdk/tests/metrics/integration_test/test_histogram_export.py new file mode 100644 index 0000000000..81d419819a --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_histogram_export.py @@ -0,0 +1,82 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import InMemoryMetricReader +from opentelemetry.sdk.resources import SERVICE_NAME, Resource + + +class TestHistogramExport(TestCase): + def test_histogram_counter_collection(self): + + in_memory_metric_reader = InMemoryMetricReader() + + provider = MeterProvider( + resource=Resource.create({SERVICE_NAME: "otel-test"}), + metric_readers=[in_memory_metric_reader], + ) + + meter = provider.get_meter("my-meter") + + histogram = meter.create_histogram("my_histogram") + counter = meter.create_counter("my_counter") + histogram.record(5, {"attribute": "value"}) + counter.add(1, {"attribute": "value_counter"}) + + metric_data = in_memory_metric_reader.get_metrics_data() + + self.assertEqual( + len(metric_data.resource_metrics[0].scope_metrics[0].metrics), 2 + ) + + self.assertEqual( + ( + metric_data.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .bucket_counts + ), + (0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + ) + self.assertEqual( + ( + metric_data.resource_metrics[0] + .scope_metrics[0] + .metrics[1] + .data.data_points[0] + .value + ), + 1, + ) + + metric_data = in_memory_metric_reader.get_metrics_data() + + # FIXME ExplicitBucketHistogramAggregation is resetting counts to zero + # even if aggregation temporality is cumulative. + self.assertEqual( + len(metric_data.resource_metrics[0].scope_metrics[0].metrics), 1 + ) + self.assertEqual( + ( + metric_data.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .value + ), + 1, + ) diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index 97b5532fea..1da6d5bcf6 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -314,15 +314,7 @@ def test_drop_aggregation(self): ) metric_reader_storage.consume_measurement(Measurement(1, counter)) - self.assertEqual( - [], - ( - metric_reader_storage.collect() - .resource_metrics[0] - .scope_metrics[0] - .metrics - ), - ) + self.assertIsNone(metric_reader_storage.collect()) def test_same_collection_start(self): From ae42ad13c7857c5b6f22567ed7201b18773f1a90 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 14 Sep 2023 16:48:38 -0400 Subject: [PATCH 1473/1517] Temp fix for opencensus missing dependency on six (#3433) --- shim/opentelemetry-opencensus-shim/pyproject.toml | 2 ++ tox.ini | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/shim/opentelemetry-opencensus-shim/pyproject.toml b/shim/opentelemetry-opencensus-shim/pyproject.toml index b8e0c66b39..8bb5332463 100644 --- a/shim/opentelemetry-opencensus-shim/pyproject.toml +++ b/shim/opentelemetry-opencensus-shim/pyproject.toml @@ -36,6 +36,8 @@ dependencies = [ test = [ "opentelemetry-test-utils == 0.42b0.dev", "opencensus == 0.11.1", + # Temporary fix for https://github.com/census-instrumentation/opencensus-python/issues/1219 + "six == 1.16.0", ] [project.urls] diff --git a/tox.ini b/tox.ini index ce37337290..4bb52cdc66 100644 --- a/tox.ini +++ b/tox.ini @@ -166,7 +166,7 @@ commands_pre = opentracing-shim: pip install {toxinidir}/shim/opentelemetry-opentracing-shim opencensus-shim: pip install {toxinidir}/opentelemetry-sdk - opencensus-shim: pip install {toxinidir}/shim/opentelemetry-opencensus-shim + opencensus-shim: pip install {toxinidir}/shim/opentelemetry-opencensus-shim[test] exporter-prometheus: pip install {toxinidir}/exporter/opentelemetry-exporter-prometheus @@ -299,6 +299,8 @@ commands = [testenv:docker-tests-proto{3,4}] deps = pytest + # Pinning PyYAML for issue: https://github.com/yaml/pyyaml/issues/724 + PyYAML == 5.3.1 docker-compose >= 1.25.2 requests < 2.29.0 From f02029c0d982d7238942ffe459800094b5fe3656 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Fri, 15 Sep 2023 20:00:06 +0200 Subject: [PATCH 1474/1517] resolve requirement conflicts with grpcio-status in other transient dependancies when using python 3.11 (#3258) --- .../opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml index 1f481fc2a3..131f12a0b4 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "googleapis-common-protos ~= 1.52, < 1.56.3", + "googleapis-common-protos ~= 1.52, < 1.60.0", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.3", "opentelemetry-sdk ~= 1.11", From 6973de2ef18e436b9c1a610a1a7cec4479d8d4c2 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Fri, 15 Sep 2023 18:36:24 -0400 Subject: [PATCH 1475/1517] Make `opentelemetry_metrics_exporter` entrypoint support pull exporters (#3428) --- CHANGELOG.md | 3 +- .../opentelemetry/environment_variables.py | 23 +++++++++ .../sdk/_configuration/__init__.py | 27 +++++++--- opentelemetry-sdk/tests/test_configurator.py | 51 ++++++++++++++++++- 4 files changed, 93 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5a7a98398..50db7d0bd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3335](https://github.com/open-telemetry/opentelemetry-python/pull/3335)) - Fix error when no LoggerProvider configured for LoggingHandler ([#3423](https://github.com/open-telemetry/opentelemetry-python/pull/3423)) - +- Make `opentelemetry_metrics_exporter` entrypoint support pull exporters + ([#3428](https://github.com/open-telemetry/opentelemetry-python/pull/3428)) ## Version 1.20.0/0.41b0 (2023-09-04) diff --git a/opentelemetry-api/src/opentelemetry/environment_variables.py b/opentelemetry-api/src/opentelemetry/environment_variables.py index c54b13c6da..c15b96be14 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables.py @@ -22,6 +22,29 @@ """ .. envvar:: OTEL_METRICS_EXPORTER +Specifies which exporter is used for metrics. See `General SDK Configuration +`_. + +**Default value:** ``"otlp"`` + +**Example:** + +``export OTEL_METRICS_EXPORTER="prometheus"`` + +Accepted values for ``OTEL_METRICS_EXPORTER`` are: + +- ``"otlp"`` +- ``"prometheus"`` +- ``"none"``: No automatically configured exporter for metrics. + +.. note:: + + Exporter packages may add entry points for group ``opentelemetry_metrics_exporter`` which + can then be used with this environment variable by name. The entry point should point to + either a `opentelemetry.sdk.metrics.export.MetricExporter` (push exporter) or + `opentelemetry.sdk.metrics.export.MetricReader` (pull exporter) subclass; it must be + constructable without any required arguments. This mechanism is considered experimental and + may change in subsequent releases. """ OTEL_PROPAGATORS = "OTEL_PROPAGATORS" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 958a50394e..e2abcbefa1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -21,7 +21,7 @@ import os from abc import ABC, abstractmethod from os import environ -from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type +from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, Union from typing_extensions import Literal @@ -47,6 +47,7 @@ from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( MetricExporter, + MetricReader, PeriodicExportingMetricReader, ) from opentelemetry.sdk.resources import Resource @@ -210,16 +211,24 @@ def _init_tracing( def _init_metrics( - exporters: Dict[str, Type[MetricExporter]], + exporters_or_readers: Dict[ + str, Union[Type[MetricExporter], Type[MetricReader]] + ], resource: Resource = None, ): metric_readers = [] - for _, exporter_class in exporters.items(): + for _, exporter_or_reader_class in exporters_or_readers.items(): exporter_args = {} - metric_readers.append( - PeriodicExportingMetricReader(exporter_class(**exporter_args)) - ) + + if issubclass(exporter_or_reader_class, MetricReader): + metric_readers.append(exporter_or_reader_class(**exporter_args)) + else: + metric_readers.append( + PeriodicExportingMetricReader( + exporter_or_reader_class(**exporter_args) + ) + ) provider = MeterProvider(resource=resource, metric_readers=metric_readers) set_meter_provider(provider) @@ -249,7 +258,7 @@ def _import_exporters( log_exporter_names: Sequence[str], ) -> Tuple[ Dict[str, Type[SpanExporter]], - Dict[str, Type[MetricExporter]], + Dict[str, Union[Type[MetricExporter], Type[MetricReader]]], Dict[str, Type[LogExporter]], ]: trace_exporters = {} @@ -267,7 +276,9 @@ def _import_exporters( for (exporter_name, exporter_impl,) in _import_config_components( metric_exporter_names, "opentelemetry_metrics_exporter" ): - if issubclass(exporter_impl, MetricExporter): + # The metric exporter components may be push MetricExporter or pull exporters which + # subclass MetricReader directly + if issubclass(exporter_impl, (MetricExporter, MetricReader)): metric_exporters[exporter_name] = exporter_impl else: raise RuntimeError(f"{exporter_name} is not a metric exporter") diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index e64e64ade0..3696dc61b3 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -18,7 +18,7 @@ from os import environ from typing import Dict, Iterable, Optional, Sequence from unittest import TestCase -from unittest.mock import patch +from unittest.mock import Mock, patch from pytest import raises @@ -158,6 +158,20 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: return True +# MetricReader that can be configured as a pull exporter +class DummyMetricReaderPullExporter(MetricReader): + def _receive_metrics( + self, + metrics: Iterable[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> None: + pass + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + return True + + class DummyOTLPMetricExporter: def __init__(self, *args, **kwargs): self.export_called = False @@ -309,7 +323,6 @@ def tearDown(self): environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=my-test-service"} ) def test_trace_init_default(self): - auto_resource = Resource.create( { "telemetry.auto.version": "test-version", @@ -740,6 +753,18 @@ def test_metrics_init_exporter(self): self.assertIsInstance(reader, DummyMetricReader) self.assertIsInstance(reader.exporter, DummyOTLPMetricExporter) + def test_metrics_init_pull_exporter(self): + resource = Resource.create({}) + _init_metrics( + {"dummy_metric_reader": DummyMetricReaderPullExporter}, + resource=resource, + ) + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyMeterProvider) + reader = provider._sdk_config.metric_readers[0] + self.assertIsInstance(reader, DummyMetricReaderPullExporter) + class TestExporterNames(TestCase): @patch.dict( @@ -835,6 +860,28 @@ def test_console_exporters(self): ConsoleMetricExporter.__class__, ) + @patch( + "opentelemetry.sdk._configuration.entry_points", + ) + def test_metric_pull_exporter(self, mock_entry_points: Mock): + def mock_entry_points_impl(group, name): + if name == "dummy_pull_exporter": + return [ + IterEntryPoint( + name=name, class_type=DummyMetricReaderPullExporter + ) + ] + return [] + + mock_entry_points.side_effect = mock_entry_points_impl + _, metric_exporters, _ = _import_exporters( + [], ["dummy_pull_exporter"], [] + ) + self.assertIs( + metric_exporters["dummy_pull_exporter"], + DummyMetricReaderPullExporter, + ) + class TestImportConfigComponents(TestCase): @patch( From 647fbc7019c4ee45fc998d5a999cd208c3e830ac Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Tue, 19 Sep 2023 13:48:45 -0400 Subject: [PATCH 1476/1517] Allow instrument names to have '/' and up to 255 characters (#3442) --- CHANGELOG.md | 2 ++ .../metrics/_internal/instrument.py | 2 +- .../tests/metrics/test_instruments.py | 19 +++++++++++++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50db7d0bd0..bf7db9c04f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3423](https://github.com/open-telemetry/opentelemetry-python/pull/3423)) - Make `opentelemetry_metrics_exporter` entrypoint support pull exporters ([#3428](https://github.com/open-telemetry/opentelemetry-python/pull/3428)) +- Allow instrument names to have '/' and up to 255 characters + ([#3442](https://github.com/open-telemetry/opentelemetry-python/pull/3442)) ## Version 1.20.0/0.41b0 (2023-09-04) diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py index fec2879ef6..54b2fb7597 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py @@ -38,7 +38,7 @@ _logger = getLogger(__name__) -_name_regex = re_compile(r"[a-zA-Z][-_.a-zA-Z0-9]{0,62}") +_name_regex = re_compile(r"[a-zA-Z][-_./a-zA-Z0-9]{0,254}") _unit_regex = re_compile(r"[\x00-\x7F]{0,63}") diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index 4a3d3d448b..e66460de35 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -564,14 +564,13 @@ def test_observable_up_down_counter_callback(self): ) def test_name_check(self): - instrument = ChildInstrument("name") self.assertEqual( instrument._check_name_unit_description( - "a" * 63, "unit", "description" + "a" * 255, "unit", "description" )["name"], - "a" * 63, + "a" * 255, ) self.assertEqual( instrument._check_name_unit_description( @@ -591,12 +590,24 @@ def test_name_check(self): )["name"], "a_", ) + self.assertEqual( + instrument._check_name_unit_description( + "a/", "unit", "description" + )["name"], + "a/", + ) - self.assertIsNone( + # the old max length + self.assertIsNotNone( instrument._check_name_unit_description( "a" * 64, "unit", "description" )["name"] ) + self.assertIsNone( + instrument._check_name_unit_description( + "a" * 256, "unit", "description" + )["name"] + ) self.assertIsNone( instrument._check_name_unit_description( "Ñ", "unit", "description" From 0c382ee5356e1fa5a292e7a6c4961390cdfbe6df Mon Sep 17 00:00:00 2001 From: Mathieu Malaterre Date: Tue, 19 Sep 2023 20:04:32 +0200 Subject: [PATCH 1477/1517] Cleanup YAML file (#3440) --- docs/examples/logs/README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/examples/logs/README.rst b/docs/examples/logs/README.rst index 8a91c7f695..3821466e32 100644 --- a/docs/examples/logs/README.rst +++ b/docs/examples/logs/README.rst @@ -12,15 +12,15 @@ Start the Collector locally to see data being exported. Write the following file # otel-collector-config.yaml receivers: - otlp: + otlp: protocols: - grpc: - - exporters: - logging: + grpc: processors: - batch: + batch: + + exporters: + logging: service: pipelines: @@ -78,4 +78,4 @@ The resulting logs will appear in the output from the collector and look similar Body: Hyderabad, we have a major problem. Trace ID: 63491217958f126f727622e41d4460f3 Span ID: d90c57d6e1ca4f6c - Flags: 1 \ No newline at end of file + Flags: 1 From 3f0d03b9d405dc8040262f3482d02152f02eec81 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 25 Sep 2023 09:03:05 -0700 Subject: [PATCH 1478/1517] Do not load resource on sdk import (#3447) --- CHANGELOG.md | 2 ++ .../sdk/_logs/_internal/__init__.py | 7 +++++-- .../sdk/metrics/_internal/__init__.py | 4 +++- .../src/opentelemetry/sdk/trace/__init__.py | 18 +++++++++++++----- opentelemetry-sdk/tests/logs/test_logs.py | 19 ++++++++++++++++++- .../tests/metrics/test_metrics.py | 18 +++++++++++++++++- opentelemetry-sdk/tests/trace/test_trace.py | 18 +++++++++++++++++- 7 files changed, 75 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf7db9c04f..7a8b0656e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3428](https://github.com/open-telemetry/opentelemetry-python/pull/3428)) - Allow instrument names to have '/' and up to 255 characters ([#3442](https://github.com/open-telemetry/opentelemetry-python/pull/3442)) +- Do not load Resource on sdk import + ([#3447](https://github.com/open-telemetry/opentelemetry-python/pull/3447)) ## Version 1.20.0/0.41b0 (2023-09-04) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index d3db94d624..0707c00bff 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -581,14 +581,17 @@ def emit(self, record: LogRecord): class LoggerProvider(APILoggerProvider): def __init__( self, - resource: Resource = Resource.create(), + resource: Resource = None, shutdown_on_exit: bool = True, multi_log_record_processor: Union[ SynchronousMultiLogRecordProcessor, ConcurrentMultiLogRecordProcessor, ] = None, ): - self._resource = resource + if resource is None: + self._resource = Resource.create({}) + else: + self._resource = resource self._multi_log_record_processor = ( multi_log_record_processor or SynchronousMultiLogRecordProcessor() ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index a804e6e5e7..ffec748ccb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -341,13 +341,15 @@ def __init__( metric_readers: Sequence[ "opentelemetry.sdk.metrics.export.MetricReader" ] = (), - resource: Resource = Resource.create({}), + resource: Resource = None, shutdown_on_exit: bool = True, views: Sequence["opentelemetry.sdk.metrics.view.View"] = (), ): self._lock = Lock() self._meter_lock = Lock() self._atexit_handler = None + if resource is None: + resource = Resource.create({}) self._sdk_config = SdkConfiguration( resource=resource, metric_readers=metric_readers, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 2d6147a436..0e4867a5c8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -351,7 +351,7 @@ def __init__( name: str = None, context: trace_api.SpanContext = None, parent: Optional[trace_api.SpanContext] = None, - resource: Resource = Resource.create({}), + resource: Resource = None, attributes: types.Attributes = None, events: Sequence[Event] = (), links: Sequence[trace_api.Link] = (), @@ -373,7 +373,10 @@ def __init__( self._attributes = attributes self._events = events self._links = links - self._resource = resource + if resource is None: + self._resource = Resource.create({}) + else: + self._resource = resource self._status = status @property @@ -745,7 +748,7 @@ def __init__( parent: Optional[trace_api.SpanContext] = None, sampler: Optional[sampling.Sampler] = None, trace_config: None = None, # TODO - resource: Resource = Resource.create({}), + resource: Resource = None, attributes: types.Attributes = None, events: Sequence[Event] = None, links: Sequence[trace_api.Link] = (), @@ -757,6 +760,8 @@ def __init__( limits=_UnsetLimits, instrumentation_scope: InstrumentationScope = None, ) -> None: + if resource is None: + resource = Resource.create({}) super().__init__( name=name, context=context, @@ -1128,7 +1133,7 @@ class TracerProvider(trace_api.TracerProvider): def __init__( self, sampler: sampling.Sampler = None, - resource: Resource = Resource.create({}), + resource: Resource = None, shutdown_on_exit: bool = True, active_span_processor: Union[ SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor @@ -1143,7 +1148,10 @@ def __init__( self.id_generator = RandomIdGenerator() else: self.id_generator = id_generator - self._resource = resource + if resource is None: + self._resource = Resource.create({}) + else: + self._resource = resource if not sampler: sampler = sampling._get_from_env_or_default() self.sampler = sampler diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index 80a7d66f1c..935b5ee249 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -15,8 +15,12 @@ # pylint: disable=protected-access import unittest +from unittest.mock import patch from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk._logs._internal import ( + SynchronousMultiLogRecordProcessor, +) from opentelemetry.sdk.resources import Resource @@ -29,7 +33,7 @@ def test_resource(self): logger_provider_0 = LoggerProvider() logger_provider_1 = LoggerProvider() - self.assertIs( + self.assertEqual( logger_provider_0.resource, logger_provider_1.resource, ) @@ -56,3 +60,16 @@ def test_get_logger(self): self.assertEqual( logger._instrumentation_scope.schema_url, "schema_url" ) + + @patch.object(Resource, "create") + def test_logger_provider_init(self, resource_patch): + logger_provider = LoggerProvider() + resource_patch.assert_called_once() + self.assertIsNotNone(logger_provider._resource) + self.assertTrue( + isinstance( + logger_provider._multi_log_record_processor, + SynchronousMultiLogRecordProcessor, + ) + ) + self.assertIsNotNone(logger_provider._at_exit_handler) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 37a6a77aff..8373d3dfe0 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -30,6 +30,7 @@ ObservableUpDownCounter, UpDownCounter, ) +from opentelemetry.sdk.metrics._internal import SynchronousMeasurementConsumer from opentelemetry.sdk.metrics.export import ( Metric, MetricExporter, @@ -63,6 +64,21 @@ def tearDown(self): MeterProvider._all_metric_readers = set() + @patch.object(Resource, "create") + def test_init_default(self, resource_patch): + meter_provider = MeterProvider() + resource_mock = resource_patch.return_value + resource_patch.assert_called_once() + self.assertIsNotNone(meter_provider._sdk_config) + self.assertEqual(meter_provider._sdk_config.resource, resource_mock) + self.assertTrue( + isinstance( + meter_provider._measurement_consumer, + SynchronousMeasurementConsumer, + ) + ) + self.assertIsNotNone(meter_provider._atexit_handler) + def test_register_metric_readers(self): mock_exporter = Mock() mock_exporter._preferred_temporality = None @@ -88,7 +104,7 @@ def test_resource(self): meter_provider_0 = MeterProvider() meter_provider_1 = MeterProvider() - self.assertIs( + self.assertEqual( meter_provider_0._sdk_config.resource, meter_provider_1._sdk_config.resource, ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index a49b86b851..fc46cb136f 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -23,7 +23,7 @@ from time import time_ns from typing import Optional from unittest import mock -from unittest.mock import Mock +from unittest.mock import Mock, patch from opentelemetry import trace as trace_api from opentelemetry.context import Context @@ -1942,3 +1942,19 @@ def test_child_parent_span_exception(self): self.assertTrue(parent_span.status.is_ok) self.assertIsNone(parent_span.status.description) self.assertTupleEqual(parent_span.events, ()) + + +# pylint: disable=protected-access +class TestTracerProvider(unittest.TestCase): + @patch("opentelemetry.sdk.trace.sampling._get_from_env_or_default") + @patch.object(Resource, "create") + def test_tracer_provider_init_default(self, resource_patch, sample_patch): + tracer_provider = trace.TracerProvider() + self.assertTrue( + isinstance(tracer_provider.id_generator, RandomIdGenerator) + ) + resource_patch.assert_called_once() + self.assertIsNotNone(tracer_provider._resource) + sample_patch.assert_called_once() + self.assertIsNotNone(tracer_provider._span_limits) + self.assertIsNotNone(tracer_provider._atexit_handler) From c8e5c3e96c25e0621c4234a720d27cf75a594bc4 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 26 Sep 2023 10:33:13 -0700 Subject: [PATCH 1479/1517] Update __init__.py (#3448) --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0e4867a5c8..ea015bc606 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -344,7 +344,12 @@ def wrapper(self, *args, **kwargs): class ReadableSpan: - """Provides read-only access to span attributes""" + """Provides read-only access to span attributes. + + Users should NOT be creating these objects directly. `ReadableSpan`s are created as + a direct result from using the tracing pipeline via the `Tracer`. + + """ def __init__( self, From 495d70545f84cf0695c4c35aef4b47354f4a4cfe Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Mon, 2 Oct 2023 08:52:09 -0700 Subject: [PATCH 1480/1517] Update semconv to 1.21.0 (#3251) --- .codespellrc | 2 +- .github/workflows/test.yml | 2 +- .gitignore | 3 + CHANGELOG.md | 4 +- docs/examples/django/README.rst | 18 +- opentelemetry-sdk/tests/trace/test_trace.py | 14 +- .../opentelemetry/semconv/metrics/__init__.py | 185 +- .../semconv/resource/__init__.py | 284 ++- .../opentelemetry/semconv/trace/__init__.py | 1807 +++++++++++++---- scripts/semconv/generate.sh | 37 +- .../semconv/templates/semantic_attributes.j2 | 323 ++- scripts/semconv/templates/semantic_metrics.j2 | 40 + 12 files changed, 2202 insertions(+), 517 deletions(-) create mode 100644 scripts/semconv/templates/semantic_metrics.j2 diff --git a/.codespellrc b/.codespellrc index f5236d7c1b..b81e62f640 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,4 +1,4 @@ [codespell] # skipping auto generated folders -skip = ./.tox,./.mypy_cache,./docs/_build,./target,*/LICENSE,./venv,.git +skip = ./.tox,./.mypy_cache,./docs/_build,./target,*/LICENSE,./venv,.git,./opentelemetry-semantic-conventions ignore-words-list = ans,ue,ot,hist,ro diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd600c03af..88c1675c47 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -119,7 +119,7 @@ jobs: # Contrib unit test suite in order to ensure changes in core do not break anything in contrib. # We only run contrib unit tests on the oldest supported Python version (3.7) as running the same tests - # on all versions is somewhat redundant. + # on all versions is somewhat redundant. contrib-build: env: # We use these variables to convert between tox and GHA version literals diff --git a/.gitignore b/.gitignore index 9b3ce8568f..f2324c7bfc 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ target # Django example docs/examples/django/db.sqlite3 + +# Semantic conventions +scripts/semconv/semantic-conventions diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a8b0656e5..e5e0dd09f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3442](https://github.com/open-telemetry/opentelemetry-python/pull/3442)) - Do not load Resource on sdk import ([#3447](https://github.com/open-telemetry/opentelemetry-python/pull/3447)) +- Update semantic conventions to version 1.21.0 + ([#3251](https://github.com/open-telemetry/opentelemetry-python/pull/3251)) ## Version 1.20.0/0.41b0 (2023-09-04) @@ -29,7 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3372](https://github.com/open-telemetry/opentelemetry-python/pull/3372)) - Update the body type in the log ([$3343](https://github.com/open-telemetry/opentelemetry-python/pull/3343)) -- Add max_scale option to Exponential Bucket Histogram Aggregation +- Add max_scale option to Exponential Bucket Histogram Aggregation ([#3323](https://github.com/open-telemetry/opentelemetry-python/pull/3323)) - Use BoundedAttributes instead of raw dict to extract attributes from LogRecord ([#3310](https://github.com/open-telemetry/opentelemetry-python/pull/3310)) diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst index 2e071127ff..1dd8999c03 100644 --- a/docs/examples/django/README.rst +++ b/docs/examples/django/README.rst @@ -84,16 +84,14 @@ output similar to this one: "status_code": "OK" }, "attributes": { - "http.method": "GET", - "http.server_name": "localhost", - "http.scheme": "http", - "host.port": 8000, - "http.host": "localhost:8000", - "http.url": "http://localhost:8000/?param=hello", - "net.peer.ip": "127.0.0.1", - "http.flavor": "1.1", - "http.status_text": "OK", - "http.status_code": 200 + "http.request.method": "GET", + "server.address": "localhost", + "url.scheme": "http", + "server.port": 8000, + "url.full": "http://localhost:8000/?param=hello", + "server.socket.address": "127.0.0.1", + "network.protocol.version": "1.1", + "http.response.status_code": 200 }, "events": [], "links": [] diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index fc46cb136f..b08afce4a6 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -640,12 +640,12 @@ def test_attributes(self): with self.tracer.start_as_current_span("root") as root: root.set_attributes( { - "http.method": "GET", - "http.url": "https://example.com:779/path/12/?q=d#123", + "http.request.method": "GET", + "url.full": "https://example.com:779/path/12/?q=d#123", } ) - root.set_attribute("http.status_code", 200) + root.set_attribute("http.response.status_code", 200) root.set_attribute("http.status_text", "OK") root.set_attribute("misc.pi", 3.14) @@ -661,12 +661,12 @@ def test_attributes(self): root.set_attribute("list-of-numerics", list_of_numerics) self.assertEqual(len(root.attributes), 9) - self.assertEqual(root.attributes["http.method"], "GET") + self.assertEqual(root.attributes["http.request.method"], "GET") self.assertEqual( - root.attributes["http.url"], + root.attributes["url.full"], "https://example.com:779/path/12/?q=d#123", ) - self.assertEqual(root.attributes["http.status_code"], 200) + self.assertEqual(root.attributes["http.response.status_code"], 200) self.assertEqual(root.attributes["http.status_text"], "OK") self.assertEqual(root.attributes["misc.pi"], 3.14) self.assertEqual(root.attributes["attr-key"], "attr-value2") @@ -1007,7 +1007,7 @@ def test_ended_span(self): self.assertEqual(end_time0, root.end_time) with self.assertLogs(level=WARNING): - root.set_attribute("http.method", "GET") + root.set_attribute("http.request.method", "GET") self.assertEqual(len(root.attributes), 0) with self.assertLogs(level=WARNING): diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/__init__.py index e7a03cf818..9cd7cee94f 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/__init__.py @@ -12,23 +12,200 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=too-many-lines - class MetricInstruments: + SCHEMA_URL = "https://opentelemetry.io/schemas/v1.21.0" + """ + The URL of the OpenTelemetry schema for these keys and values. + """ HTTP_SERVER_DURATION = "http.server.duration" + """ + Measures the duration of inbound HTTP requests + Instrument: histogram + Unit: s + """ + + HTTP_SERVER_ACTIVE_REQUESTS = "http.server.active_requests" + """ + Measures the number of concurrent HTTP requests that are currently in-flight + Instrument: updowncounter + Unit: {request} + """ HTTP_SERVER_REQUEST_SIZE = "http.server.request.size" + """ + Measures the size of HTTP request messages (compressed) + Instrument: histogram + Unit: By + """ HTTP_SERVER_RESPONSE_SIZE = "http.server.response.size" - - HTTP_SERVER_ACTIVE_REQUESTS = "http.server.active_requests" + """ + Measures the size of HTTP response messages (compressed) + Instrument: histogram + Unit: By + """ HTTP_CLIENT_DURATION = "http.client.duration" + """ + Measures the duration of outbound HTTP requests + Instrument: histogram + Unit: s + """ HTTP_CLIENT_REQUEST_SIZE = "http.client.request.size" + """ + Measures the size of HTTP request messages (compressed) + Instrument: histogram + Unit: By + """ HTTP_CLIENT_RESPONSE_SIZE = "http.client.response.size" + """ + Measures the size of HTTP response messages (compressed) + Instrument: histogram + Unit: By + """ + + PROCESS_RUNTIME_JVM_MEMORY_INIT = "process.runtime.jvm.memory.init" + """ + Measure of initial memory requested + Instrument: updowncounter + Unit: By + """ + + PROCESS_RUNTIME_JVM_SYSTEM_CPU_UTILIZATION = ( + "process.runtime.jvm.system.cpu.utilization" + ) + """ + Recent CPU utilization for the whole system as reported by the JVM + Instrument: gauge + Unit: 1 + """ + + PROCESS_RUNTIME_JVM_SYSTEM_CPU_LOAD_1M = ( + "process.runtime.jvm.system.cpu.load_1m" + ) + """ + Average CPU load of the whole system for the last minute as reported by the JVM + Instrument: gauge + Unit: 1 + """ + + PROCESS_RUNTIME_JVM_BUFFER_USAGE = "process.runtime.jvm.buffer.usage" + """ + Measure of memory used by buffers + Instrument: updowncounter + Unit: By + """ + + PROCESS_RUNTIME_JVM_BUFFER_LIMIT = "process.runtime.jvm.buffer.limit" + """ + Measure of total memory capacity of buffers + Instrument: updowncounter + Unit: By + """ + + PROCESS_RUNTIME_JVM_BUFFER_COUNT = "process.runtime.jvm.buffer.count" + """ + Number of buffers in the pool + Instrument: updowncounter + Unit: {buffer} + """ + + PROCESS_RUNTIME_JVM_MEMORY_USAGE = "process.runtime.jvm.memory.usage" + """ + Measure of memory used + Instrument: updowncounter + Unit: By + """ + + PROCESS_RUNTIME_JVM_MEMORY_COMMITTED = ( + "process.runtime.jvm.memory.committed" + ) + """ + Measure of memory committed + Instrument: updowncounter + Unit: By + """ + + PROCESS_RUNTIME_JVM_MEMORY_LIMIT = "process.runtime.jvm.memory.limit" + """ + Measure of max obtainable memory + Instrument: updowncounter + Unit: By + """ + + PROCESS_RUNTIME_JVM_MEMORY_USAGE_AFTER_LAST_GC = ( + "process.runtime.jvm.memory.usage_after_last_gc" + ) + """ + Measure of memory used, as measured after the most recent garbage collection event on this pool + Instrument: updowncounter + Unit: By + """ + + PROCESS_RUNTIME_JVM_GC_DURATION = "process.runtime.jvm.gc.duration" + """ + Duration of JVM garbage collection actions + Instrument: histogram + Unit: s + """ + + PROCESS_RUNTIME_JVM_THREADS_COUNT = "process.runtime.jvm.threads.count" + """ + Number of executing platform threads + Instrument: updowncounter + Unit: {thread} + """ + + PROCESS_RUNTIME_JVM_CLASSES_LOADED = "process.runtime.jvm.classes.loaded" + """ + Number of classes loaded since JVM start + Instrument: counter + Unit: {class} + """ + + PROCESS_RUNTIME_JVM_CLASSES_UNLOADED = ( + "process.runtime.jvm.classes.unloaded" + ) + """ + Number of classes unloaded since JVM start + Instrument: counter + Unit: {class} + """ + + PROCESS_RUNTIME_JVM_CLASSES_CURRENT_LOADED = ( + "process.runtime.jvm.classes.current_loaded" + ) + """ + Number of classes currently loaded + Instrument: updowncounter + Unit: {class} + """ + + PROCESS_RUNTIME_JVM_CPU_TIME = "process.runtime.jvm.cpu.time" + """ + CPU time used by the process as reported by the JVM + Instrument: counter + Unit: s + """ + + PROCESS_RUNTIME_JVM_CPU_RECENT_UTILIZATION = ( + "process.runtime.jvm.cpu.recent_utilization" + ) + """ + Recent CPU utilization for the process as reported by the JVM + Instrument: gauge + Unit: 1 + """ + + # Manually defined metrics DB_CLIENT_CONNECTIONS_USAGE = "db.client.connections.usage" + """ + The number of connections that are currently in state described by the `state` attribute + Instrument: UpDownCounter + Unit: {connection} + """ diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py index 028fedd6df..590135934c 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py @@ -12,10 +12,47 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines + from enum import Enum class ResourceAttributes: + SCHEMA_URL = "https://opentelemetry.io/schemas/v1.21.0" + """ + The URL of the OpenTelemetry schema for these keys and values. + """ + BROWSER_BRANDS = "browser.brands" + """ + Array of brand name and version separated by a space. + Note: This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.brands`). + """ + + BROWSER_PLATFORM = "browser.platform" + """ + The platform on which the browser is running. + Note: This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.platform`). If unavailable, the legacy `navigator.platform` API SHOULD NOT be used instead and this attribute SHOULD be left unset in order for the values to be consistent. + The list of possible values is defined in the [W3C User-Agent Client Hints specification](https://wicg.github.io/ua-client-hints/#sec-ch-ua-platform). Note that some (but not all) of these values can overlap with values in the [`os.type` and `os.name` attributes](./os.md). However, for consistency, the values in the `browser.platform` attribute should capture the exact value that the user agent provides. + """ + + BROWSER_MOBILE = "browser.mobile" + """ + A boolean that is true if the browser is running on a mobile device. + Note: This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.mobile`). If unavailable, this attribute SHOULD be left unset. + """ + + BROWSER_LANGUAGE = "browser.language" + """ + Preferred language of the user using the browser. + Note: This value is intended to be taken from the Navigator API `navigator.language`. + """ + + USER_AGENT_ORIGINAL = "user_agent.original" + """ + Full user-agent string provided by the browser. + Note: The user-agent value SHOULD be provided only from browsers that do not have a mechanism to retrieve brands and platform individually from the User-Agent Client Hints API. To retrieve the value, the legacy `navigator.userAgent` API can be used. + """ + CLOUD_PROVIDER = "cloud.provider" """ Name of the cloud provider. @@ -29,7 +66,29 @@ class ResourceAttributes: CLOUD_REGION = "cloud.region" """ The geographical region the resource is running. - Note: Refer to your provider's docs to see the available regions, for example [Alibaba Cloud regions](https://www.alibabacloud.com/help/doc-detail/40654.htm), [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/en-us/global-infrastructure/geographies/), [Google Cloud regions](https://cloud.google.com/about/locations), or [Tencent Cloud regions](https://intl.cloud.tencent.com/document/product/213/6091). + Note: Refer to your provider's docs to see the available regions, for example [Alibaba Cloud regions](https://www.alibabacloud.com/help/doc-detail/40654.htm), [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/en-us/global-infrastructure/geographies/), [Google Cloud regions](https://cloud.google.com/about/locations), or [Tencent Cloud regions](https://www.tencentcloud.com/document/product/213/6091). + """ + + CLOUD_RESOURCE_ID = "cloud.resource_id" + """ + Cloud provider-specific native identifier of the monitored cloud resource (e.g. an [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) on AWS, a [fully qualified resource ID](https://learn.microsoft.com/en-us/rest/api/resources/resources/get-by-id) on Azure, a [full resource name](https://cloud.google.com/apis/design/resource_names#full_resource_name) on GCP). + Note: On some cloud providers, it may not be possible to determine the full ID at startup, + so it may be necessary to set `cloud.resource_id` as a span attribute instead. + + The exact value to use for `cloud.resource_id` depends on the cloud provider. + The following well-known definitions MUST be used if you set this attribute and they apply: + + * **AWS Lambda:** The function [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html). + Take care not to use the "invoked ARN" directly but replace any + [alias suffix](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html) + with the resolved function version, as the same runtime instance may be invokable with + multiple different aliases. + * **GCP:** The [URI of the resource](https://cloud.google.com/iam/docs/full-resource-names) + * **Azure:** The [Fully Qualified Resource ID](https://docs.microsoft.com/en-us/rest/api/resources/resources/get-by-id) of the invoked function, + *not* the function app, having the form + `/subscriptions//resourceGroups//providers/Microsoft.Web/sites//functions/`. + This means that a span attribute MUST be used, as an Azure function app can host multiple functions that would usually share + a TracerProvider. """ CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone" @@ -102,6 +161,41 @@ class ResourceAttributes: Note: See the [log stream ARN format documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html#CWL_ARN_Format). One log group can contain several log streams, so these ARNs necessarily identify both a log group and a log stream. """ + GCP_CLOUD_RUN_JOB_EXECUTION = "gcp.cloud_run.job.execution" + """ + The name of the Cloud Run [execution](https://cloud.google.com/run/docs/managing/job-executions) being run for the Job, as set by the [`CLOUD_RUN_EXECUTION`](https://cloud.google.com/run/docs/container-contract#jobs-env-vars) environment variable. + """ + + GCP_CLOUD_RUN_JOB_TASK_INDEX = "gcp.cloud_run.job.task_index" + """ + The index for a task within an execution as provided by the [`CLOUD_RUN_TASK_INDEX`](https://cloud.google.com/run/docs/container-contract#jobs-env-vars) environment variable. + """ + + GCP_GCE_INSTANCE_NAME = "gcp.gce.instance.name" + """ + The instance name of a GCE instance. This is the value provided by `host.name`, the visible name of the instance in the Cloud Console UI, and the prefix for the default hostname of the instance as defined by the [default internal DNS name](https://cloud.google.com/compute/docs/internal-dns#instance-fully-qualified-domain-names). + """ + + GCP_GCE_INSTANCE_HOSTNAME = "gcp.gce.instance.hostname" + """ + The hostname of a GCE instance. This is the full value of the default or [custom hostname](https://cloud.google.com/compute/docs/instances/custom-hostname-vm). + """ + + HEROKU_RELEASE_CREATION_TIMESTAMP = "heroku.release.creation_timestamp" + """ + Time and date the release was created. + """ + + HEROKU_RELEASE_COMMIT = "heroku.release.commit" + """ + Commit hash for the current release. + """ + + HEROKU_APP_ID = "heroku.app.id" + """ + Unique identifier for the application. + """ + CONTAINER_NAME = "container.name" """ Container name used by container runtime. @@ -127,6 +221,30 @@ class ResourceAttributes: Container image tag. """ + CONTAINER_IMAGE_ID = "container.image.id" + """ + Runtime specific image identifier. Usually a hash algorithm followed by a UUID. + Note: Docker defines a sha256 of the image id; `container.image.id` corresponds to the `Image` field from the Docker container inspect [API](https://docs.docker.com/engine/api/v1.43/#tag/Container/operation/ContainerInspect) endpoint. + K8s defines a link to the container registry repository with digest `"imageID": "registry.azurecr.io /namespace/service/dockerfile@sha256:bdeabd40c3a8a492eaf9e8e44d0ebbb84bac7ee25ac0cf8a7159d25f62555625"`. + OCI defines a digest of manifest. + """ + + CONTAINER_COMMAND = "container.command" + """ + The command used to run the container (i.e. the command name). + Note: If using embedded credentials or sensitive data, it is recommended to remove them to prevent potential leakage. + """ + + CONTAINER_COMMAND_LINE = "container.command_line" + """ + The full command run by the container as a single string representing the full command. [2]. + """ + + CONTAINER_COMMAND_ARGS = "container.command_args" + """ + All the command arguments (including the command/executable itself) run by the container. [2]. + """ + DEPLOYMENT_ENVIRONMENT = "deployment.environment" """ Name of the [deployment environment](https://en.wikipedia.org/wiki/Deployment_environment) (aka deployment tier). @@ -159,26 +277,22 @@ class ResourceAttributes: FAAS_NAME = "faas.name" """ The name of the single function that this runtime instance executes. - Note: This is the name of the function as configured/deployed on the FaaS platform and is usually different from the name of the callback function (which may be stored in the [`code.namespace`/`code.function`](../../trace/semantic_conventions/span-general.md#source-code-attributes) span attributes). - """ + Note: This is the name of the function as configured/deployed on the FaaS + platform and is usually different from the name of the callback + function (which may be stored in the + [`code.namespace`/`code.function`](/docs/general/general-attributes.md#source-code-attributes) + span attributes). - FAAS_ID = "faas.id" - """ - The unique ID of the single function that this runtime instance executes. - Note: Depending on the cloud provider, use: - -* **AWS Lambda:** The function [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html). -Take care not to use the "invoked ARN" directly but replace any -[alias suffix](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html) with the resolved function version, as the same runtime instance may be invocable with multiple -different aliases. -* **GCP:** The [URI of the resource](https://cloud.google.com/iam/docs/full-resource-names) -* **Azure:** The [Fully Qualified Resource ID](https://docs.microsoft.com/en-us/rest/api/resources/resources/get-by-id). + For some cloud providers, the above definition is ambiguous. The following + definition of function name MUST be used for this attribute + (and consequently the span name) for the listed cloud providers/products: -On some providers, it may not be possible to determine the full ID at startup, -which is why this field cannot be made required. For example, on AWS the account ID -part of the ARN is not available without calling another AWS API -which may be deemed too slow for a short-running lambda function. -As an alternative, consider setting `faas.id` as a span attribute instead. + * **Azure:** The full name `/`, i.e., function app name + followed by a forward slash followed by the function name (this form + can also be seen in the resource JSON for the function). + This means that a span attribute MUST be used, as an Azure function + app can host multiple functions that would usually share + a TracerProvider (see also the `cloud.resource_id` attribute). """ FAAS_VERSION = "faas.version" @@ -186,13 +300,13 @@ class ResourceAttributes: The immutable version of the function being executed. Note: Depending on the cloud provider and platform, use: -* **AWS Lambda:** The [function version](https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html) - (an integer represented as a decimal string). -* **Google Cloud Run:** The [revision](https://cloud.google.com/run/docs/managing/revisions) - (i.e., the function name plus the revision suffix). -* **Google Cloud Functions:** The value of the - [`K_REVISION` environment variable](https://cloud.google.com/functions/docs/env-var#runtime_environment_variables_set_automatically). -* **Azure Functions:** Not applicable. Do not set this attribute. + * **AWS Lambda:** The [function version](https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html) + (an integer represented as a decimal string). + * **Google Cloud Run (Services):** The [revision](https://cloud.google.com/run/docs/managing/revisions) + (i.e., the function name plus the revision suffix). + * **Google Cloud Functions:** The value of the + [`K_REVISION` environment variable](https://cloud.google.com/functions/docs/env-var#runtime_environment_variables_set_automatically). + * **Azure Functions:** Not applicable. Do not set this attribute. """ FAAS_INSTANCE = "faas.instance" @@ -203,13 +317,13 @@ class ResourceAttributes: FAAS_MAX_MEMORY = "faas.max_memory" """ - The amount of memory available to the serverless function in MiB. - Note: It's recommended to set this attribute since e.g. too little memory can easily stop a Java AWS Lambda function from working correctly. On AWS Lambda, the environment variable `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` provides this information. + The amount of memory available to the serverless function converted to Bytes. + Note: It's recommended to set this attribute since e.g. too little memory can easily stop a Java AWS Lambda function from working correctly. On AWS Lambda, the environment variable `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` provides this information (which must be multiplied by 1,048,576). """ HOST_ID = "host.id" """ - Unique host ID. For Cloud, this must be the instance_id assigned by the cloud provider. + Unique host ID. For Cloud, this must be the instance_id assigned by the cloud provider. For non-containerized systems, this should be the `machine-id`. See the table below for the sources to use to determine the `machine-id` based on operating system. """ HOST_NAME = "host.name" @@ -234,12 +348,12 @@ class ResourceAttributes: HOST_IMAGE_ID = "host.image.id" """ - VM image ID. For Cloud, this value is from the provider. + VM image ID or host OS image ID. For Cloud, this value is from the provider. """ HOST_IMAGE_VERSION = "host.image.version" """ - The version string of the VM image as defined in [Version Attributes](README.md#version-attributes). + The version string of the VM image or host OS as defined in [Version Attributes](README.md#version-attributes). """ K8S_CLUSTER_NAME = "k8s.cluster.name" @@ -247,6 +361,33 @@ class ResourceAttributes: The name of the cluster. """ + K8S_CLUSTER_UID = "k8s.cluster.uid" + """ + A pseudo-ID for the cluster, set to the UID of the `kube-system` namespace. + Note: K8s does not have support for obtaining a cluster ID. If this is ever + added, we will recommend collecting the `k8s.cluster.uid` through the + official APIs. In the meantime, we are able to use the `uid` of the + `kube-system` namespace as a proxy for cluster ID. Read on for the + rationale. + + Every object created in a K8s cluster is assigned a distinct UID. The + `kube-system` namespace is used by Kubernetes itself and will exist + for the lifetime of the cluster. Using the `uid` of the `kube-system` + namespace is a reasonable proxy for the K8s ClusterID as it will only + change if the cluster is rebuilt. Furthermore, Kubernetes UIDs are + UUIDs as standardized by + [ISO/IEC 9834-8 and ITU-T X.667](https://www.itu.int/ITU-T/studygroups/com17/oid.html). + Which states: + + > If generated according to one of the mechanisms defined in Rec. + ITU-T X.667 | ISO/IEC 9834-8, a UUID is either guaranteed to be + different from all other UUIDs generated before 3603 A.D., or is + extremely likely to be different (depending on the mechanism chosen). + + Therefore, UIDs between clusters should be extremely unlikely to + conflict. + """ + K8S_NODE_NAME = "k8s.node.name" """ The name of the Node. @@ -359,7 +500,7 @@ class ResourceAttributes: OS_VERSION = "os.version" """ - The version string of the operating system as defined in [Version Attributes](../../resource/semantic_conventions/README.md#version-attributes). + The version string of the operating system as defined in [Version Attributes](/docs/resource/README.md#version-attributes). """ PROCESS_PID = "process.pid" @@ -367,6 +508,11 @@ class ResourceAttributes: Process identifier (PID). """ + PROCESS_PARENT_PID = "process.parent_pid" + """ + Parent Process identifier (PID). + """ + PROCESS_EXECUTABLE_NAME = "process.executable.name" """ The name of the process executable. On Linux based systems, can be set to the `Name` in `proc/[pid]/status`. On Windows, can be set to the base name of `GetProcessImageFileNameW`. @@ -418,6 +564,11 @@ class ResourceAttributes: Note: MUST be the same for all instances of horizontally scaled services. If the value was not specified, SDKs MUST fallback to `unknown_service:` concatenated with [`process.executable.name`](process.md#process), e.g. `unknown_service:bash`. If `process.executable.name` is not available, the value MUST be set to `unknown_service`. """ + SERVICE_VERSION = "service.version" + """ + The version string of the service API or implementation. The format is not defined by these conventions. + """ + SERVICE_NAMESPACE = "service.namespace" """ A namespace for `service.name`. @@ -430,14 +581,15 @@ class ResourceAttributes: Note: MUST be unique for each instance of the same `service.namespace,service.name` pair (in other words `service.namespace,service.name,service.instance.id` triplet MUST be globally unique). The ID helps to distinguish instances of the same service that exist at the same time (e.g. instances of a horizontally scaled service). It is preferable for the ID to be persistent and stay the same for the lifetime of the service instance, however it is acceptable that the ID is ephemeral and changes during important lifetime events for the service (e.g. service restarts). If the service has no inherent unique ID that can be used as the value of this attribute it is recommended to generate a random Version 1 or Version 4 RFC 4122 UUID (services aiming for reproducible UUIDs may also use Version 5, see RFC 4122 for more recommendations). """ - SERVICE_VERSION = "service.version" - """ - The version string of the service API or implementation. - """ - TELEMETRY_SDK_NAME = "telemetry.sdk.name" """ The name of the telemetry SDK as defined above. + Note: The OpenTelemetry SDK MUST set the `telemetry.sdk.name` attribute to `opentelemetry`. + If another SDK, like a fork or a vendor-provided implementation, is used, this SDK MUST set the + `telemetry.sdk.name` attribute to the fully-qualified class or module name of this SDK's main entry point + or another suitable identifier depending on the language. + The identifier `opentelemetry` is reserved and MUST NOT be used in this case. + All custom identifiers SHOULD be stable across different versions of an implementation. """ TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language" @@ -470,6 +622,33 @@ class ResourceAttributes: Additional description of the web engine (e.g. detailed version and edition information). """ + OTEL_SCOPE_NAME = "otel.scope.name" + """ + The name of the instrumentation scope - (`InstrumentationScope.Name` in OTLP). + """ + + OTEL_SCOPE_VERSION = "otel.scope.version" + """ + The version of the instrumentation scope - (`InstrumentationScope.Version` in OTLP). + """ + + OTEL_LIBRARY_NAME = "otel.library.name" + """ + Deprecated, use the `otel.scope.name` attribute. + """ + + OTEL_LIBRARY_VERSION = "otel.library.version" + """ + Deprecated, use the `otel.scope.version` attribute. + """ + + # Manually defined deprecated attributes + + FAAS_ID = "faas.id" + """ + Deprecated, use the `cloud.resource.id` attribute. + """ + class CloudProviderValues(Enum): ALIBABA_CLOUD = "alibaba_cloud" @@ -484,6 +663,12 @@ class CloudProviderValues(Enum): GCP = "gcp" """Google Cloud Platform.""" + HEROKU = "heroku" + """Heroku Platform as a Service.""" + + IBM_CLOUD = "ibm_cloud" + """IBM Cloud.""" + TENCENT_CLOUD = "tencent_cloud" """Tencent Cloud.""" @@ -495,6 +680,9 @@ class CloudPlatformValues(Enum): ALIBABA_CLOUD_FC = "alibaba_cloud_fc" """Alibaba Cloud Function Compute.""" + ALIBABA_CLOUD_OPENSHIFT = "alibaba_cloud_openshift" + """Red Hat OpenShift on Alibaba Cloud.""" + AWS_EC2 = "aws_ec2" """AWS Elastic Compute Cloud.""" @@ -513,6 +701,9 @@ class CloudPlatformValues(Enum): AWS_APP_RUNNER = "aws_app_runner" """AWS App Runner.""" + AWS_OPENSHIFT = "aws_openshift" + """Red Hat OpenShift on AWS (ROSA).""" + AZURE_VM = "azure_vm" """Azure Virtual Machines.""" @@ -528,6 +719,12 @@ class CloudPlatformValues(Enum): AZURE_APP_SERVICE = "azure_app_service" """Azure App Service.""" + AZURE_OPENSHIFT = "azure_openshift" + """Azure Red Hat OpenShift.""" + + GCP_BARE_METAL_SOLUTION = "gcp_bare_metal_solution" + """Google Bare Metal Solution (BMS).""" + GCP_COMPUTE_ENGINE = "gcp_compute_engine" """Google Cloud Compute Engine (GCE).""" @@ -543,6 +740,12 @@ class CloudPlatformValues(Enum): GCP_APP_ENGINE = "gcp_app_engine" """Google Cloud App Engine (GAE).""" + GCP_OPENSHIFT = "gcp_openshift" + """Red Hat OpenShift on Google Cloud.""" + + IBM_CLOUD_OPENSHIFT = "ibm_cloud_openshift" + """Red Hat OpenShift on IBM Cloud.""" + TENCENT_CLOUD_CVM = "tencent_cloud_cvm" """Tencent Cloud Cloud Virtual Machine (CVM).""" @@ -650,8 +853,11 @@ class TelemetrySdkLanguageValues(Enum): RUBY = "ruby" """ruby.""" - WEBJS = "webjs" - """webjs.""" + RUST = "rust" + """rust.""" SWIFT = "swift" """swift.""" + + WEBJS = "webjs" + """webjs.""" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py index 36bab3d1a1..48df586dc1 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py @@ -16,162 +16,150 @@ from enum import Enum +from deprecated import deprecated + class SpanAttributes: - AWS_LAMBDA_INVOKED_ARN = "aws.lambda.invoked_arn" + SCHEMA_URL = "https://opentelemetry.io/schemas/v1.21.0" """ - The full invoked ARN as provided on the `Context` passed to the function (`Lambda-Runtime-Invoked-Function-Arn` header on the `/runtime/invocation/next` applicable). - Note: This may be different from `faas.id` if an alias is involved. + The URL of the OpenTelemetry schema for these keys and values. """ - - CLOUDEVENTS_EVENT_ID = "cloudevents.event_id" + CLIENT_ADDRESS = "client.address" """ - The [event_id](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#id) uniquely identifies the event. + Client address - unix domain socket name, IPv4 or IPv6 address. + Note: When observed from the server side, and when communicating through an intermediary, `client.address` SHOULD represent client address behind any intermediaries (e.g. proxies) if it's available. """ - CLOUDEVENTS_EVENT_SOURCE = "cloudevents.event_source" + CLIENT_PORT = "client.port" """ - The [source](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#source-1) identifies the context in which an event happened. + Client port number. + Note: When observed from the server side, and when communicating through an intermediary, `client.port` SHOULD represent client port behind any intermediaries (e.g. proxies) if it's available. """ - CLOUDEVENTS_EVENT_SPEC_VERSION = "cloudevents.event_spec_version" + CLIENT_SOCKET_ADDRESS = "client.socket.address" """ - The [version of the CloudEvents specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#specversion) which the event uses. + Immediate client peer address - unix domain socket name, IPv4 or IPv6 address. """ - CLOUDEVENTS_EVENT_TYPE = "cloudevents.event_type" + CLIENT_SOCKET_PORT = "client.socket.port" """ - The [event_type](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#type) contains a value describing the type of event related to the originating occurrence. + Immediate client peer port number. """ - CLOUDEVENTS_EVENT_SUBJECT = "cloudevents.event_subject" + HTTP_METHOD = "http.method" """ - The [subject](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#subject) of the event in the context of the event producer (identified by source). + Deprecated, use `http.request.method` instead. """ - OPENTRACING_REF_TYPE = "opentracing.ref_type" + HTTP_STATUS_CODE = "http.status_code" """ - Parent-child Reference type. - Note: The causal relationship between a child Span and a parent Span. + Deprecated, use `http.response.status_code` instead. """ - DB_SYSTEM = "db.system" + HTTP_SCHEME = "http.scheme" """ - An identifier for the database management system (DBMS) product being used. See below for a list of well-known identifiers. + Deprecated, use `url.scheme` instead. """ - DB_CONNECTION_STRING = "db.connection_string" + HTTP_URL = "http.url" """ - The connection string used to connect to the database. It is recommended to remove embedded credentials. + Deprecated, use `url.full` instead. """ - DB_USER = "db.user" + HTTP_TARGET = "http.target" """ - Username for accessing the database. + Deprecated, use `url.path` and `url.query` instead. """ - DB_JDBC_DRIVER_CLASSNAME = "db.jdbc.driver_classname" + HTTP_REQUEST_CONTENT_LENGTH = "http.request_content_length" """ - The fully-qualified class name of the [Java Database Connectivity (JDBC)](https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/) driver used to connect. + Deprecated, use `http.request.body.size` instead. """ - DB_NAME = "db.name" + HTTP_RESPONSE_CONTENT_LENGTH = "http.response_content_length" """ - This attribute is used to report the name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). - Note: In some SQL databases, the database name to be used is called "schema name". In case there are multiple layers that could be considered for database name (e.g. Oracle instance name and schema name), the database name to be used is the more specific layer (e.g. Oracle schema name). + Deprecated, use `http.response.body.size` instead. """ - DB_STATEMENT = "db.statement" + NET_SOCK_PEER_NAME = "net.sock.peer.name" """ - The database statement being executed. - Note: The value may be sanitized to exclude sensitive information. + Deprecated, use `server.socket.domain` on client spans. """ - DB_OPERATION = "db.operation" + NET_SOCK_PEER_ADDR = "net.sock.peer.addr" """ - The name of the operation being executed, e.g. the [MongoDB command name](https://docs.mongodb.com/manual/reference/command/#database-operations) such as `findAndModify`, or the SQL keyword. - Note: When setting this to an SQL keyword, it is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if the operation name is provided by the library being instrumented. If the SQL statement has an ambiguous operation, or performs more than one operation, this value may be omitted. + Deprecated, use `server.socket.address` on client spans and `client.socket.address` on server spans. """ - NET_PEER_NAME = "net.peer.name" + NET_SOCK_PEER_PORT = "net.sock.peer.port" """ - Remote hostname or similar, see note below. - Note: `net.peer.name` SHOULD NOT be set if capturing it would require an extra DNS lookup. + Deprecated, use `server.socket.port` on client spans and `client.socket.port` on server spans. """ - NET_PEER_IP = "net.peer.ip" + NET_PEER_NAME = "net.peer.name" """ - Remote address of the peer (dotted decimal for IPv4 or [RFC5952](https://tools.ietf.org/html/rfc5952) for IPv6). + Deprecated, use `server.address` on client spans and `client.address` on server spans. """ NET_PEER_PORT = "net.peer.port" """ - Remote port number. - """ - - NET_TRANSPORT = "net.transport" - """ - Transport protocol used. See note below. + Deprecated, use `server.port` on client spans and `client.port` on server spans. """ - DB_MSSQL_INSTANCE_NAME = "db.mssql.instance_name" + NET_HOST_NAME = "net.host.name" """ - The Microsoft SQL Server [instance name](https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15) connecting to. This name is used to determine the port of a named instance. - Note: If setting a `db.mssql.instance_name`, `net.peer.port` is no longer required (but still recommended if non-standard). + Deprecated, use `server.address`. """ - DB_CASSANDRA_PAGE_SIZE = "db.cassandra.page_size" + NET_HOST_PORT = "net.host.port" """ - The fetch size used for paging, i.e. how many rows will be returned at once. + Deprecated, use `server.port`. """ - DB_CASSANDRA_CONSISTENCY_LEVEL = "db.cassandra.consistency_level" + NET_SOCK_HOST_ADDR = "net.sock.host.addr" """ - The consistency level of the query. Based on consistency values from [CQL](https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/dml/dmlConfigConsistency.html). + Deprecated, use `server.socket.address`. """ - DB_CASSANDRA_TABLE = "db.cassandra.table" + NET_SOCK_HOST_PORT = "net.sock.host.port" """ - The name of the primary table that the operation is acting upon, including the keyspace name (if applicable). - Note: This mirrors the db.sql.table attribute but references cassandra rather than sql. It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. + Deprecated, use `server.socket.port`. """ - DB_CASSANDRA_IDEMPOTENCE = "db.cassandra.idempotence" + NET_TRANSPORT = "net.transport" """ - Whether or not the query is idempotent. + Deprecated, use `network.transport`. """ - DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT = ( - "db.cassandra.speculative_execution_count" - ) + NET_PROTOCOL_NAME = "net.protocol.name" """ - The number of times a query was speculatively executed. Not set or `0` if the query was not executed speculatively. + Deprecated, use `network.protocol.name`. """ - DB_CASSANDRA_COORDINATOR_ID = "db.cassandra.coordinator.id" + NET_PROTOCOL_VERSION = "net.protocol.version" """ - The ID of the coordinating node for a query. + Deprecated, use `network.protocol.version`. """ - DB_CASSANDRA_COORDINATOR_DC = "db.cassandra.coordinator.dc" + NET_SOCK_FAMILY = "net.sock.family" """ - The data center of the coordinating node for a query. + Deprecated, use `network.transport` and `network.type`. """ - DB_REDIS_DATABASE_INDEX = "db.redis.database_index" + DESTINATION_DOMAIN = "destination.domain" """ - The index of the database being accessed as used in the [`SELECT` command](https://redis.io/commands/select), provided as an integer. To be used instead of the generic `db.name` attribute. + The domain name of the destination system. + Note: This value may be a host name, a fully qualified domain name, or another host naming format. """ - DB_MONGODB_COLLECTION = "db.mongodb.collection" + DESTINATION_ADDRESS = "destination.address" """ - The collection being accessed within the database stated in `db.name`. + Peer address, for example IP address or UNIX socket name. """ - DB_SQL_TABLE = "db.sql.table" + DESTINATION_PORT = "destination.port" """ - The name of the primary table that the operation is acting upon, including the database name (if applicable). - Note: It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. + Peer port number. """ EXCEPTION_TYPE = "exception.type" @@ -189,377 +177,703 @@ class SpanAttributes: A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG. """ - EXCEPTION_ESCAPED = "exception.escaped" + HTTP_REQUEST_METHOD = "http.request.method" """ - SHOULD be set to true if the exception event is recorded at a point where it is known that the exception is escaping the scope of the span. - Note: An exception is considered to have escaped (or left) the scope of a span, -if that span is ended while the exception is still logically "in flight". -This may be actually "in flight" in some languages (e.g. if the exception -is passed to a Context manager's `__exit__` method in Python) but will -usually be caught at the point of recording the exception in most languages. + HTTP request method. + Note: HTTP request method value SHOULD be "known" to the instrumentation. + By default, this convention defines "known" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods) + and the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html). -It is usually not possible to determine at the point where an exception is thrown -whether it will escape the scope of a span. -However, it is trivial to know that an exception -will escape, if one checks for an active exception just before ending the span, -as done in the [example above](#recording-an-exception). + If the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER` and, except if reporting a metric, MUST + set the exact method received in the request line as value of the `http.request.method_original` attribute. -It follows that an exception may still escape the scope of the span -even if the `exception.escaped` attribute was not set or set to false, -since the event might have been recorded at a time where it was not -clear whether the exception will escape. - """ + If the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override + the list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named + OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods + (this list MUST be a full override of the default known method, it is not a list of known methods in addition to the defaults). - FAAS_TRIGGER = "faas.trigger" + HTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly. + Instrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent. + Tracing instrumentations that do so, MUST also set `http.request.method_original` to the original value. """ - Type of the trigger which caused this function execution. - Note: For the server/consumer span on the incoming side, -`faas.trigger` MUST be set. -Clients invoking FaaS instances usually cannot set `faas.trigger`, -since they would typically need to look in the payload to determine -the event type. If clients set it, it should be the same as the -trigger that corresponding incoming would have (i.e., this has -nothing to do with the underlying transport used to make the API -call to invoke the lambda, which is often HTTP). + HTTP_RESPONSE_STATUS_CODE = "http.response.status_code" + """ + [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). """ - FAAS_EXECUTION = "faas.execution" + NETWORK_PROTOCOL_NAME = "network.protocol.name" """ - The execution ID of the current function execution. + [OSI Application Layer](https://osi-model.com/application-layer/) or non-OSI equivalent. The value SHOULD be normalized to lowercase. """ - FAAS_DOCUMENT_COLLECTION = "faas.document.collection" + NETWORK_PROTOCOL_VERSION = "network.protocol.version" """ - The name of the source on which the triggering operation was performed. For example, in Cloud Storage or S3 corresponds to the bucket name, and in Cosmos DB to the database name. + Version of the application layer protocol used. See note below. + Note: `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client used has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. """ - FAAS_DOCUMENT_OPERATION = "faas.document.operation" + SERVER_ADDRESS = "server.address" """ - Describes the type of the operation that was performed on the data. + Host identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to. + Note: Determined by using the first of the following that applies + + - Host identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) + if it's sent in absolute-form + - Host identifier of the `Host` header + + SHOULD NOT be set if capturing it would require an extra DNS lookup. """ - FAAS_DOCUMENT_TIME = "faas.document.time" + SERVER_PORT = "server.port" """ - A string containing the time when the data was accessed in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). + Port identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to. + Note: When [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) is absolute URI, `server.port` MUST match URI port identifier, otherwise it MUST match `Host` header port identifier. """ - FAAS_DOCUMENT_NAME = "faas.document.name" + HTTP_ROUTE = "http.route" """ - The document name/table subjected to the operation. For example, in Cloud Storage or S3 is the name of the file, and in Cosmos DB the table name. + The matched route (path template in the format used by the respective server framework). See note below. + Note: MUST NOT be populated when this is not supported by the HTTP server framework as the route attribute should have low-cardinality and the URI path can NOT substitute it. + SHOULD include the [application root](/docs/http/http-spans.md#http-server-definitions) if there is one. """ - HTTP_METHOD = "http.method" + URL_SCHEME = "url.scheme" """ - HTTP request method. + The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. """ - HTTP_URL = "http.url" + EVENT_NAME = "event.name" """ - Full HTTP request URL in the form `scheme://host[:port]/path?query[#fragment]`. Usually the fragment is not transmitted over HTTP, but if it is known, it should be included nevertheless. - Note: `http.url` MUST NOT contain credentials passed via URL in form of `https://username:password@www.example.com/`. In such case the attribute's value should be `https://www.example.com/`. + The name identifies the event. """ - HTTP_TARGET = "http.target" + EVENT_DOMAIN = "event.domain" """ - The full request target as passed in a HTTP request line or equivalent. + The domain identifies the business context for the events. + Note: Events across different domains may have same `event.name`, yet be + unrelated events. """ - HTTP_HOST = "http.host" + LOG_RECORD_UID = "log.record.uid" """ - The value of the [HTTP host header](https://tools.ietf.org/html/rfc7230#section-5.4). An empty Host header should also be reported, see note. - Note: When the header is present but empty the attribute SHOULD be set to the empty string. Note that this is a valid situation that is expected in certain cases, according the aforementioned [section of RFC 7230](https://tools.ietf.org/html/rfc7230#section-5.4). When the header is not set the attribute MUST NOT be set. + A unique identifier for the Log Record. + Note: If an id is provided, other log records with the same id will be considered duplicates and can be removed safely. This means, that two distinguishable log records MUST have different values. + The id MAY be an [Universally Unique Lexicographically Sortable Identifier (ULID)](https://github.com/ulid/spec), but other identifiers (e.g. UUID) may be used as needed. """ - HTTP_SCHEME = "http.scheme" + FEATURE_FLAG_KEY = "feature_flag.key" """ - The URI scheme identifying the used protocol. + The unique identifier of the feature flag. """ - HTTP_STATUS_CODE = "http.status_code" + FEATURE_FLAG_PROVIDER_NAME = "feature_flag.provider_name" """ - [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). + The name of the service provider that performs the flag evaluation. """ - HTTP_FLAVOR = "http.flavor" + FEATURE_FLAG_VARIANT = "feature_flag.variant" """ - Kind of HTTP protocol used. - Note: If `net.transport` is not specified, it can be assumed to be `IP.TCP` except if `http.flavor` is `QUIC`, in which case `IP.UDP` is assumed. + SHOULD be a semantic identifier for a value. If one is unavailable, a stringified version of the value can be used. + Note: A semantic identifier, commonly referred to as a variant, provides a means + for referring to a value without including the value itself. This can + provide additional context for understanding the meaning behind a value. + For example, the variant `red` maybe be used for the value `#c05543`. + + A stringified version of the value can be used in situations where a + semantic identifier is unavailable. String representation of the value + should be determined by the implementer. """ - HTTP_USER_AGENT = "http.user_agent" + LOG_IOSTREAM = "log.iostream" """ - Value of the [HTTP User-Agent](https://tools.ietf.org/html/rfc7231#section-5.5.3) header sent by the client. + The stream associated with the log. See below for a list of well-known values. """ - HTTP_REQUEST_CONTENT_LENGTH = "http.request_content_length" + LOG_FILE_NAME = "log.file.name" """ - The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://tools.ietf.org/html/rfc7230#section-3.3.2) header. For requests using transport encoding, this should be the compressed size. + The basename of the file. """ - HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED = ( - "http.request_content_length_uncompressed" - ) + LOG_FILE_PATH = "log.file.path" """ - The size of the uncompressed request payload body after transport decoding. Not set if transport encoding not used. + The full path to the file. """ - HTTP_RESPONSE_CONTENT_LENGTH = "http.response_content_length" + LOG_FILE_NAME_RESOLVED = "log.file.name_resolved" """ - The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://tools.ietf.org/html/rfc7230#section-3.3.2) header. For requests using transport encoding, this should be the compressed size. + The basename of the file, with symlinks resolved. """ - HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED = ( - "http.response_content_length_uncompressed" - ) + LOG_FILE_PATH_RESOLVED = "log.file.path_resolved" """ - The size of the uncompressed response payload body after transport decoding. Not set if transport encoding not used. + The full path to the file, with symlinks resolved. """ - HTTP_RETRY_COUNT = "http.retry_count" + SERVER_SOCKET_ADDRESS = "server.socket.address" """ - The ordinal number of request re-sending attempt. + Physical server IP address or Unix socket address. If set from the client, should simply use the socket's peer address, and not attempt to find any actual server IP (i.e., if set from client, this may represent some proxy server instead of the logical server). """ - HTTP_SERVER_NAME = "http.server_name" + POOL = "pool" """ - The primary server name of the matched virtual host. This should be obtained via configuration. If no such configuration can be obtained, this attribute MUST NOT be set ( `net.host.name` should be used instead). - Note: `http.url` is usually not readily available on the server side but would have to be assembled in a cumbersome and sometimes lossy process from other information (see e.g. open-telemetry/opentelemetry-python/pull/148). It is thus preferred to supply the raw data that is available. + Name of the buffer pool. + Note: Pool names are generally obtained via [BufferPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/BufferPoolMXBean.html#getName()). """ - HTTP_ROUTE = "http.route" + TYPE = "type" """ - The matched route (path template). + The type of memory. """ - HTTP_CLIENT_IP = "http.client_ip" + SERVER_SOCKET_DOMAIN = "server.socket.domain" + """ + The domain name of an immediate peer. + Note: Typically observed from the client side, and represents a proxy or other intermediary domain name. """ - The IP address of the original client behind all proxies, if known (e.g. from [X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)). - Note: This is not necessarily the same as `net.peer.ip`, which would -identify the network-level peer, which may be a proxy. -This attribute should be set when a source of information different -from the one used for `net.peer.ip`, is available even if that other -source just confirms the same value as `net.peer.ip`. -Rationale: For `net.peer.ip`, one typically does not know if it -comes from a proxy, reverse proxy, or the actual client. Setting -`http.client_ip` when it's the same as `net.peer.ip` means that -one is at least somewhat confident that the address is not that of -the closest proxy. + SERVER_SOCKET_PORT = "server.socket.port" + """ + Physical server port. """ - NET_HOST_IP = "net.host.ip" + SOURCE_DOMAIN = "source.domain" """ - Like `net.peer.ip` but for the host IP. Useful in case of a multi-IP host. + The domain name of the source system. + Note: This value may be a host name, a fully qualified domain name, or another host naming format. """ - NET_HOST_PORT = "net.host.port" + SOURCE_ADDRESS = "source.address" """ - Like `net.peer.port` but for the host port. + Source address, for example IP address or Unix socket name. """ - NET_HOST_NAME = "net.host.name" + SOURCE_PORT = "source.port" """ - Local hostname or similar, see note below. + Source port number. """ - NET_HOST_CONNECTION_TYPE = "net.host.connection.type" + AWS_LAMBDA_INVOKED_ARN = "aws.lambda.invoked_arn" """ - The internet connection type currently being used by the host. + The full invoked ARN as provided on the `Context` passed to the function (`Lambda-Runtime-Invoked-Function-Arn` header on the `/runtime/invocation/next` applicable). + Note: This may be different from `cloud.resource_id` if an alias is involved. """ - NET_HOST_CONNECTION_SUBTYPE = "net.host.connection.subtype" + CLOUDEVENTS_EVENT_ID = "cloudevents.event_id" """ - This describes more details regarding the connection.type. It may be the type of cell technology connection, but it could be used for describing details about a wifi connection. + The [event_id](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#id) uniquely identifies the event. """ - NET_HOST_CARRIER_NAME = "net.host.carrier.name" + CLOUDEVENTS_EVENT_SOURCE = "cloudevents.event_source" """ - The name of the mobile carrier. + The [source](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#source-1) identifies the context in which an event happened. """ - NET_HOST_CARRIER_MCC = "net.host.carrier.mcc" + CLOUDEVENTS_EVENT_SPEC_VERSION = "cloudevents.event_spec_version" """ - The mobile carrier country code. + The [version of the CloudEvents specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#specversion) which the event uses. """ - NET_HOST_CARRIER_MNC = "net.host.carrier.mnc" + CLOUDEVENTS_EVENT_TYPE = "cloudevents.event_type" """ - The mobile carrier network code. + The [event_type](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#type) contains a value describing the type of event related to the originating occurrence. """ - NET_HOST_CARRIER_ICC = "net.host.carrier.icc" + CLOUDEVENTS_EVENT_SUBJECT = "cloudevents.event_subject" """ - The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network. + The [subject](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#subject) of the event in the context of the event producer (identified by source). """ - MESSAGING_SYSTEM = "messaging.system" + OPENTRACING_REF_TYPE = "opentracing.ref_type" """ - A string identifying the messaging system. + Parent-child Reference type. + Note: The causal relationship between a child Span and a parent Span. """ - MESSAGING_DESTINATION = "messaging.destination" + DB_SYSTEM = "db.system" """ - The message destination name. This might be equal to the span name but is required nevertheless. + An identifier for the database management system (DBMS) product being used. See below for a list of well-known identifiers. """ - MESSAGING_DESTINATION_KIND = "messaging.destination_kind" + DB_CONNECTION_STRING = "db.connection_string" """ - The kind of message destination. + The connection string used to connect to the database. It is recommended to remove embedded credentials. """ - MESSAGING_TEMP_DESTINATION = "messaging.temp_destination" + DB_USER = "db.user" """ - A boolean that is true if the message destination is temporary. + Username for accessing the database. """ - MESSAGING_PROTOCOL = "messaging.protocol" + DB_JDBC_DRIVER_CLASSNAME = "db.jdbc.driver_classname" """ - The name of the transport protocol. + The fully-qualified class name of the [Java Database Connectivity (JDBC)](https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/) driver used to connect. """ - MESSAGING_PROTOCOL_VERSION = "messaging.protocol_version" + DB_NAME = "db.name" """ - The version of the transport protocol. + This attribute is used to report the name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). + Note: In some SQL databases, the database name to be used is called "schema name". In case there are multiple layers that could be considered for database name (e.g. Oracle instance name and schema name), the database name to be used is the more specific layer (e.g. Oracle schema name). """ - MESSAGING_URL = "messaging.url" + DB_STATEMENT = "db.statement" """ - Connection string. + The database statement being executed. """ - MESSAGING_MESSAGE_ID = "messaging.message_id" + DB_OPERATION = "db.operation" """ - A value used by the messaging system as an identifier for the message, represented as a string. + The name of the operation being executed, e.g. the [MongoDB command name](https://docs.mongodb.com/manual/reference/command/#database-operations) such as `findAndModify`, or the SQL keyword. + Note: When setting this to an SQL keyword, it is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if the operation name is provided by the library being instrumented. If the SQL statement has an ambiguous operation, or performs more than one operation, this value may be omitted. """ - MESSAGING_CONVERSATION_ID = "messaging.conversation_id" + NETWORK_TRANSPORT = "network.transport" """ - The [conversation ID](#conversations) identifying the conversation to which the message belongs, represented as a string. Sometimes called "Correlation ID". + [OSI Transport Layer](https://osi-model.com/transport-layer/) or [Inter-process Communication method](https://en.wikipedia.org/wiki/Inter-process_communication). The value SHOULD be normalized to lowercase. """ - MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES = ( - "messaging.message_payload_size_bytes" - ) + NETWORK_TYPE = "network.type" """ - The (uncompressed) size of the message payload in bytes. Also use this attribute if it is unknown whether the compressed or uncompressed payload size is reported. + [OSI Network Layer](https://osi-model.com/network-layer/) or non-OSI equivalent. The value SHOULD be normalized to lowercase. """ - MESSAGING_MESSAGE_PAYLOAD_COMPRESSED_SIZE_BYTES = ( - "messaging.message_payload_compressed_size_bytes" - ) + DB_MSSQL_INSTANCE_NAME = "db.mssql.instance_name" """ - The compressed size of the message payload in bytes. + The Microsoft SQL Server [instance name](https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15) connecting to. This name is used to determine the port of a named instance. + Note: If setting a `db.mssql.instance_name`, `server.port` is no longer required (but still recommended if non-standard). """ - FAAS_TIME = "faas.time" + DB_CASSANDRA_PAGE_SIZE = "db.cassandra.page_size" """ - A string containing the function invocation time in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). + The fetch size used for paging, i.e. how many rows will be returned at once. """ - FAAS_CRON = "faas.cron" + DB_CASSANDRA_CONSISTENCY_LEVEL = "db.cassandra.consistency_level" """ - A string containing the schedule period as [Cron Expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm). + The consistency level of the query. Based on consistency values from [CQL](https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/dml/dmlConfigConsistency.html). """ - FAAS_COLDSTART = "faas.coldstart" + DB_CASSANDRA_TABLE = "db.cassandra.table" """ - A boolean that is true if the serverless function is executed for the first time (aka cold-start). + The name of the primary table that the operation is acting upon, including the keyspace name (if applicable). + Note: This mirrors the db.sql.table attribute but references cassandra rather than sql. It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. """ - FAAS_INVOKED_NAME = "faas.invoked_name" + DB_CASSANDRA_IDEMPOTENCE = "db.cassandra.idempotence" """ - The name of the invoked function. - Note: SHOULD be equal to the `faas.name` resource attribute of the invoked function. + Whether or not the query is idempotent. """ - FAAS_INVOKED_PROVIDER = "faas.invoked_provider" + DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT = ( + "db.cassandra.speculative_execution_count" + ) """ - The cloud provider of the invoked function. - Note: SHOULD be equal to the `cloud.provider` resource attribute of the invoked function. + The number of times a query was speculatively executed. Not set or `0` if the query was not executed speculatively. """ - FAAS_INVOKED_REGION = "faas.invoked_region" + DB_CASSANDRA_COORDINATOR_ID = "db.cassandra.coordinator.id" """ - The cloud region of the invoked function. - Note: SHOULD be equal to the `cloud.region` resource attribute of the invoked function. + The ID of the coordinating node for a query. """ - PEER_SERVICE = "peer.service" + DB_CASSANDRA_COORDINATOR_DC = "db.cassandra.coordinator.dc" """ - The [`service.name`](../../resource/semantic_conventions/README.md#service) of the remote service. SHOULD be equal to the actual `service.name` resource attribute of the remote service if any. + The data center of the coordinating node for a query. """ - ENDUSER_ID = "enduser.id" + DB_REDIS_DATABASE_INDEX = "db.redis.database_index" """ - Username or client_id extracted from the access token or [Authorization](https://tools.ietf.org/html/rfc7235#section-4.2) header in the inbound request from outside the system. + The index of the database being accessed as used in the [`SELECT` command](https://redis.io/commands/select), provided as an integer. To be used instead of the generic `db.name` attribute. """ - ENDUSER_ROLE = "enduser.role" + DB_MONGODB_COLLECTION = "db.mongodb.collection" """ - Actual/assumed role the client is making the request under extracted from token or application security context. + The collection being accessed within the database stated in `db.name`. """ - ENDUSER_SCOPE = "enduser.scope" + URL_FULL = "url.full" """ - Scopes or granted authorities the client currently possesses extracted from token or application security context. The value would come from the scope associated with an [OAuth 2.0 Access Token](https://tools.ietf.org/html/rfc6749#section-3.3) or an attribute value in a [SAML 2.0 Assertion](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html). + Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986). + Note: For network calls, URL usually has `scheme://host[:port][path][?query][#fragment]` format, where the fragment is not transmitted over HTTP, but if it is known, it should be included nevertheless. + `url.full` MUST NOT contain credentials passed via URL in form of `https://username:password@www.example.com/`. In such case username and password should be redacted and attribute's value should be `https://REDACTED:REDACTED@www.example.com/`. + `url.full` SHOULD capture the absolute URL when it is available (or can be reconstructed) and SHOULD NOT be validated or modified except for sanitizing purposes. """ - THREAD_ID = "thread.id" + DB_SQL_TABLE = "db.sql.table" """ - Current "managed" thread ID (as opposed to OS thread ID). + The name of the primary table that the operation is acting upon, including the database name (if applicable). + Note: It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. """ - THREAD_NAME = "thread.name" + DB_COSMOSDB_CLIENT_ID = "db.cosmosdb.client_id" """ - Current thread name. + Unique Cosmos client instance id. """ - CODE_FUNCTION = "code.function" + DB_COSMOSDB_OPERATION_TYPE = "db.cosmosdb.operation_type" """ - The method or function name, or equivalent (usually rightmost part of the code unit's name). + CosmosDB Operation Type. """ - CODE_NAMESPACE = "code.namespace" + USER_AGENT_ORIGINAL = "user_agent.original" """ - The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. + Full user-agent string is generated by Cosmos DB SDK. + Note: The user-agent value is generated by SDK which is a combination of
    `sdk_version` : Current version of SDK. e.g. 'cosmos-netstandard-sdk/3.23.0'
    `direct_pkg_version` : Direct package version used by Cosmos DB SDK. e.g. '3.23.1'
    `number_of_client_instances` : Number of cosmos client instances created by the application. e.g. '1'
    `type_of_machine_architecture` : Machine architecture. e.g. 'X64'
    `operating_system` : Operating System. e.g. 'Linux 5.4.0-1098-azure 104 18'
    `runtime_framework` : Runtime Framework. e.g. '.NET Core 3.1.32'
    `failover_information` : Generated key to determine if region failover enabled. + Format Reg-{D (Disabled discovery)}-S(application region)|L(List of preferred regions)|N(None, user did not configure it). + Default value is "NS". """ - CODE_FILEPATH = "code.filepath" + DB_COSMOSDB_CONNECTION_MODE = "db.cosmosdb.connection_mode" """ - The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). + Cosmos client connection mode. """ - CODE_LINENO = "code.lineno" + DB_COSMOSDB_CONTAINER = "db.cosmosdb.container" """ - The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. + Cosmos DB container name. """ - RPC_SYSTEM = "rpc.system" + DB_COSMOSDB_REQUEST_CONTENT_LENGTH = "db.cosmosdb.request_content_length" """ - The value `aws-api`. + Request payload size in bytes. """ - RPC_SERVICE = "rpc.service" + DB_COSMOSDB_STATUS_CODE = "db.cosmosdb.status_code" """ - The name of the service to which a request is made, as returned by the AWS SDK. - Note: This is the logical name of the service from the RPC interface perspective, which can be different from the name of any implementing class. The `code.namespace` attribute may be used to store the latter (despite the attribute name, it may include a class name; e.g., class with method actually executing the call on the server side, RPC client stub class on the client side). + Cosmos DB status code. """ - RPC_METHOD = "rpc.method" + DB_COSMOSDB_SUB_STATUS_CODE = "db.cosmosdb.sub_status_code" """ - The name of the operation corresponding to the request, as returned by the AWS SDK. - Note: This is the logical name of the method from the RPC interface perspective, which can be different from the name of any implementing method/function. The `code.function` attribute may be used to store the latter (e.g., method actually executing the call on the server side, RPC client stub method on the client side). + Cosmos DB sub status code. """ - AWS_DYNAMODB_TABLE_NAMES = "aws.dynamodb.table_names" + DB_COSMOSDB_REQUEST_CHARGE = "db.cosmosdb.request_charge" """ - The keys in the `RequestItems` object field. + RU consumed for that operation. """ - AWS_DYNAMODB_CONSUMED_CAPACITY = "aws.dynamodb.consumed_capacity" + OTEL_STATUS_CODE = "otel.status_code" """ - The JSON-serialized value of each item in the `ConsumedCapacity` response field. + Name of the code, either "OK" or "ERROR". MUST NOT be set if the status code is UNSET. + """ + + OTEL_STATUS_DESCRIPTION = "otel.status_description" + """ + Description of the Status if it has a value, otherwise not set. + """ + + FAAS_TRIGGER = "faas.trigger" + """ + Type of the trigger which caused this function invocation. + Note: For the server/consumer span on the incoming side, + `faas.trigger` MUST be set. + + Clients invoking FaaS instances usually cannot set `faas.trigger`, + since they would typically need to look in the payload to determine + the event type. If clients set it, it should be the same as the + trigger that corresponding incoming would have (i.e., this has + nothing to do with the underlying transport used to make the API + call to invoke the lambda, which is often HTTP). + """ + + FAAS_INVOCATION_ID = "faas.invocation_id" + """ + The invocation ID of the current function invocation. + """ + + CLOUD_RESOURCE_ID = "cloud.resource_id" + """ + Cloud provider-specific native identifier of the monitored cloud resource (e.g. an [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) on AWS, a [fully qualified resource ID](https://learn.microsoft.com/en-us/rest/api/resources/resources/get-by-id) on Azure, a [full resource name](https://cloud.google.com/apis/design/resource_names#full_resource_name) on GCP). + Note: On some cloud providers, it may not be possible to determine the full ID at startup, + so it may be necessary to set `cloud.resource_id` as a span attribute instead. + + The exact value to use for `cloud.resource_id` depends on the cloud provider. + The following well-known definitions MUST be used if you set this attribute and they apply: + + * **AWS Lambda:** The function [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html). + Take care not to use the "invoked ARN" directly but replace any + [alias suffix](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html) + with the resolved function version, as the same runtime instance may be invokable with + multiple different aliases. + * **GCP:** The [URI of the resource](https://cloud.google.com/iam/docs/full-resource-names) + * **Azure:** The [Fully Qualified Resource ID](https://docs.microsoft.com/en-us/rest/api/resources/resources/get-by-id) of the invoked function, + *not* the function app, having the form + `/subscriptions//resourceGroups//providers/Microsoft.Web/sites//functions/`. + This means that a span attribute MUST be used, as an Azure function app can host multiple functions that would usually share + a TracerProvider. + """ + + FAAS_DOCUMENT_COLLECTION = "faas.document.collection" + """ + The name of the source on which the triggering operation was performed. For example, in Cloud Storage or S3 corresponds to the bucket name, and in Cosmos DB to the database name. + """ + + FAAS_DOCUMENT_OPERATION = "faas.document.operation" + """ + Describes the type of the operation that was performed on the data. + """ + + FAAS_DOCUMENT_TIME = "faas.document.time" + """ + A string containing the time when the data was accessed in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). + """ + + FAAS_DOCUMENT_NAME = "faas.document.name" + """ + The document name/table subjected to the operation. For example, in Cloud Storage or S3 is the name of the file, and in Cosmos DB the table name. + """ + + URL_PATH = "url.path" + """ + The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component. + Note: When missing, the value is assumed to be `/`. + """ + + URL_QUERY = "url.query" + """ + The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component. + Note: Sensitive content provided in query string SHOULD be scrubbed when instrumentations can identify it. + """ + + MESSAGING_SYSTEM = "messaging.system" + """ + A string identifying the messaging system. + """ + + MESSAGING_OPERATION = "messaging.operation" + """ + A string identifying the kind of messaging operation as defined in the [Operation names](#operation-names) section above. + Note: If a custom value is used, it MUST be of low cardinality. + """ + + MESSAGING_BATCH_MESSAGE_COUNT = "messaging.batch.message_count" + """ + The number of messages sent, received, or processed in the scope of the batching operation. + Note: Instrumentations SHOULD NOT set `messaging.batch.message_count` on spans that operate with a single message. When a messaging client library supports both batch and single-message API for the same operation, instrumentations SHOULD use `messaging.batch.message_count` for batching APIs and SHOULD NOT use it for single-message APIs. + """ + + MESSAGING_CLIENT_ID = "messaging.client_id" + """ + A unique identifier for the client that consumes or produces a message. + """ + + MESSAGING_DESTINATION_NAME = "messaging.destination.name" + """ + The message destination name. + Note: Destination name SHOULD uniquely identify a specific queue, topic or other entity within the broker. If + the broker does not have such notion, the destination name SHOULD uniquely identify the broker. + """ + + MESSAGING_DESTINATION_TEMPLATE = "messaging.destination.template" + """ + Low cardinality representation of the messaging destination name. + Note: Destination names could be constructed from templates. An example would be a destination name involving a user name or product id. Although the destination name in this case is of high cardinality, the underlying template is of low cardinality and can be effectively used for grouping and aggregation. + """ + + MESSAGING_DESTINATION_TEMPORARY = "messaging.destination.temporary" + """ + A boolean that is true if the message destination is temporary and might not exist anymore after messages are processed. + """ + + MESSAGING_DESTINATION_ANONYMOUS = "messaging.destination.anonymous" + """ + A boolean that is true if the message destination is anonymous (could be unnamed or have auto-generated name). + """ + + MESSAGING_MESSAGE_ID = "messaging.message.id" + """ + A value used by the messaging system as an identifier for the message, represented as a string. + """ + + MESSAGING_MESSAGE_CONVERSATION_ID = "messaging.message.conversation_id" + """ + The [conversation ID](#conversations) identifying the conversation to which the message belongs, represented as a string. Sometimes called "Correlation ID". + """ + + MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES = ( + "messaging.message.payload_size_bytes" + ) + """ + The (uncompressed) size of the message payload in bytes. Also use this attribute if it is unknown whether the compressed or uncompressed payload size is reported. + """ + + MESSAGING_MESSAGE_PAYLOAD_COMPRESSED_SIZE_BYTES = ( + "messaging.message.payload_compressed_size_bytes" + ) + """ + The compressed size of the message payload in bytes. + """ + + FAAS_TIME = "faas.time" + """ + A string containing the function invocation time in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). + """ + + FAAS_CRON = "faas.cron" + """ + A string containing the schedule period as [Cron Expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm). + """ + + FAAS_COLDSTART = "faas.coldstart" + """ + A boolean that is true if the serverless function is executed for the first time (aka cold-start). + """ + + FAAS_INVOKED_NAME = "faas.invoked_name" + """ + The name of the invoked function. + Note: SHOULD be equal to the `faas.name` resource attribute of the invoked function. + """ + + FAAS_INVOKED_PROVIDER = "faas.invoked_provider" + """ + The cloud provider of the invoked function. + Note: SHOULD be equal to the `cloud.provider` resource attribute of the invoked function. + """ + + FAAS_INVOKED_REGION = "faas.invoked_region" + """ + The cloud region of the invoked function. + Note: SHOULD be equal to the `cloud.region` resource attribute of the invoked function. + """ + + NETWORK_CONNECTION_TYPE = "network.connection.type" + """ + The internet connection type. + """ + + NETWORK_CONNECTION_SUBTYPE = "network.connection.subtype" + """ + This describes more details regarding the connection.type. It may be the type of cell technology connection, but it could be used for describing details about a wifi connection. + """ + + NETWORK_CARRIER_NAME = "network.carrier.name" + """ + The name of the mobile carrier. + """ + + NETWORK_CARRIER_MCC = "network.carrier.mcc" + """ + The mobile carrier country code. + """ + + NETWORK_CARRIER_MNC = "network.carrier.mnc" + """ + The mobile carrier network code. + """ + + NETWORK_CARRIER_ICC = "network.carrier.icc" + """ + The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network. + """ + + PEER_SERVICE = "peer.service" + """ + The [`service.name`](/docs/resource/README.md#service) of the remote service. SHOULD be equal to the actual `service.name` resource attribute of the remote service if any. + """ + + ENDUSER_ID = "enduser.id" + """ + Username or client_id extracted from the access token or [Authorization](https://tools.ietf.org/html/rfc7235#section-4.2) header in the inbound request from outside the system. + """ + + ENDUSER_ROLE = "enduser.role" + """ + Actual/assumed role the client is making the request under extracted from token or application security context. + """ + + ENDUSER_SCOPE = "enduser.scope" + """ + Scopes or granted authorities the client currently possesses extracted from token or application security context. The value would come from the scope associated with an [OAuth 2.0 Access Token](https://tools.ietf.org/html/rfc6749#section-3.3) or an attribute value in a [SAML 2.0 Assertion](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html). + """ + + THREAD_ID = "thread.id" + """ + Current "managed" thread ID (as opposed to OS thread ID). + """ + + THREAD_NAME = "thread.name" + """ + Current thread name. + """ + + CODE_FUNCTION = "code.function" + """ + The method or function name, or equivalent (usually rightmost part of the code unit's name). + """ + + CODE_NAMESPACE = "code.namespace" + """ + The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. + """ + + CODE_FILEPATH = "code.filepath" + """ + The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). + """ + + CODE_LINENO = "code.lineno" + """ + The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. + """ + + CODE_COLUMN = "code.column" + """ + The column number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. + """ + + HTTP_REQUEST_METHOD_ORIGINAL = "http.request.method_original" + """ + Original HTTP method sent by the client in the request line. + """ + + HTTP_REQUEST_BODY_SIZE = "http.request.body.size" + """ + The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. + """ + + HTTP_RESPONSE_BODY_SIZE = "http.response.body.size" + """ + The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. + """ + + HTTP_RESEND_COUNT = "http.resend_count" + """ + The ordinal number of request resending attempt (for any reason, including redirects). + Note: The resend count SHOULD be updated each time an HTTP request gets resent by the client, regardless of what was the cause of the resending (e.g. redirection, authorization failure, 503 Server Unavailable, network issues, or any other). + """ + + RPC_SYSTEM = "rpc.system" + """ + The value `aws-api`. + """ + + RPC_SERVICE = "rpc.service" + """ + The name of the service to which a request is made, as returned by the AWS SDK. + Note: This is the logical name of the service from the RPC interface perspective, which can be different from the name of any implementing class. The `code.namespace` attribute may be used to store the latter (despite the attribute name, it may include a class name; e.g., class with method actually executing the call on the server side, RPC client stub class on the client side). + """ + + RPC_METHOD = "rpc.method" + """ + The name of the operation corresponding to the request, as returned by the AWS SDK. + Note: This is the logical name of the method from the RPC interface perspective, which can be different from the name of any implementing method/function. The `code.function` attribute may be used to store the latter (e.g., method actually executing the call on the server side, RPC client stub method on the client side). + """ + + AWS_REQUEST_ID = "aws.request_id" + """ + The AWS request ID as returned in the response headers `x-amz-request-id` or `x-amz-requestid`. + """ + + AWS_DYNAMODB_TABLE_NAMES = "aws.dynamodb.table_names" + """ + The keys in the `RequestItems` object field. + """ + + AWS_DYNAMODB_CONSUMED_CAPACITY = "aws.dynamodb.consumed_capacity" + """ + The JSON-serialized value of each item in the `ConsumedCapacity` response field. """ AWS_DYNAMODB_ITEM_COLLECTION_METRICS = ( @@ -632,171 +946,664 @@ class SpanAttributes: The value of the `ExclusiveStartTableName` request parameter. """ - AWS_DYNAMODB_TABLE_COUNT = "aws.dynamodb.table_count" - """ - The the number of items in the `TableNames` response parameter. - """ + AWS_DYNAMODB_TABLE_COUNT = "aws.dynamodb.table_count" + """ + The the number of items in the `TableNames` response parameter. + """ + + AWS_DYNAMODB_SCAN_FORWARD = "aws.dynamodb.scan_forward" + """ + The value of the `ScanIndexForward` request parameter. + """ + + AWS_DYNAMODB_SEGMENT = "aws.dynamodb.segment" + """ + The value of the `Segment` request parameter. + """ + + AWS_DYNAMODB_TOTAL_SEGMENTS = "aws.dynamodb.total_segments" + """ + The value of the `TotalSegments` request parameter. + """ + + AWS_DYNAMODB_COUNT = "aws.dynamodb.count" + """ + The value of the `Count` response parameter. + """ + + AWS_DYNAMODB_SCANNED_COUNT = "aws.dynamodb.scanned_count" + """ + The value of the `ScannedCount` response parameter. + """ + + AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS = "aws.dynamodb.attribute_definitions" + """ + The JSON-serialized value of each item in the `AttributeDefinitions` request field. + """ + + AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES = ( + "aws.dynamodb.global_secondary_index_updates" + ) + """ + The JSON-serialized value of each item in the the `GlobalSecondaryIndexUpdates` request field. + """ + + AWS_S3_BUCKET = "aws.s3.bucket" + """ + The S3 bucket name the request refers to. Corresponds to the `--bucket` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) operations. + Note: The `bucket` attribute is applicable to all S3 operations that reference a bucket, i.e. that require the bucket name as a mandatory parameter. + This applies to almost all S3 operations except `list-buckets`. + """ + + AWS_S3_KEY = "aws.s3.key" + """ + The S3 object key the request refers to. Corresponds to the `--key` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) operations. + Note: The `key` attribute is applicable to all object-related S3 operations, i.e. that require the object key as a mandatory parameter. + This applies in particular to the following operations: + + - [copy-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html) + - [delete-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-object.html) + - [get-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/get-object.html) + - [head-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/head-object.html) + - [put-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-object.html) + - [restore-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/restore-object.html) + - [select-object-content](https://docs.aws.amazon.com/cli/latest/reference/s3api/select-object-content.html) + - [abort-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/abort-multipart-upload.html) + - [complete-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/complete-multipart-upload.html) + - [create-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/create-multipart-upload.html) + - [list-parts](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-parts.html) + - [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) + - [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). + """ + + AWS_S3_COPY_SOURCE = "aws.s3.copy_source" + """ + The source object (in the form `bucket`/`key`) for the copy operation. + Note: The `copy_source` attribute applies to S3 copy operations and corresponds to the `--copy-source` parameter + of the [copy-object operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html). + This applies in particular to the following operations: + + - [copy-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html) + - [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). + """ + + AWS_S3_UPLOAD_ID = "aws.s3.upload_id" + """ + Upload ID that identifies the multipart upload. + Note: The `upload_id` attribute applies to S3 multipart-upload operations and corresponds to the `--upload-id` parameter + of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) multipart operations. + This applies in particular to the following operations: + + - [abort-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/abort-multipart-upload.html) + - [complete-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/complete-multipart-upload.html) + - [list-parts](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-parts.html) + - [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) + - [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). + """ + + AWS_S3_DELETE = "aws.s3.delete" + """ + The delete request container that specifies the objects to be deleted. + Note: The `delete` attribute is only applicable to the [delete-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-object.html) operation. + The `delete` attribute corresponds to the `--delete` parameter of the + [delete-objects operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-objects.html). + """ + + AWS_S3_PART_NUMBER = "aws.s3.part_number" + """ + The part number of the part being uploaded in a multipart-upload operation. This is a positive integer between 1 and 10,000. + Note: The `part_number` attribute is only applicable to the [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) + and [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html) operations. + The `part_number` attribute corresponds to the `--part-number` parameter of the + [upload-part operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html). + """ + + GRAPHQL_OPERATION_NAME = "graphql.operation.name" + """ + The name of the operation being executed. + """ + + GRAPHQL_OPERATION_TYPE = "graphql.operation.type" + """ + The type of the operation being executed. + """ + + GRAPHQL_DOCUMENT = "graphql.document" + """ + The GraphQL document being executed. + Note: The value may be sanitized to exclude sensitive information. + """ + + MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY = ( + "messaging.rabbitmq.destination.routing_key" + ) + """ + RabbitMQ message routing key. + """ + + MESSAGING_KAFKA_MESSAGE_KEY = "messaging.kafka.message.key" + """ + Message keys in Kafka are used for grouping alike messages to ensure they're processed on the same partition. They differ from `messaging.message.id` in that they're not unique. If the key is `null`, the attribute MUST NOT be set. + Note: If the key type is not string, it's string representation has to be supplied for the attribute. If the key has no unambiguous, canonical string form, don't include its value. + """ + + MESSAGING_KAFKA_CONSUMER_GROUP = "messaging.kafka.consumer.group" + """ + Name of the Kafka Consumer Group that is handling the message. Only applies to consumers, not producers. + """ + + MESSAGING_KAFKA_DESTINATION_PARTITION = ( + "messaging.kafka.destination.partition" + ) + """ + Partition the message is sent to. + """ + + MESSAGING_KAFKA_MESSAGE_OFFSET = "messaging.kafka.message.offset" + """ + The offset of a record in the corresponding Kafka partition. + """ + + MESSAGING_KAFKA_MESSAGE_TOMBSTONE = "messaging.kafka.message.tombstone" + """ + A boolean that is true if the message is a tombstone. + """ + + MESSAGING_ROCKETMQ_NAMESPACE = "messaging.rocketmq.namespace" + """ + Namespace of RocketMQ resources, resources in different namespaces are individual. + """ + + MESSAGING_ROCKETMQ_CLIENT_GROUP = "messaging.rocketmq.client_group" + """ + Name of the RocketMQ producer/consumer group that is handling the message. The client type is identified by the SpanKind. + """ + + MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP = ( + "messaging.rocketmq.message.delivery_timestamp" + ) + """ + The timestamp in milliseconds that the delay message is expected to be delivered to consumer. + """ + + MESSAGING_ROCKETMQ_MESSAGE_DELAY_TIME_LEVEL = ( + "messaging.rocketmq.message.delay_time_level" + ) + """ + The delay time level for delay message, which determines the message delay time. + """ + + MESSAGING_ROCKETMQ_MESSAGE_GROUP = "messaging.rocketmq.message.group" + """ + It is essential for FIFO message. Messages that belong to the same message group are always processed one by one within the same consumer group. + """ + + MESSAGING_ROCKETMQ_MESSAGE_TYPE = "messaging.rocketmq.message.type" + """ + Type of message. + """ + + MESSAGING_ROCKETMQ_MESSAGE_TAG = "messaging.rocketmq.message.tag" + """ + The secondary classifier of message besides topic. + """ + + MESSAGING_ROCKETMQ_MESSAGE_KEYS = "messaging.rocketmq.message.keys" + """ + Key(s) of message, another way to mark message besides message id. + """ + + MESSAGING_ROCKETMQ_CONSUMPTION_MODEL = ( + "messaging.rocketmq.consumption_model" + ) + """ + Model of message consumption. This only applies to consumer spans. + """ + + RPC_GRPC_STATUS_CODE = "rpc.grpc.status_code" + """ + The [numeric status code](https://github.com/grpc/grpc/blob/v1.33.2/doc/statuscodes.md) of the gRPC request. + """ + + RPC_JSONRPC_VERSION = "rpc.jsonrpc.version" + """ + Protocol version as in `jsonrpc` property of request/response. Since JSON-RPC 1.0 does not specify this, the value can be omitted. + """ + + RPC_JSONRPC_REQUEST_ID = "rpc.jsonrpc.request_id" + """ + `id` property of request or response. Since protocol allows id to be int, string, `null` or missing (for notifications), value is expected to be cast to string for simplicity. Use empty string in case of `null` value. Omit entirely if this is a notification. + """ + + RPC_JSONRPC_ERROR_CODE = "rpc.jsonrpc.error_code" + """ + `error.code` property of response if it is an error response. + """ + + RPC_JSONRPC_ERROR_MESSAGE = "rpc.jsonrpc.error_message" + """ + `error.message` property of response if it is an error response. + """ + + MESSAGE_TYPE = "message.type" + """ + Whether this is a received or sent message. + """ + + MESSAGE_ID = "message.id" + """ + MUST be calculated as two different counters starting from `1` one for sent messages and one for received message. + Note: This way we guarantee that the values will be consistent between different implementations. + """ + + MESSAGE_COMPRESSED_SIZE = "message.compressed_size" + """ + Compressed size of the message in bytes. + """ + + MESSAGE_UNCOMPRESSED_SIZE = "message.uncompressed_size" + """ + Uncompressed size of the message in bytes. + """ + + RPC_CONNECT_RPC_ERROR_CODE = "rpc.connect_rpc.error_code" + """ + The [error codes](https://connect.build/docs/protocol/#error-codes) of the Connect request. Error codes are always string values. + """ + + EXCEPTION_ESCAPED = "exception.escaped" + """ + SHOULD be set to true if the exception event is recorded at a point where it is known that the exception is escaping the scope of the span. + Note: An exception is considered to have escaped (or left) the scope of a span, + if that span is ended while the exception is still logically "in flight". + This may be actually "in flight" in some languages (e.g. if the exception + is passed to a Context manager's `__exit__` method in Python) but will + usually be caught at the point of recording the exception in most languages. + + It is usually not possible to determine at the point where an exception is thrown + whether it will escape the scope of a span. + However, it is trivial to know that an exception + will escape, if one checks for an active exception just before ending the span, + as done in the [example above](#recording-an-exception). + + It follows that an exception may still escape the scope of the span + even if the `exception.escaped` attribute was not set or set to false, + since the event might have been recorded at a time where it was not + clear whether the exception will escape. + """ + + URL_FRAGMENT = "url.fragment" + """ + The [URI fragment](https://www.rfc-editor.org/rfc/rfc3986#section-3.5) component. + """ + + # Manually defined deprecated attributes + + NET_PEER_IP = "net.peer.ip" + """ + Deprecated, use the `client.socket.address` attribute. + """ + + NET_HOST_IP = "net.host.ip" + """ + Deprecated, use the `server.socket.address` attribute. + """ + + HTTP_SERVER_NAME = "http.server_name" + """ + Deprecated, use the `server.address` attribute. + """ + + HTTP_HOST = "http.host" + """ + Deprecated, use the `server.address` and `server.port` attributes. + """ + + HTTP_RETRY_COUNT = "http.retry_count" + """ + Deprecated, use the `http.resend_count` attribute. + """ + + HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED = ( + "http.request_content_length_uncompressed" + ) + """ + Deprecated, use the `http.request.body.size` attribute. + """ + + HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED = ( + "http.response_content_length_uncompressed" + ) + """ + Deprecated, use the `http.response.body.size` attribute. + """ + + MESSAGING_DESTINATION = "messaging.destination" + """ + Deprecated, use the `messaging.destination.name` attribute. + """ + + MESSAGING_DESTINATION_KIND = "messaging.destination_kind" + """ + Deprecated. + """ + + MESSAGING_TEMP_DESTINATION = "messaging.temp_destination" + """ + Deprecated. Use `messaging.destination.temporary` attribute. + """ + + MESSAGING_PROTOCOL = "messaging.protocol" + """ + Deprecated. Use `network.protocol.name` attribute. + """ + + MESSAGING_PROTOCOL_VERSION = "messaging.protocol_version" + """ + Deprecated. Use `network.protocol.version` attribute. + """ + + MESSAGING_URL = "messaging.url" + """ + Deprecated. Use `server.address` and `server.port` attributes. + """ + + MESSAGING_CONVERSATION_ID = "messaging.conversation_id" + """ + Deprecated. Use `messaging.message.conversation.id` attribute. + """ + + MESSAGING_KAFKA_PARTITION = "messaging.kafka.partition" + """ + Deprecated. Use `messaging.kafka.destination.partition` attribute. + """ + + FAAS_EXECUTION = "faas.execution" + """ + Deprecated. Use `faas.invocation_id` attribute. + """ + + HTTP_USER_AGENT = "http.user_agent" + """ + Deprecated. Use `user_agent.original` attribute. + """ + + MESSAGING_RABBITMQ_ROUTING_KEY = "messaging.rabbitmq.routing_key" + """ + Deprecated. Use `messaging.rabbitmq.destination.routing_key` attribute. + """ + + MESSAGING_KAFKA_TOMBSTONE = "messaging.kafka.tombstone" + """ + Deprecated. Use `messaging.kafka.destination.tombstone` attribute. + """ + + NET_APP_PROTOCOL_NAME = "net.app.protocol.name" + """ + Deprecated. Use `network.protocol.name` attribute. + """ + + NET_APP_PROTOCOL_VERSION = "net.app.protocol.version" + """ + Deprecated. Use `network.protocol.version` attribute. + """ + + HTTP_CLIENT_IP = "http.client_ip" + """ + Deprecated. Use `client.address` attribute. + """ + + HTTP_FLAVOR = "http.flavor" + """ + Deprecated. Use `network.protocol.name` and `network.protocol.version` attributes. + """ + + NET_HOST_CONNECTION_TYPE = "net.host.connection.type" + """ + Deprecated. Use `network.connection.type` attribute. + """ + + NET_HOST_CONNECTION_SUBTYPE = "net.host.connection.subtype" + """ + Deprecated. Use `network.connection.subtype` attribute. + """ + + NET_HOST_CARRIER_NAME = "net.host.carrier.name" + """ + Deprecated. Use `network.carrier.name` attribute. + """ + + NET_HOST_CARRIER_MCC = "net.host.carrier.mcc" + """ + Deprecated. Use `network.carrier.mcc` attribute. + """ + + NET_HOST_CARRIER_MNC = "net.host.carrier.mnc" + """ + Deprecated. Use `network.carrier.mnc` attribute. + """ + + MESSAGING_CONSUMER_ID = "messaging.consumer_id" + """ + Deprecated. Use `messaging.client_id` attribute. + """ + + MESSAGING_KAFKA_CLIENT_ID = "messaging.kafka.client_id" + """ + Deprecated. Use `messaging.client_id` attribute. + """ + + MESSAGING_ROCKETMQ_CLIENT_ID = "messaging.rocketmq.client_id" + """ + Deprecated. Use `messaging.client_id` attribute. + """ + + +@deprecated( + version="1.18.0", + reason="Removed from the specification in favor of `network.protocol.name` and `network.protocol.version` attributes", +) +class HttpFlavorValues(Enum): + HTTP_1_0 = "1.0" + + HTTP_1_1 = "1.1" + + HTTP_2_0 = "2.0" + + HTTP_3_0 = "3.0" + + SPDY = "SPDY" + + QUIC = "QUIC" + + +@deprecated( + version="1.18.0", + reason="Removed from the specification", +) +class MessagingDestinationKindValues(Enum): + QUEUE = "queue" + """A message sent to a queue.""" + + TOPIC = "topic" + """A message sent to a topic.""" + + +@deprecated( + version="1.21.0", + reason="Renamed to NetworkConnectionTypeValues", +) +class NetHostConnectionTypeValues(Enum): + WIFI = "wifi" + """wifi.""" + + WIRED = "wired" + """wired.""" + + CELL = "cell" + """cell.""" + + UNAVAILABLE = "unavailable" + """unavailable.""" + + UNKNOWN = "unknown" + """unknown.""" + + +@deprecated( + version="1.21.0", + reason="Renamed to NetworkConnectionSubtypeValues", +) +class NetHostConnectionSubtypeValues(Enum): + GPRS = "gprs" + """GPRS.""" + + EDGE = "edge" + """EDGE.""" + + UMTS = "umts" + """UMTS.""" + + CDMA = "cdma" + """CDMA.""" + + EVDO_0 = "evdo_0" + """EVDO Rel. 0.""" + + EVDO_A = "evdo_a" + """EVDO Rev. A.""" + + CDMA2000_1XRTT = "cdma2000_1xrtt" + """CDMA2000 1XRTT.""" + + HSDPA = "hsdpa" + """HSDPA.""" + + HSUPA = "hsupa" + """HSUPA.""" + + HSPA = "hspa" + """HSPA.""" + + IDEN = "iden" + """IDEN.""" + + EVDO_B = "evdo_b" + """EVDO Rev. B.""" + + LTE = "lte" + """LTE.""" + + EHRPD = "ehrpd" + """EHRPD.""" + + HSPAP = "hspap" + """HSPAP.""" + + GSM = "gsm" + """GSM.""" + + TD_SCDMA = "td_scdma" + """TD-SCDMA.""" + + IWLAN = "iwlan" + """IWLAN.""" + + NR = "nr" + """5G NR (New Radio).""" + + NRNSA = "nrnsa" + """5G NRNSA (New Radio Non-Standalone).""" + + LTE_CA = "lte_ca" + """LTE CA.""" - AWS_DYNAMODB_SCAN_FORWARD = "aws.dynamodb.scan_forward" - """ - The value of the `ScanIndexForward` request parameter. - """ - AWS_DYNAMODB_SEGMENT = "aws.dynamodb.segment" - """ - The value of the `Segment` request parameter. - """ +class NetTransportValues(Enum): + IP_TCP = "ip_tcp" + """ip_tcp.""" - AWS_DYNAMODB_TOTAL_SEGMENTS = "aws.dynamodb.total_segments" - """ - The value of the `TotalSegments` request parameter. - """ + IP_UDP = "ip_udp" + """ip_udp.""" - AWS_DYNAMODB_COUNT = "aws.dynamodb.count" - """ - The value of the `Count` response parameter. - """ + PIPE = "pipe" + """Named or anonymous pipe.""" - AWS_DYNAMODB_SCANNED_COUNT = "aws.dynamodb.scanned_count" - """ - The value of the `ScannedCount` response parameter. - """ + INPROC = "inproc" + """In-process communication.""" - AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS = "aws.dynamodb.attribute_definitions" - """ - The JSON-serialized value of each item in the `AttributeDefinitions` request field. - """ + OTHER = "other" + """Something else (non IP-based).""" - AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES = ( - "aws.dynamodb.global_secondary_index_updates" - ) - """ - The JSON-serialized value of each item in the the `GlobalSecondaryIndexUpdates` request field. - """ - MESSAGING_OPERATION = "messaging.operation" - """ - A string identifying the kind of message consumption as defined in the [Operation names](#operation-names) section above. If the operation is "send", this attribute MUST NOT be set, since the operation can be inferred from the span kind in that case. - """ +class NetSockFamilyValues(Enum): + INET = "inet" + """IPv4 address.""" - MESSAGING_CONSUMER_ID = "messaging.consumer_id" - """ - The identifier for the consumer receiving a message. For Kafka, set it to `{messaging.kafka.consumer_group} - {messaging.kafka.client_id}`, if both are present, or only `messaging.kafka.consumer_group`. For brokers, such as RabbitMQ and Artemis, set it to the `client_id` of the client consuming the message. - """ + INET6 = "inet6" + """IPv6 address.""" - MESSAGING_RABBITMQ_ROUTING_KEY = "messaging.rabbitmq.routing_key" - """ - RabbitMQ message routing key. - """ + UNIX = "unix" + """Unix domain socket path.""" - MESSAGING_KAFKA_MESSAGE_KEY = "messaging.kafka.message_key" - """ - Message keys in Kafka are used for grouping alike messages to ensure they're processed on the same partition. They differ from `messaging.message_id` in that they're not unique. If the key is `null`, the attribute MUST NOT be set. - Note: If the key type is not string, it's string representation has to be supplied for the attribute. If the key has no unambiguous, canonical string form, don't include its value. - """ - MESSAGING_KAFKA_CONSUMER_GROUP = "messaging.kafka.consumer_group" - """ - Name of the Kafka Consumer Group that is handling the message. Only applies to consumers, not producers. - """ +class HttpRequestMethodValues(Enum): + CONNECT = "CONNECT" + """CONNECT method.""" - MESSAGING_KAFKA_CLIENT_ID = "messaging.kafka.client_id" - """ - Client Id for the Consumer or Producer that is handling the message. - """ + DELETE = "DELETE" + """DELETE method.""" - MESSAGING_KAFKA_PARTITION = "messaging.kafka.partition" - """ - Partition the message is sent to. - """ + GET = "GET" + """GET method.""" - MESSAGING_KAFKA_TOMBSTONE = "messaging.kafka.tombstone" - """ - A boolean that is true if the message is a tombstone. - """ + HEAD = "HEAD" + """HEAD method.""" - MESSAGING_ROCKETMQ_NAMESPACE = "messaging.rocketmq.namespace" - """ - Namespace of RocketMQ resources, resources in different namespaces are individual. - """ + OPTIONS = "OPTIONS" + """OPTIONS method.""" - MESSAGING_ROCKETMQ_CLIENT_GROUP = "messaging.rocketmq.client_group" - """ - Name of the RocketMQ producer/consumer group that is handling the message. The client type is identified by the SpanKind. - """ + PATCH = "PATCH" + """PATCH method.""" - MESSAGING_ROCKETMQ_CLIENT_ID = "messaging.rocketmq.client_id" - """ - The unique identifier for each client. - """ + POST = "POST" + """POST method.""" - MESSAGING_ROCKETMQ_MESSAGE_TYPE = "messaging.rocketmq.message_type" - """ - Type of message. - """ + PUT = "PUT" + """PUT method.""" - MESSAGING_ROCKETMQ_MESSAGE_TAG = "messaging.rocketmq.message_tag" - """ - The secondary classifier of message besides topic. - """ + TRACE = "TRACE" + """TRACE method.""" - MESSAGING_ROCKETMQ_MESSAGE_KEYS = "messaging.rocketmq.message_keys" - """ - Key(s) of message, another way to mark message besides message id. - """ + OTHER = "_OTHER" + """Any HTTP method that the instrumentation has no prior knowledge of.""" - MESSAGING_ROCKETMQ_CONSUMPTION_MODEL = ( - "messaging.rocketmq.consumption_model" - ) - """ - Model of message consumption. This only applies to consumer spans. - """ - RPC_GRPC_STATUS_CODE = "rpc.grpc.status_code" - """ - The [numeric status code](https://github.com/grpc/grpc/blob/v1.33.2/doc/statuscodes.md) of the gRPC request. - """ +class EventDomainValues(Enum): + BROWSER = "browser" + """Events from browser apps.""" - RPC_JSONRPC_VERSION = "rpc.jsonrpc.version" - """ - Protocol version as in `jsonrpc` property of request/response. Since JSON-RPC 1.0 does not specify this, the value can be omitted. - """ + DEVICE = "device" + """Events from mobile apps.""" - RPC_JSONRPC_REQUEST_ID = "rpc.jsonrpc.request_id" - """ - `id` property of request or response. Since protocol allows id to be int, string, `null` or missing (for notifications), value is expected to be cast to string for simplicity. Use empty string in case of `null` value. Omit entirely if this is a notification. - """ + K8S = "k8s" + """Events from Kubernetes.""" - RPC_JSONRPC_ERROR_CODE = "rpc.jsonrpc.error_code" - """ - `error.code` property of response if it is an error response. - """ - RPC_JSONRPC_ERROR_MESSAGE = "rpc.jsonrpc.error_message" - """ - `error.message` property of response if it is an error response. - """ +class LogIostreamValues(Enum): + STDOUT = "stdout" + """Logs from stdout stream.""" - MESSAGE_TYPE = "message.type" - """ - Whether this is a received or sent message. - """ + STDERR = "stderr" + """Events from stderr stream.""" - MESSAGE_ID = "message.id" - """ - MUST be calculated as two different counters starting from `1` one for sent messages and one for received message. - Note: This way we guarantee that the values will be consistent between different implementations. - """ - MESSAGE_COMPRESSED_SIZE = "message.compressed_size" - """ - Compressed size of the message in bytes. - """ +class TypeValues(Enum): + HEAP = "heap" + """Heap memory.""" - MESSAGE_UNCOMPRESSED_SIZE = "message.uncompressed_size" - """ - Uncompressed size of the message in bytes. - """ + NON_HEAP = "non_heap" + """Non-heap memory.""" class OpentracingRefTypeValues(Enum): @@ -814,6 +1621,9 @@ class DbSystemValues(Enum): MSSQL = "mssql" """Microsoft SQL Server.""" + MSSQLCOMPACT = "mssqlcompact" + """Microsoft SQL Server Compact.""" + MYSQL = "mysql" """MySQL.""" @@ -949,28 +1759,39 @@ class DbSystemValues(Enum): COCKROACHDB = "cockroachdb" """CockroachDB.""" + OPENSEARCH = "opensearch" + """OpenSearch.""" -class NetTransportValues(Enum): - IP_TCP = "ip_tcp" - """ip_tcp.""" + CLICKHOUSE = "clickhouse" + """ClickHouse.""" - IP_UDP = "ip_udp" - """ip_udp.""" + SPANNER = "spanner" + """Cloud Spanner.""" - IP = "ip" - """Another IP-based protocol.""" + TRINO = "trino" + """Trino.""" - UNIX = "unix" - """Unix Domain socket. See below.""" + +class NetworkTransportValues(Enum): + TCP = "tcp" + """TCP.""" + + UDP = "udp" + """UDP.""" PIPE = "pipe" """Named or anonymous pipe. See note below.""" - INPROC = "inproc" - """In-process communication.""" + UNIX = "unix" + """Unix domain socket.""" - OTHER = "other" - """Something else (non IP-based).""" + +class NetworkTypeValues(Enum): + IPV4 = "ipv4" + """IPv4.""" + + IPV6 = "ipv6" + """IPv6.""" class DbCassandraConsistencyLevelValues(Enum): @@ -1008,6 +1829,69 @@ class DbCassandraConsistencyLevelValues(Enum): """local_serial.""" +class DbCosmosdbOperationTypeValues(Enum): + INVALID = "Invalid" + """invalid.""" + + CREATE = "Create" + """create.""" + + PATCH = "Patch" + """patch.""" + + READ = "Read" + """read.""" + + READ_FEED = "ReadFeed" + """read_feed.""" + + DELETE = "Delete" + """delete.""" + + REPLACE = "Replace" + """replace.""" + + EXECUTE = "Execute" + """execute.""" + + QUERY = "Query" + """query.""" + + HEAD = "Head" + """head.""" + + HEAD_FEED = "HeadFeed" + """head_feed.""" + + UPSERT = "Upsert" + """upsert.""" + + BATCH = "Batch" + """batch.""" + + QUERY_PLAN = "QueryPlan" + """query_plan.""" + + EXECUTE_JAVASCRIPT = "ExecuteJavaScript" + """execute_javascript.""" + + +class DbCosmosdbConnectionModeValues(Enum): + GATEWAY = "gateway" + """Gateway (HTTP) connections mode.""" + + DIRECT = "direct" + """Direct connection.""" + + +class OtelStatusCodeValues(Enum): + OK = "OK" + """The operation has been validated by an Application developer or Operator to have completed successfully.""" + + ERROR = "ERROR" + """The operation contains an error.""" + + class FaasTriggerValues(Enum): DATASOURCE = "datasource" """A response to some data source operation such as a database or filesystem read/write.""" @@ -1036,27 +1920,35 @@ class FaasDocumentOperationValues(Enum): """When an object is deleted.""" -class HttpFlavorValues(Enum): - HTTP_1_0 = "1.0" - """HTTP/1.0.""" +class MessagingOperationValues(Enum): + PUBLISH = "publish" + """publish.""" - HTTP_1_1 = "1.1" - """HTTP/1.1.""" + RECEIVE = "receive" + """receive.""" - HTTP_2_0 = "2.0" - """HTTP/2.""" + PROCESS = "process" + """process.""" - HTTP_3_0 = "3.0" - """HTTP/3.""" - SPDY = "SPDY" - """SPDY protocol.""" +class FaasInvokedProviderValues(Enum): + ALIBABA_CLOUD = "alibaba_cloud" + """Alibaba Cloud.""" - QUIC = "QUIC" - """QUIC protocol.""" + AWS = "aws" + """Amazon Web Services.""" + AZURE = "azure" + """Microsoft Azure.""" -class NetHostConnectionTypeValues(Enum): + GCP = "gcp" + """Google Cloud Platform.""" + + TENCENT_CLOUD = "tencent_cloud" + """Tencent Cloud.""" + + +class NetworkConnectionTypeValues(Enum): WIFI = "wifi" """wifi.""" @@ -1073,7 +1965,7 @@ class NetHostConnectionTypeValues(Enum): """unknown.""" -class NetHostConnectionSubtypeValues(Enum): +class NetworkConnectionSubtypeValues(Enum): GPRS = "gprs" """GPRS.""" @@ -1138,31 +2030,6 @@ class NetHostConnectionSubtypeValues(Enum): """LTE CA.""" -class MessagingDestinationKindValues(Enum): - QUEUE = "queue" - """A message sent to a queue.""" - - TOPIC = "topic" - """A message sent to a topic.""" - - -class FaasInvokedProviderValues(Enum): - ALIBABA_CLOUD = "alibaba_cloud" - """Alibaba Cloud.""" - - AWS = "aws" - """Amazon Web Services.""" - - AZURE = "azure" - """Microsoft Azure.""" - - GCP = "gcp" - """Google Cloud Platform.""" - - TENCENT_CLOUD = "tencent_cloud" - """Tencent Cloud.""" - - class RpcSystemValues(Enum): GRPC = "grpc" """gRPC.""" @@ -1176,13 +2043,19 @@ class RpcSystemValues(Enum): APACHE_DUBBO = "apache_dubbo" """Apache Dubbo.""" + CONNECT_RPC = "connect_rpc" + """Connect RPC.""" -class MessagingOperationValues(Enum): - RECEIVE = "receive" - """receive.""" - PROCESS = "process" - """process.""" +class GraphqlOperationTypeValues(Enum): + QUERY = "query" + """GraphQL query.""" + + MUTATION = "mutation" + """GraphQL mutation.""" + + SUBSCRIPTION = "subscription" + """GraphQL subscription.""" class MessagingRocketmqMessageTypeValues(Enum): @@ -1266,3 +2139,53 @@ class MessageTypeValues(Enum): RECEIVED = "RECEIVED" """received.""" + + +class RpcConnectRpcErrorCodeValues(Enum): + CANCELLED = "cancelled" + """cancelled.""" + + UNKNOWN = "unknown" + """unknown.""" + + INVALID_ARGUMENT = "invalid_argument" + """invalid_argument.""" + + DEADLINE_EXCEEDED = "deadline_exceeded" + """deadline_exceeded.""" + + NOT_FOUND = "not_found" + """not_found.""" + + ALREADY_EXISTS = "already_exists" + """already_exists.""" + + PERMISSION_DENIED = "permission_denied" + """permission_denied.""" + + RESOURCE_EXHAUSTED = "resource_exhausted" + """resource_exhausted.""" + + FAILED_PRECONDITION = "failed_precondition" + """failed_precondition.""" + + ABORTED = "aborted" + """aborted.""" + + OUT_OF_RANGE = "out_of_range" + """out_of_range.""" + + UNIMPLEMENTED = "unimplemented" + """unimplemented.""" + + INTERNAL = "internal" + """internal.""" + + UNAVAILABLE = "unavailable" + """unavailable.""" + + DATA_LOSS = "data_loss" + """data_loss.""" + + UNAUTHENTICATED = "unauthenticated" + """unauthenticated.""" diff --git a/scripts/semconv/generate.sh b/scripts/semconv/generate.sh index 866832fe6b..3a453db025 100755 --- a/scripts/semconv/generate.sh +++ b/scripts/semconv/generate.sh @@ -4,39 +4,56 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR="${SCRIPT_DIR}/../../" # freeze the spec version to make SemanticAttributes generation reproducible -SPEC_VERSION=v1.11.0 -OTEL_SEMCONV_GEN_IMG_VERSION=0.11.1 +SPEC_VERSION=v1.21.0 +SCHEMA_URL=https://opentelemetry.io/schemas/$SPEC_VERSION +OTEL_SEMCONV_GEN_IMG_VERSION=0.21.0 cd ${SCRIPT_DIR} -rm -rf opentelemetry-specification || true -mkdir opentelemetry-specification -cd opentelemetry-specification +rm -rf semantic-conventions || true +mkdir semantic-conventions +cd semantic-conventions git init -git remote add origin https://github.com/open-telemetry/opentelemetry-specification.git +git remote add origin https://github.com/open-telemetry/semantic-conventions.git git fetch origin "$SPEC_VERSION" git reset --hard FETCH_HEAD cd ${SCRIPT_DIR} docker run --rm \ - -v ${SCRIPT_DIR}/opentelemetry-specification/semantic_conventions/trace:/source \ + -v ${SCRIPT_DIR}/semantic-conventions/model:/source \ -v ${SCRIPT_DIR}/templates:/templates \ -v ${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/:/output \ otel/semconvgen:$OTEL_SEMCONV_GEN_IMG_VERSION \ + --only span,event,attribute_group \ -f /source code \ --template /templates/semantic_attributes.j2 \ --output /output/__init__.py \ - -Dclass=SpanAttributes + -Dclass=SpanAttributes \ + -DschemaUrl=$SCHEMA_URL docker run --rm \ - -v ${SCRIPT_DIR}/opentelemetry-specification/semantic_conventions/resource:/source \ + -v ${SCRIPT_DIR}/semantic-conventions/model:/source \ -v ${SCRIPT_DIR}/templates:/templates \ -v ${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/:/output \ otel/semconvgen:$OTEL_SEMCONV_GEN_IMG_VERSION \ + --only resource \ -f /source code \ --template /templates/semantic_attributes.j2 \ --output /output/__init__.py \ - -Dclass=ResourceAttributes + -Dclass=ResourceAttributes \ + -DschemaUrl=$SCHEMA_URL + +docker run --rm \ + -v ${SCRIPT_DIR}/semantic-conventions/model:/source \ + -v ${SCRIPT_DIR}/templates:/templates \ + -v ${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/:/output \ + otel/semconvgen:$OTEL_SEMCONV_GEN_IMG_VERSION \ + --only metric \ + -f /source code \ + --template /templates/semantic_metrics.j2 \ + --output /output/__init__.py \ + -Dclass=MetricInstruments \ + -DschemaUrl=$SCHEMA_URL cd "$ROOT_DIR" diff --git a/scripts/semconv/templates/semantic_attributes.j2 b/scripts/semconv/templates/semantic_attributes.j2 index 32d3f18a1b..7e48d74768 100644 --- a/scripts/semconv/templates/semantic_attributes.j2 +++ b/scripts/semconv/templates/semantic_attributes.j2 @@ -12,22 +12,32 @@ # See the License for the specific language governing permissions and # limitations under the License. - +# pylint: disable=too-many-lines {%- macro print_value(type, value) -%} {{ "\"" if type == "string"}}{{value}}{{ "\"" if type == "string"}} {%- endmacro %} from enum import Enum +{%- if class == "SpanAttributes" %} + +from deprecated import deprecated + +{%- endif %} + class {{class}}: + SCHEMA_URL = "{{schemaUrl}}" + """ + The URL of the OpenTelemetry schema for these keys and values. + """ {%- for attribute in attributes | unique(attribute="fqn") %} {{attribute.fqn | to_const_name}} = "{{attribute.fqn}}" """ {{attribute.brief | to_doc_brief}}. {%- if attribute.note %} - Note: {{attribute.note | to_doc_brief}}. + Note: {{attribute.note | to_doc_brief | indent}}. {%- endif %} {%- if attribute.deprecated %} @@ -37,6 +47,315 @@ class {{class}}: {# Extra line #} {%- endfor %} + {%- if class == "SpanAttributes" %} + # Manually defined deprecated attributes + {# + Deprecated attributes and types are defined here for backward compatibility reasons. + They were removed from OpenTelemetry semantic conventions completely. + + Attributes that were deprecated in OpenTelemetry semantic conventions + (https://github.com/open-telemetry/semantic-conventions/tree/main/model/deprecated) + are auto-generated with comments indicating deprecated status, so they don't need + to be manually defined. + #} + + NET_PEER_IP = "net.peer.ip" + """ + Deprecated, use the `client.socket.address` attribute. + """ + + NET_HOST_IP = "net.host.ip" + """ + Deprecated, use the `server.socket.address` attribute. + """ + + HTTP_SERVER_NAME = "http.server_name" + """ + Deprecated, use the `server.address` attribute. + """ + + HTTP_HOST = "http.host" + """ + Deprecated, use the `server.address` and `server.port` attributes. + """ + + HTTP_RETRY_COUNT = "http.retry_count" + """ + Deprecated, use the `http.resend_count` attribute. + """ + + HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED = ( + "http.request_content_length_uncompressed" + ) + """ + Deprecated, use the `http.request.body.size` attribute. + """ + + HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED = ( + "http.response_content_length_uncompressed" + ) + """ + Deprecated, use the `http.response.body.size` attribute. + """ + + MESSAGING_DESTINATION = "messaging.destination" + """ + Deprecated, use the `messaging.destination.name` attribute. + """ + + MESSAGING_DESTINATION_KIND = "messaging.destination_kind" + """ + Deprecated. + """ + + MESSAGING_TEMP_DESTINATION = "messaging.temp_destination" + """ + Deprecated. Use `messaging.destination.temporary` attribute. + """ + + MESSAGING_PROTOCOL = "messaging.protocol" + """ + Deprecated. Use `network.protocol.name` attribute. + """ + + MESSAGING_PROTOCOL_VERSION = "messaging.protocol_version" + """ + Deprecated. Use `network.protocol.version` attribute. + """ + + MESSAGING_URL = "messaging.url" + """ + Deprecated. Use `server.address` and `server.port` attributes. + """ + + MESSAGING_CONVERSATION_ID = "messaging.conversation_id" + """ + Deprecated. Use `messaging.message.conversation.id` attribute. + """ + + MESSAGING_KAFKA_PARTITION = "messaging.kafka.partition" + """ + Deprecated. Use `messaging.kafka.destination.partition` attribute. + """ + + FAAS_EXECUTION = "faas.execution" + """ + Deprecated. Use `faas.invocation_id` attribute. + """ + + HTTP_USER_AGENT = "http.user_agent" + """ + Deprecated. Use `user_agent.original` attribute. + """ + + MESSAGING_RABBITMQ_ROUTING_KEY = "messaging.rabbitmq.routing_key" + """ + Deprecated. Use `messaging.rabbitmq.destination.routing_key` attribute. + """ + + MESSAGING_KAFKA_TOMBSTONE = "messaging.kafka.tombstone" + """ + Deprecated. Use `messaging.kafka.destination.tombstone` attribute. + """ + + NET_APP_PROTOCOL_NAME = "net.app.protocol.name" + """ + Deprecated. Use `network.protocol.name` attribute. + """ + + NET_APP_PROTOCOL_VERSION = "net.app.protocol.version" + """ + Deprecated. Use `network.protocol.version` attribute. + """ + + HTTP_CLIENT_IP = "http.client_ip" + """ + Deprecated. Use `client.address` attribute. + """ + + HTTP_FLAVOR = "http.flavor" + """ + Deprecated. Use `network.protocol.name` and `network.protocol.version` attributes. + """ + + NET_HOST_CONNECTION_TYPE = "net.host.connection.type" + """ + Deprecated. Use `network.connection.type` attribute. + """ + + NET_HOST_CONNECTION_SUBTYPE = "net.host.connection.subtype" + """ + Deprecated. Use `network.connection.subtype` attribute. + """ + + NET_HOST_CARRIER_NAME = "net.host.carrier.name" + """ + Deprecated. Use `network.carrier.name` attribute. + """ + + NET_HOST_CARRIER_MCC = "net.host.carrier.mcc" + """ + Deprecated. Use `network.carrier.mcc` attribute. + """ + + NET_HOST_CARRIER_MNC = "net.host.carrier.mnc" + """ + Deprecated. Use `network.carrier.mnc` attribute. + """ + + MESSAGING_CONSUMER_ID = "messaging.consumer_id" + """ + Deprecated. Use `messaging.client_id` attribute. + """ + + MESSAGING_KAFKA_CLIENT_ID = "messaging.kafka.client_id" + """ + Deprecated. Use `messaging.client_id` attribute. + """ + + MESSAGING_ROCKETMQ_CLIENT_ID = "messaging.rocketmq.client_id" + """ + Deprecated. Use `messaging.client_id` attribute. + """ + +@deprecated( + version="1.18.0", + reason="Removed from the specification in favor of `network.protocol.name` and `network.protocol.version` attributes", +) +class HttpFlavorValues(Enum): + HTTP_1_0 = "1.0" + + HTTP_1_1 = "1.1" + + HTTP_2_0 = "2.0" + + HTTP_3_0 = "3.0" + + SPDY = "SPDY" + + QUIC = "QUIC" + +@deprecated( + version="1.18.0", + reason="Removed from the specification", +) +class MessagingDestinationKindValues(Enum): + QUEUE = "queue" + """A message sent to a queue.""" + + TOPIC = "topic" + """A message sent to a topic.""" + + +@deprecated( + version="1.21.0", + reason="Renamed to NetworkConnectionTypeValues", +) +class NetHostConnectionTypeValues(Enum): + WIFI = "wifi" + """wifi.""" + + WIRED = "wired" + """wired.""" + + CELL = "cell" + """cell.""" + + UNAVAILABLE = "unavailable" + """unavailable.""" + + UNKNOWN = "unknown" + """unknown.""" + + +@deprecated( + version="1.21.0", + reason="Renamed to NetworkConnectionSubtypeValues", +) +class NetHostConnectionSubtypeValues(Enum): + GPRS = "gprs" + """GPRS.""" + + EDGE = "edge" + """EDGE.""" + + UMTS = "umts" + """UMTS.""" + + CDMA = "cdma" + """CDMA.""" + + EVDO_0 = "evdo_0" + """EVDO Rel. 0.""" + + EVDO_A = "evdo_a" + """EVDO Rev. A.""" + + CDMA2000_1XRTT = "cdma2000_1xrtt" + """CDMA2000 1XRTT.""" + + HSDPA = "hsdpa" + """HSDPA.""" + + HSUPA = "hsupa" + """HSUPA.""" + + HSPA = "hspa" + """HSPA.""" + + IDEN = "iden" + """IDEN.""" + + EVDO_B = "evdo_b" + """EVDO Rev. B.""" + + LTE = "lte" + """LTE.""" + + EHRPD = "ehrpd" + """EHRPD.""" + + HSPAP = "hspap" + """HSPAP.""" + + GSM = "gsm" + """GSM.""" + + TD_SCDMA = "td_scdma" + """TD-SCDMA.""" + + IWLAN = "iwlan" + """IWLAN.""" + + NR = "nr" + """5G NR (New Radio).""" + + NRNSA = "nrnsa" + """5G NRNSA (New Radio Non-Standalone).""" + + LTE_CA = "lte_ca" + """LTE CA.""" + + {% endif %} + + {%- if class == "ResourceAttributes" %} + # Manually defined deprecated attributes + {# + Deprecated attributes and types are defined here for backward compatibility reasons. + They were removed from OpenTelemetry semantic conventions completely. + + Attributes that were deprecated in OpenTelemetry semantic conventions + (https://github.com/open-telemetry/semantic-conventions/tree/main/model/deprecated) + are auto-generated with comments indicating deprecated status, so they don't need + to be manually defined. + #} + + FAAS_ID = "faas.id" + """ + Deprecated, use the `cloud.resource.id` attribute. + """ + {% endif %} + {%- for attribute in attributes | unique(attribute="fqn") %} {%- if attribute.is_enum %} {%- set class_name = attribute.fqn | to_camelcase(True) ~ "Values" %} diff --git a/scripts/semconv/templates/semantic_metrics.j2 b/scripts/semconv/templates/semantic_metrics.j2 new file mode 100644 index 0000000000..4fa1260cb5 --- /dev/null +++ b/scripts/semconv/templates/semantic_metrics.j2 @@ -0,0 +1,40 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class {{class}}: + SCHEMA_URL = "{{schemaUrl}}" + """ + The URL of the OpenTelemetry schema for these keys and values. + """ + {% for id in semconvs %}{%- if semconvs[id].GROUP_TYPE_NAME == 'metric' %}{% set metric = semconvs[id] %} + {{metric.metric_name | to_const_name}} = "{{metric.metric_name}}" + """ + {{metric.brief | to_doc_brief}} + Instrument: {{ metric.instrument }} + Unit: {{ metric.unit }} + """ +{# Extra line #} + {%- endif %}{% endfor %} + + # Manually defined metrics + {# + Metrics defined here manually were not yaml-ified in 1.21.0 release + and therefore are not auto-generated. + #} + DB_CLIENT_CONNECTIONS_USAGE = "db.client.connections.usage" + """ + The number of connections that are currently in state described by the `state` attribute + Instrument: UpDownCounter + Unit: {connection} + """ \ No newline at end of file From 7f8c75d4de1a53beec59eed21cf95d2aa6428969 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 2 Oct 2023 15:17:52 -0700 Subject: [PATCH 1481/1517] schema (#3454) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/_logs/_internal/__init__.py | 3 ++- .../src/opentelemetry/metrics/_internal/__init__.py | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e0dd09f5..0f9ef1720a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3447](https://github.com/open-telemetry/opentelemetry-python/pull/3447)) - Update semantic conventions to version 1.21.0 ([#3251](https://github.com/open-telemetry/opentelemetry-python/pull/3251)) +- Add missing schema_url in global api for logging and metrics + ([#3251](https://github.com/open-telemetry/opentelemetry-python/pull/3251)) ## Version 1.20.0/0.41b0 (2023-09-04) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index c6d35dbf07..f5f9437f14 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -215,6 +215,7 @@ def get_logger( instrumenting_module_name: str, instrumenting_library_version: str = "", logger_provider: Optional[LoggerProvider] = None, + schema_url: Optional[str] = None, ) -> "Logger": """Returns a `Logger` for use within a python process. @@ -226,5 +227,5 @@ def get_logger( if logger_provider is None: logger_provider = get_logger_provider() return logger_provider.get_logger( - instrumenting_module_name, instrumenting_library_version + instrumenting_module_name, instrumenting_library_version, schema_url ) diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py index 630e9c4053..aad76b33a7 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py @@ -719,6 +719,7 @@ def get_meter( name: str, version: str = "", meter_provider: Optional[MeterProvider] = None, + schema_url: Optional[str] = None, ) -> "Meter": """Returns a `Meter` for use by the given instrumentation library. @@ -729,7 +730,7 @@ def get_meter( """ if meter_provider is None: meter_provider = get_meter_provider() - return meter_provider.get_meter(name, version) + return meter_provider.get_meter(name, version, schema_url) def _set_meter_provider(meter_provider: MeterProvider, log: bool) -> None: From 7ca1dd149c6c759dfb8735578d14e8b30bf2cba2 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Tue, 3 Oct 2023 17:30:20 -0400 Subject: [PATCH 1482/1517] Fix typo causing failing CI (#3459) --- .../src/opentelemetry/sdk/_configuration/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index e2abcbefa1..33c5147a59 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -415,7 +415,7 @@ class _OTelSDKConfigurator(_BaseConfigurator): NOTE: This class should not be instantiated nor should it become an entry point on the `opentelemetry-sdk` package. Instead, distros should subclass - this Configurator and enchance it as needed. + this Configurator and enhance it as needed. """ def _configure(self, **kwargs): From 4280235f5383cb5034993f76aee22502a631a0b2 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Wed, 4 Oct 2023 12:09:47 -0400 Subject: [PATCH 1483/1517] Prometheus exporter support for auto instrumentation (#3413) --- CHANGELOG.md | 3 + .../pyproject.toml | 7 +- .../exporter/prometheus/__init__.py | 24 ++++++ .../tests/test_entrypoints.py | 75 +++++++++++++++++++ .../sdk/environment_variables.py | 24 ++++++ 5 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 exporter/opentelemetry-exporter-prometheus/tests/test_entrypoints.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f9ef1720a..3faa61e06a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3251](https://github.com/open-telemetry/opentelemetry-python/pull/3251)) - Add missing schema_url in global api for logging and metrics ([#3251](https://github.com/open-telemetry/opentelemetry-python/pull/3251)) +- Prometheus exporter support for auto instrumentation + ([#3413](https://github.com/open-telemetry/opentelemetry-python/pull/3413)) + ## Version 1.20.0/0.41b0 (2023-09-04) diff --git a/exporter/opentelemetry-exporter-prometheus/pyproject.toml b/exporter/opentelemetry-exporter-prometheus/pyproject.toml index f7018469ba..48f4afb90f 100644 --- a/exporter/opentelemetry-exporter-prometheus/pyproject.toml +++ b/exporter/opentelemetry-exporter-prometheus/pyproject.toml @@ -26,15 +26,16 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-sdk ~= 1.12", + # DONOTMERGE: confirm that this will becomes ~= 1.21 in the next release + "opentelemetry-sdk ~= 1.21.0.dev", "prometheus_client >= 0.5.0, < 1.0.0", ] [project.optional-dependencies] test = [] -[project.entry-points.opentelemetry_metric_reader] -prometheus = "opentelemetry.exporter.prometheus:PrometheusMetricReader" +[project.entry-points.opentelemetry_metrics_exporter] +prometheus = "opentelemetry.exporter.prometheus:_AutoPrometheusMetricReader" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-prometheus" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 252f240b35..8d9e18d733 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -66,9 +66,11 @@ from itertools import chain from json import dumps from logging import getLogger +from os import environ from re import IGNORECASE, UNICODE, compile from typing import Dict, Sequence, Tuple, Union +from prometheus_client import start_http_server from prometheus_client.core import ( REGISTRY, CounterMetricFamily, @@ -78,6 +80,10 @@ ) from prometheus_client.core import Metric as PrometheusMetric +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_PROMETHEUS_HOST, + OTEL_EXPORTER_PROMETHEUS_PORT, +) from opentelemetry.sdk.metrics import Counter from opentelemetry.sdk.metrics import Histogram as HistogramInstrument from opentelemetry.sdk.metrics import ( @@ -375,3 +381,21 @@ def _create_info_metric( info = InfoMetricFamily(name, description, labels=attributes) info.add_metric(labels=list(attributes.keys()), value=attributes) return info + + +class _AutoPrometheusMetricReader(PrometheusMetricReader): + """Thin wrapper around PrometheusMetricReader used for the opentelemetry_metrics_exporter entry point. + + This allows users to use the prometheus exporter with opentelemetry-instrument. It handles + starting the Prometheus http server on the the correct port and host. + """ + + def __init__(self) -> None: + super().__init__() + + # Default values are specified in + # https://github.com/open-telemetry/opentelemetry-specification/blob/v1.24.0/specification/configuration/sdk-environment-variables.md#prometheus-exporter + start_http_server( + port=int(environ.get(OTEL_EXPORTER_PROMETHEUS_PORT, "9464")), + addr=environ.get(OTEL_EXPORTER_PROMETHEUS_HOST, "localhost"), + ) diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_entrypoints.py b/exporter/opentelemetry-exporter-prometheus/tests/test_entrypoints.py new file mode 100644 index 0000000000..96846e0759 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_entrypoints.py @@ -0,0 +1,75 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=no-self-use + +import os +from unittest import TestCase +from unittest.mock import ANY, Mock, patch + +from opentelemetry.exporter.prometheus import _AutoPrometheusMetricReader +from opentelemetry.sdk._configuration import _import_exporters +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_PROMETHEUS_HOST, + OTEL_EXPORTER_PROMETHEUS_PORT, +) + + +class TestEntrypoints(TestCase): + def test_import_exporters(self) -> None: + """ + Tests that the entrypoint can be loaded and doesn't have a typo in the name + """ + ( + _trace_exporters, + metric_exporters, + _logs_exporters, + ) = _import_exporters( + trace_exporter_names=[], + metric_exporter_names=["prometheus"], + log_exporter_names=[], + ) + + self.assertIs( + metric_exporters["prometheus"], + _AutoPrometheusMetricReader, + ) + + @patch("opentelemetry.exporter.prometheus.start_http_server") + @patch.dict(os.environ) + def test_starts_http_server_defaults( + self, mock_start_http_server: Mock + ) -> None: + _AutoPrometheusMetricReader() + mock_start_http_server.assert_called_once_with( + port=9464, addr="localhost" + ) + + @patch("opentelemetry.exporter.prometheus.start_http_server") + @patch.dict(os.environ, {OTEL_EXPORTER_PROMETHEUS_HOST: "1.2.3.4"}) + def test_starts_http_server_host_envvar( + self, mock_start_http_server: Mock + ) -> None: + _AutoPrometheusMetricReader() + mock_start_http_server.assert_called_once_with( + port=ANY, addr="1.2.3.4" + ) + + @patch("opentelemetry.exporter.prometheus.start_http_server") + @patch.dict(os.environ, {OTEL_EXPORTER_PROMETHEUS_PORT: "9999"}) + def test_starts_http_server_port_envvar( + self, mock_start_http_server: Mock + ) -> None: + _AutoPrometheusMetricReader() + mock_start_http_server.assert_called_once_with(port=9999, addr=ANY) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 10031e5ef3..dc8f91f600 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -678,3 +678,27 @@ experimental feature and the name of this variable and its behavior can change in a non-backwards compatible way. """ + +OTEL_EXPORTER_PROMETHEUS_HOST = "OTEL_EXPORTER_PROMETHEUS_HOST" +""" +.. envvar:: OTEL_EXPORTER_PROMETHEUS_HOST + +The :envvar:`OTEL_EXPORTER_PROMETHEUS_HOST` environment variable configures the host used by +the Prometheus exporter. +Default: "localhost" + +This is an experimental environment variable and the name of this variable and its behavior can +change in a non-backwards compatible way. +""" + +OTEL_EXPORTER_PROMETHEUS_PORT = "OTEL_EXPORTER_PROMETHEUS_PORT" +""" +.. envvar:: OTEL_EXPORTER_PROMETHEUS_PORT + +The :envvar:`OTEL_EXPORTER_PROMETHEUS_PORT` environment variable configures the port used by +the Prometheus exporter. +Default: 9464 + +This is an experimental environment variable and the name of this variable and its behavior can +change in a non-backwards compatible way. +""" From 5ae8f6669eb506034253f0899f6656357da873d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:58:47 -0700 Subject: [PATCH 1484/1517] Bump opentelemetry-instrumentation (#3457) --- .../examples/fork-process-model/flask-gunicorn/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt index ab17c5debf..d763cc96d4 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt +++ b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt @@ -8,7 +8,7 @@ Jinja2==2.11.3 MarkupSafe==1.1.1 opentelemetry-api==0.18b0 opentelemetry-exporter-otlp==0.18b0 -opentelemetry-instrumentation==0.18b0 +opentelemetry-instrumentation==0.41b0 opentelemetry-instrumentation-flask==0.18b1 opentelemetry-instrumentation-wsgi==0.18b1 opentelemetry-sdk==0.18b0 From 5986c2512009b737ed4b6abc7b224ea8e2faea76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 16:31:46 +0000 Subject: [PATCH 1485/1517] Bump opentelemetry-instrumentation (#3456) --- docs/examples/fork-process-model/flask-uwsgi/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt index ab17c5debf..d763cc96d4 100644 --- a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt +++ b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt @@ -8,7 +8,7 @@ Jinja2==2.11.3 MarkupSafe==1.1.1 opentelemetry-api==0.18b0 opentelemetry-exporter-otlp==0.18b0 -opentelemetry-instrumentation==0.18b0 +opentelemetry-instrumentation==0.41b0 opentelemetry-instrumentation-flask==0.18b1 opentelemetry-instrumentation-wsgi==0.18b1 opentelemetry-sdk==0.18b0 From 0660924810d7c87b6ab07edc17dc000cec2014dc Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 11 Oct 2023 13:15:21 -0700 Subject: [PATCH 1486/1517] Update flask example dependencies (#3469) --- .../fork-process-model/flask-gunicorn/requirements.txt | 10 +++++----- .../fork-process-model/flask-uwsgi/requirements.txt | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt index d763cc96d4..8f7a7bbf31 100644 --- a/docs/examples/fork-process-model/flask-gunicorn/requirements.txt +++ b/docs/examples/fork-process-model/flask-gunicorn/requirements.txt @@ -6,12 +6,12 @@ gunicorn==20.0.4 itsdangerous==1.1.0 Jinja2==2.11.3 MarkupSafe==1.1.1 -opentelemetry-api==0.18b0 -opentelemetry-exporter-otlp==0.18b0 +opentelemetry-api==1.20.0 +opentelemetry-exporter-otlp==1.20.0 opentelemetry-instrumentation==0.41b0 -opentelemetry-instrumentation-flask==0.18b1 -opentelemetry-instrumentation-wsgi==0.18b1 -opentelemetry-sdk==0.18b0 +opentelemetry-instrumentation-flask==0.41b0 +opentelemetry-instrumentation-wsgi==0.41b0 +opentelemetry-sdk==1.20.0 protobuf==3.18.3 six==1.15.0 thrift==0.13.0 diff --git a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt index d763cc96d4..8f7a7bbf31 100644 --- a/docs/examples/fork-process-model/flask-uwsgi/requirements.txt +++ b/docs/examples/fork-process-model/flask-uwsgi/requirements.txt @@ -6,12 +6,12 @@ gunicorn==20.0.4 itsdangerous==1.1.0 Jinja2==2.11.3 MarkupSafe==1.1.1 -opentelemetry-api==0.18b0 -opentelemetry-exporter-otlp==0.18b0 +opentelemetry-api==1.20.0 +opentelemetry-exporter-otlp==1.20.0 opentelemetry-instrumentation==0.41b0 -opentelemetry-instrumentation-flask==0.18b1 -opentelemetry-instrumentation-wsgi==0.18b1 -opentelemetry-sdk==0.18b0 +opentelemetry-instrumentation-flask==0.41b0 +opentelemetry-instrumentation-wsgi==0.41b0 +opentelemetry-sdk==1.20.0 protobuf==3.18.3 six==1.15.0 thrift==0.13.0 From d054dff47d2da663a39b9656d106c3d15f344269 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 13 Oct 2023 17:27:19 -0600 Subject: [PATCH 1487/1517] Fix SumAggregation (#3390) Co-authored-by: Aaron Abbott --- CHANGELOG.md | 3 +- .../_internal/_view_instrument_match.py | 8 +- .../sdk/metrics/_internal/aggregation.py | 307 +++++++++--- .../integration_test/test_sum_aggregation.py | 443 ++++++++++++++++++ .../tests/metrics/test_aggregation.py | 26 +- 5 files changed, 698 insertions(+), 89 deletions(-) create mode 100644 opentelemetry-sdk/tests/metrics/integration_test/test_sum_aggregation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3faa61e06a..b81274a358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Fix `SumAggregation` + ([#3390](https://github.com/open-telemetry/opentelemetry-python/pull/3390)) - Fix handling of empty metric collection cycles ([#3335](https://github.com/open-telemetry/opentelemetry-python/pull/3335)) - Fix error when no LoggerProvider configured for LoggingHandler @@ -24,7 +26,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prometheus exporter support for auto instrumentation ([#3413](https://github.com/open-telemetry/opentelemetry-python/pull/3413)) - ## Version 1.20.0/0.41b0 (2023-09-04) - Modify Prometheus exporter to translate non-monotonic Sums into Gauges diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index 110f963a48..7dd7f58f27 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -74,8 +74,8 @@ def conflicts(self, other: "_ViewInstrumentMatch") -> bool: result and self._aggregation._instrument_is_monotonic == other._aggregation._instrument_is_monotonic - and self._aggregation._instrument_temporality - == other._aggregation._instrument_temporality + and self._aggregation._instrument_aggregation_temporality + == other._aggregation._instrument_aggregation_temporality ) return result @@ -124,7 +124,7 @@ def consume_measurement(self, measurement: Measurement) -> None: def collect( self, - aggregation_temporality: AggregationTemporality, + collection_aggregation_temporality: AggregationTemporality, collection_start_nanos: int, ) -> Optional[Sequence[DataPointT]]: @@ -132,7 +132,7 @@ def collect( with self._lock: for aggregation in self._attributes_aggregation.values(): data_point = aggregation.collect( - aggregation_temporality, collection_start_nanos + collection_aggregation_temporality, collection_start_nanos ) if data_point is not None: data_points.append(data_point) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py index ae21db907d..1f6d4c4c13 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py @@ -88,7 +88,7 @@ def aggregate(self, measurement: Measurement) -> None: @abstractmethod def collect( self, - aggregation_temporality: AggregationTemporality, + collection_aggregation_temporality: AggregationTemporality, collection_start_nano: int, ) -> Optional[_DataPointVarT]: pass @@ -100,7 +100,7 @@ def aggregate(self, measurement: Measurement) -> None: def collect( self, - aggregation_temporality: AggregationTemporality, + collection_aggregation_temporality: AggregationTemporality, collection_start_nano: int, ) -> Optional[_DataPointVarT]: pass @@ -111,89 +111,234 @@ def __init__( self, attributes: Attributes, instrument_is_monotonic: bool, - instrument_temporality: AggregationTemporality, + instrument_aggregation_temporality: AggregationTemporality, start_time_unix_nano: int, ): super().__init__(attributes) self._start_time_unix_nano = start_time_unix_nano - self._instrument_temporality = instrument_temporality + self._instrument_aggregation_temporality = ( + instrument_aggregation_temporality + ) self._instrument_is_monotonic = instrument_is_monotonic - if self._instrument_temporality is AggregationTemporality.DELTA: - self._value = 0 - else: - self._value = None + self._current_value = None + + self._previous_collection_start_nano = self._start_time_unix_nano + self._previous_cumulative_value = 0 def aggregate(self, measurement: Measurement) -> None: with self._lock: - if self._value is None: - self._value = 0 - self._value = self._value + measurement.value + if self._current_value is None: + self._current_value = 0 + + self._current_value = self._current_value + measurement.value def collect( self, - aggregation_temporality: AggregationTemporality, + collection_aggregation_temporality: AggregationTemporality, collection_start_nano: int, ) -> Optional[NumberDataPoint]: """ Atomically return a point for the current value of the metric and reset the aggregation value. + + Synchronous instruments have a method which is called directly with + increments for a given quantity: + + For example, an instrument that counts the amount of passengers in + every vehicle that crosses a certain point in a highway: + + synchronous_instrument.add(2) + collect(...) # 2 passengers are counted + synchronous_instrument.add(3) + collect(...) # 3 passengers are counted + synchronous_instrument.add(1) + collect(...) # 1 passenger is counted + + In this case the instrument aggregation temporality is DELTA because + every value represents an increment to the count, + + Asynchronous instruments have a callback which returns the total value + of a given quantity: + + For example, an instrument that measures the amount of bytes written to + a certain hard drive: + + callback() -> 1352 + collect(...) # 1352 bytes have been written so far + callback() -> 2324 + collect(...) # 2324 bytes have been written so far + callback() -> 4542 + collect(...) # 4542 bytes have been written so far + + In this case the instrument aggregation temporality is CUMULATIVE + because every value represents the total of the measurement. + + There is also the collection aggregation temporality, which is passed + to this method. The collection aggregation temporality defines the + nature of the returned value by this aggregation. + + When the collection aggregation temporality matches the + instrument aggregation temporality, then this method returns the + current value directly: + + synchronous_instrument.add(2) + collect(DELTA) -> 2 + synchronous_instrument.add(3) + collect(DELTA) -> 3 + synchronous_instrument.add(1) + collect(DELTA) -> 1 + + callback() -> 1352 + collect(CUMULATIVE) -> 1352 + callback() -> 2324 + collect(CUMULATIVE) -> 2324 + callback() -> 4542 + collect(CUMULATIVE) -> 4542 + + When the collection aggregation temporality does not match the + instrument aggregation temporality, then a conversion is made. For this + purpose, this aggregation keeps a private attribute, + self._previous_cumulative. + + When the instrument is synchronous: + + self._previous_cumulative_value is the sum of every previously + collected (delta) value. In this case, the returned (cumulative) value + will be: + + self._previous_cumulative_value + current_value + + synchronous_instrument.add(2) + collect(CUMULATIVE) -> 2 + synchronous_instrument.add(3) + collect(CUMULATIVE) -> 5 + synchronous_instrument.add(1) + collect(CUMULATIVE) -> 6 + + Also, as a diagram: + + time -> + + self._previous_cumulative_value + |-------------| + + current_value (delta) + |----| + + returned value (cumulative) + |------------------| + + When the instrument is asynchronous: + + self._previous_cumulative_value is the value of the previously + collected (cumulative) value. In this case, the returned (delta) value + will be: + + current_value - self._previous_cumulative_value + + callback() -> 1352 + collect(DELTA) -> 1352 + callback() -> 2324 + collect(DELTA) -> 972 + callback() -> 4542 + collect(DELTA) -> 2218 + + Also, as a diagram: + + time -> + + self._previous_cumulative_value + |-------------| + + current_value (cumulative) + |------------------| + + returned value (delta) + |----| """ - if self._instrument_temporality is AggregationTemporality.DELTA: - with self._lock: - value = self._value - start_time_unix_nano = self._start_time_unix_nano + with self._lock: + current_value = self._current_value + self._current_value = None - self._value = 0 - self._start_time_unix_nano = collection_start_nano + if ( + self._instrument_aggregation_temporality + is AggregationTemporality.DELTA + ): + # This happens when the corresponding instrument for this + # aggregation is synchronous. + if ( + collection_aggregation_temporality + is AggregationTemporality.DELTA + ): + + if current_value is None: + return None + + previous_collection_start_nano = ( + self._previous_collection_start_nano + ) + self._previous_collection_start_nano = ( + collection_start_nano + ) + + return NumberDataPoint( + attributes=self._attributes, + start_time_unix_nano=previous_collection_start_nano, + time_unix_nano=collection_start_nano, + value=current_value, + ) + + if current_value is None: + current_value = 0 + + self._previous_cumulative_value = ( + current_value + self._previous_cumulative_value + ) - else: + return NumberDataPoint( + attributes=self._attributes, + start_time_unix_nano=self._start_time_unix_nano, + time_unix_nano=collection_start_nano, + value=self._previous_cumulative_value, + ) - with self._lock: - if self._value is None: - return None - value = self._value - self._value = None - start_time_unix_nano = self._start_time_unix_nano + # This happens when the corresponding instrument for this + # aggregation is asynchronous. - current_point = NumberDataPoint( - attributes=self._attributes, - start_time_unix_nano=start_time_unix_nano, - time_unix_nano=collection_start_nano, - value=value, - ) + if current_value is None: + # This happens when the corresponding instrument callback + # does not produce measurements. + return None - if self._previous_point is None or ( - self._instrument_temporality is aggregation_temporality - ): - # Output DELTA for a synchronous instrument - # Output CUMULATIVE for an asynchronous instrument - self._previous_point = current_point - return current_point + if ( + collection_aggregation_temporality + is AggregationTemporality.DELTA + ): + result_value = current_value - self._previous_cumulative_value - if aggregation_temporality is AggregationTemporality.DELTA: - # Output temporality DELTA for an asynchronous instrument - value = current_point.value - self._previous_point.value - output_start_time_unix_nano = self._previous_point.time_unix_nano + self._previous_cumulative_value = current_value - else: - # Output CUMULATIVE for a synchronous instrument - value = current_point.value + self._previous_point.value - output_start_time_unix_nano = ( - self._previous_point.start_time_unix_nano - ) + previous_collection_start_nano = ( + self._previous_collection_start_nano + ) + self._previous_collection_start_nano = collection_start_nano - current_point = NumberDataPoint( - attributes=self._attributes, - start_time_unix_nano=output_start_time_unix_nano, - time_unix_nano=current_point.time_unix_nano, - value=value, - ) + return NumberDataPoint( + attributes=self._attributes, + start_time_unix_nano=previous_collection_start_nano, + time_unix_nano=collection_start_nano, + value=result_value, + ) - self._previous_point = current_point - return current_point + return NumberDataPoint( + attributes=self._attributes, + start_time_unix_nano=self._start_time_unix_nano, + time_unix_nano=collection_start_nano, + value=current_value, + ) class _LastValueAggregation(_Aggregation[Gauge]): @@ -207,7 +352,7 @@ def aggregate(self, measurement: Measurement): def collect( self, - aggregation_temporality: AggregationTemporality, + collection_aggregation_temporality: AggregationTemporality, collection_start_nano: int, ) -> Optional[_DataPointVarT]: """ @@ -263,7 +408,7 @@ def __init__( # Histogram instrument is DELTA, like the "natural" aggregation # temporality for a Counter is DELTA and the "natural" aggregation # temporality for an ObservableCounter is CUMULATIVE. - self._instrument_temporality = AggregationTemporality.DELTA + self._instrument_aggregation_temporality = AggregationTemporality.DELTA def _get_empty_bucket_counts(self) -> List[int]: return [0] * (len(self._boundaries) + 1) @@ -282,7 +427,7 @@ def aggregate(self, measurement: Measurement) -> None: def collect( self, - aggregation_temporality: AggregationTemporality, + collection_aggregation_temporality: AggregationTemporality, collection_start_nano: int, ) -> Optional[_DataPointVarT]: """ @@ -317,7 +462,8 @@ def collect( ) if self._previous_point is None or ( - self._instrument_temporality is aggregation_temporality + self._instrument_aggregation_temporality + is collection_aggregation_temporality ): self._previous_point = current_point return current_point @@ -325,7 +471,10 @@ def collect( max_ = current_point.max min_ = current_point.min - if aggregation_temporality is AggregationTemporality.CUMULATIVE: + if ( + collection_aggregation_temporality + is AggregationTemporality.CUMULATIVE + ): start_time_unix_nano = self._previous_point.start_time_unix_nano sum_ = current_point.sum + self._previous_point.sum # Only update min/max on delta -> cumulative @@ -439,7 +588,7 @@ def __init__( ) self._mapping = LogarithmMapping(self._max_scale) - self._instrument_temporality = AggregationTemporality.DELTA + self._instrument_aggregation_temporality = AggregationTemporality.DELTA self._start_time_unix_nano = start_time_unix_nano self._previous_scale = None @@ -560,7 +709,7 @@ def aggregate(self, measurement: Measurement) -> None: def collect( self, - aggregation_temporality: AggregationTemporality, + collection_aggregation_temporality: AggregationTemporality, collection_start_nano: int, ) -> Optional[_DataPointVarT]: """ @@ -623,7 +772,8 @@ def collect( ) if self._previous_scale is None or ( - self._instrument_temporality is aggregation_temporality + self._instrument_aggregation_temporality + is collection_aggregation_temporality ): self._previous_scale = current_scale self._previous_start_time_unix_nano = ( @@ -662,7 +812,10 @@ def collect( self._previous_negative, ) - if aggregation_temporality is AggregationTemporality.CUMULATIVE: + if ( + collection_aggregation_temporality + is AggregationTemporality.CUMULATIVE + ): start_time_unix_nano = self._previous_start_time_unix_nano sum_ = current_sum + self._previous_sum @@ -675,14 +828,14 @@ def collect( current_positive, current_scale, min_scale, - aggregation_temporality, + collection_aggregation_temporality, ) self._merge( self._previous_negative, current_negative, current_scale, min_scale, - aggregation_temporality, + collection_aggregation_temporality, ) else: @@ -696,14 +849,14 @@ def collect( current_positive, current_scale, min_scale, - aggregation_temporality, + collection_aggregation_temporality, ) self._merge( self._previous_negative, current_negative, current_scale, min_scale, - aggregation_temporality, + collection_aggregation_temporality, ) current_point = ExponentialHistogramDataPoint( @@ -908,14 +1061,18 @@ def _create_aggregation( return _SumAggregation( attributes, instrument_is_monotonic=True, - instrument_temporality=AggregationTemporality.DELTA, + instrument_aggregation_temporality=( + AggregationTemporality.DELTA + ), start_time_unix_nano=start_time_unix_nano, ) if isinstance(instrument, UpDownCounter): return _SumAggregation( attributes, instrument_is_monotonic=False, - instrument_temporality=AggregationTemporality.DELTA, + instrument_aggregation_temporality=( + AggregationTemporality.DELTA + ), start_time_unix_nano=start_time_unix_nano, ) @@ -923,7 +1080,9 @@ def _create_aggregation( return _SumAggregation( attributes, instrument_is_monotonic=True, - instrument_temporality=AggregationTemporality.CUMULATIVE, + instrument_aggregation_temporality=( + AggregationTemporality.CUMULATIVE + ), start_time_unix_nano=start_time_unix_nano, ) @@ -931,7 +1090,9 @@ def _create_aggregation( return _SumAggregation( attributes, instrument_is_monotonic=False, - instrument_temporality=AggregationTemporality.CUMULATIVE, + instrument_aggregation_temporality=( + AggregationTemporality.CUMULATIVE + ), start_time_unix_nano=start_time_unix_nano, ) diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_sum_aggregation.py b/opentelemetry-sdk/tests/metrics/integration_test/test_sum_aggregation.py new file mode 100644 index 0000000000..708b44f5fe --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_sum_aggregation.py @@ -0,0 +1,443 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from itertools import count +from logging import ERROR +from platform import system +from unittest import TestCase + +from pytest import mark + +from opentelemetry.metrics import Observation +from opentelemetry.sdk.metrics import Counter, MeterProvider, ObservableCounter +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + InMemoryMetricReader, +) +from opentelemetry.sdk.metrics.view import SumAggregation + + +class TestSumAggregation(TestCase): + @mark.skipif( + system() != "Linux", + reason=( + "Tests fail because Windows time_ns resolution is too low so " + "two different time measurements may end up having the exact same" + "value." + ), + ) + def test_asynchronous_delta_temporality(self): + + eight_multiple_generator = count(start=8, step=8) + + counter = 0 + + def observable_counter_callback(callback_options): + nonlocal counter + counter += 1 + + if counter < 11: + yield + + elif counter < 21: + yield Observation(next(eight_multiple_generator)) + + else: + yield + + aggregation = SumAggregation() + + reader = InMemoryMetricReader( + preferred_aggregation={ObservableCounter: aggregation}, + preferred_temporality={ + ObservableCounter: AggregationTemporality.DELTA + }, + ) + + provider = MeterProvider(metric_readers=[reader]) + meter = provider.get_meter("name", "version") + + meter.create_observable_counter( + "observable_counter", [observable_counter_callback] + ) + + results = [] + + for _ in range(10): + with self.assertLogs(level=ERROR): + results.append(reader.get_metrics_data()) + + self.assertEqual(counter, 10) + + for metrics_data in results: + self.assertIsNone(metrics_data) + + results = [] + + for _ in range(10): + results.append(reader.get_metrics_data()) + + self.assertEqual(counter, 20) + + previous_time_unix_nano = ( + results[0] + .resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .time_unix_nano + ) + + self.assertEqual( + ( + results[0] + .resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .value + ), + 8, + ) + + self.assertLess( + ( + results[0] + .resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .start_time_unix_nano + ), + previous_time_unix_nano, + ) + + for metrics_data in results[1:]: + + metric_data = ( + metrics_data.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + ) + + self.assertEqual( + previous_time_unix_nano, metric_data.start_time_unix_nano + ) + previous_time_unix_nano = metric_data.time_unix_nano + self.assertEqual(metric_data.value, 8) + self.assertLess( + metric_data.start_time_unix_nano, metric_data.time_unix_nano + ) + + results = [] + + for _ in range(10): + with self.assertLogs(level=ERROR): + results.append(reader.get_metrics_data()) + + self.assertEqual(counter, 30) + + provider.shutdown() + + for metrics_data in results: + self.assertIsNone(metrics_data) + + @mark.skipif( + system() != "Linux", + reason=( + "Tests fail because Windows time_ns resolution is too low so " + "two different time measurements may end up having the exact same" + "value." + ), + ) + def test_asynchronous_cumulative_temporality(self): + + eight_multiple_generator = count(start=8, step=8) + + counter = 0 + + def observable_counter_callback(callback_options): + nonlocal counter + counter += 1 + + if counter < 11: + yield + + elif counter < 21: + yield Observation(next(eight_multiple_generator)) + + else: + yield + + aggregation = SumAggregation() + + reader = InMemoryMetricReader( + preferred_aggregation={ObservableCounter: aggregation}, + preferred_temporality={ + ObservableCounter: AggregationTemporality.CUMULATIVE + }, + ) + + provider = MeterProvider(metric_readers=[reader]) + meter = provider.get_meter("name", "version") + + meter.create_observable_counter( + "observable_counter", [observable_counter_callback] + ) + + results = [] + + for _ in range(10): + with self.assertLogs(level=ERROR): + results.append(reader.get_metrics_data()) + + self.assertEqual(counter, 10) + + for metrics_data in results: + self.assertIsNone(metrics_data) + + results = [] + + for _ in range(10): + results.append(reader.get_metrics_data()) + + self.assertEqual(counter, 20) + + start_time_unix_nano = ( + results[0] + .resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .start_time_unix_nano + ) + + for index, metrics_data in enumerate(results): + + metric_data = ( + metrics_data.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + ) + + self.assertEqual( + start_time_unix_nano, metric_data.start_time_unix_nano + ) + self.assertEqual(metric_data.value, 8 * (index + 1)) + + results = [] + + for _ in range(10): + with self.assertLogs(level=ERROR): + results.append(reader.get_metrics_data()) + + self.assertEqual(counter, 30) + + provider.shutdown() + + for metrics_data in results: + self.assertIsNone(metrics_data) + + @mark.skipif( + system() != "Linux", + reason=( + "Tests fail because Windows time_ns resolution is too low so " + "two different time measurements may end up having the exact same" + "value." + ), + ) + def test_synchronous_delta_temporality(self): + + aggregation = SumAggregation() + + reader = InMemoryMetricReader( + preferred_aggregation={Counter: aggregation}, + preferred_temporality={Counter: AggregationTemporality.DELTA}, + ) + + provider = MeterProvider(metric_readers=[reader]) + meter = provider.get_meter("name", "version") + + counter = meter.create_counter("counter") + + results = [] + + for _ in range(10): + + results.append(reader.get_metrics_data()) + + for metrics_data in results: + self.assertIsNone(metrics_data) + + results = [] + + for _ in range(10): + counter.add(8) + results.append(reader.get_metrics_data()) + + previous_time_unix_nano = ( + results[0] + .resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .time_unix_nano + ) + + self.assertEqual( + ( + results[0] + .resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .value + ), + 8, + ) + + self.assertLess( + ( + results[0] + .resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .start_time_unix_nano + ), + previous_time_unix_nano, + ) + + for metrics_data in results[1:]: + + metric_data = ( + metrics_data.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + ) + + self.assertEqual( + previous_time_unix_nano, metric_data.start_time_unix_nano + ) + previous_time_unix_nano = metric_data.time_unix_nano + self.assertEqual(metric_data.value, 8) + self.assertLess( + metric_data.start_time_unix_nano, metric_data.time_unix_nano + ) + + results = [] + + for _ in range(10): + + results.append(reader.get_metrics_data()) + + provider.shutdown() + + for metrics_data in results: + self.assertIsNone(metrics_data) + + @mark.skipif( + system() != "Linux", + reason=( + "Tests fail because Windows time_ns resolution is too low so " + "two different time measurements may end up having the exact same" + "value." + ), + ) + def test_synchronous_cumulative_temporality(self): + + aggregation = SumAggregation() + + reader = InMemoryMetricReader( + preferred_aggregation={Counter: aggregation}, + preferred_temporality={Counter: AggregationTemporality.CUMULATIVE}, + ) + + provider = MeterProvider(metric_readers=[reader]) + meter = provider.get_meter("name", "version") + + counter = meter.create_counter("counter") + + results = [] + + for _ in range(10): + + results.append(reader.get_metrics_data()) + + for metrics_data in results: + self.assertIsNone(metrics_data) + + results = [] + + for _ in range(10): + + counter.add(8) + results.append(reader.get_metrics_data()) + + start_time_unix_nano = ( + results[0] + .resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .start_time_unix_nano + ) + + for index, metrics_data in enumerate(results): + + metric_data = ( + metrics_data.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + ) + + self.assertEqual( + start_time_unix_nano, metric_data.start_time_unix_nano + ) + self.assertEqual(metric_data.value, 8 * (index + 1)) + + results = [] + + for _ in range(10): + + results.append(reader.get_metrics_data()) + + provider.shutdown() + + start_time_unix_nano = ( + results[0] + .resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .start_time_unix_nano + ) + + for metrics_data in results: + + metric_data = ( + metrics_data.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + ) + + self.assertEqual( + start_time_unix_nano, metric_data.start_time_unix_nano + ) + self.assertEqual(metric_data.value, 80) diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index 9c9de1f2cb..b7cfc63cd4 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -65,7 +65,7 @@ def test_aggregate_delta(self): synchronous_sum_aggregation.aggregate(measurement(2)) synchronous_sum_aggregation.aggregate(measurement(3)) - self.assertEqual(synchronous_sum_aggregation._value, 6) + self.assertEqual(synchronous_sum_aggregation._current_value, 6) synchronous_sum_aggregation = _SumAggregation( Mock(), True, AggregationTemporality.DELTA, 0 @@ -75,7 +75,7 @@ def test_aggregate_delta(self): synchronous_sum_aggregation.aggregate(measurement(-2)) synchronous_sum_aggregation.aggregate(measurement(3)) - self.assertEqual(synchronous_sum_aggregation._value, 2) + self.assertEqual(synchronous_sum_aggregation._current_value, 2) def test_aggregate_cumulative(self): """ @@ -90,7 +90,7 @@ def test_aggregate_cumulative(self): synchronous_sum_aggregation.aggregate(measurement(2)) synchronous_sum_aggregation.aggregate(measurement(3)) - self.assertEqual(synchronous_sum_aggregation._value, 6) + self.assertEqual(synchronous_sum_aggregation._current_value, 6) synchronous_sum_aggregation = _SumAggregation( Mock(), True, AggregationTemporality.CUMULATIVE, 0 @@ -100,7 +100,7 @@ def test_aggregate_cumulative(self): synchronous_sum_aggregation.aggregate(measurement(-2)) synchronous_sum_aggregation.aggregate(measurement(3)) - self.assertEqual(synchronous_sum_aggregation._value, 2) + self.assertEqual(synchronous_sum_aggregation._current_value, 2) def test_collect_delta(self): """ @@ -409,7 +409,8 @@ def test_sum_factory(self): self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) self.assertEqual( - aggregation._instrument_temporality, AggregationTemporality.DELTA + aggregation._instrument_aggregation_temporality, + AggregationTemporality.DELTA, ) aggregation2 = factory._create_aggregation(counter, Mock(), 0) self.assertNotEqual(aggregation, aggregation2) @@ -420,7 +421,8 @@ def test_sum_factory(self): self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) self.assertEqual( - aggregation._instrument_temporality, AggregationTemporality.DELTA + aggregation._instrument_aggregation_temporality, + AggregationTemporality.DELTA, ) counter = _ObservableCounter("name", Mock(), Mock(), None) @@ -429,7 +431,7 @@ def test_sum_factory(self): self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) self.assertEqual( - aggregation._instrument_temporality, + aggregation._instrument_aggregation_temporality, AggregationTemporality.CUMULATIVE, ) @@ -471,7 +473,8 @@ def test_counter(self): self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) self.assertEqual( - aggregation._instrument_temporality, AggregationTemporality.DELTA + aggregation._instrument_aggregation_temporality, + AggregationTemporality.DELTA, ) def test_up_down_counter(self): @@ -482,7 +485,8 @@ def test_up_down_counter(self): self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) self.assertEqual( - aggregation._instrument_temporality, AggregationTemporality.DELTA + aggregation._instrument_aggregation_temporality, + AggregationTemporality.DELTA, ) def test_observable_counter(self): @@ -495,7 +499,7 @@ def test_observable_counter(self): self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) self.assertEqual( - aggregation._instrument_temporality, + aggregation._instrument_aggregation_temporality, AggregationTemporality.CUMULATIVE, ) @@ -511,7 +515,7 @@ def test_observable_up_down_counter(self): self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) self.assertEqual( - aggregation._instrument_temporality, + aggregation._instrument_aggregation_temporality, AggregationTemporality.CUMULATIVE, ) From 4a2cb86f2885159d9e3f8d5f04fe409640df23e4 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 3 Nov 2023 13:18:09 -0600 Subject: [PATCH 1488/1517] Fix Jaeger exporter and getting started testing failures (#3498) Fixes #3497 Fixes #3501 --- docs/getting_started/tests/requirements.txt | 27 +++++++++++++++++++ .../pyproject.toml | 1 + tox.ini | 5 ++-- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 docs/getting_started/tests/requirements.txt diff --git a/docs/getting_started/tests/requirements.txt b/docs/getting_started/tests/requirements.txt new file mode 100644 index 0000000000..c4c62067ac --- /dev/null +++ b/docs/getting_started/tests/requirements.txt @@ -0,0 +1,27 @@ +asgiref==3.7.2 +attrs==23.1.0 +certifi==2023.7.22 +charset-normalizer==2.0.12 +click==8.1.7 +Deprecated==1.2.14 +flaky==3.7.0 +Flask==2.0.1 +idna==3.4 +importlib-metadata==6.8.0 +iniconfig==2.0.0 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.3 +packaging==23.2 +pluggy==1.3.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +requests==2.26.0 +tomli==2.0.1 +typing_extensions==4.8.0 +urllib3==1.26.18 +Werkzeug==2.3.7 +wrapt==1.15.0 +zipp==3.17.0 diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml index 131f12a0b4..7a0c628d36 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ + "protobuf ~= 3.20.0", "googleapis-common-protos ~= 1.52, < 1.60.0", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.3", diff --git a/tox.ini b/tox.ini index 4bb52cdc66..e6b8c8aeb5 100644 --- a/tox.ini +++ b/tox.ini @@ -19,8 +19,7 @@ envlist = pypy3-opentelemetry-semantic-conventions ; docs/getting-started - py3{7,8,9,10,11}-opentelemetry-getting-started - pypy3-opentelemetry-getting-started + py3{8,9,10,11}-opentelemetry-getting-started py3{7,8,9,10,11}-opentelemetry-opentracing-shim pypy3-opentelemetry-opentracing-shim @@ -132,7 +131,7 @@ commands_pre = protobuf: pip install {toxinidir}/opentelemetry-proto - getting-started: pip install requests==2.26.0 flask==2.0.1 + getting-started: pip install -r requirements.txt getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http" getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation" getting-started: pip install -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-requests&subdirectory=instrumentation/opentelemetry-instrumentation-requests" From edaaade9b9a7206e73f0b71c6b48470d86c4c3e3 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 3 Nov 2023 14:21:10 -0600 Subject: [PATCH 1489/1517] Add -ra option to pytest runs (#3496) --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 88c1675c47..db2aee75ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,7 +62,7 @@ jobs: if: ${{ matrix.os == 'windows-2019'}} run: git config --system core.longpaths true - name: run tox - run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- --benchmark-json=${{ + run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- -ra --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json # - name: Find and merge benchmarks # id: find_and_merge_benchmarks @@ -159,4 +159,4 @@ jobs: key: v3-tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib - name: run tox - run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} + run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- -ra From 952339c6492d98de94d379eca173a16deddda8ca Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 3 Nov 2023 15:02:58 -0600 Subject: [PATCH 1490/1517] Add benchmarks workflow (#3450) --- .github/workflows/benchmarks.yml | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/benchmarks.yml diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 0000000000..2d7a6efd73 --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,54 @@ +name: SDK Benchmark Tests + +on: + push: + branches: [ main ] + +jobs: + sdk-benchmarks: + env: + py311: "3.11" + RUN_MATRIX_COMBINATION: ${{ matrix.python-version }}-sdk-${{ matrix.os }} + runs-on: self-hosted + strategy: + # Ensures the entire test matrix is run, even if one permutation fails + fail-fast: false + matrix: + python-version: [py311] + os: [ubuntu-20.04, windows-2019] + steps: + - name: Checkout Core Repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v2 + - name: Set up Python ${{ env[matrix.python-version] }} + uses: actions/setup-python@v2 + with: + python-version: ${{ env[matrix.python-version] }} + architecture: 'x64' + - name: Install tox + run: pip install tox==3.27.1 -U tox-factor + - name: Cache tox environment + # Preserves .tox directory between runs for faster installs + uses: actions/cache@v2 + with: + path: | + .tox + ~/.cache/pip + key: v3-tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', + 'dev-requirements.txt') }}-core + - name: Run tox + run: tox -f ${{ matrix.python-version }}-sdk -- -k opentelemetry-sdk/tests/performance/benchmarks --benchmark-json=output.json + - name: Report on SDK benchmark results + uses: benchmark-action/github-action-benchmark@v1 + with: + name: OpenTelemetry Python SDK Benchmarks - Python ${{ env[matrix.python-version ]}} - SDK + tool: pytest + output-file-path: opentelemetry-sdk/tests/output.json + gh-pages-branch: benchmarks + github-token: ${{ secrets.GITHUB_TOKEN }} + # Make a commit on `gh-pages` with benchmarks from previous step + benchmark-data-dir-path: "benchmarks" + auto-push: true + max-items-in-chart: 100 + # Alert with a commit comment on possible performance regression + alert-threshold: '200%' + comment-on-alert: true From c2a1bcfa1eb7e1fc8b1a27e3b8a30d4aa31be9f0 Mon Sep 17 00:00:00 2001 From: Andreas Fehlner Date: Mon, 6 Nov 2023 18:08:11 +0100 Subject: [PATCH 1491/1517] Update README.md, missing , (#3479) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f42fd0bd5..d925fffe25 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Emeritus Approvers - [Héctor Hernández](https://github.com/hectorhdzg), Microsoft - [Mauricio Vásquez](https://github.com/mauriciovasquezbernal), Kinvolk - [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), AWS -- [Tahir H. Butt](https://github.com/majorgreys) DataDog +- [Tahir H. Butt](https://github.com/majorgreys), DataDog *For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver).* From 6676a28e339313b6ddfa9581f663b9cce41d55c1 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 6 Nov 2023 12:14:53 -0600 Subject: [PATCH 1492/1517] Remove old benchmarks (#3511) --- .github/workflows/test.yml | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db2aee75ea..218098b82e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,29 +64,6 @@ jobs: - name: run tox run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- -ra --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json -# - name: Find and merge benchmarks -# id: find_and_merge_benchmarks -# run: >- -# jq -s '.[0].benchmarks = ([.[].benchmarks] | add) -# | if .[0].benchmarks == null then null else .[0] end' -# $(find . -name '*${{ matrix.package }}*-benchmark.json') > output.json -# && echo "json_plaintext=$(cat output.json)" >> $GITHUB_OUTPUT -# - name: Report on benchmark results -# if: steps.find_and_merge_benchmarks.outputs.json_plaintext != 'null' -# uses: rhysd/github-action-benchmark@v1 -# with: -# name: OpenTelemetry Python Benchmarks - Python ${{ env[matrix.python-version ]}} - ${{ matrix.package }} -# tool: pytest -# output-file-path: output.json -# github-token: ${{ secrets.GITHUB_TOKEN }} -# max-items-in-chart: 100 -# # Alert with a commit comment on possible performance regression -# alert-threshold: 200% -# fail-on-alert: true -# # Make a commit on `gh-pages` with benchmarks from previous step -# auto-push: ${{ github.ref == 'refs/heads/main' }} -# gh-pages-branch: gh-pages -# benchmark-data-dir-path: benchmarks misc: strategy: fail-fast: false From fcaa6c08fe909921309414d3ba24b593dbd4cf3c Mon Sep 17 00:00:00 2001 From: Koichi Shiraishi Date: Tue, 7 Nov 2023 03:50:16 +0900 Subject: [PATCH 1493/1517] Fix argument name to logger_provider on set_logger_provider (#3476) --- .../src/opentelemetry/_logs/_internal/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index f5f9437f14..4c25eed074 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -202,13 +202,13 @@ def set_lp() -> None: _logger.warning("Overriding of current LoggerProvider is not allowed") -def set_logger_provider(meter_provider: LoggerProvider) -> None: +def set_logger_provider(logger_provider: LoggerProvider) -> None: """Sets the current global :class:`~.LoggerProvider` object. This can only be done once, a warning will be logged if any further attempt is made. """ - _set_logger_provider(meter_provider, log=True) + _set_logger_provider(logger_provider, log=True) def get_logger( From 2a8d4ed433feb8f4ba8db141643773e6f097bdca Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 6 Nov 2023 14:48:02 -0600 Subject: [PATCH 1494/1517] Remove testing for Jaeger exporter (#3509) --- .../README.rst | 2 ++ .../pyproject.toml | 1 - .../opentelemetry-exporter-jaeger-thrift/README.rst | 2 ++ exporter/opentelemetry-exporter-jaeger/README.rst | 2 ++ tox.ini | 13 ------------- 5 files changed, 6 insertions(+), 14 deletions(-) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst b/exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst index 3f170bb12a..f371fd0c3e 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst @@ -5,6 +5,8 @@ OpenTelemetry Jaeger Protobuf Exporter Since v1.35, the Jaeger supports OTLP natively. Please use the OTLP exporter instead. Support for this exporter will end July 2023. + This package is no longer being tested. + |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger-proto-grpc.svg diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml index 7a0c628d36..131f12a0b4 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml @@ -26,7 +26,6 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "protobuf ~= 3.20.0", "googleapis-common-protos ~= 1.52, < 1.60.0", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.3", diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/README.rst b/exporter/opentelemetry-exporter-jaeger-thrift/README.rst index 1b01237ce4..d6930f624f 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/README.rst +++ b/exporter/opentelemetry-exporter-jaeger-thrift/README.rst @@ -5,6 +5,8 @@ OpenTelemetry Jaeger Thrift Exporter Since v1.35, the Jaeger supports OTLP natively. Please use the OTLP exporter instead. Support for this exporter will end July 2023. + This package is no longer being tested. + |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger-thrift.svg diff --git a/exporter/opentelemetry-exporter-jaeger/README.rst b/exporter/opentelemetry-exporter-jaeger/README.rst index 43d0f76d0e..9f0cec9250 100644 --- a/exporter/opentelemetry-exporter-jaeger/README.rst +++ b/exporter/opentelemetry-exporter-jaeger/README.rst @@ -5,6 +5,8 @@ OpenTelemetry Jaeger Exporter Since v1.35, the Jaeger supports OTLP natively. Please use the OTLP exporter instead. Support for this exporter will end July 2023. + This package is no longer being tested. + |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger.svg diff --git a/tox.ini b/tox.ini index e6b8c8aeb5..351054f26e 100644 --- a/tox.ini +++ b/tox.ini @@ -27,12 +27,6 @@ envlist = py3{7,8,9,10,11}-opentelemetry-opencensus-shim ; opencensus-shim intentionally excluded from pypy3 (grpcio install fails) - py3{7,8,9,10,11}-opentelemetry-exporter-jaeger-combined - - py3{7,8,9,10,11}-opentelemetry-exporter-jaeger-proto-grpc - - py3{7,8,9,10,11}-opentelemetry-exporter-jaeger-thrift - py3{7,8,9,10,11}-opentelemetry-exporter-opencensus ; exporter-opencensus intentionally excluded from pypy3 @@ -106,9 +100,6 @@ changedir = opentracing-shim: shim/opentelemetry-opentracing-shim/tests opencensus-shim: shim/opentelemetry-opencensus-shim/tests - exporter-jaeger-combined: exporter/opentelemetry-exporter-jaeger/tests - exporter-jaeger-proto-grpc: exporter/opentelemetry-exporter-jaeger-proto-grpc/tests - exporter-jaeger-thrift: exporter/opentelemetry-exporter-jaeger-thrift/tests exporter-opencensus: exporter/opentelemetry-exporter-opencensus/tests exporter-otlp-proto-common: exporter/opentelemetry-exporter-otlp-proto-common/tests exporter-otlp-combined: exporter/opentelemetry-exporter-otlp/tests @@ -157,10 +148,6 @@ commands_pre = exporter-otlp-proto-http: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common exporter-otlp-proto-http: pip install {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http[test] - exporter-jaeger-combined: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift {toxinidir}/exporter/opentelemetry-exporter-jaeger - exporter-jaeger-proto-grpc: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc - exporter-jaeger-thrift: pip install {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift - opentracing-shim: pip install {toxinidir}/opentelemetry-sdk opentracing-shim: pip install {toxinidir}/shim/opentelemetry-opentracing-shim From a74e82baa5431e4d5c0fae1a6de24b8391c69e9d Mon Sep 17 00:00:00 2001 From: Pablo Collins Date: Mon, 6 Nov 2023 18:21:05 -0500 Subject: [PATCH 1495/1517] Tweak sdk tests so they run faster (#3513) --- .../tests/trace/export/test_export.py | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 7784ef4c9d..8175c09f59 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -33,6 +33,7 @@ OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_SCHEDULE_DELAY, ) +from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import export from opentelemetry.sdk.trace.export import logger from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( @@ -151,7 +152,7 @@ def test_simple_span_processor_not_sampled(self): self.assertListEqual([], spans_names_list) -def _create_start_and_end_span(name, span_processor): +def _create_start_and_end_span(name, span_processor, resource): span = trace._Span( name, trace_api.SpanContext( @@ -161,6 +162,7 @@ def _create_start_and_end_span(name, span_processor): trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ), span_processor=span_processor, + resource=resource, ) span.start() span.end() @@ -245,8 +247,9 @@ def test_shutdown(self): span_names = ["xxx", "bar", "foo"] + resource = Resource.create({}) for name in span_names: - _create_start_and_end_span(name, span_processor) + _create_start_and_end_span(name, span_processor, resource) span_processor.shutdown() self.assertTrue(my_exporter.is_shutdown) @@ -264,15 +267,16 @@ def test_flush(self): span_names0 = ["xxx", "bar", "foo"] span_names1 = ["yyy", "baz", "fox"] + resource = Resource.create({}) for name in span_names0: - _create_start_and_end_span(name, span_processor) + _create_start_and_end_span(name, span_processor, resource) self.assertTrue(span_processor.force_flush()) self.assertListEqual(span_names0, spans_names_list) # create some more spans to check that span processor still works for name in span_names1: - _create_start_and_end_span(name, span_processor) + _create_start_and_end_span(name, span_processor, resource) self.assertTrue(span_processor.force_flush()) self.assertListEqual(span_names0 + span_names1, spans_names_list) @@ -298,10 +302,12 @@ def test_flush_from_multiple_threads(self): my_exporter, max_queue_size=512, max_export_batch_size=128 ) + resource = Resource.create({}) + def create_spans_and_flush(tno: int): for span_idx in range(num_spans): _create_start_and_end_span( - f"Span {tno}-{span_idx}", span_processor + f"Span {tno}-{span_idx}", span_processor, resource ) self.assertTrue(span_processor.force_flush()) @@ -323,7 +329,8 @@ def test_flush_timeout(self): ) span_processor = export.BatchSpanProcessor(my_exporter) - _create_start_and_end_span("foo", span_processor) + resource = Resource.create({}) + _create_start_and_end_span("foo", span_processor, resource) # check that the timeout is not meet with self.assertLogs(level=WARNING): @@ -341,8 +348,9 @@ def test_batch_span_processor_lossless(self): my_exporter, max_queue_size=512, max_export_batch_size=128 ) + resource = Resource.create({}) for _ in range(512): - _create_start_and_end_span("foo", span_processor) + _create_start_and_end_span("foo", span_processor, resource) time.sleep(1) self.assertTrue(span_processor.force_flush()) @@ -363,9 +371,10 @@ def test_batch_span_processor_many_spans(self): schedule_delay_millis=100, ) + resource = Resource.create({}) for _ in range(4): for _ in range(256): - _create_start_and_end_span("foo", span_processor) + _create_start_and_end_span("foo", span_processor, resource) time.sleep(0.1) # give some time for the exporter to upload spans @@ -467,7 +476,8 @@ def test_batch_span_processor_scheduled_delay(self): ) # create single span - _create_start_and_end_span("foo", span_processor) + resource = Resource.create({}) + _create_start_and_end_span("foo", span_processor, resource) self.assertTrue(export_event.wait(2)) export_time = time.time() @@ -497,7 +507,8 @@ def test_batch_span_processor_reset_timeout(self): ) with mock.patch.object(span_processor.condition, "wait") as mock_wait: - _create_start_and_end_span("foo", span_processor) + resource = Resource.create({}) + _create_start_and_end_span("foo", span_processor, resource) self.assertTrue(export_event.wait(2)) # give some time for exporter to loop From 91ac1bea6ed2b50e7e17b91cec2770674f869517 Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:06:00 -0300 Subject: [PATCH 1496/1517] Update version to 1.22.0.dev/0.43b0.dev (#3493) Co-authored-by: Diego Hurtado --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ eachdist.ini | 4 ++-- .../src/opentelemetry/exporter/jaeger/proto/grpc/version.py | 2 +- .../src/opentelemetry/exporter/jaeger/thrift/version.py | 2 +- exporter/opentelemetry-exporter-jaeger/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/jaeger/version.py | 2 +- exporter/opentelemetry-exporter-opencensus/pyproject.toml | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-common/pyproject.toml | 2 +- .../src/opentelemetry/exporter/otlp/proto/common/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-grpc/pyproject.toml | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-http/pyproject.toml | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-prometheus/pyproject.toml | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/pyproject.toml | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/pyproject.toml | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opencensus-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opencensus/version.py | 2 +- shim/opentelemetry-opentracing-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/pyproject.toml | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 37 files changed, 48 insertions(+), 46 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 218098b82e..b929cd05a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 481972cf87c1506e789f586843ba82d93633eac7 + CONTRIB_REPO_SHA: 21b26b39c62b27b116978813d47b9b7d104b54be # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index b81274a358..db8b1b26d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## Version 1.21.0/0.42b0 (2023-11-01) + - Fix `SumAggregation`  ([#3390](https://github.com/open-telemetry/opentelemetry-python/pull/3390)) - Fix handling of empty metric collection cycles diff --git a/eachdist.ini b/eachdist.ini index 884bff2242..3a2205712b 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.21.0.dev +version=1.22.0.dev packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.42b0.dev +version=0.43b0.dev packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index e755ddf0c9..f2d56fac64 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index e755ddf0c9..f2d56fac64 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger/pyproject.toml b/exporter/opentelemetry-exporter-jaeger/pyproject.toml index 996bf1a672..755e36c51e 100644 --- a/exporter/opentelemetry-exporter-jaeger/pyproject.toml +++ b/exporter/opentelemetry-exporter-jaeger/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-jaeger-proto-grpc == 1.21.0.dev", - "opentelemetry-exporter-jaeger-thrift == 1.21.0.dev", + "opentelemetry-exporter-jaeger-proto-grpc == 1.22.0.dev", + "opentelemetry-exporter-jaeger-thrift == 1.22.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index e755ddf0c9..f2d56fac64 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/exporter/opentelemetry-exporter-opencensus/pyproject.toml b/exporter/opentelemetry-exporter-opencensus/pyproject.toml index 6c6439e27e..a6565ebaf4 100644 --- a/exporter/opentelemetry-exporter-opencensus/pyproject.toml +++ b/exporter/opentelemetry-exporter-opencensus/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ dependencies = [ "grpcio >= 1.0.0, < 2.0.0", "opencensus-proto >= 0.1.0, < 1.0.0", - "opentelemetry-api >= 1.21.0.dev", + "opentelemetry-api >= 1.22.0.dev", "opentelemetry-sdk >= 1.15", "protobuf ~= 3.13", "setuptools >= 16.0", diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index c2996671d6..2e4aa8c751 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.42b0.dev" +__version__ = "0.43b0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml index 74519c1237..4751da63a4 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ - "opentelemetry-proto == 1.21.0.dev", + "opentelemetry-proto == 1.22.0.dev", "backoff >= 1.10.0, < 2.0.0; python_version<'3.7'", "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py index 87246ad9db..0616613de8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index 1b2af79e0e..8750f8c9e4 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -31,9 +31,9 @@ dependencies = [ "googleapis-common-protos ~= 1.52", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.21.0.dev", - "opentelemetry-sdk ~= 1.21.0.dev", - "opentelemetry-exporter-otlp-proto-common == 1.21.0.dev", + "opentelemetry-proto == 1.22.0.dev", + "opentelemetry-sdk ~= 1.22.0.dev", + "opentelemetry-exporter-otlp-proto-common == 1.22.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 87246ad9db..0616613de8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index 4e89ea5b81..8174819166 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -30,9 +30,9 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.21.0.dev", - "opentelemetry-sdk ~= 1.21.0.dev", - "opentelemetry-exporter-otlp-proto-common == 1.21.0.dev", + "opentelemetry-proto == 1.22.0.dev", + "opentelemetry-sdk ~= 1.22.0.dev", + "opentelemetry-exporter-otlp-proto-common == 1.22.0.dev", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 87246ad9db..0616613de8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index 64ac953180..cfd8843cab 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-otlp-proto-grpc == 1.21.0.dev", - "opentelemetry-exporter-otlp-proto-http == 1.21.0.dev", + "opentelemetry-exporter-otlp-proto-grpc == 1.22.0.dev", + "opentelemetry-exporter-otlp-proto-http == 1.22.0.dev", ] [project.entry-points.opentelemetry_logs_exporter] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 87246ad9db..0616613de8 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/exporter/opentelemetry-exporter-prometheus/pyproject.toml b/exporter/opentelemetry-exporter-prometheus/pyproject.toml index 48f4afb90f..19afcaae25 100644 --- a/exporter/opentelemetry-exporter-prometheus/pyproject.toml +++ b/exporter/opentelemetry-exporter-prometheus/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ dependencies = [ "opentelemetry-api ~= 1.12", # DONOTMERGE: confirm that this will becomes ~= 1.21 in the next release - "opentelemetry-sdk ~= 1.21.0.dev", + "opentelemetry-sdk ~= 1.22.0.dev", "prometheus_client >= 0.5.0, < 1.0.0", ] diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index c2996671d6..2e4aa8c751 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.42b0.dev" +__version__ = "0.43b0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 87246ad9db..0616613de8 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml index be59f5e54c..f9046c6d5a 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.3", - "opentelemetry-exporter-zipkin-json == 1.21.0.dev", + "opentelemetry-exporter-zipkin-json == 1.22.0.dev", "opentelemetry-sdk ~= 1.11", "protobuf ~= 3.12", "requests ~= 2.7", diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 87246ad9db..0616613de8 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin/pyproject.toml b/exporter/opentelemetry-exporter-zipkin/pyproject.toml index f4750e7c1b..16c5eb83aa 100644 --- a/exporter/opentelemetry-exporter-zipkin/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-zipkin-json == 1.21.0.dev", - "opentelemetry-exporter-zipkin-proto-http == 1.21.0.dev", + "opentelemetry-exporter-zipkin-json == 1.22.0.dev", + "opentelemetry-exporter-zipkin-proto-http == 1.22.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 87246ad9db..0616613de8 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 87246ad9db..0616613de8 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 87246ad9db..0616613de8 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index 603a9b1fbf..2c5568c178 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-api == 1.21.0.dev", - "opentelemetry-semantic-conventions == 0.42b0.dev", + "opentelemetry-api == 1.22.0.dev", + "opentelemetry-semantic-conventions == 0.43b0.dev", "typing-extensions >= 3.7.4", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 87246ad9db..0616613de8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index c2996671d6..2e4aa8c751 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.42b0.dev" +__version__ = "0.43b0.dev" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 87246ad9db..0616613de8 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 87246ad9db..0616613de8 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0.dev" +__version__ = "1.22.0.dev" diff --git a/shim/opentelemetry-opencensus-shim/pyproject.toml b/shim/opentelemetry-opencensus-shim/pyproject.toml index 8bb5332463..60dfad777e 100644 --- a/shim/opentelemetry-opencensus-shim/pyproject.toml +++ b/shim/opentelemetry-opencensus-shim/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.42b0.dev", + "opentelemetry-test-utils == 0.43b0.dev", "opencensus == 0.11.1", # Temporary fix for https://github.com/census-instrumentation/opencensus-python/issues/1219 "six == 1.16.0", diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py index c2996671d6..2e4aa8c751 100644 --- a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.42b0.dev" +__version__ = "0.43b0.dev" diff --git a/shim/opentelemetry-opentracing-shim/pyproject.toml b/shim/opentelemetry-opentracing-shim/pyproject.toml index 1dac33ee2d..036af25e6c 100644 --- a/shim/opentelemetry-opentracing-shim/pyproject.toml +++ b/shim/opentelemetry-opentracing-shim/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.42b0.dev", + "opentelemetry-test-utils == 0.43b0.dev", "opentracing ~= 2.2.0", ] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index c2996671d6..2e4aa8c751 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.42b0.dev" +__version__ = "0.43b0.dev" diff --git a/tests/opentelemetry-test-utils/pyproject.toml b/tests/opentelemetry-test-utils/pyproject.toml index d9f7908e38..b7a0a53a91 100644 --- a/tests/opentelemetry-test-utils/pyproject.toml +++ b/tests/opentelemetry-test-utils/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ ] dependencies = [ "asgiref ~= 3.0", - "opentelemetry-api == 1.21.0.dev", - "opentelemetry-sdk == 1.21.0.dev", + "opentelemetry-api == 1.22.0.dev", + "opentelemetry-sdk == 1.22.0.dev", ] [project.optional-dependencies] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index f4c8e12962..5262902096 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.42b0.dev" +__version__ = "0.43b0.dev" From 0a70dc337955ced4d81a7821413ee7442a9d2578 Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:14:45 -0300 Subject: [PATCH 1497/1517] Copy change log updates from release/v1.21.x-0.42bx (#3517) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db8b1b26d9..87f3e2a8bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -## Version 1.21.0/0.42b0 (2023-11-01) +## Version 1.21.0/0.42b0 (2023-11-07) - Fix `SumAggregation`  ([#3390](https://github.com/open-telemetry/opentelemetry-python/pull/3390)) From 3d9de97d7c38dcf0d78d35409c37de07c6d9473e Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:45:20 -0600 Subject: [PATCH 1498/1517] Fix typing in ReadableSpan (#3528) Co-authored-by: Srikanth Chekuri --- .../src/opentelemetry/sdk/trace/__init__.py | 149 ++++++++---------- opentelemetry-sdk/tests/trace/test_trace.py | 9 +- 2 files changed, 74 insertions(+), 84 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index ea015bc606..8897585607 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -21,7 +21,6 @@ import threading import traceback import typing -from collections import OrderedDict from contextlib import contextmanager from os import environ from time import time_ns @@ -31,6 +30,7 @@ Callable, Dict, Iterator, + List, Optional, Sequence, Tuple, @@ -353,19 +353,19 @@ class ReadableSpan: def __init__( self, - name: str = None, - context: trace_api.SpanContext = None, + name: str, + context: Optional[trace_api.SpanContext] = None, parent: Optional[trace_api.SpanContext] = None, - resource: Resource = None, + resource: Optional[Resource] = None, attributes: types.Attributes = None, events: Sequence[Event] = (), links: Sequence[trace_api.Link] = (), kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, - instrumentation_info: InstrumentationInfo = None, + instrumentation_info: Optional[InstrumentationInfo] = None, status: Status = Status(StatusCode.UNSET), start_time: Optional[int] = None, end_time: Optional[int] = None, - instrumentation_scope: InstrumentationScope = None, + instrumentation_scope: Optional[InstrumentationScope] = None, ) -> None: self._name = name self._context = context @@ -386,19 +386,19 @@ def __init__( @property def dropped_attributes(self) -> int: - if self._attributes: + if isinstance(self._attributes, BoundedAttributes): return self._attributes.dropped return 0 @property def dropped_events(self) -> int: - if self._events: + if isinstance(self._events, BoundedList): return self._events.dropped return 0 @property def dropped_links(self) -> int: - if self._links: + if isinstance(self._links, BoundedList): return self._links.dropped return 0 @@ -435,7 +435,7 @@ def status(self) -> trace_api.Status: @property def attributes(self) -> types.Attributes: - return MappingProxyType(self._attributes) + return MappingProxyType(self._attributes or {}) @property def events(self) -> Sequence[Event]: @@ -453,23 +453,17 @@ def resource(self) -> Resource: @deprecated( version="1.11.1", reason="You should use instrumentation_scope" ) - def instrumentation_info(self) -> InstrumentationInfo: + def instrumentation_info(self) -> Optional[InstrumentationInfo]: return self._instrumentation_info @property - def instrumentation_scope(self) -> InstrumentationScope: + def instrumentation_scope(self) -> Optional[InstrumentationScope]: return self._instrumentation_scope - def to_json(self, indent=4): + def to_json(self, indent: int = 4): parent_id = None if self.parent is not None: - if isinstance(self.parent, Span): - ctx = self.parent.context - parent_id = f"0x{trace_api.format_span_id(ctx.span_id)}" - elif isinstance(self.parent, SpanContext): - parent_id = ( - f"0x{trace_api.format_span_id(self.parent.span_id)}" - ) + parent_id = f"0x{trace_api.format_span_id(self.parent.span_id)}" start_time = None if self._start_time: @@ -479,77 +473,72 @@ def to_json(self, indent=4): if self._end_time: end_time = util.ns_to_iso_str(self._end_time) - if self._status is not None: - status = OrderedDict() - status["status_code"] = str(self._status.status_code.name) - if self._status.description: - status["description"] = self._status.description - - f_span = OrderedDict() - - f_span["name"] = self._name - f_span["context"] = self._format_context(self._context) - f_span["kind"] = str(self.kind) - f_span["parent_id"] = parent_id - f_span["start_time"] = start_time - f_span["end_time"] = end_time - if self._status is not None: - f_span["status"] = status - f_span["attributes"] = self._format_attributes(self._attributes) - f_span["events"] = self._format_events(self._events) - f_span["links"] = self._format_links(self._links) - f_span["resource"] = json.loads(self.resource.to_json()) + status = { + "status_code": str(self._status.status_code.name), + } + if self._status.description: + status["description"] = self._status.description + + f_span = { + "name": self._name, + "context": self._format_context(self._context) + if self._context + else None, + "kind": str(self.kind), + "parent_id": parent_id, + "start_time": start_time, + "end_time": end_time, + "status": status, + "attributes": self._format_attributes(self._attributes), + "events": self._format_events(self._events), + "links": self._format_links(self._links), + "resource": json.loads(self.resource.to_json()), + } return json.dumps(f_span, indent=indent) @staticmethod - def _format_context(context): - x_ctx = OrderedDict() - x_ctx["trace_id"] = f"0x{trace_api.format_trace_id(context.trace_id)}" - x_ctx["span_id"] = f"0x{trace_api.format_span_id(context.span_id)}" - x_ctx["trace_state"] = repr(context.trace_state) - return x_ctx + def _format_context(context: SpanContext) -> Dict[str, str]: + return { + "trace_id": f"0x{trace_api.format_trace_id(context.trace_id)}", + "span_id": f"0x{trace_api.format_span_id(context.span_id)}", + "trace_state": repr(context.trace_state), + } @staticmethod - def _format_attributes(attributes): - if isinstance(attributes, BoundedAttributes): - return attributes._dict # pylint: disable=protected-access - if isinstance(attributes, MappingProxyType): - return attributes.copy() + def _format_attributes( + attributes: types.Attributes, + ) -> Optional[Dict[str, Any]]: + if attributes is not None and not isinstance(attributes, dict): + return dict(attributes) return attributes @staticmethod - def _format_events(events): - f_events = [] - for event in events: - f_event = OrderedDict() - f_event["name"] = event.name - f_event["timestamp"] = util.ns_to_iso_str(event.timestamp) - f_event[ - "attributes" - ] = Span._format_attributes( # pylint: disable=protected-access - event.attributes - ) - f_events.append(f_event) - return f_events + def _format_events(events: Sequence[Event]) -> List[Dict[str, Any]]: + return [ + { + "name": event.name, + "timestamp": util.ns_to_iso_str(event.timestamp), + "attributes": Span._format_attributes( # pylint: disable=protected-access + event.attributes + ), + } + for event in events + ] @staticmethod - def _format_links(links): - f_links = [] - for link in links: - f_link = OrderedDict() - f_link[ - "context" - ] = Span._format_context( # pylint: disable=protected-access - link.context - ) - f_link[ - "attributes" - ] = Span._format_attributes( # pylint: disable=protected-access - link.attributes - ) - f_links.append(f_link) - return f_links + def _format_links(links: Sequence[trace_api.Link]) -> List[Dict[str, Any]]: + return [ + { + "context": Span._format_context( # pylint: disable=protected-access + link.context + ), + "attributes": Span._format_attributes( # pylint: disable=protected-access + link.attributes + ), + } + for link in links + ] class SpanLimits: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index b08afce4a6..83a4ece5ed 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -605,10 +605,11 @@ def test_surplus_span_attributes(self): class TestReadableSpan(unittest.TestCase): def test_links(self): - span = trace.ReadableSpan() + span = trace.ReadableSpan("test") self.assertEqual(span.links, ()) span = trace.ReadableSpan( + "test", links=[trace_api.Link(context=trace_api.INVALID_SPAN_CONTEXT)] * 2, ) self.assertEqual(len(span.links), 2) @@ -616,13 +617,13 @@ def test_links(self): self.assertFalse(link.context.is_valid) def test_events(self): - span = trace.ReadableSpan() + span = trace.ReadableSpan("test") self.assertEqual(span.events, ()) events = [ trace.Event("foo1", {"bar1": "baz1"}), trace.Event("foo2", {"bar2": "baz2"}), ] - span = trace.ReadableSpan(events=events) + span = trace.ReadableSpan("test", events=events) self.assertEqual(span.events, tuple(events)) @@ -1376,7 +1377,7 @@ def test_to_json(self): ) parent = trace._Span("parent-name", context, resource=Resource({})) span = trace._Span( - "span-name", context, resource=Resource({}), parent=parent + "span-name", context, resource=Resource({}), parent=parent.context ) self.assertEqual( From 36aaa84d17284de6c873eadc0b955d97fe06b07b Mon Sep 17 00:00:00 2001 From: Siim Kallas Date: Thu, 16 Nov 2023 21:54:01 +0200 Subject: [PATCH 1499/1517] fix: timeout period when acquiring grpc exporter lock on shutdown (#3524) --- CHANGELOG.md | 5 ++++- .../src/opentelemetry/exporter/otlp/proto/grpc/exporter.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87f3e2a8bb..fa1ad66ccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -## Version 1.21.0/0.42b0 (2023-11-07) +- Fix OTLPExporterMixin shutdown timeout period + ([#3524](https://github.com/open-telemetry/opentelemetry-python/pull/3524)) + +## Version 1.21.0/0.42b0 (2023-11-01) - Fix `SumAggregation`  ([#3390](https://github.com/open-telemetry/opentelemetry-python/pull/3390)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index dd169ff239..67188d04a4 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -322,7 +322,7 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: logger.warning("Exporter already shutdown, ignoring call") return # wait for the last export if any - self._export_lock.acquire(timeout=timeout_millis) + self._export_lock.acquire(timeout=timeout_millis / 1e3) self._shutdown = True self._export_lock.release() From c0576a0f87da9bc75dbbba75a562df643edac4c2 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 21 Nov 2023 23:35:44 -0800 Subject: [PATCH 1500/1517] Add checking instrumentation scope in test base (#3539) --- .../src/opentelemetry/test/test_base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py index f7451f0d3c..6a65e112f7 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py @@ -74,6 +74,12 @@ def assertEqualSpanInstrumentationInfo(self, span, module): self.assertEqual(span.instrumentation_info.name, module.__name__) self.assertEqual(span.instrumentation_info.version, module.__version__) + def assertEqualSpanInstrumentationScope(self, span, module): + self.assertEqual(span.instrumentation_scope.name, module.__name__) + self.assertEqual( + span.instrumentation_scope.version, module.__version__ + ) + def assertSpanHasAttributes(self, span, attributes): for key, val in attributes.items(): self.assertIn(key, span.attributes) From cc8fd8838aaf9cbd8b19d9617da4e3d4aaee384b Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 22 Nov 2023 22:29:46 +0530 Subject: [PATCH 1501/1517] Update lint deps and configs (#3533) Co-authored-by: Diego Hurtado --- .pylintrc | 11 ++---- dev-requirements.txt | 30 ++++++++-------- docs-requirements.txt | 17 ++++----- .../exporter/jaeger/thrift/send.py | 2 +- .../tests/__init__.py | 0 .../pyproject.toml | 3 ++ .../tests/__init__.py | 0 .../tests/__init__.py | 0 .../tests/metrics/__init__.py | 0 .../pyproject.toml | 3 ++ .../tests/__init__.py | 0 .../tests/__init__.py | 0 gen-requirements.txt | 6 ++++ .../opentelemetry/_logs/_internal/__init__.py | 2 +- .../metrics/_internal/__init__.py | 3 +- .../metrics/_internal/instrument.py | 1 + .../src/opentelemetry/propagators/textmap.py | 1 + .../tests/context/test_contextvars_context.py | 5 ++- .../test_tracecontexthttptextformat.py | 8 ++--- .../sdk/_logs/_internal/export/__init__.py | 16 ++++----- .../sdk/environment_variables.py | 2 +- .../sdk/metrics/_internal/aggregation.py | 1 + .../sdk/metrics/_internal/point.py | 1 + .../opentelemetry/sdk/resources/__init__.py | 2 +- .../src/opentelemetry/sdk/trace/__init__.py | 4 ++- opentelemetry-sdk/tests/trace/test_trace.py | 1 + pyproject.toml | 3 ++ scripts/proto_codegen.sh | 2 +- scripts/update_sha.py | 2 +- .../tests/__init__.py | 0 .../shim/opentracing_shim/__init__.py | 1 + .../test_multiple_callbacks/test_threads.py | 1 + .../src/opentelemetry/test/test_base.py | 4 ++- tox.ini | 35 ++++++------------- 34 files changed, 88 insertions(+), 79 deletions(-) create mode 100644 exporter/opentelemetry-exporter-jaeger/tests/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/tests/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/tests/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp/tests/__init__.py create mode 100644 exporter/opentelemetry-exporter-zipkin/tests/__init__.py create mode 100644 gen-requirements.txt create mode 100644 shim/opentelemetry-opencensus-shim/tests/__init__.py diff --git a/.pylintrc b/.pylintrc index cf61fde78d..5b0f786252 100644 --- a/.pylintrc +++ b/.pylintrc @@ -28,7 +28,7 @@ limit-inference-results=100 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. -load-plugins= +load-plugins=pylint.extensions.no_self_use # Pickle collected data for later comparisons. persistent=yes @@ -68,7 +68,6 @@ disable=missing-docstring, duplicate-code, ungrouped-imports, # Leave this up to isort wrong-import-order, # Leave this up to isort - bad-continuation, # Leave this up to black line-too-long, # Leave this up to black exec-used, super-with-arguments, # temp-pylint-upgrade @@ -76,6 +75,7 @@ disable=missing-docstring, raise-missing-from, # temp-pylint-upgrade unused-argument, # temp-pylint-upgrade redefined-builtin, + cyclic-import, # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -263,13 +263,6 @@ max-line-length=79 # Maximum number of lines in a module. max-module-lines=1000 -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no diff --git a/dev-requirements.txt b/dev-requirements.txt index 732acd8b2e..613b5984c1 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,21 +1,15 @@ -pylint==2.11.0 -flake8~=3.7 -isort~=5.8 -black~=22.3.0 -httpretty~=1.0 +pylint==3.0.2 +flake8==6.1.0 +isort==5.12.0 +black==22.3.0 +httpretty==1.1.4 mypy==0.931 -sphinx~=3.5.4 -sphinx-rtd-theme~=0.5 -sphinx-autodoc-typehints~=1.12.0 +sphinx==7.1.2 +sphinx-rtd-theme==2.0.0rc4 +sphinx-autodoc-typehints==1.25.2 pytest==7.1.3 -pytest-cov>=2.8 -readme-renderer~=24.0 -# This version of grpcio-tools ships with protoc 3.19.4 which appears to be compatible with -# both protobuf 3.19.x and 4.x (see https://github.com/protocolbuffers/protobuf/issues/11123). -# Bump this version with caution to preserve compatibility with protobuf 3. -# https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-proto/pyproject.toml#L28 -grpcio-tools==1.48.1 -mypy-protobuf~=3.0.0 +pytest-cov==4.1.0 +readme-renderer==42.0 # temporary fix. we should update the jinja, flask deps # See https://github.com/pallets/markupsafe/issues/282 # breaking change introduced in markupsafe causes jinja, flask to break @@ -24,3 +18,7 @@ bleach==4.1.0 # This dependency was updated to a breaking version. codespell==2.1.0 requests==2.31.0 ruamel.yaml==0.17.21 +asgiref==3.7.2 +psutil==5.9.6 +GitPython==3.1.40 +flaky==3.7.0 diff --git a/docs-requirements.txt b/docs-requirements.txt index c167ab785d..dcbcd41bfa 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -1,23 +1,24 @@ -sphinx~=3.5.4 -sphinx-rtd-theme~=0.5 -sphinx-autodoc-typehints +sphinx==7.1.2 +sphinx-rtd-theme==2.0.0rc4 +sphinx-autodoc-typehints==1.25.2 # used to generate docs for the website -sphinx-jekyll-builder +sphinx-jekyll-builder==0.3.0 # Need to install the api/sdk in the venv for autodoc. Modifying sys.path # doesn't work for pkg_resources. ./opentelemetry-api ./opentelemetry-semantic-conventions ./opentelemetry-sdk +./shim/opentelemetry-opencensus-shim +./shim/opentelemetry-opentracing-shim # Required by instrumentation and exporter packages -ddtrace>=0.34.0 grpcio~=1.27 -Deprecated>=1.2.6 -django>=2.2 +Deprecated~=1.2 +django~=4.2 flask~=1.0 opentracing~=2.2.0 -thrift>=0.10.0 +thrift~=0.10 wrapt>=1.0.0,<2.0.0 # temporary fix. we should update the jinja, flask deps # See https://github.com/pallets/markupsafe/issues/282 diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py index 9ddc6d7b27..0e3a411de6 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py @@ -125,7 +125,7 @@ def __init__(self, thrift_url="", auth=None, timeout_in_millis=None): if auth is not None: auth_header = f"{auth[0]}:{auth[1]}" decoded = base64.b64encode(auth_header.encode()).decode("ascii") - basic_auth = dict(Authorization=f"Basic {decoded}") + basic_auth = {"Authorization": f"Basic {decoded}"} self.http_transport.setCustomHeaders(basic_auth) def submit(self, batch: jaeger.Batch): diff --git a/exporter/opentelemetry-exporter-jaeger/tests/__init__.py b/exporter/opentelemetry-exporter-jaeger/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml index 4751da63a4..bef34ed82b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml @@ -30,6 +30,9 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", ] +[project.optional-dependencies] +test = [] + [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-common" diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index cfd8843cab..707b8c9455 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -30,6 +30,9 @@ dependencies = [ "opentelemetry-exporter-otlp-proto-http == 1.22.0.dev", ] +[project.optional-dependencies] +test = [] + [project.entry-points.opentelemetry_logs_exporter] otlp = "opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter" diff --git a/exporter/opentelemetry-exporter-otlp/tests/__init__.py b/exporter/opentelemetry-exporter-otlp/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-zipkin/tests/__init__.py b/exporter/opentelemetry-exporter-zipkin/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gen-requirements.txt b/gen-requirements.txt new file mode 100644 index 0000000000..88f8817b2d --- /dev/null +++ b/gen-requirements.txt @@ -0,0 +1,6 @@ +# This version of grpcio-tools ships with protoc 3.19.4 which appears to be compatible with +# both protobuf 3.19.x and 4.x (see https://github.com/protocolbuffers/protobuf/issues/11123). +# Bump this version with caution to preserve compatibility with protobuf 3. +# https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-proto/pyproject.toml#L28 +grpcio-tools==1.48.1 +mypy-protobuf~=3.0.0 diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index 4c25eed074..e67f28439b 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -177,7 +177,7 @@ def get_logger_provider() -> LoggerProvider: """Gets the current global :class:`~.LoggerProvider` object.""" global _LOGGER_PROVIDER # pylint: disable=global-statement if _LOGGER_PROVIDER is None: - if _OTEL_PYTHON_LOGGER_PROVIDER not in environ.keys(): + if _OTEL_PYTHON_LOGGER_PROVIDER not in environ: # TODO: return proxy _LOGGER_PROVIDER = NoOpLoggerProvider() return _LOGGER_PROVIDER diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py index aad76b33a7..dc1e76c8ae 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py @@ -75,6 +75,7 @@ _logger = getLogger(__name__) +# pylint: disable=invalid-name _ProxyInstrumentT = Union[ _ProxyCounter, _ProxyHistogram, @@ -760,7 +761,7 @@ def get_meter_provider() -> MeterProvider: """Gets the current global :class:`~.MeterProvider` object.""" if _METER_PROVIDER is None: - if OTEL_PYTHON_METER_PROVIDER not in environ.keys(): + if OTEL_PYTHON_METER_PROVIDER not in environ: return _PROXY_METER_PROVIDER meter_provider: MeterProvider = _load_provider( # type: ignore diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py index 54b2fb7597..b02a15005c 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py @@ -55,6 +55,7 @@ class CallbackOptions: InstrumentT = TypeVar("InstrumentT", bound="Instrument") +# pylint: disable=invalid-name CallbackT = Union[ Callable[[CallbackOptions], Iterable[Observation]], Generator[Iterable[Observation], CallbackOptions, None], diff --git a/opentelemetry-api/src/opentelemetry/propagators/textmap.py b/opentelemetry-api/src/opentelemetry/propagators/textmap.py index afadd35000..42f1124f36 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/textmap.py +++ b/opentelemetry-api/src/opentelemetry/propagators/textmap.py @@ -18,6 +18,7 @@ from opentelemetry.context.context import Context CarrierT = typing.TypeVar("CarrierT") +# pylint: disable=invalid-name CarrierValT = typing.Union[typing.List[str], str] diff --git a/opentelemetry-api/tests/context/test_contextvars_context.py b/opentelemetry-api/tests/context/test_contextvars_context.py index dc27e27190..e9af3107d8 100644 --- a/opentelemetry-api/tests/context/test_contextvars_context.py +++ b/opentelemetry-api/tests/context/test_contextvars_context.py @@ -17,10 +17,12 @@ from opentelemetry import context from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext -from .base_context import ContextTestCases +# pylint: disable=import-error,no-name-in-module +from tests.context.base_context import ContextTestCases class TestContextVarsContext(ContextTestCases.BaseTest): + # pylint: disable=invalid-name def setUp(self) -> None: super().setUp() self.mock_runtime = patch.object( @@ -30,6 +32,7 @@ def setUp(self) -> None: ) self.mock_runtime.start() + # pylint: disable=invalid-name def tearDown(self) -> None: super().tearDown() self.mock_runtime.stop() diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index ac57b5c919..7fefd8dea6 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -39,7 +39,7 @@ def test_no_traceparent_header(self): If no traceparent header is received, the vendor creates a new trace-id and parent-id that represents the current request. """ - output = {} # type:typing.Dict[str, typing.List[str]] + output: typing.Dict[str, typing.List[str]] = {} span = trace.get_current_span(FORMAT.extract(output)) self.assertIsInstance(span.get_span_context(), trace.SpanContext) @@ -66,7 +66,7 @@ def test_headers_with_tracestate(self): span_context.trace_state, {"foo": "1", "bar": "2", "baz": "3"} ) self.assertTrue(span_context.is_remote) - output = {} # type:typing.Dict[str, str] + output: typing.Dict[str, str] = {} span = trace.NonRecordingSpan(span_context) ctx = trace.set_span_in_context(span) @@ -145,7 +145,7 @@ def test_no_send_empty_tracestate(self): Empty and whitespace-only list members are allowed. Vendors MUST accept empty tracestate headers but SHOULD avoid sending them. """ - output = {} # type:typing.Dict[str, str] + output: typing.Dict[str, str] = {} span = trace.NonRecordingSpan( trace.SpanContext(self.TRACE_ID, self.SPAN_ID, is_remote=False) ) @@ -177,7 +177,7 @@ def test_format_not_supported(self): def test_propagate_invalid_context(self): """Do not propagate invalid trace context.""" - output = {} # type:typing.Dict[str, str] + output: typing.Dict[str, str] = {} ctx = trace.set_span_in_context(trace.INVALID_SPAN) FORMAT.inject(output, context=ctx) self.assertFalse("traceparent" in output) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py index 8ccad56c7a..14140d26b7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -166,6 +166,10 @@ class BatchLogRecordProcessor(LogRecordProcessor): - :envvar:`OTEL_BLRP_EXPORT_TIMEOUT` """ + _queue: Deque[LogData] + _flush_request: Optional[_FlushRequest] + _log_records: List[Optional[LogData]] + def __init__( self, exporter: LogExporter, @@ -201,9 +205,7 @@ def __init__( self._schedule_delay_millis = schedule_delay_millis self._max_export_batch_size = max_export_batch_size self._export_timeout_millis = export_timeout_millis - self._queue = collections.deque( - [], max_queue_size - ) # type: Deque[LogData] + self._queue = collections.deque([], max_queue_size) self._worker_thread = threading.Thread( name="OtelBatchLogRecordProcessor", target=self.worker, @@ -211,10 +213,8 @@ def __init__( ) self._condition = threading.Condition(threading.Lock()) self._shutdown = False - self._flush_request = None # type: Optional[_FlushRequest] - self._log_records = [ - None - ] * self._max_export_batch_size # type: List[Optional[LogData]] + self._flush_request = None + self._log_records = [None] * self._max_export_batch_size self._worker_thread.start() # Only available in *nix since py37. if hasattr(os, "register_at_fork"): @@ -236,7 +236,7 @@ def _at_fork_reinit(self): def worker(self): timeout = self._schedule_delay_millis / 1e3 - flush_request = None # type: Optional[_FlushRequest] + flush_request: Optional[_FlushRequest] = None while not self._shutdown: with self._condition: if self._shutdown: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index dc8f91f600..a69e451cbb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -674,7 +674,7 @@ The :envvar:`OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` is a comma-separated string of names of resource detectors. These names must be the same as the names of -entry points for the `opentelemetry_resource_detector` entry point. This is an +entry points for the ```opentelemetry_resource_detector``` entry point. This is an experimental feature and the name of this variable and its behavior can change in a non-backwards compatible way. """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py index 1f6d4c4c13..62ba091ebf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py @@ -646,6 +646,7 @@ def aggregate(self, measurement: Measurement) -> None: # 3. Determine if a change of scale is needed. is_rescaling_needed = False + low, high = 0, 0 if len(buckets) == 0: buckets.index_start = index diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py index cba37e7fdf..c30705c59a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py @@ -170,6 +170,7 @@ def to_json(self, indent=4) -> str: ) +# pylint: disable=invalid-name DataT = Union[Sum, Gauge, Histogram] DataPointT = Union[NumberDataPoint, HistogramDataPoint] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index dd5bea43d4..42fcf125bb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -365,11 +365,11 @@ def get_aggregated_resources( futures = [executor.submit(detector.detect) for detector in detectors] for detector_ind, future in enumerate(futures): detector = detectors[detector_ind] + detected_resource: Resource = _EMPTY_RESOURCE try: detected_resource = future.result(timeout=timeout) # pylint: disable=broad-except except Exception as ex: - detected_resource = _EMPTY_RESOURCE if detector.raise_on_error: raise ex logger.warning( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 8897585607..6dae70b2f6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -140,10 +140,12 @@ class SynchronousMultiSpanProcessor(SpanProcessor): added. """ + _span_processors: Tuple[SpanProcessor, ...] + def __init__(self): # use a tuple to avoid race conditions when adding a new span and # iterating through it on "on_start" and "on_end". - self._span_processors = () # type: Tuple[SpanProcessor, ...] + self._span_processors = () self._lock = threading.Lock() def add_span_processor(self, span_processor: SpanProcessor) -> None: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 83a4ece5ed..4150d60d10 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -1220,6 +1220,7 @@ def test_record_exception_with_attributes_and_timestamp(self): self.assertEqual(1604238587112021089, exception_event.timestamp) def test_record_exception_context_manager(self): + span = None try: with self.tracer.start_as_current_span("span") as span: raise RuntimeError("example error") diff --git a/pyproject.toml b/pyproject.toml index 27d530a7f6..405792011e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,9 @@ exclude = ''' /( # generated files .tox| venv| + venv.*| + .venv.*| + target.*| .*/build/lib/.*| exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen| exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen| diff --git a/scripts/proto_codegen.sh b/scripts/proto_codegen.sh index 53d8ad8fc7..26fb8b1ee8 100755 --- a/scripts/proto_codegen.sh +++ b/scripts/proto_codegen.sh @@ -32,7 +32,7 @@ echo "Creating temporary virtualenv at $venv_dir using $(python3 --version)" python3 -m venv $venv_dir source $venv_dir/bin/activate python -m pip install \ - -c $repo_root/dev-requirements.txt \ + -c $repo_root/gen-requirements.txt \ grpcio-tools mypy-protobuf echo 'python -m grpc_tools.protoc --version' python -m grpc_tools.protoc --version diff --git a/scripts/update_sha.py b/scripts/update_sha.py index c7da2e1a54..429470c055 100644 --- a/scripts/update_sha.py +++ b/scripts/update_sha.py @@ -25,7 +25,7 @@ def get_sha(branch): url = API_URL + branch - response = requests.get(url) + response = requests.get(url, timeout=15) response.raise_for_status() return response.json()["sha"] diff --git a/shim/opentelemetry-opencensus-shim/tests/__init__.py b/shim/opentelemetry-opencensus-shim/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index 7d29a3412f..8fd72da972 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -387,6 +387,7 @@ def from_context_manager(cls, manager: "ScopeManagerShim", span_cm): :meth:`opentelemetry.trace.use_span`. """ + # pylint: disable=unnecessary-dunder-call otel_span = span_cm.__enter__() span_context = SpanContextShim(otel_span.get_span_context()) span = SpanShim(manager.tracer, span_context, otel_span) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py index 6e8b405cce..d94f834e51 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py @@ -55,6 +55,7 @@ def test_main(self): def task(self, interval, parent_span): logger.info("Starting task") + scope = None try: scope = self.tracer.scope_manager.activate(parent_span, False) with self.tracer.start_active_span("task"): diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py index 6a65e112f7..f9ac2dfc19 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py @@ -186,7 +186,9 @@ def is_data_points_equal( data_point: DataPointT, est_value_delta: Optional[float] = 0, ): - if type(expected_data_point) != type(data_point) or not isinstance( + if type(expected_data_point) != type( # noqa: E721 + data_point + ) or not isinstance( expected_data_point, (HistogramDataPoint, NumberDataPoint) ): return False diff --git a/tox.ini b/tox.ini index 351054f26e..e3f0866f52 100644 --- a/tox.ini +++ b/tox.ini @@ -191,7 +191,7 @@ commands = mypyinstalled: mypy --install-types --non-interactive --namespace-packages opentelemetry-api/tests/mypysmoke.py --strict [testenv:spellcheck] -basepython: python3.10 +basepython: python3 recreate = True deps = codespell @@ -200,20 +200,10 @@ commands = codespell [testenv:lint] -basepython: python3.10 +basepython: python3 recreate = True deps = - -c dev-requirements.txt - asgiref - pylint - flake8 - isort - black - psutil - readme_renderer - httpretty - GitPython - flaky + -r dev-requirements.txt commands_pre = python -m pip install -e {toxinidir}/opentelemetry-api[test] @@ -245,20 +235,17 @@ commands = python scripts/eachdist.py lint --check-only [testenv:docs] -# FIXME See #2984 -basepython: python3.9 +basepython: python3 recreate = True deps = -c {toxinidir}/dev-requirements.txt -r {toxinidir}/docs-requirements.txt - changedir = docs - commands = sphinx-build -E -a -W -b html -T . _build/html [testenv:tracecontext] -basepython: python3.10 +basepython: python3 deps = # needed for tracecontext aiohttp~=3.6 @@ -284,11 +271,11 @@ commands = [testenv:docker-tests-proto{3,4}] deps = - pytest + pytest==7.1.3 # Pinning PyYAML for issue: https://github.com/yaml/pyyaml/issues/724 - PyYAML == 5.3.1 - docker-compose >= 1.25.2 - requests < 2.29.0 + PyYAML==5.3.1 + docker-compose==1.29.2 + requests==2.28.2 ; proto 3 and 4 tests install the respective version of protobuf proto3: protobuf~=3.19.0 @@ -320,9 +307,9 @@ commands_post = docker-compose down -v [testenv:public-symbols-check] -basepython: python3.10 +basepython: python3 recreate = True deps = - GitPython + GitPython==3.1.40 commands = python {toxinidir}/scripts/public_symbols_checker.py From f399195f25c7c4ff1966efa6ef875eabf5405e37 Mon Sep 17 00:00:00 2001 From: Jacob Davis-Hansson Date: Wed, 22 Nov 2023 19:40:11 +0100 Subject: [PATCH 1502/1517] Add stack trace on `StatusCode.UNKNOWN` export error (#3536) --- CHANGELOG.md | 2 ++ .../exporter/otlp/proto/grpc/exporter.py | 1 + .../tests/test_otlp_metrics_exporter.py | 33 +++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa1ad66ccf..9cc24ba9dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Log stacktrace on `UNKNOWN` status OTLP export error + ([#3536](https://github.com/open-telemetry/opentelemetry-python/pull/3536)) - Fix OTLPExporterMixin shutdown timeout period ([#3524](https://github.com/open-telemetry/opentelemetry-python/pull/3524)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 67188d04a4..b422682828 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -308,6 +308,7 @@ def _export( self._exporting, self._endpoint, error.code(), + exc_info=error.code() == StatusCode.UNKNOWN, ) if error.code() == StatusCode.OK: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py index 1d9d65405c..291e9457ef 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py @@ -112,6 +112,14 @@ def Export(self, request, context): return ExportMetricsServiceResponse() +class MetricsServiceServicerUNKNOWN(MetricsServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.UNKNOWN) + + return ExportMetricsServiceResponse() + + class MetricsServiceServicerSUCCESS(MetricsServiceServicer): # pylint: disable=invalid-name,unused-argument,no-self-use def Export(self, request, context): @@ -441,6 +449,31 @@ def test_unavailable_delay(self, mock_sleep, mock_expo): ) mock_sleep.assert_called_with(4) + @patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter._create_exp_backoff_generator" + ) + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep") + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.logger.error") + def test_unknown_logs(self, mock_logger_error, mock_sleep, mock_expo): + + mock_expo.configure_mock(**{"return_value": [1]}) + + add_MetricsServiceServicer_to_server( + MetricsServiceServicerUNKNOWN(), self.server + ) + self.assertEqual( + self.exporter.export(self.metrics["sum_int"]), + MetricExportResult.FAILURE, + ) + mock_sleep.assert_not_called() + mock_logger_error.assert_called_with( + "Failed to export %s to %s, error code: %s", + "metrics", + "localhost:4317", + StatusCode.UNKNOWN, + exc_info=True, + ) + def test_success(self): add_MetricsServiceServicer_to_server( MetricsServiceServicerSUCCESS(), self.server From de89b22b4828c0ca6ab19002d9f6ffc182f48283 Mon Sep 17 00:00:00 2001 From: Allen Kim Date: Thu, 23 Nov 2023 05:00:09 +0900 Subject: [PATCH 1503/1517] Feature/process resource detector (#3472) --- CHANGELOG.md | 3 + .../opentelemetry/sdk/resources/__init__.py | 44 +++++++++---- .../tests/resources/test_resources.py | 61 +++++++++++++++++-- 3 files changed, 92 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cc24ba9dc..4f895dec6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3251](https://github.com/open-telemetry/opentelemetry-python/pull/3251)) - Prometheus exporter support for auto instrumentation ([#3413](https://github.com/open-telemetry/opentelemetry-python/pull/3413)) +- Implement Process Resource detector + ([#3472](https://github.com/open-telemetry/opentelemetry-python/pull/3472)) + ## Version 1.20.0/0.41b0 (2023-09-04) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 42fcf125bb..f92fdb964e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -58,6 +58,7 @@ import abc import concurrent.futures import logging +import os import sys import typing from json import dumps @@ -74,11 +75,15 @@ from opentelemetry.util._importlib_metadata import entry_points, version from opentelemetry.util.types import AttributeValue +try: + import psutil +except ImportError: + psutil = None + LabelValue = AttributeValue Attributes = typing.Dict[str, LabelValue] logger = logging.getLogger(__name__) - CLOUD_PROVIDER = ResourceAttributes.CLOUD_PROVIDER CLOUD_ACCOUNT_ID = ResourceAttributes.CLOUD_ACCOUNT_ID CLOUD_REGION = ResourceAttributes.CLOUD_REGION @@ -117,6 +122,7 @@ OS_TYPE = ResourceAttributes.OS_TYPE OS_DESCRIPTION = ResourceAttributes.OS_DESCRIPTION PROCESS_PID = ResourceAttributes.PROCESS_PID +PROCESS_PARENT_PID = ResourceAttributes.PROCESS_PARENT_PID PROCESS_EXECUTABLE_NAME = ResourceAttributes.PROCESS_EXECUTABLE_NAME PROCESS_EXECUTABLE_PATH = ResourceAttributes.PROCESS_EXECUTABLE_PATH PROCESS_COMMAND = ResourceAttributes.PROCESS_COMMAND @@ -135,7 +141,6 @@ TELEMETRY_AUTO_VERSION = ResourceAttributes.TELEMETRY_AUTO_VERSION TELEMETRY_SDK_LANGUAGE = ResourceAttributes.TELEMETRY_SDK_LANGUAGE - _OPENTELEMETRY_SDK_VERSION = version("opentelemetry-sdk") @@ -180,7 +185,6 @@ def create( otel_experimental_resource_detectors.append("otel") for resource_detector in otel_experimental_resource_detectors: - resource_detectors.append( next( iter( @@ -337,14 +341,32 @@ def detect(self) -> "Resource": else sys.version_info, ) ) - - return Resource( - { - PROCESS_RUNTIME_DESCRIPTION: sys.version, - PROCESS_RUNTIME_NAME: sys.implementation.name, - PROCESS_RUNTIME_VERSION: _runtime_version, - } - ) + _process_pid = os.getpid() + _process_executable_name = sys.executable + _process_executable_path = os.path.dirname(_process_executable_name) + _process_command = sys.argv[0] + _process_command_line = " ".join(sys.argv) + _process_command_args = sys.argv[1:] + resource_info = { + PROCESS_RUNTIME_DESCRIPTION: sys.version, + PROCESS_RUNTIME_NAME: sys.implementation.name, + PROCESS_RUNTIME_VERSION: _runtime_version, + PROCESS_PID: _process_pid, + PROCESS_EXECUTABLE_NAME: _process_executable_name, + PROCESS_EXECUTABLE_PATH: _process_executable_path, + PROCESS_COMMAND: _process_command, + PROCESS_COMMAND_LINE: _process_command_line, + PROCESS_COMMAND_ARGS: _process_command_args, + } + if hasattr(os, "getppid"): + # pypy3 does not have getppid() + resource_info[PROCESS_PARENT_PID] = os.getppid() + + if psutil is not None: + process = psutil.Process() + resource_info[PROCESS_OWNER] = process.username() + + return Resource(resource_info) def get_aggregated_resources( diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 1fc81c7cae..53ecf30cab 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -11,9 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -# pylint: disable=protected-access - +import os +import sys import unittest import uuid from logging import ERROR, WARNING @@ -30,7 +29,14 @@ _OPENTELEMETRY_SDK_VERSION, OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, + PROCESS_COMMAND, + PROCESS_COMMAND_ARGS, + PROCESS_COMMAND_LINE, PROCESS_EXECUTABLE_NAME, + PROCESS_EXECUTABLE_PATH, + PROCESS_OWNER, + PROCESS_PARENT_PID, + PROCESS_PID, PROCESS_RUNTIME_DESCRIPTION, PROCESS_RUNTIME_NAME, PROCESS_RUNTIME_VERSION, @@ -45,6 +51,11 @@ get_aggregated_resources, ) +try: + import psutil +except ImportError: + psutil = None + class TestResources(unittest.TestCase): def setUp(self) -> None: @@ -524,6 +535,10 @@ def test_service_name_env_precedence(self): Resource({"service.name": "from-service-name"}), ) + @patch( + "sys.argv", + ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"], + ) def test_process_detector(self): initial_resource = Resource({"foo": "bar"}) aggregated_resource = get_aggregated_resources( @@ -543,8 +558,42 @@ def test_process_detector(self): aggregated_resource.attributes.keys(), ) - def test_resource_detector_entry_points_default(self): + self.assertEqual( + aggregated_resource.attributes[PROCESS_PID], os.getpid() + ) + if hasattr(os, "getppid"): + self.assertEqual( + aggregated_resource.attributes[PROCESS_PARENT_PID], + os.getppid(), + ) + + if psutil is not None: + self.assertEqual( + aggregated_resource.attributes[PROCESS_OWNER], + psutil.Process().username(), + ) + self.assertEqual( + aggregated_resource.attributes[PROCESS_EXECUTABLE_NAME], + sys.executable, + ) + self.assertEqual( + aggregated_resource.attributes[PROCESS_EXECUTABLE_PATH], + os.path.dirname(sys.executable), + ) + self.assertEqual( + aggregated_resource.attributes[PROCESS_COMMAND], sys.argv[0] + ) + self.assertEqual( + aggregated_resource.attributes[PROCESS_COMMAND_LINE], + " ".join(sys.argv), + ) + self.assertEqual( + aggregated_resource.attributes[PROCESS_COMMAND_ARGS], + tuple(sys.argv[1:]), + ) + + def test_resource_detector_entry_points_default(self): resource = Resource({}).create() self.assertEqual( @@ -644,7 +693,9 @@ def test_resource_detector_entry_points_otel(self): resource.attributes["telemetry.sdk.name"], "opentelemetry" ) self.assertEqual( - resource.attributes["service.name"], "unknown_service" + resource.attributes["service.name"], + "unknown_service:" + + resource.attributes["process.executable.name"], ) self.assertEqual(resource.attributes["a"], "b") self.assertEqual(resource.attributes["c"], "d") From 1625b35ab55782f1781dd189f5742efab5fdbf8a Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 23 Nov 2023 17:11:12 -0600 Subject: [PATCH 1504/1517] Remove Jaeger exporter (#3554) --- .flake8 | 4 - CHANGELOG.md | 2 + docs/exporter/jaeger/jaeger.rst | 35 - docs/getting_started/jaeger_example.py | 42 - eachdist.ini | 3 - .../LICENSE | 201 -- .../README.rst | 42 - .../examples/jaeger_exporter_example.py | 45 - .../proto/api_v2/collector.proto | 66 - .../proto/api_v2/model.proto | 166 -- .../pyproject.toml | 54 - .../exporter/jaeger/proto/grpc/__init__.py | 196 -- .../jaeger/proto/grpc/gen/__init__.py | 4 - .../jaeger/proto/grpc/gen/collector_pb2.py | 134 -- .../proto/grpc/gen/collector_pb2_grpc.py | 46 - .../proto/grpc/gen/gogoproto/gogo_pb2.py | 794 -------- .../grpc/gen/google/api/annotations_pb2.py | 46 - .../proto/grpc/gen/google/api/http_pb2.py | 250 --- .../jaeger/proto/grpc/gen/model_pb2.py | 686 ------- .../options/annotations_pb2.py | 90 - .../options/openapiv2_pb2.py | 1777 ----------------- .../exporter/jaeger/proto/grpc/py.typed | 0 .../jaeger/proto/grpc/send/__init__.py | 0 .../jaeger/proto/grpc/translate/__init__.py | 406 ---- .../exporter/jaeger/proto/grpc/util.py | 43 - .../exporter/jaeger/proto/grpc/version.py | 16 - .../tests/__init__.py | 0 .../tests/certs/cred.cert | 0 .../tests/test_jaeger_exporter_protobuf.py | 524 ----- .../LICENSE | 201 -- .../README.rst | 42 - .../examples/jaeger_exporter_example.py | 50 - .../pyproject.toml | 53 - .../exporter/jaeger/thrift/__init__.py | 233 --- .../exporter/jaeger/thrift/gen/__init__.py | 4 - .../jaeger/thrift/gen/agent/Agent-remote | 124 -- .../exporter/jaeger/thrift/gen/agent/Agent.py | 246 --- .../jaeger/thrift/gen/agent/__init__.py | 1 - .../jaeger/thrift/gen/agent/constants.py | 12 - .../jaeger/thrift/gen/agent/ttypes.py | 15 - .../jaeger/thrift/gen/jaeger/Collector-remote | 117 -- .../jaeger/thrift/gen/jaeger/Collector.py | 243 --- .../jaeger/thrift/gen/jaeger/__init__.py | 1 - .../jaeger/thrift/gen/jaeger/constants.py | 12 - .../jaeger/thrift/gen/jaeger/ttypes.py | 831 -------- .../gen/zipkincore/ZipkinCollector-remote | 117 -- .../thrift/gen/zipkincore/ZipkinCollector.py | 243 --- .../jaeger/thrift/gen/zipkincore/__init__.py | 1 - .../jaeger/thrift/gen/zipkincore/constants.py | 28 - .../jaeger/thrift/gen/zipkincore/ttypes.py | 647 ------ .../exporter/jaeger/thrift/py.typed | 0 .../exporter/jaeger/thrift/send.py | 146 -- .../jaeger/thrift/translate/__init__.py | 320 --- .../exporter/jaeger/thrift/version.py | 16 - .../tests/__init__.py | 0 .../tests/certs/cred.cert | 0 .../tests/test_jaeger_exporter_thrift.py | 689 ------- .../thrift/agent.thrift | 27 - .../thrift/jaeger.thrift | 85 - .../thrift/zipkincore.thrift | 346 ---- .../opentelemetry-exporter-jaeger/LICENSE | 201 -- .../opentelemetry-exporter-jaeger/README.rst | 35 - .../pyproject.toml | 52 - .../opentelemetry/exporter/jaeger/py.typed | 0 .../opentelemetry/exporter/jaeger/version.py | 16 - .../tests/__init__.py | 0 .../tests/test_jaeger.py | 31 - .../src/opentelemetry/_logs/__init__.py | 3 +- opentelemetry-sdk/tests/test_configurator.py | 6 +- pyproject.toml | 2 - rationale.md | 2 +- scripts/coverage.sh | 2 - scripts/public_symbols_checker.py | 6 +- tox.ini | 3 - 74 files changed, 11 insertions(+), 10870 deletions(-) delete mode 100644 docs/exporter/jaeger/jaeger.rst delete mode 100644 docs/getting_started/jaeger_example.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/LICENSE delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/examples/jaeger_exporter_example.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/proto/api_v2/collector.proto delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/proto/api_v2/model.proto delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/__init__.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2_grpc.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/gogoproto/gogo_pb2.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/google/api/annotations_pb2.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/google/api/http_pb2.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/model_pb2.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/protoc_gen_swagger/options/annotations_pb2.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/protoc_gen_swagger/options/openapiv2_pb2.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/py.typed delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/send/__init__.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/util.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/__init__.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/certs/cred.cert delete mode 100644 exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/LICENSE delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/README.rst delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/examples/jaeger_exporter_example.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/__init__.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/Agent-remote delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/Agent.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/__init__.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/constants.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/ttypes.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/Collector-remote delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/Collector.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/__init__.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/constants.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/ttypes.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ZipkinCollector-remote delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ZipkinCollector.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/__init__.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/constants.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ttypes.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/py.typed delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/tests/__init__.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/tests/certs/cred.cert delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/thrift/agent.thrift delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/thrift/jaeger.thrift delete mode 100644 exporter/opentelemetry-exporter-jaeger-thrift/thrift/zipkincore.thrift delete mode 100644 exporter/opentelemetry-exporter-jaeger/LICENSE delete mode 100644 exporter/opentelemetry-exporter-jaeger/README.rst delete mode 100644 exporter/opentelemetry-exporter-jaeger/pyproject.toml delete mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/py.typed delete mode 100644 exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py delete mode 100644 exporter/opentelemetry-exporter-jaeger/tests/__init__.py delete mode 100644 exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py diff --git a/.flake8 b/.flake8 index 292bb01461..83786a686a 100644 --- a/.flake8 +++ b/.flake8 @@ -23,10 +23,6 @@ exclude = venv*/ target __pycache__ - exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/ - exporter/opentelemetry-exporter-jaeger-proto-grpc/build/* - exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/ - exporter/opentelemetry-exporter-jaeger-thrift/build/* exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen/ docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ docs/examples/opentelemetry-example-app/build/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f895dec6a..23c58969a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Remove Jaeger exporters + ([#3554](https://github.com/open-telemetry/opentelemetry-python/pull/3554)) - Log stacktrace on `UNKNOWN` status OTLP export error ([#3536](https://github.com/open-telemetry/opentelemetry-python/pull/3536)) - Fix OTLPExporterMixin shutdown timeout period diff --git a/docs/exporter/jaeger/jaeger.rst b/docs/exporter/jaeger/jaeger.rst deleted file mode 100644 index 1fc02948d8..0000000000 --- a/docs/exporter/jaeger/jaeger.rst +++ /dev/null @@ -1,35 +0,0 @@ -OpenTelemetry Jaeger Exporters -============================== - -.. automodule:: opentelemetry.exporter.jaeger - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: opentelemetry.exporter.jaeger.thrift - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: opentelemetry.exporter.jaeger.proto.grpc - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. automodule:: opentelemetry.exporter.jaeger.thrift.gen.jaeger.ttypes - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: opentelemetry.exporter.jaeger.thrift.send - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: opentelemetry.exporter.jaeger.proto.grpc.gen.collector_pb2_grpc - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/getting_started/jaeger_example.py b/docs/getting_started/jaeger_example.py deleted file mode 100644 index 3d31b18fb9..0000000000 --- a/docs/getting_started/jaeger_example.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# jaeger_example.py -from opentelemetry import trace -from opentelemetry.exporter.jaeger.thrift import JaegerExporter -from opentelemetry.sdk.resources import SERVICE_NAME, Resource -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor - -trace.set_tracer_provider( - TracerProvider( - resource=Resource.create({SERVICE_NAME: "my-helloworld-service"}) - ) -) - -jaeger_exporter = JaegerExporter( - agent_host_name="localhost", - agent_port=6831, -) - -trace.get_tracer_provider().add_span_processor( - BatchSpanProcessor(jaeger_exporter) -) - -tracer = trace.get_tracer(__name__) - -with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("baz"): - print("Hello world from OpenTelemetry Python!") diff --git a/eachdist.ini b/eachdist.ini index 3a2205712b..7d94c9e64c 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -24,9 +24,6 @@ packages= opentelemetry-exporter-otlp-proto-grpc opentelemetry-exporter-otlp-proto-http opentelemetry-exporter-otlp - opentelemetry-exporter-jaeger-thrift - opentelemetry-exporter-jaeger-proto-grpc - opentelemetry-exporter-jaeger opentelemetry-api [prerelease] diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/LICENSE b/exporter/opentelemetry-exporter-jaeger-proto-grpc/LICENSE deleted file mode 100644 index 1ef7dad2c5..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 The OpenTelemetry Authors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst b/exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst deleted file mode 100644 index f371fd0c3e..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/README.rst +++ /dev/null @@ -1,42 +0,0 @@ -OpenTelemetry Jaeger Protobuf Exporter -====================================== - -.. warning:: - Since v1.35, the Jaeger supports OTLP natively. Please use the OTLP exporter instead. - Support for this exporter will end July 2023. - - This package is no longer being tested. - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger-proto-grpc.svg - :target: https://pypi.org/project/opentelemetry-exporter-jaeger-proto-grpc/ - -This library allows to export tracing data to `Jaeger `_. - -Installation ------------- - -:: - - pip install opentelemetry-exporter-jaeger-proto-grpc - - -.. _Jaeger: https://www.jaegertracing.io/ -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ - -Configuration -------------- - -OpenTelemetry Jaeger Exporter can be configured by setting `JaegerExporter parameters -`_ or by setting -`environment variables `_ - -References ----------- - -* `OpenTelemetry Jaeger Exporter `_ -* `Jaeger `_ -* `OpenTelemetry Project `_ diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/examples/jaeger_exporter_example.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/examples/jaeger_exporter_example.py deleted file mode 100644 index dc3c539e75..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/examples/jaeger_exporter_example.py +++ /dev/null @@ -1,45 +0,0 @@ -import time - -from opentelemetry import trace -from opentelemetry.exporter.jaeger.proto import grpc -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor - -trace.set_tracer_provider(TracerProvider()) -tracer = trace.get_tracer(__name__) - -# Create a JaegerExporter to send spans with gRPC -# If there is no encryption or authentication set `insecure` to True -# If server has authentication with SSL/TLS you can set the -# parameter credentials=ChannelCredentials(...) or the environment variable -# `EXPORTER_JAEGER_CERTIFICATE` with file containing creds. - -jaeger_exporter = grpc.JaegerExporter( - collector_endpoint="localhost:14250", - insecure=True, -) - -# create a BatchSpanProcessor and add the exporter to it -span_processor = BatchSpanProcessor(jaeger_exporter) - -# add to the tracer factory -trace.get_tracer_provider().add_span_processor(span_processor) - -# create some spans for testing -with tracer.start_as_current_span("foo") as foo: - time.sleep(0.1) - foo.set_attribute("my_atribbute", True) - foo.add_event("event in foo", {"name": "foo1"}) - with tracer.start_as_current_span( - "bar", links=[trace.Link(foo.get_span_context())] - ) as bar: - time.sleep(0.2) - bar.set_attribute("speed", 100.0) - - with tracer.start_as_current_span("baz") as baz: - time.sleep(0.3) - baz.set_attribute("name", "mauricio") - - time.sleep(0.2) - - time.sleep(0.1) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/proto/api_v2/collector.proto b/exporter/opentelemetry-exporter-jaeger-proto-grpc/proto/api_v2/collector.proto deleted file mode 100644 index e897a043cd..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/proto/api_v2/collector.proto +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2019 The Jaeger Authors. -// Copyright (c) 2018 Uber Technologies, 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. - -syntax="proto3"; - -package jaeger.api_v2; - -import "model.proto"; -import "gogoproto/gogo.proto"; -import "google/api/annotations.proto"; -import "protoc-gen-swagger/options/annotations.proto"; - -option go_package = "api_v2"; -option java_package = "io.jaegertracing.api_v2"; - -// Enable gogoprotobuf extensions (https://github.com/gogo/protobuf/blob/master/extensions.md). -// Enable custom Marshal method. -option (gogoproto.marshaler_all) = true; -// Enable custom Unmarshal method. -option (gogoproto.unmarshaler_all) = true; -// Enable custom Size method (Required by Marshal and Unmarshal). -option (gogoproto.sizer_all) = true; -// Enable registration with golang/protobuf for the grpc-gateway. -option (gogoproto.goproto_registration) = true; - -option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { - info: { - version: "1.0"; - }; - external_docs: { - url: "https://github.com/jaegertracing/jaeger"; - description: "Jaeger API"; - } - schemes: HTTP; - schemes: HTTPS; -}; - -message PostSpansRequest { - Batch batch = 1 [ - (gogoproto.nullable) = false - ]; -} - -message PostSpansResponse { -} - -service CollectorService { - rpc PostSpans(PostSpansRequest) returns (PostSpansResponse) { - option (google.api.http) = { - post: "/api/v2/spans" - body: "*" - }; - } -} diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/proto/api_v2/model.proto b/exporter/opentelemetry-exporter-jaeger-proto-grpc/proto/api_v2/model.proto deleted file mode 100644 index 3cc15df3b8..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/proto/api_v2/model.proto +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) 2018 Uber Technologies, 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. - -syntax="proto3"; - -package jaeger.api_v2; - -import "gogoproto/gogo.proto"; -import "google/protobuf/timestamp.proto"; -import "google/protobuf/duration.proto"; - -// TODO: document all types and fields - -// TODO: once this moves to jaeger-idl repo, we may want to change Go pkg to api_v2 -// and rewrite it to model only in this repo. That should make it easier to generate -// classes in other languages. -option go_package = "model"; -option java_package = "io.jaegertracing.api_v2"; - -// Enable gogoprotobuf extensions (https://github.com/gogo/protobuf/blob/master/extensions.md). -// Enable custom Marshal method. -option (gogoproto.marshaler_all) = true; -// Enable custom Unmarshal method. -option (gogoproto.unmarshaler_all) = true; -// Enable custom Size method (Required by Marshal and Unmarshal). -option (gogoproto.sizer_all) = true; -// Enable registration with golang/protobuf for the grpc-gateway. -option (gogoproto.goproto_registration) = true; - -enum ValueType { - STRING = 0; - BOOL = 1; - INT64 = 2; - FLOAT64 = 3; - BINARY = 4; -}; - -message KeyValue { - option (gogoproto.equal) = true; - option (gogoproto.compare) = true; - - string key = 1; - ValueType v_type = 2; - string v_str = 3; - bool v_bool = 4; - int64 v_int64 = 5; - double v_float64 = 6; - bytes v_binary = 7; -} - -message Log { - google.protobuf.Timestamp timestamp = 1 [ - (gogoproto.stdtime) = true, - (gogoproto.nullable) = false - ]; - repeated KeyValue fields = 2 [ - (gogoproto.nullable) = false - ]; -} - -enum SpanRefType { - CHILD_OF = 0; - FOLLOWS_FROM = 1; -}; - -message SpanRef { - bytes trace_id = 1 [ - (gogoproto.nullable) = false, - (gogoproto.customtype) = "TraceID", - (gogoproto.customname) = "TraceID" - ]; - bytes span_id = 2 [ - (gogoproto.nullable) = false, - (gogoproto.customtype) = "SpanID", - (gogoproto.customname) = "SpanID" - ]; - SpanRefType ref_type = 3; -} - -message Process { - string service_name = 1; - repeated KeyValue tags = 2 [ - (gogoproto.nullable) = false - ]; -} - -message Span { - bytes trace_id = 1 [ - (gogoproto.nullable) = false, - (gogoproto.customtype) = "TraceID", - (gogoproto.customname) = "TraceID" - ]; - bytes span_id = 2 [ - (gogoproto.nullable) = false, - (gogoproto.customtype) = "SpanID", - (gogoproto.customname) = "SpanID" - ]; - string operation_name = 3; - repeated SpanRef references = 4 [ - (gogoproto.nullable) = false - ]; - uint32 flags = 5 [ - (gogoproto.nullable) = false, - (gogoproto.customtype) = "Flags" - ]; - google.protobuf.Timestamp start_time = 6 [ - (gogoproto.stdtime) = true, - (gogoproto.nullable) = false - ]; - google.protobuf.Duration duration = 7 [ - (gogoproto.stdduration) = true, - (gogoproto.nullable) = false - ]; - repeated KeyValue tags = 8 [ - (gogoproto.nullable) = false - ]; - repeated Log logs = 9 [ - (gogoproto.nullable) = false - ]; - Process process = 10; - string process_id = 11 [ - (gogoproto.customname) = "ProcessID" - ]; - repeated string warnings = 12; -} - -message Trace { - message ProcessMapping { - string process_id = 1 [ - (gogoproto.customname) = "ProcessID" - ]; - Process process = 2 [ - (gogoproto.nullable) = false - ]; - } - repeated Span spans = 1; - repeated ProcessMapping process_map = 2 [ - (gogoproto.nullable) = false - ]; - repeated string warnings = 3; -} - -message Batch { - repeated Span spans = 1; - Process process = 2 [ - (gogoproto.nullable) = true - ]; -} - -message DependencyLink { - string parent = 1; - string child = 2; - uint64 call_count = 3; - string source = 4; -} diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml deleted file mode 100644 index 131f12a0b4..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/pyproject.toml +++ /dev/null @@ -1,54 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "opentelemetry-exporter-jaeger-proto-grpc" -dynamic = ["version"] -description = "Jaeger Protobuf Exporter for OpenTelemetry" -readme = "README.rst" -license = "Apache-2.0" -requires-python = ">=3.7" -authors = [ - { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, -] -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Typing :: Typed", -] -dependencies = [ - "googleapis-common-protos ~= 1.52, < 1.60.0", - "grpcio >= 1.0.0, < 2.0.0", - "opentelemetry-api ~= 1.3", - "opentelemetry-sdk ~= 1.11", -] - -[project.optional-dependencies] -test = [] - -[project.entry-points.opentelemetry_traces_exporter] -jaeger_proto = "opentelemetry.exporter.jaeger.proto.grpc:JaegerExporter" - -[project.urls] -Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger-proto-grpc" - -[tool.hatch.version] -path = "src/opentelemetry/exporter/jaeger/proto/grpc/version.py" - -[tool.hatch.build.targets.sdist] -include = [ - "/src", - "/tests", -] - -[tool.hatch.build.targets.wheel] -packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py deleted file mode 100644 index f08b20e726..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/__init__.py +++ /dev/null @@ -1,196 +0,0 @@ -# Copyright 2018, OpenCensus Authors -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" - -OpenTelemetry Jaeger Protobuf Exporter --------------------------------------- - -The **OpenTelemetry Jaeger Protobuf Exporter** allows to export `OpenTelemetry`_ traces to `Jaeger`_. -This exporter always sends traces to the configured agent using Protobuf via gRPC. - -Usage ------ - -.. code:: python - - from opentelemetry import trace - from opentelemetry.exporter.jaeger.proto.grpc import JaegerExporter - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchSpanProcessor - - trace.set_tracer_provider(TracerProvider()) - tracer = trace.get_tracer(__name__) - - # create a JaegerExporter - jaeger_exporter = JaegerExporter( - # optional: configure collector - # collector_endpoint='localhost:14250', - # insecure=True, # optional - # credentials=xxx # optional channel creds - # max_tag_value_length=None # optional - ) - - # Create a BatchSpanProcessor and add the exporter to it - span_processor = BatchSpanProcessor(jaeger_exporter) - - # add to the tracer - trace.get_tracer_provider().add_span_processor(span_processor) - - with tracer.start_as_current_span('foo'): - print('Hello world!') - -You can configure the exporter with the following environment variables: - -- :envvar:`OTEL_EXPORTER_JAEGER_ENDPOINT` -- :envvar:`OTEL_EXPORTER_JAEGER_CERTIFICATE` -- :envvar:`OTEL_EXPORTER_JAEGER_TIMEOUT` - -API ---- -.. _Jaeger: https://www.jaegertracing.io/ -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ -""" -# pylint: disable=protected-access - -import logging -from os import environ -from typing import Optional -from deprecated import deprecated - -from grpc import ChannelCredentials, RpcError, insecure_channel, secure_channel - -from opentelemetry import trace -from opentelemetry.exporter.jaeger.proto.grpc import util -from opentelemetry.exporter.jaeger.proto.grpc.gen import model_pb2 -from opentelemetry.exporter.jaeger.proto.grpc.gen.collector_pb2 import ( - PostSpansRequest, -) -from opentelemetry.exporter.jaeger.proto.grpc.gen.collector_pb2_grpc import ( - CollectorServiceStub, -) -from opentelemetry.exporter.jaeger.proto.grpc.translate import ( - ProtobufTranslator, - Translate, -) -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_JAEGER_ENDPOINT, - OTEL_EXPORTER_JAEGER_TIMEOUT, - OTEL_EXPORTER_JAEGER_GRPC_INSECURE, -) -from opentelemetry.sdk.resources import SERVICE_NAME, Resource -from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult - -DEFAULT_GRPC_COLLECTOR_ENDPOINT = "localhost:14250" -DEFAULT_EXPORT_TIMEOUT = 10 - -logger = logging.getLogger(__name__) - - -class JaegerExporter(SpanExporter): - """Jaeger span exporter for OpenTelemetry. - - Args: - collector_endpoint: The endpoint of the Jaeger collector that uses - Protobuf via gRPC. - insecure: True if collector has no encryption or authentication - credentials: Credentials for server authentication. - max_tag_value_length: Max length string attribute values can have. Set to None to disable. - timeout: Maximum time the Jaeger exporter should wait for each batch export. - """ - - @deprecated( - version="1.16.0", - reason="Since v1.35, the Jaeger supports OTLP natively. Please use the OTLP exporter instead. Support for this exporter will end July 2023.", - ) - def __init__( - self, - collector_endpoint: Optional[str] = None, - insecure: Optional[bool] = None, - credentials: Optional[ChannelCredentials] = None, - max_tag_value_length: Optional[int] = None, - timeout: Optional[int] = None, - ): - self._max_tag_value_length = max_tag_value_length - - self.collector_endpoint = collector_endpoint or environ.get( - OTEL_EXPORTER_JAEGER_ENDPOINT, DEFAULT_GRPC_COLLECTOR_ENDPOINT - ) - self.insecure = ( - insecure - or environ.get(OTEL_EXPORTER_JAEGER_GRPC_INSECURE, "") - .strip() - .lower() - == "true" - ) - self._timeout = timeout or int( - environ.get(OTEL_EXPORTER_JAEGER_TIMEOUT, DEFAULT_EXPORT_TIMEOUT) - ) - self._grpc_client = None - self.credentials = util._get_credentials(credentials) - tracer_provider = trace.get_tracer_provider() - self.service_name = ( - tracer_provider.resource.attributes[SERVICE_NAME] - if getattr(tracer_provider, "resource", None) - else Resource.create().attributes.get(SERVICE_NAME) - ) - - @property - def _collector_grpc_client(self) -> Optional[CollectorServiceStub]: - - if self._grpc_client is None: - if self.insecure: - self._grpc_client = CollectorServiceStub( - insecure_channel(self.collector_endpoint) - ) - else: - self._grpc_client = CollectorServiceStub( - secure_channel(self.collector_endpoint, self.credentials) - ) - return self._grpc_client - - def export(self, spans) -> SpanExportResult: - # Populate service_name from first span - # We restrict any SpanProcessor to be only associated with a single - # TracerProvider, so it is safe to assume that all Spans in a single - # batch all originate from one TracerProvider (and in turn have all - # the same service.name) - if spans: - service_name = spans[0].resource.attributes.get(SERVICE_NAME) - if service_name: - self.service_name = service_name - translator = Translate(spans) - pb_translator = ProtobufTranslator( - self.service_name, self._max_tag_value_length - ) - jaeger_spans = translator._translate(pb_translator) - batch = model_pb2.Batch(spans=jaeger_spans) - request = PostSpansRequest(batch=batch) - try: - self._collector_grpc_client.PostSpans( - request, timeout=self._timeout - ) - return SpanExportResult.SUCCESS - except RpcError as error: - logger.warning( - "Failed to export batch. Status code: %s", error.code() - ) - return SpanExportResult.FAILURE - - def shutdown(self): - pass - - def force_flush(self, timeout_millis: int = 30000) -> bool: - return True diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/__init__.py deleted file mode 100644 index 52b3cfb3e9..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ - -import sys -from os.path import dirname -sys.path.append(dirname(__file__)) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2.py deleted file mode 100644 index cdb8562a94..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2.py +++ /dev/null @@ -1,134 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: collector.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -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() - - -from opentelemetry.exporter.jaeger.proto.grpc.gen import model_pb2 as model__pb2 -from gogoproto import gogo_pb2 as gogoproto_dot_gogo__pb2 -from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 -from protoc_gen_swagger.options import annotations_pb2 as protoc__gen__swagger_dot_options_dot_annotations__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='collector.proto', - package='jaeger.api_v2', - syntax='proto3', - serialized_options=_b('\n\027io.jaegertracing.api_v2Z\006api_v2\310\342\036\001\320\342\036\001\340\342\036\001\300\343\036\001\222AB\022\0052\0031.0*\002\001\002r5\n\nJaeger API\022\'https://github.com/jaegertracing/jaeger'), - serialized_pb=_b('\n\x0f\x63ollector.proto\x12\rjaeger.api_v2\x1a\x0bmodel.proto\x1a\x14gogoproto/gogo.proto\x1a\x1cgoogle/api/annotations.proto\x1a,protoc-gen-swagger/options/annotations.proto\"=\n\x10PostSpansRequest\x12)\n\x05\x62\x61tch\x18\x01 \x01(\x0b\x32\x14.jaeger.api_v2.BatchB\x04\xc8\xde\x1f\x00\"\x13\n\x11PostSpansResponse2|\n\x10\x43ollectorService\x12h\n\tPostSpans\x12\x1f.jaeger.api_v2.PostSpansRequest\x1a .jaeger.api_v2.PostSpansResponse\"\x18\x82\xd3\xe4\x93\x02\x12\"\r/api/v2/spans:\x01*Bv\n\x17io.jaegertracing.api_v2Z\x06\x61pi_v2\xc8\xe2\x1e\x01\xd0\xe2\x1e\x01\xe0\xe2\x1e\x01\xc0\xe3\x1e\x01\x92\x41\x42\x12\x05\x32\x03\x31.0*\x02\x01\x02r5\n\nJaeger API\x12\'https://github.com/jaegertracing/jaegerb\x06proto3') - , - dependencies=[model__pb2.DESCRIPTOR,gogoproto_dot_gogo__pb2.DESCRIPTOR,google_dot_api_dot_annotations__pb2.DESCRIPTOR,protoc__gen__swagger_dot_options_dot_annotations__pb2.DESCRIPTOR,]) - - - - -_POSTSPANSREQUEST = _descriptor.Descriptor( - name='PostSpansRequest', - full_name='jaeger.api_v2.PostSpansRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='batch', full_name='jaeger.api_v2.PostSpansRequest.batch', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=145, - serialized_end=206, -) - - -_POSTSPANSRESPONSE = _descriptor.Descriptor( - name='PostSpansResponse', - full_name='jaeger.api_v2.PostSpansResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=208, - serialized_end=227, -) - -_POSTSPANSREQUEST.fields_by_name['batch'].message_type = model__pb2._BATCH -DESCRIPTOR.message_types_by_name['PostSpansRequest'] = _POSTSPANSREQUEST -DESCRIPTOR.message_types_by_name['PostSpansResponse'] = _POSTSPANSRESPONSE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -PostSpansRequest = _reflection.GeneratedProtocolMessageType('PostSpansRequest', (_message.Message,), { - 'DESCRIPTOR' : _POSTSPANSREQUEST, - '__module__' : 'collector_pb2' - # @@protoc_insertion_point(class_scope:jaeger.api_v2.PostSpansRequest) - }) -_sym_db.RegisterMessage(PostSpansRequest) - -PostSpansResponse = _reflection.GeneratedProtocolMessageType('PostSpansResponse', (_message.Message,), { - 'DESCRIPTOR' : _POSTSPANSRESPONSE, - '__module__' : 'collector_pb2' - # @@protoc_insertion_point(class_scope:jaeger.api_v2.PostSpansResponse) - }) -_sym_db.RegisterMessage(PostSpansResponse) - - -DESCRIPTOR._options = None -_POSTSPANSREQUEST.fields_by_name['batch']._options = None - -_COLLECTORSERVICE = _descriptor.ServiceDescriptor( - name='CollectorService', - full_name='jaeger.api_v2.CollectorService', - file=DESCRIPTOR, - index=0, - serialized_options=None, - serialized_start=229, - serialized_end=353, - methods=[ - _descriptor.MethodDescriptor( - name='PostSpans', - full_name='jaeger.api_v2.CollectorService.PostSpans', - index=0, - containing_service=None, - input_type=_POSTSPANSREQUEST, - output_type=_POSTSPANSRESPONSE, - serialized_options=_b('\202\323\344\223\002\022\"\r/api/v2/spans:\001*'), - ), -]) -_sym_db.RegisterServiceDescriptor(_COLLECTORSERVICE) - -DESCRIPTOR.services_by_name['CollectorService'] = _COLLECTORSERVICE - -# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2_grpc.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2_grpc.py deleted file mode 100644 index b6fcc3592a..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/collector_pb2_grpc.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -import grpc - -import collector_pb2 as collector__pb2 - - -class CollectorServiceStub(object): - # missing associated documentation comment in .proto file - pass - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.PostSpans = channel.unary_unary( - '/jaeger.api_v2.CollectorService/PostSpans', - request_serializer=collector__pb2.PostSpansRequest.SerializeToString, - response_deserializer=collector__pb2.PostSpansResponse.FromString, - ) - - -class CollectorServiceServicer(object): - # missing associated documentation comment in .proto file - pass - - def PostSpans(self, request, context): - # missing associated documentation comment in .proto file - pass - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_CollectorServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'PostSpans': grpc.unary_unary_rpc_method_handler( - servicer.PostSpans, - request_deserializer=collector__pb2.PostSpansRequest.FromString, - response_serializer=collector__pb2.PostSpansResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'jaeger.api_v2.CollectorService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/gogoproto/gogo_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/gogoproto/gogo_pb2.py deleted file mode 100644 index 7268c1b693..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/gogoproto/gogo_pb2.py +++ /dev/null @@ -1,794 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: gogoproto/gogo.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -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() - - -from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='gogoproto/gogo.proto', - package='gogoproto', - syntax='proto2', - serialized_options=_b('\n\023com.google.protobufB\nGoGoProtosZ\"github.com/gogo/protobuf/gogoproto'), - serialized_pb=_b('\n\x14gogoproto/gogo.proto\x12\tgogoproto\x1a google/protobuf/descriptor.proto:;\n\x13goproto_enum_prefix\x12\x1c.google.protobuf.EnumOptions\x18\xb1\xe4\x03 \x01(\x08:=\n\x15goproto_enum_stringer\x12\x1c.google.protobuf.EnumOptions\x18\xc5\xe4\x03 \x01(\x08:5\n\renum_stringer\x12\x1c.google.protobuf.EnumOptions\x18\xc6\xe4\x03 \x01(\x08:7\n\x0f\x65num_customname\x12\x1c.google.protobuf.EnumOptions\x18\xc7\xe4\x03 \x01(\t:0\n\x08\x65numdecl\x12\x1c.google.protobuf.EnumOptions\x18\xc8\xe4\x03 \x01(\x08:A\n\x14\x65numvalue_customname\x12!.google.protobuf.EnumValueOptions\x18\xd1\x83\x04 \x01(\t:;\n\x13goproto_getters_all\x12\x1c.google.protobuf.FileOptions\x18\x99\xec\x03 \x01(\x08:?\n\x17goproto_enum_prefix_all\x12\x1c.google.protobuf.FileOptions\x18\x9a\xec\x03 \x01(\x08:<\n\x14goproto_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\x9b\xec\x03 \x01(\x08:9\n\x11verbose_equal_all\x12\x1c.google.protobuf.FileOptions\x18\x9c\xec\x03 \x01(\x08:0\n\x08\x66\x61\x63\x65_all\x12\x1c.google.protobuf.FileOptions\x18\x9d\xec\x03 \x01(\x08:4\n\x0cgostring_all\x12\x1c.google.protobuf.FileOptions\x18\x9e\xec\x03 \x01(\x08:4\n\x0cpopulate_all\x12\x1c.google.protobuf.FileOptions\x18\x9f\xec\x03 \x01(\x08:4\n\x0cstringer_all\x12\x1c.google.protobuf.FileOptions\x18\xa0\xec\x03 \x01(\x08:3\n\x0bonlyone_all\x12\x1c.google.protobuf.FileOptions\x18\xa1\xec\x03 \x01(\x08:1\n\tequal_all\x12\x1c.google.protobuf.FileOptions\x18\xa5\xec\x03 \x01(\x08:7\n\x0f\x64\x65scription_all\x12\x1c.google.protobuf.FileOptions\x18\xa6\xec\x03 \x01(\x08:3\n\x0btestgen_all\x12\x1c.google.protobuf.FileOptions\x18\xa7\xec\x03 \x01(\x08:4\n\x0c\x62\x65nchgen_all\x12\x1c.google.protobuf.FileOptions\x18\xa8\xec\x03 \x01(\x08:5\n\rmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xa9\xec\x03 \x01(\x08:7\n\x0funmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xaa\xec\x03 \x01(\x08:<\n\x14stable_marshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xab\xec\x03 \x01(\x08:1\n\tsizer_all\x12\x1c.google.protobuf.FileOptions\x18\xac\xec\x03 \x01(\x08:A\n\x19goproto_enum_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\xad\xec\x03 \x01(\x08:9\n\x11\x65num_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\xae\xec\x03 \x01(\x08:<\n\x14unsafe_marshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xaf\xec\x03 \x01(\x08:>\n\x16unsafe_unmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xb0\xec\x03 \x01(\x08:B\n\x1agoproto_extensions_map_all\x12\x1c.google.protobuf.FileOptions\x18\xb1\xec\x03 \x01(\x08:@\n\x18goproto_unrecognized_all\x12\x1c.google.protobuf.FileOptions\x18\xb2\xec\x03 \x01(\x08:8\n\x10gogoproto_import\x12\x1c.google.protobuf.FileOptions\x18\xb3\xec\x03 \x01(\x08:6\n\x0eprotosizer_all\x12\x1c.google.protobuf.FileOptions\x18\xb4\xec\x03 \x01(\x08:3\n\x0b\x63ompare_all\x12\x1c.google.protobuf.FileOptions\x18\xb5\xec\x03 \x01(\x08:4\n\x0ctypedecl_all\x12\x1c.google.protobuf.FileOptions\x18\xb6\xec\x03 \x01(\x08:4\n\x0c\x65numdecl_all\x12\x1c.google.protobuf.FileOptions\x18\xb7\xec\x03 \x01(\x08:<\n\x14goproto_registration\x12\x1c.google.protobuf.FileOptions\x18\xb8\xec\x03 \x01(\x08:7\n\x0fmessagename_all\x12\x1c.google.protobuf.FileOptions\x18\xb9\xec\x03 \x01(\x08:=\n\x15goproto_sizecache_all\x12\x1c.google.protobuf.FileOptions\x18\xba\xec\x03 \x01(\x08:;\n\x13goproto_unkeyed_all\x12\x1c.google.protobuf.FileOptions\x18\xbb\xec\x03 \x01(\x08::\n\x0fgoproto_getters\x12\x1f.google.protobuf.MessageOptions\x18\x81\xf4\x03 \x01(\x08:;\n\x10goproto_stringer\x12\x1f.google.protobuf.MessageOptions\x18\x83\xf4\x03 \x01(\x08:8\n\rverbose_equal\x12\x1f.google.protobuf.MessageOptions\x18\x84\xf4\x03 \x01(\x08:/\n\x04\x66\x61\x63\x65\x12\x1f.google.protobuf.MessageOptions\x18\x85\xf4\x03 \x01(\x08:3\n\x08gostring\x12\x1f.google.protobuf.MessageOptions\x18\x86\xf4\x03 \x01(\x08:3\n\x08populate\x12\x1f.google.protobuf.MessageOptions\x18\x87\xf4\x03 \x01(\x08:3\n\x08stringer\x12\x1f.google.protobuf.MessageOptions\x18\xc0\x8b\x04 \x01(\x08:2\n\x07onlyone\x12\x1f.google.protobuf.MessageOptions\x18\x89\xf4\x03 \x01(\x08:0\n\x05\x65qual\x12\x1f.google.protobuf.MessageOptions\x18\x8d\xf4\x03 \x01(\x08:6\n\x0b\x64\x65scription\x12\x1f.google.protobuf.MessageOptions\x18\x8e\xf4\x03 \x01(\x08:2\n\x07testgen\x12\x1f.google.protobuf.MessageOptions\x18\x8f\xf4\x03 \x01(\x08:3\n\x08\x62\x65nchgen\x12\x1f.google.protobuf.MessageOptions\x18\x90\xf4\x03 \x01(\x08:4\n\tmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x91\xf4\x03 \x01(\x08:6\n\x0bunmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x92\xf4\x03 \x01(\x08:;\n\x10stable_marshaler\x12\x1f.google.protobuf.MessageOptions\x18\x93\xf4\x03 \x01(\x08:0\n\x05sizer\x12\x1f.google.protobuf.MessageOptions\x18\x94\xf4\x03 \x01(\x08:;\n\x10unsafe_marshaler\x12\x1f.google.protobuf.MessageOptions\x18\x97\xf4\x03 \x01(\x08:=\n\x12unsafe_unmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x98\xf4\x03 \x01(\x08:A\n\x16goproto_extensions_map\x12\x1f.google.protobuf.MessageOptions\x18\x99\xf4\x03 \x01(\x08:?\n\x14goproto_unrecognized\x12\x1f.google.protobuf.MessageOptions\x18\x9a\xf4\x03 \x01(\x08:5\n\nprotosizer\x12\x1f.google.protobuf.MessageOptions\x18\x9c\xf4\x03 \x01(\x08:2\n\x07\x63ompare\x12\x1f.google.protobuf.MessageOptions\x18\x9d\xf4\x03 \x01(\x08:3\n\x08typedecl\x12\x1f.google.protobuf.MessageOptions\x18\x9e\xf4\x03 \x01(\x08:6\n\x0bmessagename\x12\x1f.google.protobuf.MessageOptions\x18\xa1\xf4\x03 \x01(\x08:<\n\x11goproto_sizecache\x12\x1f.google.protobuf.MessageOptions\x18\xa2\xf4\x03 \x01(\x08::\n\x0fgoproto_unkeyed\x12\x1f.google.protobuf.MessageOptions\x18\xa3\xf4\x03 \x01(\x08:1\n\x08nullable\x12\x1d.google.protobuf.FieldOptions\x18\xe9\xfb\x03 \x01(\x08:.\n\x05\x65mbed\x12\x1d.google.protobuf.FieldOptions\x18\xea\xfb\x03 \x01(\x08:3\n\ncustomtype\x12\x1d.google.protobuf.FieldOptions\x18\xeb\xfb\x03 \x01(\t:3\n\ncustomname\x12\x1d.google.protobuf.FieldOptions\x18\xec\xfb\x03 \x01(\t:0\n\x07jsontag\x12\x1d.google.protobuf.FieldOptions\x18\xed\xfb\x03 \x01(\t:1\n\x08moretags\x12\x1d.google.protobuf.FieldOptions\x18\xee\xfb\x03 \x01(\t:1\n\x08\x63\x61sttype\x12\x1d.google.protobuf.FieldOptions\x18\xef\xfb\x03 \x01(\t:0\n\x07\x63\x61stkey\x12\x1d.google.protobuf.FieldOptions\x18\xf0\xfb\x03 \x01(\t:2\n\tcastvalue\x12\x1d.google.protobuf.FieldOptions\x18\xf1\xfb\x03 \x01(\t:0\n\x07stdtime\x12\x1d.google.protobuf.FieldOptions\x18\xf2\xfb\x03 \x01(\x08:4\n\x0bstdduration\x12\x1d.google.protobuf.FieldOptions\x18\xf3\xfb\x03 \x01(\x08:3\n\nwktpointer\x12\x1d.google.protobuf.FieldOptions\x18\xf4\xfb\x03 \x01(\x08\x42\x45\n\x13\x63om.google.protobufB\nGoGoProtosZ\"github.com/gogo/protobuf/gogoproto') - , - dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,]) - - -GOPROTO_ENUM_PREFIX_FIELD_NUMBER = 62001 -goproto_enum_prefix = _descriptor.FieldDescriptor( - name='goproto_enum_prefix', full_name='gogoproto.goproto_enum_prefix', index=0, - number=62001, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_ENUM_STRINGER_FIELD_NUMBER = 62021 -goproto_enum_stringer = _descriptor.FieldDescriptor( - name='goproto_enum_stringer', full_name='gogoproto.goproto_enum_stringer', index=1, - number=62021, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -ENUM_STRINGER_FIELD_NUMBER = 62022 -enum_stringer = _descriptor.FieldDescriptor( - name='enum_stringer', full_name='gogoproto.enum_stringer', index=2, - number=62022, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -ENUM_CUSTOMNAME_FIELD_NUMBER = 62023 -enum_customname = _descriptor.FieldDescriptor( - name='enum_customname', full_name='gogoproto.enum_customname', index=3, - number=62023, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -ENUMDECL_FIELD_NUMBER = 62024 -enumdecl = _descriptor.FieldDescriptor( - name='enumdecl', full_name='gogoproto.enumdecl', index=4, - number=62024, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -ENUMVALUE_CUSTOMNAME_FIELD_NUMBER = 66001 -enumvalue_customname = _descriptor.FieldDescriptor( - name='enumvalue_customname', full_name='gogoproto.enumvalue_customname', index=5, - number=66001, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_GETTERS_ALL_FIELD_NUMBER = 63001 -goproto_getters_all = _descriptor.FieldDescriptor( - name='goproto_getters_all', full_name='gogoproto.goproto_getters_all', index=6, - number=63001, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_ENUM_PREFIX_ALL_FIELD_NUMBER = 63002 -goproto_enum_prefix_all = _descriptor.FieldDescriptor( - name='goproto_enum_prefix_all', full_name='gogoproto.goproto_enum_prefix_all', index=7, - number=63002, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_STRINGER_ALL_FIELD_NUMBER = 63003 -goproto_stringer_all = _descriptor.FieldDescriptor( - name='goproto_stringer_all', full_name='gogoproto.goproto_stringer_all', index=8, - number=63003, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -VERBOSE_EQUAL_ALL_FIELD_NUMBER = 63004 -verbose_equal_all = _descriptor.FieldDescriptor( - name='verbose_equal_all', full_name='gogoproto.verbose_equal_all', index=9, - number=63004, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -FACE_ALL_FIELD_NUMBER = 63005 -face_all = _descriptor.FieldDescriptor( - name='face_all', full_name='gogoproto.face_all', index=10, - number=63005, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOSTRING_ALL_FIELD_NUMBER = 63006 -gostring_all = _descriptor.FieldDescriptor( - name='gostring_all', full_name='gogoproto.gostring_all', index=11, - number=63006, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -POPULATE_ALL_FIELD_NUMBER = 63007 -populate_all = _descriptor.FieldDescriptor( - name='populate_all', full_name='gogoproto.populate_all', index=12, - number=63007, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -STRINGER_ALL_FIELD_NUMBER = 63008 -stringer_all = _descriptor.FieldDescriptor( - name='stringer_all', full_name='gogoproto.stringer_all', index=13, - number=63008, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -ONLYONE_ALL_FIELD_NUMBER = 63009 -onlyone_all = _descriptor.FieldDescriptor( - name='onlyone_all', full_name='gogoproto.onlyone_all', index=14, - number=63009, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -EQUAL_ALL_FIELD_NUMBER = 63013 -equal_all = _descriptor.FieldDescriptor( - name='equal_all', full_name='gogoproto.equal_all', index=15, - number=63013, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -DESCRIPTION_ALL_FIELD_NUMBER = 63014 -description_all = _descriptor.FieldDescriptor( - name='description_all', full_name='gogoproto.description_all', index=16, - number=63014, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -TESTGEN_ALL_FIELD_NUMBER = 63015 -testgen_all = _descriptor.FieldDescriptor( - name='testgen_all', full_name='gogoproto.testgen_all', index=17, - number=63015, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -BENCHGEN_ALL_FIELD_NUMBER = 63016 -benchgen_all = _descriptor.FieldDescriptor( - name='benchgen_all', full_name='gogoproto.benchgen_all', index=18, - number=63016, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -MARSHALER_ALL_FIELD_NUMBER = 63017 -marshaler_all = _descriptor.FieldDescriptor( - name='marshaler_all', full_name='gogoproto.marshaler_all', index=19, - number=63017, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -UNMARSHALER_ALL_FIELD_NUMBER = 63018 -unmarshaler_all = _descriptor.FieldDescriptor( - name='unmarshaler_all', full_name='gogoproto.unmarshaler_all', index=20, - number=63018, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -STABLE_MARSHALER_ALL_FIELD_NUMBER = 63019 -stable_marshaler_all = _descriptor.FieldDescriptor( - name='stable_marshaler_all', full_name='gogoproto.stable_marshaler_all', index=21, - number=63019, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -SIZER_ALL_FIELD_NUMBER = 63020 -sizer_all = _descriptor.FieldDescriptor( - name='sizer_all', full_name='gogoproto.sizer_all', index=22, - number=63020, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_ENUM_STRINGER_ALL_FIELD_NUMBER = 63021 -goproto_enum_stringer_all = _descriptor.FieldDescriptor( - name='goproto_enum_stringer_all', full_name='gogoproto.goproto_enum_stringer_all', index=23, - number=63021, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -ENUM_STRINGER_ALL_FIELD_NUMBER = 63022 -enum_stringer_all = _descriptor.FieldDescriptor( - name='enum_stringer_all', full_name='gogoproto.enum_stringer_all', index=24, - number=63022, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -UNSAFE_MARSHALER_ALL_FIELD_NUMBER = 63023 -unsafe_marshaler_all = _descriptor.FieldDescriptor( - name='unsafe_marshaler_all', full_name='gogoproto.unsafe_marshaler_all', index=25, - number=63023, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -UNSAFE_UNMARSHALER_ALL_FIELD_NUMBER = 63024 -unsafe_unmarshaler_all = _descriptor.FieldDescriptor( - name='unsafe_unmarshaler_all', full_name='gogoproto.unsafe_unmarshaler_all', index=26, - number=63024, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_EXTENSIONS_MAP_ALL_FIELD_NUMBER = 63025 -goproto_extensions_map_all = _descriptor.FieldDescriptor( - name='goproto_extensions_map_all', full_name='gogoproto.goproto_extensions_map_all', index=27, - number=63025, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_UNRECOGNIZED_ALL_FIELD_NUMBER = 63026 -goproto_unrecognized_all = _descriptor.FieldDescriptor( - name='goproto_unrecognized_all', full_name='gogoproto.goproto_unrecognized_all', index=28, - number=63026, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOGOPROTO_IMPORT_FIELD_NUMBER = 63027 -gogoproto_import = _descriptor.FieldDescriptor( - name='gogoproto_import', full_name='gogoproto.gogoproto_import', index=29, - number=63027, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -PROTOSIZER_ALL_FIELD_NUMBER = 63028 -protosizer_all = _descriptor.FieldDescriptor( - name='protosizer_all', full_name='gogoproto.protosizer_all', index=30, - number=63028, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -COMPARE_ALL_FIELD_NUMBER = 63029 -compare_all = _descriptor.FieldDescriptor( - name='compare_all', full_name='gogoproto.compare_all', index=31, - number=63029, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -TYPEDECL_ALL_FIELD_NUMBER = 63030 -typedecl_all = _descriptor.FieldDescriptor( - name='typedecl_all', full_name='gogoproto.typedecl_all', index=32, - number=63030, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -ENUMDECL_ALL_FIELD_NUMBER = 63031 -enumdecl_all = _descriptor.FieldDescriptor( - name='enumdecl_all', full_name='gogoproto.enumdecl_all', index=33, - number=63031, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_REGISTRATION_FIELD_NUMBER = 63032 -goproto_registration = _descriptor.FieldDescriptor( - name='goproto_registration', full_name='gogoproto.goproto_registration', index=34, - number=63032, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -MESSAGENAME_ALL_FIELD_NUMBER = 63033 -messagename_all = _descriptor.FieldDescriptor( - name='messagename_all', full_name='gogoproto.messagename_all', index=35, - number=63033, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_SIZECACHE_ALL_FIELD_NUMBER = 63034 -goproto_sizecache_all = _descriptor.FieldDescriptor( - name='goproto_sizecache_all', full_name='gogoproto.goproto_sizecache_all', index=36, - number=63034, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_UNKEYED_ALL_FIELD_NUMBER = 63035 -goproto_unkeyed_all = _descriptor.FieldDescriptor( - name='goproto_unkeyed_all', full_name='gogoproto.goproto_unkeyed_all', index=37, - number=63035, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_GETTERS_FIELD_NUMBER = 64001 -goproto_getters = _descriptor.FieldDescriptor( - name='goproto_getters', full_name='gogoproto.goproto_getters', index=38, - number=64001, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_STRINGER_FIELD_NUMBER = 64003 -goproto_stringer = _descriptor.FieldDescriptor( - name='goproto_stringer', full_name='gogoproto.goproto_stringer', index=39, - number=64003, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -VERBOSE_EQUAL_FIELD_NUMBER = 64004 -verbose_equal = _descriptor.FieldDescriptor( - name='verbose_equal', full_name='gogoproto.verbose_equal', index=40, - number=64004, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -FACE_FIELD_NUMBER = 64005 -face = _descriptor.FieldDescriptor( - name='face', full_name='gogoproto.face', index=41, - number=64005, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOSTRING_FIELD_NUMBER = 64006 -gostring = _descriptor.FieldDescriptor( - name='gostring', full_name='gogoproto.gostring', index=42, - number=64006, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -POPULATE_FIELD_NUMBER = 64007 -populate = _descriptor.FieldDescriptor( - name='populate', full_name='gogoproto.populate', index=43, - number=64007, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -STRINGER_FIELD_NUMBER = 67008 -stringer = _descriptor.FieldDescriptor( - name='stringer', full_name='gogoproto.stringer', index=44, - number=67008, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -ONLYONE_FIELD_NUMBER = 64009 -onlyone = _descriptor.FieldDescriptor( - name='onlyone', full_name='gogoproto.onlyone', index=45, - number=64009, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -EQUAL_FIELD_NUMBER = 64013 -equal = _descriptor.FieldDescriptor( - name='equal', full_name='gogoproto.equal', index=46, - number=64013, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -DESCRIPTION_FIELD_NUMBER = 64014 -description = _descriptor.FieldDescriptor( - name='description', full_name='gogoproto.description', index=47, - number=64014, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -TESTGEN_FIELD_NUMBER = 64015 -testgen = _descriptor.FieldDescriptor( - name='testgen', full_name='gogoproto.testgen', index=48, - number=64015, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -BENCHGEN_FIELD_NUMBER = 64016 -benchgen = _descriptor.FieldDescriptor( - name='benchgen', full_name='gogoproto.benchgen', index=49, - number=64016, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -MARSHALER_FIELD_NUMBER = 64017 -marshaler = _descriptor.FieldDescriptor( - name='marshaler', full_name='gogoproto.marshaler', index=50, - number=64017, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -UNMARSHALER_FIELD_NUMBER = 64018 -unmarshaler = _descriptor.FieldDescriptor( - name='unmarshaler', full_name='gogoproto.unmarshaler', index=51, - number=64018, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -STABLE_MARSHALER_FIELD_NUMBER = 64019 -stable_marshaler = _descriptor.FieldDescriptor( - name='stable_marshaler', full_name='gogoproto.stable_marshaler', index=52, - number=64019, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -SIZER_FIELD_NUMBER = 64020 -sizer = _descriptor.FieldDescriptor( - name='sizer', full_name='gogoproto.sizer', index=53, - number=64020, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -UNSAFE_MARSHALER_FIELD_NUMBER = 64023 -unsafe_marshaler = _descriptor.FieldDescriptor( - name='unsafe_marshaler', full_name='gogoproto.unsafe_marshaler', index=54, - number=64023, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -UNSAFE_UNMARSHALER_FIELD_NUMBER = 64024 -unsafe_unmarshaler = _descriptor.FieldDescriptor( - name='unsafe_unmarshaler', full_name='gogoproto.unsafe_unmarshaler', index=55, - number=64024, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_EXTENSIONS_MAP_FIELD_NUMBER = 64025 -goproto_extensions_map = _descriptor.FieldDescriptor( - name='goproto_extensions_map', full_name='gogoproto.goproto_extensions_map', index=56, - number=64025, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_UNRECOGNIZED_FIELD_NUMBER = 64026 -goproto_unrecognized = _descriptor.FieldDescriptor( - name='goproto_unrecognized', full_name='gogoproto.goproto_unrecognized', index=57, - number=64026, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -PROTOSIZER_FIELD_NUMBER = 64028 -protosizer = _descriptor.FieldDescriptor( - name='protosizer', full_name='gogoproto.protosizer', index=58, - number=64028, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -COMPARE_FIELD_NUMBER = 64029 -compare = _descriptor.FieldDescriptor( - name='compare', full_name='gogoproto.compare', index=59, - number=64029, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -TYPEDECL_FIELD_NUMBER = 64030 -typedecl = _descriptor.FieldDescriptor( - name='typedecl', full_name='gogoproto.typedecl', index=60, - number=64030, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -MESSAGENAME_FIELD_NUMBER = 64033 -messagename = _descriptor.FieldDescriptor( - name='messagename', full_name='gogoproto.messagename', index=61, - number=64033, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_SIZECACHE_FIELD_NUMBER = 64034 -goproto_sizecache = _descriptor.FieldDescriptor( - name='goproto_sizecache', full_name='gogoproto.goproto_sizecache', index=62, - number=64034, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -GOPROTO_UNKEYED_FIELD_NUMBER = 64035 -goproto_unkeyed = _descriptor.FieldDescriptor( - name='goproto_unkeyed', full_name='gogoproto.goproto_unkeyed', index=63, - number=64035, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -NULLABLE_FIELD_NUMBER = 65001 -nullable = _descriptor.FieldDescriptor( - name='nullable', full_name='gogoproto.nullable', index=64, - number=65001, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -EMBED_FIELD_NUMBER = 65002 -embed = _descriptor.FieldDescriptor( - name='embed', full_name='gogoproto.embed', index=65, - number=65002, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -CUSTOMTYPE_FIELD_NUMBER = 65003 -customtype = _descriptor.FieldDescriptor( - name='customtype', full_name='gogoproto.customtype', index=66, - number=65003, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -CUSTOMNAME_FIELD_NUMBER = 65004 -customname = _descriptor.FieldDescriptor( - name='customname', full_name='gogoproto.customname', index=67, - number=65004, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -JSONTAG_FIELD_NUMBER = 65005 -jsontag = _descriptor.FieldDescriptor( - name='jsontag', full_name='gogoproto.jsontag', index=68, - number=65005, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -MORETAGS_FIELD_NUMBER = 65006 -moretags = _descriptor.FieldDescriptor( - name='moretags', full_name='gogoproto.moretags', index=69, - number=65006, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -CASTTYPE_FIELD_NUMBER = 65007 -casttype = _descriptor.FieldDescriptor( - name='casttype', full_name='gogoproto.casttype', index=70, - number=65007, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -CASTKEY_FIELD_NUMBER = 65008 -castkey = _descriptor.FieldDescriptor( - name='castkey', full_name='gogoproto.castkey', index=71, - number=65008, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -CASTVALUE_FIELD_NUMBER = 65009 -castvalue = _descriptor.FieldDescriptor( - name='castvalue', full_name='gogoproto.castvalue', index=72, - number=65009, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -STDTIME_FIELD_NUMBER = 65010 -stdtime = _descriptor.FieldDescriptor( - name='stdtime', full_name='gogoproto.stdtime', index=73, - number=65010, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -STDDURATION_FIELD_NUMBER = 65011 -stdduration = _descriptor.FieldDescriptor( - name='stdduration', full_name='gogoproto.stdduration', index=74, - number=65011, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -WKTPOINTER_FIELD_NUMBER = 65012 -wktpointer = _descriptor.FieldDescriptor( - name='wktpointer', full_name='gogoproto.wktpointer', index=75, - number=65012, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) - -DESCRIPTOR.extensions_by_name['goproto_enum_prefix'] = goproto_enum_prefix -DESCRIPTOR.extensions_by_name['goproto_enum_stringer'] = goproto_enum_stringer -DESCRIPTOR.extensions_by_name['enum_stringer'] = enum_stringer -DESCRIPTOR.extensions_by_name['enum_customname'] = enum_customname -DESCRIPTOR.extensions_by_name['enumdecl'] = enumdecl -DESCRIPTOR.extensions_by_name['enumvalue_customname'] = enumvalue_customname -DESCRIPTOR.extensions_by_name['goproto_getters_all'] = goproto_getters_all -DESCRIPTOR.extensions_by_name['goproto_enum_prefix_all'] = goproto_enum_prefix_all -DESCRIPTOR.extensions_by_name['goproto_stringer_all'] = goproto_stringer_all -DESCRIPTOR.extensions_by_name['verbose_equal_all'] = verbose_equal_all -DESCRIPTOR.extensions_by_name['face_all'] = face_all -DESCRIPTOR.extensions_by_name['gostring_all'] = gostring_all -DESCRIPTOR.extensions_by_name['populate_all'] = populate_all -DESCRIPTOR.extensions_by_name['stringer_all'] = stringer_all -DESCRIPTOR.extensions_by_name['onlyone_all'] = onlyone_all -DESCRIPTOR.extensions_by_name['equal_all'] = equal_all -DESCRIPTOR.extensions_by_name['description_all'] = description_all -DESCRIPTOR.extensions_by_name['testgen_all'] = testgen_all -DESCRIPTOR.extensions_by_name['benchgen_all'] = benchgen_all -DESCRIPTOR.extensions_by_name['marshaler_all'] = marshaler_all -DESCRIPTOR.extensions_by_name['unmarshaler_all'] = unmarshaler_all -DESCRIPTOR.extensions_by_name['stable_marshaler_all'] = stable_marshaler_all -DESCRIPTOR.extensions_by_name['sizer_all'] = sizer_all -DESCRIPTOR.extensions_by_name['goproto_enum_stringer_all'] = goproto_enum_stringer_all -DESCRIPTOR.extensions_by_name['enum_stringer_all'] = enum_stringer_all -DESCRIPTOR.extensions_by_name['unsafe_marshaler_all'] = unsafe_marshaler_all -DESCRIPTOR.extensions_by_name['unsafe_unmarshaler_all'] = unsafe_unmarshaler_all -DESCRIPTOR.extensions_by_name['goproto_extensions_map_all'] = goproto_extensions_map_all -DESCRIPTOR.extensions_by_name['goproto_unrecognized_all'] = goproto_unrecognized_all -DESCRIPTOR.extensions_by_name['gogoproto_import'] = gogoproto_import -DESCRIPTOR.extensions_by_name['protosizer_all'] = protosizer_all -DESCRIPTOR.extensions_by_name['compare_all'] = compare_all -DESCRIPTOR.extensions_by_name['typedecl_all'] = typedecl_all -DESCRIPTOR.extensions_by_name['enumdecl_all'] = enumdecl_all -DESCRIPTOR.extensions_by_name['goproto_registration'] = goproto_registration -DESCRIPTOR.extensions_by_name['messagename_all'] = messagename_all -DESCRIPTOR.extensions_by_name['goproto_sizecache_all'] = goproto_sizecache_all -DESCRIPTOR.extensions_by_name['goproto_unkeyed_all'] = goproto_unkeyed_all -DESCRIPTOR.extensions_by_name['goproto_getters'] = goproto_getters -DESCRIPTOR.extensions_by_name['goproto_stringer'] = goproto_stringer -DESCRIPTOR.extensions_by_name['verbose_equal'] = verbose_equal -DESCRIPTOR.extensions_by_name['face'] = face -DESCRIPTOR.extensions_by_name['gostring'] = gostring -DESCRIPTOR.extensions_by_name['populate'] = populate -DESCRIPTOR.extensions_by_name['stringer'] = stringer -DESCRIPTOR.extensions_by_name['onlyone'] = onlyone -DESCRIPTOR.extensions_by_name['equal'] = equal -DESCRIPTOR.extensions_by_name['description'] = description -DESCRIPTOR.extensions_by_name['testgen'] = testgen -DESCRIPTOR.extensions_by_name['benchgen'] = benchgen -DESCRIPTOR.extensions_by_name['marshaler'] = marshaler -DESCRIPTOR.extensions_by_name['unmarshaler'] = unmarshaler -DESCRIPTOR.extensions_by_name['stable_marshaler'] = stable_marshaler -DESCRIPTOR.extensions_by_name['sizer'] = sizer -DESCRIPTOR.extensions_by_name['unsafe_marshaler'] = unsafe_marshaler -DESCRIPTOR.extensions_by_name['unsafe_unmarshaler'] = unsafe_unmarshaler -DESCRIPTOR.extensions_by_name['goproto_extensions_map'] = goproto_extensions_map -DESCRIPTOR.extensions_by_name['goproto_unrecognized'] = goproto_unrecognized -DESCRIPTOR.extensions_by_name['protosizer'] = protosizer -DESCRIPTOR.extensions_by_name['compare'] = compare -DESCRIPTOR.extensions_by_name['typedecl'] = typedecl -DESCRIPTOR.extensions_by_name['messagename'] = messagename -DESCRIPTOR.extensions_by_name['goproto_sizecache'] = goproto_sizecache -DESCRIPTOR.extensions_by_name['goproto_unkeyed'] = goproto_unkeyed -DESCRIPTOR.extensions_by_name['nullable'] = nullable -DESCRIPTOR.extensions_by_name['embed'] = embed -DESCRIPTOR.extensions_by_name['customtype'] = customtype -DESCRIPTOR.extensions_by_name['customname'] = customname -DESCRIPTOR.extensions_by_name['jsontag'] = jsontag -DESCRIPTOR.extensions_by_name['moretags'] = moretags -DESCRIPTOR.extensions_by_name['casttype'] = casttype -DESCRIPTOR.extensions_by_name['castkey'] = castkey -DESCRIPTOR.extensions_by_name['castvalue'] = castvalue -DESCRIPTOR.extensions_by_name['stdtime'] = stdtime -DESCRIPTOR.extensions_by_name['stdduration'] = stdduration -DESCRIPTOR.extensions_by_name['wktpointer'] = wktpointer -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension(goproto_enum_prefix) -google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension(goproto_enum_stringer) -google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension(enum_stringer) -google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension(enum_customname) -google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension(enumdecl) -google_dot_protobuf_dot_descriptor__pb2.EnumValueOptions.RegisterExtension(enumvalue_customname) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_getters_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_enum_prefix_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_stringer_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(verbose_equal_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(face_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(gostring_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(populate_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(stringer_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(onlyone_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(equal_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(description_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(testgen_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(benchgen_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(marshaler_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(unmarshaler_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(stable_marshaler_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(sizer_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_enum_stringer_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(enum_stringer_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(unsafe_marshaler_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(unsafe_unmarshaler_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_extensions_map_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_unrecognized_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(gogoproto_import) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(protosizer_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(compare_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(typedecl_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(enumdecl_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_registration) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(messagename_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_sizecache_all) -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(goproto_unkeyed_all) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(goproto_getters) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(goproto_stringer) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(verbose_equal) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(face) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(gostring) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(populate) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(stringer) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(onlyone) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(equal) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(description) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(testgen) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(benchgen) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(marshaler) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(unmarshaler) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(stable_marshaler) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(sizer) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(unsafe_marshaler) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(unsafe_unmarshaler) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(goproto_extensions_map) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(goproto_unrecognized) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(protosizer) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(compare) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(typedecl) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(messagename) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(goproto_sizecache) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(goproto_unkeyed) -google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(nullable) -google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(embed) -google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(customtype) -google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(customname) -google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(jsontag) -google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(moretags) -google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(casttype) -google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(castkey) -google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(castvalue) -google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(stdtime) -google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(stdduration) -google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(wktpointer) - -DESCRIPTOR._options = None -# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/google/api/annotations_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/google/api/annotations_pb2.py deleted file mode 100644 index e72a7f8b74..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/google/api/annotations_pb2.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/api/annotations.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -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() - - -from google.api import http_pb2 as google_dot_api_dot_http__pb2 -from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='google/api/annotations.proto', - package='google.api', - syntax='proto3', - serialized_options=_b('\n\016com.google.apiB\020AnnotationsProtoP\001ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\242\002\004GAPI'), - serialized_pb=_b('\n\x1cgoogle/api/annotations.proto\x12\ngoogle.api\x1a\x15google/api/http.proto\x1a google/protobuf/descriptor.proto:E\n\x04http\x12\x1e.google.protobuf.MethodOptions\x18\xb0\xca\xbc\" \x01(\x0b\x32\x14.google.api.HttpRuleBn\n\x0e\x63om.google.apiB\x10\x41nnotationsProtoP\x01ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\xa2\x02\x04GAPIb\x06proto3') - , - dependencies=[google_dot_api_dot_http__pb2.DESCRIPTOR,google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,]) - - -HTTP_FIELD_NUMBER = 72295728 -http = _descriptor.FieldDescriptor( - name='http', full_name='google.api.http', index=0, - number=72295728, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) - -DESCRIPTOR.extensions_by_name['http'] = http -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -http.message_type = google_dot_api_dot_http__pb2._HTTPRULE -google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(http) - -DESCRIPTOR._options = None -# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/google/api/http_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/google/api/http_pb2.py deleted file mode 100644 index 01352aca81..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/google/api/http_pb2.py +++ /dev/null @@ -1,250 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/api/http.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -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.FileDescriptor( - name='google/api/http.proto', - package='google.api', - syntax='proto3', - serialized_options=_b('\n\016com.google.apiB\tHttpProtoP\001ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\370\001\001\242\002\004GAPI'), - serialized_pb=_b('\n\x15google/api/http.proto\x12\ngoogle.api\"T\n\x04Http\x12#\n\x05rules\x18\x01 \x03(\x0b\x32\x14.google.api.HttpRule\x12\'\n\x1f\x66ully_decode_reserved_expansion\x18\x02 \x01(\x08\"\x81\x02\n\x08HttpRule\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\r\n\x03get\x18\x02 \x01(\tH\x00\x12\r\n\x03put\x18\x03 \x01(\tH\x00\x12\x0e\n\x04post\x18\x04 \x01(\tH\x00\x12\x10\n\x06\x64\x65lete\x18\x05 \x01(\tH\x00\x12\x0f\n\x05patch\x18\x06 \x01(\tH\x00\x12/\n\x06\x63ustom\x18\x08 \x01(\x0b\x32\x1d.google.api.CustomHttpPatternH\x00\x12\x0c\n\x04\x62ody\x18\x07 \x01(\t\x12\x15\n\rresponse_body\x18\x0c \x01(\t\x12\x31\n\x13\x61\x64\x64itional_bindings\x18\x0b \x03(\x0b\x32\x14.google.api.HttpRuleB\t\n\x07pattern\"/\n\x11\x43ustomHttpPattern\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\tBj\n\x0e\x63om.google.apiB\tHttpProtoP\x01ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\xf8\x01\x01\xa2\x02\x04GAPIb\x06proto3') -) - - - - -_HTTP = _descriptor.Descriptor( - name='Http', - full_name='google.api.Http', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='rules', full_name='google.api.Http.rules', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='fully_decode_reserved_expansion', full_name='google.api.Http.fully_decode_reserved_expansion', index=1, - number=2, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=37, - serialized_end=121, -) - - -_HTTPRULE = _descriptor.Descriptor( - name='HttpRule', - full_name='google.api.HttpRule', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='selector', full_name='google.api.HttpRule.selector', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='get', full_name='google.api.HttpRule.get', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='put', full_name='google.api.HttpRule.put', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='post', full_name='google.api.HttpRule.post', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='delete', full_name='google.api.HttpRule.delete', index=4, - number=5, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='patch', full_name='google.api.HttpRule.patch', index=5, - number=6, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='custom', full_name='google.api.HttpRule.custom', index=6, - number=8, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='body', full_name='google.api.HttpRule.body', index=7, - number=7, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='response_body', full_name='google.api.HttpRule.response_body', index=8, - number=12, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='additional_bindings', full_name='google.api.HttpRule.additional_bindings', index=9, - number=11, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='pattern', full_name='google.api.HttpRule.pattern', - index=0, containing_type=None, fields=[]), - ], - serialized_start=124, - serialized_end=381, -) - - -_CUSTOMHTTPPATTERN = _descriptor.Descriptor( - name='CustomHttpPattern', - full_name='google.api.CustomHttpPattern', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='kind', full_name='google.api.CustomHttpPattern.kind', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='path', full_name='google.api.CustomHttpPattern.path', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=383, - serialized_end=430, -) - -_HTTP.fields_by_name['rules'].message_type = _HTTPRULE -_HTTPRULE.fields_by_name['custom'].message_type = _CUSTOMHTTPPATTERN -_HTTPRULE.fields_by_name['additional_bindings'].message_type = _HTTPRULE -_HTTPRULE.oneofs_by_name['pattern'].fields.append( - _HTTPRULE.fields_by_name['get']) -_HTTPRULE.fields_by_name['get'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] -_HTTPRULE.oneofs_by_name['pattern'].fields.append( - _HTTPRULE.fields_by_name['put']) -_HTTPRULE.fields_by_name['put'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] -_HTTPRULE.oneofs_by_name['pattern'].fields.append( - _HTTPRULE.fields_by_name['post']) -_HTTPRULE.fields_by_name['post'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] -_HTTPRULE.oneofs_by_name['pattern'].fields.append( - _HTTPRULE.fields_by_name['delete']) -_HTTPRULE.fields_by_name['delete'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] -_HTTPRULE.oneofs_by_name['pattern'].fields.append( - _HTTPRULE.fields_by_name['patch']) -_HTTPRULE.fields_by_name['patch'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] -_HTTPRULE.oneofs_by_name['pattern'].fields.append( - _HTTPRULE.fields_by_name['custom']) -_HTTPRULE.fields_by_name['custom'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] -DESCRIPTOR.message_types_by_name['Http'] = _HTTP -DESCRIPTOR.message_types_by_name['HttpRule'] = _HTTPRULE -DESCRIPTOR.message_types_by_name['CustomHttpPattern'] = _CUSTOMHTTPPATTERN -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -Http = _reflection.GeneratedProtocolMessageType('Http', (_message.Message,), { - 'DESCRIPTOR' : _HTTP, - '__module__' : 'google.api.http_pb2' - # @@protoc_insertion_point(class_scope:google.api.Http) - }) -_sym_db.RegisterMessage(Http) - -HttpRule = _reflection.GeneratedProtocolMessageType('HttpRule', (_message.Message,), { - 'DESCRIPTOR' : _HTTPRULE, - '__module__' : 'google.api.http_pb2' - # @@protoc_insertion_point(class_scope:google.api.HttpRule) - }) -_sym_db.RegisterMessage(HttpRule) - -CustomHttpPattern = _reflection.GeneratedProtocolMessageType('CustomHttpPattern', (_message.Message,), { - 'DESCRIPTOR' : _CUSTOMHTTPPATTERN, - '__module__' : 'google.api.http_pb2' - # @@protoc_insertion_point(class_scope:google.api.CustomHttpPattern) - }) -_sym_db.RegisterMessage(CustomHttpPattern) - - -DESCRIPTOR._options = None -# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/model_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/model_pb2.py deleted file mode 100644 index 8abc302a62..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/model_pb2.py +++ /dev/null @@ -1,686 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: model.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf.internal import enum_type_wrapper -from google.protobuf import descriptor as _descriptor -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() - - -from gogoproto import gogo_pb2 as gogoproto_dot_gogo__pb2 -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='model.proto', - package='jaeger.api_v2', - syntax='proto3', - serialized_options=_b('\n\027io.jaegertracing.api_v2Z\005model\310\342\036\001\320\342\036\001\340\342\036\001\300\343\036\001'), - serialized_pb=_b('\n\x0bmodel.proto\x12\rjaeger.api_v2\x1a\x14gogoproto/gogo.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\"\xa0\x01\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12(\n\x06v_type\x18\x02 \x01(\x0e\x32\x18.jaeger.api_v2.ValueType\x12\r\n\x05v_str\x18\x03 \x01(\t\x12\x0e\n\x06v_bool\x18\x04 \x01(\x08\x12\x0f\n\x07v_int64\x18\x05 \x01(\x03\x12\x11\n\tv_float64\x18\x06 \x01(\x01\x12\x10\n\x08v_binary\x18\x07 \x01(\x0c:\x08\xe8\xa0\x1f\x01\xe8\xa1\x1f\x01\"m\n\x03Log\x12\x37\n\ttimestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x08\x90\xdf\x1f\x01\xc8\xde\x1f\x00\x12-\n\x06\x66ields\x18\x02 \x03(\x0b\x32\x17.jaeger.api_v2.KeyValueB\x04\xc8\xde\x1f\x00\"\x90\x01\n\x07SpanRef\x12,\n\x08trace_id\x18\x01 \x01(\x0c\x42\x1a\xc8\xde\x1f\x00\xda\xde\x1f\x07TraceID\xe2\xde\x1f\x07TraceID\x12)\n\x07span_id\x18\x02 \x01(\x0c\x42\x18\xc8\xde\x1f\x00\xda\xde\x1f\x06SpanID\xe2\xde\x1f\x06SpanID\x12,\n\x08ref_type\x18\x03 \x01(\x0e\x32\x1a.jaeger.api_v2.SpanRefType\"L\n\x07Process\x12\x14\n\x0cservice_name\x18\x01 \x01(\t\x12+\n\x04tags\x18\x02 \x03(\x0b\x32\x17.jaeger.api_v2.KeyValueB\x04\xc8\xde\x1f\x00\"\xeb\x03\n\x04Span\x12,\n\x08trace_id\x18\x01 \x01(\x0c\x42\x1a\xc8\xde\x1f\x00\xda\xde\x1f\x07TraceID\xe2\xde\x1f\x07TraceID\x12)\n\x07span_id\x18\x02 \x01(\x0c\x42\x18\xc8\xde\x1f\x00\xda\xde\x1f\x06SpanID\xe2\xde\x1f\x06SpanID\x12\x16\n\x0eoperation_name\x18\x03 \x01(\t\x12\x30\n\nreferences\x18\x04 \x03(\x0b\x32\x16.jaeger.api_v2.SpanRefB\x04\xc8\xde\x1f\x00\x12\x1c\n\x05\x66lags\x18\x05 \x01(\rB\r\xc8\xde\x1f\x00\xda\xde\x1f\x05\x46lags\x12\x38\n\nstart_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x08\x90\xdf\x1f\x01\xc8\xde\x1f\x00\x12\x35\n\x08\x64uration\x18\x07 \x01(\x0b\x32\x19.google.protobuf.DurationB\x08\x98\xdf\x1f\x01\xc8\xde\x1f\x00\x12+\n\x04tags\x18\x08 \x03(\x0b\x32\x17.jaeger.api_v2.KeyValueB\x04\xc8\xde\x1f\x00\x12&\n\x04logs\x18\t \x03(\x0b\x32\x12.jaeger.api_v2.LogB\x04\xc8\xde\x1f\x00\x12\'\n\x07process\x18\n \x01(\x0b\x32\x16.jaeger.api_v2.Process\x12!\n\nprocess_id\x18\x0b \x01(\tB\r\xe2\xde\x1f\tProcessID\x12\x10\n\x08warnings\x18\x0c \x03(\t\"\xe1\x01\n\x05Trace\x12\"\n\x05spans\x18\x01 \x03(\x0b\x32\x13.jaeger.api_v2.Span\x12>\n\x0bprocess_map\x18\x02 \x03(\x0b\x32#.jaeger.api_v2.Trace.ProcessMappingB\x04\xc8\xde\x1f\x00\x12\x10\n\x08warnings\x18\x03 \x03(\t\x1a\x62\n\x0eProcessMapping\x12!\n\nprocess_id\x18\x01 \x01(\tB\r\xe2\xde\x1f\tProcessID\x12-\n\x07process\x18\x02 \x01(\x0b\x32\x16.jaeger.api_v2.ProcessB\x04\xc8\xde\x1f\x00\"Z\n\x05\x42\x61tch\x12\"\n\x05spans\x18\x01 \x03(\x0b\x32\x13.jaeger.api_v2.Span\x12-\n\x07process\x18\x02 \x01(\x0b\x32\x16.jaeger.api_v2.ProcessB\x04\xc8\xde\x1f\x01\"S\n\x0e\x44\x65pendencyLink\x12\x0e\n\x06parent\x18\x01 \x01(\t\x12\r\n\x05\x63hild\x18\x02 \x01(\t\x12\x12\n\ncall_count\x18\x03 \x01(\x04\x12\x0e\n\x06source\x18\x04 \x01(\t*E\n\tValueType\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x42OOL\x10\x01\x12\t\n\x05INT64\x10\x02\x12\x0b\n\x07\x46LOAT64\x10\x03\x12\n\n\x06\x42INARY\x10\x04*-\n\x0bSpanRefType\x12\x0c\n\x08\x43HILD_OF\x10\x00\x12\x10\n\x0c\x46OLLOWS_FROM\x10\x01\x42\x30\n\x17io.jaegertracing.api_v2Z\x05model\xc8\xe2\x1e\x01\xd0\xe2\x1e\x01\xe0\xe2\x1e\x01\xc0\xe3\x1e\x01\x62\x06proto3') - , - dependencies=[gogoproto_dot_gogo__pb2.DESCRIPTOR,google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,google_dot_protobuf_dot_duration__pb2.DESCRIPTOR,]) - -_VALUETYPE = _descriptor.EnumDescriptor( - name='ValueType', - full_name='jaeger.api_v2.ValueType', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='STRING', index=0, number=0, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='BOOL', index=1, number=1, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='INT64', index=2, number=2, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='FLOAT64', index=3, number=3, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='BINARY', index=4, number=4, - serialized_options=None, - type=None), - ], - containing_type=None, - serialized_options=None, - serialized_start=1515, - serialized_end=1584, -) -_sym_db.RegisterEnumDescriptor(_VALUETYPE) - -ValueType = enum_type_wrapper.EnumTypeWrapper(_VALUETYPE) -_SPANREFTYPE = _descriptor.EnumDescriptor( - name='SpanRefType', - full_name='jaeger.api_v2.SpanRefType', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='CHILD_OF', index=0, number=0, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='FOLLOWS_FROM', index=1, number=1, - serialized_options=None, - type=None), - ], - containing_type=None, - serialized_options=None, - serialized_start=1586, - serialized_end=1631, -) -_sym_db.RegisterEnumDescriptor(_SPANREFTYPE) - -SpanRefType = enum_type_wrapper.EnumTypeWrapper(_SPANREFTYPE) -STRING = 0 -BOOL = 1 -INT64 = 2 -FLOAT64 = 3 -BINARY = 4 -CHILD_OF = 0 -FOLLOWS_FROM = 1 - - - -_KEYVALUE = _descriptor.Descriptor( - name='KeyValue', - full_name='jaeger.api_v2.KeyValue', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='jaeger.api_v2.KeyValue.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='v_type', full_name='jaeger.api_v2.KeyValue.v_type', index=1, - number=2, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='v_str', full_name='jaeger.api_v2.KeyValue.v_str', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='v_bool', full_name='jaeger.api_v2.KeyValue.v_bool', index=3, - number=4, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='v_int64', full_name='jaeger.api_v2.KeyValue.v_int64', index=4, - number=5, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='v_float64', full_name='jaeger.api_v2.KeyValue.v_float64', index=5, - number=6, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='v_binary', full_name='jaeger.api_v2.KeyValue.v_binary', index=6, - number=7, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=_b('\350\240\037\001\350\241\037\001'), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=118, - serialized_end=278, -) - - -_LOG = _descriptor.Descriptor( - name='Log', - full_name='jaeger.api_v2.Log', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='timestamp', full_name='jaeger.api_v2.Log.timestamp', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\220\337\037\001\310\336\037\000'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='fields', full_name='jaeger.api_v2.Log.fields', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=280, - serialized_end=389, -) - - -_SPANREF = _descriptor.Descriptor( - name='SpanRef', - full_name='jaeger.api_v2.SpanRef', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='trace_id', full_name='jaeger.api_v2.SpanRef.trace_id', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\000\332\336\037\007TraceID\342\336\037\007TraceID'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='span_id', full_name='jaeger.api_v2.SpanRef.span_id', index=1, - number=2, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\000\332\336\037\006SpanID\342\336\037\006SpanID'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='ref_type', full_name='jaeger.api_v2.SpanRef.ref_type', index=2, - number=3, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=392, - serialized_end=536, -) - - -_PROCESS = _descriptor.Descriptor( - name='Process', - full_name='jaeger.api_v2.Process', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='service_name', full_name='jaeger.api_v2.Process.service_name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='tags', full_name='jaeger.api_v2.Process.tags', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=538, - serialized_end=614, -) - - -_SPAN = _descriptor.Descriptor( - name='Span', - full_name='jaeger.api_v2.Span', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='trace_id', full_name='jaeger.api_v2.Span.trace_id', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\000\332\336\037\007TraceID\342\336\037\007TraceID'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='span_id', full_name='jaeger.api_v2.Span.span_id', index=1, - number=2, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\000\332\336\037\006SpanID\342\336\037\006SpanID'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='operation_name', full_name='jaeger.api_v2.Span.operation_name', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='references', full_name='jaeger.api_v2.Span.references', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='flags', full_name='jaeger.api_v2.Span.flags', index=4, - number=5, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\000\332\336\037\005Flags'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='start_time', full_name='jaeger.api_v2.Span.start_time', index=5, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\220\337\037\001\310\336\037\000'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='duration', full_name='jaeger.api_v2.Span.duration', index=6, - number=7, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\230\337\037\001\310\336\037\000'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='tags', full_name='jaeger.api_v2.Span.tags', index=7, - number=8, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='logs', full_name='jaeger.api_v2.Span.logs', index=8, - number=9, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='process', full_name='jaeger.api_v2.Span.process', index=9, - number=10, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='process_id', full_name='jaeger.api_v2.Span.process_id', index=10, - number=11, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\342\336\037\tProcessID'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='warnings', full_name='jaeger.api_v2.Span.warnings', index=11, - number=12, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=617, - serialized_end=1108, -) - - -_TRACE_PROCESSMAPPING = _descriptor.Descriptor( - name='ProcessMapping', - full_name='jaeger.api_v2.Trace.ProcessMapping', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='process_id', full_name='jaeger.api_v2.Trace.ProcessMapping.process_id', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\342\336\037\tProcessID'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='process', full_name='jaeger.api_v2.Trace.ProcessMapping.process', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1238, - serialized_end=1336, -) - -_TRACE = _descriptor.Descriptor( - name='Trace', - full_name='jaeger.api_v2.Trace', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='spans', full_name='jaeger.api_v2.Trace.spans', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='process_map', full_name='jaeger.api_v2.Trace.process_map', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\000'), file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='warnings', full_name='jaeger.api_v2.Trace.warnings', index=2, - number=3, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_TRACE_PROCESSMAPPING, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1111, - serialized_end=1336, -) - - -_BATCH = _descriptor.Descriptor( - name='Batch', - full_name='jaeger.api_v2.Batch', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='spans', full_name='jaeger.api_v2.Batch.spans', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='process', full_name='jaeger.api_v2.Batch.process', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=_b('\310\336\037\001'), file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1338, - serialized_end=1428, -) - - -_DEPENDENCYLINK = _descriptor.Descriptor( - name='DependencyLink', - full_name='jaeger.api_v2.DependencyLink', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='parent', full_name='jaeger.api_v2.DependencyLink.parent', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='child', full_name='jaeger.api_v2.DependencyLink.child', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='call_count', full_name='jaeger.api_v2.DependencyLink.call_count', index=2, - number=3, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='source', full_name='jaeger.api_v2.DependencyLink.source', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1430, - serialized_end=1513, -) - -_KEYVALUE.fields_by_name['v_type'].enum_type = _VALUETYPE -_LOG.fields_by_name['timestamp'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP -_LOG.fields_by_name['fields'].message_type = _KEYVALUE -_SPANREF.fields_by_name['ref_type'].enum_type = _SPANREFTYPE -_PROCESS.fields_by_name['tags'].message_type = _KEYVALUE -_SPAN.fields_by_name['references'].message_type = _SPANREF -_SPAN.fields_by_name['start_time'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP -_SPAN.fields_by_name['duration'].message_type = google_dot_protobuf_dot_duration__pb2._DURATION -_SPAN.fields_by_name['tags'].message_type = _KEYVALUE -_SPAN.fields_by_name['logs'].message_type = _LOG -_SPAN.fields_by_name['process'].message_type = _PROCESS -_TRACE_PROCESSMAPPING.fields_by_name['process'].message_type = _PROCESS -_TRACE_PROCESSMAPPING.containing_type = _TRACE -_TRACE.fields_by_name['spans'].message_type = _SPAN -_TRACE.fields_by_name['process_map'].message_type = _TRACE_PROCESSMAPPING -_BATCH.fields_by_name['spans'].message_type = _SPAN -_BATCH.fields_by_name['process'].message_type = _PROCESS -DESCRIPTOR.message_types_by_name['KeyValue'] = _KEYVALUE -DESCRIPTOR.message_types_by_name['Log'] = _LOG -DESCRIPTOR.message_types_by_name['SpanRef'] = _SPANREF -DESCRIPTOR.message_types_by_name['Process'] = _PROCESS -DESCRIPTOR.message_types_by_name['Span'] = _SPAN -DESCRIPTOR.message_types_by_name['Trace'] = _TRACE -DESCRIPTOR.message_types_by_name['Batch'] = _BATCH -DESCRIPTOR.message_types_by_name['DependencyLink'] = _DEPENDENCYLINK -DESCRIPTOR.enum_types_by_name['ValueType'] = _VALUETYPE -DESCRIPTOR.enum_types_by_name['SpanRefType'] = _SPANREFTYPE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -KeyValue = _reflection.GeneratedProtocolMessageType('KeyValue', (_message.Message,), { - 'DESCRIPTOR' : _KEYVALUE, - '__module__' : 'model_pb2' - # @@protoc_insertion_point(class_scope:jaeger.api_v2.KeyValue) - }) -_sym_db.RegisterMessage(KeyValue) - -Log = _reflection.GeneratedProtocolMessageType('Log', (_message.Message,), { - 'DESCRIPTOR' : _LOG, - '__module__' : 'model_pb2' - # @@protoc_insertion_point(class_scope:jaeger.api_v2.Log) - }) -_sym_db.RegisterMessage(Log) - -SpanRef = _reflection.GeneratedProtocolMessageType('SpanRef', (_message.Message,), { - 'DESCRIPTOR' : _SPANREF, - '__module__' : 'model_pb2' - # @@protoc_insertion_point(class_scope:jaeger.api_v2.SpanRef) - }) -_sym_db.RegisterMessage(SpanRef) - -Process = _reflection.GeneratedProtocolMessageType('Process', (_message.Message,), { - 'DESCRIPTOR' : _PROCESS, - '__module__' : 'model_pb2' - # @@protoc_insertion_point(class_scope:jaeger.api_v2.Process) - }) -_sym_db.RegisterMessage(Process) - -Span = _reflection.GeneratedProtocolMessageType('Span', (_message.Message,), { - 'DESCRIPTOR' : _SPAN, - '__module__' : 'model_pb2' - # @@protoc_insertion_point(class_scope:jaeger.api_v2.Span) - }) -_sym_db.RegisterMessage(Span) - -Trace = _reflection.GeneratedProtocolMessageType('Trace', (_message.Message,), { - - 'ProcessMapping' : _reflection.GeneratedProtocolMessageType('ProcessMapping', (_message.Message,), { - 'DESCRIPTOR' : _TRACE_PROCESSMAPPING, - '__module__' : 'model_pb2' - # @@protoc_insertion_point(class_scope:jaeger.api_v2.Trace.ProcessMapping) - }) - , - 'DESCRIPTOR' : _TRACE, - '__module__' : 'model_pb2' - # @@protoc_insertion_point(class_scope:jaeger.api_v2.Trace) - }) -_sym_db.RegisterMessage(Trace) -_sym_db.RegisterMessage(Trace.ProcessMapping) - -Batch = _reflection.GeneratedProtocolMessageType('Batch', (_message.Message,), { - 'DESCRIPTOR' : _BATCH, - '__module__' : 'model_pb2' - # @@protoc_insertion_point(class_scope:jaeger.api_v2.Batch) - }) -_sym_db.RegisterMessage(Batch) - -DependencyLink = _reflection.GeneratedProtocolMessageType('DependencyLink', (_message.Message,), { - 'DESCRIPTOR' : _DEPENDENCYLINK, - '__module__' : 'model_pb2' - # @@protoc_insertion_point(class_scope:jaeger.api_v2.DependencyLink) - }) -_sym_db.RegisterMessage(DependencyLink) - - -DESCRIPTOR._options = None -_KEYVALUE._options = None -_LOG.fields_by_name['timestamp']._options = None -_LOG.fields_by_name['fields']._options = None -_SPANREF.fields_by_name['trace_id']._options = None -_SPANREF.fields_by_name['span_id']._options = None -_PROCESS.fields_by_name['tags']._options = None -_SPAN.fields_by_name['trace_id']._options = None -_SPAN.fields_by_name['span_id']._options = None -_SPAN.fields_by_name['references']._options = None -_SPAN.fields_by_name['flags']._options = None -_SPAN.fields_by_name['start_time']._options = None -_SPAN.fields_by_name['duration']._options = None -_SPAN.fields_by_name['tags']._options = None -_SPAN.fields_by_name['logs']._options = None -_SPAN.fields_by_name['process_id']._options = None -_TRACE_PROCESSMAPPING.fields_by_name['process_id']._options = None -_TRACE_PROCESSMAPPING.fields_by_name['process']._options = None -_TRACE.fields_by_name['process_map']._options = None -_BATCH.fields_by_name['process']._options = None -# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/protoc_gen_swagger/options/annotations_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/protoc_gen_swagger/options/annotations_pb2.py deleted file mode 100644 index 42bc642368..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/protoc_gen_swagger/options/annotations_pb2.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: protoc-gen-swagger/options/annotations.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -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() - - -from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 -from protoc_gen_swagger.options import openapiv2_pb2 as protoc__gen__swagger_dot_options_dot_openapiv2__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='protoc-gen-swagger/options/annotations.proto', - package='grpc.gateway.protoc_gen_swagger.options', - syntax='proto3', - serialized_options=_b('ZAgithub.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options'), - serialized_pb=_b('\n,protoc-gen-swagger/options/annotations.proto\x12\'grpc.gateway.protoc_gen_swagger.options\x1a google/protobuf/descriptor.proto\x1a*protoc-gen-swagger/options/openapiv2.proto:j\n\x11openapiv2_swagger\x12\x1c.google.protobuf.FileOptions\x18\x92\x08 \x01(\x0b\x32\x30.grpc.gateway.protoc_gen_swagger.options.Swagger:p\n\x13openapiv2_operation\x12\x1e.google.protobuf.MethodOptions\x18\x92\x08 \x01(\x0b\x32\x32.grpc.gateway.protoc_gen_swagger.options.Operation:k\n\x10openapiv2_schema\x12\x1f.google.protobuf.MessageOptions\x18\x92\x08 \x01(\x0b\x32/.grpc.gateway.protoc_gen_swagger.options.Schema:e\n\ropenapiv2_tag\x12\x1f.google.protobuf.ServiceOptions\x18\x92\x08 \x01(\x0b\x32,.grpc.gateway.protoc_gen_swagger.options.Tag:l\n\x0fopenapiv2_field\x12\x1d.google.protobuf.FieldOptions\x18\x92\x08 \x01(\x0b\x32\x33.grpc.gateway.protoc_gen_swagger.options.JSONSchemaBCZAgithub.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/optionsb\x06proto3') - , - dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,protoc__gen__swagger_dot_options_dot_openapiv2__pb2.DESCRIPTOR,]) - - -OPENAPIV2_SWAGGER_FIELD_NUMBER = 1042 -openapiv2_swagger = _descriptor.FieldDescriptor( - name='openapiv2_swagger', full_name='grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger', index=0, - number=1042, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -OPENAPIV2_OPERATION_FIELD_NUMBER = 1042 -openapiv2_operation = _descriptor.FieldDescriptor( - name='openapiv2_operation', full_name='grpc.gateway.protoc_gen_swagger.options.openapiv2_operation', index=1, - number=1042, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -OPENAPIV2_SCHEMA_FIELD_NUMBER = 1042 -openapiv2_schema = _descriptor.FieldDescriptor( - name='openapiv2_schema', full_name='grpc.gateway.protoc_gen_swagger.options.openapiv2_schema', index=2, - number=1042, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -OPENAPIV2_TAG_FIELD_NUMBER = 1042 -openapiv2_tag = _descriptor.FieldDescriptor( - name='openapiv2_tag', full_name='grpc.gateway.protoc_gen_swagger.options.openapiv2_tag', index=3, - number=1042, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) -OPENAPIV2_FIELD_FIELD_NUMBER = 1042 -openapiv2_field = _descriptor.FieldDescriptor( - name='openapiv2_field', full_name='grpc.gateway.protoc_gen_swagger.options.openapiv2_field', index=4, - number=1042, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - serialized_options=None, file=DESCRIPTOR) - -DESCRIPTOR.extensions_by_name['openapiv2_swagger'] = openapiv2_swagger -DESCRIPTOR.extensions_by_name['openapiv2_operation'] = openapiv2_operation -DESCRIPTOR.extensions_by_name['openapiv2_schema'] = openapiv2_schema -DESCRIPTOR.extensions_by_name['openapiv2_tag'] = openapiv2_tag -DESCRIPTOR.extensions_by_name['openapiv2_field'] = openapiv2_field -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -openapiv2_swagger.message_type = protoc__gen__swagger_dot_options_dot_openapiv2__pb2._SWAGGER -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(openapiv2_swagger) -openapiv2_operation.message_type = protoc__gen__swagger_dot_options_dot_openapiv2__pb2._OPERATION -google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(openapiv2_operation) -openapiv2_schema.message_type = protoc__gen__swagger_dot_options_dot_openapiv2__pb2._SCHEMA -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(openapiv2_schema) -openapiv2_tag.message_type = protoc__gen__swagger_dot_options_dot_openapiv2__pb2._TAG -google_dot_protobuf_dot_descriptor__pb2.ServiceOptions.RegisterExtension(openapiv2_tag) -openapiv2_field.message_type = protoc__gen__swagger_dot_options_dot_openapiv2__pb2._JSONSCHEMA -google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(openapiv2_field) - -DESCRIPTOR._options = None -# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/protoc_gen_swagger/options/openapiv2_pb2.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/protoc_gen_swagger/options/openapiv2_pb2.py deleted file mode 100644 index c2e7aa0ba1..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen/protoc_gen_swagger/options/openapiv2_pb2.py +++ /dev/null @@ -1,1777 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: protoc-gen-swagger/options/openapiv2.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -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() - - -from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 -from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='protoc-gen-swagger/options/openapiv2.proto', - package='grpc.gateway.protoc_gen_swagger.options', - syntax='proto3', - serialized_options=_b('ZAgithub.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options'), - serialized_pb=_b('\n*protoc-gen-swagger/options/openapiv2.proto\x12\'grpc.gateway.protoc_gen_swagger.options\x1a\x19google/protobuf/any.proto\x1a\x1cgoogle/protobuf/struct.proto\"\xa0\x07\n\x07Swagger\x12\x0f\n\x07swagger\x18\x01 \x01(\t\x12;\n\x04info\x18\x02 \x01(\x0b\x32-.grpc.gateway.protoc_gen_swagger.options.Info\x12\x0c\n\x04host\x18\x03 \x01(\t\x12\x11\n\tbase_path\x18\x04 \x01(\t\x12O\n\x07schemes\x18\x05 \x03(\x0e\x32>.grpc.gateway.protoc_gen_swagger.options.Swagger.SwaggerScheme\x12\x10\n\x08\x63onsumes\x18\x06 \x03(\t\x12\x10\n\x08produces\x18\x07 \x03(\t\x12R\n\tresponses\x18\n \x03(\x0b\x32?.grpc.gateway.protoc_gen_swagger.options.Swagger.ResponsesEntry\x12Z\n\x14security_definitions\x18\x0b \x01(\x0b\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions\x12N\n\x08security\x18\x0c \x03(\x0b\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityRequirement\x12U\n\rexternal_docs\x18\x0e \x01(\x0b\x32>.grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation\x12T\n\nextensions\x18\x0f \x03(\x0b\x32@.grpc.gateway.protoc_gen_swagger.options.Swagger.ExtensionsEntry\x1a\x63\n\x0eResponsesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12@\n\x05value\x18\x02 \x01(\x0b\x32\x31.grpc.gateway.protoc_gen_swagger.options.Response:\x02\x38\x01\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"B\n\rSwaggerScheme\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x08\n\x04HTTP\x10\x01\x12\t\n\x05HTTPS\x10\x02\x12\x06\n\x02WS\x10\x03\x12\x07\n\x03WSS\x10\x04J\x04\x08\x08\x10\tJ\x04\x08\t\x10\nJ\x04\x08\r\x10\x0e\"\xa9\x05\n\tOperation\x12\x0c\n\x04tags\x18\x01 \x03(\t\x12\x0f\n\x07summary\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12U\n\rexternal_docs\x18\x04 \x01(\x0b\x32>.grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation\x12\x14\n\x0coperation_id\x18\x05 \x01(\t\x12\x10\n\x08\x63onsumes\x18\x06 \x03(\t\x12\x10\n\x08produces\x18\x07 \x03(\t\x12T\n\tresponses\x18\t \x03(\x0b\x32\x41.grpc.gateway.protoc_gen_swagger.options.Operation.ResponsesEntry\x12\x0f\n\x07schemes\x18\n \x03(\t\x12\x12\n\ndeprecated\x18\x0b \x01(\x08\x12N\n\x08security\x18\x0c \x03(\x0b\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityRequirement\x12V\n\nextensions\x18\r \x03(\x0b\x32\x42.grpc.gateway.protoc_gen_swagger.options.Operation.ExtensionsEntry\x1a\x63\n\x0eResponsesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12@\n\x05value\x18\x02 \x01(\x0b\x32\x31.grpc.gateway.protoc_gen_swagger.options.Response:\x02\x38\x01\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01J\x04\x08\x08\x10\t\"\x8e\x02\n\x08Response\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12?\n\x06schema\x18\x02 \x01(\x0b\x32/.grpc.gateway.protoc_gen_swagger.options.Schema\x12U\n\nextensions\x18\x05 \x03(\x0b\x32\x41.grpc.gateway.protoc_gen_swagger.options.Response.ExtensionsEntry\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01J\x04\x08\x03\x10\x04J\x04\x08\x04\x10\x05\"\xf9\x02\n\x04Info\x12\r\n\x05title\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x18\n\x10terms_of_service\x18\x03 \x01(\t\x12\x41\n\x07\x63ontact\x18\x04 \x01(\x0b\x32\x30.grpc.gateway.protoc_gen_swagger.options.Contact\x12\x41\n\x07license\x18\x05 \x01(\x0b\x32\x30.grpc.gateway.protoc_gen_swagger.options.License\x12\x0f\n\x07version\x18\x06 \x01(\t\x12Q\n\nextensions\x18\x07 \x03(\x0b\x32=.grpc.gateway.protoc_gen_swagger.options.Info.ExtensionsEntry\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"3\n\x07\x43ontact\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\"$\n\x07License\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\"9\n\x15\x45xternalDocumentation\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\"\x80\x02\n\x06Schema\x12H\n\x0bjson_schema\x18\x01 \x01(\x0b\x32\x33.grpc.gateway.protoc_gen_swagger.options.JSONSchema\x12\x15\n\rdiscriminator\x18\x02 \x01(\t\x12\x11\n\tread_only\x18\x03 \x01(\x08\x12U\n\rexternal_docs\x18\x05 \x01(\x0b\x32>.grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation\x12%\n\x07\x65xample\x18\x06 \x01(\x0b\x32\x14.google.protobuf.AnyJ\x04\x08\x04\x10\x05\"\xba\x05\n\nJSONSchema\x12\x0b\n\x03ref\x18\x03 \x01(\t\x12\r\n\x05title\x18\x05 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12\x0f\n\x07\x64\x65\x66\x61ult\x18\x07 \x01(\t\x12\x11\n\tread_only\x18\x08 \x01(\x08\x12\x13\n\x0bmultiple_of\x18\n \x01(\x01\x12\x0f\n\x07maximum\x18\x0b \x01(\x01\x12\x19\n\x11\x65xclusive_maximum\x18\x0c \x01(\x08\x12\x0f\n\x07minimum\x18\r \x01(\x01\x12\x19\n\x11\x65xclusive_minimum\x18\x0e \x01(\x08\x12\x12\n\nmax_length\x18\x0f \x01(\x04\x12\x12\n\nmin_length\x18\x10 \x01(\x04\x12\x0f\n\x07pattern\x18\x11 \x01(\t\x12\x11\n\tmax_items\x18\x14 \x01(\x04\x12\x11\n\tmin_items\x18\x15 \x01(\x04\x12\x14\n\x0cunique_items\x18\x16 \x01(\x08\x12\x16\n\x0emax_properties\x18\x18 \x01(\x04\x12\x16\n\x0emin_properties\x18\x19 \x01(\x04\x12\x10\n\x08required\x18\x1a \x03(\t\x12\r\n\x05\x61rray\x18\" \x03(\t\x12W\n\x04type\x18# \x03(\x0e\x32I.grpc.gateway.protoc_gen_swagger.options.JSONSchema.JSONSchemaSimpleTypes\"w\n\x15JSONSchemaSimpleTypes\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05\x41RRAY\x10\x01\x12\x0b\n\x07\x42OOLEAN\x10\x02\x12\x0b\n\x07INTEGER\x10\x03\x12\x08\n\x04NULL\x10\x04\x12\n\n\x06NUMBER\x10\x05\x12\n\n\x06OBJECT\x10\x06\x12\n\n\x06STRING\x10\x07J\x04\x08\x01\x10\x02J\x04\x08\x02\x10\x03J\x04\x08\x04\x10\x05J\x04\x08\t\x10\nJ\x04\x08\x12\x10\x13J\x04\x08\x13\x10\x14J\x04\x08\x17\x10\x18J\x04\x08\x1b\x10\x1cJ\x04\x08\x1c\x10\x1dJ\x04\x08\x1d\x10\x1eJ\x04\x08\x1e\x10\"J\x04\x08$\x10*J\x04\x08*\x10+J\x04\x08+\x10.\"w\n\x03Tag\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12U\n\rexternal_docs\x18\x03 \x01(\x0b\x32>.grpc.gateway.protoc_gen_swagger.options.ExternalDocumentationJ\x04\x08\x01\x10\x02\"\xdd\x01\n\x13SecurityDefinitions\x12\\\n\x08security\x18\x01 \x03(\x0b\x32J.grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions.SecurityEntry\x1ah\n\rSecurityEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x46\n\x05value\x18\x02 \x01(\x0b\x32\x37.grpc.gateway.protoc_gen_swagger.options.SecurityScheme:\x02\x38\x01\"\x96\x06\n\x0eSecurityScheme\x12J\n\x04type\x18\x01 \x01(\x0e\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityScheme.Type\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x46\n\x02in\x18\x04 \x01(\x0e\x32:.grpc.gateway.protoc_gen_swagger.options.SecurityScheme.In\x12J\n\x04\x66low\x18\x05 \x01(\x0e\x32<.grpc.gateway.protoc_gen_swagger.options.SecurityScheme.Flow\x12\x19\n\x11\x61uthorization_url\x18\x06 \x01(\t\x12\x11\n\ttoken_url\x18\x07 \x01(\t\x12?\n\x06scopes\x18\x08 \x01(\x0b\x32/.grpc.gateway.protoc_gen_swagger.options.Scopes\x12[\n\nextensions\x18\t \x03(\x0b\x32G.grpc.gateway.protoc_gen_swagger.options.SecurityScheme.ExtensionsEntry\x1aI\n\x0f\x45xtensionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"K\n\x04Type\x12\x10\n\x0cTYPE_INVALID\x10\x00\x12\x0e\n\nTYPE_BASIC\x10\x01\x12\x10\n\x0cTYPE_API_KEY\x10\x02\x12\x0f\n\x0bTYPE_OAUTH2\x10\x03\"1\n\x02In\x12\x0e\n\nIN_INVALID\x10\x00\x12\x0c\n\x08IN_QUERY\x10\x01\x12\r\n\tIN_HEADER\x10\x02\"j\n\x04\x46low\x12\x10\n\x0c\x46LOW_INVALID\x10\x00\x12\x11\n\rFLOW_IMPLICIT\x10\x01\x12\x11\n\rFLOW_PASSWORD\x10\x02\x12\x14\n\x10\x46LOW_APPLICATION\x10\x03\x12\x14\n\x10\x46LOW_ACCESS_CODE\x10\x04\"\xc9\x02\n\x13SecurityRequirement\x12s\n\x14security_requirement\x18\x01 \x03(\x0b\x32U.grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementEntry\x1a)\n\x18SecurityRequirementValue\x12\r\n\x05scope\x18\x01 \x03(\t\x1a\x91\x01\n\x18SecurityRequirementEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x64\n\x05value\x18\x02 \x01(\x0b\x32U.grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementValue:\x02\x38\x01\"\x81\x01\n\x06Scopes\x12I\n\x05scope\x18\x01 \x03(\x0b\x32:.grpc.gateway.protoc_gen_swagger.options.Scopes.ScopeEntry\x1a,\n\nScopeEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x43ZAgithub.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/optionsb\x06proto3') - , - dependencies=[google_dot_protobuf_dot_any__pb2.DESCRIPTOR,google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,]) - - - -_SWAGGER_SWAGGERSCHEME = _descriptor.EnumDescriptor( - name='SwaggerScheme', - full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.SwaggerScheme', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='UNKNOWN', index=0, number=0, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='HTTP', index=1, number=1, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='HTTPS', index=2, number=2, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='WS', index=3, number=3, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='WSS', index=4, number=4, - serialized_options=None, - type=None), - ], - containing_type=None, - serialized_options=None, - serialized_start=989, - serialized_end=1055, -) -_sym_db.RegisterEnumDescriptor(_SWAGGER_SWAGGERSCHEME) - -_JSONSCHEMA_JSONSCHEMASIMPLETYPES = _descriptor.EnumDescriptor( - name='JSONSchemaSimpleTypes', - full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.JSONSchemaSimpleTypes', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='UNKNOWN', index=0, number=0, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='ARRAY', index=1, number=1, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='BOOLEAN', index=2, number=2, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='INTEGER', index=3, number=3, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='NULL', index=4, number=4, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='NUMBER', index=5, number=5, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='OBJECT', index=6, number=6, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='STRING', index=7, number=7, - serialized_options=None, - type=None), - ], - containing_type=None, - serialized_options=None, - serialized_start=3317, - serialized_end=3436, -) -_sym_db.RegisterEnumDescriptor(_JSONSCHEMA_JSONSCHEMASIMPLETYPES) - -_SECURITYSCHEME_TYPE = _descriptor.EnumDescriptor( - name='Type', - full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.Type', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='TYPE_INVALID', index=0, number=0, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='TYPE_BASIC', index=1, number=1, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='TYPE_API_KEY', index=2, number=2, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='TYPE_OAUTH2', index=3, number=3, - serialized_options=None, - type=None), - ], - containing_type=None, - serialized_options=None, - serialized_start=4424, - serialized_end=4499, -) -_sym_db.RegisterEnumDescriptor(_SECURITYSCHEME_TYPE) - -_SECURITYSCHEME_IN = _descriptor.EnumDescriptor( - name='In', - full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.In', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='IN_INVALID', index=0, number=0, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='IN_QUERY', index=1, number=1, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='IN_HEADER', index=2, number=2, - serialized_options=None, - type=None), - ], - containing_type=None, - serialized_options=None, - serialized_start=4501, - serialized_end=4550, -) -_sym_db.RegisterEnumDescriptor(_SECURITYSCHEME_IN) - -_SECURITYSCHEME_FLOW = _descriptor.EnumDescriptor( - name='Flow', - full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.Flow', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='FLOW_INVALID', index=0, number=0, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='FLOW_IMPLICIT', index=1, number=1, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='FLOW_PASSWORD', index=2, number=2, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='FLOW_APPLICATION', index=3, number=3, - serialized_options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='FLOW_ACCESS_CODE', index=4, number=4, - serialized_options=None, - type=None), - ], - containing_type=None, - serialized_options=None, - serialized_start=4552, - serialized_end=4658, -) -_sym_db.RegisterEnumDescriptor(_SECURITYSCHEME_FLOW) - - -_SWAGGER_RESPONSESENTRY = _descriptor.Descriptor( - name='ResponsesEntry', - full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.ResponsesEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.ResponsesEntry.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.ResponsesEntry.value', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=_b('8\001'), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=813, - serialized_end=912, -) - -_SWAGGER_EXTENSIONSENTRY = _descriptor.Descriptor( - name='ExtensionsEntry', - full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.ExtensionsEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.ExtensionsEntry.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.ExtensionsEntry.value', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=_b('8\001'), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=914, - serialized_end=987, -) - -_SWAGGER = _descriptor.Descriptor( - name='Swagger', - full_name='grpc.gateway.protoc_gen_swagger.options.Swagger', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='swagger', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.swagger', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='info', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.info', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='host', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.host', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='base_path', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.base_path', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='schemes', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.schemes', index=4, - number=5, type=14, cpp_type=8, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='consumes', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.consumes', index=5, - number=6, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='produces', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.produces', index=6, - number=7, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='responses', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.responses', index=7, - number=10, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='security_definitions', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.security_definitions', index=8, - number=11, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='security', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.security', index=9, - number=12, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='external_docs', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.external_docs', index=10, - number=14, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='extensions', full_name='grpc.gateway.protoc_gen_swagger.options.Swagger.extensions', index=11, - number=15, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_SWAGGER_RESPONSESENTRY, _SWAGGER_EXTENSIONSENTRY, ], - enum_types=[ - _SWAGGER_SWAGGERSCHEME, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=145, - serialized_end=1073, -) - - -_OPERATION_RESPONSESENTRY = _descriptor.Descriptor( - name='ResponsesEntry', - full_name='grpc.gateway.protoc_gen_swagger.options.Operation.ResponsesEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.ResponsesEntry.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.ResponsesEntry.value', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=_b('8\001'), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=813, - serialized_end=912, -) - -_OPERATION_EXTENSIONSENTRY = _descriptor.Descriptor( - name='ExtensionsEntry', - full_name='grpc.gateway.protoc_gen_swagger.options.Operation.ExtensionsEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.ExtensionsEntry.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.ExtensionsEntry.value', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=_b('8\001'), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=914, - serialized_end=987, -) - -_OPERATION = _descriptor.Descriptor( - name='Operation', - full_name='grpc.gateway.protoc_gen_swagger.options.Operation', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='tags', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.tags', index=0, - number=1, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='summary', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.summary', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='description', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.description', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='external_docs', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.external_docs', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='operation_id', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.operation_id', index=4, - number=5, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='consumes', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.consumes', index=5, - number=6, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='produces', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.produces', index=6, - number=7, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='responses', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.responses', index=7, - number=9, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='schemes', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.schemes', index=8, - number=10, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='deprecated', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.deprecated', index=9, - number=11, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='security', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.security', index=10, - number=12, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='extensions', full_name='grpc.gateway.protoc_gen_swagger.options.Operation.extensions', index=11, - number=13, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_OPERATION_RESPONSESENTRY, _OPERATION_EXTENSIONSENTRY, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1076, - serialized_end=1757, -) - - -_RESPONSE_EXTENSIONSENTRY = _descriptor.Descriptor( - name='ExtensionsEntry', - full_name='grpc.gateway.protoc_gen_swagger.options.Response.ExtensionsEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Response.ExtensionsEntry.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Response.ExtensionsEntry.value', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=_b('8\001'), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=914, - serialized_end=987, -) - -_RESPONSE = _descriptor.Descriptor( - name='Response', - full_name='grpc.gateway.protoc_gen_swagger.options.Response', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='description', full_name='grpc.gateway.protoc_gen_swagger.options.Response.description', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='schema', full_name='grpc.gateway.protoc_gen_swagger.options.Response.schema', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='extensions', full_name='grpc.gateway.protoc_gen_swagger.options.Response.extensions', index=2, - number=5, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_RESPONSE_EXTENSIONSENTRY, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1760, - serialized_end=2030, -) - - -_INFO_EXTENSIONSENTRY = _descriptor.Descriptor( - name='ExtensionsEntry', - full_name='grpc.gateway.protoc_gen_swagger.options.Info.ExtensionsEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Info.ExtensionsEntry.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Info.ExtensionsEntry.value', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=_b('8\001'), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=914, - serialized_end=987, -) - -_INFO = _descriptor.Descriptor( - name='Info', - full_name='grpc.gateway.protoc_gen_swagger.options.Info', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='title', full_name='grpc.gateway.protoc_gen_swagger.options.Info.title', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='description', full_name='grpc.gateway.protoc_gen_swagger.options.Info.description', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='terms_of_service', full_name='grpc.gateway.protoc_gen_swagger.options.Info.terms_of_service', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='contact', full_name='grpc.gateway.protoc_gen_swagger.options.Info.contact', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='license', full_name='grpc.gateway.protoc_gen_swagger.options.Info.license', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='version', full_name='grpc.gateway.protoc_gen_swagger.options.Info.version', index=5, - number=6, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='extensions', full_name='grpc.gateway.protoc_gen_swagger.options.Info.extensions', index=6, - number=7, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_INFO_EXTENSIONSENTRY, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2033, - serialized_end=2410, -) - - -_CONTACT = _descriptor.Descriptor( - name='Contact', - full_name='grpc.gateway.protoc_gen_swagger.options.Contact', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='grpc.gateway.protoc_gen_swagger.options.Contact.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='url', full_name='grpc.gateway.protoc_gen_swagger.options.Contact.url', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='email', full_name='grpc.gateway.protoc_gen_swagger.options.Contact.email', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2412, - serialized_end=2463, -) - - -_LICENSE = _descriptor.Descriptor( - name='License', - full_name='grpc.gateway.protoc_gen_swagger.options.License', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='grpc.gateway.protoc_gen_swagger.options.License.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='url', full_name='grpc.gateway.protoc_gen_swagger.options.License.url', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2465, - serialized_end=2501, -) - - -_EXTERNALDOCUMENTATION = _descriptor.Descriptor( - name='ExternalDocumentation', - full_name='grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='description', full_name='grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation.description', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='url', full_name='grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation.url', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2503, - serialized_end=2560, -) - - -_SCHEMA = _descriptor.Descriptor( - name='Schema', - full_name='grpc.gateway.protoc_gen_swagger.options.Schema', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='json_schema', full_name='grpc.gateway.protoc_gen_swagger.options.Schema.json_schema', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='discriminator', full_name='grpc.gateway.protoc_gen_swagger.options.Schema.discriminator', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='read_only', full_name='grpc.gateway.protoc_gen_swagger.options.Schema.read_only', index=2, - number=3, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='external_docs', full_name='grpc.gateway.protoc_gen_swagger.options.Schema.external_docs', index=3, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='example', full_name='grpc.gateway.protoc_gen_swagger.options.Schema.example', index=4, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2563, - serialized_end=2819, -) - - -_JSONSCHEMA = _descriptor.Descriptor( - name='JSONSchema', - full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='ref', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.ref', index=0, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='title', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.title', index=1, - number=5, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='description', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.description', index=2, - number=6, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='default', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.default', index=3, - number=7, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='read_only', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.read_only', index=4, - number=8, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='multiple_of', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.multiple_of', index=5, - number=10, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='maximum', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.maximum', index=6, - number=11, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='exclusive_maximum', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.exclusive_maximum', index=7, - number=12, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='minimum', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.minimum', index=8, - number=13, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='exclusive_minimum', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.exclusive_minimum', index=9, - number=14, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='max_length', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.max_length', index=10, - number=15, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='min_length', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.min_length', index=11, - number=16, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='pattern', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.pattern', index=12, - number=17, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='max_items', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.max_items', index=13, - number=20, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='min_items', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.min_items', index=14, - number=21, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='unique_items', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.unique_items', index=15, - number=22, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='max_properties', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.max_properties', index=16, - number=24, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='min_properties', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.min_properties', index=17, - number=25, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='required', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.required', index=18, - number=26, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='array', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.array', index=19, - number=34, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='type', full_name='grpc.gateway.protoc_gen_swagger.options.JSONSchema.type', index=20, - number=35, type=14, cpp_type=8, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _JSONSCHEMA_JSONSCHEMASIMPLETYPES, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2822, - serialized_end=3520, -) - - -_TAG = _descriptor.Descriptor( - name='Tag', - full_name='grpc.gateway.protoc_gen_swagger.options.Tag', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='description', full_name='grpc.gateway.protoc_gen_swagger.options.Tag.description', index=0, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='external_docs', full_name='grpc.gateway.protoc_gen_swagger.options.Tag.external_docs', index=1, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3522, - serialized_end=3641, -) - - -_SECURITYDEFINITIONS_SECURITYENTRY = _descriptor.Descriptor( - name='SecurityEntry', - full_name='grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions.SecurityEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions.SecurityEntry.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='value', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions.SecurityEntry.value', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=_b('8\001'), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3761, - serialized_end=3865, -) - -_SECURITYDEFINITIONS = _descriptor.Descriptor( - name='SecurityDefinitions', - full_name='grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='security', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions.security', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_SECURITYDEFINITIONS_SECURITYENTRY, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3644, - serialized_end=3865, -) - - -_SECURITYSCHEME_EXTENSIONSENTRY = _descriptor.Descriptor( - name='ExtensionsEntry', - full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.ExtensionsEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.ExtensionsEntry.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='value', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.ExtensionsEntry.value', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=_b('8\001'), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=914, - serialized_end=987, -) - -_SECURITYSCHEME = _descriptor.Descriptor( - name='SecurityScheme', - full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='type', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.type', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='description', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.description', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='name', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.name', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='in', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.in', index=3, - number=4, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='flow', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.flow', index=4, - number=5, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='authorization_url', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.authorization_url', index=5, - number=6, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='token_url', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.token_url', index=6, - number=7, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='scopes', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.scopes', index=7, - number=8, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='extensions', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityScheme.extensions', index=8, - number=9, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_SECURITYSCHEME_EXTENSIONSENTRY, ], - enum_types=[ - _SECURITYSCHEME_TYPE, - _SECURITYSCHEME_IN, - _SECURITYSCHEME_FLOW, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3868, - serialized_end=4658, -) - - -_SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE = _descriptor.Descriptor( - name='SecurityRequirementValue', - full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementValue', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='scope', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementValue.scope', index=0, - number=1, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=4801, - serialized_end=4842, -) - -_SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY = _descriptor.Descriptor( - name='SecurityRequirementEntry', - full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementEntry.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='value', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementEntry.value', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=_b('8\001'), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=4845, - serialized_end=4990, -) - -_SECURITYREQUIREMENT = _descriptor.Descriptor( - name='SecurityRequirement', - full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='security_requirement', full_name='grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.security_requirement', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE, _SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=4661, - serialized_end=4990, -) - - -_SCOPES_SCOPEENTRY = _descriptor.Descriptor( - name='ScopeEntry', - full_name='grpc.gateway.protoc_gen_swagger.options.Scopes.ScopeEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='grpc.gateway.protoc_gen_swagger.options.Scopes.ScopeEntry.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='value', full_name='grpc.gateway.protoc_gen_swagger.options.Scopes.ScopeEntry.value', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=_b('8\001'), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=5078, - serialized_end=5122, -) - -_SCOPES = _descriptor.Descriptor( - name='Scopes', - full_name='grpc.gateway.protoc_gen_swagger.options.Scopes', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='scope', full_name='grpc.gateway.protoc_gen_swagger.options.Scopes.scope', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_SCOPES_SCOPEENTRY, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=4993, - serialized_end=5122, -) - -_SWAGGER_RESPONSESENTRY.fields_by_name['value'].message_type = _RESPONSE -_SWAGGER_RESPONSESENTRY.containing_type = _SWAGGER -_SWAGGER_EXTENSIONSENTRY.fields_by_name['value'].message_type = google_dot_protobuf_dot_struct__pb2._VALUE -_SWAGGER_EXTENSIONSENTRY.containing_type = _SWAGGER -_SWAGGER.fields_by_name['info'].message_type = _INFO -_SWAGGER.fields_by_name['schemes'].enum_type = _SWAGGER_SWAGGERSCHEME -_SWAGGER.fields_by_name['responses'].message_type = _SWAGGER_RESPONSESENTRY -_SWAGGER.fields_by_name['security_definitions'].message_type = _SECURITYDEFINITIONS -_SWAGGER.fields_by_name['security'].message_type = _SECURITYREQUIREMENT -_SWAGGER.fields_by_name['external_docs'].message_type = _EXTERNALDOCUMENTATION -_SWAGGER.fields_by_name['extensions'].message_type = _SWAGGER_EXTENSIONSENTRY -_SWAGGER_SWAGGERSCHEME.containing_type = _SWAGGER -_OPERATION_RESPONSESENTRY.fields_by_name['value'].message_type = _RESPONSE -_OPERATION_RESPONSESENTRY.containing_type = _OPERATION -_OPERATION_EXTENSIONSENTRY.fields_by_name['value'].message_type = google_dot_protobuf_dot_struct__pb2._VALUE -_OPERATION_EXTENSIONSENTRY.containing_type = _OPERATION -_OPERATION.fields_by_name['external_docs'].message_type = _EXTERNALDOCUMENTATION -_OPERATION.fields_by_name['responses'].message_type = _OPERATION_RESPONSESENTRY -_OPERATION.fields_by_name['security'].message_type = _SECURITYREQUIREMENT -_OPERATION.fields_by_name['extensions'].message_type = _OPERATION_EXTENSIONSENTRY -_RESPONSE_EXTENSIONSENTRY.fields_by_name['value'].message_type = google_dot_protobuf_dot_struct__pb2._VALUE -_RESPONSE_EXTENSIONSENTRY.containing_type = _RESPONSE -_RESPONSE.fields_by_name['schema'].message_type = _SCHEMA -_RESPONSE.fields_by_name['extensions'].message_type = _RESPONSE_EXTENSIONSENTRY -_INFO_EXTENSIONSENTRY.fields_by_name['value'].message_type = google_dot_protobuf_dot_struct__pb2._VALUE -_INFO_EXTENSIONSENTRY.containing_type = _INFO -_INFO.fields_by_name['contact'].message_type = _CONTACT -_INFO.fields_by_name['license'].message_type = _LICENSE -_INFO.fields_by_name['extensions'].message_type = _INFO_EXTENSIONSENTRY -_SCHEMA.fields_by_name['json_schema'].message_type = _JSONSCHEMA -_SCHEMA.fields_by_name['external_docs'].message_type = _EXTERNALDOCUMENTATION -_SCHEMA.fields_by_name['example'].message_type = google_dot_protobuf_dot_any__pb2._ANY -_JSONSCHEMA.fields_by_name['type'].enum_type = _JSONSCHEMA_JSONSCHEMASIMPLETYPES -_JSONSCHEMA_JSONSCHEMASIMPLETYPES.containing_type = _JSONSCHEMA -_TAG.fields_by_name['external_docs'].message_type = _EXTERNALDOCUMENTATION -_SECURITYDEFINITIONS_SECURITYENTRY.fields_by_name['value'].message_type = _SECURITYSCHEME -_SECURITYDEFINITIONS_SECURITYENTRY.containing_type = _SECURITYDEFINITIONS -_SECURITYDEFINITIONS.fields_by_name['security'].message_type = _SECURITYDEFINITIONS_SECURITYENTRY -_SECURITYSCHEME_EXTENSIONSENTRY.fields_by_name['value'].message_type = google_dot_protobuf_dot_struct__pb2._VALUE -_SECURITYSCHEME_EXTENSIONSENTRY.containing_type = _SECURITYSCHEME -_SECURITYSCHEME.fields_by_name['type'].enum_type = _SECURITYSCHEME_TYPE -_SECURITYSCHEME.fields_by_name['in'].enum_type = _SECURITYSCHEME_IN -_SECURITYSCHEME.fields_by_name['flow'].enum_type = _SECURITYSCHEME_FLOW -_SECURITYSCHEME.fields_by_name['scopes'].message_type = _SCOPES -_SECURITYSCHEME.fields_by_name['extensions'].message_type = _SECURITYSCHEME_EXTENSIONSENTRY -_SECURITYSCHEME_TYPE.containing_type = _SECURITYSCHEME -_SECURITYSCHEME_IN.containing_type = _SECURITYSCHEME -_SECURITYSCHEME_FLOW.containing_type = _SECURITYSCHEME -_SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE.containing_type = _SECURITYREQUIREMENT -_SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY.fields_by_name['value'].message_type = _SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE -_SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY.containing_type = _SECURITYREQUIREMENT -_SECURITYREQUIREMENT.fields_by_name['security_requirement'].message_type = _SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY -_SCOPES_SCOPEENTRY.containing_type = _SCOPES -_SCOPES.fields_by_name['scope'].message_type = _SCOPES_SCOPEENTRY -DESCRIPTOR.message_types_by_name['Swagger'] = _SWAGGER -DESCRIPTOR.message_types_by_name['Operation'] = _OPERATION -DESCRIPTOR.message_types_by_name['Response'] = _RESPONSE -DESCRIPTOR.message_types_by_name['Info'] = _INFO -DESCRIPTOR.message_types_by_name['Contact'] = _CONTACT -DESCRIPTOR.message_types_by_name['License'] = _LICENSE -DESCRIPTOR.message_types_by_name['ExternalDocumentation'] = _EXTERNALDOCUMENTATION -DESCRIPTOR.message_types_by_name['Schema'] = _SCHEMA -DESCRIPTOR.message_types_by_name['JSONSchema'] = _JSONSCHEMA -DESCRIPTOR.message_types_by_name['Tag'] = _TAG -DESCRIPTOR.message_types_by_name['SecurityDefinitions'] = _SECURITYDEFINITIONS -DESCRIPTOR.message_types_by_name['SecurityScheme'] = _SECURITYSCHEME -DESCRIPTOR.message_types_by_name['SecurityRequirement'] = _SECURITYREQUIREMENT -DESCRIPTOR.message_types_by_name['Scopes'] = _SCOPES -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -Swagger = _reflection.GeneratedProtocolMessageType('Swagger', (_message.Message,), { - - 'ResponsesEntry' : _reflection.GeneratedProtocolMessageType('ResponsesEntry', (_message.Message,), { - 'DESCRIPTOR' : _SWAGGER_RESPONSESENTRY, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Swagger.ResponsesEntry) - }) - , - - 'ExtensionsEntry' : _reflection.GeneratedProtocolMessageType('ExtensionsEntry', (_message.Message,), { - 'DESCRIPTOR' : _SWAGGER_EXTENSIONSENTRY, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Swagger.ExtensionsEntry) - }) - , - 'DESCRIPTOR' : _SWAGGER, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Swagger) - }) -_sym_db.RegisterMessage(Swagger) -_sym_db.RegisterMessage(Swagger.ResponsesEntry) -_sym_db.RegisterMessage(Swagger.ExtensionsEntry) - -Operation = _reflection.GeneratedProtocolMessageType('Operation', (_message.Message,), { - - 'ResponsesEntry' : _reflection.GeneratedProtocolMessageType('ResponsesEntry', (_message.Message,), { - 'DESCRIPTOR' : _OPERATION_RESPONSESENTRY, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Operation.ResponsesEntry) - }) - , - - 'ExtensionsEntry' : _reflection.GeneratedProtocolMessageType('ExtensionsEntry', (_message.Message,), { - 'DESCRIPTOR' : _OPERATION_EXTENSIONSENTRY, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Operation.ExtensionsEntry) - }) - , - 'DESCRIPTOR' : _OPERATION, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Operation) - }) -_sym_db.RegisterMessage(Operation) -_sym_db.RegisterMessage(Operation.ResponsesEntry) -_sym_db.RegisterMessage(Operation.ExtensionsEntry) - -Response = _reflection.GeneratedProtocolMessageType('Response', (_message.Message,), { - - 'ExtensionsEntry' : _reflection.GeneratedProtocolMessageType('ExtensionsEntry', (_message.Message,), { - 'DESCRIPTOR' : _RESPONSE_EXTENSIONSENTRY, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Response.ExtensionsEntry) - }) - , - 'DESCRIPTOR' : _RESPONSE, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Response) - }) -_sym_db.RegisterMessage(Response) -_sym_db.RegisterMessage(Response.ExtensionsEntry) - -Info = _reflection.GeneratedProtocolMessageType('Info', (_message.Message,), { - - 'ExtensionsEntry' : _reflection.GeneratedProtocolMessageType('ExtensionsEntry', (_message.Message,), { - 'DESCRIPTOR' : _INFO_EXTENSIONSENTRY, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Info.ExtensionsEntry) - }) - , - 'DESCRIPTOR' : _INFO, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Info) - }) -_sym_db.RegisterMessage(Info) -_sym_db.RegisterMessage(Info.ExtensionsEntry) - -Contact = _reflection.GeneratedProtocolMessageType('Contact', (_message.Message,), { - 'DESCRIPTOR' : _CONTACT, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Contact) - }) -_sym_db.RegisterMessage(Contact) - -License = _reflection.GeneratedProtocolMessageType('License', (_message.Message,), { - 'DESCRIPTOR' : _LICENSE, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.License) - }) -_sym_db.RegisterMessage(License) - -ExternalDocumentation = _reflection.GeneratedProtocolMessageType('ExternalDocumentation', (_message.Message,), { - 'DESCRIPTOR' : _EXTERNALDOCUMENTATION, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.ExternalDocumentation) - }) -_sym_db.RegisterMessage(ExternalDocumentation) - -Schema = _reflection.GeneratedProtocolMessageType('Schema', (_message.Message,), { - 'DESCRIPTOR' : _SCHEMA, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Schema) - }) -_sym_db.RegisterMessage(Schema) - -JSONSchema = _reflection.GeneratedProtocolMessageType('JSONSchema', (_message.Message,), { - 'DESCRIPTOR' : _JSONSCHEMA, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.JSONSchema) - }) -_sym_db.RegisterMessage(JSONSchema) - -Tag = _reflection.GeneratedProtocolMessageType('Tag', (_message.Message,), { - 'DESCRIPTOR' : _TAG, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Tag) - }) -_sym_db.RegisterMessage(Tag) - -SecurityDefinitions = _reflection.GeneratedProtocolMessageType('SecurityDefinitions', (_message.Message,), { - - 'SecurityEntry' : _reflection.GeneratedProtocolMessageType('SecurityEntry', (_message.Message,), { - 'DESCRIPTOR' : _SECURITYDEFINITIONS_SECURITYENTRY, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions.SecurityEntry) - }) - , - 'DESCRIPTOR' : _SECURITYDEFINITIONS, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityDefinitions) - }) -_sym_db.RegisterMessage(SecurityDefinitions) -_sym_db.RegisterMessage(SecurityDefinitions.SecurityEntry) - -SecurityScheme = _reflection.GeneratedProtocolMessageType('SecurityScheme', (_message.Message,), { - - 'ExtensionsEntry' : _reflection.GeneratedProtocolMessageType('ExtensionsEntry', (_message.Message,), { - 'DESCRIPTOR' : _SECURITYSCHEME_EXTENSIONSENTRY, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityScheme.ExtensionsEntry) - }) - , - 'DESCRIPTOR' : _SECURITYSCHEME, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityScheme) - }) -_sym_db.RegisterMessage(SecurityScheme) -_sym_db.RegisterMessage(SecurityScheme.ExtensionsEntry) - -SecurityRequirement = _reflection.GeneratedProtocolMessageType('SecurityRequirement', (_message.Message,), { - - 'SecurityRequirementValue' : _reflection.GeneratedProtocolMessageType('SecurityRequirementValue', (_message.Message,), { - 'DESCRIPTOR' : _SECURITYREQUIREMENT_SECURITYREQUIREMENTVALUE, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementValue) - }) - , - - 'SecurityRequirementEntry' : _reflection.GeneratedProtocolMessageType('SecurityRequirementEntry', (_message.Message,), { - 'DESCRIPTOR' : _SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityRequirement.SecurityRequirementEntry) - }) - , - 'DESCRIPTOR' : _SECURITYREQUIREMENT, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.SecurityRequirement) - }) -_sym_db.RegisterMessage(SecurityRequirement) -_sym_db.RegisterMessage(SecurityRequirement.SecurityRequirementValue) -_sym_db.RegisterMessage(SecurityRequirement.SecurityRequirementEntry) - -Scopes = _reflection.GeneratedProtocolMessageType('Scopes', (_message.Message,), { - - 'ScopeEntry' : _reflection.GeneratedProtocolMessageType('ScopeEntry', (_message.Message,), { - 'DESCRIPTOR' : _SCOPES_SCOPEENTRY, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Scopes.ScopeEntry) - }) - , - 'DESCRIPTOR' : _SCOPES, - '__module__' : 'protoc_gen_swagger.options.openapiv2_pb2' - # @@protoc_insertion_point(class_scope:grpc.gateway.protoc_gen_swagger.options.Scopes) - }) -_sym_db.RegisterMessage(Scopes) -_sym_db.RegisterMessage(Scopes.ScopeEntry) - - -DESCRIPTOR._options = None -_SWAGGER_RESPONSESENTRY._options = None -_SWAGGER_EXTENSIONSENTRY._options = None -_OPERATION_RESPONSESENTRY._options = None -_OPERATION_EXTENSIONSENTRY._options = None -_RESPONSE_EXTENSIONSENTRY._options = None -_INFO_EXTENSIONSENTRY._options = None -_SECURITYDEFINITIONS_SECURITYENTRY._options = None -_SECURITYSCHEME_EXTENSIONSENTRY._options = None -_SECURITYREQUIREMENT_SECURITYREQUIREMENTENTRY._options = None -_SCOPES_SCOPEENTRY._options = None -# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/py.typed b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/send/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/send/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py deleted file mode 100644 index ea1f17c00f..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/translate/__init__.py +++ /dev/null @@ -1,406 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import abc -from typing import Optional, Sequence - -from google.protobuf.duration_pb2 import Duration -from google.protobuf.timestamp_pb2 import Timestamp - -from opentelemetry.exporter.jaeger.proto.grpc.gen import model_pb2 - -from opentelemetry.sdk.trace import ReadableSpan, StatusCode -from opentelemetry.trace import SpanKind -from opentelemetry.util import types - - -OTLP_JAEGER_SPAN_KIND = { - SpanKind.CLIENT: "client", - SpanKind.SERVER: "server", - SpanKind.CONSUMER: "consumer", - SpanKind.PRODUCER: "producer", - SpanKind.INTERNAL: "internal", -} - -NAME_KEY = "otel.library.name" -VERSION_KEY = "otel.library.version" -_SCOPE_NAME_KEY = "otel.scope.name" -_SCOPE_VERSION_KEY = "otel.scope.version" - - -def _nsec_to_usec_round(nsec: int) -> int: - """Round nanoseconds to microseconds""" - return (nsec + 500) // 10**3 - - -def _convert_int_to_i64(val): - """Convert integer to signed int64 (i64)""" - if val > 0x7FFFFFFFFFFFFFFF: - val -= 0x10000000000000000 - return val - - -class Translator(abc.ABC): - def __init__(self, max_tag_value_length: Optional[int] = None): - self._max_tag_value_length = max_tag_value_length - - @abc.abstractmethod - def _translate_span(self, span): - """Translates span to jaeger format. - - Args: - span: span to translate - """ - - @abc.abstractmethod - def _extract_tags(self, span): - """Extracts tags from span and returns list of jaeger Tags. - - Args: - span: span to extract tags - """ - - @abc.abstractmethod - def _extract_refs(self, span): - """Extracts references from span and returns list of jaeger SpanRefs. - - Args: - span: span to extract references - """ - - @abc.abstractmethod - def _extract_logs(self, span): - """Extracts logs from span and returns list of jaeger Logs. - - Args: - span: span to extract logs - """ - - -class Translate: - def __init__(self, spans): - self.spans = spans - - def _translate(self, translator: Translator): - translated_spans = [] - for span in self.spans: - # pylint: disable=protected-access - translated_span = translator._translate_span(span) - translated_spans.append(translated_span) - return translated_spans - - -# pylint: disable=no-member,too-many-locals,no-self-use - - -def _trace_id_to_bytes(trace_id: int) -> bytes: - """Returns bytes representation of trace id.""" - return trace_id.to_bytes(16, "big") - - -def _span_id_to_bytes(span_id: int) -> bytes: - """Returns bytes representation of span id""" - return span_id.to_bytes(8, "big") - - -def _get_string_key_value(key, value: str) -> model_pb2.KeyValue: - """Returns jaeger string KeyValue.""" - return model_pb2.KeyValue( - key=key, v_str=value, v_type=model_pb2.ValueType.STRING - ) - - -def _get_bool_key_value(key: str, value: bool) -> model_pb2.KeyValue: - """Returns jaeger boolean KeyValue.""" - return model_pb2.KeyValue( - key=key, v_bool=value, v_type=model_pb2.ValueType.BOOL - ) - - -def _get_long_key_value(key: str, value: int) -> model_pb2.KeyValue: - """Returns jaeger long KeyValue.""" - return model_pb2.KeyValue( - key=key, v_int64=value, v_type=model_pb2.ValueType.INT64 - ) - - -def _get_double_key_value(key: str, value: float) -> model_pb2.KeyValue: - """Returns jaeger double KeyValue.""" - return model_pb2.KeyValue( - key=key, v_float64=value, v_type=model_pb2.ValueType.FLOAT64 - ) - - -def _get_binary_key_value(key: str, value: bytes) -> model_pb2.KeyValue: - """Returns jaeger double KeyValue.""" - return model_pb2.KeyValue( - key=key, v_binary=value, v_type=model_pb2.ValueType.BINARY - ) - - -def _translate_attribute( - key: str, value: types.AttributeValue, max_length: Optional[int] -) -> Optional[model_pb2.KeyValue]: - """Convert the attributes to jaeger keyvalues.""" - translated = None - if isinstance(value, bool): - translated = _get_bool_key_value(key, value) - elif isinstance(value, str): - if max_length is not None: - value = value[:max_length] - translated = _get_string_key_value(key, value) - elif isinstance(value, int): - translated = _get_long_key_value(key, value) - elif isinstance(value, float): - translated = _get_double_key_value(key, value) - elif isinstance(value, tuple): - value = str(value) - if max_length is not None: - value = value[:max_length] - translated = _get_string_key_value(key, value) - return translated - - -def _extract_resource_tags( - span: ReadableSpan, max_tag_value_length: Optional[int] -) -> Sequence[model_pb2.KeyValue]: - """Extracts resource attributes from span and returns - list of jaeger keyvalues. - - Args: - span: span to extract keyvalues - """ - tags = [] - for key, value in span.resource.attributes.items(): - tag = _translate_attribute(key, value, max_tag_value_length) - if tag: - tags.append(tag) - return tags - - -def _duration_from_two_time_stamps( - start: Timestamp, end: Timestamp -) -> Duration: - """Compute Duration from two Timestamps. - - See https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#duration - """ - duration = Duration( - seconds=end.seconds - start.seconds, - nanos=end.nanos - start.nanos, - ) - # pylint: disable=chained-comparison - if duration.seconds < 0 and duration.nanos > 0: - duration.seconds += 1 - duration.nanos -= 1000000000 - elif duration.seconds > 0 and duration.nanos < 0: - duration.seconds -= 1 - duration.nanos += 1000000000 - return duration - - -def _proto_timestamp_from_epoch_nanos(nsec: int) -> Timestamp: - """Create a Timestamp from the number of nanoseconds elapsed from the epoch. - - See https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#timestamp - """ - nsec_time = nsec / 1e9 - seconds = int(nsec_time) - nanos = int((nsec_time - seconds) * 1e9) - return Timestamp(seconds=seconds, nanos=nanos) - - -class ProtobufTranslator(Translator): - def __init__( - self, svc_name: str, max_tag_value_length: Optional[int] = None - ): - super().__init__(max_tag_value_length) - self.svc_name = svc_name - - def _translate_span(self, span: ReadableSpan) -> model_pb2.Span: - - ctx = span.get_span_context() - # pb2 span expects in byte format - trace_id = _trace_id_to_bytes(ctx.trace_id) - span_id = _span_id_to_bytes(ctx.span_id) - - start_time = _proto_timestamp_from_epoch_nanos(span.start_time) - end_time = _proto_timestamp_from_epoch_nanos(span.end_time) - duration = _duration_from_two_time_stamps(start_time, end_time) - - tags = self._extract_tags(span) - refs = self._extract_refs(span) - logs = self._extract_logs(span) - - flags = int(ctx.trace_flags) - - process = model_pb2.Process( - service_name=self.svc_name, - tags=_extract_resource_tags(span, self._max_tag_value_length), - ) - jaeger_span = model_pb2.Span( - trace_id=trace_id, - span_id=span_id, - operation_name=span.name, - references=refs, - flags=flags, - start_time=start_time, - duration=duration, - tags=tags, - logs=logs, - process=process, - ) - return jaeger_span - - def _extract_tags( - self, span: ReadableSpan - ) -> Sequence[model_pb2.KeyValue]: - translated = [] - if span.attributes: - for key, value in span.attributes.items(): - key_value = _translate_attribute( - key, value, self._max_tag_value_length - ) - if key_value is not None: - translated.append(key_value) - if span.resource.attributes: - for key, value in span.resource.attributes.items(): - key_value = _translate_attribute( - key, value, self._max_tag_value_length - ) - if key_value: - translated.append(key_value) - - status = span.status - if status.status_code is not StatusCode.UNSET: - translated.append( - _get_string_key_value( - "otel.status_code", status.status_code.name - ) - ) - if status.description is not None: - translated.append( - _get_string_key_value( - "otel.status_description", status.description - ) - ) - translated.append( - _get_string_key_value( - "span.kind", OTLP_JAEGER_SPAN_KIND[span.kind] - ) - ) - - # Instrumentation scope KeyValues - if span.instrumentation_scope: - name = _get_string_key_value( - NAME_KEY, span.instrumentation_scope.name - ) - version = _get_string_key_value( - VERSION_KEY, span.instrumentation_scope.version - ) - scope_name = _get_string_key_value( - _SCOPE_NAME_KEY, span.instrumentation_scope.name - ) - scope_version = _get_string_key_value( - _SCOPE_VERSION_KEY, span.instrumentation_scope.version - ) - translated.extend([name, version]) - translated.extend([scope_name, scope_version]) - - # Make sure to add "error" tag if span status is not OK - if not span.status.is_ok: - translated.append(_get_bool_key_value("error", True)) - - if span.dropped_attributes: - translated.append( - _get_long_key_value( - "otel.dropped_attributes_count", span.dropped_attributes - ) - ) - if span.dropped_events: - translated.append( - _get_long_key_value( - "otel.dropped_events_count", span.dropped_events - ) - ) - if span.dropped_links: - translated.append( - _get_long_key_value( - "otel.dropped_links_count", span.dropped_links - ) - ) - return translated - - def _extract_refs( - self, span: ReadableSpan - ) -> Optional[Sequence[model_pb2.SpanRef]]: - - refs = [] - if span.parent: - ctx = span.get_span_context() - parent_id = span.parent.span_id - parent_ref = model_pb2.SpanRef( - ref_type=model_pb2.SpanRefType.CHILD_OF, - trace_id=_trace_id_to_bytes(ctx.trace_id), - span_id=_span_id_to_bytes(parent_id), - ) - refs.append(parent_ref) - - for link in span.links: - trace_id = link.context.trace_id - span_id = link.context.span_id - refs.append( - model_pb2.SpanRef( - ref_type=model_pb2.SpanRefType.FOLLOWS_FROM, - trace_id=_trace_id_to_bytes(trace_id), - span_id=_span_id_to_bytes(span_id), - ) - ) - return refs - - def _extract_logs( - self, span: ReadableSpan - ) -> Optional[Sequence[model_pb2.Log]]: - if not span.events: - return None - - logs = [] - for event in span.events: - fields = [] - for key, value in event.attributes.items(): - tag = _translate_attribute( - key, value, self._max_tag_value_length - ) - if tag: - fields.append(tag) - - if event.attributes.dropped: - fields.append( - _translate_attribute( - "otel.dropped_attributes_count", - event.attributes.dropped, - self._max_tag_value_length, - ) - ) - - fields.append( - _get_string_key_value( - key="message", - value=event.name, - ) - ) - event_ts = _proto_timestamp_from_epoch_nanos(event.timestamp) - logs.append(model_pb2.Log(timestamp=event_ts, fields=fields)) - - return logs diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/util.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/util.py deleted file mode 100644 index 4f943a0e4c..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/util.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -from os import environ - -from grpc import ChannelCredentials, ssl_channel_credentials - -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_JAEGER_CERTIFICATE, -) - -logger = logging.getLogger(__name__) - - -def _load_credential_from_file(path) -> ChannelCredentials: - try: - with open(path, "rb") as creds_file: - credential = creds_file.read() - return ssl_channel_credentials(credential) - except FileNotFoundError: - logger.exception("Failed to read credential file") - return None - - -def _get_credentials(param): - if param is not None: - return param - creds_env = environ.get(OTEL_EXPORTER_JAEGER_CERTIFICATE) - if creds_env: - return _load_credential_from_file(creds_env) - return ssl_channel_credentials() diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py deleted file mode 100644 index f2d56fac64..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2019, OpenCensus Authors -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "1.22.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/__init__.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/certs/cred.cert b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/certs/cred.cert deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py deleted file mode 100644 index a886a43c52..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/tests/test_jaeger_exporter_protobuf.py +++ /dev/null @@ -1,524 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import unittest -from collections import OrderedDict -from unittest import mock - -import opentelemetry.exporter.jaeger.proto.grpc.translate as pb_translator -from opentelemetry import trace as trace_api -from opentelemetry.exporter.jaeger.proto.grpc import JaegerExporter - -# pylint:disable=no-name-in-module -# pylint:disable=import-error -from opentelemetry.exporter.jaeger.proto.grpc.gen import model_pb2 -from opentelemetry.exporter.jaeger.proto.grpc.translate import ( - _SCOPE_NAME_KEY, - _SCOPE_VERSION_KEY, - NAME_KEY, - VERSION_KEY, - Translate, -) -from opentelemetry.sdk import trace -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_JAEGER_CERTIFICATE, - OTEL_EXPORTER_JAEGER_ENDPOINT, - OTEL_EXPORTER_JAEGER_GRPC_INSECURE, - OTEL_EXPORTER_JAEGER_TIMEOUT, - OTEL_RESOURCE_ATTRIBUTES, -) -from opentelemetry.sdk.resources import SERVICE_NAME, Resource -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SpanExportResult -from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.test.spantestutil import ( - get_span_with_dropped_attributes_events_links, -) -from opentelemetry.trace.status import Status, StatusCode - - -def _translate_spans_with_dropped_attributes(): - span = get_span_with_dropped_attributes_events_links() - translate = Translate([span]) - - # pylint: disable=protected-access - return translate._translate(pb_translator.ProtobufTranslator("svc")) - - -# pylint:disable=no-member -class TestJaegerExporter(unittest.TestCase): - def setUp(self): - # create and save span to be used in tests - self.context = trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x00000000DEADBEF0, - is_remote=False, - ) - - self._test_span = trace._Span("test_span", context=self.context) - self._test_span.start() - self._test_span.end() - - # pylint: disable=protected-access - - def test_constructor_by_environment_variables(self): - """Test using Environment Variables.""" - # pylint: disable=protected-access - service = "my-opentelemetry-jaeger" - - collector_endpoint = "localhost:14250" - - env_patch = mock.patch.dict( - "os.environ", - { - OTEL_EXPORTER_JAEGER_ENDPOINT: collector_endpoint, - OTEL_EXPORTER_JAEGER_CERTIFICATE: os.path.dirname(__file__) - + "/certs/cred.cert", - OTEL_RESOURCE_ATTRIBUTES: "service.name=my-opentelemetry-jaeger", - OTEL_EXPORTER_JAEGER_TIMEOUT: "5", - OTEL_EXPORTER_JAEGER_GRPC_INSECURE: "False", - }, - ) - - env_patch.start() - provider = TracerProvider(resource=Resource.create({})) - trace_api.set_tracer_provider(provider) - exporter = JaegerExporter() - self.assertEqual(exporter.service_name, service) - self.assertIsNotNone(exporter._collector_grpc_client) - self.assertEqual(exporter.collector_endpoint, collector_endpoint) - self.assertEqual(exporter._timeout, 5) - self.assertIsNotNone(exporter.credentials) - self.assertEqual(exporter.insecure, False) - env_patch.stop() - - # pylint: disable=too-many-locals,too-many-statements - def test_translate_to_jaeger(self): - - span_names = ("test1", "test2", "test3") - trace_id = 0x6E0C63257DE34C926F9EFCD03927272E - span_id = 0x34BF92DEEFC58C92 - parent_id = 0x1111111111111111 - other_id = 0x2222222222222222 - - base_time = 683647322 * 10**9 # in ns - start_times = ( - base_time, - base_time + 150 * 10**6, - base_time + 300 * 10**6, - ) - durations = (50 * 10**6, 100 * 10**6, 200 * 10**6) - end_times = ( - start_times[0] + durations[0], - start_times[1] + durations[1], - start_times[2] + durations[2], - ) - - span_context = trace_api.SpanContext( - trace_id, span_id, is_remote=False - ) - parent_span_context = trace_api.SpanContext( - trace_id, parent_id, is_remote=False - ) - other_context = trace_api.SpanContext( - trace_id, other_id, is_remote=False - ) - - event_attributes = OrderedDict( - [ - ("annotation_bool", True), - ("annotation_string", "annotation_test"), - ("key_float", 0.3), - ] - ) - - event_timestamp = base_time + 50 * 10**6 - # pylint:disable=protected-access - event_timestamp_proto = ( - pb_translator._proto_timestamp_from_epoch_nanos(event_timestamp) - ) - - event = trace.Event( - name="event0", - timestamp=event_timestamp, - attributes=event_attributes, - ) - - link_attributes = {"key_bool": True} - - link = trace_api.Link( - context=other_context, attributes=link_attributes - ) - - default_tags = [ - model_pb2.KeyValue( - key="span.kind", - v_type=model_pb2.ValueType.STRING, - v_str="internal", - ), - ] - - otel_spans = [ - trace._Span( - name=span_names[0], - context=span_context, - parent=parent_span_context, - events=(event,), - links=(link,), - kind=trace_api.SpanKind.CLIENT, - resource=Resource( - attributes={"key_resource": "some_resource"} - ), - ), - trace._Span( - name=span_names[1], - context=parent_span_context, - parent=None, - resource=Resource({}), - ), - trace._Span( - name=span_names[2], - context=other_context, - parent=None, - resource=Resource({}), - instrumentation_scope=InstrumentationScope( - name="name", version="version" - ), - ), - ] - - otel_spans[0].start(start_time=start_times[0]) - # added here to preserve order - otel_spans[0].set_attribute("key_bool", False) - otel_spans[0].set_attribute("key_string", "hello_world") - otel_spans[0].set_attribute("key_float", 111.22) - otel_spans[0].set_attribute("key_tuple", ("tuple_element",)) - otel_spans[0].set_status( - Status(StatusCode.ERROR, "Example description") - ) - otel_spans[0].end(end_time=end_times[0]) - - otel_spans[1].start(start_time=start_times[1]) - otel_spans[1].end(end_time=end_times[1]) - - otel_spans[2].start(start_time=start_times[2]) - otel_spans[2].set_status(Status(StatusCode.OK)) - otel_spans[2].end(end_time=end_times[2]) - - translate = Translate(otel_spans) - # pylint: disable=protected-access - spans = translate._translate(pb_translator.ProtobufTranslator("svc")) - - span1_start_time = pb_translator._proto_timestamp_from_epoch_nanos( - start_times[0] - ) - span2_start_time = pb_translator._proto_timestamp_from_epoch_nanos( - start_times[1] - ) - span3_start_time = pb_translator._proto_timestamp_from_epoch_nanos( - start_times[2] - ) - - span1_end_time = pb_translator._proto_timestamp_from_epoch_nanos( - end_times[0] - ) - span2_end_time = pb_translator._proto_timestamp_from_epoch_nanos( - end_times[1] - ) - span3_end_time = pb_translator._proto_timestamp_from_epoch_nanos( - end_times[2] - ) - - span1_duration = pb_translator._duration_from_two_time_stamps( - span1_start_time, span1_end_time - ) - span2_duration = pb_translator._duration_from_two_time_stamps( - span2_start_time, span2_end_time - ) - span3_duration = pb_translator._duration_from_two_time_stamps( - span3_start_time, span3_end_time - ) - - expected_spans = [ - model_pb2.Span( - operation_name=span_names[0], - trace_id=pb_translator._trace_id_to_bytes(trace_id), - span_id=pb_translator._span_id_to_bytes(span_id), - start_time=span1_start_time, - duration=span1_duration, - flags=0, - tags=[ - model_pb2.KeyValue( - key="key_bool", - v_type=model_pb2.ValueType.BOOL, - v_bool=False, - ), - model_pb2.KeyValue( - key="key_string", - v_type=model_pb2.ValueType.STRING, - v_str="hello_world", - ), - model_pb2.KeyValue( - key="key_float", - v_type=model_pb2.ValueType.FLOAT64, - v_float64=111.22, - ), - model_pb2.KeyValue( - key="key_tuple", - v_type=model_pb2.ValueType.STRING, - v_str="('tuple_element',)", - ), - model_pb2.KeyValue( - key="key_resource", - v_type=model_pb2.ValueType.STRING, - v_str="some_resource", - ), - model_pb2.KeyValue( - key="otel.status_code", - v_type=model_pb2.ValueType.STRING, - v_str="ERROR", - ), - model_pb2.KeyValue( - key="otel.status_description", - v_type=model_pb2.ValueType.STRING, - v_str="Example description", - ), - model_pb2.KeyValue( - key="span.kind", - v_type=model_pb2.ValueType.STRING, - v_str="client", - ), - model_pb2.KeyValue( - key="error", - v_type=model_pb2.ValueType.BOOL, - v_bool=True, - ), - ], - references=[ - model_pb2.SpanRef( - ref_type=model_pb2.SpanRefType.CHILD_OF, - trace_id=pb_translator._trace_id_to_bytes(trace_id), - span_id=pb_translator._span_id_to_bytes(parent_id), - ), - model_pb2.SpanRef( - ref_type=model_pb2.SpanRefType.FOLLOWS_FROM, - trace_id=pb_translator._trace_id_to_bytes(trace_id), - span_id=pb_translator._span_id_to_bytes(other_id), - ), - ], - logs=[ - model_pb2.Log( - timestamp=event_timestamp_proto, - fields=[ - model_pb2.KeyValue( - key="annotation_bool", - v_type=model_pb2.ValueType.BOOL, - v_bool=True, - ), - model_pb2.KeyValue( - key="annotation_string", - v_type=model_pb2.ValueType.STRING, - v_str="annotation_test", - ), - model_pb2.KeyValue( - key="key_float", - v_type=model_pb2.ValueType.FLOAT64, - v_float64=0.3, - ), - model_pb2.KeyValue( - key="message", - v_type=model_pb2.ValueType.STRING, - v_str="event0", - ), - ], - ) - ], - process=model_pb2.Process( - service_name="svc", - tags=[ - model_pb2.KeyValue( - key="key_resource", - v_str="some_resource", - v_type=model_pb2.ValueType.STRING, - ) - ], - ), - ), - model_pb2.Span( - operation_name=span_names[1], - trace_id=pb_translator._trace_id_to_bytes(trace_id), - span_id=pb_translator._span_id_to_bytes(parent_id), - start_time=span2_start_time, - duration=span2_duration, - flags=0, - tags=default_tags, - process=model_pb2.Process( - service_name="svc", - ), - ), - model_pb2.Span( - operation_name=span_names[2], - trace_id=pb_translator._trace_id_to_bytes(trace_id), - span_id=pb_translator._span_id_to_bytes(other_id), - start_time=span3_start_time, - duration=span3_duration, - flags=0, - tags=[ - model_pb2.KeyValue( - key="otel.status_code", - v_type=model_pb2.ValueType.STRING, - v_str="OK", - ), - model_pb2.KeyValue( - key="span.kind", - v_type=model_pb2.ValueType.STRING, - v_str="internal", - ), - model_pb2.KeyValue( - key=NAME_KEY, - v_type=model_pb2.ValueType.STRING, - v_str="name", - ), - model_pb2.KeyValue( - key=VERSION_KEY, - v_type=model_pb2.ValueType.STRING, - v_str="version", - ), - model_pb2.KeyValue( - key=_SCOPE_NAME_KEY, - v_type=model_pb2.ValueType.STRING, - v_str="name", - ), - model_pb2.KeyValue( - key=_SCOPE_VERSION_KEY, - v_type=model_pb2.ValueType.STRING, - v_str="version", - ), - ], - process=model_pb2.Process( - service_name="svc", - ), - ), - ] - - # events are complicated to compare because order of fields - # (attributes) in otel is not important but in jeager it is - # pylint: disable=no-member - self.assertCountEqual( - spans[0].logs[0].fields, - expected_spans[0].logs[0].fields, - ) - - self.assertEqual(spans, expected_spans) - - def test_max_tag_value_length(self): - span = trace._Span( - name="span", - resource=Resource( - attributes={ - "key_resource": "some_resource some_resource some_more_resource" - } - ), - context=trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x00000000DEADBEF0, - is_remote=False, - ), - ) - - span.start() - span.set_attribute("key_bool", False) - span.set_attribute("key_string", "hello_world hello_world hello_world") - span.set_attribute("key_float", 111.22) - span.set_attribute("key_int", 1100) - span.set_attribute("key_tuple", ("tuple_element", "tuple_element2")) - span.end() - - translate = Translate([span]) - - # does not truncate by default - # pylint: disable=protected-access - spans = translate._translate(pb_translator.ProtobufTranslator("svc")) - tags_by_keys = { - tag.key: tag.v_str - for tag in spans[0].tags - if tag.v_type == model_pb2.ValueType.STRING - } - self.assertEqual( - "hello_world hello_world hello_world", tags_by_keys["key_string"] - ) - self.assertEqual( - "('tuple_element', 'tuple_element2')", tags_by_keys["key_tuple"] - ) - self.assertEqual( - "some_resource some_resource some_more_resource", - tags_by_keys["key_resource"], - ) - - # truncates when max_tag_value_length is passed - # pylint: disable=protected-access - spans = translate._translate( - pb_translator.ProtobufTranslator("svc", max_tag_value_length=5) - ) - tags_by_keys = { - tag.key: tag.v_str - for tag in spans[0].tags - if tag.v_type == model_pb2.ValueType.STRING - } - self.assertEqual("hello", tags_by_keys["key_string"]) - self.assertEqual("('tup", tags_by_keys["key_tuple"]) - self.assertEqual("some_", tags_by_keys["key_resource"]) - - def test_export(self): - client_mock = mock.Mock() - spans = [] - exporter = JaegerExporter() - exporter._grpc_client = client_mock - status = exporter.export(spans) - self.assertEqual(SpanExportResult.SUCCESS, status) - - def test_export_span_service_name(self): - resource = Resource.create({SERVICE_NAME: "test"}) - span = trace._Span( - "test_span", context=self.context, resource=resource - ) - span.start() - span.end() - client_mock = mock.Mock() - exporter = JaegerExporter() - exporter._grpc_client = client_mock - exporter.export([span]) - self.assertEqual(exporter.service_name, "test") - - def test_dropped_span_attributes(self): - spans = _translate_spans_with_dropped_attributes() - tags_by_keys = { - tag.key: tag.v_str or tag.v_int64 for tag in spans[0].tags - } - self.assertEqual(1, tags_by_keys["otel.dropped_links_count"]) - self.assertEqual(2, tags_by_keys["otel.dropped_attributes_count"]) - self.assertEqual(3, tags_by_keys["otel.dropped_events_count"]) - - def test_dropped_event_attributes(self): - spans = _translate_spans_with_dropped_attributes() - fields_by_keys = { - tag.key: tag.v_str or tag.v_int64 - for tag in spans[0].logs[0].fields - } - # get events - self.assertEqual( - 2, - fields_by_keys["otel.dropped_attributes_count"], - ) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/LICENSE b/exporter/opentelemetry-exporter-jaeger-thrift/LICENSE deleted file mode 100644 index 1ef7dad2c5..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 The OpenTelemetry Authors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/README.rst b/exporter/opentelemetry-exporter-jaeger-thrift/README.rst deleted file mode 100644 index d6930f624f..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/README.rst +++ /dev/null @@ -1,42 +0,0 @@ -OpenTelemetry Jaeger Thrift Exporter -==================================== - -.. warning:: - Since v1.35, the Jaeger supports OTLP natively. Please use the OTLP exporter instead. - Support for this exporter will end July 2023. - - This package is no longer being tested. - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger-thrift.svg - :target: https://pypi.org/project/opentelemetry-exporter-jaeger-thrift/ - -This library allows to export tracing data to `Jaeger `_ using Thrift. - -Installation ------------- - -:: - - pip install opentelemetry-exporter-jaeger-thrift - - -.. _Jaeger: https://www.jaegertracing.io/ -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ - -Configuration -------------- - -OpenTelemetry Jaeger Exporter can be configured by setting `JaegerExporter parameters -`_ or by setting -`environment variables `_ - -References ----------- - -* `OpenTelemetry Jaeger Exporter `_ -* `Jaeger `_ -* `OpenTelemetry Project `_ diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/examples/jaeger_exporter_example.py b/exporter/opentelemetry-exporter-jaeger-thrift/examples/jaeger_exporter_example.py deleted file mode 100644 index 5678e3346d..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/examples/jaeger_exporter_example.py +++ /dev/null @@ -1,50 +0,0 @@ -import time - -from opentelemetry import trace -from opentelemetry.exporter.jaeger import thrift -from opentelemetry.sdk.resources import SERVICE_NAME, Resource -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor - -trace.set_tracer_provider( - TracerProvider( - resource=Resource.create({SERVICE_NAME: "my-helloworld-service"}) - ) -) -tracer = trace.get_tracer(__name__) - -# create a JaegerExporter -jaeger_exporter = thrift.JaegerExporter( - # configure agent - agent_host_name="localhost", - agent_port=6831, - # optional: configure also collector - # collector_endpoint="http://localhost:14268/api/traces?format=jaeger.thrift", - # username=xxxx, # optional - # password=xxxx, # optional -) - -# create a BatchSpanProcessor and add the exporter to it -span_processor = BatchSpanProcessor(jaeger_exporter) - -# add to the tracer factory -trace.get_tracer_provider().add_span_processor(span_processor) - -# create some spans for testing -with tracer.start_as_current_span("foo") as foo: - time.sleep(0.1) - foo.set_attribute("my_atribbute", True) - foo.add_event("event in foo", {"name": "foo1"}) - with tracer.start_as_current_span( - "bar", links=[trace.Link(foo.get_span_context())] - ) as bar: - time.sleep(0.2) - bar.set_attribute("speed", 100.0) - - with tracer.start_as_current_span("baz") as baz: - time.sleep(0.3) - baz.set_attribute("name", "mauricio") - - time.sleep(0.2) - - time.sleep(0.1) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/pyproject.toml b/exporter/opentelemetry-exporter-jaeger-thrift/pyproject.toml deleted file mode 100644 index 48e3a76848..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/pyproject.toml +++ /dev/null @@ -1,53 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "opentelemetry-exporter-jaeger-thrift" -dynamic = ["version"] -description = "Jaeger Thrift Exporter for OpenTelemetry" -readme = "README.rst" -license = "Apache-2.0" -requires-python = ">=3.7" -authors = [ - { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, -] -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Typing :: Typed", -] -dependencies = [ - "opentelemetry-api ~= 1.3", - "opentelemetry-sdk ~= 1.11", - "thrift >= 0.10.0", -] - -[project.optional-dependencies] -test = [] - -[project.entry-points.opentelemetry_traces_exporter] -jaeger_thrift = "opentelemetry.exporter.jaeger.thrift:JaegerExporter" - -[project.urls] -Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger-thrift" - -[tool.hatch.version] -path = "src/opentelemetry/exporter/jaeger/thrift/version.py" - -[tool.hatch.build.targets.sdist] -include = [ - "/src", - "/tests", -] - -[tool.hatch.build.targets.wheel] -packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py deleted file mode 100644 index 95181ad617..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/__init__.py +++ /dev/null @@ -1,233 +0,0 @@ -# Copyright 2018, OpenCensus Authors -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" - -OpenTelemetry Jaeger Thrift Exporter ------------------------------------- - -The **OpenTelemetry Jaeger Thrift Exporter** allows to export `OpenTelemetry`_ traces to `Jaeger`_. -This exporter always sends traces to the configured agent using the Thrift compact protocol over UDP. -When it is not feasible to deploy Jaeger Agent next to the application, for example, when the -application code is running as Lambda function, a collector can be configured to send spans -using Thrift over HTTP. If both agent and collector are configured, the exporter sends traces -only to the collector to eliminate the duplicate entries. - -Usage ------ - -.. code:: python - - from opentelemetry import trace - from opentelemetry.exporter.jaeger.thrift import JaegerExporter - from opentelemetry.sdk.resources import SERVICE_NAME, Resource - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchSpanProcessor - - trace.set_tracer_provider( - TracerProvider( - resource=Resource.create({SERVICE_NAME: "my-helloworld-service"}) - ) - ) - tracer = trace.get_tracer(__name__) - - # create a JaegerExporter - jaeger_exporter = JaegerExporter( - # configure agent - agent_host_name='localhost', - agent_port=6831, - # optional: configure also collector - # collector_endpoint='http://localhost:14268/api/traces?format=jaeger.thrift', - # username=xxxx, # optional - # password=xxxx, # optional - # max_tag_value_length=None # optional - ) - - # Create a BatchSpanProcessor and add the exporter to it - span_processor = BatchSpanProcessor(jaeger_exporter) - - # add to the tracer - trace.get_tracer_provider().add_span_processor(span_processor) - - with tracer.start_as_current_span('foo'): - print('Hello world!') - -You can configure the exporter with the following environment variables: - -- :envvar:`OTEL_EXPORTER_JAEGER_USER` -- :envvar:`OTEL_EXPORTER_JAEGER_PASSWORD` -- :envvar:`OTEL_EXPORTER_JAEGER_ENDPOINT` -- :envvar:`OTEL_EXPORTER_JAEGER_AGENT_PORT` -- :envvar:`OTEL_EXPORTER_JAEGER_AGENT_HOST` -- :envvar:`OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES` -- :envvar:`OTEL_EXPORTER_JAEGER_TIMEOUT` - -API ---- -.. _Jaeger: https://www.jaegertracing.io/ -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ -""" -# pylint: disable=protected-access - -import logging -from os import environ -from typing import Optional - -from deprecated import deprecated - -from opentelemetry import trace -from opentelemetry.exporter.jaeger.thrift.gen.jaeger import ( - Collector as jaeger_thrift, -) -from opentelemetry.exporter.jaeger.thrift.send import AgentClientUDP, Collector -from opentelemetry.exporter.jaeger.thrift.translate import ( - ThriftTranslator, - Translate, -) -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_JAEGER_AGENT_HOST, - OTEL_EXPORTER_JAEGER_AGENT_PORT, - OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES, - OTEL_EXPORTER_JAEGER_ENDPOINT, - OTEL_EXPORTER_JAEGER_PASSWORD, - OTEL_EXPORTER_JAEGER_TIMEOUT, - OTEL_EXPORTER_JAEGER_USER, -) -from opentelemetry.sdk.resources import SERVICE_NAME, Resource -from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult - -DEFAULT_AGENT_HOST_NAME = "localhost" -DEFAULT_AGENT_PORT = 6831 -DEFAULT_EXPORT_TIMEOUT = 10 - -logger = logging.getLogger(__name__) - - -class JaegerExporter(SpanExporter): - """Jaeger span exporter for OpenTelemetry. - - Args: - agent_host_name: The host name of the Jaeger-Agent. - agent_port: The port of the Jaeger-Agent. - collector_endpoint: The endpoint of the Jaeger collector that uses - Thrift over HTTP/HTTPS. - username: The user name of the Basic Auth if authentication is - required. - password: The password of the Basic Auth if authentication is - required. - max_tag_value_length: Max length string attribute values can have. Set to None to disable. - udp_split_oversized_batches: Re-emit oversized batches in smaller chunks. - timeout: Maximum time the Jaeger exporter should wait for each batch export. - """ - - @deprecated( - version="1.16.0", - reason="Since v1.35, the Jaeger supports OTLP natively. Please use the OTLP exporter instead. Support for this exporter will end July 2023.", - ) - def __init__( - self, - agent_host_name: Optional[str] = None, - agent_port: Optional[int] = None, - collector_endpoint: Optional[str] = None, - username: Optional[str] = None, - password: Optional[str] = None, - max_tag_value_length: Optional[int] = None, - udp_split_oversized_batches: bool = None, - timeout: Optional[int] = None, - ): - self._max_tag_value_length = max_tag_value_length - self.agent_host_name = agent_host_name or environ.get( - OTEL_EXPORTER_JAEGER_AGENT_HOST, DEFAULT_AGENT_HOST_NAME - ) - - self.agent_port = agent_port or int( - environ.get(OTEL_EXPORTER_JAEGER_AGENT_PORT, DEFAULT_AGENT_PORT) - ) - - self._timeout = timeout or int( - environ.get(OTEL_EXPORTER_JAEGER_TIMEOUT, DEFAULT_EXPORT_TIMEOUT) - ) - - self.udp_split_oversized_batches = udp_split_oversized_batches or bool( - environ.get(OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES) - ) - self._agent_client = AgentClientUDP( - host_name=self.agent_host_name, - port=self.agent_port, - split_oversized_batches=self.udp_split_oversized_batches, - ) - self.collector_endpoint = collector_endpoint or environ.get( - OTEL_EXPORTER_JAEGER_ENDPOINT - ) - self.username = username or environ.get(OTEL_EXPORTER_JAEGER_USER) - self.password = password or environ.get(OTEL_EXPORTER_JAEGER_PASSWORD) - self._collector = None - tracer_provider = trace.get_tracer_provider() - self.service_name = ( - tracer_provider.resource.attributes[SERVICE_NAME] - if getattr(tracer_provider, "resource", None) - else Resource.create().attributes.get(SERVICE_NAME) - ) - - @property - def _collector_http_client(self) -> Optional[Collector]: - if self._collector is not None: - return self._collector - - if self.collector_endpoint is None: - return None - - auth = None - if self.username is not None and self.password is not None: - auth = (self.username, self.password) - - # Thrift HTTP Client expects timeout in millis - timeout_in_millis = self._timeout * 1000.0 - self._collector = Collector( - thrift_url=self.collector_endpoint, - auth=auth, - timeout_in_millis=timeout_in_millis, - ) - return self._collector - - def export(self, spans) -> SpanExportResult: - # Populate service_name from first span - # We restrict any SpanProcessor to be only associated with a single - # TracerProvider, so it is safe to assume that all Spans in a single - # batch all originate from one TracerProvider (and in turn have all - # the same service.name) - if spans: - service_name = spans[0].resource.attributes.get(SERVICE_NAME) - if service_name: - self.service_name = service_name - translator = Translate(spans) - thrift_translator = ThriftTranslator(self._max_tag_value_length) - jaeger_spans = translator._translate(thrift_translator) - batch = jaeger_thrift.Batch( - spans=jaeger_spans, - process=jaeger_thrift.Process(serviceName=self.service_name), - ) - if self._collector_http_client is not None: - self._collector_http_client.submit(batch) - else: - self._agent_client.emit(batch) - - return SpanExportResult.SUCCESS - - def shutdown(self): - pass - - def force_flush(self, timeout_millis: int = 30000) -> bool: - return True diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/__init__.py deleted file mode 100644 index 52b3cfb3e9..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ - -import sys -from os.path import dirname -sys.path.append(dirname(__file__)) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/Agent-remote b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/Agent-remote deleted file mode 100644 index 5db3d20804..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/Agent-remote +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python -# -# Autogenerated by Thrift Compiler (0.10.0) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py -# - -import sys -import pprint -if sys.version_info[0] > 2: - from urllib.parse import urlparse -else: - from urlparse import urlparse -from thrift.transport import TTransport, TSocket, TSSLSocket, THttpClient -from thrift.protocol.TBinaryProtocol import TBinaryProtocol - -from agent import Agent -from agent.ttypes import * - -if len(sys.argv) <= 1 or sys.argv[1] == '--help': - print('') - print('Usage: ' + sys.argv[0] + ' [-h host[:port]] [-u url] [-f[ramed]] [-s[sl]] [-novalidate] [-ca_certs certs] [-keyfile keyfile] [-certfile certfile] function [arg1 [arg2...]]') - print('') - print('Functions:') - print(' void emitZipkinBatch( spans)') - print(' void emitBatch(Batch batch)') - print('') - sys.exit(0) - -pp = pprint.PrettyPrinter(indent=2) -host = 'localhost' -port = 9090 -uri = '' -framed = False -ssl = False -validate = True -ca_certs = None -keyfile = None -certfile = None -http = False -argi = 1 - -if sys.argv[argi] == '-h': - parts = sys.argv[argi + 1].split(':') - host = parts[0] - if len(parts) > 1: - port = int(parts[1]) - argi += 2 - -if sys.argv[argi] == '-u': - url = urlparse(sys.argv[argi + 1]) - parts = url[1].split(':') - host = parts[0] - if len(parts) > 1: - port = int(parts[1]) - else: - port = 80 - uri = url[2] - if url[4]: - uri += '?%s' % url[4] - http = True - argi += 2 - -if sys.argv[argi] == '-f' or sys.argv[argi] == '-framed': - framed = True - argi += 1 - -if sys.argv[argi] == '-s' or sys.argv[argi] == '-ssl': - ssl = True - argi += 1 - -if sys.argv[argi] == '-novalidate': - validate = False - argi += 1 - -if sys.argv[argi] == '-ca_certs': - ca_certs = sys.argv[argi+1] - argi += 2 - -if sys.argv[argi] == '-keyfile': - keyfile = sys.argv[argi+1] - argi += 2 - -if sys.argv[argi] == '-certfile': - certfile = sys.argv[argi+1] - argi += 2 - -cmd = sys.argv[argi] -args = sys.argv[argi + 1:] - -if http: - transport = THttpClient.THttpClient(host, port, uri) -else: - if ssl: - socket = TSSLSocket.TSSLSocket(host, port, validate=validate, ca_certs=ca_certs, keyfile=keyfile, certfile=certfile) - else: - socket = TSocket.TSocket(host, port) - if framed: - transport = TTransport.TFramedTransport(socket) - else: - transport = TTransport.TBufferedTransport(socket) -protocol = TBinaryProtocol(transport) -client = Agent.Client(protocol) -transport.open() - -if cmd == 'emitZipkinBatch': - if len(args) != 1: - print('emitZipkinBatch requires 1 args') - sys.exit(1) - pp.pprint(client.emitZipkinBatch(eval(args[0]),)) - -elif cmd == 'emitBatch': - if len(args) != 1: - print('emitBatch requires 1 args') - sys.exit(1) - pp.pprint(client.emitBatch(eval(args[0]),)) - -else: - print('Unrecognized method %s' % cmd) - sys.exit(1) - -transport.close() diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/Agent.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/Agent.py deleted file mode 100644 index e8e0fe662e..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/Agent.py +++ /dev/null @@ -1,246 +0,0 @@ -# -# Autogenerated by Thrift Compiler (0.10.0) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py -# - -from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException -from thrift.protocol.TProtocol import TProtocolException -import sys -import logging -from .ttypes import * -from thrift.Thrift import TProcessor -from thrift.transport import TTransport - - -class Iface(object): - def emitZipkinBatch(self, spans): - """ - Parameters: - - spans - """ - pass - - def emitBatch(self, batch): - """ - Parameters: - - batch - """ - pass - - -class Client(Iface): - def __init__(self, iprot, oprot=None): - self._iprot = self._oprot = iprot - if oprot is not None: - self._oprot = oprot - self._seqid = 0 - - def emitZipkinBatch(self, spans): - """ - Parameters: - - spans - """ - self.send_emitZipkinBatch(spans) - - def send_emitZipkinBatch(self, spans): - self._oprot.writeMessageBegin('emitZipkinBatch', TMessageType.ONEWAY, self._seqid) - args = emitZipkinBatch_args() - args.spans = spans - args.write(self._oprot) - self._oprot.writeMessageEnd() - self._oprot.trans.flush() - - def emitBatch(self, batch): - """ - Parameters: - - batch - """ - self.send_emitBatch(batch) - - def send_emitBatch(self, batch): - self._oprot.writeMessageBegin('emitBatch', TMessageType.ONEWAY, self._seqid) - args = emitBatch_args() - args.batch = batch - args.write(self._oprot) - self._oprot.writeMessageEnd() - self._oprot.trans.flush() - - -class Processor(Iface, TProcessor): - def __init__(self, handler): - self._handler = handler - self._processMap = {} - self._processMap["emitZipkinBatch"] = Processor.process_emitZipkinBatch - self._processMap["emitBatch"] = Processor.process_emitBatch - - def process(self, iprot, oprot): - (name, type, seqid) = iprot.readMessageBegin() - if name not in self._processMap: - iprot.skip(TType.STRUCT) - iprot.readMessageEnd() - x = TApplicationException(TApplicationException.UNKNOWN_METHOD, 'Unknown function %s' % (name)) - oprot.writeMessageBegin(name, TMessageType.EXCEPTION, seqid) - x.write(oprot) - oprot.writeMessageEnd() - oprot.trans.flush() - return - else: - self._processMap[name](self, seqid, iprot, oprot) - return True - - def process_emitZipkinBatch(self, seqid, iprot, oprot): - args = emitZipkinBatch_args() - args.read(iprot) - iprot.readMessageEnd() - try: - self._handler.emitZipkinBatch(args.spans) - except (TTransport.TTransportException, KeyboardInterrupt, SystemExit): - raise - except: - pass - - def process_emitBatch(self, seqid, iprot, oprot): - args = emitBatch_args() - args.read(iprot) - iprot.readMessageEnd() - try: - self._handler.emitBatch(args.batch) - except (TTransport.TTransportException, KeyboardInterrupt, SystemExit): - raise - except: - pass - -# HELPER FUNCTIONS AND STRUCTURES - - -class emitZipkinBatch_args(object): - """ - Attributes: - - spans - """ - - thrift_spec = ( - None, # 0 - (1, TType.LIST, 'spans', (TType.STRUCT, (zipkincore.ttypes.Span, zipkincore.ttypes.Span.thrift_spec), False), None, ), # 1 - ) - - def __init__(self, spans=None,): - self.spans = spans - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.LIST: - self.spans = [] - (_etype3, _size0) = iprot.readListBegin() - for _i4 in range(_size0): - _elem5 = zipkincore.ttypes.Span() - _elem5.read(iprot) - self.spans.append(_elem5) - iprot.readListEnd() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('emitZipkinBatch_args') - if self.spans is not None: - oprot.writeFieldBegin('spans', TType.LIST, 1) - oprot.writeListBegin(TType.STRUCT, len(self.spans)) - for iter6 in self.spans: - iter6.write(oprot) - oprot.writeListEnd() - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - - -class emitBatch_args(object): - """ - Attributes: - - batch - """ - - thrift_spec = ( - None, # 0 - (1, TType.STRUCT, 'batch', (jaeger.ttypes.Batch, jaeger.ttypes.Batch.thrift_spec), None, ), # 1 - ) - - def __init__(self, batch=None,): - self.batch = batch - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.STRUCT: - self.batch = jaeger.ttypes.Batch() - self.batch.read(iprot) - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('emitBatch_args') - if self.batch is not None: - oprot.writeFieldBegin('batch', TType.STRUCT, 1) - self.batch.write(oprot) - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/__init__.py deleted file mode 100644 index 1059cfbc01..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ['ttypes', 'constants', 'Agent'] diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/constants.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/constants.py deleted file mode 100644 index eb0d35aa12..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/constants.py +++ /dev/null @@ -1,12 +0,0 @@ -# -# Autogenerated by Thrift Compiler (0.10.0) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py -# - -from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException -from thrift.protocol.TProtocol import TProtocolException -import sys -from .ttypes import * diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/ttypes.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/ttypes.py deleted file mode 100644 index fc8743cba9..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/agent/ttypes.py +++ /dev/null @@ -1,15 +0,0 @@ -# -# Autogenerated by Thrift Compiler (0.10.0) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py -# - -from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException -from thrift.protocol.TProtocol import TProtocolException -import sys -import jaeger.ttypes -import zipkincore.ttypes - -from thrift.transport import TTransport diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/Collector-remote b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/Collector-remote deleted file mode 100644 index 5903f02360..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/Collector-remote +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python -# -# Autogenerated by Thrift Compiler (0.10.0) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py -# - -import sys -import pprint -if sys.version_info[0] > 2: - from urllib.parse import urlparse -else: - from urlparse import urlparse -from thrift.transport import TTransport, TSocket, TSSLSocket, THttpClient -from thrift.protocol.TBinaryProtocol import TBinaryProtocol - -from jaeger import Collector -from jaeger.ttypes import * - -if len(sys.argv) <= 1 or sys.argv[1] == '--help': - print('') - print('Usage: ' + sys.argv[0] + ' [-h host[:port]] [-u url] [-f[ramed]] [-s[sl]] [-novalidate] [-ca_certs certs] [-keyfile keyfile] [-certfile certfile] function [arg1 [arg2...]]') - print('') - print('Functions:') - print(' submitBatches( batches)') - print('') - sys.exit(0) - -pp = pprint.PrettyPrinter(indent=2) -host = 'localhost' -port = 9090 -uri = '' -framed = False -ssl = False -validate = True -ca_certs = None -keyfile = None -certfile = None -http = False -argi = 1 - -if sys.argv[argi] == '-h': - parts = sys.argv[argi + 1].split(':') - host = parts[0] - if len(parts) > 1: - port = int(parts[1]) - argi += 2 - -if sys.argv[argi] == '-u': - url = urlparse(sys.argv[argi + 1]) - parts = url[1].split(':') - host = parts[0] - if len(parts) > 1: - port = int(parts[1]) - else: - port = 80 - uri = url[2] - if url[4]: - uri += '?%s' % url[4] - http = True - argi += 2 - -if sys.argv[argi] == '-f' or sys.argv[argi] == '-framed': - framed = True - argi += 1 - -if sys.argv[argi] == '-s' or sys.argv[argi] == '-ssl': - ssl = True - argi += 1 - -if sys.argv[argi] == '-novalidate': - validate = False - argi += 1 - -if sys.argv[argi] == '-ca_certs': - ca_certs = sys.argv[argi+1] - argi += 2 - -if sys.argv[argi] == '-keyfile': - keyfile = sys.argv[argi+1] - argi += 2 - -if sys.argv[argi] == '-certfile': - certfile = sys.argv[argi+1] - argi += 2 - -cmd = sys.argv[argi] -args = sys.argv[argi + 1:] - -if http: - transport = THttpClient.THttpClient(host, port, uri) -else: - if ssl: - socket = TSSLSocket.TSSLSocket(host, port, validate=validate, ca_certs=ca_certs, keyfile=keyfile, certfile=certfile) - else: - socket = TSocket.TSocket(host, port) - if framed: - transport = TTransport.TFramedTransport(socket) - else: - transport = TTransport.TBufferedTransport(socket) -protocol = TBinaryProtocol(transport) -client = Collector.Client(protocol) -transport.open() - -if cmd == 'submitBatches': - if len(args) != 1: - print('submitBatches requires 1 args') - sys.exit(1) - pp.pprint(client.submitBatches(eval(args[0]),)) - -else: - print('Unrecognized method %s' % cmd) - sys.exit(1) - -transport.close() diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/Collector.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/Collector.py deleted file mode 100644 index f6f809b089..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/Collector.py +++ /dev/null @@ -1,243 +0,0 @@ -# -# Autogenerated by Thrift Compiler (0.10.0) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py -# - -from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException -from thrift.protocol.TProtocol import TProtocolException -import sys -import logging -from .ttypes import * -from thrift.Thrift import TProcessor -from thrift.transport import TTransport - - -class Iface(object): - def submitBatches(self, batches): - """ - Parameters: - - batches - """ - pass - - -class Client(Iface): - def __init__(self, iprot, oprot=None): - self._iprot = self._oprot = iprot - if oprot is not None: - self._oprot = oprot - self._seqid = 0 - - def submitBatches(self, batches): - """ - Parameters: - - batches - """ - self.send_submitBatches(batches) - return self.recv_submitBatches() - - def send_submitBatches(self, batches): - self._oprot.writeMessageBegin('submitBatches', TMessageType.CALL, self._seqid) - args = submitBatches_args() - args.batches = batches - args.write(self._oprot) - self._oprot.writeMessageEnd() - self._oprot.trans.flush() - - def recv_submitBatches(self): - iprot = self._iprot - (fname, mtype, rseqid) = iprot.readMessageBegin() - if mtype == TMessageType.EXCEPTION: - x = TApplicationException() - x.read(iprot) - iprot.readMessageEnd() - raise x - result = submitBatches_result() - result.read(iprot) - iprot.readMessageEnd() - if result.success is not None: - return result.success - raise TApplicationException(TApplicationException.MISSING_RESULT, "submitBatches failed: unknown result") - - -class Processor(Iface, TProcessor): - def __init__(self, handler): - self._handler = handler - self._processMap = {} - self._processMap["submitBatches"] = Processor.process_submitBatches - - def process(self, iprot, oprot): - (name, type, seqid) = iprot.readMessageBegin() - if name not in self._processMap: - iprot.skip(TType.STRUCT) - iprot.readMessageEnd() - x = TApplicationException(TApplicationException.UNKNOWN_METHOD, 'Unknown function %s' % (name)) - oprot.writeMessageBegin(name, TMessageType.EXCEPTION, seqid) - x.write(oprot) - oprot.writeMessageEnd() - oprot.trans.flush() - return - else: - self._processMap[name](self, seqid, iprot, oprot) - return True - - def process_submitBatches(self, seqid, iprot, oprot): - args = submitBatches_args() - args.read(iprot) - iprot.readMessageEnd() - result = submitBatches_result() - try: - result.success = self._handler.submitBatches(args.batches) - msg_type = TMessageType.REPLY - except (TTransport.TTransportException, KeyboardInterrupt, SystemExit): - raise - except Exception as ex: - msg_type = TMessageType.EXCEPTION - logging.exception(ex) - result = TApplicationException(TApplicationException.INTERNAL_ERROR, 'Internal error') - oprot.writeMessageBegin("submitBatches", msg_type, seqid) - result.write(oprot) - oprot.writeMessageEnd() - oprot.trans.flush() - -# HELPER FUNCTIONS AND STRUCTURES - - -class submitBatches_args(object): - """ - Attributes: - - batches - """ - - thrift_spec = ( - None, # 0 - (1, TType.LIST, 'batches', (TType.STRUCT, (Batch, Batch.thrift_spec), False), None, ), # 1 - ) - - def __init__(self, batches=None,): - self.batches = batches - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.LIST: - self.batches = [] - (_etype45, _size42) = iprot.readListBegin() - for _i46 in range(_size42): - _elem47 = Batch() - _elem47.read(iprot) - self.batches.append(_elem47) - iprot.readListEnd() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('submitBatches_args') - if self.batches is not None: - oprot.writeFieldBegin('batches', TType.LIST, 1) - oprot.writeListBegin(TType.STRUCT, len(self.batches)) - for iter48 in self.batches: - iter48.write(oprot) - oprot.writeListEnd() - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - - -class submitBatches_result(object): - """ - Attributes: - - success - """ - - thrift_spec = ( - (0, TType.LIST, 'success', (TType.STRUCT, (BatchSubmitResponse, BatchSubmitResponse.thrift_spec), False), None, ), # 0 - ) - - def __init__(self, success=None,): - self.success = success - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 0: - if ftype == TType.LIST: - self.success = [] - (_etype52, _size49) = iprot.readListBegin() - for _i53 in range(_size49): - _elem54 = BatchSubmitResponse() - _elem54.read(iprot) - self.success.append(_elem54) - iprot.readListEnd() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('submitBatches_result') - if self.success is not None: - oprot.writeFieldBegin('success', TType.LIST, 0) - oprot.writeListBegin(TType.STRUCT, len(self.success)) - for iter55 in self.success: - iter55.write(oprot) - oprot.writeListEnd() - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/__init__.py deleted file mode 100644 index 515d97d672..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ['ttypes', 'constants', 'Collector'] diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/constants.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/constants.py deleted file mode 100644 index eb0d35aa12..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/constants.py +++ /dev/null @@ -1,12 +0,0 @@ -# -# Autogenerated by Thrift Compiler (0.10.0) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py -# - -from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException -from thrift.protocol.TProtocol import TProtocolException -import sys -from .ttypes import * diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/ttypes.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/ttypes.py deleted file mode 100644 index a43252b79d..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/jaeger/ttypes.py +++ /dev/null @@ -1,831 +0,0 @@ -# -# Autogenerated by Thrift Compiler (0.10.0) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py -# - -from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException -from thrift.protocol.TProtocol import TProtocolException -import sys - -from thrift.transport import TTransport - - -class TagType(object): - STRING = 0 - DOUBLE = 1 - BOOL = 2 - LONG = 3 - BINARY = 4 - - _VALUES_TO_NAMES = { - 0: "STRING", - 1: "DOUBLE", - 2: "BOOL", - 3: "LONG", - 4: "BINARY", - } - - _NAMES_TO_VALUES = { - "STRING": 0, - "DOUBLE": 1, - "BOOL": 2, - "LONG": 3, - "BINARY": 4, - } - - -class SpanRefType(object): - CHILD_OF = 0 - FOLLOWS_FROM = 1 - - _VALUES_TO_NAMES = { - 0: "CHILD_OF", - 1: "FOLLOWS_FROM", - } - - _NAMES_TO_VALUES = { - "CHILD_OF": 0, - "FOLLOWS_FROM": 1, - } - - -class Tag(object): - """ - Attributes: - - key - - vType - - vStr - - vDouble - - vBool - - vLong - - vBinary - """ - - thrift_spec = ( - None, # 0 - (1, TType.STRING, 'key', 'UTF8', None, ), # 1 - (2, TType.I32, 'vType', None, None, ), # 2 - (3, TType.STRING, 'vStr', 'UTF8', None, ), # 3 - (4, TType.DOUBLE, 'vDouble', None, None, ), # 4 - (5, TType.BOOL, 'vBool', None, None, ), # 5 - (6, TType.I64, 'vLong', None, None, ), # 6 - (7, TType.STRING, 'vBinary', 'BINARY', None, ), # 7 - ) - - def __init__(self, key=None, vType=None, vStr=None, vDouble=None, vBool=None, vLong=None, vBinary=None,): - self.key = key - self.vType = vType - self.vStr = vStr - self.vDouble = vDouble - self.vBool = vBool - self.vLong = vLong - self.vBinary = vBinary - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.STRING: - self.key = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() - else: - iprot.skip(ftype) - elif fid == 2: - if ftype == TType.I32: - self.vType = iprot.readI32() - else: - iprot.skip(ftype) - elif fid == 3: - if ftype == TType.STRING: - self.vStr = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() - else: - iprot.skip(ftype) - elif fid == 4: - if ftype == TType.DOUBLE: - self.vDouble = iprot.readDouble() - else: - iprot.skip(ftype) - elif fid == 5: - if ftype == TType.BOOL: - self.vBool = iprot.readBool() - else: - iprot.skip(ftype) - elif fid == 6: - if ftype == TType.I64: - self.vLong = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 7: - if ftype == TType.STRING: - self.vBinary = iprot.readBinary() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('Tag') - if self.key is not None: - oprot.writeFieldBegin('key', TType.STRING, 1) - oprot.writeString(self.key.encode('utf-8') if sys.version_info[0] == 2 else self.key) - oprot.writeFieldEnd() - if self.vType is not None: - oprot.writeFieldBegin('vType', TType.I32, 2) - oprot.writeI32(self.vType) - oprot.writeFieldEnd() - if self.vStr is not None: - oprot.writeFieldBegin('vStr', TType.STRING, 3) - oprot.writeString(self.vStr.encode('utf-8') if sys.version_info[0] == 2 else self.vStr) - oprot.writeFieldEnd() - if self.vDouble is not None: - oprot.writeFieldBegin('vDouble', TType.DOUBLE, 4) - oprot.writeDouble(self.vDouble) - oprot.writeFieldEnd() - if self.vBool is not None: - oprot.writeFieldBegin('vBool', TType.BOOL, 5) - oprot.writeBool(self.vBool) - oprot.writeFieldEnd() - if self.vLong is not None: - oprot.writeFieldBegin('vLong', TType.I64, 6) - oprot.writeI64(self.vLong) - oprot.writeFieldEnd() - if self.vBinary is not None: - oprot.writeFieldBegin('vBinary', TType.STRING, 7) - oprot.writeBinary(self.vBinary) - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - if self.key is None: - raise TProtocolException(message='Required field key is unset!') - if self.vType is None: - raise TProtocolException(message='Required field vType is unset!') - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - - -class Log(object): - """ - Attributes: - - timestamp - - fields - """ - - thrift_spec = ( - None, # 0 - (1, TType.I64, 'timestamp', None, None, ), # 1 - (2, TType.LIST, 'fields', (TType.STRUCT, (Tag, Tag.thrift_spec), False), None, ), # 2 - ) - - def __init__(self, timestamp=None, fields=None,): - self.timestamp = timestamp - self.fields = fields - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.I64: - self.timestamp = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 2: - if ftype == TType.LIST: - self.fields = [] - (_etype3, _size0) = iprot.readListBegin() - for _i4 in range(_size0): - _elem5 = Tag() - _elem5.read(iprot) - self.fields.append(_elem5) - iprot.readListEnd() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('Log') - if self.timestamp is not None: - oprot.writeFieldBegin('timestamp', TType.I64, 1) - oprot.writeI64(self.timestamp) - oprot.writeFieldEnd() - if self.fields is not None: - oprot.writeFieldBegin('fields', TType.LIST, 2) - oprot.writeListBegin(TType.STRUCT, len(self.fields)) - for iter6 in self.fields: - iter6.write(oprot) - oprot.writeListEnd() - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - if self.timestamp is None: - raise TProtocolException(message='Required field timestamp is unset!') - if self.fields is None: - raise TProtocolException(message='Required field fields is unset!') - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - - -class SpanRef(object): - """ - Attributes: - - refType - - traceIdLow - - traceIdHigh - - spanId - """ - - thrift_spec = ( - None, # 0 - (1, TType.I32, 'refType', None, None, ), # 1 - (2, TType.I64, 'traceIdLow', None, None, ), # 2 - (3, TType.I64, 'traceIdHigh', None, None, ), # 3 - (4, TType.I64, 'spanId', None, None, ), # 4 - ) - - def __init__(self, refType=None, traceIdLow=None, traceIdHigh=None, spanId=None,): - self.refType = refType - self.traceIdLow = traceIdLow - self.traceIdHigh = traceIdHigh - self.spanId = spanId - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.I32: - self.refType = iprot.readI32() - else: - iprot.skip(ftype) - elif fid == 2: - if ftype == TType.I64: - self.traceIdLow = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 3: - if ftype == TType.I64: - self.traceIdHigh = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 4: - if ftype == TType.I64: - self.spanId = iprot.readI64() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('SpanRef') - if self.refType is not None: - oprot.writeFieldBegin('refType', TType.I32, 1) - oprot.writeI32(self.refType) - oprot.writeFieldEnd() - if self.traceIdLow is not None: - oprot.writeFieldBegin('traceIdLow', TType.I64, 2) - oprot.writeI64(self.traceIdLow) - oprot.writeFieldEnd() - if self.traceIdHigh is not None: - oprot.writeFieldBegin('traceIdHigh', TType.I64, 3) - oprot.writeI64(self.traceIdHigh) - oprot.writeFieldEnd() - if self.spanId is not None: - oprot.writeFieldBegin('spanId', TType.I64, 4) - oprot.writeI64(self.spanId) - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - if self.refType is None: - raise TProtocolException(message='Required field refType is unset!') - if self.traceIdLow is None: - raise TProtocolException(message='Required field traceIdLow is unset!') - if self.traceIdHigh is None: - raise TProtocolException(message='Required field traceIdHigh is unset!') - if self.spanId is None: - raise TProtocolException(message='Required field spanId is unset!') - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - - -class Span(object): - """ - Attributes: - - traceIdLow - - traceIdHigh - - spanId - - parentSpanId - - operationName - - references - - flags - - startTime - - duration - - tags - - logs - """ - - thrift_spec = ( - None, # 0 - (1, TType.I64, 'traceIdLow', None, None, ), # 1 - (2, TType.I64, 'traceIdHigh', None, None, ), # 2 - (3, TType.I64, 'spanId', None, None, ), # 3 - (4, TType.I64, 'parentSpanId', None, None, ), # 4 - (5, TType.STRING, 'operationName', 'UTF8', None, ), # 5 - (6, TType.LIST, 'references', (TType.STRUCT, (SpanRef, SpanRef.thrift_spec), False), None, ), # 6 - (7, TType.I32, 'flags', None, None, ), # 7 - (8, TType.I64, 'startTime', None, None, ), # 8 - (9, TType.I64, 'duration', None, None, ), # 9 - (10, TType.LIST, 'tags', (TType.STRUCT, (Tag, Tag.thrift_spec), False), None, ), # 10 - (11, TType.LIST, 'logs', (TType.STRUCT, (Log, Log.thrift_spec), False), None, ), # 11 - ) - - def __init__(self, traceIdLow=None, traceIdHigh=None, spanId=None, parentSpanId=None, operationName=None, references=None, flags=None, startTime=None, duration=None, tags=None, logs=None,): - self.traceIdLow = traceIdLow - self.traceIdHigh = traceIdHigh - self.spanId = spanId - self.parentSpanId = parentSpanId - self.operationName = operationName - self.references = references - self.flags = flags - self.startTime = startTime - self.duration = duration - self.tags = tags - self.logs = logs - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.I64: - self.traceIdLow = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 2: - if ftype == TType.I64: - self.traceIdHigh = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 3: - if ftype == TType.I64: - self.spanId = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 4: - if ftype == TType.I64: - self.parentSpanId = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 5: - if ftype == TType.STRING: - self.operationName = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() - else: - iprot.skip(ftype) - elif fid == 6: - if ftype == TType.LIST: - self.references = [] - (_etype10, _size7) = iprot.readListBegin() - for _i11 in range(_size7): - _elem12 = SpanRef() - _elem12.read(iprot) - self.references.append(_elem12) - iprot.readListEnd() - else: - iprot.skip(ftype) - elif fid == 7: - if ftype == TType.I32: - self.flags = iprot.readI32() - else: - iprot.skip(ftype) - elif fid == 8: - if ftype == TType.I64: - self.startTime = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 9: - if ftype == TType.I64: - self.duration = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 10: - if ftype == TType.LIST: - self.tags = [] - (_etype16, _size13) = iprot.readListBegin() - for _i17 in range(_size13): - _elem18 = Tag() - _elem18.read(iprot) - self.tags.append(_elem18) - iprot.readListEnd() - else: - iprot.skip(ftype) - elif fid == 11: - if ftype == TType.LIST: - self.logs = [] - (_etype22, _size19) = iprot.readListBegin() - for _i23 in range(_size19): - _elem24 = Log() - _elem24.read(iprot) - self.logs.append(_elem24) - iprot.readListEnd() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('Span') - if self.traceIdLow is not None: - oprot.writeFieldBegin('traceIdLow', TType.I64, 1) - oprot.writeI64(self.traceIdLow) - oprot.writeFieldEnd() - if self.traceIdHigh is not None: - oprot.writeFieldBegin('traceIdHigh', TType.I64, 2) - oprot.writeI64(self.traceIdHigh) - oprot.writeFieldEnd() - if self.spanId is not None: - oprot.writeFieldBegin('spanId', TType.I64, 3) - oprot.writeI64(self.spanId) - oprot.writeFieldEnd() - if self.parentSpanId is not None: - oprot.writeFieldBegin('parentSpanId', TType.I64, 4) - oprot.writeI64(self.parentSpanId) - oprot.writeFieldEnd() - if self.operationName is not None: - oprot.writeFieldBegin('operationName', TType.STRING, 5) - oprot.writeString(self.operationName.encode('utf-8') if sys.version_info[0] == 2 else self.operationName) - oprot.writeFieldEnd() - if self.references is not None: - oprot.writeFieldBegin('references', TType.LIST, 6) - oprot.writeListBegin(TType.STRUCT, len(self.references)) - for iter25 in self.references: - iter25.write(oprot) - oprot.writeListEnd() - oprot.writeFieldEnd() - if self.flags is not None: - oprot.writeFieldBegin('flags', TType.I32, 7) - oprot.writeI32(self.flags) - oprot.writeFieldEnd() - if self.startTime is not None: - oprot.writeFieldBegin('startTime', TType.I64, 8) - oprot.writeI64(self.startTime) - oprot.writeFieldEnd() - if self.duration is not None: - oprot.writeFieldBegin('duration', TType.I64, 9) - oprot.writeI64(self.duration) - oprot.writeFieldEnd() - if self.tags is not None: - oprot.writeFieldBegin('tags', TType.LIST, 10) - oprot.writeListBegin(TType.STRUCT, len(self.tags)) - for iter26 in self.tags: - iter26.write(oprot) - oprot.writeListEnd() - oprot.writeFieldEnd() - if self.logs is not None: - oprot.writeFieldBegin('logs', TType.LIST, 11) - oprot.writeListBegin(TType.STRUCT, len(self.logs)) - for iter27 in self.logs: - iter27.write(oprot) - oprot.writeListEnd() - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - if self.traceIdLow is None: - raise TProtocolException(message='Required field traceIdLow is unset!') - if self.traceIdHigh is None: - raise TProtocolException(message='Required field traceIdHigh is unset!') - if self.spanId is None: - raise TProtocolException(message='Required field spanId is unset!') - if self.parentSpanId is None: - raise TProtocolException(message='Required field parentSpanId is unset!') - if self.operationName is None: - raise TProtocolException(message='Required field operationName is unset!') - if self.flags is None: - raise TProtocolException(message='Required field flags is unset!') - if self.startTime is None: - raise TProtocolException(message='Required field startTime is unset!') - if self.duration is None: - raise TProtocolException(message='Required field duration is unset!') - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - - -class Process(object): - """ - Attributes: - - serviceName - - tags - """ - - thrift_spec = ( - None, # 0 - (1, TType.STRING, 'serviceName', 'UTF8', None, ), # 1 - (2, TType.LIST, 'tags', (TType.STRUCT, (Tag, Tag.thrift_spec), False), None, ), # 2 - ) - - def __init__(self, serviceName=None, tags=None,): - self.serviceName = serviceName - self.tags = tags - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.STRING: - self.serviceName = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() - else: - iprot.skip(ftype) - elif fid == 2: - if ftype == TType.LIST: - self.tags = [] - (_etype31, _size28) = iprot.readListBegin() - for _i32 in range(_size28): - _elem33 = Tag() - _elem33.read(iprot) - self.tags.append(_elem33) - iprot.readListEnd() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('Process') - if self.serviceName is not None: - oprot.writeFieldBegin('serviceName', TType.STRING, 1) - oprot.writeString(self.serviceName.encode('utf-8') if sys.version_info[0] == 2 else self.serviceName) - oprot.writeFieldEnd() - if self.tags is not None: - oprot.writeFieldBegin('tags', TType.LIST, 2) - oprot.writeListBegin(TType.STRUCT, len(self.tags)) - for iter34 in self.tags: - iter34.write(oprot) - oprot.writeListEnd() - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - if self.serviceName is None: - raise TProtocolException(message='Required field serviceName is unset!') - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - - -class Batch(object): - """ - Attributes: - - process - - spans - """ - - thrift_spec = ( - None, # 0 - (1, TType.STRUCT, 'process', (Process, Process.thrift_spec), None, ), # 1 - (2, TType.LIST, 'spans', (TType.STRUCT, (Span, Span.thrift_spec), False), None, ), # 2 - ) - - def __init__(self, process=None, spans=None,): - self.process = process - self.spans = spans - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.STRUCT: - self.process = Process() - self.process.read(iprot) - else: - iprot.skip(ftype) - elif fid == 2: - if ftype == TType.LIST: - self.spans = [] - (_etype38, _size35) = iprot.readListBegin() - for _i39 in range(_size35): - _elem40 = Span() - _elem40.read(iprot) - self.spans.append(_elem40) - iprot.readListEnd() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('Batch') - if self.process is not None: - oprot.writeFieldBegin('process', TType.STRUCT, 1) - self.process.write(oprot) - oprot.writeFieldEnd() - if self.spans is not None: - oprot.writeFieldBegin('spans', TType.LIST, 2) - oprot.writeListBegin(TType.STRUCT, len(self.spans)) - for iter41 in self.spans: - iter41.write(oprot) - oprot.writeListEnd() - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - if self.process is None: - raise TProtocolException(message='Required field process is unset!') - if self.spans is None: - raise TProtocolException(message='Required field spans is unset!') - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - - -class BatchSubmitResponse(object): - """ - Attributes: - - ok - """ - - thrift_spec = ( - None, # 0 - (1, TType.BOOL, 'ok', None, None, ), # 1 - ) - - def __init__(self, ok=None,): - self.ok = ok - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.BOOL: - self.ok = iprot.readBool() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('BatchSubmitResponse') - if self.ok is not None: - oprot.writeFieldBegin('ok', TType.BOOL, 1) - oprot.writeBool(self.ok) - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - if self.ok is None: - raise TProtocolException(message='Required field ok is unset!') - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ZipkinCollector-remote b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ZipkinCollector-remote deleted file mode 100644 index 2b59c3275d..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ZipkinCollector-remote +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python -# -# Autogenerated by Thrift Compiler (0.10.0) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py -# - -import sys -import pprint -if sys.version_info[0] > 2: - from urllib.parse import urlparse -else: - from urlparse import urlparse -from thrift.transport import TTransport, TSocket, TSSLSocket, THttpClient -from thrift.protocol.TBinaryProtocol import TBinaryProtocol - -from zipkincore import ZipkinCollector -from zipkincore.ttypes import * - -if len(sys.argv) <= 1 or sys.argv[1] == '--help': - print('') - print('Usage: ' + sys.argv[0] + ' [-h host[:port]] [-u url] [-f[ramed]] [-s[sl]] [-novalidate] [-ca_certs certs] [-keyfile keyfile] [-certfile certfile] function [arg1 [arg2...]]') - print('') - print('Functions:') - print(' submitZipkinBatch( spans)') - print('') - sys.exit(0) - -pp = pprint.PrettyPrinter(indent=2) -host = 'localhost' -port = 9090 -uri = '' -framed = False -ssl = False -validate = True -ca_certs = None -keyfile = None -certfile = None -http = False -argi = 1 - -if sys.argv[argi] == '-h': - parts = sys.argv[argi + 1].split(':') - host = parts[0] - if len(parts) > 1: - port = int(parts[1]) - argi += 2 - -if sys.argv[argi] == '-u': - url = urlparse(sys.argv[argi + 1]) - parts = url[1].split(':') - host = parts[0] - if len(parts) > 1: - port = int(parts[1]) - else: - port = 80 - uri = url[2] - if url[4]: - uri += '?%s' % url[4] - http = True - argi += 2 - -if sys.argv[argi] == '-f' or sys.argv[argi] == '-framed': - framed = True - argi += 1 - -if sys.argv[argi] == '-s' or sys.argv[argi] == '-ssl': - ssl = True - argi += 1 - -if sys.argv[argi] == '-novalidate': - validate = False - argi += 1 - -if sys.argv[argi] == '-ca_certs': - ca_certs = sys.argv[argi+1] - argi += 2 - -if sys.argv[argi] == '-keyfile': - keyfile = sys.argv[argi+1] - argi += 2 - -if sys.argv[argi] == '-certfile': - certfile = sys.argv[argi+1] - argi += 2 - -cmd = sys.argv[argi] -args = sys.argv[argi + 1:] - -if http: - transport = THttpClient.THttpClient(host, port, uri) -else: - if ssl: - socket = TSSLSocket.TSSLSocket(host, port, validate=validate, ca_certs=ca_certs, keyfile=keyfile, certfile=certfile) - else: - socket = TSocket.TSocket(host, port) - if framed: - transport = TTransport.TFramedTransport(socket) - else: - transport = TTransport.TBufferedTransport(socket) -protocol = TBinaryProtocol(transport) -client = ZipkinCollector.Client(protocol) -transport.open() - -if cmd == 'submitZipkinBatch': - if len(args) != 1: - print('submitZipkinBatch requires 1 args') - sys.exit(1) - pp.pprint(client.submitZipkinBatch(eval(args[0]),)) - -else: - print('Unrecognized method %s' % cmd) - sys.exit(1) - -transport.close() diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ZipkinCollector.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ZipkinCollector.py deleted file mode 100644 index 6167a8e9f1..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ZipkinCollector.py +++ /dev/null @@ -1,243 +0,0 @@ -# -# Autogenerated by Thrift Compiler (0.10.0) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py -# - -from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException -from thrift.protocol.TProtocol import TProtocolException -import sys -import logging -from .ttypes import * -from thrift.Thrift import TProcessor -from thrift.transport import TTransport - - -class Iface(object): - def submitZipkinBatch(self, spans): - """ - Parameters: - - spans - """ - pass - - -class Client(Iface): - def __init__(self, iprot, oprot=None): - self._iprot = self._oprot = iprot - if oprot is not None: - self._oprot = oprot - self._seqid = 0 - - def submitZipkinBatch(self, spans): - """ - Parameters: - - spans - """ - self.send_submitZipkinBatch(spans) - return self.recv_submitZipkinBatch() - - def send_submitZipkinBatch(self, spans): - self._oprot.writeMessageBegin('submitZipkinBatch', TMessageType.CALL, self._seqid) - args = submitZipkinBatch_args() - args.spans = spans - args.write(self._oprot) - self._oprot.writeMessageEnd() - self._oprot.trans.flush() - - def recv_submitZipkinBatch(self): - iprot = self._iprot - (fname, mtype, rseqid) = iprot.readMessageBegin() - if mtype == TMessageType.EXCEPTION: - x = TApplicationException() - x.read(iprot) - iprot.readMessageEnd() - raise x - result = submitZipkinBatch_result() - result.read(iprot) - iprot.readMessageEnd() - if result.success is not None: - return result.success - raise TApplicationException(TApplicationException.MISSING_RESULT, "submitZipkinBatch failed: unknown result") - - -class Processor(Iface, TProcessor): - def __init__(self, handler): - self._handler = handler - self._processMap = {} - self._processMap["submitZipkinBatch"] = Processor.process_submitZipkinBatch - - def process(self, iprot, oprot): - (name, type, seqid) = iprot.readMessageBegin() - if name not in self._processMap: - iprot.skip(TType.STRUCT) - iprot.readMessageEnd() - x = TApplicationException(TApplicationException.UNKNOWN_METHOD, 'Unknown function %s' % (name)) - oprot.writeMessageBegin(name, TMessageType.EXCEPTION, seqid) - x.write(oprot) - oprot.writeMessageEnd() - oprot.trans.flush() - return - else: - self._processMap[name](self, seqid, iprot, oprot) - return True - - def process_submitZipkinBatch(self, seqid, iprot, oprot): - args = submitZipkinBatch_args() - args.read(iprot) - iprot.readMessageEnd() - result = submitZipkinBatch_result() - try: - result.success = self._handler.submitZipkinBatch(args.spans) - msg_type = TMessageType.REPLY - except (TTransport.TTransportException, KeyboardInterrupt, SystemExit): - raise - except Exception as ex: - msg_type = TMessageType.EXCEPTION - logging.exception(ex) - result = TApplicationException(TApplicationException.INTERNAL_ERROR, 'Internal error') - oprot.writeMessageBegin("submitZipkinBatch", msg_type, seqid) - result.write(oprot) - oprot.writeMessageEnd() - oprot.trans.flush() - -# HELPER FUNCTIONS AND STRUCTURES - - -class submitZipkinBatch_args(object): - """ - Attributes: - - spans - """ - - thrift_spec = ( - None, # 0 - (1, TType.LIST, 'spans', (TType.STRUCT, (Span, Span.thrift_spec), False), None, ), # 1 - ) - - def __init__(self, spans=None,): - self.spans = spans - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.LIST: - self.spans = [] - (_etype17, _size14) = iprot.readListBegin() - for _i18 in range(_size14): - _elem19 = Span() - _elem19.read(iprot) - self.spans.append(_elem19) - iprot.readListEnd() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('submitZipkinBatch_args') - if self.spans is not None: - oprot.writeFieldBegin('spans', TType.LIST, 1) - oprot.writeListBegin(TType.STRUCT, len(self.spans)) - for iter20 in self.spans: - iter20.write(oprot) - oprot.writeListEnd() - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - - -class submitZipkinBatch_result(object): - """ - Attributes: - - success - """ - - thrift_spec = ( - (0, TType.LIST, 'success', (TType.STRUCT, (Response, Response.thrift_spec), False), None, ), # 0 - ) - - def __init__(self, success=None,): - self.success = success - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 0: - if ftype == TType.LIST: - self.success = [] - (_etype24, _size21) = iprot.readListBegin() - for _i25 in range(_size21): - _elem26 = Response() - _elem26.read(iprot) - self.success.append(_elem26) - iprot.readListEnd() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('submitZipkinBatch_result') - if self.success is not None: - oprot.writeFieldBegin('success', TType.LIST, 0) - oprot.writeListBegin(TType.STRUCT, len(self.success)) - for iter27 in self.success: - iter27.write(oprot) - oprot.writeListEnd() - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/__init__.py deleted file mode 100644 index 90e4f9d9c7..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ['ttypes', 'constants', 'ZipkinCollector'] diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/constants.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/constants.py deleted file mode 100644 index d66961b02b..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/constants.py +++ /dev/null @@ -1,28 +0,0 @@ -# -# Autogenerated by Thrift Compiler (0.10.0) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py -# - -from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException -from thrift.protocol.TProtocol import TProtocolException -import sys -from .ttypes import * -CLIENT_SEND = "cs" -CLIENT_RECV = "cr" -SERVER_SEND = "ss" -SERVER_RECV = "sr" -MESSAGE_SEND = "ms" -MESSAGE_RECV = "mr" -WIRE_SEND = "ws" -WIRE_RECV = "wr" -CLIENT_SEND_FRAGMENT = "csf" -CLIENT_RECV_FRAGMENT = "crf" -SERVER_SEND_FRAGMENT = "ssf" -SERVER_RECV_FRAGMENT = "srf" -LOCAL_COMPONENT = "lc" -CLIENT_ADDR = "ca" -SERVER_ADDR = "sa" -MESSAGE_ADDR = "ma" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ttypes.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ttypes.py deleted file mode 100644 index a31db167ee..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen/zipkincore/ttypes.py +++ /dev/null @@ -1,647 +0,0 @@ -# -# Autogenerated by Thrift Compiler (0.10.0) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py -# - -from thrift.Thrift import TType, TMessageType, TFrozenDict, TException, TApplicationException -from thrift.protocol.TProtocol import TProtocolException -import sys - -from thrift.transport import TTransport - - -class AnnotationType(object): - BOOL = 0 - BYTES = 1 - I16 = 2 - I32 = 3 - I64 = 4 - DOUBLE = 5 - STRING = 6 - - _VALUES_TO_NAMES = { - 0: "BOOL", - 1: "BYTES", - 2: "I16", - 3: "I32", - 4: "I64", - 5: "DOUBLE", - 6: "STRING", - } - - _NAMES_TO_VALUES = { - "BOOL": 0, - "BYTES": 1, - "I16": 2, - "I32": 3, - "I64": 4, - "DOUBLE": 5, - "STRING": 6, - } - - -class Endpoint(object): - """ - Indicates the network context of a service recording an annotation with two - exceptions. - - When a BinaryAnnotation, and key is CLIENT_ADDR or SERVER_ADDR, - the endpoint indicates the source or destination of an RPC. This exception - allows zipkin to display network context of uninstrumented services, or - clients such as web browsers. - - Attributes: - - ipv4: IPv4 host address packed into 4 bytes. - - Ex for the ip 1.2.3.4, it would be (1 << 24) | (2 << 16) | (3 << 8) | 4 - - port: IPv4 port - - Note: this is to be treated as an unsigned integer, so watch for negatives. - - Conventionally, when the port isn't known, port = 0. - - service_name: Service name in lowercase, such as "memcache" or "zipkin-web" - - Conventionally, when the service name isn't known, service_name = "unknown". - - ipv6: IPv6 host address packed into 16 bytes. Ex Inet6Address.getBytes() - """ - - thrift_spec = ( - None, # 0 - (1, TType.I32, 'ipv4', None, None, ), # 1 - (2, TType.I16, 'port', None, None, ), # 2 - (3, TType.STRING, 'service_name', 'UTF8', None, ), # 3 - (4, TType.STRING, 'ipv6', 'BINARY', None, ), # 4 - ) - - def __init__(self, ipv4=None, port=None, service_name=None, ipv6=None,): - self.ipv4 = ipv4 - self.port = port - self.service_name = service_name - self.ipv6 = ipv6 - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.I32: - self.ipv4 = iprot.readI32() - else: - iprot.skip(ftype) - elif fid == 2: - if ftype == TType.I16: - self.port = iprot.readI16() - else: - iprot.skip(ftype) - elif fid == 3: - if ftype == TType.STRING: - self.service_name = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() - else: - iprot.skip(ftype) - elif fid == 4: - if ftype == TType.STRING: - self.ipv6 = iprot.readBinary() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('Endpoint') - if self.ipv4 is not None: - oprot.writeFieldBegin('ipv4', TType.I32, 1) - oprot.writeI32(self.ipv4) - oprot.writeFieldEnd() - if self.port is not None: - oprot.writeFieldBegin('port', TType.I16, 2) - oprot.writeI16(self.port) - oprot.writeFieldEnd() - if self.service_name is not None: - oprot.writeFieldBegin('service_name', TType.STRING, 3) - oprot.writeString(self.service_name.encode('utf-8') if sys.version_info[0] == 2 else self.service_name) - oprot.writeFieldEnd() - if self.ipv6 is not None: - oprot.writeFieldBegin('ipv6', TType.STRING, 4) - oprot.writeBinary(self.ipv6) - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - - -class Annotation(object): - """ - An annotation is similar to a log statement. It includes a host field which - allows these events to be attributed properly, and also aggregatable. - - Attributes: - - timestamp: Microseconds from epoch. - - This value should use the most precise value possible. For example, - gettimeofday or syncing nanoTime against a tick of currentTimeMillis. - - value - - host: Always the host that recorded the event. By specifying the host you allow - rollup of all events (such as client requests to a service) by IP address. - """ - - thrift_spec = ( - None, # 0 - (1, TType.I64, 'timestamp', None, None, ), # 1 - (2, TType.STRING, 'value', 'UTF8', None, ), # 2 - (3, TType.STRUCT, 'host', (Endpoint, Endpoint.thrift_spec), None, ), # 3 - ) - - def __init__(self, timestamp=None, value=None, host=None,): - self.timestamp = timestamp - self.value = value - self.host = host - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.I64: - self.timestamp = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 2: - if ftype == TType.STRING: - self.value = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() - else: - iprot.skip(ftype) - elif fid == 3: - if ftype == TType.STRUCT: - self.host = Endpoint() - self.host.read(iprot) - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('Annotation') - if self.timestamp is not None: - oprot.writeFieldBegin('timestamp', TType.I64, 1) - oprot.writeI64(self.timestamp) - oprot.writeFieldEnd() - if self.value is not None: - oprot.writeFieldBegin('value', TType.STRING, 2) - oprot.writeString(self.value.encode('utf-8') if sys.version_info[0] == 2 else self.value) - oprot.writeFieldEnd() - if self.host is not None: - oprot.writeFieldBegin('host', TType.STRUCT, 3) - self.host.write(oprot) - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - - -class BinaryAnnotation(object): - """ - Binary annotations are tags applied to a Span to give it context. For - example, a binary annotation of "http.uri" could the path to a resource in a - RPC call. - - Binary annotations of type STRING are always queryable, though more a - historical implementation detail than a structural concern. - - Binary annotations can repeat, and vary on the host. Similar to Annotation, - the host indicates who logged the event. This allows you to tell the - difference between the client and server side of the same key. For example, - the key "http.uri" might be different on the client and server side due to - rewriting, like "/api/v1/myresource" vs "/myresource. Via the host field, - you can see the different points of view, which often help in debugging. - - Attributes: - - key - - value - - annotation_type - - host: The host that recorded tag, which allows you to differentiate between - multiple tags with the same key. There are two exceptions to this. - - When the key is CLIENT_ADDR or SERVER_ADDR, host indicates the source or - destination of an RPC. This exception allows zipkin to display network - context of uninstrumented services, or clients such as web browsers. - """ - - thrift_spec = ( - None, # 0 - (1, TType.STRING, 'key', 'UTF8', None, ), # 1 - (2, TType.STRING, 'value', 'BINARY', None, ), # 2 - (3, TType.I32, 'annotation_type', None, None, ), # 3 - (4, TType.STRUCT, 'host', (Endpoint, Endpoint.thrift_spec), None, ), # 4 - ) - - def __init__(self, key=None, value=None, annotation_type=None, host=None,): - self.key = key - self.value = value - self.annotation_type = annotation_type - self.host = host - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.STRING: - self.key = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() - else: - iprot.skip(ftype) - elif fid == 2: - if ftype == TType.STRING: - self.value = iprot.readBinary() - else: - iprot.skip(ftype) - elif fid == 3: - if ftype == TType.I32: - self.annotation_type = iprot.readI32() - else: - iprot.skip(ftype) - elif fid == 4: - if ftype == TType.STRUCT: - self.host = Endpoint() - self.host.read(iprot) - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('BinaryAnnotation') - if self.key is not None: - oprot.writeFieldBegin('key', TType.STRING, 1) - oprot.writeString(self.key.encode('utf-8') if sys.version_info[0] == 2 else self.key) - oprot.writeFieldEnd() - if self.value is not None: - oprot.writeFieldBegin('value', TType.STRING, 2) - oprot.writeBinary(self.value) - oprot.writeFieldEnd() - if self.annotation_type is not None: - oprot.writeFieldBegin('annotation_type', TType.I32, 3) - oprot.writeI32(self.annotation_type) - oprot.writeFieldEnd() - if self.host is not None: - oprot.writeFieldBegin('host', TType.STRUCT, 4) - self.host.write(oprot) - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - - -class Span(object): - """ - A trace is a series of spans (often RPC calls) which form a latency tree. - - The root span is where trace_id = id and parent_id = Nil. The root span is - usually the longest interval in the trace, starting with a SERVER_RECV - annotation and ending with a SERVER_SEND. - - Attributes: - - trace_id - - name: Span name in lowercase, rpc method for example - - Conventionally, when the span name isn't known, name = "unknown". - - id - - parent_id - - annotations - - binary_annotations - - debug - - timestamp: Microseconds from epoch of the creation of this span. - - This value should be set directly by instrumentation, using the most - precise value possible. For example, gettimeofday or syncing nanoTime - against a tick of currentTimeMillis. - - For compatibility with instrumentation that precede this field, collectors - or span stores can derive this via Annotation.timestamp. - For example, SERVER_RECV.timestamp or CLIENT_SEND.timestamp. - - This field is optional for compatibility with old data: first-party span - stores are expected to support this at time of introduction. - - duration: Measurement of duration in microseconds, used to support queries. - - This value should be set directly, where possible. Doing so encourages - precise measurement decoupled from problems of clocks, such as skew or NTP - updates causing time to move backwards. - - For compatibility with instrumentation that precede this field, collectors - or span stores can derive this by subtracting Annotation.timestamp. - For example, SERVER_SEND.timestamp - SERVER_RECV.timestamp. - - If this field is persisted as unset, zipkin will continue to work, except - duration query support will be implementation-specific. Similarly, setting - this field non-atomically is implementation-specific. - - This field is i64 vs i32 to support spans longer than 35 minutes. - - trace_id_high: Optional unique 8-byte additional identifier for a trace. If non zero, this - means the trace uses 128 bit traceIds instead of 64 bit. - """ - - thrift_spec = ( - None, # 0 - (1, TType.I64, 'trace_id', None, None, ), # 1 - None, # 2 - (3, TType.STRING, 'name', 'UTF8', None, ), # 3 - (4, TType.I64, 'id', None, None, ), # 4 - (5, TType.I64, 'parent_id', None, None, ), # 5 - (6, TType.LIST, 'annotations', (TType.STRUCT, (Annotation, Annotation.thrift_spec), False), None, ), # 6 - None, # 7 - (8, TType.LIST, 'binary_annotations', (TType.STRUCT, (BinaryAnnotation, BinaryAnnotation.thrift_spec), False), None, ), # 8 - (9, TType.BOOL, 'debug', None, False, ), # 9 - (10, TType.I64, 'timestamp', None, None, ), # 10 - (11, TType.I64, 'duration', None, None, ), # 11 - (12, TType.I64, 'trace_id_high', None, None, ), # 12 - ) - - def __init__(self, trace_id=None, name=None, id=None, parent_id=None, annotations=None, binary_annotations=None, debug=thrift_spec[9][4], timestamp=None, duration=None, trace_id_high=None,): - self.trace_id = trace_id - self.name = name - self.id = id - self.parent_id = parent_id - self.annotations = annotations - self.binary_annotations = binary_annotations - self.debug = debug - self.timestamp = timestamp - self.duration = duration - self.trace_id_high = trace_id_high - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.I64: - self.trace_id = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 3: - if ftype == TType.STRING: - self.name = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() - else: - iprot.skip(ftype) - elif fid == 4: - if ftype == TType.I64: - self.id = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 5: - if ftype == TType.I64: - self.parent_id = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 6: - if ftype == TType.LIST: - self.annotations = [] - (_etype3, _size0) = iprot.readListBegin() - for _i4 in range(_size0): - _elem5 = Annotation() - _elem5.read(iprot) - self.annotations.append(_elem5) - iprot.readListEnd() - else: - iprot.skip(ftype) - elif fid == 8: - if ftype == TType.LIST: - self.binary_annotations = [] - (_etype9, _size6) = iprot.readListBegin() - for _i10 in range(_size6): - _elem11 = BinaryAnnotation() - _elem11.read(iprot) - self.binary_annotations.append(_elem11) - iprot.readListEnd() - else: - iprot.skip(ftype) - elif fid == 9: - if ftype == TType.BOOL: - self.debug = iprot.readBool() - else: - iprot.skip(ftype) - elif fid == 10: - if ftype == TType.I64: - self.timestamp = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 11: - if ftype == TType.I64: - self.duration = iprot.readI64() - else: - iprot.skip(ftype) - elif fid == 12: - if ftype == TType.I64: - self.trace_id_high = iprot.readI64() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('Span') - if self.trace_id is not None: - oprot.writeFieldBegin('trace_id', TType.I64, 1) - oprot.writeI64(self.trace_id) - oprot.writeFieldEnd() - if self.name is not None: - oprot.writeFieldBegin('name', TType.STRING, 3) - oprot.writeString(self.name.encode('utf-8') if sys.version_info[0] == 2 else self.name) - oprot.writeFieldEnd() - if self.id is not None: - oprot.writeFieldBegin('id', TType.I64, 4) - oprot.writeI64(self.id) - oprot.writeFieldEnd() - if self.parent_id is not None: - oprot.writeFieldBegin('parent_id', TType.I64, 5) - oprot.writeI64(self.parent_id) - oprot.writeFieldEnd() - if self.annotations is not None: - oprot.writeFieldBegin('annotations', TType.LIST, 6) - oprot.writeListBegin(TType.STRUCT, len(self.annotations)) - for iter12 in self.annotations: - iter12.write(oprot) - oprot.writeListEnd() - oprot.writeFieldEnd() - if self.binary_annotations is not None: - oprot.writeFieldBegin('binary_annotations', TType.LIST, 8) - oprot.writeListBegin(TType.STRUCT, len(self.binary_annotations)) - for iter13 in self.binary_annotations: - iter13.write(oprot) - oprot.writeListEnd() - oprot.writeFieldEnd() - if self.debug is not None: - oprot.writeFieldBegin('debug', TType.BOOL, 9) - oprot.writeBool(self.debug) - oprot.writeFieldEnd() - if self.timestamp is not None: - oprot.writeFieldBegin('timestamp', TType.I64, 10) - oprot.writeI64(self.timestamp) - oprot.writeFieldEnd() - if self.duration is not None: - oprot.writeFieldBegin('duration', TType.I64, 11) - oprot.writeI64(self.duration) - oprot.writeFieldEnd() - if self.trace_id_high is not None: - oprot.writeFieldBegin('trace_id_high', TType.I64, 12) - oprot.writeI64(self.trace_id_high) - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - - -class Response(object): - """ - Attributes: - - ok - """ - - thrift_spec = ( - None, # 0 - (1, TType.BOOL, 'ok', None, None, ), # 1 - ) - - def __init__(self, ok=None,): - self.ok = ok - - def read(self, iprot): - if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: - iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec)) - return - iprot.readStructBegin() - while True: - (fname, ftype, fid) = iprot.readFieldBegin() - if ftype == TType.STOP: - break - if fid == 1: - if ftype == TType.BOOL: - self.ok = iprot.readBool() - else: - iprot.skip(ftype) - else: - iprot.skip(ftype) - iprot.readFieldEnd() - iprot.readStructEnd() - - def write(self, oprot): - if oprot._fast_encode is not None and self.thrift_spec is not None: - oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec))) - return - oprot.writeStructBegin('Response') - if self.ok is not None: - oprot.writeFieldBegin('ok', TType.BOOL, 1) - oprot.writeBool(self.ok) - oprot.writeFieldEnd() - oprot.writeFieldStop() - oprot.writeStructEnd() - - def validate(self): - if self.ok is None: - raise TProtocolException(message='Required field ok is unset!') - return - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.items()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/py.typed b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py deleted file mode 100644 index 0e3a411de6..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/send.py +++ /dev/null @@ -1,146 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import base64 -import logging -import math -import socket - -from thrift.protocol import TBinaryProtocol, TCompactProtocol -from thrift.transport import THttpClient, TTransport - -from opentelemetry.exporter.jaeger.thrift.gen.agent import Agent as agent -from opentelemetry.exporter.jaeger.thrift.gen.jaeger import Collector as jaeger - -UDP_PACKET_MAX_LENGTH = 65000 - - -logger = logging.getLogger(__name__) - - -class AgentClientUDP: - """Implement a UDP client to agent. - - Args: - host_name: The host name of the Jaeger server. - port: The port of the Jaeger server. - max_packet_size: Maximum size of UDP packet. - client: Class for creating new client objects for agencies. - split_oversized_batches: Re-emit oversized batches in smaller chunks. - """ - - def __init__( - self, - host_name, - port, - max_packet_size=UDP_PACKET_MAX_LENGTH, - client=agent.Client, - split_oversized_batches=False, - ): - self.address = (host_name, port) - self.max_packet_size = max_packet_size - self.buffer = TTransport.TMemoryBuffer() - self.client = client( - iprot=TCompactProtocol.TCompactProtocol(trans=self.buffer) - ) - self.split_oversized_batches = split_oversized_batches - - def emit(self, batch: jaeger.Batch): - """ - Args: - batch: Object to emit Jaeger spans. - """ - - # pylint: disable=protected-access - self.client._seqid = 0 - # truncate and reset the position of BytesIO object - self.buffer._buffer.truncate(0) - self.buffer._buffer.seek(0) - self.client.emitBatch(batch) - buff = self.buffer.getvalue() - if len(buff) > self.max_packet_size: - if self.split_oversized_batches and len(batch.spans) > 1: - packets = math.ceil(len(buff) / self.max_packet_size) - div = math.ceil(len(batch.spans) / packets) - for packet in range(packets): - start = packet * div - end = (packet + 1) * div - if start < len(batch.spans): - self.emit( - jaeger.Batch( - process=batch.process, - spans=batch.spans[start:end], - ) - ) - else: - logger.warning( - "Data exceeds the max UDP packet size; size %r, max %r", - len(buff), - self.max_packet_size, - ) - return - - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_socket: - udp_socket.sendto(buff, self.address) - - -class Collector: - """Submits collected spans to Jaeger collector in jaeger.thrift - format over binary thrift protocol. This is recommend option in cases where - it is not feasible to deploy Jaeger Agent next to the application, - for example, when the application code is running as AWS Lambda function. - In these scenarios the Jaeger Clients can be configured to submit spans directly - to the Collectors over HTTP/HTTPS. - - Args: - thrift_url: Endpoint used to send spans - directly to Collector the over HTTP. - auth: Auth tuple that contains username and password for Basic Auth. - timeout_in_millis: timeout for THttpClient. - """ - - def __init__(self, thrift_url="", auth=None, timeout_in_millis=None): - self.thrift_url = thrift_url - self.auth = auth - self.http_transport = THttpClient.THttpClient( - uri_or_host=self.thrift_url - ) - if timeout_in_millis is not None: - self.http_transport.setTimeout(timeout_in_millis) - - self.protocol = TBinaryProtocol.TBinaryProtocol(self.http_transport) - - # set basic auth header - if auth is not None: - auth_header = f"{auth[0]}:{auth[1]}" - decoded = base64.b64encode(auth_header.encode()).decode("ascii") - basic_auth = {"Authorization": f"Basic {decoded}"} - self.http_transport.setCustomHeaders(basic_auth) - - def submit(self, batch: jaeger.Batch): - """Submits batches to Thrift HTTP Server through Binary Protocol. - - Args: - batch: Object to emit Jaeger spans. - """ - batch.write(self.protocol) - self.http_transport.flush() - code = self.http_transport.code - msg = self.http_transport.message - if code >= 300 or code < 200: - logger.error( - "Traces cannot be uploaded; HTTP status code: %s, message: %s", - code, - msg, - ) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py deleted file mode 100644 index 922833fcca..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/translate/__init__.py +++ /dev/null @@ -1,320 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import abc -from typing import Optional, Sequence - -from opentelemetry.exporter.jaeger.thrift.gen.jaeger import ( - Collector as TCollector, -) -from opentelemetry.sdk.trace import ReadableSpan, StatusCode -from opentelemetry.trace import SpanKind -from opentelemetry.util import types - -OTLP_JAEGER_SPAN_KIND = { - SpanKind.CLIENT: "client", - SpanKind.SERVER: "server", - SpanKind.CONSUMER: "consumer", - SpanKind.PRODUCER: "producer", - SpanKind.INTERNAL: "internal", -} - -NAME_KEY = "otel.library.name" -VERSION_KEY = "otel.library.version" -_SCOPE_NAME_KEY = "otel.scope.name" -_SCOPE_VERSION_KEY = "otel.scope.version" - - -def _nsec_to_usec_round(nsec: int) -> int: - """Round nanoseconds to microseconds""" - return (nsec + 500) // 10**3 - - -def _convert_int_to_i64(val): - """Convert integer to signed int64 (i64)""" - if val > 0x7FFFFFFFFFFFFFFF: - val -= 0x10000000000000000 - return val - - -def _append_dropped(tags, key, val): - if val: - tags.append(_get_long_tag(key, val)) - - -class Translator(abc.ABC): - def __init__(self, max_tag_value_length: Optional[int] = None): - self._max_tag_value_length = max_tag_value_length - - @abc.abstractmethod - def _translate_span(self, span): - """Translates span to jaeger format. - - Args: - span: span to translate - """ - - @abc.abstractmethod - def _extract_tags(self, span): - """Extracts tags from span and returns list of jaeger Tags. - - Args: - span: span to extract tags - """ - - @abc.abstractmethod - def _extract_refs(self, span): - """Extracts references from span and returns list of jaeger SpanRefs. - - Args: - span: span to extract references - """ - - @abc.abstractmethod - def _extract_logs(self, span): - """Extracts logs from span and returns list of jaeger Logs. - - Args: - span: span to extract logs - """ - - -class Translate: - def __init__(self, spans): - self.spans = spans - - def _translate(self, translator: Translator): - translated_spans = [] - for span in self.spans: - # pylint: disable=protected-access - translated_span = translator._translate_span(span) - translated_spans.append(translated_span) - return translated_spans - - -def _get_string_tag(key, value: str) -> TCollector.Tag: - """Returns jaeger string tag.""" - return TCollector.Tag(key=key, vStr=value, vType=TCollector.TagType.STRING) - - -def _get_bool_tag(key: str, value: bool) -> TCollector.Tag: - """Returns jaeger boolean tag.""" - return TCollector.Tag(key=key, vBool=value, vType=TCollector.TagType.BOOL) - - -def _get_long_tag(key: str, value: int) -> TCollector.Tag: - """Returns jaeger long tag.""" - return TCollector.Tag(key=key, vLong=value, vType=TCollector.TagType.LONG) - - -def _get_double_tag(key: str, value: float) -> TCollector.Tag: - """Returns jaeger double tag.""" - return TCollector.Tag( - key=key, vDouble=value, vType=TCollector.TagType.DOUBLE - ) - - -def _get_trace_id_low(trace_id): - return _convert_int_to_i64(trace_id & 0xFFFFFFFFFFFFFFFF) - - -def _get_trace_id_high(trace_id): - return _convert_int_to_i64((trace_id >> 64) & 0xFFFFFFFFFFFFFFFF) - - -def _translate_attribute( - key: str, value: types.AttributeValue, max_length: Optional[int] -) -> Optional[TCollector.Tag]: - """Convert the attributes to jaeger tags.""" - if isinstance(value, bool): - return _get_bool_tag(key, value) - if isinstance(value, str): - if max_length is not None: - value = value[:max_length] - return _get_string_tag(key, value) - if isinstance(value, int): - return _get_long_tag(key, value) - if isinstance(value, float): - return _get_double_tag(key, value) - if isinstance(value, tuple): - value = str(value) - if max_length is not None: - value = value[:max_length] - return _get_string_tag(key, value) - return None - - -class ThriftTranslator(Translator): - def _translate_span(self, span: ReadableSpan) -> TCollector.Span: - ctx = span.get_span_context() - trace_id = ctx.trace_id - span_id = ctx.span_id - - start_time_us = _nsec_to_usec_round(span.start_time) - duration_us = _nsec_to_usec_round(span.end_time - span.start_time) - - parent_id = span.parent.span_id if span.parent else 0 - - tags = self._extract_tags(span) - refs = self._extract_refs(span) - logs = self._extract_logs(span) - - flags = int(ctx.trace_flags) - - jaeger_span = TCollector.Span( - traceIdHigh=_get_trace_id_high(trace_id), - traceIdLow=_get_trace_id_low(trace_id), - spanId=_convert_int_to_i64(span_id), - operationName=span.name, - startTime=start_time_us, - duration=duration_us, - tags=tags, - logs=logs, - references=refs, - flags=flags, - parentSpanId=_convert_int_to_i64(parent_id), - ) - return jaeger_span - - def _extract_tags(self, span: ReadableSpan) -> Sequence[TCollector.Tag]: - # pylint: disable=too-many-branches - translated = [] - if span.attributes: - for key, value in span.attributes.items(): - tag = _translate_attribute( - key, value, self._max_tag_value_length - ) - if tag: - translated.append(tag) - if span.resource.attributes: - for key, value in span.resource.attributes.items(): - tag = _translate_attribute( - key, value, self._max_tag_value_length - ) - if tag: - translated.append(tag) - - status = span.status - if status.status_code is not StatusCode.UNSET: - translated.append( - _get_string_tag("otel.status_code", status.status_code.name) - ) - if status.description is not None: - translated.append( - _get_string_tag( - "otel.status_description", status.description - ) - ) - - translated.append( - _get_string_tag("span.kind", OTLP_JAEGER_SPAN_KIND[span.kind]) - ) - - # Instrumentation info tags - if span.instrumentation_scope: - name = _get_string_tag(NAME_KEY, span.instrumentation_scope.name) - version = _get_string_tag( - VERSION_KEY, span.instrumentation_scope.version - ) - scope_name = _get_string_tag( - _SCOPE_NAME_KEY, span.instrumentation_scope.name - ) - scope_version = _get_string_tag( - _SCOPE_VERSION_KEY, span.instrumentation_scope.version - ) - - translated.extend([name, version]) - translated.extend([scope_name, scope_version]) - - # Make sure to add "error" tag if span status is not OK - if not span.status.is_ok: - translated.append(_get_bool_tag("error", True)) - - _append_dropped( - translated, - "otel.dropped_attributes_count", - span.dropped_attributes, - ) - _append_dropped( - translated, "otel.dropped_events_count", span.dropped_events - ) - _append_dropped( - translated, "otel.dropped_links_count", span.dropped_links - ) - - return translated - - def _extract_refs( - self, span: ReadableSpan - ) -> Optional[Sequence[TCollector.SpanRef]]: - if not span.links: - return None - - refs = [] - for link in span.links: - trace_id = link.context.trace_id - span_id = link.context.span_id - refs.append( - TCollector.SpanRef( - refType=TCollector.SpanRefType.FOLLOWS_FROM, - traceIdHigh=_get_trace_id_high(trace_id), - traceIdLow=_get_trace_id_low(trace_id), - spanId=_convert_int_to_i64(span_id), - ) - ) - return refs - - def _extract_logs( - self, span: ReadableSpan - ) -> Optional[Sequence[TCollector.Log]]: - """Returns jaeger logs if events exists, otherwise None. - - Args: - span: span to extract logs - """ - if not span.events: - return None - - logs = [] - for event in span.events: - fields = [] - for key, value in event.attributes.items(): - tag = _translate_attribute( - key, value, self._max_tag_value_length - ) - if tag: - fields.append(tag) - - _append_dropped( - fields, - "otel.dropped_attributes_count", - event.attributes.dropped, - ) - - fields.append( - TCollector.Tag( - key="message", - vType=TCollector.TagType.STRING, - vStr=event.name, - ) - ) - - event_timestamp_us = _nsec_to_usec_round(event.timestamp) - logs.append( - TCollector.Log( - timestamp=int(event_timestamp_us), fields=fields - ) - ) - - return logs diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py deleted file mode 100644 index f2d56fac64..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2019, OpenCensus Authors -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "1.22.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/tests/__init__.py b/exporter/opentelemetry-exporter-jaeger-thrift/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/tests/certs/cred.cert b/exporter/opentelemetry-exporter-jaeger-thrift/tests/certs/cred.cert deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py deleted file mode 100644 index 8d9f2a4247..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/tests/test_jaeger_exporter_thrift.py +++ /dev/null @@ -1,689 +0,0 @@ -# Copyright 2018, OpenCensus Authors -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -from unittest import mock - -# pylint:disable=no-name-in-module -# pylint:disable=import-error -import opentelemetry.exporter.jaeger.thrift as jaeger_exporter -from opentelemetry import trace as trace_api -from opentelemetry.exporter.jaeger.thrift.gen.jaeger import ttypes as jaeger -from opentelemetry.exporter.jaeger.thrift.translate import ( - ThriftTranslator, - Translate, -) -from opentelemetry.sdk import trace -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_JAEGER_AGENT_HOST, - OTEL_EXPORTER_JAEGER_AGENT_PORT, - OTEL_EXPORTER_JAEGER_ENDPOINT, - OTEL_EXPORTER_JAEGER_PASSWORD, - OTEL_EXPORTER_JAEGER_TIMEOUT, - OTEL_EXPORTER_JAEGER_USER, -) -from opentelemetry.sdk.resources import SERVICE_NAME -from opentelemetry.sdk.trace import Resource, TracerProvider -from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.test.globals_test import TraceGlobalsTest -from opentelemetry.test.spantestutil import ( - get_span_with_dropped_attributes_events_links, -) -from opentelemetry.trace import SpanKind -from opentelemetry.trace.status import Status, StatusCode - - -def _translate_spans_with_dropped_attributes(): - span = get_span_with_dropped_attributes_events_links() - translate = Translate([span]) - - # pylint: disable=protected-access - return translate._translate(ThriftTranslator(max_tag_value_length=5)) - - -class TestJaegerExporter(TraceGlobalsTest, unittest.TestCase): - def setUp(self): - # create and save span to be used in tests - self.context = trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x00000000DEADBEF0, - is_remote=False, - ) - - self._test_span = trace._Span( - "test_span", - context=self.context, - # Use a fixed version because a longer/shorter version number - # might break tests that care about packet size. - resource=Resource.create({"telemetry.sdk.version": "0.0.0.dev0"}), - ) - self._test_span.start(start_time=1) - self._test_span.end(end_time=3) - # pylint: disable=protected-access - - def test_constructor_default(self): - # pylint: disable=protected-access - """Test the default values assigned by constructor.""" - service_name = "my-service-name" - agent_host_name = "localhost" - agent_port = 6831 - - trace_api.set_tracer_provider( - TracerProvider( - resource=Resource.create({SERVICE_NAME: "my-service-name"}) - ) - ) - - exporter = jaeger_exporter.JaegerExporter() - self.assertEqual(exporter.service_name, service_name) - self.assertEqual(exporter.agent_host_name, agent_host_name) - self.assertEqual(exporter.agent_port, agent_port) - self.assertEqual(exporter.collector_endpoint, None) - self.assertEqual(exporter.username, None) - self.assertEqual(exporter.password, None) - self.assertTrue(exporter._collector_http_client is None) - self.assertTrue(exporter._agent_client is not None) - self.assertIsNone(exporter._max_tag_value_length) - - def test_constructor_explicit(self): - # pylint: disable=protected-access - """Test the constructor passing all the options.""" - service = "my-opentelemetry-jaeger" - collector_endpoint = "https://opentelemetry.io:15875" - - agent_port = 14268 - agent_host_name = "opentelemetry.io" - - username = "username" - password = "password" - auth = (username, password) - trace_api.set_tracer_provider( - TracerProvider( - resource=Resource.create( - {SERVICE_NAME: "my-opentelemetry-jaeger"} - ) - ) - ) - - exporter = jaeger_exporter.JaegerExporter( - agent_host_name=agent_host_name, - agent_port=agent_port, - collector_endpoint=collector_endpoint, - username=username, - password=password, - max_tag_value_length=42, - ) - self.assertEqual(exporter.service_name, service) - self.assertEqual(exporter.agent_host_name, agent_host_name) - self.assertEqual(exporter.agent_port, agent_port) - self.assertTrue(exporter._collector_http_client is not None) - self.assertEqual(exporter._collector_http_client.auth, auth) - # property should not construct new object - collector = exporter._collector_http_client - self.assertEqual(exporter._collector_http_client, collector) - # property should construct new object - exporter._collector = None - exporter.username = None - exporter.password = None - self.assertNotEqual(exporter._collector_http_client, collector) - self.assertTrue(exporter._collector_http_client.auth is None) - self.assertEqual(exporter._max_tag_value_length, 42) - - def test_constructor_by_environment_variables(self): - # pylint: disable=protected-access - """Test the constructor using Environment Variables.""" - service = "my-opentelemetry-jaeger" - - agent_host_name = "opentelemetry.io" - agent_port = "6831" - - collector_endpoint = "https://opentelemetry.io:15875" - - username = "username" - password = "password" - auth = (username, password) - - environ_patcher = mock.patch.dict( - "os.environ", - { - OTEL_EXPORTER_JAEGER_AGENT_HOST: agent_host_name, - OTEL_EXPORTER_JAEGER_AGENT_PORT: agent_port, - OTEL_EXPORTER_JAEGER_ENDPOINT: collector_endpoint, - OTEL_EXPORTER_JAEGER_USER: username, - OTEL_EXPORTER_JAEGER_PASSWORD: password, - OTEL_EXPORTER_JAEGER_TIMEOUT: "20", - }, - ) - - trace_api.set_tracer_provider( - TracerProvider( - resource=Resource.create( - {SERVICE_NAME: "my-opentelemetry-jaeger"} - ) - ) - ) - - environ_patcher.start() - exporter = jaeger_exporter.JaegerExporter() - self.assertEqual(exporter.service_name, service) - self.assertEqual(exporter.agent_host_name, agent_host_name) - self.assertEqual(exporter.agent_port, int(agent_port)) - self.assertEqual(exporter._timeout, 20) - self.assertTrue(exporter._collector_http_client is not None) - self.assertEqual(exporter.collector_endpoint, collector_endpoint) - self.assertEqual(exporter._collector_http_client.auth, auth) - # property should not construct new object - collector = exporter._collector_http_client - self.assertEqual(exporter._collector_http_client, collector) - # property should construct new object - exporter._collector = None - exporter.username = None - exporter.password = None - self.assertNotEqual(exporter._collector_http_client, collector) - self.assertTrue(exporter._collector_http_client.auth is None) - environ_patcher.stop() - - def test_constructor_with_no_traceprovider_resource(self): - - """Test the constructor when there is no resource attached to trace_provider""" - - exporter = jaeger_exporter.JaegerExporter() - - self.assertEqual(exporter.service_name, "unknown_service") - - def test_nsec_to_usec_round(self): - # pylint: disable=protected-access - nsec_to_usec_round = jaeger_exporter.translate._nsec_to_usec_round - - self.assertEqual(nsec_to_usec_round(5000), 5) - self.assertEqual(nsec_to_usec_round(5499), 5) - self.assertEqual(nsec_to_usec_round(5500), 6) - - def test_all_otlp_span_kinds_are_mapped(self): - for kind in SpanKind: - self.assertIn( - kind, jaeger_exporter.translate.OTLP_JAEGER_SPAN_KIND - ) - - # pylint: disable=too-many-locals - def test_translate_to_jaeger(self): - # pylint: disable=invalid-name - self.maxDiff = None - - span_names = ("test1", "test2", "test3") - trace_id = 0x6E0C63257DE34C926F9EFCD03927272E - trace_id_high = 0x6E0C63257DE34C92 - trace_id_low = 0x6F9EFCD03927272E - span_id = 0x34BF92DEEFC58C92 - parent_id = 0x1111111111111111 - other_id = 0x2222222222222222 - - base_time = 683647322 * 10**9 # in ns - start_times = ( - base_time, - base_time + 150 * 10**6, - base_time + 300 * 10**6, - ) - durations = (50 * 10**6, 100 * 10**6, 200 * 10**6) - end_times = ( - start_times[0] + durations[0], - start_times[1] + durations[1], - start_times[2] + durations[2], - ) - - span_context = trace_api.SpanContext( - trace_id, span_id, is_remote=False - ) - parent_span_context = trace_api.SpanContext( - trace_id, parent_id, is_remote=False - ) - other_context = trace_api.SpanContext( - trace_id, other_id, is_remote=False - ) - - event_attributes = { - "annotation_bool": True, - "annotation_string": "annotation_test", - "key_float": 0.3, - } - - event_timestamp = base_time + 50 * 10**6 - event = trace.Event( - name="event0", - timestamp=event_timestamp, - attributes=event_attributes, - ) - - link_attributes = {"key_bool": True} - - link = trace_api.Link( - context=other_context, attributes=link_attributes - ) - - default_tags = [ - jaeger.Tag( - key="span.kind", - vType=jaeger.TagType.STRING, - vStr="internal", - ), - ] - - otel_spans = [ - trace._Span( - name=span_names[0], - context=span_context, - parent=parent_span_context, - events=(event,), - links=(link,), - kind=trace_api.SpanKind.CLIENT, - resource=Resource( - attributes={"key_resource": "some_resource"} - ), - ), - trace._Span( - name=span_names[1], - context=parent_span_context, - parent=None, - resource=Resource({}), - ), - trace._Span( - name=span_names[2], - context=other_context, - parent=None, - resource=Resource({}), - instrumentation_scope=InstrumentationScope( - name="name", version="version" - ), - ), - ] - - otel_spans[0].start(start_time=start_times[0]) - # added here to preserve order - otel_spans[0].set_attribute("key_bool", False) - otel_spans[0].set_attribute("key_string", "hello_world") - otel_spans[0].set_attribute("key_float", 111.22) - otel_spans[0].set_attribute("key_tuple", ("tuple_element",)) - otel_spans[0].set_status( - Status(StatusCode.ERROR, "Example description") - ) - otel_spans[0].end(end_time=end_times[0]) - - otel_spans[1].start(start_time=start_times[1]) - otel_spans[1].end(end_time=end_times[1]) - - otel_spans[2].start(start_time=start_times[2]) - otel_spans[2].set_status(Status(StatusCode.OK)) - otel_spans[2].end(end_time=end_times[2]) - - translate = Translate(otel_spans) - # pylint: disable=protected-access - spans = translate._translate(ThriftTranslator()) - - expected_spans = [ - jaeger.Span( - operationName=span_names[0], - traceIdHigh=trace_id_high, - traceIdLow=trace_id_low, - spanId=span_id, - parentSpanId=parent_id, - startTime=start_times[0] // 10**3, - duration=durations[0] // 10**3, - flags=0, - tags=[ - jaeger.Tag( - key="key_bool", vType=jaeger.TagType.BOOL, vBool=False - ), - jaeger.Tag( - key="key_string", - vType=jaeger.TagType.STRING, - vStr="hello_world", - ), - jaeger.Tag( - key="key_float", - vType=jaeger.TagType.DOUBLE, - vDouble=111.22, - ), - jaeger.Tag( - key="key_tuple", - vType=jaeger.TagType.STRING, - vStr="('tuple_element',)", - ), - jaeger.Tag( - key="key_resource", - vType=jaeger.TagType.STRING, - vStr="some_resource", - ), - jaeger.Tag( - key="otel.status_code", - vType=jaeger.TagType.STRING, - vStr="ERROR", - ), - jaeger.Tag( - key="otel.status_description", - vType=jaeger.TagType.STRING, - vStr="Example description", - ), - jaeger.Tag( - key="span.kind", - vType=jaeger.TagType.STRING, - vStr="client", - ), - jaeger.Tag( - key="error", vType=jaeger.TagType.BOOL, vBool=True - ), - ], - references=[ - jaeger.SpanRef( - refType=jaeger.SpanRefType.FOLLOWS_FROM, - traceIdHigh=trace_id_high, - traceIdLow=trace_id_low, - spanId=other_id, - ) - ], - logs=[ - jaeger.Log( - timestamp=event_timestamp // 10**3, - fields=[ - jaeger.Tag( - key="annotation_bool", - vType=jaeger.TagType.BOOL, - vBool=True, - ), - jaeger.Tag( - key="annotation_string", - vType=jaeger.TagType.STRING, - vStr="annotation_test", - ), - jaeger.Tag( - key="key_float", - vType=jaeger.TagType.DOUBLE, - vDouble=0.3, - ), - jaeger.Tag( - key="message", - vType=jaeger.TagType.STRING, - vStr="event0", - ), - ], - ) - ], - ), - jaeger.Span( - operationName=span_names[1], - traceIdHigh=trace_id_high, - traceIdLow=trace_id_low, - spanId=parent_id, - parentSpanId=0, - startTime=start_times[1] // 10**3, - duration=durations[1] // 10**3, - flags=0, - tags=default_tags, - ), - jaeger.Span( - operationName=span_names[2], - traceIdHigh=trace_id_high, - traceIdLow=trace_id_low, - spanId=other_id, - parentSpanId=0, - startTime=start_times[2] // 10**3, - duration=durations[2] // 10**3, - flags=0, - tags=[ - jaeger.Tag( - key="otel.status_code", - vType=jaeger.TagType.STRING, - vStr="OK", - ), - jaeger.Tag( - key="span.kind", - vType=jaeger.TagType.STRING, - vStr="internal", - ), - jaeger.Tag( - key=jaeger_exporter.translate.NAME_KEY, - vType=jaeger.TagType.STRING, - vStr="name", - ), - jaeger.Tag( - key=jaeger_exporter.translate.VERSION_KEY, - vType=jaeger.TagType.STRING, - vStr="version", - ), - jaeger.Tag( - key=jaeger_exporter.translate._SCOPE_NAME_KEY, - vType=jaeger.TagType.STRING, - vStr="name", - ), - jaeger.Tag( - key=jaeger_exporter.translate._SCOPE_VERSION_KEY, - vType=jaeger.TagType.STRING, - vStr="version", - ), - ], - ), - ] - - # events are complicated to compare because order of fields - # (attributes) in otel is not important but in jeager it is - self.assertCountEqual( - spans[0].logs[0].fields, expected_spans[0].logs[0].fields - ) - # get rid of fields to be able to compare the whole spans - spans[0].logs[0].fields = None - expected_spans[0].logs[0].fields = None - - self.assertEqual(spans, expected_spans) - - def test_export(self): - - """Test that agent and/or collector are invoked""" - - trace_api.set_tracer_provider( - TracerProvider( - resource=Resource.create({SERVICE_NAME: "text_export"}) - ) - ) - - exporter = jaeger_exporter.JaegerExporter( - agent_host_name="localhost", agent_port=6318 - ) - - # just agent is configured now - agent_client_mock = mock.Mock(spec=jaeger_exporter.AgentClientUDP) - # pylint: disable=protected-access - exporter._agent_client = agent_client_mock - - exporter.export((self._test_span,)) - self.assertEqual(agent_client_mock.emit.call_count, 1) - - # add also a collector and test that both are called - collector_mock = mock.Mock(spec=jaeger_exporter.Collector) - # pylint: disable=protected-access - exporter._collector = collector_mock - - exporter.export((self._test_span,)) - self.assertEqual(agent_client_mock.emit.call_count, 1) - self.assertEqual(collector_mock.submit.call_count, 1) - - def test_export_span_service_name(self): - trace_api.set_tracer_provider( - TracerProvider( - resource=Resource.create({SERVICE_NAME: "text_export"}) - ) - ) - exporter = jaeger_exporter.JaegerExporter( - agent_host_name="localhost", agent_port=6318 - ) - agent_client_mock = mock.Mock(spec=jaeger_exporter.AgentClientUDP) - exporter._agent_client = agent_client_mock - resource = Resource.create({SERVICE_NAME: "test"}) - span = trace._Span( - "test_span", context=self.context, resource=resource - ) - span.start() - span.end() - exporter.export([span]) - self.assertEqual(exporter.service_name, "test") - - def test_agent_client(self): - agent_client = jaeger_exporter.AgentClientUDP( - host_name="localhost", port=6354 - ) - - translate = Translate([self._test_span]) - # pylint: disable=protected-access - spans = translate._translate(ThriftTranslator()) - - batch = jaeger.Batch( - spans=spans, - process=jaeger.Process(serviceName="xxx"), - ) - - agent_client.emit(batch) - - def test_max_tag_value_length(self): - span = trace._Span( - name="span", - resource=Resource( - attributes={ - "key_resource": "some_resource some_resource some_more_resource" - } - ), - context=trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x00000000DEADBEF0, - is_remote=False, - ), - ) - - span.start() - span.set_attribute("key_bool", False) - span.set_attribute("key_string", "hello_world hello_world hello_world") - span.set_attribute("key_float", 111.22) - span.set_attribute("key_int", 1100) - span.set_attribute("key_tuple", ("tuple_element", "tuple_element2")) - span.end() - - translate = Translate([span]) - - # does not truncate by default - # pylint: disable=protected-access - spans = translate._translate(ThriftTranslator()) - tags_by_keys = { - tag.key: tag.vStr - for tag in spans[0].tags - if tag.vType == jaeger.TagType.STRING - } - self.assertEqual( - "hello_world hello_world hello_world", tags_by_keys["key_string"] - ) - self.assertEqual( - "('tuple_element', 'tuple_element2')", tags_by_keys["key_tuple"] - ) - self.assertEqual( - "some_resource some_resource some_more_resource", - tags_by_keys["key_resource"], - ) - - # truncates when max_tag_value_length is passed - # pylint: disable=protected-access - spans = translate._translate(ThriftTranslator(max_tag_value_length=5)) - tags_by_keys = { - tag.key: tag.vStr - for tag in spans[0].tags - if tag.vType == jaeger.TagType.STRING - } - self.assertEqual("hello", tags_by_keys["key_string"]) - self.assertEqual("('tup", tags_by_keys["key_tuple"]) - self.assertEqual("some_", tags_by_keys["key_resource"]) - - def test_dropped_span_attributes(self): - spans = _translate_spans_with_dropped_attributes() - tags_by_keys = { - tag.key: tag.vLong - for tag in spans[0].tags - if tag.vType == jaeger.TagType.LONG - } - - self.assertEqual(1, tags_by_keys["otel.dropped_links_count"]) - self.assertEqual(2, tags_by_keys["otel.dropped_attributes_count"]) - self.assertEqual(3, tags_by_keys["otel.dropped_events_count"]) - - def test_dropped_event_attributes(self): - spans = _translate_spans_with_dropped_attributes() - tags_by_keys = { - tag.key: tag.vLong - for tag in spans[0].logs[0].fields - if tag.vType == jaeger.TagType.LONG - } - self.assertEqual( - 2, - tags_by_keys["otel.dropped_attributes_count"], - ) - - def test_agent_client_split(self): - agent_client = jaeger_exporter.AgentClientUDP( - host_name="localhost", - port=6354, - max_packet_size=250, - split_oversized_batches=True, - ) - - translator = jaeger_exporter.Translate((self._test_span,)) - small_batch = jaeger.Batch( - # pylint: disable=protected-access - spans=translator._translate(ThriftTranslator()), - process=jaeger.Process(serviceName="xxx"), - ) - - with unittest.mock.patch( - "socket.socket.sendto", autospec=True - ) as fake_sendto: - agent_client.emit(small_batch) - self.assertEqual(fake_sendto.call_count, 1) - - translator = jaeger_exporter.Translate([self._test_span] * 2) - large_batch = jaeger.Batch( - # pylint: disable=protected-access - spans=translator._translate(ThriftTranslator()), - process=jaeger.Process(serviceName="xxx"), - ) - - with unittest.mock.patch( - "socket.socket.sendto", autospec=True - ) as fake_sendto: - agent_client.emit(large_batch) - self.assertEqual(fake_sendto.call_count, 2) - - def test_agent_client_dont_send_empty_spans(self): - agent_client = jaeger_exporter.AgentClientUDP( - host_name="localhost", - port=6354, - max_packet_size=415, - split_oversized_batches=True, - ) - - translator = jaeger_exporter.Translate([self._test_span] * 4) - large_batch = jaeger.Batch( - # pylint: disable=protected-access - spans=translator._translate(ThriftTranslator()), - process=jaeger.Process(serviceName="xxx"), - ) - - with unittest.mock.patch( - "socket.socket.sendto", autospec=True - ) as fake_sendto: - agent_client.emit(large_batch) - self.assertEqual(fake_sendto.call_count, 4) diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/thrift/agent.thrift b/exporter/opentelemetry-exporter-jaeger-thrift/thrift/agent.thrift deleted file mode 100644 index 5d3c9201b6..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/thrift/agent.thrift +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) 2016 Uber Technologies, 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. - -include "jaeger.thrift" -include "zipkincore.thrift" - -namespace cpp jaegertracing.agent.thrift -namespace java io.jaegertracing.agent.thrift -namespace php Jaeger.Thrift.Agent -namespace netcore Jaeger.Thrift.Agent -namespace lua jaeger.thrift.agent - -service Agent { - oneway void emitZipkinBatch(1: list spans) - oneway void emitBatch(1: jaeger.Batch batch) -} diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/thrift/jaeger.thrift b/exporter/opentelemetry-exporter-jaeger-thrift/thrift/jaeger.thrift deleted file mode 100644 index ae9fcaa014..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/thrift/jaeger.thrift +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) 2016 Uber Technologies, 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. - -namespace cpp jaegertracing.thrift -namespace java io.jaegertracing.thriftjava -namespace php Jaeger.Thrift -namespace netcore Jaeger.Thrift -namespace lua jaeger.thrift - -# TagType denotes the type of a Tag's value. -enum TagType { STRING, DOUBLE, BOOL, LONG, BINARY } - -# Tag is a basic strongly typed key/value pair. It has been flattened to reduce the use of pointers in golang -struct Tag { - 1: required string key - 2: required TagType vType - 3: optional string vStr - 4: optional double vDouble - 5: optional bool vBool - 6: optional i64 vLong - 7: optional binary vBinary -} - -# Log is a timed even with an arbitrary set of tags. -struct Log { - 1: required i64 timestamp - 2: required list fields -} - -enum SpanRefType { CHILD_OF, FOLLOWS_FROM } - -# SpanRef describes causal relationship of the current span to another span (e.g. 'child-of') -struct SpanRef { - 1: required SpanRefType refType - 2: required i64 traceIdLow - 3: required i64 traceIdHigh - 4: required i64 spanId -} - -# Span represents a named unit of work performed by a service. -struct Span { - 1: required i64 traceIdLow # the least significant 64 bits of a traceID - 2: required i64 traceIdHigh # the most significant 64 bits of a traceID; 0 when only 64bit IDs are used - 3: required i64 spanId # unique span id (only unique within a given trace) - 4: required i64 parentSpanId # since nearly all spans will have parents spans, CHILD_OF refs do not have to be explicit - 5: required string operationName - 6: optional list references # causal references to other spans - 7: required i32 flags # a bit field used to propagate sampling decisions. 1 signifies a SAMPLED span, 2 signifies a DEBUG span. - 8: required i64 startTime - 9: required i64 duration - 10: optional list tags - 11: optional list logs -} - -# Process describes the traced process/service that emits spans. -struct Process { - 1: required string serviceName - 2: optional list tags -} - -# Batch is a collection of spans reported out of process. -struct Batch { - 1: required Process process - 2: required list spans -} - -# BatchSubmitResponse is the response on submitting a batch. -struct BatchSubmitResponse { - 1: required bool ok # The Collector's client is expected to only log (or emit a counter) when not ok equals false -} - -service Collector { - list submitBatches(1: list batches) -} diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/thrift/zipkincore.thrift b/exporter/opentelemetry-exporter-jaeger-thrift/thrift/zipkincore.thrift deleted file mode 100644 index 2004ed581b..0000000000 --- a/exporter/opentelemetry-exporter-jaeger-thrift/thrift/zipkincore.thrift +++ /dev/null @@ -1,346 +0,0 @@ -# Copyright 2012 Twitter 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. -namespace cpp twitter.zipkin.thrift -namespace java com.twitter.zipkin.thriftjava -#@namespace scala com.twitter.zipkin.thriftscala -namespace rb Zipkin -namespace php Jaeger.Thrift.Agent.Zipkin -namespace netcore Jaeger.Thrift.Agent.Zipkin -namespace lua jaeger.thrift.agent - - -#************** Annotation.value ************** -/** - * The client sent ("cs") a request to a server. There is only one send per - * span. For example, if there's a transport error, each attempt can be logged - * as a WIRE_SEND annotation. - * - * If chunking is involved, each chunk could be logged as a separate - * CLIENT_SEND_FRAGMENT in the same span. - * - * Annotation.host is not the server. It is the host which logged the send - * event, almost always the client. When logging CLIENT_SEND, instrumentation - * should also log the SERVER_ADDR. - */ -const string CLIENT_SEND = "cs" -/** - * The client received ("cr") a response from a server. There is only one - * receive per span. For example, if duplicate responses were received, each - * can be logged as a WIRE_RECV annotation. - * - * If chunking is involved, each chunk could be logged as a separate - * CLIENT_RECV_FRAGMENT in the same span. - * - * Annotation.host is not the server. It is the host which logged the receive - * event, almost always the client. The actual endpoint of the server is - * recorded separately as SERVER_ADDR when CLIENT_SEND is logged. - */ -const string CLIENT_RECV = "cr" -/** - * The server sent ("ss") a response to a client. There is only one response - * per span. If there's a transport error, each attempt can be logged as a - * WIRE_SEND annotation. - * - * Typically, a trace ends with a server send, so the last timestamp of a trace - * is often the timestamp of the root span's server send. - * - * If chunking is involved, each chunk could be logged as a separate - * SERVER_SEND_FRAGMENT in the same span. - * - * Annotation.host is not the client. It is the host which logged the send - * event, almost always the server. The actual endpoint of the client is - * recorded separately as CLIENT_ADDR when SERVER_RECV is logged. - */ -const string SERVER_SEND = "ss" -/** - * The server received ("sr") a request from a client. There is only one - * request per span. For example, if duplicate responses were received, each - * can be logged as a WIRE_RECV annotation. - * - * Typically, a trace starts with a server receive, so the first timestamp of a - * trace is often the timestamp of the root span's server receive. - * - * If chunking is involved, each chunk could be logged as a separate - * SERVER_RECV_FRAGMENT in the same span. - * - * Annotation.host is not the client. It is the host which logged the receive - * event, almost always the server. When logging SERVER_RECV, instrumentation - * should also log the CLIENT_ADDR. - */ -const string SERVER_RECV = "sr" -/** - * Message send ("ms") is a request to send a message to a destination, usually - * a broker. This may be the only annotation in a messaging span. If WIRE_SEND - * exists in the same span, it follows this moment and clarifies delays sending - * the message, such as batching. - * - * Unlike RPC annotations like CLIENT_SEND, messaging spans never share a span - * ID. For example, "ms" should always be the parent of "mr". - * - * Annotation.host is not the destination, it is the host which logged the send - * event: the producer. When annotating MESSAGE_SEND, instrumentation should - * also tag the MESSAGE_ADDR. - */ -const string MESSAGE_SEND = "ms" -/** - * A consumer received ("mr") a message from a broker. This may be the only - * annotation in a messaging span. If WIRE_RECV exists in the same span, it - * precedes this moment and clarifies any local queuing delay. - * - * Unlike RPC annotations like SERVER_RECV, messaging spans never share a span - * ID. For example, "mr" should always be a child of "ms" unless it is a root - * span. - * - * Annotation.host is not the broker, it is the host which logged the receive - * event: the consumer. When annotating MESSAGE_RECV, instrumentation should - * also tag the MESSAGE_ADDR. - */ -const string MESSAGE_RECV = "mr" -/** - * Optionally logs an attempt to send a message on the wire. Multiple wire send - * events could indicate network retries. A lag between client or server send - * and wire send might indicate queuing or processing delay. - */ -const string WIRE_SEND = "ws" -/** - * Optionally logs an attempt to receive a message from the wire. Multiple wire - * receive events could indicate network retries. A lag between wire receive - * and client or server receive might indicate queuing or processing delay. - */ -const string WIRE_RECV = "wr" -/** - * Optionally logs progress of a (CLIENT_SEND, WIRE_SEND). For example, this - * could be one chunk in a chunked request. - */ -const string CLIENT_SEND_FRAGMENT = "csf" -/** - * Optionally logs progress of a (CLIENT_RECV, WIRE_RECV). For example, this - * could be one chunk in a chunked response. - */ -const string CLIENT_RECV_FRAGMENT = "crf" -/** - * Optionally logs progress of a (SERVER_SEND, WIRE_SEND). For example, this - * could be one chunk in a chunked response. - */ -const string SERVER_SEND_FRAGMENT = "ssf" -/** - * Optionally logs progress of a (SERVER_RECV, WIRE_RECV). For example, this - * could be one chunk in a chunked request. - */ -const string SERVER_RECV_FRAGMENT = "srf" - -#***** BinaryAnnotation.key ****** -/** - * The value of "lc" is the component or namespace of a local span. - * - * BinaryAnnotation.host adds service context needed to support queries. - * - * Local Component("lc") supports three key features: flagging, query by - * service and filtering Span.name by namespace. - * - * While structurally the same, local spans are fundamentally different than - * RPC spans in how they should be interpreted. For example, zipkin v1 tools - * center on RPC latency and service graphs. Root local-spans are neither - * indicative of critical path RPC latency, nor have impact on the shape of a - * service graph. By flagging with "lc", tools can special-case local spans. - * - * Zipkin v1 Spans are unqueryable unless they can be indexed by service name. - * The only path to a service name is by (Binary)?Annotation.host.serviceName. - * By logging "lc", a local span can be queried even if no other annotations - * are logged. - * - * The value of "lc" is the namespace of Span.name. For example, it might be - * "finatra2", for a span named "bootstrap". "lc" allows you to resolves - * conflicts for the same Span.name, for example "finatra/bootstrap" vs - * "finch/bootstrap". Using local component, you'd search for spans named - * "bootstrap" where "lc=finch" - */ -const string LOCAL_COMPONENT = "lc" - -#***** BinaryAnnotation.key where value = [1] and annotation_type = BOOL ****** -/** - * Indicates a client address ("ca") in a span. Most likely, there's only one. - * Multiple addresses are possible when a client changes its ip or port within - * a span. - */ -const string CLIENT_ADDR = "ca" -/** - * Indicates a server address ("sa") in a span. Most likely, there's only one. - * Multiple addresses are possible when a client is redirected, or fails to a - * different server ip or port. - */ -const string SERVER_ADDR = "sa" -/** - * Indicates the remote address of a messaging span, usually the broker. - */ -const string MESSAGE_ADDR = "ma" - -/** - * Indicates the network context of a service recording an annotation with two - * exceptions. - * - * When a BinaryAnnotation, and key is CLIENT_ADDR or SERVER_ADDR, - * the endpoint indicates the source or destination of an RPC. This exception - * allows zipkin to display network context of uninstrumented services, or - * clients such as web browsers. - */ -struct Endpoint { - /** - * IPv4 host address packed into 4 bytes. - * - * Ex for the ip 1.2.3.4, it would be (1 << 24) | (2 << 16) | (3 << 8) | 4 - */ - 1: i32 ipv4 - /** - * IPv4 port - * - * Note: this is to be treated as an unsigned integer, so watch for negatives. - * - * Conventionally, when the port isn't known, port = 0. - */ - 2: i16 port - /** - * Service name in lowercase, such as "memcache" or "zipkin-web" - * - * Conventionally, when the service name isn't known, service_name = "unknown". - */ - 3: string service_name - /** - * IPv6 host address packed into 16 bytes. Ex Inet6Address.getBytes() - */ - 4: optional binary ipv6 -} - -/** - * An annotation is similar to a log statement. It includes a host field which - * allows these events to be attributed properly, and also aggregatable. - */ -struct Annotation { - /** - * Microseconds from epoch. - * - * This value should use the most precise value possible. For example, - * gettimeofday or syncing nanoTime against a tick of currentTimeMillis. - */ - 1: i64 timestamp - 2: string value // what happened at the timestamp? - /** - * Always the host that recorded the event. By specifying the host you allow - * rollup of all events (such as client requests to a service) by IP address. - */ - 3: optional Endpoint host - // don't reuse 4: optional i32 OBSOLETE_duration // how long did the operation take? microseconds -} - -enum AnnotationType { BOOL, BYTES, I16, I32, I64, DOUBLE, STRING } - -/** - * Binary annotations are tags applied to a Span to give it context. For - * example, a binary annotation of "http.uri" could the path to a resource in a - * RPC call. - * - * Binary annotations of type STRING are always queryable, though more a - * historical implementation detail than a structural concern. - * - * Binary annotations can repeat, and vary on the host. Similar to Annotation, - * the host indicates who logged the event. This allows you to tell the - * difference between the client and server side of the same key. For example, - * the key "http.uri" might be different on the client and server side due to - * rewriting, like "/api/v1/myresource" vs "/myresource. Via the host field, - * you can see the different points of view, which often help in debugging. - */ -struct BinaryAnnotation { - 1: string key, - 2: binary value, - 3: AnnotationType annotation_type, - /** - * The host that recorded tag, which allows you to differentiate between - * multiple tags with the same key. There are two exceptions to this. - * - * When the key is CLIENT_ADDR or SERVER_ADDR, host indicates the source or - * destination of an RPC. This exception allows zipkin to display network - * context of uninstrumented services, or clients such as web browsers. - */ - 4: optional Endpoint host -} - -/** - * A trace is a series of spans (often RPC calls) which form a latency tree. - * - * The root span is where trace_id = id and parent_id = Nil. The root span is - * usually the longest interval in the trace, starting with a SERVER_RECV - * annotation and ending with a SERVER_SEND. - */ -struct Span { - 1: i64 trace_id # unique trace id, use for all spans in trace - /** - * Span name in lowercase, rpc method for example - * - * Conventionally, when the span name isn't known, name = "unknown". - */ - 3: string name, - 4: i64 id, # unique span id, only used for this span - 5: optional i64 parent_id, # parent span id - 6: list annotations, # all annotations/events that occurred, sorted by timestamp - 8: list binary_annotations # any binary annotations - 9: optional bool debug = 0 # if true, we DEMAND that this span passes all samplers - /** - * Microseconds from epoch of the creation of this span. - * - * This value should be set directly by instrumentation, using the most - * precise value possible. For example, gettimeofday or syncing nanoTime - * against a tick of currentTimeMillis. - * - * For compatibility with instrumentation that precede this field, collectors - * or span stores can derive this via Annotation.timestamp. - * For example, SERVER_RECV.timestamp or CLIENT_SEND.timestamp. - * - * This field is optional for compatibility with old data: first-party span - * stores are expected to support this at time of introduction. - */ - 10: optional i64 timestamp, - /** - * Measurement of duration in microseconds, used to support queries. - * - * This value should be set directly, where possible. Doing so encourages - * precise measurement decoupled from problems of clocks, such as skew or NTP - * updates causing time to move backwards. - * - * For compatibility with instrumentation that precede this field, collectors - * or span stores can derive this by subtracting Annotation.timestamp. - * For example, SERVER_SEND.timestamp - SERVER_RECV.timestamp. - * - * If this field is persisted as unset, zipkin will continue to work, except - * duration query support will be implementation-specific. Similarly, setting - * this field non-atomically is implementation-specific. - * - * This field is i64 vs i32 to support spans longer than 35 minutes. - */ - 11: optional i64 duration - /** - * Optional unique 8-byte additional identifier for a trace. If non zero, this - * means the trace uses 128 bit traceIds instead of 64 bit. - */ - 12: optional i64 trace_id_high -} - -# define TChannel service - -struct Response { - 1: required bool ok -} - -service ZipkinCollector { - list submitZipkinBatch(1: list spans) -} diff --git a/exporter/opentelemetry-exporter-jaeger/LICENSE b/exporter/opentelemetry-exporter-jaeger/LICENSE deleted file mode 100644 index 1ef7dad2c5..0000000000 --- a/exporter/opentelemetry-exporter-jaeger/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 The OpenTelemetry Authors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/exporter/opentelemetry-exporter-jaeger/README.rst b/exporter/opentelemetry-exporter-jaeger/README.rst deleted file mode 100644 index 9f0cec9250..0000000000 --- a/exporter/opentelemetry-exporter-jaeger/README.rst +++ /dev/null @@ -1,35 +0,0 @@ -OpenTelemetry Jaeger Exporter -============================= - -.. warning:: - Since v1.35, the Jaeger supports OTLP natively. Please use the OTLP exporter instead. - Support for this exporter will end July 2023. - - This package is no longer being tested. - -|pypi| - -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-jaeger.svg - :target: https://pypi.org/project/opentelemetry-exporter-jaeger/ - -This library is provided as a convenience to install all supported Jaeger Exporters. Currently it installs: -* opentelemetry-exporter-jaeger-proto-grpc -* opentelemetry-exporter-jaeger-thrift - -To avoid unnecessary dependencies, users should install the specific package once they've determined their -preferred serialization method. - -Installation ------------- - -:: - - pip install opentelemetry-exporter-jaeger - - -References ----------- - -* `OpenTelemetry Jaeger Exporter `_ -* `Jaeger `_ -* `OpenTelemetry Project `_ diff --git a/exporter/opentelemetry-exporter-jaeger/pyproject.toml b/exporter/opentelemetry-exporter-jaeger/pyproject.toml deleted file mode 100644 index 755e36c51e..0000000000 --- a/exporter/opentelemetry-exporter-jaeger/pyproject.toml +++ /dev/null @@ -1,52 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "opentelemetry-exporter-jaeger" -dynamic = ["version"] -description = "Jaeger Exporters for OpenTelemetry" -readme = "README.rst" -license = "Apache-2.0" -requires-python = ">=3.7" -authors = [ - { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, -] -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Typing :: Typed", -] -dependencies = [ - "opentelemetry-exporter-jaeger-proto-grpc == 1.22.0.dev", - "opentelemetry-exporter-jaeger-thrift == 1.22.0.dev", -] - -[project.optional-dependencies] -test = [] - -[project.entry-points.opentelemetry_traces_exporter] -jaeger = "opentelemetry.exporter.jaeger.proto.grpc:JaegerExporter" - -[project.urls] -Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-jaeger" - -[tool.hatch.version] -path = "src/opentelemetry/exporter/jaeger/version.py" - -[tool.hatch.build.targets.sdist] -include = [ - "/src", - "/tests", -] - -[tool.hatch.build.targets.wheel] -packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/py.typed b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py deleted file mode 100644 index f2d56fac64..0000000000 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2019, OpenCensus Authors -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "1.22.0.dev" diff --git a/exporter/opentelemetry-exporter-jaeger/tests/__init__.py b/exporter/opentelemetry-exporter-jaeger/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py deleted file mode 100644 index 935a297607..0000000000 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -# pylint:disable=no-name-in-module -# pylint:disable=import-error -from opentelemetry.exporter.jaeger import thrift -from opentelemetry.exporter.jaeger.proto import grpc - - -# pylint:disable=no-member -class TestJaegerExporter(unittest.TestCase): - def test_constructors(self): - """Test ensures both exporters can co-exist""" - try: - grpc.JaegerExporter() - thrift.JaegerExporter() - except Exception as exc: # pylint: disable=broad-except - self.assertIsNone(exc) diff --git a/opentelemetry-api/src/opentelemetry/_logs/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/__init__.py index a01d1b14ee..aaf29e5fe6 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/__init__.py @@ -14,8 +14,7 @@ """ The OpenTelemetry logging API describes the classes used to generate logs and events. -The :class:`.LoggerProvider` provides users access to the :class:`.Logger` which in -turn is used to create :class:`.Event` and :class:`.Log` objects. +The :class:`.LoggerProvider` provides users access to the :class:`.Logger`. This module provides abstract (i.e. unimplemented) classes required for logging, and a concrete no-op implementation :class:`.NoOpLogger` that allows applications diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 3696dc61b3..b825ae931c 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -826,11 +826,9 @@ def test_otlp_exporter_conflict(self): ) assert len(logs_context.output) == 1 - @patch.dict(environ, {"OTEL_TRACES_EXPORTER": "jaeger,zipkin"}) + @patch.dict(environ, {"OTEL_TRACES_EXPORTER": "zipkin"}) def test_multiple_exporters(self): - self.assertEqual( - sorted(_get_exporter_names("traces")), ["jaeger", "zipkin"] - ) + self.assertEqual(sorted(_get_exporter_names("traces")), ["zipkin"]) @patch.dict(environ, {"OTEL_TRACES_EXPORTER": "none"}) def test_none_exporters(self): diff --git a/pyproject.toml b/pyproject.toml index 405792011e..01ae2999af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,6 @@ exclude = ''' .venv.*| target.*| .*/build/lib/.*| - exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen| - exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen| exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen| opentelemetry-proto/src/opentelemetry/proto/.*/.*| scripts diff --git a/rationale.md b/rationale.md index 4f1dd3ab82..9c10727fd5 100644 --- a/rationale.md +++ b/rationale.md @@ -28,7 +28,7 @@ Public portions of the SDK (constructors, configuration, end-user interfaces) mu ## Core components -Core components refer to the set of components which are required as per the spec. This includes API, SDK, propagators (B3 and Jaeger) and exporters which are required by the specification. These exporters are OTLP, Jaeger and Zipkin. +Core components refer to the set of components which are required as per the spec. This includes API, SDK, propagators (B3 and Jaeger) and exporters which are required by the specification. These exporters are OTLP, and Zipkin. ## Mature or stable Signals diff --git a/scripts/coverage.sh b/scripts/coverage.sh index df43010ec6..56cbc78dde 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -32,8 +32,6 @@ cov opentelemetry-sdk cov exporter/opentelemetry-exporter-datadog cov instrumentation/opentelemetry-instrumentation-flask cov instrumentation/opentelemetry-instrumentation-requests -cov exporter/opentelemetry-exporter-jaeger-proto-grpc -cov exporter/opentelemetry-exporter-jaeger-thrift cov instrumentation/opentelemetry-instrumentation-opentracing-shim cov util/opentelemetry-util-http cov exporter/opentelemetry-exporter-zipkin diff --git a/scripts/public_symbols_checker.py b/scripts/public_symbols_checker.py index c485c4740e..05b7ad4abb 100644 --- a/scripts/public_symbols_checker.py +++ b/scripts/public_symbols_checker.py @@ -41,7 +41,11 @@ def get_symbols(change_type, diff_lines_getter, prefix): .iter_change_type(change_type) ): - b_file_path = diff_lines.b_blob.path + if diff_lines.b_blob is None: + # This happens if a file has been removed completely. + b_file_path = diff_lines.a_blob.path + else: + b_file_path = diff_lines.b_blob.path b_file_path_obj = Path(b_file_path) if ( diff --git a/tox.ini b/tox.ini index e3f0866f52..86ac70d0c5 100644 --- a/tox.ini +++ b/tox.ini @@ -213,9 +213,6 @@ commands_pre = python -m pip install -e {toxinidir}/tests/opentelemetry-test-utils[test] python -m pip install -e {toxinidir}/shim/opentelemetry-opentracing-shim[test] python -m pip install -e {toxinidir}/shim/opentelemetry-opencensus-shim[test] - python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-proto-grpc[test] - python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger-thrift[test] - python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-jaeger[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-opencensus[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc[test] From 3dfe2249cc4a203bf24578483b192fec7266596b Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Wed, 6 Dec 2023 19:16:43 -0500 Subject: [PATCH 1505/1517] Python 3.12 compat.: LogRecord now has a taskName attribute (#3557) --- CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/_logs/_internal/__init__.py | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23c58969a3..894dccc990 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3536](https://github.com/open-telemetry/opentelemetry-python/pull/3536)) - Fix OTLPExporterMixin shutdown timeout period ([#3524](https://github.com/open-telemetry/opentelemetry-python/pull/3524)) +- Handle `taskName` `logrecord` attribute + ([#3557](https://github.com/open-telemetry/opentelemetry-python/pull/3557)) ## Version 1.21.0/0.42b0 (2023-11-01) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 0707c00bff..cfa4d6cfa9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -428,6 +428,7 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: "stack_info", "thread", "threadName", + "taskName", ) ) From 3cc459e445998d4886f761da5e95bd06e06617f2 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 14 Dec 2023 14:03:10 -0600 Subject: [PATCH 1506/1517] Update contrib SHA (#3585) Fixes #3584 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b929cd05a6..6cd7b49883 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 21b26b39c62b27b116978813d47b9b7d104b54be + CONTRIB_REPO_SHA: b6c11054b9f3237eab143d536b072ff3df8dc970 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when From b7e1888e266b7df2d8f0f0377cc0597ca34b0808 Mon Sep 17 00:00:00 2001 From: Clemens Korner Date: Fri, 15 Dec 2023 03:02:38 +0100 Subject: [PATCH 1507/1517] Prometheus exporter sanitize info metric (#3572) * Prometheus Exporter sanitize info metric * Update CHANGELOG.md * Test Prometheus exporter target info sanitize * Prometheus exporter paint it black --------- Co-authored-by: Diego Hurtado --- CHANGELOG.md | 1 + .../exporter/prometheus/__init__.py | 4 +++ .../tests/test_prometheus_exporter.py | 32 +++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 894dccc990..0e29f65d61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Prometheus exporter sanitize info metric ([#3572](https://github.com/open-telemetry/opentelemetry-python/pull/3572)) - Remove Jaeger exporters ([#3554](https://github.com/open-telemetry/opentelemetry-python/pull/3554)) - Log stacktrace on `UNKNOWN` status OTLP export error diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 8d9e18d733..4c90329778 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -378,6 +378,10 @@ def _create_info_metric( self, name: str, description: str, attributes: Dict[str, str] ) -> InfoMetricFamily: """Create an Info Metric Family with list of attributes""" + # sanitize the attribute names according to Prometheus rule + attributes = { + self._sanitize(key): value for key, value in attributes.items() + } info = InfoMetricFamily(name, description, labels=attributes) info.add_metric(labels=list(attributes.keys()), value=attributes) return info diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index a9ab05d01e..db920a5c73 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -389,3 +389,35 @@ def test_target_info_disabled(self): ) self.assertNotIn("os", prometheus_metric.samples[0].labels) self.assertNotIn("histo", prometheus_metric.samples[0].labels) + + def test_target_info_sanitize(self): + metric_reader = PrometheusMetricReader() + provider = MeterProvider( + metric_readers=[metric_reader], + resource=Resource( + { + "system.os": "Unix", + "system.name": "Prometheus Target Sanitize", + } + ), + ) + meter = provider.get_meter("getting-started", "0.1.2") + counter = meter.create_counter("counter") + counter.add(1) + prometheus_metric = list(metric_reader._collector.collect())[0] + + self.assertEqual(type(prometheus_metric), InfoMetricFamily) + self.assertEqual(prometheus_metric.name, "target") + self.assertEqual(prometheus_metric.documentation, "Target metadata") + self.assertTrue(len(prometheus_metric.samples) == 1) + self.assertEqual(prometheus_metric.samples[0].value, 1) + self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) + self.assertTrue("system_os" in prometheus_metric.samples[0].labels) + self.assertEqual( + prometheus_metric.samples[0].labels["system_os"], "Unix" + ) + self.assertTrue("system_name" in prometheus_metric.samples[0].labels) + self.assertEqual( + prometheus_metric.samples[0].labels["system_name"], + "Prometheus Target Sanitize", + ) From 48fdb6389a78d7c357565268a68d1705706f453f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 14 Dec 2023 20:33:34 -0600 Subject: [PATCH 1508/1517] Rename benchmarks branch to gh-pages (#3581) Fixes #3580 --- .github/workflows/benchmarks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 2d7a6efd73..1036cb7540 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -43,7 +43,7 @@ jobs: name: OpenTelemetry Python SDK Benchmarks - Python ${{ env[matrix.python-version ]}} - SDK tool: pytest output-file-path: opentelemetry-sdk/tests/output.json - gh-pages-branch: benchmarks + gh-pages-branch: gh-pages github-token: ${{ secrets.GITHUB_TOKEN }} # Make a commit on `gh-pages` with benchmarks from previous step benchmark-data-dir-path: "benchmarks" From bede4d259aa497bd2d62d1dd249c0b43dc4067a2 Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Fri, 15 Dec 2023 23:54:07 +0100 Subject: [PATCH 1509/1517] Update version to 1.23.0.dev/0.44b0.dev (#3582) * Update version to 1.23.0.dev/0.44b0.dev * Update SHA --------- Co-authored-by: Diego Hurtado --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 5 ++++- eachdist.ini | 4 ++-- exporter/opentelemetry-exporter-opencensus/pyproject.toml | 2 +- .../src/opentelemetry/exporter/opencensus/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-common/pyproject.toml | 2 +- .../src/opentelemetry/exporter/otlp/proto/common/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-grpc/pyproject.toml | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/grpc/version.py | 2 +- .../opentelemetry-exporter-otlp-proto-http/pyproject.toml | 6 +++--- .../src/opentelemetry/exporter/otlp/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-otlp/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/otlp/version.py | 2 +- exporter/opentelemetry-exporter-prometheus/pyproject.toml | 2 +- .../src/opentelemetry/exporter/prometheus/version.py | 2 +- .../src/opentelemetry/exporter/zipkin/json/version.py | 2 +- .../opentelemetry-exporter-zipkin-proto-http/pyproject.toml | 2 +- .../src/opentelemetry/exporter/zipkin/proto/http/version.py | 2 +- exporter/opentelemetry-exporter-zipkin/pyproject.toml | 4 ++-- .../src/opentelemetry/exporter/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/version.py | 2 +- opentelemetry-proto/src/opentelemetry/proto/version.py | 2 +- opentelemetry-sdk/pyproject.toml | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- .../src/opentelemetry/semconv/version.py | 2 +- .../src/opentelemetry/propagators/b3/version.py | 2 +- .../src/opentelemetry/propagators/jaeger/version.py | 2 +- shim/opentelemetry-opencensus-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opencensus/version.py | 2 +- shim/opentelemetry-opentracing-shim/pyproject.toml | 2 +- .../src/opentelemetry/shim/opentracing_shim/version.py | 2 +- tests/opentelemetry-test-utils/pyproject.toml | 4 ++-- .../src/opentelemetry/test/version.py | 2 +- 33 files changed, 45 insertions(+), 42 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6cd7b49883..04309915ab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: b6c11054b9f3237eab143d536b072ff3df8dc970 + CONTRIB_REPO_SHA: 2977f143df1d474735e8bdfecd91d92d534e80dc # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e29f65d61..ed48ae5141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -- Prometheus exporter sanitize info metric ([#3572](https://github.com/open-telemetry/opentelemetry-python/pull/3572)) +## Version 1.22.0/0.43b0 (2023-12-14) + +- Prometheus exporter sanitize info metric + ([#3572](https://github.com/open-telemetry/opentelemetry-python/pull/3572)) - Remove Jaeger exporters ([#3554](https://github.com/open-telemetry/opentelemetry-python/pull/3554)) - Log stacktrace on `UNKNOWN` status OTLP export error diff --git a/eachdist.ini b/eachdist.ini index 7d94c9e64c..4683736acc 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.22.0.dev +version=1.23.0.dev packages= opentelemetry-sdk @@ -27,7 +27,7 @@ packages= opentelemetry-api [prerelease] -version=0.43b0.dev +version=0.44b0.dev packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-opencensus/pyproject.toml b/exporter/opentelemetry-exporter-opencensus/pyproject.toml index a6565ebaf4..e4ecc1119c 100644 --- a/exporter/opentelemetry-exporter-opencensus/pyproject.toml +++ b/exporter/opentelemetry-exporter-opencensus/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ dependencies = [ "grpcio >= 1.0.0, < 2.0.0", "opencensus-proto >= 0.1.0, < 1.0.0", - "opentelemetry-api >= 1.22.0.dev", + "opentelemetry-api >= 1.23.0.dev", "opentelemetry-sdk >= 1.15", "protobuf ~= 3.13", "setuptools >= 16.0", diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 2e4aa8c751..ff896307c3 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.43b0.dev" +__version__ = "0.44b0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml index bef34ed82b..307bed9c61 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ - "opentelemetry-proto == 1.22.0.dev", + "opentelemetry-proto == 1.23.0.dev", "backoff >= 1.10.0, < 2.0.0; python_version<'3.7'", "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py index 0616613de8..60f04a0743 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.22.0.dev" +__version__ = "1.23.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index 8750f8c9e4..bdb152021a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -31,9 +31,9 @@ dependencies = [ "googleapis-common-protos ~= 1.52", "grpcio >= 1.0.0, < 2.0.0", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.22.0.dev", - "opentelemetry-sdk ~= 1.22.0.dev", - "opentelemetry-exporter-otlp-proto-common == 1.22.0.dev", + "opentelemetry-proto == 1.23.0.dev", + "opentelemetry-sdk ~= 1.23.0.dev", + "opentelemetry-exporter-otlp-proto-common == 1.23.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 0616613de8..60f04a0743 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.22.0.dev" +__version__ = "1.23.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index 8174819166..d750377301 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -30,9 +30,9 @@ dependencies = [ "backoff >= 1.10.0, < 3.0.0; python_version>='3.7'", "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.15", - "opentelemetry-proto == 1.22.0.dev", - "opentelemetry-sdk ~= 1.22.0.dev", - "opentelemetry-exporter-otlp-proto-common == 1.22.0.dev", + "opentelemetry-proto == 1.23.0.dev", + "opentelemetry-sdk ~= 1.23.0.dev", + "opentelemetry-exporter-otlp-proto-common == 1.23.0.dev", "requests ~= 2.7", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 0616613de8..60f04a0743 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.22.0.dev" +__version__ = "1.23.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index 707b8c9455..0d909c0e12 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-otlp-proto-grpc == 1.22.0.dev", - "opentelemetry-exporter-otlp-proto-http == 1.22.0.dev", + "opentelemetry-exporter-otlp-proto-grpc == 1.23.0.dev", + "opentelemetry-exporter-otlp-proto-http == 1.23.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 0616613de8..60f04a0743 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.22.0.dev" +__version__ = "1.23.0.dev" diff --git a/exporter/opentelemetry-exporter-prometheus/pyproject.toml b/exporter/opentelemetry-exporter-prometheus/pyproject.toml index 19afcaae25..b634c3df88 100644 --- a/exporter/opentelemetry-exporter-prometheus/pyproject.toml +++ b/exporter/opentelemetry-exporter-prometheus/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ dependencies = [ "opentelemetry-api ~= 1.12", # DONOTMERGE: confirm that this will becomes ~= 1.21 in the next release - "opentelemetry-sdk ~= 1.22.0.dev", + "opentelemetry-sdk ~= 1.23.0.dev", "prometheus_client >= 0.5.0, < 1.0.0", ] diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 2e4aa8c751..ff896307c3 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.43b0.dev" +__version__ = "0.44b0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 0616613de8..60f04a0743 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.22.0.dev" +__version__ = "1.23.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml index f9046c6d5a..02a480c3a3 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.3", - "opentelemetry-exporter-zipkin-json == 1.22.0.dev", + "opentelemetry-exporter-zipkin-json == 1.23.0.dev", "opentelemetry-sdk ~= 1.11", "protobuf ~= 3.12", "requests ~= 2.7", diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 0616613de8..60f04a0743 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.22.0.dev" +__version__ = "1.23.0.dev" diff --git a/exporter/opentelemetry-exporter-zipkin/pyproject.toml b/exporter/opentelemetry-exporter-zipkin/pyproject.toml index 16c5eb83aa..e2340769d5 100644 --- a/exporter/opentelemetry-exporter-zipkin/pyproject.toml +++ b/exporter/opentelemetry-exporter-zipkin/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-exporter-zipkin-json == 1.22.0.dev", - "opentelemetry-exporter-zipkin-proto-http == 1.22.0.dev", + "opentelemetry-exporter-zipkin-json == 1.23.0.dev", + "opentelemetry-exporter-zipkin-proto-http == 1.23.0.dev", ] [project.optional-dependencies] diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 0616613de8..60f04a0743 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.22.0.dev" +__version__ = "1.23.0.dev" diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 0616613de8..60f04a0743 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.22.0.dev" +__version__ = "1.23.0.dev" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 0616613de8..60f04a0743 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.22.0.dev" +__version__ = "1.23.0.dev" diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index 2c5568c178..925eadb2a0 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "opentelemetry-api == 1.22.0.dev", - "opentelemetry-semantic-conventions == 0.43b0.dev", + "opentelemetry-api == 1.23.0.dev", + "opentelemetry-semantic-conventions == 0.44b0.dev", "typing-extensions >= 3.7.4", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 0616613de8..60f04a0743 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.22.0.dev" +__version__ = "1.23.0.dev" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 2e4aa8c751..ff896307c3 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.43b0.dev" +__version__ = "0.44b0.dev" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 0616613de8..60f04a0743 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.22.0.dev" +__version__ = "1.23.0.dev" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 0616613de8..60f04a0743 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.22.0.dev" +__version__ = "1.23.0.dev" diff --git a/shim/opentelemetry-opencensus-shim/pyproject.toml b/shim/opentelemetry-opencensus-shim/pyproject.toml index 60dfad777e..ef4dfd76c8 100644 --- a/shim/opentelemetry-opencensus-shim/pyproject.toml +++ b/shim/opentelemetry-opencensus-shim/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.43b0.dev", + "opentelemetry-test-utils == 0.44b0.dev", "opencensus == 0.11.1", # Temporary fix for https://github.com/census-instrumentation/opencensus-python/issues/1219 "six == 1.16.0", diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py index 2e4aa8c751..ff896307c3 100644 --- a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.43b0.dev" +__version__ = "0.44b0.dev" diff --git a/shim/opentelemetry-opentracing-shim/pyproject.toml b/shim/opentelemetry-opentracing-shim/pyproject.toml index 036af25e6c..7d32301daf 100644 --- a/shim/opentelemetry-opentracing-shim/pyproject.toml +++ b/shim/opentelemetry-opentracing-shim/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "opentelemetry-test-utils == 0.43b0.dev", + "opentelemetry-test-utils == 0.44b0.dev", "opentracing ~= 2.2.0", ] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 2e4aa8c751..ff896307c3 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.43b0.dev" +__version__ = "0.44b0.dev" diff --git a/tests/opentelemetry-test-utils/pyproject.toml b/tests/opentelemetry-test-utils/pyproject.toml index b7a0a53a91..1eb24bae36 100644 --- a/tests/opentelemetry-test-utils/pyproject.toml +++ b/tests/opentelemetry-test-utils/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ ] dependencies = [ "asgiref ~= 3.0", - "opentelemetry-api == 1.22.0.dev", - "opentelemetry-sdk == 1.22.0.dev", + "opentelemetry-api == 1.23.0.dev", + "opentelemetry-sdk == 1.23.0.dev", ] [project.optional-dependencies] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 5262902096..ecc7bc1725 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.43b0.dev" +__version__ = "0.44b0.dev" From da48e0b131ff34ff382b7d1206f71b2e31929cab Mon Sep 17 00:00:00 2001 From: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Date: Wed, 27 Dec 2023 18:53:13 +0100 Subject: [PATCH 1510/1517] Copy change log updates from release/v1.22.x-0.43bx (#3594) --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed48ae5141..f359c6b633 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -## Version 1.22.0/0.43b0 (2023-12-14) +## Version 1.22.0/0.43b0 (2023-12-15) -- Prometheus exporter sanitize info metric - ([#3572](https://github.com/open-telemetry/opentelemetry-python/pull/3572)) +- Prometheus exporter sanitize info metric ([#3572](https://github.com/open-telemetry/opentelemetry-python/pull/3572)) - Remove Jaeger exporters ([#3554](https://github.com/open-telemetry/opentelemetry-python/pull/3554)) - Log stacktrace on `UNKNOWN` status OTLP export error From 8e7334b8dd282c20db36acd09f225ac47be11d8d Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Fri, 5 Jan 2024 23:08:18 +0100 Subject: [PATCH 1511/1517] Fix docker proto tests (#3611) * Fix docker proto tests * Remove changelog --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 86ac70d0c5..646d3bb58e 100644 --- a/tox.ini +++ b/tox.ini @@ -271,6 +271,8 @@ deps = pytest==7.1.3 # Pinning PyYAML for issue: https://github.com/yaml/pyyaml/issues/724 PyYAML==5.3.1 + # Pinning docker for issue: https://github.com/docker/compose/issues/11309 + docker<7 docker-compose==1.29.2 requests==2.28.2 From 975733c71473cddddd0859c6fcbd2b02405f7e12 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 5 Jan 2024 16:21:29 -0600 Subject: [PATCH 1512/1517] Separate contrib jobs per instrumentation (#3507) * Separate contrib jobs per instrumentation Fixes #3499 * Separate exporters * Refactor listings * Fix lists --- .github/workflows/test.yml | 73 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 04309915ab..7a6519731b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,8 +36,28 @@ jobs: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: python-version: [py37, py38, py39, py310, py311, pypy3] - package: ["api", "sdk", "semantic", "getting", "shim", "exporter", "protobuf", - "propagator"] + package: + - "api" + - "sdk" + - "semantic-conventions" + - "getting-started" + - "opentracing-shim" + - "opencensus-shim" + - "exporter-jaeger-combined" + - "exporter-jaeger-proto-grpc" + - "exporter-jaeger-thrift" + - "exporter-opencensus" + - "exporter-otlp-proto-common" + - "exporter-otlp-combined" + - "exporter-otlp-proto-grpc" + - "exporter-otlp-proto-http" + - "exporter-prometheus" + - "exporter-zipkin-combined" + - "exporter-zipkin-proto-http" + - "exporter-zipkin-json" + - "protobuf" + - "propagator-b3" + - "propagator-jaeger" os: [ubuntu-20.04, windows-2019] steps: - name: Checkout Core Repo @ SHA - ${{ github.sha }} @@ -106,7 +126,54 @@ jobs: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: python-version: [py37] - package: ["instrumentation", "exporter"] + package: + - "aiohttp-client" + - "aiohttp-server" + - "aiopg" + - "aio-pika" + - "asgi" + - "asyncpg" + - "aws-lambda" + - "boto" + - "boto3sqs" + - "botocore" + - "cassandra" + - "celery" + - "confluent-kafka" + - "dbapi" + - "django" + - "elasticsearch" + - "falcon" + - "fastapi" + - "flask" + - "grpc" + - "httpx" + - "jinja2" + - "kafka-python" + - "logging" + - "mysql" + - "mysqlclient" + - "pika" + - "psycopg2" + - "pymemcache" + - "pymongo" + - "pymysql" + - "pyramid" + - "redis" + - "remoulade" + - "requests" + - "sklearn" + - "sqlalchemy" + - "sqlite3" + - "starlette" + - "system-metrics" + - "tornado" + - "tortoiseorm" + - "urllib" + - "urllib3" + - "wsgi" + - "prometheus-remote-write" + - "richconsole" os: [ubuntu-20.04] steps: - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} From 18c0cb4c0de9af5ab9783d6c7770e57b448c3bf2 Mon Sep 17 00:00:00 2001 From: Jeremy Voss Date: Wed, 24 Jan 2024 10:11:30 -0800 Subject: [PATCH 1513/1517] Improve Resource Detector timeout messaging (#3645) --- CHANGELOG.md | 3 +++ .../opentelemetry/sdk/resources/__init__.py | 8 ++++++++ .../tests/resources/test_resources.py | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f359c6b633..b46a17246a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Improve Resource Detector timeout messaging + ([#3645](https://github.com/open-telemetry/opentelemetry-python/pull/3645)) + ## Version 1.22.0/0.43b0 (2023-12-15) - Prometheus exporter sanitize info metric ([#3572](https://github.com/open-telemetry/opentelemetry-python/pull/3572)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index f92fdb964e..852b23f500 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -390,6 +390,14 @@ def get_aggregated_resources( detected_resource: Resource = _EMPTY_RESOURCE try: detected_resource = future.result(timeout=timeout) + except concurrent.futures.TimeoutError as ex: + if detector.raise_on_error: + raise ex + logger.warning( + "Detector %s took longer than %s seconds, skipping", + detector, + timeout, + ) # pylint: disable=broad-except except Exception as ex: if detector.raise_on_error: diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 53ecf30cab..da3f946961 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -15,6 +15,7 @@ import sys import unittest import uuid +from concurrent.futures import TimeoutError from logging import ERROR, WARNING from os import environ from unittest.mock import Mock, patch @@ -420,6 +421,23 @@ def test_resource_detector_raise_error(self): Exception, get_aggregated_resources, [resource_detector] ) + @patch("opentelemetry.sdk.resources.logger") + def test_resource_detector_timeout(self, mock_logger): + resource_detector = Mock(spec=ResourceDetector) + resource_detector.detect.side_effect = TimeoutError() + resource_detector.raise_on_error = False + self.assertEqual( + get_aggregated_resources([resource_detector]), + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") + ), + ) + mock_logger.warning.assert_called_with( + "Detector %s took longer than %s seconds, skipping", + resource_detector, + 5, + ) + @patch.dict( environ, {"OTEL_RESOURCE_ATTRIBUTES": "key1=env_value1,key2=env_value2"}, From 8b262d3d784fcd0b005fedbc2d3b0a95eaaf2eb1 Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Thu, 25 Jan 2024 14:42:04 -0500 Subject: [PATCH 1514/1517] Remove useless shebang lines (#3650) These files do not have the executable bit set in their filesystem permissions, so the shebang lines are not useful. Fixes #3643. --- docs/examples/opencensus-exporter-tracer/collector.py | 2 -- tests/w3c_tracecontext_validation_server.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/docs/examples/opencensus-exporter-tracer/collector.py b/docs/examples/opencensus-exporter-tracer/collector.py index 5c98cc4ce9..cd33c89617 100644 --- a/docs/examples/opencensus-exporter-tracer/collector.py +++ b/docs/examples/opencensus-exporter-tracer/collector.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 -# # Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py index d6c468025e..5c47708ee1 100644 --- a/tests/w3c_tracecontext_validation_server.py +++ b/tests/w3c_tracecontext_validation_server.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 -# # Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); From 373ed5175cea87951aa6b8f0922284357828bb99 Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Thu, 25 Jan 2024 15:00:06 -0500 Subject: [PATCH 1515/1517] =?UTF-8?q?Don=E2=80=99t=20pin=20an=20exact=20ve?= =?UTF-8?q?rsion=20of=20responses=20for=20testing=20(#3642)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Don’t pin an exact version of responses for testing In the test dependencies for opentelemetry-exporter-otlp-proto-http, allow `responses >= 0.22.0` rather than `responses == 0.22.0`. These tests use responses in a very straightforward way, and it’s unlikely that they will be affected by any future breaking changes; meanwhile, allowing newer versions helps with compatibility with newer Python interpreter versions and makes distribution packagers’ lives easier. * Upper-bound responses test dependency to the current minor release As requested in: https://github.com/open-telemetry/opentelemetry-python/pull/3642#issuecomment-1904877072 --------- Co-authored-by: Diego Hurtado --- exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index d750377301..dfab84f6f9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "responses == 0.22.0", + "responses >= 0.22.0, < 0.25", ] [project.entry-points.opentelemetry_traces_exporter] From 4bac02e97ebe6d1eecab1d8beaf9a93e4671c384 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 25 Jan 2024 17:35:10 -0600 Subject: [PATCH 1516/1517] Add test context for no exception raised (#3630) * Add test context for no exception raised Fixes #1891 * Add missing test class --- .../tests/test_otlp.py | 10 +-- .../test_exponent_mapping.py | 7 +- ...xponential_bucket_histogram_aggregation.py | 31 +------ .../tests/metrics/test_backward_compat.py | 14 +--- .../tests/metrics/test_import.py | 14 +--- .../tests/metrics/test_metrics.py | 12 +-- .../tests/test_jaeger_propagator.py | 8 +- .../src/opentelemetry/test/__init__.py | 55 +++++++++++++ .../tests/__init__.py | 13 +++ .../tests/test_utils.py | 82 +++++++++++++++++++ tox.ini | 4 + 11 files changed, 176 insertions(+), 74 deletions(-) create mode 100644 tests/opentelemetry-test-utils/src/opentelemetry/test/__init__.py create mode 100644 tests/opentelemetry-test-utils/tests/__init__.py create mode 100644 tests/opentelemetry-test-utils/tests/test_utils.py diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py index 5b574a9d60..7e18002289 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter, @@ -26,9 +25,10 @@ from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( OTLPSpanExporter as HTTPSpanExporter, ) +from opentelemetry.test import TestCase -class TestOTLPExporters(unittest.TestCase): +class TestOTLPExporters(TestCase): def test_constructors(self): for exporter in [ OTLPSpanExporter, @@ -36,9 +36,5 @@ def test_constructors(self): OTLPLogExporter, OTLPMetricExporter, ]: - try: + with self.assertNotRaises(Exception): exporter() - except Exception: # pylint: disable=broad-except - self.fail( - f"Unexpected exception raised when instantiating {exporter.__name__}" - ) diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py index ae06d963ab..96ba399181 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py @@ -14,7 +14,6 @@ from math import inf from sys import float_info, version_info -from unittest import TestCase from unittest.mock import patch from pytest import mark @@ -31,6 +30,7 @@ MIN_NORMAL_EXPONENT, MIN_NORMAL_VALUE, ) +from opentelemetry.test import TestCase if version_info >= (3, 9): from math import nextafter @@ -69,12 +69,9 @@ def test_init_called_once(self, mock_init): def test_exponent_mapping_0(self): - try: + with self.assertNotRaises(Exception): ExponentMapping(0) - except Exception as error: - self.fail(f"Unexpected exception raised: {error}") - def test_exponent_mapping_zero(self): exponent_mapping = ExponentMapping(0) diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py index 9bea75e426..311f00a0b0 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py @@ -17,7 +17,6 @@ from math import ldexp from sys import float_info from types import MethodType -from unittest import TestCase from unittest.mock import Mock, patch from opentelemetry.sdk.metrics._internal.aggregation import ( @@ -41,6 +40,7 @@ from opentelemetry.sdk.metrics.view import ( ExponentialBucketHistogramAggregation, ) +from opentelemetry.test import TestCase def get_counts(buckets: Buckets) -> int: @@ -793,11 +793,8 @@ def test_boundary_statistics(self): index = mapping.map_to_index(value) - try: + with self.assertNotRaises(Exception): boundary = mapping.get_lower_boundary(index + 1) - except Exception as error: - raise error - self.fail(f"Unexpected exception {error} raised") if boundary < value: above += 1 @@ -836,30 +833,6 @@ def test_aggregate_collect(self): """ Tests a repeated cycle of aggregation and collection. """ - """ - try: - exponential_histogram_aggregation = ( - _ExponentialBucketHistogramAggregation( - Mock(), - Mock(), - ) - ) - - exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) - exponential_histogram_aggregation.collect( - AggregationTemporality.CUMULATIVE, 0 - ) - exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) - exponential_histogram_aggregation.collect( - AggregationTemporality.CUMULATIVE, 0 - ) - exponential_histogram_aggregation.aggregate(Measurement(2, Mock())) - exponential_histogram_aggregation.collect( - AggregationTemporality.CUMULATIVE, 0 - ) - except Exception as error: - self.fail(f"Unexpected exception raised: {error}") - """ exponential_histogram_aggregation = ( _ExponentialBucketHistogramAggregation( Mock(), diff --git a/opentelemetry-sdk/tests/metrics/test_backward_compat.py b/opentelemetry-sdk/tests/metrics/test_backward_compat.py index 7f8fff2acf..46008554fe 100644 --- a/opentelemetry-sdk/tests/metrics/test_backward_compat.py +++ b/opentelemetry-sdk/tests/metrics/test_backward_compat.py @@ -26,7 +26,6 @@ """ from typing import Iterable, Sequence -from unittest import TestCase from opentelemetry.metrics import CallbackOptions, Observation from opentelemetry.sdk.metrics import MeterProvider @@ -38,6 +37,7 @@ MetricReader, PeriodicExportingMetricReader, ) +from opentelemetry.test import TestCase # Do not change these classes until after major version 1 @@ -82,30 +82,24 @@ def test_metric_exporter(self): ) # produce some data meter_provider.get_meter("foo").create_counter("mycounter").add(12) - try: + with self.assertNotRaises(Exception): meter_provider.shutdown() - except Exception: - self.fail() def test_metric_reader(self): reader = OrigMetricReader() meter_provider = MeterProvider(metric_readers=[reader]) # produce some data meter_provider.get_meter("foo").create_counter("mycounter").add(12) - try: + with self.assertNotRaises(Exception): meter_provider.shutdown() - except Exception: - self.fail() def test_observable_callback(self): reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[reader]) # produce some data meter_provider.get_meter("foo").create_counter("mycounter").add(12) - try: + with self.assertNotRaises(Exception): metrics_data = reader.get_metrics_data() - except Exception: - self.fail() self.assertEqual(len(metrics_data.resource_metrics), 1) self.assertEqual( diff --git a/opentelemetry-sdk/tests/metrics/test_import.py b/opentelemetry-sdk/tests/metrics/test_import.py index 70afa91497..f0302e00de 100644 --- a/opentelemetry-sdk/tests/metrics/test_import.py +++ b/opentelemetry-sdk/tests/metrics/test_import.py @@ -14,7 +14,7 @@ # pylint: disable=unused-import -from unittest import TestCase +from opentelemetry.test import TestCase class TestImport(TestCase): @@ -23,7 +23,7 @@ def test_import_init(self): Test that the metrics root module has the right symbols """ - try: + with self.assertNotRaises(Exception): from opentelemetry.sdk.metrics import ( # noqa: F401 Counter, Histogram, @@ -34,15 +34,13 @@ def test_import_init(self): ObservableUpDownCounter, UpDownCounter, ) - except Exception as error: - self.fail(f"Unexpected error {error} was raised") def test_import_export(self): """ Test that the metrics export module has the right symbols """ - try: + with self.assertNotRaises(Exception): from opentelemetry.sdk.metrics.export import ( # noqa: F401 AggregationTemporality, ConsoleMetricExporter, @@ -63,15 +61,13 @@ def test_import_export(self): ScopeMetrics, Sum, ) - except Exception as error: - self.fail(f"Unexpected error {error} was raised") def test_import_view(self): """ Test that the metrics view module has the right symbols """ - try: + with self.assertNotRaises(Exception): from opentelemetry.sdk.metrics.view import ( # noqa: F401 Aggregation, DefaultAggregation, @@ -81,5 +77,3 @@ def test_import_view(self): SumAggregation, View, ) - except Exception as error: - self.fail(f"Unexpected error {error} was raised") diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 8373d3dfe0..0ccadf47ce 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -16,7 +16,6 @@ from logging import WARNING from time import sleep from typing import Iterable, Sequence -from unittest import TestCase from unittest.mock import MagicMock, Mock, patch from opentelemetry.metrics import NoOpMeter @@ -40,6 +39,7 @@ ) from opentelemetry.sdk.metrics.view import SumAggregation, View from opentelemetry.sdk.resources import Resource +from opentelemetry.test import TestCase from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc @@ -59,7 +59,7 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: return True -class TestMeterProvider(ConcurrencyTestBase): +class TestMeterProvider(ConcurrencyTestBase, TestCase): def tearDown(self): MeterProvider._all_metric_readers = set() @@ -86,11 +86,9 @@ def test_register_metric_readers(self): metric_reader_0 = PeriodicExportingMetricReader(mock_exporter) metric_reader_1 = PeriodicExportingMetricReader(mock_exporter) - try: + with self.assertNotRaises(Exception): MeterProvider(metric_readers=(metric_reader_0,)) MeterProvider(metric_readers=(metric_reader_1,)) - except Exception as error: - self.fail(f"Unexpected exception {error} raised") with self.assertRaises(Exception): MeterProvider(metric_readers=(metric_reader_0,)) @@ -356,7 +354,7 @@ def setUp(self): self.meter = Meter(Mock(), Mock()) def test_repeated_instrument_names(self): - try: + with self.assertNotRaises(Exception): self.meter.create_counter("counter") self.meter.create_up_down_counter("up_down_counter") self.meter.create_observable_counter( @@ -369,8 +367,6 @@ def test_repeated_instrument_names(self): self.meter.create_observable_up_down_counter( "observable_up_down_counter", callbacks=[Mock()] ) - except Exception as error: - self.fail(f"Unexpected exception raised {error}") for instrument_name in [ "counter", diff --git a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py index 81187389a1..a836cdf403 100644 --- a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from unittest.mock import Mock import opentelemetry.trace as trace_api @@ -24,6 +23,7 @@ ) from opentelemetry.sdk import trace from opentelemetry.sdk.trace import id_generator +from opentelemetry.test import TestCase FORMAT = jaeger.JaegerPropagator() @@ -57,7 +57,7 @@ def get_context_new_carrier(old_carrier, carrier_baggage=None): return ctx, new_carrier -class TestJaegerPropagator(unittest.TestCase): +class TestJaegerPropagator(TestCase): @classmethod def setUpClass(cls): generator = id_generator.RandomIdGenerator() @@ -236,7 +236,5 @@ def test_non_recording_span_does_not_crash(self): mock_setter = Mock() span = trace_api.NonRecordingSpan(trace_api.SpanContext(1, 1, True)) with trace_api.use_span(span, end_on_exit=True): - try: + with self.assertNotRaises(Exception): FORMAT.inject({}, setter=mock_setter) - except Exception as exc: # pylint: disable=broad-except - self.fail(f"Injecting failed for NonRecordingSpan with {exc}") diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/__init__.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/__init__.py new file mode 100644 index 0000000000..068ed12e86 --- /dev/null +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/__init__.py @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# type: ignore + +from traceback import format_tb +from unittest import TestCase + + +class _AssertNotRaisesMixin: + class _AssertNotRaises: + def __init__(self, test_case): + self._test_case = test_case + + def __enter__(self): + return self + + def __exit__(self, type_, value, tb): # pylint: disable=invalid-name + if value is not None and type_ in self._exception_types: + + self._test_case.fail( + "Unexpected exception was raised:\n{}".format( + "\n".join(format_tb(tb)) + ) + ) + + return True + + def __call__(self, exception, *exceptions): + # pylint: disable=attribute-defined-outside-init + self._exception_types = (exception, *exceptions) + return self + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + # pylint: disable=invalid-name + self.assertNotRaises = self._AssertNotRaises(self) + + +class TestCase( + _AssertNotRaisesMixin, TestCase +): # pylint: disable=function-redefined + pass diff --git a/tests/opentelemetry-test-utils/tests/__init__.py b/tests/opentelemetry-test-utils/tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/tests/opentelemetry-test-utils/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/opentelemetry-test-utils/tests/test_utils.py b/tests/opentelemetry-test-utils/tests/test_utils.py new file mode 100644 index 0000000000..ce97951f86 --- /dev/null +++ b/tests/opentelemetry-test-utils/tests/test_utils.py @@ -0,0 +1,82 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry.test import TestCase + + +class TestAssertNotRaises(TestCase): + def test_no_exception(self): + + try: + + with self.assertNotRaises(Exception): + pass + + except Exception as error: # pylint: disable=broad-except + + self.fail( # pylint: disable=no-member + f"Unexpected exception {error} was raised" + ) + + def test_no_specified_exception_single(self): + + try: + + with self.assertNotRaises(KeyError): + 1 / 0 # pylint: disable=pointless-statement + + except Exception as error: # pylint: disable=broad-except + + self.fail( # pylint: disable=no-member + f"Unexpected exception {error} was raised" + ) + + def test_no_specified_exception_multiple(self): + + try: + + with self.assertNotRaises(KeyError, IndexError): + 1 / 0 # pylint: disable=pointless-statement + + except Exception as error: # pylint: disable=broad-except + + self.fail( # pylint: disable=no-member + f"Unexpected exception {error} was raised" + ) + + def test_exception(self): + + with self.assertRaises(AssertionError): + + with self.assertNotRaises(ZeroDivisionError): + 1 / 0 # pylint: disable=pointless-statement + + def test_missing_exception(self): + + with self.assertRaises(AssertionError) as error: + + with self.assertNotRaises(ZeroDivisionError): + + def raise_zero_division_error(): + raise ZeroDivisionError() + + raise_zero_division_error() + + error_lines = error.exception.args[0].split("\n") + + self.assertEqual( + error_lines[0].strip(), "Unexpected exception was raised:" + ) + self.assertEqual(error_lines[2].strip(), "raise_zero_division_error()") + self.assertEqual(error_lines[5].strip(), "raise ZeroDivisionError()") diff --git a/tox.ini b/tox.ini index 646d3bb58e..f619b488dc 100644 --- a/tox.ini +++ b/tox.ini @@ -61,6 +61,9 @@ envlist = py3{7,8,9,10,11}-opentelemetry-propagator-jaeger pypy3-opentelemetry-propagator-jaeger + py3{7,8,9,10,11}-opentelemetry-test-utils + pypy3-opentelemetry-test-utils + lint spellcheck tracecontext @@ -112,6 +115,7 @@ changedir = propagator-b3: propagator/opentelemetry-propagator-b3/tests propagator-jaeger: propagator/opentelemetry-propagator-jaeger/tests + test-utils: tests/opentelemetry-test-utils/tests commands_pre = ; Install without -e to test the actual installation From c4d17e9f14f3cafb6757b96eefabdc7ed4891306 Mon Sep 17 00:00:00 2001 From: gshiva Date: Thu, 25 Jan 2024 16:27:50 -0800 Subject: [PATCH 1517/1517] Handle HTTP 2XX responses as successful in OTLP exporters (#3623) * Handle HTTP 2XX responses as successful in OTLP exporters * Add test cases for 2XX HTTP responses * Add CHANGELOG entry * Fix lint --- CHANGELOG.md | 5 ++++- .../otlp/proto/http/_log_exporter/__init__.py | 2 +- .../otlp/proto/http/metric_exporter/__init__.py | 2 +- .../otlp/proto/http/trace_exporter/__init__.py | 2 +- .../tests/metrics/test_otlp_metrics_exporter.py | 13 ++++++++++++- .../tests/test_proto_log_exporter.py | 13 ++++++++++++- .../tests/test_proto_span_exporter.py | 13 ++++++++++++- 7 files changed, 43 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b46a17246a..6dd0ee3b7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Handle HTTP 2XX responses as successful in OTLP exporters + ([#3623](https://github.com/open-telemetry/opentelemetry-python/pull/3623)) - Improve Resource Detector timeout messaging ([#3645](https://github.com/open-telemetry/opentelemetry-python/pull/3645)) ## Version 1.22.0/0.43b0 (2023-12-15) -- Prometheus exporter sanitize info metric ([#3572](https://github.com/open-telemetry/opentelemetry-python/pull/3572)) +- Prometheus exporter sanitize info metric + ([#3572](https://github.com/open-telemetry/opentelemetry-python/pull/3572)) - Remove Jaeger exporters ([#3554](https://github.com/open-telemetry/opentelemetry-python/pull/3554)) - Log stacktrace on `UNKNOWN` status OTLP export error diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index caacdbdfca..4703b10286 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -146,7 +146,7 @@ def export(self, batch: Sequence[LogData]) -> LogExportResult: resp = self._export(serialized_data) # pylint: disable=no-else-return - if resp.status_code in (200, 202): + if resp.ok: return LogExportResult.SUCCESS elif self._retryable(resp): _logger.warning( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index ed878dabe8..becdab257f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -178,7 +178,7 @@ def export( resp = self._export(serialized_data.SerializeToString()) # pylint: disable=no-else-return - if resp.status_code in (200, 202): + if resp.ok: return MetricExportResult.SUCCESS elif self._retryable(resp): _logger.warning( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 2ab7e97e02..d98a1b84a7 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -144,7 +144,7 @@ def export(self, spans) -> SpanExportResult: resp = self._export(serialized_data) # pylint: disable=no-else-return - if resp.status_code in (200, 202): + if resp.ok: return SpanExportResult.SUCCESS elif self._retryable(resp): _logger.warning( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index d9011322b9..c06b5db3c2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -15,7 +15,7 @@ from logging import WARNING from os import environ from unittest import TestCase -from unittest.mock import patch +from unittest.mock import MagicMock, Mock, patch from requests import Session from requests.models import Response @@ -476,3 +476,14 @@ def test_exponential_explicit_bucket_histogram(self): OTLPMetricExporter()._preferred_aggregation[Histogram], ExplicitBucketHistogramAggregation, ) + + @patch.object(OTLPMetricExporter, "_export", return_value=Mock(ok=True)) + def test_2xx_status_code(self, mock_otlp_metric_exporter): + """ + Test that any HTTP 2XX code returns a successful result + """ + + self.assertEqual( + OTLPMetricExporter().export(MagicMock()), + MetricExportResult.SUCCESS, + ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index 5300ce85de..e601e5d00c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -16,7 +16,7 @@ import unittest from typing import List -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, Mock, patch import requests import responses @@ -34,6 +34,7 @@ from opentelemetry.exporter.otlp.proto.http.version import __version__ from opentelemetry.sdk._logs import LogData from opentelemetry.sdk._logs import LogRecord as SDKLogRecord +from opentelemetry.sdk._logs.export import LogExportResult from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_COMPRESSION, @@ -262,3 +263,13 @@ def _get_sdk_log_data() -> List[LogData]: ) return [log1, log2, log3, log4] + + @patch.object(OTLPLogExporter, "_export", return_value=Mock(ok=True)) + def test_2xx_status_code(self, mock_otlp_metric_exporter): + """ + Test that any HTTP 2XX code returns a successful result + """ + + self.assertEqual( + OTLPLogExporter().export(MagicMock()), LogExportResult.SUCCESS + ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index 9a1d1604a2..eb5b375e40 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -14,7 +14,7 @@ import unittest from collections import OrderedDict -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch import requests import responses @@ -42,6 +42,7 @@ OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) from opentelemetry.sdk.trace import _Span +from opentelemetry.sdk.trace.export import SpanExportResult OS_ENV_ENDPOINT = "os.env.base" OS_ENV_CERTIFICATE = "os/env/base.crt" @@ -239,3 +240,13 @@ def generate_delays(*args, **kwargs): exporter.export([span]) mock_sleep.assert_called_once_with(1) + + @patch.object(OTLPSpanExporter, "_export", return_value=Mock(ok=True)) + def test_2xx_status_code(self, mock_otlp_metric_exporter): + """ + Test that any HTTP 2XX code returns a successful result + """ + + self.assertEqual( + OTLPSpanExporter().export(MagicMock()), SpanExportResult.SUCCESS + )

    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 literal 0 HcmV?d00001 diff --git a/docs/index.rst b/docs/index.rst index 3007b7f74d..bc09b5a475 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -54,26 +54,13 @@ install pip install -e ./ext/opentelemetry-ext-{integration} -Quick Start ------------ - -OpenTelemetry can be used to emit distributed traces and metrics from your application. -The following are two simple examples using the API and SDK, you can find more -elaborated examples in `Examples`_. - -.. TODO: Link to complete and better examples - -Tracing -~~~~~~~ - -.. literalinclude:: trace_example.py - :language: python +.. toctree:: + :maxdepth: 1 + :caption: Getting Started + :name: getting-started -Metrics -~~~~~~~ + getting-started -.. literalinclude:: metrics_example.py - :language: python .. toctree:: :maxdepth: 1 diff --git a/docs/trace_example.py b/docs/trace_example.py index 8197dee156..5c87807ec5 100644 --- a/docs/trace_example.py +++ b/docs/trace_example.py @@ -5,8 +5,8 @@ SimpleExportSpanProcessor, ) -trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) -trace.tracer_provider().add_span_processor( +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) diff --git a/examples/metrics/docker/prometheus.yaml b/examples/metrics/docker/prometheus.yaml index 4eb2357231..c6f369c757 100644 --- a/examples/metrics/docker/prometheus.yaml +++ b/examples/metrics/docker/prometheus.yaml @@ -1,4 +1,4 @@ -scrape_configs: +scrape_config: - job_name: 'otel-collector' scrape_interval: 5s static_configs: From b82dbe0801b6f724b4cabd879103c46a1fe66998 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 16 Mar 2020 14:19:47 -0700 Subject: [PATCH 0241/1517] Bugfix: Fixing example app test (#495) --- .../src/opentelemetry_example_app/flask_example.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index f8a4997392..d523224d13 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -23,8 +23,10 @@ from opentelemetry import trace from opentelemetry.ext.flask import instrument_app from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ConsoleSpanExporter -from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( @@ -42,6 +44,3 @@ def hello(): with tracer.start_as_current_span("example-request"): requests.get("http://www.example.com") return "hello" - - -app.run(debug=True) From d870abf4e6d997fad44f3cf0657b145f138f7581 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 16 Mar 2020 15:54:30 -0600 Subject: [PATCH 0242/1517] Removed set_preferred_meter_provider_implementation (#497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mauricio Vásquez Co-authored-by: Chris Kleinknecht --- docs/metrics_example.py | 2 +- ext/opentelemetry-ext-otcollector/README.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/metrics_example.py b/docs/metrics_example.py index 8ae01f5e98..736b9c7f30 100644 --- a/docs/metrics_example.py +++ b/docs/metrics_example.py @@ -3,7 +3,7 @@ from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController -metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) +metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__) exporter = ConsoleMetricsExporter() controller = PushController(meter, exporter, 5) diff --git a/ext/opentelemetry-ext-otcollector/README.rst b/ext/opentelemetry-ext-otcollector/README.rst index 200ec9a91d..7cd35b3f8c 100644 --- a/ext/opentelemetry-ext-otcollector/README.rst +++ b/ext/opentelemetry-ext-otcollector/README.rst @@ -70,7 +70,7 @@ The **OpenTelemetry Collector Exporter** allows to export `OpenTelemetry`_ metri ) # Meter is responsible for creating and recording metrics - metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) + metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__) # controller collects metrics created from meter and exports it via the # exporter every interval @@ -87,7 +87,7 @@ The **OpenTelemetry Collector Exporter** allows to export `OpenTelemetry`_ metri # metric that you want to record. These are useful for pre-aggregation and can # be used to store custom dimensions pertaining to a metric label_set = meter.get_label_set({"environment": "staging"}) - + counter.add(25, label_set) From 6bfc48bbed72c42019e2f4cac12b8bea1824c3f1 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 17 Mar 2020 11:42:19 -0700 Subject: [PATCH 0243/1517] Update master to 0.6.dev0 (#499) Co-authored-by: Yusuke Tsutsumi Co-authored-by: Leighton Chen --- .../opentelemetry-example-app/setup.py | 2 +- ext/opentelemetry-ext-dbapi/setup.cfg | 2 +- .../src/opentelemetry/ext/dbapi/version.py | 2 +- .../src/opentelemetry/ext/flask/version.py | 2 +- ext/opentelemetry-ext-http-requests/setup.cfg | 2 +- .../ext/http_requests/version.py | 2 +- .../src/opentelemetry/ext/jaeger/version.py | 2 +- ext/opentelemetry-ext-mysql/setup.cfg | 2 +- .../src/opentelemetry/ext/mysql/version.py | 2 +- .../ext/opentracing_shim/version.py | 2 +- .../CHANGELOG.md | 5 ++++ ext/opentelemetry-ext-otcollector/setup.cfg | 4 ++-- .../opentelemetry/ext/otcollector/version.py | 2 +- .../opentelemetry/ext/prometheus/version.py | 2 +- ext/opentelemetry-ext-psycopg2/setup.cfg | 2 +- .../src/opentelemetry/ext/psycopg2/version.py | 2 +- ext/opentelemetry-ext-pymongo/setup.cfg | 2 +- .../src/opentelemetry/ext/pymongo/version.py | 2 +- .../src/opentelemetry/ext/testutil/version.py | 2 +- .../src/opentelemetry/ext/wsgi/version.py | 2 +- .../src/opentelemetry/ext/zipkin/version.py | 2 +- opentelemetry-api/CHANGELOG.md | 23 +++++++++++++++++++ .../src/opentelemetry/util/version.py | 2 +- opentelemetry-sdk/CHANGELOG.md | 17 ++++++++++++++ opentelemetry-sdk/setup.py | 2 +- .../src/opentelemetry/sdk/version.py | 2 +- 26 files changed, 69 insertions(+), 24 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py index 637ad084d8..aa40b5b26b 100644 --- a/docs/examples/opentelemetry-example-app/setup.py +++ b/docs/examples/opentelemetry-example-app/setup.py @@ -16,7 +16,7 @@ setuptools.setup( name="opentelemetry-example-app", - version="0.5.dev0", + version="0.6.dev0", author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 3826d80893..451a994f92 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.5.dev0 + opentelemetry-api >= 0.6.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py index d13bf96748..ef946f5872 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py index d13bf96748..ef946f5872 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg index a064eb3c62..7537aa340f 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.5.dev0 + opentelemetry-api >= 0.6.dev0 requests ~= 2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py index d13bf96748..ef946f5872 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index fdf63e3792..aadc5afd63 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index b29636fa13..2d69dadd0c 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.5.dev0 + opentelemetry-api >= 0.6.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index d13bf96748..ef946f5872 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index d13bf96748..ef946f5872 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/ext/opentelemetry-ext-otcollector/CHANGELOG.md b/ext/opentelemetry-ext-otcollector/CHANGELOG.md index 617d979ab2..2dbd7217b7 100644 --- a/ext/opentelemetry-ext-otcollector/CHANGELOG.md +++ b/ext/opentelemetry-ext-otcollector/CHANGELOG.md @@ -2,3 +2,8 @@ ## Unreleased +## 0.5b0 + +Released 2020-03-16 + +Initial release. \ No newline at end of file diff --git a/ext/opentelemetry-ext-otcollector/setup.cfg b/ext/opentelemetry-ext-otcollector/setup.cfg index acc5b37723..203f2a6c82 100644 --- a/ext/opentelemetry-ext-otcollector/setup.cfg +++ b/ext/opentelemetry-ext-otcollector/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api >= 0.5.dev0 - opentelemetry-sdk >= 0.5.dev0 + opentelemetry-api >= 0.6.dev0 + opentelemetry-sdk >= 0.6.dev0 protobuf >= 3.8.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py index f48cb5bee5..373ae92cb8 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py index f48cb5bee5..373ae92cb8 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index a4a67e2019..c7ddb5c25c 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.5.dev0 + opentelemetry-api >= 0.6.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py index f48cb5bee5..373ae92cb8 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index 0f6a06ea7f..927b2db335 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.5.dev0 + opentelemetry-api >= 0.6.dev0 pymongo ~= 3.1 [options.packages.find] diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index d13bf96748..ef946f5872 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py index 9aea0d23ea..d0b2d05c07 100644 --- a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py +++ b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py @@ -1 +1 @@ -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index d13bf96748..ef946f5872 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py index d13bf96748..ef946f5872 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 9864fc67d3..a19ed76e4f 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,29 @@ ## Unreleased +## 0.5b0 + +Released 2020-03-16 + +- Adding Correlation Context API and propagator + ([#471](https://github.com/open-telemetry/opentelemetry-python/pull/471)) +- Adding a global configuration module to simplify setting and getting globals + ([#466](https://github.com/open-telemetry/opentelemetry-python/pull/466)) +- Rename metric handle to bound metric instrument + ([#470](https://github.com/open-telemetry/opentelemetry-python/pull/470)) +- Moving resources to sdk + ([#464](https://github.com/open-telemetry/opentelemetry-python/pull/464)) +- Implementing propagators to API to use context + ([#446](https://github.com/open-telemetry/opentelemetry-python/pull/446)) +- Adding named meters, removing batchers + ([#431](https://github.com/open-telemetry/opentelemetry-python/pull/431)) +- Renaming TraceOptions to TraceFlags + ([#450](https://github.com/open-telemetry/opentelemetry-python/pull/450)) +- Renaming TracerSource to TraceProvider + ([#441](https://github.com/open-telemetry/opentelemetry-python/pull/441)) +- Adding attach/detach methods as per spec + ([#429](https://github.com/open-telemetry/opentelemetry-python/pull/450) + ## 0.4a0 Released 2020-02-21 diff --git a/opentelemetry-api/src/opentelemetry/util/version.py b/opentelemetry-api/src/opentelemetry/util/version.py index d13bf96748..ef946f5872 100644 --- a/opentelemetry-api/src/opentelemetry/util/version.py +++ b/opentelemetry-api/src/opentelemetry/util/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 54502d7eae..aa8aafbac9 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,23 @@ ## Unreleased +## 0.5b0 + +Released 2020-03-16 + +- Adding Correlation Context SDK and propagator + ([#471](https://github.com/open-telemetry/opentelemetry-python/pull/471)) +- Adding OT Collector metrics exporter + ([#454](https://github.com/open-telemetry/opentelemetry-python/pull/454)) +- Improve validation of attributes + ([#460](https://github.com/open-telemetry/opentelemetry-python/pull/460)) +- Moving resources to sdk + ([#464](https://github.com/open-telemetry/opentelemetry-python/pull/464)) +- Re-raise errors caught in opentelemetry.sdk.trace.Tracer.use_span() + ([#469](https://github.com/open-telemetry/opentelemetry-python/pull/469)) +- Implement observer instrument + ([#425](https://github.com/open-telemetry/opentelemetry-python/pull/425)) + ## 0.4a0 Released 2020-02-21 diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index c471ff0231..02bfc0a9e8 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -44,7 +44,7 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["opentelemetry-api==0.5.dev0"], + install_requires=["opentelemetry-api==0.6.dev0"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index d13bf96748..ef946f5872 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.dev0" +__version__ = "0.6.dev0" From 4e551ba21b4f9c84cc79ebdf7798446d67d7841c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 17 Mar 2020 15:35:29 -0500 Subject: [PATCH 0244/1517] metrics: Implement release for handles and observers (#435) This commit implements a solution for releasing instrument handles and observers. For the handles it is based on a ref count that is increased each time the handled is acquired, when the ref count reaches 0 the handle is removed on collection time. The direct call convention is updated to release the handle after it has been updated. The observer instrument is only updated on collection time, so it can be removed as soon as the user request to do so. --- docs/examples/metrics/record.py | 9 +- .../src/opentelemetry/metrics/__init__.py | 14 +++ .../tests/metrics/test_metrics.py | 8 +- .../tests/test_implementation.py | 5 ++ .../src/opentelemetry/sdk/metrics/__init__.py | 88 ++++++++++++++----- .../tests/metrics/test_metrics.py | 72 ++++++++++++++- 6 files changed, 170 insertions(+), 26 deletions(-) diff --git a/docs/examples/metrics/record.py b/docs/examples/metrics/record.py index b558e18fca..e1c7901fa0 100644 --- a/docs/examples/metrics/record.py +++ b/docs/examples/metrics/record.py @@ -67,7 +67,11 @@ # Therefore, getting a bound metric instrument using the same set of labels # will yield the same bound metric instrument. bound_counter = counter.bind(label_set) -bound_counter.add(100) +for i in range(1000): + bound_counter.add(i) + +# You can release the bound instrument we you are done +bound_counter.release() # Direct metric usage # You can record metrics directly using the metric instrument. You pass in a @@ -79,4 +83,5 @@ # (metric, value) pairs. The value would be recorded for each metric using the # specified labelset for each. meter.record_batch(label_set, [(counter, 50), (counter2, 70)]) -time.sleep(100) + +time.sleep(10) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 8254594282..0db7f125c8 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -60,6 +60,9 @@ def record(self, value: ValueT) -> None: value: The value to record to the bound metric instrument. """ + def release(self) -> None: + """No-op implementation of release.""" + class BoundCounter: def add(self, value: ValueT) -> None: @@ -350,6 +353,14 @@ def register_observer( Returns: A new ``Observer`` metric instrument. """ + @abc.abstractmethod + def unregister_observer(self, observer: "Observer") -> None: + """Unregisters an ``Observer`` metric instrument. + + Args: + observer: The observer to unregister. + """ + @abc.abstractmethod def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": """Gets a `LabelSet` with the given labels. @@ -396,6 +407,9 @@ def register_observer( ) -> "Observer": return DefaultObserver() + def unregister_observer(self, observer: "Observer") -> None: + pass + def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": # pylint: disable=no-self-use return DefaultLabelSet() diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 9ecbede9f2..a2552d9f57 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -50,7 +50,8 @@ def test_measure_record(self): measure.record(1, label_set) def test_default_bound_metric(self): - metrics.DefaultBoundInstrument() + bound_instrument = metrics.DefaultBoundInstrument() + bound_instrument.release() def test_bound_counter(self): bound_counter = metrics.BoundCounter() @@ -59,3 +60,8 @@ def test_bound_counter(self): def test_bound_measure(self): bound_measure = metrics.BoundMeasure() bound_measure.record(1) + + def test_observer(self): + observer = metrics.DefaultObserver() + label_set = metrics.LabelSet() + observer.observe(1, label_set) diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index 7271eb5139..6d2aa215f5 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -87,6 +87,11 @@ def test_register_observer(self): observer = meter.register_observer(callback, "", "", "", int, (), True) self.assertIsInstance(observer, metrics.DefaultObserver) + def test_unregister_observer(self): + meter = metrics.DefaultMeter() + observer = metrics.DefaultObserver() + meter.unregister_observer(observer) + def test_get_label_set(self): meter = metrics.DefaultMeter() label_set = meter.get_label_set({}) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 9c840da298..55552ace7b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. import logging +import threading from typing import Dict, Sequence, Tuple, Type from opentelemetry import metrics as metrics_api @@ -71,6 +72,8 @@ def __init__( self.enabled = enabled self.aggregator = aggregator self.last_update_timestamp = time_ns() + self._ref_count = 0 + self._ref_count_lock = threading.Lock() def _validate_update(self, value: metrics_api.ValueT) -> bool: if not self.enabled: @@ -86,6 +89,21 @@ def update(self, value: metrics_api.ValueT): self.last_update_timestamp = time_ns() self.aggregator.update(value) + def release(self): + self.decrease_ref_count() + + def decrease_ref_count(self): + with self._ref_count_lock: + self._ref_count -= 1 + + def increase_ref_count(self): + with self._ref_count_lock: + self._ref_count += 1 + + def ref_count(self): + with self._ref_count_lock: + return self._ref_count + def __repr__(self): return '{}(data="{}", last_update_timestamp={})'.format( type(self).__name__, @@ -137,18 +155,21 @@ def __init__( self.label_keys = label_keys self.enabled = enabled self.bound_instruments = {} + self.bound_instruments_lock = threading.Lock() def bind(self, label_set: LabelSet) -> BaseBoundInstrument: """See `opentelemetry.metrics.Metric.bind`.""" - bound_instrument = self.bound_instruments.get(label_set) - if not bound_instrument: - bound_instrument = self.BOUND_INSTR_TYPE( - self.value_type, - self.enabled, - # Aggregator will be created based off type of metric - self.meter.batcher.aggregator_for(self.__class__), - ) - self.bound_instruments[label_set] = bound_instrument + with self.bound_instruments_lock: + bound_instrument = self.bound_instruments.get(label_set) + if bound_instrument is None: + bound_instrument = self.BOUND_INSTR_TYPE( + self.value_type, + self.enabled, + # Aggregator will be created based off type of metric + self.meter.batcher.aggregator_for(self.__class__), + ) + self.bound_instruments[label_set] = bound_instrument + bound_instrument.increase_ref_count() return bound_instrument def __repr__(self): @@ -167,7 +188,9 @@ class Counter(Metric, metrics_api.Counter): def add(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: """See `opentelemetry.metrics.Counter.add`.""" - self.bind(label_set).add(value) + bound_intrument = self.bind(label_set) + bound_intrument.add(value) + bound_intrument.release() UPDATE_FUNCTION = add @@ -179,7 +202,9 @@ class Measure(Metric, metrics_api.Measure): def record(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: """See `opentelemetry.metrics.Measure.record`.""" - self.bind(label_set).record(value) + bound_intrument = self.bind(label_set) + bound_intrument.record(value) + bound_intrument.release() UPDATE_FUNCTION = record @@ -279,6 +304,7 @@ def __init__( self.metrics = set() self.observers = set() self.batcher = UngroupedBatcher(stateful) + self.observers_lock = threading.Lock() self.resource = resource def collect(self) -> None: @@ -294,7 +320,12 @@ def collect(self) -> None: def _collect_metrics(self) -> None: for metric in self.metrics: - if metric.enabled: + if not metric.enabled: + continue + + to_remove = [] + + with metric.bound_instruments_lock: for label_set, bound_instr in metric.bound_instruments.items(): # TODO: Consider storing records in memory? record = Record(metric, label_set, bound_instr.aggregator) @@ -302,18 +333,26 @@ def _collect_metrics(self) -> None: # Applies different batching logic based on type of batcher self.batcher.process(record) + if bound_instr.ref_count() == 0: + to_remove.append(label_set) + + # Remove handles that were released + for label_set in to_remove: + del metric.bound_instruments[label_set] + def _collect_observers(self) -> None: - for observer in self.observers: - if not observer.enabled: - continue + with self.observers_lock: + for observer in self.observers: + if not observer.enabled: + continue - # TODO: capture timestamp? - if not observer.run(): - continue + # TODO: capture timestamp? + if not observer.run(): + continue - for label_set, aggregator in observer.aggregators.items(): - record = Record(observer, label_set, aggregator) - self.batcher.process(record) + for label_set, aggregator in observer.aggregators.items(): + record = Record(observer, label_set, aggregator) + self.batcher.process(record) def record_batch( self, @@ -368,9 +407,14 @@ def register_observer( label_keys, enabled, ) - self.observers.add(ob) + with self.observers_lock: + self.observers.add(ob) return ob + def unregister_observer(self, observer: "Observer") -> None: + with self.observers_lock: + self.observers.remove(observer) + def get_label_set(self, labels: Dict[str, str]): """See `opentelemetry.metrics.Meter.create_metric`. diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 4e7e532d86..dc09091c35 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -49,7 +49,7 @@ def test_collect(self): ) kvp = {"key1": "value1"} label_set = meter.get_label_set(kvp) - counter.add(label_set, 1.0) + counter.add(1.0, label_set) meter.metrics.add(counter) meter.collect() self.assertTrue(batcher_mock.process.called) @@ -179,6 +179,18 @@ def test_register_observer(self): self.assertEqual(observer.label_keys, ()) self.assertTrue(observer.enabled) + def test_unregister_observer(self): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = mock.Mock() + + observer = meter.register_observer( + callback, "name", "desc", "unit", int, (), True + ) + + meter.unregister_observer(observer) + self.assertEqual(len(meter.observers), 0) + def test_get_label_set(self): meter = metrics.MeterProvider().get_meter(__name__) kvp = {"environment": "staging", "a": "z"} @@ -193,6 +205,64 @@ def test_get_label_set_empty(self): label_set = meter.get_label_set(kvp) self.assertEqual(label_set, metrics.EMPTY_LABEL_SET) + def test_direct_call_release_bound_instrument(self): + meter = metrics.MeterProvider().get_meter(__name__) + label_keys = ("key1",) + kvp = {"key1": "value1"} + label_set = meter.get_label_set(kvp) + + counter = metrics.Counter( + "name", "desc", "unit", float, meter, label_keys + ) + meter.metrics.add(counter) + counter.add(4.0, label_set) + + measure = metrics.Measure( + "name", "desc", "unit", float, meter, label_keys + ) + meter.metrics.add(measure) + measure.record(42.0, label_set) + + self.assertEqual(len(counter.bound_instruments), 1) + self.assertEqual(len(measure.bound_instruments), 1) + + meter.collect() + + self.assertEqual(len(counter.bound_instruments), 0) + self.assertEqual(len(measure.bound_instruments), 0) + + def test_release_bound_instrument(self): + meter = metrics.MeterProvider().get_meter(__name__) + label_keys = ("key1",) + kvp = {"key1": "value1"} + label_set = meter.get_label_set(kvp) + + counter = metrics.Counter( + "name", "desc", "unit", float, meter, label_keys + ) + meter.metrics.add(counter) + bound_counter = counter.bind(label_set) + bound_counter.add(4.0) + + measure = metrics.Measure( + "name", "desc", "unit", float, meter, label_keys + ) + meter.metrics.add(measure) + bound_measure = measure.bind(label_set) + bound_measure.record(42) + + bound_counter.release() + bound_measure.release() + + # be sure that bound instruments are only released after collection + self.assertEqual(len(counter.bound_instruments), 1) + self.assertEqual(len(measure.bound_instruments), 1) + + meter.collect() + + self.assertEqual(len(counter.bound_instruments), 0) + self.assertEqual(len(measure.bound_instruments), 0) + class TestMetric(unittest.TestCase): def test_bind(self): From b615804db876bc725ec40afb1325c6c2ee398997 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Wed, 18 Mar 2020 13:25:50 -0700 Subject: [PATCH 0245/1517] Correlation context docs (#490) Co-authored-by: Alex Boten Co-authored-by: Diego Hurtado --- docs/api/api.rst | 1 + docs/api/correlationcontext.propagation.rst | 7 +++++++ docs/api/correlationcontext.rst | 14 ++++++++++++++ .../opentelemetry/correlationcontext/__init__.py | 8 +++++--- .../correlationcontext/propagation/__init__.py | 9 +++++---- 5 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 docs/api/correlationcontext.propagation.rst create mode 100644 docs/api/correlationcontext.rst diff --git a/docs/api/api.rst b/docs/api/api.rst index 8af950652f..e1e8211429 100644 --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -8,5 +8,6 @@ OpenTelemetry Python API configuration context + correlationcontext metrics trace diff --git a/docs/api/correlationcontext.propagation.rst b/docs/api/correlationcontext.propagation.rst new file mode 100644 index 0000000000..a9b94aa4fb --- /dev/null +++ b/docs/api/correlationcontext.propagation.rst @@ -0,0 +1,7 @@ +opentelemetry.correlationcontext.propagation package +==================================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.correlationcontext.propagation diff --git a/docs/api/correlationcontext.rst b/docs/api/correlationcontext.rst new file mode 100644 index 0000000000..10e7b2e573 --- /dev/null +++ b/docs/api/correlationcontext.rst @@ -0,0 +1,14 @@ +opentelemetry.correlationcontext package +======================================== + +Subpackages +----------- + +.. toctree:: + + correlationcontext.propagation + +Module contents +--------------- + +.. automodule:: opentelemetry.correlationcontext diff --git a/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py b/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py index b0b3624d72..543c175dfe 100644 --- a/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import abc import typing from opentelemetry.context import get_value, set_value @@ -24,7 +23,7 @@ def get_correlations( context: typing.Optional[Context] = None, ) -> typing.Dict[str, object]: - """ Returns the name/value pairs in the CorrelationContext + """Returns the name/value pairs in the CorrelationContext Args: context: The Context to use. If not set, uses current Context @@ -41,7 +40,8 @@ def get_correlations( def get_correlation( name: str, context: typing.Optional[Context] = None ) -> typing.Optional[object]: - """ Provides access to the value for a name/value pair in the CorrelationContext + """Provides access to the value for a name/value pair in the + CorrelationContext Args: name: The name of the value to retrieve @@ -76,6 +76,7 @@ def remove_correlation( name: str, context: typing.Optional[Context] = None ) -> Context: """Removes a value from the CorrelationContext + Args: name: The name of the value to remove context: The Context to use. If not set, uses current Context @@ -91,6 +92,7 @@ def remove_correlation( def clear_correlations(context: typing.Optional[Context] = None) -> Context: """Removes all values from the CorrelationContext + Args: context: The Context to use. If not set, uses current Context diff --git a/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py index 72ad80de1a..c336f4476b 100644 --- a/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import re import typing import urllib.parse @@ -36,9 +35,10 @@ def extract( carrier: httptextformat.HTTPTextFormatT, context: typing.Optional[Context] = None, ) -> Context: - """ Extract CorrelationContext from the carrier. + """Extract CorrelationContext from the carrier. - See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract` + See + `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract` """ if context is None: @@ -79,7 +79,8 @@ def inject( ) -> None: """Injects CorrelationContext into the carrier. - See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` + See + `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` """ correlations = correlationcontext.get_correlations(context=context) if not correlations: From 12b4d6a20ff5e79c96faae59c535324448e0c636 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 18 Mar 2020 13:59:01 -0700 Subject: [PATCH 0246/1517] Adding link to registry in readme (#507) Co-authored-by: Chris Kleinknecht --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 906481fec9..ae94f28910 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,10 @@ The online documentation is available at https://opentelemetry-python.readthedoc if you want to access the documentation for the latest version use https://opentelemetry-python.readthedocs.io/en/latest/. +## Compatible Exporters + +See the [OpenTelemetry registry](https://opentelemetry.io/registry/?s=python) for a list of exporters available. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) From 8aa58fbbd996ee24c91e21f8220ccc1cadf06ed3 Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 20 Mar 2020 10:49:55 -0700 Subject: [PATCH 0247/1517] Adding v0.5.0 notes to readme (#509) Co-authored-by: Chris Kleinknecht --- README.md | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ae94f28910..94305652e7 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,27 @@ Thank you to the following individuals for contributing to this release: * Mauricio Vásquez * Yusuke Tsutsumi +The [v0.5 beta +release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.5.0) release includes: + +- W3C Correlation Context Propagation +- OpenTelemetry Collector Exporter Integration for both metrics and traces +- Metrics SDK +- Global configuration module +- Documentation improvements + +Thank you to the following individuals for contributing to this release: + +* Alex Boten +* Chris Kleinknecht +* Dave Grochowski +* Diego Hurtado +* Hector Hernandez +* Leighton Chen +* Liz Fong-Jones +* Mauricio Vásquez +* Yusuke Tsutsumi + See the [project milestones](https://github.com/open-telemetry/opentelemetry-python/milestones) for details on upcoming releases. The dates and features described here are @@ -158,11 +179,9 @@ estimates, and subject to change. Future releases targets include: -| Component | Version | Target Date | -| ----------------------------------- | ---------- | ------------ | -| W3C Correlation Context Propagation | Beta v1 | March 16 2020| -| Support for Tags/Baggage | Beta v1 | March 16 2020| -| gRPC Integrations | Beta v1 | March 16 2020| -| OpenTelemetry Collector Exporter | Beta v1 | March 16 2020| -| OpenCensus Bridge | Beta v1 | March 16 2020| -| Metrics SDK (Complete) | Beta v1 | March 16 2020| +| Component | Version | Target Date | +| ---------------------------------- | ------- | ------------- | + +| Stable API for metrics and tracing | Beta v2 | March 31 2020 | +| Support for Tags/Baggage | Beta v2 | March 31 2020 | +| gRPC Integration | Beta v2 | March 31 2020 | From a2a858fff51bac68d428c18f458c8fdfe8b35323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez?= Date: Mon, 23 Mar 2020 17:14:49 +0100 Subject: [PATCH 0248/1517] readme: Fix future releases table (#521) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel González Lopes --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 94305652e7..2bb9188b55 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,6 @@ Future releases targets include: | Component | Version | Target Date | | ---------------------------------- | ------- | ------------- | - | Stable API for metrics and tracing | Beta v2 | March 31 2020 | | Support for Tags/Baggage | Beta v2 | March 31 2020 | | gRPC Integration | Beta v2 | March 31 2020 | From 5fbc75eb33a892e8ad2557a372b07b2a23039ce5 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 23 Mar 2020 12:37:03 -0700 Subject: [PATCH 0249/1517] docs: Fix otcollector README (#500) Fixes % python -m readme_renderer ext/opentelemetry-ext-otcollector/README.rst :22: (ERROR/3) Unknown target name: "opentelemetry". --- ext/opentelemetry-ext-otcollector/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-otcollector/README.rst b/ext/opentelemetry-ext-otcollector/README.rst index 7cd35b3f8c..8c18b65223 100644 --- a/ext/opentelemetry-ext-otcollector/README.rst +++ b/ext/opentelemetry-ext-otcollector/README.rst @@ -95,4 +95,4 @@ References ---------- * `OpenTelemetry Collector `_ -* `OpenTelemetry Project `_ +* `OpenTelemetry `_ From 929adcd17731b21cc78fd59d174727b177cef3e1 Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Mon, 23 Mar 2020 21:04:22 +0100 Subject: [PATCH 0250/1517] api: Allow digit as first chr in vendor specific trace state key (#511) Allows vendor specific trace state keys to start with a digit as specified in: https://github.com/w3c/trace-context/blob/master/spec/20-http_header_format.md#key --- .../propagation/tracecontexthttptextformat.py | 2 +- .../test_tracecontexthttptextformat.py | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py index 28db4e4557..07d3ee05ab 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py @@ -33,7 +33,7 @@ _KEY_WITHOUT_VENDOR_FORMAT = r"[a-z][_0-9a-z\-\*\/]{0,255}" _KEY_WITH_VENDOR_FORMAT = ( - r"[a-z][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}" + r"[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}" ) _KEY_FORMAT = _KEY_WITHOUT_VENDOR_FORMAT + "|" + _KEY_WITH_VENDOR_FORMAT diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 6ee4a957d2..a13d9630ae 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -222,3 +222,32 @@ def test_tracestate_header_with_trailing_comma(self): ) ) self.assertEqual(span.get_context().trace_state["foo"], "1") + + def test_tracestate_keys(self): + """Test for valid key patterns in the tracestate + """ + tracestate_value = ",".join( + [ + "1a-2f@foo=bar1", + "1a-_*/2b@foo=bar2", + "foo=bar3", + "foo-_*/bar=bar4", + ] + ) + span = get_span_from_context( + FORMAT.extract( + get_as_list, + { + "traceparent": [ + "00-12345678901234567890123456789012-1234567890123456-00" + ], + "tracestate": [tracestate_value], + }, + ) + ) + self.assertEqual(span.get_context().trace_state["1a-2f@foo"], "bar1") + self.assertEqual( + span.get_context().trace_state["1a-_*/2b@foo"], "bar2" + ) + self.assertEqual(span.get_context().trace_state["foo"], "bar3") + self.assertEqual(span.get_context().trace_state["foo-_*/bar"], "bar4") From fbfafa4712ae332416b11cbfec45133d4c29bb51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez?= Date: Tue, 24 Mar 2020 00:02:41 +0100 Subject: [PATCH 0251/1517] build: Add caching for pip files (#520) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caches lets Travis CI store directories between builds, which is useful for storing dependencies that take longer to compile or download. -Travis CI Docs Signed-off-by: Daniel González Lopes danielgonzalezlopes@gmail.com --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 67e3a58da1..b531cfb338 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,8 @@ dist: xenial language: python +cache: pip + python: - '3.4' - '3.5' From 04a9533ec6b8f5bbe2d41efc1ba1e92e51f1ca74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 24 Mar 2020 16:45:54 -0500 Subject: [PATCH 0252/1517] docs: Reorganize examples and improve ext packages documentation (#483) This commit moves the text on the readmes of the external packages to their source code as a docstring. This improves the generated documentation and helps to consolidate the documentation in a single place. Also unify the readmes present on each package to include a 1 line description, installation instructions and a link to the online documentation. This small readme will be the one shown on Github and on PyPI. Also change the examples structure to have the following examples: - basic meter & tracer - http integration - jaeger, prometheus, otcollector{tracer, metrics} - opentracing shim Co-authored-by: alrex Co-authored-by: Diego Hurtado --- docs-requirements.txt | 1 + docs/examples/basic_meter/README.rst | 42 ++++++++++ .../basic_metrics.py} | 30 +++---- .../calling_conventions.py} | 57 ++++++------- .../observer.py} | 7 +- docs/examples/basic_tracer/README.rst | 61 +++----------- docs/examples/basic_tracer/tracer.py | 26 +----- docs/examples/http/README.rst | 62 +++++--------- .../http/{tracer_client.py => client.py} | 15 +--- docs/examples/http/server.py | 16 +--- docs/examples/http/tests/test_http.py | 2 +- docs/examples/jaeger_exporter/README.rst | 46 +++++++++++ .../jaeger_exporter/jaeger_exporter.py | 46 +++++++++++ docs/examples/opentracing/README.rst | 42 ++++++---- .../images/jaeger-span-expanded.png | Bin 171564 -> 0 bytes .../opentracing/images/jaeger-trace-full.png | Bin 164051 -> 0 bytes docs/examples/otcollector-metrics/README.rst | 51 ++++++++++++ .../otcollector-metrics}/collector.py | 34 ++++---- .../docker/collector-config.yaml | 0 .../docker/docker-compose.yaml | 0 .../docker/prometheus.yaml | 0 docs/examples/otcollector-tracer/README.rst | 50 ++++++++++++ docs/examples/otcollector-tracer/collector.py | 36 +++++++++ .../docker/collector-config.yaml | 0 .../docker/docker-compose.yaml | 0 docs/examples/prometheus/README.rst | 34 ++++++++ .../{metrics => prometheus}/prometheus.py | 35 ++++---- docs/ext/dbapi/dbapi.rst | 7 +- docs/ext/flask/flask.rst | 7 +- docs/ext/http_requests/http_requests.rst | 6 +- docs/ext/jaeger/jaeger.rst | 7 +- docs/ext/mysql/mysql.rst | 7 +- docs/ext/otcollector/otcollector.rst | 7 +- docs/ext/prometheus/prometheus.rst | 7 +- docs/ext/psycopg2/psycopg2.rst | 7 +- docs/ext/pymongo/pymongo.rst | 6 +- docs/ext/wsgi/wsgi.rst | 7 +- docs/index.rst | 2 +- ext/opentelemetry-ext-dbapi/README.rst | 26 ++---- .../src/opentelemetry/ext/dbapi/__init__.py | 26 +++++- ext/opentelemetry-ext-flask/README.rst | 33 +++----- .../src/opentelemetry/ext/flask/__init__.py | 31 +++++++ .../README.rst | 27 +------ .../ext/http_requests/__init__.py | 28 ++++++- ext/opentelemetry-ext-jaeger/README.rst | 46 +---------- .../src/opentelemetry/ext/jaeger/__init__.py | 48 ++++++++++- ext/opentelemetry-ext-mysql/README.rst | 30 +++---- .../src/opentelemetry/ext/mysql/__init__.py | 25 +++++- .../README.rst | 3 +- ext/opentelemetry-ext-otcollector/README.rst | 76 +----------------- .../opentelemetry/ext/otcollector/__init__.py | 73 +++++++++++++++++ ext/opentelemetry-ext-prometheus/README.rst | 51 +----------- .../opentelemetry/ext/prometheus/__init__.py | 54 ++++++++++++- ext/opentelemetry-ext-psycopg2/README.rst | 28 +++---- .../opentelemetry/ext/psycopg2/__init__.py | 28 ++++++- ext/opentelemetry-ext-pymongo/README.rst | 26 +++--- .../src/opentelemetry/ext/pymongo/__init__.py | 24 +++++- ext/opentelemetry-ext-wsgi/README.rst | 36 +-------- .../src/opentelemetry/ext/wsgi/__init__.py | 40 ++++++++- ext/opentelemetry-ext-zipkin/README.rst | 45 +---------- .../src/opentelemetry/ext/zipkin/__init__.py | 57 ++++++++++++- tox.ini | 1 + 62 files changed, 945 insertions(+), 680 deletions(-) create mode 100644 docs/examples/basic_meter/README.rst rename docs/examples/{metrics/simple_example.py => basic_meter/basic_metrics.py} (91%) rename docs/examples/{metrics/record.py => basic_meter/calling_conventions.py} (64%) rename docs/examples/{metrics/observer_example.py => basic_meter/observer.py} (94%) rename docs/examples/http/{tracer_client.py => client.py} (79%) create mode 100644 docs/examples/jaeger_exporter/README.rst create mode 100644 docs/examples/jaeger_exporter/jaeger_exporter.py delete mode 100644 docs/examples/opentracing/images/jaeger-span-expanded.png delete mode 100644 docs/examples/opentracing/images/jaeger-trace-full.png create mode 100644 docs/examples/otcollector-metrics/README.rst rename {examples/metrics => docs/examples/otcollector-metrics}/collector.py (60%) rename {examples/metrics => docs/examples/otcollector-metrics}/docker/collector-config.yaml (100%) rename {examples/metrics => docs/examples/otcollector-metrics}/docker/docker-compose.yaml (100%) rename {examples/metrics => docs/examples/otcollector-metrics}/docker/prometheus.yaml (100%) create mode 100644 docs/examples/otcollector-tracer/README.rst create mode 100644 docs/examples/otcollector-tracer/collector.py rename docs/examples/{basic_tracer => otcollector-tracer}/docker/collector-config.yaml (100%) rename docs/examples/{basic_tracer => otcollector-tracer}/docker/docker-compose.yaml (100%) create mode 100644 docs/examples/prometheus/README.rst rename docs/examples/{metrics => prometheus}/prometheus.py (53%) diff --git a/docs-requirements.txt b/docs-requirements.txt index 41600cfe8c..2b8f732401 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -11,3 +11,4 @@ flask~=1.0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 psycopg2-binary >= 2.7.3.1 +prometheus_client >= 0.5.0, < 1.0.0 diff --git a/docs/examples/basic_meter/README.rst b/docs/examples/basic_meter/README.rst new file mode 100644 index 0000000000..a48e5bb612 --- /dev/null +++ b/docs/examples/basic_meter/README.rst @@ -0,0 +1,42 @@ +Basic Meter +=========== + +These examples show how to use OpenTelemetry to capture and report metrics. + +There are three different examples: + +* basic_metrics: Shows to how create a metric instrument, how to configure an + exporter and a controller and also how to capture data by using the direct + calling convention. + +* calling_conventions: Shows how to use the direct, bound and batch calling conventions. + +* observer: Shows how to use the observer instrument. + +The source files of these examples are available :scm_web:`here `. + +Installation +------------ + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + pip install psutil # needed to get ram and cpu usage in the observer example + +Run the Example +--------------- + +.. code-block:: sh + + python .py + +The output will be shown in the console after few seconds. + +Useful links +------------ + +- OpenTelemetry_ +- :doc:`../../api/metrics` + +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/docs/examples/metrics/simple_example.py b/docs/examples/basic_meter/basic_metrics.py similarity index 91% rename from docs/examples/metrics/simple_example.py rename to docs/examples/basic_meter/basic_metrics.py index a0152b29f3..ac06284c32 100644 --- a/docs/examples/metrics/simple_example.py +++ b/docs/examples/basic_meter/basic_metrics.py @@ -13,7 +13,8 @@ # limitations under the License. # """ -This module serves as an example for a simple application using metrics +This module serves as an example for a simple application using metrics. + It shows: - How to configure a meter passing a sateful or stateless. - How to configure an exporter and how to create a controller. @@ -27,7 +28,7 @@ from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController -batcher_mode = "stateful" +stateful = True def usage(argv): @@ -42,6 +43,11 @@ def usage(argv): print("bad mode specified.") usage(sys.argv) sys.exit(1) + stateful = batcher_mode == "stateful" + +print( + "Starting example, values will be printed to the console every 5 seconds." +) # The Meter is responsible for creating and recording metrics. Each meter has a @@ -57,7 +63,7 @@ def usage(argv): # A PushController collects metrics created from meter and exports it via the # exporter every interval -controller = PushController(meter, exporter, 5) +controller = PushController(meter=meter, exporter=exporter, interval=5) # Metric instruments allow to capture measurements requests_counter = meter.create_metric( @@ -69,15 +75,6 @@ def usage(argv): label_keys=("environment",), ) -clicks_counter = meter.create_metric( - name="clicks", - description="number of clicks", - unit="1", - value_type=int, - metric_type=Counter, - label_keys=("environment",), -) - requests_size = meter.create_metric( name="requests_size", description="size of requests", @@ -94,17 +91,14 @@ def usage(argv): testing_label_set = meter.get_label_set({"environment": "testing"}) # Update the metric instruments using the direct calling convention -requests_size.record(100, staging_label_set) requests_counter.add(25, staging_label_set) +requests_size.record(100, staging_label_set) time.sleep(5) -requests_size.record(5000, staging_label_set) requests_counter.add(50, staging_label_set) +requests_size.record(5000, staging_label_set) time.sleep(5) -requests_size.record(2, testing_label_set) requests_counter.add(35, testing_label_set) -time.sleep(5) - -clicks_counter.add(5, staging_label_set) +requests_size.record(2, testing_label_set) time.sleep(5) diff --git a/docs/examples/metrics/record.py b/docs/examples/basic_meter/calling_conventions.py similarity index 64% rename from docs/examples/metrics/record.py rename to docs/examples/basic_meter/calling_conventions.py index e1c7901fa0..946dccda62 100644 --- a/docs/examples/metrics/record.py +++ b/docs/examples/basic_meter/calling_conventions.py @@ -13,28 +13,23 @@ # limitations under the License. # """ -This module serves as an example for a simple application using metrics. -It demonstrates the different ways you can record metrics via the meter. +This example shows how to use the different modes to capture metrics. +It shows the usage of the direct, bound and batch calling conventions. """ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController # Use the meter type provided by the SDK package metrics.set_meter_provider(MeterProvider()) -# Meter is responsible for creating and recording metrics meter = metrics.get_meter(__name__) -# exporter to export metrics to the console exporter = ConsoleMetricsExporter() -# controller collects metrics created from meter and exports it via the -# exporter every interval controller = PushController(meter=meter, exporter=exporter, interval=5) -# Example to show how to record using the meter -counter = meter.create_metric( +requests_counter = meter.create_metric( name="requests", description="number of requests", unit="1", @@ -43,7 +38,16 @@ label_keys=("environment",), ) -counter2 = meter.create_metric( +requests_size = meter.create_metric( + name="requests_size", + description="size of requests", + unit="1", + value_type=int, + metric_type=Measure, + label_keys=("environment",), +) + +clicks_counter = meter.create_metric( name="clicks", description="number of clicks", unit="1", @@ -52,36 +56,27 @@ label_keys=("environment",), ) -# Labelsets are used to identify key-values that are associated with a specific -# metric that you want to record. These are useful for pre-aggregation and can -# be used to store custom dimensions pertaining to a metric - -# The meter takes a dictionary of key value pairs label_set = meter.get_label_set({"environment": "staging"}) -# Bound instrument usage +print("Updating using direct calling convention...") +# You can record metrics directly using the metric instrument. You pass in a +# labelset that you would like to record for. +requests_counter.add(25, label_set) +time.sleep(5) +print("Updating using a bound instrument...") # You can record metrics with bound metric instruments. Bound metric # instruments are created by passing in a labelset. A bound metric instrument # is essentially metric data that corresponds to a specific set of labels. # Therefore, getting a bound metric instrument using the same set of labels # will yield the same bound metric instrument. -bound_counter = counter.bind(label_set) -for i in range(1000): - bound_counter.add(i) - -# You can release the bound instrument we you are done -bound_counter.release() - -# Direct metric usage -# You can record metrics directly using the metric instrument. You pass in a -# labelset that you would like to record for. -counter.add(25, label_set) +bound_requests_counter = requests_counter.bind(label_set) +bound_requests_counter.add(100) +time.sleep(5) -# Record batch usage +print("Updating using batch calling convention...") # You can record metrics in a batch by passing in a labelset and a sequence of # (metric, value) pairs. The value would be recorded for each metric using the # specified labelset for each. -meter.record_batch(label_set, [(counter, 50), (counter2, 70)]) - -time.sleep(10) +meter.record_batch(label_set, ((requests_counter, 50), (clicks_counter, 70))) +time.sleep(5) diff --git a/docs/examples/metrics/observer_example.py b/docs/examples/basic_meter/observer.py similarity index 94% rename from docs/examples/metrics/observer_example.py rename to docs/examples/basic_meter/observer.py index 563d503c3e..2b459f25be 100644 --- a/docs/examples/metrics/observer_example.py +++ b/docs/examples/basic_meter/observer.py @@ -26,14 +26,9 @@ # Configure a stateful batcher batcher = UngroupedBatcher(stateful=True) - metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__) - -# Exporter to export metrics to the console exporter = ConsoleMetricsExporter() - -# Configure a push controller controller = PushController(meter=meter, exporter=exporter, interval=2) @@ -69,4 +64,4 @@ def get_ram_usage_callback(observer): label_keys=(), ) -input("Press a key to finish...\n") +input("Metrics will be printed soon. Press a key to finish...\n") diff --git a/docs/examples/basic_tracer/README.rst b/docs/examples/basic_tracer/README.rst index 2cc2d0c6a6..47b16074c5 100644 --- a/docs/examples/basic_tracer/README.rst +++ b/docs/examples/basic_tracer/README.rst @@ -2,24 +2,25 @@ Basic Tracer ============ This example shows how to use OpenTelemetry to instrument a Python application - e.g. a batch job. -It supports exporting spans either to the console or to Jaeger_. -The source files required to run this example are available :scm_web:`here `. +The source files of this example are available :scm_web:`here `. +Installation +------------ -Run the application -------------------- +.. code-block:: sh -Console -******* + pip install opentelemetry-api + pip install opentelemetry-sdk -* Run the sample +Run the Example +--------------- .. code-block:: sh - $ python tracer.py + python tracer.py -The output will be displayed at the console +The output will be displayed in the console: :: @@ -29,48 +30,10 @@ The output will be displayed at the console Span(name="foo", context=SpanContext(trace_id=0xf906f80f64d57c71ea8da4dfbbd2ddf2, span_id=0x1d5d87441ec2f410, trace_state={}), kind=SpanKind.INTERNAL, parent=None, start_time=2019-11-07T21:26:45.934369Z, end_time=2019-11-07T21:26:45.934580Z) -Jaeger -****** - -Setup `Jaeger Tracing `_. - -* Run the sample - -.. code-block:: sh - - $ pip install opentelemetry-ext-jaeger - $ EXPORTER=jaeger python tracer.py - - -The traces should be available in the Jaeger UI at ``_ - - -Collector -********* - -* Start Collector - -.. code-block:: sh - - $ pip install docker-compose - $ cd docker - $ docker-compose up - -* Run the sample - -.. code-block:: sh - - $ pip install opentelemetry-ext-otcollector - $ EXPORTER=collector python tracer.py - - -Collector is configured to export to Jaeger, follow Jaeger UI instructions to find the traces. - Useful links ------------ -- For more information on OpenTelemetry, visit OpenTelemetry_. -- For more information on tracing in Python, visit Jaeger_. +- OpenTelemetry_ +- :doc:`../../api/trace` -.. _Jaeger: https://www.jaegertracing.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ \ No newline at end of file diff --git a/docs/examples/basic_tracer/tracer.py b/docs/examples/basic_tracer/tracer.py index 8982e5cd7b..51b2e69b12 100755 --- a/docs/examples/basic_tracer/tracer.py +++ b/docs/examples/basic_tracer/tracer.py @@ -23,35 +23,17 @@ ConsoleSpanExporter, ) -if os.getenv("EXPORTER") == "jaeger": - from opentelemetry.ext.jaeger import JaegerSpanExporter - - print("Using JaegerSpanExporter") - exporter = JaegerSpanExporter( - service_name="basic-service", - agent_host_name="localhost", - agent_port=6831, - ) -elif os.getenv("EXPORTER") == "collector": - from opentelemetry.ext.otcollector.trace_exporter import ( - CollectorSpanExporter, - ) - - print("Using CollectorSpanExporter") - exporter = CollectorSpanExporter( - service_name="basic-service", endpoint="localhost:55678" - ) -else: - print("Using ConsoleSpanExporter") - exporter = ConsoleSpanExporter() - +# The preferred tracer implementation must be set, as the opentelemetry-api +# defines the interface with a no-op implementation. trace.set_tracer_provider(TracerProvider()) + # We tell OpenTelemetry who it is that is creating spans. In this case, we have # no real name (no setup.py), so we make one up. If we had a version, we would # also specify it here. tracer = trace.get_tracer(__name__) # SpanExporter receives the spans and send them to the target location. +exporter = ConsoleSpanExporter() span_processor = BatchExportSpanProcessor(exporter) trace.get_tracer_provider().add_span_processor(span_processor) diff --git a/docs/examples/http/README.rst b/docs/examples/http/README.rst index 2d7ef887d7..c462d9f092 100644 --- a/docs/examples/http/README.rst +++ b/docs/examples/http/README.rst @@ -1,53 +1,50 @@ -HTTP Example -============ +HTTP Integration Example +======================== This example shows how to use -`OpenTelemetryMiddleware `_ -and `requests `_ integrations to instrument a client and a server in Python. -It supports exporting spans either to the console or to Jaeger_. +:doc:`WSGI Middleware <../../ext/wsgi/wsgi>` +and :doc:`requests <../../ext/http_requests/http_requests>` integrations to +instrument an HTTP client and server in Python. The source files required to run this example are available :scm_web:`here `. - Installation ------------ .. code-block:: sh - $ pip install opentelemetry-api - $ pip install opentelemetry-sdk - $ pip install opentelemetry-ext-wsgi - $ pip install opentelemetry-ext-http-requests - + pip install opentelemetry-api + pip install opentelemetry-sdk + pip install opentelemetry-ext-wsgi + pip install opentelemetry-ext-http-requests + pip install flask -Run the application -------------------- -Console -******* +Run the Example +--------------- * Run the server .. code-block:: sh - $ python server.py + python server.py * Run the client from a different terminal .. code-block:: sh - $ python tracer_client.py + python client.py -The output will be displayed at the console on the client side +The output will be displayed in the console on the client side: :: Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x3703fd889dcdeb2b, trace_state={}), kind=SpanKind.CLIENT, parent=None, start_time=2019-11-07T21:52:59.591634Z, end_time=2019-11-07T21:53:00.386014Z) -And on the server +And on the server: :: @@ -57,31 +54,12 @@ And on the server Span(name="/", context=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x36050ac596949bc1, trace_state={}), kind=SpanKind.SERVER, parent=SpanContext(trace_id=0x7c5c0d62031570f00fd106d968139300, span_id=0x3703fd889dcdeb2b, trace_state={}), start_time=2019-11-07T21:52:59.600816Z, end_time=2019-11-07T21:53:00.385322Z) -Jaeger -****** - -Setup `Jaeger Tracing `_. - -* Run the server - -.. code-block:: sh - - $ pip install opentelemetry-ext-jaeger - $ EXPORTER=jaeger python server.py - -* Run the client from a different terminal - -.. code-block:: sh - - $ EXPORTER=jaeger python tracer_client.py - -The traces should be available in the Jaeger UI at ``_ - Useful links ------------ -- For more information on OpenTelemetry, visit OpenTelemetry_. -- For more information on tracing in Python, visit Jaeger_. +- OpenTelemetry_ +- :doc:`../../api/trace` +- :doc:`../../ext/wsgi/wsgi` +- :doc:`../../ext/http_requests/http_requests` -.. _Jaeger: https://www.jaegertracing.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ \ No newline at end of file diff --git a/docs/examples/http/tracer_client.py b/docs/examples/http/client.py similarity index 79% rename from docs/examples/http/tracer_client.py rename to docs/examples/http/client.py index 0e83c5c53f..7c5aaa4130 100755 --- a/docs/examples/http/tracer_client.py +++ b/docs/examples/http/client.py @@ -26,21 +26,12 @@ ConsoleSpanExporter, ) -if os.getenv("EXPORTER") == "jaeger": - from opentelemetry.ext.jaeger import JaegerSpanExporter - - exporter = JaegerSpanExporter( - service_name="http-client", - agent_host_name="localhost", - agent_port=6831, - ) -else: - exporter = ConsoleSpanExporter() - +# The preferred tracer implementation must be set, as the opentelemetry-api +# defines the interface with a no-op implementation. trace.set_tracer_provider(TracerProvider()) tracer_provider = trace.get_tracer_provider() -# SpanExporter receives the spans and send them to the target location. +exporter = ConsoleSpanExporter() span_processor = BatchExportSpanProcessor(exporter) tracer_provider.add_span_processor(span_processor) diff --git a/docs/examples/http/server.py b/docs/examples/http/server.py index a665abdd49..ab066e99bf 100755 --- a/docs/examples/http/server.py +++ b/docs/examples/http/server.py @@ -28,20 +28,12 @@ ConsoleSpanExporter, ) -if os.getenv("EXPORTER") == "jaeger": - from opentelemetry.ext.jaeger import JaegerSpanExporter - - exporter = JaegerSpanExporter( - service_name="http-server", - agent_host_name="localhost", - agent_port=6831, - ) -else: - exporter = ConsoleSpanExporter() - +# The preferred tracer implementation must be set, as the opentelemetry-api +# defines the interface with a no-op implementation. +trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) -# SpanExporter receives the spans and send them to the target location. +exporter = ConsoleSpanExporter() span_processor = BatchExportSpanProcessor(exporter) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor(span_processor) diff --git a/docs/examples/http/tests/test_http.py b/docs/examples/http/tests/test_http.py index 0ae81fe7de..dec0ce5f38 100644 --- a/docs/examples/http/tests/test_http.py +++ b/docs/examples/http/tests/test_http.py @@ -28,7 +28,7 @@ def setup_class(cls): def test_http(self): dirpath = os.path.dirname(os.path.realpath(__file__)) - test_script = "{}/../tracer_client.py".format(dirpath) + test_script = "{}/../client.py".format(dirpath) output = subprocess.check_output( (sys.executable, test_script) ).decode() diff --git a/docs/examples/jaeger_exporter/README.rst b/docs/examples/jaeger_exporter/README.rst new file mode 100644 index 0000000000..d4e20bad3f --- /dev/null +++ b/docs/examples/jaeger_exporter/README.rst @@ -0,0 +1,46 @@ +Jaeger Exporter Example +======================= + +This example shows how to use OpenTelemetry to send tracing data to Jaeger. + +The source files of this example are available :scm_web:`here `. + +Installation +------------ + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + pip install opentelemetry-ext-jaeger + +Run the Example +--------------- + +* Start Jaeger + +.. code-block:: sh + + docker run --rm \ + -p 6831:6831/udp \ + -p 6832:6832/udp \ + -p 16686:16686 \ + jaegertracing/all-in-one:1.13 \ + --log-level=debug + +* Run the example + +.. code-block:: sh + + python jaeger_exporter.py + +The traces will be available at http://localhost:16686/. + +Useful links +------------ + +- OpenTelemetry_ +- :doc:`../../api/trace` +- :doc:`../../ext/jaeger/jaeger` + +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ \ No newline at end of file diff --git a/docs/examples/jaeger_exporter/jaeger_exporter.py b/docs/examples/jaeger_exporter/jaeger_exporter.py new file mode 100644 index 0000000000..f8914c353a --- /dev/null +++ b/docs/examples/jaeger_exporter/jaeger_exporter.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from opentelemetry import trace +from opentelemetry.ext.jaeger import JaegerSpanExporter +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer(__name__) + +exporter = JaegerSpanExporter( + service_name="my-helloworld-service", + # configure agent + agent_host_name="localhost", + agent_port=6831, + # optional: configure also collector + # collector_host_name="localhost", + # collector_port=14268, + # collector_endpoint="/api/traces?format=jaeger.thrift", + # username=xxxx, # optional + # password=xxxx, # optional +) + +span_processor = BatchExportSpanProcessor(exporter) +trace.get_tracer_provider().add_span_processor(span_processor) + +with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): + print("Hello world from OpenTelemetry Python!") diff --git a/docs/examples/opentracing/README.rst b/docs/examples/opentracing/README.rst index 0305da9955..06c767b199 100644 --- a/docs/examples/opentracing/README.rst +++ b/docs/examples/opentracing/README.rst @@ -1,8 +1,8 @@ OpenTracing Shim Example ========================== -This example shows how to use the `opentelemetry-ext-opentracing-shim -package `_ +This example shows how to use the :doc:`opentelemetry-ext-opentracing-shim +package <../../ext/opentracing_shim/opentracing_shim>` to interact with libraries instrumented with `opentracing-python `_. @@ -21,7 +21,16 @@ Installation Jaeger ****** -Setup `Jaeger Tracing `_. +Start Jaeger + +.. code-block:: sh + + docker run --rm \ + -p 6831:6831/udp \ + -p 6832:6832/udp \ + -p 16686:16686 \ + jaegertracing/all-in-one:1.13 \ + --log-level=debug Redis ***** @@ -32,30 +41,30 @@ Make sure that the Redis server is running by executing this: .. code-block:: sh - $ redis-server + redis-server Python Dependencies ******************* -Install the Python dependencies in :scm_raw_web:`requirements.txt ` +Install the Python dependencies in :scm_raw_web:`requirements.txt ` .. code-block:: sh - $ pip install -r requirements.txt + pip install -r requirements.txt Alternatively, you can install the Python dependencies separately: .. code-block:: sh - $ pip install \ - opentelemetry-api \ - opentelemetry-sdk \ - opentelemetry-ext-jaeger \ - opentelemetry-opentracing-shim \ - redis \ - redis_opentracing + pip install \ + opentelemetry-api \ + opentelemetry-sdk \ + opentelemetry-ext-jaeger \ + opentelemetry-opentracing-shim \ + redis \ + redis_opentracing Run the Application @@ -69,7 +78,7 @@ To run the script: .. code-block:: sh - $ python main.py + python main.py After running, you can view the generated trace in the Jaeger UI. @@ -90,8 +99,7 @@ from both tracing systems appear in the exported trace. Useful links ------------ -- For more information on OpenTelemetry, visit OpenTelemetry_. -- For more information on tracing in Python, visit Jaeger_. +- OpenTelemetry_ +- :doc:`../../ext/opentracing_shim/opentracing_shim` -.. _Jaeger: https://www.jaegertracing.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/docs/examples/opentracing/images/jaeger-span-expanded.png b/docs/examples/opentracing/images/jaeger-span-expanded.png deleted file mode 100644 index 6da4b4b014d5d95cc1989e8436513302a4f657fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 171564 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 NQdidbJzdGd`=2Fgo`e7Z diff --git a/docs/examples/opentracing/images/jaeger-trace-full.png b/docs/examples/opentracing/images/jaeger-trace-full.png deleted file mode 100644 index c28255bd1309182c0de47c7a8c0ed8fc9650d738..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 164051 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%0A1Mx;;G!u|Za9rL~ zmuR~>EJ^Ko^`_frY9y{Fj9|4pCUFwmq|)rul&04)_pdplmS% zLaj(%fB%t5b0NtN^X<2asV17FF2boNGkoAZ+*ZA(rhxe{(o^$UB@1#m3P=@1G>g^5 zO)BMEWEdfGK{OS|ZDJc^YLw{$CghB0XVC@88YX%r@_n}bxL<3s#&mEXGD&lI|XKdoXRV zz_2#%eyRv@zU<0R?)p0#ZUL_Q(7V0YD{ZmYA0IYzmX`a2QMQuq?C4XHxQ3axg873_ zRmi($o^ci_|N5b}_TrViw`BIzG~%MG4r|ZsHV}&1gp7+~oMfNMl8>AU_kNyxlq+uf z6!U4eqpUykc%(u5ps?NLNQP}YB96^_iRCoW5ASY>_>|kOHwazg_Xw0Z00}9w?CX%^E#aXj-ok^!d&` zogdeN#p>8aGb$|_vXy=YB-TPb@WiM)B6iGg9UQr~#cZZL;D|Lx*azQTK3@L$$RA03 z{|nYjkwVZ(^13v)H#?BdR{CBL2(A}0CeD$JRkAk&Ya+QSu;^v9W5oO-;MXPVu~erA zCtw}?fs@;R&aa!HFG�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%0Ao2j)UFb_;B9sKygjCajOgOhYI_doK2$Z~h4|GVp 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 z z{>1NytDq$kov&pt?1$5v& z11LTH)TYNCL`#_^f`Pm!z7`}Qr{c|Bel=H4wtRL1Q300 zAvEUsp?LtZkas9>-mWx?XKt7U7e5Q=DPv2SbeKp^+w}3$LPnx6-wI5?gH0EJFFYE+ zIw2<{>8MNe{>1ZHvfgvuGyACwNE*%KMN?&v^nl$v^NaM;3F=;CBX3>^WNl(qNu(|C z>I`I@I70H6w1C6b^JL4`xu>6C$}+Y#fxA3^c`M_R{hPfkvg$1MdeQr)k74 zd33oo-8(`xwaW_xJms*R0z*((9lIkRaR>P(5&#Ho9XMh`k|vp@{5r{cAK1u4T0R1g zCoJ=RRFm;n7$2%o_C@?()al{d-|NtT5I&DR+HxvOLel4XgjHZe%`X@1$Oz>sKst;} z5k`Bc08YRZZ;$K@h)_})FKKpI4Pbr$+lSJQjRx54ewkNwVErGvQ2edS^02xvhF~(w z^3gl->{{Em`>fLLrK;R3P}%@u&&-KyifL%T^v>k8(}(V|N4jWSzK1Ik8x=IAo?hyHLN}1e3r+68R);eCR+3c=d~8PzmaU z8|kR8G`3nIkp?94jUGb<2sct~G}3L$JS5JrLH)rCV7rTGngf-p3-jh%9@@6z%|}#n zgKEc60%zjyqVz0vYYcxvvjNyLpG#Hvx-u-@b;`I5NxgvrhNOu_&_CArWA=ba5#u6r z;CO82>Vs^MHNBBATg?PXI+CrV;UJ= zCRV&8jFs+)FjkY&D)7UJ!Rxf~9tgSyjL} zb$ekU=<0n87EBVYB2D__$}G0^2bz&TISVF+hm5T1JCPBw(05>FlO54zzM7aS=w=-k za`>@ap0ra&D0`|^(6-{%p$RdrpY@i~lNwIP9a+wcvYv^^dJB>5IX~{KwOzU{a9@2W zXpn==(_DeR)@teV+qdUhUBp##Avt0w?;Ou7WO<)Dop=aFm34^txy4@n& z?_tgw{oH|Pa%^-WDG$iKU?Y2&RgQBnf;+i(<2U|d1=z%UND&|2>!xE6z+oL#k?kUN zjQxn3iA{GmuUR)28+TbP zIO_1Thy$O&;$>hLq}btdE#+h1Y;fIr#-(%#le~xF2fGvHWu$m*L4YZ*#8>&~b>*Je zJW#u`VoCXjM}@hzio^lmU@|e%GRMj3gtFGdr5ZGG;TY8aER$!}akx)%3Y4P*eP=~1*W(AL?~7+G58Brg z7SU^<7`;X5EvW~fGr60hzI;yVS%DmT+22MN@OFZ3iV*vD;c0B1P0N|brU4n3 zYt*(w6su9}@%mTVdVu%(ArZIve{@Y&(Y9Tq*8jj3#%6xoKE4JQjgoZkQZ5{FIBni& zUO6dl^p`n3;hUq*<4v!Co(_DUo|@ysIY1%^C;->Nc5#uEgyhQOkF-CCQ?Ikr`XgMo zT6*hQ{n3u|flb$|8Nx{ox9)z0_nwauIB znb%Eo5?orn9Jn`rNUyj`Cy{Ec=?AwzZ(O^Xp6d2^W3_Q7>BS;O$z!3Non##~RqlZO zMV_XCI%|RZ_dmbGEUKe=>ZOclKoiy>%>Y7fwLo?THMiPBX-u4bsowjK^zDD1&0u^Q zPq6!JQlO)wONIWo>n{DzH{6n@zKL0O#B(H-JfCX+0rtz}b6zUdlS6l^8`^oiD$g6g z&+AXw&Zcvos_cJ=MK%(Xg_wZK=l&5xcAXqwtq!T{B^avFU-pui8$Dm@%M7VzkSaPzL(!} zM%s!7)*4Bl>Gue-{wx=(OQu6~|8h~E|Ff2X6Qvnqol^NXFFiJ5)c%tgf|G=I(wg!Q zGd!RhAC!a7mgvfa((+LOLJHE#LK%(;1Yz@hdFq`Bjg&voU~(eL4aHl*7FRPMx<>1P z)bb$cssm%dc~HfC2Q$1I;F308kJWi3PvC{83yqH7Kt_u_;2|HF!3#$@qLlYv1qhZE z5Ji472F%CQ;B)F2WBsRK0xu8v6eiENRUutJR0?3FPJl*vcRG#aaPt6`{K2NpZvaMv z5!hmJQIME03&o@=vcUsX3gUKjN-cauCpqLIG>fe*8l| zD3uLX0g>Gx0EqJ)d2oRqHy{VG0jO~#c?kIjtw5s{JId+(15QftbUC~|etz%W-lJ$d z9P+HLT-dRG)AuaZ(@c;u5E?!zAm9Fw>3puOlo!$jZ=>fbsrQr7#eH6w`wmE%EJKeJ zv`{#;biXOMRI^(b0jGa|IdZK$<--i*^1rAD z)hAu{^D_i0!;sU+hl(taL%axt3{~3AAQ)^ks{$bdvJ}BfD@e7WkKS{4LOIGjWPtd9 zla_V(X(4=e{(x3bppmipiUSPvcKyO7b?U6N7f|C?q5VkRXVoJ3O}kK$?!@Zpx*4UT zuTQvEPz312wQ3tbIn;27)5~2t*KH*2$gU@PsA3kW^+mcBAsKqe{hXF+@UfK(k6T4Bn=BBzZ&ujVMRM1m@5L(A+>= zwN?#Oh-nSS7lg3Qzu!2b7IR7*o#{<$V0c`)t3^T?i+C^5e7^j1YH(_~V%*QHyfzpi zJc|kGok4a-SN3t+4bTx&w7nAdFVDkyfg(vI+B1)$3c3(J5fECSL`n-w7yZ2tqw^z- z&hJ9Zg@Twyx#$!oN;T%>rSr%ZW@RCro37;+M>^N2L#csZFwg-h)Yw|O7$_@HxI#eZ zx@+k*h)>`nW_(cylq|RS)JY3t)}voEd8=QNnnJu|fZO%0n4}4b2VYznyYT3)C6m|| zgQzKa6`9h81WFo9?_(Gx=;&%()v?E&>ssce0`Rzg`s{--OTei@lC zHZ~=mD&lL9T2iJ&`A4=Yc3y&}%Q`4rFxzq+BqK&Bn-FO`xQ{YYboOg@qH}p;Hwx^| zw1pGvTP|=0ME~IofOq%mQr?6Yk%_>DP?5EMsdZ#-Ow4;QfwOay4-Uh<5_Xhm7P9N! zItuvWF%Xi*_~r(@l&=t-B~@o|VA|^nKzFS1_U76`f6UbUeW07ue;*xJcHrItDzj`FE-neiJryd&a0|s>7 zJjNbfUHX*;00Ha^nv#T2yBP5tL?h$z-ziS6{rW|_Kg#o>a~1m_ ziKq_EqAFy66f2$nbjm%iIIKKqzLnau(SlTc!KzVTAae2f(AEj}H<^oDs1QoE9Xq5# zul-_YS?^)2S6{-foT%81(GkFJ{S_070bHhxiVf?)jS-2 z6$of9=$$iY2g#wYZl^&yjpWk9k5#B@R7!s7oFyRMGG_7{BcR=$Sx@ zL=E`LtKNv5%&VMvjI@9q@x{j!`F+b7_v$ClNHX+#SGs0I-KOU$cn&Lw5yeG^fLb{$ z9<3^^cjXyNCtX33Z|2bNyhxyu;3HgLbhi+D?P*HYikl)lkhNWKCaF9%t?!b+SX;ke z!|(txeY~y{r!x`X16QyMaDZJvXP#r@`1S7}#-G@IS7f2*(D=MYax?ODdw;6c<225p z1`YY9dcwrgfLiWq=@*Jya(=F&=B*L!_2kovFW87Ryub6+;Trph${t9=C1*|l)L8BI z@!kbny*FIu!%D6vR7RWz?Dn3wx?QmHvTDnjMM630)6*}$cO`0m#rXQZ*@Dh2u>Oy2 zNZ-bK`#O_a(aeF5FiWcMY1e)M&WRAUbyvm}=rx?4R}0`rJAd{abWR@8?4`Ifh#sGF$Z#fl!qVcn6$NDJ(DB-EZWR5{jyBrj=KX3FEVh;>hmVikmg6gRd zjfL17NnsCaz&t+!sVBH-7;kWI2}TZ~qt|pNXx+vi6x}}b&6|Vo8ZIGHi}o3r=4v;R zC-5M_p^Q46JZm6& zVxPSXaL369TWC--6k<%VyJvC;9{Vm4&aG4-H^lLRf2iT7puv`2&_)O42BmohUYtfb zyQm8(z%GdDg!d%MEe`I?KWUp~$DjVSdk|<$)~1_ZL26E+6ssq7De&u~HXL7xd5;+o zH26);qG=qcc?%;Va=2KAQ}iphoe1nkX3qpNH+q}Aa*Xk)Qez`z++DwZu}$!X4b#L+ zUxI$t!-!mtQz_@KyUq6`X?jg`$g+S~tujo=mDlXv@=@N_aT+n-GYXw+8J!9n9A|Bf zwGWS;NG!Rct=}5Gwbd)Qd4;Nb&75^J$wMQsj7a)sxtmM{p@s?-Au9ZwJljQ0Q&qFB^J!2ge_!C=BlnB~v{D8VaZ(CR-7iq=5f@l|^Ip6fw<9Xh9QBeK^=ICMlGb(}PoE0Y_lIU~M z82}tYkpZhue2y`o!od)&u{>?5S9{~<$aLZ*w4~o^l8wBHN+%H2i5}nVCaOVGY@ldB ztUqnt7v+B57{*1!{qHuuC^g^t@)=->DM3Fve`Wp&wuf~3F6~`N{?H}_MFWytFcQ%> zYQPRamU$qxO*U5-_&mi;FmbF4{t5H`VVS?!z9J8nd1F1&qrXwrzaTILGvHa%D(kX} z54hvcJ+(?tx8Us^7W}#iK@~ygWo~U4Y;*tL;4Fg|6DZ;Ju&G}CBhK1g@=L?um3)29 zS|+>qDD~@N(92~I|#tER#Ke4#)t{4cr?!r3<3P@U=_FEETVB3U^~ z>l^{#iwlG<5vmwM5#DIBteZu73dI5wgK3H5S^Ifnu~V}(JKGbJucn3}Ifeq*&qIDh zbcewu5Yd?v*|7oP=6)z}6S?3~fhj~-_@Ev) zSD3yjBnM$52L)1rqqZ+(mWM`neAp`RK>g?83YqJ5{B4?ZBKIh@br?e+u~zHoi&H3@ z9mwC)n);stTEXD&ocwKuSJVp`BY2@4w-CTy6DNuG0zzgsu8_~47Hu!>Obm?!@v#3; zn(sBsqZ6Ak7CH;L0#A~b){NQ_E#n2Q4Y(HRaXut&js&6o>CNpQHJ4^LC0XO2$tu2B zOY4tivRk*^D6rMD>>5A>lIjc2Fb@`06&!oBW=E*@)7Qz^@HtSv)TNLnviYdTb zG36`-!0cwtqp~-YCP}?ERf8yJVfJ}y+A~g?A8|GWwY3`+pA_06qz0$P(++!j1MaB zWNtz#12yhJNC?0hOs`6iv6}SaiUv~V8XrwT56Zj`Y7=#}efm~rBsKU%D^9~@Nss{C z>Vx(c(=}IeGbAutI`=ynX&s8uIRYvVk3Q?%CrF_`R!Na}aRKgu2rDV+HJpP@I(L_; zioZhWUyvEDq$l4src;N_c*@p>`GWbzz5-Uq1MfkNX*n zU65Y{$elfeO@OajQ&97QhXk|gOc5*-XDtJ{%Y9s?;UN4_xI;WK?+RZg>W>8MtA_#D zH{#$ylmtggjFR9WX5G-jgwr&Ksr&nV_1Nh(#l~3BN#-5x{D`dn3|-$)F2L$8SCBTV zN(@2REno7VUlXkc3EhQ)H~l3$rqJf0#T~u#)yQLJs?1j!A3Qo^cqaTayDT^#Qq7NRInw2~;EG_ms->oW) z1*?>y4tVwXW8s|NQ~vq`-jz6?pzo-&(MuHzF*z;t{`P~grXRIzA5r9og`ItpKWZJg zle96Mx)WnGO^1Z8@X8p^YJE!C&!2Du0f-WGyUu2mnhD4m19KA6!zayRYuVcwL7ACb zQme%;D*Uie{_x!bR8{=VmkupW&=DbTgC>$v-Cj|`wb zP`uQteio4Q(~D+v;pV1~na7FRmee0icMN~Vu`&IcpIEHwcoUA<{gCNxAIf1uN8R_} z$Uy1VP1|Zm>*k@ziRpBem59qXS``*#jhCtTl2@V}Xpg_(xi@(bw(-Nvp3JiDGFe?u zmE6lBqr(;`l#lPs?QGSwhm_Acq5uZZdOW}KpqN%6P`a^VHj`Rlm_iQY9a9~w__Hx7 z$Y=^R-B^D6b=18RdAy{t!+TlqwI_;CeYyqKx=z}ryC*-Qyp!^_kNQ6Y;m&)gfu6tL z?v93@IbdJ8ZJ5CLB8mp-Ys77aaf4PL5fo^N8ZH*uj0kOrN**+B&}}X2BG&G2&IdtG z$eR%3)+1JRRP&RMPJcoLxvU%Eyss!m{eC;nsPbZQ&&1I&3)n^*%|5I02r0E+0qr8s zsQ*GeaM1j=++sws!R8rfNSsicX1OwU2_}p}shh=Pv3{=X(r`@D!k)T9*1}rrt)zJC zC{+7eyv=c<S1_uZ~**)yMr~59rt<8%za*Ts>FC=)8DIXc0b6W6f zNS$)Re7zi4O|(alZ4rn0ep?KFZ#L9$tsMpod@THn!8^DIepY20+TGMw!k(|I0@bu> z{LxWQ0Bi{hekuc+P)eC@40?l#j})ykU^9L+xm1s-s_=u2j3~O)m}r<32;>wNsJbQ*isE zJ}yN(2A|x*=H)ZDfSEoyF}D0F;vtw9ExP)KZ8_}pbh+`&iXMYe(sDF|X@;@<1{568 zPhn!cJ*i~J$k)heDx%~fHPIfeS_`v|CzZw4m#v>GMf`Nyfres%1uPwt7c6>=h6~ z|Fw>MYj-4>s}+~woil#V>~AE9+B5pFb((xfM)OR@2J5|e;J(B*HfrE051{y>FmGaj zl8$vqjPl?FY{KB9=eEZASm2&OYwLwR3|bu>b}Bw9HOi@|+5M&RNgD|8+Em=^7;wcg zaeiZWC1bG?VvoBoVG$mBLAM^c-$K2;MKZN;V5ckM{DQ;o`0NheJCV&FO_!L8;^{^+ zM-Al^88ZLKgv+dC9&|{%1m1CZ%is+~jdM$FEAFpji!cHkxFtizQx1%s^KUFF9;15T zUltKc6Q{Bq@Vdd3MGE6q8breFmaw)@L`uJL)86#qB0m2wI2U<-w>ApsqU*F}lbdi? ze_R@Cc~Xdj%PWx7c(Y9}iP1TycujrVqnaa*WPjd-YIc#&ot-rU%tcgl2mOUN-Z++C zGt{`Lp@1`Ac>n8`j!_9zqZkd2G>(!)#<}4VLw-%36mA9fP?wv0yy~k2m?TJr^`N7F zrczwfe?+-p^2W*1eo!c%%N19^DV9FlRb-T_9y*0i)v~bDkmJ^{sNTg6V2KcWQF9}awkNA(7Q8MZr=fZr&O~!uex0$lBJdp`z zOnBOV1Lw*U{U4mW7h3F$))mctCzpjGtv3+P3JP1D%)f1z%!*JW< zPV+G6p@w1<6NaKiaZ!IrIqHpvL+nuq-Ayb6#HuZ_8xn^!yBeL4*lj8T{<0{!4M58L zAAS3h=a4Oz8us_nfLy@6yV=~X;3VDcxdUpv9%Y@8Xk>wD7S$|D)F9xCa`Sdg@xWNa z%xE-K|MwHfeMb91G)oSf^4ouQCtfZ$@SaTDi6fPH&y`oN#=RF%OK^r)BaCL_o~Gg0 zk0l{r4-;(h5NnET@%++DP=br<@n`|3i~u8w*T${!;;oQ2jFWi*+bM3jUNM4pcxV;9 zmR(i}g7LX|c+m%E@`>unS79tn0D+~%a6-1r!!n^P=mEW%%mOvfNRSPne-u}q%O2PK zn-9b*Luwc-^EfCwyY{6i=);L&if5<;lVhr@uaP`85uE$;C>#l*y!a;d;hb((4NQO- zieVp_l9puaph7b)i?WZV`zaYl9`RHYkypoF@(lmZW4fIxKDhLD)-@ID>h`ASc`RQB z0M9CndR8=ulG-z6zH%mY2x-6@Umm1)Wmq|&cAsMj{`hq(fIfXNSF8svlkO;kx=ZXJ zT+URp8Mq&vg(f02ur$Vsb_?&oASeoQG?e?lTeU0%){L=;A5OP*htRIp2V7_!ia|sX z|2k*ghCpn-Tif@FHO>cZBT@cYmc_*4s|>(mGt+0ihLZ;&YH<@4b)MlRT8c?^E=HuF$YqL>At#yfyDLne?zH3SYr^9gOP_I0jSt%5Po9e!mYg(Kq~ z>#91>S6V1`YkElaHdg$nA@&y$2&tE~LI^CY;Xt~qnGOgg>mZH7&@^5u7q@u@&`7fb zNhmiQ!fwRBm01VAgtl2aqzDaM*{k_KJ~{LC?Hc~$l2!Rgv1`y}IdKxIH=+2=J^xT* z4$J%VSRkmlJ}WSSaa}hEQ{F~B^BJ?us6z#cHaOWSda^xHOaw0n(7Sax+)-wvsQ13z z{{r`5K9zy}Igh)+RBZ2n9^Tn@rFD3~zncLE`tc_JFaulJD{zpu^&kVEW8Q3dgQLBXmH=)fsXyS6xc>`^qK@|2_P}AXv&b;#tdQGN6 zwlN|h&e`2(|x%-s7O-U_bK zWG7G2Yy{l-19At&HrPndN{LD*D-sjngjC(S1)PTlDdt=LmJUF ze0=u~GG`tj{SSj?VzDLFrNhecEq^;Pl!n!!k%6PAncw9Q+nDtv+t+cY!-yz3z9M05 zoKWy7$o9K-@(Y~V2ZQUl$Jj%z&)Z0Io4+)vGQ~Y!>zZLw?}zz-^1`9&oM?NjkYJpX zwXWun`Vl;_JFt}y1TNj?@HNCA&VOzQ5P4Kq5&bj^qRxo{<>y542F zZ|XzDZYzp0&+F+>q7pUOKDrlZ+oNVUY4FZ3BqN|bnxGEec{vpSH|GC!b{_M8Q1{;9 zSoi<`f21P9X^ZT=MZ=lOa)pU?Z_e!tzh z|Cxli{h4D#kqq~^y%wB0hfuy<1K_|ZkBbtGX8dB!acodm$yeNFPOE!5v$Qo?S_~8I z?z1>5Tn!zIjGHX)-$RCe4)Pz(P8d9x|<}tte^hop~|&% zb+hvkA3m&nmXL=Y0oE zj_G*M$UZxrq(x>Pjmz6!6^hq)DiHFr;5kUm{7Pi}^%ASg9FnrXw;- zf5VMNTAthRaw>)N-XUxOd;UL;-fAMobQ>zw7FnZtlqOACYl3B+mV_ldu9*Z8QLG=` z1M9lNM0JT2oYWQ*Ex0?^brv6DpyJw|WXBdCfi^VPsu@ljzRame=DHJ#E5vs`$dJ<` zeP{~M(E66^Q?DAO$c;g71J=Xro=clViX8`2{XcKYfIbd5WGTiY)0N`tZ-TXw5w<}z ze|RMP8HtAWN7Ag=Ypqw??8tKRXOi;A&)O&Sw|n zdN|nuo6hex_-&y%#a-O6goz}1uIX-^*utr^4%d)kUbb|@=}ya-n%9R9M$Y!Zi2h40 zgA~|L?@ocni!|T7(2aUhwL`XpbienVT~4wCht*Lkk8tR5k-xX2XL{<7kux0$=dUy8 zjUk2hAzG?5;L;swzcepw@ynZzf-~fq^b1&u)Yzn4V9*IH=*ewSjc&6*)NeA&en=tmgDe zmB}PVKyFA5BJ|A;m>JJe&2mYy_!5{hhIeFgAu@yY^vbym&85WcoTy}9nQ;;5r?CV2 zi3}QH(Ewu0M~YqWr-``rFnW<*b&^zl^Kr%LYnLW+>JYR`sO=DxuCgQ$ct+O~u(QUZ zj(fE=WFsZ+DP@2ovKE42i;7BSVbt9Ko0Z)`-dex1RR+q#*#3-g1m7@}IVqX{^W8y> z8m%3hL+KpsE_iGvtF%z6-%v{N(x1Tq(_V7v_?h;(iZTC3$9YxaqhrlLbh-}d8cISX zkCTztQZ?NYH;!2w9zBK533KS91}n8|`_&ea#k=Wksk5t&+A(~q|eElG(CMF zm|uIA(;71eGg#i~VlBNQ3%l-8DS}{XH|&KIdUO%KMeQr;(1kGG_6bApX7dhSMm%fQ zQ~Uy{yQst<*Ve5%ddWkar_d_-0+T*uNBxl`8iN3I^0W~NM4g%L}A#Bjpg znD0k4Xua~JodovCXD&B3>?b_7*5C3vSYCt}&dM%Kn2TtmjJkEO)6+LqT+q#S)K7of zc;7&mKou+git+m_`o^oe`cGD@1T#ldkaN1~_j7tJ=>xVWP}6>JP7l|EqIMC4-rk3d zGdkF?&CWoU6u%+*9)>RzwFj2mNE!}ian<|r%-5zsqnR}9?&eF_{y@lhX13DA@ft?N=uIgO#`SFWCf9oHpxJxj={p$fhr})z_4uAcFS$C%7e`3~!sA(pQ0`~UC zimX$#E@xQ90W<8!ANAd>EqqYWt|xo$#|-MvTR8@JC6Ld`KmPF_-zJ`nkk!n&GEV)g z1^dPW@s@=Ao|-q@UYtxM{?EV2L~ zcLgLkqy;|FZsCHo6>x;!c@1>-YEvo*ry79`oLlh9`D0fh@7TlbI zQvn|iKsx1{j&MePg7cAS zC3cC#v@Mh*@jwmE4U{G+hZa5pNkdA;4@TEu?Fm4}D^SV~Sl&rEFnM`Q!~FB0H7+<0 za|+aVmbj{2FnP5LmyI!{z&a+M=S@sYfbIO9MJRC6 zAJJz{n$C1;i|?iNqC=no*GTivpSkt(v0nfE%c85Vfwgo1>xPwvI7U0u?U8Oix*DOo!qJ7q+^#~so?<Z?tYEPz5fOprG!RrC!e(GYX!+xxcJXz1{aXNY zO>|@g!VT@47W9kCBM5T`2|BB(ZG(<7PXvtull(C+^E8Wi92TB6-r}0i47b0~|NR_4 zyPPi=TaHyfBzF#p0B!*GPEmD>um$NUFR5F&cxWHC&FC-hI}YxQIW3i)Ak~ThrPlEz zRnOxLJM_SfHC3@x5N!n#QO>1Z zcm;iyv*^khyL^a`D_8o?@w^*_W;X<+7$SUTCi7iyD{_&UTnq=i{(kUEXf=yi?0p@= zhld=7MLmtS0%)G87g-QwE5dj+Td^ia-xPh4qgYRs^^$oLW35~eRJ7d@(H=CGhd7B5 z(bW2NDNqR(kGAyU0II%)SmN{;xT<5d%C4AR&AE#7w#=`@=#S+H1Uh*Zm|87u!oHZQNp2=OtSgXYoV%t z>I`Lss^iG?UQkolxo86@dwAxo`_b)U1diq-+1c*c{Az;2mIx~kdu5L{2 zd!dP9lAO(KBd~sNC)bH<$v@k@FSVKfxT;fEFEk*+6MW?4XtC!D!56+k_Occ-Vak_0 zq3|wUlhfX{Y^*i(Aou>mhEC}ax=TRjh6-Ew-kOr-sgbIXnQI3@d#EBptf5^@c z+`NT;hq>EB_0l+4%}DWRG3Kg+E50i5gq;iV;88zypEsyz@^0>p%j*kY!41&Hx^6XJ z5Y!lWuZ!q5PSG%fP`j_}SC-ZGXCLvb#-#JO&2(MSmk^etZLKe^I8R$*>=h^(XaU#S zNrb}_T9v>ZIWVZ@l9&+Dbfjo{lcOz^@rs*ew}3%K!~Wj;M;G~whyr&Gf|`Ht;s~<2 z&_g|CuQZr*Xt=q{>|qviaD_g?HyK+f&{3W!ec2^0rO?Ni)N#IjtpUmY`um&ILgnRr z=mpsxq+Ks<9CIPdO-+}dLkpb|Yi93Rm>IP3y*SLTu)#!XJcqD9Y+1l6ztWn}>0udH zD}cg|MM)DWOr^43Rj)6Tma5gLHsU)$@A^_jldsjJ_JV6moK2K`-97{Z zDC_i8>$}{u)EUYyK$yPS7wbj$X-#rpmMSpPnB>2-ssxlm=L&05UT8*iJNi}6uk+L` z$Pj&O3(OU6Xdjkhq-(e9T)_y=kH?SHL&j*R7HIyoug#vv7+4c&kL{#Ax||%9^6E}| zzDrE$`D6T>$1Oq;bI3Ptw`RU6dc8vr(PX*t6_THouyZ@~X`i&$M4a_RYL|}2FnVQ3 z8^&j-mu$(ViJ;dmJ_S<&y8ZRQ-pjQLb7lmB(QOX`BfGrS$C_zW-*2>D6Bd%BXP+!n znMdpdwgwy{t?8;REe?-pxsCOJ&fIM&8y(IFcKUQY7w=4$yw)RcVZQ$Hz)f#>vIDk4 zrw}G{o%COBdV3=_j6sOma!^9*EWWMKA=-~8=rhMuG7n=r=R-7v(LW()HJo9J;mfRe zV+D`b*!6Ia_chD`&o2|x>Dzs0mTEQ+`80-YA0Gt%-Nj73!*h6dU>;H;hnPF2T5~*W zUorZkH(+-LqU26C$o=d!qVX!l%r9v_`QkQ_tul$6nV#Pu9zW@ZnkmY}Yzjph-IgZ| z^n7#P2eDTkhT6FRJ~Oxt$qhZJ9ShYT>9w8I&USXTsK5D!Q|$}qMf7US*VPFj!4?(X zU_7XnKy{bj{`@XpV0NohHfS=cG%V+{e2HNROK4lJ)+kF`u>{WrJJTlm(CAf^Xgt?_ z4uu>;F$LQz)2vra>w~~^;EU0C{R5KwgtG(L5&2qKBxa~6x;6B=sjD3KyW+9*i{XAs zsdTj6-c*r-g|5kxCS?& zFm^+N1_7*9gu+h8TVf5(6Qo}vjh8oN;?B>Zg&kw>*tp)eB>4=pb(iQ?Yz@-4K8SDp zQzjplT~n0d*A~ehUw?!V+|wTA*_=G9&}Ajlew#Ik;$cte3G=kmm9%vqU4r)pEXJ0z z-Xi*c-_kV_#vO9UjJ71zl(qLCCd$L9^5>U&PJAH!mUJgur*cC}T5E4L-R%t4&>1i= z?3L_yMZG4PFTHoF95FDoq;GRaW<_leCrm*b_Fk}aoUzv}M%(?k8iR3s6%-L_6u&)? zP-o;ccBT;&U8>E##i3@wu;8N6lTA}S>#M*N3ZM9Q(kkJKGqq|d-Ekdz)%h<>fQr;-_ z);fa|yn?{o_1qtp80KSCqQn@O-bW;Ots)Nd2MMoFgiy_&O_U@Wl1d(-_1cv`2*p>a zNA0?Az0sbo=Dl{CB*+&nOpD;x z*S}_%`g;xJ4T+cO8;x6QVLutqeppgzyb=(tfj_B0`gae6Jh^NWWOF`xq~hFfp2@+p z7w<)WUGhF@b-6r^g5cab0gZq8VRC?8elw6}ll-qg>ID!7Id|o*|7-D_R|d=$oGg@{ zDE_k1{p&}jy~aAtu}d`y!TAF_&!Pe%6xRRi({J>|f<(Nn_rJeJ|2Yv4-c;y!Q)@sQ z{c=g%e)uZJzdbS7jMEtQ46})Uh-$<7AN3JZv4fuQ99NIP=T4Bx#D%$y&+6?p;=$TW++mk>^?gm z$}jol)7ut^gC~_uLSuj^e5~nlz~{ES;ZBH@jUayaq^t_-34x@W1DDT6aQ3z$$#n#K z?}41fGscKkku2elpCRNt7>;}d=t&(coK1k7h4zA=q~JgM9c~j+oW70^JM&n1QJ9*C zynQ`Vpv&|YoTpt@q{cq=(?HcSg6+vLT_GJNm0cs+ED*A=L~nn>0XZHQX*d^`Pv$d~ ztpevV`d&RuxFung>LSua)iTS+De>5EA!b7-voZw{a9YO+SPp+i&T$B!|Jfz`3Jv{} z6!br6SMXOFoTLp6vdJ~NKrm4d`%=p?euF*_nS*X^7LaD(k@3(R&x6Uk zA-wh%GW&lQ3a}*#{xcMyDrS%ntLJbTlD-tV1@GXQ7O5Urh^H|0@B%d^As$hRSpoH(3T&7GT>QK6Gwg) zwE9N*A;br9keW_*8VTB&f|(dnYK}n}Ww(hK3|pWouSSd9PgY#3ZspOKA6ubJmjp)2 z)|Q|$(KpM);Eu&J11#*?Y#6zkZ*|!8bEl&{AdN5!j5=;Ybc4qWtWkAvuzK-FvHyg=KG3fFbHU#pzSstMjGXEiW?}%)mkTL<6IEoklpJkLUAg~$p3D#Ko zjDby_#G}vc(Z$3Wxw>-4XZL8yuNX)zpH>&+{rb6K|I~z>2Zo&ocU5>&VjpZtQmv~R z-Qc2%AKZRL&*?`f9%`PtyMutQL-d#3$F<_83*=1>+`B(qK}yH4c$Si&*8_+4B@B0p z5wtyA2sN#7i8lOYc+4zPn(bM>)h7OQpWt=xu=DH@UnDbCOc6Otl~+g&8-#+w^t`%B zW1HI*Eji4@kc?`=zSDGs)ED^2t|ck=oe|DPOg#fu4plSSLMhSVsL{t{=@cokE+&Xs zoj0iw>Ak~+7r18yaZ`j&?`DbZu^x|?cQwBuGT0mMj{rIX#Jx=QV%uS47)><+S6&Mwc1W*y0^tQeGtl)`&m^T9@d&=?pg_!bJS|ASI!viu0d@7-)y~ zZNVqo35cLcKq~yYIlj9!GWpcZ+`b`cqv;xDv6LsZVHY01GL6+Y-oW(2d_=Ydzlfl6 zz5!yb8i!j6<&mbbGRG_0clqhq{Fjk1$4ODk_;4>oidpr|){d#L8sk%3rm01H8TmUE z#$oWnzi2Q_=ULse*<)u}pSF?caySQ4Fi*82>g9Z*lCNzc_MqIlPCKB^$-N+ly&g)z zZO}FrV@uKfB`b`j{#5D?-%zngUMjh{>$19zy=NYo;Ll&DG$t$K zU($D0rc`d99$Ss4>B%e+SVT~#_AyMjf`OSsZQG}mW8xo%`jAgM;?7&Oj}|q2bEEvi z1=3p%mWNzgAF=@Q$8LI_x{S`QT6i(+BrzV3>B*9Ud(CV|KH!nlzq>vjj@7U?Q$R{L z2`1>|%k7c;Sa}R{uYxt{3wAq8{$v55^X!k7Ff_L%PkZo0d;Or@$nUS4NRo>|4S8}- zs?s5N`8A11P*SelGVX)d0waf_ebomxN)=Kn$7T-?oCSBt9=JnVui1!mZ2HlPFqzbd z869z%(2PMAcN)F{u{S2d?LP)FOlyuJL5#ro)=^eOzrpb$d`MzMY$A&pbLT=BmbTb~ zd-2gkf*a#)+xlOCVH%IJTQLcK(Q?KX1|mcQS7Ge`TD?0e%1R*W{=vJTqUV?W^Avf= z6x}5?MxZ@tjCQd!f?p%~jHQN%B31ic%zSs25b}GzjV8_}d?Gsb51R2cpN!wg6Z&L& z-_%#lEV*>)`220kY>&@Rr=_{aZ`Mx}WLqNp(sAOw0x?Wo%sdw!W4}*Ydj$!hFBzN3 zBuy>k24BU?+~fFi;-tk&FR>)ezDo&1TswG8;=c0`Gb1$*c!-ly0}AN23K%lJYMG-Z zE(J_Yyi96y7c1?Ol9QjKkqPElv0rIq2tC|^*h{)5#Lvq=S@w5^OTfH!*gZbVz%Ug5 z=nk3Q`Yva`N{`kiP2uKI!V9lK#POt_4kVEAiYuz-m==in{4_ex9l{Jh?Ky@R(K9-f zK5}AZR__KmH(Fh6#=zIWrH$t0HOR?#Y8I+2Z&6+;VW!0>UaKL0g3N@9FY(WD^@*AG zy7Ufn4KgUnV{EFb2IrVpp7F;JS8G#oxhrYXREFPV^{29vN%akjaG%(=jp%mtO%k=a z6Iz?4@&<{4ew}f$DXltEnsb44!A$S_ql(z3$LQz`@?1vvdT9@&oy@iQQ^7B<>!<|- zC=t{;{*r4dQ^ECE07?uHx%&e6M11UgF(zhw68q5zVlwC>g^&B!M8sZd+vWz zKF!$9a)+_fl8c@QJ;YO)+SrND#^q9t6 z6iIL@veJ536O3S~dR>yYnm{n4-yV-v!w%%u8a>r>m*UJ7x}D0B+UWu!mAQrj9Q$$X zIZpvikw=%i!!MqOc#ea~%goKMzpOdMXm9ty@s zCl1QYX^7raoW+Xctznpd-8J&$^d?N$S=L{G`2n=}fH@8GH}5O*Dnys59l#pPHe8R0 zMTy+g{{iT*(fQFa(+DgyE9>7aG&&OibQlVNEN4sSg)%SPaZD zo4Wz)I`Qs{(aIydt>bUlp5_ZH#5N8f9e}5^U(O4o21~O(zZ4{b>iHnvYo8&}JL)8& z1djQWb*`%vA(Ee;)bHC6CuA-DCd{J53+|9$^7u!04=@j5aRRNhjG4^9cz9Pp>@R+2 z{}mgKwA`=85U(x*9tLsK%foNogY%re?2bRxk-Zz2t{%j$Blpy#?3ZQd`#rV}m^nJ! z+_%S)`);A-mC9`f{^MBx$8J{uR3zumYsvre{~@9G22qhj^*Gi4Mff2A%^p_k?IV@9 z|GFXlde7fNc2HD>()w@UT%kYo_Q}S-d}}rq$X{n9#)N;ASRqNt5g5-827}J>7N((Z9T&smPOZ-A&1Bb8fRj z+I4d4h4)?F?>-$<6il_j1G>g*PBW;*~JFwBdiJ z80F?Ob`Bw}6_BjH791yFtz&iZiOnlM3$eYAs%B5|DZW!7e2(E2lGr;nc^{d*dl(lJ zA(FoyD>J_ilQ0)(s7E(`1L4+YU;&c6{(R~dOzq*^ommw;BWl#_>m8H_-j8ef{E60c z)j%RFUwQKDyE#ONzTE4=KDqZ@C$g;V2s;UpnbTL9zg8%$o-CR;9ufdr(S}~0+slEU z;fo&|pw;rAb`!?Ect~ZUmRbCpFq;y<-1{PN%r_l!A0EhYVF^y!U8opF>=glU$`Dq% z=~AY22RX;M;ia|Q`x%m_Q!2*Z}ey>b?PmDi;GnqrGjd^wTgA>Ncb zZM|>ITw)OX<(4o+j~+^nx`2iDiylNy>AQ3r(4F&fWLcRo);iZ`!yrmNFHM=u z!hbu)T-e~*d;1U(4NIR_qFK)6j|7T~PlOOYcXYC6${a#J$kkXK`7u{hGy7lXYAgf( zk*i_pv2S7RN9c_%AAXJ0gf!FHt8Q$HKn2Ylh4|Fw+n>OV5=e&y`reYvC!lb4yr=g3 z%1d)RvVED`b4PCNC741;anCN?TZit8w!|qvQUfxsn%C(pT=Y!M=(B{Vvwz7!;Hndy z(sQ*8ddaeQjly0V<%X~^?#3yIfde3s%)}YSbF-j5g^iVw(+xCx+18pM$4ZuQ0*AXF zg63;Lx-{2}9P^)rZC>oy6^dF`zcJWk5?&zxI=#Pl)&0V8)$xvsWw#GcB^vO(W zEkF18#osD^hoNy4Q#q=H>^lT9blHw%95fRCOJpI&p+&`DLp(o2<~t}42tDJI;had` zWQEu0MFc{{3)k-qFkb@=_GC9>vRe+Gax5>GqQU{cfJL}f7i7N-cIyT%S*?4E+p4=R z3h(BdaxX(gaB!aKecn4c1T(3EF_yvTE49`wPov@kd+~(6Y36bfH0l~{E_9Z7h}Z@l1NDE|AFLi`xli#f8qw@0Od>b5)xXGyZt! zpMk?cFryk%6`Zw3qHA?u|Jr0dP+%W@em}kc?elgF5h{Mw+b;MepFd0Ygu3ry`3!kV zfeZ1^!88bFuzP{ctb_N|e8;uWEW~ybm?>1ma2xc?d8Y6SqevDlIr3R#``gE=xE*l? z&$Muu86~lKoULJ;LUh&s8REL}XQSf?SHY|eoj!v_#189}ah-!x?JJj?#;vCmCdTxa zdP(UhZL1&5B2}0J>xQj)DBI_!{b+;iRw(OtvieUPl9Obaz`RG6@+cam38V0z1e=ZQ zRv&)y4aFRUoB?6K*^L%w=j}tEN*1iUoT1lkJ6Mp1WT!dMDH7-zlWSG+(WcN(a{}|> z$B12ge`H8gU6Q5_6xsP4-;I2>`h4fE+UCMxv226xC$JC3cuL#o?y6!RkO zBV#f;C->)iT1Sa5(hS(?(3!EDjDZ*^@TI(}w<=USgv`Y86T)(<>d#kgPl@Vxxknxn z3KwL8Xm~{Y-rqLU`qP50`gEbRYB80FE%x!+3ig=oj8GM@o!Ue&y(Te=V;ZnB?ag1{ z)t*L?M{d_JcvS&>V+^jEp+-Z`q(ta*V@Yc%F)}b%RTl=E>OzN&{NZ_BkOnR(E zVJW6E5}ol!RQvdq4-1x%*HpYRt7pnlaN1_>XWvcz*~KuH5SNYHA7JodMn&)qq#KTc zOr2l$vS9GOtL+;aOMZ;G<5W`Uz*_T~3}0n?+35oM2jTWys=MU2j2CDWOva-$rUXq4 zNBSEHW9(UZwXsepxzSp7iHGaE-Vd;{4tzSEJv*0Dv$!>AvK7+Ex0UzwQ>7N>`X}T* zc)ax%tpM|eKv3f4$suS4ZxzbnI26rU!?#6sMLyGq{&Z=xC_BwNSo=0I8;un>g#83L z_8U7x$X>$c*W2W>LD`Tr^*T5--9q@@o~Newgso#U#ES@zsCxTQg9k@> zk~5Fe_j0ZI#yelu5(mt`XqH@@^v{LuRpdP5Jem`S5rWgk(%zGXJ$xD8a#y%d&bAU>2}Nfz$N*>e34n;-_-NRMO)BvrzpO8~&HQD2<)9$>fb)&_Nb{Ag zEHd^UK2DpTeCw+c@Wuu{Ab zeCjd4EA^DZ2D(tng_YArG%T*T*_y6i;~%Bu1Cyrp|pCiUMWzzkge zLg4T(TPvb-Xzdmp8lkbT!5U|LD{eF5Zm=M085enMX0fBMUbjF}%m5k3IZkChTb z;h4QTWU!*0)$@r#%%r9K^-bsc@;e@hBQJcMCX=d-nwlSM7^$Yb`Pyzm41kez=j#M4 zA&^?~lxpB(6mF~HU^h`8;QZV$v{1}a*tNjQ=;4=%(D@tEPi0ci=y&R#`I9YzA>hk9 z*Z6oCM{e2F?Xb6wtDyhDvYJ!s6Aw)PTEVp+$d9 z0kNerOd@ngSN|-Qlw`X$?5Iy9Wg8mA@73cnS-C#m-_zUh&3#k^v#}lwo1*YdPE{~< z)M3P#-Y2oit}7@v1xdyU9}?5T**cjR!9L<8dwcv9sk|22iri-;_Tah+F*>di=$()~ zD9oUA3ImEiPX+5r=isu%XtQIj;|Wfb71zpWoMxm&6qmfbbdv*wCgaZt&Hurkqbj}? zrAYm&i;4h~oH;6^lWt=6x;b-HV89Dn-|VMuMJ{lZpRvpZ>VZfew8#jmHx?@xsnrIi_X(o*-A z0K^YV@em38!WE0Mqqx5;(0@J0v}|}{B*TlPC4YVCr?>86t}^J;c(?Y`*g&&2!Lzjwxd(?uaEh?BMaN|$?u;vB2*#m9(C@V z`u{3aJwhPa7rCvpUmkY;yRi|}B+e`OS0(0O%(|893;$e?QxY(9#>e{KXgtJ ztOJ)00F2`r)|W;4^)}aj)L$66dkDG^U6U!H=GHH?l2dDc$C~!1r(Ew|R<^RP!jvKE zL1!~Qgtq04YCCi^RMRg6t?e!rT2rX@jPW5}cDBHkW4!EX#^};Gab@1dl0h;`O_-bS zZ-4A5IAvyL9uprn?V2+7ZbeCb&-u#x!K?SO z5I05@9G0;)ZXqTt#Ez8x6yJ*wsx7B+k);$4p-}N87zaEvv3xqcItJMbx%plu_}Pn~T^hTmn~knk9y9$8|i)9AkjS!?Jy zB;|^s>Cq2-9%3`kKc?N@zl(O*>J?!2$Q8x*Za(cS`Gy!mka`M2X2k;u2kA#Z{B5@S zQ+M`n4kB<)1e84sQKCwl)uAg$`sJZqESC{9Lt#-^6?;Vo3=YnN6o>eU29!RN z5KWN1jPs0~p$F2xEoA?*fADvC2Si5#MFT1G_qp$Jz+7hd*7P>UIvg{P_0fOX{np0T zJBzJnC<>T`Rs*3p?WGkA{P`WlJ$FcJRb})!a#{B*jmw^1N{t7n-jTi|TaMvK5ypbT zfCn|`c{HNU5FI3i{3EU74aXBKQsNlD{9(gGbMxcUDyBh98cey0z$}%AZn2!hGujC$ z2?+7`Ix*5A!*ABZQhCahwwR>m;_~Y;;`uSc<@|Iz42D9IaBDxd9T4OyKHbBmQFp;r za)=}dv=QRAH$e=4F^V$~1;a(KY{s8KQ=8m;k^=5OrfUO>$%Pw}Nw59)VaMkS(7W;0 zVTdz5bEu{3i6)xlNCmo$9f+z&eR*58ZQQC5Iw=VdSNI5Z;nVHHj%3xa;ia}l2(}0m z$9ijkcsk-UL|~rqtvH0U$nTUBm*9F;IlmpB1|yx{oS^5_iNlOH0E|(k9M<{fFSI$qX5$`K8^XzBgJe zRVZL6*V>1d{ubVc>t7s2R?t(*#dZ!KiyJPwI;}#!9uedz#G?=7pu-*Eft?j45T}aA zbO)ZDcI+M`&6;!B6)4K8E9Xok7P1t+nn(r8NFfyA7N`b6*~U1e2qq@+7y3X$%UEHU zx~BecVQkogv1mTJl_hV>u6g6gD;?&CnGW?QCzu#k;R-*PYu2qETwAvJCHq+!N z*xxC#?1n&!Vg@33)t(Eu$SjXJLfG=tsiqilRl`?@@k9)}T}e8UqS>@|<*t>>Rw!t5 zvPVQ=m^xVzKcvbyl8tHp5NEtm!AtzK95GH+>xtl`hg9wzoo{@2UnHNrkA0CI5rYH? zq=eADPyy`T*&U#`JRpk;PNbCN^wt!3^+8kHG@8d^fFX~1nNz?+Y8+rdoxv9|rN|hw zmx02J8~6-tTIHsbR;YQX_Oouf$RZ`hSS0}pd*4#NK3F*w@WWn4=8-7#>z}rn6hhjb zVy0%md5YtXIh9T8?Us8e#y-+~hRvZ^R!z zRJN5ga~oMFpxP-zV|K1p0r5=g#$x%}_F)^ddoy{_$aP^-R-B(9@6*t0vtJl*mw%~| zblzyT*^g`?ayRNL zjd6xDdO>ij5u#dL`K6Sx1?Q~iKcdw~JXP44%2uY=?Ku%GQIF|+5wGTpqtDV(0+&w3 z4Pt!qv*+JjAj>=_JZbmB`StTA=ZxFu=%^*Y=VM=UL>IM3YwC4O(A8JxTM#}NMoTP2 z#N#*vzg68M=PcFMd)f+-DiY)}_PyK9yDP>qK2_snNjM<3J#*M`_`y7r*#+|IO5VW= z;*h*53#=)s&HkQwurBf$O*dpEHV1r=ls&XL*X(V{Z)00?r$Xq-y7`n#DA|%~ zwT!4fbu)Yaq6ldXRg)vFs66>~;j=EdQ&5Iu#jArk4Js4=7e(%?DO;{FS)4*wU!d$7 z+G{R5#P1r2VqSvT;quwp8Y!|1+48vW-%SBOrj?B{$$pzg9Melk6RSaD$GHj{Ri@`2 zX8oyNp;DDpWGtmA@HP6CnGq>NsY@9lqD>Plq?FbYB%TAW4wdW>Rsd>9BY3(eg!Aii zH)Nd+1+^T89OyfGyR)DUV=0coJUb+>nlEVIRq2#%uLD*U1M_>uscFad(#h#Q-iW+U zK-WHfkju{AQ#ek%z6sKv@)PA#*_Oij4YPw*8wI(QZInS!%o~Cvq$x z&IN456Jdw5(vO{$cEMf{WWQxSP&e*VxWF|+SM%T^k@C>1)Ovl3%#y%BOJZ|Q=i9el zs>B}C|70Jq$<%2Zc(abV-;B$LTA6AEBT#4&EtKpseyl|+TOe$+n$So2n zs>`@?&Zl-E>3L{~!ZdkVW`5<%atnp<(T%y9hfjS&dLik$$|rax(K1AKvQ0KN45@@t z=FY(xNDgV0k>>x(zgS~>HQWc!f(}M97OuATZ`poXF5y4f1kgUBm1DJYKfFjM5z(%e z$?B&6pbPgDgMe>!V3(l=hTAb#B9pgG8rQacmJrmJmaX2+L!sv$AH75+ltlNssK(*G zMIPOPeu3)KDp!sc+8gSum_r&R*7K$pA=B~ac_=X?Ch;WIdSH?GFqU%%gxm%T9Ir#Fxj=4TZIcHNN9@i=w@{}-hGVSsS z81mX$a#yQ&vp?gOO&MCiJ8Nt|&qqV<{Sg);)46lYaUX&$qdfUQn=>)Jxp|V21?~B->lfsto6=16JQPDn-#VH2rZO)mq1bJCB;2 zFzH+G=?E~dj^WfDC!>6v^FhrCp?&9PnN=1rD$rRhV{Ycty)$!DTpQ)7kmx?n(js<> zLM_xeYL+(ELfxS-hV|WML1@zB(UBG0SHK;TGO@e!VDJHIbUtlWRFcp~8ik|L)+m$0 z9W4=!uV+t8a1&}=TS?x}7HZZ%4V%uYR}fHj$WjqtMZ+8yfjW=pd79jWJoeFZGE%-R zO~H4lB|g9sR`zyY_|8<8)y#*fDGJ-#7d8(9T>>)~$QHFa-zKnK@@t&AIuxMwwuzzw zY4t<8)azN4W4Xl}fdOXfEwtKUvMd1BIv7Una2>{53{SbQPpl_Tr7*nNWPYn7!MnS?&W7`u zob9_kGO$Q}>X4}S^2Hv-jZyZ{y4f8y)ry$q)`jJ9wF%ni14rFnmWuYzoqu*K_wBLM zwl{D`UN|oK`-PBXjXzH7w#%o>|Mn#NRB$sYTlt!Wfh4I7Mu>Yr*IwdvTA}i|k59s0 z^^ZkWTDuG6vFdNzuW!UKtJ9hLs$ocDR6>h{)`O*+`q?ET^eWXtvFe9qt5LRD_&EAF zkK_&++a`5HSzniPFzDCIm9omz6=6je66lhYV*1RC?wBc_)G~&psRwa0zGJpKc16uY z$DYrT?YzrDcQ{+GS6EP`*LQeJ;QDhKgQQ^*TY^^>Aru0Mo(bB-NoJ2A6Uj`Fnl&aB zA}x-Q%K{td?Z#v3AMu^E;MQt^`TBF}(NK~Q@iR1O)1G4~*N&!V-m-K(Z#j5WYEsPt zt1HI;2D>dfNM^^Qb+=e;y`!rC2y)))BR2&rJzcTTiUn4h~M zXiu4n6X!>E@ta0XOQhn%$21)`r2ETs26$UncLEl1)vu-$&9kqJZ|=rz_VdTSoxkIn z_}bgOR`0_v_j>rx=n!$MLbjq9O<#eimnC^q8CaL}^b>+$1|}{a^7ON&APb;OiYZG& z`L4`}*8v2hr`HnuW7ZHx?w1Pn8+PN$|5#?>Op(@%x>uz$t9ZHX`n_eI9I^t|FB&}Q zG_IE}`zzeKdDLBHAwb-zp>p$ra>h@m$Um=jH4JnDP4;GOB?>kUG>70ArlRY9qBLjZ zfA?7Ma5T1X?pQiuIz|$;dPu``LU@?z*H!JWUtB#^yf@b~P*@U$kM^&Bo{7PhYR5d6 z0N&7l-toVGB^M3ZcFi!X;{S(l+Poq@cjH?;m%1_78e()ZkNovK{`r^a#JjD-U3b0> zx&7<&!uN8Zqp`0@YRuF558uW3E4$nHXI+jFoy*@2TI08}6mgz$q9 z(SZO&o8&SVgb;oz9S|TKLS{h-rx_fd5#EPtmf{HrVaZjWf9VHQ+7n3mS=TFM>#KNn zF)C`c%W)Rs5d>i3FinLflNyo4^qSv)I@0uD7s*Mw7SFy1I@sZKK>a}9R&z-Q6$Joc zBJH3&!50p*oFbdg=#Aic_Um1u8Uefk{Z}{%1U-Ogm4f9(n9CA&e}iyStNVE~p-%v& zV27zwG0hyC_Y4whLeeGA!4Rwj^QQwRPU}!-By#{rKZ16XNUSVdv&cZlp1A2Y7?xZK zHK962I*;FMRfLpNXQ7Pv20~Nyh5h&h*=4-~r7W2+)ITp7uY9bJiNc(rDg1&QdgVf^ zce*TFgexrl&eVUYug$%pUDP@prPXz7783u4&2JFE4*-!8uOp&DGm>ysgvj7u8rCpW z!~X9ytY`0EKr!3{fo_XEsMu5sy`^|g^I;g`OXGo-mtyc^_`#02Xy~GV=eMuLU2J*2 z;}s#(OI7uD(#+i4PSU@W+2%d7$_lu5T=G_9Nt7z`H0WSJ_549(5`Im-Xxe?pr zQ)%hPkLUG8x1orta4`~)1|np^k?PD(wst*s3n0Ce!Bbj)CEv zoP_(^wC>4qjH_=llEG6ZD+KMbb$RvtNjyg06|rwojn!O25o@boI-HjH@JO{n$$05X zC|*)^i@-QWWI89cA7DC9hFtqH!p8J;gWA9H^WPZCZmdr8=-U9vC$;wSI8rH8mfQ{I zkKxJIz*mUVt;F)|+dfu}b%MK=oM*ow^oG3EbFN}{NPqLemdALF3b6wj>}f)_FaSl7 zj8ulRgt?^q;tu&E;{HC!u=xa)O5}Z^?%*aq^ zX3xY0KB09u*m6}ccH?_)k{jTy98&RHKEx=<>h;?H1A*WYkhv{aFpxqIyHTS6MrR~{ z$mzLynK}8IB$NKs%E>FY9BXF9koGO8Llm&z0_JZiB_M|wj|k#;d_nfxSjN5O_+#alRx&0YK4S652O#w^yrEhi|gp4wGg zyMuQJWxHFex@?DS z`and){6iJ%3yZTf$%xkALv6!kx0LG(=%aq>nh64Cj@E4*-p{Tij#pTkTZ@lQ{w<3+ zr-)QOz9s}!#qw2VT~2qqg>Yjsf^V)6Ub04oEJ3)?w=VmVx_?a2KbEOLaSN<83&R^1 zDBCJ(mJ%#8-IPJ@7bb7hL zio&!u8JLEtm2Z;h?sx7jey_@r!(1EmjON+LrtM324H0h9!yrAOlc;m3D?7X!^9=Z8 z_EmI&Zb%_nkXO$3efptIGls->J2&f6PFp+}j`$GcUL05sRn3va2((C62@h9#ySgBm z0=MwFcA3h=^V!fqS-Z$_NZ$6kR)LC?NOQ7iS%mgRW(Wd*(=>2x8=$Z|zk^?^E{U`OT&X2L$a+STcBG#t9CUS`(wOB8BcySFOC75dN!NhBOQ>eX9dzTET6!X2Inn zCBzbu;`bH{NWyif*Ky3*3+IHsixG;c(6%i}xP!``y+m``vHlt1+A_C|oXC|}tnlfG zFsG^z*UNW(;%81S*UYA()1BL~*hvQX_Jl{kS9-}LQyMIV{xy;Q524d(5{GYRW(XDq zBt}0^e(vS+EyJ9ne-Bpf(hR(r>O}Kvt(u|gRxSL(bq`mDL+yi8I<89xH{;){N;slP zsb`9^;lni;XuU(E!Rk^_F;cqOiM{fnawb(wXdVAyvH$Ivg_}mvrb1-)_;ga$ zPLt9?(*dkvQXWyh*bjT?#R-I%%D}Ak@%8=eRJB8(ofKn*VBQd7Gg+p^)qUe&6zzup zeA0p<3(xS>tR(q|u#;zujjJz`Erig7c^&hn6r|gXd>jIK(<4$c;fPTS?EW|Yl-$}< zBD5|laT%K>*)(ZLdAh7FG=%^U>+6HewCx!|)^6#^S)Ba?NxdM9f)pyljR!oxfa3(ic% zN6vw$fsAs<1)yz1;nI_;dq? zgR1AI?yU^=Fw_*jdXUj#xG@J?nY~Kl5wA zLZL#dBwmf~Q>~EKvQ)}a%C|gsoipOw>F5g8Z4PPmz4E)HPZiivbph4-HS_L=bufPQ zS*+7{&A&Z?%7Szsm}7cOZ?H#xyO)0GCH+}5lS-%ykXs>A9Iw-B7!du2G+8X*`02ym zzV}FvE*m1?xq^u36Vi)=Q;~naw?g{VJ7dN(O%M-{&W=$#D&!$|ymI#BQcX~sn~LsC ztMs}V4O@8h>Uf)8f7!{aK2?VHuX@$`qZ&ljo8?7Rz00v?o0$(<9*E@NXc1%k+^p*e z3c~bzeyIz6(g)^cS$kyU`9Ts{b}gHajCEUuNLx2u&rhC_(_c8+g!N`eqmzE1!cxWhl-O%C0hW4c`j$_-ktm<+YLIW$>gHRSaqAm)Rc2k*m% z^50Z%U;~y+c41{zRjcFgm5J(JHogpQy;IwEq0wk-Qn6i7X|#zGOyG zC_6B*bG&nKN%eq&jHfgM?9Aj+&qU^~N0&8PXychR9Dm>H>e6%aL>;x^sk+w9(0fJ= z5>{6-h;$`49iFAei6uT$CY0`(fj*-k^Q!BqH^U0ukWqU5gvE4DAle-o!v?pU(Y0d?lilBKt~(I=y4yQWEK zTS5<>z%LXNbw%)h`-l7frDs#-!pFmIs-u_j$rw=F-GxqR*V@iEoDWzfV|Uywi&sv( z*%QWsaeb`Ke%eU!L(98{?70TmEXP>r2hn z*Y+7|T|a8UO@+ywj(PdYiD+^@0CXGI1juzAnn^BxFzOSVI zS76Fb^0*bP@>4HT1j~=T<8TY#k1z^C*QZd zu3NyOT#UEBbq&+t$vU3Kb22CQnH0~lmo=?i^srq}v~PYLO3?CDn+AKLT#6#)+@WPf z45-=;srYcYt_l%-=2W|Z!xI&|a*vZKd9r9`&7PgIA!xKI{Q@(Ef=7!ZAn>Tu)o|SB8tW)^|XHe|_&N zk21o;-1PgQA*0J)>%}PEo%G<`DoT~C|Bd)_AG7(K7Y7dNxtIjH4IAc6gRYUxms6$# zBg&=ccs`$5iyKY%g+kD2RJ_mxFkzdHS9Z83eLmjUlmm9@c{-!u)fhrYw`qk47Jse} z6Z0p`bu7wfb@c<&O2qWOr>wjCvYMHiGwK>Ps{X*iVP994yQsGtF!k(4CEo_UVu9VQ zQinC7+eSv#HP0%m^ELOo)8rF0Hb2u_ykWfV`k1e~MQ~VX>VDCJ`R&Gj6~9AFIZYpS zxV9&zM!r$7C>W6r!pwcJT14yhW>}ixQ+tFRzO?`JqI5q#CGE}xh$}9`x_eUlHQD_> z2!JbdW>O^F7MPEUN;i*X#O?n+)J7(s$h^<}k2yvYmztIJCS!b~K6{q!LpEu@IGL$4 z`F&b_Ui(~H5CjO(O~5L(Do;&axTznIb|1Sxu-OBF>vEYk_u8}t{)BhGf1>pmnGYJ| zn&NZq_XdB?+hX_taBj%!O#c&bsL$}_6SQ{ze&j{doi{re|* zX91ELr0V7${B!N~>!tsXFLcc?l_lQ>|Bcm~DJwe^|W!@%1z`o?JF9-Ld{CU>O?~W>{`ho zYqhEea%uOBi{PI}OpCe0JWtyFp&GddX6+Hy3@)vwEJ1hCS*Fg*H3r)zewg5(!F>{Z z!F^%sncchLE~wZ-sy>iRA~IM86T;H`2-M6GAeDtiplW-JL0WC$&#=+E^<3m=uLAPa z0P)JQHGbc9in`q=Krz|?M*k$2QdqwS>uQLyPFJj>zJRz0xr+>5f!&Pk;B2P>JkW%T} zfZ7`qF=%GN7;OP&Vw>3zXm~mXP}CA;vxg`QDeyjn6lN7#z-~hWWiTvkZ(q6R%?A14 z5dJd)M=xN~4{76qKIAt@J~%XiwWpPRg=AxsbOF=Za)?OJc&zmnihx;=O?tBu_FC|T z^VcH&_)jiz4>0#4 zLu!dLmu2uY#u43N1h^PKg4fXivcik{V_6jMdPS&~J(wG-@t+$mby7~7e&Gj&Nu14M zx#@*H%_8Afy6>GdFt(<4rDpM<6v>Blud=>atdN4BCUxQC5|cb|A{FSH>^E&tr2u=N za6LFhtlI}n?06RYO2y4ka;C&u}uiH=HDWUc?2wyL8gyeW)&`n&Iu4UefpoSPIerIeWl6Mt^ z`23LK_IJ}Cgj!ca(mIh0Qb{<7izOQ08=+2&3TfmP8oV42f#P&V-Xanbh(4tU)}W@{ zl1Ljov|c@aJj3QudjLNLsn$kuWeDjoSOb9am;k)JkzF6hs`@DW2e3k$Y4?>;31T`S zZhm!Ypr?^M=FUyj;g4_s280a%00h2@9LlT*$(j72Dhp z1xH}gh3@hf0%>+!y(#`S@SlNNgto6+)DKKr@xsbGCj&l#Qt&C+082hC+HQj^Y&;k@ z|18?6B?F1UUQ6Fx69P)5WSU{H&y`wQ4M*Y<+~zGo@<#$3M)wO02`IZq);z>Rm>)6c ztzVQHFHe_dK?2kpAa|dhU;8P~7!mdTE8RSLy$^Y5X&vMvut&Y+?X!jwXpz#lqD0vX zH}xh&rC+P7eD?6G6yb%_b_b0(0AC>m3%vs=JyJ$B5hOdU$lAsuGV=2G^5vIGR@cu%Y z_CR@h-ubuy1r0$)XluR|AjhXoWd_~gX)Fe>BYwpT*M|ETk=FJ*JwD*#I@D+%bstor zNtcq+mLLP`T3QP+h3!Nwl_%oCv&`nxjp) zM-& zjP=$z+{iqoE-kWZmfh{2V35!$)Fd=LG?ySD6pe?=LOQTE+2TVnL-PqV8!ySP9oOSR zz~I<#Fi30W7F#uqRGwj;<6-BZOU~YkGy7D41j9vgOA&phK<4bDp*fY`E&21|@-vFp z>m`wG0z*n2>Pzh>gdOec&(4z+L6yc%F>W2D^PujbVq%J8meG#$2|Mt?)9l`hK2@iO z&ldeM(t65y`c^FoBc9`Wd_8-5SzB5Wk_II|^bNK&mzT$&uVHi~EwmLw8>EY;j;o!N zKta&{-+H=$<*x*V1byh!XgSwvbn4gb@ZqD~@kavdnxZZ&8&BlJu=I|g$-wk?AYP}IFS=C#U(FU-z6=vJpr5*s^tUovPSOE%<$@ZlHW zHo3I~mxb=n%Aly5cky|4P*{=-Pb0{rhGQcqY&<887uBj}u1V{IE4&JK;AC>NsDk4s zyjhH}Il(-m?`lB#wRr``=a#Qf!zuGacbfXa9I5T=1s?Cm?YC}ld~J{-4f-AzNJU;e z6Uh+JvtT90z0EGi)G-;lInGGJ!9w+WmHSIJR{RK$f2-`Oit(B+YHN?K^-?R@wMh3} zB@u7Q9?opCxQP`D%m>N;audHo`5beZZt0*69as1jzqXAFy}|FyJk3#*x zV0v7Bt}wypZ*Er{PHFU30*!8-ka7u<{&AqfVqtiKcPeBB%f+m3+i*y8YqojZNf&L$ z%hZG81^8rxmNoP2ZBXp%A9TP<$r-rgsC0a7yjGS-11X`aB5Ks??c(%IVhV zGD`(SCtmVt3J3oNcKvS|#|xUK!&p-LNT+<&seg(NlUD{PWQvI%JPKJv68D_$x=3zg z){?q{J#^^3Ob!$|6q%CMy!-wtXP<+lWr7=;v1Y^_nlm4WDNK3C|1j%DrdgR}Kz_s( z-+Li7U$8`PRZ?bCM>5(6g_ajq+6Bzlh$FIyStzGl6!P7cde`G%bXt*VEUidY6Id z62L9rEqh0Nak0NJ`3(pCis|S^nOL;wObWV@Lh+cGOSDS^61HVe&R0ZNLG`6m+`}uW zWC=!c2}`^NqaD33$8P=Pad=)umb>$-nvL>KLH%Du%50PdGJowmem@BCr5sOI9Z_SOArUCS)elt6}>k4@Tqp#_jnR{XXDo!bF6csA|}0tJznI_NE-ZUrxVwx zc#O;QzTHXke5tWyqWbbBGk2ab#fdB-s(Uz66|m}lC&%W3j&X{wOm+Lr7Q*VSa~y}0 z#%%t2G-dqq_)jHWj9e=*Z?I8QrtY`Z;}4@8AR;P>9GIKl^63Y>L1!_U_nZ{^T){VZ9>PN+IA(7O+fMm%I9#qM z!JhR#h`)2LOQhkAOw5BcJt|_8nWDE|J5%0`4kP_2GQV9Vye=j~XQtZq8UEeI#q)go zY^5HuN`Jl)qd3gTgM9y0& zJFq0yLjD@=r8El@F59sv5>+9=M7=B31D^I;YeS6!ZRK}Y&Y1eOx=S62BJWFKvfANKmi(x~l- zh$#50c+CB+CbLtgtg9U=)eEd2<=PK14y^1z5Erxwi$lG9Qq8;IO(-N!Q&Mvrmb~%x z{Th0SJamDch4d;WLsHtL$f3i{!J8Aui9MSS%Z@8lSB^9=zWYGix^m(Ar>k^0L>dHb zQ+PIgazmZ?3dC|W`1X0>N|M!HVx9Z!Q>EE(iO)tV6FSFB8?KGU6Kh4@4k)CWpp+%= zE@qfgn>NN^3R2QXxEnJkQ|{+zW-O=Wq_3+Dm$iDynH{VDcAS<%*o^qb-#3G z-pCbf4bMc#`W6XaO}YE+;*msGdGwQpdok$XO@so#wS;KZ{Rt;QBoiM>exl`GvZ?`;*o zaBrG<_9h{~+upe2>cjl!1uMGcJns76KPF7BokBoZ`%{i-9b|LD9b*5=W9g9}ecsg9 z#?C?`Ap~Wa2wfjC-H)AQG9x0o*e-=b*NC?D3SDEuFGFIp%|J^@4R_M_g${xKr-q;H zxTq<4DF0?Wzb~T)DDp4}??Loh$rS(6&>cSyOQJuA;d?(xU_q2y1pdYaUG6xA*7>SC ze=2W({Vun9=FFS$7jpZ(?O%$y%+mmCRGA-a?sG%@wS0&P1koFn)(fS7dMOnUy`5+7 zgNE@r(_I@PeaBssytsYlzJLE|7gl(_q1Dizocix)z%So{^sa*B1lADl5$QX7M7oIYpFS@+WsK|joGkDC@y^fhuQd*i3HL?YbN}?DP*@N| z+Ft#~kNE6#CZ^ChP^EMbDykW7lQA+N{miRC!h%89b{j&_Ll`8k2SIm?#0N)jvt7VH z!BA*x!@UtBOG*fPIHzU@z~@L~95UqqB@8d1EcRrx=c68wqcs6Qi4^f(?IplN{~nT3 zE&>{r2Jqq7Pa`BO3tAE;GH;Qp7%u~hV}zmR2@roIyWA)uLL%w_F1$>5g^X4)E^Qnf z1BtWskQBxGX`#Pe5~(rw7Ls@0=MoAhtaKur6Yj?~X8%tdZ2N3q%!i`$*ZK&+f@Nqm z2@brsL%q;-*7yuGLgxfBqK`*?hpp!kNYdmEExW^12D|QHO{wpKNTFWeV^d-AjgE5% z1l%FKW&$Z~qW=s#;bRN1CNe1xPpnvL7J@ z5VQ8>YhT*;lszJyIo4-u`3d~}6WLAPTvf1i$+~pw>qJdw8V#2&_L=~F`Hk}NKq57x z8xT(^GhE_8DfSr(Y`|j30CD>o1c*H?&-i)qAgzIsWiT4#j%*7p54|DoJ_NqeDhqc| z#7TaG9I${2A_|cJ_-IS~5O!G%nO~+5?p0=>2<`@~0PjRxw*QaaET^$*R)?gwUn7Nv z@{X-wJc^jv@FPeHMDS{k+yRJ-(ifP6!D~oix0VRC8_?k4$R@N9 zLQ>Bix|ctG0_brYBxO`~Q%GWx2W+3L+SfN9I>i=WxYqp0mpb_je%4lze_Q0Ccl8B+ zVGD($9nflT)1@BNXJj<3MDhJF8G}7`91L`}_|a#SXul$zw#fLz9vayvK;=XxA}BF8 zDrHfNV_G_$WO^G3d+IrV59vpjg+`c$#xTkTh!>}P2d8W?FkRDR&oQYF35>P>pL{ID z{eSYYkBV}CD(9fciOv+momux064i*qX@9JA9&`xg+kDe?@z!Bh4CH~0>W>}QNa3#= zYTqIl4|T^VZf$GQ2;yofq;Pb0dzA(uA?9vtaDpyH@gQOZ(riprtp z`_=@sWRgIj&)HMd+C+ykL_ksJAV&XGGebtMeWv1gx@VUJMGAoN&I zU5VSY*y7?|LLE{Um7<7YRmWL=3ohf%Ao=wi?7H~wm&pde8je_MDf&?*3>5i+f8UP) z$psoO;|OP@)E!k~I&!Xi_QhL`=aUt1M%6B9_u*3QEg>)v!2<&K6`zrv}nDJV+lOafAj@j|RDqQ$weD z4qxOX9@SzEnf+D;cFP8E^V$mQmz?@=(3VkWnk0wejiy@a*`@{AyqS?A$h3Riw7B20 zDOCC%VP>J=)t=hsx~#c9uJ*Opj+*2F0h;BF= zMNx6E>MwzjEJiKfug8X2MzSrlE}f=Z&!hDJ$QOHx&rz{@CSI2WHp#5UsB}5sfJS+N z@uSN+F1N3*o?Pq3zmzR@zMd|d#)=)68kd!sKA<+VB3ifSVDB4w&QV;ZK4vfLw-Ww| z`UV2{r#2@mr)8?=vco@i9-L+g!eUtSLP|+P^FwbpLIk~iAX7cMKpIb8j+I;;vD@#| z^**G#C}^%(Gz5aj2b|4GYnP@%WUn7=6p_RWWM|IHJz{a3ng5{SnrzMpaQ7ipLsyss zwhZ$A9yxH!zMuSec6LX%1nwTru|m<|l)`Jkjh!6T zO#c&0r7h*}Y>M^n<_bR(QFg=?uSZ5ydHcqiP58ymGE!wJ z@{FA_hr9d8T@O&S@*PlfpJQbZ(U=@(DdY4@pKHFllPRd%Qie0K!X&^rh45u ze2cWvNO(^3D$cQSbl2JL>Q8YBSDP*?alQrvUT+)6iWIE-ybq(@j!xbCv|VGM|F&=Gf{_f{!UnW*#Y93r{%dz!d8lPJ}rj&9T|2s3MoJ{lXH ziE8W+`4IRxba(aAC+TsyM;mm9oTNhES1%8L?F&}>v8;<|z5?bMqRZ6K5ssd@OO?SF zm41LzyhC5L^h*lK&QI$vcM+K4BeCj_d|^~)e@|+pgM3T%$QQJ^5}~v?$_VYyyd-~S zOYlHK2p!EuQC+la)5XQQ&NwPcJPxX|)VR?=s>8uumLDB@^!8cu7Kk0dWUsk;iDz)p zUH80{eWykLV)sRXM4rH5h&#rq6pd3oPe8#s>(X6B9*lTJmA>N*Q zp=WIPG&Z=eUMTvm6?fntkRkXKUVlh;4BzvVRZ{qqP>nscTSj<4wcp5Hf(TTj+f#P( zSExOyTUhXfl@)xh;;}k|CG65rBs~MDo%G)OL`xn zMb)I%gAtVkTaYVNtM3D`n|Jp?XRC1INH#h&fW%O(NxUVVr$Q9Hx9+S79qFaZqFK9_ zNiSvo!;$Okw~5kP_vMKil4_Q3*I&&?-SjH@wOIJ~2WP|&GswPie;pak&kBB_n6{i{!zSSK zGrxAekVX7T_~5CNkn$C_ypf>&#Jnm&!}6VeLhVa$B>sdYN~HdMtSS}ls!f`M%`GqI z4;HSTPencKleUgMD=#@SZ1zY*IU10+ocn{bN}jw&>8Lx$nSL2jTeI40E%1aTbk0v5 zMO~WTsyh@RhM}bCA*|(lqDZ^LpY9H6hTP3~_Oh?1cG)xLJ zzEh#r{l(nEE?1b`ory&*N{n%OUwlR5Ueo;3lIq~|AC}a#I>eGnii9u_=0I?VpmP+X z9GFrc_>DkqVGB?G0Vt<%SYj=`=6z^e{&oD_Z>NL_=JB9u`!uEglZpjS$}!OvjNwS~ zUSgVc3>&yoZD5)UdEJH!vTdMW3&wKFAN!YnE$sd;{hIK_M!mzm>*FJ<^|iW;wKs*0 z*IG)|g{L3NR#w#z-XalRMub{;*A7?9*z5n=1^)X3ppCaQd&OuwaVeQ3j3gx3s#KM^ zQq}qSFWfGFgLk13foYSi7UU7SMmd~ME;ZC|c`fy1dTqND*J^7 z(X{y*M?3;WgKxI-TK6@m(r)Ps6vxi0T5GZ58Fp(bmqK6^rQws=l-N<+vXH&;jbQKjk;aN*XrdLnAH8(&r@*p9{zmuO~4dWhc>qt#CT zPs{0w&dQSfC;5(FhSN43GJls5Y{Il`kAWQ8-!=GZ2RrFub5+`pWV(zmI!Y%~&|lVl z&=W~^8|vl5-#R8Q9-%Kh2}AcEG>MFg zk<4~WM=LNa#ZzLOz;oEtc|E@94wZ0hRXyO#`z-QBt6P_2jUb=k4uL%)hwklE>PX{6 zJVByHW*F6%>eWeWEYkdLrb1?~wzC?&)dp{uoSxn6ct{f3={CzIkF#yfLDU=ZH$RiQ zT(&Oz0Qec&8fzY$PnOvvu>Rpt8!Cz~84T3P8`4#k!ENW!xfO3|gddHKxbWl_ICc+< z6?q23R(O*|nvdrDZsC~s07WiQIuu+T#y^Z9es(t=`>^W zH{~mLxE4J+_q1e*1&_k4^P(x+^X3QTl&uVk4k}ygUa4?6X3Z0&7wEJuw6-$Eh#kIIVSDPTGhoc}%=W5%M8xVX1RqEfFB;4K-zw*!Hcp>XAlNOmy--@fSc~ zG8ZVMWml=LuLSIQLv)@yZcdr6i?rZma2VgQzcj`HdeRuUgf9S%d zb#7MT#jX8L+g}_`I;N~3{NKRTpRcEK5mY}MnYOh1fApU>#XJGOZ(msJ_v2W`. + +Installation +------------ + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + pip install opentelemetry-ext-otcollector + +Run the Example +--------------- + +Before running the example, it's necessary to run the OpenTelemetry collector +and Prometheus. The :scm_web:`docker ` +folder contains the a docker-compose template with the configuration of those +services. + +.. code-block:: sh + + pip install docker-compose + cd docker + docker-compose up + + +Now, the example can be executed: + +.. code-block:: sh + + python collector.py + + +The metrics are available in the Prometheus dashboard at http://localhost:9090/graph, +look for the "requests" metric on the list. + +Useful links +------------ + +- OpenTelemetry_ +- OTCollector_ +- :doc:`../../api/metrics` +- :doc:`../../ext/otcollector/otcollector` + +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +.. _OTCollector: https://github.com/open-telemetry/opentelemetry-collector \ No newline at end of file diff --git a/examples/metrics/collector.py b/docs/examples/otcollector-metrics/collector.py similarity index 60% rename from examples/metrics/collector.py rename to docs/examples/otcollector-metrics/collector.py index 929cfd43f3..5e1f401e38 100644 --- a/examples/metrics/collector.py +++ b/docs/examples/otcollector-metrics/collector.py @@ -13,8 +13,7 @@ # limitations under the License. # """ -This module serves as an example for a simple application using metrics -exporting to Collector +This example shows how to export metrics to the OT collector. """ from opentelemetry import metrics @@ -24,30 +23,25 @@ from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export.controller import PushController -# Meter is responsible for creating and recording metrics -metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__) -# exporter to export metrics to OT Collector exporter = CollectorMetricsExporter( service_name="basic-service", endpoint="localhost:55678" ) -# controller collects metrics created from meter and exports it via the -# exporter every interval + +metrics.set_meter_provider(MeterProvider()) +meter = metrics.get_meter(__name__) controller = PushController(meter, exporter, 5) -counter = meter.create_metric( - "requests", - "number of requests", - "requests", - int, - Counter, - ("environment",), +requests_counter = meter.create_metric( + name="requests", + description="number of requests", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), ) -# Labelsets are used to identify key-values that are associated with a specific -# metric that you want to record. These are useful for pre-aggregation and can -# be used to store custom dimensions pertaining to a metric -label_set = meter.get_label_set({"environment": "staging"}) +staging_label_set = meter.get_label_set({"environment": "staging"}) +requests_counter.add(25, staging_label_set) -counter.add(25, label_set) +print("Metrics are available now at http://localhost:9090/graph") input("Press any key to exit...") diff --git a/examples/metrics/docker/collector-config.yaml b/docs/examples/otcollector-metrics/docker/collector-config.yaml similarity index 100% rename from examples/metrics/docker/collector-config.yaml rename to docs/examples/otcollector-metrics/docker/collector-config.yaml diff --git a/examples/metrics/docker/docker-compose.yaml b/docs/examples/otcollector-metrics/docker/docker-compose.yaml similarity index 100% rename from examples/metrics/docker/docker-compose.yaml rename to docs/examples/otcollector-metrics/docker/docker-compose.yaml diff --git a/examples/metrics/docker/prometheus.yaml b/docs/examples/otcollector-metrics/docker/prometheus.yaml similarity index 100% rename from examples/metrics/docker/prometheus.yaml rename to docs/examples/otcollector-metrics/docker/prometheus.yaml diff --git a/docs/examples/otcollector-tracer/README.rst b/docs/examples/otcollector-tracer/README.rst new file mode 100644 index 0000000000..d82803df40 --- /dev/null +++ b/docs/examples/otcollector-tracer/README.rst @@ -0,0 +1,50 @@ +OT Collector Tracer Exporter Example +==================================== + +This example shows how to export traces to the OT collector. + +The source files of this example are available :scm_web:`here `. + +Installation +------------ + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + pip install opentelemetry-ext-otcollector + +Run the Example +--------------- + +Before running the example, it's necessary to run the OpenTelemetry collector +and Jaeger. The :scm_web:`docker ` +folder contains the a docker-compose template with the configuration of those +services. + +.. code-block:: sh + + pip install docker-compose + cd docker + docker-compose up + + +Now, the example can be executed: + +.. code-block:: sh + + python collector.py + + +The traces are available in the Jaeger UI at http://localhost:16686/. + +Useful links +------------ + +- OpenTelemetry_ +- OTCollector_ +- :doc:`../../api/trace` +- :doc:`../../ext/otcollector/otcollector` + +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +.. _OTCollector: https://github.com/open-telemetry/opentelemetry-collector \ No newline at end of file diff --git a/docs/examples/otcollector-tracer/collector.py b/docs/examples/otcollector-tracer/collector.py new file mode 100644 index 0000000000..5591effd33 --- /dev/null +++ b/docs/examples/otcollector-tracer/collector.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from opentelemetry import trace +from opentelemetry.ext.otcollector.trace_exporter import CollectorSpanExporter +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + +exporter = CollectorSpanExporter( + service_name="basic-service", endpoint="localhost:55678" +) + +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer(__name__) +span_processor = BatchExportSpanProcessor(exporter) + +trace.get_tracer_provider().add_span_processor(span_processor) +with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): + print("Hello world from OpenTelemetry Python!") diff --git a/docs/examples/basic_tracer/docker/collector-config.yaml b/docs/examples/otcollector-tracer/docker/collector-config.yaml similarity index 100% rename from docs/examples/basic_tracer/docker/collector-config.yaml rename to docs/examples/otcollector-tracer/docker/collector-config.yaml diff --git a/docs/examples/basic_tracer/docker/docker-compose.yaml b/docs/examples/otcollector-tracer/docker/docker-compose.yaml similarity index 100% rename from docs/examples/basic_tracer/docker/docker-compose.yaml rename to docs/examples/otcollector-tracer/docker/docker-compose.yaml diff --git a/docs/examples/prometheus/README.rst b/docs/examples/prometheus/README.rst new file mode 100644 index 0000000000..963687fdd9 --- /dev/null +++ b/docs/examples/prometheus/README.rst @@ -0,0 +1,34 @@ +Prometheus Metrics Exporter Example +=================================== + +This example shows how to export metrics to Prometheus. + +The source files of this example are available :scm_web:`here `. + +Installation +------------ + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + pip install opentelemetry-ext-prometheus + +Run the Example +--------------- + +.. code-block:: sh + + python prometheus.py + + +The metrics are available at http://localhost:8000/. + +Useful links +------------ + +- OpenTelemetry_ +- :doc:`../../api/metrics` +- :doc:`OpenTelemetry Prometheus Exporter <../../ext/prometheus/prometheus>` + +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ diff --git a/docs/examples/metrics/prometheus.py b/docs/examples/prometheus/prometheus.py similarity index 53% rename from docs/examples/metrics/prometheus.py rename to docs/examples/prometheus/prometheus.py index c3754c89a2..8806d00780 100644 --- a/docs/examples/metrics/prometheus.py +++ b/docs/examples/prometheus/prometheus.py @@ -13,43 +13,36 @@ # limitations under the License. # """ -This module serves as an example for a simple application using metrics -Examples show how to recording affects the collection of metrics to be exported +This example shows how to export metrics to Prometheus. """ from prometheus_client import start_http_server from opentelemetry import metrics from opentelemetry.ext.prometheus import PrometheusMetricsExporter -from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider from opentelemetry.sdk.metrics.export.controller import PushController # Start Prometheus client start_http_server(port=8000, addr="localhost") -# Meter is responsible for creating and recording metrics metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__) -# exporter to export metrics to Prometheus -prefix = "MyAppPrefix" -exporter = PrometheusMetricsExporter(prefix) -# controller collects metrics created from meter and exports it via the -# exporter every interval + +exporter = PrometheusMetricsExporter(prefix="MyAppPrefix") controller = PushController(meter, exporter, 5) -counter = meter.create_metric( - "requests", - "number of requests", - "requests", - int, - Counter, - ("environment",), +requests_counter = meter.create_metric( + name="requests", + description="number of requests", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), ) -# Labelsets are used to identify key-values that are associated with a specific -# metric that you want to record. These are useful for pre-aggregation and can -# be used to store custom dimensions pertaining to a metric -label_set = meter.get_label_set({"environment": "staging"}) +staging_label_set = meter.get_label_set({"environment": "staging"}) +requests_counter.add(25, staging_label_set) -counter.add(25, label_set) +print("Metrics are available now at http://localhost:8000/") input("Press any key to exit...") diff --git a/docs/ext/dbapi/dbapi.rst b/docs/ext/dbapi/dbapi.rst index dbe6dbbeab..d87d968b4b 100644 --- a/docs/ext/dbapi/dbapi.rst +++ b/docs/ext/dbapi/dbapi.rst @@ -1,8 +1,5 @@ -.. include:: ../../../ext/opentelemetry-ext-dbapi/README.rst - - -Module contents ---------------- +OpenTelemetry Database API Integration +====================================== .. automodule:: opentelemetry.ext.dbapi :members: diff --git a/docs/ext/flask/flask.rst b/docs/ext/flask/flask.rst index e65323cc81..2fc19de3ec 100644 --- a/docs/ext/flask/flask.rst +++ b/docs/ext/flask/flask.rst @@ -1,8 +1,5 @@ -.. include:: ../../../ext/opentelemetry-ext-flask/README.rst - - -Module contents ---------------- +OpenTelemetry Flask Integration +=============================== .. automodule:: opentelemetry.ext.flask :members: diff --git a/docs/ext/http_requests/http_requests.rst b/docs/ext/http_requests/http_requests.rst index 779be3e033..9cc09de547 100644 --- a/docs/ext/http_requests/http_requests.rst +++ b/docs/ext/http_requests/http_requests.rst @@ -1,7 +1,5 @@ -.. include:: ../../../ext/opentelemetry-ext-http-requests/README.rst - -Module contents ---------------- +OpenTelemetry requests Integration +================================== .. automodule:: opentelemetry.ext.http_requests :members: diff --git a/docs/ext/jaeger/jaeger.rst b/docs/ext/jaeger/jaeger.rst index 70b9c04205..d7b93a6f10 100644 --- a/docs/ext/jaeger/jaeger.rst +++ b/docs/ext/jaeger/jaeger.rst @@ -1,13 +1,12 @@ -.. include:: ../../../ext/opentelemetry-ext-jaeger/README.rst - -Module contents ---------------- +Opentelemetry Jaeger Exporter +============================= .. automodule:: opentelemetry.ext.jaeger :members: :undoc-members: :show-inheritance: + Submodules ---------- diff --git a/docs/ext/mysql/mysql.rst b/docs/ext/mysql/mysql.rst index e2c01371cd..4fd4749731 100644 --- a/docs/ext/mysql/mysql.rst +++ b/docs/ext/mysql/mysql.rst @@ -1,8 +1,5 @@ -.. include:: ../../../ext/opentelemetry-ext-mysql/README.rst - - -Module contents ---------------- +OpenTelemetry MySQL Integration +=============================== .. automodule:: opentelemetry.ext.mysql :members: diff --git a/docs/ext/otcollector/otcollector.rst b/docs/ext/otcollector/otcollector.rst index c940bfacaa..286aed6c08 100644 --- a/docs/ext/otcollector/otcollector.rst +++ b/docs/ext/otcollector/otcollector.rst @@ -1,8 +1,5 @@ -.. include:: ../../../ext/opentelemetry-ext-otcollector/README.rst - - -Module contents ---------------- +OpenTelemetry Collector Exporter +================================ .. automodule:: opentelemetry.ext.otcollector :members: diff --git a/docs/ext/prometheus/prometheus.rst b/docs/ext/prometheus/prometheus.rst index 7f331cb6da..9ca7754af9 100644 --- a/docs/ext/prometheus/prometheus.rst +++ b/docs/ext/prometheus/prometheus.rst @@ -1,8 +1,5 @@ -.. include:: ../../../ext/opentelemetry-ext-prometheus/README.rst - - -Module contents ---------------- +OpenTelemetry Prometheus Exporter +================================= .. automodule:: opentelemetry.ext.prometheus :members: diff --git a/docs/ext/psycopg2/psycopg2.rst b/docs/ext/psycopg2/psycopg2.rst index 89cebce345..c9c0037546 100644 --- a/docs/ext/psycopg2/psycopg2.rst +++ b/docs/ext/psycopg2/psycopg2.rst @@ -1,8 +1,5 @@ -.. include:: ../../../ext/opentelemetry-ext-psycopg2/README.rst - - -Module contents ---------------- +OpenTelemetry Psycopg Integration +================================= .. automodule:: opentelemetry.ext.psycopg2 :members: diff --git a/docs/ext/pymongo/pymongo.rst b/docs/ext/pymongo/pymongo.rst index 848c0dba25..e75f4f4168 100644 --- a/docs/ext/pymongo/pymongo.rst +++ b/docs/ext/pymongo/pymongo.rst @@ -1,7 +1,5 @@ -.. include:: ../../../ext/opentelemetry-ext-pymongo/README.rst - -Module contents ---------------- +OpenTelemetry pymongo Integration +================================= .. automodule:: opentelemetry.ext.pymongo :members: diff --git a/docs/ext/wsgi/wsgi.rst b/docs/ext/wsgi/wsgi.rst index be8194bdd6..af2bd4dd36 100644 --- a/docs/ext/wsgi/wsgi.rst +++ b/docs/ext/wsgi/wsgi.rst @@ -1,8 +1,5 @@ -.. include:: ../../../ext/opentelemetry-ext-wsgi/README.rst - - -Module contents ---------------- +OpenTelemetry WSGI Middleware +============================= .. automodule:: opentelemetry.ext.wsgi :members: diff --git a/docs/index.rst b/docs/index.rst index bc09b5a475..fd60e72f03 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -71,7 +71,7 @@ install sdk/sdk .. toctree:: - :maxdepth: 1 + :maxdepth: 2 :caption: OpenTelemetry Integrations :name: integrations :glob: diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst index b3f91511c3..1ff464cb48 100644 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -1,31 +1,21 @@ OpenTelemetry Database API integration ====================================== -The trace integration with Database API supports libraries following the specification. +|pypi| -.. PEP 249 -- Python Database API Specification v2.0: https://www.python.org/dev/peps/pep-0249/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-dbapi.svg + :target: https://pypi.org/project/opentelemetry-ext-dbapi/ -Usage ------ +Installation +------------ -.. code-block:: python +:: - import mysql.connector - import pyodbc - - from opentelemetry.ext.dbapi import trace_integration - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.trace import tracer_provider - - trace.set_tracer_provider(TracerProvider()) - tracer = trace.get_tracer(__name__) - # Ex: mysql.connector - trace_integration(tracer_provider(), mysql.connector, "connect", "mysql", "sql") - # Ex: pyodbc - trace_integration(tracer_provider(), pyodbc, "Connection", "odbc", "sql") + pip install opentelemetry-ext-dbapi References ---------- +* `OpenTelemetry Database API integration `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index 88e9d3a0b1..7b015dd2fb 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -13,9 +13,28 @@ # limitations under the License. """ -The opentelemetry-ext-dbapi package allows tracing queries made by the -ibraries following Ptyhon Database API specification: -https://www.python.org/dev/peps/pep-0249/ +The trace integration with Database API supports libraries following the +`Python Database API Specification v2.0. `_ + +Usage +----- + +.. code-block:: python + + import mysql.connector + import pyodbc + from opentelemetry.trace import tracer_provider + from opentelemetry.ext.dbapi import trace_integration + + trace.set_tracer_provider(TracerProvider()) + tracer = trace.get_tracer(__name__) + # Ex: mysql.connector + trace_integration(tracer_provider(), mysql.connector, "connect", "mysql", "sql") + # Ex: pyodbc + trace_integration(tracer_provider(), pyodbc, "Connection", "odbc", "sql") + +API +--- """ import functools @@ -40,6 +59,7 @@ def trace_integration( ): """Integrate with DB API library. https://www.python.org/dev/peps/pep-0249/ + Args: tracer: The :class:`Tracer` to use. connect_module: Module name where connect method is available. diff --git a/ext/opentelemetry-ext-flask/README.rst b/ext/opentelemetry-ext-flask/README.rst index 182f0960b2..135b2c398c 100644 --- a/ext/opentelemetry-ext-flask/README.rst +++ b/ext/opentelemetry-ext-flask/README.rst @@ -1,35 +1,24 @@ -OpenTelemetry Flask tracing +OpenTelemetry Flask Tracing =========================== -This library builds on the OpenTelemetry WSGI middleware to track web requests -in Flask applications. In addition to opentelemetry-ext-wsgi, it supports -flask-specific features such as: - -* The Flask endpoint name is used as the Span name. -* The ``http.route`` Span attribute is set so that one can see which URL rule - matched a request. +|pypi| -Usage ------ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-flask.svg + :target: https://pypi.org/project/opentelemetry-ext-flask/ -.. code-block:: python - - from flask import Flask - from opentelemetry.ext.flask import instrument_app +This library builds on the OpenTelemetry WSGI middleware to track web requests +in Flask applications. - app = Flask(__name__) - instrument_app(app) # This is where the magic happens. ✨ +Installation +------------ - @app.route("/") - def hello(): - return "Hello!" +:: - if __name__ == "__main__": - app.run(debug=True) + pip install opentelemetry-ext-flask References ---------- +* `OpenTelemetry Flask Tracing `_ * `OpenTelemetry Project `_ -* `OpenTelemetry WSGI extension `_ diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index b30b42d3fd..11c027ecbc 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -1,6 +1,37 @@ # Note: This package is not named "flask" because of # https://github.com/PyCQA/pylint/issues/2648 +""" +This library builds on the OpenTelemetry WSGI middleware to track web requests +in Flask applications. In addition to opentelemetry-ext-wsgi, it supports +flask-specific features such as: + +* The Flask endpoint name is used as the Span name. +* The ``http.route`` Span attribute is set so that one can see which URL rule + matched a request. + +Usage +----- + +.. code-block:: python + + from flask import Flask + from opentelemetry.ext.flask import instrument_app + + app = Flask(__name__) + instrument_app(app) # This is where the magic happens. ✨ + + @app.route("/") + def hello(): + return "Hello!" + + if __name__ == "__main__": + app.run(debug=True) + +API +--- +""" + import logging from flask import request as flask_request diff --git a/ext/opentelemetry-ext-http-requests/README.rst b/ext/opentelemetry-ext-http-requests/README.rst index a4b79005b5..0b05b2643f 100644 --- a/ext/opentelemetry-ext-http-requests/README.rst +++ b/ext/opentelemetry-ext-http-requests/README.rst @@ -1,4 +1,4 @@ -OpenTelemetry requests integration +OpenTelemetry requests Integration ================================== |pypi| @@ -6,7 +6,8 @@ OpenTelemetry requests integration .. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-http-requests.svg :target: https://pypi.org/project/opentelemetry-ext-http-requests/ -This library allows tracing HTTP requests made by the popular `requests `_ library. +This library allows tracing HTTP requests made by the +`requests `_ library. Installation ------------ @@ -15,28 +16,8 @@ Installation pip install opentelemetry-ext-http-requests -Usage ------ - -.. code-block:: python - - import requests - import opentelemetry.ext.http_requests - from opentelemetry.trace import tracer_provider - - opentelemetry.ext.http_requests.enable(tracer_provider()) - response = requests.get(url='https://www.example.org/') - -Limitations ------------ - -Note that calls that do not use the higher-level APIs but use -:code:`requests.sessions.Session.send` (or an alias thereof) directly, are -currently not traced. If you find any other way to trigger an untraced HTTP -request, please report it via a GitHub issue with :code:`[requests: untraced -API]` in the title. - References ---------- +* `OpenTelemetry requests Integration `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index 8e4b3e2cc0..517069ad0d 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -13,8 +13,32 @@ # limitations under the License. """ -The opentelemetry-ext-requests package allows tracing HTTP requests made by the -popular requests library. +This library allows tracing HTTP requests made by the +`requests `_ library. + +Usage +----- + +.. code-block:: python + + import requests + import opentelemetry.ext.http_requests + from opentelemetry.trace import tracer_provider + + opentelemetry.ext.http_requests.enable(tracer_provider()) + response = requests.get(url='https://www.example.org/') + +Limitations +----------- + +Note that calls that do not use the higher-level APIs but use +:code:`requests.sessions.Session.send` (or an alias thereof) directly, are +currently not traced. If you find any other way to trigger an untraced HTTP +request, please report it via a GitHub issue with :code:`[requests: untraced +API]` in the title. + +API +--- """ import functools diff --git a/ext/opentelemetry-ext-jaeger/README.rst b/ext/opentelemetry-ext-jaeger/README.rst index 23be60ac68..caa3afa932 100644 --- a/ext/opentelemetry-ext-jaeger/README.rst +++ b/ext/opentelemetry-ext-jaeger/README.rst @@ -13,58 +13,16 @@ Installation :: - pip install opentelemetry-ext-jaeger - - -Usage ------ - -The **OpenTelemetry Jaeger Exporter** allows to export `OpenTelemetry`_ traces to `Jaeger`_. -This exporter always send traces to the configured agent using Thrift compact protocol over UDP. -An optional collector can be configured, in this case Thrift binary protocol over HTTP is used. -gRPC is still not supported by this implementation. + pip install opentelemetry-ext-jaeger .. _Jaeger: https://www.jaegertracing.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ -.. code:: python - - from opentelemetry import trace - from opentelemetry.ext import jaeger - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - - trace.set_tracer_provider(TracerProvider()) - tracer = trace.get_tracer(__name__) - - # create a JaegerSpanExporter - jaeger_exporter = jaeger.JaegerSpanExporter( - service_name='my-helloworld-service', - # configure agent - agent_host_name='localhost', - agent_port=6831, - # optional: configure also collector - # collector_host_name='localhost', - # collector_port=14268, - # collector_endpoint='/api/traces?format=jaeger.thrift', - # username=xxxx, # optional - # password=xxxx, # optional - ) - - # Create a BatchExportSpanProcessor and add the exporter to it - span_processor = BatchExportSpanProcessor(jaeger_exporter) - - # add to the tracer - trace.tracer_provider().add_span_processor(span_processor) - - with tracer.start_as_current_span('foo'): - print('Hello world!') - -The `examples <./examples>`_ folder contains more elaborated examples. References ---------- +* `OpenTelemetry Jaeger Exporter `_ * `Jaeger `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py index 6679ce6b7e..cee1596711 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -13,7 +13,53 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Jaeger Span Exporter for OpenTelemetry.""" +""" +The **OpenTelemetry Jaeger Exporter** allows to export `OpenTelemetry`_ traces to `Jaeger`_. +This exporter always send traces to the configured agent using Thrift compact protocol over UDP. +An optional collector can be configured, in this case Thrift binary protocol over HTTP is used. +gRPC is still not supported by this implementation. + +Usage +----- + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext import jaeger + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + + trace.set_tracer_provider(TracerProvider()) + tracer = trace.get_tracer(__name__) + + # create a JaegerSpanExporter + jaeger_exporter = jaeger.JaegerSpanExporter( + service_name='my-helloworld-service', + # configure agent + agent_host_name='localhost', + agent_port=6831, + # optional: configure also collector + # collector_host_name='localhost', + # collector_port=14268, + # collector_endpoint='/api/traces?format=jaeger.thrift', + # username=xxxx, # optional + # password=xxxx, # optional + ) + + # Create a BatchExportSpanProcessor and add the exporter to it + span_processor = BatchExportSpanProcessor(jaeger_exporter) + + # add to the tracer + trace.tracer_provider().add_span_processor(span_processor) + + with tracer.start_as_current_span('foo'): + print('Hello world!') + +API +--- +.. _Jaeger: https://www.jaegertracing.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +""" import base64 import logging diff --git a/ext/opentelemetry-ext-mysql/README.rst b/ext/opentelemetry-ext-mysql/README.rst index 087d4cb361..0a8577e867 100644 --- a/ext/opentelemetry-ext-mysql/README.rst +++ b/ext/opentelemetry-ext-mysql/README.rst @@ -1,29 +1,25 @@ -OpenTelemetry MySQL integration +OpenTelemetry MySQL Integration =============================== -The integration with MySQL supports the `mysql-connector`_ library and is specified -to ``trace_integration`` using ``'MySQL'``. +|pypi| -.. _mysql-connector: https://pypi.org/project/mysql-connector/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-mysql.svg + :target: https://pypi.org/project/opentelemetry-ext-mysql/ -Usage ------ +Integration with MySQL that supports the mysql-connector library and is +specified to trace_integration using 'MySQL'. -.. code:: python - import mysql.connector - from opentelemetry.trace import tracer_provider - from opentelemetry.ext.mysql import trace_integration +Installation +------------ - trace_integration(tracer_provider()) - cnx = mysql.connector.connect(database='MySQL_Database') - cursor = cnx.cursor() - cursor.execute("INSERT INTO test (testField) VALUES (123)" - cursor.close() - cnx.close() +:: + + pip install opentelemetry-ext-mysql References ---------- - +* `OpenTelemetry MySQL Integration `_ * `OpenTelemetry Project `_ + diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index 9c8c3e9da7..34151431ad 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -13,8 +13,29 @@ # limitations under the License. """ -The opentelemetry-ext-mysql package allows tracing MySQL queries made by the -MySQL Connector/Python library. +The integration with MySQL supports the `mysql-connector`_ library and is specified +to ``trace_integration`` using ``'MySQL'``. + +.. _mysql-connector: https://pypi.org/project/mysql-connector/ + +Usage +----- + +.. code:: python + + import mysql.connector + from opentelemetry.trace import tracer_provider + from opentelemetry.ext.mysql import trace_integration + + trace_integration(tracer_provider()) + cnx = mysql.connector.connect(database='MySQL_Database') + cursor = cnx.cursor() + cursor.execute("INSERT INTO test (testField) VALUES (123)" + cursor.close() + cnx.close() + +API +--- """ import mysql.connector diff --git a/ext/opentelemetry-ext-opentracing-shim/README.rst b/ext/opentelemetry-ext-opentracing-shim/README.rst index 2e81391219..3bba15f167 100644 --- a/ext/opentelemetry-ext-opentracing-shim/README.rst +++ b/ext/opentelemetry-ext-opentracing-shim/README.rst @@ -1,5 +1,5 @@ OpenTracing Shim for OpenTelemetry -============================================================================ +================================== |pypi| @@ -16,4 +16,5 @@ Installation References ---------- +* `OpenTracing Shim for OpenTelemetry `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-otcollector/README.rst b/ext/opentelemetry-ext-otcollector/README.rst index 8c18b65223..1a0e3c330e 100644 --- a/ext/opentelemetry-ext-otcollector/README.rst +++ b/ext/opentelemetry-ext-otcollector/README.rst @@ -16,83 +16,9 @@ Installation pip install opentelemetry-ext-otcollector -Traces Usage ------------- - -The **OpenTelemetry Collector Exporter** allows to export `OpenTelemetry`_ traces to `OpenTelemetry Collector`_. - -.. code:: python - - from opentelemetry import trace - from opentelemetry.ext.otcollector.trace_exporter import CollectorSpanExporter - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - - - # create a CollectorSpanExporter - collector_exporter = CollectorSpanExporter( - # optional: - # endpoint="myCollectorUrl:55678", - # service_name="test_service", - # host_name="machine/container name", - ) - - # Create a BatchExportSpanProcessor and add the exporter to it - span_processor = BatchExportSpanProcessor(collector_exporter) - - # Configure the tracer to use the collector exporter - tracer_provider = TracerProvider() - tracer_provider.add_span_processor(span_processor) - tracer = TracerProvider().get_tracer(__name__) - - with tracer.start_as_current_span("foo"): - print("Hello world!") - -Metrics Usage -------------- - -The **OpenTelemetry Collector Exporter** allows to export `OpenTelemetry`_ metrics to `OpenTelemetry Collector`_. - -.. code:: python - - from opentelemetry import metrics - from opentelemetry.ext.otcollector.metrics_exporter import CollectorMetricsExporter - from opentelemetry.sdk.metrics import Counter, MeterProvider - from opentelemetry.sdk.metrics.export.controller import PushController - - - # create a CollectorMetricsExporter - collector_exporter = CollectorMetricsExporter( - # optional: - # endpoint="myCollectorUrl:55678", - # service_name="test_service", - # host_name="machine/container name", - ) - - # Meter is responsible for creating and recording metrics - metrics.set_meter_provider(MeterProvider()) - meter = metrics.get_meter(__name__) - # controller collects metrics created from meter and exports it via the - # exporter every interval - controller = PushController(meter, collector_exporter, 5) - counter = meter.create_metric( - "requests", - "number of requests", - "requests", - int, - Counter, - ("environment",), - ) - # Labelsets are used to identify key-values that are associated with a specific - # metric that you want to record. These are useful for pre-aggregation and can - # be used to store custom dimensions pertaining to a metric - label_set = meter.get_label_set({"environment": "staging"}) - - counter.add(25, label_set) - - References ---------- +* `OpenTelemetry Collector Exporter `_ * `OpenTelemetry Collector `_ * `OpenTelemetry `_ diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py index 6ab2e961ec..0b2c26cb1d 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py @@ -11,3 +11,76 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +""" +The **OpenTelemetry Collector Exporter** allows to export OpenTelemetry traces to OpenTelemetry Collector. + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext.otcollector.trace_exporter import CollectorSpanExporter + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + + + # create a CollectorSpanExporter + collector_exporter = CollectorSpanExporter( + # optional: + # endpoint="myCollectorUrl:55678", + # service_name="test_service", + # host_name="machine/container name", + ) + + # Create a BatchExportSpanProcessor and add the exporter to it + span_processor = BatchExportSpanProcessor(collector_exporter) + + # Configure the tracer to use the collector exporter + tracer_provider = TracerProvider() + tracer_provider.add_span_processor(span_processor) + tracer = TracerProvider().get_tracer(__name__) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + +Metrics Usage +------------- + +The **OpenTelemetry Collector Exporter** allows to export OpenTelemetry metrics to OpenTelemetry Collector. + +.. code:: python + + from opentelemetry import metrics + from opentelemetry.ext.otcollector.metrics_exporter import CollectorMetricsExporter + from opentelemetry.sdk.metrics import Counter, MeterProvider + from opentelemetry.sdk.metrics.export.controller import PushController + + + # create a CollectorMetricsExporter + collector_exporter = CollectorMetricsExporter( + # optional: + # endpoint="myCollectorUrl:55678", + # service_name="test_service", + # host_name="machine/container name", + ) + + # Meter is responsible for creating and recording metrics + metrics.set_meter_provider(MeterProvider()) + meter = metrics.get_meter(__name__) + # controller collects metrics created from meter and exports it via the + # exporter every interval + controller = PushController(meter, collector_exporter, 5) + counter = meter.create_metric( + "requests", + "number of requests", + "requests", + int, + Counter, + ("environment",), + ) + # Labelsets are used to identify key-values that are associated with a specific + # metric that you want to record. These are useful for pre-aggregation and can + # be used to store custom dimensions pertaining to a metric + label_set = meter.get_label_set({"environment": "staging"}) + + counter.add(25, label_set) +""" diff --git a/ext/opentelemetry-ext-prometheus/README.rst b/ext/opentelemetry-ext-prometheus/README.rst index f9e3d9416b..5a85f03582 100644 --- a/ext/opentelemetry-ext-prometheus/README.rst +++ b/ext/opentelemetry-ext-prometheus/README.rst @@ -15,58 +15,9 @@ Installation pip install opentelemetry-ext-prometheus - -Usage ------ - -The **OpenTelemetry Prometheus Exporter** allows to export `OpenTelemetry`_ metrics to `Prometheus`_. - - -.. _Prometheus: https://prometheus.io/ -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ - -.. code:: python - - from opentelemetry import metrics - from opentelemetry.ext.prometheus import PrometheusMetricsExporter - from opentelemetry.sdk.metrics import Counter, MeterProvider - from opentelemetry.sdk.metrics.export.controller import PushController - from prometheus_client import start_http_server - - # Start Prometheus client - start_http_server(port=8000, addr="localhost") - - # Meter is responsible for creating and recording metrics - metrics.set_meter_provider(MeterProvider()) - meter = metrics.meter() - # exporter to export metrics to Prometheus - prefix = "MyAppPrefix" - exporter = PrometheusMetricsExporter(prefix) - # controller collects metrics created from meter and exports it via the - # exporter every interval - controller = PushController(meter, exporter, 5) - - counter = meter.create_metric( - "requests", - "number of requests", - "requests", - int, - Counter, - ("environment",), - ) - - # Labelsets are used to identify key-values that are associated with a specific - # metric that you want to record. These are useful for pre-aggregation and can - # be used to store custom dimensions pertaining to a metric - label_set = meter.get_label_set({"environment": "staging"}) - - counter.add(25, label_set) - input("Press any key to exit...") - - - References ---------- +* `OpenTelemetry Prometheus Exporter `_ * `Prometheus `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py index ebe68e3f4d..533355aaf8 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py @@ -12,7 +12,59 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Prometheus Metrics Exporter for OpenTelemetry.""" +""" +This library allows export of metrics data to `Prometheus `_. + +Usage +----- + +The **OpenTelemetry Prometheus Exporter** allows export of `OpenTelemetry`_ metrics to `Prometheus`_. + + +.. _Prometheus: https://prometheus.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + +.. code:: python + + from opentelemetry import metrics + from opentelemetry.ext.prometheus import PrometheusMetricsExporter + from opentelemetry.sdk.metrics import Counter, Meter + from opentelemetry.sdk.metrics.export.controller import PushController + from prometheus_client import start_http_server + + # Start Prometheus client + start_http_server(port=8000, addr="localhost") + + # Meter is responsible for creating and recording metrics + metrics.set_meter_provider(MeterProvider()) + meter = metrics.meter() + # exporter to export metrics to Prometheus + prefix = "MyAppPrefix" + exporter = PrometheusMetricsExporter(prefix) + # controller collects metrics created from meter and exports it via the + # exporter every interval + controller = PushController(meter, exporter, 5) + + counter = meter.create_metric( + "requests", + "number of requests", + "requests", + int, + Counter, + ("environment",), + ) + + # Labelsets are used to identify key-values that are associated with a specific + # metric that you want to record. These are useful for pre-aggregation and can + # be used to store custom dimensions pertaining to a metric + label_set = meter.get_label_set({"environment": "staging"}) + + counter.add(25, label_set) + input("Press any key to exit...") + +API +--- +""" import collections import logging diff --git a/ext/opentelemetry-ext-psycopg2/README.rst b/ext/opentelemetry-ext-psycopg2/README.rst index 00222a4013..18cee0c2b2 100644 --- a/ext/opentelemetry-ext-psycopg2/README.rst +++ b/ext/opentelemetry-ext-psycopg2/README.rst @@ -1,30 +1,20 @@ -OpenTelemetry Psycopg integration +OpenTelemetry Psycopg Integration ================================= -The integration with PostgreSQL supports the `Psycopg`_ library and is specified -to ``trace_integration`` using ``'PostgreSQL'``. +|pypi| -.. _Psycopg: http://initd.org/psycopg/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-psycopg2.svg + :target: https://pypi.org/project/opentelemetry-ext-psycopg2/ -Usage ------ +Installation +------------ -.. code-block:: python +:: - import psycopg2 - from opentelemetry import trace - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.trace.ext.psycopg2 import trace_integration + pip install opentelemetry-ext-psycopg2 - trace.set_tracer_provider(TracerProvider()) - tracer = trace.get_tracer(__name__) - trace_integration(tracer) - cnx = psycopg2.connect(database='Database') - cursor = cnx.cursor() - cursor.execute("INSERT INTO test (testField) VALUES (123)") - cursor.close() - cnx.close() References ---------- +* `OpenTelemetry Psycopg Integration `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py index 4181688489..f2fb4c4098 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py @@ -13,8 +13,32 @@ # limitations under the License. """ -The opentelemetry-ext-psycopg2 package allows tracing PostgreSQL queries made by the -Psycopg2 library. +The integration with PostgreSQL supports the `Psycopg`_ library and is specified +to ``trace_integration`` using ``'PostgreSQL'``. + +.. _Psycopg: http://initd.org/psycopg/ + +Usage +----- + +.. code-block:: python + + import psycopg2 + from opentelemetry import trace + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.trace.ext.psycopg2 import trace_integration + + trace.set_tracer_provider(TracerProvider()) + tracer = trace.get_tracer(__name__) + trace_integration(tracer) + cnx = psycopg2.connect(database='Database') + cursor = cnx.cursor() + cursor.execute("INSERT INTO test (testField) VALUES (123)") + cursor.close() + cnx.close() + +API +--- """ import logging diff --git a/ext/opentelemetry-ext-pymongo/README.rst b/ext/opentelemetry-ext-pymongo/README.rst index a4ecd9c904..ada56efed9 100644 --- a/ext/opentelemetry-ext-pymongo/README.rst +++ b/ext/opentelemetry-ext-pymongo/README.rst @@ -1,27 +1,21 @@ -OpenTelemetry pymongo integration +OpenTelemetry pymongo Integration ================================= -The integration with MongoDB supports the `pymongo`_ library and is specified -to ``trace_integration`` using ``'pymongo'``. +|pypi| -.. _pymongo: https://pypi.org/project/pymongo +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-pymongo.svg + :target: https://pypi.org/project/opentelemetry-ext-pymongo/ -Usage ------ +Installation +------------ -.. code:: python +:: - from pymongo import MongoClient - from opentelemetry.trace import tracer_provider - from opentelemetry.trace.ext.pymongo import trace_integration + pip install opentelemetry-ext-pymongo - trace_integration(tracer_provider()) - client = MongoClient() - db = client["MongoDB_Database"] - collection = db["MongoDB_Collection"] - collection.find_one() References ---------- - +* `OpenTelemetry pymongo Integration `_ * `OpenTelemetry Project `_ + diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index 3c95a30615..b27101e590 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -13,8 +13,28 @@ # limitations under the License. """ -The opentelemetry-ext-pymongo package allows tracing commands made by the -pymongo library. +The integration with MongoDB supports the `pymongo`_ library and is specified +to ``trace_integration`` using ``'pymongo'``. + +.. _pymongo: https://pypi.org/project/pymongo + +Usage +----- + +.. code:: python + + from pymongo import MongoClient + from opentelemetry.trace import tracer_provider + from opentelemetry.trace.ext.pymongo import trace_integration + + trace_integration(tracer_provider()) + client = MongoClient() + db = client["MongoDB_Database"] + collection = db["MongoDB_Collection"] + collection.find_one() + +API +--- """ from pymongo import monitoring diff --git a/ext/opentelemetry-ext-wsgi/README.rst b/ext/opentelemetry-ext-wsgi/README.rst index 82641bcaa4..12ee81cb09 100644 --- a/ext/opentelemetry-ext-wsgi/README.rst +++ b/ext/opentelemetry-ext-wsgi/README.rst @@ -18,43 +18,9 @@ Installation pip install opentelemetry-ext-wsgi -Usage (Flask) -------------- - -.. code-block:: python - - from flask import Flask - from opentelemetry.ext.wsgi import OpenTelemetryMiddleware - - app = Flask(__name__) - app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) - - @app.route("/") - def hello(): - return "Hello!" - - if __name__ == "__main__": - app.run(debug=True) - - -Usage (Django) --------------- - -Modify the application's ``wsgi.py`` file as shown below. - -.. code-block:: python - - import os - from opentelemetry.ext.wsgi import OpenTelemetryMiddleware - from django.core.wsgi import get_wsgi_application - - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') - - application = get_wsgi_application() - application = OpenTelemetryMiddleware(application) - References ---------- +* `OpenTelemetry WSGI Middleware `_ * `OpenTelemetry Project `_ * `WSGI `_ diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index b96fc057d1..aa015fab2b 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -13,9 +13,43 @@ # limitations under the License. """ -The opentelemetry-ext-wsgi package provides a WSGI middleware that can be used -on any WSGI framework (such as Django / Flask) to track requests timing through -OpenTelemetry. +This library provides a WSGI middleware that can be used on any WSGI framework +(such as Django / Flask) to track requests timing through OpenTelemetry. + +Usage (Flask) +------------- + +.. code-block:: python + + from flask import Flask + from opentelemetry.ext.wsgi import OpenTelemetryMiddleware + + app = Flask(__name__) + app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) + + @app.route("/") + def hello(): + return "Hello!" + + if __name__ == "__main__": + app.run(debug=True) + + +Usage (Django) +-------------- + +Modify the application's ``wsgi.py`` file as shown below. + +.. code-block:: python + + import os + from opentelemetry.ext.wsgi import OpenTelemetryMiddleware + from django.core.wsgi import get_wsgi_application + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') + + application = get_wsgi_application() + application = OpenTelemetryMiddleware(application) """ import functools diff --git a/ext/opentelemetry-ext-zipkin/README.rst b/ext/opentelemetry-ext-zipkin/README.rst index af119fb410..c746051992 100644 --- a/ext/opentelemetry-ext-zipkin/README.rst +++ b/ext/opentelemetry-ext-zipkin/README.rst @@ -16,52 +16,9 @@ Installation pip install opentelemetry-ext-zipkin -Usage ------ - -The **OpenTelemetry Zipkin Exporter** allows to export `OpenTelemetry`_ traces to `Zipkin`_. -This exporter always send traces to the configured Zipkin collector using HTTP. - - -.. _Zipkin: https://zipkin.io/ -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ - -.. code:: python - - from opentelemetry import trace - from opentelemetry.ext import zipkin - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - - trace.set_tracer_provider(TracerProvider()) - tracer = trace.get_tracer(__name__) - - # create a ZipkinSpanExporter - zipkin_exporter = zipkin.ZipkinSpanExporter( - service_name="my-helloworld-service", - # optional: - # host_name="localhost", - # port=9411, - # endpoint="/api/v2/spans", - # protocol="http", - # ipv4="", - # ipv6="", - # retry=False, - ) - - # Create a BatchExportSpanProcessor and add the exporter to it - span_processor = BatchExportSpanProcessor(zipkin_exporter) - - # add to the tracer - trace.tracer_provider().add_span_processor(span_processor) - - with tracer.start_as_current_span("foo"): - print("Hello world!") - -The `examples <./examples>`_ folder contains more elaborated examples. - References ---------- +* `OpenTelemetry Zipkin Exporter `_ * `Zipkin `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index 077fa9a6b4..01e455a831 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -12,7 +12,62 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Zipkin Span Exporter for OpenTelemetry.""" +""" +This library allows to export tracing data to `Zipkin `_. + +Installation +------------ + +:: + + pip install opentelemetry-ext-zipkin + + +Usage +----- + +The **OpenTelemetry Zipkin Exporter** allows to export `OpenTelemetry`_ traces to `Zipkin`_. +This exporter always send traces to the configured Zipkin collector using HTTP. + + +.. _Zipkin: https://zipkin.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext import zipkin + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + + trace.set_tracer_provider(TracerProvider()) + tracer = trace.get_tracer(__name__) + + # create a ZipkinSpanExporter + zipkin_exporter = zipkin.ZipkinSpanExporter( + service_name="my-helloworld-service", + # optional: + # host_name="localhost", + # port=9411, + # endpoint="/api/v2/spans", + # protocol="http", + # ipv4="", + # ipv6="", + # retry=False, + ) + + # Create a BatchExportSpanProcessor and add the exporter to it + span_processor = BatchExportSpanProcessor(zipkin_exporter) + + # add to the tracer + trace.tracer_provider().add_span_processor(span_processor) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + +API +--- +""" import json import logging diff --git a/tox.ini b/tox.ini index 52c82eba07..2eb35491da 100644 --- a/tox.ini +++ b/tox.ini @@ -211,6 +211,7 @@ deps = mysql-connector-python wrapt psycopg2-binary + prometheus_client changedir = docs From abdfa12dfd76613bef5cc5e9266439e983074bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 25 Mar 2020 14:00:54 -0500 Subject: [PATCH 0253/1517] Remove copyright year (#523) It is not needed and becomes a burden of maintaince work. --- docs/examples/basic_meter/basic_metrics.py | 2 +- docs/examples/basic_meter/calling_conventions.py | 2 +- docs/examples/basic_meter/observer.py | 2 +- docs/examples/basic_tracer/__init__.py | 2 +- docs/examples/basic_tracer/tests/__init__.py | 2 +- docs/examples/basic_tracer/tests/test_tracer.py | 2 +- docs/examples/basic_tracer/tracer.py | 2 +- docs/examples/http/__init__.py | 2 +- docs/examples/http/client.py | 2 +- docs/examples/http/server.py | 2 +- docs/examples/http/tests/__init__.py | 2 +- docs/examples/http/tests/test_http.py | 2 +- docs/examples/opentelemetry-example-app/setup.py | 2 +- .../src/opentelemetry_example_app/flask_example.py | 2 +- docs/examples/opentelemetry-example-app/tests/__init__.py | 2 +- .../opentelemetry-example-app/tests/test_flask_example.py | 2 +- docs/examples/otcollector-metrics/collector.py | 2 +- docs/examples/prometheus/prometheus.py | 2 +- ext/opentelemetry-ext-dbapi/setup.cfg | 2 +- ext/opentelemetry-ext-dbapi/setup.py | 2 +- .../src/opentelemetry/ext/dbapi/__init__.py | 2 +- .../src/opentelemetry/ext/dbapi/version.py | 2 +- ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py | 2 +- .../tests/pymongo/test_pymongo_functional.py | 2 +- ext/opentelemetry-ext-flask/setup.cfg | 2 +- ext/opentelemetry-ext-flask/setup.py | 2 +- .../src/opentelemetry/ext/flask/version.py | 2 +- ext/opentelemetry-ext-flask/tests/test_flask_integration.py | 2 +- ext/opentelemetry-ext-http-requests/setup.cfg | 2 +- ext/opentelemetry-ext-http-requests/setup.py | 2 +- .../src/opentelemetry/ext/http_requests/__init__.py | 2 +- .../src/opentelemetry/ext/http_requests/version.py | 2 +- .../tests/test_requests_integration.py | 2 +- ext/opentelemetry-ext-jaeger/setup.cfg | 2 +- ext/opentelemetry-ext-jaeger/setup.py | 2 +- .../src/opentelemetry/ext/jaeger/__init__.py | 2 +- .../src/opentelemetry/ext/jaeger/version.py | 2 +- ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py | 2 +- ext/opentelemetry-ext-mysql/setup.cfg | 2 +- ext/opentelemetry-ext-mysql/setup.py | 2 +- .../src/opentelemetry/ext/mysql/__init__.py | 2 +- .../src/opentelemetry/ext/mysql/version.py | 2 +- ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py | 2 +- ext/opentelemetry-ext-opentracing-shim/setup.cfg | 2 +- ext/opentelemetry-ext-opentracing-shim/setup.py | 2 +- .../src/opentelemetry/ext/opentracing_shim/__init__.py | 2 +- .../src/opentelemetry/ext/opentracing_shim/util.py | 2 +- .../src/opentelemetry/ext/opentracing_shim/version.py | 2 +- ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py | 2 +- ext/opentelemetry-ext-opentracing-shim/tests/test_util.py | 2 +- ext/opentelemetry-ext-otcollector/setup.cfg | 2 +- ext/opentelemetry-ext-otcollector/setup.py | 2 +- .../src/opentelemetry/ext/otcollector/__init__.py | 2 +- .../opentelemetry/ext/otcollector/metrics_exporter/__init__.py | 2 +- .../opentelemetry/ext/otcollector/trace_exporter/__init__.py | 2 +- .../src/opentelemetry/ext/otcollector/util.py | 2 +- .../src/opentelemetry/ext/otcollector/version.py | 2 +- .../tests/test_otcollector_metrics_exporter.py | 2 +- .../tests/test_otcollector_trace_exporter.py | 2 +- ext/opentelemetry-ext-prometheus/setup.cfg | 2 +- ext/opentelemetry-ext-prometheus/setup.py | 2 +- .../src/opentelemetry/ext/prometheus/__init__.py | 2 +- .../src/opentelemetry/ext/prometheus/version.py | 2 +- ext/opentelemetry-ext-prometheus/tests/__init__.py | 2 +- .../tests/test_prometheus_exporter.py | 2 +- ext/opentelemetry-ext-psycopg2/setup.cfg | 2 +- ext/opentelemetry-ext-psycopg2/setup.py | 2 +- .../src/opentelemetry/ext/psycopg2/__init__.py | 2 +- .../src/opentelemetry/ext/psycopg2/version.py | 2 +- .../tests/test_psycopg2_integration.py | 2 +- ext/opentelemetry-ext-pymongo/setup.cfg | 2 +- ext/opentelemetry-ext-pymongo/setup.py | 2 +- .../src/opentelemetry/ext/pymongo/__init__.py | 2 +- .../src/opentelemetry/ext/pymongo/version.py | 2 +- ext/opentelemetry-ext-pymongo/tests/test_pymongo.py | 2 +- ext/opentelemetry-ext-testutil/setup.cfg | 2 +- ext/opentelemetry-ext-testutil/setup.py | 2 +- ext/opentelemetry-ext-wsgi/setup.cfg | 2 +- ext/opentelemetry-ext-wsgi/setup.py | 2 +- .../src/opentelemetry/ext/wsgi/__init__.py | 2 +- .../src/opentelemetry/ext/wsgi/version.py | 2 +- ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py | 2 +- ext/opentelemetry-ext-zipkin/setup.cfg | 2 +- ext/opentelemetry-ext-zipkin/setup.py | 2 +- .../src/opentelemetry/ext/zipkin/__init__.py | 2 +- .../src/opentelemetry/ext/zipkin/version.py | 2 +- ext/opentelemetry-ext-zipkin/tests/__init__.py | 2 +- ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py | 2 +- opentelemetry-api/setup.py | 2 +- opentelemetry-api/src/opentelemetry/configuration/__init__.py | 2 +- opentelemetry-api/src/opentelemetry/context/__init__.py | 2 +- .../src/opentelemetry/context/aiocontextvarsfix.py | 2 +- opentelemetry-api/src/opentelemetry/context/context.py | 2 +- .../src/opentelemetry/context/contextvars_context.py | 2 +- .../src/opentelemetry/context/threadlocal_context.py | 2 +- .../src/opentelemetry/correlationcontext/__init__.py | 2 +- .../opentelemetry/correlationcontext/propagation/__init__.py | 2 +- opentelemetry-api/src/opentelemetry/logs/__init__.py | 2 +- opentelemetry-api/src/opentelemetry/metrics/__init__.py | 2 +- opentelemetry-api/src/opentelemetry/propagators/__init__.py | 2 +- opentelemetry-api/src/opentelemetry/propagators/composite.py | 2 +- opentelemetry-api/src/opentelemetry/trace/__init__.py | 2 +- .../src/opentelemetry/trace/propagation/__init__.py | 2 +- .../src/opentelemetry/trace/propagation/httptextformat.py | 2 +- .../trace/propagation/tracecontexthttptextformat.py | 2 +- opentelemetry-api/src/opentelemetry/trace/sampling.py | 2 +- opentelemetry-api/src/opentelemetry/trace/status.py | 2 +- opentelemetry-api/src/opentelemetry/util/__init__.py | 2 +- opentelemetry-api/src/opentelemetry/util/types.py | 2 +- opentelemetry-api/src/opentelemetry/util/version.py | 2 +- opentelemetry-api/tests/__init__.py | 2 +- opentelemetry-api/tests/configuration/test_configuration.py | 2 +- opentelemetry-api/tests/context/base_context.py | 2 +- opentelemetry-api/tests/context/test_context.py | 2 +- opentelemetry-api/tests/context/test_contextvars_context.py | 2 +- opentelemetry-api/tests/context/test_threadlocal_context.py | 2 +- .../tests/correlationcontext/test_correlation_context.py | 2 +- .../correlationcontext/test_correlation_context_propagation.py | 2 +- opentelemetry-api/tests/distributedcontext/__init__.py | 2 +- opentelemetry-api/tests/metrics/__init__.py | 2 +- opentelemetry-api/tests/metrics/test_metrics.py | 2 +- opentelemetry-api/tests/mypysmoke.py | 2 +- opentelemetry-api/tests/propagators/test_composite.py | 2 +- .../tests/propagators/test_global_httptextformat.py | 2 +- opentelemetry-api/tests/test_implementation.py | 2 +- opentelemetry-api/tests/trace/__init__.py | 2 +- .../tests/trace/propagation/test_tracecontexthttptextformat.py | 2 +- opentelemetry-api/tests/trace/test_defaultspan.py | 2 +- opentelemetry-api/tests/trace/test_sampling.py | 2 +- opentelemetry-api/tests/trace/test_tracer.py | 2 +- opentelemetry-sdk/setup.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/__init__.py | 2 +- .../src/opentelemetry/sdk/context/propagation/b3_format.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py | 2 +- .../src/opentelemetry/sdk/metrics/export/__init__.py | 2 +- .../src/opentelemetry/sdk/metrics/export/aggregate.py | 2 +- .../src/opentelemetry/sdk/metrics/export/batcher.py | 2 +- .../src/opentelemetry/sdk/metrics/export/controller.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 2 +- .../src/opentelemetry/sdk/trace/export/__init__.py | 2 +- .../opentelemetry/sdk/trace/export/in_memory_span_exporter.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- opentelemetry-sdk/tests/__init__.py | 2 +- opentelemetry-sdk/tests/conftest.py | 2 +- opentelemetry-sdk/tests/context/propagation/test_b3_format.py | 2 +- opentelemetry-sdk/tests/context/test_asyncio.py | 2 +- opentelemetry-sdk/tests/correlationcontext/__init__.py | 2 +- opentelemetry-sdk/tests/metrics/__init__.py | 2 +- opentelemetry-sdk/tests/metrics/export/__init__.py | 2 +- opentelemetry-sdk/tests/metrics/export/test_export.py | 2 +- opentelemetry-sdk/tests/metrics/test_implementation.py | 2 +- opentelemetry-sdk/tests/metrics/test_metrics.py | 2 +- opentelemetry-sdk/tests/resources/test_resources.py | 2 +- opentelemetry-sdk/tests/test_util.py | 2 +- opentelemetry-sdk/tests/trace/__init__.py | 2 +- opentelemetry-sdk/tests/trace/export/__init__.py | 2 +- opentelemetry-sdk/tests/trace/export/test_export.py | 2 +- .../tests/trace/export/test_in_memory_span_exporter.py | 2 +- opentelemetry-sdk/tests/trace/test_implementation.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- tests/w3c_tracecontext_validation_server.py | 2 +- 164 files changed, 164 insertions(+), 164 deletions(-) diff --git a/docs/examples/basic_meter/basic_metrics.py b/docs/examples/basic_meter/basic_metrics.py index ac06284c32..e702be42e2 100644 --- a/docs/examples/basic_meter/basic_metrics.py +++ b/docs/examples/basic_meter/basic_metrics.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/basic_meter/calling_conventions.py b/docs/examples/basic_meter/calling_conventions.py index 946dccda62..6efa737e63 100644 --- a/docs/examples/basic_meter/calling_conventions.py +++ b/docs/examples/basic_meter/calling_conventions.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/basic_meter/observer.py b/docs/examples/basic_meter/observer.py index 2b459f25be..716968b4ae 100644 --- a/docs/examples/basic_meter/observer.py +++ b/docs/examples/basic_meter/observer.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/basic_tracer/__init__.py b/docs/examples/basic_tracer/__init__.py index 88051cd8bb..c9670f8041 100644 --- a/docs/examples/basic_tracer/__init__.py +++ b/docs/examples/basic_tracer/__init__.py @@ -1,5 +1,5 @@ # pylint: disable=C0103 -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/basic_tracer/tests/__init__.py b/docs/examples/basic_tracer/tests/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/docs/examples/basic_tracer/tests/__init__.py +++ b/docs/examples/basic_tracer/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/basic_tracer/tests/test_tracer.py b/docs/examples/basic_tracer/tests/test_tracer.py index 4bd1a25c0e..8f73c2bbb0 100644 --- a/docs/examples/basic_tracer/tests/test_tracer.py +++ b/docs/examples/basic_tracer/tests/test_tracer.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/basic_tracer/tracer.py b/docs/examples/basic_tracer/tracer.py index 51b2e69b12..20c520b88a 100755 --- a/docs/examples/basic_tracer/tracer.py +++ b/docs/examples/basic_tracer/tracer.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/http/__init__.py b/docs/examples/http/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/docs/examples/http/__init__.py +++ b/docs/examples/http/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/http/client.py b/docs/examples/http/client.py index 7c5aaa4130..2b37d89597 100755 --- a/docs/examples/http/client.py +++ b/docs/examples/http/client.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/http/server.py b/docs/examples/http/server.py index ab066e99bf..67476c25db 100755 --- a/docs/examples/http/server.py +++ b/docs/examples/http/server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/http/tests/__init__.py b/docs/examples/http/tests/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/docs/examples/http/tests/__init__.py +++ b/docs/examples/http/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/http/tests/test_http.py b/docs/examples/http/tests/test_http.py index dec0ce5f38..fe2a38cec0 100644 --- a/docs/examples/http/tests/test_http.py +++ b/docs/examples/http/tests/test_http.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py index aa40b5b26b..220bcbf6ca 100644 --- a/docs/examples/opentelemetry-example-app/setup.py +++ b/docs/examples/opentelemetry-example-app/setup.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index d523224d13..a37048b7d4 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/opentelemetry-example-app/tests/__init__.py b/docs/examples/opentelemetry-example-app/tests/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/docs/examples/opentelemetry-example-app/tests/__init__.py +++ b/docs/examples/opentelemetry-example-app/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/opentelemetry-example-app/tests/test_flask_example.py b/docs/examples/opentelemetry-example-app/tests/test_flask_example.py index cbefadc532..bc903a9c60 100644 --- a/docs/examples/opentelemetry-example-app/tests/test_flask_example.py +++ b/docs/examples/opentelemetry-example-app/tests/test_flask_example.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/otcollector-metrics/collector.py b/docs/examples/otcollector-metrics/collector.py index 5e1f401e38..9bef12603a 100644 --- a/docs/examples/otcollector-metrics/collector.py +++ b/docs/examples/otcollector-metrics/collector.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/prometheus/prometheus.py b/docs/examples/prometheus/prometheus.py index 8806d00780..2561138fb1 100644 --- a/docs/examples/prometheus/prometheus.py +++ b/docs/examples/prometheus/prometheus.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 451a994f92..146e7ee216 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-dbapi/setup.py b/ext/opentelemetry-ext-dbapi/setup.py index 5e1a68ac51..f5c1b3fa81 100644 --- a/ext/opentelemetry-ext-dbapi/setup.py +++ b/ext/opentelemetry-ext-dbapi/setup.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index 7b015dd2fb..0c18c67224 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py index ef946f5872..0941210ca3 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py index afe5a49a02..f0773788bc 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py index c728aebf38..7b018dab25 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index 1d4956a82f..61545bba1b 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-flask/setup.py b/ext/opentelemetry-ext-flask/setup.py index 34b27c6034..df9742c900 100644 --- a/ext/opentelemetry-ext-flask/setup.py +++ b/ext/opentelemetry-ext-flask/setup.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py index ef946f5872..0941210ca3 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py index 9d2f256011..9f61920ce9 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg index 7537aa340f..36a81d7fc5 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-http-requests/setup.py b/ext/opentelemetry-ext-http-requests/setup.py index 2f757e6cde..d7917ed893 100644 --- a/ext/opentelemetry-ext-http-requests/setup.py +++ b/ext/opentelemetry-ext-http-requests/setup.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index 517069ad0d..418bdee2e6 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py index ef946f5872..0941210ca3 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index ea37cbbf1b..5a007f5a25 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/ext/opentelemetry-ext-jaeger/setup.cfg index a5f04f1e9b..f28cb84332 100644 --- a/ext/opentelemetry-ext-jaeger/setup.cfg +++ b/ext/opentelemetry-ext-jaeger/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-jaeger/setup.py b/ext/opentelemetry-ext-jaeger/setup.py index 44f6eb32b1..842a6f6416 100644 --- a/ext/opentelemetry-ext-jaeger/setup.py +++ b/ext/opentelemetry-ext-jaeger/setup.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py index cee1596711..77435aa261 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -1,5 +1,5 @@ # Copyright 2018, OpenCensus Authors -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index aadc5afd63..cebb24db32 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -1,5 +1,5 @@ # Copyright 2019, OpenCensus Authors -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py index 08c5a4aded..e8ad734e0e 100644 --- a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py +++ b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py @@ -1,5 +1,5 @@ # Copyright 2018, OpenCensus Authors -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 2d69dadd0c..916deb99c2 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-mysql/setup.py b/ext/opentelemetry-ext-mysql/setup.py index b2c62679e1..4ba48fb404 100644 --- a/ext/opentelemetry-ext-mysql/setup.py +++ b/ext/opentelemetry-ext-mysql/setup.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index 34151431ad..9c146c6638 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index ef946f5872..0941210ca3 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py index 3b6eaa0c64..b4f1a006a1 100644 --- a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg index a9daabee0b..aa89f10a49 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.py b/ext/opentelemetry-ext-opentracing-shim/setup.py index bbec88b500..5924121c43 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.py +++ b/ext/opentelemetry-ext-opentracing-shim/setup.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index 6e77ac5bc7..a547bca22e 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py index 97e2415e44..eb7d3d9aca 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index ef946f5872..0941210ca3 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index 1d619ab277..12c792d4e6 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_util.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_util.py index 84bdc73a6a..cbbd4b075a 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_util.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_util.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-otcollector/setup.cfg b/ext/opentelemetry-ext-otcollector/setup.cfg index 203f2a6c82..069331815e 100644 --- a/ext/opentelemetry-ext-otcollector/setup.cfg +++ b/ext/opentelemetry-ext-otcollector/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-otcollector/setup.py b/ext/opentelemetry-ext-otcollector/setup.py index ecd8419511..d5c2886ac5 100644 --- a/ext/opentelemetry-ext-otcollector/setup.py +++ b/ext/opentelemetry-ext-otcollector/setup.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py index 0b2c26cb1d..e3e5e43cdc 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py index 12715035c2..8fcfd81373 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py index 8712682ecf..495579574c 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py index 7d605ab8f9..16c5af7e73 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py index 373ae92cb8..0941210ca3 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py index ab6f4c8ccd..716dac493c 100644 --- a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py index 9a17ea6c94..a731626b08 100644 --- a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/ext/opentelemetry-ext-prometheus/setup.cfg index f6bfd7c38b..f060b7c797 100644 --- a/ext/opentelemetry-ext-prometheus/setup.cfg +++ b/ext/opentelemetry-ext-prometheus/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-prometheus/setup.py b/ext/opentelemetry-ext-prometheus/setup.py index aa968af60d..0276cd6655 100644 --- a/ext/opentelemetry-ext-prometheus/setup.py +++ b/ext/opentelemetry-ext-prometheus/setup.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py index 533355aaf8..05d16b552d 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py index 373ae92cb8..0941210ca3 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-prometheus/tests/__init__.py b/ext/opentelemetry-ext-prometheus/tests/__init__.py index 6ab2e961ec..b0a6f42841 100644 --- a/ext/opentelemetry-ext-prometheus/tests/__init__.py +++ b/ext/opentelemetry-ext-prometheus/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py index 512a3170ad..6c2d900c77 100644 --- a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py +++ b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index c7ddb5c25c..bb9bff1f44 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-psycopg2/setup.py b/ext/opentelemetry-ext-psycopg2/setup.py index a84391e6dd..df7f7c2128 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.py +++ b/ext/opentelemetry-ext-psycopg2/setup.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py index f2fb4c4098..13c990dfbc 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py index 373ae92cb8..0941210ca3 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py b/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py index 56ab3a8aae..a512ba4fa5 100644 --- a/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py +++ b/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index 927b2db335..418986cbcd 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-pymongo/setup.py b/ext/opentelemetry-ext-pymongo/setup.py index ed63ddf42d..301aded338 100644 --- a/ext/opentelemetry-ext-pymongo/setup.py +++ b/ext/opentelemetry-ext-pymongo/setup.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index b27101e590..f44bf4925e 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index ef946f5872..0941210ca3 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py index 0889d9d994..8c82e01679 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-testutil/setup.cfg b/ext/opentelemetry-ext-testutil/setup.cfg index 520df8bf97..d7fbf21c59 100644 --- a/ext/opentelemetry-ext-testutil/setup.cfg +++ b/ext/opentelemetry-ext-testutil/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-testutil/setup.py b/ext/opentelemetry-ext-testutil/setup.py index 9de576d356..33b875ed06 100644 --- a/ext/opentelemetry-ext-testutil/setup.py +++ b/ext/opentelemetry-ext-testutil/setup.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index 1db49209be..deffea3df9 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-wsgi/setup.py b/ext/opentelemetry-ext-wsgi/setup.py index 3f8ef9cc5f..886800eacf 100644 --- a/ext/opentelemetry-ext-wsgi/setup.py +++ b/ext/opentelemetry-ext-wsgi/setup.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index aa015fab2b..8ac9ec4a83 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index ef946f5872..0941210ca3 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index 1912dd0079..2e009696c5 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg index 89d60d149a..d4957f24a6 100644 --- a/ext/opentelemetry-ext-zipkin/setup.cfg +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-zipkin/setup.py b/ext/opentelemetry-ext-zipkin/setup.py index f93bbad449..a01df70c9d 100644 --- a/ext/opentelemetry-ext-zipkin/setup.py +++ b/ext/opentelemetry-ext-zipkin/setup.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index 01e455a831..ab2a6d1d94 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py index ef946f5872..0941210ca3 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-zipkin/tests/__init__.py b/ext/opentelemetry-ext-zipkin/tests/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/ext/opentelemetry-ext-zipkin/tests/__init__.py +++ b/ext/opentelemetry-ext-zipkin/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py index c779c7388f..34183e923b 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index 5eb91d1f1b..2ba3602bf3 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index f038e57016..850f0d4fe0 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 1ac837f51c..779bdc488c 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py b/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py index f0d4ce56ff..bd8100041c 100644 --- a/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py +++ b/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py @@ -1,5 +1,5 @@ # type: ignore -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 1c7cfba963..c7508603a5 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index 0d075e0776..429fd10c2c 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py b/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py index 6a0e76bb69..43e9fb7ce9 100644 --- a/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py +++ b/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py b/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py index 543c175dfe..c16d75162a 100644 --- a/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py index c336f4476b..fca9465fbb 100644 --- a/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/logs/__init__.py b/opentelemetry-api/src/opentelemetry/logs/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/opentelemetry-api/src/opentelemetry/logs/__init__.py +++ b/opentelemetry-api/src/opentelemetry/logs/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 0db7f125c8..64b47e3a12 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index 264fe06b5c..5aa53e25dc 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/propagators/composite.py b/opentelemetry-api/src/opentelemetry/propagators/composite.py index 4ec953c839..50fba01423 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/composite.py +++ b/opentelemetry-api/src/opentelemetry/propagators/composite.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 69cca344b6..4ea2ebd745 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 90e7f9dcb3..f17350c745 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/httptextformat.py index 500014d738..e15e2a0e6d 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/httptextformat.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py index 07d3ee05ab..4676501131 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/trace/sampling.py b/opentelemetry-api/src/opentelemetry/trace/sampling.py index c398efbf86..892689783d 100644 --- a/opentelemetry-api/src/opentelemetry/trace/sampling.py +++ b/opentelemetry-api/src/opentelemetry/trace/sampling.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/trace/status.py b/opentelemetry-api/src/opentelemetry/trace/status.py index 0abe9747ad..4ae2ad96d0 100644 --- a/opentelemetry-api/src/opentelemetry/trace/status.py +++ b/opentelemetry-api/src/opentelemetry/trace/status.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index 9bfc79df21..9f68ef2d39 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index 5ce93d84b2..9883680553 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/util/version.py b/opentelemetry-api/src/opentelemetry/util/version.py index ef946f5872..0941210ca3 100644 --- a/opentelemetry-api/src/opentelemetry/util/version.py +++ b/opentelemetry-api/src/opentelemetry/util/version.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/__init__.py b/opentelemetry-api/tests/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/opentelemetry-api/tests/__init__.py +++ b/opentelemetry-api/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index 754f9c4894..a9387017f9 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/context/base_context.py b/opentelemetry-api/tests/context/base_context.py index 66e6df97a2..05acc95d89 100644 --- a/opentelemetry-api/tests/context/base_context.py +++ b/opentelemetry-api/tests/context/base_context.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/context/test_context.py b/opentelemetry-api/tests/context/test_context.py index 8942a333ed..7dad1191c6 100644 --- a/opentelemetry-api/tests/context/test_context.py +++ b/opentelemetry-api/tests/context/test_context.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/context/test_contextvars_context.py b/opentelemetry-api/tests/context/test_contextvars_context.py index d19ac5ca12..7a384f1678 100644 --- a/opentelemetry-api/tests/context/test_contextvars_context.py +++ b/opentelemetry-api/tests/context/test_contextvars_context.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/context/test_threadlocal_context.py b/opentelemetry-api/tests/context/test_threadlocal_context.py index 342163020e..02c62ba180 100644 --- a/opentelemetry-api/tests/context/test_threadlocal_context.py +++ b/opentelemetry-api/tests/context/test_threadlocal_context.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/correlationcontext/test_correlation_context.py b/opentelemetry-api/tests/correlationcontext/test_correlation_context.py index 3bddb951e2..31996c6913 100644 --- a/opentelemetry-api/tests/correlationcontext/test_correlation_context.py +++ b/opentelemetry-api/tests/correlationcontext/test_correlation_context.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py b/opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py index d0ecbdb17f..c33326b173 100644 --- a/opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py +++ b/opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/distributedcontext/__init__.py b/opentelemetry-api/tests/distributedcontext/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/opentelemetry-api/tests/distributedcontext/__init__.py +++ b/opentelemetry-api/tests/distributedcontext/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/metrics/__init__.py b/opentelemetry-api/tests/metrics/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/opentelemetry-api/tests/metrics/__init__.py +++ b/opentelemetry-api/tests/metrics/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index a2552d9f57..f0f6976eb0 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/mypysmoke.py b/opentelemetry-api/tests/mypysmoke.py index 2891f3ee62..ede4af74e0 100644 --- a/opentelemetry-api/tests/mypysmoke.py +++ b/opentelemetry-api/tests/mypysmoke.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/propagators/test_composite.py b/opentelemetry-api/tests/propagators/test_composite.py index 09ac0ecf68..8c61b6dc1f 100644 --- a/opentelemetry-api/tests/propagators/test_composite.py +++ b/opentelemetry-api/tests/propagators/test_composite.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index 6045feb675..cac24f30a0 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index 6d2aa215f5..ea1e929eef 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/trace/__init__.py b/opentelemetry-api/tests/trace/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/opentelemetry-api/tests/trace/__init__.py +++ b/opentelemetry-api/tests/trace/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index a13d9630ae..f3c97aae01 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/trace/test_defaultspan.py b/opentelemetry-api/tests/trace/test_defaultspan.py index a20b55cc4e..b6595579f9 100644 --- a/opentelemetry-api/tests/trace/test_defaultspan.py +++ b/opentelemetry-api/tests/trace/test_defaultspan.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/trace/test_sampling.py b/opentelemetry-api/tests/trace/test_sampling.py index 0a3d819528..3b5f1dc6fc 100644 --- a/opentelemetry-api/tests/trace/test_sampling.py +++ b/opentelemetry-api/tests/trace/test_sampling.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py index 20c218ad8f..4fe3d20f78 100644 --- a/opentelemetry-api/tests/trace/test_tracer.py +++ b/opentelemetry-api/tests/trace/test_tracer.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index 02bfc0a9e8..4e7ff2c99b 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py index 7b87f0d8f6..ca21f8b860 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py index 3e03c9aa02..6b8d16bf56 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 55552ace7b..05d88fc764 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index 6901a4efe4..a2513b1bfb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 5b730cc804..20e5e5c209 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index f4418c6139..a2a3ec48d3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py index 03c857f04d..3b7db0e886 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 05c015de68..193f283175 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0c98cc33af..94efa16011 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index e5d96eff9e..d67f7b9f81 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py index 0818805f39..e50ca614d6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index 009a0bcdd7..b8ebd22fc8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index 893a6066d9..ee8111fdb1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index ef946f5872..0941210ca3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/__init__.py b/opentelemetry-sdk/tests/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/opentelemetry-sdk/tests/__init__.py +++ b/opentelemetry-sdk/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/conftest.py b/opentelemetry-sdk/tests/conftest.py index 59e306f130..80a79222a1 100644 --- a/opentelemetry-sdk/tests/conftest.py +++ b/opentelemetry-sdk/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py index 0cdda1bcd0..75b3f562b5 100644 --- a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index ea7ebbddbf..f213753c59 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/correlationcontext/__init__.py b/opentelemetry-sdk/tests/correlationcontext/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/opentelemetry-sdk/tests/correlationcontext/__init__.py +++ b/opentelemetry-sdk/tests/correlationcontext/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/metrics/__init__.py b/opentelemetry-sdk/tests/metrics/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/opentelemetry-sdk/tests/metrics/__init__.py +++ b/opentelemetry-sdk/tests/metrics/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/metrics/export/__init__.py b/opentelemetry-sdk/tests/metrics/export/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/opentelemetry-sdk/tests/metrics/export/__init__.py +++ b/opentelemetry-sdk/tests/metrics/export/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 33d8d07c29..5e0a8e46fb 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/metrics/test_implementation.py b/opentelemetry-sdk/tests/metrics/test_implementation.py index 1fedc9ae57..29b3b987f9 100644 --- a/opentelemetry-sdk/tests/metrics/test_implementation.py +++ b/opentelemetry-sdk/tests/metrics/test_implementation.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index dc09091c35..9212a9967c 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 16cf29057c..959e23f0de 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/test_util.py b/opentelemetry-sdk/tests/test_util.py index ead310bd8d..cacc77dbd9 100644 --- a/opentelemetry-sdk/tests/test_util.py +++ b/opentelemetry-sdk/tests/test_util.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/trace/__init__.py b/opentelemetry-sdk/tests/trace/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/opentelemetry-sdk/tests/trace/__init__.py +++ b/opentelemetry-sdk/tests/trace/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/trace/export/__init__.py b/opentelemetry-sdk/tests/trace/export/__init__.py index d853a7bcf6..b0a6f42841 100644 --- a/opentelemetry-sdk/tests/trace/export/__init__.py +++ b/opentelemetry-sdk/tests/trace/export/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index cedb596766..7a63963648 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py index 45b65fb372..1054cc413d 100644 --- a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py +++ b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/trace/test_implementation.py b/opentelemetry-sdk/tests/trace/test_implementation.py index 74d3d5a923..cd53a64395 100644 --- a/opentelemetry-sdk/tests/trace/test_implementation.py +++ b/opentelemetry-sdk/tests/trace/test_implementation.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index a0e22f9311..3c5d1f1a58 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py index c2119f9587..7790fac12d 100644 --- a/tests/w3c_tracecontext_validation_server.py +++ b/tests/w3c_tracecontext_validation_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 39654a020e1f3c54c256c7bf49440766bc7a31a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 25 Mar 2020 14:03:19 -0500 Subject: [PATCH 0254/1517] Update PyPI classifiers (#525) - Update development status to beta - Add Python 3.8 to list of languages --- docs/examples/opentelemetry-example-app/setup.py | 2 +- ext/opentelemetry-ext-dbapi/setup.cfg | 3 ++- ext/opentelemetry-ext-flask/setup.cfg | 3 ++- ext/opentelemetry-ext-http-requests/setup.cfg | 3 ++- ext/opentelemetry-ext-jaeger/setup.cfg | 3 ++- ext/opentelemetry-ext-mysql/setup.cfg | 3 ++- ext/opentelemetry-ext-opentracing-shim/setup.cfg | 3 ++- ext/opentelemetry-ext-otcollector/setup.cfg | 3 ++- ext/opentelemetry-ext-prometheus/setup.cfg | 3 ++- ext/opentelemetry-ext-psycopg2/setup.cfg | 3 ++- ext/opentelemetry-ext-pymongo/setup.cfg | 3 ++- ext/opentelemetry-ext-testutil/setup.cfg | 3 ++- ext/opentelemetry-ext-wsgi/setup.cfg | 3 ++- ext/opentelemetry-ext-zipkin/setup.cfg | 3 ++- opentelemetry-api/setup.py | 3 ++- opentelemetry-sdk/setup.py | 3 ++- 16 files changed, 31 insertions(+), 16 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py index 220bcbf6ca..c90da05047 100644 --- a/docs/examples/opentelemetry-example-app/setup.py +++ b/docs/examples/opentelemetry-example-app/setup.py @@ -20,7 +20,7 @@ author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 146e7ee216..84d3ed4a20 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index 61545bba1b..3e5f722ed9 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg index 36a81d7fc5..ced12c71eb 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/ext/opentelemetry-ext-jaeger/setup.cfg index f28cb84332..941c20d537 100644 --- a/ext/opentelemetry-ext-jaeger/setup.cfg +++ b/ext/opentelemetry-ext-jaeger/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 916deb99c2..d43bfd0645 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg index aa89f10a49..7b2278c78e 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/ext/opentelemetry-ext-otcollector/setup.cfg b/ext/opentelemetry-ext-otcollector/setup.cfg index 069331815e..804e596d06 100644 --- a/ext/opentelemetry-ext-otcollector/setup.cfg +++ b/ext/opentelemetry-ext-otcollector/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/ext/opentelemetry-ext-prometheus/setup.cfg index f060b7c797..ed0b5e43c1 100644 --- a/ext/opentelemetry-ext-prometheus/setup.cfg +++ b/ext/opentelemetry-ext-prometheus/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index bb9bff1f44..25ee7012a6 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index 418986cbcd..2dad6e62cb 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/ext/opentelemetry-ext-testutil/setup.cfg b/ext/opentelemetry-ext-testutil/setup.cfg index d7fbf21c59..833a61b767 100644 --- a/ext/opentelemetry-ext-testutil/setup.cfg +++ b/ext/opentelemetry-ext-testutil/setup.cfg @@ -22,7 +22,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -31,6 +31,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index deffea3df9..885e9c2dfb 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg index d4957f24a6..66326cbd58 100644 --- a/ext/opentelemetry-ext-zipkin/setup.cfg +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index 2ba3602bf3..61ad9ab320 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -28,7 +28,7 @@ author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", @@ -37,6 +37,7 @@ "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", ], description="OpenTelemetry Python API", include_package_data=True, diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index 4e7ff2c99b..ce17cb4577 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -30,7 +30,7 @@ author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", @@ -39,6 +39,7 @@ "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", ], description="OpenTelemetry Python SDK", include_package_data=True, From 144ab3956f2f9d2833387e21ec00fc33c7a2c05d Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Thu, 26 Mar 2020 16:42:10 +0100 Subject: [PATCH 0255/1517] Add indicator if SpanContext was propagated from remote parent (#516) According to the spec a SpanContext should have an indicator if it was propagated from a remote parent. Introduces an is_remote flag on the SpanContext which gets set when the SpanContext is extracted in a propagaton. --- .../tests/test_jaeger_exporter.py | 13 ++++-- .../tests/test_shim.py | 15 +++++-- .../tests/test_otcollector_trace_exporter.py | 14 +++++-- .../tests/test_zipkin_exporter.py | 14 +++++-- .../src/opentelemetry/trace/__init__.py | 17 +++++--- .../propagation/tracecontexthttptextformat.py | 1 + .../test_tracecontexthttptextformat.py | 3 +- .../tests/trace/test_defaultspan.py | 6 ++- .../tests/trace/test_sampling.py | 42 ++++++++++++++----- .../sdk/context/propagation/b3_format.py | 1 + .../src/opentelemetry/sdk/trace/__init__.py | 6 ++- .../context/propagation/test_b3_format.py | 4 ++ opentelemetry-sdk/tests/trace/test_trace.py | 11 +++++ 13 files changed, 116 insertions(+), 31 deletions(-) diff --git a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py index e8ad734e0e..eac90455ad 100644 --- a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py +++ b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py @@ -31,6 +31,7 @@ def setUp(self): context = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, + is_remote=False, ) self._test_span = trace.Span("test_span", context=context) @@ -133,9 +134,15 @@ def test_translate_to_jaeger(self): start_times[2] + durations[2], ) - span_context = trace_api.SpanContext(trace_id, span_id) - parent_context = trace_api.SpanContext(trace_id, parent_id) - other_context = trace_api.SpanContext(trace_id, other_id) + span_context = trace_api.SpanContext( + trace_id, span_id, is_remote=False + ) + parent_context = trace_api.SpanContext( + trace_id, parent_id, is_remote=False + ) + other_context = trace_api.SpanContext( + trace_id, other_id, is_remote=False + ) event_attributes = { "annotation_bool": True, diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index 12c792d4e6..9f62b90ddf 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -450,7 +450,7 @@ def test_log_event(self): def test_span_context(self): """Test construction of `SpanContextShim` objects.""" - otel_context = trace.SpanContext(1234, 5678) + otel_context = trace.SpanContext(1234, 5678, is_remote=False) context = opentracingshim.SpanContextShim(otel_context) self.assertIsInstance(context, opentracing.SpanContext) @@ -476,7 +476,9 @@ def test_span_on_error(self): def test_inject_http_headers(self): """Test `inject()` method for Format.HTTP_HEADERS.""" - otel_context = trace.SpanContext(trace_id=1220, span_id=7478) + otel_context = trace.SpanContext( + trace_id=1220, span_id=7478, is_remote=False + ) context = opentracingshim.SpanContextShim(otel_context) headers = {} @@ -487,7 +489,9 @@ def test_inject_http_headers(self): def test_inject_text_map(self): """Test `inject()` method for Format.TEXT_MAP.""" - otel_context = trace.SpanContext(trace_id=1220, span_id=7478) + otel_context = trace.SpanContext( + trace_id=1220, span_id=7478, is_remote=False + ) context = opentracingshim.SpanContextShim(otel_context) # Verify Format.TEXT_MAP @@ -499,7 +503,9 @@ def test_inject_text_map(self): def test_inject_binary(self): """Test `inject()` method for Format.BINARY.""" - otel_context = trace.SpanContext(trace_id=1220, span_id=7478) + otel_context = trace.SpanContext( + trace_id=1220, span_id=7478, is_remote=False + ) context = opentracingshim.SpanContextShim(otel_context) # Verify exception for non supported binary format. @@ -561,6 +567,7 @@ def extract( trace.SpanContext( trace_id=int(trace_id_list[0]), span_id=int(span_id_list[0]), + is_remote=True, ) ) ) diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py index a731626b08..636639bb1a 100644 --- a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py @@ -92,11 +92,16 @@ def test_translate_to_collector(self): span_context = trace_api.SpanContext( trace_id, span_id, + is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state=trace_api.TraceState({"testKey": "testValue"}), ) - parent_context = trace_api.SpanContext(trace_id, parent_id) - other_context = trace_api.SpanContext(trace_id, span_id) + parent_context = trace_api.SpanContext( + trace_id, parent_id, is_remote=False + ) + other_context = trace_api.SpanContext( + trace_id, span_id, is_remote=False + ) event_attributes = { "annotation_bool": True, "annotation_string": "annotation_test", @@ -279,7 +284,10 @@ def test_export(self): trace_id = 0x6E0C63257DE34C926F9EFCD03927272E span_id = 0x34BF92DEEFC58C92 span_context = trace_api.SpanContext( - trace_id, span_id, trace_flags=TraceFlags(TraceFlags.SAMPLED) + trace_id, + span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), ) otel_spans = [ trace.Span( diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py index 34183e923b..22c64b8015 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -35,6 +35,7 @@ def setUp(self): context = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, + is_remote=False, ) self._test_span = trace.Span("test_span", context=context) @@ -114,10 +115,17 @@ def test_export(self): ) span_context = trace_api.SpanContext( - trace_id, span_id, trace_flags=TraceFlags(TraceFlags.SAMPLED) + trace_id, + span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + parent_context = trace_api.SpanContext( + trace_id, parent_id, is_remote=False + ) + other_context = trace_api.SpanContext( + trace_id, other_id, is_remote=False ) - parent_context = trace_api.SpanContext(trace_id, parent_id) - other_context = trace_api.SpanContext(trace_id, other_id) event_attributes = { "annotation_bool": True, diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 4ea2ebd745..69cfb157d6 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -319,12 +319,14 @@ class SpanContext: span_id: This span's ID. trace_flags: Trace options to propagate. trace_state: Tracing-system-specific info to propagate. + is_remote: True if propagated from a remote parent. """ def __init__( self, trace_id: int, span_id: int, + is_remote: bool, trace_flags: "TraceFlags" = DEFAULT_TRACE_OPTIONS, trace_state: "TraceState" = DEFAULT_TRACE_STATE, ) -> None: @@ -336,13 +338,17 @@ def __init__( self.span_id = span_id self.trace_flags = trace_flags self.trace_state = trace_state + self.is_remote = is_remote def __repr__(self) -> str: - return "{}(trace_id={}, span_id={}, trace_state={!r})".format( + return ( + "{}(trace_id={}, span_id={}, trace_state={!r}, is_remote={})" + ).format( type(self).__name__, format_trace_id(self.trace_id), format_span_id(self.span_id), self.trace_state, + self.is_remote, ) def is_valid(self) -> bool: @@ -402,10 +408,11 @@ def set_status(self, status: Status) -> None: INVALID_SPAN_ID = 0x0000000000000000 INVALID_TRACE_ID = 0x00000000000000000000000000000000 INVALID_SPAN_CONTEXT = SpanContext( - INVALID_TRACE_ID, - INVALID_SPAN_ID, - DEFAULT_TRACE_OPTIONS, - DEFAULT_TRACE_STATE, + trace_id=INVALID_TRACE_ID, + span_id=INVALID_SPAN_ID, + is_remote=False, + trace_flags=DEFAULT_TRACE_OPTIONS, + trace_state=DEFAULT_TRACE_STATE, ) INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py index 4676501131..732ce96c66 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py @@ -105,6 +105,7 @@ def extract( span_context = trace.SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), + is_remote=True, trace_flags=trace.TraceFlags(trace_flags), trace_state=tracestate, ) diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index f3c97aae01..11a8ecd56e 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -72,6 +72,7 @@ def test_headers_with_tracestate(self): self.assertEqual( span_context.trace_state, {"foo": "1", "bar": "2", "baz": "3"} ) + self.assertTrue(span_context.is_remote) output = {} # type:typing.Dict[str, str] span = trace.DefaultSpan(span_context) @@ -155,7 +156,7 @@ def test_no_send_empty_tracestate(self): """ output = {} # type:typing.Dict[str, str] span = trace.DefaultSpan( - trace.SpanContext(self.TRACE_ID, self.SPAN_ID) + trace.SpanContext(self.TRACE_ID, self.SPAN_ID, is_remote=False) ) ctx = set_span_in_context(span) FORMAT.inject(dict.__setitem__, output, ctx) diff --git a/opentelemetry-api/tests/trace/test_defaultspan.py b/opentelemetry-api/tests/trace/test_defaultspan.py index b6595579f9..d27f2b1bbc 100644 --- a/opentelemetry-api/tests/trace/test_defaultspan.py +++ b/opentelemetry-api/tests/trace/test_defaultspan.py @@ -20,7 +20,11 @@ class TestDefaultSpan(unittest.TestCase): def test_ctor(self): context = trace.SpanContext( - 1, 1, trace.DEFAULT_TRACE_OPTIONS, trace.DEFAULT_TRACE_STATE + 1, + 1, + is_remote=False, + trace_flags=trace.DEFAULT_TRACE_OPTIONS, + trace_state=trace.DEFAULT_TRACE_STATE, ) span = trace.DefaultSpan(context) self.assertEqual(context, span.get_context()) diff --git a/opentelemetry-api/tests/trace/test_sampling.py b/opentelemetry-api/tests/trace/test_sampling.py index 3b5f1dc6fc..0be222f3dc 100644 --- a/opentelemetry-api/tests/trace/test_sampling.py +++ b/opentelemetry-api/tests/trace/test_sampling.py @@ -25,7 +25,9 @@ class TestSampler(unittest.TestCase): def test_always_on(self): no_record_always_on = sampling.ALWAYS_ON.should_sample( - trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_DEFAULT), + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_DEFAULT + ), 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling on", @@ -34,7 +36,9 @@ def test_always_on(self): self.assertEqual(no_record_always_on.attributes, {}) sampled_always_on = sampling.ALWAYS_ON.should_sample( - trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_SAMPLED), + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_SAMPLED + ), 0xDEADBEF1, 0xDEADBEF2, "sampled parent, sampling on", @@ -44,7 +48,9 @@ def test_always_on(self): def test_always_off(self): no_record_always_off = sampling.ALWAYS_OFF.should_sample( - trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_DEFAULT), + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_DEFAULT + ), 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling off", @@ -53,7 +59,9 @@ def test_always_off(self): self.assertEqual(no_record_always_off.attributes, {}) sampled_always_on = sampling.ALWAYS_OFF.should_sample( - trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_SAMPLED), + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_SAMPLED + ), 0xDEADBEF1, 0xDEADBEF2, "sampled parent, sampling off", @@ -63,7 +71,9 @@ def test_always_off(self): def test_default_on(self): no_record_default_on = sampling.DEFAULT_ON.should_sample( - trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_DEFAULT), + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_DEFAULT + ), 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling on", @@ -72,7 +82,9 @@ def test_default_on(self): self.assertEqual(no_record_default_on.attributes, {}) sampled_default_on = sampling.DEFAULT_ON.should_sample( - trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_SAMPLED), + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_SAMPLED + ), 0xDEADBEF1, 0xDEADBEF2, "sampled parent, sampling on", @@ -82,7 +94,9 @@ def test_default_on(self): def test_default_off(self): no_record_default_off = sampling.DEFAULT_OFF.should_sample( - trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_DEFAULT), + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_DEFAULT + ), 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling off", @@ -91,7 +105,9 @@ def test_default_off(self): self.assertEqual(no_record_default_off.attributes, {}) sampled_default_off = sampling.DEFAULT_OFF.should_sample( - trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_SAMPLED), + trace.SpanContext( + 0xDEADBEEF, 0xDEADBEF0, is_remote=False, trace_flags=TO_SAMPLED + ), 0xDEADBEF1, 0xDEADBEF2, "sampled parent, sampling off", @@ -120,7 +136,10 @@ def test_probability_sampler(self): self.assertFalse( sampler.should_sample( trace.SpanContext( - 0xDEADBEF0, 0xDEADBEF1, trace_flags=TO_DEFAULT + 0xDEADBEF0, + 0xDEADBEF1, + is_remote=False, + trace_flags=TO_DEFAULT, ), 0x7FFFFFFFFFFFFFFF, 0xDEADBEEF, @@ -130,7 +149,10 @@ def test_probability_sampler(self): self.assertTrue( sampler.should_sample( trace.SpanContext( - 0xDEADBEF0, 0xDEADBEF1, trace_flags=TO_SAMPLED + 0xDEADBEF0, + 0xDEADBEF1, + is_remote=False, + trace_flags=TO_SAMPLED, ), 0x8000000000000000, 0xDEADBEEF, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py index 6b8d16bf56..e082ed03e4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -112,6 +112,7 @@ def extract( # trace an span ids are encoded in hex, so must be converted trace_id=int(trace_id, 16), span_id=int(span_id, 16), + is_remote=True, trace_flags=trace.TraceFlags(options), trace_state=trace.TraceState(), ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 94efa16011..6211d1878f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -453,7 +453,11 @@ def start_span( # pylint: disable=too-many-locals trace_state = parent_context.trace_state context = trace_api.SpanContext( - trace_id, generate_span_id(), trace_flags, trace_state + trace_id, + generate_span_id(), + is_remote=False, + trace_flags=trace_flags, + trace_state=trace_state, ) # The sampler decides whether to create a real or no-op span at the diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py index 75b3f562b5..8f06912b9e 100644 --- a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -41,6 +41,7 @@ def get_child_parent_new_carrier(old_carrier): trace_api.SpanContext( parent_context.trace_id, trace.generate_span_id(), + is_remote=False, trace_flags=parent_context.trace_flags, trace_state=parent_context.trace_state, ), @@ -90,6 +91,7 @@ def test_extract_multi_header(self): new_carrier[FORMAT.PARENT_SPAN_ID_KEY], b3_format.format_span_id(parent.context.span_id), ) + self.assertTrue(parent.context.is_remote) self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") def test_extract_single_header(self): @@ -111,6 +113,7 @@ def test_extract_single_header(self): b3_format.format_span_id(child.context.span_id), ) self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + self.assertTrue(parent.context.is_remote) child, parent, new_carrier = get_child_parent_new_carrier( { @@ -134,6 +137,7 @@ def test_extract_single_header(self): new_carrier[FORMAT.PARENT_SPAN_ID_KEY], b3_format.format_span_id(parent.context.span_id), ) + self.assertTrue(parent.context.is_remote) self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") def test_extract_header_precedence(self): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 3c5d1f1a58..acf7e66b44 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -268,6 +268,7 @@ def test_start_span_explicit(self): other_parent = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, + is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) @@ -337,6 +338,7 @@ def test_start_as_current_span_explicit(self): other_parent = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, + is_remote=False, ) self.assertIsNone(tracer.get_current_span()) @@ -378,6 +380,12 @@ def test_default_span_resource(self): # pylint: disable=protected-access self.assertIs(span.resource, resources._EMPTY_RESOURCE) + def test_span_context_remote_flag(self): + tracer = new_tracer() + + span = tracer.start_span("foo") + self.assertFalse(span.context.is_remote) + class TestSpan(unittest.TestCase): def setUp(self): @@ -564,14 +572,17 @@ def test_links(self): other_context1 = trace_api.SpanContext( trace_id=trace.generate_trace_id(), span_id=trace.generate_span_id(), + is_remote=False, ) other_context2 = trace_api.SpanContext( trace_id=trace.generate_trace_id(), span_id=trace.generate_span_id(), + is_remote=False, ) other_context3 = trace_api.SpanContext( trace_id=trace.generate_trace_id(), span_id=trace.generate_span_id(), + is_remote=False, ) links = [ trace_api.Link(other_context1), From 2a05f6890593940a5b509eb4709e8b971fea17e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez?= Date: Thu, 26 Mar 2020 20:50:06 +0100 Subject: [PATCH 0256/1517] docs: Remove authors from releases (#531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A git shortlog provides the same functionality. Signed-off-by: Daniel González Lopes --- README.md | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/README.md b/README.md index 2bb9188b55..abd77f006f 100644 --- a/README.md +++ b/README.md @@ -136,21 +136,6 @@ release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0. - Prometheus Metrics Exporter - New Examples and Improvements to Existing Examples -Thank you to the following individuals for contributing to this release: - -* Alex Boten -* Chris Kleinknecht -* Christian Neumüller -* Daniel González -* Diego Hurtado -* Golovin Pavel -* Hector Hernandez -* Jake Malachowski -* Joshua H Lang -* Leighton Chen -* Mauricio Vásquez -* Yusuke Tsutsumi - The [v0.5 beta release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.5.0) release includes: @@ -160,18 +145,6 @@ release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0. - Global configuration module - Documentation improvements -Thank you to the following individuals for contributing to this release: - -* Alex Boten -* Chris Kleinknecht -* Dave Grochowski -* Diego Hurtado -* Hector Hernandez -* Leighton Chen -* Liz Fong-Jones -* Mauricio Vásquez -* Yusuke Tsutsumi - See the [project milestones](https://github.com/open-telemetry/opentelemetry-python/milestones) for details on upcoming releases. The dates and features described here are From 44b5b045f4f3a0a3f914b41043c34eb92f93f422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 27 Mar 2020 11:33:43 -0500 Subject: [PATCH 0257/1517] docs: fix getting started guide (#512) This commit fixes a series of issues in the getting started guide: - Avoid using relative paths for docker volumes. Use `pwd` instead: https://github.com/moby/moby/issues/4830#issuecomment-264366876 - Fix indentation in prometheus config - Fix otcollector example - Missing span processor - Remove sampling_initial and sampling_thereafter keys in config --- .../docker/prometheus.yaml | 2 +- docs/getting-started.rst | 33 +++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/docs/examples/otcollector-metrics/docker/prometheus.yaml b/docs/examples/otcollector-metrics/docker/prometheus.yaml index c6f369c757..4eb2357231 100644 --- a/docs/examples/otcollector-metrics/docker/prometheus.yaml +++ b/docs/examples/otcollector-metrics/docker/prometheus.yaml @@ -1,4 +1,4 @@ -scrape_config: +scrape_configs: - job_name: 'otel-collector' scrape_interval: 5s static_configs: diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 08d6e300d6..615d71bd8a 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -285,19 +285,19 @@ Let's start by bringing up a Prometheus instance ourselves, to scrape our applic .. code-block:: yaml - # prometheus.yml + # /tmp/prometheus.yml scrape_configs: - job_name: 'my-app' - scrape_interval: 5s - static_configs: - - targets: ['localhost:8000'] + scrape_interval: 5s + static_configs: + - targets: ['localhost:8000'] And start a docker container for it: .. code-block:: sh # --net=host will not work properly outside of Linux. - docker run --net=host -v ./prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus\ + docker run --net=host -v /tmp/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus \ --log.level=debug --config.file=/etc/prometheus/prometheus.yml For our Python application, we will need to install an exporter specific to Prometheus: @@ -371,15 +371,13 @@ To see how this works in practice, let's start the Collector locally. Write the .. code-block:: yaml - # otel-collector-config.yaml + # /tmp/otel-collector-config.yaml receivers: opencensus: endpoint: 0.0.0.0:55678 exporters: logging: loglevel: debug - sampling_initial: 10 - sampling_thereafter: 50 processors: batch: queued_retry: @@ -397,8 +395,8 @@ Start the docker container: .. code-block:: sh - docker run -p 55678:55678\ - -v ./otel-collector-config.yaml:/etc/otel-collector-config.yaml\ + docker run -p 55678:55678 \ + -v /tmp/otel-collector-config.yaml:/etc/otel-collector-config.yaml \ omnition/opentelemetry-collector-contrib:latest \ --config=/etc/otel-collector-config.yaml @@ -433,6 +431,7 @@ And execute the following script: ) tracer_provider = TracerProvider() trace.set_tracer_provider(tracer_provider) + span_processor = BatchExportSpanProcessor(span_exporter) tracer_provider.add_span_processor(span_processor) # create a CollectorMetricsExporter @@ -448,7 +447,7 @@ And execute the following script: meter = metrics.get_meter(__name__) # controller collects metrics created from meter and exports it via the # exporter every interval - controller = PushController(meter, collector_exporter, 5) + controller = PushController(meter, metric_exporter, 5) # Configure the tracer to use the collector exporter tracer = trace.get_tracer_provider().get_tracer(__name__) @@ -456,13 +455,19 @@ And execute the following script: with tracer.start_as_current_span("foo"): print("Hello world!") - counter = meter.create_metric( - "requests", "number of requests", "requests", int, Counter, ("environment",), + requests_counter = meter.create_metric( + name="requests", + description="number of requests", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), ) + # Labelsets are used to identify key-values that are associated with a specific # metric that you want to record. These are useful for pre-aggregation and can # be used to store custom dimensions pertaining to a metric label_set = meter.get_label_set({"environment": "staging"}) - counter.add(25, label_set) + requests_counter.add(25, label_set) time.sleep(10) # give push_controller time to push metrics From 490f2cfb07a25006b6eae05ccae009810e34c925 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Fri, 27 Mar 2020 20:15:39 -0700 Subject: [PATCH 0258/1517] api: Missing license boilerplate fixes (#532) Removing copyright year from additional examples, to match the standardize boilerplate. --- docs/examples/jaeger_exporter/jaeger_exporter.py | 2 +- docs/examples/otcollector-tracer/collector.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/jaeger_exporter/jaeger_exporter.py b/docs/examples/jaeger_exporter/jaeger_exporter.py index f8914c353a..1b0bf5acb4 100644 --- a/docs/examples/jaeger_exporter/jaeger_exporter.py +++ b/docs/examples/jaeger_exporter/jaeger_exporter.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/examples/otcollector-tracer/collector.py b/docs/examples/otcollector-tracer/collector.py index 5591effd33..d92fa52f3a 100644 --- a/docs/examples/otcollector-tracer/collector.py +++ b/docs/examples/otcollector-tracer/collector.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 51cfe7620471d13334a508d67b12a0ae035af25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 27 Mar 2020 22:27:30 -0500 Subject: [PATCH 0259/1517] ext/jaeger: fix exporting to collector (#508) The exporting of traces to the collector is broken, it replies with the "Unable to process request body: Required field Process is not set" error. The current implementation is based on OpenCensus [1], what appears to be broken too, it's not totally clear at this time what's wrong with that. This commit changes the exporting logic to be similar to the opentelemetry-go [2] one that is working. The main change is to perform the request directly without using the client provided by the generated files. [1] https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-jaeger [2] https://github.com/open-telemetry/opentelemetry-go/blob/master/exporters/trace/jaeger/jaeger.go --- .../src/opentelemetry/ext/jaeger/__init__.py | 41 +++++++------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py index 77435aa261..7d8d7676e6 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -388,23 +388,15 @@ class Collector: Args: thrift_url: URL of the Jaeger HTTP Thrift. auth: Auth tuple that contains username and password for Basic Auth. - client: Class for creating a Jaeger collector client. - http_transport: Class for creating new client for Thrift HTTP server. """ - def __init__( - self, - thrift_url="", - auth=None, - client=jaeger.Client, - http_transport=THttpClient.THttpClient, - ): + def __init__(self, thrift_url="", auth=None): self.thrift_url = thrift_url self.auth = auth - self.http_transport = http_transport(uri_or_host=thrift_url) - self.client = client( - iprot=TBinaryProtocol.TBinaryProtocol(trans=self.http_transport) + self.http_transport = THttpClient.THttpClient( + uri_or_host=self.thrift_url ) + self.protocol = TBinaryProtocol.TBinaryProtocol(self.http_transport) # set basic auth header if auth is not None: @@ -419,18 +411,13 @@ def submit(self, batch: jaeger.Batch): Args: batch: Object to emit Jaeger spans. """ - try: - self.client.submitBatches([batch]) - # it will call http_transport.flush() and - # status code and message will be updated - code = self.http_transport.code - msg = self.http_transport.message - if code >= 300 or code < 200: - logger.error( - "Traces cannot be uploaded; HTTP status code: %s, message %s", - code, - msg, - ) - finally: - if self.http_transport.isOpen(): - self.http_transport.close() + batch.write(self.protocol) + self.http_transport.flush() + code = self.http_transport.code + msg = self.http_transport.message + if code >= 300 or code < 200: + logger.error( + "Traces cannot be uploaded; HTTP status code: %s, message: %s", + code, + msg, + ) From 6afcea979a1793906eb2c3c9ef00ae98a32cdd7d Mon Sep 17 00:00:00 2001 From: Disflux <31159882+disfluxly@users.noreply.github.com> Date: Fri, 27 Mar 2020 23:50:38 -0400 Subject: [PATCH 0260/1517] docs: Update for `get_tracer_provider()` (#503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update to the zipkin/jaeger ext READMEs and also a small doc update. 0.5.0 changed .tracer_provider() to .get_tracer_provider() Co-authored-by: Mauricio Vásquez Co-authored-by: Yusuke Tsutsumi --- .../src/opentelemetry/ext/dbapi/__init__.py | 7 ++++--- .../src/opentelemetry/ext/http_requests/__init__.py | 4 ++-- .../src/opentelemetry/ext/jaeger/__init__.py | 2 +- .../src/opentelemetry/ext/mysql/__init__.py | 7 +++++-- .../src/opentelemetry/ext/pymongo/__init__.py | 7 +++++-- .../src/opentelemetry/ext/zipkin/__init__.py | 2 +- opentelemetry-api/src/opentelemetry/metrics/__init__.py | 2 +- opentelemetry-api/src/opentelemetry/trace/__init__.py | 2 +- 8 files changed, 20 insertions(+), 13 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index 0c18c67224..ed4e93b9a3 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -23,15 +23,16 @@ import mysql.connector import pyodbc - from opentelemetry.trace import tracer_provider + from opentelemetry.ext.dbapi import trace_integration + from opentelemetry.trace import TracerProvider trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) # Ex: mysql.connector - trace_integration(tracer_provider(), mysql.connector, "connect", "mysql", "sql") + trace_integration(tracer, mysql.connector, "connect", "mysql", "sql") # Ex: pyodbc - trace_integration(tracer_provider(), pyodbc, "Connection", "odbc", "sql") + trace_integration(tracer, pyodbc, "Connection", "odbc", "sql") API --- diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index 418bdee2e6..3b04b362e5 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -23,9 +23,9 @@ import requests import opentelemetry.ext.http_requests - from opentelemetry.trace import tracer_provider + from opentelemetry.trace import TracerProvider - opentelemetry.ext.http_requests.enable(tracer_provider()) + opentelemetry.ext.http_requests.enable(TracerProvider()) response = requests.get(url='https://www.example.org/') Limitations diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py index 7d8d7676e6..8a8d40b260 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -50,7 +50,7 @@ span_processor = BatchExportSpanProcessor(jaeger_exporter) # add to the tracer - trace.tracer_provider().add_span_processor(span_processor) + trace.get_tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span('foo'): print('Hello world!') diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index 9c146c6638..db799204e5 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -24,10 +24,13 @@ .. code:: python import mysql.connector - from opentelemetry.trace import tracer_provider + from opentelemetry.trace import TracerProvider from opentelemetry.ext.mysql import trace_integration - trace_integration(tracer_provider()) + trace.set_tracer_provider(TracerProvider()) + tracer = trace.get_tracer(__name__) + + trace_integration(tracer) cnx = mysql.connector.connect(database='MySQL_Database') cursor = cnx.cursor() cursor.execute("INSERT INTO test (testField) VALUES (123)" diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index f44bf4925e..9292b88052 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -24,10 +24,13 @@ .. code:: python from pymongo import MongoClient - from opentelemetry.trace import tracer_provider + from opentelemetry.trace import TracerProvider from opentelemetry.trace.ext.pymongo import trace_integration - trace_integration(tracer_provider()) + trace.set_tracer_provider(TracerProvider()) + tracer = trace.get_tracer(__name__) + + trace_integration(tracer) client = MongoClient() db = client["MongoDB_Database"] collection = db["MongoDB_Collection"] diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index ab2a6d1d94..fdd9819144 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -60,7 +60,7 @@ span_processor = BatchExportSpanProcessor(zipkin_exporter) # add to the tracer - trace.tracer_provider().add_span_processor(span_processor) + trace.get_tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): print("Hello world!") diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 64b47e3a12..a3a1c340c8 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -425,7 +425,7 @@ def get_meter( ) -> "Meter": """Returns a `Meter` for use by the given instrumentation library. This function is a convenience wrapper for - opentelemetry.metrics.meter_provider().get_meter + opentelemetry.metrics.get_meter_provider().get_meter """ return get_meter_provider().get_meter( instrumenting_module_name, stateful, instrumenting_library_version diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 69cfb157d6..cfbc124e2e 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -660,7 +660,7 @@ def get_tracer( """Returns a `Tracer` for use by the given instrumentation library. This function is a convenience wrapper for - opentelemetry.trace.tracer_provider().get_tracer + opentelemetry.trace.get_tracer_provider().get_tracer """ return get_tracer_provider().get_tracer( instrumenting_module_name, instrumenting_library_version From 046057d54c0420f56fb405c990608113253449ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 27 Mar 2020 23:14:24 -0500 Subject: [PATCH 0261/1517] metrics: Remove LabelSet (#527) Remove LabelSet according to OTEP 90: https://github.com/open-telemetry/oteps/blob/master/text/0090-remove-labelset-from-metrics-api.md Co-Authored-By: Leighton Chen --- docs/examples/basic_meter/basic_metrics.py | 18 +-- .../basic_meter/calling_conventions.py | 18 +-- docs/examples/basic_meter/observer.py | 8 +- .../examples/otcollector-metrics/collector.py | 4 +- docs/examples/prometheus/prometheus.py | 4 +- docs/getting-started.rst | 24 ++-- docs/metrics_example.py | 4 +- .../opentelemetry/ext/otcollector/__init__.py | 6 +- .../otcollector/metrics_exporter/__init__.py | 9 +- .../test_otcollector_metrics_exporter.py | 27 +++-- .../opentelemetry/ext/prometheus/__init__.py | 8 +- .../tests/test_prometheus_exporter.py | 22 ++-- .../src/opentelemetry/metrics/__init__.py | 85 ++++---------- .../tests/metrics/test_metrics.py | 18 +-- .../tests/test_implementation.py | 8 +- .../src/opentelemetry/sdk/metrics/__init__.py | 91 ++++++-------- .../sdk/metrics/export/__init__.py | 18 ++- .../sdk/metrics/export/batcher.py | 6 +- .../tests/metrics/export/test_export.py | 61 +++++----- .../tests/metrics/test_implementation.py | 4 +- .../tests/metrics/test_metrics.py | 111 +++++++----------- 21 files changed, 230 insertions(+), 324 deletions(-) diff --git a/docs/examples/basic_meter/basic_metrics.py b/docs/examples/basic_meter/basic_metrics.py index e702be42e2..5d137bf01e 100644 --- a/docs/examples/basic_meter/basic_metrics.py +++ b/docs/examples/basic_meter/basic_metrics.py @@ -84,21 +84,21 @@ def usage(argv): label_keys=("environment",), ) -# Labelsets are used to identify key-values that are associated with a specific +# Labels are used to identify key-values that are associated with a specific # metric that you want to record. These are useful for pre-aggregation and can # be used to store custom dimensions pertaining to a metric -staging_label_set = meter.get_label_set({"environment": "staging"}) -testing_label_set = meter.get_label_set({"environment": "testing"}) +staging_labels = {"environment": "staging"} +testing_labels = {"environment": "testing"} # Update the metric instruments using the direct calling convention -requests_counter.add(25, staging_label_set) -requests_size.record(100, staging_label_set) +requests_counter.add(25, staging_labels) +requests_size.record(100, staging_labels) time.sleep(5) -requests_counter.add(50, staging_label_set) -requests_size.record(5000, staging_label_set) +requests_counter.add(50, staging_labels) +requests_size.record(5000, staging_labels) time.sleep(5) -requests_counter.add(35, testing_label_set) -requests_size.record(2, testing_label_set) +requests_counter.add(35, testing_labels) +requests_size.record(2, testing_labels) time.sleep(5) diff --git a/docs/examples/basic_meter/calling_conventions.py b/docs/examples/basic_meter/calling_conventions.py index 6efa737e63..15b57fdafc 100644 --- a/docs/examples/basic_meter/calling_conventions.py +++ b/docs/examples/basic_meter/calling_conventions.py @@ -56,27 +56,27 @@ label_keys=("environment",), ) -label_set = meter.get_label_set({"environment": "staging"}) +labels = {"environment": "staging"} print("Updating using direct calling convention...") -# You can record metrics directly using the metric instrument. You pass in a -# labelset that you would like to record for. -requests_counter.add(25, label_set) +# You can record metrics directly using the metric instrument. You pass in +# labels that you would like to record for. +requests_counter.add(25, labels) time.sleep(5) print("Updating using a bound instrument...") # You can record metrics with bound metric instruments. Bound metric -# instruments are created by passing in a labelset. A bound metric instrument +# instruments are created by passing in labels. A bound metric instrument # is essentially metric data that corresponds to a specific set of labels. # Therefore, getting a bound metric instrument using the same set of labels # will yield the same bound metric instrument. -bound_requests_counter = requests_counter.bind(label_set) +bound_requests_counter = requests_counter.bind(labels) bound_requests_counter.add(100) time.sleep(5) print("Updating using batch calling convention...") -# You can record metrics in a batch by passing in a labelset and a sequence of +# You can record metrics in a batch by passing in labels and a sequence of # (metric, value) pairs. The value would be recorded for each metric using the -# specified labelset for each. -meter.record_batch(label_set, ((requests_counter, 50), (clicks_counter, 70))) +# specified labels for each. +meter.record_batch(labels, ((requests_counter, 50), (clicks_counter, 70))) time.sleep(5) diff --git a/docs/examples/basic_meter/observer.py b/docs/examples/basic_meter/observer.py index 716968b4ae..0490fbe8ef 100644 --- a/docs/examples/basic_meter/observer.py +++ b/docs/examples/basic_meter/observer.py @@ -19,7 +19,7 @@ import psutil from opentelemetry import metrics -from opentelemetry.sdk.metrics import LabelSet, MeterProvider +from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.metrics.export.controller import PushController @@ -35,8 +35,8 @@ # Callback to gather cpu usage def get_cpu_usage_callback(observer): for (number, percent) in enumerate(psutil.cpu_percent(percpu=True)): - label_set = meter.get_label_set({"cpu_number": str(number)}) - observer.observe(percent, label_set) + labels = {"cpu_number": str(number)} + observer.observe(percent, labels) meter.register_observer( @@ -52,7 +52,7 @@ def get_cpu_usage_callback(observer): # Callback to gather RAM memory usage def get_ram_usage_callback(observer): ram_percent = psutil.virtual_memory().percent - observer.observe(ram_percent, LabelSet()) + observer.observe(ram_percent, {}) meter.register_observer( diff --git a/docs/examples/otcollector-metrics/collector.py b/docs/examples/otcollector-metrics/collector.py index 9bef12603a..b0420e1bb8 100644 --- a/docs/examples/otcollector-metrics/collector.py +++ b/docs/examples/otcollector-metrics/collector.py @@ -40,8 +40,8 @@ label_keys=("environment",), ) -staging_label_set = meter.get_label_set({"environment": "staging"}) -requests_counter.add(25, staging_label_set) +staging_labels = {"environment": "staging"} +requests_counter.add(25, staging_labels) print("Metrics are available now at http://localhost:9090/graph") input("Press any key to exit...") diff --git a/docs/examples/prometheus/prometheus.py b/docs/examples/prometheus/prometheus.py index 2561138fb1..cdb60e9cec 100644 --- a/docs/examples/prometheus/prometheus.py +++ b/docs/examples/prometheus/prometheus.py @@ -41,8 +41,8 @@ label_keys=("environment",), ) -staging_label_set = meter.get_label_set({"environment": "staging"}) -requests_counter.add(25, staging_label_set) +staging_labels = {"environment": "staging"} +requests_counter.add(25, staging_labels) print("Metrics are available now at http://localhost:8000/") input("Press any key to exit...") diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 615d71bd8a..bbd696015f 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -249,7 +249,7 @@ The following is an example of emitting metrics to console, in a similar fashion exporter = ConsoleMetricsExporter() controller = PushController(meter, exporter, 5) - staging_label_set = meter.get_label_set({"environment": "staging"}) + staging_labels = {"environment": "staging"} requests_counter = meter.create_metric( name="requests", @@ -260,10 +260,10 @@ The following is an example of emitting metrics to console, in a similar fashion label_keys=("environment",), ) - requests_counter.add(25, staging_label_set) + requests_counter.add(25, staging_labels) time.sleep(5) - requests_counter.add(20, staging_label_set) + requests_counter.add(20, staging_labels) time.sleep(5) @@ -272,8 +272,8 @@ The sleeps will cause the script to take a while, but running it should yield: .. code-block:: sh $ python metrics.py - ConsoleMetricsExporter(data="Counter(name="requests", description="number of requests")", label_set="(('environment', 'staging'),)", value=25) - ConsoleMetricsExporter(data="Counter(name="requests", description="number of requests")", label_set="(('environment', 'staging'),)", value=45) + ConsoleMetricsExporter(data="Counter(name="requests", description="number of requests")", labels="(('environment', 'staging'),)", value=25) + ConsoleMetricsExporter(data="Counter(name="requests", description="number of requests")", labels="(('environment', 'staging'),)", value=45) Using Prometheus ---------------- @@ -331,7 +331,7 @@ And use that instead of the `ConsoleMetricsExporter`: exporter = PrometheusMetricsExporter("MyAppPrefix") controller = PushController(meter, exporter, 5) - staging_label_set = meter.get_label_set({"environment": "staging"}) + staging_labels = {"environment": "staging"} requests_counter = meter.create_metric( name="requests", @@ -342,10 +342,10 @@ And use that instead of the `ConsoleMetricsExporter`: label_keys=("environment",), ) - requests_counter.add(25, staging_label_set) + requests_counter.add(25, staging_labels) time.sleep(5) - requests_counter.add(20, staging_label_set) + requests_counter.add(20, staging_labels) time.sleep(5) # This line is added to keep the HTTP server up long enough to scrape. @@ -463,11 +463,9 @@ And execute the following script: metric_type=Counter, label_keys=("environment",), ) - - # Labelsets are used to identify key-values that are associated with a specific + # Labels are used to identify key-values that are associated with a specific # metric that you want to record. These are useful for pre-aggregation and can # be used to store custom dimensions pertaining to a metric - label_set = meter.get_label_set({"environment": "staging"}) - - requests_counter.add(25, label_set) + labels = {"environment": "staging"} + requests_counter.add(25, labels) time.sleep(10) # give push_controller time to push metrics diff --git a/docs/metrics_example.py b/docs/metrics_example.py index 736b9c7f30..6ab4fd55fb 100644 --- a/docs/metrics_example.py +++ b/docs/metrics_example.py @@ -17,7 +17,7 @@ label_keys=("environment",), ) -staging_label_set = meter.get_label_set({"environment": "staging"}) -requests_counter.add(25, staging_label_set) +staging_labels = {"environment": "staging"} +requests_counter.add(25, staging_labels) input("Press a key to finish...\n") diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py index e3e5e43cdc..ae5e2ac33f 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py @@ -77,10 +77,10 @@ Counter, ("environment",), ) - # Labelsets are used to identify key-values that are associated with a specific + # Labels are used to identify key-values that are associated with a specific # metric that you want to record. These are useful for pre-aggregation and can # be used to store custom dimensions pertaining to a metric - label_set = meter.get_label_set({"environment": "staging"}) + labels = {"environment": "staging"} - counter.add(25, label_set) + counter.add(25, labels) """ diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py index 8fcfd81373..5f8424fd00 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py @@ -106,7 +106,7 @@ def translate_to_collector( label_values = [] label_keys = [] - for label_tuple in metric_record.label_set.labels: + for label_tuple in metric_record.labels: label_keys.append(metrics_pb2.LabelKey(key=label_tuple[0])) label_values.append( metrics_pb2.LabelValue( @@ -145,11 +145,12 @@ def get_collector_metric_type(metric: Metric) -> metrics_pb2.MetricDescriptor: def get_collector_point(metric_record: MetricRecord) -> metrics_pb2.Point: + # TODO: horrible hack to get original list of keys to then get the bound + # instrument + key = dict(metric_record.labels) point = metrics_pb2.Point( timestamp=utils.proto_timestamp_from_time_ns( - metric_record.metric.bind( - metric_record.label_set - ).last_update_timestamp + metric_record.metric.bind(key).last_update_timestamp ) ) if metric_record.metric.value_type == int: diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py index 716dac493c..6133b3bd88 100644 --- a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py @@ -21,7 +21,12 @@ from opentelemetry import metrics from opentelemetry.ext.otcollector import metrics_exporter -from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider +from opentelemetry.sdk.metrics import ( + Counter, + Measure, + MeterProvider, + get_labels_as_key, +) from opentelemetry.sdk.metrics.export import ( MetricRecord, MetricsExportResult, @@ -36,8 +41,8 @@ def setUpClass(cls): # pylint: disable=protected-access metrics.set_meter_provider(MeterProvider()) cls._meter = metrics.get_meter(__name__) - kvp = {"environment": "staging"} - cls._test_label_set = cls._meter.get_label_set(kvp) + cls._labels = {"environment": "staging"} + cls._key_labels = get_labels_as_key(cls._labels) def test_constructor(self): mock_get_node = mock.Mock() @@ -77,7 +82,6 @@ def test_get_collector_metric_type(self): def test_get_collector_point(self): aggregator = aggregate.CounterAggregator() - label_set = self._meter.get_label_set({"environment": "staging"}) int_counter = self._meter.create_metric( "testName", "testDescription", "unit", int, Counter ) @@ -88,7 +92,7 @@ def test_get_collector_point(self): "testName", "testDescription", "unit", float, Measure ) result = metrics_exporter.get_collector_point( - MetricRecord(aggregator, label_set, int_counter) + MetricRecord(aggregator, self._key_labels, int_counter) ) self.assertIsInstance(result, metrics_pb2.Point) self.assertIsInstance(result.timestamp, Timestamp) @@ -96,13 +100,13 @@ def test_get_collector_point(self): aggregator.update(123.5) aggregator.take_checkpoint() result = metrics_exporter.get_collector_point( - MetricRecord(aggregator, label_set, float_counter) + MetricRecord(aggregator, self._key_labels, float_counter) ) self.assertEqual(result.double_value, 123.5) self.assertRaises( TypeError, metrics_exporter.get_collector_point( - MetricRecord(aggregator, label_set, measure) + MetricRecord(aggregator, self._key_labels, measure) ), ) @@ -118,7 +122,7 @@ def test_export(self): "testname", "testdesc", "unit", int, Counter, ["environment"] ) record = MetricRecord( - aggregate.CounterAggregator(), self._test_label_set, test_metric + aggregate.CounterAggregator(), self._key_labels, test_metric ) result = collector_exporter.export([record]) @@ -137,14 +141,13 @@ def test_export(self): ) def test_translate_to_collector(self): - test_metric = self._meter.create_metric( "testname", "testdesc", "unit", int, Counter, ["environment"] ) aggregator = aggregate.CounterAggregator() aggregator.update(123) aggregator.take_checkpoint() - record = MetricRecord(aggregator, self._test_label_set, test_metric) + record = MetricRecord(aggregator, self._key_labels, test_metric) output_metrics = metrics_exporter.translate_to_collector([record]) self.assertEqual(len(output_metrics), 1) self.assertIsInstance(output_metrics[0], metrics_pb2.Metric) @@ -175,12 +178,12 @@ def test_translate_to_collector(self): self.assertEqual(len(output_metrics[0].timeseries[0].points), 1) self.assertEqual( output_metrics[0].timeseries[0].points[0].timestamp.seconds, - record.metric.bind(record.label_set).last_update_timestamp + record.metric.bind(self._labels).last_update_timestamp // 1000000000, ) self.assertEqual( output_metrics[0].timeseries[0].points[0].timestamp.nanos, - record.metric.bind(record.label_set).last_update_timestamp + record.metric.bind(self._labels).last_update_timestamp % 1000000000, ) self.assertEqual( diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py index 05d16b552d..1dd307af93 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py @@ -54,12 +54,12 @@ ("environment",), ) - # Labelsets are used to identify key-values that are associated with a specific + # Labels are used to identify key-values that are associated with a specific # metric that you want to record. These are useful for pre-aggregation and can # be used to store custom dimensions pertaining to a metric - label_set = meter.get_label_set({"environment": "staging"}) + labels = {"environment": "staging"} - counter.add(25, label_set) + counter.add(25, labels) input("Press any key to exit...") API @@ -145,7 +145,7 @@ def _translate_to_prometheus(self, metric_record: MetricRecord): prometheus_metric = None label_values = [] label_keys = [] - for label_tuple in metric_record.label_set.labels: + for label_tuple in metric_record.labels: label_keys.append(self._sanitize(label_tuple[0])) label_values.append(label_tuple[1]) diff --git a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py index 6c2d900c77..f986e0c4f5 100644 --- a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py +++ b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py @@ -39,8 +39,8 @@ def setUp(self): metrics.Counter, ["environment"], ) - kvp = {"environment": "staging"} - self._test_label_set = self._meter.get_label_set(kvp) + labels = {"environment": "staging"} + self._labels_key = metrics.get_labels_as_key(labels) self._mock_registry_register = mock.Mock() self._registry_register_patch = mock.patch( @@ -67,7 +67,7 @@ def test_shutdown(self): def test_export(self): with self._registry_register_patch: record = MetricRecord( - CounterAggregator(), self._test_label_set, self._test_metric + CounterAggregator(), self._labels_key, self._test_metric ) exporter = PrometheusMetricsExporter() result = exporter.export([record]) @@ -85,12 +85,12 @@ def test_counter_to_prometheus(self): metrics.Counter, ["environment@", "os"], ) - kvp = {"environment@": "staging", "os": "Windows"} - label_set = meter.get_label_set(kvp) + labels = {"environment@": "staging", "os": "Windows"} + key_labels = metrics.get_labels_as_key(labels) aggregator = CounterAggregator() aggregator.update(123) aggregator.take_checkpoint() - record = MetricRecord(aggregator, label_set, metric) + record = MetricRecord(aggregator, key_labels, metric) collector = CustomCollector("testprefix") collector.add_metrics_data([record]) @@ -114,11 +114,11 @@ def test_counter_to_prometheus(self): def test_invalid_metric(self): meter = get_meter_provider().get_meter(__name__) metric = meter.create_metric( - "tesname", "testdesc", "unit", int, TestMetric + "tesname", "testdesc", "unit", int, StubMetric ) - kvp = {"environment": "staging"} - label_set = meter.get_label_set(kvp) - record = MetricRecord(None, label_set, metric) + labels = {"environment": "staging"} + key_labels = metrics.get_labels_as_key(labels) + record = MetricRecord(None, key_labels, metric) collector = CustomCollector("testprefix") collector.add_metrics_data([record]) collector.collect() @@ -135,7 +135,7 @@ def test_sanitize(self): self.assertEqual(collector._sanitize("aAbBcC_12_oi"), "aAbBcC_12_oi") -class TestMetric(metrics.Metric): +class StubMetric(metrics.Metric): def __init__( self, name: str, diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index a3a1c340c8..c0ab525b7d 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -82,25 +82,6 @@ def record(self, value: ValueT) -> None: """ -class LabelSet(abc.ABC): - """A canonicalized set of labels useful for preaggregation - - Re-usable LabelSet objects provide a potential optimization for scenarios - where bound metric instruments might not be effective. For example, if the - LabelSet will be re-used but only used once per metrics, bound metric - instruments do not offer any optimization. It may best to pre-compute a - canonicalized LabelSet once and re-use it with the direct calling - convention. LabelSets are immutable and should be opaque in implementation. - """ - - -class DefaultLabelSet(LabelSet): - """The default LabelSet. - - Used when no LabelSet implementation is available. - """ - - class Metric(abc.ABC): """Base class for various types of metrics. @@ -109,7 +90,7 @@ class Metric(abc.ABC): """ @abc.abstractmethod - def bind(self, label_set: LabelSet) -> "object": + def bind(self, labels: Dict[str, str]) -> "object": """Gets a bound metric instrument. Bound metric instruments are useful to reduce the cost of repeatedly @@ -121,51 +102,51 @@ def bind(self, label_set: LabelSet) -> "object": provided are permitted. Args: - label_set: `LabelSet` to associate with the bound instrument. + labels: Labels to associate with the bound instrument. """ class DefaultMetric(Metric): """The default Metric used when no Metric implementation is available.""" - def bind(self, label_set: LabelSet) -> "DefaultBoundInstrument": + def bind(self, labels: Dict[str, str]) -> "DefaultBoundInstrument": """Gets a `DefaultBoundInstrument`. Args: - label_set: `LabelSet` to associate with the bound instrument. + labels: Labels to associate with the bound instrument. """ return DefaultBoundInstrument() - def add(self, value: ValueT, label_set: LabelSet) -> None: + def add(self, value: ValueT, labels: Dict[str, str]) -> None: """No-op implementation of `Counter` add. Args: value: The value to add to the counter metric. - label_set: `LabelSet` to associate with the bound instrument. + labels: Labels to associate with the bound instrument. """ - def record(self, value: ValueT, label_set: LabelSet) -> None: + def record(self, value: ValueT, labels: Dict[str, str]) -> None: """No-op implementation of `Measure` record. Args: value: The value to record to this measure metric. - label_set: `LabelSet` to associate with the bound instrument. + labels: Labels to associate with the bound instrument. """ class Counter(Metric): """A counter type metric that expresses the computation of a sum.""" - def bind(self, label_set: LabelSet) -> "BoundCounter": + def bind(self, labels: Dict[str, str]) -> "BoundCounter": """Gets a `BoundCounter`.""" return BoundCounter() - def add(self, value: ValueT, label_set: LabelSet) -> None: + def add(self, value: ValueT, labels: Dict[str, str]) -> None: """Increases the value of the counter by ``value``. Args: value: The value to add to the counter metric. - label_set: `LabelSet` to associate with the returned bound counter. + labels: Labels to associate with the bound instrument. """ @@ -175,16 +156,16 @@ class Measure(Metric): Measure metrics represent raw statistics that are recorded. """ - def bind(self, label_set: LabelSet) -> "BoundMeasure": + def bind(self, labels: Dict[str, str]) -> "BoundMeasure": """Gets a `BoundMeasure`.""" return BoundMeasure() - def record(self, value: ValueT, label_set: LabelSet) -> None: + def record(self, value: ValueT, labels: Dict[str, str]) -> None: """Records the ``value`` to the measure. Args: value: The value to record to this measure metric. - label_set: `LabelSet` to associate with the returned bound measure. + labels: Labels to associate with the bound instrument. """ @@ -199,24 +180,24 @@ class Observer(abc.ABC): """ @abc.abstractmethod - def observe(self, value: ValueT, label_set: LabelSet) -> None: + def observe(self, value: ValueT, labels: Dict[str, str]) -> None: """Captures ``value`` to the observer. Args: value: The value to capture to this observer metric. - label_set: `LabelSet` associated to ``value``. + labels: Labels associated to ``value``. """ class DefaultObserver(Observer): """No-op implementation of ``Observer``.""" - def observe(self, value: ValueT, label_set: LabelSet) -> None: + def observe(self, value: ValueT, labels: Dict[str, str]) -> None: """Captures ``value`` to the observer. Args: value: The value to capture to this observer metric. - label_set: `LabelSet` associated to ``value``. + labels: Labels associated to ``value``. """ @@ -286,20 +267,20 @@ class Meter(abc.ABC): @abc.abstractmethod def record_batch( self, - label_set: LabelSet, + labels: Dict[str, str], record_tuples: Sequence[Tuple["Metric", ValueT]], ) -> None: """Atomically records a batch of `Metric` and value pairs. Allows the functionality of acting upon multiple metrics with a single API call. Implementations should find bound metric instruments that - match the key-value pairs in the labelset. + match the key-value pairs in the labels. - Args: label_set: The `LabelSet` associated with all measurements in the - batch. A measurement is a tuple, representing the `Metric` being - recorded and the corresponding value to record. record_tuples: A - sequence of pairs of `Metric` s and the corresponding value to - record for that metric. + Args: + labels: Labels associated with all measurements in the + batch. + record_tuples: A sequence of pairs of `Metric` s and the + corresponding value to record for that metric. """ @abc.abstractmethod @@ -361,23 +342,13 @@ def unregister_observer(self, observer: "Observer") -> None: observer: The observer to unregister. """ - @abc.abstractmethod - def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": - """Gets a `LabelSet` with the given labels. - - Args: - labels: A dictionary representing label key to label value pairs. - - Returns: A `LabelSet` object canonicalized using the given input. - """ - class DefaultMeter(Meter): """The default Meter used when no Meter implementation is available.""" def record_batch( self, - label_set: LabelSet, + labels: Dict[str, str], record_tuples: Sequence[Tuple["Metric", ValueT]], ) -> None: pass @@ -410,10 +381,6 @@ def register_observer( def unregister_observer(self, observer: "Observer") -> None: pass - def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": - # pylint: disable=no-self-use - return DefaultLabelSet() - _METER_PROVIDER = None diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index f0f6976eb0..3e760d3d98 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -21,33 +21,28 @@ class TestMetrics(unittest.TestCase): def test_default(self): default = metrics.DefaultMetric() - default_ls = metrics.DefaultLabelSet() - bound_metric_instr = default.bind(default_ls) + bound_metric_instr = default.bind({}) self.assertIsInstance( bound_metric_instr, metrics.DefaultBoundInstrument ) def test_counter(self): counter = metrics.Counter() - label_set = metrics.LabelSet() - bound_counter = counter.bind(label_set) + bound_counter = counter.bind({}) self.assertIsInstance(bound_counter, metrics.BoundCounter) def test_counter_add(self): counter = metrics.Counter() - label_set = metrics.LabelSet() - counter.add(1, label_set) + counter.add(1, {}) def test_measure(self): measure = metrics.Measure() - label_set = metrics.LabelSet() - bound_measure = measure.bind(label_set) + bound_measure = measure.bind({}) self.assertIsInstance(bound_measure, metrics.BoundMeasure) def test_measure_record(self): measure = metrics.Measure() - label_set = metrics.LabelSet() - measure.record(1, label_set) + measure.record(1, {}) def test_default_bound_metric(self): bound_instrument = metrics.DefaultBoundInstrument() @@ -63,5 +58,4 @@ def test_bound_measure(self): def test_observer(self): observer = metrics.DefaultObserver() - label_set = metrics.LabelSet() - observer.observe(1, label_set) + observer.observe(1, {}) diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index ea1e929eef..735ac4a683 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -73,8 +73,7 @@ def test_default_meter(self): def test_record_batch(self): meter = metrics.DefaultMeter() counter = metrics.Counter() - label_set = metrics.LabelSet() - meter.record_batch(label_set, ((counter, 1),)) + meter.record_batch({}, ((counter, 1),)) def test_create_metric(self): meter = metrics.DefaultMeter() @@ -91,8 +90,3 @@ def test_unregister_observer(self): meter = metrics.DefaultMeter() observer = metrics.DefaultObserver() meter.unregister_observer(observer) - - def test_get_label_set(self): - meter = metrics.DefaultMeter() - label_set = meter.get_label_set({}) - self.assertIsInstance(label_set, metrics.DefaultLabelSet) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 05d88fc764..e6f264494d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -26,26 +26,9 @@ logger = logging.getLogger(__name__) -# pylint: disable=redefined-outer-name -class LabelSet(metrics_api.LabelSet): - """See `opentelemetry.metrics.LabelSet`.""" - - def __init__(self, labels: Dict[str, str] = None): - if labels is None: - labels = {} - # LabelSet properties used only in dictionaries for fast lookup - self._labels = tuple(labels.items()) - self._encoded = tuple(sorted(labels.items())) - - @property - def labels(self): - return self._labels - - def __hash__(self): - return hash(self._encoded) - - def __eq__(self, other): - return self._encoded == other._encoded +def get_labels_as_key(labels: Dict[str, str]) -> Tuple[Tuple[str, str]]: + """Gets a list of labels that can be used as a key in a dictionary.""" + return tuple(sorted(labels.items())) class BaseBoundInstrument: @@ -157,10 +140,11 @@ def __init__( self.bound_instruments = {} self.bound_instruments_lock = threading.Lock() - def bind(self, label_set: LabelSet) -> BaseBoundInstrument: + def bind(self, labels: Dict[str, str]) -> BaseBoundInstrument: """See `opentelemetry.metrics.Metric.bind`.""" + key = get_labels_as_key(labels) with self.bound_instruments_lock: - bound_instrument = self.bound_instruments.get(label_set) + bound_instrument = self.bound_instruments.get(key) if bound_instrument is None: bound_instrument = self.BOUND_INSTR_TYPE( self.value_type, @@ -168,7 +152,7 @@ def bind(self, label_set: LabelSet) -> BaseBoundInstrument: # Aggregator will be created based off type of metric self.meter.batcher.aggregator_for(self.__class__), ) - self.bound_instruments[label_set] = bound_instrument + self.bound_instruments[key] = bound_instrument bound_instrument.increase_ref_count() return bound_instrument @@ -186,9 +170,9 @@ class Counter(Metric, metrics_api.Counter): BOUND_INSTR_TYPE = BoundCounter - def add(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: + def add(self, value: metrics_api.ValueT, labels: Dict[str, str]) -> None: """See `opentelemetry.metrics.Counter.add`.""" - bound_intrument = self.bind(label_set) + bound_intrument = self.bind(labels) bound_intrument.add(value) bound_intrument.release() @@ -200,9 +184,11 @@ class Measure(Metric, metrics_api.Measure): BOUND_INSTR_TYPE = BoundMeasure - def record(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: + def record( + self, value: metrics_api.ValueT, labels: Dict[str, str] + ) -> None: """See `opentelemetry.metrics.Measure.record`.""" - bound_intrument = self.bind(label_set) + bound_intrument = self.bind(labels) bound_intrument.record(value) bound_intrument.release() @@ -234,7 +220,9 @@ def __init__( self.aggregators = {} - def observe(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: + def observe( + self, value: metrics_api.ValueT, labels: Dict[str, str] + ) -> None: if not self.enabled: return if not isinstance(value, self.value_type): @@ -243,12 +231,13 @@ def observe(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: ) return - if label_set not in self.aggregators: + key = get_labels_as_key(labels) + if key not in self.aggregators: # TODO: how to cleanup aggregators? - self.aggregators[label_set] = self.meter.batcher.aggregator_for( + self.aggregators[key] = self.meter.batcher.aggregator_for( self.__class__ ) - aggregator = self.aggregators[label_set] + aggregator = self.aggregators[key] aggregator.update(value) def run(self) -> bool: @@ -274,18 +263,14 @@ class Record: def __init__( self, metric: metrics_api.MetricT, - label_set: LabelSet, + labels: Dict[str, str], aggregator: Aggregator, ): self.metric = metric - self.label_set = label_set + self.labels = labels self.aggregator = aggregator -# Used when getting a LabelSet with no key/values -EMPTY_LABEL_SET = LabelSet() - - class Meter(metrics_api.Meter): """See `opentelemetry.metrics.Meter`. @@ -326,19 +311,19 @@ def _collect_metrics(self) -> None: to_remove = [] with metric.bound_instruments_lock: - for label_set, bound_instr in metric.bound_instruments.items(): + for labels, bound_instr in metric.bound_instruments.items(): # TODO: Consider storing records in memory? - record = Record(metric, label_set, bound_instr.aggregator) + record = Record(metric, labels, bound_instr.aggregator) # Checkpoints the current aggregators # Applies different batching logic based on type of batcher self.batcher.process(record) if bound_instr.ref_count() == 0: - to_remove.append(label_set) + to_remove.append(labels) # Remove handles that were released - for label_set in to_remove: - del metric.bound_instruments[label_set] + for labels in to_remove: + del metric.bound_instruments[labels] def _collect_observers(self) -> None: with self.observers_lock: @@ -350,18 +335,20 @@ def _collect_observers(self) -> None: if not observer.run(): continue - for label_set, aggregator in observer.aggregators.items(): - record = Record(observer, label_set, aggregator) + for labels, aggregator in observer.aggregators.items(): + record = Record(observer, labels, aggregator) self.batcher.process(record) def record_batch( self, - label_set: LabelSet, + labels: Dict[str, str], record_tuples: Sequence[Tuple[metrics_api.Metric, metrics_api.ValueT]], ) -> None: """See `opentelemetry.metrics.Meter.record_batch`.""" + # TODO: Avoid enconding the labels for each instrument, encode once + # and reuse. for metric, value in record_tuples: - metric.UPDATE_FUNCTION(value, label_set) + metric.UPDATE_FUNCTION(value, labels) def create_metric( self, @@ -415,18 +402,6 @@ def unregister_observer(self, observer: "Observer") -> None: with self.observers_lock: self.observers.remove(observer) - def get_label_set(self, labels: Dict[str, str]): - """See `opentelemetry.metrics.Meter.create_metric`. - - This implementation encodes the labels to use as a map key. - - Args: - labels: The dictionary of label keys to label values. - """ - if len(labels) == 0: - return EMPTY_LABEL_SET - return LabelSet(labels=labels) - class MeterProvider(metrics_api.MeterProvider): def __init__( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index a2513b1bfb..8b0fbdb1a3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -15,6 +15,9 @@ from enum import Enum from typing import Sequence, Tuple +from opentelemetry import metrics as metrics_api +from opentelemetry.sdk.metrics.export.aggregate import Aggregator + class MetricsExportResult(Enum): SUCCESS = 0 @@ -23,9 +26,14 @@ class MetricsExportResult(Enum): class MetricRecord: - def __init__(self, aggregator, label_set, metric): + def __init__( + self, + aggregator: Aggregator, + labels: Tuple[Tuple[str, str]], + metric: metrics_api.MetricT, + ): self.aggregator = aggregator - self.label_set = label_set + self.labels = labels self.metric = metric @@ -43,7 +51,7 @@ def export( Args: metric_records: A sequence of `MetricRecord` s. A `MetricRecord` - contains the metric to be exported, the label set associated + contains the metric to be exported, the labels associated with that metric, as well as the aggregator used to export the current checkpointed value. @@ -70,10 +78,10 @@ def export( ) -> "MetricsExportResult": for record in metric_records: print( - '{}(data="{}", label_set="{}", value={})'.format( + '{}(data="{}", labels="{}", value={})'.format( type(self).__name__, record.metric, - record.label_set.labels, + record.labels, record.aggregator.checkpoint, ) ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index a2a3ec48d3..7b599f4c7d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -63,8 +63,8 @@ def checkpoint_set(self) -> Sequence[MetricRecord]: data in all of the aggregators in this batcher. """ metric_records = [] - for (metric, label_set), aggregator in self._batch_map.items(): - metric_records.append(MetricRecord(aggregator, label_set, metric)) + for (metric, labels), aggregator in self._batch_map.items(): + metric_records.append(MetricRecord(aggregator, labels, metric)) return metric_records def finished_collection(self): @@ -90,7 +90,7 @@ class UngroupedBatcher(Batcher): def process(self, record): # Checkpoints the current aggregator value to be collected for export record.aggregator.take_checkpoint() - batch_key = (record.metric, record.label_set) + batch_key = (record.metric, record.labels) batch_value = self._batch_map.get(batch_key) aggregator = record.aggregator if batch_value: diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 5e0a8e46fb..311237505c 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -45,14 +45,13 @@ def test_export(self): meter, ("environment",), ) - kvp = {"environment": "staging"} - label_set = metrics.LabelSet(kvp) + labels = {"environment": "staging"} aggregator = CounterAggregator() - record = MetricRecord(aggregator, label_set, metric) - result = '{}(data="{}", label_set="{}", value={})'.format( + record = MetricRecord(aggregator, labels, metric) + result = '{}(data="{}", labels="{}", value={})'.format( ConsoleMetricsExporter.__name__, metric, - label_set.labels, + labels, aggregator.checkpoint, ) with mock.patch("sys.stdout") as mock_stdout: @@ -84,14 +83,14 @@ def test_checkpoint_set(self): ("environment",), ) aggregator.update(1.0) - label_set = metrics.LabelSet() + labels = () _batch_map = {} - _batch_map[(metric, label_set)] = aggregator + _batch_map[(metric, labels)] = aggregator batcher._batch_map = _batch_map records = batcher.checkpoint_set() self.assertEqual(len(records), 1) self.assertEqual(records[0].metric, metric) - self.assertEqual(records[0].label_set, label_set) + self.assertEqual(records[0].labels, labels) self.assertEqual(records[0].aggregator, aggregator) def test_checkpoint_set_empty(self): @@ -112,9 +111,9 @@ def test_finished_collection_stateless(self): ("environment",), ) aggregator.update(1.0) - label_set = metrics.LabelSet() + labels = () _batch_map = {} - _batch_map[(metric, label_set)] = aggregator + _batch_map[(metric, labels)] = aggregator batcher._batch_map = _batch_map batcher.finished_collection() self.assertEqual(len(batcher._batch_map), 0) @@ -132,9 +131,9 @@ def test_finished_collection_stateful(self): ("environment",), ) aggregator.update(1.0) - label_set = metrics.LabelSet() + labels = () _batch_map = {} - _batch_map[(metric, label_set)] = aggregator + _batch_map[(metric, labels)] = aggregator batcher._batch_map = _batch_map batcher.finished_collection() self.assertEqual(len(batcher._batch_map), 1) @@ -153,20 +152,18 @@ def test_ungrouped_batcher_process_exists(self): meter, ("environment",), ) - label_set = metrics.LabelSet() + labels = () _batch_map = {} - _batch_map[(metric, label_set)] = aggregator + _batch_map[(metric, labels)] = aggregator aggregator2.update(1.0) batcher._batch_map = _batch_map - record = metrics.Record(metric, label_set, aggregator2) + record = metrics.Record(metric, labels, aggregator2) batcher.process(record) self.assertEqual(len(batcher._batch_map), 1) - self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) + self.assertIsNotNone(batcher._batch_map.get((metric, labels))) + self.assertEqual(batcher._batch_map.get((metric, labels)).current, 0) self.assertEqual( - batcher._batch_map.get((metric, label_set)).current, 0 - ) - self.assertEqual( - batcher._batch_map.get((metric, label_set)).checkpoint, 1.0 + batcher._batch_map.get((metric, labels)).checkpoint, 1.0 ) def test_ungrouped_batcher_process_not_exists(self): @@ -181,19 +178,17 @@ def test_ungrouped_batcher_process_not_exists(self): meter, ("environment",), ) - label_set = metrics.LabelSet() + labels = () _batch_map = {} aggregator.update(1.0) batcher._batch_map = _batch_map - record = metrics.Record(metric, label_set, aggregator) + record = metrics.Record(metric, labels, aggregator) batcher.process(record) self.assertEqual(len(batcher._batch_map), 1) - self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) - self.assertEqual( - batcher._batch_map.get((metric, label_set)).current, 0 - ) + self.assertIsNotNone(batcher._batch_map.get((metric, labels))) + self.assertEqual(batcher._batch_map.get((metric, labels)).current, 0) self.assertEqual( - batcher._batch_map.get((metric, label_set)).checkpoint, 1.0 + batcher._batch_map.get((metric, labels)).checkpoint, 1.0 ) def test_ungrouped_batcher_process_not_stateful(self): @@ -208,19 +203,17 @@ def test_ungrouped_batcher_process_not_stateful(self): meter, ("environment",), ) - label_set = metrics.LabelSet() + labels = () _batch_map = {} aggregator.update(1.0) batcher._batch_map = _batch_map - record = metrics.Record(metric, label_set, aggregator) + record = metrics.Record(metric, labels, aggregator) batcher.process(record) self.assertEqual(len(batcher._batch_map), 1) - self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) - self.assertEqual( - batcher._batch_map.get((metric, label_set)).current, 0 - ) + self.assertIsNotNone(batcher._batch_map.get((metric, labels))) + self.assertEqual(batcher._batch_map.get((metric, labels)).current, 0) self.assertEqual( - batcher._batch_map.get((metric, label_set)).checkpoint, 1.0 + batcher._batch_map.get((metric, labels)).checkpoint, 1.0 ) diff --git a/opentelemetry-sdk/tests/metrics/test_implementation.py b/opentelemetry-sdk/tests/metrics/test_implementation.py index 29b3b987f9..1679f61834 100644 --- a/opentelemetry-sdk/tests/metrics/test_implementation.py +++ b/opentelemetry-sdk/tests/metrics/test_implementation.py @@ -14,7 +14,7 @@ import unittest -from opentelemetry.metrics import DefaultLabelSet, DefaultMeter, DefaultMetric +from opentelemetry.metrics import DefaultMeter, DefaultMetric from opentelemetry.sdk import metrics @@ -29,7 +29,5 @@ class TestMeterImplementation(unittest.TestCase): def test_meter(self): meter = metrics.MeterProvider().get_meter(__name__) metric = meter.create_metric("", "", "", float, metrics.Counter) - label_set = meter.get_label_set({"key1": "val1"}) self.assertNotIsInstance(meter, DefaultMeter) self.assertNotIsInstance(metric, DefaultMetric) - self.assertNotIsInstance(label_set, DefaultLabelSet) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 9212a9967c..1bbe18b178 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -47,9 +47,8 @@ def test_collect(self): counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) - kvp = {"key1": "value1"} - label_set = meter.get_label_set(kvp) - counter.add(1.0, label_set) + labels = {"key1": "value1"} + counter.add(1.0, labels) meter.metrics.add(counter) meter.collect() self.assertTrue(batcher_mock.process.called) @@ -69,9 +68,8 @@ def test_collect_disabled_metric(self): counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys, False ) - kvp = {"key1": "value1"} - label_set = meter.get_label_set(kvp) - counter.add(label_set, 1.0) + labels = {"key1": "value1"} + counter.add(1.0, labels) meter.metrics.add(counter) meter.collect() self.assertFalse(batcher_mock.process.called) @@ -83,7 +81,7 @@ def test_collect_observers(self): def callback(observer): self.assertIsInstance(observer, metrics_api.Observer) - observer.observe(45, meter.get_label_set(())) + observer.observe(45, {}) observer = metrics.Observer( callback, "name", "desc", "unit", int, meter, (), True @@ -96,20 +94,18 @@ def callback(observer): def test_record_batch(self): meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1",) + labels = {"key1": "value1"} counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) - kvp = {"key1": "value1"} - label_set = meter.get_label_set(kvp) record_tuples = [(counter, 1.0)] - meter.record_batch(label_set, record_tuples) - self.assertEqual(counter.bind(label_set).aggregator.current, 1.0) + meter.record_batch(labels, record_tuples) + self.assertEqual(counter.bind(labels).aggregator.current, 1.0) def test_record_batch_multiple(self): meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1", "key2", "key3") - kvp = {"key1": "value1", "key2": "value2", "key3": "value3"} - label_set = meter.get_label_set(kvp) + labels = {"key1": "value1", "key2": "value2", "key3": "value3"} counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) @@ -117,25 +113,24 @@ def test_record_batch_multiple(self): "name", "desc", "unit", float, meter, label_keys ) record_tuples = [(counter, 1.0), (measure, 3.0)] - meter.record_batch(label_set, record_tuples) - self.assertEqual(counter.bind(label_set).aggregator.current, 1.0) + meter.record_batch(labels, record_tuples) + self.assertEqual(counter.bind(labels).aggregator.current, 1.0) self.assertEqual( - measure.bind(label_set).aggregator.current, (3.0, 3.0, 3.0, 1) + measure.bind(labels).aggregator.current, (3.0, 3.0, 3.0, 1) ) def test_record_batch_exists(self): meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1",) - kvp = {"key1": "value1"} - label_set = meter.get_label_set(kvp) + labels = {"key1": "value1"} counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) - counter.add(1.0, label_set) - bound_counter = counter.bind(label_set) + counter.add(1.0, labels) + bound_counter = counter.bind(labels) record_tuples = [(counter, 1.0)] - meter.record_batch(label_set, record_tuples) - self.assertEqual(counter.bind(label_set), bound_counter) + meter.record_batch(labels, record_tuples) + self.assertEqual(counter.bind(labels), bound_counter) self.assertEqual(bound_counter.aggregator.current, 2.0) def test_create_metric(self): @@ -191,37 +186,22 @@ def test_unregister_observer(self): meter.unregister_observer(observer) self.assertEqual(len(meter.observers), 0) - def test_get_label_set(self): - meter = metrics.MeterProvider().get_meter(__name__) - kvp = {"environment": "staging", "a": "z"} - label_set = meter.get_label_set(kvp) - label_set2 = meter.get_label_set(kvp) - labels = set([label_set, label_set2]) - self.assertEqual(len(labels), 1) - - def test_get_label_set_empty(self): - meter = metrics.MeterProvider().get_meter(__name__) - kvp = {} - label_set = meter.get_label_set(kvp) - self.assertEqual(label_set, metrics.EMPTY_LABEL_SET) - def test_direct_call_release_bound_instrument(self): meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1",) - kvp = {"key1": "value1"} - label_set = meter.get_label_set(kvp) + labels = {"key1": "value1"} counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) meter.metrics.add(counter) - counter.add(4.0, label_set) + counter.add(4.0, labels) measure = metrics.Measure( "name", "desc", "unit", float, meter, label_keys ) meter.metrics.add(measure) - measure.record(42.0, label_set) + measure.record(42.0, labels) self.assertEqual(len(counter.bound_instruments), 1) self.assertEqual(len(measure.bound_instruments), 1) @@ -234,21 +214,20 @@ def test_direct_call_release_bound_instrument(self): def test_release_bound_instrument(self): meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1",) - kvp = {"key1": "value1"} - label_set = meter.get_label_set(kvp) + labels = {"key1": "value1"} counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) meter.metrics.add(counter) - bound_counter = counter.bind(label_set) + bound_counter = counter.bind(labels) bound_counter.add(4.0) measure = metrics.Measure( "name", "desc", "unit", float, meter, label_keys ) meter.metrics.add(measure) - bound_measure = measure.bind(label_set) + bound_measure = measure.bind(labels) bound_measure.record(42) bound_counter.release() @@ -268,13 +247,13 @@ class TestMetric(unittest.TestCase): def test_bind(self): meter = metrics.MeterProvider().get_meter(__name__) metric_types = [metrics.Counter, metrics.Measure] + labels = {"key": "value"} + key_labels = tuple(sorted(labels.items())) for _type in metric_types: metric = _type("name", "desc", "unit", int, meter, ("key",)) - kvp = {"key": "value"} - label_set = meter.get_label_set(kvp) - bound_instrument = metric.bind(label_set) + bound_instrument = metric.bind(labels) self.assertEqual( - metric.bound_instruments.get(label_set), bound_instrument + metric.bound_instruments.get(key_labels), bound_instrument ) @@ -282,11 +261,10 @@ class TestCounter(unittest.TestCase): def test_add(self): meter = metrics.MeterProvider().get_meter(__name__) metric = metrics.Counter("name", "desc", "unit", int, meter, ("key",)) - kvp = {"key": "value"} - label_set = meter.get_label_set(kvp) - bound_counter = metric.bind(label_set) - metric.add(3, label_set) - metric.add(2, label_set) + labels = {"key": "value"} + bound_counter = metric.bind(labels) + metric.add(3, labels) + metric.add(2, labels) self.assertEqual(bound_counter.aggregator.current, 5) @@ -294,12 +272,11 @@ class TestMeasure(unittest.TestCase): def test_record(self): meter = metrics.MeterProvider().get_meter(__name__) metric = metrics.Measure("name", "desc", "unit", int, meter, ("key",)) - kvp = {"key": "value"} - label_set = meter.get_label_set(kvp) - bound_measure = metric.bind(label_set) + labels = {"key": "value"} + bound_measure = metric.bind(labels) values = (37, 42, 7) for val in values: - metric.record(val, label_set) + metric.record(val, labels) self.assertEqual( bound_measure.aggregator.current, (min(values), max(values), sum(values), len(values)), @@ -312,26 +289,25 @@ def test_observe(self): observer = metrics.Observer( None, "name", "desc", "unit", int, meter, ("key",), True ) - kvp = {"key": "value"} - label_set = meter.get_label_set(kvp) + labels = {"key": "value"} + key_labels = tuple(sorted(labels.items())) values = (37, 42, 7, 21) for val in values: - observer.observe(val, label_set) + observer.observe(val, labels) self.assertEqual( - observer.aggregators[label_set].mmsc.current, + observer.aggregators[key_labels].mmsc.current, (min(values), max(values), sum(values), len(values)), ) - self.assertEqual(observer.aggregators[label_set].current, values[-1]) + self.assertEqual(observer.aggregators[key_labels].current, values[-1]) def test_observe_disabled(self): meter = metrics.MeterProvider().get_meter(__name__) observer = metrics.Observer( None, "name", "desc", "unit", int, meter, ("key",), False ) - kvp = {"key": "value"} - label_set = meter.get_label_set(kvp) - observer.observe(37, label_set) + labels = {"key": "value"} + observer.observe(37, labels) self.assertEqual(len(observer.aggregators), 0) @mock.patch("opentelemetry.sdk.metrics.logger") @@ -340,9 +316,8 @@ def test_observe_incorrect_type(self, logger_mock): observer = metrics.Observer( None, "name", "desc", "unit", int, meter, ("key",), True ) - kvp = {"key": "value"} - label_set = meter.get_label_set(kvp) - observer.observe(37.0, label_set) + labels = {"key": "value"} + observer.observe(37.0, labels) self.assertEqual(len(observer.aggregators), 0) self.assertTrue(logger_mock.warning.called) From 5fbf9996149208aee9ca8a1e1a585d2cf12edf11 Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Sat, 28 Mar 2020 05:45:15 +0100 Subject: [PATCH 0262/1517] lint: Add test for package readme syntax errors (#492) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a test to ensure readmes render properly Also adds README.rst for testutil package to pass new test. Co-authored-by: Christian Neumüller --- dev-requirements.txt | 1 + ext/opentelemetry-ext-otcollector/README.rst | 2 +- ext/opentelemetry-ext-testutil/README.rst | 9 ++++ scripts/check_for_valid_readme.py | 51 ++++++++++++++++++++ scripts/eachdist.py | 6 +++ tox.ini | 1 + 6 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 ext/opentelemetry-ext-testutil/README.rst create mode 100644 scripts/check_for_valid_readme.py diff --git a/dev-requirements.txt b/dev-requirements.txt index 6ce5479306..3d7a4e2656 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -8,3 +8,4 @@ sphinx-rtd-theme~=0.4 sphinx-autodoc-typehints~=1.10.2 pytest!=5.2.3 pytest-cov>=2.8 +readme-renderer~=24.0 diff --git a/ext/opentelemetry-ext-otcollector/README.rst b/ext/opentelemetry-ext-otcollector/README.rst index 1a0e3c330e..916c64ffe6 100644 --- a/ext/opentelemetry-ext-otcollector/README.rst +++ b/ext/opentelemetry-ext-otcollector/README.rst @@ -6,7 +6,7 @@ OpenTelemetry Collector Exporter .. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-otcollector.svg :target: https://pypi.org/project/opentelemetry-ext-otcollector/ -This library allows to export data to `OpenTelemetry Collector `_ , currently using OpenCensus receiver in Collector side. +This library allows to export data to `OpenTelemetry Collector`_ , currently using OpenCensus receiver in Collector side. Installation ------------ diff --git a/ext/opentelemetry-ext-testutil/README.rst b/ext/opentelemetry-ext-testutil/README.rst new file mode 100644 index 0000000000..58a75149bd --- /dev/null +++ b/ext/opentelemetry-ext-testutil/README.rst @@ -0,0 +1,9 @@ +OpenTelemetry Test Utilities +============================ + +Test utilities for OpenTelemetry unit tests + + +References +---------- +* `OpenTelemetry Project `_ diff --git a/scripts/check_for_valid_readme.py b/scripts/check_for_valid_readme.py new file mode 100644 index 0000000000..edf94d9c3e --- /dev/null +++ b/scripts/check_for_valid_readme.py @@ -0,0 +1,51 @@ +"""Test script to check given paths for valid README.rst files.""" +import argparse +import sys +from pathlib import Path + +import readme_renderer.rst + + +def is_valid_rst(path): + """Checks if RST can be rendered on PyPI.""" + with open(path) as readme_file: + markup = readme_file.read() + return readme_renderer.rst.render(markup) is not None + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Checks README.rst file in path for syntax errors." + ) + parser.add_argument( + "paths", nargs="+", help="paths containing a README.rst to test" + ) + parser.add_argument("-v", "--verbose", action="store_true") + return parser.parse_args() + + +def main(): + args = parse_args() + error = False + + for path in map(Path, args.paths): + readme = path / "README.rst" + try: + if not is_valid_rst(readme): + error = True + print("FAILED: RST syntax errors in", readme) + continue + except FileNotFoundError: + error = True + print("FAILED: README.rst not found in", path) + continue + if args.verbose: + print("PASSED:", readme) + + if error: + sys.exit(1) + print("All clear.") + + +if __name__ == "__main__": + main() diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 406afb6ebf..f1c5e18b60 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -482,6 +482,12 @@ def lint_args(args): args, ("exec", "pylint {}", "--all", "--mode", "lintroots") ) ) + execute_args( + parse_subargs( + args, + ("exec", "python scripts/check_for_valid_readme.py {}", "--all",), + ) + ) def test_args(args): diff --git a/tox.ini b/tox.ini index 2eb35491da..3c0023bd47 100644 --- a/tox.ini +++ b/tox.ini @@ -188,6 +188,7 @@ deps = isort black psutil + readme_renderer commands_pre = python scripts/eachdist.py install --editable From 7b1d866f749e7b6d716b07a5af73a4fd51008da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Sun, 29 Mar 2020 23:51:48 -0500 Subject: [PATCH 0263/1517] sdk: Add support for lazy events and links (#474) The computation of attributes is expensive in some conditions, even in some cases those attributes are not used at all. This commit fixes the add_lazy_event method by providing a true solution that delays the attributes calculation until attributes are accessed. It also provides a new LazyLink class. Events are also moved to SDK, as they are not a required object to satisfy the API specification. Co-authored-by: Leighton Chen --- .../tests/test_jaeger_exporter.py | 2 +- .../tests/test_otcollector_trace_exporter.py | 2 +- .../tests/test_zipkin_exporter.py | 2 +- .../src/opentelemetry/trace/__init__.py | 73 +++++++---- .../src/opentelemetry/util/types.py | 3 +- .../src/opentelemetry/sdk/trace/__init__.py | 120 +++++++++++++++--- opentelemetry-sdk/tests/trace/test_trace.py | 19 ++- 7 files changed, 169 insertions(+), 52 deletions(-) diff --git a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py index eac90455ad..2c792fd5d1 100644 --- a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py +++ b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py @@ -151,7 +151,7 @@ def test_translate_to_jaeger(self): } event_timestamp = base_time + 50 * 10 ** 6 - event = trace_api.Event( + event = trace.Event( name="event0", timestamp=event_timestamp, attributes=event_attributes, diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py index 636639bb1a..74c1d08872 100644 --- a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py @@ -108,7 +108,7 @@ def test_translate_to_collector(self): "key_float": 0.3, } event_timestamp = base_time + 50 * 10 ** 6 - event = trace_api.Event( + event = trace.Event( name="event0", timestamp=event_timestamp, attributes=event_attributes, diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py index 22c64b8015..b99398e066 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -134,7 +134,7 @@ def test_export(self): } event_timestamp = base_time + 50 * 10 ** 6 - event = trace_api.Event( + event = trace.Event( name="event0", timestamp=event_timestamp, attributes=event_attributes, diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index cfbc124e2e..856745e077 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -87,48 +87,59 @@ ParentSpan = typing.Optional[typing.Union["Span", "SpanContext"]] -class Link: - """A link to a `Span`.""" - - def __init__( - self, context: "SpanContext", attributes: types.Attributes = None - ) -> None: +class LinkBase(abc.ABC): + def __init__(self, context: "SpanContext") -> None: self._context = context - if attributes is None: - self._attributes = {} # type: types.Attributes - else: - self._attributes = attributes @property def context(self) -> "SpanContext": return self._context @property + @abc.abstractmethod def attributes(self) -> types.Attributes: - return self._attributes + pass -class Event: - """A text annotation with a set of attributes.""" +class Link(LinkBase): + """A link to a `Span`. + + Args: + context: `SpanContext` of the `Span` to link to. + attributes: Link's attributes. + """ def __init__( - self, name: str, attributes: types.Attributes, timestamp: int + self, context: "SpanContext", attributes: types.Attributes = None, ) -> None: - self._name = name + super().__init__(context) self._attributes = attributes - self._timestamp = timestamp - - @property - def name(self) -> str: - return self._name @property def attributes(self) -> types.Attributes: return self._attributes + +class LazyLink(LinkBase): + """A lazy link to a `Span`. + + Args: + context: `SpanContext` of the `Span` to link to. + link_formatter: Callable object that returns the attributes of the + Link. + """ + + def __init__( + self, + context: "SpanContext", + link_formatter: types.AttributesFormatter, + ) -> None: + super().__init__(context) + self._link_formatter = link_formatter + @property - def timestamp(self) -> int: - return self._timestamp + def attributes(self) -> types.Attributes: + return self._link_formatter() class SpanKind(enum.Enum): @@ -206,10 +217,17 @@ def add_event( """ @abc.abstractmethod - def add_lazy_event(self, event: Event) -> None: + def add_lazy_event( + self, + name: str, + event_formatter: types.AttributesFormatter, + timestamp: typing.Optional[int] = None, + ) -> None: """Adds an `Event`. - Adds an `Event` that has previously been created. + Adds a single `Event` with the name, an event formatter that calculates + the attributes lazily and, optionally, a timestamp. Implementations + should generate a timestamp if the `timestamp` argument is omitted. """ @abc.abstractmethod @@ -395,7 +413,12 @@ def add_event( ) -> None: pass - def add_lazy_event(self, event: Event) -> None: + def add_lazy_event( + self, + name: str, + event_formatter: types.AttributesFormatter, + timestamp: typing.Optional[int] = None, + ) -> None: pass def update_name(self, name: str) -> None: diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index 9883680553..6830a0dcd1 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -13,7 +13,7 @@ # limitations under the License. -from typing import Dict, Optional, Sequence, Union +from typing import Callable, Dict, Optional, Sequence, Union AttributeValue = Union[ str, @@ -26,3 +26,4 @@ Sequence[float], ] Attributes = Optional[Dict[str, AttributeValue]] +AttributesFormatter = Callable[[], Optional[Dict[str, AttributeValue]]] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 6211d1878f..c1389594a3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. +import abc import atexit import logging import random @@ -114,6 +115,77 @@ def shutdown(self) -> None: sp.shutdown() +class EventBase(abc.ABC): + def __init__(self, name: str, timestamp: Optional[int] = None) -> None: + self._name = name + if timestamp is None: + self._timestamp = time_ns() + else: + self._timestamp = timestamp + + @property + def name(self) -> str: + return self._name + + @property + def timestamp(self) -> int: + return self._timestamp + + @property + @abc.abstractmethod + def attributes(self) -> types.Attributes: + pass + + +class Event(EventBase): + """A text annotation with a set of attributes. + + Args: + name: Name of the event. + attributes: Attributes of the event. + timestamp: Timestamp of the event. If `None` it will filled + automatically. + """ + + def __init__( + self, + name: str, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + ) -> None: + super().__init__(name, timestamp) + self._attributes = attributes + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + +class LazyEvent(EventBase): + """A text annotation with a set of attributes. + + Args: + name: Name of the event. + event_formatter: Callable object that returns the attributes of the + event. + timestamp: Timestamp of the event. If `None` it will filled + automatically. + """ + + def __init__( + self, + name: str, + event_formatter: types.AttributesFormatter, + timestamp: Optional[int] = None, + ) -> None: + super().__init__(name, timestamp) + self._event_formatter = event_formatter + + @property + def attributes(self) -> types.Attributes: + return self._event_formatter() + + class Span(trace_api.Span): """See `opentelemetry.trace.Span`. @@ -149,7 +221,7 @@ def __init__( trace_config: None = None, # TODO resource: None = None, attributes: types.Attributes = None, # TODO - events: Sequence[trace_api.Event] = None, # TODO + events: Sequence[Event] = None, # TODO links: Sequence[trace_api.Link] = (), kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, span_processor: SpanProcessor = SpanProcessor(), @@ -266,21 +338,7 @@ def _check_attribute_value_sequence(sequence: Sequence) -> Optional[str]: return "different type" return None - def add_event( - self, - name: str, - attributes: types.Attributes = None, - timestamp: Optional[int] = None, - ) -> None: - self.add_lazy_event( - trace_api.Event( - name, - Span._empty_attributes if attributes is None else attributes, - time_ns() if timestamp is None else timestamp, - ) - ) - - def add_lazy_event(self, event: trace_api.Event) -> None: + def _add_event(self, event: EventBase) -> None: with self._lock: if not self.is_recording_events(): return @@ -293,6 +351,36 @@ def add_lazy_event(self, event: trace_api.Event) -> None: return self.events.append(event) + def add_event( + self, + name: str, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + ) -> None: + if attributes is None: + attributes = Span._empty_attributes + self._add_event( + Event( + name=name, + attributes=attributes, + timestamp=time_ns() if timestamp is None else timestamp, + ) + ) + + def add_lazy_event( + self, + name: str, + event_formatter: types.AttributesFormatter, + timestamp: Optional[int] = None, + ) -> None: + self._add_event( + LazyEvent( + name=name, + event_formatter=event_formatter, + timestamp=time_ns() if timestamp is None else timestamp, + ) + ) + def start(self, start_time: Optional[int] = None) -> None: with self._lock: if not self.is_recording_events(): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index acf7e66b44..4781c7cf80 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -547,10 +547,11 @@ def test_events(self): now = time_ns() root.add_event("event2", {"name": "birthday"}, now) + def event_formatter(): + return {"name": "hello"} + # lazy event - root.add_lazy_event( - trace_api.Event("event3", {"name": "hello"}, now) - ) + root.add_lazy_event("event3", event_formatter, now) self.assertEqual(len(root.events), 4) @@ -584,11 +585,15 @@ def test_links(self): span_id=trace.generate_span_id(), is_remote=False, ) - links = [ + + def get_link_attributes(): + return {"component": "http"} + + links = ( trace_api.Link(other_context1), trace_api.Link(other_context2, {"name": "neighbor"}), - trace_api.Link(other_context3, {"component": "http"}), - ] + trace_api.LazyLink(other_context3, get_link_attributes), + ) with self.tracer.start_as_current_span("root", links=links) as root: self.assertEqual(len(root.links), 3) @@ -598,7 +603,7 @@ def test_links(self): self.assertEqual( root.links[0].context.span_id, other_context1.span_id ) - self.assertEqual(root.links[0].attributes, {}) + self.assertEqual(root.links[0].attributes, None) self.assertEqual( root.links[1].context.trace_id, other_context2.trace_id ) From a137bc228ab4e82b0e99d88c4f075a19f73fc0d6 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 30 Mar 2020 13:44:43 -0600 Subject: [PATCH 0264/1517] Add an autoinstrumentation mechanism and an instrumentor for Flask (#327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding an autoinstrumentation mechanism and a Flask instrumentor (an instrumentor is a class that implements the _instrument and _uninstrument methods). It works like this: A console command is defined. This makes it possible to run a command named opentelemetry-auto-instrumentation that will execute this function. When the opentelemetry-auto-instrumentation command is executed, then the instrument method of the different instrumentors is called, which are made available via an entry-point. 2.In the case of the Flask instrumentor, the original flask.Flask gets replaced with _InstrumentedFlask (in this case, the Flask instrumentor uses monkey patching to perform the instrumentation, nevertheless, monkey patching is not always the method used to do this, so the name instrumentor is preferred over patcher). Once all instrumentation is enabled, the app is executed. Co-Authored-By: Mauricio Vásquez Co-authored-by: Chris Kleinknecht --- .../auto_instrumentation.rst | 7 + docs/auto_instrumentation/instrumentor.rst | 7 + docs/conf.py | 1 + docs/examples/auto-instrumentation/README.md | 112 +++++++++++ docs/examples/auto-instrumentation/client.py | 50 +++++ .../server_instrumented.py | 47 +++++ .../server_uninstrumented.py | 40 ++++ .../flask_example.py | 4 +- docs/index.rst | 2 +- ext/opentelemetry-ext-flask/setup.py | 9 +- .../src/opentelemetry/ext/flask/__init__.py | 175 ++++++++++-------- ext/opentelemetry-ext-flask/tests/conftest.py | 24 +++ .../tests/test_flask_integration.py | 4 +- .../CHANGELOG.md | 1 + .../MANIFEST.in | 7 + opentelemetry-auto-instrumentation/README.rst | 19 ++ opentelemetry-auto-instrumentation/setup.cfg | 50 +++++ opentelemetry-auto-instrumentation/setup.py | 27 +++ .../auto_instrumentation/__init__.py | 28 +++ .../auto_instrumentation.py | 36 ++++ .../auto_instrumentation/instrumentor.py | 65 +++++++ .../auto_instrumentation/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/test_instrumentor.py | 44 +++++ tox.ini | 9 + 25 files changed, 701 insertions(+), 82 deletions(-) create mode 100644 docs/auto_instrumentation/auto_instrumentation.rst create mode 100644 docs/auto_instrumentation/instrumentor.rst create mode 100644 docs/examples/auto-instrumentation/README.md create mode 100644 docs/examples/auto-instrumentation/client.py create mode 100644 docs/examples/auto-instrumentation/server_instrumented.py create mode 100644 docs/examples/auto-instrumentation/server_uninstrumented.py create mode 100644 ext/opentelemetry-ext-flask/tests/conftest.py create mode 100644 opentelemetry-auto-instrumentation/CHANGELOG.md create mode 100644 opentelemetry-auto-instrumentation/MANIFEST.in create mode 100644 opentelemetry-auto-instrumentation/README.rst create mode 100644 opentelemetry-auto-instrumentation/setup.cfg create mode 100644 opentelemetry-auto-instrumentation/setup.py create mode 100644 opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py create mode 100644 opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py create mode 100644 opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py create mode 100644 opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py create mode 100644 opentelemetry-auto-instrumentation/tests/__init__.py create mode 100644 opentelemetry-auto-instrumentation/tests/test_instrumentor.py diff --git a/docs/auto_instrumentation/auto_instrumentation.rst b/docs/auto_instrumentation/auto_instrumentation.rst new file mode 100644 index 0000000000..a45512f7f9 --- /dev/null +++ b/docs/auto_instrumentation/auto_instrumentation.rst @@ -0,0 +1,7 @@ +OpenTelemetry Python Autoinstrumentation +======================================== + +.. toctree:: + :maxdepth: 1 + + instrumentor diff --git a/docs/auto_instrumentation/instrumentor.rst b/docs/auto_instrumentation/instrumentor.rst new file mode 100644 index 0000000000..c94c0237f5 --- /dev/null +++ b/docs/auto_instrumentation/instrumentor.rst @@ -0,0 +1,7 @@ +opentelemetry.auto_instrumentation.instrumentor package +======================================================= + +.. automodule:: opentelemetry.auto_instrumentation.instrumentor + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py index a509f14f5d..acd44c0c7a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,6 +18,7 @@ source_dirs = [ os.path.abspath("../opentelemetry-api/src/"), os.path.abspath("../opentelemetry-sdk/src/"), + os.path.abspath("../opentelemetry-auto-instrumentation/src/"), ] ext = "../ext" diff --git a/docs/examples/auto-instrumentation/README.md b/docs/examples/auto-instrumentation/README.md new file mode 100644 index 0000000000..46b0b44b2c --- /dev/null +++ b/docs/examples/auto-instrumentation/README.md @@ -0,0 +1,112 @@ +# Overview + +This example shows how to use auto-instrumentation in OpenTelemetry. This example is also based on a previous example +for OpenTracing that can be found [here](https://github.com/yurishkuro/opentracing-tutorial/tree/master/python). + +This example uses 2 scripts whose main difference is they being instrumented manually or not: + +1. `server_instrumented.py` which has been instrumented manually +2. `server_uninstrumented.py` which has not been instrumented manually + +The former will be run without the automatic instrumentation agent and the latter with the automatic instrumentation +agent. They should produce the same result, showing that the automatic instrumentation agent does the equivalent +of what manual instrumentation does. + +In order to understand this better, here is the relevant part of both scripts: + +## Manually instrumented server + +`server_instrumented.py` + +```python +@app.route("/server_request") +def server_request(): + with tracer.start_as_current_span( + "server_request", + parent=propagators.extract( + lambda dict_, key: dict_.get(key, []), request.headers + )["current-span"], + ): + print(request.args.get("param")) + return "served" +``` + +## Publisher not instrumented manually + +`server_uninstrumented.py` + +```python +@app.route("/server_request") +def server_request(): + print(request.args.get("param")) + return "served" +``` + +# Preparation + +This example will be executed in a separate virtual environment: + +```sh +$ mkdir auto_instrumentation +$ virtualenv auto_instrumentation +$ source auto_instrumentation/bin/activate +``` + +# Installation + +```sh +$ pip install opentelemetry-api +$ pip install opentelemetry-sdk +$ pip install opentelemetry-auto-instrumentation +$ pip install ext/opentelemetry-ext-flask +$ pip install flask +$ pip install requests +``` + +# Execution + +## Execution of the manually instrumented server + +This is done in 2 separate consoles, one to run each of the scripts that make up this example: + +```sh +$ source auto_instrumentation/bin/activate +$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/server_instrumented.py +``` + +```sh +$ source auto_instrumentation/bin/activate +$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/client.py testing +``` + +The execution of `server_instrumented.py` should return an output similar to: + +```sh +Hello, testing! +Span(name="serv_request", context=SpanContext(trace_id=0x9c0e0ce8f7b7dbb51d1d6e744a4dad49, span_id=0xd1ba3ec4c76a0d7f, trace_state={}), kind=SpanKind.INTERNAL, parent=None, start_time=2020-03-19T00:06:31.275719Z, end_time=2020-03-19T00:06:31.275920Z) +127.0.0.1 - - [18/Mar/2020 18:06:31] "GET /serv_request?helloStr=Hello%2C+testing%21 HTTP/1.1" 200 - +``` + +## Execution of an automatically instrumented server + +Now, kill the execution of `server_instrumented.py` with `ctrl + c` and run this instead: + +```sh +$ opentelemetry-auto-instrumentation opentelemetry-python/opentelemetry-auto-instrumentation/example/server_uninstrumented.py +``` + +In the console where you previously executed `client.py`, run again this again: + +```sh +$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/client.py testing +``` + +The execution of `server_uninstrumented.py` should return an output similar to: + +```sh +Hello, testing! +Span(name="serv_request", context=SpanContext(trace_id=0xf26b28b5243e48f5f96bfc753f95f3f0, span_id=0xbeb179a095d087ed, trace_state={}), kind=SpanKind.SERVER, parent=, start_time=2020-03-19T00:24:18.828561Z, end_time=2020-03-19T00:24:18.845127Z) +127.0.0.1 - - [18/Mar/2020 18:24:18] "GET /serv_request?helloStr=Hello%2C+testing%21 HTTP/1.1" 200 - +``` + +As you can see, both outputs are equivalentsince the automatic instrumentation does what the manual instrumentation does too. diff --git a/docs/examples/auto-instrumentation/client.py b/docs/examples/auto-instrumentation/client.py new file mode 100644 index 0000000000..c8301003be --- /dev/null +++ b/docs/examples/auto-instrumentation/client.py @@ -0,0 +1,50 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sys import argv + +from flask import Flask +from requests import get + +from opentelemetry import propagators, trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +app = Flask(__name__) + +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer_provider().get_tracer(__name__) + +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) + + +assert len(argv) == 2 + +with tracer.start_as_current_span("client"): + + with tracer.start_as_current_span("client-server"): + headers = {} + propagators.inject(dict.__setitem__, headers) + requested = get( + "http://localhost:8082/server_request", + params={"param": argv[1]}, + headers=headers, + ) + + assert requested.status_code == 200 diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_instrumented.py new file mode 100644 index 0000000000..1c78aab15d --- /dev/null +++ b/docs/examples/auto-instrumentation/server_instrumented.py @@ -0,0 +1,47 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from flask import Flask, request + +from opentelemetry import propagators, trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +app = Flask(__name__) + +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer_provider().get_tracer(__name__) + +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) + + +@app.route("/server_request") +def server_request(): + with tracer.start_as_current_span( + "server_request", + parent=propagators.extract( + lambda dict_, key: dict_.get(key, []), request.headers + )["current-span"], + ): + print(request.args.get("param")) + return "served" + + +if __name__ == "__main__": + app.run(port=8082) diff --git a/docs/examples/auto-instrumentation/server_uninstrumented.py b/docs/examples/auto-instrumentation/server_uninstrumented.py new file mode 100644 index 0000000000..b8360341ab --- /dev/null +++ b/docs/examples/auto-instrumentation/server_uninstrumented.py @@ -0,0 +1,40 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from flask import Flask, request + +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +app = Flask(__name__) + +trace.set_tracer_provider(TracerProvider()) + +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) + + +@app.route("/server_request") +def server_request(): + print(request.args.get("param")) + return "served" + + +if __name__ == "__main__": + app.run(port=8082) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index a37048b7d4..21a9962d86 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -21,7 +21,7 @@ import opentelemetry.ext.http_requests from opentelemetry import trace -from opentelemetry.ext.flask import instrument_app +from opentelemetry.ext.flask import FlaskInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -33,9 +33,9 @@ SimpleExportSpanProcessor(ConsoleSpanExporter()) ) +FlaskInstrumentor().instrument() app = flask.Flask(__name__) opentelemetry.ext.http_requests.enable(trace.get_tracer_provider()) -instrument_app(app) @app.route("/") diff --git a/docs/index.rst b/docs/index.rst index fd60e72f03..2d26e24f83 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -61,7 +61,6 @@ install getting-started - .. toctree:: :maxdepth: 1 :caption: OpenTelemetry Python Packages @@ -69,6 +68,7 @@ install api/api sdk/sdk + auto_instrumentation/auto_instrumentation .. toctree:: :maxdepth: 2 diff --git a/ext/opentelemetry-ext-flask/setup.py b/ext/opentelemetry-ext-flask/setup.py index df9742c900..84b33c23b2 100644 --- a/ext/opentelemetry-ext-flask/setup.py +++ b/ext/opentelemetry-ext-flask/setup.py @@ -23,4 +23,11 @@ with open(VERSION_FILENAME) as f: exec(f.read(), PACKAGE_INFO) -setuptools.setup(version=PACKAGE_INFO["__version__"]) +setuptools.setup( + version=PACKAGE_INFO["__version__"], + entry_points={ + "opentelemetry_instrumentor": [ + "flask = opentelemetry.ext.flask:FlaskInstrumentor" + ] + }, +) diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index 11c027ecbc..02b0de8652 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -34,12 +34,12 @@ def hello(): import logging -from flask import request as flask_request +import flask import opentelemetry.ext.wsgi as otel_wsgi from opentelemetry import context, propagators, trace +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.flask.version import __version__ -from opentelemetry.trace.propagation import get_span_from_context from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -50,82 +50,105 @@ def hello(): _ENVIRON_TOKEN = "opentelemetry-flask.token" -def instrument_app(flask): - """Makes the passed-in Flask object traced by OpenTelemetry. +class _InstrumentedFlask(flask.Flask): + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + # Single use variable here to avoid recursion issues. + wsgi = self.wsgi_app + + def wrapped_app(environ, start_response): + # We want to measure the time for route matching, etc. + # In theory, we could start the span here and use + # update_name later but that API is "highly discouraged" so + # we better avoid it. + environ[_ENVIRON_STARTTIME_KEY] = time_ns() + + def _start_response(status, response_headers, *args, **kwargs): + span = flask.request.environ.get(_ENVIRON_SPAN_KEY) + if span: + otel_wsgi.add_response_attributes( + span, status, response_headers + ) + else: + logger.warning( + "Flask environ's OpenTelemetry span " + "missing at _start_response(%s)", + status, + ) + + return start_response( + status, response_headers, *args, **kwargs + ) + + return wsgi(environ, _start_response) + + self.wsgi_app = wrapped_app + + @self.before_request + def _before_flask_request(): + environ = flask.request.environ + span_name = ( + flask.request.endpoint + or otel_wsgi.get_default_span_name(environ) + ) + token = context.attach( + propagators.extract(otel_wsgi.get_header_from_environ, environ) + ) + + tracer = trace.get_tracer(__name__, __version__) + + attributes = otel_wsgi.collect_request_attributes(environ) + if flask.request.url_rule: + # For 404 that result from no route found, etc, we + # don't have a url_rule. + attributes["http.route"] = flask.request.url_rule.rule + span = tracer.start_span( + span_name, + kind=trace.SpanKind.SERVER, + attributes=attributes, + start_time=environ.get(_ENVIRON_STARTTIME_KEY), + ) + activation = tracer.use_span(span, end_on_exit=True) + activation.__enter__() + environ[_ENVIRON_ACTIVATION_KEY] = activation + environ[_ENVIRON_SPAN_KEY] = span + environ[_ENVIRON_TOKEN] = token + + @self.teardown_request + def _teardown_flask_request(exc): + activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY) + if not activation: + logger.warning( + "Flask environ's OpenTelemetry activation missing" + "at _teardown_flask_request(%s)", + exc, + ) + return + + if exc is None: + activation.__exit__(None, None, None) + else: + activation.__exit__( + type(exc), exc, getattr(exc, "__traceback__", None) + ) + context.detach(flask.request.environ.get(_ENVIRON_TOKEN)) - You must not call this function multiple times on the same Flask object. + +class FlaskInstrumentor(BaseInstrumentor): + """A instrumentor for flask.Flask + + See `BaseInstrumentor` """ - wsgi = flask.wsgi_app + def __init__(self): + super().__init__() + self._original_flask = None - def wrapped_app(environ, start_response): - # We want to measure the time for route matching, etc. - # In theory, we could start the span here and use update_name later - # but that API is "highly discouraged" so we better avoid it. - environ[_ENVIRON_STARTTIME_KEY] = time_ns() + def _instrument(self): + self._original_flask = flask.Flask + flask.Flask = _InstrumentedFlask - def _start_response(status, response_headers, *args, **kwargs): - span = flask_request.environ.get(_ENVIRON_SPAN_KEY) - if span: - otel_wsgi.add_response_attributes( - span, status, response_headers - ) - else: - logger.warning( - "Flask environ's OpenTelemetry span missing at _start_response(%s)", - status, - ) - return start_response(status, response_headers, *args, **kwargs) - - return wsgi(environ, _start_response) - - flask.wsgi_app = wrapped_app - - flask.before_request(_before_flask_request) - flask.teardown_request(_teardown_flask_request) - - -def _before_flask_request(): - environ = flask_request.environ - span_name = flask_request.endpoint or otel_wsgi.get_default_span_name( - environ - ) - token = context.attach( - propagators.extract(otel_wsgi.get_header_from_environ, environ) - ) - - tracer = trace.get_tracer(__name__, __version__) - - attributes = otel_wsgi.collect_request_attributes(environ) - if flask_request.url_rule: - # For 404 that result from no route found, etc, we don't have a url_rule. - attributes["http.route"] = flask_request.url_rule.rule - span = tracer.start_span( - span_name, - kind=trace.SpanKind.SERVER, - attributes=attributes, - start_time=environ.get(_ENVIRON_STARTTIME_KEY), - ) - activation = tracer.use_span(span, end_on_exit=True) - activation.__enter__() - environ[_ENVIRON_ACTIVATION_KEY] = activation - environ[_ENVIRON_SPAN_KEY] = span - environ[_ENVIRON_TOKEN] = token - - -def _teardown_flask_request(exc): - activation = flask_request.environ.get(_ENVIRON_ACTIVATION_KEY) - if not activation: - logger.warning( - "Flask environ's OpenTelemetry activation missing at _teardown_flask_request(%s)", - exc, - ) - return - - if exc is None: - activation.__exit__(None, None, None) - else: - activation.__exit__( - type(exc), exc, getattr(exc, "__traceback__", None) - ) - context.detach(flask_request.environ.get(_ENVIRON_TOKEN)) + def _uninstrument(self): + flask.Flask = self._original_flask diff --git a/ext/opentelemetry-ext-flask/tests/conftest.py b/ext/opentelemetry-ext-flask/tests/conftest.py new file mode 100644 index 0000000000..22a587ab2e --- /dev/null +++ b/ext/opentelemetry-ext-flask/tests/conftest.py @@ -0,0 +1,24 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from opentelemetry.ext.flask import FlaskInstrumentor + +_FLASK_INSTRUMENTOR = FlaskInstrumentor() + + +def pytest_sessionstart(session): # pylint: disable=unused-argument + _FLASK_INSTRUMENTOR.instrument() + + +def pytest_sessionfinish(session): # pylint: disable=unused-argument + _FLASK_INSTRUMENTOR.uninstrument() diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py index 9f61920ce9..b63424b905 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py @@ -18,7 +18,6 @@ from werkzeug.test import Client from werkzeug.wrappers import BaseResponse -import opentelemetry.ext.flask as otel_flask from opentelemetry import trace as trace_api from opentelemetry.ext.testutil.wsgitestutil import WsgiTestBase @@ -43,6 +42,8 @@ def expected_attributes(override_attributes): class TestFlaskIntegration(WsgiTestBase): def setUp(self): + # No instrumentation code is here because it is present in the + # conftest.py file next to this file. super().setUp() self.app = Flask(__name__) @@ -54,7 +55,6 @@ def hello_endpoint(helloid): self.app.route("/hello/")(hello_endpoint) - otel_flask.instrument_app(self.app) self.client = Client(self.app, BaseResponse) def test_only_strings_in_environ(self): diff --git a/opentelemetry-auto-instrumentation/CHANGELOG.md b/opentelemetry-auto-instrumentation/CHANGELOG.md new file mode 100644 index 0000000000..825c32f0d0 --- /dev/null +++ b/opentelemetry-auto-instrumentation/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/opentelemetry-auto-instrumentation/MANIFEST.in b/opentelemetry-auto-instrumentation/MANIFEST.in new file mode 100644 index 0000000000..191b7d1959 --- /dev/null +++ b/opentelemetry-auto-instrumentation/MANIFEST.in @@ -0,0 +1,7 @@ +prune tests +graft src +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include MANIFEST.in +include README.rst diff --git a/opentelemetry-auto-instrumentation/README.rst b/opentelemetry-auto-instrumentation/README.rst new file mode 100644 index 0000000000..b153072ae5 --- /dev/null +++ b/opentelemetry-auto-instrumentation/README.rst @@ -0,0 +1,19 @@ +OpenTelemetry Auto Instrumentation +================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-auto-instrumentation.svg + :target: https://pypi.org/project/opentelemetry-auto-instrumentation/ + +Installation +------------ + +:: + + pip install opentelemetry-auto-instrumentation + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/opentelemetry-auto-instrumentation/setup.cfg b/opentelemetry-auto-instrumentation/setup.cfg new file mode 100644 index 0000000000..182b15866f --- /dev/null +++ b/opentelemetry-auto-instrumentation/setup.cfg @@ -0,0 +1,50 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-auto-instrumentation +description = Auto Instrumentation for OpenTelemetry Python +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-auto-instrumentation" +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +zip_safe = False +include_package_data = True +install_requires = opentelemetry-api==0.6.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +console_scripts = + opentelemetry-auto-instrumentation = opentelemetry.auto_instrumentation.auto_instrumentation:run diff --git a/opentelemetry-auto-instrumentation/setup.py b/opentelemetry-auto-instrumentation/setup.py new file mode 100644 index 0000000000..86f8faedbc --- /dev/null +++ b/opentelemetry-auto-instrumentation/setup.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "auto_instrumentation", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"],) diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py new file mode 100644 index 0000000000..dfafb5386a --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py @@ -0,0 +1,28 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Usage +----- + +This package provides a command that automatically instruments a program: + +:: + + opentelemetry-auto-instrumentation program.py + +The code in ``program.py`` needs to use one of the packages for which there is +an OpenTelemetry extension. For a list of the available extensions please check +`here `_. +""" diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py new file mode 100644 index 0000000000..00ccf6a0ea --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from logging import getLogger +from runpy import run_path +from sys import argv + +from pkg_resources import iter_entry_points + +logger = getLogger(__file__) + + +def run() -> None: + + for entry_point in iter_entry_points("opentelemetry_instrumentor"): + try: + entry_point.load()().instrument() # type: ignore + logger.debug("Instrumented %s", entry_point.name) + + except Exception: # pylint: disable=broad-except + logger.exception("Instrumenting of %s failed", entry_point.name) + + run_path(argv[1], run_name="__main__") # type: ignore diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py new file mode 100644 index 0000000000..9deb6b1523 --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py @@ -0,0 +1,65 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +""" +OpenTelemetry Base Instrumentor +""" + +from abc import ABC, abstractmethod +from logging import getLogger + +_LOG = getLogger(__name__) + + +class BaseInstrumentor(ABC): + """An ABC for instrumentors""" + + def __init__(self): + self._is_instrumented = False + + @abstractmethod + def _instrument(self) -> None: + """Instrument""" + + @abstractmethod + def _uninstrument(self) -> None: + """Uninstrument""" + + def instrument(self) -> None: + """Instrument""" + + if not self._is_instrumented: + result = self._instrument() + self._is_instrumented = True + return result + + _LOG.warning("Attempting to instrument while already instrumented") + + return None + + def uninstrument(self) -> None: + """Uninstrument""" + + if self._is_instrumented: + result = self._uninstrument() + self._is_instrumented = False + return result + + _LOG.warning("Attempting to uninstrument while already uninstrumented") + + return None + + +__all__ = ["BaseInstrumentor"] diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py new file mode 100644 index 0000000000..0941210ca3 --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.6.dev0" diff --git a/opentelemetry-auto-instrumentation/tests/__init__.py b/opentelemetry-auto-instrumentation/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-auto-instrumentation/tests/test_instrumentor.py b/opentelemetry-auto-instrumentation/tests/test_instrumentor.py new file mode 100644 index 0000000000..1324213536 --- /dev/null +++ b/opentelemetry-auto-instrumentation/tests/test_instrumentor.py @@ -0,0 +1,44 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from logging import WARNING +from unittest import TestCase + +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor + + +class TestInstrumentor(TestCase): + def test_protect(self): + class Instrumentor(BaseInstrumentor): + def _instrument(self): + return "instrumented" + + def _uninstrument(self): + return "uninstrumented" + + instrumentor = Instrumentor() + + with self.assertLogs(level=WARNING): + self.assertIs(instrumentor.uninstrument(), None) + + self.assertEqual(instrumentor.instrument(), "instrumented") + + with self.assertLogs(level=WARNING): + self.assertIs(instrumentor.instrument(), None) + + self.assertEqual(instrumentor.uninstrument(), "uninstrumented") + + with self.assertLogs(level=WARNING): + self.assertIs(instrumentor.uninstrument(), None) diff --git a/tox.ini b/tox.ini index 3c0023bd47..6e64bdbd44 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,10 @@ envlist = py3{4,5,6,7,8}-test-sdk pypy3-test-sdk + ; opentelemetry-auto-instrumentation + py3{4,5,6,7,8}-test-auto-instrumentation + pypy3-test-auto-instrumentation + ; opentelemetry-example-app py3{4,5,6,7,8}-test-example-app pypy3-test-example-app @@ -43,6 +47,7 @@ envlist = ; opentelemetry-ext-mysql py3{4,5,6,7,8}-test-ext-mysql pypy3-test-ext-mysql + ; opentelemetry-ext-otcollector py3{4,5,6,7,8}-test-ext-otcollector ; ext-otcollector intentionally excluded from pypy3 @@ -101,6 +106,7 @@ setenv = changedir = test-api: opentelemetry-api/tests test-sdk: opentelemetry-sdk/tests + test-auto-instrumentation: opentelemetry-auto-instrumentation/tests test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests @@ -122,7 +128,9 @@ commands_pre = python -m pip install -U pip setuptools wheel test: pip install {toxinidir}/opentelemetry-api test-sdk: pip install {toxinidir}/opentelemetry-sdk + test-auto-instrumentation: pip install {toxinidir}/opentelemetry-auto-instrumentation example-app: pip install {toxinidir}/opentelemetry-sdk + example-app: pip install {toxinidir}/opentelemetry-auto-instrumentation example-app: pip install {toxinidir}/ext/opentelemetry-ext-http-requests example-app: pip install {toxinidir}/ext/opentelemetry-ext-wsgi example-app: pip install {toxinidir}/ext/opentelemetry-ext-flask @@ -139,6 +147,7 @@ commands_pre = wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-testutil wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-wsgi wsgi,flask: pip install {toxinidir}/opentelemetry-sdk + flask: pip install {toxinidir}/opentelemetry-auto-instrumentation flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi From 28ee03ff831b26f00af882295475a25582e080c9 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 30 Mar 2020 14:43:58 -0700 Subject: [PATCH 0265/1517] docs: standard usage format across ext packages (#534) The rendered documentation is a little out of sync in a couple areas. Resolving those. - Adding an API subsection footer to those that don't have it - Removing installation instructions in the readme - Referencing automodule vs a direct include of the readme --- docs/ext/zipkin/zipkin.rst | 7 ++----- .../src/opentelemetry/ext/opentracing_shim/__init__.py | 2 ++ .../src/opentelemetry/ext/wsgi/__init__.py | 3 +++ .../src/opentelemetry/ext/zipkin/__init__.py | 8 -------- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/docs/ext/zipkin/zipkin.rst b/docs/ext/zipkin/zipkin.rst index 8826178908..8a5191a86d 100644 --- a/docs/ext/zipkin/zipkin.rst +++ b/docs/ext/zipkin/zipkin.rst @@ -1,8 +1,5 @@ -.. include:: ../../../ext/opentelemetry-ext-zipkin/README.rst - - -Module contents ---------------- +Opentelemetry Zipkin Exporter +============================= .. automodule:: opentelemetry.ext.zipkin :members: diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index a547bca22e..9bb6fc8d6b 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -76,6 +76,8 @@ While testing this library, the aforementioned imprecisions were observed to be of *less than a microsecond*. +API +--- .. _Floating Point Arithmetic\\: Issues and Limitations: https://docs.python.org/3/tutorial/floatingpoint.html """ diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 8ac9ec4a83..61e8126c19 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -50,6 +50,9 @@ def hello(): application = get_wsgi_application() application = OpenTelemetryMiddleware(application) + +API +--- """ import functools diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index fdd9819144..e319e12149 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -15,14 +15,6 @@ """ This library allows to export tracing data to `Zipkin `_. -Installation ------------- - -:: - - pip install opentelemetry-ext-zipkin - - Usage ----- From 9320cfed022aba4158cfc92a500675b44904448c Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 30 Mar 2020 16:00:19 -0700 Subject: [PATCH 0266/1517] Misc isort fixes (#535) --- .isort.cfg | 1 + opentelemetry-api/tests/configuration/test_configuration.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.isort.cfg b/.isort.cfg index 4e7bff8bb4..014a7c64ea 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -15,3 +15,4 @@ multi_line_output=3 skip=target skip_glob=ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/*,.venv*/*,venv*/* known_first_party=opentelemetry +known_third_party=psutil,pytest diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index a9387017f9..c69c09b52b 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -16,9 +16,10 @@ from unittest import TestCase from unittest.mock import patch -from opentelemetry.configuration import Configuration # type: ignore from pytest import fixture # type: ignore # pylint: disable=import-error +from opentelemetry.configuration import Configuration # type: ignore + class TestConfiguration(TestCase): class IterEntryPointsMock: From 54b390f7c177a7e6ca4b30bbd5e55f7b73f46343 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 30 Mar 2020 18:04:24 -0700 Subject: [PATCH 0267/1517] gRPC integration (#476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a port of grpcio-opentracing, and borrows from opencensus-ext-grpc. It adds client and server interceptors that wrap each request in a span and use the new context API to inject/extract the traceparent header to/from gRPC metadata. Co-authored-by: alrex Co-authored-by: Mauricio Vásquez Co-authored-by: Mario Jonke Co-authored-by: Daniel González Co-authored-by: Alex Boten Co-authored-by: Yusuke Tsutsumi --- .flake8 | 1 + .isort.cfg | 4 +- .../opentelemetry-example-app/setup.py | 1 + .../grpc/__init__.py | 0 .../grpc/gen/__init__.py | 19 + .../grpc/gen/codegen.py | 32 + .../grpc/gen/helloworld.proto | 35 + .../grpc/gen/helloworld_pb2.py | 131 ++++ .../grpc/gen/helloworld_pb2_grpc.py | 46 ++ .../grpc/gen/route_guide.proto | 108 ++++ .../grpc/gen/route_guide_pb2.py | 328 ++++++++++ .../grpc/gen/route_guide_pb2_grpc.py | 113 ++++ .../grpc/hello_world_client.py | 88 +++ .../grpc/hello_world_server.py | 88 +++ .../grpc/route_guide_client.py | 180 ++++++ .../grpc/route_guide_db.json | 601 ++++++++++++++++++ .../grpc/route_guide_resources.py | 46 ++ .../grpc/route_guide_server.py | 179 ++++++ docs/ext/grpc/grpc.client_interceptor.rst | 7 + docs/ext/grpc/grpc.rst | 17 + docs/ext/grpc/grpc.server_interceptor.rst | 7 + ext/opentelemetry-ext-grpc/CHANGELOG.md | 0 ext/opentelemetry-ext-grpc/README.rst | 18 + ext/opentelemetry-ext-grpc/setup.cfg | 46 ++ ext/opentelemetry-ext-grpc/setup.py | 27 + .../src/opentelemetry/ext/grpc/__init__.py | 46 ++ .../src/opentelemetry/ext/grpc/_client.py | 176 +++++ .../src/opentelemetry/ext/grpc/_server.py | 209 ++++++ .../src/opentelemetry/ext/grpc/_utilities.py | 33 + .../ext/grpc/grpcext/__init__.py | 216 +++++++ .../ext/grpc/grpcext/_interceptor.py | 423 ++++++++++++ .../src/opentelemetry/ext/grpc/version.py | 15 + ext/opentelemetry-ext-grpc/tests/__init__.py | 13 + .../tests/test_server_interceptor.py | 243 +++++++ .../src/opentelemetry/context/__init__.py | 45 +- pyproject.toml | 5 +- tox.ini | 9 +- 37 files changed, 3530 insertions(+), 25 deletions(-) create mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/__init__.py create mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/__init__.py create mode 100755 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/codegen.py create mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld.proto create mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2.py create mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2_grpc.py create mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide.proto create mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2.py create mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2_grpc.py create mode 100755 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py create mode 100755 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py create mode 100755 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py create mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_db.json create mode 100644 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_resources.py create mode 100755 docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py create mode 100644 docs/ext/grpc/grpc.client_interceptor.rst create mode 100644 docs/ext/grpc/grpc.rst create mode 100644 docs/ext/grpc/grpc.server_interceptor.rst create mode 100644 ext/opentelemetry-ext-grpc/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-grpc/README.rst create mode 100644 ext/opentelemetry-ext-grpc/setup.cfg create mode 100644 ext/opentelemetry-ext-grpc/setup.py create mode 100644 ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py create mode 100644 ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py create mode 100644 ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_server.py create mode 100644 ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py create mode 100644 ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/__init__.py create mode 100644 ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/_interceptor.py create mode 100644 ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py create mode 100644 ext/opentelemetry-ext-grpc/tests/__init__.py create mode 100644 ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py diff --git a/.flake8 b/.flake8 index 6d1c2bc175..0c2204782f 100644 --- a/.flake8 +++ b/.flake8 @@ -17,3 +17,4 @@ exclude = __pycache__ ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/ ext/opentelemetry-ext-jaeger/build/* + docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ diff --git a/.isort.cfg b/.isort.cfg index 014a7c64ea..c5723b06a2 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -13,6 +13,6 @@ line_length=79 ; docs: https://github.com/timothycrosley/isort#multi-line-output-modes multi_line_output=3 skip=target -skip_glob=ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/*,.venv*/*,venv*/* -known_first_party=opentelemetry +skip_glob=**/gen/*,.venv*/*,venv*/* +known_first_party=opentelemetry,opentelemetry_example_app known_third_party=psutil,pytest diff --git a/docs/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py index c90da05047..1466fd9463 100644 --- a/docs/examples/opentelemetry-example-app/setup.py +++ b/docs/examples/opentelemetry-example-app/setup.py @@ -41,6 +41,7 @@ "opentelemetry-ext-flask", "flask", "requests", + "protobuf~=3.11", ], license="Apache-2.0", package_dir={"": "src"}, diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/__init__.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/__init__.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/__init__.py new file mode 100644 index 0000000000..bcedda2270 --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/__init__.py @@ -0,0 +1,19 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib +import sys + +# gRPC-generated modules expect other generated modules to be on the path. +sys.path.extend(importlib.util.find_spec(__name__).submodule_search_locations) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/codegen.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/codegen.py new file mode 100755 index 0000000000..72c24bad39 --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/codegen.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from grpc_tools import protoc + + +def main(): + return protoc.main( + [ + "-I.", + "--python_out=.", + "--grpc_python_out=.", + "helloworld.proto", + "route_guide.proto", + ] + ) + + +if __name__ == "__main__": + main() diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld.proto b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld.proto new file mode 100644 index 0000000000..446102166d --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld.proto @@ -0,0 +1,35 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// https://github.com/grpc/grpc/blob/master/examples/protos/helloworld.proto + +syntax = "proto3"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2.py new file mode 100644 index 0000000000..0e37874ebc --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: helloworld.proto + +from google.protobuf import descriptor as _descriptor +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.FileDescriptor( + name='helloworld.proto', + package='helloworld', + syntax='proto3', + serialized_options=None, + serialized_pb=b'\n\x10helloworld.proto\x12\nhelloworld\"\x1c\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x1d\n\nHelloReply\x12\x0f\n\x07message\x18\x01 \x01(\t2I\n\x07Greeter\x12>\n\x08SayHello\x12\x18.helloworld.HelloRequest\x1a\x16.helloworld.HelloReply\"\x00\x62\x06proto3' +) + + + + +_HELLOREQUEST = _descriptor.Descriptor( + name='HelloRequest', + full_name='helloworld.HelloRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='helloworld.HelloRequest.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=32, + serialized_end=60, +) + + +_HELLOREPLY = _descriptor.Descriptor( + name='HelloReply', + full_name='helloworld.HelloReply', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='message', full_name='helloworld.HelloReply.message', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=62, + serialized_end=91, +) + +DESCRIPTOR.message_types_by_name['HelloRequest'] = _HELLOREQUEST +DESCRIPTOR.message_types_by_name['HelloReply'] = _HELLOREPLY +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +HelloRequest = _reflection.GeneratedProtocolMessageType('HelloRequest', (_message.Message,), { + 'DESCRIPTOR' : _HELLOREQUEST, + '__module__' : 'helloworld_pb2' + # @@protoc_insertion_point(class_scope:helloworld.HelloRequest) + }) +_sym_db.RegisterMessage(HelloRequest) + +HelloReply = _reflection.GeneratedProtocolMessageType('HelloReply', (_message.Message,), { + 'DESCRIPTOR' : _HELLOREPLY, + '__module__' : 'helloworld_pb2' + # @@protoc_insertion_point(class_scope:helloworld.HelloReply) + }) +_sym_db.RegisterMessage(HelloReply) + + + +_GREETER = _descriptor.ServiceDescriptor( + name='Greeter', + full_name='helloworld.Greeter', + file=DESCRIPTOR, + index=0, + serialized_options=None, + serialized_start=93, + serialized_end=166, + methods=[ + _descriptor.MethodDescriptor( + name='SayHello', + full_name='helloworld.Greeter.SayHello', + index=0, + containing_service=None, + input_type=_HELLOREQUEST, + output_type=_HELLOREPLY, + serialized_options=None, + ), +]) +_sym_db.RegisterServiceDescriptor(_GREETER) + +DESCRIPTOR.services_by_name['Greeter'] = _GREETER + +# @@protoc_insertion_point(module_scope) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2_grpc.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2_grpc.py new file mode 100644 index 0000000000..18e07d1679 --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/helloworld_pb2_grpc.py @@ -0,0 +1,46 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +import grpc + +import helloworld_pb2 as helloworld__pb2 + + +class GreeterStub(object): + """The greeting service definition. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.SayHello = channel.unary_unary( + '/helloworld.Greeter/SayHello', + request_serializer=helloworld__pb2.HelloRequest.SerializeToString, + response_deserializer=helloworld__pb2.HelloReply.FromString, + ) + + +class GreeterServicer(object): + """The greeting service definition. + """ + + def SayHello(self, request, context): + """Sends a greeting + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_GreeterServicer_to_server(servicer, server): + rpc_method_handlers = { + 'SayHello': grpc.unary_unary_rpc_method_handler( + servicer.SayHello, + request_deserializer=helloworld__pb2.HelloRequest.FromString, + response_serializer=helloworld__pb2.HelloReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'helloworld.Greeter', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide.proto b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide.proto new file mode 100644 index 0000000000..7017e93498 --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide.proto @@ -0,0 +1,108 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// https://github.com/grpc/grpc/blob/master/examples/protos/route_guide.proto + +syntax = "proto3"; + +package routeguide; + +// Interface exported by the server. +service RouteGuide { + // A simple RPC. + // + // Obtains the feature at a given position. + // + // A feature with an empty name is returned if there's no feature at the given + // position. + rpc GetFeature(Point) returns (Feature) {} + + // A server-to-client streaming RPC. + // + // Obtains the Features available within the given Rectangle. Results are + // streamed rather than returned at once (e.g. in a response message with a + // repeated field), as the rectangle may cover a large area and contain a + // huge number of features. + rpc ListFeatures(Rectangle) returns (stream Feature) {} + + // A client-to-server streaming RPC. + // + // Accepts a stream of Points on a route being traversed, returning a + // RouteSummary when traversal is completed. + rpc RecordRoute(stream Point) returns (RouteSummary) {} + + // A Bidirectional streaming RPC. + // + // Accepts a stream of RouteNotes sent while a route is being traversed, + // while receiving other RouteNotes (e.g. from other users). + rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} +} + +// Points are represented as latitude-longitude pairs in the E7 representation +// (degrees multiplied by 10**7 and rounded to the nearest integer). +// Latitudes should be in the range +/- 90 degrees and longitude should be in +// the range +/- 180 degrees (inclusive). +message Point { + int32 latitude = 1; + int32 longitude = 2; +} + +// A latitude-longitude rectangle, represented as two diagonally opposite +// points "lo" and "hi". +message Rectangle { + // One corner of the rectangle. + Point lo = 1; + + // The other corner of the rectangle. + Point hi = 2; +} + +// A feature names something at a given point. +// +// If a feature could not be named, the name is empty. +message Feature { + // The name of the feature. + string name = 1; + + // The point where the feature is detected. + Point location = 2; +} + +// A RouteNote is a message sent while at a given point. +message RouteNote { + // The location from which the message is sent. + Point location = 1; + + // The message to be sent. + string message = 2; +} + +// A RouteSummary is received in response to a RecordRoute rpc. +// +// It contains the number of individual points received, the number of +// detected features, and the total distance covered as the cumulative sum of +// the distance between each point. +message RouteSummary { + // The number of points received. + int32 point_count = 1; + + // The number of known features passed while traversing the route. + int32 feature_count = 2; + + // The distance covered in metres. + int32 distance = 3; + + // The duration of the traversal in seconds. + int32 elapsed_time = 4; +} diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2.py new file mode 100644 index 0000000000..4a4006a2c7 --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2.py @@ -0,0 +1,328 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: route_guide.proto + +from google.protobuf import descriptor as _descriptor +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.FileDescriptor( + name='route_guide.proto', + package='routeguide', + syntax='proto3', + serialized_options=None, + serialized_pb=b'\n\x11route_guide.proto\x12\nrouteguide\",\n\x05Point\x12\x10\n\x08latitude\x18\x01 \x01(\x05\x12\x11\n\tlongitude\x18\x02 \x01(\x05\"I\n\tRectangle\x12\x1d\n\x02lo\x18\x01 \x01(\x0b\x32\x11.routeguide.Point\x12\x1d\n\x02hi\x18\x02 \x01(\x0b\x32\x11.routeguide.Point\"<\n\x07\x46\x65\x61ture\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x08location\x18\x02 \x01(\x0b\x32\x11.routeguide.Point\"A\n\tRouteNote\x12#\n\x08location\x18\x01 \x01(\x0b\x32\x11.routeguide.Point\x12\x0f\n\x07message\x18\x02 \x01(\t\"b\n\x0cRouteSummary\x12\x13\n\x0bpoint_count\x18\x01 \x01(\x05\x12\x15\n\rfeature_count\x18\x02 \x01(\x05\x12\x10\n\x08\x64istance\x18\x03 \x01(\x05\x12\x14\n\x0c\x65lapsed_time\x18\x04 \x01(\x05\x32\x85\x02\n\nRouteGuide\x12\x36\n\nGetFeature\x12\x11.routeguide.Point\x1a\x13.routeguide.Feature\"\x00\x12>\n\x0cListFeatures\x12\x15.routeguide.Rectangle\x1a\x13.routeguide.Feature\"\x00\x30\x01\x12>\n\x0bRecordRoute\x12\x11.routeguide.Point\x1a\x18.routeguide.RouteSummary\"\x00(\x01\x12?\n\tRouteChat\x12\x15.routeguide.RouteNote\x1a\x15.routeguide.RouteNote\"\x00(\x01\x30\x01\x62\x06proto3' +) + + + + +_POINT = _descriptor.Descriptor( + name='Point', + full_name='routeguide.Point', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='latitude', full_name='routeguide.Point.latitude', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='longitude', full_name='routeguide.Point.longitude', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=33, + serialized_end=77, +) + + +_RECTANGLE = _descriptor.Descriptor( + name='Rectangle', + full_name='routeguide.Rectangle', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='lo', full_name='routeguide.Rectangle.lo', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='hi', full_name='routeguide.Rectangle.hi', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=79, + serialized_end=152, +) + + +_FEATURE = _descriptor.Descriptor( + name='Feature', + full_name='routeguide.Feature', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='routeguide.Feature.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='location', full_name='routeguide.Feature.location', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=154, + serialized_end=214, +) + + +_ROUTENOTE = _descriptor.Descriptor( + name='RouteNote', + full_name='routeguide.RouteNote', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='location', full_name='routeguide.RouteNote.location', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='message', full_name='routeguide.RouteNote.message', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=216, + serialized_end=281, +) + + +_ROUTESUMMARY = _descriptor.Descriptor( + name='RouteSummary', + full_name='routeguide.RouteSummary', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='point_count', full_name='routeguide.RouteSummary.point_count', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='feature_count', full_name='routeguide.RouteSummary.feature_count', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='distance', full_name='routeguide.RouteSummary.distance', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='elapsed_time', full_name='routeguide.RouteSummary.elapsed_time', index=3, + number=4, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=283, + serialized_end=381, +) + +_RECTANGLE.fields_by_name['lo'].message_type = _POINT +_RECTANGLE.fields_by_name['hi'].message_type = _POINT +_FEATURE.fields_by_name['location'].message_type = _POINT +_ROUTENOTE.fields_by_name['location'].message_type = _POINT +DESCRIPTOR.message_types_by_name['Point'] = _POINT +DESCRIPTOR.message_types_by_name['Rectangle'] = _RECTANGLE +DESCRIPTOR.message_types_by_name['Feature'] = _FEATURE +DESCRIPTOR.message_types_by_name['RouteNote'] = _ROUTENOTE +DESCRIPTOR.message_types_by_name['RouteSummary'] = _ROUTESUMMARY +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Point = _reflection.GeneratedProtocolMessageType('Point', (_message.Message,), { + 'DESCRIPTOR' : _POINT, + '__module__' : 'route_guide_pb2' + # @@protoc_insertion_point(class_scope:routeguide.Point) + }) +_sym_db.RegisterMessage(Point) + +Rectangle = _reflection.GeneratedProtocolMessageType('Rectangle', (_message.Message,), { + 'DESCRIPTOR' : _RECTANGLE, + '__module__' : 'route_guide_pb2' + # @@protoc_insertion_point(class_scope:routeguide.Rectangle) + }) +_sym_db.RegisterMessage(Rectangle) + +Feature = _reflection.GeneratedProtocolMessageType('Feature', (_message.Message,), { + 'DESCRIPTOR' : _FEATURE, + '__module__' : 'route_guide_pb2' + # @@protoc_insertion_point(class_scope:routeguide.Feature) + }) +_sym_db.RegisterMessage(Feature) + +RouteNote = _reflection.GeneratedProtocolMessageType('RouteNote', (_message.Message,), { + 'DESCRIPTOR' : _ROUTENOTE, + '__module__' : 'route_guide_pb2' + # @@protoc_insertion_point(class_scope:routeguide.RouteNote) + }) +_sym_db.RegisterMessage(RouteNote) + +RouteSummary = _reflection.GeneratedProtocolMessageType('RouteSummary', (_message.Message,), { + 'DESCRIPTOR' : _ROUTESUMMARY, + '__module__' : 'route_guide_pb2' + # @@protoc_insertion_point(class_scope:routeguide.RouteSummary) + }) +_sym_db.RegisterMessage(RouteSummary) + + + +_ROUTEGUIDE = _descriptor.ServiceDescriptor( + name='RouteGuide', + full_name='routeguide.RouteGuide', + file=DESCRIPTOR, + index=0, + serialized_options=None, + serialized_start=384, + serialized_end=645, + methods=[ + _descriptor.MethodDescriptor( + name='GetFeature', + full_name='routeguide.RouteGuide.GetFeature', + index=0, + containing_service=None, + input_type=_POINT, + output_type=_FEATURE, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name='ListFeatures', + full_name='routeguide.RouteGuide.ListFeatures', + index=1, + containing_service=None, + input_type=_RECTANGLE, + output_type=_FEATURE, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name='RecordRoute', + full_name='routeguide.RouteGuide.RecordRoute', + index=2, + containing_service=None, + input_type=_POINT, + output_type=_ROUTESUMMARY, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name='RouteChat', + full_name='routeguide.RouteGuide.RouteChat', + index=3, + containing_service=None, + input_type=_ROUTENOTE, + output_type=_ROUTENOTE, + serialized_options=None, + ), +]) +_sym_db.RegisterServiceDescriptor(_ROUTEGUIDE) + +DESCRIPTOR.services_by_name['RouteGuide'] = _ROUTEGUIDE + +# @@protoc_insertion_point(module_scope) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2_grpc.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2_grpc.py new file mode 100644 index 0000000000..05c1b79312 --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/route_guide_pb2_grpc.py @@ -0,0 +1,113 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +import grpc + +import route_guide_pb2 as route__guide__pb2 + + +class RouteGuideStub(object): + """Interface exported by the server. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetFeature = channel.unary_unary( + '/routeguide.RouteGuide/GetFeature', + request_serializer=route__guide__pb2.Point.SerializeToString, + response_deserializer=route__guide__pb2.Feature.FromString, + ) + self.ListFeatures = channel.unary_stream( + '/routeguide.RouteGuide/ListFeatures', + request_serializer=route__guide__pb2.Rectangle.SerializeToString, + response_deserializer=route__guide__pb2.Feature.FromString, + ) + self.RecordRoute = channel.stream_unary( + '/routeguide.RouteGuide/RecordRoute', + request_serializer=route__guide__pb2.Point.SerializeToString, + response_deserializer=route__guide__pb2.RouteSummary.FromString, + ) + self.RouteChat = channel.stream_stream( + '/routeguide.RouteGuide/RouteChat', + request_serializer=route__guide__pb2.RouteNote.SerializeToString, + response_deserializer=route__guide__pb2.RouteNote.FromString, + ) + + +class RouteGuideServicer(object): + """Interface exported by the server. + """ + + def GetFeature(self, request, context): + """A simple RPC. + + Obtains the feature at a given position. + + A feature with an empty name is returned if there's no feature at the given + position. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListFeatures(self, request, context): + """A server-to-client streaming RPC. + + Obtains the Features available within the given Rectangle. Results are + streamed rather than returned at once (e.g. in a response message with a + repeated field), as the rectangle may cover a large area and contain a + huge number of features. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def RecordRoute(self, request_iterator, context): + """A client-to-server streaming RPC. + + Accepts a stream of Points on a route being traversed, returning a + RouteSummary when traversal is completed. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def RouteChat(self, request_iterator, context): + """A Bidirectional streaming RPC. + + Accepts a stream of RouteNotes sent while a route is being traversed, + while receiving other RouteNotes (e.g. from other users). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_RouteGuideServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetFeature': grpc.unary_unary_rpc_method_handler( + servicer.GetFeature, + request_deserializer=route__guide__pb2.Point.FromString, + response_serializer=route__guide__pb2.Feature.SerializeToString, + ), + 'ListFeatures': grpc.unary_stream_rpc_method_handler( + servicer.ListFeatures, + request_deserializer=route__guide__pb2.Rectangle.FromString, + response_serializer=route__guide__pb2.Feature.SerializeToString, + ), + 'RecordRoute': grpc.stream_unary_rpc_method_handler( + servicer.RecordRoute, + request_deserializer=route__guide__pb2.Point.FromString, + response_serializer=route__guide__pb2.RouteSummary.SerializeToString, + ), + 'RouteChat': grpc.stream_stream_rpc_method_handler( + servicer.RouteChat, + request_deserializer=route__guide__pb2.RouteNote.FromString, + response_serializer=route__guide__pb2.RouteNote.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'routeguide.RouteGuide', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py new file mode 100755 index 0000000000..2f2351b9af --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=import-error + +"""The Python implementation of the GRPC helloworld.Greeter client. + +Note that you need ``opentelemetry-ext-grpc`` and ``protobuf`` to be installed +to run these examples. To run this script in the context of the example app, +install ``opentelemetry-example-app``:: + + pip install -e ext/opentelemetry-ext-grpc/ + pip install -e docs/examples/opentelemetry-example-app + +Then run the server in one shell:: + + python -m opentelemetry_example_app.grpc.hello_world_server + +and the client in another:: + + python -m opentelemetry_example_app.grpc.hello_world_client + +See also: +https://github.com/grpc/grpc/blob/master/examples/python/helloworld/greeter_client.py +https://github.com/grpc/grpc/blob/v1.16.x/examples/python/interceptors/default_value/greeter_client.py +""" + +import logging + +import grpc + +from opentelemetry import trace +from opentelemetry.ext.grpc import client_interceptor +from opentelemetry.ext.grpc.grpcext import intercept_channel +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +try: + # Relative imports should work in the context of the package, e.g.: + # `python -m opentelemetry_example_app.grpc.hello_world_client`. + from .gen import helloworld_pb2, helloworld_pb2_grpc +except ImportError: + # This will fail when running the file as a script, e.g.: + # `./hello_world_client.py` + # fall back to importing from the same directory in this case. + from gen import helloworld_pb2, helloworld_pb2_grpc + +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) +tracer = trace.get_tracer(__name__) + + +def run(): + # NOTE(gRPC Python Team): .close() is possible on a channel and should be + # used in circumstances in which the with statement does not fit the needs + # of the code. + with grpc.insecure_channel("localhost:50051") as channel: + + channel = intercept_channel(channel, client_interceptor(tracer)) + + stub = helloworld_pb2_grpc.GreeterStub(channel) + + # stub.SayHello is a _InterceptorUnaryUnaryMultiCallable + response = stub.SayHello(helloworld_pb2.HelloRequest(name="YOU")) + + print("Greeter client received: " + response.message) + + +if __name__ == "__main__": + logging.basicConfig() + run() diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py new file mode 100755 index 0000000000..86dcd66527 --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=import-error + +"""The Python implementation of the GRPC helloworld.Greeter server. + +Note that you need ``opentelemetry-ext-grpc`` and ``protobuf`` to be installed +to run these examples. To run this script in the context of the example app, +install ``opentelemetry-example-app``:: + + pip install -e ext/opentelemetry-ext-grpc/ + pip install -e docs/examples/opentelemetry-example-app + +Then run the server in one shell:: + + python -m opentelemetry_example_app.grpc.hello_world_client + +and the client in another:: + + python -m opentelemetry_example_app.grpc.hello_world_server + +See also: +https://github.com/grpc/grpc/blob/master/examples/python/helloworld/greeter_server.py +""" + +import logging +from concurrent import futures + +import grpc + +from opentelemetry import trace +from opentelemetry.ext.grpc import server_interceptor +from opentelemetry.ext.grpc.grpcext import intercept_server +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +try: + # Relative imports should work in the context of the package, e.g.: + # `python -m opentelemetry_example_app.grpc.hello_world_server`. + from .gen import helloworld_pb2, helloworld_pb2_grpc +except ImportError: + # This will fail when running the file as a script, e.g.: + # `./hello_world_server.py` + # fall back to importing from the same directory in this case. + from gen import helloworld_pb2, helloworld_pb2_grpc + +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) +tracer = trace.get_tracer(__name__) + + +class Greeter(helloworld_pb2_grpc.GreeterServicer): + def SayHello(self, request, context): + return helloworld_pb2.HelloReply(message="Hello, %s!" % request.name) + + +def serve(): + + server = grpc.server(futures.ThreadPoolExecutor()) + server = intercept_server(server, server_interceptor(tracer)) + + helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server) + server.add_insecure_port("[::]:50051") + server.start() + server.wait_for_termination() + + +if __name__ == "__main__": + logging.basicConfig() + serve() diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py new file mode 100755 index 0000000000..18391b4228 --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=import-error + +"""The Python implementation of the gRPC route guide client. + +Note that you need ``opentelemetry-ext-grpc`` and ``protobuf`` to be installed +to run these examples. To run this script in the context of the example app, +install ``opentelemetry-example-app``:: + + pip install -e ext/opentelemetry-ext-grpc/ + pip install -e docs/examples/opentelemetry-example-app + +Then run the server in one shell:: + + python -m opentelemetry_example_app.grpc.route_guide_server + +and the client in another:: + + python -m opentelemetry_example_app.grpc.route_guide_client + +See also: +https://github.com/grpc/grpc/tree/master/examples/python/route_guide +""" + + +import logging +import random + +import grpc + +from opentelemetry import trace +from opentelemetry.ext.grpc import client_interceptor +from opentelemetry.ext.grpc.grpcext import intercept_channel +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +try: + # Relative imports should work in the context of the package, e.g.: + # `python -m opentelemetry_example_app.grpc.route_guide_client`. + from .gen import route_guide_pb2, route_guide_pb2_grpc + from . import route_guide_resources +except ImportError: + # This will fail when running the file as a script, e.g.: + # `./route_guide_client.py` + # fall back to importing from the same directory in this case. + from gen import route_guide_pb2, route_guide_pb2_grpc + import route_guide_resources + +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) +tracer = trace.get_tracer(__name__) + + +def make_route_note(message, latitude, longitude): + return route_guide_pb2.RouteNote( + message=message, + location=route_guide_pb2.Point(latitude=latitude, longitude=longitude), + ) + + +def guide_get_one_feature(stub, point): + feature = stub.GetFeature(point) + if not feature.location: + print("Server returned incomplete feature") + return + + if feature.name: + print("Feature called %s at %s" % (feature.name, feature.location)) + else: + print("Found no feature at %s" % feature.location) + + +def guide_get_feature(stub): + guide_get_one_feature( + stub, route_guide_pb2.Point(latitude=409146138, longitude=-746188906) + ) + guide_get_one_feature(stub, route_guide_pb2.Point(latitude=0, longitude=0)) + + +def guide_list_features(stub): + rectangle = route_guide_pb2.Rectangle( + lo=route_guide_pb2.Point(latitude=400000000, longitude=-750000000), + hi=route_guide_pb2.Point(latitude=420000000, longitude=-730000000), + ) + print("Looking for features between 40, -75 and 42, -73") + + features = stub.ListFeatures(rectangle) + + for feature in features: + print("Feature called %s at %s" % (feature.name, feature.location)) + + +def generate_route(feature_list): + for _ in range(0, 10): + random_feature = feature_list[random.randint(0, len(feature_list) - 1)] + print("Visiting point %s" % random_feature.location) + yield random_feature.location + + +def guide_record_route(stub): + feature_list = route_guide_resources.read_route_guide_database() + + route_iterator = generate_route(feature_list) + route_summary = stub.RecordRoute(route_iterator) + print("Finished trip with %s points " % route_summary.point_count) + print("Passed %s features " % route_summary.feature_count) + print("Travelled %s meters " % route_summary.distance) + print("It took %s seconds " % route_summary.elapsed_time) + + +def generate_messages(): + messages = [ + make_route_note("First message", 0, 0), + make_route_note("Second message", 0, 1), + make_route_note("Third message", 1, 0), + make_route_note("Fourth message", 0, 0), + make_route_note("Fifth message", 1, 0), + ] + for msg in messages: + print("Sending %s at %s" % (msg.message, msg.location)) + yield msg + + +def guide_route_chat(stub): + responses = stub.RouteChat(generate_messages()) + for response in responses: + print( + "Received message %s at %s" % (response.message, response.location) + ) + + +def run(): + + # NOTE(gRPC Python Team): .close() is possible on a channel and should be + # used in circumstances in which the with statement does not fit the needs + # of the code. + with grpc.insecure_channel("localhost:50051") as channel: + channel = intercept_channel(channel, client_interceptor(tracer)) + + stub = route_guide_pb2_grpc.RouteGuideStub(channel) + + # Unary + print("-------------- GetFeature --------------") + guide_get_feature(stub) + + # Server streaming + print("-------------- ListFeatures --------------") + guide_list_features(stub) + + # Client streaming + print("-------------- RecordRoute --------------") + guide_record_route(stub) + + # Bidirectional streaming + print("-------------- RouteChat --------------") + guide_route_chat(stub) + + +if __name__ == "__main__": + logging.basicConfig() + run() diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_db.json b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_db.json new file mode 100644 index 0000000000..9d6a980ab7 --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_db.json @@ -0,0 +1,601 @@ +[{ + "location": { + "latitude": 407838351, + "longitude": -746143763 + }, + "name": "Patriots Path, Mendham, NJ 07945, USA" +}, { + "location": { + "latitude": 408122808, + "longitude": -743999179 + }, + "name": "101 New Jersey 10, Whippany, NJ 07981, USA" +}, { + "location": { + "latitude": 413628156, + "longitude": -749015468 + }, + "name": "U.S. 6, Shohola, PA 18458, USA" +}, { + "location": { + "latitude": 419999544, + "longitude": -740371136 + }, + "name": "5 Conners Road, Kingston, NY 12401, USA" +}, { + "location": { + "latitude": 414008389, + "longitude": -743951297 + }, + "name": "Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA" +}, { + "location": { + "latitude": 419611318, + "longitude": -746524769 + }, + "name": "287 Flugertown Road, Livingston Manor, NY 12758, USA" +}, { + "location": { + "latitude": 406109563, + "longitude": -742186778 + }, + "name": "4001 Tremley Point Road, Linden, NJ 07036, USA" +}, { + "location": { + "latitude": 416802456, + "longitude": -742370183 + }, + "name": "352 South Mountain Road, Wallkill, NY 12589, USA" +}, { + "location": { + "latitude": 412950425, + "longitude": -741077389 + }, + "name": "Bailey Turn Road, Harriman, NY 10926, USA" +}, { + "location": { + "latitude": 412144655, + "longitude": -743949739 + }, + "name": "193-199 Wawayanda Road, Hewitt, NJ 07421, USA" +}, { + "location": { + "latitude": 415736605, + "longitude": -742847522 + }, + "name": "406-496 Ward Avenue, Pine Bush, NY 12566, USA" +}, { + "location": { + "latitude": 413843930, + "longitude": -740501726 + }, + "name": "162 Merrill Road, Highland Mills, NY 10930, USA" +}, { + "location": { + "latitude": 410873075, + "longitude": -744459023 + }, + "name": "Clinton Road, West Milford, NJ 07480, USA" +}, { + "location": { + "latitude": 412346009, + "longitude": -744026814 + }, + "name": "16 Old Brook Lane, Warwick, NY 10990, USA" +}, { + "location": { + "latitude": 402948455, + "longitude": -747903913 + }, + "name": "3 Drake Lane, Pennington, NJ 08534, USA" +}, { + "location": { + "latitude": 406337092, + "longitude": -740122226 + }, + "name": "6324 8th Avenue, Brooklyn, NY 11220, USA" +}, { + "location": { + "latitude": 406421967, + "longitude": -747727624 + }, + "name": "1 Merck Access Road, Whitehouse Station, NJ 08889, USA" +}, { + "location": { + "latitude": 416318082, + "longitude": -749677716 + }, + "name": "78-98 Schalck Road, Narrowsburg, NY 12764, USA" +}, { + "location": { + "latitude": 415301720, + "longitude": -748416257 + }, + "name": "282 Lakeview Drive Road, Highland Lake, NY 12743, USA" +}, { + "location": { + "latitude": 402647019, + "longitude": -747071791 + }, + "name": "330 Evelyn Avenue, Hamilton Township, NJ 08619, USA" +}, { + "location": { + "latitude": 412567807, + "longitude": -741058078 + }, + "name": "New York State Reference Route 987E, Southfields, NY 10975, USA" +}, { + "location": { + "latitude": 416855156, + "longitude": -744420597 + }, + "name": "103-271 Tempaloni Road, Ellenville, NY 12428, USA" +}, { + "location": { + "latitude": 404663628, + "longitude": -744820157 + }, + "name": "1300 Airport Road, North Brunswick Township, NJ 08902, USA" +}, { + "location": { + "latitude": 407113723, + "longitude": -749746483 + }, + "name": "" +}, { + "location": { + "latitude": 402133926, + "longitude": -743613249 + }, + "name": "" +}, { + "location": { + "latitude": 400273442, + "longitude": -741220915 + }, + "name": "" +}, { + "location": { + "latitude": 411236786, + "longitude": -744070769 + }, + "name": "" +}, { + "location": { + "latitude": 411633782, + "longitude": -746784970 + }, + "name": "211-225 Plains Road, Augusta, NJ 07822, USA" +}, { + "location": { + "latitude": 415830701, + "longitude": -742952812 + }, + "name": "" +}, { + "location": { + "latitude": 413447164, + "longitude": -748712898 + }, + "name": "165 Pedersen Ridge Road, Milford, PA 18337, USA" +}, { + "location": { + "latitude": 405047245, + "longitude": -749800722 + }, + "name": "100-122 Locktown Road, Frenchtown, NJ 08825, USA" +}, { + "location": { + "latitude": 418858923, + "longitude": -746156790 + }, + "name": "" +}, { + "location": { + "latitude": 417951888, + "longitude": -748484944 + }, + "name": "650-652 Willi Hill Road, Swan Lake, NY 12783, USA" +}, { + "location": { + "latitude": 407033786, + "longitude": -743977337 + }, + "name": "26 East 3rd Street, New Providence, NJ 07974, USA" +}, { + "location": { + "latitude": 417548014, + "longitude": -740075041 + }, + "name": "" +}, { + "location": { + "latitude": 410395868, + "longitude": -744972325 + }, + "name": "" +}, { + "location": { + "latitude": 404615353, + "longitude": -745129803 + }, + "name": "" +}, { + "location": { + "latitude": 406589790, + "longitude": -743560121 + }, + "name": "611 Lawrence Avenue, Westfield, NJ 07090, USA" +}, { + "location": { + "latitude": 414653148, + "longitude": -740477477 + }, + "name": "18 Lannis Avenue, New Windsor, NY 12553, USA" +}, { + "location": { + "latitude": 405957808, + "longitude": -743255336 + }, + "name": "82-104 Amherst Avenue, Colonia, NJ 07067, USA" +}, { + "location": { + "latitude": 411733589, + "longitude": -741648093 + }, + "name": "170 Seven Lakes Drive, Sloatsburg, NY 10974, USA" +}, { + "location": { + "latitude": 412676291, + "longitude": -742606606 + }, + "name": "1270 Lakes Road, Monroe, NY 10950, USA" +}, { + "location": { + "latitude": 409224445, + "longitude": -748286738 + }, + "name": "509-535 Alphano Road, Great Meadows, NJ 07838, USA" +}, { + "location": { + "latitude": 406523420, + "longitude": -742135517 + }, + "name": "652 Garden Street, Elizabeth, NJ 07202, USA" +}, { + "location": { + "latitude": 401827388, + "longitude": -740294537 + }, + "name": "349 Sea Spray Court, Neptune City, NJ 07753, USA" +}, { + "location": { + "latitude": 410564152, + "longitude": -743685054 + }, + "name": "13-17 Stanley Street, West Milford, NJ 07480, USA" +}, { + "location": { + "latitude": 408472324, + "longitude": -740726046 + }, + "name": "47 Industrial Avenue, Teterboro, NJ 07608, USA" +}, { + "location": { + "latitude": 412452168, + "longitude": -740214052 + }, + "name": "5 White Oak Lane, Stony Point, NY 10980, USA" +}, { + "location": { + "latitude": 409146138, + "longitude": -746188906 + }, + "name": "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" +}, { + "location": { + "latitude": 404701380, + "longitude": -744781745 + }, + "name": "1007 Jersey Avenue, New Brunswick, NJ 08901, USA" +}, { + "location": { + "latitude": 409642566, + "longitude": -746017679 + }, + "name": "6 East Emerald Isle Drive, Lake Hopatcong, NJ 07849, USA" +}, { + "location": { + "latitude": 408031728, + "longitude": -748645385 + }, + "name": "1358-1474 New Jersey 57, Port Murray, NJ 07865, USA" +}, { + "location": { + "latitude": 413700272, + "longitude": -742135189 + }, + "name": "367 Prospect Road, Chester, NY 10918, USA" +}, { + "location": { + "latitude": 404310607, + "longitude": -740282632 + }, + "name": "10 Simon Lake Drive, Atlantic Highlands, NJ 07716, USA" +}, { + "location": { + "latitude": 409319800, + "longitude": -746201391 + }, + "name": "11 Ward Street, Mount Arlington, NJ 07856, USA" +}, { + "location": { + "latitude": 406685311, + "longitude": -742108603 + }, + "name": "300-398 Jefferson Avenue, Elizabeth, NJ 07201, USA" +}, { + "location": { + "latitude": 419018117, + "longitude": -749142781 + }, + "name": "43 Dreher Road, Roscoe, NY 12776, USA" +}, { + "location": { + "latitude": 412856162, + "longitude": -745148837 + }, + "name": "Swan Street, Pine Island, NY 10969, USA" +}, { + "location": { + "latitude": 416560744, + "longitude": -746721964 + }, + "name": "66 Pleasantview Avenue, Monticello, NY 12701, USA" +}, { + "location": { + "latitude": 405314270, + "longitude": -749836354 + }, + "name": "" +}, { + "location": { + "latitude": 414219548, + "longitude": -743327440 + }, + "name": "" +}, { + "location": { + "latitude": 415534177, + "longitude": -742900616 + }, + "name": "565 Winding Hills Road, Montgomery, NY 12549, USA" +}, { + "location": { + "latitude": 406898530, + "longitude": -749127080 + }, + "name": "231 Rocky Run Road, Glen Gardner, NJ 08826, USA" +}, { + "location": { + "latitude": 407586880, + "longitude": -741670168 + }, + "name": "100 Mount Pleasant Avenue, Newark, NJ 07104, USA" +}, { + "location": { + "latitude": 400106455, + "longitude": -742870190 + }, + "name": "517-521 Huntington Drive, Manchester Township, NJ 08759, USA" +}, { + "location": { + "latitude": 400066188, + "longitude": -746793294 + }, + "name": "" +}, { + "location": { + "latitude": 418803880, + "longitude": -744102673 + }, + "name": "40 Mountain Road, Napanoch, NY 12458, USA" +}, { + "location": { + "latitude": 414204288, + "longitude": -747895140 + }, + "name": "" +}, { + "location": { + "latitude": 414777405, + "longitude": -740615601 + }, + "name": "" +}, { + "location": { + "latitude": 415464475, + "longitude": -747175374 + }, + "name": "48 North Road, Forestburgh, NY 12777, USA" +}, { + "location": { + "latitude": 404062378, + "longitude": -746376177 + }, + "name": "" +}, { + "location": { + "latitude": 405688272, + "longitude": -749285130 + }, + "name": "" +}, { + "location": { + "latitude": 400342070, + "longitude": -748788996 + }, + "name": "" +}, { + "location": { + "latitude": 401809022, + "longitude": -744157964 + }, + "name": "" +}, { + "location": { + "latitude": 404226644, + "longitude": -740517141 + }, + "name": "9 Thompson Avenue, Leonardo, NJ 07737, USA" +}, { + "location": { + "latitude": 410322033, + "longitude": -747871659 + }, + "name": "" +}, { + "location": { + "latitude": 407100674, + "longitude": -747742727 + }, + "name": "" +}, { + "location": { + "latitude": 418811433, + "longitude": -741718005 + }, + "name": "213 Bush Road, Stone Ridge, NY 12484, USA" +}, { + "location": { + "latitude": 415034302, + "longitude": -743850945 + }, + "name": "" +}, { + "location": { + "latitude": 411349992, + "longitude": -743694161 + }, + "name": "" +}, { + "location": { + "latitude": 404839914, + "longitude": -744759616 + }, + "name": "1-17 Bergen Court, New Brunswick, NJ 08901, USA" +}, { + "location": { + "latitude": 414638017, + "longitude": -745957854 + }, + "name": "35 Oakland Valley Road, Cuddebackville, NY 12729, USA" +}, { + "location": { + "latitude": 412127800, + "longitude": -740173578 + }, + "name": "" +}, { + "location": { + "latitude": 401263460, + "longitude": -747964303 + }, + "name": "" +}, { + "location": { + "latitude": 412843391, + "longitude": -749086026 + }, + "name": "" +}, { + "location": { + "latitude": 418512773, + "longitude": -743067823 + }, + "name": "" +}, { + "location": { + "latitude": 404318328, + "longitude": -740835638 + }, + "name": "42-102 Main Street, Belford, NJ 07718, USA" +}, { + "location": { + "latitude": 419020746, + "longitude": -741172328 + }, + "name": "" +}, { + "location": { + "latitude": 404080723, + "longitude": -746119569 + }, + "name": "" +}, { + "location": { + "latitude": 401012643, + "longitude": -744035134 + }, + "name": "" +}, { + "location": { + "latitude": 404306372, + "longitude": -741079661 + }, + "name": "" +}, { + "location": { + "latitude": 403966326, + "longitude": -748519297 + }, + "name": "" +}, { + "location": { + "latitude": 405002031, + "longitude": -748407866 + }, + "name": "" +}, { + "location": { + "latitude": 409532885, + "longitude": -742200683 + }, + "name": "" +}, { + "location": { + "latitude": 416851321, + "longitude": -742674555 + }, + "name": "" +}, { + "location": { + "latitude": 406411633, + "longitude": -741722051 + }, + "name": "3387 Richmond Terrace, Staten Island, NY 10303, USA" +}, { + "location": { + "latitude": 413069058, + "longitude": -744597778 + }, + "name": "261 Van Sickle Road, Goshen, NY 10924, USA" +}, { + "location": { + "latitude": 418465462, + "longitude": -746859398 + }, + "name": "" +}, { + "location": { + "latitude": 411733222, + "longitude": -744228360 + }, + "name": "" +}, { + "location": { + "latitude": 410248224, + "longitude": -747127767 + }, + "name": "3 Hasta Way, Newton, NJ 07860, USA" +}] diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_resources.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_resources.py new file mode 100644 index 0000000000..c7977698ba --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_resources.py @@ -0,0 +1,46 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# https://github.com/grpc/grpc/tree/master/examples/python/route_guide + +# pylint: disable=import-error + +"""Common resources used in the gRPC route guide example.""" + +import json +import os + +import route_guide_pb2 + + +def read_route_guide_database(): + """Reads the route guide database. + + Returns: + The full contents of the route guide database as a sequence of + route_guide_pb2.Features. + """ + feature_list = [] + db_file = os.path.join(os.path.dirname(__file__), "route_guide_db.json") + with open(db_file) as route_guide_db_file: + for item in json.load(route_guide_db_file): + feature = route_guide_pb2.Feature( + name=item["name"], + location=route_guide_pb2.Point( + latitude=item["location"]["latitude"], + longitude=item["location"]["longitude"], + ), + ) + feature_list.append(feature) + return feature_list diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py new file mode 100755 index 0000000000..9cd9db666e --- /dev/null +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=import-error +# pylint: disable=invalid-name + +"""The Python implementation of the gRPC route guide server. + +Note that you need ``opentelemetry-ext-grpc`` and ``protobuf`` to be installed +to run these examples. To run this script in the context of the example app, +install ``opentelemetry-example-app``:: + + pip install -e ext/opentelemetry-ext-grpc/ + pip install -e docs/examples/opentelemetry-example-app + +Then run the server in one shell:: + + python -m opentelemetry_example_app.grpc.route_guide_server + +and the client in another:: + + python -m opentelemetry_example_app.grpc.route_guide_client + +See also: +https://github.com/grpc/grpc/tree/master/examples/python/route_guide +""" + +import logging +import math +import time +from concurrent import futures + +import grpc + +from opentelemetry import trace +from opentelemetry.ext.grpc import server_interceptor +from opentelemetry.ext.grpc.grpcext import intercept_server +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +try: + # Relative imports should work in the context of the package, e.g.: + # `python -m opentelemetry_example_app.grpc.route_guide_server`. + from .gen import route_guide_pb2, route_guide_pb2_grpc + from . import route_guide_resources +except ImportError: + # This will fail when running the file as a script, e.g.: + # `./route_guide_server.py` + # fall back to importing from the same directory in this case. + from gen import route_guide_pb2, route_guide_pb2_grpc + import route_guide_resources + +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) +tracer = trace.get_tracer(__name__) + + +def get_feature(feature_db, point): + """Returns Feature at given location or None.""" + for feature in feature_db: + if feature.location == point: + return feature + return None + + +def get_distance(start, end): + """Distance between two points.""" + coord_factor = 10000000.0 + lat_1 = start.latitude / coord_factor + lat_2 = end.latitude / coord_factor + lon_1 = start.longitude / coord_factor + lon_2 = end.longitude / coord_factor + lat_rad_1 = math.radians(lat_1) + lat_rad_2 = math.radians(lat_2) + delta_lat_rad = math.radians(lat_2 - lat_1) + delta_lon_rad = math.radians(lon_2 - lon_1) + + # Formula is based on http://mathforum.org/library/drmath/view/51879.html + a = pow(math.sin(delta_lat_rad / 2), 2) + ( + math.cos(lat_rad_1) + * math.cos(lat_rad_2) + * pow(math.sin(delta_lon_rad / 2), 2) + ) + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + R = 6371000 + # metres + return R * c + + +class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer): + """Provides methods that implement functionality of route guide server.""" + + def __init__(self): + self.db = route_guide_resources.read_route_guide_database() + + def GetFeature(self, request, context): + feature = get_feature(self.db, request) + if feature is None: + return route_guide_pb2.Feature(name="", location=request) + return feature + + def ListFeatures(self, request, context): + left = min(request.lo.longitude, request.hi.longitude) + right = max(request.lo.longitude, request.hi.longitude) + top = max(request.lo.latitude, request.hi.latitude) + bottom = min(request.lo.latitude, request.hi.latitude) + for feature in self.db: + if ( + feature.location.longitude >= left + and feature.location.longitude <= right + and feature.location.latitude >= bottom + and feature.location.latitude <= top + ): + yield feature + + def RecordRoute(self, request_iterator, context): + point_count = 0 + feature_count = 0 + distance = 0.0 + prev_point = None + + start_time = time.time() + for point in request_iterator: + point_count += 1 + if get_feature(self.db, point): + feature_count += 1 + if prev_point: + distance += get_distance(prev_point, point) + prev_point = point + + elapsed_time = time.time() - start_time + return route_guide_pb2.RouteSummary( + point_count=point_count, + feature_count=feature_count, + distance=int(distance), + elapsed_time=int(elapsed_time), + ) + + def RouteChat(self, request_iterator, context): + prev_notes = [] + for new_note in request_iterator: + for prev_note in prev_notes: + if prev_note.location == new_note.location: + yield prev_note + prev_notes.append(new_note) + + +def serve(): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + server = intercept_server(server, server_interceptor(tracer)) + + route_guide_pb2_grpc.add_RouteGuideServicer_to_server( + RouteGuideServicer(), server + ) + server.add_insecure_port("[::]:50051") + server.start() + server.wait_for_termination() + + +if __name__ == "__main__": + logging.basicConfig() + serve() diff --git a/docs/ext/grpc/grpc.client_interceptor.rst b/docs/ext/grpc/grpc.client_interceptor.rst new file mode 100644 index 0000000000..46de43810a --- /dev/null +++ b/docs/ext/grpc/grpc.client_interceptor.rst @@ -0,0 +1,7 @@ +grpc.client\_interceptor module +=============================== + +.. automodule:: opentelemetry.ext.grpc.client_interceptor + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ext/grpc/grpc.rst b/docs/ext/grpc/grpc.rst new file mode 100644 index 0000000000..7e7d5f5426 --- /dev/null +++ b/docs/ext/grpc/grpc.rst @@ -0,0 +1,17 @@ +.. include:: ../../../ext/opentelemetry-ext-grpc/README.rst + +Submodules +---------- + +.. toctree:: + + grpc.client_interceptor + grpc.server_interceptor + +Module contents +--------------- + +.. automodule:: opentelemetry.ext.grpc + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ext/grpc/grpc.server_interceptor.rst b/docs/ext/grpc/grpc.server_interceptor.rst new file mode 100644 index 0000000000..b51ae1a7cb --- /dev/null +++ b/docs/ext/grpc/grpc.server_interceptor.rst @@ -0,0 +1,7 @@ +grpc.server\_interceptor module +=============================== + +.. automodule:: opentelemetry.ext.grpc.server_interceptor + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-grpc/CHANGELOG.md b/ext/opentelemetry-ext-grpc/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-grpc/README.rst b/ext/opentelemetry-ext-grpc/README.rst new file mode 100644 index 0000000000..335c03614b --- /dev/null +++ b/ext/opentelemetry-ext-grpc/README.rst @@ -0,0 +1,18 @@ +OpenTelemetry gRPC Integration +============================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-grpc.svg + :target: https://pypi.org/project/opentelemetry-ext-grpc/ + +Client and server interceptors for `gRPC Python`_. + +.. _gRPC Python: https://grpc.github.io/grpc/python/grpc.html + +Installation +------------ + +:: + + pip install opentelemetry-ext-grpc diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg new file mode 100644 index 0000000000..df7074078b --- /dev/null +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -0,0 +1,46 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[metadata] +name = opentelemetry-ext-grpc +description = OpenTelemetry gRPC Integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-grpc +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api + grpcio~=1.27 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-grpc/setup.py b/ext/opentelemetry-ext-grpc/setup.py new file mode 100644 index 0000000000..9a0a4b5d1e --- /dev/null +++ b/ext/opentelemetry-ext-grpc/setup.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "grpc", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py new file mode 100644 index 0000000000..8807abcb1f --- /dev/null +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py @@ -0,0 +1,46 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint:disable=import-outside-toplevel +# pylint:disable=import-self +# pylint:disable=no-name-in-module +# pylint:disable=relative-beyond-top-level + + +def client_interceptor(tracer): + """Create a gRPC client channel interceptor. + + Args: + tracer: The tracer to use to create client-side spans. + + Returns: + An invocation-side interceptor object. + """ + from . import _client + + return _client.OpenTelemetryClientInterceptor(tracer) + + +def server_interceptor(tracer): + """Create a gRPC server interceptor. + + Args: + tracer: The tracer to use to create server-side spans. + + Returns: + A service-side interceptor object. + """ + from . import _server + + return _server.OpenTelemetryServerInterceptor(tracer) diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py new file mode 100644 index 0000000000..ebf455910c --- /dev/null +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_client.py @@ -0,0 +1,176 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint:disable=relative-beyond-top-level +# pylint:disable=arguments-differ +# pylint:disable=no-member +# pylint:disable=signature-differs + +"""Implementation of the invocation-side open-telemetry interceptor.""" + +from collections import OrderedDict +from typing import MutableMapping + +import grpc + +from opentelemetry import propagators, trace + +from . import grpcext +from ._utilities import RpcInfo + + +class _GuardedSpan: + def __init__(self, span): + self.span = span + self._engaged = True + + def __enter__(self): + self.span.__enter__() + return self + + def __exit__(self, *args, **kwargs): + if self._engaged: + return self.span.__exit__(*args, **kwargs) + return False + + def release(self): + self._engaged = False + return self.span + + +def _inject_span_context(metadata: MutableMapping[str, str]) -> None: + # pylint:disable=unused-argument + def append_metadata( + carrier: MutableMapping[str, str], key: str, value: str + ): + metadata[key] = value + + # Inject current active span from the context + propagators.inject(append_metadata, metadata) + + +def _make_future_done_callback(span, rpc_info): + def callback(response_future): + with span: + code = response_future.code() + if code != grpc.StatusCode.OK: + rpc_info.error = code + return + response = response_future.result() + rpc_info.response = response + + return callback + + +class OpenTelemetryClientInterceptor( + grpcext.UnaryClientInterceptor, grpcext.StreamClientInterceptor +): + def __init__(self, tracer): + self._tracer = tracer + + def _start_span(self, method): + return self._tracer.start_as_current_span( + name=method, kind=trace.SpanKind.CLIENT + ) + + # pylint:disable=no-self-use + def _trace_result(self, guarded_span, rpc_info, result): + # If the RPC is called asynchronously, release the guard and add a + # callback so that the span can be finished once the future is done. + if isinstance(result, grpc.Future): + result.add_done_callback( + _make_future_done_callback(guarded_span.release(), rpc_info) + ) + return result + response = result + # Handle the case when the RPC is initiated via the with_call + # method and the result is a tuple with the first element as the + # response. + # http://www.grpc.io/grpc/python/grpc.html#grpc.UnaryUnaryMultiCallable.with_call + if isinstance(result, tuple): + response = result[0] + rpc_info.response = response + return result + + def _start_guarded_span(self, *args, **kwargs): + return _GuardedSpan(self._start_span(*args, **kwargs)) + + def intercept_unary(self, request, metadata, client_info, invoker): + if not metadata: + mutable_metadata = OrderedDict() + else: + mutable_metadata = OrderedDict(metadata) + + with self._start_guarded_span(client_info.full_method) as guarded_span: + _inject_span_context(mutable_metadata) + metadata = tuple(mutable_metadata.items()) + + rpc_info = RpcInfo( + full_method=client_info.full_method, + metadata=metadata, + timeout=client_info.timeout, + request=request, + ) + result = invoker(request, metadata) + return self._trace_result(guarded_span, rpc_info, result) + + # For RPCs that stream responses, the result can be a generator. To record + # the span across the generated responses and detect any errors, we wrap + # the result in a new generator that yields the response values. + def _intercept_server_stream( + self, request_or_iterator, metadata, client_info, invoker + ): + if not metadata: + mutable_metadata = OrderedDict() + else: + mutable_metadata = OrderedDict(metadata) + + with self._start_span(client_info.full_method): + _inject_span_context(mutable_metadata) + metadata = tuple(mutable_metadata.items()) + rpc_info = RpcInfo( + full_method=client_info.full_method, + metadata=metadata, + timeout=client_info.timeout, + ) + if client_info.is_client_stream: + rpc_info.request = request_or_iterator + result = invoker(request_or_iterator, metadata) + for response in result: + yield response + + def intercept_stream( + self, request_or_iterator, metadata, client_info, invoker + ): + if client_info.is_server_stream: + return self._intercept_server_stream( + request_or_iterator, metadata, client_info, invoker + ) + + if not metadata: + mutable_metadata = OrderedDict() + else: + mutable_metadata = OrderedDict(metadata) + + with self._start_guarded_span(client_info.full_method) as guarded_span: + _inject_span_context(mutable_metadata) + metadata = tuple(mutable_metadata.items()) + rpc_info = RpcInfo( + full_method=client_info.full_method, + metadata=metadata, + timeout=client_info.timeout, + request=request_or_iterator, + ) + result = invoker(request_or_iterator, metadata) + return self._trace_result(guarded_span, rpc_info, result) diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_server.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_server.py new file mode 100644 index 0000000000..cb0e997d36 --- /dev/null +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_server.py @@ -0,0 +1,209 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint:disable=relative-beyond-top-level +# pylint:disable=arguments-differ +# pylint:disable=no-member +# pylint:disable=signature-differs + +"""Implementation of the service-side open-telemetry interceptor. + +This library borrows heavily from the OpenTracing gRPC integration: +https://github.com/opentracing-contrib/python-grpc +""" + +from contextlib import contextmanager +from typing import List + +import grpc + +from opentelemetry import propagators, trace +from opentelemetry.context import attach, detach + +from . import grpcext +from ._utilities import RpcInfo + + +# pylint:disable=abstract-method +class _OpenTelemetryServicerContext(grpc.ServicerContext): + def __init__(self, servicer_context, active_span): + self._servicer_context = servicer_context + self._active_span = active_span + self.code = grpc.StatusCode.OK + self.details = None + super(_OpenTelemetryServicerContext, self).__init__() + + def is_active(self, *args, **kwargs): + return self._servicer_context.is_active(*args, **kwargs) + + def time_remaining(self, *args, **kwargs): + return self._servicer_context.time_remaining(*args, **kwargs) + + def cancel(self, *args, **kwargs): + return self._servicer_context.cancel(*args, **kwargs) + + def add_callback(self, *args, **kwargs): + return self._servicer_context.add_callback(*args, **kwargs) + + def invocation_metadata(self, *args, **kwargs): + return self._servicer_context.invocation_metadata(*args, **kwargs) + + def peer(self, *args, **kwargs): + return self._servicer_context.peer(*args, **kwargs) + + def peer_identities(self, *args, **kwargs): + return self._servicer_context.peer_identities(*args, **kwargs) + + def peer_identity_key(self, *args, **kwargs): + return self._servicer_context.peer_identity_key(*args, **kwargs) + + def auth_context(self, *args, **kwargs): + return self._servicer_context.auth_context(*args, **kwargs) + + def send_initial_metadata(self, *args, **kwargs): + return self._servicer_context.send_initial_metadata(*args, **kwargs) + + def set_trailing_metadata(self, *args, **kwargs): + return self._servicer_context.set_trailing_metadata(*args, **kwargs) + + def abort(self, *args, **kwargs): + if not hasattr(self._servicer_context, "abort"): + raise RuntimeError( + "abort() is not supported with the installed version of grpcio" + ) + return self._servicer_context.abort(*args, **kwargs) + + def abort_with_status(self, *args, **kwargs): + if not hasattr(self._servicer_context, "abort_with_status"): + raise RuntimeError( + "abort_with_status() is not supported with the installed " + "version of grpcio" + ) + return self._servicer_context.abort_with_status(*args, **kwargs) + + def set_code(self, code): + self.code = code + return self._servicer_context.set_code(code) + + def set_details(self, details): + self.details = details + return self._servicer_context.set_details(details) + + +# On the service-side, errors can be signaled either by exceptions or by +# calling `set_code` on the `servicer_context`. This function checks for the +# latter and updates the span accordingly. +# pylint:disable=unused-argument +def _check_error_code(span, servicer_context, rpc_info): + if servicer_context.code != grpc.StatusCode.OK: + rpc_info.error = servicer_context.code + + +class OpenTelemetryServerInterceptor( + grpcext.UnaryServerInterceptor, grpcext.StreamServerInterceptor +): + def __init__(self, tracer): + self._tracer = tracer + + @contextmanager + # pylint:disable=no-self-use + def _set_remote_context(self, servicer_context): + metadata = servicer_context.invocation_metadata() + if metadata: + md_dict = {md.key: md.value for md in metadata} + + def get_from_grpc_metadata(metadata, key) -> List[str]: + return [md_dict[key]] if key in md_dict else [] + + # Update the context with the traceparent from the RPC metadata. + ctx = propagators.extract(get_from_grpc_metadata, metadata) + token = attach(ctx) + try: + yield + finally: + detach(token) + else: + yield + + def _start_span(self, method): + span = self._tracer.start_as_current_span( + name=method, kind=trace.SpanKind.SERVER + ) + return span + + def intercept_unary(self, request, servicer_context, server_info, handler): + + with self._set_remote_context(servicer_context): + with self._start_span(server_info.full_method) as span: + rpc_info = RpcInfo( + full_method=server_info.full_method, + metadata=servicer_context.invocation_metadata(), + timeout=servicer_context.time_remaining(), + request=request, + ) + servicer_context = _OpenTelemetryServicerContext( + servicer_context, span + ) + response = handler(request, servicer_context) + + _check_error_code(span, servicer_context, rpc_info) + + rpc_info.response = response + + return response + + # For RPCs that stream responses, the result can be a generator. To record + # the span across the generated responses and detect any errors, we wrap + # the result in a new generator that yields the response values. + def _intercept_server_stream( + self, request_or_iterator, servicer_context, server_info, handler + ): + with self._set_remote_context(servicer_context): + with self._start_span(server_info.full_method) as span: + rpc_info = RpcInfo( + full_method=server_info.full_method, + metadata=servicer_context.invocation_metadata(), + timeout=servicer_context.time_remaining(), + ) + if not server_info.is_client_stream: + rpc_info.request = request_or_iterator + servicer_context = _OpenTelemetryServicerContext( + servicer_context, span + ) + result = handler(request_or_iterator, servicer_context) + for response in result: + yield response + _check_error_code(span, servicer_context, rpc_info) + + def intercept_stream( + self, request_or_iterator, servicer_context, server_info, handler + ): + if server_info.is_server_stream: + return self._intercept_server_stream( + request_or_iterator, servicer_context, server_info, handler + ) + with self._set_remote_context(servicer_context): + with self._start_span(server_info.full_method) as span: + rpc_info = RpcInfo( + full_method=server_info.full_method, + metadata=servicer_context.invocation_metadata(), + timeout=servicer_context.time_remaining(), + ) + servicer_context = _OpenTelemetryServicerContext( + servicer_context, span + ) + response = handler(request_or_iterator, servicer_context) + _check_error_code(span, servicer_context, rpc_info) + rpc_info.response = response + return response diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py new file mode 100644 index 0000000000..b6ff7d311a --- /dev/null +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/_utilities.py @@ -0,0 +1,33 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Internal utilities.""" + + +class RpcInfo: + def __init__( + self, + full_method=None, + metadata=None, + timeout=None, + request=None, + response=None, + error=None, + ): + self.full_method = full_method + self.metadata = metadata + self.timeout = timeout + self.request = request + self.response = response + self.error = error diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/__init__.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/__init__.py new file mode 100644 index 0000000000..fe83467a70 --- /dev/null +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/__init__.py @@ -0,0 +1,216 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint:disable=import-outside-toplevel +# pylint:disable=import-self +# pylint:disable=no-name-in-module + +import abc + + +class UnaryClientInfo(abc.ABC): + """Consists of various information about a unary RPC on the + invocation-side. + + Attributes: + full_method: A string of the full RPC method, i.e., + /package.service/method. + timeout: The length of time in seconds to wait for the computation to + terminate or be cancelled, or None if this method should block until + the computation is terminated or is cancelled no matter how long that + takes. + """ + + +class StreamClientInfo(abc.ABC): + """Consists of various information about a stream RPC on the + invocation-side. + + Attributes: + full_method: A string of the full RPC method, i.e., + /package.service/method. + is_client_stream: Indicates whether the RPC is client-streaming. + is_server_stream: Indicates whether the RPC is server-streaming. + timeout: The length of time in seconds to wait for the computation to + terminate or be cancelled, or None if this method should block until + the computation is terminated or is cancelled no matter how long that + takes. + """ + + +class UnaryClientInterceptor(abc.ABC): + """Affords intercepting unary-unary RPCs on the invocation-side.""" + + @abc.abstractmethod + def intercept_unary(self, request, metadata, client_info, invoker): + """Intercepts unary-unary RPCs on the invocation-side. + + Args: + request: The request value for the RPC. + metadata: Optional :term:`metadata` to be transmitted to the + service-side of the RPC. + client_info: A UnaryClientInfo containing various information about + the RPC. + invoker: The handler to complete the RPC on the client. It is the + interceptor's responsibility to call it. + + Returns: + The result from calling invoker(request, metadata). + """ + raise NotImplementedError() + + +class StreamClientInterceptor(abc.ABC): + """Affords intercepting stream RPCs on the invocation-side.""" + + @abc.abstractmethod + def intercept_stream( + self, request_or_iterator, metadata, client_info, invoker + ): + """Intercepts stream RPCs on the invocation-side. + + Args: + request_or_iterator: The request value for the RPC if + `client_info.is_client_stream` is `false`; otherwise, an iterator of + request values. + metadata: Optional :term:`metadata` to be transmitted to the service-side + of the RPC. + client_info: A StreamClientInfo containing various information about + the RPC. + invoker: The handler to complete the RPC on the client. It is the + interceptor's responsibility to call it. + + Returns: + The result from calling invoker(metadata). + """ + raise NotImplementedError() + + +def intercept_channel(channel, *interceptors): + """Creates an intercepted channel. + + Args: + channel: A Channel. + interceptors: Zero or more UnaryClientInterceptors or + StreamClientInterceptors + + Returns: + A Channel. + + Raises: + TypeError: If an interceptor derives from neither UnaryClientInterceptor + nor StreamClientInterceptor. + """ + from . import _interceptor + + return _interceptor.intercept_channel(channel, *interceptors) + + +class UnaryServerInfo(abc.ABC): + """Consists of various information about a unary RPC on the service-side. + + Attributes: + full_method: A string of the full RPC method, i.e., + /package.service/method. + """ + + +class StreamServerInfo(abc.ABC): + """Consists of various information about a stream RPC on the service-side. + + Attributes: + full_method: A string of the full RPC method, i.e., + /package.service/method. + is_client_stream: Indicates whether the RPC is client-streaming. + is_server_stream: Indicates whether the RPC is server-streaming. + """ + + +class UnaryServerInterceptor(abc.ABC): + """Affords intercepting unary-unary RPCs on the service-side.""" + + @abc.abstractmethod + def intercept_unary(self, request, servicer_context, server_info, handler): + """Intercepts unary-unary RPCs on the service-side. + + Args: + request: The request value for the RPC. + servicer_context: A ServicerContext. + server_info: A UnaryServerInfo containing various information about + the RPC. + handler: The handler to complete the RPC on the server. It is the + interceptor's responsibility to call it. + + Returns: + The result from calling handler(request, servicer_context). + """ + raise NotImplementedError() + + +class StreamServerInterceptor(abc.ABC): + """Affords intercepting stream RPCs on the service-side.""" + + @abc.abstractmethod + def intercept_stream( + self, request_or_iterator, servicer_context, server_info, handler + ): + """Intercepts stream RPCs on the service-side. + + Args: + request_or_iterator: The request value for the RPC if + `server_info.is_client_stream` is `False`; otherwise, an iterator of + request values. + servicer_context: A ServicerContext. + server_info: A StreamServerInfo containing various information about + the RPC. + handler: The handler to complete the RPC on the server. It is the + interceptor's responsibility to call it. + + Returns: + The result from calling handler(servicer_context). + """ + raise NotImplementedError() + + +def intercept_server(server, *interceptors): + """Creates an intercepted server. + + Args: + server: A Server. + interceptors: Zero or more UnaryServerInterceptors or + StreamServerInterceptors + + Returns: + A Server. + + Raises: + TypeError: If an interceptor derives from neither UnaryServerInterceptor + nor StreamServerInterceptor. + """ + from . import _interceptor + + return _interceptor.intercept_server(server, *interceptors) + + +__all__ = ( + "UnaryClientInterceptor", + "StreamClientInfo", + "StreamClientInterceptor", + "UnaryServerInfo", + "StreamServerInfo", + "UnaryServerInterceptor", + "StreamServerInterceptor", + "intercept_channel", + "intercept_server", +) diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/_interceptor.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/_interceptor.py new file mode 100644 index 0000000000..0cae2cf9fd --- /dev/null +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/grpcext/_interceptor.py @@ -0,0 +1,423 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint:disable=relative-beyond-top-level +# pylint:disable=arguments-differ +# pylint:disable=no-member +# pylint:disable=signature-differs + +"""Implementation of gRPC Python interceptors.""" + + +import collections + +import grpc + +from .. import grpcext + + +class _UnaryClientInfo( + collections.namedtuple("_UnaryClientInfo", ("full_method", "timeout")) +): + pass + + +class _StreamClientInfo( + collections.namedtuple( + "_StreamClientInfo", + ("full_method", "is_client_stream", "is_server_stream", "timeout"), + ) +): + pass + + +class _InterceptorUnaryUnaryMultiCallable(grpc.UnaryUnaryMultiCallable): + def __init__(self, method, base_callable, interceptor): + self._method = method + self._base_callable = base_callable + self._interceptor = interceptor + + def __call__(self, request, timeout=None, metadata=None, credentials=None): + def invoker(request, metadata): + return self._base_callable(request, timeout, metadata, credentials) + + client_info = _UnaryClientInfo(self._method, timeout) + return self._interceptor.intercept_unary( + request, metadata, client_info, invoker + ) + + def with_call( + self, request, timeout=None, metadata=None, credentials=None + ): + def invoker(request, metadata): + return self._base_callable.with_call( + request, timeout, metadata, credentials + ) + + client_info = _UnaryClientInfo(self._method, timeout) + return self._interceptor.intercept_unary( + request, metadata, client_info, invoker + ) + + def future(self, request, timeout=None, metadata=None, credentials=None): + def invoker(request, metadata): + return self._base_callable.future( + request, timeout, metadata, credentials + ) + + client_info = _UnaryClientInfo(self._method, timeout) + return self._interceptor.intercept_unary( + request, metadata, client_info, invoker + ) + + +class _InterceptorUnaryStreamMultiCallable(grpc.UnaryStreamMultiCallable): + def __init__(self, method, base_callable, interceptor): + self._method = method + self._base_callable = base_callable + self._interceptor = interceptor + + def __call__(self, request, timeout=None, metadata=None, credentials=None): + def invoker(request, metadata): + return self._base_callable(request, timeout, metadata, credentials) + + client_info = _StreamClientInfo(self._method, False, True, timeout) + return self._interceptor.intercept_stream( + request, metadata, client_info, invoker + ) + + +class _InterceptorStreamUnaryMultiCallable(grpc.StreamUnaryMultiCallable): + def __init__(self, method, base_callable, interceptor): + self._method = method + self._base_callable = base_callable + self._interceptor = interceptor + + def __call__( + self, request_iterator, timeout=None, metadata=None, credentials=None + ): + def invoker(request_iterator, metadata): + return self._base_callable( + request_iterator, timeout, metadata, credentials + ) + + client_info = _StreamClientInfo(self._method, True, False, timeout) + return self._interceptor.intercept_stream( + request_iterator, metadata, client_info, invoker + ) + + def with_call( + self, request_iterator, timeout=None, metadata=None, credentials=None + ): + def invoker(request_iterator, metadata): + return self._base_callable.with_call( + request_iterator, timeout, metadata, credentials + ) + + client_info = _StreamClientInfo(self._method, True, False, timeout) + return self._interceptor.intercept_stream( + request_iterator, metadata, client_info, invoker + ) + + def future( + self, request_iterator, timeout=None, metadata=None, credentials=None + ): + def invoker(request_iterator, metadata): + return self._base_callable.future( + request_iterator, timeout, metadata, credentials + ) + + client_info = _StreamClientInfo(self._method, True, False, timeout) + return self._interceptor.intercept_stream( + request_iterator, metadata, client_info, invoker + ) + + +class _InterceptorStreamStreamMultiCallable(grpc.StreamStreamMultiCallable): + def __init__(self, method, base_callable, interceptor): + self._method = method + self._base_callable = base_callable + self._interceptor = interceptor + + def __call__( + self, request_iterator, timeout=None, metadata=None, credentials=None + ): + def invoker(request_iterator, metadata): + return self._base_callable( + request_iterator, timeout, metadata, credentials + ) + + client_info = _StreamClientInfo(self._method, True, True, timeout) + return self._interceptor.intercept_stream( + request_iterator, metadata, client_info, invoker + ) + + +class _InterceptorChannel(grpc.Channel): + def __init__(self, channel, interceptor): + self._channel = channel + self._interceptor = interceptor + + def subscribe(self, *args, **kwargs): + self._channel.subscribe(*args, **kwargs) + + def unsubscribe(self, *args, **kwargs): + self._channel.unsubscribe(*args, **kwargs) + + def unary_unary( + self, method, request_serializer=None, response_deserializer=None + ): + base_callable = self._channel.unary_unary( + method, request_serializer, response_deserializer + ) + if isinstance(self._interceptor, grpcext.UnaryClientInterceptor): + return _InterceptorUnaryUnaryMultiCallable( + method, base_callable, self._interceptor + ) + return base_callable + + def unary_stream( + self, method, request_serializer=None, response_deserializer=None + ): + base_callable = self._channel.unary_stream( + method, request_serializer, response_deserializer + ) + if isinstance(self._interceptor, grpcext.StreamClientInterceptor): + return _InterceptorUnaryStreamMultiCallable( + method, base_callable, self._interceptor + ) + return base_callable + + def stream_unary( + self, method, request_serializer=None, response_deserializer=None + ): + base_callable = self._channel.stream_unary( + method, request_serializer, response_deserializer + ) + if isinstance(self._interceptor, grpcext.StreamClientInterceptor): + return _InterceptorStreamUnaryMultiCallable( + method, base_callable, self._interceptor + ) + return base_callable + + def stream_stream( + self, method, request_serializer=None, response_deserializer=None + ): + base_callable = self._channel.stream_stream( + method, request_serializer, response_deserializer + ) + if isinstance(self._interceptor, grpcext.StreamClientInterceptor): + return _InterceptorStreamStreamMultiCallable( + method, base_callable, self._interceptor + ) + return base_callable + + def close(self): + if not hasattr(self._channel, "close"): + raise RuntimeError( + "close() is not supported with the installed version of grpcio" + ) + self._channel.close() + + +def intercept_channel(channel, *interceptors): + result = channel + for interceptor in interceptors: + if not isinstance( + interceptor, grpcext.UnaryClientInterceptor + ) and not isinstance(interceptor, grpcext.StreamClientInterceptor): + raise TypeError( + "interceptor must be either a " + "grpcext.UnaryClientInterceptor or a " + "grpcext.StreamClientInterceptor" + ) + result = _InterceptorChannel(result, interceptor) + return result + + +class _UnaryServerInfo( + collections.namedtuple("_UnaryServerInfo", ("full_method",)) +): + pass + + +class _StreamServerInfo( + collections.namedtuple( + "_StreamServerInfo", + ("full_method", "is_client_stream", "is_server_stream"), + ) +): + pass + + +class _InterceptorRpcMethodHandler(grpc.RpcMethodHandler): + def __init__(self, rpc_method_handler, method, interceptor): + self._rpc_method_handler = rpc_method_handler + self._method = method + self._interceptor = interceptor + + @property + def request_streaming(self): + return self._rpc_method_handler.request_streaming + + @property + def response_streaming(self): + return self._rpc_method_handler.response_streaming + + @property + def request_deserializer(self): + return self._rpc_method_handler.request_deserializer + + @property + def response_serializer(self): + return self._rpc_method_handler.response_serializer + + @property + def unary_unary(self): + if not isinstance(self._interceptor, grpcext.UnaryServerInterceptor): + return self._rpc_method_handler.unary_unary + + def adaptation(request, servicer_context): + def handler(request, servicer_context): + return self._rpc_method_handler.unary_unary( + request, servicer_context + ) + + return self._interceptor.intercept_unary( + request, + servicer_context, + _UnaryServerInfo(self._method), + handler, + ) + + return adaptation + + @property + def unary_stream(self): + if not isinstance(self._interceptor, grpcext.StreamServerInterceptor): + return self._rpc_method_handler.unary_stream + + def adaptation(request, servicer_context): + def handler(request, servicer_context): + return self._rpc_method_handler.unary_stream( + request, servicer_context + ) + + return self._interceptor.intercept_stream( + request, + servicer_context, + _StreamServerInfo(self._method, False, True), + handler, + ) + + return adaptation + + @property + def stream_unary(self): + if not isinstance(self._interceptor, grpcext.StreamServerInterceptor): + return self._rpc_method_handler.stream_unary + + def adaptation(request_iterator, servicer_context): + def handler(request_iterator, servicer_context): + return self._rpc_method_handler.stream_unary( + request_iterator, servicer_context + ) + + return self._interceptor.intercept_stream( + request_iterator, + servicer_context, + _StreamServerInfo(self._method, True, False), + handler, + ) + + return adaptation + + @property + def stream_stream(self): + if not isinstance(self._interceptor, grpcext.StreamServerInterceptor): + return self._rpc_method_handler.stream_stream + + def adaptation(request_iterator, servicer_context): + def handler(request_iterator, servicer_context): + return self._rpc_method_handler.stream_stream( + request_iterator, servicer_context + ) + + return self._interceptor.intercept_stream( + request_iterator, + servicer_context, + _StreamServerInfo(self._method, True, True), + handler, + ) + + return adaptation + + +class _InterceptorGenericRpcHandler(grpc.GenericRpcHandler): + def __init__(self, generic_rpc_handler, interceptor): + self.generic_rpc_handler = generic_rpc_handler + self._interceptor = interceptor + + def service(self, handler_call_details): + result = self.generic_rpc_handler.service(handler_call_details) + if result: + result = _InterceptorRpcMethodHandler( + result, handler_call_details.method, self._interceptor + ) + return result + + +class _InterceptorServer(grpc.Server): + def __init__(self, server, interceptor): + self._server = server + self._interceptor = interceptor + + def add_generic_rpc_handlers(self, generic_rpc_handlers): + generic_rpc_handlers = [ + _InterceptorGenericRpcHandler( + generic_rpc_handler, self._interceptor + ) + for generic_rpc_handler in generic_rpc_handlers + ] + return self._server.add_generic_rpc_handlers(generic_rpc_handlers) + + def add_insecure_port(self, *args, **kwargs): + return self._server.add_insecure_port(*args, **kwargs) + + def add_secure_port(self, *args, **kwargs): + return self._server.add_secure_port(*args, **kwargs) + + def start(self, *args, **kwargs): + return self._server.start(*args, **kwargs) + + def stop(self, *args, **kwargs): + return self._server.stop(*args, **kwargs) + + def wait_for_termination(self, *args, **kwargs): + return self._server.wait_for_termination(*args, **kwargs) + + +def intercept_server(server, *interceptors): + result = server + for interceptor in interceptors: + if not isinstance( + interceptor, grpcext.UnaryServerInterceptor + ) and not isinstance(interceptor, grpcext.StreamServerInterceptor): + raise TypeError( + "interceptor must be either a " + "grpcext.UnaryServerInterceptor or a " + "grpcext.StreamServerInterceptor" + ) + result = _InterceptorServer(result, interceptor) + return result diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py new file mode 100644 index 0000000000..0941210ca3 --- /dev/null +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.6.dev0" diff --git a/ext/opentelemetry-ext-grpc/tests/__init__.py b/ext/opentelemetry-ext-grpc/tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/ext/opentelemetry-ext-grpc/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py new file mode 100644 index 0000000000..8dabd11fdf --- /dev/null +++ b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py @@ -0,0 +1,243 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint:disable=unused-argument +# pylint:disable=no-self-use + +import threading +import unittest +from concurrent import futures +from contextlib import contextmanager +from unittest import mock + +import grpc + +from opentelemetry import trace +from opentelemetry.ext.grpc import server_interceptor +from opentelemetry.ext.grpc.grpcext import intercept_server +from opentelemetry.sdk import trace as trace_sdk + + +class UnaryUnaryMethodHandler(grpc.RpcMethodHandler): + def __init__(self, handler): + self.request_streaming = False + self.response_streaming = False + self.request_deserializer = None + self.response_serializer = None + self.unary_unary = handler + self.unary_stream = None + self.stream_unary = None + self.stream_stream = None + + +class UnaryUnaryRpcHandler(grpc.GenericRpcHandler): + def __init__(self, handler): + self._unary_unary_handler = handler + + def service(self, handler_call_details): + return UnaryUnaryMethodHandler(self._unary_unary_handler) + + +class TestOpenTelemetryServerInterceptor(unittest.TestCase): + def test_create_span(self): + """Check that the interceptor wraps calls with spans server-side.""" + + @contextmanager + def mock_start_as_current_span(*args, **kwargs): + yield mock.Mock(spec=trace.Span) + + # Intercept gRPC calls... + tracer = mock.Mock(spec=trace.Tracer) + tracer.start_as_current_span.side_effect = mock_start_as_current_span + interceptor = server_interceptor(tracer) + + # No-op RPC handler + def handler(request, context): + return b"" + + server = grpc.server( + futures.ThreadPoolExecutor(max_workers=1), + options=(("grpc.so_reuseport", 0),), + ) + # FIXME: grpcext interceptor doesn't apply to handlers passed to server + # init, should use intercept_service API instead. + server = intercept_server(server, interceptor) + server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) + + port = server.add_insecure_port("[::]:0") + channel = grpc.insecure_channel("localhost:{:d}".format(port)) + + try: + server.start() + channel.unary_unary("")(b"") + finally: + server.stop(None) + + tracer.start_as_current_span.assert_called_once_with( + name="", kind=trace.SpanKind.SERVER + ) + + def test_span_lifetime(self): + """Check that the span is active for the duration of the call.""" + + tracer_provider = trace_sdk.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) + interceptor = server_interceptor(tracer) + + # To capture the current span at the time the handler is called + active_span_in_handler = None + + def handler(request, context): + nonlocal active_span_in_handler + active_span_in_handler = tracer.get_current_span() + return b"" + + server = grpc.server( + futures.ThreadPoolExecutor(max_workers=1), + options=(("grpc.so_reuseport", 0),), + ) + server = intercept_server(server, interceptor) + server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) + + port = server.add_insecure_port("[::]:0") + channel = grpc.insecure_channel("localhost:{:d}".format(port)) + + active_span_before_call = tracer.get_current_span() + try: + server.start() + channel.unary_unary("")(b"") + finally: + server.stop(None) + active_span_after_call = tracer.get_current_span() + + self.assertIsNone(active_span_before_call) + self.assertIsNone(active_span_after_call) + self.assertIsInstance(active_span_in_handler, trace_sdk.Span) + self.assertIsNone(active_span_in_handler.parent) + + def test_sequential_server_spans(self): + """Check that sequential RPCs get separate server spans.""" + + tracer_provider = trace_sdk.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) + + interceptor = server_interceptor(tracer) + + # Capture the currently active span in each thread + active_spans_in_handler = [] + + def handler(request, context): + active_spans_in_handler.append(tracer.get_current_span()) + return b"" + + server = grpc.server( + futures.ThreadPoolExecutor(max_workers=1), + options=(("grpc.so_reuseport", 0),), + ) + server = intercept_server(server, interceptor) + server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) + + port = server.add_insecure_port("[::]:0") + channel = grpc.insecure_channel("localhost:{:d}".format(port)) + + try: + server.start() + channel.unary_unary("")(b"") + channel.unary_unary("")(b"") + finally: + server.stop(None) + + self.assertEqual(len(active_spans_in_handler), 2) + # pylint:disable=unbalanced-tuple-unpacking + span1, span2 = active_spans_in_handler + # Spans should belong to separate traces, and each should be a root + # span + self.assertNotEqual(span1.context.span_id, span2.context.span_id) + self.assertNotEqual(span1.context.trace_id, span2.context.trace_id) + self.assertIsNone(span1.parent) + self.assertIsNone(span1.parent) + + def test_concurrent_server_spans(self): + """Check that concurrent RPC calls don't interfere with each other. + + This is the same check as test_sequential_server_spans except that the + RPCs are concurrent. Two handlers are invoked at the same time on two + separate threads. Each one should see a different active span and + context. + """ + + tracer_provider = trace_sdk.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) + + interceptor = server_interceptor(tracer) + + # Capture the currently active span in each thread + active_spans_in_handler = [] + latch = get_latch(2) + + def handler(request, context): + latch() + active_spans_in_handler.append(tracer.get_current_span()) + return b"" + + server = grpc.server( + futures.ThreadPoolExecutor(max_workers=2), + options=(("grpc.so_reuseport", 0),), + ) + server = intercept_server(server, interceptor) + server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) + + port = server.add_insecure_port("[::]:0") + channel = grpc.insecure_channel("localhost:{:d}".format(port)) + + try: + server.start() + # Interleave calls so spans are active on each thread at the same + # time + with futures.ThreadPoolExecutor(max_workers=2) as tpe: + f1 = tpe.submit(channel.unary_unary(""), b"") + f2 = tpe.submit(channel.unary_unary(""), b"") + futures.wait((f1, f2)) + finally: + server.stop(None) + + self.assertEqual(len(active_spans_in_handler), 2) + # pylint:disable=unbalanced-tuple-unpacking + span1, span2 = active_spans_in_handler + # Spans should belong to separate traces, and each should be a root + # span + self.assertNotEqual(span1.context.span_id, span2.context.span_id) + self.assertNotEqual(span1.context.trace_id, span2.context.trace_id) + self.assertIsNone(span1.parent) + self.assertIsNone(span1.parent) + + +def get_latch(num): + """Get a countdown latch function for use in n threads.""" + cv = threading.Condition() + count = 0 + + def countdown_latch(): + """Block until n-1 other threads have called.""" + nonlocal count + cv.acquire() + count += 1 + cv.notify() + cv.release() + cv.acquire() + while count < num: + cv.wait() + cv.release() + + return countdown_latch diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 779bdc488c..adf1bc0869 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. import logging +import threading import typing from functools import wraps from os import environ @@ -24,7 +25,7 @@ logger = logging.getLogger(__name__) _RUNTIME_CONTEXT = None # type: typing.Optional[RuntimeContext] - +_RUNTIME_CONTEXT_LOCK = threading.Lock() _F = typing.TypeVar("_F", bound=typing.Callable[..., typing.Any]) @@ -42,26 +43,30 @@ def wrapper( **kwargs: typing.Dict[typing.Any, typing.Any] ) -> typing.Optional[typing.Any]: global _RUNTIME_CONTEXT # pylint: disable=global-statement - if _RUNTIME_CONTEXT is None: - # FIXME use a better implementation of a configuration manager to avoid having - # to get configuration values straight from environment variables - if version_info < (3, 5): - # contextvars are not supported in 3.4, use thread-local storage - default_context = "threadlocal_context" - else: - default_context = "contextvars_context" - - configured_context = environ.get( - "OPENTELEMETRY_CONTEXT", default_context - ) # type: str - try: - _RUNTIME_CONTEXT = next( - iter_entry_points( - "opentelemetry_context", configured_context + + with _RUNTIME_CONTEXT_LOCK: + if _RUNTIME_CONTEXT is None: + # FIXME use a better implementation of a configuration manager to avoid having + # to get configuration values straight from environment variables + if version_info < (3, 5): + # contextvars are not supported in 3.4, use thread-local storage + default_context = "threadlocal_context" + else: + default_context = "contextvars_context" + + configured_context = environ.get( + "OPENTELEMETRY_CONTEXT", default_context + ) # type: str + try: + _RUNTIME_CONTEXT = next( + iter_entry_points( + "opentelemetry_context", configured_context + ) + ).load()() + except Exception: # pylint: disable=broad-except + logger.error( + "Failed to load context: %s", configured_context ) - ).load()() - except Exception: # pylint: disable=broad-except - logger.error("Failed to load context: %s", configured_context) return func(*args, **kwargs) # type: ignore return wrapper # type:ignore diff --git a/pyproject.toml b/pyproject.toml index 5d19c29882..961304074c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,8 +2,9 @@ line-length = 79 exclude = ''' ( - /( - ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen # generated files + /( # generated files + docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen| + ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen )/ ) ''' diff --git a/tox.ini b/tox.ini index 6e64bdbd44..06a54bc83d 100644 --- a/tox.ini +++ b/tox.ini @@ -76,8 +76,12 @@ envlist = py3{4,5,6,7,8}-test-opentracing-shim pypy3-test-opentracing-shim + ; opentelemetry-opentracing-shim + py3{4,5,6,7,8}-test-opentracing-shim + pypy3-test-opentracing-shim - py3{4,5,6,7,8}-coverage + ; opentelemetry-ext-grpc + py3{4,5,6,7,8}-test-ext-grpc ; Coverage is temporarily disabled for pypy3 due to the pytest bug. ; pypy3-coverage @@ -107,6 +111,7 @@ changedir = test-api: opentelemetry-api/tests test-sdk: opentelemetry-sdk/tests test-auto-instrumentation: opentelemetry-auto-instrumentation/tests + test-ext-grpc: ext/opentelemetry-ext-grpc/tests test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests @@ -144,6 +149,8 @@ commands_pre = example-http: pip install -r {toxinidir}/docs/examples/http/requirements.txt ext: pip install {toxinidir}/opentelemetry-api + grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc + grpc: pip install {toxinidir}/opentelemetry-sdk wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-testutil wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-wsgi wsgi,flask: pip install {toxinidir}/opentelemetry-sdk From e2f6c3d744f08487be243100f6f91c85b7c8b117 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 30 Mar 2020 18:16:22 -0700 Subject: [PATCH 0268/1517] docs: updating changelogs for 0.6 release (#533) --- ext/opentelemetry-ext-flask/CHANGELOG.md | 10 +++++++++- ext/opentelemetry-ext-grpc/CHANGELOG.md | 9 +++++++++ ext/opentelemetry-ext-jaeger/CHANGELOG.md | 7 +++++++ ext/opentelemetry-ext-otcollector/CHANGELOG.md | 2 +- ext/opentelemetry-ext-prometheus/CHANGELOG.md | 1 - ext/opentelemetry-ext-pymongo/CHANGELOG.md | 1 - opentelemetry-api/CHANGELOG.md | 16 +++++++++++++--- opentelemetry-auto-instrumentation/CHANGELOG.md | 6 ++++++ opentelemetry-sdk/CHANGELOG.md | 14 +++++++++++++- 9 files changed, 58 insertions(+), 8 deletions(-) diff --git a/ext/opentelemetry-ext-flask/CHANGELOG.md b/ext/opentelemetry-ext-flask/CHANGELOG.md index d3bf663073..f7523f36c4 100644 --- a/ext/opentelemetry-ext-flask/CHANGELOG.md +++ b/ext/opentelemetry-ext-flask/CHANGELOG.md @@ -2,12 +2,20 @@ ## Unreleased +## 0.6b0 + +Released 2020-03-30 + +- Add an entry_point to be usable in auto-instrumentation + ([#327](https://github.com/open-telemetry/opentelemetry-python/pull/327)) + ## 0.4a0 +Released 2020-02-21 + - Use string keys for WSGI environ values ([#366](https://github.com/open-telemetry/opentelemetry-python/pull/366)) - ## 0.3a0 Released 2019-12-11 diff --git a/ext/opentelemetry-ext-grpc/CHANGELOG.md b/ext/opentelemetry-ext-grpc/CHANGELOG.md index e69de29bb2..3d9a300699 100644 --- a/ext/opentelemetry-ext-grpc/CHANGELOG.md +++ b/ext/opentelemetry-ext-grpc/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## 0.6b0 + +Released 2020-03-30 + +- Add gRPC integration + ([#476](https://github.com/open-telemetry/opentelemetry-python/pull/476)) +- Initial release diff --git a/ext/opentelemetry-ext-jaeger/CHANGELOG.md b/ext/opentelemetry-ext-jaeger/CHANGELOG.md index 2b360199c0..879e6b0f84 100644 --- a/ext/opentelemetry-ext-jaeger/CHANGELOG.md +++ b/ext/opentelemetry-ext-jaeger/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +## 0.6b0 + +Released 2020-03-30 + +- Exporting to collector now works + ([#508](https://github.com/open-telemetry/opentelemetry-python/pull/508)) + ## 0.4a0 Released 2020-02-21 diff --git a/ext/opentelemetry-ext-otcollector/CHANGELOG.md b/ext/opentelemetry-ext-otcollector/CHANGELOG.md index 2dbd7217b7..0f6ed327bb 100644 --- a/ext/opentelemetry-ext-otcollector/CHANGELOG.md +++ b/ext/opentelemetry-ext-otcollector/CHANGELOG.md @@ -6,4 +6,4 @@ Released 2020-03-16 -Initial release. \ No newline at end of file +- Initial release. diff --git a/ext/opentelemetry-ext-prometheus/CHANGELOG.md b/ext/opentelemetry-ext-prometheus/CHANGELOG.md index 91730a080a..d5a548aa13 100644 --- a/ext/opentelemetry-ext-prometheus/CHANGELOG.md +++ b/ext/opentelemetry-ext-prometheus/CHANGELOG.md @@ -7,4 +7,3 @@ Released 2020-02-21 - Initial release - diff --git a/ext/opentelemetry-ext-pymongo/CHANGELOG.md b/ext/opentelemetry-ext-pymongo/CHANGELOG.md index c13f1be4a2..d947fff944 100644 --- a/ext/opentelemetry-ext-pymongo/CHANGELOG.md +++ b/ext/opentelemetry-ext-pymongo/CHANGELOG.md @@ -9,7 +9,6 @@ Released 2020-02-21 - Updating network connection attribute names ([#350](https://github.com/open-telemetry/opentelemetry-python/pull/350)) - ## 0.3a0 Released 2019-12-11 diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index a19ed76e4f..b6b28c119d 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,19 @@ ## Unreleased +## 0.6b0 + +Released 2020-03-30 + +- Add support for lazy events and links + ([#474](https://github.com/open-telemetry/opentelemetry-python/pull/474)) +- Metrics API no longer uses LabelSet + ([#527](https://github.com/open-telemetry/opentelemetry-python/pull/527)) +- Adding is_remote flag to SpanContext, indicating when a span is remote + ([#516](https://github.com/open-telemetry/opentelemetry-python/pull/516)) +- Allow digit as first char in vendor specific trace state key + ([#511](https://github.com/open-telemetry/opentelemetry-python/pull/511)) + ## 0.5b0 Released 2020-03-16 @@ -46,9 +59,6 @@ Released 2020-02-21 - Adding trace.get_tracer function ([#430](https://github.com/open-telemetry/opentelemetry-python/pull/430)) - - - ## 0.3a0 Released 2019-12-11 diff --git a/opentelemetry-auto-instrumentation/CHANGELOG.md b/opentelemetry-auto-instrumentation/CHANGELOG.md index 825c32f0d0..fba2c751d6 100644 --- a/opentelemetry-auto-instrumentation/CHANGELOG.md +++ b/opentelemetry-auto-instrumentation/CHANGELOG.md @@ -1 +1,7 @@ # Changelog + +## 0.6b0 + +Released 2020-03-30 + +- Initial release. \ No newline at end of file diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index aa8aafbac9..23663a9960 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,19 @@ ## Unreleased +## 0.6b0 + +Released 2020-03-30 + +- Add support for lazy events and links + ([#474](https://github.com/open-telemetry/opentelemetry-python/pull/474)) +- Metrics API no longer uses LabelSet + ([#527](https://github.com/open-telemetry/opentelemetry-python/pull/527)) +- Adding is_remote flag to SpanContext, indicating when a span is remote + ([#516](https://github.com/open-telemetry/opentelemetry-python/pull/516)) +- Adding a solution to release metric handles and observers + ([#435](https://github.com/open-telemetry/opentelemetry-python/pull/435)) + ## 0.5b0 Released 2020-03-16 @@ -55,7 +68,6 @@ Released 2020-02-21 - Implement MinMaxSumCount aggregator ([#422](https://github.com/open-telemetry/opentelemetry-python/pull/422)) - ## 0.3a0 Released 2019-12-11 From 2ca94a686fe4143db9796315a43c804f9f65d947 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 30 Mar 2020 21:15:12 -0700 Subject: [PATCH 0269/1517] sdk: Freeze sequence-valued span attributes (#449) reducing potential user errors by converting attributes into immutable tuples when added. --- .../src/opentelemetry/sdk/trace/__init__.py | 5 ++++- opentelemetry-sdk/tests/trace/test_trace.py | 20 ++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index c1389594a3..c162ea5c1c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -20,7 +20,7 @@ import threading from contextlib import contextmanager from types import TracebackType -from typing import Iterator, Optional, Sequence, Tuple, Type +from typing import Iterator, MutableSequence, Optional, Sequence, Tuple, Type from opentelemetry import context as context_api from opentelemetry import trace as trace_api @@ -314,6 +314,9 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: if error_message is not None: logger.warning("%s in attribute value sequence", error_message) return + # Freeze mutable sequences defensively + if isinstance(value, MutableSequence): + value = tuple(value) elif not isinstance(value, (bool, str, int, float)): logger.warning("invalid type for attribute value") return diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 4781c7cf80..ccb1ccd1e4 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -412,8 +412,10 @@ def test_attributes(self): root.set_attribute("attr-key", "attr-value2") root.set_attribute("empty-list", []) - root.set_attribute("list-of-bools", [True, True, False]) - root.set_attribute("list-of-numerics", [123, 314, 0]) + list_of_bools = [True, True, False] + root.set_attribute("list-of-bools", list_of_bools) + list_of_numerics = [123, 314, 0] + root.set_attribute("list-of-numerics", list_of_numerics) self.assertEqual(len(root.attributes), 10) self.assertEqual(root.attributes["component"], "http") @@ -426,12 +428,20 @@ def test_attributes(self): self.assertEqual(root.attributes["http.status_text"], "OK") self.assertEqual(root.attributes["misc.pi"], 3.14) self.assertEqual(root.attributes["attr-key"], "attr-value2") - self.assertEqual(root.attributes["empty-list"], []) + self.assertEqual(root.attributes["empty-list"], ()) self.assertEqual( - root.attributes["list-of-bools"], [True, True, False] + root.attributes["list-of-bools"], (True, True, False) ) + list_of_bools.append(False) self.assertEqual( - root.attributes["list-of-numerics"], [123, 314, 0] + root.attributes["list-of-bools"], (True, True, False) + ) + self.assertEqual( + root.attributes["list-of-numerics"], (123, 314, 0) + ) + list_of_numerics.append(227) + self.assertEqual( + root.attributes["list-of-numerics"], (123, 314, 0) ) attributes = { From 889480596b19f14fa0cdda121c96c53b557f30ed Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 30 Mar 2020 22:26:31 -0700 Subject: [PATCH 0270/1517] Version bump for master post-v0.6b0 release (#537) --- docs/examples/opentelemetry-example-app/setup.py | 2 +- ext/opentelemetry-ext-dbapi/setup.cfg | 2 +- .../src/opentelemetry/ext/dbapi/version.py | 2 +- .../src/opentelemetry/ext/flask/version.py | 2 +- .../src/opentelemetry/ext/grpc/version.py | 2 +- ext/opentelemetry-ext-http-requests/setup.cfg | 2 +- .../src/opentelemetry/ext/http_requests/version.py | 2 +- .../src/opentelemetry/ext/jaeger/version.py | 2 +- ext/opentelemetry-ext-mysql/setup.cfg | 2 +- .../src/opentelemetry/ext/mysql/version.py | 2 +- .../src/opentelemetry/ext/opentracing_shim/version.py | 2 +- ext/opentelemetry-ext-otcollector/setup.cfg | 4 ++-- .../src/opentelemetry/ext/otcollector/version.py | 2 +- .../src/opentelemetry/ext/prometheus/version.py | 2 +- ext/opentelemetry-ext-psycopg2/setup.cfg | 2 +- .../src/opentelemetry/ext/psycopg2/version.py | 2 +- ext/opentelemetry-ext-pymongo/setup.cfg | 2 +- .../src/opentelemetry/ext/pymongo/version.py | 2 +- .../src/opentelemetry/ext/testutil/version.py | 2 +- .../src/opentelemetry/ext/wsgi/version.py | 2 +- .../src/opentelemetry/ext/zipkin/version.py | 2 +- opentelemetry-api/src/opentelemetry/util/version.py | 2 +- opentelemetry-auto-instrumentation/setup.cfg | 2 +- .../src/opentelemetry/auto_instrumentation/version.py | 2 +- opentelemetry-sdk/setup.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/version.py | 2 +- 26 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py index 1466fd9463..9764784ba2 100644 --- a/docs/examples/opentelemetry-example-app/setup.py +++ b/docs/examples/opentelemetry-example-app/setup.py @@ -16,7 +16,7 @@ setuptools.setup( name="opentelemetry-example-app", - version="0.6.dev0", + version="0.7.dev0", author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 84d3ed4a20..7a474632a2 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.6.dev0 + opentelemetry-api >= 0.7.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py index 0941210ca3..86c61362ab 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py index 0941210ca3..86c61362ab 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py index 0941210ca3..86c61362ab 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg index ced12c71eb..9e7cee9ea0 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.6.dev0 + opentelemetry-api >= 0.7.dev0 requests ~= 2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py index 0941210ca3..86c61362ab 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index cebb24db32..75b9029c1f 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index d43bfd0645..fe27b9f920 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.6.dev0 + opentelemetry-api >= 0.7.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index 0941210ca3..86c61362ab 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index 0941210ca3..86c61362ab 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-otcollector/setup.cfg b/ext/opentelemetry-ext-otcollector/setup.cfg index 804e596d06..aab6af03e5 100644 --- a/ext/opentelemetry-ext-otcollector/setup.cfg +++ b/ext/opentelemetry-ext-otcollector/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api >= 0.6.dev0 - opentelemetry-sdk >= 0.6.dev0 + opentelemetry-api >= 0.7.dev0 + opentelemetry-sdk >= 0.7.dev0 protobuf >= 3.8.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py index 0941210ca3..86c61362ab 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py index 0941210ca3..86c61362ab 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index 25ee7012a6..8fbcfe539a 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.6.dev0 + opentelemetry-api >= 0.7.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py index 0941210ca3..86c61362ab 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index 2dad6e62cb..be6f29488b 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.6.dev0 + opentelemetry-api >= 0.7.dev0 pymongo ~= 3.1 [options.packages.find] diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index 0941210ca3..86c61362ab 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py index d0b2d05c07..55bf2e584c 100644 --- a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py +++ b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py @@ -1 +1 @@ -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index 0941210ca3..86c61362ab 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py index 0941210ca3..86c61362ab 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/opentelemetry-api/src/opentelemetry/util/version.py b/opentelemetry-api/src/opentelemetry/util/version.py index 0941210ca3..86c61362ab 100644 --- a/opentelemetry-api/src/opentelemetry/util/version.py +++ b/opentelemetry-api/src/opentelemetry/util/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/opentelemetry-auto-instrumentation/setup.cfg b/opentelemetry-auto-instrumentation/setup.cfg index 182b15866f..3cd85e5bd1 100644 --- a/opentelemetry-auto-instrumentation/setup.cfg +++ b/opentelemetry-auto-instrumentation/setup.cfg @@ -40,7 +40,7 @@ package_dir= packages=find_namespace: zip_safe = False include_package_data = True -install_requires = opentelemetry-api==0.6.dev0 +install_requires = opentelemetry-api==0.7.dev0 [options.packages.find] where = src diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py index 0941210ca3..86c61362ab 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index ce17cb4577..26c7e348d5 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -45,7 +45,7 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["opentelemetry-api==0.6.dev0"], + install_requires=["opentelemetry-api==0.7.dev0"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 0941210ca3..86c61362ab 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.dev0" +__version__ = "0.7.dev0" From ac56d0e33b91d62659a28c061cd66d1b7dfcc40c Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Wed, 1 Apr 2020 10:21:16 -0700 Subject: [PATCH 0271/1517] sdk: Adding suppress_instrumentation in Metrics (#529) Suppressing instrumentation in metrics to ensure no infinite loops of emitting telemetry. --- .../sdk/metrics/export/controller.py | 4 ++++ .../tests/metrics/export/test_export.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py index 3b7db0e886..5cbb44e2fd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py @@ -15,6 +15,8 @@ import atexit import threading +from opentelemetry.context import attach, detach, set_value + class PushController(threading.Thread): """A push based controller, used for exporting. @@ -50,7 +52,9 @@ def shutdown(self): def tick(self): # Collect all of the meter's metrics to be exported self.meter.collect() + token = attach(set_value("suppress_instrumentation", True)) # Export the given metrics in the batcher self.exporter.export(self.meter.batcher.checkpoint_set()) + detach(token) # Perform post-exporting logic based on batcher configuration self.meter.batcher.finished_collection() diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 311237505c..69cdcf6e16 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -17,6 +17,7 @@ import unittest from unittest import mock +from opentelemetry.context import get_value from opentelemetry.sdk import metrics from opentelemetry.sdk.metrics.export import ( ConsoleMetricsExporter, @@ -511,3 +512,18 @@ def test_push_controller(self): controller.shutdown() self.assertTrue(controller.finished.isSet()) exporter.shutdown.assert_any_call() + + def test_push_controller_suppress_instrumentation(self): + meter = mock.Mock() + exporter = mock.Mock() + exporter.export = lambda x: self.assertIsNotNone( + get_value("suppress_instrumentation") + ) + with mock.patch( + "opentelemetry.context._RUNTIME_CONTEXT" + ) as context_patch: + controller = PushController(meter, exporter, 30.0) + controller.tick() + self.assertEqual(context_patch.attach.called, True) + self.assertEqual(context_patch.detach.called, True) + self.assertEqual(get_value("suppress_instrumentation"), None) From 6edee7fd3007e345f5a7f0d6546e496eb7c633c4 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 2 Apr 2020 08:24:07 -0700 Subject: [PATCH 0272/1517] Update README for 0.6.0 release (#540) --- README.md | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index abd77f006f..d7fe2e819a 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ Libraries that produce telemetry data should only depend on `opentelemetry-api`, and defer the choice of the SDK to the application developer. Applications may depend on `opentelemetry-sdk` or another package that implements the API. -**Please note** that this library is currently in _alpha_, and shouldn't be -used in production environments. +**Please note** that this library is currently in _beta_, and shouldn't +generally be used in production environments. The API and SDK packages are available on PyPI, and can installed via `pip`: @@ -91,8 +91,8 @@ OpenTelemetry Python is under active development. The library is not yet _generally available_, and releases aren't guaranteed to conform to a specific version of the specification. Future releases will not attempt to maintain backwards compatibility with previous releases. Each alpha -release includes significant changes to the API and SDK packages, making them -incompatible with each other. +and beta release includes significant changes to the API and SDK packages, +making them incompatible with each other. The [v0.1 alpha release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.1.0) @@ -123,7 +123,8 @@ includes: - PyMongo Integration The [v0.4 alpha -release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.4.0) release includes: +release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.4.0) +includes: - Metrics MinMaxSumCount Aggregator - Context API @@ -137,7 +138,8 @@ release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0. - New Examples and Improvements to Existing Examples The [v0.5 beta -release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.5.0) release includes: +release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.5.0) +includes: - W3C Correlation Context Propagation - OpenTelemetry Collector Exporter Integration for both metrics and traces @@ -145,15 +147,15 @@ release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0. - Global configuration module - Documentation improvements -See the [project -milestones](https://github.com/open-telemetry/opentelemetry-python/milestones) -for details on upcoming releases. The dates and features described here are -estimates, and subject to change. +The [v0.6 beta +release](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.6.0) +includes: -Future releases targets include: +- API changes and bugfixes +- An autoinstrumentation package and updated Flask instrumentation +- gRPC integration -| Component | Version | Target Date | -| ---------------------------------- | ------- | ------------- | -| Stable API for metrics and tracing | Beta v2 | March 31 2020 | -| Support for Tags/Baggage | Beta v2 | March 31 2020 | -| gRPC Integration | Beta v2 | March 31 2020 | +See the [project +milestones](https://github.com/open-telemetry/opentelemetry-python/milestones) +for details on upcoming releases. The dates and features described in issues +and milestones are estimates, and subject to change. From 83e89d1f30c051800330047f6d3f383f1e153867 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 2 Apr 2020 10:15:22 -0700 Subject: [PATCH 0273/1517] sdk: Add last_updated_timestamp for metric observers (#522) Refactors last_updated_timestamp into aggregators instead of bound metric instrument. --- .../otcollector/metrics_exporter/__init__.py | 3 +- .../test_otcollector_metrics_exporter.py | 6 +- .../src/opentelemetry/sdk/metrics/__init__.py | 14 +-- .../sdk/metrics/export/aggregate.py | 34 ++++++- .../tests/metrics/export/test_export.py | 94 ++++++++++++++++++- .../tests/metrics/test_metrics.py | 10 +- 6 files changed, 128 insertions(+), 33 deletions(-) diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py index 5f8424fd00..d77dcd4324 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py @@ -147,10 +147,9 @@ def get_collector_metric_type(metric: Metric) -> metrics_pb2.MetricDescriptor: def get_collector_point(metric_record: MetricRecord) -> metrics_pb2.Point: # TODO: horrible hack to get original list of keys to then get the bound # instrument - key = dict(metric_record.labels) point = metrics_pb2.Point( timestamp=utils.proto_timestamp_from_time_ns( - metric_record.metric.bind(key).last_update_timestamp + metric_record.aggregator.last_update_timestamp ) ) if metric_record.metric.value_type == int: diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py index 6133b3bd88..7dcbf452e1 100644 --- a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_metrics_exporter.py @@ -178,13 +178,11 @@ def test_translate_to_collector(self): self.assertEqual(len(output_metrics[0].timeseries[0].points), 1) self.assertEqual( output_metrics[0].timeseries[0].points[0].timestamp.seconds, - record.metric.bind(self._labels).last_update_timestamp - // 1000000000, + record.aggregator.last_update_timestamp // 1000000000, ) self.assertEqual( output_metrics[0].timeseries[0].points[0].timestamp.nanos, - record.metric.bind(self._labels).last_update_timestamp - % 1000000000, + record.aggregator.last_update_timestamp % 1000000000, ) self.assertEqual( output_metrics[0].timeseries[0].points[0].int64_value, 123 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index e6f264494d..1d35648fd3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -21,7 +21,6 @@ from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo -from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -54,7 +53,6 @@ def __init__( self.value_type = value_type self.enabled = enabled self.aggregator = aggregator - self.last_update_timestamp = time_ns() self._ref_count = 0 self._ref_count_lock = threading.Lock() @@ -69,7 +67,6 @@ def _validate_update(self, value: metrics_api.ValueT) -> bool: return True def update(self, value: metrics_api.ValueT): - self.last_update_timestamp = time_ns() self.aggregator.update(value) def release(self): @@ -88,10 +85,8 @@ def ref_count(self): return self._ref_count def __repr__(self): - return '{}(data="{}", last_update_timestamp={})'.format( - type(self).__name__, - self.aggregator.current, - self.last_update_timestamp, + return '{}(data="{}")'.format( + type(self).__name__, self.aggregator.current ) @@ -331,7 +326,6 @@ def _collect_observers(self) -> None: if not observer.enabled: continue - # TODO: capture timestamp? if not observer.run(): continue @@ -404,9 +398,7 @@ def unregister_observer(self, observer: "Observer") -> None: class MeterProvider(metrics_api.MeterProvider): - def __init__( - self, resource: Resource = Resource.create_empty(), - ): + def __init__(self, resource: Resource = Resource.create_empty()): self.resource = resource def get_meter( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 20e5e5c209..ea8c40a7e7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -16,6 +16,8 @@ import threading from collections import namedtuple +from opentelemetry.util import time_ns + class Aggregator(abc.ABC): """Base class for aggregators. @@ -49,10 +51,12 @@ def __init__(self): self.current = 0 self.checkpoint = 0 self._lock = threading.Lock() + self.last_update_timestamp = None def update(self, value): with self._lock: self.current += value + self.last_update_timestamp = time_ns() def take_checkpoint(self): with self._lock: @@ -62,6 +66,9 @@ def take_checkpoint(self): def merge(self, other): with self._lock: self.checkpoint += other.checkpoint + self.last_update_timestamp = get_latest_timestamp( + self.last_update_timestamp, other.last_update_timestamp + ) class MinMaxSumCountAggregator(Aggregator): @@ -88,6 +95,7 @@ def __init__(self): self.current = self._EMPTY self.checkpoint = self._EMPTY self._lock = threading.Lock() + self.last_update_timestamp = None def update(self, value): with self._lock: @@ -100,6 +108,7 @@ def update(self, value): self.current.sum + value, self.current.count + 1, ) + self.last_update_timestamp = time_ns() def take_checkpoint(self): with self._lock: @@ -111,6 +120,9 @@ def merge(self, other): self.checkpoint = self._merge_checkpoint( self.checkpoint, other.checkpoint ) + self.last_update_timestamp = get_latest_timestamp( + self.last_update_timestamp, other.last_update_timestamp + ) class ObserverAggregator(Aggregator): @@ -123,10 +135,12 @@ def __init__(self): self.mmsc = MinMaxSumCountAggregator() self.current = None self.checkpoint = self._TYPE(None, None, None, 0, None) + self.last_update_timestamp = None def update(self, value): self.mmsc.update(value) self.current = value + self.last_update_timestamp = time_ns() def take_checkpoint(self): self.mmsc.take_checkpoint() @@ -134,9 +148,19 @@ def take_checkpoint(self): def merge(self, other): self.mmsc.merge(other.mmsc) - self.checkpoint = self._TYPE( - *( - self.mmsc.checkpoint - + (other.checkpoint.last or self.checkpoint.last,) - ) + last = self.checkpoint.last + self.last_update_timestamp = get_latest_timestamp( + self.last_update_timestamp, other.last_update_timestamp ) + if self.last_update_timestamp == other.last_update_timestamp: + last = other.checkpoint.last + self.checkpoint = self._TYPE(*(self.mmsc.checkpoint + (last,))) + + +def get_latest_timestamp(time_stamp, other_timestamp): + if time_stamp is None: + return other_timestamp + if other_timestamp is not None: + if time_stamp < other_timestamp: + return other_timestamp + return time_stamp diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 69cdcf6e16..3cf305eb7e 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -228,11 +228,14 @@ def call_update(counter): update_total += val return update_total - def test_update(self): + @mock.patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") + def test_update(self, time_mock): + time_mock.return_value = 123 counter = CounterAggregator() counter.update(1.0) counter.update(2.0) self.assertEqual(counter.current, 3.0) + self.assertEqual(counter.last_update_timestamp, 123) def test_checkpoint(self): counter = CounterAggregator() @@ -246,8 +249,10 @@ def test_merge(self): counter2 = CounterAggregator() counter.checkpoint = 1.0 counter2.checkpoint = 3.0 + counter2.last_update_timestamp = 123 counter.merge(counter2) self.assertEqual(counter.checkpoint, 4.0) + self.assertEqual(counter.last_update_timestamp, 123) def test_concurrent_update(self): counter = CounterAggregator() @@ -296,7 +301,9 @@ def call_update(mmsc): count_ += 1 return MinMaxSumCountAggregator._TYPE(min_, max_, sum_, count_) - def test_update(self): + @mock.patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") + def test_update(self, time_mock): + time_mock.return_value = 123 mmsc = MinMaxSumCountAggregator() # test current values without any update self.assertEqual(mmsc.current, MinMaxSumCountAggregator._EMPTY) @@ -309,6 +316,7 @@ def test_update(self): self.assertEqual( mmsc.current, (min(values), max(values), sum(values), len(values)) ) + self.assertEqual(mmsc.last_update_timestamp, 123) def test_checkpoint(self): mmsc = MinMaxSumCountAggregator() @@ -340,6 +348,9 @@ def test_merge(self): mmsc1.checkpoint = checkpoint1 mmsc2.checkpoint = checkpoint2 + mmsc1.last_update_timestamp = 100 + mmsc2.last_update_timestamp = 123 + mmsc1.merge(mmsc2) self.assertEqual( @@ -348,6 +359,7 @@ def test_merge(self): checkpoint1, checkpoint2 ), ) + self.assertEqual(mmsc1.last_update_timestamp, 123) def test_merge_checkpoint(self): func = MinMaxSumCountAggregator._merge_checkpoint @@ -421,7 +433,9 @@ def test_concurrent_update_and_checkpoint(self): class TestObserverAggregator(unittest.TestCase): - def test_update(self): + @mock.patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") + def test_update(self, time_mock): + time_mock.return_value = 123 observer = ObserverAggregator() # test current values without any update self.assertEqual(observer.mmsc.current, (None, None, None, 0)) @@ -436,6 +450,7 @@ def test_update(self): observer.mmsc.current, (min(values), max(values), sum(values), len(values)), ) + self.assertEqual(observer.last_update_timestamp, 123) self.assertEqual(observer.current, values[-1]) @@ -471,6 +486,77 @@ def test_merge(self): observer1.mmsc.checkpoint = mmsc_checkpoint1 observer2.mmsc.checkpoint = mmsc_checkpoint2 + observer1.last_update_timestamp = 100 + observer2.last_update_timestamp = 123 + + observer1.checkpoint = checkpoint1 + observer2.checkpoint = checkpoint2 + + observer1.merge(observer2) + + self.assertEqual( + observer1.checkpoint, + ( + min(checkpoint1.min, checkpoint2.min), + max(checkpoint1.max, checkpoint2.max), + checkpoint1.sum + checkpoint2.sum, + checkpoint1.count + checkpoint2.count, + checkpoint2.last, + ), + ) + self.assertEqual(observer1.last_update_timestamp, 123) + + def test_merge_last_updated(self): + observer1 = ObserverAggregator() + observer2 = ObserverAggregator() + + mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) + mmsc_checkpoint2 = MinMaxSumCountAggregator._TYPE(1, 33, 44, 2) + + checkpoint1 = ObserverAggregator._TYPE(*(mmsc_checkpoint1 + (23,))) + + checkpoint2 = ObserverAggregator._TYPE(*(mmsc_checkpoint2 + (27,))) + + observer1.mmsc.checkpoint = mmsc_checkpoint1 + observer2.mmsc.checkpoint = mmsc_checkpoint2 + + observer1.last_update_timestamp = 123 + observer2.last_update_timestamp = 100 + + observer1.checkpoint = checkpoint1 + observer2.checkpoint = checkpoint2 + + observer1.merge(observer2) + + self.assertEqual( + observer1.checkpoint, + ( + min(checkpoint1.min, checkpoint2.min), + max(checkpoint1.max, checkpoint2.max), + checkpoint1.sum + checkpoint2.sum, + checkpoint1.count + checkpoint2.count, + checkpoint1.last, + ), + ) + self.assertEqual(observer1.last_update_timestamp, 123) + + def test_merge_last_updated_none(self): + observer1 = ObserverAggregator() + observer2 = ObserverAggregator() + + mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) + mmsc_checkpoint2 = MinMaxSumCountAggregator._TYPE(1, 33, 44, 2) + + checkpoint1 = ObserverAggregator._TYPE(*(mmsc_checkpoint1 + (23,))) + + checkpoint2 = ObserverAggregator._TYPE(*(mmsc_checkpoint2 + (27,))) + + observer1.mmsc.checkpoint = mmsc_checkpoint1 + observer2.mmsc.checkpoint = mmsc_checkpoint2 + + observer1.last_update_timestamp = None + observer2.last_update_timestamp = 100 + observer1.checkpoint = checkpoint1 observer2.checkpoint = checkpoint2 @@ -486,6 +572,7 @@ def test_merge(self): checkpoint2.last, ), ) + self.assertEqual(observer1.last_update_timestamp, 100) def test_merge_with_empty(self): observer1 = ObserverAggregator() @@ -496,6 +583,7 @@ def test_merge_with_empty(self): observer1.mmsc.checkpoint = mmsc_checkpoint1 observer1.checkpoint = checkpoint1 + observer1.last_update_timestamp = 100 observer1.merge(observer2) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 1bbe18b178..3298064705 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -368,13 +368,10 @@ def test_add_incorrect_type(self, logger_mock): self.assertEqual(bound_counter.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) - @mock.patch("opentelemetry.sdk.metrics.time_ns") - def test_update(self, time_mock): + def test_update(self): aggregator = export.aggregate.CounterAggregator() bound_counter = metrics.BoundCounter(int, True, aggregator) - time_mock.return_value = 123 bound_counter.update(4.0) - self.assertEqual(bound_counter.last_update_timestamp, 123) self.assertEqual(bound_counter.aggregator.current, 4.0) @@ -403,11 +400,8 @@ def test_record_incorrect_type(self, logger_mock): ) self.assertTrue(logger_mock.warning.called) - @mock.patch("opentelemetry.sdk.metrics.time_ns") - def test_update(self, time_mock): + def test_update(self): aggregator = export.aggregate.MinMaxSumCountAggregator() bound_measure = metrics.BoundMeasure(int, True, aggregator) - time_mock.return_value = 123 bound_measure.update(4.0) - self.assertEqual(bound_measure.last_update_timestamp, 123) self.assertEqual(bound_measure.aggregator.current, (4.0, 4.0, 4.0, 1)) From 9a9db2b2cc022d2d225c8761f9a2358a968bd069 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 2 Apr 2020 15:16:06 -0700 Subject: [PATCH 0274/1517] Mark new packages as beta (#539) Update dev status classifiers to beta Add py3.8 classifier This change only affects the classifier for packages installed from source, the packages on PyPI will be updated with the next release. --- ext/opentelemetry-ext-grpc/setup.cfg | 3 ++- opentelemetry-auto-instrumentation/setup.cfg | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index df7074078b..50757d9bbd 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-e platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 diff --git a/opentelemetry-auto-instrumentation/setup.cfg b/opentelemetry-auto-instrumentation/setup.cfg index 3cd85e5bd1..3e4680fd50 100644 --- a/opentelemetry-auto-instrumentation/setup.cfg +++ b/opentelemetry-auto-instrumentation/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentel platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python @@ -32,6 +32,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] python_requires = >=3.4 From aea65301455570093dc9a1552d67f7d78d825946 Mon Sep 17 00:00:00 2001 From: Liz Fong-Jones Date: Thu, 2 Apr 2020 18:17:08 -0400 Subject: [PATCH 0275/1517] docs: fix typo (#543) --- .../src/opentelemetry/auto_instrumentation/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py index dfafb5386a..adfb4fd461 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py @@ -24,5 +24,5 @@ The code in ``program.py`` needs to use one of the packages for which there is an OpenTelemetry extension. For a list of the available extensions please check -`here `_. +`here `_. """ From 33a75d0050efe0f71c75cf48c9e591bf8862909f Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Sat, 4 Apr 2020 05:35:07 +0530 Subject: [PATCH 0276/1517] Fixed `debug` field usage in zipkin exporter (#549) Zipkin spec expects the debug field to be a boolean, not an integer. --- .../src/opentelemetry/ext/zipkin/__init__.py | 2 +- ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index e319e12149..edc9ee7031 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -180,7 +180,7 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): } if context.trace_flags.sampled: - zipkin_span["debug"] = 1 + zipkin_span["debug"] = True if isinstance(span.parent, Span): zipkin_span["parentId"] = format( diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py index b99398e066..578b533e0e 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -197,7 +197,7 @@ def test_export(self): "value": "event0", } ], - "debug": 1, + "debug": True, "parentId": format(parent_id, "x"), }, { From 331093e180bae5d7ec893dc8b3b08a86ecdbd7bc Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 6 Apr 2020 20:40:11 -0700 Subject: [PATCH 0277/1517] Remove unused logs dir (#558) --- .../src/opentelemetry/logs/__init__.py | 13 ------------- opentelemetry-api/src/opentelemetry/logs/py.typed | 0 2 files changed, 13 deletions(-) delete mode 100644 opentelemetry-api/src/opentelemetry/logs/__init__.py delete mode 100644 opentelemetry-api/src/opentelemetry/logs/py.typed diff --git a/opentelemetry-api/src/opentelemetry/logs/__init__.py b/opentelemetry-api/src/opentelemetry/logs/__init__.py deleted file mode 100644 index b0a6f42841..0000000000 --- a/opentelemetry-api/src/opentelemetry/logs/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/opentelemetry-api/src/opentelemetry/logs/py.typed b/opentelemetry-api/src/opentelemetry/logs/py.typed deleted file mode 100644 index e69de29bb2..0000000000 From 82a431046988392ca91ce0ddffbdf9a0d5d40c67 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 7 Apr 2020 09:51:41 -0600 Subject: [PATCH 0278/1517] Fix Flask ext dependencies (#546) Some Flask ext dependencies are missing or misplaced in the configuration file. Fixes #545 --- ext/opentelemetry-ext-flask/setup.cfg | 4 +++- tox.ini | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index 3e5f722ed9..ce3035ad2b 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -40,11 +40,13 @@ package_dir= =src packages=find_namespace: install_requires = + flask~=1.0 opentelemetry-ext-wsgi + opentelemetry-auto-instrumentation + opentelemetry-api [options.extras_require] test = - flask~=1.0 opentelemetry-ext-testutil [options.packages.find] diff --git a/tox.ini b/tox.ini index 06a54bc83d..bd7b762fe1 100644 --- a/tox.ini +++ b/tox.ini @@ -154,7 +154,6 @@ commands_pre = wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-testutil wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-wsgi wsgi,flask: pip install {toxinidir}/opentelemetry-sdk - flask: pip install {toxinidir}/opentelemetry-auto-instrumentation flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi From 4b8892c222c4685161248df04c2de71e57e7e6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 7 Apr 2020 13:29:49 -0500 Subject: [PATCH 0279/1517] Fix copyright headers (#553) --- .../src/opentelemetry/ext/flask/__init__.py | 14 ++++++++++++++ ext/opentelemetry-ext-flask/tests/conftest.py | 2 +- opentelemetry-auto-instrumentation/setup.cfg | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index 02b0de8652..d3a94558db 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -1,3 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # Note: This package is not named "flask" because of # https://github.com/PyCQA/pylint/issues/2648 diff --git a/ext/opentelemetry-ext-flask/tests/conftest.py b/ext/opentelemetry-ext-flask/tests/conftest.py index 22a587ab2e..25a5ee457e 100644 --- a/ext/opentelemetry-ext-flask/tests/conftest.py +++ b/ext/opentelemetry-ext-flask/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-auto-instrumentation/setup.cfg b/opentelemetry-auto-instrumentation/setup.cfg index 3e4680fd50..ffb0cba413 100644 --- a/opentelemetry-auto-instrumentation/setup.cfg +++ b/opentelemetry-auto-instrumentation/setup.cfg @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 2e5c1cbe4eee289131bd4e9ca8272d5517ffda10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 7 Apr 2020 14:03:17 -0500 Subject: [PATCH 0280/1517] docs/examples/http: Remove duplicated call to set_tracer_provider (#560) set_tracer_provider() was called twice using different TracerProvider objects. It caused that the "parent" span wasn't exported because it was created with a tracer from the first call to set_tracer_provider that didn't have a configured span processor. --- docs/examples/http/server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/examples/http/server.py b/docs/examples/http/server.py index 67476c25db..806bd0b88c 100755 --- a/docs/examples/http/server.py +++ b/docs/examples/http/server.py @@ -35,7 +35,6 @@ exporter = ConsoleSpanExporter() span_processor = BatchExportSpanProcessor(exporter) -trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor(span_processor) # Integrations are the glue that binds the OpenTelemetry API and the From f01b8eece97c3fb5667bedb26c6ad8d4e674e1d1 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Wed, 8 Apr 2020 13:27:03 -0700 Subject: [PATCH 0281/1517] Add tests and license to release tarballs (#557) - Add previously-excluded test packages to API and SDK package manifests - Make API and SDK packages use setup.cfg (like other packages in repo) - Add license and manifest files to all built packages - Move API version.py back into top level package, use pkg_resources to get version in other packages - Incidental cleanup and fixes --- ext/opentelemetry-ext-dbapi/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-dbapi/MANIFEST.in | 9 + ext/opentelemetry-ext-flask/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-flask/MANIFEST.in | 9 + ext/opentelemetry-ext-flask/tests/conftest.py | 1 + ext/opentelemetry-ext-grpc/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-grpc/MANIFEST.in | 9 + ext/opentelemetry-ext-http-requests/LICENSE | 201 ++++++++++++++++++ .../MANIFEST.in | 9 + ext/opentelemetry-ext-jaeger/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-jaeger/MANIFEST.in | 9 + ext/opentelemetry-ext-mysql/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-mysql/MANIFEST.in | 9 + .../LICENSE | 201 ++++++++++++++++++ .../MANIFEST.in | 9 + ext/opentelemetry-ext-otcollector/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-otcollector/MANIFEST.in | 9 + .../src/opentelemetry/ext/otcollector/util.py | 8 +- ext/opentelemetry-ext-prometheus/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-prometheus/MANIFEST.in | 9 + ext/opentelemetry-ext-psycopg2/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-psycopg2/MANIFEST.in | 9 + ext/opentelemetry-ext-pymongo/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-pymongo/MANIFEST.in | 9 + ext/opentelemetry-ext-wsgi/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-wsgi/MANIFEST.in | 9 + ext/opentelemetry-ext-zipkin/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-zipkin/MANIFEST.in | 9 + opentelemetry-api/LICENSE | 201 ++++++++++++++++++ opentelemetry-api/MANIFEST.in | 4 +- opentelemetry-api/setup.cfg | 59 +++++ opentelemetry-api/setup.py | 62 +----- .../src/opentelemetry/{util => }/version.py | 0 opentelemetry-sdk/LICENSE | 201 ++++++++++++++++++ opentelemetry-sdk/MANIFEST.in | 4 +- opentelemetry-sdk/setup.cfg | 54 +++++ opentelemetry-sdk/setup.py | 43 +--- 37 files changed, 3263 insertions(+), 104 deletions(-) create mode 100644 ext/opentelemetry-ext-dbapi/LICENSE create mode 100644 ext/opentelemetry-ext-dbapi/MANIFEST.in create mode 100644 ext/opentelemetry-ext-flask/LICENSE create mode 100644 ext/opentelemetry-ext-flask/MANIFEST.in create mode 100644 ext/opentelemetry-ext-grpc/LICENSE create mode 100644 ext/opentelemetry-ext-grpc/MANIFEST.in create mode 100644 ext/opentelemetry-ext-http-requests/LICENSE create mode 100644 ext/opentelemetry-ext-http-requests/MANIFEST.in create mode 100644 ext/opentelemetry-ext-jaeger/LICENSE create mode 100644 ext/opentelemetry-ext-jaeger/MANIFEST.in create mode 100644 ext/opentelemetry-ext-mysql/LICENSE create mode 100644 ext/opentelemetry-ext-mysql/MANIFEST.in create mode 100644 ext/opentelemetry-ext-opentracing-shim/LICENSE create mode 100644 ext/opentelemetry-ext-opentracing-shim/MANIFEST.in create mode 100644 ext/opentelemetry-ext-otcollector/LICENSE create mode 100644 ext/opentelemetry-ext-otcollector/MANIFEST.in create mode 100644 ext/opentelemetry-ext-prometheus/LICENSE create mode 100644 ext/opentelemetry-ext-prometheus/MANIFEST.in create mode 100644 ext/opentelemetry-ext-psycopg2/LICENSE create mode 100644 ext/opentelemetry-ext-psycopg2/MANIFEST.in create mode 100644 ext/opentelemetry-ext-pymongo/LICENSE create mode 100644 ext/opentelemetry-ext-pymongo/MANIFEST.in create mode 100644 ext/opentelemetry-ext-wsgi/LICENSE create mode 100644 ext/opentelemetry-ext-wsgi/MANIFEST.in create mode 100644 ext/opentelemetry-ext-zipkin/LICENSE create mode 100644 ext/opentelemetry-ext-zipkin/MANIFEST.in create mode 100644 opentelemetry-api/LICENSE create mode 100644 opentelemetry-api/setup.cfg rename opentelemetry-api/src/opentelemetry/{util => }/version.py (100%) create mode 100644 opentelemetry-sdk/LICENSE create mode 100644 opentelemetry-sdk/setup.cfg diff --git a/ext/opentelemetry-ext-dbapi/LICENSE b/ext/opentelemetry-ext-dbapi/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-dbapi/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-dbapi/MANIFEST.in b/ext/opentelemetry-ext-dbapi/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-dbapi/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-flask/LICENSE b/ext/opentelemetry-ext-flask/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-flask/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-flask/MANIFEST.in b/ext/opentelemetry-ext-flask/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-flask/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-flask/tests/conftest.py b/ext/opentelemetry-ext-flask/tests/conftest.py index 25a5ee457e..8c0754f2c6 100644 --- a/ext/opentelemetry-ext-flask/tests/conftest.py +++ b/ext/opentelemetry-ext-flask/tests/conftest.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + from opentelemetry.ext.flask import FlaskInstrumentor _FLASK_INSTRUMENTOR = FlaskInstrumentor() diff --git a/ext/opentelemetry-ext-grpc/LICENSE b/ext/opentelemetry-ext-grpc/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-grpc/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-grpc/MANIFEST.in b/ext/opentelemetry-ext-grpc/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-grpc/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-http-requests/LICENSE b/ext/opentelemetry-ext-http-requests/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-http-requests/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-http-requests/MANIFEST.in b/ext/opentelemetry-ext-http-requests/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-http-requests/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-jaeger/LICENSE b/ext/opentelemetry-ext-jaeger/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-jaeger/MANIFEST.in b/ext/opentelemetry-ext-jaeger/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-jaeger/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-mysql/LICENSE b/ext/opentelemetry-ext-mysql/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-mysql/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-mysql/MANIFEST.in b/ext/opentelemetry-ext-mysql/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-mysql/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-opentracing-shim/LICENSE b/ext/opentelemetry-ext-opentracing-shim/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-opentracing-shim/MANIFEST.in b/ext/opentelemetry-ext-opentracing-shim/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-otcollector/LICENSE b/ext/opentelemetry-ext-otcollector/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-otcollector/MANIFEST.in b/ext/opentelemetry-ext-otcollector/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py index 16c5af7e73..e8717b0c8b 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py @@ -16,6 +16,7 @@ import socket import time +import pkg_resources from google.protobuf.timestamp_pb2 import Timestamp from opencensus.proto.agent.common.v1 import common_pb2 from opencensus.proto.trace.v1 import trace_pb2 @@ -24,7 +25,10 @@ __version__ as otcollector_exporter_version, ) from opentelemetry.trace import SpanKind -from opentelemetry.util.version import __version__ as opentelemetry_version + +OPENTELEMETRY_VERSION = pkg_resources.get_distribution( + "opentelemetry-api" +).version def proto_timestamp_from_time_ns(time_ns): @@ -93,7 +97,7 @@ def get_node(service_name, host_name): library_info=common_pb2.LibraryInfo( language=common_pb2.LibraryInfo.Language.Value("PYTHON"), exporter_version=otcollector_exporter_version, - core_library_version=opentelemetry_version, + core_library_version=OPENTELEMETRY_VERSION, ), service_info=common_pb2.ServiceInfo(name=service_name), ) diff --git a/ext/opentelemetry-ext-prometheus/LICENSE b/ext/opentelemetry-ext-prometheus/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-prometheus/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-prometheus/MANIFEST.in b/ext/opentelemetry-ext-prometheus/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-prometheus/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-psycopg2/LICENSE b/ext/opentelemetry-ext-psycopg2/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-psycopg2/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-psycopg2/MANIFEST.in b/ext/opentelemetry-ext-psycopg2/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-psycopg2/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-pymongo/LICENSE b/ext/opentelemetry-ext-pymongo/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-pymongo/MANIFEST.in b/ext/opentelemetry-ext-pymongo/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-wsgi/LICENSE b/ext/opentelemetry-ext-wsgi/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-wsgi/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-wsgi/MANIFEST.in b/ext/opentelemetry-ext-wsgi/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-wsgi/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-zipkin/LICENSE b/ext/opentelemetry-ext-zipkin/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-zipkin/MANIFEST.in b/ext/opentelemetry-ext-zipkin/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/opentelemetry-api/LICENSE b/opentelemetry-api/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/opentelemetry-api/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/opentelemetry-api/MANIFEST.in b/opentelemetry-api/MANIFEST.in index 191b7d1959..aed3e33273 100644 --- a/opentelemetry-api/MANIFEST.in +++ b/opentelemetry-api/MANIFEST.in @@ -1,7 +1,9 @@ -prune tests graft src +graft tests global-exclude *.pyc global-exclude *.pyo global-exclude __pycache__/* +include CHANGELOG.md include MANIFEST.in include README.rst +include LICENSE diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg new file mode 100644 index 0000000000..d6d68a710d --- /dev/null +++ b/opentelemetry-api/setup.cfg @@ -0,0 +1,59 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-api +description = OpenTelemetry Python API +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-api +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +zip_safe = False +include_package_data = True +install_requires = + typing; python_version<'3.5' + aiocontextvars; python_version<'3.7' and python_version>='3.5' + +[options.packages.find] +where = src +include = opentelemetry.* + +[options.entry_points] +opentelemetry_context = + contextvars_context = opentelemetry.context.contextvars_context:ContextVarsRuntimeContext + threadlocal_context = opentelemetry.context.threadlocal_context:ThreadLocalRuntimeContext +opentelemetry_meter_provider = + default_meter_provider = opentelemetry.metrics:DefaultMeterProvider +opentelemetry_tracer_provider = + default_tracer_provider = opentelemetry.trace:DefaultTracerProvider diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index 61ad9ab320..75d213ae4e 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -12,68 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from os.path import dirname, join +import os import setuptools -BASE_DIR = dirname(__file__) -VERSION_FILENAME = join(BASE_DIR, "src", "opentelemetry", "util", "version.py") +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join(BASE_DIR, "src", "opentelemetry", "version.py") PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: exec(f.read(), PACKAGE_INFO) -setuptools.setup( - name="opentelemetry-api", - version=PACKAGE_INFO["__version__"], - author="OpenTelemetry Authors", - author_email="cncf-opentelemetry-contributors@lists.cncf.io", - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - ], - description="OpenTelemetry Python API", - include_package_data=True, - long_description=open("README.rst").read(), - long_description_content_type="text/x-rst", - install_requires=[ - "typing; python_version<'3.5'", - "aiocontextvars; python_version<'3.7' and python_version>='3.5'", - ], - extras_require={}, - license="Apache-2.0", - package_dir={"": "src"}, - packages=setuptools.find_namespace_packages( - where="src", include="opentelemetry.*" - ), - url=( - "https://github.com/open-telemetry/opentelemetry-python" - "/tree/master/opentelemetry-api" - ), - zip_safe=False, - entry_points={ - "opentelemetry_context": [ - "contextvars_context = " - "opentelemetry.context.contextvars_context:" - "ContextVarsRuntimeContext", - "threadlocal_context = " - "opentelemetry.context.threadlocal_context:" - "ThreadLocalRuntimeContext", - ], - "opentelemetry_meter_provider": [ - "default_meter_provider = " - "opentelemetry.metrics:DefaultMeterProvider" - ], - "opentelemetry_tracer_provider": [ - "default_tracer_provider = " - "opentelemetry.trace:DefaultTracerProvider" - ], - }, -) +setuptools.setup(version=PACKAGE_INFO["__version__"],) diff --git a/opentelemetry-api/src/opentelemetry/util/version.py b/opentelemetry-api/src/opentelemetry/version.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/util/version.py rename to opentelemetry-api/src/opentelemetry/version.py diff --git a/opentelemetry-sdk/LICENSE b/opentelemetry-sdk/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/opentelemetry-sdk/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/opentelemetry-sdk/MANIFEST.in b/opentelemetry-sdk/MANIFEST.in index 191b7d1959..b3bfe49c7d 100644 --- a/opentelemetry-sdk/MANIFEST.in +++ b/opentelemetry-sdk/MANIFEST.in @@ -1,7 +1,9 @@ -prune tests graft src +graft tests global-exclude *.pyc global-exclude *.pyo global-exclude __pycache__/* +include CHANGELOG.md +include LICENSE include MANIFEST.in include README.rst diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg new file mode 100644 index 0000000000..da0f282ea6 --- /dev/null +++ b/opentelemetry-sdk/setup.cfg @@ -0,0 +1,54 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-sdk +description = OpenTelemetry Python SDK +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-sdk +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +zip_safe = False +include_package_data = True +install_requires = opentelemetry-api==0.7.dev0 + +[options.packages.find] +where = src +include = opentelemetry.sdk.* + +[options.entry_points] +opentelemetry_meter_provider = + sdk_meter_provider = opentelemetry.sdk.metrics:MeterProvider +opentelemetry_tracer_provider = + sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index 26c7e348d5..3ba02ba245 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -24,45 +24,4 @@ with open(VERSION_FILENAME) as f: exec(f.read(), PACKAGE_INFO) -setuptools.setup( - name="opentelemetry-sdk", - version=PACKAGE_INFO["__version__"], - author="OpenTelemetry Authors", - author_email="cncf-opentelemetry-contributors@lists.cncf.io", - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - ], - description="OpenTelemetry Python SDK", - include_package_data=True, - long_description=open("README.rst").read(), - long_description_content_type="text/x-rst", - install_requires=["opentelemetry-api==0.7.dev0"], - extras_require={}, - license="Apache-2.0", - package_dir={"": "src"}, - packages=setuptools.find_namespace_packages( - where="src", include="opentelemetry.sdk.*" - ), - url=( - "https://github.com/open-telemetry/opentelemetry-python" - "/tree/master/opentelemetry-sdk" - ), - zip_safe=False, - entry_points={ - "opentelemetry_meter_provider": [ - "sdk_meter_provider = opentelemetry.sdk.metrics:MeterProvider" - ], - "opentelemetry_tracer_provider": [ - "sdk_tracer_provider = opentelemetry.sdk.trace:TracerProvider" - ], - }, -) +setuptools.setup(version=PACKAGE_INFO["__version__"],) From b3e4f3c4826ad93add9a90e9d5f39af8ffe30ffb Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Wed, 8 Apr 2020 13:49:32 -0700 Subject: [PATCH 0282/1517] Require latest version of other packages in repo (#561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change unversioned requirements for packages in the same repo to ==0.7.dev0. Co-authored-by: Mauricio Vásquez --- eachdist.ini | 1 + ext/opentelemetry-ext-dbapi/setup.cfg | 2 +- ext/opentelemetry-ext-flask/setup.cfg | 10 +++++----- ext/opentelemetry-ext-grpc/setup.cfg | 4 ++-- ext/opentelemetry-ext-http-requests/setup.cfg | 2 +- ext/opentelemetry-ext-jaeger/setup.cfg | 4 ++-- ext/opentelemetry-ext-mysql/setup.cfg | 2 +- ext/opentelemetry-ext-opentracing-shim/setup.cfg | 4 ++-- ext/opentelemetry-ext-otcollector/setup.cfg | 4 ++-- ext/opentelemetry-ext-prometheus/setup.cfg | 4 ++-- ext/opentelemetry-ext-psycopg2/setup.cfg | 4 ++-- ext/opentelemetry-ext-pymongo/setup.cfg | 2 +- ext/opentelemetry-ext-wsgi/setup.cfg | 4 ++-- ext/opentelemetry-ext-zipkin/setup.cfg | 6 +++--- opentelemetry-auto-instrumentation/setup.cfg | 2 +- tox.ini | 2 ++ 16 files changed, 30 insertions(+), 27 deletions(-) diff --git a/eachdist.ini b/eachdist.ini index 8baab0c2e1..59f212f7c7 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -4,6 +4,7 @@ sortfirst= opentelemetry-api opentelemetry-sdk + opentelemetry-auto-instrumentation ext/opentelemetry-ext-wsgi ext/* diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 7a474632a2..6255184fe7 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.7.dev0 + opentelemetry-api == 0.7.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index ce3035ad2b..ec7ba987c3 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -40,14 +40,14 @@ package_dir= =src packages=find_namespace: install_requires = - flask~=1.0 - opentelemetry-ext-wsgi - opentelemetry-auto-instrumentation - opentelemetry-api + flask ~= 1.0 + opentelemetry-ext-wsgi == 0.7.dev0 + opentelemetry-auto-instrumentation == 0.7.dev0 + opentelemetry-api == 0.7.dev0 [options.extras_require] test = - opentelemetry-ext-testutil + opentelemetry-ext-testutil == 0.7.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index 50757d9bbd..bb2847fc12 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -40,8 +40,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api - grpcio~=1.27 + opentelemetry-api == 0.7.dev0 + grpcio ~= 1.27 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg index 9e7cee9ea0..886bea11fa 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.7.dev0 + opentelemetry-api == 0.7.dev0 requests ~= 2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/ext/opentelemetry-ext-jaeger/setup.cfg index 941c20d537..f8261d747b 100644 --- a/ext/opentelemetry-ext-jaeger/setup.cfg +++ b/ext/opentelemetry-ext-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = thrift >= 0.10.0 - opentelemetry-api - opentelemetry-sdk + opentelemetry-api == 0.7.dev0 + opentelemetry-sdk == 0.7.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index fe27b9f920..451f0a4587 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.7.dev0 + opentelemetry-api == 0.7.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg index 7b2278c78e..d36dbe804c 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = Deprecated >= 1.2.6 - opentracing - opentelemetry-api + opentracing ~= 2.0 + opentelemetry-api == 0.7.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-otcollector/setup.cfg b/ext/opentelemetry-ext-otcollector/setup.cfg index aab6af03e5..ca6d28c491 100644 --- a/ext/opentelemetry-ext-otcollector/setup.cfg +++ b/ext/opentelemetry-ext-otcollector/setup.cfg @@ -42,8 +42,8 @@ packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 opencensus-proto >= 0.1.0, < 1.0.0 - opentelemetry-api >= 0.7.dev0 - opentelemetry-sdk >= 0.7.dev0 + opentelemetry-api == 0.7.dev0 + opentelemetry-sdk == 0.7.dev0 protobuf >= 3.8.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/ext/opentelemetry-ext-prometheus/setup.cfg index ed0b5e43c1..d86649b5d3 100644 --- a/ext/opentelemetry-ext-prometheus/setup.cfg +++ b/ext/opentelemetry-ext-prometheus/setup.cfg @@ -41,8 +41,8 @@ package_dir= packages=find_namespace: install_requires = prometheus_client >= 0.5.0, < 1.0.0 - opentelemetry-api - opentelemetry-sdk + opentelemetry-api == 0.7.dev0 + opentelemetry-sdk == 0.7.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index 8fbcfe539a..5a5d3a7e40 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -40,9 +40,9 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.7.dev0 + opentelemetry-api == 0.7.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] -where = src \ No newline at end of file +where = src diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index be6f29488b..1506237a5c 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -40,7 +40,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.7.dev0 + opentelemetry-api == 0.7.dev0 pymongo ~= 3.1 [options.packages.find] diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index 885e9c2dfb..038dac3496 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -40,11 +40,11 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api + opentelemetry-api == 0.7.dev0 [options.extras_require] test = - opentelemetry-ext-testutil + opentelemetry-ext-testutil == 0.7.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg index 66326cbd58..bedb5e7f73 100644 --- a/ext/opentelemetry-ext-zipkin/setup.cfg +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -40,9 +40,9 @@ package_dir= =src packages=find_namespace: install_requires = - requests~=2.7 - opentelemetry-api - opentelemetry-sdk + requests ~= 2.7 + opentelemetry-api == 0.7.dev0 + opentelemetry-sdk == 0.7.dev0 [options.packages.find] where = src diff --git a/opentelemetry-auto-instrumentation/setup.cfg b/opentelemetry-auto-instrumentation/setup.cfg index ffb0cba413..7d7ccf0cfe 100644 --- a/opentelemetry-auto-instrumentation/setup.cfg +++ b/opentelemetry-auto-instrumentation/setup.cfg @@ -41,7 +41,7 @@ package_dir= packages=find_namespace: zip_safe = False include_package_data = True -install_requires = opentelemetry-api==0.7.dev0 +install_requires = opentelemetry-api == 0.7.dev0 [options.packages.find] where = src diff --git a/tox.ini b/tox.ini index bd7b762fe1..7d7bed9382 100644 --- a/tox.ini +++ b/tox.ini @@ -154,6 +154,7 @@ commands_pre = wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-testutil wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-wsgi wsgi,flask: pip install {toxinidir}/opentelemetry-sdk + flask: pip install {toxinidir}/opentelemetry-auto-instrumentation flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi @@ -248,6 +249,7 @@ commands_pre = -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/ext/opentelemetry-ext-http-requests \ -e {toxinidir}/ext/opentelemetry-ext-wsgi \ + -e {toxinidir}/opentelemetry-auto-instrumentation \ -e {toxinidir}/ext/opentelemetry-ext-flask commands = From 91cdfe7cfcc5c758b28e132e2f9ef5a7a0ae7b24 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 8 Apr 2020 15:28:27 -0600 Subject: [PATCH 0283/1517] Fix example in documentation (#551) Fixes #550 --- docs/getting-started.rst | 5 +++-- .../src/opentelemetry/ext/flask/__init__.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/getting-started.rst b/docs/getting-started.rst index bbd696015f..47c19e32fc 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -184,12 +184,14 @@ And let's write a small Flask application that sends an HTTP request, activating .. code-block:: python # flask_example.py + from opentelemetry.ext.flask import FlaskInstrumentor + FlaskInstrumentor().instrument() # This needs to be executed before importing Flask + import flask import requests import opentelemetry.ext.http_requests from opentelemetry import trace - from opentelemetry.ext.flask import instrument_app from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor @@ -201,7 +203,6 @@ And let's write a small Flask application that sends an HTTP request, activating app = flask.Flask(__name__) opentelemetry.ext.http_requests.enable(trace.get_tracer_provider()) - instrument_app(app) @app.route("/") def hello(): diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index d3a94558db..9b21696c59 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -29,11 +29,11 @@ .. code-block:: python + from opentelemetry.ext.flask import FlaskInstrumentor + FlaskInstrumentor().instrument() # This needs to be executed before importing Flask from flask import Flask - from opentelemetry.ext.flask import instrument_app app = Flask(__name__) - instrument_app(app) # This is where the magic happens. ✨ @app.route("/") def hello(): From cbe1cba28e3f10f59676892dd5c9ad96f06efdf6 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 9 Apr 2020 08:54:09 -0700 Subject: [PATCH 0284/1517] Add opentelemetry-auto-instrumentation to build.sh (#556) --- scripts/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index cd857d048a..682276561b 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in opentelemetry-api/ opentelemetry-sdk/ ext/*/ ; do + for d in opentelemetry-api/ opentelemetry-sdk/ opentelemetry-auto-instrumentation/ ext/*/ ; do ( echo "building $d" cd "$d" From 08b100f471d161d56507c78acdb3726683ff4615 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 9 Apr 2020 16:21:20 -0700 Subject: [PATCH 0285/1517] Adding functional tests for psycopg2 integration (#528) End to end verification for span creation using psycopg2 and dbapi integrations --- .../tests/check_availability.py | 79 ++++++++++++ .../tests/docker-compose.yml | 12 +- .../tests/postgres/test_psycopg_functional.py | 115 ++++++++++++++++++ tox.ini | 6 +- 4 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 ext/opentelemetry-ext-docker-tests/tests/check_availability.py create mode 100644 ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/check_availability.py b/ext/opentelemetry-ext-docker-tests/tests/check_availability.py new file mode 100644 index 0000000000..b0f1e55b20 --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/check_availability.py @@ -0,0 +1,79 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import time +import traceback + +import psycopg2 +import pymongo + +MONGODB_COLLECTION_NAME = "test" +MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME", "opentelemetry-tests") +MONGODB_HOST = os.getenv("MONGODB_HOST", "localhost") +MONGODB_PORT = int(os.getenv("MONGODB_PORT", "27017")) +POSTGRES_DB_NAME = os.getenv("POSTGRESQL_DB_NAME", "opentelemetry-tests") +POSTGRES_HOST = os.getenv("POSTGRESQL_HOST", "localhost") +POSTGRES_PASSWORD = os.getenv("POSTGRESQL_HOST", "testpassword") +POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT", "5432")) +POSTGRES_USER = os.getenv("POSTGRESQL_HOST", "testuser") +RETRY_COUNT = 5 +RETRY_INTERVAL = 5 # Seconds + + +def check_pymongo_connection(): + # Try to connect to DB + for i in range(RETRY_COUNT): + try: + client = pymongo.MongoClient( + MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000 + ) + db = client[MONGODB_DB_NAME] + collection = db[MONGODB_COLLECTION_NAME] + collection.find_one() + client.close() + break + except Exception as ex: + if i == RETRY_COUNT - 1: + raise (ex) + traceback.print_exc() + time.sleep(RETRY_INTERVAL) + + +def check_postgres_connection(): + # Try to connect to DB + for i in range(RETRY_COUNT): + try: + connection = psycopg2.connect( + dbname=POSTGRES_DB_NAME, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD, + host=POSTGRES_HOST, + port=POSTGRES_PORT, + ) + connection.close() + break + except Exception as ex: + if i == RETRY_COUNT - 1: + raise (ex) + traceback.print_exc() + time.sleep(RETRY_INTERVAL) + + +def check_docker_services_availability(): + # Check if Docker services accept connections + check_pymongo_connection() + check_postgres_connection() + + +check_docker_services_availability() diff --git a/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml b/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml index 1dab842f89..0abccc213d 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml +++ b/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml @@ -4,4 +4,14 @@ services: otmongo: ports: - "27017:27017" - image: mongo:latest \ No newline at end of file + image: mongo:latest + + otpostgres: + image: postgres + ports: + - "5432:5432" + environment: + POSTGRES_USER: testuser + POSTGRES_PASSWORD: testpassword + POSTGRES_DB: opentelemetry-tests + diff --git a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py new file mode 100644 index 0000000000..d58e332c1b --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py @@ -0,0 +1,115 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import time +import unittest + +import psycopg2 + +from opentelemetry import trace as trace_api +from opentelemetry.ext.psycopg2 import trace_integration +from opentelemetry.sdk.trace import Tracer, TracerProvider +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +POSTGRES_HOST = os.getenv("POSTGRESQL_HOST ", "localhost") +POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT ", "5432")) +POSTGRES_DB_NAME = os.getenv("POSTGRESQL_DB_NAME ", "opentelemetry-tests") +POSTGRES_PASSWORD = os.getenv("POSTGRESQL_HOST ", "testpassword") +POSTGRES_USER = os.getenv("POSTGRESQL_HOST ", "testuser") + + +class TestFunctionalPsycopg(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls._connection = None + cls._cursor = None + cls._tracer_provider = TracerProvider() + cls._tracer = Tracer(cls._tracer_provider, None) + cls._span_exporter = InMemorySpanExporter() + cls._span_processor = SimpleExportSpanProcessor(cls._span_exporter) + cls._tracer_provider.add_span_processor(cls._span_processor) + trace_integration(cls._tracer) + cls._connection = psycopg2.connect( + dbname=POSTGRES_DB_NAME, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD, + host=POSTGRES_HOST, + port=POSTGRES_PORT, + ) + cls._connection.set_session(autocommit=True) + cls._cursor = cls._connection.cursor() + + @classmethod + def tearDownClass(cls): + if cls._cursor: + cls._cursor.close() + if cls._connection: + cls._connection.close() + + def setUp(self): + self._span_exporter.clear() + + def validate_spans(self): + spans = self._span_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + for span in spans: + if span.name == "rootSpan": + root_span = span + else: + child_span = span + self.assertIsInstance(span.start_time, int) + self.assertIsInstance(span.end_time, int) + self.assertIsNotNone(root_span) + self.assertIsNotNone(child_span) + self.assertEqual(root_span.name, "rootSpan") + self.assertEqual(child_span.name, "postgresql.opentelemetry-tests") + self.assertIsNotNone(child_span.parent) + self.assertEqual(child_span.parent.name, root_span.name) + self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual( + child_span.attributes["db.instance"], POSTGRES_DB_NAME + ) + self.assertEqual(child_span.attributes["net.peer.name"], POSTGRES_HOST) + self.assertEqual(child_span.attributes["net.peer.port"], POSTGRES_PORT) + + def test_execute(self): + """Should create a child span for execute method + """ + with self._tracer.start_as_current_span("rootSpan"): + self._cursor.execute( + "CREATE TABLE IF NOT EXISTS test (id integer)" + ) + self.validate_spans() + + def test_executemany(self): + """Should create a child span for executemany + """ + with self._tracer.start_as_current_span("rootSpan"): + data = ("1", "2", "3") + stmt = "INSERT INTO test (id) VALUES (%s)" + self._cursor.executemany(stmt, data) + self.validate_spans() + + def test_callproc(self): + """Should create a child span for callproc + """ + with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( + Exception + ): + self._cursor.callproc("test", ()) + self.validate_spans() diff --git a/tox.ini b/tox.ini index 7d7bed9382..f3d1af4edc 100644 --- a/tox.ini +++ b/tox.ini @@ -260,6 +260,7 @@ deps = pytest docker-compose >= 1.25.2 pymongo ~= 3.1 + psycopg2 ~= 2.8.4 changedir = ext/opentelemetry-ext-docker-tests/tests @@ -267,8 +268,11 @@ changedir = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-sdk \ + -e {toxinidir}/ext/opentelemetry-ext-dbapi \ + -e {toxinidir}/ext/opentelemetry-ext-psycopg2 \ -e {toxinidir}/ext/opentelemetry-ext-pymongo - - docker-compose up -d + docker-compose up -d + python check_availability.py commands = pytest {posargs} From 7c2cebac9658c59c298bd5960af3699b85789289 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 13 Apr 2020 09:35:51 -0600 Subject: [PATCH 0286/1517] docs: Remove unnecessary Flask app (#570) The current auto instrumentation example includes an unnecessary Flask app in its client. --- docs/examples/auto-instrumentation/client.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/examples/auto-instrumentation/client.py b/docs/examples/auto-instrumentation/client.py index c8301003be..8cef055b35 100644 --- a/docs/examples/auto-instrumentation/client.py +++ b/docs/examples/auto-instrumentation/client.py @@ -14,7 +14,6 @@ from sys import argv -from flask import Flask from requests import get from opentelemetry import propagators, trace @@ -24,8 +23,6 @@ SimpleExportSpanProcessor, ) -app = Flask(__name__) - trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer_provider().get_tracer(__name__) From b44bf4177354be56ffdd962c8bd267a76c1586fc Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Mon, 13 Apr 2020 09:01:01 -0700 Subject: [PATCH 0287/1517] test: Adding functional tests for MySQL integration (#526) E2E verification for span creation using mysql and dbapi integrations --- .../tests/check_availability.py | 37 +++++- .../tests/docker-compose.yml | 11 +- .../tests/mysql/test_mysql_functional.py | 108 ++++++++++++++++++ tox.ini | 2 + 4 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py diff --git a/ext/opentelemetry-ext-docker-tests/tests/check_availability.py b/ext/opentelemetry-ext-docker-tests/tests/check_availability.py index b0f1e55b20..0b4376030d 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/check_availability.py +++ b/ext/opentelemetry-ext-docker-tests/tests/check_availability.py @@ -1,4 +1,4 @@ -# Copyright 2020, OpenTelemetry Authors +# Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,10 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import logging import os import time -import traceback +import mysql.connector import psycopg2 import pymongo @@ -22,6 +23,11 @@ MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME", "opentelemetry-tests") MONGODB_HOST = os.getenv("MONGODB_HOST", "localhost") MONGODB_PORT = int(os.getenv("MONGODB_PORT", "27017")) +MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME ", "opentelemetry-tests") +MYSQL_HOST = os.getenv("MYSQL_HOST ", "localhost") +MYSQL_PORT = int(os.getenv("MYSQL_PORT ", "3306")) +MYSQL_USER = os.getenv("MYSQL_USER ", "testuser") +MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD ", "testpassword") POSTGRES_DB_NAME = os.getenv("POSTGRESQL_DB_NAME", "opentelemetry-tests") POSTGRES_HOST = os.getenv("POSTGRESQL_HOST", "localhost") POSTGRES_PASSWORD = os.getenv("POSTGRESQL_HOST", "testpassword") @@ -30,6 +36,8 @@ RETRY_COUNT = 5 RETRY_INTERVAL = 5 # Seconds +logger = logging.getLogger(__name__) + def check_pymongo_connection(): # Try to connect to DB @@ -46,7 +54,27 @@ def check_pymongo_connection(): except Exception as ex: if i == RETRY_COUNT - 1: raise (ex) - traceback.print_exc() + logger.exception(ex) + time.sleep(RETRY_INTERVAL) + + +def check_mysql_connection(): + # Try to connect to DB + for i in range(RETRY_COUNT): + try: + connection = mysql.connector.connect( + user=MYSQL_USER, + password=MYSQL_PASSWORD, + host=MYSQL_HOST, + port=MYSQL_PORT, + database=MYSQL_DB_NAME, + ) + connection.close() + break + except Exception as ex: + if i == RETRY_COUNT - 1: + raise (ex) + logger.exception(ex) time.sleep(RETRY_INTERVAL) @@ -66,13 +94,14 @@ def check_postgres_connection(): except Exception as ex: if i == RETRY_COUNT - 1: raise (ex) - traceback.print_exc() + logger.exception(ex) time.sleep(RETRY_INTERVAL) def check_docker_services_availability(): # Check if Docker services accept connections check_pymongo_connection() + check_mysql_connection() check_postgres_connection() diff --git a/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml b/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml index 0abccc213d..c2632dc247 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml +++ b/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml @@ -5,7 +5,16 @@ services: ports: - "27017:27017" image: mongo:latest - + otmysql: + ports: + - "3306:3306" + image: mysql:latest + restart: always + environment: + MYSQL_USER: testuser + MYSQL_PASSWORD: testpassword + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + MYSQL_DATABASE: opentelemetry-tests otpostgres: image: postgres ports: diff --git a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py new file mode 100644 index 0000000000..305bea00bf --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py @@ -0,0 +1,108 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import time +import unittest + +import mysql.connector + +from opentelemetry import trace as trace_api +from opentelemetry.ext.mysql import trace_integration +from opentelemetry.sdk.trace import Tracer, TracerProvider +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +MYSQL_USER = os.getenv("MYSQL_USER ", "testuser") +MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD ", "testpassword") +MYSQL_HOST = os.getenv("MYSQL_HOST ", "localhost") +MYSQL_PORT = int(os.getenv("MYSQL_PORT ", "3306")) +MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME ", "opentelemetry-tests") + + +class TestFunctionalMysql(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls._connection = None + cls._cursor = None + cls._tracer_provider = TracerProvider() + cls._tracer = Tracer(cls._tracer_provider, None) + cls._span_exporter = InMemorySpanExporter() + cls._span_processor = SimpleExportSpanProcessor(cls._span_exporter) + cls._tracer_provider.add_span_processor(cls._span_processor) + trace_integration(cls._tracer) + cls._connection = mysql.connector.connect( + user=MYSQL_USER, + password=MYSQL_PASSWORD, + host=MYSQL_HOST, + port=MYSQL_PORT, + database=MYSQL_DB_NAME, + ) + cls._cursor = cls._connection.cursor() + + @classmethod + def tearDownClass(cls): + if cls._connection: + cls._connection.close() + + def setUp(self): + self._span_exporter.clear() + + def validate_spans(self): + spans = self._span_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + for span in spans: + if span.name == "rootSpan": + root_span = span + else: + db_span = span + self.assertIsInstance(span.start_time, int) + self.assertIsInstance(span.end_time, int) + self.assertIsNotNone(root_span) + self.assertIsNotNone(db_span) + self.assertEqual(root_span.name, "rootSpan") + self.assertEqual(db_span.name, "mysql.opentelemetry-tests") + self.assertIsNotNone(db_span.parent) + self.assertEqual(db_span.parent.name, root_span.name) + self.assertIs(db_span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual(db_span.attributes["db.instance"], MYSQL_DB_NAME) + self.assertEqual(db_span.attributes["net.peer.name"], MYSQL_HOST) + self.assertEqual(db_span.attributes["net.peer.port"], MYSQL_PORT) + + def test_execute(self): + """Should create a child span for execute + """ + with self._tracer.start_as_current_span("rootSpan"): + self._cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") + self.validate_spans() + + def test_executemany(self): + """Should create a child span for executemany + """ + with self._tracer.start_as_current_span("rootSpan"): + data = ["1", "2", "3"] + stmt = "INSERT INTO test (id) VALUES (%s)" + self._cursor.executemany(stmt, data) + self.validate_spans() + + def test_callproc(self): + """Should create a child span for callproc + """ + with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( + Exception + ): + self._cursor.callproc("test", ()) + self.validate_spans() diff --git a/tox.ini b/tox.ini index f3d1af4edc..ac308148db 100644 --- a/tox.ini +++ b/tox.ini @@ -259,6 +259,7 @@ commands = deps = pytest docker-compose >= 1.25.2 + mysql-connector-python ~= 8.0 pymongo ~= 3.1 psycopg2 ~= 2.8.4 @@ -269,6 +270,7 @@ commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/ext/opentelemetry-ext-dbapi \ + -e {toxinidir}/ext/opentelemetry-ext-mysql \ -e {toxinidir}/ext/opentelemetry-ext-psycopg2 \ -e {toxinidir}/ext/opentelemetry-ext-pymongo docker-compose up -d From c8b336d338289a3fed761053e917398f3bf6fdd7 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 13 Apr 2020 10:28:02 -0600 Subject: [PATCH 0288/1517] api: Make the configuration object universal (#563) The configuration object used to provide only configuration for the meter and tracer providers. Now it can be used to load any configuration value stored in an environment variable that starts with OPENTELEMETRY_PYTHON_ whose characters match with [A-Z_]. All this is explained with greater detail in the documentation. The documentation also includes a section that gathers and explains all the current environment variables that are meaningful for OpenTelemetry Python. In this way, the end user can have them all listed in one single place. If in the future, more environment variables are used, then they should be added there and documented accordingly. --- .../opentelemetry/configuration/__init__.py | 131 ++++++++---------- .../src/opentelemetry/metrics/__init__.py | 8 +- .../src/opentelemetry/trace/__init__.py | 9 +- .../src/opentelemetry/util/__init__.py | 25 ++++ .../tests/configuration/test_configuration.py | 89 +++--------- 5 files changed, 110 insertions(+), 152 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index 850f0d4fe0..9b23c52bee 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -18,12 +18,42 @@ """ Simple configuration manager -This is a configuration manager for the Tracer and Meter providers. It reads -configuration from environment variables prefixed with -``OPENTELEMETRY_PYTHON_``: +This is a configuration manager for OpenTelemetry. It reads configuration +values from environment variables prefixed with +``OPENTELEMETRY_PYTHON_`` whose characters are only all caps and underscores. +The first character after ``OPENTELEMETRY_PYTHON_`` must be an uppercase +character. -1. ``OPENTELEMETRY_PYTHON_TRACER_PROVIDER`` -2. ``OPENTELEMETRY_PYTHON_METER_PROVIDER`` +For example, these environment variables will be read: + +1. ``OPENTELEMETRY_PYTHON_SOMETHING`` +2. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_`` +3. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND__ELSE`` + +These won't: + +1. ``OPENTELEMETRY_PYTH_SOMETHING`` +2. ``OPENTELEMETRY_PYTHON_something`` +3. ``OPENTELEMETRY_PYTHON_SOMETHING_2_AND__ELSE`` +4. ``OPENTELEMETRY_PYTHON_SOMETHING_%_ELSE`` + +The values stored in the environment variables can be found in an instance of +``opentelemetry.configuration.Configuration``. This class can be instantiated +freely because instantiating it returns a singleton. + +For example, if the environment variable +``OPENTELEMETRY_PYTHON_METER_PROVIDER`` value is ``my_meter_provider``, then +``Configuration().meter_provider == "my_meter_provider"`` would be ``True``. + +Non defined attributes will always return ``None``. This is intended to make it +easier to use the ``Configuration`` object in actual code, because it won't be +necessary to check for the attribute to be defined first. + +Environment variables used by OpenTelemetry +------------------------------------------- + +1. OPENTELEMETRY_PYTHON_METER_PROVIDER +2. OPENTELEMETRY_PYTHON_TRACER_PROVIDER The value of these environment variables should be the name of the entry point that points to the class that implements either provider. This OpenTelemetry @@ -47,85 +77,46 @@ "default_meter_provider" (this is not actually necessary since the OpenTelemetry API provided providers are the default ones used if no configuration is found in the environment variables). - -Once this is done, the configuration manager can be used by simply importing -it from opentelemetry.configuration.Configuration. This is a class that can -be instantiated as many times as needed without concern because it will -always produce the same instance. Its attributes are lazy loaded and they -hold an instance of their corresponding provider. So, for example, to get -the configured meter provider:: - - from opentelemetry.configuration import Configuration - - tracer_provider = Configuration().tracer_provider - """ -from logging import getLogger from os import environ - -from pkg_resources import iter_entry_points - -logger = getLogger(__name__) +from re import fullmatch class Configuration: _instance = None - __slots__ = ("tracer_provider", "meter_provider") + __slots__ = [] def __new__(cls) -> "Configuration": if Configuration._instance is None: - configuration = { - key: "default_{}".format(key) for key in cls.__slots__ - } - - for key, value in configuration.items(): - configuration[key] = environ.get( - "OPENTELEMETRY_PYTHON_{}".format(key.upper()), value - ) - - for key, value in configuration.items(): - underscored_key = "_{}".format(key) - - setattr(Configuration, underscored_key, None) - setattr( - Configuration, - key, - property( - fget=lambda cls, local_key=key, local_value=value: cls._load( - key=local_key, value=local_value - ) - ), - ) + for key, value in environ.items(): + + match = fullmatch("OPENTELEMETRY_PYTHON_([A-Z][A-Z_]*)", key) + + if match is not None: + + key = match.group(1).lower() + + setattr(Configuration, "_{}".format(key), value) + setattr( + Configuration, + key, + property( + fget=lambda cls, key=key: getattr( + cls, "_{}".format(key) + ) + ), + ) + + Configuration.__slots__.append(key) + + Configuration.__slots__ = tuple(Configuration.__slots__) Configuration._instance = object.__new__(cls) return cls._instance - @classmethod - def _load(cls, key=None, value=None): - underscored_key = "_{}".format(key) - - if getattr(cls, underscored_key) is None: - try: - setattr( - cls, - underscored_key, - next( - iter_entry_points( - "opentelemetry_{}".format(key), name=value, - ) - ).load()(), - ) - except Exception: # pylint: disable=broad-except - # FIXME Decide on how to handle this. Should an exception be - # raised here, or only a message should be logged and should - # we fall back to the default meter provider? - logger.error( - "Failed to load configured provider %s", value, - ) - raise - - return getattr(cls, underscored_key) + def __getattr__(self, name): + return None diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index c0ab525b7d..b3fe69ab59 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -34,7 +34,7 @@ from logging import getLogger from typing import Callable, Dict, Sequence, Tuple, Type, TypeVar -from opentelemetry.configuration import Configuration # type: ignore +from opentelemetry.util import _load_provider logger = getLogger(__name__) ValueT = TypeVar("ValueT", int, float) @@ -410,8 +410,6 @@ def get_meter_provider() -> MeterProvider: global _METER_PROVIDER # pylint: disable=global-statement if _METER_PROVIDER is None: - _METER_PROVIDER = ( - Configuration().meter_provider # type: ignore # pylint: disable=no-member - ) + _METER_PROVIDER = _load_provider("meter_provider") - return _METER_PROVIDER # type: ignore + return _METER_PROVIDER diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 856745e077..773a3908ce 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -77,9 +77,8 @@ from contextlib import contextmanager from logging import getLogger -from opentelemetry.configuration import Configuration # type: ignore from opentelemetry.trace.status import Status -from opentelemetry.util import types +from opentelemetry.util import _load_provider, types logger = getLogger(__name__) @@ -701,8 +700,6 @@ def get_tracer_provider() -> TracerProvider: global _TRACER_PROVIDER # pylint: disable=global-statement if _TRACER_PROVIDER is None: - _TRACER_PROVIDER = ( - Configuration().tracer_provider # type: ignore # pylint: disable=no-member - ) + _TRACER_PROVIDER = _load_provider("tracer_provider") - return _TRACER_PROVIDER # type: ignore + return _TRACER_PROVIDER diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index 9f68ef2d39..8701d9ffba 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -12,6 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. import time +from logging import getLogger +from typing import Union + +from pkg_resources import iter_entry_points + +from opentelemetry.configuration import Configuration # type: ignore + +logger = getLogger(__name__) # Since we want API users to be able to provide timestamps, # this needs to be in the API. @@ -23,3 +31,20 @@ def time_ns() -> int: return int(time.time() * 1e9) + + +def _load_provider(provider: str) -> Union["TracerProvider", "MeterProvider"]: # type: ignore + try: + return next( # type: ignore + iter_entry_points( + "opentelemetry_{}".format(provider), + name=getattr( # type: ignore + Configuration(), provider, "default_{}".format(provider), # type: ignore + ), + ) + ).load()() + except Exception: # pylint: disable=broad-except + logger.error( + "Failed to load configured provider %s", provider, + ) + raise diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index c69c09b52b..d5a6363091 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -11,100 +11,44 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable-all -from json import dumps from unittest import TestCase from unittest.mock import patch -from pytest import fixture # type: ignore # pylint: disable=import-error - from opentelemetry.configuration import Configuration # type: ignore class TestConfiguration(TestCase): - class IterEntryPointsMock: - def __init__( - self, argument, name=None - ): # pylint: disable=unused-argument - self._name = name - - def __next__(self): - return self - - def __call__(self): - return self._name - - def load(self): - return self - - @fixture(autouse=True) - def configdir(self, tmpdir): # type: ignore # pylint: disable=no-self-use - tmpdir.chdir() - tmpdir.mkdir(".config").join("opentelemetry_python.json").write( - dumps({"tracer_provider": "overridden_tracer_provider"}) - ) - def setUp(self): - Configuration._instance = None # pylint: disable=protected-access + from opentelemetry.configuration import Configuration # type: ignore def tearDown(self): - Configuration._instance = None # pylint: disable=protected-access + from opentelemetry.configuration import Configuration # type: ignore def test_singleton(self): + self.assertIsInstance(Configuration(), Configuration) self.assertIs(Configuration(), Configuration()) - @patch( - "opentelemetry.configuration.iter_entry_points", - **{"side_effect": IterEntryPointsMock} # type: ignore - ) - def test_lazy( # type: ignore - self, mock_iter_entry_points, # pylint: disable=unused-argument - ): - configuration = Configuration() - - self.assertIsNone( - configuration._tracer_provider # pylint: disable=no-member,protected-access - ) - - configuration.tracer_provider # pylint: disable=pointless-statement - - self.assertEqual( - configuration._tracer_provider, # pylint: disable=no-member,protected-access - "default_tracer_provider", - ) - - @patch( - "opentelemetry.configuration.iter_entry_points", - **{"side_effect": IterEntryPointsMock} # type: ignore + @patch.dict( + "os.environ", # type: ignore + { + "OPENTELEMETRY_PYTHON_METER_PROVIDER": "meter_provider", + "OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider", + }, ) - def test_default_values( # type: ignore - self, mock_iter_entry_points # pylint: disable=unused-argument - ): + def test_environment_variables(self): # type: ignore self.assertEqual( - Configuration().tracer_provider, "default_tracer_provider" + Configuration().meter_provider, "meter_provider" ) # pylint: disable=no-member self.assertEqual( - Configuration().meter_provider, "default_meter_provider" + Configuration().tracer_provider, "tracer_provider" ) # pylint: disable=no-member - @patch( - "opentelemetry.configuration.iter_entry_points", - **{"side_effect": IterEntryPointsMock} # type: ignore - ) @patch.dict( - "os.environ", - {"OPENTELEMETRY_PYTHON_METER_PROVIDER": "overridden_meter_provider"}, + "os.environ", # type: ignore + {"OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider"}, ) - def test_environment_variables( # type: ignore - self, mock_iter_entry_points # pylint: disable=unused-argument - ): # type: ignore - self.assertEqual( - Configuration().tracer_provider, "default_tracer_provider" - ) # pylint: disable=no-member - self.assertEqual( - Configuration().meter_provider, "overridden_meter_provider" - ) # pylint: disable=no-member - def test_property(self): with self.assertRaises(AttributeError): Configuration().tracer_provider = "new_tracer_provider" @@ -112,3 +56,6 @@ def test_property(self): def test_slots(self): with self.assertRaises(AttributeError): Configuration().xyz = "xyz" # pylint: disable=assigning-non-slot + + def test_getattr(self): + Configuration().xyz is None From 380ce957418bf8d15554aac87512f32168e1115e Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 14 Apr 2020 15:28:40 -0700 Subject: [PATCH 0289/1517] docs: updating meeting time in readme (#580) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7fe2e819a..0333853eec 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ See the [OpenTelemetry registry](https://opentelemetry.io/registry/?s=python) fo See [CONTRIBUTING.md](CONTRIBUTING.md) -We meet weekly on Thursday at 8AM PT. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates. +We meet weekly on Thursday, and the time of the meeting alternates between 9AM PT and 4PM PT. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates. Meetings take place via [Zoom video conference](https://zoom.us/j/6729396170). From 7d11cf1c92132ace87d2d12fbfdb70164f6b9cbf Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 14 Apr 2020 18:19:02 -0700 Subject: [PATCH 0290/1517] Move shared test code from ext/ to tests/ (#559) opentelemetry-ext-testutil is a package with shared test classes used by ext packages (right now only opentelemetry-ext-flask). We don't release this package, just import it in other tests. Right now, on each release, we build everything in ext/. This means whoever does the release has to remember to exclude this package when they push the others to PyPI. This moves the files and package: Move files ext/opentelemetry-ext-testutil -> tests/util Move package opentelemetry.ext.testutil -> opentelemetry.test This makes maintainers' lives easier, but it does mean that other packages that use testutils will have to install install the opentelemetry.test package from source. But this is already the case since we don't publish opentelemetry-ext-testutil. we move shared test code back into the main repo until we move a package that depends on it into a separate repo, at which point we'll have to put this code in its own top-level package. --- ext/opentelemetry-ext-flask/setup.cfg | 3 ++- .../tests/test_flask_integration.py | 7 ++----- ext/opentelemetry-ext-wsgi/setup.cfg | 2 +- ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py | 2 +- {ext/opentelemetry-ext-testutil => tests/util}/README.rst | 0 {ext/opentelemetry-ext-testutil => tests/util}/setup.cfg | 5 ++--- {ext/opentelemetry-ext-testutil => tests/util}/setup.py | 2 +- .../util/src/opentelemetry/test}/__init__.py | 0 .../util/src/opentelemetry/test}/version.py | 0 .../util/src/opentelemetry/test}/wsgitestutil.py | 0 tox.ini | 2 +- 11 files changed, 10 insertions(+), 13 deletions(-) rename {ext/opentelemetry-ext-testutil => tests/util}/README.rst (100%) rename {ext/opentelemetry-ext-testutil => tests/util}/setup.cfg (91%) rename {ext/opentelemetry-ext-testutil => tests/util}/setup.py (92%) rename {ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil => tests/util/src/opentelemetry/test}/__init__.py (100%) rename {ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil => tests/util/src/opentelemetry/test}/version.py (100%) rename {ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil => tests/util/src/opentelemetry/test}/wsgitestutil.py (100%) diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index ec7ba987c3..70d1e2da69 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -47,7 +47,8 @@ install_requires = [options.extras_require] test = - opentelemetry-ext-testutil == 0.7.dev0 + flask~=1.0 + opentelemetry-test == 0.7.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py index b63424b905..34432be3dd 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py @@ -19,7 +19,7 @@ from werkzeug.wrappers import BaseResponse from opentelemetry import trace as trace_api -from opentelemetry.ext.testutil.wsgitestutil import WsgiTestBase +from opentelemetry.test.wsgitestutil import WsgiTestBase def expected_attributes(override_attributes): @@ -78,10 +78,7 @@ def assert_environ(): def test_simple(self): expected_attrs = expected_attributes( - { - "http.target": "/hello/123", - "http.route": "/hello/", - } + {"http.target": "/hello/123", "http.route": "/hello/"} ) resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index 038dac3496..a2017baa7d 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -44,7 +44,7 @@ install_requires = [options.extras_require] test = - opentelemetry-ext-testutil == 0.7.dev0 + opentelemetry-test == 0.7.dev0 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index 2e009696c5..e56410b612 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -20,7 +20,7 @@ import opentelemetry.ext.wsgi as otel_wsgi from opentelemetry import trace as trace_api -from opentelemetry.ext.testutil.wsgitestutil import WsgiTestBase +from opentelemetry.test.wsgitestutil import WsgiTestBase class Response: diff --git a/ext/opentelemetry-ext-testutil/README.rst b/tests/util/README.rst similarity index 100% rename from ext/opentelemetry-ext-testutil/README.rst rename to tests/util/README.rst diff --git a/ext/opentelemetry-ext-testutil/setup.cfg b/tests/util/setup.cfg similarity index 91% rename from ext/opentelemetry-ext-testutil/setup.cfg rename to tests/util/setup.cfg index 833a61b767..b68c5e3a62 100644 --- a/ext/opentelemetry-ext-testutil/setup.cfg +++ b/tests/util/setup.cfg @@ -11,14 +11,13 @@ # 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. -# [metadata] -name = opentelemetry-ext-testutil +name = opentelemetry-test description = Test utilities for OpenTelemetry unit tests author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-testutil +url = https://github.com/open-telemetry/opentelemetry-python/tests/util platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-testutil/setup.py b/tests/util/setup.py similarity index 92% rename from ext/opentelemetry-ext-testutil/setup.py rename to tests/util/setup.py index 33b875ed06..1a42a2c55d 100644 --- a/ext/opentelemetry-ext-testutil/setup.py +++ b/tests/util/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "testutil", "version.py" + BASE_DIR, "src", "opentelemetry", "test", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/__init__.py b/tests/util/src/opentelemetry/test/__init__.py similarity index 100% rename from ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/__init__.py rename to tests/util/src/opentelemetry/test/__init__.py diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py b/tests/util/src/opentelemetry/test/version.py similarity index 100% rename from ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py rename to tests/util/src/opentelemetry/test/version.py diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py b/tests/util/src/opentelemetry/test/wsgitestutil.py similarity index 100% rename from ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py rename to tests/util/src/opentelemetry/test/wsgitestutil.py diff --git a/tox.ini b/tox.ini index ac308148db..cc6a4e2c02 100644 --- a/tox.ini +++ b/tox.ini @@ -151,7 +151,7 @@ commands_pre = ext: pip install {toxinidir}/opentelemetry-api grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc grpc: pip install {toxinidir}/opentelemetry-sdk - wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-testutil + wsgi,flask: pip install {toxinidir}/tests/util wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-wsgi wsgi,flask: pip install {toxinidir}/opentelemetry-sdk flask: pip install {toxinidir}/opentelemetry-auto-instrumentation From 44b592cf2dff901fd466990e8fc4f7196a2c157b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Tue, 14 Apr 2020 22:20:54 -0500 Subject: [PATCH 0291/1517] requests: Improvements for requests integration (#573) Adding a TestBase class which wraps a tracer provider that is configured with a memory span exporter. This class inherits from unitest.TestCase, hence other test classes can inherit from it to get access to the underlying memory span exporter and tracer provider. Adding a mock propagator that could be used for testing propagation in different packages. It was implemented in the opentracing-shim and this commit moves it to a generic place. Adding disable_session(), which can be used to disable the instrumentation on a single requests' session object. --- dev-requirements.txt | 1 + .../README.rst | 2 +- ext/opentelemetry-ext-http-requests/setup.cfg | 5 + .../ext/http_requests/__init__.py | 11 +- .../tests/test_requests_integration.py | 183 ++++++++++-------- .../setup.cfg | 4 + .../tests/test_shim.py | 56 +----- .../opentelemetry/test/mock_httptextformat.py | 71 +++++++ .../util/src/opentelemetry/test/test_base.py | 39 ++++ tox.ini | 5 + 10 files changed, 236 insertions(+), 141 deletions(-) create mode 100644 tests/util/src/opentelemetry/test/mock_httptextformat.py create mode 100644 tests/util/src/opentelemetry/test/test_base.py diff --git a/dev-requirements.txt b/dev-requirements.txt index 3d7a4e2656..7854f313b8 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -9,3 +9,4 @@ sphinx-autodoc-typehints~=1.10.2 pytest!=5.2.3 pytest-cov>=2.8 readme-renderer~=24.0 +httpretty~=1.0 diff --git a/ext/opentelemetry-ext-http-requests/README.rst b/ext/opentelemetry-ext-http-requests/README.rst index 0b05b2643f..88cdecf31e 100644 --- a/ext/opentelemetry-ext-http-requests/README.rst +++ b/ext/opentelemetry-ext-http-requests/README.rst @@ -7,7 +7,7 @@ OpenTelemetry requests Integration :target: https://pypi.org/project/opentelemetry-ext-http-requests/ This library allows tracing HTTP requests made by the -`requests `_ library. +`requests `_ library. Installation ------------ diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg index 886bea11fa..33c4e579a5 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -43,5 +43,10 @@ install_requires = opentelemetry-api == 0.7.dev0 requests ~= 2.0 +[options.extras_require] +test = + opentelemetry-test == 0.7.dev0 + httpretty ~= 1.0 + [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index 3b04b362e5..cd6f3378a0 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -42,6 +42,7 @@ """ import functools +import types from urllib.parse import urlparse from requests.sessions import Session @@ -96,9 +97,6 @@ def instrumented_request(self, method, url, *args, **kwargs): span.set_attribute("http.method", method.upper()) span.set_attribute("http.url", url) - # TODO: Propagate the trace context via headers once we have a way - # to access propagators. - headers = kwargs.setdefault("headers", {}) propagators.inject(type(headers).__setitem__, headers) result = wrapped(self, method, url, *args, **kwargs) # *** PROCEED @@ -129,3 +127,10 @@ def disable(): if getattr(Session.request, "opentelemetry_ext_requests_applied", False): original = Session.request.__wrapped__ # pylint:disable=no-member Session.request = original + + +def disable_session(session): + """Disables instrumentation on the session object.""" + if getattr(session.request, "opentelemetry_ext_requests_applied", False): + original = session.request.__wrapped__ # pylint:disable=no-member + session.request = types.MethodType(original, session) diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index 5a007f5a25..441ce74e06 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -13,95 +13,50 @@ # limitations under the License. import sys -import unittest -from unittest import mock -import pkg_resources +import httpretty import requests import urllib3 import opentelemetry.ext.http_requests -from opentelemetry import trace +from opentelemetry import context, propagators, trace +from opentelemetry.test.mock_httptextformat import MockHTTPTextFormat +from opentelemetry.test.test_base import TestBase -class TestRequestsIntegration(unittest.TestCase): +class TestRequestsIntegration(TestBase): + URL = "http://httpbin.org/status/200" - # TODO: Copy & paste from test_wsgi_middleware def setUp(self): - self.span_attrs = {} - self.tracer_provider = trace.DefaultTracerProvider() - self.tracer = trace.DefaultTracer() - self.get_tracer_patcher = mock.patch.object( - self.tracer_provider, - "get_tracer", - autospec=True, - spec_set=True, - return_value=self.tracer, - ) - self.get_tracer = self.get_tracer_patcher.start() - self.span_context_manager = mock.MagicMock() - self.span = mock.create_autospec(trace.Span, spec_set=True) - self.span.get_context.return_value = trace.INVALID_SPAN_CONTEXT - self.span_context_manager.__enter__.return_value = self.span - - def setspanattr(key, value): - self.assertIsInstance(key, str) - self.span_attrs[key] = value - - self.span.set_attribute = setspanattr - self.start_span_patcher = mock.patch.object( - self.tracer, - "start_as_current_span", - autospec=True, - spec_set=True, - return_value=self.span_context_manager, - ) - - mocked_response = requests.models.Response() - mocked_response.status_code = 200 - mocked_response.reason = "Roger that!" - self.send_patcher = mock.patch.object( - requests.Session, - "send", - autospec=True, - spec_set=True, - return_value=mocked_response, - ) - - self.start_as_current_span = self.start_span_patcher.start() - self.send = self.send_patcher.start() - + super().setUp() opentelemetry.ext.http_requests.enable(self.tracer_provider) - distver = pkg_resources.get_distribution( - "opentelemetry-ext-http-requests" - ).version - self.get_tracer.assert_called_with( - opentelemetry.ext.http_requests.__name__, distver + httpretty.enable() + httpretty.register_uri( + httpretty.GET, self.URL, body="Hello!", ) def tearDown(self): + super().tearDown() opentelemetry.ext.http_requests.disable() - self.get_tracer_patcher.stop() - self.send_patcher.stop() - self.start_span_patcher.stop() + httpretty.disable() def test_basic(self): - url = "https://www.example.org/foo/bar?x=y#top" - requests.get(url=url) - self.assertEqual(1, len(self.send.call_args_list)) - self.tracer.start_as_current_span.assert_called_with( # pylint:disable=no-member - "/foo/bar", kind=trace.SpanKind.CLIENT - ) - self.span_context_manager.__enter__.assert_called_with() - self.span_context_manager.__exit__.assert_called_with(None, None, None) + result = requests.get(self.URL) + self.assertEqual(result.text, "Hello!") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + span = span_list[0] + self.assertIs(span.kind, trace.SpanKind.CLIENT) + self.assertEqual(span.name, "/status/200") + self.assertEqual( - self.span_attrs, + span.attributes, { "component": "http", "http.method": "GET", - "http.url": url, + "http.url": self.URL, "http.status_code": 200, - "http.status_text": "Roger that!", + "http.status_text": "OK", }, ) @@ -114,20 +69,84 @@ def test_invalid_url(self): exception_type = ValueError with self.assertRaises(exception_type): - requests.post(url=url) - call_args = ( - self.tracer.start_as_current_span.call_args # pylint:disable=no-member - ) - self.assertTrue( - call_args[0][0].startswith(" Context: - trace_id_list = get_from_carrier(carrier, self.TRACE_ID_KEY) - span_id_list = get_from_carrier(carrier, self.SPAN_ID_KEY) - - if not trace_id_list or not span_id_list: - return set_span_in_context(trace.INVALID_SPAN) - - return set_span_in_context( - trace.DefaultSpan( - trace.SpanContext( - trace_id=int(trace_id_list[0]), - span_id=int(span_id_list[0]), - is_remote=True, - ) - ) - ) - - def inject( - self, - set_in_carrier: Setter[HTTPTextFormatT], - carrier: HTTPTextFormatT, - context: typing.Optional[Context] = None, - ) -> None: - span = get_span_from_context(context) - set_in_carrier( - carrier, self.TRACE_ID_KEY, str(span.get_context().trace_id) - ) - set_in_carrier( - carrier, self.SPAN_ID_KEY, str(span.get_context().span_id) - ) diff --git a/tests/util/src/opentelemetry/test/mock_httptextformat.py b/tests/util/src/opentelemetry/test/mock_httptextformat.py new file mode 100644 index 0000000000..1d4b1d5d51 --- /dev/null +++ b/tests/util/src/opentelemetry/test/mock_httptextformat.py @@ -0,0 +1,71 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing + +from opentelemetry import trace +from opentelemetry.context import Context +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, +) +from opentelemetry.trace.propagation.httptextformat import ( + Getter, + HTTPTextFormat, + HTTPTextFormatT, + Setter, +) + + +class MockHTTPTextFormat(HTTPTextFormat): + """Mock propagator for testing purposes.""" + + TRACE_ID_KEY = "mock-traceid" + SPAN_ID_KEY = "mock-spanid" + + def extract( + self, + get_from_carrier: Getter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + trace_id_list = get_from_carrier(carrier, self.TRACE_ID_KEY) + span_id_list = get_from_carrier(carrier, self.SPAN_ID_KEY) + + if not trace_id_list or not span_id_list: + return set_span_in_context(trace.INVALID_SPAN) + + return set_span_in_context( + trace.DefaultSpan( + trace.SpanContext( + trace_id=int(trace_id_list[0]), + span_id=int(span_id_list[0]), + is_remote=True, + ) + ) + ) + + def inject( + self, + set_in_carrier: Setter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> None: + span = get_span_from_context(context) + set_in_carrier( + carrier, self.TRACE_ID_KEY, str(span.get_context().trace_id) + ) + set_in_carrier( + carrier, self.SPAN_ID_KEY, str(span.get_context().span_id) + ) diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py new file mode 100644 index 0000000000..974cc89408 --- /dev/null +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -0,0 +1,39 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from importlib import reload + +from opentelemetry import trace as trace_api +from opentelemetry.sdk.trace import TracerProvider, export +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + + +class TestBase(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.tracer_provider = TracerProvider() + trace_api.set_tracer_provider(cls.tracer_provider) + cls.memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(cls.memory_exporter) + cls.tracer_provider.add_span_processor(span_processor) + + @classmethod + def tearDownClass(cls): + reload(trace_api) + + def setUp(self): + self.memory_exporter.clear() diff --git a/tox.ini b/tox.ini index cc6a4e2c02..ad8e501a77 100644 --- a/tox.ini +++ b/tox.ini @@ -167,9 +167,13 @@ commands_pre = psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-psycopg2 http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests + http-requests: pip install {toxinidir}/tests/util + http-requests: pip install {toxinidir}/opentelemetry-sdk + http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests[test] jaeger: pip install {toxinidir}/opentelemetry-sdk jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-opentracing-shim + opentracing-shim: pip install {toxinidir}/tests/util zipkin: pip install {toxinidir}/opentelemetry-sdk zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin @@ -205,6 +209,7 @@ deps = black psutil readme_renderer + httpretty commands_pre = python scripts/eachdist.py install --editable From 915643cc57b65b57caefa9fcda854731b2d430e7 Mon Sep 17 00:00:00 2001 From: Yuichi Inagaki Date: Wed, 15 Apr 2020 13:54:03 +0900 Subject: [PATCH 0292/1517] sdk: Fix DefaultSpan raising an exception in use_span (#577) Fixes the error caused by span.status in use_span. As status is not included in opentelemetry.trace.Span, span.status in use_span may cause an error. For example, when use_span is called with DefaultSpan, it causes the error below. 'DefaultSpan' object has no attribute 'status' --- .../src/opentelemetry/sdk/trace/__init__.py | 3 ++- opentelemetry-sdk/tests/trace/test_trace.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index c162ea5c1c..3b64006d3d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -605,7 +605,8 @@ def use_span( except Exception as error: # pylint: disable=broad-except if ( - span.status is None + isinstance(span, Span) + and span.status is None and span._set_status_on_exception # pylint:disable=protected-access # noqa ): span.set_status( diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index ccb1ccd1e4..f9db925f11 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -107,6 +107,16 @@ def run_general_code(shutdown_on_exit, explicit_shutdown): out = run_general_code(False, False) self.assertTrue(out.startswith(b"0")) + def test_use_span_exception(self): + class TestUseSpanException(Exception): + pass + + default_span = trace_api.DefaultSpan(trace_api.INVALID_SPAN_CONTEXT) + tracer = new_tracer() + with self.assertRaises(TestUseSpanException): + with tracer.use_span(default_span): + raise TestUseSpanException() + class TestTracerSampling(unittest.TestCase): def test_default_sampler(self): From 9bf985515860dd039d5fd909b2d0ad66dcdec674 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 14 Apr 2020 23:22:35 -0600 Subject: [PATCH 0293/1517] docs: Inform new developers about other related repos (#575) This is the main OpenTelemetry Python repo, but now we have another one (and possibly more in the future) for related instrumentation-related code. Explain this in the contributing instructions to inform new developers about the existence of this repo so that they are aware of this and they efforts can be directed to the right place. --- CONTRIBUTING.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e61bb85c2b..8173bbd46f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,16 @@ on how to become a [**Member**](https://github.com/open-telemetry/community/blob [**Approver**](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver) and [**Maintainer**](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer). +# Find your right repo + +This is the main repo for OpenTelemetry Python. Nevertheless, there are other repos that are related to this project. +Please take a look at this list first, your contributions may belong in one of these repos better: + +1. [OpenTelemetry Contrib](https://github.com/open-telemetry/opentelemetry-python-contrib): Instrumentations for third-party + libraries and frameworks. There is an ongoing effort to migrate into the Opentelemetry Contrib repo some of the existing + programmatic instrumentations that are now in the `ext` directory in the main OpenTelemetry repo. Please ask in the Gitter + channel (see below) for guidance if you want to contribute with these instrumentations. + ## Find a Buddy and get Started Quickly! If you are looking for someone to help you find a starting point and be a resource for your first contribution, join our From b62c2339126bb057b8a83e2c44397de214c77f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 15 Apr 2020 00:42:35 -0500 Subject: [PATCH 0294/1517] sdk: Move b3_format to trace/propagation (#568) B3 format is a specific context propagator for tracing. This commit moves it to trace/propagation. --- .../sdk/{context => trace/propagation}/__init__.py | 0 .../sdk/{context => trace}/propagation/b3_format.py | 0 opentelemetry-sdk/tests/context/propagation/__init__.py | 0 .../sdk/context => tests/trace}/propagation/__init__.py | 0 .../tests/{context => trace}/propagation/test_b3_format.py | 2 +- 5 files changed, 1 insertion(+), 1 deletion(-) rename opentelemetry-sdk/src/opentelemetry/sdk/{context => trace/propagation}/__init__.py (100%) rename opentelemetry-sdk/src/opentelemetry/sdk/{context => trace}/propagation/b3_format.py (100%) delete mode 100644 opentelemetry-sdk/tests/context/propagation/__init__.py rename opentelemetry-sdk/{src/opentelemetry/sdk/context => tests/trace}/propagation/__init__.py (100%) rename opentelemetry-sdk/tests/{context => trace}/propagation/test_b3_format.py (99%) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py similarity index 100% rename from opentelemetry-sdk/src/opentelemetry/sdk/context/__init__.py rename to opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py similarity index 100% rename from opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py rename to opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py diff --git a/opentelemetry-sdk/tests/context/propagation/__init__.py b/opentelemetry-sdk/tests/context/propagation/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/__init__.py b/opentelemetry-sdk/tests/trace/propagation/__init__.py similarity index 100% rename from opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/__init__.py rename to opentelemetry-sdk/tests/trace/propagation/__init__.py diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py similarity index 99% rename from opentelemetry-sdk/tests/context/propagation/test_b3_format.py rename to opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index 8f06912b9e..1f6d07707f 100644 --- a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -14,8 +14,8 @@ import unittest -import opentelemetry.sdk.context.propagation.b3_format as b3_format import opentelemetry.sdk.trace as trace +import opentelemetry.sdk.trace.propagation.b3_format as b3_format import opentelemetry.trace as trace_api from opentelemetry.trace.propagation import ( get_span_from_context, From c24e80b98a2843d2cc33918ba92b8f515d2ba42b Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 16 Apr 2020 13:35:25 -0700 Subject: [PATCH 0295/1517] updating psycopg2 dependency (#588) Without using binary here, users will need the tools to compile psycopg2 on their systems. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ad8e501a77..9d1d1a26cc 100644 --- a/tox.ini +++ b/tox.ini @@ -266,7 +266,7 @@ deps = docker-compose >= 1.25.2 mysql-connector-python ~= 8.0 pymongo ~= 3.1 - psycopg2 ~= 2.8.4 + psycopg2-binary ~= 2.8.4 changedir = ext/opentelemetry-ext-docker-tests/tests From 03292aaaf82f200c8c23365151099570276865f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Thu, 16 Apr 2020 23:13:59 -0500 Subject: [PATCH 0296/1517] ext: Fix URLs in setup.cfg for ext packages (#584) URLs were missing the tree/master part. --- ext/opentelemetry-ext-dbapi/setup.cfg | 2 +- ext/opentelemetry-ext-flask/setup.cfg | 2 +- ext/opentelemetry-ext-grpc/setup.cfg | 2 +- ext/opentelemetry-ext-http-requests/setup.cfg | 2 +- ext/opentelemetry-ext-jaeger/setup.cfg | 2 +- ext/opentelemetry-ext-mysql/setup.cfg | 2 +- ext/opentelemetry-ext-opentracing-shim/setup.cfg | 2 +- ext/opentelemetry-ext-otcollector/setup.cfg | 2 +- ext/opentelemetry-ext-prometheus/setup.cfg | 2 +- ext/opentelemetry-ext-psycopg2/setup.cfg | 2 +- ext/opentelemetry-ext-pymongo/setup.cfg | 2 +- ext/opentelemetry-ext-wsgi/setup.cfg | 2 +- ext/opentelemetry-ext-zipkin/setup.cfg | 2 +- opentelemetry-auto-instrumentation/setup.cfg | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 6255184fe7..125812c96d 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-dbapi +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-dbapi platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-flask/setup.cfg b/ext/opentelemetry-ext-flask/setup.cfg index 70d1e2da69..12abffc081 100644 --- a/ext/opentelemetry-ext-flask/setup.cfg +++ b/ext/opentelemetry-ext-flask/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-flask +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-flask platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index bb2847fc12..211cf46553 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-grpc +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-grpc platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg index 33c4e579a5..d77e524774 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-http-requests +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-http-requests platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-jaeger/setup.cfg b/ext/opentelemetry-ext-jaeger/setup.cfg index f8261d747b..ead723c82b 100644 --- a/ext/opentelemetry-ext-jaeger/setup.cfg +++ b/ext/opentelemetry-ext-jaeger/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-jaeger +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-jaeger platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 451f0a4587..37fb0b5b61 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-mysql +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-mysql platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg index 64e2bd8a50..1d2b19e0d5 100644 --- a/ext/opentelemetry-ext-opentracing-shim/setup.cfg +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-opentracing-shim +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-opentracing-shim platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-otcollector/setup.cfg b/ext/opentelemetry-ext-otcollector/setup.cfg index ca6d28c491..0e02201bc8 100644 --- a/ext/opentelemetry-ext-otcollector/setup.cfg +++ b/ext/opentelemetry-ext-otcollector/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-otcollector +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-otcollector platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-prometheus/setup.cfg b/ext/opentelemetry-ext-prometheus/setup.cfg index d86649b5d3..84ec3938d1 100644 --- a/ext/opentelemetry-ext-prometheus/setup.cfg +++ b/ext/opentelemetry-ext-prometheus/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-prometheus +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-prometheus platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index 5a5d3a7e40..f8c13ff9b0 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-psycopg2 +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-psycopg2 platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index 1506237a5c..236715c3d5 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-pymongo +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-pymongo platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-wsgi/setup.cfg b/ext/opentelemetry-ext-wsgi/setup.cfg index a2017baa7d..fd4a6a60d8 100644 --- a/ext/opentelemetry-ext-wsgi/setup.cfg +++ b/ext/opentelemetry-ext-wsgi/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-wsgi +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-wsgi platforms = any license = Apache-2.0 classifiers = diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg index bedb5e7f73..68ae439e39 100644 --- a/ext/opentelemetry-ext-zipkin/setup.cfg +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-zipkin +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-zipkin platforms = any license = Apache-2.0 classifiers = diff --git a/opentelemetry-auto-instrumentation/setup.cfg b/opentelemetry-auto-instrumentation/setup.cfg index 7d7ccf0cfe..1ab2010cd8 100644 --- a/opentelemetry-auto-instrumentation/setup.cfg +++ b/opentelemetry-auto-instrumentation/setup.cfg @@ -19,7 +19,7 @@ long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-auto-instrumentation" +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-auto-instrumentation platforms = any license = Apache-2.0 classifiers = From 920b4b45e98df938c553f575f744146361d57b4b Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Mon, 20 Apr 2020 13:18:46 -0700 Subject: [PATCH 0297/1517] Updating export result enums (#590) --- .../ext/otcollector/metrics_exporter/__init__.py | 2 +- .../opentelemetry/ext/otcollector/trace_exporter/__init__.py | 2 +- .../src/opentelemetry/ext/zipkin/__init__.py | 4 ++-- ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py | 2 +- .../src/opentelemetry/sdk/metrics/export/__init__.py | 3 +-- .../src/opentelemetry/sdk/trace/export/__init__.py | 3 +-- .../opentelemetry/sdk/trace/export/in_memory_span_exporter.py | 2 +- .../tests/trace/export/test_in_memory_span_exporter.py | 2 +- 8 files changed, 9 insertions(+), 11 deletions(-) diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py index d77dcd4324..3a43d9cc84 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/metrics_exporter/__init__.py @@ -80,7 +80,7 @@ def export( pass except grpc.RpcError: - return MetricsExportResult.FAILED_RETRYABLE + return MetricsExportResult.FAILURE return MetricsExportResult.SUCCESS diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py index 495579574c..d287bcfb4a 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py @@ -73,7 +73,7 @@ def export(self, spans: Sequence[Span]) -> SpanExportResult: pass except grpc.RpcError: - return SpanExportResult.FAILED_NOT_RETRYABLE + return SpanExportResult.FAILURE return SpanExportResult.SUCCESS diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index edc9ee7031..8b4aa93c72 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -142,8 +142,8 @@ def export(self, spans: Sequence[Span]) -> SpanExportResult: ) if self.retry: - return SpanExportResult.FAILED_RETRYABLE - return SpanExportResult.FAILED_NOT_RETRYABLE + return SpanExportResult.FAILURE + return SpanExportResult.FAILURE return SpanExportResult.SUCCESS def _translate_to_zipkin(self, spans: Sequence[Span]): diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py index 578b533e0e..cd21839e3e 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -242,4 +242,4 @@ def test_invalid_response(self, mock_post): spans = [] exporter = ZipkinSpanExporter("test-service") status = exporter.export(spans) - self.assertEqual(SpanExportResult.FAILED_NOT_RETRYABLE, status) + self.assertEqual(SpanExportResult.FAILURE, status) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index 8b0fbdb1a3..f5a8693268 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -21,8 +21,7 @@ class MetricsExportResult(Enum): SUCCESS = 0 - FAILED_RETRYABLE = 1 - FAILED_NOT_RETRYABLE = 2 + FAILURE = 1 class MetricRecord: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index d67f7b9f81..515962ca36 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -31,8 +31,7 @@ class SpanExportResult(Enum): SUCCESS = 0 - FAILED_RETRYABLE = 1 - FAILED_NOT_RETRYABLE = 2 + FAILURE = 1 class SpanExporter: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py index e50ca614d6..f652d56e82 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py @@ -45,7 +45,7 @@ def get_finished_spans(self): def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: """Stores a list of spans in memory.""" if self._stopped: - return SpanExportResult.FAILED_NOT_RETRYABLE + return SpanExportResult.FAILURE with self._lock: self._finished_spans.extend(spans) return SpanExportResult.SUCCESS diff --git a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py index 1054cc413d..4925bdb182 100644 --- a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py +++ b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py @@ -72,4 +72,4 @@ def test_return_code(self): # after shutdown export should fail ret = memory_exporter.export(span_list) - self.assertEqual(ret, export.SpanExportResult.FAILED_NOT_RETRYABLE) + self.assertEqual(ret, export.SpanExportResult.FAILURE) From 4115c1b5bd29a15591fb82d2a96eacb561889b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 20 Apr 2020 15:58:49 -0500 Subject: [PATCH 0298/1517] ext: Use TestBase (#586) Update tests to use TestBase as described on #303. Co-authored-by: Yusuke Tsutsumi Co-authored-by: Chris Kleinknecht --- ext/opentelemetry-ext-dbapi/setup.cfg | 4 + .../tests/test_dbapi_integration.py | 108 ++++++------------ .../tests/mysql/test_mysql_functional.py | 21 +--- .../tests/postgres/test_psycopg_functional.py | 21 +--- .../tests/pymongo/test_pymongo_functional.py | 22 +--- ext/opentelemetry-ext-grpc/setup.cfg | 5 + .../tests/test_server_interceptor.py | 27 +++-- .../tests/test_requests_integration.py | 2 + ext/opentelemetry-ext-mysql/setup.cfg | 6 +- .../tests/test_mysql_integration.py | 19 +-- ext/opentelemetry-ext-pymongo/setup.cfg | 4 + .../tests/test_pymongo.py | 85 +++++--------- tox.ini | 49 ++++---- 13 files changed, 143 insertions(+), 230 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 125812c96d..53fe066dfd 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -43,5 +43,9 @@ install_requires = opentelemetry-api == 0.7.dev0 wrapt >= 1.0.0, < 2.0.0 +[options.extras_require] +test = + opentelemetry-test == 0.7.dev0 + [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py index f0773788bc..9d894a2ccb 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -12,29 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest -from unittest import mock - from opentelemetry import trace as trace_api from opentelemetry.ext.dbapi import DatabaseApiIntegration +from opentelemetry.test.test_base import TestBase -class TestDBApiIntegration(unittest.TestCase): +class TestDBApiIntegration(TestBase): def setUp(self): - self.tracer = trace_api.DefaultTracer() - self.span = MockSpan() - self.start_current_span_patcher = mock.patch.object( - self.tracer, - "start_as_current_span", - autospec=True, - spec_set=True, - return_value=self.span, - ) - - self.start_as_current_span = self.start_current_span_patcher.start() - - def tearDown(self): - self.start_current_span_patcher.stop() + super().setUp() + self.tracer = self.tracer_provider.get_tracer(__name__) def test_span_succeeded(self): connection_props = { @@ -57,28 +43,25 @@ def test_span_succeeded(self): ) cursor = mock_connection.cursor() cursor.execute("Test query", ("param1Value", False)) - self.assertTrue(self.start_as_current_span.called) + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "testcomponent.testdatabase") + self.assertIs(span.kind, trace_api.SpanKind.CLIENT) + + self.assertEqual(span.attributes["component"], "testcomponent") + self.assertEqual(span.attributes["db.type"], "testtype") + self.assertEqual(span.attributes["db.instance"], "testdatabase") + self.assertEqual(span.attributes["db.statement"], "Test query") self.assertEqual( - self.start_as_current_span.call_args[0][0], - "testcomponent.testdatabase", - ) - self.assertIs( - self.start_as_current_span.call_args[1]["kind"], - trace_api.SpanKind.CLIENT, - ) - self.assertEqual(self.span.attributes["component"], "testcomponent") - self.assertEqual(self.span.attributes["db.type"], "testtype") - self.assertEqual(self.span.attributes["db.instance"], "testdatabase") - self.assertEqual(self.span.attributes["db.statement"], "Test query") - self.assertEqual( - self.span.attributes["db.statement.parameters"], + span.attributes["db.statement.parameters"], "('param1Value', False)", ) - self.assertEqual(self.span.attributes["db.user"], "testuser") - self.assertEqual(self.span.attributes["net.peer.name"], "testhost") - self.assertEqual(self.span.attributes["net.peer.port"], 123) + self.assertEqual(span.attributes["db.user"], "testuser") + self.assertEqual(span.attributes["net.peer.name"], "testhost") + self.assertEqual(span.attributes["net.peer.port"], 123) self.assertIs( - self.span.status.canonical_code, + span.status.canonical_code, trace_api.status.StatusCanonicalCode.OK, ) @@ -88,17 +71,18 @@ def test_span_failed(self): mock_connect, {}, {} ) cursor = mock_connection.cursor() - try: + with self.assertRaises(Exception): cursor.execute("Test query", throw_exception=True) - except Exception: # pylint: disable=broad-except - self.assertEqual( - self.span.attributes["db.statement"], "Test query" - ) - self.assertIs( - self.span.status.canonical_code, - trace_api.status.StatusCanonicalCode.UNKNOWN, - ) - self.assertEqual(self.span.status.description, "Test Exception") + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.attributes["db.statement"], "Test query") + self.assertIs( + span.status.canonical_code, + trace_api.status.StatusCanonicalCode.UNKNOWN, + ) + self.assertEqual(span.status.description, "Test Exception") def test_executemany(self): db_integration = DatabaseApiIntegration(self.tracer, "testcomponent") @@ -107,8 +91,10 @@ def test_executemany(self): ) cursor = mock_connection.cursor() cursor.executemany("Test query") - self.assertTrue(self.start_as_current_span.called) - self.assertEqual(self.span.attributes["db.statement"], "Test query") + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.attributes["db.statement"], "Test query") def test_callproc(self): db_integration = DatabaseApiIntegration(self.tracer, "testcomponent") @@ -117,9 +103,11 @@ def test_callproc(self): ) cursor = mock_connection.cursor() cursor.callproc("Test stored procedure") - self.assertTrue(self.start_as_current_span.called) + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] self.assertEqual( - self.span.attributes["db.statement"], "Test stored procedure" + span.attributes["db.statement"], "Test stored procedure" ) @@ -159,23 +147,3 @@ def executemany(self, query, params=None, throw_exception=False): def callproc(self, query, params=None, throw_exception=False): if throw_exception: raise Exception("Test Exception") - - -class MockSpan: - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - return False - - def __init__(self): - self.status = None - self.name = "" - self.kind = trace_api.SpanKind.INTERNAL - self.attributes = {} - - def set_attribute(self, key, value): - self.attributes[key] = value - - def set_status(self, status): - self.status = status diff --git a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py index 305bea00bf..c0790396b0 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py @@ -14,17 +14,12 @@ import os import time -import unittest import mysql.connector from opentelemetry import trace as trace_api from opentelemetry.ext.mysql import trace_integration -from opentelemetry.sdk.trace import Tracer, TracerProvider -from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, -) +from opentelemetry.test.test_base import TestBase MYSQL_USER = os.getenv("MYSQL_USER ", "testuser") MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD ", "testpassword") @@ -33,16 +28,13 @@ MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME ", "opentelemetry-tests") -class TestFunctionalMysql(unittest.TestCase): +class TestFunctionalMysql(TestBase): @classmethod def setUpClass(cls): + super().setUpClass() cls._connection = None cls._cursor = None - cls._tracer_provider = TracerProvider() - cls._tracer = Tracer(cls._tracer_provider, None) - cls._span_exporter = InMemorySpanExporter() - cls._span_processor = SimpleExportSpanProcessor(cls._span_exporter) - cls._tracer_provider.add_span_processor(cls._span_processor) + cls._tracer = cls.tracer_provider.get_tracer(__name__) trace_integration(cls._tracer) cls._connection = mysql.connector.connect( user=MYSQL_USER, @@ -58,11 +50,8 @@ def tearDownClass(cls): if cls._connection: cls._connection.close() - def setUp(self): - self._span_exporter.clear() - def validate_spans(self): - spans = self._span_exporter.get_finished_spans() + spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) for span in spans: if span.name == "rootSpan": diff --git a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py index d58e332c1b..a0ddfcae15 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py @@ -14,17 +14,12 @@ import os import time -import unittest import psycopg2 from opentelemetry import trace as trace_api from opentelemetry.ext.psycopg2 import trace_integration -from opentelemetry.sdk.trace import Tracer, TracerProvider -from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, -) +from opentelemetry.test.test_base import TestBase POSTGRES_HOST = os.getenv("POSTGRESQL_HOST ", "localhost") POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT ", "5432")) @@ -33,16 +28,13 @@ POSTGRES_USER = os.getenv("POSTGRESQL_HOST ", "testuser") -class TestFunctionalPsycopg(unittest.TestCase): +class TestFunctionalPsycopg(TestBase): @classmethod def setUpClass(cls): + super().setUpClass() cls._connection = None cls._cursor = None - cls._tracer_provider = TracerProvider() - cls._tracer = Tracer(cls._tracer_provider, None) - cls._span_exporter = InMemorySpanExporter() - cls._span_processor = SimpleExportSpanProcessor(cls._span_exporter) - cls._tracer_provider.add_span_processor(cls._span_processor) + cls._tracer = cls.tracer_provider.get_tracer(__name__) trace_integration(cls._tracer) cls._connection = psycopg2.connect( dbname=POSTGRES_DB_NAME, @@ -61,11 +53,8 @@ def tearDownClass(cls): if cls._connection: cls._connection.close() - def setUp(self): - self._span_exporter.clear() - def validate_spans(self): - spans = self._span_exporter.get_finished_spans() + spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) for span in spans: if span.name == "rootSpan": diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py index 7b018dab25..6f6a728e51 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py @@ -13,18 +13,12 @@ # limitations under the License. import os -import typing -import unittest from pymongo import MongoClient from opentelemetry import trace as trace_api from opentelemetry.ext.pymongo import trace_integration -from opentelemetry.sdk.trace import Span, Tracer, TracerProvider -from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, -) +from opentelemetry.test.test_base import TestBase MONGODB_HOST = os.getenv("MONGODB_HOST ", "localhost") MONGODB_PORT = int(os.getenv("MONGODB_PORT ", "27017")) @@ -32,14 +26,11 @@ MONGODB_COLLECTION_NAME = "test" -class TestFunctionalPymongo(unittest.TestCase): +class TestFunctionalPymongo(TestBase): @classmethod def setUpClass(cls): - cls._tracer_provider = TracerProvider() - cls._tracer = Tracer(cls._tracer_provider, None) - cls._span_exporter = InMemorySpanExporter() - cls._span_processor = SimpleExportSpanProcessor(cls._span_exporter) - cls._tracer_provider.add_span_processor(cls._span_processor) + super().setUpClass() + cls._tracer = cls.tracer_provider.get_tracer(__name__) trace_integration(cls._tracer) client = MongoClient( MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000 @@ -47,11 +38,8 @@ def setUpClass(cls): db = client[MONGODB_DB_NAME] cls._collection = db[MONGODB_COLLECTION_NAME] - def setUp(self): - self._span_exporter.clear() - def validate_spans(self): - spans = self._span_exporter.get_finished_spans() + spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) for span in spans: if span.name == "rootSpan": diff --git a/ext/opentelemetry-ext-grpc/setup.cfg b/ext/opentelemetry-ext-grpc/setup.cfg index 211cf46553..f774ddc6c5 100644 --- a/ext/opentelemetry-ext-grpc/setup.cfg +++ b/ext/opentelemetry-ext-grpc/setup.cfg @@ -43,5 +43,10 @@ install_requires = opentelemetry-api == 0.7.dev0 grpcio ~= 1.27 +[options.extras_require] +test = + opentelemetry-test == 0.7.dev0 + opentelemetry-sdk == 0.7.dev0 + [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py index 8dabd11fdf..74ce9babab 100644 --- a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py +++ b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py @@ -16,10 +16,7 @@ # pylint:disable=no-self-use import threading -import unittest from concurrent import futures -from contextlib import contextmanager -from unittest import mock import grpc @@ -27,6 +24,7 @@ from opentelemetry.ext.grpc import server_interceptor from opentelemetry.ext.grpc.grpcext import intercept_server from opentelemetry.sdk import trace as trace_sdk +from opentelemetry.test.test_base import TestBase class UnaryUnaryMethodHandler(grpc.RpcMethodHandler): @@ -49,18 +47,16 @@ def service(self, handler_call_details): return UnaryUnaryMethodHandler(self._unary_unary_handler) -class TestOpenTelemetryServerInterceptor(unittest.TestCase): +class TestOpenTelemetryServerInterceptor(TestBase): + def setUp(self): + super().setUp() + self.tracer = self.tracer_provider.get_tracer(__name__) + def test_create_span(self): """Check that the interceptor wraps calls with spans server-side.""" - @contextmanager - def mock_start_as_current_span(*args, **kwargs): - yield mock.Mock(spec=trace.Span) - # Intercept gRPC calls... - tracer = mock.Mock(spec=trace.Tracer) - tracer.start_as_current_span.side_effect = mock_start_as_current_span - interceptor = server_interceptor(tracer) + interceptor = server_interceptor(self.tracer) # No-op RPC handler def handler(request, context): @@ -84,9 +80,12 @@ def handler(request, context): finally: server.stop(None) - tracer.start_as_current_span.assert_called_once_with( - name="", kind=trace.SpanKind.SERVER - ) + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertEqual(span.name, "") + self.assertIs(span.kind, trace.SpanKind.SERVER) def test_span_lifetime(self): """Check that the span is active for the duration of the call.""" diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index 441ce74e06..67de0692f1 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -88,6 +88,8 @@ def test_disable(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) + opentelemetry.ext.http_requests.disable() + def test_disable_session(self): session1 = requests.Session() opentelemetry.ext.http_requests.disable_session(session1) diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 37fb0b5b61..3da2aad0f0 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -41,8 +41,12 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.7.dev0 - mysql-connector-python ~= 8.0 + mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 +[options.extras_require] +test = + opentelemetry-test == 0.7.dev0 + [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py index b4f1a006a1..150f9f51f9 100644 --- a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -12,27 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from unittest import mock import mysql.connector -from opentelemetry import trace as trace_api from opentelemetry.ext.mysql import trace_integration +from opentelemetry.test.test_base import TestBase -class TestMysqlIntegration(unittest.TestCase): +class TestMysqlIntegration(TestBase): def test_trace_integration(self): - tracer = trace_api.DefaultTracer() - span = mock.create_autospec(trace_api.Span, spec_set=True) - start_current_span_patcher = mock.patch.object( - tracer, - "start_as_current_span", - autospec=True, - spec_set=True, - return_value=span, - ) - start_as_current_span = start_current_span_patcher.start() + tracer = self.tracer_provider.get_tracer(__name__) with mock.patch("mysql.connector.connect") as mock_connect: mock_connect.get.side_effect = mysql.connector.MySQLConnection() @@ -41,4 +31,5 @@ def test_trace_integration(self): cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) - self.assertTrue(start_as_current_span.called) + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index 236715c3d5..790ea8bff2 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -43,5 +43,9 @@ install_requires = opentelemetry-api == 0.7.dev0 pymongo ~= 3.1 +[options.extras_require] +test = + opentelemetry-test == 0.7.dev0 + [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py index 8c82e01679..197c4d0266 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py @@ -12,23 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from unittest import mock from opentelemetry import trace as trace_api from opentelemetry.ext.pymongo import CommandTracer, trace_integration -from opentelemetry.util import time_ns +from opentelemetry.test.test_base import TestBase -class TestPymongo(unittest.TestCase): +class TestPymongo(TestBase): + def setUp(self): + super().setUp() + self.tracer = self.tracer_provider.get_tracer(__name__) + def test_trace_integration(self): mock_register = mock.Mock() patch = mock.patch( "pymongo.monitoring.register", side_effect=mock_register ) - mock_tracer = MockTracer() with patch: - trace_integration(mock_tracer) + trace_integration(self.tracer) self.assertTrue(mock_register.called) @@ -40,12 +42,13 @@ def test_started(self): "pipeline": "pipeline", "command_name": "find", } - mock_tracer = MockTracer() - command_tracer = CommandTracer(mock_tracer) + command_tracer = CommandTracer(self.tracer) mock_event = MockEvent( command_attrs, ("test.com", "1234"), "test_request_id" ) command_tracer.started(event=mock_event) + # the memory exporter can't be used here because the span isn't ended + # yet # pylint: disable=protected-access span = command_tracer._get_span(mock_event) self.assertIs(span.kind, trace_api.SpanKind.CLIENT) @@ -69,13 +72,13 @@ def test_started(self): self.assertEqual(span.attributes["db.mongo.pipeline"], "pipeline") def test_succeeded(self): - mock_tracer = MockTracer() mock_event = MockEvent({}) - command_tracer = CommandTracer(mock_tracer) + command_tracer = CommandTracer(self.tracer) command_tracer.started(event=mock_event) - # pylint: disable=protected-access - span = command_tracer._get_span(mock_event) command_tracer.succeeded(event=mock_event) + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] self.assertEqual( span.attributes["db.mongo.duration_micros"], "duration_micros" ) @@ -86,13 +89,15 @@ def test_succeeded(self): self.assertIsNotNone(span.end_time) def test_failed(self): - mock_tracer = MockTracer() mock_event = MockEvent({}) - command_tracer = CommandTracer(mock_tracer) + command_tracer = CommandTracer(self.tracer) command_tracer.started(event=mock_event) - # pylint: disable=protected-access - span = command_tracer._get_span(mock_event) command_tracer.failed(event=mock_event) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual( span.attributes["db.mongo.duration_micros"], "duration_micros" ) @@ -104,19 +109,19 @@ def test_failed(self): self.assertIsNotNone(span.end_time) def test_multiple_commands(self): - mock_tracer = MockTracer() first_mock_event = MockEvent({}, ("firstUrl", "123"), "first") second_mock_event = MockEvent({}, ("secondUrl", "456"), "second") - command_tracer = CommandTracer(mock_tracer) + command_tracer = CommandTracer(self.tracer) command_tracer.started(event=first_mock_event) - # pylint: disable=protected-access - first_span = command_tracer._get_span(first_mock_event) command_tracer.started(event=second_mock_event) - # pylint: disable=protected-access - second_span = command_tracer._get_span(second_mock_event) command_tracer.succeeded(event=first_mock_event) command_tracer.failed(event=second_mock_event) + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 2) + first_span = spans_list[0] + second_span = spans_list[1] + self.assertEqual(first_span.attributes["db.mongo.request_id"], "first") self.assertIs( first_span.status.canonical_code, @@ -147,41 +152,3 @@ def __init__(self, command_attrs, connection_id=None, request_id=""): def __getattr__(self, item): return item - - -class MockSpan: - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - return False - - def __init__(self): - self.status = None - self.name = "" - self.kind = trace_api.SpanKind.INTERNAL - self.attributes = None - self.end_time = None - - def set_attribute(self, key, value): - self.attributes[key] = value - - def set_status(self, status): - self.status = status - - def end(self, end_time=None): - self.end_time = end_time if end_time is not None else time_ns() - - -class MockTracer: - def __init__(self): - self.end_span = mock.Mock() - - # pylint: disable=no-self-use - def start_span(self, name, kind): - span = MockSpan() - span.attributes = {} - span.status = None - span.name = name - span.kind = kind - return span diff --git a/tox.ini b/tox.ini index 9d1d1a26cc..dc7f20aa8d 100644 --- a/tox.ini +++ b/tox.ini @@ -131,50 +131,52 @@ changedir = commands_pre = ; Install without -e to test the actual installation python -m pip install -U pip setuptools wheel + +; Install common packages for all the tests. These are not needed in all the +; cases but it saves a lot of boilerplate in this file. test: pip install {toxinidir}/opentelemetry-api - test-sdk: pip install {toxinidir}/opentelemetry-sdk + test: pip install {toxinidir}/opentelemetry-sdk + test: pip install {toxinidir}/tests/util + test-auto-instrumentation: pip install {toxinidir}/opentelemetry-auto-instrumentation - example-app: pip install {toxinidir}/opentelemetry-sdk + example-app: pip install {toxinidir}/opentelemetry-auto-instrumentation example-app: pip install {toxinidir}/ext/opentelemetry-ext-http-requests example-app: pip install {toxinidir}/ext/opentelemetry-ext-wsgi example-app: pip install {toxinidir}/ext/opentelemetry-ext-flask example-app: pip install {toxinidir}/docs/examples/opentelemetry-example-app - example-basic-tracer: pip install -e {toxinidir}/opentelemetry-api - example-basic-tracer: pip install -e {toxinidir}/opentelemetry-sdk - example-http: pip install -e {toxinidir}/opentelemetry-api - example-http: pip install -e {toxinidir}/opentelemetry-sdk + example-http: pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests example-http: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi example-http: pip install -r {toxinidir}/docs/examples/http/requirements.txt - ext: pip install {toxinidir}/opentelemetry-api - grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc - grpc: pip install {toxinidir}/opentelemetry-sdk - wsgi,flask: pip install {toxinidir}/tests/util + grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc[test] + wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-wsgi - wsgi,flask: pip install {toxinidir}/opentelemetry-sdk + flask: pip install {toxinidir}/opentelemetry-auto-instrumentation flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] - dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi + + dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi[test] + mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi - mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql - otcollector: pip install {toxinidir}/opentelemetry-sdk + mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql[test] + otcollector: pip install {toxinidir}/ext/opentelemetry-ext-otcollector - prometheus: pip install {toxinidir}/opentelemetry-sdk + prometheus: pip install {toxinidir}/ext/opentelemetry-ext-prometheus - pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo + + pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo[test] + psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-psycopg2 - http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests - http-requests: pip install {toxinidir}/tests/util - http-requests: pip install {toxinidir}/opentelemetry-sdk + http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests[test] - jaeger: pip install {toxinidir}/opentelemetry-sdk + jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger - opentracing-shim: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-opentracing-shim - opentracing-shim: pip install {toxinidir}/tests/util - zipkin: pip install {toxinidir}/opentelemetry-sdk + + opentracing-shim: pip install {toxinidir}/ext/opentelemetry-ext-opentracing-shim + zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin ; In order to get a healthy coverage report, @@ -274,6 +276,7 @@ changedir = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-sdk \ + -e {toxinidir}/tests/util \ -e {toxinidir}/ext/opentelemetry-ext-dbapi \ -e {toxinidir}/ext/opentelemetry-ext-mysql \ -e {toxinidir}/ext/opentelemetry-ext-psycopg2 \ From dfd5f7a7fb5a53dbf9331eb988ef2210eccc0866 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 21 Apr 2020 15:17:04 -0700 Subject: [PATCH 0299/1517] minor cleanup of examples (#603) Couple of examples were missing import statements. --- .../src/opentelemetry/ext/mysql/__init__.py | 1 + .../src/opentelemetry/ext/pymongo/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index db799204e5..16b64655c7 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -24,6 +24,7 @@ .. code:: python import mysql.connector + from opentelemetry import trace from opentelemetry.trace import TracerProvider from opentelemetry.ext.mysql import trace_integration diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index 9292b88052..4ab87158be 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -24,6 +24,7 @@ .. code:: python from pymongo import MongoClient + from opentelemetry import trace from opentelemetry.trace import TracerProvider from opentelemetry.trace.ext.pymongo import trace_integration From 16b9d88a2bbe5b4064a53bfc745bc105d1700445 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 21 Apr 2020 16:10:57 -0700 Subject: [PATCH 0300/1517] Removing reload in testbase (#604) --- tests/util/src/opentelemetry/test/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index 974cc89408..d35eef650a 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -13,7 +13,6 @@ # limitations under the License. import unittest -from importlib import reload from opentelemetry import trace as trace_api from opentelemetry.sdk.trace import TracerProvider, export @@ -26,6 +25,7 @@ class TestBase(unittest.TestCase): @classmethod def setUpClass(cls): cls.tracer_provider = TracerProvider() + cls.original_provider = trace_api.get_tracer_provider() trace_api.set_tracer_provider(cls.tracer_provider) cls.memory_exporter = InMemorySpanExporter() span_processor = export.SimpleExportSpanProcessor(cls.memory_exporter) @@ -33,7 +33,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - reload(trace_api) + trace_api.set_tracer_provider(cls.original_provider) def setUp(self): self.memory_exporter.clear() From 305c1f42d9286dae7dd5790b5c298677ea4bdac6 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 22 Apr 2020 10:59:26 -0600 Subject: [PATCH 0301/1517] auto-instr: Add support for programmatic instrumentation (#579) Fixes #554 This makes it possible to call the instrument method with arguments that make programmatic instrumentation possible. This also makes the children of BaseInstrumentors to be singletons. In this way regardless of how many times the programmatic instrumentation or uninstrumentation methods are called they will only be executed once. --- .../src/opentelemetry/ext/flask/__init__.py | 4 ++-- .../auto_instrumentation/instrumentor.py | 23 ++++++++++++------- .../tests/test_instrumentor.py | 18 ++++++++------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index 9b21696c59..eb008eeadc 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -160,9 +160,9 @@ def __init__(self): super().__init__() self._original_flask = None - def _instrument(self): + def _instrument(self, **kwargs): self._original_flask = flask.Flask flask.Flask = _InstrumentedFlask - def _uninstrument(self): + def _uninstrument(self, **kwargs): flask.Flask = self._original_flask diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py index 9deb6b1523..f5d7cf7ddc 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py @@ -26,22 +26,29 @@ class BaseInstrumentor(ABC): """An ABC for instrumentors""" - def __init__(self): - self._is_instrumented = False + _instance = None + _is_instrumented = False + + def __new__(cls): + + if cls._instance is None: + cls._instance = object.__new__(cls) + + return cls._instance @abstractmethod - def _instrument(self) -> None: + def _instrument(self, **kwargs): """Instrument""" @abstractmethod - def _uninstrument(self) -> None: + def _uninstrument(self, **kwargs): """Uninstrument""" - def instrument(self) -> None: + def instrument(self, **kwargs): """Instrument""" if not self._is_instrumented: - result = self._instrument() + result = self._instrument(**kwargs) self._is_instrumented = True return result @@ -49,11 +56,11 @@ def instrument(self) -> None: return None - def uninstrument(self) -> None: + def uninstrument(self, **kwargs): """Uninstrument""" if self._is_instrumented: - result = self._uninstrument() + result = self._uninstrument(**kwargs) self._is_instrumented = False return result diff --git a/opentelemetry-auto-instrumentation/tests/test_instrumentor.py b/opentelemetry-auto-instrumentation/tests/test_instrumentor.py index 1324213536..40e762230a 100644 --- a/opentelemetry-auto-instrumentation/tests/test_instrumentor.py +++ b/opentelemetry-auto-instrumentation/tests/test_instrumentor.py @@ -20,21 +20,20 @@ class TestInstrumentor(TestCase): - def test_protect(self): - class Instrumentor(BaseInstrumentor): - def _instrument(self): - return "instrumented" + class Instrumentor(BaseInstrumentor): + def _instrument(self, **kwargs): + return "instrumented" - def _uninstrument(self): - return "uninstrumented" + def _uninstrument(self, **kwargs): + return "uninstrumented" - instrumentor = Instrumentor() + def test_protect(self): + instrumentor = self.Instrumentor() with self.assertLogs(level=WARNING): self.assertIs(instrumentor.uninstrument(), None) self.assertEqual(instrumentor.instrument(), "instrumented") - with self.assertLogs(level=WARNING): self.assertIs(instrumentor.instrument(), None) @@ -42,3 +41,6 @@ def _uninstrument(self): with self.assertLogs(level=WARNING): self.assertIs(instrumentor.uninstrument(), None) + + def test_singleton(self): + self.assertIs(self.Instrumentor(), self.Instrumentor()) From 232bfdda0ccf4fc6ba6810edce87d5eb302b69d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 22 Apr 2020 21:58:08 -0500 Subject: [PATCH 0302/1517] sdk: Improve console span exporter (#505) The current version of the exporter prints everything in a single line, making it difficult to read. It's also missing events, links and attributes. This commit changes the console span exporter to use multiple lines and also adds the missing information about attributes, events and links. --- .../basic_tracer/tests/test_tracer.py | 6 +- docs/examples/http/tests/test_http.py | 2 +- .../src/opentelemetry/sdk/trace/__init__.py | 89 ++++++++++++++++--- .../sdk/trace/export/__init__.py | 2 +- .../tests/trace/export/test_export.py | 4 +- 5 files changed, 83 insertions(+), 20 deletions(-) diff --git a/docs/examples/basic_tracer/tests/test_tracer.py b/docs/examples/basic_tracer/tests/test_tracer.py index 8f73c2bbb0..77b25be5f0 100644 --- a/docs/examples/basic_tracer/tests/test_tracer.py +++ b/docs/examples/basic_tracer/tests/test_tracer.py @@ -25,6 +25,6 @@ def test_basic_tracer(self): (sys.executable, test_script) ).decode() - self.assertIn('name="foo"', output) - self.assertIn('name="bar"', output) - self.assertIn('name="baz"', output) + self.assertIn('"name": "foo"', output) + self.assertIn('"name": "bar"', output) + self.assertIn('"name": "baz"', output) diff --git a/docs/examples/http/tests/test_http.py b/docs/examples/http/tests/test_http.py index fe2a38cec0..6749f9b799 100644 --- a/docs/examples/http/tests/test_http.py +++ b/docs/examples/http/tests/test_http.py @@ -32,7 +32,7 @@ def test_http(self): output = subprocess.check_output( (sys.executable, test_script) ).decode() - self.assertIn('name="/"', output) + self.assertIn('"name": "/"', output) @classmethod def teardown_class(cls): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 3b64006d3d..3d2fc96c1b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -15,9 +15,12 @@ import abc import atexit +import json import logging +import os import random import threading +from collections import OrderedDict from contextlib import contextmanager from types import TracebackType from typing import Iterator, MutableSequence, Optional, Sequence, Tuple, Type @@ -276,19 +279,79 @@ def __repr__(self): type(self).__name__, self.name, self.context ) - def __str__(self): - return ( - '{}(name="{}", context={}, kind={}, ' - "parent={}, start_time={}, end_time={})" - ).format( - type(self).__name__, - self.name, - self.context, - self.kind, - repr(self.parent), - util.ns_to_iso_str(self.start_time) if self.start_time else "None", - util.ns_to_iso_str(self.end_time) if self.end_time else "None", - ) + @staticmethod + def _format_context(context): + x_ctx = OrderedDict() + x_ctx["trace_id"] = trace_api.format_trace_id(context.trace_id) + x_ctx["span_id"] = trace_api.format_span_id(context.span_id) + x_ctx["trace_state"] = repr(context.trace_state) + return x_ctx + + @staticmethod + def _format_attributes(attributes): + if isinstance(attributes, BoundedDict): + return attributes._dict # pylint: disable=protected-access + return attributes + + @staticmethod + def _format_events(events): + f_events = [] + for event in events: + f_event = OrderedDict() + f_event["name"] = event.name + f_event["timestamp"] = util.ns_to_iso_str(event.timestamp) + f_event["attributes"] = Span._format_attributes(event.attributes) + f_events.append(f_event) + return f_events + + @staticmethod + def _format_links(links): + f_links = [] + for link in links: + f_link = OrderedDict() + f_link["context"] = Span._format_context(link.context) + f_link["attributes"] = Span._format_attributes(link.attributes) + f_links.append(f_link) + return f_links + + def to_json(self): + parent_id = None + if self.parent is not None: + if isinstance(self.parent, Span): + ctx = self.parent.context + parent_id = trace_api.format_span_id(ctx.span_id) + elif isinstance(self.parent, SpanContext): + parent_id = trace_api.format_span_id(self.parent.span_id) + + start_time = None + if self.start_time: + start_time = util.ns_to_iso_str(self.start_time) + + end_time = None + if self.end_time: + end_time = util.ns_to_iso_str(self.end_time) + + if self.status is not None: + status = OrderedDict() + status["canonical_code"] = str(self.status.canonical_code.name) + if self.status.description: + status["description"] = self.status.description + + f_span = OrderedDict() + + f_span["name"] = self.name + f_span["context"] = self._format_context(self.context) + f_span["kind"] = str(self.kind) + f_span["parent_id"] = parent_id + f_span["start_time"] = start_time + f_span["end_time"] = end_time + if self.status is not None: + f_span["status"] = status + f_span["attributes"] = self._format_attributes(self.attributes) + f_span["events"] = self._format_events(self.events) + f_span["links"] = self._format_links(self.links) + + return json.dumps(f_span, indent=4) def get_context(self): return self.context diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 515962ca36..fbe30720ea 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -270,7 +270,7 @@ class ConsoleSpanExporter(SpanExporter): def __init__( self, out: typing.IO = sys.stdout, - formatter: typing.Callable[[Span], str] = lambda span: str(span) + formatter: typing.Callable[[Span], str] = lambda span: span.to_json() + os.linesep, ): self.out = out diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 7a63963648..43b7893951 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -286,10 +286,10 @@ def test_export(self): # pylint: disable=no-self-use # Mocking stdout interferes with debugging and test reporting, mock on # the exporter instance instead. - span = trace.Span("span name", mock.Mock()) + span = trace.Span("span name", trace_api.INVALID_SPAN_CONTEXT) with mock.patch.object(exporter, "out") as mock_stdout: exporter.export([span]) - mock_stdout.write.assert_called_once_with(str(span) + os.linesep) + mock_stdout.write.assert_called_once_with(span.to_json() + os.linesep) self.assertEqual(mock_stdout.write.call_count, 1) self.assertEqual(mock_stdout.flush.call_count, 1) From 7cb57c78d38836c9b75de720f6a43d648ff600e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 22 Apr 2020 22:30:59 -0500 Subject: [PATCH 0303/1517] ext: Expect tracer provider instead of tracer in integrations (#602) Standardize the interface that trace providers are specified in integrations, as specified in #585. Adding a helper to create and return a configured TracerProvider with a the span processor and the memory exporter api: Add tracer provider parameter to trace.get_tracer(). This eliminates the need for a helper function and boilerplate code to retrieve the appropriate tracer from a passed tracer_provider. --- .../grpc/hello_world_client.py | 3 +- .../grpc/hello_world_server.py | 3 +- .../grpc/route_guide_client.py | 3 +- .../grpc/route_guide_server.py | 3 +- .../src/opentelemetry/ext/dbapi/__init__.py | 44 ++++++++++++++++--- .../tests/mysql/test_mysql_functional.py | 2 +- .../tests/postgres/test_psycopg_functional.py | 2 +- .../tests/pymongo/test_pymongo_functional.py | 2 +- .../src/opentelemetry/ext/grpc/__init__.py | 11 ++++- .../tests/test_server_interceptor.py | 26 +++++------ .../src/opentelemetry/ext/mysql/__init__.py | 17 ++++--- .../tests/test_mysql_integration.py | 33 +++++++++++--- .../opentelemetry/ext/psycopg2/__init__.py | 11 +++-- .../tests/test_psycopg2_integration.py | 4 +- .../src/opentelemetry/ext/pymongo/__init__.py | 16 ++++--- .../tests/test_pymongo.py | 2 +- .../src/opentelemetry/trace/__init__.py | 12 +++-- opentelemetry-api/tests/trace/test_globals.py | 4 ++ .../util/src/opentelemetry/test/test_base.py | 30 +++++++++++-- 19 files changed, 163 insertions(+), 65 deletions(-) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py index 2f2351b9af..7457c63148 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_client.py @@ -64,7 +64,6 @@ trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) -tracer = trace.get_tracer(__name__) def run(): @@ -73,7 +72,7 @@ def run(): # of the code. with grpc.insecure_channel("localhost:50051") as channel: - channel = intercept_channel(channel, client_interceptor(tracer)) + channel = intercept_channel(channel, client_interceptor()) stub = helloworld_pb2_grpc.GreeterStub(channel) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py index 86dcd66527..858426a468 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/hello_world_server.py @@ -64,7 +64,6 @@ trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) -tracer = trace.get_tracer(__name__) class Greeter(helloworld_pb2_grpc.GreeterServicer): @@ -75,7 +74,7 @@ def SayHello(self, request, context): def serve(): server = grpc.server(futures.ThreadPoolExecutor()) - server = intercept_server(server, server_interceptor(tracer)) + server = intercept_server(server, server_interceptor()) helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server) server.add_insecure_port("[::]:50051") diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py index 18391b4228..d24875913d 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_client.py @@ -67,7 +67,6 @@ trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) -tracer = trace.get_tracer(__name__) def make_route_note(message, latitude, longitude): @@ -154,7 +153,7 @@ def run(): # used in circumstances in which the with statement does not fit the needs # of the code. with grpc.insecure_channel("localhost:50051") as channel: - channel = intercept_channel(channel, client_interceptor(tracer)) + channel = intercept_channel(channel, client_interceptor()) stub = route_guide_pb2_grpc.RouteGuideStub(channel) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py index 9cd9db666e..54a68b9f5a 100755 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/route_guide_server.py @@ -69,7 +69,6 @@ trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) -tracer = trace.get_tracer(__name__) def get_feature(feature_db, point): @@ -164,7 +163,7 @@ def RouteChat(self, request_iterator, context): def serve(): server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) - server = intercept_server(server, server_interceptor(tracer)) + server = intercept_server(server, server_interceptor()) route_guide_pb2_grpc.add_RouteGuideServicer_to_server( RouteGuideServicer(), server diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index ed4e93b9a3..441434b189 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -24,15 +24,16 @@ import mysql.connector import pyodbc + from opentelemetry import trace from opentelemetry.ext.dbapi import trace_integration from opentelemetry.trace import TracerProvider trace.set_tracer_provider(TracerProvider()) - tracer = trace.get_tracer(__name__) + # Ex: mysql.connector - trace_integration(tracer, mysql.connector, "connect", "mysql", "sql") + trace_integration(mysql.connector, "connect", "mysql", "sql") # Ex: pyodbc - trace_integration(tracer, pyodbc, "Connection", "odbc", "sql") + trace_integration(pyodbc, "Connection", "odbc", "sql") API --- @@ -44,13 +45,44 @@ import wrapt -from opentelemetry.trace import SpanKind, Tracer +from opentelemetry.ext.dbapi.version import __version__ +from opentelemetry.trace import SpanKind, Tracer, TracerProvider, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode logger = logging.getLogger(__name__) def trace_integration( + connect_module: typing.Callable[..., any], + connect_method_name: str, + database_component: str, + database_type: str = "", + connection_attributes: typing.Dict = None, + tracer_provider: typing.Optional[TracerProvider] = None, +): + """Integrate with DB API library. + https://www.python.org/dev/peps/pep-0249/ + + Args: + connect_module: Module name where connect method is available. + connect_method_name: The connect method name. + database_component: Database driver name or database name "JDBI", "jdbc", "odbc", "postgreSQL". + database_type: The Database type. For any SQL database, "sql". + connection_attributes: Attribute names for database, port, host and user in Connection object. + tracer_provider: The :class:`TracerProvider` to use. If ommited the current configured one is used. + """ + tracer = get_tracer(__name__, __version__, tracer_provider) + wrap_connect( + tracer, + connect_module, + connect_method_name, + database_component, + database_type, + connection_attributes, + ) + + +def wrap_connect( tracer: Tracer, connect_module: typing.Callable[..., any], connect_method_name: str, @@ -71,7 +103,7 @@ def trace_integration( """ # pylint: disable=unused-argument - def wrap_connect( + def wrap_connect_( wrapped: typing.Callable[..., any], instance: typing.Any, args: typing.Tuple[any, any], @@ -87,7 +119,7 @@ def wrap_connect( try: wrapt.wrap_function_wrapper( - connect_module, connect_method_name, wrap_connect + connect_module, connect_method_name, wrap_connect_ ) except Exception as ex: # pylint: disable=broad-except logger.warning("Failed to integrate with DB API. %s", str(ex)) diff --git a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py index c0790396b0..36fd7bf3d3 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py @@ -35,7 +35,7 @@ def setUpClass(cls): cls._connection = None cls._cursor = None cls._tracer = cls.tracer_provider.get_tracer(__name__) - trace_integration(cls._tracer) + trace_integration(cls.tracer_provider) cls._connection = mysql.connector.connect( user=MYSQL_USER, password=MYSQL_PASSWORD, diff --git a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py index a0ddfcae15..c53baf81a6 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py @@ -35,7 +35,7 @@ def setUpClass(cls): cls._connection = None cls._cursor = None cls._tracer = cls.tracer_provider.get_tracer(__name__) - trace_integration(cls._tracer) + trace_integration(cls.tracer_provider) cls._connection = psycopg2.connect( dbname=POSTGRES_DB_NAME, user=POSTGRES_USER, diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py index 6f6a728e51..567e0d86a0 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py @@ -31,7 +31,7 @@ class TestFunctionalPymongo(TestBase): def setUpClass(cls): super().setUpClass() cls._tracer = cls.tracer_provider.get_tracer(__name__) - trace_integration(cls._tracer) + trace_integration(cls.tracer_provider) client = MongoClient( MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000 ) diff --git a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py index 8807abcb1f..0e9b19ef51 100644 --- a/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py +++ b/ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py @@ -17,8 +17,11 @@ # pylint:disable=no-name-in-module # pylint:disable=relative-beyond-top-level +from opentelemetry import trace +from opentelemetry.ext.grpc.version import __version__ -def client_interceptor(tracer): + +def client_interceptor(tracer_provider=None): """Create a gRPC client channel interceptor. Args: @@ -29,10 +32,12 @@ def client_interceptor(tracer): """ from . import _client + tracer = trace.get_tracer(__name__, __version__, tracer_provider) + return _client.OpenTelemetryClientInterceptor(tracer) -def server_interceptor(tracer): +def server_interceptor(tracer_provider=None): """Create a gRPC server interceptor. Args: @@ -43,4 +48,6 @@ def server_interceptor(tracer): """ from . import _server + tracer = trace.get_tracer(__name__, __version__, tracer_provider) + return _server.OpenTelemetryServerInterceptor(tracer) diff --git a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py index 74ce9babab..6c055c863c 100644 --- a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py +++ b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py @@ -20,6 +20,7 @@ import grpc +import opentelemetry.ext.grpc from opentelemetry import trace from opentelemetry.ext.grpc import server_interceptor from opentelemetry.ext.grpc.grpcext import intercept_server @@ -48,15 +49,11 @@ def service(self, handler_call_details): class TestOpenTelemetryServerInterceptor(TestBase): - def setUp(self): - super().setUp() - self.tracer = self.tracer_provider.get_tracer(__name__) - def test_create_span(self): """Check that the interceptor wraps calls with spans server-side.""" # Intercept gRPC calls... - interceptor = server_interceptor(self.tracer) + interceptor = server_interceptor() # No-op RPC handler def handler(request, context): @@ -87,18 +84,21 @@ def handler(request, context): self.assertEqual(span.name, "") self.assertIs(span.kind, trace.SpanKind.SERVER) + # Check version and name in span's instrumentation info + self.check_span_instrumentation_info(span, opentelemetry.ext.grpc) + def test_span_lifetime(self): """Check that the span is active for the duration of the call.""" - tracer_provider = trace_sdk.TracerProvider() - tracer = tracer_provider.get_tracer(__name__) - interceptor = server_interceptor(tracer) + interceptor = server_interceptor() + tracer = self.tracer_provider.get_tracer(__name__) # To capture the current span at the time the handler is called active_span_in_handler = None def handler(request, context): nonlocal active_span_in_handler + # The current span is shared among all the tracers. active_span_in_handler = tracer.get_current_span() return b"" @@ -128,10 +128,9 @@ def handler(request, context): def test_sequential_server_spans(self): """Check that sequential RPCs get separate server spans.""" - tracer_provider = trace_sdk.TracerProvider() - tracer = tracer_provider.get_tracer(__name__) + tracer = self.tracer_provider.get_tracer(__name__) - interceptor = server_interceptor(tracer) + interceptor = server_interceptor() # Capture the currently active span in each thread active_spans_in_handler = [] @@ -176,10 +175,9 @@ def test_concurrent_server_spans(self): context. """ - tracer_provider = trace_sdk.TracerProvider() - tracer = tracer_provider.get_tracer(__name__) + tracer = self.tracer_provider.get_tracer(__name__) - interceptor = server_interceptor(tracer) + interceptor = server_interceptor() # Capture the currently active span in each thread active_spans_in_handler = [] diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index 16b64655c7..2a8b2ab3a8 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -29,9 +29,8 @@ from opentelemetry.ext.mysql import trace_integration trace.set_tracer_provider(TracerProvider()) - tracer = trace.get_tracer(__name__) - trace_integration(tracer) + trace_integration() cnx = mysql.connector.connect(database='MySQL_Database') cursor = cnx.cursor() cursor.execute("INSERT INTO test (testField) VALUES (123)" @@ -42,23 +41,29 @@ --- """ +import typing + import mysql.connector -from opentelemetry.ext.dbapi import trace_integration as db_integration -from opentelemetry.trace import Tracer +from opentelemetry.ext.dbapi import wrap_connect +from opentelemetry.ext.mysql.version import __version__ +from opentelemetry.trace import TracerProvider, get_tracer -def trace_integration(tracer: Tracer): +def trace_integration(tracer_provider: typing.Optional[TracerProvider] = None): """Integrate with MySQL Connector/Python library. https://dev.mysql.com/doc/connector-python/en/ """ + + tracer = get_tracer(__name__, __version__, tracer_provider) + connection_attributes = { "database": "database", "port": "server_port", "host": "server_host", "user": "user", } - db_integration( + wrap_connect( tracer, mysql.connector, "connect", diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py index 150f9f51f9..39a70bc551 100644 --- a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -16,20 +16,43 @@ import mysql.connector -from opentelemetry.ext.mysql import trace_integration +import opentelemetry.ext.mysql +from opentelemetry.sdk import resources from opentelemetry.test.test_base import TestBase class TestMysqlIntegration(TestBase): def test_trace_integration(self): - tracer = self.tracer_provider.get_tracer(__name__) + with mock.patch("mysql.connector.connect") as mock_connect: + mock_connect.get.side_effect = mysql.connector.MySQLConnection() + opentelemetry.ext.mysql.trace_integration() + cnx = mysql.connector.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check version and name in span's instrumentation info + self.check_span_instrumentation_info(span, opentelemetry.ext.mysql) + + def test_custom_tracer_provider(self): + resource = resources.Resource.create({}) + result = self.create_tracer_provider(resource=resource) + tracer_provider, exporter = result with mock.patch("mysql.connector.connect") as mock_connect: mock_connect.get.side_effect = mysql.connector.MySQLConnection() - trace_integration(tracer) + opentelemetry.ext.mysql.trace_integration(tracer_provider) cnx = mysql.connector.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) - spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 1) + + span_list = exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + span = span_list[0] + + self.assertIs(span.resource, resource) diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py index 13c990dfbc..a0db2993ba 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/__init__.py @@ -29,8 +29,8 @@ from opentelemetry.trace.ext.psycopg2 import trace_integration trace.set_tracer_provider(TracerProvider()) - tracer = trace.get_tracer(__name__) - trace_integration(tracer) + + trace_integration() cnx = psycopg2.connect(database='Database') cursor = cnx.cursor() cursor.execute("INSERT INTO test (testField) VALUES (123)") @@ -49,7 +49,8 @@ from psycopg2.sql import Composable from opentelemetry.ext.dbapi import DatabaseApiIntegration, TracedCursor -from opentelemetry.trace import Tracer +from opentelemetry.ext.psycopg2.version import __version__ +from opentelemetry.trace import Tracer, get_tracer logger = logging.getLogger(__name__) @@ -57,11 +58,13 @@ DATABASE_TYPE = "sql" -def trace_integration(tracer): +def trace_integration(tracer_provider=None): """Integrate with PostgreSQL Psycopg library. Psycopg: http://initd.org/psycopg/ """ + tracer = get_tracer(__name__, __version__, tracer_provider) + connection_attributes = { "database": "info.dbname", "port": "info.port", diff --git a/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py b/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py index a512ba4fa5..b4724e308b 100644 --- a/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py +++ b/ext/opentelemetry-ext-psycopg2/tests/test_psycopg2_integration.py @@ -17,14 +17,12 @@ import psycopg2 -from opentelemetry import trace as trace_api from opentelemetry.ext.psycopg2 import trace_integration class TestPostgresqlIntegration(unittest.TestCase): def test_trace_integration(self): - tracer = trace_api.DefaultTracer() with mock.patch("psycopg2.connect"): - trace_integration(tracer) + trace_integration() cnx = psycopg2.connect(database="test") self.assertIsNotNone(cnx.cursor_factory) diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index 4ab87158be..b85bf42379 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -29,9 +29,8 @@ from opentelemetry.trace.ext.pymongo import trace_integration trace.set_tracer_provider(TracerProvider()) - tracer = trace.get_tracer(__name__) - trace_integration(tracer) + trace_integration() client = MongoClient() db = client["MongoDB_Database"] collection = db["MongoDB_Collection"] @@ -43,25 +42,30 @@ from pymongo import monitoring -from opentelemetry.trace import SpanKind +from opentelemetry.ext.pymongo.version import __version__ +from opentelemetry.trace import SpanKind, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode DATABASE_TYPE = "mongodb" COMMAND_ATTRIBUTES = ["filter", "sort", "skip", "limit", "pipeline"] -def trace_integration(tracer=None): +def trace_integration(tracer_provider=None): """Integrate with pymongo to trace it using event listener. https://api.mongodb.com/python/current/api/pymongo/monitoring.html + + Args: + tracer_provider: The `TracerProvider` to use. If none is passed the + current configured one is used. """ + tracer = get_tracer(__name__, __version__, tracer_provider) + monitoring.register(CommandTracer(tracer)) class CommandTracer(monitoring.CommandListener): def __init__(self, tracer): - if tracer is None: - raise ValueError("The tracer is not provided.") self._tracer = tracer self._span_dict = {} diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py index 197c4d0266..d85b23bfc9 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py @@ -30,7 +30,7 @@ def test_trace_integration(self): "pymongo.monitoring.register", side_effect=mock_register ) with patch: - trace_integration(self.tracer) + trace_integration(self.tracer_provider) self.assertTrue(mock_register.called) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 773a3908ce..a4b18a3ddc 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -677,14 +677,20 @@ def use_span( def get_tracer( - instrumenting_module_name: str, instrumenting_library_version: str = "" + instrumenting_module_name: str, + instrumenting_library_version: str = "", + tracer_provider: typing.Optional[TracerProvider] = None, ) -> "Tracer": """Returns a `Tracer` for use by the given instrumentation library. This function is a convenience wrapper for - opentelemetry.trace.get_tracer_provider().get_tracer + opentelemetry.trace.TracerProvider.get_tracer. + + If tracer_provider is ommited the current configured one is used. """ - return get_tracer_provider().get_tracer( + if tracer_provider is None: + tracer_provider = get_tracer_provider() + return tracer_provider.get_tracer( instrumenting_module_name, instrumenting_library_version ) diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 2e0339b99d..4f38f99ee8 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -16,3 +16,7 @@ def test_get_tracer(self): """trace.get_tracer should proxy to the global tracer provider.""" trace.get_tracer("foo", "var") self._mock_tracer_provider.get_tracer.assert_called_with("foo", "var") + + mock_provider = unittest.mock.Mock() + trace.get_tracer("foo", "var", mock_provider) + mock_provider.get_tracer.assert_called_with("foo", "var") diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index d35eef650a..787439b1d7 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -24,12 +24,10 @@ class TestBase(unittest.TestCase): @classmethod def setUpClass(cls): - cls.tracer_provider = TracerProvider() cls.original_provider = trace_api.get_tracer_provider() + result = cls.create_tracer_provider() + cls.tracer_provider, cls.memory_exporter = result trace_api.set_tracer_provider(cls.tracer_provider) - cls.memory_exporter = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(cls.memory_exporter) - cls.tracer_provider.add_span_processor(span_processor) @classmethod def tearDownClass(cls): @@ -37,3 +35,27 @@ def tearDownClass(cls): def setUp(self): self.memory_exporter.clear() + + def check_span_instrumentation_info(self, span, module): + self.assertEqual(span.instrumentation_info.name, module.__name__) + self.assertEqual(span.instrumentation_info.version, module.__version__) + + @staticmethod + def create_tracer_provider(**kwargs): + """Helper to create a configured tracer provider. + + Creates and configures a `TracerProvider` with a + `SimpleExportSpanProcessor` and a `InMemorySpanExporter`. + All the parameters passed are forwarded to the TracerProvider + constructor. + + Returns: + A list with the tracer provider in the first element and the + memory exporter in the second. + """ + tracer_provider = TracerProvider(**kwargs) + memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(memory_exporter) + tracer_provider.add_span_processor(span_processor) + + return tracer_provider, memory_exporter From 216bd5a0ab030f13b745b4a7daa7bf5b01b6162e Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 24 Apr 2020 13:18:02 -0700 Subject: [PATCH 0304/1517] PyMySQL Integration (#504) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integration for PyMySQL. Fixes some documentation as well for other db integrations. Leverages dbapi. Co-authored-by: Mauricio Vásquez --- docs/ext/pymysql/pymysql.rst | 7 ++ .../src/opentelemetry/ext/dbapi/__init__.py | 3 + .../tests/pymysql/test_pymysql_functional.py | 108 ++++++++++++++++++ ext/opentelemetry-ext-mysql/setup.cfg | 1 + ext/opentelemetry-ext-pymysql/CHANGELOG.md | 3 + ext/opentelemetry-ext-pymysql/README.rst | 24 ++++ ext/opentelemetry-ext-pymysql/setup.cfg | 48 ++++++++ ext/opentelemetry-ext-pymysql/setup.py | 26 +++++ .../src/opentelemetry/ext/pymysql/__init__.py | 68 +++++++++++ .../src/opentelemetry/ext/pymysql/version.py | 15 +++ .../tests/__init__.py | 0 .../tests/test_pymysql_integration.py | 38 ++++++ tox.ini | 13 ++- 13 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 docs/ext/pymysql/pymysql.rst create mode 100644 ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py create mode 100644 ext/opentelemetry-ext-pymysql/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-pymysql/README.rst create mode 100644 ext/opentelemetry-ext-pymysql/setup.cfg create mode 100644 ext/opentelemetry-ext-pymysql/setup.py create mode 100644 ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py create mode 100644 ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py create mode 100644 ext/opentelemetry-ext-pymysql/tests/__init__.py create mode 100644 ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py diff --git a/docs/ext/pymysql/pymysql.rst b/docs/ext/pymysql/pymysql.rst new file mode 100644 index 0000000000..23dca80c4f --- /dev/null +++ b/docs/ext/pymysql/pymysql.rst @@ -0,0 +1,7 @@ +OpenTelemetry PyMySQL Integration +================================= + +.. automodule:: opentelemetry.ext.pymysql + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index 441434b189..7a94b58957 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -178,6 +178,9 @@ def get_connection_attributes(self, connection): self.name = self.database_component self.database = self.connection_props.get("database", "") if self.database: + # PyMySQL encodes names with utf-8 + if hasattr(self.database, "decode"): + self.database = self.database.decode(errors="ignore") self.name += "." + self.database user = self.connection_props.get("user") if user is not None: diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py new file mode 100644 index 0000000000..e4ee65af30 --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py @@ -0,0 +1,108 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import time +import unittest + +import pymysql as pymy + +from opentelemetry import trace as trace_api +from opentelemetry.ext.pymysql import trace_integration +from opentelemetry.sdk.trace import Tracer, TracerProvider +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +MYSQL_USER = os.getenv("MYSQL_USER ", "testuser") +MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD ", "testpassword") +MYSQL_HOST = os.getenv("MYSQL_HOST ", "localhost") +MYSQL_PORT = int(os.getenv("MYSQL_PORT ", "3306")) +MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME ", "opentelemetry-tests") + + +class TestFunctionalPyMysql(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls._connection = None + cls._cursor = None + cls._tracer_provider = TracerProvider() + cls._tracer = Tracer(cls._tracer_provider, None) + cls._span_exporter = InMemorySpanExporter() + cls._span_processor = SimpleExportSpanProcessor(cls._span_exporter) + cls._tracer_provider.add_span_processor(cls._span_processor) + trace_integration(cls._tracer_provider) + cls._connection = pymy.connect( + user=MYSQL_USER, + password=MYSQL_PASSWORD, + host=MYSQL_HOST, + port=MYSQL_PORT, + database=MYSQL_DB_NAME, + ) + cls._cursor = cls._connection.cursor() + + @classmethod + def tearDownClass(cls): + if cls._connection: + cls._connection.close() + + def setUp(self): + self._span_exporter.clear() + + def validate_spans(self): + spans = self._span_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + for span in spans: + if span.name == "rootSpan": + root_span = span + else: + db_span = span + self.assertIsInstance(span.start_time, int) + self.assertIsInstance(span.end_time, int) + self.assertIsNotNone(root_span) + self.assertIsNotNone(db_span) + self.assertEqual(root_span.name, "rootSpan") + self.assertEqual(db_span.name, "mysql.opentelemetry-tests") + self.assertIsNotNone(db_span.parent) + self.assertEqual(db_span.parent.name, root_span.name) + self.assertIs(db_span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual(db_span.attributes["db.instance"], MYSQL_DB_NAME) + self.assertEqual(db_span.attributes["net.peer.name"], MYSQL_HOST) + self.assertEqual(db_span.attributes["net.peer.port"], MYSQL_PORT) + + def test_execute(self): + """Should create a child span for execute + """ + with self._tracer.start_as_current_span("rootSpan"): + self._cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") + self.validate_spans() + + def test_executemany(self): + """Should create a child span for executemany + """ + with self._tracer.start_as_current_span("rootSpan"): + data = ["1", "2", "3"] + stmt = "INSERT INTO test (id) VALUES (%s)" + self._cursor.executemany(stmt, data) + self.validate_spans() + + def test_callproc(self): + """Should create a child span for callproc + """ + with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( + Exception + ): + self._cursor.callproc("test", ()) + self.validate_spans() diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 3da2aad0f0..0b592641ce 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -41,6 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.7.dev0 + opentelemetry-ext-dbapi == 0.7.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-pymysql/CHANGELOG.md b/ext/opentelemetry-ext-pymysql/CHANGELOG.md new file mode 100644 index 0000000000..1512c42162 --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +## Unreleased diff --git a/ext/opentelemetry-ext-pymysql/README.rst b/ext/opentelemetry-ext-pymysql/README.rst new file mode 100644 index 0000000000..3cf845366b --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/README.rst @@ -0,0 +1,24 @@ +OpenTelemetry PyMySQL integration +================================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-pymysql.svg + :target: https://pypi.org/project/opentelemetry-ext-pymysql/ + +Integration with PyMySQL that supports the PyMySQL library and is +specified to trace_integration using 'PyMySQL'. + + +Installation +------------ + +:: + + pip install opentelemetry-ext-pymysql + + +References +---------- +* `OpenTelemetry PyMySQL Integration `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-pymysql/setup.cfg b/ext/opentelemetry-ext-pymysql/setup.cfg new file mode 100644 index 0000000000..9f44953a40 --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/setup.cfg @@ -0,0 +1,48 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-pymysql +description = OpenTelemetry PyMySQL integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-pymysql +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.7.dev0 + opentelemetry-ext-dbapi == 0.7.dev0 + PyMySQL ~= 0.9.3 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-pymysql/setup.py b/ext/opentelemetry-ext-pymysql/setup.py new file mode 100644 index 0000000000..a3f057b310 --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "pymysql", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py new file mode 100644 index 0000000000..0f06578e0b --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py @@ -0,0 +1,68 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The integration with PyMySQL supports the `PyMySQL`_ library and is specified +to ``trace_integration`` using ``'PyMySQL'``. + +.. _PyMySQL: https://pypi.org/project/PyMySQL/ + +Usage +----- + +.. code:: python + + import pymysql + from opentelemetry import trace + from opentelemetry.ext.pymysql import trace_integration + from opentelemetry.sdk.trace import TracerProvider + + trace.set_tracer_provider(TracerProvider()) + trace_integration() + cnx = pymysql.connect(database="MySQL_Database") + cursor = cnx.cursor() + cursor.execute("INSERT INTO test (testField) VALUES (123)" + cnx.commit() + cursor.close() + cnx.close() + +API +--- +""" + +import typing + +import pymysql + +from opentelemetry.ext.dbapi import wrap_connect +from opentelemetry.ext.pymysql.version import __version__ +from opentelemetry.trace import TracerProvider, get_tracer + + +def trace_integration(tracer_provider: typing.Optional[TracerProvider] = None): + """Integrate with the PyMySQL library. + https://github.com/PyMySQL/PyMySQL/ + """ + + tracer = get_tracer(__name__, __version__, tracer_provider) + + connection_attributes = { + "database": "db", + "port": "port", + "host": "host", + "user": "user", + } + wrap_connect( + tracer, pymysql, "connect", "mysql", "sql", connection_attributes + ) diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py new file mode 100644 index 0000000000..86c61362ab --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-pymysql/tests/__init__.py b/ext/opentelemetry-ext-pymysql/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py b/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py new file mode 100644 index 0000000000..c452c31ec6 --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py @@ -0,0 +1,38 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import mock + +import pymysql + +import opentelemetry.ext.pymysql +from opentelemetry.ext.pymysql import trace_integration +from opentelemetry.test.test_base import TestBase + + +class TestPyMysqlIntegration(TestBase): + def test_trace_integration(self): + with mock.patch("pymysql.connect"): + trace_integration() + cnx = pymysql.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check version and name in span's instrumentation info + self.check_span_instrumentation_info(span, opentelemetry.ext.pymysql) diff --git a/tox.ini b/tox.ini index dc7f20aa8d..e0c4314bc0 100644 --- a/tox.ini +++ b/tox.ini @@ -64,6 +64,10 @@ envlist = py3{4,5,6,7,8}-test-ext-pymongo pypy3-test-ext-pymongo + ; opentelemetry-ext-pymysql + py3{4,5,6,7,8}-test-ext-pymysql + pypy3-test-ext-pymysql + ; opentelemetry-ext-wsgi py3{4,5,6,7,8}-test-ext-wsgi pypy3-test-ext-wsgi @@ -120,6 +124,7 @@ changedir = test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests + test-ext-pymysql: ext/opentelemetry-ext-pymysql/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests test-ext-flask: ext/opentelemetry-ext-flask/tests @@ -171,6 +176,9 @@ commands_pre = psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-psycopg2 + pymysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi + pymysql: pip install {toxinidir}/ext/opentelemetry-ext-pymysql + http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests[test] jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger @@ -232,6 +240,7 @@ deps = thrift pymongo flask + pymysql mysql-connector-python wrapt psycopg2-binary @@ -268,6 +277,7 @@ deps = docker-compose >= 1.25.2 mysql-connector-python ~= 8.0 pymongo ~= 3.1 + pymysql ~= 0.9.3 psycopg2-binary ~= 2.8.4 changedir = @@ -280,7 +290,8 @@ commands_pre = -e {toxinidir}/ext/opentelemetry-ext-dbapi \ -e {toxinidir}/ext/opentelemetry-ext-mysql \ -e {toxinidir}/ext/opentelemetry-ext-psycopg2 \ - -e {toxinidir}/ext/opentelemetry-ext-pymongo + -e {toxinidir}/ext/opentelemetry-ext-pymongo \ + -e {toxinidir}/ext/opentelemetry-ext-pymysql docker-compose up -d python check_availability.py commands = From 5f188f777dccd82af3fa9b13a1142762efcfa708 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Fri, 24 Apr 2020 21:26:52 -0700 Subject: [PATCH 0305/1517] sdk: span parents are now always spancontext (#548) Exporter and span handling complexity was increasing due to the Span.parent attribute being either a Span or a SpanContext. As there is no requirement in the specification to have the parent attribute be a Span, removing this complexity and handling. Co-authored-by: Chris Kleinknecht Co-authored-by: Alex Boten --- .../tests/mysql/test_mysql_functional.py | 2 +- .../tests/postgres/test_psycopg_functional.py | 2 +- .../tests/pymongo/test_pymongo_functional.py | 2 +- .../tests/pymysql/test_pymysql_functional.py | 2 +- .../src/opentelemetry/ext/jaeger/__init__.py | 6 +----- .../tests/test_shim.py | 11 ++++++++--- .../ext/otcollector/trace_exporter/__init__.py | 17 ++--------------- .../tests/test_otcollector_trace_exporter.py | 5 ++++- .../src/opentelemetry/sdk/trace/__init__.py | 10 +++++----- opentelemetry-sdk/tests/context/test_asyncio.py | 2 +- opentelemetry-sdk/tests/trace/test_trace.py | 4 ++-- 11 files changed, 27 insertions(+), 36 deletions(-) diff --git a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py index 36fd7bf3d3..d0261d2f63 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py @@ -65,7 +65,7 @@ def validate_spans(self): self.assertEqual(root_span.name, "rootSpan") self.assertEqual(db_span.name, "mysql.opentelemetry-tests") self.assertIsNotNone(db_span.parent) - self.assertEqual(db_span.parent.name, root_span.name) + self.assertIs(db_span.parent, root_span.get_context()) self.assertIs(db_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(db_span.attributes["db.instance"], MYSQL_DB_NAME) self.assertEqual(db_span.attributes["net.peer.name"], MYSQL_HOST) diff --git a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py index c53baf81a6..0a43926145 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py @@ -68,7 +68,7 @@ def validate_spans(self): self.assertEqual(root_span.name, "rootSpan") self.assertEqual(child_span.name, "postgresql.opentelemetry-tests") self.assertIsNotNone(child_span.parent) - self.assertEqual(child_span.parent.name, root_span.name) + self.assertIs(child_span.parent, root_span.get_context()) self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual( child_span.attributes["db.instance"], POSTGRES_DB_NAME diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py index 567e0d86a0..1b2e269482 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py @@ -51,7 +51,7 @@ def validate_spans(self): self.assertIsNot(root_span, None) self.assertIsNot(pymongo_span, None) self.assertIsNotNone(pymongo_span.parent) - self.assertEqual(pymongo_span.parent.name, root_span.name) + self.assertIs(pymongo_span.parent, root_span.get_context()) self.assertIs(pymongo_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual( pymongo_span.attributes["db.instance"], MONGODB_DB_NAME diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py index e4ee65af30..a7dc213655 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py @@ -76,7 +76,7 @@ def validate_spans(self): self.assertEqual(root_span.name, "rootSpan") self.assertEqual(db_span.name, "mysql.opentelemetry-tests") self.assertIsNotNone(db_span.parent) - self.assertEqual(db_span.parent.name, root_span.name) + self.assertIs(db_span.parent, root_span.get_context()) self.assertIs(db_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(db_span.attributes["db.instance"], MYSQL_DB_NAME) self.assertEqual(db_span.attributes["net.peer.name"], MYSQL_HOST) diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py index 8a8d40b260..23ed6e725d 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -194,11 +194,7 @@ def _translate_to_jaeger(spans: Span): status = span.status - parent_id = 0 - if isinstance(span.parent, trace_api.Span): - parent_id = span.parent.get_context().span_id - elif isinstance(span.parent, trace_api.SpanContext): - parent_id = span.parent.span_id + parent_id = span.parent.span_id if span.parent else 0 tags = _extract_tags(span.attributes) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index 75e9886079..7a0913f973 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -281,7 +281,8 @@ def test_parent_child_implicit(self): self.assertEqual(parent_trace_id, child_trace_id) self.assertEqual( - child.span.unwrap().parent, parent.span.unwrap() + child.span.unwrap().parent, + parent.span.unwrap().get_context(), ) # Verify parent span becomes the active span again. @@ -309,7 +310,9 @@ def test_parent_child_explicit_span(self): child_trace_id = child.span.unwrap().get_context().trace_id self.assertEqual(child_trace_id, parent_trace_id) - self.assertEqual(child.span.unwrap().parent, parent.unwrap()) + self.assertEqual( + child.span.unwrap().parent, parent.unwrap().get_context() + ) with self.shim.start_span("ParentSpan") as parent: child = self.shim.start_span("ChildSpan", child_of=parent) @@ -318,7 +321,9 @@ def test_parent_child_explicit_span(self): child_trace_id = child.unwrap().get_context().trace_id self.assertEqual(child_trace_id, parent_trace_id) - self.assertEqual(child.unwrap().parent, parent.unwrap()) + self.assertEqual( + child.unwrap().parent, parent.unwrap().get_context() + ) child.finish() diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py index d287bcfb4a..fb6237e86d 100644 --- a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py @@ -109,9 +109,7 @@ def translate_to_collector(spans: Sequence[Span]): ) parent_id = 0 - if isinstance(span.parent, trace_api.Span): - parent_id = span.parent.get_context().span_id - elif isinstance(span.parent, trace_api.SpanContext): + if span.parent is not None: parent_id = span.parent.span_id collector_span.parent_span_id = parent_id.to_bytes(8, "big") @@ -157,18 +155,7 @@ def translate_to_collector(spans: Sequence[Span]): collector_span_link.type = ( trace_pb2.Span.Link.Type.TYPE_UNSPECIFIED ) - - if isinstance(span.parent, trace_api.Span): - if ( - link.context.span_id - == span.parent.get_context().span_id - and link.context.trace_id - == span.parent.get_context().trace_id - ): - collector_span_link.type = ( - trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN - ) - elif isinstance(span.parent, trace_api.SpanContext): + if span.parent is not None: if ( link.context.span_id == span.parent.span_id and link.context.trace_id == span.parent.trace_id diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py index 74c1d08872..4a0a556137 100644 --- a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_trace_exporter.py @@ -135,7 +135,10 @@ def test_translate_to_collector(self): kind=trace_api.SpanKind.SERVER, ) span_3 = trace.Span( - name="test3", context=other_context, links=(link_2,), parent=span_2 + name="test3", + context=other_context, + links=(link_2,), + parent=span_2.get_context(), ) otel_spans = [span_1, span_2, span_3] otel_spans[0].start(start_time=start_times[0]) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 3d2fc96c1b..5eff5e6130 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -198,8 +198,8 @@ class Span(trace_api.Span): Args: name: The name of the operation this span represents context: The immutable span context - parent: This span's parent, may be a `SpanContext` if the parent is - remote, null if this is a root span + parent: This span's parent's `SpanContext`, or + null if this is a root span sampler: The sampler used to create this span trace_config: TODO resource: Entity producing telemetry @@ -219,7 +219,7 @@ def __init__( self, name: str, context: trace_api.SpanContext, - parent: trace_api.ParentSpan = None, + parent: Optional[trace_api.SpanContext] = None, sampler: Optional[sampling.Sampler] = None, trace_config: None = None, # TODO resource: None = None, @@ -594,7 +594,7 @@ def start_span( # pylint: disable=too-many-locals if parent_context is not None and not isinstance( parent_context, trace_api.SpanContext ): - raise TypeError + raise TypeError("parent must be a Span, SpanContext or None.") if parent_context is None or not parent_context.is_valid(): parent = parent_context = None @@ -640,7 +640,7 @@ def start_span( # pylint: disable=too-many-locals span = Span( name=name, context=context, - parent=parent, + parent=parent_context, sampler=self.source.sampler, resource=self.source.resource, attributes=span_attributes, diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index f213753c59..4fa653205b 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -108,4 +108,4 @@ def test_with_asyncio(self): for span in span_list: if span is expected_parent: continue - self.assertEqual(span.parent, expected_parent) + self.assertEqual(span.parent, expected_parent.get_context()) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index f9db925f11..1094f1afb9 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -243,7 +243,7 @@ def test_start_span_implicit(self): with tracer.start_span( "child", kind=trace_api.SpanKind.CLIENT ) as child: - self.assertIs(child.parent, root) + self.assertIs(child.parent, root.get_context()) self.assertEqual(child.kind, trace_api.SpanKind.CLIENT) self.assertIsNotNone(child.start_time) @@ -332,7 +332,7 @@ def test_start_as_current_span_implicit(self): with tracer.start_as_current_span("child") as child: self.assertIs(tracer.get_current_span(), child) - self.assertIs(child.parent, root) + self.assertIs(child.parent, root.get_context()) # After exiting the child's scope the parent should become the # current span again. From 490951ab205c59828cd03e32708ab6cf40f31159 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 27 Apr 2020 09:29:34 -0600 Subject: [PATCH 0306/1517] test: Add missing dependency (#614) Fixes #613 some packages depend on the newest dev version of dbapi, which isn't published yet when a package in a virtualenv is installed, it will attempt to install those dependencies, which searches on pypi Installing the local package ensures that 0.7 is installed, so pip sees that and skips trying to resolve against pypi (when it won't find the version). --- eachdist.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/eachdist.ini b/eachdist.ini index 59f212f7c7..b573b838fd 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -6,6 +6,7 @@ sortfirst= opentelemetry-sdk opentelemetry-auto-instrumentation ext/opentelemetry-ext-wsgi + ext/opentelemetry-ext-dbapi ext/* [lintroots] From 8f0e5845df7af4a73378c43426960c45830455d8 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 27 Apr 2020 09:22:18 -0700 Subject: [PATCH 0307/1517] tests: pin pylint (#618) Pinning the version of pylint as it's causing failures in our build. Related issue: PyCQA/pylint#3524 --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 7854f313b8..24da70defa 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,4 @@ -pylint~=2.3 +pylint==2.4.4 flake8~=3.7 isort~=4.3 black>=19.3b0,==19.* From a832bb08abf97638a59848774b5b83d0c84331e2 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 27 Apr 2020 09:48:39 -0700 Subject: [PATCH 0308/1517] redis: Porting redis instrumentation from contrib repo (#595) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Porting the existing redis instrumentation from the contrib repo to using the OpenTelemetry API and the OpenTelemetry Auto-instrumentation Instrumentor interface. Similiar to the sqlalchemy PR, the main thing that will need updating is to remove the patch/unpatch methods once the instrumentor interface changes have been merged. This is replacing open-telemetry/opentelemetry-python-contrib#21 Co-authored-by: Mauricio Vásquez Co-authored-by: Leighton Chen Co-authored-by: Diego Hurtado Co-authored-by: Chris Kleinknecht --- .isort.cfg | 2 +- docs/ext/redis/redis.rst | 7 + .../tests/check_availability.py | 107 ++++++------ .../tests/docker-compose.yml | 4 + .../tests/redis/test_redis_functional.py | 120 +++++++++++++ ext/opentelemetry-ext-redis/CHANGELOG.md | 5 + ext/opentelemetry-ext-redis/MANIFEST.in | 9 + ext/opentelemetry-ext-redis/README.rst | 23 +++ ext/opentelemetry-ext-redis/setup.cfg | 58 +++++++ ext/opentelemetry-ext-redis/setup.py | 26 +++ .../src/opentelemetry/ext/redis/__init__.py | 163 ++++++++++++++++++ .../src/opentelemetry/ext/redis/util.py | 57 ++++++ .../src/opentelemetry/ext/redis/version.py | 15 ++ ext/opentelemetry-ext-redis/tests/__init__.py | 13 ++ .../tests/test_redis.py | 51 ++++++ tox.ini | 16 +- 16 files changed, 623 insertions(+), 53 deletions(-) create mode 100644 docs/ext/redis/redis.rst create mode 100644 ext/opentelemetry-ext-docker-tests/tests/redis/test_redis_functional.py create mode 100644 ext/opentelemetry-ext-redis/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-redis/MANIFEST.in create mode 100644 ext/opentelemetry-ext-redis/README.rst create mode 100644 ext/opentelemetry-ext-redis/setup.cfg create mode 100644 ext/opentelemetry-ext-redis/setup.py create mode 100644 ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/__init__.py create mode 100644 ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/util.py create mode 100644 ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py create mode 100644 ext/opentelemetry-ext-redis/tests/__init__.py create mode 100644 ext/opentelemetry-ext-redis/tests/test_redis.py diff --git a/.isort.cfg b/.isort.cfg index c5723b06a2..ae2dfc3252 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -15,4 +15,4 @@ multi_line_output=3 skip=target skip_glob=**/gen/*,.venv*/*,venv*/* known_first_party=opentelemetry,opentelemetry_example_app -known_third_party=psutil,pytest +known_third_party=psutil,pytest,redis,redis_opentracing diff --git a/docs/ext/redis/redis.rst b/docs/ext/redis/redis.rst new file mode 100644 index 0000000000..38a72ad52f --- /dev/null +++ b/docs/ext/redis/redis.rst @@ -0,0 +1,7 @@ +OpenTelemetry Redis Instrumentation +=================================== + +.. automodule:: opentelemetry.ext.redis + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-docker-tests/tests/check_availability.py b/ext/opentelemetry-ext-docker-tests/tests/check_availability.py index 0b4376030d..91b8e5539d 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/check_availability.py +++ b/ext/opentelemetry-ext-docker-tests/tests/check_availability.py @@ -18,6 +18,7 @@ import mysql.connector import psycopg2 import pymongo +import redis MONGODB_COLLECTION_NAME = "test" MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME", "opentelemetry-tests") @@ -33,69 +34,74 @@ POSTGRES_PASSWORD = os.getenv("POSTGRESQL_HOST", "testpassword") POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT", "5432")) POSTGRES_USER = os.getenv("POSTGRESQL_HOST", "testuser") +REDIS_HOST = os.getenv("REDIS_HOST", "localhost") +REDIS_PORT = int(os.getenv("REDIS_PORT ", "6379")) RETRY_COUNT = 5 RETRY_INTERVAL = 5 # Seconds logger = logging.getLogger(__name__) +def retryable(func): + def wrapper(): + # Try to connect to DB + for i in range(RETRY_COUNT): + try: + func() + return + except Exception as ex: # pylint: disable=broad-except + logger.error( + "waiting for %s, retry %d/%d [%s]", + func.__name__, + i + 1, + RETRY_COUNT, + ex, + ) + time.sleep(RETRY_INTERVAL) + raise Exception("waiting for {} failed".format(func.__name__)) + + return wrapper + + +@retryable def check_pymongo_connection(): - # Try to connect to DB - for i in range(RETRY_COUNT): - try: - client = pymongo.MongoClient( - MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000 - ) - db = client[MONGODB_DB_NAME] - collection = db[MONGODB_COLLECTION_NAME] - collection.find_one() - client.close() - break - except Exception as ex: - if i == RETRY_COUNT - 1: - raise (ex) - logger.exception(ex) - time.sleep(RETRY_INTERVAL) + client = pymongo.MongoClient( + MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000 + ) + db = client[MONGODB_DB_NAME] + collection = db[MONGODB_COLLECTION_NAME] + collection.find_one() + client.close() +@retryable def check_mysql_connection(): - # Try to connect to DB - for i in range(RETRY_COUNT): - try: - connection = mysql.connector.connect( - user=MYSQL_USER, - password=MYSQL_PASSWORD, - host=MYSQL_HOST, - port=MYSQL_PORT, - database=MYSQL_DB_NAME, - ) - connection.close() - break - except Exception as ex: - if i == RETRY_COUNT - 1: - raise (ex) - logger.exception(ex) - time.sleep(RETRY_INTERVAL) + connection = mysql.connector.connect( + user=MYSQL_USER, + password=MYSQL_PASSWORD, + host=MYSQL_HOST, + port=MYSQL_PORT, + database=MYSQL_DB_NAME, + ) + connection.close() +@retryable def check_postgres_connection(): - # Try to connect to DB - for i in range(RETRY_COUNT): - try: - connection = psycopg2.connect( - dbname=POSTGRES_DB_NAME, - user=POSTGRES_USER, - password=POSTGRES_PASSWORD, - host=POSTGRES_HOST, - port=POSTGRES_PORT, - ) - connection.close() - break - except Exception as ex: - if i == RETRY_COUNT - 1: - raise (ex) - logger.exception(ex) - time.sleep(RETRY_INTERVAL) + connection = psycopg2.connect( + dbname=POSTGRES_DB_NAME, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD, + host=POSTGRES_HOST, + port=POSTGRES_PORT, + ) + connection.close() + + +@retryable +def check_redis_connection(): + connection = redis.Redis(host=REDIS_HOST, port=REDIS_PORT) + connection.hgetall("*") def check_docker_services_availability(): @@ -103,6 +109,7 @@ def check_docker_services_availability(): check_pymongo_connection() check_mysql_connection() check_postgres_connection() + check_redis_connection() check_docker_services_availability() diff --git a/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml b/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml index c2632dc247..3e642c584e 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml +++ b/ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml @@ -23,4 +23,8 @@ services: POSTGRES_USER: testuser POSTGRES_PASSWORD: testpassword POSTGRES_DB: opentelemetry-tests + otredis: + image: redis:4.0-alpine + ports: + - "127.0.0.1:6379:6379" diff --git a/ext/opentelemetry-ext-docker-tests/tests/redis/test_redis_functional.py b/ext/opentelemetry-ext-docker-tests/tests/redis/test_redis_functional.py new file mode 100644 index 0000000000..45283c442c --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/redis/test_redis_functional.py @@ -0,0 +1,120 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import redis + +from opentelemetry import trace +from opentelemetry.ext.redis import RedisInstrumentor +from opentelemetry.test.test_base import TestBase + + +class TestRedisInstrument(TestBase): + + test_service = "redis" + + def setUp(self): + super().setUp() + self.redis_client = redis.Redis(port=6379) + self.redis_client.flushall() + RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) + + def tearDown(self): + super().tearDown() + RedisInstrumentor().uninstrument() + + def _check_span(self, span): + self.assertEqual(span.attributes["service"], self.test_service) + self.assertEqual(span.name, "redis.command") + self.assertIs( + span.status.canonical_code, trace.status.StatusCanonicalCode.OK + ) + self.assertEqual(span.attributes.get("db.instance"), 0) + self.assertEqual( + span.attributes.get("db.url"), "redis://localhost:6379" + ) + + def test_long_command(self): + self.redis_client.mget(*range(1000)) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self._check_span(span) + self.assertTrue( + span.attributes.get("db.statement").startswith("MGET 0 1 2 3") + ) + self.assertTrue(span.attributes.get("db.statement").endswith("...")) + + def test_basics(self): + self.assertIsNone(self.redis_client.get("cheese")) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self._check_span(span) + self.assertEqual(span.attributes.get("db.statement"), "GET cheese") + self.assertEqual(span.attributes.get("redis.args_length"), 2) + + def test_pipeline_traced(self): + with self.redis_client.pipeline(transaction=False) as pipeline: + pipeline.set("blah", 32) + pipeline.rpush("foo", "éé") + pipeline.hgetall("xxx") + pipeline.execute() + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self._check_span(span) + self.assertEqual( + span.attributes.get("db.statement"), + "SET blah 32\nRPUSH foo éé\nHGETALL xxx", + ) + self.assertEqual(span.attributes.get("redis.pipeline_length"), 3) + + def test_pipeline_immediate(self): + with self.redis_client.pipeline() as pipeline: + pipeline.set("a", 1) + pipeline.immediate_execute_command("SET", "b", 2) + pipeline.execute() + + spans = self.memory_exporter.get_finished_spans() + # expecting two separate spans here, rather than a + # single span for the whole pipeline + self.assertEqual(len(spans), 2) + span = spans[0] + self._check_span(span) + self.assertEqual(span.attributes.get("db.statement"), "SET b 2") + + def test_parent(self): + """Ensure OpenTelemetry works with redis.""" + ot_tracer = trace.get_tracer("redis_svc") + + with ot_tracer.start_as_current_span("redis_get"): + self.assertIsNone(self.redis_client.get("cheese")) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + child_span, parent_span = spans[0], spans[1] + + # confirm the parenting + self.assertIsNone(parent_span.parent) + self.assertIs(child_span.parent, parent_span.get_context()) + + self.assertEqual(parent_span.name, "redis_get") + self.assertEqual(parent_span.instrumentation_info.name, "redis_svc") + + self.assertEqual( + child_span.attributes.get("service"), self.test_service + ) + self.assertEqual(child_span.name, "redis.command") diff --git a/ext/opentelemetry-ext-redis/CHANGELOG.md b/ext/opentelemetry-ext-redis/CHANGELOG.md new file mode 100644 index 0000000000..33144da913 --- /dev/null +++ b/ext/opentelemetry-ext-redis/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-redis/MANIFEST.in b/ext/opentelemetry-ext-redis/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-redis/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-redis/README.rst b/ext/opentelemetry-ext-redis/README.rst new file mode 100644 index 0000000000..49cc95f6b1 --- /dev/null +++ b/ext/opentelemetry-ext-redis/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry Redis Instrumentation +=================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-redis.svg + :target: https://pypi.org/project/opentelemetry-ext-redis/ + +This library allows tracing requests made by the Redis library. + +Installation +------------ + +:: + + pip install opentelemetry-ext-redis + + +References +---------- + +* `OpenTelemetry Redis Instrumentation `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-redis/setup.cfg b/ext/opentelemetry-ext-redis/setup.cfg new file mode 100644 index 0000000000..e24605c8e3 --- /dev/null +++ b/ext/opentelemetry-ext-redis/setup.cfg @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-redis +description = Redis tracing for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-redis +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.7dev0 + opentelemetry-auto-instrumentation == 0.7dev0 + redis >= 2.6 + wrapt >= 1.12.1 + +[options.extras_require] +test = + opentelemetry-test == 0.7.dev0 + opentelemetry-sdk == 0.7dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + redis = opentelemetry.ext.redis:RedisInstrumentor diff --git a/ext/opentelemetry-ext-redis/setup.py b/ext/opentelemetry-ext-redis/setup.py new file mode 100644 index 0000000000..d09847efd8 --- /dev/null +++ b/ext/opentelemetry-ext-redis/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "redis", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/__init__.py b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/__init__.py new file mode 100644 index 0000000000..e653936ca5 --- /dev/null +++ b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/__init__.py @@ -0,0 +1,163 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +Instrument `redis`_ to report Redis queries. + +There are two options for instrumenting code. The first option is to use the +``opentelemetry-auto-instrumentation`` executable which will automatically +instrument your Redis client. The second is to programmatically enable +instrumentation via the following code: + +.. _redis: https://pypi.org/project/redis/ + +Usage +----- + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext.redis import RedisInstrumentor + from opentelemetry.sdk.trace import TracerProvider + import redis + + trace.set_tracer_provider(TracerProvider()) + + # Instrument redis + RedisInstrumentor().instrument(tracer_provider=trace.get_tracer_provider()) + + # This will report a span with the default settings + client = redis.StrictRedis(host="localhost", port=6379) + client.get("my-key") + +API +--- +""" + +import redis +from wrapt import ObjectProxy, wrap_function_wrapper + +from opentelemetry import trace +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.ext.redis.util import ( + _extract_conn_attributes, + _format_command_args, +) +from opentelemetry.ext.redis.version import __version__ + +_DEFAULT_SERVICE = "redis" +_RAWCMD = "db.statement" +_CMD = "redis.command" + + +def _set_connection_attributes(span, conn): + for key, value in _extract_conn_attributes( + conn.connection_pool.connection_kwargs + ).items(): + span.set_attribute(key, value) + + +def _unwrap(obj, attr): + func = getattr(obj, attr, None) + if isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"): + setattr(obj, attr, func.__wrapped__) + + +def _traced_execute_command(func, instance, args, kwargs): + tracer = getattr(redis, "_opentelemetry_tracer") + query = _format_command_args(args) + with tracer.start_as_current_span(_CMD) as span: + span.set_attribute("service", tracer.instrumentation_info.name) + span.set_attribute(_RAWCMD, query) + _set_connection_attributes(span, instance) + span.set_attribute("redis.args_length", len(args)) + return func(*args, **kwargs) + + +def _traced_execute_pipeline(func, instance, args, kwargs): + tracer = getattr(redis, "_opentelemetry_tracer") + + cmds = [_format_command_args(c) for c, _ in instance.command_stack] + resource = "\n".join(cmds) + + with tracer.start_as_current_span(_CMD) as span: + span.set_attribute("service", tracer.instrumentation_info.name) + span.set_attribute(_RAWCMD, resource) + _set_connection_attributes(span, instance) + span.set_attribute( + "redis.pipeline_length", len(instance.command_stack) + ) + return func(*args, **kwargs) + + +class RedisInstrumentor(BaseInstrumentor): + """An instrumentor for Redis + See `BaseInstrumentor` + """ + + def _instrument(self, **kwargs): + tracer_provider = kwargs.get( + "tracer_provider", trace.get_tracer_provider() + ) + setattr( + redis, + "_opentelemetry_tracer", + tracer_provider.get_tracer(_DEFAULT_SERVICE, __version__), + ) + + if redis.VERSION < (3, 0, 0): + wrap_function_wrapper( + "redis", "StrictRedis.execute_command", _traced_execute_command + ) + wrap_function_wrapper( + "redis.client", + "BasePipeline.execute", + _traced_execute_pipeline, + ) + wrap_function_wrapper( + "redis.client", + "BasePipeline.immediate_execute_command", + _traced_execute_command, + ) + else: + wrap_function_wrapper( + "redis", "Redis.execute_command", _traced_execute_command + ) + wrap_function_wrapper( + "redis.client", "Pipeline.execute", _traced_execute_pipeline + ) + wrap_function_wrapper( + "redis.client", + "Pipeline.immediate_execute_command", + _traced_execute_command, + ) + + def _uninstrument(self, **kwargs): + if redis.VERSION < (3, 0, 0): + _unwrap(redis.StrictRedis, "execute_command") + _unwrap(redis.StrictRedis, "pipeline") + _unwrap(redis.Redis, "pipeline") + _unwrap( + redis.client.BasePipeline, # pylint:disable=no-member + "execute", + ) + _unwrap( + redis.client.BasePipeline, # pylint:disable=no-member + "immediate_execute_command", + ) + else: + _unwrap(redis.Redis, "execute_command") + _unwrap(redis.Redis, "pipeline") + _unwrap(redis.client.Pipeline, "execute") + _unwrap(redis.client.Pipeline, "immediate_execute_command") diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/util.py b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/util.py new file mode 100644 index 0000000000..2895134089 --- /dev/null +++ b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/util.py @@ -0,0 +1,57 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +Some utils used by the redis integration +""" + + +def _extract_conn_attributes(conn_kwargs): + """ Transform redis conn info into dict """ + attributes = { + "db.type": "redis", + "db.instance": conn_kwargs.get("db", 0), + } + try: + attributes["db.url"] = "redis://{}:{}".format( + conn_kwargs["host"], conn_kwargs["port"] + ) + except KeyError: + pass # don't include url attribute + + return attributes + + +def _format_command_args(args): + """Format command arguments and trim them as needed""" + value_max_len = 100 + value_too_long_mark = "..." + cmd_max_len = 1000 + length = 0 + out = [] + for arg in args: + cmd = str(arg) + + if len(cmd) > value_max_len: + cmd = cmd[:value_max_len] + value_too_long_mark + + if length + len(cmd) > cmd_max_len: + prefix = cmd[: cmd_max_len - length] + out.append("%s%s" % (prefix, value_too_long_mark)) + break + + out.append(cmd) + length += len(cmd) + + return " ".join(out) diff --git a/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py new file mode 100644 index 0000000000..86c61362ab --- /dev/null +++ b/ext/opentelemetry-ext-redis/src/opentelemetry/ext/redis/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-redis/tests/__init__.py b/ext/opentelemetry-ext-redis/tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/ext/opentelemetry-ext-redis/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ext/opentelemetry-ext-redis/tests/test_redis.py b/ext/opentelemetry-ext-redis/tests/test_redis.py new file mode 100644 index 0000000000..b36e96a627 --- /dev/null +++ b/ext/opentelemetry-ext-redis/tests/test_redis.py @@ -0,0 +1,51 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from unittest import mock + +import redis + +from opentelemetry.ext.redis import RedisInstrumentor +from opentelemetry.test.test_base import TestBase + + +class TestRedis(TestBase): + def test_instrument_uninstrument(self): + redis_client = redis.Redis() + RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) + + with mock.patch.object(redis_client, "connection"): + redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.memory_exporter.clear() + + # Test uninstrument + RedisInstrumentor().uninstrument() + + with mock.patch.object(redis_client, "connection"): + redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + self.memory_exporter.clear() + + # Test instrument again + RedisInstrumentor().instrument() + + with mock.patch.object(redis_client, "connection"): + redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) diff --git a/tox.ini b/tox.ini index e0c4314bc0..0e35428def 100644 --- a/tox.ini +++ b/tox.ini @@ -87,6 +87,10 @@ envlist = ; opentelemetry-ext-grpc py3{4,5,6,7,8}-test-ext-grpc + ; opentelemetry-ext-redis + py3{4,5,6,7,8}-test-ext-redis + pypy3-test-ext-redis + ; Coverage is temporarily disabled for pypy3 due to the pytest bug. ; pypy3-coverage @@ -132,6 +136,7 @@ changedir = test-example-basic-tracer: docs/examples/basic_tracer/tests test-example-http: docs/examples/http/tests test-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests + test-ext-redis: ext/opentelemetry-ext-redis/tests commands_pre = ; Install without -e to test the actual installation @@ -179,6 +184,9 @@ commands_pre = pymysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi pymysql: pip install {toxinidir}/ext/opentelemetry-ext-pymysql + redis: pip install {toxinidir}/opentelemetry-auto-instrumentation + redis: pip install {toxinidir}/ext/opentelemetry-ext-redis[test] + http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests[test] jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger @@ -239,6 +247,7 @@ deps = Deprecated thrift pymongo + redis flask pymysql mysql-connector-python @@ -262,10 +271,10 @@ deps = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ + -e {toxinidir}/opentelemetry-auto-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/ext/opentelemetry-ext-http-requests \ -e {toxinidir}/ext/opentelemetry-ext-wsgi \ - -e {toxinidir}/opentelemetry-auto-instrumentation \ -e {toxinidir}/ext/opentelemetry-ext-flask commands = @@ -279,6 +288,7 @@ deps = pymongo ~= 3.1 pymysql ~= 0.9.3 psycopg2-binary ~= 2.8.4 + redis ~= 3.3.11 changedir = ext/opentelemetry-ext-docker-tests/tests @@ -286,12 +296,14 @@ changedir = commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-sdk \ + -e {toxinidir}/opentelemetry-auto-instrumentation \ -e {toxinidir}/tests/util \ -e {toxinidir}/ext/opentelemetry-ext-dbapi \ -e {toxinidir}/ext/opentelemetry-ext-mysql \ -e {toxinidir}/ext/opentelemetry-ext-psycopg2 \ -e {toxinidir}/ext/opentelemetry-ext-pymongo \ - -e {toxinidir}/ext/opentelemetry-ext-pymysql + -e {toxinidir}/ext/opentelemetry-ext-pymysql \ + -e {toxinidir}/ext/opentelemetry-ext-redis docker-compose up -d python check_availability.py commands = From 5d675eeb231dc28a67c10192f542eac1ee58451a Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 27 Apr 2020 11:35:30 -0600 Subject: [PATCH 0309/1517] api: Add several configuration related fixes (#583) Fixes #582 This makes the attributes of the configuration object case sensitive in order to match better the environment variables that are too. This makes the attributes of the configuration object accept any legal Python variable name (Configuration().some_2_attribute) is valid now. This makes non-defined attributes return None when queried instead of raising an AttributeError. This makes it easier to use the configuration object because the user won't have to check for the existence of the attribute beforehand: if Configuration().some_attribute == "value": # do stuff instead of if hasattr(Configuration(), "some_attribute") and Configuration().some_attribute == "value": # do stuff --- .../opentelemetry/configuration/__init__.py | 22 ++++++++++-------- .../tests/configuration/test_configuration.py | 23 +++++++++++++++---- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py index 9b23c52bee..57b1c324c6 100644 --- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py +++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py @@ -19,27 +19,27 @@ Simple configuration manager This is a configuration manager for OpenTelemetry. It reads configuration -values from environment variables prefixed with -``OPENTELEMETRY_PYTHON_`` whose characters are only all caps and underscores. -The first character after ``OPENTELEMETRY_PYTHON_`` must be an uppercase -character. +values from environment variables prefixed with ``OPENTELEMETRY_PYTHON_`` whose +characters are only alphanumeric characters and unserscores, except for the +first character after ``OPENTELEMETRY_PYTHON_`` which must not be a number. For example, these environment variables will be read: 1. ``OPENTELEMETRY_PYTHON_SOMETHING`` 2. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_`` 3. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND__ELSE`` +4. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND_else`` +4. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND_else2`` These won't: 1. ``OPENTELEMETRY_PYTH_SOMETHING`` -2. ``OPENTELEMETRY_PYTHON_something`` -3. ``OPENTELEMETRY_PYTHON_SOMETHING_2_AND__ELSE`` -4. ``OPENTELEMETRY_PYTHON_SOMETHING_%_ELSE`` +2. ``OPENTELEMETRY_PYTHON_2_SOMETHING_AND__ELSE`` +3. ``OPENTELEMETRY_PYTHON_SOMETHING_%_ELSE`` The values stored in the environment variables can be found in an instance of ``opentelemetry.configuration.Configuration``. This class can be instantiated -freely because instantiating it returns a singleton. +freely because instantiating it returns always the same object. For example, if the environment variable ``OPENTELEMETRY_PYTHON_METER_PROVIDER`` value is ``my_meter_provider``, then @@ -93,11 +93,13 @@ def __new__(cls) -> "Configuration": for key, value in environ.items(): - match = fullmatch("OPENTELEMETRY_PYTHON_([A-Z][A-Z_]*)", key) + match = fullmatch( + r"OPENTELEMETRY_PYTHON_([A-Za-z_][\w_]*)", key + ) if match is not None: - key = match.group(1).lower() + key = match.group(1) setattr(Configuration, "_{}".format(key), value) setattr( diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py index d5a6363091..9688ec28b6 100644 --- a/opentelemetry-api/tests/configuration/test_configuration.py +++ b/opentelemetry-api/tests/configuration/test_configuration.py @@ -21,6 +21,9 @@ class TestConfiguration(TestCase): def setUp(self): + # This is added here to force a reload of the whole Configuration + # class, resetting its internal attributes so that each tests starts + # with a clean class. from opentelemetry.configuration import Configuration # type: ignore def tearDown(self): @@ -35,15 +38,25 @@ def test_singleton(self): { "OPENTELEMETRY_PYTHON_METER_PROVIDER": "meter_provider", "OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider", + "OPENTELEMETRY_PYTHON_OThER": "other", + "OPENTELEMETRY_PYTHON_OTHER_7": "other_7", + "OPENTELEMETRY_PTHON_TRACEX_PROVIDER": "tracex_provider", }, ) def test_environment_variables(self): # type: ignore self.assertEqual( - Configuration().meter_provider, "meter_provider" + Configuration().METER_PROVIDER, "meter_provider" ) # pylint: disable=no-member self.assertEqual( - Configuration().tracer_provider, "tracer_provider" + Configuration().TRACER_PROVIDER, "tracer_provider" ) # pylint: disable=no-member + self.assertEqual( + Configuration().OThER, "other" + ) # pylint: disable=no-member + self.assertEqual( + Configuration().OTHER_7, "other_7" + ) # pylint: disable=no-member + self.assertIsNone(Configuration().TRACEX_PROVIDER) @patch.dict( "os.environ", # type: ignore @@ -51,11 +64,11 @@ def test_environment_variables(self): # type: ignore ) def test_property(self): with self.assertRaises(AttributeError): - Configuration().tracer_provider = "new_tracer_provider" + Configuration().TRACER_PROVIDER = "new_tracer_provider" def test_slots(self): with self.assertRaises(AttributeError): - Configuration().xyz = "xyz" # pylint: disable=assigning-non-slot + Configuration().XYZ = "xyz" # pylint: disable=assigning-non-slot def test_getattr(self): - Configuration().xyz is None + self.assertIsNone(Configuration().XYZ) From 8e3ed3552691dfcc54d9fafa4bf767bdb688a441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 27 Apr 2020 13:16:49 -0500 Subject: [PATCH 0310/1517] ext/requests: Add instrumentor (#597) Implement the BaseInstrumentor interface to make this library compatible with the opentelemetry-auto-instr command. There is an issue about getting the span when the global tracer provider hasn't been configured, this should be changed in the future once we extend the opentelemetry-auto-instr command to also configure the SDK. --- docs/examples/http/client.py | 9 +- docs/examples/http/server.py | 15 +-- .../flask_example.py | 10 +- docs/getting-started.rst | 2 +- .../CHANGELOG.md | 2 + ext/opentelemetry-ext-http-requests/setup.cfg | 5 + .../ext/http_requests/__init__.py | 92 +++++++++++++------ .../tests/test_requests_integration.py | 65 +++++++++++-- tests/w3c_tracecontext_validation_server.py | 2 +- tox.ini | 3 + 10 files changed, 155 insertions(+), 50 deletions(-) diff --git a/docs/examples/http/client.py b/docs/examples/http/client.py index 2b37d89597..483e95b516 100755 --- a/docs/examples/http/client.py +++ b/docs/examples/http/client.py @@ -28,15 +28,18 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. +# It must be done before instrumenting any library. trace.set_tracer_provider(TracerProvider()) -tracer_provider = trace.get_tracer_provider() +# Enable instrumentation in the requests library. +http_requests.RequestsInstrumentor().instrument() + +# Configure a console span exporter. exporter = ConsoleSpanExporter() span_processor = BatchExportSpanProcessor(exporter) -tracer_provider.add_span_processor(span_processor) +trace.get_tracer_provider().add_span_processor(span_processor) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(tracer_provider) response = requests.get(url="http://127.0.0.1:5000/") diff --git a/docs/examples/http/server.py b/docs/examples/http/server.py index 806bd0b88c..46e2e22103 100755 --- a/docs/examples/http/server.py +++ b/docs/examples/http/server.py @@ -30,20 +30,23 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. +# It must be done before instrumenting any library. trace.set_tracer_provider(TracerProvider()) -tracer = trace.get_tracer(__name__) - -exporter = ConsoleSpanExporter() -span_processor = BatchExportSpanProcessor(exporter) -trace.get_tracer_provider().add_span_processor(span_processor) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(trace.get_tracer_provider()) +http_requests.RequestsInstrumentor().instrument() app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) +# Configure a console span exporter. +exporter = ConsoleSpanExporter() +span_processor = BatchExportSpanProcessor(exporter) +trace.get_tracer_provider().add_span_processor(span_processor) + +tracer = trace.get_tracer(__name__) + @app.route("/") def hello(): diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 21a9962d86..1a9de31012 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -28,14 +28,20 @@ SimpleExportSpanProcessor, ) +# The preferred tracer implementation must be set, as the opentelemetry-api +# defines the interface with a no-op implementation. +# It must be done before instrumenting any library trace.set_tracer_provider(TracerProvider()) + +opentelemetry.ext.http_requests.RequestsInstrumentor().instrument() +FlaskInstrumentor().instrument() + trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) -FlaskInstrumentor().instrument() + app = flask.Flask(__name__) -opentelemetry.ext.http_requests.enable(trace.get_tracer_provider()) @app.route("/") diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 47c19e32fc..f4e6918d96 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -202,7 +202,7 @@ And let's write a small Flask application that sends an HTTP request, activating ) app = flask.Flask(__name__) - opentelemetry.ext.http_requests.enable(trace.get_tracer_provider()) + opentelemetry.ext.http_requests.RequestsInstrumentor().instrument() @app.route("/") def hello(): diff --git a/ext/opentelemetry-ext-http-requests/CHANGELOG.md b/ext/opentelemetry-ext-http-requests/CHANGELOG.md index 0e9be6e475..a3f8a8f1fb 100644 --- a/ext/opentelemetry-ext-http-requests/CHANGELOG.md +++ b/ext/opentelemetry-ext-http-requests/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Implement instrumentor interface ([#597](https://github.com/open-telemetry/opentelemetry-python/pull/597)) + ## 0.3a0 Released 2019-10-29 diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg index d77e524774..fc2ff72457 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -41,6 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.7.dev0 + opentelemetry-auto-instrumentation == 0.7.dev0 requests ~= 2.0 [options.extras_require] @@ -50,3 +51,7 @@ test = [options.packages.find] where = src + +[options.entry_points] +opentelemetry_instrumentor = + requests = opentelemetry.ext.http_requests:RequestsInstrumentor diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index cd6f3378a0..9bb176dfd1 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -14,7 +14,7 @@ """ This library allows tracing HTTP requests made by the -`requests `_ library. +`requests `_ library. Usage ----- @@ -23,10 +23,10 @@ import requests import opentelemetry.ext.http_requests - from opentelemetry.trace import TracerProvider - opentelemetry.ext.http_requests.enable(TracerProvider()) - response = requests.get(url='https://www.example.org/') + # You can optionally pass a custom TracerProvider to RequestInstrumentor.instrument() + opentelemetry.ext.http_requests.RequestInstrumentor.instrument() + response = requests.get(url="https://www.example.org/") Limitations ----------- @@ -47,17 +47,15 @@ from requests.sessions import Session -from opentelemetry import context, propagators +from opentelemetry import context, propagators, trace +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.http_requests.version import __version__ -from opentelemetry.trace import SpanKind +from opentelemetry.trace import SpanKind, get_tracer +from opentelemetry.trace.status import Status, StatusCanonicalCode -# NOTE: Currently we force passing a tracer. But in turn, this forces the user -# to configure a SDK before enabling this integration. In turn, this means that -# if the SDK/tracer is already using `requests` they may, in theory, bypass our -# instrumentation when using `import from`, etc. (currently we only instrument -# a instance method so the probability for that is very low). -def enable(tracer_provider): +# pylint: disable=unused-argument +def _instrument(tracer_provider=None): """Enables tracing of all requests calls that go through :code:`requests.session.Session.request` (this includes :code:`requests.get`, etc.).""" @@ -69,20 +67,17 @@ def enable(tracer_provider): # before v1.0.0, Dec 17, 2012, see # https://github.com/psf/requests/commit/4e5c4a6ab7bb0195dececdd19bb8505b872fe120) - # Guard against double instrumentation - disable() - - tracer = tracer_provider.get_tracer(__name__, __version__) - wrapped = Session.request + tracer = trace.get_tracer(__name__, __version__, tracer_provider) + @functools.wraps(wrapped) def instrumented_request(self, method, url, *args, **kwargs): if context.get_value("suppress_instrumentation"): return wrapped(self, method, url, *args, **kwargs) # See - # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#http-client + # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#http-client try: parsed_url = urlparse(url) except ValueError as exc: # Invalid URL @@ -103,12 +98,12 @@ def instrumented_request(self, method, url, *args, **kwargs): span.set_attribute("http.status_code", result.status_code) span.set_attribute("http.status_text", result.reason) + span.set_status( + Status(_http_status_to_canonical_code(result.status_code)) + ) return result - # TODO: How to handle exceptions? Should we create events for them? Set - # certain attributes? - instrumented_request.opentelemetry_ext_requests_applied = True Session.request = instrumented_request @@ -119,18 +114,59 @@ def instrumented_request(self, method, url, *args, **kwargs): # different, then push the current URL, pop it afterwards) -def disable(): +def _uninstrument(): + # pylint: disable=global-statement """Disables instrumentation of :code:`requests` through this module. Note that this only works if no other module also patches requests.""" - if getattr(Session.request, "opentelemetry_ext_requests_applied", False): original = Session.request.__wrapped__ # pylint:disable=no-member Session.request = original -def disable_session(session): - """Disables instrumentation on the session object.""" - if getattr(session.request, "opentelemetry_ext_requests_applied", False): - original = session.request.__wrapped__ # pylint:disable=no-member - session.request = types.MethodType(original, session) +def _http_status_to_canonical_code(code: int, allow_redirect: bool = True): + # pylint:disable=too-many-branches,too-many-return-statements + if code < 100: + return StatusCanonicalCode.UNKNOWN + if code <= 299: + return StatusCanonicalCode.OK + if code <= 399: + if allow_redirect: + return StatusCanonicalCode.OK + return StatusCanonicalCode.DEADLINE_EXCEEDED + if code <= 499: + if code == 401: # HTTPStatus.UNAUTHORIZED: + return StatusCanonicalCode.UNAUTHENTICATED + if code == 403: # HTTPStatus.FORBIDDEN: + return StatusCanonicalCode.PERMISSION_DENIED + if code == 404: # HTTPStatus.NOT_FOUND: + return StatusCanonicalCode.NOT_FOUND + if code == 429: # HTTPStatus.TOO_MANY_REQUESTS: + return StatusCanonicalCode.RESOURCE_EXHAUSTED + return StatusCanonicalCode.INVALID_ARGUMENT + if code <= 599: + if code == 501: # HTTPStatus.NOT_IMPLEMENTED: + return StatusCanonicalCode.UNIMPLEMENTED + if code == 503: # HTTPStatus.SERVICE_UNAVAILABLE: + return StatusCanonicalCode.UNAVAILABLE + if code == 504: # HTTPStatus.GATEWAY_TIMEOUT: + return StatusCanonicalCode.DEADLINE_EXCEEDED + return StatusCanonicalCode.INTERNAL + return StatusCanonicalCode.UNKNOWN + + +class RequestsInstrumentor(BaseInstrumentor): + def _instrument(self, **kwargs): + _instrument(tracer_provider=kwargs.get("tracer_provider")) + + def _uninstrument(self, **kwargs): + _uninstrument() + + @staticmethod + def uninstrument_session(session): + """Disables instrumentation on the session object.""" + if getattr( + session.request, "opentelemetry_ext_requests_applied", False + ): + original = session.request.__wrapped__ # pylint:disable=no-member + session.request = types.MethodType(original, session) diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index 67de0692f1..3cc0ac006a 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -18,8 +18,9 @@ import requests import urllib3 -import opentelemetry.ext.http_requests from opentelemetry import context, propagators, trace +from opentelemetry.ext import http_requests +from opentelemetry.sdk import resources from opentelemetry.test.mock_httptextformat import MockHTTPTextFormat from opentelemetry.test.test_base import TestBase @@ -29,7 +30,7 @@ class TestRequestsIntegration(TestBase): def setUp(self): super().setUp() - opentelemetry.ext.http_requests.enable(self.tracer_provider) + http_requests.RequestsInstrumentor().instrument() httpretty.enable() httpretty.register_uri( httpretty.GET, self.URL, body="Hello!", @@ -37,15 +38,17 @@ def setUp(self): def tearDown(self): super().tearDown() - opentelemetry.ext.http_requests.disable() + http_requests.RequestsInstrumentor().uninstrument() httpretty.disable() def test_basic(self): result = requests.get(self.URL) self.assertEqual(result.text, "Hello!") + span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] + self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "/status/200") @@ -60,6 +63,32 @@ def test_basic(self): }, ) + self.assertIs( + span.status.canonical_code, trace.status.StatusCanonicalCode.OK + ) + + self.check_span_instrumentation_info(span, http_requests) + + def test_not_foundbasic(self): + url_404 = "http://httpbin.org/status/404" + httpretty.register_uri( + httpretty.GET, url_404, status=404, + ) + result = requests.get(url_404) + self.assertEqual(result.status_code, 404) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + span = span_list[0] + + self.assertEqual(span.attributes.get("http.status_code"), 404) + self.assertEqual(span.attributes.get("http.status_text"), "Not Found") + + self.assertIs( + span.status.canonical_code, + trace.status.StatusCanonicalCode.NOT_FOUND, + ) + def test_invalid_url(self): url = "http://[::1/nope" exception_type = requests.exceptions.InvalidURL @@ -81,18 +110,18 @@ def test_invalid_url(self): {"component": "http", "http.method": "POST", "http.url": url}, ) - def test_disable(self): - opentelemetry.ext.http_requests.disable() + def test_uninstrument(self): + http_requests.RequestsInstrumentor().uninstrument() result = requests.get(self.URL) self.assertEqual(result.text, "Hello!") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) + # instrument again to avoid annoying warning message + http_requests.RequestsInstrumentor().instrument() - opentelemetry.ext.http_requests.disable() - - def test_disable_session(self): + def test_uninstrument_session(self): session1 = requests.Session() - opentelemetry.ext.http_requests.disable_session(session1) + http_requests.RequestsInstrumentor().uninstrument_session(session1) result = session1.get(self.URL) self.assertEqual(result.text, "Hello!") @@ -152,3 +181,21 @@ def test_distributed_context(self): finally: propagators.set_global_httptextformat(previous_propagator) + + def test_custom_tracer_provider(self): + resource = resources.Resource.create({}) + result = self.create_tracer_provider(resource=resource) + tracer_provider, exporter = result + http_requests.RequestsInstrumentor().uninstrument() + http_requests.RequestsInstrumentor().instrument( + tracer_provider=tracer_provider + ) + + result = requests.get(self.URL) + self.assertEqual(result.text, "Hello!") + + span_list = exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + span = span_list[0] + + self.assertIs(span.resource, resource) diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py index 7790fac12d..3e4eb985d7 100644 --- a/tests/w3c_tracecontext_validation_server.py +++ b/tests/w3c_tracecontext_validation_server.py @@ -40,7 +40,7 @@ # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. trace.set_tracer_provider(TracerProvider()) -http_requests.enable(trace.get_tracer_provider()) +http_requests.RequestsInstrumentor().instrument() # SpanExporter receives the spans and send them to the target location. span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter()) diff --git a/tox.ini b/tox.ini index 0e35428def..198ca773ef 100644 --- a/tox.ini +++ b/tox.ini @@ -156,6 +156,7 @@ commands_pre = example-app: pip install {toxinidir}/ext/opentelemetry-ext-flask example-app: pip install {toxinidir}/docs/examples/opentelemetry-example-app + example-http: pip install -e {toxinidir}/opentelemetry-auto-instrumentation example-http: pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests example-http: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi example-http: pip install -r {toxinidir}/docs/examples/http/requirements.txt @@ -187,6 +188,7 @@ commands_pre = redis: pip install {toxinidir}/opentelemetry-auto-instrumentation redis: pip install {toxinidir}/ext/opentelemetry-ext-redis[test] + http-requests: pip install {toxinidir}/opentelemetry-auto-instrumentation http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests[test] jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger @@ -273,6 +275,7 @@ commands_pre = pip install -e {toxinidir}/opentelemetry-api \ -e {toxinidir}/opentelemetry-auto-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ + -e {toxinidir}/opentelemetry-auto-instrumentation \ -e {toxinidir}/ext/opentelemetry-ext-http-requests \ -e {toxinidir}/ext/opentelemetry-ext-wsgi \ -e {toxinidir}/ext/opentelemetry-ext-flask From 8c360a182d61a8694904bbcc99ef9f60a28e342a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 27 Apr 2020 15:11:07 -0500 Subject: [PATCH 0311/1517] ext/pymongo: Add instrumentor (#612) The current Pymongo integration uses the monitoring.register() [1] to hook the different internal calls of Pymongo. This integration doesn't allow to unregister a monitor. This commit workaround that limitation by adding an enable flag to the CommandTracer class and adds a logic to disable the integration. This solution is not perfect becasue there will be some overhead even when the instrumentation is disabled, but that's what we can do with the current approach. [1] https://api.mongodb.com/python/current/api/pymongo/monitoring.html#pymongo.monitoring.register Co-authored-by: Alex Boten --- .../tests/pymongo/test_pymongo_functional.py | 24 +++- ext/opentelemetry-ext-pymongo/CHANGELOG.md | 3 + ext/opentelemetry-ext-pymongo/setup.cfg | 5 + .../src/opentelemetry/ext/pymongo/__init__.py | 103 +++++++++++------- .../tests/test_pymongo.py | 8 +- tox.ini | 1 + 6 files changed, 98 insertions(+), 46 deletions(-) diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py index 1b2e269482..f85a32d450 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py @@ -17,7 +17,7 @@ from pymongo import MongoClient from opentelemetry import trace as trace_api -from opentelemetry.ext.pymongo import trace_integration +from opentelemetry.ext.pymongo import PymongoInstrumentor from opentelemetry.test.test_base import TestBase MONGODB_HOST = os.getenv("MONGODB_HOST ", "localhost") @@ -31,7 +31,7 @@ class TestFunctionalPymongo(TestBase): def setUpClass(cls): super().setUpClass() cls._tracer = cls.tracer_provider.get_tracer(__name__) - trace_integration(cls.tracer_provider) + PymongoInstrumentor().instrument() client = MongoClient( MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000 ) @@ -94,3 +94,23 @@ def test_delete(self): with self._tracer.start_as_current_span("rootSpan"): self._collection.delete_one({"name": "testName"}) self.validate_spans() + + def test_uninstrument(self): + # check that integration is working + self._collection.find_one() + spans = self.memory_exporter.get_finished_spans() + self.memory_exporter.clear() + self.assertEqual(len(spans), 1) + + # uninstrument and check not new spans are created + PymongoInstrumentor().uninstrument() + self._collection.find_one() + spans = self.memory_exporter.get_finished_spans() + self.memory_exporter.clear() + self.assertEqual(len(spans), 0) + + # re-enable and check that it works again + PymongoInstrumentor().instrument() + self._collection.find_one() + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) diff --git a/ext/opentelemetry-ext-pymongo/CHANGELOG.md b/ext/opentelemetry-ext-pymongo/CHANGELOG.md index d947fff944..c8661e548d 100644 --- a/ext/opentelemetry-ext-pymongo/CHANGELOG.md +++ b/ext/opentelemetry-ext-pymongo/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Implement instrumentor interface ([#612](https://github.com/open-telemetry/opentelemetry-python/pull/612)) + + ## 0.4a0 Released 2020-02-21 diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index 790ea8bff2..5c65a39551 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -41,6 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.7.dev0 + opentelemetry-auto-instrumentation == 0.7.dev0 pymongo ~= 3.1 [options.extras_require] @@ -49,3 +50,7 @@ test = [options.packages.find] where = src + +[options.entry_points] +opentelemetry_instrumentor = + pymongo = opentelemetry.ext.pymongo:PymongoInstrumentor diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index b85bf42379..ee61f6d4f9 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -13,8 +13,8 @@ # limitations under the License. """ -The integration with MongoDB supports the `pymongo`_ library and is specified -to ``trace_integration`` using ``'pymongo'``. +The integration with MongoDB supports the `pymongo`_ library, it can be +enabled using the ``PymongoInstrumentor``. .. _pymongo: https://pypi.org/project/pymongo @@ -26,11 +26,11 @@ from pymongo import MongoClient from opentelemetry import trace from opentelemetry.trace import TracerProvider - from opentelemetry.trace.ext.pymongo import trace_integration + from opentelemetry.trace.ext.pymongo import PymongoInstrumentor trace.set_tracer_provider(TracerProvider()) - trace_integration() + PymongoInstrumentor().instrument() client = MongoClient() db = client["MongoDB_Database"] collection = db["MongoDB_Collection"] @@ -42,6 +42,8 @@ from pymongo import monitoring +from opentelemetry import trace +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.pymongo.version import __version__ from opentelemetry.trace import SpanKind, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode @@ -50,27 +52,16 @@ COMMAND_ATTRIBUTES = ["filter", "sort", "skip", "limit", "pipeline"] -def trace_integration(tracer_provider=None): - """Integrate with pymongo to trace it using event listener. - https://api.mongodb.com/python/current/api/pymongo/monitoring.html - - Args: - tracer_provider: The `TracerProvider` to use. If none is passed the - current configured one is used. - """ - - tracer = get_tracer(__name__, __version__, tracer_provider) - - monitoring.register(CommandTracer(tracer)) - - class CommandTracer(monitoring.CommandListener): def __init__(self, tracer): self._tracer = tracer self._span_dict = {} + self.is_enabled = True def started(self, event: monitoring.CommandStartedEvent): """ Method to handle a pymongo CommandStartedEvent """ + if not self.is_enabled: + return command = event.command.get(event.command_name, "") name = DATABASE_TYPE + "." + event.command_name statement = event.command_name @@ -103,38 +94,70 @@ def started(self, event: monitoring.CommandStartedEvent): if span is not None: span.set_status(Status(StatusCanonicalCode.INTERNAL, str(ex))) span.end() - self._remove_span(event) + self._pop_span(event) def succeeded(self, event: monitoring.CommandSucceededEvent): """ Method to handle a pymongo CommandSucceededEvent """ - span = self._get_span(event) - if span is not None: - span.set_attribute( - "db.mongo.duration_micros", event.duration_micros - ) - span.set_status(Status(StatusCanonicalCode.OK, event.reply)) - span.end() - self._remove_span(event) + if not self.is_enabled: + return + span = self._pop_span(event) + if span is None: + return + span.set_attribute("db.mongo.duration_micros", event.duration_micros) + span.set_status(Status(StatusCanonicalCode.OK, event.reply)) + span.end() def failed(self, event: monitoring.CommandFailedEvent): """ Method to handle a pymongo CommandFailedEvent """ - span = self._get_span(event) - if span is not None: - span.set_attribute( - "db.mongo.duration_micros", event.duration_micros - ) - span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure)) - span.end() - self._remove_span(event) - - def _get_span(self, event): - return self._span_dict.get(_get_span_dict_key(event)) + if not self.is_enabled: + return + span = self._pop_span(event) + if span is None: + return + span.set_attribute("db.mongo.duration_micros", event.duration_micros) + span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure)) + span.end() - def _remove_span(self, event): - self._span_dict.pop(_get_span_dict_key(event)) + def _pop_span(self, event): + return self._span_dict.pop(_get_span_dict_key(event), None) def _get_span_dict_key(event): if event.connection_id is not None: return (event.request_id, event.connection_id) return event.request_id + + +class PymongoInstrumentor(BaseInstrumentor): + _commandtracer_instance = None # type CommandTracer + # The instrumentation for PyMongo is based on the event listener interface + # https://api.mongodb.com/python/current/api/pymongo/monitoring.html. + # This interface only allows to register listeners and does not provide + # an unregister API. In order to provide a mechanishm to disable + # instrumentation an enabled flag is implemented in CommandTracer, + # it's checked in the different listeners. + + def _instrument(self, **kwargs): + """Integrate with pymongo to trace it using event listener. + https://api.mongodb.com/python/current/api/pymongo/monitoring.html + + Args: + tracer_provider: The `TracerProvider` to use. If none is passed the + current configured one is used. + """ + + tracer_provider = kwargs.get("tracer_provider") + + # Create and register a CommandTracer only the first time + if self._commandtracer_instance is None: + tracer = get_tracer(__name__, __version__, tracer_provider) + + self._commandtracer_instance = CommandTracer(tracer) + monitoring.register(self._commandtracer_instance) + + # If already created, just enable it + self._commandtracer_instance.is_enabled = True + + def _uninstrument(self, **kwargs): + if self._commandtracer_instance is not None: + self._commandtracer_instance.is_enabled = False diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py index d85b23bfc9..abb4e8ab50 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo.py @@ -15,7 +15,7 @@ from unittest import mock from opentelemetry import trace as trace_api -from opentelemetry.ext.pymongo import CommandTracer, trace_integration +from opentelemetry.ext.pymongo import CommandTracer, PymongoInstrumentor from opentelemetry.test.test_base import TestBase @@ -24,13 +24,13 @@ def setUp(self): super().setUp() self.tracer = self.tracer_provider.get_tracer(__name__) - def test_trace_integration(self): + def test_pymongo_instrumentor(self): mock_register = mock.Mock() patch = mock.patch( "pymongo.monitoring.register", side_effect=mock_register ) with patch: - trace_integration(self.tracer_provider) + PymongoInstrumentor().instrument() self.assertTrue(mock_register.called) @@ -50,7 +50,7 @@ def test_started(self): # the memory exporter can't be used here because the span isn't ended # yet # pylint: disable=protected-access - span = command_tracer._get_span(mock_event) + span = command_tracer._pop_span(mock_event) self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.name, "mongodb.command_name.find") self.assertEqual(span.attributes["component"], "mongodb") diff --git a/tox.ini b/tox.ini index 198ca773ef..ef8b633403 100644 --- a/tox.ini +++ b/tox.ini @@ -177,6 +177,7 @@ commands_pre = prometheus: pip install {toxinidir}/ext/opentelemetry-ext-prometheus + pymongo: pip install {toxinidir}/opentelemetry-auto-instrumentation pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo[test] psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi From bdd99257032e752fc0d615e1e64e5b442276d6d7 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 29 Apr 2020 13:16:45 -0700 Subject: [PATCH 0312/1517] infra: reduce build time by running longest job early (#626) Currently builds take roughly 15 minutes, as the longest job (pypy3) isn't kicked off until one of the other jobs is finished running (max 5 concurrent jobs). Prioritizing the longer jobs first should reduce the build time by about 5 minutes. --- .flake8 | 1 + .travis.yml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.flake8 b/.flake8 index 0c2204782f..5922f31d8f 100644 --- a/.flake8 +++ b/.flake8 @@ -18,3 +18,4 @@ exclude = ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/ ext/opentelemetry-ext-jaeger/build/* docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ + docs/examples/opentelemetry-example-app/build/* diff --git a/.travis.yml b/.travis.yml index b531cfb338..999abc7f43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,12 +5,12 @@ language: python cache: pip python: + - 'pypy3' + - '3.8' - '3.4' - '3.5' - '3.6' - '3.7' - - '3.8' - - 'pypy3' #matrix: # allow_failures: From e6a9d97cc8455f9d7be73f6c2c7ec0098b321596 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 29 Apr 2020 13:43:56 -0700 Subject: [PATCH 0313/1517] Porting sqlalchemy instrumentation from contrib repo (#591) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Porting the existing sqlalchemy instrumentation from https://github.com/open-telemetry/opentelemetry-python-contrib/tree/master/reference/ddtrace/contrib/sqlalchemy Co-authored-by: Mauricio Vásquez Co-authored-by: Diego Hurtado Co-authored-by: Chris Kleinknecht --- docs/ext/sqlalchemy/sqlalchemy.rst | 7 + .../tests/sqlalchemy_tests/__init__.py | 13 ++ .../tests/sqlalchemy_tests/mixins.py | 184 ++++++++++++++++ .../tests/sqlalchemy_tests/test_instrument.py | 72 +++++++ .../tests/sqlalchemy_tests/test_mysql.py | 77 +++++++ .../tests/sqlalchemy_tests/test_postgres.py | 92 ++++++++ .../tests/sqlalchemy_tests/test_sqlite.py | 61 ++++++ ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md | 5 + ext/opentelemetry-ext-sqlalchemy/LICENSE | 201 ++++++++++++++++++ ext/opentelemetry-ext-sqlalchemy/MANIFEST.in | 9 + ext/opentelemetry-ext-sqlalchemy/README.rst | 24 +++ ext/opentelemetry-ext-sqlalchemy/setup.cfg | 58 +++++ ext/opentelemetry-ext-sqlalchemy/setup.py | 26 +++ .../opentelemetry/ext/sqlalchemy/__init__.py | 99 +++++++++ .../opentelemetry/ext/sqlalchemy/engine.py | 142 +++++++++++++ .../opentelemetry/ext/sqlalchemy/version.py | 15 ++ .../tests/__init__.py | 13 ++ .../tests/test_sqlalchemy.py | 50 +++++ tox.ini | 10 + 19 files changed, 1158 insertions(+) create mode 100644 docs/ext/sqlalchemy/sqlalchemy.rst create mode 100644 ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/__init__.py create mode 100644 ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/mixins.py create mode 100644 ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_instrument.py create mode 100644 ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_mysql.py create mode 100644 ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_postgres.py create mode 100644 ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_sqlite.py create mode 100644 ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-sqlalchemy/LICENSE create mode 100644 ext/opentelemetry-ext-sqlalchemy/MANIFEST.in create mode 100644 ext/opentelemetry-ext-sqlalchemy/README.rst create mode 100644 ext/opentelemetry-ext-sqlalchemy/setup.cfg create mode 100644 ext/opentelemetry-ext-sqlalchemy/setup.py create mode 100644 ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py create mode 100644 ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/engine.py create mode 100644 ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py create mode 100644 ext/opentelemetry-ext-sqlalchemy/tests/__init__.py create mode 100644 ext/opentelemetry-ext-sqlalchemy/tests/test_sqlalchemy.py diff --git a/docs/ext/sqlalchemy/sqlalchemy.rst b/docs/ext/sqlalchemy/sqlalchemy.rst new file mode 100644 index 0000000000..5a3afbb3bb --- /dev/null +++ b/docs/ext/sqlalchemy/sqlalchemy.rst @@ -0,0 +1,7 @@ +OpenTelemetry SQLAlchemy Instrumentation +======================================== + +.. automodule:: opentelemetry.ext.sqlalchemy + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/__init__.py b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/mixins.py b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/mixins.py new file mode 100644 index 0000000000..84c6fd05f9 --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/mixins.py @@ -0,0 +1,184 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib + +from sqlalchemy import Column, Integer, String, create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +from opentelemetry import trace +from opentelemetry.ext.sqlalchemy import SQLAlchemyInstrumentor +from opentelemetry.ext.sqlalchemy.engine import _DB, _ROWS, _STMT +from opentelemetry.test.test_base import TestBase + +Base = declarative_base() + + +def _create_engine(engine_args): + # create a SQLAlchemy engine + config = dict(engine_args) + url = config.pop("url") + return create_engine(url, **config) + + +class Player(Base): + """Player entity used to test SQLAlchemy ORM""" + + __tablename__ = "players" + + id = Column(Integer, primary_key=True) + name = Column(String(20)) + + +class SQLAlchemyTestMixin(TestBase): + __test__ = False + + """SQLAlchemy test mixin that includes a complete set of tests + that must be executed for different engine. When a new test (or + a regression test) should be added to SQLAlchemy test suite, a new + entry must be appended here so that it will be executed for all + available and supported engines. If the test is specific to only + one engine, that test must be added to the specific `TestCase` + implementation. + + To support a new engine, create a new `TestCase` that inherits from + `SQLAlchemyTestMixin` and `TestCase`. Then you must define the following + static class variables: + * VENDOR: the database vendor name + * SQL_DB: the `db.type` tag that we expect (it's the name of the database available in the `.env` file) + * SERVICE: the service that we expect by default + * ENGINE_ARGS: all arguments required to create the engine + + To check specific tags in each test, you must implement the + `check_meta(self, span)` method. + """ + + VENDOR = None + SQL_DB = None + SERVICE = None + ENGINE_ARGS = None + + @contextlib.contextmanager + def connection(self): + # context manager that provides a connection + # to the underlying database + try: + conn = self.engine.connect() + yield conn + finally: + conn.close() + + def check_meta(self, span): + """function that can be implemented according to the + specific engine implementation + """ + + def setUp(self): + super().setUp() + # create an engine with the given arguments + self.engine = _create_engine(self.ENGINE_ARGS) + + # create the database / entities and prepare a session for the test + Base.metadata.drop_all(bind=self.engine) + Base.metadata.create_all(self.engine, checkfirst=False) + self.session = sessionmaker(bind=self.engine)() + # trace the engine + SQLAlchemyInstrumentor().instrument( + engine=self.engine, tracer_provider=self.tracer_provider + ) + self.memory_exporter.clear() + + def tearDown(self): + # pylint: disable=invalid-name + # clear the database and dispose the engine + self.session.close() + Base.metadata.drop_all(bind=self.engine) + self.engine.dispose() + SQLAlchemyInstrumentor().uninstrument() + super().tearDown() + + def _check_span(self, span): + self.assertEqual(span.name, "{}.query".format(self.VENDOR)) + self.assertEqual(span.attributes.get("service"), self.SERVICE) + self.assertEqual(span.attributes.get(_DB), self.SQL_DB) + self.assertIs( + span.status.canonical_code, trace.status.StatusCanonicalCode.OK + ) + self.assertGreater((span.end_time - span.start_time), 0) + + def test_orm_insert(self): + # ensures that the ORM session is traced + wayne = Player(id=1, name="wayne") + self.session.add(wayne) + self.session.commit() + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self._check_span(span) + self.assertIn("INSERT INTO players", span.attributes.get(_STMT)) + self.assertEqual(span.attributes.get(_ROWS), 1) + self.check_meta(span) + + def test_session_query(self): + # ensures that the Session queries are traced + out = list(self.session.query(Player).filter_by(name="wayne")) + self.assertEqual(len(out), 0) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self._check_span(span) + self.assertIn( + "SELECT players.id AS players_id, players.name AS players_name \nFROM players \nWHERE players.name", + span.attributes.get(_STMT), + ) + self.check_meta(span) + + def test_engine_connect_execute(self): + # ensures that engine.connect() is properly traced + with self.connection() as conn: + rows = conn.execute("SELECT * FROM players").fetchall() + self.assertEqual(len(rows), 0) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self._check_span(span) + self.assertEqual(span.attributes.get(_STMT), "SELECT * FROM players") + self.check_meta(span) + + def test_parent(self): + """Ensure that sqlalchemy works with opentelemetry.""" + tracer = self.tracer_provider.get_tracer("sqlalch_svc") + + with tracer.start_as_current_span("sqlalch_op"): + with self.connection() as conn: + rows = conn.execute("SELECT * FROM players").fetchall() + self.assertEqual(len(rows), 0) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + child_span, parent_span = spans + + # confirm the parenting + self.assertIsNone(parent_span.parent) + self.assertIs(child_span.parent, parent_span.get_context()) + + self.assertEqual(parent_span.name, "sqlalch_op") + self.assertEqual(parent_span.instrumentation_info.name, "sqlalch_svc") + + self.assertEqual(child_span.name, "{}.query".format(self.VENDOR)) + self.assertEqual(child_span.attributes.get("service"), self.SERVICE) diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_instrument.py b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_instrument.py new file mode 100644 index 0000000000..845bf26cfa --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_instrument.py @@ -0,0 +1,72 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import unittest + +import sqlalchemy + +from opentelemetry import trace +from opentelemetry.ext.sqlalchemy import SQLAlchemyInstrumentor +from opentelemetry.test.test_base import TestBase + +POSTGRES_CONFIG = { + "host": "127.0.0.1", + "port": int(os.getenv("TEST_POSTGRES_PORT", "5432")), + "user": os.getenv("TEST_POSTGRES_USER", "testuser"), + "password": os.getenv("TEST_POSTGRES_PASSWORD", "testpassword"), + "dbname": os.getenv("TEST_POSTGRES_DB", "opentelemetry-tests"), +} + + +class SQLAlchemyInstrumentTestCase(TestBase): + """TestCase that checks if the engine is properly traced + when the `instrument()` method is used. + """ + + def setUp(self): + # create a traced engine with the given arguments + SQLAlchemyInstrumentor().instrument() + dsn = ( + "postgresql://%(user)s:%(password)s@%(host)s:%(port)s/%(dbname)s" + % POSTGRES_CONFIG + ) + self.engine = sqlalchemy.create_engine(dsn) + + # prepare a connection + self.conn = self.engine.connect() + super().setUp() + + def tearDown(self): + # clear the database and dispose the engine + self.conn.close() + self.engine.dispose() + SQLAlchemyInstrumentor().uninstrument() + + def test_engine_traced(self): + # ensures that the engine is traced + rows = self.conn.execute("SELECT 1").fetchall() + self.assertEqual(len(rows), 1) + + traces = self.memory_exporter.get_finished_spans() + # trace composition + self.assertEqual(len(traces), 1) + span = traces[0] + # check subset of span fields + self.assertEqual(span.name, "postgres.query") + self.assertEqual(span.attributes.get("service"), "postgres") + self.assertIs( + span.status.canonical_code, trace.status.StatusCanonicalCode.OK + ) + self.assertGreater((span.end_time - span.start_time), 0) diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_mysql.py b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_mysql.py new file mode 100644 index 0000000000..3b8adc8c62 --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_mysql.py @@ -0,0 +1,77 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import unittest + +import pytest +from sqlalchemy.exc import ProgrammingError + +from opentelemetry import trace +from opentelemetry.ext.sqlalchemy.engine import _DB, _HOST, _PORT, _ROWS, _STMT + +from .mixins import SQLAlchemyTestMixin + +MYSQL_CONFIG = { + "host": "127.0.0.1", + "port": int(os.getenv("TEST_MYSQL_PORT", "3306")), + "user": os.getenv("TEST_MYSQL_USER", "testuser"), + "password": os.getenv("TEST_MYSQL_PASSWORD", "testpassword"), + "database": os.getenv("TEST_MYSQL_DATABASE", "opentelemetry-tests"), +} + + +class MysqlConnectorTestCase(SQLAlchemyTestMixin): + """TestCase for mysql-connector engine""" + + __test__ = True + + VENDOR = "mysql" + SQL_DB = "opentelemetry-tests" + SERVICE = "mysql" + ENGINE_ARGS = { + "url": "mysql+mysqlconnector://%(user)s:%(password)s@%(host)s:%(port)s/%(database)s" + % MYSQL_CONFIG + } + + def check_meta(self, span): + # check database connection tags + self.assertEqual(span.attributes.get(_HOST), MYSQL_CONFIG["host"]) + self.assertEqual(span.attributes.get(_PORT), MYSQL_CONFIG["port"]) + + def test_engine_execute_errors(self): + # ensures that SQL errors are reported + with pytest.raises(ProgrammingError): + with self.connection() as conn: + conn.execute("SELECT * FROM a_wrong_table").fetchall() + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + # span fields + self.assertEqual(span.name, "{}.query".format(self.VENDOR)) + self.assertEqual(span.attributes.get("service"), self.SERVICE) + self.assertEqual( + span.attributes.get(_STMT), "SELECT * FROM a_wrong_table" + ) + self.assertEqual(span.attributes.get(_DB), self.SQL_DB) + self.assertIsNone(span.attributes.get(_ROWS)) + self.check_meta(span) + self.assertTrue(span.end_time - span.start_time > 0) + # check the error + self.assertIs( + span.status.canonical_code, + trace.status.StatusCanonicalCode.UNKNOWN, + ) + self.assertIn("a_wrong_table", span.status.description) diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_postgres.py b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_postgres.py new file mode 100644 index 0000000000..125c925209 --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_postgres.py @@ -0,0 +1,92 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import unittest + +import psycopg2 +import pytest +from sqlalchemy.exc import ProgrammingError + +from opentelemetry import trace +from opentelemetry.ext.sqlalchemy.engine import _DB, _HOST, _PORT, _ROWS, _STMT + +from .mixins import SQLAlchemyTestMixin + +POSTGRES_CONFIG = { + "host": "127.0.0.1", + "port": int(os.getenv("TEST_POSTGRES_PORT", "5432")), + "user": os.getenv("TEST_POSTGRES_USER", "testuser"), + "password": os.getenv("TEST_POSTGRES_PASSWORD", "testpassword"), + "dbname": os.getenv("TEST_POSTGRES_DB", "opentelemetry-tests"), +} + + +class PostgresTestCase(SQLAlchemyTestMixin): + """TestCase for Postgres Engine""" + + __test__ = True + + VENDOR = "postgres" + SQL_DB = "opentelemetry-tests" + SERVICE = "postgres" + ENGINE_ARGS = { + "url": "postgresql://%(user)s:%(password)s@%(host)s:%(port)s/%(dbname)s" + % POSTGRES_CONFIG + } + + def check_meta(self, span): + # check database connection tags + self.assertEqual(span.attributes.get(_HOST), POSTGRES_CONFIG["host"]) + self.assertEqual(span.attributes.get(_PORT), POSTGRES_CONFIG["port"]) + + def test_engine_execute_errors(self): + # ensures that SQL errors are reported + with pytest.raises(ProgrammingError): + with self.connection() as conn: + conn.execute("SELECT * FROM a_wrong_table").fetchall() + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + # span fields + self.assertEqual(span.name, "{}.query".format(self.VENDOR)) + self.assertEqual(span.attributes.get("service"), self.SERVICE) + self.assertEqual( + span.attributes.get(_STMT), "SELECT * FROM a_wrong_table" + ) + self.assertEqual(span.attributes.get(_DB), self.SQL_DB) + self.assertIsNone(span.attributes.get(_ROWS)) + self.check_meta(span) + self.assertTrue(span.end_time - span.start_time > 0) + # check the error + self.assertIs( + span.status.canonical_code, + trace.status.StatusCanonicalCode.UNKNOWN, + ) + self.assertIn("a_wrong_table", span.status.description) + + +class PostgresCreatorTestCase(PostgresTestCase): + """TestCase for Postgres Engine that includes the same tests set + of `PostgresTestCase`, but it uses a specific `creator` function. + """ + + VENDOR = "postgres" + SQL_DB = "opentelemetry-tests" + SERVICE = "postgres" + ENGINE_ARGS = { + "url": "postgresql://", + "creator": lambda: psycopg2.connect(**POSTGRES_CONFIG), + } diff --git a/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_sqlite.py b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_sqlite.py new file mode 100644 index 0000000000..7d8a54368f --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/sqlalchemy_tests/test_sqlite.py @@ -0,0 +1,61 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import pytest +from sqlalchemy.exc import OperationalError + +from opentelemetry import trace +from opentelemetry.ext.sqlalchemy.engine import _DB, _ROWS, _STMT + +from .mixins import SQLAlchemyTestMixin + + +class SQLiteTestCase(SQLAlchemyTestMixin): + """TestCase for the SQLite engine""" + + __test__ = True + + VENDOR = "sqlite" + SQL_DB = ":memory:" + SERVICE = "sqlite" + ENGINE_ARGS = {"url": "sqlite:///:memory:"} + + def test_engine_execute_errors(self): + # ensures that SQL errors are reported + with pytest.raises(OperationalError): + with self.connection() as conn: + conn.execute("SELECT * FROM a_wrong_table").fetchall() + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + # span fields + self.assertEqual(span.name, "{}.query".format(self.VENDOR)) + self.assertEqual(span.attributes.get("service"), self.SERVICE) + self.assertEqual( + span.attributes.get(_STMT), "SELECT * FROM a_wrong_table" + ) + self.assertEqual(span.attributes.get(_DB), self.SQL_DB) + self.assertIsNone(span.attributes.get(_ROWS)) + self.assertTrue((span.end_time - span.start_time) > 0) + # check the error + self.assertIs( + span.status.canonical_code, + trace.status.StatusCanonicalCode.UNKNOWN, + ) + self.assertEqual( + span.status.description, "no such table: a_wrong_table" + ) diff --git a/ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md b/ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md new file mode 100644 index 0000000000..33144da913 --- /dev/null +++ b/ext/opentelemetry-ext-sqlalchemy/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release \ No newline at end of file diff --git a/ext/opentelemetry-ext-sqlalchemy/LICENSE b/ext/opentelemetry-ext-sqlalchemy/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-sqlalchemy/LICENSE @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-sqlalchemy/MANIFEST.in b/ext/opentelemetry-ext-sqlalchemy/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-sqlalchemy/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-sqlalchemy/README.rst b/ext/opentelemetry-ext-sqlalchemy/README.rst new file mode 100644 index 0000000000..2485c96a58 --- /dev/null +++ b/ext/opentelemetry-ext-sqlalchemy/README.rst @@ -0,0 +1,24 @@ +OpenTelemetry SQLAlchemy Tracing +================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-sqlalchemy.svg + :target: https://pypi.org/project/opentelemetry-ext-sqlalchemy/ + +This library allows tracing requests made by the SQLAlchemy library. + +Installation +------------ + +:: + + pip install opentelemetry-ext-sqlalchemy + + +References +---------- + +* `SQLAlchemy Project `_ +* `OpenTelemetry SQLAlchemy Tracing `_ +* `OpenTelemetry Project `_ \ No newline at end of file diff --git a/ext/opentelemetry-ext-sqlalchemy/setup.cfg b/ext/opentelemetry-ext-sqlalchemy/setup.cfg new file mode 100644 index 0000000000..a13cead988 --- /dev/null +++ b/ext/opentelemetry-ext-sqlalchemy/setup.cfg @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-sqlalchemy +description = SQLAlchemy tracing for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-sqlalchemy +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.7.dev0 + opentelemetry-auto-instrumentation == 0.7.dev0 + wrapt >= 1.11.2 + sqlalchemy + +[options.extras_require] +test = + opentelemetry-sdk == 0.7.dev0 + pytest + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + sqlalchemy = opentelemetry.ext.sqlalchemy:SQLAlchemyInstrumentor diff --git a/ext/opentelemetry-ext-sqlalchemy/setup.py b/ext/opentelemetry-ext-sqlalchemy/setup.py new file mode 100644 index 0000000000..d776a90e82 --- /dev/null +++ b/ext/opentelemetry-ext-sqlalchemy/setup.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "sqlalchemy", "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py new file mode 100644 index 0000000000..19078fe0a5 --- /dev/null +++ b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/__init__.py @@ -0,0 +1,99 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Instrument `sqlalchemy`_ to report SQL queries. + +There are two options for instrumenting code. The first option is to use +the `opentelemetry-auto-instrumentation` executable which will automatically +instrument your SQLAlchemy engine. The second is to programmatically enable +instrumentation via the following code: + +.. _sqlalchemy: https://pypi.org/project/sqlalchemy/ +Usage +----- +.. code:: python + + from sqlalchemy import create_engine + + from opentelemetry import trace + from opentelemetry.ext.sqlalchemy import SQLAlchemyInstrumentor + from opentelemetry.sdk.trace import TracerProvider + import sqlalchemy + + trace.set_tracer_provider(TracerProvider()) + engine = create_engine("sqlite:///:memory:") + SQLAlchemyInstrumentor().instrument( + engine=engine, + service="service-A", + ) + +API +--- +""" +import sqlalchemy +import wrapt +from wrapt import wrap_function_wrapper as _w + +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.ext.sqlalchemy.engine import ( + EngineTracer, + _get_tracer, + _wrap_create_engine, +) + + +def _unwrap(obj, attr): + func = getattr(obj, attr, None) + if ( + func + and isinstance(func, wrapt.ObjectProxy) + and hasattr(func, "__wrapped__") + ): + setattr(obj, attr, func.__wrapped__) + + +class SQLAlchemyInstrumentor(BaseInstrumentor): + """An instrumentor for SQLAlchemy + See `BaseInstrumentor` + """ + + def _instrument(self, **kwargs): + """Instruments SQLAlchemy engine creation methods and the engine + if passed as an argument. + + Args: + **kwargs: Optional arguments + ``engine``: a SQLAlchemy engine instance + ``tracer_provider``: a TracerProvider, defaults to global + ``service``: the name of the service to trace. + + Returns: + An instrumented engine if passed in as an argument, None otherwise. + """ + _w("sqlalchemy", "create_engine", _wrap_create_engine) + _w("sqlalchemy.engine", "create_engine", _wrap_create_engine) + if kwargs.get("engine") is not None: + return EngineTracer( + _get_tracer( + kwargs.get("engine"), kwargs.get("tracer_provider") + ), + kwargs.get("service"), + kwargs.get("engine"), + ) + return None + + def _uninstrument(self, **kwargs): + _unwrap(sqlalchemy, "create_engine") + _unwrap(sqlalchemy.engine, "create_engine") diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/engine.py b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/engine.py new file mode 100644 index 0000000000..890bbc7c8d --- /dev/null +++ b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/engine.py @@ -0,0 +1,142 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sqlalchemy.event import listen + +from opentelemetry import trace +from opentelemetry.ext.sqlalchemy.version import __version__ +from opentelemetry.trace.status import Status, StatusCanonicalCode + +# Network attribute semantic convention here: +# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/span-general.md#general-network-connection-attributes +_HOST = "net.peer.name" +_PORT = "net.peer.port" +# Database semantic conventions here: +# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/database.md +_ROWS = "sql.rows" # number of rows returned by a query +_STMT = "db.statement" +_DB = "db.type" +_URL = "db.url" + + +def _normalize_vendor(vendor): + """Return a canonical name for a type of database.""" + if not vendor: + return "db" # should this ever happen? + + if "sqlite" in vendor: + return "sqlite" + + if "postgres" in vendor or vendor == "psycopg2": + return "postgres" + + return vendor + + +def _get_tracer(engine, tracer_provider=None): + if tracer_provider is None: + tracer_provider = trace.get_tracer_provider() + return tracer_provider.get_tracer( + _normalize_vendor(engine.name), __version__ + ) + + +# pylint: disable=unused-argument +def _wrap_create_engine(func, module, args, kwargs): + """Trace the SQLAlchemy engine, creating an `EngineTracer` + object that will listen to SQLAlchemy events. + """ + engine = func(*args, **kwargs) + EngineTracer(_get_tracer(engine), None, engine) + return engine + + +class EngineTracer: + def __init__(self, tracer, service, engine): + self.tracer = tracer + self.engine = engine + self.vendor = _normalize_vendor(engine.name) + self.service = service or self.vendor + self.name = "%s.query" % self.vendor + self.current_span = None + + listen(engine, "before_cursor_execute", self._before_cur_exec) + listen(engine, "after_cursor_execute", self._after_cur_exec) + listen(engine, "handle_error", self._handle_error) + + # pylint: disable=unused-argument + def _before_cur_exec(self, conn, cursor, statement, *args): + self.current_span = self.tracer.start_span(self.name) + with self.tracer.use_span(self.current_span, end_on_exit=False): + self.current_span.set_attribute("service", self.vendor) + self.current_span.set_attribute(_STMT, statement) + + if not _set_attributes_from_url( + self.current_span, conn.engine.url + ): + _set_attributes_from_cursor( + self.current_span, self.vendor, cursor + ) + + # pylint: disable=unused-argument + def _after_cur_exec(self, conn, cursor, statement, *args): + if self.current_span is None: + return + + try: + if cursor and cursor.rowcount >= 0: + self.current_span.set_attribute(_ROWS, cursor.rowcount) + finally: + self.current_span.end() + + def _handle_error(self, context): + if self.current_span is None: + return + + try: + self.current_span.set_status( + Status( + StatusCanonicalCode.UNKNOWN, + str(context.original_exception), + ) + ) + finally: + self.current_span.end() + + +def _set_attributes_from_url(span: trace.Span, url): + """Set connection tags from the url. return true if successful.""" + if url.host: + span.set_attribute(_HOST, url.host) + if url.port: + span.set_attribute(_PORT, url.port) + if url.database: + span.set_attribute(_DB, url.database) + + return bool(url.host) + + +def _set_attributes_from_cursor(span: trace.Span, vendor, cursor): + """Attempt to set db connection attributes by introspecting the cursor.""" + if vendor == "postgres": + # pylint: disable=import-outside-toplevel + from psycopg2.extensions import parse_dsn + + if hasattr(cursor, "connection") and hasattr(cursor.connection, "dsn"): + dsn = getattr(cursor.connection, "dsn", None) + if dsn: + data = parse_dsn(dsn) + span.set_attribute(_DB, data.get("dbname")) + span.set_attribute(_HOST, data.get("host")) + span.set_attribute(_PORT, int(data.get("port"))) diff --git a/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py new file mode 100644 index 0000000000..86c61362ab --- /dev/null +++ b/ext/opentelemetry-ext-sqlalchemy/src/opentelemetry/ext/sqlalchemy/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.7.dev0" diff --git a/ext/opentelemetry-ext-sqlalchemy/tests/__init__.py b/ext/opentelemetry-ext-sqlalchemy/tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/ext/opentelemetry-ext-sqlalchemy/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ext/opentelemetry-ext-sqlalchemy/tests/test_sqlalchemy.py b/ext/opentelemetry-ext-sqlalchemy/tests/test_sqlalchemy.py new file mode 100644 index 0000000000..858cc652ef --- /dev/null +++ b/ext/opentelemetry-ext-sqlalchemy/tests/test_sqlalchemy.py @@ -0,0 +1,50 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sqlalchemy import create_engine + +from opentelemetry.ext.sqlalchemy import SQLAlchemyInstrumentor +from opentelemetry.test.test_base import TestBase + + +class TestSqlalchemyInstrumentation(TestBase): + def tearDown(self): + super().tearDown() + SQLAlchemyInstrumentor().uninstrument() + + def test_trace_integration(self): + engine = create_engine("sqlite:///:memory:") + SQLAlchemyInstrumentor().instrument( + engine=engine, + tracer_provider=self.tracer_provider, + service="my-database", + ) + cnx = engine.connect() + cnx.execute("SELECT 1 + 1;").fetchall() + spans = self.memory_exporter.get_finished_spans() + + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].name, "sqlite.query") + + def test_create_engine_wrapper(self): + SQLAlchemyInstrumentor().instrument() + from sqlalchemy import create_engine # pylint: disable-all + + engine = create_engine("sqlite:///:memory:") + cnx = engine.connect() + cnx.execute("SELECT 1 + 1;").fetchall() + spans = self.memory_exporter.get_finished_spans() + + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].name, "sqlite.query") diff --git a/tox.ini b/tox.ini index ef8b633403..118d0b0963 100644 --- a/tox.ini +++ b/tox.ini @@ -87,6 +87,10 @@ envlist = ; opentelemetry-ext-grpc py3{4,5,6,7,8}-test-ext-grpc + ; opentelemetry-ext-sqlalchemy + py3{4,5,6,7,8}-test-ext-sqlalchemy + pypy3-test-ext-sqlalchemy + ; opentelemetry-ext-redis py3{4,5,6,7,8}-test-ext-redis pypy3-test-ext-redis @@ -136,6 +140,7 @@ changedir = test-example-basic-tracer: docs/examples/basic_tracer/tests test-example-http: docs/examples/http/tests test-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests + test-ext-sqlalchemy: ext/opentelemetry-ext-sqlalchemy/tests test-ext-redis: ext/opentelemetry-ext-redis/tests commands_pre = @@ -198,6 +203,9 @@ commands_pre = zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin + sqlalchemy: pip install {toxinidir}/opentelemetry-auto-instrumentation + sqlalchemy: pip install {toxinidir}/ext/opentelemetry-ext-sqlalchemy + ; In order to get a healthy coverage report, ; we have to install packages in editable mode. coverage: python {toxinidir}/scripts/eachdist.py install --editable @@ -292,6 +300,7 @@ deps = pymongo ~= 3.1 pymysql ~= 0.9.3 psycopg2-binary ~= 2.8.4 + sqlalchemy ~= 1.3.16 redis ~= 3.3.11 changedir = @@ -307,6 +316,7 @@ commands_pre = -e {toxinidir}/ext/opentelemetry-ext-psycopg2 \ -e {toxinidir}/ext/opentelemetry-ext-pymongo \ -e {toxinidir}/ext/opentelemetry-ext-pymysql \ + -e {toxinidir}/ext/opentelemetry-ext-sqlalchemy \ -e {toxinidir}/ext/opentelemetry-ext-redis docker-compose up -d python check_availability.py From 299295038dab56392199e3d79522550b78bd6e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 29 Apr 2020 16:30:58 -0500 Subject: [PATCH 0314/1517] ext/pymysql: Add Instrumentor (#611) Co-authored-by: Diego Hurtado --- .../src/opentelemetry/ext/dbapi/__init__.py | 10 +++- .../tests/test_dbapi_integration.py | 38 +++++++++++-- .../tests/pymysql/test_pymysql_functional.py | 26 +++------ ext/opentelemetry-ext-pymysql/CHANGELOG.md | 3 + ext/opentelemetry-ext-pymysql/README.rst | 4 -- ext/opentelemetry-ext-pymysql/setup.cfg | 9 +++ .../src/opentelemetry/ext/pymysql/__init__.py | 46 +++++++++------- .../tests/test_pymysql_integration.py | 55 ++++++++++++++++--- .../util/src/opentelemetry/test/test_base.py | 12 ++++ tox.ini | 3 +- 10 files changed, 149 insertions(+), 57 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index 7a94b58957..abb936bf7b 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -125,6 +125,14 @@ def wrap_connect_( logger.warning("Failed to integrate with DB API. %s", str(ex)) +def unwrap_connect( + connect_module: typing.Callable[..., any], connect_method_name: str, +): + conn = getattr(connect_module, connect_method_name, None) + if isinstance(conn, wrapt.ObjectProxy): + setattr(connect_module, connect_method_name, conn.__wrapped__) + + class DatabaseApiIntegration: def __init__( self, @@ -184,7 +192,7 @@ def get_connection_attributes(self, connection): self.name += "." + self.database user = self.connection_props.get("user") if user is not None: - self.span_attributes["db.user"] = user + self.span_attributes["db.user"] = str(user) host = self.connection_props.get("host") if host is not None: self.span_attributes["net.peer.name"] = host diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py index 9d894a2ccb..6614a8809b 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +from unittest import mock + from opentelemetry import trace as trace_api -from opentelemetry.ext.dbapi import DatabaseApiIntegration +from opentelemetry.ext import dbapi from opentelemetry.test.test_base import TestBase @@ -35,7 +37,7 @@ def test_span_succeeded(self): "host": "server_host", "user": "user", } - db_integration = DatabaseApiIntegration( + db_integration = dbapi.DatabaseApiIntegration( self.tracer, "testcomponent", "testtype", connection_attributes ) mock_connection = db_integration.wrapped_connection( @@ -66,7 +68,9 @@ def test_span_succeeded(self): ) def test_span_failed(self): - db_integration = DatabaseApiIntegration(self.tracer, "testcomponent") + db_integration = dbapi.DatabaseApiIntegration( + self.tracer, "testcomponent" + ) mock_connection = db_integration.wrapped_connection( mock_connect, {}, {} ) @@ -85,7 +89,9 @@ def test_span_failed(self): self.assertEqual(span.status.description, "Test Exception") def test_executemany(self): - db_integration = DatabaseApiIntegration(self.tracer, "testcomponent") + db_integration = dbapi.DatabaseApiIntegration( + self.tracer, "testcomponent" + ) mock_connection = db_integration.wrapped_connection( mock_connect, {}, {} ) @@ -97,7 +103,9 @@ def test_executemany(self): self.assertEqual(span.attributes["db.statement"], "Test query") def test_callproc(self): - db_integration = DatabaseApiIntegration(self.tracer, "testcomponent") + db_integration = dbapi.DatabaseApiIntegration( + self.tracer, "testcomponent" + ) mock_connection = db_integration.wrapped_connection( mock_connect, {}, {} ) @@ -110,6 +118,26 @@ def test_callproc(self): span.attributes["db.statement"], "Test stored procedure" ) + @mock.patch("opentelemetry.ext.dbapi") + def test_wrap_connect(self, mock_dbapi): + dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-") + connection = mock_dbapi.connect() + self.assertEqual(mock_dbapi.connect.call_count, 1) + self.assertIsInstance(connection, dbapi.TracedConnectionProxy) + self.assertIsInstance(connection.__wrapped__, mock.Mock) + + @mock.patch("opentelemetry.ext.dbapi") + def test_unwrap_connect(self, mock_dbapi): + dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-") + connection = mock_dbapi.connect() + self.assertEqual(mock_dbapi.connect.call_count, 1) + self.assertIsInstance(connection, dbapi.TracedConnectionProxy) + + dbapi.unwrap_connect(mock_dbapi, "connect") + connection = mock_dbapi.connect() + self.assertEqual(mock_dbapi.connect.call_count, 2) + self.assertIsInstance(connection, mock.Mock) + # pylint: disable=unused-argument def mock_connect(*args, **kwargs): diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py index a7dc213655..06507c4f35 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py @@ -13,18 +13,12 @@ # limitations under the License. import os -import time -import unittest import pymysql as pymy from opentelemetry import trace as trace_api -from opentelemetry.ext.pymysql import trace_integration -from opentelemetry.sdk.trace import Tracer, TracerProvider -from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, -) +from opentelemetry.ext.pymysql import PyMySQLInstrumentor +from opentelemetry.test.test_base import TestBase MYSQL_USER = os.getenv("MYSQL_USER ", "testuser") MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD ", "testpassword") @@ -33,17 +27,14 @@ MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME ", "opentelemetry-tests") -class TestFunctionalPyMysql(unittest.TestCase): +class TestFunctionalPyMysql(TestBase): @classmethod def setUpClass(cls): + super().setUpClass() cls._connection = None cls._cursor = None - cls._tracer_provider = TracerProvider() - cls._tracer = Tracer(cls._tracer_provider, None) - cls._span_exporter = InMemorySpanExporter() - cls._span_processor = SimpleExportSpanProcessor(cls._span_exporter) - cls._tracer_provider.add_span_processor(cls._span_processor) - trace_integration(cls._tracer_provider) + cls._tracer = cls.tracer_provider.get_tracer(__name__) + PyMySQLInstrumentor().instrument() cls._connection = pymy.connect( user=MYSQL_USER, password=MYSQL_PASSWORD, @@ -58,11 +49,8 @@ def tearDownClass(cls): if cls._connection: cls._connection.close() - def setUp(self): - self._span_exporter.clear() - def validate_spans(self): - spans = self._span_exporter.get_finished_spans() + spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) for span in spans: if span.name == "rootSpan": diff --git a/ext/opentelemetry-ext-pymysql/CHANGELOG.md b/ext/opentelemetry-ext-pymysql/CHANGELOG.md index 1512c42162..224f1bac53 100644 --- a/ext/opentelemetry-ext-pymysql/CHANGELOG.md +++ b/ext/opentelemetry-ext-pymysql/CHANGELOG.md @@ -1,3 +1,6 @@ # Changelog ## Unreleased + +- Implement PyMySQL integration ([#504](https://github.com/open-telemetry/opentelemetry-python/pull/504)) +- Implement instrumentor interface ([#611](https://github.com/open-telemetry/opentelemetry-python/pull/611)) diff --git a/ext/opentelemetry-ext-pymysql/README.rst b/ext/opentelemetry-ext-pymysql/README.rst index 3cf845366b..455d8fa7bd 100644 --- a/ext/opentelemetry-ext-pymysql/README.rst +++ b/ext/opentelemetry-ext-pymysql/README.rst @@ -6,10 +6,6 @@ OpenTelemetry PyMySQL integration .. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-pymysql.svg :target: https://pypi.org/project/opentelemetry-ext-pymysql/ -Integration with PyMySQL that supports the PyMySQL library and is -specified to trace_integration using 'PyMySQL'. - - Installation ------------ diff --git a/ext/opentelemetry-ext-pymysql/setup.cfg b/ext/opentelemetry-ext-pymysql/setup.cfg index 9f44953a40..9564f3f637 100644 --- a/ext/opentelemetry-ext-pymysql/setup.cfg +++ b/ext/opentelemetry-ext-pymysql/setup.cfg @@ -42,7 +42,16 @@ packages=find_namespace: install_requires = opentelemetry-api == 0.7.dev0 opentelemetry-ext-dbapi == 0.7.dev0 + opentelemetry-auto-instrumentation == 0.7.dev0 PyMySQL ~= 0.9.3 +[options.extras_require] +test = + opentelemetry-test == 0.7.dev0 + [options.packages.find] where = src + +[options.entry_points] +opentelemetry_instrumentor = + pymysql = opentelemetry.ext.pymysql:PyMySQLInstrumentor diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py index 0f06578e0b..29baedc5cd 100644 --- a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py @@ -13,8 +13,8 @@ # limitations under the License. """ -The integration with PyMySQL supports the `PyMySQL`_ library and is specified -to ``trace_integration`` using ``'PyMySQL'``. +The integration with PyMySQL supports the `PyMySQL`_ library and can be enabled +by using ``PyMySQLInstrumentor``. .. _PyMySQL: https://pypi.org/project/PyMySQL/ @@ -25,11 +25,13 @@ import pymysql from opentelemetry import trace - from opentelemetry.ext.pymysql import trace_integration + from opentelemetry.ext.pymysql import PyMySQLInstrumentor from opentelemetry.sdk.trace import TracerProvider trace.set_tracer_provider(TracerProvider()) - trace_integration() + + PyMySQLInstrumentor().instrument() + cnx = pymysql.connect(database="MySQL_Database") cursor = cnx.cursor() cursor.execute("INSERT INTO test (testField) VALUES (123)" @@ -45,24 +47,30 @@ import pymysql -from opentelemetry.ext.dbapi import wrap_connect +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.ext.dbapi import unwrap_connect, wrap_connect from opentelemetry.ext.pymysql.version import __version__ from opentelemetry.trace import TracerProvider, get_tracer -def trace_integration(tracer_provider: typing.Optional[TracerProvider] = None): - """Integrate with the PyMySQL library. - https://github.com/PyMySQL/PyMySQL/ - """ +class PyMySQLInstrumentor(BaseInstrumentor): + def _instrument(self, **kwargs): + """Integrate with the PyMySQL library. + https://github.com/PyMySQL/PyMySQL/ + """ + tracer_provider = kwargs.get("tracer_provider") + + tracer = get_tracer(__name__, __version__, tracer_provider) - tracer = get_tracer(__name__, __version__, tracer_provider) + connection_attributes = { + "database": "db", + "port": "port", + "host": "host", + "user": "user", + } + wrap_connect( + tracer, pymysql, "connect", "mysql", "sql", connection_attributes + ) - connection_attributes = { - "database": "db", - "port": "port", - "host": "host", - "user": "user", - } - wrap_connect( - tracer, pymysql, "connect", "mysql", "sql", connection_attributes - ) + def _uninstrument(self, **kwargs): + unwrap_connect(pymysql, "connect") diff --git a/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py b/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py index c452c31ec6..efcd8af2f7 100644 --- a/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py +++ b/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py @@ -17,18 +17,26 @@ import pymysql import opentelemetry.ext.pymysql -from opentelemetry.ext.pymysql import trace_integration +from opentelemetry.ext.pymysql import PyMySQLInstrumentor +from opentelemetry.sdk import resources from opentelemetry.test.test_base import TestBase class TestPyMysqlIntegration(TestBase): - def test_trace_integration(self): - with mock.patch("pymysql.connect"): - trace_integration() - cnx = pymysql.connect(database="test") - cursor = cnx.cursor() - query = "SELECT * FROM test" - cursor.execute(query) + def tearDown(self): + super().tearDown() + with self.disable_logging(): + PyMySQLInstrumentor().uninstrument() + + @mock.patch("pymysql.connect") + # pylint: disable=unused-argument + def test_instrumentor(self, mock_connect): + PyMySQLInstrumentor().instrument() + + cnx = pymysql.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @@ -36,3 +44,34 @@ def test_trace_integration(self): # Check version and name in span's instrumentation info self.check_span_instrumentation_info(span, opentelemetry.ext.pymysql) + + # check that no spans are generated after uninstrument + PyMySQLInstrumentor().uninstrument() + + cnx = pymysql.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + @mock.patch("pymysql.connect") + # pylint: disable=unused-argument + def test_custom_tracer_provider(self, mock_connect): + resource = resources.Resource.create({}) + result = self.create_tracer_provider(resource=resource) + tracer_provider, exporter = result + + PyMySQLInstrumentor().instrument(tracer_provider=tracer_provider) + + cnx = pymysql.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertIs(span.resource, resource) diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index 787439b1d7..ca015ff011 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import unittest +from contextlib import contextmanager from opentelemetry import trace as trace_api from opentelemetry.sdk.trace import TracerProvider, export @@ -59,3 +61,13 @@ def create_tracer_provider(**kwargs): tracer_provider.add_span_processor(span_processor) return tracer_provider, memory_exporter + + @staticmethod + @contextmanager + def disable_logging(highest_level=logging.CRITICAL): + logging.disable(highest_level) + + try: + yield + finally: + logging.disable(logging.NOTSET) diff --git a/tox.ini b/tox.ini index 118d0b0963..c9b1df4bfb 100644 --- a/tox.ini +++ b/tox.ini @@ -188,8 +188,9 @@ commands_pre = psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-psycopg2 + pymysql: pip install {toxinidir}/opentelemetry-auto-instrumentation pymysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi - pymysql: pip install {toxinidir}/ext/opentelemetry-ext-pymysql + pymysql: pip install {toxinidir}/ext/opentelemetry-ext-pymysql[test] redis: pip install {toxinidir}/opentelemetry-auto-instrumentation redis: pip install {toxinidir}/ext/opentelemetry-ext-redis[test] From d01d779e0132c351f3344fafa6c4887e719887ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Thu, 30 Apr 2020 18:35:15 -0500 Subject: [PATCH 0315/1517] Docs: fix docs dependencies (#625) --- docs-requirements.txt | 16 +++++++++------- docs/ext/grpc/grpc.client_interceptor.rst | 7 ------- docs/ext/grpc/grpc.rst | 11 ++--------- docs/ext/grpc/grpc.server_interceptor.rst | 7 ------- tox.ini | 17 +---------------- 5 files changed, 12 insertions(+), 46 deletions(-) delete mode 100644 docs/ext/grpc/grpc.client_interceptor.rst delete mode 100644 docs/ext/grpc/grpc.server_interceptor.rst diff --git a/docs-requirements.txt b/docs-requirements.txt index 2b8f732401..f2d6ec2a57 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -3,12 +3,14 @@ sphinx-rtd-theme~=0.4 sphinx-autodoc-typehints~=1.10.2 # Required by ext packages -opentracing~=2.2.0 Deprecated>=1.2.6 -thrift>=0.10.0 -pymongo~=3.1 +PyMySQL~=0.9.3 flask~=1.0 -mysql-connector-python ~= 8.0 -wrapt >= 1.0.0, < 2.0.0 -psycopg2-binary >= 2.7.3.1 -prometheus_client >= 0.5.0, < 1.0.0 +mysql-connector-python~=8.0 +opentracing~=2.2.0 +prometheus_client>=0.5.0,<1.0.0 +psycopg2-binary>=2.7.3.1 +pymongo~=3.1 +redis>=2.6 +thrift>=0.10.0 +wrapt >=1.0.0,<2.0.0 diff --git a/docs/ext/grpc/grpc.client_interceptor.rst b/docs/ext/grpc/grpc.client_interceptor.rst deleted file mode 100644 index 46de43810a..0000000000 --- a/docs/ext/grpc/grpc.client_interceptor.rst +++ /dev/null @@ -1,7 +0,0 @@ -grpc.client\_interceptor module -=============================== - -.. automodule:: opentelemetry.ext.grpc.client_interceptor - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/ext/grpc/grpc.rst b/docs/ext/grpc/grpc.rst index 7e7d5f5426..8a0775f28f 100644 --- a/docs/ext/grpc/grpc.rst +++ b/docs/ext/grpc/grpc.rst @@ -1,12 +1,5 @@ -.. include:: ../../../ext/opentelemetry-ext-grpc/README.rst - -Submodules ----------- - -.. toctree:: - - grpc.client_interceptor - grpc.server_interceptor +OpenTelemetry gRPC Integration +============================== Module contents --------------- diff --git a/docs/ext/grpc/grpc.server_interceptor.rst b/docs/ext/grpc/grpc.server_interceptor.rst deleted file mode 100644 index b51ae1a7cb..0000000000 --- a/docs/ext/grpc/grpc.server_interceptor.rst +++ /dev/null @@ -1,7 +0,0 @@ -grpc.server\_interceptor module -=============================== - -.. automodule:: opentelemetry.ext.grpc.server_interceptor - :members: - :undoc-members: - :show-inheritance: diff --git a/tox.ini b/tox.ini index c9b1df4bfb..1b0243c072 100644 --- a/tox.ini +++ b/tox.ini @@ -250,22 +250,7 @@ commands = [testenv:docs] deps = -c dev-requirements.txt - -c docs-requirements.txt - sphinx - sphinx-rtd-theme - sphinx-autodoc-typehints - # Required by ext packages - opentracing - Deprecated - thrift - pymongo - redis - flask - pymysql - mysql-connector-python - wrapt - psycopg2-binary - prometheus_client + -r docs-requirements.txt changedir = docs From c772bb7deb659fd3ba27a235b77c31ddea23169d Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 30 Apr 2020 16:56:11 -0700 Subject: [PATCH 0316/1517] fix: b3 propagation needs to check if parent is SpanContext (#621) --- .../src/opentelemetry/sdk/trace/propagation/b3_format.py | 2 +- opentelemetry-sdk/tests/trace/propagation/test_b3_format.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py index e082ed03e4..c0a080e631 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py @@ -137,7 +137,7 @@ def inject( set_in_carrier( carrier, self.PARENT_SPAN_ID_KEY, - format_span_id(span.parent.context.span_id), + format_span_id(span.parent.span_id), ) set_in_carrier(carrier, self.SAMPLED_KEY, "1" if sampled else "0") diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index 1f6d07707f..e6c644dcdb 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -45,7 +45,7 @@ def get_child_parent_new_carrier(old_carrier): trace_flags=parent_context.trace_flags, trace_state=parent_context.trace_state, ), - parent=parent, + parent=parent.get_context(), ) new_carrier = {} From 6babff1ed23f2c8ea1b48e1c3843d43bd3678923 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 30 Apr 2020 23:06:41 -0600 Subject: [PATCH 0317/1517] auto-instr: Fix the auto instrumentation command (#567) Right now the auto instrumentation command works like this: opentelemetry-auto-instrumentation some_file.py The auto instrumentation command invokes an interpreter that executes the some_file.py. Nevertheless, this will not work for frameworks that have their own execution command (flask_run, for example). Fixes #566 Co-authored-by: Alex Boten Co-authored-by: Chris Kleinknecht --- docs/examples/auto-instrumentation/README.md | 81 ++++++++++--- .../server_instrumented.py | 3 + .../auto_instrumentation/__init__.py | 6 +- .../auto_instrumentation.py | 38 +++++-- .../auto_instrumentation/sitecustomize.py | 28 +++++ .../tests/test_run.py | 106 ++++++++++++++++++ 6 files changed, 235 insertions(+), 27 deletions(-) create mode 100644 opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/sitecustomize.py create mode 100644 opentelemetry-auto-instrumentation/tests/test_run.py diff --git a/docs/examples/auto-instrumentation/README.md b/docs/examples/auto-instrumentation/README.md index 46b0b44b2c..7ed40f6b96 100644 --- a/docs/examples/auto-instrumentation/README.md +++ b/docs/examples/auto-instrumentation/README.md @@ -55,11 +55,9 @@ $ source auto_instrumentation/bin/activate # Installation ```sh -$ pip install opentelemetry-api $ pip install opentelemetry-sdk $ pip install opentelemetry-auto-instrumentation -$ pip install ext/opentelemetry-ext-flask -$ pip install flask +$ pip install opentelemetry-ext-flask $ pip install requests ``` @@ -71,20 +69,46 @@ This is done in 2 separate consoles, one to run each of the scripts that make up ```sh $ source auto_instrumentation/bin/activate -$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/server_instrumented.py +$ python opentelemetry-python/docs/examples/auto-instrumentation/server_instrumented.py ``` ```sh $ source auto_instrumentation/bin/activate -$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/client.py testing +$ python opentelemetry-python/docs/examples/auto-instrumentation/client.py testing ``` The execution of `server_instrumented.py` should return an output similar to: ```sh -Hello, testing! -Span(name="serv_request", context=SpanContext(trace_id=0x9c0e0ce8f7b7dbb51d1d6e744a4dad49, span_id=0xd1ba3ec4c76a0d7f, trace_state={}), kind=SpanKind.INTERNAL, parent=None, start_time=2020-03-19T00:06:31.275719Z, end_time=2020-03-19T00:06:31.275920Z) -127.0.0.1 - - [18/Mar/2020 18:06:31] "GET /serv_request?helloStr=Hello%2C+testing%21 HTTP/1.1" 200 - +{ + "name": "server_request", + "context": { + "trace_id": "0xfa002aad260b5f7110db674a9ddfcd23", + "span_id": "0x8b8bbaf3ca9c5131", + "trace_state": "{}" + }, + "kind": "SpanKind.SERVER", + "parent_id": null, + "start_time": "2020-04-30T17:28:57.886397Z", + "end_time": "2020-04-30T17:28:57.886490Z", + "status": { + "canonical_code": "OK" + }, + "attributes": { + "component": "http", + "http.method": "GET", + "http.server_name": "127.0.0.1", + "http.scheme": "http", + "host.port": 8082, + "http.host": "localhost:8082", + "http.target": "/server_request?param=testing", + "net.peer.ip": "127.0.0.1", + "net.peer.port": 52872, + "http.flavor": "1.1" + }, + "events": [], + "links": [] +} ``` ## Execution of an automatically instrumented server @@ -92,21 +116,50 @@ Span(name="serv_request", context=SpanContext(trace_id=0x9c0e0ce8f7b7dbb51d1d6e7 Now, kill the execution of `server_instrumented.py` with `ctrl + c` and run this instead: ```sh -$ opentelemetry-auto-instrumentation opentelemetry-python/opentelemetry-auto-instrumentation/example/server_uninstrumented.py +$ opentelemetry-auto-instrumentation python docs/examples/auto-instrumentation/server_uninstrumented.py ``` In the console where you previously executed `client.py`, run again this again: ```sh -$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/client.py testing +$ python opentelemetry-python/docs/examples/auto-instrumentation/client.py testing ``` The execution of `server_uninstrumented.py` should return an output similar to: ```sh -Hello, testing! -Span(name="serv_request", context=SpanContext(trace_id=0xf26b28b5243e48f5f96bfc753f95f3f0, span_id=0xbeb179a095d087ed, trace_state={}), kind=SpanKind.SERVER, parent=, start_time=2020-03-19T00:24:18.828561Z, end_time=2020-03-19T00:24:18.845127Z) -127.0.0.1 - - [18/Mar/2020 18:24:18] "GET /serv_request?helloStr=Hello%2C+testing%21 HTTP/1.1" 200 - +{ + "name": "server_request", + "context": { + "trace_id": "0x9f528e0b76189f539d9c21b1a7a2fc24", + "span_id": "0xd79760685cd4c269", + "trace_state": "{}" + }, + "kind": "SpanKind.SERVER", + "parent_id": "0xb4fb7eee22ef78e4", + "start_time": "2020-04-30T17:10:02.400604Z", + "end_time": "2020-04-30T17:10:02.401858Z", + "status": { + "canonical_code": "OK" + }, + "attributes": { + "component": "http", + "http.method": "GET", + "http.server_name": "127.0.0.1", + "http.scheme": "http", + "host.port": 8082, + "http.host": "localhost:8082", + "http.target": "/server_request?param=testing", + "net.peer.ip": "127.0.0.1", + "net.peer.port": 48240, + "http.flavor": "1.1", + "http.route": "/server_request", + "http.status_text": "OK", + "http.status_code": 200 + }, + "events": [], + "links": [] +} ``` -As you can see, both outputs are equivalentsince the automatic instrumentation does what the manual instrumentation does too. +Both outputs are equivalent since the automatic instrumentation does what the manual instrumentation does too. diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_instrumented.py index 1c78aab15d..528b107e03 100644 --- a/docs/examples/auto-instrumentation/server_instrumented.py +++ b/docs/examples/auto-instrumentation/server_instrumented.py @@ -15,6 +15,7 @@ from flask import Flask, request from opentelemetry import propagators, trace +from opentelemetry.ext.wsgi import collect_request_attributes from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -38,6 +39,8 @@ def server_request(): parent=propagators.extract( lambda dict_, key: dict_.get(key, []), request.headers )["current-span"], + kind=trace.SpanKind.SERVER, + attributes=collect_request_attributes(request.environ), ): print(request.args.get("param")) return "served" diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py index adfb4fd461..0d8d7dff27 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py @@ -20,9 +20,9 @@ :: - opentelemetry-auto-instrumentation program.py + opentelemetry-auto-instrumentation python program.py The code in ``program.py`` needs to use one of the packages for which there is -an OpenTelemetry extension. For a list of the available extensions please check -`here `_. +an OpenTelemetry integration. For a list of the available integrations please +check `here `_. """ diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py index 00ccf6a0ea..893b8939b9 100644 --- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py @@ -15,22 +15,40 @@ # limitations under the License. from logging import getLogger -from runpy import run_path +from os import environ, execl, getcwd +from os.path import abspath, dirname, pathsep +from shutil import which from sys import argv -from pkg_resources import iter_entry_points - logger = getLogger(__file__) def run() -> None: - for entry_point in iter_entry_points("opentelemetry_instrumentor"): - try: - entry_point.load()().instrument() # type: ignore - logger.debug("Instrumented %s", entry_point.name) + python_path = environ.get("PYTHONPATH") + + if not python_path: + python_path = [] + + else: + python_path = python_path.split(pathsep) + + cwd_path = getcwd() + + # This is being added to support applications that are being run from their + # own executable, like Django. + # FIXME investigate if there is another way to achieve this + if cwd_path not in python_path: + python_path.insert(0, cwd_path) + + filedir_path = dirname(abspath(__file__)) + + python_path = [path for path in python_path if path != filedir_path] + + python_path.insert(0, filedir_path) + + environ["PYTHONPATH"] = pathsep.join(python_path) - except Exception: # pylint: disable=broad-except - logger.exception("Instrumenting of %s failed", entry_point.name) + executable = which(argv[1]) - run_path(argv[1], run_name="__main__") # type: ignore + execl(executable, executable, *argv[2:]) diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/sitecustomize.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/sitecustomize.py new file mode 100644 index 0000000000..b070bf5d77 --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/sitecustomize.py @@ -0,0 +1,28 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from logging import getLogger + +from pkg_resources import iter_entry_points + +logger = getLogger(__file__) + + +for entry_point in iter_entry_points("opentelemetry_instrumentor"): + try: + entry_point.load()().instrument() # type: ignore + logger.debug("Instrumented %s", entry_point.name) + + except Exception: # pylint: disable=broad-except + logger.exception("Instrumenting of %s failed", entry_point.name) diff --git a/opentelemetry-auto-instrumentation/tests/test_run.py b/opentelemetry-auto-instrumentation/tests/test_run.py new file mode 100644 index 0000000000..8b37882f5b --- /dev/null +++ b/opentelemetry-auto-instrumentation/tests/test_run.py @@ -0,0 +1,106 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from os import environ, getcwd +from os.path import abspath, dirname, pathsep +from unittest import TestCase +from unittest.mock import patch + +from opentelemetry.auto_instrumentation import auto_instrumentation + + +class TestRun(TestCase): + auto_instrumentation_path = dirname(abspath(auto_instrumentation.__file__)) + + @classmethod + def setUpClass(cls): + cls.argv_patcher = patch( + "opentelemetry.auto_instrumentation.auto_instrumentation.argv" + ) + cls.execl_patcher = patch( + "opentelemetry.auto_instrumentation.auto_instrumentation.execl" + ) + cls.which_patcher = patch( + "opentelemetry.auto_instrumentation.auto_instrumentation.which" + ) + + cls.argv_patcher.start() + cls.execl_patcher.start() + cls.which_patcher.start() + + @classmethod + def tearDownClass(cls): + cls.argv_patcher.stop() + cls.execl_patcher.stop() + cls.which_patcher.stop() + + @patch.dict("os.environ", {"PYTHONPATH": ""}) + def test_empty(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd()]), + ) + + @patch.dict("os.environ", {"PYTHONPATH": "abc"}) + def test_non_empty(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), + ) + + @patch.dict( + "os.environ", + {"PYTHONPATH": pathsep.join(["abc", auto_instrumentation_path])}, + ) + def test_after_path(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), + ) + + @patch.dict( + "os.environ", + { + "PYTHONPATH": pathsep.join( + [auto_instrumentation_path, "abc", auto_instrumentation_path] + ) + }, + ) + def test_single_path(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), + ) + + +class TestExecl(TestCase): + @patch( + "opentelemetry.auto_instrumentation.auto_instrumentation.argv", + new=[1, 2, 3], + ) + @patch("opentelemetry.auto_instrumentation.auto_instrumentation.which") + @patch("opentelemetry.auto_instrumentation.auto_instrumentation.execl") + def test_execl( + self, mock_execl, mock_which + ): # pylint: disable=no-self-use + mock_which.configure_mock(**{"return_value": "python"}) + + auto_instrumentation.run() + + mock_execl.assert_called_with("python", "python", 3) From 1d84ee9d918c56cc0606b07ee9ac0ea336682b5e Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Sat, 2 May 2020 21:09:47 -0700 Subject: [PATCH 0318/1517] flask: Add exclude lists for flask integration (#630) Leverage global configurations to allow users to specify paths and hosts that they do not want to trace within their Flask applications. OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_HOSTS and OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_PATHS are the env variables used. Use a comma delimited string to represent seperate hosts/paths to blacklist. --- ext/opentelemetry-ext-flask/CHANGELOG.md | 3 ++ ext/opentelemetry-ext-flask/README.rst | 11 ++++ .../src/opentelemetry/ext/flask/__init__.py | 51 ++++++++++++++----- .../tests/test_flask_integration.py | 37 ++++++++++++-- .../src/opentelemetry/util/__init__.py | 36 ++++++++++--- 5 files changed, 114 insertions(+), 24 deletions(-) diff --git a/ext/opentelemetry-ext-flask/CHANGELOG.md b/ext/opentelemetry-ext-flask/CHANGELOG.md index f7523f36c4..7d4d85b719 100644 --- a/ext/opentelemetry-ext-flask/CHANGELOG.md +++ b/ext/opentelemetry-ext-flask/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add exclude list for paths and hosts + ([#630](https://github.com/open-telemetry/opentelemetry-python/pull/630)) + ## 0.6b0 Released 2020-03-30 diff --git a/ext/opentelemetry-ext-flask/README.rst b/ext/opentelemetry-ext-flask/README.rst index 135b2c398c..0a4c894077 100644 --- a/ext/opentelemetry-ext-flask/README.rst +++ b/ext/opentelemetry-ext-flask/README.rst @@ -16,6 +16,17 @@ Installation pip install opentelemetry-ext-flask +Configuration +------------- + +Exclude lists +************* +Excludes certain hosts and paths from being tracked. Pass in comma delimited string into environment variables. +Host refers to the entire url and path refers to the part of the url after the domain. Host matches the exact string that is given, where as path matches if the url starts with the given excluded path. + +Excluded hosts: OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_HOSTS +Excluded paths: OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_PATHS + References ---------- diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index eb008eeadc..1e936da115 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -51,10 +51,14 @@ def hello(): import flask import opentelemetry.ext.wsgi as otel_wsgi -from opentelemetry import context, propagators, trace +from opentelemetry import configuration, context, propagators, trace from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.flask.version import __version__ -from opentelemetry.util import time_ns +from opentelemetry.util import ( + disable_tracing_hostname, + disable_tracing_path, + time_ns, +) logger = logging.getLogger(__name__) @@ -80,17 +84,18 @@ def wrapped_app(environ, start_response): environ[_ENVIRON_STARTTIME_KEY] = time_ns() def _start_response(status, response_headers, *args, **kwargs): - span = flask.request.environ.get(_ENVIRON_SPAN_KEY) - if span: - otel_wsgi.add_response_attributes( - span, status, response_headers - ) - else: - logger.warning( - "Flask environ's OpenTelemetry span " - "missing at _start_response(%s)", - status, - ) + if not _disable_trace(flask.request.url): + span = flask.request.environ.get(_ENVIRON_SPAN_KEY) + if span: + otel_wsgi.add_response_attributes( + span, status, response_headers + ) + else: + logger.warning( + "Flask environ's OpenTelemetry span " + "missing at _start_response(%s)", + status, + ) return start_response( status, response_headers, *args, **kwargs @@ -102,6 +107,9 @@ def _start_response(status, response_headers, *args, **kwargs): @self.before_request def _before_flask_request(): + # Do not trace if the url is excluded + if _disable_trace(flask.request.url): + return environ = flask.request.environ span_name = ( flask.request.endpoint @@ -132,6 +140,9 @@ def _before_flask_request(): @self.teardown_request def _teardown_flask_request(exc): + # Not traced if the url is excluded + if _disable_trace(flask.request.url): + return activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY) if not activation: logger.warning( @@ -150,6 +161,20 @@ def _teardown_flask_request(exc): context.detach(flask.request.environ.get(_ENVIRON_TOKEN)) +def _disable_trace(url): + excluded_hosts = configuration.Configuration().FLASK_EXCLUDED_HOSTS + excluded_paths = configuration.Configuration().FLASK_EXCLUDED_PATHS + if excluded_hosts: + excluded_hosts = str.split(excluded_hosts, ",") + if disable_tracing_hostname(url, excluded_hosts): + return True + if excluded_paths: + excluded_paths = str.split(excluded_paths, ",") + if disable_tracing_path(url, excluded_paths): + return True + return False + + class FlaskInstrumentor(BaseInstrumentor): """A instrumentor for flask.Flask diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py index 34432be3dd..1babfff2f5 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py @@ -13,12 +13,14 @@ # limitations under the License. import unittest +from unittest.mock import patch from flask import Flask, request from werkzeug.test import Client from werkzeug.wrappers import BaseResponse from opentelemetry import trace as trace_api +from opentelemetry.configuration import Configuration from opentelemetry.test.wsgitestutil import WsgiTestBase @@ -45,7 +47,8 @@ def setUp(self): # No instrumentation code is here because it is present in the # conftest.py file next to this file. super().setUp() - + Configuration._instance = None # pylint:disable=protected-access + Configuration.__slots__ = [] self.app = Flask(__name__) def hello_endpoint(helloid): @@ -53,10 +56,22 @@ def hello_endpoint(helloid): raise ValueError(":-(") return "Hello: " + str(helloid) + def excluded_endpoint(): + return "excluded" + + def excluded2_endpoint(): + return "excluded2" + self.app.route("/hello/")(hello_endpoint) + self.app.route("/excluded")(excluded_endpoint) + self.app.route("/excluded2")(excluded2_endpoint) self.client = Client(self.app, BaseResponse) + def tearDown(self): + Configuration._instance = None # pylint:disable=protected-access + Configuration.__slots__ = [] + def test_only_strings_in_environ(self): """ Some WSGI servers (such as Gunicorn) expect keys in the environ object @@ -80,9 +95,8 @@ def test_simple(self): expected_attrs = expected_attributes( {"http.target": "/hello/123", "http.route": "/hello/"} ) - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) + self.client.get("/hello/123") + span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "hello_endpoint") @@ -126,6 +140,21 @@ def test_internal_error(self): self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) + @patch.dict( + "os.environ", # type: ignore + { + "OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_HOSTS": "http://localhost/excluded", + "OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_PATHS": "excluded2", + }, + ) + def test_excluded_path(self): + self.client.get("/hello/123") + self.client.get("/excluded") + self.client.get("/excluded2") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "hello_endpoint") + if __name__ == "__main__": unittest.main() diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index 8701d9ffba..48c350730e 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -11,9 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import re import time from logging import getLogger -from typing import Union +from typing import Sequence, Union from pkg_resources import iter_entry_points @@ -33,18 +34,39 @@ def time_ns() -> int: return int(time.time() * 1e9) -def _load_provider(provider: str) -> Union["TracerProvider", "MeterProvider"]: # type: ignore +def _load_provider( + provider: str, +) -> Union["TracerProvider", "MeterProvider"]: # type: ignore try: return next( # type: ignore iter_entry_points( "opentelemetry_{}".format(provider), - name=getattr( # type: ignore - Configuration(), provider, "default_{}".format(provider), # type: ignore + name=getattr( + Configuration(), # type: ignore + provider, + "default_{}".format(provider), ), ) ).load()() except Exception: # pylint: disable=broad-except - logger.error( - "Failed to load configured provider %s", provider, - ) + logger.error("Failed to load configured provider %s", provider) raise + + +# Pattern for matching up until the first '/' after the 'https://' part. +_URL_PATTERN = r"(https?|ftp)://.*?/" + + +def disable_tracing_path(url: str, excluded_paths: Sequence[str]) -> bool: + if excluded_paths: + # Match only the part after the first '/' that is not in _URL_PATTERN + regex = "{}({})".format(_URL_PATTERN, "|".join(excluded_paths)) + if re.match(regex, url): + return True + return False + + +def disable_tracing_hostname( + url: str, excluded_hostnames: Sequence[str] +) -> bool: + return url in excluded_hostnames From 090b6640495bcb994a9467ab23e2b1abe7fc4af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Sat, 2 May 2020 23:31:15 -0500 Subject: [PATCH 0319/1517] requests: Rename http_requests to requests (#619) The requests integration is named http-requests because at the time it was created there were some problems with pylint. other integrations are using opentelemetry.ext.integration without problems, tests are passing without issue, even renamed. --- docs/examples/http/README.rst | 6 +++--- docs/examples/http/client.py | 4 ++-- docs/examples/http/server.py | 4 ++-- .../opentelemetry-example-app/setup.py | 2 +- .../flask_example.py | 4 ++-- .../requests.rst} | 2 +- docs/getting-started.rst | 6 +++--- .../CHANGELOG.md | 1 + .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 8 +++---- .../setup.cfg | 6 +++--- .../setup.py | 2 +- .../opentelemetry/ext/requests}/__init__.py | 6 +++--- .../opentelemetry/ext/requests}/version.py | 0 .../tests/__init__.py | 0 .../tests/test_requests_integration.py | 21 +++++++++---------- .../src/opentelemetry/metrics/__init__.py | 2 +- .../src/opentelemetry/trace/__init__.py | 2 +- scripts/coverage.sh | 2 +- tests/w3c_tracecontext_validation_server.py | 17 +++++++-------- tox.ini | 18 ++++++++-------- 22 files changed, 56 insertions(+), 57 deletions(-) rename docs/ext/{http_requests/http_requests.rst => requests/requests.rst} (72%) rename ext/{opentelemetry-ext-http-requests => opentelemetry-ext-requests}/CHANGELOG.md (70%) rename ext/{opentelemetry-ext-http-requests => opentelemetry-ext-requests}/LICENSE (100%) rename ext/{opentelemetry-ext-http-requests => opentelemetry-ext-requests}/MANIFEST.in (100%) rename ext/{opentelemetry-ext-http-requests => opentelemetry-ext-requests}/README.rst (58%) rename ext/{opentelemetry-ext-http-requests => opentelemetry-ext-requests}/setup.cfg (91%) rename ext/{opentelemetry-ext-http-requests => opentelemetry-ext-requests}/setup.py (91%) rename ext/{opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests => opentelemetry-ext-requests/src/opentelemetry/ext/requests}/__init__.py (97%) rename ext/{opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests => opentelemetry-ext-requests/src/opentelemetry/ext/requests}/version.py (100%) rename ext/{opentelemetry-ext-http-requests => opentelemetry-ext-requests}/tests/__init__.py (100%) rename ext/{opentelemetry-ext-http-requests => opentelemetry-ext-requests}/tests/test_requests_integration.py (91%) diff --git a/docs/examples/http/README.rst b/docs/examples/http/README.rst index c462d9f092..edbb5044f6 100644 --- a/docs/examples/http/README.rst +++ b/docs/examples/http/README.rst @@ -3,7 +3,7 @@ HTTP Integration Example This example shows how to use :doc:`WSGI Middleware <../../ext/wsgi/wsgi>` -and :doc:`requests <../../ext/http_requests/http_requests>` integrations to +and :doc:`requests <../../ext/requests/requests>` integrations to instrument an HTTP client and server in Python. The source files required to run this example are available :scm_web:`here `. @@ -16,7 +16,7 @@ Installation pip install opentelemetry-api pip install opentelemetry-sdk pip install opentelemetry-ext-wsgi - pip install opentelemetry-ext-http-requests + pip install opentelemetry-ext-requests pip install flask @@ -60,6 +60,6 @@ Useful links - OpenTelemetry_ - :doc:`../../api/trace` - :doc:`../../ext/wsgi/wsgi` -- :doc:`../../ext/http_requests/http_requests` +- :doc:`../../ext/requests/requests` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ \ No newline at end of file diff --git a/docs/examples/http/client.py b/docs/examples/http/client.py index 483e95b516..56b0ca6ea7 100755 --- a/docs/examples/http/client.py +++ b/docs/examples/http/client.py @@ -19,7 +19,7 @@ import requests from opentelemetry import trace -from opentelemetry.ext import http_requests +from opentelemetry.ext.requests import RequestsInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, @@ -32,7 +32,7 @@ trace.set_tracer_provider(TracerProvider()) # Enable instrumentation in the requests library. -http_requests.RequestsInstrumentor().instrument() +RequestsInstrumentor().instrument() # Configure a console span exporter. exporter = ConsoleSpanExporter() diff --git a/docs/examples/http/server.py b/docs/examples/http/server.py index 46e2e22103..10af2eb951 100755 --- a/docs/examples/http/server.py +++ b/docs/examples/http/server.py @@ -20,7 +20,7 @@ import requests from opentelemetry import trace -from opentelemetry.ext import http_requests +from opentelemetry.ext.requests import RequestsInstrumentor from opentelemetry.ext.wsgi import OpenTelemetryMiddleware from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( @@ -36,7 +36,7 @@ # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.RequestsInstrumentor().instrument() +RequestsInstrumentor().instrument() app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) diff --git a/docs/examples/opentelemetry-example-app/setup.py b/docs/examples/opentelemetry-example-app/setup.py index 9764784ba2..fb78cd8446 100644 --- a/docs/examples/opentelemetry-example-app/setup.py +++ b/docs/examples/opentelemetry-example-app/setup.py @@ -37,7 +37,7 @@ "typing; python_version<'3.5'", "opentelemetry-api", "opentelemetry-sdk", - "opentelemetry-ext-http-requests", + "opentelemetry-ext-requests", "opentelemetry-ext-flask", "flask", "requests", diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 1a9de31012..863d6f3389 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -19,7 +19,7 @@ import flask import requests -import opentelemetry.ext.http_requests +import opentelemetry.ext.requests from opentelemetry import trace from opentelemetry.ext.flask import FlaskInstrumentor from opentelemetry.sdk.trace import TracerProvider @@ -33,7 +33,7 @@ # It must be done before instrumenting any library trace.set_tracer_provider(TracerProvider()) -opentelemetry.ext.http_requests.RequestsInstrumentor().instrument() +opentelemetry.ext.requests.RequestsInstrumentor().instrument() FlaskInstrumentor().instrument() trace.get_tracer_provider().add_span_processor( diff --git a/docs/ext/http_requests/http_requests.rst b/docs/ext/requests/requests.rst similarity index 72% rename from docs/ext/http_requests/http_requests.rst rename to docs/ext/requests/requests.rst index 9cc09de547..5959d4c924 100644 --- a/docs/ext/http_requests/http_requests.rst +++ b/docs/ext/requests/requests.rst @@ -1,7 +1,7 @@ OpenTelemetry requests Integration ================================== -.. automodule:: opentelemetry.ext.http_requests +.. automodule:: opentelemetry.ext.requests :members: :undoc-members: :show-inheritance: diff --git a/docs/getting-started.rst b/docs/getting-started.rst index f4e6918d96..f25cf79b77 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -176,7 +176,7 @@ We will now instrument a basic Flask application that uses the requests library .. code-block:: sh pip install opentelemetry-ext-flask - pip install opentelemetry-ext-http-requests + pip install opentelemetry-ext-requests And let's write a small Flask application that sends an HTTP request, activating each instrumentation during the initialization: @@ -190,7 +190,7 @@ And let's write a small Flask application that sends an HTTP request, activating import flask import requests - import opentelemetry.ext.http_requests + import opentelemetry.ext.requests from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter @@ -202,7 +202,7 @@ And let's write a small Flask application that sends an HTTP request, activating ) app = flask.Flask(__name__) - opentelemetry.ext.http_requests.RequestsInstrumentor().instrument() + opentelemetry.ext.requests.RequestsInstrumentor().instrument() @app.route("/") def hello(): diff --git a/ext/opentelemetry-ext-http-requests/CHANGELOG.md b/ext/opentelemetry-ext-requests/CHANGELOG.md similarity index 70% rename from ext/opentelemetry-ext-http-requests/CHANGELOG.md rename to ext/opentelemetry-ext-requests/CHANGELOG.md index a3f8a8f1fb..34ca8ff226 100644 --- a/ext/opentelemetry-ext-http-requests/CHANGELOG.md +++ b/ext/opentelemetry-ext-requests/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Rename package to opentelemetry-ext-requests ([#619](https://github.com/open-telemetry/opentelemetry-python/pull/619)) - Implement instrumentor interface ([#597](https://github.com/open-telemetry/opentelemetry-python/pull/597)) ## 0.3a0 diff --git a/ext/opentelemetry-ext-http-requests/LICENSE b/ext/opentelemetry-ext-requests/LICENSE similarity index 100% rename from ext/opentelemetry-ext-http-requests/LICENSE rename to ext/opentelemetry-ext-requests/LICENSE diff --git a/ext/opentelemetry-ext-http-requests/MANIFEST.in b/ext/opentelemetry-ext-requests/MANIFEST.in similarity index 100% rename from ext/opentelemetry-ext-http-requests/MANIFEST.in rename to ext/opentelemetry-ext-requests/MANIFEST.in diff --git a/ext/opentelemetry-ext-http-requests/README.rst b/ext/opentelemetry-ext-requests/README.rst similarity index 58% rename from ext/opentelemetry-ext-http-requests/README.rst rename to ext/opentelemetry-ext-requests/README.rst index 88cdecf31e..95d80d4997 100644 --- a/ext/opentelemetry-ext-http-requests/README.rst +++ b/ext/opentelemetry-ext-requests/README.rst @@ -3,8 +3,8 @@ OpenTelemetry requests Integration |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-http-requests.svg - :target: https://pypi.org/project/opentelemetry-ext-http-requests/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-requests.svg + :target: https://pypi.org/project/opentelemetry-ext-requests/ This library allows tracing HTTP requests made by the `requests `_ library. @@ -14,10 +14,10 @@ Installation :: - pip install opentelemetry-ext-http-requests + pip install opentelemetry-ext-requests References ---------- -* `OpenTelemetry requests Integration `_ +* `OpenTelemetry requests Integration `_ * `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-requests/setup.cfg similarity index 91% rename from ext/opentelemetry-ext-http-requests/setup.cfg rename to ext/opentelemetry-ext-requests/setup.cfg index fc2ff72457..9ab8401dd0 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-requests/setup.cfg @@ -13,13 +13,13 @@ # limitations under the License. # [metadata] -name = opentelemetry-ext-http-requests +name = opentelemetry-ext-requests description = OpenTelemetry requests integration long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-http-requests +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-requests platforms = any license = Apache-2.0 classifiers = @@ -54,4 +54,4 @@ where = src [options.entry_points] opentelemetry_instrumentor = - requests = opentelemetry.ext.http_requests:RequestsInstrumentor + requests = opentelemetry.ext.requests:RequestsInstrumentor diff --git a/ext/opentelemetry-ext-http-requests/setup.py b/ext/opentelemetry-ext-requests/setup.py similarity index 91% rename from ext/opentelemetry-ext-http-requests/setup.py rename to ext/opentelemetry-ext-requests/setup.py index d7917ed893..a71a8d44b5 100644 --- a/ext/opentelemetry-ext-http-requests/setup.py +++ b/ext/opentelemetry-ext-requests/setup.py @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(__file__) VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "ext", "http_requests", "version.py" + BASE_DIR, "src", "opentelemetry", "ext", "requests", "version.py" ) PACKAGE_INFO = {} with open(VERSION_FILENAME) as f: diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py similarity index 97% rename from ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py rename to ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py index 9bb176dfd1..c98a24cc88 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py @@ -22,10 +22,10 @@ .. code-block:: python import requests - import opentelemetry.ext.http_requests + import opentelemetry.ext.requests # You can optionally pass a custom TracerProvider to RequestInstrumentor.instrument() - opentelemetry.ext.http_requests.RequestInstrumentor.instrument() + opentelemetry.ext.requests.RequestInstrumentor.instrument() response = requests.get(url="https://www.example.org/") Limitations @@ -49,7 +49,7 @@ from opentelemetry import context, propagators, trace from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.ext.http_requests.version import __version__ +from opentelemetry.ext.requests.version import __version__ from opentelemetry.trace import SpanKind, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py b/ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py similarity index 100% rename from ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py rename to ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/version.py diff --git a/ext/opentelemetry-ext-http-requests/tests/__init__.py b/ext/opentelemetry-ext-requests/tests/__init__.py similarity index 100% rename from ext/opentelemetry-ext-http-requests/tests/__init__.py rename to ext/opentelemetry-ext-requests/tests/__init__.py diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-requests/tests/test_requests_integration.py similarity index 91% rename from ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py rename to ext/opentelemetry-ext-requests/tests/test_requests_integration.py index 3cc0ac006a..7764aad3ec 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-requests/tests/test_requests_integration.py @@ -18,8 +18,9 @@ import requests import urllib3 +import opentelemetry.ext.requests from opentelemetry import context, propagators, trace -from opentelemetry.ext import http_requests +from opentelemetry.ext.requests import RequestsInstrumentor from opentelemetry.sdk import resources from opentelemetry.test.mock_httptextformat import MockHTTPTextFormat from opentelemetry.test.test_base import TestBase @@ -30,7 +31,7 @@ class TestRequestsIntegration(TestBase): def setUp(self): super().setUp() - http_requests.RequestsInstrumentor().instrument() + RequestsInstrumentor().instrument() httpretty.enable() httpretty.register_uri( httpretty.GET, self.URL, body="Hello!", @@ -38,7 +39,7 @@ def setUp(self): def tearDown(self): super().tearDown() - http_requests.RequestsInstrumentor().uninstrument() + RequestsInstrumentor().uninstrument() httpretty.disable() def test_basic(self): @@ -67,7 +68,7 @@ def test_basic(self): span.status.canonical_code, trace.status.StatusCanonicalCode.OK ) - self.check_span_instrumentation_info(span, http_requests) + self.check_span_instrumentation_info(span, opentelemetry.ext.requests) def test_not_foundbasic(self): url_404 = "http://httpbin.org/status/404" @@ -111,17 +112,17 @@ def test_invalid_url(self): ) def test_uninstrument(self): - http_requests.RequestsInstrumentor().uninstrument() + RequestsInstrumentor().uninstrument() result = requests.get(self.URL) self.assertEqual(result.text, "Hello!") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) # instrument again to avoid annoying warning message - http_requests.RequestsInstrumentor().instrument() + RequestsInstrumentor().instrument() def test_uninstrument_session(self): session1 = requests.Session() - http_requests.RequestsInstrumentor().uninstrument_session(session1) + RequestsInstrumentor().uninstrument_session(session1) result = session1.get(self.URL) self.assertEqual(result.text, "Hello!") @@ -186,10 +187,8 @@ def test_custom_tracer_provider(self): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result - http_requests.RequestsInstrumentor().uninstrument() - http_requests.RequestsInstrumentor().instrument( - tracer_provider=tracer_provider - ) + RequestsInstrumentor().uninstrument() + RequestsInstrumentor().instrument(tracer_provider=tracer_provider) result = requests.get(self.URL) self.assertEqual(result.text, "Hello!") diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index b3fe69ab59..b7ad62adb2 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -221,7 +221,7 @@ def get_meter( This should *not* be the name of the module that is instrumented but the name of the module doing the instrumentation. E.g., instead of ``"requests"``, use - ``"opentelemetry.ext.http_requests"``. + ``"opentelemetry.ext.requests"``. stateful: True/False to indicate whether the meter will be stateful. True indicates the meter computes checkpoints diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index a4b18a3ddc..13aaf2c6a4 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -461,7 +461,7 @@ def get_tracer( This should *not* be the name of the module that is instrumented but the name of the module doing the instrumentation. E.g., instead of ``"requests"``, use - ``"opentelemetry.ext.http_requests"``. + ``"opentelemetry.ext.requests"``. instrumenting_library_version: Optional. The version string of the instrumenting library. Usually this should be the same as diff --git a/scripts/coverage.sh b/scripts/coverage.sh index b9f9b6493e..839380c27f 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -18,7 +18,7 @@ coverage erase cov opentelemetry-api cov opentelemetry-sdk cov ext/opentelemetry-ext-flask -cov ext/opentelemetry-ext-http-requests +cov ext/opentelemetry-ext-requests cov ext/opentelemetry-ext-jaeger cov ext/opentelemetry-ext-opentracing-shim cov ext/opentelemetry-ext-wsgi diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py index 3e4eb985d7..5961a3b039 100644 --- a/tests/w3c_tracecontext_validation_server.py +++ b/tests/w3c_tracecontext_validation_server.py @@ -23,24 +23,23 @@ import flask import requests +from opentelemetry import trace +from opentelemetry.ext.requests import RequestsInstrumentor +from opentelemetry.ext.wsgi import OpenTelemetryMiddleware from opentelemetry.sdk.trace import TracerProvider - -# FIXME This could likely be avoided by integrating this script into the -# standard test running mechanisms. - -from opentelemetry import trace # noqa # isort:skip -from opentelemetry.ext import http_requests # noqa # isort:skip" -from opentelemetry.ext.wsgi import OpenTelemetryMiddleware # noqa # isort:skip -from opentelemetry.sdk.trace.export import ( # noqa # isort:skip +from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleExportSpanProcessor, ) +# FIXME This could likely be avoided by integrating this script into the +# standard test running mechanisms. + # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. trace.set_tracer_provider(TracerProvider()) -http_requests.RequestsInstrumentor().instrument() +RequestsInstrumentor().instrument() # SpanExporter receives the spans and send them to the target location. span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter()) diff --git a/tox.ini b/tox.ini index 1b0243c072..1570df787c 100644 --- a/tox.ini +++ b/tox.ini @@ -36,9 +36,9 @@ envlist = py3{4,5,6,7,8}-test-ext-flask pypy3-test-ext-flask - ; opentelemetry-ext-http-requests - py3{4,5,6,7,8}-test-ext-http-requests - pypy3-test-ext-http-requests + ; opentelemetry-ext-requests + py3{4,5,6,7,8}-test-ext-requests + pypy3-test-ext-requests ; opentelemetry-ext-jaeger py3{4,5,6,7,8}-test-ext-jaeger @@ -124,7 +124,7 @@ changedir = test-sdk: opentelemetry-sdk/tests test-auto-instrumentation: opentelemetry-auto-instrumentation/tests test-ext-grpc: ext/opentelemetry-ext-grpc/tests - test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests + test-ext-requests: ext/opentelemetry-ext-requests/tests test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests test-ext-mysql: ext/opentelemetry-ext-mysql/tests @@ -156,13 +156,13 @@ commands_pre = test-auto-instrumentation: pip install {toxinidir}/opentelemetry-auto-instrumentation example-app: pip install {toxinidir}/opentelemetry-auto-instrumentation - example-app: pip install {toxinidir}/ext/opentelemetry-ext-http-requests + example-app: pip install {toxinidir}/ext/opentelemetry-ext-requests example-app: pip install {toxinidir}/ext/opentelemetry-ext-wsgi example-app: pip install {toxinidir}/ext/opentelemetry-ext-flask example-app: pip install {toxinidir}/docs/examples/opentelemetry-example-app example-http: pip install -e {toxinidir}/opentelemetry-auto-instrumentation - example-http: pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests + example-http: pip install -e {toxinidir}/ext/opentelemetry-ext-requests example-http: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi example-http: pip install -r {toxinidir}/docs/examples/http/requirements.txt @@ -195,8 +195,8 @@ commands_pre = redis: pip install {toxinidir}/opentelemetry-auto-instrumentation redis: pip install {toxinidir}/ext/opentelemetry-ext-redis[test] - http-requests: pip install {toxinidir}/opentelemetry-auto-instrumentation - http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests[test] + requests: pip install {toxinidir}/opentelemetry-auto-instrumentation + requests: pip install {toxinidir}/ext/opentelemetry-ext-requests[test] jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger @@ -271,7 +271,7 @@ commands_pre = -e {toxinidir}/opentelemetry-auto-instrumentation \ -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/opentelemetry-auto-instrumentation \ - -e {toxinidir}/ext/opentelemetry-ext-http-requests \ + -e {toxinidir}/ext/opentelemetry-ext-requests \ -e {toxinidir}/ext/opentelemetry-ext-wsgi \ -e {toxinidir}/ext/opentelemetry-ext-flask From e1192855ee804c4a4dff2d9de14e63365f38d0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 4 May 2020 21:55:32 -0500 Subject: [PATCH 0320/1517] ext/docker-tests: Fix SQL docker tests (#646) The executemany test fails in some conditions because the argument used is a sequence and not a sequence of sequences as it should be. Fixes #589. --- .../tests/mysql/test_mysql_functional.py | 2 +- .../tests/postgres/test_psycopg_functional.py | 2 +- .../tests/pymysql/test_pymysql_functional.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py index d0261d2f63..46f63d3c66 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/mysql/test_mysql_functional.py @@ -82,7 +82,7 @@ def test_executemany(self): """Should create a child span for executemany """ with self._tracer.start_as_current_span("rootSpan"): - data = ["1", "2", "3"] + data = (("1",), ("2",), ("3",)) stmt = "INSERT INTO test (id) VALUES (%s)" self._cursor.executemany(stmt, data) self.validate_spans() diff --git a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py index 0a43926145..e0537ad293 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/postgres/test_psycopg_functional.py @@ -89,7 +89,7 @@ def test_executemany(self): """Should create a child span for executemany """ with self._tracer.start_as_current_span("rootSpan"): - data = ("1", "2", "3") + data = (("1",), ("2",), ("3",)) stmt = "INSERT INTO test (id) VALUES (%s)" self._cursor.executemany(stmt, data) self.validate_spans() diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py index 06507c4f35..15ec6dccca 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py @@ -81,7 +81,7 @@ def test_executemany(self): """Should create a child span for executemany """ with self._tracer.start_as_current_span("rootSpan"): - data = ["1", "2", "3"] + data = (("1",), ("2",), ("3",)) stmt = "INSERT INTO test (id) VALUES (%s)" self._cursor.executemany(stmt, data) self.validate_spans() From c074853a7cf9177847017c7656a2ccce461b191a Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 5 May 2020 10:59:29 -0600 Subject: [PATCH 0321/1517] flask: Add Flask Instrumentation fixes (#601) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reffactoring the Flask instrumentation to better enable auto-instrumentation. Co-authored-by: Mauricio Vásquez --- .../flask_example.py | 4 +- docs/getting-started.rst | 7 +- .../src/opentelemetry/ext/flask/__init__.py | 225 ++++++++++-------- ...test_flask_integration.py => base_test.py} | 57 ++--- ext/opentelemetry-ext-flask/tests/conftest.py | 25 -- .../tests/test_automatic.py | 64 +++++ .../tests/test_programmatic.py | 56 +++++ .../tests/test_instrumentor.py | 1 + 8 files changed, 279 insertions(+), 160 deletions(-) rename ext/opentelemetry-ext-flask/tests/{test_flask_integration.py => base_test.py} (73%) delete mode 100644 ext/opentelemetry-ext-flask/tests/conftest.py create mode 100644 ext/opentelemetry-ext-flask/tests/test_automatic.py create mode 100644 ext/opentelemetry-ext-flask/tests/test_programmatic.py diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 863d6f3389..8f44273b6e 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -34,15 +34,15 @@ trace.set_tracer_provider(TracerProvider()) opentelemetry.ext.requests.RequestsInstrumentor().instrument() -FlaskInstrumentor().instrument() trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) - app = flask.Flask(__name__) +FlaskInstrumentor().instrument_app(app) + @app.route("/") def hello(): diff --git a/docs/getting-started.rst b/docs/getting-started.rst index f25cf79b77..5d20fbe2c0 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -184,9 +184,6 @@ And let's write a small Flask application that sends an HTTP request, activating .. code-block:: python # flask_example.py - from opentelemetry.ext.flask import FlaskInstrumentor - FlaskInstrumentor().instrument() # This needs to be executed before importing Flask - import flask import requests @@ -195,6 +192,7 @@ And let's write a small Flask application that sends an HTTP request, activating from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor + from opentelemetry.ext.flask import FlaskInstrumentor trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( @@ -202,7 +200,8 @@ And let's write a small Flask application that sends an HTTP request, activating ) app = flask.Flask(__name__) - opentelemetry.ext.requests.RequestsInstrumentor().instrument() + FlaskInstrumentor().instrument_app(app) + opentelemetry.ext.http_requests.RequestsInstrumentor().instrument() @app.route("/") def hello(): diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index 1e936da115..040c8770c6 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -29,12 +29,13 @@ .. code-block:: python - from opentelemetry.ext.flask import FlaskInstrumentor - FlaskInstrumentor().instrument() # This needs to be executed before importing Flask from flask import Flask + from opentelemetry.ext.flask import FlaskInstrumentor app = Flask(__name__) + FlaskInstrumentor().instrument_app(app) + @app.route("/") def hello(): return "Hello!" @@ -46,7 +47,7 @@ def hello(): --- """ -import logging +from logging import getLogger import flask @@ -60,7 +61,7 @@ def hello(): time_ns, ) -logger = logging.getLogger(__name__) +_logger = getLogger(__name__) _ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key" _ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key" @@ -68,102 +69,104 @@ def hello(): _ENVIRON_TOKEN = "opentelemetry-flask.token" +def _rewrapped_app(wsgi_app): + def _wrapped_app(environ, start_response): + # We want to measure the time for route matching, etc. + # In theory, we could start the span here and use + # update_name later but that API is "highly discouraged" so + # we better avoid it. + environ[_ENVIRON_STARTTIME_KEY] = time_ns() + + def _start_response(status, response_headers, *args, **kwargs): + + if not _disable_trace(flask.request.url): + + span = flask.request.environ.get(_ENVIRON_SPAN_KEY) + + if span: + otel_wsgi.add_response_attributes( + span, status, response_headers + ) + else: + _logger.warning( + "Flask environ's OpenTelemetry span " + "missing at _start_response(%s)", + status, + ) + + return start_response(status, response_headers, *args, **kwargs) + + return wsgi_app(environ, _start_response) + + return _wrapped_app + + +def _before_request(): + if _disable_trace(flask.request.url): + return + + environ = flask.request.environ + span_name = flask.request.endpoint or otel_wsgi.get_default_span_name( + environ + ) + token = context.attach( + propagators.extract(otel_wsgi.get_header_from_environ, environ) + ) + + tracer = trace.get_tracer(__name__, __version__) + + attributes = otel_wsgi.collect_request_attributes(environ) + if flask.request.url_rule: + # For 404 that result from no route found, etc, we + # don't have a url_rule. + attributes["http.route"] = flask.request.url_rule.rule + span = tracer.start_span( + span_name, + kind=trace.SpanKind.SERVER, + attributes=attributes, + start_time=environ.get(_ENVIRON_STARTTIME_KEY), + ) + activation = tracer.use_span(span, end_on_exit=True) + activation.__enter__() + environ[_ENVIRON_ACTIVATION_KEY] = activation + environ[_ENVIRON_SPAN_KEY] = span + environ[_ENVIRON_TOKEN] = token + + +def _teardown_request(exc): + activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY) + if not activation: + _logger.warning( + "Flask environ's OpenTelemetry activation missing" + "at _teardown_flask_request(%s)", + exc, + ) + return + + if exc is None: + activation.__exit__(None, None, None) + else: + activation.__exit__( + type(exc), exc, getattr(exc, "__traceback__", None) + ) + context.detach(flask.request.environ.get(_ENVIRON_TOKEN)) + + class _InstrumentedFlask(flask.Flask): def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # Single use variable here to avoid recursion issues. - wsgi = self.wsgi_app - - def wrapped_app(environ, start_response): - # We want to measure the time for route matching, etc. - # In theory, we could start the span here and use - # update_name later but that API is "highly discouraged" so - # we better avoid it. - environ[_ENVIRON_STARTTIME_KEY] = time_ns() - - def _start_response(status, response_headers, *args, **kwargs): - if not _disable_trace(flask.request.url): - span = flask.request.environ.get(_ENVIRON_SPAN_KEY) - if span: - otel_wsgi.add_response_attributes( - span, status, response_headers - ) - else: - logger.warning( - "Flask environ's OpenTelemetry span " - "missing at _start_response(%s)", - status, - ) - - return start_response( - status, response_headers, *args, **kwargs - ) - - return wsgi(environ, _start_response) - - self.wsgi_app = wrapped_app - - @self.before_request - def _before_flask_request(): - # Do not trace if the url is excluded - if _disable_trace(flask.request.url): - return - environ = flask.request.environ - span_name = ( - flask.request.endpoint - or otel_wsgi.get_default_span_name(environ) - ) - token = context.attach( - propagators.extract(otel_wsgi.get_header_from_environ, environ) - ) + self._original_wsgi_ = self.wsgi_app + self.wsgi_app = _rewrapped_app(self.wsgi_app) - tracer = trace.get_tracer(__name__, __version__) - - attributes = otel_wsgi.collect_request_attributes(environ) - if flask.request.url_rule: - # For 404 that result from no route found, etc, we - # don't have a url_rule. - attributes["http.route"] = flask.request.url_rule.rule - span = tracer.start_span( - span_name, - kind=trace.SpanKind.SERVER, - attributes=attributes, - start_time=environ.get(_ENVIRON_STARTTIME_KEY), - ) - activation = tracer.use_span(span, end_on_exit=True) - activation.__enter__() - environ[_ENVIRON_ACTIVATION_KEY] = activation - environ[_ENVIRON_SPAN_KEY] = span - environ[_ENVIRON_TOKEN] = token - - @self.teardown_request - def _teardown_flask_request(exc): - # Not traced if the url is excluded - if _disable_trace(flask.request.url): - return - activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY) - if not activation: - logger.warning( - "Flask environ's OpenTelemetry activation missing" - "at _teardown_flask_request(%s)", - exc, - ) - return - - if exc is None: - activation.__exit__(None, None, None) - else: - activation.__exit__( - type(exc), exc, getattr(exc, "__traceback__", None) - ) - context.detach(flask.request.environ.get(_ENVIRON_TOKEN)) + self.before_request(_before_request) + self.teardown_request(_teardown_request) def _disable_trace(url): excluded_hosts = configuration.Configuration().FLASK_EXCLUDED_HOSTS excluded_paths = configuration.Configuration().FLASK_EXCLUDED_PATHS + if excluded_hosts: excluded_hosts = str.split(excluded_hosts, ",") if disable_tracing_hostname(url, excluded_hosts): @@ -176,18 +179,50 @@ def _disable_trace(url): class FlaskInstrumentor(BaseInstrumentor): - """A instrumentor for flask.Flask + # pylint: disable=protected-access,attribute-defined-outside-init + """An instrumentor for flask.Flask See `BaseInstrumentor` """ - def __init__(self): - super().__init__() - self._original_flask = None - def _instrument(self, **kwargs): self._original_flask = flask.Flask flask.Flask = _InstrumentedFlask + def instrument_app(self, app): # pylint: disable=no-self-use + if not hasattr(app, "_is_instrumented"): + app._is_instrumented = False + + if not app._is_instrumented: + app._original_wsgi_app = app.wsgi_app + app.wsgi_app = _rewrapped_app(app.wsgi_app) + + app.before_request(_before_request) + app.teardown_request(_teardown_request) + app._is_instrumented = True + else: + _logger.warning( + "Attempting to instrument Flask app while already instrumented" + ) + def _uninstrument(self, **kwargs): flask.Flask = self._original_flask + + def uninstrument_app(self, app): # pylint: disable=no-self-use + if not hasattr(app, "_is_instrumented"): + app._is_instrumented = False + + if app._is_instrumented: + app.wsgi_app = app._original_wsgi_app + + # FIXME add support for other Flask blueprints that are not None + app.before_request_funcs[None].remove(_before_request) + app.teardown_request_funcs[None].remove(_teardown_request) + del app._original_wsgi_app + + app._is_instrumented = False + else: + _logger.warning( + "Attempting to uninstrument Flask " + "app while already uninstrumented" + ) diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/base_test.py similarity index 73% rename from ext/opentelemetry-ext-flask/tests/test_flask_integration.py rename to ext/opentelemetry-ext-flask/tests/base_test.py index 1babfff2f5..7147afd719 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/base_test.py @@ -12,16 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from unittest.mock import patch -from flask import Flask, request +from flask import request from werkzeug.test import Client from werkzeug.wrappers import BaseResponse -from opentelemetry import trace as trace_api -from opentelemetry.configuration import Configuration -from opentelemetry.test.wsgitestutil import WsgiTestBase +from opentelemetry import trace def expected_attributes(override_attributes): @@ -42,36 +39,30 @@ def expected_attributes(override_attributes): return default_attributes -class TestFlaskIntegration(WsgiTestBase): - def setUp(self): - # No instrumentation code is here because it is present in the - # conftest.py file next to this file. - super().setUp() - Configuration._instance = None # pylint:disable=protected-access - Configuration.__slots__ = [] - self.app = Flask(__name__) - - def hello_endpoint(helloid): - if helloid == 500: - raise ValueError(":-(") - return "Hello: " + str(helloid) +class InstrumentationTest: + @staticmethod + def _hello_endpoint(helloid): + if helloid == 500: + raise ValueError(":-(") + return "Hello: " + str(helloid) + def _common_initialization(self): def excluded_endpoint(): return "excluded" def excluded2_endpoint(): return "excluded2" - self.app.route("/hello/")(hello_endpoint) + # pylint: disable=no-member + self.app.route("/hello/")(self._hello_endpoint) + self.app.route("/excluded/")(self._hello_endpoint) self.app.route("/excluded")(excluded_endpoint) self.app.route("/excluded2")(excluded2_endpoint) + # pylint: disable=attribute-defined-outside-init self.client = Client(self.app, BaseResponse) - def tearDown(self): - Configuration._instance = None # pylint:disable=protected-access - Configuration.__slots__ = [] - + # pylint: disable=no-member def test_only_strings_in_environ(self): """ Some WSGI servers (such as Gunicorn) expect keys in the environ object @@ -99,8 +90,8 @@ def test_simple(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "hello_endpoint") - self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) + self.assertEqual(span_list[0].name, "_hello_endpoint") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) def test_404(self): @@ -119,7 +110,7 @@ def test_404(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "/bye") - self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) def test_internal_error(self): @@ -136,14 +127,16 @@ def test_internal_error(self): resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "hello_endpoint") - self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) + self.assertEqual(span_list[0].name, "_hello_endpoint") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) @patch.dict( "os.environ", # type: ignore { - "OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_HOSTS": "http://localhost/excluded", + "OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_HOSTS": ( + "http://localhost/excluded" + ), "OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_PATHS": "excluded2", }, ) @@ -153,8 +146,4 @@ def test_excluded_path(self): self.client.get("/excluded2") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "hello_endpoint") - - -if __name__ == "__main__": - unittest.main() + self.assertEqual(span_list[0].name, "_hello_endpoint") diff --git a/ext/opentelemetry-ext-flask/tests/conftest.py b/ext/opentelemetry-ext-flask/tests/conftest.py deleted file mode 100644 index 8c0754f2c6..0000000000 --- a/ext/opentelemetry-ext-flask/tests/conftest.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from opentelemetry.ext.flask import FlaskInstrumentor - -_FLASK_INSTRUMENTOR = FlaskInstrumentor() - - -def pytest_sessionstart(session): # pylint: disable=unused-argument - _FLASK_INSTRUMENTOR.instrument() - - -def pytest_sessionfinish(session): # pylint: disable=unused-argument - _FLASK_INSTRUMENTOR.uninstrument() diff --git a/ext/opentelemetry-ext-flask/tests/test_automatic.py b/ext/opentelemetry-ext-flask/tests/test_automatic.py new file mode 100644 index 0000000000..04f43d6e64 --- /dev/null +++ b/ext/opentelemetry-ext-flask/tests/test_automatic.py @@ -0,0 +1,64 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import flask +from werkzeug.test import Client +from werkzeug.wrappers import BaseResponse + +from opentelemetry.configuration import Configuration +from opentelemetry.ext.flask import FlaskInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.test.wsgitestutil import WsgiTestBase + +# pylint: disable=import-error +from .base_test import InstrumentationTest + + +class TestAutomatic(InstrumentationTest, TestBase, WsgiTestBase): + def setUp(self): + super().setUp() + + Configuration._instance = None # pylint: disable=protected-access + Configuration.__slots__ = [] # pylint: disable=protected-access + FlaskInstrumentor().instrument() + + self.app = flask.Flask(__name__) + + self._common_initialization() + + def tearDown(self): + super().tearDown() + with self.disable_logging(): + FlaskInstrumentor().uninstrument() + + def test_uninstrument(self): + # pylint: disable=access-member-before-definition + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + FlaskInstrumentor().uninstrument() + self.app = flask.Flask(__name__) + + self.app.route("/hello/")(self._hello_endpoint) + # pylint: disable=attribute-defined-outside-init + self.client = Client(self.app, BaseResponse) + + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) diff --git a/ext/opentelemetry-ext-flask/tests/test_programmatic.py b/ext/opentelemetry-ext-flask/tests/test_programmatic.py new file mode 100644 index 0000000000..1075f808cb --- /dev/null +++ b/ext/opentelemetry-ext-flask/tests/test_programmatic.py @@ -0,0 +1,56 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from flask import Flask + +from opentelemetry.configuration import Configuration +from opentelemetry.ext.flask import FlaskInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.test.wsgitestutil import WsgiTestBase + +# pylint: disable=import-error +from .base_test import InstrumentationTest + + +class TestProgrammatic(InstrumentationTest, TestBase, WsgiTestBase): + def setUp(self): + super().setUp() + + Configuration._instance = None # pylint: disable=protected-access + Configuration.__slots__ = [] # pylint: disable=protected-access + self.app = Flask(__name__) + + FlaskInstrumentor().instrument_app(self.app) + + self._common_initialization() + + def tearDown(self): + super().tearDown() + with self.disable_logging(): + FlaskInstrumentor().uninstrument_app(self.app) + + def test_uninstrument(self): + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + FlaskInstrumentor().uninstrument_app(self.app) + + resp = self.client.get("/hello/123") + self.assertEqual(200, resp.status_code) + self.assertEqual([b"Hello: 123"], list(resp.response)) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) diff --git a/opentelemetry-auto-instrumentation/tests/test_instrumentor.py b/opentelemetry-auto-instrumentation/tests/test_instrumentor.py index 40e762230a..a768a40eb4 100644 --- a/opentelemetry-auto-instrumentation/tests/test_instrumentor.py +++ b/opentelemetry-auto-instrumentation/tests/test_instrumentor.py @@ -34,6 +34,7 @@ def test_protect(self): self.assertIs(instrumentor.uninstrument(), None) self.assertEqual(instrumentor.instrument(), "instrumented") + with self.assertLogs(level=WARNING): self.assertIs(instrumentor.instrument(), None) From 48ad6ad9ab2c31d7acbe5eeaf3cd5c337cecb5df Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 5 May 2020 11:07:59 -0700 Subject: [PATCH 0322/1517] docs: updating readme (#639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is making the readme consistent with some of the other SIG repos: - [js](https://github.com/open-telemetry/opentelemetry-js/blob/master/README.md) - [collector](https://github.com/open-telemetry/opentelemetry-collector) Co-authored-by: Mauricio Vásquez Co-authored-by: Yusuke Tsutsumi --- README.md | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0333853eec..6e47958fe1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,42 @@ -# OpenTelemetry Python -[![Gitter chat](https://img.shields.io/gitter/room/opentelemetry/opentelemetry-python)](https://gitter.im/open-telemetry/opentelemetry-python) -[![Build status](https://travis-ci.org/open-telemetry/opentelemetry-python.svg?branch=master)](https://travis-ci.org/open-telemetry/opentelemetry-python) +--- +

    mYC(BKw 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

    mYC(BKw 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